Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
37 changes: 19 additions & 18 deletions src/dashboard/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { getLogs, subscribeToLogs, unsubscribeFromLogs } from './logger.js';
import { getProxyConfig, getProxyConfigMasked, setGlobalProxy, setAccountProxy, removeProxy, getEffectiveProxy } from './proxy-config.js';
import { MODELS, MODEL_TIER_ACCESS as _TIER_TABLE, getTierModels as _getTierModels } from '../models.js';
import { windsurfLogin, refreshFirebaseToken, reRegisterWithCodeium } from './windsurf-login.js';
import { getModelAccessConfig, setModelAccessMode, setModelAccessList, addModelToList, removeModelFromList } from './model-access.js';
import { getModelAccessConfig, setModelAccessMode, setModelAccessList, addModelToList, removeModelFromList, setDefaultModel } from './model-access.js';
import { checkMessageRateLimit } from '../windsurf-api.js';
import { getNativeBridgeConfigStatus } from '../cascade-native-bridge.js';
import { getNativeBridgeStats } from '../native-bridge-stats.js';
Expand Down Expand Up @@ -1251,6 +1251,7 @@ export async function handleDashboardApi(method, subpath, body, req, res) {
if (subpath === '/model-access' && method === 'PUT') {
if (body.mode) setModelAccessMode(body.mode);
if (body.list) setModelAccessList(body.list);
if (body.defaultModel !== undefined) setDefaultModel(body.defaultModel);
return json(res, 200, { success: true, config: getModelAccessConfig() });
}

Expand Down Expand Up @@ -1556,7 +1557,7 @@ async function gitStatus() {
try {
await runGit(['fetch', '--quiet', 'origin']);
remote = (await runGit(['rev-parse', `origin/${branch}`])).trim();
} catch {}
} catch { }
const localMsg = (await runGit(['log', '-1', '--pretty=format:%s'])).trim();
const behind = remote && remote !== commit;
const remoteMsg = behind ? (await runGit(['log', '-1', '--pretty=format:%s', remote]).catch(() => '')).trim() : '';
Expand Down Expand Up @@ -1611,21 +1612,21 @@ async function testProxy({ host, port, username, password, type }) {

// TLS handshake + GET to verify the tunnel works
return new Promise((resolve, reject) => {
const tlsSock = tls.connect({ socket, servername: targetHost, rejectUnauthorized: false }, () => {
tlsSock.write(`GET / HTTP/1.1\r\nHost: ${targetHost}\r\nConnection: close\r\nUser-Agent: WindsurfAPI/ProxyTest\r\n\r\n`);
});
const chunks = [];
tlsSock.on('data', c => chunks.push(c));
tlsSock.on('end', () => {
const body = Buffer.concat(chunks).toString('utf-8');
const match = body.match(/\r\n\r\n([^\r\n]+)/);
const ip = match ? match[1].trim() : '';
tlsSock.destroy();
if (!ip || !/^\d+\.\d+\.\d+\.\d+$/.test(ip)) {
return reject(new Error('ERR_TLS_TUNNEL_ERROR'));
}
resolve({ egressIp: ip, type });
});
tlsSock.on('error', (err) => reject(new Error(`ERR_TLS_FAILED:${err.message}`)));
const tlsSock = tls.connect({ socket, servername: targetHost, rejectUnauthorized: false }, () => {
tlsSock.write(`GET / HTTP/1.1\r\nHost: ${targetHost}\r\nConnection: close\r\nUser-Agent: WindsurfAPI/ProxyTest\r\n\r\n`);
});
const chunks = [];
tlsSock.on('data', c => chunks.push(c));
tlsSock.on('end', () => {
const body = Buffer.concat(chunks).toString('utf-8');
const match = body.match(/\r\n\r\n([^\r\n]+)/);
const ip = match ? match[1].trim() : '';
tlsSock.destroy();
if (!ip || !/^\d+\.\d+\.\d+\.\d+$/.test(ip)) {
return reject(new Error('ERR_TLS_TUNNEL_ERROR'));
}
resolve({ egressIp: ip, type });
});
tlsSock.on('error', (err) => reject(new Error(`ERR_TLS_FAILED:${err.message}`)));
});
}
16 changes: 15 additions & 1 deletion src/dashboard/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2013,6 +2013,11 @@ <h1 class="page-title" data-i18n="page.models">模型控制</h1>
<label><input type="radio" name="model-mode" value="allowlist" onchange="App.setModelMode(this.value)"><span data-i18n="model.mode.allowlist">白名单</span></label>
<label><input type="radio" name="model-mode" value="blocklist" onchange="App.setModelMode(this.value)"><span data-i18n="model.mode.blocklist">黑名单</span></label>
</div>
<div class="field mt-4">
<label class="field-label" data-i18n="field.defaultModel.label">默认模型</label>
<input id="default-model-input" class="input" data-i18n-placeholder="field.defaultModel.placeholder" placeholder="白名单外的模型将使用此模型,留空则拒绝" oninput="App.handleDefaultModelInput()">
<div class="field-hint" data-i18n="field.defaultModel.hint">当请求的模型不在白名单中时,将使用此模型代替。留空则拒绝白名单外的请求。</div>
</div>
</div>
</div>

Expand Down Expand Up @@ -4598,6 +4603,7 @@ <h3 data-i18n="login.title">控制台登录</h3>
this.allModels = modelData.models || [];
this.modelAccessConfig = accessData.mode ? accessData : { mode: 'all', list: [] };
document.querySelector(`input[name="model-mode"][value="${this.modelAccessConfig.mode}"]`).checked = true;
document.getElementById('default-model-input').value = this.modelAccessConfig.defaultModel || '';
this.updateModelListUI();
},

Expand Down Expand Up @@ -4653,12 +4659,20 @@ <h3 data-i18n="login.title">控制台登录</h3>
},

async setModelMode(mode) {
await this.api('PUT', '/model-access', { mode });
const defaultModel = document.getElementById('default-model-input').value.trim();
await this.api('PUT', '/model-access', { mode, defaultModel });
this.modelAccessConfig.mode = mode;
this.modelAccessConfig.defaultModel = defaultModel;
this.updateModelListUI();
this.toast(I18n.t('toast.modeUpdated'));
},

async handleDefaultModelInput() {
const defaultModel = document.getElementById('default-model-input').value.trim();
await this.api('PUT', '/model-access', { defaultModel });
this.modelAccessConfig.defaultModel = defaultModel;
},

async toggleModelInList(modelId) {
const idx = this.modelAccessConfig.list.indexOf(modelId);
if (idx > -1) {
Expand Down
10 changes: 10 additions & 0 deletions src/dashboard/model-access.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const ACCESS_FILE = join(config.dataDir, 'model-access.json');
const _config = {
mode: 'all',
list: [], // model IDs in the list
defaultModel: '', // default model to use if the requested one is blocked or not specified; optional, for UI purposes
};

// Load
Expand Down Expand Up @@ -60,6 +61,15 @@ export function removeModelFromList(modelId) {
save();
}

export function setDefaultModel(modelId) {
_config.defaultModel = modelId || '';
save();
}

export function getDefaultModel() {
return _config.defaultModel || '';
}

/**
* Some models in the catalog are simply the reasoning-mode variant of a
* base model (claude-opus-4.6 vs claude-opus-4.6-thinking). For
Expand Down
Loading