felo-ai 0.2.4 → 0.2.6
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.en.md +24 -2
- package/README.md +62 -1
- package/felo-slides/SKILL.md +13 -1
- package/felo-web-extract/README.md +78 -0
- package/felo-web-extract/SKILL.md +200 -0
- package/felo-web-extract/scripts/run_web_extract.mjs +232 -0
- package/felo-youtube-subtitling/README.md +59 -0
- package/felo-youtube-subtitling/SKILL.md +161 -0
- package/felo-youtube-subtitling/scripts/run_youtube_subtitling.mjs +239 -0
- package/package.json +4 -2
- package/src/cli.js +123 -49
- package/src/slides.js +141 -57
- package/src/webExtract.js +148 -0
- package/src/youtubeSubtitling.js +179 -0
package/src/cli.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { createRequire } from
|
|
4
|
-
import { Command } from
|
|
5
|
-
import { search } from
|
|
6
|
-
import { slides } from
|
|
7
|
-
import
|
|
3
|
+
import { createRequire } from "module";
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { search } from "./search.js";
|
|
6
|
+
import { slides } from "./slides.js";
|
|
7
|
+
import { webExtract } from "./webExtract.js";
|
|
8
|
+
import { youtubeSubtitling } from "./youtubeSubtitling.js";
|
|
9
|
+
import * as config from "./config.js";
|
|
8
10
|
|
|
9
11
|
const require = createRequire(import.meta.url);
|
|
10
|
-
const pkg = require(
|
|
12
|
+
const pkg = require("../package.json");
|
|
11
13
|
|
|
12
14
|
/** Delay (ms) before process.exit to let Windows libuv finish handle cleanup. */
|
|
13
15
|
const EXIT_DELAY_MS = 50;
|
|
@@ -22,13 +24,13 @@ function flushStdioThenExit(code) {
|
|
|
22
24
|
const doExit = () => setTimeout(() => process.exit(code), EXIT_DELAY_MS);
|
|
23
25
|
const flushStderr = () => {
|
|
24
26
|
if (process.stderr?.writable && !process.stderr.destroyed) {
|
|
25
|
-
process.stderr.write(
|
|
27
|
+
process.stderr.write("", () => doExit());
|
|
26
28
|
} else {
|
|
27
29
|
doExit();
|
|
28
30
|
}
|
|
29
31
|
};
|
|
30
32
|
if (process.stdout?.writable && !process.stdout.destroyed) {
|
|
31
|
-
process.stdout.write(
|
|
33
|
+
process.stdout.write("", () => flushStderr());
|
|
32
34
|
} else {
|
|
33
35
|
flushStderr();
|
|
34
36
|
}
|
|
@@ -37,17 +39,17 @@ function flushStdioThenExit(code) {
|
|
|
37
39
|
const program = new Command();
|
|
38
40
|
|
|
39
41
|
program
|
|
40
|
-
.name(
|
|
41
|
-
.description(
|
|
42
|
+
.name("felo")
|
|
43
|
+
.description("Felo AI CLI - real-time search from the terminal")
|
|
42
44
|
.version(pkg.version);
|
|
43
45
|
|
|
44
46
|
program
|
|
45
|
-
.command(
|
|
46
|
-
.description(
|
|
47
|
-
.argument(
|
|
48
|
-
.option(
|
|
49
|
-
.option(
|
|
50
|
-
.option(
|
|
47
|
+
.command("search")
|
|
48
|
+
.description("Search for current information (weather, news, docs, etc.)")
|
|
49
|
+
.argument("<query>", "search query")
|
|
50
|
+
.option("-j, --json", "output raw JSON")
|
|
51
|
+
.option("-v, --verbose", "show query analysis and sources")
|
|
52
|
+
.option("-t, --timeout <seconds>", "request timeout in seconds", "60")
|
|
51
53
|
.action(async (query, opts) => {
|
|
52
54
|
const timeoutMs = parseInt(opts.timeout, 10) * 1000;
|
|
53
55
|
const code = await search(query, {
|
|
@@ -60,13 +62,26 @@ program
|
|
|
60
62
|
});
|
|
61
63
|
|
|
62
64
|
program
|
|
63
|
-
.command(
|
|
64
|
-
.description(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
.
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
.command("slides")
|
|
66
|
+
.description(
|
|
67
|
+
"Generate PPT/slides from a prompt (async task, outputs live doc URL when done)"
|
|
68
|
+
)
|
|
69
|
+
.argument(
|
|
70
|
+
"<query>",
|
|
71
|
+
'PPT generation prompt (e.g. "Felo, 2 pages" or "Introduction to React")'
|
|
72
|
+
)
|
|
73
|
+
.option("-j, --json", "output raw JSON with task_id and live_doc_url")
|
|
74
|
+
.option("-v, --verbose", "show polling status")
|
|
75
|
+
.option(
|
|
76
|
+
"-t, --timeout <seconds>",
|
|
77
|
+
"request timeout in seconds for each API call",
|
|
78
|
+
"60"
|
|
79
|
+
)
|
|
80
|
+
.option(
|
|
81
|
+
"--poll-timeout <seconds>",
|
|
82
|
+
"max seconds to wait for task completion",
|
|
83
|
+
"1200"
|
|
84
|
+
)
|
|
70
85
|
.action(async (query, opts) => {
|
|
71
86
|
const timeoutMs = parseInt(opts.timeout, 10) * 1000;
|
|
72
87
|
const pollTimeoutMs = parseInt(opts.pollTimeout, 10) * 1000 || 1_200_000;
|
|
@@ -81,97 +96,156 @@ program
|
|
|
81
96
|
});
|
|
82
97
|
|
|
83
98
|
const configCmd = program
|
|
84
|
-
.command(
|
|
85
|
-
.description(
|
|
99
|
+
.command("config")
|
|
100
|
+
.description(
|
|
101
|
+
"Manage persisted config (e.g. FELO_API_KEY). Stored in ~/.felo/config.json"
|
|
102
|
+
);
|
|
86
103
|
|
|
87
104
|
configCmd
|
|
88
|
-
.command(
|
|
89
|
-
.description(
|
|
105
|
+
.command("set <key> <value>")
|
|
106
|
+
.description(
|
|
107
|
+
"Set a config value (e.g. felo config set FELO_API_KEY your-key)"
|
|
108
|
+
)
|
|
90
109
|
.action(async (key, value) => {
|
|
91
110
|
try {
|
|
92
111
|
await config.setConfig(key, value);
|
|
93
112
|
console.log(`Set ${key}`);
|
|
94
113
|
flushStdioThenExit(0);
|
|
95
114
|
} catch (e) {
|
|
96
|
-
console.error(
|
|
115
|
+
console.error("Error:", e.message);
|
|
97
116
|
flushStdioThenExit(1);
|
|
98
117
|
}
|
|
99
118
|
});
|
|
100
119
|
|
|
101
120
|
configCmd
|
|
102
|
-
.command(
|
|
103
|
-
.description(
|
|
121
|
+
.command("get <key>")
|
|
122
|
+
.description("Get a config value (sensitive keys are masked)")
|
|
104
123
|
.action(async (key) => {
|
|
105
124
|
try {
|
|
106
125
|
const value = await config.getConfigValue(key);
|
|
107
126
|
if (value === undefined || value === null) {
|
|
108
|
-
console.log(
|
|
127
|
+
console.log("(not set)");
|
|
109
128
|
} else {
|
|
110
129
|
console.log(config.maskValueForDisplay(key, value));
|
|
111
130
|
}
|
|
112
131
|
flushStdioThenExit(0);
|
|
113
132
|
} catch (e) {
|
|
114
|
-
console.error(
|
|
133
|
+
console.error("Error:", e.message);
|
|
115
134
|
flushStdioThenExit(1);
|
|
116
135
|
}
|
|
117
136
|
});
|
|
118
137
|
|
|
119
138
|
configCmd
|
|
120
|
-
.command(
|
|
121
|
-
.description(
|
|
139
|
+
.command("list")
|
|
140
|
+
.description("List all config keys (values are hidden)")
|
|
122
141
|
.action(async () => {
|
|
123
142
|
try {
|
|
124
143
|
const c = await config.listConfig();
|
|
125
144
|
const keys = Object.keys(c);
|
|
126
145
|
if (keys.length === 0) {
|
|
127
|
-
console.log(
|
|
146
|
+
console.log("No config set. Use: felo config set FELO_API_KEY <key>");
|
|
128
147
|
} else {
|
|
129
148
|
keys.forEach((k) => console.log(k));
|
|
130
149
|
}
|
|
131
150
|
flushStdioThenExit(0);
|
|
132
151
|
} catch (e) {
|
|
133
|
-
console.error(
|
|
152
|
+
console.error("Error:", e.message);
|
|
134
153
|
flushStdioThenExit(1);
|
|
135
154
|
}
|
|
136
155
|
});
|
|
137
156
|
|
|
138
157
|
configCmd
|
|
139
|
-
.command(
|
|
140
|
-
.description(
|
|
158
|
+
.command("unset <key>")
|
|
159
|
+
.description("Remove a config value")
|
|
141
160
|
.action(async (key) => {
|
|
142
161
|
try {
|
|
143
162
|
await config.unsetConfig(key);
|
|
144
163
|
console.log(`Unset ${key}`);
|
|
145
164
|
flushStdioThenExit(0);
|
|
146
165
|
} catch (e) {
|
|
147
|
-
console.error(
|
|
166
|
+
console.error("Error:", e.message);
|
|
148
167
|
flushStdioThenExit(1);
|
|
149
168
|
}
|
|
150
169
|
});
|
|
151
170
|
|
|
152
171
|
configCmd
|
|
153
|
-
.command(
|
|
154
|
-
.description(
|
|
172
|
+
.command("path")
|
|
173
|
+
.description("Show config file path")
|
|
155
174
|
.action(() => {
|
|
156
175
|
console.log(config.getConfigPath());
|
|
157
176
|
flushStdioThenExit(0);
|
|
158
177
|
});
|
|
159
178
|
|
|
160
179
|
program
|
|
161
|
-
.command(
|
|
162
|
-
.description(
|
|
163
|
-
.
|
|
180
|
+
.command("web-extract")
|
|
181
|
+
.description("Extract webpage content from a URL (markdown, text, or html)")
|
|
182
|
+
.requiredOption("-u, --url <url>", "page URL to extract")
|
|
183
|
+
.option(
|
|
184
|
+
"-f, --format <format>",
|
|
185
|
+
"output format: html, text, markdown",
|
|
186
|
+
"markdown"
|
|
187
|
+
)
|
|
188
|
+
.option(
|
|
189
|
+
"--target-selector <selector>",
|
|
190
|
+
"CSS selector for target element only"
|
|
191
|
+
)
|
|
192
|
+
.option(
|
|
193
|
+
"--wait-for-selector <selector>",
|
|
194
|
+
"wait for selector before extracting"
|
|
195
|
+
)
|
|
196
|
+
.option("--readability", "use readability (main content only)")
|
|
197
|
+
.option("--crawl-mode <mode>", "crawl mode: fast or fine", "fast")
|
|
198
|
+
.option("-t, --timeout <seconds>", "request timeout in seconds", "60")
|
|
199
|
+
.option("-j, --json", "output full API response as JSON")
|
|
200
|
+
.action(async (opts) => {
|
|
201
|
+
const timeoutMs = parseInt(opts.timeout, 10) * 1000;
|
|
202
|
+
const code = await webExtract({
|
|
203
|
+
url: opts.url,
|
|
204
|
+
format: opts.format,
|
|
205
|
+
targetSelector: opts.targetSelector,
|
|
206
|
+
waitForSelector: opts.waitForSelector,
|
|
207
|
+
readability: opts.readability,
|
|
208
|
+
crawlMode: opts.crawlMode,
|
|
209
|
+
timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
|
|
210
|
+
json: opts.json,
|
|
211
|
+
});
|
|
212
|
+
process.exitCode = code;
|
|
213
|
+
flushStdioThenExit(code);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
program
|
|
217
|
+
.command("youtube-subtitling")
|
|
218
|
+
.description("Fetch YouTube video subtitles/captions by video URL or ID")
|
|
219
|
+
.requiredOption("-v, --video-code <url-or-id>", "YouTube video URL or video ID (e.g. https://youtube.com/watch?v=ID)")
|
|
220
|
+
.option("-l, --language <code>", "Subtitle language (e.g. en, zh-CN)")
|
|
221
|
+
.option("--with-time", "Include start/duration per segment")
|
|
222
|
+
.option("-j, --json", "Output full API response as JSON")
|
|
223
|
+
.action(async (opts) => {
|
|
224
|
+
const code = await youtubeSubtitling({
|
|
225
|
+
videoCode: opts.videoCode,
|
|
226
|
+
language: opts.language,
|
|
227
|
+
withTime: opts.withTime,
|
|
228
|
+
json: opts.json,
|
|
229
|
+
});
|
|
230
|
+
process.exitCode = code;
|
|
231
|
+
flushStdioThenExit(code);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
program
|
|
235
|
+
.command("summarize")
|
|
236
|
+
.description("Summarize text or URL (coming when API is available)")
|
|
237
|
+
.argument("[input]", "text or URL to summarize")
|
|
164
238
|
.action(() => {
|
|
165
|
-
console.error(
|
|
239
|
+
console.error("summarize: not yet implemented. Use felo search for now.");
|
|
166
240
|
flushStdioThenExit(1);
|
|
167
241
|
});
|
|
168
242
|
|
|
169
243
|
program
|
|
170
|
-
.command(
|
|
171
|
-
.description(
|
|
172
|
-
.argument(
|
|
244
|
+
.command("translate")
|
|
245
|
+
.description("Translate text (coming when API is available)")
|
|
246
|
+
.argument("[text]", "text to translate")
|
|
173
247
|
.action(() => {
|
|
174
|
-
console.error(
|
|
248
|
+
console.error("translate: not yet implemented. Use felo search for now.");
|
|
175
249
|
flushStdioThenExit(1);
|
|
176
250
|
});
|
|
177
251
|
|
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
|
}
|