Skip to content

Commit 9be9997

Browse files
committed
feat: v0.5.0 — 安全加固、效率分析、全局命令面板、灵感空间与上云部署
安全加固(Pre-Alpha.0): - JWT 认证中间件全面升级(auth.py) - 输入验证中间件(input_validation.py) - 频率限制中间件优化(rate_limit.py) - 离线 AI 队列(offlineAIQueue.ts)容错增强 效率分析与全局命令面板(Beta.1): - 新增效率分析图表(EfficiencyChart)与分析页面(AnalyticsPage) - 新增全局命令面板(CommandPalette + useCommandPalette) - 新增待办统计组件(TodoStats) - 成就提示组件(AchievementToast) 灵感空间(Beta.2): - 新增灵感空间功能模块(features/inspiration/) - 新增设置页面重构(pages/settings/) - 新增设置状态管理(useSettingsStore) AI 进阶能力: - 费曼反问链(feynman_question_chain.py + router) - 闪卡优化链(optimize_card_chain.py) - 内容标签链(tag_content_chain.py + router) - AIPluginLoader / routeDispatcher / LocalFallback 全面优化 前端交互优化: - 番茄钟 TimerRing 改进 - 笔记/闪卡/费曼页面交互增强 - 闪卡导入预览(ImportPreviewModal) - AppLayout / Navbar / Sidebar 布局优化 后端上云部署: - 新增 Docker 生产编排(docker-compose.prod.yml) - 新增 Nginx 反向代理配置(nginx/) - AI 网关 Dockerfile 优化 - sync-service Dockerfile 优化 - Redis 缓存、WebSocket、健康检查端点增强 - 数据模型层扩展(Schema v6)
1 parent 087c311 commit 9be9997

75 files changed

Lines changed: 5707 additions & 806 deletions

Some content is hidden

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

‎.env.example‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ VITE_SYNC_SERVER_URL=http://localhost:8080
1717

1818
# --- 后端 (server/) ---
1919

20+
# 应用环境(development/production)
21+
APP_ENV=development
22+
2023
# AI 模型 API Key(在对应平台注册获取)
2124
# 通义千问:https://dashscope.aliyun.com
2225
QWEN_API_KEY=sk-your-qwen-api-key
@@ -25,7 +28,7 @@ QWEN_API_KEY=sk-your-qwen-api-key
2528
DEEPSEEK_API_KEY=sk-your-deepseek-key
2629

2730
# 智谱 GLM(可选,作为备选模型):https://open.bigmodel.cn
28-
GLM_API_KEY=51bc3a84f5f54b52ba18fc171ed0aa6d.PIDW4gMS0kN72fKI
31+
GLM_API_KEY=sk-your-glm-api-key
2932

3033
# 数据库(Docker Compose 默认值,本地开发无需修改)
3134
DATABASE_URL=postgresql://keban:keban_dev@localhost:5432/keban
@@ -44,3 +47,14 @@ SUPABASE_URL=https://your-project-id.supabase.co
4447
# 服务端口
4548
SYNC_SERVICE_PORT=8080
4649
AI_GATEWAY_PORT=8000
50+
51+
# --- 生产部署配置 ---
52+
53+
# CORS 允许的前端域名(多个用逗号分隔)
54+
# CORS_ORIGINS=https://keban.app,https://www.keban.app
55+
56+
# 前端 API 基础 URL(生产环境设置为后端服务域名)
57+
# VITE_API_BASE_URL=https://api.keban.app
58+
59+
# 日志级别(DEBUG/INFO/WARNING/ERROR)
60+
# LOG_LEVEL=INFO

‎client/electron/main.ts‎

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,107 @@ ipcMain.handle(
439439
},
440440
);
441441

