本文档面向开发者和 AI 编码助手,涵盖项目架构、开发环境、编码规范。
| 层 | 技术 |
|---|---|
| Agent 框架 | Google ADK |
| 后端 | FastAPI + SQLAlchemy 2.0 async + Pydantic v2 |
| 数据库 | SQLite(开发)/ PostgreSQL(生产) |
| 前端 | React 19 + Vite 8 + TypeScript 6 + TailwindCSS 4 + shadcn/ui |
| 认证 | JWT + OAuth 2.0 OIDC + API Token |
| 部署 | Docker Compose |
| 工具 | 版本 |
|---|---|
| Python | >= 3.13 |
| uv | >= 0.6 |
| Node.js | >= 20 |
| pnpm | >= 10 |
adk-demo/
├── root_agent/ # 根调度 Agent
│ ├── agent.py # root_agent 定义
│ └── agents/ # 子 Agent(gitea/misskey/push/search)
├── server/ # FastAPI 后端
│ ├── main.py # 应用入口
│ ├── models/ # ORM 模型
│ ├── schemas/ # Pydantic 模型
│ ├── routers/ # API 路由
│ └── services/ # 业务逻辑层
├── web/ # React 前端
├── local_agent/ # Local Agent(TypeScript CLI)
├── docs/ # 文档
├── Dockerfile # 后端镜像
├── web/Dockerfile # 前端镜像
└── docker-compose.yml # 编排
| 命令 | 说明 |
|---|---|
uv run server |
启动后端(FastAPI + uvicorn, reload 模式) |
uv run dev |
启动 Agent 交互模式(adk run) |
uv run lint |
Ruff 代码检查 |
uv run fmt |
Ruff 格式化 |
uv run fix |
自动修复 + 格式化 |
uv run check |
CI 用:lint + format 检查 |
cd web
pnpm install # 安装依赖
pnpm dev # 开发服务器 http://localhost:5173
pnpm build # 生产构建
pnpm lint # ESLint 检查
pnpm lint --fix # 自动修复cd local_agent
bun install # 安装依赖
bun run build # 构建
npx tsc --noEmit # 类型检查支持 Gemini、DeepSeek、OpenAI、Ollama 等 LLM,通过环境变量配置。
优先级:{AGENT_NAME}_MODEL → AGENT_MODEL → gemini-2.5-flash
# 全局用 DeepSeek
AGENT_MODEL=deepseek/deepseek-v4-flash
AGENT_TOKEN=sk-xxx
AGENT_API=https://api.deepseek.com
# root_agent 单独用 Gemini
ROOT_AGENT_MODEL=gemini-2.5-flash全局统一 ISO 8601 UTC(带 Z 后缀)。 后端 API 返回的所有时间字段格式为 2026-05-01T03:00:00Z,前端负责转换为用户本地时区显示。
所有面向用户的文本必须走 i18n,禁止硬编码。
- 前端:
useTranslation('namespace')+locales/{zh,en,ja}/module.json - Local Agent:
src/i18n/目录,t.xxx.yyy访问
# 后端
uv sync
# 前端
cd web && pnpm install项目使用 pre-commit 在提交前自动检查代码质量。首次 clone 后必须安装:
uv run pre-commit install每次 git commit 时会自动运行:
- ruff:Python lint + 自动修复
- ruff format:Python 代码格式化
- ESLint:前端 TypeScript lint
- tsc --noEmit:前端类型检查
手动全量检查:
uv run pre-commit run --all-files项目使用 Alembic 管理数据库 schema 变更,不需要手动 ALTER TABLE 或删库重建。
# 改了 ORM 模型后,生成迁移脚本
uv run alembic revision --autogenerate -m "add xxx column"
# 手动执行迁移(通常不需要,应用启动时自动执行)
uv run alembic upgrade head
# 查看当前数据库版本
uv run alembic current应用启动时会自动执行 alembic upgrade head,无需手动操作。
- 新增 NOT NULL 列:已有数据的表不能直接加 NOT NULL,必须分步:
# 1. 加 nullable 列(带 server_default) op.add_column("table", sa.Column("col", sa.String(20), nullable=True, server_default="value")) # 2. 填充已有行 op.execute("UPDATE table SET col = 'value' WHERE col IS NULL") # 3. 改为 NOT NULL op.alter_column("table", "col", nullable=False)
op.alter_column:SQLite 不支持大部分 ALTER COLUMN 操作,如需修改列类型需使用op.batch_alter_table- Boolean 类型:PostgreSQL 有原生 Boolean,SQLite 用 INTEGER(0/1),ORM 层自动处理,但原始 SQL 注意区分
- 测试:迁移写完后建议分别在本地 SQLite 和 docker-compose 的 PostgreSQL 上验证
任何代码变更后,必须对变更部分执行 lint 检查,确认通过后再提交。 这是强制性规则,适用于所有开发者和 AI 编码助手。
# Python 变更
uv run ruff check --fix <changed_files>
uv run ruff format <changed_files>
# 前端变更
cd web && pnpm exec tsc --noEmit && npx eslint src/
# 或一次性全量检查
uv run pre-commit run --all-files- 缩进:Python 4 空格,TypeScript 2 空格
- 文件末尾保留一个空行
- 字符串统一用单引号(Python / TypeScript 均适用)
- 提交信息遵循 Conventional Commits:
feat:/fix:/refactor:/docs:/chore: - 不提交
.env、.credentials.json、node_modules、__pycache__等敏感或生成文件 - 注释语言:Python 代码的注释、模块 docstring、行内注释、段落标题统一用中文。工具函数的 docstring(
tools/*.py)保持当前语言以确保 LLM 工具识别准确。TypeScript 注释保持英文。 - 配置项变更:新增或修改环境变量时,必须同步更新
.env.example,并用注释标注:是否必填、作用说明、默认值。格式示例:# [必填] JWT 签名密钥,用于用户认证令牌签发 SECRET_KEY=请替换为随机字符串 # [可选] PostgreSQL 连接地址,默认使用 SQLite # DATABASE_URL=postgresql+asyncpg://user:pass@host:5432/db
- 类型注解必须写,函数参数和返回值都要有
- 使用 Python 3.13+ 内置类型,禁止从
typing导入已内置的泛型:dict不用typing.Dictlist不用typing.Listtuple不用typing.Tupleset不用typing.Settype不用typing.TypeX | Y不用typing.Union[X, Y]X | None不用typing.Optional[X]
- 使用
from __future__ import annotations开启延迟求值(文件顶部) - 类型注解与运行时导入:当类型仅用于注解而不在运行时使用时,使用字符串字面量形式(forward reference)而非
TYPE_CHECKING导入:规则:如果框架在运行时需要解析函数签名(如 ADK 工具注册、FastAPI 依赖注入、Pydantic 模型),类型必须运行时可用——正常导入即可。仅在纯静态分析场景(不被框架反射的内部函数)才考虑 forward reference。# ✅ 正确:字符串字面量(不需要运行时导入) def process(data: "DataFrame") -> "Series": ... # ✅ 正确:运行时确实需要该类型(如框架反射解析签名)则正常导入 from google.adk.tools import ToolContext def my_tool(tool_context: ToolContext) -> str: ... # ❌ 错误:TYPE_CHECKING + 裸注解(ADK 等框架运行时 eval 注解会报 NameError) from typing import TYPE_CHECKING if TYPE_CHECKING: from google.adk.tools import ToolContext def my_tool(tool_context: ToolContext) -> str: ... # 运行时爆炸
- 优先
pathlib.Path而非os.path - 禁止遗留
print()(tools 目录除外,调试用) - import 顺序:stdlib → 第三方 → 本项目(isort 自动处理)
- 导入风格:包内用相对导入,跨包用绝对导入
- 包内:
from .tools import all_tools、from ..client import GiteaClient - 跨包:
from server.config import settings、from model_config import get_model - 同一个包内保持一致,不混用
- 包内:
- 每个 Agent 一个独立 Python 包(目录),放在
root_agent/agents/下 - Agent 名使用
snake_case,与目录名一致,如gitea_agent/→name="gitea_agent" - 工具函数必须有 docstring + Args 注释,这是 LLM 识别工具的唯一依据
tool_context: ToolContext参数不需要在 docstring 里写,ADK 自动注入- Agent 的
instruction用中文写,这是面向最终用户的 - 模型统一通过
get_model("agent_name")获取,不硬编码模型名
所有需要外部凭据的工具必须通过 credential_provider 读取,禁止直接 os.getenv 或读文件。
这是多用户安全隔离的核心。每个用户的凭据(如 Gitea Token)互不可见。
from credential_provider import CredentialKey, CredentialSchema
GITEA_CREDENTIALS = CredentialSchema(
namespace="gitea", # 与 UserConfig 的 namespace 一致
keys={
"base_url": CredentialKey(env_var="GITEA_BASE_URL", required=True),
"token": CredentialKey(env_var="GITEA_TOKEN", secret=True),
},
)from google.adk.tools import ToolContext
from credential_provider import credentials
def my_tool(owner: str, tool_context: ToolContext, page: int = 1) -> dict:
"""工具示例。tool_context 放在必需参数之后、可选参数之前。"""
creds = credentials("my_service", ["base_url", "token"], tool_context)
# 或者用 schema:
creds = MY_CREDENTIALS.resolve(tool_context)tool_context.state["{namespace}_{key}"] ← 用户专属(UserConfig 注入)
↓ 空则
os.environ["{NAMESPACE}_{KEY}"] ← 全局默认(.env)
↓ 空则
CredentialKey.default ← 声明的默认值
tool_context: ToolContext 必须放在 所有必需参数之后、可选参数之前:
# ✅ 正确
def func(owner: str, repo: str, tool_context: ToolContext, page: int = 1) -> dict:
# ❌ 错误:tool_context 在可选参数之后(Python 语法错误)
def func(owner: str, page: int = 1, tool_context: ToolContext) -> dict:# 工具函数示例
def list_repos(owner: str, tool_context: ToolContext, page: int = 1, limit: int = 20) -> dict:
"""列出指定用户或组织拥有的仓库
Args:
owner: 仓库所有者的用户名或组织名,例如 liteyuki
page: 页码,从 1 开始
limit: 每页数量,默认 20,最大 50
"""root_agent/agents/{agent_name}/
├── __init__.py # 空文件或 re-export
├── agent.py # Agent 定义(model, instruction, tools, sub_agents)
├── client.py # 外部 API 客户端封装(如有)
└── tools/
├── __init__.py # 汇总 all_tools 列表
├── feature_a.py # 按功能域拆分,每个文件底部 export all_tools: list
└── feature_b.py
后端采用 Router → Service → Model 三层架构:
server/
├── main.py # FastAPI 应用 + 启动初始化
├── config.py # Settings(从 .env 读取)
├── database.py # SQLAlchemy async 引擎 + session factory
├── deps.py # 依赖注入(get_db, get_current_user, require_admin)
├── models/ # SQLAlchemy ORM 模型(数据库表定义)
├── schemas/ # Pydantic 请求/响应模型(数据校验)
├── routers/ # API 路由(薄层,只做参数校验和调用 service)
└── services/ # 业务逻辑(所有核心逻辑在这里)
规则:
- Router 不写业务逻辑,只做参数解析、权限检查(通过 Depends)、调用 Service
- Service 函数接收
db: AsyncSession作为第一个参数 - Model 使用
from __future__ import annotations,主键统一uuid4字符串 - 新增路由必须写 response_model 和 status_code
- 所有接口
/api/v1前缀
本项目使用 SQLAlchemy create_all() 在启动时自动建表。已存在的表不会被自动修改。
- 新增表:直接加 Model,重启后端即可自动创建
- 修改已有表结构(加字段、改类型等):需要删除
data.db并重启这会丢失所有开发数据(用户、会话等),重启后自动重建表 + 初始超级用户rm data.db && uv run server - 生产环境:后续引入 Alembic 做正式迁移,开发阶段直接重建即可
- 注意:改完 Model 后如果不删库重建,会出现 500 错误(表结构不匹配)
uv run lint # 检查
uv run fix # 自动修复 + 格式化
uv run check # CI 用,不修改文件- 强制使用 pnpm,禁止使用 npm / yarn
- 安装依赖:
pnpm install,添加包:pnpm add <pkg> - 项目已配置
packageManager字段,确保团队版本一致
- 使用
interface而非type(除非需要联合类型或映射类型) - 组件文件使用
PascalCase.tsx,工具函数使用camelCase.ts - React 组件用命名导出
export function Component(),不用export default - Props 类型与组件同文件定义,命名为
{ComponentName}Props - 路径引用一律用
@/别名,不写相对路径
web/src/
├── components/
│ ├── chat/ # 聊天业务组件(ChatArea, MessageBubble, Sidebar, ...)
│ ├── layout/ # 布局组件
│ └── ui/ # shadcn/ui 基础组件(不手动修改)
├── hooks/ # 自定义 hooks(useChat, useAuth)
├── pages/ # 页面级组件
│ ├── LoginPage.tsx # 登录(密码 + OAuth)
│ ├── ChatPage.tsx # 聊天主页
│ ├── SettingsPage.tsx # 个人设置(Token + 用量)
│ └── admin/ # 管理后台
│ ├── AdminLayout.tsx
│ ├── UsersPage.tsx
│ ├── OAuthPage.tsx
│ └── QuotaPage.tsx
├── types/ # 共享类型定义
├── lib/ # 工具函数(api.ts, utils.ts, i18n.ts)
├── locales/ # i18n 翻译文件(zh/en/ja)
├── App.tsx # 路由 + Toaster
├── main.tsx # 入口(AuthProvider + TooltipProvider)
└── index.css # 全局样式 + Tailwind CSS 变量
/login → LoginPage(公开)
/ → ChatPage(需登录)
/settings → SettingsPage(需登录)
/admin/users → UsersPage(需 admin)
/admin/oauth → OAuthPage(需 admin)
/admin/quota → QuotaPage(需 admin)
认证守卫由 ProtectedRoute 组件实现,未登录自动重定向 /login。
- 用 TailwindCSS class,不写自定义 CSS(除
index.css的 CSS 变量) - shadcn/ui 组件目录
components/ui/由 CLI 生成,不要手动修改 - 颜色使用 CSS 变量(
bg-background、text-foreground),不硬编码色值
- 统一使用 lucide-react 图标库:
import { IconName } from 'lucide-react' - 菜单项、按钮等交互元素必须搭配图标,提升可识别性
- 图标尺寸:跟随文本用
size-4(16px),独立按钮用size-5(20px) - 不要混用其他图标库(heroicons、phosphor 等)
- 使用 sonner 库,全局
<Toaster>在App.tsx中挂载 - 操作失败用
toast.error(message),成功用toast.success(message) - 禁止用
useState管理错误提示,统一走 toast - 导入方式:
import { toast } from 'sonner'
- 所有面向用户的文案走 i18n:
const { t } = useTranslation('namespace') - 翻译文件:
src/locales/{zh,en,ja}/{module}.json,按模块拆分一层 - 不在组件里硬编码中文/英文字符串
- 使用
src/lib/api.ts中的封装函数:apiGet,apiPost,apiPatch,apiDelete,streamSSE - 401 响应自动清除 token,由
ProtectedRoute驱动重定向,不在 API 层硬跳转 - SSE 流式响应使用
streamSSE()async generator
cd web
pnpm lint # ESLint 检查
pnpm lint --fix # 自动修复