Skip to content

Commit 29b14fc

Browse files
authored
feat(web-ui): improve config panel layout and validation (#181)
* style(web-ui): align OpenClaw config panel layout * fix(config): require model when adding provider * fix(config): expose Claude model field in config modal * test(config): cover model usage after provider edits * fix(config): preserve legacy add-provider model fallback * fix(web-ui): show inline Claude config validation * fix(config): require provider credential fields * fix(config): allow external Claude credentials without key
1 parent 5769ed4 commit 29b14fc

20 files changed

Lines changed: 1132 additions & 249 deletions

cli.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2081,12 +2081,22 @@ function addProviderToConfig(params = {}) {
20812081
const name = typeof params.name === 'string' ? params.name.trim() : '';
20822082
const url = typeof params.url === 'string' ? params.url.trim() : '';
20832083
const key = typeof params.key === 'string' ? params.key.trim() : '';
2084+
const requireModel = !!params.requireModel;
2085+
const fallbackModel = (() => {
2086+
if (requireModel) return '';
2087+
const list = readModels();
2088+
return Array.isArray(list) && typeof list[0] === 'string' ? list[0].trim() : '';
2089+
})();
2090+
const model = typeof params.model === 'string' && params.model.trim()
2091+
? params.model.trim()
2092+
: fallbackModel;
20842093
const useTransform = !!params.useTransform;
20852094
const allowManaged = !!params.allowManaged;
20862095
const normalizedUrl = normalizeBaseUrl(url);
20872096

20882097
if (!name) return { error: '名称不能为空' };
20892098
if (!url) return { error: 'URL 不能为空' };
2099+
if (!model) return { error: '模型名称不能为空' };
20902100
if (!isValidProviderName(name)) {
20912101
return { error: '名称仅支持字母/数字/._-' };
20922102
}
@@ -2163,6 +2173,7 @@ function addProviderToConfig(params = {}) {
21632173
`wire_api = "responses"`,
21642174
`requires_openai_auth = ${requiresOpenaiAuth ? 'true' : 'false'}`,
21652175
`preferred_auth_method = "${safeKey}"`,
2176+
`models = [{ id = "${escapeTomlBasicString(model)}", name = "${escapeTomlBasicString(model)}" }]`,
21662177
...extraLines,
21672178
`request_max_retries = 4`,
21682179
`stream_max_retries = 10`,
@@ -2173,6 +2184,13 @@ function addProviderToConfig(params = {}) {
21732184

21742185
try {
21752186
writeConfig(newContent);
2187+
const models = readModels();
2188+
if (!models.includes(model)) {
2189+
writeModels([...models, model]);
2190+
}
2191+
const currentModels = readCurrentModels();
2192+
currentModels[name] = model;
2193+
writeCurrentModels(currentModels);
21762194
} catch (e) {
21772195
return { error: `写入配置失败: ${e.message}` };
21782196
}
@@ -10866,7 +10884,7 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
1086610884
result = buildConfigTemplateDiff(params || {});
1086710885
break;
1086810886
case 'add-provider':
10869-
result = addProviderToConfig(params || {});
10887+
result = addProviderToConfig({ ...(params || {}), requireModel: true });
1087010888
break;
1087110889
case 'update-provider':
1087210890
result = updateProviderInConfig(params || {});

tests/e2e/test-claude-proxy.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,8 @@ module.exports = async function testClaudeProxy(ctx) {
123123
const addProvider = await api('add-provider', {
124124
name: 'claude-proxy-e2e',
125125
url: upstreamUrl,
126-
key: 'sk-claude-upstream'
126+
key: 'sk-claude-upstream',
127+
model: 'claude-proxy-e2e-model'
127128
});
128129
assert(addProvider.success === true, 'add-provider(claude-proxy-e2e) failed');
129130

tests/e2e/test-config.js

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ preferred_auth_method = "shadow-key"
292292

293293
// ========== Add Provider Tests ==========
294294
const addProviderInputUrl = `${mockProviderUrl}/`;
295-
const addProvider = await api('add-provider', { name: 'e2e-api', url: addProviderInputUrl, key: 'sk-e2e-api' });
295+
const addProvider = await api('add-provider', { name: 'e2e-api', url: addProviderInputUrl, key: 'sk-e2e-api', model: 'gpt-e2e-api' });
296296
assert(addProvider.success === true, 'add-provider failed');
297297

298298
const apiListAfterAdd = await api('list');
@@ -301,6 +301,23 @@ preferred_auth_method = "shadow-key"
301301
: null;
302302
assert(addedProvider, 'add-provider not reflected in list');
303303
assert(addedProvider.url === mockProviderUrl, 'add-provider should persist normalized provider url');
304+
assert(
305+
Array.isArray(addedProvider.models) && addedProvider.models.some((model) => model && model.id === 'gpt-e2e-api'),
306+
'add-provider should expose the entered model on the new provider'
307+
);
308+
const exportAfterAdd = await api('export-config', { includeKeys: true });
309+
assert(exportAfterAdd.data && exportAfterAdd.data.currentModels && exportAfterAdd.data.currentModels['e2e-api'] === 'gpt-e2e-api', 'add-provider should persist entered model as provider current model');
310+
assert(exportAfterAdd.data && Array.isArray(exportAfterAdd.data.models) && exportAfterAdd.data.models.includes('gpt-e2e-api'), 'add-provider should persist entered model in model list');
311+
const addedProviderTemplate = await api('get-config-template', {
312+
provider: 'e2e-api',
313+
model: exportAfterAdd.data.currentModels['e2e-api']
314+
});
315+
assert(!addedProviderTemplate.error, `get-config-template should accept add-provider model: ${addedProviderTemplate.error || ''}`);
316+
assert(addedProviderTemplate.template.includes('model_provider = "e2e-api"'), 'entered provider should be usable in generated config template');
317+
assert(addedProviderTemplate.template.includes('model = "gpt-e2e-api"'), 'entered model should be used in generated config template');
318+
319+
const addProviderMissingModel = await api('add-provider', { name: 'test-empty-model', url: mockProviderUrl, key: 'sk-empty-model', model: ' ' });
320+
assert(addProviderMissingModel.error, 'add-provider should reject empty model');
304321

305322
const addProviderEmptyName = await api('add-provider', { name: '', url: mockProviderUrl });
306323
assert(addProviderEmptyName.error, 'add-provider should reject empty name');
@@ -318,6 +335,19 @@ preferred_auth_method = "shadow-key"
318335
assert(addProviderInvalidName.error, 'add-provider should reject invalid provider name');
319336
const addProviderInvalidUrl = await api('add-provider', { name: 'bad-url', url: 'not-a-url' });
320337
assert(addProviderInvalidUrl.error, 'add-provider should reject invalid provider url');
338+
339+
const cliAddBridgeNoModel = runSync(node, [cliPath, 'add', 'legacy-bridge-no-model', mockProviderUrl, 'sk-legacy-bridge', '--bridge', 'openai'], { env });
340+
assert(
341+
cliAddBridgeNoModel.status === 0,
342+
`CLI add --bridge without model should remain backward compatible: ${cliAddBridgeNoModel.stderr || cliAddBridgeNoModel.stdout}`
343+
);
344+
const exportAfterLegacyBridgeAdd = await api('export-config', { includeKeys: true });
345+
assert(
346+
exportAfterLegacyBridgeAdd.data
347+
&& exportAfterLegacyBridgeAdd.data.currentModels
348+
&& exportAfterLegacyBridgeAdd.data.currentModels['legacy-bridge-no-model'],
349+
'legacy CLI add --bridge should initialize a fallback current model'
350+
);
321351
const cliAddInvalidUrl = runSync(node, [cliPath, 'add', 'cli-bad-url', 'not-a-url'], { env });
322352
assert(cliAddInvalidUrl.status !== 0, 'cli add should reject invalid provider url');
323353
const apiListAfterInvalidName = await api('list');
@@ -428,7 +458,8 @@ preferred_auth_method = "shadow-key"
428458
const legacyAddDup = await legacyApi('add-provider', {
429459
name: 'foo.bar',
430460
url: 'https://dup.example.com/v1',
431-
key: 'sk-dup'
461+
key: 'sk-dup',
462+
model: 'gpt-dup'
432463
});
433464
assert(legacyAddDup.error, 'legacy duplicate add-provider should be rejected');
434465
const legacyConfigAfterDup = fs.readFileSync(legacyConfigPath, 'utf-8');

tests/e2e/test-messages.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ module.exports = async function testMessages(ctx) {
298298
assert(resetConfigResult.backup !== undefined || resetConfigResult.success !== undefined || resetConfigResult.error, 'reset-config should return backup/success or error');
299299

300300
// ========== 添加提供商测试 ==========
301-
const addProviderResult = await api('add-provider', { name: 'test-duplicate', url: 'http://test.com', key: 'test-key' });
301+
const addProviderResult = await api('add-provider', { name: 'test-duplicate', url: 'http://test.com', key: 'test-key', model: 'gpt-test-duplicate' });
302302
assert(addProviderResult.success !== undefined || addProviderResult.error, 'add-provider should return success or error');
303303

304304
const addProviderDupName = await api('add-provider', { name: '', url: 'http://test.com' });

0 commit comments

Comments
 (0)