Skip to content

Commit b73a088

Browse files
committed
feat(electron): 主进程 AI 请求 429 配额耗尽事件推送(gatewayHttp/Stream/preload)
1 parent 5312345 commit b73a088

3 files changed

Lines changed: 31 additions & 0 deletions

File tree

‎client/electron/ai/gatewayHttp.ts‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
* source 字段('local'|'remote')供渲染层展示推理来源,勿删。
1111
*/
1212
import { randomUUID } from 'crypto';
13+
import { BrowserWindow } from 'electron';
1314
import { logger } from '../logger.js';
1415
import { gatewayUrl } from './gatewayConfig.js';
1516
import { isLocalInferenceEnabled } from './ollama/config.js';
@@ -34,6 +35,19 @@ function logNetworkHint(errDetail: string): void {
3435
}
3536
}
3637

38+
/**
39+
* 429 配额耗尽 → 推送渲染进程(主进程代理路径的统一感知点)。
40+
*
41+
* 仅配额类 429 触发(文案含「已达上限」);上游服务商 429 为临时故障不打扰用户。
42+
* 渲染进程 QuotaNotice 组件监听 ai:quota-exhausted 事件弹非阻断提示并刷新配额。
43+
* gatewayStream 复用本函数(唯一语义点,避免两处漂移)。
44+
*/
45+
export function notifyQuotaExhaustedToRenderer(detail: string): void {
46+
if (!/已达上限/.test(detail)) return;
47+
const win = BrowserWindow.getAllWindows().find((w) => !w.isDestroyed());
48+
win?.webContents.send('ai:quota-exhausted', detail);
49+
}
50+
3751
/**
3852
* POST 公共骨架:超时控制 + 日志 + req-id + 错误诊断 + JSON 响应解析
3953
*/
@@ -95,6 +109,8 @@ async function executePost<TRes>(
95109
// 截取响应体前 500 字符防止日志爆炸
96110
const detailPreview = detail.length > 500 ? `${detail.slice(0, 500)}...(+${detail.length - 500} chars)` : detail;
97111
logger.error(`[AI] ✖ HTTP ${resp.status} ${url} (${elapsed}ms) [req-id: ${requestId ?? clientRequestId}]: ${detailPreview}`);
112+
// 配额类 429:通知渲染进程展示配额耗尽提示(不改变既有抛错行为)
113+
if (resp.status === 429) notifyQuotaExhaustedToRenderer(detail);
98114
throw new Error(`HTTP ${resp.status}: ${detail}`);
99115
}
100116

‎client/electron/ai/gatewayStream.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import { randomUUID } from 'crypto';
1010
import { logger } from '../logger.js';
1111
import { gatewayUrl } from './gatewayConfig.js';
12+
import { notifyQuotaExhaustedToRenderer } from './gatewayHttp.js';
1213

1314
/**
1415
* 流式 POST 请求:解析 SSE data: 行,逐 chunk yield 文本
@@ -73,6 +74,8 @@ export async function* postJsonStream<TReq>(
7374
if (!resp.ok) {
7475
const detail = await resp.text().catch(() => 'unknown error');
7576
logger.error(`[AI] ✖ Stream HTTP ${resp.status} ${url} [req-id: ${requestId ?? clientRequestId}]: ${detail.slice(0, 200)}`);
77+
// 配额类 429:通知渲染进程展示配额耗尽提示(不改变既有抛错行为)
78+
if (resp.status === 429) notifyQuotaExhaustedToRenderer(detail);
7679
throw new Error(`Stream HTTP ${resp.status}: ${detail}`);
7780
}
7881

‎client/electron/preload.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,8 @@ const ALLOWED_EVENT_CHANNELS = [
198198
'shortcut:triggered',
199199
// 3.18 电子墨水学习板:主进程推送复习卡片到次窗口
200200
'eink:card',
201+
// AI 配额耗尽(429):主进程代理路径推送,渲染进程弹非阻断提示 + 刷新配额
202+
'ai:quota-exhausted',
201203
] as const;
202204

203205
/** 允许渲染进程单向发送的 channel 白名单(渲染进程 → 主进程,fire-and-forget) */
@@ -370,4 +372,14 @@ contextBridge.exposeInMainWorld('electronAPI', {
370372
return () => ipcRenderer.removeListener('ollama:pull-progress', handler);
371373
},
372374
},
375+
// ---- AI 配额耗尽(429):主进程代理路径推送 ----
376+
onAIQuotaExhausted: (callback: (detail: string) => void) => {
377+
if (!(ALLOWED_EVENT_CHANNELS as readonly string[]).includes('ai:quota-exhausted')) {
378+
console.warn('[preload] 不允许的事件 channel: ai:quota-exhausted');
379+
return () => {};
380+
}
381+
const handler = (_event: unknown, detail: string) => callback(detail);
382+
ipcRenderer.on('ai:quota-exhausted', handler);
383+
return () => ipcRenderer.removeListener('ai:quota-exhausted', handler);
384+
},
373385
});

0 commit comments

Comments
 (0)