@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.
- package/README.md +3 -3
- package/README.zh-CN.md +3 -3
- package/env.example +2 -2
- package/package.json +2 -2
- package/skills/social-media-crawl/SKILL.md +20 -16
- package/skills/social-media-crawl/agents/openai.yaml +2 -2
- package/skills/social-media-crawl/references/api-catalog/aliases.json +237 -0
- package/skills/social-media-crawl/references/api-catalog/capabilities.json +362 -0
- package/skills/social-media-crawl/references/api-catalog/metadata.json +11 -0
- package/skills/social-media-crawl/references/api-catalog/operations.json +100875 -0
- package/skills/social-media-crawl/references/api-catalog/overrides.json +10 -0
- package/skills/social-media-crawl/references/api-catalog/platforms.json +463 -0
- package/skills/social-media-crawl/references/api-routing-contract.md +73 -0
- package/skills/social-media-crawl/references/contracts/output-envelope.md +1 -1
- package/skills/social-media-crawl/scripts/core/api_catalog.py +104 -0
- package/skills/social-media-crawl/scripts/core/call_tikomni_api.py +269 -0
- package/skills/social-media-crawl/scripts/core/config_loader.py +0 -2
- package/skills/social-media-crawl/scripts/core/resolve_api_endpoint.py +176 -0
- package/skills/social-media-crawl/scripts/pipelines/run_douyin_single_work.py +16 -17
- package/skills/social-media-crawl/scripts/pipelines/run_xiaohongshu_single_work.py +16 -16
- package/skills/social-media-crawl/scripts/run_task.py +96 -0
- package/skills/social-media-crawl/tests/test_api_only_routing.py +130 -0
- package/skills/social-media-crawl/tests/test_fixed_pipeline_fallback.py +50 -0
- package/skills/social-media-crawl/references/guides/generic-mcp-objects.md +0 -40
- package/skills/social-media-crawl/references/mcp-usage-contract.md +0 -40
- 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
|
-
|
|
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
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
{
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
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
|
-
|
|
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
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
{
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
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())
|