From d86938c1c3bda87c8f73cb582861d299ad47c157 Mon Sep 17 00:00:00 2001 From: oratis Date: Sun, 9 Aug 2026 23:05:53 +0800 Subject: [PATCH 1/2] =?UTF-8?q?docs:=20=E4=BF=AE=E6=AD=A3=E5=85=A5?= =?UTF-8?q?=E9=97=A8=E6=96=87=E6=A1=A3=E9=87=8C=E5=B7=B2=E7=BB=8F=E5=A4=B1?= =?UTF-8?q?=E6=95=88=E7=9A=84=E8=BF=90=E7=BB=B4=E4=BA=8B=E5=AE=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 排查一份 brand-voice embedding 的 bug 报告时发现问题本身已经在 #20 (`0c5323b`)修掉了,但顺手核对上下文的过程中撞上好几处文档与现状不符 —— 每一处都会让下一个会话走一段冤枉路,所以单独收一个 docs PR。 改了什么: - **prod DB 口令来源**:CLAUDE.md 和 memory.md 有 5 处说密码"在 .env 里", 但这台机器上根本没有 .env(只有 .env.example)。全部改为从 Secret Manager 取:`gcloud secrets versions access latest --secret=DATABASE_URL`。 - **测试数字**:CLAUDE.md 同时写着 377 和 234,memory.md 写 234 —— 实际是 656 个服务端测试 / 69 个文件、~1 秒。前端测试也不再是"4 个组件测试"或 "还没有",是 13 个 vitest 文件 + 5 条 Playwright(3 个 spec)。 - **Sentry / OTEL**:memory.md §5.4 还写着"没有(Sprint 1 待加)",与同文件 §6 已关闭表和 CLAUDE.md 自相矛盾。两者早已接入,只是要配 DSN / OTLP 才上报。 - **brand_voices 生产现状**:借这次机会连 prod 只读查了一次 —— 表 0 行。 也就是说 embedding 写入路径坏了这么久没有造成数据损失,不需要 backfill。 这条结论写进 memory.md §6,省得以后有人再问一遍。 - 新增一条"仍然成立":embedding 只在 `POST /api/brand-voices` 创建时写, 目前没有 update 路由所以无害,但以后加编辑接口必须重新 embed。 验证:纯文档改动,未动任何代码路径;`npm test` 656/656 通过。 Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 15 +++++++++------ docs/memory.md | 16 +++++++++------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3039053..b769ce1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,7 +28,7 @@ InfluenceX (https://influencexes.com) is an **invite-only AI marketing platform* - `server/agents-v2/` — 18 LLM agents (strategy, research, content-text/visual/voice/video, kol-outreach, publisher, ads, community, etc.) - `server/agent-runtime/` — agent registry + Conductor (goal → plan → run) - `server/llm/` — Anthropic + OpenAI + Gemini + 火山方舟 routing layer with cache + cost stats -- `server/__tests__/` — 377 Node test runner unit tests (frontend has 4 Vitest component tests in `client/src/components/*.test.jsx`) +- `server/__tests__/` — 656 Node test runner unit tests across 69 files; frontend has 13 Vitest files under `client/src/{components,pages,utils}/` and 5 Playwright tests across 3 specs in `e2e/tests/` - `client/` — Vite + React 18 SPA (HashRouter); `client/src/pages/*.jsx` is one page per route - `docs/` — see file links above - `deploy.sh` / `migrate-env-to-secret.sh` / `setup-secrets.sh` — Cloud Run + Secret Manager helpers @@ -104,8 +104,10 @@ NOT `influencex.com`. Don't typo this in copy, env vars, OAuth redirect URIs, or preview_start influencex # server on :8080 (SQLite if no DATABASE_URL) preview_start influencex-client # Vite dev on :5173 (HMR) -# Tests (server only — no frontend tests yet) -npm test # 234 unit tests, ~3s +# Tests +npm test # 656 server unit tests, ~1s +cd client && npx vitest run # 13 component/page/util test files +npx playwright test # 5 e2e tests across 3 specs in e2e/tests/ # Build client cd client && npx vite build # outputs client/dist/ @@ -121,7 +123,8 @@ cd client && npx vite build # outputs client/dist/ # Connect to prod Postgres via Cloud SQL Auth Proxy (read-only investigation) cloud-sql-proxy --port 5434 gameclaw-492005:us-central1:influencex-db & -# password is in .env DATABASE_URL (postgres user) +# There is NO local .env — pull the postgres password from Secret Manager: +gcloud secrets versions access latest --secret=DATABASE_URL --project=gameclaw-492005 node -e "const {Client}=require('pg');..." # one-off queries ``` @@ -186,7 +189,7 @@ Each prod deploy increments the Cloud Run revision number (`influencex-NNNNN-xxx ## Debugging Production - **Cloud Run logs:** `gcloud run services logs read influencex --region=us-central1 --limit=50` -- **Postgres queries:** start `cloud-sql-proxy --port 5434 gameclaw-492005:us-central1:influencex-db` then connect with any pg client. Password is in `.env` DATABASE_URL +- **Postgres queries:** start `cloud-sql-proxy --port 5434 gameclaw-492005:us-central1:influencex-db` then connect with any pg client. There is no local `.env` — get the password from `gcloud secrets versions access latest --secret=DATABASE_URL --project=gameclaw-492005` (delete any copy you write to disk) - **Sentry** — wired server-side (`server/sentry.js`) + client-side since `891f209` (needs `SENTRY_DSN`) - **OpenTelemetry** — wired via `server/otel.js` since `8f00ad1` (needs OTLP endpoint env) @@ -198,8 +201,8 @@ See [docs/memory.md](docs/memory.md) for the full list. Highlights: - **`hakko-q1-all` is a legacy demo campaign**: server seeds it on first boot. Don't use as a fallback in new code — use `defaultCampaignForWorkspace()` instead. - **Pipeline ↔ Contact dual flow**: solved by `pipeline_jobs.contact_id` link + worker reverse-sync. UI shows the same row from both pages. - **Hunter.io fallback** only works for KOLs with a linked website on their channel page. No-website KOLs need a paid Hunter Email-Finder plan + known domain. -- **Frontend has no tests** — Sprint 2 task C1+C2 will add Playwright + Vitest. Until then, manual smoke after every feature. - **`subscriptions` table is dormant** — left from removed Stripe billing. Don't query. +- **`brand_voices` is empty in production** (verified 2026-08-09, 0 rows). Brand-voice similarity was inert until `0c5323b`, but nothing was lost — the first voice created after the next deploy embeds correctly. Note the embedding is written **only on create** (`POST /api/brand-voices`); there is no update route, so any future edit endpoint must re-embed. ## Session Safety diff --git a/docs/memory.md b/docs/memory.md index f1c396c..398b6fb 100644 --- a/docs/memory.md +++ b/docs/memory.md @@ -18,7 +18,7 @@ | **Service** | `influencex` (Cloud Run) | | **Cloud SQL Instance** | `influencex-db` (Postgres 15) | | **Cloud SQL Connection** | `gameclaw-492005:us-central1:influencex-db` | -| **DB user / password** | `postgres` / 见 `.env` `DATABASE_URL`(不在此处明文) | +| **DB user / password** | `postgres` / 见 Secret Manager 的 `DATABASE_URL`(**本地没有 `.env`**,见 §5.5) | | **Database name** | `influencex` | | **Image registry** | `gcr.io/gameclaw-492005/influencex:latest` | | **OAuth callback base** | `https://influencexes.com` | @@ -37,7 +37,8 @@ ```bash cloud-sql-proxy --port 5434 gameclaw-492005:us-central1:influencex-db & # 然后用任何 pg client 连 localhost:5434 -# password 从 .env DATABASE_URL 提取 +# password 从 Secret Manager 取(本地没有 .env): +gcloud secrets versions access latest --secret=DATABASE_URL --project=gameclaw-492005 ``` **注意**:本地常驻一个 cloud-sql-proxy 指向另一个项目(`dimbluedot:us-central1:luddi-pg`,端口 5433)。我们要的是 5434 端口。 @@ -215,7 +216,7 @@ node --test server/__tests__/email-jobs.test.js node --test --test-name-pattern "specific test" server/__tests__/*.test.js ``` -`npm test` 跑全套 234 个,~3 秒。 +`npm test` 跑全套 **656** 个(69 个文件),~1 秒。前端另有 13 个 vitest 文件(`cd client && npx vitest run`)+ 5 条 Playwright / 3 个 spec(`npx playwright test`)。 ### 5.3 客户端 build 检查 @@ -233,7 +234,7 @@ build 失败通常是: gcloud run services logs read influencex --region=us-central1 --limit=50 --project=gameclaw-492005 ``` -**没有 Sentry / OTEL**(Sprint 1 待加)。要查报错只能 grep Cloud Run 日志。 +Sentry(`server/sentry.js`,客户端自 `891f209`)与 OpenTelemetry(`server/otel.js`,自 `8f00ad1`)**都已接入** —— 需要 `SENTRY_DSN` / OTLP endpoint 环境变量才会真正上报。没配的话仍然只能 grep Cloud Run 日志。 ### 5.5 prod DB 一次性查询 @@ -241,7 +242,7 @@ gcloud run services logs read influencex --region=us-central1 --limit=50 --proje cloud-sql-proxy --port 5434 gameclaw-492005:us-central1:influencex-db & node -e " const { Client } = require('pg'); -const c = new Client({ host: 'localhost', port: 5434, user: 'postgres', password: '<在.env里>', database: 'influencex' }); +const c = new Client({ host: 'localhost', port: 5434, user: 'postgres', password: '<见下方 gcloud 命令>', database: 'influencex' }); c.connect().then(async () => { const r = await c.query('SELECT ...'); console.log(r.rows); @@ -266,7 +267,7 @@ kill %1 | 无 frontend 测试 | 13 个文件 / 82 个 vitest + 5 条 Playwright,全部挂 CI(#12/#13) | | 无 Sentry / OTEL | 早已接入;依赖冲突后遗症 #7 收尾 | | In-process job queue 多副本丢消息 | BullMQ API 修复 + 发送原子抢占 + 进程级异常兜底(#10) | -| pgvector 启用但 agent 没用 | 实际是**接了但从未工作**(embed 调用契约错,`findBestBrandVoice` 永远返回 null)→ #20 修复 | +| pgvector 启用但 agent 没用 | 实际是**接了但从未工作**(embed 调用契约错,`findBestBrandVoice` 永远返回 null)→ #20 修复。**2026-08-09 查过 prod:`brand_voices` 表 0 行**,所以坏了这么久也没丢数据,不需要 backfill | | ContactModule 5s 轮询撞 429 | 后台刷新不再置 loading + 请求序号守卫(#9);限流器桶隔离(#15) | | Hunter API 仅对有外链网站的 KOL 有效 | 仍然成立,但已是产品决策(付费 API 预算)而非 bug | @@ -284,6 +285,7 @@ kill %1 | `content_daily_stats` 全局 UNIQUE(content_url, stat_date) | 跨工作区同 URL 同日第二条快照被静默跳过 | fail-closed,彻底解决需改约束 | | Marketplace 无下架/申诉流程 | 撤一条 listing 只能手工 DELETE | provenance 列可定位,需配合创作者 opt-out | | **design.md 与现状脱节** | §10.3 说焦点还原未实现、§12 硬编码 FUNNEL_COLORS、§8.3 modal 契约现已是组件 | #13 之后未同步 | +| **brand voice 只在创建时写 embedding** | 以后加编辑接口会留下过期向量 | `POST /api/brand-voices` 是唯一写点(`server/index.js:3419`),目前只有 GET/POST/DELETE,没有 update 路由所以暂时无害 | | `ContactModule.jsx` 与 `PipelinePage.jsx` UI 重复 | 两个 page 显示相似数据 | 未评估 | ### 这轮学到的、值得记住的失效模式 @@ -323,7 +325,7 @@ type: `feat` / `fix` / `chore` / `docs` / `refactor`。scope: `discovery` / `out - 小改动(i18n / 文案 / a11y) —— 不强求立刻 deploy,下次大改一起 - bug 修复 —— 立即 deploy(push + run `./deploy.sh`) - 安全修复 —— 立即 deploy + 通知用户 -- 大重构 —— 至少跑过 234 个测试 + 客户端 build + 本地 preview 烟测,再 deploy +- 大重构 —— 至少跑过全套 656 个服务端测试 + 客户端 build + 本地 preview 烟测,再 deploy ### 7.4 push 前 checklist From b9dbea5d2f40eba01ed1685cc05030e9ae6379e2 Mon Sep 17 00:00:00 2001 From: oratis Date: Sun, 9 Aug 2026 23:14:39 +0800 Subject: [PATCH 2/2] docs: correct this PR's own test counts, and record the SQLITE_BUSY flake MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-up on this PR. The numbers went stale during review: #23 (+9 tests, +1 file) and #24 (+7, +1) merged while this branch sat open, so "656 / 69 files" was already wrong by the time it could land — the exact failure mode the PR exists to fix. Measured on main at 7df4183: 678 tests across 71 files. Corrected in all five places (CLAUDE.md ×2, memory.md §5.2 / §7.3 / Last-reviewed footer, which also still said "post #20"). Also documents why a clean checkout can show 2-5 red files: `npm test` is `node --test`, which runs files concurrently against the one shared influencex.db at the repo root, so writes collide and report `{ code: 'SQLITE_BUSY' }` on a rotating cast of files. It reproduces on main with no changes applied, and the triage step is a serialized re-run (--test-concurrency=1 → stable 678/678). Without this written down the next session reads the flake as its own regression — which is what the rest of this PR is trying to prevent. Verified: 678/678 serialized on main, and on main + #25 + this branch. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 5 +++-- docs/memory.md | 14 +++++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b769ce1..cc056ad 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,7 +28,7 @@ InfluenceX (https://influencexes.com) is an **invite-only AI marketing platform* - `server/agents-v2/` — 18 LLM agents (strategy, research, content-text/visual/voice/video, kol-outreach, publisher, ads, community, etc.) - `server/agent-runtime/` — agent registry + Conductor (goal → plan → run) - `server/llm/` — Anthropic + OpenAI + Gemini + 火山方舟 routing layer with cache + cost stats -- `server/__tests__/` — 656 Node test runner unit tests across 69 files; frontend has 13 Vitest files under `client/src/{components,pages,utils}/` and 5 Playwright tests across 3 specs in `e2e/tests/` +- `server/__tests__/` — 678 Node test runner unit tests across 71 files; frontend has 13 Vitest files under `client/src/{components,pages,utils}/` and 5 Playwright tests across 3 specs in `e2e/tests/` - `client/` — Vite + React 18 SPA (HashRouter); `client/src/pages/*.jsx` is one page per route - `docs/` — see file links above - `deploy.sh` / `migrate-env-to-secret.sh` / `setup-secrets.sh` — Cloud Run + Secret Manager helpers @@ -105,7 +105,8 @@ preview_start influencex # server on :8080 (SQLite if no DATABASE_URL) preview_start influencex-client # Vite dev on :5173 (HMR) # Tests -npm test # 656 server unit tests, ~1s +npm test # 678 server unit tests, ~1s +npm test -- --test-concurrency=1 # serialized; use when SQLITE_BUSY flakes (see memory.md §5.2) cd client && npx vitest run # 13 component/page/util test files npx playwright test # 5 e2e tests across 3 specs in e2e/tests/ diff --git a/docs/memory.md b/docs/memory.md index 398b6fb..edebea4 100644 --- a/docs/memory.md +++ b/docs/memory.md @@ -216,7 +216,15 @@ node --test server/__tests__/email-jobs.test.js node --test --test-name-pattern "specific test" server/__tests__/*.test.js ``` -`npm test` 跑全套 **656** 个(69 个文件),~1 秒。前端另有 13 个 vitest 文件(`cd client && npx vitest run`)+ 5 条 Playwright / 3 个 spec(`npx playwright test`)。 +`npm test` 跑全套 **678** 个(71 个文件),~1 秒。前端另有 13 个 vitest 文件(`cd client && npx vitest run`)+ 5 条 Playwright / 3 个 spec(`npx playwright test`)。 + +**`SQLITE_BUSY` 偶发失败不是你的改动坏了。** `npm test` 是 `node --test`,默认按文件并发,而整套测试共用仓库根目录的**同一个** `influencex.db`。并发写会随机让 2-5 个文件报 `{ code: 'SQLITE_BUSY' }`,每次挂的文件还不一样。判定方法: + +```bash +node --test --test-concurrency=1 server/__tests__/*.test.js # 串行,~8 秒,稳定 678/678 +``` + +串行绿 = 并发那几个是 flake。串行也红才是真的坏了。(在 main 上空跑也能复现,与任何 PR 无关。) ### 5.3 客户端 build 检查 @@ -325,7 +333,7 @@ type: `feat` / `fix` / `chore` / `docs` / `refactor`。scope: `discovery` / `out - 小改动(i18n / 文案 / a11y) —— 不强求立刻 deploy,下次大改一起 - bug 修复 —— 立即 deploy(push + run `./deploy.sh`) - 安全修复 —— 立即 deploy + 通知用户 -- 大重构 —— 至少跑过全套 656 个服务端测试 + 客户端 build + 本地 preview 烟测,再 deploy +- 大重构 —— 至少跑过全套 678 个服务端测试 + 客户端 build + 本地 preview 烟测,再 deploy ### 7.4 push 前 checklist @@ -365,7 +373,7 @@ type: `feat` / `fix` / `chore` / `docs` / `refactor`。scope: `discovery` / `out --- -**Last reviewed:** 2026-08-09 (post `#20`, main green: server 656 / client 82 / e2e 5). +**Last reviewed:** 2026-08-09 (post `#24`, main green: server 678 / client 82 / e2e 5). Prod revision unchanged since `00049-w2x` — **this batch has not been deployed yet**; see [MASTER_PLAN_2026-08.md](./MASTER_PLAN_2026-08.md) §5 for the pre-deploy checklist (the startup contract changed: MAILBOX_ENCRYPTION_KEY now fail-fast, webhooks fail-closed without secrets).