type: custom

customType: model-snapshot

provider: deepseek

modelApi: openai-completions

modelId: deepseek-chat


type: message

role: user

type: text

text:
[Mon 2026-05-11 09:16 GMT+8] [Subagent Context] You are running as a subagent (depth 1/1). Results auto-announce to your requester; do not busy-poll for status.

Begin. Your assigned task is in the system prompt under **Your Role**; execute it to completion.


role: assistant

text:
I'll implement this step by step: create the scripts directory, write the Node.js converter, and then the shell wrapper.

Let me start by checking what's available on the system.

exec

type: toolCall

command: which node && node --version && which chromium-browser || which chromium || which google-chrome || which google-chrome-stable || echo "no chromium found" && which npx 2>/dev/null; which puppeteer 2>/dev/null || echo "no puppeteer"; which playwright 2>/dev/null || echo "no playwright"

command: ls /root/.openclaw/agents/101/workspace/scripts/ 2>/dev/null || echo "dir not exist"; mkdir -p /root/.openclaw/agents/101/workspace/scripts/md-to-longimg

api: openai-completions

model: deepseek-chat

stopReason: toolUse


role: toolResult

toolName: exec

text:
/usr/bin/node
v22.22.1
no chromium found
/usr/bin/npx
no puppeteer
no playwright

status: completed


text:
install-cron.sh
test-doc-access.js
wecom-smartsheet-config.json
wecom-smartsheet-monitor.js


text: Node.js is available. No chromium/puppeteer/playwright installed yet. Let me write the core Node.js converter first, then the shell wrapper. I'll use Puppeteer since it's straightforward.

write

content:
#!/usr/bin/env node
/**
 * md-to-longimg - Markdown → HTML → Long image converter
 *
 * Usage: node converter.js <input.md> [-o output.png] [--width 640]
 *
 * Converts a Markdown file to a styled HTML page (WeChat article style),
 * then uses Puppeteer to take a full-page screenshot.
 */

const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');

// === Parse args ===
const args = process.argv.slice(2);
let inputFile = null;
let outputFile = null;
let pageWidth = 640;

for (let i = 0; i < args.length; i++) {
  if (args[i] === '-o' && i + 1 < args.length) {
    outputFile = args[++i];
  } else if (args[i] === '--width' && i + 1 < args.length) {
    pageWidth = parseInt(args[++i], 10);
  } else if (!args[i].startsWith('-')) {
    inputFile = args[i];
  }
}

if (!inputFile) {
  console.error('Usage: node converter.js <input.md> [-o output.png] [--width 640]');
  process.exit(1);
}

if (!fs.existsSync(inputFile)) {
  console.error(`Error: input file not found: ${inputFile}`);
  process.exit(1);
}

if (!outputFile) {
  outputFile = path.join(
    path.dirname(inputFile),
    path.basename(inputFile, path.extname(inputFile)) + '.png'
  );
}

// Ensure output dir exists
fs.mkdirSync(path.dirname(outputFile), { recursive: true });

// === Read markdown ===
const markdown = fs.readFileSync(inputFile, 'utf-8');

// === Convert markdown to HTML ===
// We use marked for parsing; fallback to simple regex if not available

