Skip to content

feat: durable worker, ops dashboard, and cloud single-host deployment - #9

Merged
Akaliup merged 10 commits into
masterfrom
feat/final-agent-validation
Nov 2, 2026
Merged

Akaliup merged 10 commits into
masterfrom
feat/final-agent-validation

Conversation

@Akaliup

@Akaliup Akaliup commented Nov 1, 2026 •

Copy link
Copy Markdown
Owner

概要

本 PR 完成 ProfitOps AI 的最终工程收口:持久化任务队列与独立 Worker、JWT/RBAC 与多租户隔离、可观测性/限流/熔断、Alembic 迁移、原生 Tool Calling 与 RAG 评测,以及云端单机 Compose 部署准备。最新提交 2cfe045 增加独立 Worker 的云端覆盖配置、一次性初始化服务、HTTPS 入口和部署文档,并在隔离环境完成十项 Compose 验收。

所有业务执行保持 simulation,不对真实广告、采购、支付产生影响。本 PR 不自动合并;合并前需要 CI 通过和人工审查。

最新提交(2cfe045):云端单机 Compose

  • compose.cloud.yaml:基于现有 compose.yaml 的覆盖配置。backend 只运行 FastAPI 并关闭内置 Worker/BackgroundTasks;worker 复用后端镜像执行 python -m app.worker;db 使用 PostgreSQL 16 + 持久卷;frontend 由 Nginx 提供 HTTPS。
  • backend/app/init.py:一次性初始化入口,执行 schema 初始化、身份 Seed、演示数据和知识库 Seed。
  • init 服务先执行 alembic upgrade head,再执行 python -m app.init;backend/worker 通过 service_completed_successfully 等待它完成,避免全新数据库上的并发建表/Seed 竞态。
  • frontend/nginx.cloud.conf:只发布 443;/、/api/demo、/api/auth/login 由 HTTP Basic Auth 保护;/api/ 保持 Bearer 认证,避免 Nginx 覆盖 SPA 的 Authorization 头。
  • scripts/cloud_prepare.py:生成不进入 Git 的随机数据库密码、JWT 密钥、analyst/approver token、Basic Auth 口令、htpasswd 和 TLS 材料;拒绝把开发 .env 当作云端配置文件。
  • docs/CLOUD_DEPLOYMENT.md、docs/RUNBOOK.md、README.md:补充 Windows 11 SSH 部署、首次初始化/迁移、启停、备份恢复、排障、访问控制与验收记录。
  • 本轮未新增 API/数据库契约变更;backend/tests、frontend/src/types.ts、docs/CONTRACT.md 保持同步。

本轮实际验证(2026-11-02)

检查 命令 结果
后端全量 .venv/Scripts/python.exe -m pytest backend/tests -q 263 passed, 6 skipped, 1 warning(101.85s);6 项跳过均为未设置 PROFITOPS_TEST_POSTGRES_URL 的 PostgreSQL 专项
前端单元/交互 npm --prefix frontend test -- --run 17 passed(1 file,10.14s)
TypeScript + 生产构建 npm --prefix frontend run build PASS;2252 modules,9.22s
HTTP 冒烟 独立临时 SQLite + 8031 端口:python scripts/smoke.py --base-url http://127.0.0.1:8031 15/15 passed,failed=0
Compose 配置 docker compose -f compose.yaml -f compose.cloud.yaml --env-file .env.cloud.verify config --quiet PASS
首次初始化 alembic -c alembic.ini upgrade head && python -m app.init PASS;应用 0001_baseline、0002_tenant_scoped_idempotency;2 个身份、60 天演示数据、6 篇知识文档;重复执行幂等
云端单机 Compose 十项验收 隔离项目 profitops-cloud-verify,PostgreSQL 16 + backend + worker + frontend 10/10 PASS,详见 docs/CLOUD_DEPLOYMENT.md
云端前端浏览器访问 Playwright Chromium 经 HTTPS + Basic Auth PASS;运维看板标签齐全,consoleErrors=[]、pageErrors=[]
GitHub Actions 提交 2cfe045 4/4 passed:Backend pytest、Backend pytest (Python 3.11)、Frontend tests and production build、Container image builds
真实 DeepSeek 本轮未调用 SKIP;LLM_PROVIDER=disabled、LLM_API_KEY="",付费调用 0
真实云服务器部署 无云服务器凭据 SKIP/BLOCKED;仅完成本地隔离 Compose 验收与部署准备
BGE-M3 真实检索 沿用既有结论 BLOCKED;本机无模型,未用 mock/随机向量冒充
多副本 / 分布式限流 未实现 SKIP;当前只验证单机单 worker

