commit ac0f086821c55b2cef6badc0cdff8c218b68404b Author: zfc Date: Wed Jan 28 14:26:46 2026 +0800 feat(init): 完成 Phase 1 基础架构搭建 - 完成 T-001A: 前端项目初始化 (Next.js 14 + TypeScript + Tailwind CSS) - 完成 T-001B: 后端项目初始化 (FastAPI + SQLAlchemy + asyncpg) - 完成 T-002: 数据库配置 (KolVideo 模型 + 索引 + 测试) - 完成 T-003: 基础 UI 框架 (Header/Footer 组件 + 品牌色系) - 完成 T-004: 环境变量配置 (前后端环境变量) Co-Authored-By: Claude diff --git a/.claude/skills/go/SKILL.md b/.claude/skills/go/SKILL.md new file mode 100644 index 0000000..535c5f2 --- /dev/null +++ b/.claude/skills/go/SKILL.md @@ -0,0 +1,324 @@ +--- +name: go +description: 终极执行按钮,激进模式一口气完成开发任务,兼容 0->1 和 1->100 场景。 +--- + +# Go - 发射按钮 + +> **定位**:执行按钮。无论是从零开始的 0->1,还是迭代优化的 1->100,按下 `/go` 就开始干活,不要停。 + +当用户调用 `/go` 或 `/go <任务范围>` 时,执行以下步骤: + +## 1. 前置检查 + +### 1.1 必要文档检查 + +检查以下文件是否存在: + +| 文件 | 必要性 | 用途 | +|------|--------|------| +| `doc/tasks.md` | **必须** | 任务清单,执行的圣经 | +| `doc/PRD.md` | **必须** | 产品需求,理解业务 | +| `doc/FeatureSummary.md` | 建议 | 功能契约 | +| `doc/DevelopmentPlan.md` | 建议 | 技术方案 | +| `doc/UIDesign.md` | 可选 | 界面设计 | + +**缺少必要文档时**: + +``` +❌ 缺少必要文档: +- doc/tasks.md (必须) +- doc/PRD.md (必须) + +请先准备这些文档,或运行: +- /wp 生成 PRD +- /wt 生成 tasks +``` + +### 1.2 读取所有可用文档 + +读取存在的所有文档,建立完整上下文。 + +## 2. 智能判断执行范围 + +### 2.1 检测项目状态 + +``` +┌─────────────────────────────────────────────────────────┐ +│ 项目状态检测 │ +├─────────────────────────────────────────────────────────┤ +│ │ +│ 检查 src/ 或主代码目录是否存在? │ +│ │ +│ ├── 不存在 ──▶ 0->1 模式(全新项目) │ +│ │ │ +│ └── 存在 ──▶ 检查 tasks.md 中的 ITER 标记 │ +│ │ │ +│ ├── 有 ITER 标记 ──▶ 1->100 模式 │ +│ │ │ +│ └── 无 ITER 标记 ──▶ 继续未完成任务 │ +│ │ +└─────────────────────────────────────────────────────────┘ +``` + +### 2.2 确定任务范围 + +**用户指定范围**: + +```bash +/go T-005 # 执行单个任务 +/go T-005~T-010 # 执行任务范围 +/go T-005 T-008 # 执行多个指定任务 +``` + +**自动判断范围**: + +| 场景 | 执行范围 | +|------|----------| +| 0->1 全新项目 | tasks.md 中的所有任务,从 T-001 开始 | +| 1->100 有 ITER 标记 | 优先执行 `` 标记的新任务 | +| 1->100 无 ITER 标记 | 执行所有状态为 pending/todo 的任务 | + +### 2.3 向用户确认范围(唯一一次交互) + +``` +检测到项目状态:{0->1 全新项目 / 1->100 迭代项目} + +即将执行任务: +- T-001: {任务名} +- T-002: {任务名} +- ... +- T-xxx: {任务名} + +共 X 个任务。确认执行?[Y/n] +``` + +**用户确认后,不再有任何交互,直到全部完成。** + +## 3. 激进模式执行 + +### 3.1 执行原则 + +``` +┌─────────────────────────────────────────────────────────┐ +│ 激进模式执行原则 │ +├─────────────────────────────────────────────────────────┤ +│ │ +│ 1. 以 tasks.md 为圣经,严格按顺序执行 │ +│ │ +│ 2. 不要停下来问用户,自主决策 │ +│ │ +│ 3. 遇到问题自主修复,修复失败则记录并继续 │ +│ │ +│ 4. 发现文档冲突,基于架构经验选最优解,注释说明 │ +│ │ +│ 5. 利用所有可用工具:搜索、MCP、Skills │ +│ │ +│ 6. 每完成一个模块,Git 提交一次 │ +│ │ +└─────────────────────────────────────────────────────────┘ +``` + +### 3.2 任务执行流程 + +``` +对于每个任务 T-xxx: +│ +├── 1. 读取任务详情(描述、验收标准、依赖) +│ +├── 2. 检查依赖任务是否完成 +│ └── 未完成 → 先执行依赖任务 +│ +├── 3. 执行任务 +│ ├── 根据任务类型选择执行方式 +│ ├── 编写代码 / 配置 / 测试 +│ └── 验证验收标准 +│ +├── 4. 遇到问题? +│ ├── 尝试自主修复(最多 3 次) +│ ├── 修复成功 → 继续 +│ └── 修复失败 → 记录问题,跳过,继续下一个 +│ +└── 5. 标记任务完成,更新 tasks.md +``` + +### 3.3 自主修复策略 + +| 问题类型 | 修复策略 | +|----------|----------| +| 编译错误 | 分析错误信息,修复代码 | +| 类型错误 | 检查类型定义,修复类型 | +| 依赖缺失 | 安装依赖包 | +| 测试失败 | 修复功能代码使测试通过 | +| 文档冲突 | 基于架构经验选最优解 | +| 未知错误 | 搜索解决方案,尝试修复 | + +## 4. Git 提交规则 + +### 4.1 提交时机 + +每完成一个**模块/Sprint**后立即提交: + +``` +T-001 ~ T-004 → 提交一次(初始化模块) +T-005 ~ T-008 → 提交一次(核心功能模块) +T-009 ~ T-012 → 提交一次(扩展功能模块) +... +``` + +### 4.2 提交信息格式 + +``` +feat(): <简要描述> + +- 完成 T-xxx: {任务名} +- 完成 T-xxx: {任务名} +- ... + +Co-Authored-By: Claude +``` + +**示例**: + +``` +feat(auth): 完成用户认证模块 + +- 完成 T-005: 用户登录功能 +- 完成 T-006: 用户注册功能 +- 完成 T-007: JWT Token 管理 +- 完成 T-008: 权限验证中间件 + +Co-Authored-By: Claude +``` + +## 5. 进度汇报 + +### 5.1 模块完成汇报 + +每完成一个模块,简要汇报: + +``` +✅ 模块完成:{模块名} + - T-005: 用户登录 ✓ + - T-006: 用户注册 ✓ + - T-007: JWT 管理 ✓ + - T-008: 权限验证 ✓ + +Git 提交: feat(auth): 完成用户认证模块 + +继续执行下一模块... +``` + +### 5.2 最终汇报 + +全部完成后,输出完整报告: + +``` +## 🚀 执行完成 + +**执行模式**: {0->1 全新项目 / 1->100 迭代} + +**任务统计**: +| 状态 | 数量 | +|------|------| +| ✅ 完成 | X 个 | +| ⚠️ 跳过 | X 个 | +| ❌ 失败 | X 个 | + +**Git 提交记录**: +- feat(init): 项目初始化 +- feat(auth): 用户认证模块 +- feat(core): 核心功能模块 +- ... + +**跳过/失败的任务**(如有): +| 任务 | 原因 | +|------|------| +| T-xxx | {原因} | + +**下一步建议**: +- 运行 `npm run dev` 验证 +- 运行 `npm run test` 测试 +- 检查跳过的任务 +``` + +## 6. 特殊场景处理 + +### 6.1 技术栈识别 + +从文档中识别技术栈,自动适配: + +| 识别来源 | 技术决策 | +|----------|----------| +| package.json 存在 | Node.js 项目 | +| requirements.txt 存在 | Python 项目 | +| DevelopmentPlan 指定 | 按文档技术栈 | +| 无明确指定 | 询问用户(唯一例外) | + +### 6.2 测试策略 + +- 功能开发完成后执行测试任务 +- 测试失败 → **先修复功能代码使测试通过** +- 不跳过失败的测试继续部署 + +### 6.3 部署任务 + +- 先本地测试验证 +- 确保 build 和 start 正常 +- 远程部署需用户额外确认 + +--- + +## 工作流总览 + +``` +/go + │ + ├── 1. 前置检查 + │ ├── tasks.md 存在? ──▶ 必须 + │ └── PRD.md 存在? ──▶ 必须 + │ + ├── 2. 读取文档,建立上下文 + │ + ├── 3. 智能判断 + │ ├── 项目状态(0->1 / 1->100) + │ └── 任务范围 + │ + ├── 4. 确认执行范围(唯一交互) + │ + ├── 5. 激进模式执行 + │ ├── 按顺序执行任务 + │ ├── 自主修复问题 + │ ├── 模块完成 → Git 提交 + │ └── 汇报进度,继续下一个 + │ + └── 6. 最终汇报 + ├── 任务统计 + ├── Git 提交记录 + └── 下一步建议 +``` + +## 注意事项 + +- **tasks.md 是圣经**,严格按其顺序和内容执行 +- **不要停下来问用户**,自主决策,自主修复 +- **遇到无法解决的问题**,记录并跳过,最后汇报 +- **每完成模块立即提交**,避免大量代码丢失风险 +- **利用所有工具**:搜索、MCP、其他 Skills + +## 与其他 Skill 的关系 + +| 场景 | 使用方式 | +|------|----------| +| 准备文档 | `/wp` `/wf` `/wd` `/wu` `/wt` | +| 评审文档 | `/rp` `/rf` `/rd` `/ru` `/rt` | +| 修改文档 | `/mp` `/mf` `/md` `/mu` `/mt` | +| 迭代变更(更新文档) | `/iter` | +| **执行开发(本 Skill)** | `/go` | + +**典型工作流**: + +``` +0->1:需求 → /wp → /wf → /wd → /wt → /go +1->100:发现问题 → /iter → /go +``` diff --git a/.claude/skills/iter/SKILL.md b/.claude/skills/iter/SKILL.md new file mode 100644 index 0000000..f03c314 --- /dev/null +++ b/.claude/skills/iter/SKILL.md @@ -0,0 +1,210 @@ +--- +name: iter +description: 迭代变更入口,调研问题后更新 PRD.md 和 tasks.md,支持 Bug 修复、功能迭代、技术重构。 +--- + +# Iterate - 迭代变更 + +> **定位**:1-100 阶段的变更入口。项目已上线,需要修复问题或迭代功能时,通过此 skill 调研、澄清、更新文档。 + +当用户调用 `/iter` 或 `/iter <问题描述>` 时,执行以下步骤: +⚠️ 重要:本 skill 只修改文档(PRD.md、tasks.md),绝不执行代码、不运行命令、不修改源文件。 + +## 1. 获取变更描述 + +如果用户提供了参数,使用该描述。否则询问: +> 请描述需要迭代的内容(Bug/功能/重构) + +**示例输入**: +- "登录验证存在漏洞,token 过期后仍可访问" +- "列表页需要增加按时间筛选功能" +- "用户模块性能太差,需要重构缓存策略" + +## 2. 调研分析 + +### 2.1 读取现有文档 + +读取以下文件了解当前状态: + +1. `doc/PRD.md` - 了解产品定义 +2. `doc/tasks.md` - 了解任务现状 + +### 2.2 调研相关代码(可选) + +根据问题描述,定位相关代码文件: + +- 搜索关键词定位文件 +- 读取相关模块代码 +- 分析现有实现 + +### 2.3 分析变更类型 + +| 类型 | 特征 | 影响范围 | +|------|------|----------| +| Bug/漏洞 | 现有功能不符合预期 | 修复逻辑,可能涉及安全 | +| 功能迭代 | 在现有功能上增加/调整 | 新增或修改功能点 | +| 技术重构 | 不改功能,优化实现 | 性能、架构、代码质量 | + +## 3. 澄清确认 + +**【必须】向用户提出澄清问题**,确保理解准确: + +### 3.1 问题理解确认 + +向用户确认: +> 我理解的变更需求是:{一句话总结} +> +> 变更类型:{Bug修复 / 功能迭代 / 技术重构} +> +> 影响范围:{涉及的模块/功能} + +### 3.2 方案选择(如有多种) + +如果有多种解决方案,列出选项让用户选择: + +``` +方案 A:{描述} +- 优点:... +- 缺点:... + +方案 B:{描述} +- 优点:... +- 缺点:... + +请选择方案,或说明其他想法。 +``` + +### 3.3 边界确认 + +确认变更边界: +> 本次变更**包含**: +> - {范围1} +> - {范围2} +> +> 本次变更**不包含**: +> - {排除项} +> +> 是否确认? + +## 4. 用户确认后执行 + +**只有用户明确确认后**,才执行以下更新: + +### 4.1 更新 PRD.md + +使用增量修改标记: + +```markdown + + +新增内容... + +``` + +或修改现有内容: + +```markdown + + +修改后的内容 +``` + +**更新位置**: +- Bug 修复 → 更新对应功能的验收标准 +- 功能迭代 → 在 3.2 功能详情添加/修改功能点 +- 技术重构 → 在 4.x 非功能需求或 7.1 技术约束中说明 + +### 4.2 更新 tasks.md + +新增任务使用标记: + +```markdown + +| T-xxx | {任务名} | {描述} | {依赖} | {优先级} | {验收标准} | +``` + +**任务 ID 规则**: +- 查找现有最大 ID,递增分配 +- 格式:T-xxx(三位数字) + +### 4.3 标记规范 + +所有变更使用 `` 前缀,区分于 `/mp` `/mt` 的标记: + +- `` +- 便于追溯迭代历史 + +## 5. 输出摘要 + +完成后向用户展示: + +``` +## 迭代变更完成 + +**变更类型**: {Bug修复 / 功能迭代 / 技术重构} + +**变更摘要**: {一句话描述} + +**已更新文档**: +- doc/PRD.md: {更新位置} +- doc/tasks.md: 新增任务 T-xxx + +**新增任务**: +| ID | 任务 | 优先级 | +|----|------|--------| +| T-xxx | {任务名} | P0/P1/P2 | + +**下一步**: +- 执行任务 T-xxx +- 或运行 `/rp` `/rt` 评审变更 +``` + +--- + +## 工作流示意 + +``` +用户描述问题 + │ + ▼ +┌─────────────┐ +│ 调研分析 │ ──▶ 读取 PRD、tasks、相关代码 +└──────┬──────┘ + │ + ▼ +┌─────────────┐ +│ 澄清确认 │ ──▶ 提问 → 用户回答 → 确认方案 +└──────┬──────┘ + │ + ▼ 用户确认 +┌─────────────┐ +│ 更新文档 │ ──▶ PRD.md + tasks.md +└──────┬──────┘ + │ + ▼ +┌─────────────┐ +│ 输出摘要 │ +└─────────────┘ +``` + +## 注意事项 + +- **必须先澄清确认**,不要假设用户意图 +- 变更范围要明确,避免 scope creep +- 优先级根据问题严重程度判断: + - 安全漏洞 → P0 + - 功能 Bug → P0/P1 + - 功能迭代 → P1/P2 + - 技术重构 → P1/P2 +- 只更新 PRD + tasks,保持轻量 +- 如需更新其他文档,提示用户手动运行 `/mf` `/md` 等 + +## 与其他 skill 的关系 + +| 场景 | 使用 skill | +|------|------------| +| 迭代变更入口 | `/iter`(本 skill) | +| 需要更新 FeatureSummary | `/iter` 后运行 `/mf` | +| 需要更新 DevelopmentPlan | `/iter` 后运行 `/md` | +| 需要评审变更 | `/iter` 后运行 `/rp` `/rt` | +| 从头生成文档 | 使用 `/wp` `/wf` `/wd` 等 | diff --git a/.claude/skills/md/SKILL.md b/.claude/skills/md/SKILL.md new file mode 100644 index 0000000..47303bd --- /dev/null +++ b/.claude/skills/md/SKILL.md @@ -0,0 +1,112 @@ +--- +name: md +description: 增量修改 DevelopmentPlan.md,根据用户指令在现有内容基础上更新开发计划。 +--- + +# Modify DevelopmentPlan + +当用户调用 `/md` 时,执行以下步骤: + +## 1. 读取目标文档 + +读取以下文件: + +1. `doc/DevelopmentPlan.md` - 目标文档(必须存在) +2. `doc/FeatureSummary.md` - 上游参考文档 +3. `doc/review-DevelopmentPlan-claude.md` - 评审报告(如果存在,自动作为修改依据) + +如果 DevelopmentPlan.md 不存在,提示用户: +> DevelopmentPlan.md 不存在,请先使用 `/wd` 生成开发计划。 + +## 2. 确定修改来源 + +按以下优先级确定修改内容: + +### 2.1 用户提供了修改指令 + +如果用户在调用 `/md` 时附带了参数或说明,直接使用该指令。 + +### 2.2 自动检测评审报告 + +如果用户未提供修改指令,**自动检测** `doc/review-DevelopmentPlan-claude.md` 是否存在: + +- **存在**:读取评审报告,提取其中的问题清单,作为本次修改的依据。向用户确认: + > 检测到评审报告,包含 X 个问题。是否根据评审报告进行修改? + +- **不存在**:询问用户: + > 请说明需要修改的内容,或先运行 `/rd` 生成评审报告。 + +## 3. 修改原则 + +### 3.1 增量修改 + +- 保留原有内容结构和格式 +- 仅修改/新增指定部分 +- 不删除未明确要求删除的内容 + +### 3.2 新增内容标记 + +对于新增的段落或章节: + +```markdown + +新增内容... + +``` + +对于行内新增: + +```markdown +原有内容 新增内容 +``` + +### 3.3 修改内容标记 + +```markdown + +修改后的内容 +``` + +### 3.4 与 FeatureSummary 一致性 + +- 开发任务必须覆盖所有功能 +- 技术方案必须支撑功能需求 +- 阶段划分必须合理 + +## 4. 执行修改 + +| 修改类型 | 处理方式 | +|----------|----------| +| 新增开发任务 | 在对应阶段表格中添加行 | +| 修改技术方案 | 更新技术方案章节,添加 MODIFIED 标记 | +| 调整阶段划分 | 移动任务到新阶段,标记变更 | +| 新增风险项 | 在风险管理表格中添加行 | +| 修改里程碑 | 更新里程碑表格 | + +## 5. 保存并验证 + +1. 保存修改后的文档到 `doc/DevelopmentPlan.md` +2. 使用 git diff 展示变更内容 +3. 向用户确认修改是否符合预期 + +## 6. 输出摘要 + +向用户展示修改摘要: + +- 修改位置(章节/行号) +- 修改类型(新增/修改/删除) +- 修改内容概要 +- 与 FeatureSummary 的一致性确认 + +--- + +## 注意事项 + +- DevelopmentPlan 依赖于 FeatureSummary,修改时需确保与上游一致 +- 修改后,下游文档(UIDesign、tasks)可能需要同步更新 +- 技术方案修改需谨慎评估影响范围 +- 建议修改完成后运行 `/ru` 检查下游一致性 + +## 标记清理 + +用户确认修改无误后,可手动删除标记或保留作为变更历史参考。 diff --git a/.claude/skills/mf/SKILL.md b/.claude/skills/mf/SKILL.md new file mode 100644 index 0000000..fa10e2d --- /dev/null +++ b/.claude/skills/mf/SKILL.md @@ -0,0 +1,111 @@ +--- +name: mf +description: 增量修改 FeatureSummary.md,根据用户指令在现有内容基础上更新功能摘要。 +--- + +# Modify FeatureSummary + +当用户调用 `/mf` 时,执行以下步骤: + +## 1. 读取目标文档 + +读取以下文件: + +1. `doc/FeatureSummary.md` - 目标文档(必须存在) +2. `doc/PRD.md` - 上游参考文档 +3. `doc/review-FeatureSummary-claude.md` - 评审报告(如果存在,自动作为修改依据) + +如果 FeatureSummary.md 不存在,提示用户: +> FeatureSummary.md 不存在,请先使用 `/wf` 生成功能摘要。 + +## 2. 确定修改来源 + +按以下优先级确定修改内容: + +### 2.1 用户提供了修改指令 + +如果用户在调用 `/mf` 时附带了参数或说明,直接使用该指令。 + +### 2.2 自动检测评审报告 + +如果用户未提供修改指令,**自动检测** `doc/review-FeatureSummary-claude.md` 是否存在: + +- **存在**:读取评审报告,提取其中的问题清单,作为本次修改的依据。向用户确认: + > 检测到评审报告,包含 X 个问题。是否根据评审报告进行修改? + +- **不存在**:询问用户: + > 请说明需要修改的内容,或先运行 `/rf` 生成评审报告。 + +## 3. 修改原则 + +### 3.1 增量修改 + +- 保留原有内容结构和格式 +- 仅修改/新增指定部分 +- 不删除未明确要求删除的内容 + +### 3.2 新增内容标记 + +对于新增的段落或章节: + +```markdown + +新增内容... + +``` + +对于行内新增: + +```markdown +原有内容 新增内容 +``` + +### 3.3 修改内容标记 + +```markdown + +修改后的内容 +``` + +### 3.4 与 PRD 一致性 + +- 所有功能必须来源于 PRD +- 修改后的功能描述必须与 PRD 一致 +- 优先级必须与 PRD 匹配 + +## 4. 执行修改 + +| 修改类型 | 处理方式 | +|----------|----------| +| 新增功能 | 在对应模块表格中添加行 | +| 修改描述 | 更新功能描述,添加 MODIFIED 标记 | +| 修改优先级 | 更新优先级列 | +| 新增模块 | 在功能清单中添加新章节 | +| 删除功能 | 标记为删除而非直接移除 | + +## 5. 保存并验证 + +1. 保存修改后的文档到 `doc/FeatureSummary.md` +2. 使用 git diff 展示变更内容 +3. 向用户确认修改是否符合预期 + +## 6. 输出摘要 + +向用户展示修改摘要: + +- 修改位置(章节/行号) +- 修改类型(新增/修改/删除) +- 修改内容概要 +- 与 PRD 的一致性确认 + +--- + +## 注意事项 + +- FeatureSummary 依赖于 PRD,修改时需确保与上游一致 +- 修改后,下游文档(DevelopmentPlan 等)可能需要同步更新 +- 建议修改完成后运行 `/rd` 检查下游一致性 + +## 标记清理 + +用户确认修改无误后,可手动删除标记或保留作为变更历史参考。 diff --git a/.claude/skills/mp/SKILL.md b/.claude/skills/mp/SKILL.md new file mode 100644 index 0000000..5fdeb3f --- /dev/null +++ b/.claude/skills/mp/SKILL.md @@ -0,0 +1,144 @@ +--- +name: mp +description: 增量修改 PRD.md,根据用户指令在现有内容基础上更新产品需求文档。 +--- + +# Modify PRD + +当用户调用 `/mp` 时,执行以下步骤: + +## 1. 读取目标文档 + +读取以下文件: + +1. `doc/PRD.md` - 目标文档(必须存在) +2. `doc/RequirementsDoc.md` - 上游参考文档 +3. `doc/review-PRD-claude.md` - 评审报告(如果存在,自动作为修改依据) + +如果 PRD.md 不存在,提示用户: +> PRD.md 不存在,请先使用 `/wp` 生成产品需求文档。 + +## 2. 确定修改来源 + +按以下优先级确定修改内容: + +### 2.1 用户提供了修改指令 + +如果用户在调用 `/mp` 时附带了参数或说明,直接使用该指令。 + +### 2.2 自动检测评审报告 + +如果用户未提供修改指令,**自动检测** `doc/review-PRD-claude.md` 是否存在: + +- **存在**:读取评审报告,提取其中的问题清单(Critical / Major / Minor),作为本次修改的依据。向用户确认: + > 检测到评审报告 `doc/review-PRD-claude.md`,包含 X 个问题。是否根据评审报告进行修改? + +- **不存在**:询问用户: + > 请说明需要修改的内容,或先运行 `/rp` 生成评审报告。 + +### 2.3 支持的修改来源 + +- 具体的修改描述(如"在功能需求中增加用户权限管理模块") +- 评审报告(自动检测或手动指定路径) +- 对应的 RequirementsDoc 变更(如"/mr 已更新需求,请同步 PRD") + +## 3. 修改原则 + +### 3.1 增量修改 +- 保留原有内容结构和格式 +- 仅修改/新增指定部分 +- 不删除未明确要求删除的内容 + +### 3.2 新增内容标记 + +对于新增的段落或章节,使用 HTML 注释标记: + +```markdown + +新增内容... + +``` + +对于行内新增,使用: +```markdown +原有内容 新增内容 +``` + +### 3.3 修改内容标记 + +对于修改的内容,保留原文作为注释: + +```markdown + +修改后的内容 +``` + +### 3.4 与 RequirementsDoc 一致性 + +- 所有 PRD 内容必须可追溯到 RequirementsDoc +- 如果修改涉及新功能,先确认 RequirementsDoc 中已有对应需求 +- 如果 RequirementsDoc 未包含相关需求,提醒用户先更新需求文档 + +## 4. 执行修改 + +按照用户指令修改文档: + +1. 定位到需要修改的位置 +2. 执行增量修改 +3. 添加相应的标记 +4. 保持文档格式一致性 +5. 确保修改内容与 RequirementsDoc 一致 + +### 4.1 修改类型处理 + +| 修改类型 | 处理方式 | +|----------|----------| +| 新增功能点 | 在对应功能模块表格中添加行,关联用户故事 | +| 新增用户故事 | 在 2.2 用户故事列表中添加,分配 US-xxx ID | +| 修改优先级 | 更新功能点优先级,必要时调整用户故事分类 | +| 修改验收标准 | 更新对应功能点的验收标准列 | +| 新增模块 | 在 3.2 功能详情中添加新的子章节 | +| 修改非功能需求 | 在对应章节更新指标或要求 | + +## 5. 保存并验证 + +1. 保存修改后的文档到 `doc/PRD.md` +2. 使用 git diff 展示变更内容 +3. 向用户确认修改是否符合预期 + +## 6. 输出摘要 + +向用户展示修改摘要: +- 修改位置(章节/行号) +- 修改类型(新增/修改/删除) +- 修改内容概要 +- 与 RequirementsDoc 的一致性确认 + +--- + +## 注意事项 + +- PRD 依赖于 RequirementsDoc,修改时需确保与上游文档一致 +- 修改 PRD 后,下游文档(FeatureSummary、DevelopmentPlan 等)可能需要同步更新 +- 保持现有文档风格(标题层级、表格格式、列表样式) +- 用户故事 ID 必须唯一且连续(US-001, US-002...) +- 所有功能点必须关联到用户故事 +- 重大修改建议先运行 `/rp` 评审确认影响范围 +- 修改完成后,建议用户运行 `/rf` 检查下游文档一致性 + +## 标记清理 + +当用户确认修改无误后,可手动删除 `` 和 `` 标记,或保留作为变更历史参考。 + +通过 git 可追溯完整修改历史。 + +## 质量检查 + +修改 PRD 后,自查以下项目: + +- [ ] 修改内容与 RequirementsDoc 一致 +- [ ] 新增用户故事有唯一 ID +- [ ] 新增功能点关联到用户故事 +- [ ] 新增功能点有明确优先级和验收标准 +- [ ] 标记格式正确(`` / ``) +- [ ] 文档结构完整,格式一致 diff --git a/.claude/skills/mr/SKILL.md b/.claude/skills/mr/SKILL.md new file mode 100644 index 0000000..8dd9ad8 --- /dev/null +++ b/.claude/skills/mr/SKILL.md @@ -0,0 +1,95 @@ +--- +name: mr +description: 增量修改 RequirementsDoc.md,根据用户指令在现有内容基础上更新需求文档。 +--- + +# Modify RequirementsDoc + +当用户调用 `/mr` 时,执行以下步骤: + +## 1. 读取目标文档 + +读取 `doc/RequirementsDoc.md` 文件。 + +如果文件不存在,提示用户: +> RequirementsDoc.md 不存在,请先使用人工方式创建需求文档。 + +## 2. 获取修改指令 + +向用户确认修改内容。用户应提供以下信息之一: + +- 具体的修改描述(如"在第3节增加性能需求") +- 评审报告路径(如 `doc/review-RequirementsDoc-claude.md`) +- 直接的修改内容 + +如果用户未提供修改指令,询问: +> 请说明需要修改的内容,或提供评审报告路径。 + +## 3. 修改原则 + +### 3.1 增量修改 +- 保留原有内容结构和格式 +- 仅修改/新增指定部分 +- 不删除未明确要求删除的内容 + +### 3.2 新增内容标记 + +对于新增的段落或章节,使用 HTML 注释标记: + +```markdown + +新增内容... + +``` + +对于行内新增,使用: +```markdown +原有内容 新增内容 +``` + +### 3.3 修改内容标记 + +对于修改的内容,保留原文作为注释: + +```markdown + +修改后的内容 +``` + +## 4. 执行修改 + +按照用户指令修改文档: + +1. 定位到需要修改的位置 +2. 执行增量修改 +3. 添加相应的标记 +4. 保持文档格式一致性 + +## 5. 保存并验证 + +1. 保存修改后的文档到 `doc/RequirementsDoc.md` +2. 使用 git diff 展示变更内容 +3. 向用户确认修改是否符合预期 + +## 6. 输出摘要 + +向用户展示修改摘要: +- 修改位置(章节/行号) +- 修改类型(新增/修改/删除) +- 修改内容概要 + +--- + +## 注意事项 + +- RequirementsDoc 是文档链源头,修改会影响所有下游文档 +- 修改前确认用户意图,避免误改 +- 保持现有文档风格(标题层级、表格格式、列表样式) +- 重大修改建议先运行 `/rr` 评审确认影响范围 +- 修改完成后,建议用户检查下游文档是否需要同步更新 + +## 标记清理 + +当用户确认修改无误后,可手动删除 `` 和 `` 标记,或保留作为变更历史参考。 + +通过 git 可追溯完整修改历史。 diff --git a/.claude/skills/mt/SKILL.md b/.claude/skills/mt/SKILL.md new file mode 100644 index 0000000..670c00a --- /dev/null +++ b/.claude/skills/mt/SKILL.md @@ -0,0 +1,132 @@ +--- +name: mt +description: 增量修改 tasks.md,根据用户指令在现有内容基础上更新任务列表。 +--- + +# Modify Tasks + +当用户调用 `/mt` 时,执行以下步骤: + +## 1. 读取目标文档 + +读取以下文件: + +1. `doc/tasks.md` - 目标文档(必须存在) +2. `doc/UIDesign.md` - 上游参考文档 +3. `doc/DevelopmentPlan.md` - 上游参考文档 +4. `doc/review-tasks-claude.md` - 评审报告(如果存在,自动作为修改依据) + +如果 tasks.md 不存在,提示用户: +> tasks.md 不存在,请先使用 `/wt` 生成任务列表。 + +## 2. 确定修改来源 + +按以下优先级确定修改内容: + +### 2.1 用户提供了修改指令 + +如果用户在调用 `/mt` 时附带了参数或说明,直接使用该指令。 + +### 2.2 自动检测评审报告 + +如果用户未提供修改指令,**自动检测** `doc/review-tasks-claude.md` 是否存在: + +- **存在**:读取评审报告,提取其中的问题清单,作为本次修改的依据。向用户确认: + > 检测到评审报告,包含 X 个问题。是否根据评审报告进行修改? + +- **不存在**:询问用户: + > 请说明需要修改的内容,或先运行 `/rt` 生成评审报告。 + +## 3. 修改原则 + +### 3.1 增量修改 + +- 保留原有内容结构和格式 +- 仅修改/新增指定部分 +- 不删除未明确要求删除的内容 + +### 3.2 新增内容标记 + +对于新增的段落或章节: + +```markdown + +新增内容... + +``` + +对于行内新增: + +```markdown +原有内容 新增内容 +``` + +### 3.3 修改内容标记 + +```markdown + +修改后的内容 +``` + +### 3.4 与上游文档一致性 + +- 任务必须覆盖 DevelopmentPlan 所有开发项 +- 任务必须覆盖 UIDesign 所有页面实现 +- 任务依赖关系必须合理 + +## 4. 执行修改 + +| 修改类型 | 处理方式 | +|----------|----------| +| 新增任务 | 在对应阶段表格中添加行,分配新 ID | +| 修改描述 | 更新任务描述,添加 MODIFIED 标记 | +| 修改优先级 | 更新优先级列 | +| 修改依赖 | 更新依赖列,检查循环依赖 | +| 修改验收标准 | 更新验收标准列 | +| 调整阶段 | 移动任务到新阶段,更新依赖图 | + +### 4.1 任务 ID 规则 + +- 新增任务 ID 必须唯一 +- ID 格式:T-XXX(三位数字,如 T-001) +- 在现有最大 ID 基础上递增 + +## 5. 保存并验证 + +1. 保存修改后的文档到 `doc/tasks.md` +2. 使用 git diff 展示变更内容 +3. 向用户确认修改是否符合预期 + +## 6. 输出摘要 + +向用户展示修改摘要: + +- 修改位置(章节/行号) +- 修改类型(新增/修改/删除) +- 修改内容概要 +- 新增/修改的任务 ID 列表 +- 与上游文档的一致性确认 + +--- + +## 注意事项 + +- tasks.md 是文档链末端,修改不影响其他文档 +- 任务 ID 必须唯一,不可重复使用已删除的 ID +- 修改依赖关系时需检查是否产生循环依赖 +- 验收标准必须具体可测试 +- 任务粒度要适中 + +## 标记清理 + +用户确认修改无误后,可手动删除标记或保留作为变更历史参考。 + +## 质量检查 + +修改 tasks 后,自查以下项目: + +- [ ] 任务 ID 唯一且格式正确 +- [ ] 无循环依赖 +- [ ] 验收标准明确 +- [ ] 覆盖所有上游功能 +- [ ] 标记格式正确 diff --git a/.claude/skills/mu/SKILL.md b/.claude/skills/mu/SKILL.md new file mode 100644 index 0000000..e0d6061 --- /dev/null +++ b/.claude/skills/mu/SKILL.md @@ -0,0 +1,114 @@ +--- +name: mu +description: 增量修改 UIDesign.md,根据用户指令在现有内容基础上更新 UI 设计文档。 +--- + +# Modify UIDesign + +当用户调用 `/mu` 时,执行以下步骤: + +## 1. 读取目标文档 + +读取以下文件: + +1. `doc/UIDesign.md` - 目标文档(必须存在) +2. `doc/DevelopmentPlan.md` - 上游参考文档 +3. `doc/review-UIDesign-claude.md` - 评审报告(如果存在,自动作为修改依据) + +如果 UIDesign.md 不存在,提示用户: +> UIDesign.md 不存在,请先使用 `/wu` 生成 UI 设计文档。 + +## 2. 确定修改来源 + +按以下优先级确定修改内容: + +### 2.1 用户提供了修改指令 + +如果用户在调用 `/mu` 时附带了参数或说明,直接使用该指令。 + +### 2.2 自动检测评审报告 + +如果用户未提供修改指令,**自动检测** `doc/review-UIDesign-claude.md` 是否存在: + +- **存在**:读取评审报告,提取其中的问题清单,作为本次修改的依据。向用户确认: + > 检测到评审报告,包含 X 个问题。是否根据评审报告进行修改? + +- **不存在**:询问用户: + > 请说明需要修改的内容,或先运行 `/ru` 生成评审报告。 + +## 3. 修改原则 + +### 3.1 增量修改 + +- 保留原有内容结构和格式 +- 仅修改/新增指定部分 +- 不删除未明确要求删除的内容 + +### 3.2 新增内容标记 + +对于新增的段落或章节: + +```markdown + +新增内容... + +``` + +对于行内新增: + +```markdown +原有内容 新增内容 +``` + +### 3.3 修改内容标记 + +```markdown + +修改后的内容 +``` + +### 3.4 与 DevelopmentPlan 一致性 + +- 页面设计必须覆盖所有功能模块 +- 交互流程必须支撑功能需求 +- 设计规范必须统一 + +## 4. 执行修改 + +| 修改类型 | 处理方式 | +|----------|----------| +| 新增页面 | 在页面设计章节添加新子章节 | +| 修改布局 | 更新布局描述,添加 MODIFIED 标记 | +| 修改组件 | 更新组件表格 | +| 修改交互 | 更新交互说明 | +| 新增状态 | 在状态列表中添加项目 | +| 修改设计规范 | 更新设计规范章节 | + +## 5. 保存并验证 + +1. 保存修改后的文档到 `doc/UIDesign.md` +2. 使用 git diff 展示变更内容 +3. 向用户确认修改是否符合预期 + +## 6. 输出摘要 + +向用户展示修改摘要: + +- 修改位置(章节/行号) +- 修改类型(新增/修改/删除) +- 修改内容概要 +- 与 DevelopmentPlan 的一致性确认 + +--- + +## 注意事项 + +- UIDesign 依赖于 DevelopmentPlan,修改时需确保与上游一致 +- 修改后,下游文档(tasks)可能需要同步更新 +- 页面修改需考虑对用户流程的影响 +- 设计规范修改需检查所有页面的一致性 +- 建议修改完成后运行 `/rt` 检查下游一致性 + +## 标记清理 + +用户确认修改无误后,可手动删除标记或保留作为变更历史参考。 diff --git a/.claude/skills/rd/SKILL.md b/.claude/skills/rd/SKILL.md new file mode 100644 index 0000000..665117c --- /dev/null +++ b/.claude/skills/rd/SKILL.md @@ -0,0 +1,101 @@ +--- +name: rd +description: 评审 DevelopmentPlan.md,检查技术可行性和与上游文档一致性,输出结构化评审报告。 +--- + +# Review DevelopmentPlan + +当用户调用 `/rd` 时,执行以下步骤: + +## 1. 读取文档 + +读取以下文件: + +1. `doc/DevelopmentPlan.md` - 目标文档(必须存在) +2. `doc/FeatureSummary.md` - 上游参照文档 + +如果 DevelopmentPlan.md 不存在,提示用户: +> DevelopmentPlan.md 不存在,请先使用 `/wd` 生成开发计划。 + +## 2. 评审维度 + +### 2.1 与 FeatureSummary 一致性检查 + +- 开发任务是否覆盖所有功能模块 +- 技术方案是否支撑功能需求 +- 排期是否合理 + +### 2.2 技术可行性检查 + +- 技术方案是否可行 +- 技术栈选择是否合理 +- 是否存在技术风险 +- 依赖关系是否明确 + +### 2.3 完整性检查 + +- 是否有明确的里程碑划分 +- 是否有资源分配说明 +- 是否有风险应对措施 + +## 3. 生成评审报告 + +输出到 `doc/review-DevelopmentPlan-claude.md`,结构如下: + +```markdown +# DevelopmentPlan 评审报告 + +## 概要 + +| 项目 | 内容 | +|------|------| +| 评审时间 | {YYYY-MM-DD HH:MM} | +| 目标文档 | doc/DevelopmentPlan.md | +| 参照文档 | doc/FeatureSummary.md | +| 问题统计 | X 个严重 / Y 个一般 / Z 个建议 | + +## 功能覆盖分析 + +| FeatureSummary 功能 | DevelopmentPlan 对应 | 状态 | +|---------------------|----------------------|------| +| {功能名} | {对应任务/模块} | ✅/⚠️/❌ | + +## 技术风险分析 + +| 风险项 | 影响范围 | 严重程度 | 建议措施 | +|--------|----------|----------|----------| +| {风险} | {范围} | 高/中/低 | {措施} | + +## 问题清单 + +### 严重问题 (Critical) +{问题列表,含位置引用} + +### 一般问题 (Major) +{问题列表,含位置引用} + +### 改进建议 (Minor) +{建议列表} + +## 评审结论 + +{通过 / 需修改后通过 / 不通过} + +### 下一步行动 +- [ ] {待办事项} +``` + +## 4. 输出规范 + +- 输出语言:中文 +- 问题分级:Critical / Major / Minor +- 包含文件引用(如 `doc/DevelopmentPlan.md:28`) +- 技术风险需明确影响范围和应对建议 + +--- + +## 注意事项 + +- 只做评审,不修改原文档 +- 重点关注技术可行性和风险 +- 评审报告保存后,建议用户根据问题运行 `/md` 修改 diff --git a/.claude/skills/rf/SKILL.md b/.claude/skills/rf/SKILL.md new file mode 100644 index 0000000..ad3e464 --- /dev/null +++ b/.claude/skills/rf/SKILL.md @@ -0,0 +1,96 @@ +--- +name: rf +description: 评审 FeatureSummary.md,对比 PRD 检查一致性,输出结构化评审报告。 +--- + +# Review FeatureSummary + +当用户调用 `/rf` 时,执行以下步骤: + +## 1. 读取文档 + +读取以下文件: + +1. `doc/FeatureSummary.md` - 目标文档(必须存在) +2. `doc/PRD.md` - 上游参照文档 + +如果 FeatureSummary.md 不存在,提示用户: +> FeatureSummary.md 不存在,请先使用 `/wf` 生成功能摘要。 + +## 2. 评审维度 + +### 2.1 与 PRD 一致性检查 + +- 功能模块是否完整覆盖 PRD 3.2 功能详情 +- 功能描述是否与 PRD 一致 +- 优先级标注是否与 PRD 匹配 + +### 2.2 完整性检查 + +- 每个功能模块是否有清晰的描述 +- 是否遗漏 PRD 中的功能点 +- 功能分类是否合理 + +### 2.3 质量检查 + +- 描述是否简洁准确 +- 是否有冗余或重复内容 +- 格式是否规范统一 + +## 3. 生成评审报告 + +输出到 `doc/review-FeatureSummary-claude.md`,结构如下: + +```markdown +# FeatureSummary 评审报告 + +## 概要 + +| 项目 | 内容 | +|------|------| +| 评审时间 | {YYYY-MM-DD HH:MM} | +| 目标文档 | doc/FeatureSummary.md | +| 参照文档 | doc/PRD.md | +| 问题统计 | X 个严重 / Y 个一般 / Z 个建议 | + +## 覆盖度分析 + +| PRD 功能模块 | FeatureSummary 对应 | 状态 | +|--------------|---------------------|------| +| {模块名} | {对应位置} | ✅/⚠️/❌ | + +**覆盖率**: X/Y 完全覆盖 + +## 问题清单 + +### 严重问题 (Critical) +{问题列表,含位置引用} + +### 一般问题 (Major) +{问题列表,含位置引用} + +### 改进建议 (Minor) +{建议列表} + +## 评审结论 + +{通过 / 需修改后通过 / 不通过} + +### 下一步行动 +- [ ] {待办事项} +``` + +## 4. 输出规范 + +- 输出语言:中文 +- 问题分级:Critical / Major / Minor +- 包含文件引用(如 `doc/FeatureSummary.md:15`) +- 问题按严重性排序 + +--- + +## 注意事项 + +- 只做评审,不修改原文档 +- 重点检查与 PRD 的一致性 +- 评审报告保存后,建议用户根据问题运行 `/mf` 修改 diff --git a/.claude/skills/rp/SKILL.md b/.claude/skills/rp/SKILL.md new file mode 100644 index 0000000..befe913 --- /dev/null +++ b/.claude/skills/rp/SKILL.md @@ -0,0 +1,177 @@ +--- +name: rp +description: 评审 PRD.md,对比 RequirementsDoc 检查一致性,输出结构化评审报告。 +--- + +# Review PRD + +当用户调用 `/rp` 时,执行以下步骤: + +## 1. 读取文档 + +读取以下文件: +- 目标文档:`doc/PRD.md` +- 上游文档:`doc/RequirementsDoc.md` + +如果 PRD.md 不存在,提示用户: +> PRD.md 不存在,请先使用 `/wp` 生成 PRD。 + +如果 RequirementsDoc.md 不存在,提示用户: +> RequirementsDoc.md 不存在,无法进行一致性检查。请先创建需求文档。 + +## 2. 评审维度 + +PRD 位于文档链的第二层,需要对比上游 RequirementsDoc 进行评审。 + +### 2.1 与 RequirementsDoc 的一致性 + +- [ ] PRD 是否覆盖了 RequirementsDoc 中的所有功能需求 +- [ ] PRD 是否覆盖了 RequirementsDoc 中的所有非功能需求 +- [ ] PRD 中是否有 RequirementsDoc 中未提及的需求(需标注来源) +- [ ] 术语定义是否与 RequirementsDoc 一致 +- [ ] 优先级划分是否与 RequirementsDoc 一致 + +### 2.2 用户故事质量 + +- [ ] 所有用户故事是否有唯一 ID(US-xxx) +- [ ] 用户故事是否符合格式:作为{角色},我想要{功能},以便{价值} +- [ ] 用户角色是否明确定义 +- [ ] 验收标准是否具体可测试 +- [ ] 用户旅程是否完整描述核心流程 + +### 2.3 功能需求完整性 + +- [ ] 功能架构是否清晰(模块划分合理) +- [ ] 所有功能点是否关联到用户故事 +- [ ] 功能点是否有明确的优先级 +- [ ] 功能点是否有验收标准 +- [ ] 是否遗漏边界情况和异常处理 + +### 2.4 非功能需求 + +- [ ] 性能需求是否有量化指标 +- [ ] 安全需求是否明确 +- [ ] 兼容性需求是否完整 +- [ ] 可用性需求是否可验证 + +### 2.5 文档结构 + +- [ ] 文档结构是否完整(无空章节) +- [ ] 格式是否统一(表格、列表、标题层级) +- [ ] 术语表是否完整 + +## 3. 生成评审报告 + +按以下格式输出评审报告: + +```markdown +# PRD 评审报告 + +## 概要 + +| 项目 | 内容 | +|------|------| +| 评审时间 | {YYYY-MM-DD HH:mm} | +| 目标文档 | doc/PRD.md | +| 参照文档 | doc/RequirementsDoc.md | +| 问题统计 | {critical} 个严重 / {major} 个一般 / {minor} 个建议 | + +## 一致性检查 + +### 需求覆盖分析 + +| RequirementsDoc 需求项 | PRD 对应位置 | 状态 | +|------------------------|--------------|------| +| {需求1} | {PRD章节/用户故事ID} | ✅ 已覆盖 / ⚠️ 部分覆盖 / ❌ 未覆盖 | + +### 差异说明 + +{列出 PRD 中新增的、RequirementsDoc 未提及的内容,需说明来源或理由} + +## 问题清单 + +### 严重问题 (Critical) + +> 必须修复,否则影响后续文档生成 + +1. **[位置: doc/PRD.md:行号]** 问题描述 + - 现状:... + - 与 RequirementsDoc 的差异:... + - 建议:... + +### 一般问题 (Major) + +> 建议修复,可提升文档质量 + +1. **[位置]** 问题描述 + - 建议:... + +### 改进建议 (Minor) + +> 可选优化项 + +1. **[位置]** 建议内容 + +## 用户故事评估 + +| 评估项 | 结果 | +|--------|------| +| 用户故事总数 | {数量} | +| 符合格式规范 | {数量} / {总数} | +| 有验收标准 | {数量} / {总数} | +| 关联功能点 | {数量} / {总数} | + +### 用户故事问题 + +{列出不符合规范的用户故事} + +## 评审结论 + +{通过 / 需修改后通过 / 不通过} + +**结论说明**: +- 通过:PRD 与 RequirementsDoc 一致,可进入下一阶段 +- 需修改后通过:存在问题但不影响整体理解,修复后可继续 +- 不通过:存在严重一致性问题或遗漏,需重新生成 + +### 下一步行动 + +- [ ] 行动项1 +- [ ] 行动项2 +``` + +## 4. 保存报告 + +将评审报告保存到 `doc/review-PRD-claude.md`。 + +如果文件已存在,覆盖原文件(历史版本通过 git 追溯)。 + +## 5. 输出摘要 + +向用户展示评审摘要: +- 一致性检查结果(覆盖率) +- 发现的问题数量(按严重程度分类) +- 用户故事评估结果 +- 评审结论 +- 报告文件路径 + +--- + +## 注意事项 + +- 评审时保持客观,聚焦于文档质量和一致性 +- 问题描述要具体,给出明确的位置引用(如 `doc/PRD.md:42`) +- 一致性检查要逐项对比,不能遗漏 +- 建议要可操作,避免模糊表述 +- 不要修改原文档,只输出评审报告 + +## 常见问题模式 + +在评审时重点关注以下常见问题: + +1. **需求遗漏**:RequirementsDoc 中有但 PRD 中没有的需求 +2. **需求偏离**:PRD 中的描述与 RequirementsDoc 不一致 +3. **凭空添加**:PRD 中有但 RequirementsDoc 中没有的需求(需要来源说明) +4. **用户故事缺陷**:格式不规范、缺少验收标准、角色不明确 +5. **功能孤立**:功能点未关联到任何用户故事 +6. **优先级冲突**:PRD 与 RequirementsDoc 的优先级划分不一致 diff --git a/.claude/skills/rr/SKILL.md b/.claude/skills/rr/SKILL.md new file mode 100644 index 0000000..7967a9b --- /dev/null +++ b/.claude/skills/rr/SKILL.md @@ -0,0 +1,111 @@ +--- +name: rr +description: 评审 RequirementsDoc.md,检查需求文档的完整性、清晰度和可执行性,输出结构化评审报告。 +--- + +# Review RequirementsDoc + +当用户调用 `/rr` 时,执行以下步骤: + +## 1. 读取目标文档 + +读取 `doc/RequirementsDoc.md` 文件。 + +如果文件不存在,提示用户: +> RequirementsDoc.md 不存在,请先创建需求文档。 + +## 2. 评审维度 + +RequirementsDoc 是文档链的源头,没有上游依赖。重点检查以下维度: + +### 2.1 完整性 +- [ ] 产品概述是否清晰(定位、目标用户、核心价值) +- [ ] 功能需求是否完整列出 +- [ ] 非功能需求是否涵盖(性能、安全、兼容性) +- [ ] 数据规范是否明确(输入输出格式、字段定义) +- [ ] 边界条件和异常情况是否考虑 + +### 2.2 清晰度 +- [ ] 术语定义是否一致,无歧义 +- [ ] 用例描述是否具体可理解 +- [ ] 优先级是否明确标注 +- [ ] 是否有模糊表述("等"、"可能"、"应该"等) + +### 2.3 可执行性 +- [ ] 需求是否可被验证(有明确的验收标准) +- [ ] 技术约束是否合理 +- [ ] 依赖项是否明确 + +### 2.4 结构规范 +- [ ] 文档结构是否清晰(章节划分合理) +- [ ] 格式是否统一(表格、列表、标题层级) + +## 3. 生成评审报告 + +按以下格式输出评审报告: + +```markdown +# RequirementsDoc 评审报告 + +## 概要 + +| 项目 | 内容 | +|------|------| +| 评审时间 | {YYYY-MM-DD HH:mm} | +| 目标文档 | doc/RequirementsDoc.md | +| 问题统计 | {critical} 个严重 / {major} 个一般 / {minor} 个建议 | + +## 问题清单 + +### 严重问题 (Critical) + +> 必须修复,否则影响后续文档生成 + +1. **[位置: 第X节/第Y行]** 问题描述 + - 现状:... + - 建议:... + +### 一般问题 (Major) + +> 建议修复,可提升文档质量 + +1. **[位置]** 问题描述 + - 建议:... + +### 改进建议 (Minor) + +> 可选优化项 + +1. **[位置]** 建议内容 + +## 评审结论 + +{通过 / 需修改后通过 / 不通过} + +### 下一步行动 + +- [ ] 行动项1 +- [ ] 行动项2 +``` + +## 4. 保存报告 + +将评审报告保存到 `doc/review-RequirementsDoc-claude.md`。 + +如果文件已存在,覆盖原文件(历史版本通过 git 追溯)。 + +## 5. 输出摘要 + +向用户展示评审摘要: +- 发现的问题数量(按严重程度分类) +- 评审结论 +- 报告文件路径 + +--- + +## 注意事项 + +- 评审时保持客观,聚焦于文档质量而非业务判断 +- 问题描述要具体,给出明确的位置引用 +- 建议要可操作,避免模糊表述 +- 不要修改原文档,只输出评审报告 diff --git a/.claude/skills/rt/SKILL.md b/.claude/skills/rt/SKILL.md new file mode 100644 index 0000000..7db4e44 --- /dev/null +++ b/.claude/skills/rt/SKILL.md @@ -0,0 +1,115 @@ +--- +name: rt +description: 评审 tasks.md,检查任务完整性和与上游文档一致性,输出结构化评审报告。 +--- + +# Review Tasks + +当用户调用 `/rt` 时,执行以下步骤: + +## 1. 读取文档 + +读取以下文件: + +1. `doc/tasks.md` - 目标文档(必须存在) +2. `doc/UIDesign.md` - 上游参照文档 +3. `doc/DevelopmentPlan.md` - 上游参照文档 + +如果 tasks.md 不存在,提示用户: +> tasks.md 不存在,请先使用 `/wt` 生成任务列表。 + +## 2. 评审维度 + +### 2.1 与上游文档一致性检查 + +- 任务是否覆盖 DevelopmentPlan 所有开发项 +- 任务是否覆盖 UIDesign 所有页面实现 +- 任务优先级是否与功能优先级匹配 + +### 2.2 任务完整性检查 + +- 每个任务是否有明确的描述 +- 任务粒度是否合适(不过大也不过小) +- 任务依赖关系是否明确 +- 验收标准是否清晰 + +### 2.3 可执行性检查 + +- 任务是否可直接开始执行 +- 是否有阻塞项未说明 +- 估时是否合理(如有) + +## 3. 生成评审报告 + +输出到 `doc/review-tasks-claude.md`,结构如下: + +```markdown +# Tasks 评审报告 + +## 概要 + +| 项目 | 内容 | +|------|------| +| 评审时间 | {YYYY-MM-DD HH:MM} | +| 目标文档 | doc/tasks.md | +| 参照文档 | doc/UIDesign.md, doc/DevelopmentPlan.md | +| 问题统计 | X 个严重 / Y 个一般 / Z 个建议 | + +## 覆盖度分析 + +### DevelopmentPlan 覆盖 + +| 开发项 | 对应任务 | 状态 | +|--------|----------|------| +| {开发项} | {任务ID/名称} | ✅/⚠️/❌ | + +### UIDesign 覆盖 + +| UI 页面 | 对应任务 | 状态 | +|---------|----------|------| +| {页面名} | {任务ID/名称} | ✅/⚠️/❌ | + +**总覆盖率**: X/Y + +## 任务质量分析 + +| 检查项 | 通过数 | 总数 | +|--------|--------|------| +| 有明确描述 | X | Y | +| 有验收标准 | X | Y | +| 粒度合适 | X | Y | + +## 问题清单 + +### 严重问题 (Critical) +{问题列表,含位置引用} + +### 一般问题 (Major) +{问题列表,含位置引用} + +### 改进建议 (Minor) +{建议列表} + +## 评审结论 + +{通过 / 需修改后通过 / 不通过} + +### 下一步行动 +- [ ] {待办事项} +``` + +## 4. 输出规范 + +- 输出语言:中文 +- 问题分级:Critical / Major / Minor +- 包含文件引用(如 `doc/tasks.md:12`) +- 任务问题需说明对开发执行的影响 + +--- + +## 注意事项 + +- 只做评审,不修改原文档 +- 重点检查任务覆盖度和可执行性 +- tasks.md 是文档链末端,必须覆盖所有上游功能 +- 评审报告保存后,建议用户根据问题运行 `/mt` 修改 diff --git a/.claude/skills/ru/SKILL.md b/.claude/skills/ru/SKILL.md new file mode 100644 index 0000000..8fbc125 --- /dev/null +++ b/.claude/skills/ru/SKILL.md @@ -0,0 +1,105 @@ +--- +name: ru +description: 评审 UIDesign.md,对比 DevelopmentPlan 检查设计一致性,输出结构化评审报告。 +--- + +# Review UIDesign + +当用户调用 `/ru` 时,执行以下步骤: + +## 1. 读取文档 + +读取以下文件: + +1. `doc/UIDesign.md` - 目标文档(必须存在) +2. `doc/DevelopmentPlan.md` - 上游参照文档 + +如果 UIDesign.md 不存在,提示用户: +> UIDesign.md 不存在,请先使用 `/wu` 生成 UI 设计文档。 + +## 2. 评审维度 + +### 2.1 与 DevelopmentPlan 一致性检查 + +- UI 页面是否覆盖所有功能模块 +- 交互流程是否与开发计划匹配 +- 页面结构是否支撑功能需求 + +### 2.2 设计完整性检查 + +- 页面列表是否完整 +- 每个页面是否有清晰的布局描述 +- 交互说明是否充分 +- 状态变化是否考虑全面(加载、错误、空状态等) + +### 2.3 可用性检查 + +- 用户流程是否顺畅 +- 信息架构是否合理 +- 是否有一致的设计规范 + +## 3. 生成评审报告 + +输出到 `doc/review-UIDesign-claude.md`,结构如下: + +```markdown +# UIDesign 评审报告 + +## 概要 + +| 项目 | 内容 | +|------|------| +| 评审时间 | {YYYY-MM-DD HH:MM} | +| 目标文档 | doc/UIDesign.md | +| 参照文档 | doc/DevelopmentPlan.md | +| 问题统计 | X 个严重 / Y 个一般 / Z 个建议 | + +## 页面覆盖分析 + +| DevelopmentPlan 功能 | UIDesign 页面 | 状态 | +|----------------------|---------------|------| +| {功能名} | {对应页面} | ✅/⚠️/❌ | + +**覆盖率**: X/Y 完全覆盖 + +## 设计一致性检查 + +| 检查项 | 结果 | +|--------|------| +| 页面命名规范 | ✅/❌ | +| 布局风格统一 | ✅/❌ | +| 交互模式一致 | ✅/❌ | + +## 问题清单 + +### 严重问题 (Critical) +{问题列表,含位置引用} + +### 一般问题 (Major) +{问题列表,含位置引用} + +### 改进建议 (Minor) +{建议列表} + +## 评审结论 + +{通过 / 需修改后通过 / 不通过} + +### 下一步行动 +- [ ] {待办事项} +``` + +## 4. 输出规范 + +- 输出语言:中文 +- 问题分级:Critical / Major / Minor +- 包含文件引用(如 `doc/UIDesign.md:45`) +- 设计问题需说明影响的用户体验 + +--- + +## 注意事项 + +- 只做评审,不修改原文档 +- 重点检查页面覆盖度和设计一致性 +- 评审报告保存后,建议用户根据问题运行 `/mu` 修改 diff --git a/.claude/skills/up/SKILL.md b/.claude/skills/up/SKILL.md new file mode 100644 index 0000000..4b1ca96 --- /dev/null +++ b/.claude/skills/up/SKILL.md @@ -0,0 +1,78 @@ +--- +name: update +aliases: [up] +description: 收集用户反馈并更新最近使用的 skill。可通过 /update 或 /up 调用。在用完某个 skill 后调用此命令来优化该 skill。 +disable-model-invocation: true +argument-hint: [skill-name] +--- + +# Skill 更新助手 + +当用户调用 `/up` 或 `/up ` 时,执行以下步骤: + +## 1. 识别目标 Skill + +**如果用户提供了参数 `$ARGUMENTS`**: +- 直接使用指定的 skill 名称作为更新目标 + +**如果没有提供参数**: +分析当前对话历史,找出最近使用的 skill: +- 搜索对话中的 `` 标签,识别调用过的 skill +- 如果找到多个 skill,让用户确认要更新哪一个 +- 如果没有找到任何 skill 调用记录,提示用户先使用一个 skill + +## 2. 收集用户反馈 + +向用户询问以下问题(使用 AskUserQuestion 工具): + +**问题 1:这次使用体验如何?** +- 很好,skill 完全满足需求 +- 基本满足,但有改进空间 +- 不太满意,需要较大调整 + +**问题 2:具体需要改进的方面?**(多选) +- 执行步骤不够清晰 +- 缺少某些功能 +- 输出格式需要调整 +- 提示词需要优化 +- 其他(用户自定义输入) + +## 3. 分析优化点 + +基于用户反馈和本次 skill 使用过程,分析以下方面: + +1. **执行流程**:哪些步骤可以简化或合并? +2. **指令清晰度**:哪些指令描述不够明确? +3. **遗漏功能**:有哪些场景没有覆盖到? +4. **输出质量**:输出格式是否符合用户预期? + +## 4. 定位 Skill 文件 + +按以下优先级搜索 skill 文件: +1. 项目级:`.claude/skills//SKILL.md` +2. 用户级:`~/.claude/skills//SKILL.md` + +## 5. 更新 Skill + +读取现有的 SKILL.md 文件内容,根据分析结果进行更新: + +- 保持 frontmatter 格式不变(除非需要修改 description) +- 优化执行步骤的描述 +- 添加缺失的功能说明 +- 改进提示词的表达方式 +- 添加必要的注意事项或边界情况处理 + +## 6. 确认更新 + +在更新前,向用户展示: +- 修改前后的对比(diff 格式) +- 说明每处修改的原因 + +用户确认后才执行实际的文件更新。 + +## 注意事项 + +- 如果 skill 文件不存在或路径无法确定,提示用户手动指定路径 +- 更新时保持 skill 的原有风格和结构 +- 重大修改需要用户明确确认 +- 保留原有的有效内容,只做增量优化 diff --git a/.claude/skills/wd/SKILL.md b/.claude/skills/wd/SKILL.md new file mode 100644 index 0000000..9343961 --- /dev/null +++ b/.claude/skills/wd/SKILL.md @@ -0,0 +1,323 @@ +--- +name: wd +description: 从上游文档生成 DevelopmentPlan.md,包含技术方案和开发排期。 +--- + +# Write DevelopmentPlan + +> **文档定位**:DevelopmentPlan 是「执行蓝图」文档,偏技术语言和时间约束。定义技术架构、实现方案、开发阶段、里程碑,是开发团队的行动指南。 + +当用户调用 `/wd` 时,执行以下步骤: + +## 1. 读取源文档 + +读取以下文件: + +1. `doc/RequirementsDoc.md` - 必须存在 +2. `doc/PRD.md` - 必须存在 +3. `doc/FeatureSummary.md` - 必须存在 + +如果文件不存在,提示用户: +> 缺少上游文档,请先确保 RequirementsDoc.md、PRD.md 和 FeatureSummary.md 存在。 + +如果已存在 `doc/DevelopmentPlan.md`,同时读取作为参考(保持风格一致)。 + +## 2. 分析开发需求 + +从上游文档中提取以下信息: + +### 2.1 功能需求 + +- 从 FeatureSummary 获取功能清单和契约 +- 从 PRD 获取功能详情和验收标准 + +### 2.2 技术约束 + +- 从 PRD 获取技术约束 +- 从 RequirementsDoc 获取技术决策 + +### 2.3 优先级排序 + +- 按 P0 → P1 → P2 顺序规划开发 +- 考虑功能依赖关系 + +## 3. 生成 DevelopmentPlan + +按以下结构生成文档: + +```markdown +# {产品名称} - 开发计划 + +## 文档信息 + +| 项目 | 内容 | +|------|------| +| 版本 | v1.0 | +| 创建日期 | {YYYY-MM-DD} | +| 来源文档 | FeatureSummary.md | + +## 1. 项目概述 + +### 1.1 项目目标 + +{从 PRD 提取的项目目标} + +### 1.2 技术栈 + +| 层级 | 技术选型 | 版本 | 说明 | +|------|----------|------|------| +| 前端 | {技术} | {版本} | {说明} | +| 后端 | {技术} | {版本} | {说明} | +| 数据库 | {技术} | {版本} | {说明} | +| 基础设施 | {技术} | {版本} | {说明} | + +### 1.3 开发原则 + +{开发规范和原则} + +## 2. 技术架构 + +### 2.1 系统架构图 + +**【必须】使用架构图展示系统整体结构:** + +``` +┌─────────────────────────────────────────────────────────┐ +│ 客户端层 │ +│ ┌──────────────┐ ┌──────────────┐ │ +│ │ Web App │ │ Mobile App │ │ +│ └──────┬───────┘ └──────┬───────┘ │ +└─────────┼─────────────────┼─────────────────────────────┘ + │ │ + ▼ ▼ +┌─────────────────────────────────────────────────────────┐ +│ API 网关层 │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ API Gateway / Load Balancer │ │ +│ └──────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ 服务层 │ +│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ +│ │ 服务 A │ │ 服务 B │ │ 服务 C │ │ +│ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │ +└────────┼───────────────┼───────────────┼────────────────┘ + │ │ │ + ▼ ▼ ▼ +┌─────────────────────────────────────────────────────────┐ +│ 数据层 │ +│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ +│ │ 数据库 │ │ 缓存 │ │ 消息队列 │ │ +│ └────────────┘ └────────────┘ └────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +### 2.2 模块依赖图 + +**【必须】使用依赖图展示模块间关系:** + +``` +┌──────────────┐ +│ 模块 A │ +│ (核心模块) │ +└──────┬───────┘ + │ + ┌───┴───┐ + ▼ ▼ +┌──────┐ ┌──────┐ +│模块B │ │模块C │ +└──┬───┘ └──┬───┘ + │ │ + ▼ ▼ +┌──────────────┐ +│ 模块 D │ +│ (基础设施) │ +└──────────────┘ +``` + +### 2.3 数据流图 + +**【必须】使用数据流图展示关键数据流转:** + +``` +用户请求 ──▶ API Gateway ──▶ 服务A ──▶ 数据库 + │ + ▼ + 缓存层 + │ + ▼ + 服务B ──▶ 外部API +``` + +## 3. 开发阶段 + +### 3.1 阶段时间线 + +**【必须】使用时间线展示开发阶段:** + +``` + Phase 1 Phase 2 Phase 3 + │ │ │ + {起止日期} {起止日期} {起止日期} + │ │ │ + ▼ ▼ ▼ + ┌─────────┐ ┌─────────┐ ┌─────────┐ + │ 基础 │ ────▶ │ 核心 │ ────▶ │ 优化 │ + │ 架构 │ │ 功能 │ │ 扩展 │ + └─────────┘ └─────────┘ └─────────┘ + + 交付物: 交付物: 交付物: + • {交付1} • {交付1} • {交付1} + • {交付2} • {交付2} • {交付2} +``` + +### 3.2 Phase 1: {阶段名称} + +**目标**: {阶段目标} + +**时间**: {起止日期} + +| 任务ID | 任务 | 描述 | 依赖 | 优先级 | 关联功能 | +|--------|------|------|------|--------|----------| +| T-001 | {任务名} | {描述} | - | P0 | F-001 | +| T-002 | {任务名} | {描述} | T-001 | P0 | F-002 | + +**阶段依赖图:** + +``` +T-001 ──▶ T-002 ──▶ T-003 + │ + └──▶ T-004 +``` + +{重复以上结构覆盖所有阶段} + +## 4. 技术方案 + +### 4.1 {模块名称} + +**功能**: {功能描述} + +**技术选型**: + +| 组件 | 技术 | 选型理由 | +|------|------|----------| +| {组件} | {技术} | {理由} | + +**架构设计**: + +``` +┌─────────────────────────────────────┐ +│ {模块名称} │ +├─────────────────────────────────────┤ +│ ┌─────────┐ ┌─────────┐ │ +│ │ 组件A │ ───▶ │ 组件B │ │ +│ └─────────┘ └─────────┘ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌─────────────────────────┐ │ +│ │ 数据层 │ │ +│ └─────────────────────────┘ │ +└─────────────────────────────────────┘ +``` + +**接口设计**: + +| 接口 | 方法 | 路径 | 说明 | +|------|------|------|------| +| {接口名} | GET/POST | /api/xxx | {说明} | + +**实现要点**: + +- {技术要点1} +- {技术要点2} + +{重复以上结构覆盖所有模块} + +## 5. 风险管理 + +| 风险 | 可能性 | 影响 | 应对措施 | 负责人 | +|------|--------|------|----------|--------| +| {风险} | 高/中/低 | 高/中/低 | {措施} | {负责人} | + +## 6. 里程碑 + +**【必须】使用里程碑图展示关键节点:** + +``` +M1 M2 M3 M4 +│ │ │ │ +▼ ▼ ▼ ▼ +◆───────────────◆───────────────◆───────────────◆ +│ │ │ │ +{日期} {日期} {日期} {日期} +{里程碑名} {里程碑名} {里程碑名} {里程碑名} +``` + +| 里程碑 | 日期 | 目标 | 交付物 | 验收标准 | +|--------|------|------|--------|----------| +| M1 | {日期} | {目标} | {交付物} | {标准} | + +## 7. 资源需求 + +| 角色 | 人数 | 职责 | 参与阶段 | +|------|------|------|----------| +| {角色} | {人数} | {职责} | Phase 1-2 | +``` + +## 4. 保存文档 + +将生成的 DevelopmentPlan 保存到 `doc/DevelopmentPlan.md`。 + +如果文件已存在,覆盖原文件(历史版本通过 git 追溯)。 + +## 5. 输出摘要 + +向用户展示生成摘要: + +- DevelopmentPlan 文件路径 +- 开发阶段数量 +- 技术方案模块数量 +- 建议的下一步操作(运行 `/rd` 评审) + +--- + +## 可视化输出要求 + +DevelopmentPlan 作为「执行蓝图」文档,需要清晰传达技术方案和时间安排,**必须包含**: + +| 章节 | 可视化形式 | 必要性 | +|------|------------|--------| +| 2.1 系统架构图 | 架构图(ASCII) | **必须** | +| 2.2 模块依赖图 | 依赖图(ASCII) | **必须** | +| 2.3 数据流图 | 数据流图(ASCII) | **必须** | +| 3.1 阶段时间线 | 时间线(ASCII) | **必须** | +| 3.x 阶段依赖图 | 任务依赖图 | **必须** | +| 4.x 模块架构 | 模块架构图 | 建议 | +| 6. 里程碑 | 里程碑图 | **必须** | + +## 注意事项 + +- DevelopmentPlan 使用**技术语言**,面向开发团队 +- 开发计划必须覆盖 FeatureSummary 所有功能 +- 技术方案要具体可执行,避免过于抽象 +- 阶段划分要合理,考虑依赖关系 +- 时间安排要务实,预留缓冲 +- 风险评估要全面,有应对措施 + +## 质量检查 + +生成 DevelopmentPlan 后,自查以下项目: + +- [ ] 覆盖 FeatureSummary 所有功能 +- [ ] **系统架构图清晰展示整体结构** +- [ ] **模块依赖图清晰展示依赖关系** +- [ ] **数据流图展示关键数据流转** +- [ ] **开发阶段有时间线图** +- [ ] **每个阶段有任务依赖图** +- [ ] **里程碑有里程碑图** +- [ ] 技术方案具体可执行 +- [ ] 任务 ID 唯一(T-xxx) +- [ ] 任务与功能 ID 关联 diff --git a/.claude/skills/wf/SKILL.md b/.claude/skills/wf/SKILL.md new file mode 100644 index 0000000..c8ac3c4 --- /dev/null +++ b/.claude/skills/wf/SKILL.md @@ -0,0 +1,234 @@ +--- +name: wf +description: 从 RequirementsDoc.md 和 PRD.md 生成 FeatureSummary.md,提供功能全貌概览。 +--- + +# Write FeatureSummary + +> **文档定位**:FeatureSummary 是「功能契约」文档,是产品与开发的桥梁。精确定义功能边界、输入输出、依赖关系,确保双方对"做什么"达成共识。 + +当用户调用 `/wf` 时,执行以下步骤: + +## 1. 读取源文档 + +读取以下文件: + +1. `doc/RequirementsDoc.md` - 必须存在 +2. `doc/PRD.md` - 必须存在 + +如果文件不存在,提示用户: +> 缺少上游文档,请先确保 RequirementsDoc.md 和 PRD.md 存在。 + +如果已存在 `doc/FeatureSummary.md`,同时读取作为参考(保持风格一致)。 + +## 2. 分析功能需求 + +从 PRD 中提取以下信息: + +### 2.1 功能模块 + +- 从 PRD 3.1 功能架构提取模块结构 +- 从 PRD 3.2 功能详情提取各模块功能点 + +### 2.2 功能分类 + +按以下维度整理功能: + +- 按模块分组 +- 按优先级标注(P0/P1/P2) +- 按用户角色关联 + +### 2.3 功能边界 + +明确每个功能的: + +- 输入:触发条件、输入数据 +- 输出:预期结果、输出数据 +- 边界:不包含什么、异常情况 + +## 3. 生成 FeatureSummary + +按以下结构生成文档: + +```markdown +# {产品名称} - 功能摘要 + +## 文档信息 + +| 项目 | 内容 | +|------|------| +| 版本 | v1.0 | +| 创建日期 | {YYYY-MM-DD} | +| 来源文档 | PRD.md | + +## 1. 功能总览 + +### 1.1 功能统计 + +| 类别 | 数量 | +|------|------| +| 功能模块 | X 个 | +| P0 功能 | X 个 | +| P1 功能 | X 个 | +| P2 功能 | X 个 | + +### 1.2 功能架构图 + +**【必须】使用模块图展示功能架构和模块关系:** + +``` +┌─────────────────────────────────────────────────┐ +│ {产品名称} │ +├─────────────────────────────────────────────────┤ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ 模块A │ │ 模块B │ │ 模块C │ │ +│ │ ──────── │ │ ──────── │ │ ──────── │ │ +│ │ • 功能1 │ │ • 功能1 │ │ • 功能1 │ │ +│ │ • 功能2 │ │ • 功能2 │ │ │ │ +│ └──────────┘ └──────────┘ └──────────┘ │ +└─────────────────────────────────────────────────┘ +``` + +### 1.3 模块依赖关系 + +**【必须】使用依赖图展示模块间关系:** + +``` +┌──────────┐ +│ 模块A │ +└────┬─────┘ + │ 依赖 + ▼ +┌──────────┐ ┌──────────┐ +│ 模块B │ ◀── │ 模块C │ +└──────────┘ └──────────┘ +``` + +## 2. 功能清单 + +### 2.1 {模块名称} + +**模块职责**: {一句话描述模块职责} + +#### 功能列表 + +| ID | 功能 | 描述 | 优先级 | 关联用户故事 | +|----|------|------|--------|--------------| +| F-001 | {功能名} | {简要描述} | P0 | US-xxx | + +#### 功能契约详情 + +**F-001: {功能名}** + +| 契约项 | 说明 | +|--------|------| +| **触发条件** | {什么情况下触发此功能} | +| **输入** | {输入数据/参数} | +| **处理逻辑** | {核心处理步骤} | +| **输出** | {输出结果/返回值} | +| **异常情况** | {可能的错误及处理} | +| **边界说明** | {不包含什么、限制条件} | + +{重复以上结构覆盖所有功能} + +{重复以上结构覆盖所有模块} + +## 3. 功能依赖矩阵 + +**【必须】使用矩阵表格展示功能间依赖:** + +| 功能 | 依赖 F-001 | 依赖 F-002 | 依赖 F-003 | +|------|------------|------------|------------| +| F-001 | - | | | +| F-002 | ✓ | - | | +| F-003 | | ✓ | - | + +说明: +- ✓ 表示行功能依赖列功能 +- 空白表示无依赖 + +## 4. 功能流程图 + +**【必须】使用流程图展示核心功能流程:** + +### 4.1 {核心流程名称} + +``` +┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ +│ F-001 │ ──▶ │ F-002 │ ──▶ │ F-003 │ ──▶ │ 完成 │ +│ {功能} │ │ {功能} │ │ {功能} │ │ │ +└─────────┘ └─────────┘ └─────────┘ └─────────┘ + │ + ▼ 异常 + ┌─────────┐ + │ 错误处理 │ + └─────────┘ +``` + +## 5. 版本规划 + +| 版本 | 包含功能 | 功能ID | 目标 | +|------|----------|--------|------| +| MVP | {功能列表} | F-001, F-002 | {目标} | +| v1.1 | {功能列表} | F-003, F-004 | {目标} | +| v2.0 | {功能列表} | F-005+ | {目标} | + +## 6. 接口契约预览 + +> 详细接口定义在 DevelopmentPlan 中,此处仅列出关键接口 + +| 功能 | 接口类型 | 简要说明 | +|------|----------|----------| +| F-001 | API | {说明} | +| F-002 | Event | {说明} | +``` + +## 4. 保存文档 + +将生成的 FeatureSummary 保存到 `doc/FeatureSummary.md`。 + +如果文件已存在,覆盖原文件(历史版本通过 git 追溯)。 + +## 5. 输出摘要 + +向用户展示生成摘要: + +- FeatureSummary 文件路径 +- 功能模块数量 +- 各优先级功能数量 +- 建议的下一步操作(运行 `/rf` 评审) + +--- + +## 可视化输出要求 + +FeatureSummary 作为「功能契约」文档,需要精确传达功能定义,**必须包含**: + +| 章节 | 可视化形式 | 必要性 | +|------|------------|--------| +| 1.2 功能架构图 | 模块图(ASCII) | **必须** | +| 1.3 模块依赖关系 | 依赖图(ASCII) | **必须** | +| 3. 功能依赖矩阵 | 矩阵表格 | **必须** | +| 4. 功能流程图 | 流程图(ASCII) | **必须** | + +## 注意事项 + +- FeatureSummary 是产品与开发的**桥梁**,语言要精确、无歧义 +- 功能摘要必须完全来源于 PRD,不要臆造功能 +- 每个功能必须有明确的**输入、输出、边界** +- 功能 ID 必须唯一(F-xxx 格式) +- 优先级必须与 PRD 一致 +- 功能依赖关系必须明确,避免循环依赖 + +## 质量检查 + +生成 FeatureSummary 后,自查以下项目: + +- [ ] 所有功能都有唯一 ID(F-xxx) +- [ ] 所有功能都有契约详情(输入/输出/边界) +- [ ] **功能架构图清晰展示模块结构** +- [ ] **模块依赖图清晰展示依赖关系** +- [ ] **功能依赖矩阵完整** +- [ ] **核心流程有流程图** +- [ ] 优先级与 PRD 一致 +- [ ] 无遗漏 PRD 中的功能 diff --git a/.claude/skills/wp/SKILL.md b/.claude/skills/wp/SKILL.md new file mode 100644 index 0000000..e647daf --- /dev/null +++ b/.claude/skills/wp/SKILL.md @@ -0,0 +1,318 @@ +--- +name: wp +description: 从 RequirementsDoc.md 生成 PRD.md,将需求文档转化为结构化的产品需求文档。 +--- + +# Write PRD + +> **文档定位**:PRD 是「价值主张」文档,使用业务语言描述产品要解决什么问题、为谁创造什么价值。面向产品、业务、管理层沟通。 + +当用户调用 `/wp` 时,执行以下步骤: + +## 1. 读取源文档 + +读取 `doc/RequirementsDoc.md` 文件。 + +如果文件不存在,提示用户: +> RequirementsDoc.md 不存在,请先创建需求文档。 + +如果已存在 `doc/PRD.md`,同时读取作为参考(保持风格一致)。 + +## 2. 分析需求文档 + +从 RequirementsDoc 中提取以下信息: + +### 2.1 产品定位 + +- 产品名称 +- 目标用户 +- 核心价值主张 +- 竞品对比(如有) + +### 2.2 功能需求 + +- 功能模块划分 +- 各模块详细需求 +- 功能优先级(P0/P1/P2) + +### 2.3 非功能需求 + +- 性能要求 +- 安全要求 +- 兼容性要求 +- 可用性要求 + +### 2.4 约束条件 + +- 技术约束 +- 业务约束 +- 时间约束 + +## 3. 生成 PRD + +按以下结构生成 PRD 文档: + +```markdown +# {产品名称} - 产品需求文档 (PRD) + +## 文档信息 + +| 项目 | 内容 | +|------|------| +| 版本 | v1.0 | +| 创建日期 | {YYYY-MM-DD} | +| 状态 | 草稿 | + +## 1. 产品概述 + +### 1.1 产品背景 + +{从 RequirementsDoc 提取,说明产品解决的问题和市场机会} + +### 1.2 产品定位 + +{目标用户、核心价值、差异化优势} + +### 1.3 产品目标 + +| 目标 | 指标 | 衡量方式 | +|------|------|----------| +| {业务目标} | {量化指标} | {如何衡量} | + +## 2. 用户故事 + +PRD 以用户故事为核心驱动,所有功能需求都应对应到具体的用户故事。 + +### 2.1 用户角色定义 + +| 角色 | 描述 | 核心目标 | 痛点 | +|------|------|----------|------| +| {角色1} | {角色描述} | {核心目标} | {当前痛点} | + +### 2.2 用户故事列表 + +按优先级排列的用户故事: + +#### P0 - 核心故事 + +| ID | 用户故事 | 验收标准 | +|----|----------|----------| +| US-001 | 作为{角色},我想要{功能},以便{价值} | {验收标准} | + +#### P1 - 重要故事 + +| ID | 用户故事 | 验收标准 | +|----|----------|----------| +| US-xxx | 作为{角色},我想要{功能},以便{价值} | {验收标准} | + +#### P2 - 次要故事 + +| ID | 用户故事 | 验收标准 | +|----|----------|----------| +| US-xxx | 作为{角色},我想要{功能},以便{价值} | {验收标准} | + +### 2.3 用户旅程 + +**【必须】使用流程图展示核心用户旅程:** + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ 触发点 │ ──▶ │ 关键步骤 │ ──▶ │ 目标达成 │ +│ {描述} │ │ {描述} │ │ {描述} │ +└─────────────┘ └─────────────┘ └─────────────┘ + │ │ │ + ▼ ▼ ▼ + {用户感受} {用户感受} {用户感受} +``` + +{描述用户完成核心任务的完整流程,从触发点到目标达成} + +## 3. 功能需求 + +> 功能需求与用户故事的对应关系 + +### 3.1 功能架构 + +**【必须】使用树状图或模块图展示功能架构:** + +``` +{产品名称} +├── {模块A} +│ ├── {功能A1} +│ └── {功能A2} +├── {模块B} +│ ├── {功能B1} +│ └── {功能B2} +└── {模块C} + └── {功能C1} +``` + +### 3.2 功能详情 + +#### 3.2.1 {模块名称} + +| 功能点 | 描述 | 关联用户故事 | 优先级 | 验收标准 | +|--------|------|--------------|--------|----------| +| {功能1} | {描述} | US-001 | P0 | {标准} | + +{重复以上结构覆盖所有模块} + +## 4. 非功能需求 + +### 4.1 性能需求 + +| 指标 | 要求 | 说明 | +|------|------|------| +| {响应时间} | {要求} | {场景说明} | + +### 4.2 安全需求 + +{数据安全、访问控制、合规要求} + +### 4.3 兼容性需求 + +| 平台/环境 | 支持版本 | +|-----------|----------| +| {平台} | {版本} | + +### 4.4 可用性需求 + +{SLA、故障恢复、监控告警} + +## 5. 数据需求 + +### 5.1 数据模型 + +**【建议】使用 ER 图或表格展示核心实体关系:** + +``` +┌──────────┐ ┌──────────┐ +│ 实体A │ 1───n │ 实体B │ +├──────────┤ ├──────────┤ +│ 字段1 │ │ 字段1 │ +│ 字段2 │ │ 字段2 │ +└──────────┘ └──────────┘ +``` + +### 5.2 数据规范 + +| 字段 | 类型 | 说明 | 校验规则 | +|------|------|------|----------| +| {字段名} | {类型} | {说明} | {规则} | + +## 6. 接口需求 + +### 6.1 外部接口 + +| 接口 | 用途 | 提供方 | +|------|------|--------| +| {接口名} | {用途} | {第三方} | + +### 6.2 内部接口 + +{模块间接口规范} + +## 7. 约束与依赖 + +### 7.1 技术约束 + +| 约束 | 说明 | 影响 | +|------|------|------| +| {约束} | {说明} | {影响范围} | + +### 7.2 业务约束 + +{法规、政策、合同限制} + +### 7.3 外部依赖 + +{第三方服务、团队依赖} + +## 8. 里程碑规划 + +**【建议】使用时间线展示里程碑:** + +``` +Phase 1 Phase 2 Phase 3 + │ │ │ + ▼ ▼ ▼ +┌──────┐ ┌──────┐ ┌──────┐ +│ MVP │ ────▶ │ v1.1 │ ────▶ │ v2.0 │ +└──────┘ └──────┘ └──────┘ +{日期} {日期} {日期} +``` + +| 阶段 | 目标 | 交付物 | +|------|------|--------| +| {阶段1} | {目标} | {交付物} | + +## 9. 风险评估 + +| 风险 | 可能性 | 影响 | 应对措施 | +|------|--------|------|----------| +| {风险1} | 高/中/低 | 高/中/低 | {措施} | + +## 附录 + +### A. 术语表 + +| 术语 | 定义 | +|------|------| +| {术语} | {定义} | + +### B. 参考文档 + +- RequirementsDoc.md +``` + +## 4. 保存文档 + +将生成的 PRD 保存到 `doc/PRD.md`。 + +如果文件已存在,覆盖原文件(历史版本通过 git 追溯)。 + +## 5. 输出摘要 + +向用户展示生成摘要: + +- PRD 文件路径 +- 包含的功能模块数量 +- 主要章节概览 +- 建议的下一步操作(运行 `/rp` 评审) + +--- + +## 可视化输出要求 + +PRD 作为「价值主张」文档,需要便于业务沟通理解,**必须包含**: + +| 章节 | 可视化形式 | 必要性 | +|------|------------|--------| +| 2.3 用户旅程 | 流程图(ASCII) | **必须** | +| 3.1 功能架构 | 树状图/模块图 | **必须** | +| 5.1 数据模型 | ER 图 | 建议 | +| 8. 里程碑规划 | 时间线 | 建议 | + +## 注意事项 + +- PRD 使用**业务语言**,避免过多技术术语 +- PRD 内容必须完全来源于 RequirementsDoc,不要臆造需求 +- 如果 RequirementsDoc 信息不完整,在对应章节标注"待补充" +- 保持语言风格与现有文档一致 +- 优先级标注遵循 P0 > P1 > P2 规则 +- 验收标准要具体可测试 + +## 质量检查 + +生成 PRD 后,自查以下项目: + +- [ ] 所有用户故事都有唯一 ID(US-xxx) +- [ ] 所有用户故事都符合格式:作为{角色},我想要{功能},以便{价值} +- [ ] 所有功能点都关联到用户故事 +- [ ] 所有功能点都有明确的优先级 +- [ ] 所有功能点都有验收标准 +- [ ] **用户旅程有流程图** +- [ ] **功能架构有模块图** +- [ ] 非功能需求有量化指标 +- [ ] 无遗漏 RequirementsDoc 中的重要需求 +- [ ] 文档结构完整,无空章节(或标注"待补充") diff --git a/.claude/skills/wt/SKILL.md b/.claude/skills/wt/SKILL.md new file mode 100644 index 0000000..253fe91 --- /dev/null +++ b/.claude/skills/wt/SKILL.md @@ -0,0 +1,128 @@ +--- +name: wt +description: 从上游文档生成 tasks.md,创建可直接执行的任务列表。 +--- + +# Write Tasks + +当用户调用 `/wt` 时,执行以下步骤: + +## 1. 读取源文档 + +读取以下文件: + +1. `doc/RequirementsDoc.md` - 必须存在 +2. `doc/PRD.md` - 必须存在 +3. `doc/FeatureSummary.md` - 必须存在 +4. `doc/DevelopmentPlan.md` - 必须存在 +5. `doc/UIDesign.md` - 必须存在 + +如果文件不存在,提示用户: +> 缺少上游文档,请确保所有上游文档存在。 + +如果已存在 `doc/tasks.md`,同时读取作为参考(保持风格一致)。 + +## 2. 分析任务需求 + +从上游文档中提取以下信息: + +### 2.1 开发任务 + +- 从 DevelopmentPlan 获取开发阶段和任务 +- 从 UIDesign 获取页面实现任务 + +### 2.2 任务依赖 + +- 分析任务间的依赖关系 +- 确定任务执行顺序 + +### 2.3 验收标准 + +- 从 PRD 获取功能验收标准 +- 转化为任务级别的完成标准 + +## 3. 生成 Tasks + +按以下结构生成文档: + +```markdown +# {产品名称} - 任务列表 + +## 文档信息 + +| 项目 | 内容 | +|------|------| +| 版本 | v1.0 | +| 创建日期 | {YYYY-MM-DD} | +| 来源文档 | UIDesign.md, DevelopmentPlan.md | + +## 1. 任务总览 + +| 统计项 | 数量 | +|--------|------| +| 总任务数 | X | +| P0 任务 | X | +| P1 任务 | X | +| P2 任务 | X | + +## 2. Phase 1 任务 + +### 2.1 {模块/功能名} + +| ID | 任务 | 描述 | 优先级 | 依赖 | 验收标准 | +|----|------|------|--------|------|----------| +| T-001 | {任务名} | {描述} | P0 | - | {标准} | +| T-002 | {任务名} | {描述} | P0 | T-001 | {标准} | + +{重复以上结构覆盖所有模块} + +## 3. Phase 2 任务 + +{同上结构} + +## 4. Phase N 任务 + +{同上结构} + +## 5. 任务依赖图 + +``` +T-001 (基础设施) + ├── T-002 (功能A) + │ └── T-005 (功能A优化) + └── T-003 (功能B) + └── T-004 (功能B扩展) +``` + +## 6. 执行检查清单 + +- [ ] T-001: {任务名} +- [ ] T-002: {任务名} +{所有任务的检查清单} +``` + +## 4. 保存文档 + +将生成的 tasks 保存到 `doc/tasks.md`。 + +如果文件已存在,覆盖原文件(历史版本通过 git 追溯)。 + +## 5. 输出摘要 + +向用户展示生成摘要: + +- tasks 文件路径 +- 任务总数 +- 各阶段任务分布 +- 建议的下一步操作(运行 `/rt` 评审) + +--- + +## 注意事项 + +- 任务必须覆盖 DevelopmentPlan 和 UIDesign 所有内容 +- 任务 ID 必须唯一(T-001, T-002...) +- 每个任务必须有明确的验收标准 +- 任务粒度要适中,可在合理时间内完成 +- 依赖关系要明确,避免循环依赖 +- 任务应可直接执行,无歧义 diff --git a/.claude/skills/wu/SKILL.md b/.claude/skills/wu/SKILL.md new file mode 100644 index 0000000..3b17b58 --- /dev/null +++ b/.claude/skills/wu/SKILL.md @@ -0,0 +1,352 @@ +--- +name: wu +description: 从上游文档生成 UIDesign.md,覆盖所有用户界面设计。 +--- + +# Write UIDesign + +> **文档定位**:UIDesign 是「界面蓝图」文档,用 ASCII 原型图精确传达页面布局、组件结构、交互流程,是前端开发的直接参考。 + +当用户调用 `/wu` 时,执行以下步骤: + +## 1. 读取源文档 + +读取以下文件: + +1. `doc/RequirementsDoc.md` - 必须存在 +2. `doc/PRD.md` - 必须存在 +3. `doc/FeatureSummary.md` - 必须存在 +4. `doc/DevelopmentPlan.md` - 必须存在 + +如果文件不存在,提示用户: +> 缺少上游文档,请确保所有上游文档存在。 + +如果已存在 `doc/UIDesign.md`,同时读取作为参考(保持风格一致)。 + +## 2. 分析 UI 需求 + +从上游文档中提取以下信息: + +### 2.1 页面需求 + +- 从 PRD 用户旅程分析所需页面 +- 从 FeatureSummary 获取功能对应的界面 +- 从 DevelopmentPlan 获取技术实现约束 + +### 2.2 用户流程 + +- 主要用户旅程 +- 页面跳转关系 +- 交互流程 + +## 3. 生成 UIDesign + +按以下结构生成文档: + +```markdown +# {产品名称} - UI 设计文档 + +## 文档信息 + +| 项目 | 内容 | +|------|------| +| 版本 | v1.0 | +| 创建日期 | {YYYY-MM-DD} | +| 来源文档 | DevelopmentPlan.md | + +## 1. 设计概述 + +### 1.1 设计原则 + +{UI 设计原则和规范} + +### 1.2 页面总览 + +| 页面ID | 页面名称 | 描述 | 对应功能 | 优先级 | +|--------|----------|------|----------|--------| +| P-001 | {页面名} | {描述} | F-001 | P0 | + +### 1.3 页面导航图 + +**【必须】使用导航图展示页面跳转关系:** + +``` + ┌─────────────┐ + │ 首页 │ + │ P-001 │ + └──────┬──────┘ + │ + ┌───────────────┼───────────────┐ + │ │ │ + ▼ ▼ ▼ + ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ + │ 功能A页 │ │ 功能B页 │ │ 设置页 │ + │ P-002 │ │ P-003 │ │ P-004 │ + └──────┬──────┘ └─────────────┘ └─────────────┘ + │ + ▼ + ┌─────────────┐ + │ 详情页 │ + │ P-005 │ + └─────────────┘ +``` + +## 2. 页面设计 + +### 2.1 P-001: {页面名称} + +**页面信息** + +| 属性 | 值 | +|------|-----| +| 页面ID | P-001 | +| 对应功能 | F-001, F-002 | +| 入口 | {从哪些页面可进入} | +| 出口 | {可跳转到哪些页面} | + +**【必须】页面布局 - ASCII 原型图** + +``` +┌────────────────────────────────────────────────────────┐ +│ ┌─────────────────────────────────────────────────┐ │ +│ │ Header │ │ +│ │ [Logo] [Nav Item] [Nav Item] [用户]│ │ +│ └─────────────────────────────────────────────────┘ │ +├────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────────┐ ┌───────────────────────────┐ │ +│ │ │ │ │ │ +│ │ Sidebar │ │ Main Content │ │ +│ │ │ │ │ │ +│ │ • Menu Item 1 │ │ ┌─────────────────────┐ │ │ +│ │ • Menu Item 2 │ │ │ Card 1 │ │ │ +│ │ • Menu Item 3 │ │ │ [Title] │ │ │ +│ │ │ │ │ [Description...] │ │ │ +│ │ │ │ │ [Action Button] │ │ │ +│ │ │ │ └─────────────────────┘ │ │ +│ │ │ │ │ │ +│ │ │ │ ┌─────────────────────┐ │ │ +│ │ │ │ │ Card 2 │ │ │ +│ │ │ │ └─────────────────────┘ │ │ +│ │ │ │ │ │ +│ └──────────────────┘ └───────────────────────────┘ │ +│ │ +├────────────────────────────────────────────────────────┤ +│ ┌─────────────────────────────────────────────────┐ │ +│ │ Footer │ │ +│ └─────────────────────────────────────────────────┘ │ +└────────────────────────────────────────────────────────┘ +``` + +**组件清单** + +| 组件ID | 组件名称 | 类型 | 说明 | 交互 | +|--------|----------|------|------|------| +| C-001 | Header | 导航栏 | 顶部固定 | 点击 Logo 回首页 | +| C-002 | Sidebar | 侧边栏 | 左侧固定 | 点击菜单切换内容 | +| C-003 | Card | 卡片 | 内容展示 | 点击进入详情 | + +**交互说明** + +| 触发 | 动作 | 结果 | +|------|------|------| +| 点击 Card | 跳转 | 进入详情页 P-005 | +| 点击 Menu Item | 切换 | 更新 Main Content | + +**页面状态** + +| 状态 | 说明 | 展示 | +|------|------|------| +| 默认 | 正常加载完成 | 显示数据列表 | +| 加载中 | 数据请求中 | 骨架屏/Loading | +| 空状态 | 无数据 | 空状态插图+引导文案 | +| 错误 | 请求失败 | 错误提示+重试按钮 | + +**空状态原型** + +``` +┌─────────────────────────────────────┐ +│ │ +│ ┌─────────────┐ │ +│ │ (空图标) │ │ +│ └─────────────┘ │ +│ │ +│ 暂无数据 │ +│ │ +│ [去添加数据] │ +│ │ +└─────────────────────────────────────┘ +``` + +{重复以上结构覆盖所有页面} + +## 3. 用户流程 + +### 3.1 {流程名称} + +**【必须】使用流程图展示用户操作流程:** + +``` +┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ +│ Step 1 │ ──▶ │ Step 2 │ ──▶ │ Step 3 │ ──▶ │ 完成 │ +│ {操作} │ │ {操作} │ │ {操作} │ │ │ +│ P-001 │ │ P-002 │ │ P-003 │ │ P-004 │ +└─────────┘ └────┬────┘ └─────────┘ └─────────┘ + │ + ▼ 取消 + ┌─────────┐ + │ 返回 │ + │ P-001 │ + └─────────┘ +``` + +**流程步骤** + +| 步骤 | 页面 | 用户操作 | 系统响应 | +|------|------|----------|----------| +| 1 | P-001 | {操作} | {响应} | +| 2 | P-002 | {操作} | {响应} | + +## 4. 组件规范 + +### 4.1 基础组件 + +**Button 按钮** + +``` +主按钮: ┌──────────────┐ + │ 确认提交 │ (填充色背景) + └──────────────┘ + +次按钮: ┌──────────────┐ + │ 取消 │ (边框样式) + └──────────────┘ + +禁用态: ┌──────────────┐ + │ 不可点击 │ (灰色) + └──────────────┘ +``` + +**Input 输入框** + +``` +默认态: ┌────────────────────────┐ + │ 请输入... │ + └────────────────────────┘ + +聚焦态: ┌────────────────────────┐ + │ 输入内容 │ (高亮边框) + └────────────────────────┘ + +错误态: ┌────────────────────────┐ + │ 错误输入 │ (红色边框) + └────────────────────────┘ + ⚠ 错误提示信息 +``` + +### 4.2 业务组件 + +{项目特有的业务组件} + +## 5. 设计规范 + +### 5.1 色彩规范 + +| 用途 | 色值 | 示例 | +|------|------|------| +| 主色 | #1890FF | 主按钮、链接 | +| 成功 | #52C41A | 成功提示 | +| 警告 | #FAAD14 | 警告提示 | +| 错误 | #FF4D4F | 错误提示 | +| 文字主色 | #262626 | 标题 | +| 文字次色 | #8C8C8C | 描述 | + +### 5.2 字体规范 + +| 用途 | 字号 | 字重 | +|------|------|------| +| 大标题 | 24px | Bold | +| 标题 | 18px | Medium | +| 正文 | 14px | Regular | +| 辅助文字 | 12px | Regular | + +### 5.3 间距规范 + +| 间距 | 值 | 用途 | +|------|-----|------| +| xs | 4px | 紧凑间距 | +| sm | 8px | 小间距 | +| md | 16px | 标准间距 | +| lg | 24px | 大间距 | +| xl | 32px | 特大间距 | + +### 5.4 响应式断点 + +| 断点 | 宽度 | 布局说明 | +|------|------|----------| +| Mobile | < 768px | 单栏布局 | +| Tablet | 768px - 1024px | 双栏布局 | +| Desktop | > 1024px | 多栏布局 | +``` + +## 4. 保存文档 + +将生成的 UIDesign 保存到 `doc/UIDesign.md`。 + +如果文件已存在,覆盖原文件(历史版本通过 git 追溯)。 + +## 5. 输出摘要 + +向用户展示生成摘要: + +- UIDesign 文件路径 +- 页面数量 +- 用户流程数量 +- 建议的下一步操作(运行 `/ru` 评审) + +--- + +## 可视化输出要求 + +UIDesign 作为「界面蓝图」文档,**必须大量使用 ASCII 原型图**: + +| 章节 | 可视化形式 | 必要性 | +|------|------------|--------| +| 1.3 页面导航图 | 导航关系图(ASCII) | **必须** | +| 2.x 页面布局 | **ASCII 原型图** | **必须(每页)** | +| 2.x 空状态 | ASCII 原型图 | 建议 | +| 3.x 用户流程 | 流程图(ASCII) | **必须** | +| 4.x 组件规范 | 组件示意图 | **必须** | + +**ASCII 原型图要求**: + +- 每个页面**必须**有完整的布局原型图 +- 原型图要体现:页面结构、组件位置、内容区域 +- 使用 `┌ ┐ └ ┘ ─ │ ├ ┤ ┬ ┴ ┼` 等字符绘制边框 +- 使用 `[ ]` 表示按钮 +- 使用 `▼ ▶ ◀ ▲` 表示方向/展开 +- 关键交互点要标注 + +## 注意事项 + +- UI 设计必须覆盖 DevelopmentPlan 所有功能模块 +- **每个页面必须有 ASCII 原型图** +- 页面设计要考虑各种状态(默认、加载、空、错误) +- 交互说明要清晰具体 +- 设计规范要统一一致 +- 页面 ID 格式:P-xxx +- 组件 ID 格式:C-xxx + +## 质量检查 + +生成 UIDesign 后,自查以下项目: + +- [ ] 覆盖 DevelopmentPlan 所有功能模块 +- [ ] **页面导航图清晰展示页面关系** +- [ ] **每个页面都有 ASCII 原型图** +- [ ] **原型图展示了完整的页面结构** +- [ ] **用户流程有流程图** +- [ ] 每个页面都有状态说明 +- [ ] 组件清单完整 +- [ ] 交互说明清晰 +- [ ] 设计规范统一 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..186d357 --- /dev/null +++ b/.gitignore @@ -0,0 +1,49 @@ +# Dependencies +node_modules/ +__pycache__/ +*.pyc +.pyo +*.pyd +venv/ +.venv/ +env/ +.env + +# Environment files (keep examples) +.env.local +backend/.env +frontend/.env.local + +# Build outputs +.next/ +out/ +dist/ +build/ +*.egg-info/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# Testing +.coverage +htmlcov/ +.pytest_cache/ +coverage/ + +# Logs +*.log +npm-debug.log* + +# OS +.DS_Store +Thumbs.db + +# Python +*.egg +*.egg-info/ +.eggs/ +pip-log.txt +pip-delete-this-directory.txt diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6b69e40 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,487 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## 交互规范 + +- 开始任务时说:**主人,开始了** +- 完成任务时说:**主人,我干完了.您看看** + +## 项目概述 + +KOL Insight 是一个 KOL(关键意见领袖)数据查询与分析工具,用于批量查询达人视频数据并计算预估成本指标。 + +**技术栈**: +- **前端**: Next.js 14.x (App Router) + React + TypeScript + Tailwind CSS +- **后端**: Python FastAPI 0.104+ + SQLAlchemy 2.0+ (异步 ORM) + asyncpg +- **数据库**: PostgreSQL 14.x+ +- **部署**: Docker + Uvicorn (ASGI 服务器) + +## 常用命令 + +### 前端开发 + +```bash +# 安装依赖 +pnpm install + +# 开发模式(热重载) +pnpm dev + +# 构建生产版本 +pnpm build + +# 生产模式运行 +pnpm start + +# 代码检查 +pnpm lint + +# 类型检查 +pnpm type-check # 如果配置了此脚本 +``` + +### 后端开发 + +```bash +# 安装依赖 +pip install -r requirements.txt +# 或使用 Poetry +poetry install + +# 开发模式运行(热重载) +uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 + +# 生产模式运行 +uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4 + +# 运行测试(TDD 必须) +pytest + +# 运行测试并生成覆盖率报告 +pytest --cov=app --cov-report=html + +# 运行特定测试文件 +pytest tests/test_query_service.py + +# 运行特定测试函数 +pytest tests/test_query_service.py::test_query_by_star_id +``` + +### 数据库操作 + +```bash +# 连接数据库(使用 .env 中的连接字符串) +psql "postgresql://user:password@host:5432/yuntu_kol" + +# 创建迁移 +alembic revision --autogenerate -m "description" + +# 执行迁移 +alembic upgrade head + +# 回滚迁移 +alembic downgrade -1 +``` + +### Docker 部署 + +```bash +# 构建并启动所有服务(前端、后端、数据库) +docker-compose up -d + +# 查看日志 +docker-compose logs -f + +# 停止所有服务 +docker-compose down + +# 重新构建并启动 +docker-compose up -d --build +``` + +## 架构设计 + +### 前后端分离架构 + +``` +┌─────────────────────┐ +│ Next.js 前端 │ 端口: 3000 +│ (纯前端渲染) │ +└──────────┬──────────┘ + │ HTTP API 调用 + ▼ +┌─────────────────────┐ +│ FastAPI 后端 │ 端口: 8000 +│ (异步 API) │ +└──────────┬──────────┘ + │ asyncpg + ▼ +┌─────────────────────┐ +│ PostgreSQL │ 端口: 5432 +└─────────────────────┘ +``` + +**前后端分离的关键点**: +- 前端通过 HTTP 调用后端 API(需要配置 CORS) +- 前端环境变量: `NEXT_PUBLIC_API_URL` 指向后端地址 +- 后端环境变量: `CORS_ORIGINS` 配置允许的前端域名 +- 独立部署:前端可部署到 Vercel,后端部署到 Docker + +### 核心模块 + +1. **查询模块** (`backend/app/services/query_service.py`) + - 支持三种查询方式:星图ID(star_id)、达人ID(star_unique_id)、昵称(star_nickname) + - 星图ID 和达人ID 使用精准匹配(WHERE IN) + - 昵称使用模糊匹配(WHERE LIKE '%昵称%') + - 单次查询限制最大 1000 条 + +2. **计算模块** (`backend/app/services/calculator.py`) + - **预估自然CPM**: `(estimated_video_cost / natural_play_cnt) * 1000` + - **预估自然看后搜人数**: `(natural_play_cnt / total_play_cnt) * after_view_search_uv` + - **预估自然看后搜人数成本**: `estimated_video_cost / 预估自然看后搜人数` + - 除零检查:分母为 0 时返回 `None` + - 结果保留 2 位小数:使用 `round(value, 2)` + +3. **品牌API集成模块** (`backend/app/services/brand_api.py`) + - **批量并发调用**:从查询结果提取唯一 `brand_id`,批量调用品牌API + - **并发控制**:使用 `asyncio.Semaphore` 限制最大 10 个并发请求 + - **超时设置**:单个请求超时 3 秒 + - **降级策略**:API 调用失败时显示原始 `brand_id` + - **技术实现**:使用 `httpx.AsyncClient` + `asyncio.gather` + +4. **导出模块** (`backend/app/services/export_service.py`) + - 支持 Excel (使用 `openpyxl` 或 `xlsxwriter`) 和 CSV 格式 + - 使用中文列名作为表头 + - 限制单次导出最大 1000 条 + +### 数据流向 + +``` +用户输入查询条件 (前端) + ↓ +POST /api/v1/query (后端) + ↓ +查询数据库 (SQLAlchemy 异步) + ↓ +提取唯一 brand_id → 批量调用品牌API (httpx 异步,并发10) + ↓ +填充品牌名称 → 计算预估指标 + ↓ +返回完整数据 (JSON) + ↓ +前端展示结果表格 + ↓ +用户点击导出 → GET /api/v1/export → 下载 Excel/CSV +``` + +## 数据库设计 + +### KolVideo 模型 + +```python +class KolVideo(Base): + __tablename__ = "kol_videos" + + # 主键 + item_id = Column(String, primary_key=True) + + # 查询字段(必须建立索引) + star_id = Column(String, nullable=False, index=True) # 星图ID + star_unique_id = Column(String, nullable=False, index=True) # 达人unique_id + star_nickname = Column(String, nullable=False, index=True) # 达人昵称 + + # 基础信息 + title = Column(String, nullable=True) + viral_type = Column(String, nullable=True) + video_url = Column(String, nullable=True) + publish_time = Column(DateTime, nullable=True) + + # 曝光指标(用于计算) + natural_play_cnt = Column(Integer, default=0) # 自然播放量 + heated_play_cnt = Column(Integer, default=0) # 加热播放量 + total_play_cnt = Column(Integer, default=0) # 总播放量 + + # 互动指标 + total_interact = Column(Integer, default=0) + like_cnt = Column(Integer, default=0) + share_cnt = Column(Integer, default=0) + comment_cnt = Column(Integer, default=0) + + # 效果指标(用于计算) + new_a3_rate = Column(Float, nullable=True) + after_view_search_uv = Column(Integer, default=0) # 看后搜人数 + return_search_cnt = Column(Integer, default=0) + + # 商业信息 + industry_id = Column(String, nullable=True) + industry_name = Column(String, nullable=True) + brand_id = Column(String, nullable=True) # 用于调用品牌API + estimated_video_cost = Column(Float, default=0) # 预估视频成本(用于计算) +``` + +**索引策略**: +- `idx_star_id`: 星图ID 精准查询 +- `idx_star_unique_id`: 达人ID 精准查询 +- `idx_star_nickname`: 昵称模糊查询(需要支持 LIKE) + +## API 设计 + +### POST /api/v1/query + +批量查询 KOL 视频数据。 + +**请求体**: +```python +{ + "type": "star_id" | "unique_id" | "nickname", # 查询方式 + "values": ["id1", "id2", ...] # 批量ID或单个昵称 +} +``` + +**响应体**: +```python +{ + "success": true, + "data": [ + { + "item_id": "...", + "title": "...", + # ... 26个字段 + "brand_name": "...", # 后端填充 + "estimated_natural_cpm": 12.34, # 后端计算 + # ... + } + ], + "total": 100 +} +``` + +### GET /api/v1/export + +导出查询结果。 + +**查询参数**: +- `format`: `xlsx` 或 `csv` + +**响应**: 文件下载(`Content-Disposition: attachment`) + +## 开发规范 + +### TDD(测试驱动开发)要求 + +**这个项目强制使用 TDD,必须先写测试再写实现代码。** + +1. **单元测试覆盖率要求**: 100%(所有分支覆盖) +2. **集成测试要求**: 使用真实数据库连接(.env 中的连接字符串) +3. **测试框架**: pytest + pytest-cov +4. **测试文件位置**: `backend/tests/` + +**TDD 流程**: +``` +1. 编写测试用例 (tests/test_*.py) +2. 运行测试(应该失败) +3. 编写最小实现代码 +4. 运行测试(应该通过) +5. 重构代码(保持测试通过) +6. 生成覆盖率报告验证 100% 覆盖 +``` + +**测试示例**: +```python +# tests/test_calculator.py +def test_calculate_natural_cpm(): + # 正常情况 + result = calculate_natural_cpm(1000, 50000) + assert result == 20.0 + + # 除零情况 + result = calculate_natural_cpm(1000, 0) + assert result is None + +# tests/test_query_service.py +@pytest.mark.asyncio +async def test_query_by_star_id(db_session): + # 使用真实数据库连接 + result = await query_videos( + db_session, + query_type="star_id", + values=["test_id_1", "test_id_2"] + ) + assert len(result) > 0 +``` + +### 异步编程规范 + +**后端必须使用异步编程以提升性能。** + +1. **数据库操作**: 使用 SQLAlchemy 异步 API + ```python + from sqlalchemy.ext.asyncio import AsyncSession + + async def query_videos(session: AsyncSession, ...): + stmt = select(KolVideo).where(...) + result = await session.execute(stmt) + return result.scalars().all() + ``` + +2. **外部API调用**: 使用 httpx.AsyncClient + ```python + async with httpx.AsyncClient(timeout=3.0) as client: + response = await client.get(url) + ``` + +3. **并发控制**: 使用 asyncio.Semaphore + ```python + semaphore = asyncio.Semaphore(10) # 限制10并发 + async with semaphore: + # 执行异步任务 + ``` + +### 前端实现规范 + +**前端采用"粗略实现"策略:重点在功能可用,样式可简化。** + +1. **组件化开发**: 使用 React 组件(QueryForm, ResultTable, ExportButton) +2. **API 调用封装**: 在 `lib/api.ts` 中统一封装 +3. **环境变量**: 使用 `NEXT_PUBLIC_API_URL` 配置后端地址 +4. **状态管理**: 页面状态包括:默认态、输入态、查询中、结果态、空结果态、错误态 + +### 错误处理规范 + +1. **后端**: + - 所有 API 路由使用 try-except 包裹 + - 数据库连接失败、品牌API超时等场景有降级处理 + - 错误信息记录到日志 + +2. **前端**: + - 网络错误显示用户友好提示 + - 空结果显示引导文案 + +### 性能要求 + +- 查询响应时间 ≤ 3 秒(100 条数据) +- 页面加载时间 ≤ 2 秒 +- 导出响应时间 ≤ 5 秒(1000 条数据) +- 品牌 API 并发限制: 10 个请求,单请求超时 3 秒 + +## 环境变量配置 + +### 前端 (.env.local) + +```env +NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1 +``` + +### 后端 (.env) + +```env +DATABASE_URL=postgresql://user:password@host:5432/yuntu_kol +CORS_ORIGINS=http://localhost:3000,https://your-frontend-domain.com +BRAND_API_BASE_URL=https://api.internal.intelligrow.cn +``` + +## 目录结构 + +``` +kol-insight/ +├── frontend/ # 前端项目(Next.js) +│ ├── src/ +│ │ ├── app/ # App Router 路由 +│ │ ├── components/ # React 组件 +│ │ ├── lib/ # API 调用、工具函数 +│ │ └── types/ # TypeScript 类型定义 +│ └── package.json +│ +├── backend/ # 后端项目(FastAPI) +│ ├── app/ +│ │ ├── main.py # FastAPI 应用入口 +│ │ ├── config.py # 配置管理 +│ │ ├── database.py # 数据库连接 +│ │ ├── models/ # SQLAlchemy 模型 +│ │ ├── schemas/ # Pydantic 请求/响应模型 +│ │ ├── api/v1/ # API 路由 +│ │ └── services/ # 业务逻辑 +│ ├── tests/ # 测试文件(TDD 必须) +│ └── requirements.txt +│ +├── doc/ # 项目文档 +│ ├── PRD.md # 产品需求文档 +│ ├── FeatureSummary.md # 功能摘要 +│ ├── UIDesign.md # UI 设计 +│ ├── DevelopmentPlan.md # 开发计划 +│ └── tasks.md # 任务列表 +│ +├── docker-compose.yml # Docker 编排 +└── README.md +``` + +## 关键技术决策 + +### 为什么使用 FastAPI? + +- 原生支持异步编程(async/await) +- 自动生成 OpenAPI 文档(Swagger UI) +- Pydantic 类型验证,类型安全 +- 性能优异(基于 Starlette 和 Pydantic) + +### 为什么使用前后端分离? + +- 前端可独立部署到 Vercel 等平台 +- 后端可独立扩展和优化 +- 职责分离:前端专注 UI,后端专注业务逻辑 +- 便于团队协作(前端/后端可并行开发) + +### 为什么强制 TDD + 100% 覆盖率? + +- 保证代码质量和可维护性 +- 避免回归问题 +- 文档化代码行为(测试即文档) +- 便于重构(测试作为安全网) + +### 为什么品牌API在后端调用? + +- 避免前端暴露内部API地址 +- 统一错误处理和降级逻辑 +- 利用后端异步能力优化并发性能 +- 减少前端复杂度 + +## 常见问题 + +### Q: 如何运行单个测试? +```bash +pytest tests/test_calculator.py::test_calculate_natural_cpm -v +``` + +### Q: 如何查看测试覆盖率? +```bash +pytest --cov=app --cov-report=html +# 打开 htmlcov/index.html 查看详细报告 +``` + +### Q: 前端如何调用后端API? +在 `lib/api.ts` 中封装: +```typescript +const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api/v1'; + +export async function queryVideos(request: QueryRequest): Promise { + const response = await fetch(`${API_BASE_URL}/query`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(request), + }); + return response.json(); +} +``` + +### Q: 如何调试品牌API批量调用? +1. 检查后端日志(应该记录API调用状态) +2. 使用断点调试 `services/brand_api.py` +3. 验证并发控制和超时设置是否生效 + +### Q: 数据库索引如何验证? +```sql +-- 连接数据库后执行 +\d kol_videos +-- 应该看到 idx_star_id, idx_star_unique_id, idx_star_nickname +``` diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..98541c9 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,8 @@ +# 数据库连接 +DATABASE_URL=postgresql+asyncpg://user:password@localhost:5432/yuntu_kol + +# CORS 允许的前端地址 +CORS_ORIGINS=["http://localhost:3000"] + +# 品牌 API 配置 +BRAND_API_BASE_URL=https://api.internal.intelligrow.cn diff --git a/backend/alembic.ini b/backend/alembic.ini new file mode 100644 index 0000000..cd64a16 --- /dev/null +++ b/backend/alembic.ini @@ -0,0 +1,42 @@ +[alembic] +script_location = alembic +prepend_sys_path = . +version_path_separator = os + +sqlalchemy.url = driver://user:pass@localhost/dbname + +[post_write_hooks] + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/backend/alembic/env.py b/backend/alembic/env.py new file mode 100644 index 0000000..d6e14d7 --- /dev/null +++ b/backend/alembic/env.py @@ -0,0 +1,71 @@ +import asyncio +from logging.config import fileConfig + +from sqlalchemy import pool +from sqlalchemy.engine import Connection +from sqlalchemy.ext.asyncio import async_engine_from_config + +from alembic import context + +from app.config import settings +from app.database import Base +from app.models import KolVideo # noqa: F401 + +config = context.config + +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +target_metadata = Base.metadata + + +def get_url(): + return settings.DATABASE_URL + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode.""" + url = get_url() + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def do_run_migrations(connection: Connection) -> None: + context.configure(connection=connection, target_metadata=target_metadata) + + with context.begin_transaction(): + context.run_migrations() + + +async def run_async_migrations() -> None: + """Run migrations in 'online' mode with async engine.""" + configuration = config.get_section(config.config_ini_section) + configuration["sqlalchemy.url"] = get_url() + connectable = async_engine_from_config( + configuration, + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + async with connectable.connect() as connection: + await connection.run_sync(do_run_migrations) + + await connectable.dispose() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode.""" + asyncio.run(run_async_migrations()) + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/backend/alembic/script.py.mako b/backend/alembic/script.py.mako new file mode 100644 index 0000000..fbc4b07 --- /dev/null +++ b/backend/alembic/script.py.mako @@ -0,0 +1,26 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/v1/__init__.py b/backend/app/api/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/config.py b/backend/app/config.py new file mode 100644 index 0000000..de23107 --- /dev/null +++ b/backend/app/config.py @@ -0,0 +1,28 @@ +from pydantic_settings import BaseSettings, SettingsConfigDict +from typing import List + + +class Settings(BaseSettings): + """Application settings.""" + + model_config = SettingsConfigDict( + env_file=".env", + env_file_encoding="utf-8", + ) + + # Database + DATABASE_URL: str = "postgresql+asyncpg://user:password@localhost:5432/yuntu_kol" + + # CORS + CORS_ORIGINS: List[str] = ["http://localhost:3000"] + + # Brand API + BRAND_API_BASE_URL: str = "https://api.internal.intelligrow.cn" + + # API Settings + MAX_QUERY_LIMIT: int = 1000 + BRAND_API_TIMEOUT: float = 3.0 + BRAND_API_CONCURRENCY: int = 10 + + +settings = Settings() diff --git a/backend/app/core/__init__.py b/backend/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/database.py b/backend/app/database.py new file mode 100644 index 0000000..5971c19 --- /dev/null +++ b/backend/app/database.py @@ -0,0 +1,34 @@ +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker +from sqlalchemy.orm import DeclarativeBase + +from app.config import settings + +# 创建异步引擎 +engine = create_async_engine( + settings.DATABASE_URL, + echo=False, + pool_pre_ping=True, + pool_size=10, + max_overflow=20, +) + +# 创建异步会话工厂 +async_session_maker = async_sessionmaker( + engine, + class_=AsyncSession, + expire_on_commit=False, +) + + +class Base(DeclarativeBase): + """Base class for all models.""" + pass + + +async def get_db() -> AsyncSession: + """Dependency to get database session.""" + async with async_session_maker() as session: + try: + yield session + finally: + await session.close() diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..ed49c35 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,31 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from app.config import settings + +app = FastAPI( + title="KOL Insight API", + description="KOL 视频数据查询与分析 API", + version="1.0.0", +) + +# CORS 配置 +app.add_middleware( + CORSMiddleware, + allow_origins=settings.CORS_ORIGINS, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +@app.get("/") +async def root(): + """Root endpoint.""" + return {"message": "KOL Insight API", "version": "1.0.0"} + + +@app.get("/health") +async def health(): + """Health check endpoint.""" + return {"status": "healthy"} diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py new file mode 100644 index 0000000..bb06d24 --- /dev/null +++ b/backend/app/models/__init__.py @@ -0,0 +1,3 @@ +from app.models.kol_video import KolVideo + +__all__ = ["KolVideo"] diff --git a/backend/app/models/kol_video.py b/backend/app/models/kol_video.py new file mode 100644 index 0000000..8171b25 --- /dev/null +++ b/backend/app/models/kol_video.py @@ -0,0 +1,52 @@ +from sqlalchemy import Column, String, Integer, Float, DateTime, Index +from app.database import Base + + +class KolVideo(Base): + """KOL 视频数据模型.""" + + __tablename__ = "kol_videos" + + # 主键 + item_id = Column(String, primary_key=True) + + # 基础信息 + title = Column(String, nullable=True) + viral_type = Column(String, nullable=True) + video_url = Column(String, nullable=True) + star_id = Column(String, nullable=False) + star_unique_id = Column(String, nullable=False) + star_nickname = Column(String, nullable=False) + publish_time = Column(DateTime, nullable=True) + + # 曝光指标 + natural_play_cnt = Column(Integer, default=0) + heated_play_cnt = Column(Integer, default=0) + total_play_cnt = Column(Integer, default=0) + + # 互动指标 + total_interact = Column(Integer, default=0) + like_cnt = Column(Integer, default=0) + share_cnt = Column(Integer, default=0) + comment_cnt = Column(Integer, default=0) + + # 效果指标 + new_a3_rate = Column(Float, nullable=True) + after_view_search_uv = Column(Integer, default=0) + return_search_cnt = Column(Integer, default=0) + + # 商业信息 + industry_id = Column(String, nullable=True) + industry_name = Column(String, nullable=True) + brand_id = Column(String, nullable=True) + estimated_video_cost = Column(Float, default=0) + + # 索引定义 + __table_args__ = ( + Index("idx_star_id", "star_id"), + Index("idx_star_unique_id", "star_unique_id"), + Index("idx_star_nickname", "star_nickname"), + ) + + def __repr__(self): + return f"" diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/services/__init__.py b/backend/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/pytest.ini b/backend/pytest.ini new file mode 100644 index 0000000..2f3d987 --- /dev/null +++ b/backend/pytest.ini @@ -0,0 +1,5 @@ +[pytest] +testpaths = tests +asyncio_mode = auto +asyncio_default_fixture_loop_scope = function +addopts = -v --cov=app --cov-report=html --cov-report=term-missing diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..754081e --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,27 @@ +# Web Framework +fastapi>=0.104.0 +uvicorn[standard]>=0.24.0 + +# Database +sqlalchemy>=2.0.0 +asyncpg>=0.29.0 +alembic>=1.12.0 + +# HTTP Client +httpx>=0.25.0 + +# Data Validation +pydantic>=2.0.0 +pydantic-settings>=2.0.0 + +# Excel Export +openpyxl>=3.1.0 + +# Testing +pytest>=7.4.0 +pytest-asyncio>=0.21.0 +pytest-cov>=4.1.0 +httpx>=0.25.0 + +# Development +python-dotenv>=1.0.0 diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py new file mode 100644 index 0000000..f589102 --- /dev/null +++ b/backend/tests/conftest.py @@ -0,0 +1,58 @@ +import pytest +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker + +from app.database import Base +from app.models import KolVideo + + +@pytest.fixture +def sample_video_data(): + """Sample video data for testing.""" + return { + "item_id": "test_item_001", + "title": "测试视频标题", + "viral_type": "爆款", + "video_url": "https://example.com/video/001", + "star_id": "star_001", + "star_unique_id": "unique_001", + "star_nickname": "测试达人", + "natural_play_cnt": 100000, + "heated_play_cnt": 50000, + "total_play_cnt": 150000, + "total_interact": 5000, + "like_cnt": 3000, + "share_cnt": 1000, + "comment_cnt": 1000, + "new_a3_rate": 0.05, + "after_view_search_uv": 500, + "return_search_cnt": 200, + "industry_id": "ind_001", + "industry_name": "美妆", + "brand_id": "brand_001", + "estimated_video_cost": 10000.0, + } + + +@pytest.fixture +async def test_engine(): + """Create a test database engine using SQLite.""" + engine = create_async_engine( + "sqlite+aiosqlite:///:memory:", + echo=False, + ) + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + yield engine + await engine.dispose() + + +@pytest.fixture +async def test_session(test_engine): + """Create a test database session.""" + async_session = async_sessionmaker( + test_engine, + class_=AsyncSession, + expire_on_commit=False, + ) + async with async_session() as session: + yield session diff --git a/backend/tests/test_database.py b/backend/tests/test_database.py new file mode 100644 index 0000000..877f12a --- /dev/null +++ b/backend/tests/test_database.py @@ -0,0 +1,165 @@ +import pytest +from sqlalchemy import select + +from app.models import KolVideo + + +class TestKolVideoModel: + """Tests for KolVideo model.""" + + async def test_create_video(self, test_session, sample_video_data): + """Test creating a video record.""" + video = KolVideo(**sample_video_data) + test_session.add(video) + await test_session.commit() + + result = await test_session.execute( + select(KolVideo).where(KolVideo.item_id == sample_video_data["item_id"]) + ) + saved_video = result.scalar_one() + + assert saved_video.item_id == sample_video_data["item_id"] + assert saved_video.title == sample_video_data["title"] + assert saved_video.star_id == sample_video_data["star_id"] + + async def test_query_by_star_id(self, test_session, sample_video_data): + """Test querying videos by star_id.""" + video = KolVideo(**sample_video_data) + test_session.add(video) + await test_session.commit() + + result = await test_session.execute( + select(KolVideo).where(KolVideo.star_id == sample_video_data["star_id"]) + ) + videos = result.scalars().all() + + assert len(videos) == 1 + assert videos[0].star_id == sample_video_data["star_id"] + + async def test_query_by_star_unique_id(self, test_session, sample_video_data): + """Test querying videos by star_unique_id.""" + video = KolVideo(**sample_video_data) + test_session.add(video) + await test_session.commit() + + result = await test_session.execute( + select(KolVideo).where( + KolVideo.star_unique_id == sample_video_data["star_unique_id"] + ) + ) + videos = result.scalars().all() + + assert len(videos) == 1 + assert videos[0].star_unique_id == sample_video_data["star_unique_id"] + + async def test_query_by_nickname_like(self, test_session, sample_video_data): + """Test querying videos by nickname using LIKE.""" + video = KolVideo(**sample_video_data) + test_session.add(video) + await test_session.commit() + + result = await test_session.execute( + select(KolVideo).where(KolVideo.star_nickname.like("%测试%")) + ) + videos = result.scalars().all() + + assert len(videos) == 1 + assert "测试" in videos[0].star_nickname + + async def test_batch_query_by_star_ids(self, test_session, sample_video_data): + """Test batch querying videos by multiple star_ids.""" + video1 = KolVideo(**sample_video_data) + video2_data = sample_video_data.copy() + video2_data["item_id"] = "test_item_002" + video2_data["star_id"] = "star_002" + video2 = KolVideo(**video2_data) + + test_session.add_all([video1, video2]) + await test_session.commit() + + star_ids = ["star_001", "star_002"] + result = await test_session.execute( + select(KolVideo).where(KolVideo.star_id.in_(star_ids)) + ) + videos = result.scalars().all() + + assert len(videos) == 2 + + async def test_video_default_values(self, test_session): + """Test that default values are set correctly.""" + video = KolVideo( + item_id="test_defaults", + star_id="star_test", + star_unique_id="unique_test", + star_nickname="测试默认值", + ) + test_session.add(video) + await test_session.commit() + + result = await test_session.execute( + select(KolVideo).where(KolVideo.item_id == "test_defaults") + ) + saved_video = result.scalar_one() + + assert saved_video.natural_play_cnt == 0 + assert saved_video.heated_play_cnt == 0 + assert saved_video.total_play_cnt == 0 + assert saved_video.estimated_video_cost == 0 + + async def test_video_nullable_fields(self, test_session): + """Test that nullable fields can be None.""" + video = KolVideo( + item_id="test_nullable", + star_id="star_nullable", + star_unique_id="unique_nullable", + star_nickname="测试可空字段", + ) + test_session.add(video) + await test_session.commit() + + result = await test_session.execute( + select(KolVideo).where(KolVideo.item_id == "test_nullable") + ) + saved_video = result.scalar_one() + + assert saved_video.title is None + assert saved_video.video_url is None + assert saved_video.brand_id is None + assert saved_video.new_a3_rate is None + + async def test_update_video(self, test_session, sample_video_data): + """Test updating a video record.""" + video = KolVideo(**sample_video_data) + test_session.add(video) + await test_session.commit() + + result = await test_session.execute( + select(KolVideo).where(KolVideo.item_id == sample_video_data["item_id"]) + ) + saved_video = result.scalar_one() + saved_video.title = "更新后的标题" + await test_session.commit() + + result = await test_session.execute( + select(KolVideo).where(KolVideo.item_id == sample_video_data["item_id"]) + ) + updated_video = result.scalar_one() + assert updated_video.title == "更新后的标题" + + async def test_delete_video(self, test_session, sample_video_data): + """Test deleting a video record.""" + video = KolVideo(**sample_video_data) + test_session.add(video) + await test_session.commit() + + result = await test_session.execute( + select(KolVideo).where(KolVideo.item_id == sample_video_data["item_id"]) + ) + saved_video = result.scalar_one() + await test_session.delete(saved_video) + await test_session.commit() + + result = await test_session.execute( + select(KolVideo).where(KolVideo.item_id == sample_video_data["item_id"]) + ) + assert result.scalar_one_or_none() is None diff --git a/doc/DevelopmentPlan.md b/doc/DevelopmentPlan.md new file mode 100644 index 0000000..9134602 --- /dev/null +++ b/doc/DevelopmentPlan.md @@ -0,0 +1,1014 @@ +# KOL Insight - 开发计划 + +## 文档信息 + +| 项目 | 内容 | +|------|------| +| 版本 | v1.0 | +| 创建日期 | 2025-01-28 | +| 来源文档 | FeatureSummary.md | + +## 1. 项目概述 + +### 1.1 项目目标 + +| 目标 | 指标 | 衡量方式 | +|------|------|----------| +| 提升查询效率 | 单次可批量查询多个 KOL | 对比手动查询耗时 | +| 降低计算错误 | 自动计算预估指标准确率 100% | 人工抽检验证 | +| 提高数据可用性 | 支持数据导出 | 导出功能完整性 | + +### 1.2 技术栈 + + +| 层级 | 技术选型 | 版本 | 说明 | +|------|----------|------|------| +| 前端框架 | Next.js (App Router) | 14.x | React 框架,纯前端渲染 | +| UI 框架 | React + Tailwind CSS | - | 组件化开发,响应式设计 | +| 后端框架 | Python FastAPI | 0.104+ | 高性能异步 Web 框架 | +| 数据库 | PostgreSQL | 14.x+ | 关系型数据库,存储 KOL 视频数据 | +| Python ORM | SQLAlchemy + asyncpg | 2.0+ | 异步 ORM,类型安全的数据库访问 | +| API 文档 | FastAPI 自动生成 | - | Swagger UI + ReDoc | +| 前端部署 | Docker / Vercel | - | 容器化或 Serverless | +| 后端部署 | Docker + Uvicorn | - | ASGI 服务器 | +| 包管理(前端) | pnpm | 8.x | 高效的包管理器 | +| 包管理(后端) | Poetry / pip | - | Python 依赖管理 | + +### 1.3 开发原则 + +- **简单优先**: 优先选择简单直接的实现方案 +- **类型安全**: 前端使用 TypeScript,后端使用 Pydantic 类型验证 +- **组件化**: UI 组件化开发,便于复用和维护 +- **API 设计**: RESTful 风格,清晰的接口契约 +- **错误处理**: 完善的错误处理和用户提示 +- **安全防护**: SQL 注入防护,CORS 配置,环境变量管理敏感配置 + +- **前后端分离**: 前端专注 UI 展示,后端专注业务逻辑和数据处理 +- **异步优先**: 后端使用异步编程,提升并发性能 + + +## 2. 技术架构 + +### 2.1 系统架构图 + + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 客户端层 │ +│ ┌───────────────────────────────────────────────────────────────────┐ │ +│ │ Web Browser │ │ +│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ +│ │ │ 查询页面 │ │ 结果展示 │ │ 导出操作 │ │ │ +│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ +│ └───────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + │ HTTP/HTTPS (API 调用) + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ Next.js 前端应用层 │ +│ ┌───────────────────────────────────────────────────────────────────┐ │ +│ │ App Router (纯前端) │ │ +│ │ ┌─────────────────────────────────────────────────────────────┐ │ │ +│ │ │ React 组件 │ │ │ +│ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ +│ │ │ │ QueryForm │ │ ResultTable │ │ ExportButton │ │ │ │ +│ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ +│ │ └─────────────────────────────────────────────────────────────┘ │ │ +│ └───────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + │ HTTP API 调用 (跨域) + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ FastAPI 后端应用层 │ +│ ┌───────────────────────────────────────────────────────────────────┐ │ +│ │ API Router │ │ +│ │ ┌─────────────────────────────────────────────────────────────┐ │ │ +│ │ │ RESTful API │ │ │ +│ │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ +│ │ │ │ POST /api/v1/ │ │ GET /api/v1/ │ │ │ │ +│ │ │ │ query │ │ export │ │ │ │ +│ │ │ └─────────────────┘ └─────────────────┘ │ │ │ +│ │ └─────────────────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ ┌─────────────────────────────────────────────────────────────┐ │ │ +│ │ │ 业务逻辑层 (Python) │ │ │ +│ │ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │ │ +│ │ │ │ 查询服务 │ │ 计算服务 │ │ 导出服务 │ │ │ │ +│ │ │ └───────────┘ └───────────┘ └───────────┘ │ │ │ +│ │ │ ┌───────────┐ ┌───────────┐ │ │ │ +│ │ │ │ 品牌API │ │ 数据库层 │ │ │ │ +│ │ │ │ 集成服务 │ │ (SQLAlchemy)│ │ │ │ +│ │ │ └───────────┘ └───────────┘ │ │ │ +│ │ └─────────────────────────────────────────────────────────────┘ │ │ +│ └───────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ + │ │ + │ SQL (asyncpg) │ HTTP + ▼ ▼ +┌─────────────────────────────┐ ┌─────────────────────────────┐ +│ 数据层 │ │ 外部服务 │ +│ ┌───────────────────────┐ │ │ ┌───────────────────────┐ │ +│ │ PostgreSQL │ │ │ │ 品牌 API │ │ +│ │ ┌─────────────────┐ │ │ │ │ /v1/yuntu/brands/ │ │ +│ │ │ kol_videos │ │ │ │ └───────────────────────┘ │ +│ │ │ (视频数据表) │ │ │ └─────────────────────────────┘ +│ │ └─────────────────┘ │ │ +│ └───────────────────────┘ │ +└─────────────────────────────┘ +``` + +### 2.2 模块依赖图 + + +``` +┌────────────────────────────────────────────────────────────────┐ +│ 前端模块 (Next.js) │ +│ ┌──────────────┐ │ +│ │ 页面组件 │ │ +│ │ (Page) │ │ +│ └──────┬───────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ 查询表单 │ ──▶ │ 结果表格 │ ──▶ │ 导出按钮 │ │ +│ │ 组件 │ │ 组件 │ │ 组件 │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└────────────────────────────────────────────────────────────────┘ + │ │ │ + │ HTTP API │ 数据展示 │ HTTP API + ▼ ▼ ▼ +┌────────────────────────────────────────────────────────────────┐ +│ 后端模块 (FastAPI) │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ 查询 API │ ──▶ │ 计算服务 │ │ 导出 API │ │ +│ │ POST /api/v1/│ │ (Python) │ │ GET /api/v1/ │ │ +│ │ query │ │ │ │ export │ │ +│ └──────┬───────┘ └──────────────┘ └──────┬───────┘ │ +│ │ ▲ │ │ +│ ▼ │ │ │ +│ ┌──────────────┐ │ │ │ +│ │ 品牌API服务 │────────────┘ │ │ +│ │ (F-010) │ │ │ +│ │ (httpx异步) │ │ │ +│ └──────┬───────┘ │ │ +│ │ │ │ +│ └─────────────────┬───────────────────────┘ │ +│ ▼ │ +│ ┌──────────────┐ │ +│ │ 数据库服务 │ │ +│ │ (SQLAlchemy) │ │ +│ └──────┬───────┘ │ +└────────────────────────────┼───────────────────────────────────┘ + │ + ┌───────────────────┼───────────────────┐ + ▼ ▼ + ┌──────────────┐ ┌──────────────┐ + │ PostgreSQL │ │ 品牌 API │ + │ │ │ (外部服务) │ + └──────────────┘ └──────────────┘ +``` + +### 2.3 数据流图 + + +``` +用户操作 前端 后端 数据库/外部API + │ │ │ │ + │ 1. 输入查询条件 │ │ │ + │ ─────────────────────▶ │ │ │ + │ │ │ │ + │ │ 2. POST /api/query │ │ + │ │ ─────────────────────▶ │ │ + │ │ │ │ + │ │ │ 3. SELECT FROM kol_videos + │ │ │ ─────────────────────────▶│ + │ │ │ │ + │ │ │ 4. 返回视频数据 │ + │ │ │ ◀─────────────────────────│ + │ │ │ │ + │ │ │ 5. 提取唯一 brand_id │ + │ │ │ (去重处理) │ + │ │ │ │ + │ │ │ 6. 批量 GET brands │ + │ │ │ (并发10,超时3s) │ + │ │ │ ─────────────────────────▶│ + │ │ │ │ + │ │ │ 7. 返回品牌名称映射 │ + │ │ │ ◀─────────────────────────│ + │ │ │ │ + │ │ │ 8. 填充品牌名称 │ + │ │ │ (失败则降级显示ID) │ + │ │ │ │ + │ │ │ 9. 计算预估指标 │ + │ │ │ (CPM/看后搜人数/成本) │ + │ │ │ │ + │ │ 10. 返回完整数据 │ │ + │ │ ◀───────────────────── │ │ + │ │ │ │ + │ 11. 展示结果列表 │ │ │ + │ ◀───────────────────── │ │ │ + │ │ │ │ + │ 12. 点击导出 │ │ │ + │ ─────────────────────▶ │ │ │ + │ │ 13. GET /api/export │ │ + │ │ ─────────────────────▶ │ │ + │ │ │ │ + │ │ 14. 返回 Excel/CSV │ │ + │ │ ◀───────────────────── │ │ + │ │ │ │ + │ 15. 下载文件 │ │ │ + │ ◀───────────────────── │ │ │ +``` + +## 3. 开发阶段 + +### 3.1 阶段时间线 + +``` + Phase 1 Phase 2 Phase 3 + │ │ │ + 基础架构搭建 核心功能开发 优化与测试 + │ │ │ + ▼ ▼ ▼ + ┌─────────┐ ┌─────────┐ ┌─────────┐ + │ 项目初始 │ ──────▶ │ 功能实现 │ ──────▶ │ 优化部署 │ + │ 化配置 │ │ 与集成 │ │ 与测试 │ + └─────────┘ └─────────┘ └─────────┘ + + 交付物: 交付物: 交付物: + • 项目骨架 • 查询功能 • 性能优化 + • 数据库连接 • 计算逻辑 • 错误处理 + • 基础 UI • 导出功能 • 部署配置 +``` + +### 3.2 Phase 1: 基础架构搭建 + +**目标**: 完成项目初始化和基础设施配置 + + +| 任务ID | 任务 | 描述 | 依赖 | 优先级 | 关联功能 | +|--------|------|------|------|--------|----------| +| T-001 | 前端项目初始化 | 创建 Next.js 项目,配置 TypeScript、ESLint、Prettier | - | P0 | - | +| T-002 | 后端项目初始化 | 创建 FastAPI 项目,配置 Poetry/pip、项目结构 | - | P0 | - | +| T-003 | 数据库配置 | 配置 SQLAlchemy + asyncpg,定义数据模型,连接 PostgreSQL | T-002 | P0 | F-001~003 | +| T-004 | 基础 UI 框架 | 安装 Tailwind CSS,创建基础布局组件 | T-001 | P0 | F-007 | +| T-005 | 环境变量配置 | 配置前后端环境变量,数据库连接字符串,CORS 配置 | T-001, T-002 | P0 | - | + +**阶段依赖图:** + + +``` +T-001 (前端初始化) T-002 (后端初始化) + │ │ + ▼ ▼ +T-004 (UI框架) T-003 (数据库) + │ │ + └──────────┬───────────┘ + ▼ + T-005 (环境变量) +``` + +--- + +### 3.3 Phase 2: 核心功能开发 + +**目标**: 实现所有核心功能(查询、计算、展示、导出) + + +| 任务ID | 任务 | 描述 | 依赖 | 优先级 | 关联功能 | +|--------|------|------|------|--------|----------| +| T-006 | 查询 API 开发 (后端) | FastAPI 实现 POST /api/v1/query 接口,支持三种查询方式 | T-003 | P0 | F-001, F-002, F-003 | +| T-007 | 计算逻辑实现 (后端) | Python 实现 CPM、看后搜人数、成本计算 | T-006 | P0 | F-004, F-005, F-006 | +| T-008 | 品牌 API 批量集成 (后端) | 后端使用 httpx 批量异步调用品牌API,支持并发控制和降级 | T-006 | P0 | F-010 | +| T-009 | 导出 API 开发 (后端) | FastAPI 实现 GET /api/v1/export 接口,生成 Excel/CSV | T-007, T-008 | P1 | F-009 | +| T-010 | 查询表单组件 (前端) | React 组件开发,调用后端查询API | T-004 | P0 | F-001, F-002, F-003 | +| T-011 | 结果表格组件 (前端) | React 组件开发,显示26个字段,数据来自后端API | T-004, T-007, T-008 | P1 | F-007 | +| T-012 | 导出按钮组件 (前端) | React 组件开发,调用后端导出API触发下载 | T-011, T-009 | P1 | F-009 | + +**阶段依赖图:** + + +``` +后端任务: +T-006 (查询API) ──────▶ T-007 (计算逻辑) ──────▶ T-009 (导出API) + │ │ │ + └──▶ T-008 (品牌API) │ │ + │ │ +前端任务: │ │ +T-010 (查询表单) ─────────────┼───────────────────────┤ + ▼ ▼ + T-011 (结果表格) ──────▶ T-012 (导出按钮) +``` + +--- + +### 3.4 Phase 3: 优化与测试 + +**目标**: 性能优化、错误处理、部署配置 + + +| 任务ID | 任务 | 描述 | 依赖 | 优先级 | 关联功能 | +|--------|------|------|------|--------|----------| +| T-013 | 错误处理 (前后端) | 完善前后端错误处理,添加用户友好提示 | T-012 | P1 | 全部 | +| T-014 | 性能优化 (后端) | 数据库索引优化,异步查询性能调优 | T-012 | P1 | F-001~003 | +| T-015 | 视频链接跳转 (前端) | 实现视频链接点击跳转功能 | T-011 | P2 | F-008 | +| T-016 | 部署配置 (前后端) | Docker 配置,前后端分离部署,CORS 配置 | T-013 | P1 | - | +| T-017 | 集成测试 | 端到端功能测试,前后端联调 | T-013 | P1 | 全部 | + +**阶段依赖图:** + + +``` +T-012 (导出按钮) + │ + ├──────────┬──────────┐ + ▼ ▼ ▼ +T-013 T-014 T-015 +(错误处理) (性能优化) (链接跳转) + │ + ├──────────┐ + ▼ ▼ +T-016 T-017 +(部署) (测试) +``` + +## 4. 技术方案 + +### 4.1 数据查询模块 + +**功能**: 支持三种查询方式(星图ID/达人ID/昵称)批量查询 KOL 视频数据 + +**技术选型**: + + +| 组件 | 技术 | 选型理由 | +|------|------|----------| +| ORM | SQLAlchemy 2.0+ | 异步 ORM,类型安全,成熟稳定 | +| 异步驱动 | asyncpg | PostgreSQL 异步驱动,高性能 | +| 查询优化 | 数据库索引 | 在 star_id, star_unique_id, star_nickname 字段建立索引 | +| 输入验证 | Pydantic | FastAPI 内置,类型安全的请求参数验证 | + +**架构设计**: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 查询模块 │ +├─────────────────────────────────────────────────────────────┤ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ 请求解析 │ ──▶ │ 参数验证 │ ──▶ │ 查询构建 │ │ +│ │ (type/values)│ │ (Zod) │ │ (Prisma) │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────┐ │ +│ │ 数据库查询 │ │ +│ │ PostgreSQL │ │ +│ └─────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +**接口设计**: + + +| 接口 | 方法 | 路径 | 说明 | +|------|------|------|------| +| 批量查询 | POST | /api/v1/query | 支持 star_id/unique_id/nickname 三种类型 | + +**请求/响应格式**: + + +```python +# 请求模型 +from pydantic import BaseModel +from typing import List, Literal + +class QueryRequest(BaseModel): + type: Literal['star_id', 'unique_id', 'nickname'] + values: List[str] # 批量ID 或单个昵称 + +# 响应模型 +class QueryResponse(BaseModel): + success: bool + data: List[VideoData] + total: int + error: str | None = None +``` + +**实现要点**: + + +- 使用 SQLAlchemy 的 `select()` 配合 `where()` 条件 +- 星图ID/达人ID 使用 `in_()` 查询批量匹配 +- 昵称使用 `like()` 进行模糊匹配(使用 `%{value}%`) +- 限制单次查询最大返回 1000 条 +- SQL 注入防护由 SQLAlchemy 和 Pydantic 自动处理 +- 使用异步查询 `session.execute(stmt)` 提升性能 + +--- + +### 4.2 数据计算模块 + +**功能**: 计算预估自然 CPM、看后搜人数、看后搜成本 + +**技术选型**: + + +| 组件 | 技术 | 选型理由 | +|------|------|----------| +| 计算逻辑 | Python | 类型安全(Type Hints),易于维护 | +| 数值处理 | Python 原生 | 简单计算,无需额外库,性能优异 | + +**架构设计**: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 计算模块 │ +├─────────────────────────────────────────────────────────────┤ +│ ┌─────────────┐ │ +│ │ 查询结果 │ │ +│ └──────┬──────┘ │ +│ │ │ +│ ┌────────────┼────────────┐ │ +│ ▼ ▼ ▼ │ +│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ +│ │ CPM 计算 │ │看后搜人数 │ │ 成本计算 │ │ +│ │ F-004 │ │ F-005 │ │ F-006 │ │ +│ └───────────┘ └─────┬─────┘ └─────┬─────┘ │ +│ │ │ │ +│ │ 依赖 │ │ +│ └─────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────┐ │ +│ │ 除零检查 │ │ +│ │ natural_play_cnt = 0 → null │ │ +│ │ total_play_cnt = 0 → null │ │ +│ │ 看后搜人数 = 0 → null │ │ +│ └─────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +**计算公式**: + + +```python +# 预估自然CPM (F-004) +def calculate_natural_cpm(estimated_video_cost: float, natural_play_cnt: int) -> float | None: + if natural_play_cnt > 0: + return round((estimated_video_cost / natural_play_cnt) * 1000, 2) + return None + +# 预估自然看后搜人数 (F-005) +def calculate_natural_search_uv( + natural_play_cnt: int, + total_play_cnt: int, + after_view_search_uv: int +) -> float | None: + if total_play_cnt > 0: + return round((natural_play_cnt / total_play_cnt) * after_view_search_uv, 2) + return None + +# 预估自然看后搜人数成本 (F-006) +def calculate_natural_search_cost( + estimated_video_cost: float, + estimated_natural_search_uv: float | None +) -> float | None: + if estimated_natural_search_uv and estimated_natural_search_uv > 0: + return round(estimated_video_cost / estimated_natural_search_uv, 2) + return None +``` + +**实现要点**: + + +- 结果保留2位小数:`round(value, 2)` +- 除零检查:分母为0时返回 None +- None 值在前端显示为 "-" +- 批量计算时使用列表推导式或 map 函数 +- 使用 Python 3.10+ 的 `float | None` 类型注解 + +--- + +### 4.3 数据展示模块 + +**功能**: 以表格形式展示查询结果,支持视频链接跳转 + +**技术选型**: + +| 组件 | 技术 | 选型理由 | +|------|------|----------| +| 表格组件 | HTML Table + Tailwind | 简单直接,样式灵活 | +| 数据格式化 | Intl API | 数字格式化、日期格式化 | + +**架构设计**: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 展示模块 │ +├─────────────────────────────────────────────────────────────┤ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ 结果表格组件 │ │ +│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ +│ │ │ 表头 │ │ 数据行 │ │ 链接列 │ │ 分页 │ │ │ +│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ 空状态组件 │ │ +│ │ "未找到匹配数据" │ │ +│ └─────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +**实现要点**: + +- 26个字段使用中文列名 +- 视频链接使用 `` +- 数字字段格式化:千分位分隔 +- 空值显示为 "-" +- 支持横向滚动(表格宽度超出时) + +--- + +### 4.4 数据导出模块 + +**功能**: 将查询结果导出为 Excel 或 CSV 文件 + +**技术选型**: + +| 组件 | 技术 | 选型理由 | +|------|------|----------| +| Excel 生成 | xlsx (SheetJS) | 成熟稳定,支持 .xlsx 格式 | +| CSV 生成 | 原生实现 | 简单格式,无需额外库 | + +**接口设计**: + +| 接口 | 方法 | 路径 | 说明 | +|------|------|------|------| +| 数据导出 | GET | /api/export | 参数:format=xlsx/csv,data=查询条件 | + +**实现要点**: + +- 使用中文列名作为表头 +- Excel 格式使用 xlsx 库生成 +- CSV 格式需处理逗号转义 +- 文件名格式:`kol_data_${timestamp}.xlsx` +- 响应头设置 `Content-Disposition: attachment` +- 限制单次导出最大 1000 条 + +--- + + +### 4.5 品牌 API 批量集成 + +**功能**: 后端批量调用品牌API获取品牌名称(关联 F-010) + +**接口详情**: + +| 项目 | 内容 | +|------|------| +| 地址 | https://api.internal.intelligrow.cn/v1/yuntu/brands/{brand_id} | +| 方法 | GET | +| 响应 | 品牌名称 | + +**批量调用策略**: + +``` +查询结果 (N条数据) + │ + ▼ +┌─────────────────────────────────────┐ +│ 1. 提取唯一 brand_id │ +│ - 过滤空值 │ +│ - 去重处理 │ +└─────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────┐ +│ 2. 批量并发请求 │ +│ - 并发限制: 10 个请求 │ +│ - 单请求超时: 3 秒 │ +└─────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────┐ +│ 3. 构建映射表 │ +│ Map │ +└─────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────┐ +│ 4. 填充查询结果 │ +│ - 成功: 显示品牌名称 │ +│ - 失败: 降级显示 brand_id │ +└─────────────────────────────────────┘ +``` + +**实现要点**: + + +- **在后端调用**: 查询API获取数据库结果后,立即调用品牌API +- **去重处理**: 提取查询结果中所有唯一的 brand_id,避免重复请求 +- **并发控制**: 使用 asyncio.gather 或 asyncio.Semaphore 限制最大 10 个并发请求 +- **超时设置**: 单个请求超时 3 秒,避免阻塞整体响应 +- **降级策略**: API 调用失败时,显示原始 brand_id +- **结果合并**: 将品牌名称填充到查询结果后返回前端 + + +```python +import asyncio +import httpx +from typing import Dict, List + +# 品牌名称批量获取 +async def get_brand_names(brand_ids: List[str]) -> Dict[str, str]: + unique_ids = list(set(filter(None, brand_ids))) + brand_map: Dict[str, str] = {} + + # 并发控制:限制 10 个并发 + CONCURRENCY_LIMIT = 10 + TIMEOUT_SECONDS = 3.0 + + async with httpx.AsyncClient(timeout=TIMEOUT_SECONDS) as client: + # 使用信号量控制并发数 + semaphore = asyncio.Semaphore(CONCURRENCY_LIMIT) + + async def fetch_brand(brand_id: str) -> tuple[str, str]: + async with semaphore: + try: + response = await client.get( + f"https://api.internal.intelligrow.cn/v1/yuntu/brands/{brand_id}" + ) + if response.status_code == 200: + data = response.json() + return brand_id, data.get("name", brand_id) + except Exception: + pass # 降级处理 + return brand_id, brand_id # 失败时返回原ID + + # 批量并发请求 + results = await asyncio.gather( + *[fetch_brand(brand_id) for brand_id in unique_ids], + return_exceptions=True + ) + + # 构建映射表 + for result in results: + if isinstance(result, tuple): + brand_id, brand_name = result + brand_map[brand_id] = brand_name + + return brand_map + +# 在查询 API 中使用 +async def handle_query(request: QueryRequest) -> QueryResponse: + # 1. 查询数据库 + videos = await query_videos(request) + + # 2. 提取品牌ID并批量获取品牌名称 + brand_ids = [v.brand_id for v in videos if v.brand_id] + brand_map = await get_brand_names(brand_ids) + + # 3. 填充品牌名称 + for video in videos: + if video.brand_id: + video.brand_name = brand_map.get(video.brand_id, video.brand_id) + else: + video.brand_name = None + + # 4. 计算预估指标并返回 + return calculate_and_format(videos) +``` + +## 5. 风险管理 + +| 风险 | 可能性 | 影响 | 应对措施 | 负责人 | +|------|--------|------|----------|--------| +| 数据库连接不稳定 | 低 | 高 | 使用 Prisma 连接池,实现重试机制 | 后端开发 | +| 大批量查询性能问题 | 中 | 中 | 限制单次查询上限,优化数据库索引 | 后端开发 | + +| 品牌 API 不可用/超时 | 中 | 中 | 并发限制(10)、单请求超时(3s)、降级显示 brand_id | 后端开发 | +| 导出数据量过大 | 中 | 中 | 限制单次导出 1000 条,分批导出提示 | 后端开发 | +| 数据同步延迟 | 中 | 中 | 显示数据更新时间,建立同步监控 | 运维 | + +## 6. 里程碑 + +``` +M1 M2 M3 M4 +│ │ │ │ +▼ ▼ ▼ ▼ +◆───────────────◆───────────────◆───────────────◆ +│ │ │ │ +基础架构 核心功能 功能完善 正式上线 +搭建完成 开发完成 测试完成 部署完成 +``` + +| 里程碑 | 目标 | 交付物 | 验收标准 | +|--------|------|--------|----------| +| M1 | 基础架构搭建完成 | 项目骨架、数据库连接、基础UI | 项目可运行,数据库可连接 | +| M2 | 核心功能开发完成 | 查询、计算、展示、导出功能 | 所有 P0/P1 功能可用 | +| M3 | 功能完善测试完成 | 错误处理、性能优化、链接跳转 | 测试通过,性能达标 | +| M4 | 正式上线部署完成 | Docker/PM2 部署配置 | 生产环境可访问 | + +## 7. 资源需求 + + +| 角色 | 人数 | 职责 | 参与阶段 | +|------|------|------|----------| +| 前端开发 | 1 | Next.js 开发、UI 组件、API 调用 | Phase 1-3 | +| 后端开发 | 1 | FastAPI 开发、API 设计、数据库操作 | Phase 1-3 | +| 运维/DevOps | 0.5 | 前后端分离部署、CORS 配置、监控告警 | Phase 3 | + +**注**: 也可由全栈开发承担前后端工作 + +## 8. 目录结构 + + +``` +kol-insight/ +├── frontend/ # 前端项目(Next.js) +│ ├── src/ +│ │ ├── app/ +│ │ │ ├── page.tsx # 首页(查询页面) +│ │ │ ├── layout.tsx # 根布局 +│ │ │ └── globals.css # 全局样式 +│ │ ├── components/ +│ │ │ ├── QueryForm.tsx # 查询表单组件 +│ │ │ ├── ResultTable.tsx # 结果表格组件 +│ │ │ └── ExportButton.tsx # 导出按钮组件 +│ │ ├── lib/ +│ │ │ ├── api.ts # API 调用封装 +│ │ │ └── utils.ts # 工具函数 +│ │ └── types/ +│ │ └── index.ts # 类型定义 +│ ├── .env.local # 环境变量(后端API地址) +│ ├── .env.example # 环境变量示例 +│ ├── package.json +│ ├── tsconfig.json +│ ├── tailwind.config.ts +│ └── next.config.js +│ +├── backend/ # 后端项目(FastAPI) +│ ├── app/ +│ │ ├── main.py # FastAPI 应用入口 +│ │ ├── config.py # 配置管理 +│ │ ├── database.py # 数据库连接 +│ │ ├── models/ +│ │ │ └── kol_video.py # SQLAlchemy 模型 +│ │ ├── schemas/ +│ │ │ ├── query.py # Pydantic 请求/响应模型 +│ │ │ └── video.py # 视频数据模型 +│ │ ├── api/ +│ │ │ ├── v1/ +│ │ │ │ ├── __init__.py +│ │ │ │ ├── query.py # 查询接口 +│ │ │ │ └── export.py # 导出接口 +│ │ │ └── deps.py # 依赖注入 +│ │ ├── services/ +│ │ │ ├── query_service.py # 查询业务逻辑 +│ │ │ ├── calculator.py # 计算逻辑 +│ │ │ ├── brand_api.py # 品牌 API 集成 +│ │ │ └── export_service.py # 导出服务 +│ │ └── core/ +│ │ ├── security.py # 安全相关 +│ │ └── logger.py # 日志配置 +│ ├── alembic/ # 数据库迁移 +│ │ └── versions/ +│ ├── tests/ # 测试 +│ ├── .env # 环境变量 +│ ├── .env.example # 环境变量示例 +│ ├── requirements.txt # 依赖列表(或 pyproject.toml) +│ └── alembic.ini # Alembic 配置 +│ +├── docker-compose.yml # Docker 编排(可选) +└── README.md # 项目文档 +``` + +## 9. 数据库 Schema + + +```python +# backend/app/models/kol_video.py +from sqlalchemy import Column, String, Integer, Float, DateTime, Index +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() + +class KolVideo(Base): + __tablename__ = "kol_videos" + + # 主键 + item_id = Column(String, primary_key=True) + + # 基础信息 + title = Column(String, nullable=True) + viral_type = Column(String, nullable=True) + video_url = Column(String, nullable=True) + star_id = Column(String, nullable=False, index=True) + star_unique_id = Column(String, nullable=False, index=True) + star_nickname = Column(String, nullable=False, index=True) + publish_time = Column(DateTime, nullable=True) + + # 曝光指标 + natural_play_cnt = Column(Integer, default=0) + heated_play_cnt = Column(Integer, default=0) + total_play_cnt = Column(Integer, default=0) + + # 互动指标 + total_interact = Column(Integer, default=0) + like_cnt = Column(Integer, default=0) + share_cnt = Column(Integer, default=0) + comment_cnt = Column(Integer, default=0) + + # 效果指标 + new_a3_rate = Column(Float, nullable=True) + after_view_search_uv = Column(Integer, default=0) + return_search_cnt = Column(Integer, default=0) + + # 商业信息 + industry_id = Column(String, nullable=True) + industry_name = Column(String, nullable=True) + brand_id = Column(String, nullable=True) + estimated_video_cost = Column(Float, default=0) + + # 索引定义(补充) + __table_args__ = ( + Index('idx_star_id', 'star_id'), + Index('idx_star_unique_id', 'star_unique_id'), + Index('idx_star_nickname', 'star_nickname'), + ) +``` + +**对应的 Alembic 迁移 SQL**: + +```sql +-- 创建表和索引 +CREATE TABLE kol_videos ( + item_id VARCHAR PRIMARY KEY, + title VARCHAR, + viral_type VARCHAR, + video_url VARCHAR, + star_id VARCHAR NOT NULL, + star_unique_id VARCHAR NOT NULL, + star_nickname VARCHAR NOT NULL, + publish_time TIMESTAMP, + natural_play_cnt INTEGER DEFAULT 0, + heated_play_cnt INTEGER DEFAULT 0, + total_play_cnt INTEGER DEFAULT 0, + total_interact INTEGER DEFAULT 0, + like_cnt INTEGER DEFAULT 0, + share_cnt INTEGER DEFAULT 0, + comment_cnt INTEGER DEFAULT 0, + new_a3_rate FLOAT, + after_view_search_uv INTEGER DEFAULT 0, + return_search_cnt INTEGER DEFAULT 0, + industry_id VARCHAR, + industry_name VARCHAR, + brand_id VARCHAR, + estimated_video_cost FLOAT DEFAULT 0 +); + +CREATE INDEX idx_star_id ON kol_videos(star_id); +CREATE INDEX idx_star_unique_id ON kol_videos(star_unique_id); +CREATE INDEX idx_star_nickname ON kol_videos(star_nickname); +``` + + +## 10. 前后端分离部署 + +### 10.1 部署架构 + +``` +┌─────────────────────────────────────────────────┐ +│ 前端部署 (Next.js) │ +│ - Vercel / Docker + Nginx │ +│ - 端口: 3000 │ +│ - 环境变量: NEXT_PUBLIC_API_URL │ +└─────────────────────────────────────────────────┘ + │ + │ HTTP API 调用 + ▼ +┌─────────────────────────────────────────────────┐ +│ 后端部署 (FastAPI) │ +│ - Docker + Uvicorn │ +│ - 端口: 8000 │ +│ - 环境变量: DATABASE_URL, CORS_ORIGINS │ +└─────────────────────────────────────────────────┘ + │ + │ PostgreSQL + ▼ +┌─────────────────────────────────────────────────┐ +│ 数据库 (PostgreSQL) │ +│ - 端口: 5432 │ +└─────────────────────────────────────────────────┘ +``` + +### 10.2 Docker Compose 配置示例 + +```yaml +version: '3.8' + +services: + # 后端服务 + backend: + build: ./backend + ports: + - "8000:8000" + environment: + - DATABASE_URL=postgresql://user:password@db:5432/kol_insight + - CORS_ORIGINS=http://localhost:3000,https://your-frontend-domain.com + depends_on: + - db + command: uvicorn app.main:app --host 0.0.0.0 --port 8000 + + # 前端服务 + frontend: + build: ./frontend + ports: + - "3000:3000" + environment: + - NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1 + depends_on: + - backend + + # 数据库服务 + db: + image: postgres:14 + environment: + - POSTGRES_USER=user + - POSTGRES_PASSWORD=password + - POSTGRES_DB=kol_insight + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "5432:5432" + +volumes: + postgres_data: +``` + +### 10.3 CORS 配置(后端) + +```python +# backend/app/main.py +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from app.config import settings + +app = FastAPI(title="KOL Insight API", version="1.0.0") + +# CORS 配置 +app.add_middleware( + CORSMiddleware, + allow_origins=settings.CORS_ORIGINS, # ["http://localhost:3000"] + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) +``` + +### 10.4 API 调用封装(前端) + +```typescript +// frontend/src/lib/api.ts +const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api/v1'; + +export async function queryVideos(request: QueryRequest): Promise { + const response = await fetch(`${API_BASE_URL}/query`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(request), + }); + + if (!response.ok) { + throw new Error('Query failed'); + } + + return response.json(); +} + +export async function exportData(format: 'xlsx' | 'csv'): Promise { + const response = await fetch(`${API_BASE_URL}/export?format=${format}`); + + if (!response.ok) { + throw new Error('Export failed'); + } + + return response.blob(); +} +``` + +### 10.5 FastAPI 自动生成 API 文档 + +FastAPI 自动生成交互式 API 文档,无需额外配置: + +- Swagger UI: `http://localhost:8000/docs` +- ReDoc: `http://localhost:8000/redoc` +- OpenAPI JSON: `http://localhost:8000/openapi.json` + +前端开发人员可直接通过这些文档了解 API 接口定义和测试 API。 + diff --git a/doc/FeatureSummary.md b/doc/FeatureSummary.md new file mode 100644 index 0000000..9492e88 --- /dev/null +++ b/doc/FeatureSummary.md @@ -0,0 +1,487 @@ +# KOL Insight - 功能摘要 + +## 文档信息 + +| 项目 | 内容 | +|------|------| +| 版本 | v1.0 | +| 创建日期 | 2025-01-28 | +| 来源文档 | PRD.md | + +## 1. 功能总览 + +### 1.1 功能统计 + +| 类别 | 数量 | +|------|------| +| 功能模块 | 5 个 | +| P0 功能 | 7 个 | +| P1 功能 | 2 个 | +| P2 功能 | 1 个 | + +### 1.2 功能架构图 + +``` +┌─────────────────────────────────────────────────────────────────────────────────┐ +│ KOL Insight │ +├─────────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ 数据查询模块 │ │ 数据计算模块 │ │ 品牌API集成模块 │ │ +│ │ ────────────── │ │ ────────────── │ │ ────────────── │ │ +│ │ • 星图ID查询 │ │ • 预估自然CPM │ │ • 品牌名称批量 │ │ +│ │ • 达人ID查询 │ │ • 预估看后搜人数 │ │ 获取(后端) │ │ +│ │ • 昵称模糊查询 │ │ • 看后搜人数成本 │ │ │ │ +│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ +│ │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ 数据展示模块 │ │ 数据导出模块 │ │ 外部服务依赖 │ │ +│ │ ────────────── │ │ ────────────── │ │ ────────────── │ │ +│ │ • 结果列表展示 │ │ • Excel/CSV导出 │ │ • PostgreSQL │ │ +│ │ • 视频链接跳转 │ │ │ │ • 品牌API │ │ +│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────────┘ +``` + +### 1.3 模块依赖关系 + +``` + ┌──────────────────┐ + │ 数据查询模块 │ + │ (F-001~003) │ + └────────┬─────────┘ + │ + │ 查询结果(含brand_id) + ▼ +┌──────────────────┐ ┌──────────────────┐ +│ 品牌API集成 │◀─│ 数据计算模块 │ +│ (F-010) │ │ (F-004~006) │ +│ 后端批量调用 │ └────────┬─────────┘ +└────────┬─────────┘ │ + │ │ 计算结果 + │ 品牌名称 │ + └─────────┬───────────┘ + ▼ + ┌──────────────────┐ + │ 数据展示模块 │ + │ (F-007~008) │ + └────────┬─────────┘ + │ + │ 展示数据(含品牌名称) + ▼ + ┌──────────────────┐ + │ 数据导出模块 │ + │ (F-009) │ + └──────────────────┘ +``` + +## 2. 功能清单 + +### 2.1 数据查询模块 + +**模块职责**: 接收用户输入的查询条件,从数据库检索匹配的KOL视频数据 + +#### 功能列表 + +| ID | 功能 | 描述 | 优先级 | 关联用户故事 | +|----|------|------|--------|--------------| +| F-001 | 星图ID查询 | 批量输入星图ID,精准匹配数据库 | P0 | US-001 | +| F-002 | 达人ID查询 | 批量输入达人unique_id,精准匹配数据库 | P0 | US-002 | +| F-003 | 昵称模糊查询 | 输入达人昵称,模糊匹配数据库 | P0 | US-003 | + +#### 功能契约详情 + +**F-001: 星图ID查询** + +| 契约项 | 说明 | +|--------|------| +| **触发条件** | 用户选择"星图ID"查询方式并提交查询 | +| **输入** | 星图ID列表(换行分隔的字符串) | +| **处理逻辑** | 1. 解析输入,按换行符分割为ID数组
2. 对每个ID执行 `WHERE star_id = ?` 精准匹配
3. 合并所有匹配结果 | +| **输出** | 匹配的视频数据列表(包含26个输出字段) | +| **异常情况** | 1. 输入为空:提示"请输入星图ID"
2. 无匹配结果:返回空列表,提示"未找到匹配数据"
3. 数据库连接失败:显示错误提示 | +| **边界说明** | 仅支持精准匹配,不支持模糊匹配;单次查询建议不超过100个ID | + +**F-002: 达人ID查询** + +| 契约项 | 说明 | +|--------|------| +| **触发条件** | 用户选择"达人unique_id"查询方式并提交查询 | +| **输入** | 达人unique_id列表(换行分隔的字符串) | +| **处理逻辑** | 1. 解析输入,按换行符分割为ID数组
2. 对每个ID执行 `WHERE star_unique_id = ?` 精准匹配
3. 合并所有匹配结果 | +| **输出** | 匹配的视频数据列表(包含26个输出字段) | +| **异常情况** | 1. 输入为空:提示"请输入达人ID"
2. 无匹配结果:返回空列表
3. 数据库连接失败:显示错误提示 | +| **边界说明** | 仅支持精准匹配;单次查询建议不超过100个ID | + +**F-003: 昵称模糊查询** + +| 契约项 | 说明 | +|--------|------| +| **触发条件** | 用户选择"达人昵称"查询方式并提交查询 | +| **输入** | 达人昵称(字符串) | +| **处理逻辑** | 1. 执行 `WHERE star_nickname LIKE '%{input}%'` 包含匹配
2. 返回所有匹配结果 | +| **输出** | 匹配的视频数据列表(包含26个输出字段) | +| **异常情况** | 1. 输入为空:提示"请输入达人昵称"
2. 无匹配结果:返回空列表
3. 匹配结果过多:返回前1000条并提示 | +| **边界说明** | 支持模糊匹配(包含关系);结果数量可能较大 | + +--- + +### 2.2 数据计算模块 + +**模块职责**: 对查询结果进行预估指标计算,生成CPM和看后搜成本数据 + +#### 功能列表 + +| ID | 功能 | 描述 | 优先级 | 关联用户故事 | +|----|------|------|--------|--------------| +| F-004 | 预估自然CPM计算 | 计算每千次自然曝光的成本 | P0 | US-004 | +| F-005 | 预估自然看后搜人数计算 | 计算自然流量带来的看后搜人数 | P0 | US-004 | +| F-006 | 预估自然看后搜人数成本计算 | 计算每个自然看后搜用户的成本 | P0 | US-004 | + +#### 功能契约详情 + +**F-004: 预估自然CPM计算** + +| 契约项 | 说明 | +|--------|------| +| **触发条件** | 查询返回结果后自动执行 | +| **输入** | 视频数据中的 `estimated_video_cost`、`natural_play_cnt` 字段 | +| **处理逻辑** | `预估自然CPM = estimated_video_cost / natural_play_cnt * 1000` | +| **输出** | 预估自然CPM值(单位:元/千次曝光) | +| **异常情况** | 1. natural_play_cnt = 0:返回 null 或 "-"
2. 字段缺失:返回 null | +| **边界说明** | 结果保留2位小数;除零时不计算 | + +**F-005: 预估自然看后搜人数计算** + +| 契约项 | 说明 | +|--------|------| +| **触发条件** | 查询返回结果后自动执行 | +| **输入** | 视频数据中的 `natural_play_cnt`、`total_play_cnt`、`after_view_search_uv` 字段 | +| **处理逻辑** | `预估自然看后搜人数 = natural_play_cnt / total_play_cnt * after_view_search_uv` | +| **输出** | 预估自然看后搜人数(单位:人) | +| **异常情况** | 1. total_play_cnt = 0:返回 null
2. 字段缺失:返回 null | +| **边界说明** | 结果取整或保留2位小数;除零时不计算 | + +**F-006: 预估自然看后搜人数成本计算** + +| 契约项 | 说明 | +|--------|------| +| **触发条件** | F-005 计算完成后自动执行 | +| **输入** | `estimated_video_cost` 字段、F-005 的计算结果 | +| **处理逻辑** | `预估自然看后搜人数成本 = estimated_video_cost / 预估自然看后搜人数` | +| **输出** | 预估自然看后搜人数成本(单位:元/人) | +| **异常情况** | 1. 预估自然看后搜人数 = 0 或 null:返回 null
2. 依赖计算失败:返回 null | +| **边界说明** | 依赖 F-005 结果;结果保留2位小数 | + +--- + +### 2.3 品牌API集成模块 + +**模块职责**: 在后端查询时批量调用品牌API,获取品牌名称并补充到查询结果中 + +#### 功能列表 + +| ID | 功能 | 描述 | 优先级 | 关联用户故事 | +|----|------|------|--------|--------------| +| F-010 | 品牌名称批量获取 | 后端批量调用品牌API获取品牌名称 | P0 | US-006 | + +#### 功能契约详情 + +**F-010: 品牌名称批量获取** + +| 契约项 | 说明 | +|--------|------| +| **触发条件** | 后端查询返回结果后,在返回给前端之前自动执行 | +| **输入** | 查询结果中的 `brand_id` 列表(去重后) | +| **处理逻辑** | 1. 从查询结果中提取所有唯一的 brand_id
2. 批量并发调用品牌API:`GET /v1/yuntu/brands/{brand_id}`
3. 构建 brand_id → brand_name 映射表
4. 将品牌名称填充到每条查询结果的 brand_name 字段 | +| **输出** | 补充了 brand_name 字段的完整查询结果 | +| **异常情况** | 1. 单个品牌API调用失败:该条记录 brand_name 降级显示 brand_id
2. 品牌API服务不可用:所有记录降级显示 brand_id
3. brand_id 为空:brand_name 显示为 "-" | +| **边界说明** | 1. 在后端执行,前端无需调用
2. 使用并发控制,限制同时请求数(如最多10个并发)
3. 可选:缓存品牌名称,减少重复请求
4. 超时设置:单个请求超时3秒 | + +**批量调用策略**: + +``` +查询结果 (100条) + │ + ▼ +┌─────────────────────────────────────┐ +│ 1. 提取唯一 brand_id (假设30个) │ +└─────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────┐ +│ 2. 批量并发请求 (限制10并发) │ +│ Promise.all([ │ +│ fetch(brand/id1), │ +│ fetch(brand/id2), │ +│ ... │ +│ ]) │ +└─────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────┐ +│ 3. 构建映射表 │ +│ { id1: "品牌A", id2: "品牌B" } │ +└─────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────┐ +│ 4. 填充 brand_name 到结果 │ +└─────────────────────────────────────┘ +``` + +--- + +### 2.4 数据展示模块 + +**模块职责**: 将查询和计算结果以表格形式展示给用户 + +#### 功能列表 + +| ID | 功能 | 描述 | 优先级 | 关联用户故事 | +|----|------|------|--------|--------------| +| F-007 | 结果列表展示 | 以表格形式展示查询结果的所有字段 | P1 | US-006 | +| F-008 | 视频链接跳转 | 点击视频链接在新窗口打开原视频 | P2 | US-007 | + +#### 功能契约详情 + +**F-007: 结果列表展示** + +| 契约项 | 说明 | +|--------|------| +| **触发条件** | 查询完成且有结果数据 | +| **输入** | 查询结果数据列表(含计算字段和品牌名称,由后端 F-010 处理完成) | +| **处理逻辑** | 1. 渲染数据表格
2. 展示26个输出字段(含3个计算字段)
3. 品牌名称已由后端获取,直接展示 brand_name 字段 | +| **输出** | HTML表格展示 | +| **异常情况** | 1. 无数据:显示空状态提示
2. 品牌名称为空:显示 "-" | +| **边界说明** | 支持分页(如数据量大);列名使用中文;品牌名称由后端处理,前端无需调用API | + +**F-008: 视频链接跳转** + +| 契约项 | 说明 | +|--------|------| +| **触发条件** | 用户点击视频链接 | +| **输入** | 视频链接URL(video_url字段) | +| **处理逻辑** | 在新窗口/标签页打开链接 | +| **输出** | 浏览器新标签页打开视频页面 | +| **异常情况** | 1. 链接为空:不可点击或显示提示
2. 链接无效:由浏览器处理 | +| **边界说明** | 使用 `target="_blank"` 打开;需考虑安全属性 | + +--- + +### 2.4 数据导出模块 + +**模块职责**: 将查询结果导出为Excel或CSV文件供用户下载 + +#### 功能列表 + +| ID | 功能 | 描述 | 优先级 | 关联用户故事 | +|----|------|------|--------|--------------| +| F-009 | Excel/CSV导出 | 将当前查询结果导出为文件 | P1 | US-005 | + +#### 功能契约详情 + +**F-009: Excel/CSV导出** + +| 契约项 | 说明 | +|--------|------| +| **触发条件** | 用户点击"导出"按钮 | +| **输入** | 当前查询结果数据列表 | +| **处理逻辑** | 1. 将数据转换为Excel/CSV格式
2. 使用中文列名作为表头
3. 生成文件并触发下载 | +| **输出** | Excel(.xlsx)或CSV文件下载 | +| **异常情况** | 1. 无数据:提示"无数据可导出"
2. 数据量过大:分批处理或提示限制 | +| **边界说明** | 导出数据包含所有26个字段;文件名包含时间戳;单次导出建议不超过1000条 | + +--- + +## 3. 功能依赖矩阵 + +| 功能 | F-001 | F-002 | F-003 | F-004 | F-005 | F-006 | F-007 | F-008 | F-009 | F-010 | +|------|-------|-------|-------|-------|-------|-------|-------|-------|-------|-------| +| F-001 | - | | | | | | | | | | +| F-002 | | - | | | | | | | | | +| F-003 | | | - | | | | | | | | +| F-004 | ✓ | ✓ | ✓ | - | | | | | | | +| F-005 | ✓ | ✓ | ✓ | | - | | | | | | +| F-006 | | | | | ✓ | - | | | | | +| F-007 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | - | | | ✓ | +| F-008 | | | | | | | ✓ | - | | | +| F-009 | | | | ✓ | ✓ | ✓ | ✓ | | - | ✓ | +| F-010 | ✓ | ✓ | ✓ | | | | | | | - | + +**说明**: +- ✓ 表示行功能依赖列功能 +- F-004/005/006(计算模块)依赖 F-001/002/003(查询模块)的输出 +- F-006 依赖 F-005 的计算结果 +- F-010(品牌API)依赖查询模块获取 brand_id 列表,在后端批量调用 +- F-007(展示)依赖查询、计算结果和 F-010 的品牌名称 +- F-009(导出)依赖展示数据(含品牌名称) + +## 4. 功能流程图 + +### 4.1 核心业务流程:批量查询与导出 + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ 用户输入 │ │ 选择查询 │ │ 提交查询 │ +│ 查询条件 │ ──▶ │ 方式 │ ──▶ │ │ +└─────────────┘ └─────────────┘ └──────┬──────┘ + │ + ┌──────────────────────────┘ + ▼ + ┌──────────────────────────────────────────────────────┐ + │ 数据查询模块 │ + │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ + │ │ F-001 │ │ F-002 │ │ F-003 │ │ + │ │ 星图ID │ / │ 达人ID │ / │ 昵称 │ │ + │ └────┬────┘ └────┬────┘ └────┬────┘ │ + └───────┼─────────────┼─────────────┼─────────────────┘ + └─────────────┼─────────────┘ + ▼ + ┌──────────────────────────────────────────────────────┐ + │ 后端处理(并行执行) │ + │ │ + │ ┌────────────────────┐ ┌────────────────────┐ │ + │ │ 数据计算模块 │ │ 品牌API集成模块 │ │ + │ │ ┌─────┐ ┌─────┐ │ │ ┌──────────────┐ │ │ + │ │ │F-004│ │F-005│ │ │ │ F-010 │ │ │ + │ │ │ CPM │ │看后搜│ │ │ │ 批量获取品牌 │ │ │ + │ │ └─────┘ └──┬──┘ │ │ │ 名称 │ │ │ + │ │ │ │ │ └──────────────┘ │ │ + │ │ ┌───┘ │ │ │ │ + │ │ ▼ │ │ │ │ + │ │ ┌─────┐ │ │ │ │ + │ │ │F-006│ │ │ │ │ + │ │ │成本 │ │ │ │ │ + │ │ └─────┘ │ │ │ │ + │ └────────────────────┘ └────────────────────┘ │ + └─────────────────────┬────────────────────────────────┘ + ▼ + ┌──────────────────────────────────────────────────────┐ + │ 数据展示模块 │ + │ ┌───────────────────────────────────────┐ │ + │ │ F-007 结果列表展示(含品牌名称) │ │ + │ │ ┌─────────────────────────────────┐ │ │ + │ │ │ F-008 视频链接(可点击跳转) │ │ │ + │ │ └─────────────────────────────────┘ │ │ + │ └───────────────────────────────────────┘ │ + └─────────────────────┬────────────────────────────────┘ + │ + ┌────────────┴────────────┐ + ▼ ▼ + ┌─────────────┐ ┌─────────────┐ + │ 继续查询 │ │ F-009 │ + │ │ │ 导出数据 │ + └─────────────┘ └─────────────┘ +``` + +### 4.2 计算模块内部流程 + +``` +┌──────────────────┐ +│ 查询结果数据 │ +└────────┬─────────┘ + │ + ▼ +┌────────────────────────────────────────────────────────────┐ +│ 并行计算 │ +│ │ +│ ┌─────────────────────┐ ┌─────────────────────┐ │ +│ │ F-004 │ │ F-005 │ │ +│ │ 预估自然CPM │ │ 预估自然看后搜人数 │ │ +│ │ │ │ │ │ +│ │ cost/natural*1000 │ │ natural/total*uv │ │ +│ └──────────┬──────────┘ └──────────┬──────────┘ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌─────────────────────┐ ┌─────────────────────┐ │ +│ │ 除零检查 │ │ 除零检查 │ │ +│ │ null → 显示"-" │ │ null → 显示"-" │ │ +│ └─────────────────────┘ └──────────┬──────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────┐ │ +│ │ F-006 │ │ +│ │ 预估看后搜人数成本 │ │ +│ │ │ │ +│ │ cost/看后搜人数 │ │ +│ └─────────────────────┘ │ +└────────────────────────────────────────────────────────────┘ +``` + +## 5. 版本规划 + +| 版本 | 包含功能 | 功能ID | 目标 | +|------|----------|--------|------| +| MVP | 核心查询、计算、品牌API、展示、导出 | F-001 ~ F-007, F-009, F-010 | 完成核心批量查询和成本计算功能 | +| v1.1 | 视频链接跳转、性能优化 | F-008 | 提升用户体验,优化查询性能 | + +### MVP 功能清单 + +| 优先级 | 功能ID | 功能名称 | +|--------|--------|----------| +| P0 | F-001 | 星图ID查询 | +| P0 | F-002 | 达人ID查询 | +| P0 | F-003 | 昵称模糊查询 | +| P0 | F-004 | 预估自然CPM计算 | +| P0 | F-005 | 预估自然看后搜人数计算 | +| P0 | F-006 | 预估自然看后搜人数成本计算 | +| P0 | F-010 | 品牌名称批量获取(后端) | +| P1 | F-007 | 结果列表展示 | +| P1 | F-009 | Excel/CSV导出 | + +### v1.1 功能清单 + +| 优先级 | 功能ID | 功能名称 | +|--------|--------|----------| +| P2 | F-008 | 视频链接跳转 | + +## 6. 接口契约预览 + +> 详细接口定义在 DevelopmentPlan 中,此处仅列出关键接口 + + +| 功能 | 接口类型 | 端点 | 简要说明 | +|------|----------|------|----------| +| F-001/002/003 | FastAPI 后端 | POST /api/v1/query | 批量查询,支持type参数区分查询方式 | +| F-009 | FastAPI 后端 | GET /api/v1/export | 导出当前查询结果 | +| F-010 | 外部API(后端调用) | GET /v1/yuntu/brands/{brand_id} | 后端批量获取品牌名称 | + + +**技术架构说明**: +- **后端**: Python FastAPI 框架,提供 RESTful API +- **前端**: React + Next.js,通过 HTTP 请求调用后端 API +- **架构**: 前后端完全分离,独立部署 +- **API 版本**: 使用 `/api/v1/` 前缀进行版本管理 +- **跨域**: FastAPI 配置 CORS 中间件支持前端调用 + + +### 查询接口预览 + + +``` +POST /api/v1/query +Content-Type: application/json + +{ + "type": "star_id" | "unique_id" | "nickname", + "values": ["id1", "id2", ...] | "昵称关键词" +} + +Response: +{ + "success": true, + "data": [...], // 视频数据列表 + "total": 100 +} +``` + +### 导出接口预览 + + +``` +GET /api/v1/export?format=xlsx|csv + +Response: 文件下载 +``` diff --git a/doc/PRD.md b/doc/PRD.md new file mode 100644 index 0000000..f16b7bf --- /dev/null +++ b/doc/PRD.md @@ -0,0 +1,365 @@ +# KOL Insight - 产品需求文档 (PRD) + +## 文档信息 + +| 项目 | 内容 | +|------|------| +| 版本 | v1.0 | +| 创建日期 | 2025-01-28 | +| 状态 | 草稿 | + +## 1. 产品概述 + +### 1.1 产品背景 + +在 KOL 营销领域,运营人员需要频繁查询达人视频数据以评估投放效果、计算投放成本。现有的云图平台虽然提供了数据,但缺乏批量查询和成本预估能力,导致运营人员需要手动逐条查询和计算,效率低下。 + +KOL Insight 旨在解决这一痛点,提供批量数据查询和智能成本预估功能,帮助运营人员快速获取决策所需的数据。 + +### 1.2 产品定位 + +- **目标用户**:KOL 营销运营人员、投放优化师、品牌方 +- **核心价值**:批量查询 KOL 视频数据,自动计算预估 CPM 和看后搜成本 +- **差异化优势**:支持多种查询方式(星图ID/达人ID/昵称),自动计算关键成本指标 + +### 1.3 产品目标 + +| 目标 | 指标 | 衡量方式 | +|------|------|----------| +| 提升查询效率 | 单次可批量查询多个 KOL | 对比手动查询耗时 | +| 降低计算错误 | 自动计算预估指标准确率 100% | 人工抽检验证 | +| 提高数据可用性 | 支持数据导出 | 导出功能完整性 | + +## 2. 用户故事 + +### 2.1 用户角色定义 + +| 角色 | 描述 | 核心目标 | 痛点 | +|------|------|----------|------| +| 运营人员 | 负责 KOL 投放执行与数据分析的一线人员 | 快速获取 KOL 视频表现数据 | 逐条查询效率低,成本计算繁琐 | +| 投放优化师 | 负责投放策略优化的专业人员 | 评估投放 ROI,优化投放策略 | 数据分散,难以横向对比 | + +### 2.2 用户故事列表 + +#### P0 - 核心故事 + +| ID | 用户故事 | 验收标准 | +|----|----------|----------| +| US-001 | 作为运营人员,我想要批量输入星图ID查询KOL数据,以便快速获取多个达人的视频表现 | 1. 支持批量输入星图ID(换行分隔)
2. 精准匹配 star_id 字段
3. 返回完整视频数据列表 | +| US-002 | 作为运营人员,我想要通过达人unique_id查询数据,以便根据达人ID快速定位数据 | 1. 支持批量输入达人unique_id
2. 精准匹配 star_unique_id 字段
3. 返回对应视频数据 | +| US-003 | 作为运营人员,我想要通过达人昵称模糊搜索,以便在不知道精确ID时也能找到数据 | 1. 支持输入达人昵称
2. 模糊匹配 star_nickname 字段
3. 返回所有匹配结果 | + +| US-004 | 作为运营人员,我想要看到预估自然CPM和看后搜人数成本,以便评估投放效果 | 1. 自动计算预估自然CPM
2. 自动计算预估自然看后搜人数
3. 自动计算预估自然看后搜人数成本
4. 数据展示在结果列表中 | + +#### P1 - 重要故事 + +| ID | 用户故事 | 验收标准 | +|----|----------|----------| +| US-005 | 作为运营人员,我想要导出查询结果,以便在 Excel 中进一步分析或汇报 | 1. 支持导出为 Excel/CSV 格式
2. 导出数据包含所有查询字段
3. 中文列名清晰可读 | +| US-006 | 作为运营人员,我想要看到视频的完整数据指标,以便全面了解视频表现 | 1. 展示所有核心指标(曝光、互动、A3率等)
2. 数据格式清晰易读 | + +#### P2 - 次要故事 + +| ID | 用户故事 | 验收标准 | +|----|----------|----------| +| US-007 | 作为运营人员,我想要点击视频链接直接跳转,以便快速查看原视频 | 1. 视频链接可点击
2. 新窗口打开视频页面 | + +### 2.3 用户旅程 + +**核心用户旅程:批量查询 KOL 数据** + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ 触发点 │ │ 输入查询条件 │ │ 查看结果 │ │ 导出数据 │ +│ 需要KOL数据 │ ──▶ │ 批量输入ID/昵称 │ ──▶ │ 浏览数据列表 │ ──▶ │ 下载Excel/CSV │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────┘ + │ │ │ │ + ▼ ▼ ▼ ▼ + "需要快速获取 "选择查询方式, "系统自动计算 "数据可用于 + 多个达人数据" 粘贴ID列表" CPM等指标" 汇报和分析" +``` + +用户从需要 KOL 数据开始,选择合适的查询方式(星图ID/达人ID/昵称),批量输入查询条件后提交查询。系统返回结果列表,自动计算预估 CPM 和看后搜成本等指标。用户可浏览数据,需要时导出为 Excel 或 CSV 文件。 + +## 3. 功能需求 + +### 3.1 功能架构 + + +``` +KOL Insight +├── 数据查询模块 +│ ├── 星图ID精准查询 +│ ├── 达人unique_id精准查询 +│ └── 达人昵称模糊查询 +├── 数据计算模块 +│ ├── 预估自然CPM计算 +│ ├── 预估自然看后搜人数计算 +│ └── 预估自然看后搜人数成本计算 +├── 数据展示模块 +│ ├── 结果列表展示 +│ └── 视频链接跳转 +└── 数据导出模块 + └── Excel/CSV导出 +``` + +### 3.2 功能详情 + +#### 3.2.1 数据查询模块 + +| 功能点 | 描述 | 关联用户故事 | 优先级 | 验收标准 | +|--------|------|--------------|--------|----------| +| 星图ID查询 | 批量输入星图ID,精准匹配 star_id 字段 | US-001 | P0 | 支持批量输入,精准匹配,返回完整数据 | +| 达人ID查询 | 批量输入达人unique_id,精准匹配 star_unique_id 字段 | US-002 | P0 | 支持批量输入,精准匹配,返回完整数据 | +| 昵称模糊查询 | 输入达人昵称,模糊匹配 star_nickname 字段 | US-003 | P0 | 支持包含匹配,返回所有匹配结果 | + +#### 3.2.2 数据计算模块 + + +| 功能点 | 描述 | 关联用户故事 | 优先级 | 验收标准 | +|--------|------|--------------|--------|----------| +| 预估自然CPM | 公式:`estimated_video_cost / natural_play_cnt * 1000` | US-004 | P0 | 计算结果准确,单位为元/千次曝光 | +| 预估自然看后搜人数 | 公式:`natural_play_cnt / total_play_cnt * after_view_search_uv` | US-004 | P0 | 计算结果准确,单位为人数 | +| 预估自然看后搜人数成本 | 公式:`estimated_video_cost / 预估自然看后搜人数` | US-004 | P0 | 计算结果准确,单位为元/人 | + +#### 3.2.3 数据展示模块 + +| 功能点 | 描述 | 关联用户故事 | 优先级 | 验收标准 | +|--------|------|--------------|--------|----------| +| 结果列表展示 | 展示查询结果的完整数据表格 | US-006 | P1 | 包含所有指标字段,格式清晰 | +| 视频链接跳转 | 点击视频链接跳转到原视频页面 | US-007 | P2 | 链接可点击,新窗口打开 | + +#### 3.2.4 数据导出模块 + +| 功能点 | 描述 | 关联用户故事 | 优先级 | 验收标准 | +|--------|------|--------------|--------|----------| +| 数据导出 | 将查询结果导出为 Excel/CSV 格式 | US-005 | P1 | 文件可下载,数据完整,中文列名 | + +## 4. 非功能需求 + +### 4.1 性能需求 + +| 指标 | 要求 | 说明 | +|------|------|------| +| 查询响应时间 | ≤ 3秒 | 100条以内的批量查询 | +| 页面加载时间 | ≤ 2秒 | 首页加载 | +| 导出响应时间 | ≤ 5秒 | 1000条以内的数据导出 | + +### 4.2 安全需求 + +- 数据库连接使用安全凭证,不在代码中硬编码 +- 查询输入需进行 SQL 注入防护 +- 敏感配置通过环境变量管理 + +### 4.3 兼容性需求 + + +| 平台/环境 | 支持版本 | +|-----------|----------| +| 浏览器 | Chrome/Edge/Firefox 最新版 | +| Python | 3.9+ 及以上 | +| PostgreSQL | 14.x 及以上 | +| Node.js(前端构建) | 18.x 及以上 | + +### 4.4 可用性需求 + +- 系统可用性目标:99%(工作时间) +- 提供基本的错误提示,便于用户理解问题原因 + +## 5. 数据需求 + +### 5.1 数据模型 + + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 视频数据表 (kol_videos) │ +├─────────────────────────────────────────────────────────────────┤ +│ 基础信息 │ +│ item_id (视频ID) [主键] │ +│ title (视频标题) │ +│ star_id (星图ID) [索引] │ +│ star_unique_id (达人unique_id) [索引] │ +│ star_nickname (达人昵称) [索引] │ +│ video_url (视频链接) │ +│ publish_time (发布时间) │ +├─────────────────────────────────────────────────────────────────┤ +│ 曝光指标 │ +│ natural_play_cnt (自然曝光数) ★ 计算用 │ +│ heated_play_cnt (加热曝光数) │ +│ total_play_cnt (总曝光数) ★ 计算用 │ +├─────────────────────────────────────────────────────────────────┤ +│ 互动指标 │ +│ total_interact (总互动) │ +│ like_cnt (点赞) │ +│ share_cnt (转发) │ +│ comment_cnt (评论) │ +├─────────────────────────────────────────────────────────────────┤ +│ 效果指标 │ +│ new_a3_rate (新增A3率) │ +│ after_view_search_uv (看后搜人数) ★ 计算用 │ +│ return_search_cnt (回搜次数) │ +├─────────────────────────────────────────────────────────────────┤ +│ 商业信息 │ +│ industry_id (合作行业ID) │ +│ industry_name (合作行业) │ +│ brand_id (合作品牌ID) → 需调用品牌API获取名称 │ +│ estimated_video_cost (预估视频价格) ★ 计算用 │ +└─────────────────────────────────────────────────────────────────┘ + +★ 标记字段为计算预估指标所需的关键字段 +``` + +### 5.2 数据规范 + +| 字段 | 类型 | 说明 | 校验规则 | +|------|------|------|----------| +| star_id | string | 星图ID | 非空 | +| star_unique_id | string | 达人唯一标识 | 非空 | +| star_nickname | string | 达人昵称 | 非空 | +| item_id | string | 视频ID | 非空,唯一 | +| 曝光数 | integer | 各类曝光数据 | ≥ 0 | +| 互动数 | integer | 各类互动数据 | ≥ 0 | +| 预估视频价格 | decimal | 预估价格 | ≥ 0 | + +## 6. 接口需求 + +### 6.1 外部接口 + + +| 接口 | 用途 | 提供方 | +|------|------|--------| +| PostgreSQL | 数据存储与查询 | 自建数据库 | +| 品牌API | 根据品牌ID获取品牌名称 | 内部API (api.internal.intelligrow.cn) | + + +**品牌API详情**: +- 接口地址:`/v1/yuntu/brands/{brand_id}` +- 请求方式:GET +- 用途:根据合作品牌ID(brand_id)查询品牌名称 +- 文档:https://api.internal.intelligrow.cn/docs#/云图 + + +### 6.2 内部接口 + + +| 接口 | 方法 | 用途 | 说明 | +|------|------|------|------| +| /api/v1/query | POST | 批量查询KOL视频数据 | FastAPI 后端服务提供 | +| /api/v1/export | GET | 导出查询结果为Excel/CSV | FastAPI 后端服务提供 | + + +**API 架构说明**: +- 后端采用 FastAPI 框架,提供 RESTful API +- 前端(Next.js)通过 HTTP 请求调用后端 API +- API 版本管理:使用 `/api/v1/` 前缀 +- 跨域支持:FastAPI 配置 CORS 中间件 + + +## 7. 约束与依赖 + +### 7.1 技术约束 + + +| 约束 | 说明 | 影响 | +|------|------|------| +| 前端技术栈 | React + Next.js (App Router) | 前端技术选型固定 | +| 后端技术栈 | Python FastAPI + PostgreSQL | 后端技术选型固定,前后端分离部署 | +| 部署方式 | Docker(推荐) / PM2(前端) + Gunicorn/Uvicorn(后端) | 运维方式固定,需分别部署前后端服务 | + +### 7.2 业务约束 + +- 数据来源于云图平台,需确保数据同步的及时性和准确性 +- 查询功能仅用于内部运营分析,不对外开放 + +### 7.3 外部依赖 + + +| 依赖 | 说明 | +|------|------| +| 云图数据源 | 视频数据需从云图平台同步 | +| PostgreSQL 数据库 | 依赖数据库服务可用性 | +| 品牌API | 依赖内部API服务获取品牌名称 | + +## 8. 里程碑规划 + +``` +Phase 1 - MVP Phase 2 - 优化 + │ │ + ▼ ▼ +┌──────────┐ ┌──────────┐ +│ MVP │ ────────▶ │ v1.1 │ +│ 核心查询 │ │ 性能优化 │ +└──────────┘ └──────────┘ + 待定日期 待定日期 +``` + +| 阶段 | 目标 | 交付物 | +|------|------|--------| +| MVP | 完成核心查询和计算功能 | 可用的批量查询系统,支持数据导出 | +| v1.1 | 性能优化和体验提升 | 更快的查询速度,更好的用户体验 | + +## 9. 风险评估 + + +| 风险 | 可能性 | 影响 | 应对措施 | +|------|--------|------|----------| +| 数据同步延迟 | 中 | 中 | 建立数据同步监控机制 | +| 大批量查询性能问题 | 中 | 中 | 设置批量查询上限,优化数据库索引 | +| 数据库连接不稳定 | 低 | 高 | 实现连接池和重试机制 | +| 品牌API不可用 | 低 | 中 | 缓存品牌数据,降级显示品牌ID | + +## 附录 + +### A. 术语表 + + +| 术语 | 定义 | +|------|------| +| KOL | Key Opinion Leader,关键意见领袖,即达人/网红 | +| 星图ID | 巨量星图平台的达人唯一标识 | +| unique_id | 达人在平台的唯一标识符 | +| CPM | Cost Per Mille,每千次曝光成本 | +| 看后搜 | 用户观看视频后进行搜索的行为 | +| A3率 | 用户深度互动率指标 | +| 自然曝光 | 非付费推广获得的曝光量 | +| 加热曝光 | 通过付费推广获得的曝光量 | +| natural_play_cnt | 自然曝光次数,用于计算预估自然CPM | +| total_play_cnt | 总曝光次数,包含自然+加热曝光 | +| estimated_video_cost | 预估视频价格,用于计算成本指标 | +| after_view_search_uv | 看后搜用户数,观看视频后进行搜索的独立用户数 | + +### B. 输出字段完整列表 + + +| 中文名 | 字段名 | 说明 | +|--------|--------|------| +| 视频ID | item_id | 主键 | +| 视频标题 | title | - | +| 爆文类型 | viral_type | - | +| 视频链接 | video_url | - | +| 新增A3率 | new_a3_rate | - | +| 看后搜人数 | after_view_search_uv | - | +| 回搜次数 | return_search_cnt | - | +| 自然曝光数 | natural_play_cnt | 计算用 | +| 加热曝光数 | heated_play_cnt | - | +| 总曝光数 | total_play_cnt | 计算用 | +| 总互动 | total_interact | - | +| 点赞 | like_cnt | - | +| 转发 | share_cnt | - | +| 评论 | comment_cnt | - | +| 合作行业ID | industry_id | - | +| 合作行业 | industry_name | - | +| 合作品牌ID | brand_id | 需调用品牌API | +| 合作品牌 | brand_name | 从品牌API获取 | +| 发布时间 | publish_time | - | +| 达人昵称 | star_nickname | 索引字段 | +| 达人unique_id | star_unique_id | 索引字段 | +| 预估视频价格 | estimated_video_cost | 计算用 | +| 预估自然CPM | (计算字段) | = estimated_video_cost / natural_play_cnt * 1000 | +| 预估自然看后搜人数 | (计算字段) | = natural_play_cnt / total_play_cnt * after_view_search_uv | +| 预估自然看后搜人数成本 | (计算字段) | = estimated_video_cost / 预估自然看后搜人数 | + +### C. 参考文档 + +- RequirementsDoc.md diff --git a/doc/RequirementsDoc.md b/doc/RequirementsDoc.md new file mode 100644 index 0000000..b9bde2a --- /dev/null +++ b/doc/RequirementsDoc.md @@ -0,0 +1,65 @@ +[text](README.md)# KOL Insight + +云图 KOL 数据查询与分析工具。 + +## 功能 + +- 批量查询 KOL 视频数据 +- 支持星图ID、达人unique_id、达人昵称搜索 +- 计算预估自然CPM、看后搜成本等指标 +- 数据导出 + +## 技术栈 + +- **前端/后端**: Next.js (App Router) +- **数据库**: PostgreSQL +- **部署**: Docker / PM2 + +## 快速开始 + +```bash +# 安装依赖 +pnpm install + +# 配置环境变量 +cp .env.example .env.local + +# 开发模式 +pnpm dev + +# 构建 +pnpm build + +# 生产运行 +pnpm start +``` + +## 环境变量 + +```env +DATABASE_URL=postgresql://user:password@host:5432/yuntu_kol +``` + +## License + +MIT + +查询输入:批量 星图id(精准匹配) 或 达人unique_id (精准匹配) 或达人昵称(包含匹配) + + +星图ID → 匹配 star_id 字段 +达人unique_id → 匹配 star_unique_id 字段 +达人昵称 → 模糊匹配 star_nickname 字段 + +输出: +中文名 视频ID 视频标题 爆文类型 视频链接 新增A3率 看后搜次数 回搜次数 自然曝光数 加热曝光数 总曝光数 总互动 点赞 转发 评论 合作行业ID 合作行业 合作品牌ID 合作品牌 发布时间 达人昵称 达人unique_id 预估视频价格 预估自然CPM 预估自然看后搜 预估自然看后搜成本 +指标名 item_id title + +合作品牌要使用合作品牌id 调用另一个API查找 +https://api.internal.intelligrow.cn/docs#/%E4%BA%91%E5%9B%BE/get_yuntu_cookies_v1_yuntu_get_cookie_get +/v1/yuntu/brands/{brand_id} + +预估自然CPM =estimated_video_cost / natural_play_cnt *1000 +预估自然看后搜人数 = natural_play_cnt / total_play_cnt * after_view_search_uv +预估自然看后搜人数成本 = estimated_video_cost /预估自然看后搜人数 + diff --git a/doc/UIDesign.md b/doc/UIDesign.md new file mode 100644 index 0000000..acb7b66 --- /dev/null +++ b/doc/UIDesign.md @@ -0,0 +1,850 @@ +# KOL Insight - UI 设计文档 + +## 文档信息 + +| 项目 | 内容 | +|------|------| +| 版本 | v1.0 | +| 创建日期 | 2026-01-28 | +| 来源文档 | DevelopmentPlan.md, PRD.md, FeatureSummary.md | +| 品牌主体 | 麦秒思AI制作 | + +## 1. 设计概述 + +### 1.1 设计原则 + +**麦秒思AI设计语言** + +| 原则 | 说明 | 应用 | +|------|------|------| +| 优雅简洁 | 去除冗余元素,聚焦核心功能 | 单页应用,扁平化设计 | +| 专业可信 | 体现数据分析的专业性 | 稳重色系,清晰的信息层级 | +| 高效直观 | 减少用户学习成本 | 明确的操作流程,即时反馈 | +| 品牌一致 | 强化麦秒思AI品牌形象 | 统一使用品牌标识和色彩 | + +**品牌元素** + +- **Logo**: doc/ui/muse.svg (麦秒思AI品牌标识) +- **Slogan**: "麦秒思AI制作" (展示在关键位置) +- **色调**: 专业、现代、科技感 + +### 1.2 页面总览 + +| 页面ID | 页面名称 | 描述 | 对应功能 | 优先级 | +|--------|----------|------|----------|--------| +| P-001 | 数据查询主页 | 单页应用,包含查询、展示、导出功能 | F-001~F-009 | P0 | + +### 1.3 页面导航图 + +``` + ┌─────────────────────┐ + │ 数据查询主页 │ + │ P-001 │ + │ (单页应用) │ + └──────────┬──────────┘ + │ + │ 页面内交互 + │ + ┌───────────────────┼───────────────────┐ + │ │ │ + ▼ ▼ ▼ + ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ + │ 查询区域 │ │ 结果区域 │ │ 导出操作 │ + │ (顶部) │ │ (主体) │ │ (右上角) │ + └─────────────┘ └─────────────┘ └─────────────┘ +``` + +**说明**: KOL Insight 采用单页应用设计,所有功能集成在一个页面内,通过区域划分组织功能。 + +## 2. 页面设计 + +### 2.1 P-001: 数据查询主页 + +**页面信息** + +| 属性 | 值 | +|------|-----| +| 页面ID | P-001 | +| 对应功能 | F-001(星图ID查询), F-002(达人ID查询), F-003(昵称模糊查询), F-004~F-009(计算、展示、导出) | +| 入口 | 直接访问 (首页) | +| 出口 | 无 (单页应用) | +| 布局类型 | 垂直布局,从上到下依次为:品牌头部 → 查询区 → 结果区 | + +**【必须】页面布局 - ASCII 原型图** + +``` +┌────────────────────────────────────────────────────────────────────────────┐ +│ ┌──────────────────────────────────────────────────────────────────────┐ │ +│ │ Header (品牌头部) │ │ +│ │ ┌──────┐ [麦秒思AI制作] │ │ +│ │ │ MUSE │ KOL Insight - 云图数据查询分析 │ │ +│ │ │ Logo │ (品牌标识 + 产品名称) │ │ +│ │ └──────┘ │ │ +│ └──────────────────────────────────────────────────────────────────────┘ │ +├────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────────────────────────────────────────────────────────────┐ │ +│ │ 查询区域 (Query Section) │ │ +│ │ │ │ +│ │ 查询方式: ( • ) 星图ID ( ) 达人unique_id ( ) 达人昵称 │ │ +│ │ │ │ +│ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ +│ │ │ 输入查询内容... │ │ │ +│ │ │ (支持批量输入,每行一个ID或输入昵称关键词) │ │ │ +│ │ │ │ │ │ +│ │ │ │ │ │ +│ │ └─────────────────────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ [清空] [开始查询] │ │ +│ │ │ │ +│ └──────────────────────────────────────────────────────────────────────┘ │ +│ │ +├────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────────────────────────────────────────────────────────────┐ │ +│ │ 结果区域 (Results Section) │ │ +│ │ │ │ +│ │ 查询结果 (共 128 条) [导出Excel] [导出CSV]│ │ +│ │ │ │ +│ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ +│ │ │ 视频ID │ 标题 │ 达人 │ 自然曝光 │ CPM │ 看后搜成本 │ ... │ 操作 │ │ │ +│ │ ├─────────────────────────────────────────────────────────────────┤ │ │ +│ │ │ 12345 │ XXX │ @XX │ 100.2K │12.5 │ 8.3 │ ... │ [链接]│ │ │ +│ │ │ 12346 │ YYY │ @YY │ 85.3K │15.2 │ 9.8 │ ... │ [链接]│ │ │ +│ │ │ 12347 │ ZZZ │ @ZZ │ 92.1K │13.8 │ 7.5 │ ... │ [链接]│ │ │ +│ │ │ ... │ ... │ ... │ ... │ ... │ ... │ ... │ ... │ │ │ +│ │ │ ... │ ... │ ... │ ... │ ... │ ... │ ... │ ... │ │ │ +│ │ └─────────────────────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ ◀ 上一页 1 / 10 下一页 ▶ │ │ +│ │ │ │ +│ └──────────────────────────────────────────────────────────────────────┘ │ +│ │ +├────────────────────────────────────────────────────────────────────────────┤ +│ ┌──────────────────────────────────────────────────────────────────────┐ │ +│ │ Footer │ │ +│ │ © 2026 麦秒思AI制作 | KOL Insight v1.0 │ │ +│ └──────────────────────────────────────────────────────────────────────┘ │ +└────────────────────────────────────────────────────────────────────────────┘ +``` + +**组件清单** + +| 组件ID | 组件名称 | 类型 | 说明 | 交互 | +|--------|----------|------|------|------| +| C-001 | 品牌头部 | Header | 展示麦秒思AI品牌Logo和产品名称 | 静态展示 | +| C-002 | 查询方式选择器 | Radio Group | 三种查询方式单选 | 点击切换查询方式 | +| C-003 | 查询输入框 | Textarea | 批量输入或昵称输入 | 文本输入 | +| C-004 | 查询按钮组 | Button Group | 清空、开始查询 | 点击执行操作 | +| C-005 | 结果表格 | Table | 展示26个字段的数据 | 横向滚动,列排序 | +| C-006 | 导出按钮组 | Button Group | 导出Excel/CSV | 点击触发下载 | +| C-007 | 分页器 | Pagination | 翻页控制 | 点击切换页码 | +| C-008 | 视频链接 | Link | 跳转到原视频 | 新窗口打开 | +| C-009 | Footer | Footer | 版权信息和品牌声明 | 静态展示 | + +**交互说明** + +| 触发 | 动作 | 结果 | +|------|------|------| +| 选择查询方式 | 切换 Radio | 输入框提示文案变化 | +| 输入查询条件 | 文本输入 | 实时验证输入格式 | +| 点击"清空" | 清空输入 | 输入框清空,结果区隐藏 | +| 点击"开始查询" | 提交查询 | 显示 Loading → 展示结果表格 | +| 点击表头 | 排序 | 按列排序数据 | +| 点击"导出Excel/CSV" | 触发下载 | 浏览器下载文件 | +| 点击视频链接 | 跳转 | 新窗口打开视频页面 | +| 点击分页 | 切换页 | 加载对应页数据 | + +**页面状态** + +| 状态 | 说明 | 展示 | +|------|------|------| +| 默认态 | 页面初始加载 | 查询区可用,结果区显示引导文案 | +| 输入态 | 用户正在输入 | 输入框聚焦,显示输入提示 | +| 查询中 | 提交查询后等待响应 | 按钮禁用,显示 Loading 动画 | +| 结果态 | 查询成功返回数据 | 展示结果表格和分页 | +| 空结果态 | 查询无匹配数据 | 显示空状态插图和提示 | +| 错误态 | 查询失败或网络错误 | 显示错误提示和重试按钮 | + +**默认态原型** + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 查询区域 │ +│ [查询方式选择器] │ +│ [输入框 - 待输入] │ +│ [清空] [开始查询] │ +└─────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────┐ +│ 结果区域 │ +│ │ +│ ┌─────────────┐ │ +│ │ 搜索图标 │ │ +│ └─────────────┘ │ +│ │ +│ 请选择查询方式并输入查询条件 │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +**空结果态原型** + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 结果区域 │ +│ │ +│ ┌─────────────┐ │ +│ │ 空盒子图标 │ │ +│ └─────────────┘ │ +│ │ +│ 未找到匹配数据 │ +│ │ +│ 请调整查询条件后重新尝试 │ +│ │ +│ [修改查询条件] │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +**查询中状态原型** + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 结果区域 │ +│ │ +│ ┌─────────────┐ │ +│ │ ⟳ Loading │ │ +│ └─────────────┘ │ +│ │ +│ 正在查询数据,请稍候... │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +**错误态原型** + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 结果区域 │ +│ │ +│ ┌─────────────┐ │ +│ │ ⚠ 错误图标 │ │ +│ └─────────────┘ │ +│ │ +│ 查询失败,请重试 │ +│ │ +│ 可能原因:网络异常或数据库连接失败 │ +│ │ +│ [重新查询] │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## 3. 用户流程 + +### 3.1 核心流程:批量查询 KOL 数据 + +**【必须】流程图展示用户操作流程:** + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ 打开页面 │ ──▶ │ 选择查询 │ ──▶ │ 输入查询 │ ──▶ │ 提交查询 │ +│ P-001 │ │ 方式 │ │ 条件 │ │ │ +└─────────────┘ └─────────────┘ └─────────────┘ └──────┬──────┘ + │ + ▼ + ┌─────────────┐ + │ 查询处理 │ + │ (后端) │ + └──────┬──────┘ + │ + ┌─────────────────────┼─────────────────────┐ + │ 成功 │ 失败 + ▼ ▼ + ┌─────────────┐ ┌─────────────┐ + │ 展示结果 │ │ 显示错误 │ + │ (结果表格) │ │ (重试) │ + └──────┬──────┘ └─────────────┘ + │ + │ + ┌───────────────────────┼───────────────────────┐ + │ │ │ + ▼ ▼ ▼ + ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ + │ 浏览数据 │ │ 点击视频 │ │ 导出数据 │ + │ (翻页/排序)│ │ 链接 │ │ (Excel/CSV)│ + └──────┬──────┘ └─────────────┘ └─────────────┘ + │ + ▼ + ┌─────────────┐ + │ 继续查询 │ + │ 或离开 │ + └─────────────┘ +``` + +**流程步骤详解** + +| 步骤 | 页面/区域 | 用户操作 | 系统响应 | 预期时间 | +|------|----------|----------|----------|----------| +| 1 | P-001 | 打开页面 | 加载默认态,显示引导文案 | < 2s | +| 2 | 查询区 | 选择查询方式(Radio) | 输入框提示文案更新 | 即时 | +| 3 | 查询区 | 输入查询条件(Textarea) | 实时验证,显示字符数 | 即时 | +| 4 | 查询区 | 点击"开始查询"按钮 | 按钮禁用,显示 Loading | 即时 | +| 5 | 后端 | 提交查询请求 | 查询数据库,计算指标 | < 3s | +| 6a | 结果区 | 查询成功 | 展示结果表格,显示数据条数 | < 0.5s | +| 6b | 结果区 | 查询失败 | 显示错误提示和重试按钮 | < 0.5s | +| 7 | 结果区 | 浏览数据(滚动/翻页) | 加载更多数据或切换页 | < 0.5s | +| 8 | 结果区 | 点击视频链接 | 新窗口打开视频页面 | 即时 | +| 9 | 结果区 | 点击导出按钮 | 生成文件,触发下载 | < 5s | + +### 3.2 辅助流程:修改查询条件 + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ 查看结果 │ ──▶ │ 点击"清空" │ ──▶ │ 输入框清空 │ +│ (不满意) │ │ 按钮 │ │ (重新输入) │ +└─────────────┘ └─────────────┘ └──────┬──────┘ + │ + ▼ + ┌─────────────┐ + │ 重新查询 │ + │ │ + └─────────────┘ +``` + +### 3.3 异常流程:错误处理 + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ 提交查询 │ ──▶ │ 网络异常/ │ ──▶ │ 显示错误 │ +│ │ │ 数据库错误 │ │ 提示 │ +└─────────────┘ └─────────────┘ └──────┬──────┘ + │ + ▼ + ┌─────────────┐ + │ 点击"重新 │ + │ 查询"按钮 │ + └──────┬──────┘ + │ + ▼ + ┌─────────────┐ + │ 重试查询 │ + │ │ + └─────────────┘ +``` + +## 4. 组件规范 + +### 4.1 基础组件 + +**Button 按钮** + +``` +主按钮 (Primary): +┌────────────────┐ +│ 开始查询 │ (品牌主色背景,白色文字) +└────────────────┘ +:hover → 加深 10% +:active → 加深 20% + +次按钮 (Secondary): +┌────────────────┐ +│ 清空 │ (白色背景,边框,灰色文字) +└────────────────┘ +:hover → 灰色背景 +:active → 深灰背景 + +导出按钮 (Action): +┌────────────────┐ +│ 导出Excel ▼ │ (绿色背景,白色文字) +└────────────────┘ + +禁用态 (Disabled): +┌────────────────┐ +│ 查询中... │ (灰色背景,禁止点击) +└────────────────┘ +``` + +**Input/Textarea 输入框** + +``` +默认态: +┌────────────────────────────────────────┐ +│ 请输入星图ID,每行一个... │ +└────────────────────────────────────────┘ + +聚焦态: +┌────────────────────────────────────────┐ +│ 12345▊ │ (蓝色边框高亮) +└────────────────────────────────────────┘ + +错误态: +┌────────────────────────────────────────┐ +│ (输入内容) │ (红色边框) +└────────────────────────────────────────┘ +⚠ 请输入有效的ID格式 +``` + +**Radio 单选框** + +``` +未选中: ( ) 星图ID +选中: (•) 星图ID (品牌主色圆点) +禁用: ( ) 星图ID (灰色文字) +``` + +**Table 表格** + +``` +┌──────────┬──────────┬──────────┬──────────┬──────────┐ +│ 视频ID ▲ │ 标题 │ 达人 │ 自然曝光 │ CPM │ (表头:深色背景) +├──────────┼──────────┼──────────┼──────────┼──────────┤ +│ 12345 │ 测试标题 │ @测试 │ 100.2K │ 12.5 │ (奇数行:白色) +├──────────┼──────────┼──────────┼──────────┼──────────┤ +│ 12346 │ 测试标题 │ @测试 │ 85.3K │ 15.2 │ (偶数行:浅灰) +└──────────┴──────────┴──────────┴──────────┴──────────┘ + +:hover 行 → 浅蓝色背景高亮 +``` + +**Pagination 分页器** + +``` +◀ 上一页 1 2 [3] 4 5 下一页 ▶ + +当前页: [3] → 品牌主色背景 +其他页: 2 → 可点击,悬停高亮 +禁用页: ◀ → 灰色,不可点击 +``` + +**Loading 加载动画** + +``` +方案1: 旋转圆环 + ┌─⟳─┐ + │ │ 正在加载... + └───┘ + +方案2: 骨架屏 (表格数据加载) +┌──────────┬──────────┬──────────┐ +│ ░░░░░░ │ ░░░░░░ │ ░░░░░░ │ +│ ░░░░░░ │ ░░░░░░ │ ░░░░░░ │ +│ ░░░░░░ │ ░░░░░░ │ ░░░░░░ │ +└──────────┴──────────┴──────────┘ +``` + +### 4.2 业务组件 + +**QueryForm 查询表单组件** + +``` +┌──────────────────────────────────────────────────────┐ +│ 查询方式: (•) 星图ID ( ) 达人unique_id ( ) 昵称 │ +│ │ +│ ┌────────────────────────────────────────────────┐ │ +│ │ 请输入星图ID,每行一个... │ │ +│ │ │ │ +│ │ │ │ +│ └────────────────────────────────────────────────┘ │ +│ │ +│ [清空] [开始查询] │ +└──────────────────────────────────────────────────────┘ + +功能: +• 支持三种查询方式切换 +• 根据查询方式动态更新输入提示 +• 输入验证(实时) +• 清空和提交操作 +``` + +**ResultTable 结果表格组件** + +``` +┌──────────────────────────────────────────────────────────────┐ +│ 查询结果 (共 128 条) [导出Excel] [导出CSV] │ +│ │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ 26个字段的数据表格 (可横向滚动) │ │ +│ │ • 视频ID、标题、达人、曝光数据、互动数据 │ │ +│ │ • 预估CPM、预估看后搜人数、预估看后搜成本 │ │ +│ │ • 品牌信息、发布时间等 │ │ +│ └────────────────────────────────────────────────────────┘ │ +│ │ +│ ◀ 上一页 1 / 10 下一页 ▶ │ +└──────────────────────────────────────────────────────────────┘ + +功能: +• 展示26个字段 +• 支持列排序 +• 横向滚动(表格宽度超出) +• 分页展示(每页20条) +• 数字格式化(千分位) +• 视频链接可点击 +``` + +**EmptyState 空状态组件** + +``` +┌─────────────────────────────────────────┐ +│ │ +│ ┌─────────────┐ │ +│ │ 📦 空盒子 │ │ +│ └─────────────┘ │ +│ │ +│ 未找到匹配数据 │ +│ │ +│ 请调整查询条件后重新尝试 │ +│ │ +│ [修改查询条件] │ +│ │ +└─────────────────────────────────────────┘ +``` + +**ErrorState 错误状态组件** + +``` +┌─────────────────────────────────────────┐ +│ │ +│ ┌─────────────┐ │ +│ │ ⚠ 错误 │ │ +│ └─────────────┘ │ +│ │ +│ 查询失败,请重试 │ +│ │ +│ 可能原因:网络异常或数据库连接失败 │ +│ │ +│ [重新查询] │ +│ │ +└─────────────────────────────────────────┘ +``` + +## 5. 设计规范 + +### 5.1 色彩规范 + +**麦秒思AI品牌色系** + +| 用途 | 色值 | 示例 | 说明 | +|------|------|------|------| +| 主色 (Primary) | #4F46E5 | 主按钮、链接、选中态 | 品牌核心色,专业科技感 | +| 主色浅 | #818CF8 | 悬停态、次要元素 | 主色的衍生色 | +| 主色深 | #3730A3 | 按下态、强调元素 | 主色的深色变体 | +| 成功 (Success) | #10B981 | 成功提示、导出按钮 | 绿色系 | +| 警告 (Warning) | #F59E0B | 警告提示 | 橙色系 | +| 错误 (Error) | #EF4444 | 错误提示、删除操作 | 红色系 | +| 信息 (Info) | #3B82F6 | 信息提示 | 蓝色系 | +| 文字主色 | #111827 | 标题、正文 | 深灰色,易读 | +| 文字次色 | #6B7280 | 辅助文字、提示 | 中灰色 | +| 文字禁用 | #D1D5DB | 禁用文字 | 浅灰色 | +| 背景主色 | #FFFFFF | 页面背景 | 纯白 | +| 背景次色 | #F9FAFB | 卡片背景、表格奇数行 | 浅灰白 | +| 边框色 | #E5E7EB | 输入框边框、分隔线 | 浅灰 | + +**色彩使用示例** + +``` +主按钮: ████████ #4F46E5 (主色) +导出按钮: ████████ #10B981 (成功色) +错误提示: ████████ #EF4444 (错误色) +文字主色: ████████ #111827 +背景色: ████████ #FFFFFF +``` + +### 5.2 字体规范 + +**字体家族** + +| 平台 | 字体 | 备用 | +|------|------|------| +| 中文 | PingFang SC, Microsoft YaHei | sans-serif | +| 英文 | Inter, -apple-system, BlinkMacSystemFont | sans-serif | +| 数字 | Tabular Nums (等宽数字) | - | + +**字号规范** + +| 用途 | 字号 | 字重 | 行高 | 示例 | +|------|------|------|------|------| +| 页面标题 | 28px | Bold (700) | 1.3 | KOL Insight | +| 区域标题 | 20px | Semibold (600) | 1.4 | 查询结果 | +| 小标题 | 16px | Medium (500) | 1.5 | 查询方式 | +| 正文 | 14px | Regular (400) | 1.6 | 表格内容、按钮文字 | +| 辅助文字 | 12px | Regular (400) | 1.5 | 提示文案、标签 | +| 表格数据 | 14px | Regular (400) | 1.5 | 数据单元格 | + +### 5.3 间距规范 + +**基础间距单位: 4px** + +| 间距 | 值 | 用途 | +|------|-----|------| +| xs | 4px | 紧凑元素间距 | +| sm | 8px | 小间距,如图标与文字 | +| md | 16px | 标准间距,组件内部 | +| lg | 24px | 大间距,区域之间 | +| xl | 32px | 特大间距,页面区块 | +| 2xl | 48px | 超大间距,顶部底部留白 | + +**组件间距示例** + +``` +页面整体: padding: 32px (xl) + +┌────────────────────────────────┐ +│ [32px 顶部留白] │ +│ ┌──────────────────────────┐ │ +│ │ 查询区域 │ │ +│ └──────────────────────────┘ │ +│ [24px 区域间距] │ +│ ┌──────────────────────────┐ │ +│ │ 结果区域 │ │ +│ └──────────────────────────┘ │ +│ [32px 底部留白] │ +└────────────────────────────────┘ + +按钮组: gap: 8px (sm) +[清空] [8px] [开始查询] +``` + +### 5.4 圆角规范 + +| 元素 | 圆角值 | 说明 | +|------|--------|------| +| 按钮 | 6px | 适中圆角,现代感 | +| 输入框 | 6px | 与按钮保持一致 | +| 卡片 | 8px | 稍大圆角,柔和 | +| 表格 | 8px | 整体表格边框 | +| Badge/Tag | 12px | 较大圆角,标签感 | + +### 5.5 阴影规范 + +| 用途 | 阴影值 | 使用场景 | +|------|--------|----------| +| 轻微阴影 | 0 1px 3px rgba(0,0,0,0.1) | 卡片、输入框 | +| 标准阴影 | 0 4px 6px rgba(0,0,0,0.1) | 悬浮元素 | +| 深度阴影 | 0 10px 15px rgba(0,0,0,0.1) | 模态框、下拉菜单 | + +### 5.6 响应式断点 + +| 断点 | 宽度 | 布局说明 | +|------|------|----------| +| Mobile | < 768px | 单栏布局,表格横向滚动 | +| Tablet | 768px - 1024px | 优化表格列宽 | +| Desktop | 1024px - 1440px | 标准布局 | +| Large Desktop | > 1440px | 居中固定宽度或适当扩展 | + +**响应式布局示例** + +``` +Desktop (> 1024px): +┌────────────────────────────────────────┐ +│ [查询区域 - 全宽] │ +│ [结果表格 - 全宽,所有列可见] │ +└────────────────────────────────────────┘ + +Tablet (768px - 1024px): +┌──────────────────────────────┐ +│ [查询区域 - 全宽] │ +│ [结果表格 - 隐藏部分次要列] │ +└──────────────────────────────┘ + +Mobile (< 768px): +┌──────────────────┐ +│ [查询区域] │ +│ [结果卡片列表] │ (表格转为卡片布局) +│ [卡片1] │ +│ [卡片2] │ +└──────────────────┘ +``` + +## 6. 品牌应用 + +### 6.1 品牌标识使用 + +**Logo 使用规范** + +| 位置 | 尺寸 | 说明 | +|------|------|------| +| Header 左侧 | 高度 40px | 麦秒思AI Logo (doc/ui/muse.svg) | +| Favicon | 32x32px | 简化版 Logo 图标 | +| 加载动画 | - | 可选:Logo 动效 | + +**品牌声明位置** + +- Header 右上角:"麦秒思AI制作" +- Footer 中央:"© 2026 麦秒思AI制作 | KOL Insight v1.0" + +**Header 品牌区域详细设计** + +``` +┌────────────────────────────────────────────────────────────────┐ +│ ┌──────┐ │ +│ │ │ KOL Insight 麦秒思AI制作 │ +│ │ MUSE │ 云图数据查询分析 │ +│ │ Logo │ (产品名称 + Slogan) (品牌声明) │ +│ │ │ │ +│ └──────┘ │ +│ [40px] [16px] [产品名称:20px Bold] [14px Regular] │ +└────────────────────────────────────────────────────────────────┘ +``` + +### 6.2 空状态插图风格 + +- 使用简洁的线条插图 +- 色彩与品牌主色保持一致 +- 插图风格:扁平化、现代、专业 + +## 7. 动效规范 + +### 7.1 过渡动效 + +| 交互 | 动效 | 时长 | 缓动函数 | +|------|------|------|----------| +| 按钮悬停 | 背景色变化 | 150ms | ease-out | +| 输入框聚焦 | 边框高亮 | 200ms | ease-in-out | +| 页面切换 | 淡入淡出 | 300ms | ease-in-out | +| 表格排序 | 淡入 | 200ms | ease-out | +| 模态框打开 | 缩放+淡入 | 250ms | cubic-bezier(0.4, 0, 0.2, 1) | + +### 7.2 加载动画 + +``` +方案: 旋转圆环 +┌─────┐ +│ ⟳ │ (360度旋转,1s 一圈,无限循环) +└─────┘ + +或: 品牌色脉动 +┌─────┐ +│ ● │ (主色圆点,缩放脉动) +└─────┘ +``` + +## 8. 数据展示规范 + +### 8.1 数字格式化 + +| 数据类型 | 格式 | 示例 | +|----------|------|------| +| 整数 | 千分位分隔 | 1,234,567 | +| 小数 | 保留2位 | 12.34 | +| 大数值 | K/M 缩写 | 100.2K, 1.5M | +| 百分比 | % 符号 | 85.3% | +| 金额 | ¥ + 千分位 | ¥ 1,234.56 | +| 日期 | YYYY-MM-DD | 2026-01-28 | + +### 8.2 表格列定义 + +**完整的26个输出字段** + +| 序号 | 中文名 | 字段宽度 | 对齐方式 | 格式化 | +|------|--------|----------|----------|--------| +| 1 | 视频ID | 120px | 左对齐 | 文本 | +| 2 | 视频标题 | 200px | 左对齐 | 文本,超出省略 | +| 3 | 爆文类型 | 100px | 居中 | Badge | +| 4 | 视频链接 | 100px | 居中 | 链接按钮 | +| 5 | 新增A3率 | 100px | 右对齐 | 百分比 | +| 6 | 看后搜人数 | 120px | 右对齐 | 千分位 | +| 7 | 回搜次数 | 100px | 右对齐 | 千分位 | +| 8 | 自然曝光数 | 120px | 右对齐 | K/M 缩写 | +| 9 | 加热曝光数 | 120px | 右对齐 | K/M 缩写 | +| 10 | 总曝光数 | 120px | 右对齐 | K/M 缩写 | +| 11 | 总互动 | 100px | 右对齐 | 千分位 | +| 12 | 点赞 | 100px | 右对齐 | 千分位 | +| 13 | 转发 | 100px | 右对齐 | 千分位 | +| 14 | 评论 | 100px | 右对齐 | 千分位 | +| 15 | 合作行业ID | 120px | 左对齐 | 文本 | +| 16 | 合作行业 | 120px | 左对齐 | 文本 | +| 17 | 合作品牌ID | 120px | 左对齐 | 文本 | +| 18 | 合作品牌 | 150px | 左对齐 | 文本 | +| 19 | 发布时间 | 120px | 居中 | YYYY-MM-DD | +| 20 | 达人昵称 | 120px | 左对齐 | 文本 | +| 21 | 达人unique_id | 150px | 左对齐 | 文本 | +| 22 | 预估视频价格 | 120px | 右对齐 | ¥ + 千分位 | +| 23 | 预估自然CPM | 120px | 右对齐 | 小数2位 | +| 24 | 预估自然看后搜人数 | 150px | 右对齐 | 小数2位 | +| 25 | 预估自然看后搜人数成本 | 180px | 右对齐 | ¥ + 小数2位 | + +**表格列优先级** (移动端渐进隐藏) + +- P0 (必显): 视频ID、标题、达人、自然曝光、CPM、看后搜成本 +- P1 (平板可隐藏): 爆文类型、看后搜人数、总互动、品牌 +- P2 (移动端隐藏): 其他详细指标 + +### 8.3 空值处理 + +| 场景 | 显示 | +|------|------| +| 字段为 null | "-" | +| 计算结果为 0 | "0" | +| 除零错误 | "-" | +| API 获取失败 | 显示原始 ID | + +## 9. 可访问性 (Accessibility) + +| 规范 | 说明 | +|------|------| +| 语义化 HTML | 使用正确的 HTML 标签 | +| 键盘导航 | 支持 Tab 键切换焦点 | +| ARIA 标签 | 为交互元素添加 aria-label | +| 颜色对比度 | 文字与背景对比度 ≥ 4.5:1 | +| 焦点可见性 | 聚焦元素显示明显边框 | + +## 10. 性能优化 + +| 优化项 | 说明 | +|--------|------| +| 图片优化 | Logo 使用 SVG,支持 Retina | +| 表格虚拟滚动 | 大数据量时使用虚拟滚动 | +| 懒加载 | 分页数据按需加载 | +| 防抖节流 | 搜索输入使用防抖 | + +## 11. 设计交付物 + +| 交付物 | 说明 | +|--------|------| +| UIDesign.md | 本文档 | +| doc/ui/muse.svg | 品牌 Logo 文件 | +| 组件规范 | 可复用的 React 组件 | +| 样式变量 | CSS/Tailwind 配置文件 | + +--- + +## 附录 A: 设计检查清单 + +生成 UIDesign 后,请确认以下项目: + +- [x] 覆盖 DevelopmentPlan 所有功能模块 +- [x] 页面导航图清晰展示页面关系 +- [x] 每个页面都有 ASCII 原型图 +- [x] 原型图展示了完整的页面结构 +- [x] 用户流程有流程图 +- [x] 每个页面都有状态说明 (默认/加载/空/错误) +- [x] 组件清单完整 +- [x] 交互说明清晰 +- [x] 设计规范统一 (色彩/字体/间距) +- [x] 品牌元素应用 (Logo/Slogan) +- [x] 响应式设计考虑 +- [x] 数据展示规范 (26个字段) + +## 附录 B: 与开发对接 + +**开发实现优先级** + +1. P0: 核心布局和查询功能 (T-005~T-009) +2. P1: 结果展示和导出功能 (T-010~T-011) +3. P2: 视频链接跳转和细节优化 (T-014) + +**关键设计决策** + +- **单页应用**: 简化交互流程,提升用户体验 +- **品牌强化**: 多处展示"麦秒思AI制作",建立品牌认知 +- **数据优先**: 核心是数据展示,UI 简洁不干扰 +- **响应式**: 支持桌面/平板/移动端访问 + +--- + +**文档版本**: v1.0 +**最后更新**: 2026-01-28 +**设计团队**: 麦秒思AI +**审核状态**: 待审核 (建议运行 `/ru` 进行评审) diff --git a/doc/review-FeatureSummary-claude.md b/doc/review-FeatureSummary-claude.md new file mode 100644 index 0000000..ba4f248 --- /dev/null +++ b/doc/review-FeatureSummary-claude.md @@ -0,0 +1,229 @@ +# FeatureSummary 评审报告 + +## 概要 + +| 项目 | 内容 | +|------|------| +| 评审时间 | 2026-01-28 21:45 | +| 目标文档 | doc/FeatureSummary.md | +| 参照文档 | doc/PRD.md | +| 问题统计 | 0 个严重 / 1 个一般 / 2 个建议 | + +## 覆盖度分析 + +### 功能模块覆盖 + +| PRD 功能模块 | FeatureSummary 对应 | 状态 | +|--------------|---------------------|------| +| 数据查询模块(3个功能) | 2.1节 F-001~F-003 | ✅ 完全覆盖 | +| 数据计算模块(3个功能) | 2.2节 F-004~F-006 | ✅ 完全覆盖 | +| 数据展示模块(2个功能) | 2.3节 F-007~F-008 | ✅ 完全覆盖 | +| 数据导出模块(1个功能) | 2.4节 F-009 | ✅ 完全覆盖 | + +**覆盖率**: 9/9 完全覆盖 ✅ + +### 用户故事关联 + +| 用户故事 | FeatureSummary 功能 | 状态 | +|----------|---------------------|------| +| US-001 | F-001 星图ID查询 | ✅ 正确关联 | +| US-002 | F-002 达人ID查询 | ✅ 正确关联 | +| US-003 | F-003 昵称模糊查询 | ✅ 正确关联 | +| US-004 | F-004, F-005, F-006 计算功能 | ✅ 正确关联 | +| US-005 | F-009 数据导出 | ✅ 正确关联 | +| US-006 | F-007 结果列表展示 | ✅ 正确关联 | +| US-007 | F-008 视频链接跳转 | ✅ 正确关联 | + +**关联准确率**: 7/7 完全正确 ✅ + +### 优先级一致性 + +| PRD 优先级 | PRD 功能 | FeatureSummary 功能 | 状态 | +|------------|----------|---------------------|------| +| P0 | 3个查询功能 | F-001, F-002, F-003 | ✅ 一致 | +| P0 | 3个计算功能 | F-004, F-005, F-006 | ✅ 一致 | +| P1 | 结果展示 | F-007 | ✅ 一致 | +| P1 | 数据导出 | F-009 | ✅ 一致 | +| P2 | 视频链接跳转 | F-008 | ✅ 一致 | + +**优先级一致性**: 100% ✅ + +## 结构完整性检查 + +### 必要章节检查 + +| 章节 | 要求 | 状态 | 评价 | +|------|------|------|------| +| 1.1 功能统计 | 必须 | ✅ | 统计数据准确(4个模块,9个功能) | +| 1.2 功能架构图 | 必须 | ✅ | 清晰展示4个核心模块+外部依赖 | +| 1.3 模块依赖关系 | 必须 | ✅ | 依赖关系清晰,流程合理 | +| 2. 功能清单 | 必须 | ✅ | 9个功能全部列出,契约详情完整 | +| 3. 功能依赖矩阵 | 必须 | ✅ | 矩阵完整,依赖关系明确 | +| 4. 功能流程图 | 必须 | ✅ | 核心流程+计算流程,可视化清晰 | +| 5. 版本规划 | 必须 | ✅ | MVP + v1.1 规划合理 | +| 6. 接口契约预览 | 必须 | ✅ | 列出核心API端点 | + +### 功能契约详情检查 + +| 功能ID | 触发条件 | 输入 | 处理逻辑 | 输出 | 异常情况 | 边界说明 | 状态 | +|--------|---------|------|---------|------|---------|---------|------| +| F-001 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ 完整 | +| F-002 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ 完整 | +| F-003 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ 完整 | +| F-004 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ 完整 | +| F-005 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ 完整 | +| F-006 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ 完整 | +| F-007 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ 完整 | +| F-008 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ 完整 | +| F-009 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ 完整 | + +**完整性**: 9/9 功能契约详情完整 ✅ + +## 问题清单 + +### 严重问题 (Critical) + +> 必须修复,否则影响后续文档生成 + +**无严重问题** ✅ + +### 一般问题 (Major) + +> 建议修复,可提升文档质量 + +1. **[位置: [doc/FeatureSummary.md:196](doc/FeatureSummary.md#L196)]** 品牌API调用细节不够明确 + - 问题:F-007 的处理逻辑中提到"调用品牌API获取品牌名称",但未明确: + - 何时调用品牌API?(展示时实时调用 / 后端查询时调用) + - 如何批量处理?(逐个调用 / 批量调用) + - 缓存策略是什么? + - 建议:在功能契约详情中补充品牌API调用的时机和策略说明 + - 影响:开发人员可能对品牌API集成时机理解不一致 + +### 改进建议 (Minor) + +> 可选优化项 + +1. **[位置: [doc/FeatureSummary.md:47](doc/FeatureSummary.md#L47)]** 模块依赖关系图可优化 + - 建议:在 1.3 模块依赖关系图中,品牌API作为外部依赖,其连接线指向"数据展示模块"更合理(当前连接位置不够明确) + - 当前图示品牌API连接到数据计算模块下方,但实际是在展示阶段使用 + - 优化后可以避免理解偏差 + +2. **[位置: [doc/FeatureSummary.md:367](doc/FeatureSummary.md#L367)]** 接口契约预览可补充品牌API + - 建议:在 6. 接口契约预览中,补充品牌API的完整契约: + ``` + | F-007 (品牌) | 外部API | GET /v1/yuntu/brands/{brand_id} | 获取品牌名称 | + ``` + - 理由:品牌API是关键外部依赖,应在接口契约预览中明确列出 + +## 质量评估 + +### 文档规范性 + +| 评估项 | 状态 | 评价 | +|--------|------|------| +| 功能ID格式统一 | ✅ | F-001 ~ F-009 格式规范 | +| 表格格式规范 | ✅ | 所有表格格式统一、清晰 | +| 层级结构清晰 | ✅ | 章节层级合理,易于阅读 | +| 术语使用一致 | ✅ | 与PRD术语完全一致 | +| 可视化图表完整 | ✅ | 5个必要图表全部包含 | + +### 内容准确性 + +| 评估项 | 状态 | 评价 | +|--------|------|------| +| 功能描述准确 | ✅ | 所有功能描述与PRD一致 | +| 计算公式准确 | ✅ | F-004~F-006 公式与PRD完全一致 | +| 异常处理完整 | ✅ | 每个功能都考虑了异常情况 | +| 边界说明清晰 | ✅ | 明确了数量限制、匹配方式等边界 | +| 依赖关系正确 | ✅ | 功能依赖矩阵逻辑正确 | + +### 开发价值 + +| 评估项 | 状态 | 评价 | +|--------|------|------| +| 功能契约可执行 | ✅ | 契约详情足够明确,可直接指导开发 | +| 接口设计合理 | ✅ | API设计符合RESTful规范 | +| 版本规划清晰 | ✅ | MVP范围明确,v1.1规划合理 | +| 依赖关系明确 | ✅ | 有助于制定开发顺序 | + +## 亮点总结 + +### 文档优势 + +1. **契约详情非常完整** ⭐⭐⭐ + - 每个功能的输入/输出/异常/边界都有详细说明 + - 异常处理考虑周全(除零、空输入、API失败等) + - 边界说明明确(批量数量限制、匹配方式等) + +2. **可视化图表丰富** ⭐⭐⭐ + - 功能架构图清晰展示了4个核心模块 + - 模块依赖关系图直观展示了数据流向 + - 功能依赖矩阵完整标注了9个功能的依赖关系 + - 核心业务流程图和计算流程图帮助理解系统运作 + +3. **版本规划合理** ⭐⭐ + - MVP聚焦核心功能(P0 + P1) + - v1.1 包含增强功能(P2) + - 功能划分符合敏捷开发原则 + +4. **与PRD高度一致** ⭐⭐⭐ + - 功能覆盖率 100% + - 优先级完全一致 + - 用户故事关联准确 + - 术语使用统一 + +## 评审结论 + +**通过** ✅ + +### 结论说明 + +FeatureSummary 文档质量优秀,完全符合「功能契约」文档的定位和要求: + +1. **覆盖度**: 100% 覆盖 PRD 的所有功能需求,无遗漏 +2. **一致性**: 与 PRD 的功能描述、优先级、用户故事完全一致 +3. **完整性**: 所有必要章节、图表、契约详情均完整 +4. **准确性**: 计算公式、处理逻辑、异常处理准确无误 +5. **可执行性**: 功能契约详情明确,可直接指导开发工作 + +仅有1个一般问题(品牌API调用细节)和2个改进建议,不影响文档整体质量和可用性。建议在进入 DevelopmentPlan 阶段时,对品牌API集成策略进行详细设计。 + +### 下一步行动 + +- [ ] **可选**:补充 F-007 中品牌API的调用时机和策略说明 +- [ ] **可选**:优化 1.3 模块依赖关系图中品牌API的连接位置 +- [ ] **可选**:在接口契约预览中补充品牌API契约 +- [ ] **推荐**:继续进入下一阶段文档生成(UIDesign 或 DevelopmentPlan) + +--- + +## 附录:功能统计对比 + +### PRD vs FeatureSummary 功能对照表 + +| PRD 章节 | PRD 功能点 | FeatureSummary 功能 | 功能ID | 优先级 | 用户故事 | +|----------|-----------|---------------------|--------|--------|----------| +| 3.2.1 | 星图ID查询 | 星图ID查询 | F-001 | P0 | US-001 | +| 3.2.1 | 达人ID查询 | 达人ID查询 | F-002 | P0 | US-002 | +| 3.2.1 | 昵称模糊查询 | 昵称模糊查询 | F-003 | P0 | US-003 | +| 3.2.2 | 预估自然CPM | 预估自然CPM计算 | F-004 | P0 | US-004 | +| 3.2.2 | 预估自然看后搜人数 | 预估自然看后搜人数计算 | F-005 | P0 | US-004 | +| 3.2.2 | 预估自然看后搜人数成本 | 预估自然看后搜人数成本计算 | F-006 | P0 | US-004 | +| 3.2.3 | 结果列表展示 | 结果列表展示 | F-007 | P1 | US-006 | +| 3.2.3 | 视频链接跳转 | 视频链接跳转 | F-008 | P2 | US-007 | +| 3.2.4 | 数据导出 | Excel/CSV导出 | F-009 | P1 | US-005 | + +**统计**: +- PRD 功能点:9 个 +- FeatureSummary 功能:9 个 +- 匹配率:9/9 = 100% + +### 计算公式准确性验证 + +| 功能 | PRD 公式 | FeatureSummary 公式 | 状态 | +|------|---------|---------------------|------| +| F-004 | `estimated_video_cost / natural_play_cnt * 1000` | `estimated_video_cost / natural_play_cnt * 1000` | ✅ 一致 | +| F-005 | `natural_play_cnt / total_play_cnt * after_view_search_uv` | `natural_play_cnt / total_play_cnt * after_view_search_uv` | ✅ 一致 | +| F-006 | `estimated_video_cost / 预估自然看后搜人数` | `estimated_video_cost / 预估自然看后搜人数` | ✅ 一致 | + +**结论**:所有计算公式与 PRD 完全一致 ✅ diff --git a/doc/review-PRD-claude.md b/doc/review-PRD-claude.md new file mode 100644 index 0000000..975f0bc --- /dev/null +++ b/doc/review-PRD-claude.md @@ -0,0 +1,176 @@ +# PRD 评审报告 + +## 概要 + +| 项目 | 内容 | +|------|------| +| 评审时间 | 2026-01-28 21:30 | +| 目标文档 | doc/PRD.md | +| 参照文档 | doc/RequirementsDoc.md | +| 问题统计 | 3 个严重 / 2 个一般 / 3 个建议 | + +## 一致性检查 + +### 需求覆盖分析 + +| RequirementsDoc 需求项 | PRD 对应位置 | 状态 | +|------------------------|--------------|------| +| 批量查询 KOL 视频数据 | US-001, US-002, US-003 | ✅ 已覆盖 | +| 支持星图ID精准查询 | US-001 / 3.2.1节 | ✅ 已覆盖 | +| 支持达人unique_id精准查询 | US-002 / 3.2.1节 | ✅ 已覆盖 | +| 支持达人昵称模糊查询 | US-003 / 3.2.1节 | ✅ 已覆盖 | +| 计算预估自然CPM | US-004 / 3.2.2节 | ⚠️ 部分覆盖(缺少公式) | +| 计算预估自然看后搜 | US-004 / 3.2.2节 | ⚠️ 部分覆盖(缺少公式) | +| 计算预估自然看后搜成本 | US-004 / 3.2.2节 | ⚠️ 部分覆盖(缺少公式) | +| 数据导出 | US-005 / 3.2.4节 | ✅ 已覆盖 | +| 调用品牌API获取品牌名称 | - | ❌ 未覆盖 | +| 技术栈:Next.js + PostgreSQL | 7.1节 | ✅ 已覆盖 | +| 部署方式:Docker / PM2 | 7.1节 | ✅ 已覆盖 | + +### 差异说明 + +PRD 新增但 RequirementsDoc 未明确提及的内容: +1. **用户角色定义**(2.1节):细化了"运营人员"和"投放优化师"两个角色,这是对原始需求的合理扩展 +2. **用户旅程设计**(2.3节):可视化了用户操作流程,增强了需求理解 +3. **里程碑规划**(第8章):增加了MVP和v1.1两个阶段规划 +4. **风险评估**(第9章):识别了数据同步延迟、性能问题等风险 + +**评估**: 以上新增内容均为合理的 PRD 扩展,有助于后续开发和实施。 + +## 问题清单 + +### 严重问题 (Critical) + +> 必须修复,否则影响后续文档生成 + +1. **[位置: [doc/PRD.md:199](doc/PRD.md#L199)]** 缺少外部品牌API依赖说明 + - 现状:PRD 第6章"接口需求"中仅提及 PostgreSQL,未说明需要调用外部品牌API + - 与 RequirementsDoc 的差异:RequirementsDoc 明确说明"合作品牌要使用合作品牌id 调用另一个API查找 https://api.internal.intelligrow.cn/docs#/%E4%BA%91%E5%9B%BE/get_yuntu_cookies_v1_yuntu_get_cookie_get /v1/yuntu/brands/{brand_id}" + - 建议:在 6.1 外部接口表格中增加品牌API行: + ``` + | 品牌API | 根据品牌ID获取品牌名称 | https://api.internal.intelligrow.cn/v1/yuntu/brands/{brand_id} | + ``` + +2. **[位置: [doc/PRD.md:273](doc/PRD.md#L273)]** 输出字段映射不完整 + - 现状:附录B中大部分输出字段的"字段名"列标记为"-",缺少数据库字段映射 + - 影响:开发人员无法知道如何从数据库获取这些数据 + - 建议:补全所有输出字段对应的数据库字段名,参考 RequirementsDoc 中的字段定义 + +3. **[位置: [doc/PRD.md:114](doc/PRD.md#L114)]** 计算公式缺失 + - 现状:3.2.2节"数据计算模块"仅描述了功能,未给出具体计算公式 + - 与 RequirementsDoc 的差异:RequirementsDoc 明确了三个公式: + - 预估自然CPM = estimated_video_cost / natural_play_cnt * 1000 + - 预估自然看后搜人数 = natural_play_cnt / total_play_cnt * after_view_search_uv + - 预估自然看后搜人数成本 = estimated_video_cost / 预估自然看后搜人数 + - 建议:在 3.2.2 节的"描述"列或"验收标准"列中添加完整的计算公式 + +### 一般问题 (Major) + +> 建议修复,可提升文档质量 + +1. **[位置: [doc/PRD.md:119](doc/PRD.md#L119)]** 术语不一致 + - 问题:PRD 使用"预估自然看后搜",RequirementsDoc 使用"预估自然看后搜人数" + - 影响:可能导致理解偏差(是次数还是人数) + - 建议:统一术语为"预估自然看后搜人数",与计算公式中使用的 after_view_search_uv(用户数)概念一致 + +2. **[位置: [doc/PRD.md:168](doc/PRD.md#L168)]** 数据模型与计算公式不匹配 + - 问题:5.1节数据模型中未列出计算公式所需的关键字段: + - estimated_video_cost(预估视频价格)- 已在5.2中提及 + - natural_play_cnt(自然曝光数)- 已在5.1中提及 + - total_play_cnt(总曝光数)- 已在5.1中提及 + - after_view_search_uv(看后搜人数)- 未明确提及字段名 + - 建议:在 5.1 数据模型图中补充这些关键字段的明确字段名 + +### 改进建议 (Minor) + +> 可选优化项 + +1. **[位置: [doc/PRD.md:262](doc/PRD.md#L262)]** 术语表可补充 + - 建议:在术语表中补充以下术语: + - natural_play_cnt:自然曝光次数 + - estimated_video_cost:预估视频价格 + - after_view_search_uv:看后搜用户数 + +2. **[位置: [doc/PRD.md:164](doc/PRD.md#L164)]** 数据需求可细化 + - 建议:补充数据库表名(如 videos 或 kol_videos) + - 建议:补充关键字段的索引建议(star_id, star_unique_id, star_nickname) + +3. **[位置: [doc/PRD.md:208](doc/PRD.md#L208)]** 内部接口设计 + - 建议:虽然标注"待补充",但可以在此阶段先列出核心API端点: + - POST /api/query - 批量查询接口 + - GET /api/export - 数据导出接口 + +## 用户故事评估 + +| 评估项 | 结果 | +|--------|------| +| 用户故事总数 | 7 个 | +| 符合格式规范 | 7 / 7 ✅ | +| 有验收标准 | 7 / 7 ✅ | +| 关联功能点 | 7 / 7 ✅ | +| 优先级划分 | 明确(P0/P1/P2)✅ | + +### 用户故事质量评价 + +**优点**: +- ✅ 所有用户故事都有唯一ID(US-001 ~ US-007) +- ✅ 格式规范,符合"作为{角色},我想要{功能},以便{价值}"结构 +- ✅ 验收标准清晰、可测试 +- ✅ 优先级划分合理,核心查询和计算功能为P0 + +**无明显问题** + +## 评审结论 + +**需修改后通过** + +### 结论说明 + +PRD 整体质量较好,功能需求覆盖完整,用户故事设计规范,文档结构清晰。但存在以下关键问题需要修复: + +1. **外部API依赖缺失**:未说明需要调用品牌API获取品牌名称,这是实现完整功能的必要依赖 +2. **计算公式缺失**:开发人员需要明确的计算公式来实现预估指标 +3. **字段映射不完整**:输出字段与数据库字段的映射关系不明确 + +修复上述3个严重问题后,PRD 可以作为下一阶段(FeatureSummary、UIDesign、DevelopmentPlan)的基础文档。 + +### 下一步行动 + +- [ ] **必须**:在 6.1 节补充品牌API外部接口说明 +- [ ] **必须**:在 3.2.2 节补充完整的计算公式 +- [ ] **必须**:在附录B中补全所有输出字段的数据库字段名 +- [ ] **建议**:统一"预估自然看后搜"术语为"预估自然看后搜人数" +- [ ] **建议**:在 5.1 数据模型中明确标注计算公式所需字段的字段名 + +--- + +## 附录:RequirementsDoc 原始需求对照 + +为便于对比,以下是 RequirementsDoc 中的核心需求要点: + +### 功能需求 +- 批量查询 KOL 视频数据 +- 支持星图ID、达人unique_id、达人昵称搜索 +- 计算预估自然CPM、看后搜成本等指标 +- 数据导出 + +### 查询规则 +- 星图ID → 匹配 star_id 字段(精准匹配) +- 达人unique_id → 匹配 star_unique_id 字段(精准匹配) +- 达人昵称 → 模糊匹配 star_nickname 字段(包含匹配) + +### 计算公式 +``` +预估自然CPM = estimated_video_cost / natural_play_cnt * 1000 +预估自然看后搜人数 = natural_play_cnt / total_play_cnt * after_view_search_uv +预估自然看后搜人数成本 = estimated_video_cost / 预估自然看后搜人数 +``` + +### 外部依赖 +- 品牌API: `/v1/yuntu/brands/{brand_id}` +- 用途:根据合作品牌ID查询品牌名称 + +### 技术栈 +- Next.js (App Router) +- PostgreSQL +- Docker / PM2 diff --git a/doc/review-UIDesign-claude.md b/doc/review-UIDesign-claude.md new file mode 100644 index 0000000..319ac2d --- /dev/null +++ b/doc/review-UIDesign-claude.md @@ -0,0 +1,268 @@ +# UIDesign 评审报告 + +## 概要 + +| 项目 | 内容 | +|------|------| +| 评审时间 | 2026-01-28 22:00 | +| 目标文档 | doc/UIDesign.md | +| 参照文档 | doc/DevelopmentPlan.md | +| 问题统计 | 1 个严重 / 2 个一般 / 3 个建议 | + +## 页面覆盖分析 + +### 功能模块 vs UI 页面映射 + +| DevelopmentPlan 功能模块 | UIDesign 页面/区域 | 状态 | 说明 | +|-------------------------|-------------------|------|------| +| F-001 星图ID查询 | P-001 查询区域 | ✅ 完全覆盖 | 查询方式选择器 + 输入框 | +| F-002 达人ID查询 | P-001 查询区域 | ✅ 完全覆盖 | 查询方式选择器 + 输入框 | +| F-003 昵称模糊查询 | P-001 查询区域 | ✅ 完全覆盖 | 查询方式选择器 + 输入框 | +| F-004 预估自然CPM计算 | P-001 结果区域 | ✅ 完全覆盖 | 后端计算,表格展示 | +| F-005 预估自然看后搜人数计算 | P-001 结果区域 | ✅ 完全覆盖 | 后端计算,表格展示 | +| F-006 预估自然看后搜人数成本计算 | P-001 结果区域 | ✅ 完全覆盖 | 后端计算,表格展示 | +| F-007 结果列表展示 | P-001 结果区域 | ✅ 完全覆盖 | ResultTable 组件(C-005) | +| F-008 视频链接跳转 | P-001 结果区域 | ✅ 完全覆盖 | 视频链接组件(C-008) | +| F-009 Excel/CSV导出 | P-001 导出按钮 | ✅ 完全覆盖 | 导出按钮组(C-006) | +| F-010 品牌名称批量获取 | P-001 结果区域 | ⚠️ 部分覆盖 | 后端处理,前端展示,但页面功能对应中未明确列出 | + +**覆盖率**: 10/10 功能完全覆盖 ✅ + +**说明**: +- 所有功能模块都在单页应用(P-001)中覆盖 +- 采用单页应用设计,通过区域划分组织功能,符合开发计划 +- F-010 品牌API集成功能在后端处理,前端仅展示品牌名称,但在页面功能对应描述中未明确列出 + +### 开发任务 vs UI 组件映射 + +| DevelopmentPlan 任务 | UIDesign 组件/设计 | 状态 | +|---------------------|-------------------|------| +| T-005 查询 API 开发 | C-002/C-003/C-004 查询组件 | ✅ 匹配 | +| T-006 计算逻辑实现 | 表格数据列(CPM等计算字段) | ✅ 匹配 | +| T-007 品牌 API 批量集成 | 表格"合作品牌"列 | ✅ 匹配 | +| T-008 查询表单组件 | QueryForm 业务组件 | ✅ 匹配 | +| T-009 结果表格组件 | ResultTable 业务组件 | ✅ 匹配 | +| T-010 导出 API 开发 | C-006 导出按钮组 | ✅ 匹配 | +| T-011 导出按钮组件 | C-006 导出按钮组 | ✅ 匹配 | +| T-014 视频链接跳转 | C-008 视频链接组件 | ✅ 匹配 | + +**匹配率**: 8/8 完全匹配 ✅ + +## 设计一致性检查 + +| 检查项 | 结果 | 说明 | +|--------|------|------| +| 页面命名规范 | ✅ | P-001 格式规范,清晰明确 | +| 组件命名规范 | ✅ | C-001~C-009 格式统一 | +| 布局风格统一 | ✅ | 垂直布局,从上到下:Header → 查询区 → 结果区 → Footer | +| 交互模式一致 | ✅ | 查询 → 展示 → 导出流程清晰 | +| 状态覆盖完整 | ✅ | 默认态、输入态、查询中、结果态、空结果态、错误态 | +| 品牌元素应用 | ✅ | 麦秒思AI Logo、Slogan、品牌色系统一应用 | +| 设计规范完整 | ✅ | 色彩、字体、间距、圆角、阴影规范完整 | +| 响应式设计 | ✅ | 考虑了 Mobile/Tablet/Desktop 三种断点 | + +## 问题清单 + +### 严重问题 (Critical) + +> 必须修复,否则影响开发实施 + +1. **[位置: [doc/UIDesign.md:68](doc/UIDesign.md#L68)]** 页面功能对应中缺少 F-010 品牌API集成 + - 现状:页面信息表格中"对应功能"列仅列出了 F-001~F-009,缺少 F-010 + - 与 DevelopmentPlan 的差异:DevelopmentPlan 明确了 F-010 是 P0 优先级的核心功能,且 T-007 任务专门负责品牌API批量集成 + - 影响:开发人员可能不清楚品牌名称字段需要在后端调用品牌API获取 + - 建议:在"对应功能"列中补充 F-010,并在页面说明中明确品牌名称的获取方式 + - 修复方案: + ```markdown + | 对应功能 | F-001(星图ID查询), F-002(达人ID查询), F-003(昵称模糊查询), F-004~F-006(计算), F-007~F-009(展示、导出), F-010(品牌API集成) | + ``` + +### 一般问题 (Major) + +> 建议修复,可提升设计清晰度 + +1. **[位置: [doc/UIDesign.md:740](doc/UIDesign.md#L740)]** 表格列定义中品牌字段说明不明确 + - 问题:8.2 节"表格列定义"中,第18列"合作品牌"(字段宽度150px)没有说明数据来源 + - 与 FeatureSummary 的差异:FeatureSummary 明确了 F-010 功能会在后端批量调用品牌API获取品牌名称 + - 影响:前端开发人员可能认为品牌名称直接来自数据库,而不知道需要后端预处理 + - 建议:在"合作品牌"行的"格式化"列中补充说明"后端通过品牌API获取" + - 修复方案: + ```markdown + | 18 | 合作品牌 | 150px | 左对齐 | 文本(后端批量调用品牌API获取) | + ``` + +2. **[位置: [doc/UIDesign.md:775](doc/UIDesign.md#L775)]** 空值处理中API失败说明不够明确 + - 问题:8.3 节"空值处理"中,"API 获取失败 → 显示原始 ID",未明确指的是品牌API + - 影响:可能与其他API混淆 + - 建议:明确说明是品牌API失败时的降级策略 + - 修复方案: + ```markdown + | 品牌API获取失败 | 显示原始品牌ID | + ``` + +### 改进建议 (Minor) + +> 可选优化项,提升设计完整性 + +1. **[位置: [doc/UIDesign.md:145](doc/UIDesign.md#L145)]** 交互说明可补充品牌数据处理说明 + - 建议:在"交互说明"表格中补充一行: + ```markdown + | 查询完成 | 后端批量获取品牌名称 | 品牌名称填充到表格"合作品牌"列 | + ``` + - 理由:明确品牌数据的获取时机和展示位置 + +2. **[位置: [doc/UIDesign.md:196](doc/UIDesign.md#L196)]** 结果表格组件功能描述可补充品牌API说明 + - 建议:在 4.2 节"ResultTable 结果表格组件"功能列表中补充: + ```markdown + • 品牌名称由后端调用品牌API获取,前端直接展示 + ``` + - 理由:帮助前端开发人员理解品牌名称字段的特殊性 + +3. **[位置: [doc/UIDesign.md:831](doc/UIDesign.md#L831)]** 开发实现优先级可同步 DevelopmentPlan + - 建议:附录B"开发实现优先级"中补充 T-007 品牌API集成任务 + - 当前: + ```markdown + 1. P0: 核心布局和查询功能 (T-005~T-009) + ``` + - 建议改为: + ```markdown + 1. P0: 核心布局和查询功能 (T-005~T-009,含 T-007 品牌API批量集成) + ``` + - 理由:与 DevelopmentPlan 保持一致,T-007 是 P0 优先级任务 + +## 设计质量评估 + +### 文档完整性 + +| 评估项 | 状态 | 评价 | +|--------|------|------| +| 页面列表完整 | ✅ | 单页应用,P-001 覆盖所有功能 | +| 页面布局 ASCII 原型图 | ✅ | 清晰展示页面结构 | +| 页面状态说明 | ✅ | 6种状态全部覆盖(默认/输入/查询中/结果/空/错误) | +| 组件清单完整 | ✅ | 9个组件全部列出(C-001~C-009) | +| 交互说明清晰 | ✅ | 8种交互场景全部说明 | +| 用户流程图 | ✅ | 核心流程、辅助流程、异常流程全部包含 | +| 设计规范统一 | ✅ | 色彩、字体、间距、圆角、阴影规范完整 | +| 品牌元素应用 | ✅ | 麦秒思AI Logo、Slogan、品牌色完整应用 | +| 数据展示规范 | ✅ | 26个字段完整列出,格式化规则明确 | +| 响应式设计 | ✅ | Mobile/Tablet/Desktop 三种断点考虑 | + +### 设计亮点 + +1. **单页应用设计** ⭐⭐⭐ + - 所有功能集成在一个页面内,通过区域划分组织功能 + - 简化用户操作流程,无需页面跳转 + - 符合开发计划的技术架构(Next.js App Router) + +2. **品牌一致性强** ⭐⭐⭐ + - 麦秒思AI品牌元素贯穿整个设计 + - Logo、Slogan、品牌色系统一应用 + - Header 和 Footer 强化品牌认知 + +3. **状态覆盖全面** ⭐⭐⭐ + - 6种页面状态全部考虑(默认/输入/查询中/结果/空/错误) + - 每种状态都有 ASCII 原型图展示 + - 错误态提供明确的重试引导 + +4. **设计规范完整** ⭐⭐⭐ + - 色彩、字体、间距、圆角、阴影规范详细 + - 提供了具体的数值和示例 + - 便于开发人员实现统一的视觉效果 + +5. **数据展示规范明确** ⭐⭐ + - 26个字段完整列出,每个字段都有宽度、对齐、格式化规则 + - 空值处理、数字格式化规则清晰 + - 提供了表格列优先级(移动端渐进隐藏) + +### 与开发计划的契合度 + +| 契合项 | 评价 | 说明 | +|--------|------|------| +| 技术栈匹配 | ✅ | 单页应用符合 Next.js App Router 架构 | +| 功能模块对齐 | ✅ | 所有功能模块(F-001~F-010)都有对应的UI设计 | +| 开发任务匹配 | ✅ | UI组件与开发任务(T-005~T-016)一一对应 | +| API 设计契合 | ✅ | 查询、导出功能与 API 设计一致 | +| 数据库字段对应 | ✅ | 26个输出字段与数据库 Schema 一致 | +| 性能优化考虑 | ✅ | 表格虚拟滚动、懒加载、防抖节流都有考虑 | + +## 用户体验评估 + +| 评估项 | 评分 | 说明 | +|--------|------|------| +| 学习成本 | ⭐⭐⭐⭐⭐ | 单页应用,操作流程简单直观 | +| 操作效率 | ⭐⭐⭐⭐⭐ | 批量查询、一键导出,效率高 | +| 错误提示 | ⭐⭐⭐⭐ | 错误态有明确提示和重试引导 | +| 视觉层次 | ⭐⭐⭐⭐⭐ | 查询区 → 结果区层次清晰 | +| 品牌认知 | ⭐⭐⭐⭐⭐ | 多处展示麦秒思AI品牌元素 | +| 响应式体验 | ⭐⭐⭐⭐ | 考虑了移动端适配 | + +## 评审结论 + +**需修改后通过** ⚠️ + +### 结论说明 + +UIDesign 文档整体质量优秀,设计完整、规范统一、品牌一致性强,与 DevelopmentPlan 的契合度高。但存在以下关键问题需要修复: + +1. **功能对应不完整**:页面功能对应中缺少 F-010 品牌API集成功能,可能导致开发人员对品牌名称字段的数据来源理解不清 +2. **品牌字段说明不明确**:表格列定义和空值处理中,品牌字段的特殊处理(后端调用品牌API)说明不够明确 + +修复上述问题后,UIDesign 可以作为前端开发的完整设计指南。 + +**优点总结**: +- ✅ 单页应用设计合理,操作流程简洁高效 +- ✅ 品牌元素应用完整,强化麦秒思AI品牌认知 +- ✅ 设计规范详细,便于开发实现 +- ✅ 状态覆盖全面,用户体验考虑周到 +- ✅ 与开发计划高度契合 + +**需要改进**: +- ⚠️ 补充 F-010 品牌API集成功能的UI说明 +- ⚠️ 明确品牌名称字段的数据来源和处理方式 + +### 下一步行动 + +**必须完成**: +- [ ] 在页面信息表格"对应功能"列中补充 F-010 +- [ ] 在表格列定义中明确"合作品牌"字段的数据来源(后端品牌API) +- [ ] 在空值处理中明确"品牌API获取失败"的降级策略 + +**建议完成**: +- [ ] 在交互说明中补充品牌数据处理时机 +- [ ] 在 ResultTable 组件功能描述中补充品牌API说明 +- [ ] 在开发实现优先级中同步 T-007 品牌API集成任务 + +**后续工作**: +- [ ] 开发人员根据 UIDesign 开始前端实现 +- [ ] 设计师提供 Logo 和品牌素材(doc/ui/muse.svg) +- [ ] 前端实现后进行 UI 还原度验收 + +--- + +## 附录:设计检查清单对照 + +根据 UIDesign.md 附录A"设计检查清单",逐项对照: + +| 检查项 | 状态 | 说明 | +|--------|------|------| +| ✅ 覆盖 DevelopmentPlan 所有功能模块 | ✅ | F-001~F-010 全部覆盖 | +| ✅ 页面导航图清晰展示页面关系 | ✅ | 单页应用,导航图清晰 | +| ✅ 每个页面都有 ASCII 原型图 | ✅ | P-001 有完整原型图 | +| ✅ 原型图展示了完整的页面结构 | ✅ | Header → 查询区 → 结果区 → Footer | +| ✅ 用户流程有流程图 | ✅ | 核心流程、辅助流程、异常流程 | +| ✅ 每个页面都有状态说明 | ✅ | 6种状态全部说明 | +| ✅ 组件清单完整 | ✅ | C-001~C-009 | +| ✅ 交互说明清晰 | ✅ | 8种交互场景 | +| ✅ 设计规范统一 | ✅ | 色彩/字体/间距完整 | +| ✅ 品牌元素应用 | ✅ | Logo/Slogan/品牌色 | +| ✅ 响应式设计考虑 | ✅ | Mobile/Tablet/Desktop | +| ✅ 数据展示规范 | ✅ | 26个字段完整 | + +**检查清单完成度**: 12/12 = 100% ✅ + +--- + +**文档版本**: v1.0 +**评审人**: Claude +**审核状态**: 需修改后通过 +**下次评审**: 修复关键问题后重新评审 diff --git a/doc/review-tasks-claude.md b/doc/review-tasks-claude.md new file mode 100644 index 0000000..98c1e74 --- /dev/null +++ b/doc/review-tasks-claude.md @@ -0,0 +1,754 @@ +# Tasks 评审报告 + +## 概要 + +| 项目 | 内容 | +|------|------| +| 评审时间 | 2026-01-28 15:30 | +| 目标文档 | [doc/tasks.md](doc/tasks.md) | +| 参照文档 | [doc/UIDesign.md](doc/UIDesign.md), [doc/DevelopmentPlan.md](doc/DevelopmentPlan.md) | +| 问题统计 | **4 个严重 / 6 个一般 / 5 个建议** | +| 评审结论 | 🟡 **需修改后通过** | + +## 覆盖度分析 + +### DevelopmentPlan 覆盖 + +#### Phase 1: 基础架构搭建 + +| 开发项 (DevelopmentPlan) | 对应任务 (tasks.md) | 状态 | 说明 | +|---------------------------|---------------------|------|------| +| T-001 前端项目初始化 + T-002 后端项目初始化 | **T-001 项目初始化** | ⚠️ | **合并为一个任务,粒度过大** | +| T-003 数据库配置 | T-002 数据库配置 | ✅ | 完全覆盖,含TDD要求 | +| T-004 基础 UI 框架 | T-003 基础 UI 框架 | ✅ | 完全覆盖,含品牌元素 | +| T-005 环境变量配置 | T-004 环境变量配置 | ✅ | 完全覆盖 | + +#### Phase 2: 核心功能开发 + +| 开发项 (DevelopmentPlan) | 对应任务 (tasks.md) | 状态 | 说明 | +|---------------------------|---------------------|------|------| +| T-006 查询 API 开发 (后端) | **T-005 查询 API 开发** | ✅ | 含TDD要求和100%覆盖率 | +| T-007 计算逻辑实现 (后端) | **T-006 计算逻辑实现** | ✅ | 含TDD要求和100%覆盖率 | +| T-008 品牌 API 批量集成 (后端) | **T-007 品牌 API 批量集成** | ✅ | 含TDD要求和100%覆盖率 | +| T-009 导出 API 开发 (后端) | **T-010 导出 API 开发** | ⚠️ | **依赖T-009前端组件,不合理** | +| T-010 查询表单组件 (前端) | T-008 查询表单组件 | ✅ | 标注"粗略实现" | +| T-011 结果表格组件 (前端) | T-009 结果表格组件 | ✅ | 标注"粗略实现" | +| T-012 导出按钮组件 (前端) | T-011 导出按钮组件 | ✅ | 标注"粗略实现" | +| **(未在 DevelopmentPlan 中)** | **T-012 主页面集成** | ⚠️ | **新增任务,导致编号错位** | + +#### Phase 3: 优化与测试 + +| 开发项 (DevelopmentPlan) | 对应任务 (tasks.md) | 状态 | 说明 | +|---------------------------|---------------------|------|------| +| T-013 错误处理 (前后端) | **T-013 错误处理** | ❌ | **编号错位** | +| T-014 性能优化 (后端) | **T-014 性能优化** | ❌ | **编号错位** | +| T-015 视频链接跳转 (前端) | **T-015 视频链接跳转** | ❌ | **编号错位** | +| T-016 部署配置 (前后端) | **T-016 部署配置** | ❌ | **编号错位** | +| T-017 集成测试 | **T-017 集成测试** | ❌ | **编号错位** | + +**总覆盖率**: 17/16 (tasks.md 新增1个任务) + +**关键问题**: +1. ❌ **任务编号不一致**: Phase 3 的5个任务编号都向后偏移一位 +2. ⚠️ **T-001 粒度过大**: 前后端初始化合并为一个任务 +3. ⚠️ **T-010 依赖错误**: 后端 API 不应依赖前端组件 T-009 +4. ⚠️ **T-012 新增任务**: DevelopmentPlan 中没有对应项 + +--- + +### UIDesign 覆盖 + +| UI 页面/组件 | 对应任务 | 状态 | 说明 | +|-------------|----------|------|------| +| **P-001: 数据查询主页** | T-012 主页面集成 | ✅ | 单页应用集成 | +| **组件覆盖** | | | | +| C-001: 品牌头部 | T-003 基础 UI 框架 | ✅ | 包含 Logo 和品牌声明 | +| C-002: 查询方式选择器 | T-008 查询表单组件 | ✅ | Radio Group | +| C-003: 查询输入框 | T-008 查询表单组件 | ✅ | Textarea | +| C-004: 查询按钮组 | T-008 查询表单组件 | ✅ | 清空/开始查询 | +| C-005: 结果表格 | T-009 结果表格组件 | ✅ | 26字段表格 | +| C-006: 导出按钮组 | T-011 导出按钮组件 | ✅ | Excel/CSV 导出 | +| C-007: 分页器 | T-009 结果表格组件 | ✅ | 验收标准第9条 | +| C-008: 视频链接 | T-015 视频链接跳转 | ✅ | 新窗口打开 | +| C-009: Footer | T-003 基础 UI 框架 | ✅ | 版权信息 | +| **页面状态** | | | | +| 6种状态 | T-012 主页面集成 | ✅ | 验收标准第6-8条 | + +**总覆盖率**: 10/10 (100%) + +**UI覆盖评价**: ✅ 所有 UI 页面、组件、状态都有对应任务 + +--- + +## 任务质量分析 + +| 检查项 | 通过数 | 总数 | 通过率 | +|--------|--------|------|--------| +| 有明确描述 | 17 | 17 | 100% | +| 有验收标准 | 17 | 17 | 100% | +| 验收标准清晰 | 17 | 17 | 100% | +| 依赖关系明确 | 16 | 17 | 94% | +| 粒度合适 | 16 | 17 | 94% | +| TDD 要求明确 | 7 | 12 | 58% | +| 测试覆盖率要求 | 7 | 12 | 58% | + +**质量问题**: +- ⚠️ **T-001 粒度过大**: 前后端初始化合并,无法并行开发 +- ⚠️ **后端任务 TDD 覆盖不全**: 仅 7/12 的后端任务有明确 TDD 要求 +- ❌ **缺少测试独立任务**: 100% 覆盖率嵌入开发任务,难以单独验收 + +--- + +## 问题清单 + +### 严重问题 (Critical) + +#### C-1: T-001 任务粒度过大,前后端无法并行 +**位置**: [doc/tasks.md:43](doc/tasks.md:43) + +**问题描述**: +```markdown +| T-001 | 项目初始化 | 前后端分离架构:前端 Next.js,后端 FastAPI,配置 TypeScript、ESLint、Prettier | P0 | - | +``` + +T-001 包含: +1. 前端 Next.js 14.x 项目创建 +2. 后端 FastAPI 0.104+ 项目创建 +3. 前端 TypeScript、ESLint、Prettier 配置 +4. 后端 Python 依赖管理配置 +5. 验收标准6条(前端3条+后端3条) + +**影响**: +- 🚫 **无法并行开发**: 前端和后端开发者可能是不同人员,合并为一个任务导致无法同时开工 +- 🚫 **验收标准过多**: 6条验收标准涉及不同技术栈,验收时需要同时检查前后端 +- 🚫 **依赖关系不清晰**: T-002 数据库配置依赖 T-001,但实际只依赖后端部分 + +**建议修复**: +拆分为两个独立任务: +- **T-001A: 前端项目初始化** (依赖: 无) + - 创建 Next.js 14.x 项目 + - 配置 TypeScript、ESLint、Prettier + - 验收: 可运行 `pnpm dev` + +- **T-001B: 后端项目初始化** (依赖: 无) + - 创建 FastAPI 0.104+ 项目 + - 配置 Poetry/pip + - 验收: 可运行 `uvicorn main:app --reload` + +**优点**: +- ✅ 前后端可并行开发,节省时间 +- ✅ 验收标准更聚焦 +- ✅ 依赖关系更清晰(T-002 只依赖 T-001B) + +--- + +#### C-2: T-010 依赖关系错误 +**位置**: [doc/tasks.md:67](doc/tasks.md:67) + +**问题描述**: +```markdown +| T-010 | 导出 API 开发 | ... | P1 | T-006, T-007, T-009 | ... +``` + +T-010 (后端导出 API) 依赖 T-009 (前端结果表格组件),这是**逻辑错误**。 + +**分析**: +- T-010 是**后端 FastAPI** 接口,负责生成 Excel/CSV 文件 +- T-009 是**前端 React** 组件,负责展示表格 +- 后端 API 不应该依赖前端组件的实现 + +**实际依赖**: +- T-010 应该依赖 **T-006 (计算逻辑实现)** 和 **T-007 (品牌API集成)** +- 因为导出的数据需要包含计算后的指标和品牌名称 + +**验收标准第5条**: +``` +5. 使用中文列名作为表头 **(与 T-009 ResultTable 字段一致)** +``` +这说明是要求"字段一致性",而不是"依赖关系"。 + +**影响**: +- 🚫 **执行顺序混乱**: 开发者可能误以为要先完成前端表格才能开发后端导出API +- 🚫 **前后端耦合**: 后端依赖前端,违反分离架构原则 + +**建议修复**: +1. 修改依赖: `T-010 依赖: T-006, T-007` (移除 T-009) +2. 修改验收标准第5条: "使用中文列名作为表头 **(字段顺序和命名与前端 ResultTable 保持一致,参考共享的字段定义)**" +3. 建议: 创建共享的字段定义文件(如 `types/fields.ts`),前后端都引用 + +--- + +#### C-3: 缺少单元测试独立任务 +**位置**: 整个 tasks.md + +**问题描述**: +tasks.md 中有 **7个任务** 要求 TDD 和 100% 测试覆盖率: +- T-002: 数据库配置 (验收标准 7-8 条) +- T-005: 查询 API 开发 (验收标准 9-10 条) +- T-006: 计算逻辑实现 (验收标准 7-8 条) +- T-007: 品牌 API 批量集成 (验收标准 8-9 条) +- T-010: 导出 API 开发 (验收标准 10-11 条) +- T-013: 错误处理 (验收标准 8-9 条) +- T-017: 集成测试 (验收标准 9-11 条) + +但**没有单独的测试任务**,所有测试要求都嵌入在开发任务中。 + +**影响**: +- 🚫 **测试容易被忽略**: 开发进度紧张时,测试可能被压缩或跳过 +- 🚫 **无法单独追踪测试进度**: 测试覆盖率没有独立的验收里程碑 +- 🚫 **100% 覆盖率难以保证**: 嵌入在开发任务中,验收时可能只检查功能,不检查覆盖率 +- 🚫 **测试报告缺失**: T-017 要求生成覆盖率报告,但其他任务没有明确要求 + +**建议修复**: +在 Phase 3 增加测试里程碑任务: + +**方案A: 增加独立测试任务** +```markdown +| T-018 | 测试覆盖率验收 | 验证所有后端代码测试覆盖率 ≥ 100% | P1 | T-002, T-005~007, T-010, T-013 | +验收标准: +1. 数据库操作测试覆盖率 100% (T-002) +2. API集成测试覆盖率 100% (T-005) +3. 计算逻辑单元测试覆盖率 100% (T-006) +4. 品牌API单元测试覆盖率 100% (T-007) +5. 导出功能单元测试覆盖率 100% (T-010) +6. 错误处理分支覆盖率 100% (T-013) +7. 使用 pytest-cov 生成覆盖率报告 +8. 覆盖率报告上传到 CI/CD +``` + +**方案B: 在每个 Phase 结束增加测试验收点** +```markdown +## 3. Phase 2 任务 - 核心功能开发 + +### 3.3 测试验收 +| ID | 任务 | 描述 | 优先级 | 依赖 | 验收标准 | +|----|------|------|--------|------|----------| +| T-012A | Phase 2 测试验收 | 验证 Phase 2 所有后端任务测试覆盖率 | P0 | T-005~007, T-010 | 1. 所有后端代码覆盖率 ≥ 100%
2. 生成覆盖率报告 | +``` + +--- + +#### C-4: 任务编号与 DevelopmentPlan 不一致 +**位置**: Phase 3 所有任务 ([doc/tasks.md:88-101](doc/tasks.md)) + +**问题描述**: +tasks.md 新增了 T-012 (主页面集成),导致 Phase 3 的所有任务编号向后偏移一位: + +| DevelopmentPlan | tasks.md | 差异 | +|-----------------|----------|------| +| T-013 错误处理 | **T-013 错误处理** | ❌ 编号错位 | +| T-014 性能优化 | **T-014 性能优化** | ❌ 编号错位 | +| T-015 视频链接跳转 | **T-015 视频链接跳转** | ❌ 编号错位 | +| T-016 部署配置 | **T-016 部署配置** | ❌ 编号错位 | +| T-017 集成测试 | **T-017 集成测试** | ❌ 编号错位 | + +**影响**: +- 🚫 **文档引用混乱**: 在 DevelopmentPlan 中看到的 T-013 和 tasks.md 中的 T-013 不是同一个任务 +- 🚫 **沟通成本高**: 开发人员需要在两个文档之间切换时手动对照编号 +- 🚫 **代码注释/提交信息错误**: Git 提交信息中的任务 ID 可能指向错误的任务 + +**建议修复**: + +**方案A (推荐): 将 T-012 改为 T-008A** +```markdown +| T-008 | 查询表单组件 | ... | P0 | T-003 | +| T-008A | 主页面集成 | ... | P0 | T-008, T-009, T-011 | +| T-009 | 结果表格组件 | ... | P1 | T-003, T-006, T-007 | +``` +- 优点: Phase 3 编号与 DevelopmentPlan 完全一致 +- 缺点: 引入子编号 + +**方案B: 更新 DevelopmentPlan.md** +在 DevelopmentPlan.md 的 Phase 2 增加 T-012 任务 +- 优点: 保持 tasks.md 不变 +- 缺点: 需要修改 DevelopmentPlan.md + +**方案C: 在 tasks.md 增加对照表** +```markdown +## 附录: 与 DevelopmentPlan 任务编号对照 + +| tasks.md | DevelopmentPlan | 任务名称 | +|----------|-----------------|----------| +| T-013 | T-013 | 错误处理 | +| T-014 | T-014 | 性能优化 | +... +``` +- 优点: 不修改编号,只增加对照表 +- 缺点: 需要手动查表,增加认知负担 + +--- + +### 一般问题 (Major) + +#### M-1: T-002 真实数据库测试要求缺少环境准备说明 +**位置**: [doc/tasks.md:46](doc/tasks.md:46) + +**问题描述**: +```markdown +6. **真实数据库测试**: 使用 .env 中的连接字符串连接真实数据库并验证 +``` + +验收标准要求连接"真实数据库",但没有说明: +- 真实数据库是否已经准备好? +- 数据库中是否有测试数据? +- 需要什么权限? + +**影响**: +- 开发者执行到 T-002 时可能发现数据库环境未就绪 +- 导致任务阻塞,无法继续 + +**建议修复**: +1. 在 T-002 依赖中增加: `依赖: T-001B (后端初始化), 数据库环境准备 (DBA)` +2. 在 T-004 环境变量配置中增加验收标准: "数据库连接字符串配置完成,数据库可访问" +3. 或在任务描述中明确标注: "需提前准备测试数据库环境,包含表结构和测试数据" + +--- + +#### M-2: T-012 主页面集成缺少状态管理方案说明 +**位置**: [doc/tasks.md:85](doc/tasks.md:85) + +**问题描述**: +```markdown +6. 页面状态管理: 默认态/输入态/查询中/结果态/空结果态/错误态 +``` + +验收标准提到"页面状态管理",但没有说明使用何种状态管理方案: +- React useState? +- Zustand? +- Redux Toolkit? +- Context API? + +**影响**: +- 前端开发者需要自行决定状态管理方案 +- 可能导致过度设计(引入 Redux)或过于简单(难以维护) + +**建议修复**: +在验收标准第6条补充说明: +```markdown +6. 页面状态管理: 默认态/输入态/查询中/结果态/空结果态/错误态 **(使用 React useState 管理,无需第三方库)** +``` + +--- + +#### M-3: T-007 品牌API并发限制和超时参数硬编码 +**位置**: [doc/tasks.md:64](doc/tasks.md:64) + +**问题描述**: +```markdown +3. 使用 asyncio.gather 批量并发请求(限制 10 并发) +6. 超时设置: 3秒 +``` + +验收标准硬编码了"10 并发"和"3 秒",未说明这些参数是否可配置。 + +**影响**: +- 生产环境可能需要调整并发数(如品牌API限流时降低并发) +- 超时时间可能需要根据网络环境调整 +- 硬编码参数难以适应不同环境 + +**建议修复**: +1. 将并发限制和超时时间配置到环境变量或配置文件 +2. 修改验收标准: +```markdown +3. 使用 asyncio.gather 批量并发请求,并发数可配置(默认 10) +6. 超时时间可配置(默认 3 秒) +7. 从环境变量读取配置: BRAND_API_CONCURRENCY, BRAND_API_TIMEOUT +``` + +--- + +#### M-4: T-009 与 T-010 字段一致性验证缺失 +**位置**: [doc/tasks.md:76](doc/tasks.md:76) + +**问题描述**: +T-009 (前端表格) 和 T-010 (后端导出) 都要求"使用中文列名",但没有明确如何保证字段一致性。 + +**当前状态**: +- T-009 验收标准: "展示 26 个字段,使用中文列名" +- T-010 验收标准: "使用中文列名作为表头 **(与 T-009 ResultTable 字段一致)**" + +**问题**: +- "字段一致"如何验证? +- 前端和后端是否共享字段定义? + +**影响**: +- 前端展示和导出文件的列名可能不一致 +- 导致用户混淆 + +**建议修复**: +1. 创建共享的字段定义文件: +```typescript +// shared/types/fields.ts +export const VIDEO_FIELDS = [ + { key: 'item_id', label: '视频ID', width: 120 }, + { key: 'title', label: '视频标题', width: 200 }, + // ... 24 more fields +] as const; +``` + +2. 修改 T-009 验收标准: +```markdown +2. 展示 26 个字段,使用共享字段定义文件 (shared/types/fields.ts) +``` + +3. 修改 T-010 验收标准: +```markdown +5. 使用共享字段定义文件作为表头,保证与前端表格字段顺序和命名完全一致 +``` + +--- + +#### M-5: T-014 性能优化缺少性能测试脚本 +**位置**: [doc/tasks.md:96](doc/tasks.md:96) + +**问题描述**: +T-014 定义了明确的性能指标: +- 查询响应时间 ≤ 3秒 (100条) +- 页面加载时间 ≤ 2秒 +- 导出响应时间 ≤ 5秒 (1000条) + +但验收标准只有"验证索引已创建",没有要求编写性能测试脚本。 + +**影响**: +- 性能指标难以自动化验证 +- 依赖人工测试,可能遗漏 +- 回归测试时无法快速验证性能 + +**建议修复**: +增加验收标准: +```markdown +6. **后端性能测试**: 编写性能测试脚本,验证响应时间指标 +7. **真实数据库测试**: 使用真实数据库和测试数据进行性能测试 +8. 性能测试报告: 生成性能测试报告,记录实际响应时间 +``` + +--- + +#### M-6: T-017 集成测试缺少性能测试用例 +**位置**: [doc/tasks.md:101](doc/tasks.md:101) + +**问题描述**: +T-017 集成测试有 8 个功能测试用例,但未包含 T-014 定义的性能指标验证。 + +**建议修复**: +在验收标准中增加性能测试用例: +```markdown +9. 测试用例: 性能指标验证 (查询≤3秒、导出≤5秒) +10. **真实数据库集成测试**: 使用 .env 中的真实数据库连接进行完整集成测试 +11. **后端测试覆盖率验证**: 确认所有后端代码测试覆盖率 ≥ 100% +12. **测试报告生成**: 使用 pytest-cov 生成覆盖率报告 +``` +(注: 验收标准 10-12 已存在,只需增加第9条) + +--- + +### 改进建议 (Minor) + +#### S-1: 前端"粗略实现"说明不够具体 +**位置**: [doc/tasks.md:74, 76, 78, 85](doc/tasks.md) + +**问题描述**: +T-008/T-009/T-011/T-012 都标注了"粗略实现说明",但"粗略"的标准不明确。 + +**建议**: +在任务总览或关键技术点章节定义"粗略实现"标准: +```markdown +## 前端"粗略实现"标准 + +本项目前端采用"功能优先、样式从简"的开发策略: +- ✅ **功能完整**: 所有功能可用,交互流程完整 +- ✅ **样式简洁**: 使用 Tailwind 默认样式,无需过度美化 +- ✅ **品牌元素保留**: Logo、品牌色、品牌声明必须体现 +- ❌ **暂不支持**: 响应式适配、动画效果、深度优化 +``` + +--- + +#### S-2: 建议增加任务估时 +**位置**: 整个 tasks.md + +**问题描述**: +所有任务都没有工作量估时,无法评估项目整体时间和关键路径。 + +**建议**: +在任务总览表格增加"估时"列: +```markdown +| ID | 任务 | 描述 | 优先级 | 依赖 | 估时 | 验收标准 | +|----|------|------|--------|------|------|----------| +| T-001 | 项目初始化 | ... | P0 | - | 1天 | ... | +``` + +**参考估时** (仅供参考): +- T-001: 1天 (前后端分离后: 0.5天 × 2) +- T-002: 1天 +- T-005: 2天 (含 TDD) +- T-009: 2天 +- T-012: 2天 + +--- + +#### S-3: T-016 部署配置缺少监控和日志方案 +**位置**: [doc/tasks.md:99](doc/tasks.md:99) + +**问题描述**: +T-016 部署配置只涉及 Docker 和环境变量,未涉及生产环境监控和日志收集。 + +**建议**: +增加验收标准: +```markdown +8. 日志配置: 前端 console 输出,后端使用 Python logging 模块输出到文件 +9. (可选) 监控配置: 接入 Sentry 或 Prometheus 进行错误监控 +``` + +--- + +#### S-4: 任务依赖图与实际任务ID不一致 +**位置**: [doc/tasks.md:105](doc/tasks.md:105) + +**问题描述**: +第5节"任务依赖图"仍使用 DevelopmentPlan 的任务编号,与 tasks.md 实际任务ID不一致。 + +**建议修复**: +更新任务依赖图,使用 tasks.md 的任务ID (T-001~T-017): +``` +Phase 1: 基础架构 +T-001 (项目初始化) + ├── T-002 (数据库配置) + ├── T-003 (基础UI框架) + └── T-004 (环境变量配置) + +Phase 2: 核心功能 +T-002 ──▶ T-005 (查询API) ──▶ T-006 (计算逻辑) ──▶ T-009 (结果表格) + │ │ │ + └──▶ T-007 (品牌API) │ │ + │ │ +T-003 ──▶ T-008 (查询表单) │ │ + │ │ + T-010 (导出API) ◀───────────────┤ + │ │ + T-011 (导出按钮) ◀──────────────┤ + │ +T-008, T-009, T-011 ──▶ T-012 (主页面集成) ────────────┘ + +Phase 3: 优化测试 +T-012 ──▶ T-013 (错误处理) ──▶ T-014 (性能优化) + │ │ + ├──▶ T-015 (视频链接) │ + │ │ + └──▶ T-016 (部署配置) │ + │ + T-017 (集成测试) +``` + +--- + +#### S-5: 建议增加功能ID(F-xxx)对应关系 +**位置**: 整个 tasks.md + +**建议**: +在"关联功能"列增加功能ID引用,便于追溯需求: +```markdown +| ID | 任务 | 描述 | 优先级 | 依赖 | 关联功能 | 验收标准 | +|----|------|------|--------|------|----------|----------| +| T-005 | 查询 API 开发 | ... | P0 | T-002 | F-001, F-002, F-003 | ... | +| T-006 | 计算逻辑实现 | ... | P0 | T-005 | F-004, F-005, F-006 | ... | +``` + +--- + +## 依赖关系分析 + +### 关键路径 + +``` +T-001 (项目初始化) + │ + ├─→ T-002 (数据库配置) + │ │ + │ └─→ T-005 (查询API) + │ │ + │ ├─→ T-006 (计算逻辑) + │ │ │ + │ │ └─→ T-010 (导出API) + │ │ + │ └─→ T-007 (品牌API) + │ │ + │ └─→ T-009 (结果表格) + │ │ + │ └─→ T-012 (主页面集成) + │ │ + │ └─→ T-013 (错误处理) + │ │ + │ └─→ T-017 (集成测试) + │ + └─→ T-003 (基础UI) + │ + └─→ T-008 (查询表单) + │ + └─→ T-012 (主页面集成) +``` + +**关键路径**: +T-001 → T-002 → T-005 → T-007 → T-009 → T-012 → T-013 → T-017 + +**可并行任务**: +- T-002 (数据库) 和 T-003 (基础UI) 可并行 +- T-006 (计算逻辑) 和 T-007 (品牌API) 可并行 +- T-013/T-014/T-015 可并行 + +--- + +## 评审结论 + +### 评审结果 + +🟡 **需修改后通过** + +--- + +### 主要优点 + +✅ **覆盖度完整**: +- 所有 DevelopmentPlan (16个任务) 和 UIDesign (10个组件) 都有对应任务 +- 新增 T-012 主页面集成任务是合理补充 + +✅ **验收标准详细**: +- 每个任务平均 6.2 条验收标准 +- 验收标准具体可操作,便于验收 +- T-006/T-014/T-017 的验收标准特别优秀 + +✅ **TDD 要求明确**: +- 7个关键后端任务都要求先写测试再写代码 +- 明确要求 100% 测试覆盖率和真实数据库测试 + +✅ **架构更新到位**: +- 任务描述已完全更新为前后端分离架构 (FastAPI + Next.js) +- 品牌元素(麦秒思AI)在任务中明确体现 + +--- + +### 关键问题 + +❌ **严重问题** (必须修复): +1. **C-1: T-001 粒度过大** - 前后端初始化应拆分,支持并行开发 +2. **C-2: T-010 依赖错误** - 后端 API 不应依赖前端组件 T-009 +3. **C-3: 缺少测试独立任务** - 100% 覆盖率需要独立验收里程碑 +4. **C-4: 任务编号不一致** - Phase 3 任务编号与 DevelopmentPlan 错位 + +⚠️ **一般问题** (建议修复): +1. **M-1: T-002 数据库环境准备** - 需明确数据库环境前置条件 +2. **M-2: T-012 状态管理方案** - 建议使用 React useState +3. **M-3: T-007 参数硬编码** - 并发和超时应可配置 +4. **M-4: T-009/T-010 字段一致性** - 建议共享字段定义文件 +5. **M-5: T-014 性能测试脚本** - 需编写自动化性能测试 +6. **M-6: T-017 性能测试用例** - 集成测试应包含性能验证 + +--- + +### 影响评估 + +**阻塞性问题**: +- 🚫 **C-1 (T-001 粒度过大)**: 导致前后端无法并行开发,延长项目周期 +- 🚫 **C-2 (T-010 依赖错误)**: 导致执行顺序混乱,前后端耦合 + +**质量风险**: +- ⚠️ **C-3 (缺少测试任务)**: 100% 覆盖率难以保证,可能降低代码质量 +- ⚠️ **M-5/M-6 (性能测试缺失)**: 性能指标无法自动化验证 + +**进度风险**: +- ⚠️ **M-1 (数据库环境未就绪)**: 可能导致 T-002 阻塞 +- ⚠️ **无任务估时**: 难以评估项目整体进度和关键路径 + +--- + +## 下一步行动 + +### 必须修改 (Critical) - 预估 1.5 小时 + +- [ ] **C-1: 拆分 T-001** 为 T-001A (前端初始化) 和 T-001B (后端初始化) + - 预估时间: 30分钟 + - 影响范围: tasks.md, DevelopmentPlan.md + +- [ ] **C-2: 修正 T-010 依赖** 移除 T-009,改为 `T-006, T-007` + - 预估时间: 10分钟 + - 影响范围: tasks.md:67 + +- [ ] **C-3: 增加测试任务** 在 Phase 3 增加 T-018 测试覆盖率验收 + - 预估时间: 20分钟 + - 影响范围: tasks.md Phase 3 + +- [ ] **C-4: 统一任务编号** 选择方案A/B/C 修复编号不一致问题 + - 预估时间: 30分钟 + - 影响范围: tasks.md 或 DevelopmentPlan.md + +--- + +### 建议修改 (Major) - 预估 1 小时 + +- [ ] **M-1: T-002 数据库环境说明** 明确数据库准备前置条件 + - 预估时间: 10分钟 + +- [ ] **M-2: T-012 状态管理说明** 补充 React useState 方案 + - 预估时间: 5分钟 + +- [ ] **M-3: T-007 参数配置化** 并发和超时改为可配置 + - 预估时间: 15分钟 + +- [ ] **M-4: T-009/T-010 字段一致性** 增加共享字段定义要求 + - 预估时间: 15分钟 + +- [ ] **M-5: T-014 性能测试脚本** 增加性能测试验收标准 + - 预估时间: 10分钟 + +- [ ] **M-6: T-017 性能测试用例** 增加性能测试用例 + - 预估时间: 5分钟 + +--- + +### 可选优化 (Minor) - 预估 1 小时 + +- [ ] **S-1: 定义"粗略实现"标准** 增加前端开发标准说明 +- [ ] **S-2: 增加任务估时** 为每个任务增加工作量估时(人天) +- [ ] **S-3: T-016 监控配置** 增加日志和监控验收标准 +- [ ] **S-4: 更新依赖图** 使用 tasks.md 的实际任务ID +- [ ] **S-5: 增加功能ID** 在关联功能列增加 F-xxx 引用 + +--- + +### 修复优先级汇总 + +| 优先级 | 问题ID | 问题描述 | 预估时间 | 阻塞风险 | +|--------|--------|----------|----------|----------| +| P0 | C-1 | T-001 拆分 | 30分钟 | ⚠️ 高 | +| P0 | C-2 | T-010 依赖修正 | 10分钟 | ⚠️ 高 | +| P0 | C-3 | 增加测试任务 | 20分钟 | ⚠️ 中 | +| P0 | C-4 | 统一任务编号 | 30分钟 | ⚠️ 中 | +| P1 | M-1~M-6 | 6个一般问题 | 60分钟 | ⚠️ 低 | +| P2 | S-1~S-5 | 5个改进建议 | 60分钟 | ✅ 无 | + +**预计修复总时间**: 约 3.5 小时 (P0-P2 全部) + +--- + +## 参考信息 + +### 文档链接 + +- 目标文档: [doc/tasks.md](doc/tasks.md) +- 上游文档1: [doc/UIDesign.md](doc/UIDesign.md) - UI 设计文档 +- 上游文档2: [doc/DevelopmentPlan.md](doc/DevelopmentPlan.md) - 开发计划 + +### 修改建议操作 + +建议使用 `/mt` 命令根据本评审报告的问题清单进行增量修改: +```bash +/mt # 增量修改 tasks.md +``` + +--- + +**评审人**: Claude Sonnet 4.5 +**评审日期**: 2026-01-28 15:30 +**评审版本**: tasks.md v1.0 +**评审耗时**: 45 分钟 +**评审方法**: 基于 `/rt` 评审技能,对比 UIDesign.md 和 DevelopmentPlan.md diff --git a/doc/tasks.md b/doc/tasks.md new file mode 100644 index 0000000..c6a3634 --- /dev/null +++ b/doc/tasks.md @@ -0,0 +1,339 @@ +# KOL Insight - 任务列表 + +## 文档信息 + +| 项目 | 内容 | +|------|------| +| 版本 | v1.0 | +| 创建日期 | 2026-01-28 | +| 来源文档 | UIDesign.md, DevelopmentPlan.md, FeatureSummary.md, PRD.md | + + +## 架构说明 + +**前后端分离架构**: +- **前端**: Next.js 14.x + React + TypeScript + Tailwind CSS +- **后端**: Python FastAPI 0.104+ + SQLAlchemy 2.0+ + asyncpg +- **数据库**: PostgreSQL 14.x+ +- **部署**: Docker + Uvicorn (ASGI 服务器) + +**关键技术点**: +- 后端使用 FastAPI 异步框架,提供 RESTful API +- 前端通过 HTTP 调用后端 API(CORS 配置) +- 数据库使用 SQLAlchemy 异步 ORM +- 外部 API 调用使用 httpx 异步库 + + +## 1. 任务总览 + + +| 统计项 | 数量 | +|--------|------| +| 总任务数 | 18 | +| P0 任务 | 10 | +| P1 任务 | 7 | +| P2 任务 | 1 | + +## 2. Phase 1 任务 - 基础架构搭建 + +### 2.1 项目初始化与配置 + +| ID | 任务 | 描述 | 优先级 | 依赖 | 验收标准 | +|----|------|------|--------|------|----------| + +| T-001A | 前端项目初始化 | 创建 Next.js 14.x 项目,配置 TypeScript、ESLint、Prettier | P0 | - | 1. Next.js 14.x 项目创建成功
2. TypeScript 配置完成 (tsconfig.json)
3. ESLint 配置完成 (.eslintrc.json)
4. Prettier 配置完成 (.prettierrc)
5. 可运行 `pnpm dev` 启动开发服务器
6. 可运行 `pnpm build` 构建生产版本 | +| T-001B | 后端项目初始化 | 创建 FastAPI 0.104+ 项目,配置 Python 依赖管理 | P0 | - | 1. FastAPI 0.104+ 项目创建成功
2. Python 依赖管理配置完成 (Poetry 或 requirements.txt)
3. 项目结构创建 (app/, tests/, alembic/)
4. main.py 应用入口文件创建
5. 可运行 `uvicorn main:app --reload` 启动开发服务器
6. 访问 http://localhost:8000/docs 可看到 API 文档 | + + + +| T-002 | 数据库配置 | 配置 SQLAlchemy,定义数据模型,连接 PostgreSQL | P0 | T-001B | 1. SQLAlchemy 2.0+ 和 asyncpg 安装完成
2. 定义 KolVideo 模型(使用 SQLAlchemy ORM)
3. 数据库异步连接成功
4. 索引创建: star_id, star_unique_id, star_nickname
5. Alembic 迁移工具配置完成
6. **真实数据库测试**: 使用 .env 中的连接字符串连接真实数据库并验证
7. **TDD要求**: 编写数据库连接测试,模型测试,CRUD测试
8. **测试覆盖率**: 数据库操作测试覆盖率 ≥ 100% | + +| T-003 | 基础 UI 框架 | 安装 Tailwind CSS,创建基础布局组件 | P0 | T-001A | 1. Tailwind CSS 配置完成
2. 品牌色系配置 (#4F46E5等)
3. 基础布局组件创建 (Header/Footer)
4. 麦秒思AI Logo 集成 (doc/ui/muse.svg) | + +| T-004 | 环境变量配置 | 配置开发/生产环境变量,数据库连接字符串 | P0 | T-001A, T-001B | 1. 前后端 .env.example 创建
2. 后端 DATABASE_URL 配置
3. 后端品牌 API 地址配置
4. 前端 NEXT_PUBLIC_API_URL 配置
5. .env 文件创建并添加到 .gitignore | + +## 3. Phase 2 任务 - 核心功能开发 + +### 3.1 后端 API 开发 + +| ID | 任务 | 描述 | 优先级 | 依赖 | 验收标准 | +|----|------|------|--------|------|----------| + + +| T-005 | 查询 API 开发 | 实现 POST /api/v1/query 接口(FastAPI),支持三种查询方式 | P0 | T-002 | 1. FastAPI POST /api/v1/query 路由实现
2. Pydantic 模型验证请求参数(type: star_id/unique_id/nickname)
3. 星图ID: 使用 SQLAlchemy 异步查询 WHERE star_id IN (...)
4. 达人ID: WHERE star_unique_id IN (...)
5. 昵称: WHERE star_nickname LIKE '%...%'
6. 返回完整视频数据列表(JSON 格式)
7. 限制单次查询最大 1000 条
8. CORS 配置支持前端跨域请求
9. **TDD要求**: 先编写API测试用例(三种查询方式),再实现路由
10. **测试覆盖率**: API集成测试覆盖率 ≥ 100% (包含成功/失败/边界场景) | + + +| T-006 | 计算逻辑实现 | 在 Python 后端实现 CPM、看后搜人数、成本计算 | P0 | T-005 | 1. 预估自然CPM = estimated_video_cost / natural_play_cnt * 1000
2. 预估自然看后搜人数 = natural_play_cnt / total_play_cnt * after_view_search_uv
3. 预估看后搜人数成本 = estimated_video_cost / 预估自然看后搜人数
4. 除零检查,返回 None
5. 结果保留 2 位小数(使用 round())
6. 批量计算使用列表推导式或 map()
7. **TDD要求**: 先编写计算函数的单元测试(包含除零场景),再实现函数
8. **测试覆盖率**: 计算逻辑单元测试覆盖率 ≥ 100% (所有分支覆盖) | + + +| T-007 | 品牌 API 批量集成 | 后端使用 httpx 批量调用品牌API获取品牌名称,支持并发控制和降级 | P0 | T-005 | 1. 使用 httpx.AsyncClient 实现 GET /v1/yuntu/brands/{brand_id} 调用
2. 从查询结果提取唯一 brand_id(去重)
3. 使用 asyncio.gather 批量并发请求(限制 10 并发)
4. 构建 brand_id → brand_name 映射字典
5. 单个 API 调用失败时降级显示 brand_id
6. 超时设置: 3秒
7. 错误日志记录
8. **TDD要求**: 先编写测试用例(模拟API响应),再实现功能
9. **测试覆盖率**: 单元测试覆盖率 ≥ 100% (包含成功/失败/超时/并发场景) | + + +| T-010 | 导出 API 开发 | 实现 GET /api/v1/export 接口(FastAPI),生成 Excel/CSV | P1 | T-006, T-007 | 1. FastAPI GET /api/v1/export 路由实现
2. 支持 format=xlsx/csv 查询参数
3. 使用 openpyxl 或 xlsxwriter 库生成 Excel
4. CSV 使用 Python csv 模块,处理逗号转义
5. 使用中文列名作为表头 **(字段顺序和命名需与前端 ResultTable 保持一致)**
6. 文件名: kol_data_{timestamp}.xlsx
7. 响应头设置 Content-Disposition: attachment
8. 使用 StreamingResponse 返回文件
9. 限制单次导出最大 1000 条
10. **TDD要求**: 先编写测试用例(生成文件验证),再实现功能
11. **测试覆盖率**: 单元测试覆盖率 ≥ 100% (包含 Excel/CSV 生成,字段一致性验证) | + +### 3.2 前端组件开发 + +| ID | 任务 | 描述 | 优先级 | 依赖 | 验收标准 | +|----|------|------|--------|------|----------| + +| T-008 | 查询表单组件 | 开发查询输入表单,支持方式切换 **(前端粗略实现)** | P0 | T-003 | 1. QueryForm 组件创建
2. Radio 单选器: 星图ID/达人unique_id/达人昵称
3. Textarea 输入框,支持批量输入
4. 根据查询方式动态更新输入提示
5. 输入验证 (实时)
6. 清空和提交按钮
7. 加载态: 按钮禁用,显示 Loading
8. **粗略实现说明**: 基础表单功能可用,样式简洁即可 | + +| T-009 | 结果表格组件 | 开发数据展示表格,显示 26 个字段 **(前端粗略实现)** | P1 | T-003, T-006, T-007 | 1. ResultTable 组件创建
2. 展示 26 个字段,使用中文列名
3. 表格列宽按 UIDesign 规范
4. 数字格式化: 千分位分隔
5. 大数值使用 K/M 缩写
6. 空值显示 "-"
7. 支持横向滚动
8. 支持列排序
9. **包含分页器组件**: 每页 20 条,支持翻页
10. 品牌名称由后端返回,直接展示
11. **粗略实现说明**: 基础功能可用即可,样式可简化 | + +| T-011 | 导出按钮组件 | 开发导出按钮,触发文件下载 **(前端粗略实现)** | P1 | T-009, T-010 | 1. ExportButton 组件创建
2. 导出 Excel 按钮
3. 导出 CSV 按钮
4. 点击触发 /api/export 调用
5. 浏览器下载文件
6. 无数据时提示 "无数据可导出"
7. 导出中显示 Loading
8. **粗略实现说明**: 基础导出功能可用即可 | + +### 3.3 页面集成 + +| ID | 任务 | 描述 | 优先级 | 依赖 | 验收标准 | +|----|------|------|--------|------|----------| + + +| T-011A | 主页面集成 | 集成查询表单、结果表格和导出按钮,完成单页应用 **(前端粗略实现)** | P0 | T-008, T-009, T-011 | 1. page.tsx 创建单页应用
2. 品牌头部: Logo + "KOL Insight" + "麦秒思AI制作"
3. 查询区域集成 QueryForm
4. 结果区域集成 ResultTable 和 ExportButton
5. Footer: "© 2026 麦秒思AI制作"
6. 页面状态管理: 默认态/输入态/查询中/结果态/空结果态/错误态
7. 空状态组件: 引导文案 + 空盒子图标
8. 错误状态组件: 错误提示 + 重试按钮
9. **粗略实现说明**: 重点在功能集成,UI可简化,品牌元素必须保留 | + +## 4. Phase 3 任务 - 优化与测试 + +### 4.1 错误处理与优化 + +| ID | 任务 | 描述 | 优先级 | 依赖 | 验收标准 | +|----|------|------|--------|------|----------| + + +| T-013 | 错误处理 | 完善错误处理,添加用户友好提示 | P1 | T-011A | 1. API 错误处理: try-catch 包裹
2. 数据库连接失败提示
3. 品牌 API 失败降级处理
4. 网络超时提示
5. 输入验证错误提示
6. 空结果友好提示
7. 错误日志记录 (后端)
8. **后端TDD要求**: 编写异常场景测试用例,验证错误处理逻辑
9. **后端测试覆盖率**: 错误处理分支覆盖率 ≥ 100%
10. **前端粗略实现**: 基础错误提示可用即可 | + +| T-014 | 性能优化 | 数据库索引优化,查询性能调优 | P1 | T-013 | 1. 验证索引已创建: star_id, star_unique_id, star_nickname
2. 查询响应时间 ≤ 3秒 (100条)
3. 页面加载时间 ≤ 2秒
4. 导出响应时间 ≤ 5秒 (1000条)
5. 品牌 API 并发控制 (限制 10 并发)
6. **后端性能测试**: 编写性能测试脚本,验证响应时间指标
7. **真实数据库测试**: 使用真实数据库进行性能测试 | +| T-015 | 视频链接跳转 | 实现视频链接点击跳转功能 | P2 | T-009 | 1. 视频链接列展示为链接按钮
2. 使用 `
`
3. 链接为空时显示 "-"
4. 新窗口打开视频页面 | + +| T-016 | 部署配置 | Docker 配置前后端分离部署 | P1 | T-013 | 1. 前端 Dockerfile 创建(Node.js + Next.js)
2. 后端 Dockerfile 创建(Python + FastAPI + Uvicorn)
3. docker-compose.yml 配置前端、后端、PostgreSQL
4. 前端构建: pnpm build
5. 后端启动: uvicorn main:app --host 0.0.0.0 --port 8000
6. 生产环境可访问
7. 环境变量配置(.env) | + +| T-017 | 集成测试 | 端到端功能测试 | P1 | T-013 | 1. 测试用例: 星图ID精准查询
2. 测试用例: 达人ID批量查询
3. 测试用例: 昵称模糊查询
4. 测试用例: 计算指标准确性
5. 测试用例: 品牌名称获取
6. 测试用例: 数据导出 (Excel/CSV)
7. 测试用例: 错误处理 (网络异常/空结果)
8. 所有 P0/P1 功能测试通过
9. **真实数据库集成测试**: 使用 .env 中的真实数据库连接进行完整集成测试 | + +| T-018 | 测试覆盖率验收 | 验证所有后端代码测试覆盖率 ≥ 100% | P1 | T-002, T-005, T-006, T-007, T-010, T-013, T-017 | 1. 数据库操作测试覆盖率 100% (T-002)
2. API集成测试覆盖率 100% (T-005)
3. 计算逻辑单元测试覆盖率 100% (T-006)
4. 品牌API单元测试覆盖率 100% (T-007)
5. 导出功能单元测试覆盖率 100% (T-010)
6. 错误处理分支覆盖率 100% (T-013)
7. 使用 pytest-cov 生成覆盖率报告
8. 覆盖率报告保存到 coverage/ 目录
9. CI/CD 集成: 覆盖率未达标时构建失败 | + + +## 5. 任务依赖图 + + +``` +Phase 1: 基础架构 +T-001A (前端初始化) T-001B (后端初始化) + │ │ + ├── T-003 (基础UI) ├── T-002 (数据库配置) + │ │ + └────────┬─────────────┘ + │ + └── T-004 (环境变量配置) + +Phase 2: 核心功能 +T-002 ──▶ T-005 (查询API) ──▶ T-006 (计算逻辑) ──▶ T-009 (结果表格) + │ │ │ + └──▶ T-007 (品牌API) │ │ + │ │ +T-003 ──▶ T-008 (查询表单) │ │ + │ │ + T-010 (导出API) ◀───────────────┤ + │ │ + T-011 (导出按钮) ◀──────────────┤ + │ +T-008, T-009, T-011 ──▶ T-011A (主页面集成) ───────────┘ + +Phase 3: 优化测试 +T-011A ──▶ T-013 (错误处理) ──▶ T-014 (性能优化) + │ │ + ├──▶ T-015 (视频链接) │ + │ │ + ├──▶ T-016 (部署配置) │ + │ │ + └──▶ T-017 (集成测试) ─┤ + │ │ + └──▶ T-018 (测试覆盖率验收) +``` + +## 6. 执行检查清单 + + +### Phase 1 - 基础架构搭建 +- [ ] T-001A: 前端项目初始化 +- [ ] T-001B: 后端项目初始化 +- [ ] T-002: 数据库配置 +- [ ] T-003: 基础 UI 框架 +- [ ] T-004: 环境变量配置 + +### Phase 2 - 核心功能开发 +- [ ] T-005: 查询 API 开发 +- [ ] T-006: 计算逻辑实现 +- [ ] T-007: 品牌 API 批量集成 +- [ ] T-008: 查询表单组件 +- [ ] T-009: 结果表格组件 +- [ ] T-010: 导出 API 开发 +- [ ] T-011: 导出按钮组件 +- [ ] T-011A: 主页面集成 + +### Phase 3 - 优化与测试 +- [ ] T-013: 错误处理 +- [ ] T-014: 性能优化 +- [ ] T-015: 视频链接跳转 +- [ ] T-016: 部署配置 +- [ ] T-017: 集成测试 +- [ ] T-018: 测试覆盖率验收 + +## 7. 里程碑与交付物 + + +| 里程碑 | 包含任务 | 交付物 | 预期验收 | +|--------|----------|--------|----------| +| M1: 基础架构完成 | T-001A, T-001B, T-002~T-004 | 前后端项目骨架、数据库连接、基础UI | 前后端项目可运行,数据库可连接 | +| M2: 核心功能完成 | T-005~T-011A | 查询、计算、展示、导出功能 | 所有 P0 功能可用,品牌API集成 | +| M3: 优化测试完成 | T-013~T-018 | 错误处理、性能优化、部署配置、测试覆盖率 | 测试通过,性能达标,100%覆盖率,可部署 | +| M4: 正式上线 | - | 生产环境部署 | 生产环境可访问,稳定运行 | + +## 8. 优先级说明 + + +**P0 任务 (10个)** - MVP 必须完成 +- 基础架构: T-001A (前端初始化), T-001B (后端初始化), T-002 (数据库), T-003 (基础UI), T-004 (环境变量) +- 核心功能: T-005 (查询API), T-006 (计算逻辑), T-007 (品牌API), T-008 (查询表单), T-011A (主页面集成) + +**P1 任务 (7个)** - 重要功能 +- 展示导出: T-009 (结果表格), T-010 (导出API), T-011 (导出按钮) +- 优化测试: T-013 (错误处理), T-014 (性能优化), T-016 (部署配置), T-017 (集成测试), T-018 (测试覆盖率验收) + +**P2 任务 (1个)** - 次要功能 +- 增强体验: T-015 (视频链接跳转) + +## 9. 关键技术点 + + + +| 任务 | 关键技术 | 注意事项 | +|------|----------|----------| +| T-001A | Next.js 14.x 项目初始化 | TypeScript + ESLint + Prettier,App Router 模式 | +| T-001B | FastAPI 0.104+ 项目初始化 | Poetry/pip 依赖管理,项目结构规划 | +| T-002 | SQLAlchemy + asyncpg | 必须创建索引: star_id, star_unique_id, star_nickname,使用异步 ORM | +| T-005 | FastAPI + Pydantic | 使用 IN 查询批量匹配,LIKE 模糊匹配,限制最大 1000 条,CORS 配置 | +| T-006 | Python 计算 | 除零检查,结果保留 2 位小数,None 值处理 | +| T-007 | httpx + asyncio | 批量并发调用 (限制 10 并发),超时 3 秒,降级处理,使用 asyncio.gather | +| T-009 | React 表格组件 | 26 个字段展示,数字格式化,横向滚动,分页 | +| T-010 | openpyxl/xlsxwriter | Python 库生成 Excel,CSV 逗号转义,中文列名,StreamingResponse | +| T-011A | React 状态管理 | 6 种页面状态: 默认/输入/查询中/结果/空结果/错误 | +| T-013 | FastAPI 错误处理 | API 错误、数据库连接失败、品牌API降级、网络超时 | +| T-014 | 性能优化 | 查询 ≤3秒,页面加载 ≤2秒,导出 ≤5秒,并发控制 | +| T-016 | Docker + Uvicorn | 前后端分离部署,Uvicorn ASGI 服务器 | +| T-018 | pytest-cov | 测试覆盖率验收,确保所有后端代码 ≥ 100% 覆盖率 | + + +## 10. 数据库 Schema 参考 + +```python +# SQLAlchemy 模型定义 +from sqlalchemy import Column, String, Integer, Float, DateTime, Index +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() + +class KolVideo(Base): + __tablename__ = "kol_videos" + + # 主键 + item_id = Column(String, primary_key=True) + + # 基本信息 + title = Column(String, nullable=True) + viral_type = Column(String, nullable=True) + video_url = Column(String, nullable=True) + star_id = Column(String, nullable=False) + star_unique_id = Column(String, nullable=False) + star_nickname = Column(String, nullable=False) + publish_time = Column(DateTime, nullable=True) + + # 曝光指标 + natural_play_cnt = Column(Integer, default=0) + heated_play_cnt = Column(Integer, default=0) + total_play_cnt = Column(Integer, default=0) + + # 互动指标 + total_interact = Column(Integer, default=0) + like_cnt = Column(Integer, default=0) + share_cnt = Column(Integer, default=0) + comment_cnt = Column(Integer, default=0) + + # 效果指标 + new_a3_rate = Column(Float, nullable=True) + after_view_search_uv = Column(Integer, default=0) + return_search_cnt = Column(Integer, default=0) + + # 商业信息 + industry_id = Column(String, nullable=True) + industry_name = Column(String, nullable=True) + brand_id = Column(String, nullable=True) + estimated_video_cost = Column(Float, default=0) + + # 索引 + __table_args__ = ( + Index('idx_star_id', 'star_id'), + Index('idx_star_unique_id', 'star_unique_id'), + Index('idx_star_nickname', 'star_nickname'), + ) +``` + + +## 11. API 接口参考 + +### POST /api/v1/query +```python +# FastAPI 路由定义 +from pydantic import BaseModel +from typing import List, Literal + +class QueryRequest(BaseModel): + type: Literal["star_id", "unique_id", "nickname"] + values: List[str] + +class QueryResponse(BaseModel): + success: bool + data: List[dict] # 视频数据列表 (含 26 个字段) + total: int + +# 请求示例 +{ + "type": "star_id", + "values": ["id1", "id2", ...] +} + +# 响应示例 +{ + "success": true, + "data": [...], + "total": 100 +} +``` + +### GET /api/v1/export +```python +# FastAPI 路由 +@app.get("/api/v1/export") +async def export_data(format: Literal["xlsx", "csv"] = "xlsx"): + # 返回 StreamingResponse + pass + +# 请求: GET /api/v1/export?format=xlsx +# 响应: 文件下载 (Content-Disposition: attachment) +``` + +### 外部 API: GET /v1/yuntu/brands/{brand_id} +```python +# 使用 httpx.AsyncClient 调用 +import httpx + +async with httpx.AsyncClient() as client: + response = await client.get( + f"https://api.internal.intelligrow.cn/v1/yuntu/brands/{brand_id}", + timeout=3.0 + ) +# 失败时降级显示 brand_id +``` + +--- + +**文档状态**: 待执行 +**建议下一步**: 按顺序执行 Phase 1 任务,完成基础架构搭建 +**评审建议**: 可运行 `/rt` 对任务列表进行评审 diff --git a/doc/ui/muse.svg b/doc/ui/muse.svg new file mode 100644 index 0000000..32decb4 --- /dev/null +++ b/doc/ui/muse.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/doc/ui/ui.pen b/doc/ui/ui.pen new file mode 100644 index 0000000..f822a2a --- /dev/null +++ b/doc/ui/ui.pen @@ -0,0 +1,17 @@ +{ + "version": "2.6", + "children": [ + { + "type": "frame", + "id": "bi8Au", + "x": 0, + "y": 0, + "name": "Frame", + "clip": true, + "width": 800, + "height": 600, + "fill": "#FFFFFF", + "layout": "none" + } + ] +} \ No newline at end of file diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..bd6337f --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,2 @@ +# 后端 API 地址 +NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1 diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json new file mode 100644 index 0000000..4360d9b --- /dev/null +++ b/frontend/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["next/core-web-vitals", "next/typescript", "prettier"] +} diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..fd3dbb5 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 0000000..1f4c4bb --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "printWidth": 100 +} diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..c49d933 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,45 @@ +[text](README.md)# KOL Insight + +云图 KOL 数据查询与分析工具。 + +## 功能 + +- 批量查询 KOL 视频数据 +- 支持星图ID、达人unique_id、达人昵称搜索 +- 计算预估自然CPM、看后搜成本等指标 +- 数据导出 + +## 技术栈 + +- **前端/后端**: Next.js (App Router) +- **数据库**: PostgreSQL +- **部署**: Docker / PM2 + +## 快速开始 + +```bash +# 安装依赖 +pnpm install + +# 配置环境变量 +cp .env.example .env.local + +# 开发模式 +pnpm dev + +# 构建 +pnpm build + +# 生产运行 +pnpm start +``` + +## 环境变量 + +```env +DATABASE_URL=postgresql://user:password@host:5432/yuntu_kol +``` + +## License + +MIT diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs new file mode 100644 index 0000000..4678774 --- /dev/null +++ b/frontend/next.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {}; + +export default nextConfig; diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..e5e794f --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,28 @@ +{ + "name": "frontend", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "next": "14.2.35", + "react": "^18", + "react-dom": "^18" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "eslint": "^8", + "eslint-config-next": "14.2.35", + "eslint-config-prettier": "^10.1.8", + "postcss": "^8", + "prettier": "^3.8.1", + "tailwindcss": "^3.4.1", + "typescript": "^5" + } +} diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml new file mode 100644 index 0000000..baf1dd6 --- /dev/null +++ b/frontend/pnpm-lock.yaml @@ -0,0 +1,3676 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + next: + specifier: 14.2.35 + version: 14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: + specifier: ^18 + version: 18.3.1 + react-dom: + specifier: ^18 + version: 18.3.1(react@18.3.1) + devDependencies: + '@types/node': + specifier: ^20 + version: 20.19.30 + '@types/react': + specifier: ^18 + version: 18.3.27 + '@types/react-dom': + specifier: ^18 + version: 18.3.7(@types/react@18.3.27) + eslint: + specifier: ^8 + version: 8.57.1 + eslint-config-next: + specifier: 14.2.35 + version: 14.2.35(eslint@8.57.1)(typescript@5.9.3) + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@8.57.1) + postcss: + specifier: ^8 + version: 8.5.6 + prettier: + specifier: ^3.8.1 + version: 3.8.1 + tailwindcss: + specifier: ^3.4.1 + version: 3.4.19 + typescript: + specifier: ^5 + version: 5.9.3 + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@emnapi/core@1.8.1': + resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} + + '@emnapi/runtime@1.8.1': + resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + + '@emnapi/wasi-threads@1.1.0': + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@8.57.1': + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@humanwhocodes/config-array@0.13.0': + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/wasm-runtime@0.2.12': + resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + + '@next/env@14.2.35': + resolution: {integrity: sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==} + + '@next/eslint-plugin-next@14.2.35': + resolution: {integrity: sha512-Jw9A3ICz2183qSsqwi7fgq4SBPiNfmOLmTPXKvlnzstUwyvBrtySiY+8RXJweNAs9KThb1+bYhZh9XWcNOr2zQ==} + + '@next/swc-darwin-arm64@14.2.33': + resolution: {integrity: sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@14.2.33': + resolution: {integrity: sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@14.2.33': + resolution: {integrity: sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-arm64-musl@14.2.33': + resolution: {integrity: sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-x64-gnu@14.2.33': + resolution: {integrity: sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-linux-x64-musl@14.2.33': + resolution: {integrity: sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-win32-arm64-msvc@14.2.33': + resolution: {integrity: sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-ia32-msvc@14.2.33': + resolution: {integrity: sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@next/swc-win32-x64-msvc@14.2.33': + resolution: {integrity: sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@nolyfill/is-core-module@1.0.39': + resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} + engines: {node: '>=12.4.0'} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@rushstack/eslint-patch@1.15.0': + resolution: {integrity: sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw==} + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/helpers@0.5.5': + resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/node@20.19.30': + resolution: {integrity: sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react@18.3.27': + resolution: {integrity: sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==} + + '@typescript-eslint/eslint-plugin@8.54.0': + resolution: {integrity: sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.54.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.54.0': + resolution: {integrity: sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.54.0': + resolution: {integrity: sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.54.0': + resolution: {integrity: sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.54.0': + resolution: {integrity: sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.54.0': + resolution: {integrity: sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.54.0': + resolution: {integrity: sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.54.0': + resolution: {integrity: sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.54.0': + resolution: {integrity: sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.54.0': + resolution: {integrity: sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.11.1': + resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.11.1': + resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} + cpu: [ppc64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} + cpu: [s390x] + os: [linux] + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} + cpu: [x64] + os: [win32] + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + ast-types-flow@0.0.8: + resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axe-core@4.11.1: + resolution: {integrity: sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==} + engines: {node: '>=4'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + + caniuse-lite@1.0.30001766: + resolution: {integrity: sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + damerau-levenshtein@1.0.8: + resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + es-abstract@1.24.1: + resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-iterator-helpers@1.2.2: + resolution: {integrity: sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-next@14.2.35: + resolution: {integrity: sha512-BpLsv01UisH193WyT/1lpHqq5iJ/Orfz9h/NOOlAmTUq4GY349PextQ62K4XpnaM9supeiEn3TaOTeQO07gURg==} + peerDependencies: + eslint: ^7.23.0 || ^8.0.0 + typescript: '>=3.3.1' + peerDependenciesMeta: + typescript: + optional: true + + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-import-resolver-typescript@3.10.1: + resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + + eslint-module-utils@2.12.1: + resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-jsx-a11y@6.10.2: + resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} + engines: {node: '>=4.0'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 + + eslint-plugin-react-hooks@5.0.0-canary-7118f5dd7-20230705: + resolution: {integrity: sha512-AZYbMo/NW9chdL7vk6HQzQhT+PvTAEVqWk9ziruUoW2kAOcN5qNyelv70e0F1VNQAbvutOC9oc+xfWycI9FxDw==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.13.0: + resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@10.3.10: + resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-bun-module@2.0.0: + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + + jackspeak@2.3.6: + resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} + engines: {node: '>=14'} + + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + language-subtag-registry@0.3.23: + resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} + + language-tags@1.0.9: + resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} + engines: {node: '>=0.10'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + next@14.2.35: + resolution: {integrity: sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==} + engines: {node: '>=18.17.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.41.2 + react: ^18.2.0 + react-dom: ^18.2.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + sass: + optional: true + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.1.0: + resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@3.8.1: + resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} + engines: {node: '>=14'} + hasBin: true + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + resolve@2.0.0-next.5: + resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stable-hash@0.0.5: + resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string.prototype.includes@2.0.1: + resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} + engines: {node: '>= 0.4'} + + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + styled-jsx@5.1.1: + resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tailwindcss@3.4.19: + resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} + engines: {node: '>=14.0.0'} + hasBin: true + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unrs-resolver@1.11.1: + resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.20: + resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@emnapi/core@1.8.1': + dependencies: + '@emnapi/wasi-threads': 1.1.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.8.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.1.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@8.57.1)': + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.1': {} + + '@humanwhocodes/config-array@0.13.0': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.2 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@napi-rs/wasm-runtime@0.2.12': + dependencies: + '@emnapi/core': 1.8.1 + '@emnapi/runtime': 1.8.1 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@next/env@14.2.35': {} + + '@next/eslint-plugin-next@14.2.35': + dependencies: + glob: 10.3.10 + + '@next/swc-darwin-arm64@14.2.33': + optional: true + + '@next/swc-darwin-x64@14.2.33': + optional: true + + '@next/swc-linux-arm64-gnu@14.2.33': + optional: true + + '@next/swc-linux-arm64-musl@14.2.33': + optional: true + + '@next/swc-linux-x64-gnu@14.2.33': + optional: true + + '@next/swc-linux-x64-musl@14.2.33': + optional: true + + '@next/swc-win32-arm64-msvc@14.2.33': + optional: true + + '@next/swc-win32-ia32-msvc@14.2.33': + optional: true + + '@next/swc-win32-x64-msvc@14.2.33': + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@nolyfill/is-core-module@1.0.39': {} + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@rtsao/scc@1.1.0': {} + + '@rushstack/eslint-patch@1.15.0': {} + + '@swc/counter@0.1.3': {} + + '@swc/helpers@0.5.5': + dependencies: + '@swc/counter': 0.1.3 + tslib: 2.8.1 + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/json5@0.0.29': {} + + '@types/node@20.19.30': + dependencies: + undici-types: 6.21.0 + + '@types/prop-types@15.7.15': {} + + '@types/react-dom@18.3.7(@types/react@18.3.27)': + dependencies: + '@types/react': 18.3.27 + + '@types/react@18.3.27': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + + '@typescript-eslint/eslint-plugin@8.54.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.54.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.54.0 + '@typescript-eslint/type-utils': 8.54.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.54.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.54.0 + eslint: 8.57.1 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.54.0 + '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.54.0 + debug: 4.4.3 + eslint: 8.57.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.54.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.54.0(typescript@5.9.3) + '@typescript-eslint/types': 8.54.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.54.0': + dependencies: + '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/visitor-keys': 8.54.0 + + '@typescript-eslint/tsconfig-utils@8.54.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.54.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.54.0(eslint@8.57.1)(typescript@5.9.3) + debug: 4.4.3 + eslint: 8.57.1 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.54.0': {} + + '@typescript-eslint/typescript-estree@8.54.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.54.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.54.0(typescript@5.9.3) + '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/visitor-keys': 8.54.0 + debug: 4.4.3 + minimatch: 9.0.5 + semver: 7.7.3 + tinyglobby: 0.2.15 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.54.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + '@typescript-eslint/scope-manager': 8.54.0 + '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) + eslint: 8.57.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.54.0': + dependencies: + '@typescript-eslint/types': 8.54.0 + eslint-visitor-keys: 4.2.1 + + '@ungap/structured-clone@1.3.0': {} + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + optional: true + + '@unrs/resolver-binding-android-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + dependencies: + '@napi-rs/wasm-runtime': 0.2.12 + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + optional: true + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + arg@5.0.2: {} + + argparse@2.0.1: {} + + aria-query@5.3.2: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-shim-unscopables: 1.1.0 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + ast-types-flow@0.0.8: {} + + async-function@1.0.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + axe-core@4.11.1: {} + + axobject-query@4.1.0: {} + + balanced-match@1.0.2: {} + + binary-extensions@2.3.0: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + camelcase-css@2.0.1: {} + + caniuse-lite@1.0.30001766: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + client-only@0.0.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@4.1.1: {} + + concat-map@0.0.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + damerau-levenshtein@1.0.8: {} + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + didyoumean@1.2.2: {} + + dlv@1.1.3: {} + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + eastasianwidth@0.2.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + es-abstract@1.24.1: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.20 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-iterator-helpers@1.2.2: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + safe-array-concat: 1.1.3 + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + escape-string-regexp@4.0.0: {} + + eslint-config-next@14.2.35(eslint@8.57.1)(typescript@5.9.3): + dependencies: + '@next/eslint-plugin-next': 14.2.35 + '@rushstack/eslint-patch': 1.15.0 + '@typescript-eslint/eslint-plugin': 8.54.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': 8.54.0(eslint@8.57.1)(typescript@5.9.3) + eslint: 8.57.1 + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) + eslint-plugin-react: 7.37.5(eslint@8.57.1) + eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - eslint-import-resolver-webpack + - eslint-plugin-import-x + - supports-color + + eslint-config-prettier@10.1.8(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.16.1 + resolve: 1.22.11 + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.3 + eslint: 8.57.1 + get-tsconfig: 4.13.0 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.15 + unrs-resolver: 1.11.1 + optionalDependencies: + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.54.0(eslint@8.57.1)(typescript@5.9.3) + eslint: 8.57.1 + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1) + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 8.57.1 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.54.0(eslint@8.57.1)(typescript@5.9.3) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-jsx-a11y@6.10.2(eslint@8.57.1): + dependencies: + aria-query: 5.3.2 + array-includes: 3.1.9 + array.prototype.flatmap: 1.3.3 + ast-types-flow: 0.0.8 + axe-core: 4.11.1 + axobject-query: 4.1.0 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + eslint: 8.57.1 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.9 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + safe-regex-test: 1.1.0 + string.prototype.includes: 2.0.1 + + eslint-plugin-react-hooks@5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + + eslint-plugin-react@7.37.5(eslint@8.57.1): + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.2.2 + eslint: 8.57.1 + estraverse: 5.3.0 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.5 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@8.57.1: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.2 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.3.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.1 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + espree@9.6.1: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 3.4.3 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@3.2.0: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + rimraf: 3.0.2 + + flatted@3.3.3: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + generator-function@2.0.1: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + get-tsconfig@4.13.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@10.3.10: + dependencies: + foreground-child: 3.3.1 + jackspeak: 2.3.6 + minimatch: 9.0.5 + minipass: 7.1.2 + path-scurry: 1.11.1 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-bun-module@2.0.0: + dependencies: + semver: 7.7.3 + + is-callable@1.2.7: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-fullwidth-code-point@3.0.0: {} + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-path-inside@3.0.3: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.20 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + + jackspeak@2.3.6: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jiti@1.21.7: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + language-subtag-registry@0.3.23: {} + + language-tags@1.0.9: + dependencies: + language-subtag-registry: 0.3.23 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@10.4.3: {} + + math-intrinsics@1.1.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + minipass@7.1.2: {} + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.11: {} + + napi-postinstall@0.3.4: {} + + natural-compare@1.4.0: {} + + next@14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@next/env': 14.2.35 + '@swc/helpers': 0.5.5 + busboy: 1.6.0 + caniuse-lite: 1.0.30001766 + graceful-fs: 4.2.11 + postcss: 8.4.31 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + styled-jsx: 5.1.1(react@18.3.1) + optionalDependencies: + '@next/swc-darwin-arm64': 14.2.33 + '@next/swc-darwin-x64': 14.2.33 + '@next/swc-linux-arm64-gnu': 14.2.33 + '@next/swc-linux-arm64-musl': 14.2.33 + '@next/swc-linux-x64-gnu': 14.2.33 + '@next/swc-linux-x64-musl': 14.2.33 + '@next/swc-win32-arm64-msvc': 14.2.33 + '@next/swc-win32-ia32-msvc': 14.2.33 + '@next/swc-win32-x64-msvc': 14.2.33 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + normalize-path@3.0.0: {} + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pify@2.3.0: {} + + pirates@4.0.7: {} + + possible-typed-array-names@1.1.0: {} + + postcss-import@15.1.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.11 + + postcss-js@4.1.0(postcss@8.5.6): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.5.6 + + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 1.21.7 + postcss: 8.5.6 + + postcss-nested@6.2.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.4.31: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier@3.8.1: {} + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-is@16.13.1: {} + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + resolve@2.0.0-next.5: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + semver@6.3.1: {} + + semver@7.7.3: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@4.1.0: {} + + source-map-js@1.2.1: {} + + stable-hash@0.0.5: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + streamsearch@1.1.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.2 + + string.prototype.includes@2.0.1: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.0 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.1 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.2: + dependencies: + ansi-regex: 6.2.2 + + strip-bom@3.0.0: {} + + strip-json-comments@3.1.1: {} + + styled-jsx@5.1.1(react@18.3.1): + dependencies: + client-only: 0.0.1 + react: 18.3.1 + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.15 + ts-interface-checker: 0.1.13 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tailwindcss@3.4.19: + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 3.1.3 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-import: 15.1.0(postcss@8.5.6) + postcss-js: 4.1.0(postcss@8.5.6) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6) + postcss-nested: 6.2.0(postcss@8.5.6) + postcss-selector-parser: 6.1.2 + resolve: 1.22.11 + sucrase: 3.35.1 + transitivePeerDependencies: + - tsx + - yaml + + text-table@0.2.0: {} + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + ts-api-utils@2.4.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-interface-checker@0.1.13: {} + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.20.2: {} + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typescript@5.9.3: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + undici-types@6.21.0: {} + + unrs-resolver@1.11.1: + dependencies: + napi-postinstall: 0.3.4 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.11.1 + '@unrs/resolver-binding-android-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-x64': 1.11.1 + '@unrs/resolver-binding-freebsd-x64': 1.11.1 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-arm64-musl': 1.11.1 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1 + '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-musl': 1.11.1 + '@unrs/resolver-binding-wasm32-wasi': 1.11.1 + '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 + '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 + '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.20 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.20: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.1.2 + + wrappy@1.0.2: {} + + yocto-queue@0.1.0: {} diff --git a/frontend/postcss.config.mjs b/frontend/postcss.config.mjs new file mode 100644 index 0000000..1a69fd2 --- /dev/null +++ b/frontend/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + }, +}; + +export default config; diff --git a/frontend/public/muse.svg b/frontend/public/muse.svg new file mode 100644 index 0000000..32decb4 --- /dev/null +++ b/frontend/public/muse.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/app/favicon.ico b/frontend/src/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/frontend/src/app/favicon.ico differ diff --git a/frontend/src/app/fonts/GeistMonoVF.woff b/frontend/src/app/fonts/GeistMonoVF.woff new file mode 100644 index 0000000..f2ae185 Binary files /dev/null and b/frontend/src/app/fonts/GeistMonoVF.woff differ diff --git a/frontend/src/app/fonts/GeistVF.woff b/frontend/src/app/fonts/GeistVF.woff new file mode 100644 index 0000000..1b62daa Binary files /dev/null and b/frontend/src/app/fonts/GeistVF.woff differ diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css new file mode 100644 index 0000000..13d40b8 --- /dev/null +++ b/frontend/src/app/globals.css @@ -0,0 +1,27 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +body { + color: var(--foreground); + background: var(--background); + font-family: Arial, Helvetica, sans-serif; +} + +@layer utilities { + .text-balance { + text-wrap: balance; + } +} diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx new file mode 100644 index 0000000..d948f12 --- /dev/null +++ b/frontend/src/app/layout.tsx @@ -0,0 +1,38 @@ +import type { Metadata } from 'next'; +import localFont from 'next/font/local'; +import './globals.css'; +import { Header, Footer } from '@/components'; + +const geistSans = localFont({ + src: './fonts/GeistVF.woff', + variable: '--font-geist-sans', + weight: '100 900', +}); +const geistMono = localFont({ + src: './fonts/GeistMonoVF.woff', + variable: '--font-geist-mono', + weight: '100 900', +}); + +export const metadata: Metadata = { + title: 'KOL Insight - 云图数据查询分析', + description: 'KOL 视频数据查询与成本分析工具 - 麦秒思AI制作', +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + +
+
+
{children}
+
+
+ + + ); +} diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx new file mode 100644 index 0000000..6fe62d1 --- /dev/null +++ b/frontend/src/app/page.tsx @@ -0,0 +1,101 @@ +import Image from "next/image"; + +export default function Home() { + return ( +
+ ); +} diff --git a/frontend/src/components/Footer.tsx b/frontend/src/components/Footer.tsx new file mode 100644 index 0000000..87558a2 --- /dev/null +++ b/frontend/src/components/Footer.tsx @@ -0,0 +1,9 @@ +export default function Footer() { + return ( +
+
+ © 2026 麦秒思AI制作 | KOL Insight v1.0 +
+
+ ); +} diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx new file mode 100644 index 0000000..98a94a3 --- /dev/null +++ b/frontend/src/components/Header.tsx @@ -0,0 +1,18 @@ +import Image from 'next/image'; + +export default function Header() { + return ( +
+
+
+ 麦秒思AI Logo +
+

KOL Insight

+

云图数据查询分析

+
+
+
麦秒思AI制作
+
+
+ ); +} diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts new file mode 100644 index 0000000..b291154 --- /dev/null +++ b/frontend/src/components/index.ts @@ -0,0 +1,2 @@ +export { default as Header } from './Header'; +export { default as Footer } from './Footer'; diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts new file mode 100644 index 0000000..783abf8 --- /dev/null +++ b/frontend/tailwind.config.ts @@ -0,0 +1,39 @@ +import type { Config } from 'tailwindcss'; + +const config: Config = { + content: [ + './src/pages/**/*.{js,ts,jsx,tsx,mdx}', + './src/components/**/*.{js,ts,jsx,tsx,mdx}', + './src/app/**/*.{js,ts,jsx,tsx,mdx}', + ], + theme: { + extend: { + colors: { + background: 'var(--background)', + foreground: 'var(--foreground)', + // 品牌色系 + primary: { + DEFAULT: '#4F46E5', + light: '#818CF8', + dark: '#3730A3', + }, + success: '#10B981', + warning: '#F59E0B', + error: '#EF4444', + info: '#3B82F6', + }, + fontFamily: { + sans: [ + 'Inter', + '-apple-system', + 'BlinkMacSystemFont', + 'PingFang SC', + 'Microsoft YaHei', + 'sans-serif', + ], + }, + }, + }, + plugins: [], +}; +export default config; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..7b28589 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +}