Skill: sube-py/pdflux-test (version 1.0.3)

--- name: PDFlux-PDF2Markdown description: Convert unstructured documents into LLM-ready structured data. Supports PDF, Word, PPT, and images; extracts paragraphs, formulas, tables, charts, and other elements in one step; generates up to 8 levels of headings; and outputs Markdown organized in reading order. Useful for field extraction, comparison and validation, knowledge retrieval, and intelligent Q&A. metadata: {"author":"PAODINGAI","version":"1.0.2","openclaw":{"emoji":"📝","primaryEnv":"PD_ROUTER_API_KEY","requires":{"env":["PD_ROUTER_API_KEY"],"bins":["node"]}}} --- # PDFlux-PDF2Markdown Run a JavaScript workflow that uploads a single local file to the `pdflux` service through PDRouter, polls the parsing status, and then downloads the resulting Markdown. This is suitable for document parsing, table extraction, content verification, and handing document content off to follow-up scripts. ## Installation ```bash npx skills add PaodingAI/skills ``` ## Usage ```bash node skills/pdflux-saas-markdown/scripts/upload_to_markdown.js <local-file-path> [output-markdown-path] ``` ## Execution Constraints - You must invoke `scripts/upload_to_markdown.js` directly. Do not reimplement the upload, polling, and Markdown download flow yourself. - The behavior contract below explains what the script does, what it outputs, and when to use it. It is not a manual checklist for the model to imitate step by step. - Even if the task is only to extract tables, read fields, inspect body text, or prepare input for later scripts, you must run this script first and continue from the generated Markdown. - Only inspect or modify the script implementation when the script itself is unavailable, failing, or needs a fix. Do not bypass it during normal use. ## When to Use - Use this skill when the user wants to parse a document, retrieve specific document content, or extract tables from a document. - Use this skill when the user says things like "convert to Markdown", "output Markdown", "export Markdown", or "extract Markdown", and return the Markdown content directly. - When later work depends on the document content, such as summarization, field extraction, document-processing scripts, table comparison, or rule-based validation, use this skill first to parse the document. - When the document content is only needed as input for subsequent steps, do not default to showing the full raw Markdown to the user. Prefer saving it to a temporary or working file first, then read, filter, and extract only what is needed. - When the user explicitly asks for the original Markdown output or clearly wants a direct document-to-Markdown conversion, show the full Markdown directly. ## Environment Variables - `PD_ROUTER_API_KEY`: Required. The Bearer API key for PDRouter. If it is missing, the script fails immediately. In a skill workflow, the AI should ask the user to provide a valid key, or inject it into the environment before retrying. The API key can be obtained from the PDRouter platform: [https://platform.paodingai.com/](https://platform.paodingai.com/) - `PDFLUX_INCLUDE_IMAGES`: Optional. Boolean. Markdown output does not include image data by default. ## Security and Data Flow - This skill uploads one user-specified local file to the official PDRouter endpoint at `https://platform.paodingai.com`. - The script reads `PD_ROUTER_API_KEY` only to authenticate requests to the official PDRouter service. - The script does not print the API key to stdout or stderr. - Generated Markdown is written to stdout and, if requested, to the user-provided output file path. - The script does not support arbitrary network targets or custom upload hosts. ## Default Behavior and Optional Settings - Parsed results do not include chart or image extraction by default. - If charts, images, or similar content are required, enable them explicitly through API parameters. These results are usually returned as base64 and will increase token usage. - Markdown output does not include image data by default. If you need embedded image data, set `PDFLUX_INCLUDE_IMAGES=true`. ## Script Behavior 1. Read the token from `PD_ROUTER_API_KEY`. If it is missing, fail immediately and prompt the AI to ask the user for a key or inject the environment variable first. 2. Upload the file to the official PDRouter endpoint with `POST /openapi/{serviceCode}/upload` using `Authorization: Bearer <token>`. 3. Poll `GET /openapi/{serviceCode}/document/{uuid}` until `parsed === 2`. 4. Fail immediately if the parsing status becomes negative. 5. Download the Markdown from `GET /openapi/{serviceCode}/document/{uuid}/markdown`. 6. If `output-markdown-path` is provided, the script also writes the Markdown to that file while still printing it to stdout. 7. The script writes progress and errors to stderr and returns a non-zero exit code on failure. 8. When the goal is to retrieve specific content, fields, or tables, read the parsed result and return only the necessary information instead of echoing the full raw Markdown to the user. 9. When the user explicitly asks to "convert to Markdown", "output Markdown", or expresses an equivalent intent, return the Markdown content directly rather than only a summary or extracted fields.

path: scripts/upload_to_markdown.js
content: #!/usr/bin/env node

const fs = require('node:fs/promises');
const path = require('node:path');
const { stderr, stdout } = require('node:process');

const PD_ROUTER_BASE_URL = 'https://platform.paodingai.com';
const DEFAULT_SERVICE_CODE = 'pdflux';
const DEFAULT_POLL_INTERVAL_MS = 2000;
const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

function normalizeServiceCode(serviceCode) {
  return (serviceCode || DEFAULT_SERVICE_CODE).trim() || DEFAULT_SERVICE_CODE;
}

function parseBooleanEnv(name) {
  const rawValue = process.env[name];
  if (rawValue == null) {
    return null;
  }

  const normalizedValue = rawValue.trim().toLowerCase();
  if (['1', 'true', 'yes', 'on'].includes(normalizedValue)) {
    return true;
  }
  if (['0', 'false', 'no', 'off'].includes(normalizedValue)) {
    return false;
  }

  throw new Error(`${name} must be a boolean string like true/false/1/0.`);
}

function requireGatewayApiKey() {
  const fromEnv = (process.env.PD_ROUTER_API_KEY || '').trim();
  if (fromEnv) {
    return fromEnv;
  }

  throw new Error(
    '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.',
  );
}

async function parseResponse(response) {
  const contentType = response.headers.get('content-type') || '';
  const bodyText = await response.text();
  if (contentType.includes('application/json')) {
    try {
      return JSON.parse(bodyText);
    } catch {
      return { status: false, msg: bodyText || 'Invalid JSON response' };
    }
  }
  return bodyText;
}

function extractApiError(payload, fallback) {
  if (!payload) {
    return fallback;
  }
  if (typeof payload === 'string') {
    return payload || fallback;
  }
  if (typeof payload === 'object') {
    return payload.code || payload.msg || payload.message || JSON.stringify(payload);
  }
  return fallback;
}

function buildOpenApiUrl(serviceCode, endpoint) {
  const normalizedEndpoint = endpoint.replace(/^\/+/, '');
  return new URL(`/openapi/${serviceCode}/${normalizedEndpoint}`, `${PD_ROUTER_BASE_URL}/`).toString();
}

function buildAuthHeaders(apiKey) {
  return {
    Authorization: `Bearer ${apiKey}`,
  };
}

async function uploadFile({ serviceCode, apiKey, filePath, forceUpdate, forceOcr }) {
  const formData = new FormData();
  const filename = path.basename(filePath);
  const bytes = await fs.readFile(filePath);
  const fileBlob = new Blob([bytes], { type: 'application/pdf' });
  formData.append('file', fileBlob, filename);
  formData.append('force_update', String(forceUpdate));
  formData.append('force_ocr', String(forceOcr));

  const response = await fetch(buildOpenApiUrl(serviceCode, 'upload'), {
    method: 'POST',
    headers: buildAuthHeaders(apiKey),
    body: formData,
  });

  const payload = await parseResponse(response);
  if (!response.ok) {
    throw new Error(`Upload failed (${response.status}): ${extractApiError(payload, 'Request failed')}`);
  }
  if (typeof payload !== 'object' || payload.status === false) {
    throw new Error(`Upload failed: ${extractApiError(payload, 'Invalid upload response')}`);
  }

  const uuid = payload?.data?.uuid;
  if (!uuid) {
    throw new Error(`Upload succeeded but uuid is missing: ${JSON.stringify(payload)}`);
  }

  return uuid;
}

async function pollParsed({ serviceCode, apiKey, uuid, pollIntervalMs, timeoutMs }) {
  const startTime = Date.now();

  while (Date.now() - startTime < timeoutMs) {
    const response = await fetch(buildOpenApiUrl(serviceCode, `document/${uuid}`), {
      method: 'GET',
      headers: buildAuthHeaders(apiKey),
    });

    const payload = await parseResponse(response);
    if (!response.ok) {
      throw new Error(`Polling failed (${response.status}): ${extractApiError(payload, 'Request failed')}`);
    }
    if (typeof payload !== 'object' || payload.status === false) {
      throw new Error(`Polling failed: ${extractApiError(payload, 'Invalid status response')}`);
    }

    const parsed = payload?.data?.parsed;
    if (parsed === 2) {
      return payload;
    }
    if (typeof parsed === 'number' && parsed < 0) {
      throw new Error(`Parsing failed with status ${parsed}: ${extractApiError(payload, 'Parse failed')}`);
    }

    await sleep(pollIntervalMs);
  }

  throw new Error(`Polling timed out after ${Math.floor(timeoutMs / 1000)} seconds.`);
}

async function downloadMarkdown({ serviceCode, apiKey, uuid, outputPath, includeImages }) {
  const endpoint = includeImages ? `document/${uuid}/markdown?include_images=true` : `document/${uuid}/markdown`;
  const response = await fetch(buildOpenApiUrl(serviceCode, endpoint), {
    method: 'GET',
    headers: buildAuthHeaders(apiKey),
  });

  const contentType = response.headers.get('content-type') || '';
  const bodyText = await response.text();

  if (!response.ok) {
    let errorMessage = bodyText;
    if (contentType.includes('application/json')) {
      try {
        const payload = JSON.parse(bodyText);
        errorMessage = extractApiError(payload, bodyText);
      } catch {
        // keep bodyText
      }
    }
    throw new Error(`Markdown download failed (${response.status}): ${errorMessage || 'Request failed'}`);
  }

  if (contentType.includes('application/json')) {
    try {
      const payload = JSON.parse(bodyText);
      if (payload?.status === false) {
        throw new Error(`Markdown download failed: ${extractApiError(payload, 'API returned error')}`);
      }
    } catch (error) {
      if (error instanceof Error) {
        throw error;
      }
    }
  }

  if (outputPath) {
    await fs.writeFile(outputPath, bodyText, 'utf8');
  }
  return bodyText;
}

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

  const resolvedPath = path.resolve(filePathArg);
  let stat;
  try {
    stat = await fs.stat(resolvedPath);
  } catch {
    throw new Error(`Input file does not exist: ${resolvedPath}`);
  }

  if (!stat.isFile()) {
    throw new Error(`Input path is not a file: ${resolvedPath}`);
  }

  return resolvedPath;
}

async function main() {
  const filePath = await ensureInputFile(process.argv[2]);
  const outputPath = process.argv[3] ? path.resolve(process.argv[3]) : null;
  const [REDACTED_SECRET]);
  const serviceCode = normalizeServiceCode(DEFAULT_SERVICE_CODE);
  const forceUpdate = (process.env.PDFLUX_FORCE_UPDATE || 'true').trim().toLowerCase() !== 'false';
  const forceOcr = (process.env.PDFLUX_FORCE_OCR || 'true').trim().toLowerCase() !== 'false';
  const includeImages = parseBooleanEnv('PDFLUX_INCLUDE_IMAGES') === true;

  stderr.write(`[pd-router-pdflux-markdown] Uploading ${path.basename(filePath)} via ${PD_ROUTER_BASE_URL}\n`);
  const uuid = await uploadFile({ serviceCode, apiKey, filePath, forceUpdate, forceOcr });

  stderr.write(`[pd-router-pdflux-markdown] Uploaded uuid=${uuid}\n`);
  stderr.write('[pd-router-pdflux-markdown] Polling parse status\n');
  await pollParsed({
    serviceCode,
    apiKey,
    uuid,
    pollIntervalMs: DEFAULT_POLL_INTERVAL_MS,
    timeoutMs: DEFAULT_TIMEOUT_MS,
  });

  stderr.write('[pd-router-pdflux-markdown] Parse completed, downloading markdown\n');
  const markdown = await downloadMarkdown({
    serviceCode,
    apiKey,
    uuid,
    outputPath,
    includeImages,
  });

  if (outputPath) {
    stderr.write(`[pd-router-pdflux-markdown] Markdown saved at ${outputPath}\n`);
  }

  stdout.write(markdown);
  if (!markdown.endsWith('\n')) {
    stdout.write('\n');
  }
}

main().catch(error => {
  stderr.write(`[pd-router-pdflux-markdown] ${error instanceof Error ? error.message : String(error)}\n`);
  process.exitCode = 1;
});


path: agents/openai.yaml
content: interface:
  display_name: "PDFlux SaaS Markdown"
  short_description: "Parse docs and tables from PDF, DOCX, and images"
  default_prompt: "Always execute the bundled scripts/upload_to_markdown.js script to parse one local PDF, DOCX, PPT, image, or similar file. If the user explicitly asks to convert to markdown or output markdown, return the markdown content directly; otherwise extract only the document content or tables needed for the task."


Security scan verdicts — ClawScan: clean | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)