@tikomni/skills 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/README.md +3 -3
  2. package/README.zh-CN.md +3 -3
  3. package/env.example +2 -2
  4. package/package.json +2 -2
  5. package/skills/social-media-crawl/SKILL.md +20 -16
  6. package/skills/social-media-crawl/agents/openai.yaml +2 -2
  7. package/skills/social-media-crawl/references/api-catalog/aliases.json +237 -0
  8. package/skills/social-media-crawl/references/api-catalog/capabilities.json +362 -0
  9. package/skills/social-media-crawl/references/api-catalog/metadata.json +11 -0
  10. package/skills/social-media-crawl/references/api-catalog/operations.json +100875 -0
  11. package/skills/social-media-crawl/references/api-catalog/overrides.json +10 -0
  12. package/skills/social-media-crawl/references/api-catalog/platforms.json +463 -0
  13. package/skills/social-media-crawl/references/api-routing-contract.md +73 -0
  14. package/skills/social-media-crawl/references/contracts/output-envelope.md +1 -1
  15. package/skills/social-media-crawl/scripts/core/api_catalog.py +104 -0
  16. package/skills/social-media-crawl/scripts/core/call_tikomni_api.py +269 -0
  17. package/skills/social-media-crawl/scripts/core/config_loader.py +0 -2
  18. package/skills/social-media-crawl/scripts/core/resolve_api_endpoint.py +176 -0
  19. package/skills/social-media-crawl/scripts/pipelines/run_douyin_single_work.py +16 -17
  20. package/skills/social-media-crawl/scripts/pipelines/run_xiaohongshu_single_work.py +16 -16
  21. package/skills/social-media-crawl/scripts/run_task.py +96 -0
  22. package/skills/social-media-crawl/tests/test_api_only_routing.py +130 -0
  23. package/skills/social-media-crawl/tests/test_fixed_pipeline_fallback.py +50 -0
  24. package/skills/social-media-crawl/references/guides/generic-mcp-objects.md +0 -40
  25. package/skills/social-media-crawl/references/mcp-usage-contract.md +0 -40
  26. package/skills/social-media-crawl/scripts/core/mcp_dispatch.py +0 -161
@@ -0,0 +1,269 @@
1
+ #!/usr/bin/env python3
2
+ """Call TikOmni API by catalog endpoint_id."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import re
9
+ import sys
10
+ from pathlib import Path
11
+ import urllib.parse
12
+ from typing import Any, Dict, List, Optional, Tuple
13
+
14
+ SKILL_ROOT = Path(__file__).resolve().parents[2]
15
+ if str(SKILL_ROOT) not in sys.path:
16
+ sys.path.insert(0, str(SKILL_ROOT))
17
+
18
+ from scripts.core.api_catalog import load_operations, operation_by_endpoint_id
19
+ from scripts.core.tikomni_common import call_json_api, resolve_runtime
20
+
21
+ RESERVED_PARAM_KEYS = {"path", "method"}
22
+
23
+
24
+ def _json_stdout(payload: Dict[str, Any]) -> None:
25
+ print(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True))
26
+
27
+
28
+ def _parse_params(raw_params: str) -> Dict[str, Any]:
29
+ if not raw_params:
30
+ return {}
31
+ payload = json.loads(raw_params)
32
+ if not isinstance(payload, dict):
33
+ raise ValueError("params_must_be_json_object")
34
+ return payload
35
+
36
+
37
+ def _schema_type(schema: Dict[str, Any]) -> str:
38
+ raw_type = schema.get("type")
39
+ if isinstance(raw_type, list):
40
+ return "|".join(str(item) for item in raw_type)
41
+ return str(raw_type or "")
42
+
43
+
44
+ def _validate_type(name: str, value: Any, schema: Dict[str, Any]) -> Optional[str]:
45
+ schema_type = _schema_type(schema)
46
+ if not schema_type:
47
+ return None
48
+ allowed = set(schema_type.split("|"))
49
+ if "null" in allowed and value is None:
50
+ return None
51
+ if "string" in allowed and isinstance(value, str):
52
+ return None
53
+ if "integer" in allowed and isinstance(value, int) and not isinstance(value, bool):
54
+ return None
55
+ if "number" in allowed and isinstance(value, (int, float)) and not isinstance(value, bool):
56
+ return None
57
+ if "boolean" in allowed and isinstance(value, bool):
58
+ return None
59
+ if "array" in allowed and isinstance(value, list):
60
+ return None
61
+ if "object" in allowed and isinstance(value, dict):
62
+ return None
63
+ return f"invalid_type:{name}:expected_{schema_type or 'declared'}"
64
+
65
+
66
+ def _parameter_maps(operation: Dict[str, Any]) -> Tuple[Dict[str, Dict[str, Any]], Dict[str, Dict[str, Any]], Dict[str, Dict[str, Any]]]:
67
+ path_params: Dict[str, Dict[str, Any]] = {}
68
+ query_params: Dict[str, Dict[str, Any]] = {}
69
+ header_params: Dict[str, Dict[str, Any]] = {}
70
+ for param in operation.get("parameters") or []:
71
+ if not isinstance(param, dict):
72
+ continue
73
+ name = str(param.get("name") or "")
74
+ location = str(param.get("in") or "query")
75
+ if not name:
76
+ continue
77
+ if location == "path":
78
+ path_params[name] = param
79
+ elif location == "header":
80
+ header_params[name] = param
81
+ else:
82
+ query_params[name] = param
83
+ return path_params, query_params, header_params
84
+
85
+
86
+ def _body_properties(operation: Dict[str, Any]) -> Tuple[Dict[str, Any], bool, bool]:
87
+ body = operation.get("request_body") if isinstance(operation.get("request_body"), dict) else None
88
+ if not body:
89
+ return {}, False, False
90
+ schema = body.get("schema") if isinstance(body.get("schema"), dict) else {}
91
+ props = schema.get("properties") if isinstance(schema.get("properties"), dict) else {}
92
+ return props, bool(body.get("required")), True
93
+
94
+
95
+ def _render_path(path: str, params: Dict[str, Any], path_params: Dict[str, Dict[str, Any]]) -> Tuple[str, List[str]]:
96
+ missing: List[str] = []
97
+
98
+ def replace(match: re.Match[str]) -> str:
99
+ name = match.group(1)
100
+ if name not in params or params.get(name) in (None, ""):
101
+ missing.append(name)
102
+ return match.group(0)
103
+ return urllib.parse.quote(str(params[name]), safe="")
104
+
105
+ rendered = re.sub(r"\{([^}]+)\}", replace, path)
106
+ for name, param in path_params.items():
107
+ if param.get("required") and (name not in params or params.get(name) in (None, "")):
108
+ missing.append(name)
109
+ return rendered, sorted(set(missing))
110
+
111
+
112
+ def build_request(operation: Dict[str, Any], params: Dict[str, Any]) -> Tuple[Optional[Dict[str, Any]], Optional[Dict[str, Any]]]:
113
+ input_schema = operation.get("input_schema") if isinstance(operation.get("input_schema"), dict) else {}
114
+ properties = input_schema.get("properties") if isinstance(input_schema.get("properties"), dict) else {}
115
+ reserved = sorted(key for key in params.keys() if key in RESERVED_PARAM_KEYS and key not in properties)
116
+ if reserved:
117
+ return None, {"code": "RESERVED_PARAM_KEY", "message": f"Reserved API routing keys are not allowed in params: {', '.join(reserved)}"}
118
+
119
+ required = set(str(item) for item in input_schema.get("required") or [])
120
+ missing_required = sorted(name for name in required if name not in params or params.get(name) in (None, ""))
121
+ if missing_required:
122
+ return None, {"code": "MISSING_REQUIRED_PARAMS", "message": f"Missing required params: {', '.join(missing_required)}"}
123
+
124
+ unknown = sorted(key for key in params.keys() if key not in properties)
125
+ if unknown:
126
+ return None, {"code": "UNKNOWN_PARAMS", "message": f"Unknown params for endpoint_id={operation.get('endpoint_id')}: {', '.join(unknown)}"}
127
+
128
+ for name, value in params.items():
129
+ schema = properties.get(name) if isinstance(properties.get(name), dict) else {}
130
+ error = _validate_type(name, value, schema)
131
+ if error:
132
+ return None, {"code": "INVALID_PARAM_TYPE", "message": error}
133
+
134
+ path_params, query_params, header_params = _parameter_maps(operation)
135
+ path, missing_path = _render_path(str(operation.get("path") or ""), params, path_params)
136
+ if missing_path:
137
+ return None, {"code": "MISSING_PATH_PARAMS", "message": f"Missing path params: {', '.join(missing_path)}"}
138
+
139
+ query = {name: params[name] for name in query_params.keys() if name in params}
140
+ headers = {name: str(params[name]) for name in header_params.keys() if name in params}
141
+ body_props, body_required, has_body = _body_properties(operation)
142
+ body: Optional[Dict[str, Any]] = None
143
+ if has_body:
144
+ if body_props:
145
+ body = {name: params[name] for name in body_props.keys() if name in params}
146
+ elif "body" in params:
147
+ raw_body = params["body"]
148
+ if not isinstance(raw_body, dict):
149
+ return None, {"code": "INVALID_PARAM_TYPE", "message": "invalid_type:body:expected_object"}
150
+ body = raw_body
151
+ elif body_required:
152
+ return None, {"code": "MISSING_REQUIRED_PARAMS", "message": "Missing required params: body"}
153
+ else:
154
+ body = {}
155
+
156
+ return {
157
+ "method": str(operation.get("method") or "GET"),
158
+ "path": path,
159
+ "query": query,
160
+ "headers": headers,
161
+ "body": body,
162
+ }, None
163
+
164
+
165
+ def call_endpoint(
166
+ *,
167
+ endpoint_id: str,
168
+ params: Dict[str, Any],
169
+ env_file: Optional[str] = None,
170
+ base_url: Optional[str] = None,
171
+ timeout_ms: Optional[int] = None,
172
+ allow_process_env: bool = False,
173
+ dry_run: bool = False,
174
+ ) -> Dict[str, Any]:
175
+ operations = load_operations()
176
+ operation = operation_by_endpoint_id(operations, endpoint_id)
177
+ if not operation:
178
+ return {
179
+ "ok": False,
180
+ "code": "UNKNOWN_ENDPOINT_ID",
181
+ "endpoint_id": endpoint_id,
182
+ "error_reason": f"endpoint_id_not_found:{endpoint_id}",
183
+ }
184
+
185
+ request_plan, validation_error = build_request(operation, params)
186
+ if validation_error:
187
+ return {
188
+ "ok": False,
189
+ "code": validation_error["code"],
190
+ "endpoint_id": endpoint_id,
191
+ "platform": operation.get("platform"),
192
+ "capability": operation.get("capability"),
193
+ "variant": operation.get("variant"),
194
+ "error_reason": validation_error["message"],
195
+ "alternatives": operation.get("alternatives") or [],
196
+ }
197
+
198
+ if dry_run:
199
+ return {
200
+ "ok": True,
201
+ "dry_run": True,
202
+ "endpoint_id": endpoint_id,
203
+ "platform": operation.get("platform"),
204
+ "capability": operation.get("capability"),
205
+ "variant": operation.get("variant"),
206
+ "alternatives": operation.get("alternatives") or [],
207
+ "request": request_plan,
208
+ }
209
+
210
+ runtime = resolve_runtime(
211
+ env_file=env_file,
212
+ api_key_env="TIKOMNI_API_KEY",
213
+ base_url=base_url,
214
+ timeout_ms=timeout_ms,
215
+ allow_process_env=allow_process_env,
216
+ )
217
+ response = call_json_api(
218
+ base_url=str(runtime["base_url"]),
219
+ path=str(request_plan["path"]),
220
+ token=str(runtime["token"]),
221
+ method=str(request_plan["method"]),
222
+ timeout_ms=int(runtime["timeout_ms"]),
223
+ params=request_plan.get("query") if isinstance(request_plan.get("query"), dict) else None,
224
+ body=request_plan.get("body") if isinstance(request_plan.get("body"), dict) else None,
225
+ extra_headers=request_plan.get("headers") if isinstance(request_plan.get("headers"), dict) else None,
226
+ )
227
+ return {
228
+ "ok": bool(response.get("ok")),
229
+ "status_code": response.get("status_code"),
230
+ "request_id": response.get("request_id"),
231
+ "endpoint_id": endpoint_id,
232
+ "platform": operation.get("platform"),
233
+ "capability": operation.get("capability"),
234
+ "variant": operation.get("variant"),
235
+ "alternatives": operation.get("alternatives") or [],
236
+ "error_reason": response.get("error_reason"),
237
+ "data": response.get("data") if response.get("ok") else {},
238
+ }
239
+
240
+
241
+ def main(argv: Optional[List[str]] = None) -> int:
242
+ parser = argparse.ArgumentParser(description="Call TikOmni API by endpoint_id.")
243
+ parser.add_argument("--endpoint-id", required=True)
244
+ parser.add_argument("--params", default="{}")
245
+ parser.add_argument("--env-file", default=None)
246
+ parser.add_argument("--base-url", default=None)
247
+ parser.add_argument("--timeout-ms", type=int, default=None)
248
+ parser.add_argument("--allow-process-env", action="store_true")
249
+ parser.add_argument("--dry-run", action="store_true")
250
+ args = parser.parse_args(argv)
251
+ try:
252
+ params = _parse_params(args.params)
253
+ payload = call_endpoint(
254
+ endpoint_id=args.endpoint_id,
255
+ params=params,
256
+ env_file=args.env_file,
257
+ base_url=args.base_url,
258
+ timeout_ms=args.timeout_ms,
259
+ allow_process_env=bool(args.allow_process_env),
260
+ dry_run=bool(args.dry_run),
261
+ )
262
+ except Exception as exc:
263
+ payload = {"ok": False, "code": "CALLER_EXCEPTION", "error_reason": str(exc)}
264
+ _json_stdout(payload)
265
+ return 0 if payload.get("ok") else 2
266
+
267
+
268
+ if __name__ == "__main__":
269
+ sys.exit(main())
@@ -23,7 +23,6 @@ BUILTIN_DEFAULT_CONFIG: Dict[str, Any] = {
23
23
  "profile": "default",
24
24
  "runtime": {
25
25
  "base_url": "https://api.tikomni.com",
26
- "mcp_url": "https://mcp.tikomni.com/mcp",
27
26
  "auth_env_key": "TIKOMNI_API_KEY",
28
27
  "timeout_ms": 60000,
29
28
  "u2_pending_timeout_sec": 60,
@@ -246,7 +245,6 @@ def apply_env_overrides(config: Dict[str, Any], env_values: Optional[Dict[str, s
246
245
 
247
246
  for env_key, config_key in {
248
247
  "TIKOMNI_BASE_URL": "base_url",
249
- "TIKOMNI_MCP_URL": "mcp_url",
250
248
  }.items():
251
249
  value = _env_text(env_key, env_values=env_values)
252
250
  if value is not None:
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env python3
2
+ """Resolve a TikOmni API endpoint_id from platform and capability."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ SKILL_ROOT = Path(__file__).resolve().parents[2]
13
+ if str(SKILL_ROOT) not in sys.path:
14
+ sys.path.insert(0, str(SKILL_ROOT))
15
+
16
+ from scripts.core.api_catalog import (
17
+ infer_alias_from_text,
18
+ load_aliases,
19
+ load_operations,
20
+ resolve_alias,
21
+ sorted_operations,
22
+ )
23
+
24
+
25
+ def _json_stdout(payload: Dict[str, Any]) -> None:
26
+ print(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True))
27
+
28
+
29
+ def _load_context(raw_json: Optional[str]) -> Dict[str, Any]:
30
+ if not raw_json:
31
+ return {}
32
+ try:
33
+ payload = json.loads(raw_json)
34
+ except Exception:
35
+ return {"_raw": raw_json}
36
+ return payload if isinstance(payload, dict) else {"value": payload}
37
+
38
+
39
+ def _suggestions(platform: str, capability: str, operations: List[Dict[str, Any]]) -> List[Dict[str, str]]:
40
+ suggestions: List[Dict[str, str]] = []
41
+ if platform == "wechat" and capability.startswith("article"):
42
+ suggestions.append({"platform": "wechat_mp", "capability": capability})
43
+ if platform == "wechat" and capability in {"video_detail", "comments", "search", "hot_search"}:
44
+ suggestions.append({"platform": "wechat_channels", "capability": capability})
45
+
46
+ for operation in operations:
47
+ if capability and operation.get("capability") != capability:
48
+ continue
49
+ item = {
50
+ "platform": str(operation.get("platform") or ""),
51
+ "capability": str(operation.get("capability") or ""),
52
+ }
53
+ if item["platform"] and item not in suggestions:
54
+ suggestions.append(item)
55
+ if len(suggestions) >= 8:
56
+ break
57
+ return suggestions
58
+
59
+
60
+ def resolve_endpoint(
61
+ *,
62
+ platform: str = "",
63
+ capability: str = "",
64
+ intent: str = "",
65
+ url: str = "",
66
+ context: Optional[Dict[str, Any]] = None,
67
+ include_demo: bool = False,
68
+ ) -> Dict[str, Any]:
69
+ aliases = load_aliases()
70
+ operations = load_operations()
71
+ context = context or {}
72
+
73
+ platform_aliases = aliases.get("platforms") if isinstance(aliases.get("platforms"), dict) else {}
74
+ capability_aliases = aliases.get("capabilities") if isinstance(aliases.get("capabilities"), dict) else {}
75
+ text = " ".join(
76
+ str(value)
77
+ for value in [
78
+ intent,
79
+ url,
80
+ context.get("intent"),
81
+ context.get("url"),
82
+ context.get("share_url"),
83
+ context.get("input"),
84
+ ]
85
+ if value
86
+ )
87
+
88
+ resolved_platform = resolve_alias(platform, platform_aliases)
89
+ if not resolved_platform:
90
+ resolved_platform = infer_alias_from_text(text, platform_aliases)
91
+
92
+ resolved_capability = resolve_alias(capability, capability_aliases)
93
+ if not resolved_capability:
94
+ resolved_capability = infer_alias_from_text(text, capability_aliases)
95
+
96
+ if not resolved_platform or not resolved_capability:
97
+ return {
98
+ "ok": False,
99
+ "code": "ENDPOINT_NOT_RESOLVED",
100
+ "message": "platform and capability are required for deterministic API endpoint resolution.",
101
+ "platform": resolved_platform,
102
+ "capability": resolved_capability,
103
+ "suggestions": _suggestions(resolved_platform, resolved_capability, operations),
104
+ }
105
+
106
+ candidates = [
107
+ operation
108
+ for operation in operations
109
+ if operation.get("platform") == resolved_platform
110
+ and operation.get("capability") == resolved_capability
111
+ and (include_demo or not operation.get("demo"))
112
+ ]
113
+ if not candidates:
114
+ return {
115
+ "ok": False,
116
+ "code": "ENDPOINT_NOT_RESOLVED",
117
+ "message": (
118
+ "No TikOmni public API endpoint matches "
119
+ f"platform={resolved_platform} capability={resolved_capability}."
120
+ ),
121
+ "platform": resolved_platform,
122
+ "capability": resolved_capability,
123
+ "suggestions": _suggestions(resolved_platform, resolved_capability, operations),
124
+ }
125
+
126
+ ordered = sorted_operations(candidates)
127
+ recommended = ordered[0]
128
+ alternatives = [
129
+ {
130
+ "endpoint_id": str(operation.get("endpoint_id") or ""),
131
+ "variant": str(operation.get("variant") or ""),
132
+ "method": str(operation.get("method") or ""),
133
+ "path": str(operation.get("path") or ""),
134
+ "summary": str(operation.get("summary") or ""),
135
+ }
136
+ for operation in ordered[1:]
137
+ ]
138
+ return {
139
+ "ok": True,
140
+ "platform": resolved_platform,
141
+ "capability": resolved_capability,
142
+ "recommended": {
143
+ "endpoint_id": str(recommended.get("endpoint_id") or ""),
144
+ "method": str(recommended.get("method") or ""),
145
+ "path": str(recommended.get("path") or ""),
146
+ "variant": str(recommended.get("variant") or ""),
147
+ "summary": str(recommended.get("summary") or ""),
148
+ },
149
+ "alternatives": alternatives,
150
+ "reason": "catalog_recommended_or_variant_priority",
151
+ }
152
+
153
+
154
+ def main(argv: Optional[List[str]] = None) -> int:
155
+ parser = argparse.ArgumentParser(description="Resolve a TikOmni API endpoint_id.")
156
+ parser.add_argument("--platform", default="")
157
+ parser.add_argument("--capability", default="")
158
+ parser.add_argument("--intent", default="")
159
+ parser.add_argument("--url", default="")
160
+ parser.add_argument("--json", default="", help="Optional JSON context.")
161
+ parser.add_argument("--include-demo", action="store_true")
162
+ args = parser.parse_args(argv)
163
+ payload = resolve_endpoint(
164
+ platform=args.platform,
165
+ capability=args.capability,
166
+ intent=args.intent,
167
+ url=args.url,
168
+ context=_load_context(args.json),
169
+ include_demo=bool(args.include_demo),
170
+ )
171
+ _json_stdout(payload)
172
+ return 0 if payload.get("ok") else 2
173
+
174
+
175
+ if __name__ == "__main__":
176
+ sys.exit(main())
@@ -424,7 +424,6 @@ def _u1_fetch_one_video(
424
424
  fallback_reason="" if app_response.get("ok") else (
425
425
  "primary_timeout_retry_exhausted" if app_response.get("timeout_retry_exhausted") else "primary_non_timeout_failure"
426
426
  ),
427
- extra={"response": app_response},
428
427
  )
429
428
  )
430
429
  if app_response.get("ok"):
@@ -458,7 +457,6 @@ def _u1_fetch_one_video(
458
457
  fallback_reason="" if web_response.get("ok") else (
459
458
  "fallback_timeout_retry_exhausted" if web_response.get("timeout_retry_exhausted") else "fallback_non_timeout_failure"
460
459
  ),
461
- extra={"response": web_response},
462
460
  )
463
461
  )
464
462
  web_response["_attempts"] = attempts
@@ -891,23 +889,24 @@ def run_douyin_single_video(
891
889
  all_routes_failed=not bool(one_video_response.get("ok")),
892
890
  )
893
891
  for index, attempt in enumerate(attempts, start=1):
894
- response = attempt.get("response") if isinstance(attempt, dict) else None
892
+ if not isinstance(attempt, dict):
893
+ continue
894
+ response = attempt.get("response") if isinstance(attempt.get("response"), dict) else attempt
895
895
  endpoint = attempt.get("endpoint") if isinstance(attempt, dict) else None
896
896
  label = attempt.get("route_label") if isinstance(attempt, dict) else None
897
- if not isinstance(response, dict):
898
- if attempt.get("skipped"):
899
- trace.append(
900
- {
901
- "step": f"u1_fetch_one_video_attempt_{index}",
902
- "route_label": label,
903
- "endpoint": endpoint,
904
- "accept_reason": attempt.get("accept_reason"),
905
- "fallback_reason": attempt.get("fallback_reason"),
906
- "param_readiness": attempt.get("param_readiness"),
907
- "param_reason": attempt.get("param_reason"),
908
- "skipped": True,
909
- }
910
- )
897
+ if attempt.get("skipped"):
898
+ trace.append(
899
+ {
900
+ "step": f"u1_fetch_one_video_attempt_{index}",
901
+ "route_label": label,
902
+ "endpoint": endpoint,
903
+ "accept_reason": attempt.get("accept_reason"),
904
+ "fallback_reason": attempt.get("fallback_reason"),
905
+ "param_readiness": attempt.get("param_readiness"),
906
+ "param_reason": attempt.get("param_reason"),
907
+ "skipped": True,
908
+ }
909
+ )
911
910
  continue
