Skip to content
Merged
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
6 changes: 3 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ jobs:
# Verify the symlink was created
pkg-config --exists webkit2gtk-4.0 && echo "✓ webkit2gtk-4.0 found" || echo "✗ webkit2gtk-4.0 NOT found"
pkg-config --modversion webkit2gtk-4.0 || true
go install github.com/wailsapp/wails/v2/cmd/wails@latest
go install github.com/wailsapp/wails/v2/cmd/wails@v2.11.0

- name: Install Linux dependencies (cached)
if: matrix.platform == 'linux' && steps.cache-wails.outputs.cache-hit == 'true'
Expand All @@ -107,12 +107,12 @@ jobs:
- name: Install Wails (macOS)
if: matrix.platform == 'darwin' && steps.cache-wails.outputs.cache-hit != 'true'
run: |
go install github.com/wailsapp/wails/v2/cmd/wails@latest
go install github.com/wailsapp/wails/v2/cmd/wails@v2.11.0

- name: Install Wails (Windows)
if: matrix.platform == 'windows' && steps.cache-wails.outputs.cache-hit != 'true'
run: |
go install github.com/wailsapp/wails/v2/cmd/wails@latest
go install github.com/wailsapp/wails/v2/cmd/wails@v2.11.0
shell: pwsh

- name: Build application (Linux/macOS)
Expand Down
5 changes: 4 additions & 1 deletion cmd/desktop/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,10 @@ func (a *App) SwitchToEndpoint(endpointName string) error {
}
func (a *App) TestEndpoint(index int) string { return a.endpoint.TestEndpoint(index) }
func (a *App) TestEndpointLight(index int) string { return a.endpoint.TestEndpointLight(index) }
func (a *App) TestAllEndpointsZeroCost() string { return a.endpoint.TestAllEndpointsZeroCost() }
func (a *App) TestEndpointLightWithModel(index int, model string) string {
return a.endpoint.TestEndpointLightWithModel(index, model)
}
func (a *App) TestAllEndpointsZeroCost() string { return a.endpoint.TestAllEndpointsZeroCost() }
func (a *App) FetchModels(apiUrl, apiKey, transformer string) string {
return a.endpoint.FetchModels(apiUrl, apiKey, transformer)
}
Expand Down
4 changes: 4 additions & 0 deletions cmd/desktop/frontend/src/i18n/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ export default {
transformerHelp: 'Select the API format for this endpoint',
model: 'Model',
modelPlaceholder: 'e.g., claude-sonnet-4-5-20250929',
testModel: 'Select Test Model',
testModelHelp: 'This model is used only for this test and will not be saved or affect normal requests.',
testModelRequired: 'Enter a model to test',
modelHelp: 'Optional: Override the model specified in requests',
modelHelpClaude: 'Optional: Override the model specified in requests',
modelHelpOpenAI: 'Optional: Override the OpenAI model specified in requests',
Expand All @@ -118,6 +121,7 @@ export default {
confirmDelete: 'Are you sure you want to delete endpoint "{name}"?',
deleteFailed: 'Failed to delete: {error}',
fetchModels: 'Fetch Models',
selectModel: 'Select Model',
fetchModelsBtn: 'Fetch',
fetchModelsNoUrl: 'Please enter API URL first',
fetchModelsNoKey: 'Please enter API Key first',
Expand Down
4 changes: 4 additions & 0 deletions cmd/desktop/frontend/src/i18n/zh-CN.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ export default {
transformerHelp: '选择此端点的 API 格式',
model: '模型',
modelPlaceholder: '例如:claude-sonnet-4-5-20250929',
testModel: '选择测试模型',
testModelHelp: '该模型仅用于本次测试,不会保存或影响正式请求。',
testModelRequired: '请输入测试模型',
modelHelp: '可选:覆盖请求中指定的模型',
modelHelpClaude: '可选:覆盖请求中指定的模型',
modelHelpOpenAI: '可选:覆盖请求中指定的 OpenAI 模型',
Expand All @@ -118,6 +121,7 @@ export default {
confirmDelete: '确认删除端点 "{name}" 吗?',
deleteFailed: '删除失败:{error}',
fetchModels: '获取模型列表',
selectModel: '选择模型',
fetchModelsBtn: '检测',
fetchModelsNoUrl: '请先填写 API 地址',
fetchModelsNoKey: '请先填写 API 密钥',
Expand Down
4 changes: 2 additions & 2 deletions cmd/desktop/frontend/src/modules/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ export async function testEndpoint(index) {
return JSON.parse(resultStr);
}

export async function testEndpointLight(index) {
const resultStr = await window.go.main.App.TestEndpointLight(index);
export async function testEndpointLight(index, model = '') {
const resultStr = await window.go.main.App.TestEndpointLightWithModel(index, model);
return JSON.parse(resultStr);
}

Expand Down
2 changes: 2 additions & 0 deletions cmd/desktop/frontend/src/modules/endpoints.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ export async function renderEndpoints(endpoints) {
item.className = 'endpoint-item';
item.dataset.name = ep.name;
item.dataset.index = index;
item.dataset.model = model;

// 筛选激活时禁用拖拽
if (isFiltered) {
Expand Down Expand Up @@ -1615,6 +1616,7 @@ function renderCompactView(sortedEndpoints, container, currentEndpointName, isFi
item.className = 'endpoint-item-compact';
item.dataset.name = ep.name;
item.dataset.index = index;
item.dataset.model = model;

// 筛选激活时禁用拖拽
if (isFiltered) {
Expand Down
134 changes: 130 additions & 4 deletions cmd/desktop/frontend/src/modules/modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -630,13 +630,139 @@ function isTestNotSupported(statusCode, message) {
return false;
}

function requestTestModel(endpoint = {}) {
const configuredModel = endpoint.model?.trim() || '';
const overlay = document.createElement('div');
overlay.className = 'modal active';
overlay.innerHTML = `
<div class="modal-content" role="dialog" aria-modal="true" aria-labelledby="testModelTitle">
<div class="modal-header">
<h2 id="testModelTitle">${escapeHtml(t('modal.testModel'))}</h2>
</div>
<form>
<div class="modal-body">
<div class="form-group">
<label for="temporaryTestModel">${escapeHtml(t('modal.model'))}</label>
<div class="model-input-wrapper">
<input class="form-input" id="temporaryTestModel" name="model" type="text" placeholder="${escapeHtml(t('modal.modelPlaceholder'))}" required>
<button class="btn btn-secondary test-model-fetch" type="button">${escapeHtml(t('modal.fetchModels'))}</button>
</div>
<select class="form-input test-model-select" aria-label="${escapeHtml(t('modal.selectModel'))}" hidden></select>
<small>${escapeHtml(t('modal.testModelHelp'))}</small>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary test-model-cancel" type="button">${escapeHtml(t('common.cancel'))}</button>
<button class="btn btn-primary" type="submit">${escapeHtml(t('endpoints.test'))}</button>
</div>
</form>
</div>
`;
document.body.appendChild(overlay);

return new Promise(resolve => {
let settled = false;
const form = overlay.querySelector('form');
const input = overlay.querySelector('#temporaryTestModel');
const fetchButton = overlay.querySelector('.test-model-fetch');
const modelSelect = overlay.querySelector('.test-model-select');
input.value = configuredModel;
const finish = value => {
if (settled) {
return;
}
settled = true;
overlay.remove();
resolve(value);
};

fetchButton.addEventListener('click', async () => {
fetchButton.disabled = true;
fetchButton.textContent = '⏳';
try {
const resultStr = await window.go.main.App.FetchModels(endpoint.apiUrl, endpoint.apiKey, endpoint.transformer);
if (!overlay.isConnected) {
return;
}
const result = JSON.parse(resultStr);
const models = result.success && Array.isArray(result.models) ? result.models : [];
if (models.length === 0) {
showNotification(t('modal.fetchModelsEmpty'), 'error');
return;
}
modelSelect.replaceChildren();
const placeholder = document.createElement('option');
placeholder.value = '';
placeholder.textContent = t('modal.selectModel');
modelSelect.appendChild(placeholder);
models.forEach(model => {
const option = document.createElement('option');
option.value = String(model);
option.textContent = String(model);
modelSelect.appendChild(option);
});
modelSelect.value = models.includes(input.value) ? input.value : '';
modelSelect.hidden = false;
showNotification(t('modal.fetchModelsSuccess').replace('{count}', models.length), 'success');
} catch (error) {
if (overlay.isConnected) {
showNotification(`${t('modal.fetchModelsFailed')}: ${error}`, 'error');
}
} finally {
if (fetchButton.isConnected) {
fetchButton.disabled = false;
fetchButton.textContent = t('modal.fetchModels');
}
}
});
modelSelect.addEventListener('change', () => {
if (modelSelect.value) {
input.value = modelSelect.value;
input.setCustomValidity('');
}
});
form.addEventListener('submit', event => {
event.preventDefault();
const value = input.value.trim();
if (!value) {
input.setCustomValidity(t('modal.testModelRequired'));
input.reportValidity();
return;
}
finish(value);
});
input.addEventListener('input', () => input.setCustomValidity(''));
overlay.querySelector('.test-model-cancel').addEventListener('click', () => finish(null));
overlay.addEventListener('mousedown', event => {
if (event.target === overlay) {
finish(null);
}
});
overlay.addEventListener('keydown', event => {
if (event.key === 'Escape') {
event.preventDefault();
finish(null);
}
});
requestAnimationFrame(() => input.focus());
});
}

// Test Result Modal
export async function testEndpointHandler(index, buttonElement) {
setTestState(buttonElement, index);

// 获取端点名称用于保存测试状态(兼容详情视图和简洁视图)
const endpointItem = buttonElement.closest('.endpoint-item') || buttonElement.closest('.endpoint-item-compact');
const endpointName = endpointItem ? endpointItem.dataset.name : null;
const endpoint = {
...(window.config?.endpoints?.[index] || {}),
model: endpointItem?.dataset.model || ''
};
const testModel = await requestTestModel(endpoint);
if (testModel == null) {
return;
}

setTestState(buttonElement, index);

// 简洁视图:同时更新 moreBtn
const moreBtn = endpointItem ? endpointItem.querySelector('[data-action="more"]') : null;
Expand All @@ -650,7 +776,7 @@ export async function testEndpointHandler(index, buttonElement) {
buttonElement.innerHTML = '⏳';

// 使用轻量级测试(优先零消耗方法)
const result = await testEndpointLight(index);
const result = await testEndpointLight(index, testModel);

const resultContent = document.getElementById('testResultContent');
const resultTitle = document.getElementById('testResultTitle');
Expand Down Expand Up @@ -744,4 +870,4 @@ export function openArticle() {
if (window.go?.main?.App) {
window.go.main.App.OpenURL('https://mp.weixin.qq.com/s/ohtkyIMd5YC7So1q-gE0og');
}
}
}
2 changes: 2 additions & 0 deletions cmd/desktop/frontend/wailsjs/go/main/App.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,8 @@ export function TestEndpoint(arg1:number):Promise<string>;

export function TestEndpointLight(arg1:number):Promise<string>;

export function TestEndpointLightWithModel(arg1:number,arg2:string):Promise<string>;

export function TestS3Connection(arg1:string,arg2:string,arg3:string,arg4:string,arg5:string,arg6:string,arg7:string,arg8:boolean,arg9:boolean):Promise<string>;

export function TestWebDAVConnection(arg1:string,arg2:string,arg3:string):Promise<string>;
Expand Down
4 changes: 4 additions & 0 deletions cmd/desktop/frontend/wailsjs/go/main/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,10 @@ export function TestEndpointLight(arg1) {
return window['go']['main']['App']['TestEndpointLight'](arg1);
}

export function TestEndpointLightWithModel(arg1, arg2) {
return window['go']['main']['App']['TestEndpointLightWithModel'](arg1, arg2);
}

export function TestS3Connection(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) {
return window['go']['main']['App']['TestS3Connection'](arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9);
}
Expand Down
44 changes: 44 additions & 0 deletions cmd/desktop/frontend_contract_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package main

import (
"os"
"strings"
"testing"
)

func readDesktopFrontend(t *testing.T, path string) string {
t.Helper()
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
return string(data)
}

func TestDesktopEndpointTestUsesTemporaryModelPrompt(t *testing.T) {
endpoints := readDesktopFrontend(t, "frontend/src/modules/endpoints.js")
if strings.Count(endpoints, "item.dataset.model = model;") < 2 {
t.Error("detail and compact endpoint cards must expose their configured model to the test action")
}

modal := readDesktopFrontend(t, "frontend/src/modules/modal.js")
for _, value := range []string{
"requestTestModel",
"await testEndpointLight(index, testModel)",
"input.value = configuredModel;",
"test-model-fetch",
"FetchModels(endpoint.apiUrl, endpoint.apiKey, endpoint.transformer)",
} {
if !strings.Contains(modal, value) {
t.Errorf("desktop test flow must contain %q", value)
}
}
if strings.Contains(modal, "return Promise.resolve(model)") {
t.Error("desktop test flow must always open the model prompt")
}

configJS := readDesktopFrontend(t, "frontend/src/modules/config.js")
if !strings.Contains(configJS, "TestEndpointLightWithModel(index, model)") {
t.Error("desktop frontend must pass the temporary model through the Wails binding")
}
}
Loading
Loading