diff --git a/.changes/report-entry-submit-require-recipient.md b/.changes/report-entry-submit-require-recipient.md new file mode 100644 index 00000000..76607c4f --- /dev/null +++ b/.changes/report-entry-submit-require-recipient.md @@ -0,0 +1,5 @@ +--- +category: Changed +--- + +- **report entry submit requires recipients** — `dws report entry submit`(及废弃别名 `dws report create`)的 `--to-user-ids` 从可选提升为必填:无接收人的日志提交在服务端仍返回成功,但日志对任何接收人都不可见。openAPI `create_report` 的 `toUserIds` 参数保持可选不动,规则仅在 dws CLI 侧收紧——Cobra required 拦截未传场景,RunE 内对空值/纯分隔符(如 `--to-user-ids ","`)同样 fail-closed 拒绝。修复 [#85724185](https://project.aone.alibaba-inc.com/v2/project/2170318/bug/85724185)。 diff --git a/internal/helpers/report.go b/internal/helpers/report.go index d88168ef..5267a8b1 100644 --- a/internal/helpers/report.go +++ b/internal/helpers/report.go @@ -23,8 +23,8 @@ const ( reportDingtalkOpenLinkDescription = "点击后打开钉钉客户端的日志详情页,可查看或修改刚创建的日志。" reportContentsMaxBytes = 10 * 1024 * 1024 reportDispatchTemplateSuccessHint = "dws report template get --name <模板名> --format json" - reportDispatchTemplateDetailHint = "dws report entry submit --template-id --contents-file --format json" - reportDispatchCreateHint = "dws report template list --format json\n dws report template get --name <模板名> --format json\n dws report entry submit --template-id --contents-file --format json" + reportDispatchTemplateDetailHint = "dws report entry submit --template-id --contents-file --to-user-ids , --format json" + reportDispatchCreateHint = "dws report template list --format json\n dws report template get --name <模板名> --format json\n dws report entry submit --template-id --contents-file --to-user-ids , --format json" reportDispatchDetailHint = "dws report outbox list --cursor 0 --size 20 --format json\n dws report entry get --report-id --format json" reportDispatchStatsHint = "dws report outbox list --cursor 0 --size 20 --format json\n dws report entry stats --report-id --format json" reportDispatchListHint = "dws report inbox list --start \"YYYY-MM-DDT00:00:00+08:00\" --end \"YYYY-MM-DDT23:59:59+08:00\" --cursor 0 --size 20 --format json" @@ -279,14 +279,15 @@ func newReportCommand() *cobra.Command { Long: `按模版提交一份日报。--contents 为 JSON 数组,每项需含 key、sort、content、contentType、type, 与远程 create_report 一致;可先通过 report template list / template get 取得 templateId 与控件定义。 +--to-user-ids 必填:无接收人的提交服务端仍会返回成功,但日志实际对任何人都不可见,因此 dws 侧强制要求接收人。 长内容(含中文换行 / Markdown)建议走 --contents-file 避免 shell 引号问题; 也可用 --contents - 从 stdin 读取。 提交成功后会自动反查详情,并在返回中追加 dingtalkOpenUrl / dingtalkOpenMarkdownLink 跳转链接字段。`, - Example: ` dws report entry submit --template-id TPL_ID --contents '[{"content":"完成开发","sort":"0","key":"今日完成","contentType":"markdown","type":"1"}]' + Example: ` dws report entry submit --template-id TPL_ID --contents '[{"content":"完成开发","sort":"0","key":"今日完成","contentType":"markdown","type":"1"}]' --to-user-ids userId1 # 推荐:长内容走文件 - dws report entry submit --template-id TPL_ID --contents-file ./report.json + dws report entry submit --template-id TPL_ID --contents-file ./report.json --to-user-ids userId1,userId2 # 或 stdin - cat report.json | dws report entry submit --template-id TPL_ID --contents - + cat report.json | dws report entry submit --template-id TPL_ID --contents - --to-user-ids userId1 dws report entry submit --template-id TPL_ID --contents '[...]' --to-chat --to-user-ids userId1,userId2`, RunE: runReportCreate, } @@ -312,14 +313,17 @@ func newReportCommand() *cobra.Command { }, Selection: contract.SelectionSpec{ AgentSummary: "按模版提交一份新日报", - UseWhen: []string{"已取得 templateId 与字段定义,需要按模版提交日报/周报(contents[].key 必须等于模板 field_name)时"}, + UseWhen: []string{ + "已取得 templateId 与字段定义,需要按模版提交日报/周报(contents[].key 必须等于模板 field_name)时", + "提交时必须通过 --to-user-ids 指定至少一个接收人;无接收人的日志提交后对任何人都不可见", + }, AvoidWhen: []string{ "尚未读取模板字段时先用 dws report template list / template get", "只需查看已有日志正文时改用 dws report entry get", }, Examples: []string{ - "dws report entry submit --template-id --contents-file ./report.json --format json", - "dws report entry submit --template-id --contents '[{\"key\":\"今日完成\",\"sort\":\"0\",\"content\":\"完成了需求评审\",\"contentType\":\"markdown\",\"type\":\"1\"}]' --format json", + "dws report entry submit --template-id --contents-file ./report.json --to-user-ids , --format json", + "dws report entry submit --template-id --contents '[{\"key\":\"今日完成\",\"sort\":\"0\",\"content\":\"完成了需求评审\",\"contentType\":\"markdown\",\"type\":\"1\"}]' --to-user-ids --format json", }, }, Parameters: []contract.ParamDecl{ @@ -448,7 +452,7 @@ func newReportCommand() *cobra.Command { createCmd := &cobra.Command{ Use: "create", Short: "[deprecated] 已废弃,请改用 `dws report entry submit`", - Example: ` dws report create --template-id TPL_ID --contents-file ./report.json`, + Example: ` dws report create --template-id TPL_ID --contents-file ./report.json --to-user-ids userId1,userId2`, RunE: withReportDeprecationWarning("create", "entry submit", runReportCreate), } addReportCreateFlags(createCmd) @@ -596,14 +600,24 @@ func runReportCreate(cmd *cobra.Command, args []string) error { ddFrom = "dws" } toChat, _ := cmd.Flags().GetBool("to-chat") + // Cobra required 只拦截 flag 未传,不拦截空值;create_report 对无接收人的 + // 提交仍返回成功,但日志对任何接收人都不可见,因此这里对解析后的空接收人 + // 列表同样 fail-closed。 + toUserIDs := parseReportUserIDs(mustGetFlag(cmd, "to-user-ids")) + if len(toUserIDs) == 0 { + return &CLIError{ + Code: CodeMissingParam, + Message: "to-user-ids is required", + Suggestion: "通过 --to-user-ids userId1,userId2 指定至少一个日志接收人;无接收人的提交服务端仍返回成功,但日志对任何人都不可见", + Operation: "report.create", + } + } toolArgs := map[string]any{ "templateId": tplID, "contents": contents, "ddFrom": ddFrom, "toChat": toChat, - } - if v, _ := cmd.Flags().GetString("to-user-ids"); v != "" { - toolArgs["toUserIds"] = parseReportUserIDs(v) + "toUserIds": toUserIDs, } return callReportCreateWithDetailURL(toolArgs) } @@ -765,7 +779,11 @@ func addReportCreateFlags(cmd *cobra.Command) { cmd.Flags().String("contents-file", "", "从文件读取 contents JSON(推荐用于含中文/换行/Markdown 的长内容,避免 shell 引号转义;优先级:--contents-file > --contents - (stdin) > --contents '')") cmd.Flags().String("dd-from", "dws", "创建来源标识") cmd.Flags().Bool("to-chat", false, "是否发送到日志接收人单聊") - cmd.Flags().String("to-user-ids", "", "接收人 userId,逗号分隔 (可选)") + // 无接收人的 create_report 服务端仍返回成功但日志不可见;openAPI 历史参数 + // 保持可选,dws 侧强制必填(dws report entry submit 与废弃别名 report create + // 共用本函数,两侧 required 标记保持一致)。 + cmd.Flags().String("to-user-ids", "", "接收人 userId,逗号分隔 (必填);无接收人的日志提交后对任何人都不可见") + _ = cmd.MarkFlagRequired("to-user-ids") } // withReportDeprecationWarning 包装旧命令的 RunE:调用时往 stderr 打废弃提醒, diff --git a/internal/helpers/report_recipient_required_test.go b/internal/helpers/report_recipient_required_test.go new file mode 100644 index 00000000..264fc164 --- /dev/null +++ b/internal/helpers/report_recipient_required_test.go @@ -0,0 +1,118 @@ +// 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 + +package helpers + +import ( + "bytes" + "context" + "io" + "strings" + "testing" + + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition" + "github.com/spf13/cobra" +) + +// reportCreateArgsRecorder 记录 create_report 收到的原始参数, +// 用于验证 runReportCreate 对 --to-user-ids 的解析与传递。 +type reportCreateArgsRecorder struct { + response string + format string + args map[string]any +} + +func (c *reportCreateArgsRecorder) CallTool(_ context.Context, _, tool string, args map[string]any) (*edition.ToolResult, error) { + if tool == "create_report" { + c.args = args + } + return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: c.response}}}, nil +} + +func (c *reportCreateArgsRecorder) Format() string { return c.format } +func (*reportCreateArgsRecorder) DryRun() bool { return false } +func (*reportCreateArgsRecorder) Fields() string { return "" } +func (*reportCreateArgsRecorder) JQ() string { return "" } + +func TestReportEntrySubmitRequiresRecipientFlag(t *testing.T) { + t.Cleanup(func() { contract.ClearProductDeclForTest("report") }) + root := newReportCommand() + // 主命令与废弃别名共用 addReportCreateFlags,两侧的 Cobra required + // 标记必须同时存在(NativeRequired 一致性校验要求严格相等)。 + for _, path := range [][]string{{"entry", "submit"}, {"create"}} { + leaf, _, err := root.Find(path) + if err != nil || leaf == nil { + t.Fatalf("%v command missing: %v", path, err) + } + flag := leaf.Flags().Lookup("to-user-ids") + if flag == nil { + t.Fatalf("%v missing --to-user-ids", path) + } + if _, required := flag.Annotations[cobra.BashCompOneRequiredFlag]; !required { + t.Fatalf("%v --to-user-ids must stay Cobra required", path) + } + } + + root.SetOut(io.Discard) + root.SetErr(io.Discard) + root.SilenceErrors = true + root.SilenceUsage = true + // Cobra 的 Execute 会跳回 Root 执行,必须携带完整命令路径; + // required 校验在 RunE 之前拦截未传的 --to-user-ids。 + root.SetArgs([]string{ + "entry", "submit", + "--template-id", "TPL", + "--contents", `[{"key":"k","sort":"0","content":"c","contentType":"markdown","type":"1"}]`, + }) + if err := root.Execute(); err == nil || !strings.Contains(err.Error(), "to-user-ids") { + t.Fatalf("missing --to-user-ids error = %v", err) + } +} + +func TestReportCreateRejectsBlankRecipient(t *testing.T) { + t.Cleanup(func() { contract.ClearProductDeclForTest("report") }) + root := newReportCommand() + submit, _, _ := root.Find([]string{"entry", "submit"}) + _ = submit.Flags().Set("template-id", "TPL") + _ = submit.Flags().Set("contents", `[{"key":"k","sort":"0","content":"c","contentType":"markdown","type":"1"}]`) + // Cobra required 只拦未传;空值/纯分隔符仍会进入 RunE,必须 fail-closed。 + _ = submit.Flags().Set("to-user-ids", " , ") + err := runReportCreate(submit, nil) + cliErr, ok := err.(*CLIError) + if !ok || cliErr.Code != CodeMissingParam { + t.Fatalf("blank to-user-ids error = %#v", err) + } +} + +func TestReportCreatePassesRecipientsToCreateReport(t *testing.T) { + t.Cleanup(func() { contract.ClearProductDeclForTest("report") }) + recorder := &reportCreateArgsRecorder{ + format: "json", + response: `{"reportId":"id","url":"dingtalk://direct"}`, + } + testseam.Protect(t, &deps) + InitDeps(recorder) + out := &bytes.Buffer{} + errOut := &bytes.Buffer{} + deps.Out.w = out + deps.Out.errW = errOut + + root := newReportCommand() + submit, _, _ := root.Find([]string{"entry", "submit"}) + _ = submit.Flags().Set("template-id", "TPL") + _ = submit.Flags().Set("contents", `[{"key":"k","sort":"0","content":"c","contentType":"markdown","type":"1"}]`) + _ = submit.Flags().Set("to-user-ids", " userA , userB ") + if err := runReportCreate(submit, nil); err != nil { + t.Fatalf("runReportCreate: %v", err) + } + got, _ := recorder.args["toUserIds"].([]string) + if len(got) != 2 || got[0] != "userA" || got[1] != "userB" { + t.Fatalf("toUserIds = %#v", recorder.args["toUserIds"]) + } +} diff --git a/scripts/policy/interface-migrations/approved-flag-migrations-v1.json b/scripts/policy/interface-migrations/approved-flag-migrations-v1.json index a1ef7b32..e7a2491c 100644 --- a/scripts/policy/interface-migrations/approved-flag-migrations-v1.json +++ b/scripts/policy/interface-migrations/approved-flag-migrations-v1.json @@ -146,7 +146,7 @@ "scope": "local" } }, - "state": "pending", + "state": "consumed", "reason": "无接收人的日志提交服务端仍返回成功但日志对任何人都不可见;仅在 dws CLI 侧将 --to-user-ids 提升为必填,openAPI create_report 保持可选(bug 85724185)" }, { @@ -166,7 +166,7 @@ "scope": "local" } }, - "state": "pending", + "state": "consumed", "reason": "废弃别名 dws report create(含 dws log create 拼写)与 dws report entry submit 共用 addReportCreateFlags,required 提升必须两侧一致;openAPI create_report 保持可选(bug 85724185)" } ] diff --git a/skills/mono/references/best_practices/05-reporting.md b/skills/mono/references/best_practices/05-reporting.md index 48dfb49d..4d3fe983 100644 --- a/skills/mono/references/best_practices/05-reporting.md +++ b/skills/mono/references/best_practices/05-reporting.md @@ -24,6 +24,6 @@ | query-report | **0. 前置判定**:query 含「查日志 / 看日志 / 我发过的日志 / 收到的日志 / 日志详情」且语义指向钉钉日志 OA 应用?是 → 直接走 `dws report`;否 → 先按 doc/report 分歧澄清
1. 用户说「我发过 / 我创建」→ `report outbox list --cursor 0 --size 20 --format json`;用户说「收到 / 别人发给我」→ `report inbox list --start "" --end "" --cursor 0 --size 20 --format json`
2. 时间 flag 只允许 `--start` / `--end`,禁止 `--start-date` / `--end-date` / `--date`;不要只传裸日期,必须展开完整 ISO;不要先查 `help`,不要预先登录;只有命令返回认证错误时才处理认证
3. 从列表返回中取 `reportId` 留给内部后续调用;如果用户已直接提供 `reportId`,跳过列表
4. 面向用户展示列表时必须基于 `result[]` 拼 Markdown 表:`日期 | 标题 | 发送人 | 状态 | 钉钉链接`;每条 `result[]` 都带这五个中文字段;不要把日志 ID 作为主列;已读状态字段缺失则不展示,不要编造
5. 用户要看正文时再执行 `report entry get --report-id --format json`;用户要统计 / 已读情况时执行 `report entry stats --report-id --format json`
**不要把 inbox list/outbox list 当正文接口**;查询正文必须补 `entry get`
**不要再生成** `report list` / `report sent` / `report detail` / `report stats`(deprecated alias,仍能跑但会打 stderr 警告) | | generate-daily-report | 1. 按[「多源并行采集」](_common/conventions.md#多源并行采集公共模式)执行(时间=今日)
2. 交叉汇总并把日报内容写入临时文件 `.md`(UTF-8,真实换行)
3. **创建文档**:`doc create --name "<日报名>" --content-file .md`(> 200KB 按 [write-doc 兜底](./04-document.md) 走 create 空 → 循环 update) | | generate-weekly-report | 1. 按[「多源并行采集」](_common/conventions.md#多源并行采集公共模式)执行(时间=本周)
2. 交叉对比并把周报内容写入临时文件 `.md`
3. **创建文档**:`doc create --name "<周报名>" --content-file .md`(兜底同上) | -| submit-report | **0. 前置判定**:query 含「钉钉日志 / OA 周报模板 / 我的钉钉日志」等强信号?是 → 继续;否 → 切换到 `generate-weekly-report` 或 `generate-daily-report`(走 dws doc)
1. 按[「多源并行采集」](_common/conventions.md#多源并行采集公共模式)执行(时间=当日)
2. `report template list --format json` → 取 `report_template_id`
3. `report template get --name "<模版名>" --format json` → 取 `result.report_template_fields[]`,每项含 `field_name`/`field_sort`/`field_type`
4. **把 contents 写入临时文件**(避免 shell 引号问题):每项含 `key`/`sort`/`content`/`contentType`/`type` 五个字段,**严格映射** `field_name → key`、`field_sort → sort`、`field_type → type`,再填 `content` 与 `contentType`
5. `report entry submit --template-id --contents-file .json --format json` → CLI 会在提交成功后自动反查详情并追加 `dingtalkOpenMarkdownLink` / `dingtalkOpenUrl` / `dingtalkOpenLink` 字段;取返回的 `reportId` 与钉钉打开链接
6. final reply 优先直接使用 `dingtalkOpenMarkdownLink`,让用户点击跳转钉钉客户端查看 / 修改;仅当 submit 返回中缺少 `dingtalkOpenUrl` 时,才手动执行 `report entry get --report-id --format json` 补取 `result.url`,再包装成 `[在钉钉中查看日志](result.url)`
**不要走 doc 写文档**;**禁止跳过 2/3 步**直接 submit;**禁止把 raw `dingtalk://...` URL 直接粘到回复**,必须包成 markdown link
**不要再生成** `report template detail` / `report create`(deprecated alias,仍能跑但会打 stderr 警告) | +| submit-report | **0. 前置判定**:query 含「钉钉日志 / OA 周报模板 / 我的钉钉日志」等强信号?是 → 继续;否 → 切换到 `generate-weekly-report` 或 `generate-daily-report`(走 dws doc)
1. 按[「多源并行采集」](_common/conventions.md#多源并行采集公共模式)执行(时间=当日)
2. `report template list --format json` → 取 `report_template_id`
3. `report template get --name "<模版名>" --format json` → 取 `result.report_template_fields[]`,每项含 `field_name`/`field_sort`/`field_type`
4. **把 contents 写入临时文件**(避免 shell 引号问题):每项含 `key`/`sort`/`content`/`contentType`/`type` 五个字段,**严格映射** `field_name → key`、`field_sort → sort`、`field_type → type`,再填 `content` 与 `contentType`
5. `report entry submit --template-id --contents-file .json --to-user-ids , --format json` → `--to-user-ids` 必填:无接收人的提交服务端仍返回成功但日志对任何人都不可见;CLI 会在提交成功后自动反查详情并追加 `dingtalkOpenMarkdownLink` / `dingtalkOpenUrl` / `dingtalkOpenLink` 字段;取返回的 `reportId` 与钉钉打开链接
6. final reply 优先直接使用 `dingtalkOpenMarkdownLink`,让用户点击跳转钉钉客户端查看 / 修改;仅当 submit 返回中缺少 `dingtalkOpenUrl` 时,才手动执行 `report entry get --report-id --format json` 补取 `result.url`,再包装成 `[在钉钉中查看日志](result.url)`
**不要走 doc 写文档**;**禁止跳过 2/3 步**直接 submit;**禁止把 raw `dingtalk://...` URL 直接粘到回复**,必须包成 markdown link
**不要再生成** `report template detail` / `report create`(deprecated alias,仍能跑但会打 stderr 警告) | | generate-monthly-report | 1. 按[「多源并行采集」](_common/conventions.md#多源并行采集公共模式)执行(时间=当月)
2. `report outbox list --start "<月初ISO>" --end "<月末ISO>"` → 取当月已提交日志
3. 按周分段归纳并把月报内容写入临时文件 `.md`
4. **创建文档**:`doc create --name "<月报名>" --content-file .md`(兜底同上) | | generate-topic-report | 1. 提取主题关键词;推断时间范围("最近"默认近 30 天)
2. 按[「多源并行采集」](_common/conventions.md#多源并行采集公共模式)执行
3. 按时间线排列,交叉归纳核心结论/决策/行动项/未解决问题/演进脉络,并把内容写入临时文件 `.md`
4. **创建文档**:`doc create --name "<报告名>" --content-file .md`(兜底同上) | diff --git a/skills/mono/references/products/report.md b/skills/mono/references/products/report.md index 6db1d12e..2bff3697 100644 --- a/skills/mono/references/products/report.md +++ b/skills/mono/references/products/report.md @@ -24,7 +24,7 @@ - `dws report outbox list` = 列出**我发出**的日报(我创建或提交的)。 - `dws report entry get --report-id ` = 读取单份日报正文 + 钉钉跳转链接。 - `dws report entry stats --report-id ` = 读取单份日报的已读统计。 -- `dws report entry submit --template-id ... --contents-file ...` = 按模版提交一份新日报。 +- `dws report entry submit --template-id ... --contents-file ... --to-user-ids ...` = 按模版提交一份新日报(--to-user-ids 必填:无接收人的日志对任何人都不可见)。 - `dws report template list` = 列出可用日报模版。 - `dws report template get --name "<模版名>"` = 读取单个模版的字段定义(contents 拼装来源)。 @@ -127,7 +127,7 @@ CLI 列表命令只返回 JSON-first 数据,不把 Markdown 表作为裸文本 1. `dws report template list --format json` — 取 `report_template_id` 与可见模版名 2. `dws report template get --name "<模版名>" --format json` — 取 `result.report_template_fields[]`,每项含 `field_name` / `field_sort` / `field_type` -3. `dws report entry submit --template-id --contents-file --format json` — contents 数组按上面「字段映射」严格对齐第 2 步:`field_name → key`,`field_sort → sort`,`field_type → type`,再填 `content` 与 `contentType`;CLI 提交成功后会自动反查详情并追加钉钉打开链接字段,返回中直接取 `reportId` 与 `dingtalkOpenMarkdownLink` / `dingtalkOpenUrl` +3. `dws report entry submit --template-id --contents-file --to-user-ids , --format json` — contents 数组按上面「字段映射」严格对齐第 2 步:`field_name → key`,`field_sort → sort`,`field_type → type`,再填 `content` 与 `contentType`;`--to-user-ids` 必填:无接收人的提交服务端仍返回成功但日志对任何人都不可见;CLI 提交成功后会自动反查详情并追加钉钉打开链接字段,返回中直接取 `reportId` 与 `dingtalkOpenMarkdownLink` / `dingtalkOpenUrl` 4. 仅当第 3 步返回中缺少 `dingtalkOpenUrl` 时,执行 `dws report entry get --report-id --format json` 补取 `result.url`(`dingtalk://...` 协议深链接)。final reply 中优先使用 `dingtalkOpenMarkdownLink`,否则用 `[在钉钉中查看日志](dingtalkOpenUrl)`。**禁止把 raw `dingtalk://...` URL 原样写进回复**,必须包成 markdown link 让用户可点击跳转钉钉客户端 跳步风险(已实证): @@ -136,6 +136,7 @@ CLI 列表命令只返回 JSON-first 数据,不把 Markdown 表作为裸文本 - 跳过第 2 步用 LLM 经验编 `key` 名 → 服务端返回 `PARAM_ERROR`,且**不告诉你哪个字段错**;服务端 PARAM_ERROR 信号弱,事后无法定位,**只能靠前置 schema 同步避免**; - 未取到 `dingtalkOpenUrl` 且不补查 `entry get` → 用户拿不到跳转链接,无法在钉钉客户端打开刚提交的日志查看 / 修改; - 用 `--contents` 直传长 JSON → shell 引号转义破坏 JSON → `INPUT_INVALID_JSON`。**长内容务必走 `--contents-file ` 或 `--contents -` (stdin)**。 +- 不传 `--to-user-ids` → 服务端仍返回成功但日志对任何接收人都不可见;CLI 已强制必填,缺 flag 或传空值都会被拒绝 - contents JSON 大小限制为 10MB,**不支持分批次提交**。超过限制需精简内容或拆分为多个独立日志提交。 推荐:Agent 在多轮场景中应在内存里持久化第 1/2 步的结果,避免每轮重新跑。 @@ -167,22 +168,22 @@ Usage: dws report entry submit [flags] Example: # 推荐:长内容走文件,避免 shell 引号问题 - dws report entry submit --template-id --contents-file ./report.json --format json + dws report entry submit --template-id --contents-file ./report.json --to-user-ids , --format json # stdin 输入 - cat report.json | dws report entry submit --template-id --contents - --format json + cat report.json | dws report entry submit --template-id --contents - --to-user-ids --format json # 内联(短内容) dws report entry submit --template-id \ --contents '[{"key":"今日完成","sort":"0","content":"完成了需求评审","contentType":"markdown","type":"1"}]' \ - --format json + --to-user-ids --format json Flags: --template-id string 日志模版 ID (必填),从 template list 返回中取 --contents string 日志内容 JSON 数组 (必填,或用 --contents-file);传 `-` 表示从 stdin 读取 --contents-file string 从文件读取 contents JSON(推荐用于含中文/换行/Markdown 的长内容) --dd-from string 创建来源标识 (默认 dws) --to-chat 是否发送到日志接收人单聊 (默认 false,传本 flag 则为 true) - --to-user-ids string 接收人 userId,逗号分隔 (可选) + --to-user-ids string 接收人 userId,逗号分隔 (必填);无接收人的日志提交后对任何人都不可见 ``` @@ -316,8 +317,8 @@ dws report template list --format json # 2. 按名称读取模版字段定义 dws report template get --name "日报" --format json -# 2b. 提交日志(从步骤 1/2 取 templateId 与 contents 字段)— 推荐 --contents-file 传入避免 shell 引号 -dws report entry submit --template-id --contents-file ./report.json --format json +# 2b. 提交日志(从步骤 1/2 取 templateId 与 contents 字段)— 推荐 --contents-file 传入避免 shell 引号;--to-user-ids 必填 +dws report entry submit --template-id --contents-file ./report.json --to-user-ids , --format json # submit 成功会自动反查详情并追加 dingtalkOpenMarkdownLink / dingtalkOpenUrl; # final reply 直接使用 dingtalkOpenMarkdownLink: [在钉钉中查看日志](dingtalk://...) diff --git a/skills/multi/dingtalk-misc/references/05-reporting.md b/skills/multi/dingtalk-misc/references/05-reporting.md index a3997286..8be17bbe 100644 --- a/skills/multi/dingtalk-misc/references/05-reporting.md +++ b/skills/multi/dingtalk-misc/references/05-reporting.md @@ -24,6 +24,6 @@ | query-report | **0. 前置判定**:query 含「查日志 / 看日志 / 我发过的日志 / 收到的日志 / 日志详情」且语义指向钉钉日志 OA 应用?是 → 直接走 `dws report`;CSV 附件、群聊导出日志、系统日志或聊天记录核验不属于 OA 日志,应转到文件/群聊/表格相关 skill;其余歧义先按 doc/report 分歧澄清
1. 第一条有效查询必须按视角选择新命令:用户说「我发过 / 我创建 / 已发送」→ `report outbox list --cursor 0 --size 20 --format json`;用户说「收到 / 收件箱 / 别人发给我 / 最近收到」→ `report inbox list --start "" --end "" --cursor 0 --size 20 --format json`;不要先生成 `report list` / `report sent` 等 deprecated alias,也不要用 inbox 代替 outbox
2. 时间 flag 只允许 `--start` / `--end`;裸日期必须展开完整 ISO + `+08:00`;禁止 `--start-date` / `--end-date` / `--date`、UTC `Z`、`date -u`;用户只说「最近 / 近期 / 最近收到 / 最近一周」默认最近 7 天;`--size` 最大 20,更多结果按 `cursor` 分页,禁止传 50/100
3. 按发件人查收件箱时,先 `aisearch person --query "<姓名>" --dimension name --format json` 取 `userId/staffId`,再给 `inbox list` 加 `--sender-user-ids `;如果列表中找不到目标发件人或目标标记日志,必须说明不可见 / 未找到,不得改选其他发件人或其他日志
4. 从列表返回中取 `reportId` 留给内部后续调用;如果用户已直接提供 `reportId`,跳过列表;面向用户展示列表时基于 `result[]` 拼 Markdown 表:`日期 | 标题 | 发送人 | 状态 | 钉钉链接`,不要把日志 ID 作为主列,缺失字段不编造
5. 用户要正文、详情、汇总、总结多篇日志或检查内容时,必须对选中的每篇日志逐条执行 `report entry get --report-id --format json`;例如“总结最近收到的 5 篇”应先 list 取前 5 篇(不足 5 篇按实际数量说明),再执行相同数量的 `entry get`;用户要统计 / 已读情况时执行 `report entry stats --report-id --format json`
**不要把 inbox list/outbox list 当正文接口**;查询正文必须补 `entry get`
**不要再生成** `report list` / `report sent` / `report detail` / `report stats`(deprecated alias,仍能跑但会打 stderr 警告) | | generate-daily-report | 1. 按[「多源并行采集」](./report-conventions.md#多源并行采集公共模式)执行(时间=今日)
2. 交叉汇总并把日报内容写入临时文件 `.md`(UTF-8,真实换行)
3. **创建文档**:`doc create --name "<日报名>" --content-file .md`(> 200KB 按 write-doc 兜底(见 `dingtalk-doc/references/04-document.md`) 走 create 空 → 循环 update) | | generate-weekly-report | 1. 按[「多源并行采集」](./report-conventions.md#多源并行采集公共模式)执行(时间=本周)
2. 交叉对比并把周报内容写入临时文件 `.md`
3. **创建文档**:`doc create --name "<周报名>" --content-file .md`(兜底同上) | -| submit-report | **0. 前置判定**:query 含「钉钉日志 / OA 周报模板 / 我的钉钉日志」等强信号?是 → 继续;否 → 切换到 `generate-weekly-report` 或 `generate-daily-report`(走 dws doc)
1. 按[「多源并行采集」](./report-conventions.md#多源并行采集公共模式)执行(时间=当日)
2. `report template list --format json` → 取 `report_template_id`
3. `report template get --name "<模版名>" --format json` → 取 `result.report_template_fields[]`,每项含 `field_name`/`field_sort`/`field_type`
4. **把 contents 写入临时文件**(避免 shell 引号问题):每项含 `key`/`sort`/`content`/`contentType`/`type` 五个字段,**严格映射** `field_name → key`、`field_sort → sort`、`field_type → type`,再填 `content` 与 `contentType`
5. `report entry submit --template-id --contents-file .json --format json` → CLI 会在提交成功后自动反查详情并追加 `dingtalkOpenMarkdownLink` / `dingtalkOpenUrl` / `dingtalkOpenLink` 字段;取返回的 `reportId` 与钉钉打开链接
6. final reply 优先直接使用 `dingtalkOpenMarkdownLink`,让用户点击跳转钉钉客户端查看 / 修改;仅当 submit 返回中缺少 `dingtalkOpenUrl` 时,才手动执行 `report entry get --report-id --format json` 补取 `result.url`,再包装成 `[在钉钉中查看日志](result.url)`
**不要走 doc 写文档**;**禁止跳过 2/3 步**直接 submit;**禁止把 raw `dingtalk://...` URL 直接粘到回复**,必须包成 markdown link
**不要再生成** `report template detail` / `report create`(deprecated alias,仍能跑但会打 stderr 警告) | +| submit-report | **0. 前置判定**:query 含「钉钉日志 / OA 周报模板 / 我的钉钉日志」等强信号?是 → 继续;否 → 切换到 `generate-weekly-report` 或 `generate-daily-report`(走 dws doc)
1. 按[「多源并行采集」](./report-conventions.md#多源并行采集公共模式)执行(时间=当日)
2. `report template list --format json` → 取 `report_template_id`
3. `report template get --name "<模版名>" --format json` → 取 `result.report_template_fields[]`,每项含 `field_name`/`field_sort`/`field_type`
4. **把 contents 写入临时文件**(避免 shell 引号问题):每项含 `key`/`sort`/`content`/`contentType`/`type` 五个字段,**严格映射** `field_name → key`、`field_sort → sort`、`field_type → type`,再填 `content` 与 `contentType`
5. `report entry submit --template-id --contents-file .json --to-user-ids , --format json` → `--to-user-ids` 必填:无接收人的提交服务端仍返回成功但日志对任何人都不可见;CLI 会在提交成功后自动反查详情并追加 `dingtalkOpenMarkdownLink` / `dingtalkOpenUrl` / `dingtalkOpenLink` 字段;取返回的 `reportId` 与钉钉打开链接
6. final reply 优先直接使用 `dingtalkOpenMarkdownLink`,让用户点击跳转钉钉客户端查看 / 修改;仅当 submit 返回中缺少 `dingtalkOpenUrl` 时,才手动执行 `report entry get --report-id --format json` 补取 `result.url`,再包装成 `[在钉钉中查看日志](result.url)`
**不要走 doc 写文档**;**禁止跳过 2/3 步**直接 submit;**禁止把 raw `dingtalk://...` URL 直接粘到回复**,必须包成 markdown link
**不要再生成** `report template detail` / `report create`(deprecated alias,仍能跑但会打 stderr 警告) | | generate-monthly-report | 1. 按[「多源并行采集」](./report-conventions.md#多源并行采集公共模式)执行(时间=当月)
2. `report outbox list --start "<月初ISO>" --end "<月末ISO>"` → 取当月已提交日志
3. 按周分段归纳并把月报内容写入临时文件 `.md`
4. **创建文档**:`doc create --name "<月报名>" --content-file .md`(兜底同上) | | generate-topic-report | 1. 提取主题关键词;推断时间范围("最近"默认近 30 天)
2. 按[「多源并行采集」](./report-conventions.md#多源并行采集公共模式)执行
3. 按时间线排列,交叉归纳核心结论/决策/行动项/未解决问题/演进脉络,并把内容写入临时文件 `.md`
4. **创建文档**:`doc create --name "<报告名>" --content-file .md`(兜底同上) | diff --git a/skills/multi/dingtalk-misc/references/report.md b/skills/multi/dingtalk-misc/references/report.md index cf73d9d5..ea547cdc 100644 --- a/skills/multi/dingtalk-misc/references/report.md +++ b/skills/multi/dingtalk-misc/references/report.md @@ -24,7 +24,7 @@ - `dws report outbox list` = 列出**我发出**的日报(我创建或提交的)。 - `dws report entry get --report-id --format json` = 读取单份日报正文 + 钉钉跳转链接。 - `dws report entry stats --report-id --format json` = 读取单份日报的已读统计。 -- `dws report entry submit --template-id ... --contents-file ...` = 按模版提交一份新日报。 +- `dws report entry submit --template-id ... --contents-file ... --to-user-ids ...` = 按模版提交一份新日报(--to-user-ids 必填:无接收人的日志对任何人都不可见)。 - `dws report template list` = 列出可用日报模版。 - `dws report template get --name "<模版名>"` = 读取单个模版的字段定义(contents 拼装来源)。 @@ -153,7 +153,7 @@ CLI 列表命令只返回 JSON-first 数据,不把 Markdown 表作为裸文本 1. `dws report template list --format json` — 取 `report_template_id` 与可见模版名 2. `dws report template get --name "<模版名>" --format json` — 取 `result.report_template_fields[]`,每项含 `field_name` / `field_sort` / `field_type` -3. `dws report entry submit --template-id --contents-file --format json` — contents 数组按上面「字段映射」严格对齐第 2 步:`field_name → key`,`field_sort → sort`,`field_type → type`,再填 `content` 与 `contentType`;CLI 提交成功后会自动反查详情并追加钉钉打开链接字段,返回中直接取 `reportId` 与 `dingtalkOpenMarkdownLink` / `dingtalkOpenUrl` +3. `dws report entry submit --template-id --contents-file --to-user-ids , --format json` — contents 数组按上面「字段映射」严格对齐第 2 步:`field_name → key`,`field_sort → sort`,`field_type → type`,再填 `content` 与 `contentType`;`--to-user-ids` 必填:无接收人的提交服务端仍返回成功但日志对任何人都不可见;CLI 提交成功后会自动反查详情并追加钉钉打开链接字段,返回中直接取 `reportId` 与 `dingtalkOpenMarkdownLink` / `dingtalkOpenUrl` 4. 仅当第 3 步返回中缺少 `dingtalkOpenUrl` 时,执行 `dws report entry get --report-id --format json` 补取 `result.url`(`dingtalk://...` 协议深链接)。final reply 中优先使用 `dingtalkOpenMarkdownLink`,否则用 `[在钉钉中查看日志](dingtalkOpenUrl)`。**禁止把 raw `dingtalk://...` URL 原样写进回复**,必须包成 markdown link 让用户可点击跳转钉钉客户端 跳步风险(已实证): @@ -162,6 +162,7 @@ CLI 列表命令只返回 JSON-first 数据,不把 Markdown 表作为裸文本 - 跳过第 2 步用 LLM 经验编 `key` 名 → 服务端返回 `PARAM_ERROR`,且**不告诉你哪个字段错**;服务端 PARAM_ERROR 信号弱,事后无法定位,**只能靠前置 schema 同步避免**; - 未取到 `dingtalkOpenUrl` 且不补查 `entry get` → 用户拿不到跳转链接,无法在钉钉客户端打开刚提交的日志查看 / 修改; - 用 `--contents` 直传长 JSON → shell 引号转义破坏 JSON → `INPUT_INVALID_JSON`。**长内容务必走 `--contents-file ` 或 `--contents -` (stdin)**。 +- 不传 `--to-user-ids` → 服务端仍返回成功但日志对任何接收人都不可见;CLI 已强制必填,缺 flag 或传空值都会被拒绝 - contents JSON 大小限制为 10MB,**不支持分批次提交**。超过限制需精简内容或拆分为多个独立日志提交。 推荐:Agent 在多轮场景中应在内存里持久化第 1/2 步的结果,避免每轮重新跑。 @@ -193,22 +194,22 @@ Usage: dws report entry submit [flags] Example: # 推荐:长内容走文件,避免 shell 引号问题 - dws report entry submit --template-id --contents-file ./report.json --format json + dws report entry submit --template-id --contents-file ./report.json --to-user-ids , --format json # stdin 输入 - cat report.json | dws report entry submit --template-id --contents - --format json + cat report.json | dws report entry submit --template-id --contents - --to-user-ids --format json # 内联(短内容) dws report entry submit --template-id \ --contents '[{"key":"今日完成","sort":"0","content":"完成了需求评审","contentType":"markdown","type":"1"}]' \ - --format json + --to-user-ids --format json Flags: --template-id string 日志模版 ID (必填),从 template list 返回中取 --contents string 日志内容 JSON 数组 (必填,或用 --contents-file);传 `-` 表示从 stdin 读取 --contents-file string 从文件读取 contents JSON(推荐用于含中文/换行/Markdown 的长内容) --dd-from string 创建来源标识 (默认 dws) --to-chat 是否发送到日志接收人单聊 (默认 false,传本 flag 则为 true) - --to-user-ids string 接收人 userId,逗号分隔 (可选) + --to-user-ids string 接收人 userId,逗号分隔 (必填);无接收人的日志提交后对任何人都不可见 ``` @@ -342,8 +343,8 @@ dws report template list --format json # 2. 按名称读取模版字段定义 dws report template get --name "日报" --format json -# 2b. 提交日志(从步骤 1/2 取 templateId 与 contents 字段)— 推荐 --contents-file 传入避免 shell 引号 -dws report entry submit --template-id --contents-file ./report.json --format json +# 2b. 提交日志(从步骤 1/2 取 templateId 与 contents 字段)— 推荐 --contents-file 传入避免 shell 引号;--to-user-ids 必填 +dws report entry submit --template-id --contents-file ./report.json --to-user-ids , --format json # submit 成功会自动反查详情并追加 dingtalkOpenMarkdownLink / dingtalkOpenUrl; # final reply 直接使用 dingtalkOpenMarkdownLink: [在钉钉中查看日志](dingtalk://...) @@ -461,7 +462,7 @@ dws report outbox list --cursor 0 --size 20 --format json |--------|------| | "今天 / 最近 / 最近一周收到的日志" | `dws report inbox list --start "" --end "" --cursor 0 --size 20 --format json` | | "看日志模版" | `dws report template list --format json` → `dws report template get --name "<模版名>" --format json` | -| "提交日报 / 周报(按模版)" | `dws report entry submit --template-id --contents-file --format json` | +| "提交日报 / 周报(按模版)" | `dws report entry submit --template-id --contents-file --to-user-ids , --format json` | | "我已发送 / 我创建 / 我发过的日志" | `dws report outbox list --cursor 0 --size 20 --format json` | | "看日志正文 / 总结多篇日志" | 列表取 `reportId` 后逐篇 `dws report entry get --report-id --format json` | | "日志已读统计" | `dws report entry stats --report-id --format json` |