diff --git a/.dockerignore b/.dockerignore index 8c323e5..acbbf99 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,3 +7,4 @@ frontend/node_modules/ # 忽略 kubernetes.yaml kubernetes.yaml +config-overrides.json diff --git a/.env.example b/.env.example index 7d1feb2..a05bc14 100644 --- a/.env.example +++ b/.env.example @@ -19,3 +19,35 @@ PORT=3000 # 在Linux/Mac终端: openssl rand -hex 32 # 或者在Node.js中: require('crypto').randomBytes(32).toString('hex') WEBHOOK_SECRET=your_webhook_secret + +# Agent审查配置(默认关闭,开启请设置为agent) +REVIEW_ENGINE=legacy +REVIEW_WORKDIR=/tmp/gitea-assistant +REVIEW_MODEL_PLANNER=gpt-4o-mini +REVIEW_MODEL_SPECIALIST=gpt-4o-mini +REVIEW_MODEL_JUDGE=gpt-4o-mini +REVIEW_MAX_PARALLEL_RUNS=2 +REVIEW_MAX_FILES_PER_RUN=200 +REVIEW_MAX_FILE_CONTENT_CHARS=40000 +REVIEW_AUTO_PUBLISH_MIN_CONFIDENCE=0.8 +REVIEW_ENABLE_HUMAN_GATE=true +REVIEW_ALLOWED_COMMANDS=git,rg,cat,sed,wc +REVIEW_COMMAND_TIMEOUT_MS=10000 + +# 向量记忆和学习系统配置(可选,第二阶段功能) +# Qdrant向量数据库URL(如果不配置则禁用记忆系统) +QDRANT_URL=http://localhost:6333 +# 是否启用记忆系统(需要先配置QDRANT_URL) +ENABLE_MEMORY=false +# Few-shot学习示例数量(0-20) +FEW_SHOT_EXAMPLES_COUNT=10 + +# Reflection和Debate配置(可选,第三阶段功能) +# 是否启用Reflection自我批评机制(提升审查质量) +ENABLE_REFLECTION=false +# Reflection最大轮次(1-5) +MAX_REFLECTION_ROUNDS=2 +# 是否启用Debate多代理辩论机制(提升高严重性问题准确性) +ENABLE_DEBATE=false +# Debate触发阈值(high=仅高严重性, medium=高和中等严重性) +DEBATE_THRESHOLD=high diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f6a74b8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: CI + +on: + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Lint + run: bun run lint + continue-on-error: true # Pre-existing lint violations — non-blocking until cleanup + + - name: Type check + run: bun run build + + - name: Run tests + run: bun test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..33ecd32 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,81 @@ +name: Release + +on: + push: + branches: + - main + tags: + - 'v*' + +permissions: + contents: read + packages: write + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Lint + run: bun run lint + continue-on-error: true + + - name: Type check + run: bun run build + + - name: Run tests + run: bun test + + docker: + needs: test + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,prefix= + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.gitignore b/.gitignore index 8fb0065..90640fc 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ node_modules/ dist/ .env kubernetes.yaml +config-overrides.json +.sisyphus/ +e2e/.env.e2e diff --git a/Dockerfile b/Dockerfile index df4946a..063d3a2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ FROM oven/bun:1 as frontend-builder WORKDIR /app/frontend # 拷贝前端的 package.json 和 lockfile -COPY frontend/package.json frontend/bun.lockb ./ +COPY frontend/package.json frontend/bun.lock* ./ # 安装前端依赖 RUN bun install --frozen-lockfile @@ -22,7 +22,7 @@ FROM oven/bun:1 as backend-builder WORKDIR /app # 拷贝后端的 package.json 和 lockfile -COPY package.json bun.lockb ./ +COPY package.json bun.lock* ./ # 只安装生产环境依赖 RUN bun install --frozen-lockfile --production diff --git a/Makefile b/Makefile deleted file mode 100644 index b00c216..0000000 --- a/Makefile +++ /dev/null @@ -1,40 +0,0 @@ -NAME = gitea-assistant -REGISTRY = docker-hosted.nexus.satfabric.com -SHA1 = $(shell git rev-parse HEAD) -REVISION = $(shell git rev-list --count HEAD) -LABELS = --label SHA1=${SHA1} --label REVISION=${REVISION} -VERSION = $(shell ./auto-ver.sh).${REVISION} - -OS ?= linux -ARCH ?= amd64 - -.PHONY: build -build: - npm run build - -.PHONY: container.build -container.build: - make build - docker buildx build --platform=${OS}/${ARCH} -t ${REGISTRY}/${NAME}:v${VERSION} ${LABELS} -f Dockerfile . - -.PHONY: container.push -container.push: - make build - docker buildx build --platform=${OS}/${ARCH} -t ${REGISTRY}/${NAME}:v${VERSION} ${LABELS} -f Dockerfile . --push - -.PHONY: k8s.yaml -k8s.yaml: - cp ./kubernetes.yaml.template ./kubernetes.yaml - sed -i.bak 's@<%= IMAGE_FROM %>@registry.kuiper.com/${NAME}:v${VERSION}@g' ./kubernetes.yaml - sed -i.bak 's@<%= APP_NAME %>@${NAME}@g' ./kubernetes.yaml - sed -i.bak 's@<%= VERSION %>@${VERSION}@g' ./kubernetes.yaml - rm -f ./kubernetes.yaml.bak - -.PHONY: help -help: - @echo 'Usage: make [target]' - - @echo 'Available targets:' - @printf " %-25s %s\n" "container." "本地构建(或构建加推送)容器镜像,若不执行TAG参数,则自动生成VERSION字段" - @printf " %-25s %s\n" "k8s.yaml" "生成kubernetes.yaml文件" - @printf " %-25s %s\n" "help" "显示此帮助信息" diff --git a/README.md b/README.md index e5b670d..5c33231 100644 --- a/README.md +++ b/README.md @@ -1,389 +1,180 @@ -# Gitea Assistant +# Gitea AI Assistant -Gitea功能增强助手,基于Bun和TypeScript开发,提供AI驱动的代码审查等增强功能。本工具通过Webhook与Gitea集成,自动对Pull Request和提交进行代码审查,并提供智能化的代码质量分析。 +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -## 功能特点 +AI-powered code review assistant for Gitea. Automatically reviews Pull Requests and commits using OpenAI, providing intelligent code quality analysis with both summary comments and line-level feedback. -- ✅ 自动对Gitea Pull Request进行代码审查 -- ✅ 自动对成功状态的单个提交进行代码审查 -- ✅ 使用OpenAI API进行代码分析 -- ✅ 提供总体代码审查评论 -- ✅ 支持代码行级别评论 -- ✅ 安全的Webhook验证 -- ✅ 飞书通知集成 -- ✅ 异步处理机制 -- ✅ 智能PR关联分析 -- ✅ 灵活的审查规则配置 -- ✅ **后台管理页面**: 提供UI界面,用于自动配置项目的Webhook。 +**[中文文档](./docs/README.zh-CN.md)** -## 架构设计 +## Features -### 核心组件 +- 🤖 **AI Code Review** - Automatic review of PRs and commits using OpenAI models +- 📝 **Line-Level Comments** - Precise feedback on specific code changes +- 🔄 **Dual Review Engines** - Legacy (simple) or Agent-based (multi-agent) review modes +- 🔔 **Feishu Notifications** - Integrated notification system for PR events +- 🎛️ **Admin Dashboard** - Web UI for managing repository webhooks and configuration +- 🔐 **Secure Webhooks** - HMAC-SHA256 signature verification -1. **Webhook处理层** - - 统一的Webhook端点处理 - - 事件类型自动识别 - - 请求签名验证 - - 异步处理机制 +## Architecture -2. **代码审查引擎** - - 差异分析 - - 文件变更追踪 - - 智能PR关联 - - 审查结果格式化 - -3. **AI集成层** - - OpenAI API集成 - - 可配置的提示模板 - - 结果解析和格式化 - - 错误处理和重试机制 - -4. **通知系统** - - 飞书Webhook集成 - - 多类型通知支持 - - 通知模板配置 - - 失败重试机制 - -### 安全特性 - -- Webhook请求签名验证 -- 环境变量配置管理 -- 敏感信息保护 -- 开发环境安全控制 -- 防时序攻击保护 - -### 性能优化 - -- 异步处理机制 -- 批量处理优化 -- 缓存策略 -- 资源使用监控 -- 错误重试机制 - -### 扩展性 - -- 模块化设计 -- 插件化架构 -- 配置驱动 -- 自定义审查规则 -- 多通知渠道支持 - -## 技术栈 - -- Bun -- TypeScript -- Hono (轻量级Web框架) -- OpenAI API -- Gitea API - -## 安装 - -1. 克隆仓库 - - ```bash - git clone - cd ai-review - ``` - -2. 安装依赖 - - ```bash - bun install - ``` - -3. 配置环境变量 - - 复制.env.example文件为.env并填写必要配置: - - ```bash - cp .env.example .env - ``` - - 编辑.env文件,填写Gitea和OpenAI相关配置。 - -## 配置项 - -- `GITEA_API_URL`: Gitea API URL (例如: `http://your-gitea-instance.com/api/v1`) -- `GITEA_ACCESS_TOKEN`: 用于代码审查的 Gitea 访问令牌 (需要仓库读权限和评论权限)。 -- `GITEA_ADMIN_TOKEN`: **(可选)** 用于后台管理的 Gitea 管理员令牌 (需要仓库读写及 Webhook 管理权限)。若不提供,则使用 `GITEA_ACCESS_TOKEN`。 -- `OPENAI_BASE_URL`: OpenAI 请求地址 -- `OPENAI_API_KEY`: OpenAI API密钥 -- `OPENAI_MODEL`:OpenAI 使用模型 -- `CUSTOM_SUMMARY_PROMPT`: 自定义总结审查提示 (可选) -- `CUSTOM_LINE_COMMENT_PROMPT`: 自定义行评论提示 (可选) -- `PORT`: 应用监听端口 (默认: 3000) -- `WEBHOOK_SECRET`: Webhook秘钥,用于验证请求来源 -- `FEISHU_WEBHOOK_URL`: 飞书Webhook地址,用于发送通知 -- `FEISHU_WEBHOOK_SECRET`: 飞书Webhook秘यो (可选) -- `ADMIN_PASSWORD`: 后台管理页面的登录密码 (默认: `password`) -- `JWT_SECRET`: 用于签发后台登录Token的秘钥 (默认会使用一个安全字符串) - -## 使用方法 - -1. 启动服务 - - ```bash - bun run dev # 开发模式 - # 或 - bun run start # 生产模式 - ``` - -2. **(新)** 访问后台管理页面自动配置 - - 启动服务后,直接在浏览器中访问 `http://your-server:3000`。您会看到一个登录页面。 - - **登录**: 使用您在环境变量中设置的 `ADMIN_PASSWORD` 进行登录。 - - **管理仓库**: 登录后,您将看到 Gitea 实例上的仓库列表。 - - **一键启用/停用**: 点击每行最右侧的按钮,即可为该仓库自动创建或删除 AI 代码审查所需的 Webhook。 - - > 强烈推荐使用此方法来代替手动配置,它更简单、更不容易出错。 - -3. 在Gitea仓库中**手动**配置Webhook - - 如果您不想使用后台管理功能,也可以继续手动配置。在Gitea仓库设置中添加Webhook: - - **统一Webhook端点**: - - URL: `http://your-server:3000/webhook/gitea` - - 内容类型: `application/json` - - 秘钥: 设置为与`WEBHOOK_SECRET`环境变量相同的值 - - 触发事件: 选择"Pull Request"和"Status"事件 - - > 注意: 系统使用统一的webhook端点处理所有事件类型,包括Pull Request和Commit Status事件。 - -### Webhook签名验证 - -为确保请求安全,系统使用Gitea的Webhook签名验证机制: - -1. 设置环境变量`WEBHOOK_SECRET`为一个安全的随机字符串 -2. 在Gitea的Webhook配置中,使用相同的字符串作为"秘钥" -3. 每次请求时,系统会验证请求头中的`X-Gitea-Signature` -4. 如果签名验证失败,请求会被拒绝处理 -5. 在开发环境下(`NODE_ENV=development`),如果没有提供签名,系统会跳过验证 - -验证方法使用SHA-256哈希算法,在处理高负载的情况下这能防止恶意请求并保证请求来源的真实性。 - -## 功能说明 - -### PR代码审查 - -当PR被创建或更新时,系统会自动进行代码审查,提供总体评价和行级评论。 - -### PR通知功能 - -系统支持通过飞书发送PR相关的通知: - -1. **PR创建通知** - - 当PR被创建且有指定审阅者时 - - 通知内容包括PR标题和链接 - - 通知会发送给所有指定的审阅者 - -2. **PR审阅者指派通知** - - 当有新的审阅者被指派到PR时 - - 通知内容包括PR标题和链接 - - 通知会发送给新指派的审阅者 - -### 单个提交审查 - -当提交状态变为"success"(如CI通过)时,系统会: - -1. 对该提交进行代码审查 -2. 提供总体评价作为提交评论 -3. 尝试找到关联的PR,添加行级评论 - -这对于增量工作尤其有用,可以只对最新的变更进行审查,避免重复评审已审查过的代码。 - -## 代码审查规则 - -### 总体评价规则 - -系统会从以下几个方面对代码进行总体评价: - -1. **代码质量** - - 代码结构是否清晰 - - 命名是否规范 - - 代码是否易于维护 - - 是否有重复代码 - -2. **潜在问题** - - 是否存在明显的逻辑错误 - - 是否有未处理的异常情况 - - 是否有边界条件未考虑 - -3. **性能考虑** - - 是否存在性能瓶颈 - - 是否有不必要的计算或循环 - - 内存使用是否合理 - -4. **安全性** - - 是否有潜在的安全漏洞 - - 敏感信息处理是否安全 - - 输入验证是否充分 - -5. **最佳实践** - - 是否符合语言/框架的最佳实践 - - 是否遵循设计模式 - - 是否有适当的注释和文档 - -### 行级评论规则 - -系统只会在以下情况下对特定代码行提供评论: - -1. **严重问题** - - 明显的bug或逻辑错误 - - 可能导致系统崩溃的代码 - - 严重的安全漏洞 - -2. **性能问题** - - 明显的性能瓶颈 - - 不必要的高复杂度操作 - - 资源使用不当 - -3. **数据一致性问题** - - 可能导致数据不一致的操作 - - 并发访问问题 - - 事务处理不当 - -4. **代码规范问题** - - 严重违反代码规范的情况 - - 可能导致维护困难的结构 - -> 注意:系统默认采用保守策略,不会对没有明显问题的代码行提供评论,以避免产生过多的噪音。 - -## 开发 - -- `bun run dev`: 开发模式运行 -- `bun run build`: 构建项目 -- `bun run start`: 生产模式运行 -- `bun run lint`: 运行代码风格检查 - -## 许可证 - -MIT - -## 自定义AI审查提示 - -默认情况下,AI代码审查工具配置为只对明显的bug和严重问题进行评论。你可以通过环境变量自定义AI使用的提示: - -### 自定义总结提示 - -设置`CUSTOM_SUMMARY_PROMPT`环境变量来自定义代码审查总结。你可以在提示中使用以下变量,它们会在运行时被自动替换: - -- `${context.diffContent}` - 代码差异内容 -- `${JSON.stringify(fileInfo, null, 2)}` - 变更文件的完整信息 - -### 自定义行评论提示 - -设置`CUSTOM_LINE_COMMENT_PROMPT`环境变量来自定义行级评论生成。你可以在提示中使用以下变量: - -- `${file.path}` - 当前文件路径 -- `${fileContent}` - 文件的完整内容 -- ```${file.changes.map(c => `${c.lineNumber}: ${c.content} (${c.type === 'add' ? '新增' : '上下文'})`).join('\n')}``` - 变更行的上下文 - -请确保你的自定义提示返回正确的格式,特别是对于行评论,必须返回有效的JSON数组。 - -## 部署 - -### Docker部署 - -1. 构建Docker镜像 - - ```bash - docker build -t gitea-assistant . - ``` - -2. 运行容器 - - ```bash - docker run -d \ - --name gitea-assistant \ - -p 3000:3000 \ - --env-file .env \ - gitea-assistant - ``` - -### Kubernetes部署 - -1. 使用提供的kubernetes.yaml模板 - - ```bash - cp kubernetes.yaml.template kubernetes.yaml - ``` - -2. 编辑kubernetes.yaml,更新环境变量和配置 - -3. 部署到Kubernetes集群 - - ```bash - kubectl apply -f kubernetes.yaml - ``` - -## 监控和日志 - -- 应用日志可以通过Docker或Kubernetes的标准日志收集机制获取 -- 建议配置日志聚合服务(如ELK、Grafana Loki等)进行日志管理 -- 关键操作(如代码审查、Webhook处理)都会记录详细日志 -- 错误和异常会被记录并包含堆栈跟踪信息 - -## 故障排除 - -### 常见问题 - -1. **Webhook验证失败** - - 检查`WEBHOOK_SECRET`环境变量是否与Gitea配置匹配 - - 确保请求头中的`X-Gitea-Signature`正确传递 - -2. **OpenAI API调用失败** - - 验证`OPENAI_API_KEY`是否正确设置 - - 检查网络连接和API端点可访问性 - - 确认API配额是否充足 - -3. **Gitea API调用失败** - - 验证`GITEA_ACCESS_TOKEN`是否有效 - - 检查Gitea实例是否可访问 - - 确认令牌权限是否足够 - -### 调试模式 - -在开发环境中,可以设置以下环境变量启用调试模式: - -```bash -DEBUG=true -NODE_ENV=development +``` +┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ +│ Gitea Server │────▶│ Gitea Assistant │────▶│ OpenAI API │ +│ (Webhooks) │ │ (Hono + Bun) │ │ │ +└─────────────────┘ └──────────────────┘ └─────────────────┘ + │ + ▼ + ┌──────────────────┐ + │ Admin Dashboard │ + │ (React SPA) │ + └──────────────────┘ ``` -## 贡献指南 +### Review Engines -### 开发流程 +| Engine | Description | Use Case | +|--------|-------------|----------| +| `legacy` | Single-pass AI review with summary + line comments | Simple, fast reviews | +| `agent` | Multi-agent orchestration with specialists, reflection, and debate | Deep, comprehensive analysis | -1. Fork项目并创建特性分支 -2. 提交变更并编写测试 -3. 确保代码通过lint检查 -4. 提交Pull Request +## Quick Start -### 代码规范 +### Prerequisites -- 使用TypeScript编写代码 -- 遵循项目中的tslint配置 -- 编写清晰的注释和文档 -- 保持代码风格一致 +- [Bun](https://bun.sh/) >= 1.2.5 +- Gitea instance with API access +- OpenAI API key -### 测试 +### Installation -- 编写单元测试覆盖新功能 -- 确保现有测试通过 -- 测试Webhook处理逻辑 -- 验证AI审查功能 +```bash +git clone https://github.com/user/gitea-ai-assistant.git +cd gitea-ai-assistant +bun install +cp .env.example .env +``` -## 版本历史 +### Configuration -- v1.0.0: 初始版本发布 - - 基础代码审查功能 - - Webhook集成 - - OpenAI集成 - - 飞书通知支持 +Edit `.env` with your settings: -## 社区支持 +```bash +# Gitea +GITEA_API_URL=https://your-gitea-instance.com/api/v1 +GITEA_ACCESS_TOKEN=your_gitea_token -- 提交Issue报告问题 -- 参与讨论和功能建议 -- 贡献代码和文档 -- 分享使用经验 +# OpenAI +OPENAI_API_KEY=your_openai_key +OPENAI_MODEL=gpt-4o-mini + +# Security +WEBHOOK_SECRET=your_webhook_secret # openssl rand -hex 32 + +# Admin Dashboard +ADMIN_PASSWORD=your_admin_password +``` + +See [Configuration Reference](#configuration-reference) for all options. + +### Running + +```bash +bun run dev # Development mode +bun run start # Production mode +``` + +### Setting Up Webhooks + +**Option 1: Admin Dashboard (Recommended)** + +1. Access `http://your-server:3000` +2. Log in with `ADMIN_PASSWORD` +3. Click "Enable" on repositories to auto-configure webhooks + +**Option 2: Manual Configuration** + +In Gitea repository settings, add a webhook: +- **URL**: `http://your-server:3000/webhook/gitea` +- **Content Type**: `application/json` +- **Secret**: Same as `WEBHOOK_SECRET` +- **Events**: "Pull Request" and "Status" + +## Configuration Reference + +### Core Settings + +| Variable | Description | Default | +|----------|-------------|---------| +| `GITEA_API_URL` | Gitea API endpoint | Required | +| `GITEA_ACCESS_TOKEN` | Token for code review (read + comment permissions) | Required | +| `GITEA_ADMIN_TOKEN` | Token for webhook management (optional) | - | +| `OPENAI_BASE_URL` | OpenAI API base URL | `https://api.openai.com/v1` | +| `OPENAI_API_KEY` | OpenAI API key | Required | +| `OPENAI_MODEL` | Model to use | `gpt-4o-mini` | +| `PORT` | Server port | `3000` | +| `WEBHOOK_SECRET` | Webhook signature secret | Required | + +### Custom Prompts + +| Variable | Description | +|----------|-------------| +| `CUSTOM_SUMMARY_PROMPT` | Override the default summary review prompt | +| `CUSTOM_LINE_COMMENT_PROMPT` | Override the default line comment prompt | + +### Admin Dashboard + +| Variable | Description | Default | +|----------|-------------|---------| +| `ADMIN_PASSWORD` | Dashboard login password | `password` | +| `JWT_SECRET` | JWT signing secret | Auto-generated | + +### Feishu Integration + +| Variable | Description | +|----------|-------------| +| `FEISHU_WEBHOOK_URL` | Feishu bot webhook URL | +| `FEISHU_WEBHOOK_SECRET` | Feishu webhook secret (optional) | + +### Agent Review Engine + +Enable with `REVIEW_ENGINE=agent` for advanced multi-agent reviews: + +| Variable | Description | Default | +|----------|-------------|---------| +| `REVIEW_ENGINE` | Engine mode (`legacy` or `agent`) | `legacy` | +| `REVIEW_WORKDIR` | Working directory for repo clones | `/tmp/gitea-assistant` | +| `REVIEW_MODEL_PLANNER` | Planner model | `gpt-4o-mini` | +| `REVIEW_MODEL_SPECIALIST` | Specialist model | `gpt-4o-mini` | +| `REVIEW_MODEL_JUDGE` | Judge model | `gpt-4o-mini` | +| `REVIEW_MAX_PARALLEL_RUNS` | Max concurrent tasks | `2` | +| `REVIEW_MAX_FILES_PER_RUN` | Max files per review | `200` | +| `REVIEW_AUTO_PUBLISH_MIN_CONFIDENCE` | Min confidence for auto-publish | `0.8` | +| `REVIEW_ENABLE_HUMAN_GATE` | Enable human approval | `true` | + +### Memory & Learning (Experimental) + +| Variable | Description | Default | +|----------|-------------|---------| +| `QDRANT_URL` | Qdrant vector database URL | - | +| `ENABLE_MEMORY` | Enable memory system | `false` | +| `ENABLE_REFLECTION` | Enable self-critique | `false` | +| `ENABLE_DEBATE` | Enable multi-agent debate | `false` | + +## Deployment + +### Docker + +```bash +docker build -t gitea-assistant . +docker run -d -p 3000:3000 --env-file .env gitea-assistant +``` + +### Docker Compose + +```bash +docker-compose up -d +``` + +## License + +MIT License diff --git a/auto-ver.sh b/auto-ver.sh deleted file mode 100755 index fc48252..0000000 --- a/auto-ver.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/sh - -# 生成基于日期的版本号函数 -generate_date_version() { - date +"%Y.%-m.%-d" -} - -# 由于可能允许在detached状态下执行脚本,因此我们允许外部指定HEAD的REF -HEAD_REF_NAME=$1 -if [ -z "$HEAD_REF_NAME" ]; then - HEAD_REF_NAME=$(git rev-parse --abbrev-ref HEAD) -fi -if [ "$HEAD_REF_NAME" = "main" ]; then - # 对于main分支,使用日期格式的版本号 - generate_date_version - exit 0 -fi -TAG_RECENT=$(git describe --tags --abbrev=0 --match "v*" 2>/dev/null) -if [ -z "$TAG_RECENT" ]; then - # 无法获取到符合semver格式的tag时,使用日期格式的版本号 - generate_date_version - exit 0 -fi -TAG_COMMIT=$(git log $TAG_RECENT --oneline -1 | awk '{print $1}') -TAG_VERSION=$(echo $TAG_RECENT | tr -d "v") - -sub_version_by_type() -{ - type=$1 - field=3 - if [ "$type" = "major" ]; then - field=1 - elif [ "$type" = "minor" ]; then - field=2 - else - field=3 - fi - echo $TAG_VERSION | awk -F '.' -v field=$field '{print $field}' -} - -MAJOR=$(sub_version_by_type major) -MINOR=$(sub_version_by_type minor) -PATCH=$(sub_version_by_type patch) -REVISION_TO_HEAD=$(git rev-list --count $TAG_COMMIT...HEAD) - -echo "$MAJOR.$MINOR.$(($PATCH+$REVISION_TO_HEAD))" diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..e39ead2 --- /dev/null +++ b/biome.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "correctness": { + "noUnusedImports": "warn", + "noUnusedVariables": "warn" + }, + "style": { + "noNonNullAssertion": "off", + "useImportType": "off" + }, + "suspicious": { + "noExplicitAny": "off", + "noExportsInTest": "off", + "noImplicitAnyLet": "off" + }, + "complexity": { + "noForEach": "off" + } + } + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 100 + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "semicolons": "always", + "trailingCommas": "es5" + } + }, + "files": { + "ignore": [ + "node_modules", + "dist", + "frontend", + "*.json", + "*.md" + ] + } +} diff --git a/bun.lock b/bun.lock index 786c774..4d2dc3d 100644 --- a/bun.lock +++ b/bun.lock @@ -5,29 +5,50 @@ "name": "ai-review", "dependencies": { "@hono/zod-validator": "^0.4.3", + "@qdrant/js-client-rest": "^1.16.2", "axios": "^1.8.3", "dotenv": "^16.4.7", - "hono": "^4.7.4", + "hono": "^4.11.9", "lodash-es": "^4.17.21", "openai": "^4.87.3", - "zod": "^3.24.2", + "zod": "^3.25.1", + "zod-to-json-schema": "^3.25.1", }, "devDependencies": { + "@biomejs/biome": "^1.9.4", "@types/lodash-es": "^4.17.12", "@types/node": "^22.13.10", - "tslint": "^6.1.3", + "concurrently": "^9.2.1", "typescript": "^5.8.2", }, }, }, "packages": { - "@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="], + "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], "@hono/zod-validator": ["@hono/zod-validator@0.4.3", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.19.1" } }, "sha512-xIgMYXDyJ4Hj6ekm9T9Y27s080Nl9NXHcJkOvkXPhubOLj8hZkOL8pDnnXfvCf5xEE8Q4oMFenQUZZREUY2gqQ=="], - "@types/lodash": ["@types/lodash@4.17.16", "", {}, "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g=="], + "@qdrant/js-client-rest": ["@qdrant/js-client-rest@1.16.2", "", { "dependencies": { "@qdrant/openapi-typescript-fetch": "1.2.6", "undici": "^6.0.0" }, "peerDependencies": { "typescript": ">=4.7" } }, "sha512-Zm4wEZURrZ24a+Hmm4l1QQYjiz975Ep3vF0yzWR7ICGcxittNz47YK2iBOk8kb8qseCu8pg7WmO1HOIsO8alvw=="], + + "@qdrant/openapi-typescript-fetch": ["@qdrant/openapi-typescript-fetch@1.2.6", "", {}, "sha512-oQG/FejNpItrxRHoyctYvT3rwGZOnK4jr3JdppO/c78ktDvkWiPXPHNsrDf33K9sZdRb6PR7gi4noIapu5q4HA=="], + + "@types/lodash": ["@types/lodash@4.17.23", "", {}, "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA=="], "@types/lodash-es": ["@types/lodash-es@4.17.12", "", { "dependencies": { "@types/lodash": "*" } }, "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ=="], @@ -39,42 +60,36 @@ "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], - "ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], "axios": ["axios@1.8.3", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A=="], - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - - "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], - - "builtin-modules": ["builtin-modules@1.1.1", "", {}, "sha512-wxXCdllwGhI2kCC0MnvTGYTMvnVZTvqgypkiTI8Pa5tcz2i6VqsqwYGgqwXji+4RgCzms6EajE4IxiUH6HH8nQ=="], - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], - "chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - "color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], - "commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], - - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + "concurrently": ["concurrently@9.2.1", "", { "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", "shell-quote": "1.8.3", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng=="], "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], - "diff": ["diff@4.0.2", "", {}, "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="], - "dotenv": ["dotenv@16.4.7", "", {}, "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], @@ -83,9 +98,7 @@ "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], - "escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], - - "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], @@ -97,19 +110,17 @@ "formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="], - "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + "get-intrinsic": ["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" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], - "has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], @@ -117,21 +128,13 @@ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - "hono": ["hono@4.7.4", "", {}, "sha512-Pst8FuGqz3L7tFF+u9Pu70eI0xa5S3LPUmrNd5Jm8nTHze9FxLTK9Kaj5g/k4UcwuJSXTP65SyHOPLrffpcAJg=="], + "hono": ["hono@4.11.9", "", {}, "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ=="], "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], - "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], - - "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - - "js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], - - "lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], + "lodash-es": ["lodash-es@4.17.23", "", {}, "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], @@ -139,50 +142,38 @@ "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - - "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - "openai": ["openai@4.87.3", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" }, "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-d2D54fzMuBYTxMW8wcNmhT1rYKcTfMJ8t+4KjH2KtvYenygITiGBgHoIrzHwnDQWW+C5oCA+ikIR2jgPCFqcKQ=="], - "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], - - "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], - "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], - "semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], - "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], - "supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], - "tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], - "tslint": ["tslint@6.1.3", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "builtin-modules": "^1.1.1", "chalk": "^2.3.0", "commander": "^2.12.1", "diff": "^4.0.1", "glob": "^7.1.1", "js-yaml": "^3.13.1", "minimatch": "^3.0.4", "mkdirp": "^0.5.3", "resolve": "^1.3.2", "semver": "^5.3.0", "tslib": "^1.13.0", "tsutils": "^2.29.0" }, "peerDependencies": { "typescript": ">=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev || >= 3.1.0-dev || >= 3.2.0-dev || >= 4.0.0-dev" }, "bin": { "tslint": "bin/tslint" } }, "sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg=="], - - "tsutils": ["tsutils@2.29.0", "", { "dependencies": { "tslib": "^1.8.1" }, "peerDependencies": { "typescript": ">=2.1.0 || >=2.1.0-dev || >=2.2.0-dev || >=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >= 3.0.0-dev || >= 3.1.0-dev" } }, "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], + "undici": ["undici@6.23.0", "", {}, "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g=="], + "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], "web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="], @@ -191,12 +182,22 @@ "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - "zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], "@types/node-fetch/@types/node": ["@types/node@18.19.80", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-kEWeMwMeIvxYkeg1gTc01awpwLbfMRZXdIhwRcakd/KlK53jmRC26LqcbIt7fnAQTu5GzlnWmzA3H6+l1u6xxQ=="], + "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "openai/@types/node": ["@types/node@18.19.80", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-kEWeMwMeIvxYkeg1gTc01awpwLbfMRZXdIhwRcakd/KlK53jmRC26LqcbIt7fnAQTu5GzlnWmzA3H6+l1u6xxQ=="], "@types/node-fetch/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], diff --git a/docker-compose.e2e.yml b/docker-compose.e2e.yml new file mode 100644 index 0000000..5603594 --- /dev/null +++ b/docker-compose.e2e.yml @@ -0,0 +1,71 @@ +version: '3.8' + +# E2E 测试环境:Gitea + gitea-assistant +# 用法: +# docker compose -f docker-compose.e2e.yml up -d +# # 等待服务启动后运行 seed 脚本: +# ./e2e/seed.sh +# # 运行 E2E 测试: +# ./e2e/test.sh +# # 清理: +# docker compose -f docker-compose.e2e.yml down -v + +services: + gitea: + image: gitea/gitea:1.22 + container_name: e2e-gitea + environment: + - GITEA__database__DB_TYPE=sqlite3 + - GITEA__server__ROOT_URL=http://gitea:3000 + - GITEA__server__HTTP_PORT=3000 + - GITEA__security__INSTALL_LOCK=true + - GITEA__service__DISABLE_REGISTRATION=false + - GITEA__service__REQUIRE_SIGNIN_VIEW=false + - GITEA__webhook__ALLOWED_HOST_LIST=* + - GITEA__webhook__SKIP_TLS_VERIFY=true + ports: + - "3333:3000" + volumes: + - gitea-data:/data + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/api/v1/version"] + interval: 5s + timeout: 3s + retries: 20 + start_period: 10s + + assistant: + build: + context: . + dockerfile: e2e/Dockerfile.e2e + container_name: e2e-assistant + depends_on: + gitea: + condition: service_healthy + environment: + - NODE_ENV=production + - GITEA_API_URL=http://gitea:3000/api/v1 + - GITEA_ACCESS_TOKEN=${E2E_GITEA_TOKEN:-placeholder} + - OPENAI_BASE_URL=${OPENAI_BASE_URL:-https://api.openai.com/v1} + - OPENAI_API_KEY=${OPENAI_API_KEY:-test_key} + - OPENAI_MODEL=${OPENAI_MODEL:-gpt-4o-mini} + - FEISHU_WEBHOOK_URL=http://localhost:9999/noop + - PORT=3000 + - WEBHOOK_SECRET=e2e-test-secret + - REVIEW_ENGINE=agent + - REVIEW_WORKDIR=/tmp/e2e-review + - REVIEW_AUTO_PUBLISH_MIN_CONFIDENCE=0.5 + - REVIEW_ENABLE_HUMAN_GATE=false + - REVIEW_ALLOWED_COMMANDS=git,rg,cat,sed,wc + - REVIEW_COMMAND_TIMEOUT_MS=30000 + ports: + - "3334:3000" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/"] + interval: 5s + timeout: 3s + retries: 10 + start_period: 5s + +volumes: + gitea-data: diff --git a/docker-compose.yml b/docker-compose.yml index 8b01d17..448a658 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,6 +10,8 @@ services: restart: unless-stopped ports: - "3000:3000" + volumes: + - ./config-overrides.json:/app/config-overrides.json env_file: - .env healthcheck: diff --git a/docs/ADMIN_UI_DESIGN.md b/docs/ADMIN_UI_DESIGN.md deleted file mode 100644 index eae53df..0000000 --- a/docs/ADMIN_UI_DESIGN.md +++ /dev/null @@ -1,93 +0,0 @@ -# Gitea AI Assistant 后台管理页面设计方案 - -## 1. 总体目标 - -构建一个简单、安全的后台管理界面,允许管理员浏览 Gitea 上的代码仓库,并为选定的仓库一键“开启”或“关闭” AI 代码审查的 Webhook。此功能旨在替代当前繁琐的手动配置过程,简化新项目的接入流程。 - -## 2. 技术选型 - -- **后端增强**: 继续使用 **Hono** 框架,在现有应用中开辟一组新的 API 路由。 -- **前端框架**: **React (Vite + TypeScript)**,用于快速构建现代化交互界面。 -- **UI 组件库**: **shadcn/ui** 配合 **Tailwind CSS**,以实现专业且美观的界面。 -- **状态管理**: **React Query (TanStack Query)**,用于高效管理服务器状态和数据缓存。 -- **认证机制**: **JSON Web Tokens (JWT)**,实现安全无状态的登录认证。 - -## 3. 架构设计 - -采用前后端分离的架构。 - -- **后端 (Backend)**: - - 在 Hono 应用中新增路由组 `/admin/api`,提供 RESTful API。 - - 通过一个新的管理员级别 Gitea Token 与 Gitea API 交互,该 Token 需具备读写仓库和 Webhook 的权限。 - - 所有 `/admin/api` 路由都将受到 JWT 中间件的保护。 - -- **前端 (Frontend)**: - - 在项目根目录创建新的 `frontend` 文件夹,存放所有前端代码。 - - 前端为单页面应用 (SPA),负责登录、展示仓库列表和提供 Webhook 管理操作。 - -- **部署 (Deployment)**: - - 采用多阶段 `Dockerfile` 进行构建。 - - 第一阶段构建前端静态文件。 - - 第二阶段构建后端服务。 - - 最终镜像将前端静态文件集成到后端服务中,由 Hono 统一提供服务,实现单容器部署。 - - 相应更新 `kubernetes.yaml.template` 文件。 - -## 4. 核心功能模块设计 - -### A. 认证流程 - -1. **访问**: 用户访问管理页面,若无本地有效 JWT,则跳转至登录页。 -2. **登录**: 用户输入在环境变量中配置的 `ADMIN_PASSWORD`。 -3. **验证**: 前端调用 `POST /admin/api/login`,后端验证密码。 -4. **Token 生成**: 验证成功后,后端使用 `JWT_SECRET` 生成一个有时效性的 JWT 并返回给前端。 -5. **存储**: 前端将 JWT 存储在 `localStorage` 中。 -6. **请求**: 后续所有对 `/admin/api` 的请求均在 `Authorization` 请求头中携带 `Bearer `。 - -### B. 后端 API 设计 (`/admin/api`) - -- **`POST /admin/api/login`** (公开) - - **功能**: 用户登录。 - - **请求体**: `{ password: "..." }` - - **响应**: `{ token: "jwt_token" }` 或认证失败错误。 - -- **`GET /admin/api/repositories`** (需认证) - - **功能**: 获取 Gitea 实例上管理员可见的所有仓库列表,并附带其 Webhook 状态。 - - **逻辑**: 调用 Gitea API 获取仓库列表,并对每个仓库检查是否存在由本应用创建的 Webhook。 - - **返回**: `[{ name: "owner/repo", webhook_status: "active" | "inactive" }]` - -- **`POST /admin/api/repositories/{owner}/{repo}/webhook`** (需认证) - - **功能**: 为指定仓库创建 AI Review 的 Webhook。 - - **逻辑**: 调用 Gitea API 创建 Webhook,目标 URL 指向本服务的 `/api/webhook`。 - - **返回**: `{ success: true }` - -- **`DELETE /admin/api/repositories/{owner}/{repo}/webhook`** (需认证) - - **功能**: 删除为 AI Review 创建的 Webhook。 - - **逻辑**: 调用 Gitea API 查找并删除与本服务相关的 Webhook。 - - **返回**: `{ success: true }` - -### C. 前端页面设计 - -- **登录页**: 一个居中的表单,包含密码输入框和“登录”按钮。 -- **管理主页 (Dashboard)**: - - 顶部标题和刷新按钮。 - - 仓库列表,支持按名称搜索/筛选。 - - 列表项包括: - - 仓库名称 (`owner/repo`)。 - - Webhook 状态标识 (例如,彩色的图标和文字)。 - - 操作按钮 (例如,“启用”或“停用”)。 - -## 5. 环境变量配置 - -需要新增以下环境变量: - -- `ADMIN_PASSWORD`: 后台管理页面的登录密码。 -- `JWT_SECRET`: 用于签发和验证 JWT 的密钥。 -- `GITEA_ADMIN_TOKEN`: 一个拥有 Gitea 管理权限的 Token,用于 API 调用。 - -## 6. 实施步骤规划 - -1. **项目初始化**: 创建 `frontend` 目录并初始化 Vite (React+TS) 项目,配置 UI 库。 -2. **后端开发**: 实现 `/admin/api` 路由组,包括登录、JWT 中间件和 Webhook 管理逻辑。 -3. **前端开发**: 创建登录页和管理主页,实现与后端 API 的交互。 -4. **容器化**: 更新 `Dockerfile` 为多阶段构建,并调整 `kubernetes.yaml.template`。 -5. **文档更新**: 在 `README.md` 中说明新功能的使用和配置方法。 diff --git a/docs/README.zh-CN.md b/docs/README.zh-CN.md new file mode 100644 index 0000000..b561482 --- /dev/null +++ b/docs/README.zh-CN.md @@ -0,0 +1,180 @@ +# Gitea AI Assistant + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +基于 AI 的 Gitea 代码审查助手。自动审查 Pull Request 和提交,使用 OpenAI 提供智能代码质量分析,支持总体评论和行级反馈。 + +**[English Documentation](../README.md)** + +## 功能特点 + +- 🤖 **AI 代码审查** - 使用 OpenAI 模型自动审查 PR 和提交 +- 📝 **行级评论** - 针对具体代码变更的精确反馈 +- 🔄 **双引擎模式** - Legacy(简单)或 Agent(多代理)审查模式 +- 🔔 **飞书通知** - PR 事件通知集成 +- 🎛️ **管理后台** - 用于管理仓库 Webhook 和配置的 Web 界面 +- 🔐 **安全验证** - HMAC-SHA256 签名验证 + +## 架构设计 + +``` +┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ +│ Gitea 服务器 │────▶│ Gitea Assistant │────▶│ OpenAI API │ +│ (Webhooks) │ │ (Hono + Bun) │ │ │ +└─────────────────┘ └──────────────────┘ └─────────────────┘ + │ + ▼ + ┌──────────────────┐ + │ 管理后台 │ + │ (React SPA) │ + └──────────────────┘ +``` + +### 审查引擎对比 + +| 引擎 | 描述 | 适用场景 | +|------|------|----------| +| `legacy` | 单次 AI 审查,生成总结和行级评论 | 简单、快速的审查 | +| `agent` | 多代理编排,支持专家、反思和辩论 | 深度、全面的分析 | + +## 快速开始 + +### 环境要求 + +- [Bun](https://bun.sh/) >= 1.2.5 +- 可访问的 Gitea 实例 +- OpenAI API 密钥 + +### 安装步骤 + +```bash +git clone https://github.com/user/gitea-ai-assistant.git +cd gitea-ai-assistant +bun install +cp .env.example .env +``` + +### 配置说明 + +编辑 `.env` 文件: + +```bash +# Gitea +GITEA_API_URL=https://your-gitea-instance.com/api/v1 +GITEA_ACCESS_TOKEN=your_gitea_token + +# OpenAI +OPENAI_API_KEY=your_openai_key +OPENAI_MODEL=gpt-4o-mini + +# 安全 +WEBHOOK_SECRET=your_webhook_secret # openssl rand -hex 32 + +# 管理后台 +ADMIN_PASSWORD=your_admin_password +``` + +完整配置项请参阅 [配置参考](#配置参考)。 + +### 启动服务 + +```bash +bun run dev # 开发模式 +bun run start # 生产模式 +``` + +### 配置 Webhook + +**方式一:管理后台(推荐)** + +1. 在浏览器中访问 `http://your-server:3000` +2. 使用 `ADMIN_PASSWORD` 登录 +3. 点击仓库对应的「启用」按钮自动配置 Webhook + +**方式二:手动配置** + +在 Gitea 仓库设置中添加 Webhook: +- **URL**: `http://your-server:3000/webhook/gitea` +- **内容类型**: `application/json` +- **密钥**: 与 `WEBHOOK_SECRET` 相同 +- **触发事件**: 「Pull Request」和「Status」 + +## 配置参考 + +### 核心配置 + +| 变量 | 描述 | 默认值 | +|------|------|--------| +| `GITEA_API_URL` | Gitea API 地址 | 必填 | +| `GITEA_ACCESS_TOKEN` | 代码审查令牌(需要读取和评论权限) | 必填 | +| `GITEA_ADMIN_TOKEN` | Webhook 管理令牌(可选) | - | +| `OPENAI_BASE_URL` | OpenAI API 基础地址 | `https://api.openai.com/v1` | +| `OPENAI_API_KEY` | OpenAI API 密钥 | 必填 | +| `OPENAI_MODEL` | 使用的模型 | `gpt-4o-mini` | +| `PORT` | 服务端口 | `3000` | +| `WEBHOOK_SECRET` | Webhook 签名验证密钥 | 必填 | + +### 自定义提示词 + +| 变量 | 描述 | +|------|------| +| `CUSTOM_SUMMARY_PROMPT` | 自定义总结审查提示词 | +| `CUSTOM_LINE_COMMENT_PROMPT` | 自定义行级评论提示词 | + +### 管理后台 + +| 变量 | 描述 | 默认值 | +|------|------|--------| +| `ADMIN_PASSWORD` | 后台登录密码 | `password` | +| `JWT_SECRET` | JWT 签名密钥 | 自动生成 | + +### 飞书集成 + +| 变量 | 描述 | +|------|------| +| `FEISHU_WEBHOOK_URL` | 飞书机器人 Webhook 地址 | +| `FEISHU_WEBHOOK_SECRET` | 飞书 Webhook 密钥(可选) | + +### Agent 审查引擎 + +设置 `REVIEW_ENGINE=agent` 启用多代理审查: + +| 变量 | 描述 | 默认值 | +|------|------|--------| +| `REVIEW_ENGINE` | 引擎模式(`legacy` 或 `agent`) | `legacy` | +| `REVIEW_WORKDIR` | 仓库克隆工作目录 | `/tmp/gitea-assistant` | +| `REVIEW_MODEL_PLANNER` | 规划模型 | `gpt-4o-mini` | +| `REVIEW_MODEL_SPECIALIST` | 专家模型 | `gpt-4o-mini` | +| `REVIEW_MODEL_JUDGE` | 判断模型 | `gpt-4o-mini` | +| `REVIEW_MAX_PARALLEL_RUNS` | 最大并发任务数 | `2` | +| `REVIEW_MAX_FILES_PER_RUN` | 单次审查最大文件数 | `200` | +| `REVIEW_AUTO_PUBLISH_MIN_CONFIDENCE` | 自动发布最小置信度 | `0.8` | +| `REVIEW_ENABLE_HUMAN_GATE` | 启用人工审批 | `true` | + +### 记忆与学习(实验性) + +| 变量 | 描述 | 默认值 | +|------|------|--------| +| `QDRANT_URL` | Qdrant 向量数据库地址 | - | +| `ENABLE_MEMORY` | 启用记忆系统 | `false` | +| `ENABLE_REFLECTION` | 启用自我批评 | `false` | +| `ENABLE_DEBATE` | 启用多代理辩论 | `false` | + +## 部署指南 + +### Docker + +```bash +docker build -t gitea-assistant . +docker run -d -p 3000:3000 --env-file .env gitea-assistant +``` + +### Docker Compose + +```bash +docker-compose up -d +``` + +## 许可证 + +MIT 许可证 diff --git a/e2e/Dockerfile.e2e b/e2e/Dockerfile.e2e new file mode 100644 index 0000000..778535d --- /dev/null +++ b/e2e/Dockerfile.e2e @@ -0,0 +1,16 @@ +# E2E 测试用简化 Dockerfile(跳过 frontend 构建) +FROM oven/bun:1 + +WORKDIR /app + +RUN apt-get update && apt-get install -y git ripgrep curl && rm -rf /var/lib/apt/lists/* + +COPY package.json bun.lock* bun.lockb* ./ +RUN bun install --no-frozen-lockfile + +COPY src ./src +COPY tsconfig.json . + +EXPOSE 3000 + +CMD ["bun", "run", "start"] diff --git a/e2e/seed.sh b/e2e/seed.sh new file mode 100755 index 0000000..22bcd26 --- /dev/null +++ b/e2e/seed.sh @@ -0,0 +1,175 @@ +#!/usr/bin/env bash +set -euo pipefail + +# E2E Seed Script +# 初始化 Gitea 测试环境:创建用户、生成 Token、创建仓库、推送代码、配置 Webhook、创建 PR +# +# 前置条件: docker compose -f docker-compose.e2e.yml up -d && 等待 healthy +# 产出: 写入 e2e/.env.e2e 供 test.sh 使用 + +GITEA_URL="http://localhost:3333" +ASSISTANT_URL="http://localhost:3334" +ADMIN_USER="e2e-admin" +ADMIN_PASS="e2ePassword123!" +ADMIN_EMAIL="admin@e2e-test.local" +WEBHOOK_SECRET="e2e-test-secret" +REPO_NAME="e2e-test-repo" + +echo "=== [1/6] 等待 Gitea 就绪 ===" +for i in $(seq 1 30); do + if curl -sf "${GITEA_URL}/api/v1/version" > /dev/null 2>&1; then + echo "Gitea 已就绪" + break + fi + echo " 等待中... ($i/30)" + sleep 2 +done + +echo "=== [2/6] 创建管理员用户 ===" +docker exec e2e-gitea gitea admin user create \ + --username "${ADMIN_USER}" \ + --password "${ADMIN_PASS}" \ + --email "${ADMIN_EMAIL}" \ + --admin \ + --must-change-password=false 2>/dev/null || echo " 用户已存在,跳过" + +echo "=== [3/6] 生成 API Token ===" +TOKEN_RESPONSE=$(curl -sf -X POST "${GITEA_URL}/api/v1/users/${ADMIN_USER}/tokens" \ + -u "${ADMIN_USER}:${ADMIN_PASS}" \ + -H "Content-Type: application/json" \ + -d "{\"name\": \"e2e-token-$(date +%s)\", \"scopes\": [\"all\"]}" 2>/dev/null || true) + +if [ -z "${TOKEN_RESPONSE}" ]; then + echo " Token 创建失败,尝试使用密码认证" + GITEA_TOKEN="" +else + GITEA_TOKEN=$(echo "${TOKEN_RESPONSE}" | grep -o '"sha1":"[^"]*"' | head -1 | cut -d'"' -f4) + if [ -z "${GITEA_TOKEN}" ]; then + GITEA_TOKEN=$(echo "${TOKEN_RESPONSE}" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha1',''))" 2>/dev/null || true) + fi +fi + +if [ -z "${GITEA_TOKEN}" ]; then + echo " ERROR: 无法获取 Token" + exit 1 +fi +echo " Token: ${GITEA_TOKEN:0:8}..." + +echo "=== [4/6] 创建测试仓库并推送代码 ===" +curl -sf -X POST "${GITEA_URL}/api/v1/user/repos" \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{\"name\": \"${REPO_NAME}\", \"auto_init\": true, \"default_branch\": \"main\"}" > /dev/null 2>&1 || echo " 仓库已存在,跳过" + +CLONE_DIR=$(mktemp -d) +trap "rm -rf ${CLONE_DIR}" EXIT + +git clone "http://${ADMIN_USER}:${ADMIN_PASS}@localhost:3333/${ADMIN_USER}/${REPO_NAME}.git" "${CLONE_DIR}/repo" 2>/dev/null + +pushd "${CLONE_DIR}/repo" > /dev/null +git config user.email "e2e@test.local" +git config user.name "E2E Bot" + +mkdir -p src +cat > src/auth.ts << 'TSEOF' +export function authenticate(token: string): boolean { + // 正确的认证实现 + if (!token || token.length < 10) { + return false; + } + return verifyToken(token); +} + +function verifyToken(token: string): boolean { + return token.startsWith('valid-'); +} +TSEOF + +git add -A +git commit -m "initial: add auth module" --allow-empty 2>/dev/null || true +git push origin main 2>/dev/null || true + +git checkout -b feature/add-user-handler +cat > src/user-handler.ts << 'TSEOF' +import { authenticate } from './auth'; + +export function handleUserRequest(input: any) { + // Bug: 没有对 input 做 null 检查 + const userId = input.userId; + + // Bug: SQL 注入风险 + const query = `SELECT * FROM users WHERE id = '${userId}'`; + + // Bug: 硬编码密钥 + const secret = "super-secret-api-key-12345"; + + // Bug: 不安全的 eval + const config = eval(input.config); + + return { query, config }; +} +TSEOF + +git add -A +git commit -m "feat: add user handler" +git push origin feature/add-user-handler 2>/dev/null +popd > /dev/null + +echo "=== [5/6] 配置 Webhook ===" +curl -sf -X POST "${GITEA_URL}/api/v1/repos/${ADMIN_USER}/${REPO_NAME}/hooks" \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{ + \"type\": \"gitea\", + \"active\": true, + \"events\": [\"pull_request\"], + \"config\": { + \"url\": \"http://assistant:3000/webhook/gitea\", + \"content_type\": \"json\", + \"secret\": \"${WEBHOOK_SECRET}\" + } + }" > /dev/null 2>&1 || echo " Webhook 配置失败(可能已存在)" + +echo "=== [6/6] 创建 Pull Request ===" +PR_RESPONSE=$(curl -sf -X POST "${GITEA_URL}/api/v1/repos/${ADMIN_USER}/${REPO_NAME}/pulls" \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{ + \"title\": \"feat: add user handler\", + \"body\": \"Add user request handler with authentication\", + \"head\": \"feature/add-user-handler\", + \"base\": \"main\" + }" 2>/dev/null || true) + +PR_NUMBER=$(echo "${PR_RESPONSE}" | python3 -c "import sys,json; print(json.load(sys.stdin).get('number',''))" 2>/dev/null || echo "") + +if [ -z "${PR_NUMBER}" ]; then + echo " PR 创建失败或已存在,尝试查找现有 PR..." + PR_NUMBER=$(curl -sf "${GITEA_URL}/api/v1/repos/${ADMIN_USER}/${REPO_NAME}/pulls?state=open" \ + -H "Authorization: token ${GITEA_TOKEN}" | \ + python3 -c "import sys,json; prs=json.load(sys.stdin); print(prs[0]['number'] if prs else '')" 2>/dev/null || echo "1") +fi + +echo " PR #${PR_NUMBER} 已创建" + +cat > e2e/.env.e2e << EOF +GITEA_URL=${GITEA_URL} +ASSISTANT_URL=${ASSISTANT_URL} +GITEA_TOKEN=${GITEA_TOKEN} +ADMIN_USER=${ADMIN_USER} +REPO_NAME=${REPO_NAME} +PR_NUMBER=${PR_NUMBER} +EOF + +echo "" +echo "=== Seed 完成 ===" +echo " Gitea: ${GITEA_URL}" +echo " Assistant: ${ASSISTANT_URL}" +echo " Repo: ${ADMIN_USER}/${REPO_NAME}" +echo " PR: #${PR_NUMBER}" +echo " Token: ${GITEA_TOKEN:0:8}..." +echo "" +echo "下一步:" +echo " 1. 更新 assistant 容器的 GITEA_ACCESS_TOKEN:" +echo " E2E_GITEA_TOKEN=${GITEA_TOKEN} docker compose -f docker-compose.e2e.yml up -d assistant" +echo " 2. 运行测试: ./e2e/test.sh" diff --git a/e2e/test.sh b/e2e/test.sh new file mode 100755 index 0000000..c67e59f --- /dev/null +++ b/e2e/test.sh @@ -0,0 +1,150 @@ +#!/usr/bin/env bash +set -euo pipefail + +# E2E Test Script +# 验证 AI 代码审查是否在 PR 上产生了评论 +# +# 前置条件: +# 1. docker compose -f docker-compose.e2e.yml up -d +# 2. ./e2e/seed.sh +# 3. E2E_GITEA_TOKEN=xxx docker compose -f docker-compose.e2e.yml up -d assistant + +ENV_FILE="e2e/.env.e2e" +if [ ! -f "${ENV_FILE}" ]; then + echo "ERROR: ${ENV_FILE} 不存在,请先运行 ./e2e/seed.sh" + exit 1 +fi + +source "${ENV_FILE}" + +MAX_WAIT=180 # 最多等待 3 分钟 +POLL_INTERVAL=5 +PASS=0 +FAIL=0 + +echo "=== E2E 测试开始 ===" +echo " Gitea: ${GITEA_URL}" +echo " Repo: ${ADMIN_USER}/${REPO_NAME}" +echo " PR: #${PR_NUMBER}" +echo "" + +# ─── 测试 1: Assistant 服务健康检查 ─── +echo "[TEST 1] Assistant 服务健康检查" +if curl -sf "${ASSISTANT_URL}/" > /dev/null 2>&1; then + echo " ✅ PASS: Assistant 服务正常" + PASS=$((PASS + 1)) +else + echo " ❌ FAIL: Assistant 服务不可达" + FAIL=$((FAIL + 1)) +fi + +# ─── 测试 2: Gitea API 可用 ─── +echo "[TEST 2] Gitea API 可用性" +VERSION=$(curl -sf "${GITEA_URL}/api/v1/version" | python3 -c "import sys,json; print(json.load(sys.stdin).get('version','unknown'))" 2>/dev/null || echo "unknown") +if [ "${VERSION}" != "unknown" ]; then + echo " ✅ PASS: Gitea v${VERSION}" + PASS=$((PASS + 1)) +else + echo " ❌ FAIL: Gitea API 不可用" + FAIL=$((FAIL + 1)) +fi + +# ─── 测试 3: PR 存在 ─── +echo "[TEST 3] PR 存在性" +PR_STATE=$(curl -sf "${GITEA_URL}/api/v1/repos/${ADMIN_USER}/${REPO_NAME}/pulls/${PR_NUMBER}" \ + -H "Authorization: token ${GITEA_TOKEN}" | \ + python3 -c "import sys,json; print(json.load(sys.stdin).get('state',''))" 2>/dev/null || echo "") + +if [ "${PR_STATE}" = "open" ]; then + echo " ✅ PASS: PR #${PR_NUMBER} 状态为 open" + PASS=$((PASS + 1)) +else + echo " ❌ FAIL: PR #${PR_NUMBER} 状态异常 (${PR_STATE})" + FAIL=$((FAIL + 1)) +fi + +# ─── 测试 4: 等待 AI 审查评论出现 ─── +echo "[TEST 4] AI 审查评论(最多等待 ${MAX_WAIT}s)" +COMMENT_FOUND=false +WAITED=0 + +while [ ${WAITED} -lt ${MAX_WAIT} ]; do + COMMENTS=$(curl -sf "${GITEA_URL}/api/v1/repos/${ADMIN_USER}/${REPO_NAME}/issues/${PR_NUMBER}/comments" \ + -H "Authorization: token ${GITEA_TOKEN}" 2>/dev/null || echo "[]") + + AI_COMMENTS=$(echo "${COMMENTS}" | python3 -c " +import sys, json +comments = json.load(sys.stdin) +ai = [c for c in comments if 'AI' in c.get('body', '') or 'Agent' in c.get('body', '')] +print(len(ai)) +" 2>/dev/null || echo "0") + + if [ "${AI_COMMENTS}" -gt "0" ]; then + COMMENT_FOUND=true + echo " ✅ PASS: 发现 ${AI_COMMENTS} 条 AI 审查评论 (${WAITED}s)" + PASS=$((PASS + 1)) + break + fi + + echo " ⏳ 等待中... (${WAITED}/${MAX_WAIT}s, 已有评论: $(echo "${COMMENTS}" | python3 -c 'import sys,json; print(len(json.load(sys.stdin)))' 2>/dev/null || echo 0))" + sleep ${POLL_INTERVAL} + WAITED=$((WAITED + POLL_INTERVAL)) +done + +if [ "${COMMENT_FOUND}" = false ]; then + echo " ❌ FAIL: ${MAX_WAIT}s 内未发现 AI 审查评论" + FAIL=$((FAIL + 1)) + + echo " --- 调试信息 ---" + echo " PR 所有评论:" + curl -sf "${GITEA_URL}/api/v1/repos/${ADMIN_USER}/${REPO_NAME}/issues/${PR_NUMBER}/comments" \ + -H "Authorization: token ${GITEA_TOKEN}" 2>/dev/null | python3 -m json.tool 2>/dev/null || echo " (无法获取)" + + echo " Assistant review runs:" + curl -sf "${ASSISTANT_URL}/admin/api/review/runs" 2>/dev/null | python3 -m json.tool 2>/dev/null || echo " (无法获取)" +fi + +# ─── 测试 5: Review Run 状态检查 ─── +echo "[TEST 5] Review Run 状态" +ADMIN_JWT=$(curl -sf -X POST "${ASSISTANT_URL}/admin/api/login" \ + -H "Content-Type: application/json" \ + -d '{"password":"password"}' | python3 -c "import sys,json; print(json.load(sys.stdin).get('token',''))" 2>/dev/null || echo "") +RUNS=$(curl -sf "${ASSISTANT_URL}/admin/api/review/runs" \ + -H "Authorization: Bearer ${ADMIN_JWT}" 2>/dev/null || echo "[]") +RUN_COUNT=$(echo "${RUNS}" | python3 -c "import sys,json; d=json.load(sys.stdin); print(len(d.get('data',d if isinstance(d,list) else [])))" 2>/dev/null || echo "0") + +if [ "${RUN_COUNT}" -gt "0" ]; then + echo " ✅ PASS: 发现 ${RUN_COUNT} 个 review run(s)" + PASS=$((PASS + 1)) + + echo "${RUNS}" | python3 -c " +import sys, json +data = json.load(sys.stdin) +runs = data.get('data', data if isinstance(data, list) else data.get('runs', [])) +for r in runs[:3]: + print(f\" - {r.get('id','?')[:8]}... status={r.get('status','?')} attempts={r.get('attempts','?')}\") +" 2>/dev/null || true +else + echo " ❌ FAIL: 无 review runs" + FAIL=$((FAIL + 1)) +fi + +# ─── 结果汇总 ─── +echo "" +echo "=== E2E 测试结果 ===" +TOTAL=$((PASS + FAIL)) +echo " 通过: ${PASS}/${TOTAL}" +echo " 失败: ${FAIL}/${TOTAL}" + +if [ ${FAIL} -gt 0 ]; then + echo "" + echo "⚠️ 部分测试失败。如果 AI 评论测试失败,请确保:" + echo " 1. OPENAI_API_KEY 已正确配置" + echo " 2. assistant 容器的 GITEA_ACCESS_TOKEN 已设置为 seed 生成的 token" + echo " 3. Webhook 已正确触发(检查 Gitea webhook 日志)" + exit 1 +else + echo "" + echo "🎉 所有 E2E 测试通过!" + exit 0 +fi diff --git a/frontend/README.md b/frontend/README.md deleted file mode 100644 index d5e3c12..0000000 --- a/frontend/README.md +++ /dev/null @@ -1,69 +0,0 @@ -# React + TypeScript + Vite - -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: - -```js -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - - // Remove tseslint.configs.recommended and replace with this - tseslint.configs.recommendedTypeChecked, - // Alternatively, use this for stricter rules - tseslint.configs.strictTypeChecked, - // Optionally, add this for stylistic rules - tseslint.configs.stylisticTypeChecked, - - // Other configs... - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) -``` - -You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: - -```js -// eslint.config.js -import reactX from 'eslint-plugin-react-x' -import reactDom from 'eslint-plugin-react-dom' - -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - // Enable lint rules for React - reactX.configs['recommended-typescript'], - // Enable lint rules for React DOM - reactDom.configs.recommended, - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) -``` diff --git a/frontend/bun.lock b/frontend/bun.lock index 30f01dc..f615453 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -5,7 +5,11 @@ "name": "frontend", "dependencies": { "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", "@tanstack/react-query": "^5.90.1", "@tanstack/react-table": "^8.21.3", "axios": "^1.12.2", @@ -15,6 +19,7 @@ "next-themes": "^0.4.6", "react": "^19.1.1", "react-dom": "^19.1.1", + "react-router-dom": "^7.13.1", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", }, @@ -150,6 +155,14 @@ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.5", "", { "dependencies": { "@eslint/core": "^0.15.2", "levn": "^0.4.1" } }, "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w=="], + "@floating-ui/core": ["@floating-ui/core@1.7.4", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.7.5", "", { "dependencies": { "@floating-ui/core": "^1.7.4", "@floating-ui/utils": "^0.2.10" } }, "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg=="], + + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.7", "", { "dependencies": { "@floating-ui/dom": "^1.7.5" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], @@ -178,14 +191,70 @@ "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], + + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], + + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], + + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], + "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], + "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], + + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], + + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], + + "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], + + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], + "@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="], + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], + + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], + + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="], + + "@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="], + + "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="], + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="], + + "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="], + + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], + + "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], + + "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], + + "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], + + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + + "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="], + + "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="], + + "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], + + "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="], + + "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.35", "", {}, "sha512-slYrCpoxJUqzFDDNlvrOYRazQUNRvWPjXA17dAOISY3rDMxX6k8K4cj2H+hEYMHF81HO3uNd5rHVigAWRM5dSg=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.0", "", { "os": "android", "cpu": "arm" }, "sha512-VxDYCDqOaR7NXzAtvRx7G1u54d2kEHopb28YH/pKzY6y0qmogP3gG7CSiWsq9WvDFxOQMpNEyjVAHZFXfH3o/A=="], @@ -298,6 +367,8 @@ "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], "autoprefixer": ["autoprefixer@10.4.21", "", { "dependencies": { "browserslist": "^4.24.4", "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ=="], @@ -344,6 +415,8 @@ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], @@ -358,6 +431,8 @@ "detect-libc": ["detect-libc@2.1.0", "", {}, "sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg=="], + "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="], "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], @@ -442,6 +517,8 @@ "get-intrinsic": ["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" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], "glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], @@ -626,6 +703,16 @@ "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], + "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], + + "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + + "react-router": ["react-router@7.13.1", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA=="], + + "react-router-dom": ["react-router-dom@7.13.1", "", { "dependencies": { "react-router": "7.13.1" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw=="], + + "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="], "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], @@ -644,6 +731,8 @@ "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], @@ -688,6 +777,8 @@ "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "tw-animate-css": ["tw-animate-css@1.3.8", "", {}, "sha512-Qrk3PZ7l7wUcGYhwZloqfkWCmaXZAoqjkdbIDvzfGshwGtexa/DAs9koXxIkrpEasyevandomzCBAV1Yyop5rw=="], "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], @@ -702,6 +793,10 @@ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], + + "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "vite": ["vite@7.1.7", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA=="], @@ -724,6 +819,8 @@ "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + "@radix-ui/react-separator/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -758,6 +855,8 @@ "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@radix-ui/react-separator/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], diff --git a/frontend/package.json b/frontend/package.json index b296260..4b9f534 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,7 +11,11 @@ }, "dependencies": { "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", "@tanstack/react-query": "^5.90.1", "@tanstack/react-table": "^8.21.3", "axios": "^1.12.2", @@ -21,6 +25,7 @@ "next-themes": "^0.4.6", "react": "^19.1.1", "react-dom": "^19.1.1", + "react-router-dom": "^7.13.1", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1" }, diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 37fdbfc..b92f966 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,26 +1,56 @@ +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { useAuth } from './hooks/useAuth'; import { LoginPage } from './pages/LoginPage'; import DashboardPage from './pages/DashboardPage'; +import { RepositoryManager } from './components/RepositoryManager'; +import { ConfigManager } from './components/ConfigManager'; import { Toaster } from "@/components/ui/sonner" -function App() { +function AuthGuard({ children }: { children: React.ReactNode }) { const { isAuthenticated, isLoading } = useAuth(); if (isLoading) { return ( -
-
正在加载...
+
+
+
+
+
+
+
+
INITIALIZING...
+
); } - const Page = isAuthenticated ? DashboardPage : LoginPage; + if (!isAuthenticated) { + return ; + } + return <>{children}; +} + +function App() { return ( - <> - + + + + + + } + > + } /> + } /> + } /> + } /> + + - + ); } diff --git a/frontend/src/components/ConfigFieldInput.tsx b/frontend/src/components/ConfigFieldInput.tsx new file mode 100644 index 0000000..41a5e4a --- /dev/null +++ b/frontend/src/components/ConfigFieldInput.tsx @@ -0,0 +1,140 @@ + +import type { ConfigFieldDto } from '@/services/configService'; +import { Input } from '@/components/ui/input'; +import { Switch } from '@/components/ui/switch'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Textarea } from '@/components/ui/textarea'; +import { Badge } from '@/components/ui/badge'; +import { Label } from '@/components/ui/label'; +import { Lock } from 'lucide-react'; + +interface ConfigFieldInputProps { + field: ConfigFieldDto; + value: any; + onChange: (value: any) => void; +} + +export function ConfigFieldInput({ field, value, onChange }: ConfigFieldInputProps) { + const isReadonly = !!field.readonly; + + const renderInput = () => { + const baseInputClasses = "bg-zinc-900/50 border-white/10 focus-visible:ring-primary focus-visible:border-primary transition-all duration-200" + (isReadonly ? " opacity-50 cursor-not-allowed" : ""); + switch (field.type) { + case 'boolean': + return ( + + ); + case 'enum': + return ( + + ); + case 'text': + return ( +