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
5 changes: 3 additions & 2 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -1492,8 +1492,9 @@ func (a *App) TestEndpoint(index int) string {
// Check status code
if resp.StatusCode != http.StatusOK {
result := map[string]interface{}{
"success": false,
"message": fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(respBody)),
"success": false,
"statusCode": resp.StatusCode,
"message": fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(respBody)),
}
data, _ := json.Marshal(result)
logger.Error("Test failed for %s: HTTP %d", endpoint.Name, resp.StatusCode)
Expand Down
10 changes: 7 additions & 3 deletions app/frontend/src/i18n/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export default {
close: 'Close',
changePort: 'Change Port',
port: 'Port',
portLabel: 'Port (1-65535):',
portNote: 'Note: Changing port requires application restart',
portInvalid: 'Please enter a valid port number (1-65535)',
portUpdateSuccess: 'Port updated successfully! Please restart the application for changes to take effect.',
Expand Down Expand Up @@ -101,7 +102,8 @@ export default {
failedTitle: '❌ Test Failed',
connectionSuccess: 'Connection successful!',
connectionFailed: 'Connection failed',
testError: 'Test error'
testError: 'Test error',
notSupportedMessage: 'This endpoint not support test API, try using it directly in the client'
},
welcome: {
title: 'Welcome to ccNexus!',
Expand Down Expand Up @@ -253,7 +255,8 @@ export default {
chooseConfig: 'Please choose which configuration to use:',
useRemote: 'Use Remote Backup',
keepLocal: 'Keep Local Configuration',
enterBackupName: 'Please enter backup filename',
enterBackupName: 'Backup filename:',
filenameRequired: 'Please enter filename',
inputFilename: 'Input filename',
enabled: 'Enabled',
remark: 'Remark',
Expand Down Expand Up @@ -363,9 +366,10 @@ export default {
confirmDelete: 'Are you sure you want to delete this session? This cannot be undone.',
deleted: 'Session deleted',
deleteFailed: 'Failed to delete session',
renamePrompt: 'Enter session alias',
renamePrompt: 'Session alias:',
renamed: 'Session renamed',
renameFailed: 'Failed to rename session',
aliasRequired: 'Please enter session alias',
modTime: 'Modified',
size: 'Size',
confirmAndReturn: 'Confirm and Return',
Expand Down
12 changes: 8 additions & 4 deletions app/frontend/src/i18n/zh-CN.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ export default {
close: '关闭',
changePort: '修改端口',
port: '端口',
portNote: '注意:修改端口需要重启应用',
portLabel: '端口号 (1-65535):',
portNote: '注意:修改端口号需要重启应用',
portInvalid: '请输入有效的端口号(1-65535)',
portUpdateSuccess: '端口修改成功!请重启应用以使更改生效。',
portUpdateFailed: '端口修改失败:{error}',
Expand Down Expand Up @@ -101,7 +102,8 @@ export default {
failedTitle: '❌ 测试失败',
connectionSuccess: '连接成功!',
connectionFailed: '连接失败',
testError: '测试出错'
testError: '测试出错',
notSupportedMessage: '当前端点不支持测试接口,可尝试直接到客户端中使用'
},
welcome: {
title: '欢迎使用 ccNexus!',
Expand Down Expand Up @@ -253,7 +255,8 @@ export default {
chooseConfig: '请选择要使用的配置:',
useRemote: '使用远程备份',
keepLocal: '保留本地配置',
enterBackupName: '请输入备份文件名',
enterBackupName: '备份文件名:',
filenameRequired: '请先输入文件名',
enabled: '启用状态',
remark: '备注',
apiUrl: 'API 地址',
Expand Down Expand Up @@ -364,9 +367,10 @@ export default {
confirmDelete: '确定要删除此会话吗?删除后无法恢复。',
deleted: '会话已删除',
deleteFailed: '删除会话失败',
renamePrompt: '请输入会话别名',
renamePrompt: '会话别名:',
renamed: '会话已重命名',
renameFailed: '重命名失败',
aliasRequired: '请先输入会话别名',
modTime: '修改时间',
size: '大小',
summary: '摘要',
Expand Down
85 changes: 83 additions & 2 deletions app/frontend/src/modules/endpoints.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,48 @@ import { formatTokens, maskApiKey } from '../utils/format.js';
import { getEndpointStats } from './stats.js';
import { toggleEndpoint } from './config.js';

const ENDPOINT_TEST_STATUS_KEY = 'ccNexus_endpointTestStatus';
const CURRENT_ENDPOINT_KEY = 'ccNexus_currentEndpoint';

// 获取端点测试状态
export function getEndpointTestStatus(endpointName) {
try {
const statusMap = JSON.parse(localStorage.getItem(ENDPOINT_TEST_STATUS_KEY) || '{}');
return statusMap[endpointName]; // true=成功, false=失败, undefined=未测试
} catch {
return undefined;
}
}

// 保存端点测试状态
export function saveEndpointTestStatus(endpointName, success) {
try {
const statusMap = JSON.parse(localStorage.getItem(ENDPOINT_TEST_STATUS_KEY) || '{}');
statusMap[endpointName] = success;
localStorage.setItem(ENDPOINT_TEST_STATUS_KEY, JSON.stringify(statusMap));
} catch (error) {
console.error('Failed to save endpoint test status:', error);
}
}

// 获取保存的当前端点名称
export function getSavedCurrentEndpoint() {
try {
return localStorage.getItem(CURRENT_ENDPOINT_KEY) || '';
} catch {
return '';
}
}

// 保存当前端点名称
export function saveCurrentEndpoint(endpointName) {
try {
localStorage.setItem(CURRENT_ENDPOINT_KEY, endpointName);
} catch (error) {
console.error('Failed to save current endpoint:', error);
}
}

let currentTestButton = null;
let currentTestButtonOriginalText = '';
let currentTestIndex = -1;
Expand Down Expand Up @@ -39,14 +81,41 @@ export function setTestState(button, index) {
export async function renderEndpoints(endpoints) {
const container = document.getElementById('endpointList');

// Get current endpoint
// Get current endpoint from backend
let currentEndpointName = '';
try {
currentEndpointName = await window.go.main.App.GetCurrentEndpoint();
} catch (error) {
console.error('Failed to get current endpoint:', error);
}

// 检查 localStorage 中保存的当前端点,如果与后端不一致则同步
const savedEndpoint = getSavedCurrentEndpoint();
if (savedEndpoint && savedEndpoint !== currentEndpointName) {
// 检查保存的端点是否存在且启用
const savedExists = endpoints.some(ep => ep.name === savedEndpoint && (ep.enabled !== false));
if (savedExists) {
try {
await window.go.main.App.SwitchToEndpoint(savedEndpoint);
currentEndpointName = savedEndpoint;
} catch (error) {
console.error('Failed to restore saved endpoint:', error);
// 如果恢复失败,更新 localStorage 为后端的当前端点
if (currentEndpointName) {
saveCurrentEndpoint(currentEndpointName);
}
}
} else {
// 保存的端点不存在或未启用,更新 localStorage
if (currentEndpointName) {
saveCurrentEndpoint(currentEndpointName);
}
}
} else if (!savedEndpoint && currentEndpointName) {
// localStorage 没有保存,初始化保存当前端点
saveCurrentEndpoint(currentEndpointName);
}

if (endpoints.length === 0) {
container.innerHTML = `
<div class="empty-state">
Expand Down Expand Up @@ -78,11 +147,21 @@ export async function renderEndpoints(endpoints) {
item.draggable = true;
item.dataset.name = ep.name;
item.dataset.index = index;
// 获取测试状态:true=成功显示✅,false=失败显示❌,undefined/unknown=未测试/未知显示⚠️
const testStatus = getEndpointTestStatus(ep.name);
let testStatusIcon = '⚠️'; // 默认未测试
if (testStatus === true) {
testStatusIcon = '✅';
} else if (testStatus === false) {
testStatusIcon = '❌';
}
// 'unknown' 或 undefined 都显示 ⚠️

item.innerHTML = `
<div class="endpoint-info">
<h3>
${ep.name}
${enabled ? '✅' : '❌'}
${testStatusIcon}
${isCurrentEndpoint ? '<span class="current-badge">' + t('endpoints.current') + '</span>' : ''}
${enabled && !isCurrentEndpoint ? '<button class="btn btn-switch" data-action="switch" data-name="' + ep.name + '">' + t('endpoints.switchTo') + '</button>' : ''}
</h3>
Expand Down Expand Up @@ -155,6 +234,8 @@ export async function renderEndpoints(endpoints) {
switchBtn.disabled = true;
switchBtn.innerHTML = '⏳';
await window.go.main.App.SwitchToEndpoint(name);
// 保存当前端点到 localStorage
saveCurrentEndpoint(name);
window.loadConfig(); // Refresh display
} catch (error) {
console.error('Failed to switch endpoint:', error);
Expand Down
59 changes: 56 additions & 3 deletions app/frontend/src/modules/modal.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { t } from '../i18n/index.js';
import { escapeHtml } from '../utils/format.js';
import { addEndpoint, updateEndpoint, removeEndpoint, testEndpoint, updatePort } from './config.js';
import { setTestState, clearTestState } from './endpoints.js';
import { hideAboutBadge } from './updater.js';
import { setTestState, clearTestState, saveEndpointTestStatus } from './endpoints.js';

let currentEditIndex = -1;

Expand Down Expand Up @@ -370,7 +369,6 @@ export function closePortModal() {
// Welcome Modal
export async function showWelcomeModal() {
document.getElementById('welcomeModal').classList.add('active');
hideAboutBadge();

try {
const version = await window.go.main.App.GetVersion();
Expand Down Expand Up @@ -451,10 +449,30 @@ export function showWelcomeModalIfFirstTime() {
}
}

// 判断是否为"不支持测试"的情况
function isTestNotSupported(statusCode, message) {
// 可能不支持测试的 HTTP 状态码
const notSupportedCodes = [404, 405, 501];
// 认证错误关键词(如果包含这些,说明是真正的错误,不是不支持测试)
const authErrorKeywords = ['unauthorized', 'invalid key', 'invalid_api_key', 'authentication', 'api key', 'api_key', 'forbidden', 'permission', 'access denied'];

if (notSupportedCodes.includes(statusCode)) {
const lowerMsg = (message || '').toLowerCase();
// 排除明显的认证错误
const isAuthError = authErrorKeywords.some(kw => lowerMsg.includes(kw));
return !isAuthError;
}
return false;
}

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

// 获取端点名称用于保存测试状态
const endpointItem = buttonElement.closest('.endpoint-item');
const endpointName = endpointItem ? endpointItem.dataset.name : null;

try {
buttonElement.disabled = true;
buttonElement.innerHTML = '⏳';
Expand All @@ -472,6 +490,24 @@ export async function testEndpointHandler(index, buttonElement) {
</div>
<div style="padding: 15px; background: #f8f9fa; border-radius: 5px; font-family: monospace; white-space: pre-line; word-break: break-all;">${escapeHtml(result.message)}</div>
`;
// 保存测试成功状态
if (endpointName) {
saveEndpointTestStatus(endpointName, true);
}
} else if (isTestNotSupported(result.statusCode, result.message)) {
// 可能不支持测试的情况,使用 toast 提示
showNotification(t('test.notSupportedMessage'), 'warning');
// 保存为未知状态
if (endpointName) {
saveEndpointTestStatus(endpointName, 'unknown');
}
// 清除测试状态,恢复按钮
clearTestState();
// 刷新端点列表以更新图标
if (window.loadConfig) {
window.loadConfig();
}
return; // 不显示测试结果弹窗
} else {
resultTitle.innerHTML = t('test.failedTitle');
resultContent.innerHTML = `
Expand All @@ -480,9 +516,17 @@ export async function testEndpointHandler(index, buttonElement) {
</div>
<div style="padding: 15px; background: #f8f9fa; border-radius: 5px; font-family: monospace; white-space: pre-line; word-break: break-all;"><strong>Error:</strong><br>${escapeHtml(result.message)}</div>
`;
// 保存测试失败状态
if (endpointName) {
saveEndpointTestStatus(endpointName, false);
}
}

document.getElementById('testResultModal').classList.add('active');
// 刷新端点列表以更新图标
if (window.loadConfig) {
window.loadConfig();
}

} catch (error) {
console.error('Test failed:', error);
Expand All @@ -498,7 +542,16 @@ export async function testEndpointHandler(index, buttonElement) {
<div style="padding: 15px; background: #f8f9fa; border-radius: 5px; font-family: monospace; white-space: pre-line;">${escapeHtml(error.toString())}</div>
`;

// 保存测试失败状态(异常情况)
if (endpointName) {
saveEndpointTestStatus(endpointName, false);
}

document.getElementById('testResultModal').classList.add('active');
// 刷新端点列表以更新图标
if (window.loadConfig) {
window.loadConfig();
}
}
}

Expand Down
27 changes: 18 additions & 9 deletions app/frontend/src/modules/session.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { GetSessions, DeleteSession, RenameSession, GetSessionData } from '../../wailsjs/go/main/App';
import { t } from '../i18n/index.js';
import { showNotification } from './modal.js';
import { parseMarkdown } from '../utils/markdown.js';

let currentProjectDir = '';
let sessions = [];
Expand Down Expand Up @@ -285,7 +286,7 @@ function showPromptDialog(message, defaultValue = '') {
</div>
<div class="modal-body">
<div class="prompt-dialog">
<p>${message}</p>
<p><span class="required">*</span>${message}</p>
<div class="prompt-body">
<input type="text" id="promptInput" class="form-input" value="${defaultValue}" />
</div>
Expand All @@ -310,11 +311,18 @@ function showPromptDialog(message, defaultValue = '') {
setTimeout(() => modal.remove(), 300);
};

modal.querySelector('#promptOk').onclick = () => {
const handleSubmit = () => {
const value = input.value.trim();
if (!value) {
showNotification(t('session.aliasRequired'), 'warning');
input.focus();
return;
}
closeModal();
resolve(value || null);
resolve(value);
};

modal.querySelector('#promptOk').onclick = handleSubmit;
modal.querySelector('#promptCancel').onclick = () => {
closeModal();
resolve(null);
Expand All @@ -325,9 +333,7 @@ function showPromptDialog(message, defaultValue = '') {
};
input.onkeydown = (e) => {
if (e.key === 'Enter') {
const value = input.value.trim();
closeModal();
resolve(value || null);
handleSubmit();
}
};
});
Expand Down Expand Up @@ -391,12 +397,15 @@ function renderMessages(messages) {
container.innerHTML = messages.map(msg => {
const isUser = msg.type === 'user';
const label = isUser ? t('session.user') : t('session.assistant');
const content = msg.content.trim().replace(/\n/g, '<br>');
// 使用 markdown 解析器处理内容
const content = parseMarkdown(msg.content.trim());

return `
<div class="message-card ${isUser ? 'message-user' : 'message-assistant'}">
<div class="message-row ${isUser ? 'message-row-user' : 'message-row-assistant'}">
<div class="message-label">${label}</div>
<div class="message-content">${content}</div>
<div class="message-bubble ${isUser ? 'bubble-user' : 'bubble-assistant'}">
<div class="message-content markdown-body">${content}</div>
</div>
</div>
`;
}).join('');
Expand Down
Loading
Loading