Documentation
¶
Overview ¶
Package syncwire 是桌面端 ↔ server 工作区同步协议的线上契约:结构、词表、上限, 以及载荷守卫(guard.go)—— 两个宿主跑的是同一份守卫,而不是两份对齐的抄本。
桌面端 internal/pkg/syncwire 与本包同名,分工是清楚的:协议归本包,那边只做别名 再导出加上本端专属的东西(业务码的客户端表达、账号级实时通道的解码)。
字段的标签在这条协议里是**承重的**:PushItem.Payload 少一个 omitempty,墓碑就会带上 JSON null,server 的 GuardPayload 判 ErrPayloadNotObject、把这一条单独拒掉,这次删除 就传不到别的设备。syncwire_test.go 把它钉住了。
一个结构同时承担两侧的角色,所以两套标签都要在:json 标签管桌面端的编码与服务端的 解码,binding 标签管服务端的入参校验。binding 标签是惰性的,只有 gin 绑定时才读, 本 module 因此零外部依赖。
Index ¶
- Constants
- Variables
- func GuardPayload(kind string, payload []byte) error
- func KindValid(kind string) bool
- func PayloadFor(kind string) (any, bool)
- type AgentBackendCLIPayload
- type AgentBackendConfig
- type AgentBackendPayload
- type AgentExecTargetPayload
- type AgentPayload
- type DepartmentPayload
- type IssueLabelPayload
- type IssuePayload
- type LLMProviderModel
- type LLMProviderPayload
- type LabelPayload
- type LocalPathItem
- type ProjectAgentPayload
- type ProjectLocationPayload
- type ProjectPayload
- type PullItem
- type PullPage
- type PushItem
- type PushResult
Constants ¶
const ( KindProject = "project" KindDepartment = "department" KindAgent = "agent" KindAgentBackend = "agent_backend" KindAgentExecTarget = "agent_exec_target" KindProjectAgent = "project_agent" KindProjectLocation = "project_location" KindLLMProvider = "llm_provider" KindAgentBackendCLI = "agent_backend_cli" // KindLabel / KindIssue / KindIssueLabel 是看板并入账号级同步组带来的三个类型。 // 它们互相引用的方向是 label ← issue_label → issue,任务本身还引用项目、Agent // 与 backend —— 全部用同步标识表达,载荷里没有一个本地自增 ID。 KindLabel = "label" KindIssue = "issue" KindIssueLabel = "issue_label" )
同步组承载的对象类型,与 server 的 sync_entity 逐字一致。
const ( // PushStatusAccepted 基版本与该行当前版本相符,或该同步标识 server 从未见过。 PushStatusAccepted = "accepted" // PushStatusConflict 基版本与当前版本不符,或基版本为空但同步标识已存在。 // 本次上行按后到者胜照常生效,应答里回报被覆盖的版本与来源设备。 PushStatusConflict = "conflict" // PushStatusRejected 这一条没有生效,原因见 Reason。 PushStatusRejected = "rejected" )
const ( // PushRejectReasonDeleted 该对象在 server 上已是墓碑。删除不会被复活,恢复动作 // 因此明确失败;界面据此提供「按这份内容新建」—— 那是一个新的同步标识。 PushRejectReasonDeleted = "deleted" // PushRejectReasonKind 对象类型不属于同步组、与该同步标识已有行的类型不符, // 或缺少该类型必需的自然键。 PushRejectReasonKind = "kind_invalid" // PushRejectReasonPayload 载荷过不了服务端 ValidatePayload 的守卫。 PushRejectReasonPayload = "payload_rejected" )
单条拒绝的原因。
**凡是能拒掉一条的理由,都只拒那一条。** 整批拒是一个永久性的堵:上行端整批失败时 一行都不出队,下一轮再发同一批、再被同一条拒掉,那台机器的上行队列从此不动 —— 连 删除也传不出去。校验不通过的行以 rejected 回报,上行端据此把它移出队列并记进 「没能同步的改动」。
const ( MaxPushBatch = 500 MaxPullLimit = 1000 MaxLocalPathItems = 2000 )
MaxPushBatch / MaxPullLimit / MaxLocalPathItems 是三个请求的批量上限。
它们与 agentre-server 那三条 gin 标签里的字面量逐字相符 —— 标签写不了常量引用, 所以那一致性由消费仓自己的守卫盯着。桌面端一次实际发多少是它自己的选择,只要 不超过 MaxPushBatch。
const CodeCursorUnknown = 30505
CodeCursorUnknown 是「下行游标超出本账号版本序列的头」的业务码:那段历史 server 不认识 —— 库被重建,或用户换了一套自建服务端。
const CodeResyncRequired = 30500
CodeResyncRequired 是「设备距上次成功同步已超过墓碑保留窗口」的业务码。
Variables ¶
var ErrPayloadAvatarContent = errors.New("sync payload carries avatar content instead of a content hash")
ErrPayloadAvatarContent 表示载荷里出现了头像正文而不是内容哈希(R16a)。
var ErrPayloadCredential = errors.New("sync payload carries a credential or a provider row")
ErrPayloadCredential 表示载荷里出现了凭据或 provider 行正文。
var ErrPayloadLocalID = errors.New("sync payload carries a local auto-increment id")
ErrPayloadLocalID 表示载荷里出现了本地自增 ID。
var ErrPayloadNotObject = errors.New("sync payload must be a json object")
ErrPayloadNotObject 表示载荷不是一个 JSON 对象。
var Kinds = []string{ KindProject, KindDepartment, KindAgent, KindAgentBackend, KindAgentBackendCLI, KindAgentExecTarget, KindProjectAgent, KindProjectLocation, KindLLMProvider, KindLabel, KindIssue, KindIssueLabel, }
Kinds 是同步组的全部对象类型,按「被引用者在前」排列。
常量表与**成员资格**都只在这里:两个宿主要是各自枚举成员资格,任何一边漏掉一个新 kind,那一类对象就在那一端整类静默不同步 —— 没有报错,只是没有。两个宿主都只引用它。
次序是承重的:认领(R12a)与任何需要遍历全部类型的地方都按它走,父行因此先入队、 先落地,R2a 的暂缓少绕一圈。追加新 kind 时按引用方向插进去,别随手贴到末尾。
Functions ¶
func GuardPayload ¶
GuardPayload 是同步载荷的结构守卫。kind 让唯一可携带 API Key 的 llm_provider 与所有其它对象明确分开;调用者不可省略它。它按**键名**挡住三类东西:
- 本地自增 ID(键名以 id 结尾、取值是数字)。跨机引用一律用同步标识、agentred 指纹或 provider_key 表达,它们全是字符串;这个形状只可能是某台机器的本地主键, 在别的机器上指向完全不同的对象。
- `api_key` 这个键,以及整行 provider 正文(`provider` / `providers` 取值是 对象或数组)。只有 kind=llm_provider 能携带 api_key;其它对象只传 provider_key 这个字符串。agent_backend 身份载荷也不得含 cli_path。
- `avatar_data_url`:头像正文按内容哈希单独传,不进同步载荷。
**它挡不住、也没打算挡的:载荷里以 JSON 字符串形式携带的正文。** 最要紧的一处是 backend 的 `env_json`——它是用户自填的透传环境变量表,是 backend 配置的一部分, 按设计随 backend 上行,在账号下明文存放。里面不会有 App 自管的那 15 个保留键 (ANTHROPIC_API_KEY / OPENAI_API_KEY 等,由桌面端的 agent_backend_entity.Check 拒绝入库),但用户自己往里放的任何别的密钥都会原样过机。
这条守卫不是凭据扫描器,别把它当成一个——要挡住 env_json 就得整个字段不上行, 那是规格决定,不是守卫能顺手做掉的事。
**两个宿主跑的是这一份实现,不是两份对齐的抄本。** 桌面端在上行前调用它,让坏载荷 根本发不出去;服务端在落库前调用它,保护账号里的其它设备。守卫零外部依赖,这个 module 正是为「服务端也能 import」而存在的。
规则一旦分叉,后果是单条被拒(server 回 rejected,本端出队并进 R5 列表),不会堵住 整条队列 —— 所以分叉不会自己暴露,只能靠同一份实现防住。
空载荷合法:墓碑不带正文。
func PayloadFor ¶
PayloadFor 按对象类型造一份新的空载荷,返回的是**指针**,可以直接交给 json.Unmarshal。不认识的类型返回 (nil, false) —— 取值域与 Kinds 一样是闭合的。
知道自己在解什么的调用方直接用具体类型(`var p syncwire.IssuePayload`)更清楚; 这个口子是给按 kind 分发、拿不到静态类型的那几处用的。
**它不是写路径的工具。** sync_objects 是整行 last-write-wins,把一份载荷解进 结构体再 marshal 回去,会把这个类型当下不认识的键(对端新版本刚加的)静默抹掉。 写一个键就走 map[string]any 往返,只覆盖这次真的涉及的键。
Types ¶
type AgentBackendCLIPayload ¶
type AgentBackendCLIPayload struct {
CLIPath string `json:"cli_path"`
}
AgentBackendCLIPayload 是 kind=agent_backend_cli 的载荷:某个后端在某台机器上的 可执行文件路径。
它与 project_location 同形 —— 只带正文,身份(后端同步标识, agentred 指纹)在 ScopeSyncID / AgentredFingerprint 两列上,绝不进后端自己的身份载荷。这一条覆盖 缺席只意味着那台机器上走 PATH,账号级身份在每一端照常可用。
type AgentBackendConfig ¶
type AgentBackendConfig struct {
// ModelRoutes 是嵌套的 JSON 对象而不是字符串:套一层字符串会把
// `{"OPUS":{…}}` 转义成 `"{\"OPUS\":…}"`,人读不了,工具也进不去。
ModelRoutes json.RawMessage `json:"modelRoutes,omitempty"`
Sandbox string `json:"sandbox,omitempty"`
Approval string `json:"approval,omitempty"`
DefaultPermissionMode string `json:"defaultPermissionMode,omitempty"`
DefaultModel string `json:"defaultModel,omitempty"`
OpenClawGatewayURL string `json:"openclawGatewayUrl,omitempty"`
OpenClawAgentID string `json:"openclawAgentId,omitempty"`
OpenClawDefaultModel string `json:"openclawDefaultModel,omitempty"`
OpenClawSessionMode string `json:"openclawSessionMode,omitempty"`
HermesURL string `json:"hermesUrl,omitempty"`
// HermesAuthProvider / HermesUserID 是 gated serve 的非敏感展示字段。refresh
// token 绝不进这里(它只在桌面端 keychain 里)。
HermesAuthProvider string `json:"hermesAuthProvider,omitempty"`
HermesUserID string `json:"hermesUserId,omitempty"`
// ACPCommand / ACPArgs 仅 acp 使用:外部 ACP Agent 的可执行文件与附加 argv。
// 它们是账号级身份(与 hermesUrl 同形),没有每设备覆盖通路 —— acp 没有
// 「已知 CLI」的概念,可执行文件由 backend 自己声明。
ACPCommand string `json:"acpCommand,omitempty"`
ACPArgs []string `json:"acpArgs,omitempty"`
}
AgentBackendConfig 是后端**单类型独占设置**的唯一键表:桌面端 agent_backends.config_json 列、同步载荷的 config 对象与 web API 用的是同一个形状、同一组 camelCase 键。
每个键都 omitempty:「没配」在线上就是键不在,全空编成 {}。这里的零值都是合法取值 (空 sandbox = 走 CLI 默认,空网关地址 = 还没配),哪个键属于哪种后端类型由桌面端的 后端实体校验,契约只定形状。
type AgentBackendPayload ¶
type AgentBackendPayload struct {
Type string `json:"type"`
Name string `json:"name"`
ProviderKey string `json:"provider_key"`
ModelKey string `json:"model_key"`
EnvJSON string `json:"env_json"`
ReasoningEffort string `json:"reasoning_effort"`
// Config 是后端的单类型独占设置,整份过机:接收端用它**整体替换**本地那一份,
// 缺席的键即为空(缺整个 config 等同 {})。
Config AgentBackendConfig `json:"config"`
}
AgentBackendPayload 是 kind=agent_backend 的载荷:后端的**账号级身份**。
供应商配置自成一个对象(kind=llm_provider),这里只带 ProviderKey 这个稳定字符串 键。cli_path 是每设备覆盖,自成一个对象(kind=agent_backend_cli),**永不出现在 这里** —— GuardPayload 对 kind=agent_backend 明确挡住 cli_path 这个键。
运行设备同样不是载荷里的键:它走上行/下行项自己的 agentred_fingerprint 列, 后端在一台桌面端上配好之后,在每一端与 server 上指的都是同一台机器。
**EnvJSON 是用户自填的透传环境变量表,按设计随后端上行,在账号下明文存放。** App 自管的那批保留键(ANTHROPIC_API_KEY 等)由桌面端的实体校验拒绝入库,但用户 自己往里放的任何别的密钥都会原样过机 —— GuardPayload 不是凭据扫描器,挡不住以 JSON 字符串形式携带的正文(见它的文档)。
type AgentExecTargetPayload ¶
type AgentExecTargetPayload struct {
AgentSyncID string `json:"agent_sync_id"`
BackendSyncID string `json:"backend_sync_id"`
// SortOrder 是这一档在链上的位次;0 基,越小越先用。
SortOrder int `json:"sort_order"`
// SkillsJSON 是这一档的技能授权(R15e:授权下沉到档,不在 Agent 行上)。
SkillsJSON string `json:"skills_json"`
}
AgentExecTargetPayload 是 kind=agent_exec_target 的载荷:Agent 的一档执行目标。
type AgentPayload ¶
type AgentPayload struct {
Name string `json:"name"`
Description string `json:"description"`
AvatarColor string `json:"avatar_color"`
AvatarIcon string `json:"avatar_icon"`
// AvatarHash 是自定义头像正文的内容哈希;空串 = 没有自定义头像。正文按这个哈希
// 单独取(R16a),取不到时接收端保留本机已有的那份,不阻塞这一行落地。
AvatarHash string `json:"avatar_hash,omitempty"`
// SystemBadge 非空即那一个系统 Agent(唯一合法的「既不属于部门也没有上级」)。
SystemBadge string `json:"system_badge"`
// DepartmentSyncID 与 ParentAgentSyncID 是归属的二选一,两个都要如实带出:
// 接收端的归属选择器靠这两个键决定自己停在哪一组上,不能只带推导后的结果。
DepartmentSyncID string `json:"department_sync_id,omitempty"`
ParentAgentSyncID string `json:"parent_agent_sync_id,omitempty"`
SortOrder int `json:"sort_order"`
// PromptJSON / ToolsJSON 是两份 JSON 字符串,形状归桌面端所有,契约只搬运不解析
// —— 解析它就等于在两个宿主里各维护一份同一个结构。
PromptJSON string `json:"prompt_json"`
ToolsJSON string `json:"tools_json"`
Pinned bool `json:"pinned"`
}
AgentPayload 是 kind=agent 的载荷。
里面只有 AvatarHash 而没有头像正文:正文按内容哈希单独走一条路(R16a), 一律不进同步载荷(GuardPayload 挡住 avatar_data_url 这个键)。也没有 skills_json —— 技能授权下沉到执行目标行(R15e)。
type DepartmentPayload ¶
type DepartmentPayload struct {
Name string `json:"name"`
Description string `json:"description"`
Icon string `json:"icon"`
AccentColor string `json:"accent_color"`
// ParentSyncID 为空即根部门。
ParentSyncID string `json:"parent_sync_id,omitempty"`
// LeadAgentSyncID 是部门负责人的同步标识。
//
// 它与 department ↔ agent 之间存在**引用环**:负责人必须是本部门成员,所以那个
// Agent 的落地又等着这个部门。接收端因此不能把它当成阻塞引用 —— 两边都阻塞就是
// 死锁,两行一起躺到过期(桌面端 sync_svc/adapter_org.go 的 refs 注释与
// adapter_refcycle_test.go 是那道守卫)。
LeadAgentSyncID string `json:"lead_agent_sync_id,omitempty"`
SortOrder int `json:"sort_order"`
}
DepartmentPayload 是 kind=department 的载荷。
type IssueLabelPayload ¶
type IssueLabelPayload struct {
IssueSyncID string `json:"issue_sync_id"`
LabelSyncID string `json:"label_sync_id"`
}
IssueLabelPayload 是 kind=issue_label 的载荷:任务 ↔ 标签的关联。 关联表的主键是 (issue_id, label_id) 两个本地自增值,在另一台机器上指向完全不同的 两行,因此两端都只能用同步标识表达。
type IssuePayload ¶
type IssuePayload struct {
Title string `json:"title"`
Description string `json:"description"`
Stage string `json:"stage"`
// Position 是同一列内的排序位置,取浮点是为了在两张卡之间插入而不重排整列。
Position float64 `json:"position"`
ProjectSyncID string `json:"project_sync_id,omitempty"`
AgentSyncID string `json:"agent_sync_id,omitempty"`
AgentBackendSyncID string `json:"agent_backend_sync_id,omitempty"`
LLMProviderKey string `json:"llm_provider_key"`
LLMModelKey string `json:"llm_model_key"`
// ClosedAt 是完成时刻(Unix 毫秒),0 = 未完成。
ClosedAt int64 `json:"closed_at"`
}
IssuePayload 是 kind=issue 的载荷。
载荷里**没有运行态**:agent_status、session_id 与 source 是某一台机器上这一轮跑 成什么样,跨机没有意义,也不该让另一端的界面显示一个它并不持有的会话。也没有 state —— 它完全由 Stage 推导(stage=done 即已完成),两端各自算。
执行归属的四个字段里,Agent 与后端是账号级对象、用同步标识表达;供应商与模型 本来就是稳定的字符串键。
type LLMProviderModel ¶
type LLMProviderModel struct {
// ModelKey 是账号内稳定的模型键,别的对象(后端、任务)按它引用模型。
ModelKey string `json:"model_key"`
// ModelID 是发给供应商 API 的那个字符串。它以 id 结尾但**是字符串**,因此不落进
// GuardPayload 的本地自增 ID 那一条。
ModelID string `json:"model_id"`
Name string `json:"name"`
Enabled bool `json:"enabled"`
// ContextWindow / MaxOutput 为 0 = 未知,带 omitempty 一并缺席:线上「没填」与
// 「填了 0」不该长成同一个样子。
ContextWindow int `json:"context_window,omitempty"`
MaxOutput int `json:"max_output,omitempty"`
}
LLMProviderModel 是内嵌在 LLMProviderPayload 里的一行模型。它**不是**一个 kind: 模型跟着供应商整行走。
type LLMProviderPayload ¶
type LLMProviderPayload struct {
Name string `json:"name"`
Type string `json:"type"`
BaseURL string `json:"base_url"`
APIKey string `json:"api_key"`
// DefaultModelKey 指向 Models 里某一项的 ModelKey。
DefaultModelKey string `json:"default_model_key"`
Enabled bool `json:"enabled"`
Models []LLMProviderModel `json:"models"`
}
LLMProviderPayload 是 kind=llm_provider 的载荷,**唯一携带 API Key 的对象** (GuardPayload 只对这一个 kind 放行 api_key 这个键)。
它的同步标识就是 ProviderKey 本身:别的对象跨机引用供应商时只传这个稳定字符串键 (决策 6),因此这个对象没有第二套标识。
模型行**内嵌**而不另起一个 kind:model_key 由此保持稳定,又不必为一张从属表再造 一整套同步对象。
type LabelPayload ¶
type LabelPayload struct {
Name string `json:"name"`
Tone string `json:"tone"`
Status int `json:"status"`
}
LabelPayload 是 kind=label 的载荷。
Status 在载荷里,而别的对象的存活 / 墓碑只靠 DeletedAt 表达:server 没有本地行, 它的看板读路径把 sync_objects 的载荷直接拼成响应,没有这一列就判不出一个标签还 在不在。桌面端落地时**不**读它 —— 一条活着的下行项按定义就是活的(决策 20)。
type LocalPathItem ¶
type LocalPathItem struct {
ProjectSyncID string `json:"project_sync_id" binding:"required,max=128"`
Path string `json:"path" binding:"max=1024"`
}
LocalPathItem 是上报组的一条:某个项目在这台设备上的真实本机路径。
与同步组的那些表无关 —— 本机路径不在桌面端之间流动,只单向上报给 server, 按设备分命名空间存放。
type ProjectAgentPayload ¶
type ProjectAgentPayload struct {
ProjectSyncID string `json:"project_sync_id"`
AgentSyncID string `json:"agent_sync_id"`
// JoinedAt 是加入时刻(Unix 毫秒)。成员关系没有别的可改内容:它要么在、要么不在。
JoinedAt int64 `json:"joined_at"`
}
ProjectAgentPayload 是 kind=project_agent 的载荷:项目 ↔ Agent 的成员关系。 关系表的主键是两个本地自增值,因此两端都只能用同步标识表达。
type ProjectLocationPayload ¶
type ProjectLocationPayload struct {
Path string `json:"path"`
}
ProjectLocationPayload 是 kind=project_location 的载荷:某个项目在某台 agentred 上的路径。
只带路径正文。账号内自然键(项目同步标识, agentred 指纹)**在列上**—— ScopeSyncID 装项目、AgentredFingerprint 装机器,server 据此按 R4b 合并。
type ProjectPayload ¶
type ProjectPayload struct {
Name string `json:"name"`
Icon string `json:"icon"`
Color string `json:"color"`
Description string `json:"description"`
// ParentSyncID 为空即根项目。项目树的父引用,跨机用同步标识表达。
ParentSyncID string `json:"parent_sync_id,omitempty"`
// SortOrder 是同一层内的手工排序位次。
SortOrder int `json:"sort_order"`
}
ProjectPayload 是 kind=project 的载荷。
**没有 path**:本机路径住在桌面端的 projects.path,只上报、不同步(决策 6、R9); 同步进来的项目在接收端是「未配置路径」状态(R10)。也没有 status —— 存活 / 墓碑 由上行/下行项的 DeletedAt 表达,本地 status 是它的本机投影。
type PullItem ¶
type PullItem struct {
Kind string `json:"kind"`
SyncID string `json:"sync_id"`
ScopeSyncID string `json:"scope_sync_id,omitempty"`
AgentredFingerprint string `json:"agentred_fingerprint,omitempty"`
Payload json.RawMessage `json:"payload"`
Version int64 `json:"version"`
UpdatedAt int64 `json:"updated_at"`
// OriginFingerprint 是最后一次修改来自哪台机器(决策 14:跨机引用一律用指纹,
// 数值设备主键是 server 的本地键,桌面端离线创建的行没有它)。空串 = 服务端直写。
OriginFingerprint string `json:"origin_fingerprint"`
// DeletedAt 非零 = 墓碑,值是删除时刻(Unix 毫秒,决策 20)。
DeletedAt int64 `json:"deleted_at"`
}
PullItem 是下行的一行,墓碑也在其中(DeletedAt > 0),删除靠它到达各端。
type PullPage ¶
type PullPage struct {
Items []PullItem `json:"items"`
NextCursor int64 `json:"next_cursor"`
HasMore bool `json:"has_more"`
}
PullPage 是一次下行的一页。
type PushItem ¶
type PushItem struct {
Kind string `json:"kind" binding:"required,max=32"`
SyncID string `json:"sync_id" binding:"required,max=128"`
// BaseVersion 是发起端最后一次见到的同步版本号;本端新建、server 从未见过的行填 0。
BaseVersion int64 `json:"base_version"`
// UpdatedAt 是发起端的最后修改时间,只用于展示与 30 天窗口计算,不参与冲突裁决。
UpdatedAt int64 `json:"updated_at"`
// DeletedAt 非零表示这是一条墓碑,值是发起端记下的删除时刻(Unix 毫秒)。
// 契约上是**时刻**而不是布尔:发起端库、线格式与 server 库三处都是时刻,压成布尔
// 之后 server 落地只能另行编造一个删除时间(2026-08-27-schema-overhaul 决策 20)。
DeletedAt int64 `json:"deleted_at"`
AgentredFingerprint string `json:"agentred_fingerprint" binding:"max=128"`
// ScopeSyncID 装什么取决于 kind(project_location 装项目、agent_backend_cli
// 装后端),与 server 的 sync_objects.scope_sync_id 同义。
ScopeSyncID string `json:"scope_sync_id" binding:"max=128"`
// Payload 的 omitempty 是**承重的**:墓碑不带正文,而 json.RawMessage 的零值编出
// 来是 JSON null —— null 不是对象,server 的 GuardPayload 会把这一条拒掉,这次删除
// 就传不到别的设备。类型必须是 json.RawMessage 而不是 []byte,否则
// encoding/json 会把整份文档编成 base64。
Payload json.RawMessage `json:"payload,omitempty"`
}
PushItem 是一次上行里的一条改动。
type PushResult ¶
type PushResult struct {
SyncID string `json:"sync_id"`
Kind string `json:"kind"`
// Version 是 server 为这次上行分配的新版本号;被拒时是 server 上的当前版本。
Version int64 `json:"version"`
Status string `json:"status"`
Reason string `json:"reason,omitempty"`
OverwrittenVersion int64 `json:"overwritten_version,omitempty"`
OverwrittenOriginFingerprint string `json:"overwritten_origin_fingerprint,omitempty"`
OverwrittenPayload json.RawMessage `json:"overwritten_payload,omitempty"`
MergedSyncID string `json:"merged_sync_id,omitempty"`
MergedVersion int64 `json:"merged_version,omitempty"`
// MergedOriginFingerprint 是落败那一份来自哪台机器(决策 14);空串 = 服务端直写。
MergedOriginFingerprint string `json:"merged_origin_fingerprint,omitempty"`
}
PushResult 是一条上行的处置结果。
Overwritten* 只在 Status 为 conflict 时有值:被这次上行覆盖掉的是哪一版、来自哪台 机器、正文是什么。正文只有 server 有 —— 上行端手上那一份是**覆盖别人的**那一份, 「追回被覆盖的那一版」靠它。
Merged* 只在自然键合并发生时有值:落败那一份的同步标识、版本与来源机器,它已在 server 落墓碑。