
Linkfox Temu Promotion Us
- 168 installs
- 64 repo stars
- Updated August 3, 2026
- linkfox-ai/linkfox-skills
Helps with ai & agent building tasks.
About
linkfox-temu-promotion-us is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- linkfox-temu-promotion-us
- AI & Agent Building
- AI-coding skill
Linkfox Temu Promotion Us by the numbers
- 168 all-time installs (skills.sh)
- +35 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #3,153 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/linkfox-ai/linkfox-skills --skill linkfox-temu-promotion-usAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 168 |
|---|---|
| repo stars | ★ 64 |
| Last updated | August 3, 2026 |
| Repository | linkfox-ai/linkfox-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Temu 美国站 — 电商促销(Promotion)
本 skill(linkfox-temu-promotion-us)覆盖 Partner Platform for US Promotion / 电商促销(促销活动、优惠券、活动报名等;Partner 后台菜单可能显示为 Marketing/Promotion)相关 bg.* / temu.* 接口(menu_code 与各 sub_menu_code 以 Partner 后台为准,见 partner-us-catalog.md)。
已接入 6 条 Partner Promotion 接口,清单见 partner-us-catalog.md。
网关(本 skill 内置):
| 能力 | 方法 | 路径 |
|---|---|---|
促销 OpenAPI(us_promotion_*、temu_us_proxy) | POST | https://tool-gateway.linkfox.com/temu/proxy |
| 加签文件下载 | POST | https://tool-gateway.linkfox.com/temu/fileDownload |
相关 skill
| 场景 | skill |
|---|---|
| 促销/营销活动(本 skill) | `linkfox-temu-promotion-us` |
| 广告 Ads | linkfox-temu-ads-us |
| 商品列表/详情/编辑/库存/上下架 | linkfox-temu-manage-product-us |
| 发品 | linkfox-temu-add-product-us |
| 价格/供货价、定价单 | linkfox-temu-price-us |
| 订单列表/详情 | linkfox-temu-order-us |
| 退货与退款 | linkfox-temu-returns-refunds-us |
| 网关与 Temu token | 本 skill scripts/ |
API Usage
| 文档 | 内容 |
|---|---|
| api.md | 网关、鉴权、错误码、接入约定 |
| partner-us-catalog.md | 接口目录 + Partner URL + 脚本(随接入更新) |
| apis/README.md | 按接口分文件(apis/<type-slug>.md) |
默认参数
| 字段 | 默认 | 说明 |
|---|---|---|
| site | us | Partner US |
| managementType | semi-managed | 半托管(具体接口以 Partner 文档为准) |
| tokenPurpose | product-inventory | 与商品/促销场景一致(见 access-token.md) |
鉴权
1. LinkFox:LINKFOXAGENT_API_KEY → Header Authorization + Token 2. Temu:accessToken 或 storeKey(storeKey 时建议带 tokenPurpose=product-inventory)
Scripts
| 脚本 | 说明 |
|---|---|
temu_us_proxy.py | 任意 type |
temu_us_file_download.py | 加签下载 |
us_promotion_activity_query.py | bg.promotion.activity.query |
us_promotion_activity_candidate_goods_query.py | bg.promotion.activity.candidate.goods.query |
us_promotion_activity_goods_query.py | bg.promotion.activity.goods.query |
us_promotion_activity_goods_enroll.py | bg.promotion.activity.goods.enroll |
us_promotion_activity_goods_operation_query.py | bg.promotion.activity.goods.operation.query |
us_promotion_activity_goods_update.py | bg.promotion.activity.goods.update |
接入新接口(约定)
你每提供一条 Partner 文档(type + sub_menu_code + URL + Request/Response 参数表),将:
1. 新增 references/apis/<type-slug>.md(完整入参/出参层级) 2. 新增 scripts/us_promotion_<slug>.py(调用 _us_promotion_script.run_cli) 3. 更新 partner-us-catalog.md、apis/README.md 与本表
示例(通用代理)
export LINKFOXAGENT_API_KEY="<key>"
python scripts/temu_us_proxy.py '{
"accessToken": "TOKEN",
"tokenPurpose": "product-inventory",
"type": "<TEMU_API_TYPE>",
"params": {
"request": {}
}
}'Feedback: skillName:linkfox-temu-promotion-us
网关与授权脚本
| 脚本 | 说明 |
|---|---|
check_linkfox_token.py | 校验 LinkFox 用户 Token |
temu_token_guide.py | Temu accessToken 后台授权步骤 |
save_temu_access_token.py | 保存 accessToken 到本地 |
list_temu_access_tokens.py | 列出已保存 token |
get_temu_access_token.py | 读取已保存 token |
temu_proxy.py | 通用网关转发(多 site) |
temu_file_download.py | 加签文件下载(多 site) |
授权说明:references/access-token.md
<!-- LF_LARGE_RESPONSE_BLOCK -->
Handling Large Responses
To avoid overflowing the agent context, persist the response to disk and extract only the fields you need:
python scripts/response_io.py run --script scripts/check_linkfox_token.py --out-dir <DIR> '<params>'
python scripts/response_io.py read <file> --fields "<paths>" # or --path "<JMESPath>"Pick--out-diroutside any git working tree (e.g./tmp/...on Unix,%TEMP%/...on Windows). Persisted responses may contain PII, pricing, or auth-sensitive data — do not commit them. Files are not auto-deleted; clean up when the task is done.
This skill exposes multiple entry scripts:check_linkfox_token.py,get_temu_access_token.py,list_temu_access_tokens.py,save_temu_access_token.py,temu_file_download.py,temu_proxy.py,temu_token_guide.py,temu_us_file_download.py,temu_us_proxy.py,us_promotion_activity_candidate_goods_query.py,us_promotion_activity_goods_enroll.py,us_promotion_activity_goods_operation_query.py,us_promotion_activity_goods_query.py,us_promotion_activity_goods_update.py,us_promotion_activity_query.py. Pass--script scripts/<name>.pyto choose the one you need.
run writes the full response to a file and emits only a schema preview + file path. read projects specific fields, with --limit/--offset for slicing and --format json|jsonl|csv|table for output.
When to prefer this pattern — apply your judgment based on the response characteristics, e.g.:
- High field count per record, or fields you don't need
- Batch/paginated results (multiple items per call)
- Long-text fields (descriptions, reviews, HTML, time series)
- Output reused across later steps rather than consumed immediately
For small, single-use responses, calling the main script directly is fine.
⚠️ The preview is a truncated schema + sample, not the full data. Any field-level decision must read from the persisted file via read. <!-- /LF_LARGE_RESPONSE_BLOCK -->
Temu accessToken 授权与获取
Temu 没有 LinkFox 侧自动 OAuth;accessToken 须在 Temu 卖家后台「服务市场 → 授权管理」手动复制。与 LINKFOXAGENT_API_KEY(LinkFox 用户鉴权)是两套令牌。
两种鉴权(勿混淆)
| 令牌 | 用途 | 获取方式 |
|---|---|---|
| LinkFox 用户 Token | 调用 /temu/proxy、/temu/fileDownload 必填 | LINKFOXAGENT_API_KEY 或请求 JSON 的 token;Header Authorization + Token(同 amazon-store-auth) |
| Temu accessToken | Temu 业务 API(经紫鸟转发) | Temu 卖家后台授权后复制 |
export LINKFOXAGENT_API_KEY="<your-key>"
python scripts/check_linkfox_token.pytokenPurpose 与场景
| tokenPurpose | 店铺类型 | 推荐 site | 授权应用 |
|---|---|---|---|
product-inventory | 半托管 | cn / partner | 酷鸟卖家助手 |
order-shipping | 半托管 | us / global / eu | Cyber-ERP酷鸟助手 |
full-managed | 全托管 | cn / partner | 酷鸟卖家助手 |
local-native | 本土(美/欧主体) | us 等 | Cyber-ERP |
1. 半托管 — 商品/库存 Token
1. 登录 seller.kuajingmaihuo.com 或 agentseller.temu.com 2. 系统管理 → 服务市场 → 授权管理 3. 获取授权 → 选择 「酷鸟卖家助手」 4. 全选常规和特殊授权 → 确认 → 复制 access_token 5. 调用 API:site=cn 或 partner,managementType=semi-managed
2. 半托管 — 订单/发货 Token
1. 登录 Temu 卖家后台 2. 右上角 Seller Central → 切换到目标区域(美区 / 欧区 / 全球) 3. 服务市场 → 授权管理 4. 获取授权 → 选择 「Cyber-ERP酷鸟助手」 5. 全选授权 → 确认 → 复制 token 6. 调用 API:site=us / global / eu,managementType=semi-managed
3. 全托管店铺
1. 登录 Temu 平台 2. 系统管理 → 服务市场 → 授权管理 3. 获取授权 → 「酷鸟卖家助手」 → 全选 → 复制 token 4. 调用 API:managementType=full-managed,site 通常 cn 或 partner
4. 本土店铺(美区、欧区主体)
1. 登录 Temu 2. Apps And Services → Manage Your Apps 3. Authorize a new app → 搜索 「Cyber-ERP」 4. 一般权限与敏感权限全选 → 确认 → 复制 token
站点与 OpenAPI 网关
| site | 说明 | Temu 网关 |
|---|---|---|
| cn | 中国站 | openapi.kuajingmaihuo.com |
| partner | Partner 网关 | openapi-b-partner.temu.com |
| us | 美国站 | openapi-b-us.temu.com |
| global | 全球区 | openapi-b-global.temu.com |
| eu | 欧洲站 | openapi-b-eu.temu.com |
本地保存 Token(推荐)
默认存储:~/.linkfox/temu-access-tokens.json(可用 TEMU_TOKEN_STORE_PATH 覆盖)。
# 查看授权步骤
python scripts/temu_token_guide.py '{"shopType":"semi-managed","tokenPurpose":"product-inventory","site":"cn"}'
# 保存 token
python scripts/save_temu_access_token.py '{
"storeKey": "my-shop",
"label": "中国半托管",
"site": "cn",
"managementType": "semi-managed",
"tokenPurpose": "product-inventory",
"accessToken": "PASTE_TOKEN"
}'
# 列出已保存(脱敏)
python scripts/list_temu_access_tokens.py
# 用 storeKey 调 API(无需每次粘贴 token)
python scripts/temu_proxy.py '{
"storeKey": "my-shop",
"site": "cn",
"managementType": "semi-managed",
"tokenPurpose": "product-inventory",
"type": "bg.goods.category.mapping",
"params": {"goodsName": "测试", "goodsNameEn": "Test"}
}'注意事项
1. 子账号:可能无法进入服务市场,需主账号授权。 2. 多站点:美国站、英国站等需分别登录对应后台获取 Token。 3. 有效期:Token 会过期,过期后重新在后台复制并 save_temu_access_token.py。 4. IP 白名单:需联系紫鸟开放平台配置调用 IP。 5. 参数小写:site、managementType 必须小写。
脚本一览
| 脚本 | 说明 |
|---|---|
temu_token_guide.py | 按店铺类型/用途输出授权步骤 |
save_temu_access_token.py | 保存 accessToken 到本地 |
list_temu_access_tokens.py | 列出已保存 token(默认脱敏) |
get_temu_access_token.py | 读取指定 store 的 token |
linkfox-temu-promotion-us — API 参考
Temu 美国站电商促销(Partner US Promotion / 促销活动等),经本 skill temu_us_proxy(POST /temu/proxy) 转发。Temu 的 type 写在 Body,不是 URL 路径。
网关与鉴权:本 skillscripts/(LINKFOXAGENT_API_KEY、accessToken/storeKey)。授权见references/access-token.md。
---
调用规范
| 项 | 说明 |
|---|---|
| 网关根地址 | https://tool-gateway.linkfox.com(可用 TEMU_API_BASE_URL / STORE_API_BASE_URL 覆盖) |
| 促销 OpenAPI | POST /temu/proxy |
| 加签文件下载 | POST /temu/fileDownload(temu_us_file_download.py) |
| LinkFox 鉴权 | Header `Authorization` 与 `Token`(同值);或 LINKFOXAGENT_API_KEY;或 JSON token |
| Temu 鉴权 | Body accessToken,或 storeKey + site + managementType + tokenPurpose |
| 默认 | site=us,managementType=semi-managed,tokenPurpose=product-inventory |
| 上游 OpenAPI(US) | https://openapi-b-us.temu.com/openapi/router(网关按 site 解析) |
网关请求 Body(/temu/proxy)
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| site | string | 是 | us(本 skill 默认) |
| managementType | string | 是 | semi-managed |
| accessToken | string | 与 storeKey 二选一 | Temu 店铺令牌 |
| storeKey | string | 与 accessToken 二选一 | ~/.linkfox/temu-access-tokens.json 中的键 |
| tokenPurpose | string | 否 | 建议 `product-inventory` |
| type | string | 是 | Temu 接口名,如 Partner Promotion 菜单下的 bg.* / temu.* |
| params | object | 否 | 业务参数;多数接口业务块在 `params.request` |
网关响应
| 字段 | 类型 | 说明 |
|---|---|---|
| body | string | Temu 原始 JSON 字符串;脚本解析为 temuBody |
| code | integer | 网关错误码:1002 参数/Token,1003 转发失败 |
解析顺序:网关 `code` → JSON.parse(body) → success / errorCode / errorMsg / result。
脚本调用
export LINKFOXAGENT_API_KEY="<key>"
python scripts/temu_us_proxy.py '{"accessToken":"TOKEN","tokenPurpose":"product-inventory","type":"<API_TYPE>","params":{"request":{}}}'业务字段可放在顶层或嵌套 params;含 request 时通常整体作为 params 转发。
---
接口一览
完整 sub_menu_code 与 Partner 文档 URL 见 partner-us-catalog.md。
每个接口单独一份文档:apis/README.md(随接入递增)。
| type | 说明 | 文档 |
|---|---|---|
bg.promotion.activity.query | 促销活动查询 | apis/bg-promotion-activity-query.md |
bg.promotion.activity.candidate.goods.query | 活动候选商品查询 | apis/bg-promotion-activity-candidate-goods-query.md |
bg.promotion.activity.goods.query | 活动已报名商品查询 | apis/bg-promotion-activity-goods-query.md |
bg.promotion.activity.goods.enroll | 活动商品报名 | apis/bg-promotion-activity-goods-enroll.md |
bg.promotion.activity.goods.operation.query | 活动商品操作结果查询 | apis/bg-promotion-activity-goods-operation-query.md |
bg.promotion.activity.goods.update | 活动商品更新 | apis/bg-promotion-activity-goods-update.md |
---
典型促销流程
1. bg.promotion.activity.query → 查可参加/已参加的活动
2. bg.promotion.activity.candidate.goods.query → 查候选商品与推荐活动价
3. bg.promotion.activity.goods.enroll → 报名(得 draftId)
4. bg.promotion.activity.goods.operation.query → 轮询报名/更新结果
5. bg.promotion.activity.goods.query → 查已报名商品与活动价
6. bg.promotion.activity.goods.update → 改价/改量/下架/加 SKU---
与其他 Temu US skill 的区分
| 能力 | skill |
|---|---|
| 促销(本 skill) | `linkfox-temu-promotion-us` |
| 商品管理(列表/编辑/库存/上下架) | linkfox-temu-manage-product-us |
| 价格/供货价 | linkfox-temu-price-us |
| 发品 | linkfox-temu-add-product-us |
| 订单 | linkfox-temu-order-us |
---
网关错误码
| code | 说明 | 处理 |
|---|---|---|
| 1002 | 参数或 LinkFox Token 无效 | 修正参数与 LINKFOXAGENT_API_KEY |
| 1003 | 转发失败 | 检查 Temu token、tokenPurpose、白名单、网络 |
---
Feedback API
- POST
https://skill-api.linkfox.com/api/v1/public/feedback skillName: `linkfox-temu-promotion-us`
活动候选商品查询 — bg.promotion.activity.candidate.goods.query
| 项 | 值 |
|---|---|
| 脚本 | scripts/us_promotion_activity_candidate_goods_query.py |
| Partner 文档 | https://partner-us.temu.com/documentation?menu_code=873ac072a78249c893e5f8d0e656a11f&sub_menu_code=0a11814e7d4146b595918ff3c0f3e239 |
| 网关 | POST /temu/proxy,type=bg.promotion.activity.candidate.goods.query,业务载荷放在 Body 的 params |
Description: Query candidate goods for a promotion activity.
`activityId`、`pageNumber`、`pageSize` 均为必填。
返回 `isAllowedEnroll` 与推荐活动价/数量,用于报名前选品。
网关鉴权字段由本 skill 网关脚本处理;业务参数见下方 `request`。建议使用 `tokenPurpose=product-inventory`,`managementType=semi-managed`。
---
Request 结构(官方顶层)
params
└── request (OBJECT, 选填)
├── activityId (LONG, 必填)
├── pageNumber (INTEGER, 必填)
└── pageSize (INTEGER, 必填)request 内字段
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| activityId | LONG | 是 | unique identifier for the activity |
| pageNumber | INTEGER | 是 | page number for pagination, default is 1. |
| pageSize | INTEGER | 是 | Page size for pagination, default is 10, max is 100. |
官方 Request 表将顶层 `request` 标为选填(False);标 必填 的字段须在 `params.request` 中提供。
网关 params 写法
{
"accessToken": "TOKEN",
"tokenPurpose": "product-inventory",
"request": {
"activityId": 123456,
"pageNumber": 1,
"pageSize": 20
}
}---
Response(Temu body 解析后)
Partner Response 表嵌套子行在导出 HTML 中多为折叠状态,下列层级按 Response 表 + Response Example 全部展开。
response
├── success / errorCode / errorMsg
└── result
├── total (LONG)
├── activityInfo (OBJECT)
│ └── activityId, activityName, activityType, activityStatus, …
└── goodsList[]
├── goodsId (LONG)
├── isAllowedEnroll (BOOLEAN)
├── recommendActivityQuantity (INTEGER)
└── skuList[]
├── skuId (LONG)
└── recommendActivitySupplierPrice (LONG)顶层字段(response 内,或根级)
| 参数 | 类型 | 说明 |
|---|---|---|
| success | BOOLEAN | success(当前请求是否成功) |
| errorCode | INTEGER | error code |
| errorMsg | STRING | error message |
| result | OBJECT 或 OBJECT[] | 业务结果(见下表;goods.operation.query 的 result 为数组) |
result
| 参数 | 类型 | 说明 |
|---|---|---|
| total | LONG | Total records(总记录数) |
| activityInfo | OBJECT | Activity information(当前活动信息) |
| goodsList | OBJECT[] | Candidate goods list(候选商品列表) |
activityInfo
| 参数 | 类型 | 说明 |
|---|---|---|
| activityId | LONG | unique identifier for the activity |
| activityName | STRING | Activity name |
| activityType | INTEGER | Activity type |
| activityStatus | INTEGER | Activity status(1 Not started;2 Ongoing;3 Ended) |
| activityStartTime | LONG | Start time in seconds |
| activityEndTime | LONG | End time in seconds |
| isJoinedActivity | BOOLEAN | Whether joined |
goodsList[]
| 参数 | 类型 | 说明 |
|---|---|---|
| goodsId | LONG | goods id |
| isAllowedEnroll | BOOLEAN | Whether the goods is allowed to enroll(是否允许报名) |
| recommendActivityQuantity | INTEGER | Recommended activity quantity(推荐活动数量) |
| skuList | OBJECT[] | SKU list with recommended prices |
goodsList[].skuList[]
| 参数 | 类型 | 说明 |
|---|---|---|
| skuId | LONG | sku id |
| recommendActivitySupplierPrice | LONG | Recommended activity supplier price(推荐活动供货价) |
---
Error Code(Partner 表)
| errorCode | errorMsg | Error SOP / 处理建议 |
|---|---|---|
| 220010001 | parameter is illegal | 见 Partner 文档;修正入参或活动状态后重试 |
| 220010002 | system error, please try again later | 见 Partner 文档;修正入参或活动状态后重试 |
| 220010003 | The activity has been cancelled, please select another activity to participate | 见 Partner 文档;修正入参或活动状态后重试 |
| 220010004 | The activity has ended, please select another activity to participate | 见 Partner 文档;修正入参或活动状态后重试 |
---
脚本
export LINKFOXAGENT_API_KEY="<key>"
python scripts/us_promotion_activity_candidate_goods_query.py '{"accessToken": "TOKEN", "tokenPurpose": "product-inventory", "request": {"activityId": 123456, "pageNumber": 1, "pageSize": 20}}'典型流程: 在 activity.query 取得 `activityId` 后,查询可报名商品与推荐价。
活动商品报名 — bg.promotion.activity.goods.enroll
| 项 | 值 |
|---|---|
| 脚本 | scripts/us_promotion_activity_goods_enroll.py |
| Partner 文档 | https://partner-us.temu.com/documentation?menu_code=873ac072a78249c893e5f8d0e656a11f&sub_menu_code=27a87ec9d0d94273a48096c050f17854 |
| 网关 | POST /temu/proxy,type=bg.promotion.activity.goods.enroll,业务载荷放在 Body 的 params |
Description: Enroll goods into a promotion activity.
`activityId`、`enrollGoods` 必填。
`enrollGoods` 子字段在 Partner Request 表中为折叠行,按 Request Example 展开。
返回 `draftId`,可用 goods.operation.query 轮询 `operationStatus`。
网关鉴权字段由本 skill 网关脚本处理;业务参数见下方 `request`。建议使用 `tokenPurpose=product-inventory`,`managementType=semi-managed`。
---
Request 结构(官方顶层)
params
└── request (OBJECT, 选填)
├── activityId (LONG, 必填)
└── enrollGoods (OBJECT, 必填)
├── goodsId (LONG, 必填)
├── activityQuantity (LONG, 必填)
├── traceCode (STRING, 否)
└── enrollSkuList[] (OBJECT[], 必填)
├── skuId (LONG)
└── activitySupplierPrice (LONG)request 内字段
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| activityId | LONG | 是 | unique identifier for the activity |
| enrollGoods | OBJECT | 是 | goods information required for activity registration |
enrollGoods(OBJECT,必填)
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| goodsId | LONG | 是 | goods id |
| activityQuantity | LONG/INTEGER | 是 | Activity quantity for enrollment(报名活动数量) |
| traceCode | STRING | 否 | Idempotent key(幂等键,单次操作长度不超过 32 字符,Partner Request Example) |
| enrollSkuList | OBJECT[] | 是 | SKU list to enroll |
enrollGoods.enrollSkuList[]
| 参数 | 类型 | 说明 |
|---|---|---|
| skuId | LONG | sku id |
| activitySupplierPrice | LONG | Activity supplier price for this SKU(该 SKU 活动供货价) |
官方 Request 表将顶层 `request` 标为选填(False);标 必填 的字段须在 `params.request` 中提供。
网关 params 写法
{
"accessToken": "TOKEN",
"tokenPurpose": "product-inventory",
"request": {
"activityId": 123456,
"enrollGoods": {
"goodsId": 100001,
"activityQuantity": 50,
"traceCode": "enroll-001",
"enrollSkuList": [
{
"skuId": 200001,
"activitySupplierPrice": 1999
}
]
}
}
}---
Response(Temu body 解析后)
Partner Response 表嵌套子行在导出 HTML 中多为折叠状态,下列层级按 Response 表 + Response Example 全部展开。
response
├── success / errorCode / errorMsg
└── result
├── draftId (LONG)
├── operationStatus (INTEGER)
├── failReason (STRING)
└── goodsId (LONG)顶层字段(response 内,或根级)
| 参数 | 类型 | 说明 |
|---|---|---|
| success | BOOLEAN | success(当前请求是否成功) |
| errorCode | INTEGER | error code |
| errorMsg | STRING | error message |
| result | OBJECT 或 OBJECT[] | 业务结果(见下表;goods.operation.query 的 result 为数组) |
result
| 参数 | 类型 | 说明 |
|---|---|---|
| draftId | LONG | goods registration activity draft id(报名草稿 ID;前置流程成功时生成,用于 goods.operation.query) |
| operationStatus | INTEGER | Operation status(操作状态码;异步处理时请轮询 operation.query) |
| failReason | STRING | Failure reason when operation failed(失败原因) |
| goodsId | LONG | goods id |
---
Error Code(Partner 表)
| errorCode | errorMsg | Error SOP / 处理建议 |
|---|---|---|
| 220010001 | parameter is illegal | 见 Partner 文档;修正入参或活动状态后重试 |
| 220010002 | system error, please try again later | 见 Partner 文档;修正入参或活动状态后重试 |
| 220010003 | The activity has been cancelled, please select another activity to participate | 见 Partner 文档;修正入参或活动状态后重试 |
| 220010004 | The activity has ended, please select another activity to participate | 见 Partner 文档;修正入参或活动状态后重试 |
---
脚本
export LINKFOXAGENT_API_KEY="<key>"
python scripts/us_promotion_activity_goods_enroll.py '{"accessToken": "TOKEN", "tokenPurpose": "product-inventory", "request": {"activityId": 123456, "enrollGoods": {"goodsId": 100001, "activityQuantity": 50, "traceCode": "enroll-001", "enrollSkuList": [{"skuId": 200001, "activitySupplierPrice": 1999}]}}}'典型流程: 报名 → 保存 `draftId` → goods.operation.query 确认结果。
活动商品操作结果查询 — bg.promotion.activity.goods.operation.query
| 项 | 值 |
|---|---|
| 脚本 | scripts/us_promotion_activity_goods_operation_query.py |
| Partner 文档 | https://partner-us.temu.com/documentation?menu_code=873ac072a78249c893e5f8d0e656a11f&sub_menu_code=57a37eb5dd104e3f9f90118e3276b291 |
| 网关 | POST /temu/proxy,type=bg.promotion.activity.goods.operation.query,业务载荷放在 Body 的 params |
Description: Query promotion goods operation result by draft ID.
`draftIdList` 为必填(来自 enroll/update 返回的 `draftId`)。
Partner Response Example 中 `result` 为 OBJECT[](非带 total 的对象包装)。网关鉴权字段由本 skill 网关脚本处理;业务参数见下方 `request`。建议使用 `tokenPurpose=product-inventory`,`managementType=semi-managed`。
---
Request 结构(官方顶层)
params
└── request (OBJECT, 选填)
└── draftIdList (LONG[], 必填)request 内字段
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| draftIdList | LONG[] | 是 | goods registration activity draft id, which will be generated when the pre-procedure is successfully completed. |
官方 Request 表将顶层 `request` 标为选填(False);标 必填 的字段须在 `params.request` 中提供。
网关 params 写法
{
"accessToken": "TOKEN",
"tokenPurpose": "product-inventory",
"request": {
"draftIdList": [
10001,
10002
]
}
}---
Response(Temu body 解析后)
Partner Response 表嵌套子行在导出 HTML 中多为折叠状态,下列层级按 Response 表 + Response Example 全部展开。
response
├── success / errorCode / errorMsg
└── result[]
├── draftId (LONG)
├── operationStatus (INTEGER)
├── failReason (STRING)
└── goodsId (LONG)顶层字段(response 内,或根级)
| 参数 | 类型 | 说明 |
|---|---|---|
| success | BOOLEAN | success(当前请求是否成功) |
| errorCode | INTEGER | error code |
| errorMsg | STRING | error message |
| result | OBJECT 或 OBJECT[] | 业务结果(见下表;goods.operation.query 的 result 为数组) |
result(OBJECT[])
Partner Response Example 中 `result` 直接为数组。每个元素:
| 参数 | 类型 | 说明 |
|---|---|---|
| draftId | LONG | Draft id |
| operationStatus | INTEGER | Operation status(操作状态码) |
| failReason | STRING | Failure reason |
| goodsId | LONG | goods id |
---
Error Code(Partner 表)
| errorCode | errorMsg | Error SOP / 处理建议 |
|---|---|---|
| 220010001 | parameter is illegal | 见 Partner 文档;修正入参或活动状态后重试 |
| 220010002 | system error, please try again later | 见 Partner 文档;修正入参或活动状态后重试 |
---
脚本
export LINKFOXAGENT_API_KEY="<key>"
python scripts/us_promotion_activity_goods_operation_query.py '{"accessToken": "TOKEN", "tokenPurpose": "product-inventory", "request": {"draftIdList": [10001, 10002]}}'典型流程: 轮询 goods.enroll / goods.update 提交的 `draftId`。
活动已报名商品查询 — bg.promotion.activity.goods.query
| 项 | 值 |
|---|---|
| 脚本 | scripts/us_promotion_activity_goods_query.py |
| Partner 文档 | https://partner-us.temu.com/documentation?menu_code=873ac072a78249c893e5f8d0e656a11f&sub_menu_code=08f3f87d05a24bac882732141e0d9672 |
| 网关 | POST /temu/proxy,type=bg.promotion.activity.goods.query,业务载荷放在 Body 的 params |
Description: Query enrolled goods in a promotion activity.
`activityId`、`pageNumber`、`pageSize` 均为必填。
网关鉴权字段由本 skill 网关脚本处理;业务参数见下方 `request`。建议使用 `tokenPurpose=product-inventory`,`managementType=semi-managed`。
---
Request 结构(官方顶层)
params
└── request (OBJECT, 选填)
├── activityId (LONG, 必填)
├── pageNumber (INTEGER, 必填)
└── pageSize (INTEGER, 必填)request 内字段
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| activityId | LONG | 是 | unique identifier for the activity |
| pageNumber | INTEGER | 是 | page number for pagination, default is 1. |
| pageSize | INTEGER | 是 | Page size for pagination, default is 10, max is 100. |
官方 Request 表将顶层 `request` 标为选填(False);标 必填 的字段须在 `params.request` 中提供。
网关 params 写法
{
"accessToken": "TOKEN",
"tokenPurpose": "product-inventory",
"request": {
"activityId": 123456,
"pageNumber": 1,
"pageSize": 20
}
}---
Response(Temu body 解析后)
Partner Response 表嵌套子行在导出 HTML 中多为折叠状态,下列层级按 Response 表 + Response Example 全部展开。
response
├── success / errorCode / errorMsg
└── result
├── total (LONG)
├── activityInfo (OBJECT)
└── goodsList[]
├── goodsId (LONG)
├── activityQuantity (INTEGER)
├── remainingActivityQuantity (INTEGER)
└── skuList[]
├── skuId (LONG)
└── activitySupplierPrice (LONG)顶层字段(response 内,或根级)
| 参数 | 类型 | 说明 |
|---|---|---|
| success | BOOLEAN | success(当前请求是否成功) |
| errorCode | INTEGER | error code |
| errorMsg | STRING | error message |
| result | OBJECT 或 OBJECT[] | 业务结果(见下表;goods.operation.query 的 result 为数组) |
result
| 参数 | 类型 | 说明 |
|---|---|---|
| total | LONG | Total records |
| activityInfo | OBJECT | Activity information(字段同 candidate.goods.query 之 activityInfo) |
| goodsList | OBJECT[] | Enrolled goods list(已报名商品列表) |
goodsList[]
| 参数 | 类型 | 说明 |
|---|---|---|
| goodsId | LONG | goods id |
| activityQuantity | INTEGER | Activity quantity(活动数量/名额) |
| remainingActivityQuantity | INTEGER | Remaining activity quantity(剩余活动数量) |
| skuList | OBJECT[] | Enrolled SKU list |
goodsList[].skuList[]
| 参数 | 类型 | 说明 |
|---|---|---|
| skuId | LONG | sku id |
| activitySupplierPrice | LONG | Activity supplier price(活动供货价) |
---
Error Code(Partner 表)
| errorCode | errorMsg | Error SOP / 处理建议 |
|---|---|---|
| 220010001 | parameter is illegal | 见 Partner 文档;修正入参或活动状态后重试 |
| 220010002 | system error, please try again later | 见 Partner 文档;修正入参或活动状态后重试 |
| 220010003 | The activity has been cancelled, please select another activity to participate | 见 Partner 文档;修正入参或活动状态后重试 |
| 220010004 | The activity has ended, please select another activity to participate | 见 Partner 文档;修正入参或活动状态后重试 |
---
脚本
export LINKFOXAGENT_API_KEY="<key>"
python scripts/us_promotion_activity_goods_query.py '{"accessToken": "TOKEN", "tokenPurpose": "product-inventory", "request": {"activityId": 123456, "pageNumber": 1, "pageSize": 20}}'典型流程: 查看已报名商品、活动价与剩余名额。
活动商品更新 — bg.promotion.activity.goods.update
| 项 | 值 |
|---|---|
| 脚本 | scripts/us_promotion_activity_goods_update.py |
| Partner 文档 | https://partner-us.temu.com/documentation?menu_code=873ac072a78249c893e5f8d0e656a11f&sub_menu_code=29959238217c41f38f5904e32bf1d14f |
| 网关 | POST /temu/proxy,type=bg.promotion.activity.goods.update,业务载荷放在 Body 的 params |
Description: Update enrolled promotion activity goods.
`activityId`、`goodsId`、`operateType` 必填。
`operateType` 决定必填子参数(见下表);`updateSkuList`/`addSkuList` 按 Request Example 展开。
网关鉴权字段由本 skill 网关脚本处理;业务参数见下方 `request`。建议使用 `tokenPurpose=product-inventory`,`managementType=semi-managed`。
---
Request 结构(官方顶层)
params
└── request (OBJECT, 选填)
├── traceCode (STRING, 否)
├── activityId (LONG, 必填)
├── activityQuantity (LONG, 否)
├── goodsId (LONG, 必填)
├── operateType (INTEGER, 必填)
├── updateSkuList[] (OBJECT[], 否)
│ ├── skuId (LONG)
│ └── activitySupplierPrice (LONG)
└── addSkuList[] (OBJECT[], 否)
├── skuId (LONG)
└── activitySupplierPrice (LONG)request 内字段
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| traceCode | STRING | 否 | Optional. The traceCode is an Idempotent Key for single operations. Its length cannot exceed 32 characters. |
| activityId | LONG | 是 | unique identifier for the activity |
| activityQuantity | LONG | 否 | activity quantity, the quantity you set for participating activities. This is independent of product quantity. The updated activity quantity must be more than the original one. |
| goodsId | LONG | 是 | goods id |
| operateType | INTEGER | 是 | the type of operation. It determines which parameters are required. For example, if the operation type is 20, then the "activityQuantity" parameter must not be null, while all other parameters should be null. If the operation type is 30, then all other parameters must be null. 10 - update activity supplier price 20 - update activity quantity 30 - deactivate activity goods 40 - add activity sku |
| updateSkuList | OBJECT[] | 否 | sku information required for updating activity sku information |
| addSkuList | OBJECT[] | 否 | sku information required for adding activity sku information |
operateType(必填)
| 值 | 说明 | 必填参数 |
|---|---|---|
10 | update activity supplier price | updateSkuList(其他参数应为 null) |
20 | update activity quantity | activityQuantity(须大于原活动数量;其他参数应为 null) |
30 | deactivate activity goods | 其他参数应为 null |
40 | add activity sku | addSkuList |
updateSkuList[] / addSkuList[](Request Example 展开)
| 参数 | 类型 | 说明 |
|---|---|---|
| skuId | LONG | sku id |
| activitySupplierPrice | LONG | Activity supplier price |
官方 Request 表将顶层 `request` 标为选填(False);标 必填 的字段须在 `params.request` 中提供。
网关 params 写法
{
"accessToken": "TOKEN",
"tokenPurpose": "product-inventory",
"request": {
"activityId": 123456,
"goodsId": 100001,
"operateType": 10,
"traceCode": "update-001",
"updateSkuList": [
{
"skuId": 200001,
"activitySupplierPrice": 1899
}
]
}
}---
Response(Temu body 解析后)
Partner Response 表嵌套子行在导出 HTML 中多为折叠状态,下列层级按 Response 表 + Response Example 全部展开。
response
├── success / errorCode / errorMsg
└── result
├── draftId (LONG)
├── operationStatus (INTEGER)
├── failReason (STRING)
└── goodsId (LONG)顶层字段(response 内,或根级)
| 参数 | 类型 | 说明 |
|---|---|---|
| success | BOOLEAN | success(当前请求是否成功) |
| errorCode | INTEGER | error code |
| errorMsg | STRING | error message |
| result | OBJECT 或 OBJECT[] | 业务结果(见下表;goods.operation.query 的 result 为数组) |
result
| 参数 | 类型 | 说明 |
|---|---|---|
| draftId | LONG | Draft id for async operation |
| operationStatus | INTEGER | Operation status |
| failReason | STRING | Failure reason |
| goodsId | LONG | goods id |
---
Error Code(Partner 表)
| errorCode | errorMsg | Error SOP / 处理建议 |
|---|---|---|
| 220010001 | parameter is illegal | 见 Partner 文档;修正入参或活动状态后重试 |
| 220010002 | system error, please try again later | 见 Partner 文档;修正入参或活动状态后重试 |
| 220010003 | The activity has been cancelled, please select another activity to participate | 见 Partner 文档;修正入参或活动状态后重试 |
| 220010004 | The activity has ended, please select another activity to participate | 见 Partner 文档;修正入参或活动状态后重试 |
---
脚本
export LINKFOXAGENT_API_KEY="<key>"
python scripts/us_promotion_activity_goods_update.py '{"accessToken": "TOKEN", "tokenPurpose": "product-inventory", "request": {"activityId": 123456, "goodsId": 100001, "operateType": 10, "traceCode": "update-001", "updateSkuList": [{"skuId": 200001, "activitySupplierPrice": 1899}]}}'典型流程: 按 `operateType` 更新价/量/下架/加 SKU → 用 goods.operation.query 查 `draftId` 结果。
促销活动查询 — bg.promotion.activity.query
| 项 | 值 |
|---|---|
| 脚本 | scripts/us_promotion_activity_query.py |
| Partner 文档 | https://partner-us.temu.com/documentation?menu_code=873ac072a78249c893e5f8d0e656a11f&sub_menu_code=05820fed7179430c8e353905692d51b6 |
| 网关 | POST /temu/proxy,type=bg.promotion.activity.query,业务载荷放在 Body 的 params |
Description: Query promotion activities.
入参 `activityType` 为必填;分页使用 `pageNumber` / `pageSize`(与部分订单接口的 pageNo 不同)。可用 `activityStartTime`/`activityEndTime`、`activityStatus`、`activityIdList`、`onlyQueryJoinedActivity` 筛选。
网关鉴权字段由本 skill 网关脚本处理;业务参数见下方 `request`。建议使用 `tokenPurpose=product-inventory`,`managementType=semi-managed`。
---
Request 结构(官方顶层)
params
└── request (OBJECT, 选填)
├── pageNumber (INTEGER, 必填)
├── activityEndTime (LONG, 否)
├── activityIdList (LONG[], 否)
├── activityStatus (INTEGER, 否)
├── pageSize (INTEGER, 必填)
├── activityStartTime (LONG, 否)
├── activityType (INTEGER, 必填)
└── onlyQueryJoinedActivity (BOOLEAN, 否)request 内字段
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| pageNumber | INTEGER | 是 | page number for pagination, default is 1. |
| activityEndTime | LONG | 否 | end time for querying activity, in seconds. |
| activityIdList | LONG[] | 否 | unique identifier for the activity |
| activityStatus | INTEGER | 否 | the status of activity 1 - Not started 2 - Ongoing 3 - Ended |
| pageSize | INTEGER | 是 | Page size for pagination, default is 10, max is 100. |
| activityStartTime | LONG | 否 | start time for querying activity, in seconds. |
| activityType | INTEGER | 是 | the type of activity 2 - lightning deals 13 - advanced big sale 27 - Clearance deals 100 - official big sale |
| onlyQueryJoinedActivity | BOOLEAN | 否 | whether to query only joined activities. TRUE / FALSE |
activityType(必填)
| 值 | 说明 |
|---|---|
2 | lightning deals |
13 | advanced big sale |
27 | Clearance deals |
100 | official big sale |
activityStatus(入参筛选)
| 值 | 说明 |
|---|---|
1 | Not started |
2 | Ongoing |
3 | Ended |
官方 Request 表将顶层 `request` 标为选填(False);标 必填 的字段须在 `params.request` 中提供。
网关 params 写法
{
"accessToken": "TOKEN",
"tokenPurpose": "product-inventory",
"request": {
"pageNumber": 1,
"pageSize": 20,
"activityType": 2,
"activityStatus": 2,
"onlyQueryJoinedActivity": false
}
}---
Response(Temu body 解析后)
Partner Response 表嵌套子行在导出 HTML 中多为折叠状态,下列层级按 Response 表 + Response Example 全部展开。
response
├── success / errorCode / errorMsg
└── result
├── total (LONG)
└── activityList[]
├── activityId (LONG)
├── activityName (STRING)
├── activityType (INTEGER)
├── activityStatus (INTEGER)
├── activityStartTime (LONG)
├── activityEndTime (LONG)
└── isJoinedActivity (BOOLEAN)顶层字段(response 内,或根级)
| 参数 | 类型 | 说明 |
|---|---|---|
| success | BOOLEAN | success(当前请求是否成功) |
| errorCode | INTEGER | error code |
| errorMsg | STRING | error message |
| result | OBJECT 或 OBJECT[] | 业务结果(见下表;goods.operation.query 的 result 为数组) |
result
| 参数 | 类型 | 说明 |
|---|---|---|
| total | LONG | Total number of matching activities(匹配活动总数) |
| activityList | OBJECT[] | Activity list(活动列表) |
activityList[]
| 参数 | 类型 | 说明 |
|---|---|---|
| activityId | LONG | unique identifier for the activity(活动 ID) |
| activityName | STRING | Activity name(活动名称) |
| activityType | INTEGER | the type of activity(活动类型),见入参 activityType 枚举 |
| activityStatus | INTEGER | the status of activity(活动状态):1 Not started;2 Ongoing;3 Ended |
| activityStartTime | LONG | Activity start time in seconds(活动开始时间,秒级时间戳) |
| activityEndTime | LONG | Activity end time in seconds(活动结束时间,秒级时间戳) |
| isJoinedActivity | BOOLEAN | Whether the seller has joined the activity(卖家是否已报名该活动) |
---
Error Code(Partner 表)
| errorCode | errorMsg | Error SOP / 处理建议 |
|---|---|---|
| 220010001 | parameter is illegal | 见 Partner 文档;修正入参或活动状态后重试 |
| 220010002 | system error, please try again later | 见 Partner 文档;修正入参或活动状态后重试 |
---
脚本
export LINKFOXAGENT_API_KEY="<key>"
python scripts/us_promotion_activity_query.py '{"accessToken": "TOKEN", "tokenPurpose": "product-inventory", "request": {"pageNumber": 1, "pageSize": 20, "activityType": 2, "activityStatus": 2, "onlyQueryJoinedActivity": false}}'典型流程: 促销流程入口:按 `activityType` 与时间窗查询 `activityList` → 用 `activityId` 查候选/已报名商品。
Promotion — 接口文档索引
网关见 ../api.md;目录见 ../partner-us-catalog.md。
活动查询
| type | 说明 | 文档 |
|---|---|---|
bg.promotion.activity.query | 促销活动查询 | bg-promotion-activity-query.md |
bg.promotion.activity.candidate.goods.query | 活动候选商品查询 | bg-promotion-activity-candidate-goods-query.md |
bg.promotion.activity.goods.query | 活动已报名商品查询 | bg-promotion-activity-goods-query.md |
报名 / 操作结果
| type | 说明 | 文档 |
|---|---|---|
bg.promotion.activity.goods.enroll | 活动商品报名 | bg-promotion-activity-goods-enroll.md |
bg.promotion.activity.goods.operation.query | 活动商品操作结果查询 | bg-promotion-activity-goods-operation-query.md |
商品更新
| type | 说明 | 文档 |
|---|---|---|
bg.promotion.activity.goods.update | 活动商品更新 | bg-promotion-activity-goods-update.md |
Temu 授权流程
用户确认店铺类型与用途
↓
temu_token_guide.py(输出后台操作步骤)
↓
用户在 Temu 卖家后台复制 access_token
↓
save_temu_access_token.py(可选,写入本地 store)
↓
temu_proxy.py / temu_file_download.py
- 直接传 accessToken,或
- 传 storeKey + site + managementType + tokenPurpose决策表
| 用户需求 | shopType | tokenPurpose | site |
|---|---|---|---|
| 半托管查商品/库存 | semi-managed | product-inventory | cn / partner |
| 半托管查订单/发货 | semi-managed | order-shipping | us / global / eu |
| 全托管任意接口 | full-managed | full-managed | cn / partner |
| 美欧本土店 | local-native | local-native | us 等 |
与 API 调用的对应关系
紫鸟转发路径:/temu-proxy/{site}/{managementType}
- 全托管 CN:
cn+full-managed - 半托管商品:
cn/partner+semi-managed+ 商品 token - 半托管订单:
us/global/eu+semi-managed+ 订单 token
授权应用名称必须与后台一致:酷鸟卖家助手 或 Cyber-ERP酷鸟助手 / Cyber-ERP。
Partner US — Promotion 接口目录
Partner Platform for US 菜单:Promotion(menu_code=873ac072a78249c893e5f8d0e656a11f)。
已接入(6)
| sub_menu_code | type | 脚本 | 参数文档 |
|---|---|---|---|
05820fed7179430c8e353905692d51b6 | bg.promotion.activity.query | us_promotion_activity_query.py | bg-promotion-activity-query.md |
0a11814e7d4146b595918ff3c0f3e239 | bg.promotion.activity.candidate.goods.query | us_promotion_activity_candidate_goods_query.py | bg-promotion-activity-candidate-goods-query.md |
08f3f87d05a24bac882732141e0d9672 | bg.promotion.activity.goods.query | us_promotion_activity_goods_query.py | bg-promotion-activity-goods-query.md |
27a87ec9d0d94273a48096c050f17854 | bg.promotion.activity.goods.enroll | us_promotion_activity_goods_enroll.py | bg-promotion-activity-goods-enroll.md |
57a37eb5dd104e3f9f90118e3276b291 | bg.promotion.activity.goods.operation.query | us_promotion_activity_goods_operation_query.py | bg-promotion-activity-goods-operation-query.md |
29959238217c41f38f5904e32bf1d14f | bg.promotion.activity.goods.update | us_promotion_activity_goods_update.py | bg-promotion-activity-goods-update.md |
| type | URL |
|---|---|
bg.promotion.activity.query | https://partner-us.temu.com/documentation?menu_code=873ac072a78249c893e5f8d0e656a11f&sub_menu_code=05820fed7179430c8e353905692d51b6 |
bg.promotion.activity.candidate.goods.query | https://partner-us.temu.com/documentation?menu_code=873ac072a78249c893e5f8d0e656a11f&sub_menu_code=0a11814e7d4146b595918ff3c0f3e239 |
bg.promotion.activity.goods.query | https://partner-us.temu.com/documentation?menu_code=873ac072a78249c893e5f8d0e656a11f&sub_menu_code=08f3f87d05a24bac882732141e0d9672 |
bg.promotion.activity.goods.enroll | https://partner-us.temu.com/documentation?menu_code=873ac072a78249c893e5f8d0e656a11f&sub_menu_code=27a87ec9d0d94273a48096c050f17854 |
bg.promotion.activity.goods.operation.query | https://partner-us.temu.com/documentation?menu_code=873ac072a78249c893e5f8d0e656a11f&sub_menu_code=57a37eb5dd104e3f9f90118e3276b291 |
bg.promotion.activity.goods.update | https://partner-us.temu.com/documentation?menu_code=873ac072a78249c893e5f8d0e656a11f&sub_menu_code=29959238217c41f38f5904e32bf1d14f |
通用脚本
| temu_us_proxy.py | 任意 type | | temu_us_file_download.py | 加签下载 |
相关 skill
| 促销 | `linkfox-temu-promotion-us` | | 商品 | linkfox-temu-manage-product-us | | 价格 | linkfox-temu-price-us | | 欧洲站促销 | linkfox-temu-promotion-eu | | 全球站促销 | linkfox-temu-promotion-global |
Token:`tokenPurpose=product-inventory`。
#!/usr/bin/env python3
"""Authorization guide data derived from TEMU_API_README.md."""
VALID_SHOP_TYPES = frozenset({"semi-managed", "full-managed", "local-native"})
VALID_TOKEN_PURPOSES = frozenset(
{"product-inventory", "order-shipping", "full-managed", "local-native", "default"}
)
LOGIN_URLS = {
"cn": "https://seller.kuajingmaihuo.com",
"partner": "https://agentseller.temu.com",
}
APPS = {
"kuniao-seller": "酷鸟卖家助手",
"cyber-erp": "Cyber-ERP酷鸟助手",
"cyber-erp-native": "Cyber-ERP",
}
def _steps(*items: str) -> list[str]:
return list(items)
def build_guide(shop_type: str, token_purpose: str, site=None) -> dict:
shop_type = shop_type.strip().lower()
token_purpose = (token_purpose or "default").strip().lower()
if shop_type not in VALID_SHOP_TYPES:
raise ValueError(
f"Invalid shopType '{shop_type}'. Use: semi-managed, full-managed, local-native"
)
if token_purpose not in VALID_TOKEN_PURPOSES:
raise ValueError(
"Invalid tokenPurpose. Use: product-inventory, order-shipping, "
"full-managed, local-native, default"
)
guide = {
"shopType": shop_type,
"tokenPurpose": token_purpose,
"note": (
"Temu accessToken 需在卖家后台手动复制,无自动 OAuth。"
"保存后可使用 save_temu_access_token.py / storeKey 调用 API。"
),
}
if shop_type == "semi-managed" and token_purpose == "product-inventory":
site = site or "cn"
login = LOGIN_URLS.get(site, LOGIN_URLS["cn"])
guide.update(
{
"recommendedSite": site,
"recommendedManagementType": "semi-managed",
"loginUrl": login,
"appName": APPS["kuniao-seller"],
"menuPath": "系统管理 > 服务市场 > 授权管理",
"steps": _steps(
f"登录 Temu 卖家后台:{login}",
"进入:系统管理 > 服务市场 > 授权管理",
"点击「获取授权」,选择「酷鸟卖家助手」",
"全选常规和特殊授权 > 确认",
"复制 access_token,用 save_temu_access_token.py 保存",
),
"usageHint": "用于半托管商品、库存类 API;site 通常为 cn 或 partner。",
}
)
elif shop_type == "semi-managed" and token_purpose == "order-shipping":
site = site or "us"
guide.update(
{
"recommendedSite": site,
"recommendedManagementType": "semi-managed",
"loginUrl": LOGIN_URLS["cn"],
"appName": APPS["cyber-erp"],
"menuPath": "Seller Central 对应站点 > 服务市场 > 授权管理",
"steps": _steps(
"登录 Temu 卖家后台(seller.kuajingmaihuo.com 或 agentseller.temu.com)",
"右上角 Seller Central,切换到目标区域站点(美区/欧区/全球)",
"进入:服务市场 > 授权管理",
"点击「获取授权」,选择「Cyber-ERP酷鸟助手」",
"全选常规和特殊授权 > 确认",
"复制 access_token;调用 API 时 site 用 us / global / eu",
),
"usageHint": "用于半托管订单、发货类 API。",
}
)
elif shop_type == "full-managed" or token_purpose == "full-managed":
site = site or "cn"
login = LOGIN_URLS.get(site, LOGIN_URLS["cn"])
guide.update(
{
"recommendedSite": site,
"recommendedManagementType": "full-managed",
"loginUrl": login,
"appName": APPS["kuniao-seller"],
"menuPath": "系统管理 > 服务市场 > 授权管理",
"steps": _steps(
f"登录 Temu 平台:{login}",
"系统管理 > 服务市场 > 授权管理",
"获取授权 > 选择「酷鸟卖家助手」",
"全选常规和特殊授权 > 确认 > 复制 access_token",
),
"usageHint": "全托管店铺通用;site 通常为 cn 或 partner。",
}
)
elif shop_type == "local-native" or token_purpose == "local-native":
guide.update(
{
"recommendedSite": site or "us",
"recommendedManagementType": "semi-managed",
"loginUrl": LOGIN_URLS["cn"],
"appName": APPS["cyber-erp-native"],
"menuPath": "Apps And Services > Manage Your Apps",
"steps": _steps(
"登录 Temu 卖家后台",
"进入:Apps And Services(应用程序和服务)",
"Manage Your Apps(管理您的应用)",
"Authorize a new app(授权新应用)",
"搜索「Cyber-ERP」",
"一般权限和敏感权限全选 > 确认 > 复制 access_token",
),
"usageHint": "适用于美区、欧区本土主体店铺。",
}
)
else:
guide.update(
{
"steps": _steps(
"确认店铺类型:semi-managed / full-managed / local-native",
"确认用途:product-inventory(商品库存)或 order-shipping(订单发货)",
"运行 temu_token_guide.py 获取对应步骤",
"复制 token 后执行 save_temu_access_token.py",
),
"usageHint": "请指定 shopType 与 tokenPurpose,见 references/access-token.md",
}
)
guide["cautions"] = [
"子账号可能无法进入服务市场,需主账号授权",
"多站点需分别登录对应后台获取 Token",
"Token 有有效期,过期后需重新在后台复制",
"调用网关前需联系紫鸟配置 IP 白名单",
]
return guide
#!/usr/bin/env python3
"""Shared helpers for LinkFox Temu API skill scripts."""
import json
import os
import sys
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
from _temu_token_store import get_token
BASE_URL = os.environ.get("TEMU_API_BASE_URL") or os.environ.get(
"STORE_API_BASE_URL", "https://tool-gateway.linkfox.com"
)
BASE_URL = BASE_URL.rstrip("/")
PROXY_URL = f"{BASE_URL}/temu/proxy"
FILE_DOWNLOAD_URL = f"{BASE_URL}/temu/fileDownload"
VALID_SITES = frozenset({"cn", "partner", "us", "global", "eu"})
VALID_MANAGEMENT_TYPES = frozenset({"full-managed", "semi-managed"})
# LinkFox 用户 Token(网关鉴权),勿与 Body 中的 Temu accessToken 混淆
LINKFOX_TOKEN_PARAM_KEYS = ("token", "linkfoxToken", "linkfox_token")
def get_linkfox_token(params=None) -> str:
"""
LinkFox 用户鉴权 Token,与 linkfox-amazon-store-auth 一致。
优先级:请求 JSON 中的 token / linkfoxToken > 环境变量 LINKFOXAGENT_API_KEY。
"""
if params:
for key in LINKFOX_TOKEN_PARAM_KEYS:
value = params.get(key)
if value is not None and str(value).strip():
return str(value).strip()
key = os.environ.get("LINKFOXAGENT_API_KEY")
if not key:
print(
"LinkFox user Token not configured. Same as linkfox-amazon-store-auth:\n"
"1. Visit https://skill.linkfox.com/linkfoxskills/guide.htm to obtain your Key\n"
"2. export LINKFOXAGENT_API_KEY=your-key-here\n"
" Or pass \"token\" in the JSON parameters of proxy/fileDownload scripts.",
file=sys.stderr,
)
sys.exit(1)
return key
def build_gateway_headers(linkfox_token: str) -> dict:
"""网关鉴权:Authorization(全站通用)+ Token(TEMU_API_SPEC 约定)。"""
return {
"Authorization": linkfox_token,
"Token": linkfox_token,
"Content-Type": "application/json",
"User-Agent": "LinkFox-Skill/1.0",
}
def load_json_arg(argv: list) -> dict:
if len(argv) < 2:
return {}
try:
return json.loads(argv[1])
except json.JSONDecodeError as e:
print(f"Invalid parameter format: {e}", file=sys.stderr)
sys.exit(1)
def require_text(params: dict, key: str, label=None) -> str:
value = params.get(key)
if value is None or not str(value).strip():
print(f"Error: '{label or key}' is required.", file=sys.stderr)
sys.exit(1)
return str(value).strip()
def validate_site(site: str) -> str:
if site not in VALID_SITES:
print(
f"Error: invalid site '{site}'. Must be one of: {', '.join(sorted(VALID_SITES))}",
file=sys.stderr,
)
sys.exit(1)
return site
def validate_management_type(management_type: str) -> str:
if management_type not in VALID_MANAGEMENT_TYPES:
print(
"Error: invalid managementType. Must be: full-managed, semi-managed",
file=sys.stderr,
)
sys.exit(1)
return management_type
def call_temu_api(
url: str,
body: dict,
timeout: int = 60,
linkfox_params=None,
) -> dict:
"""调用 Temu 网关接口;必须先具备 LinkFox 用户 Token。"""
linkfox_token = get_linkfox_token(linkfox_params)
data = json.dumps(body, ensure_ascii=False).encode("utf-8")
req = Request(
url,
data=data,
headers=build_gateway_headers(linkfox_token),
method="POST",
)
try:
with urlopen(req, timeout=timeout) as response:
return json.loads(response.read().decode("utf-8"))
except HTTPError as e:
raw = e.read().decode("utf-8") if e.fp else ""
try:
return json.loads(raw)
except json.JSONDecodeError:
return {"error": f"HTTP {e.code}: {e.reason}", "details": raw}
except URLError as e:
return {"error": f"Connection failed: {e.reason}"}
def is_linkfox_auth_error(result: dict) -> bool:
msg = str(result.get("message") or result.get("error") or "")
return "无法识别当前用户" in msg or "重新登录" in msg
def resolve_access_token(params: dict) -> str:
"""Temu 店铺 accessToken:直接传入或从本地 storeKey 读取。"""
if params.get("accessToken"):
return str(params["accessToken"]).strip()
store_key = params.get("storeKey")
if not store_key:
print(
"Error: provide Temu 'accessToken' or 'storeKey' (+ site, managementType).",
file=sys.stderr,
)
sys.exit(1)
site = validate_site(require_text(params, "site"))
management_type = validate_management_type(require_text(params, "managementType"))
token_purpose = str(params.get("tokenPurpose", "default")).strip() or "default"
token = get_token(str(store_key).strip(), site, management_type, token_purpose)
if not token:
print(
f"Error: no Temu token for storeKey={store_key}, site={site}, "
f"managementType={management_type}, tokenPurpose={token_purpose}. "
"Run temu_token_guide.py and save_temu_access_token.py first.",
file=sys.stderr,
)
sys.exit(1)
return token
def parse_nested_body(result: dict) -> dict:
"""If gateway returns a JSON string in body, parse it into temuBody."""
body = result.get("body")
if isinstance(body, str) and body.strip():
try:
result["temuBody"] = json.loads(body)
except json.JSONDecodeError:
pass
return result
#!/usr/bin/env python3
"""Local Temu accessToken storage (manual authorization from seller console)."""
from __future__ import annotations
import json
import os
from datetime import datetime, timezone
DEFAULT_STORE_PATH = os.path.expanduser("~/.linkfox/temu-access-tokens.json")
def store_path() -> str:
return os.environ.get("TEMU_TOKEN_STORE_PATH", DEFAULT_STORE_PATH)
def _utc_now() -> str:
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def _load_store() -> dict:
path = store_path()
if not os.path.isfile(path):
return {"stores": []}
with open(path, encoding="utf-8") as f:
data = json.load(f)
if "stores" not in data or not isinstance(data["stores"], list):
return {"stores": []}
return data
def _save_store(data: dict) -> None:
path = store_path()
parent = os.path.dirname(path)
if parent:
os.makedirs(parent, exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
f.write("\n")
def _find_store(data: dict, store_key: str):
for store in data["stores"]:
if store.get("storeKey") == store_key:
return store
return None
def _token_key(site: str, management_type: str, token_purpose: str) -> tuple:
return site, management_type, token_purpose or "default"
def save_token(
store_key: str,
site: str,
management_type: str,
access_token: str,
token_purpose: str = "default",
label: str | None = None,
) -> dict:
data = _load_store()
store = _find_store(data, store_key)
if store is None:
store = {"storeKey": store_key, "label": label or store_key, "tokens": []}
data["stores"].append(store)
elif label:
store["label"] = label
key = _token_key(site, management_type, token_purpose)
entry = {
"site": site,
"managementType": management_type,
"tokenPurpose": token_purpose,
"accessToken": access_token,
"updatedAt": _utc_now(),
}
replaced = False
for i, item in enumerate(store["tokens"]):
if _token_key(
item.get("site", ""),
item.get("managementType", ""),
item.get("tokenPurpose", "default"),
) == key:
store["tokens"][i] = entry
replaced = True
break
if not replaced:
store["tokens"].append(entry)
_save_store(data)
return {
"storeKey": store_key,
"site": site,
"managementType": management_type,
"tokenPurpose": token_purpose,
"storePath": store_path(),
"updatedAt": entry["updatedAt"],
"replaced": replaced,
}
def get_token(
store_key: str,
site: str,
management_type: str,
token_purpose: str = "default",
):
data = _load_store()
store = _find_store(data, store_key)
if not store:
return None
key = _token_key(site, management_type, token_purpose)
for item in store.get("tokens", []):
if _token_key(
item.get("site", ""),
item.get("managementType", ""),
item.get("tokenPurpose", "default"),
) == key:
return item.get("accessToken")
return None
def list_stores(mask: bool = True) -> dict:
data = _load_store()
stores = []
for store in data.get("stores", []):
tokens = []
for item in store.get("tokens", []):
token = item.get("accessToken", "")
if mask and token:
shown = token[:6] + "..." + token[-4:] if len(token) > 12 else "***"
else:
shown = token
tokens.append(
{
"site": item.get("site"),
"managementType": item.get("managementType"),
"tokenPurpose": item.get("tokenPurpose", "default"),
"accessToken": shown,
"updatedAt": item.get("updatedAt"),
}
)
stores.append(
{
"storeKey": store.get("storeKey"),
"label": store.get("label"),
"tokens": tokens,
}
)
return {"storePath": store_path(), "stores": stores}
#!/usr/bin/env python3
"""US / Partner Promotion API helpers; local gateway helpers."""
import json
from _temu_common import (
FILE_DOWNLOAD_URL,
PROXY_URL,
call_temu_api,
load_json_arg,
parse_nested_body,
resolve_access_token,
)
# Partner US 促销:默认美国站半托管 + 商品/促销场景 token
DEFAULT_SITE = "us"
DEFAULT_MANAGEMENT_TYPE = "semi-managed"
DEFAULT_TOKEN_PURPOSE = "product-inventory"
RESERVED_KEYS = frozenset(
{
"token",
"linkfoxToken",
"linkfox_token",
"accessToken",
"storeKey",
"site",
"managementType",
"tokenPurpose",
"type",
"params",
}
)
def extract_business_params(params: dict) -> dict:
if isinstance(params.get("params"), dict):
return dict(params["params"])
return {k: v for k, v in params.items() if k not in RESERVED_KEYS}
def build_us_proxy_body(params: dict, api_type: str, business=None) -> dict:
site = str(params.get("site", DEFAULT_SITE)).strip().lower() or DEFAULT_SITE
management_type = (
str(params.get("managementType", DEFAULT_MANAGEMENT_TYPE)).strip().lower()
or DEFAULT_MANAGEMENT_TYPE
)
if "tokenPurpose" not in params and params.get("storeKey") and not params.get("accessToken"):
params = dict(params)
params.setdefault("tokenPurpose", DEFAULT_TOKEN_PURPOSE)
params.setdefault("site", site)
params.setdefault("managementType", management_type)
body = {
"site": site,
"managementType": management_type,
"accessToken": resolve_access_token(params),
"type": api_type,
}
biz = business if business is not None else extract_business_params(params)
if biz:
body["params"] = biz
return body
def us_proxy_call(params: dict, api_type: str, business=None, timeout: int = 60) -> dict:
body = build_us_proxy_body(params, api_type, business)
return call_temu_api(PROXY_URL, body, timeout=timeout, linkfox_params=params)
def us_file_download_call(params: dict, timeout: int = 120) -> dict:
if "tokenPurpose" not in params and params.get("storeKey") and not params.get("accessToken"):
params = dict(params)
params.setdefault("tokenPurpose", DEFAULT_TOKEN_PURPOSE)
from _temu_common import require_text # noqa: E402
site = str(params.get("site", DEFAULT_SITE)).strip().lower() or DEFAULT_SITE
management_type = (
str(params.get("managementType", DEFAULT_MANAGEMENT_TYPE)).strip().lower()
or DEFAULT_MANAGEMENT_TYPE
)
body = {
"site": site,
"managementType": management_type,
"accessToken": resolve_access_token(params),
"url": require_text(params, "url"),
}
return call_temu_api(FILE_DOWNLOAD_URL, body, timeout=timeout, linkfox_params=params)
def run_and_print(params: dict, api_type: str, business=None) -> None:
result = parse_nested_body(us_proxy_call(params, api_type, business))
print(json.dumps(result, indent=2, ensure_ascii=False))
#!/usr/bin/env python3
"""Factory for thin US Promotion API CLI scripts."""
import sys
import _temu_us_common # noqa: F401 — gateway helpers in scripts/
from _temu_common import load_json_arg
from _temu_us_common import extract_business_params, run_and_print
def run_cli(api_type: str, usage: str) -> None:
if len(sys.argv) < 2:
print(f"Usage: {usage}", file=sys.stderr)
sys.exit(1)
params = load_json_arg(sys.argv)
run_and_print(params, api_type, extract_business_params(params))
#!/usr/bin/env python3
"""
Check LinkFox user Token before calling Temu gateway APIs.
Same role as LINKFOXAGENT_API_KEY in linkfox-amazon-store-auth.
Usage:
python check_linkfox_token.py
python check_linkfox_token.py '{"token": "your-linkfox-key"}'
"""
import json
import sys
from _temu_common import PROXY_URL, call_temu_api, get_linkfox_token, is_linkfox_auth_error, load_json_arg
def main():
params = load_json_arg(sys.argv) if len(sys.argv) >= 2 else {}
linkfox_token = get_linkfox_token(params)
masked = (
linkfox_token[:6] + "..." + linkfox_token[-4:]
if len(linkfox_token) > 12
else "***"
)
# 最小探测请求:LinkFox Token 有效时应返回业务/参数类 1002,而非「无法识别当前用户」
probe_body = {
"site": "cn",
"managementType": "full-managed",
"accessToken": "probe",
"type": "probe",
}
result = call_temu_api(PROXY_URL, probe_body, timeout=30, linkfox_params=params)
if is_linkfox_auth_error(result):
print(
json.dumps(
{
"ok": False,
"reason": "linkfox_token_invalid",
"message": result.get("message") or result,
"hint": "Set LINKFOXAGENT_API_KEY or pass token in JSON.",
},
indent=2,
ensure_ascii=False,
)
)
sys.exit(1)
print(
json.dumps(
{
"ok": True,
"linkfoxToken": masked,
"gatewayReachable": True,
"note": "LinkFox user Token accepted by gateway.",
},
indent=2,
ensure_ascii=False,
)
)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Get Temu accessToken from local store.
Usage:
python get_temu_access_token.py '{
"storeKey": "my-shop",
"site": "cn",
"managementType": "semi-managed",
"tokenPurpose": "product-inventory"
}'
"""
import json
import sys
from _temu_common import load_json_arg, require_text, validate_management_type, validate_site
from _temu_token_store import get_token
def main():
if len(sys.argv) < 2:
print("Usage: get_temu_access_token.py '<JSON parameters>'", file=sys.stderr)
sys.exit(1)
params = load_json_arg(sys.argv)
store_key = require_text(params, "storeKey")
site = validate_site(require_text(params, "site"))
management_type = validate_management_type(require_text(params, "managementType"))
token_purpose = str(params.get("tokenPurpose", "default")).strip() or "default"
token = get_token(store_key, site, management_type, token_purpose)
if not token:
print(
json.dumps(
{
"found": False,
"storeKey": store_key,
"site": site,
"managementType": management_type,
"tokenPurpose": token_purpose,
"hint": "Run temu_token_guide.py then save_temu_access_token.py",
},
indent=2,
ensure_ascii=False,
)
)
sys.exit(1)
print(
json.dumps(
{
"found": True,
"storeKey": store_key,
"site": site,
"managementType": management_type,
"tokenPurpose": token_purpose,
"accessToken": token,
},
indent=2,
ensure_ascii=False,
)
)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
List locally saved Temu accessTokens (masked).
Usage:
python list_temu_access_tokens.py
python list_temu_access_tokens.py '{"mask": false}'
"""
import json
import sys
from _temu_token_store import list_stores
def main():
mask = True
if len(sys.argv) >= 2:
try:
params = json.loads(sys.argv[1])
mask = params.get("mask", True)
except json.JSONDecodeError as e:
print(f"Invalid parameter format: {e}", file=sys.stderr)
sys.exit(1)
print(json.dumps(list_stores(mask=mask), indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Skill response I/O helper — wraps any main script to persist large API
responses to disk, then offers a `read` subcommand to extract specific fields
from those persisted files. Generic, business-agnostic.
This script is bundled into each skill's scripts/ directory by tools/response_io/sync.py.
The agent must pass --script <path> to identify which main script to execute.
Usage:
python scripts/response_io.py run --script <PATH> --out-dir <DIR> '<json_params>' [--label NAME] [--timeout SEC]
python scripts/response_io.py read <file> (--path "<JMESPath>" | --fields "f1,f2,...") [--limit N] [--offset M] [--format json|jsonl|csv|table]
"""
from __future__ import annotations
import sys
if sys.version_info < (3, 10):
sys.exit(
"Error: Python 3.10+ required (current: "
f"{sys.version_info.major}.{sys.version_info.minor}). "
"Please upgrade Python."
)
import argparse
import csv
import io
import json
import os
import re
import secrets
import subprocess
from datetime import datetime
from pathlib import Path
from typing import Any
# Force UTF-8 stdout/stderr so non-ASCII chars in previews and API responses
# print correctly on Windows (default cp936 / gbk).
for stream in (sys.stdout, sys.stderr):
try:
stream.reconfigure(encoding="utf-8") # type: ignore[attr-defined]
except (AttributeError, OSError):
pass
try:
import jmespath # type: ignore
HAS_JMESPATH = True
except ImportError:
HAS_JMESPATH = False
MAX_STRING_LEN = 120
MAX_DEPTH = 3
SAMPLE_KEY_CAP = 15
RAW_TEXT_PEEK = 500
DEFAULT_TIMEOUT_SEC = 300
# ---------------------------------------------------------------------------
# Shared helpers
# ---------------------------------------------------------------------------
def _err(msg: str, code: int = 1) -> None:
print(msg, file=sys.stderr)
sys.exit(code)
def _resolve_script(script_arg: str) -> Path:
p = Path(script_arg).expanduser()
if not p.is_absolute():
# Resolve relative to the current working directory the agent invoked from.
p = (Path.cwd() / p).resolve()
else:
p = p.resolve()
if not p.is_file():
_err(f"--script path not found: {p}")
return p
def _resolve_skill_name(main_script: Path) -> str:
"""Best-effort skill name extraction for filename prefixing.
main_script lives at <skill_dir>/scripts/<name>.py — return <skill_dir>'s
folder name. Fall back to the script's stem if structure differs.
"""
try:
if main_script.parent.name == "scripts":
return main_script.parents[1].name
except IndexError:
pass
return main_script.stem
def _sanitize_label(label: str) -> str:
"""Allow only safe filename chars in --label to prevent path traversal."""
cleaned = re.sub(r"[^\w\-]", "_", label)
return cleaned[:64] # cap length
def _truncate_string(s: str) -> str:
if len(s) <= MAX_STRING_LEN:
return s
return s[:MAX_STRING_LEN] + f"...(truncated, total {len(s)} chars)"
def _truncate_value(value: Any, depth: int = 0) -> Any:
"""Recursively truncate strings, deep nesting, and large arrays for preview."""
if depth >= MAX_DEPTH:
if isinstance(value, dict):
return f"<truncated nested object, keys: {list(value.keys())[:10]}>"
if isinstance(value, list):
return f"<truncated nested array, length: {len(value)}>"
if isinstance(value, str):
return _truncate_string(value)
return value
if isinstance(value, str):
return _truncate_string(value)
if isinstance(value, dict):
out = {k: _truncate_value(v, depth + 1) for k, v in value.items()}
return out
if isinstance(value, list):
if not value:
return []
truncated = [_truncate_value(value[0], depth + 1)]
if len(value) > 1:
# Note total length on the parent — keep the array type-homogeneous
# so downstream consumers can iterate without special-casing strings.
truncated.append({"_omitted_items": len(value) - 1})
return truncated
return value
def _shape_of(value: Any, top: bool = False) -> Any:
"""Lightweight schema description for the preview block."""
if isinstance(value, dict):
keys = list(value.keys())
out: dict[str, Any] = {"type": "object", "top_keys" if top else "keys": keys}
if top:
for k in keys[:8]:
out[k] = _shape_of(value[k])
return out
if isinstance(value, list):
out = {"type": "array", "length": len(value)}
if value and isinstance(value[0], dict):
out["item_keys"] = list(value[0].keys())
elif value:
out["item_type"] = type(value[0]).__name__
return out
return {"type": type(value).__name__}
def _build_sample(value: Any) -> Any:
"""First-record sample with explicit truncation marker."""
if isinstance(value, list):
if not value:
return {"_truncated_record": True, "_note": "array is empty"}
first = value[0]
if isinstance(first, dict):
sample = {"_truncated_record": True, "_note": f"first of {len(value)} items"}
sample.update(_truncate_value(first, depth=1))
return sample
return {"_truncated_record": True, "_note": f"first of {len(value)} items", "value": _truncate_value(first, depth=1)}
if isinstance(value, dict):
sample = {"_truncated_record": True, "_note": "top-level object (truncated)"}
sample.update(_truncate_value(value, depth=1))
return sample
return {"_truncated_record": True, "value": _truncate_value(value, depth=1)}
def _shrink_preview(preview: dict) -> dict:
"""Cap the sample's value fields when it has many keys.
`shape.*.item_keys` is the single source of truth for the full key list
(always complete, no truncation). The sample only ever shows up to
SAMPLE_KEY_CAP fields with their concrete values, since the agent only
needs a feel for value shapes — for the full menu of available fields,
they read `shape`.
"""
sample = preview.get("sample")
if isinstance(sample, dict):
meta_keys = {"_truncated_record", "_note"}
data_keys = [k for k in sample.keys() if k not in meta_keys]
if len(data_keys) > SAMPLE_KEY_CAP:
kept = data_keys[:SAMPLE_KEY_CAP]
new_sample = {k: v for k, v in sample.items() if k in meta_keys or k in kept}
base_note = sample.get("_note", "")
extra = (
f"showing first {SAMPLE_KEY_CAP} of {len(data_keys)} fields "
f"(see `shape` for the complete key list)"
)
new_sample["_note"] = f"{base_note}; {extra}" if base_note else extra
preview["sample"] = new_sample
return preview
# ---------------------------------------------------------------------------
# `run` subcommand
# ---------------------------------------------------------------------------
def cmd_run(args: argparse.Namespace) -> int:
main_script = _resolve_script(args.script)
skill_name = _resolve_skill_name(main_script)
out_dir = Path(args.out_dir).expanduser().resolve()
try:
out_dir.mkdir(parents=True, exist_ok=True)
except OSError as e:
_err(f"Failed to create --out-dir {out_dir}: {e}")
if not os.access(out_dir, os.W_OK):
_err(f"--out-dir is not writable: {out_dir}")
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
rand = secrets.token_hex(3)
safe_label = _sanitize_label(args.label) if args.label else ""
label_part = f"__{safe_label}" if safe_label else ""
out_file = out_dir / f"{skill_name}__{timestamp}_{rand}{label_part}.json"
# Force the child process to emit UTF-8 regardless of the host console
# encoding (Windows defaults to cp936 / gbk and would otherwise corrupt
# non-ASCII bytes when we read them back).
child_env = os.environ.copy()
child_env["PYTHONIOENCODING"] = "utf-8"
timed_out = False
try:
proc = subprocess.run(
[sys.executable, str(main_script), args.params],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
env=child_env,
timeout=args.timeout,
)
stdout_text = proc.stdout or ""
stderr_text = proc.stderr or ""
returncode = proc.returncode
except subprocess.TimeoutExpired as e:
timed_out = True
stdout_text = (e.stdout.decode("utf-8", errors="replace") if isinstance(e.stdout, bytes) else (e.stdout or "")) or ""
stderr_text = (e.stderr.decode("utf-8", errors="replace") if isinstance(e.stderr, bytes) else (e.stderr or "")) or ""
returncode = 124 # convention for timeout
# Always write the captured stdout to disk, even if not JSON.
try:
out_file.write_text(stdout_text, encoding="utf-8")
except OSError as e:
_err(f"Failed to write output file {out_file}: {e}")
if stderr_text:
sys.stderr.write(stderr_text)
# Try to parse the captured stdout as JSON for the preview.
try:
parsed = json.loads(stdout_text) if stdout_text.strip() else None
format_kind = "json"
except json.JSONDecodeError:
parsed = None
format_kind = "raw_text"
preview: dict[str, Any] = {
"_preview": {
"is_preview": True,
"warning": (
"PREVIEW ONLY — NOT FULL DATA. The full response is saved to `file`. "
"Use `python scripts/response_io.py read <file> --fields '...'` to extract "
"specific fields, or `--path '<JMESPath>'` for complex projections."
),
},
}
# Surface failures prominently so agents don't mistake a stub preview for success.
if returncode != 0 or timed_out:
stderr_snippet = stderr_text[-500:] if stderr_text else ""
preview["_error"] = {
"exit_code": returncode,
"timed_out": timed_out,
"stderr_snippet": stderr_snippet,
"hint": "The wrapped script failed or timed out. The output file may be empty or partial.",
}
preview.update({
"file": str(out_file),
"size_bytes": out_file.stat().st_size,
"skill": skill_name,
"exit_code": returncode,
"format": format_kind,
"label": safe_label or None,
"next_steps_hint": (
"use: python scripts/response_io.py read <file> --fields '...' | --path '...'"
),
})
if format_kind == "json":
preview["shape"] = _shape_of(parsed, top=True)
preview["sample"] = _build_sample(parsed)
else:
peek = stdout_text[:RAW_TEXT_PEEK]
preview["raw_text_peek"] = peek
preview["raw_text_total_chars"] = len(stdout_text)
preview["sample"] = {
"_truncated_record": True,
"_note": f"stdout was not valid JSON; first {RAW_TEXT_PEEK} chars shown above in raw_text_peek",
}
preview = _shrink_preview(preview)
print(json.dumps(preview, ensure_ascii=False, indent=2))
return returncode
# ---------------------------------------------------------------------------
# `read` subcommand
# ---------------------------------------------------------------------------
def _load_json(path: Path) -> Any:
try:
text = path.read_text(encoding="utf-8")
except OSError as e:
_err(f"Failed to read file {path}: {e}")
try:
return json.loads(text)
except json.JSONDecodeError as e:
_err(f"File is not valid JSON: {path}\n{e}")
def _basic_dot_path(data: Any, path: str) -> Any:
"""Pure-stdlib dot-path resolver. No [*] support — callers fall back here only when jmespath is unavailable AND the path has no [*]."""
cur = data
for part in path.split("."):
if isinstance(cur, dict):
cur = cur.get(part)
else:
return None
return cur
def _resolve_field(data: Any, expr: str) -> Any:
if HAS_JMESPATH:
return jmespath.search(expr, data)
if "[" in expr or "*" in expr:
_err(
f"jmespath is required for expression '{expr}'. "
f"Install with: pip install jmespath"
)
return _basic_dot_path(data, expr)
def _project_fields(data: Any, fields: list[str]) -> Any:
"""Run each field expr; if any returns a list, zip them into list-of-dicts."""
resolved: dict[str, Any] = {f: _resolve_field(data, f) for f in fields}
list_lengths = [len(v) for v in resolved.values() if isinstance(v, list)]
if not list_lengths:
return resolved
# All list values must be same length to zip cleanly.
if len(set(list_lengths)) > 1:
# Fallback: return the dict as-is so caller can inspect mismatches.
return resolved
n = list_lengths[0]
rows = []
for i in range(n):
row = {}
for f, v in resolved.items():
row[f] = v[i] if isinstance(v, list) else v
rows.append(row)
return rows
def _apply_slice(value: Any, limit: int | None, offset: int | None) -> Any:
if not isinstance(value, list):
return value
start = offset or 0
end = (start + limit) if limit is not None else None
return value[start:end]
def _format_output(value: Any, fmt: str) -> str:
if fmt == "json":
return json.dumps(value, ensure_ascii=False, indent=2)
if fmt == "jsonl":
if isinstance(value, list):
return "\n".join(json.dumps(item, ensure_ascii=False) for item in value)
return json.dumps(value, ensure_ascii=False)
if fmt in ("csv", "table"):
if not isinstance(value, list) or not value:
_err(f"--format {fmt} requires a non-empty list result")
if not all(isinstance(item, dict) for item in value):
_err(f"--format {fmt} requires list-of-objects, got list of {type(value[0]).__name__}")
keys: list[str] = []
for item in value:
for k in item.keys():
if k not in keys:
keys.append(k)
if fmt == "csv":
buf = io.StringIO()
writer = csv.DictWriter(buf, fieldnames=keys, extrasaction="ignore")
writer.writeheader()
for item in value:
writer.writerow({k: _stringify(item.get(k)) for k in keys})
return buf.getvalue().rstrip("\n")
# table: simple aligned columns
rows = [[_stringify(item.get(k)) for k in keys] for item in value]
widths = [len(k) for k in keys]
for row in rows:
for i, cell in enumerate(row):
widths[i] = max(widths[i], len(cell))
lines = [
" ".join(k.ljust(widths[i]) for i, k in enumerate(keys)),
" ".join("-" * widths[i] for i in range(len(keys))),
]
for row in rows:
lines.append(" ".join(row[i].ljust(widths[i]) for i in range(len(keys))))
return "\n".join(lines)
_err(f"Unknown --format: {fmt}")
return "" # unreachable
def _stringify(v: Any) -> str:
if v is None:
return ""
if isinstance(v, (dict, list)):
return json.dumps(v, ensure_ascii=False)
return str(v)
def cmd_read(args: argparse.Namespace) -> int:
if not args.path and not args.fields:
_err("read: either --path or --fields is required")
if args.path and args.fields:
_err("read: --path and --fields are mutually exclusive")
file_path = Path(args.file).expanduser().resolve()
data = _load_json(file_path)
if args.path:
result = _resolve_field(data, args.path)
else:
fields = [f.strip() for f in args.fields.split(",") if f.strip()]
if not fields:
_err("--fields parsed to empty list")
result = _project_fields(data, fields)
result = _apply_slice(result, args.limit, args.offset)
print(_format_output(result, args.format))
return 0
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main() -> int:
parser = argparse.ArgumentParser(
prog="response_io.py",
description="Persist large skill API responses to disk and read fields on demand.",
)
sub = parser.add_subparsers(dest="cmd", required=True)
p_run = sub.add_parser(
"run",
help="Execute a main script and persist its stdout to a file; "
"print only a lightweight preview to stdout.",
)
p_run.add_argument("params", help="JSON params string passed verbatim to the main script (argv[1]).")
p_run.add_argument("--script", required=True, help="Path to the main script to execute, e.g. scripts/my_api.py")
p_run.add_argument("--out-dir", required=True, help="Directory to write the response file into (created if missing).")
p_run.add_argument("--label", default=None, help="Optional filename suffix; sanitized to safe filename characters.")
p_run.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT_SEC, help=f"Subprocess timeout in seconds (default: {DEFAULT_TIMEOUT_SEC}).")
p_run.set_defaults(func=cmd_run)
p_read = sub.add_parser(
"read",
help="Extract specific fields from a previously persisted response file.",
)
p_read.add_argument("file", help="Path to the persisted JSON response file.")
g = p_read.add_mutually_exclusive_group()
g.add_argument("--path", default=None, help="JMESPath expression, e.g. 'data[*].{asin: asin, title: title}'.")
g.add_argument("--fields", default=None, help="Comma-separated field paths, e.g. 'data[*].asin,data[*].title'.")
p_read.add_argument("--limit", type=int, default=None, help="Take at most N items (when result is a list).")
p_read.add_argument("--offset", type=int, default=None, help="Skip the first M items (when result is a list).")
p_read.add_argument("--format", choices=["json", "jsonl", "csv", "table"], default="json", help="Output format (default: json).")
p_read.set_defaults(func=cmd_read)
args = parser.parse_args()
return args.func(args)
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env python3
"""
Save Temu accessToken to local store for reuse in proxy/fileDownload calls.
Usage:
python save_temu_access_token.py '{
"storeKey": "my-shop",
"site": "cn",
"managementType": "semi-managed",
"tokenPurpose": "product-inventory",
"accessToken": "PASTE_TOKEN_HERE",
"label": "中国半托管主店"
}'
"""
import json
import sys
from _temu_common import load_json_arg, require_text, validate_management_type, validate_site
from _temu_token_store import save_token
def main():
if len(sys.argv) < 2:
print("Usage: save_temu_access_token.py '<JSON parameters>'", file=sys.stderr)
sys.exit(1)
params = load_json_arg(sys.argv)
store_key = require_text(params, "storeKey")
site = validate_site(require_text(params, "site"))
management_type = validate_management_type(require_text(params, "managementType"))
access_token = require_text(params, "accessToken")
token_purpose = str(params.get("tokenPurpose", "default")).strip() or "default"
label = params.get("label")
result = save_token(
store_key=store_key,
site=site,
management_type=management_type,
access_token=access_token,
token_purpose=token_purpose,
label=label,
)
print(json.dumps(result, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Temu Signed File Download - LinkFox Skill
Downloads Temu signed resource files via /temu/fileDownload.
Usage:
python temu_file_download.py '{
"site": "us",
"managementType": "semi-managed",
"accessToken": "your_temu_access_token",
"url": "https://example.com/signed-file-url"
}'
"""
import json
import sys
from _temu_common import (
FILE_DOWNLOAD_URL,
call_temu_api,
load_json_arg,
require_text,
resolve_access_token,
validate_management_type,
validate_site,
)
def build_request(params: dict) -> dict:
site = validate_site(require_text(params, "site"))
management_type = validate_management_type(
require_text(params, "managementType")
)
access_token = resolve_access_token(params)
url = require_text(params, "url")
return {
"site": site,
"managementType": management_type,
"accessToken": access_token,
"url": url,
}
def main():
if len(sys.argv) < 2:
print("Usage: temu_file_download.py '<JSON parameters>'", file=sys.stderr)
print(
'Example: temu_file_download.py \'{"site":"us","managementType":"semi-managed",'
'"accessToken":"TOKEN","url":"https://example.com/signed-file-url"}\'',
file=sys.stderr,
)
sys.exit(1)
params = load_json_arg(sys.argv)
body = build_request(params)
result = call_temu_api(FILE_DOWNLOAD_URL, body, timeout=120, linkfox_params=params)
print(json.dumps(result, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Temu API Proxy - LinkFox Skill
Forwards Temu platform API requests via /temu/proxy.
Usage:
export LINKFOXAGENT_API_KEY="<linkfox-user-token>"
python temu_proxy.py '{
"site": "cn",
"managementType": "full-managed",
"accessToken": "your_temu_access_token",
"type": "bg.goods.category.mapping",
"params": {"goodsName": "测试商品", "goodsNameEn": "Test Product"}
}'
"""
import json
import sys
from _temu_common import (
PROXY_URL,
call_temu_api,
load_json_arg,
parse_nested_body,
require_text,
resolve_access_token,
validate_management_type,
validate_site,
)
def build_request(params: dict) -> dict:
site = validate_site(require_text(params, "site"))
management_type = validate_management_type(
require_text(params, "managementType")
)
access_token = resolve_access_token(params)
api_type = require_text(params, "type")
body = {
"site": site,
"managementType": management_type,
"accessToken": access_token,
"type": api_type,
}
if "params" in params and params["params"] is not None:
if not isinstance(params["params"], dict):
print("Error: 'params' must be a JSON object.", file=sys.stderr)
sys.exit(1)
body["params"] = params["params"]
return body
def main():
if len(sys.argv) < 2:
print("Usage: temu_proxy.py '<JSON parameters>'", file=sys.stderr)
print(
'Example: temu_proxy.py \'{"site":"cn","managementType":"full-managed",'
'"accessToken":"TOKEN","type":"bg.goods.category.mapping",'
'"params":{"goodsName":"测试","goodsNameEn":"Test"}}\'',
file=sys.stderr,
)
sys.exit(1)
params = load_json_arg(sys.argv)
body = build_request(params)
result = call_temu_api(PROXY_URL, body, linkfox_params=params)
print(json.dumps(parse_nested_body(result), indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Temu accessToken authorization guide - LinkFox Skill
Returns step-by-step instructions from TEMU_API_README.md by shop type.
Usage:
python temu_token_guide.py '{"shopType":"semi-managed","tokenPurpose":"product-inventory","site":"cn"}'
python temu_token_guide.py '{"shopType":"semi-managed","tokenPurpose":"order-shipping","site":"us"}'
python temu_token_guide.py '{"shopType":"full-managed"}'
python temu_token_guide.py '{"shopType":"local-native"}'
"""
import json
import sys
from _temu_auth_guide import build_guide
def main():
if len(sys.argv) < 2:
print("Usage: temu_token_guide.py '<JSON parameters>'", file=sys.stderr)
print(
'Example: temu_token_guide.py \'{"shopType":"semi-managed",'
'"tokenPurpose":"product-inventory","site":"cn"}\'',
file=sys.stderr,
)
sys.exit(1)
try:
params = json.loads(sys.argv[1])
except json.JSONDecodeError as e:
print(f"Invalid parameter format: {e}", file=sys.stderr)
sys.exit(1)
shop_type = params.get("shopType") or params.get("managementType")
if not shop_type:
print("Error: 'shopType' is required.", file=sys.stderr)
sys.exit(1)
token_purpose = params.get("tokenPurpose", "default")
site = params.get("site")
try:
guide = build_guide(shop_type, token_purpose, site)
except ValueError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
print(json.dumps(guide, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""US site signed file download via /temu/fileDownload."""
import json
import sys
from _temu_common import load_json_arg
from _temu_us_common import DEFAULT_SITE, us_file_download_call
def main():
if len(sys.argv) < 2:
print(
'Usage: temu_us_file_download.py \'{"accessToken":"...","url":"https://..."}\'',
file=sys.stderr,
)
sys.exit(1)
params = load_json_arg(sys.argv)
params.setdefault("site", DEFAULT_SITE)
result = us_file_download_call(params)
print(json.dumps(result, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Temu US Promotion generic proxy (site=us, semi-managed by default).
Usage:
python temu_us_proxy.py '{
"accessToken": "TOKEN",
"tokenPurpose": "product-inventory",
"type": "<TEMU_API_TYPE>",
"params": {
"request": {}
}
}'
"""
import json
import sys
from _temu_common import load_json_arg, parse_nested_body, require_text
from _temu_us_common import DEFAULT_SITE, us_proxy_call
def main():
if len(sys.argv) < 2:
print("Usage: temu_us_proxy.py '<JSON>'", file=sys.stderr)
sys.exit(1)
params = load_json_arg(sys.argv)
params.setdefault("site", DEFAULT_SITE)
api_type = require_text(params, "type")
result = parse_nested_body(us_proxy_call(params, api_type))
print(json.dumps(result, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""bg.promotion.activity.candidate.goods.query — 活动候选商品查询."""
import _us_promotion_script as m
if __name__ == "__main__":
m.run_cli(
"bg.promotion.activity.candidate.goods.query",
"us_promotion_activity_candidate_goods_query.py '<JSON>'",
)
#!/usr/bin/env python3
"""bg.promotion.activity.goods.enroll — 活动商品报名."""
import _us_promotion_script as m
if __name__ == "__main__":
m.run_cli(
"bg.promotion.activity.goods.enroll",
"us_promotion_activity_goods_enroll.py '<JSON>'",
)
#!/usr/bin/env python3
"""bg.promotion.activity.goods.operation.query — 活动商品操作结果查询."""
import _us_promotion_script as m
if __name__ == "__main__":
m.run_cli(
"bg.promotion.activity.goods.operation.query",
"us_promotion_activity_goods_operation_query.py '<JSON>'",
)
#!/usr/bin/env python3
"""bg.promotion.activity.goods.query — 活动已报名商品查询."""
import _us_promotion_script as m
if __name__ == "__main__":
m.run_cli(
"bg.promotion.activity.goods.query",
"us_promotion_activity_goods_query.py '<JSON>'",
)
#!/usr/bin/env python3
"""bg.promotion.activity.goods.update — 活动商品更新."""
import _us_promotion_script as m
if __name__ == "__main__":
m.run_cli(
"bg.promotion.activity.goods.update",
"us_promotion_activity_goods_update.py '<JSON>'",
)
#!/usr/bin/env python3
"""bg.promotion.activity.query — 促销活动查询."""
import _us_promotion_script as m
if __name__ == "__main__":
m.run_cli(
"bg.promotion.activity.query",
"us_promotion_activity_query.py '<JSON>'",
)