View on GitHub

WePush

专注批量推送的小而美的工具,目前支持:模板消息-公众号、模板消息-小程序、微信客服消息、微信企业号/企业微信消息、阿里云短信、阿里大于模板短信 、腾讯云短信、云片网短信、E-Mail、HTTP请求、钉钉、华为云短信、百度云短信、又拍云短信、七牛云短信

WePush Next 详细设计

1. 文档目的

本文档在架构与概要设计基础上,进一步定义 WePush Next 各组件的模块、接口、状态、数据、协议、异常、并发和部署细节,使开发人员可以据此拆分任务并开始实现。

文中的 Java 接口和 JSON 结构用于表达设计契约,正式实现时可以调整语法和类型名称,但不得未经评审改变组件职责、依赖方向和对外协议语义。

2. 已确定的设计基线

项目 首期设计基线
Java Java 21
构建 next/pom.xml 独立 Maven 聚合工程
Service Java 21、Spring Boot 4.1.x,初始版本 4.1.1
API HTTPS REST;Workspace 资源前缀 /api/v1/workspaces/{workspaceId}
实时事件 Server-Sent Events
API 契约 Contract First OpenAPI 3.1
Provider 配置 JSON Schema 2020-12 + UI Schema
WebUI TypeScript 6.0.x、Vite 8.1.x、React 19.2.x、Tailwind CSS、按需 shadcn/ui
Desktop Electron 43.x,复用 WebUI Renderer
Agent 协议 HTTP/2 + Protobuf + gRPC 双向流;Enrollment 使用 HTTPS REST
Provider 插件 PF4J 3.15.x、签名包、ClassLoader 隔离、滚动重启更新
Standalone 数据库 SQLite
Server 数据库 PostgreSQL 18.x;至少两个无状态 Service 实例构成正式 HA
Secret LocalEnvelopeSecretStore,AES-256-GCM 信封加密
大对象 Standalone 本地文件;Server 使用 S3-compatible Artifact Store
租户边界 Workspace 逻辑多租户进入正式 Server 范围
投递语义 At Least Once;支持时使用 Provider 幂等键
单机执行 Service 内嵌 Agent
分布式执行 远程 Agent 主动连接 Service

上述基线分别由 ADR-0002ADR-0008 固化。WePush Next 只面向用户自建和自运维场景;公共 SaaS、计费订阅、外部 Vault/云 KMS/Secret Manager、恶意公共租户物理隔离和跨区域控制面属于长期非目标。

3. 命名与代码约定

3.1 Maven 坐标

建议使用以下坐标:

groupId:    com.fangxuele.wepush.next
artifactId: wepush-next-<module>

Next 不复用 Classic 的包名,防止类路径混淆和误依赖。

3.2 Java 包名

com.fangxuele.wepush.next.core.api
com.fangxuele.wepush.next.core.engine
com.fangxuele.wepush.next.provider.spi
com.fangxuele.wepush.next.provider.http
com.fangxuele.wepush.next.agent.protocol
com.fangxuele.wepush.next.agent.runtime
com.fangxuele.wepush.next.service.api
com.fangxuele.wepush.next.service.domain
com.fangxuele.wepush.next.service.application
com.fangxuele.wepush.next.service.infrastructure
com.fangxuele.wepush.next.sdk
com.fangxuele.wepush.next.embedded

3.3 领域术语

术语 含义
Provider 一个具体消息渠道能力实现
Account Provider 账号配置及 Secret 引用
Message 可版本化的消息模板
Audience 受众定义
Audience Snapshot 不可变、可执行的受众快照
Job Account、Message、Audience 和执行策略的组合
Schedule Job 的定时触发规则
Run Job 的一次执行实例
Run Snapshot Run 启动时冻结的全部执行配置
Recipient 一条目标数据及模板变量
Item Run 中一条 Recipient 的执行单元
Artifact 输入、日志、结果等大体积制品
Lease Agent 在限定时间内对 Run 的执行权

4. 代码模块和职责

4.1 Core 模块

core/core-api
    ID、值对象、RunExecutionSpec、RunEvent、RunSummary、执行策略

core/provider-spi
    ProviderFactory、ProviderSession、配置 Schema、错误分类

core/engine
    DefaultExecutionEngine、调度循环、并发、限流、重试、事件聚合

4.2 Agent 模块

agent/agent-protocol
    Agent 与 Service 的版本化 DTO,不直接复用 Service 数据库实体

agent/agent-runtime
    Agent 状态机、Lease 管理、Core 调用、事件缓冲、命令执行

agent/agent-app
    远程 Agent 启动入口、Provider 组合、配置和操作系统集成

4.3 Service 模块

service/service-api
    OpenAPI 文件、生成的 Server 接口、公开 DTO、错误码

service/service-domain
    领域对象、状态机、Repository Port、领域规则

service/service-application
    用例、权限、事务编排、DTO 映射

service/service-infrastructure
    JDBC、Artifact、Secret、Agent 协议、系统时间等适配器

service/service-app
    Spring Boot、Controller、Security、配置和内嵌 Agent 组合

4.4 组合原则

5. 标识、时间和版本

5.1 标识

5.2 时间

5.3 版本

6. Core API 数据模型

6.1 配置文档

Core 不依赖 Jackson JsonNode。跨 Core 边界的 Provider 配置使用不可变文档:

public record ConfigDocument(
        String schemaId,
        String schemaVersion,
        String mediaType,
        byte[] canonicalContent
) {
    public ConfigDocument {
        canonicalContent = canonicalContent.clone();
    }

    @Override
    public byte[] canonicalContent() {
        return canonicalContent.clone();
    }
}

默认 mediaTypeapplication/json。Provider 在自己的模块中解析为强类型配置。

6.2 Secret 引用

public record SecretRef(String namespace, String name, String version) {}

public interface SecretResolver {
    SecretValue resolve(SecretRef ref);
}

public interface SecretValue extends AutoCloseable {
    char[] copyChars();
    byte[] copyBytes();
    @Override void close();
}

SecretValuetoString() 必须返回固定掩码,关闭时尽力清理内部缓冲区。

6.3 Recipient

public record RecipientRecord(
        String itemId,
        long sequence,
        Map<String, RecipientValue> fields
) {}

public sealed interface RecipientValue
        permits TextValue, NumberValue, BooleanValue, NullValue, BinaryRefValue {}

字段名区分大小写;Provider Schema 必须声明必填字段和字段用途。

6.4 RunExecutionSpec

public record RunExecutionSpec(
        String runId,
        ProviderRef provider,
        ConfigDocument accountConfig,
        ConfigDocument messageConfig,
        ExecutionPolicies policies,
        Map<String, String> attributes,
        boolean dryRun,
        Instant createdAt
) {}

Recipient Source、Secret、Artifact 和 Event Sink 是运行端口,不直接序列化进 RunExecutionSpec

6.5 执行策略

public record ExecutionPolicies(
        ConcurrencyPolicy concurrency,
        RateLimitPolicy rateLimit,
        RetryPolicy retry,
        TimeoutPolicy timeout,
        ResultPolicy result
) {}

6.6 Run 状态

public enum RunState {
    PENDING,
    LEASED,
    RUNNING,
    PAUSED,
    CANCELLING,
    CANCELLED,
    SUCCEEDED,
    PARTIAL,
    FAILED,
    LOST,
    RECOVERING
}
状态 含义
PENDING 已创建,等待可用 Agent
LEASED Service 已授予 Lease,等待 Agent 确认或启动
RUNNING Agent 和 Core 正在执行
PAUSED 暂停派发新 Item,保留执行上下文
CANCELLING 已接受取消,正在结束在途请求和 Flush 结果
CANCELLED 取消处理完成
SUCCEEDED 全部 Item 成功或按策略跳过
PARTIAL 存在明确失败、未知或未发送 Item,但 Run 正常完成收尾
FAILED Run 级错误导致无法继续或无法正确完成收尾
LOST Agent 失联,当前执行结果无法立即确认
RECOVERING Service 正在判断是否恢复、重新领取或终止

Service 管理完整状态机;Core 主要处理 RUNNINGPAUSEDCANCELLING 和执行终态。Core 不自行把 LOST 转为 RECOVERING

7. Core Engine 接口

7.1 外部接口

public interface ExecutionEngine {
    RunHandle start(RunExecutionSpec spec, ExecutionPorts ports);
}

public record ExecutionPorts(
        RecipientSource recipientSource,
        SecretResolver secretResolver,
        ResultSink resultSink,
        ArtifactSink artifactSink,
        RunEventSink eventSink,
        ExecutionClock clock
) {}

public interface RunHandle {
    String runId();
    RunState state();
    CommandResult submit(RunCommand command);
    CompletionStage<RunSummary> completion();
}

start() 在完成参数校验和资源预留后返回。完整执行结果通过 completion() 获取。

7.2 命令模型

public sealed interface RunCommand
        permits PauseRun, ResumeRun, CancelRun, ChangeConcurrency {}

public record PauseRun(String commandId) implements RunCommand {}
public record ResumeRun(String commandId) implements RunCommand {}
public record CancelRun(String commandId, String reason) implements RunCommand {}
public record ChangeConcurrency(String commandId, int target) implements RunCommand {}

命令按 commandId 幂等。同一个命令重复提交返回原处理结果。

7.3 Engine 内部组件

DefaultExecutionEngine
├── RunCoordinator
├── RecipientDispatcher
├── ConcurrencyGate
├── RateLimiter
├── RetryExecutor
├── ProviderSessionManager
├── ProgressAggregator
├── ResultBatcher
├── EventDispatcher
└── ResourceScope

