diff --git a/src/api.js b/src/api.js index fd62a15..963fe4d 100644 --- a/src/api.js +++ b/src/api.js @@ -19,11 +19,18 @@ api.interceptors.request.use((config) => { return config; }); +const shouldSkipErrorHandler = (config) => Boolean(config?.skipErrorHandler); + // Global error handler api.interceptors.response.use( (res) => { // Handle success:false responses with user-visible errors - if (res.data && res.data.success === false && res.data.message) { + if ( + res.data && + res.data.success === false && + res.data.message && + !shouldSkipErrorHandler(res.config) + ) { toast.error(res.data.message); } return res; @@ -34,8 +41,10 @@ api.interceptors.response.use( localStorage.removeItem('dist_user_id'); // Emit event so AuthContext can clear React state window.dispatchEvent(new Event('auth:logout')); - toast.error('Session expired, please log in again'); - } else { + if (!shouldSkipErrorHandler(err.config)) { + toast.error('Session expired, please log in again'); + } + } else if (!shouldSkipErrorHandler(err.config)) { toast.error(msg); } return Promise.reject(err); @@ -56,7 +65,7 @@ export const login = (data) => api.post('/api/dist/user/login', data); export const logout = () => api.post('/api/dist/user/logout'); // ===== User ===== -export const getUserSelf = () => api.get('/api/dist/user/self'); +export const getUserSelf = (config) => api.get('/api/dist/user/self', config); export const getUserUsage = () => api.get('/api/dist/user/usage'); export const getUserLogs = (params) => api.get('/api/dist/user/logs', { params }); @@ -70,7 +79,8 @@ export const deleteToken = (id) => api.delete(`/api/dist/token/${id}`); // ===== Purchase ===== export const redeemCode = (key) => api.post('/api/dist/topup/redeem', { key }); // backend field is "key" export const subscribePackage = (packageId) => api.post('/api/dist/package/subscribe', { package_id: packageId }); -export const getActiveSubscriptions = () => api.get('/api/dist/package/subscriptions'); +export const getActiveSubscriptions = (config) => + api.get('/api/dist/package/subscriptions', config); // ===== Online Topup ===== export const getTopupInfo = () => api.get('/api/dist/topup/info'); diff --git a/src/components/ConfigExporter.jsx b/src/components/ConfigExporter.jsx index 9661635..fbcdd4d 100644 --- a/src/components/ConfigExporter.jsx +++ b/src/components/ConfigExporter.jsx @@ -6,7 +6,6 @@ import { useSite } from '../context/SiteContext'; const TOOLS = [ { id: 'claudecode', name: 'Claude Code', path: '~/.claude/settings.json' }, - { id: 'ccswitch', name: 'CC Switch', path: 'ccswitch://v1/import' }, { id: 'openclaw', name: 'OpenClaw', path: '~/.openclaw/openclaw.json' }, { id: 'opencode', name: 'OpenCode', path: '~/.config/opencode/opencode.json' }, { id: 'cursor', name: 'Cursor', path: 'Settings -> Models -> OpenAI API Key' }, @@ -287,17 +286,39 @@ print(message.content[0].text)`; } }; + const getSelectedToolBaseUrl = () => { + if ( + selectedTool === 'claudecode' || + selectedTool === 'anthropic' || + selectedTool === 'openclaw' + ) { + return `${serverAddress}/`; + } + return `${serverAddress}/v1`; + }; + + const handleCopyValue = async (text, successKey = 'config.copied') => { + if (!text) return; + await copyToClipboard(text); + toast.success(t(successKey)); + }; + const handleCopy = async () => { const config = generateConfig(); if (!config) return; await copyToClipboard(config); setCopied(true); - toast.success( - t(selectedTool === 'ccswitch' ? 'config.importLinkCopied' : 'config.copied'), - ); + toast.success(t('config.copied')); window.setTimeout(() => setCopied(false), 2000); }; + const handleCopyCCSwitchLink = async () => { + const deeplink = generateCCSwitchLink(); + if (!deeplink) return; + await copyToClipboard(deeplink); + toast.success(t('config.importLinkCopied')); + }; + const handleDownload = () => { const config = generateConfig(); if (!config) return; @@ -361,6 +382,7 @@ print(message.content[0].text)`; }; const config = generateConfig(); + const ccSwitchLink = generateCCSwitchLink(); if (tokens.length === 0) { return ( @@ -481,6 +503,78 @@ print(message.content[0].text)`; +
+ {t('config.apiUrlTitle')} +
++ {t('config.apiUrlHint')} +
+
+ {getSelectedToolBaseUrl()}
+
+ - {t('config.selectCCSwitchApp')} -
-- {t('config.ccswitchHint')} -
-+ {t('config.ccswitchTitle')} +
++ {t('config.ccswitchHint')} +
++ {t('config.selectCCSwitchApp')} +
+
+ {ccSwitchLink}
+
+
diff --git a/src/context/AuthContext.jsx b/src/context/AuthContext.jsx
index f3d39e7..0422728 100644
--- a/src/context/AuthContext.jsx
+++ b/src/context/AuthContext.jsx
@@ -9,9 +9,9 @@ export function AuthProvider({ children }) {
const [loading, setLoading] = useState(true);
// Refresh user data — used after login, redeem, subscribe, etc.
- const refreshUser = useCallback(async () => {
+ const refreshUser = useCallback(async (config = {}) => {
try {
- const res = await getUserSelf();
+ const res = await getUserSelf(config);
if (res.data.success) {
setUser(res.data.data);
return res.data.data;
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index c4e3c13..76bd876 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -197,6 +197,25 @@
"config.noKeyDesc": "Config files will be generated once you have a key",
"config.searchModel": "Search models...",
"config.noModelMatch": "No models match your search",
+ "config.loadingModels": "Loading models...",
+ "config.apiUrlTitle": "API Endpoints",
+ "config.apiUrlHint": "After creating an API key, you can connect directly with the Base URLs below. Click any address to copy it.",
+ "config.copyCurrentApiUrl": "Copy current tool URL",
+ "config.currentToolApiUrl": "Recommended Base URL for current tool",
+ "config.openaiApiUrl": "OpenAI-compatible Base URL",
+ "config.anthropicApiUrl": "Anthropic / Claude Base URL",
+ "config.apiUrlCopied": "API URL copied",
+ "config.ccswitchTitle": "CC Switch Quick Import",
+ "config.selectCCSwitchApp": "Choose target app",
+ "config.ccswitchHint": "Pick the destination app and import into CC Switch directly without switching the export tool first.",
+ "config.importToCCSwitch": "Import to CC Switch",
+ "config.launchingCCSwitch": "Launching CC Switch...",
+ "config.copyImportLink": "Copy import link",
+ "config.importLinkCopied": "CC Switch import link copied",
+ "config.ccswitchNotInstalledTitle": "CC Switch Not Detected",
+ "config.ccswitchNotInstalledDesc": "If CC Switch did not open automatically, it may not be installed on this device yet. Install it first, then try the quick import again.",
+ "config.downloadCCSwitch": "Download CC Switch",
+ "config.openCCSwitchRepo": "Open project page",
"pricing.title": "Model Pricing",
"pricing.subtitle": "Transparent, pay-as-you-go pricing for all available models. Prices shown per 1M tokens.",
diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json
index c6ed649..8ea6686 100644
--- a/src/i18n/locales/zh.json
+++ b/src/i18n/locales/zh.json
@@ -197,6 +197,25 @@
"config.noKeyDesc": "创建密钥后即可自动生成配置文件",
"config.searchModel": "搜索模型...",
"config.noModelMatch": "没有匹配的模型",
+ "config.loadingModels": "正在加载模型...",
+ "config.apiUrlTitle": "API 接入地址",
+ "config.apiUrlHint": "创建好 API Key 后,可直接使用下面的 Base URL 接入。点击地址即可复制。",
+ "config.copyCurrentApiUrl": "复制当前工具地址",
+ "config.currentToolApiUrl": "当前工具推荐 Base URL",
+ "config.openaiApiUrl": "OpenAI 兼容 Base URL",
+ "config.anthropicApiUrl": "Anthropic / Claude Base URL",
+ "config.apiUrlCopied": "API 地址已复制",
+ "config.ccswitchTitle": "CC Switch 一键导入",
+ "config.selectCCSwitchApp": "选择导入目标",
+ "config.ccswitchHint": "不用切换工具,直接选择目标应用后即可一键导入到 CC Switch。",
+ "config.importToCCSwitch": "一键导入到 CC Switch",
+ "config.launchingCCSwitch": "正在唤起 CC Switch...",
+ "config.copyImportLink": "复制导入链接",
+ "config.importLinkCopied": "CC Switch 导入链接已复制",
+ "config.ccswitchNotInstalledTitle": "未检测到 CC Switch",
+ "config.ccswitchNotInstalledDesc": "如果没有自动唤起 CC Switch,说明本机可能还未安装。可以先下载安装,再回来继续一键导入。",
+ "config.downloadCCSwitch": "下载 CC Switch",
+ "config.openCCSwitchRepo": "打开项目主页",
"pricing.title": "模型定价",
"pricing.subtitle": "透明的按量付费定价。价格以每百万 token 计。",
diff --git a/src/pages/Packages.jsx b/src/pages/Packages.jsx
index 3fb5ac7..6aa449a 100644
--- a/src/pages/Packages.jsx
+++ b/src/pages/Packages.jsx
@@ -69,11 +69,14 @@ export default function Packages() {
if (res.data.success) {
toast.success(t('packages.subscribedSuccess'));
setConfirmPkg(null);
- await refreshUser();
- // Reload subscriptions
- getActiveSubscriptions()
- .then((r) => { if (r.data.success) setActiveSubs(r.data.data || []); })
- .catch(() => {});
+ // Background refresh errors shouldn't override a successful purchase toast.
+ await refreshUser({ skipErrorHandler: true }).catch(() => null);
+ const subsRes = await getActiveSubscriptions({
+ skipErrorHandler: true,
+ }).catch(() => null);
+ if (subsRes?.data?.success) {
+ setActiveSubs(subsRes.data.data || []);
+ }
} else {
toast.error(res.data.message || t('common.requestFailed'));
}