yuanflow-cli 0.1.5 → 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.
@@ -0,0 +1,344 @@
1
+ import { callEndpoint } from './request.js';
2
+ import { extractTargetId, normalizePlatform } from './comment-collector.js';
3
+
4
+ import {
5
+ CONTENT_SEARCH_ENDPOINTS,
6
+ DETAIL_ENDPOINTS,
7
+ DOWNLOAD_ENDPOINTS,
8
+ USER_SEARCH_ENDPOINTS,
9
+ } from './work-tool-definitions.js';
10
+
11
+ export function listWorkCommands() {
12
+ return [
13
+ ...Object.keys(DETAIL_ENDPOINTS).map((platform) =>
14
+ workCommand(platform, 'detail', firstPrimary(DETAIL_ENDPOINTS[platform]), 'work-detail'),
15
+ ),
16
+ ...Object.keys(DOWNLOAD_ENDPOINTS).map((platform) =>
17
+ workCommand(platform, 'download', firstPrimary(DOWNLOAD_ENDPOINTS[platform]), 'work-download'),
18
+ ),
19
+ ];
20
+ }
21
+
22
+ export function listSearchCommands() {
23
+ return [
24
+ ...Object.keys(CONTENT_SEARCH_ENDPOINTS).map((platform) =>
25
+ searchCommand(platform, 'content', CONTENT_SEARCH_ENDPOINTS[platform], 'content-search'),
26
+ ),
27
+ ...Object.keys(USER_SEARCH_ENDPOINTS).map((platform) =>
28
+ searchCommand(platform, 'users', USER_SEARCH_ENDPOINTS[platform], 'user-search'),
29
+ ),
30
+ ];
31
+ }
32
+
33
+ export async function getWorkDetail({ platform, target, prefer = 'primary', kind, targetKind, options }) {
34
+ return callWorkEndpoint({
35
+ endpoints: DETAIL_ENDPOINTS,
36
+ action: 'detail',
37
+ platform,
38
+ target,
39
+ prefer,
40
+ kind,
41
+ targetKind,
42
+ options,
43
+ });
44
+ }
45
+
46
+ export async function getWorkDownload({ platform, target, prefer = 'primary', kind, targetKind, options }) {
47
+ return callWorkEndpoint({
48
+ endpoints: DOWNLOAD_ENDPOINTS,
49
+ action: 'download',
50
+ platform,
51
+ target,
52
+ prefer,
53
+ kind,
54
+ targetKind,
55
+ options,
56
+ });
57
+ }
58
+
59
+ export async function searchContent({ platform, keyword, options }) {
60
+ return callSearchEndpoint({
61
+ endpoints: CONTENT_SEARCH_ENDPOINTS,
62
+ action: 'content',
63
+ kind: 'content-search',
64
+ platform,
65
+ keyword,
66
+ options,
67
+ });
68
+ }
69
+
70
+ export async function searchUsers({ platform, keyword, options }) {
71
+ return callSearchEndpoint({
72
+ endpoints: USER_SEARCH_ENDPOINTS,
73
+ action: 'users',
74
+ kind: 'user-search',
75
+ platform,
76
+ keyword,
77
+ options,
78
+ });
79
+ }
80
+
81
+ async function callWorkEndpoint({ endpoints, action, platform, target, prefer, kind, targetKind, options }) {
82
+ const normalizedPlatform = normalizeWorkPlatform(platform);
83
+ const candidates = endpoints[normalizedPlatform] || [];
84
+ const endpoint = selectWorkEndpoint(candidates, { target, prefer, kind, targetKind });
85
+ if (!endpoint) {
86
+ throw new Error(`当前平台暂未接入作品${action === 'detail' ? '详情' : '下载'}工具:${normalizedPlatform}`);
87
+ }
88
+ validateRequiredOptions(endpoint, options);
89
+ const targetValue = resolveTargetValue(normalizedPlatform, endpoint, target);
90
+ if (!targetValue) {
91
+ throw new Error('请提供作品链接或作品 ID。');
92
+ }
93
+ const body = buildPayload(endpoint, endpoint.targetParam, targetValue, options);
94
+ const response = await callEndpoint(endpoint.path, {
95
+ ...options,
96
+ method: endpoint.method,
97
+ body,
98
+ });
99
+ return {
100
+ ok: true,
101
+ platform: normalizedPlatform,
102
+ action,
103
+ target: targetValue,
104
+ endpoint: endpointSummary(endpoint),
105
+ request: endpoint.bodyMode ? { body } : { params: body },
106
+ response,
107
+ };
108
+ }
109
+
110
+ async function callSearchEndpoint({ endpoints, action, kind, platform, keyword, options }) {
111
+ const normalizedPlatform = normalizeWorkPlatform(platform);
112
+ const endpoint = endpoints[normalizedPlatform];
113
+ if (!endpoint) {
114
+ throw new Error(`当前平台暂未接入${action === 'content' ? '综合搜索' : '用户搜索'}工具:${normalizedPlatform}`);
115
+ }
116
+ const cleanedKeyword = cleanOptional(keyword);
117
+ if (!cleanedKeyword) {
118
+ throw new Error('缺少 --keyword 或关键词位置参数。');
119
+ }
120
+ const body = buildPayload(endpoint, endpoint.keywordParam, cleanedKeyword, options);
121
+ const response = await callEndpoint(endpoint.path, {
122
+ ...options,
123
+ method: endpoint.method,
124
+ body,
125
+ });
126
+ return {
127
+ ok: true,
128
+ platform: normalizedPlatform,
129
+ action,
130
+ keyword: cleanedKeyword,
131
+ endpoint: endpointSummary(endpoint, kind),
132
+ request: endpoint.bodyMode ? { body } : { params: body },
133
+ response,
134
+ };
135
+ }
136
+
137
+ function workCommand(platform, action, endpoint, kind) {
138
+ return {
139
+ key: `works.${platform}.${action}`,
140
+ command: `works ${action} --platform ${platform}`,
141
+ kind,
142
+ description: endpoint.description,
143
+ method: endpoint.method,
144
+ socialPath: endpoint.path,
145
+ positionals: [],
146
+ options: buildWorkOptions(endpoint, action),
147
+ queryParams: endpoint.bodyMode ? [] : buildCommandParams(endpoint, 'target'),
148
+ requestBody: endpoint.bodyMode ? requestBodyExample(endpoint, 'target') : null,
149
+ returns:
150
+ action === 'download'
151
+ ? '返回作品可播放媒体信息、下载地址候选或视频流信息,字段以上游实际响应为准。'
152
+ : '返回作品详情、作者信息、正文/标题、互动统计和媒体信息,字段以上游实际响应为准。',
153
+ };
154
+ }
155
+
156
+ function searchCommand(platform, action, endpoint, kind) {
157
+ return {
158
+ key: `search.${platform}.${action}`,
159
+ command: `search ${action} --platform ${platform}`,
160
+ kind,
161
+ description: endpoint.description,
162
+ method: endpoint.method,
163
+ socialPath: endpoint.path,
164
+ positionals: [],
165
+ options: buildSearchOptions(endpoint, action),
166
+ queryParams: endpoint.bodyMode ? [] : buildCommandParams(endpoint, 'keyword'),
167
+ requestBody: endpoint.bodyMode ? requestBodyExample(endpoint, 'keyword') : null,
168
+ returns:
169
+ action === 'users'
170
+ ? '返回用户/频道搜索结果、分页字段和平台原始摘要字段,字段以上游实际响应为准。'
171
+ : '返回内容搜索结果、分页字段和平台原始摘要字段,字段以上游实际响应为准。',
172
+ };
173
+ }
174
+
175
+ function buildWorkOptions(endpoint, action) {
176
+ const options = [
177
+ { flag: '--platform', name: 'platform', required: true, label: '平台标识,例如 douyin、youtube、bilibili。' },
178
+ { flag: '--target', name: 'target', required: true, label: '作品链接、分享链接或作品 ID。' },
179
+ { flag: '--prefer', name: 'prefer', required: false, label: '接口偏好:primary、fallback;YouTube 下载可用 signed。' },
180
+ { flag: '--kind', name: 'kind', required: false, label: '内容类型,例如 video、post、article、question。' },
181
+ { flag: '--target-kind', name: 'targetKind', required: false, label: '目标 ID 类型,例如 Instagram 的 code 或 media_id。' },
182
+ { flag: '--extra', name: 'extra', required: false, label: 'JSON 字符串补充参数,会合并进请求参数。' },
183
+ { flag: '--format', name: 'format', required: false, label: 'Agent 调用时建议使用 agent-json。' },
184
+ { flag: '--dry-run', name: 'dryRun', required: false, label: '仅返回请求映射,不发起真实接口请求。' },
185
+ ];
186
+ for (const flag of Object.keys(endpoint.optionParams || {})) {
187
+ options.push({
188
+ flag: `--${flag}`,
189
+ name: flag,
190
+ required: (endpoint.requiredOptions || []).includes(flag),
191
+ label: `${action === 'download' ? '下载' : '详情'}接口补充参数:${endpoint.optionParams[flag]}。`,
192
+ });
193
+ }
194
+ return options;
195
+ }
196
+
197
+ function buildSearchOptions(endpoint) {
198
+ const options = [
199
+ { flag: '--platform', name: 'platform', required: true, label: '平台标识,例如 douyin、xiaohongshu、instagram。' },
200
+ { flag: '--keyword', name: 'keyword', required: true, label: '搜索关键词。' },
201
+ { flag: '--extra', name: 'extra', required: false, label: 'JSON 字符串补充参数,会合并进请求参数。' },
202
+ { flag: '--format', name: 'format', required: false, label: 'Agent 调用时建议使用 agent-json。' },
203
+ { flag: '--dry-run', name: 'dryRun', required: false, label: '仅返回请求映射,不发起真实接口请求。' },
204
+ ];
205
+ for (const flag of Object.keys(endpoint.optionParams || {})) {
206
+ options.push({
207
+ flag: `--${flag}`,
208
+ name: flag,
209
+ required: false,
210
+ label: `搜索接口补充参数:${endpoint.optionParams[flag]}。`,
211
+ });
212
+ }
213
+ return options;
214
+ }
215
+
216
+ function buildCommandParams(endpoint, mainName) {
217
+ const mainParam = endpoint.targetParam || endpoint.keywordParam;
218
+ const params = [{ name: mainParam, required: true, description: `由 --${mainName} 映射得到。` }];
219
+ for (const paramName of Object.values(endpoint.optionParams || {})) {
220
+ params.push({ name: paramName, required: (endpoint.requiredOptions || []).includes(paramName), description: '由同名 CLI 参数或 --extra 映射得到。' });
221
+ }
222
+ return params;
223
+ }
224
+
225
+ function requestBodyExample(endpoint, mainName) {
226
+ const mainParam = endpoint.targetParam || endpoint.keywordParam;
227
+ return {
228
+ ...(endpoint.defaults || {}),
229
+ [mainParam]: `<${mainName}>`,
230
+ };
231
+ }
232
+
233
+ function firstPrimary(endpoints) {
234
+ return endpoints.find((endpoint) => !endpoint.prefer) || endpoints[0];
235
+ }
236
+
237
+ function selectWorkEndpoint(candidates, { target, prefer, kind, targetKind }) {
238
+ const urlTarget = looksLikeUrl(target);
239
+ const preferred = candidates.filter((endpoint) => {
240
+ if (prefer && prefer !== 'primary') {
241
+ return endpoint.prefer === prefer;
242
+ }
243
+ return !endpoint.prefer;
244
+ });
245
+ const pool = preferred.length > 0 ? preferred : candidates;
246
+ return (
247
+ pool.find((endpoint) => kind && endpoint.kind === kind) ||
248
+ pool.find((endpoint) => targetKind && endpoint.targetKind === targetKind) ||
249
+ pool.find((endpoint) => urlTarget && ['raw-url', 'raw'].includes(endpoint.targetMode)) ||
250
+ pool.find((endpoint) => !urlTarget && endpoint.targetMode === 'id') ||
251
+ pool[0]
252
+ );
253
+ }
254
+
255
+ function resolveTargetValue(platform, endpoint, target) {
256
+ if (endpoint.targetMode === 'raw' || endpoint.targetMode === 'raw-url') {
257
+ return String(target || '').trim();
258
+ }
259
+ return extractTargetId(platform, target);
260
+ }
261
+
262
+ function buildPayload(endpoint, mainParam, mainValue, options) {
263
+ const payload = { ...(endpoint.defaults || {}), [mainParam]: mainValue };
264
+ for (const [flag, param] of Object.entries(endpoint.optionParams || {})) {
265
+ const value = cleanOptional(options.named?.[flag]);
266
+ if (value !== undefined) {
267
+ payload[param] = normalizeValue(value);
268
+ }
269
+ }
270
+ for (const [key, value] of Object.entries(parseExtra(options.named?.extra))) {
271
+ const cleaned = cleanOptional(value);
272
+ if (cleaned !== undefined) {
273
+ payload[key] = normalizeValue(cleaned);
274
+ }
275
+ }
276
+ return payload;
277
+ }
278
+
279
+ function validateRequiredOptions(endpoint, options) {
280
+ const missing = (endpoint.requiredOptions || []).filter((flag) => cleanOptional(options.named?.[flag]) === undefined);
281
+ if (missing.length > 0) {
282
+ throw new Error(`缺少必要参数:${missing.map((item) => `--${item}`).join(', ')}。`);
283
+ }
284
+ }
285
+
286
+ function endpointSummary(endpoint, kind = '') {
287
+ return {
288
+ method: endpoint.method,
289
+ path: endpoint.path,
290
+ description: endpoint.description,
291
+ ...(kind ? { kind } : {}),
292
+ };
293
+ }
294
+
295
+ function normalizeWorkPlatform(value) {
296
+ const normalized = normalizePlatform(value);
297
+ if (['wechat', 'channels', 'wechat_channels', '微信视频号', '视频号'].includes(normalized)) {
298
+ return 'wechat_channels';
299
+ }
300
+ if (['mp', 'wechat_mp', '微信公众号', '公众号'].includes(normalized)) {
301
+ return 'wechat_mp';
302
+ }
303
+ return normalized;
304
+ }
305
+
306
+ function looksLikeUrl(value) {
307
+ return /^https?:\/\//i.test(String(value || '').trim());
308
+ }
309
+
310
+ function cleanOptional(value) {
311
+ if (value === undefined || value === null) {
312
+ return undefined;
313
+ }
314
+ if (typeof value === 'string') {
315
+ const trimmed = value.trim();
316
+ return trimmed ? trimmed : undefined;
317
+ }
318
+ return value;
319
+ }
320
+
321
+ function normalizeValue(value) {
322
+ if (typeof value !== 'string') {
323
+ return value;
324
+ }
325
+ if (/^-?\d+$/.test(value)) {
326
+ return Number(value);
327
+ }
328
+ return value;
329
+ }
330
+
331
+ function parseExtra(value) {
332
+ if (!value) {
333
+ return {};
334
+ }
335
+ if (typeof value === 'object') {
336
+ return value;
337
+ }
338
+ try {
339
+ const parsed = JSON.parse(String(value));
340
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
341
+ } catch {
342
+ return {};
343
+ }
344
+ }