agent-reader 1.0.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 +213 -0
- package/bin/agent-reader.js +83 -0
- package/package.json +52 -0
- package/src/cli/commands.js +602 -0
- package/src/core/assets.js +429 -0
- package/src/core/exporter.js +710 -0
- package/src/core/opener.js +329 -0
- package/src/core/renderer.js +235 -0
- package/src/core/sanitizer.js +79 -0
- package/src/core/slideshow.js +383 -0
- package/src/core/templates/docx-table.lua +4 -0
- package/src/core/templates/reference.docx +0 -0
- package/src/core/themes/dark.css +256 -0
- package/src/core/themes/light.css +312 -0
- package/src/core/themes/print.css +54 -0
- package/src/mcp/server.js +381 -0
- package/src/templates/document.html +145 -0
- package/src/templates/slideshow.html +42 -0
- package/src/utils/logger.js +64 -0
- package/src/utils/naturalSort.js +12 -0
- package/src/utils/output.js +85 -0
- package/src/utils/preferences.js +89 -0
- package/src/utils/server.js +295 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
+
import * as z from 'zod/v4';
|
|
6
|
+
import { renderMarkdown } from '../core/renderer.js';
|
|
7
|
+
import { exportDOCX, exportPDF } from '../core/exporter.js';
|
|
8
|
+
import { createSlideshow } from '../core/slideshow.js';
|
|
9
|
+
import { openTarget } from '../core/opener.js';
|
|
10
|
+
import { createOutputDir } from '../utils/output.js';
|
|
11
|
+
import {
|
|
12
|
+
getPreferencesPath,
|
|
13
|
+
loadPreferences,
|
|
14
|
+
normalizeOpenMode,
|
|
15
|
+
updatePreferences,
|
|
16
|
+
} from '../utils/preferences.js';
|
|
17
|
+
|
|
18
|
+
const MAX_CONTENT_BYTES = 50 * 1024 * 1024;
|
|
19
|
+
|
|
20
|
+
function getBaseDirFromSourcePath(sourcePath) {
|
|
21
|
+
if (!sourcePath) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
return path.dirname(path.resolve(sourcePath));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function toTextResult(payload) {
|
|
28
|
+
return {
|
|
29
|
+
content: [
|
|
30
|
+
{
|
|
31
|
+
type: 'text',
|
|
32
|
+
text: JSON.stringify(payload, null, 2),
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
structuredContent: payload,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function toErrorResult(error, code = 'internal_error') {
|
|
40
|
+
const payload = {
|
|
41
|
+
error: error instanceof Error ? error.message : String(error),
|
|
42
|
+
code,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
content: [
|
|
47
|
+
{
|
|
48
|
+
type: 'text',
|
|
49
|
+
text: JSON.stringify(payload),
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
structuredContent: payload,
|
|
53
|
+
isError: true,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function byteLengthOfString(value) {
|
|
58
|
+
return Buffer.byteLength(value, 'utf8');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function maybeTooLargePayload(textOrBase64) {
|
|
62
|
+
return byteLengthOfString(textOrBase64) > MAX_CONTENT_BYTES;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function saveHtmlResult(html, outputDir, name = 'output') {
|
|
66
|
+
const htmlPath = path.join(outputDir, `${name}.html`);
|
|
67
|
+
await fs.writeFile(htmlPath, html, 'utf8');
|
|
68
|
+
const size = (await fs.stat(htmlPath)).size;
|
|
69
|
+
return { htmlPath, size };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const server = new McpServer({
|
|
73
|
+
name: 'agent-reader',
|
|
74
|
+
version: '0.2.0',
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
server.registerTool(
|
|
78
|
+
'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
|
+
},
|
|
89
|
+
async ({ content, source_path, theme, return_content }) => {
|
|
90
|
+
try {
|
|
91
|
+
const baseDir = getBaseDirFromSourcePath(source_path);
|
|
92
|
+
const wantsContent = Boolean(return_content);
|
|
93
|
+
|
|
94
|
+
if (wantsContent) {
|
|
95
|
+
const rendered = await renderMarkdown(content, {
|
|
96
|
+
theme: theme || 'light',
|
|
97
|
+
baseDir,
|
|
98
|
+
inlineAll: true,
|
|
99
|
+
fetchRemote: true,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (!maybeTooLargePayload(rendered.html)) {
|
|
103
|
+
return toTextResult({
|
|
104
|
+
content_data: rendered.html,
|
|
105
|
+
format: 'html',
|
|
106
|
+
size: byteLengthOfString(rendered.html),
|
|
107
|
+
warnings: rendered.warnings,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const outputDir = await createOutputDir('render-markdown');
|
|
112
|
+
const stored = await saveHtmlResult(rendered.html, outputDir, 'rendered');
|
|
113
|
+
return toTextResult({
|
|
114
|
+
html_path: stored.htmlPath,
|
|
115
|
+
format: 'html',
|
|
116
|
+
size: stored.size,
|
|
117
|
+
warnings: [...rendered.warnings, 'content_too_large'],
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const outputDir = await createOutputDir('render-markdown');
|
|
122
|
+
const rendered = await renderMarkdown(content, {
|
|
123
|
+
theme: theme || 'light',
|
|
124
|
+
baseDir,
|
|
125
|
+
inlineAll: false,
|
|
126
|
+
fetchRemote: true,
|
|
127
|
+
outDir: outputDir,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const stored = await saveHtmlResult(rendered.html, outputDir, 'rendered');
|
|
131
|
+
return toTextResult({
|
|
132
|
+
html_path: stored.htmlPath,
|
|
133
|
+
format: 'html',
|
|
134
|
+
size: stored.size,
|
|
135
|
+
warnings: rendered.warnings,
|
|
136
|
+
});
|
|
137
|
+
} catch (error) {
|
|
138
|
+
return toErrorResult(error, 'render_failed');
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
server.registerTool(
|
|
144
|
+
'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
|
+
},
|
|
154
|
+
async ({ content, source_path, format, return_content }) => {
|
|
155
|
+
try {
|
|
156
|
+
const baseDir = getBaseDirFromSourcePath(source_path);
|
|
157
|
+
const outputDir = await createOutputDir('export-document');
|
|
158
|
+
let filePath;
|
|
159
|
+
let warnings = [];
|
|
160
|
+
|
|
161
|
+
if (format === 'pdf') {
|
|
162
|
+
const rendered = await renderMarkdown(content, {
|
|
163
|
+
baseDir,
|
|
164
|
+
inlineAll: true,
|
|
165
|
+
fetchRemote: true,
|
|
166
|
+
outDir: outputDir,
|
|
167
|
+
title: 'export',
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const htmlPath = path.join(outputDir, 'export.html');
|
|
171
|
+
await fs.writeFile(htmlPath, rendered.html, 'utf8');
|
|
172
|
+
|
|
173
|
+
const pdf = await exportPDF(rendered.html, {
|
|
174
|
+
outDir: outputDir,
|
|
175
|
+
fileName: 'export.pdf',
|
|
176
|
+
htmlPath,
|
|
177
|
+
});
|
|
178
|
+
filePath = pdf.pdfPath;
|
|
179
|
+
warnings = [...warnings, ...rendered.warnings, ...pdf.warnings];
|
|
180
|
+
} else {
|
|
181
|
+
const docx = await exportDOCX(content, {
|
|
182
|
+
baseDir,
|
|
183
|
+
outDir: outputDir,
|
|
184
|
+
fileName: 'export.docx',
|
|
185
|
+
});
|
|
186
|
+
filePath = docx.docxPath;
|
|
187
|
+
warnings = [...warnings, ...docx.warnings];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const fileBuffer = await fs.readFile(filePath);
|
|
191
|
+
const fileSize = fileBuffer.byteLength;
|
|
192
|
+
|
|
193
|
+
if (return_content) {
|
|
194
|
+
const base64 = fileBuffer.toString('base64');
|
|
195
|
+
if (!maybeTooLargePayload(base64)) {
|
|
196
|
+
return toTextResult({
|
|
197
|
+
content_data: base64,
|
|
198
|
+
format,
|
|
199
|
+
size: fileSize,
|
|
200
|
+
warnings,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return toTextResult({
|
|
205
|
+
file_path: filePath,
|
|
206
|
+
format,
|
|
207
|
+
size: fileSize,
|
|
208
|
+
warnings: [...warnings, 'content_too_large'],
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return toTextResult({
|
|
213
|
+
file_path: filePath,
|
|
214
|
+
format,
|
|
215
|
+
size: fileSize,
|
|
216
|
+
warnings,
|
|
217
|
+
});
|
|
218
|
+
} catch (error) {
|
|
219
|
+
return toErrorResult(error, 'export_failed');
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
server.registerTool(
|
|
225
|
+
'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
|
+
},
|
|
235
|
+
async ({ image_dir, auto_play, return_content }) => {
|
|
236
|
+
try {
|
|
237
|
+
const outputDir = await createOutputDir('create-slideshow');
|
|
238
|
+
const wantsContent = Boolean(return_content);
|
|
239
|
+
const result = await createSlideshow(image_dir, {
|
|
240
|
+
autoPlay: auto_play,
|
|
241
|
+
inlineAll: wantsContent,
|
|
242
|
+
outDir: outputDir,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const htmlPath = path.join(outputDir, 'slideshow.html');
|
|
246
|
+
await fs.writeFile(htmlPath, result.html, 'utf8');
|
|
247
|
+
const fileSize = (await fs.stat(htmlPath)).size;
|
|
248
|
+
|
|
249
|
+
if (wantsContent) {
|
|
250
|
+
if (!maybeTooLargePayload(result.html)) {
|
|
251
|
+
return toTextResult({
|
|
252
|
+
content_data: result.html,
|
|
253
|
+
format: 'html',
|
|
254
|
+
size: byteLengthOfString(result.html),
|
|
255
|
+
warnings: result.warnings,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return toTextResult({
|
|
260
|
+
html_path: htmlPath,
|
|
261
|
+
format: 'html',
|
|
262
|
+
size: fileSize,
|
|
263
|
+
warnings: [...result.warnings, 'content_too_large'],
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return toTextResult({
|
|
268
|
+
html_path: htmlPath,
|
|
269
|
+
format: 'html',
|
|
270
|
+
size: fileSize,
|
|
271
|
+
warnings: result.warnings,
|
|
272
|
+
});
|
|
273
|
+
} catch (error) {
|
|
274
|
+
return toErrorResult(error, 'slideshow_failed');
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
server.registerTool(
|
|
280
|
+
'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
|
+
},
|
|
291
|
+
async ({ file_path, open_as, theme, auto_play, return_content }) => {
|
|
292
|
+
try {
|
|
293
|
+
const preferences = await loadPreferences();
|
|
294
|
+
const requested = normalizeOpenMode(open_as || 'auto', 'auto');
|
|
295
|
+
const result = await openTarget(file_path, {
|
|
296
|
+
mode: requested,
|
|
297
|
+
defaultMode: preferences.default_open_mode,
|
|
298
|
+
theme: theme || preferences.default_theme,
|
|
299
|
+
autoPlay: auto_play,
|
|
300
|
+
returnContent: Boolean(return_content),
|
|
301
|
+
maxContentBytes: MAX_CONTENT_BYTES,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const payload = {
|
|
305
|
+
path: result.path,
|
|
306
|
+
content_data: result.content_data,
|
|
307
|
+
format: result.format,
|
|
308
|
+
size: result.size,
|
|
309
|
+
warnings: result.warnings || [],
|
|
310
|
+
resolved_mode: result.resolved_mode,
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
return toTextResult(payload);
|
|
314
|
+
} catch (error) {
|
|
315
|
+
return toErrorResult(error, 'open_failed');
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
server.registerTool(
|
|
321
|
+
'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
|
+
},
|
|
329
|
+
async ({ default_open_mode, default_theme }) => {
|
|
330
|
+
try {
|
|
331
|
+
const updates = {};
|
|
332
|
+
if (default_open_mode) {
|
|
333
|
+
updates.default_open_mode = normalizeOpenMode(default_open_mode, 'web');
|
|
334
|
+
}
|
|
335
|
+
if (default_theme) {
|
|
336
|
+
updates.default_theme = default_theme;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const preferences = Object.keys(updates).length
|
|
340
|
+
? await updatePreferences(updates)
|
|
341
|
+
: await loadPreferences();
|
|
342
|
+
|
|
343
|
+
return toTextResult({
|
|
344
|
+
preferences,
|
|
345
|
+
config_path: getPreferencesPath(),
|
|
346
|
+
});
|
|
347
|
+
} catch (error) {
|
|
348
|
+
return toErrorResult(error, 'configure_failed');
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
server.registerTool(
|
|
354
|
+
'get_user_preferences',
|
|
355
|
+
{
|
|
356
|
+
description: 'Read current user preferences for open behavior',
|
|
357
|
+
inputSchema: {},
|
|
358
|
+
},
|
|
359
|
+
async () => {
|
|
360
|
+
try {
|
|
361
|
+
const preferences = await loadPreferences();
|
|
362
|
+
return toTextResult({
|
|
363
|
+
preferences,
|
|
364
|
+
config_path: getPreferencesPath(),
|
|
365
|
+
});
|
|
366
|
+
} catch (error) {
|
|
367
|
+
return toErrorResult(error, 'read_preferences_failed');
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
async function main() {
|
|
373
|
+
const transport = new StdioServerTransport();
|
|
374
|
+
await server.connect(transport);
|
|
375
|
+
process.stderr.write('agent-reader MCP server running on stdio\n');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
main().catch((error) => {
|
|
379
|
+
process.stderr.write(`server error: ${error.message}\n`);
|
|
380
|
+
process.exit(1);
|
|
381
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Agent Reader</title>
|
|
7
|
+
{{CSS_INJECTION}}
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<aside id="sidebar">
|
|
11
|
+
<div class="sidebar-header">
|
|
12
|
+
<div class="logo">✨ Agent Reader</div>
|
|
13
|
+
</div>
|
|
14
|
+
<div id="toc-container">
|
|
15
|
+
{{TOC_CONTENT}}
|
|
16
|
+
</div>
|
|
17
|
+
</aside>
|
|
18
|
+
|
|
19
|
+
<main id="main-content">
|
|
20
|
+
<header class="doc-toolbar">
|
|
21
|
+
<button id="btn-toggle-toc" class="action-btn">
|
|
22
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 6h16M4 12h16M4 18h7"/></svg>
|
|
23
|
+
<span>收起目录</span>
|
|
24
|
+
</button>
|
|
25
|
+
<div class="toolbar-right">
|
|
26
|
+
<button id="btn-export-word" class="action-btn primary">
|
|
27
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8.342a2 2 0 0 0-.602-1.43l-4.44-4.342A2 2 0 0 0 13.56 2H6a2 2 0 0 0-2 2z"/><path d="M9 13l2 3 2-3M11 10v6"/></svg>
|
|
28
|
+
导出 Word
|
|
29
|
+
</button>
|
|
30
|
+
<button id="btn-export-pdf" class="action-btn primary">
|
|
31
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6M12 18v-6M9 15l3 3 3-3"/></svg>
|
|
32
|
+
导出 PDF
|
|
33
|
+
</button>
|
|
34
|
+
</div>
|
|
35
|
+
</header>
|
|
36
|
+
|
|
37
|
+
<article class="markdown-body" id="content">
|
|
38
|
+
{{CONTENT}}
|
|
39
|
+
</article>
|
|
40
|
+
</main>
|
|
41
|
+
|
|
42
|
+
<script>
|
|
43
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
44
|
+
const sidebar = document.getElementById('sidebar');
|
|
45
|
+
const mainContent = document.getElementById('main-content');
|
|
46
|
+
const btnToggleToc = document.getElementById('btn-toggle-toc');
|
|
47
|
+
|
|
48
|
+
btnToggleToc.addEventListener('click', () => {
|
|
49
|
+
sidebar.classList.toggle('hidden');
|
|
50
|
+
mainContent.classList.toggle('expanded');
|
|
51
|
+
const span = btnToggleToc.querySelector('span');
|
|
52
|
+
span.textContent = sidebar.classList.contains('hidden') ? '展开目录' : '收起目录';
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const tocItems = document.querySelectorAll('#toc-container li');
|
|
56
|
+
tocItems.forEach((li) => {
|
|
57
|
+
const nestedUl = li.querySelector('ul');
|
|
58
|
+
if (nestedUl) {
|
|
59
|
+
li.classList.add('has-children');
|
|
60
|
+
const toggleArrow = document.createElement('span');
|
|
61
|
+
toggleArrow.className = 'toc-toggle-arrow';
|
|
62
|
+
toggleArrow.innerHTML = '▼';
|
|
63
|
+
li.insertBefore(toggleArrow, li.firstChild);
|
|
64
|
+
toggleArrow.addEventListener('click', (event) => {
|
|
65
|
+
event.stopPropagation();
|
|
66
|
+
li.classList.toggle('collapsed');
|
|
67
|
+
toggleArrow.innerHTML = li.classList.contains('collapsed') ? '▶' : '▼';
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Auto-optimize table column widths: short-content columns stay on one line
|
|
73
|
+
document.querySelectorAll('.markdown-body table').forEach((table) => {
|
|
74
|
+
const rows = table.querySelectorAll('tr');
|
|
75
|
+
if (!rows.length) return;
|
|
76
|
+
const colCount = rows[0].children.length;
|
|
77
|
+
const visualLen = (text) => {
|
|
78
|
+
let n = 0;
|
|
79
|
+
for (const ch of text) n += ch.charCodeAt(0) > 0x7F ? 2 : 1;
|
|
80
|
+
return n;
|
|
81
|
+
};
|
|
82
|
+
for (let col = 0; col < colCount; col++) {
|
|
83
|
+
let maxVLen = 0;
|
|
84
|
+
rows.forEach((row) => {
|
|
85
|
+
const cell = row.children[col];
|
|
86
|
+
if (cell) maxVLen = Math.max(maxVLen, visualLen(cell.textContent.trim()));
|
|
87
|
+
});
|
|
88
|
+
if (maxVLen <= 20) {
|
|
89
|
+
rows.forEach((row) => {
|
|
90
|
+
const cell = row.children[col];
|
|
91
|
+
if (cell) {
|
|
92
|
+
cell.style.whiteSpace = 'nowrap';
|
|
93
|
+
cell.style.width = '1%';
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const getFileName = () => {
|
|
101
|
+
const h1 = document.querySelector('.markdown-body h1');
|
|
102
|
+
return h1 ? h1.innerText.trim() : 'Agent-Document';
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
async function triggerBackendExport(format, btnElement) {
|
|
106
|
+
const originalText = btnElement.innerHTML;
|
|
107
|
+
btnElement.innerHTML = `⏳ 正在请求后端生成完美 ${format.toUpperCase()}...`;
|
|
108
|
+
btnElement.disabled = true;
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const source = encodeURIComponent(window.location.pathname || '/');
|
|
112
|
+
const response = await fetch(`/api/export?format=${format}&source=${source}`);
|
|
113
|
+
if (!response.ok) {
|
|
114
|
+
throw new Error('后端导出接口未响应');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const blob = await response.blob();
|
|
118
|
+
const url = window.URL.createObjectURL(blob);
|
|
119
|
+
const anchor = document.createElement('a');
|
|
120
|
+
anchor.href = url;
|
|
121
|
+
anchor.download = `${getFileName()}.${format}`;
|
|
122
|
+
document.body.appendChild(anchor);
|
|
123
|
+
anchor.click();
|
|
124
|
+
document.body.removeChild(anchor);
|
|
125
|
+
window.URL.revokeObjectURL(url);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error(error);
|
|
128
|
+
window.alert('请求失败。\n要使用一键完美导出,请确保您是通过 "agent-reader render --serve" 启动了本地服务。');
|
|
129
|
+
} finally {
|
|
130
|
+
btnElement.innerHTML = originalText;
|
|
131
|
+
btnElement.disabled = false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
document.getElementById('btn-export-pdf').addEventListener('click', function handlePdf() {
|
|
136
|
+
triggerBackendExport('pdf', this);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
document.getElementById('btn-export-word').addEventListener('click', function handleWord() {
|
|
140
|
+
triggerBackendExport('docx', this);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
</script>
|
|
144
|
+
</body>
|
|
145
|
+
</html>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<meta http-equiv="Content-Security-Policy" content="{{CSP}}" />
|
|
7
|
+
<title>{{TITLE}}</title>
|
|
8
|
+
<style>{{STYLES}}</style>
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div class="shell">
|
|
12
|
+
<div id="slide-toc-drawer" class="slide-toc-drawer" aria-label="目录">
|
|
13
|
+
<h2 class="slide-toc-title">目录</h2>
|
|
14
|
+
<div class="slide-toc-list">
|
|
15
|
+
{{TOC_ITEMS}}
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div class="toolbar">
|
|
20
|
+
<button id="toc-toggle-btn" type="button"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg><span>目录</span></button>
|
|
21
|
+
<button id="fullscreen-btn" type="button"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg><span>全屏</span></button>
|
|
22
|
+
<button id="export-btn" type="button"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg><span>导出</span></button>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div class="swiper main-swiper">
|
|
26
|
+
<div class="swiper-wrapper">
|
|
27
|
+
{{SLIDES}}
|
|
28
|
+
</div>
|
|
29
|
+
<div class="swiper-button-prev"></div>
|
|
30
|
+
<div class="swiper-button-next"></div>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div class="swiper thumb-swiper">
|
|
34
|
+
<div class="swiper-wrapper">
|
|
35
|
+
{{THUMBS}}
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<script nonce="{{NONCE}}">{{SCRIPT}}</script>
|
|
41
|
+
</body>
|
|
42
|
+
</html>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
function writeLine(stream, args) {
|
|
2
|
+
const line = args
|
|
3
|
+
.map((arg) => (typeof arg === 'string' ? arg : JSON.stringify(arg)))
|
|
4
|
+
.join(' ');
|
|
5
|
+
stream.write(`${line}\n`);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function createLogger(profile = 'human', jsonOnly = false) {
|
|
9
|
+
const agentLike = profile === 'agent' || jsonOnly;
|
|
10
|
+
|
|
11
|
+
const info = (...args) => {
|
|
12
|
+
if (agentLike) {
|
|
13
|
+
writeLine(process.stderr, args);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
writeLine(process.stdout, args);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const warn = (...args) => {
|
|
20
|
+
writeLine(process.stderr, args);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const error = (...args) => {
|
|
24
|
+
writeLine(process.stderr, args);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const json = (obj) => {
|
|
28
|
+
process.stdout.write(`${JSON.stringify(obj)}\n`);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const patchConsole = () => {
|
|
32
|
+
if (!agentLike) {
|
|
33
|
+
return () => {};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const original = {
|
|
37
|
+
log: console.log,
|
|
38
|
+
info: console.info,
|
|
39
|
+
warn: console.warn,
|
|
40
|
+
error: console.error,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
console.log = (...args) => writeLine(process.stderr, args);
|
|
44
|
+
console.info = (...args) => writeLine(process.stderr, args);
|
|
45
|
+
console.warn = (...args) => writeLine(process.stderr, args);
|
|
46
|
+
console.error = (...args) => writeLine(process.stderr, args);
|
|
47
|
+
|
|
48
|
+
return () => {
|
|
49
|
+
console.log = original.log;
|
|
50
|
+
console.info = original.info;
|
|
51
|
+
console.warn = original.warn;
|
|
52
|
+
console.error = original.error;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
info,
|
|
58
|
+
warn,
|
|
59
|
+
error,
|
|
60
|
+
json,
|
|
61
|
+
patchConsole,
|
|
62
|
+
isJsonOnly: agentLike,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const collator = new Intl.Collator(undefined, {
|
|
2
|
+
numeric: true,
|
|
3
|
+
sensitivity: 'base',
|
|
4
|
+
});
|
|
5
|
+
|
|
6
|
+
export function naturalCompare(a, b) {
|
|
7
|
+
return collator.compare(a, b);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function naturalSort(values) {
|
|
11
|
+
return [...values].sort(naturalCompare);
|
|
12
|
+
}
|