@tikomni/skills 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/social-media-crawl/scripts/core/tikomni_common.py +7 -0
- package/skills/social-media-crawl/scripts/core/u3_fallback.py +146 -28
- package/skills/social-media-crawl/scripts/pipelines/douyin_metadata.py +151 -0
- package/skills/social-media-crawl/scripts/pipelines/home_asr.py +40 -37
- package/skills/social-media-crawl/scripts/pipelines/homepage_collectors.py +5 -11
- package/skills/social-media-crawl/scripts/pipelines/input_contracts.py +318 -0
- package/skills/social-media-crawl/scripts/pipelines/media_url_rules.py +86 -0
- package/skills/social-media-crawl/scripts/pipelines/platform_adapters.py +77 -30
- package/skills/social-media-crawl/scripts/pipelines/run_douyin_creator_home.py +84 -6
- package/skills/social-media-crawl/scripts/pipelines/run_douyin_single_work.py +79 -73
- package/skills/social-media-crawl/scripts/pipelines/run_xiaohongshu_creator_home.py +84 -6
- package/skills/social-media-crawl/scripts/pipelines/run_xiaohongshu_single_work.py +86 -60
- package/skills/social-media-crawl/scripts/writers/write_work_fact_card.py +5 -3
|
@@ -27,10 +27,15 @@ from scripts.core.config_loader import config_get, load_tikomni_config, resolve_
|
|
|
27
27
|
from scripts.core.progress_report import build_progress_reporter
|
|
28
28
|
from scripts.core.storage_router import resolve_author_directory_name
|
|
29
29
|
from scripts.core.tikomni_common import resolve_runtime, write_json_stdout
|
|
30
|
+
from scripts.pipelines.input_contracts import normalize_douyin_creator_input
|
|
31
|
+
from scripts.pipelines.schema import build_author_profile
|
|
30
32
|
from scripts.pipelines.douyin_creator_home_helpers import collect_and_adapt
|
|
31
33
|
from scripts.pipelines.home_asr import enrich_author_home_asr
|
|
32
34
|
from scripts.writers.write_work_fact_card import build_work_fact_card, persist_output_envelope, write_work_fact_card
|
|
33
35
|
|
|
36
|
+
DEFAULT_MAX_ITEMS = 200
|
|
37
|
+
MAX_ITEMS_HARD_LIMIT = 200
|
|
38
|
+
|
|
34
39
|
|
|
35
40
|
def _write_collection_artifacts(
|
|
36
41
|
*,
|
|
@@ -81,11 +86,12 @@ def run_douyin_creator_home(
|
|
|
81
86
|
*,
|
|
82
87
|
input_value: str,
|
|
83
88
|
config: Dict[str, Any],
|
|
84
|
-
runtime: Dict[str, Any],
|
|
89
|
+
runtime: Dict[str, Any] | None,
|
|
85
90
|
max_items: int,
|
|
86
91
|
write_card: bool,
|
|
87
92
|
persist_output: bool,
|
|
88
93
|
) -> Dict[str, Any]:
|
|
94
|
+
bounded_max_items = max(1, min(int(max_items), MAX_ITEMS_HARD_LIMIT))
|
|
89
95
|
progress = build_progress_reporter(
|
|
90
96
|
workflow="social-media-crawl",
|
|
91
97
|
platform="douyin",
|
|
@@ -94,15 +100,69 @@ def run_douyin_creator_home(
|
|
|
94
100
|
scope="workflow",
|
|
95
101
|
)
|
|
96
102
|
progress.started(stage="author_home.workflow", message="douyin author_home workflow started")
|
|
103
|
+
preflight = normalize_douyin_creator_input(input_value)
|
|
104
|
+
normalized_input_value = str(preflight.get("input_value") or "")
|
|
105
|
+
if preflight.get("error_reason"):
|
|
106
|
+
request_id = ensure_request_id(None, fallback_seed=input_value)
|
|
107
|
+
empty_profile = build_author_profile(platform="douyin", request_id=request_id)
|
|
108
|
+
extract_trace = [
|
|
109
|
+
{
|
|
110
|
+
"step": "input.preflight",
|
|
111
|
+
"ok": False,
|
|
112
|
+
"input_kind": "creator_url_or_sec_uid",
|
|
113
|
+
"normalized_input_value": normalized_input_value or None,
|
|
114
|
+
"error_reason": preflight.get("error_reason"),
|
|
115
|
+
"missing_fields": list(preflight.get("missing_fields") or []),
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
envelope = {
|
|
119
|
+
"object_type": "creator",
|
|
120
|
+
"platform": "douyin",
|
|
121
|
+
"input": input_value,
|
|
122
|
+
"normalized": {
|
|
123
|
+
"creator_profile": {**empty_profile, "request_id": request_id, "extract_trace": extract_trace},
|
|
124
|
+
"work_collection": {
|
|
125
|
+
"platform": "douyin",
|
|
126
|
+
"platform_author_id": "",
|
|
127
|
+
"count": 0,
|
|
128
|
+
"items": [],
|
|
129
|
+
"request_id": request_id,
|
|
130
|
+
"extract_trace": extract_trace,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
"completeness": evaluate_collection(empty_profile, []),
|
|
134
|
+
"missing_fields": normalize_missing_fields(preflight.get("missing_fields")),
|
|
135
|
+
"error_reason": str(preflight.get("error_reason") or "invalid_creator_input"),
|
|
136
|
+
"extract_trace": extract_trace,
|
|
137
|
+
"request_id": request_id,
|
|
138
|
+
"card_write": {
|
|
139
|
+
"enabled": bool(write_card),
|
|
140
|
+
"ok": False,
|
|
141
|
+
"count": 0,
|
|
142
|
+
"results": [],
|
|
143
|
+
"reason": "skipped_invalid_input",
|
|
144
|
+
},
|
|
145
|
+
"collection_artifacts": {},
|
|
146
|
+
"output_persist": {"enabled": False, "skipped": True, "reason": "invalid_input"},
|
|
147
|
+
}
|
|
148
|
+
progress.done(
|
|
149
|
+
stage="author_home.workflow",
|
|
150
|
+
message="douyin author_home workflow finished",
|
|
151
|
+
data={"request_id": request_id, "works_count": 0, "error_reason": envelope["error_reason"]},
|
|
152
|
+
)
|
|
153
|
+
return envelope
|
|
154
|
+
|
|
155
|
+
if runtime is None:
|
|
156
|
+
raise ValueError("runtime_required_for_valid_input")
|
|
97
157
|
|
|
98
158
|
raw, profile, works, missing = collect_and_adapt(
|
|
99
|
-
input_value=input_value,
|
|
159
|
+
input_value=normalized_input_value or input_value,
|
|
100
160
|
base_url=runtime["base_url"],
|
|
101
161
|
token=runtime["token"],
|
|
102
162
|
timeout_ms=runtime["timeout_ms"],
|
|
103
163
|
page_size=20,
|
|
104
164
|
pages_max=50,
|
|
105
|
-
max_items=
|
|
165
|
+
max_items=bounded_max_items,
|
|
106
166
|
progress=progress.child(scope="author_home.collect"),
|
|
107
167
|
)
|
|
108
168
|
|
|
@@ -138,7 +198,7 @@ def run_douyin_creator_home(
|
|
|
138
198
|
|
|
139
199
|
request_id = ensure_request_id(
|
|
140
200
|
raw.get("request_id") or profile.get("request_id"),
|
|
141
|
-
fallback_seed=input_value,
|
|
201
|
+
fallback_seed=normalized_input_value or input_value,
|
|
142
202
|
)
|
|
143
203
|
extract_trace = list(raw.get("extract_trace") or []) + list(asr_bundle.get("trace") or [])
|
|
144
204
|
|
|
@@ -206,7 +266,12 @@ def main() -> None:
|
|
|
206
266
|
parser.add_argument("--allow-process-env", action="store_true", help="Allow process env overrides")
|
|
207
267
|
parser.add_argument("--base-url", default=None, help="Override Tikomni base URL")
|
|
208
268
|
parser.add_argument("--timeout-ms", type=int, default=None, help="Override timeout in ms")
|
|
209
|
-
parser.add_argument(
|
|
269
|
+
parser.add_argument(
|
|
270
|
+
"--max-items",
|
|
271
|
+
type=int,
|
|
272
|
+
default=DEFAULT_MAX_ITEMS,
|
|
273
|
+
help=f"Max works to collect from homepage (default full crawl, capped at {MAX_ITEMS_HARD_LIMIT})",
|
|
274
|
+
)
|
|
210
275
|
parser.set_defaults(write_card=True, persist_output=True)
|
|
211
276
|
parser.add_argument("--write-card", dest="write_card", action="store_true", help="Write work fact cards")
|
|
212
277
|
parser.add_argument("--no-write-card", dest="write_card", action="store_false", help="Skip card writing")
|
|
@@ -215,6 +280,19 @@ def main() -> None:
|
|
|
215
280
|
args = parser.parse_args()
|
|
216
281
|
|
|
217
282
|
config, _ = load_tikomni_config(args.config, env_file=args.env_file, allow_process_env=args.allow_process_env)
|
|
283
|
+
preflight = normalize_douyin_creator_input(args.input)
|
|
284
|
+
if preflight.get("error_reason"):
|
|
285
|
+
write_json_stdout(
|
|
286
|
+
run_douyin_creator_home(
|
|
287
|
+
input_value=args.input,
|
|
288
|
+
config=config,
|
|
289
|
+
runtime=None,
|
|
290
|
+
max_items=int(args.max_items),
|
|
291
|
+
write_card=bool(args.write_card),
|
|
292
|
+
persist_output=bool(args.persist_output),
|
|
293
|
+
)
|
|
294
|
+
)
|
|
295
|
+
return
|
|
218
296
|
runtime = resolve_runtime(
|
|
219
297
|
env_file=args.env_file,
|
|
220
298
|
api_key_env=str(config_get(config, "runtime.auth_env_key", "TIKOMNI_API_KEY")),
|
|
@@ -224,7 +302,7 @@ def main() -> None:
|
|
|
224
302
|
)
|
|
225
303
|
write_json_stdout(
|
|
226
304
|
run_douyin_creator_home(
|
|
227
|
-
input_value=args.input,
|
|
305
|
+
input_value=str(preflight.get("input_value") or args.input),
|
|
228
306
|
config=config,
|
|
229
307
|
runtime=runtime,
|
|
230
308
|
max_items=int(args.max_items),
|
|
@@ -31,6 +31,13 @@ from scripts.core.config_loader import config_get, load_tikomni_config
|
|
|
31
31
|
from scripts.core.extract_pipeline import resolve_trace_error_context
|
|
32
32
|
from scripts.core.progress_report import ProgressReporter, build_progress_reporter
|
|
33
33
|
from scripts.pipelines.douyin_video_type_matrix import normalize_douyin_video_type
|
|
34
|
+
from scripts.pipelines.douyin_metadata import (
|
|
35
|
+
extract_douyin_author as extract_shared_douyin_author,
|
|
36
|
+
extract_douyin_caption as extract_shared_douyin_caption,
|
|
37
|
+
extract_douyin_metrics as extract_shared_douyin_metrics,
|
|
38
|
+
extract_douyin_title as extract_shared_douyin_title,
|
|
39
|
+
)
|
|
40
|
+
from scripts.pipelines.input_contracts import normalize_douyin_work_input
|
|
34
41
|
from scripts.core.asr_pipeline import derive_asr_clean_text, run_u2_asr_with_timeout_retry
|
|
35
42
|
from scripts.pipelines.select_low_quality_video_url import select_low_quality_video_url
|
|
36
43
|
from scripts.core.tikomni_common import (
|
|
@@ -156,14 +163,8 @@ def _normalize_input(
|
|
|
156
163
|
input_value: Optional[str],
|
|
157
164
|
share_url: Optional[str],
|
|
158
165
|
) -> Dict[str, Optional[str]]:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if input_value and not normalized_share:
|
|
162
|
-
candidate = input_value.strip()
|
|
163
|
-
if candidate.startswith("http://") or candidate.startswith("https://"):
|
|
164
|
-
normalized_share = candidate
|
|
165
|
-
|
|
166
|
-
return {"share_url": normalized_share}
|
|
166
|
+
normalized = normalize_douyin_work_input(input_value, share_url)
|
|
167
|
+
return {"share_url": normalize_text(normalized.get("share_url")) or None}
|
|
167
168
|
|
|
168
169
|
|
|
169
170
|
def _extract_aweme_detail(payload: Any) -> Optional[Dict[str, Any]]:
|
|
@@ -238,76 +239,19 @@ def _normalize_duration_ms(item: Dict[str, Any]) -> Optional[int]:
|
|
|
238
239
|
|
|
239
240
|
|
|
240
241
|
def _pick_title(item: Dict[str, Any]) -> str:
|
|
241
|
-
|
|
242
|
-
value = item.get(key)
|
|
243
|
-
text = normalize_text(value)
|
|
244
|
-
if text:
|
|
245
|
-
return text
|
|
246
|
-
return ""
|
|
242
|
+
return extract_shared_douyin_title(item)
|
|
247
243
|
|
|
248
244
|
|
|
249
245
|
def _pick_desc(item: Dict[str, Any]) -> str:
|
|
250
|
-
|
|
251
|
-
value = item.get(key)
|
|
252
|
-
text = normalize_text(value)
|
|
253
|
-
if text:
|
|
254
|
-
return text
|
|
255
|
-
return ""
|
|
246
|
+
return extract_shared_douyin_caption(item)
|
|
256
247
|
|
|
257
248
|
|
|
258
249
|
def _extract_author(item: Dict[str, Any]) -> Dict[str, Optional[str]]:
|
|
259
|
-
|
|
260
|
-
if not isinstance(author, dict):
|
|
261
|
-
author = {}
|
|
262
|
-
|
|
263
|
-
author_platform_id = normalize_text(author.get("uid")) or normalize_text(author.get("id")) or normalize_text(item.get("author_user_id"))
|
|
264
|
-
author_handle = normalize_text(author.get("short_id")) or normalize_text(author.get("nickname"))
|
|
265
|
-
douyin_sec_uid = normalize_text(author.get("sec_uid"))
|
|
266
|
-
douyin_aweme_author_id = normalize_text(item.get("author_user_id")) or author_platform_id
|
|
267
|
-
|
|
268
|
-
return {
|
|
269
|
-
"author_handle": author_handle or None,
|
|
270
|
-
"platform_author_id": author_platform_id or None,
|
|
271
|
-
"author_platform_id": author_platform_id or None,
|
|
272
|
-
"douyin_sec_uid": douyin_sec_uid or None,
|
|
273
|
-
"douyin_aweme_author_id": douyin_aweme_author_id or None,
|
|
274
|
-
"nickname": normalize_text(author.get("nickname")) or None,
|
|
275
|
-
"signature": normalize_text(author.get("signature")) or None,
|
|
276
|
-
}
|
|
250
|
+
return extract_shared_douyin_author(item)
|
|
277
251
|
|
|
278
252
|
|
|
279
253
|
def _extract_metrics(item: Dict[str, Any]) -> Dict[str, Optional[int]]:
|
|
280
|
-
|
|
281
|
-
if not isinstance(statistics, dict):
|
|
282
|
-
statistics = {}
|
|
283
|
-
|
|
284
|
-
def metric(*keys: str, default: Optional[int] = 0) -> Optional[int]:
|
|
285
|
-
for key in keys:
|
|
286
|
-
value = _safe_int(statistics.get(key))
|
|
287
|
-
if value is not None:
|
|
288
|
-
return value
|
|
289
|
-
value = _safe_int(item.get(key))
|
|
290
|
-
if value is not None:
|
|
291
|
-
return value
|
|
292
|
-
return default
|
|
293
|
-
|
|
294
|
-
metrics = {
|
|
295
|
-
"digg_count": metric("digg_count"),
|
|
296
|
-
"comment_count": metric("comment_count"),
|
|
297
|
-
"collect_count": metric("collect_count"),
|
|
298
|
-
"share_count": metric("share_count", "forward_count"),
|
|
299
|
-
"play_count": metric("play_count", default=None),
|
|
300
|
-
}
|
|
301
|
-
play_count = metrics.get("play_count")
|
|
302
|
-
engagement_floor = max(
|
|
303
|
-
int(metrics.get("digg_count") or 0),
|
|
304
|
-
int(metrics.get("comment_count") or 0),
|
|
305
|
-
int(metrics.get("collect_count") or 0),
|
|
306
|
-
int(metrics.get("share_count") or 0),
|
|
307
|
-
)
|
|
308
|
-
if play_count is not None and int(play_count) <= 0 and engagement_floor > 0:
|
|
309
|
-
metrics["play_count"] = None
|
|
310
|
-
return metrics
|
|
254
|
+
return extract_shared_douyin_metrics(item)
|
|
311
255
|
|
|
312
256
|
|
|
313
257
|
def _extract_platform_work_id(item: Dict[str, Any]) -> Optional[str]:
|
|
@@ -633,6 +577,7 @@ def _build_result(
|
|
|
633
577
|
cover_image: Optional[str] = None,
|
|
634
578
|
asr_source: str = "fallback_none",
|
|
635
579
|
timings: Optional[Dict[str, int]] = None,
|
|
580
|
+
missing_fields: Optional[List[Dict[str, str]]] = None,
|
|
636
581
|
) -> Dict[str, Any]:
|
|
637
582
|
summary_block = summarize_content(raw_content, source="douyin:single-video-low-quality")
|
|
638
583
|
insights = list(summary_block.get("insights", []))
|
|
@@ -698,13 +643,13 @@ def _build_result(
|
|
|
698
643
|
"insights": insights,
|
|
699
644
|
"confidence": confidence,
|
|
700
645
|
"error_reason": error_reason,
|
|
701
|
-
"missing_fields": _build_missing_fields(
|
|
646
|
+
"missing_fields": list(missing_fields or _build_missing_fields(
|
|
702
647
|
title=title,
|
|
703
648
|
desc=desc,
|
|
704
649
|
platform_work_id=platform_work_id,
|
|
705
650
|
video_down_url=video_down_url,
|
|
706
651
|
author=author,
|
|
707
|
-
),
|
|
652
|
+
)),
|
|
708
653
|
"extract_trace": extract_trace,
|
|
709
654
|
"fallback_trace": fallback_trace,
|
|
710
655
|
"request_id": request_id,
|
|
@@ -741,7 +686,8 @@ def run_douyin_single_video(
|
|
|
741
686
|
workflow_started_at = time.perf_counter()
|
|
742
687
|
timings = _empty_timings()
|
|
743
688
|
parse_started_at = time.perf_counter()
|
|
744
|
-
|
|
689
|
+
preflight = normalize_douyin_work_input(input_value, share_url)
|
|
690
|
+
source_input = {"share_url": normalize_text(preflight.get("share_url")) or None}
|
|
745
691
|
timings["url_parse_ms"] = _elapsed_ms(parse_started_at)
|
|
746
692
|
if progress is not None:
|
|
747
693
|
progress.started(
|
|
@@ -749,6 +695,66 @@ def run_douyin_single_video(
|
|
|
749
695
|
message="douyin single_video workflow started",
|
|
750
696
|
data={"analysis_mode": analysis_mode, "write_card": bool(write_card), "persist_output": bool(persist_output)},
|
|
751
697
|
)
|
|
698
|
+
preflight_trace = [
|
|
699
|
+
{
|
|
700
|
+
"step": "input.preflight",
|
|
701
|
+
"ok": preflight.get("error_reason") is None,
|
|
702
|
+
"input_kind": "share_url",
|
|
703
|
+
"normalized_share_url": source_input.get("share_url"),
|
|
704
|
+
"error_reason": preflight.get("error_reason"),
|
|
705
|
+
"missing_fields": list(preflight.get("missing_fields") or []),
|
|
706
|
+
}
|
|
707
|
+
]
|
|
708
|
+
if preflight.get("error_reason"):
|
|
709
|
+
result = _build_result(
|
|
710
|
+
source_input=source_input,
|
|
711
|
+
platform_work_id=None,
|
|
712
|
+
title="",
|
|
713
|
+
desc="",
|
|
714
|
+
duration_ms=None,
|
|
715
|
+
video_down_url=None,
|
|
716
|
+
author={"author_handle": None, "author_platform_id": None, "douyin_sec_uid": None, "douyin_aweme_author_id": None, "nickname": None, "signature": None},
|
|
717
|
+
metrics=_empty_metrics(),
|
|
718
|
+
tags=[],
|
|
719
|
+
is_video=False,
|
|
720
|
+
video_type_reason="invalid_input",
|
|
721
|
+
raw_content="",
|
|
722
|
+
confidence="low",
|
|
723
|
+
error_reason=str(preflight.get("error_reason") or "invalid_share_url"),
|
|
724
|
+
extract_trace=preflight_trace,
|
|
725
|
+
fallback_trace=[],
|
|
726
|
+
request_id=None,
|
|
727
|
+
u2_task_id=None,
|
|
728
|
+
u2_task_status="UNKNOWN",
|
|
729
|
+
u2_gate_reason="invalid_input",
|
|
730
|
+
analysis_mode=analysis_mode,
|
|
731
|
+
timings=timings,
|
|
732
|
+
missing_fields=list(preflight.get("missing_fields") or []),
|
|
733
|
+
)
|
|
734
|
+
if write_card:
|
|
735
|
+
card_started_at = time.perf_counter()
|
|
736
|
+
result["card_write"] = write_work_fact_card(
|
|
737
|
+
payload=result,
|
|
738
|
+
platform="douyin",
|
|
739
|
+
card_type=card_type,
|
|
740
|
+
card_root=card_root,
|
|
741
|
+
content_kind=content_kind,
|
|
742
|
+
storage_config=storage_config,
|
|
743
|
+
analysis_mode=analysis_mode,
|
|
744
|
+
progress=progress.child(scope="card_write") if progress is not None else None,
|
|
745
|
+
)
|
|
746
|
+
timings["card_write_ms"] = _elapsed_ms(card_started_at)
|
|
747
|
+
timings["llm_analysis_ms"] = _safe_int((result.get("card_write") or {}).get("llm_analysis_ms"))
|
|
748
|
+
timings["total_ms"] = _elapsed_ms(workflow_started_at)
|
|
749
|
+
result["timings"] = dict(timings)
|
|
750
|
+
_update_pipeline_status(result)
|
|
751
|
+
return _finalize_result(
|
|
752
|
+
result=result,
|
|
753
|
+
source_input=source_input,
|
|
754
|
+
platform_work_id=None,
|
|
755
|
+
storage_config=storage_config,
|
|
756
|
+
persist_output=persist_output,
|
|
757
|
+
)
|
|
752
758
|
if not source_input.get("share_url"):
|
|
753
759
|
result = _build_result(
|
|
754
760
|
source_input=source_input,
|
|
@@ -765,7 +771,7 @@ def run_douyin_single_video(
|
|
|
765
771
|
raw_content="",
|
|
766
772
|
confidence="low",
|
|
767
773
|
error_reason="missing_share_url",
|
|
768
|
-
extract_trace=
|
|
774
|
+
extract_trace=preflight_trace,
|
|
769
775
|
fallback_trace=[],
|
|
770
776
|
request_id=None,
|
|
771
777
|
u2_task_id=None,
|
|
@@ -28,9 +28,14 @@ from scripts.core.progress_report import build_progress_reporter
|
|
|
28
28
|
from scripts.core.storage_router import resolve_author_directory_name
|
|
29
29
|
from scripts.core.tikomni_common import resolve_runtime, write_json_stdout
|
|
30
30
|
from scripts.pipelines.home_asr import enrich_author_home_asr
|
|
31
|
+
from scripts.pipelines.input_contracts import normalize_xhs_creator_input
|
|
32
|
+
from scripts.pipelines.schema import build_author_profile
|
|
31
33
|
from scripts.pipelines.xiaohongshu_creator_home_helpers import collect_and_adapt
|
|
32
34
|
from scripts.writers.write_work_fact_card import build_work_fact_card, persist_output_envelope, write_work_fact_card
|
|
33
35
|
|
|
36
|
+
DEFAULT_MAX_ITEMS = 200
|
|
37
|
+
MAX_ITEMS_HARD_LIMIT = 200
|
|
38
|
+
|
|
34
39
|
|
|
35
40
|
def _write_collection_artifacts(
|
|
36
41
|
*,
|
|
@@ -81,11 +86,12 @@ def run_xiaohongshu_creator_home(
|
|
|
81
86
|
*,
|
|
82
87
|
input_value: str,
|
|
83
88
|
config: Dict[str, Any],
|
|
84
|
-
runtime: Dict[str, Any],
|
|
89
|
+
runtime: Dict[str, Any] | None,
|
|
85
90
|
max_items: int,
|
|
86
91
|
write_card: bool,
|
|
87
92
|
persist_output: bool,
|
|
88
93
|
) -> Dict[str, Any]:
|
|
94
|
+
bounded_max_items = max(1, min(int(max_items), MAX_ITEMS_HARD_LIMIT))
|
|
89
95
|
progress = build_progress_reporter(
|
|
90
96
|
workflow="social-media-crawl",
|
|
91
97
|
platform="xiaohongshu",
|
|
@@ -94,15 +100,69 @@ def run_xiaohongshu_creator_home(
|
|
|
94
100
|
scope="workflow",
|
|
95
101
|
)
|
|
96
102
|
progress.started(stage="author_home.workflow", message="xiaohongshu author_home workflow started")
|
|
103
|
+
preflight = normalize_xhs_creator_input(input_value)
|
|
104
|
+
normalized_input_value = str(preflight.get("input_value") or "")
|
|
105
|
+
if preflight.get("error_reason"):
|
|
106
|
+
request_id = ensure_request_id(None, fallback_seed=input_value)
|
|
107
|
+
empty_profile = build_author_profile(platform="xiaohongshu", request_id=request_id)
|
|
108
|
+
extract_trace = [
|
|
109
|
+
{
|
|
110
|
+
"step": "input.preflight",
|
|
111
|
+
"ok": False,
|
|
112
|
+
"input_kind": "creator_url_or_user_id",
|
|
113
|
+
"normalized_input_value": normalized_input_value or None,
|
|
114
|
+
"error_reason": preflight.get("error_reason"),
|
|
115
|
+
"missing_fields": list(preflight.get("missing_fields") or []),
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
envelope = {
|
|
119
|
+
"object_type": "creator",
|
|
120
|
+
"platform": "xiaohongshu",
|
|
121
|
+
"input": input_value,
|
|
122
|
+
"normalized": {
|
|
123
|
+
"creator_profile": {**empty_profile, "request_id": request_id, "extract_trace": extract_trace},
|
|
124
|
+
"work_collection": {
|
|
125
|
+
"platform": "xiaohongshu",
|
|
126
|
+
"platform_author_id": "",
|
|
127
|
+
"count": 0,
|
|
128
|
+
"items": [],
|
|
129
|
+
"request_id": request_id,
|
|
130
|
+
"extract_trace": extract_trace,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
"completeness": evaluate_collection(empty_profile, []),
|
|
134
|
+
"missing_fields": normalize_missing_fields(preflight.get("missing_fields")),
|
|
135
|
+
"error_reason": str(preflight.get("error_reason") or "invalid_creator_input"),
|
|
136
|
+
"extract_trace": extract_trace,
|
|
137
|
+
"request_id": request_id,
|
|
138
|
+
"card_write": {
|
|
139
|
+
"enabled": bool(write_card),
|
|
140
|
+
"ok": False,
|
|
141
|
+
"count": 0,
|
|
142
|
+
"results": [],
|
|
143
|
+
"reason": "skipped_invalid_input",
|
|
144
|
+
},
|
|
145
|
+
"collection_artifacts": {},
|
|
146
|
+
"output_persist": {"enabled": False, "skipped": True, "reason": "invalid_input"},
|
|
147
|
+
}
|
|
148
|
+
progress.done(
|
|
149
|
+
stage="author_home.workflow",
|
|
150
|
+
message="xiaohongshu author_home workflow finished",
|
|
151
|
+
data={"request_id": request_id, "works_count": 0, "error_reason": envelope["error_reason"]},
|
|
152
|
+
)
|
|
153
|
+
return envelope
|
|
154
|
+
|
|
155
|
+
if runtime is None:
|
|
156
|
+
raise ValueError("runtime_required_for_valid_input")
|
|
97
157
|
|
|
98
158
|
raw, profile, works, missing = collect_and_adapt(
|
|
99
|
-
input_value=input_value,
|
|
159
|
+
input_value=normalized_input_value or input_value,
|
|
100
160
|
base_url=runtime["base_url"],
|
|
101
161
|
token=runtime["token"],
|
|
102
162
|
timeout_ms=runtime["timeout_ms"],
|
|
103
163
|
page_size=20,
|
|
104
164
|
pages_max=50,
|
|
105
|
-
max_items=
|
|
165
|
+
max_items=bounded_max_items,
|
|
106
166
|
progress=progress.child(scope="author_home.collect"),
|
|
107
167
|
)
|
|
108
168
|
|
|
@@ -138,7 +198,7 @@ def run_xiaohongshu_creator_home(
|
|
|
138
198
|
|
|
139
199
|
request_id = ensure_request_id(
|
|
140
200
|
raw.get("request_id") or profile.get("request_id"),
|
|
141
|
-
fallback_seed=input_value,
|
|
201
|
+
fallback_seed=normalized_input_value or input_value,
|
|
142
202
|
)
|
|
143
203
|
extract_trace = list(raw.get("extract_trace") or []) + list(asr_bundle.get("trace") or [])
|
|
144
204
|
|
|
@@ -206,7 +266,12 @@ def main() -> None:
|
|
|
206
266
|
parser.add_argument("--allow-process-env", action="store_true", help="Allow process env overrides")
|
|
207
267
|
parser.add_argument("--base-url", default=None, help="Override Tikomni base URL")
|
|
208
268
|
parser.add_argument("--timeout-ms", type=int, default=None, help="Override timeout in ms")
|
|
209
|
-
parser.add_argument(
|
|
269
|
+
parser.add_argument(
|
|
270
|
+
"--max-items",
|
|
271
|
+
type=int,
|
|
272
|
+
default=DEFAULT_MAX_ITEMS,
|
|
273
|
+
help=f"Max works to collect from homepage (default full crawl, capped at {MAX_ITEMS_HARD_LIMIT})",
|
|
274
|
+
)
|
|
210
275
|
parser.set_defaults(write_card=True, persist_output=True)
|
|
211
276
|
parser.add_argument("--write-card", dest="write_card", action="store_true", help="Write work fact cards")
|
|
212
277
|
parser.add_argument("--no-write-card", dest="write_card", action="store_false", help="Skip card writing")
|
|
@@ -215,6 +280,19 @@ def main() -> None:
|
|
|
215
280
|
args = parser.parse_args()
|
|
216
281
|
|
|
217
282
|
config, _ = load_tikomni_config(args.config, env_file=args.env_file, allow_process_env=args.allow_process_env)
|
|
283
|
+
preflight = normalize_xhs_creator_input(args.input)
|
|
284
|
+
if preflight.get("error_reason"):
|
|
285
|
+
write_json_stdout(
|
|
286
|
+
run_xiaohongshu_creator_home(
|
|
287
|
+
input_value=args.input,
|
|
288
|
+
config=config,
|
|
289
|
+
runtime=None,
|
|
290
|
+
max_items=int(args.max_items),
|
|
291
|
+
write_card=bool(args.write_card),
|
|
292
|
+
persist_output=bool(args.persist_output),
|
|
293
|
+
)
|
|
294
|
+
)
|
|
295
|
+
return
|
|
218
296
|
runtime = resolve_runtime(
|
|
219
297
|
env_file=args.env_file,
|
|
220
298
|
api_key_env=str(config_get(config, "runtime.auth_env_key", "TIKOMNI_API_KEY")),
|
|
@@ -224,7 +302,7 @@ def main() -> None:
|
|
|
224
302
|
)
|
|
225
303
|
write_json_stdout(
|
|
226
304
|
run_xiaohongshu_creator_home(
|
|
227
|
-
input_value=args.input,
|
|
305
|
+
input_value=str(preflight.get("input_value") or args.input),
|
|
228
306
|
config=config,
|
|
229
307
|
runtime=runtime,
|
|
230
308
|
max_items=int(args.max_items),
|