felo-ai 0.2.15 → 0.2.17

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/CHANGELOG.md CHANGED
@@ -5,7 +5,41 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [0.2.12] - 2026-03-102
8
+ ## [0.2.17] - 2026-03-14
9
+
10
+ ### Added
11
+
12
+ - **`felo livedoc route <id>`**: route relevant resource IDs by query for targeted retrieval; supports `--max-resources`
13
+ - **`felo livedoc retrieve` `--resource-ids`**: specify resource IDs to search within (comma-separated, max 50); auto-routes when omitted
14
+
15
+ ### Changed
16
+
17
+ - `felo livedoc retrieve`: renamed request field `content` to `query` to align with backend API
18
+
19
+ ### Fixed
20
+
21
+ - Fixed truncated README in `felo-livedoc` (was cut off at 53 lines)
22
+
23
+ ---
24
+
25
+ ## [0.2.14] - 2026-03-13
26
+
27
+ ### Added
28
+
29
+ - **`felo livedoc` command**: full CRUD for LiveDocs (knowledge bases) — `create`, `list`, `update`, `delete`
30
+ - **`felo livedoc` resource management**: `add-doc`, `add-urls`, `upload`, `resources`, `resource`, `remove-resource`
31
+ - **`felo livedoc retrieve <id>`**: semantic search across resources in a LiveDoc
32
+ - **`felo superagent` new options**: `--thread-id` (follow-up conversation), `--skill-id`, `--selected-resource-ids`, `--ext`
33
+ - **`FELO_API_BASE` config persistence**: support `felo config set FELO_API_BASE <url>`, priority: env > config > default
34
+
35
+ ### Changed
36
+
37
+ - SuperAgent SSE `type=processing` events are now silently ignored
38
+ - Replaced all hardcoded Chinese strings with English in superAgent
39
+
40
+ ---
41
+
42
+ ## [0.2.12] - 2026-03-10
9
43
 
10
44
  Streamline the process and reduce the need for confirmation and selection.
11
45
 
