@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.
Files changed (62) hide show
  1. package/dist/client.d.ts +10 -0
  2. package/dist/client.js +45 -0
  3. package/dist/commands/auth.d.ts +2 -0
  4. package/dist/commands/auth.js +18 -0
  5. package/dist/commands/config.d.ts +2 -0
  6. package/dist/commands/config.js +30 -0
  7. package/dist/commands/feedback.d.ts +2 -0
  8. package/dist/commands/feedback.js +48 -0
  9. package/dist/commands/monitor.d.ts +2 -0
  10. package/dist/commands/monitor.js +101 -0
  11. package/dist/commands/search.d.ts +2 -0
  12. package/dist/commands/search.js +135 -0
  13. package/dist/commands/task.d.ts +2 -0
  14. package/dist/commands/task.js +20 -0
  15. package/dist/commands/transcribe.d.ts +2 -0
  16. package/dist/commands/transcribe.js +59 -0
  17. package/dist/config.d.ts +8 -0
  18. package/dist/config.js +37 -0
  19. package/dist/index.d.ts +2 -0
  20. package/dist/index.js +25 -0
  21. package/dist/output.d.ts +6 -0
  22. package/dist/output.js +49 -0
  23. package/dist/postinstall.d.ts +5 -0
  24. package/dist/postinstall.js +17 -0
  25. package/dist/utils/format.d.ts +1 -0
  26. package/dist/utils/format.js +16 -0
  27. package/package.json +31 -0
  28. package/skills/9000AI-hub/SKILL.md +195 -0
  29. package/skills/9000AI-hub/configure.py +56 -0
  30. package/skills/9000AI-hub/init/SKILL.md +130 -0
  31. package/skills/9000AI-hub/init/templates/CLAUDE.md +24 -0
  32. package/skills/9000AI-hub/init/templates/agents/README.md +7 -0
  33. package/skills/9000AI-hub/init/templates/agents/content-agent.md +181 -0
  34. package/skills/9000AI-hub/init/templates/claims/README.md +91 -0
  35. package/skills/9000AI-hub/init/templates/claims/claims.json +7 -0
  36. package/skills/9000AI-hub/init/templates/guide.md +185 -0
  37. package/skills/9000AI-hub/init/templates/inbox/README.md +26 -0
  38. package/skills/9000AI-hub/init/templates/profile/identity.md +8 -0
  39. package/skills/9000AI-hub/init/templates/profile/product.md +26 -0
  40. package/skills/9000AI-hub/init/templates/profile/topics.md +7 -0
  41. package/skills/9000AI-hub/init/templates/profile/voice.md +8 -0
  42. package/skills/9000AI-hub/init/templates/projects/README.md +5 -0
  43. package/skills/9000AI-hub/references/env.example +5 -0
  44. package/skills/9000AI-hub/references/runner-spec-v1.md +138 -0
  45. package/skills/9000AI-hub/shared/__init__.py +1 -0
  46. package/skills/9000AI-hub/shared/runner.py +135 -0
  47. package/skills/douyin-monitor/SKILL.md +112 -0
  48. package/skills/douyin-monitor/agents/openai.yaml +3 -0
  49. package/skills/douyin-monitor/references/endpoints.md +104 -0
  50. package/skills/douyin-monitor/scripts/douyin_monitor_api.py +273 -0
  51. package/skills/douyin-topic-discovery/SKILL.md +146 -0
  52. package/skills/douyin-topic-discovery/agents/openai.yaml +3 -0
  53. package/skills/douyin-topic-discovery/references/endpoints.md +127 -0
  54. package/skills/douyin-topic-discovery/scripts/douyin_topic_discovery_api.py +497 -0
  55. package/skills/douyin-topic-discovery/workflow/topic-research.md +216 -0
  56. package/skills/feedback/SKILL.md +69 -0
  57. package/skills/feedback/references/endpoints.md +46 -0
  58. package/skills/feedback/scripts/feedback_api.py +93 -0
  59. package/skills/video-transcription/SKILL.md +108 -0
  60. package/skills/video-transcription/agents/openai.yaml +3 -0
  61. package/skills/video-transcription/references/endpoints.md +82 -0
  62. 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,7 @@
1
+ [
2
+ {
3
+ "id": "cl001",
4
+ "name": "(待填写:你的第一个主张)",
5
+ "description": "(待填写:用一段话说清楚这个主张的核心论点)"
6
+ }
7
+ ]
@@ -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,8 @@
1
+ # 身份
2
+
3
+ (待填写)
4
+
5
+ - 你是谁
6
+ - 做什么业务
7
+ - 目标受众是谁
8
+ - 你的商业模式
@@ -0,0 +1,26 @@
1
+ # 产品与业务
2
+
3
+ (待填写)
4
+
5
+ ## 产品是什么
6
+
7
+ - 解决什么问题
8
+ - 目标用户是谁
9
+
10
+ ## 交付形式
11
+
12
+ - 社群 / 咨询 / 工具 / 课程 / 其他
13
+
14
+ ## 定价结构
15
+
16
+ - 主力产品及价格
17
+ - 引流产品(低价或免费)
18
+
19
+ ## 核心卖点
20
+
21
+ - 与竞品的差异化在哪
22
+ - 用户买单的核心理由
23
+
24
+ ## 业务边界
25
+
26
+ - 不做什么(明确拒绝的模式)
@@ -0,0 +1,7 @@
1
+ # 关注领域
2
+
3
+ (待填写)
4
+
5
+ - 主攻方向
6
+ - 细分赛道
7
+ - 不碰的领域
@@ -0,0 +1,8 @@
1
+ # 表达风格
2
+
3
+ (待填写)
4
+
5
+ - 语气:直接/温和/犀利
6
+ - 常用句式
7
+ - 禁忌用语
8
+ - 参考博主风格
@@ -0,0 +1,5 @@
1
+ # projects/ — 选题项目
2
+
3
+ 每个子目录是一个选题项目。
4
+
5
+ 项目由 agent 在内容工作流中创建,也可以手动建。一个选题对应一个项目目录,里面存放该选题的素材、草稿、产出记录。
@@ -0,0 +1,5 @@
1
+ # 9000AI hub common env
2
+ # 推荐用 CLI 初始化,环境变量作为备用覆盖手段:
3
+ # 9000ai config set --base-url http://192.168.x.x:8025 --api-key sk-xxx
4
+ 9000AI_BASE_URL=http://127.0.0.1:8025
5
+ 9000AI_API_KEY=sk-xxx
@@ -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))