fix: 三端联调 P1 修复 — API密钥页崩溃 + 桌面端401恢复 + 用量统计全零
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
P1-03: vite.config.ts proxy '/api' → '/api/' 加尾部斜杠, 防止前缀匹配 /api-keys 导致 SPA 路由崩溃 P1-01: kernel_init 增加 api_key 变更检测(token 刷新后自动重连), streamStore 增加 401 自动恢复(refresh token → kernel reconnect), KernelClient 新增 getConfig() 方法 P1-02: /api/v1/usage 总计改从 billing_usage_quotas 读取 (authoritative source,SSE 和 JSON 均写入), by_model/by_day 仍从 usage_records 读取
This commit is contained in:
@@ -73,15 +73,18 @@ pub async fn kernel_init(
|
||||
// Get current config from kernel
|
||||
let current_config = kernel.config();
|
||||
|
||||
// Check if config changed
|
||||
// Check if config changed (model, base_url, or api_key)
|
||||
let config_changed = if let Some(ref req) = config_request {
|
||||
let default_base_url = zclaw_kernel::config::KernelConfig::from_provider(
|
||||
&req.provider, "", &req.model, None, &req.api_protocol
|
||||
).llm.base_url;
|
||||
let request_base_url = req.base_url.clone().unwrap_or(default_base_url.clone());
|
||||
let current_api_key = ¤t_config.llm.api_key;
|
||||
let request_api_key = req.api_key.as_deref().unwrap_or("");
|
||||
|
||||
current_config.llm.model != req.model ||
|
||||
current_config.llm.base_url != request_base_url
|
||||
current_config.llm.base_url != request_base_url ||
|
||||
current_api_key != request_api_key
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
@@ -164,6 +164,11 @@ export class KernelClient {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/** Get current kernel configuration (for auth token refresh) */
|
||||
getConfig(): KernelConfig | undefined {
|
||||
return this.config;
|
||||
}
|
||||
|
||||
getState(): ConnectionState {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
@@ -38,6 +38,47 @@ import { useArtifactStore } from './artifactStore';
|
||||
|
||||
const log = createLogger('StreamStore');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 401 Auth Error Recovery
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Detect and handle 401 auth errors during chat streaming.
|
||||
* Attempts token refresh → kernel reconnect → auto-retry.
|
||||
* Returns a user-friendly error message if recovery fails.
|
||||
*/
|
||||
async function tryRecoverFromAuthError(error: string): Promise<string | null> {
|
||||
const is401 = /401|Unauthorized|UNAUTHORIZED|未认证|认证已过期/.test(error);
|
||||
if (!is401) return null;
|
||||
|
||||
log.info('Detected 401 auth error, attempting token refresh...');
|
||||
try {
|
||||
const { saasClient } = await import('../../lib/saas-client');
|
||||
const newToken = await saasClient.refreshMutex();
|
||||
if (newToken) {
|
||||
// Update kernel config with refreshed token → triggers kernel re-init via changed api_key detection
|
||||
const { useConnectionStore } = await import('../connectionStore');
|
||||
const { getKernelClient } = await import('../../lib/kernel-client');
|
||||
const kernelClient = getKernelClient();
|
||||
const currentConfig = kernelClient.getConfig();
|
||||
if (currentConfig) {
|
||||
kernelClient.setConfig({ ...currentConfig, apiKey: newToken });
|
||||
await kernelClient.connect();
|
||||
log.info('Kernel reconnected with refreshed token');
|
||||
}
|
||||
return '认证已刷新,请重新发送消息';
|
||||
}
|
||||
} catch (refreshErr) {
|
||||
log.warn('Token refresh failed, triggering logout:', refreshErr);
|
||||
try {
|
||||
const { useSaaSStore } = await import('../saasStore');
|
||||
useSaaSStore.getState().logout();
|
||||
} catch { /* non-critical */ }
|
||||
return 'SaaS 会话已过期,请重新登录';
|
||||
}
|
||||
return '认证失败,请重新登录';
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -517,7 +558,7 @@ export const useStreamStore = create<StreamState>()(
|
||||
}
|
||||
}
|
||||
},
|
||||
onError: (error: string) => {
|
||||
onError: async (error: string) => {
|
||||
// Flush any remaining buffered deltas before erroring
|
||||
if (flushTimer !== null) {
|
||||
clearTimeout(flushTimer);
|
||||
@@ -525,10 +566,14 @@ export const useStreamStore = create<StreamState>()(
|
||||
}
|
||||
flushBuffers();
|
||||
|
||||
// Attempt 401 auth recovery (token refresh + kernel reconnect)
|
||||
const recoveryMsg = await tryRecoverFromAuthError(error);
|
||||
const displayError = recoveryMsg || error;
|
||||
|
||||
_chat?.updateMessages(msgs =>
|
||||
msgs.map(m =>
|
||||
m.id === assistantId
|
||||
? { ...m, content: error, streaming: false, error }
|
||||
? { ...m, content: displayError, streaming: false, error: displayError }
|
||||
: m.role === 'user' && m.optimistic && m.timestamp.getTime() >= streamStartTime
|
||||
? { ...m, optimistic: false }
|
||||
: m
|
||||
@@ -573,13 +618,18 @@ export const useStreamStore = create<StreamState>()(
|
||||
textBuffer = '';
|
||||
thinkBuffer = '';
|
||||
|
||||
const errorMessage = err instanceof Error ? err.message : '无法连接 Gateway';
|
||||
let errorMessage = err instanceof Error ? err.message : '无法连接 Gateway';
|
||||
|
||||
// Attempt 401 auth recovery
|
||||
const recoveryMsg = await tryRecoverFromAuthError(errorMessage);
|
||||
if (recoveryMsg) errorMessage = recoveryMsg;
|
||||
|
||||
_chat?.updateMessages(msgs =>
|
||||
msgs.map(m =>
|
||||
m.id === assistantId
|
||||
? {
|
||||
...m,
|
||||
content: `⚠️ ${errorMessage}`,
|
||||
content: errorMessage,
|
||||
streaming: false,
|
||||
error: errorMessage,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user