@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,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"capability_overrides": {},
|
|
3
|
+
"notes": [
|
|
4
|
+
"endpoint_id is generated automatically and must not be manually overridden.",
|
|
5
|
+
"Overrides may adjust platform, capability, recommended endpoint, alternatives, and aliases only."
|
|
6
|
+
],
|
|
7
|
+
"platform_overrides": {},
|
|
8
|
+
"recommendation_overrides": {},
|
|
9
|
+
"schema_version": "v1"
|
|
10
|
+
}
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"aliases": [
|
|
4
|
+
"b23.tv",
|
|
5
|
+
"bilibili",
|
|
6
|
+
"b站",
|
|
7
|
+
"哔哩哔哩"
|
|
8
|
+
],
|
|
9
|
+
"capabilities": [
|
|
10
|
+
"comments",
|
|
11
|
+
"general",
|
|
12
|
+
"hot_search",
|
|
13
|
+
"livestream",
|
|
14
|
+
"media_upload",
|
|
15
|
+
"search",
|
|
16
|
+
"transcription",
|
|
17
|
+
"user_posts",
|
|
18
|
+
"user_profile",
|
|
19
|
+
"video_detail"
|
|
20
|
+
],
|
|
21
|
+
"operation_count": 41,
|
|
22
|
+
"slug": "bilibili"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"aliases": [
|
|
26
|
+
"demo"
|
|
27
|
+
],
|
|
28
|
+
"capabilities": [
|
|
29
|
+
"general"
|
|
30
|
+
],
|
|
31
|
+
"operation_count": 1,
|
|
32
|
+
"slug": "demo"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"aliases": [
|
|
36
|
+
"douyin",
|
|
37
|
+
"dy",
|
|
38
|
+
"iesdouyin.com",
|
|
39
|
+
"v.douyin.com",
|
|
40
|
+
"抖音"
|
|
41
|
+
],
|
|
42
|
+
"capabilities": [
|
|
43
|
+
"article_detail",
|
|
44
|
+
"comments",
|
|
45
|
+
"general",
|
|
46
|
+
"hot_search",
|
|
47
|
+
"livestream",
|
|
48
|
+
"media_upload",
|
|
49
|
+
"music",
|
|
50
|
+
"note_detail",
|
|
51
|
+
"product_detail",
|
|
52
|
+
"ranking",
|
|
53
|
+
"search",
|
|
54
|
+
"user_posts",
|
|
55
|
+
"user_profile",
|
|
56
|
+
"video_detail"
|
|
57
|
+
],
|
|
58
|
+
"operation_count": 292,
|
|
59
|
+
"slug": "douyin"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"aliases": [
|
|
63
|
+
"douyin_search"
|
|
64
|
+
],
|
|
65
|
+
"capabilities": [
|
|
66
|
+
"search"
|
|
67
|
+
],
|
|
68
|
+
"operation_count": 1,
|
|
69
|
+
"slug": "douyin_search"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"aliases": [
|
|
73
|
+
"health"
|
|
74
|
+
],
|
|
75
|
+
"capabilities": [
|
|
76
|
+
"general"
|
|
77
|
+
],
|
|
78
|
+
"operation_count": 1,
|
|
79
|
+
"slug": "health"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"aliases": [
|
|
83
|
+
"hybrid"
|
|
84
|
+
],
|
|
85
|
+
"capabilities": [
|
|
86
|
+
"video_detail"
|
|
87
|
+
],
|
|
88
|
+
"operation_count": 1,
|
|
89
|
+
"slug": "hybrid"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"aliases": [
|
|
93
|
+
"ig",
|
|
94
|
+
"ins",
|
|
95
|
+
"instagram"
|
|
96
|
+
],
|
|
97
|
+
"capabilities": [
|
|
98
|
+
"comments",
|
|
99
|
+
"general",
|
|
100
|
+
"music",
|
|
101
|
+
"ranking",
|
|
102
|
+
"search",
|
|
103
|
+
"user_posts",
|
|
104
|
+
"user_profile"
|
|
105
|
+
],
|
|
106
|
+
"operation_count": 89,
|
|
107
|
+
"slug": "instagram"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"aliases": [
|
|
111
|
+
"ios_shortcut"
|
|
112
|
+
],
|
|
113
|
+
"capabilities": [
|
|
114
|
+
"general"
|
|
115
|
+
],
|
|
116
|
+
"operation_count": 1,
|
|
117
|
+
"slug": "ios_shortcut"
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"aliases": [
|
|
121
|
+
"ks",
|
|
122
|
+
"kuaishou",
|
|
123
|
+
"快手"
|
|
124
|
+
],
|
|
125
|
+
"capabilities": [
|
|
126
|
+
"comments",
|
|
127
|
+
"general",
|
|
128
|
+
"hot_search",
|
|
129
|
+
"livestream",
|
|
130
|
+
"note_detail",
|
|
131
|
+
"product_detail",
|
|
132
|
+
"search",
|
|
133
|
+
"transcription",
|
|
134
|
+
"user_posts",
|
|
135
|
+
"user_profile",
|
|
136
|
+
"video_detail"
|
|
137
|
+
],
|
|
138
|
+
"operation_count": 34,
|
|
139
|
+
"slug": "kuaishou"
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"aliases": [
|
|
143
|
+
"lemon8"
|
|
144
|
+
],
|
|
145
|
+
"capabilities": [
|
|
146
|
+
"comments",
|
|
147
|
+
"general",
|
|
148
|
+
"hot_search",
|
|
149
|
+
"search",
|
|
150
|
+
"user_profile"
|
|
151
|
+
],
|
|
152
|
+
"operation_count": 16,
|
|
153
|
+
"slug": "lemon8"
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"aliases": [
|
|
157
|
+
"linkedin",
|
|
158
|
+
"领英"
|
|
159
|
+
],
|
|
160
|
+
"capabilities": [
|
|
161
|
+
"comments",
|
|
162
|
+
"general",
|
|
163
|
+
"search",
|
|
164
|
+
"user_posts",
|
|
165
|
+
"user_profile"
|
|
166
|
+
],
|
|
167
|
+
"operation_count": 25,
|
|
168
|
+
"slug": "linkedin"
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"aliases": [
|
|
172
|
+
"pipixia",
|
|
173
|
+
"皮皮虾"
|
|
174
|
+
],
|
|
175
|
+
"capabilities": [
|
|
176
|
+
"comments",
|
|
177
|
+
"general",
|
|
178
|
+
"hot_search",
|
|
179
|
+
"search",
|
|
180
|
+
"user_posts",
|
|
181
|
+
"user_profile",
|
|
182
|
+
"video_detail"
|
|
183
|
+
],
|
|
184
|
+
"operation_count": 17,
|
|
185
|
+
"slug": "pipixia"
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"aliases": [
|
|
189
|
+
"reddit"
|
|
190
|
+
],
|
|
191
|
+
"capabilities": [
|
|
192
|
+
"comments",
|
|
193
|
+
"general",
|
|
194
|
+
"livestream",
|
|
195
|
+
"note_detail",
|
|
196
|
+
"ranking",
|
|
197
|
+
"search",
|
|
198
|
+
"user_profile"
|
|
199
|
+
],
|
|
200
|
+
"operation_count": 24,
|
|
201
|
+
"slug": "reddit"
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
"aliases": [
|
|
205
|
+
"sora2"
|
|
206
|
+
],
|
|
207
|
+
"capabilities": [
|
|
208
|
+
"comments",
|
|
209
|
+
"media_upload",
|
|
210
|
+
"note_detail",
|
|
211
|
+
"search",
|
|
212
|
+
"user_profile"
|
|
213
|
+
],
|
|
214
|
+
"operation_count": 17,
|
|
215
|
+
"slug": "sora2"
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
"aliases": [
|
|
219
|
+
"temp_mail"
|
|
220
|
+
],
|
|
221
|
+
"capabilities": [
|
|
222
|
+
"general",
|
|
223
|
+
"user_profile"
|
|
224
|
+
],
|
|
225
|
+
"operation_count": 3,
|
|
226
|
+
"slug": "temp_mail"
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
"aliases": [
|
|
230
|
+
"threads"
|
|
231
|
+
],
|
|
232
|
+
"capabilities": [
|
|
233
|
+
"comments",
|
|
234
|
+
"search",
|
|
235
|
+
"user_posts",
|
|
236
|
+
"user_profile"
|
|
237
|
+
],
|
|
238
|
+
"operation_count": 11,
|
|
239
|
+
"slug": "threads"
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"aliases": [
|
|
243
|
+
"tik tok",
|
|
244
|
+
"tiktok"
|
|
245
|
+
],
|
|
246
|
+
"capabilities": [
|
|
247
|
+
"comments",
|
|
248
|
+
"general",
|
|
249
|
+
"livestream",
|
|
250
|
+
"media_upload",
|
|
251
|
+
"music",
|
|
252
|
+
"note_detail",
|
|
253
|
+
"product_detail",
|
|
254
|
+
"ranking",
|
|
255
|
+
"search",
|
|
256
|
+
"user_posts",
|
|
257
|
+
"user_profile",
|
|
258
|
+
"video_detail"
|
|
259
|
+
],
|
|
260
|
+
"operation_count": 208,
|
|
261
|
+
"slug": "tiktok"
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
"aliases": [
|
|
265
|
+
"toutiao",
|
|
266
|
+
"今日头条",
|
|
267
|
+
"头条"
|
|
268
|
+
],
|
|
269
|
+
"capabilities": [
|
|
270
|
+
"article_detail",
|
|
271
|
+
"comments",
|
|
272
|
+
"user_profile",
|
|
273
|
+
"video_detail"
|
|
274
|
+
],
|
|
275
|
+
"operation_count": 7,
|
|
276
|
+
"slug": "toutiao"
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
"aliases": [
|
|
280
|
+
"twitter",
|
|
281
|
+
"x",
|
|
282
|
+
"x.com",
|
|
283
|
+
"推特"
|
|
284
|
+
],
|
|
285
|
+
"capabilities": [
|
|
286
|
+
"comments",
|
|
287
|
+
"general",
|
|
288
|
+
"ranking",
|
|
289
|
+
"search",
|
|
290
|
+
"user_posts",
|
|
291
|
+
"user_profile"
|
|
292
|
+
],
|
|
293
|
+
"operation_count": 13,
|
|
294
|
+
"slug": "twitter"
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
"aliases": [
|
|
298
|
+
"asr",
|
|
299
|
+
"transcription",
|
|
300
|
+
"u2",
|
|
301
|
+
"语音转写",
|
|
302
|
+
"转文案"
|
|
303
|
+
],
|
|
304
|
+
"capabilities": [
|
|
305
|
+
"transcription"
|
|
306
|
+
],
|
|
307
|
+
"operation_count": 2,
|
|
308
|
+
"slug": "u2"
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
"aliases": [
|
|
312
|
+
"media upload",
|
|
313
|
+
"u3",
|
|
314
|
+
"upload",
|
|
315
|
+
"中转",
|
|
316
|
+
"媒体上传"
|
|
317
|
+
],
|
|
318
|
+
"capabilities": [
|
|
319
|
+
"media_upload"
|
|
320
|
+
],
|
|
321
|
+
"operation_count": 2,
|
|
322
|
+
"slug": "u3"
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
"aliases": [
|
|
326
|
+
"wechat"
|
|
327
|
+
],
|
|
328
|
+
"capabilities": [
|
|
329
|
+
"article_url"
|
|
330
|
+
],
|
|
331
|
+
"operation_count": 1,
|
|
332
|
+
"slug": "wechat"
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
"aliases": [
|
|
336
|
+
"wechat channels",
|
|
337
|
+
"wechat_channels",
|
|
338
|
+
"微信视频号",
|
|
339
|
+
"视频号"
|
|
340
|
+
],
|
|
341
|
+
"capabilities": [
|
|
342
|
+
"comments",
|
|
343
|
+
"hot_search",
|
|
344
|
+
"livestream",
|
|
345
|
+
"search",
|
|
346
|
+
"user_profile",
|
|
347
|
+
"video_detail"
|
|
348
|
+
],
|
|
349
|
+
"operation_count": 10,
|
|
350
|
+
"slug": "wechat_channels"
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
"aliases": [
|
|
354
|
+
"mp.weixin.qq.com",
|
|
355
|
+
"wechat official accounts",
|
|
356
|
+
"wechat_mp",
|
|
357
|
+
"公众号",
|
|
358
|
+
"微信公众号"
|
|
359
|
+
],
|
|
360
|
+
"capabilities": [
|
|
361
|
+
"article_detail",
|
|
362
|
+
"article_list",
|
|
363
|
+
"article_url",
|
|
364
|
+
"comments"
|
|
365
|
+
],
|
|
366
|
+
"operation_count": 10,
|
|
367
|
+
"slug": "wechat_mp"
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
"aliases": [
|
|
371
|
+
"weibo",
|
|
372
|
+
"微博"
|
|
373
|
+
],
|
|
374
|
+
"capabilities": [
|
|
375
|
+
"article_list",
|
|
376
|
+
"comments",
|
|
377
|
+
"general",
|
|
378
|
+
"hot_search",
|
|
379
|
+
"music",
|
|
380
|
+
"note_detail",
|
|
381
|
+
"ranking",
|
|
382
|
+
"search",
|
|
383
|
+
"transcription",
|
|
384
|
+
"user_posts",
|
|
385
|
+
"user_profile"
|
|
386
|
+
],
|
|
387
|
+
"operation_count": 64,
|
|
388
|
+
"slug": "weibo"
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
"aliases": [
|
|
392
|
+
"xhs",
|
|
393
|
+
"xhslink.com",
|
|
394
|
+
"xiaohongshu",
|
|
395
|
+
"xiaohongshu.com",
|
|
396
|
+
"小红书"
|
|
397
|
+
],
|
|
398
|
+
"capabilities": [
|
|
399
|
+
"comments",
|
|
400
|
+
"general",
|
|
401
|
+
"note_detail",
|
|
402
|
+
"product_detail",
|
|
403
|
+
"ranking",
|
|
404
|
+
"search",
|
|
405
|
+
"user_posts",
|
|
406
|
+
"user_profile"
|
|
407
|
+
],
|
|
408
|
+
"operation_count": 78,
|
|
409
|
+
"slug": "xiaohongshu"
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
"aliases": [
|
|
413
|
+
"xigua",
|
|
414
|
+
"西瓜视频"
|
|
415
|
+
],
|
|
416
|
+
"capabilities": [
|
|
417
|
+
"comments",
|
|
418
|
+
"search",
|
|
419
|
+
"user_posts",
|
|
420
|
+
"user_profile",
|
|
421
|
+
"video_detail"
|
|
422
|
+
],
|
|
423
|
+
"operation_count": 7,
|
|
424
|
+
"slug": "xigua"
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
"aliases": [
|
|
428
|
+
"youtu.be",
|
|
429
|
+
"youtube",
|
|
430
|
+
"yt"
|
|
431
|
+
],
|
|
432
|
+
"capabilities": [
|
|
433
|
+
"comments",
|
|
434
|
+
"general",
|
|
435
|
+
"livestream",
|
|
436
|
+
"media_upload",
|
|
437
|
+
"note_detail",
|
|
438
|
+
"ranking",
|
|
439
|
+
"search",
|
|
440
|
+
"transcription",
|
|
441
|
+
"user_profile",
|
|
442
|
+
"video_detail"
|
|
443
|
+
],
|
|
444
|
+
"operation_count": 44,
|
|
445
|
+
"slug": "youtube"
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
"aliases": [
|
|
449
|
+
"zhihu",
|
|
450
|
+
"知乎"
|
|
451
|
+
],
|
|
452
|
+
"capabilities": [
|
|
453
|
+
"article_detail",
|
|
454
|
+
"comments",
|
|
455
|
+
"general",
|
|
456
|
+
"search",
|
|
457
|
+
"user_profile",
|
|
458
|
+
"video_detail"
|
|
459
|
+
],
|
|
460
|
+
"operation_count": 34,
|
|
461
|
+
"slug": "zhihu"
|
|
462
|
+
}
|
|
463
|
+
]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# API Routing Contract
|
|
2
|
+
|
|
3
|
+
## Scope
|
|
4
|
+
|
|
5
|
+
This skill calls TikOmni API only and does not require any external server configuration.
|
|
6
|
+
|
|
7
|
+
Supported social-media tasks include single works, posts, creator homepages, comments, search results, rankings, livestreams, products, subtitles, transcripts, and structured fields.
|
|
8
|
+
|
|
9
|
+
## Required Inputs
|
|
10
|
+
|
|
11
|
+
- Auth: `TIKOMNI_API_KEY`.
|
|
12
|
+
- Default API base URL: `https://api.tikomni.com`.
|
|
13
|
+
- Optional override: `TIKOMNI_BASE_URL`.
|
|
14
|
+
|
|
15
|
+
Do not repeat the API key inside tool parameters.
|
|
16
|
+
|
|
17
|
+
## Required Route Order
|
|
18
|
+
|
|
19
|
+
1. Detect platform and capability.
|
|
20
|
+
2. If a fixed pipeline matches, run that fixed pipeline.
|
|
21
|
+
3. Otherwise resolve a TikOmni API endpoint:
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
python scripts/core/resolve_api_endpoint.py --platform <platform> --capability <capability> --json '<context>'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
4. Call the API by `endpoint_id`:
|
|
28
|
+
|
|
29
|
+
```text
|
|
30
|
+
python scripts/core/call_tikomni_api.py --endpoint-id <endpoint_id> --params '<json>'
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
5. For common tasks, the one-shot entry is allowed:
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
python scripts/run_task.py --platform <platform> --capability <capability> --params '<json>'
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Hard Rules
|
|
40
|
+
|
|
41
|
+
- Do not construct TikOmni API paths manually.
|
|
42
|
+
- Do not pass arbitrary `path` or `method` values to callers.
|
|
43
|
+
- Do not call endpoint paths that are absent from `references/api-catalog/operations.json`.
|
|
44
|
+
- Resolve `endpoint_id` first, then call the API.
|
|
45
|
+
- Return recommended endpoint and alternatives to the user or agent.
|
|
46
|
+
- Do not automatically call alternatives; alternatives are explicit next choices.
|
|
47
|
+
- If platform or capability is ambiguous, return a clarification request instead of guessing.
|
|
48
|
+
|
|
49
|
+
## Variant Priority
|
|
50
|
+
|
|
51
|
+
For endpoints under the same platform and capability, default priority is:
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
app_v3 > app_v2 > app_v1/app > web_v3 > web_v2 > web_v1/web
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The catalog generator may apply deterministic specificity rules for common tasks such as hot search or article detail. All alternatives must remain visible.
|
|
58
|
+
|
|
59
|
+
## U2/U3
|
|
60
|
+
|
|
61
|
+
U2 and U3 stay inside this skill because they support video-to-text workflows:
|
|
62
|
+
|
|
63
|
+
- U2 handles ASR / transcription.
|
|
64
|
+
- U3 handles media upload or relay when a source media URL is not directly usable.
|
|
65
|
+
|
|
66
|
+
Use them only as TikOmni API service capabilities.
|
|
67
|
+
|
|
68
|
+
## Output Rules
|
|
69
|
+
|
|
70
|
+
- Keep factual fields separate from derived metadata.
|
|
71
|
+
- Include `request_id` when the API returns one.
|
|
72
|
+
- Include `endpoint_id`, `platform`, `capability`, `variant`, and `alternatives`.
|
|
73
|
+
- Include `completeness`, `missing_fields`, `error_reason`, and `extract_trace` in normalized task outputs.
|
|
@@ -18,4 +18,4 @@
|
|
|
18
18
|
- `completeness` allows `complete`, `partial`, and `incomplete`.
|
|
19
19
|
- `missing_fields` is the list of missing fields.
|
|
20
20
|
- `error_reason` may be an empty string or `null` on success.
|
|
21
|
-
- `extract_trace` records
|
|
21
|
+
- `extract_trace` records fixed-pipeline steps or API-only resolver/caller steps, including `endpoint_id`, selected variant, alternatives, and request status when available.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Skill-local API catalog helpers."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict, Iterable, List, Optional
|
|
10
|
+
|
|
11
|
+
SKILL_ROOT = Path(__file__).resolve().parents[2]
|
|
12
|
+
CATALOG_ROOT = SKILL_ROOT / "references" / "api-catalog"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def normalize_key(value: Any) -> str:
|
|
16
|
+
text = str(value or "").strip().lower()
|
|
17
|
+
text = re.sub(r"\s+", " ", text)
|
|
18
|
+
return text
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def load_json(path: Path) -> Any:
|
|
22
|
+
with path.open("r", encoding="utf-8") as handle:
|
|
23
|
+
return json.load(handle)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_operations() -> List[Dict[str, Any]]:
|
|
27
|
+
payload = load_json(CATALOG_ROOT / "operations.json")
|
|
28
|
+
if not isinstance(payload, list):
|
|
29
|
+
raise ValueError("api_catalog_operations_must_be_list")
|
|
30
|
+
return [item for item in payload if isinstance(item, dict)]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def load_aliases() -> Dict[str, Any]:
|
|
34
|
+
payload = load_json(CATALOG_ROOT / "aliases.json")
|
|
35
|
+
if not isinstance(payload, dict):
|
|
36
|
+
raise ValueError("api_catalog_aliases_must_be_object")
|
|
37
|
+
return payload
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_alias_index(alias_map: Dict[str, Iterable[Any]]) -> Dict[str, str]:
|
|
41
|
+
index: Dict[str, str] = {}
|
|
42
|
+
for slug, aliases in alias_map.items():
|
|
43
|
+
index[normalize_key(slug)] = str(slug)
|
|
44
|
+
for alias in aliases or []:
|
|
45
|
+
normalized = normalize_key(alias)
|
|
46
|
+
if normalized:
|
|
47
|
+
index[normalized] = str(slug)
|
|
48
|
+
return index
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def resolve_alias(value: Optional[str], alias_map: Dict[str, Iterable[Any]]) -> str:
|
|
52
|
+
if not value:
|
|
53
|
+
return ""
|
|
54
|
+
normalized = normalize_key(value)
|
|
55
|
+
index = build_alias_index(alias_map)
|
|
56
|
+
return index.get(normalized, normalized.replace("-", "_").replace(" ", "_"))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _has_non_ascii(value: str) -> bool:
|
|
60
|
+
return any(ord(char) > 127 for char in value)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _alias_in_text(alias: str, normalized_text: str) -> bool:
|
|
64
|
+
if not alias:
|
|
65
|
+
return False
|
|
66
|
+
if _has_non_ascii(alias):
|
|
67
|
+
return alias in normalized_text
|
|
68
|
+
pattern = rf"(?<![a-z0-9_]){re.escape(alias)}(?![a-z0-9_])"
|
|
69
|
+
return re.search(pattern, normalized_text) is not None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def infer_alias_from_text(text: str, alias_map: Dict[str, Iterable[Any]]) -> str:
|
|
73
|
+
normalized_text = normalize_key(text)
|
|
74
|
+
if not normalized_text:
|
|
75
|
+
return ""
|
|
76
|
+
index = build_alias_index(alias_map)
|
|
77
|
+
hits: List[tuple[int, str, str]] = []
|
|
78
|
+
for alias, slug in index.items():
|
|
79
|
+
if not alias:
|
|
80
|
+
continue
|
|
81
|
+
if _alias_in_text(alias, normalized_text):
|
|
82
|
+
hits.append((len(alias), alias, slug))
|
|
83
|
+
if not hits:
|
|
84
|
+
return ""
|
|
85
|
+
hits.sort(reverse=True)
|
|
86
|
+
return hits[0][2]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def operation_by_endpoint_id(operations: Iterable[Dict[str, Any]], endpoint_id: str) -> Optional[Dict[str, Any]]:
|
|
90
|
+
for operation in operations:
|
|
91
|
+
if operation.get("endpoint_id") == endpoint_id:
|
|
92
|
+
return operation
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def sorted_operations(operations: Iterable[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
97
|
+
return sorted(
|
|
98
|
+
operations,
|
|
99
|
+
key=lambda item: (
|
|
100
|
+
not bool(item.get("recommended")),
|
|
101
|
+
str(item.get("variant") or ""),
|
|
102
|
+
str(item.get("path") or ""),
|
|
103
|
+
),
|
|
104
|
+
)
|