felo-ai 0.2.4 → 0.2.5
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/package.json +1 -1
- package/src/cli.js +1 -1
- package/src/slides.js +141 -57
package/package.json
CHANGED
package/src/cli.js
CHANGED
package/src/slides.js
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
getApiKey,
|
|
3
|
+
fetchWithTimeoutAndRetry,
|
|
4
|
+
NO_KEY_MESSAGE,
|
|
5
|
+
} from "./search.js";
|
|
2
6
|
|
|
3
|
-
const DEFAULT_API_BASE =
|
|
7
|
+
const DEFAULT_API_BASE = "https://openapi.felo.ai";
|
|
4
8
|
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
|
|
5
9
|
const POLL_INTERVAL_MS = 10_000;
|
|
6
10
|
const MAX_POLL_TIMEOUT_MS = 1_200_000; // 20 minutes max wait
|
|
7
11
|
|
|
8
|
-
const SPINNER_FRAMES = [
|
|
12
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
9
13
|
const STATUS_LINE_PAD = 50;
|
|
10
14
|
|
|
11
15
|
/** API base URL (default https://openapi.felo.ai). Override via FELO_API_BASE env or config if needed. */
|
|
12
16
|
async function getApiBase() {
|
|
13
17
|
let base = process.env.FELO_API_BASE?.trim();
|
|
14
18
|
if (!base) {
|
|
15
|
-
const { getConfigValue } = await import(
|
|
16
|
-
const v = await getConfigValue(
|
|
17
|
-
base = typeof v ===
|
|
19
|
+
const { getConfigValue } = await import("./config.js");
|
|
20
|
+
const v = await getConfigValue("FELO_API_BASE");
|
|
21
|
+
base = typeof v === "string" ? v.trim() : "";
|
|
18
22
|
}
|
|
19
|
-
const normalized = (base || DEFAULT_API_BASE).replace(/\/$/,
|
|
23
|
+
const normalized = (base || DEFAULT_API_BASE).replace(/\/$/, "");
|
|
20
24
|
return normalized;
|
|
21
25
|
}
|
|
22
26
|
|
|
@@ -24,19 +28,21 @@ function sleep(ms) {
|
|
|
24
28
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
25
29
|
}
|
|
26
30
|
|
|
27
|
-
/** Write
|
|
28
|
-
function writeStatusLine(spinnerFrame, elapsedSec) {
|
|
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) {
|
|
29
33
|
const s = `Generating slides... ${spinnerFrame} ${elapsedSec}s`;
|
|
30
|
-
const padded = s.padEnd(STATUS_LINE_PAD,
|
|
31
|
-
process.stderr.write(`\r${padded}`);
|
|
34
|
+
const padded = s.padEnd(STATUS_LINE_PAD, " ");
|
|
35
|
+
process.stderr.write(overwrite ? `\r${padded}` : `${padded}\n`);
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
function clearStatusLine() {
|
|
35
|
-
process.stderr.write(`\r${
|
|
39
|
+
process.stderr.write(`\r${" ".repeat(STATUS_LINE_PAD)}\r`);
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
function normalizeTaskStatus(status) {
|
|
39
|
-
return String(status ||
|
|
43
|
+
return String(status || "")
|
|
44
|
+
.trim()
|
|
45
|
+
.toUpperCase();
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
/**
|
|
@@ -48,11 +54,11 @@ async function createPptTask(apiKey, query, timeoutMs, apiBase) {
|
|
|
48
54
|
const res = await fetchWithTimeoutAndRetry(
|
|
49
55
|
url,
|
|
50
56
|
{
|
|
51
|
-
method:
|
|
57
|
+
method: "POST",
|
|
52
58
|
headers: {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
59
|
+
Accept: "application/json",
|
|
60
|
+
Authorization: `Bearer ${apiKey}`,
|
|
61
|
+
"Content-Type": "application/json",
|
|
56
62
|
},
|
|
57
63
|
body: JSON.stringify({ query: query.trim() }),
|
|
58
64
|
},
|
|
@@ -61,19 +67,20 @@ async function createPptTask(apiKey, query, timeoutMs, apiBase) {
|
|
|
61
67
|
|
|
62
68
|
const data = await res.json().catch(() => ({}));
|
|
63
69
|
|
|
64
|
-
if (data.status ===
|
|
65
|
-
const msg = data.message || data.code ||
|
|
70
|
+
if (data.status === "error") {
|
|
71
|
+
const msg = data.message || data.code || "Unknown error";
|
|
66
72
|
throw new Error(msg);
|
|
67
73
|
}
|
|
68
74
|
|
|
69
75
|
if (!res.ok) {
|
|
70
|
-
const msg =
|
|
76
|
+
const msg =
|
|
77
|
+
data.message || data.error || res.statusText || `HTTP ${res.status}`;
|
|
71
78
|
throw new Error(msg);
|
|
72
79
|
}
|
|
73
80
|
|
|
74
81
|
const payload = data.data;
|
|
75
82
|
if (!payload || !payload.task_id) {
|
|
76
|
-
throw new Error(
|
|
83
|
+
throw new Error("Unexpected response: missing task_id");
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
return payload;
|
|
@@ -88,10 +95,10 @@ async function getTaskHistorical(apiKey, taskId, timeoutMs, apiBase) {
|
|
|
88
95
|
const res = await fetchWithTimeoutAndRetry(
|
|
89
96
|
url,
|
|
90
97
|
{
|
|
91
|
-
method:
|
|
98
|
+
method: "GET",
|
|
92
99
|
headers: {
|
|
93
|
-
|
|
94
|
-
|
|
100
|
+
Accept: "application/json",
|
|
101
|
+
Authorization: `Bearer ${apiKey}`,
|
|
95
102
|
},
|
|
96
103
|
},
|
|
97
104
|
timeoutMs
|
|
@@ -99,19 +106,20 @@ async function getTaskHistorical(apiKey, taskId, timeoutMs, apiBase) {
|
|
|
99
106
|
|
|
100
107
|
const data = await res.json().catch(() => ({}));
|
|
101
108
|
|
|
102
|
-
if (data.status ===
|
|
103
|
-
const msg = data.message || data.code ||
|
|
109
|
+
if (data.status === "error") {
|
|
110
|
+
const msg = data.message || data.code || "Unknown error";
|
|
104
111
|
throw new Error(msg);
|
|
105
112
|
}
|
|
106
113
|
|
|
107
114
|
if (!res.ok) {
|
|
108
|
-
const msg =
|
|
115
|
+
const msg =
|
|
116
|
+
data.message || data.error || res.statusText || `HTTP ${res.status}`;
|
|
109
117
|
throw new Error(msg);
|
|
110
118
|
}
|
|
111
119
|
|
|
112
120
|
const payload = data.data;
|
|
113
121
|
if (!payload) {
|
|
114
|
-
throw new Error(
|
|
122
|
+
throw new Error("Unexpected response: missing data");
|
|
115
123
|
}
|
|
116
124
|
|
|
117
125
|
return {
|
|
@@ -120,6 +128,7 @@ async function getTaskHistorical(apiKey, taskId, timeoutMs, apiBase) {
|
|
|
120
128
|
ppt_biz_id: payload.ppt_biz_id,
|
|
121
129
|
live_doc_url: payload.live_doc_url,
|
|
122
130
|
live_doc_short_id: payload.live_doc_short_id ?? payload.livedoc_short_id,
|
|
131
|
+
error_message: payload.error_message,
|
|
123
132
|
};
|
|
124
133
|
}
|
|
125
134
|
|
|
@@ -141,64 +150,85 @@ export async function slides(query, options = {}) {
|
|
|
141
150
|
try {
|
|
142
151
|
const apiBase = await getApiBase();
|
|
143
152
|
|
|
144
|
-
process.stderr.write(
|
|
153
|
+
process.stderr.write("Creating PPT task...\n");
|
|
145
154
|
|
|
146
|
-
const createResult = await createPptTask(
|
|
155
|
+
const createResult = await createPptTask(
|
|
156
|
+
apiKey,
|
|
157
|
+
query,
|
|
158
|
+
requestTimeoutMs,
|
|
159
|
+
apiBase
|
|
160
|
+
);
|
|
147
161
|
const taskId = createResult.task_id;
|
|
148
162
|
|
|
149
163
|
if (options.json && options.verbose) {
|
|
150
164
|
process.stderr.write(`Task ID: ${taskId}\n`);
|
|
151
165
|
}
|
|
152
166
|
|
|
153
|
-
|
|
154
|
-
|
|
167
|
+
// 默认显示 spinner 动画;仅在使用 -v/--json 时改为逐行状态输出
|
|
168
|
+
const useLiveStatus = !options.verbose && !options.json;
|
|
169
|
+
const canOverwriteLine = process.stderr.isTTY && useLiveStatus;
|
|
155
170
|
if (!useLiveStatus) {
|
|
156
|
-
process.stderr.write(
|
|
171
|
+
process.stderr.write("Generating slides (this may take a minute)...\n");
|
|
157
172
|
}
|
|
158
173
|
|
|
159
174
|
const startTime = Date.now();
|
|
160
175
|
let lastStatus;
|
|
161
176
|
let spinIndex = 0;
|
|
162
|
-
if (useLiveStatus) writeStatusLine(SPINNER_FRAMES[0], 0);
|
|
177
|
+
if (useLiveStatus) writeStatusLine(SPINNER_FRAMES[0], 0, canOverwriteLine);
|
|
163
178
|
|
|
164
179
|
while (Date.now() - startTime < pollTimeoutMs) {
|
|
165
180
|
await sleep(pollIntervalMs);
|
|
166
181
|
|
|
167
|
-
const historical = await getTaskHistorical(
|
|
182
|
+
const historical = await getTaskHistorical(
|
|
183
|
+
apiKey,
|
|
184
|
+
taskId,
|
|
185
|
+
requestTimeoutMs,
|
|
186
|
+
apiBase
|
|
187
|
+
);
|
|
168
188
|
const normalizedStatus = normalizeTaskStatus(historical.task_status);
|
|
169
189
|
lastStatus = normalizedStatus || historical.task_status;
|
|
170
190
|
|
|
171
191
|
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
172
192
|
if (useLiveStatus) {
|
|
173
193
|
spinIndex = (spinIndex + 1) % SPINNER_FRAMES.length;
|
|
174
|
-
writeStatusLine(SPINNER_FRAMES[spinIndex], elapsed);
|
|
194
|
+
writeStatusLine(SPINNER_FRAMES[spinIndex], elapsed, canOverwriteLine);
|
|
175
195
|
} else if (!options.verbose && !options.json) {
|
|
176
196
|
process.stderr.write(` Generating... ${elapsed}s\n`);
|
|
177
197
|
}
|
|
178
198
|
|
|
179
|
-
const done =
|
|
199
|
+
const done =
|
|
200
|
+
normalizedStatus === "COMPLETED" || normalizedStatus === "SUCCESS";
|
|
180
201
|
if (done) {
|
|
181
|
-
if (useLiveStatus) clearStatusLine();
|
|
202
|
+
if (useLiveStatus && canOverwriteLine) clearStatusLine();
|
|
182
203
|
const pptUrl =
|
|
183
204
|
historical.ppt_url ||
|
|
184
|
-
(historical.ppt_biz_id
|
|
205
|
+
(historical.ppt_biz_id
|
|
206
|
+
? `https://dev.felo.ai/slides/${historical.ppt_biz_id}`
|
|
207
|
+
: null);
|
|
185
208
|
const liveDocUrl =
|
|
186
209
|
historical.live_doc_url ||
|
|
187
|
-
(historical.live_doc_short_id
|
|
188
|
-
|
|
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);
|
|
189
216
|
const url = pptUrl || liveDocUrl;
|
|
190
217
|
if (options.json) {
|
|
191
218
|
console.log(
|
|
192
219
|
JSON.stringify(
|
|
193
220
|
{
|
|
194
|
-
status:
|
|
221
|
+
status: "ok",
|
|
195
222
|
data: {
|
|
196
223
|
task_id: taskId,
|
|
197
224
|
task_status: normalizedStatus || historical.task_status,
|
|
198
225
|
ppt_url: pptUrl,
|
|
199
|
-
ppt_biz_id:
|
|
226
|
+
ppt_biz_id:
|
|
227
|
+
historical.ppt_biz_id ?? createResult.ppt_business_id,
|
|
200
228
|
live_doc_url: liveDocUrl,
|
|
201
|
-
livedoc_short_id:
|
|
229
|
+
livedoc_short_id:
|
|
230
|
+
historical.live_doc_short_id ??
|
|
231
|
+
createResult.livedoc_short_id,
|
|
202
232
|
ppt_business_id: createResult.ppt_business_id,
|
|
203
233
|
},
|
|
204
234
|
},
|
|
@@ -209,40 +239,94 @@ export async function slides(query, options = {}) {
|
|
|
209
239
|
} else {
|
|
210
240
|
if (url) {
|
|
211
241
|
if (pptUrl && !options.json) {
|
|
212
|
-
process.stderr.write(
|
|
242
|
+
process.stderr.write("PPT ready. Open this link to preview:\n");
|
|
213
243
|
}
|
|
214
244
|
console.log(pptUrl || liveDocUrl);
|
|
215
245
|
} else {
|
|
216
246
|
if (useLiveStatus) clearStatusLine();
|
|
217
|
-
console.error(
|
|
247
|
+
console.error(
|
|
248
|
+
"Error: Completed but no ppt_url or live_doc_url in response"
|
|
249
|
+
);
|
|
218
250
|
return 1;
|
|
219
251
|
}
|
|
220
252
|
}
|
|
221
253
|
return 0;
|
|
222
254
|
}
|
|
223
255
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
+
|
|
230
307
|
return 1;
|
|
231
308
|
}
|
|
232
309
|
|
|
233
310
|
if (options.verbose) {
|
|
234
|
-
process.stderr.write(
|
|
311
|
+
process.stderr.write(
|
|
312
|
+
` Status: ${
|
|
313
|
+
normalizedStatus || historical.task_status || "UNKNOWN"
|
|
314
|
+
}\n`
|
|
315
|
+
);
|
|
235
316
|
}
|
|
236
317
|
}
|
|
237
318
|
|
|
238
|
-
if (useLiveStatus) clearStatusLine();
|
|
319
|
+
if (useLiveStatus && canOverwriteLine) clearStatusLine();
|
|
239
320
|
console.error(
|
|
240
|
-
`Error: Timed out after ${pollTimeoutMs / 1000}s. Last status: ${
|
|
321
|
+
`Error: Timed out after ${pollTimeoutMs / 1000}s. Last status: ${
|
|
322
|
+
lastStatus ?? "unknown"
|
|
323
|
+
}`
|
|
241
324
|
);
|
|
242
325
|
return 1;
|
|
243
326
|
} catch (err) {
|
|
244
|
-
if (process.stderr.isTTY && !options.verbose && !options.json)
|
|
245
|
-
|
|
327
|
+
if (process.stderr.isTTY && !options.verbose && !options.json)
|
|
328
|
+
clearStatusLine();
|
|
329
|
+
console.error("Error:", err.message || err);
|
|
246
330
|
return 1;
|
|
247
331
|
}
|
|
248
332
|
}
|