From 95e25b1b32fe82b105b8cc46ca57d7700b9129e0 Mon Sep 17 00:00:00 2001 From: mingming Date: Sat, 23 May 2026 15:16:05 +0800 Subject: [PATCH 01/30] docs: add DESIGN.md compliance fix specification Co-Authored-By: Claude Opus 4.7 --- .../2026-05-23-design-md-compliance-fix.md | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-23-design-md-compliance-fix.md diff --git a/docs/superpowers/specs/2026-05-23-design-md-compliance-fix.md b/docs/superpowers/specs/2026-05-23-design-md-compliance-fix.md new file mode 100644 index 0000000..53ef252 --- /dev/null +++ b/docs/superpowers/specs/2026-05-23-design-md-compliance-fix.md @@ -0,0 +1,109 @@ +# DESIGN.md Compliance Fix — Specification + +## Status + +Approved + +## Date + +2026-05-23 + +## Owner + +Claude Code + +## Deciders + +User + +## Context + +项目已创建 `DESIGN.md` 设计系统规范,但现有前端代码中存在大量硬编码颜色值、font-size、间距和圆角,未使用 `--eify-*` CSS 变量和 `.eify-*` 组件类名。通过全局扫描发现约 20 个文件需要修复,按严重程度分为 HIGH/MEDIUM/LOW 三级。 + +## Decision + +采用 **Approach A:按文件优先级逐个修**,每修完一个批次跑 `vue-tsc --noEmit` + `vitest run` 验证。 + +### Scope + +全修:硬编码颜色、font-size、padding/margin、border-radius。 + +### 豁免项 + +- SVG `` 标签中的颜色(不支持 CSS 变量) +- 工作流节点类型色(`--wf-node-color`,属于领域设计令牌) +- MCP Server 日志查看器的 Catppuccin 主题色 +- Element Plus `:deep()` 覆盖中的 CSS 变量 fallback 值 +- `LoginView.vue` 的 `#0f0f1a` 背景色(设计令牌无对应值,保留原样) +- 48px 以上大字号(Logo 专用,不适用工具类) + +### 修复顺序(4 批 20 个文件) + +**Batch 1 — 品牌色重灾区:** `LoginView.vue` +**Batch 2 — 共享组件:** `EifySearch.vue`, `EifyHeader.vue`, `ConfirmDialog.vue`, `EifyListPage.vue`, `EifyFormDialog.vue`, `EifyTable.vue`, `DocumentPreview.vue` +**Batch 3 — 业务页面:** `ChatView.vue`, `AgentList.vue`, `ProviderList.vue`, `McpServerList.vue`, `KnowledgeView.vue`, `DocumentView.vue`, `WorkflowList.vue`, `ProfileView.vue` +**Batch 4 — 工作流相关:** `WorkflowEdit.vue`, `WorkflowCreate.vue`, `NodePanel.vue` + +### 颜色替换映射 + +| 硬编码 | 替换为 | +|:---|:---| +| `#ffffff` (bg) | `var(--eify-bg-base)` | +| `#fff` (text) | `var(--eify-text-inverse)` | +| `#f8fafc` | `var(--eify-bg-secondary)` | +| `#f1f5f9` | `var(--eify-bg-surface)` | +| `#fef2f2` | `var(--eify-error-light)` | +| `#fecaca` | `var(--eify-error-200)` | +| `#6366f1` | `var(--eify-primary)` | +| `#8b5cf6` | `var(--eify-primary-400)` | +| `#4f46e5` | `var(--eify-primary-600)` | +| `gradient(#6366f1, #8b5cf6)` | `var(--eify-gradient-primary)` | +| `#ef4444` | `var(--eify-error)` | +| `#dc2626` | `var(--eify-error-600)` | +| `#f59e0b` | `var(--eify-warning)` | +| `#fbbf24` | `var(--eify-warning-400)` | +| `#d97706` | `var(--eify-warning-600)` | +| `#22c55e` | `var(--eify-success)` | +| `#059669` | `var(--eify-success-600)` | +| `#3b82f6` | `var(--eify-info-500)` | +| `#e2e8f0` / `#e5e7eb` | `var(--eify-border-default)` | +| `#0f172a` | `var(--eify-text-primary)` | +| `#1e293b` | `var(--eify-gray-800)` | +| `#334155` | `var(--eify-gray-700)` | +| `#475569` | `var(--eify-text-secondary)` | +| `#64748b` | `var(--eify-text-secondary)` | +| `#94a3b8` | `var(--eify-text-tertiary)` | +| `#a5b4fc` | `var(--eify-primary-300)` | + +### font-size 替换 + +| px | 工具类 | +|:---|:---| +| 10-12px | `.text-xs` | +| 13px | `.text-sm` | +| 14px | `.text-base` | +| 15-16px | `.text-lg` | +| 18px | `.text-xl` | +| 20-22px | `.text-2xl` | +| 24px | `.text-3xl` | +| 28px+ | 保留 | + +### 验证策略 + +每批完成后:`vue-tsc --noEmit` + `vitest run` +全部完成后:`mvn test -q` + +### 完成标准 + +- 20 个文件硬编码色值全部替换 +- font-size 替换为工具类 +- 豁免项保持原样 +- TypeScript 类型检查通过 +- 前后端测试通过 +- git diff 逐文件自查确认 + +## Consequences + +- AI 生成的前端代码将自动使用 Eify 设计令牌,视觉一致性大幅提升 +- 后续新增页面只需参照 DESIGN.md,无需记忆色值 +- LoginView 的 `#0f0f1a` 背景色需后续纳入设计令牌体系 From cf39a4da62fab42add7e3c2986d3e003562516ce Mon Sep 17 00:00:00 2001 From: mingming Date: Sat, 23 May 2026 15:17:42 +0800 Subject: [PATCH 02/30] docs: move spec to docs/specs, enforce spec location convention in CLAUDE.md Co-Authored-By: Claude Opus 4.7 --- CLAUDE.md | 3 ++- .../specs/2026-05-23-design-md-compliance-fix.md | 0 2 files changed, 2 insertions(+), 1 deletion(-) rename docs/{superpowers => }/specs/2026-05-23-design-md-compliance-fix.md (100%) diff --git a/CLAUDE.md b/CLAUDE.md index f420e06..e759127 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -105,7 +105,7 @@ grep 'YOUR_TRACE_ID' ./logs/eify.log | jq | **docs/** | 按开发者查阅路径组织的规范文档 | `ARCHITECTURE.md`、`API-SPEC.md` | | **docs/guides/** | HOW-TO 指南(体量大、持续更新) | `DATABASE.md`、`LOGGING.md` | | **docs/ADRs/** | 设计决策记录(ADR,一次性归档) | `ADR-0001-cursor-pagination-improvement.md` | -| **docs/specs/** | 功能实现规格说明(开发前编写,实施中迭代) | `2026-05-23-mcp-workspace-isolation-fix-design.md` | +| **docs/specs/** | 功能实现规格说明(开发前编写,实施中迭代)。**所有 spec 必须放此目录,禁止使用 `docs/superpowers/specs/` 等其他路径** | `2026-05-23-mcp-workspace-isolation-fix-design.md` | | **scripts/** | 开发/运维工具脚本 | `mock-mcp-server.py` | | **deploy/** | 部署配置和脚本 | `Dockerfile`、`nginx.conf`、`k8s/`、`infra/` | | **deploy/infra/deploy/** | Docker Compose 和部署脚本 | `docker-compose.yml`、`deploy-local.sh` | @@ -120,6 +120,7 @@ grep 'YOUR_TRACE_ID' ./logs/eify.log | jq - **规范文档**(docs/):开发时随时查阅的入口文档,持续更新,与代码同步 - **HOW-TO 指南**(docs/guides/):体量较大的操作指南,持续更新 - **决策记录**(docs/ADRs/):一次性写入的 ADR,基本不改 +- **规格说明**(docs/specs/):功能实现前的设计文档,命名 `YYYY-MM-DD--design.md` - 每个指南对应一个开发场景:"我要建表" → guides/DATABASE.md,"我要加日志" → guides/LOGGING.md ### 环境配置规范 diff --git a/docs/superpowers/specs/2026-05-23-design-md-compliance-fix.md b/docs/specs/2026-05-23-design-md-compliance-fix.md similarity index 100% rename from docs/superpowers/specs/2026-05-23-design-md-compliance-fix.md rename to docs/specs/2026-05-23-design-md-compliance-fix.md From 9826ebd694f71955e232c17a0dcba9e93eb72600 Mon Sep 17 00:00:00 2001 From: mingming Date: Sat, 23 May 2026 15:20:45 +0800 Subject: [PATCH 03/30] docs: expand spec with visual verification strategy and 12-page checklist Co-Authored-By: Claude Opus 4.7 --- .../2026-05-23-design-md-compliance-fix.md | 66 ++++++++++++++++--- 1 file changed, 58 insertions(+), 8 deletions(-) diff --git a/docs/specs/2026-05-23-design-md-compliance-fix.md b/docs/specs/2026-05-23-design-md-compliance-fix.md index 53ef252..11ceb7c 100644 --- a/docs/specs/2026-05-23-design-md-compliance-fix.md +++ b/docs/specs/2026-05-23-design-md-compliance-fix.md @@ -90,17 +90,67 @@ User ### 验证策略 -每批完成后:`vue-tsc --noEmit` + `vitest run` -全部完成后:`mvn test -q` +CSS 变量替换本质上是**视觉层变更**,当前项目没有视觉回归测试(如 Storybook + Chromatic)。验证分两层:自动化保障代码不坏,人工保障视觉不错。 + +#### 自动验证(每批 + 全部) + +每批文件改完后: +```bash +cd eify-web && npx vue-tsc --noEmit && cd .. # TypeScript 类型检查 +cd eify-web && npx vitest run && cd .. # 前端单元测试(1 文件 11 用例) +``` + +全部改完后: +```bash +mvn test -q # 后端全部测试 +``` + +**自动化验证能发现的:** +- 类型错误(import 路径、props 类型不匹配) +- ConfirmDialog 组件逻辑被打破 +- 后端编译/测试失败 + +**自动化验证检测不到的:** +- CSS 变量替换后颜色偏差(如目标 token 的色值与原硬编码不完全一致) +- font-size 工具类与原始 px 值的微小视觉差异(如 11px → `.text-xs`(12px)) +- CSS 优先级变化导致样式失效 +- 替换后遗漏导致部分元素回退到浏览器默认样式 + +#### 视觉验证(全改完后手动执行) + +```bash +./start.sh dev # 启动应用 +``` + +逐页目视检查清单(12 页): + +| # | 页面 | 路由 | 重点检查 | +|:---|:---|:---|:---| +| 1 | 登录页 | `/login` | 背景色、品牌渐变、按钮颜色、输入框聚焦环 | +| 2 | Agent 列表 | `/agents` | 卡片背景、表格行 hover、标签颜色、搜索框 | +| 3 | Chat 对话 | `/chat` | 聊天气泡渐变、错误提示色、加载动画 | +| 4 | Provider 列表 | `/providers` | 卡片、表格、状态标签 | +| 5 | MCP Server 列表 | `/mcp-servers` | 卡片、日志查看器颜色(Catppuccin 豁免区) | +| 6 | 知识库 | `/knowledge` | 卡片、搜索区、上传区域 | +| 7 | 文档 | `/documents` | 预览组件、分页 | +| 8 | 工作流列表 | `/workflows` | 卡片、状态色(running/stopped/error) | +| 9 | 工作流创建 | `/workflows/create` | 节点面板、表单输入框 | +| 10 | 工作流编辑 | `/workflows/:id/edit` | 画布连接线、节点颜色、变量表格 | +| 11 | 个人设置 | `/profile` | 头像渐变、表单、分页 | +| 12 | 侧边栏 + 顶栏 | 全局 | 菜单 hover/active 效果、Logo glow、折叠按钮 | + +每页通过标准:颜色无异常变化、文字层级正确、交互反馈(hover/active/focus)正常。 ### 完成标准 -- 20 个文件硬编码色值全部替换 -- font-size 替换为工具类 -- 豁免项保持原样 -- TypeScript 类型检查通过 -- 前后端测试通过 -- git diff 逐文件自查确认 +- [ ] 20 个文件硬编码色值全部替换 +- [ ] font-size 替换为工具类 +- [ ] 豁免项保持原样 +- [ ] `vue-tsc --noEmit` 通过 +- [ ] `vitest run` 通过 +- [ ] `mvn test -q` 通过 +- [ ] 12 页视觉检查全部通过 +- [ ] git diff 逐文件自查确认 ## Consequences From f62e2a57ad6977150240cd115e90c58f4b77bb83 Mon Sep 17 00:00:00 2001 From: mingming Date: Sat, 23 May 2026 15:24:15 +0800 Subject: [PATCH 04/30] docs: add DESIGN.md compliance fix implementation plan 20 tasks across 4 batches, shared replacement rules, final verification. Co-Authored-By: Claude Opus 4.7 --- .../2026-05-23-design-md-compliance-fix.md | 727 ++++++++++++++++++ 1 file changed, 727 insertions(+) create mode 100644 docs/plans/2026-05-23-design-md-compliance-fix.md diff --git a/docs/plans/2026-05-23-design-md-compliance-fix.md b/docs/plans/2026-05-23-design-md-compliance-fix.md new file mode 100644 index 0000000..1c478b5 --- /dev/null +++ b/docs/plans/2026-05-23-design-md-compliance-fix.md @@ -0,0 +1,727 @@ +# DESIGN.md Compliance Fix — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace all hardcoded CSS values in 20 Vue files with `--eify-*` design tokens and `.eify-*` / `.text-*` utility classes. + +**Architecture:** Four-batch file-by-file approach. Each task edits one file — locate hardcoded values, replace per the mapping rules, verify with `vue-tsc --noEmit`. Commit per batch, full `vitest run` + `mvn test` at the end. + +**Tech Stack:** Vue 3 SFC with scoped CSS, CSS custom properties, utility classes from `eify-web/src/styles/utilities.css`. + +**Source spec:** `docs/specs/2026-05-23-design-md-compliance-fix.md` + +--- + +## Shared Replacement Rules + +Apply these to every file below. Read the file first, identify each violation, then edit. + +### Color Replacements + +| Hardcoded | Replace with | +|:---|:---| +| `#ffffff` (used as background) | `var(--eify-bg-base)` | +| `#fff` (used as text color) | `var(--eify-text-inverse)` | +| `#f8fafc` | `var(--eify-bg-secondary)` | +| `#f1f5f9` | `var(--eify-bg-surface)` | +| `#fef2f2` | `var(--eify-error-light)` | +| `#fecaca` | `var(--eify-error-200)` | +| `#6366f1` | `var(--eify-primary)` | +| `#8b5cf6` | `var(--eify-primary-400)` | +| `#4f46e5` | `var(--eify-primary-600)` | +| `#7c3aed` | `var(--eify-primary-400)` | +| `linear-gradient(135deg, #6366f1, #8b5cf6)` | `var(--eify-gradient-primary)` | +| `#ef4444` | `var(--eify-error)` | +| `#e11d48` | `var(--eify-error)` | +| `#dc2626` | `var(--eify-error-600)` | +| `#f59e0b` | `var(--eify-warning)` | +| `#fbbf24` | `var(--eify-warning-400)` | +| `#d97706` | `var(--eify-warning-600)` | +| `#22c55e` | `var(--eify-success)` | +| `#059669` | `var(--eify-success-600)` | +| `#3b82f6` | `var(--eify-info-500)` | +| `#0ea5e9` | `var(--eify-info)` | +| `#e2e8f0` / `#e5e7eb` | `var(--eify-border-default)` | +| `#f1f5f9` (as border color) | `var(--eify-border-subtle)` | +| `#0f172a` | `var(--eify-text-primary)` | +| `#1e293b` (background) | `var(--eify-gray-800)` | +| `#1e293b` (text) | `var(--eify-text-primary)` | +| `#334155` | `var(--eify-gray-700)` | +| `#475569` | `var(--eify-text-secondary)` | +| `#64748b` / `#6b7280` | `var(--eify-text-secondary)` | +| `#94a3b8` | `var(--eify-text-tertiary)` | +| `#a5b4fc` | `var(--eify-primary-300)` | +| `#78350f` | `var(--eify-warning-900)` | + +### font-size Replacements + +| px value | Replace with utility class | +|:---|:---| +| `font-size: 10px` or `11px` or `12px` | Add `.text-xs` to element's class, remove the font-size rule | +| `font-size: 13px` | Add `.text-sm` | +| `font-size: 14px` | Add `.text-base` | +| `font-size: 15px` or `16px` | Add `.text-lg` | +| `font-size: 18px` | Add `.text-xl` | +| `font-size: 20px` or `22px` | Add `.text-2xl` | +| `font-size: 24px` | Add `.text-3xl` | +| `font-size: 28px` and above | Keep as-is | + +### padding / margin / border-radius / box-shadow + +Replace with nearest `var(--eify-spacing-N)` or `var(--eify-radius-*)` or `var(--eify-shadow-*)` token. + +### Do NOT touch + +- SVG `` tags +- Workflow node colors (`--wf-node-color`, `#22c55e`, `#ef4444`, `#8b5cf6`, `#f97316`, `#eab308`, `#3b82f6`, `#06b6d4` in node context) +- MCP Server Catppuccin theme colors (`#a6e3a1`, `#f38ba8`, `#1e1e2e` in McpServerList.vue log viewer) +- `var(--eify-*, fallback)` patterns (Element Plus overrides) +- `LoginView.vue`'s `#0f0f1a` background + +--- + +## Batch 1: Brand Color Epicenter + +### Task 1.1: Fix LoginView.vue + +**Files:** +- Modify: `eify-web/src/views/LoginView.vue` + +- [ ] **Step 1: Read the file and identify all violations** + +Read `eify-web/src/views/LoginView.vue`. In the ` @@ -465,7 +458,7 @@ async function handleRegister() { } .locale-popper .el-select-dropdown__item.is-selected { - color: #6366f1 !important; + color: var(--eify-primary) !important; font-weight: 600; background: transparent !important; } From b7bd8bc7ac02d7e699234a753d3e1c484c16612f Mon Sep 17 00:00:00 2001 From: mingming Date: Sat, 23 May 2026 15:35:48 +0800 Subject: [PATCH 07/30] fix: replace hardcoded values with design tokens in EifyFormDialog - Replaced #ffffff background with var(--eify-bg-base) - Replaced #ffffff text color with var(--eify-text-inverse) - Replaced font-size: 14px with .text-base utility class on buttons Co-Authored-By: Claude Opus 4.7 --- eify-web/src/components/EifyFormDialog.vue | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/eify-web/src/components/EifyFormDialog.vue b/eify-web/src/components/EifyFormDialog.vue index 680eca3..f5b9b39 100644 --- a/eify-web/src/components/EifyFormDialog.vue +++ b/eify-web/src/components/EifyFormDialog.vue @@ -27,9 +27,9 @@ @@ -349,7 +349,7 @@ defineExpose({ diff --git a/eify-web/src/views/WorkflowCreate.vue b/eify-web/src/views/WorkflowCreate.vue index aeb385b..c1ae26b 100644 --- a/eify-web/src/views/WorkflowCreate.vue +++ b/eify-web/src/views/WorkflowCreate.vue @@ -2,7 +2,7 @@
{{ t('common.back') }} -

