From 2ac56e5084d74b5122b00ad95fcd6c6bf7bcba79 Mon Sep 17 00:00:00 2001 From: qzl Date: Mon, 2 Mar 2026 10:55:46 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=95=B4=E7=90=86=20runtime=20?= =?UTF-8?q?=E7=B3=BB=E5=88=97=E6=96=87=E6=A1=A3=EF=BC=8C=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=20API=20=E7=AB=AF=E7=82=B9=E5=90=8D=E7=A7=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/AGENTS.md | 11 + .../calendar/ui/widgets/bottom_dock.dart | 4 +- .../ui/screens/todo_quadrants_screen.dart | 314 ++++++++++-------- .../config/static/agent_chat/llm_catalog.yaml | 51 +-- backend/src/core/initialization/init_data.py | 4 +- docs/runtime/runtime-route.md | 58 +++- docs/runtime/runtime-runbook.md | 33 +- infra/scripts/dev-migrate.sh | 46 +++ pyproject.toml | 1 + 9 files changed, 350 insertions(+), 172 deletions(-) create mode 100755 infra/scripts/dev-migrate.sh diff --git a/apps/AGENTS.md b/apps/AGENTS.md index f00e1bc..d188e2d 100644 --- a/apps/AGENTS.md +++ b/apps/AGENTS.md @@ -121,3 +121,14 @@ AppBanner(message: '请检查输入', type: ToastType.warning) - Use `AppBanner` for persistent inline messages (form errors) - DO NOT create custom SnackBar, Dialog, or Banner components - DO NOT use raw `ScaffoldMessenger` + +## App Debugging + +**DO NOT automatically start Flutter app debugging.** + +After completing code changes, inform the user to manually run: +```bash +flutter run --dart-define=MOCK_API=true -d emulator-5554 +``` + +Let the user control when to launch the app for testing. diff --git a/apps/lib/features/calendar/ui/widgets/bottom_dock.dart b/apps/lib/features/calendar/ui/widgets/bottom_dock.dart index dd59336..2503e09 100644 --- a/apps/lib/features/calendar/ui/widgets/bottom_dock.dart +++ b/apps/lib/features/calendar/ui/widgets/bottom_dock.dart @@ -97,9 +97,9 @@ class BottomDock extends StatelessWidget { width: 44, height: 44, decoration: BoxDecoration( - color: AppColors.todoHomeBtnBg, + color: AppColors.todoToggleBg, borderRadius: BorderRadius.circular(AppRadius.xl), - border: Border.all(color: AppColors.todoHomeBtnBorder), + border: Border.all(color: AppColors.todoToggleBorder), ), child: const Icon( LucideIcons.home, diff --git a/apps/lib/features/todo/ui/screens/todo_quadrants_screen.dart b/apps/lib/features/todo/ui/screens/todo_quadrants_screen.dart index d8ef176..2eff3d6 100644 --- a/apps/lib/features/todo/ui/screens/todo_quadrants_screen.dart +++ b/apps/lib/features/todo/ui/screens/todo_quadrants_screen.dart @@ -1,13 +1,46 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:lucide_icons/lucide_icons.dart'; import '../../../../core/di/injection.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../calendar/ui/calendar_state_manager.dart'; +import '../../../calendar/ui/widgets/bottom_dock.dart'; -class TodoQuadrantsScreen extends StatelessWidget { +class TodoQuadrantsScreen extends StatefulWidget { const TodoQuadrantsScreen({super.key}); + @override + State createState() => _TodoQuadrantsScreenState(); +} + +class _TodoQuadrantsScreenState extends State { + late List<_TodoItem> _importantUrgent; + late List<_TodoItem> _urgentNotImportant; + late List<_TodoItem> _importantNotUrgent; + + @override + void initState() { + super.initState(); + _importantUrgent = [ + _TodoItem(title: '18:00 前提交活动方案'), + _TodoItem(title: '回复客户邀约确认'), + ]; + _urgentNotImportant = [ + _TodoItem(title: '确认会场停车信息'), + _TodoItem(title: '代订明早高铁票'), + ]; + _importantNotUrgent = [ + _TodoItem(title: '本周复盘与下周规划'), + _TodoItem(title: '整理个人知识库结构'), + _TodoItem(title: '优化三月目标里程碑'), + ]; + } + + void _removeItem(String id, List<_TodoItem> list) { + setState(() { + list.removeWhere((item) => item.id == id); + }); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -17,7 +50,7 @@ class TodoQuadrantsScreen extends StatelessWidget { children: [ _buildHeader(), Expanded(child: _buildContent()), - _buildBottomDock(context), + _buildBottomDock(), ], ), ), @@ -52,29 +85,29 @@ class TodoQuadrantsScreen extends StatelessWidget { children: [ _buildQuadrant( title: '重要紧急', - count: 2, textColor: AppColors.g1Text, dividerColor: AppColors.g1Divider, borderColor: AppColors.g1Border, - items: ['18:00 前提交活动方案', '回复客户邀约确认'], + items: _importantUrgent, + onRemove: (id) => _removeItem(id, _importantUrgent), ), const SizedBox(height: 12), _buildQuadrant( title: '紧急不重要', - count: 2, textColor: AppColors.g2Text, dividerColor: AppColors.g2Divider, borderColor: AppColors.g2Border, - items: ['确认会场停车信息', '代订明早高铁票'], + items: _urgentNotImportant, + onRemove: (id) => _removeItem(id, _urgentNotImportant), ), const SizedBox(height: 12), _buildQuadrant( title: '重要不紧急', - count: 3, textColor: AppColors.g3Text, dividerColor: AppColors.g3Divider, borderColor: AppColors.g3Border, - items: ['本周复盘与下周规划', '整理个人知识库结构', '优化三月目标里程碑'], + items: _importantNotUrgent, + onRemove: (id) => _removeItem(id, _importantNotUrgent), ), ], ), @@ -83,11 +116,11 @@ class TodoQuadrantsScreen extends StatelessWidget { Widget _buildQuadrant({ required String title, - required int count, required Color textColor, required Color dividerColor, required Color borderColor, - required List items, + required List<_TodoItem> items, + required void Function(String) onRemove, }) { return Container( width: double.infinity, @@ -113,7 +146,7 @@ class TodoQuadrantsScreen extends StatelessWidget { ), ), Text( - '$count项', + '${items.length}项', style: TextStyle( fontFamily: 'Inter', fontSize: 12, @@ -126,149 +159,152 @@ class TodoQuadrantsScreen extends StatelessWidget { const SizedBox(height: 8), Container(height: 1, color: dividerColor), const SizedBox(height: 8), - ...items.map((item) => _buildTodoItem(item)), + ...items.map((item) => _buildTodoItem(item, onRemove)), ], ), ); } - Widget _buildTodoItem(String title) { + Widget _buildTodoItem(_TodoItem item, void Function(String) onRemove) { + return _TodoItemWidget( + key: ValueKey(item.id), + item: item, + onRemove: onRemove, + ); + } + + Widget _buildBottomDock() { + return BottomDock( + activeTab: DockTab.todo, + onTodoTap: () {}, + onCalendarTap: () { + final manager = sl(); + final viewType = manager.viewType; + final date = manager.selectedDate; + final dateStr = + '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; + if (viewType == CalendarViewType.month) { + context.push('/calendar/month'); + } else { + context.push('/calendar/dayweek?date=$dateStr'); + } + }, + onHomeTap: () => context.go('/home'), + ); + } +} + +class _TodoItem { + final String id; + final String title; + + _TodoItem({required this.title}) + : id = DateTime.now().microsecondsSinceEpoch.toString(); +} + +class _TodoItemWidget extends StatefulWidget { + final _TodoItem item; + final void Function(String) onRemove; + + const _TodoItemWidget({ + super.key, + required this.item, + required this.onRemove, + }); + + @override + State<_TodoItemWidget> createState() => _TodoItemWidgetState(); +} + +class _TodoItemWidgetState extends State<_TodoItemWidget> + with SingleTickerProviderStateMixin { + bool _isChecked = false; + late AnimationController _controller; + late Animation _scaleAnimation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 200), + vsync: this, + ); + _scaleAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutBack)); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _handleTap() { + if (_isChecked) return; + + setState(() { + _isChecked = true; + }); + _controller.forward().then((_) { + widget.onRemove(widget.item.id); + }); + } + + @override + Widget build(BuildContext context) { return SizedBox( height: 42, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text( - title, - style: const TextStyle( - fontFamily: 'Inter', - fontSize: 13, - fontWeight: FontWeight.w600, - color: AppColors.slate700, + Expanded( + child: Text( + widget.item.title, + style: const TextStyle( + fontFamily: 'Inter', + fontSize: 13, + fontWeight: FontWeight.w600, + color: AppColors.slate700, + ), ), ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 8, - height: 8, - decoration: BoxDecoration( - color: AppColors.slate300, - shape: BoxShape.circle, - ), - ), - const SizedBox(width: 8), - Container( - width: 8, - height: 8, - decoration: BoxDecoration( - color: AppColors.slate300, - shape: BoxShape.circle, - ), - ), - ], + GestureDetector( + onTap: _handleTap, + child: AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return Container( + width: 20, + height: 20, + decoration: BoxDecoration( + color: _isChecked ? AppColors.blue600 : Colors.white, + border: Border.all( + color: _isChecked + ? AppColors.blue600 + : AppColors.slate300, + width: 1.5, + ), + borderRadius: BorderRadius.circular(4), + ), + child: _isChecked + ? Transform.scale( + scale: _scaleAnimation.value, + child: const Icon( + Icons.check, + size: 14, + color: Colors.white, + ), + ) + : null, + ); + }, + ), ), ], ), ); } - - Widget _buildBottomDock(BuildContext context) { - return SizedBox( - height: 61, - child: Padding( - padding: const EdgeInsets.only( - left: 20, - right: 20, - top: 12, - bottom: 18, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - width: 102, - height: 52, - padding: const EdgeInsets.symmetric(horizontal: 5), - decoration: BoxDecoration( - color: AppColors.todoToggleBg, - borderRadius: BorderRadius.circular(24), - border: Border.all(color: AppColors.todoToggleBorder, width: 1), - ), - child: Row( - children: [ - Container( - width: 44, - height: 44, - decoration: BoxDecoration( - color: AppColors.todoToggleActiveBg, - borderRadius: BorderRadius.circular(18), - border: Border.all( - color: AppColors.todoToggleActiveBorder, - width: 1, - ), - ), - child: const Icon( - LucideIcons.listTodo, - size: 20, - color: AppColors.blue600, - ), - ), - const SizedBox(width: 4), - GestureDetector( - onTap: () { - final manager = sl(); - final viewType = manager.viewType; - final date = manager.selectedDate; - final dateStr = - '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; - if (viewType == CalendarViewType.month) { - context.push('/calendar/month'); - } else { - context.push('/calendar/dayweek?date=$dateStr'); - } - }, - child: Container( - width: 44, - height: 44, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(18), - ), - child: const Icon( - LucideIcons.calendar, - size: 20, - color: AppColors.slate500, - ), - ), - ), - ], - ), - ), - GestureDetector( - onTap: () => Navigator.of(context).pop(), - child: Container( - width: 44, - height: 44, - decoration: BoxDecoration( - color: AppColors.todoHomeBtnBg, - borderRadius: BorderRadius.circular(18), - border: Border.all( - color: AppColors.todoHomeBtnBorder, - width: 1, - ), - ), - child: const Icon( - LucideIcons.home, - size: 20, - color: Color(0xFF1E3A8A), - ), - ), - ), - ], - ), - ), - ); - } } diff --git a/backend/src/core/config/static/agent_chat/llm_catalog.yaml b/backend/src/core/config/static/agent_chat/llm_catalog.yaml index d6b1b8f..dcab2ab 100644 --- a/backend/src/core/config/static/agent_chat/llm_catalog.yaml +++ b/backend/src/core/config/static/agent_chat/llm_catalog.yaml @@ -1,25 +1,32 @@ factories: - - name: qwen - request_url: https://dashscope.aliyuncs.com/compatible-mode/v1 - avatar: https://cdn.simpleicons.org/alibabacloud/FF6A00 - - name: minimax - request_url: https://api.minimax.chat/v1 - avatar: https://cdn.simpleicons.org/minimax/1A1A1A - - name: kimi - request_url: https://api.moonshot.cn/v1 - avatar: https://cdn.simpleicons.org/moonrepo/3B82F6 - - name: deepseek - request_url: https://api.deepseek.com/v1 - avatar: https://cdn.simpleicons.org/deepseek/4D6BFE - - name: doubao - request_url: https://ark.cn-beijing.volces.com/api/v3 - avatar: https://cdn.simpleicons.org/volkswagen/001E50 - - name: zai - request_url: https://api.z.ai/v1 - avatar: https://cdn.simpleicons.org/zotero/CC2936 + - name: dashscope + request_url: https://dashscope.aliyuncs.com/compatible-mode/v1 + avatar: https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/qwen-color.png + + - name: minimax + request_url: https://api.minimaxi.com/v1 + avatar: https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/minimax-color.png + + - name: moonshot + request_url: https://api.moonshot.cn/v1 + avatar: https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/moonshot.png + + - name: deepseek + request_url: https://api.deepseek.com/v1 + avatar: https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/deepseek-color.png + + - name: volcengine-ark + request_url: https://ark.cn-beijing.volces.com/api/v3 + avatar: https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/doubao-color.png + + - name: z-ai + request_url: https://api.z.ai/api/paas/v4 + avatar: https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/zai.png llms: - - model_code: qwen3.5-flash - factory_id: qwen - - model_code: deepseek-v3.2 - factory_id: deepseek + # 你原来的两个保留 + - model_code: qwen3.5-flash + factory_name: dashscope + + - model_code: deepseek-v3.2 + factory_name: deepseek diff --git a/backend/src/core/initialization/init_data.py b/backend/src/core/initialization/init_data.py index 9f3aefe..e3cff78 100644 --- a/backend/src/core/initialization/init_data.py +++ b/backend/src/core/initialization/init_data.py @@ -25,7 +25,7 @@ class LlmFactorySeed(BaseModel): class LlmSeed(BaseModel): model_code: str - factory_id: str + factory_name: str class LlmCatalogSeed(BaseModel): @@ -118,7 +118,7 @@ async def initialize_data() -> bool: factory_id_by_name[factory["name"]] = factory_id for llm in catalog["llms"]: - factory_name = llm["factory_id"] + factory_name = llm["factory_name"] resolved_factory_id = factory_id_by_name.get(factory_name) if resolved_factory_id is None: raise RuntimeError( diff --git a/docs/runtime/runtime-route.md b/docs/runtime/runtime-route.md index 4f3aa22..52f41fd 100644 --- a/docs/runtime/runtime-route.md +++ b/docs/runtime/runtime-route.md @@ -397,7 +397,59 @@ --- -## Inbox Messages +## Profile + +### GET /profile/me + +获取当前用户信息(需要认证)。 + +**Response:** 200 OK +```json +{ + "id": "string", + "username": "string", + "avatar_url": "string?", + "bio": "string?" +} +``` + +**Errors:** +- 401: 未认证 + +--- + +### PATCH /profile/me + +更新当前用户信息(需要认证)。 + +**Request:** +```json +{ + "username": "string? (3-30 chars)", + "avatar_url": "string? (URL)", + "bio": "string? (max 200 chars)" +} +``` + +**Response:** 200 OK + +**Errors:** +- 401: 未认证 +- 422: 请求参数无效 + +--- + +### GET /profile/{username} + +按用户名查询用户公开信息(需要认证)。 + +**Response:** 200 OK + +**Errors:** +- 401: 未认证 +- 404: 用户不存在 + +--- ## Inbox Messages @@ -469,6 +521,8 @@ ## Users +> **Note:** `/users/me` 与 `/profile/me` 功能重叠(历史兼容)。推荐使用 `/profile/me`。 + ### GET /users/me 获取当前用户信息(需要认证)。 @@ -734,7 +788,7 @@ ## Agent Chat -### POST /agent-chats +### POST /agent-chat 运行 Agent 对话(需要认证)。 diff --git a/docs/runtime/runtime-runbook.md b/docs/runtime/runtime-runbook.md index 1cca722..87a7943 100644 --- a/docs/runtime/runtime-runbook.md +++ b/docs/runtime/runtime-runbook.md @@ -35,10 +35,22 @@ docker compose --env-file .env -f infra/docker/docker-compose.yml up -d ### Step 2: 执行迁移与初始化 +#### 生产环境 ```bash -docker compose --env-file .env -f infra/docker/docker-compose.yml run --rm --build init-job bootstrap +docker compose --env-file .env -f infra/docker/docker-compose.yml run --rm --build init-job uv run python -m core.runtime.cli bootstrap ``` +#### 开发环境(推荐) +开发阶段推荐使用脚本,直接使用本地代码,无需构建镜像: + +```bash +bash infra/scripts/dev-migrate.sh bootstrap +``` + +可选命令: +- `bash infra/scripts/dev-migrate.sh migrate` - 仅运行迁移 +- `bash infra/scripts/dev-migrate.sh init-data` - 仅初始化数据 + 通过标准:命令退出码为 0,日志中无 migration/init-data 错误。 ### Step 3: 版本核对(建议) @@ -120,20 +132,29 @@ curl -sS -X POST "${WEB_BASE_URL}/api/v1/auth/login" \ ```bash # signup start -curl -sS -X POST "${WEB_BASE_URL}/api/v1/auth/signup/start" \ +curl -sS -X POST "${WEB_BASE_URL}/api/v1/auth/verifications" \ -H 'Content-Type: application/json' \ -d '{"username":"demo","email":"demo@example.com","password":"secret123"}' # signup verify -curl -sS -X POST "${WEB_BASE_URL}/api/v1/auth/signup/verify" \ +curl -sS -X POST "${WEB_BASE_URL}/api/v1/auth/verifications/verify" \ -H 'Content-Type: application/json' \ -d '{"email":"demo@example.com","token":"123456"}' +# signup resend +curl -sS -X POST "${WEB_BASE_URL}/api/v1/auth/verifications/resend" \ + -H 'Content-Type: application/json' \ + -d '{"email":"demo@example.com"}' + # profile patch curl -sS -X PATCH "${WEB_BASE_URL}/api/v1/profile/me" \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer " \ -d '{"username":"demo2","bio":"hello"}' + +# profile get +curl -sS "${WEB_BASE_URL}/api/v1/profile/me" \ + -H "Authorization: Bearer " ``` 通过标准:接口返回符合预期的 2xx 或受控业务错误,无 5xx。 @@ -145,7 +166,7 @@ PYTHONPATH=backend/src uv run pytest backend/tests/unit -k agent_chat -q PYTHONPATH=backend/src uv run pytest backend/tests/integration -k agent_chat -q PYTHONPATH=backend/src uv run pytest backend/tests/e2e/test_agent_chat_flow.py backend/tests/e2e/test_agent_chat_recent_session_home.py -q -curl -sS -X POST "${WEB_BASE_URL}/api/v1/agent-chat/run" \ +curl -sS -X POST "${WEB_BASE_URL}/api/v1/agent-chat" \ -H 'Content-Type: application/json' \ -d '{"message":"hello"}' ``` @@ -204,7 +225,7 @@ docker compose --env-file .env -f infra/docker/docker-compose.yml up -d --force- - 复核标准: - `docker inspect supabase-auth` 能看到 `GOTRUE_MAILER_TEMPLATES_CONFIRMATION/RECOVERY`。 - `supabase-mail-templates` 挂载源为 `infra/mail-templates`。 - - `POST /api/v1/auth/signup/start` 返回 `202` 且耗时恢复正常。 + - `POST /api/v1/auth/verifications` 返回 `202` 且耗时恢复正常。 --- @@ -246,3 +267,5 @@ docker compose --env-file .env -f infra/docker/docker-compose.yml up -d --force- | 2026-02-25 | 新增配置漂移故障条目:修复 Auth 邮件模板失效与 signup 超时场景 | | 2026-02-27 | 用户搜索支持邮箱精确匹配:query 含 @ 符号时走 auth.users → profiles 两步查询 | | 2026-02-28 | 邀请码功能:新增 invite_codes 表、profiles.referred_by,注册时可选填邀请码并记录邀请关系 | +| 2026-03-02 | 文档整理:修正 auth 端点名称(/verifications)、补充 profile 路由文档、修复 L2/L3 验证命令 | +| 2026-03-02 | 修正 bootstrap 命令:init-job 需要使用 `uv run python -m core.runtime.cli bootstrap` | diff --git a/infra/scripts/dev-migrate.sh b/infra/scripts/dev-migrate.sh new file mode 100755 index 0000000..0f9a446 --- /dev/null +++ b/infra/scripts/dev-migrate.sh @@ -0,0 +1,46 @@ +#!/bin/bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)" +ENV_FILE="$ROOT_DIR/.env" + +usage() { + echo "Usage: $0 {migrate|init-data|bootstrap}" + echo "" + echo "Commands:" + echo " migrate Run database migrations only" + echo " init-data Initialize seed data only" + echo " bootstrap Run migrations + init-data" + echo "" + echo "Note: Requires Supabase services running (docker compose up -d)" + exit 1 +} + +if [ ! -f "$ENV_FILE" ]; then + echo "Error: env file not found at $ENV_FILE" >&2 + exit 1 +fi + +set -a +. "$ENV_FILE" +set +a + +cd "$ROOT_DIR" + +case "${1:-}" in + migrate) + echo "=== Running Migrations ===" + PYTHONPATH=backend/src uv run python -m core.runtime.cli migrate + ;; + init-data) + echo "=== Running Init Data ===" + PYTHONPATH=backend/src uv run python -m core.runtime.cli init-data + ;; + bootstrap) + echo "=== Running Bootstrap ===" + PYTHONPATH=backend/src uv run python -m core.runtime.cli bootstrap + ;; + *) + usage + ;; +esac diff --git a/pyproject.toml b/pyproject.toml index 438977a..4bbf5be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ dependencies = [ "pydantic>=2.11.0", "pydantic-settings>=2.10.0", "pyjwt>=2.10.1", + "pyyaml>=6.0.3", "redis>=7.1.0", "sqlalchemy[asyncio]>=2.0.46", "structlog>=24.4.0",