@9000ai/cli 0.1.0
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/dist/client.d.ts +10 -0
- package/dist/client.js +45 -0
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/auth.js +18 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +30 -0
- package/dist/commands/feedback.d.ts +2 -0
- package/dist/commands/feedback.js +48 -0
- package/dist/commands/monitor.d.ts +2 -0
- package/dist/commands/monitor.js +101 -0
- package/dist/commands/search.d.ts +2 -0
- package/dist/commands/search.js +135 -0
- package/dist/commands/task.d.ts +2 -0
- package/dist/commands/task.js +20 -0
- package/dist/commands/transcribe.d.ts +2 -0
- package/dist/commands/transcribe.js +59 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.js +37 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +25 -0
- package/dist/output.d.ts +6 -0
- package/dist/output.js +49 -0
- package/dist/postinstall.d.ts +5 -0
- package/dist/postinstall.js +17 -0
- package/dist/utils/format.d.ts +1 -0
- package/dist/utils/format.js +16 -0
- package/package.json +31 -0
- package/skills/9000AI-hub/SKILL.md +195 -0
- package/skills/9000AI-hub/configure.py +56 -0
- package/skills/9000AI-hub/init/SKILL.md +130 -0
- package/skills/9000AI-hub/init/templates/CLAUDE.md +24 -0
- package/skills/9000AI-hub/init/templates/agents/README.md +7 -0
- package/skills/9000AI-hub/init/templates/agents/content-agent.md +181 -0
- package/skills/9000AI-hub/init/templates/claims/README.md +91 -0
- package/skills/9000AI-hub/init/templates/claims/claims.json +7 -0
- package/skills/9000AI-hub/init/templates/guide.md +185 -0
- package/skills/9000AI-hub/init/templates/inbox/README.md +26 -0
- package/skills/9000AI-hub/init/templates/profile/identity.md +8 -0
- package/skills/9000AI-hub/init/templates/profile/product.md +26 -0
- package/skills/9000AI-hub/init/templates/profile/topics.md +7 -0
- package/skills/9000AI-hub/init/templates/profile/voice.md +8 -0
- package/skills/9000AI-hub/init/templates/projects/README.md +5 -0
- package/skills/9000AI-hub/references/env.example +5 -0
- package/skills/9000AI-hub/references/runner-spec-v1.md +138 -0
- package/skills/9000AI-hub/shared/__init__.py +1 -0
- package/skills/9000AI-hub/shared/runner.py +135 -0
- package/skills/douyin-monitor/SKILL.md +112 -0
- package/skills/douyin-monitor/agents/openai.yaml +3 -0
- package/skills/douyin-monitor/references/endpoints.md +104 -0
- package/skills/douyin-monitor/scripts/douyin_monitor_api.py +273 -0
- package/skills/douyin-topic-discovery/SKILL.md +146 -0
- package/skills/douyin-topic-discovery/agents/openai.yaml +3 -0
- package/skills/douyin-topic-discovery/references/endpoints.md +127 -0
- package/skills/douyin-topic-discovery/scripts/douyin_topic_discovery_api.py +497 -0
- package/skills/douyin-topic-discovery/workflow/topic-research.md +216 -0
- package/skills/feedback/SKILL.md +69 -0
- package/skills/feedback/references/endpoints.md +46 -0
- package/skills/feedback/scripts/feedback_api.py +93 -0
- package/skills/video-transcription/SKILL.md +108 -0
- package/skills/video-transcription/agents/openai.yaml +3 -0
- package/skills/video-transcription/references/endpoints.md +82 -0
- package/skills/video-transcription/scripts/video_transcription_api.py +183 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# claims/ — 主张库
|
|
2
|
+
|
|
3
|
+
这是你的核心资产。所有内容创作都围绕主张展开。
|
|
4
|
+
|
|
5
|
+
## 文件结构
|
|
6
|
+
|
|
7
|
+
| 文件 | 用途 |
|
|
8
|
+
|------|------|
|
|
9
|
+
| claims.json | 所有主张的定义(ID、名称、描述) |
|
|
10
|
+
| angles.jsonl | 角度库——每个主张的具体切入角度,带匹配标签 |
|
|
11
|
+
| cases.jsonl | 案例/素材库——事件、观点、故事,关联到主张 |
|
|
12
|
+
| outputs.jsonl | 已产出记录——防止重复选题 |
|
|
13
|
+
|
|
14
|
+
## 数据规范
|
|
15
|
+
|
|
16
|
+
### claims.json
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
[
|
|
20
|
+
{
|
|
21
|
+
"id": "cl001",
|
|
22
|
+
"name": "主张名称",
|
|
23
|
+
"description": "用一段话说清楚这个主张的核心论点"
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
| 字段 | 类型 | 必填 | 说明 |
|
|
29
|
+
|------|------|------|------|
|
|
30
|
+
| id | string | 是 | 格式 cl{NNN} |
|
|
31
|
+
| name | string | 是 | 简短名称 |
|
|
32
|
+
| description | string | 是 | 完整描述 |
|
|
33
|
+
|
|
34
|
+
### angles.jsonl(每行一条 JSON)
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{"id":"ang001","claim_id":"cl001","angle":"角度描述","tags":["标签1","标签2"],"used_count":0,"last_used":null}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
| 字段 | 类型 | 必填 | 说明 |
|
|
41
|
+
|------|------|------|------|
|
|
42
|
+
| id | string | 是 | 格式 ang{NNN} |
|
|
43
|
+
| claim_id | string | 是 | 关联的主张 ID |
|
|
44
|
+
| angle | string | 是 | 角度描述(一句话) |
|
|
45
|
+
| tags | string[] | 是 | 匹配标签,用于关键词检索 |
|
|
46
|
+
| used_count | int | 是 | 使用次数,初始 0 |
|
|
47
|
+
| last_used | string\|null | 是 | 最后使用日期,初始 null |
|
|
48
|
+
|
|
49
|
+
### cases.jsonl(每行一条 JSON)
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{"id":"c001","claim_id":"cl001","angle_id":null,"type":"event","title":"标题","summary":"摘要","source":"来源","direction":"negative","date":"2026-03-28"}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
| 字段 | 类型 | 必填 | 说明 |
|
|
56
|
+
|------|------|------|------|
|
|
57
|
+
| id | string | 是 | 格式 c{NNN} |
|
|
58
|
+
| claim_id | string | 是 | 关联的主张 ID |
|
|
59
|
+
| angle_id | string\|null | 否 | 关联角度 ID,初期可为 null |
|
|
60
|
+
| type | string | 是 | event / opinion / story |
|
|
61
|
+
| title | string | 是 | 标题 |
|
|
62
|
+
| summary | string | 是 | 摘要 |
|
|
63
|
+
| source | string | 否 | 来源(平台@账号) |
|
|
64
|
+
| direction | string | 是 | positive(证实)/ negative(证伪) |
|
|
65
|
+
| date | string | 否 | 日期 |
|
|
66
|
+
|
|
67
|
+
### outputs.jsonl(每行一条 JSON)
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{"id":"o001","claim_id":"cl001","angle_id":"ang001","platform":"抖音","title":"标题","date":"2026-03-25"}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
| 字段 | 类型 | 必填 | 说明 |
|
|
74
|
+
|------|------|------|------|
|
|
75
|
+
| id | string | 是 | 格式 o{NNN} |
|
|
76
|
+
| claim_id | string | 是 | 关联主张 |
|
|
77
|
+
| angle_id | string\|null | 否 | 用了哪个角度 |
|
|
78
|
+
| platform | string | 是 | 发布平台 |
|
|
79
|
+
| title | string | 是 | 内容标题 |
|
|
80
|
+
| date | string | 是 | 发布日期 |
|
|
81
|
+
|
|
82
|
+
## 匹配流程
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
热点/案例进来
|
|
86
|
+
→ AI 提取关键词
|
|
87
|
+
→ 遍历 angles.jsonl,按 tags 匹配
|
|
88
|
+
→ 命中 → 推荐该角度 + 历史案例
|
|
89
|
+
→ 没命中 → 基于 claims.json 现推新角度
|
|
90
|
+
→ 录完 → 新角度追加到 angles.jsonl,案例追加到 cases.jsonl
|
|
91
|
+
```
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# 9000AI 内容创作工作空间 — 使用指南
|
|
2
|
+
|
|
3
|
+
## 你现在拥有什么
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
./
|
|
7
|
+
├── agents/content-agent.md # AI 助手的角色定义
|
|
8
|
+
├── profile/ # 你的画像(身份、风格、领域、产品)
|
|
9
|
+
├── inbox/ # 素材投喂入口
|
|
10
|
+
├── claims/ # 主张库(你的核心资产)
|
|
11
|
+
├── projects/ # 选题项目
|
|
12
|
+
├── CLAUDE.md # Claude Code 自动载入文件
|
|
13
|
+
└── guide.md # 就是这份指南
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 全流程概览
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
发现热点 → 匹配主张 → 找角度 → 补素材 → 转写视频 → 产出内容
|
|
22
|
+
↑ ↑ ↑ ↑ ↑
|
|
23
|
+
热榜/搜索 claims.json angles cases 视频转文字
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
1. **发现**:用热榜或搜索找到值得追的热点
|
|
27
|
+
2. **匹配**:看这个热点能挂到你的哪个主张上
|
|
28
|
+
3. **切角度**:从角度库找已有角度,或基于主张推新角度
|
|
29
|
+
4. **补素材**:从案例库拉历史素材,或搜索新素材
|
|
30
|
+
5. **转写**:对标视频转成文字稿,分析学习
|
|
31
|
+
6. **产出**:录制/撰写内容,记录到 outputs.jsonl
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 第一步:填写你的画像
|
|
36
|
+
|
|
37
|
+
打开 `profile/` 下的文件,把"待填写"替换成真实信息:
|
|
38
|
+
|
|
39
|
+
**identity.md — 你是谁**
|
|
40
|
+
- 名字 / IP 名称、业务类型、目标受众、商业模式
|
|
41
|
+
|
|
42
|
+
**voice.md — 你的表达风格**
|
|
43
|
+
- 语气、常用句式、禁忌用语、参考博主
|
|
44
|
+
|
|
45
|
+
**topics.md — 你关注什么**
|
|
46
|
+
- 主攻方向、细分赛道、不碰的领域
|
|
47
|
+
|
|
48
|
+
**product.md — 你卖什么**
|
|
49
|
+
- 产品是什么、交付形式、定价、核心卖点、业务边界
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 第二步:建立你的主张
|
|
54
|
+
|
|
55
|
+
打开 `claims/claims.json`,把占位内容替换成你的真实主张。
|
|
56
|
+
|
|
57
|
+
主张是你内容的灵魂。每条内容都服务于至少一个主张。角度和案例会在日常使用中逐渐积累。
|
|
58
|
+
|
|
59
|
+
**示例**:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
[
|
|
63
|
+
{
|
|
64
|
+
"id": "cl001",
|
|
65
|
+
"name": "知识不付费",
|
|
66
|
+
"description": "知识付费本质是贩卖焦虑。真正有价值的知识应该免费获取,付费的应该是服务和陪伴,不是知识本身。"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"id": "cl002",
|
|
70
|
+
"name": "工具不神话",
|
|
71
|
+
"description": "任何工具(包括 AI)都不值得神话。工具是手段不是目的,关键是用工具的人有没有把事情搞清楚。"
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 各能力快速入门
|
|
79
|
+
|
|
80
|
+
### 热榜 — 看当前什么话题热
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# 拉社会类热榜 top 20
|
|
84
|
+
9000ai search hot --type society --count 20
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
常用榜单类型:`hot`(综合)、`society`(社会)、`entertainment`(娱乐)、`tech`(科技)
|
|
88
|
+
|
|
89
|
+
### 搜索 — 按关键词找内容
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# 搜索关键词,最近 7 天,综合排序
|
|
93
|
+
9000ai search keyword "知识付费" --sort 0 --time 7
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
结果自动存到 `output/` 目录
|
|
97
|
+
|
|
98
|
+
### 对标监控 — 盯住你的对标账号
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# 查看已有监控目标
|
|
102
|
+
9000ai monitor list-creators
|
|
103
|
+
|
|
104
|
+
# 提交监控任务
|
|
105
|
+
9000ai monitor submit --json-file <配置文件>
|
|
106
|
+
|
|
107
|
+
# 查看执行历史
|
|
108
|
+
9000ai monitor list-runs
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 视频转文字
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# 提交转写任务
|
|
115
|
+
9000ai transcribe submit --json-file <视频列表>
|
|
116
|
+
|
|
117
|
+
# 查看任务状态
|
|
118
|
+
9000ai task status --task-id <id>
|
|
119
|
+
|
|
120
|
+
# 获取转写结果
|
|
121
|
+
9000ai transcribe text --task-id <id>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 查看任务状态
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
9000ai task status --task-id <id>
|
|
128
|
+
9000ai task results --task-id <id>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## 内容创作场景指南
|
|
134
|
+
|
|
135
|
+
### 场景 A:热点来了,快速出内容
|
|
136
|
+
|
|
137
|
+
1. `9000ai search hot` 拉热榜
|
|
138
|
+
2. 看到热点,问 agent:「这个热点能挂到我的哪个主张上?」
|
|
139
|
+
3. Agent 提取关键词 → 匹配 angles.jsonl 的 tags
|
|
140
|
+
4. 命中角度 → 推荐角度 + 拉历史案例
|
|
141
|
+
5. 没命中 → 基于主张推 2-3 个新角度,你选一个,写回 angles.jsonl
|
|
142
|
+
6. 录制完成 → 记录到 outputs.jsonl
|
|
143
|
+
|
|
144
|
+
### 场景 B:投喂素材,积累弹药
|
|
145
|
+
|
|
146
|
+
1. 把会议记录、文章、视频链接丢进 `inbox/`
|
|
147
|
+
2. 告诉 agent:「处理 inbox」
|
|
148
|
+
3. Agent 自动提取案例和观点 → 写入 `claims/cases.jsonl`
|
|
149
|
+
4. 原始文件归档到 `inbox/processed/`
|
|
150
|
+
|
|
151
|
+
### 场景 C:围绕主题做批量选题调研
|
|
152
|
+
|
|
153
|
+
1. `9000ai search keyword "关键词"` 搜索目标内容
|
|
154
|
+
2. 结果落到 output 文件
|
|
155
|
+
3. 挑出值得学的视频 → `9000ai transcribe submit` 转文字稿
|
|
156
|
+
4. 分析文字稿 → 提取案例写入 cases.jsonl
|
|
157
|
+
5. 基于积累的案例 → 规划一周选题
|
|
158
|
+
|
|
159
|
+
### 场景 D:对标监控 + 持续素材积累
|
|
160
|
+
|
|
161
|
+
1. `9000ai monitor create-creator` 添加对标账号
|
|
162
|
+
2. 定期监控,获取最新内容
|
|
163
|
+
3. 值得学的视频 → 转写 → 提取角度和案例
|
|
164
|
+
4. 找对标做了但你没做的角度 → 新选题
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## 常见问题
|
|
169
|
+
|
|
170
|
+
**怎么重新初始化?**
|
|
171
|
+
再跑一次 `/content-init`。已有文件不会被覆盖,只补全缺失的。
|
|
172
|
+
|
|
173
|
+
**API 连不上?**
|
|
174
|
+
1. 确认中台服务在运行
|
|
175
|
+
2. `9000ai config show` 检查地址
|
|
176
|
+
3. 地址有变动联系管理员,重新配置:`9000ai config set --base-url <新地址> --api-key <key>`
|
|
177
|
+
|
|
178
|
+
**怎么添加新主张?**
|
|
179
|
+
直接编辑 `claims/claims.json`,按格式追加,id 递增(cl002、cl003……)。
|
|
180
|
+
|
|
181
|
+
**怎么看我产出了什么?**
|
|
182
|
+
查看 `claims/outputs.jsonl`,每行是一条产出记录。
|
|
183
|
+
|
|
184
|
+
**inbox 里的文件处理完去哪了?**
|
|
185
|
+
归档到 `inbox/processed/`,不会被删除。
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# inbox/ — 暂存区
|
|
2
|
+
|
|
3
|
+
这是你的素材投喂入口。把任何素材丢进来,agent 会自动处理。
|
|
4
|
+
|
|
5
|
+
## 支持的格式
|
|
6
|
+
|
|
7
|
+
| 格式 | 典型用途 |
|
|
8
|
+
|------|---------|
|
|
9
|
+
| .txt | 会议记录、语音转写稿、随笔 |
|
|
10
|
+
| .md | 文案、文章、分析笔记 |
|
|
11
|
+
| .json | 结构化数据 |
|
|
12
|
+
| .url / 含链接的文本 | 视频链接(agent 会调用视频转文字) |
|
|
13
|
+
|
|
14
|
+
## 处理流程
|
|
15
|
+
|
|
16
|
+
1. 你丢文件进来
|
|
17
|
+
2. Agent 自动识别类型,提取主张相关的案例、观点、事件
|
|
18
|
+
3. 提取结果写入 claims/ 对应文件
|
|
19
|
+
4. 原始文件归档到 inbox/processed/
|
|
20
|
+
5. Agent 向你汇报处理结果
|
|
21
|
+
|
|
22
|
+
## 注意
|
|
23
|
+
|
|
24
|
+
- 一次可以丢多个文件,agent 会并行处理
|
|
25
|
+
- 不用担心格式,agent 会自己判断
|
|
26
|
+
- 处理后的原始文件不会被删除,会归档到 processed/ 子目录
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# 9000AI Runner Spec v1
|
|
2
|
+
|
|
3
|
+
## 目标
|
|
4
|
+
|
|
5
|
+
`runner spec v1` 用来统一 9000AI skill 的执行底座。
|
|
6
|
+
这一版只解决最重复的执行问题:
|
|
7
|
+
|
|
8
|
+
- 本地配置读取
|
|
9
|
+
- 环境变量解析
|
|
10
|
+
- `X-API-Key` 鉴权注入
|
|
11
|
+
- HTTP 请求发送
|
|
12
|
+
- 异步任务提交与查询
|
|
13
|
+
- 结果文件落盘
|
|
14
|
+
- 统一 JSON 输出
|
|
15
|
+
|
|
16
|
+
这一版不解决:
|
|
17
|
+
|
|
18
|
+
- 通用 DSL
|
|
19
|
+
- 动态插件发现
|
|
20
|
+
- MCP 协议抽象
|
|
21
|
+
- 跨模块编排
|
|
22
|
+
|
|
23
|
+
## 分层
|
|
24
|
+
|
|
25
|
+
- `SKILL.md`
|
|
26
|
+
- 给 agent 看的说明书
|
|
27
|
+
- 负责模块目录、使用规则、主命令
|
|
28
|
+
- `skill script`
|
|
29
|
+
- 负责命令行参数解析
|
|
30
|
+
- 负责把模块动作映射到 runner
|
|
31
|
+
- `9000AI-hub-9000AI/shared/runner.py`
|
|
32
|
+
- 负责统一执行
|
|
33
|
+
- `9000AIToolBox`
|
|
34
|
+
- 负责真正业务执行、任务状态、权限、结果
|
|
35
|
+
|
|
36
|
+
## ModuleSpec 最小字段
|
|
37
|
+
|
|
38
|
+
每个 skill 至少提供一份模块规范:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
ModuleSpec(
|
|
42
|
+
module="douyin-topic-discovery",
|
|
43
|
+
skill_root=Path(...),
|
|
44
|
+
)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
字段说明:
|
|
48
|
+
|
|
49
|
+
- `module`
|
|
50
|
+
- `skill_root`
|
|
51
|
+
- `default_base_url`
|
|
52
|
+
- `api_key_header`
|
|
53
|
+
- `timeout_seconds`
|
|
54
|
+
|
|
55
|
+
## 配置解析优先级
|
|
56
|
+
|
|
57
|
+
统一优先级:
|
|
58
|
+
|
|
59
|
+
1. 命令行参数
|
|
60
|
+
2. `9000AI_BASE_URL` / `9000AI_API_KEY`
|
|
61
|
+
3. `<skill>/local/config.json`
|
|
62
|
+
4. 默认值
|
|
63
|
+
|
|
64
|
+
本地配置格式统一为:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"base_url": "http://127.0.0.1:8025",
|
|
69
|
+
"api_key": "sk-xxx"
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## 统一执行接口
|
|
74
|
+
|
|
75
|
+
`9000AI-hub-9000AI/shared/runner.py` v1 提供:
|
|
76
|
+
|
|
77
|
+
- `configure_stdout_encoding()`
|
|
78
|
+
- `load_local_config(spec)`
|
|
79
|
+
- `save_local_config(spec, base_url, api_key)`
|
|
80
|
+
- `resolve_base_url(spec, override)`
|
|
81
|
+
- `resolve_api_key(spec, override)`
|
|
82
|
+
- `load_json_file(path)`
|
|
83
|
+
- `request_json(spec, method, base_url, api_key, path, payload=None)`
|
|
84
|
+
- `ensure_output_dir(spec)`
|
|
85
|
+
- `print_json(payload)`
|
|
86
|
+
|
|
87
|
+
## 统一输出协议
|
|
88
|
+
|
|
89
|
+
runner 输出统一遵守:
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"status": "submitted|running|completed|failed|partial_success",
|
|
94
|
+
"message": "",
|
|
95
|
+
"task_id": null,
|
|
96
|
+
"batch_id": null,
|
|
97
|
+
"artifacts": [],
|
|
98
|
+
"data": {}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## 异步任务规范
|
|
103
|
+
|
|
104
|
+
- `submit`
|
|
105
|
+
- 立即返回 `submitted`
|
|
106
|
+
- 返回 `task_id` 或 `batch_id`
|
|
107
|
+
- `result`
|
|
108
|
+
- 单独查询结果
|
|
109
|
+
- 不在 `submit` 内部长时间等待
|
|
110
|
+
|
|
111
|
+
## 结果落盘规范
|
|
112
|
+
|
|
113
|
+
每个 skill 的本地结果统一放在:
|
|
114
|
+
|
|
115
|
+
```text
|
|
116
|
+
<skill-name>/output/
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
推荐文件名:
|
|
120
|
+
|
|
121
|
+
- `latest_*.json`
|
|
122
|
+
- `latest_*.tsv`
|
|
123
|
+
|
|
124
|
+
必要时保留带时间戳快照:
|
|
125
|
+
|
|
126
|
+
- `latest_*_YYYYMMDD_HHMMSS.json`
|
|
127
|
+
- `latest_*_YYYYMMDD_HHMMSS.tsv`
|
|
128
|
+
|
|
129
|
+
## 迁移顺序
|
|
130
|
+
|
|
131
|
+
1. `douyin-topic-discovery-9000AI`
|
|
132
|
+
2. `douyin-monitor-9000AI`
|
|
133
|
+
3. `video-transcription`
|
|
134
|
+
|
|
135
|
+
原因:
|
|
136
|
+
|
|
137
|
+
- 选题发现同时覆盖同步请求、异步批次、结果落盘
|
|
138
|
+
- 最适合作为共享 runner 的样板模块
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Shared execution helpers shipped with 9000AI hub."""
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
DEFAULT_BASE_URL = "http://127.0.0.1:8025"
|
|
14
|
+
DEFAULT_TIMEOUT_SECONDS = 300
|
|
15
|
+
DEFAULT_API_KEY_HEADER = "X-API-Key"
|
|
16
|
+
HUB_BASE_URL_ENV = "9000AI_BASE_URL"
|
|
17
|
+
HUB_API_KEY_ENV = "9000AI_API_KEY"
|
|
18
|
+
|
|
19
|
+
# hub 自身的全局配置文件,位于 9000AI-hub-9000AI/local/config.json
|
|
20
|
+
_HUB_CONFIG_PATH = Path(__file__).resolve().parents[1] / "local" / "config.json"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class ModuleSpec:
|
|
25
|
+
module: str
|
|
26
|
+
skill_root: Path
|
|
27
|
+
default_base_url: str = DEFAULT_BASE_URL
|
|
28
|
+
api_key_header: str = DEFAULT_API_KEY_HEADER
|
|
29
|
+
timeout_seconds: int = DEFAULT_TIMEOUT_SECONDS
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def config_path(self) -> Path:
|
|
33
|
+
return self.skill_root / "local" / "config.json"
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def output_dir(self) -> Path:
|
|
37
|
+
return self.skill_root / "output"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def configure_stdout_encoding() -> None:
|
|
41
|
+
encoding = (sys.stdout.encoding or "").lower()
|
|
42
|
+
if encoding != "utf-8" and hasattr(sys.stdout, "reconfigure"):
|
|
43
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def load_local_config(spec: ModuleSpec) -> dict[str, Any]:
|
|
47
|
+
if not spec.config_path.exists():
|
|
48
|
+
return {}
|
|
49
|
+
return json.loads(spec.config_path.read_text(encoding="utf-8"))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def load_hub_config() -> dict[str, Any]:
|
|
53
|
+
if not _HUB_CONFIG_PATH.exists():
|
|
54
|
+
return {}
|
|
55
|
+
return json.loads(_HUB_CONFIG_PATH.read_text(encoding="utf-8"))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def save_local_config(spec: ModuleSpec, *, base_url: str, api_key: str) -> None:
|
|
59
|
+
spec.config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
spec.config_path.write_text(
|
|
61
|
+
json.dumps({"base_url": base_url.rstrip("/"), "api_key": api_key}, ensure_ascii=False, indent=2),
|
|
62
|
+
encoding="utf-8",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def save_hub_config(*, base_url: str, api_key: str) -> None:
|
|
67
|
+
"""保存 hub 全局配置,所有 skill 共享。"""
|
|
68
|
+
_HUB_CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
_HUB_CONFIG_PATH.write_text(
|
|
70
|
+
json.dumps({"base_url": base_url.rstrip("/"), "api_key": api_key}, ensure_ascii=False, indent=2),
|
|
71
|
+
encoding="utf-8",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def resolve_base_url(spec: ModuleSpec, override: str | None) -> str:
|
|
76
|
+
hub_config = load_hub_config()
|
|
77
|
+
skill_config = load_local_config(spec)
|
|
78
|
+
value = (
|
|
79
|
+
override
|
|
80
|
+
or os.getenv(HUB_BASE_URL_ENV)
|
|
81
|
+
or hub_config.get("base_url")
|
|
82
|
+
or skill_config.get("base_url")
|
|
83
|
+
or spec.default_base_url
|
|
84
|
+
)
|
|
85
|
+
return str(value).rstrip("/")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def resolve_api_key(spec: ModuleSpec, override: str | None) -> str:
|
|
89
|
+
hub_config = load_hub_config()
|
|
90
|
+
skill_config = load_local_config(spec)
|
|
91
|
+
value = override or os.getenv(HUB_API_KEY_ENV) or hub_config.get("api_key") or skill_config.get("api_key")
|
|
92
|
+
if not value:
|
|
93
|
+
raise SystemExit(
|
|
94
|
+
"还没连上 9000AI 中台。请先初始化:\n"
|
|
95
|
+
" 9000ai config set --base-url <中台地址> --api-key <你的key>\n"
|
|
96
|
+
"\n"
|
|
97
|
+
"不知道地址和 key?找中台管理员要。"
|
|
98
|
+
)
|
|
99
|
+
return str(value)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def load_json_file(path: str) -> Any:
|
|
103
|
+
return json.loads(Path(path).read_text(encoding="utf-8"))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def request_json(
|
|
107
|
+
spec: ModuleSpec,
|
|
108
|
+
*,
|
|
109
|
+
method: str,
|
|
110
|
+
base_url: str,
|
|
111
|
+
api_key: str,
|
|
112
|
+
path: str,
|
|
113
|
+
payload: Any | None = None,
|
|
114
|
+
timeout_seconds: int | None = None,
|
|
115
|
+
) -> Any:
|
|
116
|
+
response = requests.request(
|
|
117
|
+
method=method,
|
|
118
|
+
url=f"{base_url}{path}",
|
|
119
|
+
headers={spec.api_key_header: api_key, "Content-Type": "application/json"},
|
|
120
|
+
json=payload,
|
|
121
|
+
timeout=timeout_seconds or spec.timeout_seconds,
|
|
122
|
+
)
|
|
123
|
+
data = response.json()
|
|
124
|
+
if response.status_code >= 400:
|
|
125
|
+
raise SystemExit(json.dumps(data, ensure_ascii=False, indent=2))
|
|
126
|
+
return data
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def ensure_output_dir(spec: ModuleSpec) -> Path:
|
|
130
|
+
spec.output_dir.mkdir(parents=True, exist_ok=True)
|
|
131
|
+
return spec.output_dir
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def print_json(payload: Any) -> None:
|
|
135
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|