442+
/**
443+
* ai_feynman_question — POST /api/v1/ai/feynman-question
444+
*/
445+
ipcMain.handle(
446+
'ai_feynman_question',
447+
async (
448+
_event,
449+
args: {
450+
concept: string;
451+
explanation: string;
452+
authToken?: string;
453+
},
454+
) => {
455+
const startMs = Date.now();
456+
console.log(`[IPC] ai_feynman_question start, concept_length=${args.concept.length}`);
457+
const reqBody = {
458+
concept: args.concept,
459+
explanation: args.explanation,
460+
};
461+
462+
interface FeynmanQuestionResp {
463+
questions: Array<{ question: string; focus: string }>;
464+
model: string;
465+
tokens_used: number;
466+
latency_ms: number;
467+
}
468+
469+
const { data: resp, requestId } = await postJson<typeof reqBody, FeynmanQuestionResp>(
470+
'/api/v1/ai/feynman-question',
471+
reqBody,
472+
args.authToken,
473+
);
474+
475+
console.log(`[IPC] Request ID: ${requestId ?? 'N/A'}`);
476+
console.log(`[IPC] ai_feynman_question end, question_count=${resp.questions.length}`);
477+
return {
478+
questions: resp.questions.map((q) => ({
479+
question: q.question,
480+
focus: q.focus,
481+
})),
482+
model: resp.model,
483+
tokensUsed: resp.tokens_used,
484+
latencyMs: resp.latency_ms,
485+
requestId,
486+
};
487+
},
488+
);
489+
490+
/**
491+
* ai_feynman_evaluate_answers — POST /api/v1/ai/feynman-evaluate-answers
492+
*/
493+
ipcMain.handle(
494+
'ai_feynman_evaluate_answers',
495+
async (
496+
_event,
497+
args: {
498+
concept: string;
499+
questions: string[];
500+
answers: string[];
501+
authToken?: string;
502+
},
503+
) => {
504+
const startMs = Date.now();
505+
console.log(`[IPC] ai_feynman_evaluate_answers start, concept_length=${args.concept.length}`);
506+
const reqBody = {
507+
concept: args.concept,
508+
questions: args.questions,
509+
answers: args.answers,
510+
};
511+
512+
interface FeynmanAnswerEvalResp {
513+
understanding_score: number;
514+
feedback: string;
515+
strong_points: string[];
516+
weak_points: string[];
517+
model: string;
518+
tokens_used: number;
519+
latency_ms: number;
520+
}
521+
522+
const { data: resp, requestId } = await postJson<typeof reqBody, FeynmanAnswerEvalResp>(
523+
'/api/v1/ai/feynman-evaluate-answers',
524+
reqBody,
525+
args.authToken,
526+
);
527+
528+
console.log(`[IPC] Request ID: ${requestId ?? 'N/A'}`);
529+
console.log(`[IPC] ai_feynman_evaluate_answers end, score=${resp.understanding_score}`);
530+
return {
531+
understandingScore: resp.understanding_score,
532+
feedback: resp.feedback,
533+
strongPoints: resp.strong_points,
534+
weakPoints: resp.weak_points,
535+
model: resp.model,
536+
tokensUsed: resp.tokens_used,
537+
latencyMs: resp.latency_ms,
538+
requestId,
539+
};
540+
},
541+
);
542+
442543
// ================================================================
443544
// IPC Handler — 屏幕截图采集
444545
// ================================================================

