felo-ai 0.2.6 → 0.2.9
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/.github/workflows/publish-npm.yml +39 -0
- package/CHANGELOG.md +30 -0
- package/CONTRIBUTING.md +346 -346
- package/README.en.md +129 -129
- package/README.md +435 -408
- package/docs/EXAMPLES.md +632 -632
- package/docs/FAQ.md +479 -479
- package/felo-search/LICENSE +21 -21
- package/felo-search/README.md +440 -440
- package/felo-search/SKILL.md +291 -291
- package/felo-slides/LICENSE +21 -21
- package/felo-slides/README.md +87 -87
- package/felo-slides/SKILL.md +166 -166
- package/felo-slides/scripts/run_ppt_task.mjs +251 -251
- package/felo-superAgent/LICENSE +21 -0
- package/felo-superAgent/README.md +125 -0
- package/felo-superAgent/SKILL.md +165 -0
- package/felo-web-fetch/README.md +127 -0
- package/felo-web-fetch/SKILL.md +204 -0
- package/felo-web-fetch/scripts/run_web_fetch.mjs +316 -0
- package/felo-x-search/SKILL.md +204 -0
- package/felo-x-search/scripts/run_x_search.mjs +385 -0
- package/felo-youtube-subtitling/README.md +59 -59
- package/felo-youtube-subtitling/SKILL.md +161 -161
- package/felo-youtube-subtitling/scripts/run_youtube_subtitling.mjs +239 -239
- package/package.json +37 -35
- package/src/cli.js +370 -252
- package/src/config.js +66 -66
- package/src/search.js +142 -142
- package/src/slides.js +332 -332
- package/src/superAgent.js +609 -0
- package/src/{webExtract.js → webFetch.js} +148 -148
- package/src/xSearch.js +366 -0
- package/src/youtubeSubtitling.js +179 -179
- package/tests/config.test.js +78 -78
- package/tests/search.test.js +100 -100
- package/felo-web-extract/README.md +0 -78
- package/felo-web-extract/SKILL.md +0 -200
- package/felo-web-extract/scripts/run_web_extract.mjs +0 -232
package/src/slides.js
CHANGED
|
@@ -1,332 +1,332 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getApiKey,
|
|
3
|
-
fetchWithTimeoutAndRetry,
|
|
4
|
-
NO_KEY_MESSAGE,
|
|
5
|
-
} from "./search.js";
|
|
6
|
-
|
|
7
|
-
const DEFAULT_API_BASE = "https://openapi.felo.ai";
|
|
8
|
-
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
|
|
9
|
-
const POLL_INTERVAL_MS = 10_000;
|
|
10
|
-
const MAX_POLL_TIMEOUT_MS = 1_200_000; // 20 minutes max wait
|
|
11
|
-
|
|
12
|
-
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
13
|
-
const STATUS_LINE_PAD = 50;
|
|
14
|
-
|
|
15
|
-
/** API base URL (default https://openapi.felo.ai). Override via FELO_API_BASE env or config if needed. */
|
|
16
|
-
async function getApiBase() {
|
|
17
|
-
let base = process.env.FELO_API_BASE?.trim();
|
|
18
|
-
if (!base) {
|
|
19
|
-
const { getConfigValue } = await import("./config.js");
|
|
20
|
-
const v = await getConfigValue("FELO_API_BASE");
|
|
21
|
-
base = typeof v === "string" ? v.trim() : "";
|
|
22
|
-
}
|
|
23
|
-
const normalized = (base || DEFAULT_API_BASE).replace(/\/$/, "");
|
|
24
|
-
return normalized;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function sleep(ms) {
|
|
28
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Write status line: spinner + elapsed. When overwrite is true use \\r (TTY); else use \\n so non-TTY still shows animation. */
|
|
32
|
-
function writeStatusLine(spinnerFrame, elapsedSec, overwrite = true) {
|
|
33
|
-
const s = `Generating slides... ${spinnerFrame} ${elapsedSec}s`;
|
|
34
|
-
const padded = s.padEnd(STATUS_LINE_PAD, " ");
|
|
35
|
-
process.stderr.write(overwrite ? `\r${padded}` : `${padded}\n`);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function clearStatusLine() {
|
|
39
|
-
process.stderr.write(`\r${" ".repeat(STATUS_LINE_PAD)}\r`);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function normalizeTaskStatus(status) {
|
|
43
|
-
return String(status || "")
|
|
44
|
-
.trim()
|
|
45
|
-
.toUpperCase();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Create a PPT task. Returns { task_id, livedoc_short_id, ppt_business_id } or throws.
|
|
50
|
-
* Uses fetchWithTimeoutAndRetry for 5xx retry (per PPT Task API error codes).
|
|
51
|
-
*/
|
|
52
|
-
async function createPptTask(apiKey, query, timeoutMs, apiBase) {
|
|
53
|
-
const url = `${apiBase}/v2/ppts`;
|
|
54
|
-
const res = await fetchWithTimeoutAndRetry(
|
|
55
|
-
url,
|
|
56
|
-
{
|
|
57
|
-
method: "POST",
|
|
58
|
-
headers: {
|
|
59
|
-
Accept: "application/json",
|
|
60
|
-
Authorization: `Bearer ${apiKey}`,
|
|
61
|
-
"Content-Type": "application/json",
|
|
62
|
-
},
|
|
63
|
-
body: JSON.stringify({ query: query.trim() }),
|
|
64
|
-
},
|
|
65
|
-
timeoutMs
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
const data = await res.json().catch(() => ({}));
|
|
69
|
-
|
|
70
|
-
if (data.status === "error") {
|
|
71
|
-
const msg = data.message || data.code || "Unknown error";
|
|
72
|
-
throw new Error(msg);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (!res.ok) {
|
|
76
|
-
const msg =
|
|
77
|
-
data.message || data.error || res.statusText || `HTTP ${res.status}`;
|
|
78
|
-
throw new Error(msg);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const payload = data.data;
|
|
82
|
-
if (!payload || !payload.task_id) {
|
|
83
|
-
throw new Error("Unexpected response: missing task_id");
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return payload;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Get task historical info. Returns { task_status, ppt_url?, ppt_biz_id?, live_doc_url?, live_doc_short_id? } or throws.
|
|
91
|
-
* Uses fetchWithTimeoutAndRetry for 5xx retry (per PPT Task API error codes).
|
|
92
|
-
*/
|
|
93
|
-
async function getTaskHistorical(apiKey, taskId, timeoutMs, apiBase) {
|
|
94
|
-
const url = `${apiBase}/v2/tasks/${encodeURIComponent(taskId)}/historical`;
|
|
95
|
-
const res = await fetchWithTimeoutAndRetry(
|
|
96
|
-
url,
|
|
97
|
-
{
|
|
98
|
-
method: "GET",
|
|
99
|
-
headers: {
|
|
100
|
-
Accept: "application/json",
|
|
101
|
-
Authorization: `Bearer ${apiKey}`,
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
timeoutMs
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
const data = await res.json().catch(() => ({}));
|
|
108
|
-
|
|
109
|
-
if (data.status === "error") {
|
|
110
|
-
const msg = data.message || data.code || "Unknown error";
|
|
111
|
-
throw new Error(msg);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (!res.ok) {
|
|
115
|
-
const msg =
|
|
116
|
-
data.message || data.error || res.statusText || `HTTP ${res.status}`;
|
|
117
|
-
throw new Error(msg);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const payload = data.data;
|
|
121
|
-
if (!payload) {
|
|
122
|
-
throw new Error("Unexpected response: missing data");
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return {
|
|
126
|
-
task_status: payload.task_status ?? payload.status,
|
|
127
|
-
ppt_url: payload.ppt_url,
|
|
128
|
-
ppt_biz_id: payload.ppt_biz_id,
|
|
129
|
-
live_doc_url: payload.live_doc_url,
|
|
130
|
-
live_doc_short_id: payload.live_doc_short_id ?? payload.livedoc_short_id,
|
|
131
|
-
error_message: payload.error_message,
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Run slides: create PPT task, poll until terminal state, output URL or error.
|
|
137
|
-
* @returns {Promise<number>} exit code (0 success, 1 failure)
|
|
138
|
-
*/
|
|
139
|
-
export async function slides(query, options = {}) {
|
|
140
|
-
const apiKey = await getApiKey();
|
|
141
|
-
if (!apiKey) {
|
|
142
|
-
console.error(NO_KEY_MESSAGE.trim());
|
|
143
|
-
return 1;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const requestTimeoutMs = options.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
147
|
-
const pollTimeoutMs = options.pollTimeoutMs ?? MAX_POLL_TIMEOUT_MS;
|
|
148
|
-
const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
149
|
-
|
|
150
|
-
try {
|
|
151
|
-
const apiBase = await getApiBase();
|
|
152
|
-
|
|
153
|
-
process.stderr.write("Creating PPT task...\n");
|
|
154
|
-
|
|
155
|
-
const createResult = await createPptTask(
|
|
156
|
-
apiKey,
|
|
157
|
-
query,
|
|
158
|
-
requestTimeoutMs,
|
|
159
|
-
apiBase
|
|
160
|
-
);
|
|
161
|
-
const taskId = createResult.task_id;
|
|
162
|
-
|
|
163
|
-
if (options.json && options.verbose) {
|
|
164
|
-
process.stderr.write(`Task ID: ${taskId}\n`);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// 默认显示 spinner 动画;仅在使用 -v/--json 时改为逐行状态输出
|
|
168
|
-
const useLiveStatus = !options.verbose && !options.json;
|
|
169
|
-
const canOverwriteLine = process.stderr.isTTY && useLiveStatus;
|
|
170
|
-
if (!useLiveStatus) {
|
|
171
|
-
process.stderr.write("Generating slides (this may take a minute)...\n");
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const startTime = Date.now();
|
|
175
|
-
let lastStatus;
|
|
176
|
-
let spinIndex = 0;
|
|
177
|
-
if (useLiveStatus) writeStatusLine(SPINNER_FRAMES[0], 0, canOverwriteLine);
|
|
178
|
-
|
|
179
|
-
while (Date.now() - startTime < pollTimeoutMs) {
|
|
180
|
-
await sleep(pollIntervalMs);
|
|
181
|
-
|
|
182
|
-
const historical = await getTaskHistorical(
|
|
183
|
-
apiKey,
|
|
184
|
-
taskId,
|
|
185
|
-
requestTimeoutMs,
|
|
186
|
-
apiBase
|
|
187
|
-
);
|
|
188
|
-
const normalizedStatus = normalizeTaskStatus(historical.task_status);
|
|
189
|
-
lastStatus = normalizedStatus || historical.task_status;
|
|
190
|
-
|
|
191
|
-
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
192
|
-
if (useLiveStatus) {
|
|
193
|
-
spinIndex = (spinIndex + 1) % SPINNER_FRAMES.length;
|
|
194
|
-
writeStatusLine(SPINNER_FRAMES[spinIndex], elapsed, canOverwriteLine);
|
|
195
|
-
} else if (!options.verbose && !options.json) {
|
|
196
|
-
process.stderr.write(` Generating... ${elapsed}s\n`);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const done =
|
|
200
|
-
normalizedStatus === "COMPLETED" || normalizedStatus === "SUCCESS";
|
|
201
|
-
if (done) {
|
|
202
|
-
if (useLiveStatus && canOverwriteLine) clearStatusLine();
|
|
203
|
-
const pptUrl =
|
|
204
|
-
historical.ppt_url ||
|
|
205
|
-
(historical.ppt_biz_id
|
|
206
|
-
? `https://dev.felo.ai/slides/${historical.ppt_biz_id}`
|
|
207
|
-
: null);
|
|
208
|
-
const liveDocUrl =
|
|
209
|
-
historical.live_doc_url ||
|
|
210
|
-
(historical.live_doc_short_id
|
|
211
|
-
? `https://felo.ai/livedoc/${historical.live_doc_short_id}`
|
|
212
|
-
: null) ||
|
|
213
|
-
(createResult.livedoc_short_id
|
|
214
|
-
? `https://felo.ai/livedoc/${createResult.livedoc_short_id}`
|
|
215
|
-
: null);
|
|
216
|
-
const url = pptUrl || liveDocUrl;
|
|
217
|
-
if (options.json) {
|
|
218
|
-
console.log(
|
|
219
|
-
JSON.stringify(
|
|
220
|
-
{
|
|
221
|
-
status: "ok",
|
|
222
|
-
data: {
|
|
223
|
-
task_id: taskId,
|
|
224
|
-
task_status: normalizedStatus || historical.task_status,
|
|
225
|
-
ppt_url: pptUrl,
|
|
226
|
-
ppt_biz_id:
|
|
227
|
-
historical.ppt_biz_id ?? createResult.ppt_business_id,
|
|
228
|
-
live_doc_url: liveDocUrl,
|
|
229
|
-
livedoc_short_id:
|
|
230
|
-
historical.live_doc_short_id ??
|
|
231
|
-
createResult.livedoc_short_id,
|
|
232
|
-
ppt_business_id: createResult.ppt_business_id,
|
|
233
|
-
},
|
|
234
|
-
},
|
|
235
|
-
null,
|
|
236
|
-
2
|
|
237
|
-
)
|
|
238
|
-
);
|
|
239
|
-
} else {
|
|
240
|
-
if (url) {
|
|
241
|
-
if (pptUrl && !options.json) {
|
|
242
|
-
process.stderr.write("PPT ready. Open this link to preview:\n");
|
|
243
|
-
}
|
|
244
|
-
console.log(pptUrl || liveDocUrl);
|
|
245
|
-
} else {
|
|
246
|
-
if (useLiveStatus) clearStatusLine();
|
|
247
|
-
console.error(
|
|
248
|
-
"Error: Completed but no ppt_url or live_doc_url in response"
|
|
249
|
-
);
|
|
250
|
-
return 1;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
return 0;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const terminalErrorStatuses = [
|
|
257
|
-
"FAILED",
|
|
258
|
-
"PENDING",
|
|
259
|
-
"EXPIRED",
|
|
260
|
-
"CANCELED",
|
|
261
|
-
];
|
|
262
|
-
|
|
263
|
-
if (terminalErrorStatuses.includes(normalizedStatus)) {
|
|
264
|
-
if (useLiveStatus && canOverwriteLine) clearStatusLine();
|
|
265
|
-
|
|
266
|
-
const liveDocUrl =
|
|
267
|
-
historical.live_doc_url ||
|
|
268
|
-
(historical.live_doc_short_id
|
|
269
|
-
? `https://felo.ai/livedoc/${historical.live_doc_short_id}`
|
|
270
|
-
: null) ||
|
|
271
|
-
(createResult.livedoc_short_id
|
|
272
|
-
? `https://felo.ai/livedoc/${createResult.livedoc_short_id}`
|
|
273
|
-
: null);
|
|
274
|
-
|
|
275
|
-
const errorMessage =
|
|
276
|
-
historical.error_message ||
|
|
277
|
-
`Task finished with status: ${
|
|
278
|
-
normalizedStatus || historical.task_status
|
|
279
|
-
}`;
|
|
280
|
-
|
|
281
|
-
if (options.json) {
|
|
282
|
-
console.log(
|
|
283
|
-
JSON.stringify(
|
|
284
|
-
{
|
|
285
|
-
status: "error",
|
|
286
|
-
data: {
|
|
287
|
-
task_id: taskId,
|
|
288
|
-
task_status: normalizedStatus || historical.task_status,
|
|
289
|
-
live_doc_url: liveDocUrl,
|
|
290
|
-
livedoc_short_id:
|
|
291
|
-
historical.live_doc_short_id ??
|
|
292
|
-
createResult.livedoc_short_id,
|
|
293
|
-
error_message: errorMessage,
|
|
294
|
-
},
|
|
295
|
-
},
|
|
296
|
-
null,
|
|
297
|
-
2
|
|
298
|
-
)
|
|
299
|
-
);
|
|
300
|
-
} else {
|
|
301
|
-
if (liveDocUrl) {
|
|
302
|
-
console.log(liveDocUrl);
|
|
303
|
-
}
|
|
304
|
-
console.error(errorMessage);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
return 1;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
if (options.verbose) {
|
|
311
|
-
process.stderr.write(
|
|
312
|
-
` Status: ${
|
|
313
|
-
normalizedStatus || historical.task_status || "UNKNOWN"
|
|
314
|
-
}\n`
|
|
315
|
-
);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
if (useLiveStatus && canOverwriteLine) clearStatusLine();
|
|
320
|
-
console.error(
|
|
321
|
-
`Error: Timed out after ${pollTimeoutMs / 1000}s. Last status: ${
|
|
322
|
-
lastStatus ?? "unknown"
|
|
323
|
-
}`
|
|
324
|
-
);
|
|
325
|
-
return 1;
|
|
326
|
-
} catch (err) {
|
|
327
|
-
if (process.stderr.isTTY && !options.verbose && !options.json)
|
|
328
|
-
clearStatusLine();
|
|
329
|
-
console.error("Error:", err.message || err);
|
|
330
|
-
return 1;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
getApiKey,
|
|
3
|
+
fetchWithTimeoutAndRetry,
|
|
4
|
+
NO_KEY_MESSAGE,
|
|
5
|
+
} from "./search.js";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_API_BASE = "https://openapi.felo.ai";
|
|
8
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
|
|
9
|
+
const POLL_INTERVAL_MS = 10_000;
|
|
10
|
+
const MAX_POLL_TIMEOUT_MS = 1_200_000; // 20 minutes max wait
|
|
11
|
+
|
|
12
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
13
|
+
const STATUS_LINE_PAD = 50;
|
|
14
|
+
|
|
15
|
+
/** API base URL (default https://openapi.felo.ai). Override via FELO_API_BASE env or config if needed. */
|
|
16
|
+
async function getApiBase() {
|
|
17
|
+
let base = process.env.FELO_API_BASE?.trim();
|
|
18
|
+
if (!base) {
|
|
19
|
+
const { getConfigValue } = await import("./config.js");
|
|
20
|
+
const v = await getConfigValue("FELO_API_BASE");
|
|
21
|
+
base = typeof v === "string" ? v.trim() : "";
|
|
22
|
+
}
|
|
23
|
+
const normalized = (base || DEFAULT_API_BASE).replace(/\/$/, "");
|
|
24
|
+
return normalized;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function sleep(ms) {
|
|
28
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Write status line: spinner + elapsed. When overwrite is true use \\r (TTY); else use \\n so non-TTY still shows animation. */
|
|
32
|
+
function writeStatusLine(spinnerFrame, elapsedSec, overwrite = true) {
|
|
33
|
+
const s = `Generating slides... ${spinnerFrame} ${elapsedSec}s`;
|
|
34
|
+
const padded = s.padEnd(STATUS_LINE_PAD, " ");
|
|
35
|
+
process.stderr.write(overwrite ? `\r${padded}` : `${padded}\n`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function clearStatusLine() {
|
|
39
|
+
process.stderr.write(`\r${" ".repeat(STATUS_LINE_PAD)}\r`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeTaskStatus(status) {
|
|
43
|
+
return String(status || "")
|
|
44
|
+
.trim()
|
|
45
|
+
.toUpperCase();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create a PPT task. Returns { task_id, livedoc_short_id, ppt_business_id } or throws.
|
|
50
|
+
* Uses fetchWithTimeoutAndRetry for 5xx retry (per PPT Task API error codes).
|
|
51
|
+
*/
|
|
52
|
+
async function createPptTask(apiKey, query, timeoutMs, apiBase) {
|
|
53
|
+
const url = `${apiBase}/v2/ppts`;
|
|
54
|
+
const res = await fetchWithTimeoutAndRetry(
|
|
55
|
+
url,
|
|
56
|
+
{
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: {
|
|
59
|
+
Accept: "application/json",
|
|
60
|
+
Authorization: `Bearer ${apiKey}`,
|
|
61
|
+
"Content-Type": "application/json",
|
|
62
|
+
},
|
|
63
|
+
body: JSON.stringify({ query: query.trim() }),
|
|
64
|
+
},
|
|
65
|
+
timeoutMs
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const data = await res.json().catch(() => ({}));
|
|
69
|
+
|
|
70
|
+
if (data.status === "error") {
|
|
71
|
+
const msg = data.message || data.code || "Unknown error";
|
|
72
|
+
throw new Error(msg);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!res.ok) {
|
|
76
|
+
const msg =
|
|
77
|
+
data.message || data.error || res.statusText || `HTTP ${res.status}`;
|
|
78
|
+
throw new Error(msg);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const payload = data.data;
|
|
82
|
+
if (!payload || !payload.task_id) {
|
|
83
|
+
throw new Error("Unexpected response: missing task_id");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return payload;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get task historical info. Returns { task_status, ppt_url?, ppt_biz_id?, live_doc_url?, live_doc_short_id? } or throws.
|
|
91
|
+
* Uses fetchWithTimeoutAndRetry for 5xx retry (per PPT Task API error codes).
|
|
92
|
+
*/
|
|
93
|
+
async function getTaskHistorical(apiKey, taskId, timeoutMs, apiBase) {
|
|
94
|
+
const url = `${apiBase}/v2/tasks/${encodeURIComponent(taskId)}/historical`;
|
|
95
|
+
const res = await fetchWithTimeoutAndRetry(
|
|
96
|
+
url,
|
|
97
|
+
{
|
|
98
|
+
method: "GET",
|
|
99
|
+
headers: {
|
|
100
|
+
Accept: "application/json",
|
|
101
|
+
Authorization: `Bearer ${apiKey}`,
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
timeoutMs
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const data = await res.json().catch(() => ({}));
|
|
108
|
+
|
|
109
|
+
if (data.status === "error") {
|
|
110
|
+
const msg = data.message || data.code || "Unknown error";
|
|
111
|
+
throw new Error(msg);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!res.ok) {
|
|
115
|
+
const msg =
|
|
116
|
+
data.message || data.error || res.statusText || `HTTP ${res.status}`;
|
|
117
|
+
throw new Error(msg);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const payload = data.data;
|
|
121
|
+
if (!payload) {
|
|
122
|
+
throw new Error("Unexpected response: missing data");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
task_status: payload.task_status ?? payload.status,
|
|
127
|
+
ppt_url: payload.ppt_url,
|
|
128
|
+
ppt_biz_id: payload.ppt_biz_id,
|
|
129
|
+
live_doc_url: payload.live_doc_url,
|
|
130
|
+
live_doc_short_id: payload.live_doc_short_id ?? payload.livedoc_short_id,
|
|
131
|
+
error_message: payload.error_message,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Run slides: create PPT task, poll until terminal state, output URL or error.
|
|
137
|
+
* @returns {Promise<number>} exit code (0 success, 1 failure)
|
|
138
|
+
*/
|
|
139
|
+
export async function slides(query, options = {}) {
|
|
140
|
+
const apiKey = await getApiKey();
|
|
141
|
+
if (!apiKey) {
|
|
142
|
+
console.error(NO_KEY_MESSAGE.trim());
|
|
143
|
+
return 1;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const requestTimeoutMs = options.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
147
|
+
const pollTimeoutMs = options.pollTimeoutMs ?? MAX_POLL_TIMEOUT_MS;
|
|
148
|
+
const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const apiBase = await getApiBase();
|
|
152
|
+
|
|
153
|
+
process.stderr.write("Creating PPT task...\n");
|
|
154
|
+
|
|
155
|
+
const createResult = await createPptTask(
|
|
156
|
+
apiKey,
|
|
157
|
+
query,
|
|
158
|
+
requestTimeoutMs,
|
|
159
|
+
apiBase
|
|
160
|
+
);
|
|
161
|
+
const taskId = createResult.task_id;
|
|
162
|
+
|
|
163
|
+
if (options.json && options.verbose) {
|
|
164
|
+
process.stderr.write(`Task ID: ${taskId}\n`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 默认显示 spinner 动画;仅在使用 -v/--json 时改为逐行状态输出
|
|
168
|
+
const useLiveStatus = !options.verbose && !options.json;
|
|
169
|
+
const canOverwriteLine = process.stderr.isTTY && useLiveStatus;
|
|
170
|
+
if (!useLiveStatus) {
|
|
171
|
+
process.stderr.write("Generating slides (this may take a minute)...\n");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const startTime = Date.now();
|
|
175
|
+
let lastStatus;
|
|
176
|
+
let spinIndex = 0;
|
|
177
|
+
if (useLiveStatus) writeStatusLine(SPINNER_FRAMES[0], 0, canOverwriteLine);
|
|
178
|
+
|
|
179
|
+
while (Date.now() - startTime < pollTimeoutMs) {
|
|
180
|
+
await sleep(pollIntervalMs);
|
|
181
|
+
|
|
182
|
+
const historical = await getTaskHistorical(
|
|
183
|
+
apiKey,
|
|
184
|
+
taskId,
|
|
185
|
+
requestTimeoutMs,
|
|
186
|
+
apiBase
|
|
187
|
+
);
|
|
188
|
+
const normalizedStatus = normalizeTaskStatus(historical.task_status);
|
|
189
|
+
lastStatus = normalizedStatus || historical.task_status;
|
|
190
|
+
|
|
191
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
192
|
+
if (useLiveStatus) {
|
|
193
|
+
spinIndex = (spinIndex + 1) % SPINNER_FRAMES.length;
|
|
194
|
+
writeStatusLine(SPINNER_FRAMES[spinIndex], elapsed, canOverwriteLine);
|
|
195
|
+
} else if (!options.verbose && !options.json) {
|
|
196
|
+
process.stderr.write(` Generating... ${elapsed}s\n`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const done =
|
|
200
|
+
normalizedStatus === "COMPLETED" || normalizedStatus === "SUCCESS";
|
|
201
|
+
if (done) {
|
|
202
|
+
if (useLiveStatus && canOverwriteLine) clearStatusLine();
|
|
203
|
+
const pptUrl =
|
|
204
|
+
historical.ppt_url ||
|
|
205
|
+
(historical.ppt_biz_id
|
|
206
|
+
? `https://dev.felo.ai/slides/${historical.ppt_biz_id}`
|
|
207
|
+
: null);
|
|
208
|
+
const liveDocUrl =
|
|
209
|
+
historical.live_doc_url ||
|
|
210
|
+
(historical.live_doc_short_id
|
|
211
|
+
? `https://felo.ai/livedoc/${historical.live_doc_short_id}`
|
|
212
|
+
: null) ||
|
|
213
|
+
(createResult.livedoc_short_id
|
|
214
|
+
? `https://felo.ai/livedoc/${createResult.livedoc_short_id}`
|
|
215
|
+
: null);
|
|
216
|
+
const url = pptUrl || liveDocUrl;
|
|
217
|
+
if (options.json) {
|
|
218
|
+
console.log(
|
|
219
|
+
JSON.stringify(
|
|
220
|
+
{
|
|
221
|
+
status: "ok",
|
|
222
|
+
data: {
|
|
223
|
+
task_id: taskId,
|
|
224
|
+
task_status: normalizedStatus || historical.task_status,
|
|
225
|
+
ppt_url: pptUrl,
|
|
226
|
+
ppt_biz_id:
|
|
227
|
+
historical.ppt_biz_id ?? createResult.ppt_business_id,
|
|
228
|
+
live_doc_url: liveDocUrl,
|
|
229
|
+
livedoc_short_id:
|
|
230
|
+
historical.live_doc_short_id ??
|
|
231
|
+
createResult.livedoc_short_id,
|
|
232
|
+
ppt_business_id: createResult.ppt_business_id,
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
null,
|
|
236
|
+
2
|
|
237
|
+
)
|
|
238
|
+
);
|
|
239
|
+
} else {
|
|
240
|
+
if (url) {
|
|
241
|
+
if (pptUrl && !options.json) {
|
|
242
|
+
process.stderr.write("PPT ready. Open this link to preview:\n");
|
|
243
|
+
}
|
|
244
|
+
console.log(pptUrl || liveDocUrl);
|
|
245
|
+
} else {
|
|
246
|
+
if (useLiveStatus) clearStatusLine();
|
|
247
|
+
console.error(
|
|
248
|
+
"Error: Completed but no ppt_url or live_doc_url in response"
|
|
249
|
+
);
|
|
250
|
+
return 1;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return 0;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const terminalErrorStatuses = [
|
|
257
|
+
"FAILED",
|
|
258
|
+
"PENDING",
|
|
259
|
+
"EXPIRED",
|
|
260
|
+
"CANCELED",
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
if (terminalErrorStatuses.includes(normalizedStatus)) {
|
|
264
|
+
if (useLiveStatus && canOverwriteLine) clearStatusLine();
|
|
265
|
+
|
|
266
|
+
const liveDocUrl =
|
|
267
|
+
historical.live_doc_url ||
|
|
268
|
+
(historical.live_doc_short_id
|
|
269
|
+
? `https://felo.ai/livedoc/${historical.live_doc_short_id}`
|
|
270
|
+
: null) ||
|
|
271
|
+
(createResult.livedoc_short_id
|
|
272
|
+
? `https://felo.ai/livedoc/${createResult.livedoc_short_id}`
|
|
273
|
+
: null);
|
|
274
|
+
|
|
275
|
+
const errorMessage =
|
|
276
|
+
historical.error_message ||
|
|
277
|
+
`Task finished with status: ${
|
|
278
|
+
normalizedStatus || historical.task_status
|
|
279
|
+
}`;
|
|
280
|
+
|
|
281
|
+
if (options.json) {
|
|
282
|
+
console.log(
|
|
283
|
+
JSON.stringify(
|
|
284
|
+
{
|
|
285
|
+
status: "error",
|
|
286
|
+
data: {
|
|
287
|
+
task_id: taskId,
|
|
288
|
+
task_status: normalizedStatus || historical.task_status,
|
|
289
|
+
live_doc_url: liveDocUrl,
|
|
290
|
+
livedoc_short_id:
|
|
291
|
+
historical.live_doc_short_id ??
|
|
292
|
+
createResult.livedoc_short_id,
|
|
293
|
+
error_message: errorMessage,
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
null,
|
|
297
|
+
2
|
|
298
|
+
)
|
|
299
|
+
);
|
|
300
|
+
} else {
|
|
301
|
+
if (liveDocUrl) {
|
|
302
|
+
console.log(liveDocUrl);
|
|
303
|
+
}
|
|
304
|
+
console.error(errorMessage);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return 1;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (options.verbose) {
|
|
311
|
+
process.stderr.write(
|
|
312
|
+
` Status: ${
|
|
313
|
+
normalizedStatus || historical.task_status || "UNKNOWN"
|
|
314
|
+
}\n`
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (useLiveStatus && canOverwriteLine) clearStatusLine();
|
|
320
|
+
console.error(
|
|
321
|
+
`Error: Timed out after ${pollTimeoutMs / 1000}s. Last status: ${
|
|
322
|
+
lastStatus ?? "unknown"
|
|
323
|
+
}`
|
|
324
|
+
);
|
|
325
|
+
return 1;
|
|
326
|
+
} catch (err) {
|
|
327
|
+
if (process.stderr.isTTY && !options.verbose && !options.json)
|
|
328
|
+
clearStatusLine();
|
|
329
|
+
console.error("Error:", err.message || err);
|
|
330
|
+
return 1;
|
|
331
|
+
}
|
|
332
|
+
}
|