Compose 十项验收摘要

  1. 创建分析任务返回 202,初始状态 queued。
  2. 关闭 API 内 Worker 后,FastAPI 不执行任务;3 秒后仍为 queued,steps=0。
  3. 独立 Worker 消费任务并推进到 waiting_approval,attempts=1。
  4. 重启 API 后 Worker 容器 StartedAt 不变,新任务继续完成。
  5. 停止 Worker 后任务保持 queued。
  6. 恢复 Worker 后任务继续处理。
  7. 人工审批 → 模拟执行 → 同键重放:同一 execution_id、simulation=true、replayed=true;审计包含 recommendation.approve、execution.executed、execution.replayed。
  8. 前端经 HTTPS + Basic Auth 访问运维看板成功。
  9. 重启 PostgreSQL 后 jobs.total=6、executions.total=2 保持不变,分析仍为 executed。
  10. 仅 frontend 发布 127.0.0.1:8443->443/tcp;backend、worker、db 无宿主机端口。

历史修复与评测证据

独立 Worker 与运维看板(b0900bf)

  • python -m app.worker 消费数据库队列;条件 UPDATE 抢占、worker_id/lease_until 心跳、attempts/max_attempts、指数退避、过期租约回收与 dead-letter。
  • API 可关闭内联执行;进程重启不再把队列任务直接标记失败。
  • 运维窗口快速切换增加请求序号保护,旧响应不能覆盖新窗口;by_status 包含全部 10 个分析状态且求和等于 jobs.total。
  • 成本与调用口径按逻辑模型调用、实际 Provider 请求、Provider 内部重试分开统计;native 每轮独立计价,缺失 Usage/价格时返回 partial 且 amount_usd=null。

真实 Agent 与 RAG 评测(历史,本轮未重跑)

  • 真实 DeepSeek 评测共 12 次预算内 HTTP 调用:工具选择 6/6、参数 12/12、证据 2/2、任务完成 1/3、外部副作用 0;失败样本原样保留。
  • Hashing 基线:42 条固定查询,hit@1 97.22%、hit@3 100%、MRR 0.9815、no-answer 100%、citation 100%。
  • BGE-M3 真实检索仍为 BLOCKED,没有可报告的检索指标。

未验证 / 阻塞

  • 没有云服务器凭据,未实际部署公网;docs/CLOUD_DEPLOYMENT.md 中的云端步骤需要在目标主机复验。
  • 本轮未调用真实 DeepSeek;历史评测结果不能替代本轮验证。
  • BGE-M3 真实检索仍 BLOCKED。
  • 多副本 Worker、共享限流/指标、故障切换、OIDC/SSO/MFA、租户管理后台未实现或未验证。
  • 自动审查工具评论仅作辅助;本轮未声称已完成人工审查。

安全边界

  • 未提交 .env、API Key、数据库、缓存或构建产物;云端密钥、htpasswd 和 TLS 材料均由 .gitignore 排除。
  • 云端使用随机 JWT 密钥和随机 demo token;默认开发密钥不作为安全凭据。
  • /api/demo 与登录入口位于 Nginx Basic Auth 之后;demo 身份仍不是正式认证。
  • 所有执行保持 simulation,外部业务调用为 0。

🤖 Generated with Claude Code

lz and others added 6 commits November 2, 2026 02:01
- cap token buckets with stale cleanup and LRU eviction
- only accept demo tokens or signed JWTs as rate-limit identities
- add tenant-scoped GET /api/ops/summary with versioned cost estimates
- document the contract and cover both changes with tests

