Skip to content

Commit d60a239

Browse files
committed
feat(ai,classroom): streaming ASR/TTS, gateway refactor, handler IPC consolidation
- Add streaming ASR service with Sherpa model streaming support - Add speech normalizer/streamer and TTS handler for assistant - Refactor all AI handlers to unified gateway HTTP/stream transport - Simplify ai-gateway auth middleware and rate limiter - Add AsrSettingsSection UI; remove legacy apiKeyManager - Update CSP policy, preload bridge, classroom capture hooks - Add server db-backup/db-restore scripts; update docs
1 parent 7ed9eae commit d60a239

95 files changed

Lines changed: 3124 additions & 1170 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/pr-check.yml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ jobs:
7070
- uses: actions/checkout@v4
7171
- uses: actions/setup-go@v5
7272
with:
73-
go-version: '1.25'
73+
# Go 1.25 尚未发布,使用当前最新稳定版 1.24(截至 2026 年 8 月)
74+
go-version: '1.24'
7475
cache-dependency-path: server/sync-service/go.sum
7576
- name: Lint (go vet)
7677
run: go vet ./...

‎client/.env.example‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@ VITE_API_BASE_URL=https://entropydecrease.com
1919
VITE_API_HEALTH_URL=https://entropydecrease.com/health
2020

2121
# ---- AI 网关 ----
22-
# AI Gateway 服务地址
23-
# 开发环境: http://127.0.0.1:8000
22+
# AI Gateway 服务地址(electron:dev 与前端渲染进程共用)
23+
# 本地开发时指向本地 AI 网关: http://localhost:8000
24+
# 连接远程测试/生产服务器时,请改为对应地址(注意:测试服务器 IP 应写在 .env.test,不要提交到仓库)
2425
# 生产环境: https://entropydecrease.com
25-
VITE_AI_GATEWAY_URL=https://entropydecrease.com
26+
VITE_AI_GATEWAY_URL=http://localhost:8000
2627

2728
# ---- Ollama 本地推理(可选) ----
2829
# Ollama 服务地址,默认自动检测 localhost:11434

‎client/electron/ai/gatewayHttp.ts‎

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,10 @@ import { isLocalInferenceEnabled } from './ollama/config.js';
1616
import { isOllamaAvailable } from './ollama/OllamaService.js';
1717

