diff --git a/internal/command/root_sources.go b/internal/command/root_sources.go index ffc115a..06a7528 100644 --- a/internal/command/root_sources.go +++ b/internal/command/root_sources.go @@ -169,14 +169,16 @@ func (r *Root) newTiktokCommand(runCtx *runContext) *cobra.Command { return newStaticSourceCommand( "tiktok", "TikTok video, comment, and shop data", - "TikTok video, comment, and shop data.\n\nUse this command to search TikTok videos, inspect video details, list comments, and query TikTok Shop products and product details.", - " apimux tiktok search_videos --keyword laptop\n apimux tiktok get_video_detail --share-url https://www.tiktok.com/t/ZTFNEj8Hk/\n apimux tiktok shop_products --seller-id 123456", - "search_videos, get_video_detail, list_comments, shop_products, shop_product_info", + "TikTok video, comment, and shop data.\n\nUse this command to search TikTok videos, inspect video details, list comments, search TikTok Shop products, query seller products and product details, and browse product reviews.", + " apimux tiktok search_videos --keyword laptop\n apimux tiktok get_video_detail --share-url https://www.tiktok.com/t/ZTFNEj8Hk/\n apimux tiktok shop_products --seller-id 123456\n apimux tiktok search_products --keyword labubu --region US\n apimux tiktok product_reviews --product-id 1729556436942358002 --region US", + "search_videos, get_video_detail, list_comments, shop_products, shop_product_info, search_products, product_reviews", newSchemaBoundCapabilityCommand(runCtx, "tiktok.search_videos", "search_videos", "Search TikTok videos", "tiktok search_videos"), newSchemaBoundCapabilityCommand(runCtx, "tiktok.get_video_detail", "get_video_detail", "Fetch one TikTok video detail", "tiktok get_video_detail"), newSchemaBoundCapabilityCommand(runCtx, "tiktok.list_comments", "list_comments", "List TikTok video comments", "tiktok list_comments"), newSchemaBoundCapabilityCommand(runCtx, "tiktok.shop_products", "shop_products", "List TikTok Shop seller products", "tiktok shop_products"), newSchemaBoundCapabilityCommand(runCtx, "tiktok.shop_product_info", "shop_product_info", "Fetch one TikTok Shop product detail", "tiktok shop_product_info"), + newSchemaBoundCapabilityCommand(runCtx, "tiktok.search_products", "search_products", "Search TikTok Shop products", "tiktok search_products"), + newSchemaBoundCapabilityCommand(runCtx, "tiktok.product_reviews", "product_reviews", "List TikTok Shop product reviews", "tiktok product_reviews"), ) } diff --git a/internal/command/root_sources_test.go b/internal/command/root_sources_test.go index d6a718a..880b95e 100644 --- a/internal/command/root_sources_test.go +++ b/internal/command/root_sources_test.go @@ -597,6 +597,70 @@ func TestTikTokGetVideoDetailCallsService(t *testing.T) { } } +func TestTikTokSearchProductsCallsService(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if maybeServeSchema(w, r) { + return + } + if r.URL.Path != "/v1/capabilities/tiktok.search_products" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + _, _ = w.Write([]byte(`{"ok":true,"data":[{"product_id":"p-1","product_name":"Labubu Plush","product_sold_count":10,"format_available_price":"$9.99","format_origin_price":"$12.99","discount":"20% off","rating":4.6,"review_count":42}],"meta":{"capability":"tiktok.search_products","cursor":"next-page","has_more":true}}`)) + })) + defer server.Close() + + var stdout bytes.Buffer + var stderr bytes.Buffer + + root := NewRoot(&stdout, &stderr) + exitCode, err := root.Execute(context.Background(), []string{ + "--base-url", server.URL, + "tiktok", "search_products", + "--keyword", "labubu", + "--region", "US", + }) + if err != nil { + t.Fatalf("execute root: %v", err) + } + if exitCode != 0 { + t.Fatalf("expected exit code 0, got %d, stderr=%s", exitCode, stderr.String()) + } + assertCompactTableOutputContains(t, stdout.String(), `"columns":["product_id","product_name","product_sold_count","format_available_price","format_origin_price","discount","rating","review_count"]`, `"p-1"`) +} + +func TestTikTokProductReviewsCallsService(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if maybeServeSchema(w, r) { + return + } + if r.URL.Path != "/v1/capabilities/tiktok.product_reviews" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + _, _ = w.Write([]byte(`{"ok":true,"data":[{"review_id":"r-1","rating":5,"verified_purchase":true,"like_count":3,"create_time":"2023-11-14T22:13:20Z","content":"Great!"}],"meta":{"capability":"tiktok.product_reviews","has_more":true,"total":120}}`)) + })) + defer server.Close() + + var stdout bytes.Buffer + var stderr bytes.Buffer + + root := NewRoot(&stdout, &stderr) + exitCode, err := root.Execute(context.Background(), []string{ + "--base-url", server.URL, + "tiktok", "product_reviews", + "--product-id", "prod-1", + "--sort", "latest", + "--media-filter", "media", + "--star", "5", + }) + if err != nil { + t.Fatalf("execute root: %v", err) + } + if exitCode != 0 { + t.Fatalf("expected exit code 0, got %d, stderr=%s", exitCode, stderr.String()) + } + assertCompactTableOutputContains(t, stdout.String(), `"columns":["review_id","rating","verified_purchase","like_count","create_time","content"]`, `"r-1"`) +} + func TestMetaAdsSearchCallsService(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if maybeServeSchema(w, r) { diff --git a/internal/command/schema_test_helper.go b/internal/command/schema_test_helper.go index a598da3..26de1bd 100644 --- a/internal/command/schema_test_helper.go +++ b/internal/command/schema_test_helper.go @@ -87,6 +87,11 @@ func testSchemas() map[string]schema.CapabilitySchema { "amazon.get_category_trend": amazonSchema("amazon.get_category_trend", []schema.CapabilityParam{{Name: "node_id", Type: "string", Required: true, FlagName: "node-id"}, {Name: "market", Type: "string"}, {Name: "trend_types", Type: "array", Required: true, ItemsType: "string", Encoding: "csv", FlagName: "trend-types"}}), "tiktok.search_videos": {Name: "tiktok.search_videos", Parameters: []schema.CapabilityParam{{Name: "keyword", Type: "string", Required: true}, {Name: "region", Type: "string"}, {Name: "sort_by", Type: "string", Enum: []string{"relevance", "likes", "date"}, FlagName: "sort-by"}, {Name: "publish_time", Type: "string", Enum: []string{"all", "1d", "1w", "1m", "3m", "6m"}, FlagName: "publish-time"}, {Name: "cursor", Type: "integer"}, {Name: "count", Type: "integer"}}}, "tiktok.get_video_detail": {Name: "tiktok.get_video_detail", Parameters: []schema.CapabilityParam{{Name: "share_url", Type: "string", FlagName: "share-url"}, {Name: "aweme_id", Type: "string", FlagName: "aweme-id"}, {Name: "region", Type: "string"}}}, + "tiktok.list_comments": {Name: "tiktok.list_comments", Parameters: []schema.CapabilityParam{{Name: "video_id", Type: "string", Required: true, FlagName: "video-id"}, {Name: "cursor", Type: "integer"}, {Name: "count", Type: "integer"}}}, + "tiktok.shop_products": {Name: "tiktok.shop_products", Parameters: []schema.CapabilityParam{{Name: "seller_id", Type: "string", Required: true, FlagName: "seller-id"}, {Name: "region", Type: "string"}, {Name: "sort", Type: "string", Enum: []string{"sale", "rec"}}, {Name: "top_n", Type: "integer", FlagName: "top-n"}}}, + "tiktok.shop_product_info": {Name: "tiktok.shop_product_info", Parameters: []schema.CapabilityParam{{Name: "product_id", Type: "string", Required: true, FlagName: "product-id"}, {Name: "region", Type: "string"}}}, + "tiktok.search_products": {Name: "tiktok.search_products", Parameters: []schema.CapabilityParam{{Name: "keyword", Type: "string", Required: true}, {Name: "region", Type: "string", Enum: []string{"US", "GB", "SG", "MY", "PH", "TH", "VN", "ID"}}, {Name: "cursor", Type: "string"}, {Name: "offset", Type: "integer"}, {Name: "count", Type: "integer"}}}, + "tiktok.product_reviews": {Name: "tiktok.product_reviews", Parameters: []schema.CapabilityParam{{Name: "product_id", Type: "string", Required: true, FlagName: "product-id"}, {Name: "region", Type: "string", Enum: []string{"US", "GB", "SG", "MY", "PH", "TH", "VN", "ID"}}, {Name: "page", Type: "integer"}, {Name: "sort", Type: "string", Enum: []string{"default", "latest"}}, {Name: "media_filter", Type: "string", Enum: []string{"all", "media", "verified"}, FlagName: "media-filter"}, {Name: "star", Type: "string", Enum: []string{"all", "1", "2", "3", "4", "5"}}, {Name: "count", Type: "integer"}}}, "meta_ads.search_ads": {Name: "meta_ads.search_ads", Parameters: []schema.CapabilityParam{{Name: "q", Type: "string", Required: true}, {Name: "country", Type: "string"}, {Name: "ad_type", Type: "string", Enum: []string{"all", "political_and_issue_ads", "housing_ads", "employment_ads", "credit_ads"}, FlagName: "ad-type"}, {Name: "active_status", Type: "string", Enum: []string{"active", "inactive", "all"}, FlagName: "active-status"}, {Name: "media_type", Type: "string", Enum: []string{"all", "video", "image", "meme", "image_and_meme", "none"}, FlagName: "media-type"}, {Name: "platforms", Type: "string"}, {Name: "start_date", Type: "string", FlagName: "start-date"}, {Name: "end_date", Type: "string", FlagName: "end-date"}, {Name: "next_page_token", Type: "string", FlagName: "next-page-token"}}}, "meta_ads.get_ad_detail": {Name: "meta_ads.get_ad_detail", Parameters: []schema.CapabilityParam{{Name: "ad_id", Type: "string", Required: true, FlagName: "ad-id"}}}, "douyin.search_videos": {Name: "douyin.search_videos", Parameters: []schema.CapabilityParam{{Name: "keyword", Type: "string", Required: true}, {Name: "sort_type", Type: "string", Enum: []string{"comprehensive", "likes", "latest"}, FlagName: "sort-type"}, {Name: "publish_time", Type: "string", Enum: []string{"all", "1d", "1w", "6m"}, FlagName: "publish-time"}, {Name: "filter_duration", Type: "string", Enum: []string{"all", "under_1m", "1m_5m", "over_5m"}, FlagName: "filter-duration"}, {Name: "content_type", Type: "string", Enum: []string{"all", "video", "image", "article"}, FlagName: "content-type"}, {Name: "cursor", Type: "integer"}}}, diff --git a/internal/output/projection_rules.go b/internal/output/projection_rules.go index a26b8ab..510cc9e 100644 --- a/internal/output/projection_rules.go +++ b/internal/output/projection_rules.go @@ -733,6 +733,46 @@ var projectionRules = map[string]projectionRule{ }, }, }, + "tiktok.search_products": { + Compact: projectionSpec{ + Tables: []tableRule{ + { + From: "$root", + To: "$root", + Limit: 10, + Columns: []fieldRule{ + {From: "product_id", To: "product_id"}, + {From: "product_name", To: "product_name"}, + {From: "product_sold_count", To: "product_sold_count"}, + {From: "format_available_price", To: "format_available_price"}, + {From: "format_origin_price", To: "format_origin_price"}, + {From: "discount", To: "discount"}, + {From: "rating", To: "rating"}, + {From: "review_count", To: "review_count"}, + }, + }, + }, + }, + }, + "tiktok.product_reviews": { + Compact: projectionSpec{ + Tables: []tableRule{ + { + From: "$root", + To: "$root", + Limit: 10, + Columns: []fieldRule{ + {From: "review_id", To: "review_id"}, + {From: "rating", To: "rating"}, + {From: "verified_purchase", To: "verified_purchase"}, + {From: "like_count", To: "like_count"}, + {From: "create_time", To: "create_time"}, + {From: "content", To: "content"}, + }, + }, + }, + }, + }, "xiaohongshu.get_note_comments": { Compact: projectionSpec{ Tables: []tableRule{ diff --git a/internal/output/projection_test.go b/internal/output/projection_test.go index 50da2e7..06c92f8 100644 --- a/internal/output/projection_test.go +++ b/internal/output/projection_test.go @@ -821,6 +821,8 @@ func TestProjectionRulesCoverAllAgentTestCapabilities(t *testing.T) { "reddit.get_post_comments", "tiktok.shop_products", "tiktok.shop_product_info", + "tiktok.search_products", + "tiktok.product_reviews", "xiaohongshu.get_note_comments", "douyin.get_comment_replies", } diff --git a/skills/apimux-tiktok/SKILL.md b/skills/apimux-tiktok/SKILL.md index ab3e50b..51afe50 100644 --- a/skills/apimux-tiktok/SKILL.md +++ b/skills/apimux-tiktok/SKILL.md @@ -1,7 +1,7 @@ --- name: apimux-tiktok version: 1.0.0 -description: "TikTok content and TikTok Shop data. Search videos, analyze comments, list shop products, and inspect product details for content research and commerce analysis." +description: "TikTok content and TikHub-backed TikTok Shop data. Search videos, analyze comments, list shop products, search products, browse reviews, and inspect product details for content research and commerce analysis." metadata: source: tiktok requires: @@ -11,7 +11,7 @@ metadata: # TikTok -Search TikTok content and inspect TikTok Shop product data. Use this for content research, creator/product analysis, shopping research, and cross-platform market validation. +Search TikTok content and inspect TikHub-backed TikTok Shop product data. Use this for content research, creator/product analysis, shopping research, and cross-platform market validation. **Before using:** Read [`../apimux-shared/SKILL.md`](../apimux-shared/SKILL.md) for response structure, error handling, pagination metadata, and CLI conventions. @@ -22,6 +22,8 @@ Search TikTok content and inspect TikTok Shop product data. Use this for content - **Analyze comments under a video** → `list_comments` - **List products from a TikTok Shop seller** → `shop_products` - **Inspect one TikTok Shop product** → `shop_product_info` +- **Search TikHub TikTok Shop products by keyword** → `search_products` +- **Browse TikHub TikTok Shop product reviews** → `product_reviews` - **Validate a market across sources** → use `search_videos` for content demand, then [`amazon.search_products`](../apimux-amazon/SKILL.md) for supply-side checks ## Available capabilities @@ -33,6 +35,15 @@ Search TikTok content and inspect TikTok Shop product data. Use this for content | `list_comments` | List video comments | Audience feedback and comment insights | | `shop_products` | List seller products | Seller and product-selection analysis | | `shop_product_info` | Get product details | Product research and cross-platform comparison | +| `search_products` | Search shop products by keyword | Keyword-level shop discovery and trend scouting | +| `product_reviews` | List product reviews | Product sentiment, rating distribution, UGC mining | + +## Common workflows + +- Unknown product ID: use `search_products` first, pick a `product_id`, then call `shop_product_info` or `product_reviews`. +- Product validation: use `search_products` to compare visible demand signals such as sold count, price, rating, and review count; use `product_reviews` to inspect buyer language and objections. +- Review mining: use `product_reviews --sort latest` for fresh buyer feedback, `--star 1` or `--star 2` for pain points, and `--media-filter media` when the user asks for visual proof or UGC examples. +- Cross-market checks: pass `--region` explicitly when comparing markets. Supported TikHub shop regions are `US`, `GB`, `SG`, `MY`, `PH`, `TH`, `VN`, and `ID`; `shop_products` remains seller-listing only and US-only. --- @@ -223,12 +234,13 @@ Get details for one TikTok Shop product. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `product_id` | string | Yes | Product ID | -| `region` | string | No | Only `US` is supported; default `US` | +| `region` | string | No | `US`, `GB`, `SG`, `MY`, `PH`, `TH`, `VN`, or `ID`; default `US` | ### CLI usage ```bash apimux tiktok shop_product_info --product-id "1729384756" +apimux tiktok shop_product_info --product-id "1729384756" --region "GB" ``` ### Response fields @@ -252,12 +264,127 @@ apimux tiktok shop_product_info --product-id "1729384756" ### Notes - `product_id` is required. -- `region` currently supports only `US`. +- `region` defaults to `US` and accepts the 8 supported markets listed above. - Missing products return `product_not_found`. --- +## tiktok.search_products + +Search TikHub TikTok Shop products by keyword. + +Use this when the user has a product category, trend term, brand, or item name but does not yet have a TikTok Shop `product_id`. This is the entry point for discovering products before fetching detail or reviews. + +### Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `keyword` | string | Yes | Search keyword | +| `region` | string | No | `US`, `GB`, `SG`, `MY`, `PH`, `TH`, `VN`, or `ID`; default `US` | +| `cursor` | string | No | Pagination token from previous `meta.cursor` | +| `offset` | integer | No | Result offset, `>= 0`; default `0` | +| `count` | integer | No | Number of products, `1..200`; default `20` | + +### CLI usage + +```bash +apimux tiktok search_products --keyword "labubu" +apimux tiktok search_products --keyword "labubu" --region "GB" --count 40 +apimux tiktok search_products --keyword "wireless microphone" --region "US" --cursor "NEXT_CURSOR" +``` + +### Response fields + +| Field | Type | Description | +|-------|------|-------------| +| `product_id` | string | Product ID | +| `product_name` | string | Product name | +| `product_cover` | string | Product cover image | +| `product_sold_count` | integer | Sold count | +| `format_available_price` | string | Current price text | +| `format_origin_price` | string | Original price text | +| `discount` | string | Discount text | +| `rating` | number | Product rating when available | +| `review_count` | integer | Review count when available | + +### Compact output + +Default compact output is columnar and keeps the first 10 rows with: + +`product_id`, `product_name`, `product_sold_count`, `format_available_price`, `format_origin_price`, `discount`, `rating`, `review_count`. + +### Notes + +- `keyword` is required. +- `region` must be one of the 8 supported markets. +- First-page requests send an empty `page_token` upstream; subsequent pages use `meta.cursor`. +- Pagination state is returned in `meta.cursor` and `meta.has_more`. +- Use `cursor` for token-based pagination when `meta.cursor` is present. Use `offset` only when the caller explicitly wants offset-based paging. + +--- + +## tiktok.product_reviews + +List TikHub reviews for one TikTok Shop product. + +Use this after obtaining a `product_id` from `search_products`, `shop_products`, or `shop_product_info`. This is the best command for sentiment analysis, purchase objections, quality issues, and customer wording. + +### Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `product_id` | string | Yes | TikTok Shop product ID | +| `region` | string | No | `US`, `GB`, `SG`, `MY`, `PH`, `TH`, `VN`, or `ID`; default `US` | +| `page` | integer | No | Page number starting at `1`; default `1` | +| `sort` | string | No | `default` or `latest`; default `default` | +| `media_filter` | string | No | `all`, `media`, or `verified`; default `all` | +| `star` | string | No | `all` or `1`..`5`; default `all` | +| `count` | integer | No | Number of reviews, `1..100`; default `20` | + +### CLI usage + +```bash +apimux tiktok product_reviews --product-id "1729556436942358002" +apimux tiktok product_reviews --product-id "1729556436942358002" --sort "latest" --star "5" --media-filter "media" +apimux tiktok product_reviews --product-id "1729556436942358002" --region "GB" --page 2 --count 50 +``` + +### Response fields + +| Field | Type | Description | +|-------|------|-------------| +| `review_id` | string | Review ID | +| `rating` | integer | Star rating, `1..5` | +| `content` | string | Review content | +| `create_time` | string | RFC3339 timestamp when available | +| `verified_purchase` | boolean | Verified purchase indicator | +| `like_count` | integer | Likes on this review | +| `medias` | object[] | Review media entries `{type, url}` | +| `author` | object | `user_id`, `nickname`, `avatar` | +| `seller_reply` | object | Seller reply payload when present | + +### Compact output + +Default compact output is columnar and keeps the first 10 rows with: + +`review_id`, `rating`, `verified_purchase`, `like_count`, `create_time`, `content`. + +### Notes + +- `product_id` is required. +- Use `star` to isolate sentiment: `1` and `2` for problems, `4` and `5` for positive language, `all` for the default mix. +- Use `media_filter=media` when the user needs reviews with images or videos; use `media_filter=verified` when the user asks for verified purchases. +- The underlying provider endpoint is documented for Americas and Europe (e.g. + `US`, `GB`). For Southeast Asia regions (`SG`, `MY`, `PH`, `TH`, `VN`, `ID`) + results may be empty. +- Pagination state is returned in `meta.has_more`, `meta.total`, and + `meta.cursor` (the current page number). +- Review summary is surfaced in `meta.resolved_filters.average_rating` and + `meta.resolved_filters.star_distribution` when present. + +--- + ## General notes - See [`../apimux-shared/SKILL.md`](../apimux-shared/SKILL.md) for response structure and error handling. -- TikTok Shop capabilities currently support only the US market. +- TikTok Shop capabilities support the TikHub region list: `US, GB, SG, MY, PH, TH, VN, ID`. `shop_products` currently remains US-only; the other shop capabilities accept the full list with `US` as the default.