‎client/electron/preload.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ const ALLOWED_CHANNELS = [
1313
'ai_generate_cards',
1414
'ai_evaluate',
1515
'ai_recommend_duration',
16+
'ai_feynman_question',
17+
'ai_feynman_evaluate_answers',
1618
'screen_list_windows',
1719
'screen_capture_start',
1820
'screen_capture_stop',

‎client/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "client",
33
"private": true,
4-
"version": "0.4.0-alpha.1",
4+
"version": "0.5.0",
55
"description": "课伴 - 跨端学习平台",
66
"author": "KeBan Team",
77
"type": "module",

‎client/src/App.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { RouterProvider } from 'react-router-dom';
22
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
33
import { ToastProvider } from '@/components/ui/Toast';
4+
import AchievementToast from '@/components/ui/AchievementToast';
45
import { AuthProvider } from '@/lib/auth/AuthContext';
56
import { SyncProvider } from '@/lib/sync/SyncContext';
67
import { router } from '@/routes';
@@ -25,6 +26,7 @@ function App() {
2526
<SyncProvider>
2627
<ToastProvider>
2728
<RouterProvider router={router} />
29+
<AchievementToast />
2830
</ToastProvider>
2931
</SyncProvider>
3032
</AuthProvider>

‎client/src/components/layout/AppLayout.tsx‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { Outlet, useLocation } from 'react-router-dom';
33
import Sidebar from './Sidebar';
44
import BottomNav from './BottomNav';
55
import Navbar from './Navbar';
6-
import SyncStatusBar from '@/components/sync/SyncStatusBar';
6+
import CommandPalette from '../ui/CommandPalette';
7+
import SyncStatusBar from '../sync/SyncStatusBar';
78

89
const DENSITY_KEY = 'keban-density';
910

@@ -44,9 +45,6 @@ export default function AppLayout() {
4445

4546
{/* Main content area */}
4647
<div className="flex-1 flex flex-col min-h-screen">
47-
{/* Sync/network status bar */}
48-
<SyncStatusBar />
49-
5048
{/* Top navbar */}
5149
<Navbar />
5250

@@ -56,6 +54,12 @@ export default function AppLayout() {
5654
</main>
5755
</div>
5856

57+
{/* 同步状态栏 */}
58+
<SyncStatusBar />
59+
60+
{/* 全局命令面板 */}
61+
<CommandPalette />
62+
5963
{/* Mobile bottom nav */}
6064
<BottomNav />
6165
</div>

‎client/src/components/layout/Navbar.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const modeIndicatorConfig = {
2929
} as const;
3030

3131
/** 根路由(无返回按钮) */
32-
const ROOT_PATHS = new Set(['/', '/pomodoro', '/notes', '/flashcards', '/feynman', '/settings']);
32+
const ROOT_PATHS = new Set(['/', '/pomodoro', '/notes', '/flashcards', '/feynman', '/settings', '/analytics', '/inspiration']);
3333

3434
export default function Navbar() {
3535
const { theme, toggleTheme } = useTheme();

‎client/src/components/layout/Sidebar.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useState } from 'react';
22
import { NavLink, useNavigate, useLocation } from 'react-router-dom';
3-
import { Home, Timer, FileText, Layers, Lightbulb, Settings, ChevronLeft, ChevronRight, MessageSquare, Clapperboard } from 'lucide-react';
3+
import { Home, Timer, FileText, Layers, Lightbulb, Settings, ChevronLeft, ChevronRight, MessageSquare, Clapperboard, BarChart3, Sparkles } from 'lucide-react';
44
import { cn } from '@/lib/utils';
55
import { useSidebarStore } from '@/stores/useSidebarStore';
66
import { useCaptureStore } from '@/stores/useCaptureStore';
@@ -11,6 +11,8 @@ const navItems = [
1111
{ to: '/notes', label: '笔记', icon: FileText },
1212
{ to: '/flashcards', label: '闪卡', icon: Layers },
1313
{ to: '/feynman', label: '费曼', icon: Lightbulb },
14+
{ to: '/analytics', label: '效率分析', icon: BarChart3 },
15+
{ to: '/inspiration', label: '灵感空间', icon: Sparkles },
1416
];
1517

1618
export default function Sidebar() {
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { useEffect, useState, useCallback, useRef } from 'react';
2+
import {
3+
Timer, Layers, Lightbulb, FileText, Flame, Trophy, Medal,
4+
type LucideIcon,
5+
} from 'lucide-react';
6+
import type { Achievement } from '@/types/models';
7+
8+
const ICON_MAP: Record<string, LucideIcon> = {
9+
Timer, Layers, Lightbulb, FileText, Flame, Trophy, Medal,
10+
};
11+
12+
// 8 particle directions (angle in degrees)
13+
const PARTICLE_DIRS = [0, 45, 90, 135, 180, 225, 270, 315];
14+
const PARTICLE_DIST = 28; // px
15+
16+
interface ToastEntry extends Achievement {
17+
localId: number;
18+
exiting: boolean;
19+
}
20+
21+
export default function AchievementToast() {
22+
const [toasts, setToasts] = useState<ToastEntry[]>([]);
23+
const idRef = useRef(0);
24+
25+
const dismiss = useCallback((localId: number) => {
26+
setToasts((prev) => prev.map((t) => (t.localId === localId ? { ...t, exiting: true } : t)));
27+
setTimeout(() => {
28+
setToasts((prev) => prev.filter((t) => t.localId !== localId));
29+
}, 300);
30+
}, []);
31+
32+
useEffect(() => {
33+
const handler = (e: Event) => {
34+
const a = (e as CustomEvent<Achievement>).detail;
35+
if (!a) return;
36+
const localId = ++idRef.current;
37+
setToasts((prev) => [...prev, { ...a, localId, exiting: false }]);
38+
// Auto-dismiss after 4s
39+
setTimeout(() => dismiss(localId), 4000);
40+
};
41+
window.addEventListener('achievement-unlocked', handler);
42+
return () => window.removeEventListener('achievement-unlocked', handler);
43+
}, [dismiss]);
44+
45+
if (toasts.length === 0) return null;
46+
47+
return (
48+
<div className="fixed bottom-6 right-6 z-[60] flex flex-col gap-3 items-end pointer-events-none">
49+
{toasts.map((t) => {
50+
const Icon = ICON_MAP[t.icon] ?? Trophy;
51+
return (
52+
<div
53+
key={t.localId}
54+
className={[
55+
'pointer-events-auto relative flex items-center gap-3 px-4 py-3',
56+
'bg-bg-elevated/95 backdrop-blur-md rounded-kb-lg shadow-kb-lg',
57+
'border border-brand-500/30',
58+
'min-w-[240px] max-w-xs',
59+
t.exiting
60+
? 'opacity-0 translate-x-4 transition-all duration-300'
61+
: 'kb-achievement-toast',
62+
].join(' ')}
63+
>
64+
{/* Icon with particles */}
65+
<div className="relative flex-shrink-0">
66+
<div className="w-10 h-10 rounded-kb-lg bg-brand-500/15 flex items-center justify-center">
67+
<Icon className="w-5 h-5 text-brand-500" strokeWidth={1.5} />
68+
</div>
69+
{/* Particles */}
70+
{!t.exiting &&
71+
PARTICLE_DIRS.map((deg, i) => {
72+
const rad = (deg * Math.PI) / 180;
73+
const tx = Math.round(Math.cos(rad) * PARTICLE_DIST);
74+
const ty = Math.round(Math.sin(rad) * PARTICLE_DIST);
75+
return (
76+
<span
77+
key={i}
78+
className="kb-particle"
79+
style={{
80+
left: '50%',
81+
top: '50%',
82+
marginLeft: -2,
83+
marginTop: -2,
84+
// @ts-expect-error custom CSS properties
85+
'--kb-tx': `${tx}px`,
86+
'--kb-ty': `${ty}px`,
87+
animationDelay: `${i * 60}ms`,
88+
}}
89+
/>
90+
);
91+
})}
92+
</div>
93+
94+
{/* Text */}
95+
<div className="flex flex-col min-w-0">
96+
<span className="text-c1 text-brand-500 font-medium uppercase tracking-wide">
97+
成就解锁
98+
</span>
99+
<span className="text-b2 font-semibold text-text-primary truncate">
100+
{t.title}
101+
</span>
102+
<span className="text-c1 text-text-tertiary truncate">
103+
{t.description}
104+
</span>
105+
</div>
106+
107+
{/* Close */}
108+
<button
109+
onClick={() => dismiss(t.localId)}
110+
className="absolute top-1.5 right-1.5 p-1 rounded-kb-full text-text-tertiary hover:text-text-primary hover:bg-bg-tertiary transition-all"
111+
>
112+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
113+
<path d="M3 3l6 6M9 3l-6 6" />
114+
</svg>
115+
</button>
116+
</div>
117+
);
118+
})}
119+
</div>
120+
);
121+
}

0 commit comments

Comments
 (0)