Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 50 additions & 10 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -9502,9 +9502,39 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
});
req.end(payload, 'utf-8');
};
const probeWebUiAssetReadiness = (callback) => {
const requestOptions = {
hostname: openHost,
port,
path: '/web-ui/app.js',
method: 'GET'
};
let settled = false;
const finish = (ready) => {
if (settled) return;
settled = true;
callback(ready);
};
const req = http.request(requestOptions, (probeRes) => {
if (typeof probeRes.resume === 'function') {
probeRes.resume();
}
probeRes.on('end', () => {
finish(probeRes.statusCode === 200);
});
});
req.on('error', () => finish(false));
req.setTimeout(1000, () => {
try { req.destroy(); } catch (_) {}
finish(false);
});
req.end();
};
const openBrowserAfterReady = (url) => {
const maxAttempts = 40;
const retryDelayMs = 150;
// Some environments may start the listener before the bundled web-ui assets
// are ready for the first browser hit; wait until both API and assets respond.
const maxAttempts = 120;
const retryDelayMs = 200;
let finished = false;

const finish = (ready) => {
Expand Down Expand Up @@ -9539,16 +9569,26 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
}
};
const scheduleProbe = (attempt) => {
probeWebUiReadiness((ready) => {
if (ready) {
finish(true);
return;
}
if (attempt >= maxAttempts) {
finish(false);
probeWebUiReadiness((apiReady) => {
if (!apiReady) {
if (attempt >= maxAttempts) {
finish(false);
return;
}
setTimeout(() => scheduleProbe(attempt + 1), retryDelayMs);
return;
}
setTimeout(() => scheduleProbe(attempt + 1), retryDelayMs);
probeWebUiAssetReadiness((assetReady) => {
if (assetReady) {
finish(true);
return;
}
if (attempt >= maxAttempts) {
finish(false);
return;
}
setTimeout(() => scheduleProbe(attempt + 1), retryDelayMs);
});
});
};

Expand Down
31 changes: 28 additions & 3 deletions tests/unit/web-run-host.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -804,7 +804,9 @@ test('createWebServer returns 500 when fallback bundled html generation throws',
test('createWebServer waits for api readiness probe before auto-opening the browser', () => {
let listenCallback = null;
let probeRequest = null;
let assetRequest = null;
let probeEndHandler = null;
let assetEndHandler = null;
let probePayload = '';
const spawnCalls = [];
const createWebServer = instantiateFunction(createWebServerSource, 'createWebServer', {
Expand All @@ -827,7 +829,11 @@ test('createWebServer waits for api readiness probe before auto-opening the brow
};
},
request(options, callback) {
probeRequest = { options, callback };
if (options && options.method === 'POST') {
probeRequest = { options, callback };
} else {
assetRequest = { options, callback };
}
return {
on() {},
setTimeout() {},
Expand Down Expand Up @@ -921,6 +927,24 @@ test('createWebServer waits for api readiness probe before auto-opening the brow
assert.deepStrictEqual(spawnCalls, []);

probeEndHandler();
assert.ok(assetRequest, 'should probe the web ui assets before auto-open');
assert.strictEqual(assetRequest.options.hostname, '127.0.0.1');
assert.strictEqual(assetRequest.options.port, 3737);
assert.strictEqual(assetRequest.options.path, '/web-ui/app.js');
assert.strictEqual(assetRequest.options.method, 'GET');

assetRequest.callback({
statusCode: 200,
resume() {},
on(event, handler) {
if (event === 'end') {
assetEndHandler = handler;
}
}
});
assert.deepStrictEqual(spawnCalls, []);

assetEndHandler();
assert.deepStrictEqual(spawnCalls, [{
command: 'cmd',
args: ['/c', 'start', '', 'http://127.0.0.1:3737'],
Expand Down Expand Up @@ -1172,7 +1196,8 @@ test('createWebServer retries readiness probe failures before auto-opening the b
Buffer
});

requestOutcomes.push('error', 200);
// first api probe fails, second api probe succeeds, asset probe succeeds
requestOutcomes.push('error', 200, 200);
createWebServer({
htmlPath: '/repo/web-ui/index.html',
assetsDir: '/repo/res',
Expand All @@ -1185,7 +1210,7 @@ test('createWebServer retries readiness probe failures before auto-opening the b
listenCallback();
assert.deepStrictEqual(spawnCalls, []);
assert.strictEqual(timers.length, 1);
assert.strictEqual(timers[0].ms, 150);
assert.strictEqual(timers[0].ms, 200);

timers[0].callback();
assert.deepStrictEqual(spawnCalls, [{
Expand Down
26 changes: 26 additions & 0 deletions web-ui/partials/index/panel-config-claude.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,32 @@
默认应用到 <code>~/.claude/settings.json</code>。
</div>

<div class="selector-section">
<div class="selector-header">
<span class="selector-title">预设供应商</span>
</div>
<div class="btn-group" style="flex-wrap: wrap; gap: 8px;">
<button type="button" class="btn-mini" @click="closeClaudeConfigModal(); showClaudeConfigModal = true">自定义配置</button>
<button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Claude Official'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.anthropic.com'; newClaudeConfig.model = 'claude-sonnet-4'; showClaudeConfigModal = true">Claude Official</button>
<button type="button" class="btn-mini" @click="newClaudeConfig.name = 'DeepSeek'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.deepseek.com/anthropic'; newClaudeConfig.model = 'DeepSeek-V3.2'; showClaudeConfigModal = true">DeepSeek</button>
<button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Zhipu GLM'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://open.bigmodel.cn/api/anthropic'; newClaudeConfig.model = 'glm-5'; showClaudeConfigModal = true">Zhipu GLM</button>
<button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Z.ai GLM'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.z.ai/api/anthropic'; newClaudeConfig.model = 'glm-5'; showClaudeConfigModal = true">Z.ai GLM</button>
<button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Qwen Coder'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://coding.dashscope.aliyuncs.com/apps/anthropic'; newClaudeConfig.model = 'qwen3-coder'; showClaudeConfigModal = true">Qwen Coder</button>
<button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Kimi k2'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.moonshot.cn/anthropic'; newClaudeConfig.model = 'kimi-k2.5'; showClaudeConfigModal = true">Kimi k2</button>
<button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Kimi For Coding'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.kimi.com/coding/'; newClaudeConfig.model = 'kimi-k2.5'; showClaudeConfigModal = true">Kimi For Coding</button>
<button type="button" class="btn-mini" @click="newClaudeConfig.name = 'KAT-Coder'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://vanchin.streamlake.ai/api/gateway/v1/endpoints/${ENDPOINT_ID}/claude-code-proxy'; newClaudeConfig.model = 'KAT-Coder-Pro V1'; showClaudeConfigModal = true">KAT-Coder</button>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify whether ENDPOINT_ID has any replacement flow or validation elsewhere.
rg -n -C3 '\$\{ENDPOINT_ID\}|ENDPOINT_ID|baseUrl' web-ui
rg -n -C5 'addClaudeConfig\(|newClaudeConfig\.baseUrl|showMessage' web-ui/modules/app.methods.claude-config.mjs

Repository: SakuraByteCore/codexmate

Length of output: 37980


🏁 Script executed:

# Verify if there is any client-side URL validation in the modal save handler
rg -n 'baseUrl.*\$\{|baseUrl.*placeholder|validateUrl|validateBaseUrl' web-ui/modules/app.methods.claude-config.mjs web-ui/partials/index/modals-basic.html

Repository: SakuraByteCore/codexmate

Length of output: 421


🏁 Script executed:

# Check for any baseUrl validation in addClaudeConfig and saveAndApplyConfig methods
rg -A 20 'async saveAndApplyConfig\(\)|addClaudeConfig\(\)' web-ui/modules/app.methods.claude-config.mjs

Repository: SakuraByteCore/codexmate

Length of output: 1836


Add a UI hint on KAT-Coder button and validate unresolved URL placeholders before save.

Line 32 stores a literal ${ENDPOINT_ID} placeholder in the baseUrl without any substitution logic or validation. Users can save this non-routable URL by clicking the quick-config button and immediately confirming in addClaudeConfig(), which only validates the configuration name—not the baseUrl itself. Update the button text to hint that this field requires manual replacement, and add baseUrl validation to addClaudeConfig() to prevent saving configs with unresolved placeholders.

🛠️ Suggested approach

Change the button text to indicate the placeholder requires replacement:

- <button type="button" class="btn-mini" `@click`="newClaudeConfig.name = 'KAT-Coder'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://vanchin.streamlake.ai/api/gateway/v1/endpoints/${ENDPOINT_ID}/claude-code-proxy'; newClaudeConfig.model = 'KAT-Coder-Pro V1'; showClaudeConfigModal = true">KAT-Coder</button>
+ <button type="button" class="btn-mini" `@click`="newClaudeConfig.name = 'KAT-Coder'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://vanchin.streamlake.ai/api/gateway/v1/endpoints/<ENDPOINT_ID>/claude-code-proxy'; newClaudeConfig.model = 'KAT-Coder-Pro V1'; showClaudeConfigModal = true">KAT-Coder(需替换 ENDPOINT_ID)</button>

Additionally, add validation in addClaudeConfig() to check for unresolved placeholders (patterns like ${...}, <...>, etc.) and reject saves with a user-facing error.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/panel-config-claude.html` at line 32, Update the
KAT-Coder quick-config button label to indicate the baseUrl contains a
placeholder that must be replaced (e.g., "KAT-Coder (replace ${ENDPOINT_ID})")
and add validation inside addClaudeConfig() to reject configs whose
newClaudeConfig.baseUrl contains unresolved placeholders (detect patterns like
`${...}` or `<...>` with a simple regex) by showing a user-facing error and
preventing the save; reference newClaudeConfig.baseUrl and addClaudeConfig()
when implementing the check and error display.

<button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Longcat'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.longcat.chat/anthropic'; newClaudeConfig.model = 'LongCat-Flash-Chat'; showClaudeConfigModal = true">Longcat</button>
<button type="button" class="btn-mini" @click="newClaudeConfig.name = 'MiniMax'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.minimaxi.com/anthropic'; newClaudeConfig.model = 'MiniMax-M2.7'; showClaudeConfigModal = true">MiniMax</button>
<button type="button" class="btn-mini" @click="newClaudeConfig.name = 'MiniMax en'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.minimax.io/anthropic'; newClaudeConfig.model = 'MiniMax-M2.7'; showClaudeConfigModal = true">MiniMax en</button>
<button type="button" class="btn-mini" @click="newClaudeConfig.name = 'DouBaoSeed'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://ark.cn-beijing.volces.com/api/coding'; newClaudeConfig.model = 'doubao-seed-2-0-code-preview-latest'; showClaudeConfigModal = true">DouBaoSeed</button>
<button type="button" class="btn-mini" @click="newClaudeConfig.name = 'BaiLing'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.tbox.cn/api/anthropic'; newClaudeConfig.model = 'Ling-2.5-1T'; showClaudeConfigModal = true">BaiLing</button>
<button type="button" class="btn-mini" @click="newClaudeConfig.name = 'ModelScope'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api-inference.modelscope.cn'; newClaudeConfig.model = 'ZhipuAI/GLM-5'; showClaudeConfigModal = true">ModelScope</button>
<button type="button" class="btn-mini" @click="newClaudeConfig.name = 'AiHubMix'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://aihubmix.com'; newClaudeConfig.model = 'glm-4.7'; showClaudeConfigModal = true">AiHubMix</button>
<button type="button" class="btn-mini" @click="newClaudeConfig.name = 'DMXAPI'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://www.dmxapi.cn'; newClaudeConfig.model = 'glm-4.7'; showClaudeConfigModal = true">DMXAPI</button>
<button type="button" class="btn-mini" @click="newClaudeConfig.name = 'PackyCode'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://www.packyapi.com'; newClaudeConfig.model = 'glm-4.7'; showClaudeConfigModal = true">PackyCode</button>
</div>
</div>

<div class="selector-section">
<div class="selector-header">
<span class="selector-title">模型</span>
Expand Down
Loading