WePush Next 详细设计
- 文档状态:已接受基线,随实现演进
- 文档版本:0.3
- 日期:2026-08-27
- 适用范围:
next/ - 上位文档:WePush Next 架构与概要设计
- 关联决策:见 ADR 索引
- 产品范围:产品目标、边界与路线图
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-0002 至 ADR-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 组合原则
agent-runtime只依赖 Provider SPI,不编译依赖某个具体 Provider。agent-app和service-app是具体 Provider 的组合入口。embedded-java只提供 Engine 装配能力,业务应用自行添加 Provider 依赖。- Service 的公开 DTO、Agent 协议 DTO、Core 对象和数据库记录是四类不同模型,通过 Mapper 显式转换。
- 禁止为了减少转换而让数据库记录穿透到 Controller、Agent 或 Core。
5. 标识、时间和版本
5.1 标识
- 对外资源 ID 使用不透明字符串,首期建议 UUIDv7。
- 数据库主键与 API ID 使用同一逻辑 ID,避免暴露自增序号。
- Recipient Item ID 由
runId + audienceSnapshotId + sequence稳定生成。 - Agent、Lease、Event 和 Artifact 都使用独立 ID。
5.2 时间
- Java 内部使用
Instant表示绝对时间。 - API 使用 UTC ISO-8601,例如
2026-08-22T08:30:00Z。 - Schedule 单独保存 IANA 时区,例如
Asia/Shanghai。 - 数据库统一保存 UTC;仅在 API 和 UI 边界转换时区。
- 所有超时使用
Duration,禁止使用无单位的裸整数。
5.3 版本
- Provider 具有实现版本和 SPI 主版本。
- Schema 具有独立
schemaVersion。 - Agent 协议通过 URL 和消息字段双重版本化。
- Run Snapshot 保存 Provider、Schema、模板和 Audience Snapshot 版本。
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();
}
}
默认 mediaType 为 application/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();
}
SecretValue 的 toString() 必须返回固定掩码,关闭时尽力清理内部缓冲区。
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
) {}
- 并发范围必须有最小值和最大值。
- 动态调整只能落在允许范围内。
- Retry 的总耗时不得超过 Run 或 Item Timeout。
- Result Policy 决定是否保存响应摘要或完整响应 Artifact。
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 主要处理 RUNNING、PAUSED、CANCELLING 和执行终态。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 启动校验顺序
- Run ID、Provider 和端口非空。
- 执行策略内部一致。
- Provider 存在且 SPI 兼容。
- Account 和 Message Schema 版本受支持。
- Provider 执行配置校验通过。
- Recipient Source 可打开。
- Result 和 Artifact Sink 可写。
- Provider Session 创建成功。
校验失败不进入 RUNNING,返回稳定错误分类。
8.3 资源关闭
资源按与创建相反的顺序关闭。无论正常结束、取消还是异常,都必须尝试:
- 停止读取新 Recipient。
- 等待或终止在途发送。
- Flush Result Sink。
- Flush Event Sink。
- 关闭 Provider Session。
- 关闭 Recipient Source。
- 完成 RunSummary。
单个资源关闭失败不能阻止其他资源关闭,但会进入 Run 的 suppressedErrors。
9. 并发模型
9.1 调度结构
- 每个 Run 有一个协调线程或协调任务。
- Recipient 按批次读取,不预加载完整 Audience。
- 每个 Item 使用虚拟线程执行 I/O 发送。
ConcurrencyGate使用许可数限制真实在途请求。RateLimiter在获取并发许可之后、调用 Provider 之前生效。- Provider 可以声明比用户配置更小的并发上限。
最终有效并发:
min(
用户目标并发,
Job 最大并发,
Provider 最大并发,
Agent 可用容量,
Service 下发容量
)
9.2 背压
- Recipient Source 只有在执行队列低于高水位时才读取下一批。
- Result Sink 阻塞或积压时,Engine 减缓新的 Item 派发。
- Event Sink 使用有界缓冲区;进度事件可以合并,终态和错误事件不得丢弃。
- 队列容量全部可配置且有安全默认值。
9.3 动态并发
ChangeConcurrency 修改目标许可数:
- 增大时立即释放新许可。
- 减小时不强制中断在途请求,待请求结束后自然收缩。
- 目标值越界时拒绝命令并返回允许范围。
- 每次变化产生
CONCURRENCY_CHANGED事件。
9.4 Provider 线程安全
Provider Descriptor 和只读 Schema 可以跨 Run 共享。Provider Session 默认是 Run 级实例,必须明确声明:
THREAD_SAFE:同一 Session 可被并发调用。SERIALIZED:Engine 对同一 Session 串行调用。SESSION_PER_WORKER:Provider 为工作单元创建子 Session。
未声明时按 SERIALIZED 处理。
10. 暂停、取消和停止
10.1 暂停
- 暂停停止派发新 Item。
- 已经调用 Provider 的请求继续完成。
- Recipient Source 保持打开,但不得无限占用不可续期资源。
- 超过
maxPauseDuration后由策略决定自动恢复或取消。
10.2 取消
- 状态先进入
CANCELLING。 - 不再读取或派发新 Item。
- 可取消的在途请求收到 Cancellation Token。
- 不支持取消的 Provider 请求等待 Item Timeout。
- 尚未派发的 Recipient 记录为
UNSENT,不是FAILED。 - 所有结果 Flush 后进入
CANCELLED。
10.3 Agent 关闭
Agent 收到操作系统停止信号后:
- 停止领取新 Lease。
- 向 Service 上报
DRAINING。 - 在 Grace Period 内等待 Run 完成。
- 超时后对 Run 发起取消并 Flush 本地事件。
- 上报最终心跳后退出。
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 必须同时给出:
- 是否成功。
- 稳定 Provider 错误码。
- ErrorCategory。
- 是否建议重试。
- 可选
retryAfter。 - 已脱敏的诊断摘要。
- 外部请求 ID。
11.2 默认重试规则
RATE_LIMITED:按 ProviderretryAfter,否则指数退避。TEMPORARY_REMOTE、NETWORK:允许重试。TIMEOUT:仅当 Provider 调用语义允许时重试,否则转UNKNOWN。AUTHENTICATION、AUTHORIZATION:不逐条重试,可触发 Run 级快速失败。INVALID_REQUEST、RECIPIENT_INVALID、PERMANENT_REMOTE:不重试。INTERNAL、UNKNOWN:由显式策略决定,默认不自动重试。
11.3 退避
delay = min(maxDelay, initialDelay * multiplier^(attempt - 1)) + jitter
重试必须同时受最大次数、Item Deadline 和 Run Deadline 限制。
11.4 熔断和快速失败
同一 Account 在短时间内连续发生认证失败时,Engine 停止继续发送并将 Run 标记为 FAILED 或 PARTIAL。熔断统计属于 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();
}
- 默认批量大小建议 100~1000,可配置。
- 同一
itemId + attempt重复写入必须幂等。 - CSV 适合用户下载,内部恢复建议使用 JSON Lines 或结构化二进制格式。
- 响应体默认只保存摘要,完整内容由 Result Policy 显式开启。
12.4 ProgressAggregator
- 内部计数实时更新。
- 对 Agent 的进度事件按时间间隔或数量阈值合并。
- 默认每 500ms 至 1s 产生一次进度事件。
- 终态前强制发送最后一条完整计数。
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 不得通过 runId、accountId 或 messageId 查询 Service 数据库。
13.4 ProviderDescriptor
Descriptor 至少包含:
providerId- 展示名称、类别、说明和图标引用
- 实现版本、SPI 版本
- Account、Message、Recipient 和 UI Schema
- 能力:Preview、Dry Run、Idempotency、Response Body 等
- 并发上限和默认超时建议
- Thread Safety Mode
- 支持的平台和架构
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 校验层次
- UI 本地 Schema 校验,用于即时提示。
- Service Schema 校验,作为保存前权威校验。
- Provider 语义校验,例如签名算法、字段组合和渠道限制。
- 可选连接测试,不作为普通保存的隐式步骤。
UI 校验结果不能替代 Service 校验。
15. Provider 发现、隔离和更新
15.1 发现
- 外部 Provider 插件使用 PF4J 3.15.x;Provider SPI 作为 Extension Point,Core Engine 不依赖 PF4J。
- Agent App 的
ProviderCatalogAdapter把 PF4J Extension 转换为 Core 的ProviderFactory。 - 内置 Provider 作为系统 Extension 进入相同 Catalog,避免执行路径出现两套语义。
- Agent 启动时扫描
plugins/<pluginId>/<version>/,校验plugin.json、SPI 兼容范围、文件 SHA-256 和 Ed25519 签名,再装载被激活的版本。 - 重复 Provider ID、缺失 Schema、签名失败或 SPI 不兼容时,插件进入
REJECTED,原因写入健康状态和审计日志。
插件包固定包含实现 JAR、Descriptor、JSON Schema、UI Schema、图标、许可证和签名清单。正式发行默认只接受受信发布者签名的包;未签名包仅能在显式 Developer Mode 中装载。
15.2 隔离
- 每个插件使用独立 PF4J ClassLoader;JDK、Provider SPI、Core API 和统一日志 API 由 Parent ClassLoader 提供。
- 插件私有的厂商 SDK 和三方依赖由插件 ClassLoader 加载,插件不得打包 SPI/Core API 的重复副本。
- Provider 不能直接访问 Service Repository、Spring Bean 或其他 Workspace 数据。
- ClassLoader 只解决依赖冲突,不构成恶意代码沙箱。非受信插件必须通过未来的独立进程 Provider Runner 执行。
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/<version></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 <sha256>
<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 <sha256>
<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">&</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">&</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">&</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">'<backup>.zip'</span><span class="w">
</span><span class="o">&</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">'<backup>.zip'</span><span class="w"> </span><span class="nt">-ExpectedSha256</span><span class="w"> </span><span class="s1">'<sha256>'</span><span class="w">
</span><span class="o">&</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"><</span><span class="nx">sha256</span><span class="err">></span><span class="w">
</span><span class="o">&</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=<one-time-token>
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-<protocol>-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 <pluginId>.zip
<span class="nb">sudo</span> <span class="nt">-u</span> wepush-agent /opt/wepush-next/current/plugins/rollback.sh <pluginId>.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"><pluginId>.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 安全限制
- 默认只允许 HTTP/HTTPS,生产建议仅 HTTPS。
- URL 由 Base URL 和受控 Path 组合,默认不允许 Recipient 控制 Host。
- DNS 解析后执行私网、环回、链路本地和云元数据地址策略检查。
- 禁止自动将 Secret 写入日志或响应 Artifact。
- 响应体设最大字节数,超出部分截断并标记。
- 重定向默认关闭;开启时每次跳转重新执行地址安全检查。
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
- Provider 装载失败但核心仍可运行时进入
DEGRADED。 - 无有效身份时进入
UNREGISTERED,只允许 Enrollment。 - Service 不可达时进入
OFFLINE,不得领取新任务。
17.2 本地 Agent 标识
本地保存:
agentId- Agent credential 或证书引用
- Service endpoint
- 最后确认的 Event Sequence
- 未完成 Lease Journal
- Provider 清单摘要
文件权限必须限制为 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 后续认证
- 首期可使用独立 Agent Token,通过 TLS 传输。
- 支持证书后优先使用 mTLS。
- Agent Token 与用户 API Token 使用不同签发域、权限和过期策略。
- Agent Credential 支持轮换和吊销。
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,其中包含:
leaseId、runId、epoch、fencingToken、expiresAt。- Execution Snapshot 元数据和短期下载 URL。
- Audience Artifact 元数据和短期下载 URL。
- 仅限本次 Run 的 Secret Envelope。
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 和创建时间。
- Sequence 在单 Agent 范围内单调递增。
- Agent 按顺序处理并持久化最后确认 Sequence。
- 命令按
commandId幂等。 - 命令结果通过
CommandAck和 Run Event 同时体现。
20.2 事件批次
Agent 使用 EventBatch 帧发送 firstSequence、lastSequence 和事件集合。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,至少记录:
- Lease ID、Run ID、Epoch。
- Execution Snapshot 校验值。
- 已确认事件 Sequence。
- 本地待上传 Artifact。
- Agent 退出原因和时间。
Agent 重启后:
- 读取 Journal。
- 向 Service 查询 Lease 当前所有权。
- 仍有效且允许恢复时继续上报或恢复执行。
- Lease 已失效时停止旧执行,只允许上传诊断信息。
- 无法确认 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
事务内完成:
- 校验 Idempotency Key。
- 加载 Job 并检查权限。
- 加载 Account、Message Revision 和 Audience Snapshot。
- 校验 Provider 存在且配置版本兼容。
- 创建不可变 Run Snapshot。
- 创建
PENDINGRun。 - 写入
RUN_CREATED事件和审计记录。 - 保存 Idempotency 结果。
事务提交后发出可用性提示,由已连接的 Service 实例尝试生成 LeaseOffer。提示丢失不影响正确性,数据库扫描仍会发现待调度 Run。
22.3 典型用例:下发命令
- 校验用户和 Run 权限。
- 根据状态机判断命令是否合法。
- 生成 Command ID 和 Agent Sequence。
- 持久化命令。
- 返回
ACCEPTED,不等待 Agent 完成命令。 - Agent Ack 后更新命令状态并产生 Run Event。
23. Service 事务边界
23.1 原则
- 一个应用服务方法对应一个明确事务边界。
- 不在数据库事务内调用外部 Provider、上传大 Artifact 或等待 Agent。
- Repository 方法不自行提交事务。
- 事务提交后事件通过本地通知或 Outbox 触发后续处理。
- 数据库连接和 Session 按事务获取,不使用全局共享 Session。
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 包含:
- 状态和状态原因。
- Job、Provider 和 Snapshot 版本。
- Agent 与 Lease 摘要。
- 总量及各结果计数。
- 当前并发、速率和重试数。
- 开始、结束和持续时间。
- Artifact 链接。
- 允许的下一步命令列表。
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}
- SSE 心跳注释间隔默认 15 秒。
- 连接断开后客户端指数退避重连。
- Event ID 只在单 Run 内有序。
- 请求过旧且事件已被清理时返回明确的游标过期错误,客户端改用 Run Detail 恢复快照。
25. API 通用约定
25.1 分页
首期使用 Cursor Pagination:
{
"items": [],
"page": {
"nextCursor": "opaque",
"hasMore": true
}
}
Cursor 不暴露 SQL 结构,使用 HMAC 保护,并绑定名称、状态和时间筛选。公开资源页 limit 为 1..100;服务端只读取 limit + 1 条判断 hasMore,篡改、跨资源或跨筛选复用 Cursor 会被拒绝。
25.1.1 资源版本、文件导入与关联重发
- Account、Message、Audience、Job 和 Schedule 使用乐观版本更新;Message 内容变更创建新 Revision,Audience Recipient 变更创建新 Snapshot。
- CSV/TXT 使用流式 Reader 分批写入 staging 表,数据库保留首个
itemId并标记重复行;预览最多返回 20 条接受/错误记录,完整错误通过流式 CSV 下载。 - Import Commit 使用
INSERT ... SELECT创建 Snapshot 和 Recipient,不把完整文件载入 JVM;更新已有 Audience 时再次校验资源版本。 - FAILED、UNKNOWN、UNSENT 重发先返回匹配 Item 数量和短期确认令牌,再创建带
sourceRunId的新 Run;run_retry_item限定执行集合,新 Run 复制来源 Run Snapshot,不读取当前可变资源。
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 状态
200查询或同步操作成功。201资源创建完成。202异步命令已接受。204删除或无响应体操作成功。400请求格式或字段错误。401未认证。403无权限。404资源不存在或不可见。409状态、版本或幂等冲突。422Provider 语义校验失败。429调用频率超限。503暂无可用能力或服务未就绪。
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 不支持的条件索引能力通过事务校验和等价唯一字段实现。
默认保留基线:
- Run 摘要作为业务记录保存,具体归档期由 Workspace 策略配置。
- 高频 Run Event 和运行日志保存 30 天后压缩或删除。
- Audit Event 保存时间长于普通日志。
- Audience Snapshot 在被引用期间保留,解除引用后 30 天;运行结果 90 天;完整 Provider 响应 7 天;临时导出 24 小时。
- 删除前验证没有有效引用;
PINNED或LEGAL_HOLD跳过普通 TTL。 - Secret 不进入备份外的普通导出。
28. SQLite 与 PostgreSQL 18 适配
28.1 统一语义
- Repository 接口和领域行为保持一致。
- JSON 在 PostgreSQL 使用 JSONB,在 SQLite 使用规范化 JSON TEXT。
- 枚举使用可读字符串,不使用数据库特有 Enum。
- 时间以 UTC 文本或数据库支持的时间类型保存,由适配器统一转换。
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 连接管理
- 使用连接池或 DataSource,每个事务独立获取连接。
- SQLite 池规模保持较小并启用 WAL、busy timeout。
- PostgreSQL 根据 Service 实例和负载配置池大小。
- 禁止全局共享 Connection、Session 或 Mapper 实例承载事务状态。
28.4 控制面高可用
- 正式 Server HA 至少运行两个无状态 Service 实例,并置于支持 HTTP/2 与 gRPC 的负载均衡器后。
- 业务状态、Agent Sequence、Lease、Command、Event 和幂等记录都持久化在 PostgreSQL;Service 本地缓存不是事实源。
- PostgreSQL 自身复制、备份和故障转移由部署者选择并负责运维,WePush 安装包不自行组建数据库集群。
- Schema Migration 以集群级单例执行,采用 Expand → Migrate → Contract,滚动升级期间保持相邻版本兼容。
- 一个 Service 退出时 REST/SSE 客户端重连,Agent gRPC 自动重连;系统不要求连接粘滞。
详见 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 对象存储
- Server 默认实现
S3ArtifactStore,只依赖 Put/Get/Head/Delete、Range、Presigned URL 和 Multipart Upload 等受控 S3-compatible 子集。 - 对象键格式为
workspaces/{workspaceId}/{artifactType}/{yyyy}/{mm}/{artifactId},不含用户文件名和 Secret。 - Agent 不持有长期对象存储 Credential;Service 签发最小权限、短期有效的上传或下载 URL。
- Service 托管流式写入达到 100 MiB 时默认使用 Multipart。Agent 对不超过 1 GiB 的对象继续使用绑定长度和 SHA-256 的单次 Presigned Put,超过该阈值时使用 Presigned Multipart Plan,最大单 Artifact 为 5 TiB。Service 每批最多签发 100 个 Part URL,并显式约束 S3 的 10,000 Part 上限;Agent 可独立重试 Part,提交 ETag 列表后由 Service Complete 并用 Head 元数据核对总大小与 SHA-256。未完成会话由 V17 事实表驱动主动 Abort,对象存储 Lifecycle 在 24 小时后终止孤儿 Multipart 作为兜底。
- Server 模式启用对象存储原生 AES256 或由部署方自行保证等价的存储端加密;WePush 不对接或管理云 KMS Key。
- 数据库元数据是生命周期事实源,对象存储 List 只能用于对账和孤儿清理。
29.4 下载
- Service 校验权限后返回流或短期签名 URL。
- 支持 Range 请求。
- 下载文件名通过响应头提供,存储路径不暴露给客户端。
- 敏感 Artifact 可要求再次认证或更高权限。
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 不存入业务数据库。
- Standalone 首启可创建主密钥文件,并限制为当前用户可读:Unix 权限
0600,Windows 使用专用 Service Account ACL。 - Server 从只读挂载文件读取 Master Key;允许环境变量显式注入但不作为推荐生产方式。
- Server HA 的所有 Service 实例挂载相同的 Active/Retained Key Ring;轮换由单一管理操作生成新版本,再通过受控部署传播到所有实例。
- Server 发现密钥缺失、权限过宽或 Key Version 不可解析时 Fail Closed,不静默生成新密钥。
- Master Key 轮换默认只重新包裹各 Secret 的 DEK,并保留旧 Key Version 直到验证和回滚窗口结束。
- Electron
safeStorage只可用于 Desktop 自身的本地 UI Credential,不用于 Service Secret。Service 不规划 Vault、云 KMS、云 Secret Manager 或操作系统凭据存储 Adapter。
30.2 API 行为
创建或更新账号时:
{
"token": {"operation": "REPLACE", "value": "secret"},
"password": {"operation": "KEEP"}
}
查询时只返回:
{
"token": {"configured": true, "updatedAt": "..."}
}
不提供读取 Secret 明文的普通 API。
30.3 Agent Secret Envelope
- Service 只下发 Run 所需的最小 Secret 集。
- Envelope 绑定 Agent ID、Run ID、Lease ID、Epoch 和过期时间。
- 使用 Agent 公钥或受 TLS 保护的短期会话密钥加密。
- Agent 解密后只在内存持有,不写 Local Journal。
- Lease 失效后 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 扫描
- 周期性查询
enabled = true AND next_fire_at <= now。 - 在短事务内锁定 Schedule、创建 Run、更新下次时间。
- 不在调度事务中物化大型 Audience;Run 创建后异步准备 Snapshot。
- Cron 解析和 Next Fire 计算使用同一库和版本,保存测试样例防止升级行为变化。
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 重试
- GET 和明确幂等请求可对网络错误自动重试。
- POST 创建操作只有携带 Idempotency Key 时才能自动重试。
- SDK 不对 Service 返回的业务失败盲目重试。
429和503遵循Retry-After,并有最大等待限制。
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;使用 resultSinkFactory、eventSinkFactory 或 artifactSinkFactory 创建的 Sink 归单个 Run 所有,并在完成或启动拒绝后关闭。
33. WebUI 详细设计
33.1 技术与工程基线
- TypeScript 6.0.x,开启
strict、noUncheckedIndexedAccess和未使用代码检查。 - Vite 8.1.x、React 19.2.x、Node.js 24 LTS;pnpm 通过 Corepack 固定精确版本并使用单一 Lockfile。
- Tailwind CSS 提供设计 Token 和布局原语;shadcn/ui 仅按需引入复杂可访问组件,复制后的源码视为项目代码并纳入测试。
- WebUI 是纯 SPA,由 Service 提供静态资源或独立 CDN 部署;生产环境不需要 Node.js Server,也不使用 React Server Components。
33.1.1 视觉语言
WebUI 与 Desktop Renderer 采用接近 Codex 客户端的克制型工作台风格,但不复制其品牌标识或专有资产:
- 使用暖灰应用背景、浅灰紧凑侧栏、白色内容面和低对比细边框组织层级。
- 左侧导航保持稳定,顶部区域只保留页面标题、搜索/命令入口和连接状态。
- 字号、圆角和阴影偏小,依靠留白、对齐和字重形成视觉层级,避免装饰性渐变和大面积高饱和色。
- 默认以单色交互为主;绿色、黄色、红色和蓝色只表达成功、警告、失败和信息状态。
- 总览优先显示真实 Service、Provider、Agent 和 Run 数据;没有数据时使用明确空状态,不填充伪造趋势或示例成功率。
- WebUI 与 Desktop 必须复用
design-tokens和共享 UI primitives,平台外壳不得另建一套视觉变量。 - 保持键盘操作、可见焦点、语义标签、颜色对比和窄屏退化能力;后续暗色主题仍由同一语义 Token 映射实现。
前端 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 页面展示:
- 状态、进度条和结果计数。
- 实时吞吐和延迟图。
- 当前 Agent、Lease 和 Provider。
- 当前并发、允许范围、动态调整控件。
- 结构化实时日志和错误分类。
- Pause、Resume、Cancel 等状态相关操作。
- 成功、失败、未知、未发送 Artifact 下载。
SSE 断开时显示连接状态并自动重连;不能将“监控连接断开”误显示为“Run 失败”。
33.6 API 文档页
- 展示当前 Service 的实时 OpenAPI。
- 使用当前登录身份调用,但不在 Local Storage 保存长期 Token。
- Secret 示例只使用占位符。
- 对删除、取消、真实发送等操作增加风险提示。
- 只读角色禁用 Try It Out 的写操作。
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 本地认证
- Service 不因来自 localhost 就跳过认证。
- Desktop 使用受操作系统权限保护的 Bootstrap Channel 获取一次性 Token。
- 一次性 Token 换取短期会话后立即失效。
- 不把管理员长期 Token 写入普通配置文件。
34.3 进程边界
- Main、Preload 和 Renderer 严格分层;Renderer 设置
nodeIntegration: false、contextIsolation: true、sandbox: true。 - Renderer 只加载随安装包发布的本地内容,不执行任意远程代码;CSP 禁止
unsafe-eval。 - Preload 只暴露版本化、最小白名单 IPC,参数按 Schema 校验;不得暴露通用文件系统、Shell 或进程执行能力。
- Desktop 退出不等于停止 Service 或正在运行的 Run。
- Desktop 更新和 Service 更新是两个独立过程。
- Desktop 可以显示 Service 版本不兼容并引导升级。
- Desktop 不加载 Provider JAR、不读取数据库文件。
35. 认证、授权和审计
35.1 角色
| 角色 | 权限摘要 |
|---|---|
ADMIN |
用户、Agent、Secret、系统配置及全部业务操作 |
OPERATOR |
账号、消息、受众、Job、Run 和调度操作 |
VIEWER |
查询配置摘要、运行状态、日志和文档 |
读取账号不等于读取 Secret。即使 ADMIN 也不能通过普通 API 取回明文 Secret。
ADMIN、OPERATOR、VIEWER 角色绑定在 Workspace 范围内。另设显式 SYSTEM_ADMIN,只用于 Workspace 生命周期、全局 Agent 视图和跨 Workspace 应急治理;它可以越过 Workspace 角色,但每次操作仍携带目标 workspaceId 并进入审计。普通 Workspace ADMIN 不能签发、查看或吊销其他 Workspace 的 API Token/Enrollment Token。Standalone 的 Default Workspace 在普通 UI 中隐藏,但 API 和 Repository 仍使用其真实 ID。
35.2 权限检查
- Controller 只完成认证和基础 Scope 检查。
- Application Service 根据 Workspace、资源所有权和操作执行最终授权。
- Agent Internal API 仅接受 Agent 身份。
- Artifact 下载再次校验 Run 或 Audience 权限。
35.3 审计事件
必须审计:
- 登录、Token 创建和吊销。
- Account、Secret、Job、Schedule 的创建和修改。
- Run 启动、取消、暂停、恢复和并发调整。
- Agent Enrollment、禁用和 Credential 轮换。
- Artifact 下载和敏感配置导出。
- 权限失败和关键系统配置改变。
审计 Details 只保存字段名和变更摘要,不保存 Secret 原值。
36. 输入与网络安全
- 所有 API 请求设置 Body 大小上限。
- 文件导入流式解析并限制解压后大小、行数、列数和单字段长度。
- CSV/Excel 导出防止公式注入。
- 模板引擎禁止任意类访问、文件访问和反射。
- HTTP Provider 实施 SSRF 保护和响应大小限制。
- 标准渠道生产实现固定或严格校验厂商官方端点;响应体有界,诊断不包含 Secret、Token URL 或完整远端响应。
- 上传文件名仅作为展示信息,不作为真实路径。
- 日志字段使用结构化参数,不拼接未清理的控制字符。
- OpenAPI 文档和 UI Schema 中的富文本需要清理危险 HTML。
- Service 默认安全响应头,并明确 CORS Allowlist。
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
- Liveness:进程和核心线程是否存活。
- Readiness:数据库、迁移、Secret、Artifact 是否可用。
- Agent Readiness:身份、Service 连接和 Provider Catalog 是否正常。
- Provider Health 不对每次 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
规则:
- 配置支持文件、环境变量和命令行覆盖,优先级明确。
- Secret 不直接写入普通 YAML,使用环境变量引用或 Secret Store。
- 启动时打印生效配置摘要,但敏感字段必须掩码。
- 未识别配置项默认报错或告警,不能静默忽略拼写错误。
- 配置变更是否支持热加载由字段明确声明,默认需要重启。
39. 默认端口和目录
以下为初始发行基线,安装包落地时允许用发行 ADR 调整未公开的具体值:
| 平台 | 配置 | 数据 | 日志 |
|---|---|---|---|
| Linux | /etc/wepush-next/ |
/var/lib/wepush-next/ |
/var/log/wepush-next/ |
| Windows | %ProgramData%\WePush Next\service.env、agent.env |
%ProgramData%\WePush Next\service、agent |
%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
- Service 使用专用系统用户
wepush,Agent 使用独立的wepush-agent,配置组权限和数据目录彼此分离。 - systemd Unit 以非 root 用户运行。
ExecStart指向版本化发行目录;完整发行变体携带 JDK 21jlinkRuntime Image,精简变体校验并使用系统 Java 21+,启动脚本按此顺序自动选择。- 配置
Restart=on-failure和合理的 Stop Timeout。 - 安装后不自动开放公网防火墙端口。
40.2 Windows
- 使用独立 Windows Service 名称
WePushNextService。 - 通过发行包离线携带且固定长度/SHA-256 的 WinSW 启动包内 Runtime 或系统 Java 21+;WinSW 使用低权限
LocalService,ProgramDataACL 只授予 LocalService、SYSTEM 和 Administrators。 - 配置滚动日志、失败重启和优雅停止。
- 安装、卸载和 Service 控制需要管理员权限。
40.3 macOS
- 系统级 Service 使用 LaunchDaemon;用户级 Standalone 可使用 LaunchAgent。
- 安装包明确选择一种运行级别,不能混用数据目录。
- LaunchDaemon 必须使用明确存在的非 root 账号;安装器默认使用
sudo的发起用户,也允许显式设置WEPUSH_SERVICE_USER。 - 未签名告知、Release SHA-256、SBOM 和升级流程单独验证。
40.4 升级
- 拒绝或等待新 Run。
- Service 进入 Draining。
- 生成带 Manifest 和逐文件 SHA-256 的数据库、Master Key、Artifact、Agent Identity、Journal、Outbox、插件与配置一致性备份,并调用 Restore 校验。
- 停止旧进程。
- 替换应用并执行迁移。
- 启动并完成 Readiness、Flyway 当前版本和本地无网络 Provider Dry Run 检查。
- 失败时原子切回旧版本、恢复升级前备份、验证旧版本并保留可诊断的恢复目录。
数据库迁移一旦包含不可逆变化,必须明确最低可回滚版本。
41. 测试详细设计
41.1 Core 单元测试
使用 Fake Clock、Fake Provider、In-Memory Recipient Source 和 Result Sink,覆盖:
- 空 Audience、单条和大批量。
- 并发增减和背压。
- 暂停、恢复和取消竞态。
- Retry、Retry-After 和 Deadline。
- Provider Session 打开或关闭失败。
- Result/Event Sink 暂时失败。
- 最终计数恒等式。
- Secret 清理和日志脱敏。
测试不能依赖真实时间 Sleep,应通过可控 Clock 和调度器推进。
41.2 Provider 契约测试
所有 ProviderFactory 必须通过同一套测试:
- Descriptor 和 Schema 可加载。
- Provider ID 唯一且合法。
- Secret 字段正确标记。
- 非法配置返回结构化 Validation Error。
- Session 线程安全声明与行为一致。
- ErrorCategory 映射完整。
- Close 幂等。
41.3 Agent 故障测试
- LeaseOffer 后未 Ack。
- Heartbeat 超时。
- Event Batch 重传和乱序。
- 旧 Epoch Agent 上报。
- Service 不可达时 Event Outbox 满。
- Agent 在 Provider 调用前、调用中和调用后崩溃。
- 重启后 Journal 与 Service 状态冲突。
41.4 Service 集成测试
- SQLite 和 PostgreSQL 使用同一 Repository 契约测试。
- 创建 Run 事务和幂等键。
- 并发 Lease Claim 只能产生一个 Active Lease,并产生唯一有效 Fencing Token。
- 两个 Service 实例竞争调度锁时只能有一个 Schedule Scanner。
LISTEN/NOTIFY丢失后数据库扫描仍能推进状态。- Run 状态机非法转换。
- Schedule 重复扫描不重复创建 Run。
- Secret 查询不返回明文。
- Artifact 未完成、损坏和越权下载。
- SSE 游标续传和过期。
41.5 E2E
首条流水线:
启动 Standalone Service
→ Web/API 创建 HTTP Account
→ 创建 Message 和 Audience Snapshot
→ 创建 Job
→ 启动 Run
→ Embedded Agent 领取
→ Mock HTTP Server 接收请求
→ SSE 观察进度
→ 下载结果 Artifact
→ 重启 Service 验证历史
42. 性能和容量验证
首期不承诺所有 Provider 的统一吞吐,但必须建立可重复基线:
- 100,000 Recipient 不全部加载进内存。
- 结果 Artifact 流式写入,内存随 Recipient 总数近似恒定。
- 进度事件数量由时间窗口控制,不与 Item 数量线性增长。
- 并发从低到高调整时不丢失 Item。
- Service 同时维持多个 SSE 连接时不会阻塞 Run 更新。
- Agent Event Outbox 在网络抖动下有明确磁盘和内存上限。
性能报告记录硬件、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
- 创建模块和架构测试。
- 实现 Core 类型、ExecutionEngine 和 Fake Provider。
- 完成并发、取消、结果和事件单元测试。
44.2 Iteration 2:HTTP Provider
- 完成 Descriptor、Schema、Session 和 Mock Server 契约测试。
- 实现 SSRF、超时、响应限制和错误分类。
- 通过 Embedded SDK 运行。
44.3 Iteration 3:Service 单机闭环
- SQLite、迁移、Account/Message/Audience/Job/Run API。
- Embedded Agent 复用同一 Lease/状态语义,但通过 Java 接口直连,不经过 gRPC。
- OpenAPI、Java SDK 和 SSE。
44.4 Iteration 4:最小 WebUI
- Account Schema 表单。
- Message、Audience 和 Job 创建。
- Run Detail、SSE 和结果下载。
- API 文档页面。
44.5 Iteration 5:远程 Agent
- Enrollment、身份、gRPC Connect、Heartbeat、LeaseOffer/LeaseAck 和 EventBatch/EventAck。
- Fencing Token 与 Agent Journal。
- 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:产品化
- Workspace 权限、审计、
LocalEnvelopeSecretStore。 - Scheduler。
- Desktop 和三平台 Service 安装。
- PostgreSQL 18 HA、S3-compatible Artifact Store 和部署文档。
当前进度: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:日常使用闭环
- Account/Message/Audience/Job/Schedule 编辑、修订、复制、状态管理。
- CSV/TXT 流式导入、字段映射、预览、去重、错误行和不可变 Snapshot。
- Dry Run、正式发送影响确认、结果查看和失败项关联重发。
- 资源/Run/审计签名分页筛选、真实总览和 Standalone/Server Workspace UX。
当前进度:Iteration 7 已在 0.1.0-alpha.3 完成。OpenAPI、Remote Java SDK、TypeScript Client、WebUI、SQLite V12/V13 和纵向集成测试同步交付;启动恢复也使用有界分页扫描。
44.8 Iteration 8:真实消息渠道
- 内置 SMTP、飞书/钉钉/企微机器人、阿里云短信、微信公众号、小程序和企业微信应用消息。
- 每个 Provider 同步交付 Account/Message/Recipient Schema、SecretRef、Dry Run、真实发送、错误分类和本地协议测试。
- Service/Agent 通过 ServiceLoader 发现,Embedded SDK 由调用方显式选择;WebUI 继续按实时 Schema 渲染。
当前进度:Iteration 8 已在 0.1.0-alpha.4 完成。标准渠道集中在独立 provider-standard 模块,生产端点受限于厂商官方域名;微信系 Token 只在 Session 内缓存并在明确失效后刷新一次。协议、重试、限流、幂等和最小配置见内置 Provider 指南。
45. 完成定义
一个模块或功能只有同时满足以下条件才视为完成:
- 实现符合依赖规则和领域状态机。
- 公开契约、Schema 或配置已记录。
- 单元、契约和必要的集成测试通过。
- 错误、日志和指标可诊断且不泄露 Secret。
- API 和 UI 具备明确的权限行为。
- 资源关闭、取消和进程退出路径经过测试。
- 文档、示例和升级说明同步更新。
- 不需要 Classic 源码或构建产物即可编译和运行。
46. 已决 ADR 与后续事项
本轮技术基线已由 ADR-0002 至 ADR-0008 确认,包括 Web/Desktop/Service、Secret、Agent 协议、Provider 插件、PostgreSQL HA、Artifact 和 Workspace 多租户。
以下事项可以在对应能力进入迭代前继续形成独立 ADR,但不阻塞当前架构:
- OpenAPI Generator、数据库迁移工具、路由和 Server State 库的具体版本。
- Desktop 本地 Bootstrap Channel、平台安装和未签名发行完整性验证。
- 非受信 Provider 的独立进程 Runner;只有明确服务于用户自建安全场景时才进入评审。
公共 SaaS、计费订阅、外部 Vault/云 KMS/Secret Manager、恶意公共租户物理隔离和跨地域控制面不是“后续非基线事项”,而是长期产品非目标。任何后续 ADR 不得把它们重新引入。允许评审的扩展不得反向污染 Core API,也不得削弱 Workspace、Lease Fencing、Secret 和 Artifact 的既定安全边界。
47. 文档演进规则
- 本文档负责跨模块的实现级设计。
- Java API、OpenAPI、JSON Schema 和数据库迁移文件落地后,以可执行契约为准,并保持与本文一致。
- 仅影响模块内部的实现细节记录在模块 README 或模块设计文档。
- 改变组件职责、依赖方向、运行语义或安全边界时,先提交 ADR,再修改本文档。
- 文档中的示例不得包含真实 Token、Secret、生产地址或个人数据。