Co-Authored-By: Claude Code <noreply@anthropic.com>
- add the approver-only Agent ops page with queue, LLM, tool, token and cost panels
- link recent failures to the existing analysis trace
- serialize tests within a spec and retry fixture seed races
- cover role denial, aggregation and trace navigation in Playwright

Co-Authored-By: Claude Code <noreply@anthropic.com>
- require declared expected tools and order for every real case
- fail closed when criteria are missing
- stop spending budget on auth or balance failures
- report versioned cost estimates and blocked status

Co-Authored-By: Claude Code <noreply@anthropic.com>
- add 42 fixed synthetic queries and a hashing/BGE-M3 comparison runner
- reuse production seed and search paths with isolated databases
- bypass machine proxies only for loopback embedding gateways
- fail closed when BGE-M3 is unavailable instead of substituting another model

Co-Authored-By: Claude Code <noreply@anthropic.com>
- add the 12-call DeepSeek evaluation report and sanitized JSON
- add the 42-query hashing baseline and BGE-M3 BLOCKED report
- update evaluation and test reports with actual results and limits
- refresh acceptance screenshots

Co-Authored-By: Claude Code <noreply@anthropic.com>
- cover project background, architecture and three-length introductions
- add 25 likely questions with answers, sources and live demos
- separate verified execution from simulation, design and blocked work
- include ten skeptical questions with honest limits

Co-Authored-By: Claude Code <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-02T04:55:40.732860Z 2cfe045 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e99a016f9e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread backend/app/ops.py Outdated
Comment on lines +130 to +134
usage = run.get("usage")
if not isinstance(usage, dict):
runs_without_usage += 1
unpriced_runs += 1
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude uninvoked models from missing-usage accounting

When an analysis has no anomaly or insufficient data, workflow.verify() persists an LLM record with status="not_requested" and no usage; disabling the provider similarly produces configured=False fallback records without making a model call. This branch treats all of these as unpriced calls, so even one routine no-anomaly analysis changes an otherwise fully priced window to partial and hides the cost on the dashboard. Preserve their status counts, but exclude records known not to have invoked a model from usage and pricing completeness calculations.

Useful? React with 👍 / 👎.