每个 Run 创建独立 RunCoordinator,Engine 自身不保存跨 Run 的可变业务状态。

8. Core 执行生命周期

8.1 主流程

sequenceDiagram
    participant Caller as Agent/Embedded SDK
    participant Engine as ExecutionEngine
    participant Source as RecipientSource
    participant Provider as ProviderSession
    participant Sink as Result/Event Sink

    Caller->>Engine: start(spec, ports)
    Engine->>Engine: 校验策略和配置
    Engine->>Provider: open(context)
    Engine->>Sink: RUN_STARTED
    loop 流式读取
        Engine->>Source: nextBatch()
        Source-->>Engine: Recipient batch
        Engine->>Provider: send(item)
        Provider-->>Engine: ProviderResult
        Engine->>Sink: result batch + progress
    end
    Engine->>Provider: close()
    Engine->>Sink: flush + RUN_COMPLETED
    Engine-->>Caller: RunSummary

8.2 启动校验顺序

  1. Run ID、Provider 和端口非空。
  2. 执行策略内部一致。
  3. Provider 存在且 SPI 兼容。
  4. Account 和 Message Schema 版本受支持。
  5. Provider 执行配置校验通过。
  6. Recipient Source 可打开。
  7. Result 和 Artifact Sink 可写。
  8. Provider Session 创建成功。

校验失败不进入 RUNNING,返回稳定错误分类。

8.3 资源关闭

资源按与创建相反的顺序关闭。无论正常结束、取消还是异常,都必须尝试:

  1. 停止读取新 Recipient。
  2. 等待或终止在途发送。
  3. Flush Result Sink。
  4. Flush Event Sink。
  5. 关闭 Provider Session。
  6. 关闭 Recipient Source。
  7. 完成 RunSummary。

单个资源关闭失败不能阻止其他资源关闭,但会进入 Run 的 suppressedErrors

9. 并发模型

9.1 调度结构

最终有效并发:

min(
  用户目标并发,
  Job 最大并发,
  Provider 最大并发,
  Agent 可用容量,
  Service 下发容量
)

9.2 背压

9.3 动态并发

ChangeConcurrency 修改目标许可数:

9.4 Provider 线程安全

Provider Descriptor 和只读 Schema 可以跨 Run 共享。Provider Session 默认是 Run 级实例,必须明确声明:

未声明时按 SERIALIZED 处理。

10. 暂停、取消和停止

10.1 暂停

10.2 取消

10.3 Agent 关闭

Agent 收到操作系统停止信号后:

  1. 停止领取新 Lease。
  2. 向 Service 上报 DRAINING
  3. 在 Grace Period 内等待 Run 完成。
  4. 超时后对 Run 发起取消并 Flush 本地事件。
  5. 上报最终心跳后退出。

11. 重试、超时和错误分类

11.1 Provider 错误分类

public enum ErrorCategory {
    AUTHENTICATION,
    AUTHORIZATION,
    INVALID_REQUEST,
    RECIPIENT_INVALID,
    RATE_LIMITED,
    TEMPORARY_REMOTE,
    PERMANENT_REMOTE,
    NETWORK,
    TIMEOUT,
    CANCELLED,
    INTERNAL,
    UNKNOWN
}

ProviderResult 必须同时给出:

11.2 默认重试规则

11.3 退避

delay = min(maxDelay, initialDelay * multiplier^(attempt - 1)) + jitter

重试必须同时受最大次数、Item Deadline 和 Run Deadline 限制。

11.4 熔断和快速失败

同一 Account 在短时间内连续发生认证失败时,Engine 停止继续发送并将 Run 标记为 FAILEDPARTIAL。熔断统计属于 Run 级,跨 Run 熔断由 Agent 或 Service 后续扩展。

12. 结果和进度设计

12.1 Item 状态

PENDING     尚未派发
IN_FLIGHT   已调用 Provider,结果未确定
SUCCEEDED   Provider 明确成功
FAILED      Provider 明确失败且不再重试
UNKNOWN     可能已发送,但无法确认结果
UNSENT      Run 结束前未调用 Provider
SKIPPED     因校验或策略主动跳过

12.2 RunSummary

public record RunSummary(
        String runId,
        RunState finalState,
        long total,
        long succeeded,
        long failed,
        long unknown,
        long unsent,
        long skipped,
        long retried,
        Instant startedAt,
        Instant endedAt,
        List<ArtifactRef> artifacts,
        List<ExecutionError> suppressedErrors
) {}

计数必须满足:

total = succeeded + failed + unknown + unsent + skipped

retried 是尝试次数统计,不参与总数恒等式。

12.3 Result Sink

public interface ResultSink extends AutoCloseable {
    void append(List<ItemResult> batch);
    void flush();
}

12.4 ProgressAggregator

13. Provider SPI

13.1 ProviderFactory

public interface ProviderFactory {
    ProviderDescriptor descriptor();
    ValidationResult validateAccount(ConfigDocument account);
    ValidationResult validateMessage(ConfigDocument message);
    ConnectionTestResult testConnection(
            ConfigDocument account,
            SecretResolver secrets,
            Duration timeout);
    ProviderSession open(ProviderOpenContext context);
}

13.2 ProviderSession

public interface ProviderSession extends AutoCloseable {
    ProviderResult send(ProviderSendRequest request, CancellationToken token);
    default PreviewResult preview(ProviderPreviewRequest request) {
        throw new UnsupportedOperationException("preview");
    }
    @Override void close();
}

13.3 ProviderSendRequest

public record ProviderSendRequest(
        String runId,
        String itemId,
        int attempt,
        RecipientRecord recipient,
        ConfigDocument messageConfig,
        String idempotencyKey,
        Instant deadline
) {}

Provider 不得通过 runIdaccountIdmessageId 查询 Service 数据库。

13.4 ProviderDescriptor

Descriptor 至少包含:

14. Provider Schema 规范

14.1 文件布局

META-INF/wepush/provider.json
META-INF/wepush/schemas/account.schema.json
META-INF/wepush/schemas/message.schema.json
META-INF/wepush/schemas/recipient.schema.json
META-INF/wepush/schemas/ui.schema.json

一个内置模块包含多个 Provider 时,可以使用 META-INF/wepush/providers/<slug>/ 分目录保存各自 Descriptor 和三类 Schema;Factory 仍通过标准 ServiceLoader 文件发现。provider-standard 使用该布局。

14.2 扩展字段

x-wepush-secret             Secret 字段,不可回读
x-wepush-widget             自定义 UI 控件
x-wepush-variable           允许使用 Recipient 变量
x-wepush-multiline          多行编辑器
x-wepush-code-language      JSON、HTML、SQL 等编辑模式
x-wepush-display-order      展示顺序
x-wepush-advanced           放入高级设置
x-wepush-capability         依赖的 Provider 能力

14.3 校验层次

  1. UI 本地 Schema 校验,用于即时提示。
  2. Service Schema 校验,作为保存前权威校验。
  3. Provider 语义校验,例如签名算法、字段组合和渠道限制。
  4. 可选连接测试,不作为普通保存的隐式步骤。

UI 校验结果不能替代 Service 校验。

15. Provider 发现、隔离和更新

15.1 发现

插件包固定包含实现 JAR、Descriptor、JSON Schema、UI Schema、图标、许可证和签名清单。正式发行默认只接受受信发布者签名的包;未签名包仅能在显式 Developer Mode 中装载。

15.2 隔离

15.3 更新和回滚

更新按 STAGED → VERIFIED → DRAINING → RESTARTING → ACTIVE 推进:先下载到版本目录并验证,再停止向目标 Agent 分配新 Run,等待或按策略处理现有 Run,重启 Agent 后验证能力清单,最后激活新版本。多 Agent 环境逐台滚动;旧版本保留到回滚窗口结束。

不在 JVM 内卸载、替换正在使用的 Provider ClassLoader。Run Snapshot 固定 Provider ID、版本和 Schema 版本;只有存在兼容 Agent 时才调度新 Run。详见 ADR-0005

16. 首个 HTTP Provider 详细设计

HTTP Provider 用于验证完整纵向链路。

16.1 Account 配置

{
  "baseUrl": "https://api.example.com",
  "defaultHeaders": {
    "User-Agent": "WePush-Next"
  },
  "auth": {
    "type": "BEARER",
    "token": {"$secret": "http-account-token"}
  },
  "connectTimeout": "PT5S"
}

16.2 Message 配置