912
911
  _emit_http_progress(progress, stage="single_video.fetch", response=response, route_label=str(label or "route"))
913
912
  step = "u1_fetch_one_video_effective" if index == len(attempts) else f"u1_fetch_one_video_attempt_{index}"
@@ -1125,7 +1125,6 @@ def _fetch_note_info(
1125
1125
  extra={
1126
1126
  "method": str(route["method"]).upper(),
1127
1127
  "field_completeness": response.get("_field_completeness"),
1128
- "response": response,
1129
1128
  },
1130
1129
  )
1131
1130
  )
@@ -1843,23 +1842,24 @@ def run_xiaohongshu_extract(
1843
1842
  all_routes_failed=not bool(note_response.get("ok")),
1844
1843
  )
1845
1844
  for index, attempt in enumerate(attempts, start=1):
1846
- response = attempt.get("response") if isinstance(attempt, dict) else None
1845
+ if not isinstance(attempt, dict):
1846
+ continue
1847
+ response = attempt.get("response") if isinstance(attempt.get("response"), dict) else attempt
1847
1848
  endpoint = attempt.get("endpoint") if isinstance(attempt, dict) else None
1848
1849
  label = attempt.get("route_label") if isinstance(attempt, dict) else None
1849
- if not isinstance(response, dict):
1850
- if attempt.get("skipped"):
1851
- trace.append(
1852
- {
1853
- "step": f"u1_get_note_info_attempt_{index}",
1854
- "route_label": label,
1855
- "endpoint": endpoint,
1856
- "accept_reason": attempt.get("accept_reason"),
1857
- "fallback_reason": attempt.get("fallback_reason"),
1858
- "param_readiness": attempt.get("param_readiness"),
1859
- "param_reason": attempt.get("param_reason"),
1860
- "skipped": True,
1861
- }
1862
- )
1850
+ if attempt.get("skipped"):
1851
+ trace.append(
1852
+ {
1853
+ "step": f"u1_get_note_info_attempt_{index}",
1854
+ "route_label": label,
1855
+ "endpoint": endpoint,
1856
+ "accept_reason": attempt.get("accept_reason"),
1857
+ "fallback_reason": attempt.get("fallback_reason"),
1858
+ "param_readiness": attempt.get("param_readiness"),
1859
+ "param_reason": attempt.get("param_reason"),
1860
+ "skipped": True,
1861
+ }
1862
+ )
1863
1863
  continue
