Files
dingtalk-workspace-cli/internal/event/consume/formatter.go
aa6abc5ed6 feat(event): add personal event subscriptions (#589)
* dws event

* fix consume fail

* test: add stream ticket injection probe

* feat: add portal ticket stream mode

* user event

* fix: allow portal ticket normal without app secret

* event

* user event

* eventType filter

* refactor(event): 优化IPC端点路径处理和改进相关测试

- 用dwsevent.IPCEndpoint替代原先根据GOOS判断的路径逻辑
- 新增event包实现Unix socket路径长度限制及长路径fallback机制
- 添加endpoint_test.go覆盖路径短长及唯一性的单元测试
- 修改busctl模块使用统一的IPC端点获取方法,避免重复实现
- transport_unix.go新增checkSocketPath函数检查路径长度,防止EINVAL错误
- 在监听和连接Unix socket时加入路径限制检查,提升错误明晰度
- 去除多个文件中无用的runtime导入,简化代码依赖

* opt

* event skill

* default value

* install script event

* fix: remove subscribe id event fanout filter

* fix(personal): 修正指定发送人消息描述错误

* more im event

* filter subId

* fix: align personal event schema with stream payload

* fix: avoid duplicate app helper name

* feat: simplify personal event schemas

* feat: simplify event schema output

* docs: refine dingtalk event skill references

* feat: align personal event consume flags

* fix event stop and status visibility

* hide app event public entrypoints

* hide incomplete personal sender event

* remove external event reference comments

* chore(event): prepare official release

* fix(event): harden personal stream lifecycle

---------

Co-authored-by: 玉澜 <yulan.wqy@alibaba-inc.com>
Co-authored-by: zhengyubai <zhengyubai618@gmail.com>
2026-07-10 17:54:43 +08:00

158 lines
5.5 KiB
Go

// 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 consume
import (
"encoding/json"
"fmt"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event/registry"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event/transport"
)
// Format identifies the wire shape `dws event consume` writes per event.
// Values mirror dws's global -f/--format flag vocabulary (defined in
// internal/output) with the subset that makes sense for streaming.
type Format string
const (
// FormatNDJSON is the default: one compact JSON object per line.
// Pipe-friendly; one event per `read` line. Recommended for agents.
FormatNDJSON Format = "ndjson"
// FormatJSON pretty-prints each event as multi-line JSON. NOT a
// JSON array — still NDJSON-style (one document per output unit),
// just with indentation. See plan §3.1 输出约束 note about why we
// do not emit a JSON array for an unbounded stream.
FormatJSON Format = "json"
// FormatPretty is the same as FormatJSON in v1; reserved for future
// human-friendly colorisation. Kept distinct so we never silently
// degrade `--format pretty` to compact-ndjson.
FormatPretty Format = "pretty"
// FormatRaw writes only the SDK's original Data string (one per
// event, newline-terminated). Useful when piping into jq / a tool
// that wants the cloud payload verbatim without our envelope.
FormatRaw Format = "raw"
// FormatCompact runs the per-event-type compact processor (see
// registry.LookupProcessor) and emits one flattened JSON line.
FormatCompact Format = "compact"
)
// NormalizeFormat maps a raw flag value to a supported Format. Values
// outside the event command's supported set fall back to NDJSON with the
// fallback flag set true — callers SHOULD warn on stderr when fallback is
// true and the original value was non-empty (e.g. user passed
// --format table which has no meaning for an event stream).
//
// Empty input maps to NDJSON without a fallback warning.
func NormalizeFormat(raw string) (f Format, fellback bool) {
switch raw {
case "":
return FormatNDJSON, false
case string(FormatNDJSON):
return FormatNDJSON, false
case string(FormatJSON):
return FormatJSON, false
case string(FormatPretty):
return FormatPretty, false
case string(FormatRaw):
return FormatRaw, false
case string(FormatCompact):
return FormatCompact, false
default:
// Includes table/csv from the global -f vocabulary, plus any
// typo. Fall back to ndjson (the safe stream default) and let
// the caller stderr-WARN.
return FormatNDJSON, true
}
}
// Formatter renders a transport.Event into the byte stream the sink writes
// out. Implementations append their own line terminator when appropriate
// (NDJSON / Raw add '\n'; Pretty/JSON embed newlines in the JSON itself).
type Formatter interface {
Render(ev transport.Event) ([]byte, error)
}
// NewFormatter returns a Formatter for the given Format. Compact wraps
// registry.LookupProcessor so adding a new specialised compactor is just
// a registry-side change. Returns an error only if format is an internally
// unsupported value (defensive — NormalizeFormat guarantees the input is
// one of the constants).
func NewFormatter(format Format) (Formatter, error) {
switch format {
case FormatNDJSON:
return &ndjsonFormatter{}, nil
case FormatJSON, FormatPretty:
return &prettyFormatter{}, nil
case FormatRaw:
return &rawFormatter{}, nil
case FormatCompact:
return &compactFormatter{}, nil
default:
return nil, fmt.Errorf("consume: unsupported format %q", format)
}
}
// ndjsonFormatter encodes each Event as one compact JSON line + '\n'.
type ndjsonFormatter struct{}
func (ndjsonFormatter) Render(ev transport.Event) ([]byte, error) {
b, err := json.Marshal(ev)
if err != nil {
return nil, err
}
return append(b, '\n'), nil
}
// prettyFormatter encodes each Event as multi-line indented JSON + '\n'.
// json.MarshalIndent does not append a trailing newline; we add one so
// successive events are visually separated in the output.
type prettyFormatter struct{}
func (prettyFormatter) Render(ev transport.Event) ([]byte, error) {
b, err := json.MarshalIndent(ev, "", " ")
if err != nil {
return nil, err
}
return append(b, '\n'), nil
}
// rawFormatter writes ev.Data verbatim. If Data is already JSON it stays
// JSON; if it's some other string format it stays that. A trailing newline
// is appended so successive raw events are separable.
type rawFormatter struct{}
func (rawFormatter) Render(ev transport.Event) ([]byte, error) {
out := make([]byte, 0, len(ev.Data)+1)
out = append(out, ev.Data...)
if len(ev.Data) == 0 || ev.Data[len(ev.Data)-1] != '\n' {
out = append(out, '\n')
}
return out, nil
}
// compactFormatter dispatches to the registry per event_type and writes
// the flattened map as one compact JSON line + '\n'.
type compactFormatter struct{}
func (compactFormatter) Render(ev transport.Event) ([]byte, error) {
p := registry.LookupProcessor(ev.EventType)
v := p(ev)
b, err := json.Marshal(v)
if err != nil {
return nil, err
}
return append(b, '\n'), nil
}