{
  "method": "POST",
  "path": "/notify",
  "headers": {
    "Content-Type": "application/json"
  },
  "query": {},
  "bodyTemplate": "{\"mobile\":\"\",\"content\":\"<h1 id="wepush-next-部署与运维">WePush Next 部署与运维</h1>

<p>本文是 <code class="language-plaintext highlighter-rouge">1.1.0</code> 稳定自部署版的可执行部署说明。Standalone 使用 SQLite 和本地 Artifact;用户自建 Server 使用 PostgreSQL、S3-compatible Artifact、两个以上 Service 实例以及外部负载均衡器。Classic 不参与 Next 的构建、安装或运行。WePush 不提供官方托管控制面,所有部署、数据、密钥和备份均由用户掌控;产品边界见<a href="/WePush/next/docs/product-scope-and-roadmap.html">《产品目标、边界与路线图》</a>。</p>

<p>Service 的无认证开发模式仅允许绑定回环地址。任何非回环 HTTP 监听都必须启用 API Security;<code class="language-plaintext highlighter-rouge">server</code> 模式还会强制 PostgreSQL、S3-compatible Artifact Store 和 Agent gRPC TLS,缺少任一项均启动失败。</p>

<h2 id="1-构建与发行物">1. 构建与发行物</h2>

<p>要求 JDK 21+、Node.js 24 和 pnpm 11.22.0:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd </span>next/ui
pnpm <span class="nb">install</span> <span class="nt">--frozen-lockfile</span>
pnpm check

<span class="nb">cd</span> ..
./scripts/prepare-winsw.sh
./mvnw verify
./mvnw <span class="nt">-DskipTests</span> package
</code></pre></div></div>

<p>测试必须先独立通过,<code class="language-plaintext highlighter-rouge">-DskipTests</code> 只用于之后的发行打包阶段。输出位于:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">distribution/target/wepush-next-1.1.0.tar.gz</code></li>
  <li><code class="language-plaintext highlighter-rouge">distribution/target/wepush-next-1.1.0.zip</code></li>
  <li>对应 <code class="language-plaintext highlighter-rouge">.sha256</code> 文件</li>
</ul>

<p>上面两个归档是使用系统 Java 21+ 的精简包。Release 流水线还在 Linux、macOS、Windows Runner 上用 JDK 21 <code class="language-plaintext highlighter-rouge">jlink</code> 生成对应架构的完整包,目录内多出 <code class="language-plaintext highlighter-rouge">runtime/</code>;两个变体都携带固定摘要的 WinSW 2.12.0,因此 Windows 用户安装时不联网。<code class="language-plaintext highlighter-rouge">prepare-winsw.sh</code> 只在源码发行构建阶段从上游下载并验证固定长度与 SHA-256。</p>

<p>归档内包含 Service/Agent Fat JAR、生产 WebUI、统一/分组件安装脚本、正式 Backup/Restore/Upgrade、配置模板和 Provider 插件生命周期工具。Desktop 原生目录包在当前操作系统执行:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd </span>next/ui
pnpm <span class="nt">--filter</span> @wepush-next/desktop package
</code></pre></div></div>

<p>Desktop 打包器使用锁定版本的 Electron,不引入带 Git 构建依赖的额外打包链;缺少 Electron Runtime 时会运行该锁定版本随附的安装脚本。macOS、Windows、Linux 必须分别在目标操作系统打包。
pnpm 已显式信任固定版本 Electron 的原生运行时下载脚本;无法访问 GitHub Release Asset 的网络可在安装依赖时设置受信 <code class="language-plaintext highlighter-rouge">ELECTRON_MIRROR</code>。按项目明确边界,<code class="language-plaintext highlighter-rouge">1.1.0</code> 不使用商业签名:macOS 只做 ad-hoc 签名且不提交 Apple Notarization,Windows 不做 Authenticode 签名。系统可能显示未知开发者警告,请只从项目 GitHub Releases 下载并验证 <code class="language-plaintext highlighter-rouge">SHA256SUMS</code>;详见 <a href="/WePush/next/UNSIGNED-NOTICE.html"><code class="language-plaintext highlighter-rouge">UNSIGNED-NOTICE.md</code></a>。</p>

<h2 id="2-linux-standalone">2. Linux Standalone</h2>

<p>解压、校验并安装:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sha256sum </span>wepush-next-1.1.0-linux-x64.tar.gz
<span class="nb">tar</span> <span class="nt">-xzf</span> wepush-next-1.1.0-linux-x64.tar.gz
<span class="nb">sudo</span> ./wepush-next-1.1.0/install/install.sh
</code></pre></div></div>

<p>安装布局:</p>

<ul>
  <li>版本目录:<code class="language-plaintext highlighter-rouge">/opt/wepush-next/releases/&lt;version&gt;</code></li>
  <li>原子当前链接:<code class="language-plaintext highlighter-rouge">/opt/wepush-next/current</code></li>
  <li>配置:<code class="language-plaintext highlighter-rouge">/etc/wepush-next/service.env</code>、<code class="language-plaintext highlighter-rouge">agent.env</code></li>
  <li>数据:<code class="language-plaintext highlighter-rouge">/var/lib/wepush-next/service</code>、<code class="language-plaintext highlighter-rouge">agent</code></li>
  <li>systemd:默认安装 <code class="language-plaintext highlighter-rouge">wepush-next-service</code>;高级 <code class="language-plaintext highlighter-rouge">all</code> 模式另安装 <code class="language-plaintext highlighter-rouge">wepush-next-agent</code></li>
</ul>

<p>环境文件由 root 持有,只向相应服务组开放读取。修改配置后执行:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>systemctl restart wepush-next-service
curl <span class="nt">--fail</span> http://127.0.0.1:18990/actuator/health/installation
</code></pre></div></div>

<p>备份会停止两个进程以获得 SQLite、Journal、Outbox 和配置的一致快照,并恢复原先处于运行状态的服务:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo</span> /opt/wepush-next/current/install/linux/backup.sh
<span class="nb">sudo</span> /opt/wepush-next/current/install/linux/restore.sh <span class="nt">--validate-only</span> <span class="s2">"</span><span class="nv">$BACKUP_FILE</span><span class="s2">"</span>
<span class="nb">sudo</span> /opt/wepush-next/current/install/linux/restore.sh <span class="s2">"</span><span class="nv">$BACKUP_FILE</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$BACKUP_SHA256</span><span class="s2">"</span>
<span class="nb">sudo</span> /opt/wepush-next/current/install/linux/upgrade.sh release.tar.gz &lt;sha256&gt;
<span class="nb">sudo</span> /opt/wepush-next/current/install/linux/uninstall.sh
<span class="nb">sudo</span> /opt/wepush-next/current/install/linux/uninstall.sh <span class="nt">--purge</span>  <span class="c"># 明确删除配置与数据</span>
</code></pre></div></div>

<h2 id="3-macos-standalone">3. macOS Standalone</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo</span> ./wepush-next-1.1.0/install/install.sh
launchctl print system/com.fangxuele.wepush-next.service
curl <span class="nt">--fail</span> http://127.0.0.1:18990/actuator/health/installation
</code></pre></div></div>

<p>版本、数据、配置和日志分别位于 <code class="language-plaintext highlighter-rouge">/Library/WePushNext</code>、<code class="language-plaintext highlighter-rouge">/Library/Preferences/wepush-next</code> 与 <code class="language-plaintext highlighter-rouge">/Library/Logs/WePushNext</code>。安装器用 LaunchDaemon 托管 Service/Agent,并拒绝覆盖非符号链接的 <code class="language-plaintext highlighter-rouge">current</code> 路径。
LaunchDaemon 默认以执行 <code class="language-plaintext highlighter-rouge">sudo</code> 的非 root 用户运行;无人值守安装必须显式设置已存在的 <code class="language-plaintext highlighter-rouge">WEPUSH_SERVICE_USER</code>,安装器拒绝回退到 root。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo</span> /Library/WePushNext/current/install/macos/backup.sh
<span class="nb">sudo</span> /Library/WePushNext/current/install/macos/restore.sh <span class="nt">--validate-only</span> <span class="s2">"</span><span class="nv">$BACKUP_FILE</span><span class="s2">"</span>
<span class="nb">sudo</span> /Library/WePushNext/current/install/macos/restore.sh <span class="s2">"</span><span class="nv">$BACKUP_FILE</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$BACKUP_SHA256</span><span class="s2">"</span>
<span class="nb">sudo</span> /Library/WePushNext/current/install/macos/upgrade.sh release.zip &lt;sha256&gt;
<span class="nb">sudo</span> /Library/WePushNext/current/install/macos/uninstall.sh
</code></pre></div></div>

<h2 id="4-windows-standalone">4. Windows Standalone</h2>

<p>以管理员 PowerShell 执行:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Expand-Archive</span><span class="w"> </span><span class="o">.</span><span class="nx">\wepush-next-1.1.0-windows-x64.zip</span><span class="w"> </span><span class="o">.</span><span class="nx">\release</span><span class="w">
</span><span class="n">Set-ExecutionPolicy</span><span class="w"> </span><span class="nt">-Scope</span><span class="w"> </span><span class="nx">Process</span><span class="w"> </span><span class="nx">Bypass</span><span class="w">
</span><span class="o">&amp;</span><span class="w"> </span><span class="o">.</span><span class="n">\release\wepush-next-1.1.0\install\install.ps1</span><span class="w">
</span><span class="nx">Get-Service</span><span class="w"> </span><span class="nx">WePushNextService</span><span class="w">
</span><span class="n">Invoke-WebRequest</span><span class="w"> </span><span class="nx">http://127.0.0.1:18990/actuator/health/installation</span><span class="w">
</span></code></pre></div></div>

<p>安装器只使用发行包内的 WinSW 2.12.0,并在安装前再次校验固定长度和 SHA-256;缺失或被修改时失败关闭,不进行在线下载。Service 以低权限 <code class="language-plaintext highlighter-rouge">LocalService</code> 运行。版本目录位于 <code class="language-plaintext highlighter-rouge">%ProgramFiles%\WePush Next</code>,配置和数据位于 <code class="language-plaintext highlighter-rouge">%ProgramData%\WePush Next</code>;该目录移除继承 ACL,只允许 LocalService、SYSTEM 和 Administrators。备份默认写到独立的 <code class="language-plaintext highlighter-rouge">%ProgramData%\WePush Next Backups</code>。</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&amp;</span><span class="w"> </span><span class="s2">"</span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">ProgramFiles</span><span class="s2">\WePush Next\current\install\windows\backup.ps1"</span><span class="w">
</span><span class="o">&amp;</span><span class="w"> </span><span class="s2">"</span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">ProgramFiles</span><span class="s2">\WePush Next\current\install\windows\restore.ps1"</span><span class="w"> </span><span class="nt">-ValidateOnly</span><span class="w"> </span><span class="nt">-Archive</span><span class="w"> </span><span class="s1">'&lt;backup&gt;.zip'</span><span class="w">
</span><span class="o">&amp;</span><span class="w"> </span><span class="s2">"</span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">ProgramFiles</span><span class="s2">\WePush Next\current\install\windows\restore.ps1"</span><span class="w"> </span><span class="nt">-Archive</span><span class="w"> </span><span class="s1">'&lt;backup&gt;.zip'</span><span class="w"> </span><span class="nt">-ExpectedSha256</span><span class="w"> </span><span class="s1">'&lt;sha256&gt;'</span><span class="w">
</span><span class="o">&amp;</span><span class="w"> </span><span class="s2">"</span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">ProgramFiles</span><span class="s2">\WePush Next\current\install\windows\upgrade.ps1"</span><span class="w"> </span><span class="nt">-Archive</span><span class="w"> </span><span class="n">release.zip</span><span class="w"> </span><span class="nt">-ExpectedSha256</span><span class="w"> </span><span class="err">&lt;</span><span class="nx">sha256</span><span class="err">&gt;</span><span class="w">
</span><span class="o">&amp;</span><span class="w"> </span><span class="s2">"</span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">ProgramFiles</span><span class="s2">\WePush Next\current\install\windows\uninstall.ps1"</span><span class="w">
</span></code></pre></div></div>

<p>只有显式 <code class="language-plaintext highlighter-rouge">-Purge</code> 才删除持久数据。</p>

<h2 id="5-agent-enrollment-与远程运行">5. Agent Enrollment 与远程运行</h2>

<p>Server 管理员在 WebUI 设置页或 API 创建一次性 Enrollment Token,并绑定 Workspace。Agent 第一次启动设置:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>WEPUSH_AGENT_ID=agent-east-1
WEPUSH_AGENT_ENROLLMENT_TOKEN=&lt;one-time-token&gt;
WEPUSH_SERVICE_BASE_URL=https://wepush.example.com
WEPUSH_SERVICE_HOST=wepush.example.com
WEPUSH_AGENT_GRPC_PORT=19090
WEPUSH_AGENT_GRPC_PLAINTEXT=false
</code></pre></div></div>

<p>Agent 生成 P-256 私钥,在 HTTPS Enrollment 后保存长期 Credential、客户端证书与 CA;证书或 Credential 距到期不足 14 天时自动轮换。身份文件、私钥、Event Outbox、Completion Outbox 和 Lease Journal 均必须位于持久卷。非回环部署不允许匿名 Agent;gRPC 必须启用 TLS,生产基线要求 mTLS。</p>

<h2 id="6-provider-插件">6. Provider 插件</h2>

<p>HTTP、SMTP Email、飞书/钉钉/企微机器人、阿里云短信、微信公众号、小程序和企业微信应用消息是发行包内置 Provider,不需要放入插件目录。内置渠道的账号、SecretRef、最小消息和验证步骤见<a href="/WePush/next/docs/provider-guide.html">《内置 Provider 指南》</a>。</p>

<p>CMPP、SMGP、SGIP 和 SMPP 从 <code class="language-plaintext highlighter-rouge">1.1.0</code> 起作为独立 Release Asset 交付,文件名为 <code class="language-plaintext highlighter-rouge">wepush-provider-&lt;protocol&gt;-1.1.0.zip</code> 及对应 <code class="language-plaintext highlighter-rouge">.sha256</code>。官方签名 Key ID/公钥在同一 Release 的 <code class="language-plaintext highlighter-rouge">wepush-provider-trusted-key-1.1.0.env</code> 中,并带独立 <code class="language-plaintext highlighter-rouge">.sha256</code>;它们也全部列入统一 <code class="language-plaintext highlighter-rouge">SHA256SUMS</code>。插件不会随核心发行包自动启用。先从同一 GitHub Release 下载并同时校验这些文件,再执行 Stage/Activate;不要信任插件 ZIP 内或第三方页面提供的替代公钥。能力边界、源码构建与发布签名说明见 <a href="/WePush/next/plugins/"><code class="language-plaintext highlighter-rouge">plugins/README.md</code></a>。</p>

<p>正式模式必须设置 <code class="language-plaintext highlighter-rouge">WEPUSH_PLUGIN_TRUSTED_KEYS</code>,格式是 <code class="language-plaintext highlighter-rouge">keyId:Base64Ed25519PublicKey</code>,多个发布者用逗号分隔。插件 ZIP 包内 <code class="language-plaintext highlighter-rouge">plugin.json</code> 的 SHA-256 清单与 <code class="language-plaintext highlighter-rouge">signature.ed25519</code> 必须通过验证;ZIP Slip、共享 API 重复打包、未知签名者和 SPI 不兼容都会导致 Agent 失败关闭。</p>

<p>Linux 应以 Agent 服务账号执行 Stage/Activate,确保文件所有权正确:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo</span> <span class="nt">-u</span> wepush-agent /opt/wepush-next/current/plugins/stage.sh provider.zip
<span class="nb">sudo</span> <span class="nt">-u</span> wepush-agent /opt/wepush-next/current/plugins/activate.sh &lt;pluginId&gt;.zip
<span class="nb">sudo</span> <span class="nt">-u</span> wepush-agent /opt/wepush-next/current/plugins/rollback.sh &lt;pluginId&gt;.zip
</code></pre></div></div>

<p>Stage 先调用 Agent 的生产校验器读取 <code class="language-plaintext highlighter-rouge">plugin.json</code>,验证摘要、SPI 和 Ed25519 签名,再以 <code class="language-plaintext highlighter-rouge">&lt;pluginId&gt;.zip</code> 作为稳定文件名落盘。激活采用版本备份、Supervisor 重启和健康观察;新插件导致 Agent 退出时自动恢复旧包并再次启动。Desktop 的 Providers 页提供相同的本地选择、校验、Stage、Activate 和 Rollback 操作,并通过系统授权写入插件目录。该机制是受控滚动更新,不在 JVM 内热卸载 ClassLoader。</p>

<h2 id="7-serverha-容器拓扑">7. Server/HA 容器拓扑</h2>

<p><code class="language-plaintext highlighter-rouge">deployment/container/compose.server.yaml</code> 是本地/验收拓扑:PostgreSQL 18、MinIO、两个无状态 Service 和 HAProxy。HTTP API 通过 <code class="language-plaintext highlighter-rouge">127.0.0.1:18990</code> 暴露;Agent gRPC 通过 TCP 透传的 <code class="language-plaintext highlighter-rouge">19090</code> 暴露,因此 mTLS 端到端保持在 Agent 与 Service 之间。</p>

<p>首次运行:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd </span>next/deployment/container
./generate-dev-secrets.sh
docker compose <span class="nt">--env-file</span> .env.server.local <span class="nt">-f</span> compose.server.yaml up <span class="nt">--build</span> <span class="nt">-d</span>
curl <span class="nt">--fail</span> http://127.0.0.1:18990/actuator/health/readiness
curl <span class="nt">--fail</span> http://127.0.0.1:18990/actuator/prometheus
</code></pre></div></div>

<p>开发脚本生成的 CA/密钥和 <code class="language-plaintext highlighter-rouge">.env.server.local</code> 已被 <code class="language-plaintext highlighter-rouge">.gitignore</code> 排除,且拒绝覆盖已有文件。两个 Service 共享:</p>

<ul>
  <li>PostgreSQL 业务状态、Lease、Service→Agent Outbox、Schedule 与审计事实源;</li>
  <li>Agent CA、API Bootstrap Token、Secret 主密钥和 Artifact 签名密钥;</li>
  <li>MinIO Artifact 对象。</li>
</ul>

<p>Schedule Scanner 使用 PostgreSQL Advisory Lock 单 Leader;Agent 命令和 Lease Offer 使用持久 outbox,只有持有当前 gRPC 流的实例发送;SSE 以数据库事件日志和周期轮询跨实例补偿。</p>

<p><code class="language-plaintext highlighter-rouge">1.1.0</code> 另对 <code class="language-plaintext highlighter-rouge">wepush_run_pending</code>、<code class="language-plaintext highlighter-rouge">wepush_agent_outbox</code>、<code class="language-plaintext highlighter-rouge">wepush_run_event</code> 三个 PostgreSQL Channel 执行 <code class="language-plaintext highlighter-rouge">LISTEN/NOTIFY</code>,只用于缩短调度、Agent 命令和 SSE 的唤醒延迟。连接断开、通知丢失或重复不得影响正确性,周期扫描、持久 Outbox 与事件游标必须始终保持启用。</p>

<p>除 Compose 验收拓扑外,<code class="language-plaintext highlighter-rouge">deployment/templates/</code> 提供可复制的 Nginx、Traefik 和 Kubernetes 用户自建模板。模板只描述网络入口、Service 与配置挂载,不创建受项目托管的数据库、对象存储或密钥服务;部署者必须根据自己的域名、证书、StorageClass、Secret 管理和备份方案完成替换。</p>

<p>该 Compose 为本地协议验收拓扑,明确使用 <code class="language-plaintext highlighter-rouge">WEPUSH_S3_SERVER_SIDE_ENCRYPTION=NONE</code>。正式自建环境应配置对象存储原生 <code class="language-plaintext highlighter-rouge">AES256</code>,或由部署者在其存储层保证等价的静态加密;WePush 不对接或管理云 KMS。正式环境还必须:</p>

<ul>
  <li>使用由部署者负责的 PostgreSQL HA 和备份恢复;</li>
  <li>在 HTTP API 前终止受信 TLS,保留 gRPC TLS 透传或等价的受控 mTLS 方案;</li>
  <li>将 Master Key、Agent CA 和其他密钥放入权限受控的只读挂载文件,不使用提交到源码或共享目录的 Compose env 文件;</li>
  <li>配置 S3 Lifecycle 作为未完成 Multipart 与误删保护的兜底;</li>
  <li>至少两个 Service 跨故障域部署并配置 Readiness/Prometheus 告警。</li>
</ul>

<p>停止本地环境:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker compose <span class="nt">--env-file</span> .env.server.local <span class="nt">-f</span> compose.server.yaml down
</code></pre></div></div>

<p>只有确认不再需要测试数据时才执行带 <code class="language-plaintext highlighter-rouge">--volumes</code> 的删除。</p>

<h2 id="8-110-资源诊断与-artifact-运维">8. <code class="language-plaintext highlighter-rouge">1.1.0</code> 资源、诊断与 Artifact 运维</h2>

<ul>
  <li>系统管理员在 WebUI 设置页或 <code class="language-plaintext highlighter-rouge">/api/v1/workspaces/{workspaceId}/policy</code> 管理 Agent 数、活动 Run、总发送并发、Artifact 容量和默认保留期。首次显式保存策略前,新 Artifact 继续采用已有导出/Agent 分类保留配置;保存后统一使用 Workspace 保留期。调整上限不会删除既有资源;当前使用量超过新上限时拒绝新增操作,直到使用量回落。</li>
  <li><code class="language-plaintext highlighter-rouge">/api/v1/system/diagnostics</code> 只允许系统管理员手动生成 ZIP;包内配置、日志摘要和运行状态会移除 Token、Secret、完整 Recipient 与 Provider 敏感响应。诊断包仍应按敏感运维数据保存和传输。</li>
  <li><code class="language-plaintext highlighter-rouge">/api/v1/system/version-check</code> 只在用户点击或显式调用时访问 GitHub Releases,筛选稳定的 <code class="language-plaintext highlighter-rouge">next-v*</code> 版本。它没有后台调度、遥测或自动下载/安装行为;完全离线环境无需配置例外。</li>
  <li>账号认证熔断可在 Account 对应管理页检查或复位。先确认渠道凭据已经修正;直接复位但不修正根因会再次熔断。</li>
  <li>Agent Presigned Multipart 会话记录在 V17。数据库清理会 Abort 失败/过期会话;S3-compatible Bucket 还必须配置 24 小时终止未完成 Multipart 的 Lifecycle 兜底。不要只删除数据库行而保留对象存储上传会话。</li>
</ul>

<h2 id="9-升级恢复与验收">9. 升级、恢复与验收</h2>

<p>Standalone 升级顺序:一致性备份 → 校验发行 SHA-256 → 展开新版本目录 → 原子切换 <code class="language-plaintext highlighter-rouge">current</code> → 重启 → Installation Health(Readiness、Flyway 当前版本、内置 Provider 本地 Dry Run)→ 成功保留备份。任一步失败都会切回旧 <code class="language-plaintext highlighter-rouge">current</code>,用刚生成的备份恢复配置和数据,再验证旧版本;命令以失败状态退出并保留恢复目录供人工审计。Server 滚动升级先执行单实例 Migration Job,再逐个替换 Service,最后滚动 Drain/Restart Agent。</p>

<p>Backup Archive 包含 <code class="language-plaintext highlighter-rouge">BACKUP-MANIFEST</code>、逐文件 <code class="language-plaintext highlighter-rouge">SHA256SUMS</code> 和配置/数据 Payload。Restore 在展开前拒绝路径穿越,并要求实际 Payload 文件集合与摘要清单完全一致;替换前保存 <code class="language-plaintext highlighter-rouge">pre-restore-*</code>,恢复后执行相同 Installation Health,失败时恢复原目录。不要分别恢复数据库和 Master Key。</p>

<p>最低验收项:</p>

<ol>
  <li>Readiness 和 Prometheus 可访问。</li>
  <li>WebUI 能保存 Account/Message/Audience/Job 并启动 Dry Run。</li>
  <li>SSE 能重连并从 <code class="language-plaintext highlighter-rouge">Last-Event-ID</code> 回放。</li>
  <li>Agent Enrollment、Hello/Welcome、Lease、Event Ack、Artifact Commit 和 Run Completion 成功。</li>
  <li>停掉一个 Service 后 API、Schedule、SSE 和 Agent 重连仍可恢复。</li>
  <li>备份恢复后 SQLite/PostgreSQL、Artifact、Agent Journal/Outbox 与 Secret 主密钥一致。</li>
  <li>Workspace 配额拒绝与审计一致,认证熔断可观察/复位,诊断包抽查不存在 Token、Secret、完整 Recipient 或未脱敏 Provider 响应。</li>
  <li>S3 Presigned Multipart 可完成和中止;模拟丢失 PostgreSQL 通知后,轮询仍可推进 Run、Agent Outbox 与 SSE。</li>
</ol>
\"}",
  "successCondition": {
    "status": [200, 201],
    "jsonPath": "$.success",
    "equals": true
  },
  "saveResponseBody": false
}

