1046 lines
50 KiB
Go
1046 lines
50 KiB
Go
// Copyright 2026 Alibaba Group
|
||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||
// you may not use this file except in compliance with the License.
|
||
// You may obtain a copy of the License at
|
||
//
|
||
// http://www.apache.org/licenses/LICENSE-2.0
|
||
//
|
||
// Unless required by applicable law or agreed to in writing, software
|
||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
// See the License for the specific language governing permissions and
|
||
// limitations under the License.
|
||
|
||
// Package doc declares the high-fidelity `dws doc +<command>` shortcuts.
|
||
// Tool names and parameter keys are lifted verbatim from
|
||
// internal/helpers/doc.go (the single source of truth for DingTalk MCP tools).
|
||
package doc
|
||
|
||
import (
|
||
"strings"
|
||
|
||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
|
||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/localio"
|
||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
|
||
)
|
||
|
||
const (
|
||
productDoc = "doc"
|
||
productComment = "doc-comment"
|
||
)
|
||
|
||
// ── 文档浏览 / 读取 ──────────────────────────────────────────
|
||
|
||
var Search = shortcut.Shortcut{
|
||
Service: "doc",
|
||
Command: "+search",
|
||
Product: productDoc,
|
||
Description: "按关键词搜索有权限的文档 (不传则返回最近访问)",
|
||
Intent: "当你只记得文档的标题或主题词、需要先定位到某篇钉钉文档拿到它的 nodeId/URL 以便后续阅读或编辑时使用;可按关键词、扩展名、创建/访问时间、创建者等条件过滤,不传关键词则返回最近访问的文档,返回匹配的文档列表。",
|
||
Risk: shortcut.RiskRead,
|
||
Safety: contract.SafetySpec{
|
||
Effect: "read", Risk: "low",
|
||
Confirmation: "not_required", Idempotency: "idempotent",
|
||
},
|
||
Contract: corecmd.ContractDecl{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "doc",
|
||
Name: "shortcut_search",
|
||
CanonicalPath: "doc.shortcut_search",
|
||
CLIPath: "doc +search",
|
||
PrimaryCLIPath: "doc +search",
|
||
},
|
||
Description: "按关键词搜索有权限的文档 (不传则返回最近访问)",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "composite",
|
||
Availability: "available",
|
||
Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "按关键词搜索有权限的文档 (不传则返回最近访问)",
|
||
UseWhen: []string{"当你只记得文档的标题或主题词、需要先定位到某篇钉钉文档拿到它的 nodeId/URL 以便后续阅读或编辑时使用;可按关键词、扩展名、创建/访问时间、创建者等条件过滤,不传关键词则返回最近访问的文档,返回匹配的文档列表。"},
|
||
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
|
||
Examples: []string{
|
||
"dws doc +search --query \"会议纪要\"",
|
||
"dws doc +search --extensions pdf,docx",
|
||
},
|
||
},
|
||
},
|
||
Flags: []shortcut.Flag{
|
||
{Name: "query", Type: shortcut.FlagString, Desc: "搜索关键词,不传返回最近访问的文档"},
|
||
{Name: "extensions", Type: shortcut.FlagStringSlice, Desc: "按文件扩展名过滤 (如 adoc,axls,pdf)"},
|
||
{Name: "created-from", Type: shortcut.FlagInt, Desc: "创建时间起始 (毫秒时间戳)"},
|
||
{Name: "created-to", Type: shortcut.FlagInt, Desc: "创建时间截止 (毫秒时间戳)"},
|
||
{Name: "visited-from", Type: shortcut.FlagInt, Desc: "访问时间起始 (毫秒时间戳)"},
|
||
{Name: "visited-to", Type: shortcut.FlagInt, Desc: "访问时间截止 (毫秒时间戳)"},
|
||
{Name: "creator-uids", Type: shortcut.FlagStringSlice, Desc: "按创建者用户 ID 过滤"},
|
||
{Name: "editor-uids", Type: shortcut.FlagStringSlice, Desc: "按编辑者用户 ID 过滤"},
|
||
{Name: "mentioned-uids", Type: shortcut.FlagStringSlice, Desc: "按 @提及的用户 ID 过滤"},
|
||
{Name: "workspace-ids", Type: shortcut.FlagStringSlice, Desc: "按知识库 ID 过滤"},
|
||
{Name: "limit", Type: shortcut.FlagInt, Desc: "每页数量 (默认 10,最大 30)"},
|
||
{Name: "cursor", Type: shortcut.FlagString, Desc: "分页游标 (上次结果的 nextPageToken)"},
|
||
{Name: "page-all", Type: shortcut.FlagBool, Desc: "有界读取全部后续页"},
|
||
{Name: "max-pages", Type: shortcut.FlagInt, Default: "20", Desc: "--page-all 最大页数"},
|
||
{Name: "max-items", Type: shortcut.FlagInt, Default: "500", Desc: "最多返回文档数"},
|
||
},
|
||
Tips: []string{`dws doc +search --query "会议纪要"`, `dws doc +search --extensions pdf,docx`},
|
||
Execute: func(rt *shortcut.RuntimeContext) error {
|
||
params := map[string]any{}
|
||
if v := rt.Str("query"); v != "" {
|
||
params["keyword"] = v
|
||
}
|
||
if rt.Changed("extensions") {
|
||
params["extensions"] = rt.StrSlice("extensions")
|
||
}
|
||
if rt.Changed("created-from") {
|
||
params["createdTimeFrom"] = rt.Int("created-from")
|
||
}
|
||
if rt.Changed("created-to") {
|
||
params["createdTimeTo"] = rt.Int("created-to")
|
||
}
|
||
if rt.Changed("visited-from") {
|
||
params["visitedTimeFrom"] = rt.Int("visited-from")
|
||
}
|
||
if rt.Changed("visited-to") {
|
||
params["visitedTimeTo"] = rt.Int("visited-to")
|
||
}
|
||
if rt.Changed("creator-uids") {
|
||
params["creatorUserIds"] = rt.StrSlice("creator-uids")
|
||
}
|
||
if rt.Changed("editor-uids") {
|
||
params["editorUserIds"] = rt.StrSlice("editor-uids")
|
||
}
|
||
if rt.Changed("mentioned-uids") {
|
||
params["mentionedUserIds"] = rt.StrSlice("mentioned-uids")
|
||
}
|
||
if rt.Changed("workspace-ids") {
|
||
params["workspaceIds"] = rt.StrSlice("workspace-ids")
|
||
}
|
||
pageSize := rt.Int("limit")
|
||
if pageSize == 0 {
|
||
pageSize = 10
|
||
}
|
||
result, err := collectDocPages(rt, "search_documents", "documents", params, searchDocsProject, docPageOptions{
|
||
PageAll: rt.Bool("page-all"), PageSize: pageSize, MaxPages: rt.Int("max-pages"), MaxItems: rt.Int("max-items"), Cursor: rt.Str("cursor"),
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return rt.Output(result)
|
||
},
|
||
}
|
||
|
||
// searchDocsProject reshapes the raw search_documents response into a clean
|
||
// document list ({nodeId, name, docType, url, creatorId, modifiedTime}) —
|
||
// clean output projection. Both the list container and per-item
|
||
// field names are probed defensively across candidate keys so response-shape
|
||
// drift yields an empty/partial list rather than a crash or fabricated data.
|
||
func searchDocsProject(data map[string]any) []map[string]any {
|
||
raw := docResolveList(data)
|
||
out := make([]map[string]any, 0, len(raw))
|
||
for _, item := range raw {
|
||
m, ok := item.(map[string]any)
|
||
if !ok {
|
||
continue
|
||
}
|
||
row := map[string]any{}
|
||
if v, ok := docFirst(m, "nodeId", "node_id", "id", "docId", "doc_id"); ok {
|
||
row["nodeId"] = v
|
||
}
|
||
if v, ok := docFirst(m, "name", "title", "docName", "fileName"); ok {
|
||
row["name"] = v
|
||
}
|
||
if v, ok := docFirst(m, "docType", "doc_type", "type", "extension", "fileType"); ok {
|
||
row["docType"] = v
|
||
}
|
||
if v, ok := docFirst(m, "url", "docUrl", "nodeUrl", "webUrl"); ok {
|
||
row["url"] = v
|
||
}
|
||
if v, ok := docFirst(m, "creatorId", "creatorUserId", "creator_user_id", "creator"); ok {
|
||
row["creatorId"] = v
|
||
}
|
||
if v, ok := docFirst(m, "modifiedTime", "gmtModified", "visitedTime", "updateTime", "modifyTime"); ok {
|
||
row["modifiedTime"] = v
|
||
}
|
||
if len(row) > 0 {
|
||
out = append(out, row)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// docResolveList locates the list payload inside a doc-service response,
|
||
// tolerating a bare top-level array or nesting under common envelope keys, and
|
||
// optionally one level deeper inside a result/data container.
|
||
func docResolveList(data map[string]any) []any {
|
||
if data == nil {
|
||
return []any{}
|
||
}
|
||
for _, key := range []string{"nodes", "documents", "list", "items", "result", "data", "records"} {
|
||
v, ok := data[key]
|
||
if !ok {
|
||
continue
|
||
}
|
||
if arr, ok := v.([]any); ok {
|
||
return arr
|
||
}
|
||
if inner, ok := v.(map[string]any); ok {
|
||
for _, ik := range []string{"nodes", "documents", "list", "items", "records", "result", "data"} {
|
||
if arr, ok := inner[ik].([]any); ok {
|
||
return arr
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return []any{}
|
||
}
|
||
|
||
// docFirst returns the first present candidate key's value.
|
||
func docFirst(m map[string]any, keys ...string) (any, bool) {
|
||
for _, k := range keys {
|
||
if v, ok := m[k]; ok {
|
||
return v, true
|
||
}
|
||
}
|
||
return nil, false
|
||
}
|
||
|
||
var List = shortcut.Shortcut{
|
||
Service: "doc",
|
||
Command: "+list",
|
||
Product: productDoc,
|
||
Description: "列出文件夹或知识库下的直接子节点",
|
||
Intent: "当你已知某个文档文件夹或知识库的 ID、想浏览它下面直接包含的文档与子文件夹(不递归深层)以便逐层导航时使用;输入 folder 或 workspace,返回该层级的子节点列表。",
|
||
Risk: shortcut.RiskRead,
|
||
Safety: contract.SafetySpec{
|
||
Effect: "read", Risk: "low",
|
||
Confirmation: "not_required", Idempotency: "idempotent",
|
||
},
|
||
Contract: corecmd.ContractDecl{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "doc",
|
||
Name: "shortcut_list",
|
||
CanonicalPath: "doc.shortcut_list",
|
||
CLIPath: "doc +list",
|
||
PrimaryCLIPath: "doc +list",
|
||
},
|
||
Description: "列出文件夹或知识库下的直接子节点",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "composite",
|
||
Availability: "available",
|
||
Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "列出文件夹或知识库下的直接子节点",
|
||
UseWhen: []string{"当你已知某个文档文件夹或知识库的 ID、想浏览它下面直接包含的文档与子文件夹(不递归深层)以便逐层导航时使用;输入 folder 或 workspace,返回该层级的子节点列表。"},
|
||
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
|
||
Examples: []string{
|
||
"dws doc +list --folder DOC_FOLDER_NODE_ID",
|
||
"dws doc +list --workspace WS_ID --limit 20",
|
||
},
|
||
},
|
||
},
|
||
Flags: []shortcut.Flag{
|
||
{Name: "folder", Type: shortcut.FlagString, Desc: "文档文件夹 nodeId 或 alidocs 文件夹 URL"},
|
||
{Name: "workspace", Type: shortcut.FlagString, Desc: "知识库 ID"},
|
||
{Name: "limit", Type: shortcut.FlagInt, Desc: "每页数量 (默认 50,最大 50)"},
|
||
{Name: "cursor", Type: shortcut.FlagString, Desc: "分页游标 (上次结果的 nextPageToken)"},
|
||
{Name: "page-all", Type: shortcut.FlagBool, Desc: "有界读取全部后续页"},
|
||
{Name: "max-pages", Type: shortcut.FlagInt, Default: "20", Desc: "--page-all 最大页数"},
|
||
{Name: "max-items", Type: shortcut.FlagInt, Default: "500", Desc: "最多返回节点数"},
|
||
},
|
||
Tips: []string{`dws doc +list --folder DOC_FOLDER_NODE_ID`, `dws doc +list --workspace WS_ID --limit 20`},
|
||
Execute: func(rt *shortcut.RuntimeContext) error {
|
||
params := map[string]any{}
|
||
if rt.Changed("folder") {
|
||
params["folderId"] = rt.Str("folder")
|
||
}
|
||
if rt.Changed("workspace") {
|
||
params["workspaceId"] = rt.Str("workspace")
|
||
}
|
||
pageSize := rt.Int("limit")
|
||
if pageSize == 0 {
|
||
pageSize = 50
|
||
}
|
||
result, err := collectDocPages(rt, "list_nodes", "nodes", params, listNodesProject, docPageOptions{
|
||
PageAll: rt.Bool("page-all"), PageSize: pageSize, MaxPages: rt.Int("max-pages"), MaxItems: rt.Int("max-items"), Cursor: rt.Str("cursor"),
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return rt.Output(result)
|
||
},
|
||
}
|
||
|
||
// listNodesProject reshapes the raw list_nodes response into a clean child-node
|
||
// list ({nodeId, name, nodeType, url}) — clean output projection.
|
||
// The list container and per-item field names are probed defensively via the
|
||
// shared docResolveList/docFirst helpers, so an unknown shape yields an empty
|
||
// list rather than a crash or fabricated data.
|
||
func listNodesProject(data map[string]any) []map[string]any {
|
||
raw := docResolveList(data)
|
||
out := make([]map[string]any, 0, len(raw))
|
||
for _, item := range raw {
|
||
m, ok := item.(map[string]any)
|
||
if !ok {
|
||
continue
|
||
}
|
||
row := map[string]any{}
|
||
if v, ok := docFirst(m, "nodeId", "node_id", "id", "docId", "doc_id"); ok {
|
||
row["nodeId"] = v
|
||
}
|
||
if v, ok := docFirst(m, "name", "title", "nodeName", "fileName"); ok {
|
||
row["name"] = v
|
||
}
|
||
if v, ok := docFirst(m, "nodeType", "node_type", "docType", "type", "extension"); ok {
|
||
row["nodeType"] = v
|
||
}
|
||
if v, ok := docFirst(m, "url", "nodeUrl", "docUrl", "webUrl"); ok {
|
||
row["url"] = v
|
||
}
|
||
if len(row) > 0 {
|
||
out = append(out, row)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// ── 文档创建 / 更新 ──────────────────────────────────────────
|
||
|
||
var Copy = shortcut.Shortcut{
|
||
Service: "doc",
|
||
Command: "+copy",
|
||
Product: productDoc,
|
||
Description: "复制文档/文件到指定文件夹或知识库",
|
||
Intent: "当你想保留原件、在另一个文件夹或知识库里生成一份文档/文件副本(例如以某篇文档为模板另存)时使用;输入源 node 与目标 folder/workspace,会实际创建一个副本。",
|
||
Risk: shortcut.RiskWrite,
|
||
Safety: contract.SafetySpec{
|
||
Effect: "write", Risk: "medium",
|
||
Confirmation: "user_required", Idempotency: "unknown",
|
||
},
|
||
Contract: corecmd.ContractDecl{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "doc",
|
||
Name: "shortcut_copy",
|
||
CanonicalPath: "doc.shortcut_copy",
|
||
CLIPath: "doc +copy",
|
||
PrimaryCLIPath: "doc +copy",
|
||
},
|
||
Description: "复制文档/文件到指定文件夹或知识库",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "composite",
|
||
Availability: "available",
|
||
Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "复制文档/文件到指定文件夹或知识库",
|
||
UseWhen: []string{"当你想保留原件、在另一个文件夹或知识库里生成一份文档/文件副本(例如以某篇文档为模板另存)时使用;输入源 node 与目标 folder/workspace,会实际创建一个副本。"},
|
||
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
|
||
Examples: []string{"dws doc +copy --node DOC_ID --folder TARGET_FOLDER_NODE_ID"},
|
||
},
|
||
},
|
||
Flags: []shortcut.Flag{
|
||
{Name: "node", Type: shortcut.FlagString, Desc: "文档/文件 ID 或 URL", Required: true},
|
||
{Name: "folder", Type: shortcut.FlagString, Desc: "目标文档文件夹 nodeId 或 alidocs 文件夹 URL"},
|
||
{Name: "workspace", Type: shortcut.FlagString, Desc: "目标知识库 ID"},
|
||
},
|
||
Tips: []string{`dws doc +copy --node DOC_ID --folder TARGET_FOLDER_NODE_ID`},
|
||
Execute: func(rt *shortcut.RuntimeContext) error {
|
||
params := map[string]any{"nodeId": rt.Str("node")}
|
||
if rt.Changed("folder") {
|
||
params["targetFolderId"] = rt.Str("folder")
|
||
}
|
||
if rt.Changed("workspace") {
|
||
params["workspaceId"] = rt.Str("workspace")
|
||
}
|
||
return rt.CallMCP("copy_document", params)
|
||
},
|
||
}
|
||
|
||
var Move = shortcut.Shortcut{
|
||
Service: "doc",
|
||
Command: "+move",
|
||
Product: productDoc,
|
||
Description: "移动文档/文件到指定文件夹或知识库",
|
||
Intent: "当你要整理文档归属、把某篇文档/文件从当前位置挪到另一个文件夹或知识库(原位置不再保留)时使用;输入 node 与目标 folder/workspace,会实际改变文件的存放位置。",
|
||
Risk: shortcut.RiskWrite,
|
||
Safety: contract.SafetySpec{
|
||
Effect: "write", Risk: "medium",
|
||
Confirmation: "user_required", Idempotency: "unknown",
|
||
},
|
||
Contract: corecmd.ContractDecl{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "doc",
|
||
Name: "shortcut_move",
|
||
CanonicalPath: "doc.shortcut_move",
|
||
CLIPath: "doc +move",
|
||
PrimaryCLIPath: "doc +move",
|
||
},
|
||
Description: "移动文档/文件到指定文件夹或知识库",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "composite",
|
||
Availability: "available",
|
||
Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "移动文档/文件到指定文件夹或知识库",
|
||
UseWhen: []string{"当你要整理文档归属、把某篇文档/文件从当前位置挪到另一个文件夹或知识库(原位置不再保留)时使用;输入 node 与目标 folder/workspace,会实际改变文件的存放位置。"},
|
||
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
|
||
Examples: []string{"dws doc +move --node DOC_ID --folder TARGET_FOLDER_NODE_ID"},
|
||
},
|
||
},
|
||
Flags: []shortcut.Flag{
|
||
{Name: "node", Type: shortcut.FlagString, Desc: "文档/文件 ID 或 URL", Required: true},
|
||
{Name: "folder", Type: shortcut.FlagString, Desc: "目标文档文件夹 nodeId 或 alidocs 文件夹 URL"},
|
||
{Name: "workspace", Type: shortcut.FlagString, Desc: "目标知识库 ID"},
|
||
},
|
||
Tips: []string{`dws doc +move --node DOC_ID --folder TARGET_FOLDER_NODE_ID`},
|
||
Execute: func(rt *shortcut.RuntimeContext) error {
|
||
params := map[string]any{"nodeId": rt.Str("node")}
|
||
if rt.Changed("folder") {
|
||
params["targetFolderId"] = rt.Str("folder")
|
||
}
|
||
if rt.Changed("workspace") {
|
||
params["workspaceId"] = rt.Str("workspace")
|
||
}
|
||
return rt.CallMCP("move_document", params)
|
||
},
|
||
}
|
||
|
||
// ── 文件 / 文件夹 ────────────────────────────────────────────
|
||
|
||
// ── 块级编辑 ─────────────────────────────────────────────────
|
||
|
||
// ── 文档附件 ─────────────────────────────────────────────────
|
||
|
||
// ── 文档评论 (server: doc-comment) ───────────────────────────
|
||
|
||
var CommentList = shortcut.Shortcut{
|
||
Service: "doc",
|
||
Command: "+comment-list",
|
||
Product: productComment,
|
||
Description: "查询文档评论列表",
|
||
Intent: "当你想查看某篇文档上已有的评论、了解有哪些反馈或待处理意见(可按全文/划词、已解决/未解决过滤)时使用;输入 node,可用 --limit/--cursor 分页,返回评论列表及其 commentKey 以便后续回复。",
|
||
Risk: shortcut.RiskRead,
|
||
Safety: contract.SafetySpec{
|
||
Effect: "read", Risk: "low",
|
||
Confirmation: "not_required", Idempotency: "idempotent",
|
||
},
|
||
Contract: corecmd.ContractDecl{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "doc",
|
||
Name: "shortcut_comment_list",
|
||
CanonicalPath: "doc.shortcut_comment_list",
|
||
CLIPath: "doc +comment-list",
|
||
PrimaryCLIPath: "doc +comment-list",
|
||
},
|
||
Description: "查询文档评论列表",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "composite",
|
||
Availability: "available",
|
||
Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "查询文档评论列表",
|
||
UseWhen: []string{"当你想查看某篇文档上已有的评论、了解有哪些反馈或待处理意见(可按全文/划词、已解决/未解决过滤)时使用;输入 node,可用 --limit/--cursor 分页,返回评论列表及其 commentKey 以便后续回复。"},
|
||
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
|
||
Examples: []string{
|
||
"dws doc +comment-list --node DOC_ID --limit 20",
|
||
"dws doc +comment-list --node DOC_ID --limit 20 --cursor NEXT_TOKEN",
|
||
},
|
||
},
|
||
},
|
||
Flags: []shortcut.Flag{
|
||
{Name: "node", Type: shortcut.FlagString, Desc: "文档 ID 或 URL", Required: true},
|
||
{Name: "limit", Type: shortcut.FlagInt, Desc: "每页数量 (默认 50,最大 50)", Aliases: []string{"page-size"}},
|
||
{Name: "cursor", Type: shortcut.FlagString, Desc: "分页游标 (上一页返回的 nextToken)"},
|
||
{Name: "type", Type: shortcut.FlagString, Desc: "评论类型: global (全文) / inline (划词)", Enum: []string{"global", "inline"}},
|
||
{Name: "resolve-status", Type: shortcut.FlagString, Desc: "解决状态: resolved / unresolved", Enum: []string{"resolved", "unresolved"}},
|
||
},
|
||
Tips: []string{`dws doc +comment-list --node DOC_ID --limit 20`, `dws doc +comment-list --node DOC_ID --limit 20 --cursor NEXT_TOKEN`},
|
||
Execute: func(rt *shortcut.RuntimeContext) error {
|
||
params := map[string]any{"nodeId": rt.Str("node")}
|
||
if rt.Changed("limit") || rt.Changed("page-size") {
|
||
params["pageSize"] = rt.IntFirst("limit", "page-size")
|
||
}
|
||
if v := rt.Str("cursor"); v != "" {
|
||
params["nextToken"] = v
|
||
}
|
||
if v := rt.Str("type"); v != "" {
|
||
params["commentType"] = v
|
||
}
|
||
if v := rt.Str("resolve-status"); v != "" {
|
||
params["resolveStatus"] = v
|
||
}
|
||
return rt.CallMCP("list_comments", params)
|
||
},
|
||
}
|
||
|
||
const commentCreateTargetConstraint = "selection 与 block-id/start/end 高级通道互斥;block-id/start/end 必须一起提供;selection 必须唯一匹配"
|
||
|
||
var CommentCreate = shortcut.Shortcut{
|
||
Service: "doc",
|
||
Command: "+comment-create",
|
||
Product: productComment,
|
||
Description: "创建全文评论,或按 selection 创建划词评论",
|
||
Intent: "当用户要对整篇文档留言,或针对文档中唯一匹配的一段文字创建精确划词评论时使用;已知 block/start/end 时也可直接走高级通道。",
|
||
Risk: shortcut.RiskWrite,
|
||
Safety: contract.SafetySpec{
|
||
Effect: "write", Risk: "medium",
|
||
Confirmation: "user_required", Idempotency: "unknown",
|
||
},
|
||
Contract: docContract("+comment-create", "创建全文评论,或按 selection 创建划词评论",
|
||
"当用户要对整篇文档留言,或针对文档中唯一匹配的一段文字创建精确划词评论时使用;已知 block/start/end 时也可直接走高级通道。",
|
||
[]string{`dws doc +comment-create --node <DOC_ID> --content "请补充数据来源"`, `dws doc +comment-create --node <DOC_ID> --selection "计划下周发布" --content "请确认日期"`},
|
||
contract.ParamDecl{Name: "node", Property: "node"},
|
||
contract.ParamDecl{Name: "content", Property: "content"},
|
||
contract.ParamDecl{Name: "selection", Property: "selection"},
|
||
contract.ParamDecl{Name: "block-id", Property: "blockId"},
|
||
contract.ParamDecl{Name: "start", Property: "start"},
|
||
contract.ParamDecl{Name: "end", Property: "end"},
|
||
contract.ParamDecl{Name: "selected-text", Property: "selectedText"},
|
||
contract.ParamDecl{Name: "mention", Property: "mention"}),
|
||
Flags: []shortcut.Flag{
|
||
{Name: "node", Type: shortcut.FlagString, Desc: "文档 ID 或 URL", Required: true},
|
||
{Name: "content", Type: shortcut.FlagString, Desc: "评论文字内容", Required: true},
|
||
{Name: "selection", Type: shortcut.FlagString, Desc: "完整文字或 前缀...后缀;" + commentCreateTargetConstraint},
|
||
{Name: "block-id", Type: shortcut.FlagString, Desc: "高级通道 block ID;" + commentCreateTargetConstraint},
|
||
{Name: "start", Type: shortcut.FlagInt, Desc: "块内 UTF-16 起始偏移;" + commentCreateTargetConstraint},
|
||
{Name: "end", Type: shortcut.FlagInt, Desc: "块内 UTF-16 结束偏移;" + commentCreateTargetConstraint},
|
||
{Name: "selected-text", Type: shortcut.FlagString, Desc: "可选引用原文;CLI 会从 block 回读并交叉校验"},
|
||
{Name: "mention", Type: shortcut.FlagStringSlice, Desc: "被 @ 的用户 uid,多个值用逗号分隔;不要传 JSON 数组"},
|
||
},
|
||
Constraints: []shortcut.Constraint{{Kind: shortcut.ConstraintCustom, Flags: []string{"selection", "block-id", "start", "end"}, Description: commentCreateTargetConstraint}},
|
||
Tips: []string{`dws doc +comment-create --node <DOC_ID> --content "请补充数据来源"`, `dws doc +comment-create --node <DOC_ID> --selection "计划下周发布" --content "请确认日期"`},
|
||
Validate: validateCommentCreate,
|
||
Execute: executeCommentCreate,
|
||
}
|
||
|
||
var CommentReply = shortcut.Shortcut{
|
||
Service: "doc",
|
||
Command: "+comment-reply",
|
||
Product: productComment,
|
||
Description: "回复文档中的一条评论",
|
||
Intent: "当你要针对某条已有评论进行回复、参与讨论或用表情贴图回应时使用;先从评论列表拿到 comment-key,再输入 node、comment-key 与 content(--emoji 则作为表情回复),会实际发布一条回复。",
|
||
Risk: shortcut.RiskWrite,
|
||
Safety: contract.SafetySpec{
|
||
Effect: "write", Risk: "medium",
|
||
Confirmation: "user_required", Idempotency: "unknown",
|
||
},
|
||
Contract: corecmd.ContractDecl{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "doc",
|
||
Name: "shortcut_comment_reply",
|
||
CanonicalPath: "doc.shortcut_comment_reply",
|
||
CLIPath: "doc +comment-reply",
|
||
PrimaryCLIPath: "doc +comment-reply",
|
||
},
|
||
Description: "回复文档中的一条评论",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "composite",
|
||
Availability: "available",
|
||
Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "回复文档中的一条评论",
|
||
UseWhen: []string{"当你要针对某条已有评论进行回复、参与讨论或用表情贴图回应时使用;先从评论列表拿到 comment-key,再输入 node、comment-key 与 content(--emoji 则作为表情回复),会实际发布一条回复。"},
|
||
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
|
||
Examples: []string{"dws doc +comment-reply --node DOC_ID --comment-key COMMENT_KEY --content \"同意\""},
|
||
},
|
||
},
|
||
Flags: []shortcut.Flag{
|
||
{Name: "node", Type: shortcut.FlagString, Desc: "文档 ID 或 URL", Required: true},
|
||
{Name: "content", Type: shortcut.FlagString, Desc: "回复文字内容 (表情回复时填表情名称)", Required: true},
|
||
{Name: "comment-key", Type: shortcut.FlagString, Desc: "被回复评论的 commentKey (从 list/create 获取)", Required: true},
|
||
{Name: "emoji", Type: shortcut.FlagBool, Desc: "作为表情贴图回复"},
|
||
{Name: "mention", Type: shortcut.FlagStringSlice, Desc: "被 @ 的用户 uid,多个值用逗号分隔;不要传 JSON 数组"},
|
||
},
|
||
Tips: []string{`dws doc +comment-reply --node DOC_ID --comment-key COMMENT_KEY --content "同意"`},
|
||
Execute: func(rt *shortcut.RuntimeContext) error {
|
||
params := map[string]any{
|
||
"nodeId": rt.Str("node"),
|
||
"content": rt.Str("content"),
|
||
"replyCommentKey": rt.Str("comment-key"),
|
||
}
|
||
if rt.Bool("emoji") {
|
||
params["emoji"] = true
|
||
}
|
||
if rt.Changed("mention") {
|
||
mentions, err := normalizeMentionUserIDs(rt.StrSlice("mention"))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
params["mentionedUserIds"] = mentions
|
||
}
|
||
return rt.CallMCP("reply_comment", params)
|
||
},
|
||
}
|
||
|
||
var CommentCreateInline = shortcut.Shortcut{
|
||
Service: "doc",
|
||
Command: "+comment-create-inline",
|
||
Product: productComment,
|
||
Description: "兼容入口:按 block/start/end 创建划词评论",
|
||
Intent: "仅兼容既有调用;新任务统一使用 +comment-create 的 selection 或 block/start/end 通道。",
|
||
Risk: shortcut.RiskWrite,
|
||
Safety: contract.SafetySpec{Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown"},
|
||
Flags: []shortcut.Flag{
|
||
{Name: "node", Type: shortcut.FlagString, Desc: "文档 ID 或 URL", Required: true},
|
||
{Name: "content", Type: shortcut.FlagString, Desc: "评论文字内容 (纯文本)", Required: true},
|
||
{Name: "block-id", Type: shortcut.FlagString, Desc: "评论标记所在的块 ID (通过 +block-list 获取)", Required: true},
|
||
{Name: "start", Type: shortcut.FlagInt, Desc: "块内文本起始字符偏移量 (从 0 开始)", Required: true},
|
||
{Name: "end", Type: shortcut.FlagInt, Desc: "块内文本结束字符偏移量 (须大于 start)", Required: true},
|
||
{Name: "selected-text", Type: shortcut.FlagString, Desc: "选中文本内容 (展示引用原文)"},
|
||
{Name: "mention", Type: shortcut.FlagStringSlice, Desc: "被 @ 的用户 uid,多个值用逗号分隔;不要传 JSON 数组"},
|
||
},
|
||
Constraints: []shortcut.Constraint{{Kind: shortcut.ConstraintCustom, Flags: []string{"block-id", "start", "end"}, Description: "block-id/start/end 必须一起提供,CLI 回读并校验 selectedText"}},
|
||
Tips: []string{`dws doc +comment-create --node DOC_ID --block-id BLOCK_ID --start 0 --end 10 --content "这里需要修改"`},
|
||
Validate: validateCommentCreate,
|
||
Execute: executeCommentCreate,
|
||
}
|
||
|
||
// ── 协作权限 ─────────────────────────────────────────────────
|
||
|
||
// ── 导出 ─────────────────────────────────────────────────────
|
||
|
||
var ExportSubmit = shortcut.Shortcut{
|
||
Service: "doc",
|
||
Command: "+export-submit",
|
||
Product: productDoc,
|
||
Description: "提交在线文档导出任务 (docx/markdown/pdf),返回 jobId",
|
||
Intent: "仅当用户明确要求手工接管异步导出 job,且不需要当前命令下载文件时使用;返回 jobId 后只能用 +export-get 恢复。",
|
||
Risk: shortcut.RiskRead,
|
||
Safety: contract.SafetySpec{
|
||
Effect: "read", Risk: "low",
|
||
Confirmation: "not_required", Idempotency: "idempotent",
|
||
},
|
||
Contract: corecmd.ContractDecl{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "doc",
|
||
Name: "shortcut_export_submit",
|
||
CanonicalPath: "doc.shortcut_export_submit",
|
||
CLIPath: "doc +export-submit",
|
||
PrimaryCLIPath: "doc +export-submit",
|
||
},
|
||
Description: "提交在线文档导出任务 (docx/markdown/pdf),返回 jobId",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "composite",
|
||
Availability: "available",
|
||
Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "提交在线文档导出任务 (docx/markdown/pdf),返回 jobId",
|
||
UseWhen: []string{"仅当用户明确要求手工接管异步导出 job,且不需要当前命令下载文件时使用;返回 jobId 后只能用 +export-get 恢复。"},
|
||
AvoidWhen: []string{"正常导出和保存本地文件使用 doc +export;不要手工编排 submit/get,也不要用它绕过 +export 的安全下载"},
|
||
Examples: []string{"dws doc +export-submit --node DOC_ID --export-format markdown"},
|
||
},
|
||
},
|
||
Flags: []shortcut.Flag{
|
||
{Name: "node", Type: shortcut.FlagString, Desc: "要导出的文档 ID 或 URL", Required: true},
|
||
{Name: "export-format", Type: shortcut.FlagString, Default: "docx", Desc: "导出格式;省略时默认为 docx", Enum: []string{"docx", "markdown", "pdf"}},
|
||
},
|
||
Tips: []string{`dws doc +export-submit --node DOC_ID --export-format markdown`},
|
||
Execute: func(rt *shortcut.RuntimeContext) error {
|
||
result, err := rt.CallMCPWriteData(productDoc, "submit_export_job", map[string]any{
|
||
"nodeId": rt.Str("node"),
|
||
"exportFormat": rt.Str("export-format"),
|
||
})
|
||
if err != nil {
|
||
return docUnknownWriteError("doc.export_submit", "submit_export_job", rt.Str("node"), err)
|
||
}
|
||
return rt.Output(docEnvelope("doc.export_submit", map[string]any{"nodeId": rt.Str("node"), "result": result}, map[string]any{"name": "submit_export_job", "status": "success"}))
|
||
},
|
||
}
|
||
|
||
var ExportGet = shortcut.Shortcut{
|
||
Service: "doc",
|
||
Command: "+export-get",
|
||
Product: productDoc,
|
||
Description: "根据 jobId 查询文档导出任务结果",
|
||
Intent: "当 +export 已返回 jobId 但轮询、中断或下载失败时使用;复用同一 job 查询,给 output 时通过 CLI 安全下载,禁止重新提交或 curl 临时链接。",
|
||
Risk: shortcut.RiskRead,
|
||
Safety: contract.SafetySpec{
|
||
Effect: "read", Risk: "low",
|
||
Confirmation: "not_required", Idempotency: "idempotent",
|
||
},
|
||
Contract: corecmd.ContractDecl{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "doc",
|
||
Name: "shortcut_export_get",
|
||
CanonicalPath: "doc.shortcut_export_get",
|
||
CLIPath: "doc +export-get",
|
||
PrimaryCLIPath: "doc +export-get",
|
||
},
|
||
Description: "根据 jobId 查询文档导出任务结果",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "composite",
|
||
Availability: "available",
|
||
Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "根据 jobId 查询文档导出任务结果",
|
||
UseWhen: []string{"当 +export 已返回 jobId 但轮询、中断或下载失败时使用;复用同一 job 查询,给 output 时通过 CLI 安全下载,禁止重新提交或 curl 临时链接。"},
|
||
AvoidWhen: []string{"尚未提交导出或没有真实 jobId 时不要使用;正常首次导出使用 doc +export"},
|
||
Examples: []string{"dws doc +export-get --job-id JOB_ID", "dws doc +export-get --job-id JOB_ID --output ./exports/"},
|
||
},
|
||
},
|
||
Flags: []shortcut.Flag{
|
||
{Name: "job-id", Type: shortcut.FlagString, Desc: "导出任务 ID", Required: true},
|
||
{Name: "output", Shorthand: "o", Type: shortcut.FlagString, Desc: "可选:任务完成后安全下载到工作目录内相对路径"},
|
||
},
|
||
Validate: func(rt *shortcut.RuntimeContext) error {
|
||
if rt.Str("output") == "" {
|
||
return nil
|
||
}
|
||
return localio.ValidateOutput(rt.Str("output"))
|
||
},
|
||
Constraints: []shortcut.Constraint{{Kind: shortcut.ConstraintCustom, Flags: []string{"output"}, Description: "提供 --output 时必须是工作目录内相对路径;默认 no-clobber"}},
|
||
Tips: []string{`dws doc +export-get --job-id JOB_ID`, `dws doc +export-get --job-id JOB_ID --output ./exports/`},
|
||
Execute: executeExportGet,
|
||
}
|
||
|
||
// ── 历史版本 (server: doc) ───────────────────────────────────
|
||
|
||
var VersionSave = shortcut.Shortcut{
|
||
Service: "doc",
|
||
Command: "+version-save",
|
||
Product: productDoc,
|
||
Description: "手动保存文档版本快照",
|
||
Intent: "当你在做重大改动前后、想手动打一个可回滚的版本存档点时使用;输入 node,会实际为该文档保存一个当前内容的历史版本快照。",
|
||
Risk: shortcut.RiskWrite,
|
||
Safety: contract.SafetySpec{
|
||
Effect: "write", Risk: "medium",
|
||
Confirmation: "user_required", Idempotency: "unknown",
|
||
},
|
||
Contract: corecmd.ContractDecl{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "doc",
|
||
Name: "shortcut_version_save",
|
||
CanonicalPath: "doc.shortcut_version_save",
|
||
CLIPath: "doc +version-save",
|
||
PrimaryCLIPath: "doc +version-save",
|
||
},
|
||
Description: "手动保存文档版本快照",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "composite",
|
||
Availability: "available",
|
||
Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "手动保存文档版本快照",
|
||
UseWhen: []string{"当你在做重大改动前后、想手动打一个可回滚的版本存档点时使用;输入 node,会实际为该文档保存一个当前内容的历史版本快照。"},
|
||
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
|
||
Examples: []string{"dws doc +version-save --node DOC_ID"},
|
||
},
|
||
},
|
||
Flags: []shortcut.Flag{
|
||
{Name: "node", Type: shortcut.FlagString, Desc: "文档 ID 或 URL", Required: true},
|
||
},
|
||
Tips: []string{`dws doc +version-save --node DOC_ID`},
|
||
Execute: func(rt *shortcut.RuntimeContext) error {
|
||
return rt.CallMCP("save_doc_version", map[string]any{"nodeId": rt.Str("node")})
|
||
},
|
||
}
|
||
|
||
var VersionList = shortcut.Shortcut{
|
||
Service: "doc",
|
||
Command: "+version-list",
|
||
Product: productDoc,
|
||
Description: "查看文档历史版本列表",
|
||
Intent: "当你想查看某篇文档有哪些历史版本、以便挑一个版本号用于回滚时使用;输入 node,返回历史版本列表及其版本号。",
|
||
Risk: shortcut.RiskRead,
|
||
Safety: contract.SafetySpec{
|
||
Effect: "read", Risk: "low",
|
||
Confirmation: "not_required", Idempotency: "idempotent",
|
||
},
|
||
Contract: corecmd.ContractDecl{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "doc",
|
||
Name: "shortcut_version_list",
|
||
CanonicalPath: "doc.shortcut_version_list",
|
||
CLIPath: "doc +version-list",
|
||
PrimaryCLIPath: "doc +version-list",
|
||
},
|
||
Description: "查看文档历史版本列表",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "composite",
|
||
Availability: "available",
|
||
Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "查看文档历史版本列表",
|
||
UseWhen: []string{"当你想查看某篇文档有哪些历史版本、以便挑一个版本号用于回滚时使用;输入 node,返回历史版本列表及其版本号。"},
|
||
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
|
||
Examples: []string{"dws doc +version-list --node DOC_ID"},
|
||
},
|
||
},
|
||
Flags: []shortcut.Flag{
|
||
{Name: "node", Type: shortcut.FlagString, Desc: "文档 ID 或 URL", Required: true},
|
||
{Name: "limit", Type: shortcut.FlagInt, Desc: "返回版本数量上限"},
|
||
{Name: "cursor", Type: shortcut.FlagString, Desc: "分页游标"},
|
||
},
|
||
Tips: []string{`dws doc +version-list --node DOC_ID`},
|
||
Execute: func(rt *shortcut.RuntimeContext) error {
|
||
params := map[string]any{"nodeId": rt.Str("node")}
|
||
if rt.Changed("limit") {
|
||
params["maxResults"] = rt.Int("limit")
|
||
}
|
||
if v := rt.Str("cursor"); v != "" {
|
||
params["nextCursor"] = v
|
||
}
|
||
return rt.CallMCP("list_doc_versions", params)
|
||
},
|
||
}
|
||
|
||
var VersionRevert = shortcut.Shortcut{
|
||
Service: "doc",
|
||
Command: "+version-revert",
|
||
Product: productDoc,
|
||
Description: "回滚文档到指定历史版本",
|
||
Intent: "当文档被误改、你想把它整体恢复到某个历史版本时使用;先用 +version-list 找到目标版本号,再输入 node 与 version,会实际把文档内容覆盖回该版本,属于高风险写操作,需谨慎确认。",
|
||
Risk: shortcut.RiskHighWrite,
|
||
Safety: contract.SafetySpec{
|
||
Effect: "destructive", Risk: "high",
|
||
Confirmation: "user_required", Idempotency: "unknown",
|
||
},
|
||
Contract: corecmd.ContractDecl{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "doc",
|
||
Name: "shortcut_version_revert",
|
||
CanonicalPath: "doc.shortcut_version_revert",
|
||
CLIPath: "doc +version-revert",
|
||
PrimaryCLIPath: "doc +version-revert",
|
||
},
|
||
Description: "回滚文档到指定历史版本",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "composite",
|
||
Availability: "available",
|
||
Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "回滚文档到指定历史版本",
|
||
UseWhen: []string{"当文档被误改、你想把它整体恢复到某个历史版本时使用;先用 +version-list 找到目标版本号,再输入 node 与 version,会实际把文档内容覆盖回该版本,属于高风险写操作,需谨慎确认。"},
|
||
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
|
||
Examples: []string{"dws doc +version-revert --node DOC_ID --version 3"},
|
||
},
|
||
},
|
||
Flags: []shortcut.Flag{
|
||
{Name: "node", Type: shortcut.FlagString, Desc: "文档 ID 或 URL", Required: true},
|
||
{Name: "version", Type: shortcut.FlagInt, Desc: "目标版本号 (从 +version-list 获取)", Required: true},
|
||
},
|
||
Tips: []string{`dws doc +version-revert --node DOC_ID --version 3 --yes`},
|
||
Execute: func(rt *shortcut.RuntimeContext) error {
|
||
return rt.CallMCP("revert_doc_version", map[string]any{
|
||
"nodeId": rt.Str("node"),
|
||
"version": rt.Int("version"),
|
||
})
|
||
},
|
||
}
|
||
|
||
// ── 模板 (server: doc) ───────────────────────────────────────
|
||
|
||
var TemplateList = shortcut.Shortcut{
|
||
Service: "doc",
|
||
Command: "+template-list",
|
||
Product: productDoc,
|
||
Description: "获取文档模板列表",
|
||
Intent: "当你想基于模板新建文档、需要先浏览可用的模板(自己的 MY 或公共 PUBLIC)并拿到 templateId 时使用;返回模板列表,随后可配合 +template-apply 套用。",
|
||
Risk: shortcut.RiskRead,
|
||
Safety: contract.SafetySpec{
|
||
Effect: "read", Risk: "low",
|
||
Confirmation: "not_required", Idempotency: "idempotent",
|
||
},
|
||
Contract: corecmd.ContractDecl{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "doc",
|
||
Name: "shortcut_template_list",
|
||
CanonicalPath: "doc.shortcut_template_list",
|
||
CLIPath: "doc +template-list",
|
||
PrimaryCLIPath: "doc +template-list",
|
||
},
|
||
Description: "获取文档模板列表",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "composite",
|
||
Availability: "available",
|
||
Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "获取文档模板列表",
|
||
UseWhen: []string{"当你想基于模板新建文档、需要先浏览可用的模板(自己的 MY 或公共 PUBLIC)并拿到 templateId 时使用;返回模板列表,随后可配合 +template-apply 套用。"},
|
||
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
|
||
Examples: []string{"dws doc +template-list --source PUBLIC"},
|
||
},
|
||
},
|
||
Flags: []shortcut.Flag{
|
||
{Name: "source", Type: shortcut.FlagString, Desc: "模板来源: MY / PUBLIC (默认 MY)", Enum: []string{"MY", "PUBLIC"}},
|
||
{Name: "limit", Type: shortcut.FlagInt, Desc: "返回数量上限"},
|
||
{Name: "cursor", Type: shortcut.FlagString, Desc: "分页游标"},
|
||
},
|
||
Tips: []string{`dws doc +template-list --source PUBLIC`},
|
||
Execute: func(rt *shortcut.RuntimeContext) error {
|
||
params := map[string]any{}
|
||
if v := rt.Str("source"); v != "" {
|
||
params["templateSource"] = v
|
||
}
|
||
if rt.Changed("limit") {
|
||
params["maxResults"] = rt.Int("limit")
|
||
}
|
||
if v := rt.Str("cursor"); v != "" {
|
||
params["nextCursor"] = v
|
||
}
|
||
return rt.CallMCP("list_doc_templates", params)
|
||
},
|
||
}
|
||
|
||
var TemplateSearch = shortcut.Shortcut{
|
||
Service: "doc",
|
||
Command: "+template-search",
|
||
Product: productDoc,
|
||
Description: "根据关键词搜索文档模板",
|
||
Intent: "当模板较多、你想按关键词(如“周报”“合同”)快速找到合适的模板并拿到 templateId 时使用;输入 query,返回匹配的模板列表,随后可配合 +template-apply 套用。",
|
||
Risk: shortcut.RiskRead,
|
||
Safety: contract.SafetySpec{
|
||
Effect: "read", Risk: "low",
|
||
Confirmation: "not_required", Idempotency: "idempotent",
|
||
},
|
||
Contract: corecmd.ContractDecl{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "doc",
|
||
Name: "shortcut_template_search",
|
||
CanonicalPath: "doc.shortcut_template_search",
|
||
CLIPath: "doc +template-search",
|
||
PrimaryCLIPath: "doc +template-search",
|
||
},
|
||
Description: "根据关键词搜索文档模板",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "composite",
|
||
Availability: "available",
|
||
Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "根据关键词搜索文档模板",
|
||
UseWhen: []string{"当模板较多、你想按关键词(如“周报”“合同”)快速找到合适的模板并拿到 templateId 时使用;输入 query,返回匹配的模板列表,随后可配合 +template-apply 套用。"},
|
||
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
|
||
Examples: []string{"dws doc +template-search --query \"周报\""},
|
||
},
|
||
},
|
||
Flags: []shortcut.Flag{
|
||
{Name: "query", Type: shortcut.FlagString, Desc: "搜索关键词", Required: true},
|
||
{Name: "source", Type: shortcut.FlagString, Desc: "模板来源: MY / PUBLIC (默认 MY)", Enum: []string{"MY", "PUBLIC"}},
|
||
{Name: "limit", Type: shortcut.FlagInt, Desc: "返回数量上限"},
|
||
{Name: "cursor", Type: shortcut.FlagString, Desc: "分页游标"},
|
||
},
|
||
Tips: []string{`dws doc +template-search --query "周报"`},
|
||
Execute: func(rt *shortcut.RuntimeContext) error {
|
||
pageSize := rt.Int("limit")
|
||
if pageSize <= 0 {
|
||
pageSize = 50
|
||
}
|
||
params := map[string]any{"searchName": rt.Str("query"), "maxResults": pageSize}
|
||
if v := rt.Str("source"); v != "" {
|
||
params["templateSource"] = v
|
||
}
|
||
if v := rt.Str("cursor"); v != "" {
|
||
params["nextCursor"] = v
|
||
}
|
||
found, err := rt.CallMCPData(productDoc, "search_doc_templates", params)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
candidates := collectTemplateCandidates(found)
|
||
hasMore, hasMoreKnown, nextCursor := docPageState(found)
|
||
nextCursor = strings.TrimSpace(nextCursor)
|
||
complete := hasMoreKnown && !hasMore
|
||
if !hasMoreKnown && nextCursor == "" && len(candidates) < pageSize {
|
||
complete = true
|
||
}
|
||
globalComplete := complete && rt.Str("cursor") == ""
|
||
status := "selection_required"
|
||
if globalComplete && len(candidates) == 0 {
|
||
status = "not_found"
|
||
} else if globalComplete && len(candidates) == 1 {
|
||
status = "resolved"
|
||
}
|
||
selectedTemplateID := ""
|
||
if status == "resolved" {
|
||
selectedTemplateID = candidates[0]["templateId"].(string)
|
||
}
|
||
nextAction := map[string]string{"resolved": "create_once", "not_found": "stop", "selection_required": "ask_user"}[status]
|
||
if !complete {
|
||
nextAction = "continue_search"
|
||
}
|
||
return rt.Output(docEnvelope("doc.template_search", map[string]any{
|
||
"query": rt.Str("query"),
|
||
"source": rt.Str("source"),
|
||
"count": len(candidates),
|
||
"candidates": candidates,
|
||
"complete": complete,
|
||
"hasMore": !complete,
|
||
"nextCursor": nextCursor,
|
||
"selection": map[string]any{
|
||
"status": status,
|
||
"templateId": selectedTemplateID,
|
||
"nextAction": nextAction,
|
||
},
|
||
}))
|
||
},
|
||
}
|
||
|
||
var TemplateApply = shortcut.Shortcut{
|
||
Service: "doc",
|
||
Command: "+template-apply",
|
||
Product: productDoc,
|
||
Description: "使用指定模板创建新文档",
|
||
Intent: "当你已选定某个模板、想据此快速生成一篇带预设结构的新文档时使用;输入 template-id(可选 name/folder/workspace),会实际按模板创建一篇新文档并返回其 ID。",
|
||
Risk: shortcut.RiskWrite,
|
||
Flags: []shortcut.Flag{
|
||
{Name: "template-id", Type: shortcut.FlagString, Desc: "模板 ID", Required: true},
|
||
{Name: "name", Type: shortcut.FlagString, Desc: "新文档名称 (可选)"},
|
||
{Name: "folder", Type: shortcut.FlagString, Desc: "目标文件夹 ID (可选)"},
|
||
{Name: "workspace", Type: shortcut.FlagString, Desc: "知识库 ID (可选)"},
|
||
},
|
||
Tips: []string{`dws doc +template-apply --template-id TPL_ID --name "我的周报"`},
|
||
Execute: func(rt *shortcut.RuntimeContext) error {
|
||
params := map[string]any{"templateId": rt.Str("template-id")}
|
||
if v := rt.Str("name"); v != "" {
|
||
params["name"] = v
|
||
}
|
||
if rt.Changed("folder") {
|
||
params["folderId"] = rt.Str("folder")
|
||
}
|
||
if rt.Changed("workspace") {
|
||
params["workspaceId"] = rt.Str("workspace")
|
||
}
|
||
return rt.CallMCP("apply_doc_template", params)
|
||
},
|
||
}
|
||
|
||
func init() {
|
||
// Expert/recovery leaves remain callable without entering Agent discovery.
|
||
CommentCreateInline.Contract = corecmd.ContractDecl{}
|
||
TemplateApply.Contract = corecmd.ContractDecl{}
|
||
canonicalizeHistoryShortcuts()
|
||
shortcut.Register(
|
||
Search,
|
||
List,
|
||
Copy,
|
||
Move,
|
||
CommentList,
|
||
CommentCreate,
|
||
CommentReply,
|
||
CommentCreateInline,
|
||
ExportSubmit,
|
||
ExportGet,
|
||
compatHistorySave,
|
||
compatHistoryList,
|
||
compatHistoryRevert,
|
||
VersionSave,
|
||
VersionList,
|
||
VersionRevert,
|
||
TemplateList,
|
||
TemplateSearch,
|
||
TemplateApply,
|
||
)
|
||
}
|