1864
1864
  step = "u1_get_note_info_effective" if index == len(attempts) else f"u1_get_note_info_attempt_{index}"
1865
1865
  trace.append(
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env python3
2
+ """Minimal one-shot TikOmni API task runner."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ SKILL_ROOT = Path(__file__).resolve().parents[1]
13
+ if str(SKILL_ROOT) not in sys.path:
14
+ sys.path.insert(0, str(SKILL_ROOT))
15
+
16
+ from scripts.core.call_tikomni_api import call_endpoint
17
+ from scripts.core.resolve_api_endpoint import resolve_endpoint
18
+
19
+
20
+ def _json_stdout(payload: Dict[str, Any]) -> None:
21
+ print(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True))
22
+
23
+
24
+ def _parse_params(raw_params: str) -> Dict[str, Any]:
25
+ if not raw_params:
26
+ return {}
27
+ payload = json.loads(raw_params)
28
+ if not isinstance(payload, dict):
29
+ raise ValueError("params_must_be_json_object")
30
+ return payload
31
+
32
+
33
+ def run_task(
34
+ *,
35
+ intent: str,
36
+ params: Dict[str, Any],
37
+ platform: str = "",
38
+ capability: str = "",
39
+ dry_run: bool = False,
40
+ allow_process_env: bool = False,
41
+ ) -> Dict[str, Any]:
42
+ resolved = resolve_endpoint(
43
+ platform=platform,
44
+ capability=capability,
45
+ intent=intent,
46
+ context=params,
47
+ )
48
+ if not resolved.get("ok"):
49
+ return resolved
50
+
51
+ recommended = resolved.get("recommended") if isinstance(resolved.get("recommended"), dict) else {}
52
+ endpoint_id = str(recommended.get("endpoint_id") or "")
53
+ if not endpoint_id:
54
+ return {
55
+ "ok": False,
56
+ "code": "ENDPOINT_NOT_RESOLVED",
57
+ "error_reason": "resolver_returned_empty_endpoint_id",
58
+ "resolution": resolved,
59
+ }
60
+
61
+ response = call_endpoint(
62
+ endpoint_id=endpoint_id,
63
+ params=params,
64
+ allow_process_env=allow_process_env,
65
+ dry_run=dry_run,
66
+ )
67
+ response["resolution"] = resolved
68
+ return response
69
+
70
+
71
+ def main(argv: Optional[List[str]] = None) -> int:
72
+ parser = argparse.ArgumentParser(description="Run a deterministic TikOmni API task.")
73
+ parser.add_argument("--intent", default="")
74
+ parser.add_argument("--platform", default="")
75
+ parser.add_argument("--capability", default="")
76
+ parser.add_argument("--params", default="{}")
77
+ parser.add_argument("--dry-run", action="store_true")
78
+ parser.add_argument("--allow-process-env", action="store_true")
79
+ args = parser.parse_args(argv)
80
+ try:
81
+ payload = run_task(
82
+ intent=args.intent,
83
+ params=_parse_params(args.params),
84
+ platform=args.platform,
85
+ capability=args.capability,
86
+ dry_run=bool(args.dry_run),
87
+ allow_process_env=bool(args.allow_process_env),
88
+ )
89
+ except Exception as exc:
90
+ payload = {"ok": False, "code": "RUNNER_EXCEPTION", "error_reason": str(exc)}
91
+ _json_stdout(payload)
92
+ return 0 if payload.get("ok") else 2
93
+
94
+
95
+ if __name__ == "__main__":
96
+ sys.exit(main())