16.3 安全限制

16.4 错误映射

HTTP 情况 ErrorCategory 默认重试
2xx 且成功条件成立 成功
400、422 INVALID_REQUEST
401 AUTHENTICATION 否,Run 快速失败
403 AUTHORIZATION
404 PERMANENT_REMOTE
408、504 TIMEOUT 依幂等策略
429 RATE_LIMITED
500、502、503 TEMPORARY_REMOTE
DNS/连接失败 NETWORK

17. Agent Runtime 组件

AgentRuntime
├── AgentIdentityStore
├── GrpcAgentClient
├── RegistrationManager
├── HeartbeatLoop
├── AgentControlStream
├── LeaseSupervisor
├── RunExecutor
├── CommandInbox
├── EventOutbox
├── ArtifactTransfer
├── LocalJournal
└── ProviderCatalog

17.1 Agent 状态

STARTING
UNREGISTERED
CONNECTING
ONLINE
DRAINING
DEGRADED
OFFLINE
STOPPED

17.2 本地 Agent 标识

本地保存:

文件权限必须限制为 Agent 运行用户可读。

18. Agent 注册和身份

18.1 Enrollment

管理员在 Service 创建一次性 Enrollment Token。Agent 首次启动调用:

POST /internal/agent/v1/enroll
Authorization: Enrollment <one-time-token>

