开源仓库:hanserwei/halo-cli(GPL-3.0)

适用环境:Halo 2.27+、CLI 运行时 Node.js 20.19+(插件开发与构建需 Java 21+、Node.js 24.15+ / 22.22.2+、pnpm 10+)。

说明:本项目是社区独立开发的 Halo 插件与配套命令行工具,与 Halo 官方发布的 @halo-dev/cli 相互独立。文中所有站点地址、访问密钥和对象存储凭据均为脱敏示例,使用时请按实际环境替换。

日常维护 Halo 博客时,如果只是偶尔改一篇文章,浏览器控制台已经足够顺手。但当工作流变成“本地撰写 Markdown、脚本批量同步文章与分类标签、在 CI 中审阅或导出站点配置、按 Schema 定点修改 Hanlo / Hao 主题参数、管理装备/瞬间/图库/友链以及 S3 存储策略”时,反复点选 Web 表单就会出现三个明显痛点:

  1. 自动化链路割裂:Halo 的核心能力分散在 Content API、Console API、Extension CRUD 和各个插件自己的业务端点里,直接用 curl 拼接请求既要先查 metadata.name 和版本号,又要自己处理分页、快照和异步删除轮询。
  2. 凭据颗粒度过粗:如果为了跑一个发文或传图脚本而在 CI / 机器上存放高权限个人令牌(PAT),一旦泄露就会波及整个账号的权限范围,且缺少针对单条 CLI 命令路径的接口白名单与来源 IP 限制。
  3. 配置修改缺少预检与并发保护:主题、插件、系统设置和存储策略底层保存在 ConfigMap JSON 中。手写 JSON 全量覆盖很容易漏掉必填项、误把脱敏的 [REDACTED] 写回数据库,或者在并发修改时静默丢失更新。

为了解决这些问题,我开发了 Halo CLI(当前版本 1.0.0-rc.1):它由一个运行在 Halo 端的插件(提供 Console 管理界面、单文件 CLI 分发、专用密钥鉴权与存储配置版本化事务接口)和一个零额外 npm 依赖的单文件命令行 halo-cli.cjs 组成。

本文将结合 1.0.0-rc.1 的工程源码,介绍它的整体架构、双轨鉴权模型、FormKit Schema 驱动的配置引擎、S3 存储策略的双重乐观锁保护,以及日常终端与 CI 的实战用法。

一、整体架构:插件内嵌分发与同站 API 协同

Halo CLI 不是一个只封装了几条 REST 接口的外部脚本,而是**“服务端插件 + Console 前端 + 独立单文件 CLI”**的一体化工程:

halo-cli/
├── src/main/java/com/hanserwei/halocli/   # 密钥生命周期、BeforeSecurityWebFilter 鉴权、Scope 白名单与存储配置 CAS
├── src/main/resources/                    # plugin.yaml、RBAC RoleTemplate 与内嵌的 cli/halo-cli.cjs
├── ui/                                    # Halo Console「工具 / Halo CLI」密钥管理与下载视图(Vue 3)
└── cli/                                   # TypeScript CLI 源码,经 tsup 打包为单文件 dist/halo-cli.cjs

在构建阶段,Gradle 会先在 cli/ 目录执行类型检查、测试与 tsup 打包,将生成的 halo-cli.cjs 直接打入插件 JAR 的 classpath:cli/halo-cli.cjs。插件启用后,管理员或拥有 role-template-halo-cli-download 角色的用户可以直接从 Halo Console 的「工具 / Halo CLI」页面下载与当前服务端插件完全匹配的单文件 CLI,无需在目标机器上执行 npm install。

Halo CLI 架构:Console 管理、专用 key 与 PAT 独立鉴权、Halo 原生与插件 API

