agent-reader 1.0.1 → 1.1.0
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.md +64 -0
- package/SKILL.md +40 -0
- package/bin/agent-reader.js +1 -1
- package/openclaw-skill/SKILL.md +40 -0
- package/openclaw-skill/schema.json +197 -0
- package/package.json +1 -1
- package/src/cli/commands.js +22 -2
- package/src/core/assets.js +8 -0
- package/src/core/exporter.js +279 -17
- package/src/core/opener.js +4 -1
- package/src/mcp/server.js +12 -52
- package/src/mcp/toolSchemas.js +53 -0
- package/src/utils/pathGuard.js +62 -0
- package/src/utils/server.js +10 -4
package/README.md
CHANGED
|
@@ -29,6 +29,24 @@ Agent Reader 解决这三个问题:**一条命令,输出即交付**。
|
|
|
29
29
|
| 图片 → 幻灯片 | 全屏播放、键盘翻页、自动轮播、缩略图导航 |
|
|
30
30
|
| 幻灯片 → PDF | 每张图片一页,适合存档分享 |
|
|
31
31
|
|
|
32
|
+
## 幻灯片快速上手
|
|
33
|
+
|
|
34
|
+
如果你有一组图片要现场展示,直接用这一条命令:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
agent-reader slides ./my-images/
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
常用选项:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
agent-reader slides ./my-images/ --auto 5
|
|
44
|
+
agent-reader slides ./my-images/ --format pdf
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
支持格式:`png`、`jpg`、`jpeg`、`gif`、`svg`、`webp`。
|
|
48
|
+
图片按自然顺序排序(例如 `1`、`2`、`10`,不是 `1`、`10`、`2`)。
|
|
49
|
+
|
|
32
50
|
## 两种使用方式
|
|
33
51
|
|
|
34
52
|
**CLI** — 开发者和 Agent 通过命令行调用
|
|
@@ -210,3 +228,49 @@ Claude Desktop 配置(`claude_desktop_config.json`):
|
|
|
210
228
|
| Node.js 18+ | 是 | 运行环境 |
|
|
211
229
|
| Puppeteer | 是 | PDF 导出(安装时自动下载 Chromium) |
|
|
212
230
|
| Pandoc | 否 | Word 导出更好看(没有会自动降级为纯 JS 方案) |
|
|
231
|
+
|
|
232
|
+
## 云环境部署
|
|
233
|
+
|
|
234
|
+
在 Docker/CI 中,Agent Reader 会自动检测环境并在需要时为 Puppeteer 关闭沙盒参数(`--no-sandbox` 等)。
|
|
235
|
+
|
|
236
|
+
手动覆盖方式:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
# auto | on | off
|
|
240
|
+
AGENT_READER_SANDBOX=off agent-reader export report.md --format pdf
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Docker 环境建议预装 Chromium 依赖(示例):
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
apt-get update && apt-get install -y chromium-browser
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## FAQ:没有 Pandoc 怎么办?
|
|
250
|
+
|
|
251
|
+
- 不装 Pandoc 也能导出 DOCX,只是排版会使用基础模式
|
|
252
|
+
- 装 Pandoc 后,代码块和复杂表格的效果更稳定
|
|
253
|
+
|
|
254
|
+
常见安装命令:
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
# macOS
|
|
258
|
+
brew install pandoc
|
|
259
|
+
|
|
260
|
+
# Linux
|
|
261
|
+
apt-get install pandoc
|
|
262
|
+
|
|
263
|
+
# Windows
|
|
264
|
+
winget install pandoc
|
|
265
|
+
# or: choco install pandoc
|
|
266
|
+
# or: scoop install pandoc
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Puppeteer 进阶安装
|
|
270
|
+
|
|
271
|
+
如果你已经有可用浏览器,想减少安装体积:
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true npm install -g agent-reader
|
|
275
|
+
PUPPETEER_EXECUTABLE_PATH=/path/to/chrome agent-reader export report.md --format pdf
|
|
276
|
+
```
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent_reader
|
|
3
|
+
description: 把 Markdown 渲染成漂亮网页、导出 Word/PDF、图片做幻灯片。专为 AI Agent 输出设计。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Agent Reader Skill
|
|
7
|
+
|
|
8
|
+
## 能力
|
|
9
|
+
|
|
10
|
+
- Markdown 渲染为可阅读网页(目录、代码高亮、表格样式)
|
|
11
|
+
- 导出 PDF / DOCX 文档
|
|
12
|
+
- 图片目录生成幻灯片并支持导出 PDF
|
|
13
|
+
|
|
14
|
+
## 触发条件
|
|
15
|
+
|
|
16
|
+
- 用户要求把 Markdown 输出整理成可交付文档
|
|
17
|
+
- 用户需要导出 Word 或 PDF
|
|
18
|
+
- 用户希望把图片集合用于展示或汇报
|
|
19
|
+
|
|
20
|
+
## CLI 调用
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
agent-reader render report.md
|
|
24
|
+
agent-reader export report.md --format pdf
|
|
25
|
+
agent-reader export report.md --format docx
|
|
26
|
+
agent-reader slides ./images --auto 5
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## MCP 调用
|
|
30
|
+
|
|
31
|
+
- `render_markdown`
|
|
32
|
+
- `export_document`
|
|
33
|
+
- `create_slideshow`
|
|
34
|
+
- `open_file`
|
|
35
|
+
|
|
36
|
+
## 安装
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install -g agent-reader
|
|
40
|
+
```
|
package/bin/agent-reader.js
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent_reader
|
|
3
|
+
description: 把 Markdown 渲染成漂亮网页、导出 Word/PDF、图片做幻灯片。专为 AI Agent 输出设计。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Agent Reader Skill
|
|
7
|
+
|
|
8
|
+
## 能力
|
|
9
|
+
|
|
10
|
+
- Markdown 渲染为可阅读网页(目录、代码高亮、表格样式)
|
|
11
|
+
- 导出 PDF / DOCX 文档
|
|
12
|
+
- 图片目录生成幻灯片并支持导出 PDF
|
|
13
|
+
|
|
14
|
+
## 触发条件
|
|
15
|
+
|
|
16
|
+
- 用户要求把 Markdown 输出整理成可交付文档
|
|
17
|
+
- 用户需要导出 Word 或 PDF
|
|
18
|
+
- 用户希望把图片集合用于展示或汇报
|
|
19
|
+
|
|
20
|
+
## CLI 调用
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
agent-reader render report.md
|
|
24
|
+
agent-reader export report.md --format pdf
|
|
25
|
+
agent-reader export report.md --format docx
|
|
26
|
+
agent-reader slides ./images --auto 5
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## MCP 调用
|
|
30
|
+
|
|
31
|
+
- `render_markdown`
|
|
32
|
+
- `export_document`
|
|
33
|
+
- `create_slideshow`
|
|
34
|
+
- `open_file`
|
|
35
|
+
|
|
36
|
+
## 安装
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install -g agent-reader
|
|
40
|
+
```
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "agent-reader-openclaw-skill-schema",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"tools": {
|
|
6
|
+
"render_markdown": {
|
|
7
|
+
"description": "Render markdown text into styled HTML",
|
|
8
|
+
"input": {
|
|
9
|
+
"type": "object",
|
|
10
|
+
"properties": {
|
|
11
|
+
"content": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"description": "Markdown source content"
|
|
14
|
+
},
|
|
15
|
+
"source_path": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"description": "Source markdown path for relative images; relative resources must stay under source directory."
|
|
18
|
+
},
|
|
19
|
+
"theme": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"description": "Theme name"
|
|
22
|
+
},
|
|
23
|
+
"auto_open": {
|
|
24
|
+
"type": "boolean",
|
|
25
|
+
"description": "Open output automatically (ignored in MCP)"
|
|
26
|
+
},
|
|
27
|
+
"return_content": {
|
|
28
|
+
"type": "boolean",
|
|
29
|
+
"description": "Return inline HTML content directly"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"required": [
|
|
33
|
+
"content"
|
|
34
|
+
],
|
|
35
|
+
"additionalProperties": false
|
|
36
|
+
},
|
|
37
|
+
"output": {
|
|
38
|
+
"type": "object",
|
|
39
|
+
"properties": {
|
|
40
|
+
"html_path": {
|
|
41
|
+
"type": "string"
|
|
42
|
+
},
|
|
43
|
+
"content_data": {
|
|
44
|
+
"type": "string"
|
|
45
|
+
},
|
|
46
|
+
"format": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"enum": [
|
|
49
|
+
"html"
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
"size": {
|
|
53
|
+
"type": "number"
|
|
54
|
+
},
|
|
55
|
+
"warnings": {
|
|
56
|
+
"type": "array",
|
|
57
|
+
"items": {
|
|
58
|
+
"type": "string"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"required": [
|
|
63
|
+
"format",
|
|
64
|
+
"size",
|
|
65
|
+
"warnings"
|
|
66
|
+
],
|
|
67
|
+
"additionalProperties": true
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"export_document": {
|
|
71
|
+
"description": "Export markdown text into PDF or DOCX",
|
|
72
|
+
"input": {
|
|
73
|
+
"type": "object",
|
|
74
|
+
"properties": {
|
|
75
|
+
"content": {
|
|
76
|
+
"type": "string",
|
|
77
|
+
"description": "Markdown source content"
|
|
78
|
+
},
|
|
79
|
+
"source_path": {
|
|
80
|
+
"type": "string",
|
|
81
|
+
"description": "Source markdown path for relative images; relative resources must stay under source directory."
|
|
82
|
+
},
|
|
83
|
+
"format": {
|
|
84
|
+
"type": "string",
|
|
85
|
+
"enum": [
|
|
86
|
+
"pdf",
|
|
87
|
+
"docx"
|
|
88
|
+
],
|
|
89
|
+
"description": "Export format"
|
|
90
|
+
},
|
|
91
|
+
"return_content": {
|
|
92
|
+
"type": "boolean",
|
|
93
|
+
"description": "Return file bytes as base64"
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"required": [
|
|
97
|
+
"content",
|
|
98
|
+
"format"
|
|
99
|
+
],
|
|
100
|
+
"additionalProperties": false
|
|
101
|
+
},
|
|
102
|
+
"output": {
|
|
103
|
+
"type": "object",
|
|
104
|
+
"properties": {
|
|
105
|
+
"file_path": {
|
|
106
|
+
"type": "string"
|
|
107
|
+
},
|
|
108
|
+
"content_data": {
|
|
109
|
+
"type": "string"
|
|
110
|
+
},
|
|
111
|
+
"format": {
|
|
112
|
+
"type": "string",
|
|
113
|
+
"enum": [
|
|
114
|
+
"pdf",
|
|
115
|
+
"docx"
|
|
116
|
+
]
|
|
117
|
+
},
|
|
118
|
+
"size": {
|
|
119
|
+
"type": "number"
|
|
120
|
+
},
|
|
121
|
+
"warnings": {
|
|
122
|
+
"type": "array",
|
|
123
|
+
"items": {
|
|
124
|
+
"type": "string"
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
"required": [
|
|
129
|
+
"format",
|
|
130
|
+
"size",
|
|
131
|
+
"warnings"
|
|
132
|
+
],
|
|
133
|
+
"additionalProperties": true
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
"create_slideshow": {
|
|
137
|
+
"description": "Create slideshow HTML from an image directory",
|
|
138
|
+
"input": {
|
|
139
|
+
"type": "object",
|
|
140
|
+
"properties": {
|
|
141
|
+
"image_dir": {
|
|
142
|
+
"type": "string",
|
|
143
|
+
"description": "Absolute or relative image directory path"
|
|
144
|
+
},
|
|
145
|
+
"auto_play": {
|
|
146
|
+
"type": "number",
|
|
147
|
+
"description": "Autoplay interval in seconds"
|
|
148
|
+
},
|
|
149
|
+
"auto_open": {
|
|
150
|
+
"type": "boolean",
|
|
151
|
+
"description": "Open output automatically (ignored in MCP)"
|
|
152
|
+
},
|
|
153
|
+
"return_content": {
|
|
154
|
+
"type": "boolean",
|
|
155
|
+
"description": "Return inline HTML content directly"
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
"required": [
|
|
159
|
+
"image_dir"
|
|
160
|
+
],
|
|
161
|
+
"additionalProperties": false
|
|
162
|
+
},
|
|
163
|
+
"output": {
|
|
164
|
+
"type": "object",
|
|
165
|
+
"properties": {
|
|
166
|
+
"html_path": {
|
|
167
|
+
"type": "string"
|
|
168
|
+
},
|
|
169
|
+
"content_data": {
|
|
170
|
+
"type": "string"
|
|
171
|
+
},
|
|
172
|
+
"format": {
|
|
173
|
+
"type": "string",
|
|
174
|
+
"enum": [
|
|
175
|
+
"html"
|
|
176
|
+
]
|
|
177
|
+
},
|
|
178
|
+
"size": {
|
|
179
|
+
"type": "number"
|
|
180
|
+
},
|
|
181
|
+
"warnings": {
|
|
182
|
+
"type": "array",
|
|
183
|
+
"items": {
|
|
184
|
+
"type": "string"
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
"required": [
|
|
189
|
+
"format",
|
|
190
|
+
"size",
|
|
191
|
+
"warnings"
|
|
192
|
+
],
|
|
193
|
+
"additionalProperties": true
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
package/package.json
CHANGED
package/src/cli/commands.js
CHANGED
|
@@ -4,7 +4,14 @@ import open from 'open';
|
|
|
4
4
|
import { execa } from 'execa';
|
|
5
5
|
import { renderMarkdown } from '../core/renderer.js';
|
|
6
6
|
import { createSlideshow } from '../core/slideshow.js';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
checkPandoc,
|
|
9
|
+
checkPuppeteer,
|
|
10
|
+
exportDOCX,
|
|
11
|
+
exportDOCXFromHTML,
|
|
12
|
+
exportPDF,
|
|
13
|
+
resolveSandboxMode,
|
|
14
|
+
} from '../core/exporter.js';
|
|
8
15
|
import { openTarget } from '../core/opener.js';
|
|
9
16
|
import { cleanOldOutputs, createOutputDir } from '../utils/output.js';
|
|
10
17
|
import { createLogger } from '../utils/logger.js';
|
|
@@ -86,6 +93,10 @@ function dedupeWarnings(warnings) {
|
|
|
86
93
|
return [...new Set((warnings || []).filter(Boolean))];
|
|
87
94
|
}
|
|
88
95
|
|
|
96
|
+
function resolveSandboxOption(options = {}) {
|
|
97
|
+
return resolveSandboxMode(options.sandbox, process.env);
|
|
98
|
+
}
|
|
99
|
+
|
|
89
100
|
async function buildPrintHtml(imageDir) {
|
|
90
101
|
const absDir = path.resolve(imageDir);
|
|
91
102
|
const entries = await fs.readdir(absDir, { withFileTypes: true });
|
|
@@ -135,9 +146,11 @@ async function maybeServeOutput(options, outputDir, targetPath, logger, jsonOnly
|
|
|
135
146
|
return null;
|
|
136
147
|
}
|
|
137
148
|
|
|
149
|
+
const sandbox = resolveSandboxOption(options);
|
|
138
150
|
const serverHandle = await startStaticServer(outputDir, {
|
|
139
151
|
host: '127.0.0.1',
|
|
140
152
|
port: Number(options.port || 3000),
|
|
153
|
+
sandbox,
|
|
141
154
|
});
|
|
142
155
|
|
|
143
156
|
const rel = path.relative(outputDir, targetPath).split(path.sep).join('/');
|
|
@@ -222,6 +235,7 @@ export async function exportCommand(file, options) {
|
|
|
222
235
|
|
|
223
236
|
try {
|
|
224
237
|
const format = normalizeFormat(options.format);
|
|
238
|
+
const sandbox = resolveSandboxOption(options);
|
|
225
239
|
const input = await resolveMarkdownInput(file, options, mode.logger);
|
|
226
240
|
const outputDir = await createOutputDir(input.name, options.outDir);
|
|
227
241
|
const warnings = [];
|
|
@@ -249,6 +263,7 @@ export async function exportCommand(file, options) {
|
|
|
249
263
|
outDir: outputDir,
|
|
250
264
|
fileName: `${input.name}.pdf`,
|
|
251
265
|
htmlPath,
|
|
266
|
+
sandbox,
|
|
252
267
|
});
|
|
253
268
|
warnings.push(...pdf.warnings);
|
|
254
269
|
targetPath = pdf.pdfPath;
|
|
@@ -322,6 +337,7 @@ export async function slidesCommand(dir, options) {
|
|
|
322
337
|
await fs.writeFile(printHtmlPath, result.printHtml, 'utf8');
|
|
323
338
|
|
|
324
339
|
const format = String(options.format || '').toLowerCase();
|
|
340
|
+
const sandbox = resolveSandboxOption(options);
|
|
325
341
|
const dirName = path.basename(inputDir);
|
|
326
342
|
|
|
327
343
|
if (format === 'pdf') {
|
|
@@ -331,6 +347,7 @@ export async function slidesCommand(dir, options) {
|
|
|
331
347
|
outDir: outputDir,
|
|
332
348
|
fileName: `${dirName}.pdf`,
|
|
333
349
|
htmlPath: printHtmlPath,
|
|
350
|
+
sandbox,
|
|
334
351
|
});
|
|
335
352
|
|
|
336
353
|
const pdfWarnings = dedupeWarnings([...result.warnings, ...(pdf.warnings || [])]);
|
|
@@ -398,6 +415,7 @@ export async function openCommand(target, options) {
|
|
|
398
415
|
throw new Error('missing target path');
|
|
399
416
|
}
|
|
400
417
|
|
|
418
|
+
const sandbox = resolveSandboxOption(options);
|
|
401
419
|
const preferences = await loadPreferences();
|
|
402
420
|
const requestedMode = normalizeOpenMode(options.as || options.mode || 'auto', 'auto');
|
|
403
421
|
|
|
@@ -410,6 +428,7 @@ export async function openCommand(target, options) {
|
|
|
410
428
|
pageSize: options.pageSize || 'A4',
|
|
411
429
|
fetchRemote: options.fetchRemote !== false,
|
|
412
430
|
returnContent: false,
|
|
431
|
+
sandbox,
|
|
413
432
|
});
|
|
414
433
|
|
|
415
434
|
const canServe = result.format === 'html' && result.path;
|
|
@@ -598,5 +617,6 @@ export function setupCommonCommandOptions(command) {
|
|
|
598
617
|
.option('--inline-all', 'inline all assets to base64')
|
|
599
618
|
.option('--no-fetch-remote', 'disable remote image fetching')
|
|
600
619
|
.option('--serve', 'serve generated files via local HTTP server')
|
|
601
|
-
.option('--port <port>', 'port for --serve mode', '3000')
|
|
620
|
+
.option('--port <port>', 'port for --serve mode', '3000')
|
|
621
|
+
.option('--sandbox <mode>', 'Puppeteer sandbox mode: auto|on|off');
|
|
602
622
|
}
|
package/src/core/assets.js
CHANGED
|
@@ -3,6 +3,7 @@ import dns from 'node:dns/promises';
|
|
|
3
3
|
import { promises as fs } from 'node:fs';
|
|
4
4
|
import net from 'node:net';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
+
import { assertWithinBase } from '../utils/pathGuard.js';
|
|
6
7
|
|
|
7
8
|
const LOCAL_IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp']);
|
|
8
9
|
const DEFAULT_MAX_INLINE_BYTES = 200 * 1024;
|
|
@@ -241,6 +242,13 @@ async function processLocalImage(src, {
|
|
|
241
242
|
}
|
|
242
243
|
|
|
243
244
|
const resolvedPath = isAbsoluteFile ? cleanSrc : path.resolve(baseDir, cleanSrc);
|
|
245
|
+
if (!isAbsoluteFile) {
|
|
246
|
+
const withinBase = await assertWithinBase(resolvedPath, baseDir);
|
|
247
|
+
if (!withinBase) {
|
|
248
|
+
warnings.push(`path traversal blocked: ${cleanSrc}`);
|
|
249
|
+
return cleanSrc;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
244
252
|
|
|
245
253
|
let fileBuffer;
|
|
246
254
|
try {
|
package/src/core/exporter.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { promises as fs } from 'node:fs';
|
|
1
|
+
import { accessSync, constants as fsConstants, promises as fs } from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
@@ -7,6 +7,7 @@ import { execa } from 'execa';
|
|
|
7
7
|
import MarkdownIt from 'markdown-it';
|
|
8
8
|
import {
|
|
9
9
|
Document,
|
|
10
|
+
ExternalHyperlink,
|
|
10
11
|
HeadingLevel,
|
|
11
12
|
Packer,
|
|
12
13
|
Paragraph,
|
|
@@ -25,6 +26,109 @@ const LUA_TABLE_FILTER = path.join(__dirname, 'templates', 'docx-table.lua');
|
|
|
25
26
|
const POSTPROCESS_SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'postprocess-docx.py');
|
|
26
27
|
|
|
27
28
|
const markdownParser = new MarkdownIt({ html: false, linkify: true, typographer: true });
|
|
29
|
+
const SANDBOX_DISABLED_ARGS = ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'];
|
|
30
|
+
const SANDBOX_MODES = new Set(['auto', 'on', 'off']);
|
|
31
|
+
|
|
32
|
+
function isSandboxMode(mode) {
|
|
33
|
+
return SANDBOX_MODES.has(mode);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeSandboxMode(mode) {
|
|
37
|
+
if (typeof mode !== 'string') {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const normalized = mode.trim().toLowerCase();
|
|
41
|
+
return isSandboxMode(normalized) ? normalized : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function fileExistsSync(filePath) {
|
|
45
|
+
try {
|
|
46
|
+
accessSync(filePath, fsConstants.F_OK);
|
|
47
|
+
return true;
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function detectContainerEnv(runtime = {}) {
|
|
54
|
+
const env = runtime.env ?? process.env;
|
|
55
|
+
const fileExists = runtime.fileExists ?? fileExistsSync;
|
|
56
|
+
const getuid = Object.prototype.hasOwnProperty.call(runtime, 'getuid')
|
|
57
|
+
? runtime.getuid
|
|
58
|
+
: process.getuid;
|
|
59
|
+
|
|
60
|
+
if (fileExists('/.dockerenv')) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
if (fileExists('/run/.containerenv')) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
if (env?.CI === 'true') {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
if (typeof getuid === 'function' && getuid() === 0) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function resolveSandboxMode(requestedMode, env = process.env) {
|
|
82
|
+
const fromExplicit = normalizeSandboxMode(requestedMode);
|
|
83
|
+
if (fromExplicit) {
|
|
84
|
+
return fromExplicit;
|
|
85
|
+
}
|
|
86
|
+
const fromEnv = normalizeSandboxMode(env?.AGENT_READER_SANDBOX);
|
|
87
|
+
if (fromEnv) {
|
|
88
|
+
return fromEnv;
|
|
89
|
+
}
|
|
90
|
+
return 'auto';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function getSandboxArgs(sandboxMode = 'auto', runtime = {}) {
|
|
94
|
+
const mode = normalizeSandboxMode(sandboxMode) || 'auto';
|
|
95
|
+
if (mode === 'on') {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
if (mode === 'off') {
|
|
99
|
+
return [...SANDBOX_DISABLED_ARGS];
|
|
100
|
+
}
|
|
101
|
+
return detectContainerEnv(runtime) ? [...SANDBOX_DISABLED_ARGS] : [];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function runHasProperty(run, propKey) {
|
|
105
|
+
return run.properties?.root?.some((item) => item.rootKey === propKey);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function cloneRunWithStyle(run, { bold, italics }) {
|
|
109
|
+
const baseTextNode = run.root.find((node) => node.rootKey === 'w:t');
|
|
110
|
+
const text = typeof baseTextNode?.root?.[baseTextNode.root.length - 1] === 'string'
|
|
111
|
+
? baseTextNode.root[baseTextNode.root.length - 1]
|
|
112
|
+
: '';
|
|
113
|
+
const hasCodeStyle = runHasProperty(run, 'w:rFonts');
|
|
114
|
+
const hasShading = runHasProperty(run, 'w:shd');
|
|
115
|
+
|
|
116
|
+
return new TextRun({
|
|
117
|
+
text,
|
|
118
|
+
bold: bold || undefined,
|
|
119
|
+
italics: italics || undefined,
|
|
120
|
+
...(hasCodeStyle ? { font: 'Consolas' } : {}),
|
|
121
|
+
...(hasShading
|
|
122
|
+
? {
|
|
123
|
+
shading: {
|
|
124
|
+
type: ShadingType.CLEAR,
|
|
125
|
+
color: 'auto',
|
|
126
|
+
fill: 'F4F4F5',
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
: {}),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
28
132
|
|
|
29
133
|
function paragraphFromText(text) {
|
|
30
134
|
return new Paragraph({
|
|
@@ -65,6 +169,139 @@ function inlineToPlainText(token) {
|
|
|
65
169
|
.join('');
|
|
66
170
|
}
|
|
67
171
|
|
|
172
|
+
function createInlineTextRun(text, { bold, italics, code = false }) {
|
|
173
|
+
return new TextRun({
|
|
174
|
+
text,
|
|
175
|
+
bold: bold || undefined,
|
|
176
|
+
italics: italics || undefined,
|
|
177
|
+
...(code
|
|
178
|
+
? {
|
|
179
|
+
font: 'Consolas',
|
|
180
|
+
shading: {
|
|
181
|
+
type: ShadingType.CLEAR,
|
|
182
|
+
color: 'auto',
|
|
183
|
+
fill: 'F4F4F5',
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
: {}),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function pushInlineNode(target, linkStack, node) {
|
|
191
|
+
if (linkStack.length > 0) {
|
|
192
|
+
linkStack[linkStack.length - 1].children.push(node);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
target.push(node);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function closeCurrentLink(target, linkStack, style) {
|
|
199
|
+
const current = linkStack.pop();
|
|
200
|
+
if (!current) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const href = current.href || '';
|
|
205
|
+
if (!href) {
|
|
206
|
+
const fallbackRuns = current.children.length > 0
|
|
207
|
+
? current.children
|
|
208
|
+
: [createInlineTextRun('', style)];
|
|
209
|
+
for (const run of fallbackRuns) {
|
|
210
|
+
pushInlineNode(target, linkStack, run);
|
|
211
|
+
}
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const hyperlinkChildren = current.children.length > 0
|
|
216
|
+
? current.children
|
|
217
|
+
: [createInlineTextRun(href, style)];
|
|
218
|
+
const hyperlink = new ExternalHyperlink({
|
|
219
|
+
link: href,
|
|
220
|
+
children: hyperlinkChildren.map((node) => (
|
|
221
|
+
node instanceof TextRun ? cloneRunWithStyle(node, style) : node
|
|
222
|
+
)),
|
|
223
|
+
});
|
|
224
|
+
pushInlineNode(target, linkStack, hyperlink);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function inlineToDocxRuns(token) {
|
|
228
|
+
if (!token?.children?.length) {
|
|
229
|
+
const fallbackText = token?.content || '';
|
|
230
|
+
return fallbackText ? [new TextRun(fallbackText)] : [];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const output = [];
|
|
234
|
+
const linkStack = [];
|
|
235
|
+
let boldDepth = 0;
|
|
236
|
+
let italicsDepth = 0;
|
|
237
|
+
|
|
238
|
+
for (const child of token.children) {
|
|
239
|
+
const style = {
|
|
240
|
+
bold: boldDepth > 0,
|
|
241
|
+
italics: italicsDepth > 0,
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
if (child.type === 'strong_open') {
|
|
245
|
+
boldDepth += 1;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (child.type === 'strong_close') {
|
|
249
|
+
boldDepth = Math.max(0, boldDepth - 1);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (child.type === 'em_open') {
|
|
253
|
+
italicsDepth += 1;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (child.type === 'em_close') {
|
|
257
|
+
italicsDepth = Math.max(0, italicsDepth - 1);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (child.type === 'link_open') {
|
|
261
|
+
linkStack.push({
|
|
262
|
+
href: child.attrGet('href') || '',
|
|
263
|
+
children: [],
|
|
264
|
+
});
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (child.type === 'link_close') {
|
|
268
|
+
closeCurrentLink(output, linkStack, style);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (child.type === 'text') {
|
|
272
|
+
if (child.content) {
|
|
273
|
+
pushInlineNode(output, linkStack, createInlineTextRun(child.content, style));
|
|
274
|
+
}
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (child.type === 'code_inline') {
|
|
278
|
+
pushInlineNode(output, linkStack, createInlineTextRun(child.content || '', { ...style, code: true }));
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (child.type === 'softbreak' || child.type === 'hardbreak') {
|
|
282
|
+
pushInlineNode(output, linkStack, createInlineTextRun('\n', style));
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (child.type === 'image') {
|
|
286
|
+
const alt = child.content || child.attrGet('alt') || 'image';
|
|
287
|
+
pushInlineNode(output, linkStack, createInlineTextRun(`[${alt}]`, style));
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (child.content) {
|
|
291
|
+
pushInlineNode(output, linkStack, createInlineTextRun(child.content, style));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
while (linkStack.length > 0) {
|
|
296
|
+
closeCurrentLink(output, linkStack, {
|
|
297
|
+
bold: boldDepth > 0,
|
|
298
|
+
italics: italicsDepth > 0,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return output;
|
|
303
|
+
}
|
|
304
|
+
|
|
68
305
|
function collectTocEntries(tokens) {
|
|
69
306
|
const entries = [];
|
|
70
307
|
for (let i = 0; i < tokens.length; i += 1) {
|
|
@@ -123,9 +360,11 @@ function parseTable(tokens, startIndex) {
|
|
|
123
360
|
rows: rows.map(
|
|
124
361
|
(items) =>
|
|
125
362
|
new TableRow({
|
|
126
|
-
children: items.map((
|
|
363
|
+
children: items.map((itemRuns) =>
|
|
127
364
|
new TableCell({
|
|
128
|
-
children: [
|
|
365
|
+
children: [new Paragraph({
|
|
366
|
+
children: itemRuns.length > 0 ? itemRuns : [new TextRun('')],
|
|
367
|
+
})],
|
|
129
368
|
}),
|
|
130
369
|
),
|
|
131
370
|
}),
|
|
@@ -146,17 +385,17 @@ function parseTable(tokens, startIndex) {
|
|
|
146
385
|
}
|
|
147
386
|
|
|
148
387
|
if (token.type === 'th_open' || token.type === 'td_open') {
|
|
149
|
-
currentCell =
|
|
388
|
+
currentCell = [];
|
|
150
389
|
continue;
|
|
151
390
|
}
|
|
152
391
|
|
|
153
392
|
if (token.type === 'inline' && currentCell !== null) {
|
|
154
|
-
currentCell
|
|
393
|
+
currentCell.push(...inlineToDocxRuns(token));
|
|
155
394
|
continue;
|
|
156
395
|
}
|
|
157
396
|
|
|
158
397
|
if (token.type === 'th_close' || token.type === 'td_close') {
|
|
159
|
-
row.push(currentCell ||
|
|
398
|
+
row.push(currentCell || []);
|
|
160
399
|
currentCell = null;
|
|
161
400
|
}
|
|
162
401
|
}
|
|
@@ -167,10 +406,10 @@ function parseTable(tokens, startIndex) {
|
|
|
167
406
|
};
|
|
168
407
|
}
|
|
169
408
|
|
|
170
|
-
function markdownToDocx(markdown) {
|
|
409
|
+
export function markdownToDocx(markdown) {
|
|
171
410
|
const tokens = markdownParser.parse(markdown, {});
|
|
172
411
|
const tocEntries = collectTocEntries(tokens);
|
|
173
|
-
const children = [];
|
|
412
|
+
const children = [...buildDocxTocParagraphs(tocEntries)];
|
|
174
413
|
const listStack = [];
|
|
175
414
|
let pendingBlock = null;
|
|
176
415
|
|
|
@@ -237,15 +476,15 @@ function markdownToDocx(markdown) {
|
|
|
237
476
|
continue;
|
|
238
477
|
}
|
|
239
478
|
|
|
240
|
-
const
|
|
241
|
-
if (
|
|
479
|
+
const runs = inlineToDocxRuns(token);
|
|
480
|
+
if (runs.length === 0) {
|
|
242
481
|
continue;
|
|
243
482
|
}
|
|
244
483
|
|
|
245
484
|
if (pendingBlock?.type === 'heading') {
|
|
246
485
|
children.push(
|
|
247
486
|
new Paragraph({
|
|
248
|
-
|
|
487
|
+
children: runs,
|
|
249
488
|
heading: headingLevelFromNumber(pendingBlock.level),
|
|
250
489
|
}),
|
|
251
490
|
);
|
|
@@ -256,11 +495,13 @@ function markdownToDocx(markdown) {
|
|
|
256
495
|
if (listStack.length > 0) {
|
|
257
496
|
const top = listStack[listStack.length - 1];
|
|
258
497
|
const prefix = top.type === 'ordered' ? `${top.count}. ` : '• ';
|
|
259
|
-
children.push(
|
|
498
|
+
children.push(new Paragraph({
|
|
499
|
+
children: [new TextRun(prefix), ...runs],
|
|
500
|
+
}));
|
|
260
501
|
continue;
|
|
261
502
|
}
|
|
262
503
|
|
|
263
|
-
children.push(
|
|
504
|
+
children.push(new Paragraph({ children: runs }));
|
|
264
505
|
pendingBlock = null;
|
|
265
506
|
}
|
|
266
507
|
|
|
@@ -431,6 +672,24 @@ export async function checkPuppeteer() {
|
|
|
431
672
|
}
|
|
432
673
|
}
|
|
433
674
|
|
|
675
|
+
export function getPandocInstallHint(platform = process.platform) {
|
|
676
|
+
if (platform === 'darwin') {
|
|
677
|
+
return 'brew install pandoc';
|
|
678
|
+
}
|
|
679
|
+
if (platform === 'win32') {
|
|
680
|
+
return 'winget install pandoc (or choco install pandoc / scoop install pandoc)';
|
|
681
|
+
}
|
|
682
|
+
return 'apt-get install pandoc (or yum install pandoc)';
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
export function createPandocFallbackWarnings(platform = process.platform) {
|
|
686
|
+
const installHint = getPandocInstallHint(platform);
|
|
687
|
+
return [
|
|
688
|
+
'pandoc_not_found',
|
|
689
|
+
`⚡ 当前使用基础排版模式。想要更精美的代码高亮和智能表格?一行命令解锁:${installHint}`,
|
|
690
|
+
];
|
|
691
|
+
}
|
|
692
|
+
|
|
434
693
|
export async function exportPDF(html, options = {}) {
|
|
435
694
|
const {
|
|
436
695
|
pageSize = 'A4',
|
|
@@ -438,6 +697,7 @@ export async function exportPDF(html, options = {}) {
|
|
|
438
697
|
outDir = os.tmpdir(),
|
|
439
698
|
fileName = 'output.pdf',
|
|
440
699
|
htmlPath,
|
|
700
|
+
sandbox,
|
|
441
701
|
} = options;
|
|
442
702
|
|
|
443
703
|
let puppeteer;
|
|
@@ -450,10 +710,13 @@ export async function exportPDF(html, options = {}) {
|
|
|
450
710
|
|
|
451
711
|
const warnings = [];
|
|
452
712
|
const pdfPath = path.join(path.resolve(outDir), fileName);
|
|
713
|
+
const resolvedSandboxMode = resolveSandboxMode(sandbox);
|
|
714
|
+
const launchArgs = landscape ? ['--allow-file-access-from-files'] : [];
|
|
715
|
+
launchArgs.push(...getSandboxArgs(resolvedSandboxMode));
|
|
453
716
|
|
|
454
717
|
const browser = await puppeteer.launch({
|
|
455
718
|
headless: true,
|
|
456
|
-
args:
|
|
719
|
+
args: launchArgs,
|
|
457
720
|
});
|
|
458
721
|
try {
|
|
459
722
|
const page = await browser.newPage();
|
|
@@ -587,8 +850,7 @@ export async function exportDOCX(markdownString, options = {}) {
|
|
|
587
850
|
await fs.rm(tempInput, { force: true }).catch(() => {});
|
|
588
851
|
}
|
|
589
852
|
} else {
|
|
590
|
-
|
|
591
|
-
warnings.push(warning);
|
|
853
|
+
warnings.push(...createPandocFallbackWarnings());
|
|
592
854
|
|
|
593
855
|
const document = markdownToDocx(markdownString);
|
|
594
856
|
const buffer = await Packer.toBuffer(document);
|
|
@@ -677,7 +939,7 @@ export async function exportDOCXFromHTML(htmlString, options = {}) {
|
|
|
677
939
|
await fs.rm(tempInput, { force: true }).catch(() => {});
|
|
678
940
|
}
|
|
679
941
|
} else {
|
|
680
|
-
warnings.push(
|
|
942
|
+
warnings.push(...createPandocFallbackWarnings());
|
|
681
943
|
|
|
682
944
|
const plainText = String(htmlString || '')
|
|
683
945
|
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
|
package/src/core/opener.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { createOutputDir } from '../utils/output.js';
|
|
4
4
|
import { normalizeOpenMode } from '../utils/preferences.js';
|
|
5
5
|
import { renderMarkdown } from './renderer.js';
|
|
6
|
-
import { exportDOCX, exportPDF } from './exporter.js';
|
|
6
|
+
import { exportDOCX, exportPDF, resolveSandboxMode } from './exporter.js';
|
|
7
7
|
import { createSlideshow } from './slideshow.js';
|
|
8
8
|
|
|
9
9
|
const MARKDOWN_EXTENSIONS = new Set(['.md', '.markdown', '.mdown', '.mdx']);
|
|
@@ -94,7 +94,9 @@ export async function openTarget(targetPath, options = {}) {
|
|
|
94
94
|
fetchRemote = true,
|
|
95
95
|
returnContent = false,
|
|
96
96
|
maxContentBytes = 50 * 1024 * 1024,
|
|
97
|
+
sandbox,
|
|
97
98
|
} = options;
|
|
99
|
+
const resolvedSandbox = resolveSandboxMode(sandbox, process.env);
|
|
98
100
|
|
|
99
101
|
const requestedMode = normalizeOpenMode(mode, 'auto');
|
|
100
102
|
const preferredMode = requestedMode === 'auto'
|
|
@@ -258,6 +260,7 @@ export async function openTarget(targetPath, options = {}) {
|
|
|
258
260
|
outDir: outputDir,
|
|
259
261
|
fileName: `${name}.pdf`,
|
|
260
262
|
htmlPath,
|
|
263
|
+
sandbox: resolvedSandbox,
|
|
261
264
|
});
|
|
262
265
|
warnings.push(...pdf.warnings);
|
|
263
266
|
|
package/src/mcp/server.js
CHANGED
|
@@ -2,12 +2,12 @@ import path from 'node:path';
|
|
|
2
2
|
import { promises as fs } from 'node:fs';
|
|
3
3
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
4
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
-
import * as z from 'zod/v4';
|
|
6
5
|
import { renderMarkdown } from '../core/renderer.js';
|
|
7
|
-
import { exportDOCX, exportPDF } from '../core/exporter.js';
|
|
6
|
+
import { exportDOCX, exportPDF, resolveSandboxMode } from '../core/exporter.js';
|
|
8
7
|
import { createSlideshow } from '../core/slideshow.js';
|
|
9
8
|
import { openTarget } from '../core/opener.js';
|
|
10
9
|
import { createOutputDir } from '../utils/output.js';
|
|
10
|
+
import { MCP_TOOL_SCHEMAS } from './toolSchemas.js';
|
|
11
11
|
import {
|
|
12
12
|
getPreferencesPath,
|
|
13
13
|
loadPreferences,
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from '../utils/preferences.js';
|
|
17
17
|
|
|
18
18
|
const MAX_CONTENT_BYTES = 50 * 1024 * 1024;
|
|
19
|
+
const MCP_SANDBOX_MODE = resolveSandboxMode(process.env.AGENT_READER_SANDBOX, process.env);
|
|
19
20
|
|
|
20
21
|
function getBaseDirFromSourcePath(sourcePath) {
|
|
21
22
|
if (!sourcePath) {
|
|
@@ -71,21 +72,12 @@ async function saveHtmlResult(html, outputDir, name = 'output') {
|
|
|
71
72
|
|
|
72
73
|
const server = new McpServer({
|
|
73
74
|
name: 'agent-reader',
|
|
74
|
-
version: '
|
|
75
|
+
version: '1.1.0',
|
|
75
76
|
});
|
|
76
77
|
|
|
77
78
|
server.registerTool(
|
|
78
79
|
'render_markdown',
|
|
79
|
-
|
|
80
|
-
description: 'Render markdown text into styled HTML',
|
|
81
|
-
inputSchema: {
|
|
82
|
-
content: z.string().describe('Markdown source content'),
|
|
83
|
-
source_path: z.string().optional().describe('Source markdown path for relative images'),
|
|
84
|
-
theme: z.string().optional().describe('Theme name'),
|
|
85
|
-
auto_open: z.boolean().optional().describe('Open output automatically (ignored in MCP)'),
|
|
86
|
-
return_content: z.boolean().optional().describe('Return inline HTML content directly'),
|
|
87
|
-
},
|
|
88
|
-
},
|
|
80
|
+
MCP_TOOL_SCHEMAS.render_markdown,
|
|
89
81
|
async ({ content, source_path, theme, return_content }) => {
|
|
90
82
|
try {
|
|
91
83
|
const baseDir = getBaseDirFromSourcePath(source_path);
|
|
@@ -142,15 +134,7 @@ server.registerTool(
|
|
|
142
134
|
|
|
143
135
|
server.registerTool(
|
|
144
136
|
'export_document',
|
|
145
|
-
|
|
146
|
-
description: 'Export markdown text into PDF or DOCX',
|
|
147
|
-
inputSchema: {
|
|
148
|
-
content: z.string().describe('Markdown source content'),
|
|
149
|
-
source_path: z.string().optional().describe('Source markdown path for relative images'),
|
|
150
|
-
format: z.enum(['pdf', 'docx']).describe('Export format'),
|
|
151
|
-
return_content: z.boolean().optional().describe('Return file bytes as base64'),
|
|
152
|
-
},
|
|
153
|
-
},
|
|
137
|
+
MCP_TOOL_SCHEMAS.export_document,
|
|
154
138
|
async ({ content, source_path, format, return_content }) => {
|
|
155
139
|
try {
|
|
156
140
|
const baseDir = getBaseDirFromSourcePath(source_path);
|
|
@@ -174,6 +158,7 @@ server.registerTool(
|
|
|
174
158
|
outDir: outputDir,
|
|
175
159
|
fileName: 'export.pdf',
|
|
176
160
|
htmlPath,
|
|
161
|
+
sandbox: MCP_SANDBOX_MODE,
|
|
177
162
|
});
|
|
178
163
|
filePath = pdf.pdfPath;
|
|
179
164
|
warnings = [...warnings, ...rendered.warnings, ...pdf.warnings];
|
|
@@ -223,15 +208,7 @@ server.registerTool(
|
|
|
223
208
|
|
|
224
209
|
server.registerTool(
|
|
225
210
|
'create_slideshow',
|
|
226
|
-
|
|
227
|
-
description: 'Create slideshow HTML from an image directory',
|
|
228
|
-
inputSchema: {
|
|
229
|
-
image_dir: z.string().describe('Absolute or relative image directory path'),
|
|
230
|
-
auto_play: z.number().optional().describe('Autoplay interval in seconds'),
|
|
231
|
-
auto_open: z.boolean().optional().describe('Open output automatically (ignored in MCP)'),
|
|
232
|
-
return_content: z.boolean().optional().describe('Return inline HTML content directly'),
|
|
233
|
-
},
|
|
234
|
-
},
|
|
211
|
+
MCP_TOOL_SCHEMAS.create_slideshow,
|
|
235
212
|
async ({ image_dir, auto_play, return_content }) => {
|
|
236
213
|
try {
|
|
237
214
|
const outputDir = await createOutputDir('create-slideshow');
|
|
@@ -278,16 +255,7 @@ server.registerTool(
|
|
|
278
255
|
|
|
279
256
|
server.registerTool(
|
|
280
257
|
'open_file',
|
|
281
|
-
|
|
282
|
-
description: 'Open a local file/path using user preference or explicit mode: web/word/pdf/ppt',
|
|
283
|
-
inputSchema: {
|
|
284
|
-
file_path: z.string().describe('File path or image directory path'),
|
|
285
|
-
open_as: z.string().optional().describe('auto|web|word|pdf|ppt'),
|
|
286
|
-
theme: z.string().optional().describe('theme for web rendering'),
|
|
287
|
-
auto_play: z.number().optional().describe('auto play seconds for ppt mode'),
|
|
288
|
-
return_content: z.boolean().optional().describe('return generated content directly'),
|
|
289
|
-
},
|
|
290
|
-
},
|
|
258
|
+
MCP_TOOL_SCHEMAS.open_file,
|
|
291
259
|
async ({ file_path, open_as, theme, auto_play, return_content }) => {
|
|
292
260
|
try {
|
|
293
261
|
const preferences = await loadPreferences();
|
|
@@ -299,6 +267,7 @@ server.registerTool(
|
|
|
299
267
|
autoPlay: auto_play,
|
|
300
268
|
returnContent: Boolean(return_content),
|
|
301
269
|
maxContentBytes: MAX_CONTENT_BYTES,
|
|
270
|
+
sandbox: MCP_SANDBOX_MODE,
|
|
302
271
|
});
|
|
303
272
|
|
|
304
273
|
const payload = {
|
|
@@ -319,13 +288,7 @@ server.registerTool(
|
|
|
319
288
|
|
|
320
289
|
server.registerTool(
|
|
321
290
|
'configure_user_preferences',
|
|
322
|
-
|
|
323
|
-
description: 'Set default open behavior for novice users',
|
|
324
|
-
inputSchema: {
|
|
325
|
-
default_open_mode: z.string().optional().describe('web|word|pdf|ppt'),
|
|
326
|
-
default_theme: z.string().optional().describe('default web theme'),
|
|
327
|
-
},
|
|
328
|
-
},
|
|
291
|
+
MCP_TOOL_SCHEMAS.configure_user_preferences,
|
|
329
292
|
async ({ default_open_mode, default_theme }) => {
|
|
330
293
|
try {
|
|
331
294
|
const updates = {};
|
|
@@ -352,10 +315,7 @@ server.registerTool(
|
|
|
352
315
|
|
|
353
316
|
server.registerTool(
|
|
354
317
|
'get_user_preferences',
|
|
355
|
-
|
|
356
|
-
description: 'Read current user preferences for open behavior',
|
|
357
|
-
inputSchema: {},
|
|
358
|
-
},
|
|
318
|
+
MCP_TOOL_SCHEMAS.get_user_preferences,
|
|
359
319
|
async () => {
|
|
360
320
|
try {
|
|
361
321
|
const preferences = await loadPreferences();
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as z from 'zod/v4';
|
|
2
|
+
|
|
3
|
+
export const MCP_TOOL_SCHEMAS = {
|
|
4
|
+
render_markdown: {
|
|
5
|
+
description: 'Render markdown text into styled HTML',
|
|
6
|
+
inputSchema: {
|
|
7
|
+
content: z.string().describe('Markdown source content'),
|
|
8
|
+
source_path: z.string().optional().describe('Source markdown path for relative images'),
|
|
9
|
+
theme: z.string().optional().describe('Theme name'),
|
|
10
|
+
auto_open: z.boolean().optional().describe('Open output automatically (ignored in MCP)'),
|
|
11
|
+
return_content: z.boolean().optional().describe('Return inline HTML content directly'),
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
export_document: {
|
|
15
|
+
description: 'Export markdown text into PDF or DOCX',
|
|
16
|
+
inputSchema: {
|
|
17
|
+
content: z.string().describe('Markdown source content'),
|
|
18
|
+
source_path: z.string().optional().describe('Source markdown path for relative images'),
|
|
19
|
+
format: z.enum(['pdf', 'docx']).describe('Export format'),
|
|
20
|
+
return_content: z.boolean().optional().describe('Return file bytes as base64'),
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
create_slideshow: {
|
|
24
|
+
description: 'Create slideshow HTML from an image directory',
|
|
25
|
+
inputSchema: {
|
|
26
|
+
image_dir: z.string().describe('Absolute or relative image directory path'),
|
|
27
|
+
auto_play: z.number().optional().describe('Autoplay interval in seconds'),
|
|
28
|
+
auto_open: z.boolean().optional().describe('Open output automatically (ignored in MCP)'),
|
|
29
|
+
return_content: z.boolean().optional().describe('Return inline HTML content directly'),
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
open_file: {
|
|
33
|
+
description: 'Open a local file/path using user preference or explicit mode: web/word/pdf/ppt',
|
|
34
|
+
inputSchema: {
|
|
35
|
+
file_path: z.string().describe('File path or image directory path'),
|
|
36
|
+
open_as: z.string().optional().describe('auto|web|word|pdf|ppt'),
|
|
37
|
+
theme: z.string().optional().describe('theme for web rendering'),
|
|
38
|
+
auto_play: z.number().optional().describe('auto play seconds for ppt mode'),
|
|
39
|
+
return_content: z.boolean().optional().describe('return generated content directly'),
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
configure_user_preferences: {
|
|
43
|
+
description: 'Set default open behavior for novice users',
|
|
44
|
+
inputSchema: {
|
|
45
|
+
default_open_mode: z.string().optional().describe('web|word|pdf|ppt'),
|
|
46
|
+
default_theme: z.string().optional().describe('default web theme'),
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
get_user_preferences: {
|
|
50
|
+
description: 'Read current user preferences for open behavior',
|
|
51
|
+
inputSchema: {},
|
|
52
|
+
},
|
|
53
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
async function resolveWithNearestParent(targetPath) {
|
|
5
|
+
const absoluteTarget = path.resolve(targetPath);
|
|
6
|
+
try {
|
|
7
|
+
const realTarget = await fs.realpath(absoluteTarget);
|
|
8
|
+
return { ok: true, realTarget };
|
|
9
|
+
} catch (error) {
|
|
10
|
+
if (error?.code !== 'ENOENT') {
|
|
11
|
+
return { ok: false };
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const missingSegments = [];
|
|
16
|
+
let cursor = absoluteTarget;
|
|
17
|
+
|
|
18
|
+
while (true) {
|
|
19
|
+
const parent = path.dirname(cursor);
|
|
20
|
+
if (parent === cursor) {
|
|
21
|
+
return { ok: false };
|
|
22
|
+
}
|
|
23
|
+
missingSegments.push(path.basename(cursor));
|
|
24
|
+
cursor = parent;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const realParent = await fs.realpath(cursor);
|
|
28
|
+
return {
|
|
29
|
+
ok: true,
|
|
30
|
+
realTarget: path.join(realParent, ...missingSegments.reverse()),
|
|
31
|
+
};
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error?.code !== 'ENOENT') {
|
|
34
|
+
return { ok: false };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function assertWithinBase(targetPath, baseDir) {
|
|
41
|
+
if (!baseDir) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let realBase;
|
|
46
|
+
try {
|
|
47
|
+
realBase = await fs.realpath(path.resolve(baseDir));
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const resolved = await resolveWithNearestParent(targetPath);
|
|
53
|
+
if (!resolved.ok) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const relative = path.relative(realBase, resolved.realTarget);
|
|
58
|
+
if (!relative) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
return !relative.startsWith('..') && !path.isAbsolute(relative);
|
|
62
|
+
}
|
package/src/utils/server.js
CHANGED
|
@@ -2,7 +2,7 @@ import http from 'node:http';
|
|
|
2
2
|
import net from 'node:net';
|
|
3
3
|
import { promises as fs } from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import { exportDOCXFromHTML, exportPDF } from '../core/exporter.js';
|
|
5
|
+
import { exportDOCXFromHTML, exportPDF, resolveSandboxMode } from '../core/exporter.js';
|
|
6
6
|
|
|
7
7
|
const MIME_TYPES = {
|
|
8
8
|
'.html': 'text/html; charset=utf-8',
|
|
@@ -94,7 +94,7 @@ async function resolveExportSourcePath(req, rootDir, sourceParam) {
|
|
|
94
94
|
throw createHttpError(400, 'missing source path');
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
async function handleExportRequest(req, res, rootDir, urlObject) {
|
|
97
|
+
async function handleExportRequest(req, res, rootDir, urlObject, sandbox) {
|
|
98
98
|
if (req.method !== 'GET') {
|
|
99
99
|
sendJson(res, 405, { error: 'method not allowed' });
|
|
100
100
|
return;
|
|
@@ -131,6 +131,7 @@ async function handleExportRequest(req, res, rootDir, urlObject) {
|
|
|
131
131
|
fileName: `${sourceName}.pdf`,
|
|
132
132
|
htmlPath: sourcePath,
|
|
133
133
|
landscape: isLandscape,
|
|
134
|
+
sandbox,
|
|
134
135
|
});
|
|
135
136
|
outputPath = result.pdfPath;
|
|
136
137
|
warnings = result.warnings || [];
|
|
@@ -140,6 +141,7 @@ async function handleExportRequest(req, res, rootDir, urlObject) {
|
|
|
140
141
|
baseDir: sourceDir,
|
|
141
142
|
outDir: sourceDir,
|
|
142
143
|
fileName: `${sourceName}.docx`,
|
|
144
|
+
sandbox,
|
|
143
145
|
});
|
|
144
146
|
outputPath = result.docxPath;
|
|
145
147
|
warnings = result.warnings || [];
|
|
@@ -190,14 +192,18 @@ async function isPortInUse(host, port) {
|
|
|
190
192
|
});
|
|
191
193
|
}
|
|
192
194
|
|
|
193
|
-
export async function startStaticServer(
|
|
195
|
+
export async function startStaticServer(
|
|
196
|
+
rootDir,
|
|
197
|
+
{ host = '127.0.0.1', port = 3000, sandbox } = {},
|
|
198
|
+
) {
|
|
194
199
|
const absoluteRoot = path.resolve(rootDir);
|
|
200
|
+
const resolvedSandbox = resolveSandboxMode(sandbox, process.env);
|
|
195
201
|
|
|
196
202
|
const server = http.createServer(async (req, res) => {
|
|
197
203
|
const urlObject = new URL(req.url || '/', `http://${host}:${port}`);
|
|
198
204
|
try {
|
|
199
205
|
if (urlObject.pathname === '/api/export') {
|
|
200
|
-
await handleExportRequest(req, res, absoluteRoot, urlObject);
|
|
206
|
+
await handleExportRequest(req, res, absoluteRoot, urlObject, resolvedSandbox);
|
|
201
207
|
return;
|
|
202
208
|
}
|
|
203
209
|
|