请求:

{
  "displayName": "worker-shanghai-01",
  "agentVersion": "0.1.0",
  "protocolVersions": ["1"],
  "platform": {
    "os": "linux",
    "arch": "x86_64",
    "java": "21"
  },
  "providers": [
    {"id": "wepush.http", "version": "1.0.0", "spi": "1"}
  ]
}

响应包含 agentId 和长期 Agent Credential。Enrollment Token 使用一次后立即失效。

18.2 后续认证

19. Agent gRPC 控制流和租约协议

19.1 服务契约

正式契约位于 agent/agent-protocol/src/main/proto/。核心形态如下,具体字段以版本化 Proto 为准:

service AgentControlService {
  rpc Connect(stream AgentToService) returns (stream ServiceToAgent);
}

message AgentToService {
  string agent_id = 1;
  uint64 sequence = 2;
  oneof payload {
    Hello hello = 10;
    Heartbeat heartbeat = 11;
    LeaseAck lease_ack = 12;
    EventBatch event_batch = 13;
    CommandAck command_ack = 14;
    RunCompleted run_completed = 15;
    Draining draining = 16;
  }
}

message ServiceToAgent {
  uint64 sequence = 1;
  oneof payload {
    Welcome welcome = 10;
    LeaseOffer lease_offer = 11;
    RunCommand command = 12;
    EventAck event_ack = 13;
    DrainRequest drain = 14;
  }
}

同一方向的 sequence 单调递增。gRPC 保证单条活跃流内有序,但 Agent 和 Service 仍持久化最后确认位置,以处理断线、重连和重复帧。应用消息设置大小上限;Audience、结果、日志和插件包不进入控制流。

19.2 建连和心跳

Agent 使用 Enrollment 获得的身份建立 Connect,第一帧必须是 Hello,包含 Agent 版本、协议范围、平台、Provider 清单摘要、容量和恢复游标。Service 返回 Welcome,确认选定协议版本、Server 时间、心跳间隔、消息上限和双方已确认 Sequence。

Heartbeat 包含 Agent 状态、最大/活跃 Run、可用并发和当前 Lease 摘要。心跳间隔默认 10 秒;超过三个周期未收到有效帧时 Service 将连接标记为失联并启动 Lease 恢复,但具体 Lease 到期以数据库时间为准。

19.3 Lease Offer 和确认

Service 根据 Agent 能力和容量主动发送 LeaseOffer,其中包含:

Agent 下载并校验 Snapshot 后发送 LeaseAck。未在 Ack Deadline 内确认的 Offer 失效;Service 只有在校验 Agent、Lease、Epoch 和 Fencing Token 后才允许 Run 进入 RUNNING

