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

498 lines
21 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 (
"bytes"
"fmt"
"io"
"strings"
"github.com/spf13/cobra"
)
// WriteEnvelope 是统一信封的出口函数(B0 dev 试点桥接,WS1 改动点2/4)。
// 它复用 envelope.go 的权威 Envelope 类型,把信封按当前 --format 分发写出。
// Phase B 的 Emitter 类型将在此之上泛化(渲染先进 buffer、exit code 映射等);
// 本函数仅提供 Phase F dev 域迁移所需的「信封 → cmd 流」出口,不重复定义类型。
//
// format 分发(契约规范 §5.2,经 renderEnvelope 实现):
// - json(默认):输出完整信封——唯一 JSON 契约(§5.2 首行)。
// - table/pretty:业务数据人读视图 + meta 摘要(不输出信封外壳)。
// - csv/ndjson:裸记录(无信封);meta.pagination 走 stderr 诊断行。
// - raw:数据载荷透传(与 Write 的 raw 语义一致)。
//
// 数据出口一律写 cmd.OutOrStdout()、诊断出口写 cmd.ErrOrStderr()(均可被
// 测试重定向),不硬编码 os.Stdout/os.Stderr。未知 --format 值按 §5.2/AC-09
// 降级 fallback 并在此向 stderr 写一条 warning(不崩不静默)。
func WriteEnvelope(cmd *cobra.Command, env *Envelope, fallback Format) error {
w, errW := io.Writer(io.Discard), io.Writer(io.Discard)
format := fallback
fields, jq, warning := "", "", ""
if cmd != nil {
w = cmd.OutOrStdout()
errW = cmd.ErrOrStderr()
format, warning = resolveFormatWithWarning(cmd, fallback)
fields = ResolveFields(cmd)
jq = ResolveJQ(cmd)
}
if env == nil {
env = nilFallbackEnvelope()
}
if err := renderEnvelope(w, errW, env, format, fields, jq); err != nil {
return err
}
// AC-09「不静默」:warning 仅在信封成功写出后补写 stderr——
// 写出失败时错误优先,不留半截通道。
if warning != "" {
if _, werr := fmt.Fprintln(errW, "[WARN] "+warning); werr != nil {
return werr
}
}
return nil
}
// WriteEnvelopeTo 是 WriteEnvelope 的 writer 级实现(签名自 B205 保持不变),
// 供不持有 *cobra.Command 的调用方与单测直接使用。B37 起收编到 renderEnvelope
// 统一分发:json/空 format 输出完整信封(唯一 JSON 契约);table/pretty 输出
// 业务数据人读视图 + meta 摘要;csv/ndjson 输出裸记录(分页诊断行在本签名下
// 无 stderr 出口可走,按 io.Discard 处理——需要诊断行的调用方走 WriteEnvelope
// 的 cmd 路径或 Emitter)。nil 信封降级为 I3 合法的 failure 兜底信封(B208):
// 携带 internal 类 ErrorInfo——框架最后的兜底不得产出被自身 Validate() 拒绝的形态。
func WriteEnvelopeTo(w io.Writer, env *Envelope, format Format, fields, jq string) error {
if env == nil {
env = nilFallbackEnvelope()
}
return renderEnvelope(w, io.Discard, env, format, fields, jq)
}
// EmitResult is the framework 2.0 CLI adapter. Unlike the legacy Emitter,
// machine-readable JSON/NDJSON always writes the primary result to stdout for
// every outcome; stderr is diagnostics-only. Human failure formats retain the
// conventional stderr path. The result is validated before any byte is
// rendered, and the returned exit code comes from the same immutable result.
func EmitResult(cmd *cobra.Command, result CommandResult) (int, error) {
code, _, err := emitResult(cmd, result)
return code, err
}
func emitResult(cmd *cobra.Command, result CommandResult) (int, bool, error) {
if err := ValidateResult(result); err != nil {
return exitCodeInternal, false, err
}
env := result.envelope()
env = redactEnvelope(env)
format, warning := resolveFormatWithWarning(cmd, FormatJSON)
fields, jq := ResolveFields(cmd), ResolveJQ(cmd)
stdout, stderr := io.Writer(io.Discard), io.Writer(io.Discard)
if cmd != nil {
stdout, stderr = cmd.OutOrStdout(), cmd.ErrOrStderr()
}
if env.Outcome == OutcomeFailure && format != FormatJSON && format != FormatNDJSON {
message := "command failed"
if env.Error != nil && strings.TrimSpace(env.Error.Message) != "" {
message = env.Error.Message
}
var buf bytes.Buffer
_, _ = fmt.Fprintln(&buf, "Error: "+message)
n, err := writeAllCount(stderr, buf.Bytes())
if err != nil {
return exitCodeInternal, n > 0, err
}
return result.ExitCode(), n > 0, nil
}
// Failure bypasses jq/fields so a filter cannot erase the typed error.
if env.Outcome == OutcomeFailure {
fields, jq = "", ""
}
var buf bytes.Buffer
if err := renderEnvelope(&buf, stderr, env, format, fields, jq); err != nil {
return exitCodeInternal, false, err
}
n, err := writeAllCount(stdout, buf.Bytes())
if err != nil {
return exitCodeInternal, n > 0, err
}
if warning != "" {
// The primary result is already complete. Diagnostics are best-effort
// and must not reverse its outcome when stderr is unavailable.
_, _ = fmt.Fprintln(stderr, "[WARN] "+warning)
}
return result.ExitCode(), n > 0, nil
}
// nilFallbackEnvelope 构造 nil 信封的兜底信封(B208,轮6裁决⑨)。保留
// 「不 panic、必产合法 failure」的既有语义,且自身满足不变量 I3
// (error 非空 ⇔ outcome==failure)、通过 Envelope.Validate()——框架出口
// 兜底不得被自己的校验器判非法。Type 取 "internal":这是框架内部缺陷而非
// 业务错误(§4 internal=5),Agent 可据 wire-stable 字段分支处置。
func nilFallbackEnvelope() *Envelope {
return &Envelope{
OK: false,
Outcome: OutcomeFailure,
Error: &ErrorInfo{
Type: "internal",
Message: "nil envelope: framework fallback",
},
}
}
// ---------------------------------------------------------------------------
// format 分发(B35~B40):信封 → format 矩阵的统一分发点
// (契约规范 §5.2;适用域仅 stdout 数据通道——轮8裁决⑪)
// ---------------------------------------------------------------------------
// renderEnvelope 是信封渲染的统一分发点(B37,WS1 改动点4;契约规范 §5.2
// format 矩阵)。渲染先进 buffer 再一次性写出:数据通道任何一步失败都不向
// w 泄漏半截输出(§5.2 末条;与 B25 buffer-first 同口径)。
//
// 矩阵各行(适用域仅 stdout 数据通道——失败信封在 Emitter 流路由层恒以
// 完整 JSON 写 stderr,从不进入本矩阵,轮8裁决⑪):
//
// - json / 空:完整信封——唯一 JSON 契约(§5.2 首行,B38);
// - table / pretty:业务数据人读视图 + meta 层人读摘要(count/pagination),
// 不输出信封外壳(§5.2 矩阵行,B39);
// - csv / ndjson:裸记录(无信封外壳);meta.pagination 走 errW 的 stderr
// 诊断行,不污染裸记录机器可读流(§5.2 矩阵行,B40);
// - raw:数据载荷透传(writeRaw 语义)。
//
// --jq 优先于 format:对**完整信封**求值后直接输出结果(§5.2 矩阵行,B42)。
// --fields 先投影数据载荷再按 format 分发(成功通道联动)。errW 为 nil 时按
// io.Discard 处理(csv/ndjson 诊断行无出口即丢弃)。
//
// buffer-first 纪律:全部分发路径(含 json/jq)先渲染进内存 buffer,成功后
// 才一次性写出到 w(§5.2 末条对分发点整体成立)——渲染失败零泄漏、写出
// 失败原子中止、短写报 io.ErrShortWrite。csv/ndjson 的 stderr 诊断行不经
// buffer,且在数据渲染成功后才写(渲染失败不留诊断噪声)。
func renderEnvelope(w io.Writer, errW io.Writer, env *Envelope, format Format, fields, jq string) error {
if errW == nil {
errW = io.Discard
}
if env == nil {
env = nilFallbackEnvelope()
}
env = redactEnvelope(env)
var buf bytes.Buffer
if err := renderEnvelopeInto(&buf, errW, env, format, fields, jq); err != nil {
return err
}
return writeAll(w, buf.Bytes())
}
// renderEnvelopeInto 按 format 矩阵把信封渲染进 buf(renderEnvelope 的渲染段)。
func renderEnvelopeInto(buf *bytes.Buffer, errW io.Writer, env *Envelope, format Format, fields, jq string) error {
// 失败信封不参与 format/fields/jq 分发(轮8裁决⑪;契约规范 §5.1 v1.1
// 回写条款):恒输出完整 JSON 信封。§5.2 矩阵适用域仅 stdout 数据通道,
// 失败信封无 data 载荷(I3),若按矩阵分发或经 jq/fields 过滤,§2.4
// wire-stable 字段组将从错误通道消失。
if env.Outcome == OutcomeFailure {
if format == FormatNDJSON {
return Write(buf, FormatNDJSON, env)
}
return WriteFiltered(buf, FormatJSON, env, "", "")
}
if strings.TrimSpace(jq) != "" {
// jq 优先于 format:对完整信封求值后直接输出(§5.2 矩阵行)。
return ApplyJQ(buf, env, strings.TrimSpace(jq))
}
switch format {
case FormatJSON, "":
// json(默认)始终保留完整信封。--fields 只投影业务载荷,
// ok/outcome/meta 等稳定契约字段仍必须可供 Agent 分支和续页。
if trimmed := strings.TrimSpace(fields); trimmed != "" {
projected := *env
projected.Data = SelectFields(env.Data, strings.Split(trimmed, ","))
return WriteFiltered(buf, FormatJSON, &projected, "", "")
}
return WriteFiltered(buf, FormatJSON, env, "", "")
default:
}
payload := env.Data
if trimmed := strings.TrimSpace(fields); trimmed != "" {
payload = SelectFields(payload, strings.Split(trimmed, ","))
}
switch format {
case FormatTable, FormatPretty:
if err := Write(buf, format, payload); err != nil {
return err
}
if summary := renderMetaSummary(env.Meta); summary != "" {
_, _ = fmt.Fprintf(buf, "\n%s\n", summary)
}
return nil
case FormatCSV, FormatNDJSON:
if err := Write(buf, format, payload); err != nil {
return err
}
// 分页元数据走 stderr 诊断行(§5.2 csv/ndjson 行)。partial_failure
// 例外:§2.3 明确「stderr 零输出」,特例优先于 §5.2 诊断行通则。
if env.Outcome != OutcomePartialFailure {
return writePaginationDiagnostic(errW, env.Meta)
}
return nil
case FormatRaw:
return Write(buf, FormatRaw, payload)
default:
// 未知 Format 常量(归一化/ParseFormat 后理论不可达):按 json
// 数据通道语义兜底,绝不静默丢弃载荷。--fields 同样投影业务载荷。
if trimmed := strings.TrimSpace(fields); trimmed != "" {
return WriteFiltered(buf, FormatJSON, env.Data, trimmed, "")
}
return WriteFiltered(buf, FormatJSON, env, "", "")
}
}
// writeAll 把 p 一次性写入 w;短写(接受了部分字节却未写全)报
// io.ErrShortWrite——与 Emitter.writeOnce 同一短写纪律,绝不把截断输出当成功。
func writeAll(w io.Writer, p []byte) error {
_, err := writeAllCount(w, p)
return err
}
func writeAllCount(w io.Writer, p []byte) (int, error) {
n, err := w.Write(p)
if err != nil {
return n, err
}
if n < len(p) {
return n, io.ErrShortWrite
}
return n, nil
}
// renderMetaSummary 把信封 meta 层渲染为人读摘要行(§5.2:table/pretty 输出
// 「业务数据 + 人读分页摘要」)。无 meta 内容返回空串。count 单独成行;
// pagination 摘要复用 renderPaginationSummary。
func renderMetaSummary(meta *Meta) string {
if meta == nil {
return ""
}
var lines []string
if meta.Count != nil {
lines = append(lines, fmt.Sprintf("count: %d", *meta.Count))
}
if meta.Pagination != nil {
lines = append(lines, "pagination: "+renderPaginationSummary(meta.Pagination))
}
return strings.Join(lines, "\n")
}
// renderPaginationSummary 按 wire 字段名输出分页摘要:
// endpoint_exhausted/pages/items/next_token(snake_case 与 wire 一致,Agent
// 可交叉核对;§3 两态显式语义保留——false 也输出,因为它是「可续跑」的
// 有意义状态)。
func renderPaginationSummary(p *Pagination) string {
parts := []string{fmt.Sprintf("endpoint_exhausted: %t", p.EndpointExhausted)}
if p.Pages > 0 {
parts = append(parts, fmt.Sprintf("pages: %d", p.Pages))
}
if p.Items > 0 {
parts = append(parts, fmt.Sprintf("items: %d", p.Items))
}
if p.NextToken != "" {
parts = append(parts, fmt.Sprintf("next_token: %s", p.NextToken))
}
return strings.Join(parts, ", ")
}
// writePaginationDiagnostic 把分页诊断行写到 errW(§5.2:csv/ndjson 裸记录
// 格式下分页元数据走 stderr 诊断行)。无 meta/pagination 时零输出——
// 不给无分页场景制造噪声。
func writePaginationDiagnostic(errW io.Writer, meta *Meta) error {
if meta == nil || meta.Pagination == nil {
return nil
}
_, err := fmt.Fprintln(errW, "[pagination] "+renderPaginationSummary(meta.Pagination))
return err
}
// ---------------------------------------------------------------------------
// Phase B 泛化(B24~B29):Emitter 类型——success/pending/partial 走 stdout、
// failure 走 stderr 的双向出口;渲染先进 buffer 再一次性写出。
// ---------------------------------------------------------------------------
// Emitter 是统一信封的双向出口(B24,WS1 改动点2)。它持有 stdout/stderr 两个
// 写入目标与当前 format/fields/jq 配置,按契约规范 §5.1 流向纪律把信封分发到
// 正确的流:
//
// - success / pending / partial_failure → stdout(数据通道);
// - failure(含 nil 信封降级)→ stderr,stdout 严格零字节(AC-11)。
//
// 每次写出都经过「先渲染进内存 buffer、再一次性写出」(B25,契约规范 §5.2
// 末条),写一半失败不产生残缺 JSON。渲染复用 WriteEnvelopeTo(B205),
// Emitter 不重新定义信封类型或 wire 键。
type Emitter struct {
stdout io.Writer
stderr io.Writer
format Format
fields string
jq string
}
// NewEmitter 构造 Emitter(B24)。nil writer 按 io.Discard 处理(等价丢弃,
// 防 panic)。format 是解析后的当前 format(空值由 WriteEnvelopeTo 按 json
// 处理);fields/jq 为命令解析出的 --fields/--jq 过滤条件。
func NewEmitter(stdout, stderr io.Writer, format Format, fields, jq string) *Emitter {
if stdout == nil {
stdout = io.Discard
}
if stderr == nil {
stderr = io.Discard
}
return &Emitter{stdout: stdout, stderr: stderr, format: format, fields: fields, jq: jq}
}
// Emit 按 env 的 outcome 把信封写到对应流(B26/B27/B28,契约规范 §5.1):
//
// - success / pending:完整信封落 stdout(B26,ok:true,§2.1/§2.2);
// - partial_failure:三通道明细落 stdout、stderr 零输出(B28,§2.3);
// - failure:信封落 stderr、stdout 严格零字节(B27,AC-11);
// - nil 信封由 WriteEnvelopeTo 降级为 failure,同样落 stderr。
//
// exit 0 语义标注(B32 收口,契约规范 §4/I2):Emit 的流路由与
// ExitCodeForEnvelope 的退出码语义一一对应——落 stdout 的信封
// (success/pending/partial_failure 中 ok:true 的前两者)对应 exit 0,
// 落 stderr 的 failure(含 nil 降级)对应非零退出码。Emitter 只负责写出,
// 不自行 os.Exit;进程退出码由 runner 出口按同一映射统一决定(§4 规则)。
//
// 失败信封恒以完整 JSON 信封渲染并绕过 format/fields/jq:失败信封没有数据
// 载荷(I3),§5.1 要求 stderr 上的失败信封恒为结构化信封;「非 json 只渲染
// data」的分发规则只适用于携带数据的信封。若 --jq 作用于失败信封会把错误
// 通道过滤为空,Agent 将丢失错误明细,故同样绕过。
func (em *Emitter) Emit(env *Envelope) error {
if env == nil || env.Outcome == OutcomeFailure {
return em.writeOnce(em.stderr, env, FormatJSON, "", "")
}
return em.writeOnce(em.stdout, env, em.format, em.fields, em.jq)
}
// writeOnce 先把信封完整渲染进内存 buffer,再一次性写出到 target(B25,
// 契约规范 §5.2 末条「渲染先进 buffer 再一次性写出」):渲染失败不向 target
// 泄漏任何字节;写出失败原子中止——writer 报错即原样返回,短写(接受了部分
// 字节却未写全)报 io.ErrShortWrite,绝不把截断输出当成功。渲染统一走
// renderEnvelope 分发点(B37),csv/ndjson 的分页诊断行(B40)以 em.stderr
// 为诊断出口。
func (em *Emitter) writeOnce(target io.Writer, env *Envelope, format Format, fields, jq string) error {
if env == nil {
env = nilFallbackEnvelope()
}
var buf bytes.Buffer
if err := renderEnvelope(&buf, em.stderr, env, format, fields, jq); err != nil {
return err
}
p := buf.Bytes()
n, err := target.Write(p)
if err != nil {
return err
}
if n < len(p) {
return io.ErrShortWrite
}
return nil
}
// ParseFormat 把字符串形式的 format 值(来自 --format flag 或 caller 接口)
// 归一化为 Format 常量;大小写不敏感,未知/空值返回 fallback。它是不持有
// *cobra.Command 的调用方(如主漏斗 callMCPToolInternalOpts)的 ResolveFormat
// 等价物,二者共用同一归一化规则。
func ParseFormat(raw string, fallback Format) Format {
return normalizeFormat(raw, fallback)
}
// ---------------------------------------------------------------------------
// 退出码映射(B32/B33/B34):outcome / error 类别 → exit code
// (契约规范 §4;不变量 I2 —— exit code == 0 ⇔ ok == true)
// ---------------------------------------------------------------------------
// 退出码表取值对齐:契约规范 §4 × 规划 v1.2 决策(OQ-1 已关闭——保留现行
// 码表 api=1 / auth=2 / validation=3 / PAT=4 / internal=5 / discovery=6,
// 仅新增 partial_failure 专用码 7;草案表中 validation→2 / confirmation→3
// 的拆分属 wire 破坏性重排,放弃)。confirmation_required 是 failure 的子类,
// 与 validation 共享 rc=3、以 subtype 区分(AC-13;runtime 同源于
// internal/errors 的 ExitCode 表)。
const (
exitCodeOK = 0
exitCodeAPI = 1
exitCodeAuth = 2
exitCodeValidation = 3 // confirmation_required 子类共享此码(OQ-1 定案)
exitCodePermission = 4 // PAT / permission failures
exitCodeInternal = 5
exitCodeDiscovery = 6
exitCodePartial = 7 // partial_failure 专用(契约 §4;规划 WS2 第4项;B142 将在 errors 侧补同源常量)
)
// subtypeConfirmationRequired 是门禁拦截的 failure 子类标记(契约规范 §2.4),
// 与 runtime apperrors.WithReason("confirmation_required") 形态同源。
const subtypeConfirmationRequired = "confirmation_required"
// ExitCodeForEnvelope 计算信封对应的进程退出码(B33,AC-10/I2;契约规范 §4)。
// 纯函数:只读信封字段、不做任何 I/O。映射规则:
//
// - success / pending → 0(I2:exit 0 ⇔ ok;异步受理不是失败,§1/§4);
// - partial_failure → 7(部分成功专用码,区别于普通失败,§4);
// - failure → 按 Error.Type 类别映射,confirmation_required 子类恒 3;
// - nil 信封 → 5(与 WriteEnvelopeTo 的 internal 类兜底信封同源);
// - 非法 outcome → 5(非法信封属框架缺陷,归 internal)。
//
// 判定只看 outcome / error 类别,不看可能被命令层篡改的 OK 字段(与 IsOK
// 同口径,I1)。退出码由 runner 出口统一决定、命令层禁止自行 os.Exit
// (§4 规则);本函数为 Emitter 语义标注、runner 映射与测试提供同一依据。
func ExitCodeForEnvelope(env *Envelope) int {
if env == nil {
return exitCodeInternal
}
switch env.Outcome {
case OutcomeSuccess, OutcomePending:
return exitCodeOK
case OutcomePartialFailure:
return exitCodePartial
case OutcomeFailure:
return exitCodeForErrorInfo(env.Error)
default:
return exitCodeInternal
}
}
// exitCodeForErrorInfo 把失败信封的 error 类别映射为退出码(契约规范 §4)。
// subtype=confirmation_required 优先于 type:门禁拦截无论挂在哪个 type 下
// 都恒为 3(AC-13)。error 缺席(违反 I3 的形态)按 internal 处理——
// 没有明细可分支的失败不得假装可恢复。未知 type 归 internal(不应发生)。
func exitCodeForErrorInfo(info *ErrorInfo) int {
if info == nil {
return exitCodeInternal
}
if info.Subtype == subtypeConfirmationRequired {
return exitCodeValidation
}
switch info.Type {
case "api":
return exitCodeAPI
case "auth":
return exitCodeAuth
case "validation":
return exitCodeValidation
case "permission":
return exitCodePermission
case "discovery":
return exitCodeDiscovery
default:
return exitCodeInternal
}
}