如上图所示,整个系统的交互分为两条主线:

  1. 控制台管理线:用户在浏览器登录 Halo Console,访问插件提供的 /apis/console.api.halo-cli.halo.run/v1alpha1/keys 管理属于自己的专用密钥(创建、轮换、修改 Scope / IP / 过期时间、停用、删除),或通过 /downloads/cli 下载单文件脚本。
  2. 终端与自动化执行线:halo-cli 直接向目标 Halo 站点发起 HTTPS 请求,支持**专用访问密钥(hcli_...)和Halo 原生个人令牌(PAT,pat_...)**两种凭据,最终调用同站的 Halo 原生 API 与已安装插件 API。

二、安全边界:为什么在 PAT 之外还要设计 hcli_ 专用密钥?

Halo 原生已经提供了个人访问令牌(PAT)。在受信任的个人电脑上,用 PAT 操作任意接口非常方便;但在自动化脚本、AI Agent 辅助写作或定时任务场景下,我们往往希望**“这把密钥只能读写文章和上传附件,即使泄露也绝不能修改系统设置、导出其他插件密钥或操作用户账号”**。

因此,Halo CLI 设计了双轨认证机制:PAT 走 Halo 原生认证链,保留完整灵活性;而以 hcli_ 开头的专用访问密钥则由插件注册在最高优先级的 CliKeyAuthenticationFilter(实现 BeforeSecurityWebFilter,@Order(Ordered.HIGHEST_PRECEDENCE))拦截并执行五重校验。

sequenceDiagram
    participant CLI as 终端 halo-cli
    participant Filter as CliKeyAuthenticationFilter
    participant KeySvc as CliAccessKeyService
    participant Users as ReactiveUserDetailsService
    participant Policy as CliScopePolicy
    participant Halo as Halo RBAC 与业务 Handler

    CLI->>Filter: Authorization: Bearer hcli_<uuid>_<secret>
    Filter->>Filter: 解析底层 TCP Peer IP (CliConnectionPeer)
    Filter->>KeySvc: 1. 校验启用状态、过期时间、SHA-256 哈希与 CIDR 白名单
    KeySvc-->>Filter: VerifiedKey (ownerName, scopes)
    Filter->>Users: 2. 加载 Owner 账号并检查未锁定/未禁用
    Users-->>Filter: UserDetails (含当前 RBAC Authorities)
    Filter->>KeySvc: 3. 二次复核 Key 状态(防止异步查询期间被吊销)
    Filter->>Policy: 4. 校验 HTTP Method + RawPath 是否匹配已授予 Scope
    alt Scope 未授权或路径含编码绕过
        Policy-->>CLI: HTTP 403 Forbidden
    else Scope 校验通过
        Filter->>KeySvc: 异步记录 lastUsedAt(5 分钟节流 + 乐观锁)
        Filter->>Halo: 5. 移除原 Bearer 头,注入 CliKeyAuthenticationToken 进入 Halo RBAC
        Halo-->>CLI: 业务响应
    end

2.1 只存哈希与二次状态复核

创建或轮换专用密钥时,CliAccessKeyService 会生成 hcli_<uuid>_<24字节随机数Base64Url> 格式的完整令牌,仅在响应中返回一次;服务端 CliAccessKey Extension 只保存 SHA-256 哈希与 hcli_<uuid前8位> 前缀,并采用 MessageDigest.isEqual 做常量时间比对。

删除密钥时,服务端会先将 spec.enabled 置为 false 更新落库,再触发异步 delete,确保在 Halo Extension Finalizer 清理期间,软删除状态的密钥也无法继续通过认证。同时,由于 ReactiveUserDetailsService 查询用户是异步响应式调用,CliKeyAuthenticationFilter 在拿到 UserDetails 后会再次调用 keys.authenticate(token, peer) 复核密钥状态,闭合异步窗口内的吊销竞态:

// src/main/java/com/hanserwei/halocli/CliKeyAuthenticationFilter.java
private Mono<CliKeyAuthenticationToken> authenticate(String token, InetSocketAddress peer) {
    return keys.authenticate(token, peer)
        .switchIfEmpty(Mono.error(new BadCredentialsException("Invalid CLI key")))
        .flatMap(key -> owners.findByUsername(key.ownerName())
            .switchIfEmpty(Mono.error(new BadCredentialsException("Invalid CLI key owner")))
            .flatMap(owner -> {
                accountChecker.check(owner);
                // Owner lookup is asynchronous: reread key state before admitting this request.
                return keys.authenticate(token, peer)
                    .filter(fresh -> owner.getUsername().equals(fresh.ownerName()))
                    .switchIfEmpty(Mono.error(new BadCredentialsException("Invalid CLI key")))
                    .map(fresh -> new CliKeyAuthenticationToken(owner, fresh.name(),
                        fresh.displayName(), fresh.scopes()));
            }));
}

校验通过后,过滤器会从下游请求头中移除原始 Authorization 头(避免 Halo 内置的 Bearer 过滤器把 hcli_ 当作 JWT 解析报错),并将包含 Owner 当前权限集合的 CliKeyAuthenticationToken 写入 ReactiveSecurityContextHolder。这意味着:专用密钥的实际权限是“密钥勾选的 Scope 白名单”与“所属用户当前 Halo RBAC 角色”的严格交集。

2.2 未解码 rawPath 白名单与读写 Scope 隔离