{{ t('workflow.addWorkflow') }}

+

{{ t('workflow.addWorkflow') }}

@@ -159,16 +159,15 @@ function handleNext() { align-items: center; gap: 12px; padding: 12px 20px; - background: var(--eify-bg-surface, #fff); - border-bottom: 1px solid var(--eify-border-subtle, #e5e7eb); + background: var(--eify-bg-surface); + border-bottom: 1px solid var(--eify-border-subtle); flex-shrink: 0; } .create-title { margin: 0; - font-size: 18px; font-weight: 600; - color: var(--eify-text-primary, #1f2937); + color: var(--eify-text-primary); } .create-body { @@ -182,7 +181,7 @@ function handleNext() { .create-card { width: 100%; max-width: 720px; - background: var(--eify-bg-surface, #fff); + background: var(--eify-bg-surface); border-radius: 12px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06); padding: 32px; @@ -199,7 +198,7 @@ function handleNext() { gap: 16px; margin-top: 32px; padding-top: 24px; - border-top: 1px solid var(--eify-border-subtle, #e5e7eb); + border-top: 1px solid var(--eify-border-subtle); } .kv-table { diff --git a/eify-web/src/views/WorkflowEdit.vue b/eify-web/src/views/WorkflowEdit.vue index f9602db..1f9b051 100644 --- a/eify-web/src/views/WorkflowEdit.vue +++ b/eify-web/src/views/WorkflowEdit.vue @@ -14,7 +14,7 @@
- {{ t('workflow.unsavedHint') }} + {{ t('workflow.unsavedHint') }} {{ t('common.cancel') }} {{ t('common.save') }}
@@ -176,7 +176,7 @@ -
{{ t('provider.unsyncedHint') }}
+
{{ t('provider.unsyncedHint') }}
@@ -254,7 +254,7 @@ -
{{ t('workflow.conditionExpressionHint') }}
+
{{ t('workflow.conditionExpressionHint') }}
@@ -856,8 +856,8 @@ onMounted(() => { align-items: center; justify-content: space-between; padding: 8px 16px; - background: var(--eify-bg-surface, #fff); - border-bottom: 1px solid var(--eify-border-subtle, #e5e7eb); + background: var(--eify-bg-surface); + border-bottom: 1px solid var(--eify-border-subtle); z-index: 10; flex-shrink: 0; } @@ -892,8 +892,7 @@ onMounted(() => { } .header-hint { - font-size: 12px; - color: #f59e0b; + color: var(--eify-warning); } /* ========== 画布区域 ========== */ @@ -942,7 +941,6 @@ onMounted(() => { } .form-tip { - font-size: 12px; color: var(--eify-text-tertiary); margin-top: 4px; } From a83dae47db981496e8f715ed1e71e65bfcbec1b2 Mon Sep 17 00:00:00 2001 From: mingming Date: Sat, 23 May 2026 16:15:25 +0800 Subject: [PATCH 27/30] fix: replace remaining hardcoded values in WorkflowEdit focus styles Co-Authored-By: Claude Opus 4.7 --- eify-web/src/views/WorkflowEdit.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eify-web/src/views/WorkflowEdit.vue b/eify-web/src/views/WorkflowEdit.vue index 1f9b051..5a6defa 100644 --- a/eify-web/src/views/WorkflowEdit.vue +++ b/eify-web/src/views/WorkflowEdit.vue @@ -881,8 +881,8 @@ onMounted(() => { } .header-name-input :deep(.el-input__inner):focus { - background: #f8fafc; - border-bottom: 2px solid #6366f1; + background: var(--eify-bg-secondary); + border-bottom: 2px solid var(--eify-primary); } .header-right { From eb96c127be4104f548892fb33c6d59f6910c4b5f Mon Sep 17 00:00:00 2001 From: mingming Date: Sat, 23 May 2026 16:49:40 +0800 Subject: [PATCH 28/30] fix: resolve version display bug and add visual regression testing - Fix start.sh extracting ${revision} literally instead of resolved version - Add design system and visual regression testing docs to eify-web/README.md - Add Playwright screenshot script for visual verification (screenshot-check.cjs) Co-Authored-By: Claude Opus 4.7 --- eify-web/README.md | 28 +++++++++ eify-web/screenshot-check.cjs | 113 ++++++++++++++++++++++++++++++++++ start.sh | 4 +- 3 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 eify-web/screenshot-check.cjs diff --git a/eify-web/README.md b/eify-web/README.md index 8d90011..c6910ca 100644 --- a/eify-web/README.md +++ b/eify-web/README.md @@ -74,6 +74,32 @@ npm run test:watch 测试框架:Vitest + Vue Test Utils + jsdom +## 设计系统 + +项目遵循 [DESIGN.md](../DESIGN.md) 设计规范,所有 UI 代码使用 `--eify-*` CSS 变量和 `.eify-*` 组件类名。 + +| 文件 | 说明 | +|:---|:---| +| `src/styles/design-tokens.css` | 颜色、字体、间距、圆角、阴影等 CSS 变量 | +| `src/styles/components.css` | 按钮、输入框、卡片、表格、标签等组件样式 | +| `src/styles/page.css` | 顶栏、页面容器、分页、响应式布局 | +| `src/styles/sidebar.css` | 深色侧边栏样式(含版本信息) | +| `src/styles/utilities.css` | 文字、间距、布局、圆角等原子工具类 | + +核心规则: +- 禁止硬编码颜色、字号、间距 — 使用 `var(--eify-*)` 设计令牌 +- 优先使用 `.eify-*` 组件类,不使用内联样式或裸 HTML +- 布局遵循 Shell 结构:深色侧边栏 + 白色顶栏 + `#f8fafc` 内容区 + +## 视觉回归测试 + +```bash +# 截取所有页面截图(需先启动 dev 环境和后端) +node screenshot-check.cjs +``` + +截图输出到 `../logs/screenshots/`,包含 10 个页面 + 侧边栏 hover 状态共 11 张截图。 + ## 代理配置 开发环境 API 请求代理到 `http://localhost:8080`,配置在 `vite.config.ts`。 @@ -82,3 +108,5 @@ npm run test:watch - [后端项目文档](../docs/README.md) - [API 接口规范](../docs/API-SPEC.md) +- [设计系统规范](../DESIGN.md) +- [E2E 测试指南](../docs/guides/E2E-TESTING.md) diff --git a/eify-web/screenshot-check.cjs b/eify-web/screenshot-check.cjs new file mode 100644 index 0000000..68bac45 --- /dev/null +++ b/eify-web/screenshot-check.cjs @@ -0,0 +1,113 @@ +const { chromium } = require('playwright'); +const path = require('path'); + +const BASE = 'http://localhost:5173'; +const API = 'http://localhost:8080/api/v1'; +const OUT = path.join(__dirname, '..', 'logs', 'screenshots'); + +const PAGES = [ + { name: '01-login', path: '/login', fullPage: false }, + { name: '02-agents', path: '/agents', fullPage: true }, + { name: '03-chat', path: '/chat', fullPage: true }, + { name: '04-providers', path: '/providers', fullPage: true }, + { name: '05-mcp-servers', path: '/mcp-servers', fullPage: true }, + { name: '06-knowledge', path: '/knowledge', fullPage: true }, + { name: '07-documents', path: '/documents', fullPage: true }, + { name: '08-workflows', path: '/workflows', fullPage: true }, + { name: '09-workflow-create', path: '/workflows/create', fullPage: true }, + { name: '10-profile', path: '/profile', fullPage: true }, +]; + +(async () => { + const fs = require('fs'); + if (!fs.existsSync(OUT)) fs.mkdirSync(OUT, { recursive: true }); + + const browser = await chromium.launch({ channel: 'msedge' }); + const context = await browser.newContext({ + viewport: { width: 1440, height: 900 }, + }); + + // --- Login via API to get JWT token --- + let token = null; + const credentials = [ + { username: 'admin', password: 'admin123' }, + { username: 'admin', password: 'admin' }, + { username: 'admin', password: '123456' }, + { username: 'admin', password: 'Evyber@#2026' }, + ]; + + for (const cred of credentials) { + try { + const res = await fetch(`${API}/auth/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(cred), + }); + const data = await res.json(); + if (data.data && data.data.accessToken) { + token = data.data.accessToken; + console.log(`Login successful with ${cred.username}/${cred.password}`); + break; + } + } catch (e) { + // try next + } + } + + const page = await context.newPage(); + + // --- Page 1: Login page (no auth, always works) --- + console.log('Screenshotting login page...'); + await page.goto(`${BASE}/login`, { waitUntil: 'networkidle' }); + await page.waitForTimeout(1000); + await page.screenshot({ path: path.join(OUT, '01-login.png'), fullPage: false }); + console.log(' -> 01-login.png'); + + // --- If we have a token, inject it and screenshot authenticated pages --- + if (token) { + // Set the token in localStorage before navigating + await page.evaluate((t) => { + localStorage.setItem('accessToken', t); + }, token); + + for (const { name, path: pagePath, fullPage } of PAGES.slice(1)) { + try { + console.log(`Screenshotting ${pagePath}...`); + await page.goto(`${BASE}${pagePath}`, { waitUntil: 'networkidle', timeout: 15000 }); + await page.waitForTimeout(1500); + await page.screenshot({ + path: path.join(OUT, `${name}.png`), + fullPage, + }); + console.log(` -> ${name}.png`); + } catch (e) { + console.log(` -> ${name} FAILED: ${e.message}`); + } + } + + // --- Sidebar hover --- + try { + console.log('Capturing sidebar hover...'); + await page.goto(`${BASE}/agents`, { waitUntil: 'networkidle', timeout: 15000 }); + await page.waitForTimeout(1000); + const menuItem = page.locator('.eify-menu-item').first(); + if (await menuItem.count() > 0) { + await menuItem.hover(); + await page.waitForTimeout(500); + await page.screenshot({ + path: path.join(OUT, '11-sidebar-hover.png'), + fullPage: false, + }); + console.log(' -> 11-sidebar-hover.png'); + } + } catch (e) { + console.log(` -> Sidebar hover FAILED: ${e.message}`); + } + } else { + console.log('No valid credentials found. Only login page screenshot taken.'); + console.log('To capture other pages, update credentials in this script.'); + } + + await browser.close(); + console.log(`\nDone! Screenshots in ${OUT}`); +})(); diff --git a/start.sh b/start.sh index 4791d73..946ecfe 100644 --- a/start.sh +++ b/start.sh @@ -223,8 +223,8 @@ echo "" # ========== 启动后端 ========== log_info "启动后端服务..." -# 版本号 -APP_VERSION=$(grep '' pom.xml 2>/dev/null | sed 's/.*\(.*\)<\/eify.version>.*/\1/' || echo "1.0.0") +# 版本号(直接读 revision 属性,避免 ${revision} 未被 shell 解析) +APP_VERSION=$(grep '' pom.xml 2>/dev/null | sed 's/.*\(.*\)<\/revision>.*/\1/' | head -1 || echo "1.0.0") SPRING_OPTS="-Dspring.profiles.active=${ENV} -Dapp.version=${APP_VERSION}" # JVM 配置(可通过环境变量覆盖) From 948949670c05e8fed34c6bd1b1e19471e3b0347c Mon Sep 17 00:00:00 2001 From: mingming Date: Sat, 23 May 2026 16:51:31 +0800 Subject: [PATCH 29/30] docs: restructure documentation and add Playwright for visual testing - Consolidate code review checklist in CLAUDE.md, remove duplicated sections - Move Flyway idempotent migration template to DATABASE.md with monitoring queries - Add E2E-TESTING.md (Playwright patterns) and SECURITY.md (risk checklist) - Remove outdated EXAMPLES.md and SIDEBAR-COMPONENT.md - Add Playwright dependency for visual regression screenshots Co-Authored-By: Claude Opus 4.7 --- CLAUDE.md | 231 +--------------- docs/ARCHITECTURE.md | 4 +- docs/README.md | 6 +- docs/guides/DATABASE.md | 53 ++++ docs/guides/E2E-TESTING.md | 214 +++++++++++++++ docs/guides/SECURITY.md | 90 +++++++ eify-web/EXAMPLES.md | 483 ---------------------------------- eify-web/SIDEBAR-COMPONENT.md | 266 ------------------- eify-web/package-lock.json | 49 +++- eify-web/package.json | 1 + 10 files changed, 426 insertions(+), 971 deletions(-) create mode 100644 docs/guides/E2E-TESTING.md create mode 100644 docs/guides/SECURITY.md delete mode 100644 eify-web/EXAMPLES.md delete mode 100644 eify-web/SIDEBAR-COMPONENT.md diff --git a/CLAUDE.md b/CLAUDE.md index 2872ff5..cb8b3a5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,10 +11,8 @@ - [开发规范](#开发规范) - [日志系统快速参考](#日志系统快速参考) - [代码提交检查清单](#代码提交检查清单) -- [常见问题](#常见问题) - [设计系统](#设计系统) - [项目上下文](#项目上下文) -- [系统风险清单](#系统风险清单) - [文档索引](#文档索引) --- @@ -90,6 +88,7 @@ grep 'YOUR_TRACE_ID' ./logs/eify.log | jq | 🔴 **Flyway 迁移幂等** | 所有 DDL 语句必须检查对象是否已存在(ADD COLUMN → INFORMATION_SCHEMA.COLUMNS,ADD INDEX → INFORMATION_SCHEMA.STATISTICS),确保幂等可重入 | 应用启动失败,迁移阻塞 | [DATABASE.md](docs/guides/DATABASE.md) | | 🔴 **ADR 命名规范** | 架构决策记录统一放在 `docs/ADRs/`,文件命名 `ADR-{四位递增序号}-{名称}.md`,序号按创建时间递增 | ADR 命名混乱,查找困难 | — | | 🔴 **ADR 格式规范** | 所有 ADR 文档必须遵循 `docs/ADRs/ADR-XXXX-Template.md` 模板格式,包含 6 个必填章节:`# Status`、`# Date`、`# Owner`、`# Deciders`、`# Context`、`# Decision`,以及 2 个推荐章节:`## Consequences`、`# Details`。`# Considered Options` 章节列出所有候选方案及其被拒绝原因 | ADR 结构不一致,难以阅读和对比 | `docs/ADRs/ADR-XXXX-Template.md` | +| 🔴 **安全审查** | 涉及安全敏感代码时,审查前必须阅读 [SECURITY.md](docs/guides/SECURITY.md) 系统风险清单 | 遗漏安全检查 | [SECURITY.md](docs/guides/SECURITY.md) | --- @@ -197,31 +196,7 @@ private LocalDateTime updatedAt; - 软删除字段 `deleted` 必须有索引 - 分页查询优先使用游标分页(针对大表) - JSON 字段必须添加注释说明结构 -- Flyway 迁移必须幂等:DDL 通过 INFORMATION_SCHEMA 检查后执行(模板见下方) - -**Flyway 幂等迁移模板**: - -```sql --- 加列(幂等) -SET @sql = IF( - (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS - WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'table_name' AND COLUMN_NAME = 'col_name') = 0, - 'ALTER TABLE `table_name` ADD COLUMN `col_name` TYPE COMMENT ''注释'' AFTER `prev_col`', - 'SELECT ''Column col_name already exists, skipping'' AS info' -); -PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; - --- 加索引/唯一约束(幂等) -SET @sql = IF( - (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS - WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'table_name' AND INDEX_NAME = 'idx_name') = 0, - 'ALTER TABLE `table_name` ADD INDEX `idx_name` (`col1`, `col2`)', - 'SELECT ''Index idx_name already exists, skipping'' AS info' -); -PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -``` - -> **规则**:所有 Flyway 迁移中的 DDL(`ADD COLUMN`, `ADD INDEX`, `ADD UNIQUE KEY`)必须使用上述模板。DML(`UPDATE`, `INSERT`)本身是幂等的,无需包装。 +- Flyway 迁移必须幂等:DDL 通过 INFORMATION_SCHEMA 检查后执行,模板详见 [DATABASE.md](docs/guides/DATABASE.md) --- @@ -242,14 +217,6 @@ PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; | **UTC 时区** | 所有日志时间戳使用 UTC 时区 | [LOGGING.md](docs/guides/LOGGING.md) | | **MDC Keys** | `traceId`、`spanId`(由 Brave Tracing 设置) | [LOGGING.md](docs/guides/LOGGING.md) | -### 日志文档索引 - -| 文档 | 用途 | -|:---|:---| -| [LOGGING.md](docs/guides/LOGGING.md) | 日志系统完整指南(架构、格式、配置、MQ、监控、性能) | -| [DATABASE.md](docs/guides/DATABASE.md) | ClickHouse 表结构、索引策略、物化视图 | -| [deploy/infra/deploy/README.md](deploy/infra/deploy/README.md) | Vector + ClickHouse 部署指南 | - --- ## 代码提交检查清单 @@ -276,11 +243,15 @@ cd eify-web && npx vue-tsc --noEmit && cd .. # 前端类型检查 - [ ] **测试通过**:`mvn test` 全部通过,新增逻辑有对应测试用例 - [ ] **无硬编码**:IP、密码、密钥使用 `${ENV_VAR:}` 占位符 -- [ ] **异常规范**:使用 `ErrorCode` 枚举,禁止 `throw new RuntimeException()` 和 `Result.fail("裸字符串")` +- [ ] **异常规范**:使用 `ErrorCode` 枚举,禁止 `throw new RuntimeException()` 和 `Result.fail("裸字符串")`;禁止空 catch 块或仅 `e.printStackTrace()` +- [ ] **参数校验**:Controller 请求体使用 `@Valid` / `@Validated` 注解,禁止手动 if-else 校验 +- [ ] **HTTP 状态码**:认证失败 401,权限不足 403,资源不存在 404,参数错误 400,禁止全部返回 200 +- [ ] **构造器注入**:使用 `@RequiredArgsConstructor`,禁止 `@Autowired` 字段注入 ### 涉及数据库时 - [ ] **索引覆盖**:新查询有对应索引,EXPLAIN 确认无全表扫描 +- [ ] **N+1 查询**:禁止循环内逐条查询,使用批量查询或联表查询 - [ ] **Flyway 幂等**:DDL 使用 `INFORMATION_SCHEMA` 检查模板(ADD COLUMN / ADD INDEX / ADD UNIQUE KEY) - [ ] **ClickHouse 类型**:Nullable 不与 LowCardinality 嵌套;按 logType 设置 Nullable 字段 @@ -300,50 +271,13 @@ cd eify-web && npx vue-tsc --noEmit && cd .. # 前端类型检查 - [ ] **线程池隔离**:使用命名线程池执行,禁止在 Tomcat 线程上同步调用外部服务 - [ ] **超时设置**:HTTP 调用有 connect/read timeout,SSE 有 `onTimeout` + `onError` 回调 - [ ] **安全校验**:API 调用经过 SSRF 防护(`ApiNodeExecutor.validateUrl()`) +- [ ] **定时任务超时**:`@Scheduled` 方法必须有超时控制,禁止无界阻塞任务 ---- - -## 常见问题 - -### Q: 日志输出格式是什么? - -**A**: 纯 JSON 格式,UTC 时区,标准字段在顶层,业务数据在 `message` 对象中。详细说明:[LOGGING.md](docs/guides/LOGGING.md) - -### Q: 如何按 traceId 查询完整调用链? - -**A**: -```sql --- ClickHouse 查询 -SELECT * FROM app_logs WHERE traceId = 'xxx' ORDER BY timestamp; - --- 文件查询 -grep 'YOUR_TRACE_ID' ./logs/eify.log | jq -``` - -### Q: ClickHouse 类型错误:`Nullable(LowCardinality(String))`? - -**A**: ClickHouse 不支持此类型组合,改用 `Nullable(String)`。详见:[DATABASE.md](docs/guides/DATABASE.md) +### 代码质量 -### Q: Vector 连接 ClickHouse 失败? - -**A**: 检查 `deploy/infra/vector/vector.toml` 中的连接配置,确保环境变量 `CLICKHOUSE_ENDPOINT` 和 `CLICKHOUSE_PASSWORD` 已设置。参考 `.env.example`。 - -### Q: 如何切换用户的工作空间? - -**A**: 调用 `POST /api/auth/switch-workspace { workspaceId }`,后端验证成员身份后签发新 JWT(含新 `wid`)。前端 `authStore.switchWorkspace()` 自动保存新 token 并执行 `window.location.reload()` 全量刷新。详见:[AUTH-WORKSPACE.md](docs/guides/AUTH-WORKSPACE.md) - -### Q: 新建业务模块时如何实现工作空间隔离? - -**A**: -1. 数据表添加 `workspace_id BIGINT UNSIGNED` 字段 -2. Entity 实现 `WorkspaceAware` 接口 -3. Service 层所有查询使用 `.eq(Entity::getWorkspaceId, CurrentContext.getWorkspaceId())` -4. 更新/删除使用 `WorkspaceGuard.requireInWorkspace(mapper.selectById(id), ErrorCode)` 一行完成校验 -5. 创建使用 `WorkspaceGuard.bind(entity)` 绑定 workspace -6. 唯一索引必须与 `workspace_id` 组合:`UNIQUE KEY (name, workspace_id)` -7. 名称唯一性检查使用 `WorkspaceGuard.checkNameUnique(mapper, ...)` - -详见:[AUTH-WORKSPACE.md](docs/guides/AUTH-WORKSPACE.md) +- [ ] **字符串拼接**:循环内禁止 `+=` 拼接,使用 `StringBuilder` +- [ ] **泛型完整**:禁止裸类型(`List` → `List`),IDE 警告视为错误 +- [ ] **测试注解**:单元测试优先 `@ExtendWith(MockitoExtension.class)`,避免不必要 `@SpringBootTest` --- @@ -380,87 +314,11 @@ grep 'YOUR_TRACE_ID' ./logs/eify.log | jq - **版本管理**:Git 语义化版本,重要变更记录在 git commit history 中 ### 技术架构 -- **日志架构**:纯 JSON 格式(UTC),标准字段在顶层,业务字段按 logType 分类 - **链路追踪**:基于 Brave/Micrometer Tracing 实现分布式追踪 -- **日志存储**:ClickHouse + Vector 采集,使用 Nullable 优化存储 - **异步处理**:使用线程池隔离外部调用,确保服务稳定性 --- -## 系统风险清单 - -> 基于 9 个模块的架构分析与风险修复后的复盘。用于指导新功能开发时的安全审查和测试优先级。 -> 最后更新:2026-05-20 - -### 一、核心链路(5 条) - -| # | 链路名称 | 涉及模块/类 | 核心原因 | -|:---|:---|:---|:---| -| **1** | **Agent 对话链路** | `ChatController` → `ChatServiceImpl` → `ProviderAdapterFactory` → `LlmHttpClient` → SSE 流式输出 | 用户核心体验路径。涉及 LLM 调用、SSE 流式、工具调用循环(最多 5 轮)、RAG 检索增强。任一环失败用户直接感知 | -| **2** | **工作流执行链路** | `WorkflowEngine` → 7 种 `NodeExecutor`(llm / api_call / code / condition / tool_call / start / end)→ `VariableResolver` | 系统最复杂的编排路径。DAG 拓扑执行,节点超时按类型区分(LLM 3min / API 30s),代码节点有沙箱隔离,变量系统单次替换防注入 | -| **3** | **认证与工作空间隔离链路** | `JwtAuthFilter` → `JwtSecretValidator`(启动校验)→ `CurrentContext`(ThreadLocal)→ `ContextPropagatingTaskDecorator`(5 个线程池)→ `WorkspaceGuard` | 多租户安全防火墙。JWT 密钥生产环境强制校验,ThreadLocal 通过 TaskDecorator 传播到所有异步线程池 | -| **4** | **知识库 RAG 链路** | `EmbeddingStrategy` → pgvector `<=>` 余弦相似度 → `ChunkService.search()` → `mergeAndRerank()` | 知识检索质量决定 Agent 回答准确度。涉及向量化、HNSW 索引、多知识库合并去重重排序 | -| **5** | **MCP 工具调用链路** | `McpClientServiceImpl`(DCL+锁保护)→ `io.modelcontextprotocol` SDK → 外部 MCP Server | Agent 能力扩展通道。客户端缓存 5 分钟 TTL,DCL+同步锁防并发竞态,最多 2 次重试,按 serverId 缓存可用工具集 | - -### 二、风险集中区域 - -#### 🔴 严重 — 安全红线,触碰即可能造成数据泄露或服务不可用 - -| 风险点 | 类型 | 位置 | 关键防护 | -|:---|:---|:---|:---| -| **JWT 密钥泄露** | 安全 | `JwtAuthFilter` + 配置 | `JwtSecretValidator` 启动时检测已知默认值/空值/短密钥,非 dev 环境拒绝启动 | -| **ApiNodeExecutor SSRF** | 安全 | `ApiNodeExecutor.validateUrl()` + `isBlockedAddress()` | 封堵 `file/ftp/jar/gopher`,阻止云元数据 IP,IPv4/IPv6 回环/内网/链路本地地址,IPv4-mapped IPv6 递归检查 | -| **CodeNodeExecutor 沙箱** | 安全 | `CodeNodeExecutor` | 共享守护线程池(4 线程),30s 超时 + Future.cancel(true),线程池满时 AbortPolicy 拒绝 | -| **VariableResolver 注入** | 安全 | `VariableResolver` + 各消费者 | 单次替换防递归展开;消费者层防护:ConditionNodeExecutor 用 getVariable 取原始值,ApiNodeExecutor URL 校验,CodeNodeExecutor ScriptEngine.put 注入 | - -#### 🟡 高 — 可能造成部分功能不可用或数据不一致 - -| 风险点 | 类型 | 位置 | 关键防护 | -|:---|:---|:---|:---| -| **ThreadLocal 丢失** | 并发 | `CurrentContext` + `ThreadPoolConfig` | `ObjectProvider` 注入,5 个线程池自动应用 context 传播 | -| **SSE 连接泄漏** | 性能 | `ChatServiceImpl` + `LlmHttpClient` | onTimeout/onCompletion/onError 三回调 + activeSessions 清理 + OkHttp ConnectionPool(20) | -| **MCP Client 竞态** | 并发 | `McpClientServiceImpl` | DCL + synchronized(lock) + 独立锁对象 per serverId | -| **节点超时不均** | 性能 | `WorkflowEngine.coreLoop()` | 按类型区分:LLM 3min / 默认 30s | - -#### 🟢 中 — 边界场景或性能退化 - -| 风险点 | 类型 | 关键防护 | -|:---|:---|:---| -| **线程池满** | 性能 | llm/workflow/mcp/sse 四个池 AbortPolicy,仅 asyncExecutor 保留 CallerRunsPolicy | -| **API 响应截断** | 数据一致性 | 512KB 截断 + warn 日志,需工作流设计层面约束 | - -### 三、测试重心 - -#### P0 — 必须有测试覆盖 - -| 测试对象 | 原因 | 状态 | -|:---|:---|:---| -| `ApiNodeExecutor` SSRF 防护 | 安全红线 | ✅ 12 个测试 | -| `JwtSecretValidator` 密钥校验 | 安全红线 | ✅ 8 个测试 | -| `ConditionNodeExecutor` 条件路由 | 工作流核心 | ✅ 18 个测试 | -| `WorkspaceGuard` 空间隔离 | 多租户安全 | ✅ 已有 | -| `CodeNodeExecutor` 沙箱超时 | 安全红线 | ✅ 已有 | - -#### P1 — 应该有测试覆盖 - -| 测试对象 | 原因 | 状态 | -|:---|:---|:---| -| `WorkflowEngine` 端到端 | 多节点 DAG + 分支 + 超时 | ⚠️ 仅单测 | -| `ChatServiceImpl` SSE 生命周期 | 连接异常处理 | ⚠️ 仅 mock | -| `McpClientServiceImpl` 生命周期 | 缓存/竞态/降级 | ❌ 无 | -| `ProviderAdapter` SSE 解析 | 各厂商协议差异 | ❌ 无 | -| `ChunkService` RAG 检索 | 向量搜索 + 降级 | ❌ 无 | - -#### P2 — 可以后补 - -| 类别 | 原因 | -|:---|:---| -| CRUD Controller / Mapper XML | 样板代码,Service 层已验证 | -| 游标分页 | MyBatis-Plus 封装 | -| 前端组件单测 | TS 类型检查 + vitest 基础覆盖即可,复杂交互用 e2e | - ---- - ## 文档索引 ### 核心文档 @@ -504,72 +362,11 @@ grep 'YOUR_TRACE_ID' ./logs/eify.log | jq |:---|:---|:---| | [ARCHITECTURE.md](docs/ARCHITECTURE.md) | 线程池配置 | 调用外部 LLM API / 实现 SSE 流式响应 | | [WORKFLOW.md](docs/guides/WORKFLOW.md) | 工作流引擎设计 | 开发工作流节点、理解节点类型和变量系统 | +| [SECURITY.md](docs/guides/SECURITY.md) | 系统风险清单 | 安全敏感代码审查前必读 | +| [E2E-TESTING.md](docs/guides/E2E-TESTING.md) | 端到端测试指南 | 编写 Playwright e2e 测试 | ### 运维文档 | 文档 | 用途 | 何时查看 | |:---|:---|:---| | [DEPLOYMENT.md](docs/DEPLOYMENT.md) | 部署与 CI/CD | 部署到生产环境、CI/CD 流水线、故障排查 | -| [ARCHITECTURE.md](docs/ARCHITECTURE.md) | 架构与编码规范 | 模块结构、命名规范、分层职责 | - ---- - -## 附录 - -### 项目结构 - -``` -eify/ -├── CLAUDE.md # Claude Code 指导(根目录仅此 4 个文件) -├── DESIGN.md # 设计系统规范(AI 编码助手的视觉参考) -├── start.sh # 启动脚本 -├── stop.sh # 停止脚本 -│ -├── eify-common/ # 公共模块(工具类、常量、异常、DTO) -├── eify-auth/ # 认证与工作空间(JWT、用户管理、多租户隔离) -├── eify-provider/ # LLM 提供商模块 -├── eify-agent/ # Agent 模块 -├── eify-chat/ # 对话模块 -├── eify-mcp/ # MCP 工具模块 -├── eify-workflow/ # 工作流引擎模块 -├── eify-knowledge/ # 知识库模块 -├── eify-app/ # 应用模块(Spring Boot 入口) -├── eify-web/ # 前端模块(Vue 3 + TypeScript) -│ -├── docs/ # 项目文档(按开发者查阅路径组织) -│ ├── ARCHITECTURE.md # 架构 + 编码规范 -│ ├── API-SPEC.md # API 接口规范 -│ ├── DEPLOYMENT.md # 部署与 CI/CD(本地、Docker、K8s、Jenkins) -│ ├── ADRs/ # 架构决策记录(ADR) -│ └── guides/ # HOW-TO 指南 -│ ├── AUTH-WORKSPACE.md # 认证 + 多租户 -│ ├── DATABASE.md # MySQL + ClickHouse + 游标分页 -│ ├── LOGGING.md # 日志系统完整指南 -│ └── WORKFLOW.md # 工作流引擎设计 -│ -├── scripts/ # 开发/运维工具脚本 -├── Jenkinsfile # CI/CD 流水线定义 -├── deploy/ # 部署配置 -│ ├── Dockerfile # 后端镜像构建 -│ ├── Dockerfile.web # 前端镜像构建 -│ ├── nginx.conf # Nginx 反向代理 -│ ├── k8s/ # K8s 清单(Deployment、Service、Ingress) -│ ├── sql/ # MySQL 初始化脚本(统一入口) -│ ├── clickhouse/ # ClickHouse DDL -│ ├── vector/ # Vector 采集配置 -│ ├── postgresql/ # PostgreSQL pgvector 初始化脚本 -│ ├── clickvisual/ # ClickVisual 配置 -│ ├── optional/ # 可选组件(Jaeger 等) -│ └── *.yml # Docker Compose 文件 -│ -└── .env.example # 环境变量模板 -``` - -### 环境配置 - -| 环境 | 配置文件 | 用途 | 部署方式 | -|:---|:---|:---|:---| -| **开发** | `application-dev.yml` | 本地开发 | Docker Compose | -| **测试** | `application-test.yml` | CCE Turbo K8s 自动化测试 | K8s Deployment | -| **预发布** | `application-staging.yml` | CCE Turbo K8s 预发布 | K8s Deployment | -| **生产** | `application-prod.yml` | CCE Turbo K8s 生产 | K8s Deployment | diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 303cd2c..3e9eee1 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -451,13 +451,13 @@ class AgentServiceImplTest { ## 代码检查清单 +> 完整提交检查清单已统一至 **[CLAUDE.md § 代码提交检查清单](../../CLAUDE.md#代码提交检查清单)**,涵盖结构合规、安全、数据库、API、前端、外部调用、代码质量等全部检查项。此处保留架构结构相关摘要。 + - [ ] 包结构符合模块规范,类/方法命名符合规范 - [ ] Controller 只做参数校验和返回封装,业务逻辑在 Service 层 - [ ] Entity 继承 `BaseEntity`,使用 Lombok + MyBatis-Plus 注解 - [ ] DTO 使用 JSR-303 校验注解,Response 只包含必要字段 - [ ] 跨模块调用通过 Maven pom.xml 声明,不引入循环依赖 -- [ ] 异常处理使用 `ErrorCode` 枚举 - [ ] 事务在 Service 层控制,外部调用使用线程池隔离 -- [ ] 日志使用 SLF4J 占位符,级别正确(ERROR/WARN/INFO/DEBUG) - [ ] 新 Entity 实现 `WorkspaceAware`,Service 层查询过滤 `workspace_id` - [ ] 新模块有对应的单元测试(`src/test/java/.../{Service}Test.java`) diff --git a/docs/README.md b/docs/README.md index fe89446..c783e3f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -58,7 +58,9 @@ docs/ └── guides/ # HOW-TO 指南(体量大、持续更新) ├── AUTH-WORKSPACE.md # 用户认证与工作空间多租户 ├── DATABASE.md # 数据库设计(MySQL + ClickHouse + 游标分页) + ├── E2E-TESTING.md # Playwright 端到端测试指南 ├── LOGGING.md # 日志系统完整指南(架构、格式、MQ、监控、性能) + ├── SECURITY.md # 系统风险清单 + 安全审查 └── WORKFLOW.md # 工作流引擎设计(节点类型、变量系统、参考实现) ``` @@ -211,10 +213,12 @@ Eify 项目采用模块化文档管理,遵循以下原则: | **数据库设计** | DATABASE.md(含 MySQL + ClickHouse + 游标分页) | | **日志系统** | LOGGING.md | | **LLM 调用 / SSE 流式** | ARCHITECTURE.md(线程池配置)、LOGGING.md | +| **安全审查** | SECURITY.md(系统风险清单) | +| **端到端测试** | E2E-TESTING.md(Playwright 模式) | | **性能优化** | LOGGING.md#性能分析与瓶颈、DEPLOYMENT.md#扩容触发条件 | | **部署运维** | DEPLOYMENT.md | | **CI/CD 流水线** | DEPLOYMENT.md | -| **监控告警** | LOGGING.md#日志监控方案 | +| **监控告警** | LOGGING.md#日志监控方案、DATABASE.md#ClickHouse-监控查询 | ## 贡献指南 diff --git a/docs/guides/DATABASE.md b/docs/guides/DATABASE.md index c4255ed..615e7f5 100644 --- a/docs/guides/DATABASE.md +++ b/docs/guides/DATABASE.md @@ -1047,4 +1047,57 @@ DROP TABLE IF EXISTS app_logs; -- 从备份恢复 CREATE TABLE app_logs AS app_logs_backup ENGINE = MergeTree() ...; INSERT INTO app_logs SELECT * FROM app_logs_backup; + +--- + +## ClickHouse 监控查询 + +### 慢查询排查 + +```sql +-- 查询最近 1 小时耗时超过 1 秒的查询 +SELECT + query_start_time, + query_duration_ms, + user, + query +FROM system.query_log +WHERE type = 'QueryFinish' + AND query_duration_ms > 1000 + AND event_time > now() - INTERVAL 1 HOUR +ORDER BY query_duration_ms DESC +LIMIT 20; +``` + +### 存储统计 + +```sql +-- 按表查看磁盘占用和数据量 +SELECT + table, + formatReadableSize(sum(bytes_on_disk)) AS disk_size, + sum(rows) AS total_rows, + max(modification_time) AS last_modified +FROM system.parts +WHERE active +GROUP BY table +ORDER BY sum(bytes_on_disk) DESC; +``` + +### 写入吞吐监控 + +```sql +-- 最近 1 小时内各表的写入行数 +SELECT + table, + count() AS inserts, + sum(rows) AS total_rows_written, + formatReadableSize(sum(bytes)) AS total_written +FROM system.query_log +WHERE type = 'QueryFinish' + AND query_kind = 'Insert' + AND event_time > now() - INTERVAL 1 HOUR +GROUP BY table +ORDER BY inserts DESC; +``` ``` diff --git a/docs/guides/E2E-TESTING.md b/docs/guides/E2E-TESTING.md new file mode 100644 index 0000000..2f5da5e --- /dev/null +++ b/docs/guides/E2E-TESTING.md @@ -0,0 +1,214 @@ +# 端到端测试指南 + +> Playwright 端到端测试模式,适用于 Eify 核心链路的 UI 验证。 +> 参考:ECC e2e-testing skill。最后更新:2026-05-23。 + +--- + +## 目录结构 + +``` +eify-web/ +├── tests/ +│ ├── e2e/ +│ │ ├── auth/ # 登录/注册/工作空间切换 +│ │ ├── features/ # Agent 对话、工作流执行、知识库 +│ │ └── api/ # 直接 HTTP 接口验证 +│ ├── fixtures/ # 共享的 test fixtures +│ └── playwright.config.ts +``` + +--- + +## Playwright 配置 + +```typescript +// playwright.config.ts +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + fullyParallel: true, + workers: process.env.CI ? 1 : undefined, + retries: process.env.CI ? 2 : 0, + reporter: [ + ['html', { outputFolder: 'playwright-report' }], + ['junit', { outputFile: 'junit.xml' }], + ['json', { outputFile: 'results.json' }], + ], + use: { + baseURL: process.env.BASE_URL || 'http://localhost:5173', + actionTimeout: 10000, + navigationTimeout: 30000, + trace: 'retain-on-failure', + screenshot: 'only-on-failure', + video: 'retain-on-failure', + }, + projects: [ + { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, + ], + webServer: { + command: 'npm run dev', + url: 'http://localhost:5173', + reuseExistingServer: !process.env.CI, + timeout: 120000, + }, +}); +``` + +--- + +## Page Object Model + +每个页面封装为一个 Page 类,使用 `data-testid` 定位器: + +```typescript +// tests/e2e/features/AgentChatPage.ts +import { Page, Locator } from '@playwright/test'; + +export class AgentChatPage { + readonly page: Page; + readonly chatInput: Locator; + readonly sendButton: Locator; + readonly messageList: Locator; + readonly loadingIndicator: Locator; + + constructor(page: Page) { + this.page = page; + this.chatInput = page.locator('[data-testid="chat-input"]'); + this.sendButton = page.locator('[data-testid="send-button"]'); + this.messageList = page.locator('[data-testid="message-list"]'); + this.loadingIndicator = page.locator('[data-testid="loading-indicator"]'); + } + + async goto() { + await this.page.goto('/chat'); + await this.page.waitForLoadState('networkidle'); + } + + async sendMessage(text: string) { + await this.chatInput.fill(text); + await this.sendButton.click(); + } + + async getMessageCount() { + return this.messageList.locator('[data-testid="message-item"]').count(); + } +} +``` + +--- + +## 测试结构 + +```typescript +// tests/e2e/features/agent-chat.spec.ts +import { test, expect } from '@playwright/test'; +import { AgentChatPage } from './AgentChatPage'; + +test.describe('Agent Chat', () => { + let chatPage: AgentChatPage; + + test.beforeEach(async ({ page }) => { + chatPage = new AgentChatPage(page); + await chatPage.goto(); + }); + + test('send message and receive response', async ({ page }) => { + await chatPage.sendMessage('你好'); + + // 等待 SSE 响应到达 + await expect(chatPage.messageList).toContainText('你好'); + await expect(chatPage.loadingIndicator).not.toBeVisible({ timeout: 30000 }); + await expect(chatPage.getMessageCount()).toBeGreaterThan(1); + }); + + test('empty message should show validation', async () => { + await chatPage.sendButton.click(); + await expect(page.locator('[data-testid="input-error"]')).toBeVisible(); + }); +}); +``` + +--- + +## SSE 流式响应测试 + +Eify 核心链路(Agent 对话、工作流执行)都涉及 SSE。测试模式: + +```typescript +test('SSE streaming response', async ({ page }) => { + // 拦截 SSE 请求,等待响应完成 + const responsePromise = page.waitForResponse( + resp => resp.url().includes('/api/v1/chat/stream') && resp.status() === 200, + { timeout: 60000 } + ); + + await chatPage.sendMessage('写一段代码'); + + const response = await responsePromise; + expect(response.status()).toBe(200); + // 验证流式内容已渲染 + await expect(chatPage.messageList.locator('[data-testid="message-item"]').last()) + .toContainText('```', { timeout: 30000 }); +}); +``` + +--- + +## 防抖处理 + +### 跳过已知不稳定的测试 + +```typescript +test.fixme('flaky - SSE timeout on slow CI', async ({ page }) => { + // ... +}); +``` + +### 复现偶发失败 + +```bash +npx playwright test --repeat-each=10 --retries=3 agent-chat.spec.ts +``` + +### 常见原因及修复 + +| 原因 | 错误做法 | 正确做法 | +|:---|:---|:---| +| 竞态条件 | `page.click()` 后立即断言 | 使用 `expect(locator).toBeVisible()` 等自动等待断言 | +| 网络时序 | `page.waitForTimeout(5000)` 盲等 | `page.waitForResponse(urlPattern)` 等具体响应 | +| 动画时序 | 动画中点击按钮 | `waitFor({ state: 'visible' })` 或 `waitForLoadState('networkidle')` | + +--- + +## 产物管理 + +```typescript +// 截图 +await page.screenshot({ path: 'artifacts/screenshots/full.png', fullPage: true }); + +// Trace(手动控制) +await context.tracing.start({ screenshots: true, snapshots: true }); +// ... 测试步骤 ... +await context.tracing.stop({ path: 'artifacts/traces/test.zip' }); +``` + +--- + +## Eify 优先测试目标 + +按系统风险清单的 P1 缺口,e2e 应优先覆盖: + +| 测试对象 | 覆盖场景 | 优先级 | +|:---|:---|:---| +| `ChatServiceImpl` SSE 生命周期 | 发送消息 → 流式响应 → 完成/超时/报错 → UI 渲染 | P1 | +| `WorkflowEngine` 端到端 | 创建 → DAG 配置 → 执行 → 查看结果 | P1 | +| `McpClientServiceImpl` 生命周期 | 连接 → 工具列表 → 调用 → 缓存命中 → 断连降级 | P1 | + +--- + +## 相关文档 + +- [SECURITY.md](SECURITY.md) — 系统风险清单(P0/P1/P2 测试优先级) +- [CLAUDE.md](../../CLAUDE.md) — 代码提交检查清单 +- [ARCHITECTURE.md](../ARCHITECTURE.md) — 架构设计 + 模块依赖 diff --git a/docs/guides/SECURITY.md b/docs/guides/SECURITY.md new file mode 100644 index 0000000..c198ccc --- /dev/null +++ b/docs/guides/SECURITY.md @@ -0,0 +1,90 @@ +# 系统风险清单 + +> 基于 9 个模块的架构分析与风险修复后的复盘。用于指导新功能开发时的安全审查和测试优先级。 +> 最后更新:2026-05-20 + +--- + +## 一、核心链路(5 条) + +| # | 链路名称 | 涉及模块/类 | 核心原因 | +|:---|:---|:---|:---| +| **1** | **Agent 对话链路** | `ChatController` → `ChatServiceImpl` → `ProviderAdapterFactory` → `LlmHttpClient` → SSE 流式输出 | 用户核心体验路径。涉及 LLM 调用、SSE 流式、工具调用循环(最多 5 轮)、RAG 检索增强。任一环失败用户直接感知 | +| **2** | **工作流执行链路** | `WorkflowEngine` → 7 种 `NodeExecutor`(llm / api_call / code / condition / tool_call / start / end)→ `VariableResolver` | 系统最复杂的编排路径。DAG 拓扑执行,节点超时按类型区分(LLM 3min / API 30s),代码节点有沙箱隔离,变量系统单次替换防注入 | +| **3** | **认证与工作空间隔离链路** | `JwtAuthFilter` → `JwtSecretValidator`(启动校验)→ `CurrentContext`(ThreadLocal)→ `ContextPropagatingTaskDecorator`(5 个线程池)→ `WorkspaceGuard` | 多租户安全防火墙。JWT 密钥生产环境强制校验,ThreadLocal 通过 TaskDecorator 传播到所有异步线程池 | +| **4** | **知识库 RAG 链路** | `EmbeddingStrategy` → pgvector `<=>` 余弦相似度 → `ChunkService.search()` → `mergeAndRerank()` | 知识检索质量决定 Agent 回答准确度。涉及向量化、HNSW 索引、多知识库合并去重重排序 | +| **5** | **MCP 工具调用链路** | `McpClientServiceImpl`(DCL+锁保护)→ `io.modelcontextprotocol` SDK → 外部 MCP Server | Agent 能力扩展通道。客户端缓存 5 分钟 TTL,DCL+同步锁防并发竞态,最多 2 次重试,按 serverId 缓存可用工具集 | + +--- + +## 二、风险集中区域 + +### 🔴 严重 — 安全红线,触碰即可能造成数据泄露或服务不可用 + +| 风险点 | 类型 | 位置 | 关键防护 | +|:---|:---|:---|:---| +| **JWT 密钥泄露** | 安全 | `JwtAuthFilter` + 配置 | `JwtSecretValidator` 启动时检测已知默认值/空值/短密钥,非 dev 环境拒绝启动 | +| **ApiNodeExecutor SSRF** | 安全 | `ApiNodeExecutor.validateUrl()` + `isBlockedAddress()` | 封堵 `file/ftp/jar/gopher`,阻止云元数据 IP,IPv4/IPv6 回环/内网/链路本地地址,IPv4-mapped IPv6 递归检查 | +| **CodeNodeExecutor 沙箱** | 安全 | `CodeNodeExecutor` | 共享守护线程池(4 线程),30s 超时 + Future.cancel(true),线程池满时 AbortPolicy 拒绝 | +| **VariableResolver 注入** | 安全 | `VariableResolver` + 各消费者 | 单次替换防递归展开;消费者层防护:ConditionNodeExecutor 用 getVariable 取原始值,ApiNodeExecutor URL 校验,CodeNodeExecutor ScriptEngine.put 注入 | + +### 🟡 高 — 可能造成部分功能不可用或数据不一致 + +| 风险点 | 类型 | 位置 | 关键防护 | +|:---|:---|:---|:---| +| **ThreadLocal 丢失** | 并发 | `CurrentContext` + `ThreadPoolConfig` | `ObjectProvider` 注入,5 个线程池自动应用 context 传播 | +| **SSE 连接泄漏** | 性能 | `ChatServiceImpl` + `LlmHttpClient` | onTimeout/onCompletion/onError 三回调 + activeSessions 清理 + OkHttp ConnectionPool(20) | +| **MCP Client 竞态** | 并发 | `McpClientServiceImpl` | DCL + synchronized(lock) + 独立锁对象 per serverId | +| **节点超时不均** | 性能 | `WorkflowEngine.coreLoop()` | 按类型区分:LLM 3min / 默认 30s | + +### 🟢 中 — 边界场景或性能退化 + +| 风险点 | 类型 | 关键防护 | +|:---|:---|:---| +| **线程池满** | 性能 | llm/workflow/mcp/sse 四个池 AbortPolicy,仅 asyncExecutor 保留 CallerRunsPolicy | +| **API 响应截断** | 数据一致性 | 512KB 截断 + warn 日志,需工作流设计层面约束 | + +--- + +## 三、测试重心 + +### P0 — 必须有测试覆盖 + +| 测试对象 | 原因 | 状态 | +|:---|:---|:---| +| `ApiNodeExecutor` SSRF 防护 | 安全红线 | ✅ 12 个测试 | +| `JwtSecretValidator` 密钥校验 | 安全红线 | ✅ 8 个测试 | +| `ConditionNodeExecutor` 条件路由 | 工作流核心 | ✅ 18 个测试 | +| `WorkspaceGuard` 空间隔离 | 多租户安全 | ✅ 已有 | +| `CodeNodeExecutor` 沙箱超时 | 安全红线 | ✅ 已有 | + +### P1 — 应该有测试覆盖 + +| 测试对象 | 原因 | 状态 | +|:---|:---|:---| +| `WorkflowEngine` 端到端 | 多节点 DAG + 分支 + 超时 | ⚠️ 仅单测 | +| `ChatServiceImpl` SSE 生命周期 | 连接异常处理 | ⚠️ 仅 mock | +| `McpClientServiceImpl` 生命周期 | 缓存/竞态/降级 | ❌ 无 | +| `ProviderAdapter` SSE 解析 | 各厂商协议差异 | ❌ 无 | +| `ChunkService` RAG 检索 | 向量搜索 + 降级 | ❌ 无 | + +### P2 — 可以后补 + +| 类别 | 原因 | +|:---|:---| +| CRUD Controller / Mapper XML | 样板代码,Service 层已验证 | +| 游标分页 | MyBatis-Plus 封装 | +| 前端组件单测 | TS 类型检查 + vitest 基础覆盖即可,复杂交互用 e2e | + +--- + +## 相关文档 + +- [CLAUDE.md](../../CLAUDE.md) — 核心约束 + 代码提交检查清单 +- [ARCHITECTURE.md](../ARCHITECTURE.md) — 架构设计 + 编码规范 +- [AUTH-WORKSPACE.md](AUTH-WORKSPACE.md) — 认证与工作空间隔离 +- [E2E-TESTING.md](E2E-TESTING.md) — 端到端测试指南 + +--- + +**最后更新**:2026-05-20(从 CLAUDE.md 迁出) diff --git a/eify-web/EXAMPLES.md b/eify-web/EXAMPLES.md deleted file mode 100644 index 520edea..0000000 --- a/eify-web/EXAMPLES.md +++ /dev/null @@ -1,483 +0,0 @@ -# Eify 公共组件使用示例 - -## EifyTable.vue - 通用表格组件 - -### 基础用法 - -```vue - - - -``` - -### 带操作列 - -```vue - -``` - -### 自定义插槽列 - -```vue - - - -``` - ---- - -## EifyFormDialog.vue - 通用表单弹窗 - -### 基础用法 - -```vue - - - -``` - ---- - -## useConfirm.ts - 删除确认 - -### 基础用法 - -```vue - - - -``` - -### 批量删除 - -```vue - -``` - ---- - -## useRequest.ts - 请求状态管理 - -### 基础用法 - -```vue - - - -``` - -### 带回调 - -```vue - -``` - ---- - -## notify.ts - 统一通知 - -### 基础用法 - -```vue - -``` - -### 确认对话框 - -```vue - -``` - ---- - -## 完整示例:用户管理页面 - -```vue - - - -``` diff --git a/eify-web/SIDEBAR-COMPONENT.md b/eify-web/SIDEBAR-COMPONENT.md deleted file mode 100644 index 06378f5..0000000 --- a/eify-web/SIDEBAR-COMPONENT.md +++ /dev/null @@ -1,266 +0,0 @@ -# Eify 侧边栏组件文档 - -## 组件特性 - -✅ **科技感深色侧边栏**:接近纯黑(#0f172a)但不是纯黑,使用渐变和网格装饰 -✅ **品牌渐变 Logo**:主色渐变文字 + 发光动画效果 -✅ **智能选中态**:主色竖线 + 背景微亮 + 模糊光晕 -✅ **平滑过渡动效**:悬停缩放图标 + 文字渐入渐出 -✅ **折叠/展开**:底部折叠按钮 + 状态指示灯 - -## 文件结构 - -``` -eify-web/src/ -├── components/ -│ └── EifySidebar.vue # 侧边栏组件 -├── styles/ -│ ├── design-tokens.css # CSS 变量(含侧边栏变量) -│ └── sidebar.css # 侧边栏样式 -├── views/ -│ └── SidebarPreview.vue # 侧边栏预览页面 -└── App.vue # 主应用(使用侧边栏) -``` - -## 使用方式 - -### 1. 基本使用 - -在 `App.vue` 中直接使用: - -```vue - - - -``` - -### 2. 添加菜单项 - -在 `EifySidebar.vue` 中的 `menuItems` 数组添加: - -```typescript -const menuItems: MenuItem[] = [ - { - path: '/your-path', - title: '你的标题', - icon: 'IconName' // Element Plus 图标名称 - } -] -``` - -### 3. 可用图标 - -Element Plus 图标(常用): - -| 图标 | 用途 | 组件名 | -|:---|:---|:---| -| 💬 | 对话 | `ChatDotRound` | -| 👤 | 用户/Agent | `User` | -| ⚙️ | 设置 | `Setting` | -| 🎨 | 设计 | `Brush` | -| 👁️ | 预览 | `View` | -| 📊 | 数据 | `DataAnalysis` | -| 🔧 | 工具 | `Tools` | -| 📝 | 文档 | `Document` | -| 🏠 | 首页 | `HomeFilled` | - -完整图标列表:https://element-plus.org/zh-CN/component/icon.html - -### 4. 颜色变量 - -侧边栏使用的 CSS 变量: - -```css -/* 深色背景 */ ---eify-bg-sidebar: #0f172a /* 主背景 */ ---eify-bg-sidebar-hover: #1e293b /* 悬停背景 */ ---eify-bg-sidebar-active: #1e293b /* 选中背景 */ ---eify-bg-dark: #0a0f1c /* 更深黑色 */ - -/* 文字颜色 */ ---eify-text-inverse: #ffffff /* 白色文字 */ ---eify-text-quaternary: #cbd5e1 /* 灰色文字 */ - -/* 主色 */ ---eify-primary: #6366f1 /* 主色 */ ---eify-accent: #2dd4bf /* 青色辅色 */ -``` - -## 预览方式 - -### 方式1:侧边栏预览页面 -访问:http://localhost:5173/sidebar-preview - -### 方式2:实际应用 -访问:http://localhost:5173/ -- 侧边栏已集成到主应用 -- 可点击各菜单项体验效果 - -## 交互说明 - -### 默认状态 -- 白色文字 -- 透明背景 -- 无左边竖线 - -### 悬停状态 -- 文字变白 -- 背景微亮(rgba 白色 10% 透明度) -- 图标放大 1.1 倍 -- 图标变青色 - -### 选中状态 -- 文字变白 -- 背景微亮(主色 10% 透明度) -- 左边 3px 主色竖线 -- 竖线模糊光晕效果 - -### 折叠状态 -- 宽度变窄(240px → 64px) -- 隐藏文字 -- Logo 缩小 -- 隐藏副标题 - -## 动画效果 - -| 动画 | 时长 | 缓动 | -|:---|:---|:---| -| Logo 发光 | 3s | ease-in-out infinite | -| 悬停状态 | 200ms | ease-default | -| 图标缩放 | 200ms | ease-default | -| 文字渐入 | 200ms | ease-default | -| 折叠展开 | 200ms | ease-default | - -## 响应式适配 - -侧边栏支持以下断点: - -```css -/* 平板 */ -@media (max-width: 1024px) { - .eify-sidebar { - width: 200px; - } -} - -/* 移动端 */ -@media (max-width: 768px) { - .eify-sidebar { - position: fixed; - left: -100%; - z-index: var(--eify-z-modal); - transition: var(--eify-transition-base); - } - - .eify-sidebar.mobile-open { - left: 0; - } -} -``` - -## 自定义样式 - -### 修改侧边栏宽度 - -```css -.eify-sidebar { - width: 280px !important; /* 默认 240px */ -} -``` - -### 修改 Logo 样式 - -```css -.eify-sidebar-logo-title { - font-size: 32px !important; /* 默认 28px */ - letter-spacing: 3px !important; -} -``` - -### 修改选中态竖线 - -```css -.eify-menu-item.active::before { - width: 4px !important; /* 默认 3px */ -} -``` - -## 设计细节 - -### 1. 渐变背景 - -侧边栏使用微妙的渐变背景: - -```css -background: linear-gradient(180deg, - var(--eify-bg-sidebar) 0%, - #0a0f1c 100% -); -``` - -### 2. 网格装饰 - -悬停时显示科技感网格背景: - -```css -background-image: - linear-gradient(rgba(99, 102, 241, 0.02) 1px, transparent 1px), - linear-gradient(90deg, rgba(99, 102, 241, 0.02) 1px, transparent 1px); -background-size: 20px 20px; -``` - -### 3. 状态指示灯 - -底部角落的青色闪烁指示灯: - -```css -width: 6px; -height: 6px; -background: var(--eify-accent); -animation: eify-status-pulse 2s ease-in-out infinite; -``` - -### 4. 模糊光晕 - -选中项竖线的模糊光晕效果: - -```css -filter: blur(8px); -opacity: 0.5; -``` - -## 浏览器兼容 - -- Chrome 90+ -- Firefox 88+ -- Safari 14+ -- Edge 90+ - -## 性能优化 - -1. **GPU 加速**:使用 `transform` 和 `opacity` 进行动画 -2. **will-change**:动态区域使用 `will-change` 优化 -3. **防抖节流**:折叠切换使用 200ms 过渡 - -## 已知限制 - -1. 折叠状态下 Tooltip 可能显示位置不准确 -2. 长菜单项需要滚动查看 -3. 移动端需要额外的遮罩层 - -## 后续优化 - -- [ ] 移动端适配(遮罩层 + 滑动关闭) -- [ ] 多级菜单支持 -- [ ] 菜单搜索功能 -- [ ] 可配置主题色 -- [ ] 国际化支持 diff --git a/eify-web/package-lock.json b/eify-web/package-lock.json index 6902eb6..72571cf 100644 --- a/eify-web/package-lock.json +++ b/eify-web/package-lock.json @@ -1,12 +1,12 @@ { "name": "eify-web", - "version": "1.0.0-Dev", + "version": "1.0.1-SNAPSHOT", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "eify-web", - "version": "1.0.0-Dev", + "version": "1.0.1-SNAPSHOT", "dependencies": { "@element-plus/icons-vue": "^2.3.2", "@vue-flow/background": "^1.3.2", @@ -20,6 +20,7 @@ "marked": "^18.0.4", "marked-highlight": "^2.2.4", "pinia": "^3.0.4", + "playwright": "^1.60.0", "vue": "^3.5.34", "vue-i18n": "^11.4.4", "vue-router": "^5.0.7" @@ -3252,6 +3253,50 @@ "pathe": "^2.0.3" } }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.5.14", "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.14.tgz", diff --git a/eify-web/package.json b/eify-web/package.json index 8d03308..27d2394 100644 --- a/eify-web/package.json +++ b/eify-web/package.json @@ -24,6 +24,7 @@ "marked": "^18.0.4", "marked-highlight": "^2.2.4", "pinia": "^3.0.4", + "playwright": "^1.60.0", "vue": "^3.5.34", "vue-i18n": "^11.4.4", "vue-router": "^5.0.7" From 830ad4b00fb969e6730ca9deb8dfbe5ac747a57e Mon Sep 17 00:00:00 2001 From: mingming Date: Sat, 23 May 2026 16:57:17 +0800 Subject: [PATCH 30/30] fix: update CI Node.js to 22 and sync package-lock.json - CI workflow uses Node 22 (was 20) to match vue-i18n@11.x engine requirement - Regenerate package-lock.json to resolve missing @emnapi dependencies Co-Authored-By: Claude Opus 4.7 --- .github/workflows/ci.yml | 4 ++-- eify-web/package-lock.json | 36 ++++++++++++++++++------------------ 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da617b7..b4b1446 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,7 +56,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' cache: npm cache-dependency-path: eify-web/package-lock.json @@ -98,7 +98,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' cache: npm cache-dependency-path: eify-web/package-lock.json diff --git a/eify-web/package-lock.json b/eify-web/package-lock.json index 72571cf..70dfdac 100644 --- a/eify-web/package-lock.json +++ b/eify-web/package-lock.json @@ -2273,10 +2273,9 @@ } }, "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "hasInstallScript": true, "license": "MIT", "optional": true, @@ -3283,20 +3282,6 @@ "node": ">=18" } }, - "node_modules/playwright/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/postcss": { "version": "8.5.14", "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.14.tgz", @@ -3904,6 +3889,21 @@ } } }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/vitest": { "version": "4.1.6", "resolved": "https://registry.npmmirror.com/vitest/-/vitest-4.1.6.tgz",