@@ -0,0 +1,78 @@
1
+ # Felo Content to Slides Skill
2
+
3
+ Fetch content from a webpage or YouTube video, then generate a PPT from that content. Combines this repo's web-fetch, youtube-subtitling, and slides capabilities.
4
+
5
+ ## Install Skill (Claude Code)
6
+
7
+ **One-line install:**
8
+
9
+ ```bash
10
+ npx skills add Felo-Inc/felo-skills --skill felo-content-to-slides
11
+ ```
12
+
13
+ **Manual install:** Copy this directory into Claude Code's skills folder:
14
+
15
+ ```bash
16
+ # Linux/macOS
17
+ cp -r felo-content-to-slides ~/.claude/skills/
18
+
19
+ # Windows (PowerShell)
20
+ Copy-Item -Recurse felo-content-to-slides "$env:USERPROFILE\.claude\skills\"
21
+ ```
22
+
23
+ ## Notes
24
+
25
+ - **Generation time:** PPT generation usually takes **3–5 minutes** or more; please wait. Default max wait is 20 minutes (adjust with `--poll-timeout`).
26
+
27
+ ## Configuration
28
+
29
+ Same as other Felo commands: set `FELO_API_KEY` (and optionally `FELO_API_BASE`).
30
+
31
+ ```bash
32
+ felo config set FELO_API_KEY your-api-key-here
33
+ # or
34
+ export FELO_API_KEY="your-api-key-here" # Linux/macOS
35
+ $env:FELO_API_KEY="your-api-key-here" # Windows PowerShell
36
+ ```
37
+
38
+ ## Trigger
39
+
40
+ - Intent: "fetch page/video and make PPT", "turn URL into slides", "generate presentation from link"
41
+ - Explicit: `/felo-content-to-slides`
42
+
43
+ ## Using the CLI
44
+
45
+ After installing **felo-ai**:
46
+
47
+ ```bash
48
+ npm install -g felo-ai
49
+ felo content-to-slides -u "https://example.com/article" --readability
50
+ felo content-to-slides -v "https://www.youtube.com/watch?v=ID" --extra-prompt "max 10 slides"
51
+ ```
52
+
53
+ From this repo root (no global install):
54
+
55
+ ```bash
56
+ node src/cli.js content-to-slides --url "https://openclaw.ai/" --readability
57
+ node src/cli.js content-to-slides --video "https://www.youtube.com/watch?v=xxx" -l zh-CN
58
+ ```
59
+
60
+ ### Options
61
+
62
+ | Option | Flag | Description |
63
+ |--------|------|-------------|
64
+ | Web page URL | `-u`, `--url` | URL to fetch and turn into slides |
65
+ | Video URL/ID | `-v`, `--video` | YouTube link or video ID (uses subtitles as content) |
66
+ | Extra instructions | `--extra-prompt` | e.g. "max 10 slides", "focus on conclusions" |
67
+ | Readability | `--readability` | For `--url` only: extract main body only |
68
+ | Subtitle language | `-l`, `--language` | For `--video` only: e.g. zh-CN, en |
69
+ | Fetch timeout | `-t`, `--timeout` | Seconds (default 60) |
70
+ | Poll timeout | `--poll-timeout` | Max seconds to wait for PPT (default 1200) |
71
+ | JSON output | `-j`, `--json` | Output task_id, ppt_url, live_doc_url |
72
+ | Verbose | `--verbose` | Print polling status |
73
+
74
+ ## Links
75
+
76
+ - [SKILL.md](SKILL.md) – Full agent instructions
77
+ - [Felo Open Platform](https://openapi.felo.ai/docs/)
78
+ - [felo-web-fetch](../felo-web-fetch/) | [felo-youtube-subtitling](../felo-youtube-subtitling/) | [felo-slides](../felo-slides/)
@@ -0,0 +1,102 @@
1
+ ---
2
+ name: felo-content-to-slides
3
+ description: "Fetch content from a webpage or YouTube video, then generate a PPT from that content using Felo APIs. Use when the user wants to turn a URL (article/page or YouTube) into a presentation, or explicit /felo-content-to-slides."
4
+ ---
5
+
6
+ # Felo Content to Slides Skill
7
+
8
+ ## When to Use
9
+
10
+ Trigger this skill when the user wants to:
11
+
12
+ - Turn a **webpage URL** or **article link** into a presentation
13
+ - Turn **YouTube video** (subtitles/transcript) into a PPT
14
+ - Fetch page/video content and generate slides from it
15
+
16
+ Trigger keywords (examples):
17
+
18
+ - Turn URL into slides, fetch page and make presentation, video transcript to PPT
19
+ - Explicit: `/felo-content-to-slides`, "use felo content to slides"
20
+
21
+ Do NOT use for:
22
+
23
+ - Only fetching content without PPT (use `felo-web-fetch` or `felo-youtube-subtitling`)
24
+ - Only generating PPT from a topic/outline (use `felo-slides`)
25
+ - Real-time search (use `felo-search`)
26
+
27
+ ## Setup
28
+
29
+ Same as other Felo skills: set `FELO_API_KEY` (and optionally `FELO_API_BASE`). No extra setup.
30
+
31
+ ## How to Execute
32
+
33
+ Use the **CLI** from this repo. From the **felo-skills repo root**:
34
+
35
+ ```bash
36
+ node src/cli.js content-to-slides --url "https://example.com/article" [options]
37
+ # or for YouTube:
38
+ node src/cli.js content-to-slides --video "https://www.youtube.com/watch?v=ID" [options]
39
+ ```
40
+
41
+ If **felo-ai** is installed globally (`npm install -g felo-ai`):
42
+
43
+ ```bash
44
+ felo content-to-slides -u "https://example.com/article" [options]
45
+ felo content-to-slides -v "https://www.youtube.com/watch?v=ID" [options]
46
+ ```
47
+
48
+ ### Options
49
+
50
+ | Option | Description |
51
+ |--------|-------------|
52
+ | `-u, --url <url>` | Web page URL to fetch and turn into slides |
53
+ | `-v, --video <url-or-id>` | YouTube video URL or ID (use subtitles as content) |
54
+ | `--extra-prompt <text>` | Extra instructions for PPT (e.g. max 10 slides) |
55
+ | `--readability` | For --url: use readability (main content only) |
56
+ | `-l, --language <code>` | For --video: subtitle language (e.g. en, zh-CN) |
57
+ | `-t, --timeout <seconds>` | Fetch timeout (default 60) |
58
+ | `--poll-timeout <seconds>` | Max seconds to wait for PPT task (default 1200) |
59
+ | `-j, --json` | Output JSON with task_id and ppt/live_doc URL |
60
+ | `--verbose` | Show polling status |
61
+
62
+ Provide **either** `--url` or `--video`, not both.
63
+
64
+ ### Examples
65
+
66
+ ```bash
67
+ # Web page → PPT (with readability)
68
+ node src/cli.js content-to-slides --url "https://openclaw.ai/" --readability
69
+
70
+ # YouTube → PPT, with extra instruction
71
+ node src/cli.js content-to-slides -v "https://www.youtube.com/watch?v=xxx" --extra-prompt "max 10 slides"
72
+
73
+ # With JSON output
74
+ felo content-to-slides -u "https://example.com/article" --readability -j
75
+ ```
76
+
77
+ ## Output Format
78
+
79
+ On success:
80
+
81
+ ```markdown
82
+ ## Content to Slides Done
83
+
84
+ - Source: <webpage URL or YouTube video>
85
+ - Task ID: <task_id>
86
+ - PPT URL: <ppt_url>
87
+ - Live Doc: <live_doc_url or N/A>
88
+ ```
89
+
90
+ On failure: state which step failed (fetch / PPT generation) and the error message; suggest checking URL, network, or API key.
91
+
92
+ ## Important Notes
93
+
94
+ - Always check `FELO_API_KEY` before running (or prompt user to run `felo config set FELO_API_KEY <key>`).
95
+ - For long articles or transcripts, content is truncated; if PPT create fails with size error, the user may retry with a shorter page or different URL.
96
+ - Web fetch may need `--timeout` for slow sites; YouTube may have no subtitles for some videos—report clearly.
97
+
98
+ ## References
99
+
100
+ - Content fetch: felo-web-fetch, felo-youtube-subtitling (same repo)
101
+ - PPT generation: felo-slides (same repo)
102
+ - [Felo Open Platform](https://openapi.felo.ai/docs/)
@@ -9,12 +9,14 @@ Manage knowledge bases (LiveDocs) and their resources via the Felo API.
9
9
  - Create, list, update, and delete knowledge bases (LiveDocs)
10
10
  - Add resources: text documents, URLs, file uploads
11
11
  - Semantic retrieval across knowledge base resources
12
+ - Route relevant resources by query for targeted retrieval
12
13
  - Full CRUD for resources within a LiveDoc
13
14
 
14
15
  **When to use:**
15
16
  - Building or managing a knowledge base
16
17
  - Uploading documents or URLs for AI-powered retrieval
17
18
  - Searching across your knowledge base with natural language
19
+ - Routing relevant resources before targeted retrieval
18
20
 
19
21
  **When NOT to use:**
20
22
  - General web search (use `felo-search`)
@@ -51,3 +53,45 @@ felo livedoc list
51
53
  ```bash
52
54
  # Create a knowledge base
53
55
  felo livedoc create --name "My KB" --description "Project docs"
56
+
57
+ # Add a URL resource
58
+ felo livedoc add-urls SHORT_ID --urls "https://example.com"
59
+
60
+ # Upload a file
61
+ felo livedoc upload SHORT_ID --file ./report.pdf
62
+
63
+ # Semantic retrieval across all resources (auto-routes)
64
+ felo livedoc retrieve SHORT_ID --query "latest AI research"
65
+
66
+ # Retrieve within specific resources
67
+ felo livedoc retrieve SHORT_ID --query "latest AI research" --resource-ids "id1,id2"
68
+
69
+ # Route relevant resources by query
70
+ felo livedoc route SHORT_ID --query "latest AI research"
71
+ felo livedoc route SHORT_ID --query "latest AI research" --max-resources 5
72
+ ```
73
+
74
+ ---
75
+
76
+ ## Commands
77
+
78
+ | Command | Description |
79
+ |---------|-------------|
80
+ | `create` | Create a new LiveDoc |
81
+ | `list` | List all LiveDocs |
82
+ | `update <short_id>` | Update a LiveDoc |
83
+ | `delete <short_id>` | Delete a LiveDoc |
84
+ | `resources <short_id>` | List resources in a LiveDoc |
85
+ | `resource <short_id> <resource_id>` | Get a single resource |
86
+ | `add-doc <short_id>` | Create a text document resource |
87
+ | `add-urls <short_id>` | Add URL resources (max 10) |
88
+ | `upload <short_id>` | Upload a file resource |
89
+ | `remove-resource <short_id> <resource_id>` | Delete a resource |
90
+ | `retrieve <short_id>` | Semantic retrieval (auto-routes if no `--resource-ids`) |
91
+ | `route <short_id>` | Route relevant resource IDs by query |
92
+
93
+ ---
94
+
95
+ ## License
96
+
97
+ MIT
@@ -12,10 +12,11 @@ Trigger this skill when users want to:
12
12
  - **Create/manage knowledge bases:** Create, list, update, or delete LiveDocs
13
13
  - **Add resources:** Upload documents, add URLs, or create text documents in a LiveDoc
14
14
  - **Semantic retrieval:** Search across knowledge base resources using natural language queries
15
+ - **Route resources:** Find relevant resource IDs by query for targeted retrieval
15
16
  - **Resource management:** List, view, or delete resources within a LiveDoc
16
17
 
17
18
  **Trigger words:**
18
- - English: knowledge base, livedoc, live doc, upload document, add URL, semantic search, retrieve, knowledge retrieval
19
+ - English: knowledge base, livedoc, live doc, upload document, add URL, semantic search, retrieve, knowledge retrieval, route resources
19
20
  - 简体中文: 知识库, 文档库, 上传文档, 添加链接, 语义检索, 知识检索
20
21
 
21
22
  **Explicit commands:** `/felo-livedoc`, "livedoc", "felo livedoc"
@@ -110,10 +111,21 @@ node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs remove-resource SHORT
110
111
 
111
112
  ### Semantic Retrieval
112
113
 
113
- **Search across resources:**
114
+ **Route relevant resources by query:**
115
+ ```bash
116
+ node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs route SHORT_ID --query "your search query"
117
+ node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs route SHORT_ID --query "your search query" --max-resources 5
118
+ ```
119
+
120
+ **Search across all resources (auto-routes):**
114
121
  ```bash
115
122
  node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs retrieve SHORT_ID --query "your search query"
116
123
  ```
124
+
125
+ **Search within specific resources:**
126
+ ```bash
127
+ node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs retrieve SHORT_ID --query "your search query" --resource-ids "id1,id2,id3"
128
+ ```
117
129
  ### Options
118
130
 
119
131
  All commands support:
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "Felo LiveDoc",
3
+ "tagline": "Manage knowledge bases (LiveDocs) and semantic retrieval via Felo API in Claude Code",
4
+ "description": "Felo LiveDoc lets you create, list, update, and delete knowledge bases (LiveDocs), add resources (text, URLs, file uploads), and run semantic retrieval over your knowledge base from Claude Code. Full CRUD for LiveDocs and resources. Requires a Felo API key.",
5
+ "category": "data",
6
+ "tags": ["felo", "livedoc", "knowledge-base", "semantic-search", "retrieval", "api", "documents"],
7
+ "version": "1.0.0",
8
+ "license": "MIT",
9
+ "pricing": "free",
10
+ "support_url": "https://github.com/Felo-Inc/felo-skills/issues",
11
+ "homepage": "https://github.com/Felo-Inc/felo-skills"
12
+ }
@@ -143,7 +143,8 @@ function usage() {
143
143
  ' add-urls <short_id> Add URLs (--urls required, comma-separated, max 10)',
144
144
  ' upload <short_id> Upload file (--file required, --convert optional)',
145
145
  ' remove-resource <short_id> <resource_id> Delete a resource',
146
- ' retrieve <short_id> Semantic search (--query required)',
146
+ ' retrieve <short_id> Semantic search (--query required, --resource-ids optional)',
147
+ ' route <short_id> Route relevant resources by query (--query required)',
147
148
  '',
148
149
  'Options:',
149
150
  ' --name <name> LiveDoc name',
@@ -158,7 +159,9 @@ function usage() {
158
159
  ' --urls <urls> Comma-separated URLs',
159
160
  ' --file <path> File path to upload',
160
161
  ' --convert Convert uploaded file to document',
161
- ' --query <text> Retrieval query',
162
+ ' --query <text> Retrieval/route query',
163
+ ' --resource-ids <ids> Comma-separated resource IDs to search within (retrieve)',
164
+ ' --max-resources <n> Max resources to return (route)',
162
165
  ' -j, --json Output raw JSON',
163
166
  ' -t, --timeout <ms> Timeout in ms (default: 60000)',
164
167
  ' --help Show this help',
@@ -168,7 +171,7 @@ function parseArgs(argv) {
168
171
  const out = {
169
172
  action: '', positional: [], name: '', description: '', icon: '',
170
173
  keyword: '', page: '', size: '', type: '', content: '', title: '',
171
- urls: '', file: '', convert: false, query: '',
174
+ urls: '', file: '', convert: false, query: '', resourceIds: '', maxResources: '',
172
175
  json: false, timeoutMs: DEFAULT_TIMEOUT_MS, help: false,
173
176
  };
174
177
  const positional = [];
@@ -189,6 +192,8 @@ function parseArgs(argv) {
189
192
  else if (a === '--urls') out.urls = argv[++i] || '';
190
193
  else if (a === '--file') out.file = argv[++i] || '';
191
194
  else if (a === '--query') out.query = argv[++i] || '';
195
+ else if (a === '--resource-ids') out.resourceIds = argv[++i] || '';
196
+ else if (a === '--max-resources') out.maxResources = argv[++i] || '';
192
197
  else if (a === '-t' || a === '--timeout') {
193
198
  const n = parseInt(argv[++i] || '', 10);
194
199
  if (Number.isFinite(n) && n > 0) out.timeoutMs = n;
@@ -359,7 +364,11 @@ async function main() {
359
364
  if (!shortId) { console.error('ERROR: short_id is required'); break; }
360
365
  if (!args.query) { console.error('ERROR: --query is required'); break; }
361
366
  spinnerId = startSpinner('Retrieving from knowledge base');
362
- const payload = await apiRequest('POST', `/livedocs/${shortId}/resources/retrieve`, { content: args.query }, apiKey, apiBase, timeoutMs);
367
+ const body = { query: args.query };
368
+ if (args.resourceIds) {
369
+ body.resource_ids = args.resourceIds.split(',').map(id => id.trim()).filter(Boolean);
370
+ }
371
+ const payload = await apiRequest('POST', `/livedocs/${shortId}/resources/retrieve`, body, apiKey, apiBase, timeoutMs);
363
372
  if (json) { console.log(JSON.stringify(payload, null, 2)); }
364
373
  else {
365
374
  const results = payload?.data || [];
@@ -372,6 +381,28 @@ async function main() {
372
381
  code = 0;
373
382
  break;
374
383
  }
384
+ case 'route': {
385
+ if (!shortId) { console.error('ERROR: short_id is required'); break; }
386
+ if (!args.query) { console.error('ERROR: --query is required'); break; }
387
+ spinnerId = startSpinner('Routing relevant resources');
388
+ const body = { query: args.query };
389
+ if (args.maxResources) {
390
+ const n = parseInt(args.maxResources, 10);
391
+ if (Number.isFinite(n) && n > 0) body.max_resources = n;
392
+ }
393
+ const payload = await apiRequest('POST', `/livedocs/${shortId}/resources/route`, body, apiKey, apiBase, timeoutMs);
394
+ if (json) { console.log(JSON.stringify(payload, null, 2)); }
395
+ else {
396
+ const resourceIds = payload?.data || [];
397
+ if (!resourceIds.length) { process.stderr.write('No relevant resources found.\n'); }
398
+ else {
399
+ process.stdout.write(`Found ${resourceIds.length} relevant resource(s):\n\n`);
400
+ for (const id of resourceIds) process.stdout.write(`- ${id}\n`);
401
+ }
402
+ }
403
+ code = 0;
404
+ break;
405
+ }
375
406
  default:
376
407
  console.error(`Unknown action: ${action}`);
377
408
  usage();
@@ -10,8 +10,10 @@
10
10
 
11
11
  - **流式对话**:创建会话后通过 SSE 实时接收 AI 回复
12
12
  - **LiveDoc 关联**:每次会话对应一个 LiveDoc,可后续查看资源
13
- - **连续对话**:通过 `live_doc_short_id` 在已有 LiveDoc 上继续提问
13
+ - **连续对话**:通过 `--thread-id` 在已有会话上继续提问
14
+ - **LiveDoc 管理**:列举 LiveDoc 列表、查看指定 LiveDoc 下的资源
14
15
  - **多语言**:支持 `accept_language`(如 zh / en)
16
+ - **工具调用**:支持生图、研究报告、文档、PPT、HTML、Twitter 搜索等工具
15
17
 
16
18
  **适用场景:**
17
19
 
@@ -81,19 +83,60 @@ node felo-superAgent/scripts/run_superagent.mjs --query "What is the latest news
81
83
 
82
84
  输出为流式汇总后的完整回答正文。加 `--json` 可得到包含 `thread_short_id`、`live_doc_short_id` 的 JSON。
83
85
 
86
+ **CLI 命令(安装后):**
87
+
88
+ ```bash
89
+ # SuperAgent 对话
90
+ felo superagent "What is the latest news about AI?"
91
+
92
+ # 继续对话
93
+ felo superagent "Tell me more" --thread-id <thread_short_id>
94
+
95
+ # 列举 LiveDoc 列表
96
+ felo livedocs
97
+ felo livedocs --page 2 --size 10
98
+ felo livedocs --keyword AI
99
+
100
+ # 查看指定 LiveDoc 下的资源
101
+ felo livedoc-resources <livedoc-id>
102
+ ```
103
+
84
104
  ---
85
105
 
86
106
  ## 脚本参数
87
107
 
88
- | 参数 | 说明 |
89
- | -------------------------- | ---------------------------------------------------------- |
90
- | `--query <text>` | 用户问题(必填,1–2000 字符) |
91
- | `--live-doc-id <id>` | 复用已有 LiveDoc short_id(连续对话) |
92
- | `--accept-language <lang>` | 语言偏好,如 zh / en |
93
- | `--timeout <seconds>` | 请求/流超时,默认 60 |
94
- | `--json` | 输出 JSON(含 answer、thread_short_id、live_doc_short_id) |
95
- | `--verbose` | 将流连接信息打到 stderr |
96
- | `--help` | 显示帮助 |
108
+ ### superagent
109
+
110
+ | 参数 | 说明 |
111
+ | --------------------------------- | ---------------------------------------------------------- |
112
+ | `--query <text>` | 用户问题(必填,1–2000 字符) |
113
+ | `--thread-id <id>` | 已有会话 ID,用于继续对话 |
114
+ | `--live-doc-id <id>` | 复用已有 LiveDoc short_id(连续对话) |
115
+ | `--skill-id <id>` | Skill ID(仅新建会话时有效) |
116
+ | `--selected-resource-ids <ids>` | 逗号分隔的资源 ID(仅新建会话时有效) |
117
+ | `--ext <json>` | 额外参数 JSON,如 `'{"style_id":"xxx"}'`(仅新建会话时有效)|
118
+ | `--accept-language <lang>` | 语言偏好,如 zh / en |
119
+ | `--timeout <seconds>` | 请求/流超时,默认 60 |
120
+ | `--json` | 输出 JSON(含 answer、thread_short_id、live_doc_short_id) |
121
+ | `--verbose` | 将流连接信息打到 stderr |
122
+
123
+ ### livedocs
124
+
125
+ | 参数 | 说明 |
126
+ | ----------------------- | ------------------------ |
127
+ | `-p, --page <number>` | 页码,默认 1 |
128
+ | `-s, --size <number>` | 每页条数,默认 20 |
129
+ | `-k, --keyword <text>` | 关键词过滤 |
130
+ | `-j, --json` | 输出原始 JSON |
131
+ | `-t, --timeout <seconds>` | 请求超时,默认 60 |
132
+
133
+ ### livedoc-resources
134
+
135
+ | 参数 | 说明 |
136
+ | -------------------------- | ------------------------ |
137
+ | `<livedoc-id>` | LiveDoc short_id(必填) |
138
+ | `-j, --json` | 输出原始 JSON |
139
+ | `-t, --timeout <seconds>` | 请求超时,默认 60 |
97
140
 
98
141
  ---
99
142
 
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "Felo SuperAgent",
3
+ "tagline": "AI conversation with real-time SSE streaming and LiveDoc in Claude Code",
4
+ "description": "Felo SuperAgent lets you chat with Felo SuperAgent via the Open API from Claude Code: create sessions, get SSE streamed replies, and continue conversations on the same LiveDoc. Supports thread/LiveDoc management, multi-language, and tools (image gen, reports, PPT, Twitter search). Requires a Felo API key.",
5
+ "category": "conversation",
6
+ "tags": ["felo", "superagent", "chat", "streaming", "livedoc", "api", "sse"],
7
+ "version": "1.0.0",
8
+ "license": "MIT",
9
+ "pricing": "free",
10
+ "support_url": "https://github.com/Felo-Inc/felo-skills/issues",
11
+ "homepage": "https://github.com/Felo-Inc/felo-skills"
12
+ }
@@ -4,7 +4,7 @@
4
4
  "description": "Felo X Search lets you look up X (Twitter) user profiles, search users and tweets, fetch user timelines, and get tweet replies directly from Claude Code. Uses the Felo Open API; requires a Felo API key. Supports user lookup, user search, user tweets, tweet search, and tweet replies with readable Markdown or raw JSON output.",
5
5
  "category": "data",
6
6
  "tags": ["twitter", "x", "search", "social", "felo", "api"],
7
- "version": "1.0.0",
7
+ "version": "1.0.1",
8
8
  "license": "MIT",
9
9
  "pricing": "free",
10
10
  "support_url": "https://github.com/Felo-Inc/felo-skills/issues",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "felo-ai",
3
- "version": "0.2.15",
4
- "description": "Felo AI CLI - real-time search, PPT generation, web fetch, YouTube subtitles, LiveDoc knowledge base from the terminal",
3
+ "version": "0.2.17",
4
+ "description": "Felo AI CLI - real-time search, PPT generation, SuperAgent conversation, LiveDoc management, web fetch, YouTube subtitles, LiveDoc knowledge base, and X (Twitter) search from the terminal",
5
5
  "type": "module",
6
6
  "main": "src/cli.js",
7
7
  "bin": {
@@ -15,6 +15,8 @@
15
15
  "felo-ai",
16
16
  "search",
17
17
  "slides",
18
+ "superagent",
19
+ "livedoc",
18
20
  "web-fetch",
19
21
  "youtube-subtitles",
20
22
  "x-search",
package/src/cli.js CHANGED
@@ -4,9 +4,10 @@ import { createRequire } from "module";
4
4
  import { Command } from "commander";
5
5
  import { search } from "./search.js";
6
6
  import { slides } from "./slides.js";
7
- import { superAgent } from "./superAgent.js";
7
+ import { superAgent, listLiveDocs, listLiveDocResources } from "./superAgent.js";
8
8
  import { webFetch } from "./webFetch.js";
9
9
  import { youtubeSubtitling } from "./youtubeSubtitling.js";
10
+ import { contentToSlides } from "./contentToSlides.js";
10
11
  import * as xSearch from "./xSearch.js";
11
12
  import * as livedoc from "./livedoc.js";
12
13
  import * as config from "./config.js";
@@ -109,8 +110,21 @@ program
109
110
  .option("-t, --timeout <seconds>", "request/stream timeout in seconds", "60")
110
111
  .option("--live-doc-id <id>", "reuse existing LiveDoc short_id for continuous conversation")
111
112
  .option("--thread-id <id>", "existing thread/conversation ID for follow-up questions")
113
+ .option("--skill-id <id>", "skill ID (only for new conversations)")
114
+ .option("--selected-resource-ids <ids>", "comma-separated resource IDs (only for new conversations)")
115
+ .option("--ext <json>", "extra params as JSON string, e.g. '{\"style_id\":\"xxx\"}' (only for new conversations)")
112
116
  .option("--accept-language <lang>", "language preference (e.g. zh, en)")
113
117
  .action(async (query, opts) => {
118
+ let ext;
119
+ if (opts.ext) {
120
+ try {
121
+ ext = JSON.parse(opts.ext);
122
+ } catch {
123
+ console.error('Error: --ext must be valid JSON');
124
+ flushStdioThenExit(1);
125
+ return;
126
+ }
127
+ }
114
128
  const timeoutMs = parseInt(opts.timeout, 10) * 1000;
115
129
  const code = await superAgent(query, {
116
130
  json: opts.json,
@@ -118,12 +132,52 @@ program
118
132
  timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
119
133
  liveDocId: opts.liveDocId || undefined,
120
134
  threadId: opts.threadId || undefined,
135
+ skillId: opts.skillId || undefined,
136
+ selectedResourceIds: opts.selectedResourceIds ? opts.selectedResourceIds.split(',').map(s => s.trim()).filter(Boolean) : undefined,
137
+ ext,
121
138
  acceptLanguage: opts.acceptLanguage || undefined,
122
139
  });
123
140
  process.exitCode = code;
124
141
  flushStdioThenExit(code);
125
142
  });
126
143
 
144
+ program
145
+ .command("livedocs")
146
+ .description("List LiveDocs with pagination and optional keyword filtering")
147
+ .option("-p, --page <number>", "page number", "1")
148
+ .option("-s, --size <number>", "page size", "20")
149
+ .option("-k, --keyword <keyword>", "keyword filter")
150
+ .option("-j, --json", "output raw JSON")
151
+ .option("-t, --timeout <seconds>", "request timeout in seconds", "60")
152
+ .action(async (opts) => {
153
+ const timeoutMs = parseInt(opts.timeout, 10) * 1000;
154
+ const code = await listLiveDocs({
155
+ page: parseInt(opts.page, 10) || 1,
156
+ size: parseInt(opts.size, 10) || 20,
157
+ keyword: opts.keyword || undefined,
158
+ json: opts.json,
159
+ timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
160
+ });
161
+ process.exitCode = code;
162
+ flushStdioThenExit(code);
163
+ });
164
+
165
+ program
166
+ .command("livedoc-resources")
167
+ .description("List resources in a specific LiveDoc")
168
+ .argument("<livedoc-id>", "LiveDoc short_id")
169
+ .option("-j, --json", "output raw JSON")
170
+ .option("-t, --timeout <seconds>", "request timeout in seconds", "60")
171
+ .action(async (liveDocId, opts) => {
172
+ const timeoutMs = parseInt(opts.timeout, 10) * 1000;
173
+ const code = await listLiveDocResources(liveDocId, {
174
+ json: opts.json,
175
+ timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
176
+ });
177
+ process.exitCode = code;
178
+ flushStdioThenExit(code);
179
+ });
180
+
127
181
  const configCmd = program
128
182
  .command("config")
129
183
  .description(
@@ -260,6 +314,51 @@ program
260
314
  flushStdioThenExit(code);
261
315
  });
262
316
 
317
+ program
318
+ .command("content-to-slides")
319
+ .description(
320
+ "Fetch content from a webpage or YouTube video, then generate a PPT from that content"
321
+ )
322
+ .option("-u, --url <url>", "Web page URL to fetch and turn into slides")
323
+ .option(
324
+ "-v, --video <url-or-id>",
325
+ "YouTube video URL or ID (use subtitles as content)"
326
+ )
327
+ .option(
328
+ "--extra-prompt <text>",
329
+ "Extra instructions for PPT (e.g. 10页以内)"
330
+ )
331
+ .option("--readability", "For --url: use readability (main content only)")
332
+ .option(
333
+ "-l, --language <code>",
334
+ "For --video: subtitle language (e.g. en, zh-CN)"
335
+ )
336
+ .option("-t, --timeout <seconds>", "Fetch timeout in seconds", "60")
337
+ .option(
338
+ "--poll-timeout <seconds>",
339
+ "Max seconds to wait for PPT task",
340
+ "1200"
341
+ )
342
+ .option("-j, --json", "Output JSON with task_id and ppt/live_doc URL")
343
+ .option("--verbose", "Show polling status")
344
+ .action(async (opts) => {
345
+ const timeoutMs = parseInt(opts.timeout, 10) * 1000;
346
+ const pollTimeoutMs = parseInt(opts.pollTimeout, 10) * 1000;
347
+ const code = await contentToSlides({
348
+ url: opts.url,
349
+ video: opts.video,
350
+ extraPrompt: opts.extraPrompt,
351
+ readability: opts.readability,
352
+ language: opts.language,
353
+ timeoutMs: Number.isNaN(timeoutMs) ? 60_000 : timeoutMs,
354
+ pollTimeoutMs: Number.isNaN(pollTimeoutMs) ? 1_200_000 : pollTimeoutMs,
355
+ json: opts.json,
356
+ verbose: opts.verbose,
357
+ });
358
+ process.exitCode = code;
359
+ flushStdioThenExit(code);
360
+ });
361
+
263
362
  // ── X Search ──
264
363
  program
265
364
  .command("x")
@@ -0,0 +1,81 @@
1
+ import { getWebContent } from "./webFetch.js";
2
+ import { getYoutubeContent } from "./youtubeSubtitling.js";
3
+ import { slides } from "./slides.js";
4
+
5
+ const DEFAULT_PROMPT_PREFIX =
6
+ "Generate a presentation from the content below. Summarize key points and organize into clear slides:";
7
+ const MAX_CONTENT_LENGTH = 20_000;
8
+
9
+ /**
10
+ * Fetch content from URL (web or YouTube), then generate PPT.
11
+ * @param {Object} opts - { url?, video?, extraPrompt?, readability?, language?, timeoutMs?, pollTimeoutMs?, json?, verbose? }
12
+ * @returns {Promise<number>} exit code (0 or 1)
13
+ */
14
+ export async function contentToSlides(opts) {
15
+ const hasUrl = Boolean(opts?.url && String(opts.url).trim());
16
+ const hasVideo = Boolean(opts?.video && String(opts.video).trim());
17
+ if (!hasUrl && !hasVideo) {
18
+ process.stderr.write(
19
+ "ERROR: Provide either --url <page-url> or --video <youtube-url-or-id>\n"
20
+ );
21
+ return 1;
22
+ }
23
+ if (hasUrl && hasVideo) {
24
+ process.stderr.write("ERROR: Provide only one of --url or --video\n");
25
+ return 1;
26
+ }
27
+
28
+ const fetchTimeoutMs =
29
+ Number.isFinite(opts?.timeoutMs) && opts.timeoutMs > 0
30
+ ? opts.timeoutMs
31
+ : 60_000;
32
+
33
+ let content;
34
+ try {
35
+ if (hasUrl) {
36
+ process.stderr.write(`Fetching page: ${opts.url.slice(0, 60)}...\n`);
37
+ content = await getWebContent({
38
+ url: opts.url,
39
+ format: "markdown",
40
+ readability: opts?.readability ?? true,
41
+ timeoutMs: fetchTimeoutMs,
42
+ });
43
+ } else {
44
+ process.stderr.write("Fetching YouTube subtitles...\n");
45
+ content = await getYoutubeContent({
46
+ videoCode: opts.video,
47
+ language: opts?.language ?? "",
48
+ timeoutMs: fetchTimeoutMs,
49
+ });
50
+ }
51
+ } catch (err) {
52
+ process.stderr.write(`Fetch failed: ${err?.message || err}\n`);
53
+ return 1;
54
+ }
55
+
56
+ content = (content || "").trim();
57
+ if (!content) {
58
+ process.stderr.write(
59
+ "ERROR: No content fetched. Page may be empty or video has no subtitles.\n"
60
+ );
61
+ return 1;
62
+ }
63
+
64
+ if (content.length > MAX_CONTENT_LENGTH) {
65
+ content =
66
+ content.slice(0, MAX_CONTENT_LENGTH) + "\n\n[... content truncated ...]";
67
+ }
68
+
69
+ const promptSuffix = opts?.extraPrompt
70
+ ? `\n\nExtra instructions: ${opts.extraPrompt}`
71
+ : "";
72
+ const query = `${DEFAULT_PROMPT_PREFIX}\n\n${content}${promptSuffix}`;
73
+
74
+ process.stderr.write("Generating PPT (this may take a few minutes)...\n");
75
+ return slides(query, {
76
+ json: opts?.json,
77
+ verbose: opts?.verbose,
78
+ timeoutMs: fetchTimeoutMs,
79
+ pollTimeoutMs: opts?.pollTimeoutMs,
80
+ });
81
+ }
package/src/superAgent.js CHANGED
@@ -1,6 +1,6 @@
1
1
  const DEFAULT_API_BASE = 'https://openapi.felo.ai';
2
2
  const DEFAULT_TIMEOUT_MS = 60_000;
3
- /** 流式读取空闲超时:连续这么久未收到任何数据则断开,默认 5 分钟(生图等长任务需较久) */
3
+ /** Stream idle timeout: disconnect if no data received for this duration (default 2 hours for long tasks like image generation) */
4
4
  const STREAM_IDLE_TIMEOUT_MS = 2 * 60 * 60 * 1000;
5
5
  /** Tools whose params and results should be silently ignored. */
6
6
  const HIDDEN_TOOLS = new Set(['manage_outline']);
@@ -22,6 +22,18 @@ async function getApiKey() {
22
22
  return typeof fromConfig === 'string' ? fromConfig.trim() : '';
23
23
  }
24
24
 
25
+ async function getApiBase() {
26
+ if (process.env.FELO_API_BASE?.trim()) {
27
+ return process.env.FELO_API_BASE.trim().replace(/\/$/, '');
28
+ }
29
+ const { getConfigValue } = await import('./config.js');
30
+ const fromConfig = await getConfigValue('FELO_API_BASE');
31
+ if (typeof fromConfig === 'string' && fromConfig.trim()) {
32
+ return fromConfig.trim().replace(/\/$/, '');
33
+ }
34
+ return DEFAULT_API_BASE;
35
+ }
36
+
25
37
  function getMessage(payload) {
26
38
  return (
27
39
  payload?.message ||
@@ -266,11 +278,11 @@ function extractToolResults(data) {
266
278
  }
267
279
  // Discovery (research report) tool results
268
280
  if (t?.name === 'generate_discovery' && callResult?.status === 'success') {
269
- out.push({ type: 'discovery', title: callResult?.title || t?.params?.title || '研究报告' });
281
+ out.push({ type: 'discovery', title: callResult?.title || t?.params?.title || 'Discovery' });
270
282
  }
271
283
  // Document generation tool results
272
284
  if (t?.name === 'generate_document' && callResult?.status === 'success') {
273
- out.push({ type: 'document', title: callResult?.title || t?.params?.title || '文档' });
285
+ out.push({ type: 'document', title: callResult?.title || t?.params?.title || 'Document' });
274
286
  }
275
287
  // PPT generation tool results
276
288
  if (t?.name === 'generate_ppt' && callResult?.status === 'success') {
@@ -350,9 +362,9 @@ function dispatch(eventType, dataStr, onMessage, onError, onDone, onEvent, onToo
350
362
  const text = data?.content ?? data?.text ?? data?.delta;
351
363
  if (typeof text === 'string') onMessage(text);
352
364
  } else if (type === 'message' && onStatusMessage && data?.query) {
353
- onStatusMessage(`已收到: ${data.query}`);
354
- } else if (type === 'processing' && onStatusMessage && data?.message) {
355
- onStatusMessage(data.message);
365
+ onStatusMessage(`Received: ${data.query}`);
366
+ } else if (type === 'processing') {
367
+ // Silently ignore processing events
356
368
  } else if (type === 'tools' && onToolCall) {
357
369
  const params = extractToolParams(data);
358
370
  for (const item of params) onToolCall(item);
@@ -379,6 +391,160 @@ function dispatch(eventType, dataStr, onMessage, onError, onDone, onEvent, onToo
379
391
  }
380
392
  }
381
393
 
394
+ /**
395
+ * List LiveDocs with pagination and optional keyword filtering.
396
+ * @param {Object} [options]
397
+ * @param {number} [options.page] - Page number (default 1).
398
+ * @param {number} [options.size] - Page size (default 20).
399
+ * @param {string} [options.keyword] - Keyword filter.
400
+ * @param {boolean} [options.json] - Output raw JSON.
401
+ * @param {number} [options.timeoutMs] - Request timeout in ms.
402
+ * @returns {Promise<number>} Exit code 0 or 1.
403
+ */
404
+ export async function listLiveDocs(options = {}) {
405
+ const apiKey = await getApiKey();
406
+ if (!apiKey) {
407
+ process.stderr.write(NO_KEY_MESSAGE.trim() + '\n');
408
+ return 1;
409
+ }
410
+
411
+ const apiBase = await getApiBase();
412
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
413
+ const page = options.page ?? 1;
414
+ const size = options.size ?? 20;
415
+
416
+ const params = new URLSearchParams();
417
+ params.set('page', String(page));
418
+ params.set('size', String(size));
419
+ if (options.keyword) params.set('keyword', options.keyword);
420
+
421
+ const url = `${apiBase}/v2/livedocs?${params.toString()}`;
422
+
423
+ const controller = new AbortController();
424
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
425
+
426
+ try {
427
+ const res = await fetch(url, {
428
+ method: 'GET',
429
+ headers: {
430
+ Accept: 'application/json',
431
+ Authorization: `Bearer ${apiKey}`,
432
+ },
433
+ signal: controller.signal,
434
+ });
435
+
436
+ let data = {};
437
+ try {
438
+ data = await res.json();
439
+ } catch {
440
+ data = {};
441
+ }
442
+
443
+ if (!res.ok) {
444
+ throw new Error(`HTTP ${res.status}: ${getMessage(data)}`);
445
+ }
446
+ if (isApiError(data)) {
447
+ throw new Error(getMessage(data));
448
+ }
449
+
450
+ const payload = data?.data ?? {};
451
+ const total = payload.total ?? 0;
452
+ const items = payload.items ?? [];
453
+
454
+ if (options.json) {
455
+ console.log(JSON.stringify(payload, null, 2));
456
+ } else {
457
+ const isDev = apiBase.includes('-dev');
458
+ const webHost = isDev ? 'https://dev.felo.ai' : 'https://felo.ai';
459
+ const totalPages = Math.ceil(total / size);
460
+ console.log(`LiveDocs (total: ${total}, page ${page}/${totalPages})\n`);
461
+ for (const item of items) {
462
+ const shortId = item.short_id || '(no ID)';
463
+ console.log(`${webHost}/livedoc/${shortId}`);
464
+ }
465
+ }
466
+
467
+ return 0;
468
+ } catch (err) {
469
+ const msg = err?.message || err;
470
+ process.stderr.write(`Error: ${msg}\n`);
471
+ return 1;
472
+ } finally {
473
+ clearTimeout(timer);
474
+ }
475
+ }
476
+
477
+ /**
478
+ * List resources in a specific LiveDoc.
479
+ * @param {string} liveDocId - LiveDoc short_id.
480
+ * @param {Object} [options]
481
+ * @param {boolean} [options.json] - Output raw JSON.
482
+ * @param {number} [options.timeoutMs] - Request timeout in ms.
483
+ * @returns {Promise<number>} Exit code 0 or 1.
484
+ */
485
+ export async function listLiveDocResources(liveDocId, options = {}) {
486
+ const apiKey = await getApiKey();
487
+ if (!apiKey) {
488
+ process.stderr.write(NO_KEY_MESSAGE.trim() + '\n');
489
+ return 1;
490
+ }
491
+
492
+ const apiBase = await getApiBase();
493
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
494
+ const url = `${apiBase}/v2/livedocs/${encodeURIComponent(liveDocId)}/resources`;
495
+
496
+ const controller = new AbortController();
497
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
498
+
499
+ try {
500
+ const res = await fetch(url, {
501
+ method: 'GET',
502
+ headers: {
503
+ Accept: 'application/json',
504
+ Authorization: `Bearer ${apiKey}`,
505
+ },
506
+ signal: controller.signal,
507
+ });
508
+
509
+ let data = {};
510
+ try {
511
+ data = await res.json();
512
+ } catch {
513
+ data = {};
514
+ }
515
+
516
+ if (!res.ok) {
517
+ throw new Error(`HTTP ${res.status}: ${getMessage(data)}`);
518
+ }
519
+ if (isApiError(data)) {
520
+ throw new Error(getMessage(data));
521
+ }
522
+
523
+ const payload = data?.data ?? {};
524
+ const total = payload.total ?? 0;
525
+ const items = payload.items ?? [];
526
+
527
+ if (options.json) {
528
+ console.log(JSON.stringify(payload, null, 2));
529
+ } else {
530
+ console.log(`Resources (total: ${total})\n`);
531
+ for (const item of items) {
532
+ const title = item.title || '(no title)';
533
+ const id = item.id || '(no ID)';
534
+ console.log(`[${title}] ${id}`);
535
+ }
536
+ }
537
+
538
+ return 0;
539
+ } catch (err) {
540
+ const msg = err?.message || err;
541
+ process.stderr.write(`Error: ${msg}\n`);
542
+ return 1;
543
+ } finally {
544
+ clearTimeout(timer);
545
+ }
546
+ }
547
+
382
548
  /**
383
549
  * Run SuperAgent: create conversation, consume SSE stream, output answer.
384
550
  * @param {string} query - User query (1–2000 chars).
@@ -388,6 +554,9 @@ function dispatch(eventType, dataStr, onMessage, onError, onDone, onEvent, onToo
388
554
  * @param {number} [options.timeoutMs] - Request/stream timeout in ms.
389
555
  * @param {string} [options.liveDocId] - Reuse existing LiveDoc short_id.
390
556
  * @param {string} [options.threadId] - Existing thread/conversation ID for follow-up.
557
+ * @param {string} [options.skillId] - Skill ID (only for new conversations).
558
+ * @param {string[]} [options.selectedResourceIds] - Resource IDs (only for new conversations).
559
+ * @param {Object} [options.ext] - Extra params object (only for new conversations).
391
560
  * @param {string} [options.acceptLanguage] - e.g. zh, en.
392
561
  * @returns {Promise<number>} Exit code 0 or 1.
393
562
  */
@@ -398,7 +567,7 @@ export async function superAgent(query, options = {}) {
398
567
  return 1;
399
568
  }
400
569
 
401
- const apiBase = (process.env.FELO_API_BASE?.trim() || DEFAULT_API_BASE).replace(/\/$/, '');
570
+ const apiBase = await getApiBase();
402
571
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
403
572
  const body = {
404
573
  query: String(query).trim().slice(0, 2000),
@@ -408,6 +577,19 @@ export async function superAgent(query, options = {}) {
408
577
 
409
578
  try {
410
579
  const threadId = options.threadId;
580
+
581
+ // skill_id, selected_resource_ids, ext only supported for new conversations
582
+ const createOnlyParams = ['skillId', 'selectedResourceIds', 'ext'];
583
+ const hasCreateOnlyParams = createOnlyParams.some(k => options[k] !== undefined);
584
+ if (threadId && hasCreateOnlyParams) {
585
+ process.stderr.write('Warning: --skill-id, --selected-resource-ids, --ext are only supported for new conversations, ignored in follow-up mode.\n');
586
+ }
587
+ if (!threadId) {
588
+ if (options.skillId) body.skill_id = options.skillId;
589
+ if (options.selectedResourceIds) body.selected_resource_ids = options.selectedResourceIds;
590
+ if (options.ext) body.ext = options.ext;
591
+ }
592
+
411
593
  process.stderr.write(threadId ? 'SuperAgent: following up...\n' : 'SuperAgent: creating conversation...\n');
412
594
 
413
595
  const createData = await createConversation(apiKey, apiBase, body, timeoutMs, threadId);
@@ -468,7 +650,7 @@ export async function superAgent(query, options = {}) {
468
650
  if (isJson) return;
469
651
  if (item.type === 'image') {
470
652
  if (liveDocUrl) {
471
- console.log(`[${item.title || '图片'}](${liveDocUrl})`);
653
+ console.log(`[${item.title || 'Image'}](${liveDocUrl})`);
472
654
  } else {
473
655
  console.log(item.image_url);
474
656
  }
@@ -480,9 +662,9 @@ export async function superAgent(query, options = {}) {
480
662
  }
481
663
  } else if (item.type === 'document') {
482
664
  if (liveDocUrl) {
483
- console.log(`[${item.title || '文档'}](${liveDocUrl})`);
665
+ console.log(`[${item.title || 'Document'}](${liveDocUrl})`);
484
666
  } else {
485
- console.log(item.title || '文档');
667
+ console.log(item.title || 'Document');
486
668
  }
487
669
  } else if (item.type === 'ppt') {
488
670
  if (liveDocUrl) {
@@ -601,7 +783,7 @@ export async function superAgent(query, options = {}) {
601
783
  process.stderr.write(`Error: ${msg}\n`);
602
784
  if (String(msg).toLowerCase().includes('stream error')) {
603
785
  process.stderr.write(
604
- '(流式无客户端超时;若内部接口能拿到完整流,多为代理/防火墙在等待生图等长任务时空闲断连,可直连或调大代理空闲超时后重试)\n'
786
+ '(Stream idle disconnect — likely a proxy/firewall closing the connection during long tasks like image generation. Try a direct connection or increase proxy idle timeout.)\n'
605
787
  );
606
788
  }
607
789
  return 1;
package/src/webFetch.js CHANGED
@@ -146,3 +146,35 @@ export async function webFetch(opts) {
146
146
  stopSpinner(spinnerId);
147
147
  }
148
148
  }
149
+
150
+ /**
151
+ * Fetch webpage content and return as string (for content-to-slides). Does not print.
152
+ * @param {Object} opts - { url, format, readability, timeoutMs }
153
+ * @returns {Promise<string>} content or throws
154
+ */
155
+ export async function getWebContent(opts) {
156
+ const apiKey = await getApiKey();
157
+ if (!apiKey) throw new Error(NO_KEY_MESSAGE.trim());
158
+ if (!opts?.url || typeof opts.url !== 'string' || !opts.url.trim()) {
159
+ throw new Error('URL is required');
160
+ }
161
+ const apiBase = await getApiBase();
162
+ const timeoutMs = Number.isFinite(opts?.timeoutMs) && opts.timeoutMs > 0
163
+ ? opts.timeoutMs
164
+ : DEFAULT_TIMEOUT_MS;
165
+ const body = {
166
+ url: opts.url,
167
+ output_format: opts.format || 'markdown',
168
+ crawl_mode: opts.crawlMode || 'fast',
169
+ with_readability: Boolean(opts.readability),
170
+ timeout: timeoutMs,
171
+ };
172
+ if (opts.targetSelector) body.target_selector = opts.targetSelector;
173
+ if (opts.waitForSelector) body.wait_for_selector = opts.waitForSelector;
174
+ const payload = await fetchContent(apiBase, apiKey, body, timeoutMs);
175
+ const out = stringifyContent(payload?.data?.content);
176
+ if (out == null || String(out).trim() === '') {
177
+ throw new Error(`No content fetched from ${opts.url}`);
178
+ }
179
+ return out;
180
+ }
@@ -177,3 +177,35 @@ export async function youtubeSubtitling(opts) {
177
177
  stopSpinner(spinnerId);
178
178
  }
179
179
  }
180
+
181
+ /**
182
+ * Fetch YouTube subtitles and return as string (for content-to-slides). Does not print.
183
+ * @param {Object} opts - { videoCode, language, withTime, timeoutMs }
184
+ * @returns {Promise<string>} title + transcript text or throws
185
+ */
186
+ export async function getYoutubeContent(opts) {
187
+ const apiKey = await getApiKey();
188
+ if (!apiKey) throw new Error(NO_KEY_MESSAGE.trim());
189
+ const raw = opts?.videoCode != null ? String(opts.videoCode).trim() : '';
190
+ if (!raw) throw new Error('YouTube video URL or video ID is required.');
191
+ const videoCode = extractVideoId(raw);
192
+ if (!videoCode) {
193
+ throw new Error('Invalid YouTube URL or video ID. Use a link or an 11-character video ID.');
194
+ }
195
+ const apiBase = await getApiBase();
196
+ const timeoutMs =
197
+ Number.isFinite(opts?.timeoutMs) && opts.timeoutMs > 0 ? opts.timeoutMs : DEFAULT_TIMEOUT_MS;
198
+ const params = new URLSearchParams({ video_code: videoCode });
199
+ if (opts?.language && String(opts.language).trim()) params.set('language', String(opts.language).trim());
200
+ if (opts?.withTime) params.set('with_time', 'true');
201
+ const payload = await fetchSubtitling(apiBase, apiKey, params, timeoutMs);
202
+ const data = payload?.data ?? {};
203
+ const title = data?.title ?? '';
204
+ const contents = data?.contents ?? [];
205
+ const text = formatContents(contents, opts?.withTime ?? false);
206
+ if (!text || text.trim() === '') {
207
+ throw new Error(`No subtitles found for video ${videoCode}`);
208
+ }
209
+ if (title) return `# ${title}\n\n${text}`;
210
+ return text;
211
+ }