@docs/design.md — 设计系统单一权威:颜色 / 排版 / 间距 / 圆角 / 组件 / 页面布局 / a11y / 反模式。Token 数值住在 client/src/index.css 的 :root,本文档只讲怎么用。
@docs/memory.md — 项目记忆库:架构决策、历史包袱、生产环境关键事实、常见陷阱。新会话开工前先扫一遍避免重新踩坑。
docs/ROADMAP.md— 长期愿景(8 阶段 A→H)。仅在做战略决策时按需读docs/ROADMAP_2026-Q2.md— 当前 Sprint 切片(6 周计划,4 大支柱)。日常按这份执行docs/STATUS_AND_GAPS.md— 历史状态盘点(2026-04-22 后已冻结,仅供回看)docs/PLATFORM_AUDIT_2026-04.md— 用户视角 UX 审计(修复进度见 ROADMAP_2026-Q2 §1)docs/KOL_FLOW_TEST_2026-04.md— KOL 链路 E2E 测试报告(11/12 已修,剩 Hunter 扩展)docs/MULTITENANCY.md— 多租户架构契约。新增 API 端点必读docs/USER_GUIDE.md— 终端用户操作手册
This file provides context for Claude Code sessions working on this project.
InfluenceX (https://influencexes.com) is an invite-only AI marketing platform for SMBs. Brands invite team members; the platform runs creator discovery, KOL outreach, content generation, scheduled publishing, and ROI tracking — orchestrated by a swarm of LLM agents.
Positioning: "Your content marketing runs itself while you sleep."
Repo: https://github.com/oratis/influencex (MIT, single repo, no monorepo)
server/— Express 5 API, single process, ~30 modulesserver/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 statsserver/__tests__/— 234 Node test runner unit tests (no frontend tests yet)client/— Vite + React 18 SPA (HashRouter);client/src/pages/*.jsxis one page per routedocs/— see file links abovedeploy.sh/migrate-env-to-secret.sh/setup-secrets.sh— Cloud Run + Secret Manager helpers.claude/launch.json—preview_startconfigs for local dev (server: 8080, client: 5173)
Package manager: npm. No workspaces. Server deps and client deps live in two package.json files.
- Express 5 — single API service, serves static client/dist + JSON API
- better-sqlite3 (local) / pg (prod) — dual DB driver, switch via
DATABASE_URL.startsWith('postgresql://') - better-sqlite3 for
npm testand local; Postgres 15 on Cloud SQL for prod (instancegameclaw-492005:us-central1:influencex-db) - No ORM — raw SQL via
query()/queryOne()/exec()/transaction()fromserver/database.js. Thescoped()helper enforces workspace_id in every query - Vite 5 + React 18 + react-router-dom v6 (HashRouter) — frontend; lazy imports for heavy pages (RoiDashboard, DataModule)
- In-process job queue (
server/job-queue.js) — being migrated to BullMQ in Sprint 1 - In-process cache + rate limit — same migration target
- Resend for email send; Gmail OAuth as alt sender per workspace
- i18n: hand-rolled
client/src/i18n.jsxwith EN + ZH dicts,t(key, vars)API
client (HashRouter, Vite SPA)
↓ /api/* (proxied through same origin in prod)
Express
↓ workspaceContext (lenient default fallback)
↓ rate-limit (sliding window in-memory)
↓ route handler
↓ scoped(req.workspace.id).query(...) ← enforces tenant isolation in SQL
↓ JSON response
- All
/api/*routes auto-resolve workspace viaX-Workspace-Idheader →req.user.currentWorkspaceId→ user's first workspace. Skip list inWORKSPACE_SKIP_PREFIXES(auth, webhooks, OAuth callbacks) - Background jobs (
server/job-queue.js+ handlers inserver/email-jobs.js,server/scheduled-publish.js,server/scheduler.js) - Job queue worker registers handlers:
email.send,email.batch_send,email.sync_status, scheduler internal types - Stage transitions (Pipeline):
scrape → write → review → send → monitor— write-stage creates acontactsrow; approve queuesemail.send; worker syncspipeline_jobs.stageback from contact state
POST /api/auth/register returns 410 Gone (code: REGISTRATION_DISABLED). New accounts only via POST /api/invitations/:token/accept. The registerUser() internal function is still used by the admin-bootstrap path and /api/users/invite.
Every /api/* route except those in WORKSPACE_SKIP_PREFIXES (auth, webhooks, OAuth callbacks) auto-attaches req.workspace.id. Always:
- SELECT WHERE
workspace_id = ?(usescoped(req.workspace.id).query(...)for ergonomics) - INSERT with
workspace_idset explicitly - Cross-table joins must include the workspace check on every joined row
This is the floor — see docs/MULTITENANCY.md for contract.
There is no payment flow. server/billing.js was deleted in commit bccfdde. The subscriptions table exists from a prior migration but is not queried. All features are free for invited users. Don't reintroduce payment without an explicit product decision.
runPipeline(jobId, profileUrl, platform, username, campaignId, workspaceId) — the last param is required. Discovery → process used to forget it; was fixed in 7c825d7. New callers must pass it.
Both /api/contacts/:id/send (Contact module) and /api/pipeline/jobs/:id/approve (Pipeline) push into the same email.send job queue. The worker (server/email-jobs.js) is the single source of truth: it updates contacts.status, syncs pipeline_jobs.stage, writes email_replies, records events. Do not bypass it with synchronous mailAgent.sendEmail() calls.
generateOutreachEmail(kol, campaign, cooperationType, priceQuote) is now async. It tries llm.complete() first (Anthropic / OpenAI / Gemini / 火山方舟 — whichever has a key), falls back to template silently. All 5 callers must await — see commit 44324f9. Don't reintroduce the sync template-only path.
Memory: sourcing deploy.sh triggers gcloud builds submit even if you only wanted to inspect a variable. To peek at vars, extract assignments into a separate script or use bash -n deploy.sh for syntax check.
NOT influencex.com. Don't typo this in copy, env vars, OAuth redirect URIs, or DNS.
# Local dev
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
# Build client
cd client && npx vite build # outputs client/dist/
# Deploy to prod (build + push image + Cloud Run deploy)
./deploy.sh # blocking ~5 min; uses .env for Secret Manager refs
# One-time prod env→secret migration (already done; idempotent re-run safe)
./migrate-env-to-secret.sh
# Bootstrap secrets from .env into GCP Secret Manager
./setup-secrets.sh
# 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)
node -e "const {Client}=require('pg');..." # one-off queries| What | Where |
|---|---|
| Express app entry | server/index.js (~5100 lines, monolithic) |
| Database driver dispatch | server/database.js (usePostgres = DATABASE_URL.startsWith('postgresql://')) |
| Migrations | server/migrations.js (forward-only, tracked in schema_migrations table) |
| Auth (session + JWT) | server/auth.js |
| Workspace middleware | server/workspace-middleware.js (lenient global mounted at /api/*) |
| RBAC | server/rbac.js (admin / editor / viewer + permission strings) |
| LLM routing | server/llm/index.js (4 providers, cache, cost stats) |
| Job queue + workers | server/job-queue.js + server/email-jobs.js + server/scheduled-publish.js + server/scheduler.js |
| Email send (mailbox-aware) | server/email.js (signature: { to, subject, body, fromName, mailboxAccount, onCredsRefreshed }) |
| Outreach email gen (LLM) | server/index.js:5305 async function generateOutreachEmail() |
| Pipeline orchestrator | server/index.js:4303 async function runPipeline() |
| Conductor (goal → plan) | server/agent-runtime/conductor.js |
| Agent registry | server/agent-runtime/index.js |
| Agents | server/agents-v2/*.js (one file per agent) |
| Resend webhook | POST /api/webhooks/resend/events |
| Invite acceptance flow | POST /api/invitations, GET /api/invitations/:token, POST /api/invitations/:token/accept |
| Client routes | client/src/App.jsx (<Routes> for both auth'd + public) |
| Pages | client/src/pages/*.jsx (21 pages) |
| Reusable components | client/src/components/*.jsx (ErrorBoundary, ErrorCard, ContactThreadDrawer, TemplateManagerDrawer, etc.) |
| API client | client/src/api/client.js (request wrapper + toastApiError helper) |
| i18n dictionaries | client/src/i18n.jsx (EN + ZH inline) |
| Vite config | client/vite.config.js |
| GCP Cloud Build | deploy.sh (no cloudbuild.yaml; we use gcloud builds submit --tag) |
| Dockerfile | Dockerfile (root, single-stage, runs node server/index.js) |
| Env example | .env.example |
See .env.example for the full template. All sensitive values live in GCP Secret Manager (project gameclaw-492005); the local .env is only used by setup-secrets.sh to bootstrap them.
Critical to set (will fail-fast if missing in prod):
MAILBOX_ENCRYPTION_KEY— 32-byte base64; encryptsmailbox_accounts.credentials_encryptedDATABASE_URL— Postgres connection string with embedded passwordRESEND_API_KEY— primary email senderANTHROPIC_API_KEY— primary LLM (or setOPENAI_API_KEY/GOOGLE_AI_API_KEYas alternative;llm/index.jsauto-picks)
Important non-secret env (set via deploy.sh --update-env-vars):
CORS_ORIGINS— comma-separated;^##^delimiter trick handles embedded commasOAUTH_CALLBACK_BASE— must match Google Cloud Console OAuth redirect URIsLLM_DEFAULT_PROVIDER(anthropic/openai/google) — currentlyanthropic
- Platform: GCP Cloud Run (us-central1), project
gameclaw-492005 - Service:
influencex(single service), domainhttps://influencexes.com/(Cloudflare DNS → Cloud Run) - CI/CD: none yet — deploy is manual via
./deploy.sh. Build + push GCR image +gcloud run deploy - Database: Cloud SQL Postgres 15 (
gameclaw-492005:us-central1:influencex-db). Connection via Cloud SQL Auth Proxy + Unix socket on Cloud Run - Secrets: GCP Secret Manager, 36 secrets bound via
--update-secretsin deploy.sh - Latest revision: check
gcloud run services describe influencex --region=us-central1 --format='value(status.traffic[0].revisionName)'
Each prod deploy increments the Cloud Run revision number (influencex-NNNNN-xxx). No formal CHANGELOG yet — git log is the source of truth. After Sprint 3 we'll start docs/CHANGELOG.md.
- 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-dbthen connect with any pg client. Password is in.envDATABASE_URL - No Sentry yet — Sprint 1 task A1. Until then, ErrorBoundary just
console.errors - No OpenTelemetry yet — Sprint 1 task A2
See docs/memory.md for the full list. Highlights:
- Invitation conflict on already-registered emails:
/api/invitations/:token/acceptreturns 409EMAIL_EXISTS. UI surfaces "log in instead" link. hakko-q1-allis a legacy demo campaign: server seeds it on first boot. Don't use as a fallback in new code — usedefaultCampaignForWorkspace()instead.- Pipeline ↔ Contact dual flow: solved by
pipeline_jobs.contact_idlink + 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.
subscriptionstable is dormant — left from removed Stripe billing. Don't query.
- Never
git push --forceto main without explicit user approval - Never run
./deploy.shto "test something" — it deploys real revisions - Never edit
.envwithout restoring it after;.envcontains real prod secrets gcloud run services update --remove-env-vars=...triggers a new revision; if the revision fails to start, traffic stays on previous revision (safe), but accumulates failed revisions- Don't write to prod Postgres without confirming — use Cloud SQL Auth Proxy + read-only queries first