mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-18 23:11:12 +02:00
feat(tiktok): add tiktok.user_search verb for account discovery
Video/general search is login-walled for anonymous sessions, but the Users tab (/api/search/user) returns public account records without a redirect, so this exposes the one reliably-unblocked search path. A keyword yields TikTokProfileItems (name, followers, bio, verification), deduped per query, capped, and degraded to an ErrorItem when a query is empty/withheld. Reuses the browser capture (generalized over XHR markers + extractor) and the shared profile item shape. Billed per account on a new TIKTOK_USER meter (TIKTOK_MICROS_PER_USER), surfaced on the chat subagent alongside tiktok.scrape.
This commit is contained in:
parent
6652efd035
commit
192b6dc31a
24 changed files with 502 additions and 18 deletions
|
|
@ -36,6 +36,7 @@ _PLATFORM_RATE_KEYS: dict[BillingUnit, str] = {
|
|||
BillingUnit.YOUTUBE_VIDEO: "YOUTUBE_MICROS_PER_VIDEO",
|
||||
BillingUnit.YOUTUBE_COMMENT: "YOUTUBE_MICROS_PER_COMMENT",
|
||||
BillingUnit.TIKTOK_VIDEO: "TIKTOK_MICROS_PER_VIDEO",
|
||||
BillingUnit.TIKTOK_USER: "TIKTOK_MICROS_PER_USER",
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -53,6 +54,7 @@ _UNIT_NOUNS: dict[BillingUnit, str] = {
|
|||
BillingUnit.YOUTUBE_VIDEO: "video",
|
||||
BillingUnit.YOUTUBE_COMMENT: "comment",
|
||||
BillingUnit.TIKTOK_VIDEO: "video",
|
||||
BillingUnit.TIKTOK_USER: "profile",
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ class BillingUnit(StrEnum):
|
|||
YOUTUBE_VIDEO = "youtube_video"
|
||||
YOUTUBE_COMMENT = "youtube_comment"
|
||||
TIKTOK_VIDEO = "tiktok_video"
|
||||
TIKTOK_USER = "tiktok_user"
|
||||
|
||||
|
||||
class BillableInput(Protocol):
|
||||
|
|
|
|||
|
|
@ -3,3 +3,4 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from app.capabilities.tiktok.scrape import definition as _scrape # noqa: F401
|
||||
from app.capabilities.tiktok.user_search import definition as _user_search # noqa: F401
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
"""``tiktok.user_search``: find public TikTok accounts by keyword."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
"""``tiktok.user_search`` capability registration (billed per account; see config
|
||||
``TIKTOK_MICROS_PER_USER``)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from app.capabilities.core import BillingUnit, Capability, register_capability
|
||||
from app.capabilities.tiktok.user_search.executor import build_user_search_executor
|
||||
from app.capabilities.tiktok.user_search.schemas import (
|
||||
UserSearchInput,
|
||||
UserSearchOutput,
|
||||
)
|
||||
|
||||
TIKTOK_USER_SEARCH = Capability(
|
||||
name="tiktok.user_search",
|
||||
description=(
|
||||
"Find public TikTok accounts by keyword. Returns profile metadata "
|
||||
"(name, followers, bio, verification) per matching account."
|
||||
),
|
||||
input_schema=UserSearchInput,
|
||||
output_schema=UserSearchOutput,
|
||||
executor=build_user_search_executor(),
|
||||
billing_unit=BillingUnit.TIKTOK_USER,
|
||||
docs_url="/docs/connectors/native/tiktok",
|
||||
)
|
||||
|
||||
register_capability(TIKTOK_USER_SEARCH)
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
"""``tiktok.user_search`` executor: queries -> scraper -> TikTok profile items."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
|
||||
from app.capabilities.core import Executor
|
||||
from app.capabilities.core.progress import emit_progress
|
||||
from app.capabilities.tiktok.user_search.schemas import (
|
||||
UserSearchInput,
|
||||
UserSearchOutput,
|
||||
)
|
||||
from app.proprietary.platforms.tiktok import search_tiktok_users
|
||||
|
||||
SearchFn = Callable[..., Awaitable[list[dict]]]
|
||||
|
||||
|
||||
def build_user_search_executor(search_fn: SearchFn | None = None) -> Executor:
|
||||
"""Bind the executor to a search fn (defaults to the proprietary actor)."""
|
||||
search_fn = search_fn or search_tiktok_users
|
||||
|
||||
async def execute(payload: UserSearchInput) -> UserSearchOutput:
|
||||
emit_progress(
|
||||
"starting",
|
||||
"Searching TikTok accounts",
|
||||
total=payload.max_items,
|
||||
unit="item",
|
||||
)
|
||||
items = await search_fn(
|
||||
payload.queries,
|
||||
per_query=payload.results_per_query,
|
||||
limit=payload.max_items,
|
||||
)
|
||||
emit_progress(
|
||||
"done", f"Found {len(items)} account(s)", current=len(items), unit="item"
|
||||
)
|
||||
return UserSearchOutput(items=items)
|
||||
|
||||
return execute
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
"""``tiktok.user_search`` I/O contracts.
|
||||
|
||||
Account discovery over ``TikTok``'s Users tab. Where video/general search is
|
||||
login-walled for anonymous sessions, ``/api/search/user`` returns public account
|
||||
records, so this verb exposes the one reliably-unblocked search path. Each result
|
||||
is a :class:`TikTokProfileItem` (the same shape the profile verb emits).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.capabilities.tiktok.scrape.schemas import (
|
||||
MAX_TIKTOK_ITEMS,
|
||||
MAX_TIKTOK_SOURCES,
|
||||
)
|
||||
from app.proprietary.platforms.tiktok import TikTokProfileItem
|
||||
|
||||
|
||||
class UserSearchInput(BaseModel):
|
||||
queries: list[str] = Field(
|
||||
min_length=1,
|
||||
max_length=MAX_TIKTOK_SOURCES,
|
||||
description="Keywords to search for TikTok accounts (e.g. names, brands).",
|
||||
)
|
||||
results_per_query: int = Field(
|
||||
default=10,
|
||||
ge=1,
|
||||
le=MAX_TIKTOK_ITEMS,
|
||||
description="Max accounts to return per query.",
|
||||
)
|
||||
max_items: int = Field(
|
||||
default=10,
|
||||
ge=1,
|
||||
le=MAX_TIKTOK_ITEMS,
|
||||
description="Max total accounts to return across all queries.",
|
||||
)
|
||||
|
||||
@property
|
||||
def estimated_units(self) -> int:
|
||||
"""Worst-case billable accounts for the pre-flight gate: ``max_items`` is a
|
||||
hard cross-query ceiling (le=100), so no call can exceed it."""
|
||||
return self.max_items
|
||||
|
||||
|
||||
class UserSearchOutput(BaseModel):
|
||||
items: list[TikTokProfileItem] = Field(
|
||||
default_factory=list,
|
||||
description="One item per account found, in emission order.",
|
||||
)
|
||||
|
||||
@property
|
||||
def billable_units(self) -> int:
|
||||
"""One returned account = one billable unit; ErrorItems (``errorCode`` set,
|
||||
for empty/withheld queries) are surfaced but never charged."""
|
||||
return sum(1 for item in self.items if not getattr(item, "errorCode", None))
|
||||
Loading…
Add table
Add a link
Reference in a new issue