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
|
@@ -1,251 +1,251 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const DEFAULT_API_BASE = 'https://openapi.felo.ai';
|
|
4
|
-
const DEFAULT_INTERVAL_SEC = 10;
|
|
5
|
-
const DEFAULT_MAX_WAIT_SEC = 1800;
|
|
6
|
-
const DEFAULT_TIMEOUT_SEC = 60;
|
|
7
|
-
|
|
8
|
-
function usage() {
|
|
9
|
-
console.error(
|
|
10
|
-
[
|
|
11
|
-
'Usage:',
|
|
12
|
-
' node felo-slides/scripts/run_ppt_task.mjs --query "your prompt" [options]',
|
|
13
|
-
'',
|
|
14
|
-
'Options:',
|
|
15
|
-
' --query <text> PPT prompt (required)',
|
|
16
|
-
' --interval <seconds> Poll interval, default 10',
|
|
17
|
-
' --max-wait <seconds> Max wait time, default 1800',
|
|
18
|
-
' --timeout <seconds> Request timeout, default 60',
|
|
19
|
-
' --json Print JSON output',
|
|
20
|
-
' --verbose Print polling status to stderr',
|
|
21
|
-
' --help Show this help',
|
|
22
|
-
].join('\n')
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function parseArgs(argv) {
|
|
27
|
-
const out = {
|
|
28
|
-
query: '',
|
|
29
|
-
intervalSec: DEFAULT_INTERVAL_SEC,
|
|
30
|
-
maxWaitSec: DEFAULT_MAX_WAIT_SEC,
|
|
31
|
-
timeoutSec: DEFAULT_TIMEOUT_SEC,
|
|
32
|
-
json: false,
|
|
33
|
-
verbose: false,
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
37
|
-
const a = argv[i];
|
|
38
|
-
if (a === '--help' || a === '-h') {
|
|
39
|
-
out.help = true;
|
|
40
|
-
} else if (a === '--json') {
|
|
41
|
-
out.json = true;
|
|
42
|
-
} else if (a === '--verbose' || a === '-v') {
|
|
43
|
-
out.verbose = true;
|
|
44
|
-
} else if (a === '--query') {
|
|
45
|
-
out.query = argv[i + 1] ?? '';
|
|
46
|
-
i += 1;
|
|
47
|
-
} else if (a === '--interval') {
|
|
48
|
-
out.intervalSec = Number.parseInt(argv[i + 1] ?? '', 10);
|
|
49
|
-
i += 1;
|
|
50
|
-
} else if (a === '--max-wait') {
|
|
51
|
-
out.maxWaitSec = Number.parseInt(argv[i + 1] ?? '', 10);
|
|
52
|
-
i += 1;
|
|
53
|
-
} else if (a === '--timeout') {
|
|
54
|
-
out.timeoutSec = Number.parseInt(argv[i + 1] ?? '', 10);
|
|
55
|
-
i += 1;
|
|
56
|
-
} else if (!a.startsWith('-') && !out.query) {
|
|
57
|
-
out.query = a;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (!Number.isFinite(out.intervalSec) || out.intervalSec <= 0) out.intervalSec = DEFAULT_INTERVAL_SEC;
|
|
62
|
-
if (!Number.isFinite(out.maxWaitSec) || out.maxWaitSec <= 0) out.maxWaitSec = DEFAULT_MAX_WAIT_SEC;
|
|
63
|
-
if (!Number.isFinite(out.timeoutSec) || out.timeoutSec <= 0) out.timeoutSec = DEFAULT_TIMEOUT_SEC;
|
|
64
|
-
return out;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function normalizeStatus(v) {
|
|
68
|
-
return String(v ?? '').trim().toUpperCase();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function sleep(ms) {
|
|
72
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function getMessage(payload) {
|
|
76
|
-
return (
|
|
77
|
-
payload?.message ||
|
|
78
|
-
payload?.error ||
|
|
79
|
-
payload?.msg ||
|
|
80
|
-
payload?.code ||
|
|
81
|
-
'Unknown error'
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function isApiError(payload) {
|
|
86
|
-
const status = payload?.status;
|
|
87
|
-
const code = payload?.code;
|
|
88
|
-
if (typeof status === 'string' && status.toLowerCase() === 'error') return true;
|
|
89
|
-
if (typeof code === 'string' && code && code.toUpperCase() !== 'OK') return true;
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async function fetchJson(url, init, timeoutMs) {
|
|
94
|
-
const controller = new AbortController();
|
|
95
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
96
|
-
try {
|
|
97
|
-
const res = await fetch(url, { ...init, signal: controller.signal });
|
|
98
|
-
let body = {};
|
|
99
|
-
try {
|
|
100
|
-
body = await res.json();
|
|
101
|
-
} catch {
|
|
102
|
-
body = {};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (!res.ok) {
|
|
106
|
-
throw new Error(`HTTP ${res.status}: ${getMessage(body)}`);
|
|
107
|
-
}
|
|
108
|
-
if (isApiError(body)) {
|
|
109
|
-
throw new Error(getMessage(body));
|
|
110
|
-
}
|
|
111
|
-
return body;
|
|
112
|
-
} finally {
|
|
113
|
-
clearTimeout(timer);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function extractTaskUrls(historicalData, createData) {
|
|
118
|
-
const pptUrl = historicalData?.ppt_url || '';
|
|
119
|
-
const liveDocUrl =
|
|
120
|
-
historicalData?.live_doc_url ||
|
|
121
|
-
(historicalData?.live_doc_short_id || historicalData?.livedoc_short_id || createData?.livedoc_short_id
|
|
122
|
-
? `https://felo.ai/livedoc/${historicalData?.live_doc_short_id || historicalData?.livedoc_short_id || createData?.livedoc_short_id}`
|
|
123
|
-
: '');
|
|
124
|
-
return {
|
|
125
|
-
pptUrl,
|
|
126
|
-
liveDocUrl,
|
|
127
|
-
displayUrl: pptUrl || liveDocUrl,
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async function createTask(apiKey, apiBase, query, timeoutMs) {
|
|
132
|
-
const payload = await fetchJson(
|
|
133
|
-
`${apiBase}/v2/ppts`,
|
|
134
|
-
{
|
|
135
|
-
method: 'POST',
|
|
136
|
-
headers: {
|
|
137
|
-
Accept: 'application/json',
|
|
138
|
-
Authorization: `Bearer ${apiKey}`,
|
|
139
|
-
'Content-Type': 'application/json',
|
|
140
|
-
},
|
|
141
|
-
body: JSON.stringify({ query }),
|
|
142
|
-
},
|
|
143
|
-
timeoutMs
|
|
144
|
-
);
|
|
145
|
-
const data = payload?.data ?? {};
|
|
146
|
-
if (!data.task_id) {
|
|
147
|
-
throw new Error('Unexpected response: missing task_id');
|
|
148
|
-
}
|
|
149
|
-
return data;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async function queryHistorical(apiKey, apiBase, taskId, timeoutMs) {
|
|
153
|
-
const payload = await fetchJson(
|
|
154
|
-
`${apiBase}/v2/tasks/${encodeURIComponent(taskId)}/historical`,
|
|
155
|
-
{
|
|
156
|
-
method: 'GET',
|
|
157
|
-
headers: {
|
|
158
|
-
Accept: 'application/json',
|
|
159
|
-
Authorization: `Bearer ${apiKey}`,
|
|
160
|
-
},
|
|
161
|
-
},
|
|
162
|
-
timeoutMs
|
|
163
|
-
);
|
|
164
|
-
return payload?.data ?? {};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
async function main() {
|
|
168
|
-
const args = parseArgs(process.argv.slice(2));
|
|
169
|
-
if (args.help) {
|
|
170
|
-
usage();
|
|
171
|
-
process.exit(0);
|
|
172
|
-
}
|
|
173
|
-
if (!args.query) {
|
|
174
|
-
usage();
|
|
175
|
-
process.exit(1);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const apiKey = process.env.FELO_API_KEY?.trim();
|
|
179
|
-
if (!apiKey) {
|
|
180
|
-
console.error('ERROR: FELO_API_KEY not set');
|
|
181
|
-
process.exit(1);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const apiBase = (process.env.FELO_API_BASE?.trim() || DEFAULT_API_BASE).replace(/\/$/, '');
|
|
185
|
-
const timeoutMs = args.timeoutSec * 1000;
|
|
186
|
-
const intervalMs = args.intervalSec * 1000;
|
|
187
|
-
const maxWaitMs = args.maxWaitSec * 1000;
|
|
188
|
-
|
|
189
|
-
const createData = await createTask(apiKey, apiBase, args.query, timeoutMs);
|
|
190
|
-
const taskId = createData.task_id;
|
|
191
|
-
if (args.verbose) {
|
|
192
|
-
console.error(`Task ID: ${taskId}`);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const startAt = Date.now();
|
|
196
|
-
let lastStatus = '';
|
|
197
|
-
|
|
198
|
-
while (Date.now() - startAt <= maxWaitMs) {
|
|
199
|
-
const historicalData = await queryHistorical(apiKey, apiBase, taskId, timeoutMs);
|
|
200
|
-
const taskStatus = normalizeStatus(historicalData.task_status || historicalData.status);
|
|
201
|
-
const urls = extractTaskUrls(historicalData, createData);
|
|
202
|
-
lastStatus = taskStatus || 'UNKNOWN';
|
|
203
|
-
|
|
204
|
-
if (args.verbose) {
|
|
205
|
-
const elapsedSec = Math.floor((Date.now() - startAt) / 1000);
|
|
206
|
-
console.error(`[${elapsedSec}s] Status: ${lastStatus}`);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if (taskStatus === 'COMPLETED' || taskStatus === 'SUCCESS') {
|
|
210
|
-
if (!urls.displayUrl) {
|
|
211
|
-
throw new Error('Task completed but no ppt_url/live_doc_url is available');
|
|
212
|
-
}
|
|
213
|
-
if (args.json) {
|
|
214
|
-
console.log(
|
|
215
|
-
JSON.stringify(
|
|
216
|
-
{
|
|
217
|
-
status: 'ok',
|
|
218
|
-
data: {
|
|
219
|
-
task_id: taskId,
|
|
220
|
-
task_status: taskStatus,
|
|
221
|
-
ppt_url: urls.pptUrl || null,
|
|
222
|
-
live_doc_url: urls.liveDocUrl || null,
|
|
223
|
-
livedoc_short_id: createData.livedoc_short_id ?? historicalData.live_doc_short_id ?? historicalData.livedoc_short_id ?? null,
|
|
224
|
-
ppt_business_id: createData.ppt_business_id ?? null,
|
|
225
|
-
},
|
|
226
|
-
},
|
|
227
|
-
null,
|
|
228
|
-
2
|
|
229
|
-
)
|
|
230
|
-
);
|
|
231
|
-
} else {
|
|
232
|
-
console.log(urls.displayUrl);
|
|
233
|
-
}
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (taskStatus === 'FAILED' || taskStatus === 'ERROR') {
|
|
238
|
-
throw new Error(`Task finished with status: ${taskStatus}`);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
await sleep(intervalMs);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
throw new Error(`Timed out after ${args.maxWaitSec}s. Last status: ${lastStatus || 'UNKNOWN'}`);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
main().catch((err) => {
|
|
248
|
-
console.error(`ERROR: ${err?.message || err}`);
|
|
249
|
-
process.exit(1);
|
|
250
|
-
});
|
|
251
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const DEFAULT_API_BASE = 'https://openapi.felo.ai';
|
|
4
|
+
const DEFAULT_INTERVAL_SEC = 10;
|
|
5
|
+
const DEFAULT_MAX_WAIT_SEC = 1800;
|
|
6
|
+
const DEFAULT_TIMEOUT_SEC = 60;
|
|
7
|
+
|
|
8
|
+
function usage() {
|
|
9
|
+
console.error(
|
|
10
|
+
[
|
|
11
|
+
'Usage:',
|
|
12
|
+
' node felo-slides/scripts/run_ppt_task.mjs --query "your prompt" [options]',
|
|
13
|
+
'',
|
|
14
|
+
'Options:',
|
|
15
|
+
' --query <text> PPT prompt (required)',
|
|
16
|
+
' --interval <seconds> Poll interval, default 10',
|
|
17
|
+
' --max-wait <seconds> Max wait time, default 1800',
|
|
18
|
+
' --timeout <seconds> Request timeout, default 60',
|
|
19
|
+
' --json Print JSON output',
|
|
20
|
+
' --verbose Print polling status to stderr',
|
|
21
|
+
' --help Show this help',
|
|
22
|
+
].join('\n')
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseArgs(argv) {
|
|
27
|
+
const out = {
|
|
28
|
+
query: '',
|
|
29
|
+
intervalSec: DEFAULT_INTERVAL_SEC,
|
|
30
|
+
maxWaitSec: DEFAULT_MAX_WAIT_SEC,
|
|
31
|
+
timeoutSec: DEFAULT_TIMEOUT_SEC,
|
|
32
|
+
json: false,
|
|
33
|
+
verbose: false,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
37
|
+
const a = argv[i];
|
|
38
|
+
if (a === '--help' || a === '-h') {
|
|
39
|
+
out.help = true;
|
|
40
|
+
} else if (a === '--json') {
|
|
41
|
+
out.json = true;
|
|
42
|
+
} else if (a === '--verbose' || a === '-v') {
|
|
43
|
+
out.verbose = true;
|
|
44
|
+
} else if (a === '--query') {
|
|
45
|
+
out.query = argv[i + 1] ?? '';
|
|
46
|
+
i += 1;
|
|
47
|
+
} else if (a === '--interval') {
|
|
48
|
+
out.intervalSec = Number.parseInt(argv[i + 1] ?? '', 10);
|
|
49
|
+
i += 1;
|
|
50
|
+
} else if (a === '--max-wait') {
|
|
51
|
+
out.maxWaitSec = Number.parseInt(argv[i + 1] ?? '', 10);
|
|
52
|
+
i += 1;
|
|
53
|
+
} else if (a === '--timeout') {
|
|
54
|
+
out.timeoutSec = Number.parseInt(argv[i + 1] ?? '', 10);
|
|
55
|
+
i += 1;
|
|
56
|
+
} else if (!a.startsWith('-') && !out.query) {
|
|
57
|
+
out.query = a;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!Number.isFinite(out.intervalSec) || out.intervalSec <= 0) out.intervalSec = DEFAULT_INTERVAL_SEC;
|
|
62
|
+
if (!Number.isFinite(out.maxWaitSec) || out.maxWaitSec <= 0) out.maxWaitSec = DEFAULT_MAX_WAIT_SEC;
|
|
63
|
+
if (!Number.isFinite(out.timeoutSec) || out.timeoutSec <= 0) out.timeoutSec = DEFAULT_TIMEOUT_SEC;
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizeStatus(v) {
|
|
68
|
+
return String(v ?? '').trim().toUpperCase();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function sleep(ms) {
|
|
72
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getMessage(payload) {
|
|
76
|
+
return (
|
|
77
|
+
payload?.message ||
|
|
78
|
+
payload?.error ||
|
|
79
|
+
payload?.msg ||
|
|
80
|
+
payload?.code ||
|
|
81
|
+
'Unknown error'
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isApiError(payload) {
|
|
86
|
+
const status = payload?.status;
|
|
87
|
+
const code = payload?.code;
|
|
88
|
+
if (typeof status === 'string' && status.toLowerCase() === 'error') return true;
|
|
89
|
+
if (typeof code === 'string' && code && code.toUpperCase() !== 'OK') return true;
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function fetchJson(url, init, timeoutMs) {
|
|
94
|
+
const controller = new AbortController();
|
|
95
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
96
|
+
try {
|
|
97
|
+
const res = await fetch(url, { ...init, signal: controller.signal });
|
|
98
|
+
let body = {};
|
|
99
|
+
try {
|
|
100
|
+
body = await res.json();
|
|
101
|
+
} catch {
|
|
102
|
+
body = {};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!res.ok) {
|
|
106
|
+
throw new Error(`HTTP ${res.status}: ${getMessage(body)}`);
|
|
107
|
+
}
|
|
108
|
+
if (isApiError(body)) {
|
|
109
|
+
throw new Error(getMessage(body));
|
|
110
|
+
}
|
|
111
|
+
return body;
|
|
112
|
+
} finally {
|
|
113
|
+
clearTimeout(timer);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function extractTaskUrls(historicalData, createData) {
|
|
118
|
+
const pptUrl = historicalData?.ppt_url || '';
|
|
119
|
+
const liveDocUrl =
|
|
120
|
+
historicalData?.live_doc_url ||
|
|
121
|
+
(historicalData?.live_doc_short_id || historicalData?.livedoc_short_id || createData?.livedoc_short_id
|
|
122
|
+
? `https://felo.ai/livedoc/${historicalData?.live_doc_short_id || historicalData?.livedoc_short_id || createData?.livedoc_short_id}`
|
|
123
|
+
: '');
|
|
124
|
+
return {
|
|
125
|
+
pptUrl,
|
|
126
|
+
liveDocUrl,
|
|
127
|
+
displayUrl: pptUrl || liveDocUrl,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function createTask(apiKey, apiBase, query, timeoutMs) {
|
|
132
|
+
const payload = await fetchJson(
|
|
133
|
+
`${apiBase}/v2/ppts`,
|
|
134
|
+
{
|
|
135
|
+
method: 'POST',
|
|
136
|
+
headers: {
|
|
137
|
+
Accept: 'application/json',
|
|
138
|
+
Authorization: `Bearer ${apiKey}`,
|
|
139
|
+
'Content-Type': 'application/json',
|
|
140
|
+
},
|
|
141
|
+
body: JSON.stringify({ query }),
|
|
142
|
+
},
|
|
143
|
+
timeoutMs
|
|
144
|
+
);
|
|
145
|
+
const data = payload?.data ?? {};
|
|
146
|
+
if (!data.task_id) {
|
|
147
|
+
throw new Error('Unexpected response: missing task_id');
|
|
148
|
+
}
|
|
149
|
+
return data;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function queryHistorical(apiKey, apiBase, taskId, timeoutMs) {
|
|
153
|
+
const payload = await fetchJson(
|
|
154
|
+
`${apiBase}/v2/tasks/${encodeURIComponent(taskId)}/historical`,
|
|
155
|
+
{
|
|
156
|
+
method: 'GET',
|
|
157
|
+
headers: {
|
|
158
|
+
Accept: 'application/json',
|
|
159
|
+
Authorization: `Bearer ${apiKey}`,
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
timeoutMs
|
|
163
|
+
);
|
|
164
|
+
return payload?.data ?? {};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function main() {
|
|
168
|
+
const args = parseArgs(process.argv.slice(2));
|
|
169
|
+
if (args.help) {
|
|
170
|
+
usage();
|
|
171
|
+
process.exit(0);
|
|
172
|
+
}
|
|
173
|
+
if (!args.query) {
|
|
174
|
+
usage();
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const apiKey = process.env.FELO_API_KEY?.trim();
|
|
179
|
+
if (!apiKey) {
|
|
180
|
+
console.error('ERROR: FELO_API_KEY not set');
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const apiBase = (process.env.FELO_API_BASE?.trim() || DEFAULT_API_BASE).replace(/\/$/, '');
|
|
185
|
+
const timeoutMs = args.timeoutSec * 1000;
|
|
186
|
+
const intervalMs = args.intervalSec * 1000;
|
|
187
|
+
const maxWaitMs = args.maxWaitSec * 1000;
|
|
188
|
+
|
|
189
|
+
const createData = await createTask(apiKey, apiBase, args.query, timeoutMs);
|
|
190
|
+
const taskId = createData.task_id;
|
|
191
|
+
if (args.verbose) {
|
|
192
|
+
console.error(`Task ID: ${taskId}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const startAt = Date.now();
|
|
196
|
+
let lastStatus = '';
|
|
197
|
+
|
|
198
|
+
while (Date.now() - startAt <= maxWaitMs) {
|
|
199
|
+
const historicalData = await queryHistorical(apiKey, apiBase, taskId, timeoutMs);
|
|
200
|
+
const taskStatus = normalizeStatus(historicalData.task_status || historicalData.status);
|
|
201
|
+
const urls = extractTaskUrls(historicalData, createData);
|
|
202
|
+
lastStatus = taskStatus || 'UNKNOWN';
|
|
203
|
+
|
|
204
|
+
if (args.verbose) {
|
|
205
|
+
const elapsedSec = Math.floor((Date.now() - startAt) / 1000);
|
|
206
|
+
console.error(`[${elapsedSec}s] Status: ${lastStatus}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (taskStatus === 'COMPLETED' || taskStatus === 'SUCCESS') {
|
|
210
|
+
if (!urls.displayUrl) {
|
|
211
|
+
throw new Error('Task completed but no ppt_url/live_doc_url is available');
|
|
212
|
+
}
|
|
213
|
+
if (args.json) {
|
|
214
|
+
console.log(
|
|
215
|
+
JSON.stringify(
|
|
216
|
+
{
|
|
217
|
+
status: 'ok',
|
|
218
|
+
data: {
|
|
219
|
+
task_id: taskId,
|
|
220
|
+
task_status: taskStatus,
|
|
221
|
+
ppt_url: urls.pptUrl || null,
|
|
222
|
+
live_doc_url: urls.liveDocUrl || null,
|
|
223
|
+
livedoc_short_id: createData.livedoc_short_id ?? historicalData.live_doc_short_id ?? historicalData.livedoc_short_id ?? null,
|
|
224
|
+
ppt_business_id: createData.ppt_business_id ?? null,
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
null,
|
|
228
|
+
2
|
|
229
|
+
)
|
|
230
|
+
);
|
|
231
|
+
} else {
|
|
232
|
+
console.log(urls.displayUrl);
|
|
233
|
+
}
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (taskStatus === 'FAILED' || taskStatus === 'ERROR') {
|
|
238
|
+
throw new Error(`Task finished with status: ${taskStatus}`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
await sleep(intervalMs);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
throw new Error(`Timed out after ${args.maxWaitSec}s. Last status: ${lastStatus || 'UNKNOWN'}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
main().catch((err) => {
|
|
248
|
+
console.error(`ERROR: ${err?.message || err}`);
|
|
249
|
+
process.exit(1);
|
|
250
|
+
});
|
|
251
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Felo SuperAgent Skill Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Felo SuperAgent Skill for Claude Code
|
|
2
|
+
|
|
3
|
+
**AI 对话与流式输出,支持 LiveDoc 与连续会话。**
|
|
4
|
+
|
|
5
|
+
通过 Felo Open Platform 的 SuperAgent API,在 Claude Code 中发起与 SuperAgent 的对话、接收 SSE 流式回复,并可关联 LiveDoc、查询会话详情与资源。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 功能
|
|
10
|
+
|
|
11
|
+
- **流式对话**:创建会话后通过 SSE 实时接收 AI 回复
|
|
12
|
+
- **LiveDoc 关联**:每次会话对应一个 LiveDoc,可后续查看资源
|
|
13
|
+
- **连续对话**:通过 `live_doc_short_id` 在已有 LiveDoc 上继续提问
|
|
14
|
+
- **多语言**:支持 `accept_language`(如 zh / en)
|
|
15
|
+
|
|
16
|
+
**适用场景:**
|
|
17
|
+
|
|
18
|
+
- 需要 SuperAgent 流式回答
|
|
19
|
+
- 需要与 LiveDoc 关联、可追溯资源的对话
|
|
20
|
+
- 多轮/连续对话(复用同一 LiveDoc)
|
|
21
|
+
|
|
22
|
+
**不适用:**
|
|
23
|
+
|
|
24
|
+
- 仅需单次实时信息检索 → 使用 `felo-search`
|
|
25
|
+
- 仅需抓取网页正文 → 使用 `felo-web-fetch`
|
|
26
|
+
- 仅需生成 PPT → 使用 `felo-slides`
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 快速开始
|
|
31
|
+
|
|
32
|
+
### 1. 安装
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx @claude/skills add felo-superAgent
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
(若为本地 skill,确保 Cursor/Claude Code 已配置该 skill 路径。)
|
|
39
|
+
|
|
40
|
+
### 2. 配置 API Key
|
|
41
|
+
|
|
42
|
+
与其它 Felo skills 相同,使用同一 API Key:
|
|
43
|
+
|
|
44
|
+
1. 打开 [felo.ai](https://felo.ai) 登录
|
|
45
|
+
2. 头像 → **Settings** → **API Keys** → 创建并复制 Key
|
|
46
|
+
3. 设置环境变量:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Linux/macOS
|
|
50
|
+
export FELO_API_KEY="your-api-key-here"
|
|
51
|
+
|
|
52
|
+
# Windows PowerShell
|
|
53
|
+
$env:FELO_API_KEY="your-api-key-here"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 3. 使用方式
|
|
57
|
+
|
|
58
|
+
**在对话中触发:**
|
|
59
|
+
|
|
60
|
+
- 明确指令:`/felo-superagent`、"use felo super agent"
|
|
61
|
+
- 描述意图:SuperAgent 对话、流式对话、LiveDoc 对话、连续对话
|
|
62
|
+
|
|
63
|
+
**命令行直接跑脚本:**
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
node felo-superAgent/scripts/run_superagent.mjs --query "What is the latest news about AI?"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
输出为流式汇总后的完整回答正文。加 `--json` 可得到包含 `thread_short_id`、`live_doc_short_id` 的 JSON。
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 脚本参数
|
|
74
|
+
|
|
75
|
+
| 参数 | 说明 |
|
|
76
|
+
| -------------------------- | ---------------------------------------------------------- |
|
|
77
|
+
| `--query <text>` | 用户问题(必填,1–2000 字符) |
|
|
78
|
+
| `--live-doc-id <id>` | 复用已有 LiveDoc short_id(连续对话) |
|
|
79
|
+
| `--accept-language <lang>` | 语言偏好,如 zh / en |
|
|
80
|
+
| `--timeout <seconds>` | 请求/流超时,默认 60 |
|
|
81
|
+
| `--json` | 输出 JSON(含 answer、thread_short_id、live_doc_short_id) |
|
|
82
|
+
| `--verbose` | 将流连接信息打到 stderr |
|
|
83
|
+
| `--help` | 显示帮助 |
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 输出格式
|
|
88
|
+
|
|
89
|
+
**默认(纯文本):**
|
|
90
|
+
脚本 stdout 为完整回答内容(由 SSE `message` 事件拼接)。
|
|
91
|
+
|
|
92
|
+
**`--json`:**
|
|
93
|
+
单行 JSON 对象,例如:
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"status": "ok",
|
|
98
|
+
"data": {
|
|
99
|
+
"answer": "完整回答内容...",
|
|
100
|
+
"thread_short_id": "TvyKouzJirXjFdst4uKRK3",
|
|
101
|
+
"live_doc_short_id": "PvyKouzJirXjFdst4uKRK3"
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
可用 `thread_short_id` 调用「查询会话详情」接口,用 `live_doc_short_id` 调用「列举 LiveDoc 资源」等接口。
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## 错误处理
|
|
111
|
+
|
|
112
|
+
常见错误码参见 [SuperAgent API 文档](https://openapi.felo.ai/docs/api-reference/v2/superagent.html):
|
|
113
|
+
|
|
114
|
+
- `INVALID_API_KEY` (401):Key 无效或已撤销
|
|
115
|
+
- `SUPER_AGENT_CONVERSATION_CREATE_FAILED` (502):创建会话失败
|
|
116
|
+
- 其它 502:下游服务异常,可重试或联系支持
|
|
117
|
+
|
|
118
|
+
若未配置 `FELO_API_KEY`,脚本会报错并提示配置方法。
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## 参考链接
|
|
123
|
+
|
|
124
|
+
- [SuperAgent API 文档](https://openapi.felo.ai/docs/api-reference/v2/superagent.html)
|
|
125
|
+
- [Felo Open Platform](https://openapi.felo.ai/docs/)
|