1818
/** 构建公共请求头(JSON 模式含 Content-Type,multipart 由 fetch 自动生成) */
19-
function buildHeaders(clientRequestId: string, json: boolean, authToken?: string, userApiKey?: string): Record<string, string> {
19+
function buildHeaders(clientRequestId: string, json: boolean, authToken?: string): Record<string, string> {
2020
const headers: Record<string, string> = { 'X-Request-ID': clientRequestId };
2121
if (json) headers['Content-Type'] = 'application/json';
2222
if (authToken) headers['Authorization'] = `Bearer ${authToken}`;
23-
if (userApiKey) headers['X-User-API-Key'] = userApiKey;
2423
return headers;
2524
}
2625

@@ -44,7 +43,6 @@ async function executePost<TRes>(
4443
isJson: boolean,
4544
bodyDesc: string,
4645
authToken?: string,
47-
userApiKey?: string,
4846
timeoutMs: number = 60000,
4947
): Promise<{ data: TRes; requestId: string | undefined }> {
5048
const base = gatewayUrl();
@@ -57,9 +55,9 @@ async function executePost<TRes>(
5755

5856
// ── 请求前日志 ──
5957
logger.info(`[AI] → POST ${url} [req-id: ${clientRequestId}]`);
60-
logger.debug(`[AI] Request config: timeout=${timeoutMs}ms, hasAuth=${!!authToken}, hasUserKey=${!!userApiKey}, ${bodyDesc}`);
58+
logger.debug(`[AI] Request config: timeout=${timeoutMs}ms, hasAuth=${!!authToken}, ${bodyDesc}`);
6159

62-
const headers = buildHeaders(clientRequestId, isJson, authToken, userApiKey);
60+
const headers = buildHeaders(clientRequestId, isJson, authToken);
6361

6462
const controller = new AbortController();
6563
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
@@ -122,11 +120,10 @@ export async function postJson<TReq, TRes>(
122120
apiPath: string,
123121
body: TReq,
124122
authToken?: string,
125-
userApiKey?: string,
126123
timeoutMs: number = 60000,
127124
): Promise<{ data: TRes; requestId: string | undefined }> {
128125
const bodyDesc = `bodyKeys=${Object.keys(body as Record<string, unknown>).join(',')}`;
129-
return executePost<TRes>(apiPath, JSON.stringify(body), true, bodyDesc, authToken, userApiKey, timeoutMs);
126+
return executePost<TRes>(apiPath, JSON.stringify(body), true, bodyDesc, authToken, timeoutMs);
130127
}
131128

132129
/**
@@ -139,10 +136,9 @@ export async function postMultipart<TRes>(
139136
apiPath: string,
140137
formData: FormData,
141138
authToken?: string,
142-
userApiKey?: string,
143139
timeoutMs: number = 300000,
144140
): Promise<{ data: TRes; requestId: string | undefined }> {
145-
return executePost<TRes>(apiPath, formData, false, 'body=FormData', authToken, userApiKey, timeoutMs);
141+
return executePost<TRes>(apiPath, formData, false, 'body=FormData', authToken, timeoutMs);
146142
}
147143

148144
/**
@@ -158,7 +154,6 @@ export async function callWithLocalFallback<TReq, TRes>(
158154
body: TReq,
159155
localHandler: () => Promise<TRes>,
160156
authToken?: string,
161-
userApiKey?: string,
162157
timeoutMs: number = 60000,
163158
): Promise<{ data: TRes; source: 'local' | 'remote'; requestId?: string }> {
164159
// 检查本地 Ollama 是否可用
@@ -179,7 +174,6 @@ export async function callWithLocalFallback<TReq, TRes>(
179174
apiPath,
180175
body,
181176
authToken,
182-
userApiKey,
183177
timeoutMs,
184178
);
185179
return { data, source: 'remote', requestId };

‎client/electron/ai/gatewayStream.ts‎

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ export async function* postJsonStream<TReq>(
1717
apiPath: string,
1818
body: TReq,
1919
authToken?: string,
20-
userApiKey?: string,
2120
timeoutMs: number = 300000,
2221
): AsyncGenerator<string, void, unknown> {
2322
const base = gatewayUrl();
@@ -36,9 +35,6 @@ export async function* postJsonStream<TReq>(
3635
if (authToken) {
3736
headers['Authorization'] = `Bearer ${authToken}`;
3837
}
39-
if (userApiKey) {
40-
headers['X-User-API-Key'] = userApiKey;
41-
}
4238

4339
const controller = new AbortController();
4440
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
@@ -98,19 +94,24 @@ export async function* postJsonStream<TReq>(
9894
if (data === '[DONE]') {
9995
return;
10096
}
97+
// 先单独解析 JSON:仅捕获解析失败(回退纯文本),
98+
// 避免把下方 parsed.error 的主动 throw 一并吞掉——
99+
// 那个 throw 必须传播出去才能触发上层「流式失败→降级非流式」。
100+
let parsed: { error?: string; chunk?: string } | null = null;
101101
try {
102-
const parsed = JSON.parse(data);
103-
if (parsed.error) {
104-
throw new Error(`Stream error: ${parsed.error}`);
105-
}
106-
if (parsed.chunk) {
107-
yield parsed.chunk;
108-
}
109-
} catch (e) {
102+
parsed = JSON.parse(data);
103+
} catch {
110104
// JSON 解析失败,尝试作为纯文本
111-
if (data && data !== '[DONE]') {
105+
if (data) {
112106
yield data;
113107
}
108+
continue;
109+
}
110+
if (parsed?.error) {
111+
throw new Error(`Stream error: ${parsed.error}`);
112+
}
113+
if (parsed?.chunk) {
114+
yield parsed.chunk;
114115
}
115116
}
116117
}

‎client/electron/ai/handlers/anchorPointHandler.ts‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ function register(): void {
2727
content: string;
2828
title?: string;
2929
authToken?: string;
30-
userApiKey?: string;
3130
},
3231
) => {
3332
const startMs = Date.now();
@@ -68,7 +67,6 @@ function register(): void {
6867
reqBody,
6968
localHandler,
7069
args.authToken,
71-
args.userApiKey,
7270
60000,
7371
);
7472

‎client/electron/ai/handlers/chatHandler.ts‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,9 @@ export function registerChatHandlers(): void {
2929
history: Array<{ role: string; content: string }>;
3030
scene: string;
3131
authToken?: string;
32-
userApiKey?: string;
3332
},
3433
) => {
35-
const { requestId, sessionId, message, history, scene, authToken, userApiKey } = args;
34+
const { requestId, sessionId, message, history, scene, authToken } = args;
3635
const sender = event.sender;
3736
const startMs = Date.now();
3837

@@ -70,7 +69,7 @@ export function registerChatHandlers(): void {
7069
}
7170

7271
try {
73-
const stream = postJsonStream('/api/v1/ai/chat/stream', payload, authToken, userApiKey);
72+
const stream = postJsonStream('/api/v1/ai/chat/stream', payload, authToken);
7473

7574
for await (const chunk of stream) {
7675
fullResponse += chunk;

‎client/electron/ai/handlers/evaluateHandler.ts‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ function register(): void {
2727
concept: string;
2828
explanation: string;
2929
authToken?: string;
30-
userApiKey?: string;
3130
},
3231
) => {
3332
const startMs = Date.now();
@@ -83,7 +82,6 @@ function register(): void {
8382
reqBody,
8483
localHandler,
8584
args.authToken,
86-
args.userApiKey,
8785
40000,
8886
);
8987

‎client/electron/ai/handlers/feynmanHandler.ts‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ function register(): void {
3131
concept: string;
3232
explanation: string;
3333
authToken?: string;
34-
userApiKey?: string;
3534
},
3635
) => {
3736
const startMs = Date.now();
@@ -65,7 +64,6 @@ function register(): void {
6564
reqBody,
6665
localHandler,
6766
args.authToken,
68-
args.userApiKey,
6967
40000,
7068
);
7169

@@ -104,7 +102,6 @@ function register(): void {
104102
questions: string[];
105103
answers: string[];
106104
authToken?: string;
107-
userApiKey?: string;
108105
},
109106
) => {
110107
const startMs = Date.now();
@@ -142,7 +139,6 @@ function register(): void {
142139
reqBody,
143140
localHandler,
144141
args.authToken,
145-
args.userApiKey,
146142
40000,
147143
);
148144

‎client/electron/ai/handlers/flashcardHandler.ts‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ function register(): void {
2929
difficulty?: string;
3030
cardType?: string;
3131
authToken?: string;
32-
userApiKey?: string;
3332
},
3433
) => {
3534
const startMs = Date.now();
@@ -79,7 +78,6 @@ function register(): void {
7978
reqBody,
8079
localHandler,
8180
args.authToken,
82-
args.userApiKey,
8381
90000,
8482
);
8583

‎client/electron/ai/handlers/mergeNotesHandler.ts‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ function register(): void {
3030
duration?: number; // 秒
3131
language?: string;
3232
authToken?: string;
33-
userApiKey?: string;
3433
},
3534
) => {
3635
const startMs = Date.now();
@@ -105,7 +104,6 @@ function register(): void {
105104
reqBody,
106105
localHandler,
107106
args.authToken,
108-
args.userApiKey,
109107
30000, // 纯文本合并,30s 超时足够
110108
);
111109

0 commit comments

Comments
 (0)