Files
dingtalk-workspace-cli/internal/output/envelope.go

597 lines
27 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package output
import (
"encoding/json"
"errors"
"fmt"
"strings"
)
// Outcome 是一次 CLI 调用的结果类别(互斥,四值)。
// 门禁拦截(confirmation_required)是 failure 的子类,不占第五值
// (契约规范 §1 结果总图)。
type Outcome string
// 四个规范 Outcome 值。wire 值恒为这些小写字符串(契约规范 §2.5)。
const (
OutcomeSuccess Outcome = "success"
OutcomePending Outcome = "pending"
OutcomePartialFailure Outcome = "partial_failure"
OutcomeFailure Outcome = "failure"
)
// String 返回 Outcome 的 wire 表示(恒为小写规范值)。
func (o Outcome) String() string { return string(o) }
// MarshalJSON 把 Outcome 序列化为小写规范字符串。仅接受四个规范值,
// 非法值(含零值)在序列化期即报错,杜绝静默产出无效 wire 值
// (契约规范 §1/§2.5「仅四值」)。
func (o Outcome) MarshalJSON() ([]byte, error) {
switch o {
case OutcomeSuccess, OutcomePending, OutcomePartialFailure, OutcomeFailure:
return json.Marshal(string(o))
default:
return nil, fmt.Errorf("output: cannot marshal invalid outcome %q", string(o))
}
}
// ParseOutcome 严格解析 s 为 Outcome:仅接受四个小写规范值,
// 拒绝大小写变体、前后空白与空串(契约规范 §1)。
func ParseOutcome(s string) (Outcome, error) {
switch s {
case string(OutcomeSuccess):
return OutcomeSuccess, nil
case string(OutcomePending):
return OutcomePending, nil
case string(OutcomePartialFailure):
return OutcomePartialFailure, nil
case string(OutcomeFailure):
return OutcomeFailure, nil
}
return "", fmt.Errorf("output: invalid outcome %q: must be one of %q, %q, %q, %q",
s, OutcomeSuccess, OutcomePending, OutcomePartialFailure, OutcomeFailure)
}
// Envelope 是统一输出信封(契约规范 §2 信封结构)。
// 字段声明顺序对齐 §2.5 字段总表;Go 序列化按声明顺序输出,
// 由此得到稳定的顶层键顺序(golden 断言的前提)。
type Envelope struct {
// OK 为框架计算值:ok == (outcome ∈ {success, pending})(不变量 I1)。
// 恒序列化(无 omitempty),类型为 bool,禁止字符串布尔。
OK bool `json:"ok"`
// Outcome 仅四值(见 Outcome 常量)。恒序列化。
Outcome Outcome `json:"outcome"`
// Identity 为执行身份(user/app),空则省略(§2.5)。
Identity string `json:"identity,omitempty"`
// DryRun 在 dry-run 预览时为 true,默认省略(§6 场景6)。
DryRun bool `json:"dry_run,omitempty"`
// Data 是业务载荷(L2):强类型 DTO 序列化,禁止手拼 JSON。
// 可为 object 或 array;空数组 [] 是合法载荷(原样序列化,
// 见空结果表达 data:[] + count:0)。
Data any `json:"data,omitempty"`
// Meta 是元数据层(count/operation/pagination),整体 omitempty。
Meta *Meta `json:"meta,omitempty"`
// Error 是失败明细(L1):非空 ⇔ outcome==failure(不变量 I3)。
Error *ErrorInfo `json:"error,omitempty"`
// Notice 是系统级通知注入位(_notice,如升级提示),空则省略。
// 契约仅规定 object 注入位;具体通知 shape 由后续批次定型。
Notice any `json:"_notice,omitempty"`
}
// Meta 是信封元数据层(契约规范 §2.5)。
// 分页/异步操作元数据挂 meta 层而非 data 层(§3)。
type Meta struct {
// Count 是列表结果条数。指针类型区分「未设置」与「0 条」:
// 空结果场景必须显式输出 count:0(§3 / AC-06),
// nil 时字段缺席(omitempty)。
Count *int `json:"count,omitempty"`
// Operation 是异步操作信息(pending 信封,§2.2)。
Operation *OperationInfo `json:"operation,omitempty"`
// Pagination 是分页元数据(§3)。
Pagination *Pagination `json:"pagination,omitempty"`
}
// NewCount 返回 n 的指针,供 Meta.Count 使用,
// 以区分「count:0」(空结果,必须输出)与「count 缺席」(非列表场景)。
func NewCount(n int) *int { return &n }
// OperationInfo 是异步操作元数据(契约规范 §2.2)。
// 轮询超时必须保持 State 真实值并置 TimedOut:true,禁止伪装 success;
// NextCommand 必须是可直接执行的完整命令(恢复方式)。
type OperationInfo struct {
ID string `json:"id,omitempty"`
State string `json:"state,omitempty"`
TimedOut bool `json:"timed_out,omitempty"`
NextCommand string `json:"next_command,omitempty"`
}
// Pagination 是分页元数据(契约规范 §3)。
// EndpointExhausted 仅表示「观察到服务端分页耗尽」,不承诺索引健康或
// 数据全覆盖;弃用 complete 键。EndpointExhausted:false 必须携带
// NextToken(可续跑)——两者合起来即全部状态,不设 stop_reason。
// EndpointExhausted 不加 omitempty:true/false 两态都必须在 wire 上显式出现。
type Pagination struct {
EndpointExhausted bool `json:"endpoint_exhausted"`
Pages int `json:"pages,omitempty"`
Items int `json:"items,omitempty"`
NextToken string `json:"next_token,omitempty"`
}
// ErrorInfo 是失败信封的错误明细(契约规范 §2.4)。
// Type/Subtype/Code/Retryable/RetryAfterSeconds 为 wire-stable(Agent 可分支);
// Message/Hint/RequestID 为 informational(禁止分支)。
type ErrorInfo struct {
Type string `json:"type,omitempty"`
Subtype string `json:"subtype,omitempty"`
ExitCode int `json:"exit_code,omitempty"`
HTTPStatus int `json:"http_status,omitempty"`
UpstreamCode any `json:"upstream_code,omitempty"`
RPCCode int `json:"rpc_code,omitempty"`
Code int `json:"code,omitempty"`
Message string `json:"message,omitempty"`
Hint string `json:"hint,omitempty"`
// Retryable 仅为 true 时出现在 wire(§2.4;重试设计 §2.1):
// bool 零值 false 遇 omitempty 即字段缺席。CLI 不自动重放。
Retryable bool `json:"retryable,omitempty"`
// RetryAfterSeconds 是服务端给出的重试等待秒数。指针类型保留
// 零值语义:0 秒有意义(nil 缺席、&0 序列化为 0),负值应在
// 构造侧拒绝。
RetryAfterSeconds *int64 `json:"retry_after_seconds,omitempty"`
RequestID string `json:"request_id,omitempty"`
TraceID string `json:"trace_id,omitempty"`
Operation string `json:"operation,omitempty"`
ServerKey string `json:"server_key,omitempty"`
Origin string `json:"origin,omitempty"`
Stage string `json:"stage,omitempty"`
ExecutionStarted *bool `json:"execution_started,omitempty"`
NextRetryAt string `json:"next_retry_at,omitempty"`
AvailableFlags []string `json:"available_flags,omitempty"`
SnapshotPath string `json:"snapshot_path,omitempty"`
Details map[string]any `json:"details,omitempty"`
RPCData any `json:"rpc_data,omitempty"`
TechnicalDetail string `json:"technical_detail,omitempty"`
FriendlyHint string `json:"friendly_hint,omitempty"`
ActionURL string `json:"action_url,omitempty"`
Cause string `json:"cause,omitempty"`
// Param/Params 在校验错误时指明违规参数(§2.4)。
Param string `json:"param,omitempty"`
Params []string `json:"params,omitempty"`
// Actions 是可执行的补救命令(门禁拦截 subtype=confirmation_required
// 时携带,含 --yes 版本;§2.4)。
Actions []string `json:"actions,omitempty"`
}
// ValidateDataErrorExclusivity 校验 wire 层面的 data/error 互斥(契约规范
// §1 不变量 I3 的字段级推论;§2.3/§2.4 形态):error 非空时 data 必须缺席——
// 失败信封只承载 error 明细,部分成功明细走 data 且不携带顶层 error。
// 无违反返回 nil;聚合校验入口(B18 Envelope.Validate)将组合本检查。
func (e *Envelope) ValidateDataErrorExclusivity() error {
if e == nil {
return nil
}
if e.Error != nil && e.Data != nil {
return fmt.Errorf("output: envelope violates invariant I3: data and error are mutually exclusive, data must be absent when error is present")
}
return nil
}
// Validate 是信封的聚合校验入口(B18,契约规范 §1):一次性收集全部违反项
// (errors.Join)而非命中即返回,便于装配侧一轮定位所有契约缺陷。校验项:
//
// - outcome 必须是四个规范值之一(§1/§2.5,复用 ParseOutcome);
// - 不变量 I1:OK == (outcome ∈ {success, pending})——框架计算值一致性;
// - 不变量 I3:Error 非空 ⇔ outcome == failure(双向);
// - I3 字段级推论:data/error 互斥(复用 ValidateDataErrorExclusivity);
// - Data 为类型化 *PartialData 时的三通道纪律(对账、非空 succeeded、
// id 可标识与跨通道互斥,§2.3;裸 map 等业务载荷形态不做解释)。
//
// outcome 非法时其派生检查(I1/I3)失去判定基准,仅报告 outcome 本身。
// Validate 只做静态字段校验,不解释业务载荷内容(L3 真实性归产品命令,§9)。
// nil 信封返回 nil:wire 出口(WriteEnvelopeTo)对 nil 已有降级 failure
// 信封的兜底,类型层不重复拦截。
func (e *Envelope) Validate() error {
if e == nil {
return nil
}
outcome, err := ParseOutcome(string(e.Outcome))
if err != nil {
return err
}
var errs []error
wantOK := outcome == OutcomeSuccess || outcome == OutcomePending
if e.OK != wantOK {
errs = append(errs, fmt.Errorf("output: envelope violates invariant I1: ok=%v but outcome=%q requires ok=%v", e.OK, outcome, wantOK))
}
errorPresent := e.Error != nil
isFailure := outcome == OutcomeFailure
if errorPresent != isFailure {
errs = append(errs, fmt.Errorf("output: envelope violates invariant I3: error present=%v but outcome=%q (error must be present if and only if outcome is failure)", errorPresent, outcome))
}
if err := e.ValidateDataErrorExclusivity(); err != nil {
errs = append(errs, err)
}
if pd, ok := e.Data.(*PartialData); ok && pd != nil {
if err := pd.Validate(); err != nil {
errs = append(errs, err)
}
}
if e.Meta != nil && e.Meta.Operation != nil {
if err := e.Meta.Operation.Validate(); err != nil {
errs = append(errs, err)
}
}
if e.Meta != nil && e.Meta.Pagination != nil {
if err := e.Meta.Pagination.Validate(); err != nil {
errs = append(errs, err)
}
}
if e.Error != nil {
if err := e.Error.Validate(); err != nil {
errs = append(errs, err)
}
}
return errors.Join(errs...)
}
func (p *Pagination) Validate() error {
if p == nil {
return nil
}
if p.EndpointExhausted && strings.TrimSpace(p.NextToken) != "" {
return fmt.Errorf("output: exhausted pagination must not carry next_token")
}
if !p.EndpointExhausted && strings.TrimSpace(p.NextToken) == "" {
return fmt.Errorf("output: non-exhausted pagination requires next_token")
}
return nil
}
func (e *ErrorInfo) Validate() error {
if e == nil {
return fmt.Errorf("output: failure error is nil")
}
errorType := strings.TrimSpace(e.Type)
if errorType == "" {
return fmt.Errorf("output: failure error.type is required")
}
// error.type is a wire-stable Agent branch key, not an open-ended label.
// Keep this set aligned with exitCodeForErrorInfo. "permission" is the
// compatibility projection for PAT failures (rc=4).
switch errorType {
case "api", "auth", "validation", "permission", "discovery", "internal":
default:
return fmt.Errorf("output: unsupported failure error.type %q", e.Type)
}
if e.ExitCode < 0 {
return fmt.Errorf("output: failure error.exit_code must not be negative")
}
if e.RetryAfterSeconds != nil && *e.RetryAfterSeconds < 0 {
return fmt.Errorf("output: failure retry_after_seconds must be non-negative")
}
return nil
}
// NewSuccessEnvelope 构造成功信封(B19,契约规范 §2.1)。
// OK 由框架按 outcome 计算(I1),调用层不得自行改写。data 必须是强类型
// DTO 序列化产物,禁止手拼 JSON(§2.5/§7);空数组 [] 是合法载荷(AC-06)。
func NewSuccessEnvelope(data any) *Envelope {
return &Envelope{OK: true, Outcome: OutcomeSuccess, Data: data}
}
// NewPendingEnvelope 构造异步受理信封(B19,契约规范 §2.2)。
// op 携带 id/state/timed_out/next_command;op 为 nil 时 meta 整体缺席。
// 轮询超时必须保持 State 真实值并置 TimedOut:true,禁止伪装 success;
// NextCommand 必须是可直接执行的完整命令(§2.2 规则)。
//
// 本构造器保持「构造宽松 + 校验严格」分层(轮6裁决⑨):§2.2 两条硬规则
// 不在构造期拦截,由 OperationInfo.Validate 与严格装配助手
// NewPendingEnvelopeForPolling(B147/B148)把守。
func NewPendingEnvelope(op *OperationInfo) *Envelope {
env := &Envelope{OK: true, Outcome: OutcomePending}
if op != nil {
env.Meta = &Meta{Operation: op}
}
return env
}
// NewFailureEnvelope 构造失败信封(B20,契约规范 §2.4)。
// error 非空 ⇔ outcome==failure(I3):info 为 nil 的失败信封无法通过
// Validate(),调用侧不得提交;data 必须缺席(I3 字段级推论)。
func NewFailureEnvelope(info *ErrorInfo) *Envelope {
return &Envelope{OK: false, Outcome: OutcomeFailure, Error: info}
}
// NewPartialEnvelope 构造部分成功信封(B20,契约规范 §2.3)。
// 三通道明细(total/succeeded/failed/unknown)进 data,不携带顶层 error。
// 通道规则(B140/B141/B144 已落盘):推荐以 NewPartialData 装配明细——
// 构造期拒绝空 succeeded(全失败禁用 partial_failure);Envelope.Validate
// 对 *PartialData 载荷追加通道校验(同 id 跨通道互斥、total 对账等)。
func NewPartialEnvelope(data any) *Envelope {
return &Envelope{OK: false, Outcome: OutcomePartialFailure, Data: data}
}
// IsOK 计算不变量 I1(B21,契约规范 §1):ok == (outcome ∈ {success, pending})。
// 这是框架侧的唯一计算函数,命令层不可写或改写该结果;Envelope.OK 字段值
// 必须与 IsOK 一致(Validate 交叉校验,违反即 I1 缺陷)。
// nil 信封视为不 ok:没有结果可言,不得汇报为成功/受理。
// 非法(非四值)outcome 必然返回 false:I1 只定义在四个规范值上,
// 非法值由 ParseOutcome/Validate/MarshalJSON 各自拒绝。
func (e *Envelope) IsOK() bool {
if e == nil {
return false
}
return e.Outcome == OutcomeSuccess || e.Outcome == OutcomePending
}
// ---------------------------------------------------------------------------
// Phase G:partial_failure 三通道明细(B139~B146;契约规范 §2.3)
// ---------------------------------------------------------------------------
// PartialFailedEntry 是 partial 失败通道的单条明细(B139,契约规范 §2.3):
// id 标识条目,Error 携带该条目的失败明细(复用 §2.4 ErrorInfo 形态,
// wire-stable 字段组可供 Agent 分支)。
type PartialFailedEntry struct {
ID string `json:"id"`
Error *ErrorInfo `json:"error,omitempty"`
}
// PartialUnknownEntry 是 partial unknown 通道的单条明细(B144,契约规范
// §2.3/P0#9「报错却写入」类问题的诚实通道):shape = id + reason——
// 已提交但终态无法确认的条目放这里;reason 必须非空,说明不确定的原因。
type PartialUnknownEntry struct {
ID string `json:"id"`
Reason string `json:"reason"`
}
// PartialData 是部分成功三通道明细的类型化形态(B139,契约规范 §2.3),
// 整体放入 Envelope.Data。声明顺序 = wire 顺序:total/succeeded/failed/
// unknown(对齐契约示例)。三通道纪律(B140/B141/B144/B146)由
// NewPartialData 构造期校验 + Validate 集中复验:
//
// - succeeded 必须完整保留(构造器不接受丢弃明细,§2.3 规则1);
// - unknown 条目禁止归入 succeeded/failed(同一 id 跨通道互斥,§2.3 规则2);
// - 空 succeeded 禁用 partial_failure(全失败必须用普通 failure,§2.3 规则4)。
//
// 三个切片字段不带 omitempty:空通道以 [] 显式表达(不输出 null,
// §2.5 无 null 纪律),与 data:[] 空态合法载荷同口径。
type PartialData struct {
Total int `json:"total"`
Succeeded []any `json:"succeeded"`
Failed []PartialFailedEntry `json:"failed"`
Unknown []PartialUnknownEntry `json:"unknown"`
}
// NewPartialData 装配三通道明细(B139/B140/B141/B144/B146,契约规范 §2.3)。
// 装配期即校验通道纪律:
//
// 1. succeeded 非空且原样保留——空 succeeded 的 partial_failure 被拒绝
// (全部失败必须用 outcome:failure,§2.3 规则4;B141);构造器不丢弃、
// 不过滤任何 succeeded 条目(B146:Agent 需要知道哪些已生效,避免重复提交);
// 2. failed/unknown 条目 id 必须非空(条目可标识是跨通道互斥的唯一依据);
// unknown 条目 reason 必须非空(诚实通道必须说明不确定原因,B144);
// 3. 同一 id 不得跨通道或同通道重复出现——一个条目只有一个终态通道
// (「unknown 禁止归入 succeeded/failed」,§2.3 规则2;B140)。succeeded
// 条目是异构业务 DTO,其 id 按 JSON 泛化形态的 "id" 键提取;无 id 或非
// object 的条目不参与互斥判定(框架不解释业务载荷内容,§9);
// 4. total 对账:total == len(succeeded)+len(failed)+len(unknown)——
// 三通道明细是 total 的完整划分,不允许出现无明细的条目。
//
// nil 的 failed/unknown 入参归一为空切片(空通道 wire 上是 [] 而非 null)。
func NewPartialData(total int, succeeded []any, failed []PartialFailedEntry, unknown []PartialUnknownEntry) (*PartialData, error) {
if failed == nil {
failed = []PartialFailedEntry{}
}
if unknown == nil {
unknown = []PartialUnknownEntry{}
}
d := &PartialData{Total: total, Succeeded: succeeded, Failed: failed, Unknown: unknown}
return d, d.Validate()
}
// Validate 校验三通道纪律(B140)。NewPartialData 构造期已校验;本方法供
// 手工装配形态(绕过构造器)集中复验,并由 Envelope.Validate 在
// outcome==partial_failure 且 Data 为 *PartialData 时聚合调用。
func (d *PartialData) Validate() error {
if d == nil {
return nil
}
var errs []error
if d.Succeeded == nil || d.Failed == nil || d.Unknown == nil {
errs = append(errs, fmt.Errorf("output: partial channels must serialize as [] not null (§2.5 no-null discipline)"))
}
// B141:全失败禁用 partial_failure(§2.3 规则4)。
if len(d.Succeeded) == 0 {
errs = append(errs, fmt.Errorf("output: partial_failure with empty succeeded is forbidden: all-failed batches must use outcome:failure (§2.3)"))
}
// B146:succeeded 完整保留——不接受 nil(null)条目混入明细。
for i, item := range d.Succeeded {
if item == nil {
errs = append(errs, fmt.Errorf("output: partial succeeded[%d] is nil: succeeded details must be fully preserved entries (§2.3)", i))
}
}
for i, entry := range d.Failed {
if strings.TrimSpace(entry.ID) == "" {
errs = append(errs, fmt.Errorf("output: partial failed[%d] has empty id: entries must be identifiable (§2.3)", i))
}
if entry.Error == nil {
errs = append(errs, fmt.Errorf("output: partial failed[%d] requires a typed error (§2.3)", i))
} else if err := entry.Error.Validate(); err != nil {
errs = append(errs, fmt.Errorf("output: partial failed[%d] has invalid typed error: %w", i, err))
}
}
// B144:unknown shape = id + reason,两者都必须非空。
for i, entry := range d.Unknown {
if strings.TrimSpace(entry.ID) == "" {
errs = append(errs, fmt.Errorf("output: partial unknown[%d] has empty id: entries must be identifiable (§2.3)", i))
}
if strings.TrimSpace(entry.Reason) == "" {
errs = append(errs, fmt.Errorf("output: partial unknown[%d] has empty reason: the honest channel must state why the terminal state is unconfirmed (§2.3/P0#9)", i))
}
}
// B141/§2.3:total 对账——三通道明细是 total 的完整划分。
if sum := len(d.Succeeded) + len(d.Failed) + len(d.Unknown); d.Total != sum {
errs = append(errs, fmt.Errorf("output: partial total=%d does not reconcile with channel details (succeeded %d + failed %d + unknown %d = %d) (§2.3)",
d.Total, len(d.Succeeded), len(d.Failed), len(d.Unknown), sum))
}
// B140:同一 id 跨通道/同通道互斥(unknown 不得归入 succeeded/failed)。
seen := make(map[string]string)
register := func(id, channel string) {
id = strings.TrimSpace(id)
if id == "" {
return
}
if prev, dup := seen[id]; dup {
errs = append(errs, fmt.Errorf("output: partial entry id %q appears in both %s and %s: one entry has exactly one terminal channel (§2.3)", id, prev, channel))
return
}
seen[id] = channel
}
for _, item := range d.Succeeded {
register(partialEntryID(item), "succeeded")
}
for _, entry := range d.Failed {
register(entry.ID, "failed")
}
for _, entry := range d.Unknown {
register(entry.ID, "unknown")
}
return errors.Join(errs...)
}
// partialEntryID 从异构 succeeded 条目中提取 "id"(JSON 泛化形态的字符串键)。
// 无 id、id 非字符串或非 object 的条目返回空串(不参与互斥判定)。
func partialEntryID(item any) string {
generic, ok := toGeneric(item).(map[string]any)
if !ok {
return ""
}
id, _ := generic["id"].(string)
return id
}
// ---------------------------------------------------------------------------
// Phase H:operation / pagination 装配助手(B147~B152;契约规范 §2.2/§3)
// ---------------------------------------------------------------------------
// operation.state 取值表(B150,契约规范 §2.2):state 描述异步操作所处阶段,
// 规范值为以下集合(开放枚举——业务特化阶段容忍,但以下三值是 Agent 必须能
// 分支的最小集)。timed_out:true 禁止与完成态共存(§2.2 规则1,见
// OperationInfo.ValidateTimeoutState)。
const (
OperationStateProcessing = "processing"
OperationStateCompleted = "completed"
OperationStateFailed = "failed"
)
// ValidateNextCommand 校验 next_command 的可执行形态(B148,契约规范 §2.2
// 规则2「next_command 必须是可直接执行的完整命令」):非空时必须是无前后
// 空白、不含换行的单行命令串——这是框架侧对「可直接执行」的形态代理;
// 命令真实可执行性(命令存在、参数有效)归装配侧保证(§9)。
// 空串在此不拦截:omitempty wire 允许无 operation 明细的裸 pending 信封,
// 轮询形态装配 NewPendingEnvelopeForPolling 才要求非空。
func (op *OperationInfo) ValidateNextCommand() error {
if op == nil {
return nil
}
if op.NextCommand == "" {
return nil
}
if strings.TrimSpace(op.NextCommand) != op.NextCommand {
return fmt.Errorf("output: operation.next_command %q must carry no leading/trailing whitespace (§2.2: directly executable)", op.NextCommand)
}
if strings.ContainsAny(op.NextCommand, "\n\r") {
return fmt.Errorf("output: operation.next_command must be a single executable command line (§2.2)")
}
return nil
}
// ValidateTimeoutState 执行 §2.2 规则1 的防伪装校验(B149):轮询超时必须
// 保持 state 真实值并置 timed_out:true——TimedOut 时 State 必须非空(真实值
// 必报),且不得宣称任何完成态(completed/success):超时操作宣称完成态
// 即伪装 success(对齐 lark wiki_node_delete 教训)。
func (op *OperationInfo) ValidateTimeoutState() error {
if op == nil || !op.TimedOut {
return nil
}
state := strings.TrimSpace(op.State)
if state == "" {
return fmt.Errorf("output: operation.timed_out=true requires the real state to be reported (empty state, §2.2 anti-spoof)")
}
if state == OperationStateCompleted || strings.EqualFold(state, string(OutcomeSuccess)) {
return fmt.Errorf("output: operation.timed_out=true must keep the real state %q: a timed-out operation must not claim a completed state (§2.2 anti-spoof)", state)
}
return nil
}
// Validate 聚合 operation 的 §2.2 形态校验。必填字段由新框架的
// CommandResult 边界校验;这里保持旧 Envelope 构造 API 的兼容性。
func (op *OperationInfo) Validate() error {
if op == nil {
return nil
}
return errors.Join(op.ValidateNextCommand(), op.ValidateTimeoutState())
}
// NewPendingEnvelopeForPolling 装配轮询形态的 pending 信封(B147 增强,
// 契约规范 §2.2):比 NewPendingEnvelope 更严格——轮询形态的恢复方式只有
// next_command 一条出口,故 op 与 next_command 必须非空且通过可执行形态
// 校验;state 必须遵守 timed_out 防伪装规则。返回 I1 一致(ok:true/
// outcome:pending)的信封;装配失败返回 error(信封为 nil)。
func NewPendingEnvelopeForPolling(op *OperationInfo) (*Envelope, error) {
if op == nil {
return nil, fmt.Errorf("output: polling-form pending envelope requires operation (§2.2: meta.operation is the recovery carrier)")
}
if strings.TrimSpace(op.NextCommand) == "" {
return nil, fmt.Errorf("output: polling-form pending envelope requires a non-empty, directly executable next_command (§2.2 rule 2)")
}
if err := op.Validate(); err != nil {
return nil, err
}
return NewPendingEnvelope(op), nil
}
// NewPagination 装配分页元数据(B152,契约规范 §3):两态完备校验——
// endpoint_exhausted:false 必须携带 next_token(可续跑:无 token 则
// 「可续跑」不可表达,破坏 §3 两态完备性);endpoint_exhausted:true
// 不得携带 next_token(耗尽即无续跑凭据,两者合起来即全部状态,
// 不设 stop_reason)。Pages/Items 为信息性计数(omitempty),由调用方
// 按观察到的页数/条数在返回结构上另行设置。
func NewPagination(endpointExhausted bool, nextToken string) (*Pagination, error) {
token := strings.TrimSpace(nextToken)
if !endpointExhausted && token == "" {
return nil, fmt.Errorf("output: pagination endpoint_exhausted=false requires next_token (§3: resumable state must carry the resume handle)")
}
if endpointExhausted && token != "" {
return nil, fmt.Errorf("output: pagination endpoint_exhausted=true must not carry next_token (§3: exhausted and resumable are the only two states)")
}
pg := &Pagination{EndpointExhausted: endpointExhausted, NextToken: token}
return pg, nil
}