felo-ai 0.2.3 → 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/README.en.md +28 -9
- package/README.md +37 -18
- package/package.json +2 -2
- package/src/cli.js +1 -1
- package/src/slides.js +141 -57
package/README.en.md
CHANGED
|
@@ -21,12 +21,9 @@ Search the web for up-to-date information and get AI-synthesized answers. Ideal
|
|
|
21
21
|
- **Claude Code**: After installing the skill, it triggers automatically, or type `/felo-ai your question`
|
|
22
22
|
- **Examples**: `felo search "Tokyo weather"`, `felo search "React 19 new features" --verbose`
|
|
23
23
|
|
|
24
|
-
### Capability 2: Generate PPT
|
|
24
|
+
### Capability 2: Generate PPT (Felo Slides)
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
- **Terminal**: `felo slides "your topic or description"`
|
|
29
|
-
- **Examples**: `felo slides "Felo product intro, 3 slides"`, `felo slides "Introduction to React" --poll-timeout 300`
|
|
26
|
+
Terminal: `felo slides "your topic"`. In Claude Code: install `npx @claude/skills add felo-slides`, then `/felo-slides your topic`. You get an online document link when done. Examples: `felo slides "Felo product intro, 3 slides"`, `felo slides "Introduction to React"`.
|
|
30
27
|
|
|
31
28
|
---
|
|
32
29
|
|
|
@@ -62,17 +59,39 @@ Get your API key at [felo.ai](https://felo.ai) (Settings → API Keys).
|
|
|
62
59
|
| `felo config set FELO_API_KEY <key>` | Save API key |
|
|
63
60
|
| `felo config get/list/path/unset` | View / list / path / remove config |
|
|
64
61
|
|
|
62
|
+
### Examples
|
|
63
|
+
|
|
64
|
+
**Search**
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
felo search "Tokyo weather"
|
|
68
|
+
felo search "MacBook Air M3 price"
|
|
69
|
+
felo search "React 19 new features" --verbose
|
|
70
|
+
npx felo-ai search "Tokyo weather"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Slides**
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
felo slides "Felo product intro, 3 slides"
|
|
77
|
+
felo slides "Introduction to React"
|
|
78
|
+
felo slides "Q4 2024 business review, 10 pages" --poll-timeout 300
|
|
79
|
+
npx felo-ai slides "Tokyo travel guide, 5 slides"
|
|
80
|
+
```
|
|
81
|
+
|
|
65
82
|
---
|
|
66
83
|
|
|
67
|
-
## Claude Code
|
|
84
|
+
## Claude Code Skills
|
|
68
85
|
|
|
69
|
-
Install
|
|
86
|
+
**Search** — Install and use real-time search:
|
|
70
87
|
|
|
71
88
|
```bash
|
|
72
|
-
npx @claude/skills add felo-
|
|
89
|
+
npx @claude/skills add felo-search
|
|
73
90
|
```
|
|
74
91
|
|
|
75
|
-
After setting `FELO_API_KEY`, ask Claude things like “What’s the weather in Tokyo today?” or “React 19 new features
|
|
92
|
+
After setting `FELO_API_KEY`, ask Claude things like “What’s the weather in Tokyo today?” or “React 19 new features”; the search skill triggers automatically (or use `/felo-search your question`).
|
|
93
|
+
|
|
94
|
+
**Slides (PPT)** — `npx @claude/skills add felo-slides`, then `/felo-slides your topic`. Same `FELO_API_KEY`. [Details →](./felo-slides/README.md)
|
|
76
95
|
|
|
77
96
|
---
|
|
78
97
|
|
package/README.md
CHANGED
|
@@ -1,30 +1,27 @@
|
|
|
1
|
-
# Felo
|
|
1
|
+
# Felo AI CLI
|
|
2
2
|
|
|
3
|
-
**Ask anything. Get current answers
|
|
3
|
+
**Ask anything. Get current answers. Generate slides from a prompt.**
|
|
4
4
|
|
|
5
|
-
Real-time
|
|
5
|
+
[npm package: **felo-ai**](https://www.npmjs.com/package/felo-ai) — Real-time search and PPT generation from the terminal. Also works as Claude Code skills. Supports Chinese, English, Japanese, and Korean.
|
|
6
6
|
|
|
7
|
-
[](https://www.npmjs.com/package/felo-ai) []()
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
##
|
|
12
|
-
|
|
13
|
-
Use Felo search from the terminal without opening Claude Code.
|
|
14
|
-
|
|
15
|
-
### Install
|
|
11
|
+
## Install (CLI)
|
|
16
12
|
|
|
17
13
|
```bash
|
|
18
|
-
npm install -g felo-
|
|
14
|
+
npm install -g felo-ai
|
|
19
15
|
```
|
|
20
16
|
|
|
21
|
-
|
|
17
|
+
Run without installing:
|
|
22
18
|
|
|
23
19
|
```bash
|
|
24
|
-
npx felo-
|
|
20
|
+
npx felo-ai search "Tokyo weather"
|
|
21
|
+
npx felo-ai slides "Introduction to React, 5 slides"
|
|
25
22
|
```
|
|
26
23
|
|
|
27
|
-
After install, the command is `felo` (
|
|
24
|
+
After install, the command is `felo` (package name: **felo-ai**).
|
|
28
25
|
|
|
29
26
|
### Configure API key
|
|
30
27
|
|
|
@@ -53,32 +50,47 @@ Get your API key from [felo.ai](https://felo.ai) (Settings → API Keys). Enviro
|
|
|
53
50
|
| Command | Description |
|
|
54
51
|
| ------------------------------------ | ----------------------------------------------------- |
|
|
55
52
|
| `felo search "<query>"` | Search for current info (weather, news, prices, etc.) |
|
|
53
|
+
| `felo slides "<prompt>"` | Generate PPT; returns link when done |
|
|
56
54
|
| `felo config set FELO_API_KEY <key>` | Save API key to config |
|
|
57
55
|
| `felo config get FELO_API_KEY` | Print stored key |
|
|
58
56
|
| `felo config list` | List config keys |
|
|
59
57
|
| `felo config path` | Show config file path |
|
|
60
|
-
| `felo summarize` / `felo translate` | Planned; use `felo search` for now |
|
|
61
58
|
|
|
62
59
|
### Examples
|
|
63
60
|
|
|
61
|
+
**Search**
|
|
62
|
+
|
|
64
63
|
```bash
|
|
65
64
|
felo search "Tokyo weather"
|
|
66
65
|
felo search "MacBook Air M3 price"
|
|
67
66
|
felo search "React 19 new features" --verbose
|
|
68
67
|
felo search "Hangzhou tomorrow weather" --json
|
|
69
|
-
npx felo-
|
|
68
|
+
npx felo-ai search "Tokyo weather"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Slides**
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
felo slides "Felo product intro, 3 slides"
|
|
75
|
+
felo slides "Introduction to React"
|
|
76
|
+
felo slides "Q4 2024 business review, 10 pages" --poll-timeout 300
|
|
77
|
+
npx felo-ai slides "Tokyo travel guide, 5 slides"
|
|
70
78
|
```
|
|
71
79
|
|
|
72
80
|
### CLI FAQ
|
|
73
81
|
|
|
74
82
|
- **Key not found?** Run `felo config set FELO_API_KEY <key>` or set the `FELO_API_KEY` environment variable.
|
|
75
83
|
- **Request timeout?** Use `felo search "query" --timeout 120` (default 60 seconds). 5xx errors are retried automatically with backoff.
|
|
84
|
+
- **Slides taking long?** Use `felo slides "topic" --poll-timeout 300` (default 1200s) to limit wait.
|
|
76
85
|
- **Where is config stored?** Run `felo config path` to see the file (e.g. `~/.felo/config.json`).
|
|
77
|
-
- **Streaming?** Not yet; when the Felo API supports streaming, the CLI can be updated to stream output.
|
|
78
86
|
|
|
79
87
|
---
|
|
80
88
|
|
|
81
|
-
##
|
|
89
|
+
## Claude Code Skills (optional)
|
|
90
|
+
|
|
91
|
+
This repo also provides **Claude Code** skills. If you use [Claude Code](https://claude.ai/code), you can install search and/or slides as skills so Claude can run them in chat.
|
|
92
|
+
|
|
93
|
+
### Quick Start (Search skill)
|
|
82
94
|
|
|
83
95
|
Install the skill:
|
|
84
96
|
|
|
@@ -110,6 +122,8 @@ Ask Claude: "What's the weather in Tokyo today?"
|
|
|
110
122
|
|
|
111
123
|
**You're done!** The skill triggers automatically for any question needing current information.
|
|
112
124
|
|
|
125
|
+
**Felo Slides (PPT):** In terminal run `felo slides "your topic"`. In Claude Code install with `npx @claude/skills add felo-slides`, then use `/felo-slides your topic`. See [felo-slides](./felo-slides/README.md).
|
|
126
|
+
|
|
113
127
|
---
|
|
114
128
|
|
|
115
129
|
## Usage Examples
|
|
@@ -284,6 +298,10 @@ Real-time web search with AI-generated answers.
|
|
|
284
298
|
|
|
285
299
|
**[View skill documentation →](./felo-search/)**
|
|
286
300
|
|
|
301
|
+
### felo-slides
|
|
302
|
+
|
|
303
|
+
Generate PPT: in terminal use `felo slides "your topic"`, in Claude Code use `/felo-slides your topic`. **[View skill documentation →](./felo-slides/)**
|
|
304
|
+
|
|
287
305
|
---
|
|
288
306
|
|
|
289
307
|
## Contributing
|
|
@@ -302,6 +320,7 @@ Run CLI tests: `npm test`
|
|
|
302
320
|
|
|
303
321
|
## Links
|
|
304
322
|
|
|
323
|
+
- **[npm: felo-ai](https://www.npmjs.com/package/felo-ai)** — CLI package
|
|
305
324
|
- **[Felo Open Platform](https://openapi.felo.ai/docs/)** — Get your API key
|
|
306
325
|
- **[API Documentation](https://openapi.felo.ai/docs/api-reference/v2/chat.html)** — API reference
|
|
307
326
|
- **[Claude Code](https://claude.ai/code)** — AI assistant CLI
|
|
@@ -321,7 +340,7 @@ Run CLI tests: `npm test`
|
|
|
321
340
|
|
|
322
341
|
## License
|
|
323
342
|
|
|
324
|
-
MIT
|
|
343
|
+
MIT — see [LICENSE](./felo-search/LICENSE) in the repo for details.
|
|
325
344
|
|
|
326
345
|
---
|
|
327
346
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "felo-ai",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Felo AI CLI - real-time search from the terminal",
|
|
3
|
+
"version": "0.2.5",
|
|
4
|
+
"description": "Felo AI CLI - real-time search and PPT generation from the terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/cli.js",
|
|
7
7
|
"bin": {
|
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
|
}
|