Skip to content

Latest commit

 

History

History
128 lines (100 loc) · 22.1 KB

File metadata and controls

128 lines (100 loc) · 22.1 KB

AGENTS.md

本文档只记录长期有效、仅查看局部代码容易误判的项目约定与维护陷阱。可直接从代码、依赖清单或 CI 配置得出的实现细节不在此重复;新增内容也应遵循这一原则。

常用命令

# 后端开发服务器
source venv/bin/activate && python3 web.py

# 前端开发服务器(代理后端 8001)
cd frontend && pnpm run dev

# 安装后端开发依赖
venv/bin/python3 -m pip install -r requirements-dev.txt

# 后端验证(unittest,行/分支覆盖率门槛 100%;使用环境变量阻止大量日志污染输出)
CODEBUDDY_LOG_LEVEL=CRITICAL venv/bin/python3 -m coverage run -m unittest discover -s tests
venv/bin/python3 -m coverage report

# 前端完整验证
cd frontend && pnpm run format:check && pnpm run lint && pnpm run build && pnpm run test:coverage

# Docker 部署/本地构建
docker compose up -d
docker build -t codebuddy2api:local .

# 原子新增系统用户或更新密码
venv/bin/python3 scripts/hash_password.py <用户名> --output secrets/users.txt

# 使用发布镜像管理用户
docker run --rm -it -v "$PWD/secrets:/app/secrets" ghcr.io/iceean/codebuddy2api:latest add-user <用户名>

开发规定

  • 测试驱动开发:开发流程须完全遵循 TDD,保证单元测试100%覆盖、且尽可能覆盖真实用例。
  • 后端测试:使用标准库 unittestcoverage.pyvenv/bin/python3 -m coverage reportconfig.pyrelease_runtime_lock.pyweb.pysrc/ 生产代码强制执行行/分支综合 100% 覆盖率门槛。
  • 前端测试:Vitest 使用 jsdom 与 Vue Test Utils;pnpm run test:coveragesrc/ 生产代码强制执行 statements、branches、functions、lines 四项 100% 覆盖率门槛。
  • 前端修改后流程:前端修改后依次执行格式检查、lint、构建和覆盖率测试;格式检查失败时先执行 pnpm run formatpnpm run build 已包含类型检查和生产构建。
  • 保证需求的正确性:若我需要你实现的需求存在不明确的部分,请直接提问;若工作过程中出现重要的选择,停下来说明并等待回复。尽可能地不要自行推测意图和需求。
  • 快速失败而不是兜底:为保证质量、尽早发现错误,项目内各种非预期的错误应该快速失败,少对错误数据进行防御性的兜底;对外部接口行为进行适当兜底,增强兼容性。
  • 干净的修改与重构:进行 breaking change 后,无需对修改前的旧表、旧字段、旧接口等进行兼容。可以认为它们在前、后端均不再使用。
  • bug修复使用最小修改:对于bug修复,尽量保证最小修改,同时应保证遵循上述其余原则。
  • 持续更新本文档:当开发或排错过程中出现重要或常见的的通用性问题未在此文件说明的情况,随时更新此文档。可以包括项目信息、常用命令、踩坑记录等。不要写入一次性排错过程或可从单处代码直接读出的细节。

