2 Commits

Author SHA1 Message Date
jeffusion
64ba7658f9 docs: restructure documentation with progressive disclosure
- README.md: trim to product overview + minimal quick start +
  single navigation table; remove duplicated Quick start section
- docs/README.md / zh-CN.md: rewrite as grouped navigation index
  with descriptions (Getting started / Reference / Deployment)
- docs/getting-started.md / zh-CN.md: add screenshot preview,
  improve structure with numbered configuration steps, add
  "Next steps" cross-links
- docs/configuration.md / zh-CN.md: fix heading hierarchy (###
  instead of ## 1)), convert Admin UI settings to tables, move
  Agent Definitions and Tool Permissions to review-engines.md
- docs/review-engines.md / zh-CN.md: add Agent Definitions and
  Tool Permissions sections (migrated from configuration.md),
  add model resolution order, convert to tables for clarity
- CONTRIBUTING.md: add development setup, code quality commands,
  PR workflow, and Conventional Commits convention
2026-05-27 10:17:04 +08:00
jeffusion
401ba16b8b chore(docs): remove internal design docs, add CONTRIBUTING.md
- Remove docs/design/ directory (notification-service-refactoring,
  pluggable-llm-providers, ui-theme-language) — these are internal
  architecture/refactoring documents that should not be in public
  documentation navigation per project convention
- Extract UI theme conventions from ui-theme-language.md into
  CONTRIBUTING.md as contributor guidelines
- Remove Architecture & design section from docs/README.md and
  docs/README.zh-CN.md index pages
- Update README.md link from Architecture docs to Documentation index
2026-05-27 09:19:20 +08:00
13 changed files with 487 additions and 1890 deletions

119
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,119 @@
# Contributing
## Development setup
- **Runtime**: [Bun](https://bun.sh) >= 1.2.5
- **Backend**: Hono (TypeScript)
- **Frontend**: React + Vite + Tailwind CSS
- **Database**: SQLite (via Drizzle ORM)
```bash
bun install # install all dependencies
bun run dev # start dev server with hot reload
```
## Code quality
```bash
bun run lint # lint backend + frontend
bun test # backend unit tests
cd frontend && bun test # frontend unit tests
bun run build # build backend
cd frontend && bun run build # build frontend
E2E_MOCK_LLM=1 bun run test:e2e # E2E with mock LLM (no real provider needed)
```
Run all checks before pushing:
```bash
bun run lint && bun run build && bun test && cd frontend && bun run build && bun test
```
## Pull requests
1. Fork the repository and create a feature branch from `main`
2. Make your changes with clear, atomic commits
3. Ensure all quality checks pass (lint, build, test)
4. Open a PR against `main` with a concise description of the change and motivation
### Commit style
Use [Conventional Commits](https://www.conventionalcommits.org/):
```
type(scope): subject
feat(review): add size-based routing
fix(webhook): handle missing signature header
chore(deps): bump hono to 4.x
docs(config): update env variable table
```
Common types: `feat`, `fix`, `chore`, `docs`, `refactor`, `test`.
## UI development conventions
The frontend follows a three-layer design token model:
1. **Primitive** — HSL base values, defined only in global CSS tokens
2. **Semantic**`background`, `foreground`, `success`, `danger`, etc.
3. **Component** — Components consume semantic tokens only. Direct primitive references are forbidden
### Theme definition
- Theme file: `frontend/src/index.css`
- Tailwind mapping: `frontend/tailwind.config.js`
- Primary palette: **Cobalt Blue**, with light/dark variants tuned for contrast
Available palette presets (`data-palette` attribute): `cobalt` (default), `zinc`, `nord`, `tokyo-night`.
### Hard rules
- **No hardcoded dark-theme classes** in business TSX: `bg-zinc-*`, `text-zinc-*`, `border-white/10`, etc.
- **No inline color values**: `rgba(...)`, `#xxxxxx`, `rgb(...)`
- **Status colors use semantic tokens**: `success` / `warning` / `danger` / `info`
- **Panels use semantic surface tokens**: `card`, `muted`, `popover`, `background`
- **Interactive glow** uses utility classes: `theme-glow-primary|success|warning|danger`
- **Non-primary hover** uses `hover:bg-accent*` or `hover:bg-muted*`. Only primary buttons may use `hover:bg-primary/90`
### Recommended class patterns
| Context | Classes |
|---|---|
| Text | `text-foreground` / `text-muted-foreground` |
| Panel | `bg-card` / `bg-muted/50` / `bg-popover` |
| Border | `border-border` / `theme-border-soft` |
| Status | `text-success` / `bg-danger/10` / `border-warning/30` |
| Hover (non-primary) | `hover:bg-accent/60` / `hover:bg-muted/60` |
| Hover (primary action) | `hover:bg-primary/90` |
| Page frame | `theme-page-frame` / `theme-page-actions` / `theme-page-content` |
| Card | `theme-card-shell` / `theme-card-header` / `theme-card-content` |
| Dialog | `theme-dialog-panel` / `theme-dialog-header` / `theme-dialog-body` / `theme-dialog-footer` |
| Error state | `theme-error-panel` |
| Sticky bar | `theme-sticky-bar` |
| Input surface | `theme-input-surface` |
| Control pill | `theme-control-pill` |
### `destructive` vs `danger`
- `destructive` — reserved for shadcn built-in destructive variant semantics
- `danger` — business status semantics (errors, failures, risk indicators)
New business components should prefer `danger` to avoid drift.
### Pre-merge checklist
- [ ] Page renders correctly in both light and dark themes
- [ ] No `zinc`/`white` hardcoded dark-theme classes
- [ ] No inline `style` color values
- [ ] All status colors use semantic tokens
- [ ] Components do not bypass the semantic layer to access primitive colors
- [ ] `bun run ui:visual` passes (light/dark visual regression)
### Visual regression
- Generate/update baseline: `bun run ui:visual:update`
- Verify baseline consistency: `bun run ui:visual`
- Full UI check: `bun run ui:regression && bun run ui:visual`
Baseline snapshots use Linux CI environment (`*-linux.png`). Cross-system snapshot updates introduce noise and should be avoided.

104
README.md
View File

@@ -2,104 +2,58 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
AI-powered code review assistant for Gitea. It receives webhooks, runs AI review workflows, and posts summary + line-level feedback back to Gitea.
AI-powered code review assistant for Gitea. Receives webhooks, runs AI review workflows, and posts summary + line-level feedback back to Gitea.
- English docs: [./docs/README.md](./docs/README.md)
- 中文文档: [./docs/README.zh-CN.md](./docs/README.zh-CN.md)
[English](./docs/README.md) | [中文](./docs/README.zh-CN.md)
## Why this project
## Features
- 🤖 **Automated PR + commit review** via webhook events (`pull_request`, `status`)
- 🧠 **Two review engines**: `agent` (native Agent pipeline) and `codex` (Codex CLI pipeline)
- 🧵 **Pluggable LLM providers**: OpenAI Compatible, OpenAI Responses API, Anthropic, Gemini
- 📍 **Actionable output**: summary comments and line-level findings
- 🎛️ **Web Admin UI** for runtime configuration (providers, models, webhook, review policy)
- 🔔 **Notifications**: Feishu + WeCom (企业微信)
- 🔐 **Security-first defaults**: webhook signature verification + encrypted API key storage
- **Automated PR & commit review** via webhook events
- **Dynamic Agent engine** — main agent autonomously spawns subagents for focused analysis
- **Codex engine** — Codex CLI-backed review as an alternative pipeline
- **Pluggable LLM providers** — OpenAI Compatible, OpenAI Responses API, Anthropic, Gemini
- **Web Admin UI** — runtime configuration for providers, models, webhook, review policy
- **Notifications** — Feishu + WeCom (企业微信)
- **Security-first** — webhook signature verification + AES-256-GCM encrypted API key storage
## Product screenshot
![Dashboard](./docs/assets/page-repos.png)
> Dashboard screenshot is generated from local dev service.
![Gitea AI Assistant Dashboard - Repository Page](./docs/assets/page-repos.png)
More screenshots (one per admin menu): [EN](./docs/screenshots.md) | [中文](./docs/screenshots.zh-CN.md)
## Architecture (high-level)
```
Gitea Webhook -> Gitea AI Assistant (Hono + Bun) -> LLM Gateway (multi-provider)
|
+-> Admin Dashboard (React)
```
For component-level design, see [Architecture docs](./docs/README.md#architecture--design).
## Quick start (minimal)
### 1) Prerequisites
- Bun >= 1.2.5
- Reachable Gitea instance
- At least one LLM provider credential
### 2) Install
## Quick start
```bash
git clone https://github.com/user/gitea-ai-assistant.git
git clone https://github.com/jeffusion/gitea-ai-assistant.git
cd gitea-ai-assistant
bun install
bun install # installs frontend via postinstall
```
If lifecycle scripts are disabled in your environment, run:
Create `.env`:
```bash
bun run bootstrap
ENCRYPTION_KEY=$(openssl rand -hex 32)
```
### 3) Minimal `.env`
```bash
PORT=5174
ENCRYPTION_KEY= # required, 64 hex chars (openssl rand -hex 32)
# DATABASE_PATH=./data/assistant.db
# LOG_LEVEL=info # dev default; use LOG_LEVEL=error in production
```
> `ENCRYPTION_KEY` is mandatory. The app refuses to start without it.
### 4) Run
```bash
bun run dev
# or
bun run start
```
### 5) Configure in Admin UI
Open `http://localhost:5174`, login with default password `password` (change it immediately), then configure Gitea, LLM providers, and webhook in the Admin UI.
Open `http://your-server:5174`, login with default `password` (first boot only), then change it immediately.
See [Getting Started](./docs/getting-started.md) for full setup walkthrough including webhook configuration.
- Configure Gitea API + tokens
- Configure webhook secret
- Configure LLM providers/models
- Configure review engine and policy
## Documentation
### 6) Add webhook in Gitea
| Topic | Description |
|---|---|
| [Getting Started](./docs/getting-started.md) | Full installation and setup walkthrough |
| [Configuration](./docs/configuration.md) | Environment variables and Admin UI settings |
| [Review Engines](./docs/review-engines.md) | Agent engine, Codex engine, review modes |
| [Deployment](./docs/deployment.md) | Docker, Compose, and Kubernetes |
| [Screenshots](./docs/screenshots.md) | Admin UI gallery |
- URL: `http://your-server:5174/webhook/gitea`
- Content-Type: `application/json`
- Secret: same as dashboard webhook secret
- Events: Pull Request + Status
## Contributing
## Progressive disclosure: detailed docs
- [Documentation index](./docs/README.md)
- [Getting started details](./docs/getting-started.md)
- [Configuration reference](./docs/configuration.md)
- [Review engines](./docs/review-engines.md)
- [Deployment (Docker / Compose / Kubernetes)](./docs/deployment.md)
See [CONTRIBUTING.md](./CONTRIBUTING.md) for development conventions and UI guidelines.
## License
MIT License
MIT

View File

@@ -1,19 +1,20 @@
# Documentation
This project keeps the root `README.md` concise and moves implementation/deployment details here.
Setup, configuration, and deployment guides for Gitea AI Assistant.
## Start here
## Getting started
- [Getting started](./getting-started.md)
- [Configuration reference](./configuration.md)
- [Review engines](./review-engines.md)
- [Deployment](./deployment.md)
- [Screenshot gallery](./screenshots.md)
- [Getting Started](./getting-started.md) — full installation, first login, and webhook setup walkthrough
- [Screenshots](./screenshots.md) — Admin UI gallery (one page per feature area)
## Architecture & design
## Reference
- [Notification service refactoring](./design/notification-service-refactoring.md)
- [UI theme language](./design/ui-theme-language.md)
- [Configuration](./configuration.md) — environment variables, Admin UI settings, and runtime configuration model
- [Review Engines](./review-engines.md) — Agent engine, Codex engine, review modes, size policy, and agent definitions
## Deployment
- [Deployment](./deployment.md) — Docker, Docker Compose, and Kubernetes
## Language

View File

@@ -1,23 +1,20 @@
# 文档中心
根目录 `README.md` 保持简洁;本目录提供详细说明与设计文档
Gitea AI Assistant 的安装、配置与部署指南
## 快速导航
## 快速开始
- [快速开始](./getting-started.zh-CN.md)
- [配置参考](./configuration.zh-CN.md)
- [审查引擎](./review-engines.zh-CN.md)
- [部署指南](./deployment.zh-CN.md)
- [截图集](./screenshots.zh-CN.md)
- [快速开始](./getting-started.zh-CN.md) — 完整安装、首次登录与 Webhook 配置指引
- [截图集](./screenshots.zh-CN.md) — 管理后台界面一览(每个功能页面一张截图)
## 架构与设计
## 参考手册
- [通知服务重构设计](./design/notification-service-refactoring.md)
- [UI 主题语言设计](./design/ui-theme-language.md)
- [配置参考](./configuration.zh-CN.md) — 环境变量、管理后台设置与运行时配置模型
- [审查引擎](./review-engines.zh-CN.md) — Agent 引擎、Codex 引擎、审查模式、规模策略与 Agent 定义
## 产品截图
## 部署
![Gitea AI Assistant 管理后台(仓库管理页)](./assets/page-repos.png)
- [部署指南](./deployment.zh-CN.md) — Docker、Docker Compose 与 Kubernetes
## 语言切换

View File

@@ -2,21 +2,23 @@
## Configuration model
This project uses a DB-first runtime configuration model:
This project uses a **DB-first** runtime configuration model:
- `.env` contains only infrastructure-level bootstrap values.
- Runtime settings (Gitea, providers, secrets, review policy, notifications) are managed in Admin UI and stored in SQLite.
- `.env` stores only infrastructure-level bootstrap values
- Runtime settings (Gitea, providers, secrets, review policy, notifications) are managed in Admin UI and persisted to SQLite
## Environment variables (minimal)
This means you configure most settings through the web dashboard after first boot, not through environment variables.
## Environment variables
| Variable | Required | Description | Default |
|---|---|---|---|
| `ENCRYPTION_KEY` | Yes | AES-256-GCM master key (64 hex chars) for API key encryption | - |
| `ENCRYPTION_KEY` | Yes | AES-256-GCM master key for API key encryption (64 hex chars) | |
| `PORT` | No | Service port | `5174` |
| `DATABASE_PATH` | No | SQLite path | `./data/assistant.db` |
| `LOG_LEVEL` | No | Backend log level (`debug`/`info`/`warn`/`error`). Default is `info`; use `error` in production. | `info` |
| `DATABASE_PATH` | No | SQLite database path | `./data/assistant.db` |
| `LOG_LEVEL` | No | Backend log level: `debug` / `info` / `warn` / `error` | `info` |
Generate key:
Generate encryption key:
```bash
openssl rand -hex 32
@@ -24,62 +26,57 @@ openssl rand -hex 32
## First boot defaults
When database is empty:
When the database is empty on first launch:
- `JWT_SECRET` auto-generated
- `WEBHOOK_SECRET` auto-generated
- `ADMIN_PASSWORD` defaults to `password`
- `JWT_SECRET` auto-generated
- `WEBHOOK_SECRET` auto-generated
- `ADMIN_PASSWORD` defaults to `password` (**change immediately after login**)
Change `ADMIN_PASSWORD` immediately after first login.
## Admin UI settings
## Runtime groups in Admin UI
All settings below are configured through the Admin UI at `http://your-server:5174`.
## 1) Gitea
### Gitea
- API URL
- Access token
- Admin token (optional)
| Setting | Description |
|---|---|
| API URL | Gitea API endpoint (e.g. `http://gitea:3000/api/v1`) |
| Access Token | Token for cloning repos and posting comments |
| Admin Token | Optional; required for repository discovery |
## 2) Security
### Security
- Webhook secret (HMAC-SHA256 verification)
- Admin password
- JWT secret
| Setting | Description |
|---|---|
| Webhook Secret | HMAC-SHA256 key for verifying incoming webhooks |
| Admin Password | Dashboard login password |
| JWT Secret | Token signing key (auto-generated on first boot) |
## 3) LLM
### LLM
- Providers: OpenAI Compatible / OpenAI Responses / Anthropic / Gemini
- Agent runtime models:
- `AGENT_MAIN_MODEL`: The main model name used by the agent runtime when no specific model is configured. Default is `gpt-4.1`.
- `AGENT_DEFAULT_SUBAGENT_MODEL`: The default model name used by subagents when no specific model is declared in their definition or overridden during spawn. Default is `gpt-4.1-mini`.
| Setting | Description |
|---|---|
| Providers | Add one or more providers: OpenAI Compatible / OpenAI Responses / Anthropic / Gemini |
| `AGENT_MAIN_MODEL` | Default model for the main agent runtime. Default: `gpt-4.1` |
| `AGENT_DEFAULT_SUBAGENT_MODEL` | Default model for subagents when not declared in definition or spawn. Default: `gpt-4.1-mini` |
## 4) Notification
Model resolution order: `spawn override > AgentDefinition.model > AGENT_DEFAULT_SUBAGENT_MODEL > AGENT_MAIN_MODEL`
- Feishu webhook and optional secret
- WeCom (企业微信) webhook
### Notifications
## 5) Review
| Setting | Description |
|---|---|
| Feishu Webhook | Feishu bot webhook URL and optional signing secret |
| WeCom Webhook | WeCom (企业微信) bot webhook URL |
- Engine mode: `agent` or `codex`
- Triage size classification and routing hints
- Size thresholds (`small`/`medium`/`large`)
- Execution modes (`skip`/`light`/`full`)
- Token budgets and concurrency limits
### Review
> Size and mode are different layers:
>
> - `small/medium/large`: change-size classification
> - `skip/light/full`: review execution depth
| Setting | Description |
|---|---|
| Engine | `agent` or `codex` |
| Size thresholds | `small` / `medium` / `large` — classifies change size |
| Execution modes | `skip` / `light` / `full` — controls review depth |
| Token budgets | Per-mode token limits |
| Concurrency | Max parallel review runs |
## Agent Definitions
Project agent definitions are stored as Markdown files with frontmatter in the repository:
- Path: `.gitea-assistant/agents/*.md`
These files define the system prompts, metadata, and execution parameters for each agent.
## Tool Permissions
Tool permissions are controlled directly within each agent's definition file:
- `tools`: An allow-list of tool names that the agent is permitted to call. An empty list grants no tools.
- `disallowedTools`: A deny-list of tool names that the agent is explicitly forbidden from calling. This takes precedence over the allow-list.
> Size and mode are separate layers: `small/medium/large` classifies how big the change is; `skip/light/full` controls how deeply the engine reviews it.

View File

@@ -2,21 +2,23 @@
## 配置模型
项目采用 DB-first 运行时配置模型:
项目采用 **DB-first** 运行时配置模型:
- `.env`用于基础设施级引导参数
- `.env`存储基础设施级引导参数
- 运行时配置Gitea、Provider、密钥、审查策略、通知由管理后台维护并持久化到 SQLite
## 环境变量(最小集)
即大部分设置在首次启动后通过 Web 管理后台配置,而非环境变量。
## 环境变量
| 变量 | 必填 | 说明 | 默认值 |
|---|---|---|---|
| `ENCRYPTION_KEY` | 是 | API Key 加密主密钥AES-256-GCM64 位十六进制) | - |
| `ENCRYPTION_KEY` | 是 | API Key 加密主密钥AES-256-GCM64 位十六进制) | |
| `PORT` | 否 | 服务端口 | `5174` |
| `DATABASE_PATH` | 否 | SQLite 路径 | `./data/assistant.db` |
| `LOG_LEVEL` | 否 | 后端日志级别`debug`/`info`/`warn`/`error`)。默认 `info`;生产环境建议 `error` | `info` |
| `DATABASE_PATH` | 否 | SQLite 数据库路径 | `./data/assistant.db` |
| `LOG_LEVEL` | 否 | 后端日志级别`debug` / `info` / `warn` / `error` | `info` |
生成密钥:
生成加密密钥:
```bash
openssl rand -hex 32
@@ -24,62 +26,57 @@ openssl rand -hex 32
## 首次启动默认值
数据库为空时:
数据库为空时首次启动
- `JWT_SECRET` 自动生成
- `WEBHOOK_SECRET` 自动生成
- `ADMIN_PASSWORD` 默认 `password`
- `JWT_SECRET` 自动生成
- `WEBHOOK_SECRET` 自动生成
- `ADMIN_PASSWORD` 默认 `password`**登录后请立即修改**
首次登录后请立即修改管理员密码。
## 管理后台设置
## 管理后台配置分组
以下所有设置均通过管理后台 `http://your-server:5174` 配置。
## 1) Gitea
### Gitea
- API URL
- Access Token
- Admin Token可选
| 设置项 | 说明 |
|---|---|
| API URL | Gitea API 端点(如 `http://gitea:3000/api/v1` |
| Access Token | 用于克隆仓库和发布评论的令牌 |
| Admin Token | 可选;仓库发现功能需要 |
## 2) 安全
### 安全
- Webhook SecretHMAC-SHA256 验签)
- Admin Password
- JWT Secret
| 设置项 | 说明 |
|---|---|
| Webhook Secret | HMAC-SHA256 签名验证密钥 |
| Admin Password | 管理后台登录密码 |
| JWT Secret | Token 签名密钥(首次启动自动生成) |
## 3) LLM
### LLM
- ProviderOpenAI Compatible / OpenAI Responses / Anthropic / Gemini
- Agent 运行时模型:
- `AGENT_MAIN_MODEL`在没有更具体模型配置时Agent 运行时使用的主模型名称。默认值为 `gpt-4.1`
- `AGENT_DEFAULT_SUBAGENT_MODEL`当子代理Subagent未声明模型且 spawn 未覆盖时,使用的默认模型名称。默认值`gpt-4.1-mini`
| 设置项 | 说明 |
|---|---|
| Provider | 添加一个或多个提供商OpenAI Compatible / OpenAI Responses / Anthropic / Gemini |
| `AGENT_MAIN_MODEL` | 主 Agent 运行时默认模型。默认值`gpt-4.1` |
| `AGENT_DEFAULT_SUBAGENT_MODEL` | 子 Agent 未声明模型且 spawn 未覆盖时的默认模型。默认值:`gpt-4.1-mini` |
## 4) 通知
模型解析顺序:`spawn 覆盖 > AgentDefinition.model > AGENT_DEFAULT_SUBAGENT_MODEL > AGENT_MAIN_MODEL`
- Feishu Webhook 与可选签名密钥
- WeCom企业微信Webhook
### 通知
## 5) 审查
| 设置项 | 说明 |
|---|---|
| Feishu Webhook | 飞书机器人 Webhook URL 及可选签名密钥 |
| WeCom Webhook | 企业微信机器人 Webhook URL |
- 引擎模式:`agent` / `codex`
- Triage 规模分类与路由提示
- 规模阈值(`small`/`medium`/`large`
- 执行模式(`skip`/`light`/`full`
- Token 预算与并发限制
### 审查
> 规模与模式是两个层次:
>
> - `small/medium/large`:变更规模分类
> - `skip/light/full`:审查执行深度
| 设置项 | 说明 |
|---|---|
| 引擎 | `agent``codex` |
| 规模阈值 | `small` / `medium` / `large` — 变更规模分类 |
| 执行模式 | `skip` / `light` / `full` — 审查深度控制 |
| Token 预算 | 各模式 Token 限额 |
| 并发限制 | 最大并行审查数 |
## Agent 定义
项目的 Agent 定义以带有 Frontmatter 的 Markdown 文件形式存储在仓库中:
- 路径:`.gitea-assistant/agents/*.md`
这些文件定义了每个 Agent 的系统提示词、元数据和执行参数。
## 工具权限
工具权限直接在每个 Agent 的定义文件中进行控制:
- `tools`:允许该 Agent 调用的工具名称白名单。如果列表为空,则不授予任何工具权限。
- `disallowedTools`:显式禁止该 Agent 调用的工具名称黑名单。黑名单的优先级高于白名单。
> 规模与模式是两个层次:`small/medium/large` 分类变更的大小;`skip/light/full` 控制审查的深度。

View File

@@ -1,617 +0,0 @@
# 通知服务抽象化重构方案
## 1. 概述
### 1.1 背景
当前项目中的通知功能仅支持飞书(Feishu/Lark)平台代码高度耦合飞书特定的API实现。随着业务需求扩展需要支持企业微信(WeCom)等其他通知渠道。
### 1.2 目标
- 抽象通用通知服务接口,支持多平台扩展
- 支持同时配置多个通知服务(如飞书+企业微信同时推送)
- 统一通知调用入口,避免平台耦合与重复发送
- 清晰的代码结构便于后续添加新平台如Slack、钉钉等
### 1.3 非目标
- 不修改通知的业务触发逻辑
- 不改变现有的Gitea Webhook处理流程
- 不引入外部通知服务SDK依赖保持轻量
---
## 2. 现有架构分析
### 2.1 重构前实现(已下线)
```
src/
├── services/feishu.ts # 飞书服务实现156行
├── controllers/review.ts # 通知调用点
├── config/config-schema.ts # 配置定义
└── config/config-manager.ts # 配置管理
```
### 2.2 关键代码特征
- **强耦合**`review.ts` 直接调用 `feishuService.sendXXXNotification()`
- **硬编码消息格式**:飞书特定的 `msg_type: 'text'` 结构
- **签名逻辑**HMAC-SHA256(timestamp+"\n"+secret)
- **配置单一**:仅支持一组飞书配置
### 2.3 通知场景
| 场景 | 方法名 | 触发条件 |
|------|--------|----------|
| 工单创建 | `sendIssueCreatedNotification` | Issue opened + 有指派人 |
| 工单关闭 | `sendIssueClosedNotification` | Issue closed |
| 工单指派 | `sendIssueAssignedNotification` | Issue assigned |
| PR创建 | `sendPrCreatedNotification` | PR opened + 有审阅者 |
| PR指派 | `sendPrReviewerAssignedNotification` | PR review_requested |
---
## 3. 目标架构设计
### 3.1 架构模式
采用**策略模式(Strategy)** + **工厂模式(Factory)**
```
┌─────────────────────────────────────────────────────────────┐
│ Notification Service Layer │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐ │
│ │ INotification │ │ INotification │ │ INotification│ │
│ │ Service │ │ Service │ │ Service │ │
│ │ (Interface) │ │ (Interface) │ │ (Interface) │ │
│ └────────┬────────┘ └────────┬────────┘ └──────┬──────┘ │
│ │ │ │ │
│ ┌─────┴─────┐ ┌────┴────┐ ┌────┴────┐ │
│ │ Feishu │ │ WeCom │ │ Slack │ │
│ │Service │ │Service │ │ Service │ │
│ └───────────┘ └─────────┘ └─────────┘ │
└─────────────────────────────────────────────────────────────┘
┌─────────┴─────────┐
│ NotificationFactory│
└─────────┬─────────┘
┌─────────┴─────────┐
│ NotificationManager│ ← 统一入口,支持多服务
└───────────────────┘
```
### 3.2 核心接口设计
#### 3.2.1 类型定义
```typescript
// types.ts
export type NotificationProvider = 'feishu' | 'wecom' | 'slack' | 'dingtalk';
export interface NotificationContext {
// PR相关
prTitle?: string;
prUrl?: string;
prNumber?: number;
// Issue相关
issueTitle?: string;
issueUrl?: string;
issueNumber?: number;
// 用户相关
actor?: string;
assignees?: string[];
reviewers?: string[];
creator?: string;
// 仓库相关
repository?: string;
owner?: string;
// 时间戳
timestamp?: Date;
}
export interface NotificationMessage {
type: 'text' | 'markdown';
title?: string;
content: string;
atUsers?: string[];
url?: string;
}
```
#### 3.2.2 服务接口
```typescript
// INotificationService
export interface INotificationService {
readonly provider: NotificationProvider;
isEnabled(): boolean;
sendMessage(message: NotificationMessage): Promise<void>;
// 场景特定方法
sendIssueCreatedNotification(context: NotificationContext): Promise<void>;
sendIssueClosedNotification(context: NotificationContext): Promise<void>;
sendIssueAssignedNotification(context: NotificationContext): Promise<void>;
sendPrCreatedNotification(context: NotificationContext): Promise<void>;
sendPrReviewerAssignedNotification(context: NotificationContext): Promise<void>;
}
```
### 3.3 平台差异对照
| 特性 | 飞书(Feishu) | 企业微信(WeCom) | Slack |
|------|--------------|-----------------|-------|
| **Webhook格式** | `open.feishu.cn/open-apis/bot/v2/hook/{key}` | `qyapi.weixin.qq.com/cgi-bin/webhook/send?key={key}` | `hooks.slack.com/services/...` |
| **签名机制** | HMAC-SHA256(timestamp+"\n"+secret) | **无** | HMAC-SHA256(timestamp+":"+secret) |
| **@用户方式** | `<at user_id="xxx">` 或文本追加 | `mentioned_list: ["userid"]` 或手机号 | `<@user_id>` |
| **消息类型字段** | `msg_type` | `msgtype` | `type` |
| **内容字段** | `content.text` | `text.content` | `text` |
| **频率限制** | 100次/分钟 | 20条/分钟 | 1次/秒 |
---
## 4. 详细实现方案
### 4.1 目录结构
```
src/
├── services/
│ ├── notification/
│ │ ├── index.ts # 导出入口
│ │ ├── types.ts # 类型定义
│ │ ├── base-notification-service.ts # 抽象基类
│ │ ├── notification-factory.ts # 工厂
│ │ ├── notification-manager.ts # 管理器
│ │ └── providers/
│ │ ├── feishu-notification-service.ts
│ │ └── wecom-notification-service.ts
│ └── notification-manager.ts # 运行时通知管理器入口
```
### 4.2 基类实现
```typescript
// base-notification-service.ts
export abstract class BaseNotificationService implements INotificationService {
abstract readonly provider: NotificationProvider;
constructor(protected config: NotificationServiceConfig) {}
isEnabled(): boolean {
return this.config.enabled && !!this.config.webhookUrl;
}
abstract sendMessage(message: NotificationMessage): Promise<void>;
// 通用模板方法
async sendIssueCreatedNotification(context: NotificationContext): Promise<void> {
const message = this.buildIssueCreatedMessage(context);
await this.sendMessage(message);
}
// 子类实现消息构建
protected abstract buildIssueCreatedMessage(context: NotificationContext): NotificationMessage;
// ... 其他方法类似
}
```
### 4.3 飞书实现要点
```typescript
// feishu-notification-service.ts
export class FeishuNotificationService extends BaseNotificationService {
readonly provider = 'feishu' as const;
async sendMessage(message: NotificationMessage): Promise<void> {
const payload: any = {
msg_type: 'text',
content: {
text: message.content,
},
};
// 添加签名
if (this.config.webhookSecret) {
const timestamp = Math.floor(Date.now() / 1000).toString();
payload.timestamp = timestamp;
payload.sign = this.generateSign(timestamp, this.config.webhookSecret);
}
const response = await fetch(this.config.webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`Feishu notification failed: ${response.statusText}`);
}
}
protected buildIssueCreatedMessage(context: NotificationContext): NotificationMessage {
return {
type: 'text',
content: `📝 新工单已创建\n标题: ${context.issueTitle}\n链接: ${context.issueUrl}`,
atUsers: context.assignees,
};
}
private generateSign(timestamp: string, secret: string): string {
const stringToSign = `${timestamp}\n${secret}`;
const hmac = crypto.createHmac('sha256', stringToSign);
return hmac.digest('base64');
}
}
```
### 4.4 企业微信实现要点
```typescript
// wecom-notification-service.ts
export class WeComNotificationService extends BaseNotificationService {
readonly provider = 'wecom' as const;
async sendMessage(message: NotificationMessage): Promise<void> {
const payload: any = {
msgtype: 'text',
text: {
content: message.content,
},
};
// 企业微信使用 mentioned_list
if (message.atUsers?.length) {
payload.text.mentioned_list = message.atUsers.map(u =>
u.toLowerCase() === 'all' ? '@all' : u
);
}
const response = await fetch(this.config.webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`WeCom notification failed: ${response.statusText}`);
}
}
protected buildIssueCreatedMessage(context: NotificationContext): NotificationMessage {
return {
type: 'text',
content: `📝 新工单已创建\n标题: ${context.issueTitle}\n链接: ${context.issueUrl}`,
atUsers: context.assignees,
};
}
}
```
### 4.5 管理器实现
```typescript
// notification-manager.ts
export class NotificationManager {
private services: INotificationService[] = [];
constructor(configs: NotificationServiceConfig[]) {
this.services = configs
.filter(c => c.enabled && c.webhookUrl)
.map(c => NotificationFactory.createService(c));
}
// 广播到所有服务
async broadcast(
operation: (service: INotificationService) => Promise<void>
): Promise<void> {
const results = await Promise.allSettled(
this.services.map(async service => {
try {
await operation(service);
} catch (error) {
logger.error(`${service.provider} notification failed:`, error);
throw error; // 重新抛出以便Promise.allSettled捕获
}
})
);
// 记录失败统计
const failures = results.filter(r => r.status === 'rejected');
if (failures.length > 0) {
logger.warn(`${failures.length}/${this.services.length} notification services failed`);
}
}
// 便捷方法
async notifyIssueCreated(context: NotificationContext): Promise<void> {
await this.broadcast(s => s.sendIssueCreatedNotification(context));
}
async notifyIssueClosed(context: NotificationContext): Promise<void> {
await this.broadcast(s => s.sendIssueClosedNotification(context));
}
async notifyIssueAssigned(context: NotificationContext): Promise<void> {
await this.broadcast(s => s.sendIssueAssignedNotification(context));
}
async notifyPrCreated(context: NotificationContext): Promise<void> {
await this.broadcast(s => s.sendPrCreatedNotification(context));
}
async notifyPrReviewerAssigned(context: NotificationContext): Promise<void> {
await this.broadcast(s => s.sendPrReviewerAssignedNotification(context));
}
}
```
---
## 5. 配置改造
### 5.1 新增配置字段
```typescript
// config-schema.ts
export const CONFIG_FIELDS: ConfigFieldMeta[] = [
// ... 保留原有 ...
// 飞书配置(改造为可独立启用)
{
envKey: 'FEISHU_ENABLED',
group: 'notification',
label: '启用飞书通知',
description: '是否启用飞书通知',
type: 'boolean',
sensitive: false,
defaultValue: true,
},
{
envKey: 'FEISHU_WEBHOOK_URL',
group: 'notification',
label: '飞书 Webhook 地址',
description: '飞书机器人 Webhook URL',
type: 'url',
sensitive: false,
},
{
envKey: 'FEISHU_WEBHOOK_SECRET',
group: 'notification',
label: '飞书 Webhook 密钥',
description: '飞书 Webhook 签名密钥(可选)',
type: 'string',
sensitive: true,
},
// 企业微信配置(新增)
{
envKey: 'WECOM_ENABLED',
group: 'notification',
label: '启用企业微信通知',
description: '是否启用企业微信通知',
type: 'boolean',
sensitive: false,
defaultValue: false,
},
{
envKey: 'WECOM_WEBHOOK_URL',
group: 'notification',
label: '企业微信 Webhook 地址',
description: '企业微信机器人 Webhook URL',
type: 'url',
sensitive: false,
},
];
```
### 5.2 配置组调整
```typescript
// 将 'feishu' 组改为 'notification' 组
export type ConfigGroup = 'gitea' | 'notification' | 'security' | 'review' | 'memory';
export const CONFIG_GROUPS: ConfigGroupMeta[] = [
// ...
{
key: 'notification',
label: '通知服务',
description: '飞书、企业微信等通知渠道配置',
icon: 'bell',
},
// ...
];
```
---
## 6. 调用层迁移
### 6.1 review.ts 改造
```typescript
import { getNotificationManager } from '../services/notification-manager';
// PR事件处理
async function handlePullRequestEvent(c: Context, body: any): Promise<Response> {
// ... 原有逻辑 ...
const context: NotificationContext = {
prTitle: pullRequest.title,
prUrl: pullRequest.html_url,
prNumber: pullRequest.number,
reviewers: reviewerUsernames,
repository: repo.name,
owner: repo.owner.login,
actor: body.sender?.login,
};
const notificationManager = getNotificationManager();
if (body.action === 'opened' && reviewerUsernames.length > 0) {
await notificationManager.notifyPrCreated(context);
}
if (body.action === 'review_requested' && body.requested_reviewer) {
context.assignees = [body.requested_reviewer.full_name || body.requested_reviewer.login];
await notificationManager.notifyPrReviewerAssigned(context);
}
// ... 继续原有逻辑 ...
}
// Issue事件处理
async function handleIssueEvent(c: Context, body: any): Promise<Response> {
// ...
const context: NotificationContext = {
issueTitle: issue.title,
issueUrl: issue.html_url,
issueNumber: issue.number,
creator: creatorUsername,
assignees: assigneeUsernames,
repository: repository.name,
actor: body.sender?.login,
};
if (action === 'opened' && assigneeUsernames.length > 0) {
await notificationManager.notifyIssueCreated(context);
} else if (action === 'closed') {
await notificationManager.notifyIssueClosed(context);
} else if (action === 'assigned') {
await notificationManager.notifyIssueAssigned(context);
}
}
```
---
## 7. 落地决策(已执行)
### 7.1 旧飞书服务下线
- 已删除 `src/services/feishu.ts`,不再保留兼容层。
- `src/controllers/review.ts` 中所有通知发送路径已统一到 `NotificationManager`
- 通过单一通知入口避免重复发送与配置路径分裂问题。
### 7.2 运行时配置生效策略
- 通知管理器按当前配置即时创建,不再使用长生命周期缓存。
- 后台保存通知配置后,可立即在后续 webhook 事件生效。
### 7.3 落地检查清单
- [x] 飞书与企业微信通过统一通知抽象发送
- [x] 旧飞书服务文件已下线
- [x] 控制器通知链路已去重
- [x] 前端新增独立“通知管理”菜单与页面
---
## 8. 实施计划
### 8.1 阶段划分
| 阶段 | 任务 | 文件 | 优先级 |
|------|------|------|--------|
| 1 | 核心抽象层 | `types.ts`, `base-notification-service.ts` | P0 |
| 2 | 工厂与管理器 | `notification-factory.ts`, `notification-manager.ts` | P0 |
| 3 | 飞书实现 | `providers/feishu-notification-service.ts` | P1 |
| 4 | 企业微信实现 | `providers/wecom-notification-service.ts` | P1 |
| 5 | 配置改造 | `config-schema.ts`, `config-manager.ts` | P1 |
| 6 | 调用层迁移 | `review.ts` | P1 |
| 7 | 前端通知管理页面 | `App.tsx`, `DashboardPage.tsx`, `NotificationConfigPage.tsx` | P1 |
| 8 | 测试验证 | `config-manager.test.ts` 等 | P0 |
### 8.2 风险与缓解
| 风险 | 影响 | 缓解措施 |
|------|------|----------|
| 签名算法变更 | 飞书通知失效 | 保持原有签名实现,单元测试覆盖 |
| 配置迁移失败 | 服务无法启动 | 添加配置验证和默认值回退 |
| 多服务并发失败 | 部分通知丢失 | Promise.allSettled 确保独立性 |
---
## 9. 测试策略
### 9.1 单元测试
```typescript
// __tests__/notification.test.ts
describe('NotificationService', () => {
describe('FeishuNotificationService', () => {
it('should generate correct signature', () => {
// 测试签名算法
});
it('should format message correctly', () => {
// 测试消息格式转换
});
});
describe('WeComNotificationService', () => {
it('should use mentioned_list for @users', () => {
// 测试@用户格式
});
});
describe('NotificationManager', () => {
it('should broadcast to all enabled services', async () => {
// 测试广播逻辑
});
it('should not fail if one service fails', async () => {
// 测试容错
});
});
});
```
### 9.2 集成测试
- 配置真实飞书机器人测试消息发送
- 配置企业微信机器人测试消息发送
- 验证同时配置多个服务时的行为
---
## 10. 附录
### 10.1 飞书与企业微信API对比详情
#### 飞书消息格式
```json
{
"msg_type": "text",
"content": {
"text": "Hello <at user_id=\"ou_xxx\">Tom</at>"
}
}
```
#### 企业微信消息格式
```json
{
"msgtype": "text",
"text": {
"content": "Hello World",
"mentioned_list": ["wangqing", "@all"],
"mentioned_mobile_list": ["13800001111"]
}
}
```
### 10.2 扩展指南
添加新通知平台步骤:
1.`types.ts` 添加新的 `NotificationProvider` 类型
2.`providers/` 创建新的服务类,继承 `BaseNotificationService`
3.`notification-factory.ts` 添加创建逻辑
4.`config-schema.ts` 添加配置字段
5. 在 Admin Dashboard 添加UI配置项
---
**文档版本**: 1.0
**创建日期**: 2026-03-24
**作者**: Sisyphus
**状态**: 已实施(持续验证中)

View File

@@ -1,821 +0,0 @@
# 技术设计文档:可插拔 LLM Provider 架构
> **状态**: Draft
> **作者**: AI Architect
> **日期**: 2026-03-04
> **相关 Issue**: N/A
---
## 目录
- [0. 设计原则](#0-设计原则)
- [1. 目录结构](#1-目录结构新增改动部分)
- [2. 数据库表结构](#2-数据库表结构sqlite-ddl)
- [3. LLM Gateway 核心接口](#3-llm-gateway-核心-typescript-接口)
- [4. Provider Adapter 差异映射](#4-四个-provider-adapter-核心差异映射)
- [5. 后端 REST API 契约](#5-后端-rest-api-契约)
- [6. 密钥安全设计](#6-密钥安全设计)
- [7. 前端配置页设计](#7-前端配置页设计)
- [8. 现有调用点改造清单](#8-现有调用点改造清单)
- [9. 实施阶段建议](#9-实施阶段建议)
- [10. 风险与缓解](#10-风险与缓解)
---
## 0. 设计原则
| 原则 | 说明 |
|---|---|
| **UI-Only 配置** | 所有业务配置仅通过 Web 管理后台设置,不再有环境变量覆盖层(仅保留极少数启动参数如 `PORT``WEBHOOK_SECRET``DATABASE_PATH` |
| **4 Provider 并存** | `openai_compatible`(现有兼容格式)、`openai_responses`Responses API`anthropic`Messages API`gemini`generateContent API |
| **SQLite 持久化** | 使用 `bun:sqlite` 零依赖,单文件 `data/assistant.db` |
| **密钥应用层加密** | API Key 使用 AES-256-GCM 加密后存 DB主密钥通过环境变量 `ENCRYPTION_KEY` 传入hex 编码64 字符 = 32 字节),未设置则拒绝启动 |
| **不做向前兼容** | 旧 JSON 配置文件方案直接废弃,新版本仅支持数据库配置 |
### 开源参考
| 借鉴点 | 参考项目 | 具体模式 |
|---|---|---|
| 接口先行 + provider registry | [Vercel AI SDK](https://github.com/vercel/ai) | `LanguageModelV3` spec-first adapter版本化接口 |
| Provider transformation 差异映射 | [LiteLLM](https://github.com/BerriAI/litellm) | 每个 provider 独立 `transformation.py`,标准化 error/usage |
| Runtime factory + 多 provider 配置 | [LobeChat](https://github.com/lobehub/lobe-chat) | `createOpenAICompatibleRuntime` 工厂 + 动态 model list |
| 配置驱动多 endpoint | [LibreChat](https://github.com/danny-avila/LibreChat) | `librechat.yaml` Custom Endpoints 模式 |
| 能力声明/能力检测 | [Continue](https://github.com/continuedev/continue) | `supportsTools` / `supportsImages` 声明式 capability |
---
## 1. 目录结构(新增/改动部分)
```
src/
├── db/
│ ├── database.ts # bun:sqlite 初始化
│ ├── migrations/
│ │ └── 001_init.ts # 建表 DDL
│ └── repositories/
│ ├── provider-repo.ts # llm_providers CRUD
│ ├── model-role-repo.ts # model_role_assignments CRUD
│ ├── secret-repo.ts # 加密 read/write
│ └── settings-repo.ts # system_settings KV
├── llm/
│ ├── types.ts # 统一内部请求/响应类型
│ ├── capabilities.ts # 能力声明枚举
│ ├── errors.ts # LLM 层标准化错误
│ ├── gateway.ts # LLM Gateway 入口(按 provider 路由)
│ ├── tool-converter.ts # 工具定义 → 各 provider 格式转换
│ └── providers/
│ ├── base.ts # LLMProvider 抽象接口
│ ├── openai-compatible.ts # 现有兼容格式 adapter
│ ├── openai-responses.ts # OpenAI Responses API adapter
│ ├── anthropic.ts # Anthropic Messages API adapter
│ └── gemini.ts # Gemini generateContent adapter
├── crypto/
│ └── secrets.ts # AES-256-GCM 加解密 + master key 管理
├── controllers/
│ └── llm-config.ts # 新 REST API替代 config.ts 中 LLM 部分)
└── config/
├── config-manager.ts # 精简:只管非 LLM 配置gitea/feishu/app/admin/review 非模型部分)
└── config-schema.ts # 移除 openai groupLLM 配置全部走 DB
```
---
## 2. 数据库表结构SQLite DDL
### 2.1 ER 关系
```
llm_providers 1 ←──── 1 llm_secrets (每个 provider 一个加密 key)
llm_providers 1 ←──── N model_role_assignments (一个 provider 可服务多个角色)
```
### 2.2 完整 DDL
```sql
-- ============================================================
-- 表1: llm_providers — Provider 实例配置
-- ============================================================
CREATE TABLE llm_providers (
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(8)))),
name TEXT NOT NULL, -- 用户自定义显示名,如 "公司内部 OpenAI 代理"
type TEXT NOT NULL CHECK (type IN (
'openai_compatible', -- 现有兼容格式(自定义 baseUrl
'openai_responses', -- OpenAI 标准 Responses API
'anthropic', -- Anthropic Messages API
'gemini' -- Google Gemini generateContent
)),
base_url TEXT, -- 可选自定义 endpointopenai_compatible 必填)
default_model TEXT NOT NULL, -- 此 provider 的默认模型 ID
is_enabled INTEGER NOT NULL DEFAULT 1, -- 0=禁用, 1=启用
extra_config TEXT DEFAULT '{}', -- JSON: provider 特有配置(如 api_version, project_id 等)
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- ============================================================
-- 表2: llm_secrets — 加密存储的 API Key
-- ============================================================
CREATE TABLE llm_secrets (
provider_id TEXT PRIMARY KEY REFERENCES llm_providers(id) ON DELETE CASCADE,
ciphertext BLOB NOT NULL, -- AES-256-GCM 密文
iv BLOB NOT NULL, -- 12 bytes nonce
auth_tag BLOB NOT NULL, -- 16 bytes GCM tag
key_version INTEGER NOT NULL DEFAULT 1, -- 主密钥版本号(便于密钥轮换)
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- ============================================================
-- 表3: model_role_assignments — 场景 → 模型映射
-- ============================================================
-- 每个业务场景(如 planner/specialist/judge绑定到
-- 一个 provider + 具体 model支持不同场景用不同 provider。
CREATE TABLE model_role_assignments (
role TEXT PRIMARY KEY CHECK (role IN (
'planner',
'specialist',
'judge'
)),
provider_id TEXT NOT NULL REFERENCES llm_providers(id),
model TEXT NOT NULL, -- 该场景使用的具体模型 ID
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- ============================================================
-- 表4: system_settings — 通用 KV 设置
-- ============================================================
-- 存放非 LLM 的业务配置(由 UI 直接写入 DB
CREATE TABLE system_settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
is_sensitive INTEGER NOT NULL DEFAULT 0, -- 1=加密存储(复用 crypto 模块)
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- 索引
CREATE INDEX idx_providers_type ON llm_providers(type);
CREATE INDEX idx_providers_enabled ON llm_providers(is_enabled);
```
### 2.3 字段说明补充
| 表.字段 | 说明 |
|---|---|
| `llm_providers.type` | 决定使用哪个 adapter 实现 |
| `llm_providers.base_url` | `openai_compatible` 类型必填(用户自建代理地址);其他类型可选覆盖官方默认 endpoint |
| `llm_providers.extra_config` | JSON 字段,存放 provider 特有参数。例如 Gemini 的 `projectId`、OpenAI 的 `organization`、Anthropic 的 `anthropic-version` header 等 |
| `llm_secrets.key_version` | 用于密钥轮换:当 `ENCRYPTION_KEY` 更新后,启动时批量重加密所有 `key_version < current` 的记录 |
| `model_role_assignments.role` | 业务角色枚举,对应代码中不同调用场景 |
| `system_settings.is_sensitive` | 为 1 时 value 字段存密文(复用 `crypto/secrets.ts`GET API 返回 masked |
---
## 3. LLM Gateway 核心 TypeScript 接口
### 3.1 统一消息与请求/响应类型
```typescript
// ── src/llm/types.ts ────────────────────────────────────────
/** 模型角色枚举 */
export type ModelRole = 'planner' | 'specialist' | 'judge';
/** 统一消息格式(内部表达,不暴露 provider 差异) */
export interface LLMMessage {
role: 'system' | 'user' | 'assistant' | 'tool';
content: string;
toolCallId?: string; // role=tool 时关联的 tool call ID
toolCalls?: LLMToolCall[]; // role=assistant 时返回的 tool calls
}
export interface LLMToolCall {
id: string;
name: string;
arguments: string; // JSON string
}
/** 工具定义(内部通用格式,由 tool-converter.ts 转为各 provider 格式) */
export interface LLMToolDefinition {
name: string;
description: string;
parameters: Record<string, unknown>; // JSON Schema
}
/** 统一请求 */
export interface LLMChatRequest {
messages: LLMMessage[];
model: string;
temperature?: number;
maxTokens?: number;
responseFormat?: 'text' | 'json'; // 抽象 JSON mode
tools?: LLMToolDefinition[];
/** provider 透传配置(如 Anthropic thinking、Gemini safetySettings */
providerOptions?: Record<string, unknown>;
}
/** 统一响应 */
export interface LLMChatResponse {
content: string | null;
toolCalls: LLMToolCall[];
finishReason: 'stop' | 'tool_calls' | 'length' | 'content_filter' | 'error';
usage: {
promptTokens: number;
completionTokens: number;
totalTokens: number;
};
raw?: unknown; // 保留原始响应供调试
}
```
### 3.2 能力模型
```typescript
// ── src/llm/capabilities.ts ─────────────────────────────────
export interface ProviderCapabilities {
/** 是否支持 tool/function calling */
supportsTools: boolean;
/** 是否支持原生 JSON modevs 需要 prompt 指令 + 手动解析) */
supportsJsonMode: boolean;
/** 是否支持 SSE streaming */
supportsStreaming: boolean;
/** 是否支持 embedding 接口 */
supportsEmbeddings: boolean;
/** 是否支持图片/多模态输入 */
supportsMultimodal: boolean;
/** 最大输入 token 数(用于预校验,避免无效调用) */
maxInputTokens?: number;
}
/** 各 provider 默认能力声明 */
export const DEFAULT_CAPABILITIES: Record<string, ProviderCapabilities> = {
openai_compatible: {
supportsTools: true,
supportsJsonMode: true,
supportsStreaming: true,
supportsEmbeddings: true,
supportsMultimodal: false, // 取决于具体模型
},
openai_responses: {
supportsTools: true,
supportsJsonMode: true,
supportsStreaming: true,
supportsEmbeddings: true,
supportsMultimodal: true,
},
anthropic: {
supportsTools: true,
supportsJsonMode: false, // 无原生 JSON mode需 prompt 指令
supportsStreaming: true,
supportsEmbeddings: false,
supportsMultimodal: true,
},
gemini: {
supportsTools: true,
supportsJsonMode: true, // responseMimeType: 'application/json'
supportsStreaming: true,
supportsEmbeddings: true,
supportsMultimodal: true,
},
};
```
### 3.3 Provider 抽象接口
```typescript
// ── src/llm/providers/base.ts ───────────────────────────────
import type { ProviderCapabilities } from '../capabilities';
import type { LLMChatRequest, LLMChatResponse } from '../types';
export interface LLMProvider {
/** Provider 类型标识 */
readonly type: string;
/** 能力声明 */
readonly capabilities: ProviderCapabilities;
/**
* 核心调用方法。Gateway 只调用此方法。
* 各 adapter 负责:
* 1. 将 LLMChatRequest 转为 provider 原生格式
* 2. 发 HTTP / SDK 调用
* 3. 将原生响应转为 LLMChatResponse
*/
chat(request: LLMChatRequest): Promise<LLMChatResponse>;
/** 可选:嵌入接口 */
embed?(texts: string[]): Promise<number[][]>;
}
/** Provider 工厂函数签名:从 DB 配置 + 解密后 apiKey 创建实例 */
export type ProviderFactory = (config: {
baseUrl?: string;
apiKey: string;
defaultModel: string;
extraConfig: Record<string, unknown>;
}) => LLMProvider;
```
### 3.4 Gateway 入口
```typescript
// ── src/llm/gateway.ts ──────────────────────────────────────
import type { ModelRole, LLMChatRequest, LLMChatResponse } from './types';
import type { LLMProvider } from './providers/base';
/**
* LLM Gateway — 业务层唯一入口
*
* 职责:
* 1. 根据 role 查询 model_role_assignments → provider_id + model
* 2. 从 provider 缓存获取或按需创建LLMProvider 实例
* 3. 调用 provider.chat() 并返回统一响应
* 4. 如果 provider 配置变更UI 保存时invalidate 缓存
*/
export class LLMGateway {
/** provider 实例缓存provider_id → LLMProvider */
private cache = new Map<string, LLMProvider>();
/**
* 按业务角色调用 LLM
* @param role 业务角色planner/specialist/judge
* @param request 请求(不含 model由角色映射决定
*/
async chatForRole(
role: ModelRole,
request: Omit<LLMChatRequest, 'model'>
): Promise<LLMChatResponse>;
/**
* 用指定 provider 直接调用(连通性测试用)
*/
async chatDirect(
providerId: string,
request: LLMChatRequest
): Promise<LLMChatResponse>;
/** 配置变更时清除单个 provider 缓存 */
invalidateProvider(providerId: string): void;
/** 清除全部缓存(全局配置变更时) */
invalidateAll(): void;
}
```
---
## 4. 四个 Provider Adapter 核心差异映射
### 4.1 总览对照表
| 特性 | openai_compatible | openai_responses | anthropic | gemini |
|---|---|---|---|---|
| **SDK/HTTP** | `openai` npm (`chat.completions`) | `openai` npm (`responses.create`) | `@anthropic-ai/sdk` | `@google/generative-ai` 或 REST |
| **系统指令** | `messages[0].role='system'` | `instructions` 参数 | `system` 顶层参数 | `systemInstruction` 参数 |
| **JSON mode** | `response_format: {type:'json_object'}` | `text.format: {type:'json_object'}` | 无原生支持 → prompt 指令 + `JSON.parse` | `responseMimeType: 'application/json'` + `responseSchema` |
| **工具调用请求** | `tools[].type='function'` + `function.{name,description,parameters}` | `tools[].type='function'` + `function.{name,description,parameters}` | `tools[].name` + `input_schema` | `tools[].functionDeclarations[].{name,description,parameters}` |
| **工具结果返回** | `role: 'tool'` + `tool_call_id` | `type: 'function_call_output'` + `call_id` | `role: 'user'` + `content: [{type:'tool_result', tool_use_id}]` | `role: 'function'` + `parts: [{functionResponse}]` |
| **finish_reason** | `stop` / `tool_calls` / `length` | `stop` / `tool_calls` / ... | `end_turn` / `tool_use` / `max_tokens` | `STOP` / `FUNCTION_CALL` / `MAX_TOKENS` |
| **Token 用量** | `usage.{prompt,completion}_tokens` | `usage.{input,output}_tokens` | `usage.{input,output}_tokens` | `usageMetadata.{prompt,candidates}TokenCount` |
### 4.2 各 Adapter 核心转换逻辑
#### 4.2.1 openai_compatible现有兼容格式
```typescript
// 请求转换:几乎直通(这就是现有代码逻辑的抽象)
// - LLMMessage → OpenAI ChatCompletionMessage (直接映射)
// - responseFormat='json' → { type: 'json_object' }
// - tools → tools[].function (直接映射)
//
// 响应转换:
// - choices[0].message.content → content
// - choices[0].message.tool_calls → toolCalls
// - choices[0].finish_reason → finishReason (直接映射)
// - usage.{prompt,completion}_tokens → usage
```
#### 4.2.2 openai_responsesResponses API
```typescript
// 请求转换:
// - system message 提取为 instructions 参数
// - 非 system messages 转为 input items
// - responseFormat='json' → text: { format: { type: 'json_object' } }
// - tools → tools[].function
//
// 响应转换:
// - output items 中 type='message' → content
// - output items 中 type='function_call' → toolCalls
// - status → finishReason 映射
// - usage.{input,output}_tokens → usage
```
#### 4.2.3 anthropicMessages API
```typescript
// 请求转换:
// - system message 提取为 system 顶层参数
// - 非 system messages → messagesrole 直接映射)
// - responseFormat='json' → 无原生支持,在 system prompt 末尾追加:
// "You MUST respond with valid JSON only. No other text."
// - tools → tools[].{ name, description, input_schema }
// - tool results → role='user', content: [{ type: 'tool_result', tool_use_id, content }]
//
// 响应转换:
// - content blocks: type='text' → content
// - content blocks: type='tool_use' → toolCalls (id, name, JSON.stringify(input))
// - stop_reason: 'end_turn' → 'stop', 'tool_use' → 'tool_calls', 'max_tokens' → 'length'
// - usage.{input,output}_tokens → usage
//
// JSON mode 容错:
// - JSON.parse(content) 失败时,尝试正则提取 ```json...``` 块
```
#### 4.2.4 geminigenerateContent API
```typescript
// 请求转换:
// - system message 提取为 systemInstruction: { parts: [{ text }] }
// - messages → contents[].{ role: 'user'|'model', parts: [{ text }] }
// 注意Gemini 用 'model' 而非 'assistant'
// - responseFormat='json' → generationConfig: {
// responseMimeType: 'application/json',
// responseSchema: <如果有的话>
// }
// - tools → tools: [{ functionDeclarations: [...] }]
// - tool results → role: 'function', parts: [{ functionResponse: { name, response } }]
//
// 响应转换:
// - candidates[0].content.parts: type='text' → content
// - candidates[0].content.parts: functionCall → toolCalls
// - candidates[0].finishReason: 'STOP' → 'stop', 'FUNCTION_CALL' → 'tool_calls', 'MAX_TOKENS' → 'length'
// - usageMetadata.{promptTokenCount, candidatesTokenCount} → usage
```
### 4.3 tool-converter.ts 接口
```typescript
// ── src/llm/tool-converter.ts ───────────────────────────────
import type { LLMToolDefinition } from './types';
/**
* 将内部通用 LLMToolDefinition 转为各 provider 原生格式。
* 由各 adapter 在 chat() 中调用。
*/
/** → OpenAI / OpenAI Compatible 格式 */
export function toOpenAITools(tools: LLMToolDefinition[]): object[];
/** → Anthropic 格式 */
export function toAnthropicTools(tools: LLMToolDefinition[]): object[];
/** → Gemini functionDeclarations 格式 */
export function toGeminiTools(tools: LLMToolDefinition[]): object[];
```
---
## 5. 后端 REST API 契约
所有新 API 挂在 `/admin/api/llm/` 下,复用现有 JWT 鉴权中间件。
### 5.1 Provider 管理
| Method | Path | 说明 |
|---|---|---|
| `GET` | `/admin/api/llm/providers` | 列出所有 provider`hasKey` 布尔,不含明文 key |
| `POST` | `/admin/api/llm/providers` | 创建 provider + 设置 API Key |
| `GET` | `/admin/api/llm/providers/:id` | 获取单个详情 |
| `PUT` | `/admin/api/llm/providers/:id` | 更新name/base_url/default_model/extra_config/is_enabled |
| `DELETE` | `/admin/api/llm/providers/:id` | 删除(级联删 secret + role assignments |
### 5.2 API Key仅 set/clear不回显
| Method | Path | 说明 |
|---|---|---|
| `PUT` | `/admin/api/llm/providers/:id/key` | 设置/更新 API Key |
| `DELETE` | `/admin/api/llm/providers/:id/key` | 清除 API Key |
### 5.3 角色 → 模型映射
| Method | Path | 说明 |
|---|---|---|
| `GET` | `/admin/api/llm/roles` | 列出所有角色及当前绑定 |
| `PUT` | `/admin/api/llm/roles/:role` | 设置角色绑定 |
### 5.4 连通性测试
| Method | Path | 说明 |
|---|---|---|
| `POST` | `/admin/api/llm/providers/:id/test` | 发送简单 prompt 验证 provider 可达 |
### 5.5 通用设置(非 LLM
| Method | Path | 说明 |
|---|---|---|
| `GET` | `/admin/api/settings` | 列出所有sensitive 字段 masked |
| `PUT` | `/admin/api/settings` | 批量更新 |
### 5.6 请求/响应示例
#### 创建 Provider
```jsonc
// POST /admin/api/llm/providers
// Request:
{
"name": "Anthropic Claude",
"type": "anthropic",
"baseUrl": null,
"defaultModel": "claude-sonnet-4-20250514",
"apiKey": "sk-ant-xxxx",
"extraConfig": {}
}
// Response 201:
{
"id": "a1b2c3d4",
"name": "Anthropic Claude",
"type": "anthropic",
"baseUrl": null,
"defaultModel": "claude-sonnet-4-20250514",
"isEnabled": true,
"hasKey": true,
"extraConfig": {},
"createdAt": "2026-03-04T12:00:00Z"
}
```
#### 设置角色绑定
```jsonc
// PUT /admin/api/llm/roles/specialist
// Request:
{
"providerId": "a1b2c3d4",
"model": "claude-sonnet-4-20250514"
}
// Response 200:
{
"role": "specialist",
"providerId": "a1b2c3d4",
"providerName": "Anthropic Claude",
"providerType": "anthropic",
"model": "claude-sonnet-4-20250514"
}
```
#### 连通性测试
```jsonc
// POST /admin/api/llm/providers/a1b2c3d4/test
// Response 200:
{
"success": true,
"latencyMs": 823,
"model": "claude-sonnet-4-20250514",
"message": "Hello! I'm Claude, an AI assistant."
}
// Response 200 (失败):
{
"success": false,
"latencyMs": 5012,
"error": "401 Unauthorized: Invalid API key"
}
```
---
## 6. 密钥安全设计
### 6.1 Master Key 管理
```
启动流程:
1. 读取环境变量 ENCRYPTION_KEYhex 编码64 字符)
├── 未设置或为空 → 抛出错误,拒绝启动
├── 长度不正确 → 抛出错误,提示需要 64 个十六进制字符
└── 正确 → 解码为 32 字节 Buffer
2. 主密钥常驻内存(进程生命周期)
3. 绝对不写入日志、不暴露给 API
```
### 6.2 加密流程(写 API Key
```
输入: plaintext apiKey (string)
1. 生成 12 bytes IV: crypto.randomFillSync(new Uint8Array(12))
2. 创建 cipher: crypto.createCipheriv('aes-256-gcm', masterKey, iv)
3. 加密: ciphertext = Buffer.concat([cipher.update(apiKey, 'utf8'), cipher.final()])
4. 获取 auth tag: authTag = cipher.getAuthTag() // 16 bytes
5. 写 DB: INSERT INTO llm_secrets (provider_id, ciphertext, iv, auth_tag, key_version)
```
### 6.3 解密流程Gateway 需要调 provider
```
输入: provider_id
1. 从 DB 读取: { ciphertext, iv, auth_tag, key_version }
2. 创建 decipher: crypto.createDecipheriv('aes-256-gcm', masterKey, iv)
3. 设置 auth tag: decipher.setAuthTag(authTag)
4. 解密: plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()])
5. 返回明文 API Key → 传给 provider factory
6. 解密后的 key 随 provider 实例缓存在 Gateway.cache 中
```
### 6.4 密钥轮换
```
场景: 管理员更换 ENCRYPTION_KEY
1. 启动时读取新的 ENCRYPTION_KEY 环境变量
2. 查询所有 llm_secrets WHERE key_version < current_version
3. 逐条: 用旧 key 解密 → 用新 key 重加密 → 更新 key_version
4. 如果旧 key 不可用(环境变量缺失)→ 启动报错,要求重新设置所有 API Key
```
---
## 7. 前端配置页设计
### 7.1 页面结构
```
Settings 页面
├── 🔌 LLM ProvidersTab 或独立 Card
│ │
│ ├── Provider 列表表格
│ │ ┌──────────────────────────────────────────────────────────────┐
│ │ │ 名称 │ 类型 │ 默认模型 │ Key │ 状态 │ 操作 │
│ │ ├───────────────────┼───────────────┼───────────────┼─────┼────────┼──────┤
│ │ │ 公司 OpenAI 代理 │ OpenAI Compat │ gpt-4o-mini │ ● │ [开关] │ [⋮] │
│ │ │ Anthropic Claude │ Anthropic │ claude-sonnet │ ● │ [开关] │ [⋮] │
│ │ │ Google Gemini │ Gemini │ gemini-2.5-pro│ ○ │ [开关] │ [⋮] │
│ │ └──────────────────────────────────────────────────────────────┘
│ │ + 添加 Provider 按钮
│ │
│ ├── 添加/编辑 Provider 对话框
│ │ ├── 名称 (text input)
│ │ ├── 类型 (select dropdown)
│ │ │ ├── OpenAI Compatible — 兼容 OpenAI 接口的第三方服务
│ │ │ ├── OpenAI Responses — OpenAI 官方 Responses API
│ │ │ ├── Anthropic — Anthropic Messages API
│ │ │ └── Gemini — Google Gemini API
│ │ ├── Base URL (text, 条件显示openai_compatible 必填, 其他可选)
│ │ ├── 默认模型 (text + autocomplete suggestions)
│ │ ├── API Key (password input, 已有时显示 ••••••••)
│ │ ├── ▸ 高级配置 (collapsible, JSON key-value editor)
│ │ └── [测试连接] [保存] [取消]
│ │
│ └── 🧩 角色分配与分级审查映射 区域
│ ┌──────────────────────────────────────────────────────────────┐
│ │ 角色/阶段 │ Provider 下拉 │ 模型 ID │
│ ├──────────────┼──────────────────────┼──────────────────────┤
│ │ Planner(Triage) │ [公司 OpenAI 代理 ▾] │ [gpt-4o-mini ] │
│ │ Specialist(任务执行) │ [Anthropic Claude ▾] │ [claude-sonnet-4 ] │
│ │ Judge(汇总裁决) │ [公司 OpenAI 代理 ▾] │ [gpt-4o ] │
│ └──────────────────────────────────────────────────────────────┘
│ [保存角色分配]
├── ⚙️ 通用设置(现有 Gitea / 飞书 / App / Review 参数)
│ ├── Agent 分级审查参数small/medium 阈值、token budget、triage 开关
│ └── (复用现有 ConfigManager 组件,数据源统一为 DB)
```
### 7.2 交互规则
| 交互 | 行为 |
|---|---|
| **添加 Provider** | 弹出对话框;类型选择后动态显示/隐藏 `base_url` 字段 |
| **API Key 输入** | 已有 key 时展示 `••••••••`readonly 占位);清空内容后保存 = 删除 key输入新值 = 替换(调用 `PUT /key`);未修改 = 不发请求 |
| **测试连接** | 点击后调 `POST /providers/:id/test`;显示 spinner → 成功绿色 toast延迟+模型)/ 失败红色 toast错误信息 |
| **角色分配下拉** | 仅显示 `is_enabled=true``hasKey=true` 的 provider选择后自动填充该 provider 的 `default_model`(用户可修改) |
| **禁用 Provider** | 如果有角色绑定到此 provider → 弹确认对话框:"此 Provider 正被以下角色使用:[...],禁用后这些角色将无法调用 LLM。确定禁用" |
| **删除 Provider** | 同上,级联影响提示更强烈 |
| **模型建议** | 根据 provider type 显示常见模型建议列表(硬编码在前端,仅作参考,不限制输入) |
### 7.3 模型建议列表(前端硬编码参考)
```typescript
const MODEL_SUGGESTIONS: Record<string, string[]> = {
openai_compatible: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'deepseek-chat', 'qwen-plus'],
openai_responses: ['gpt-4o', 'gpt-4o-mini', 'gpt-4.1', 'gpt-4.1-mini', 'o3-mini'],
anthropic: ['claude-sonnet-4-20250514', 'claude-3-5-haiku-20241022', 'claude-opus-4-20250514'],
gemini: ['gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.0-flash'],
};
```
---
## 8. 现有调用点改造清单
### 8.1 后端代码改造
| # | 文件 | 当前代码 | 改造为 | 影响范围 |
|---|---|---|---|---|
| 1 | `src/index.ts:69-71` | `const openaiClient = new OpenAI({baseURL, apiKey})` | 删除;初始化 `LLMGateway` 单例并传入业务层 | 入口 |
| 2 | `src/controllers/review.ts` | 旧版 webhook 存在回退分支 | 删除回退分支,仅保留 `agent` / `codex` 入队逻辑 | 审查主入口 |
| 3 | `src/review/orchestrator.ts:45,61-63` | `private readonly openai: OpenAI` + `this.openai = new OpenAI(...)` | 构造函数改接收 `LLMGateway`;传给各 agent任务化分级编排skip/light/full | Agent 编排 |
| 4 | `src/review/agents/specialist-agent.ts:93` | `protected readonly openai: OpenAI` | → `protected readonly gateway: LLMGateway``reviewWithOptions()` 与 ReAct 调用改为 `this.gateway.chatForRole('specialist', ...)` | 核心 agent |
| 5 | `src/review/tools/registry.ts:22` | `toOpenAIFunctions()` | → `toToolDefinitions(): LLMToolDefinition[]`(返回内部格式),由 adapter 的 `tool-converter.ts` 负责转换 | 工具注册 |
| 6 | `src/config/config-schema.ts:120-138` | `OPENAI_BASE_URL/API_KEY/MODEL` 字段定义 | 删除这些字段;`group: 'openai'` → 整个 group 移除 | 配置 schema |
| 7 | `src/config/config-manager.ts:44-46` | `OPENAI_*` Zod schema 条目 | 删除 | 配置验证 |
| 8 | `src/config/config-manager.ts:271-273` | `openai: { baseUrl, apiKey, model }` 映射 | 删除整个 `openai` 块 | 配置输出 |
### 8.2 前端代码改造
| # | 文件 | 改造内容 |
|---|---|---|
| 1 | `frontend/src/services/configService.ts` | 新增 `llmProviderService.ts`Provider CRUD + Key 管理 + Role 管理 + Test |
| 2 | `frontend/src/components/ConfigManager.tsx` | 添加 "LLM Providers" Tab/Card引入新组件 |
| 3 | 新增 | `frontend/src/components/llm/ProviderList.tsx` — Provider 列表表格 |
| 4 | 新增 | `frontend/src/components/llm/ProviderDialog.tsx` — 添加/编辑对话框 |
| 5 | 新增 | `frontend/src/components/llm/RoleAssignment.tsx` — 角色分配面板 |
### 8.3 配置层改造
| 变更 | 说明 |
|---|---|
| `config-manager.ts` | 精简为只管非 LLM 配置;数据源统一为 `system_settings` 表 |
| `config-schema.ts` | 移除 `openai` group 及其字段;保留 gitea/feishu/app/admin/review非模型字段 |
| `controllers/config.ts` | LLM 相关接口迁到 `controllers/llm-config.ts`;通用配置接口改读写 DB |
| `.env.example` | 移除 `OPENAI_*``REVIEW_MODEL_*`;仅保留启动参数 |
---
## 9. 实施阶段建议
| 阶段 | 内容 | 依赖 | 估时 |
|---|---|---|---|
| **Phase 1: 基础设施** | DB 层 (`bun:sqlite` 初始化 + DDL) + crypto 模块 | 无 | 1d |
| **Phase 2: LLM 抽象层** | `src/llm/` 全部types + capabilities + errors + gateway + 4 adapters + tool-converter | Phase 1 | 2d |
| **Phase 3: 后端 API + 调用点替换** | `controllers/llm-config.ts` + 替换 11 个现有 OpenAI 调用点 + 测试 | Phase 2 | 1.5d |
| **Phase 4: 前端改造** | Provider 管理 + 角色分配 + 连接测试 UI + 通用设置切 DB | Phase 3 | 1.5d |
| **Phase 5: 清理与验收** | 删除旧代码 + 更新文档 + E2E 测试 + `.env.example` 精简 | Phase 4 | 0.5d |
**总计约 6.5 人天。**
### 关键里程碑
```
Day 1: DB + crypto 就绪,配置写入链路打通
Day 3: LLM Gateway 可用4 个 adapter 通过单元测试
Day 4.5: 后端 API 完成,所有调用点已替换,`bun test` 全绿
Day 6: 前端配置页可用,可通过 UI 添加/测试 Provider
Day 6.5: 旧代码清理完毕文档更新Ready for review
```
---
## 10. 风险与缓解
| 风险 | 影响 | 缓解措施 |
|---|---|---|
| **Anthropic 无原生 JSON mode** | `response_format: json_object` 不可用JSON 解析可能失败 | Adapter 内 prompt 注入 JSON 指令 + `JSON.parse()` 容错(正则提取 \`\`\`json\`\`\` 块 → 重试 parse |
| **Gemini function calling 格式差异大** | `functionDeclarations` 包装层级不同;`functionResponse` 嵌套在 `parts` 中 | `tool-converter.ts` 单独处理finish reason 映射表全覆盖测试 |
| **ENCRYPTION_KEY 丢失** | 所有加密的 API Key 不可恢复 | 启动时检测密钥版本不匹配 → 报错并要求重新设置所有 API Keytrade-off安全性 > 便利性) |
| **SQLite 并发写** | 多请求同时写入可能 SQLITE_BUSY | `bun:sqlite` 开启 WAL mode写操作走单连接序列化读可并行 |
| **Provider SDK 版本冲突** | `openai``@anthropic-ai/sdk``@google/generative-ai` 三个 SDK 共存 | 各 adapter 独立 import无交叉依赖`package.json` 锁定主版本 |
| **配置热更新** | UI 修改 provider 配置后,正在进行的审查仍用旧配置 | Gateway 缓存按 provider_id 粒度 invalidate正在执行的请求不受影响用的是已创建的实例下次请求用新实例 |
---
## 附录 A: 新增依赖
```jsonc
// package.json 新增
{
"dependencies": {
// bun:sqlite 是 Bun 内置,无需安装
"@anthropic-ai/sdk": "^0.39.0", // Anthropic adapter
"@google/generative-ai": "^0.24.0" // Gemini adapter
// "openai" 已存在: "^4.87.3" // OpenAI compatible + Responses adapter
}
}
```
## 附录 B: 环境变量精简
```bash
# .env.example仅保留启动参数
# 应用启动参数(不可通过 UI 设置)
PORT=5174
WEBHOOK_SECRET=your_webhook_secret
DATABASE_PATH=./data/assistant.db # SQLite 文件路径
# 以下配置已迁入数据库,通过 Web UI 管理:
# - LLM Provider 配置API Key / Base URL / Model
# - Gitea 配置API URL / Token
# - 飞书配置Webhook URL / Secret
# - Review 引擎配置
```

View File

@@ -1,154 +0,0 @@
# UI Theme Language亮/暗双主题统一规范)
## 目标
- 保证浅色/深色主题视觉一致、可读性稳定。
- 避免组件直接写死颜色,防止后续开发样式漂移。
- 让新增页面默认遵循同一套语义化设计语言。
## 三层设计语言模型
1. **Primitive原子值**HSL 基础值,仅在全局 token 定义处出现。
2. **Semantic语义 token**`background``foreground``success``danger` 等,按语义命名。
3. **Component组件 token**:组件只能消费语义 token不允许跨层引用原子值。
## 当前项目的主题基线
主题定义文件:`frontend/src/index.css`
- 基础语义:`background``foreground``card``muted``border``ring`
- 状态语义:`success``warning``danger``info`
- 补充语义:`surface-muted``surface-elevated``surface-overlay``text-subtle``text-soft``border-soft`
Tailwind 语义映射:`frontend/tailwind.config.js`
- 已将语义 token 映射为可直接使用的 class`bg-success/10``text-danger``border-info/20`)。
### 主色方案(当前)
- 主色选择:**Cobalt Blue钴蓝**,兼顾 light/dark 的对比度与品牌辨识度。
- Light`--primary: 224 76% 52%``--primary-foreground: 0 0% 100%`
- Dark`--primary: 224 88% 68%``--primary-foreground: 224 40% 12%`
- 焦点环:`--ring``--primary` 保持同色,确保交互一致性。
- 设计理由:亮色下避免过“脏”或偏绿感;暗色下提升明度保证可见性,同时用深色前景保证主按钮文字对比。
## 可选整套主题色方案(社区开源复用)
> 要求:切换的是**整套语义 token**,不是只改 `primary`。
当前支持四套(统一冷调科技风):
1. `cobalt`(默认)
- 本项目当前默认冷色科技风(自定义)。
2. `zinc`
- 来源shadcn/ui themesMIT
- 参考:<https://github.com/shadcn-ui/ui/blob/main/apps/v4/public/r/themes.css>
3. `nord`
- 来源NordMIT
- 参考:<https://github.com/nordtheme/nord>
4. `tokyo-night`
- 来源Tokyo NightApache-2.0
- 参考:<https://github.com/folke/tokyonight.nvim>
实现方式:
- `cobalt` 作为内置基础主题,直接由 `:root` / `.dark` 提供默认 token。
- 其余方案(`zinc|nord|tokyo-night`)通过 `data-palette` 覆盖:
- `:root[data-palette='*']` 覆盖浅色 token
- `.dark[data-palette='*']` 覆盖暗色 token
- 在根节点写入 `data-palette``cobalt|zinc|nord|tokyo-night`)。
- 组件侧不改业务 class继续消费语义 token。
## 页面风格骨架(社区方案落地)
> 目标:即使切换配色,页面结构、密度、层级、动效仍保持统一。
本项目采用了三类社区成熟范式,并映射到本仓库 utility
1. **4px 节奏与密度系统shadcn/仪表盘实践)**
- 基础节奏按 4px 递进,主内容区使用 `theme-page-content`(统一宽度 + 留白节奏)。
- 卡片内部与卡片间距默认采用 `p-6 / gap-6` 级别,避免页面“块状松散或拥挤”。
2. **三层深度系统(卡片/悬浮/遮罩)**
- 统一卡片外观:`theme-card-shell` + `theme-card-header` + `theme-card-content`
- 交互抬升统一:`theme-interactive-elevate`(轻微位移 + 阴影,不做夸张动效)。
- 页面壳层统一:`theme-shell-gradient` + `theme-sticky-bar`
3. **可控动效系统Linear/Vercel 风格)**
- Hover/按钮反馈优先短时平滑动效,避免大幅动画导致“廉价感”。
- 表单输入统一 `theme-input-surface`,状态条与统计胶囊统一 `theme-control-pill`
参考来源:
- shadcn/ui themes 与组件风格实践MIT<https://github.com/shadcn-ui/ui>
- Vercel Dashboard 设计迭代思路:<https://vercel.com/changelog/dashboard-navigation-redesign-rollout>
- Nord / Tokyo Night 社区配色体系:
- <https://github.com/nordtheme/nord>
- <https://github.com/folke/tokyonight.nvim>
## 强制规则(必须遵守)
1. **禁止在业务 TSX 中使用硬编码暗色类**:如 `bg-zinc-*``text-zinc-*``border-white/10`(历史 UI 基础组件逐步迁移,不作为新增业务代码例外)。
2. **禁止在组件内写死颜色值**:如 `rgba(...)``#xxxxxx``rgb(...)`
3. **状态色统一语义化**:成功/警告/错误/信息统一用 `success|warning|danger|info`
4. **弹窗/卡片/表格优先使用语义表面色**`card``muted``popover``background`
5. **交互阴影统一工具类**`theme-glow-primary|success|warning|danger`
6. **普通 hover 反馈禁止用主色背景**:非主操作控件统一使用 `hover:bg-accent*``hover:bg-muted*`,避免亮色主题出现重色块。
## 推荐 class 使用方式
- 文字层级:`text-foreground` / `text-muted-foreground`
- 面板层级:`bg-card` / `bg-muted/50` / `bg-popover`
- 边框层级:`border-border` / `theme-border-soft`
- 状态展示:`text-success``bg-danger/10``border-warning/30`
- 普通交互 hover`hover:bg-accent/60``hover:bg-accent``hover:bg-muted/60`
- 主操作 hover仅主按钮可用 `hover:bg-primary/90`
- 顶部吸附操作栏:`theme-sticky-bar`
- 页面骨架:`theme-page-frame` / `theme-page-actions` / `theme-page-content`
- 卡片骨架:`theme-card-shell` / `theme-card-header` / `theme-card-content`
- 弹窗骨架:`theme-dialog-panel` / `theme-dialog-header` / `theme-dialog-body` / `theme-dialog-footer`
- 错误态容器:`theme-error-panel`
- 模态遮罩:`theme-surface-overlay`
## 页面级统一约束(防止布局风格漂移)
1. 页面容器优先使用 `theme-page-frame`,避免每个页面自行定义高度和底部间距。
2. 顶部操作区统一使用 `theme-sticky-bar + theme-page-actions`,避免按钮栏视觉断层。
3. 主内容区统一使用 `theme-page-content`,确保横向节奏和留白一致。
4. 标准业务卡片统一使用 `theme-card-shell/header/content`,避免同类卡片出现不同边框/背景层级。
5. 错误提示统一使用 `theme-error-panel`,保持状态反馈视觉语言一致。
## destructive 与 danger 的约定
- `destructive`:保留给 shadcn 组件内置 destructive 变体语义。
- `danger`:业务状态语义(报错、失败、风险提示)统一使用。
- 新业务组件优先使用 `danger`,避免 `destructive/danger` 混用造成漂移。
## 新功能开发检查清单
- [ ] 页面在 light/dark 下均可读(文本、边框、状态色有对比度)
- [ ]`zinc/white` 等暗色硬编码 class
- [ ] 无内联 `style` 颜色值
- [ ] 状态色全部使用语义 token
- [ ] 组件未绕过语义层直接访问原子颜色
- [ ] `bun run ui:visual` 通过light/dark 关键页面视觉回归)
## 视觉基线截图回归Playwright
- 生成/更新基线:`bun run ui:visual:update`
- 校验基线一致性:`bun run ui:visual`
- 轻量 UI 全链路:`bun run ui:regression && bun run ui:visual`
约定:
1. PR 默认运行 `ui:visual`,出现 diff 必须人工确认是“预期视觉变更”。
2. 只有在确认设计变更成立时,才执行 `ui:visual:update` 更新基线并提交快照。
3. 不允许在未更新设计规范的情况下大量更新视觉基线,避免把漂移“固化为正确”。
4. 基线快照以 Linux CI 环境为准(当前为 `*-linux.png`),避免跨系统更新导致快照噪声。
## 迁移策略
当新增模块时,按以下顺序处理:
1. 先补充语义 token如确有新语义而不是新颜色
2. 在 Tailwind 映射语义 token。
3. 在组件中只消费语义 class。
4. 最后做 light/dark 视觉回归。

View File

@@ -2,68 +2,92 @@
## Prerequisites
- Bun >= 1.2.5
- [Bun](https://bun.sh) >= 1.2.5
- A reachable Gitea instance
- At least one LLM provider credential
- At least one LLM provider credential (OpenAI, Anthropic, Gemini, or compatible)
## Install
```bash
git clone https://github.com/user/gitea-ai-assistant.git
git clone https://github.com/jeffusion/gitea-ai-assistant.git
cd gitea-ai-assistant
bun install
```
`bun install` at repository root installs frontend dependencies via `postinstall`.
If lifecycle scripts are disabled:
`bun install` at repository root installs frontend dependencies via `postinstall`. If lifecycle scripts are disabled:
```bash
bun run bootstrap
```
## Minimal environment
## Configure environment
Create `.env`:
Create `.env` in the project root:
```bash
PORT=5174
ENCRYPTION_KEY= # required, generate with: openssl rand -hex 32
ENCRYPTION_KEY=<generate with: openssl rand -hex 32>
# PORT=5174
# DATABASE_PATH=./data/assistant.db
# LOG_LEVEL=info # local dev default; use LOG_LEVEL=error in production
# LOG_LEVEL=info
```
> `ENCRYPTION_KEY` is required. Application startup fails when it is missing.
`ENCRYPTION_KEY` is required — the application refuses to start without it. It is the AES-256-GCM master key for encrypting API keys stored in the database.
See [Configuration](./configuration.md) for all environment variables and runtime settings.
## Run
```bash
bun run dev
# or
bun run start
bun run dev # development with hot reload
bun run start # production mode
```
Open `http://localhost:5174` to access the Admin UI.
## First login
- Open `http://your-server:5174`
- Default admin password is `password` on first boot
- Change admin password immediately after login
- Default admin password is `password` on first boot.
- **Change it immediately** after login (Security section in Admin UI).
## Configure in Admin UI
The Admin UI manages all runtime settings stored in SQLite. You only need `.env` for infrastructure bootstrap values.
![Dashboard](./assets/page-repos.png)
Key settings to configure:
1. **Gitea** — API URL, access token
2. **LLM Providers** — add at least one provider (OpenAI Compatible, Anthropic, Gemini, etc.) with API key and default model
3. **Webhook Secret** — used for HMAC-SHA256 signature verification
See [Screenshots](./screenshots.md) for a full UI gallery.
## Webhook setup
### Option A: Admin UI (recommended)
In repository list, click enable to auto-provision webhook.
In the repository list page, click the enable button. The system auto-provisions the webhook in Gitea.
### Option B: Manual
In Gitea repository settings:
In Gitea repository settings → Webhooks → Add webhook:
- URL: `http://your-server:5174/webhook/gitea`
- Content Type: `application/json`
- Secret: same value as dashboard webhook secret
- Events: Pull Request + Status
| Field | Value |
|---|---|
| URL | `http://your-server:5174/webhook/gitea` |
| Content Type | `application/json` |
| Secret | Same value as configured in Admin UI |
| Events | Pull Request + Status |
## Health endpoint
## Health check
Use `/api/health` to check service status.
```
GET /api/health
```
## Next steps
- [Configuration reference](./configuration.md) — all settings and runtime model
- [Review engines](./review-engines.md) — Agent engine, Codex engine, review modes
- [Deployment](./deployment.md) — Docker, Compose, Kubernetes

View File

@@ -2,68 +2,92 @@
## 环境要求
- Bun >= 1.2.5
- [Bun](https://bun.sh) >= 1.2.5
- 可访问的 Gitea 实例
- 至少一个 LLM 提供商凭证
- 至少一个 LLM 提供商凭证OpenAI、Anthropic、Gemini 或兼容接口)
## 安装
```bash
git clone https://github.com/user/gitea-ai-assistant.git
git clone https://github.com/jeffusion/gitea-ai-assistant.git
cd gitea-ai-assistant
bun install
```
在仓库根目录执行 `bun install` 会通过 `postinstall` 自动安装 `frontend` 依赖。
如果你的环境禁用了生命周期脚本:
在仓库根目录执行 `bun install` 会通过 `postinstall` 自动安装前端依赖。如果环境禁用了生命周期脚本:
```bash
bun run bootstrap
```
## 最小环境变量
## 配置环境变量
创建 `.env` 文件:
在项目根目录创建 `.env` 文件:
```bash
PORT=5174
ENCRYPTION_KEY= # 必填,使用 openssl rand -hex 32 生成
ENCRYPTION_KEY=<使用 openssl rand -hex 32 生成>
# PORT=5174
# DATABASE_PATH=./data/assistant.db
# LOG_LEVEL=info # 本地开发默认;生产环境请使用 LOG_LEVEL=error
# LOG_LEVEL=info
```
> `ENCRYPTION_KEY` 为必填项缺失时服务会拒绝启动。
`ENCRYPTION_KEY` 为必填项——缺失时服务会拒绝启动。它是用于加密数据库中 API Key 的 AES-256-GCM 主密钥。
所有环境变量和运行时设置参见 [配置参考](./configuration.zh-CN.md)。
## 启动服务
```bash
bun run dev
# 或
bun run start
bun run dev # 开发模式(热重载)
bun run start # 生产模式
```
访问 `http://localhost:5174` 进入管理后台。
## 首次登录
- 访问 `http://your-server:5174`
- 首次启动默认管理员密码为 `password`
- 登录后请立即修改管理员密码
- **登录后请立即修改密码**(管理后台「安全」分区)
## 管理后台配置
管理后台管理所有持久化到 SQLite 的运行时配置。`.env` 仅用于基础设施级引导参数。
![管理后台](./assets/page-repos.png)
需要配置的关键项:
1. **Gitea** — API URL、Access Token
2. **LLM Provider** — 添加至少一个提供商OpenAI Compatible、Anthropic、Gemini 等)并配置 API Key 和默认模型
3. **Webhook Secret** — 用于 HMAC-SHA256 签名验证
完整界面截图参见 [截图集](./screenshots.zh-CN.md)。
## Webhook 配置
### 方式 A管理后台推荐
在仓库列表点击启用按钮,系统自动配置 webhook。
在仓库列表点击启用按钮,系统自动在 Gitea 中配置 Webhook。
### 方式 B手动配置
在 Gitea 仓库设置中配置
在 Gitea 仓库设置 → Webhooks → 添加 Webhook
- URL`http://your-server:5174/webhook/gitea`
- Content Type`application/json`
- Secret与管理后台中的 Webhook Secret 保持一致
- 事件Pull Request + Status
| 字段 | 值 |
|---|---|
| URL | `http://your-server:5174/webhook/gitea` |
| Content Type | `application/json` |
| Secret | 与管理后台中配置的 Webhook Secret 保持一致 |
| 事件 | Pull Request + Status |
## 健康检查
可通过 `/api/health` 查看服务状态。
```
GET /api/health
```
## 下一步
- [配置参考](./configuration.zh-CN.md) — 完整设置项与运行时模型
- [审查引擎](./review-engines.zh-CN.md) — Agent 引擎、Codex 引擎与审查模式
- [部署指南](./deployment.zh-CN.md) — Docker、Compose、Kubernetes

View File

@@ -1,43 +1,81 @@
# Review Engines
## Overview
The system supports two engines:
- `agent`: native Agent review pipeline
- `codex`: Codex CLI-backed review pipeline
Engine is selected by `REVIEW_ENGINE` runtime configuration.
The system supports two review engines, selected by `REVIEW_ENGINE` in Admin UI.
## Agent engine
The Agent engine runs code reviews using a dynamic agent framework. It prepares the workspace and review context, then starts a main agent to perform the review.
The Agent engine uses a dynamic agent framework. It prepares the workspace and review context, then starts a main agent to perform the review.
### Review behavior
### How it works
- **Main Agent**: The entrypoint agent that coordinates the review process. It uses the tools provided to analyze the code changes.
- **Dynamic Subagents**: The main agent can dynamically spawn subagents to perform specific tasks, such as searching code or reading files, if needed.
- **Deterministic Publishing**: Review findings and comments are collected and processed outside the agent loop. The system normalizes, deduplicates, and filters findings deterministically before posting them back to Gitea.
1. **Main Agent** — the entrypoint agent that coordinates the review. It uses available tools to analyze code changes.
2. **Dynamic Subagents** — the main agent can autonomously spawn subagents for focused tasks (e.g. searching code, reading files). Subagents are created at runtime through tool calls, not hardcoded in the workflow.
3. **Deterministic Publishing** findings and comments are collected and processed outside the agent loop. The system normalizes, deduplicates, and filters findings deterministically before posting to Gitea.
### Review modes
- `skip`: Low-risk changes may bypass the agent review entirely.
- `light`: Minimal checks for low-risk code changes.
- `full`: Full review for risky or larger changes.
| Mode | Behavior |
|---|---|
| `skip` | Low-risk changes bypass review entirely |
| `light` | Minimal checks for low-risk code changes |
| `full` | Complete review for risky or large changes |
### Size policy
`small`/`medium`/`large` thresholds are used to classify the change size, which determines the execution mode and token budgets.
Change size determines execution mode and token budgets:
| Size | Typical threshold |
|---|---|
| `small` | Few lines changed |
| `medium` | Moderate change set |
| `large` | Significant refactoring or many files |
> Size and mode are separate layers: `small/medium/large` classifies how big the change is; `skip/light/full` controls how deeply the engine reviews it.
## Codex engine
Codex engine runs review through Codex CLI with independent runtime settings:
The Codex engine runs review through Codex CLI with independent runtime settings:
- `CODEX_API_URL`
- `CODEX_API_KEY`
- `CODEX_MODEL`
- `CODEX_TIMEOUT_MS`
- `CODEX_REVIEW_PROMPT`
| Setting | Description |
|---|---|
| `CODEX_API_URL` | Codex API endpoint |
| `CODEX_API_KEY` | Codex API key |
| `CODEX_MODEL` | Model to use |
| `CODEX_TIMEOUT_MS` | Request timeout |
| `CODEX_REVIEW_PROMPT` | Custom review prompt |
## Agent definitions
Agent definitions are Markdown files with YAML frontmatter stored in the reviewed repository:
```
.gitea-assistant/agents/*.md
```
Each file defines:
- **System prompt** — instructions for the agent
- **Model** — which LLM model to use (optional; falls back to runtime defaults)
- **Max turns** — limit for the agent loop
- **Tools** — which tools the agent can access
### Model resolution
When the main agent spawns a subagent, the model is resolved in this order:
1. `spawn` override (explicit in the tool call)
2. `AgentDefinition.model` (declared in the agent definition file)
3. `AGENT_DEFAULT_SUBAGENT_MODEL` (runtime config)
4. `AGENT_MAIN_MODEL` (runtime config)
## Tool permissions
Tool permissions are controlled within each agent's definition file:
| Field | Type | Description |
|---|---|---|
| `tools` | Allow list | Tool names the agent is permitted to call. Empty list grants no tools. |
| `disallowedTools` | Deny list | Tool names the agent is explicitly forbidden from calling. Takes precedence over `tools`. |
## Event support
@@ -48,5 +86,5 @@ Both engines process:
## Output
- PR/commit summary comment
- Line-level findings with confidence and severity
- PR/commit summary comment (posted as an issue comment)
- Line-level findings with confidence and severity (posted as review comments)

View File

@@ -1,43 +1,81 @@
# 审查引擎
## 概览
系统支持两种审查引擎:
- `agent`:内置 Agent 审查流水线
- `codex`:基于 Codex CLI 的审查流水线
通过运行时配置 `REVIEW_ENGINE` 选择引擎。
系统支持两种审查引擎,通过管理后台的 `REVIEW_ENGINE` 配置选择。
## Agent 引擎
Agent 引擎使用动态 Agent 框架执行代码审查。它会准备工作区与审查上下文,然后启动主 Agent 执行审查任务。
### 审查行为
### 工作原理
- **主 Agent**协调审查流程的入口 Agent。它使用提供的工具分析代码变更。
- **动态子 Agent**主 Agent 可以根据需要动态生成子 Agent执行特定任务(如搜索代码读取文件)。
- **确定性发布**审查发现的问题与评论在 Agent 循环之外进行收集和处理。系统会在将结果发布 Gitea 之前,对发现的问题进行确定性的规范化、去重和过滤。
1. **主 Agent**协调审查流程的入口 Agent,使用可用工具分析代码变更。
2. **动态子 Agent**主 Agent 可在运行时自主生成子 Agent执行聚焦任务(如搜索代码读取文件)。子 Agent 通过工具调用动态创建,而非硬编码在工作流中。
3. **确定性发布**审查发现与评论在 Agent 循环之外收集和处理。系统发布 Gitea 之前,对发现进行确定性的规范化、去重和过滤。
### 审查模式
- `skip`:低风险改动可完全跳过 Agent 审查。
- `light`:对低风险代码执行最小化检查。
- `full`:对高风险或大规模改动执行完整审查。
| 模式 | 行为 |
|---|---|
| `skip` | 低风险改动完全跳过审查 |
| `light` | 对低风险代码执行最小化检查 |
| `full` | 对高风险或大规模改动执行完整审查 |
### 规模策略
`small` / `medium` / `large` 阈值用于对变更规模进行分类,从而决定执行模式与 Token 预算
变更规模决定执行模式与 Token 预算
| 规模 | 典型阈值 |
|---|---|
| `small` | 少量行变更 |
| `medium` | 中等变更集 |
| `large` | 大规模重构或多文件变更 |
> 规模与模式是两个层次:`small/medium/large` 分类变更的大小;`skip/light/full` 控制审查的深度。
## Codex 引擎
Codex 引擎通过 Codex CLI 执行审查,支持独立配置:
- `CODEX_API_URL`
- `CODEX_API_KEY`
- `CODEX_MODEL`
- `CODEX_TIMEOUT_MS`
- `CODEX_REVIEW_PROMPT`
| 设置项 | 说明 |
|---|---|
| `CODEX_API_URL` | Codex API 端点 |
| `CODEX_API_KEY` | Codex API 密钥 |
| `CODEX_MODEL` | 使用的模型 |
| `CODEX_TIMEOUT_MS` | 请求超时时间 |
| `CODEX_REVIEW_PROMPT` | 自定义审查提示词 |
## Agent 定义
Agent 定义以带 YAML Frontmatter 的 Markdown 文件形式存储在被审查的仓库中:
```
.gitea-assistant/agents/*.md
```
每个文件定义:
- **系统提示词** — Agent 的指令
- **模型** — 使用的 LLM 模型(可选;未指定时使用运行时默认值)
- **最大轮数** — Agent 循环上限
- **工具** — Agent 可使用的工具
### 模型解析
主 Agent 生成子 Agent 时,模型按以下顺序解析:
1. `spawn` 覆盖(工具调用中显式指定)
2. `AgentDefinition.model`Agent 定义文件中声明)
3. `AGENT_DEFAULT_SUBAGENT_MODEL`(运行时配置)
4. `AGENT_MAIN_MODEL`(运行时配置)
## 工具权限
工具权限在每个 Agent 的定义文件中控制:
| 字段 | 类型 | 说明 |
|---|---|---|
| `tools` | 白名单 | 允许该 Agent 调用的工具名称。空列表表示不授予任何工具权限 |
| `disallowedTools` | 黑名单 | 显式禁止该 Agent 调用的工具名称。优先级高于白名单 |
## 事件支持
@@ -48,5 +86,5 @@ Codex 引擎通过 Codex CLI 执行审查,支持独立配置:
## 输出
- PR/提交总结评论
- 行级问题(含置信度与严重性)
- PR/提交总结评论(作为 Issue 评论发布)
- 行级问题(含置信度与严重性,作为审查评论发布