本文档面向所有参与灵犀学院开发的 AI 协作者(Trae/Claude/Cursor 等),规定了项目约定、代码规范与安全红线。阅读本文档后再开始任何代码修改。
⚠️ 安全红线章节(见下文)为强制约束,违反将导致密钥泄露等严重后果,任何修改都必须严格遵守。
- 项目名称:灵犀学院(Lingxi Academy,包名
lingxi_academy) - 定位:引导式 AI 学习应用,面向 AI 编程与 AI 应用方向的初学者与进阶学习者,提供结构化课程、苏格拉底式对话、笔记、成就与连续学习激励。
- 目标用户:自学者、学生、对 AI 应用开发感兴趣的开发者。
- 核心理念:
- 非商业化:项目不以盈利为目的,不内置任何付费墙或广告。
- 用户自备 API:应用本身不分发 AI 服务,用户需自行在设置中配置自己的 API Key(OpenAI 兼容 / Anthropic / Gemini / Ollama)。API Key 仅本地加密存储,绝不上传。
- 双端支持:Android + Windows。不包含 iOS / Linux / macOS,提交 PR 时请不要引入仅 iOS/Linux/macOS 可用的依赖或插件。
本节记录各版本的核心交付,新增 PR 时请避免重复实现已交付的能力。
| 版本 | 发布日期 | 核心交付 |
|---|---|---|
| v0.1.0 | 2026-07-15 | 初始版本:基础功能闭环(课程学习、自由对话、笔记、成就与连续学习激励),完成项目骨架搭建 |
| v0.2.0 | 2026-07-20 | 美术与动画全面优化:LingxiGradients.dark 双主题渐变对齐、_MascotPainter 6 状态精细化绘制、SpringMotion 6 档弹簧参数对齐 Material 3 规范、ChatController 流式 50ms 动态节流 |
| v0.3.0 | 2026-07-23 | 打磨·测试·发布:Hero 共享元素动画(MascotHero + mascotHeroFlightShuttleBuilder)、按压微交互(LingxiButton scale 0.96 / LingxiCard scale 0.99 / LingxiChip AnimatedSwitcher)、SpringMotion.fastSpeed 时长修复(151ms → 148ms ≤ 150ms)、列表滚动优化(cacheExtent: 500 + RepaintBoundary)、PageView 手感升级(BouncingScrollPhysics + reduceMotion 按钮降级)、GoRouter 过渡统一(slideFadeTransitionBuilder + SpringMotion.entranceCurve)、新增 149 个测试用例 |
| 项 | 要求 |
|---|---|
| Flutter SDK | 3.44.4(推荐),兼容 pubspec 中 sdk: '>=3.10.0 <4.0.0' |
| Dart SDK | 3.12.2(随 Flutter 3.44.4 自带) |
| 开发工具 | Trae / VS Code / Android Studio(任选其一,需安装 Flutter 与 Dart 插件) |
| Android | minSdkVersion 24(见 pubspec.yaml 中 flutter_launcher_icons.min_sdk_android) |
| Windows | Windows 10 及以上,需启用开发者模式 |
- 开发环境(本机):
C:\Users\23501\AppData\Local\Temp\flutter\bin\flutter.bat - CI 环境:直接使用
flutter(假定已加入 PATH)
下文命令速查中,开发环境示例统一写作 $ flutter,实际使用时替换为上述完整路径或确保 flutter 已在 PATH 中。
以下版本号严格取自 pubspec.yaml,修改依赖时务必同步更新本节。
| 依赖 | 版本 | 用途 |
|---|---|---|
flutter_riverpod |
^2.5.1 | 状态管理(主框架) |
go_router |
^14.2.0 | 声明式路由 |
drift |
^2.18.0 | 类型安全 SQLite ORM |
sqlite3_flutter_libs |
^0.5.24 | 桌面端 SQLite FFI |
path_provider |
^2.1.3 | 跨端文件路径 |
path |
^1.9.0 | 路径处理 |
sqflite |
^2.3.3+1 | Android 端 SQLite |
uuid |
^4.4.0 | UUID v4 主键生成 |
flutter_secure_storage |
^9.2.2 | API Key 加密存储 |
dio |
^5.4.3+1 | HTTP 客户端 |
rive |
^0.13.13 | 吉祥物动画(目标方案) |
flutter_markdown |
^0.7.2+1 | Markdown 渲染 |
flutter_math_fork |
^0.7.2 | 数学公式渲染 |
markdown |
^7.2.0 | Markdown 解析 |
google_fonts |
^6.2.1 | 字体(Noto Sans SC + Quicksand) |
shared_preferences |
^2.2.3 | KV 偏好存储 |
flutter_localizations |
sdk | 本地化委托 |
| 依赖 | 版本 | 用途 |
|---|---|---|
flutter_lints |
^4.0.0 | Lint 规则集 |
build_runner |
^2.4.11 | 代码生成运行器 |
drift_dev |
^2.18.0 | Drift 代码生成 |
json_serializable |
^6.8.0 | JSON 序列化代码生成 |
flutter_launcher_icons |
^0.13.1 | 应用图标生成 |
flutter_native_splash |
^2.4.1 | 启动屏生成 |
http_mock_adapter |
^0.6.1 | Dio HTTP mock(测试用) |
fake_async |
^1.3.1 | 测试用虚拟时间(fake_async) |
- 使用
^语义化版本范围,允许 patch/minor 升级,禁止 major 自动升级。 - 升级依赖前先在分支上运行完整
flutter test,确认无回归。 pubspec.lock提交到仓库,保证团队与 CI 环境依赖一致。- 不要引入未在
pubspec.yaml中声明的新依赖而不更新本节。
lib/
├── main.dart # 应用入口(初始化 SharedPreferences → ProviderScope)
├── app.dart # LingxiApp(MaterialApp.router 根 Widget)
├── core/ # 核心层:与业务无关的基础设施
│ ├── constants/ # 常量(app_constants.dart)
│ ├── motion/ # 动画曲线(spring_motion.dart)
│ ├── providers/ # 全局 Provider(app_providers.dart)
│ ├── router/ # 路由(app_router.dart, route_names.dart)
│ └── theme/ # 主题(app_theme.dart, lingxi_colors.dart, lingxi_gradients.dart, lingxi_elevations.dart, shape_variants.dart)
├── data/ # 数据层:本地持久化与数据模型
│ ├── db/ # Drift 数据库(database.dart, connection.dart, secure_database.dart)
│ ├── models/ # 纯数据模型(provider_config.dart, course_content.dart)
│ ├── providers/ # 数据层 Provider 注册(db_providers.dart, storage_providers.dart, course_providers.dart)
│ ├── repositories/ # 仓库(每张表一个 Repository)
│ └── services/ # 数据服务(secure_storage_service.dart)
├── features/ # 功能层:按业务模块组织
│ ├── achievements/ # 成就页
│ ├── ai/ # AI Provider 抽象与实现、SSE、安全日志、提示词管理
│ ├── chat/ # 对话列表与对话页
│ ├── help/ # 帮助中心
│ ├── home/ # 首页(含 empty_states 子目录)
│ ├── learning/ # 学习路径与课时(含 widgets 子目录)
│ ├── mascot/ # 吉祥物(controller, state, widget, overlay, rive)
│ ├── notes/ # 笔记列表与编辑
│ ├── onboarding/ # 引导与 API 配置向导
│ ├── progress/ # 进度统计、成就服务、连续学习服务
│ ├── recommendation/ # 学习推荐引擎(基于学习历史与进度)
│ └── settings/ # 设置、API 设置、数据导出、Provider 编辑
└── shared/ # 共享层:跨 feature 复用
├── utils/ # 工具(responsive.dart, misconception_parser.dart)
└── widgets/ # 通用组件(lingxi_card, lingxi_button, lingxi_app_bar 等)
| 层 | 职责 | 依赖方向 |
|---|---|---|
core/ |
主题、路由、全局 Provider、常量、动画曲线。不含业务逻辑。 | 只依赖 Flutter/第三方库 |
data/ |
Drift 表定义、Repository、数据模型、SecureStorage。只做数据 CRUD,不做业务编排。 | 可依赖 core/ |
features/ |
业务模块。Page + Controller + 该模块专属 Widget。业务编排在此。 | 可依赖 core/、data/、shared/ |
shared/ |
跨 feature 复用的纯 UI 组件与工具函数。不依赖任何 feature。 | 可依赖 core/ |
- 新页面:
lib/features/<feature>/<feature>_page.dart - 新表/数据:
lib/data/db/database.dart加表 +lib/data/repositories/加 Repository +lib/data/providers/db_providers.dart注册 Provider - 新全局配置:
lib/core/providers/app_providers.dart - 新路由:
lib/core/router/route_names.dart加常量 +lib/core/router/app_router.dart加 GoRoute - 新通用组件:
lib/shared/widgets/ - 新工具函数:
lib/shared/utils/
lib/core/theme/ 由 5 个文件协同构成完整的双主题(light/dark)系统,所有视觉令牌(颜色 / 渐变 / 阴影 / 形状)均通过 ThemeExtension 注入 ThemeData.extensions,UI 层统一通过 BuildContext 扩展获取,禁止硬编码颜色值。
| 文件 | 职责 |
|---|---|
app_theme.dart |
AppTheme 静态类,提供 lightTheme / darkTheme,注册 LingxiColors / LingxiGradients / LingxiElevations 三组 ThemeExtension(按 brightness 三元切换 light/dark 实例) |
lingxi_colors.dart |
LingxiColors extends ThemeExtension<LingxiColors>,6 个语义色(mascotPrimary 星空紫 / streakFire 火焰红 / achievementGold 成就金 / misconceptionRed 误解红 / successGreen 成功绿 / infoBlue 信息蓝),light/dark 双实例,dark 实例校准 streakFire(0xFFFF7043→0xFFFF8A65)与 achievementGold(0xFFFFE082→0xFFFFD54F)以满足 WCAG AA 对比度 |
lingxi_gradients.dart |
LingxiGradients extends ThemeExtension<LingxiGradients>,6 个语义渐变(mascotHero 吉祥物主光 / streakFire 火焰 / achievementGold 成就金 / primarySurface 主色面 / celebration 庆祝 / success 成功),light/dark 双实例 |
lingxi_elevations.dart |
LingxiElevations extends ThemeExtension<LingxiElevations>,3 档语义阴影 subtle / elevated / highlighted(light/dark 双实例),同时保留 level0~level4 兼容旧调用 |
shape_variants.dart |
形状变体定义 |
// ✅ 正确:通过 context 扩展获取
final colors = context.lingxiColors;
final gradients = context.lingxiGradients;
final elevations = context.lingxiElevations;
// ❌ 错误:硬编码颜色
Container(color: Color(0xFF6750A4));
// ❌ 错误:直接引用静态实例(不随主题切换)
LingxiColors.light.mascotPrimary;lib/shared/widgets/lingxi_card.dart 的 elevation 参数映射到 LingxiElevations:
| elevation 值 | 对应阴影 | 场景 |
|---|---|---|
| 0 | subtle |
默认卡片 |
| 1 | elevated |
浮起卡片(hover / 选中) |
| 2 | highlighted |
高亮卡片(hero 区) |
实现上移除 AnimatedPhysicalModel,改用 BoxDecoration.boxShadow 以确保圆角与阴影在 Web/桌面端一致。
- 在对应
ThemeExtension类(LingxiColors/LingxiGradients/LingxiElevations)添加字段。 - 同时更新
light与dark两个静态实例,dark 实例必须满足 WCAG AA 对比度(与背景对比度 ≥ 4.5:1)。 - 在
lerp与copyWith中处理新字段。 - 如需
BuildContext扩展,在对应文件底部添加extension on BuildContext。 - 不要在业务代码中硬编码颜色值,统一走 ThemeExtension。
| 类型 | 约定 | 示例 |
|---|---|---|
| 文件名 | snake_case | chat_controller.dart、api_settings_page.dart |
| 类名 | PascalCase | ChatController、ApiSettingsPage |
| 变量/函数 | camelCase | sendMessage()、currentAssistantText |
| 常量 | camelCase 或 k 前缀 |
kAppName、kRedactedPlaceholder、defaultLocale |
| 枚举值 | camelCase | MascotMood.thinking、ProviderType.openaiCompatible |
| Provider | xxxProvider / xxxServiceProvider |
chatControllerProvider、secureStorageServiceProvider |
| Repository | XxxRepository |
ConversationRepository、NoteRepository |
| Page | XxxPage |
ChatPage、LearningPathPage |
| Widget | XxxWidget / XxxCard / XxxButton / XxxAppBar |
LingxiCard、LingxiButton、LingxiAppBar |
| Controller(StateNotifier) | XxxController + XxxControllerState |
ChatController + ChatControllerState |
| 路由路径 | /kebab-case |
/onboarding/api-setup |
| 路由 name | camelCase,通过 RouteNames.xxx 引用 |
RouteNames.apiSetup、RouteNames.noteEditor |
项目使用 flutter_riverpod 2.5.x,所有跨组件状态必须通过 Riverpod 管理,禁止使用 setState 管理全局状态(局部一次性 UI 状态除外)。
| 类型 | 适用场景 | 项目示例 |
|---|---|---|
Provider |
无状态的依赖/服务/单例 | databaseProvider、secureStorageServiceProvider、goRouterProvider |
StateProvider |
简单可变状态(一个值的 getter/setter) | themeModeProvider、localeProvider、socraticModeProvider |
FutureProvider |
异步一次性数据(加载完不变) | currentAiProviderProvider、promptManagerProvider |
StreamProvider |
持续推送的数据流 | (暂未使用,监听 Drift watch() 时可用) |
StateNotifierProvider |
复杂状态机、需要封装业务方法 | chatControllerProvider、mascotControllerProvider |
NotifierProvider |
新版 Notifier(项目目前以 StateNotifier 为主) | (暂未使用) |
- 当前项目未广泛使用
autoDispose。Repository/Service/数据库连接等单例 Provider 不要加 autoDispose。 - 仅在以下场景考虑
autoDispose:- 列表页的临时搜索结果 Provider
- 编辑页的临时表单状态 Provider
currentAiProviderProvider等 FutureProvider 可考虑加.autoDispose,但当前实现依赖invalidate重建,不要随意修改。
// 典型模式:Provider 依赖其他 Provider
final conversationRepositoryProvider = Provider<ConversationRepository>((ref) {
return ConversationRepository(ref.watch(databaseProvider));
});
// 典型模式:StateNotifierProvider 持有 Ref 以读取其他 Provider
class ChatController extends StateNotifier<ChatControllerState> {
ChatController(this._ref) : super(const ChatControllerState());
final Ref _ref;
// 内部通过 _ref.read(xxxProvider) 按需获取依赖
}| 选择 | 场景 |
|---|---|
Consumer |
在 StatelessWidget 内局部订阅(如 _AppShell 中嵌入) |
ConsumerWidget |
整个页面只需要 ref,无生命周期(多数 Page) |
ConsumerStatefulWidget |
需要 initState/dispose 等生命周期 + ref(如 LessonPage 加载课程) |
ref.watch:在build方法内订阅,状态变化时重建 UI。ref.read:在回调、事件处理、Controller 方法内一次性读取,不触发重建。- 禁止在
build内使用ref.read监听响应式数据。
// ✅ 正确
final themeMode = ref.watch(themeModeProvider);
// ✅ 正确(一次性读取)
Future<void> send() async {
final repo = ref.read(conversationRepositoryProvider);
await repo.createConversation('新对话');
}
// ❌ 错误(在 build 内 read 响应式数据)
Widget build(BuildContext context, WidgetRef ref) {
final socratic = ref.read(socraticModeProvider); // 不会随状态变化重建
...
}- 路由路径与 name 常量:
lib/core/router/route_names.dart中的RouteNames类。 - 路由配置(GoRoute 树):
lib/core/router/app_router.dart中的goRouterProvider。
- 在
RouteNames添加name与path常量(两者成对出现):static const String courseDetail = 'courseDetail'; static const String courseDetailPath = '/course/:courseId';
- 在
goRouterProvider的routes数组中添加GoRoute,引用常量而非硬编码字符串。 - 嵌套子路由放在父
GoRoute的routes字段下(如/learning/:courseId/:lessonId)。 - 跳转使用
context.go(path)(替换栈)或context.push(path)(入栈),优先用context.namedLocation(RouteNames.xxx)。
- 路径参数:通过
state.pathParameters['xxx']读取(如courseId、lessonId、conversationId、noteId)。 - 查询参数:通过
state.uri.queryParameters['xxx']读取。 - 对象传递:复杂对象不通过路由传递,改为通过 Provider 共享(如
chatControllerProvider.loadConversation(id))。
goRouterProvider 中的 redirect 实现 onboarding 引导:
- 读取
SharedPreferences的onboarding_completed标志。 - 未完成引导时:除
/onboarding、/onboarding/api-setup、/settings/api外,全部重定向到/onboarding。 - 已完成引导但访问
/onboarding或/onboarding/api-setup时:重定向回/home。 - 修改此逻辑时务必覆盖测试(见
test/onboarding_page_test.dart)。
- 主导航页面(首页、学习、对话、笔记、成就、统计、设置、API 设置、帮助)嵌套在
ShellRoute下,由_AppShell提供导航壳。 _AppShell根据屏幕宽度(≥1024)切换NavigationRail(桌面)与NavigationBar(移动)。- onboarding 与 api-setup 不在 ShellRoute 内,无导航壳。
Table(database.dart) → Repository(repositories/) → Provider(db_providers.dart)
Drift 表定义 封装 CRUD Riverpod 注册
- Table:在
lib/data/db/database.dart中以class XxxTable extends Table定义,主键统一用 UUID v4(clientDefault(_uuid))。 - Repository:每个表对应一个 Repository 类,构造函数接收
LingxiDatabase,只暴露业务语义化方法,不直接暴露db.select/update。 - Provider:在
lib/data/providers/db_providers.dart注册,依赖databaseProvider单例。
| 表 | 用途 |
|---|---|
Conversations |
对话元信息(标题、模型、时间戳) |
Messages |
对话消息(user/assistant/system,含 tokens) |
Notes |
笔记(可关联 conversationId/courseId/lessonId) |
Progress |
学习进度(status: not_started/in_progress/completed,score) |
ApiKeys |
API Key 元数据(仅元数据,真实 Key 不入库,存 SecureStorage) |
Settings |
通用 KV 设置 |
Achievements |
成就解锁记录 |
Streaks |
连续学习天数(单行表) |
- 在
database.dart添加class XxxTable extends Table,主键用clientDefault(_uuid)。 - 在
@DriftDatabase(tables: [...])注解中添加新表。 - 在
LingxiDatabase类内可通过_$LingxiDatabase生成的访问器访问。 - 在
repositories/添加XxxRepository。 - 在
db_providers.dart注册xxxRepositoryProvider。 - 必须:递增
schemaVersion并在migration.onUpgrade中添加迁移代码(见下)。 - 运行
flutter pub run build_runner build --delete-conflicting-outputs重新生成database.g.dart。
@override
int get schemaVersion => 3; // 每次表结构变更递增(以 database.dart 实际值为准)
@override
MigrationStrategy get migration => MigrationStrategy(
onCreate: (m) async => await m.createAll(),
onUpgrade: (m, from, to) async {
// 示例:if (from < 2) { await m.addColumn(...); }
// 当前 v3,按版本号顺序执行迁移
},
beforeOpen: (details) async {
await customStatement('PRAGMA foreign_keys = ON');
},
);- 必须递增
schemaVersion,否则旧用户升级后新表不会创建。 onUpgrade中按版本号顺序执行迁移,不可破坏已有数据。
@override
DriftDatabaseOptions get options =>
const DriftDatabaseOptions(storeDateTimeAsText: true);- 统一使用
storeDateTimeAsText: true,以 ISO8601 文本存储,保留毫秒精度。 - 不要修改此选项,否则会导致旧数据时间戳解析错误。
lib/data/db/connection.dart 中 openConnection():
- 数据库文件:
getApplicationDocumentsDirectory()/lingxi_academy.db - Android:使用
sqflite;旧版本(<7.0)调用applyWorkaroundToOpenSqlite3OnOldAndroidVersions()。 - 桌面(Windows):使用
sqlite3_flutter_libs提供的 FFI。 - 通过
NativeDatabase.createInBackground在后台 isolate 打开,避免阻塞 UI。 - 生产环境关闭 SQL 日志(
logStatements: false)。
factory LingxiDatabase.forTesting(QueryExecutor e) => LingxiDatabase(e);
// 测试中:NativeDatabase.memory()| ProviderType | 实现类 | 默认 baseUrl | 默认模型 |
|---|---|---|---|
openaiCompatible |
OpenAICompatibleProvider |
https://api.openai.com/v1 |
gpt-4o-mini |
anthropic |
AnthropicProvider |
https://api.anthropic.com |
claude-3-5-sonnet-20241022 |
gemini |
GeminiProvider |
https://generativelanguage.googleapis.com |
gemini-1.5-flash |
ollama |
OllamaProvider |
http://localhost:11434 |
llama3.2 |
-
实现
AiProvider抽象接口(lib/features/ai/ai_provider.dart):chatStream({required List<ChatMessage> messages, required ChatOptions options})→Stream<AiStreamEvent>chat({...})→Future<String>(内部聚合chatStream)cancel()→ 取消当前请求testConnection()→Future<bool>
-
在
ProviderType枚举添加类型(lib/data/models/provider_config.dart):deepseek( 'deepseek', 'DeepSeek', 'https://api.deepseek.com/v1', 'deepseek-chat', ),
枚举已通过 enhanced enum 持有
defaultBaseUrl与defaultModel属性,无需额外维护静态 Map。 -
在
AiProviderRegistry._createProvider注册(lib/features/ai/ai_provider_registry.dart):case ProviderType.deepseek: return DeepSeekProvider( baseUrl: config.baseUrl, apiKey: config.apiKey, model: config.model, );
-
在
ProviderEditDialog添加表单(lib/features/settings/provider_edit_dialog.dart):为新类型提供 baseUrl/model/温度等输入项。 -
API Key 处理:构造 Provider 时传入
config.apiKey(来自 SecureStorage),不要在 Provider 内持久化或日志输出 Key。
- 统一使用
lib/features/ai/sse_transformer.dart中的SseTransformer解析 SSE 流。 SseTransformer.parse(byteStream)接收Stream<List<int>>,输出Stream<String>(每个元素是一个完整事件的 data 内容)。- 遵循 W3C SSE 规范:按行分割、
data:前缀提取、空行触发 emit、:开头为注释。 - NDJSON 风格(Ollama):在 Provider 内部按行分割后逐行 JSON 解码。
- 所有流必须以
DoneEvent(正常完成)或ErrorEvent(出错)终止;被cancel时静默结束不抛异常。
- 网络错误、HTTP 非 2xx、JSON 解析失败 → emit
ErrorEvent(message)。 - 鉴权失败(401/403)→ emit
ErrorEvent,message 提示用户检查 API Key。 ChatController监听到ErrorEvent后调用_finishStreamingWithError,设置state.error并联动吉祥物sad状态。- 不要在 Provider 内部
print错误,统一通过ErrorEvent上抛。
定义在 lib/features/mascot/mascot_state.dart:
| 状态 | 含义 | 触发场景 |
|---|---|---|
idle |
待机(眨眼、轻微摇摆) | 默认状态 |
happy |
开心(跳跃、微笑) | 用户点击吉祥物 |
thinking |
思考(托腮、问号) | AI 流式响应中 |
sad |
难过(低头、泪滴) | AI 出错 |
celebrate |
庆祝(欢呼、星星) | AI 完成回复 / 连续点击 5 次彩蛋 |
curious |
好奇(歪头、放大镜) | 进入新页面(预留) |
- 在页面顶部或合适位置嵌入
MascotWidget(或MascotOverlay)。 - 通过
ref.watch(mascotControllerProvider)订阅状态。 - 通过
ref.read(mascotControllerProvider.notifier).setMood(...)切换情绪。 - 不要在页面内自行管理吉祥物状态,统一走
mascotControllerProvider。
- AI 思考中 →
setAiThinking(true)→ 切换为thinking。 - AI 完成 →
celebrate()→ 切换为celebrate,3 秒后自动恢复idle。 - AI 出错 →
setMood(MascotMood.sad)。 - 用户点击 →
triggerTap():单次点击切happy1.5 秒后恢复;2 秒内连续 5 次触发celebrate彩蛋。
参考实现:lib/features/chat/chat_controller.dart 中的 sendMessage / _commitAssistant / _finishStreamingWithError。
- 2 秒内连续点击 5 次 → 触发庆祝彩蛋(
celebrate持续 3 秒)。 - 单次点击 →
happy持续 1.5 秒后恢复idle。 mounted检查:Future.delayed回调中必须判断mounted,避免 Controller 已销毁后修改状态。
🚨 本章节为强制约束。任何修改若违反以下任一条,PR 将被直接拒绝。
- API Key 只能通过
SecureStorageService存储(底层flutter_secure_storage)。 - 绝不将 API Key 写入 Drift 数据库、
SharedPreferences、文件、日志、导出 JSON。 ApiKeys表只存元数据(providerType、baseUrl、model、temperature、maxTokens、enabled),不含 apiKey 字段。ProviderConfig.apiKey字段仅在内存中持有,toJson()必须跳过 apiKey(见lib/data/models/provider_config.dart第 94-103 行)。- 构造
AiProvider时从ProviderConfigRepository(内部读 SecureStorage)获取 apiKey 传入。
- 所有 dio 请求必须经过
SecureLogInterceptor(lib/features/ai/secure_log_interceptor.dart)。 - 自动脱敏的请求头:
authorization、x-api-key、x-goog-api-key→ 替换为[REDACTED]。 - 自动脱敏的查询参数:
key、api_key→ 替换为[REDACTED]。 - 自动脱敏的请求体字段:
api_key、apikey、apiKey、key→ 替换为[REDACTED]。 - URL 中的查询参数也会被脱敏(
redactUrl)。 - 禁止绕过
SecureLogInterceptor直接print请求/响应内容。 - 使用
debugPrint而非print(lint 规则avoid_print已启用)。
DataExportService.exportAll()中ProviderConfig.toJson()必须不含 apiKey。- 代码中已有
assert(!json.containsKey('apiKey'))断言,不要移除。 - 导入时
copyWith(apiKey: '')强制清空 apiKey(导出文件本身不含 apiKey)。 - 修改
ProviderConfig.toJson时必须保持不含 apiKey 字段。
当前 .gitignore 已包含:
*.keystore
*.jks
*.env
config.json
config.local.json
android/key.properties
macos/Runner/*.entitlements.priv
*.secure
- 禁止移除以上条目。
- 新增包含敏感信息的文件时,同步在
.gitignore添加规则。
- 提交 Issue/PR 时,截图前确认无 API Key 残留(设置页、日志、调试控制台)。
- 错误报告中若包含请求/响应,确保已通过
SecureLogInterceptor脱敏。 - 不要在 Issue/PR 描述/commit message 中粘贴真实 API Key。
test/
├── data/
│ ├── repositories/ # Repository 测试
│ └── services/ # 服务测试(secure_storage_test.dart)
├── features/
│ └── ai/ # AI Provider 测试(providers_test.dart, sse_transformer_test.dart)
├── shared/
│ └── utils/ # 工具函数测试
├── chat_controller_test.dart # Controller 测试(顶层)
├── data_export_test.dart # 导出服务测试
├── secure_log_interceptor_test.dart
├── streak_service_test.dart
└── widget_test.dart # Widget 测试
- 文件名:
xxx_test.dart(与被测文件同名加_test后缀)。 - 单元测试:
test('描述', () { ... })。 - Widget 测试:
testWidgets('描述', (tester) async { ... })。
final db = LingxiDatabase.forTesting(NativeDatabase.memory());
// 测试结束:await db.close();- Repository 测试统一用
NativeDatabase.memory(),避免污染真实数据库。 - SecureStorage 测试用 mock(见
test/data/services/secure_storage_test.dart)。
- Repository 必须有测试(每个 Repository 至少覆盖 CRUD 主路径)。
- Service 必须有测试(如
StreakService、DataExportService、SecureLogInterceptor)。 - AI Provider 必须有测试(用
http_mock_adaptermock dio 请求)。 - 新增功能时必须同步添加测试,PR 自检清单要求勾选"已为新增功能编写测试"。
flutter test # 运行所有测试
flutter test test/data/ # 运行指定目录
flutter test --coverage # 生成覆盖率报告(coverage/lcov.info)- 继承
package:flutter_lints/flutter.yaml。 - 开启 strict-casts / strict-inference / strict-raw-types 三项严格分析。
- 排除
**/*.g.dart与**/*.freezed.dart(代码生成文件)。
| 规则 | 说明 |
|---|---|
prefer_const_constructors |
能用 const 构造就用 const |
prefer_const_declarations |
能用 const 就不用 final |
prefer_final_locals |
局部变量优先 final |
prefer_final_in_for_each |
for-each 循环变量优先 final |
avoid_print |
禁止 print,用 debugPrint |
always_declare_return_types |
必须声明返回类型 |
annotate_overrides |
重写方法必须加 @override |
avoid_empty_else |
禁止空 else |
avoid_unused_constructor_parameters |
构造函数参数必须使用 |
require_trailing_commas |
多行集合/参数列表必须尾随逗号 |
- 能
const就const:构造函数、列表、Map、Widget。 constWidget 不会被重建,性能更优。- 列表字面量优先
const <Type>[]。
// ❌ 禁止
print('debug info');
// ✅ 推荐
debugPrint('debug info');
// ✅ 生产日志走 SecureLogInterceptorflutter analyze # 必须零 error、零 warning- 已有 warning 需在 PR 中说明原因。
- 禁止通过
// ignore:注释绕过 lint 规则,除非有充分理由并在注释中说明。
本项目遵循 Conventional Commits 规范:
<type>(<scope>): <description>
[optional body]
[optional footer]
| type | 说明 |
|---|---|
feat |
新功能 |
fix |
Bug 修复 |
docs |
文档变更(含 AGENTS.md / README / CONTRIBUTING) |
style |
代码格式调整(不影响功能) |
refactor |
代码重构(非新功能、非修复 Bug) |
test |
测试相关 |
chore |
构建、依赖、配置等杂项 |
scope 取功能模块名,如 chat、mascot、ai、database、router、theme、settings、learning、notes、progress、onboarding。
feat(chat): 添加苏格拉底式对话引导模式
- 在 ChatController 中根据 socraticModeProvider 注入系统提示词
- 完成流式响应后联动吉祥物庆祝动画
- 新增 saveAsNote 方法支持将 AI 回复保存为笔记
Closes #42
fix(database): 修复课程进度同步问题
Progress 表 lastStudiedAt 未正确更新,导致 Streak 计数漏算
docs(agents): 新增 AGENTS.md AI 协作者规范文档
test(mascot): 补充 MascotController 彩蛋触发测试
main分支为受保护分支,禁止直接 push。- 所有变更通过 Pull Request 合并,需至少一人 review 通过。
- 合并前必须通过
flutter analyze+flutter test。
格式:<type>/<brief-description>
| 示例 | 说明 |
|---|---|
feat/course-player |
新功能:课程播放器 |
fix/splash-crash |
修复:启动屏崩溃 |
docs/agents-md |
文档:AGENTS.md |
refactor/chat-controller |
重构:对话控制器 |
test/mascot |
测试:吉祥物 |
- 从
main切出 feature 分支。 - 编写代码 + 测试,确保
flutter analyze与flutter test通过。 - 提交遵循 Conventional Commits。
- 推送分支并在 GitHub 创建 PR,填写 PR 模板(见
CONTRIBUTING.md)。 - 等待 review,根据反馈修改。
- 合并后删除 feature 分支。
以下为项目当前已知的不足,新增相关功能时请勿被这些现状误导为"约定"。
| 项 | 现状 | 待优化方向 |
|---|---|---|
| 吉祥物动画 | v0.2.0 已完成 _MascotPainter 6 状态精细化绘制(径向渐变身体 / 角部高光 / 瞳孔高光 / 6 情绪差异化),v0.3.0 已完成 Hero 共享元素动画接入;Rive .riv 资源仍未就绪 |
Rive 动画资源待完善:待美术产出 assets/rive/lingxi_mascot.riv 后,在 RiveMascotWidget 中完成状态机映射,并保留 MascotWidget 作为加载失败 fallback |
| 图表 | fl_chart 未引入,统计页图表用 CustomPainter 手绘 |
待引入 fl_chart,重写统计页图表 |
| 国际化 | UI 文案大量硬编码中文,仅 app.dart 配置了 supportedLocales 与 delegates |
待抽取 intl ARB 文件,启用 flutter gen-l10n |
| 课程内容 | 仅 L0 Python 示例课程,assets/courses/ 内容单薄 |
待扩充更多课程(L1/L2、其他语言) |
| 数据导出 | file_picker / share_plus 未引入,导出仅写入应用文档目录返回路径 |
待引入 share_plus 支持系统分享,或 file_picker 支持自定义保存位置 |
| Riverpod 代码生成 | 已引入 riverpod_generator 但未广泛使用 @riverpod 注解,仍以手写 Provider 为主 |
待逐步迁移到代码生成风格 |
| iOS / Linux | 不支持 | 当前不计划支持,PR 不要引入 iOS/Linux 专属依赖 |
开发环境 Flutter SDK 路径:
C:\Users\23501\AppData\Local\Temp\flutter\bin\flutter.batCI 环境直接用flutter(假定已加入 PATH) 以下示例统一写作flutter,请按需替换为完整路径。
# 依赖
flutter pub get # 安装依赖
flutter pub upgrade # 升级依赖(谨慎)
# 代码生成(Drift / Riverpod / JSON)
flutter pub run build_runner build --delete-conflicting-outputs # 生成 .g.dart
flutter pub run build_runner watch --delete-conflicting-outputs # 监听模式
# 静态分析
flutter analyze # 必须零 error/warning
# 测试
flutter test # 运行所有测试
flutter test test/data/ # 运行指定目录
flutter test --coverage # 生成覆盖率(coverage/lcov.info)
# 构建(release)
flutter build apk --release # Android
flutter build windows --release # Windows
# 图标与启动屏
flutter pub run flutter_launcher_icons # 生成应用图标
flutter pub run flutter_native_splash:create # 生成启动屏flutter run -d <device-id> # 指定设备运行
flutter run -d windows # Windows 桌面
flutter run -d <android-device-id> # Android 设备
flutter devices # 列出可用设备本节专门面向 AI 协作者(Trae/Claude/Cursor 等),帮助你高效、安全地参与本项目。
- 先阅读
AGENTS.md(本文档),特别是"安全红线"章节。 - 阅读相关代码:不要凭文件名猜测实现,先
Read实际文件理解上下文。 - 大改动先写 spec:涉及多文件、跨模块的改动,先在
docs/下写一份设计文档(spec),经确认后再实现。 - 复用现有模式:新增 Repository/Provider/Widget 时,参考同类已有实现(如
ConversationRepository、LingxiCard),保持风格一致。
- 并行任务用 Sub-Agent:将独立的子任务(如写测试、写文档、写 UI)拆分给 Sub-Agent 并行执行。
- 遵守命名与分层约定:文件放对层,命名遵循上文规范。
- 不修改已签名/加密相关的代码逻辑(除非任务明确要求):
SecureStorageServiceSecureLogInterceptor的脱敏逻辑ProviderConfig.toJson的字段过滤DataExportService的 apiKey 断言database.dart的storeDateTimeAsText
- 不引入新依赖:除非任务明确要求,不要在
pubspec.yaml添加新包。如确需添加,先确认是否支持 Android + Windows 双端。
- 运行
flutter analyze确认零错误:任何 warning 都需在 PR 中说明。 - 运行相关测试:修改了 Repository/Service 必须运行对应测试;新增功能必须补测试。
- 重新生成代码:修改了 Drift 表定义或 Riverpod 注解后,运行
flutter pub run build_runner build --delete-conflicting-outputs。 - 更新文档:若修改了依赖版本、目录结构、安全相关逻辑,同步更新
AGENTS.md对应章节。 - 提交遵循 Conventional Commits:中文 commit message,type/scope 准确。
- ❌ 不要在代码中硬编码真实 API Key(即使是测试用)。
- ❌ 不要
print请求/响应内容(用debugPrint+SecureLogInterceptor)。 - ❌ 不要绕过
analysis_options.yaml的 lint 规则(除非有充分理由并注释说明)。 - ❌ 不要引入仅 iOS/Linux/macOS 可用的依赖。
- ❌ 不要直接 push 到
main分支。 - ❌ 不要在未阅读相关代码的情况下"凭直觉"修改。
本文档随项目演进持续更新。如有疑问或建议,提交 Issue 或在 PR 中讨论。
灵犀学院面向 K12 至大学阶段的学习者,AI 助手(苏格拉底对话)需根据用户画像中的 ageGroup 调整交互风格。
| 维度 | 规范 |
|---|---|
| 语言 | 简洁通俗,句子不超过 20 字为宜 |
| 解释 | 大量使用生活类比(盒子→变量、自动贩卖机→函数) |
| 情感 | 鼓励为主,永远不说"错了"、"不对" |
| 节奏 | 每次只讲一个概念,确认理解再推进 |
| 游戏化 | 描述为"闯关"、"解谜"、增加趣味性 |
| 维度 | 规范 |
|---|---|
| 语言 | 严谨但不晦涩,可使用术语但需首次出现时解释 |
| 解释 | 深度追问、举一反三、引导发现概念间关联 |
| 情感 | 尊重为主,鼓励独立思考和质疑 |
| 节奏 | 允许深度讨论,适度挑战舒适区 |
| 学术性 | 可引入设计模式、时间复杂度等进阶概念 |
- 不输出有害、歧视或不当内容
- 不替代完成作业:引导思考为主,即使学习者直接索要答案
- 困惑降级:连续 3 次无法回答时,自动切换为直接解答模式并提示
- 安全边界:不讨论非学习相关话题;超出课程范围时引导回到学习路径
- 提示词文件:
assets/prompts/socratic_young_learner.md、assets/prompts/socratic_advanced.md - 代码入口:
PromptManager.getSystemPrompt(socraticMode: true, ageGroup: ...) - 数据来源:
LearnerProfiles表的ageGroup字段
社区贡献者或 AI 协作者创建新课程内容时,需遵循以下规范。
- 校验:所有课程 JSON 必须通过
assets/courses/schema.json的 JSON Schema 校验 - ID 规范:使用层级命名
{level}_{module}_{lesson}_{kp},如l0_m1_l1_kp1 - 排序:
order字段从 1 开始,不要跳号
| 字段 | 要求 |
|---|---|
estimatedMinutes |
5-15 分钟可学完一个知识点 |
difficulty |
1-5 递增,同一课时内难度应逐步提升 |
prerequisites |
列出前置知识点 ID,避免循环依赖 |
coreExplanation |
3-5 句话说清核心概念,不超过 200 字 |
whyItMatters |
解释"为什么要学这个",与实际应用建立连接 |
- 每个知识点至少 3 道测验题
- 题型分布:单选为主 + 至少 1 道填空/多选
- 难度梯度:第 1 题考基本概念 → 第 2 题考应用 → 第 3 题考辨析
explanation必填且详尽,告诉学习者"为什么对/为什么错"
- 问题应该是开放性的,不能简单回答 YES/NO
- 应引导学习者思考"为什么"而非"是什么"
- 好的种子问题:"为什么 Python 不需要声明变量类型?"
- 坏的种子问题:"Python 需要声明变量类型吗?"
新页面或功能需要接入吉祥物时:
- 使用
ref.read(mascotControllerProvider.notifier)控制情绪 - 不要在页面内自行管理吉祥物状态
- 情绪切换后设置定时恢复(happy: 1.5s、celebrate: 3s)
- 所有
Future.delayed回调中必须判断mounted
当多个事件同时触发时,按以下优先级:
sad(AI 出错)— 最高优先级celebrate(完成成就/彩蛋)thinking(AI 请求中)happy(用户点击/streak≥3)curious(预留:进入新页面)idle(默认恢复状态)
| 状态 | 持续时间 | 恢复目标 |
|---|---|---|
| happy | 1.5s | idle |
| celebrate | 3.0s | idle |
| thinking | 不定(跟随 AI 响应) | celebrate/sad |
| sad | 不自动恢复 | 等待下次交互 |
| curious | 2.0s | idle |
| 指标 | 目标 | 测量方式 |
|---|---|---|
| 首屏渲染 | <1.5s | 从 main() 到 HomePage 首帧 |
| 流式首 token | <500ms(网络延迟除外) | 从发送到 TextDeltaEvent 首次 emit |
| 内存占用 | <200MB | Release 模式下正常使用峰值 |
| 列表滚动 | 60fps | 对话列表/课程列表不丢帧 |
| 动画帧率 | 60fps | PerformanceOverlay 不出现红条 |
| 数据库查询 | <50ms | 单次 Repository 方法调用 |
| 课程加载 | <200ms | getAllCourses() 含 JSON 解析 |
- 流式响应使用 50ms 节流,避免频繁 setState
- 课程数据内存缓存,避免重复 IO
- 图片使用适当分辨率,避免过大资源
- 长列表使用
ListView.builder懒加载 - 减少动效模式(
MediaQuery.disableAnimations)时跳过所有动画
60fps 是动画与滚动的硬指标。以下方法用于开发期与回归期验证帧率不丢帧。
| 方法 | 用途 | 使用说明 |
|---|---|---|
PerformanceOverlay |
debug 模式实时帧率查看 | MaterialApp(showPerformanceOverlay: true) 或 MediaQuery.of(context).copyWith(...),不出现红条即达标;release 模式下无效 |
debugProfileBuildsEnabled |
开启 build 阶段分析 | 在 main() 中置 debugProfileBuildsEnabled = true,配合 Dart DevTools 的 Timeline 定位非必要 build() 调用 |
RepaintBoundary |
隔离持续动画的重绘范围 | 包裹 _AuraGlow / CelebrationOverlay / AnimatedProgressBar / ShimmerLoading / _ParticleField / MascotWidget 等持续动画节点,避免父级 rebuild 时连带重绘 |
itemExtent / cacheExtent |
优化列表滚动 | 项高固定时使用 itemExtent 跳过测量;项高不固定时使用 cacheExtent: 500 预渲染后续项,避免滚动时即时 build 抖动 |
reduceMotion 无障碍降级 |
全覆盖验证 | 在系统设置开启"移除动画"后,所有动画应降级为即时切换或静态态;项目通过 SpringMotion.reduceMotionOf(context) / MediaQuery.disableAnimationsOf(context) 统一判断 |
回归验收标准:
PerformanceOverlay在首页、学习路径、对话列表、笔记列表、课时页滑动过程中均不出现红条flutter run --profile模式下使用 Dart DevTools Timeline 录制,UI 线程帧时间 ≤ 16ms- 开启系统"移除动画"后,所有页面交互保持可用,无卡顿或缺失关键反馈