$kernelink route --hydrate --safe

页面加载 /
跳到正文
auth://account/session

登录工作区

使用 Emlog 账户继续访问你的内容与互动记录。

忘记密码?

打开 Emlog 原生登录页

4.md
workspace / posts
~/posts/4.md 阅读中

给 EMlog REST API 写一个零依赖的发文脚本

EMlog 的 REST 接口可以完成发文、改文、查分类、传图:入口统一是 https://站点/?rest-api=<方法名>。官方文档只给了参数列表,实际行为有几处和直觉不一致。下面三条是写发布脚本时踩到的,结论均来自源码核对(include/controller/api_controller.php、include/lib/input.php、include/model/log_model.php)。

1. 请求体只接受表单编码

现象:POST 一个 JSON 到 ?rest-api=article_post,返回 {"code":1,"msg":"parameter error"},HTTP 400。

原因:所有入参经 Input::postStrVar() 取值:

public static function postStrVar($var_name, $var_default = '')
{
    $str = filter_input(INPUT_POST, $var_name);
    return $str ? addslashes(trim($str)) : $var_default;
}

filter_input(INPUT_POST, ...) 只解析 application/x-www-form-urlencoded 和 multipart/form-data。JSON 请求体不会进入 $_POST,于是 title、content 全部取到默认空值,触发了发布方法里的这个分支:

if (empty($title) || empty($content)) {
    Output::error('parameter error');
}

错误信息不区分"参数没传"和"参数没被解析",排查时容易往鉴权方向想。

写法:Content-Type: application/x-www-form-urlencoded。Node 侧 new URLSearchParams() 即可,上传接口例外,用 FormData。

2. article_update 是整行覆盖

现象:只想改标题,结果正文变空;只想换标签,封面丢失。

原因:更新方法组装的 $logData 无条件包含全部字段,updateLog() 把它们逐个拼进 SET 子句:

$logData = [
    'title'   => $title,
    'content' => $content,
    'excerpt' => $excerpt,
    'sortid'  => $sort_id,
    'cover'   => $cover,
    'author'  => $author_uid,
    'date'    => strtotime($post_date ?: date('Y-m-d H:i:s')),
    'hide'    => $draft === 'y' ? 'y' : 'n',
];
$this->Log_Model->updateLog($logData, $id, $author_uid);

没有"字段未传则跳过"的判断。缺省值直接落库:

未传参数 落库结果
content 正文清空
cover 封面清空
sort_id -1(未分类)
post_date 发布时间改为当前时间
draft hide='n',草稿被发布

写法:更新前先读回原记录,与本次要改的字段合并后整体提交。读取接口有两个:

  • article_detail:返回 content(Parsedown 渲染后的 HTML)和 content_raw(库中原文)。回填必须用 content_raw,否则一轮编辑之后正文里全是标签。
  • draft_detail:只返回 hide='y' 的记录,content 即原文,无需二次判断。

两者的差异还能用来判断文章状态:先查 draft_detail,命中即草稿;未命中再查 article_detail,即为已发布。

let existing, isDraft;
try {
  const d = await apiGet('draft_detail', { id }, { auth: true, soft: true });
  existing = normalizeDraft(d.draft);
  isDraft = true;
} catch {
  const d = await apiGet('article_detail', { id });
  existing = normalizeArticle(d.article);   // 取 content_raw
}
const params = { id, ...existing, ...explicitOverrides };

3. 正文存原文,渲染发生在读取时

保存链路不做任何格式转换,读取时才过 Parsedown:

$this->Parsedown = new Parsedown();
$this->Parsedown->setBreaksEnabled(true);   // 自动换行
...
'log_content' => $this->Parsedown->text($row['content']),
'content_raw' => $row['content'],

由此得出三条约束:

  1. 正文直接提交 Markdown。预先转成 HTML 会被再处理一次。
  2. setBreaksEnabled(true) 使单个换行输出 <br>。段落分隔必须用空行,不能依赖行尾空格。
  3. 后台编辑页由 editormd 渲染源码面板,看到 ##、|、``` 属正常显示,不代表转换失败。

log_model 未调用 setSafeMode,因此原生 HTML 块(<img>、<table>、<details>)原样透传给浏览器。但自带的 include/lib/parsedown.php 没有 hideLines 实现,GitHub 风格的 ||| 折叠语法不会渲染,需要折叠用 <details><summary>。

默认值与开关

项 行为
allow_remark 默认 n,即评论关闭;需显式传 y,且受全站评论总开关约束
sort_id 默认 -1(未分类);分类 ID 由 sort_list 获取,该接口无需鉴权
draft y 映射为 hide='y',前台不可见,article_detail 返回 Article not found
post_date 经 strtotime() 解析;晚于当前时间不报错,但首页查询带 date<=now,届时才可见
鉴权 api_key=<key>,或 req_time=<unix> + req_sign=md5(req_time . api_key),时间窗 ±300 秒
API 开关 后台未开启时除 article_list 外一律返回 api is closed

签名方式的价值在于密钥不出现在 URL 与访问日志里。前提是本机与服务器时钟差小于 300 秒,否则返回 request expired。

脚本

上述规则封装成单文件 CLI,无 npm 依赖(Node 18+ 自带 fetch/FormData/Blob):

node emlog.mjs post   --file 文章.md --draft --tags "EMlog,Node.js" --comments
node emlog.mjs show   --id 12          # 回读原文与元信息,校验字段是否丢失
node emlog.mjs update --id 12 --publish
node emlog.mjs upload --file 配图.png  # 返回绝对 URL,直接写入正文

update 不带 --file 时沿用线上正文,只提交显式指定的字段;带 --file 时整体替换。密钥从 ~/secrets/emlog.env 读取,不作为命令行参数传入,且所有输出(含异常堆栈)统一把密钥替换为 ***。

一个限制

REST 接口没有删除文章的方法。验证链路产生的测试稿只能登录后台手动删除,因此测试一律用 --draft(前台不可见),正式内容也先落草稿、确认后再 update --publish。

🤞 分享
comments.cmd 可写入
guest@kernelink:~/posts/4$ comment --compose
identity.env 访客信息
插入 访客会话 Text + UBB · UTF-8 · LF 0 字符