在 CliScopePolicy 中,读写权限完全独立(例如 posts:write 绝不隐含 posts:read),不存在通配符或超级管理员 Scope。更重要的是,路径匹配直接针对未解码、未归一化的 request.getURI().getRawPath() 执行,任何百分号编码(%)、矩阵参数(;)、连续斜杠(//)、点段(. / ..)或非 ASCII 字符都会直接触发 Fail-Closed 拒绝:

// src/main/java/com/hanserwei/halocli/CliScopePolicy.java
private static boolean safePath(String rawPath) {
    if (rawPath == null || !rawPath.startsWith("/") || rawPath.length() > 1024
        || rawPath.endsWith("/") || rawPath.contains("//")
        || rawPath.indexOf('%') >= 0 || rawPath.indexOf(';') >= 0
        || rawPath.indexOf('\\') >= 0) {
        return false;
    }
    String[] segments = rawPath.substring(1).split("/", -1);
    for (String segment : segments) {
        if (segment.isEmpty() || ".".equals(segment) || "..".equals(segment)) {
            return false;
        }
    }
    return true;
}

这样可以从根源上杜绝利用 %2e%2e 或路径参数绕过白名单访问未授权端点的风险。密钥管理接口本身、用户与角色接口、插件安装升级接口以及任意 ConfigMap 均不在 CliScopePolicy 白名单中。

2.3 只认底层 TCP 连接的 IP 白名单

Web 应用在反向代理后面常通过 X-Forwarded-For 或 Forwarded 获取客户端 IP,但如果 Halo 端口同时暴露在局域网或容器网络,攻击者很容易伪造转发头。CliConnectionPeer 绕过了可能被改写的 request.getRemoteAddress(),直接从底层 Reactor Netty HttpServerRequest 的 connection.channel().remoteAddress() 提取对端地址:

  • 直连 Halo 时,匹配终端机器的真实 IP / CIDR(支持 IPv4 与 IPv6)。
  • 经过反向代理时,匹配反向代理节点的连接地址,可用于限制“只允许经由受控网关访问 Halo”。
  • 如果无法解析到底层 InetSocketAddress,任何配置了非空 IP 白名单的密钥都会自动拒绝认证。

2.4 客户端防重放与敏感信息脱敏

在 CLI 客户端一侧(cli/src/client.ts 与 cli/src/secrets.ts),也做了配套的纵深防护:

  1. 禁止自动跟随重定向(maxRedirects: 0):Axios 默认会跟随 301/302 重定向。如果站点配置被劫持或跳向外部地址,自动重定向可能导致 Authorization: Bearer ... 跨源泄露。Halo CLI 对所有 API 请求强制设置 maxRedirects: 0,遇到 3xx 立即报错中断;仅在 attachment download 下载附件时逐跳校验 hasSameOrigin,只有与 Halo 同源的跳转才携带凭据,跳转到对象存储外链时自动剥离令牌。
  2. 默认递归脱敏与防误写:终端展示与普通导出时,字段名包含 password、token、secret、apiKey、credential、authorization、key、private、access 的值,以及正文中的 hcli_... / pat_... 令牌都会被递归替换为 [REDACTED]。导出文件统一使用临时文件 + 0600 权限原子写入;而所有配置导入命令都会先调用 containsRedactedValue 检查输入,一旦发现字面量 [REDACTED] 立即拒绝写入,防止把脱敏备份误写回线上覆盖真实密钥。

三、核心设计解析:Schema 驱动配置与双重乐观锁存储

在 1.0.0-rc.1 中,最核心的演进是将原本零散的站点配置、主题配置、插件配置与对象存储配置统一到了严谨的工程契约之下。

3.1 FormKit Schema 驱动的配置引擎

Halo 的系统设置、主题(如 theme-hanlo、theme-hao)和各类插件都使用 FormKit Schema 声明配置表单。如果 CLI 只是把底层 JSON 原样读写,用户在终端里根本不知道有哪些字段可改、类型是什么、有哪些枚举值。

cli/src/configuration.ts 实现了一个完整的 FormKit Schema 解析与校验引擎:

flowchart LR
    Schema["读取 Setting Schema<br/>(spec.forms)"] --> Parse["递归解析字段树<br/>group / repeater / then-else"]
    Current["读取当前配置 JSON<br/>(System / Theme / Plugin)"] --> Merge["局部修改 (JSON Pointer)<br/>或合并导入 (Merge)"]
    Parse --> Validate["前置校验:<br/>禁止 [REDACTED] / 未知字段 / 类型与枚举边界"]
    Merge --> Validate
    Validate --> DryRun{"是否 --dry-run?"}
    DryRun -->|是| Diff["输出脱敏 Diff 预览<br/>不执行网络写入"]
    DryRun -->|否| Write["提交更新并返回脱敏结果"]

它解决了四个实际问题:

  1. 完整发现嵌套与条件字段:configurationFields 会递归遍历每个分组的 formSchema,不仅处理常规输入控件、group 嵌套对象和 list / repeater / array 数组(生成如 /social/media_items/* 的通配路径),还会深入 FormKit 条件分支的 then 与 else 节点,将带有 if 表达式的字段标记为 conditional: true。
  2. RFC 6901 JSON Pointer 定点修改:无需导出再导入整个 JSON 文件,直接用一条命令修改深层字段或数组下标,自动保留同分组下的其他配置。
  3. 严格的类型与必填保护:写入前,validateConfiguration 会核对所有修改路径是否在 Schema 中存在(拒绝拼写错误的未知字段)、检查 min / max 数值边界与 options 枚举集合(当 allowCreate 未开启时拒绝非法枚举),并保护非条件必填字段不被清空。
  4. 安全的默认值重置(config-reset):重置配置时,CLI 会从 Schema 提取所有默认值;对于**没有默认值但标记为必填(required)**的字段,会自动保留当前已生效的值,避免重置操作把站点改到不可用的损坏状态。

3.2 S3 存储策略:为什么需要 Policy + ConfigMap 双重乐观锁?

在 Halo 中,一条存储策略(Policy)通过 spec.configMapName 指向保存桶名、Endpoint、AccessKey 的 ConfigMap。如果直接通过通用接口分两步读写 Policy 和 ConfigMap,在并发或异常场景下会面临两个风险:

  • 如果两个策略意外共用了同一个 ConfigMap,修改其中一个策略的密钥会静默影响另一个策略。
  • Halo 2.27 对内容完全相同的 Extension 更新会跳过版本号递增;如果在读取后、写入前,另一个管理员重绑了 Policy.spec.configMapName 或轮换了 ConfigMap 凭据,普通的单边覆盖会产生丢失更新(Lost Update)。

为此,Halo CLI 在服务端和客户端协同设计了三层防护:

  1. CliPolicyWriteGuard 限制策略重绑:当使用专用密钥调用原生 /apis/storage.halo.run/v1alpha1/policies 时,新建策略禁止预先指定非空的 configMapName,更新策略只允许修改 spec.displayName,严禁篡改 templateName 或将 configMapName 指向其他已存在的 ConfigMap(防止越权挂接读取敏感配置)。
  2. exclusive 独占性检查:CliStorageConfigurationService 在读取和写入配置前都会扫描全部 Policy(包括正在删除中的策略),一旦发现目标 ConfigMap 被多个策略共享,立即返回 409 Conflict 拒绝操作。
  3. 事务内双重 CAS(Compare-And-Swap):插件新增了专用端点 /apis/console.api.halo-cli.halo.run/v1alpha1/policies/{name}/configs/default。读取时同时返回 policyVersion、configMapName 和 configMapVersion;写入时在同一个响应式事务(TransactionalOperator)内同时校验两者版本,并通过向 Policy 写入每次唯一的 halo-cli.halo.run/storage-config-write 注解强制触发真实的数据库级 CAS 锁,彻底封堵并发重绑与覆盖:
// src/main/java/com/hanserwei/halocli/CliStorageConfigurationService.java
Mono<Void> put(String name, Snapshot expected) {
    return Mono.defer(() -> {
        validate(expected);
        return policy(name).flatMap(policy -> {
            if (!Objects.equals(expected.policyVersion(), policy.getMetadata().getVersion())
                || !Objects.equals(expected.configMapName(), reference(policy))) {
                return Mono.error(conflict());
            }
            return exclusive(policy).then(Mono.defer(() -> {
                // Halo 2.27 skips identical extension updates. A fresh annotation makes
                // this a real Policy CAS that fences concurrent deletes/rebinds.
                policy.getMetadata().setVersion(expected.policyVersion());
                if (expected.configMapName() == null) {
                    return create(policy, expected.config());
                }
                var annotations = policy.getMetadata().getAnnotations() == null
                    ? new LinkedHashMap<String, String>()
                    : new LinkedHashMap<>(policy.getMetadata().getAnnotations());
                annotations.put("halo-cli.halo.run/storage-config-write", UUID.randomUUID().toString());
                policy.getMetadata().setAnnotations(annotations);
                return client.update(policy)
                    .then(Mono.defer(() -> configuration(expected.configMapName())))
                    .flatMap(configMap -> {
                        configMap.getMetadata().setVersion(expected.configMapVersion());
                        configMap.putDataItem("default", expected.config().toString());
                        return client.update(configMap);
                    }).then();
            }));
        });
    }).as(transaction::transactional)
        .onErrorMap(OptimisticLockingFailureException.class, error -> conflict());
}

四、24 个命令组全景概览

截至 1.0.0-rc.1,halo-cli 共提供 24 个顶层命令组,覆盖了从基础内容创作到站点基础设施运维的完整生命周期:

领域 命令组 核心能力说明
连接与身份 auth / auth-provider 多 Profile 管理、--token-stdin 安全登录、认证提供者启停与参数配置
内容创作 post / page / category / tag 文章与独立页面全生命周期(草稿/发布/回收站/恢复/删除)、页面快照回滚、多级分类树、颜色标签
互动与媒体 comment / attachment / storage 评论与回复审核、本地/URL 附件上传、跨域安全下载、S3 存储策略创建与版本化配置
导航与外观 menu / theme / hanlo / hao 菜单树与拖拽等价排序(Position API)、主题激活/重载、Hanlo 主题 Schema 配置、Hao 1.7.3 体检与模板目录
插件与生态 plugin / equipment / moment / photo / link 插件启停与配置、独立装备(han-equipment)、瞬间、图库(自动提取 EXIF)、友链与友链申请管理
系统与通知 settings / notifier / mail-template 动态发现八大系统设置分组、通知器与 SMTP 验证发送、邮件模板编辑/校验/恢复默认
通用扩展 extension / search / api 预设别名与任意 group/version/resource Extension CRUD(含 JSON Pointer / JSON Patch)、搜索索引查询、同站插件 API 调用

五、终端实战:从安装连接到高频工作流

5.1 构建插件与安装单文件 CLI

克隆仓库并执行 Gradle 构建,产物位于 build/libs/:

git clone https://github.com/hanserwei/halo-cli.git
cd halo-cli
./gradlew clean build

在 Halo Console 上传并启用插件 JAR 后,进入「工具 / Halo CLI」下载 halo-cli.cjs 并安装到 ~/.local/bin:

mkdir -p ~/.local/bin
install -m 0755 ~/Downloads/halo-cli.cjs ~/.local/bin/halo-cli
halo-cli --version

接着在控制台创建一个专用访问密钥(勾选所需 Scope),通过标准输入完成登录验证并保存为 production Profile(避免密钥留在 Shell 历史记录中):

read -rsp "Halo CLI key: " HALO_KEY_INPUT
printf '\n'
printf '%s' "${HALO_KEY_INPUT}" | halo-cli auth login \
  --profile production \
  --url https://your-halo.example \
  --token-stdin
unset HALO_KEY_INPUT

# 查看当前生效的连接配置(不会回显令牌)
halo-cli auth current --json

5.2 文章创作与多级分类管理

创建多级分类后,直接将本地 Markdown 文件创建为远端草稿,确认无误后再执行独立发布:

# 1. 创建父子分类并提取 metadata.name
root_cat=$(halo-cli category create \
  --display-name "工程实践" \
  --slug engineering \
  --json | jq -r '.metadata.name')

tool_cat=$(halo-cli category create \
  --display-name "开发工具与环境" \
  --slug development-tools \
  --parent "${root_cat}" \
  --json | jq -r '.metadata.name')

# 2. 从本地 Markdown 创建文章草稿(省略 --publish 则保持草稿状态)
post_name=$(halo-cli post create \
  --title "Halo CLI 1.0 终端工作流" \
  --slug halo-cli-workflow \
  --file ./article.md \
  --raw-type markdown \
  --categories "${tool_cat}" \
  --excerpt "在终端中管理 Halo 内容与站点配置。" \
  --visible PUBLIC \
  --json | jq -r '.post.metadata.name')

# 3. 核对草稿内容后正式发布
halo-cli post get "${post_name}" --json
halo-cli post publish "${post_name}" --json

关于 Markdown 渲染的注意点:halo-cli 在 --raw-type markdown 下使用启用了 html、breaks、linkify 的 markdown-it 生成 HTML 正文,同时保留 raw 与 rawType。如果文章包含复杂的预渲染 KaTeX 公式或内联 SVG / Archify 交互式图表,也可以在本地生成审核过的 HTML 片段后,通过 --file ./upload.html --raw-type html 原样提交。

5.3 用 --dry-run 安全修改站点与 Hanlo 主题配置

在修改系统设置或 theme-hanlo 配置前,可以先列出 Schema 字段定义,再用 --dry-run 预览变更前后差异(包含 previous 与 proposed 对比),确认无误后再去掉 --dry-run 正式落库:

# 查看系统设置分组与 basic 分组的字段 Schema
halo-cli settings list --json
halo-cli settings schema basic --fields --json

# 预演修改站点标题(不实际写入服务端)
halo-cli settings set basic /title "我的技术博客" --dry-run --json

# 检查 Hanlo 主题环境并预演修改装备页大标题
halo-cli hanlo doctor --json
halo-cli hanlo config-fields --json
halo-cli hanlo config-set /equipment/bigTitle "我的主力生产力装备" --dry-run --json

# 导出当前 Hanlo 配置用于版本控制备份(敏感字段自动脱敏)
halo-cli hanlo config-export --output ./hanlo-config.json

5.4 配置 S3 存储策略与上传预检

配合 PluginS3ObjectStorage(对象存储插件),可以在终端完成从模板发现、策略创建、凭据导入到附件上传预检的闭环:

# 1. 查看可用存储策略模板并创建 S3 策略
halo-cli storage templates --json
halo-cli storage create s3-main --template s3os --display-name "主对象存储" --json

# 2. 查看策略所需的配置字段 Schema
halo-cli storage config-fields s3-main --json

# 3. 先用 --dry-run 校验私有配置文件,再正式导入
halo-cli storage config-import s3-main --file ./s3-policy.json --dry-run --json
halo-cli storage config-import s3-main --file ./s3-policy.json --json

# 4. 带 --preflight 预检上传附件到指定策略
halo-cli attachment upload --file ./cover.png --policy s3-main --preflight --json

整个上传过程中,文件直接通过 Halo 的存储策略接口流转,S3 桶凭据仅在 Halo 服务端读取,CLI 本地不需要安装 AWS S3 SDK,也不会在上传请求中接触对象存储密钥。

5.5 管理装备、瞬间、图库与邮件模板

针对日常高频使用的内容扩展插件,Halo CLI 提供了语义化的专用子命令,无需手写冗长的底层 GVK 路径:

# 发布一条私密瞬间(owner 由服务端自动绑定为当前认证用户)
halo-cli moment create --content "完成了 Halo CLI 1.0-rc.1 的存储乐观锁改造。" --visible PRIVATE --json

# 上传图片到图库分组(通过插件业务端点自动提取 EXIF,沿用 PluginPhotos 配置的存储策略)
halo-cli photo groups --json
halo-cli photo upload --file ./photo.jpg --group <photo-group-name> --json

# 校验并验证发送自定义邮件模板(真实发信必须显式加 --yes)
halo-cli mail-template list --json
halo-cli mail-template validate <reason-type-name> --html-file ./notification.html --json
halo-cli mail-template verify <reason-type-name> --yes --json

而在 CI/CD 流水线中,完全不需要在 Runner 上保存任何本地配置文件,直接通过 HALO_BASE_URL 和 HALO_TOKEN 环境变量即可无状态运行所有命令:

HALO_BASE_URL=https://your-halo.example \
HALO_TOKEN=hcli_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
halo-cli post list --size 5 --json | jq '.items[].post.spec.title'

六、适用边界与已知限制

为了让自动化过程可预期,使用 halo-cli 时也需要注意以下工程边界:

  1. 文章并发编辑无服务端原子版本参数:虽然 storage config-* 实现了严格的 Policy + ConfigMap 双重版本 CAS,但 Halo 原生 Console Post API 的 post update 与 post publish 目前不接收客户端传入的预期 headSnapshot 参数。post update 在命令内部会先读取当前模型再覆盖正文,因此多人或定时任务同时编辑同一篇文章时,仍需在工作流层面保持独占编辑窗口。
  2. 专用密钥与 PAT 的职责划分:hcli_ 专用密钥遵循最小权限原则,专门服务于日常内容、已知插件、主题与存储策略操作。如果需要修改 auth-provider 认证提供者底层 ConfigMap、或者通过 extension / api 命令访问未列入 CliScopePolicy 白名单的自定义第三方接口,请使用具备相应 RBAC 角色的 Halo PAT。
  3. 删除与危险操作的显式确认:所有永久删除(delete)、回收站操作(recycle)、快照恢复(snapshot-revert)、配置全量替换(--replace)、配置重置(config-reset)以及真实发送测试邮件(verify)均要求显式传入 --yes;且删除命令内部会通过 waitForDeletion 轮询直至资源真正返回 404,不会在异步清理完成前提前报成功。

七、参考资料