function mdToHtml(md) {
  // Escape HTML entities in code blocks and inline code
  let html = md;

  // Fenced code blocks
  html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) => {
    const escaped = code
      .replace(/&/g, '&')
      .replace(/</g, '<')
      .replace(/>/g, '>');
    return `<pre><code class="language-${lang || ''}">${escaped}</code></pre>`;
  });

  // Inline code
  html = html.replace(/`([^`]+)`/g, '$1');

  // Headers
  html = html.replace(/^###### (.+)$/gm, '
$1
');
  html = html.replace(/^##### (.+)$/gm, '
$1
');
  html = html.replace(/^#### (.+)$/gm, '
$1
');
  html = html.replace(/^### (.+)$/gm, '
$1
');
  html = html.replace(/^## (.+)$/gm, '
$1
');
  html = html.replace(/^# (.+)$/gm, '
$1
');

  // Bold and italic
  html = html.replace(/\*\*\*(.+?)\*\*\*/g, '$1');
  html = html.replace(/\*\*(.+?)\*\*/g, '$1');
  html = html.replace(/\*(.+?)\*/g, '$1');

  // Images
  html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, src) => {
    // If source is a relative path, try to make it absolute
    if (src && !src.startsWith('http') && !src.startsWith('data:')) {
      const resolvedPath = path.resolve(path.dirname(inputFile), src);
      if (fs.existsSync(resolvedPath)) {
        // We'll convert to data URI for embedding
        try {
          const imgData = fs.readFileSync(resolvedPath);
          const ext = path.extname(src).toLowerCase();
          const mime = ext === '.png' ? 'image/png'
            : ext === '.gif' ? 'image/gif'
            : ext === '.webp' ? 'image/webp'
            : 'image/jpeg';
          const base64 = imgData.toString('base64');
          src = `data:${mime};base64,${base64}`;
        } catch (e) {
          // keep original
        }
      }
    }
    return `<img src="${src}" alt="${alt}" />`;
  });

  // Links
  html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1');

  // Horizontal rules
  html = html.replace(/^---+\s*$/gm, '
');
  html = html.replace(/^\*\*\*+\s*$/gm, '
');

  // Blockquotes
  html = html.replace(/^> (.+)$/gm, '
$1
');

  // Unordered lists (simple)
  html = html.replace(/^[*-] (.+)$/gm, '
$1
');
  html = html.replace(/(
.*<\/li>)\n(?!
)/gs, '
$1
');

  // Ordered lists (simple)
  html = html.replace(/^\d+\.\s(.+)$/gm, '
$1
');

  // Paragraphs - wrap remaining text lines
  const lines = html.split('\n');
  const result = [];
  let inBlock = false;
  const blockTags = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'pre', 'ul', 'ol', 'li', 'blockquote', 'hr', 'img'];

  for (const line of lines) {
    const trimmed = line.trim();
    if (!trimmed) {
      if (!inBlock) result.push('');
      continue;
    }

    const tagMatch = trimmed.match(/^<(\/?)(\w+)/);
    if (tagMatch) {
      const tag = tagMatch[2];
      const isClosing = tagMatch[1] === '/';

      if (blockTags.includes(tag)) {
        if (tag === 'li') {
          // Check if we need to start/end ul
          if (!inBlock) {
            result.push('
');
            inBlock = true;
          }
          result.push(trimmed);
        } else {
          if (inBlock) {
            // Check if the lst
 was just added
            result.push('
');
            inBlock = false;
          }
          result.push(trimmed);
        }
        if (tag === 'pre' || tag === 'blockquote' || tag === 'ul') {
          // These stay open
        } else if (tag === 'li') {
          // li stays open
        } else if (tag === 'img' || tag === 'hr') {
          // self-closing or handled
        } else {
          // Check if it's a closing tag
          if (!isClosing) {
            // Could be opening - track
          }
        }
      } else {
        if (inBlock) { result.push('
'); inBlock = false; }
        result.push(trimmed);
      }
    } else {
      if (inBlock) { result.push('
'); inBlock = false; }
      result.push(`<p>${trimmed}</p>`);
    }
  }
  if (inBlock) result.push('
');

  return result.join('\n');
}

// === Better approach: try to use npm marked, with fallback ===
let htmlContent;
try {
  // Try using marked for proper parsing
  const marked = require('marked');
  htmlContent = marked.parse(markdown, { gfm: true });
} catch (e) {
  // Fallback to our simple converter
  console.warn('marked not available, using simple converter. Install with: npm install marked');
  htmlContent = mdToHtml(markdown);
}

// === Build full HTML with WeChat-article style ===
const fontStack = `-apple-system, "PingFang SC", "Noto Sans SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif`;

const fullHtml = `<!DOCTYPE html>

<meta charset="UTF-8">
<meta name="viewport" content="width=${pageWidth}">

${htmlContent}

`;

// === Write HTML to temp file ===
const tempHtml = path.join(path.dirname(outputFile), '.temp_' + path.basename(inputFile) + '.html');
fs.writeFileSync(tempHtml, fullHtml, 'utf-8');

// === Screenshot with Puppeteer ===
(async () => {
  try {
    const puppeteer = require('puppeteer');
    const browser = await puppeteer.launch({
      headless: 'new',
      args: [
        '--no-sandbox',
        '--disable-setuid-sandbox',
        '--disable-dev-shm-usage',
        '--disable-gpu',
      ],
    });

    const page = await browser.newPage();
    await page.setViewport({ width: pageWidth, height: 800 });
    await page.goto('file://' + path.resolve(tempHtml), {
      waitUntil: 'networkidle0',
      timeout: 30000,
    });

    // Wait a moment for fonts to load
    await new Promise(r => setTimeout(r, 500));

    // Get full page dimensions
    const bodyHandle = await page.$('body');
    const box = await bodyHandle.boundingBox();
    const height = Math.ceil(box.height);

    // Set viewport height to full content
    await page.setViewport({ width: pageWidth, height: height });

    // Take screenshot
    await page.screenshot({
      path: outputFile,
      fullPage: true,
      type: 'png',
    });

    await browser.close();

    // Clean up temp file
    try { fs.unlinkSync(tempHtml); } catch (e) {}

    const stats = fs.statSync(outputFile);
    const sizeKB = (stats.size / 1024).toFixed(1);
    console.log(`✅ 转换完成！`);
    console.log(`   输出: ${path.resolve(outputFile)}`);
    console.log(`   大小: ${sizeKB} KB`);
    console.log(`   宽度: ${pageWidth}px`);
    console.log(`   高度: ${height}px`);

  } catch (err) {
    console.error('❌ 截图失败:', err.message);
    console.error(`\nHTML 文件已保留在: ${tempHtml}`);
    console.error('你可以用浏览器打开这个 HTML 文件手动查看效果。');
    process.exit(1);
  }
})();


toolName: write

text: Successfully wrote 11046 bytes to /root/.openclaw/agents/101/workspace/scripts/md-to-longimg/converter.js


text: Now write the shell wrapper script:

content:
#!/usr/bin/env bash
#
# md-to-longimg.sh — Markdown → HTML → 长图 一键导出脚本
#
# 使用方法：
#   bash md-to-longimg.sh input.md -o output.png
#   bash md-to-longimg.sh input.md                      # 默认输出到 input.png
#   bash md-to-longimg.sh input.md -o out.png --width 750
#
# 依赖自动安装：Node.js + puppeteer（含 chromium）
#

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONVERTER_DIR="$SCRIPT_DIR/md-to-longimg"
CONVERTER_JS="$CONVERTER_DIR/converter.js"

# ====== 颜色输出 ======
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

info()  { echo -e "${GREEN}[INFO]${NC} $*"; }
warn()  { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }

# ====== 检查 Node.js ======
if ! command -v node &>/dev/null; then
    error "需要 Node.js，但未安装。请先安装 Node.js。"
    error "建议: https://nodejs.org/ 或 nvm install"
    exit 1
fi

NODE_VER=$(node -v | sed 's/v//' | cut -d. -f1)
if [ "$NODE_VER" -lt 18 ]; then
    warn "Node.js 版本过低 ($(node -v))，建议 >= 18"
fi

# ====== 解析参数 ======
INPUT_FILE=""
OUTPUT_FILE=""
WIDTH=640

while [[ $# -gt 0 ]]; do
    case "$1" in
        -o)
            shift
            OUTPUT_FILE="$1"
            ;;
        --width)
            shift
            WIDTH="$1"
            ;;
        -*)
            error "未知参数: $1"
            echo "用法: $0 <input.md> [-o output.png] [--width 640]"
            exit 1
            ;;
        *)
            if [ -z "$INPUT_FILE" ]; then
                INPUT_FILE="$1"
            else
                error "只能指定一个输入文件"
                exit 1
            fi
            ;;
    esac
    shift
done

if [ -z "$INPUT_FILE" ]; then
    error "请指定输入 Markdown 文件"
    echo "用法: $0 <input.md> [-o output.png] [--width 640]"
    exit 1
fi

if [ ! -f "$INPUT_FILE" ]; then
    error "文件不存在: $INPUT_FILE"
    exit 1
fi

INPUT_FILE="$(realpath "$INPUT_FILE")"
INPUT_DIR="$(dirname "$INPUT_FILE")"
INPUT_BASENAME="$(basename "$INPUT_FILE")"
INPUT_NAME="${INPUT_BASENAME%.*}"

if [ -z "$OUTPUT_FILE" ]; then
    OUTPUT_FILE="${INPUT_DIR}/${INPUT_NAME}.png"
else
    OUTPUT_DIR="$(dirname "$OUTPUT_FILE")"
    mkdir -p "$OUTPUT_DIR"
    OUTPUT_FILE="$(realpath "$OUTPUT_FILE")" 2>/dev/null || true
fi

# ====== 安装依赖 ======
info "检查依赖..."
cd "$CONVERTER_DIR"

# 检查是否需要安装 npm 依赖
if [ ! -d "node_modules" ] || [ ! -f "node_modules/.md-to-longimg-installed" ]; then
    info "安装 npm 依赖 (puppeteer, marked)..."
    npm init -y >/dev/null 2>&1
    npm install puppeteer marked 2>&1 | tail -5
    touch node_modules/.md-to-longimg-installed
    info "npm 依赖安装完成"
else
    info "npm 依赖已安装，跳过"
fi

# 检查 chromium 是否可用（puppeteer 自带，但确认一下）
PUPPETEER_CHROMIUM="$CONVERTER_DIR/node_modules/puppeteer-core/.local-chromium"
if [ ! -d "$PUPPETEER_CHROMIUM" ]; then
    CHROMIUM_CHECK=$(node -e "
        try {
            const p = require('puppeteer');
            console.log('ok');
        } catch(e) {
            console.log('missing');
        }
    " 2>/dev/null || echo "missing")
    if [ "$CHROMIUM_CHECK" = "missing" ]; then
        warn "Chromium 未就绪，尝试安装..."
        npx puppeteer browsers install chrome 2>&1 || true
    fi
fi

# ====== 检测中文字体 ======
FONT_CHECK=$(fc-list :lang=zh 2>/dev/null | head -3)
if [ -z "$FONT_CHECK" ]; then
    warn "未检测到中文字体，可能影响中文显示"
    warn "建议安装: apt install fonts-noto-cjk 或 fonts-wqy-microhei"
    # 尝试安装
    if command -v apt-get &>/dev/null; then
        info "尝试安装中文 Fallback 字体..."
        apt-get update -qq 2>/dev/null && apt-get install -y -qq fonts-noto-cjk 2>/dev/null && info "中文字体安装完成" || warn "中文字体安装失败，长图中文可能显示异常"
    fi
fi

# ====== 执行转换 ======
info "开始转换..."
info "  输入: $INPUT_FILE"
info "  输出: $OUTPUT_FILE"
info "  宽度: ${WIDTH}px"

node "$CONVERTER_JS" "$INPUT_FILE" -o "$OUTPUT_FILE" --width "$WIDTH"


text: Successfully wrote 3766 bytes to /root/.openclaw/agents/101/workspace/scripts/md-to-longimg.sh


text: (no output)


text: Now let me install dependencies and test it with a sample markdown file:

exec

command: cd /root/.openclaw/agents/101/workspace/scripts/md-to-longimg && npm init -y >/dev/null 2>&1 && npm install puppeteer marked 2>&1


text: Command still running (session gentle-shell, pid 58828). Use process (list/poll/log/write/kill/clear/remove) for follow-up.

status: running

sessionId: gentle-shell


process

action: poll


toolName: process

text:
(no new output)

Process still running.


action: log


text: (no output yet)


text:
(no new output)

Process exited with signal SIGKILL.

status: failed


text: Seems like npm install timed out. Let me try again with a longer timeout and capture output:

exec

command: cd /root/.openclaw/agents/101/workspace/scripts/md-to-longimg && npm install puppeteer marked 2>&1


text:
npm error code 1
npm error path /root/.openclaw/agents/101/workspace/scripts/md-to-longimg/node_modules/puppeteer
npm error command failed
npm error command sh -c node install.mjs
npm error **INFO** Skipping Firefox download as instructed.
npm error Error: ERROR: Failed to set up chrome v148.0.7778.97! Set "PUPPETEER_SKIP_DOWNLOAD" env variable to skip download.
npm error     at downloadBrowser (file:///root/.openclaw/agents/101/workspace/scripts/md-to-longimg/node_modules/puppeteer/lib/esm/puppeteer/node/install.js:26:15)
npm error     at async Promise.all (index 0)
npm error     at async downloadBrowsers (file:///root/.openclaw/agents/101/workspace/scripts/md-to-longimg/node_modules/puppeteer/lib/esm/puppeteer/node/install.js:84:9) {
npm error   [cause]: Error: All providers failed for chrome 148.0.7778.97:
npm error     - DefaultProvider: end of central directory record signature not found
npm error       at installWithProviders (file:///root/.openclaw/agents/101/workspace/scripts/md-to-longimg/node_modules/@puppeteer/browsers/lib/esm/install.js:108:11)
npm error       at async install (file:///root/.openclaw/agents/101/workspace/scripts/md-to-longimg/node_modules/@puppeteer/browsers/lib/esm/install.js:118:12)
npm error       at async downloadBrowser (file:///root/.openclaw/agents/101/workspace/scripts/md-to-longimg/node_modules/puppeteer/lib/esm/puppeteer/node/install.js:14:24)
npm error       at async Promise.all (index 0)
npm error       at async downloadBrowsers (file:///root/.openclaw/agents/101/workspace/scripts/md-to-longimg/node_modules/puppeteer/lib/esm/puppeteer/node/install.js:84:9)
npm error }
npm error A complete log of this run can be found in: /root/.npm/_logs/2026-05-11T01_21_03_660Z-debug-0.log

(Command exited with code 1)


text: Puppeteer can't download Chromium directly. Let me check if there's a system chromium available or use puppeteer-core with a pre-installed browser:

command: apt list --installed 2>/dev/null | grep -i chrom; which chromium-browser chromium google-chrome google-chrome-stable 2>/dev/null; dpkg -l | grep -i chrom 2>/dev/null | head -5


text: No chromium at all. Let me use a different approach — install puppeteer with `PUPPETEER_SKIP_DOWNLOAD` and then install chromium via apt:

command: PUPPETEER_SKIP_DOWNLOAD=1 npm install puppeteer marked 2>&1 | tail -5


text:
7 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities


text: Good, puppeteer is installed without download. Now let me install chromium via apt:

command: apt-get update -qq 2>&1 | tail -3 && apt-get install -y -qq chromium-browser 2>&1 | tail -5


text: Command still running (session kind-shoal, pid 59112). Use process (list/poll/log/write/kill/clear/remove) for follow-up.

sessionId: kind-shoal


error: request timed out | request timed out