Skip to content

feat: competitor intelligence ingest + seedance2 fixes#15

Closed
mtmtian wants to merge 11 commits into
oratis:mainfrom
mtmtian:feat/competitor-intel
Closed

feat: competitor intelligence ingest + seedance2 fixes#15
mtmtian wants to merge 11 commits into
oratis:mainfrom
mtmtian:feat/competitor-intel

Conversation

@mtmtian

@mtmtian mtmtian commented Jul 10, 2026

Copy link
Copy Markdown

竞品情报接入的第一层:把 AppGrowing 竞品素材落进 adex,并修复 seedance2 客户端对真实 API 的解析。与排队中的 #4#7 完全独立(不互为祖先、改动文件零交集),可任意顺序合并。

范围

  • 数据模型:新增 CompetitorCreative(含 segmentPlan/level 列 + (orgId,level) 索引)、RemixJob,均为新增无破坏;迁移与 schema 同 commit。
  • POST /api/ingest/competitor:HMAC 幂等入库(克隆 ingest/scenes 模式),媒体经服务端 uploadFromUrl 拉取入 GCS。
  • GET /api/competitors:org 作用域列表/筛选(session 鉴权)。
  • seedance2 修复:按真实 doubao-seedance-2-0-260128 响应形状(content 为对象、duration 在顶层)重写 resolveVideoUrl + 新增 resolveDuration
  • 设计文档 docs/growth/07/08/09

经过 xhigh code review 并修复的问题

  • seedance2 核心解析 bug:旧 resolveVideoUrlcontent 当数组 .find(),对真实响应抛 TypeError,Asset 永远卡在 generating——已按实测响应重写,对对象/数组/legacy 三形状防御。
  • SSRF:ingest 曾用服务账号 fetch 任意 mediaUrl;新增 scheme/内网主机拦截 + 100MB 上限 + 批量 50 上限。
  • fractional duration 存 Int 列 → 500(付费任务已启动);competitors 列表吞错返 200;level 由死 JSON 筛选改真列;710.4K/1,942 排序信号被判 null;等。

验证(真实 Postgres 16)

  • lint 0 错 / tsc 0 错 / build 成功、两路由注册
  • 迁移干净落库
  • e2e:competitor 3/3 + smoke 14/14 通过

🤖 Generated with Claude Code

mtmtian added 11 commits July 10, 2026 17:47
Adds the competitor-intel data model per docs/growth/07 §4 (+ 09 §3's
segmentPlan addition): CompetitorCreative caches AppGrowing ad metadata
and AI analysis, idempotent on (orgId, source, externalId); RemixJob is
a queryable ledger for turning competitor creatives into our-own-IP
assets (not wired to an engine yet — Phase 2). Purely additive, both
relate to Organization like the rest of the schema.

Migration generated via `prisma migrate diff --from-schema <base> --to-schema
prisma/schema.prisma --script` (no local Postgres available in this
worktree — see .claude/rules/schema.md).
POST /api/ingest/competitor?org=<orgId> — clones the ingest/scenes
HMAC + idempotent-upsert pattern for competitor ad creatives pushed by
the local creative-pipeline/AppGrowing bridge. Accepts a single item or
an array; mediaUrl already on our GCS bucket is linked directly, an
external mediaUrl is fetched once and uploaded to GCS. Per-item
created/updated/failed result. Connector parsing/mapping logic lives in
src/lib/platforms/appgrowing.ts so the ingest source is isolated from
the route, per docs/growth/07 §5.

GET /api/competitors — session-scoped listing (requireAuthWithOrg,
never trusts a client orgId), filterable by appName/level, orderable
by adDays/impressions.

Ref: docs/growth/09-pipeline-adex-integration.md §3
Real-world doubao-seedance-2-0-260128 task responses return the
generated video URL at content[].video_url.url, not output.video_url —
add resolveVideoUrl() that prefers content, falls back to output for
older/other model shapes. Also round the requested duration to whole
seconds before submitting to Ark; a fractional duration gets a 400
InvalidParameter.

Found via first real Seedance2 calls in the local creative-pipeline
tooling (commit e06e215/1224f1c).

Ref: docs/growth/09-pipeline-adex-integration.md §3
Register→login→GET /api/orgs→POST /api/ingest/competitor round trip:
HMAC-signed ingest creates then idempotently updates a
CompetitorCreative row, a bad signature 401s, GET /api/competitors
requires a session and reflects the upsert. playwright.config.ts's
webServer.env gains INVITE_CODES_DISABLED + INGEST_WEBHOOK_SECRET so
the spec can self-provision a user/org and sign requests without a
PlatformAuth row.

Verified the code path against a build with no DATABASE_URL (fails at
the expected `DATABASE_URL is not set` boundary, confirming
registration/ingest wiring is correct) — no live Postgres was
available in this worktree to run the full assertions end-to-end.
Adds the design docs this PR implements: 07 (competitor-intel + remix
proposal, incl. the CompetitorCreative/RemixJob model proposal), 08
(creative pipeline stages), 09 (this PR's scope — pipeline↔adex
integration, PR #1 boundaries).
WHY: product decision — the whole pipeline lands on adex, not local
tooling. Control plane stays in the Next.js service; minute-scale
audio/video batch work moves to a Cloud Run Jobs worker whose image
packages the already-verified creative-pipeline scripts. Adds
per-stage design table, PR roadmap (#1-oratis#4), cost + model-tier policy.
WHY: verified against live task cgt-20260710153030-s5t5x — real
doubao-seedance-2-0-260128 returns content as an OBJECT {video_url:string}
(not an array) and duration at TOP LEVEL. Prior resolveVideoUrl threw
TypeError (content.find is not a function), leaving Assets stuck
'generating' — the PR's core fix did not work against the target model.
WHAT: resolveVideoUrl handles object/array/legacy shapes (array form skips
echoed reference_video inputs); resolveDuration reads top-level→output→
usage; generate route rounds duration before the Int? persist (was a 500
after a paid Ark job started).
WHY: code review — the competitors list filtered level via a rawMeta JSON
path that nothing writes (dead filter), while its sibling segmentPlan got
a real column. level is the primary routing axis (09 §3), so it warrants a
first-class indexed column, not an untyped blob key.
WHY/WHAT (xhigh review findings):
- SSRF: ingest fetched caller-supplied mediaUrl with no validation → new
  storage.uploadFromUrl blocks non-http(s) + private/link-local hosts.
- OOM/timeout: whole video buffered, unbounded batch → 100MB cap + 50-item
  batch cap (413).
- competitors list masked all errors as empty 200 → returns 500 on failure;
  level filter now hits the real column.
- parseHumanNumber fixes impressions/adDays silently dropping '710.4K' /
  '1,942' (the ranking signals); mergeRawMeta no longer corrupts array
  payloads; asset type prefers content-type over extension; isOwnGcsUrl +
  gcsPublicPrefix consolidated into storage.ts (was duplicated).
WHY: register is rate-limited 5/hour/IP; registering per-test flaked across
retries/reruns. Register once in beforeAll and share the authed context;
the anon test uses the default unauthenticated request fixture (config
baseURL, no hardcoded localhost).
@mtmtian

mtmtian commented Jul 10, 2026

Copy link
Copy Markdown
Author

关闭:与上游已合并的 competitor-intel(#12#14)重复,同事已做且更完整(UI + 单测 + SSRF + seedance2 修复 + coerceImpressions 均已覆盖)。本地 creative-pipeline 的差异化能力(分段路由 reuse/remake/drop、@视频锚定复刻)与上游 text2video-only 建模需先对齐再议。

@mtmtian mtmtian closed this Jul 10, 2026
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