mirror of
https://github.com/jeffusion/gitea-ai-assistant.git
synced 2026-06-12 23:16:49 +00:00
Compare commits
4 Commits
main
...
refactor/d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d442e193dd | ||
|
|
7d6794f368 | ||
|
|
6c83e12bf5 | ||
|
|
bc1dfb6dde |
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,16 +1,3 @@
|
||||
# [1.4.0](https://github.com/jeffusion/gitea-ai-assistant/compare/v1.3.1...v1.4.0) (2026-05-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **agent-kernel:** address Oracle review round 2 findings ([27f4ac6](https://github.com/jeffusion/gitea-ai-assistant/commit/27f4ac6a18ec3510575f9234486e2fd5fc72de3c))
|
||||
* **review-agent:** complete fingerprint migration — dual-index all three keys ([2ee9f57](https://github.com/jeffusion/gitea-ai-assistant/commit/2ee9f570c4e43f6fa2901e6a1ff10d79826b7d60))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **agent-kernel:** cherry-pick high-value components from PR [#15](https://github.com/jeffusion/gitea-ai-assistant/issues/15) ([44d52cd](https://github.com/jeffusion/gitea-ai-assistant/commit/44d52cddc5e5301b9ec4ae8ca11ba926dd709cd3)), closes [hi#value](https://github.com/hi/issues/value)
|
||||
|
||||
## [1.3.1](https://github.com/jeffusion/gitea-ai-assistant/compare/v1.3.0...v1.3.1) (2026-03-26)
|
||||
|
||||
|
||||
|
||||
119
CONTRIBUTING.md
119
CONTRIBUTING.md
@@ -1,119 +0,0 @@
|
||||
# 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
104
README.md
@@ -2,58 +2,104 @@
|
||||
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
||||
AI-powered code review assistant for Gitea. Receives webhooks, runs AI review workflows, and posts summary + line-level feedback back to Gitea.
|
||||
AI-powered code review assistant for Gitea. It receives webhooks, runs AI review workflows, and posts summary + line-level feedback back to Gitea.
|
||||
|
||||
[English](./docs/README.md) | [中文](./docs/README.zh-CN.md)
|
||||
- English docs: [./docs/README.md](./docs/README.md)
|
||||
- 中文文档: [./docs/README.zh-CN.md](./docs/README.zh-CN.md)
|
||||
|
||||
## Features
|
||||
## Why this project
|
||||
|
||||
- **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
|
||||
- 🤖 **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
|
||||
|
||||

|
||||
## Product screenshot
|
||||
|
||||
## Quick start
|
||||
> Dashboard screenshot is generated from local dev service.
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||
```bash
|
||||
git clone https://github.com/jeffusion/gitea-ai-assistant.git
|
||||
git clone https://github.com/user/gitea-ai-assistant.git
|
||||
cd gitea-ai-assistant
|
||||
bun install # installs frontend via postinstall
|
||||
bun install
|
||||
```
|
||||
|
||||
Create `.env`:
|
||||
If lifecycle scripts are disabled in your environment, run:
|
||||
|
||||
```bash
|
||||
ENCRYPTION_KEY=$(openssl rand -hex 32)
|
||||
bun run bootstrap
|
||||
```
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
Open `http://localhost:5174`, login with default password `password` (change it immediately), then configure Gitea, LLM providers, and webhook in the Admin UI.
|
||||
### 5) Configure in Admin UI
|
||||
|
||||
See [Getting Started](./docs/getting-started.md) for full setup walkthrough including webhook configuration.
|
||||
Open `http://your-server:5174`, login with default `password` (first boot only), then change it immediately.
|
||||
|
||||
## Documentation
|
||||
- Configure Gitea API + tokens
|
||||
- Configure webhook secret
|
||||
- Configure LLM providers/models
|
||||
- Configure review engine and policy
|
||||
|
||||
| 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 |
|
||||
### 6) Add webhook in Gitea
|
||||
|
||||
## Contributing
|
||||
- URL: `http://your-server:5174/webhook/gitea`
|
||||
- Content-Type: `application/json`
|
||||
- Secret: same as dashboard webhook secret
|
||||
- Events: Pull Request + Status
|
||||
|
||||
See [CONTRIBUTING.md](./CONTRIBUTING.md) for development conventions and UI guidelines.
|
||||
## 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)
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
MIT License
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
# Documentation
|
||||
|
||||
Setup, configuration, and deployment guides for Gitea AI Assistant.
|
||||
This project keeps the root `README.md` concise and moves implementation/deployment details here.
|
||||
|
||||
## Getting started
|
||||
## Start here
|
||||
|
||||
- [Getting Started](./getting-started.md) — full installation, first login, and webhook setup walkthrough
|
||||
- [Screenshots](./screenshots.md) — Admin UI gallery (one page per feature area)
|
||||
- [Getting started](./getting-started.md)
|
||||
- [Configuration reference](./configuration.md)
|
||||
- [Review engines](./review-engines.md)
|
||||
- [Deployment](./deployment.md)
|
||||
- [Screenshot gallery](./screenshots.md)
|
||||
|
||||
## Reference
|
||||
## Architecture & design
|
||||
|
||||
- [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
|
||||
- [Notification service refactoring](./design/notification-service-refactoring.md)
|
||||
- [UI theme language](./design/ui-theme-language.md)
|
||||
|
||||
## Language
|
||||
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
# 文档中心
|
||||
|
||||
Gitea AI Assistant 的安装、配置与部署指南。
|
||||
根目录 `README.md` 保持简洁;本目录提供详细说明与设计文档。
|
||||
|
||||
## 快速开始
|
||||
## 快速导航
|
||||
|
||||
- [快速开始](./getting-started.zh-CN.md) — 完整安装、首次登录与 Webhook 配置指引
|
||||
- [截图集](./screenshots.zh-CN.md) — 管理后台界面一览(每个功能页面一张截图)
|
||||
- [快速开始](./getting-started.zh-CN.md)
|
||||
- [配置参考](./configuration.zh-CN.md)
|
||||
- [审查引擎](./review-engines.zh-CN.md)
|
||||
- [部署指南](./deployment.zh-CN.md)
|
||||
- [截图集](./screenshots.zh-CN.md)
|
||||
|
||||
## 参考手册
|
||||
## 架构与设计
|
||||
|
||||
- [配置参考](./configuration.zh-CN.md) — 环境变量、管理后台设置与运行时配置模型
|
||||
- [审查引擎](./review-engines.zh-CN.md) — Agent 引擎、Codex 引擎、审查模式、规模策略与 Agent 定义
|
||||
- [通知服务重构设计](./design/notification-service-refactoring.md)
|
||||
- [UI 主题语言设计](./design/ui-theme-language.md)
|
||||
|
||||
## 部署
|
||||
## 产品截图
|
||||
|
||||
- [部署指南](./deployment.zh-CN.md) — Docker、Docker Compose 与 Kubernetes
|
||||

|
||||
|
||||
## 语言切换
|
||||
|
||||
|
||||
@@ -2,23 +2,21 @@
|
||||
|
||||
## Configuration model
|
||||
|
||||
This project uses a **DB-first** runtime configuration model:
|
||||
This project uses a DB-first runtime configuration model:
|
||||
|
||||
- `.env` stores only infrastructure-level bootstrap values
|
||||
- Runtime settings (Gitea, providers, secrets, review policy, notifications) are managed in Admin UI and persisted to SQLite
|
||||
- `.env` contains only infrastructure-level bootstrap values.
|
||||
- Runtime settings (Gitea, providers, secrets, review policy, notifications) are managed in Admin UI and stored in SQLite.
|
||||
|
||||
This means you configure most settings through the web dashboard after first boot, not through environment variables.
|
||||
|
||||
## Environment variables
|
||||
## Environment variables (minimal)
|
||||
|
||||
| Variable | Required | Description | Default |
|
||||
|---|---|---|---|
|
||||
| `ENCRYPTION_KEY` | Yes | AES-256-GCM master key for API key encryption (64 hex chars) | — |
|
||||
| `ENCRYPTION_KEY` | Yes | AES-256-GCM master key (64 hex chars) for API key encryption | - |
|
||||
| `PORT` | No | Service port | `5174` |
|
||||
| `DATABASE_PATH` | No | SQLite database path | `./data/assistant.db` |
|
||||
| `LOG_LEVEL` | No | Backend log level: `debug` / `info` / `warn` / `error` | `info` |
|
||||
| `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` |
|
||||
|
||||
Generate encryption key:
|
||||
Generate key:
|
||||
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
@@ -26,57 +24,62 @@ openssl rand -hex 32
|
||||
|
||||
## First boot defaults
|
||||
|
||||
When the database is empty on first launch:
|
||||
When database is empty:
|
||||
|
||||
- `JWT_SECRET` — auto-generated
|
||||
- `WEBHOOK_SECRET` — auto-generated
|
||||
- `ADMIN_PASSWORD` — defaults to `password` (**change immediately after login**)
|
||||
- `JWT_SECRET` auto-generated
|
||||
- `WEBHOOK_SECRET` auto-generated
|
||||
- `ADMIN_PASSWORD` defaults to `password`
|
||||
|
||||
## Admin UI settings
|
||||
Change `ADMIN_PASSWORD` immediately after first login.
|
||||
|
||||
All settings below are configured through the Admin UI at `http://your-server:5174`.
|
||||
## Runtime groups in Admin UI
|
||||
|
||||
### Gitea
|
||||
## 1) Gitea
|
||||
|
||||
| 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 |
|
||||
- API URL
|
||||
- Access token
|
||||
- Admin token (optional)
|
||||
|
||||
### Security
|
||||
## 2) Security
|
||||
|
||||
| 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) |
|
||||
- Webhook secret (HMAC-SHA256 verification)
|
||||
- Admin password
|
||||
- JWT secret
|
||||
|
||||
### LLM
|
||||
## 3) LLM
|
||||
|
||||
| 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` |
|
||||
- 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`.
|
||||
|
||||
Model resolution order: `spawn override > AgentDefinition.model > AGENT_DEFAULT_SUBAGENT_MODEL > AGENT_MAIN_MODEL`
|
||||
## 4) Notification
|
||||
|
||||
### Notifications
|
||||
- Feishu webhook and optional secret
|
||||
- WeCom (企业微信) webhook
|
||||
|
||||
| Setting | Description |
|
||||
|---|---|
|
||||
| Feishu Webhook | Feishu bot webhook URL and optional signing secret |
|
||||
| WeCom Webhook | WeCom (企业微信) bot webhook URL |
|
||||
## 5) Review
|
||||
|
||||
### Review
|
||||
- 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
|
||||
|
||||
| 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 |
|
||||
> Size and mode are different layers:
|
||||
>
|
||||
> - `small/medium/large`: change-size classification
|
||||
> - `skip/light/full`: review execution depth
|
||||
|
||||
> 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.
|
||||
## 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.
|
||||
|
||||
@@ -2,23 +2,21 @@
|
||||
|
||||
## 配置模型
|
||||
|
||||
项目采用 **DB-first** 运行时配置模型:
|
||||
项目采用 DB-first 运行时配置模型:
|
||||
|
||||
- `.env` 仅存储基础设施级引导参数
|
||||
- `.env` 仅用于基础设施级引导参数
|
||||
- 运行时配置(Gitea、Provider、密钥、审查策略、通知)由管理后台维护并持久化到 SQLite
|
||||
|
||||
即大部分设置在首次启动后通过 Web 管理后台配置,而非环境变量。
|
||||
|
||||
## 环境变量
|
||||
## 环境变量(最小集)
|
||||
|
||||
| 变量 | 必填 | 说明 | 默认值 |
|
||||
|---|---|---|---|
|
||||
| `ENCRYPTION_KEY` | 是 | API Key 加密主密钥(AES-256-GCM,64 位十六进制) | — |
|
||||
| `ENCRYPTION_KEY` | 是 | API Key 加密主密钥(AES-256-GCM,64 位十六进制) | - |
|
||||
| `PORT` | 否 | 服务端口 | `5174` |
|
||||
| `DATABASE_PATH` | 否 | SQLite 数据库路径 | `./data/assistant.db` |
|
||||
| `LOG_LEVEL` | 否 | 后端日志级别:`debug` / `info` / `warn` / `error` | `info` |
|
||||
| `DATABASE_PATH` | 否 | SQLite 路径 | `./data/assistant.db` |
|
||||
| `LOG_LEVEL` | 否 | 后端日志级别(`debug`/`info`/`warn`/`error`)。默认 `info`;生产环境建议 `error`。 | `info` |
|
||||
|
||||
生成加密密钥:
|
||||
生成密钥:
|
||||
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
@@ -26,57 +24,62 @@ openssl rand -hex 32
|
||||
|
||||
## 首次启动默认值
|
||||
|
||||
数据库为空时首次启动:
|
||||
当数据库为空时:
|
||||
|
||||
- `JWT_SECRET` — 自动生成
|
||||
- `WEBHOOK_SECRET` — 自动生成
|
||||
- `ADMIN_PASSWORD` — 默认 `password`(**登录后请立即修改**)
|
||||
- `JWT_SECRET` 自动生成
|
||||
- `WEBHOOK_SECRET` 自动生成
|
||||
- `ADMIN_PASSWORD` 默认 `password`
|
||||
|
||||
## 管理后台设置
|
||||
首次登录后请立即修改管理员密码。
|
||||
|
||||
以下所有设置均通过管理后台 `http://your-server:5174` 配置。
|
||||
## 管理后台配置分组
|
||||
|
||||
### Gitea
|
||||
## 1) Gitea
|
||||
|
||||
| 设置项 | 说明 |
|
||||
|---|---|
|
||||
| API URL | Gitea API 端点(如 `http://gitea:3000/api/v1`) |
|
||||
| Access Token | 用于克隆仓库和发布评论的令牌 |
|
||||
| Admin Token | 可选;仓库发现功能需要 |
|
||||
- API URL
|
||||
- Access Token
|
||||
- Admin Token(可选)
|
||||
|
||||
### 安全
|
||||
## 2) 安全
|
||||
|
||||
| 设置项 | 说明 |
|
||||
|---|---|
|
||||
| Webhook Secret | HMAC-SHA256 签名验证密钥 |
|
||||
| Admin Password | 管理后台登录密码 |
|
||||
| JWT Secret | Token 签名密钥(首次启动自动生成) |
|
||||
- Webhook Secret(HMAC-SHA256 验签)
|
||||
- Admin Password
|
||||
- JWT Secret
|
||||
|
||||
### LLM
|
||||
## 3) LLM
|
||||
|
||||
| 设置项 | 说明 |
|
||||
|---|---|
|
||||
| Provider | 添加一个或多个提供商:OpenAI Compatible / OpenAI Responses / Anthropic / Gemini |
|
||||
| `AGENT_MAIN_MODEL` | 主 Agent 运行时默认模型。默认值:`gpt-4.1` |
|
||||
| `AGENT_DEFAULT_SUBAGENT_MODEL` | 子 Agent 未声明模型且 spawn 未覆盖时的默认模型。默认值:`gpt-4.1-mini` |
|
||||
- Provider:OpenAI Compatible / OpenAI Responses / Anthropic / Gemini
|
||||
- Agent 运行时模型:
|
||||
- `AGENT_MAIN_MODEL`:在没有更具体模型配置时,Agent 运行时使用的主模型名称。默认值为 `gpt-4.1`。
|
||||
- `AGENT_DEFAULT_SUBAGENT_MODEL`:当子代理(Subagent)未声明模型且 spawn 未覆盖时,使用的默认模型名称。默认值为 `gpt-4.1-mini`。
|
||||
|
||||
模型解析顺序:`spawn 覆盖 > AgentDefinition.model > AGENT_DEFAULT_SUBAGENT_MODEL > AGENT_MAIN_MODEL`
|
||||
## 4) 通知
|
||||
|
||||
### 通知
|
||||
- Feishu Webhook 与可选签名密钥
|
||||
- WeCom(企业微信)Webhook
|
||||
|
||||
| 设置项 | 说明 |
|
||||
|---|---|
|
||||
| Feishu Webhook | 飞书机器人 Webhook URL 及可选签名密钥 |
|
||||
| WeCom Webhook | 企业微信机器人 Webhook URL |
|
||||
## 5) 审查
|
||||
|
||||
### 审查
|
||||
- 引擎模式:`agent` / `codex`
|
||||
- Triage 规模分类与路由提示
|
||||
- 规模阈值(`small`/`medium`/`large`)
|
||||
- 执行模式(`skip`/`light`/`full`)
|
||||
- Token 预算与并发限制
|
||||
|
||||
| 设置项 | 说明 |
|
||||
|---|---|
|
||||
| 引擎 | `agent` 或 `codex` |
|
||||
| 规模阈值 | `small` / `medium` / `large` — 变更规模分类 |
|
||||
| 执行模式 | `skip` / `light` / `full` — 审查深度控制 |
|
||||
| Token 预算 | 各模式 Token 限额 |
|
||||
| 并发限制 | 最大并行审查数 |
|
||||
> 规模与模式是两个层次:
|
||||
>
|
||||
> - `small/medium/large`:变更规模分类
|
||||
> - `skip/light/full`:审查执行深度
|
||||
|
||||
> 规模与模式是两个层次:`small/medium/large` 分类变更的大小;`skip/light/full` 控制审查的深度。
|
||||
## Agent 定义
|
||||
|
||||
项目的 Agent 定义以带有 Frontmatter 的 Markdown 文件形式存储在仓库中:
|
||||
- 路径:`.gitea-assistant/agents/*.md`
|
||||
|
||||
这些文件定义了每个 Agent 的系统提示词、元数据和执行参数。
|
||||
|
||||
## 工具权限
|
||||
|
||||
工具权限直接在每个 Agent 的定义文件中进行控制:
|
||||
- `tools`:允许该 Agent 调用的工具名称白名单。如果列表为空,则不授予任何工具权限。
|
||||
- `disallowedTools`:显式禁止该 Agent 调用的工具名称黑名单。黑名单的优先级高于白名单。
|
||||
|
||||
617
docs/design/notification-service-refactoring.md
Normal file
617
docs/design/notification-service-refactoring.md
Normal file
@@ -0,0 +1,617 @@
|
||||
# 通知服务抽象化重构方案
|
||||
|
||||
## 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
|
||||
**状态**: 已实施(持续验证中)
|
||||
821
docs/design/pluggable-llm-providers.md
Normal file
821
docs/design/pluggable-llm-providers.md
Normal file
@@ -0,0 +1,821 @@
|
||||
# 技术设计文档:可插拔 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 group,LLM 配置全部走 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, -- 可选自定义 endpoint(openai_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 mode(vs 需要 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_responses(Responses 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 anthropic(Messages API)
|
||||
|
||||
```typescript
|
||||
// 请求转换:
|
||||
// - system message 提取为 system 顶层参数
|
||||
// - 非 system messages → messages(role 直接映射)
|
||||
// - 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 gemini(generateContent 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_KEY(hex 编码,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 Providers(Tab 或独立 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 Key(trade-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 引擎配置
|
||||
```
|
||||
154
docs/design/ui-theme-language.md
Normal file
154
docs/design/ui-theme-language.md
Normal file
@@ -0,0 +1,154 @@
|
||||
# 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 themes(MIT)
|
||||
- 参考:<https://github.com/shadcn-ui/ui/blob/main/apps/v4/public/r/themes.css>
|
||||
3. `nord`
|
||||
- 来源:Nord(MIT)
|
||||
- 参考:<https://github.com/nordtheme/nord>
|
||||
4. `tokyo-night`
|
||||
- 来源:Tokyo Night(Apache-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 视觉回归。
|
||||
@@ -2,92 +2,68 @@
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Bun](https://bun.sh) >= 1.2.5
|
||||
- Bun >= 1.2.5
|
||||
- A reachable Gitea instance
|
||||
- At least one LLM provider credential (OpenAI, Anthropic, Gemini, or compatible)
|
||||
- At least one LLM provider credential
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
git clone https://github.com/jeffusion/gitea-ai-assistant.git
|
||||
git clone https://github.com/user/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
|
||||
```
|
||||
|
||||
## Configure environment
|
||||
## Minimal environment
|
||||
|
||||
Create `.env` in the project root:
|
||||
Create `.env`:
|
||||
|
||||
```bash
|
||||
ENCRYPTION_KEY=<generate with: openssl rand -hex 32>
|
||||
# PORT=5174
|
||||
PORT=5174
|
||||
ENCRYPTION_KEY= # required, generate with: openssl rand -hex 32
|
||||
# DATABASE_PATH=./data/assistant.db
|
||||
# LOG_LEVEL=info
|
||||
# LOG_LEVEL=info # local dev default; use LOG_LEVEL=error in production
|
||||
```
|
||||
|
||||
`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.
|
||||
> `ENCRYPTION_KEY` is required. Application startup fails when it is missing.
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
bun run dev # development with hot reload
|
||||
bun run start # production mode
|
||||
bun run dev
|
||||
# or
|
||||
bun run start
|
||||
```
|
||||
|
||||
Open `http://localhost:5174` to access the Admin UI.
|
||||
|
||||
## First 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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
- Open `http://your-server:5174`
|
||||
- Default admin password is `password` on first boot
|
||||
- Change admin password immediately after login
|
||||
|
||||
## Webhook setup
|
||||
|
||||
### Option A: Admin UI (recommended)
|
||||
|
||||
In the repository list page, click the enable button. The system auto-provisions the webhook in Gitea.
|
||||
In repository list, click enable to auto-provision webhook.
|
||||
|
||||
### Option B: Manual
|
||||
|
||||
In Gitea repository settings → Webhooks → Add webhook:
|
||||
In Gitea repository settings:
|
||||
|
||||
| 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 |
|
||||
- URL: `http://your-server:5174/webhook/gitea`
|
||||
- Content Type: `application/json`
|
||||
- Secret: same value as dashboard webhook secret
|
||||
- Events: Pull Request + Status
|
||||
|
||||
## Health check
|
||||
## Health endpoint
|
||||
|
||||
```
|
||||
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
|
||||
Use `/api/health` to check service status.
|
||||
|
||||
@@ -2,92 +2,68 @@
|
||||
|
||||
## 环境要求
|
||||
|
||||
- [Bun](https://bun.sh) >= 1.2.5
|
||||
- Bun >= 1.2.5
|
||||
- 可访问的 Gitea 实例
|
||||
- 至少一个 LLM 提供商凭证(OpenAI、Anthropic、Gemini 或兼容接口)
|
||||
- 至少一个 LLM 提供商凭证
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
git clone https://github.com/jeffusion/gitea-ai-assistant.git
|
||||
git clone https://github.com/user/gitea-ai-assistant.git
|
||||
cd gitea-ai-assistant
|
||||
bun install
|
||||
```
|
||||
|
||||
在仓库根目录执行 `bun install` 会通过 `postinstall` 自动安装前端依赖。如果环境禁用了生命周期脚本:
|
||||
在仓库根目录执行 `bun install` 会通过 `postinstall` 自动安装 `frontend` 依赖。
|
||||
|
||||
如果你的环境禁用了生命周期脚本:
|
||||
|
||||
```bash
|
||||
bun run bootstrap
|
||||
```
|
||||
|
||||
## 配置环境变量
|
||||
## 最小环境变量
|
||||
|
||||
在项目根目录创建 `.env` 文件:
|
||||
创建 `.env` 文件:
|
||||
|
||||
```bash
|
||||
ENCRYPTION_KEY=<使用 openssl rand -hex 32 生成>
|
||||
# PORT=5174
|
||||
PORT=5174
|
||||
ENCRYPTION_KEY= # 必填,使用 openssl rand -hex 32 生成
|
||||
# DATABASE_PATH=./data/assistant.db
|
||||
# LOG_LEVEL=info
|
||||
# LOG_LEVEL=info # 本地开发默认;生产环境请使用 LOG_LEVEL=error
|
||||
```
|
||||
|
||||
`ENCRYPTION_KEY` 为必填项——缺失时服务会拒绝启动。它是用于加密数据库中 API Key 的 AES-256-GCM 主密钥。
|
||||
|
||||
所有环境变量和运行时设置参见 [配置参考](./configuration.zh-CN.md)。
|
||||
> `ENCRYPTION_KEY` 为必填项,缺失时服务会拒绝启动。
|
||||
|
||||
## 启动服务
|
||||
|
||||
```bash
|
||||
bun run dev # 开发模式(热重载)
|
||||
bun run start # 生产模式
|
||||
bun run dev
|
||||
# 或
|
||||
bun run start
|
||||
```
|
||||
|
||||
访问 `http://localhost:5174` 进入管理后台。
|
||||
|
||||
## 首次登录
|
||||
|
||||
- 访问 `http://your-server:5174`
|
||||
- 首次启动默认管理员密码为 `password`
|
||||
- **登录后请立即修改密码**(管理后台「安全」分区)
|
||||
|
||||
## 管理后台配置
|
||||
|
||||
管理后台管理所有持久化到 SQLite 的运行时配置。`.env` 仅用于基础设施级引导参数。
|
||||
|
||||

|
||||
|
||||
需要配置的关键项:
|
||||
|
||||
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:管理后台(推荐)
|
||||
|
||||
在仓库列表页点击启用按钮,系统自动在 Gitea 中配置 Webhook。
|
||||
在仓库列表点击启用按钮,由系统自动配置 webhook。
|
||||
|
||||
### 方式 B:手动配置
|
||||
|
||||
在 Gitea 仓库设置 → Webhooks → 添加 Webhook:
|
||||
在 Gitea 仓库设置中配置:
|
||||
|
||||
| 字段 | 值 |
|
||||
|---|---|
|
||||
| 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
|
||||
|
||||
## 健康检查
|
||||
|
||||
```
|
||||
GET /api/health
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- [配置参考](./configuration.zh-CN.md) — 完整设置项与运行时模型
|
||||
- [审查引擎](./review-engines.zh-CN.md) — Agent 引擎、Codex 引擎与审查模式
|
||||
- [部署指南](./deployment.zh-CN.md) — Docker、Compose、Kubernetes
|
||||
可通过 `/api/health` 查看服务状态。
|
||||
|
||||
@@ -1,81 +1,43 @@
|
||||
# Review Engines
|
||||
|
||||
The system supports two review engines, selected by `REVIEW_ENGINE` in Admin UI.
|
||||
## 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.
|
||||
|
||||
## Agent engine
|
||||
|
||||
The Agent engine uses a dynamic agent framework. It prepares the workspace and review context, then starts a main agent to perform the review.
|
||||
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.
|
||||
|
||||
### How it works
|
||||
### Review behavior
|
||||
|
||||
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.
|
||||
- **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.
|
||||
|
||||
### Review modes
|
||||
|
||||
| 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 |
|
||||
- `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.
|
||||
|
||||
### Size policy
|
||||
|
||||
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.
|
||||
`small`/`medium`/`large` thresholds are used to classify the change size, which determines the execution mode and token budgets.
|
||||
|
||||
## Codex engine
|
||||
|
||||
The Codex engine runs review through Codex CLI with independent runtime settings:
|
||||
Codex engine runs review through Codex CLI with independent runtime settings:
|
||||
|
||||
| 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`. |
|
||||
- `CODEX_API_URL`
|
||||
- `CODEX_API_KEY`
|
||||
- `CODEX_MODEL`
|
||||
- `CODEX_TIMEOUT_MS`
|
||||
- `CODEX_REVIEW_PROMPT`
|
||||
|
||||
## Event support
|
||||
|
||||
@@ -86,5 +48,5 @@ Both engines process:
|
||||
|
||||
## Output
|
||||
|
||||
- PR/commit summary comment (posted as an issue comment)
|
||||
- Line-level findings with confidence and severity (posted as review comments)
|
||||
- PR/commit summary comment
|
||||
- Line-level findings with confidence and severity
|
||||
|
||||
@@ -1,81 +1,43 @@
|
||||
# 审查引擎
|
||||
|
||||
系统支持两种审查引擎,通过管理后台的 `REVIEW_ENGINE` 配置选择。
|
||||
## 概览
|
||||
|
||||
系统支持两种审查引擎:
|
||||
|
||||
- `agent`:内置 Agent 审查流水线
|
||||
- `codex`:基于 Codex CLI 的审查流水线
|
||||
|
||||
通过运行时配置 `REVIEW_ENGINE` 选择引擎。
|
||||
|
||||
## Agent 引擎
|
||||
|
||||
Agent 引擎使用动态 Agent 框架执行代码审查。它会准备工作区与审查上下文,然后启动主 Agent 执行审查任务。
|
||||
|
||||
### 工作原理
|
||||
### 审查行为
|
||||
|
||||
1. **主 Agent** — 协调审查流程的入口 Agent,使用可用工具分析代码变更。
|
||||
2. **动态子 Agent** — 主 Agent 可在运行时自主生成子 Agent,执行聚焦任务(如搜索代码、读取文件)。子 Agent 通过工具调用动态创建,而非硬编码在工作流中。
|
||||
3. **确定性发布** — 审查发现与评论在 Agent 循环之外收集和处理。系统在发布到 Gitea 之前,对发现进行确定性的规范化、去重和过滤。
|
||||
- **主 Agent**:协调审查流程的入口 Agent。它使用提供的工具来分析代码变更。
|
||||
- **动态子 Agent**:主 Agent 可以根据需要动态生成子 Agent,以执行特定任务(例如搜索代码或读取文件)。
|
||||
- **确定性发布**:审查发现的问题与评论会在 Agent 循环之外进行收集和处理。系统会在将结果发布回 Gitea 之前,对发现的问题进行确定性的规范化、去重和过滤。
|
||||
|
||||
### 审查模式
|
||||
|
||||
| 模式 | 行为 |
|
||||
|---|---|
|
||||
| `skip` | 低风险改动完全跳过审查 |
|
||||
| `light` | 对低风险代码执行最小化检查 |
|
||||
| `full` | 对高风险或大规模改动执行完整审查 |
|
||||
- `skip`:低风险改动可完全跳过 Agent 审查。
|
||||
- `light`:对低风险代码执行最小化检查。
|
||||
- `full`:对高风险或大规模改动执行完整审查。
|
||||
|
||||
### 规模策略
|
||||
|
||||
变更规模决定执行模式与 Token 预算:
|
||||
|
||||
| 规模 | 典型阈值 |
|
||||
|---|---|
|
||||
| `small` | 少量行变更 |
|
||||
| `medium` | 中等变更集 |
|
||||
| `large` | 大规模重构或多文件变更 |
|
||||
|
||||
> 规模与模式是两个层次:`small/medium/large` 分类变更的大小;`skip/light/full` 控制审查的深度。
|
||||
`small` / `medium` / `large` 阈值用于对变更规模进行分类,从而决定执行模式与 Token 预算。
|
||||
|
||||
## Codex 引擎
|
||||
|
||||
Codex 引擎通过 Codex CLI 执行审查,支持独立配置:
|
||||
|
||||
| 设置项 | 说明 |
|
||||
|---|---|
|
||||
| `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 调用的工具名称。优先级高于白名单 |
|
||||
- `CODEX_API_URL`
|
||||
- `CODEX_API_KEY`
|
||||
- `CODEX_MODEL`
|
||||
- `CODEX_TIMEOUT_MS`
|
||||
- `CODEX_REVIEW_PROMPT`
|
||||
|
||||
## 事件支持
|
||||
|
||||
@@ -86,5 +48,5 @@ Agent 定义以带 YAML Frontmatter 的 Markdown 文件形式存储在被审查
|
||||
|
||||
## 输出
|
||||
|
||||
- PR/提交总结评论(作为 Issue 评论发布)
|
||||
- 行级问题(含置信度与严重性,作为审查评论发布)
|
||||
- PR/提交总结评论
|
||||
- 行级问题(含置信度与严重性)
|
||||
|
||||
Reference in New Issue
Block a user