package helpers import ( "encoding/json" "fmt" "io" "os" "strings" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output" ) // applyGlobalFilter applies the global --jq / --fields output filters to data // when either is set. Returns handled=true when it wrote the (filtered) output, // so callers skip their default JSON encoding. The filter helpers live in // internal/output (the same ones used by `dws api`); the helper Formatter used // by product commands previously ignored these flags, making them no-ops. func (f *Formatter) applyGlobalFilter(data any) (handled bool, err error) { if deps == nil || deps.Caller == nil { return false, nil } jq := strings.TrimSpace(deps.Caller.JQ()) fields := strings.TrimSpace(deps.Caller.Fields()) if jq == "" && fields == "" { return false, nil } format := output.Format(strings.TrimSpace(deps.Caller.Format())) if format == "" { format = output.FormatJSON } return true, output.WriteFiltered(f.w, format, data, fields, jq) } // Formatter provides output formatting compatible with the old Wukong CLI. type Formatter struct { w io.Writer errW io.Writer } func NewFormatter() *Formatter { return NewFormatterWithWriters(os.Stdout, os.Stderr) } // NewFormatterWithWriters 是注入式构造函数(B45,WS1 改动点3):数据流 w 与 // 诊断流 errW 均由调用方注入。nil 按默认进程流处理(w→os.Stdout、 // errW→os.Stderr),因此 NewFormatter 的既有行为不变,只是收口到本构造器。 func NewFormatterWithWriters(w, errW io.Writer) *Formatter { if w == nil { w = os.Stdout } if errW == nil { errW = os.Stderr } return &Formatter{w: w, errW: errW} } // SetWriters 运行期替换两个写入目标(B46,WS1 改动点3):deps.Out 的 writer // 不再是构造期一次性硬编码,而是可注入接缝。nil 侧保留当前值(支持单侧替换)。 // 既有的 deps.Out.w / deps.Out.errW 直接替换习惯不受影响。 func (f *Formatter) SetWriters(w, errW io.Writer) { if w != nil { f.w = w } if errW != nil { f.errW = errW } } // PrintJSON serializes data as pretty-printed JSON and writes it to the output stream. // Go 的 json.Encoder 默认开启 HTML 转义(SetEscapeHTML(true)),会将 &、<、> 分别 // 转义为 \u0026、\u003c、\u003e。对于大多数 CLI 输出场景这是安全的默认行为。 // 如果返回值中包含 URL 等不应被转义的内容,请使用 PrintJSONUnescaped。 func (f *Formatter) PrintJSON(data any) error { if handled, err := f.applyGlobalFilter(data); handled { return err } enc := json.NewEncoder(f.w) enc.SetIndent("", " ") return enc.Encode(data) } // PrintJSONUnescaped 与 PrintJSON 功能相同,但禁用了 HTML 转义。 // 适用于返回值中包含带查询参数的 URL(如预签名上传 URL)的场景, // 避免 & 被转义为 \u0026 导致 URL 无法直接使用。 // // 使用场景示例: // - minutes upload create 返回的 presignedUrl 包含多个 & 分隔的查询参数 // - 其他返回值中包含需要原样输出的 URL 的接口 // // 影响范围:仅在调用方显式选择时生效,不影响全局 PrintJSON 的行为。 func (f *Formatter) PrintJSONUnescaped(data any) error { if handled, err := f.applyGlobalFilter(data); handled { return err } enc := json.NewEncoder(f.w) enc.SetIndent("", " ") enc.SetEscapeHTML(false) return enc.Encode(data) } func (f *Formatter) PrintRaw(text string) { fmt.Fprint(f.w, text) if !strings.HasSuffix(text, "\n") { fmt.Fprintln(f.w) } } func runeWidth(s string) int { w := 0 for _, r := range s { if r >= 0x1100 && (r <= 0x115f || r == 0x2329 || r == 0x232a || (r >= 0x2e80 && r <= 0xa4cf && r != 0x303f) || (r >= 0xac00 && r <= 0xd7a3) || (r >= 0xf900 && r <= 0xfaff) || (r >= 0xfe10 && r <= 0xfe19) || (r >= 0xfe30 && r <= 0xfe6f) || (r >= 0xff00 && r <= 0xff60) || (r >= 0xffe0 && r <= 0xffe6) || (r >= 0x20000 && r <= 0x2fffd) || (r >= 0x30000 && r <= 0x3fffd)) { w += 2 } else { w++ } } return w } func padRight(s string, width int) string { rw := runeWidth(s) if rw >= width { return s } return s + strings.Repeat(" ", width-rw) } // PrintTable preserves the legacy formatter contract. Unified-result commands // bypass this formatter and use internal/output, so rollout does not silently // move bytes for commands that have not migrated yet. func (f *Formatter) PrintTable(headers []string, rows [][]string) { if len(rows) == 0 { return } widths := make([]int, len(headers)) for i, h := range headers { widths[i] = runeWidth(h) } for _, row := range rows { for i, cell := range row { if i < len(widths) && runeWidth(cell) > widths[i] { widths[i] = runeWidth(cell) } } } for i, h := range headers { fmt.Fprintf(f.w, "%s ", padRight(h, widths[i])) _ = i } fmt.Fprintln(f.w) for _, w := range widths { fmt.Fprintf(f.w, "%s ", strings.Repeat("-", w)) } fmt.Fprintln(f.w) for _, row := range rows { for i, cell := range row { if i < len(widths) { fmt.Fprintf(f.w, "%s ", padRight(cell, widths[i])) } } fmt.Fprintln(f.w) } fmt.Fprintf(f.w, "共 %d 条\n", len(rows)) } func (f *Formatter) PrintSuccess(msg string) { fmt.Fprintf(f.w, "[OK] %s\n", msg) } func (f *Formatter) PrintError(msg string) { fmt.Fprintf(f.w, "[ERROR] %s\n", msg) } func (f *Formatter) PrintWarning(msg string) { fmt.Fprintf(f.errW, "[WARN] %s\n", msg) } func (f *Formatter) PrintInfo(msg string) { fmt.Fprintf(f.w, "[INFO] %s\n", msg) } func (f *Formatter) PrintProgress(msg string) { fmt.Fprintf(f.errW, "%s\n", msg) } func (f *Formatter) PrintDim(msg string) { fmt.Fprintf(f.w, " %s\n", msg) } // printJSONSafeInfo keeps command progress out of stdout when the caller // requested JSON. A successful JSON command must leave stdout parseable; its // human-readable progress belongs on stderr instead. func printJSONSafeInfo(msg string) { if deps != nil && deps.Caller != nil && strings.EqualFold(strings.TrimSpace(deps.Caller.Format()), "json") { deps.Out.PrintProgress("[INFO] " + msg) return } deps.Out.PrintInfo(msg) } func (f *Formatter) PrintKeyValue(key, value string) { fmt.Fprintf(f.w, "%-16s%s\n", key+":", value) }