当前实现可通过 wepush.execution.mode=remote 启用该链路:agent_lease 保存会话归属、Epoch、Fencing Token、状态和最后连续 Event Sequence;Execution Spec 与 Audience 由受 Agent Credential/Bootstrap Token 保护的 /internal/agent/v1/leases/{leaseId}/... JSON API 提供,并在 Offer 中携带 SHA-256。Secret Envelope 已采用一次性 X25519 + HKDF-SHA-256 + AES-256-GCM 实现,Agent 会话公钥由 Hello 发布并持久化,密文同时绑定 Agent/Run/Lease/Epoch/Fence/过期时间,明文只在 Agent 运行内存短期存在。Agent Artifact 使用 Lease 绑定的短期上传计划,可经 Service 流式上传或 S3 Presigned Put,Commit 前执行 Head、大小和 SHA-256 完整性校验;Service 直接写入达到 100 MiB 时使用 Multipart。

19.4 Fencing 和重连

每次重新分配 Lease 都递增 epoch 并生成新的不透明 Fencing Token。所有 LeaseAck、Heartbeat Lease、EventBatch、CommandAck 和 RunCompleted 都携带 leaseId + epoch + fencingToken,旧 Epoch 写入一律拒绝。

Agent 断线后以指数退避和抖动重连负载均衡器,并在 Hello 中报告未完成 Lease、双方 Sequence 和 Outbox 范围。任意 Service 实例从 PostgreSQL 恢复状态并返回继续、停止或重新同步指令;连接到原实例不是恢复前提。

20. Agent 命令和事件上报

20.1 命令结构

RunCommand 包含 Agent 方向 Sequence、Command ID、Run ID、Lease Fencing 信息、命令类型、Payload 和创建时间。

20.2 事件批次

Agent 使用 EventBatch 帧发送 firstSequencelastSequence 和事件集合。Service 持久化后发送 EventAck,给出最后连续确认 Sequence。Agent 可以安全重传整个批次,Service 通过 runId + eventSequence 去重;发现缺口时要求从已确认位置重发。

Event Outbox 有内存和本地磁盘上限。达到高水位时优先聚合进度类事件并对 Core 施加背压,状态转换、错误和终态事件不得静默丢弃。

20.3 完成和 Artifact

RunCompleted 包含 RunSummary、Artifact ID/校验值/大小和最后 Event Sequence。大文件由 Agent 使用 Service 签发的短期 HTTPS URL 直接上传;Service 通过 Head 和 Checksum 验证所有必需 Artifact 均为 READY 后才提交 Run 终态。缺失内容时发送可重试拒绝,Agent 保留 Journal 和 Outbox。

21. Agent 本地恢复

Local Journal 不保存明文 Secret,至少记录:

Agent 重启后:

  1. 读取 Journal。
  2. 向 Service 查询 Lease 当前所有权。
  3. 仍有效且允许恢复时继续上报或恢复执行。
  4. Lease 已失效时停止旧执行,只允许上传诊断信息。
  5. 无法确认 Provider 调用结果的 Item 标记为 UNKNOWN

首期可以不支持从中间 Recipient 位置继续执行,但必须正确终止和报告,不允许静默丢失 Run。

22. Service 领域和应用服务

22.1 主要应用服务

ProviderQueryService
AccountApplicationService
MessageApplicationService
AudienceApplicationService
JobApplicationService
ScheduleApplicationService
RunApplicationService
RunCommandService
AgentApplicationService
AgentLeaseService
ArtifactApplicationService
SecretApplicationService
AuditApplicationService

22.2 典型用例:创建 Run

事务内完成:

  1. 校验 Idempotency Key。
  2. 加载 Job 并检查权限。
  3. 加载 Account、Message Revision 和 Audience Snapshot。
  4. 校验 Provider 存在且配置版本兼容。
  5. 创建不可变 Run Snapshot。
  6. 创建 PENDING Run。
  7. 写入 RUN_CREATED 事件和审计记录。
  8. 保存 Idempotency 结果。

事务提交后发出可用性提示,由已连接的 Service 实例尝试生成 LeaseOffer。提示丢失不影响正确性,数据库扫描仍会发现待调度 Run。

22.3 典型用例:下发命令

  1. 校验用户和 Run 权限。
  2. 根据状态机判断命令是否合法。
  3. 生成 Command ID 和 Agent Sequence。
  4. 持久化命令。
  5. 返回 ACCEPTED,不等待 Agent 完成命令。
  6. Agent Ack 后更新命令状态并产生 Run Event。

23. Service 事务边界

23.1 原则

23.2 乐观锁

可修改聚合包含 version 字段:

UPDATE job_definition
SET ..., version = version + 1
WHERE id = ? AND version = ?

受影响行数为 0 时返回并发修改错误。

23.3 Run 状态并发

Run 状态变化使用状态条件和版本双重保护:

UPDATE run
SET state = ?, version = version + 1
WHERE id = ? AND state IN (...) AND version = ?

禁止先读后无条件写。

24. Service 公开 API

24.1 OpenAPI 文件拆分

service/service-api/src/main/openapi/
├── openapi.yaml
├── paths/
│   ├── accounts.yaml
│   ├── messages.yaml
│   ├── audiences.yaml
│   ├── jobs.yaml
│   ├── schedules.yaml
│   ├── runs.yaml
│   └── agents.yaml
└── schemas/
    ├── common.yaml
    ├── problem.yaml
    ├── account.yaml
    ├── run.yaml
    └── event.yaml

构建时先校验和打包 OpenAPI,再生成 Server Stub 与 Remote Java SDK。生成代码不得手工修改。

24.2 创建 Run

POST /api/v1/workspaces/{workspaceId}/jobs/{jobId}/runs
Idempotency-Key: 20260822-manual-001
Content-Type: application/json
{
  "dryRun": false,
  "policyOverrides": {
    "concurrency": {"target": 50}
  },
  "reason": "manual",
  "confirmationToken": "token-from-run-confirmation"
}

手工正式发送必须先调用 POST /jobs/{jobId}/run-confirmation。Service 返回 Provider、Account、Audience 数量、策略、并发、限速、预计规模和五分钟令牌;令牌绑定 Workspace、Job 及 Account/Message/Audience/Policy 版本,资源发生变化或令牌过期后必须重新确认。Scheduler 使用内部可信入口,不依赖交互令牌。

成功返回 202 Accepted

{
  "id": "run-id",
  "state": "PENDING",
  "createdAt": "2026-08-22T08:30:00Z",
  "links": {
    "self": "/api/v1/workspaces/workspace-id/runs/run-id",
    "events": "/api/v1/workspaces/workspace-id/runs/run-id/events"
  }
}

24.3 运行详情

Run Detail 包含:

24.4 SSE

GET /api/v1/workspaces/{workspaceId}/runs/{runId}/events
Accept: text/event-stream
Last-Event-ID: 120
id: 121
event: progress
data: {"runId":"...","succeeded":300,"failed":2,"inFlight":50}

25. API 通用约定

25.1 分页

首期使用 Cursor Pagination:

{
  "items": [],
  "page": {
    "nextCursor": "opaque",
    "hasMore": true
  }
}

Cursor 不暴露 SQL 结构,使用 HMAC 保护,并绑定名称、状态和时间筛选。公开资源页 limit1..100;服务端只读取 limit + 1 条判断 hasMore,篡改、跨资源或跨筛选复用 Cursor 会被拒绝。

25.1.1 资源版本、文件导入与关联重发

25.2 错误响应

{
  "type": "https://wepush.example/errors/run-state-conflict",
  "title": "Run state conflict",
  "status": 409,
  "code": "RUN_STATE_CONFLICT",
  "detail": "Run cannot be paused from SUCCEEDED",
  "traceId": "...",
  "errors": []
}

稳定错误码至少包含:

VALIDATION_FAILED
AUTHENTICATION_REQUIRED
ACCESS_DENIED
RESOURCE_NOT_FOUND
VERSION_CONFLICT
IDEMPOTENCY_CONFLICT
PROVIDER_NOT_AVAILABLE
PROVIDER_CONFIG_INVALID
RUN_STATE_CONFLICT
AGENT_NOT_AVAILABLE
LEASE_EXPIRED
ARTIFACT_NOT_READY
RATE_LIMITED
INTERNAL_ERROR

25.3 HTTP 状态

26. 数据库逻辑模型

所有业务表包含:

id               不透明 ID
workspace_id     工作空间边界
created_at       UTC 创建时间
updated_at       UTC 更新时间
version          乐观锁版本

即使首期只有一个 Workspace,也保留 workspace_id

26.1 身份和权限

关键字段
workspace name、status、settings_json
app_user username、display_name、status、password_hash 或 external_subject
role_binding user_id、workspace_id、role
api_token owner_id、token_hash、scopes、expires_at、last_used_at
agent_credential agent_id、credential_hash/cert_fingerprint、expires_at、revoked_at

Token 只保存不可逆 Hash,明文只在创建时返回一次。

26.2 Provider 和账号

关键字段
provider_catalog provider_id、implementation_version、spi_version、descriptor_json、status
account provider_id、name、config_json、secret_set_id、status、last_test_at
secret_record secret_set_id、secret_name、ciphertext、key_version、updated_at

account.config_json 不包含明文 Secret,只保存普通配置和 Secret 引用。

26.3 消息和受众

关键字段
message_template provider_id、name、current_revision、status
message_revision message_id、revision、schema_version、content_json、content_hash
audience name、source_type、source_config_json、status
audience_snapshot audience_id、revision、artifact_id、record_count、content_hash、state

Message Revision 和 Audience Snapshot 创建后不可修改。

26.4 Job、Schedule 和 Run

关键字段
job_definition name、account_id、message_id、audience_id、policies_json、enabled
schedule job_id、cron、timezone、misfire_policy、enabled、next_fire_at
run job_id、snapshot_id、state、state_reason、agent_id、计数、时间、version
run_snapshot run_id、provider_ref、account_config、message_content、policies、hash
run_command run_id、agent_id、sequence、type、payload_json、state、ack_at
run_event run_id、sequence、type、occurred_at、payload_json、severity

