From 43e9025c6db29ab31f170e5d4cf516e47ec97b03 Mon Sep 17 00:00:00 2001 From: chiguyong Date: Sun, 28 Jun 2026 14:24:58 +0800 Subject: [PATCH] =?UTF-8?q?fix(calendar):=20=E6=97=A5=E5=8E=86=E8=83=BD?= =?UTF-8?q?=E5=8A=9B=E7=BC=BA=E5=A4=B1=E4=BF=AE=E5=A4=8D=20+=20UI=20?= =?UTF-8?q?=E5=B8=83=E5=B1=80=E4=BC=98=E5=8C=96=20+=20=E4=BC=9A=E8=AF=9D40?= =?UTF-8?q?4=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P0: calendar_tool reminder_rules 未传入 create_event,提醒功能完全失效。P1: chat.ts deleteConversation 未清理 pending + 404 递归保护。P2: app.py 系统提示重复段落 + gui_mode F821 + SystemMonitorPanel flex 布局。P3: portal send_json 快照 + WS connected 清除 is_local + 移除死代码。验证: ruff+pytest 98passed+typecheck 通过。 --- .opencodereview/rule.code.json | 40 + .opencodereview/rule.docs.json | 32 + .../calendar-capability-and-ui-fixes.md | 74 + src/agentkit/calendar/service.py | 15 + .../chat/sqlite_conversation_store.py | 21 + src/agentkit/server/app.py | 73 +- src/agentkit/server/auth/middleware.py | 13 + src/agentkit/server/frontend/components.d.ts | 17 + src/agentkit/server/frontend/src/api/base.ts | 43 +- .../server/frontend/src/api/client.ts | 5 + src/agentkit/server/frontend/src/api/tauri.ts | 2 +- src/agentkit/server/frontend/src/api/types.ts | 2 + .../components/calendar/CalendarDrawer.vue | 15 +- .../src/components/calendar/CalendarGrid.vue | 37 +- .../src/components/calendar/SyncSettings.vue | 10 +- .../src/components/chat/ChatSidebar.vue | 55 +- .../src/components/layout/RightPanel.vue | 7 +- .../src/components/layout/SplashScreen.vue | 2 +- .../components/layout/SystemMonitorPanel.vue | 231 +- .../frontend/src/components/layout/TopNav.vue | 6 + .../components/layout/tabs/DocumentsTab.vue | 139 ++ .../src/components/layout/tabs/SkillsTab.vue | 84 +- .../components/preview/scenes/Scene6Error.vue | 4 +- .../settings/ActiveSessionsPanel.vue | 2 +- .../src/components/workflow/PropertyPanel.vue | 2 +- .../server/frontend/src/router/index.ts | 8 + .../server/frontend/src/stores/chat.ts | 1913 ++++++++++------- .../server/frontend/src/views/ChatView.vue | 6 +- src/agentkit/server/routes/portal.py | 79 +- src/agentkit/tools/calendar_tool.py | 41 +- 30 files changed, 2007 insertions(+), 971 deletions(-) create mode 100644 .opencodereview/rule.code.json create mode 100644 .opencodereview/rule.docs.json create mode 100644 docs/solutions/logic-errors/calendar-capability-and-ui-fixes.md create mode 100644 src/agentkit/server/frontend/src/components/layout/tabs/DocumentsTab.vue diff --git a/.opencodereview/rule.code.json b/.opencodereview/rule.code.json new file mode 100644 index 0000000..14aa016 --- /dev/null +++ b/.opencodereview/rule.code.json @@ -0,0 +1,40 @@ +{ + "rules": [ + { + "path": "**/*.py", + "rule": "代码评审 10 维度(项目 AGENTS.md + ponytail 规则):1) 正确性 — 边界条件、异常路径、空值处理;2) 安全性 — API Key 比较必须 hmac.compare_digest(恒定时间),JWT HS256 access 15min/refresh 7d,RBAC 三级权限位,专家名必须 _EXPERT_NAME_RE=re.compile(r\"^[a-zA-Z0-9_-]{1,64}$\") 校验,bcrypt rounds=12,禁止 SQL 注入/路径穿越;3) 类型安全 — 禁止 any,必须类型注解,所有数据模型用 pydantic>=2.0 且 model_config=ConfigDict(...) 而非 class Config;4) 并发异步 — HandoffTransport 队列有界 maxsize=1024,关闭用 sentinel 模式,async def 含 yield 禁止在首个 yield 前用 return(必须 return; yield 模式或重构),禁止阻塞调用在 async 函数中;5) 性能 — N+1 查询、未加索引、未用缓存、内存泄漏、未关闭资源;6) 可维护性 — 命名、重复代码、圈复杂度、函数过长;7) 错误处理 — 信任边界必须校验输入,防止数据丢失,禁止裸 except;8) 依赖配置 — 不得擅自改 pyproject.toml 版本,配置优先级 CLI>yaml>env>.env>默认;9) 测试 — 非平凡逻辑必须留下一个可运行检查(assert demo 或小测试文件,无框架无 fixtures);10) Ponytail 原则 — 无未请求的抽象、无未请求的新依赖、删除优先于新增、复杂处用 ponytail: 注释标记已知上限与升级路径。" + }, + { + "path": "**/*.ts", + "rule": "前端 TS 评审维度(AGENTS.md 前端规范):1) 类型安全 — 禁止 any,必须显式类型注解,interface/type 优先;2) Vue3 Composition API — 禁止 Options API,必须 + + diff --git a/src/agentkit/server/frontend/src/components/layout/tabs/SkillsTab.vue b/src/agentkit/server/frontend/src/components/layout/tabs/SkillsTab.vue index a0ca3e4..e07404a 100644 --- a/src/agentkit/server/frontend/src/components/layout/tabs/SkillsTab.vue +++ b/src/agentkit/server/frontend/src/components/layout/tabs/SkillsTab.vue @@ -2,7 +2,7 @@
- 加载技能列表... + 加载{{ title }}...
@@ -10,14 +10,14 @@ {{ skillsStore.error }}
-
- - 暂无已注册技能 +
+ + {{ emptyText }}
- {{ skill.name }} - - {{ isEngine(skill) ? '引擎' : '技能' }} - + {{ skill.name }}

{{ skill.description || '暂无描述' }}

- + {{ cap }} @@ -60,18 +53,51 @@ @@ -137,31 +165,35 @@ onMounted(() => { .skills-tab__item-header { display: flex; - align-items: center; + align-items: flex-start; gap: var(--space-2); margin-bottom: var(--space-1); + min-width: 0; } .skills-tab__item-icon { + flex-shrink: 0; color: var(--accent-team); font-size: var(--font-sm); + margin-top: 2px; } .skills-tab__item--agent_template .skills-tab__item-icon { color: var(--accent-board); } -.skills-tab__item-type { - margin-right: 0; -} - .skills-tab__item-name { + flex: 1; + min-width: 0; font-weight: var(--font-weight-medium); color: var(--text-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .skills-tab__item-status { - margin-left: auto; + flex-shrink: 0; } .skills-tab__item-desc { diff --git a/src/agentkit/server/frontend/src/components/preview/scenes/Scene6Error.vue b/src/agentkit/server/frontend/src/components/preview/scenes/Scene6Error.vue index 16417c0..b4bc716 100644 --- a/src/agentkit/server/frontend/src/components/preview/scenes/Scene6Error.vue +++ b/src/agentkit/server/frontend/src/components/preview/scenes/Scene6Error.vue @@ -26,14 +26,14 @@ import { ref } from 'vue' import { MessageShell, UserBubble, ErrorCard } from '@/components/chat/messages' -const errorDetail = ref('Connection refused: 无法连接到本地 MCP 服务器(localhost:8080)。请检查服务是否已启动。') +const errorDetail = ref('连接被拒绝:无法连接到本地 MCP 服务器(localhost:8080)。请检查服务是否已启动。') const retried = ref(false) function handleRetry(): void { retried.value = true errorDetail.value = '正在重试…' setTimeout(() => { - errorDetail.value = 'Connection refused: 无法连接到本地 MCP 服务器(localhost:8080)。请检查服务是否已启动。' + errorDetail.value = '连接被拒绝:无法连接到本地 MCP 服务器(localhost:8080)。请检查服务是否已启动。' }, 1500) } diff --git a/src/agentkit/server/frontend/src/components/settings/ActiveSessionsPanel.vue b/src/agentkit/server/frontend/src/components/settings/ActiveSessionsPanel.vue index 953c2e8..8fed311 100644 --- a/src/agentkit/server/frontend/src/components/settings/ActiveSessionsPanel.vue +++ b/src/agentkit/server/frontend/src/components/settings/ActiveSessionsPanel.vue @@ -22,7 +22,7 @@ @@ -111,7 +108,6 @@ import ChatMessage from '@/components/chat/ChatMessage.vue' import ChatInput from '@/components/chat/ChatInput.vue' import ExpertTeamView from '@/components/chat/ExpertTeamView.vue' import BoardStatusView from '@/components/chat/BoardStatusView.vue' -import DocumentPanel from '@/components/chat/DocumentPanel.vue' const ATypographyText = ATypography.Text diff --git a/src/agentkit/server/routes/portal.py b/src/agentkit/server/routes/portal.py index ebddb4e..a2b03e7 100644 --- a/src/agentkit/server/routes/portal.py +++ b/src/agentkit/server/routes/portal.py @@ -7,6 +7,7 @@ import uuid from dataclasses import dataclass, field from datetime import datetime, timezone from pathlib import Path +from typing import Any from fastapi import ( APIRouter, @@ -178,6 +179,60 @@ class Conversation: _WS_HEARTBEAT_TIMEOUT = float(os.environ.get("AGENTKIT_WS_TIMEOUT", "120")) _conversation_store = SqliteConversationStore(db_path=_CONVERSATIONS_DB_PATH) + +# --------------------------------------------------------------------------- +# Active portal WebSocket connections by user_id +# --------------------------------------------------------------------------- + + +class PortalConnectionManager: + """Track active portal WebSocket connections by authenticated user_id. + + Used by the calendar reminder scheduler (and other user-scoped push + features) to deliver real-time messages to a user's open chat tab(s). + """ + + def __init__(self) -> None: + # user_id -> list of active WebSocket connections + self._connections: dict[str, list[WebSocket]] = {} + + def add(self, user_id: str, ws: WebSocket) -> None: + self._connections.setdefault(user_id, []).append(ws) + + def remove(self, user_id: str, ws: WebSocket) -> None: + conns = self._connections.get(user_id) + if conns is None: + return + self._connections[user_id] = [w for w in conns if w is not ws] + if not self._connections[user_id]: + del self._connections[user_id] + + async def send_json(self, user_id: str, message: dict[str, Any]) -> None: + """Broadcast a JSON message to all connections for *user_id*. + + Removes stale connections that fail to send. + """ + conns = list(self._connections.get(user_id, [])) + if not conns: + return + stale: list[WebSocket] = [] + for ws in conns: + try: + await ws.send_json(message) + except Exception: + stale.append(ws) + for ws in stale: + self.remove(user_id, ws) + + +portal_connection_manager = PortalConnectionManager() + + +async def send_to_user(user_id: str, message: dict[str, Any]) -> None: + """Public helper to push a message to all portal WebSockets for a user.""" + await portal_connection_manager.send_json(user_id, message) + + # P1 #9 fix: ReAct event type -> TurnEventType mapping for EQ subscribers. # Preserves the original EQ contract so CLI and other subscribers that # filter on TurnEventType constants (e.g. 'turn.thinking') keep working. @@ -669,9 +724,7 @@ async def list_conversations(limit: int = 20, _auth: None = Depends(_verify_api_ # Re-derive title from the persisted user message so cache misses # after a restart don't surface the default placeholder. first_user = await _conversation_store.get_first_user_message(c.id) - title = _derive_conversation_title_from_content( - first_user.content if first_user else None - ) + title = _derive_conversation_title_from_content(first_user.content if first_user else None) result.append( { "id": c.id, @@ -736,6 +789,15 @@ async def get_conversation( } +@router.delete("/portal/conversations/{conversation_id}") +async def delete_conversation(conversation_id: str, _auth: None = Depends(_verify_api_key)): + """Delete a conversation and all its messages.""" + deleted = await _conversation_store.delete_conversation(conversation_id) + if not deleted: + raise HTTPException(status_code=404, detail=f"Conversation '{conversation_id}' not found") + return {"deleted": True, "id": conversation_id} + + def _derive_title_from_messages(messages: list) -> str: """Derive title from a list of Message objects (SessionManager format).""" for msg in messages: @@ -931,6 +993,13 @@ async def portal_websocket(websocket: WebSocket): await websocket.close(code=4001, reason="Invalid or missing api_key") return + # Track authenticated portal connections for user-scoped push (calendar + # reminders, etc.). user_id is None for API-key / dev-mode clients. + current_user = getattr(websocket.state, "current_user", None) or {} + ws_user_id: str | None = current_user.get("user_id") + if ws_user_id: + portal_connection_manager.add(ws_user_id, websocket) + # Wait for first chat message before creating conversation conv: Conversation | None = None # task_id is per-user-message; tracked here so the outer except can emit task.failed @@ -1658,3 +1727,7 @@ async def portal_websocket(websocket: WebSocket): await websocket.send_json({"type": "error", "data": {"message": str(e)}}) except Exception: pass + finally: + # Remove from user-scoped push tracking on any disconnect/error/return. + if ws_user_id: + portal_connection_manager.remove(ws_user_id, websocket) diff --git a/src/agentkit/tools/calendar_tool.py b/src/agentkit/tools/calendar_tool.py index ccdce32..8452310 100644 --- a/src/agentkit/tools/calendar_tool.py +++ b/src/agentkit/tools/calendar_tool.py @@ -13,6 +13,7 @@ from __future__ import annotations from typing import Any +from agentkit.calendar.models import ReminderRule from agentkit.calendar.service import CalendarService from agentkit.tools.base import Tool @@ -27,7 +28,8 @@ class CalendarTool(Tool): super().__init__( name="calendar", description=( - "Create, query, update, and delete calendar events. " + "Create, query, update, and delete calendar events and reminders. " + "Use create_event to schedule events and set reminders (e.g. 'remind me Monday morning'). " "Actions: create_event, query_events, update_event, delete_event." ), input_schema={ @@ -92,6 +94,15 @@ class CalendarTool(Tool): "type": "string", "description": "Conversation ID to associate with the event (create_event).", }, + "reminder_offset_minutes": { + "type": "integer", + "description": "Minutes before event start to fire a reminder. Negative means before, e.g. -15 = 15 min before (create_event).", + }, + "reminder_channels": { + "type": "array", + "items": {"type": "string"}, + "description": "Channels for the reminder, e.g. [\"client\"] (create_event).", + }, "start_date": { "type": "string", "description": "Range start, ISO 8601 UTC (query_events).", @@ -147,6 +158,33 @@ class CalendarTool(Tool): is_all_day = kwargs.get("is_all_day", False) rrule = kwargs.get("rrule") conversation_id = kwargs.get("conversation_id") + reminder_offset = kwargs.get("reminder_offset_minutes") + reminder_channels = kwargs.get("reminder_channels") or ["client"] + + # Build explicit reminder rules if requested + reminder_rules: list[ReminderRule] | None = None + if reminder_offset is not None: + try: + offset_int = int(reminder_offset) + except (TypeError, ValueError): + return { + "success": False, + "error": f"reminder_offset_minutes must be an integer, got {reminder_offset!r}", + } + if offset_int < 0 or offset_int > 43200: # 最多 30 天 + return { + "success": False, + "error": f"reminder_offset_minutes must be in [0, 43200], got {offset_int}", + } + reminder_rules = [ + ReminderRule( + id="", + event_id=None, + event_type_id=None, + offset_minutes=offset_int, + channels=list(reminder_channels), + ) + ] # Resolve event_type_name → event_type_id (look up or create) event_type_id: str | None = None @@ -174,6 +212,7 @@ class CalendarTool(Tool): source="agent", conversation_id=conversation_id, tag_ids=tag_ids, + reminder_rules=reminder_rules, ) return {"success": True, "event": event.to_dict()} except Exception as e: