4 Commits

Author SHA1 Message Date
jeffusion
d442e193dd fix(review-agent): complete fingerprint migration — dual-index all three keys
- existingPublished now indexes finding.fingerprint, modern, and legacy
- rememberPublished merges published status with true-wins semantics
- regression test: legacy published finding stays published after migration to modern fingerprint
2026-05-27 00:04:16 +08:00
jeffusion
7d6794f368 fix(agent-kernel): address Oracle review round 2 findings
- tryParseFinalSubmission: return result when summaryMarkdown is non-empty even with empty findings
- consecutiveToolFailures: append tool result to transcript before checking termination threshold
- fingerprint: add buildLegacyFingerprint and dual-index existingPublished for migration compatibility
- regression tests: empty findings+summary, clip newline boundary, fingerprint format migration
2026-05-26 23:42:13 +08:00
jeffusion
6c83e12bf5 feat(agent-kernel): cherry-pick high-value components from PR #15
- Zod findingResponseSchema + LLM repair for malformed model output (max 2 attempts)
- Budget guards: maxSubagents, maxEmptyResponses, maxConsecutiveToolFailures
  - maxSubagents: refuse spawn at limit, allow model to summarize
  - consecutiveToolFailures: per-tool-call update, reset on success, immediate terminate
- Tool permission scope system (6 scopes, allow/deny static policy, no ask)
  - allowListSpecified flag distinguishes subagent vs main agent resolution
- SHA256 finding fingerprint with JSON tuple input (avoids colon ambiguity)
- Token counting + context clipping via tokenlens (newline-boundary clipping)
- diffBudget floor of 1000 tokens (prevents negative budget for small models)
- tryParseFinalSubmission: full JSON first (preserves summaryMarkdown), Zod fallback
- normalizeFinding: Zod-only validation, no lax fallback
- E2E README: fix webhook signing (--data-binary, openssl dgst, repository.name)
2026-05-26 22:43:56 +08:00
jeffusion
bc1dfb6dde refactor: replace fixed review workflow with dynamic agent framework
- Add agent-kernel runtime (MainAgentRunner with while-true state machine,
  subagent spawning, tool loop, budget control)
- Add review-agent entrypoint with read_file/search_code/spawn_subagent tools
- Add deterministic publish adapter with cross-specialist finding dedup
- Delete old fixed workflow (orchestrator, triage, specialist, judge agents,
  4 domain agents, critic/debate/reflexion agents, learning/memory system)
- Remove legacy ModelRole (planner/specialist/judge) from LLM types, gateway,
  config schema, DB, and frontend RoleAssignment UI
- Replace RoleAssignment with AgentModelSettings for role-based model config
- Add agent config API endpoints (GET/PUT /admin/api/agents/config)
- Add review session detail page with observability/findings/logs tabs
- Add runtime contract tests and review adapter integration tests
- Add E2E mock LLM with scripted behavior support for deterministic testing
- Update E2E test script with subagent and finding assertions
- Add e2e/README.md with real PR review testing guide
- Fix seed.sh to run gitea admin commands as git user (not root)
- Update docs (configuration, review-engines, deployment, README)
- Remove unused feedback controller, qdrant k8s manifest, embedding migration
- Add .omo/ and .opencode/ to .gitignore
2026-05-26 15:45:22 +08:00
14 changed files with 1890 additions and 500 deletions

View File

@@ -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)

View File

@@ -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
View File

@@ -2,58 +2,104 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
![Dashboard](./docs/assets/page-repos.png)
## Product screenshot
## Quick start
> Dashboard screenshot is generated from local dev service.
![Gitea AI Assistant Dashboard - Repository Page](./docs/assets/page-repos.png)
More screenshots (one per admin menu): [EN](./docs/screenshots.md) | [中文](./docs/screenshots.zh-CN.md)
## Architecture (high-level)
```
Gitea Webhook -> Gitea AI Assistant (Hono + Bun) -> LLM Gateway (multi-provider)
|
+-> Admin Dashboard (React)
```
For component-level design, see [Architecture docs](./docs/README.md#architecture--design).
## Quick start (minimal)
### 1) Prerequisites
- Bun >= 1.2.5
- Reachable Gitea instance
- At least one LLM provider credential
### 2) Install
```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

View File

@@ -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

View File

@@ -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
![Gitea AI Assistant 管理后台(仓库管理页)](./assets/page-repos.png)
## 语言切换

View File

@@ -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.

View File

@@ -2,23 +2,21 @@
## 配置模型
项目采用 **DB-first** 运行时配置模型:
项目采用 DB-first 运行时配置模型:
- `.env`存储基础设施级引导参数
- `.env`用于基础设施级引导参数
- 运行时配置Gitea、Provider、密钥、审查策略、通知由管理后台维护并持久化到 SQLite
即大部分设置在首次启动后通过 Web 管理后台配置,而非环境变量。
## 环境变量
## 环境变量(最小集)
| 变量 | 必填 | 说明 | 默认值 |
|---|---|---|---|
| `ENCRYPTION_KEY` | 是 | API Key 加密主密钥AES-256-GCM64 位十六进制) | |
| `ENCRYPTION_KEY` | 是 | API Key 加密主密钥AES-256-GCM64 位十六进制) | - |
| `PORT` | 否 | 服务端口 | `5174` |
| `DATABASE_PATH` | 否 | SQLite 数据库路径 | `./data/assistant.db` |
| `LOG_LEVEL` | 否 | 后端日志级别`debug` / `info` / `warn` / `error` | `info` |
| `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 SecretHMAC-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` |
- ProviderOpenAI 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 调用的工具名称黑名单。黑名单的优先级高于白名单。

View 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
**状态**: 已实施(持续验证中)

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

View File

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

View File

@@ -2,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.
![Dashboard](./assets/page-repos.png)
Key settings to configure:
1. **Gitea** — API URL, access token
2. **LLM Providers** — add at least one provider (OpenAI Compatible, Anthropic, Gemini, etc.) with API key and default model
3. **Webhook Secret** — used for HMAC-SHA256 signature verification
See [Screenshots](./screenshots.md) for a full UI gallery.
- 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.

View File

@@ -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` 仅用于基础设施级引导参数。
![管理后台](./assets/page-repos.png)
需要配置的关键项:
1. **Gitea** — API URL、Access Token
2. **LLM Provider** — 添加至少一个提供商OpenAI Compatible、Anthropic、Gemini 等)并配置 API Key 和默认模型
3. **Webhook Secret** — 用于 HMAC-SHA256 签名验证
完整界面截图参见 [截图集](./screenshots.zh-CN.md)。
- 登录后请立即修改管理员密码
## Webhook 配置
### 方式 A管理后台推荐
在仓库列表点击启用按钮,系统自动在 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` 查看服务状态。

View File

@@ -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

View File

@@ -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/提交总结评论
- 行级问题(含置信度与严重性)