26.5 Agent 和 Lease

关键字段
agent name、status、version、protocol_version、platform_json、last_seen_at
agent_provider agent_id、provider_id、version、spi_version、status
agent_lease run_id、agent_id、epoch、token_hash、state、expires_at、acked_at

约束:同一 Run 同时最多一个 ACTIVE Lease。

26.6 Artifact、幂等和审计

关键字段
artifact type、backend、location、size、sha256、content_type、state、expires_at
idempotency_record scope、key_hash、request_hash、response_status、response_body、expires_at
audit_event actor_type、actor_id、action、resource_type、resource_id、result、details_json

27. 索引和保留策略

建议索引:

run(workspace_id, state, created_at)
run(job_id, created_at)
run_event(run_id, sequence) UNIQUE
schedule(enabled, next_fire_at)
agent(status, last_seen_at)
agent_lease(run_id, state) UNIQUE WHERE state = ACTIVE
agent_lease(agent_id, expires_at)
message_revision(message_id, revision) UNIQUE
audience_snapshot(audience_id, revision) UNIQUE
idempotency_record(scope, key_hash) UNIQUE

SQLite 不支持的条件索引能力通过事务校验和等价唯一字段实现。

默认保留基线:

28. SQLite 与 PostgreSQL 18 适配

28.1 统一语义

28.2 调度和 Lease Claim 差异

PostgreSQL 使用行锁和 SKIP LOCKED 领取候选 Run。SQLite 使用短事务和单写者模型完成领取,Standalone 限制单 Service 实例。

Server 中只有持有 PostgreSQL Session-level Advisory Lock 的 Service 实例运行 Schedule Scanner;锁使用专用连接持有,连接失效自动释放。Run Claim 在短事务中执行并同时写入递增 Epoch 和 Fencing Token,不能只依赖进程内锁。

LISTEN/NOTIFY 仅作为新 Run、新命令和新事件的低延迟唤醒提示;所有消费者都必须有数据库轮询和游标恢复路径,正确性不依赖通知必达。

28.3 连接管理

28.4 控制面高可用

详见 ADR-0006

29. Artifact Store

29.1 接口

public interface ArtifactStore {
    ArtifactUploadPlan beginUpload(ArtifactCreateCommand command);
    ArtifactMetadata completeUpload(ArtifactCompleteCommand command);
    ArtifactDownloadPlan authorizeDownload(ArtifactRef ref, Optional<ByteRange> range);
    ArtifactMetadata stat(ArtifactRef ref);
    void delete(ArtifactRef ref);
}

ArtifactUploadPlan 可以是本地受控 Stream,也可以是带到期时间的 S3 Presigned URL/Multipart Plan。Upload 先进入 UPLOADING,写入完成并校验 SHA-256 后进入 READY。失败或超时的上传由清理任务回收。

29.2 本地目录

data/artifacts/
├── audiences/<snapshot-id>/recipients.jsonl
├── runs/<run-id>/success.jsonl
├── runs/<run-id>/failed.jsonl
├── runs/<run-id>/unknown.jsonl
├── runs/<run-id>/unsent.jsonl
└── runs/<run-id>/logs.jsonl

路径由 Artifact Store 生成,不能直接使用用户输入的任务名称,避免路径穿越和非法字符问题。

29.3 Server 对象存储

29.4 下载

29.5 保留和删除

Service 清理任务根据 Workspace 策略把到期对象从 READY 标记为 DELETING,执行幂等删除后置为 DELETED。失败使用退避重试;对象不存在视为删除成功。默认保留期采用第 27 节基线,对象存储 Lifecycle 仅作为兜底。详见 ADR-0007

30. Secret 详细设计

30.1 默认实现和加密信封

默认 SecretStore 实现为 LocalEnvelopeSecretStore。每条 Secret 生成独立 256-bit 随机 DEK,使用 AES-256-GCM 和随机 Nonce 加密;AAD 至少包含 workspaceId + secretId + secretType + recordVersion,从而阻止跨记录替换密文。

plaintext secret
    ↓ 使用随机 DEK 加密
ciphertext + nonce + algorithm
    ↓ 使用 Master Key 加密 DEK
encrypted DEK + key version

数据库保存 Ciphertext、Nonce、Encrypted DEK、算法和 Key Version。Master Key 不存入业务数据库。

30.2 API 行为

创建或更新账号时:

{
  "token": {"operation": "REPLACE", "value": "secret"},
  "password": {"operation": "KEEP"}
}

查询时只返回:

{
  "token": {"configured": true, "updatedAt": "..."}
}

不提供读取 Secret 明文的普通 API。

30.3 Agent Secret Envelope

当前实现使用 Agent 进程启动时生成的 X25519 密钥对,Hello 只发布 X.509 编码公钥;Service 为每个 Envelope 生成一次性 X25519 密钥,使用 HKDF-SHA-256 派生 AES-256-GCM 密钥。冻结 Account/Message 中形如 {namespace,name,version} 的标准 SecretRef 被递归去重扫描,未引用的 Secret 不会进入 Envelope。Agent 在 ACK 前完成认证解密,运行完成/失败后清零 Secret Material。Envelope 上限为 512 KiB,单 Secret 上限为 64 KiB,并且不替代 TLS、Enrollment 和 Agent 身份认证。

31. Scheduler 详细设计

31.1 Schedule 字段

cron
timezone
enabled
misfirePolicy: SKIP | FIRE_ONCE | CATCH_UP_LIMITED
catchUpLimit
startAt
endAt
lastFireAt
nextFireAt

31.2 触发幂等

每个计划触发使用以下幂等键:

schedule:<scheduleId>:<scheduledInstant>

即使调度线程重启或重复扫描,也只创建一个 Run。

31.3 扫描

32. Remote 与 Embedded Java SDK 详细设计

32.1 包结构

sdk-java
├── generated/              OpenAPI 生成代码
├── WePushClient.java       统一入口
├── WorkspaceClient.java    Workspace 范围入口
├── AccountsClient.java
├── MessagesClient.java
├── AudiencesClient.java
├── JobsClient.java
├── RunsClient.java
├── AgentsClient.java
├── EventSubscription.java
└── WePushException.java

32.2 使用方式

try (WePushClient client = WePushClient.builder()
        .endpoint(URI.create("https://wepush.example.com"))
        .token(() -> tokenProvider.currentToken())
        .connectTimeout(Duration.ofSeconds(5))
        .requestTimeout(Duration.ofSeconds(30))
        .build()) {

    WorkspaceClient workspace = client.workspace(workspaceId);
    Run run = workspace.runs().start(jobId,
            StartRunRequest.builder().dryRun(false).build(),
            "manual-20260822-001");

    try (EventSubscription events = workspace.runs().events(run.id())) {
        events.forEach(System.out::println);
    }
}

32.3 SDK 重试

32.4 Embedded SDK

try (WePushEngine engine = WePushEngine.builder()
        .provider(new HttpProviderFactory())
        .secretResolver(secretResolver)
        .resultSink(resultSink)
        .eventSink(eventSink)
        .build()) {
    RunHandle handle = engine.start(spec, recipients);
    handle.submit(new RunCommand.ChangeConcurrency(commandId, 50));
    RunSummary summary = handle.completion().toCompletableFuture().join();
}

Embedded SDK 不创建数据库、不读取 Next Service 配置目录,也不自动发现未显式允许的第三方 Provider。共享 Sink 与 Engine 同生命周期且必须支持并发 Run;使用 resultSinkFactoryeventSinkFactoryartifactSinkFactory 创建的 Sink 归单个 Run 所有,并在完成或启动拒绝后关闭。

33. WebUI 详细设计

33.1 技术与工程基线

33.1.1 视觉语言

WebUI 与 Desktop Renderer 采用接近 Codex 客户端的克制型工作台风格,但不复制其品牌标识或专有资产:

前端 Workspace 结构固定为:

ui/
├── apps/
│   ├── web/
│   └── desktop/
└── packages/
    ├── api-client/
    ├── schema-renderer/
    ├── ui/
    ├── features/
    └── design-tokens/

33.2 路由

/login
/overview
/providers
/accounts
/accounts/:id
/messages
/messages/:id
/audiences
/audiences/:id
/jobs
/jobs/:id
/schedules
/runs
/runs/:id
/agents
/agents/:id
/docs
/settings

33.3 前端分层

app/               路由、认证、全局错误处理
api/               生成客户端和 Facade
features/          按业务功能组织页面和用例
schema-renderer/   JSON Schema 与 UI Schema 表单
components/        通用展示组件
stores/            登录身份、连接状态和少量全局状态
events/            SSE 连接、游标和重连

Server State 以 API 缓存为主,不复制成长期全局可变 Store。

33.4 Schema 表单

Schema 默认控件
string 文本框
string + format=password 或 Secret 密钥输入框
string + enum 下拉框
boolean 开关
integer/number 数字输入
array 可增删列表或表格
object 分组表单
x-wepush-code-language 代码编辑器
Recipient Variable 变量选择和插入器

表单保存时发送原始 Schema 数据,不把 UI 布局字段混入 Provider 配置。

33.5 Run 页面

Run Detail 页面展示:

SSE 断开时显示连接状态并自动重连;不能将“监控连接断开”误显示为“Run 失败”。

33.6 API 文档页

34. Desktop UI 详细设计边界

