From cefcf5b40958253d7db3860961a8bb92a425d297 Mon Sep 17 00:00:00 2001 From: wxianfeng Date: Tue, 21 Jul 2026 21:25:10 +0800 Subject: [PATCH 1/2] fix(event): make flattened output opt-in --- CHANGELOG.md | 4 + README.md | 12 +- README_zh.md | 12 +- internal/app/coverage_edges_test.go | 4 +- internal/app/event_command.go | 11 +- internal/app/event_personal_command.go | 54 +++-- internal/app/event_personal_debug_test.go | 67 +++++- internal/app/event_personal_output_test.go | 49 +++- internal/cli/schema_agent_metadata/event.json | 30 +-- internal/cli/schema_agent_metadata/index.json | 2 +- internal/cli/schema_agent_metadata_audit.json | 2 +- internal/cli/schema_catalog.json | 218 ++++++++++++++++-- internal/cli/schema_hints/index.json | 5 + .../cli/schema_hints/reference-review.json | 5 + .../cli/schema_hints/selection/event.json | 10 +- internal/event/consume/run.go | 4 + internal/event/consume/validate.go | 1 + internal/event/consume/validate_test.go | 3 +- internal/event/personal/output.go | 36 +++ internal/event/personal/registry.go | 14 +- internal/event/personal/registry_test.go | 67 ++++-- skills/mono/SKILL.md | 4 +- skills/mono/references/products/event.md | 80 +++---- skills/multi/dingtalk-event/SKILL.md | 30 ++- .../dingtalk-event/references/event-im.md | 65 +++--- test/skill_tests.md | 30 +-- test/unit/skill_docs_policy_test.go | 2 +- 27 files changed, 629 insertions(+), 192 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b274ee9c..618d7ba3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/) and th ## [Unreleased] +### Changed + +- **Personal event output compatibility** — `event consume` once again preserves the transport envelope by default for `ndjson`/`json`/`pretty`, while retaining the existing `compact` processor. New Agent workflows opt into the event-specific top-level DTO with `--flatten`; `event schema --flatten` describes that DTO, while the default schema describes `type/event_type/data/headers` and points to `.data | fromjson`. + ## [1.0.53] - 2026-07-21 This release promotes the validated `v1.0.53-beta.7` baseline to stable. It adds enterprise onboarding, declarative shortcuts, Sheet/Aitable writes, multi-account profiles, and broader personal IM events, while hardening authentication and the guarded release path. diff --git a/README.md b/README.md index fde02fa1..121329a6 100644 --- a/README.md +++ b/README.md @@ -476,6 +476,8 @@ Env vars: `DWS_SKILL_MODE=mono|multi` (also honored by `install.sh` / `install.p `dws event consume` subscribes as the currently logged-in user over a managed Stream WebSocket and emits each event as one NDJSON line on stdout. The public catalog currently covers messages that mention the current user, one-to-one messages with a specified user, and messages in a specified group. +The default `ndjson`, `json`, and `pretty` output preserves the transport envelope (`type`, `event_type`, string `data`, and `headers`) for existing scripts; `compact` retains its existing processor. Add `--flatten` to emit the stable top-level business fields used by Agent workflows. `--format` controls JSON serialization; `--flatten` controls the data structure and cannot be combined with `-f raw` or `--debug-raw-events`. + > **Prerequisite**: run `dws auth login`. Personal identity is resolved from the OAuth token and cannot be supplied through command-line identity flags. For an event-focused installation, use the official convenience installer: @@ -487,19 +489,19 @@ curl -fsSL https://raw.githubusercontent.com/DingTalk-Real-AI/dingtalk-workspace ```bash # Inspect the public personal event catalog and schema dws event list -dws event schema user_im_message_receive_o2o +dws event schema user_im_message_receive_o2o --flatten # Listen for messages that mention the current user -dws event consume user_im_message_receive_at -f ndjson +dws event consume user_im_message_receive_at --flatten -f ndjson # Listen for one-to-one messages with a specified user -dws event consume user_im_message_receive_o2o --user -f ndjson +dws event consume user_im_message_receive_o2o --user --flatten -f ndjson # Listen by openDingtalkId (external contact, bot, or cross-organization identity) -dws event consume user_im_message_receive_o2o --open-dingtalk-id -f ndjson +dws event consume user_im_message_receive_o2o --open-dingtalk-id --flatten -f ndjson # Listen for messages in a specified group -dws event consume user_im_message_receive_group --group -f ndjson +dws event consume user_im_message_receive_group --group --flatten -f ndjson # Inspect local consumers and cancel a subscription dws event status diff --git a/README_zh.md b/README_zh.md index fa9c797f..31ee238f 100644 --- a/README_zh.md +++ b/README_zh.md @@ -470,6 +470,8 @@ DWS_SKILL_SOURCE=/path/to/skills dws skill setup --mode multi `dws event consume` 使用当前 OAuth 登录用户建立托管的 Stream WebSocket 长连接,并把每条事件以 NDJSON 一行输出到 stdout。当前公开目录包括:当前用户被 @ 的消息、与指定用户的单聊消息、指定群的消息。 +默认 `ndjson`、`json`、`pretty` 输出保留兼容 transport envelope(`type`、`event_type`、字符串 `data`、`headers`),`compact` 继续沿用原 processor。Agent 或新脚本显式加 `--flatten` 后,输出稳定的顶层业务字段。`--format` 控制 JSON 序列化,`--flatten` 控制数据结构,且不能与 `-f raw` 或 `--debug-raw-events` 同时使用。 + > **前置条件**:先运行 `dws auth login`。个人身份从 OAuth token 解析,不允许通过命令行伪造。 只需要 event 能力时,可以使用官方便捷安装脚本: @@ -481,19 +483,19 @@ curl -fsSL https://raw.githubusercontent.com/DingTalk-Real-AI/dingtalk-workspace ```bash # 查看公开个人事件目录和 schema dws event list -dws event schema user_im_message_receive_o2o +dws event schema user_im_message_receive_o2o --flatten # 监听当前用户被 @ 的消息 -dws event consume user_im_message_receive_at -f ndjson +dws event consume user_im_message_receive_at --flatten -f ndjson # 监听与指定用户的单聊消息 -dws event consume user_im_message_receive_o2o --user -f ndjson +dws event consume user_im_message_receive_o2o --user --flatten -f ndjson # 使用 openDingtalkId 监听外部联系人、机器人或跨组织身份 -dws event consume user_im_message_receive_o2o --open-dingtalk-id -f ndjson +dws event consume user_im_message_receive_o2o --open-dingtalk-id --flatten -f ndjson # 监听指定群的消息 -dws event consume user_im_message_receive_group --group -f ndjson +dws event consume user_im_message_receive_group --group --flatten -f ndjson # 查看本地 consume,并取消指定订阅 dws event status diff --git a/internal/app/coverage_edges_test.go b/internal/app/coverage_edges_test.go index 2ea62f75..75aa593c 100644 --- a/internal/app/coverage_edges_test.go +++ b/internal/app/coverage_edges_test.go @@ -1469,10 +1469,10 @@ func TestCrossPlatformCoveragePersonalEventPureCoverage(t *testing.T) { if !ok { t.Fatal("mention definition missing") } - if err := renderPersonalSchema(io.Discard, def, ""); err != nil { + if err := renderPersonalSchema(io.Discard, def, "", false); err != nil { t.Fatal(err) } - if err := renderPersonalSchema(io.Discard, def, "yaml"); err == nil { + if err := renderPersonalSchema(io.Discard, def, "yaml", true); err == nil { t.Fatal("unsupported schema format succeeded") } for _, key := range []string{"", "unknown", personal.EventMention, personal.EventFromUser} { diff --git a/internal/app/event_command.go b/internal/app/event_command.go index b1235b7e..59618d4e 100644 --- a/internal/app/event_command.go +++ b/internal/app/event_command.go @@ -112,6 +112,7 @@ func newEventConsumeCommand() *cobra.Command { dryRun bool foreground bool asIdentity string + flatten bool personalOpts personalConsumeOptions streamOpts eventStreamTicketOptions ) @@ -127,7 +128,11 @@ func newEventConsumeCommand() *cobra.Command { json 每事件多行美化 JSON(必须配 --max-events 或 --duration) pretty 同 json,未来加颜色 raw 仅 SDK 原始 payload,无外层封装 - compact 扁平化 + 解析嵌套 + 抽取语义字段(Agent 友好) + compact 单行紧凑 JSON;不传 --flatten 时沿用原 compact processor + +数据结构: + ndjson/json/pretty 默认保持 transport envelope(type/event_type/data/headers) + --flatten 结构化格式输出稳定的顶层业务字段,适合 Agent / 脚本直接消费 默认使用当前 OAuth 登录态自动创建/复用个人订阅并建立个人长连接;非默认组织加 --profile。连上后 stderr 打就绪行 [event] ready,等它出现再读 stdout;停机用 @@ -144,6 +149,7 @@ SIGTERM、关 stdin,或先用 dws event stop --dry-run 预览 } if as == "user" { personalOpts.EventKey = firstArg(args) + personalOpts.Flatten = flatten personalOpts.Common = commonConsumeOptions{ EventTypes: eventTypes, Filter: filter, @@ -167,6 +173,7 @@ SIGTERM、关 stdin,或先用 dws event stop --dry-run 预览 return fmt.Errorf("event consume: --debug-raw-events is only supported with --as user") } if err := rejectChangedFlags(c, "user", + "flatten", "subscribe-id", "rule", "name", @@ -280,6 +287,8 @@ SIGTERM、关 stdin,或先用 dws event stop --dry-run 预览 "提示 bus 客户端期望 compact 渲染(语义透传,bus 仍按原 payload 投递)") f.StringVarP(&formatRaw, "format", "f", "ndjson", "输出格式 (ndjson/json/pretty/raw/compact);事件流默认 ndjson") + f.BoolVar(&flatten, "flatten", false, + "将个人事件 transport envelope 投影为稳定的顶层业务字段") f.StringVar(&outputDir, "output-dir", "", "每事件写一个文件到该目录 ({type}_{id}_{ts}.json);与 stdout 互斥") f.StringArrayVar(&routesRaw, "route", nil, diff --git a/internal/app/event_personal_command.go b/internal/app/event_personal_command.go index 35de721d..55cc9793 100644 --- a/internal/app/event_personal_command.go +++ b/internal/app/event_personal_command.go @@ -61,6 +61,7 @@ type commonConsumeOptions struct { type personalConsumeOptions struct { Common commonConsumeOptions EventKey string + Flatten bool DebugRawEvents bool SubscribeID string Rule string @@ -140,6 +141,7 @@ var ( func newEventSchemaCommand() *cobra.Command { var asIdentity string var formatRaw string + var flatten bool cmd := &cobra.Command{ Use: "schema ", Short: "显示事件 schema", @@ -157,11 +159,12 @@ func newEventSchemaCommand() *cobra.Command { if !def.Public { return personal.PublicAvailabilityError(args[0]) } - return renderPersonalSchema(c.OutOrStdout(), def, formatRaw) + return renderPersonalSchema(c.OutOrStdout(), def, formatRaw, flatten) }, } cmd.Flags().StringVar(&asIdentity, "as", "user", "事件身份: user") cmd.Flags().StringVarP(&formatRaw, "format", "f", "json", "输出格式: json") + cmd.Flags().BoolVar(&flatten, "flatten", false, "显示 --flatten 消费模式对应的顶层业务字段 schema") hideEventInternalFlags(cmd, "as") cli.AnnotateRuntimePositionals(cmd, cli.RuntimeSchemaPositional{ Name: "event_key", @@ -189,7 +192,7 @@ func runPersonalEventList(c *cobra.Command, opts personalListOptions) error { return tw.Flush() } -func renderPersonalSchema(w io.Writer, def personal.Definition, format string) error { +func renderPersonalSchema(w io.Writer, def personal.Definition, format string, flatten bool) error { format = strings.ToLower(strings.TrimSpace(format)) if format == "" { format = "json" @@ -199,7 +202,7 @@ func renderPersonalSchema(w io.Writer, def personal.Definition, format string) e } enc := json.NewEncoder(w) enc.SetIndent("", " ") - return enc.Encode(personal.BuildSchemaDocument(def)) + return enc.Encode(personal.BuildSchemaDocumentForMode(def, flatten)) } func runPersonalEventConsume(c *cobra.Command, opts personalConsumeOptions) error { @@ -207,6 +210,19 @@ func runPersonalEventConsume(c *cobra.Command, opts personalConsumeOptions) erro if err := ensurePublicPersonalEvent(opts.EventKey); err != nil { return err } + rawFormat := "" + if f := c.Flags().Lookup("format"); f != nil && f.Changed { + rawFormat = opts.Common.FormatRaw + } + normalised, fellback := consume.NormalizeFormat(rawFormat) + if fellback && !opts.Common.Quiet { + fmt.Fprintf(c.ErrOrStderr(), "WARN: --format %q has no meaning for event stream; using ndjson\n", rawFormat) + } + if err := validatePersonalEventOutputMode(opts.Flatten, opts.DebugRawEvents, normalised); err != nil { + return fmt.Errorf("event consume --as user: %w", err) + } + projector := personalEventProjector(opts.DebugRawEvents, opts.Flatten) + configDir := defaultConfigDir() identity, err := personalResolveEventIdentity(ctx, configDir, opts.StreamSourceID) if err != nil { @@ -221,16 +237,6 @@ func runPersonalEventConsume(c *cobra.Command, opts personalConsumeOptions) erro if err != nil { return fmt.Errorf("event consume --as user: %w", err) } - rawFormat := "" - if f := c.Flags().Lookup("format"); f != nil && f.Changed { - rawFormat = opts.Common.FormatRaw - } - normalised, fellback := consume.NormalizeFormat(rawFormat) - if fellback && !opts.Common.Quiet { - fmt.Fprintf(c.ErrOrStderr(), "WARN: --format %q has no meaning for event stream; using ndjson\n", rawFormat) - } - projector := personalEventProjector(opts.DebugRawEvents) - if opts.Common.DryRun { if strings.TrimSpace(opts.SubscribeID) == "" { if err := validatePersonalSubscriptionOptions(opts); err != nil { @@ -247,6 +253,7 @@ func runPersonalEventConsume(c *cobra.Command, opts personalConsumeOptions) erro Duration: opts.Common.Duration, EventKey: opts.EventKey, Format: normalised, + Flatten: opts.Flatten, OutputDir: opts.Common.OutputDir, Routes: routes, Projector: projector, @@ -303,6 +310,7 @@ func runPersonalEventConsume(c *cobra.Command, opts personalConsumeOptions) erro Duration: opts.Common.Duration, EventKey: eventKey, Format: normalised, + Flatten: opts.Flatten, OutputDir: opts.Common.OutputDir, Routes: routes, Projector: projector, @@ -366,11 +374,27 @@ func runPersonalEventConsume(c *cobra.Command, opts personalConsumeOptions) erro return err } -func personalEventProjector(debugRawEvents bool) consume.Projector { +func personalEventProjector(debugRawEvents, flatten bool) consume.Projector { if debugRawEvents { return func(ev transport.Event) (any, error) { return ev, nil } } - return personal.ProjectOutput + if flatten { + return personal.ProjectOutput + } + return nil +} + +func validatePersonalEventOutputMode(flatten, debugRawEvents bool, format consume.Format) error { + if !flatten { + return nil + } + if debugRawEvents { + return fmt.Errorf("--flatten and --debug-raw-events are mutually exclusive") + } + if format == consume.FormatRaw { + return fmt.Errorf("--flatten and --format raw are mutually exclusive") + } + return nil } func applyPersonalConsumeFilters(cfg *consume.Config, opts personalConsumeOptions, subscribeID, eventKey string) { diff --git a/internal/app/event_personal_debug_test.go b/internal/app/event_personal_debug_test.go index 8ccf7b12..f5f8cac4 100644 --- a/internal/app/event_personal_debug_test.go +++ b/internal/app/event_personal_debug_test.go @@ -20,6 +20,7 @@ import ( "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event/consume" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event/personal" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event/transport" + "github.com/spf13/cobra" ) func TestApplyPersonalConsumeFiltersDebugRawEvents(t *testing.T) { @@ -52,11 +53,14 @@ func TestApplyPersonalConsumeFiltersDefault(t *testing.T) { } } -func TestPersonalEventProjectorUsesRawEnvelopeForDebug(t *testing.T) { - if personalEventProjector(false) == nil { - t.Fatal("normal personal consume projector = nil") +func TestPersonalEventProjectorSelectsExplicitModes(t *testing.T) { + if personalEventProjector(false, false) != nil { + t.Fatal("default personal consume should preserve transport envelope") } - projector := personalEventProjector(true) + if personalEventProjector(false, true) == nil { + t.Fatal("flatten personal consume projector = nil") + } + projector := personalEventProjector(true, false) if projector == nil { t.Fatal("debug raw personal consume projector = nil") } @@ -74,6 +78,61 @@ func TestPersonalEventProjectorUsesRawEnvelopeForDebug(t *testing.T) { } } +func TestEventConsumeFlattenRejectsRawModesBeforeIdentityResolution(t *testing.T) { + for _, tc := range []struct { + name string + args []string + want string + }{ + { + name: "raw format", + args: []string{personal.EventMention, "--flatten", "--format", "raw"}, + want: "--flatten and --format raw are mutually exclusive", + }, + { + name: "raw debug", + args: []string{personal.EventMention, "--flatten", "--debug-raw-events"}, + want: "--flatten and --debug-raw-events are mutually exclusive", + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Setenv("DWS_CONFIG_DIR", t.TempDir()) + cmd := newEventConsumeCommand() + cmd.SilenceUsage = true + cmd.SilenceErrors = true + cmd.SetArgs(tc.args) + err := cmd.Execute() + if err == nil || !strings.Contains(err.Error(), tc.want) { + t.Fatalf("Execute() error = %v, want %q", err, tc.want) + } + if strings.Contains(err.Error(), "login") || strings.Contains(err.Error(), "token") { + t.Fatalf("output-mode validation ran after identity resolution: %v", err) + } + }) + } +} + +func TestEventConsumeFlattenFlagIsForwarded(t *testing.T) { + oldRun := eventRunPersonalConsume + t.Cleanup(func() { eventRunPersonalConsume = oldRun }) + + var got personalConsumeOptions + eventRunPersonalConsume = func(_ *cobra.Command, opts personalConsumeOptions) error { + got = opts + return nil + } + cmd := newEventConsumeCommand() + cmd.SilenceUsage = true + cmd.SilenceErrors = true + cmd.SetArgs([]string{personal.EventMention, "--flatten", "--format", "compact"}) + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() error = %v", err) + } + if !got.Flatten || got.Common.FormatRaw != "compact" { + t.Fatalf("forwarded options = %#v", got) + } +} + func TestEventConsumeDebugRawEventsRequiresUserMode(t *testing.T) { cmd := newEventConsumeCommand() cmd.SilenceUsage = true diff --git a/internal/app/event_personal_output_test.go b/internal/app/event_personal_output_test.go index f1d7af51..2d30c31e 100644 --- a/internal/app/event_personal_output_test.go +++ b/internal/app/event_personal_output_test.go @@ -188,7 +188,44 @@ func TestPersonalEventSchemaHidesSchemaIDs(t *testing.T) { } } -func TestPersonalEventSchemaUsesSingleJSONSchema(t *testing.T) { +func TestPersonalEventSchemaDefaultsToTransportEnvelope(t *testing.T) { + cmd := newEventSchemaCommand() + cmd.SilenceUsage = true + cmd.SilenceErrors = true + var out bytes.Buffer + cmd.SetOut(&out) + cmd.SetArgs([]string{personal.EventSingleChat}) + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() error = %v", err) + } + var doc map[string]any + if err := json.Unmarshal(out.Bytes(), &doc); err != nil { + t.Fatalf("schema output is not JSON: %v\n%s", err, out.String()) + } + if doc["jq_root_path"] != ".data | fromjson" { + t.Fatalf("jq_root_path = %#v, want .data | fromjson", doc["jq_root_path"]) + } + schema, ok := doc["schema"].(map[string]any) + if !ok { + t.Fatalf("schema = %#v, want object", doc["schema"]) + } + props, ok := schema["properties"].(map[string]any) + if !ok { + t.Fatalf("schema.properties = %#v, want object", schema["properties"]) + } + for _, field := range []string{"type", "seq", "event_type", "data", "headers", "subscribe_id"} { + if _, ok := props[field]; !ok { + t.Fatalf("default envelope schema missing %q: %#v", field, props) + } + } + for _, field := range []string{"content", "sender", "conversation_id", "timestamp"} { + if _, ok := props[field]; ok { + t.Fatalf("default envelope schema unexpectedly contains flat field %q", field) + } + } +} + +func TestPersonalEventFlattenedSchemaUsesSingleJSONSchema(t *testing.T) { for _, eventKey := range []string{ personal.EventMention, personal.EventSingleChat, @@ -200,7 +237,7 @@ func TestPersonalEventSchemaUsesSingleJSONSchema(t *testing.T) { cmd.SilenceErrors = true var out bytes.Buffer cmd.SetOut(&out) - cmd.SetArgs([]string{eventKey}) + cmd.SetArgs([]string{eventKey, "--flatten"}) if err := cmd.Execute(); err != nil { t.Fatalf("Execute() error = %v", err) } @@ -316,7 +353,7 @@ func TestPersonalActionEventSchemaMatchesFlatOutput(t *testing.T) { cmd.SilenceErrors = true var out bytes.Buffer cmd.SetOut(&out) - cmd.SetArgs([]string{eventKey}) + cmd.SetArgs([]string{eventKey, "--flatten"}) if err := cmd.Execute(); err != nil { t.Fatal(err) } @@ -367,6 +404,9 @@ func TestEventSchemaDefaultsToUser(t *testing.T) { if doc["event_key"] != personal.EventSingleChat { t.Fatalf("event_key = %#v, want %s", doc["event_key"], personal.EventSingleChat) } + if doc["jq_root_path"] != ".data | fromjson" { + t.Fatalf("jq_root_path = %#v, want default envelope path", doc["jq_root_path"]) + } } func TestPersonalEventFromUserIsPubliclyAvailable(t *testing.T) { @@ -469,6 +509,9 @@ func TestEventConsumeCobraSchemaIncludesOpenDingTalkID(t *testing.T) { if _, ok := params["odid"]; ok { t.Fatalf("schema parameters unexpectedly include odid alias: %#v", params) } + if _, ok := params["flatten"]; !ok { + t.Fatalf("schema parameters missing flatten: %#v", params) + } for _, name := range []string{"user", "open-dingtalk-id", "group"} { param, ok := params[name].(map[string]any) if !ok { diff --git a/internal/cli/schema_agent_metadata/event.json b/internal/cli/schema_agent_metadata/event.json index d4d83127..a1730463 100644 --- a/internal/cli/schema_agent_metadata/event.json +++ b/internal/cli/schema_agent_metadata/event.json @@ -2,7 +2,7 @@ "product_id": "event", "tools": { "event consume": { - "agent_summary": "订阅并持续消费指定个人事件,输出 NDJSON 事件流", + "agent_summary": "订阅并持续消费指定个人事件;Agent 使用 --flatten 输出顶层业务 NDJSON", "agent_summary_source": "dws-agent-selection/event", "availability": "available", "avoid_when": [ @@ -13,19 +13,19 @@ "effect": "write", "effect_source": "agent-hint", "examples": [ - "dws event consume user_im_message_receive_user --open-dingtalk-id open-example --max-events 1 --format ndjson", - "dws event consume user_im_message_reaction_group --group cid-example --max-events 1 --format ndjson" + "dws event consume user_im_message_receive_user --open-dingtalk-id open-example --flatten --max-events 1 --format ndjson", + "dws event consume user_im_message_reaction_group --group cid-example --flatten --max-events 1 --format ndjson" ], "field_provenance": { "agent_summary": { - "value": "订阅并持续消费指定个人事件,输出 NDJSON 事件流", + "value": "订阅并持续消费指定个人事件;Agent 使用 --flatten 输出顶层业务 NDJSON", "source": "internal/cli/schema_hints/selection/event.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", "review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。", "candidates": [ { - "value": "订阅并持续消费指定个人事件,输出 NDJSON 事件流", + "value": "订阅并持续消费指定个人事件;Agent 使用 --flatten 输出顶层业务 NDJSON", "source": "internal/cli/schema_hints/selection/event.json", "precedence": "reviewed_explicit", "selected": true, @@ -105,8 +105,8 @@ }, "examples": { "value": [ - "dws event consume user_im_message_receive_user --open-dingtalk-id open-example --max-events 1 --format ndjson", - "dws event consume user_im_message_reaction_group --group cid-example --max-events 1 --format ndjson" + "dws event consume user_im_message_receive_user --open-dingtalk-id open-example --flatten --max-events 1 --format ndjson", + "dws event consume user_im_message_reaction_group --group cid-example --flatten --max-events 1 --format ndjson" ], "source": "internal/cli/schema_hints/selection/event.json", "precedence": "reviewed_explicit", @@ -115,8 +115,8 @@ "candidates": [ { "value": [ - "dws event consume user_im_message_receive_user --open-dingtalk-id open-example --max-events 1 --format ndjson", - "dws event consume user_im_message_reaction_group --group cid-example --max-events 1 --format ndjson" + "dws event consume user_im_message_receive_user --open-dingtalk-id open-example --flatten --max-events 1 --format ndjson", + "dws event consume user_im_message_reaction_group --group cid-example --flatten --max-events 1 --format ndjson" ], "source": "internal/cli/schema_hints/selection/event.json", "precedence": "reviewed_explicit", @@ -538,7 +538,7 @@ ] }, "event schema": { - "agent_summary": "查询指定个人事件码的 payload 字段结构", + "agent_summary": "查询指定个人事件码的输出字段结构;Agent 应查询 --flatten 模式", "agent_summary_source": "dws-agent-selection/event", "availability": "available", "avoid_when": [ @@ -549,18 +549,18 @@ "effect": "read", "effect_source": "agent-hint", "examples": [ - "dws event schema user_im_message_receive_at --format json" + "dws event schema user_im_message_receive_at --flatten --format json" ], "field_provenance": { "agent_summary": { - "value": "查询指定个人事件码的 payload 字段结构", + "value": "查询指定个人事件码的输出字段结构;Agent 应查询 --flatten 模式", "source": "internal/cli/schema_hints/selection/event.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", "review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。", "candidates": [ { - "value": "查询指定个人事件码的 payload 字段结构", + "value": "查询指定个人事件码的输出字段结构;Agent 应查询 --flatten 模式", "source": "internal/cli/schema_hints/selection/event.json", "precedence": "reviewed_explicit", "selected": true, @@ -640,7 +640,7 @@ }, "examples": { "value": [ - "dws event schema user_im_message_receive_at --format json" + "dws event schema user_im_message_receive_at --flatten --format json" ], "source": "internal/cli/schema_hints/selection/event.json", "precedence": "reviewed_explicit", @@ -649,7 +649,7 @@ "candidates": [ { "value": [ - "dws event schema user_im_message_receive_at --format json" + "dws event schema user_im_message_receive_at --flatten --format json" ], "source": "internal/cli/schema_hints/selection/event.json", "precedence": "reviewed_explicit", diff --git a/internal/cli/schema_agent_metadata/index.json b/internal/cli/schema_agent_metadata/index.json index b758fbbb..d231e458 100644 --- a/internal/cli/schema_agent_metadata/index.json +++ b/internal/cli/schema_agent_metadata/index.json @@ -1,6 +1,6 @@ { "version": 1, - "source_hash": "sha256:de587cba5051dd4c2715353012d8d9f2a7c5208a0c6dee4ea703cfc97b7351f0", + "source_hash": "sha256:5df496973c41b3b4f7ae8c856ff0e9e99bcabd4455419b7d41bb23c44df6f1af", "surface_hash": "sha256:7ef588f38052f0104e027c8daff5fefd68698781f2c87715461183d4058288ed", "coverage": { "surface_products": 22, diff --git a/internal/cli/schema_agent_metadata_audit.json b/internal/cli/schema_agent_metadata_audit.json index 95387b37..a4367aef 100644 --- a/internal/cli/schema_agent_metadata_audit.json +++ b/internal/cli/schema_agent_metadata_audit.json @@ -1,6 +1,6 @@ { "version": 1, - "source_hash": "sha256:de587cba5051dd4c2715353012d8d9f2a7c5208a0c6dee4ea703cfc97b7351f0", + "source_hash": "sha256:5df496973c41b3b4f7ae8c856ff0e9e99bcabd4455419b7d41bb23c44df6f1af", "surface_hash": "sha256:7ef588f38052f0104e027c8daff5fefd68698781f2c87715461183d4058288ed", "source_files": 150, "hint_files": 46, diff --git a/internal/cli/schema_catalog.json b/internal/cli/schema_catalog.json index 8619f9e4..6ed5c588 100644 --- a/internal/cli/schema_catalog.json +++ b/internal/cli/schema_catalog.json @@ -1,12 +1,12 @@ { "version": 1, - "source_hash": "sha256:522d4b43cf13f07430a407319a772cd11e9b1f268f8d98b4d68bdb385e4e585b", + "source_hash": "sha256:14398788381c822f208e8cae059d98cd60da6625023869c74015735650d67e6f", "surface_hash": "sha256:7ef588f38052f0104e027c8daff5fefd68698781f2c87715461183d4058288ed", "catalog": { "agent_metadata": { "products_with_metadata": 22, "source": "embedded-skill-metadata", - "source_hash": "sha256:de587cba5051dd4c2715353012d8d9f2a7c5208a0c6dee4ea703cfc97b7351f0", + "source_hash": "sha256:5df496973c41b3b4f7ae8c856ff0e9e99bcabd4455419b7d41bb23c44df6f1af", "surface_hash": "sha256:7ef588f38052f0104e027c8daff5fefd68698781f2c87715461183d4058288ed", "surface_products": 22, "surface_tools": 572, @@ -12178,7 +12178,7 @@ "tools": [ { "agent_metadata_source": "embedded-skill-metadata", - "agent_summary": "订阅并持续消费指定个人事件,输出 NDJSON 事件流", + "agent_summary": "订阅并持续消费指定个人事件;Agent 使用 --flatten 输出顶层业务 NDJSON", "agent_summary_source": "dws-agent-selection/event", "availability": "available", "avoid_when": [ @@ -12189,7 +12189,7 @@ "cli_name": "consume", "cli_path": "event consume", "confirmation": "not_required", - "description": "订阅 DingTalk 个人事件并将每条事件以 NDJSON 输出到 stdout。\n\n输出格式(事件流默认 ndjson;显式 -f json/pretty/raw 可覆盖;-f table/csv 对\n事件流无意义会 fallback 到 ndjson):\n ndjson (默认) 一行一对象,适合 jq / 管道处理\n json 每事件多行美化 JSON(必须配 --max-events 或 --duration)\n pretty 同 json,未来加颜色\n raw 仅 SDK 原始 payload,无外层封装\n compact 扁平化 + 解析嵌套 + 抽取语义字段(Agent 友好)\n\n默认使用当前 OAuth 登录态自动创建/复用个人订阅并建立个人长连接;非默认组织加\n--profile。连上后 stderr 打就绪行 [event] ready,等它出现再读 stdout;停机用\nSIGTERM、关 stdin,或先用 dws event stop \u003csubscribe_id\u003e --dry-run 预览、确认后加\n--yes,绝不要 kill -9。\n--event-types/--filter 只影响本地 bus → consume 这一段投递;普通个人事件消费\n通常不需要设置。", + "description": "订阅 DingTalk 个人事件并将每条事件以 NDJSON 输出到 stdout。\n\n输出格式(事件流默认 ndjson;显式 -f json/pretty/raw 可覆盖;-f table/csv 对\n事件流无意义会 fallback 到 ndjson):\n ndjson (默认) 一行一对象,适合 jq / 管道处理\n json 每事件多行美化 JSON(必须配 --max-events 或 --duration)\n pretty 同 json,未来加颜色\n raw 仅 SDK 原始 payload,无外层封装\n compact 单行紧凑 JSON;不传 --flatten 时沿用原 compact processor\n\n数据结构:\n ndjson/json/pretty 默认保持 transport envelope(type/event_type/data/headers)\n --flatten 结构化格式输出稳定的顶层业务字段,适合 Agent / 脚本直接消费\n\n默认使用当前 OAuth 登录态自动创建/复用个人订阅并建立个人长连接;非默认组织加\n--profile。连上后 stderr 打就绪行 [event] ready,等它出现再读 stdout;停机用\nSIGTERM、关 stdin,或先用 dws event stop \u003csubscribe_id\u003e --dry-run 预览、确认后加\n--yes,绝不要 kill -9。\n--event-types/--filter 只影响本地 bus → consume 这一段投递;普通个人事件消费\n通常不需要设置。", "effect": "write", "idempotency": "non_idempotent", "interface_mode": "composite", @@ -12234,7 +12234,7 @@ }, { "agent_metadata_source": "embedded-skill-metadata", - "agent_summary": "查询指定个人事件码的 payload 字段结构", + "agent_summary": "查询指定个人事件码的输出字段结构;Agent 应查询 --flatten 模式", "agent_summary_source": "dws-agent-selection/event", "availability": "available", "avoid_when": [ @@ -290130,7 +290130,7 @@ "skills/mono/SKILL.md", "skills/mono/references/products/event.md" ], - "agent_summary": "订阅并持续消费指定个人事件,输出 NDJSON 事件流", + "agent_summary": "订阅并持续消费指定个人事件;Agent 使用 --flatten 输出顶层业务 NDJSON", "agent_summary_source": "dws-agent-selection/event", "availability": "available", "avoid_when": [ @@ -290149,13 +290149,13 @@ ] ] }, - "description": "订阅 DingTalk 个人事件并将每条事件以 NDJSON 输出到 stdout。\n\n输出格式(事件流默认 ndjson;显式 -f json/pretty/raw 可覆盖;-f table/csv 对\n事件流无意义会 fallback 到 ndjson):\n ndjson (默认) 一行一对象,适合 jq / 管道处理\n json 每事件多行美化 JSON(必须配 --max-events 或 --duration)\n pretty 同 json,未来加颜色\n raw 仅 SDK 原始 payload,无外层封装\n compact 扁平化 + 解析嵌套 + 抽取语义字段(Agent 友好)\n\n默认使用当前 OAuth 登录态自动创建/复用个人订阅并建立个人长连接;非默认组织加\n--profile。连上后 stderr 打就绪行 [event] ready,等它出现再读 stdout;停机用\nSIGTERM、关 stdin,或先用 dws event stop \u003csubscribe_id\u003e --dry-run 预览、确认后加\n--yes,绝不要 kill -9。\n--event-types/--filter 只影响本地 bus → consume 这一段投递;普通个人事件消费\n通常不需要设置。", + "description": "订阅 DingTalk 个人事件并将每条事件以 NDJSON 输出到 stdout。\n\n输出格式(事件流默认 ndjson;显式 -f json/pretty/raw 可覆盖;-f table/csv 对\n事件流无意义会 fallback 到 ndjson):\n ndjson (默认) 一行一对象,适合 jq / 管道处理\n json 每事件多行美化 JSON(必须配 --max-events 或 --duration)\n pretty 同 json,未来加颜色\n raw 仅 SDK 原始 payload,无外层封装\n compact 单行紧凑 JSON;不传 --flatten 时沿用原 compact processor\n\n数据结构:\n ndjson/json/pretty 默认保持 transport envelope(type/event_type/data/headers)\n --flatten 结构化格式输出稳定的顶层业务字段,适合 Agent / 脚本直接消费\n\n默认使用当前 OAuth 登录态自动创建/复用个人订阅并建立个人长连接;非默认组织加\n--profile。连上后 stderr 打就绪行 [event] ready,等它出现再读 stdout;停机用\nSIGTERM、关 stdin,或先用 dws event stop \u003csubscribe_id\u003e --dry-run 预览、确认后加\n--yes,绝不要 kill -9。\n--event-types/--filter 只影响本地 bus → consume 这一段投递;普通个人事件消费\n通常不需要设置。", "display": "事件订阅 (DingTalk Stream 长连接)", "effect": "write", "effect_source": "agent-hint", "examples": [ - "dws event consume user_im_message_receive_user --open-dingtalk-id open-example --max-events 1 --format ndjson", - "dws event consume user_im_message_reaction_group --group cid-example --max-events 1 --format ndjson" + "dws event consume user_im_message_receive_user --open-dingtalk-id open-example --flatten --max-events 1 --format ndjson", + "dws event consume user_im_message_reaction_group --group cid-example --flatten --max-events 1 --format ndjson" ], "field_provenance": { "agent_summary": { @@ -290165,14 +290165,14 @@ "review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。", "selected": true, "source": "internal/cli/schema_hints/selection/event.json", - "value": "订阅并持续消费指定个人事件,输出 NDJSON 事件流" + "value": "订阅并持续消费指定个人事件;Agent 使用 --flatten 输出顶层业务 NDJSON" } ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", "review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。", "source": "internal/cli/schema_hints/selection/event.json", - "value": "订阅并持续消费指定个人事件,输出 NDJSON 事件流" + "value": "订阅并持续消费指定个人事件;Agent 使用 --flatten 输出顶层业务 NDJSON" }, "availability": { "candidates": [ @@ -290250,13 +290250,13 @@ "precedence": "cobra_help", "selected": true, "source": "cobra_help", - "value": "订阅 DingTalk 个人事件并将每条事件以 NDJSON 输出到 stdout。\n\n输出格式(事件流默认 ndjson;显式 -f json/pretty/raw 可覆盖;-f table/csv 对\n事件流无意义会 fallback 到 ndjson):\n ndjson (默认) 一行一对象,适合 jq / 管道处理\n json 每事件多行美化 JSON(必须配 --max-events 或 --duration)\n pretty 同 json,未来加颜色\n raw 仅 SDK 原始 payload,无外层封装\n compact 扁平化 + 解析嵌套 + 抽取语义字段(Agent 友好)\n\n默认使用当前 OAuth 登录态自动创建/复用个人订阅并建立个人长连接;非默认组织加\n--profile。连上后 stderr 打就绪行 [event] ready,等它出现再读 stdout;停机用\nSIGTERM、关 stdin,或先用 dws event stop \u003csubscribe_id\u003e --dry-run 预览、确认后加\n--yes,绝不要 kill -9。\n--event-types/--filter 只影响本地 bus → consume 这一段投递;普通个人事件消费\n通常不需要设置。" + "value": "订阅 DingTalk 个人事件并将每条事件以 NDJSON 输出到 stdout。\n\n输出格式(事件流默认 ndjson;显式 -f json/pretty/raw 可覆盖;-f table/csv 对\n事件流无意义会 fallback 到 ndjson):\n ndjson (默认) 一行一对象,适合 jq / 管道处理\n json 每事件多行美化 JSON(必须配 --max-events 或 --duration)\n pretty 同 json,未来加颜色\n raw 仅 SDK 原始 payload,无外层封装\n compact 单行紧凑 JSON;不传 --flatten 时沿用原 compact processor\n\n数据结构:\n ndjson/json/pretty 默认保持 transport envelope(type/event_type/data/headers)\n --flatten 结构化格式输出稳定的顶层业务字段,适合 Agent / 脚本直接消费\n\n默认使用当前 OAuth 登录态自动创建/复用个人订阅并建立个人长连接;非默认组织加\n--profile。连上后 stderr 打就绪行 [event] ready,等它出现再读 stdout;停机用\nSIGTERM、关 stdin,或先用 dws event stop \u003csubscribe_id\u003e --dry-run 预览、确认后加\n--yes,绝不要 kill -9。\n--event-types/--filter 只影响本地 bus → consume 这一段投递;普通个人事件消费\n通常不需要设置。" } ], "precedence": "cobra_help", "resolution": "highest_precedence", "source": "cobra_help", - "value": "订阅 DingTalk 个人事件并将每条事件以 NDJSON 输出到 stdout。\n\n输出格式(事件流默认 ndjson;显式 -f json/pretty/raw 可覆盖;-f table/csv 对\n事件流无意义会 fallback 到 ndjson):\n ndjson (默认) 一行一对象,适合 jq / 管道处理\n json 每事件多行美化 JSON(必须配 --max-events 或 --duration)\n pretty 同 json,未来加颜色\n raw 仅 SDK 原始 payload,无外层封装\n compact 扁平化 + 解析嵌套 + 抽取语义字段(Agent 友好)\n\n默认使用当前 OAuth 登录态自动创建/复用个人订阅并建立个人长连接;非默认组织加\n--profile。连上后 stderr 打就绪行 [event] ready,等它出现再读 stdout;停机用\nSIGTERM、关 stdin,或先用 dws event stop \u003csubscribe_id\u003e --dry-run 预览、确认后加\n--yes,绝不要 kill -9。\n--event-types/--filter 只影响本地 bus → consume 这一段投递;普通个人事件消费\n通常不需要设置。" + "value": "订阅 DingTalk 个人事件并将每条事件以 NDJSON 输出到 stdout。\n\n输出格式(事件流默认 ndjson;显式 -f json/pretty/raw 可覆盖;-f table/csv 对\n事件流无意义会 fallback 到 ndjson):\n ndjson (默认) 一行一对象,适合 jq / 管道处理\n json 每事件多行美化 JSON(必须配 --max-events 或 --duration)\n pretty 同 json,未来加颜色\n raw 仅 SDK 原始 payload,无外层封装\n compact 单行紧凑 JSON;不传 --flatten 时沿用原 compact processor\n\n数据结构:\n ndjson/json/pretty 默认保持 transport envelope(type/event_type/data/headers)\n --flatten 结构化格式输出稳定的顶层业务字段,适合 Agent / 脚本直接消费\n\n默认使用当前 OAuth 登录态自动创建/复用个人订阅并建立个人长连接;非默认组织加\n--profile。连上后 stderr 打就绪行 [event] ready,等它出现再读 stdout;停机用\nSIGTERM、关 stdin,或先用 dws event stop \u003csubscribe_id\u003e --dry-run 预览、确认后加\n--yes,绝不要 kill -9。\n--event-types/--filter 只影响本地 bus → consume 这一段投递;普通个人事件消费\n通常不需要设置。" }, "effect": { "candidates": [ @@ -290282,8 +290282,8 @@ "selected": true, "source": "internal/cli/schema_hints/selection/event.json", "value": [ - "dws event consume user_im_message_receive_user --open-dingtalk-id open-example --max-events 1 --format ndjson", - "dws event consume user_im_message_reaction_group --group cid-example --max-events 1 --format ndjson" + "dws event consume user_im_message_receive_user --open-dingtalk-id open-example --flatten --max-events 1 --format ndjson", + "dws event consume user_im_message_reaction_group --group cid-example --flatten --max-events 1 --format ndjson" ] } ], @@ -290292,8 +290292,8 @@ "review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。", "source": "internal/cli/schema_hints/selection/event.json", "value": [ - "dws event consume user_im_message_receive_user --open-dingtalk-id open-example --max-events 1 --format ndjson", - "dws event consume user_im_message_reaction_group --group cid-example --max-events 1 --format ndjson" + "dws event consume user_im_message_receive_user --open-dingtalk-id open-example --flatten --max-events 1 --format ndjson", + "dws event consume user_im_message_reaction_group --group cid-example --flatten --max-events 1 --format ndjson" ] }, "idempotency": { @@ -290444,7 +290444,7 @@ "interface_reason": "Reviewed composite workflow: the command creates or reuses a remote personal-event subscription and coordinates the local event bus and Stream consumer; no single pinned RPC represents the workflow.", "is_alias": false, "name": "consume", - "parameter_count": 27, + "parameter_count": 28, "parameters": { "compact": { "description": "提示 bus 客户端期望 compact 渲染(语义透传,bus 仍按原 payload 投递)", @@ -291124,6 +291124,90 @@ "required": false, "type": "string" }, + "flatten": { + "description": "将个人事件 transport envelope 投影为稳定的顶层业务字段", + "field_provenance": { + "description": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_usage", + "value": "将个人事件 transport envelope 投影为稳定的顶层业务字段" + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": "" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_usage", + "value": "将个人事件 transport envelope 投影为稳定的顶层业务字段" + }, + "property": { + "candidates": [ + { + "precedence": "inference", + "selected": true, + "source": "flag_name_inference", + "value": "flatten" + } + ], + "precedence": "inference", + "resolution": "highest_precedence", + "source": "flag_name_inference", + "value": "flatten" + }, + "required": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": false + } + ], + "precedence": "default", + "resolution": "fallback", + "source": "default", + "value": false + }, + "required_when": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": "" + } + ], + "precedence": "default", + "resolution": "highest_precedence", + "source": "default", + "value": "" + }, + "type": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_flag_type", + "value": "boolean" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_flag_type", + "value": "boolean" + } + }, + "property": "flatten", + "required": false, + "type": "boolean" + }, "force": { "description": "仅 --foreground 模式生效:跳过单实例锁 (慎用:会让云事件被随机切分)", "field_provenance": { @@ -293464,7 +293548,7 @@ "skills/mono/SKILL.md", "skills/mono/references/products/event.md" ], - "agent_summary": "查询指定个人事件码的 payload 字段结构", + "agent_summary": "查询指定个人事件码的输出字段结构;Agent 应查询 --flatten 模式", "agent_summary_source": "dws-agent-selection/event", "availability": "available", "avoid_when": [ @@ -293480,7 +293564,7 @@ "effect": "read", "effect_source": "agent-hint", "examples": [ - "dws event schema user_im_message_receive_at --format json" + "dws event schema user_im_message_receive_at --flatten --format json" ], "field_provenance": { "agent_summary": { @@ -293490,14 +293574,14 @@ "review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。", "selected": true, "source": "internal/cli/schema_hints/selection/event.json", - "value": "查询指定个人事件码的 payload 字段结构" + "value": "查询指定个人事件码的输出字段结构;Agent 应查询 --flatten 模式" } ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", "review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。", "source": "internal/cli/schema_hints/selection/event.json", - "value": "查询指定个人事件码的 payload 字段结构" + "value": "查询指定个人事件码的输出字段结构;Agent 应查询 --flatten 模式" }, "availability": { "candidates": [ @@ -293607,7 +293691,7 @@ "selected": true, "source": "internal/cli/schema_hints/selection/event.json", "value": [ - "dws event schema user_im_message_receive_at --format json" + "dws event schema user_im_message_receive_at --flatten --format json" ] } ], @@ -293616,7 +293700,7 @@ "review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。", "source": "internal/cli/schema_hints/selection/event.json", "value": [ - "dws event schema user_im_message_receive_at --format json" + "dws event schema user_im_message_receive_at --flatten --format json" ] }, "idempotency": { @@ -293763,8 +293847,92 @@ "interface_reason": "命令读取 CLI 内置的个人事件 payload 定义,不绑定 pinned MCP RPC", "is_alias": false, "name": "schema", - "parameter_count": 1, + "parameter_count": 2, "parameters": { + "flatten": { + "description": "显示 --flatten 消费模式对应的顶层业务字段 schema", + "field_provenance": { + "description": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_usage", + "value": "显示 --flatten 消费模式对应的顶层业务字段 schema" + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": "" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_usage", + "value": "显示 --flatten 消费模式对应的顶层业务字段 schema" + }, + "property": { + "candidates": [ + { + "precedence": "inference", + "selected": true, + "source": "flag_name_inference", + "value": "flatten" + } + ], + "precedence": "inference", + "resolution": "highest_precedence", + "source": "flag_name_inference", + "value": "flatten" + }, + "required": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": false + } + ], + "precedence": "default", + "resolution": "fallback", + "source": "default", + "value": false + }, + "required_when": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": "" + } + ], + "precedence": "default", + "resolution": "highest_precedence", + "source": "default", + "value": "" + }, + "type": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_flag_type", + "value": "boolean" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_flag_type", + "value": "boolean" + } + }, + "property": "flatten", + "required": false, + "type": "boolean" + }, "format": { "default": "json", "description": "输出格式: json", diff --git a/internal/cli/schema_hints/index.json b/internal/cli/schema_hints/index.json index de218051..9b619661 100644 --- a/internal/cli/schema_hints/index.json +++ b/internal/cli/schema_hints/index.json @@ -634,6 +634,11 @@ "target": "event consume", "reason": "已审查的带 event_key 和输出参数的 Skill 引用,固定映射到当前公开 event consume leaf" }, + "event consume user_im_message_receive_at": { + "status": "alias", + "target": "event consume", + "reason": "已审查的带 event_key 参数的 Skill 引用,固定映射到当前公开 event consume leaf" + }, "event consume user_im_message_receive_group": { "status": "alias", "target": "event consume", diff --git a/internal/cli/schema_hints/reference-review.json b/internal/cli/schema_hints/reference-review.json index 9cc9e2da..d939bfb2 100644 --- a/internal/cli/schema_hints/reference-review.json +++ b/internal/cli/schema_hints/reference-review.json @@ -585,6 +585,11 @@ "target": "event consume", "reason": "已审查的带 event_key 和输出参数的 Skill 引用,固定映射到当前公开 event consume leaf" }, + "event consume user_im_message_receive_at": { + "status": "alias", + "target": "event consume", + "reason": "已审查的带 event_key 参数的 Skill 引用,固定映射到当前公开 event consume leaf" + }, "event consume user_im_message_receive_group": { "status": "alias", "target": "event consume", diff --git a/internal/cli/schema_hints/selection/event.json b/internal/cli/schema_hints/selection/event.json index ecb9b813..d33ac30f 100644 --- a/internal/cli/schema_hints/selection/event.json +++ b/internal/cli/schema_hints/selection/event.json @@ -9,7 +9,7 @@ }, "tools": { "event.consume": { - "agent_summary": "订阅并持续消费指定个人事件,输出 NDJSON 事件流", + "agent_summary": "订阅并持续消费指定个人事件;Agent 使用 --flatten 输出顶层业务 NDJSON", "use_when": [ "需要实时监听 @我、指定单聊、指定群或指定发送人的后续消息事件", "需要监听指定单聊或群聊中的消息已读、撤回或表情回应事件", @@ -20,8 +20,8 @@ "只看事件目录/字段时用 event list / event schema" ], "examples": [ - "dws event consume user_im_message_receive_user --open-dingtalk-id open-example --max-events 1 --format ndjson", - "dws event consume user_im_message_reaction_group --group cid-example --max-events 1 --format ndjson" + "dws event consume user_im_message_receive_user --open-dingtalk-id open-example --flatten --max-events 1 --format ndjson", + "dws event consume user_im_message_reaction_group --group cid-example --flatten --max-events 1 --format ndjson" ], "reviewed": true, "review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。", @@ -57,7 +57,7 @@ ] }, "event.schema": { - "agent_summary": "查询指定个人事件码的 payload 字段结构", + "agent_summary": "查询指定个人事件码的输出字段结构;Agent 应查询 --flatten 模式", "use_when": [ "已知任一公开个人消息 event_key,消费前需要理解扁平输出字段" ], @@ -66,7 +66,7 @@ "要实际收事件时用 event consume" ], "examples": [ - "dws event schema user_im_message_receive_at --format json" + "dws event schema user_im_message_receive_at --flatten --format json" ], "reviewed": true, "review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。", diff --git a/internal/event/consume/run.go b/internal/event/consume/run.go index d9620fdb..65ceb58c 100644 --- a/internal/event/consume/run.go +++ b/internal/event/consume/run.go @@ -90,6 +90,10 @@ type Config struct { // NormalizeFormat; an empty Format here defaults to NDJSON inside // BuildPipeline. Format Format + // Flatten records whether the caller explicitly selected a structured + // business projection. Projector remains the executable behavior; this + // field is surfaced in dry-run output so users can verify the final mode. + Flatten bool // OutputDir, if non-empty, switches the fallback sink from stdout to // "file per event" under this directory. OutputDir string diff --git a/internal/event/consume/validate.go b/internal/event/consume/validate.go index 33eafe0b..1982f1eb 100644 --- a/internal/event/consume/validate.go +++ b/internal/event/consume/validate.go @@ -132,6 +132,7 @@ func PrintDryRun(w io.Writer, cfg Config) { fmt.Fprintf(w, " filter : %s\n", cfg.Filter) } fmt.Fprintf(w, " format : %s\n", cfg.Format) + fmt.Fprintf(w, " flatten : %v\n", cfg.Flatten) if cfg.OutputDir != "" { fmt.Fprintf(w, " output_dir : %s\n", cfg.OutputDir) } diff --git a/internal/event/consume/validate_test.go b/internal/event/consume/validate_test.go index 4da7dff0..2f5b64a4 100644 --- a/internal/event/consume/validate_test.go +++ b/internal/event/consume/validate_test.go @@ -137,6 +137,7 @@ func TestPrintDryRun_RendersAllSetFields(t *testing.T) { c.EventTypes = []string{"im.*", "approval.*"} c.Filter = "^im\\." c.Format = FormatCompact + c.Flatten = true c.OutputDir = "/tmp/events" c.Routes, _ = ParseRoutes([]string{`^im\.=dir:/tmp/im/`}) c.MaxEvents = 5 @@ -151,7 +152,7 @@ func TestPrintDryRun_RendersAllSetFields(t *testing.T) { wants := []string{ "client_id", "workdir", "ipc_endpoint", "im.*,approval.*", "^im\\.", "compact", "/tmp/events", "route[0]", "max_events : 5", - "duration", "true", + "duration", "flatten : true", "true", } for _, w := range wants { if !strings.Contains(out, w) { diff --git a/internal/event/personal/output.go b/internal/event/personal/output.go index c2a344dc..63ffb3fa 100644 --- a/internal/event/personal/output.go +++ b/internal/event/personal/output.go @@ -394,6 +394,42 @@ func outputSchema(eventKey string) map[string]any { } } +func transportEnvelopeSchema(eventKey string) map[string]any { + eventType := reflect.TypeOf(transport.Event{}) + properties := make(map[string]any, eventType.NumField()) + for i := 0; i < eventType.NumField(); i++ { + field := eventType.Field(i) + name := strings.Split(field.Tag.Get("json"), ",")[0] + if name == "" || name == "-" { + continue + } + property := map[string]any{"type": schemaType(field.Type)} + switch name { + case "type": + property["description"] = "transport frame 类型" + property["enum"] = []string{string(transport.FrameTypeEvent)} + case "event_type": + property["description"] = "事件类型" + property["enum"] = []string{eventKey} + case "data": + property["description"] = "服务端业务 payload JSON 字符串" + property["content_media_type"] = "application/json" + case "headers": + property["description"] = "Stream transport headers" + property["additionalProperties"] = map[string]any{"type": "string"} + case "event_id": + property["description"] = "transport 事件 ID" + case "subscribe_id": + property["description"] = "个人事件订阅 ID" + } + properties[name] = property + } + return map[string]any{ + "type": "object", + "properties": properties, + } +} + func outputTypeForEvent(eventKey string) reflect.Type { switch { case isMessageReceiveEvent(eventKey): diff --git a/internal/event/personal/registry.go b/internal/event/personal/registry.go index 5f0da590..a2857a86 100644 --- a/internal/event/personal/registry.go +++ b/internal/event/personal/registry.go @@ -258,8 +258,18 @@ func Catalog(category string, enabledOnly, includePending bool) []Definition { } func BuildSchemaDocument(def Definition) SchemaDocument { + return BuildSchemaDocumentForMode(def, false) +} + +func BuildSchemaDocumentForMode(def Definition, flatten bool) SchemaDocument { requiredParams := make([]string, 0, len(def.RequiredParams)) requiredParams = append(requiredParams, def.RequiredParams...) + jqRootPath := ".data | fromjson" + schema := transportEnvelopeSchema(def.EventKey) + if flatten { + jqRootPath = "." + schema = outputSchema(def.EventKey) + } return SchemaDocument{ EventKey: def.EventKey, DisplayName: def.DisplayName, @@ -268,8 +278,8 @@ func BuildSchemaDocument(def Definition) SchemaDocument { RuleType: def.RuleType, RequiredParams: requiredParams, Constraints: cloneParameterConstraints(def.Constraints), - JQRootPath: ".", - Schema: outputSchema(def.EventKey), + JQRootPath: jqRootPath, + Schema: schema, } } diff --git a/internal/event/personal/registry_test.go b/internal/event/personal/registry_test.go index a5beaa23..affcd43b 100644 --- a/internal/event/personal/registry_test.go +++ b/internal/event/personal/registry_test.go @@ -95,7 +95,7 @@ func TestDefinitionJSONHidesInternalSchemaIDs(t *testing.T) { } } -func TestSchemaDocumentsUseSingleJSONSchema(t *testing.T) { +func TestSchemaDocumentsDefaultToTransportEnvelope(t *testing.T) { for _, eventKey := range []string{EventMention, EventSingleChat, EventInChat, EventFromUser} { t.Run(eventKey, func(t *testing.T) { def, ok := Lookup(eventKey) @@ -117,16 +117,16 @@ func TestSchemaDocumentsUseSingleJSONSchema(t *testing.T) { "required_params", "jq_root_path", "schema", + "type", + "seq", "event_id", - "timestamp", + "event_born_time", + "event_type", "subscribe_id", - "content", - "sender", - "sender_open_dingtalk_id", - "conversation_id", - "message_id", - "create_time", - "event_time", + "source_id", + "data", + "headers", + "received_at_unix_ms", } { if !strings.Contains(out, want) { t.Fatalf("schema for %s missing %q: %s", eventKey, want, out) @@ -144,7 +144,6 @@ func TestSchemaDocumentsUseSingleJSONSchema(t *testing.T) { "payload_schema", "output_schema", "data_json_path", - "headers", "audit", "tenant", "subject", @@ -152,13 +151,18 @@ func TestSchemaDocumentsUseSingleJSONSchema(t *testing.T) { "msgIdMetaq", "at_users", "sender_user_id", + "sender_open_dingtalk_id", + "conversation_id", + "message_id", + "create_time", + "event_time", } { if strings.Contains(out, leaked) { t.Fatalf("schema for %s leaked %q: %s", eventKey, leaked, out) } } - if doc.JQRootPath != "." { - t.Fatalf("jq_root_path = %q, want .", doc.JQRootPath) + if doc.JQRootPath != ".data | fromjson" { + t.Fatalf("jq_root_path = %q, want .data | fromjson", doc.JQRootPath) } if doc.RequiredParams == nil { t.Fatalf("required_params = nil, want empty slice") @@ -167,6 +171,38 @@ func TestSchemaDocumentsUseSingleJSONSchema(t *testing.T) { if !ok { t.Fatalf("schema.properties = %#v, want object", doc.Schema["properties"]) } + wantProperties := []string{ + "type", "seq", "event_id", "event_born_time", "event_corp_id", + "event_type", "event_unified_app_id", "event_scope", "subscribe_id", + "source_id", "rule_type", "data", "headers", "received_at_unix_ms", + } + if len(props) != len(wantProperties) { + t.Fatalf("schema.properties = %#v, want exactly %d transport fields", props, len(wantProperties)) + } + for _, name := range wantProperties { + if _, ok := props[name].(map[string]any); !ok { + t.Fatalf("schema.properties.%s = %#v, want object", name, props[name]) + } + } + }) + } +} + +func TestFlattenedSchemaDocumentsUseMessageDTO(t *testing.T) { + for _, eventKey := range []string{EventMention, EventSingleChat, EventInChat, EventFromUser} { + t.Run(eventKey, func(t *testing.T) { + def, ok := Lookup(eventKey) + if !ok { + t.Fatalf("Lookup(%q) failed", eventKey) + } + doc := BuildSchemaDocumentForMode(def, true) + if doc.JQRootPath != "." { + t.Fatalf("jq_root_path = %q, want .", doc.JQRootPath) + } + props, ok := doc.Schema["properties"].(map[string]any) + if !ok { + t.Fatalf("schema.properties = %#v, want object", doc.Schema["properties"]) + } wantProperties := []string{ "type", "event_id", "timestamp", "subscribe_id", "message_id", "conversation_id", "sender", "sender_open_dingtalk_id", "content", @@ -180,6 +216,11 @@ func TestSchemaDocumentsUseSingleJSONSchema(t *testing.T) { t.Fatalf("schema.properties.%s = %#v, want object", name, props[name]) } } + for _, transportField := range []string{"data", "headers", "seq", "event_type"} { + if _, ok := props[transportField]; ok { + t.Fatalf("flattened schema exposed transport field %q", transportField) + } + } }) } } @@ -286,7 +327,7 @@ func TestActionSchemaDocumentsMatchOutputDTOs(t *testing.T) { if !ok { t.Fatalf("Lookup(%q) failed", eventKey) } - doc := BuildSchemaDocument(def) + doc := BuildSchemaDocumentForMode(def, true) if doc.JQRootPath != "." { t.Fatalf("jq_root_path = %q, want .", doc.JQRootPath) } diff --git a/skills/mono/SKILL.md b/skills/mono/SKILL.md index 11785b3b..437e5908 100644 --- a/skills/mono/SKILL.md +++ b/skills/mono/SKILL.md @@ -22,7 +22,7 @@ cli_version: ">=1.0.15" - 单次批量操作不超过 30 条记录 - 所有命令必须**严格遵循**对应产品参考文档里面规定的参数格式(如:如果有参数值,则参数和参数值之间至少用一个空格隔开) - **脚本优先**:[scripts/](./scripts/) 下的 `python scripts/.py` 已封装翻页/轮询/批量逻辑,遇到对应场景(如 AI 表格批量导入导出、AI 应用创建轮询、文档创建后写内容、钉盘目录树等)**优先调用脚本**而非手写多步命令。脚本均支持 `--dry-run` 预览、`--format json` 输出,失败时回退到手动步骤 -- **实时个人消息事件例外**:用户要监听消息、订阅事件、自动回复消息或事件驱动 Agent 时,必须走 `dws event consume` 长连接,不要写脚本轮询消息历史 +- **实时个人消息事件例外**:用户要监听消息、订阅事件、自动回复消息或事件驱动 Agent 时,必须走 `dws event consume ... --flatten` 长连接,不要写脚本轮询消息历史 ## Shortcut 与原子命令的使用原则 @@ -259,7 +259,7 @@ Schema 与 Help 冲突是**契约漂移**,不得静默猜测或把两边字段 `dev.*` 包含 helper-only 执行面,其中远端 helper 未进入 pinned metadata 时标记为 `composite`,不能伪装成 `local`。`event list` / `event schema` 读取内置目录和 payload 定义,属于 `local`;`event consume` / `event status` / `event stop` 同时编排远端个人订阅控制面与本地 bus/consume,属于 `composite`。实现来源不同,不改变统一查询边界:进入全局 `dws schema` 的命令必须先进入 reviewed CommandRegistry,并由同一 `ToolSpec` 投影到 leaf、产品/分组、`--all` 与 Catalog。不得在查询时重新调用 MCP `tools/list`,也不得把 Cobra 临时合成结果作为第二条 Schema 数据路径。 -事件需要区分两种 Schema:`dws event schema ` 查询事件 payload 字段;`dws schema "event consume"` 查询 CLI 命令参数。前者是真实业务命令,后者只读取最终内嵌 SchemaRegistry;不能相互替代。 +事件需要区分两种 Schema:`dws event schema --flatten` 查询 Agent 要消费的顶层业务字段;`dws schema "event consume"` 查询 CLI 命令参数。前者是真实业务命令,后者只读取最终内嵌 SchemaRegistry;不能相互替代。 `source` 表示最终命令 identity 的来源,不表示运行时 backing;helper/local/MCP 实现机制读取 `interface_mode`、`availability` 和 provenance,不要假定 `dev.*` 必然是 `source=mcp:`,也不要假定本地命令必然是 `source=cobra`。 diff --git a/skills/mono/references/products/event.md b/skills/mono/references/products/event.md index e7653477..e4404afe 100644 --- a/skills/mono/references/products/event.md +++ b/skills/mono/references/products/event.md @@ -13,8 +13,8 @@ | Command | Purpose | |---|---| -| `dws event schema ` | 查看事件参数和输出字段 schema | -| `dws event consume [flags]` | 阻塞消费,事件写到 stdout,用 `-f ndjson` | +| `dws event schema --flatten` | 查看 Agent 使用的顶层业务字段 schema | +| `dws event consume --flatten [flags]` | 阻塞消费,事件写到 stdout,用 `-f ndjson` | | `dws event status --event ` | 查看个人订阅、bus、本地 consume | | `dws event stop --dry-run` / `--yes` | 先预览,再确认取消订阅并停止对应本地消费 | | `dws event stop --all --dry-run` / `--yes` | 先预览,再确认清理当前身份下全部个人订阅 | @@ -42,20 +42,20 @@ | 用户说 | 下一步 | |---|---| -| "监听有人 @ 我的消息" | `event consume`,事件码 `user_im_message_receive_at`,参数 `-f ndjson` | -| "监听我和 userId test-user-001 的单聊消息" | `event consume`,事件码 `user_im_message_receive_o2o`,参数 `--user test-user-001 -f ndjson` | -| "监听我和 openDingtalkId abc 的单聊消息" | `event consume`,事件码 `user_im_message_receive_o2o`,参数 `--open-dingtalk-id abc -f ndjson` | +| "监听有人 @ 我的消息" | `event consume`,事件码 `user_im_message_receive_at`,参数 `--flatten -f ndjson` | +| "监听我和 userId test-user-001 的单聊消息" | `event consume`,事件码 `user_im_message_receive_o2o`,参数 `--user test-user-001 --flatten -f ndjson` | +| "监听我和 openDingtalkId abc 的单聊消息" | `event consume`,事件码 `user_im_message_receive_o2o`,参数 `--open-dingtalk-id abc --flatten -f ndjson` | | "监听 XX 群消息" | 先 `dws chat search --query "XX" --format json`,确认后 consume group | -| "监听 userId test-user-001 发给我的消息" | `event consume`,事件码 `user_im_message_receive_user`,参数 `--user test-user-001 -f ndjson` | -| "监听 openDingtalkId abc 发给我的消息" | `event consume`,事件码 `user_im_message_receive_user`,参数 `--open-dingtalk-id abc -f ndjson` | -| "监听我发给 userId test-user-001 的消息是否已读" | `event consume`,事件码 `user_im_message_read_o2o`,参数 `--user test-user-001 -f ndjson` | +| "监听 userId test-user-001 发给我的消息" | `event consume`,事件码 `user_im_message_receive_user`,参数 `--user test-user-001 --flatten -f ndjson` | +| "监听 openDingtalkId abc 发给我的消息" | `event consume`,事件码 `user_im_message_receive_user`,参数 `--open-dingtalk-id abc --flatten -f ndjson` | +| "监听我发给 userId test-user-001 的消息是否已读" | `event consume`,事件码 `user_im_message_read_o2o`,参数 `--user test-user-001 --flatten -f ndjson` | | "监听 XX 群消息已读" | 先解析群 ID,再 consume `user_im_message_read_group --group ` | -| "监听我和 userId test-user-001 的消息撤回" | `event consume`,事件码 `user_im_message_recall_o2o`,参数 `--user test-user-001 -f ndjson` | +| "监听我和 userId test-user-001 的消息撤回" | `event consume`,事件码 `user_im_message_recall_o2o`,参数 `--user test-user-001 --flatten -f ndjson` | | "监听 XX 群消息撤回" | 先解析群 ID,再 consume `user_im_message_recall_group --group ` | -| "监听我和 userId test-user-001 的消息贴表情" | `event consume`,事件码 `user_im_message_reaction_o2o`,参数 `--user test-user-001 -f ndjson` | +| "监听我和 userId test-user-001 的消息贴表情" | `event consume`,事件码 `user_im_message_reaction_o2o`,参数 `--user test-user-001 --flatten -f ndjson` | | "监听 XX 群消息表情回应" | 先解析群 ID,再 consume `user_im_message_reaction_group --group ` | | "监听并自动回复某人的单聊消息" | 先解析对端 userId,再启动 o2o consume;不要写轮询脚本 | -| "查看个人消息事件 schema" | `dws event schema ` | +| "查看个人消息事件 schema" | `dws event schema --flatten` | | "看个人事件订阅状态" | `dws event status --event ` | | "停止这个个人事件订阅" | `dws event stop --dry-run`,确认后改用 `--yes` | @@ -66,8 +66,8 @@ ## Call flow 1. 从用户意图选择事件码;人名或群名先解析成必填 ID。 -2. 需要了解字段时运行 `dws event schema `,读取 `schema.properties`;`jq_root_path` 当前固定为 `.`。 -3. 启动 `dws event consume ... -f ndjson`,等待 stderr 出现 `[event] ready event_key= bus_pid= subscribe_id=` 后开始处理 stdout,不要用 `sleep` 猜测。 +2. 需要了解字段时运行 `dws event schema --flatten`,读取 `schema.properties`;此模式的 `jq_root_path` 为 `.`。 +3. 启动 `dws event consume ... --flatten -f ndjson`,等待 stderr 出现 `[event] ready event_key= bus_pid= subscribe_id=` 后开始处理 stdout,不要用 `sleep` 猜测。 4. stdout 每行是一个扁平事件 JSON;直接按该事件的 `schema.properties` 读取顶层字段。 5. 需要确认监听状态时运行 `dws event status --event `,查看 `Subscriptions` 和 `Consumers`。 6. 任务完成后优雅结束 consume;本次新建的订阅会自动取消。复用已有订阅或需要从外部主动取消时,先运行 `dws event stop --dry-run`,向用户确认后再以 `--yes` 执行;自测可在 consume 加 `--max-events` 或 `--duration` 自动退出。 @@ -75,31 +75,31 @@ ## Commands ```bash -dws event schema user_im_message_receive_at -dws event schema user_im_message_receive_o2o -dws event schema user_im_message_receive_group -dws event schema user_im_message_receive_user -dws event schema user_im_message_read_o2o -dws event schema user_im_message_read_group -dws event schema user_im_message_recall_o2o -dws event schema user_im_message_recall_group -dws event schema user_im_message_reaction_o2o -dws event schema user_im_message_reaction_group +dws event schema user_im_message_receive_at --flatten +dws event schema user_im_message_receive_o2o --flatten +dws event schema user_im_message_receive_group --flatten +dws event schema user_im_message_receive_user --flatten +dws event schema user_im_message_read_o2o --flatten +dws event schema user_im_message_read_group --flatten +dws event schema user_im_message_recall_o2o --flatten +dws event schema user_im_message_recall_group --flatten +dws event schema user_im_message_reaction_o2o --flatten +dws event schema user_im_message_reaction_group --flatten ``` ```bash -dws event consume user_im_message_receive_at -f ndjson -dws event consume user_im_message_receive_o2o --user test-user-001 -f ndjson -dws event consume user_im_message_receive_o2o --open-dingtalk-id abc -f ndjson -dws event consume user_im_message_receive_group --group -f ndjson -dws event consume user_im_message_receive_user --user test-user-001 -f ndjson -dws event consume user_im_message_receive_user --open-dingtalk-id abc -f ndjson -dws event consume user_im_message_read_o2o --user test-user-001 -f ndjson -dws event consume user_im_message_read_group --group -f ndjson -dws event consume user_im_message_recall_o2o --user test-user-001 -f ndjson -dws event consume user_im_message_recall_group --group -f ndjson -dws event consume user_im_message_reaction_o2o --user test-user-001 -f ndjson -dws event consume user_im_message_reaction_group --group -f ndjson +dws event consume user_im_message_receive_at --flatten -f ndjson +dws event consume user_im_message_receive_o2o --user test-user-001 --flatten -f ndjson +dws event consume user_im_message_receive_o2o --open-dingtalk-id abc --flatten -f ndjson +dws event consume user_im_message_receive_group --group --flatten -f ndjson +dws event consume user_im_message_receive_user --user test-user-001 --flatten -f ndjson +dws event consume user_im_message_receive_user --open-dingtalk-id abc --flatten -f ndjson +dws event consume user_im_message_read_o2o --user test-user-001 --flatten -f ndjson +dws event consume user_im_message_read_group --group --flatten -f ndjson +dws event consume user_im_message_recall_o2o --user test-user-001 --flatten -f ndjson +dws event consume user_im_message_recall_group --group --flatten -f ndjson +dws event consume user_im_message_reaction_o2o --user test-user-001 --flatten -f ndjson +dws event consume user_im_message_reaction_group --group --flatten -f ndjson ``` 上述所有 `*_o2o` 命令和 `user_im_message_receive_user` 都可将 `--user ` 替换为 `--open-dingtalk-id `,但两个参数不能同时使用。 @@ -125,10 +125,10 @@ dws event stop --all --yes ## Output parsing -- 推荐 `-f ndjson`:一行一个事件 JSON,适合 Agent 管道读取。 -- 人工取样可用 `-f json --max-events 1`。 -- `jq_root_path` 当前为 `.`;消息正文、发送人和会话 ID 分别直接读取顶层 `content`、`sender`、`conversation_id`。 -- 不要生成 `fromjson` 或内部 payload 路径。正常处理直接持续读取 stdout,不要改写为 `--output-dir` watcher。 +- 推荐 `--flatten -f ndjson`:顶层业务字段,一行一个事件 JSON,适合 Agent 管道读取。 +- 人工取样可用 `--flatten -f json --max-events 1`。`--format` 只控制序列化,`--flatten` 控制数据结构。 +- `--flatten` 的 `jq_root_path` 为 `.`;消息正文、发送人和会话 ID 分别直接读取顶层 `content`、`sender`、`conversation_id`。 +- Agent 已显式使用 `--flatten`,不要再生成 `fromjson` 或内部 payload 路径。不传时默认保持兼容 envelope,业务 payload 在 `.data | fromjson`。正常处理直接持续读取 stdout,不要改写为 `--output-dir` watcher。 - 群自动回复使用顶层 `conversation_id`;单聊自动回复使用顶层 `sender_open_dingtalk_id`。 - 已读事件直接读取 `reader/reader_open_dingtalk_id/read_time`;撤回事件读取 `recaller/recaller_open_dingtalk_id/recall_time`。 - 表情回应事件直接读取 `operator/operator_open_dingtalk_id/reaction_name/reaction_text/operation_type/operation_time`。 @@ -136,7 +136,7 @@ dws event stop --all --yes - 正常动作事件输出不含内部 `payload/uid/corpid/clientId/filterSubId/bizid`;原始排查才使用 `-f raw` 或 `--debug-raw-events`。 - 自己发的消息不作为事件回来(`isSelfLoop` 过滤);自发验证会看到 0 事件,测试投递使用别人或机器人发消息。 - `--jq <表达式>` 可进一步过滤或投影扁平输出。 -- `--debug-raw-events` 仅用于服务端联调,正常消费不要使用。 +- `--debug-raw-events` 仅用于服务端联调,正常消费不要使用;它和 `--flatten` 互斥,`-f raw` 也不能与 `--flatten` 同时使用。 ## Troubleshooting diff --git a/skills/multi/dingtalk-event/SKILL.md b/skills/multi/dingtalk-event/SKILL.md index 6e5a5742..8ded7a1c 100644 --- a/skills/multi/dingtalk-event/SKILL.md +++ b/skills/multi/dingtalk-event/SKILL.md @@ -19,8 +19,8 @@ description: 钉钉个人 IM 事件长连接监听、订阅与消费,覆盖消 | Command | Purpose | |---|---| | `dws event list` | 查看当前个人事件目录;不要把它当能力菜单主动展示 | -| `dws event schema ` | 查看事件参数和输出字段 schema,默认 JSON | -| `dws event consume [flags]` | 阻塞消费;事件写到 stdout,推荐 `-f ndjson` | +| `dws event schema --flatten` | 查看 Agent 使用的顶层业务字段 schema,默认 JSON | +| `dws event consume --flatten [flags]` | 阻塞消费;事件写到 stdout,推荐 `-f ndjson` | | `dws event status --event ` | 查看个人订阅、personal bus 和本地 consume | | `dws event stop --dry-run` / `--yes` | 先预览,再确认取消个人订阅并停止对应本地消费 | | `dws event stop --all --dry-run` / `--yes` | 先预览,再确认清理当前身份下本地记录的全部个人订阅 | @@ -57,17 +57,17 @@ description: 钉钉个人 IM 事件长连接监听、订阅与消费,覆盖消 - 用户只给群名时,先运行 `dws chat search --query "" --format json` 解析 openConversationId;多候选必须让用户确认。 - 用户要求执行“撤回消息”时使用 `dws chat`;只有“监听/订阅消息撤回”才使用 `dws event consume user_im_message_recall_*`。 - 用户说“贴标签”且语义是给消息贴表情时,按消息表情回应事件处理,event key 使用 `reaction`。 -- 正常 Agent 消费使用 `-f ndjson`。抓一条样本可用 `--max-events 1 -f json`。 +- 正常 Agent 消费统一显式使用 `--flatten -f ndjson`。抓一条样本可用 `--flatten --max-events 1 -f json`。`--format` 只控制 JSON 序列化,`--flatten` 才控制数据结构。 - 监听非默认组织时带 `--profile `;漏传会退回默认 profile 而失败。 - 自己发的消息不作为事件回来(`isSelfLoop` 过滤):边监听边 `dws chat message send` 回复不成环;测试投递用别人 / 机器人发(自发会看到 0 事件)。 -- `--debug-raw-events` 只用于联调确认服务端推送是否到达本地连接;正常任务不要使用。 +- `--debug-raw-events` 只用于联调确认服务端推送是否到达本地连接;正常任务不要使用。它和 `--flatten` 互斥,`-f raw` 也不能与 `--flatten` 同时使用。 - 排查:consume 报 bus 启动失败 → 报错已带真实原因,先查 `dws --profile auth status`(非默认组织带对 `--profile`);本地日志见 `~/.dws/events//personal_stream//bus.log`(`hash` 见 `dws event status` 的 Workdir);有残留先用 `dws event stop --all --dry-run` 预览,确认后加 `--yes` 清理。看着"挂住"无输出多是误加了 `--foreground`(那是跑 bus、不打印事件),去掉即可。 ## Call flow 1. 从用户意图选择事件码;人名或群名先解析成必填 ID。 -2. 需要了解字段时运行 `dws event schema `,读取 `schema.properties`;`jq_root_path` 当前固定为 `.`。 -3. 启动 `dws event consume ... -f ndjson`,等待 stderr 出现 `[event] ready event_key= bus_pid= subscribe_id=` 后开始处理 stdout,不要用 `sleep` 猜测。 +2. 需要了解字段时运行 `dws event schema --flatten`,读取 `schema.properties`;此模式的 `jq_root_path` 为 `.`。 +3. 启动 `dws event consume ... --flatten -f ndjson`,等待 stderr 出现 `[event] ready event_key= bus_pid= subscribe_id=` 后开始处理 stdout,不要用 `sleep` 猜测。 4. stdout 每行是一个扁平事件 JSON;直接按该事件的 `schema.properties` 读取顶层字段。 5. 需要确认监听状态时运行 `dws event status --event `,查看 `Subscriptions` 和 `Consumers`。 6. 任务完成后优雅结束 consume;本次新建的订阅会自动取消。复用已有订阅或需要从外部主动取消时,先用 `dws event stop --dry-run` 预览,向用户确认后再加 `--yes`;临时测试可用 `--max-events` 或 `--duration` 自动退出。 @@ -88,57 +88,67 @@ description: 钉钉个人 IM 事件长连接监听、订阅与消费,覆盖消 ```bash # 当前用户被 @ 的消息 -dws event consume user_im_message_receive_at -f ndjson +dws event consume user_im_message_receive_at --flatten -f ndjson # 当前用户与指定用户的单聊消息 dws event consume user_im_message_receive_o2o \ --user test-user-001 \ + --flatten \ -f ndjson # 使用 openDingtalkId 监听外部联系人、机器人或跨组织身份的单聊消息 dws event consume user_im_message_receive_o2o \ --open-dingtalk-id open-user-1 \ + --flatten \ -f ndjson # 指定群聊/会话消息 dws event consume user_im_message_receive_group \ --group cidxxxxxxxx \ + --flatten \ -f ndjson # 指定发送人的消息(单聊和群聊) dws event consume user_im_message_receive_user \ --user test-user-001 \ + --flatten \ -f ndjson # 使用 openDingtalkId 监听指定发送人的消息 dws event consume user_im_message_receive_user \ --open-dingtalk-id open-user-1 \ + --flatten \ -f ndjson # 指定单聊消息已读 dws event consume user_im_message_read_o2o \ --user test-user-001 \ + --flatten \ -f ndjson # 指定群聊消息撤回 dws event consume user_im_message_recall_group \ --group cidxxxxxxxx \ + --flatten \ -f ndjson # 指定单聊消息收到表情回应 dws event consume user_im_message_reaction_o2o \ --user test-user-001 \ + --flatten \ -f ndjson # 有界自测 dws event consume user_im_message_receive_at \ --duration 10m \ + --flatten \ -f ndjson # 抓一条样本 dws event consume user_im_message_receive_o2o \ --user test-user-001 \ --max-events 1 \ + --flatten \ -f json ``` @@ -146,10 +156,10 @@ dws event consume user_im_message_receive_o2o \ ## 输出处理 -- `dws event schema ` 是写解析逻辑的依据。 -- 顶层 `jq_root_path` 说明业务字段起点;当前值是 `.`。 +- `dws event schema --flatten` 是 Agent 写解析逻辑的依据。 +- `--flatten` 模式的顶层 `jq_root_path` 为 `.`;不传时为兼容存量脚本的 transport envelope,业务 payload 在 `.data | fromjson`。 - `schema.properties` 是业务字段列表,例如 `content`、`sender`、`conversation_id`、`message_id`、`event_time`。 -- 所有公开事件都是扁平业务对象,直接读取顶层字段;不要生成 `fromjson` 或内部 payload 路径。 +- Agent 命令已显式传 `--flatten`,直接读取顶层字段;不要对该模式再生成 `fromjson` 或内部 payload 路径。 - 群自动回复使用事件顶层 `conversation_id`;单聊自动回复使用顶层 `sender_open_dingtalk_id`。 - 已读事件读取顶层 `reader`、`reader_open_dingtalk_id`、`read_time`;撤回事件读取 `recaller`、`recaller_open_dingtalk_id`、`recall_time`。 - 表情回应事件读取顶层 `operator`、`operator_open_dingtalk_id`、`reaction_name`、`reaction_text`、`operation_type`、`operation_time`。 diff --git a/skills/multi/dingtalk-event/references/event-im.md b/skills/multi/dingtalk-event/references/event-im.md index 25000f68..04221d97 100644 --- a/skills/multi/dingtalk-event/references/event-im.md +++ b/skills/multi/dingtalk-event/references/event-im.md @@ -15,19 +15,19 @@ dws auth login 查看事件 schema: ```bash -dws event schema user_im_message_receive_at -dws event schema user_im_message_receive_o2o -dws event schema user_im_message_receive_group -dws event schema user_im_message_receive_user -dws event schema user_im_message_read_o2o -dws event schema user_im_message_read_group -dws event schema user_im_message_recall_o2o -dws event schema user_im_message_recall_group -dws event schema user_im_message_reaction_o2o -dws event schema user_im_message_reaction_group +dws event schema user_im_message_receive_at --flatten +dws event schema user_im_message_receive_o2o --flatten +dws event schema user_im_message_receive_group --flatten +dws event schema user_im_message_receive_user --flatten +dws event schema user_im_message_read_o2o --flatten +dws event schema user_im_message_read_group --flatten +dws event schema user_im_message_recall_o2o --flatten +dws event schema user_im_message_recall_group --flatten +dws event schema user_im_message_reaction_o2o --flatten +dws event schema user_im_message_reaction_group --flatten ``` -schema 默认 JSON。业务字段说明在 `schema.properties`,`jq_root_path` 当前固定为 `.`。 +schema 默认 JSON。Agent 使用 `--flatten` schema,业务字段在 `schema.properties`,`jq_root_path` 为 `.`。不传 `--flatten` 时查看兼容 transport envelope,其 `jq_root_path` 为 `.data | fromjson`。 ## Event catalog @@ -62,61 +62,72 @@ schema 默认 JSON。业务字段说明在 `schema.properties`,`jq_root_path` ```bash # 被 @ 消息 -dws event consume user_im_message_receive_at -f ndjson +dws event consume user_im_message_receive_at --flatten -f ndjson # 指定单聊消息 dws event consume user_im_message_receive_o2o \ --user test-user-001 \ + --flatten \ -f ndjson # 通过 openDingtalkId 指定单聊对端 dws event consume user_im_message_receive_o2o \ --open-dingtalk-id open-user-1 \ + --flatten \ -f ndjson # 指定群消息 dws event consume user_im_message_receive_group \ --group cidxxxxxxxx \ + --flatten \ -f ndjson # 指定发送人的消息(单聊和群聊) dws event consume user_im_message_receive_user \ --user test-user-001 \ + --flatten \ -f ndjson # 通过 openDingtalkId 指定发送人 dws event consume user_im_message_receive_user \ --open-dingtalk-id open-user-1 \ + --flatten \ -f ndjson # 指定单聊已读事件 dws event consume user_im_message_read_o2o \ --user test-user-001 \ + --flatten \ -f ndjson # 指定群聊已读事件 dws event consume user_im_message_read_group \ --group cidxxxxxxxx \ + --flatten \ -f ndjson # 指定单聊撤回事件 dws event consume user_im_message_recall_o2o \ --user test-user-001 \ + --flatten \ -f ndjson # 指定群聊撤回事件 dws event consume user_im_message_recall_group \ --group cidxxxxxxxx \ + --flatten \ -f ndjson # 指定单聊表情回应事件 dws event consume user_im_message_reaction_o2o \ --user test-user-001 \ + --flatten \ -f ndjson # 指定群聊表情回应事件 dws event consume user_im_message_reaction_group \ --group cidxxxxxxxx \ + --flatten \ -f ndjson ``` @@ -124,16 +135,16 @@ dws event consume user_im_message_reaction_group \ | 事件码 | 自测参数 | 触发方式 | |---|---|---| -| `user_im_message_receive_at` | `--duration 10m -f ndjson` | 让任意可触达用户在群里 @ 当前登录用户 | -| `user_im_message_receive_o2o` | `--user ` 或 `--open-dingtalk-id `,加 `--duration 10m -f ndjson` | 让对端用户给当前登录用户发送单聊消息 | -| `user_im_message_receive_group` | `--group --duration 10m -f ndjson` | 让任意用户在该群发送消息 | -| `user_im_message_receive_user` | `--user ` 或 `--open-dingtalk-id `,加 `--duration 10m -f ndjson` | 让指定用户分别在单聊或共同群聊中发送消息 | -| `user_im_message_read_o2o` | `--user ` 或 `--open-dingtalk-id `,加 `--duration 10m -f ndjson` | 当前用户给对端发送单聊消息,再让对端打开并阅读 | -| `user_im_message_read_group` | `--group --duration 10m -f ndjson` | 当前用户在群内发送消息,再让群成员打开并阅读 | -| `user_im_message_recall_o2o` | `--user ` 或 `--open-dingtalk-id `,加 `--duration 10m -f ndjson` | 在指定单聊中发送并撤回一条消息 | -| `user_im_message_recall_group` | `--group --duration 10m -f ndjson` | 在指定群聊中发送并撤回一条消息 | -| `user_im_message_reaction_o2o` | `--user ` 或 `--open-dingtalk-id `,加 `--duration 10m -f ndjson` | 在指定单聊中给消息添加表情回应 | -| `user_im_message_reaction_group` | `--group --duration 10m -f ndjson` | 在指定群聊中给消息添加表情回应 | +| `user_im_message_receive_at` | `--flatten --duration 10m -f ndjson` | 让任意可触达用户在群里 @ 当前登录用户 | +| `user_im_message_receive_o2o` | `--user ` 或 `--open-dingtalk-id `,加 `--flatten --duration 10m -f ndjson` | 让对端用户给当前登录用户发送单聊消息 | +| `user_im_message_receive_group` | `--group --flatten --duration 10m -f ndjson` | 让任意用户在该群发送消息 | +| `user_im_message_receive_user` | `--user ` 或 `--open-dingtalk-id `,加 `--flatten --duration 10m -f ndjson` | 让指定用户分别在单聊或共同群聊中发送消息 | +| `user_im_message_read_o2o` | `--user ` 或 `--open-dingtalk-id `,加 `--flatten --duration 10m -f ndjson` | 当前用户给对端发送单聊消息,再让对端打开并阅读 | +| `user_im_message_read_group` | `--group --flatten --duration 10m -f ndjson` | 当前用户在群内发送消息,再让群成员打开并阅读 | +| `user_im_message_recall_o2o` | `--user ` 或 `--open-dingtalk-id `,加 `--flatten --duration 10m -f ndjson` | 在指定单聊中发送并撤回一条消息 | +| `user_im_message_recall_group` | `--group --flatten --duration 10m -f ndjson` | 在指定群聊中发送并撤回一条消息 | +| `user_im_message_reaction_o2o` | `--user ` 或 `--open-dingtalk-id `,加 `--flatten --duration 10m -f ndjson` | 在指定单聊中给消息添加表情回应 | +| `user_im_message_reaction_group` | `--group --flatten --duration 10m -f ndjson` | 在指定群聊中给消息添加表情回应 | stderr 出现固定就绪行 `[event] ready event_key= bus_pid= subscribe_id=` 表示本地 consume 已连接到事件 bus;父进程等这行再读 stdout。stdout 每行是一个扁平事件 JSON。 @@ -141,7 +152,8 @@ stderr 出现固定就绪行 `[event] ready event_key= bus_pid= subscr | 参数 | 用途 | |---|---| -| `-f ndjson` | 推荐输出,一行一个事件 JSON | +| `--flatten` | 将 `ndjson/json/pretty` 的默认 transport envelope(或原 compact processor)投影为 Agent 可直接读取的顶层业务字段;不能与 `-f raw` 或 `--debug-raw-events` 同时使用 | +| `-f ndjson` | 控制序列化为一行一个 JSON;不改变数据结构 | | `-f json` | 人工查看单条或少量样本;必须配合 `--max-events` 或 `--duration` | | `--max-events ` | 收到 N 条后退出 | | `--duration ` | 到时退出,例如 `30s`、`10m` | @@ -153,11 +165,11 @@ stderr 出现固定就绪行 `[event] ready event_key= bus_pid= subscr | `--filter-json ` | 使用个人事件 Filter DSL 过滤 | | `--debug-raw-events` | 联调用:绕过本地过滤,输出当前 personal stream 实际收到的可解析事件 | -正常 Agent 消费不要使用 `--debug-raw-events`。它会输出当前连接收到的所有可解析事件,只用于判断服务端是否推到了本机连接。 +正常 Agent 消费不要使用 `--debug-raw-events`。它会输出当前连接收到的所有可解析事件,只用于判断服务端是否推到了本机连接,并且不能与 `--flatten` 同时使用。 ## Output parsing -`-f ndjson` 的 stdout 每行就是一个扁平业务事件对象。消息接收事件常见顶层字段: +Agent 使用 `--flatten -f ndjson`,stdout 每行是一个扁平业务事件对象。消息接收事件常见顶层字段: | 字段 | 说明 | |---|---| @@ -173,7 +185,7 @@ stderr 出现固定就绪行 `[event] ready event_key= bus_pid= subscr | `create_time` | 消息创建时间 | | `event_time` | 消息事件时间戳 | -直接按顶层字段解析,不要使用 `fromjson`,也不要依赖内部 transport payload 路径。图片、文件等媒体消息的 `content` 可能是可读描述;需要实际媒体文件时调用 `dws chat message download-media`。 +在 `--flatten` 模式下直接按顶层字段解析,不要再使用 `fromjson` 或内部 payload 路径。不传 `--flatten` 时保持兼容 transport envelope,字段为 `type/event_type/data/headers`,业务 payload 需从 `.data | fromjson` 读取。图片、文件等媒体消息的 `content` 可能是可读描述;需要实际媒体文件时调用 `dws chat message download-media`。 所有动作事件都包含顶层 `type`、`event_id`、`timestamp`、`subscribe_id`、`message_id`、`conversation_id`、`sender`、`sender_open_dingtalk_id` 和 `event_time`。各类动作的专有字段如下: @@ -204,6 +216,7 @@ stderr 出现固定就绪行 `[event] ready event_key= bus_pid= subscr dws event consume user_im_message_receive_group \ --group cidxxxxxxxx \ --query "报警,故障" \ + --flatten \ -f ndjson ``` diff --git a/test/skill_tests.md b/test/skill_tests.md index 62892c4d..02b46151 100644 --- a/test/skill_tests.md +++ b/test/skill_tests.md @@ -1000,27 +1000,27 @@ Agent 安装 dws skill 后,仅依据 skill 提供的参考文档,将自然 **event_event_consume_at_001** - Prompt: 监听有人 @ 我的消息 -- Expected: `dws event consume user_im_message_receive_at -f ndjson` +- Expected: `dws event consume user_im_message_receive_at --flatten -f ndjson` #### `dws event consume user_im_message_receive_o2o` **event_event_consume_o2o_001** - Prompt: 监听我和 userId test-user-001 的单聊消息 -- Expected: `dws event consume user_im_message_receive_o2o --user test-user-001 -f ndjson` +- Expected: `dws event consume user_im_message_receive_o2o --user test-user-001 --flatten -f ndjson` - Flags: `--user` = `test-user-001` **event_event_consume_o2o_open_id_001** - Prompt: 监听我和 openDingtalkId abc 的单聊消息 -- Expected: `dws event consume user_im_message_receive_o2o --open-dingtalk-id abc -f ndjson` +- Expected: `dws event consume user_im_message_receive_o2o --open-dingtalk-id abc --flatten -f ndjson` - Flags: `--open-dingtalk-id` = `abc` **event_event_consume_o2o_003** `[ASK_USER]` - Prompt: 监听我的个人单聊消息 -- Expected: `dws event consume user_im_message_receive_o2o -f ndjson` +- Expected: `dws event consume user_im_message_receive_o2o --flatten -f ndjson` **event_event_consume_o2o_auto_reply_001** - Prompt: 监听我和 userId test-user-001 的单聊消息,并对方发什么自动回复什么 -- Expected: `dws event consume user_im_message_receive_o2o --user test-user-001 -f ndjson` +- Expected: `dws event consume user_im_message_receive_o2o --user test-user-001 --flatten -f ndjson` - Flags: `--user` = `test-user-001` - Contract: 等待 stderr 的 `[event] ready`;从每行 NDJSON 顶层读取 `content` 和 `sender_open_dingtalk_id`,持续读取 stdout,不使用轮询或 output-dir watcher @@ -1028,7 +1028,7 @@ Agent 安装 dws skill 后,仅依据 skill 提供的参考文档,将自然 **event_event_consume_group_001** - Prompt: 监听 openConversationId cid123 的群消息 -- Expected: `dws event consume user_im_message_receive_group --group cid123 -f ndjson` +- Expected: `dws event consume user_im_message_receive_group --group cid123 --flatten -f ndjson` - Flags: `--group` = `cid123` - Contract: 群自动回复时直接读取事件顶层 `conversation_id` @@ -1036,19 +1036,19 @@ Agent 安装 dws skill 后,仅依据 skill 提供的参考文档,将自然 **event_event_consume_user_001** - Prompt: 监听 userId test-user-001 发给我的消息,包括单聊和群聊 -- Expected: `dws event consume user_im_message_receive_user --user test-user-001 -f ndjson` +- Expected: `dws event consume user_im_message_receive_user --user test-user-001 --flatten -f ndjson` - Flags: `--user` = `test-user-001` **event_event_consume_user_open_id_001** - Prompt: 监听 openDingtalkId abc 发给我的消息,包括单聊和群聊 -- Expected: `dws event consume user_im_message_receive_user --open-dingtalk-id abc -f ndjson` +- Expected: `dws event consume user_im_message_receive_user --open-dingtalk-id abc --flatten -f ndjson` - Flags: `--open-dingtalk-id` = `abc` #### `dws event consume user_im_message_read_o2o` **event_event_consume_read_o2o_001** - Prompt: 监听我发给 userId test-user-001 的单聊消息是否已读 -- Expected: `dws event consume user_im_message_read_o2o --user test-user-001 -f ndjson` +- Expected: `dws event consume user_im_message_read_o2o --user test-user-001 --flatten -f ndjson` - Flags: `--user` = `test-user-001` - Contract: 从每行 NDJSON 顶层读取 `message_id`、`reader`、`reader_open_dingtalk_id`、`read_time` @@ -1056,7 +1056,7 @@ Agent 安装 dws skill 后,仅依据 skill 提供的参考文档,将自然 **event_event_consume_read_group_001** - Prompt: 监听 openConversationId cid123 群里我发的消息是否已读 -- Expected: `dws event consume user_im_message_read_group --group cid123 -f ndjson` +- Expected: `dws event consume user_im_message_read_group --group cid123 --flatten -f ndjson` - Flags: `--group` = `cid123` - Contract: 从每行 NDJSON 顶层读取 `conversation_id`、`reader`、`reader_open_dingtalk_id`、`read_time` @@ -1064,7 +1064,7 @@ Agent 安装 dws skill 后,仅依据 skill 提供的参考文档,将自然 **event_event_consume_recall_o2o_001** - Prompt: 监听我和 userId test-user-001 的单聊消息撤回事件 -- Expected: `dws event consume user_im_message_recall_o2o --user test-user-001 -f ndjson` +- Expected: `dws event consume user_im_message_recall_o2o --user test-user-001 --flatten -f ndjson` - Flags: `--user` = `test-user-001` - Contract: 从每行 NDJSON 顶层读取 `message_id`、`recaller`、`recaller_open_dingtalk_id`、`recall_time` @@ -1072,7 +1072,7 @@ Agent 安装 dws skill 后,仅依据 skill 提供的参考文档,将自然 **event_event_consume_recall_group_001** - Prompt: 监听 openConversationId cid123 的群消息撤回事件 -- Expected: `dws event consume user_im_message_recall_group --group cid123 -f ndjson` +- Expected: `dws event consume user_im_message_recall_group --group cid123 --flatten -f ndjson` - Flags: `--group` = `cid123` - Contract: 从每行 NDJSON 顶层读取 `conversation_id`、`recaller`、`recaller_open_dingtalk_id`、`recall_time` @@ -1080,13 +1080,13 @@ Agent 安装 dws skill 后,仅依据 skill 提供的参考文档,将自然 **event_event_consume_reaction_o2o_001** - Prompt: 监听我和 userId test-user-001 的单聊消息贴表情事件 -- Expected: `dws event consume user_im_message_reaction_o2o --user test-user-001 -f ndjson` +- Expected: `dws event consume user_im_message_reaction_o2o --user test-user-001 --flatten -f ndjson` - Flags: `--user` = `test-user-001` - Contract: 从每行 NDJSON 顶层读取 `operator`、`operator_open_dingtalk_id`、`reaction_name`、`operation_type` **event_event_consume_reaction_o2o_open_id_001** - Prompt: 监听我和 openDingtalkId abc 的单聊消息贴表情事件 -- Expected: `dws event consume user_im_message_reaction_o2o --open-dingtalk-id abc -f ndjson` +- Expected: `dws event consume user_im_message_reaction_o2o --open-dingtalk-id abc --flatten -f ndjson` - Flags: `--open-dingtalk-id` = `abc` - Contract: 从每行 NDJSON 顶层读取 `operator`、`operator_open_dingtalk_id`、`reaction_name`、`operation_type` @@ -1094,7 +1094,7 @@ Agent 安装 dws skill 后,仅依据 skill 提供的参考文档,将自然 **event_event_consume_reaction_group_001** - Prompt: 监听 openConversationId cid123 的群消息表情回应事件 -- Expected: `dws event consume user_im_message_reaction_group --group cid123 -f ndjson` +- Expected: `dws event consume user_im_message_reaction_group --group cid123 --flatten -f ndjson` - Flags: `--group` = `cid123` - Contract: 从每行 NDJSON 顶层读取 `conversation_id`、`operator`、`reaction_name`、`reaction_text`、`operation_type` diff --git a/test/unit/skill_docs_policy_test.go b/test/unit/skill_docs_policy_test.go index bdd3fe1c..b60c44f8 100644 --- a/test/unit/skill_docs_policy_test.go +++ b/test/unit/skill_docs_policy_test.go @@ -95,6 +95,7 @@ func TestEventSkillUsesFlatOutputContract(t *testing.T) { text := string(content) for _, required := range []string{ "[event] ready", + "--flatten", "conversation_id", "sender_open_dingtalk_id", "reader_open_dingtalk_id", @@ -109,7 +110,6 @@ func TestEventSkillUsesFlatOutputContract(t *testing.T) { } } for _, retired := range []string{ - ".data | fromjson", "payload.body.", "尚无稳定业务样本", "暂无稳定 payload schema", From 222ee16d51409f2e879ffc84ef2172a7c56e61e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=AE=E9=9B=A8?= Date: Tue, 21 Jul 2026 22:28:54 +0800 Subject: [PATCH 2/2] test(event): close changed-code coverage gaps for flatten output mode Drop the unreachable defensive tag-skip branch in transportEnvelopeSchema (every transport.Event field carries a non-empty JSON tag) and add a unit test for the validatePersonalEventOutputMode success path so the changed code coverage gate reaches 100%. --- internal/app/event_personal_debug_test.go | 8 ++++++++ internal/event/personal/output.go | 3 --- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/internal/app/event_personal_debug_test.go b/internal/app/event_personal_debug_test.go index f5f8cac4..e27429fa 100644 --- a/internal/app/event_personal_debug_test.go +++ b/internal/app/event_personal_debug_test.go @@ -112,6 +112,14 @@ func TestEventConsumeFlattenRejectsRawModesBeforeIdentityResolution(t *testing.T } } +func TestValidatePersonalEventOutputModeAllowsFlattenStructuredFormats(t *testing.T) { + for _, format := range []consume.Format{consume.FormatNDJSON, consume.FormatJSON, consume.FormatPretty, consume.FormatCompact} { + if err := validatePersonalEventOutputMode(true, false, format); err != nil { + t.Fatalf("validatePersonalEventOutputMode(true, false, %q) error = %v", format, err) + } + } +} + func TestEventConsumeFlattenFlagIsForwarded(t *testing.T) { oldRun := eventRunPersonalConsume t.Cleanup(func() { eventRunPersonalConsume = oldRun }) diff --git a/internal/event/personal/output.go b/internal/event/personal/output.go index 63ffb3fa..e880b538 100644 --- a/internal/event/personal/output.go +++ b/internal/event/personal/output.go @@ -400,9 +400,6 @@ func transportEnvelopeSchema(eventKey string) map[string]any { for i := 0; i < eventType.NumField(); i++ { field := eventType.Field(i) name := strings.Split(field.Tag.Get("json"), ",")[0] - if name == "" || name == "-" { - continue - } property := map[string]any{"type": schemaType(field.Type)} switch name { case "type":