Comment thread backend/app/ops.py Outdated
cost = {
"status": cost_status,
"currency": "USD",
"amount_usd": round(cost_total, 6) if priced_runs else None,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Return null for the amount when pricing is incomplete

When a window contains both a priced run and a run with missing usage or an unknown model, this returns the priced subtotal as amount_usd even though the status is partial. The new /api/ops/summary contract in docs/CONTRACT.md explicitly requires amount_usd=null whenever any call cannot be priced; returning a numeric subtotal in the total field can cause API consumers to report an understated cost. Only populate this field when cost_status == "complete", or expose the known subtotal separately through a synchronized contract change.

AGENTS.md reference: AGENTS.md:L7-L7

Useful? React with 👍 / 👎.

- aggregate job counts in SQL and stream only bounded detail rows
- read LLM history from durable audit events with legacy fallback
- dedupe success and terminal audit events for the same model call
- exclude no-call runs from token/cost denominators
- return partial with amount_usd=null when any call is unpriced
- expose known_subtotal_usd separately and sync frontend contract

Co-Authored-By: Claude Code <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7da915e0c8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread backend/app/ops.py Outdated
Comment on lines +213 to +217
estimate = estimate_cost_usd(run.get("model"), usage)
if estimate is None:
llm_stats["unpriced_runs"] += 1
else:
llm_stats["priced_runs"] += 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check per-round usage before marking a native run fully priced

For a native tool run where one provider response includes usage and another omits it, run_tool_loop() retains only the known tokens in its aggregate usage, while persisting the missing usage in rounds. Pricing that aggregate here marks the entire run as priced, so /api/ops/summary returns cost.status="complete" and a numeric total that excludes the unreported round. Check per-round usage completeness before incrementing priced_runs; incomplete native runs must leave the total unknown, with any known cost reported separately. This preserves the contract's requirement that every actual call have usage before the cost is complete.

AGENTS.md reference: AGENTS.md:L7-L7

Useful? React with 👍 / 👎.

Price each native Function Calling round independently so a missing
Usage in any round makes the run partial instead of reusing another
round's usage. Expose logical model calls separately from actual
Provider requests and internal retries, and update the contract,
frontend types and ops dashboard to match.

Co-Authored-By: Claude Code <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7aff53bd1d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread frontend/src/App.tsx Outdated
Comment on lines +99 to +101
try { setOps(await api.ops(identity.token, opsHours)) }
catch (value) { showError(value) }
finally { setOpsLoading(false) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ignore stale responses when changing the ops window

If the user changes the statistics window while an ops request is pending, both requests can update the same state without checking which request is current. A slower 24-hour response can therefore overwrite a completed 7-day response, leaving the selector on “最近 7 天” while displaying 24-hour counts and costs indefinitely. An earlier request can also clear the loading flag while the current request is still pending. Add a request-sequence guard or cancellation, as the existing refresh callback does, and only let the current request update data, errors, and loading state.

Useful? React with 👍 / 👎.

@Akaliup Akaliup changed the title feat: final agent validation, ops dashboard and interview handoff feat: final agent validation, ops dashboard Nov 1, 2026
- Disable request-scoped BackgroundTasks when the in-process worker is off
- Add python -m app.worker CLI with --once and --poll-seconds
- Include all analysis states in ops by_status so it sums to jobs.total
- Guard ops window changes against stale responses
- Sync tests, frontend types, contract, and runbook

Co-Authored-By: Claude Code <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b0900bff8e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread backend/app/ops.py
Comment on lines +63 to +67
attempts = _number(run.get("attempts")) or 0
rounds = run.get("rounds")
has_rounds = isinstance(rounds, list) and bool(rounds)
if attempts == 0 and run.get("usage") is None and not has_rounds:
return False

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude native circuit-breaker rejections from provider accounting

When the circuit breaker is open, CircuitBreakerProvider._before_call() rejects the request without contacting the provider, but run_tool_loop() still records one attempt and an empty round (backend/app/llm_tools.py:461–467). This predicate therefore classifies native circuit rejections as actual calls, inflating model_calls and provider_requests and adding an unpriced call. Because the breaker is shared across tenants, a tenant with no actual requests can consequently receive cost.status="partial" instead of no_data after another tenant trips it. Preserve the rejection's fallback status, but exclude its synthetic attempt/round from actual-call accounting, consistent with the ops API contract.

AGENTS.md reference: AGENTS.md:L7-L7

Useful? React with 👍 / 👎.

- add cloud overlay with dedicated worker, one-shot init, and HTTPS-only frontend
- add cloud_prepare for non-Git secrets, htpasswd, and TLS material
- document Windows 11 SSH deployment, backup/restore, and troubleshooting
- add ten-point local Compose acceptance report
- remove interview-specific materials and wording
- refresh handoff status for Alembic, worker, and tenant isolation

Co-Authored-By: Claude Code <noreply@anthropic.com>
@Akaliup Akaliup changed the title feat: final agent validation, ops dashboard feat: durable worker, ops dashboard, and cloud single-host deployment Nov 2, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2cfe045038

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/cloud_prepare.py
Comment on lines +164 to +167
_write_private(
auth_dir / "htpasswd",
_htpasswd_line(values["PROFITOPS_ACCESS_USER"], values["PROFITOPS_ACCESS_PASSWORD"]),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Make the generated htpasswd readable by Nginx workers

On a Linux host where the preparation script runs as root or a normal deployment user, _write_private() creates this file with mode 0600 and the host user's ownership. compose.cloud.yaml bind-mounts it unchanged, but the nginx:1.27-alpine frontend serves requests through unprivileged Nginx workers that cannot read it. Consequently, valid Basic Auth requests to /, /api/demo, and /api/auth/login return HTTP 500, preventing access to the cloud demo even while its healthcheck passes. Give the password file ownership/group permissions readable by the container worker, while retaining restrictive permissions for the environment file and TLS private key.

Useful? React with 👍 / 👎.

@Akaliup
Akaliup merged commit 4b591b4 into master Nov 2, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant