type: custom

customType: model-snapshot

provider: deepseek

modelApi: openai-completions

modelId: deepseek-chat


type: message

role: user

type: text

text:
[Sun 2026-05-17 14:36 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: Let me start by reading the two files I need to work with.

read

type: toolCall

api: openai-completions

model: deepseek-chat

stopReason: toolUse


role: toolResult

toolName: read

text:
# Token越来越贵，但国家正在让算力像水电一样普惠

"不是说AI越来越便宜吗？"

你最近如果真在大量用AI产品，大概率笑不出来。

2025年底那会儿，调用一次大模型API也就几厘钱。到了2026年5月，同样一个请求，价格翻了不止一倍。腾讯云部分模型涨幅超过400%，阿里云一个月内连续三次上调定价。

涨价通知一封接一封，跟商量好了似的。

这到底是怎么回事？

## 不是AI变贵了，是用的人太多了

先看一组数据。

2026年3月，我国日均Token调用量超过了140万亿。什么概念？2024年初这个数字还不到现在的千分之一。一年多时间，增长了1000多倍。

1000倍。

你想想，任何一个行业，需求在一年半不到的时间里暴涨1000倍——价格不飞涨才奇怪。

大模型不再是什么新鲜概念了。从写周报到编代码，从客服应答到医疗诊断，企业把AI当成了标配。每个公司都在往模型里喂数据、调参数、跑推理。这些动作叠加起来，对算力的消耗是指数级的。

而算力基础设施的扩容，可没法按"倍"来涨。芯片供给有瓶颈、电力有上限、数据中心建设有周期。

供需两端一掰手腕，结果很直接：**Token涨价了。**

这其实不是什么新闻。任何资源从稀缺走向普及的过程中，都会经历这样一个阶段。早期需求爆发，供给跟不上，价格冲高，然后供给端开始大规模投入，最终价格回落。

但这次不一样的地方在于：**推动供给端变化的，不再只是市场。国家出手了。**

## 算力网：一场顶层设计的"基础设施革命"

就在上个月，国务院常务会议明确提出：加强"水网、新型电网、算力网、新一代通信网、城市地下管网、物流网"规划建设。

注意，算力网被放在了和水、电网同等的位置。

这不是随口一提。此前中央政治局会议也有相同表述，"十五五"规划更是白纸黑字写着"构建多层次算力设施体系和全国一体化算力网"。

什么叫算力网？说白了，就是**算力版的国家电网**。

把全国各地的数据中心、超算中心、智算中心连成一张网，统一调度、按需分配。你不需要知道算力从哪来，就像你不需要关心电从哪个发电厂来一样。插上插头，就有电。接入网络，就有算力。

国家发改委的数据显示，今年"六张网"及相关领域投资总额预计超过7万亿元。

7万亿。这不是做实验，这是动真格地在建基础设施。

## 想想当年的手机流量

这让我想起一个特别相似的场景：手机上网。

2012年前后，你拿手机看个视频都心疼流量——1MB好几毛钱用4G那年头，资费降了一波又一波。4G普及之后，流量从奢侈品变成了日用品。直播、短视频、移动支付，这些今天看起来理所当然的应用，都是在流量变得足够便宜之后才爆发的。

算力网要走的，就是同一条路。

早期算力贵，只有大公司用得起。大模型训练一次几千万，API调用按毫秒计费，普通的创业公司玩不转。这跟当年只有商务人士舍得开流量套餐是一个道理。

但如果"算力像水电一样普惠"真的实现了呢？

想象一下：你是一个做AI应用的创业者，不再需要关心底层算力从哪来。不用自己买GPU，不用纠结部署哪个云厂商，不用为了省几万块算力费熬夜调参数。你开箱即用，按使用量付费，像交水电费一样简单。

那意味着什么？意味着**创新门槛被砍掉了一大截**。

4G+流量便宜化催生了上万亿美元的移动互联网生态。算力网如果大规模铺开，它对AI生态的撬动作用只可能更大。

## 对创业者的启示：别只看"贵不贵"，要看"变没变"

现在这个阶段，很多人还在抱怨Token涨价。

但如果你想在这个行业里找到机会，就不该只盯着当下的价格看。你应该关心的是：基础设施正在发生什么样的变化？

每一次基础设施的革命，都会催生出一批新的物种。

电来了，才有了家电行业，才有了生产线自动化。
互联网来了，才有了电商，才有了社交网络。
移动互联网来了，才有了美团，有了滴滴，有了抖音。

而算力网，是把算力本身变成基础设施。这意味着：

**算力将会像金融一样，成为万业之源。**

想想金融是怎么改变商业世界的。没有银行、没有支付系统、没有信贷体系，就没有现代商业。算力正在走同样的路——它正在从一个"需不需要"的问题，变成一个"想用多少用多少"的问题。

那些提前意识到这一点的人，正在悄悄布局。

有人在算力网的基础上做AI Agent的服务平台。
有人在算力网的边缘节点上做实时推理应用。
有人在算力网的调度层做工具和中间件。

他们赌的不是某个大模型会赢。他们赌的是，算力普惠这件事一定会发生。

## 所以，Token还会涨价吗？

短期看，会。供需失衡还没缓解，需求还在飙涨。

但放在更长的时间维度上——当算力网这张大网铺开，当7万亿的投资落地，当算力像水电一样流入千家万户——Token的价格走上下行通道，几乎是必然的。

到那时候，真正值得思考的问题就不是"Token多少钱"了，而是：

**当算力不再稀缺，你的想象力够不够用？**


text:
Token越来越贵，但国家正在让算力像水电一样普惠

"不是说AI越来越便宜吗？"

你最近如果真在大量用AI产品，大概率笑不出来。

2025年底那会儿，调用一次大模型API也就几厘钱。到了2026年5月，同样一个请求，价格翻了不止一倍。腾讯云部分模型涨幅超过400%，阿里云一个月内连续三次上调定价。

涨价通知一封接一封，跟商量好了似的。

这到底是怎么回事？

不是AI变贵了，是用的人太多了

先看一组数据。

2026年3月，我国日均Token调用量超过了140万亿。什么概念？2024年初这个数字还不到现在的千分之一。一年多时间，增长了1000多倍。

1000倍。

你想想，任何一个行业，需求在一年半不到的时间里暴涨1000倍——价格不飞涨才奇怪。

大模型不再是什么新鲜概念了。从写周报到编代码，从客服应答到医疗诊断，企业把AI当成了标配。每个公司都在往模型里喂数据、调参数、跑推理。这些动作叠加起来，对算力的消耗是指数级的。

而算力基础设施的扩容，可没法按"倍"来涨。芯片供给有瓶颈、电力有上限、数据中心建设有周期。

供需两端一掰手腕，结果很直接：Token涨价了。

这其实不是什么新闻。任何资源从稀缺走向普及的过程中，都会经历这样一个阶段。早期需求爆发，供给跟不上，价格冲高，然后供给端开始大规模投入，最终价格回落。

但这次不一样的地方在于：推动供给端变化的，不再只是市场。国家出手了。

算力网：一场顶层设计的"基础设施革命"

就在上个月，国务院常务会议明确提出：加强"水网、新型电网、算力网、新一代通信网、城市地下管网、物流网"规划建设。

注意，算力网被放在了和水、电网同等的位置。

这不是随口一提。此前中央政治局会议也有相同表述，"十五五"规划更是白纸黑字写着"构建多层次算力设施体系和全国一体化算力网"。

什么叫算力网？说白了，就是算力版的国家电网。

把全国各地的数据中心、超算中心、智算中心连成一张网，统一调度、按需分配。你不需要知道算力从哪来，就像你不需要关心电从哪个发电厂来一样。插上插头，就有电。接入网络，就有算力。

国家发改委的数据显示，今年"六张网"及相关领域投资总额预计超过7万亿元。

7万亿。这不是做实验，这是动真格地在建基础设施。

想想当年的手机流量

这让我想起一个特别相似的场景：手机上网。

2012年前后，你拿手机看个视频都心疼流量——1MB好几毛钱用4G那年头，资费降了一波又一波。4G普及之后，流量从奢侈品变成了日用品。直播、短视频、移动支付，这些今天看起来理所当然的应用，都是在流量变得足够便宜之后才爆发的。

算力网要走的，就是同一条路。

早期算力贵，只有大公司用得起。大模型训练一次几千万，API调用按毫秒计费，普通的创业公司玩不转。这跟当年只有商务人士舍得开流量套餐是一个道理。

但如果"算力像水电一样普惠"真的实现了呢？

想象一下：你是一个做AI应用的创业者，不再需要关心底层算力从哪来。不用自己买GPU，不用纠结部署哪个云厂商，不用为了省几万块算力费熬夜调参数。你开箱即用，按使用量付费，像交水电费一样简单。

那意味着什么？意味着创新门槛被砍掉了一大截。

4G+流量便宜化催生了上万亿美元的移动互联网生态。算力网如果大规模铺开，它对AI生态的撬动作用只可能更大。

对创业者的启示：别只看"贵不贵"，要看"变没变"

现在这个阶段，很多人还在抱怨Token涨价。

但如果你想在这个行业里找到机会，就不该只盯着当下的价格看。你应该关心的是：基础设施正在发生什么样的变化？

每一次基础设施的革命，都会催生出一批新的物种。

电来了，才有了家电行业，才有了生产线自动化。
互联网来了，才有了电商，才有了社交网络。
移动互联网来了，才有了美团，有了滴滴，有了抖音。

而算力网，是把算力本身变成基础设施。这意味着：

算力将会像金融一样，成为万业之源。

想想金融是怎么改变商业世界的。没有银行、没有支付系统、没有信贷体系，就没有现代商业。算力正在走同样的路——它正在从一个"需不需要"的问题，变成一个"想用多少用多少"的问题。

那些提前意识到这一点的人，正在悄悄布局。

有人在算力网的基础上做AI Agent的服务平台。
有人在算力网的边缘节点上做实时推理应用。
有人在算力网的调度层做工具和中间件。

他们赌的不是某个大模型会赢。他们赌的是，算力普惠这件事一定会发生。

所以，Token还会涨价吗？

短期看，会。供需失衡还没缓解，需求还在飙涨。

但放在更长的时间维度上——当算力网这张大网铺开，当7万亿的投资落地，当算力像水电一样流入千家万户——Token的价格走上下行通道，几乎是必然的。

到那时候，真正值得思考的问题就不是"Token多少钱"了，而是：

当算力不再稀缺，你的想象力够不够用？


text:
Good, I have both files. Now let me execute both tasks. For Task A (WeChat), I'll run the script. For Task B (Z-Blog via XML-RPC), I'll need to write a posting script.

Let me start both:

exec

command: ls -la /root/.openclaw/agents/101/workspace/imgs/cover.png 2>/dev/null && echo "Cover exists" || echo "Cover NOT found"

command: ls -la /root/.openclaw/agents/101/workspace/skills/baoyu-post-to-wechat/scripts/wechat-api.ts 2>/dev/null && echo "Script exists" || echo "Script NOT found"


toolName: exec

text:
-rw-r--r-- 1 root root 159045 Mar 26 13:01 /root/.openclaw/agents/101/workspace/imgs/cover.png
Cover exists

status: completed


text:
-rw-r--r-- 1 root root 21517 Mar 26 12:20 /root/.openclaw/agents/101/workspace/skills/baoyu-post-to-wechat/scripts/wechat-api.ts
Script exists


text: Both files exist. Let me first check the WeChat API script to understand its expected usage and requirements:


text:
import fs from "node:fs";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { loadWechatExtendConfig, resolveAccount, loadCredentials } from "./wechat-extend-config.ts";

interface AccessTokenResponse {
  access_token?: string;
  errcode?: number;
  errmsg?: string;
}

interface UploadResponse {
  media_id: string;
  url: string;
  errcode?: number;
  errmsg?: string;
}

interface PublishResponse {
  media_id?: string;
  errcode?: number;
  errmsg?: string;
}

interface ImageInfo {
  placeholder: string;
  localPath: string;
  originalPath: string;
}

interface MarkdownRenderResult {
  title: string;
  author: string;
  summary: string;
  htmlPath: string;
  contentImages: ImageInfo[];
}

type ArticleType = "news" | "newspic";

interface ArticleOptions {
  title: string;
  author?: string;
  digest?: string;
  content: string;
  thumbMediaId: string;
  articleType: ArticleType;
  imageMediaIds?: string[];
  needOpenComment?: number;
  onlyFansCanComment?: number;
}

const TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token";
const UPLOAD_URL = "https://api.weixin.qq.com/cgi-bin/material/add_material";
const DRAFT_URL = "https://api.weixin.qq.com/cgi-bin/draft/add";

async function fetchAccessToken(appId: string, appSecret: string): Promise<string> {
  const url = `${TOKEN_URL}?grant_type=client_credential&appid=${appId}&secret=***
  const res = await fetch(url);
  if (!res.ok) {
    throw new Error(`Failed to fetch access token: ${res.status}`);
  }
  const data = await res.json() as AccessTokenResponse;
  if (data.errcode) {
    throw new Error(`Access token error ${data.errcode}: ${data.errmsg}`);
  }
  if (!data.access_token) {
    throw new Error("No access_token in response");
  }
  return data.access_token;
}

async function uploadImage(
  imagePath: string,
  accessToken: string,
  baseDir?: string
): Promise<UploadResponse> {
  let fileBuffer: Buffer;
  let filename: string;
  let contentType: string;

  if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) {
    const response = await fetch(imagePath);
    if (!response.ok) {
      throw new Error(`Failed to download image: ${imagePath}`);
    }
    const buffer = await response.arrayBuffer();
    if (buffer.byteLength === 0) {
      throw new Error(`Remote image is empty: ${imagePath}`);
    }
    fileBuffer = Buffer.from(buffer);
    const urlPath = imagePath.split("?")[0];
    filename = path.basename(urlPath) || "image.jpg";
    contentType = response.headers.get("content-type") || "image/jpeg";
  } else {
    const resolvedPath = path.isAbsolute(imagePath)
      ? imagePath
      : path.resolve(baseDir || process.cwd(), imagePath);

    if (!fs.existsSync(resolvedPath)) {
      throw new Error(`Image not found: ${resolvedPath}`);
    }
    const stats = fs.statSync(resolvedPath);
    if (stats.size === 0) {
      throw new Error(`Local image is empty: ${resolvedPath}`);
    }
    fileBuffer = fs.readFileSync(resolvedPath);
    filename = path.basename(resolvedPath);
    const ext = path.extname(filename).toLowerCase();
    const mimeTypes: Record<string, string> = {
      ".jpg": "image/jpeg",
      ".jpeg": "image/jpeg",
      ".png": "image/png",
      ".gif": "image/gif",
      ".webp": "image/webp",
    };
    contentType = mimeTypes[ext] || "image/jpeg";
  }

  const boundary = `----WebKitFormBoundary${Date.now().toString(16)}`;
  const header = [
    `--${boundary}`,
    `Content-Disposition: form-data; name="media"; filename="${filename}"`,
    `Content-Type: ${contentType}`,
    "",
    "",
  ].join("\r\n");
  const footer = `\r\n--${boundary}--\r\n`;

  const headerBuffer = Buffer.from(header, "utf-8");
  const footerBuffer = Buffer.from(footer, "utf-8");
  const body = Buffer.concat([headerBuffer, fileBuffer, footerBuffer]);

  const url = `${UPLOAD_URL}?access_token=***&type=image`;
  const res = await fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": `multipart/form-data; boundary=${boundary}`,
    },
    body,
  });

  const data = await res.json() as UploadResponse;
  if (data.errcode && data.errcode !== 0) {
    throw new Error(`Upload failed ${data.errcode}: ${data.errmsg}`);
  }

  if (data.url?.startsWith("http://")) {
    data.url = data.url.replace(/^http:\/\//i, "https://");
  }

  return data;
}

async function uploadImagesInHtml(
  html: string,
  accessToken: string,
  baseDir: string,
  contentImages: ImageInfo[] = [],
): Promise<{ html: string; firstMediaId: string; allMediaIds: string[] }> {
  const imgRegex = /<img[^>]*\ssrc=["']([^"']+)["'][^>]*>/gi;
  const matches = [...html.matchAll(imgRegex)];

  if (matches.length === 0 && contentImages.length === 0) {
    return { html, firstMediaId: "", allMediaIds: [] };
  }

  let firstMediaId = "";
  let updatedHtml = html;
  const allMediaIds: string[] = [];
  const uploadedBySource = new Map<string, UploadResponse>();

  for (const match of matches) {
    const [fullTag, src] = match;
    if (!src) continue;

    if (src.startsWith("https://mmbiz.qpic.cn")) {
      if (!firstMediaId) {
        firstMediaId = src;
      }
      continue;
    }

    const localPathMatch = fullTag.match(/data-local-path=["']([^"']+)["']/);
    const imagePath = localPathMatch ? localPathMatch[1]! : src;

    console.error(`[wechat-api] Uploading image: ${imagePath}`);
    try {
      let resp = uploadedBySource.get(imagePath);
      if (!resp) {
        resp = await uploadImage(imagePath, accessToken, baseDir);
        uploadedBySource.set(imagePath, resp);
      }
      const newTag = fullTag
        .replace(/\ssrc=["'][^"']+["']/, ` src="${resp.url}"`)
        .replace(/\sdata-local-path=["'][^"']+["']/, "");
      updatedHtml = updatedHtml.replace(fullTag, newTag);
      allMediaIds.push(resp.media_id);
      if (!firstMediaId) {
        firstMediaId = resp.media_id;
      }
    } catch (err) {
      console.error(`[wechat-api] Failed to upload ${imagePath}:`, err);
    }
  }

  for (const image of contentImages) {
    if (!updatedHtml.includes(image.placeholder)) continue;

    const imagePath = image.localPath || image.originalPath;
    console.error(`[wechat-api] Uploading placeholder image: ${imagePath}`);

    try {
      let resp = uploadedBySource.get(imagePath);
      if (!resp) {
        resp = await uploadImage(imagePath, accessToken, baseDir);
        uploadedBySource.set(imagePath, resp);
      }

      const replacementTag = `<img src="${resp.url}" style="display: block; width: 100%; margin: 1.5em auto;">`;
      updatedHtml = replaceAllPlaceholders(updatedHtml, image.placeholder, replacementTag);
      allMediaIds.push(resp.media_id);
      if (!firstMediaId) {
        firstMediaId = resp.media_id;
      }
    } catch (err) {
      console.error(`[wechat-api] Failed to upload placeholder ${image.placeholder}:`, err);
    }
  }

  return { html: updatedHtml, firstMediaId, allMediaIds };
}

async function publishToDraft(
  options: ArticleOptions,
  accessToken: string
): Promise<PublishResponse> {
  const url = `${DRAFT_URL}?access_token=***

  let article: Record<string, unknown>;

  const noc = options.needOpenComment ?? 1;
  const ofcc = options.onlyFansCanComment ?? 0;

  if (options.articleType === "newspic") {
    if (!options.imageMediaIds || options.imageMediaIds.length === 0) {
      throw new Error("newspic requires at least one image");
    }
    article = {
      article_type: "newspic",
      title: options.title,
      content: options.content,
      need_open_comment: noc,
      only_fans_can_comment: ofcc,
      image_info: {
        image_list: options.imageMediaIds.map(id => ({ image_media_id: id })),
      },
    };
    if (options.author) article.author = options.author;
  } else {
    article = {
      article_type: "news",
      title: options.title,
      content: options.content,
      thumb_media_id: options.thumbMediaId,
      need_open_comment: noc,
      only_fans_can_comment: ofcc,
    };
    if (options.author) article.author = options.author;
    if (options.digest) article.digest = options.digest;
  }

  const res = await fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ articles: [article] }),
  });

  const data = await res.json() as PublishResponse;
  if (data.errcode && data.errcode !== 0) {
    throw new Error(`Publish failed ${data.errcode}: ${data.errmsg}`);
  }

  return data;
}

function parseFrontmatter(content: string): { frontmatter: Record<string, string>; body: string } {
  const match = content.match(/^\s*---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
  if (!match) return { frontmatter: {}, body: content };

  const frontmatter: Record<string, string> = {};
  const lines = match[1]!.split("\n");
  for (const line of lines) {
    const colonIdx = line.indexOf(":");
    if (colonIdx > 0) {
      const key = line.slice(0, colonIdx).trim();
      let value = line.slice(colonIdx + 1).trim();
      if ((value.startsWith('"') && value.endsWith('"')) ||
          (value.startsWith("'") && value.endsWith("'"))) {
        value = value.slice(1, -1);
      }
      frontmatter[key] = value;
    }
  }

  return { frontmatter, body: match[2]! };
}

function renderMarkdownWithPlaceholders(
  markdownPath: string,
  theme: string = "default",
  color?: string,
  citeStatus: boolean = true,
  title?: string,
): MarkdownRenderResult {
  const __filename = fileURLToPath(import.meta.url);
  const __dirname = path.dirname(__filename);
  const mdToWechatScript = path.join(__dirname, "md-to-wechat.ts");
  const baseDir = path.dirname(markdownPath);

  const args = ["-y", "bun", mdToWechatScript, markdownPath];
  if (title) args.push("--title", title);
  if (theme) args.push("--theme", theme);
  if (color) args.push("--color", color);
  if (!citeStatus) args.push("--no-cite");

  console.error(`[wechat-api] Rendering markdown with placeholders via md-to-wechat: ${theme}${color ? `, color: ${color}` : ""}, citeStatus: ${citeStatus}`);
  const result = spawnSync("npx", args, {
    stdio: ["inherit", "pipe", "pipe"],
    cwd: baseDir,
  });

  if (result.status !== 0) {
    const stderr = result.stderr?.toString() || "";
    throw new Error(`Markdown placeholder render failed: ${stderr}`);
  }

  const stdout = result.stdout?.toString() || "";
  return JSON.parse(stdout) as MarkdownRenderResult;
}

function replaceAllPlaceholders(html: string, placeholder: string, replacement: string): string {
  const escapedPlaceholder = placeholder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  return html.replace(new RegExp(escapedPlaceholder, "g"), replacement);
}

function extractHtmlContent(htmlPath: string): string {
  const html = fs.readFileSync(htmlPath, "utf-8");
  const match = html.match(/<div id="output">([\s\S]*?)<\/div>\s*<\/body>/);
  if (match) {
    return match[1]!.trim();
  }
  const bodyMatch = html.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
  return bodyMatch ? bodyMatch[1]!.trim() : html;
}

function printUsage(): never {
  console.log(`Publish article to WeChat Official Account draft using API

Usage:
  npx -y bun wechat-api.ts <file> [options]

Arguments:
  file                Markdown (.md) or HTML (.html) file

Options:
  --type <type>       Article type: news (文章, default) or newspic (图文)
  --title <title>     Override title
  --author <name>     Author name (max 16 chars)
  --summary <text>    Article summary/digest (max 128 chars)
  --theme <name>      Theme name for markdown (default, grace, simple, modern). Default: default
  --color <name|hex>  Primary color (blue, green, vermilion, etc. or hex)
  --cover <path>      Cover image path (local or URL)
  --account <alias>   Select account by alias (for multi-account setups)

⚠ [... middle content omitted — showing head and tail ...]

    htmlContent = extractHtmlContent(htmlPath);
  }

  if (!title) {
    console.error("Error: No title found. Provide via --title, frontmatter, or <title> tag.");
    process.exit(1);
  }

  if (digest && digest.length > 120) {
    const truncated = digest.slice(0, 117);
    const lastPunct = Math.max(truncated.lastIndexOf("。"), truncated.lastIndexOf("，"), truncated.lastIndexOf("；"), truncated.lastIndexOf("、"));
    digest = lastPunct > 80 ? truncated.slice(0, lastPunct + 1) : truncated + "...";
    console.error(`[wechat-api] Digest truncated to ${digest.length} chars`);
  }

  console.error(`[wechat-api] Title: ${title}`);
  if (author) console.error(`[wechat-api] Author: ${author}`);
  if (digest) console.error(`[wechat-api] Digest: ${digest.slice(0, 50)}...`);
  console.error(`[wechat-api] Type: ${args.articleType}`);

  const extConfig = loadWechatExtendConfig();
  const resolved = resolveAccount(extConfig, args.account);
  if (resolved.name) console.error(`[wechat-api] Account: ${resolved.name} (${resolved.alias})`);

  if (!author && resolved.default_author) author = resolved.default_author;

  if (args.dryRun) {
    console.log(JSON.stringify({
      articleType: args.articleType,
      title,
      author: author || undefined,
      digest: digest || undefined,
      htmlPath,
      contentLength: htmlContent.length,
      placeholderImageCount: contentImages.length || undefined,
      account: resolved.alias || undefined,
    }, null, 2));
    return;
  }

  const creds = loadCredentials(resolved);
  console.error("[wechat-api] Fetching access token...");
  const accessToken = await fetchAccessToken(creds.appId, creds.appSecret);

  console.error("[wechat-api] Uploading images...");
  const { html: processedHtml, firstMediaId, allMediaIds } = await uploadImagesInHtml(
    htmlContent,
    accessToken,
    baseDir,
    contentImages,
  );
  htmlContent = processedHtml;

  let thumbMediaId = "";
  const rawCoverPath = args.cover ||
    frontmatter.coverImage ||
    frontmatter.featureImage ||
    frontmatter.cover ||
    frontmatter.image;
  const coverPath = rawCoverPath && !path.isAbsolute(rawCoverPath) && args.cover
    ? path.resolve(process.cwd(), rawCoverPath)
    : rawCoverPath;

  if (coverPath) {
    console.error(`[wechat-api] Uploading cover: ${coverPath}`);
    const coverResp = await uploadImage(coverPath, accessToken, baseDir);
    thumbMediaId = coverResp.media_id;
  } else if (firstMediaId) {
    if (firstMediaId.startsWith("https://")) {
      console.error(`[wechat-api] Uploading first image as cover: ${firstMediaId}`);
      const coverResp = await uploadImage(firstMediaId, accessToken, baseDir);
      thumbMediaId = coverResp.media_id;
    } else {
      thumbMediaId = firstMediaId;
    }
  }

  if (args.articleType === "news" && !thumbMediaId) {
    console.error("Error: No cover image. Provide via --cover, frontmatter.coverImage, or include an image in content.");
    process.exit(1);
  }

  if (args.articleType === "newspic" && allMediaIds.length === 0) {
    console.error("Error: newspic requires at least one image in content.");
    process.exit(1);
  }

  console.error("[wechat-api] Publishing to draft...");
  const result = await publishToDraft({
    title,
    author: author || undefined,
    digest: digest || undefined,
    content: htmlContent,
    thumbMediaId,
    articleType: args.articleType,
    imageMediaIds: args.articleType === "newspic" ? allMediaIds : undefined,
    needOpenComment: resolved.need_open_comment,
    onlyFansCanComment: resolved.only_fans_can_comment,
  }, accessToken);

  console.log(JSON.stringify({
    success: true,
    media_id: result.media_id,
    title,
    articleType: args.articleType,
  }, null, 2));

  console.error(`[wechat-api] Published successfully! media_id: ${result.media_id}`);
}

await main().catch((err) => {
  console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
  process.exit(1);
});
[... 5641 more characters truncated]


text: Now let me run Task A (WeChat post). The script is a TypeScript file that uses `bun`:

command:
cd /root/.openclaw/agents/101/workspace/post-to-wechat/2026-05-17 && \
WECHAT_APP_ID=wx88b0cf376e0386c4 \
WECHAT_APP_SECRET=b2d47ee298204c28cf82549d2798192f \
bun /root/.openclaw/agents/101/workspace/skills/baoyu-post-to-wechat/scripts/wechat-api.ts \
/root/.openclaw/agents/101/workspace/post-to-wechat/2026-05-17/算力网来了-Token涨价.md \
--theme default --color blue --author "ai张三疯" \
--cover /root/.openclaw/agents/101/workspace/imgs/cover.png 2>&1


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

status: running

sessionId: amber-summit

tail:
[wechat-api] Theme: default, color: blue, citeStatus: true
[wechat-api] Rendering markdown with placeholders via md-to-wechat: default, color: blue, citeStatus: true
[wechat-api] HTML generated: /tmp/wechat-article-images-rR2JXs/temp-article.html
[wechat-api] Placeholder images: 0
[wechat-api] Title: Token越来越贵，但国家正在让算力像水电一样普惠
[wechat-api] Author: ai张三疯
[wechat-api] Digest: 你最近如果真在大量用AI产品，大概率笑不出来。...
[wechat-api] Type: news
[wechat-api] Account: 创业张三疯 (chuangyezhangsanfeng)
[wechat-api] Fetching access token...
[wechat-api] Uploading images...
[wechat-api] Uploading cover: /root/.openclaw/agents/101/workspace/imgs/cover.png


process

action: poll


toolName: process

text:
[wechat-api] Theme: default, color: blue, citeStatus: true
[wechat-api] Rendering markdown with placeholders via md-to-wechat: default, color: blue, citeStatus: true
[wechat-api] HTML generated: /tmp/wechat-article-images-rR2JXs/temp-article.html
[wechat-api] Placeholder images: 0
[wechat-api] Title: Token越来越贵，但国家正在让算力像水电一样普惠
[wechat-api] Author: ai张三疯
[wechat-api] Digest: 你最近如果真在大量用AI产品，大概率笑不出来。...
[wechat-api] Type: news
[wechat-api] Account: 创业张三疯 (chuangyezhangsanfeng)
[wechat-api] Fetching access token...
[wechat-api] Uploading images...
[wechat-api] Uploading cover: /root/.openclaw/agents/101/workspace/imgs/cover.png
[wechat-api] Publishing to draft...
{
  "success": true,
  "media_id": "imBwNhfy9Uv4ps2luE5gfnXlhwzUnJMB-biilgfUHwExrh8fWCKw6_pL9hrHeB9Q",
  "title": "Token越来越贵，但国家正在让算力像水电一样普惠",
  "articleType": "news"
}
[wechat-api] Published successfully! media_id: imBwNhfy9Uv4ps2luE5gfnXlhwzUnJMB-biilgfUHwExrh8fWCKw6_pL9hrHeB9Q

Process exited with code 0.


text: Task A is done - posted to 创业张三疯 公众号 draft successfully. Now for Task B - posting to 创业思维网 via Z-Blog XML-RPC. I'll write a Python script to do this:

write

content:
#!/usr/bin/env python3
"""Post HTML article to Z-Blog via XML-RPC."""

import xmlrpc.client
import sys

BLOG_URL = "https://weixinsiwei.com/zb_system/xml-rpc/index.php"
USERNAME = "admin"
PASSWORD = "135269zxj"
BLOG_ID = "68ba1ff10060dd54f1e399b8e332bdba"
HTML_FILE = "/tmp/computing-net-article.html"

def extract_title_html(filepath):
    with open(filepath, "r", encoding="utf-8") as f:
        html = f.read()
    # Extract title from

    import re
    h1_match = re.search(r'
]*>([^<]+)
', html)
    title = h1_match.group(1).strip() if h1_match else "算力网来了"
    return title, html

def post():
    title, html_content = extract_title_html(HTML_FILE)

    print(f"Title: {title}")
    print(f"HTML length: {len(html_content)} chars")

    server = xmlrpc.client.ServerProxy(BLOG_URL)

    # First, let's get recent posts to verify connectivity
    print("\n=== Verifying connectivity - fetching recent posts ===")
    try:
        recent = server.metaWeblog.getRecentPosts(BLOG_ID, USERNAME, PASSWORD, 3)
        print(f"Connected! Found {len(recent)} recent posts:")
        for post in recent:
            print(f"  - [{post.get('postid', '?')}] {post.get('title', '?')}")
    except Exception as e:
        print(f"Error fetching recent posts: {e}")
        # Try alternate method
        try:
            recent = server.wp.getPosts(BLOG_ID, USERNAME, PASSWORD, {"number": 3})
            print(f"Connected via wp.getPosts! Found {len(recent)} posts:")
            for p in recent:
                print(f"  - [{p.get('post_id', '?')}] {p.get('post_title', '?')}")
        except Exception as e2:
            print(f"Also failed: {e2}")
            sys.exit(1)

    # Now post
    print("\n=== Posting article ===")
    post_data = {
        "title": title,
        "description": html_content,
        "categories": ["AI政策", "算力基础设施"],
    }

    try:
        result = server.metaWeblog.newPost(BLOG_ID, USERNAME, PASSWORD, post_data, True)
        print(f"Published! Post ID: {result}")
    except Exception as e:
        print(f"Error posting: {e}")
        sys.exit(1)

    # Verify
    print("\n=== Verifying - fetching recent posts ===")
    try:
        recent = server.metaWeblog.getRecentPosts(BLOG_ID, USERNAME, PASSWORD, 5)
        for p in recent:
            print(f"  - [{p.get('postid', '?')}] {p.get('title', '?')} - {p.get('dateCreated', '?')}")
    except Exception as e:
        print(f"Verify error: {e}")

if __name__ == "__main__":
    post()


toolName: write

text: Successfully wrote 2512 bytes to /tmp/post_zb.py


exec

command: cd /tmp && python3 /tmp/post_zb.py 2>&1


text:
Title: Token越来越贵，但国家正在让算力像水电一样普惠
HTML length: 2461 chars

=== Verifying connectivity - fetching recent posts ===
Connected! Found 3 recent posts:
  - [9596] 对于有思想的人来说，这是最好的时代
  - [9595] 未来已来：大多数人还在聊天，少数人已经在赚钱了
  - [9594] 国产103号赛级燃油投产：一桶油的国产替代，比你想的更有深意

=== Posting article ===
Published! Post ID: True

=== Verifying - fetching recent posts ===
  - [9596] 对于有思想的人来说，这是最好的时代 - 2026-05-17T21:08:55+08:00
  - [9595] 未来已来：大多数人还在聊天，少数人已经在赚钱了 - 2026-05-17T19:30:00+08:00
  - [9597] Token越来越贵，但国家正在让算力像水电一样普惠 - 2026-05-17T14:36:26+08:00
  - [9594] 国产103号赛级燃油投产：一桶油的国产替代，比你想的更有深意 - 2026-05-17T10:29:30+08:00
  - [9593] 中国新能源汽车专利突破11万件：技术护城河已经挖好，后来者还能追吗？ - 2026-05-17T10:02:16+08:00


text:
Hmm, `newPost` returned `True` instead of a post ID. That usually means Z-Blog doesn't return the post ID from metaWeblog. But verification confirms the article is there at post ID 9597. Let me also check if the post returned `True` but actually published - the verification shows it's there.

Wait, looking more carefully at the verification output: the article appears as post 9597. But `newPost` returned `True` which is odd. Let me check the full list again:

Perfect - post 9597 is there: "Token越来越贵，但国家正在让算力像水电一样普惠" - successfully published.

Both tasks complete. Let me summarize:

---

## ✅ 双平台发布完成

### 任务A：公众号「创业张三疯」✅
- **草稿已发布成功**
- media_id: `imBwNhfy9Uv4ps2luE5gfnXlhwzUnJMB-biilgfUHwExrh8fWCKw6_pL9hrHeB9Q`
- 标题：Token越来越贵，但国家正在让算力像水电一样普惠
- 作者：ai张三疯
- 封面：已上传
- 状态：草稿箱中待手动发布

### 任务B：创业思维网 weixinsiwei.com ✅
- **文章已发布成功**
- Post ID: **9597**
- 标题：Token越来越贵，但国家正在让算力像水电一样普惠
- 发布时间：2026-05-17 14:36
- 验证结果：已确认出现在最新文章中

stopReason: stop