yuanflow-cli 0.1.6 → 0.1.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 CHANGED
@@ -35,6 +35,9 @@ yuanflow-cli works detail --platform douyin --target "https://v.douyin.com/xxx/"
35
35
  yuanflow-cli works download --platform douyin --target "https://v.douyin.com/xxx/" --region CN --format agent-json
36
36
  yuanflow-cli search content --platform xiaohongshu --keyword "美妆" --format agent-json
37
37
  yuanflow-cli search users --platform instagram --keyword "nasa" --format agent-json
38
+ yuanflow-cli knowledge docs --format agent-json
39
+ yuanflow-cli knowledge entry --task-intent generate_script --content-goal "写一个创业者短视频选题" --format agent-json
40
+ yuanflow-cli oss signed-url --key path/to/file.png --ttl-seconds 1800 --format agent-json
38
41
  yuanflow-cli list douyin
39
42
  ```
40
43
 
@@ -87,6 +90,31 @@ yuanflow-cli search users --platform instagram --keyword "nasa" --format agent-j
87
90
 
88
91
  Agent 不确定参数时先执行 `commands list`,再用 `schema works.douyin.detail` 或 `schema search.xiaohongshu.content` 查看参数。`--dry-run` 可预览请求映射,不发起真实接口请求,也不要求 token。
89
92
 
93
+ ### 自媒体知识库
94
+
95
+ 知识库能力独立于社媒 `/social` 接口,走 Yuan API 的 `/api/knowledge-base/docs` 和 `/atomic/agent-rules/navigate`:
96
+
97
+ ```bash
98
+ yuanflow-cli knowledge docs --format agent-json
99
+ yuanflow-cli knowledge entry --task-intent generate_script --output-format short_video_script --domain 自媒体运营 --content-goal "写一个创业者短视频选题" --format agent-json
100
+ yuanflow-cli knowledge rules --pack-code topic_pain_point_pack --task-intent generate_script --content-goal "写一个创业者短视频选题" --format agent-json
101
+ yuanflow-cli knowledge rule-detail --rule-code some_rule_code --task-intent generate_script --content-goal "写一个创业者短视频选题" --format agent-json
102
+ ```
103
+
104
+ Agent 应先查看 `knowledge docs`,再把用户需求整理成 `task_frame`,按接口返回的 `next_actions` 渐进查询。
105
+
106
+ ### OSS 原子能力
107
+
108
+ OSS 能力独立封装为 `oss` 命令,用于临时上传、签名链接和对象复制:
109
+
110
+ ```bash
111
+ yuanflow-cli oss temp-upload --file ./cover.png --content-type image/png --format agent-json
112
+ yuanflow-cli oss signed-url --key path/to/file.png --ttl-seconds 1800 --method GET --format agent-json
113
+ yuanflow-cli oss copy --source-key temp/cover.png --target-key final/cover.png --format agent-json
114
+ ```
115
+
116
+ 上传本地文件前必须确认用户授权,不能上传密钥、cookie、账号凭据或隐私文件。
117
+
90
118
  ## Skill 安装器
91
119
 
92
120
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yuanflow-cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "YuanFlow API CLI and skill installer for supported AI coding agents.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,97 @@
1
+ ---
2
+ name: OSS文件中转工具
3
+ description: 当用户需要把本地文件上传到 YuanFlow 临时 OSS、生成对象签名访问链接、复制 OSS 对象,或为自媒体工作流准备图片、音频、视频、文档中转链接时使用。
4
+ ---
5
+
6
+ # OSS 文件中转工具
7
+
8
+ 这个 Skill 负责使用 YuanFlow 的 OSS 原子能力。它只处理文件中转、签名链接和对象复制,不负责社媒数据采集。
9
+
10
+ ## 环境判断
11
+
12
+ 1. 在 YuanFlow 主程序内,优先调用内置工具 `yuanflow_cli_call`。
13
+ 2. 在外部 Agent 中,如果本地有 `yuanflow-cli`,直接执行 CLI。
14
+ 3. 如果外部环境没有 CLI,再提示用户安装 `npm install -g yuanflow-cli`。
15
+
16
+ 不要要求用户手动填写 token。YuanFlow 主程序会注入认证 token;外部 CLI 使用 `YUANCHUANG_API_TOKEN`。
17
+
18
+ ## 能做什么
19
+
20
+ - 上传本地图片、音频、视频、文档到临时 OSS。
21
+ - 为已有 OSS 对象生成临时签名 URL。
22
+ - 把一个 OSS 对象复制到另一个 key。
23
+ - 为后续自媒体创作、发布、转写、封面处理等流程提供临时文件链接。
24
+
25
+ ## 命令
26
+
27
+ ### 临时上传
28
+
29
+ ```bash
30
+ yuanflow-cli oss temp-upload \
31
+ --file ./cover.png \
32
+ --content-type image/png \
33
+ --format agent-json
34
+ ```
35
+
36
+ 可选参数:
37
+
38
+ - `--filename`:指定上传文件名。
39
+ - `--key`:指定 OSS 对象 key。
40
+ - `--content-type`:MIME 类型,默认 `application/octet-stream`。
41
+
42
+ ### 生成签名链接
43
+
44
+ ```bash
45
+ yuanflow-cli oss signed-url \
46
+ --key path/to/file.png \
47
+ --ttl-seconds 1800 \
48
+ --method GET \
49
+ --format agent-json
50
+ ```
51
+
52
+ 可选参数:
53
+
54
+ - `--bucket`:指定 bucket。
55
+ - `--ttl-seconds`:有效期,最大值由后端限制。
56
+ - `--method`:`GET`、`PUT`、`HEAD`。
57
+
58
+ ### 复制对象
59
+
60
+ ```bash
61
+ yuanflow-cli oss copy \
62
+ --source-key temp/cover.png \
63
+ --target-key final/cover.png \
64
+ --format agent-json
65
+ ```
66
+
67
+ 可选参数:
68
+
69
+ - `--source-bucket`:源 bucket。
70
+ - `--target-bucket`:目标 bucket。
71
+
72
+ ## YuanFlow 内置工具调用示例
73
+
74
+ 在 YuanFlow 主程序内,调用 `yuanflow_cli_call`,参数数组不要包含 token:
75
+
76
+ ```json
77
+ {
78
+ "args": [
79
+ "oss",
80
+ "signed-url",
81
+ "--key",
82
+ "path/to/file.png",
83
+ "--ttl-seconds",
84
+ "1800",
85
+ "--format",
86
+ "agent-json"
87
+ ]
88
+ }
89
+ ```
90
+
91
+ ## 安全边界
92
+
93
+ - 上传本地文件前,先确认用户明确授权该文件路径。
94
+ - 不上传密钥、cookie、账号凭据、配置文件、隐私文件。
95
+ - 不把签名 URL 写入公开文档或长期日志。
96
+ - 返回结果中优先说明 `bucket`、`key`、`expires_at`,只有用户确实需要访问链接时才展示 `signed_url`。
97
+ - 在 YuanFlow 主程序中,如果工具策略限制本地文件上传,不要绕过限制,改为让用户通过受控上传入口完成。
@@ -18,6 +18,8 @@ description: Use when the user asks about social-media API workflows, platform d
18
18
  - `作品下载综合工具/`
19
19
  - `综合搜索工具/`
20
20
  - `综合用户搜索工具/`
21
+ - `自媒体知识库/`
22
+ - `OSS文件中转工具/`
21
23
 
22
24
  ## 环境判断
23
25
 
@@ -113,6 +115,30 @@ description: Use when the user asks about social-media API workflows, platform d
113
115
 
114
116
  - `综合用户搜索工具`
115
117
 
118
+ ### 7. 走 `自媒体知识库`
119
+
120
+ 遇到下面这些需求,优先进入这个子 Skill:
121
+
122
+ - 自媒体选题、开头钩子、脚本、文案、标题、改写、评分、剪辑建议、发布策略。
123
+ - 用户明确要求查询 YuanFlow 自媒体知识库、方法包、规则方向。
124
+ - 需要按照知识库 `next_actions` 渐进式查询方法论摘要。
125
+
126
+ 子 Skill 名称:
127
+
128
+ - `自媒体知识库`
129
+
130
+ ### 8. 走 `OSS文件中转工具`
131
+
132
+ 遇到下面这些需求,优先进入这个子 Skill:
133
+
134
+ - 上传图片、音频、视频、文档到临时 OSS。
135
+ - 为已有 OSS 对象生成签名访问链接。
136
+ - 复制 OSS 对象,或为其它自媒体流程准备文件中转链接。
137
+
138
+ 子 Skill 名称:
139
+
140
+ - `OSS文件中转工具`
141
+
116
142
  ## 多需求时怎么处理
117
143
 
118
144
  如果用户一次提了多段流程,不要强行塞进一个子 Skill,按阶段拆开:
@@ -0,0 +1,103 @@
1
+ ---
2
+ name: 自媒体知识库
3
+ description: 当用户需要做自媒体选题、开头钩子、脚本、文案、标题、改写、评分、剪辑建议、发布策略,或明确要求查询 YuanFlow 自媒体知识库时使用。
4
+ ---
5
+
6
+ # 自媒体知识库
7
+
8
+ 这个 Skill 负责使用 YuanFlow 的自媒体知识库原子能力。它和社媒数据采集、作品评论、作品下载是独立能力,不要混用接口。
9
+
10
+ ## 环境判断
11
+
12
+ 1. 在 YuanFlow 主程序内,优先调用内置工具 `yuanflow_cli_call`。
13
+ 2. 在外部 Agent 中,如果本地有 `yuanflow-cli`,直接执行 CLI。
14
+ 3. 如果外部环境没有 CLI,再提示用户安装 `npm install -g yuanflow-cli`。
15
+
16
+ 不要让用户手动提供 token。YuanFlow 主程序会注入认证 token;外部 CLI 使用 `YUANCHUANG_API_TOKEN`。
17
+
18
+ ## 能做什么
19
+
20
+ 适合以下任务:
21
+
22
+ - 选题生成、选题分析。
23
+ - 开头钩子、短视频脚本、口播脚本。
24
+ - 自媒体文案、标题、内容改写。
25
+ - 内容评分、复盘、优化建议。
26
+ - 剪辑建议、发布策略、平台适配。
27
+
28
+ 当前公开方向包括:
29
+
30
+ - `topic_generation`:选题生成。
31
+ - `hook_generation`:开头钩子。
32
+ - `script_generation`:脚本生成。
33
+ - `topic_analysis`:选题分析。
34
+ - `content_evaluation`:内容评分。
35
+ - `copywriting`:文案创作。
36
+ - `title_generation`:标题生成。
37
+ - `rewrite`:内容改写。
38
+ - `editing_strategy`:剪辑建议。
39
+ - `publishing_strategy`:发布策略。
40
+
41
+ ## 推荐流程
42
+
43
+ 1. 先查看知识库公开目录:
44
+
45
+ ```bash
46
+ yuanflow-cli knowledge docs --format agent-json
47
+ ```
48
+
49
+ 2. 把用户需求整理为 `task_frame`,再进入知识库:
50
+
51
+ ```bash
52
+ yuanflow-cli knowledge entry \
53
+ --task-intent generate_script \
54
+ --output-format short_video_script \
55
+ --domain 自媒体运营 \
56
+ --content-goal "写一个面向创业者的短视频选题" \
57
+ --tone 清晰直接 \
58
+ --communication-mode 口播 \
59
+ --format agent-json
60
+ ```
61
+
62
+ 3. 读取返回结果里的 `next_actions`,继续调用 `packs`、`rules` 或 `rule-detail`。
63
+
64
+ ```bash
65
+ yuanflow-cli knowledge rules \
66
+ --pack-code topic_pain_point_pack \
67
+ --task-intent generate_script \
68
+ --content-goal "写一个面向创业者的短视频选题" \
69
+ --format agent-json
70
+ ```
71
+
72
+ 4. 最后基于接口返回的公开规则摘要,完成创作、评估或优化。
73
+
74
+ ## YuanFlow 内置工具调用示例
75
+
76
+ 在 YuanFlow 主程序内,调用 `yuanflow_cli_call`,参数数组不要包含 token:
77
+
78
+ ```json
79
+ {
80
+ "args": [
81
+ "knowledge",
82
+ "entry",
83
+ "--task-intent",
84
+ "generate_script",
85
+ "--output-format",
86
+ "short_video_script",
87
+ "--domain",
88
+ "自媒体运营",
89
+ "--content-goal",
90
+ "写一个面向创业者的短视频选题",
91
+ "--format",
92
+ "agent-json"
93
+ ]
94
+ }
95
+ ```
96
+
97
+ ## 注意事项
98
+
99
+ - 不要让 Agent 自己拼 SQL。
100
+ - 不要使用 `/atomic/aliyun-db/query` 查询知识库内部表。
101
+ - 不要把知识库核心方法论静态写入回复,只按接口返回内容使用。
102
+ - 如果接口返回 `next_actions`,优先按它推荐的动作继续查询。
103
+ - 如果用户只是在问“有什么知识库方向”,先用 `knowledge docs`。
@@ -1,6 +1,8 @@
1
1
  import { listEndpoints } from './registry.js';
2
2
  import { listShortcuts } from './shortcuts.js';
3
3
  import { listCommentCommands } from './comment-collector.js';
4
+ import { listKnowledgeCommands } from './knowledge-tools.js';
5
+ import { listOssCommands } from './oss-tools.js';
4
6
  import { listSearchCommands, listWorkCommands } from './work-tools.js';
5
7
 
6
8
  const ERROR_MAP = [
@@ -86,9 +88,17 @@ export function buildCommandRegistry() {
86
88
  const commentCommands = listCommentCommands();
87
89
  const workCommands = listWorkCommands();
88
90
  const searchCommands = listSearchCommands();
89
- return [...shortcuts, ...endpoints, ...commentCommands, ...workCommands, ...searchCommands].sort((left, right) =>
90
- left.key.localeCompare(right.key),
91
- );
91
+ const knowledgeCommands = listKnowledgeCommands();
92
+ const ossCommands = listOssCommands();
93
+ return [
94
+ ...shortcuts,
95
+ ...endpoints,
96
+ ...commentCommands,
97
+ ...workCommands,
98
+ ...searchCommands,
99
+ ...knowledgeCommands,
100
+ ...ossCommands,
101
+ ].sort((left, right) => left.key.localeCompare(right.key));
92
102
  }
93
103
 
94
104
  export function findCommandByKey(key) {
@@ -112,7 +122,8 @@ export function commandToSchema(command) {
112
122
  api: {
113
123
  method: command.method,
114
124
  socialPath: command.socialPath,
115
- url: `/social${command.socialPath}`,
125
+ apiPath: command.apiPath,
126
+ url: command.apiPath || `/social${command.socialPath}`,
116
127
  queryParams: command.queryParams || [],
117
128
  requestBody: command.requestBody || null,
118
129
  },
@@ -0,0 +1,106 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { readConfig } from './config.js';
3
+
4
+ export async function callAtomic(path, options = {}) {
5
+ const config = await readConfig();
6
+ const baseUrl = cleanBaseUrl(options.baseUrl || config.baseUrl);
7
+ const token = options.token || process.env.YUANCHUANG_API_TOKEN || config.token || '';
8
+ const method = String(options.method || 'POST').toUpperCase();
9
+ const normalizedPath = normalizePath(path);
10
+ const url = new URL(normalizedPath, baseUrl);
11
+ const body = await resolveBody(options);
12
+
13
+ if (method === 'GET' && body && typeof body === 'object') {
14
+ for (const [key, value] of Object.entries(body)) {
15
+ if (value !== undefined && value !== null) {
16
+ url.searchParams.set(key, String(value));
17
+ }
18
+ }
19
+ }
20
+
21
+ if (options.dryRun) {
22
+ return {
23
+ dryRun: true,
24
+ method,
25
+ url: url.toString(),
26
+ headers: {
27
+ ...(token ? { Authorization: `Bearer ${maskToken(token)}` } : {}),
28
+ },
29
+ body: method === 'GET' ? undefined : body || {},
30
+ };
31
+ }
32
+
33
+ if (options.requiresToken !== false && !token) {
34
+ throw new Error('缺少 token。请设置 YUANCHUANG_API_TOKEN,或执行 yuanflow-cli config set-token <你的令牌>');
35
+ }
36
+
37
+ const response = await fetch(url, {
38
+ method,
39
+ headers: {
40
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
41
+ Accept: 'application/json',
42
+ ...(method === 'GET' ? {} : { 'Content-Type': 'application/json' }),
43
+ },
44
+ body: method === 'GET' ? undefined : JSON.stringify(body || {}),
45
+ });
46
+
47
+ const text = await response.text();
48
+ const payload = parseMaybeJson(text);
49
+ if (!response.ok) {
50
+ const message = typeof payload === 'object' ? JSON.stringify(payload) : text;
51
+ throw new Error(`请求失败:HTTP ${response.status} ${message}`);
52
+ }
53
+ return payload;
54
+ }
55
+
56
+ export async function readJsonFile(filePath) {
57
+ const raw = await readFile(filePath, 'utf8');
58
+ const payload = JSON.parse(raw);
59
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
60
+ throw new Error('JSON 文件内容必须是对象。');
61
+ }
62
+ return payload;
63
+ }
64
+
65
+ export function cleanBaseUrl(value) {
66
+ return (value || 'https://open.yuanchuangai.com').replace(/\/+$/, '');
67
+ }
68
+
69
+ function normalizePath(path) {
70
+ const normalized = String(path || '').trim();
71
+ if (!normalized.startsWith('/')) {
72
+ return `/${normalized}`;
73
+ }
74
+ return normalized;
75
+ }
76
+
77
+ async function resolveBody(options) {
78
+ if (options.json) {
79
+ return JSON.parse(options.json);
80
+ }
81
+ if (options.body) {
82
+ return options.body;
83
+ }
84
+ return {};
85
+ }
86
+
87
+ function parseMaybeJson(text) {
88
+ if (!text) {
89
+ return null;
90
+ }
91
+ try {
92
+ return JSON.parse(text);
93
+ } catch {
94
+ return text;
95
+ }
96
+ }
97
+
98
+ function maskToken(token) {
99
+ if (!token) {
100
+ return '';
101
+ }
102
+ if (token.length <= 10) {
103
+ return '***';
104
+ }
105
+ return `${token.slice(0, 6)}...${token.slice(-4)}`;
106
+ }
package/src/cli.js CHANGED
@@ -17,6 +17,8 @@ import {
17
17
  } from './agent-protocol.js';
18
18
  import { findShortcut, listShortcuts } from './shortcuts.js';
19
19
  import { collectComments } from './comment-collector.js';
20
+ import { getKnowledgeDocs, navigateKnowledge } from './knowledge-tools.js';
21
+ import { copyObject, signedUrl, tempUpload } from './oss-tools.js';
20
22
  import {
21
23
  getWorkDetail,
22
24
  getWorkDownload,
@@ -68,6 +70,16 @@ export async function main(argv) {
68
70
  return;
69
71
  }
70
72
 
73
+ if (command === 'knowledge') {
74
+ await handleKnowledge(rest);
75
+ return;
76
+ }
77
+
78
+ if (command === 'oss') {
79
+ await handleOss(rest);
80
+ return;
81
+ }
82
+
71
83
  if (command === 'schema') {
72
84
  await handleSchema(rest);
73
85
  return;
@@ -93,6 +105,7 @@ async function handleCommands(args) {
93
105
  description: item.description,
94
106
  method: item.method,
95
107
  socialPath: item.socialPath,
108
+ apiPath: item.apiPath,
96
109
  }));
97
110
  await outputResult(
98
111
  createAgentSuccess('commands list', { commands }),
@@ -130,6 +143,7 @@ async function handleSchema(args) {
130
143
  kind: item.kind,
131
144
  method: item.method,
132
145
  socialPath: item.socialPath,
146
+ apiPath: item.apiPath,
133
147
  }));
134
148
  await outputResult(createAgentSuccess('schema list', { commands }), {
135
149
  ...options,
@@ -325,6 +339,47 @@ async function handleSearch(args) {
325
339
  });
326
340
  }
327
341
 
342
+ async function handleKnowledge(args) {
343
+ const { positionals, options } = parseOptions(args);
344
+ const [action = 'docs'] = positionals;
345
+ if (action === 'docs') {
346
+ const result = await getKnowledgeDocs({ options });
347
+ await outputResult(result, options, {
348
+ command: 'knowledge docs',
349
+ meta: { endpoint: result.endpoint.path, kind: 'knowledge-base' },
350
+ });
351
+ return;
352
+ }
353
+ if (['entry', 'packs', 'rules', 'rule-detail', 'evaluate'].includes(action)) {
354
+ const result = await navigateKnowledge({ action, options });
355
+ await outputResult(result, options, {
356
+ command: `knowledge ${action}`,
357
+ meta: { endpoint: result.endpoint.path, kind: 'knowledge-base' },
358
+ });
359
+ return;
360
+ }
361
+ throw new Error('未知 knowledge 命令。用法:yuanflow-cli knowledge docs|entry|packs|rules|rule-detail|evaluate');
362
+ }
363
+
364
+ async function handleOss(args) {
365
+ const { positionals, options } = parseOptions(args);
366
+ const [action] = positionals;
367
+ let result;
368
+ if (action === 'temp-upload') {
369
+ result = await tempUpload({ options });
370
+ } else if (action === 'signed-url') {
371
+ result = await signedUrl({ options });
372
+ } else if (action === 'copy') {
373
+ result = await copyObject({ options });
374
+ } else {
375
+ throw new Error('未知 oss 命令。用法:yuanflow-cli oss temp-upload|signed-url|copy');
376
+ }
377
+ await outputResult(result, options, {
378
+ command: `oss ${action}`,
379
+ meta: { endpoint: result.endpoint.path, kind: 'oss-atomic' },
380
+ });
381
+ }
382
+
328
383
  async function handleGeneratedCommand(platform, args) {
329
384
  if (!getPlatforms().includes(platform)) {
330
385
  throw new Error(`未知平台:${platform}。可用平台:${getPlatforms().join(', ')}`);
@@ -483,10 +538,14 @@ function printHelp() {
483
538
  yuanflow-cli works download --platform youtube --target "dQw4w9WgXcQ" --dry-run
484
539
  yuanflow-cli search content --platform xiaohongshu --keyword "美妆" --dry-run
485
540
  yuanflow-cli search users --platform instagram --keyword "nasa" --dry-run
541
+ yuanflow-cli knowledge docs --dry-run
542
+ yuanflow-cli knowledge entry --task-intent generate_script --content-goal "写一个创业者短视频选题" --format agent-json
543
+ yuanflow-cli oss signed-url --key path/to/file.png --ttl-seconds 1800 --format agent-json
486
544
  yuanflow-cli list douyin
487
545
 
488
546
  说明:
489
- 所有请求都会调用 Yuan API 的 /social/*path,并使用 Authorization: Bearer <token>。
547
+ 社媒请求调用 Yuan API 的 /social/*path;知识库和 OSS 原子能力调用 /api/* 或 /atomic/*。
548
+ 需要鉴权的请求都会使用 Authorization: Bearer <token>。
490
549
  token 优先级:--token > YUANCHUANG_API_TOKEN > 本地 config.token。
491
550
  YuanFlow 主程序内使用时,token 由主程序认证系统注入,不需要手动配置。
492
551
  AI Agent 推荐使用 --format agent-json 获取稳定 JSON 外壳。
@@ -0,0 +1,175 @@
1
+ import { callAtomic, readJsonFile } from './atomic-request.js';
2
+
3
+ const KNOWLEDGE_NAVIGATE_PATH = '/atomic/agent-rules/navigate';
4
+ const KNOWLEDGE_DOCS_PATH = '/api/knowledge-base/docs';
5
+
6
+ export function listKnowledgeCommands() {
7
+ return [
8
+ {
9
+ key: 'knowledge.docs',
10
+ command: 'knowledge docs',
11
+ kind: 'knowledge-base',
12
+ description: '读取自媒体知识库公开目录、一级/二级方向和流程说明。',
13
+ method: 'GET',
14
+ apiPath: KNOWLEDGE_DOCS_PATH,
15
+ positionals: [],
16
+ options: commonOptions({ tokenRequired: false }),
17
+ requestBody: null,
18
+ returns: '返回知识库公开目录、能力方向和可用说明。',
19
+ },
20
+ knowledgeCommand('entry', '进入知识库,根据用户任务获取领域上下文、推荐能力和方法包。'),
21
+ knowledgeCommand('packs', '查看某类能力下的方法包。', [
22
+ { flag: '--capability-code', name: 'capabilityCode', required: false, label: '一级能力 code,例如 topic_generation。' },
23
+ ]),
24
+ knowledgeCommand('rules', '查看方法包下的规则方向。', [
25
+ { flag: '--pack-code', name: 'packCode', required: true, label: '方法包 code。' },
26
+ ]),
27
+ knowledgeCommand('rule-detail', '查看单条规则的公开摘要。', [
28
+ { flag: '--rule-code', name: 'ruleCode', required: true, label: '规则 code。' },
29
+ ]),
30
+ knowledgeCommand('evaluate', '获取内容评分维度和优化方向。'),
31
+ ];
32
+ }
33
+
34
+ export async function getKnowledgeDocs({ options }) {
35
+ const response = await callAtomic(KNOWLEDGE_DOCS_PATH, {
36
+ ...options,
37
+ method: 'GET',
38
+ requiresToken: false,
39
+ });
40
+ return {
41
+ ok: true,
42
+ action: 'docs',
43
+ endpoint: { method: 'GET', path: KNOWLEDGE_DOCS_PATH, kind: 'knowledge-base' },
44
+ response,
45
+ };
46
+ }
47
+
48
+ export async function navigateKnowledge({ action, options }) {
49
+ const taskFrame = await buildTaskFrame(options);
50
+ const params = buildParams(action, options);
51
+ const body = {
52
+ action: normalizeAction(action),
53
+ task_frame: taskFrame,
54
+ params,
55
+ };
56
+ const response = await callAtomic(KNOWLEDGE_NAVIGATE_PATH, {
57
+ ...options,
58
+ method: 'POST',
59
+ body,
60
+ });
61
+ return {
62
+ ok: true,
63
+ action: normalizeAction(action),
64
+ endpoint: { method: 'POST', path: KNOWLEDGE_NAVIGATE_PATH, kind: 'knowledge-base' },
65
+ request: { body },
66
+ response,
67
+ };
68
+ }
69
+
70
+ function knowledgeCommand(action, description, extraOptions = []) {
71
+ return {
72
+ key: `knowledge.${action}`,
73
+ command: `knowledge ${action}`,
74
+ kind: 'knowledge-base',
75
+ description,
76
+ method: 'POST',
77
+ apiPath: KNOWLEDGE_NAVIGATE_PATH,
78
+ positionals: [],
79
+ options: [
80
+ ...taskFrameOptions(),
81
+ ...extraOptions,
82
+ ...commonOptions(),
83
+ ],
84
+ requestBody: {
85
+ action: normalizeAction(action),
86
+ task_frame: '<由 task-frame 参数组装>',
87
+ params: '<由命令参数组装>',
88
+ },
89
+ returns: '返回当前知识库层级数据和 next_actions,Agent 应按 next_actions 继续查询。',
90
+ };
91
+ }
92
+
93
+ function taskFrameOptions() {
94
+ return [
95
+ { flag: '--task-intent', name: 'taskIntent', required: false, label: '任务意图,例如 generate_script、generate_copy、evaluate_content。' },
96
+ { flag: '--output-format', name: 'outputFormat', required: false, label: '输出格式,例如 short_video_script、social_post、any。' },
97
+ { flag: '--domain', name: 'domain', required: false, label: '内容领域。' },
98
+ { flag: '--content-goal', name: 'contentGoal', required: false, label: '具体创作或评估目标。' },
99
+ { flag: '--tone', name: 'tone', required: false, label: '语气风格。' },
100
+ { flag: '--communication-mode', name: 'communicationMode', required: false, label: '表达方式,例如 口播、图文、文章。' },
101
+ { flag: '--task-frame-file', name: 'taskFrameFile', required: false, label: '读取 task_frame JSON 文件。' },
102
+ ];
103
+ }
104
+
105
+ function commonOptions({ tokenRequired = true } = {}) {
106
+ return [
107
+ { flag: '--extra', name: 'extra', required: false, label: 'JSON 字符串,会合并进 params。' },
108
+ { flag: '--format', name: 'format', required: false, label: 'Agent 调用时建议使用 agent-json。' },
109
+ { flag: '--dry-run', name: 'dryRun', required: false, label: `仅预览请求映射,不发起真实请求${tokenRequired ? ',也不要求 token' : ''}。` },
110
+ ];
111
+ }
112
+
113
+ async function buildTaskFrame(options) {
114
+ const fromFile = options.named?.['task-frame-file']
115
+ ? await readJsonFile(options.named['task-frame-file'])
116
+ : {};
117
+ return removeUndefined({
118
+ ...fromFile,
119
+ task_intent: pick(options, 'task-intent', fromFile.task_intent || 'general'),
120
+ output_format: pick(options, 'output-format', fromFile.output_format || 'any'),
121
+ domain: pick(options, 'domain', fromFile.domain || '自媒体运营'),
122
+ content_goal: pick(options, 'content-goal', fromFile.content_goal || ''),
123
+ tone: pick(options, 'tone', fromFile.tone || ''),
124
+ communication_mode: pick(options, 'communication-mode', fromFile.communication_mode || ''),
125
+ });
126
+ }
127
+
128
+ function buildParams(action, options) {
129
+ const params = parseExtra(options.named?.extra);
130
+ const capabilityCode = cleanOptional(options.named?.['capability-code']);
131
+ const packCode = cleanOptional(options.named?.['pack-code']);
132
+ const ruleCode = cleanOptional(options.named?.['rule-code']);
133
+ if (capabilityCode) params.capability_code = capabilityCode;
134
+ if (packCode) params.pack_code = packCode;
135
+ if (ruleCode) params.rule_code = ruleCode;
136
+ if (action === 'rules' && !params.pack_code) {
137
+ throw new Error('缺少 --pack-code。');
138
+ }
139
+ if (action === 'rule-detail' && !params.rule_code) {
140
+ throw new Error('缺少 --rule-code。');
141
+ }
142
+ return params;
143
+ }
144
+
145
+ function normalizeAction(action) {
146
+ return action === 'rule-detail' ? 'rule_detail' : action;
147
+ }
148
+
149
+ function pick(options, key, fallback) {
150
+ return cleanOptional(options.named?.[key]) ?? cleanOptional(fallback);
151
+ }
152
+
153
+ function cleanOptional(value) {
154
+ if (value === undefined || value === null) return undefined;
155
+ if (typeof value === 'string') {
156
+ const trimmed = value.trim();
157
+ return trimmed ? trimmed : undefined;
158
+ }
159
+ return value;
160
+ }
161
+
162
+ function removeUndefined(payload) {
163
+ return Object.fromEntries(Object.entries(payload).filter(([, value]) => value !== undefined));
164
+ }
165
+
166
+ function parseExtra(value) {
167
+ if (!value) return {};
168
+ if (typeof value === 'object') return value;
169
+ try {
170
+ const parsed = JSON.parse(String(value));
171
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
172
+ } catch {
173
+ return {};
174
+ }
175
+ }
@@ -0,0 +1,181 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { callAtomic } from './atomic-request.js';
4
+
5
+ const OSS_TEMP_UPLOAD_PATH = '/atomic/oss/temp-upload';
6
+ const OSS_SIGNED_URL_PATH = '/atomic/oss/signed-url';
7
+ const OSS_COPY_PATH = '/atomic/oss/copy';
8
+
9
+ export function listOssCommands() {
10
+ return [
11
+ {
12
+ key: 'oss.temp-upload',
13
+ command: 'oss temp-upload',
14
+ kind: 'oss-atomic',
15
+ description: '上传本地文件到 Yuan API 临时 OSS bucket,并返回对象 key 和临时访问信息。',
16
+ method: 'POST',
17
+ apiPath: OSS_TEMP_UPLOAD_PATH,
18
+ positionals: [],
19
+ options: [
20
+ { flag: '--file', name: 'file', required: true, label: '本地文件路径。' },
21
+ { flag: '--filename', name: 'filename', required: false, label: '上传时使用的文件名,默认取本地文件名。' },
22
+ { flag: '--key', name: 'key', required: false, label: '可选 OSS 对象 key。' },
23
+ { flag: '--content-type', name: 'contentType', required: false, label: '文件 MIME 类型,默认 application/octet-stream。' },
24
+ ...commonOptions(),
25
+ ],
26
+ requestBody: {
27
+ filename: '<filename>',
28
+ key: '<optional key>',
29
+ content_base64: '<base64>',
30
+ content_type: '<content-type>',
31
+ },
32
+ returns: '返回 bucket、key、url、signed_url、expires_at 等字段,字段以后端实际响应为准。',
33
+ },
34
+ {
35
+ key: 'oss.signed-url',
36
+ command: 'oss signed-url',
37
+ kind: 'oss-atomic',
38
+ description: '为已有 OSS 对象生成临时签名访问链接。',
39
+ method: 'POST',
40
+ apiPath: OSS_SIGNED_URL_PATH,
41
+ positionals: [],
42
+ options: [
43
+ { flag: '--key', name: 'key', required: true, label: 'OSS 对象 key。' },
44
+ { flag: '--bucket', name: 'bucket', required: false, label: '可选 bucket,不传则使用后端默认配置。' },
45
+ { flag: '--ttl-seconds', name: 'ttlSeconds', required: false, label: '签名有效期,最大 86400 秒。' },
46
+ { flag: '--method', name: 'method', required: false, label: '签名方法:GET、PUT、HEAD。' },
47
+ ...commonOptions(),
48
+ ],
49
+ requestBody: {
50
+ key: '<key>',
51
+ bucket: '<optional bucket>',
52
+ ttl_seconds: 1800,
53
+ method: 'GET',
54
+ },
55
+ returns: '返回签名 URL、过期时间和对象信息,字段以后端实际响应为准。',
56
+ },
57
+ {
58
+ key: 'oss.copy',
59
+ command: 'oss copy',
60
+ kind: 'oss-atomic',
61
+ description: '复制 OSS 对象到目标 key。',
62
+ method: 'POST',
63
+ apiPath: OSS_COPY_PATH,
64
+ positionals: [],
65
+ options: [
66
+ { flag: '--source-key', name: 'sourceKey', required: true, label: '源 OSS 对象 key。' },
67
+ { flag: '--target-key', name: 'targetKey', required: true, label: '目标 OSS 对象 key。' },
68
+ { flag: '--source-bucket', name: 'sourceBucket', required: false, label: '可选源 bucket。' },
69
+ { flag: '--target-bucket', name: 'targetBucket', required: false, label: '可选目标 bucket。' },
70
+ ...commonOptions(),
71
+ ],
72
+ requestBody: {
73
+ source_key: '<source-key>',
74
+ target_key: '<target-key>',
75
+ source_bucket: '<optional source bucket>',
76
+ target_bucket: '<optional target bucket>',
77
+ },
78
+ returns: '返回复制后的对象信息,字段以后端实际响应为准。',
79
+ },
80
+ ];
81
+ }
82
+
83
+ export async function tempUpload({ options }) {
84
+ const filePath = cleanOptional(options.named?.file || options.file);
85
+ if (!filePath) {
86
+ throw new Error('缺少 --file。');
87
+ }
88
+ const filename = cleanOptional(options.named?.filename) || path.basename(filePath);
89
+ const contentType = cleanOptional(options.named?.['content-type']) || 'application/octet-stream';
90
+ const body = {
91
+ filename,
92
+ content_base64: options.dryRun ? '<base64 omitted in dry-run>' : (await readFile(filePath)).toString('base64'),
93
+ content_type: contentType,
94
+ ...optionalField('key', options.named?.key),
95
+ };
96
+ const response = await callAtomic(OSS_TEMP_UPLOAD_PATH, {
97
+ ...options,
98
+ method: 'POST',
99
+ body,
100
+ });
101
+ return result('temp-upload', OSS_TEMP_UPLOAD_PATH, body, response);
102
+ }
103
+
104
+ export async function signedUrl({ options }) {
105
+ const key = cleanOptional(options.named?.key);
106
+ if (!key) {
107
+ throw new Error('缺少 --key。');
108
+ }
109
+ const body = {
110
+ key,
111
+ ...optionalField('bucket', options.named?.bucket),
112
+ ...optionalField('ttl_seconds', numberOrString(options.named?.['ttl-seconds'])),
113
+ ...optionalField('method', cleanOptional(options.named?.method) || 'GET'),
114
+ };
115
+ const response = await callAtomic(OSS_SIGNED_URL_PATH, {
116
+ ...options,
117
+ method: 'POST',
118
+ body,
119
+ });
120
+ return result('signed-url', OSS_SIGNED_URL_PATH, body, response);
121
+ }
122
+
123
+ export async function copyObject({ options }) {
124
+ const sourceKey = cleanOptional(options.named?.['source-key']);
125
+ const targetKey = cleanOptional(options.named?.['target-key']);
126
+ if (!sourceKey) {
127
+ throw new Error('缺少 --source-key。');
128
+ }
129
+ if (!targetKey) {
130
+ throw new Error('缺少 --target-key。');
131
+ }
132
+ const body = {
133
+ source_key: sourceKey,
134
+ target_key: targetKey,
135
+ ...optionalField('source_bucket', options.named?.['source-bucket']),
136
+ ...optionalField('target_bucket', options.named?.['target-bucket']),
137
+ };
138
+ const response = await callAtomic(OSS_COPY_PATH, {
139
+ ...options,
140
+ method: 'POST',
141
+ body,
142
+ });
143
+ return result('copy', OSS_COPY_PATH, body, response);
144
+ }
145
+
146
+ function commonOptions() {
147
+ return [
148
+ { flag: '--format', name: 'format', required: false, label: 'Agent 调用时建议使用 agent-json。' },
149
+ { flag: '--dry-run', name: 'dryRun', required: false, label: '仅预览请求映射,不发起真实请求,也不要求 token。' },
150
+ ];
151
+ }
152
+
153
+ function result(action, endpointPath, body, response) {
154
+ return {
155
+ ok: true,
156
+ action,
157
+ endpoint: { method: 'POST', path: endpointPath, kind: 'oss-atomic' },
158
+ request: { body },
159
+ response,
160
+ };
161
+ }
162
+
163
+ function optionalField(name, value) {
164
+ const cleaned = cleanOptional(value);
165
+ return cleaned === undefined ? {} : { [name]: cleaned };
166
+ }
167
+
168
+ function cleanOptional(value) {
169
+ if (value === undefined || value === null) return undefined;
170
+ if (typeof value === 'string') {
171
+ const trimmed = value.trim();
172
+ return trimmed ? trimmed : undefined;
173
+ }
174
+ return value;
175
+ }
176
+
177
+ function numberOrString(value) {
178
+ const cleaned = cleanOptional(value);
179
+ if (cleaned === undefined) return undefined;
180
+ return /^-?\d+$/.test(String(cleaned)) ? Number(cleaned) : cleaned;
181
+ }