Skip to content

Commit cbfb0a0

Browse files
committed
Update pdflux-saas-markdown skill.
1 parent f4c6900 commit cbfb0a0

2 files changed

Lines changed: 134 additions & 108 deletions

File tree

pdflux-saas-markdown/SKILL.md

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
11
---
2-
name: pdflux-saas-markdown
3-
description: 解析文档并获取文档内容,尤其适用于提取 PDF、DOCX、DOC、PPT、PPTX、PNG、JPG、JPEG 等文件中的正文与表格内容。用于将文件转换为 markdown、响应“转 markdown”这类需求、读取文档具体内容、基于文档内容执行后续分析或脚本处理;当需要编写脚本解析文档时,优先使用这个 skill
2+
name: PDFlux-PDF2Markdown
3+
description: 将非结构化文档转化为“大模型 Ready”的结构化数据。支持 PDF、Word、PPT 及图片,一键提取段落、公式、表格、图表等元素,生成最高 8 级目录索引,并按阅读逻辑组织输出 Markdown。应用场景:字段抽取、对比校验、知识检索与智能问答
44
---
55

6-
# pdflux-saas-markdown
6+
# PDFlux-PDF2Markdown
77

8-
执行一个 JavaScript 工作流,先把单个本地文件解析为 markdown,再基于 markdown 获取正文、表格和结构化内容。支持 PDF、Word、PPT 和图片等常见格式,适合文档解析、表格提取、内容核对,以及把文档内容交给后续脚本继续处理。
8+
执行一个 JavaScript 工作流,先通过 PDRouter 上传单个本地文件到 `pdflux` 服务,再轮询解析状态并下载 markdown。适合文档解析、表格提取、内容核对,以及把文档内容交给后续脚本继续处理。
9+
10+
## 安装方式
11+
12+
```bash
13+
npx skills add PaodingAI/skills
14+
```
915

1016
## 运行方式
1117

1218
```bash
13-
node .claude/skills/pdflux-saas-markdown/scripts/upload_to_markdown.js <local-file-path>
19+
node skills/pdflux-sass-markdown/scripts/upload_to_markdown.js <local-file-path> [output-markdown-path]
1420
```
1521

1622
## 执行约束
1723

18-
- 必须直接调用 `scripts/upload_to_markdown.js` 执行,不要根据下方行为约定自行重写上传、轮询、下载 markdown 的流程。
24+
- 必须直接调用 `scripts/upload_to_markdown.js`,不要自行重写通过 PDRouter 上传、轮询、下载 markdown 的流程。
1925
- 行为约定仅用于说明脚本做什么、输出什么、何时适合使用,不是给模型手工照着执行的步骤。
2026
- 即使任务只是提取表格、获取字段、读取正文或为后续脚本准备输入,也必须先运行该脚本,再基于脚本产出的 markdown 继续处理。
2127
- 只有在脚本本身不可用、报错、或需要修复脚本时,才允许检查或修改脚本实现;在正常使用场景下不要绕过脚本。
@@ -25,22 +31,28 @@ node .claude/skills/pdflux-saas-markdown/scripts/upload_to_markdown.js <local-fi
2531
- 当用户要解析文档、获取文档具体内容或抽取文档表格时,使用这个 skill。
2632
- 当用户输入类似“转 markdown”“输出 markdown”“导出 markdown”“提取 markdown”时,使用这个 skill,并直接输出 markdown 内容。
2733
- 当后续任务依赖文档内容继续处理,例如生成摘要、抽取字段、编写脚本处理文档、对比表格或做规则校验时,优先先用这个 skill 解析文档。
28-
- 当只是需要文档内容供后续操作使用时,不默认向用户输出原始 markdown 全文;优先将 markdown 保存到临时文件,再读取、筛选、提取需要的内容。
34+
- 当只是需要文档内容供后续操作使用时,不默认向用户输出原始 markdown 全文;优先将 markdown 保存到临时文件或工作文件,再读取、筛选、提取需要的内容。
2935
- 当用户明确要求“输出 markdown 原文”或表达的是“转 markdown”类直接转换需求时,直接展示完整 markdown。
3036

3137
## 环境变量
3238

