chore: sync storyforge handoff state
Some checks failed
StoryForge CI / Baseline checks (push) Has been cancelled
StoryForge CI / Backend tests (push) Has been cancelled
StoryForge CI / Web tests (push) Has been cancelled

This commit is contained in:
kris
2026-05-02 17:50:21 +08:00
parent 6f0d944a75
commit 65db3cd336
20 changed files with 3780 additions and 250 deletions

View File

@@ -2,9 +2,11 @@ from __future__ import annotations
import json
import os
import shutil
import sys
import tempfile
import unittest
import weakref
from pathlib import Path
from types import SimpleNamespace
@@ -487,9 +489,58 @@ def _seed_domestic(db: Database, owner: dict[str, object], project_row: dict[str
return account_id
def _insert_domestic_creator_account(
db: Database,
owner: dict[str, object],
project_row: dict[str, object],
platform: str,
*,
suffix: str,
title: str,
handle: str,
bio: str,
tags: list[str],
keywords: list[str],
) -> str:
now = utc_now()
account_id = f"{platform}_acct_contract_{suffix}"
db.execute(
"""
INSERT INTO content_sources (
id, user_id, project_id, source_kind, platform, handle, source_url, title, local_path,
metadata_json, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
account_id,
owner["id"],
project_row["id"],
"creator_account",
platform,
handle,
f"https://example.com/{platform}/profile-{suffix}",
title,
"",
_json(
{
"bio": bio,
"description": bio,
"avatar_url": "https://example.com/avatar.png",
"tags": tags,
"keywords": keywords,
"max_items": 5,
}
),
now,
now,
),
)
return account_id
def _build_app(platforms: list[str]) -> tuple[FastAPI, SimpleNamespace, dict[str, object]]:
tmpdir = tempfile.TemporaryDirectory()
db = Database(str(Path(tmpdir.name) / "storyforge.db"))
tmpdir = Path(tempfile.mkdtemp(prefix="storyforge-platform-contracts-"))
db = Database(str(tmpdir / "storyforge.db"))
db.init_schema()
owner_row, project_row, model_row = _seed_base_account(db)
legacy = _make_legacy(db, owner_row)
@@ -497,6 +548,7 @@ def _build_app(platforms: list[str]) -> tuple[FastAPI, SimpleNamespace, dict[str
register_douyin_routes(app, legacy)
for platform in platforms:
register_domestic_platform_routes(app, legacy, platform=platform, label=platform)
weakref.finalize(app, shutil.rmtree, tmpdir, True)
app.state._tmpdir = tmpdir
app.state._legacy = legacy
app.state._project_row = project_row
@@ -593,6 +645,323 @@ class PlatformContractTests(unittest.TestCase):
self.assertIn("account", digest_item)
self.assertIn("video", digest_item)
def test_kuaishou_creator_center_sync_persists_snapshots_and_analysis_context(self) -> None:
app, legacy, seed = _build_app(["kuaishou"])
with TestClient(app) as client:
sync = client.post(
"/v2/kuaishou/accounts/sync",
headers={"Authorization": "Bearer dummy"},
json={
"project_id": seed["project"]["id"],
"profile_url": "https://www.kuaishou.com/profile/contract-creator",
"title": "快手合同账号",
"handle": "contract_creator",
"manual_profile_payload": {
"nickname": "快手合同账号",
"bio": "擅长创业内容和成交转化",
"avatar_url": "https://example.com/kuaishou/avatar.png",
"follower_count": 32100,
},
"manual_creator_pages": [
{
"url": "https://creator.kuaishou.com/creator/home",
"title": "快手创作者中心",
"payload": {
"creator": {
"nickname": "快手合同账号",
"fans_count": 32100,
"play_count": 987654,
"content_tags": ["创业", "转化"],
},
"works": {
"published_count": 48,
"avg_finish_rate": 0.43,
"items": [
{
"video_id": "ks_work_001",
"title": "创业成交拆解 1",
"description": "拆 1 个高转化案例",
"share_url": "https://www.kuaishou.com/short-video/ks_work_001",
"cover_url": "https://example.com/kuaishou/work-1.png",
"duration_sec": 38,
"published_at": "2026-03-20T10:00:00+00:00",
"play_count": 82000,
"like_count": 4300,
"comment_count": 280,
"share_count": 140,
"tags": ["创业", "成交"]
},
{
"video_id": "ks_work_002",
"title": "口播脚本结构拆解",
"description": "复盘 3 段爆款口播结构",
"share_url": "https://www.kuaishou.com/short-video/ks_work_002",
"cover_url": "https://example.com/kuaishou/work-2.png",
"duration_sec": 46,
"published_at": "2026-03-18T10:00:00+00:00",
"play_count": 65000,
"like_count": 3100,
"comment_count": 190,
"share_count": 96,
"tags": ["口播", "结构"]
},
],
},
},
}
],
},
)
self.assertEqual(sync.status_code, 200, sync.text)
workspace_payload = sync.json()
self.assertEqual(workspace_payload["account"]["platform"], "kuaishou")
self.assertIsNotNone(workspace_payload["latest_public_snapshot"])
self.assertIsNotNone(workspace_payload["latest_creator_snapshot"])
self.assertEqual(workspace_payload["latest_creator_snapshot"]["snapshot_type"], "creator_center")
self.assertEqual(workspace_payload["account"]["nickname"], "快手合同账号")
self.assertGreaterEqual(workspace_payload["account"]["video_summary"]["count"], 2)
account_id = workspace_payload["account"]["id"]
videos = client.get(
f"/v2/kuaishou/accounts/{account_id}/videos",
headers={"Authorization": "Bearer dummy"},
)
self.assertEqual(videos.status_code, 200, videos.text)
videos_payload = videos.json()
self.assertGreaterEqual(videos_payload["count"], 2)
self.assertTrue(videos_payload["items"])
self.assertEqual(videos_payload["items"][0]["title"], "创业成交拆解 1")
self.assertEqual(videos_payload["items"][0]["stats"]["play"], 82000)
self.assertTrue(videos_payload["top_scored_video_ids"])
self.assertEqual(videos_payload["top_scored_video_ids"][0], videos_payload["items"][0]["id"])
snapshots = client.get(
f"/v2/kuaishou/accounts/{account_id}/snapshots",
headers={"Authorization": "Bearer dummy"},
)
self.assertEqual(snapshots.status_code, 200, snapshots.text)
snapshots_payload = snapshots.json()
self.assertGreaterEqual(len(snapshots_payload), 2)
creator_snapshot = next(item for item in snapshots_payload if item["snapshot_type"] == "creator_center")
creator_fields = client.get(
f"/v2/kuaishou/accounts/{account_id}/creator-fields",
headers={"Authorization": "Bearer dummy"},
)
self.assertEqual(creator_fields.status_code, 200, creator_fields.text)
creator_fields_payload = creator_fields.json()
self.assertEqual(creator_fields_payload["id"], creator_snapshot["id"])
self.assertEqual(creator_fields_payload["snapshot_type"], "creator_center")
self.assertTrue(creator_fields_payload["fields"])
analyze = client.post(
f"/v2/kuaishou/accounts/{account_id}/analysis",
headers={"Authorization": "Bearer dummy"},
json={
"model_profile_ids": [],
"linked_account_ids": [],
"include_linked_accounts": True,
"include_recent_similar_candidates": True,
"max_videos": 6,
"extra_focus": "更关注创作者中心里的成交与转化指标",
"temperature": 0.35,
"auto_analyze_top_videos": False,
"top_video_analysis_count": 4,
},
)
self.assertEqual(analyze.status_code, 200, analyze.text)
analyze_payload = analyze.json()
self.assertIn("creator_center", analyze_payload["context"])
self.assertEqual(
analyze_payload["context"]["creator_center"]["latest_creator_snapshot"]["snapshot_type"],
"creator_center",
)
self.assertEqual(analyze_payload["context"]["requested_model_profile_ids"], [])
self.assertEqual(analyze_payload["context"]["selected_model_profile_ids"], [seed["model"]["id"]])
self.assertTrue(analyze_payload["context"]["request_options"]["include_linked_accounts"])
self.assertEqual(analyze_payload["context"]["linked_accounts"], [])
self.assertEqual(analyze_payload["top_video_analyses"], [])
def test_domestic_analysis_uses_requested_context_and_auto_top_video_followup(self) -> None:
app, legacy, seed = _build_app(["xiaohongshu"])
source_account_id = _seed_domestic(legacy.db, seed["owner"], seed["project"], "xiaohongshu")
linked_account_id = _insert_domestic_creator_account(
legacy.db,
seed["owner"],
seed["project"],
"xiaohongshu",
suffix="linked",
title="Linked Benchmark",
handle="xhs_linked",
bio="主打创业转化与爆款拆解",
tags=["创业", "转化"],
keywords=["创业", "转化"],
)
candidate_account_id = _insert_domestic_creator_account(
legacy.db,
seed["owner"],
seed["project"],
"xiaohongshu",
suffix="candidate",
title="Candidate Creator",
handle="xhs_candidate",
bio="专注创业口播、成交文案与转化漏斗",
tags=["创业", "口播", "转化"],
keywords=["口播", "成交"],
)
now = utc_now()
legacy.db.execute(
"""
INSERT INTO xiaohongshu_account_relations (
id, user_id, source_account_id, target_account_id, target_profile_url,
relation_type, note, search_id, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
"xhs_relation_contract_linked",
seed["owner"]["id"],
source_account_id,
linked_account_id,
"",
"benchmark",
"linked note",
"",
now,
),
)
legacy.db.execute(
"""
INSERT INTO xiaohongshu_similarity_searches (
id, user_id, source_account_id, prompt_text, context_json, created_at
) VALUES (?, ?, ?, ?, ?, ?)
""",
(
"xhs_search_contract_recent",
seed["owner"]["id"],
source_account_id,
"recent search",
_json({"source_account": "xiaohongshu"}),
now,
),
)
legacy.db.execute(
"""
INSERT INTO xiaohongshu_similarity_candidates (
id, search_id, candidate_account_id, candidate_profile_url, heuristic_score,
agent_score, rationale_text, dimensions_json, raw_output_json, rank_index, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
"xhs_candidate_contract_recent",
"xhs_search_contract_recent",
candidate_account_id,
"https://example.com/xiaohongshu/profile-candidate",
72,
72,
"近期相似候选",
_json({"tag_overlap": 2}),
_json({"candidate_account_id": candidate_account_id, "candidate_profile_url": "https://example.com/xiaohongshu/profile-candidate"}),
0,
now,
),
)
with TestClient(app) as client:
analyze = client.post(
f"/v2/xiaohongshu/accounts/{source_account_id}/analysis",
headers={"Authorization": "Bearer dummy"},
json={
"model_profile_ids": [seed["model"]["id"]],
"linked_account_ids": [linked_account_id],
"include_linked_accounts": True,
"include_recent_similar_candidates": True,
"max_videos": 5,
"extra_focus": "关注转化路径和选题结构",
"temperature": 0.32,
"auto_analyze_top_videos": True,
"top_video_analysis_count": 2,
},
)
self.assertEqual(analyze.status_code, 200, analyze.text)
payload = analyze.json()
self.assertEqual(payload["context"]["requested_model_profile_ids"], [seed["model"]["id"]])
self.assertEqual(payload["context"]["selected_model_profile_ids"], [seed["model"]["id"]])
self.assertEqual(payload["context"]["request_options"]["top_video_analysis_count"], 2)
self.assertEqual(len(payload["context"]["linked_accounts"]), 1)
self.assertEqual(payload["context"]["linked_accounts"][0]["target_account_id"], linked_account_id)
self.assertEqual(len(payload["context"]["recent_similar_candidates"]), 1)
self.assertEqual(payload["context"]["recent_similar_candidates"][0]["candidate_account_id"], candidate_account_id)
self.assertEqual(payload["top_video_analyses"][0]["video_id"], "xiaohongshu_video_contract_2")
self.assertEqual(len(payload["top_video_analyses"]), 2)
def test_domestic_similarity_search_merges_manual_urls_and_linked_candidates(self) -> None:
app, legacy, seed = _build_app(["xiaohongshu"])
source_account_id = _seed_domestic(legacy.db, seed["owner"], seed["project"], "xiaohongshu")
linked_account_id = _insert_domestic_creator_account(
legacy.db,
seed["owner"],
seed["project"],
"xiaohongshu",
suffix="similarlinked",
title="Linked Similar",
handle="xhs_similar_linked",
bio="创业成交、私域转化、直播承接",
tags=["创业", "成交", "转化"],
keywords=["直播", "承接"],
)
now = utc_now()
legacy.db.execute(
"""
INSERT INTO xiaohongshu_account_relations (
id, user_id, source_account_id, target_account_id, target_profile_url,
relation_type, note, search_id, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
"xhs_relation_contract_similarity",
seed["owner"]["id"],
source_account_id,
linked_account_id,
"https://example.com/xiaohongshu/profile-similarlinked",
"benchmark",
"linked similar note",
"",
now,
),
)
with TestClient(app) as client:
created = client.post(
"/v2/xiaohongshu/similar-searches",
headers={"Authorization": "Bearer dummy"},
json={
"source_account_id": source_account_id,
"candidate_urls": [
"https://example.com/xiaohongshu/external-similar"
],
"seed_linked_accounts": True,
"search_public_pages": False,
"model_profile_id": seed["model"]["id"],
"max_candidates": 6,
"extra_requirements": "优先找创业成交和口播拆解账号",
},
)
self.assertEqual(created.status_code, 200, created.text)
detail = client.get(
f"/v2/xiaohongshu/similar-searches/{created.json()['search_id']}",
headers={"Authorization": "Bearer dummy"},
)
self.assertEqual(detail.status_code, 200, detail.text)
payload = detail.json()
self.assertGreaterEqual(len(payload["candidates"]), 2)
self.assertEqual(payload["candidates"][0]["candidate_account_id"], linked_account_id)
manual_candidate = next(
item for item in payload["candidates"]
if item.get("candidate_profile_url") == "https://example.com/xiaohongshu/external-similar"
)
self.assertEqual(manual_candidate["candidate_account_id"], "")
self.assertIn("external", manual_candidate["candidate_nickname"].lower())
def test_douyin_live_first_mutation_routes_are_available(self) -> None:
app, legacy, seed = _build_app(["xiaohongshu"])
douyin_account_id = _seed_douyin(legacy.db, seed["owner"], seed["model"])