Desktop 固定采用 Electron 43.x。apps/desktop 只实现 Main/Preload、窗口、托盘、通知、自动启动和更新能力,Renderer 复用 packages/features 中的 WebUI 页面。

34.1 启动流程

flowchart TD
    Start[Desktop 启动] --> Config[读取连接配置]
    Config --> Mode{本地还是远程}
    Mode -- 远程 --> Login[连接远程 Service 并登录]
    Mode -- 本地 --> Health[探测本机 Service Health]
    Health -->|在线| Bootstrap[获取本地短期会话]
    Health -->|离线| StartService[启动或提示安装 Service]
    StartService --> Health
    Bootstrap --> UI[加载共享 UI]
    Login --> UI

34.2 本地认证

34.3 进程边界

35. 认证、授权和审计

35.1 角色

角色 权限摘要
ADMIN 用户、Agent、Secret、系统配置及全部业务操作
OPERATOR 账号、消息、受众、Job、Run 和调度操作
VIEWER 查询配置摘要、运行状态、日志和文档

读取账号不等于读取 Secret。即使 ADMIN 也不能通过普通 API 取回明文 Secret。

ADMINOPERATORVIEWER 角色绑定在 Workspace 范围内。另设显式 SYSTEM_ADMIN,只用于 Workspace 生命周期、全局 Agent 视图和跨 Workspace 应急治理;它可以越过 Workspace 角色,但每次操作仍携带目标 workspaceId 并进入审计。普通 Workspace ADMIN 不能签发、查看或吊销其他 Workspace 的 API Token/Enrollment Token。Standalone 的 Default Workspace 在普通 UI 中隐藏,但 API 和 Repository 仍使用其真实 ID。

35.2 权限检查

35.3 审计事件

必须审计:

审计 Details 只保存字段名和变更摘要,不保存 Secret 原值。

36. 输入与网络安全

37. 可观测性详细设计

37.1 日志字段

timestamp
level
service
instanceId
traceId
workspaceId
runId
agentId
leaseId
providerId
event
message

日志必须通过统一 Redactor 处理 Header、URL Query、JSON 字段和异常信息中的 Secret。

37.2 指标

wepush_runs_total{state,provider}
wepush_run_items_total{result,provider}
wepush_run_inflight{run,provider}
wepush_provider_requests_total{provider,result,error_category}
wepush_provider_request_duration_seconds{provider}
wepush_provider_retries_total{provider,category}
wepush_agent_heartbeat_age_seconds{agent}
wepush_agent_active_runs{agent}
wepush_lease_expired_total
wepush_event_outbox_size{agent}
wepush_artifact_upload_bytes_total{type}

Run ID、Agent ID 等高基数字段不作为默认 Metrics Label,应通过日志和 Trace 查询。

37.3 Health

38. 配置设计

示例:

wepush:
  mode: standalone
  server:
    bind-address: 127.0.0.1
    port: 18990
    public-base-url: http://127.0.0.1:18990
  database:
    type: sqlite
    url: jdbc:sqlite:${WEPUSH_DATA_DIR}/wepush-next.db
  artifacts:
    type: local
    directory: ${WEPUSH_DATA_DIR}/artifacts
  secrets:
    type: local-envelope
    master-key-file: ${WEPUSH_CONFIG_DIR}/master.key
  agent:
    embedded:
      enabled: true
      max-runs: 2
      max-concurrency: 200
  security:
    remote-access: false
  events:
    progress-interval: PT1S
    batch-size: 100

规则:

39. 默认端口和目录

以下为初始发行基线,安装包落地时允许用发行 ADR 调整未公开的具体值:

平台 配置 数据 日志
Linux /etc/wepush-next/ /var/lib/wepush-next/ /var/log/wepush-next/
Windows %ProgramData%\WePush Next\service.envagent.env %ProgramData%\WePush Next\serviceagent %ProgramData%\WePush Next\logs
macOS Service /Library/Preferences/wepush-next/ /Library/WePushNext/data/ /Library/Logs/WePushNext/
开发模式 next/.local/config next/.local/data next/.local/logs

默认 HTTP 端口建议 18990,不得与 Classic 使用的资源发生冲突。

开发模式目录必须加入 .gitignore

40. Service 安装和生命周期

40.1 Linux

40.2 Windows

40.3 macOS

40.4 升级

  1. 拒绝或等待新 Run。
  2. Service 进入 Draining。
  3. 生成带 Manifest 和逐文件 SHA-256 的数据库、Master Key、Artifact、Agent Identity、Journal、Outbox、插件与配置一致性备份,并调用 Restore 校验。
  4. 停止旧进程。
  5. 替换应用并执行迁移。
  6. 启动并完成 Readiness、Flyway 当前版本和本地无网络 Provider Dry Run 检查。
  7. 失败时原子切回旧版本、恢复升级前备份、验证旧版本并保留可诊断的恢复目录。

数据库迁移一旦包含不可逆变化,必须明确最低可回滚版本。

41. 测试详细设计

41.1 Core 单元测试

使用 Fake Clock、Fake Provider、In-Memory Recipient Source 和 Result Sink,覆盖:

测试不能依赖真实时间 Sleep,应通过可控 Clock 和调度器推进。

41.2 Provider 契约测试

所有 ProviderFactory 必须通过同一套测试:

41.3 Agent 故障测试

41.4 Service 集成测试

41.5 E2E

首条流水线:

启动 Standalone Service
→ Web/API 创建 HTTP Account
→ 创建 Message 和 Audience Snapshot
→ 创建 Job
→ 启动 Run
→ Embedded Agent 领取
→ Mock HTTP Server 接收请求
→ SSE 观察进度
→ 下载结果 Artifact
→ 重启 Service 验证历史

42. 性能和容量验证

首期不承诺所有 Provider 的统一吞吐,但必须建立可重复基线:

性能报告记录硬件、JVM、Provider Mock 延迟、并发、吞吐、P50/P95/P99 延迟、GC 和最大内存。

43. CI/CD 详细流程

validate
├── 格式和静态检查
├── OpenAPI 校验
├── JSON Schema 校验
└── 依赖与许可证检查

test
├── 单元测试
├── 架构测试
├── Provider 契约测试
└── Service 集成测试

package
├── Java 制品
├── WebUI
├── Service/Agent 应用镜像
└── Java SDK

platform-package
├── Linux
├── Windows
└── macOS

smoke
├── 安装
├── 启动和 Health
├── HTTP Provider 最小 Run
└── 卸载/升级验证

Next CI 使用 next/** 路径过滤,Classic 和 Next 互不依赖对方构建产物。

44. 首个纵向实现拆分

44.1 Iteration 1:纯 Core

44.2 Iteration 2:HTTP Provider

44.3 Iteration 3:Service 单机闭环

44.4 Iteration 4:最小 WebUI

44.5 Iteration 5:远程 Agent

当前进度:Iteration 5 基线已完成。Hello/Heartbeat、持久 Lease/Fence、Snapshot/Audience 下载校验、加密 Secret Envelope、远端 Core 执行、磁盘 Event/Completion Outbox、EventBatch/EventAck、RunCommand/CommandAck、Agent Artifact 上传/完整性提交、RunCompleted、重复 Offer/Event 去重、进程重启恢复、一次性 Enrollment、Credential/证书轮换以及 TLS/mTLS 已形成纵向闭环;Service→Agent 消息使用数据库 outbox 支持 Agent 在任意 Service 实例重连。

44.6 Iteration 6:产品化

当前进度:Iteration 9(1.1.0)已完成。除 1.0.0 稳定基线外,CMPP/SMGP/SGIP/SMPP 独立签名插件、Workspace 资源治理、脱敏诊断、手动版本检查、跨 Run 认证熔断、PostgreSQL 三类控制面通知、Agent Presigned Multipart 和 WebUI 主题/可访问性均已落地;具体版本边界见产品路线图1.1.0 Release Notes

44.7 Iteration 7:日常使用闭环

当前进度:Iteration 7 已在 0.1.0-alpha.3 完成。OpenAPI、Remote Java SDK、TypeScript Client、WebUI、SQLite V12/V13 和纵向集成测试同步交付;启动恢复也使用有界分页扫描。

44.8 Iteration 8:真实消息渠道

当前进度:Iteration 8 已在 0.1.0-alpha.4 完成。标准渠道集中在独立 provider-standard 模块,生产端点受限于厂商官方域名;微信系 Token 只在 Session 内缓存并在明确失效后刷新一次。协议、重试、限流、幂等和最小配置见内置 Provider 指南

45. 完成定义

一个模块或功能只有同时满足以下条件才视为完成:

46. 已决 ADR 与后续事项

本轮技术基线已由 ADR-0002ADR-0008 确认,包括 Web/Desktop/Service、Secret、Agent 协议、Provider 插件、PostgreSQL HA、Artifact 和 Workspace 多租户。

以下事项可以在对应能力进入迭代前继续形成独立 ADR,但不阻塞当前架构:

  1. OpenAPI Generator、数据库迁移工具、路由和 Server State 库的具体版本。
  2. Desktop 本地 Bootstrap Channel、平台安装和未签名发行完整性验证。
  3. 非受信 Provider 的独立进程 Runner;只有明确服务于用户自建安全场景时才进入评审。

公共 SaaS、计费订阅、外部 Vault/云 KMS/Secret Manager、恶意公共租户物理隔离和跨地域控制面不是“后续非基线事项”,而是长期产品非目标。任何后续 ADR 不得把它们重新引入。允许评审的扩展不得反向污染 Core API,也不得削弱 Workspace、Lease Fencing、Secret 和 Artifact 的既定安全边界。

47. 文档演进规则