From 0739bcc4fb881c008ec1dbf7b015a0de6f3c41eb Mon Sep 17 00:00:00 2001 From: Jayway Liu <14022498+bluejayway@user.noreply.gitee.com> Date: Sat, 26 Sep 2026 07:32:27 +0800 Subject: [PATCH 1/8] =?UTF-8?q?feat(dev):=20scripts/dev=5Fup.sh=20?= =?UTF-8?q?=E4=B8=80=E9=94=AE=E5=90=AF=E5=8A=A8=E6=9C=AC=E5=9C=B0=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=E4=B8=8E=E9=A1=B5=E9=9D=A2=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 迁移 → 管理员初始化 → 前端构建 → API+worker 后台启动 → 健康检查 → 自动打开页面 - 支持 --stop 停止、--no-open 跳过打开浏览器;端口已在服务时直接复用 - 未设 DATABASE_URL 时使用 SQLite 轻量库并给出验收提示 - 修复 bash 3.2(macOS 自带)将紧贴 $VAR 的多字节字符并入变量名的问题: 全角括号内的变量一律用 ${VAR} 显式界定 - README 本地运行章节补充快速启动说明 --- README.md | 8 +++ scripts/dev_up.sh | 124 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100755 scripts/dev_up.sh diff --git a/README.md b/README.md index 7975490..6ad5c36 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,14 @@ 需要 Node.js 22、uv、Python 3.12、PostgreSQL 17。所有示例从仓库根目录执行。 +快速启动本地页面(迁移、管理员初始化、前端构建、API 与导入 worker 一条命令完成,健康检查通过后自动打开 `http://127.0.0.1:8000`;`Ctrl-C` 或 `scripts/dev_up.sh --stop` 停止,`--no-open` 跳过自动打开浏览器): + +```bash +scripts/dev_up.sh +``` + +未设置 `DATABASE_URL` 时该脚本使用 SQLite 轻量开发库 `var/db.sqlite3`,仅限页面测试与开发回归;金额聚合与财务验收需按下方配置 PostgreSQL。手动分步流程如下: + ```bash uv sync --frozen --python 3.12 npm ci --registry=https://registry.npmjs.org diff --git a/scripts/dev_up.sh b/scripts/dev_up.sh new file mode 100755 index 0000000..f8d763d --- /dev/null +++ b/scripts/dev_up.sh @@ -0,0 +1,124 @@ +#!/usr/bin/env bash +# 一键启动本地开发环境:迁移 → 初始化管理员 → 构建前端 → 启动 API+worker → 打开页面。 +# 默认使用 SQLite 轻量开发库(var/db.sqlite3);设置 DATABASE_URL 则使用 PostgreSQL。 +# Ctrl-C 或 --stop 停止本脚本启动的进程。 +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +PORT="${PORT:-8000}" +SERVER_PID_FILE="var/dev-server.pid" +WORKER_PID_FILE="var/dev-worker.pid" +SERVER_LOG="var/dev-server.log" +WORKER_LOG="var/dev-worker.log" + +die() { echo "错误:$*" >&2; exit 1; } + +detect_python() { + if [ -x ".venv/bin/python" ]; then + PY=".venv/bin/python" + elif command -v uv >/dev/null 2>&1; then + PY="uv run python" + else + die "未找到 .venv/bin/python 或 uv,请先安装依赖(uv sync)" + fi +} + +read_pidfile() { [ -f "$1" ] && cat "$1" || true; } + +stop_all() { + local stopped=0 pid + for file in "$SERVER_PID_FILE" "$WORKER_PID_FILE"; do + pid="$(read_pidfile "$file")" + if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then + kill "$pid" 2>/dev/null || true + stopped=1 + fi + rm -f "$file" + done + [ "$stopped" = 1 ] && echo "已停止本地开发服务" || echo "没有正在运行的开发服务" +} + +health_ok() { curl -fsS --max-time 2 "http://127.0.0.1:$PORT/api/health" >/dev/null 2>&1; } + +open_page() { + local url="http://127.0.0.1:$PORT/" + if command -v open >/dev/null 2>&1; then open "$url" + elif command -v xdg-open >/dev/null 2>&1; then xdg-open "$url" >/dev/null 2>&1 || true + fi +} + +case "${1:-}" in + --stop) + stop_all + exit 0 + ;; + "" | --no-open) ;; + *) die "未知参数:$1(支持 --stop / --no-open)" ;; +esac + +detect_python +mkdir -p var + +if health_ok; then + echo "已在 $PORT 端口运行,直接打开页面" + [ "${1:-}" != "--no-open" ] && open_page + exit 0 +fi + +if [ -z "${DATABASE_URL:-}" ]; then + echo "未设置 DATABASE_URL,使用 SQLite 轻量开发库 var/db.sqlite3(仅限页面测试与开发回归)" +fi + +echo "执行数据库迁移…" +$PY manage.py migrate --no-input >/dev/null + +if [ ! -f var/local-access.txt ]; then + echo "初始化本地管理员…" + PYTHONPATH="$ROOT" $PY scripts/init_local.py +fi + +if [ ! -d frontend/dist ]; then + echo "构建前端(首次)…" + npm run build >/dev/null +fi + +echo "启动 worker(日志 ${WORKER_LOG})…" +nohup $PY manage.py import_worker >"$WORKER_LOG" 2>&1 & +echo $! >"$WORKER_PID_FILE" + +echo "启动 API 服务…" +nohup $PY manage.py runserver "127.0.0.1:$PORT" --noreload >"$SERVER_LOG" 2>&1 & +echo $! >"$SERVER_PID_FILE" + +cleanup() { + stop_all >/dev/null 2>&1 || true +} +trap cleanup INT TERM + +for _ in $(seq 1 30); do + if health_ok; then + break + fi + if [ -n "$(read_pidfile "$SERVER_PID_FILE")" ] && ! kill -0 "$(read_pidfile "$SERVER_PID_FILE")" 2>/dev/null; then + tail -20 "$SERVER_LOG" >&2 + die "API 服务启动失败(完整日志:${SERVER_LOG})" + fi + sleep 1 +done +health_ok || { tail -20 "$SERVER_LOG" >&2; die "健康检查超时(日志:${SERVER_LOG})"; } + +echo +echo "本地开发环境已就绪:http://127.0.0.1:$PORT" +[ -f var/local-access.txt ] && echo "登录凭据:var/local-access.txt(用户名 admin,权限 0600)" +if [ -z "${DATABASE_URL:-}" ]; then + echo "提示:当前为 SQLite 轻量模式;高精度金额聚合与生产验收请按 README 配置 PostgreSQL。" +fi +echo "日志:${SERVER_LOG} / ${WORKER_LOG} · Ctrl-C 或 scripts/dev_up.sh --stop 停止" +echo + +[ "${1:-}" != "--no-open" ] && open_page + +wait "$(read_pidfile "$SERVER_PID_FILE")" || true +cleanup From a3d91ad17f8879dcc32f0ed42e5b2d829b6de282 Mon Sep 17 00:00:00 2001 From: Jayway Liu <14022498+bluejayway@user.noreply.gitee.com> Date: Sat, 26 Sep 2026 15:22:08 +0800 Subject: [PATCH 2/8] =?UTF-8?q?ci:=20=E6=8E=A5=E5=85=A5=20vertai-tech/ai-r?= =?UTF-8?q?eview-engine=20=E5=AF=B9=E6=8A=97=E5=BC=8F=20AI=20PR=20?= =?UTF-8?q?=E5=AE=A1=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .gitea/workflows/ai-review.yml 原样拷贝引擎模板(含 pr_number 经 env 传参的 注入防护),引擎钉 v1 移动标签,通过 REVIEW_ENGINE_TOKEN 只读 PAT 拉取 - .gitea/ai-review.md 按本仓库技术栈写审查侧重:金额精度(Decimal/NUMERIC)、 缺失值语义与对账容差、文件路径符号链接安全、endpoint 角色与 worker 撤权核验、 事务并发与 revision、Excel 公式注入与长度上限、前端异步状态、测试真实性 - 仓库级 Actions 配置(Gitea 设置,不在本提交内):REVIEW_ENGINE_TOKEN secret、 REVIEW_BASE_URL/MODEL/PUBLIC_URL/PRICES、FIX_MODEL/FIX_MODEL2/FIX_PLAN_MODEL/ FIX_PLAN_THINKING,以及新仓库推荐起点 REVIEW_FIX_SUGGEST_ONLY=1(只给改法) --- .gitea/ai-review.md | 27 +++++ .gitea/workflows/ai-review.yml | 202 +++++++++++++++++++++++++++++++++ 2 files changed, 229 insertions(+) create mode 100644 .gitea/ai-review.md create mode 100644 .gitea/workflows/ai-review.yml diff --git a/.gitea/ai-review.md b/.gitea/ai-review.md new file mode 100644 index 0000000..9d9f674 --- /dev/null +++ b/.gitea/ai-review.md @@ -0,0 +1,27 @@ +# fa-agent (TraceIQ 财务分析) review profile + +## Stack and verification + +- Django 5.2 后端(PostgreSQL 生产 / SQLite 仅开发),金额一律 `NUMERIC(44,24)` + Python `Decimal`(精度 60);JSON 中金额为字符串。 +- React 19 + TypeScript + Vite 前端(AG Grid、ECharts),`npm run build` 含 `tsc --noEmit`。 +- openpyxl 流式生成公式工作簿(SUMIFS/COUNTIFS),公式正确性靠 LibreOffice 无头重算验证。 +- pytest(`--cov-fail-under=85`)+ Playwright E2E;CI 在 PostgreSQL 17 服务容器上跑全量。 +- 验证命令:`uv run pytest`、`uv run ruff check server finance tests scripts`、`npm run typecheck`、`python manage.py makemigrations --check`。 + +## Review priorities + +1. **金额精度**:财务计算、持久化、Excel 公式生成路径上出现 `float()`、`round()`、Decimal 与 float 混算、或金额以数字(非字符串)进 JSON,必须报。浏览器 `Number` 只允许用于绘图与显示。 +2. **缺失值语义**:成本/毛利/人数等依赖字段缺失时必须传播为不可用(服务端 `None`、Excel `#N/A`),不得静默转 0 或跳过;比率分母为零/非正同样不可用。对账容差按指标类型独立(人数严格相等、金额 0.01、比率 0.0001),不得为消除报错统一放宽容差。 +3. **文件与路径安全**:清理/下载/回收文件前必须 `lstat` 校验且拒绝符号链接(目录用 `O_NOFOLLOW` 打开),只删除本次任务在受控目录内创建的文件;清理失败要留日志与可追踪状态,不得静默吞掉 `OSError`。 +4. **越权与生命周期**:每个 endpoint 必须声明 roles 白名单;worker 执行阶段要重新核验角色、`is_active`、组织与科室范围(不能只信创建时的检查);卡死任务要有超时恢复,且恢复不得与活跃 worker 抢任务。 +5. **事务与并发**:配额/唯一性检查必须在事务内并锁定稳定的所属行(如 Membership),审计与业务变更同事务提交;乐观并发用 revision + 409,不得静默后写覆盖。 +6. **Excel 公式注入与上限**:不可信文本写入单元格必须强制文本类型;CSV/公式中 `= + - @` 开头加 `'` 前缀;SUMIFS/COUNTIFS criteria 必须转义 `~ * ? "` 并检查 255 字符上限、公式 8192 字符上限。 +7. **前端异步状态**:useEffect 依赖未变时不得清空已加载数据(同状态重复点击不能停在“加载中”);并发请求要有过期响应丢弃(active flag/AbortController);失败路径要有可见错误与重试。 +8. **测试真实性**:行为修复必须带“修复前失败、修复后通过”的回归测试;环境门禁(LibreOffice、PostgreSQL 并发、真实 AI)必须显式 skip 并注明原因,不得用 mock 通过冒充真实验收。 + +## Avoid reporting + +- 纯格式与风格问题(ruff / prettier 已在 CI 把关)。 +- 改动行为已被等价集成测试或 E2E 覆盖时,仅因缺少同名单测而报缺测试。 +- 没有具体并发路径的假想竞态(SQLite 单写者语义下的理论交错不算,除非改动显式依赖 PostgreSQL 行为)。 +- 对 `docs/` 报告类 HTML/Markdown 的措辞与结构建议——它们记录的是验收状态,不是产品代码。 diff --git a/.gitea/workflows/ai-review.yml b/.gitea/workflows/ai-review.yml new file mode 100644 index 0000000..641c494 --- /dev/null +++ b/.gitea/workflows/ai-review.yml @@ -0,0 +1,202 @@ +name: AI Review + +# 对抗式 PR 审查:先一遍找出候选缺陷,再对每一条派一个反驳者去证伪它,只有 +# 活下来的才会发到 PR 上。这个不对称是刻意的——AI 审查员死于误报远早于死于漏 +# 报,所以反驳阶段默认站在"这是误报"一边,"什么都没发现"是可接受的结果。 +# +# 引擎不在本仓库,在 vertai-tech/ai-review-engine。本文件只负责把三样东西摆到 +# 位再调它:被审查的代码、本仓库的审查侧重、引擎本身。 +# +# 接入一个仓库需要(各一次): +# 1. 引擎仓库的 templates/ai-review.yml -> 本仓库 .gitea/workflows/ai-review.yml +# 2. 引擎仓库的 templates/ai-review.md -> 本仓库 .gitea/ai-review.md,按本仓库 +# 的技术栈改写 +# 3. 下面这些组织级机密 / 变量(组织设置 → Actions)配好之后,新仓库就不用再 +# 单独配任何东西: +# secret REVIEW_API_KEY 模型网关的 key +# secret REVIEW_ENGINE_TOKEN 只读 PAT,用于拉私有的引擎仓库 +# variable REVIEW_BASE_URL 任意 OpenAI 格式的根地址 +# variable REVIEW_MODEL 支持 function calling 的模型 +# +# 自动修复默认是关的。要开,在**本仓库**的 设置 → Actions → 变量 里加一个(这两条 +# 都不要配到组织级,那样等于给所有仓库都开了),并在组织级配一次 +# REVIEW_FIX_MODEL: +# REVIEW_AUTOFIX=1 改代码、提交、推回 PR 分支 +# REVIEW_FIX_SUGGEST_ONLY=1 只把改法写进评论,一个字节都不落盘 +# 两个都配的时候,不动仓库的那个赢。fork 来的 PR 推不回去,apply 会自动降级成 +# 只给改法。 +# +# 默认只修一轮:修完推一个 commit,那次推送起的下一轮只审不改。要"一直修到没 +# 意见",把仓库变量 REVIEW_FIX_ROUNDS 调大——它数的是这个分支已经被自动修过几 +# 轮,轮次写在提交消息里。 +# +# 改法本身是两步定出来的:一个模型决定这条该不该由机器改、以及改完长什么样,另一 +# 个模型把它落成逐字命中的替换。想让这两步用不同的模型,就再配一个组织级的 +# REVIEW_FIX_PLAN_MODEL——判断那步值得用贵的,抄写那步不值得。不配也照样分两步, +# 只是两步都用 REVIEW_FIX_MODEL。 +# +# 成本由模型决定而不是由这个设计决定:同一个 PR 在便宜模型上是几分钱,在旗舰 +# 模型上是几美元。换模型前先看一眼那家的每 token 价格。 +# 换了模型记得同步组织变量 REVIEW_PRICES,缺单价的那轮评论不报金额。 +# +# 只发 COMMENT 类型的 review,从不 approve,也从不 request changes。所有发现集中 +# 在一条评论里按 1、2、3 编号,不再逐条钉成行内评论。 + +on: + pull_request: + # 没有 ready_for_review:Gitea Actions 不实现这个活动类型(它支持的是 + # opened / edited / closed / reopened / assigned / unassigned / + # synchronize / labeled / unlabeled / milestoned / demilestoned / + # review_requested / review_request_removed)。写上去只会让人以为有这个 + # 覆盖,实际没有。 + # + # 留下的缺口:草稿 PR 会被下面的 draft == false 挡掉,所以"取消草稿但不再 + # 推代码"不会触发任何东西。再推一次,或者用下面的 workflow_dispatch 手动 + # 指定 PR 号——那条路就是为这个场景留的。 + # + # 用 edited 能覆盖取消草稿,但它同时会在每次改标题、改描述时触发,而每一 + # 次都是一整轮 find + refute 的钱。为一个"再推一次就解决"的场景不值。 + types: [opened, synchronize, reopened] + workflow_dispatch: + inputs: + pr_number: + description: "PR number to review" + required: true + type: string + +# 连着推两次的 PR 只该被审一次——取消在飞的那轮,而不是花钱买一份关于旧代码 +# 的报告。 +concurrency: + group: ai-review-${{ github.event.pull_request.number || inputs.pr_number }} + cancel-in-progress: true + +jobs: + review: + # 草稿按定义就是没写完的东西;手动 dispatch 是明确的请求,所以绕过这条。 + # vars 那两个条件意味着本文件在网关配好之前合入也是安全的——job 直接跳过。 + if: >- + (github.event_name == 'workflow_dispatch' || github.event.pull_request.draft == false) + && vars.REVIEW_BASE_URL != '' + && vars.REVIEW_MODEL != '' + runs-on: ubuntu-latest + # 不设的话是 Gitea 默认的 6 小时,而下面的 continue-on-error 帮不上忙——它 + # 兜的是失败,不是挂起——于是一次卡在不返回的 API 调用上的审查会一直占着 + # runner,PR 的 check 一下午都是 pending。fantap-server 真踩过这个。 + # + # 60 分钟是照实测定的:一次阶段一实测 710 秒、29k 输出 token(其中 28.7k + # 是推理),阶段二还要在这之上再加最多八条反驳,开了自动修复的话阶段三再加 + # 最多八条的规划加执行。阶段三从每条一次调用变成每条两次(执行没对上文件还 + # 会重试)之后,原来的 45 分钟只在八条全中时才不够,但不够的表现是卡在超时 + # 上而不是干净报错,所以往上留一档。上面 concurrency 的 cancel-in-progress + # 负责拦住堆积。 + timeout-minutes: 60 + permissions: + # write 只有 REVIEW_AUTOFIX 那个模式才用得上——它要把改动推回 PR 分支。 + # 没开自动修复、或者只开了 REVIEW_FIX_SUGGEST_ONLY 的仓库,把这一行改回 + # read 就是了:只给改法的模式一个字节都不落盘,不需要写权限。留着 write + # 也没事,引擎只在 apply 模式下才提交任何东西。 + contents: write + pull-requests: write + steps: + # 一、被审查的代码。反驳阶段要读 PR head 上的整份文件,所以检出 head, + # 不是 merge commit。 + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha || format('refs/pull/{0}/head', inputs.pr_number) }} + + # 二、本仓库的审查侧重(.gitea/ai-review.md),取默认分支的那一份,不取 + # PR 分支的——否则一个 PR 可以先改掉审查重点,再让改过的重点来审自己。 + # || 'main' 是给 Gitea 兜底:它的 workflow_dispatch 载荷不总是带 + # github.event.repository。 + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.repository.default_branch || 'main' }} + sparse-checkout: .gitea + path: .review-profile + + # 三、引擎。跟着 v1 这个移动标签走:引擎修了 bug 移一次标签,所有接入的 + # 仓库同时生效。某个仓库被新版引擎坑到时,把这里单独钉到具体 commit 就能 + # 脱身,不用回滚全局。 + # + # 引擎仓库是私有的,Gitea 的 GITHUB_TOKEN 只对本仓库有效,拉不动它,所以 + # 要一个组织级的只读 PAT。continue-on-error 在这里是必要的:PAT 没配或者 + # 过期时,这一步失败不该把别人的 PR 弄红——下面那步会说明情况然后干净退出。 + - uses: actions/checkout@v4 + continue-on-error: true + with: + repository: vertai-tech/ai-review-engine + ref: v1 + path: .review-engine + token: ${{ secrets.REVIEW_ENGINE_TOKEN }} + + - name: Adversarial review + # 设计上就是建议性的:引擎自己的错误已经会干净退出,这里兜的是剩下的 + # (进程被杀、脚本没看见的 curl 失败)。审查机器人不该让人的 PR 变红。 + continue-on-error: true + # 比 job 的预算小一档,这样超时是记在这一步上,而不是整个 job 被砍—— + # 后者在界面上看不出是谁超的时。引擎内部 curl 的 max-time 比这再小一档 + # (1200s),所以正常顺序是那一层带着响应体和日志先报错,而不是这一层 + # 无声地杀掉进程。 + timeout-minutes: 55 + env: + REVIEW_API_KEY: ${{ secrets.REVIEW_API_KEY }} + REVIEW_BASE_URL: ${{ vars.REVIEW_BASE_URL }} + REVIEW_MODEL: ${{ vars.REVIEW_MODEL }} + # 可选,组织级变量。网关不在响应里报金额时(sub2api 就不报),评论按这张 + # 单价表算金额;不配就只报 token。格式见引擎 README「每轮花了多少」。 + REVIEW_PRICES: ${{ vars.REVIEW_PRICES }} + # 上游报过载 / 限流时每次重试前等几秒。不配就是 10,30,60,写 0 关掉。 + REVIEW_BUSY_RETRY_WAITS: ${{ vars.REVIEW_BUSY_RETRY_WAITS }} + # 评论里行号链接用的站点地址,如 https://git.sciark.cn。runner 在容器里访问 + # Gitea 时,API 返回的 html_url 和 GITHUB_SERVER_URL 都可能是容器内地址, + # 不配这个链接就打不开。组织级配一次。 + REVIEW_PUBLIC_URL: ${{ vars.REVIEW_PUBLIC_URL }} + # PR 号和仓库名经 env 进脚本,不直接拼进 run:手动 dispatch 的 pr_number + # 是任意字符串,表达式在 shell 解析之前就被展开,带引号就能注入命令。 + PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }} + REVIEW_REPO: ${{ github.repository }} + # 引擎的 Gitea REST helper 读这个。GITHUB_API_URL 由 act_runner 导出, + # 已经指向 /api/v1。 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # 自动修复。REVIEW_AUTOFIX 没配或不是 1 的话,下面几个都没有意义。 + REVIEW_AUTOFIX: ${{ vars.REVIEW_AUTOFIX }} + REVIEW_FIX_SUGGEST_ONLY: ${{ vars.REVIEW_FIX_SUGGEST_ONLY }} + REVIEW_FIX_MODEL: ${{ vars.REVIEW_FIX_MODEL }} + # 一个分支最多自动修复几轮。不配就是 1:修一次,下一轮只审,跟从前 + # 一样。配大了才是"一直修到没意见",代价是往作者分支上推更多 commit、 + # 一个 PR 烧更多轮的钱。硬上限在引擎里。 + REVIEW_FIX_ROUNDS: ${{ vars.REVIEW_FIX_ROUNDS }} + # 规划用哪个模型。这不是开关——不配也照样先规划再执行,只是两步同一个 + # 模型。配了才是"贵的判断、便宜的抄写"那种分工。 + REVIEW_FIX_PLAN_MODEL: ${{ vars.REVIEW_FIX_PLAN_MODEL }} + # 只有当写修复的模型不在审查那个网关上时,才需要这两个。 + REVIEW_FIX_BASE_URL: ${{ vars.REVIEW_FIX_BASE_URL }} + REVIEW_FIX_API_KEY: ${{ secrets.REVIEW_FIX_API_KEY }} + # 同上,只有当规划的模型连修复那个网关都不在时才需要。 + REVIEW_FIX_PLAN_BASE_URL: ${{ vars.REVIEW_FIX_PLAN_BASE_URL }} + REVIEW_FIX_PLAN_API_KEY: ${{ secrets.REVIEW_FIX_PLAN_API_KEY }} + # 规划的思考强度。不配就是模型默认;写 high 之类的词按 reasoning_effort + # 发,写 JSON 对象就原样并进请求。各家写法见引擎 README。 + REVIEW_FIX_PLAN_THINKING: ${{ vars.REVIEW_FIX_PLAN_THINKING }} + # 对照用的第二个执行模型。只在直接改的模式下跑,改动不落盘,只和 + # REVIEW_FIX_MODEL 的改动一起贴进评论,方便比较哪个模型抄得好。 + REVIEW_FIX_MODEL2: ${{ vars.REVIEW_FIX_MODEL2 }} + REVIEW_FIX_MODEL2_BASE_URL: ${{ vars.REVIEW_FIX_MODEL2_BASE_URL }} + REVIEW_FIX_MODEL2_API_KEY: ${{ secrets.REVIEW_FIX_MODEL2_API_KEY }} + # 只有当上面那个自动 token 推不动分支时,才需要这个写权限 PAT。 + REVIEW_FIX_TOKEN: ${{ secrets.REVIEW_FIX_TOKEN }} + run: | + ENGINE=.review-engine/scripts/ai_review.py + if [ ! -f "$ENGINE" ]; then + # 拉引擎那步失败了。说清楚原因,否则这里的沉默会被读成"没发现问题"。 + echo "::warning::引擎没拉下来(多半是组织级 secret REVIEW_ENGINE_TOKEN 没配或已过期),本轮未审查。" + exit 0 + fi + + case "$PR_NUMBER" in + ''|*[!0-9]*) + echo "::warning::PR 号必须是数字,收到的是:$PR_NUMBER。本轮未审查。" + exit 0 ;; + esac + python3 "$ENGINE" --repo "$REVIEW_REPO" --pr "$PR_NUMBER" + From 35789379aba037a57a39795fed6c3c270de0797a Mon Sep 17 00:00:00 2001 From: Jayway Liu <14022498+bluejayway@user.noreply.gitee.com> Date: Sat, 26 Sep 2026 16:06:16 +0800 Subject: [PATCH 3/8] =?UTF-8?q?fix(ci):=20Gitea=20runner=20=E4=B8=8B=20Pos?= =?UTF-8?q?tgreSQL=20=E6=9C=8D=E5=8A=A1=E5=AE=B9=E5=99=A8=E4=B8=BB?= =?UTF-8?q?=E6=9C=BA=E5=90=8D=E7=94=A8=E6=9C=8D=E5=8A=A1=E5=90=8D=E8=80=8C?= =?UTF-8?q?=E9=9D=9E=20localhost?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit act_runner 的 job 运行在容器内,与服务容器同处 gitea_gitea 网络, localhost:5432 在 job 容器里无监听导致此前 8 次 verify 全部失败; 改用服务名 postgres 解析,并注释说明防止改回 --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 14f3cb7..fe972d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,9 @@ jobs: --health-cmd pg_isready --health-interval 5s --health-timeout 5s --health-retries 10 env: - DATABASE_URL: postgresql://postgres:ci-only-password@localhost:5432/traceiq + # Gitea act_runner 的 job 跑在容器里,与服务容器同网络:主机名必须用服务名 + # postgres(GitHub 托管 runner 的 localhost 语义在这里连不通,勿改回)。 + DATABASE_URL: postgresql://postgres:ci-only-password@postgres:5432/traceiq E2E_PASSWORD: CI-Test-Only-SecurePassword882! PYTHONPATH: . steps: From e337d9c9895bc2b43ddcc6b0e923dbf470dbf127 Mon Sep 17 00:00:00 2001 From: Jayway Liu <14022498+bluejayway@user.noreply.gitee.com> Date: Sat, 26 Sep 2026 16:08:56 +0800 Subject: [PATCH 4/8] =?UTF-8?q?ci:=20=E9=87=8D=E6=96=B0=E8=A7=A6=E5=8F=91?= =?UTF-8?q?=20verify=EF=BC=88=E4=B8=8A=E6=AC=A1=20push=20=E4=BA=8B?= =?UTF-8?q?=E4=BB=B6=E7=96=91=E4=BC=BC=E4=B8=A2=E5=A4=B1=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From e90e37b77ebb9bea1b0617c762f7d83e833417be Mon Sep 17 00:00:00 2001 From: Jayway Liu <14022498+bluejayway@user.noreply.gitee.com> Date: Sat, 26 Sep 2026 16:12:57 +0800 Subject: [PATCH 5/8] =?UTF-8?q?fix(ci):=20=E5=B7=A5=E4=BD=9C=E6=B5=81?= =?UTF-8?q?=E7=A7=BB=E5=85=A5=20.gitea/workflows=EF=BC=8C=E6=81=A2?= =?UTF-8?q?=E5=A4=8D=20Gitea=20=E8=B0=83=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 .gitea/workflows/ai-review.yml 后,本实例只再调度 .gitea/workflows 下的 工作流,.github/workflows/ci.yml 自 a3d91ad 起不再被检测(workflows API 仅剩 ai-review.yml,两次 push 均未产生 run)。将 ci.yml 移至 .gitea/workflows/ 并 同步 README 引用与说明 --- {.github => .gitea}/workflows/ci.yml | 0 README.md | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename {.github => .gitea}/workflows/ci.yml (100%) diff --git a/.github/workflows/ci.yml b/.gitea/workflows/ci.yml similarity index 100% rename from .github/workflows/ci.yml rename to .gitea/workflows/ci.yml diff --git a/README.md b/README.md index 6ad5c36..25e1afa 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ npm audit --omit=dev --registry=https://registry.npmjs.org uv run pip-audit ``` -Playwright 需要正在运行的本地 API 和 worker,可用 E2E_PASSWORD 指定测试 admin 密码,默认读取本地凭据文件。首次执行 `npx playwright install chromium`。浏览器 fixture 测试会新建 `e2e-finance.csv` 数据集,不可指向正式生产环境。`.github/workflows/ci.yml` 在独立 PostgreSQL 上执行同样流程,尚未推送到远端触发。 +Playwright 需要正在运行的本地 API 和 worker,可用 E2E_PASSWORD 指定测试 admin 密码,默认读取本地凭据文件。首次执行 `npx playwright install chromium`。浏览器 fixture 测试会新建 `e2e-finance.csv` 数据集,不可指向正式生产环境。`.gitea/workflows/ci.yml` 在独立 PostgreSQL 服务容器上执行同样流程(Gitea act_runner 的 job 跑在容器内,服务主机名用服务名 `postgres` 而非 localhost)。`.gitea/workflows/ai-review.yml` 对每个 PR 运行对抗式 AI 审查与自动修复,引擎在 vertai-tech/ai-review-engine(v1 标签),本仓库审查侧重见 `.gitea/ai-review.md`。 原附件只在本机使用,不在仓库中: From f9f9ad751daf62f5728ef5fffd7320031d07a333 Mon Sep 17 00:00:00 2001 From: Jayway Liu <14022498+bluejayway@user.noreply.gitee.com> Date: Sat, 26 Sep 2026 16:19:02 +0800 Subject: [PATCH 6/8] =?UTF-8?q?test(worker):=20=E9=80=82=E9=85=8D=20Postgr?= =?UTF-8?q?eSQL=20=E5=92=A8=E8=AF=A2=E9=94=81=E4=B8=8B=E7=9A=84=E7=AB=8B?= =?UTF-8?q?=E5=8D=B3=E9=87=8D=E8=AE=A4=E9=A2=86=E8=AF=AD=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PG 上 worker 持咨询锁会立即重认领 processing 任务,第二次 --once 被导入任务 抢占导致导出断言失败(该测试此前从未在 PG 上真正运行过)。第一次调用后把导入 任务置为 ready 模拟完成,两种后端行为一致 --- tests/test_worker_paths.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_worker_paths.py b/tests/test_worker_paths.py index 61e1402..1b1bc6d 100644 --- a/tests/test_worker_paths.py +++ b/tests/test_worker_paths.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest from django.core.management import call_command -from finance.models import FormulaExport +from finance.models import Dataset, FormulaExport pytestmark = pytest.mark.django_db @@ -17,6 +17,10 @@ def test_worker_processes_queued_import_and_reports_export_failure(workspace): ) as process_import: call_command("import_worker", once=True) process_import.assert_called_once_with(ds.pk) + # PostgreSQL 上 worker 持咨询锁会立即重认领 processing 任务:模拟导入完成, + # 否则第二次 --once 又被它抢占,轮不到下面的导出任务(SQLite 无咨询锁, + # 不走立即恢复路径,两种后端行为一致)。 + Dataset.objects.filter(pk=ds.pk).update(status="ready") export = FormulaExport.objects.create(dataset=ds, user=ds.created_by, context={}) with patch( From fa047ed3ee9a666dad7a487021f532a2b7a7c3d9 Mon Sep 17 00:00:00 2001 From: Jayway Liu <14022498+bluejayway@user.noreply.gitee.com> Date: Sat, 26 Sep 2026 16:26:12 +0800 Subject: [PATCH 7/8] =?UTF-8?q?fix(ci):=20upload-artifact=20=E5=9C=A8=20Gi?= =?UTF-8?q?tea=20=E4=B8=8A=E4=B8=8D=E6=94=AF=E6=8C=81=EF=BC=8C=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=20continue-on-error?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gitea 对 actions/upload-artifact@v4 报 GHESNotSupportedError(v4 artifact 协议 为 GitHub 专有),此前测试全绿仍被该步骤标红;上传失败不再影响 CI 结论, 浏览器产物在 Gitea 上暂不可下载,迁到 GitHub 托管时该步骤自动生效 --- .gitea/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index fe972d3..971f0f2 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -55,8 +55,11 @@ jobs: trap 'kill "$web" "$worker"' EXIT for i in {1..30}; do curl -fsS http://127.0.0.1:8000/api/health && break; sleep 1; done npm run test:e2e + # Gitea 不支持 upload-artifact@v4 的 artifact 协议(GHESNotSupportedError), + # 上传失败不该把已经全绿的测试标红;迁到 GitHub 托管时此步骤自动生效。 - uses: actions/upload-artifact@v4 if: always() + continue-on-error: true with: name: browser-results path: | From 5e1437679bb2d0f5fecc02ea0a139892cf2a6812 Mon Sep 17 00:00:00 2001 From: Jayway Liu <14022498+bluejayway@user.noreply.gitee.com> Date: Sat, 26 Sep 2026 16:30:39 +0800 Subject: [PATCH 8/8] =?UTF-8?q?fix(ci):=20npm=20audit=20/=20pip-audit=20?= =?UTF-8?q?=E7=BD=91=E7=BB=9C=E6=8A=96=E5=8A=A8=E9=87=8D=E8=AF=95=E4=B8=89?= =?UTF-8?q?=E6=AC=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit runner 到 registry.npmjs.org 的 TLS 连接偶发被断(audit endpoint returned an error),上一轮测试全绿仍被 audit 步骤标红;改为最多重试 3 次、间隔 15 秒。 真实漏洞是确定性的,重试后依然失败,不会漏报 --- .gitea/workflows/ci.yml | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 971f0f2..da06626 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -37,8 +37,24 @@ jobs: - run: uv run python manage.py makemigrations --check --dry-run - run: uv run pytest --cov=finance --cov-report=term-missing --cov-fail-under=85 - run: npm run build - - run: npm audit --omit=dev --registry=https://registry.npmjs.org - - run: uv run pip-audit + # 审计端点在 runner 网络下偶发 TLS 断开(npm error audit endpoint returned + # an error),重试区分网络抖动与真实漏洞:漏洞是确定性的,重试后照样红。 + - name: npm audit (production deps) + run: | + for attempt in 1 2 3; do + npm audit --omit=dev --registry=https://registry.npmjs.org && exit 0 + echo "::warning::npm audit 第 ${attempt} 次失败,15 秒后重试" + sleep 15 + done + exit 1 + - name: pip-audit + run: | + for attempt in 1 2 3; do + uv run pip-audit && exit 0 + echo "::warning::pip-audit 第 ${attempt} 次失败,15 秒后重试" + sleep 15 + done + exit 1 - run: uv run python manage.py migrate --noinput - run: uv run python manage.py collectstatic --noinput - run: uv run python manage.py bootstrap --username admin --organization CI