33-
- `PAODINGAI_API_KEY`: 必填访问令牌。若未设置,提示 `Enter PAODINGAI_API_KEY:` 并接收手动输入。
34-
- `PAODINGAI_API_BASE_URL`: 可选 API 域名。默认值:`https://saas.pdflux.com`
39+
- `PD_ROUTER_API_KEY`: 必填。PDRouter 的 Bearer API Key。若未设置,脚本会直接报错;在 skill 场景下,AI 应提示用户提供可用的 key,或先将其注入环境变量后再重试。可通过 PDRouter 平台获取 API Key:[https://platform.paodingai.com/](https://platform.paodingai.com/)
40+
- `PDFLUX_INCLUDE_IMAGES`: 可选。布尔值。等价于在 markdown 接口增加 `include_images=true`;markdown 默认不包含图片数据。
41+
42+
## 默认行为与可选参数
43+
44+
- 文件解析结果默认不包含图表、图片类解析。
45+
- 如果业务需要图表、图片等内容,可通过接口参数显式开启;相关结果通常以 base64 形式返回,会增加额外 tokens 消耗。
46+
- markdown 结果默认不包含图片数据;如果需要包含图片,请在 markdown 接口增加 `include_images: true` 参数,或设置 `PDFLUX_INCLUDE_IMAGES=true`
3547

3648
## 脚本行为说明
3749

38-
1. 优先从 `PAODINGAI_API_KEY` 读取令牌,缺失时回退到交互式输入
39-
2. 使用 `Authorization: Bearer <token>` 调用 `/api/v1/saas/upload` 上传文件。
40-
3. 持续轮询 `/api/v1/saas/document/{uuid}`,直到 `parsed === 2`
50+
1. `PD_ROUTER_API_KEY` 读取令牌;若缺失则立即失败,并提示 AI 向用户索要 key 或先注入环境变量
51+
2. 使用 `Authorization: Bearer <token>` 调用 `POST /openapi/{serviceCode}/upload` 上传文件。
52+
3. 持续轮询 `GET /openapi/{serviceCode}/document/{uuid}`,直到 `parsed === 2`
4153
4. 当解析状态为负值时立即失败。
42-
5.`/api/v1/saas/document/{uuid}/markdown` 下载 markdown。
43-
6. 脚本默认将 markdown 内容写入 stdout,因此在实际使用中,若后续还要继续消费文档内容,优先将 stdout 重定向到临时文件或工作文件,再读取文件内容进行后续处理
44-
7. 当任务目标是获取具体内容、字段或表格时,读取解析结果并只输出必要信息,不向用户直接回显原始 markdown 全文
45-
8. 当用户明确表达“转 markdown”“输出 markdown”或等价意图时,直接返回 markdown 内容,而不是只返回提取后的摘要或字段
46-
9. 将进度与错误写入 stderr,错误时返回非零退出码
54+
5.`GET /openapi/{serviceCode}/document/{uuid}/markdown` 下载 markdown;如有需要,可附带 markdown 查询参数,例如 `include_images=true`
55+
6. 若传入 `output-markdown-path`,脚本会额外将 markdown 写入该文件;同时仍会把 markdown 输出到 stdout
56+
7. 脚本将进度与错误写入 stderr,错误时返回非零退出码
57+
8. 当任务目标是获取具体内容、字段或表格时,读取解析结果并只输出必要信息,不向用户直接回显原始 markdown 全文
58+
9. 当用户明确表达“转 markdown”“输出 markdown”或等价意图时,直接返回 markdown 内容,而不是只返回提取后的摘要或字段

pdflux-saas-markdown/scripts/upload_to_markdown.js

Lines changed: 105 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
const fs = require('node:fs/promises');
44
const path = require('node:path');
5-
const readline = require('node:readline/promises');
6-
const { stdin, stderr, stdout } = require('node:process');
5+
const { stderr, stdout } = require('node:process');
76

8-
const DEFAULT_BASE_URL = 'https://saas.pdflux.com';
7+
const DEFAULT_BASE_URL = (process.env.PD_ROUTER_BASE_URL || 'https://platform.paodingai.com/').trim();
8+
const DEFAULT_SERVICE_CODE = 'pdflux';
99
const DEFAULT_POLL_INTERVAL_MS = 2000;
1010
const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
1111

@@ -17,27 +17,36 @@ function normalizeBaseUrl(url) {
1717
return (url || DEFAULT_BASE_URL).trim().replace(/\/+$/, '');
1818
}
1919

20-
async function readAccessToken() {
21-
const fromEnv = (process.env.PAODINGAI_API_KEY || '').trim();
22-
if (fromEnv) {
23-
return fromEnv;
20+
function normalizeServiceCode(serviceCode) {
21+
return (serviceCode || DEFAULT_SERVICE_CODE).trim() || DEFAULT_SERVICE_CODE;
22+
}
23+
24+
function parseBooleanEnv(name) {
25+
const rawValue = process.env[name];
26+
if (rawValue == null) {
27+
return null;
2428
}
2529

26-
const rl = readline.createInterface({
27-
input: stdin,
28-
output: stderr,
29-
});
30+
const normalizedValue = rawValue.trim().toLowerCase();
31+
if (['1', 'true', 'yes', 'on'].includes(normalizedValue)) {
32+
return true;
33+
}
34+
if (['0', 'false', 'no', 'off'].includes(normalizedValue)) {
35+
return false;
36+
}
3037

31-
try {
32-
const input = await rl.question('Enter PAODINGAI_API_KEY: ');
33-
const token = (input || '').trim();
34-
if (!token) {
35-
throw new Error('PAODINGAI_API_KEY is required but not provided.');
36-
}
37-
return token;
38-
} finally {
39-
rl.close();
38+
throw new Error(`${name} must be a boolean string like true/false/1/0.`);
39+
}
40+
41+
function requireGatewayApiKey() {
42+
const fromEnv = (process.env.PD_ROUTER_API_KEY || '').trim();
43+
if (fromEnv) {
44+
return fromEnv;
4045
}
46+
47+
throw new Error(
48+
'PD_ROUTER_API_KEY is required. This skill script does not prompt for input, so ask the user to provide a PD Router API key or set PD_ROUTER_API_KEY before retrying.',
49+
);
4150
}
4251

4352
async function parseResponse(response) {
@@ -61,100 +70,90 @@ function extractApiError(payload, fallback) {
6170
return payload || fallback;
6271
}
6372
if (typeof payload === 'object') {
64-
return payload.msg || payload.message || JSON.stringify(payload);
73+
return payload.code || payload.msg || payload.message || JSON.stringify(payload);
6574
}
6675
return fallback;
6776
}
6877

69-
async function uploadFile({ baseUrl, token, filePath }) {
78+
function buildOpenApiUrl(baseUrl, serviceCode, endpoint) {
79+
const normalizedEndpoint = endpoint.replace(/^\/+/, '');
80+
return `${baseUrl}/openapi/${serviceCode}/${normalizedEndpoint}`;
81+
}
82+
83+
function buildAuthHeaders(apiKey) {
84+
return {
85+
Authorization: `Bearer ${apiKey}`,
86+
};
87+
}
88+
89+
async function uploadFile({ baseUrl, serviceCode, apiKey, filePath, forceUpdate, forceOcr }) {
7090
const formData = new FormData();
7191
const filename = path.basename(filePath);
7292
const bytes = await fs.readFile(filePath);
73-
const fileBlob = new Blob([bytes], { type: 'application/octet-stream' });
93+
const fileBlob = new Blob([bytes], { type: 'application/pdf' });
7494
formData.append('file', fileBlob, filename);
95+
formData.append('force_update', String(forceUpdate));
96+
formData.append('force_ocr', String(forceOcr));
7597

76-
const response = await fetch(`${baseUrl}/api/v1/saas/upload`, {
98+
const response = await fetch(buildOpenApiUrl(baseUrl, serviceCode, 'upload'), {
7799
method: 'POST',
78-
headers: {
79-
Authorization: `Bearer ${token}`,
80-
},
100+
headers: buildAuthHeaders(apiKey),
81101
body: formData,
82102
});
83103

84104
const payload = await parseResponse(response);
85105
if (!response.ok) {
86-
throw new Error(
87-
`Upload failed (${response.status}): ${extractApiError(payload, 'Request failed')}`,
88-
);
106+
throw new Error(`Upload failed (${response.status}): ${extractApiError(payload, 'Request failed')}`);
89107
}
90-
91108
if (typeof payload !== 'object' || payload.status === false) {
92-
throw new Error(
93-
`Upload failed: ${extractApiError(payload, 'Invalid upload response')}`,
94-
);
109+
throw new Error(`Upload failed: ${extractApiError(payload, 'Invalid upload response')}`);
95110
}
96111

97112
const uuid = payload?.data?.uuid;
98113
if (!uuid) {
99-
throw new Error(
100-
`Upload succeeded but uuid is missing: ${JSON.stringify(payload)}`,
101-
);
114+
throw new Error(`Upload succeeded but uuid is missing: ${JSON.stringify(payload)}`);
102115
}
103116

104117
return uuid;
105118
}
106119

107-
async function pollParsed({ baseUrl, token, uuid, pollIntervalMs, timeoutMs }) {
120+
async function pollParsed({ baseUrl, serviceCode, apiKey, uuid, pollIntervalMs, timeoutMs }) {
108121
const startTime = Date.now();
109122

110123
while (Date.now() - startTime < timeoutMs) {
111-
const response = await fetch(`${baseUrl}/api/v1/saas/document/${uuid}`, {
124+
const response = await fetch(buildOpenApiUrl(baseUrl, serviceCode, `document/${uuid}`), {
112125
method: 'GET',
113-
headers: {
114-
Authorization: `Bearer ${token}`,
115-
},
126+
headers: buildAuthHeaders(apiKey),
116127
});
117128

118129
const payload = await parseResponse(response);
119130
if (!response.ok) {
120-
throw new Error(
121-
`Polling failed (${response.status}): ${extractApiError(payload, 'Request failed')}`,
122-
);
131+
throw new Error(`Polling failed (${response.status}): ${extractApiError(payload, 'Request failed')}`);
123132
}
124133
if (typeof payload !== 'object' || payload.status === false) {
125-
throw new Error(
126-
`Polling failed: ${extractApiError(payload, 'Invalid status response')}`,
127-
);
134+
throw new Error(`Polling failed: ${extractApiError(payload, 'Invalid status response')}`);
128135
}
129136

130137
const parsed = payload?.data?.parsed;
131138
if (parsed === 2) {
132-
return;
139+
return payload;
133140
}
134141
if (typeof parsed === 'number' && parsed < 0) {
135-
throw new Error(
136-
`Parsing failed with status ${parsed}: ${extractApiError(payload, 'Parse failed')}`,
137-
);
142+
throw new Error(`Parsing failed with status ${parsed}: ${extractApiError(payload, 'Parse failed')}`);
138143
}
139144

140145
await sleep(pollIntervalMs);
141146
}
142147

143-
throw new Error(
144-
`Polling timed out after ${Math.floor(timeoutMs / 1000)} seconds.`,
145-
);
148+
throw new Error(`Polling timed out after ${Math.floor(timeoutMs / 1000)} seconds.`);
146149
}
147150

148-
async function downloadMarkdown({ baseUrl, token, uuid }) {
149-
const response = await fetch(
150-
`${baseUrl}/api/v1/saas/document/${uuid}/markdown`,
151-
{
152-
method: 'GET',
153-
headers: {
154-
Authorization: `Bearer ${token}`,
155-
},
156-
},
157-
);
151+
async function downloadMarkdown({ baseUrl, serviceCode, apiKey, uuid, outputPath, includeImages }) {
152+
const endpoint = includeImages ? `document/${uuid}/markdown?include_images=true` : `document/${uuid}/markdown`;
153+
const response = await fetch(buildOpenApiUrl(baseUrl, serviceCode, endpoint), {
154+
method: 'GET',
155+
headers: buildAuthHeaders(apiKey),
156+
});
158157

159158
const contentType = response.headers.get('content-type') || '';
160159
const bodyText = await response.text();
@@ -166,21 +165,17 @@ async function downloadMarkdown({ baseUrl, token, uuid }) {
166165
const payload = JSON.parse(bodyText);
167166
errorMessage = extractApiError(payload, bodyText);
168167
} catch {
169-
// Keep bodyText
168+
// keep bodyText
170169
}
171170
}
172-
throw new Error(
173-
`Markdown download failed (${response.status}): ${errorMessage || 'Request failed'}`,
174-
);
171+
throw new Error(`Markdown download failed (${response.status}): ${errorMessage || 'Request failed'}`);
175172
}
176173

177174
if (contentType.includes('application/json')) {
178175
try {
179176
const payload = JSON.parse(bodyText);
180177
if (payload?.status === false) {
181-
throw new Error(
182-
`Markdown download failed: ${extractApiError(payload, 'API returned error')}`,
183-
);
178+
throw new Error(`Markdown download failed: ${extractApiError(payload, 'API returned error')}`);
184179
}
185180
} catch (error) {
186181
if (error instanceof Error) {
@@ -189,12 +184,15 @@ async function downloadMarkdown({ baseUrl, token, uuid }) {
189184
}
190185
}
191186

187+
if (outputPath) {
188+
await fs.writeFile(outputPath, bodyText, 'utf8');
189+
}
192190
return bodyText;
193191
}
194192

195193
async function ensureInputFile(filePathArg) {
196194
if (!filePathArg) {
197-
throw new Error('Usage: node upload_to_markdown.js <local-file-path>');
195+
throw new Error('Usage: node upload_to_markdown.js <local-file-path> [output-markdown-path]');
198196
}
199197

200198
const resolvedPath = path.resolve(filePathArg);
@@ -214,23 +212,41 @@ async function ensureInputFile(filePathArg) {
214212

215213
async function main() {
216214
const filePath = await ensureInputFile(process.argv[2]);
217-
const token = await readAccessToken();
218-
const baseUrl = normalizeBaseUrl(process.env.PAODINGAI_API_BASE_URL);
219-
220-
const pollIntervalMs = DEFAULT_POLL_INTERVAL_MS;
221-
const timeoutMs = DEFAULT_TIMEOUT_MS;
222-
223-
stderr.write(`[pdflux-saas-markdown] Uploading ${path.basename(filePath)}\n`);
224-
const uuid = await uploadFile({ baseUrl, token, filePath });
215+
const outputPath = process.argv[3] ? path.resolve(process.argv[3]) : null;
216+
const apiKey = requireGatewayApiKey();
217+
const baseUrl = normalizeBaseUrl();
218+
const serviceCode = normalizeServiceCode(process.env.PD_ROUTER_SERVICE_CODE);
219+
const forceUpdate = (process.env.PDFLUX_FORCE_UPDATE || 'true').trim().toLowerCase() !== 'false';
220+
const forceOcr = (process.env.PDFLUX_FORCE_OCR || 'true').trim().toLowerCase() !== 'false';
221+
const includeImages = parseBooleanEnv('PDFLUX_INCLUDE_IMAGES') === true;
222+
223+
stderr.write(`[pd-router-pdflux-markdown] Uploading ${path.basename(filePath)} via ${baseUrl}\n`);
224+
const uuid = await uploadFile({ baseUrl, serviceCode, apiKey, filePath, forceUpdate, forceOcr });
225+
226+
stderr.write(`[pd-router-pdflux-markdown] Uploaded uuid=${uuid}\n`);
227+
stderr.write('[pd-router-pdflux-markdown] Polling parse status\n');
228+
await pollParsed({
229+
baseUrl,
230+
serviceCode,
231+
apiKey,
232+
uuid,
233+
pollIntervalMs: DEFAULT_POLL_INTERVAL_MS,
234+
timeoutMs: DEFAULT_TIMEOUT_MS,
235+
});
225236

226-
stderr.write(`[pdflux-saas-markdown] Uploaded uuid=${uuid}\n`);
227-
stderr.write('[pdflux-saas-markdown] Polling parse status\n');
228-
await pollParsed({ baseUrl, token, uuid, pollIntervalMs, timeoutMs });
237+
stderr.write('[pd-router-pdflux-markdown] Parse completed, downloading markdown\n');
238+
const markdown = await downloadMarkdown({
239+
baseUrl,
240+
serviceCode,
241+
apiKey,
242+
uuid,
243+
outputPath,
244+
includeImages,
245+
});
229246

230-
stderr.write(
231-
'[pdflux-saas-markdown] Parse completed, downloading markdown\n',
232-
);
233-
const markdown = await downloadMarkdown({ baseUrl, token, uuid });
247+
if (outputPath) {
248+
stderr.write(`[pd-router-pdflux-markdown] Markdown saved at ${outputPath}\n`);
249+
}
234250

235251
stdout.write(markdown);
236252
if (!markdown.endsWith('\n')) {
@@ -239,8 +255,6 @@ async function main() {
239255
}
240256

241257
main().catch(error => {
242-
stderr.write(
243-
`[pdflux-saas-markdown] ${error instanceof Error ? error.message : String(error)}\n`,
244-
);
258+
stderr.write(`[pd-router-pdflux-markdown] ${error instanceof Error ? error.message : String(error)}\n`);
245259
process.exitCode = 1;
246260
});

0 commit comments

Comments
 (0)