@9000ai/cli 0.4.0 → 0.5.1

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.
@@ -1,497 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import argparse
4
- import csv
5
- import json
6
- import sys
7
- from datetime import datetime
8
- from pathlib import Path
9
- from typing import Any
10
-
11
- REPO_ROOT = Path(__file__).resolve().parents[2]
12
- HUB_ROOT = REPO_ROOT / "9000AI-hub-9000AI"
13
- if str(HUB_ROOT) not in sys.path:
14
- sys.path.insert(0, str(HUB_ROOT))
15
-
16
- from shared.runner import ( # noqa: E402
17
- ModuleSpec,
18
- configure_stdout_encoding,
19
- ensure_output_dir as runner_ensure_output_dir,
20
- print_json,
21
- request_json,
22
- resolve_api_key,
23
- resolve_base_url,
24
- save_local_config,
25
- )
26
-
27
- SKILL_ROOT = Path(__file__).resolve().parents[1]
28
- SEARCH_RESULT_COUNT = 30
29
- MODULE_SPEC = ModuleSpec(
30
- module="douyin-topic-discovery",
31
- skill_root=SKILL_ROOT,
32
- )
33
-
34
-
35
- def ensure_output_dir() -> Path:
36
- return runner_ensure_output_dir(MODULE_SPEC)
37
-
38
-
39
- def timestamp_slug() -> str:
40
- return datetime.now().strftime("%Y%m%d_%H%M%S")
41
-
42
-
43
- def build_hot_bundle(api_response: dict[str, Any], *, board_type: str, count: int, city: str | None) -> dict[str, Any]:
44
- data = api_response.get("data", {})
45
- items = []
46
- for item in data.get("items", []):
47
- items.append(
48
- {
49
- "row_no": len(items) + 1,
50
- "rank": item.get("rank"),
51
- "topic": item.get("word", ""),
52
- "hot_value": item.get("hot_value", 0),
53
- "view_count": item.get("view_count", 0),
54
- "video_count": item.get("video_count", 0),
55
- "source_id": item.get("sentence_id", ""),
56
- }
57
- )
58
-
59
- return {
60
- "bundle_type": "douyin_hot_board",
61
- "saved_at": datetime.now().astimezone().isoformat(),
62
- "query": {
63
- "board_type": board_type,
64
- "board_name": data.get("board_name"),
65
- "city": city,
66
- "count": count,
67
- },
68
- "response_meta": {
69
- "message": api_response.get("message"),
70
- "trace_id": api_response.get("trace_id"),
71
- "total": data.get("total", len(items)),
72
- },
73
- "items": items,
74
- }
75
-
76
-
77
- def normalize_search_item(item: dict[str, Any], row_no: int) -> dict[str, Any]:
78
- play_url = item.get("play_url")
79
- download_url = item.get("download_url")
80
- return {
81
- "row_no": row_no,
82
- "keyword": item.get("keyword", ""),
83
- "item_type": item.get("type", 0),
84
- "video_id": item.get("aweme_id"),
85
- "title": item.get("desc", ""),
86
- "desc": item.get("desc", ""),
87
- "create_time": item.get("create_time"),
88
- "author_name": item.get("author_name", ""),
89
- "author_uid": item.get("author_uid"),
90
- "author_sec_uid": item.get("author_sec_uid"),
91
- "author_unique_id": item.get("author_unique_id"),
92
- "video_url": item.get("video_url", ""),
93
- "play_url": play_url,
94
- "download_url": download_url,
95
- "media_url": play_url,
96
- "media_is_ephemeral": bool(play_url),
97
- "duration": item.get("duration", 0),
98
- "image_count": item.get("image_count", 0),
99
- "aweme_type": item.get("aweme_type"),
100
- "media_type": item.get("media_type"),
101
- "stats": {
102
- "likes": item.get("likes", 0),
103
- "comments": item.get("comments", 0),
104
- "shares": item.get("shares", 0),
105
- "collects": item.get("collects", 0),
106
- "plays": item.get("plays", 0),
107
- },
108
- }
109
-
110
-
111
- def build_search_batch_bundle(api_response: dict[str, Any], *, request_payload: dict[str, Any], batch_id: str) -> dict[str, Any]:
112
- data = api_response.get("data", {})
113
- items = []
114
- for item in data.get("items", []):
115
- items.append(normalize_search_item(item, len(items) + 1))
116
-
117
- task_results = []
118
- for task in data.get("tasks", []):
119
- task_results.append(
120
- {
121
- "task_id": task.get("task_id"),
122
- "keyword": task.get("keyword", ""),
123
- "status": task.get("status", ""),
124
- "error": task.get("error"),
125
- "item_count": task.get("item_count", 0),
126
- }
127
- )
128
-
129
- return {
130
- "bundle_type": "douyin_video_search",
131
- "saved_at": datetime.now().astimezone().isoformat(),
132
- "query": request_payload,
133
- "response_meta": {
134
- "mode": "async_batch",
135
- "search_endpoint": "search_general_v3",
136
- "message": api_response.get("message"),
137
- "trace_id": api_response.get("trace_id"),
138
- "batch_id": batch_id,
139
- "status": data.get("status"),
140
- "total_tasks": data.get("total_tasks", 0),
141
- "completed_tasks": data.get("completed_tasks", 0),
142
- "success_tasks": data.get("success_tasks", 0),
143
- "failed_tasks": data.get("failed_tasks", 0),
144
- "total": data.get("total_items", len(items)),
145
- "failed_keywords": data.get("failed_keywords", []),
146
- "successful_keywords": data.get("successful_keywords", []),
147
- "task_results": task_results,
148
- },
149
- "items": items,
150
- }
151
-
152
-
153
- def write_json(path: Path, payload: dict[str, Any]) -> None:
154
- path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
155
-
156
-
157
- def write_hot_tsv(path: Path, bundle: dict[str, Any]) -> None:
158
- with path.open("w", encoding="utf-8-sig", newline="") as handle:
159
- writer = csv.DictWriter(
160
- handle,
161
- fieldnames=["row_no", "rank", "topic", "hot_value", "view_count", "video_count", "source_id"],
162
- delimiter="\t",
163
- )
164
- writer.writeheader()
165
- for item in bundle["items"]:
166
- writer.writerow(item)
167
-
168
-
169
- def write_search_tsv(path: Path, bundle: dict[str, Any]) -> None:
170
- with path.open("w", encoding="utf-8-sig", newline="") as handle:
171
- writer = csv.DictWriter(
172
- handle,
173
- fieldnames=[
174
- "row_no",
175
- "keyword",
176
- "video_id",
177
- "title",
178
- "author_name",
179
- "author_uid",
180
- "author_sec_uid",
181
- "author_unique_id",
182
- "likes",
183
- "comments",
184
- "shares",
185
- "collects",
186
- "plays",
187
- "duration",
188
- "image_count",
189
- "video_url",
190
- "play_url",
191
- "download_url",
192
- "media_url",
193
- "media_is_ephemeral",
194
- ],
195
- delimiter="\t",
196
- )
197
- writer.writeheader()
198
- for item in bundle["items"]:
199
- writer.writerow(
200
- {
201
- "row_no": item["row_no"],
202
- "keyword": item["keyword"],
203
- "video_id": item["video_id"],
204
- "title": item["title"],
205
- "author_name": item["author_name"],
206
- "author_uid": item["author_uid"],
207
- "author_sec_uid": item["author_sec_uid"],
208
- "author_unique_id": item["author_unique_id"],
209
- "likes": item["stats"]["likes"],
210
- "comments": item["stats"]["comments"],
211
- "shares": item["stats"]["shares"],
212
- "collects": item["stats"]["collects"],
213
- "plays": item["stats"]["plays"],
214
- "duration": item["duration"],
215
- "image_count": item["image_count"],
216
- "video_url": item["video_url"],
217
- "play_url": item["play_url"],
218
- "download_url": item["download_url"],
219
- "media_url": item["media_url"],
220
- "media_is_ephemeral": item["media_is_ephemeral"],
221
- }
222
- )
223
-
224
-
225
- def save_bundle(bundle: dict[str, Any], *, latest_name: str) -> dict[str, str]:
226
- output_dir = ensure_output_dir()
227
- stamp = timestamp_slug()
228
- json_latest = output_dir / f"{latest_name}.json"
229
- tsv_latest = output_dir / f"{latest_name}.tsv"
230
- json_snapshot = output_dir / f"{latest_name}_{stamp}.json"
231
- tsv_snapshot = output_dir / f"{latest_name}_{stamp}.tsv"
232
-
233
- write_json(json_latest, bundle)
234
- write_json(json_snapshot, bundle)
235
- if bundle["bundle_type"] == "douyin_hot_board":
236
- write_hot_tsv(tsv_latest, bundle)
237
- write_hot_tsv(tsv_snapshot, bundle)
238
- else:
239
- write_search_tsv(tsv_latest, bundle)
240
- write_search_tsv(tsv_snapshot, bundle)
241
-
242
- return {
243
- "json_latest": str(json_latest),
244
- "tsv_latest": str(tsv_latest),
245
- "json_snapshot": str(json_snapshot),
246
- "tsv_snapshot": str(tsv_snapshot),
247
- }
248
-
249
-
250
- def resolve_output_path(value: str) -> Path:
251
- output_dir = ensure_output_dir()
252
- aliases = {
253
- "latest_hot": output_dir / "latest_hot.json",
254
- "latest_search": output_dir / "latest_search.json",
255
- }
256
- if value in aliases:
257
- return aliases[value]
258
- candidate = Path(value)
259
- if candidate.exists():
260
- return candidate
261
- fallback = output_dir / value
262
- if fallback.exists():
263
- return fallback
264
- raise SystemExit(f"找不到结果文件: {value}")
265
-
266
-
267
- def load_bundle(path_value: str) -> dict[str, Any]:
268
- path = resolve_output_path(path_value)
269
- return json.loads(path.read_text(encoding="utf-8"))
270
-
271
-
272
- def build_parser() -> argparse.ArgumentParser:
273
- common = argparse.ArgumentParser(add_help=False)
274
- common.add_argument("--base-url", default=None)
275
- common.add_argument("--api-key", default=None)
276
-
277
- parser = argparse.ArgumentParser(description="Douyin topic discovery API client", parents=[common])
278
- subparsers = parser.add_subparsers(dest="command", required=True)
279
-
280
- subparsers.add_parser("configure", help="写入本地配置文件", parents=[common])
281
- subparsers.add_parser("whoami", help="查看当前 key 对应的调用方", parents=[common])
282
- subparsers.add_parser("capabilities", help="查看当前 key 已开通的能力", parents=[common])
283
-
284
- hot = subparsers.add_parser("hot", help="查询抖音热点榜并落 output 文件", parents=[common])
285
- hot.add_argument("--type", default="hot", choices=["hot", "city", "seeding", "entertainment", "society", "challenge"])
286
- hot.add_argument("--count", type=int, default=20)
287
- hot.add_argument("--city", default=None)
288
-
289
- search = subparsers.add_parser("search", help="按关键词异步提交抖音搜索流批次任务", parents=[common])
290
- search.add_argument("keywords", nargs="+")
291
- search.add_argument("--sort", type=int, default=0)
292
- search.add_argument("--time", type=int, default=0)
293
- search.add_argument("--content-type", type=int, default=0, choices=[0, 1, 2])
294
- search.add_argument("--filter-duration", default="")
295
- search.add_argument("--min-likes", type=int, default=0)
296
- search.add_argument("--min-comments", type=int, default=0)
297
-
298
- batch_status = subparsers.add_parser("batch-result", help="按 batch_id 查看异步搜索批次结果并落 output 文件", parents=[common])
299
- batch_status.add_argument("--batch-id", required=True)
300
-
301
- subparsers.add_parser("list-output", help="查看 output 目录下的结果文件")
302
-
303
- show_result = subparsers.add_parser("show-result", help="按行查看结果文件中的单条记录")
304
- show_result.add_argument("--input", default="latest_search")
305
- show_result.add_argument("--row", type=int, required=True)
306
-
307
- export_media = subparsers.add_parser("export-media", help="输出某一行的媒体引用,供后续脚本使用")
308
- export_media.add_argument("--input", default="latest_search")
309
- export_media.add_argument("--row", type=int, required=True)
310
- return parser
311
-
312
-
313
- def main() -> None:
314
- configure_stdout_encoding()
315
- parser = build_parser()
316
- args = parser.parse_args()
317
-
318
- if args.command == "configure":
319
- if not args.base_url or not args.api_key:
320
- raise SystemExit("configure 需要同时传 --base-url 和 --api-key")
321
- runner_save_local_config = save_local_config
322
- runner_save_local_config(MODULE_SPEC, base_url=args.base_url, api_key=args.api_key)
323
- print_json({"message": "配置已写入", "config_path": str(MODULE_SPEC.config_path)})
324
- return
325
-
326
- if args.command == "list-output":
327
- output_dir = ensure_output_dir()
328
- files = [
329
- {"name": path.name, "path": str(path), "size": path.stat().st_size}
330
- for path in sorted(output_dir.glob("*"))
331
- if path.is_file()
332
- ]
333
- print_json({"output_dir": str(output_dir), "files": files})
334
- return
335
-
336
- if args.command == "show-result":
337
- bundle = load_bundle(args.input)
338
- row = next((item for item in bundle.get("items", []) if item.get("row_no") == args.row), None)
339
- if not row:
340
- raise SystemExit(f"结果文件里不存在 row={args.row}")
341
- print_json(row)
342
- return
343
-
344
- if args.command == "export-media":
345
- bundle = load_bundle(args.input)
346
- row = next((item for item in bundle.get("items", []) if item.get("row_no") == args.row), None)
347
- if not row:
348
- raise SystemExit(f"结果文件里不存在 row={args.row}")
349
- payload = {
350
- "row_no": row.get("row_no"),
351
- "video_id": row.get("video_id"),
352
- "author_uid": row.get("author_uid"),
353
- "author_sec_uid": row.get("author_sec_uid"),
354
- "author_unique_id": row.get("author_unique_id"),
355
- "video_url": row.get("video_url"),
356
- "play_url": row.get("play_url"),
357
- "download_url": row.get("download_url"),
358
- "media_url": row.get("media_url"),
359
- "media_is_ephemeral": row.get("media_is_ephemeral"),
360
- }
361
- print_json(payload)
362
- return
363
-
364
- base_url = resolve_base_url(MODULE_SPEC, args.base_url)
365
- api_key = resolve_api_key(MODULE_SPEC, args.api_key)
366
-
367
- if args.command == "whoami":
368
- print_json(
369
- request_json(MODULE_SPEC, method="GET", base_url=base_url, api_key=api_key, path="/api/v1/auth/me")
370
- )
371
- return
372
-
373
- if args.command == "capabilities":
374
- print_json(
375
- request_json(
376
- MODULE_SPEC,
377
- method="GET",
378
- base_url=base_url,
379
- api_key=api_key,
380
- path="/api/v1/auth/capability-permissions",
381
- )
382
- )
383
- return
384
-
385
- if args.command == "hot":
386
- path = f"/api/v1/douyin/discovery/hot-board?type={args.type}&count={args.count}"
387
- if args.city:
388
- path += f"&city={args.city}"
389
- api_response = request_json(MODULE_SPEC, method="GET", base_url=base_url, api_key=api_key, path=path)
390
- bundle = build_hot_bundle(api_response, board_type=args.type, count=args.count, city=args.city)
391
- saved = save_bundle(bundle, latest_name="latest_hot")
392
- summary = [
393
- {
394
- "row_no": item["row_no"],
395
- "rank": item["rank"],
396
- "topic": item["topic"],
397
- "hot_value": item["hot_value"],
398
- }
399
- for item in bundle["items"][:10]
400
- ]
401
- print_json(
402
- {
403
- "status": "completed",
404
- "message": "已返回抖音热点榜结果",
405
- "artifacts": [saved["json_latest"], saved["tsv_latest"]],
406
- "data": {"saved": saved, "summary": summary, "total": bundle["response_meta"]["total"]},
407
- }
408
- )
409
- return
410
-
411
- if args.command == "batch-result":
412
- api_response = request_json(
413
- MODULE_SPEC,
414
- method="GET",
415
- base_url=base_url,
416
- api_key=api_key,
417
- path=f"/api/v1/douyin/discovery/search-videos/batch/{args.batch_id}/results",
418
- )
419
- bundle = build_search_batch_bundle(api_response, request_payload={"batch_id": args.batch_id}, batch_id=args.batch_id)
420
- saved = None
421
- status_value = str(bundle["response_meta"].get("status") or "").lower()
422
- artifacts: list[str] = []
423
- if status_value in {"completed", "partial_success", "failed"}:
424
- saved = save_bundle(bundle, latest_name="latest_search")
425
- artifacts = [saved["json_latest"], saved["tsv_latest"]]
426
- print_json(
427
- {
428
- "status": status_value or "running",
429
- "message": "已查询批次结果",
430
- "batch_id": args.batch_id,
431
- "artifacts": artifacts,
432
- "data": {
433
- "saved": saved,
434
- "status": bundle["response_meta"].get("status"),
435
- "total": bundle["response_meta"].get("total"),
436
- "success_tasks": bundle["response_meta"].get("success_tasks"),
437
- "failed_tasks": bundle["response_meta"].get("failed_tasks"),
438
- "failed_keywords": bundle["response_meta"].get("failed_keywords"),
439
- "summary": [
440
- {
441
- "row_no": item["row_no"],
442
- "keyword": item["keyword"],
443
- "title": item["title"],
444
- "author_name": item["author_name"],
445
- "author_sec_uid": item["author_sec_uid"],
446
- "likes": item["stats"]["likes"],
447
- "video_id": item["video_id"],
448
- }
449
- for item in bundle["items"][:10]
450
- ],
451
- },
452
- }
453
- )
454
- return
455
-
456
- if args.command == "search":
457
- payload = {
458
- "keywords": args.keywords,
459
- "count": SEARCH_RESULT_COUNT,
460
- "sort": args.sort,
461
- "time": args.time,
462
- "content_type": args.content_type,
463
- "filter_duration": args.filter_duration,
464
- "min_likes": args.min_likes,
465
- "min_comments": args.min_comments,
466
- }
467
- submit_response = request_json(
468
- MODULE_SPEC,
469
- method="POST",
470
- base_url=base_url,
471
- api_key=api_key,
472
- path="/api/v1/douyin/discovery/search-videos/batch",
473
- payload=payload,
474
- )
475
- submit_data = submit_response.get("data", {})
476
- batch_id = submit_data.get("batch_id")
477
- if not batch_id:
478
- raise SystemExit(json.dumps(submit_response, ensure_ascii=False, indent=2))
479
- print_json(
480
- {
481
- "status": "submitted",
482
- "message": "批次已提交。稍后用 batch-result --batch-id <batch_id> 查询并落 latest_search 文件。",
483
- "batch_id": batch_id,
484
- "artifacts": [],
485
- "data": {
486
- "submitted_keywords": args.keywords,
487
- "tasks": submit_data.get("tasks", []),
488
- "total_tasks": submit_data.get("total_tasks"),
489
- "result_count_per_keyword": SEARCH_RESULT_COUNT,
490
- },
491
- }
492
- )
493
- return
494
-
495
-
496
- if __name__ == "__main__":
497
- main()
@@ -1,93 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import argparse
4
- import json
5
- import sys
6
- from pathlib import Path
7
-
8
- REPO_ROOT = Path(__file__).resolve().parents[2]
9
- HUB_ROOT = REPO_ROOT / "9000AI-hub-9000AI"
10
- if str(HUB_ROOT) not in sys.path:
11
- sys.path.insert(0, str(HUB_ROOT))
12
-
13
- from shared.runner import ( # noqa: E402
14
- ModuleSpec,
15
- configure_stdout_encoding,
16
- load_json_file,
17
- print_json,
18
- request_json,
19
- resolve_api_key,
20
- resolve_base_url,
21
- save_local_config,
22
- )
23
-
24
- SKILL_ROOT = Path(__file__).resolve().parents[1]
25
- MODULE_SPEC = ModuleSpec(
26
- module="feedback",
27
- skill_root=SKILL_ROOT,
28
- )
29
-
30
-
31
- def build_parser() -> argparse.ArgumentParser:
32
- parser = argparse.ArgumentParser(description="9000AI 反馈提交")
33
- parser.add_argument("--base-url", default=None)
34
- parser.add_argument("--api-key", default=None)
35
- sub = parser.add_subparsers(dest="command", required=True)
36
-
37
- sub.add_parser("configure", help="写入本地配置")
38
- sub.add_parser("whoami", help="查看当前调用方")
39
-
40
- submit = sub.add_parser("submit", help="提交反馈")
41
- submit.add_argument("--title", help="反馈标题")
42
- submit.add_argument("--content", help="反馈详细内容")
43
- submit.add_argument("--type", default=None, help="可选分类: workflow / bug / feature / other")
44
- submit.add_argument("--context-json", default=None, help="附加上下文 JSON 字符串")
45
- submit.add_argument("--json-file", default=None, help="从 JSON 文件读取反馈内容")
46
-
47
- sub.add_parser("list", help="查看我的反馈列表")
48
- return parser
49
-
50
-
51
- def main() -> None:
52
- configure_stdout_encoding()
53
- parser = build_parser()
54
- args = parser.parse_args()
55
-
56
- if args.command == "configure":
57
- if not args.base_url or not args.api_key:
58
- raise SystemExit("configure 需要同时传 --base-url 和 --api-key")
59
- save_local_config(MODULE_SPEC, base_url=args.base_url, api_key=args.api_key)
60
- print_json({"message": "配置已写入", "config_path": str(MODULE_SPEC.config_path)})
61
- return
62
-
63
- base_url = resolve_base_url(MODULE_SPEC, args.base_url)
64
- api_key = resolve_api_key(MODULE_SPEC, args.api_key)
65
-
66
- if args.command == "whoami":
67
- print_json(request_json(MODULE_SPEC, method="GET", base_url=base_url, api_key=api_key, path="/api/v1/auth/me"))
68
- return
69
-
70
- if args.command == "submit":
71
- if args.json_file:
72
- payload = load_json_file(args.json_file)
73
- elif args.title and args.content:
74
- payload: dict = {"title": args.title, "content": args.content}
75
- if args.type:
76
- payload["type"] = args.type
77
- if args.context_json:
78
- payload["context"] = json.loads(args.context_json)
79
- else:
80
- raise SystemExit("submit 需要 --title + --content,或者 --json-file")
81
-
82
- print_json(
83
- request_json(MODULE_SPEC, method="POST", base_url=base_url, api_key=api_key, path="/api/v1/feedback", payload=payload)
84
- )
85
- return
86
-
87
- if args.command == "list":
88
- print_json(request_json(MODULE_SPEC, method="GET", base_url=base_url, api_key=api_key, path="/api/v1/feedback"))
89
- return
90
-
91
-
92
- if __name__ == "__main__":
93
- main()
@@ -1,3 +0,0 @@
1
- display_name: 批量视频转文字
2
- short_description: 提交视频转文字任务并回查结果
3
- default_prompt: 帮我提交批量视频转文字任务,拿到 task_id 后稍后回查结果。