yuanflow-cli 0.1.0 → 0.1.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.
Files changed (39) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +34 -50
  3. package/bin/yuanflow-cli.js +26 -0
  4. package/bin/yuanflow-skill.cjs +334 -0
  5. package/generated/registry.json +24641 -0
  6. package/lib/skill-installer/agents.cjs +203 -0
  7. package/lib/skill-installer/discover-skills.cjs +79 -0
  8. package/lib/skill-installer/installer.cjs +300 -0
  9. package/lib/skill-installer/publish.cjs +53 -0
  10. package/lib/skill-installer/repo-source.cjs +157 -0
  11. package/package.json +56 -6
  12. package/scripts/generate-registry.js +174 -0
  13. package/src/agent-protocol.js +169 -0
  14. package/src/cli.js +382 -0
  15. package/src/config.js +31 -0
  16. package/src/registry.js +45 -0
  17. package/src/request.js +97 -0
  18. package/src/shortcuts.js +346 -0
  19. package/cli.js +0 -3
  20. package/src/ycloud/cli.js +0 -29
  21. package/src/ycloud/commands/analysis.js +0 -82
  22. package/src/ycloud/commands/auth.js +0 -191
  23. package/src/ycloud/commands/commands.js +0 -262
  24. package/src/ycloud/commands/compliance.js +0 -146
  25. package/src/ycloud/commands/config.js +0 -103
  26. package/src/ycloud/commands/health.js +0 -35
  27. package/src/ycloud/commands/index.js +0 -381
  28. package/src/ycloud/commands/kb.js +0 -82
  29. package/src/ycloud/commands/schema.js +0 -229
  30. package/src/ycloud/commands/shared.js +0 -30
  31. package/src/ycloud/commands/tool-registry.js +0 -209
  32. package/src/ycloud/commands/tool-runner.js +0 -226
  33. package/src/ycloud/commands/tool.js +0 -178
  34. package/src/ycloud/commands/version.js +0 -84
  35. package/src/ycloud/core/config.js +0 -78
  36. package/src/ycloud/core/http.js +0 -133
  37. package/src/ycloud/core/token.js +0 -30
  38. package/src/ycloud/resources/.gitkeep +0 -1
  39. package/src/ycloud/resources/tool_catalog_full.json +0 -1
