Skip to content

Latest commit

 

History

History
473 lines (363 loc) · 16.8 KB

File metadata and controls

473 lines (363 loc) · 16.8 KB

开发与贡献指南

本文档面向开发者和 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          # 编排

开发命令

Python 后端

命令 说明
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    # 自动修复

Local Agent

cd local_agent
bun install        # 安装依赖
bun run build      # 构建
npx tsc --noEmit   # 类型检查

模型配置

支持 Gemini、DeepSeek、OpenAI、Ollama 等 LLM,通过环境变量配置。

优先级:{AGENT_NAME}_MODELAGENT_MODELgemini-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)

所有面向用户的文本必须走 i18n,禁止硬编码。

  • 前端:useTranslation('namespace') + locales/{zh,en,ja}/module.json
  • Local Agent:src/i18n/ 目录,t.xxx.yyy 访问

编码规范

开发环境设置

安装依赖

# 后端
uv sync

# 前端
cd web && pnpm install

Pre-commit Hooks

项目使用 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)

项目使用 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,无需手动操作。

⚠️ 迁移文件必须同时兼容 SQLite(开发)和 PostgreSQL(生产)。 自动生成的迁移不一定能直接用,需要手动检查并调整:

  • 新增 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

任何代码变更后,必须对变更部分执行 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 Commitsfeat: / fix: / refactor: / docs: / chore:
  • 不提交 .env.credentials.jsonnode_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

风格

  • 类型注解必须写,函数参数和返回值都要有
  • 使用 Python 3.13+ 内置类型,禁止从 typing 导入已内置的泛型
    • dict 不用 typing.Dict
    • list 不用 typing.List
    • tuple 不用 typing.Tuple
    • set 不用 typing.Set
    • type 不用 typing.Type
    • X | Y 不用 typing.Union[X, Y]
    • X | None 不用 typing.Optional[X]
  • 使用 from __future__ import annotations 开启延迟求值(文件顶部)
  • 类型注解与运行时导入:当类型仅用于注解而不在运行时使用时,使用字符串字面量形式(forward reference)而非 TYPE_CHECKING 导入:
    # ✅ 正确:字符串字面量(不需要运行时导入)
    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: ...  # 运行时爆炸
    规则:如果框架在运行时需要解析函数签名(如 ADK 工具注册、FastAPI 依赖注入、Pydantic 模型),类型必须运行时可用——正常导入即可。仅在纯静态分析场景(不被框架反射的内部函数)才考虑 forward reference。
  • 优先 pathlib.Path 而非 os.path
  • 禁止遗留 print()(tools 目录除外,调试用)
  • import 顺序:stdlib → 第三方 → 本项目(isort 自动处理)
  • 导入风格:包内用相对导入,跨包用绝对导入
    • 包内:from .tools import all_toolsfrom ..client import GiteaClient
    • 跨包:from server.config import settingsfrom model_config import get_model
    • 同一个包内保持一致,不混用

ADK Agent 规范

  • 每个 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)

所有需要外部凭据的工具必须通过 credential_provider 读取,禁止直接 os.getenv 或读文件。

这是多用户安全隔离的核心。每个用户的凭据(如 Gitea Token)互不可见。

1. 声明凭据 schema(在 client.py 中)

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),
    },
)

2. 在工具函数中使用(通过 tool_context)

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)

3. 凭据解析优先级

tool_context.state["{namespace}_{key}"]   ← 用户专属(UserConfig 注入)
         ↓ 空则
os.environ["{NAMESPACE}_{KEY}"]           ← 全局默认(.env)
         ↓ 空则
CredentialKey.default                      ← 声明的默认值

4. 工具函数签名规则

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

Agent 目录结构

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

后端架构(server/)

后端采用 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 用,不修改文件

TypeScript (web/)

包管理

  • 强制使用 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-backgroundtext-foreground),不硬编码色值

图标

  • 统一使用 lucide-react 图标库:import { IconName } from 'lucide-react'
  • 菜单项、按钮等交互元素必须搭配图标,提升可识别性
  • 图标尺寸:跟随文本用 size-4(16px),独立按钮用 size-5(20px)
  • 不要混用其他图标库(heroicons、phosphor 等)

Toast 通知

  • 使用 sonner 库,全局 <Toaster>App.tsx 中挂载
  • 操作失败用 toast.error(message),成功用 toast.success(message)
  • 禁止用 useState 管理错误提示,统一走 toast
  • 导入方式:import { toast } from 'sonner'

i18n

  • 所有面向用户的文案走 i18n:const { t } = useTranslation('namespace')
  • 翻译文件:src/locales/{zh,en,ja}/{module}.json,按模块拆分一层
  • 不在组件里硬编码中文/英文字符串

API 调用

  • 使用 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 # 自动修复