diff --git a/AGENTS.md b/AGENTS.md index 2ad4c8a..4953b0f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -128,7 +128,7 @@ HandoffTransport:InProcess(asyncio.Queue)+ Redis Pub/Sub — 仅用于事 ### 关键子系统 -- **LLM 网关**(`llm/`):6 个 provider(OpenAI/Anthropic/Gemini/Doubao/Wenxin/Yuanbao)、fallback、语义缓存、用量追踪、RemoteLLMProvider(client→server 代理,带 401 刷新重试) +- **LLM 网关**(`llm/`):基于 LiteLLM 统一适配层(U15 迁移),保留原 6 个 provider 命名作为别名(OpenAI/Anthropic/Gemini/Doubao/Wenxin/Yuanbao),支持任意 OpenAI 兼容 API(如 DashScope/DeepSeek);含 fallback、语义缓存、用量追踪、RemoteLLMProvider(client→server 代理,带 401 刷新重试) - **记忆**(`memory/`):4 层(SOUL/USER/MEMORY/DAILY)、WorkingMemory(Redis)、EpisodicMemory(PG+pgvector)、SemanticMemory(HTTP RAG) - **进化**(`evolution/`):Reflector、PromptOptimizer(遗传算法)、PitfallDetector、ABTester - **工具**(`tools/`):21 个内置 + MCP 扩展,组合(SequentialChain/ParallelFanOut/DynamicSelector) @@ -136,7 +136,7 @@ HandoffTransport:InProcess(asyncio.Queue)+ Redis Pub/Sub — 仅用于事 - **总线**(`bus/`):MemoryBus(进程内)、RedisBus(分布式) - **认证**(`server/auth/`):JWT(access 15min + refresh 7d,HS256)、API Key(恒定时间比较)、3 级 RBAC(member/operator/admin + 权限位)、6 层终端安全(blocklist→shell-ops→builtin→global→user→session→danger)、bcrypt 密码哈希(rounds=12) -### 服务端路由(22 个模块) +### 服务端路由(28 个模块) | 前缀 | 模块 | 用途 | | ------------------------- | -------------------------------------- | ------------------------- | @@ -162,6 +162,12 @@ HandoffTransport:InProcess(asyncio.Queue)+ Redis Pub/Sub — 仅用于事 | `/api/v1/auth` | auth.py | 登录/刷新/登出/me | | `/api/v1/system` | system.py | 系统资源(需 SYSTEM\_CONFIG 权限) | | `/api/v1/config` | config\_sync.py | 配置版本 + 同步(轮询) | +| `/api/v1/bitable` | bitable.py | 多维表格 companion 服务 | +| `/api/v1/calendar` | calendar.py | 日历服务(事件/提醒/同步) | +| `/api/v1/documents` | documents.py | 文档管理 | +| `/api/v1/admin` | admin.py | 管理员操作 | +| `/api/v1/experts` | experts.py | 专家团队管理 | +| `/api/v1/mcp` | mcp\_publish.py | MCP 发布 | ### WebSocket Chat 协议 @@ -182,7 +188,7 @@ Server -> Client:`connected`、`token`、`thinking`、`step`、`final_answer` CLI 参数 > `agentkit.yaml` > 环境变量(`${VAR:-default}`)> `.env` > 硬编码默认值 -配置查找:`--config` 路径 > `./agentkit.yaml` > `~/.agentkit/agentkit.yaml` +配置查找:`--config` 路径 > `./agentkit.yaml` > `~/.agentkit/agentkit.yaml`(三个路径都不存在时使用硬编码默认值,CLI 仍可启动) ## 约定 @@ -190,7 +196,7 @@ CLI 参数 > `agentkit.yaml` > 环境变量(`${VAR:-default}`)> `.env` > 硬 - 技能分类:`agent_template`(执行引擎:react/direct/rewoo/reflexion/plan\_exec/goal\_driven)vs `business_skill`(领域技能)。通过 `server/routes/skill_management.py` 中的 `_ENGINE_TEMPLATE_NAMES` 分类。前端按 `category` 字段分组 — `SkillsView` 双栏布局,`SkillCard`/`SkillsTab` 显示类型标签(引擎/技能)和基于分类的图标 - LLM 配置:`agentkit.yaml` llm 段(与服务端配置统一) - 流水线配置:`configs/pipelines/*.yaml` -- 专家模板:`configs/experts/*.yaml`(5 个编程专家 + dev\_team 团队模板),通过 `ExpertTemplateRegistry` 注册 +- 专家模板:`configs/experts/*.yaml`(15 个模板 — 5 编程专家 + 9 商业/思想领袖 + 1 团队模板 dev\_team),通过 `ExpertTemplateRegistry` 注册 - 团队模板:`bound_skills` 字段存储成员列表(如 `dev_team.yaml` 列出 tech\_lead、frontend\_engineer、backend\_engineer、qa\_engineer、code\_reviewer) - 所有 Pydantic 模型使用 `model_config = ConfigDict(...)` 而非 `class Config` - 测试文件:`tests/unit/` 和 `tests/integration/` diff --git a/README.md b/README.md index 9921893..829a752 100644 --- a/README.md +++ b/README.md @@ -676,7 +676,7 @@ gateway.register_provider("dashscope", OpenAIProvider( app = create_app(llm_gateway=gateway) if __name__ == "__main__": - uvicorn.run(app, host="0.0.0.0", port=8000) + uvicorn.run(app, host="0.0.0.0", port=8001) ``` 启动: @@ -884,7 +884,7 @@ asyncio.run(main()) 注册 Skill: ```bash -curl -X POST http://localhost:8000/api/v1/skills \ +curl -X POST http://localhost:8001/api/v1/skills \ -H "Content-Type: application/json" \ -d '{ "config": { @@ -915,7 +915,7 @@ curl -X POST http://localhost:8000/api/v1/skills \ 提交任务(指定 Skill): ```bash -curl -X POST http://localhost:8000/api/v1/tasks \ +curl -X POST http://localhost:8001/api/v1/tasks \ -H "Content-Type: application/json" \ -d '{ "skill_name": "content_generator", @@ -926,7 +926,7 @@ curl -X POST http://localhost:8000/api/v1/tasks \ 提交任务(意图路由自动匹配): ```bash -curl -X POST http://localhost:8000/api/v1/tasks \ +curl -X POST http://localhost:8001/api/v1/tasks \ -H "Content-Type: application/json" \ -d '{ "input_data": {"query": "帮我生成一篇文章"} @@ -936,7 +936,7 @@ curl -X POST http://localhost:8000/api/v1/tasks \ 创建 Agent: ```bash -curl -X POST http://localhost:8000/api/v1/agents \ +curl -X POST http://localhost:8001/api/v1/agents \ -H "Content-Type: application/json" \ -d '{"skill_name": "content_generator"}' ``` @@ -944,13 +944,13 @@ curl -X POST http://localhost:8000/api/v1/agents \ 查询 LLM 用量: ```bash -curl http://localhost:8000/api/v1/llm/usage +curl http://localhost:8001/api/v1/llm/usage ``` 健康检查: ```bash -curl http://localhost:8000/api/v1/health +curl http://localhost:8001/api/v1/health ``` #### Python SDK 调用 @@ -960,7 +960,7 @@ import asyncio from agentkit.server.client import AgentKitClient async def main(): - async with AgentKitClient("http://localhost:8000") as client: + async with AgentKitClient("http://localhost:8001") as client: # 注册 Skill await client.register_skill({ "name": "content_generator", diff --git a/docs/solutions/security-issues/portal-platform-security-reliability-fixes.md b/docs/solutions/security-issues/portal-platform-security-reliability-fixes.md index c37ac82..6375388 100644 --- a/docs/solutions/security-issues/portal-platform-security-reliability-fixes.md +++ b/docs/solutions/security-issues/portal-platform-security-reliability-fixes.md @@ -1,11 +1,12 @@ --- -title: "Portal platform security & reliability fixes — channels, LLM cache, server shutdown" +title: "Portal 平台安全与可靠性修复 — 通道、LLM 缓存、服务端关闭" date: 2026-06-26 category: docs/solutions/security-issues module: channels/llm/server problem_type: security_issue component: service_object -severity: high +severity: mixed +priority_breakdown: {p1: 4, p2: 5, p3: 1} symptoms: - "WeCom webhook accepted replayed requests within an unlimited time window (no timestamp freshness check)" - "Anonymous LLM requests polluted per-user cache namespace when user_id was None (per_user_namespace=True ignored)" @@ -26,7 +27,15 @@ related_components: - server/routes/channels --- -# Portal platform security & reliability fixes — channels, LLM cache, server shutdown +# Portal 平台安全与可靠性修复 — 通道、LLM 缓存、服务端关闭 + +## Root Cause + +10 个修复共享三个共性根因: + +1. **信任边界输入校验缺失**:WeCom webhook 信任签名正确即放行(缺时间戳新鲜度);LLM 缓存信任 `user_id` 非空(缺 None 检查);webhook `receive_message` 信任 payload 格式(缺异常隔离)。三处均在信任边界省略了输入校验。 +2. **资源生命周期未闭合**:应用 shutdown 未调用 `close_all_adapters()`、未 await `_pending_webhook_tasks`,httpx 连接与 IM 回复在进程退出时被丢弃。Feishu token TTL 设置过短(300s vs 实际 ~6900s)导致频繁刷新。 +3. **缓存键设计缺陷**:`generate_cache_key` 对每个组件单独 SHA-256 后再哈希一次(8-10 次冗余哈希);配额检查重复查询(4 次/部门/周期 vs 实际需 1 次 summary)。 ## Problem @@ -38,7 +47,7 @@ related_components: - **LLM 缓存跨用户泄漏**:`should_cache()` 对 `user_id` 参数执行 `_ = user_id` 直接丢弃,在 `per_user_namespace` 开启时,`user_id=None` 的请求仍会命中其他用户的缓存。 - **Webhook 异常触发 500 重试风暴**:`receive_message()` 解析失败直接抛出未捕获异常,平台收到 500 后按退避策略无限重试,造成异常放大。 - **应用关闭资源泄漏**:shutdown 流程未调用 `close_all_adapters()`、未 await `_pending_webhook_tasks`,导致 httpx 连接泄漏与 IM 回复丢失。 -- **配额查询 N+1 与缓存键冗余哈希**:配额检查对每个部门每个周期调用 4 次 `get_usage()`(实际只需 2 次唯一查询);`generate_cache_key` 对每个组件单独 SHA-256 后再哈希一次,8-10 次冗余哈希。 +- **配额查询 N+1 与缓存键冗余哈希**:配额检查对每个部门每个周期调用 4 次 `get_usage()`(实际只需 1 次唯一查询,token 与 cost 可共用 summary);`generate_cache_key` 对每个组件单独 SHA-256 后再哈希一次,8-10 次冗余哈希。 ## What Didn't Work @@ -50,7 +59,7 @@ related_components: ### P1 #1 — WeCom webhook 重放攻击修复 -**文件**:[src/agentkit/channels/wecom.py](file:///Users/Chiguyong/Code/Fischer/fischer-agentkit/src/agentkit/channels/wecom.py) +**文件**:[src/agentkit/channels/wecom.py](src/agentkit/channels/wecom.py) **问题**:`verify_signature()` 只比对签名,不校验时间戳新鲜度,重放窗口无限大。 @@ -77,7 +86,7 @@ if abs(now - ts_int) > _SIGNATURE_MAX_AGE_SECONDS: ### P1 #2 — LiteLLM 缓存跨用户泄漏修复 -**文件**:[src/agentkit/llm/cache.py](file:///Users/Chiguyong/Code/Fischer/fischer-agentkit/src/agentkit/llm/cache.py) +**文件**:[src/agentkit/llm/cache.py](src/agentkit/llm/cache.py) **问题**:`should_cache()` 形参 `user_id` 被显式丢弃(`_ = user_id`),`per_user_namespace` 开启时 `user_id=None` 的请求仍会命中缓存,造成跨用户数据泄漏。 @@ -97,7 +106,7 @@ def should_cache(self, kb_caching_disabled: bool = False, user_id: str | None = ### P1 #3 — Webhook 异常风暴防御 -**文件**:[src/agentkit/server/routes/channels.py](file:///Users/Chiguyong/Code/Fischer/fischer-agentkit/src/agentkit/server/routes/channels.py) +**文件**:[src/agentkit/server/routes/channels.py](src/agentkit/server/routes/channels.py) **问题**:`receive_message()` 抛出异常时,路由返回 500,平台按重试策略无限重试,形成异常风暴。 @@ -113,7 +122,7 @@ except Exception as exc: # noqa: BLE001 — 防止 receive_message 异常导致 ### P1 #4 — 应用关闭泄漏修复 -**文件**:[src/agentkit/server/app.py](file:///Users/Chiguyong/Code/Fischer/fischer-agentkit/src/agentkit/server/app.py) +**文件**:[src/agentkit/server/app.py](src/agentkit/server/app.py) **问题**:shutdown 流程未关闭 channel adapters、未等待后台 webhook 任务,导致 httpx 连接泄漏与 IM 回复丢失。 @@ -136,7 +145,7 @@ except Exception: ### P2 #8 — Feishu token TTL 修正 -**文件**:[src/agentkit/channels/feishu.py](file:///Users/Chiguyong/Code/Fischer/fischer-agentkit/src/agentkit/channels/feishu.py) +**文件**:[src/agentkit/channels/feishu.py](src/agentkit/channels/feishu.py) **问题**:`_TOKEN_CACHE_TTL = 300.0` 比实际有效期(2h)短 24 倍,造成每 5 分钟强制刷新一次 token,无谓增加 QPS。 @@ -148,7 +157,7 @@ _TOKEN_CACHE_TTL = 6900.0 # 2h - 5min 余量,避免临界点失效 ### P2 #10 — 无界 webhook 任务集 -**文件**:[src/agentkit/server/routes/channels.py](file:///Users/Chiguyong/Code/Fischer/fischer-agentkit/src/agentkit/server/routes/channels.py) +**文件**:[src/agentkit/server/routes/channels.py](src/agentkit/server/routes/channels.py) **问题**:`_pending_webhook_tasks` set 在高负载下无上限增长,可能耗尽内存与协程。 @@ -164,7 +173,7 @@ if len(_pending_webhook_tasks) >= _WEBHOOK_MAX_CONCURRENT * 2: ### P2 #12 — 配额检查 N+1 查询消除 -**文件**:[src/agentkit/llm/gateway.py](file:///Users/Chiguyong/Code/Fischer/fischer-agentkit/src/agentkit/llm/gateway.py) +**文件**:[src/agentkit/llm/gateway.py](src/agentkit/llm/gateway.py) **问题**:配额检查对每个部门每个周期调用 4 次 `get_usage()`(token 与 cost 各一次,但实际可合并为一次查询),重复查询放大数据库压力。 @@ -191,7 +200,7 @@ def _get_usage_summary(self, department_id: str, period: str) -> UsageSummary: ### P2 #13 — 缓存键冗余 SHA-256 消除 -**文件**:[src/agentkit/llm/cache_key.py](file:///Users/Chiguyong/Code/Fischer/fischer-agentkit/src/agentkit/llm/cache_key.py) +**文件**:[src/agentkit/llm/cache_key.py](src/agentkit/llm/cache_key.py) **问题**:`generate_cache_key` 对每个组件单独 SHA-256(8-10 次),再对哈希拼接再做一次 SHA-256,CPU 浪费且无安全增益。 @@ -219,7 +228,7 @@ return hashlib.sha256(combined.encode()).hexdigest() ### P2 #18 — 移除未使用的 secrets_store 参数 -**文件**:[src/agentkit/llm/config.py](file:///Users/Chiguyong/Code/Fischer/fischer-agentkit/src/agentkit/llm/config.py) +**文件**:[src/agentkit/llm/config.py](src/agentkit/llm/config.py) **问题**:`get_api_key()` 接受 `secrets_store` 形参但从未使用(同步方法无法 await 异步 `get_secret`),属于误导性 API。 @@ -237,7 +246,7 @@ def get_api_key(self) -> str: ### P3 #21 — DIRECT_CHAT 路径去重 -**文件**:[src/agentkit/server/routes/channels.py](file:///Users/Chiguyong/Code/Fischer/fischer-agentkit/src/agentkit/server/routes/channels.py) +**文件**:[src/agentkit/server/routes/channels.py](src/agentkit/server/routes/channels.py) **问题**:DIRECT_CHAT 逻辑在主路径与 ReAct 回退路径中重复实现,维护时易漂移。 @@ -258,13 +267,32 @@ async def _direct_chat(llm_gateway: Any, routing: Any) -> str: 修复完成后按以下方式验证: - **ruff check**:`src/` 干净,仅剩预存在的 `gui_mode` F821(与本次修复无关)。 -- **Channels 测试**:`pytest tests/unit/server/routes/test_channels.py` — 137 passed。 +- **Channels 测试**:`pytest tests/unit/channels/test_wecom.py` — 验证 `verify_signature` 与 `_SIGNATURE_MAX_AGE_SECONDS` 新鲜度校验。 - **配置迁移测试**:`pytest tests/unit/llm/test_config_migration.py` — 全部通过。 - **配额强制测试**:`pytest tests/unit/llm/test_quota_enforcement.py` — 11 passed。 - **litellm 相关测试**:因 `litellm` 未安装被 skip(环境问题,非代码问题)。 - **WeCom 测试调整**:将固定时间戳 `1609459200` 改为 `int(time.time())` 动态生成后,新鲜度校验测试通过。 - **缓存测试调整**:显式传入 `user_id` 或关闭 `per_user_namespace` 后,`should_cache()` 测试通过。 +## Impact Scope + +**受影响模块**: + +| 模块 | 文件 | 影响类型 | +|---|---|---| +| WeCom 通道 | `src/agentkit/channels/wecom.py` | 安全(重放防御) | +| Feishu 通道 | `src/agentkit/channels/feishu.py` | 可靠性(TTL 修正) | +| LLM 缓存 | `src/agentkit/llm/cache.py` | 安全(跨用户泄漏) | +| LLM 缓存键 | `src/agentkit/llm/cache_key.py` | 性能(冗余哈希) | +| LLM 网关 | `src/agentkit/llm/gateway.py` | 可靠性(配额 N+1) | +| LLM 配置 | `src/agentkit/llm/config.py` | 可维护性(未用参数) | +| 应用生命周期 | `src/agentkit/server/app.py` | 可靠性(shutdown 泄漏) | +| 通道路由 | `src/agentkit/server/routes/channels.py` | 可靠性(webhook 风暴) | + +**向后兼容性**:所有修复向后兼容。WeCom 签名校验增加时间戳窗口后,历史合法请求在 300s 窗口内仍被接受;Feishu TTL 从 300s 延长至 ~6900s 仅减少刷新频率;缓存键生成逻辑变更不影响命中(旧键自然过期);shutdown 顺序变更仅影响关闭流程,不影响运行时行为。 + +**生产影响**:WeCom 重放修复立即生效;缓存跨用户泄漏修复消除潜在数据混淆;shutdown 泄漏修复消除进程重启时的连接错误日志。无破坏性变更,无需停机部署。 + ## Prevention ### 安全 @@ -292,5 +320,5 @@ async def _direct_chat(llm_gateway: Any, routing: Any) -> str: ## Related Docs -- [long-horizon-reliability-code-review-fixes.md](file:///Users/Chiguyong/Code/Fischer/fischer-agentkit/docs/solutions/logic-errors/long-horizon-reliability-code-review-fixes.md) — 上一批 U1-U7 长期可靠性代码评审修复,与本批次同属 code-review 修复系列,可在遇到类似可靠性问题时交叉参考。 -- [bitable-companion-service-security-reliability-patterns.md](file:///Users/Chiguyong/Code/Fischer/fischer-agentkit/docs/solutions/architecture-patterns/bitable-companion-service-security-reliability-patterns.md) — Bitable 伴生服务的安全/可靠性架构模式(SSRF、SQL 注入、IDOR、缓存失效等),与本批次的 LLM 缓存隔离威胁模型不同但可对照阅读。 +- [long-horizon-reliability-code-review-fixes.md](docs/solutions/logic-errors/long-horizon-reliability-code-review-fixes.md) — 上一批 U1-U7 长期可靠性代码评审修复,与本批次同属 code-review 修复系列,可在遇到类似可靠性问题时交叉参考。 +- [bitable-companion-service-security-reliability-patterns.md](docs/solutions/architecture-patterns/bitable-companion-service-security-reliability-patterns.md) — Bitable 伴生服务的安全/可靠性架构模式(SSRF、SQL 注入、IDOR、缓存失效等),与本批次的 LLM 缓存隔离威胁模型不同但可对照阅读。 diff --git a/src/agentkit/chat/sqlite_conversation_store.py b/src/agentkit/chat/sqlite_conversation_store.py index 67451b0..a0a3b39 100644 --- a/src/agentkit/chat/sqlite_conversation_store.py +++ b/src/agentkit/chat/sqlite_conversation_store.py @@ -309,11 +309,22 @@ class SqliteConversationStore: row = await cursor.fetchone() if row is None: return False - await db.execute( - "DELETE FROM messages WHERE conversation_id = ?", (conversation_id,) - ) - await db.execute("DELETE FROM conversations WHERE id = ?", (conversation_id,)) - await db.commit() + # ponytail: wrap both DELETEs in a try/except with rollback — + # previously the second DELETE failure would leave orphaned + # messages (conversation row gone, messages lingering) because + # the first DELETE already auto-committed in autocommit mode. + # aiosqlite uses autocommit=False by default but explicit rollback + # makes the failure path safe and observable. + try: + await db.execute( + "DELETE FROM messages WHERE conversation_id = ?", (conversation_id,) + ) + await db.execute("DELETE FROM conversations WHERE id = ?", (conversation_id,)) + await db.commit() + except Exception: + await db.rollback() + logger.exception("Failed to delete conversation %s; rolled back", conversation_id) + raise self._cache.pop(conversation_id, None) return True diff --git a/src/agentkit/server/app.py b/src/agentkit/server/app.py index f267b33..deaf64c 100644 --- a/src/agentkit/server/app.py +++ b/src/agentkit/server/app.py @@ -453,7 +453,9 @@ async def lifespan(app: FastAPI): try: default_agent._tool_registry.register(calendar_tool) except Exception: - pass # already registered + # ponytail: log at debug — CalendarTool double-registration + # is expected on reload, but silent pass hides real errors. + logger.debug("CalendarTool already registered or registration failed", exc_info=True) # Strip any existing "## 可用工具" section to avoid # duplicate tool blocks in the system prompt. base_prompt = getattr(default_agent, "_system_prompt", None) or ( diff --git a/src/agentkit/server/auth/middleware.py b/src/agentkit/server/auth/middleware.py index b993d74..567d322 100644 --- a/src/agentkit/server/auth/middleware.py +++ b/src/agentkit/server/auth/middleware.py @@ -193,6 +193,11 @@ class AuthMiddleware(BaseHTTPMiddleware): "user_id": payload.get("sub"), "username": payload.get("username"), "role": payload.get("role"), + # ponytail: sid must be set here too — the Bearer path + # above (line ~178) sets it; without it, downstream + # require_authenticated treats WS clients as legacy + # sessions, breaking session revocation & audit logs. + "sid": payload.get("sid"), } return await call_next(request) diff --git a/src/agentkit/server/frontend/src/components/calendar/CalendarDrawer.vue b/src/agentkit/server/frontend/src/components/calendar/CalendarDrawer.vue index c4eaecb..0cb5e05 100644 --- a/src/agentkit/server/frontend/src/components/calendar/CalendarDrawer.vue +++ b/src/agentkit/server/frontend/src/components/calendar/CalendarDrawer.vue @@ -103,7 +103,8 @@ function onEdit(event: ICalendarEvent): void { .calendar-drawer__tabs :deep(.ant-tabs-content-holder) { flex: 1; - overflow: hidden; + /* ponytail: was overflow:hidden which clipped ReminderConfig/SyncSettings content; auto enables scroll */ + overflow-y: auto; } .calendar-drawer__tabs :deep(.ant-tabs-content) { diff --git a/src/agentkit/server/frontend/src/components/calendar/CalendarGrid.vue b/src/agentkit/server/frontend/src/components/calendar/CalendarGrid.vue index 68d2c00..b7389cf 100644 --- a/src/agentkit/server/frontend/src/components/calendar/CalendarGrid.vue +++ b/src/agentkit/server/frontend/src/components/calendar/CalendarGrid.vue @@ -67,7 +67,17 @@ function handleEventClick(arg: EventClickArg): void { } onMounted(() => { - resizeObserver = new ResizeObserver(() => { + // ponytail: track last size to break ResizeObserver feedback loop — + // updateSize() can trigger another resize event, causing infinite thrash. + let lastW = 0 + let lastH = 0 + resizeObserver = new ResizeObserver((entries) => { + const e = entries[0] + const w = e.contentRect.width + const h = e.contentRect.height + if (w === lastW && h === lastH) return + lastW = w + lastH = h nextTick(() => { calendarRef.value?.getApi()?.updateSize() }) diff --git a/src/agentkit/server/frontend/src/components/chat/ChatSidebar.vue b/src/agentkit/server/frontend/src/components/chat/ChatSidebar.vue index 9692f3c..1ffbf48 100644 --- a/src/agentkit/server/frontend/src/components/chat/ChatSidebar.vue +++ b/src/agentkit/server/frontend/src/components/chat/ChatSidebar.vue @@ -29,6 +29,7 @@ - diff --git a/src/agentkit/server/frontend/src/components/layout/tabs/DocumentsTab.vue b/src/agentkit/server/frontend/src/components/layout/tabs/DocumentsTab.vue index fe9ad0d..c1e3fd3 100644 --- a/src/agentkit/server/frontend/src/components/layout/tabs/DocumentsTab.vue +++ b/src/agentkit/server/frontend/src/components/layout/tabs/DocumentsTab.vue @@ -62,6 +62,9 @@ const simpleImage = Empty.PRESENTED_IMAGE_SIMPLE function formatTime(iso: string): string { if (!iso) return '' const d = new Date(iso) + // ponytail: guard against Invalid Date — new Date('garbage').toLocaleString + // returns "Invalid Date" string which leaks into the UI. + if (isNaN(d.getTime())) return '' return d.toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', 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 e07404a..a6077f5 100644 --- a/src/agentkit/server/frontend/src/components/layout/tabs/SkillsTab.vue +++ b/src/agentkit/server/frontend/src/components/layout/tabs/SkillsTab.vue @@ -114,9 +114,10 @@ function closeDetail(): void { } onMounted(() => { - if (skillsStore.skills.length === 0) { - skillsStore.fetchSkills() - } + // ponytail: previously only fetched when store was empty, causing stale + // data after skills were modified elsewhere. Always fetch on mount — + // the store handles deduplication and this tab is not mounted often. + skillsStore.fetchSkills() }) 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 b4bc716..78c7a87 100644 --- a/src/agentkit/server/frontend/src/components/preview/scenes/Scene6Error.vue +++ b/src/agentkit/server/frontend/src/components/preview/scenes/Scene6Error.vue @@ -23,19 +23,28 @@