架构与边界

  • web.py 是 FastAPI/Uvicorn 入口,config.py 管理启动级配置;环境变量优先于硬编码默认值,安全边界配置不得由用户数据库设置覆盖。可选 .env 只从应用根目录加载,不得向父目录搜索。
  • CODEBUDDY_DATA_DIR 的相对路径以 config.py 所在的应用根目录为基准。SQLite 和凭证路径必须由解析后的绝对数据目录派生,不能依赖进程工作目录。
  • 管理台设置、API Key、凭证、模型缓存和统计都按系统用户隔离。新增管理端缓存时,隔离维度必须包含用户名;涉及凭证的数据还必须使用稳定的 credential_id,不得依赖列表下标。
  • 管理 API 位于 /api/admin/*。外部协议入口遵循 /<协议>/v1/*,仅接受 API Key;管理台测试入口遵循 /api/admin/playground/<协议>/v1/*,仅接受会话 Cookie。两者复用协议处理逻辑,但不能互相接受对方的认证方式。
  • /docs/redoc/openapi.json 只接受管理台会话 Cookie,API Key 不能替代;管理台 playground 路由不得暴露在 OpenAPI schema 中。

HTTP、认证与浏览器安全

  • Web UI 使用 HttpOnly 滑动会话 Cookie;每个系统用户最多保留 10 个会话,超限时按创建时间淘汰最旧会话。外部 API 使用 sk-... Bearer Token;API Key 必须是 sk- 加 40 字节规范无填充 Base64URL,明文只在创建时返回,SQLite 仅保存带唯一索引的 SHA-256 摘要。
  • API Key 鉴权只计算一次 SHA-256 并按摘要索引查询。last_used_at 保存现实分钟起点的 Unix 时间戳,同一分钟最多实际更新一次,管理台仅显示到分钟。
  • Cookie 鉴权成功后的续期必须在最终 ASGI 响应层完成,以覆盖错误、重定向和流式响应。私有路由使用 PrivateNoStoreRouteCache-Control: private, no-storeServerErrorMiddleware 外层的最终响应中间件统一覆盖,端点中注入的临时 Response 不能保证最终响应头。
  • 所有响应(包括 404 和未处理的 500)都由最外层 ASGI 中间件统一覆盖 CSP、X-Frame-OptionsX-Content-Type-OptionsReferrer-Policy,其中 X-Frame-Options 固定为 DENY。frame-ancestors 可由环境变量 CODEBUDDY_CSP_FRAME_ANCESTORS 配置,默认 none,仅允许由 self 和无路径的 HTTP(S) Origin 组成的列表;来源主机必须先规范化为 IDNA ASCII,再按合法 DNS 标签或 IPv6 字面量校验。CSP 来源必须在启动时严格校验,不能允许通配符、指令逃逸或不可编码的响应头字符;文档页所需的外部资源白名单与管理台同源策略分开维护。
  • 请求体限制是“应用实际处理字节数”限制:先检查 Content-Length,再累计下游实际读取的流;不要为了验证无声明长度且端点本来不读取的请求体而主动消费整个流。登录请求使用更小的独立上限。
  • CODEBUDDY_MAX_CONCURRENT_REQUESTS=N 表示用户可用容量 N;传给当前固定版本 Uvicorn 时必须经 to_uvicorn_limit_concurrency() 转成 N+1,以补偿 Uvicorn 判断时已计入当前连接的语义。
  • 前端只把同时带 WWW-Authenticate: Bearer 的 401 视为本系统会话失效;上游 CodeBuddy 401 不得触发管理台登出。

数据与文件安全

  • SQLite schema 创建/迁移与 user_version 必须在同一 BEGIN IMMEDIATE 事务中提交;每次连接都要确保 WAL 可用,失败即终止。
  • 数据目录不得是符号链接;数据库及其 -wal-shm-journal sidecar 必须是非符号链接普通文件。数据库和凭证文件权限为 0600。
  • 凭证写入使用 O_NOFOLLOW,加载时忽略凭证目录中的符号链接,所有文件名都必须防路径穿越。新凭证文件名使用 NFC 规范化并允许 Unicode 字母数字,但不得以 . 开头,否则 glob("*.json") 无法发现。
  • CODEBUDDY_ALLOWED_API_ENDPOINTS 是硬白名单:为空、含非法 URL 或当前端点不在其中时必须在启动阶段失败,禁止自动补入或回退。X-Domain 只允许 [A-Za-z0-9.-]+
  • 用户认证必须对不存在或无效用户执行虚拟密码哈希,避免时序枚举。密码文件只接受规范的 pbkdf2_sha256$迭代数$盐$摘要:默认迭代数为 600000,只允许 600000 至 1000000;盐固定 16 字节,摘要固定 32 字节,Base64URL 必须规范且无填充。修改格式时必须同步所有读写入口。

CodeBuddy 与 OpenAI 兼容层

  • CodeBuddy 上游只支持流式响应。即使客户端请求非流式,也必须用 client.stream() 增量消费 SSE,再由 StreamResponseAggregator 聚合;禁止先缓冲完整上游响应体。RequestProcessor.prepare_request() 必须强制注入 stream=True
  • 上游响应采用宽松事件提取,不在事件模型层承担完整协议验证。流式与非流式路径共享首个 choice 语义;OpenAIStreamNormalizer 负责拆分混合的 reasoning/content delta,并在首块补 role: assistant
  • 上游工具调用 ID 原样透传,只为 OpenAI 流式兼容补充缺失的 index,不要重新生成 ID。
  • 强制推理模型会覆盖为最大推理并启用 thinking,但 clear_thinking 等其他客户端 thinking 子项必须继续透传,不能用新对象整体替换;其他模型默认开启 thinking,但客户端显式禁用时必须尊重。CODEBUDDY_FORCED_TEMPERATURE 非空时覆盖客户端值;模型命名空间是否剥离由 CODEBUDDY_STRIP_MODEL_NAMESPACE 控制。修改请求转换时注意这些优先级。
  • 全局上游 HTTP 客户端保持 trust_env=False,避免环境中的 SOCKS 代理在缺少 socksio 时破坏服务启动。

OAuth、凭证与模型缓存

  • src/codebuddy_auth_router.py 只负责路由;上游认证、auth state 所属关系及 token 解析/保存放在 src/codebuddy_oauth.py
  • OAuth state 一旦取消或消费便不可轮询或重放。上游登录 URL 只允许带主机、无 userinfo/控制字符的绝对 HTTP(S) URL;校验通过前不得登记 state 或让前端导航。成功响应不能向浏览器返回 token 或用户信息;保存失败应返回 500,且不得恢复已消费 state。
  • 手动添加和 OAuth 保存必须共用 token 解析:user_id 优先取 JWT sub;失败或缺失时使用真实 CLI 兼容的 anonymous_<token 后 8 位>,不得读取 ACC_USER_IDACC_USER_NICKNAME
  • 手动添加的 bearer-only 凭证不要求 account_uid、账号列表、过期时间或 refresh token,且不得进入 OAuth 刷新或账号切换流程;请求头中的 X-User-Id 对 OAuth 凭证优先使用 account_uid,否则继续使用 user_id
  • X-Department-Info 必须按 CodeBuddy CLI 语义进行 UTF-8 百分号编码,不能把中文部门名直接交给仅接受 ASCII 请求头的 HTTP 客户端。切换到个人账号时必须显式清除顶层及用户信息中的企业上下文,禁止回退切换前的企业 ID。
  • OAuth 登录由后端返回 intervalexpires_in 并由前端原样遵守;敏感的分阶段登录进度只能保存在服务端。凭证文件除规范字段外还需保存各阶段完整上游 JSON 响应体,用于后续兼容,但管理 API 不得返回这些原始响应。
  • OAuth 自动刷新只由启动任务和每小时任务触发,在 expires_at - 86400 秒进入刷新窗口;聊天、模型发现等请求路径不得触发、等待或重试刷新。
  • 刷新端点一旦返回轮换后的 refresh token,必须先按凭证代次原子持久化,再同步账号列表;账号同步失败或服务关闭时保留可恢复的 pending 状态。pending access token 仍有效或过期时间未知时,后续扫描只重试账号阶段;过期时间未知的 token 即使账号接口返回 401/403 也不得刷新。仅当过期时间已知,且 token 已过期或账号接口明确返回 401/403 时,才可使用 pending 状态中已持久化且未过期的当前 refresh token 重新刷新,不能用旧快照再次调用刷新端点。关闭刷新管理器必须取消主扫描和实际在途任务,不能只设置 stop event 后无限等待。
  • 每个系统用户拥有独立凭证目录和 Token 管理器。凭证轮换开关是用户级设置,轮换频率必须为正整数。
  • CodeBuddy /v3/config URL、HostX-Domain 必须由同一个当前 API endpoint 派生。
  • 凭证额度探测按 enterprise_id 分流:个人版使用 /v2/billing/meter/get-user-resource,额度以本周期的 CycleCapacity*Precise 为准、缺失时仅回退对应 CycleCapacity*,不能使用可能保持套餐初始值的 Capacity*;企业版使用 /v2/billing/meter/get-enterprise-user-usage,其中 credit 是已用额度、limitNum 是总额度。个人版响应中的 Accounts: nullAccounts: [] 均表示探测成功但没有可展示的个人版额度,前端显示“未探测到个人版额度”;缺少 Accounts 字段或其余异常结构仍需失败。两类额度只用于探测与展示,不参与凭证轮换或统计 billing 语义。
  • 模型缓存键至少包含系统用户与 credential_id。凭证过期、删除或失效时必须同时驱逐缓存并作废在途查询;旧请求结果不能写回,也不能被同 ID 的新凭证复用。凭证删除的文件操作、内存重载与代次推进必须原子,删除失败不得推进普通代次或额度代次;账号切换仅在规范 account_id 确实变化时推进额度代次。过期值不得作为失败回退,并发未命中使用 per-key single-flight 合并。
  • 每日签到按系统用户、当前 API endpoint 与请求实际使用的 X-User-Id 隔离,同一上游账号的多张凭证共享最近记录与并发锁。自动签到是默认关闭的用户级热加载设置;扫描任务和排队中的自动任务在发起上游请求前均需检查开关,但关闭开关不得取消已发起的请求。凭证刷新和启动签到补偿都必须在后台执行;启动签到补偿可等待首次 OAuth 凭证刷新扫描完成,但不得阻塞应用 lifespan 进入就绪状态。只有上游签到响应 code=0data.credit 是非布尔有限数值时,才作废并重新探测同一上游账号关联凭证的额度缓存;额度探测批次必须作为账号级共享完成屏障,手动签到先释放账号并发门再等待屏障,排在成功自动签到后的手动请求也须等待该批探测结束才返回已签到冲突,自动签到本身则不等待。记录时间存 UTC,但“今日”、09:30 调度、次日可签到时间和过期记录清理均按服务器本地自然日计算;code=null 的未成功异常不阻止当天后续启动补偿。

统计系统

  • 统计按系统用户隔离,区分外部 API、管理台 playground 和凭证测试。写入必须从 ASGI 事件循环卸载;写入失败不能影响聊天响应,但要记录日志并增加进程级 dropped_events。运行期数据库丢失必须快速失败,不能返回伪造的空统计。
  • 统计总览的 totals、趋势和排行应用全部筛选条件;候选维度使用自排除分面语义,每个维度忽略自身筛选、保留其他条件,并在同一 SQLite 读事务中生成。
  • 逐请求脱敏明细只保留 90 天,UTC 小时汇总永久保留;清理由启动任务和独立定时任务按索引分批执行,不能依赖后续写入触发。
  • 查询在同步 FastAPI 路由线程池中执行,并在 SQL 中有界聚合。浏览器 IANA 时区的非整点范围和本地日历边界,在保留期内用明细修正边界小时;过期历史只能按小时近似,必须通过 boundary_precision 明示。
  • 请求列表使用成对的 snapshot_idsnapshot_time 做稳定的页码分页;详情必须沿用列表快照。拒绝未来快照;清理水位越过快照可用下界后要求重新获取第一页。
  • 成功请求的永久模型维度只采用配置或上游响应确认的规范模型;失败或取消请求的明细可记录通过安全格式校验且长度不超过 64 的请求模型,超过 64 时明细记为 unknown。统计桶类型与真实模型名必须分开存储:已知模型进入 known 桶,合法但未确认的模型进入 other 桶,缺失或非法模型进入 unknown 桶;对外筛选键中已知模型统一使用 model:<真实模型名>,避免真实模型名 otherunknown 与特殊桶碰撞。历史 unknown 迁移时必须原样保留,不得推测来源。缺失 usage 保持 null,不能当作 0;覆盖率以 total_tokens 是否已知计算。延迟超过直方图上限时进入显式 overflow 桶。
  • 永远不要保存提示词、回答、请求头、Bearer/CodeBuddy Token、工具参数、原始错误体或会话 ID。模型、错误类型、思考模式和结束原因写入前必须归一化到受控值。统计不承担 billing 套餐、余额或货币换算。

前端约定

  • RouterView 的页面组件由 Transition mode="out-in" 承载,必须只有一个根节点;页面级弹窗即使内部使用 Teleport,也必须放在该根节点内,否则进出场完成钩子会丢失,导致路由白屏与主题按钮持续禁用。
  • 管理数据的 Vue Query key 必须以 ['admin', username, ...] 开头。登出、本地会话 401 或用户名变化时,同时清空 Query Cache 和 Mutation Cache。
  • 查询和 mutation 使用 networkMode="always",查询禁用 refetchOnReconnect,保证离线时立即失败且联网后不补发。手动刷新和重试统一使用 RefreshButton;它需在 refetch 前检查离线状态,并独立维持最短加载反馈,不能只依赖 isFetching
  • 前端请求超时必须覆盖后端串行上游调用的总上限并预留处理时间;调用方 AbortSignal 和业务总截止时间仍可提前取消请求。
  • 可编辑文本控件使用 Enter 提交、确认或选择时,必须在非自动重复的 keydown 阶段锁存 KeyboardEvent.isComposing 判定,并在该按键可能触发的 input 更新完成后再执行(通常在对应 keyup);不得直接用可能已重置的 keyup.isComposing 判定,也不得在 keydown 同步读取尚未更新的模型。共享组件与直接消费键盘事件的页面处理器都应覆盖这些约束。
  • 可聚焦元素不要使用 Tailwind transition-colors;它会一并过渡 outline-color,使暗色模式的键盘焦点轮廓从浏览器默认浅色短暂闪烁。应使用 transition-[color]transition-[color,background-color] 等明确的过渡属性。
  • 主题动画只在根节点维护一个数值进度,所有动画语义色由该进度派生。不要为后代递归添加颜色 transition,也不要用 dark: 在两个动画语义变量间切换。需单调变化的颜色使用等效不透明端点,避免透明色插值泛白;连续主题切换必须从当前进度反向,路由切换期间禁止启动主题切换。
  • 根视口防滚动条抖动不能只依赖 scrollbar-gutter: stable,Chromium 在当前 body overflow 传播结构下仍可能等到内容溢出才占用宽度;桌面传统滚动条由 body { overflow-y: scroll; } 固定槽位。Modal/Drawer 锁滚动时只能补偿隐藏滚动条前后 clientWidth 的实际增量,不能再按 innerWidth - clientWidth 无条件补偿,否则支持稳定 gutter 的浏览器会产生双重留白;overlay 滚动条环境不应额外补偿。补偿不能只加在普通流的 body 上,可在锁定期间持续显示的固定定位全局宿主也必须消费同一实际增量。
  • 内容哈希的静态资源长期 immutable,入口 HTML 为 no-store,未哈希资源必须重新验证。缺少 frontend/dist/index.html 时快速失败,不提供单文件回退。
  • 前端开发/构建要求 Node.js 24.11+。Vite 8/Rolldown 手动分包使用 rollupOptions.output.codeSplitting.groups,不要恢复对象形式 manualChunks;TypeScript 配置保留 vite/client 类型。

Docker 与发布

  • 容器入口必须先以 root 准备挂载目录和用户文件副本,再通过 gosu 切换到 UID 1001 的 appuser。不要用 Compose userdocker run --user 绕过入口准备。
  • 运行时挂载 ./data 和只读 ./secrets;入口固定数据目录为 /app/data,并将宿主 users.txt 复制成运行时私有只读文件。服务启动后修改用户文件必须重启容器才会生效。
  • 用户文件以同目录临时文件原子替换,不支持符号链接、非普通文件或多硬链接;重复用户名会替换全部旧记录,并发写入不提供锁。
  • 发布只接受稳定语义版本 tag。tag、web.pyAPP_VERSIONfrontend/package.json 版本及 CHANGELOG.md 对应版本必须一致。
  • 发布镜像必须同时支持 linux/amd64linux/arm64linux/arm/v7,构建时生成 SBOM/provenance,并使用 Cosign keyless signing 对最终镜像 digest 签名。发布顺序必须保持“完整验证 → 多架构按 digest 推送 → 每个架构漏洞扫描 → 对 digest 加版本/latest tag → 签名与 GitHub Release”;任一架构存在 CRITICAL 漏洞都应阻断发布。Trivy 默认包含尚无修复版本的漏洞,手动发布仅可通过 ignore_unfixed=true 忽略这类漏洞。只有最高稳定版本更新 latest
  • 发布归档必须可复现:使用 tag commit 时间,规范成员顺序、时间、权限和 owner 元数据;只收录生产文件,拒绝输入路径中的符号链接及其他非普通文件。输出目录不能位于任何输入目录内,所有产物先在临时目录完整生成再原子替换,checksum 最后发布。
  • 本地 Release 更新器必须从自身所在的 scripts 目录解析项目根目录,不能依赖调用时工作目录;只能在非 Git 的 Release 安装目录中由项目外系统 Python 执行。Release 服务与更新/回滚共用项目根目录的 .codebuddy2api-runtime.lock 独占锁,Git 开发环境和 Docker 不启用该锁;锁文件不得进入完整备份,也不得在部署或恢复时删除、替换。服务启动遇到不完整或无效 Release 标记时必须失败;更新/回滚的必需锁路径在此状态下仅可复用既存锁文件,仍须成功获取同一 OS 排他锁,禁止创建新锁绕过安装识别。--yes 只能跳过交互确认,不能绕过锁。Release 清单必须与归档成员完全一致,本地包仅接受以 codebuddy2api 开头的 .zip.tar.gz;清单和归档成员路径除 POSIX 越界形式外,还必须拒绝任意分段中的 Windows 盘符语义。完整备份固定为项目根目录下的 .update-backups/latest,正常完成更新或回滚后只能保留这一份完整备份,且备份时必须排除 .update-backups 自身。回滚提交后的残留备份清理失败不能反转事务结果或报告回滚失败;必须报告回滚已经成功、列出残留路径并提醒用户不要重试回滚。更新默认重建 venv--reuse-venv 只接受 pyvenv.cfg 中唯一且明确设置 include-system-site-packages = false 的环境,必须确保 pip 至少为 23.0 并验证安装报告版本为稳定的 1,再用全新解析报告确定新版完整依赖闭包,只保留闭包和 pipsetuptoolswheel,清理其余包并通过 pip check,任何失败都必须触发完整快照恢复。