Merge branch 'main' into fix/remove-download-domain-allowlist

This commit is contained in:
liyuan333
2026-08-26 08:40:38 +08:00
committed by GitHub
8 changed files with 177 additions and 34 deletions
@@ -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)。
+31 -13
View File
@@ -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 <templateId> --contents-file <tmp.json> --format json"
reportDispatchCreateHint = "dws report template list --format json\n dws report template get --name <模板名> --format json\n dws report entry submit --template-id <templateId> --contents-file <tmp.json> --format json"
reportDispatchTemplateDetailHint = "dws report entry submit --template-id <templateId> --contents-file <tmp.json> --to-user-ids <userId1>,<userId2> --format json"
reportDispatchCreateHint = "dws report template list --format json\n dws report template get --name <模板名> --format json\n dws report entry submit --template-id <templateId> --contents-file <tmp.json> --to-user-ids <userId1>,<userId2> --format json"
reportDispatchDetailHint = "dws report outbox list --cursor 0 --size 20 --format json\n dws report entry get --report-id <reportId> --format json"
reportDispatchStatsHint = "dws report outbox list --cursor 0 --size 20 --format json\n dws report entry stats --report-id <reportId> --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 <templateId> --contents-file ./report.json --format json",
"dws report entry submit --template-id <templateId> --contents '[{\"key\":\"今日完成\",\"sort\":\"0\",\"content\":\"完成了需求评审\",\"contentType\":\"markdown\",\"type\":\"1\"}]' --format json",
"dws report entry submit --template-id <templateId> --contents-file ./report.json --to-user-ids <userId1>,<userId2> --format json",
"dws report entry submit --template-id <templateId> --contents '[{\"key\":\"今日完成\",\"sort\":\"0\",\"content\":\"完成了需求评审\",\"contentType\":\"markdown\",\"type\":\"1\"}]' --to-user-ids <userId1> --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 '<json>')")
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 打废弃提醒,
@@ -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"])
}
}
@@ -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)"
}
]
@@ -24,6 +24,6 @@
| query-report | **0. 前置判定**:query 含「查日志 / 看日志 / 我发过的日志 / 收到的日志 / 日志详情」且语义指向钉钉日志 OA 应用?是 → 直接走 `dws report`;否 → 先按 doc/report 分歧澄清<br>1. 用户说「我发过 / 我创建」→ `report outbox list --cursor 0 --size 20 --format json`;用户说「收到 / 别人发给我」→ `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`<br>2. 时间 flag 只允许 `--start` / `--end`,禁止 `--start-date` / `--end-date` / `--date`;不要只传裸日期,必须展开完整 ISO;不要先查 `help`,不要预先登录;只有命令返回认证错误时才处理认证<br>3. 从列表返回中取 `reportId` 留给内部后续调用;如果用户已直接提供 `reportId`,跳过列表<br>4. 面向用户展示列表时必须基于 `result[]` 拼 Markdown 表:`日期 | 标题 | 发送人 | 状态 | 钉钉链接`;每条 `result[]` 都带这五个中文字段;不要把日志 ID 作为主列;已读状态字段缺失则不展示,不要编造<br>5. 用户要看正文时再执行 `report entry get --report-id <reportId> --format json`;用户要统计 / 已读情况时执行 `report entry stats --report-id <reportId> --format json`<br>**不要把 inbox list/outbox list 当正文接口**;查询正文必须补 `entry get`<br>**不要再生成** `report list` / `report sent` / `report detail` / `report stats`(deprecated alias,仍能跑但会打 stderr 警告) |
| generate-daily-report | 1. 按[「多源并行采集」](_common/conventions.md#多源并行采集公共模式)执行(时间=今日)<br>2. 交叉汇总并把日报内容写入临时文件 `<tmp>.md`(UTF-8,真实换行)<br>3. **创建文档**:`doc create --name "<日报名>" --content-file <tmp>.md`(> 200KB 按 [write-doc 兜底](./04-document.md) 走 create 空 → 循环 update) |
| generate-weekly-report | 1. 按[「多源并行采集」](_common/conventions.md#多源并行采集公共模式)执行(时间=本周)<br>2. 交叉对比并把周报内容写入临时文件 `<tmp>.md`<br>3. **创建文档**:`doc create --name "<周报名>" --content-file <tmp>.md`(兜底同上) |
| submit-report | **0. 前置判定**:query 含「钉钉日志 / OA 周报模板 / 我的钉钉日志」等强信号?是 → 继续;否 → 切换到 `generate-weekly-report` 或 `generate-daily-report`(走 dws doc)<br>1. 按[「多源并行采集」](_common/conventions.md#多源并行采集公共模式)执行(时间=当日)<br>2. `report template list --format json` → 取 `report_template_id`<br>3. `report template get --name "<模版名>" --format json` → 取 `result.report_template_fields[]`,每项含 `field_name`/`field_sort`/`field_type`<br>4. **把 contents 写入临时文件**(避免 shell 引号问题):每项含 `key`/`sort`/`content`/`contentType`/`type` 五个字段,**严格映射** `field_name → key`、`field_sort → sort`、`field_type → type`,再填 `content` 与 `contentType`<br>5. `report entry submit --template-id <id> --contents-file <tmp>.json --format json` → CLI 会在提交成功后自动反查详情并追加 `dingtalkOpenMarkdownLink` / `dingtalkOpenUrl` / `dingtalkOpenLink` 字段;取返回的 `reportId` 与钉钉打开链接<br>6. final reply 优先直接使用 `dingtalkOpenMarkdownLink`,让用户点击跳转钉钉客户端查看 / 修改;仅当 submit 返回中缺少 `dingtalkOpenUrl` 时,才手动执行 `report entry get --report-id <reportId> --format json` 补取 `result.url`,再包装成 `[在钉钉中查看日志](result.url)`<br>**不要走 doc 写文档**;**禁止跳过 2/3 步**直接 submit;**禁止把 raw `dingtalk://...` URL 直接粘到回复**,必须包成 markdown link<br>**不要再生成** `report template detail` / `report create`(deprecated alias,仍能跑但会打 stderr 警告) |
| submit-report | **0. 前置判定**:query 含「钉钉日志 / OA 周报模板 / 我的钉钉日志」等强信号?是 → 继续;否 → 切换到 `generate-weekly-report` 或 `generate-daily-report`(走 dws doc)<br>1. 按[「多源并行采集」](_common/conventions.md#多源并行采集公共模式)执行(时间=当日)<br>2. `report template list --format json` → 取 `report_template_id`<br>3. `report template get --name "<模版名>" --format json` → 取 `result.report_template_fields[]`,每项含 `field_name`/`field_sort`/`field_type`<br>4. **把 contents 写入临时文件**(避免 shell 引号问题):每项含 `key`/`sort`/`content`/`contentType`/`type` 五个字段,**严格映射** `field_name → key`、`field_sort → sort`、`field_type → type`,再填 `content` 与 `contentType`<br>5. `report entry submit --template-id <id> --contents-file <tmp>.json --to-user-ids <userId1>,<userId2> --format json` → `--to-user-ids` 必填:无接收人的提交服务端仍返回成功但日志对任何人都不可见;CLI 会在提交成功后自动反查详情并追加 `dingtalkOpenMarkdownLink` / `dingtalkOpenUrl` / `dingtalkOpenLink` 字段;取返回的 `reportId` 与钉钉打开链接<br>6. final reply 优先直接使用 `dingtalkOpenMarkdownLink`,让用户点击跳转钉钉客户端查看 / 修改;仅当 submit 返回中缺少 `dingtalkOpenUrl` 时,才手动执行 `report entry get --report-id <reportId> --format json` 补取 `result.url`,再包装成 `[在钉钉中查看日志](result.url)`<br>**不要走 doc 写文档**;**禁止跳过 2/3 步**直接 submit;**禁止把 raw `dingtalk://...` URL 直接粘到回复**,必须包成 markdown link<br>**不要再生成** `report template detail` / `report create`(deprecated alias,仍能跑但会打 stderr 警告) |
| generate-monthly-report | 1. 按[「多源并行采集」](_common/conventions.md#多源并行采集公共模式)执行(时间=当月)<br>2. `report outbox list --start "<月初ISO>" --end "<月末ISO>"` → 取当月已提交日志<br>3. 按周分段归纳并把月报内容写入临时文件 `<tmp>.md`<br>4. **创建文档**:`doc create --name "<月报名>" --content-file <tmp>.md`(兜底同上) |
| generate-topic-report | 1. 提取主题关键词;推断时间范围("最近"默认近 30 天)<br>2. 按[「多源并行采集」](_common/conventions.md#多源并行采集公共模式)执行<br>3. 按时间线排列,交叉归纳核心结论/决策/行动项/未解决问题/演进脉络,并把内容写入临时文件 `<tmp>.md`<br>4. **创建文档**:`doc create --name "<报告名>" --content-file <tmp>.md`(兜底同上) |
+9 -8
View File
@@ -24,7 +24,7 @@
- `dws report outbox list` = 列出**我发出**的日报(我创建或提交的)。
- `dws report entry get --report-id <reportId>` = 读取单份日报正文 + 钉钉跳转链接。
- `dws report entry stats --report-id <reportId>` = 读取单份日报的已读统计。
- `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 <id> --contents-file <tmp.json> --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 <id> --contents-file <tmp.json> --to-user-ids <userId1>,<userId2> --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 <reportId> --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 <path>` 或 `--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 <templateId> --contents-file ./report.json --format json
dws report entry submit --template-id <templateId> --contents-file ./report.json --to-user-ids <userId1>,<userId2> --format json
# stdin 输入
cat report.json | dws report entry submit --template-id <templateId> --contents - --format json
cat report.json | dws report entry submit --template-id <templateId> --contents - --to-user-ids <userId1> --format json
# 内联(短内容)
dws report entry submit --template-id <templateId> \
--contents '[{"key":"今日完成","sort":"0","content":"完成了需求评审","contentType":"markdown","type":"1"}]' \
--format json
--to-user-ids <userId1> --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 <templateId> --contents-file ./report.json --format json
# 2b. 提交日志(从步骤 1/2 取 templateId 与 contents 字段)— 推荐 --contents-file 传入避免 shell 引号;--to-user-ids 必填
dws report entry submit --template-id <templateId> --contents-file ./report.json --to-user-ids <userId1>,<userId2> --format json
# submit 成功会自动反查详情并追加 dingtalkOpenMarkdownLink / dingtalkOpenUrl;
# final reply 直接使用 dingtalkOpenMarkdownLink: [在钉钉中查看日志](dingtalk://...)
@@ -24,6 +24,6 @@
| query-report | **0. 前置判定**:query 含「查日志 / 看日志 / 我发过的日志 / 收到的日志 / 日志详情」且语义指向钉钉日志 OA 应用?是 → 直接走 `dws report`;CSV 附件、群聊导出日志、系统日志或聊天记录核验不属于 OA 日志,应转到文件/群聊/表格相关 skill;其余歧义先按 doc/report 分歧澄清<br>1. 第一条有效查询必须按视角选择新命令:用户说「我发过 / 我创建 / 已发送」→ `report outbox list --cursor 0 --size 20 --format json`;用户说「收到 / 收件箱 / 别人发给我 / 最近收到」→ `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`;不要先生成 `report list` / `report sent` 等 deprecated alias,也不要用 inbox 代替 outbox<br>2. 时间 flag 只允许 `--start` / `--end`;裸日期必须展开完整 ISO + `+08:00`;禁止 `--start-date` / `--end-date` / `--date`、UTC `Z`、`date -u`;用户只说「最近 / 近期 / 最近收到 / 最近一周」默认最近 7 天;`--size` 最大 20,更多结果按 `cursor` 分页,禁止传 50/100<br>3. 按发件人查收件箱时,先 `aisearch person --query "<姓名>" --dimension name --format json` 取 `userId/staffId`,再给 `inbox list` 加 `--sender-user-ids <id>`;如果列表中找不到目标发件人或目标标记日志,必须说明不可见 / 未找到,不得改选其他发件人或其他日志<br>4. 从列表返回中取 `reportId` 留给内部后续调用;如果用户已直接提供 `reportId`,跳过列表;面向用户展示列表时基于 `result[]` 拼 Markdown 表:`日期 | 标题 | 发送人 | 状态 | 钉钉链接`,不要把日志 ID 作为主列,缺失字段不编造<br>5. 用户要正文、详情、汇总、总结多篇日志或检查内容时,必须对选中的每篇日志逐条执行 `report entry get --report-id <reportId> --format json`;例如“总结最近收到的 5 篇”应先 list 取前 5 篇(不足 5 篇按实际数量说明),再执行相同数量的 `entry get`;用户要统计 / 已读情况时执行 `report entry stats --report-id <reportId> --format json`<br>**不要把 inbox list/outbox list 当正文接口**;查询正文必须补 `entry get`<br>**不要再生成** `report list` / `report sent` / `report detail` / `report stats`(deprecated alias,仍能跑但会打 stderr 警告) |
| generate-daily-report | 1. 按[「多源并行采集」](./report-conventions.md#多源并行采集公共模式)执行(时间=今日)<br>2. 交叉汇总并把日报内容写入临时文件 `<tmp>.md`(UTF-8,真实换行)<br>3. **创建文档**:`doc create --name "<日报名>" --content-file <tmp>.md`(> 200KB 按 write-doc 兜底(见 `dingtalk-doc/references/04-document.md`) 走 create 空 → 循环 update) |
| generate-weekly-report | 1. 按[「多源并行采集」](./report-conventions.md#多源并行采集公共模式)执行(时间=本周)<br>2. 交叉对比并把周报内容写入临时文件 `<tmp>.md`<br>3. **创建文档**:`doc create --name "<周报名>" --content-file <tmp>.md`(兜底同上) |
| submit-report | **0. 前置判定**:query 含「钉钉日志 / OA 周报模板 / 我的钉钉日志」等强信号?是 → 继续;否 → 切换到 `generate-weekly-report` 或 `generate-daily-report`(走 dws doc)<br>1. 按[「多源并行采集」](./report-conventions.md#多源并行采集公共模式)执行(时间=当日)<br>2. `report template list --format json` → 取 `report_template_id`<br>3. `report template get --name "<模版名>" --format json` → 取 `result.report_template_fields[]`,每项含 `field_name`/`field_sort`/`field_type`<br>4. **把 contents 写入临时文件**(避免 shell 引号问题):每项含 `key`/`sort`/`content`/`contentType`/`type` 五个字段,**严格映射** `field_name → key`、`field_sort → sort`、`field_type → type`,再填 `content` 与 `contentType`<br>5. `report entry submit --template-id <id> --contents-file <tmp>.json --format json` → CLI 会在提交成功后自动反查详情并追加 `dingtalkOpenMarkdownLink` / `dingtalkOpenUrl` / `dingtalkOpenLink` 字段;取返回的 `reportId` 与钉钉打开链接<br>6. final reply 优先直接使用 `dingtalkOpenMarkdownLink`,让用户点击跳转钉钉客户端查看 / 修改;仅当 submit 返回中缺少 `dingtalkOpenUrl` 时,才手动执行 `report entry get --report-id <reportId> --format json` 补取 `result.url`,再包装成 `[在钉钉中查看日志](result.url)`<br>**不要走 doc 写文档**;**禁止跳过 2/3 步**直接 submit;**禁止把 raw `dingtalk://...` URL 直接粘到回复**,必须包成 markdown link<br>**不要再生成** `report template detail` / `report create`(deprecated alias,仍能跑但会打 stderr 警告) |
| submit-report | **0. 前置判定**:query 含「钉钉日志 / OA 周报模板 / 我的钉钉日志」等强信号?是 → 继续;否 → 切换到 `generate-weekly-report` 或 `generate-daily-report`(走 dws doc)<br>1. 按[「多源并行采集」](./report-conventions.md#多源并行采集公共模式)执行(时间=当日)<br>2. `report template list --format json` → 取 `report_template_id`<br>3. `report template get --name "<模版名>" --format json` → 取 `result.report_template_fields[]`,每项含 `field_name`/`field_sort`/`field_type`<br>4. **把 contents 写入临时文件**(避免 shell 引号问题):每项含 `key`/`sort`/`content`/`contentType`/`type` 五个字段,**严格映射** `field_name → key`、`field_sort → sort`、`field_type → type`,再填 `content` 与 `contentType`<br>5. `report entry submit --template-id <id> --contents-file <tmp>.json --to-user-ids <userId1>,<userId2> --format json` → `--to-user-ids` 必填:无接收人的提交服务端仍返回成功但日志对任何人都不可见;CLI 会在提交成功后自动反查详情并追加 `dingtalkOpenMarkdownLink` / `dingtalkOpenUrl` / `dingtalkOpenLink` 字段;取返回的 `reportId` 与钉钉打开链接<br>6. final reply 优先直接使用 `dingtalkOpenMarkdownLink`,让用户点击跳转钉钉客户端查看 / 修改;仅当 submit 返回中缺少 `dingtalkOpenUrl` 时,才手动执行 `report entry get --report-id <reportId> --format json` 补取 `result.url`,再包装成 `[在钉钉中查看日志](result.url)`<br>**不要走 doc 写文档**;**禁止跳过 2/3 步**直接 submit;**禁止把 raw `dingtalk://...` URL 直接粘到回复**,必须包成 markdown link<br>**不要再生成** `report template detail` / `report create`(deprecated alias,仍能跑但会打 stderr 警告) |
| generate-monthly-report | 1. 按[「多源并行采集」](./report-conventions.md#多源并行采集公共模式)执行(时间=当月)<br>2. `report outbox list --start "<月初ISO>" --end "<月末ISO>"` → 取当月已提交日志<br>3. 按周分段归纳并把月报内容写入临时文件 `<tmp>.md`<br>4. **创建文档**:`doc create --name "<月报名>" --content-file <tmp>.md`(兜底同上) |
| generate-topic-report | 1. 提取主题关键词;推断时间范围("最近"默认近 30 天)<br>2. 按[「多源并行采集」](./report-conventions.md#多源并行采集公共模式)执行<br>3. 按时间线排列,交叉归纳核心结论/决策/行动项/未解决问题/演进脉络,并把内容写入临时文件 `<tmp>.md`<br>4. **创建文档**:`doc create --name "<报告名>" --content-file <tmp>.md`(兜底同上) |
@@ -24,7 +24,7 @@
- `dws report outbox list` = 列出**我发出**的日报(我创建或提交的)。
- `dws report entry get --report-id <reportId> --format json` = 读取单份日报正文 + 钉钉跳转链接。
- `dws report entry stats --report-id <reportId> --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 <id> --contents-file <tmp.json> --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 <id> --contents-file <tmp.json> --to-user-ids <userId1>,<userId2> --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 <reportId> --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 <path>` 或 `--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 <templateId> --contents-file ./report.json --format json
dws report entry submit --template-id <templateId> --contents-file ./report.json --to-user-ids <userId1>,<userId2> --format json
# stdin 输入
cat report.json | dws report entry submit --template-id <templateId> --contents - --format json
cat report.json | dws report entry submit --template-id <templateId> --contents - --to-user-ids <userId1> --format json
# 内联(短内容)
dws report entry submit --template-id <templateId> \
--contents '[{"key":"今日完成","sort":"0","content":"完成了需求评审","contentType":"markdown","type":"1"}]' \
--format json
--to-user-ids <userId1> --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 <templateId> --contents-file ./report.json --format json
# 2b. 提交日志(从步骤 1/2 取 templateId 与 contents 字段)— 推荐 --contents-file 传入避免 shell 引号;--to-user-ids 必填
dws report entry submit --template-id <templateId> --contents-file ./report.json --to-user-ids <userId1>,<userId2> --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 "<ISO+08>" --end "<ISO+08>" --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 <id> --contents-file <tmp.json> --format json` |
| "提交日报 / 周报(按模版)" | `dws report entry submit --template-id <id> --contents-file <tmp.json> --to-user-ids <userId1>,<userId2> --format json` |
| "我已发送 / 我创建 / 我发过的日志" | `dws report outbox list --cursor 0 --size 20 --format json` |
| "看日志正文 / 总结多篇日志" | 列表取 `reportId` 后逐篇 `dws report entry get --report-id <id> --format json` |
| "日志已读统计" | `dws report entry stats --report-id <id> --format json` |