package/src/cli.js ADDED
@@ -0,0 +1,382 @@
1
+ import { writeFile } from 'node:fs/promises';
2
+ import { getConfigFilePath, readConfig, writeConfig } from './config.js';
3
+ import {
4
+ findEndpointByAlias,
5
+ findEndpointByCommand,
6
+ getPlatforms,
7
+ listEndpoints,
8
+ } from './registry.js';
9
+ import { callEndpoint } from './request.js';
10
+ import {
11
+ buildCommandRegistry,
12
+ commandToSchema,
13
+ createAgentSuccess,
14
+ findCommandByKey,
15
+ getCommandName,
16
+ isAgentJsonFormat,
17
+ } from './agent-protocol.js';
18
+ import { findShortcut, listShortcuts } from './shortcuts.js';
19
+
20
+ export async function main(argv) {
21
+ const args = argv.slice(2);
22
+ const [command, ...rest] = args;
23
+
24
+ if (!command || command === 'help' || command === '--help' || command === '-h') {
25
+ printHelp();
26
+ return;
27
+ }
28
+
29
+ if (command === 'config') {
30
+ await handleConfig(rest);
31
+ return;
32
+ }
33
+
34
+ if (command === 'list') {
35
+ handleList(rest);
36
+ return;
37
+ }
38
+
39
+ if (command === 'shortcuts') {
40
+ handleShortcuts(rest);
41
+ return;
42
+ }
43
+
44
+ if (command === 'commands') {
45
+ await handleCommands(rest);
46
+ return;
47
+ }
48
+
49
+ if (command === 'schema') {
50
+ await handleSchema(rest);
51
+ return;
52
+ }
53
+
54
+ if (command === 'call') {
55
+ await handleCall(rest);
56
+ return;
57
+ }
58
+
59
+ await handleGeneratedCommand(command, rest);
60
+ }
61
+
62
+ async function handleCommands(args) {
63
+ const { positionals, options } = parseOptions(args);
64
+ const [action, key] = positionals;
65
+
66
+ if (!action || action === 'list') {
67
+ const commands = buildCommandRegistry().map((item) => ({
68
+ key: item.key,
69
+ command: item.command,
70
+ kind: item.kind,
71
+ description: item.description,
72
+ method: item.method,
73
+ socialPath: item.socialPath,
74
+ }));
75
+ await outputResult(
76
+ createAgentSuccess('commands list', { commands }),
77
+ { ...options, forceJson: true },
78
+ );
79
+ return;
80
+ }
81
+
82
+ if (action === 'describe') {
83
+ if (!key) {
84
+ throw new Error('缺少命令标识,例如 douyin.video-detail');
85
+ }
86
+ const command = findCommandByKey(key);
87
+ if (!command) {
88
+ throw new Error(`未找到命令: ${key}`);
89
+ }
90
+ await outputResult(createAgentSuccess('commands describe', command), {
91
+ ...options,
92
+ forceJson: true,
93
+ });
94
+ return;
95
+ }
96
+
97
+ throw new Error('未知 commands 命令。用法:yuanflow-cli commands list');
98
+ }
99
+
100
+ async function handleSchema(args) {
101
+ const { positionals, options } = parseOptions(args);
102
+ const [key] = positionals;
103
+
104
+ if (!key || key === 'list') {
105
+ const commands = buildCommandRegistry().map((item) => ({
106
+ key: item.key,
107
+ command: item.command,
108
+ kind: item.kind,
109
+ method: item.method,
110
+ socialPath: item.socialPath,
111
+ }));
112
+ await outputResult(createAgentSuccess('schema list', { commands }), {
113
+ ...options,
114
+ forceJson: true,
115
+ });
116
+ return;
117
+ }
118
+
119
+ const command = findCommandByKey(key);
120
+ if (!command) {
121
+ throw new Error(`不支持的 schema 命令: ${key}`);
122
+ }
123
+ await outputResult(
124
+ createAgentSuccess(`schema ${key}`, commandToSchema(command)),
125
+ { ...options, forceJson: true },
126
+ );
127
+ }
128
+
129
+ async function handleConfig(args) {
130
+ const [action, key, ...valueParts] = args;
131
+ const value = valueParts.join(' ');
132
+
133
+ if (!action || action === 'show') {
134
+ const config = await readConfig();
135
+ console.log(JSON.stringify({ ...config, token: maskToken(config.token) }, null, 2));
136
+ console.log(`配置文件:${getConfigFilePath()}`);
137
+ return;
138
+ }
139
+
140
+ if (action === 'set-token') {
141
+ await writeConfig({ token: key || value });
142
+ console.log('token 已保存');
143
+ return;
144
+ }
145
+
146
+ if (action === 'set-base-url') {
147
+ await writeConfig({ baseUrl: key || value });
148
+ console.log('baseUrl 已保存');
149
+ return;
150
+ }
151
+
152
+ throw new Error('未知 config 命令。用法:yuanflow-cli config set-token <你的令牌>');
153
+ }
154
+
155
+ function handleList(args) {
156
+ const [platform] = args;
157
+ const endpoints = listEndpoints().filter((endpoint) => {
158
+ return platform ? endpoint.platform === platform : true;
159
+ });
160
+
161
+ for (const endpoint of endpoints) {
162
+ const aliases = endpoint.aliases.length > 0 ? ` aliases: ${endpoint.aliases.join(', ')}` : '';
163
+ console.log(`${endpoint.platform} ${endpoint.command} -> ${endpoint.method} /social${endpoint.socialPath}${aliases}`);
164
+ }
165
+ }
166
+
167
+ function handleShortcuts(args) {
168
+ const [platform] = args;
169
+ const items = listShortcuts(platform);
170
+ if (platform && items.length === 0) {
171
+ throw new Error(`未找到平台 ${platform} 的快捷命令。`);
172
+ }
173
+
174
+ for (const shortcut of items) {
175
+ const positionalUsage = shortcut.positionals.map((item) => `<${item.name}>`).join(' ');
176
+ const optionUsage = shortcut.options.map((item) => `[--${item.flag}]`).join(' ');
177
+ const usage = [shortcut.platform, shortcut.command, positionalUsage, optionUsage]
178
+ .filter(Boolean)
179
+ .join(' ');
180
+ console.log(`${usage}`);
181
+ console.log(` ${shortcut.description}`);
182
+ console.log(` 接口:${shortcut.method} /social${shortcut.socialPath}`);
183
+ if (shortcut.positionals.length > 0 || shortcut.options.length > 0) {
184
+ console.log(' 参数:');
185
+ for (const item of shortcut.positionals) {
186
+ console.log(` <${item.name}> ${item.label}`);
187
+ }
188
+ for (const item of shortcut.options) {
189
+ console.log(` --${item.flag} ${item.label}`);
190
+ }
191
+ }
192
+ console.log(` 返回:${shortcut.returns}`);
193
+ if (shortcut.alternatives.length > 0) {
194
+ console.log(` 备用接口:${shortcut.alternatives.join(';')}`);
195
+ }
196
+ }
197
+ }
198
+
199
+ async function handleCall(args) {
200
+ const { positionals, options } = parseOptions(args);
201
+ const [socialPath] = positionals;
202
+ if (!socialPath) {
203
+ throw new Error('缺少接口路径。示例:yuanflow-cli call /douyin/web/fetch_multi_video_v2 --json "{\"url\":\"...\"}"');
204
+ }
205
+
206
+ const result = await callEndpoint(socialPath, options);
207
+ await outputResult(result, options, {
208
+ command: `call ${socialPath}`,
209
+ meta: { endpoint: socialPath },
210
+ });
211
+ }
212
+
213
+ async function handleGeneratedCommand(platform, args) {
214
+ if (!getPlatforms().includes(platform)) {
215
+ throw new Error(`未知平台:${platform}。可用平台:${getPlatforms().join(', ')}`);
216
+ }
217
+
218
+ const { positionals, options } = parseOptions(args);
219
+ const [commandOrAlias, ...rest] = positionals;
220
+ const shortcut = findShortcut(platform, commandOrAlias);
221
+
222
+ if (shortcut) {
223
+ const body = buildShortcutBody(shortcut, rest, options);
224
+ const result = await callEndpoint(shortcut.socialPath, {
225
+ ...options,
226
+ method: shortcut.method,
227
+ body,
228
+ });
229
+ await outputResult(result, options, {
230
+ command: getCommandName(platform, shortcut.command),
231
+ meta: { endpoint: shortcut.socialPath, kind: 'shortcut' },
232
+ });
233
+ return;
234
+ }
235
+
236
+ const aliasEndpoint = findEndpointByAlias(platform, commandOrAlias);
237
+ const endpoint = aliasEndpoint || findEndpointByCommand(platform, [commandOrAlias, ...rest]);
238
+ const commandArgs = aliasEndpoint ? rest : [];
239
+
240
+ if (!endpoint) {
241
+ throw new Error(`未找到命令:${platform} ${positionals.join(' ')}。可执行 yuanflow-cli list ${platform} 查看命令。`);
242
+ }
243
+
244
+ const body = buildBodyFromPositionals(endpoint, commandArgs, options);
245
+ const result = await callEndpoint(endpoint.socialPath, {
246
+ ...options,
247
+ method: endpoint.method,
248
+ body,
249
+ });
250
+ await outputResult(result, options, {
251
+ command: getCommandName(platform, endpoint.command),
252
+ meta: { endpoint: endpoint.socialPath, kind: 'endpoint' },
253
+ });
254
+ }
255
+
256
+ function buildShortcutBody(shortcut, rest, options) {
257
+ if (options.json || options.file) {
258
+ return undefined;
259
+ }
260
+
261
+ const body = { ...(shortcut.defaults || {}) };
262
+ for (const [index, item] of shortcut.positionals.entries()) {
263
+ const value = rest[index];
264
+ if (value === undefined) {
265
+ throw new Error(`缺少参数 <${item.name}>。可执行 yuanflow-cli shortcuts ${shortcut.platform} 查看用法。`);
266
+ }
267
+ body[item.name] = value;
268
+ }
269
+
270
+ for (const item of shortcut.options) {
271
+ const value = options.named?.[item.flag];
272
+ if (value !== undefined) {
273
+ body[item.name] = value;
274
+ }
275
+ }
276
+
277
+ return body;
278
+ }
279
+
280
+ function buildBodyFromPositionals(endpoint, rest, options) {
281
+ if (options.json || options.file) {
282
+ return undefined;
283
+ }
284
+
285
+ if (endpoint.aliases.includes('video-detail') && rest.length > 0) {
286
+ return { url: rest[0] };
287
+ }
288
+
289
+ return {};
290
+ }
291
+
292
+ function parseOptions(args) {
293
+ const options = { named: {} };
294
+ const positionals = [];
295
+
296
+ for (let index = 0; index < args.length; index += 1) {
297
+ const arg = args[index];
298
+ if (arg === '--json') {
299
+ options.json = args[++index];
300
+ } else if (arg === '--file') {
301
+ options.file = args[++index];
302
+ } else if (arg === '--output' || arg === '-o') {
303
+ options.output = args[++index];
304
+ } else if (arg === '--base-url') {
305
+ options.baseUrl = args[++index];
306
+ } else if (arg === '--token') {
307
+ options.token = args[++index];
308
+ } else if (arg === '--method') {
309
+ options.method = args[++index];
310
+ } else if (arg === '--format') {
311
+ options.format = args[++index];
312
+ } else if (arg === '--dry-run') {
313
+ options.dryRun = true;
314
+ } else if (arg.startsWith('--')) {
315
+ const name = arg.slice(2);
316
+ const next = args[index + 1];
317
+ if (next === undefined || next.startsWith('--')) {
318
+ options.named[name] = true;
319
+ } else {
320
+ options.named[name] = args[++index];
321
+ }
322
+ } else {
323
+ positionals.push(arg);
324
+ }
325
+ }
326
+
327
+ return { positionals, options };
328
+ }
329
+
330
+ async function outputResult(result, options, agentContext = {}) {
331
+ if (isAgentJsonFormat(options) && !options.forceJson) {
332
+ const payload = createAgentSuccess(
333
+ agentContext.command || 'unknown',
334
+ result,
335
+ agentContext.meta || {},
336
+ );
337
+ process.stdout.write(`${JSON.stringify(payload)}\n`);
338
+ return;
339
+ }
340
+
341
+ const text = typeof result === 'string' ? result : `${JSON.stringify(result, null, 2)}\n`;
342
+ if (options.output) {
343
+ await writeFile(options.output, text, 'utf8');
344
+ console.log(`已保存:${options.output}`);
345
+ return;
346
+ }
347
+ process.stdout.write(text);
348
+ }
349
+
350
+ function printHelp() {
351
+ console.log(`yuanflow-cli
352
+
353
+ 用法:
354
+ yuanflow-cli config set-token <你的令牌>
355
+ yuanflow-cli config set-base-url https://open.yuanchuangai.com
356
+ yuanflow-cli call /douyin/app/v3/fetch_multi_video_v2 --json '{"url":"https://v.douyin.com/xxx/"}'
357
+ yuanflow-cli douyin video-detail "https://v.douyin.com/xxx/"
358
+ yuanflow-cli douyin video-detail "https://v.douyin.com/xxx/" --dry-run
359
+ yuanflow-cli douyin video-download "https://v.douyin.com/xxx/" --region CN
360
+ yuanflow-cli xiaohongshu note-detail note_id --xsec-token xxx
361
+ yuanflow-cli bilibili user-posts uid --page 1
362
+ yuanflow-cli wechat channels-search "关键词"
363
+ yuanflow-cli shortcuts douyin
364
+ yuanflow-cli commands list
365
+ yuanflow-cli schema douyin.video-detail
366
+ yuanflow-cli list douyin
367
+
368
+ 说明:
369
+ 所有请求都会调用 Yuan API 的 /social/*path,并使用 Authorization: Bearer <token>。
370
+ AI Agent 推荐使用 --format agent-json 获取稳定 JSON 外壳。
371
+ `);
372
+ }
373
+
374
+ function maskToken(token) {
375
+ if (!token) {
376
+ return '';
377
+ }
378
+ if (token.length <= 10) {
379
+ return '***';
380
+ }
381
+ return `${token.slice(0, 6)}...${token.slice(-4)}`;
382
+ }
package/src/config.js ADDED
@@ -0,0 +1,31 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ const CONFIG_DIR = path.join(homedir(), '.yuanflow-cli');
6
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
7
+
8
+ const DEFAULT_CONFIG = {
9
+ baseUrl: 'https://open.yuanchuangai.com',
10
+ token: '',
11
+ };
12
+
13
+ export async function readConfig() {
14
+ try {
15
+ const raw = await readFile(CONFIG_FILE, 'utf8');
16
+ return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
17
+ } catch {
18
+ return { ...DEFAULT_CONFIG };
19
+ }
20
+ }
21
+
22
+ export async function writeConfig(nextConfig) {
23
+ await mkdir(CONFIG_DIR, { recursive: true });
24
+ const config = { ...(await readConfig()), ...nextConfig };
25
+ await writeFile(CONFIG_FILE, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
26
+ return config;
27
+ }
28
+
29
+ export function getConfigFilePath() {
30
+ return CONFIG_FILE;
31
+ }
@@ -0,0 +1,45 @@
1
+ import registry from '../generated/registry.json' with { type: 'json' };
2
+
3
+ export function listEndpoints() {
4
+ return registry.endpoints;
5
+ }
6
+
7
+ export function findEndpointByPath(socialPath) {
8
+ const normalized = normalizeSocialPath(socialPath);
9
+ return registry.endpoints.find((endpoint) => endpoint.socialPath === normalized);
10
+ }
11
+
12
+ export function findEndpointByCommand(platform, commandParts) {
13
+ const command = commandParts.join(' ');
14
+ return registry.endpoints.find(
15
+ (endpoint) => endpoint.platform === platform && endpoint.command === command,
16
+ );
17
+ }
18
+
19
+ export function findEndpointByAlias(platform, alias) {
20
+ return registry.endpoints.find(
21
+ (endpoint) => endpoint.platform === platform && endpoint.aliases.includes(alias),
22
+ );
23
+ }
24
+
25
+ export function getPlatforms() {
26
+ return registry.platforms;
27
+ }
28
+
29
+ export function normalizeSocialPath(value) {
30
+ if (!value || typeof value !== 'string') {
31
+ return '';
32
+ }
33
+
34
+ const trimmed = value.trim();
35
+ if (trimmed.startsWith('/social/')) {
36
+ return trimmed.slice('/social'.length);
37
+ }
38
+ if (trimmed.startsWith('/api/v1/')) {
39
+ return trimmed.slice('/api/v1'.length);
40
+ }
41
+ if (!trimmed.startsWith('/')) {
42
+ return `/${trimmed}`;
43
+ }
44
+ return trimmed;
45
+ }
package/src/request.js ADDED
@@ -0,0 +1,97 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { readConfig } from './config.js';
3
+ import { findEndpointByPath, normalizeSocialPath } from './registry.js';
4
+
5
+ export async function callEndpoint(socialPath, options = {}) {
6
+ const config = await readConfig();
7
+ const baseUrl = cleanBaseUrl(options.baseUrl || config.baseUrl);
8
+ const token = options.token || config.token || process.env.YUANFLOW_TOKEN || '';
9
+ const normalizedPath = normalizeSocialPath(socialPath);
10
+ const endpoint = findEndpointByPath(normalizedPath);
11
+ const method = options.method || endpoint?.method || 'POST';
12
+ const url = new URL(`/social${normalizedPath}`, baseUrl);
13
+
14
+ if (!token) {
15
+ throw new Error('缺少 token。请先执行 yuanflow-cli config set-token <你的令牌>');
16
+ }
17
+
18
+ const body = await resolveBody(options);
19
+ if (method.toUpperCase() === 'GET' && body && typeof body === 'object') {
20
+ for (const [key, value] of Object.entries(body)) {
21
+ if (value !== undefined && value !== null) {
22
+ url.searchParams.set(key, String(value));
23
+ }
24
+ }
25
+ }
26
+
27
+ if (options.dryRun) {
28
+ return {
29
+ dryRun: true,
30
+ method: method.toUpperCase(),
31
+ url: url.toString(),
32
+ headers: {
33
+ Authorization: `Bearer ${maskToken(token)}`,
34
+ },
35
+ body: method.toUpperCase() === 'GET' ? undefined : body || {},
36
+ };
37
+ }
38
+
39
+ const response = await fetch(url, {
40
+ method: method.toUpperCase(),
41
+ headers: {
42
+ Authorization: `Bearer ${token}`,
43
+ Accept: 'application/json',
44
+ ...(method.toUpperCase() === 'GET' ? {} : { 'Content-Type': 'application/json' }),
45
+ },
46
+ body: method.toUpperCase() === 'GET' ? undefined : JSON.stringify(body || {}),
47
+ });
48
+
49
+ const text = await response.text();
50
+ const payload = parseMaybeJson(text);
51
+
52
+ if (!response.ok) {
53
+ const message = typeof payload === 'object' ? JSON.stringify(payload) : text;
54
+ throw new Error(`请求失败:HTTP ${response.status} ${message}`);
55
+ }
56
+
57
+ return payload;
58
+ }
59
+
60
+ async function resolveBody(options) {
61
+ if (options.json) {
62
+ return JSON.parse(options.json);
63
+ }
64
+ if (options.file) {
65
+ const raw = await readFile(options.file, 'utf8');
66
+ return JSON.parse(raw);
67
+ }
68
+ if (options.body) {
69
+ return options.body;
70
+ }
71
+ return {};
72
+ }
73
+
74
+ function cleanBaseUrl(value) {
75
+ return (value || 'https://open.yuanchuangai.com').replace(/\/+$/, '');
76
+ }
77
+
78
+ function parseMaybeJson(text) {
79
+ if (!text) {
80
+ return null;
81
+ }
82
+ try {
83
+ return JSON.parse(text);
84
+ } catch {
85
+ return text;
86
+ }
87
+ }
88
+
89
+ function maskToken(token) {
90
+ if (!token) {
91
+ return '';
92
+ }
93
+ if (token.length <= 10) {
94
+ return '***';
95
+ }
96
+ return `${token.slice(0, 6)}...${token.slice(-4)}`;
97
+ }