577 lines
20 KiB
Go
577 lines
20 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 helpers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"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/output"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
const (
|
|
fileCommentServer = "doc-comment"
|
|
listFileCommentsTool = "list_file_comments"
|
|
createFileCommentTool = "create_file_comment"
|
|
fileCommentMaxPageSize = 200
|
|
fileCommentMaxAutoPages = 10
|
|
fileCommentMaxContentLength = 2099
|
|
)
|
|
|
|
type fileCommentPage struct {
|
|
nodeID string
|
|
total any
|
|
hasMore bool
|
|
nextCursor string
|
|
comments []map[string]any
|
|
}
|
|
|
|
func fileCommentNodeFlag() LeafFlag {
|
|
return LeafFlag{Name: "node", Usage: "文件 ID (dentryUuid)、数字 dentry ID 或钉盘文件 URL", Required: true, Aliases: []string{"url", "id", "node-id", "file-id"}, Bind: "fileId", Trim: true}
|
|
}
|
|
|
|
func fileCommentSpaceIDFlag() LeafFlag {
|
|
return LeafFlag{Name: "space-id", Usage: "钉盘空间 ID;仅数字 dentry ID 必填", Bind: "spaceId", OmitEmpty: true, Trim: true, RequiredWhen: "--node is a numeric dentry ID"}
|
|
}
|
|
|
|
// newDriveFileCommentCmd follows the existing resource-first comment surface:
|
|
// doc comment, sheet comment, and drive comment. The public command belongs to
|
|
// Drive, while the implementation routes to the shared doc-comment MCP server.
|
|
func newDriveFileCommentCmd() *cobra.Command {
|
|
commentCmd := &cobra.Command{
|
|
Use: "comment",
|
|
Short: "普通文件评论管理",
|
|
Long: "管理钉盘普通预览文件的评论:查询评论列表或创建全文纯文本评论。",
|
|
RunE: groupRunE,
|
|
}
|
|
|
|
listCmd := NewLeafCommand(LeafSpec{
|
|
Use: "list",
|
|
Short: "查询普通文件评论列表",
|
|
Long: `查询钉盘普通预览文件的评论。支持 dentryUuid、钉盘文件 URL,以及配合
|
|
--space-id 使用的数字 dentry ID。支持的文件类型由服务端判定。
|
|
|
|
默认返回一页;--all 固定按每页 200 条自动翻页,最多 10 页。--scope 在 CLI
|
|
侧按服务端返回的统一 anchor 过滤;total 始终表示文件的全部有效评论数,count
|
|
表示本次输出且符合 scope 的评论数。`,
|
|
Example: ` dws drive comment list --node <dentryUuid> --format json
|
|
dws drive comment list --node <dentryUuid> --all --format json`,
|
|
Tool: listFileCommentsTool,
|
|
Flags: []LeafFlag{
|
|
fileCommentNodeFlag(),
|
|
fileCommentSpaceIDFlag(),
|
|
{Name: "limit", Usage: "每页评论数,范围 1-200", Kind: LeafInt, Default: "200", Aliases: []string{"page-size"}, Bind: "maxResults"},
|
|
{Name: "cursor", Usage: "分页游标,取自上页 nextCursor", Bind: "nextToken", OmitEmpty: true, Trim: true},
|
|
{Name: "all", Usage: "自动拉取全部评论,最多 10 页 / 2000 条", Kind: LeafBool, Bind: "all"},
|
|
{Name: "scope", Usage: "评论范围: all(全部) / whole(全文) / partial(历史局部)", Default: "all", Bind: "scope", Trim: true, Enum: []string{"all", "whole", "partial"}},
|
|
},
|
|
Constraints: []LeafConstraint{
|
|
{Kind: LeafMutuallyExclusive, Flags: []string{"all", "cursor"}, Description: "--all 与 --cursor 互斥"},
|
|
{Kind: LeafMutuallyExclusive, Flags: []string{"all", "limit"}, Description: "--all 与显式 --limit/--page-size 互斥"},
|
|
{Kind: "custom", Flags: []string{"limit"}, Description: "--limit/--page-size 必须在 1-200 之间"},
|
|
{Kind: "custom", Flags: []string{"cursor"}, Description: "--cursor 必须是服务端返回的非负数字游标"},
|
|
},
|
|
Safety: contract.SafetySpec{
|
|
Effect: "read", Risk: "low",
|
|
Confirmation: "not_required", Idempotency: "idempotent",
|
|
},
|
|
Contract: LeafContract{
|
|
Identity: contract.ToolIdentitySpec{
|
|
ProductID: "drive",
|
|
Name: "list_file_comments",
|
|
CanonicalPath: "drive.list_file_comments",
|
|
CLIPath: "drive comment list",
|
|
PrimaryCLIPath: "drive comment list",
|
|
},
|
|
Description: "查询钉盘普通预览文件评论,支持安全分页聚合和全文/局部范围过滤",
|
|
DryRun: &contract.DryRunSpec{PreviewKind: contract.DryRunPreviewRequest},
|
|
Interface: &contract.InterfaceSpec{
|
|
Mode: "composite",
|
|
Availability: "available",
|
|
Reason: "The CLI wraps doc-comment/list_file_comments with bounded auto-pagination, cursor anomaly guards, local anchor-scope filtering, and a stable output projection, so no single direct MCP interface represents the complete command contract.",
|
|
},
|
|
Selection: contract.SelectionSpec{
|
|
AgentSummary: "查询 PDF、DOCX、XLSX 等钉盘普通预览文件上的评论",
|
|
UseWhen: []string{
|
|
"用户要查看普通文件上的全文评论或历史高亮/矩形评论时",
|
|
"需要从评论列表取得 commentId、作者、时间和统一 anchor,或用 --all 拉取完整列表时",
|
|
},
|
|
AvoidWhen: []string{
|
|
"在线文字文档评论使用 dws doc comment list",
|
|
"在线表格单元格评论使用 dws sheet comment list",
|
|
},
|
|
Examples: []string{
|
|
"dws drive comment list --node <dentryUuid> --format json",
|
|
"dws drive comment list --node <dentryUuid> --all --format json",
|
|
},
|
|
},
|
|
},
|
|
Validate: validateFileCommentList,
|
|
Call: runFileCommentList,
|
|
})
|
|
|
|
createCmd := NewLeafCommand(LeafSpec{
|
|
Use: "create",
|
|
Short: "创建普通文件全文评论",
|
|
Long: `在钉盘普通预览文件上创建一条全文纯文本评论。当前不支持 @人、通知选项或
|
|
局部锚点;content 按服务端规则使用 UTF-16 长度计数,最多 2099。`,
|
|
Example: ` dws drive comment create --node <dentryUuid> --content "请补充最终结论" --format json`,
|
|
Tool: createFileCommentTool,
|
|
Flags: []LeafFlag{
|
|
fileCommentNodeFlag(),
|
|
fileCommentSpaceIDFlag(),
|
|
{Name: "content", Usage: "全文评论内容,纯文本且 UTF-16 长度不超过 2099", Required: true, Bind: "content"},
|
|
},
|
|
Constraints: []LeafConstraint{
|
|
{Kind: "custom", Flags: []string{"content"}, Description: "--content 去除首尾空白后必须非空,且 UTF-16 长度不超过 2099"},
|
|
},
|
|
Safety: contract.SafetySpec{
|
|
Effect: "write", Risk: "medium",
|
|
Confirmation: "user_required", Idempotency: "unknown",
|
|
},
|
|
Contract: LeafContract{
|
|
Identity: contract.ToolIdentitySpec{
|
|
ProductID: "drive",
|
|
Name: "create_file_comment",
|
|
CanonicalPath: "drive.create_file_comment",
|
|
CLIPath: "drive comment create",
|
|
PrimaryCLIPath: "drive comment create",
|
|
},
|
|
Description: "在钉盘普通预览文件上创建一条全文纯文本评论",
|
|
DryRun: &contract.DryRunSpec{PreviewKind: contract.DryRunPreviewRequest},
|
|
Interface: &contract.InterfaceSpec{
|
|
Mode: "composite",
|
|
Availability: "available",
|
|
Reason: "The CLI wraps doc-comment/create_file_comment with exact local content validation, runtime confirmation, and a stable flattened output projection, so no single direct MCP interface represents the complete command contract.",
|
|
},
|
|
Selection: contract.SelectionSpec{
|
|
AgentSummary: "在 PDF、DOCX、XLSX 等钉盘普通预览文件上创建全文纯文本评论",
|
|
UseWhen: []string{
|
|
"用户明确要求在普通文件上留下不绑定具体位置的评论时",
|
|
},
|
|
AvoidWhen: []string{
|
|
"在线文字文档评论使用 dws doc comment create",
|
|
"在线表格单元格评论使用 dws sheet comment create;当前普通文件评论不支持 @人或局部锚点",
|
|
},
|
|
Examples: []string{
|
|
"dws drive comment create --node <dentryUuid> --content \"请补充最终结论\" --format json",
|
|
},
|
|
},
|
|
},
|
|
Validate: validateFileCommentCreate,
|
|
Call: runFileCommentCreate,
|
|
})
|
|
|
|
commentCmd.AddCommand(listCmd, createCmd)
|
|
return commentCmd
|
|
}
|
|
|
|
func validateFileCommentList(cmd *cobra.Command, _ []string) error {
|
|
if err := validateFileCommentNodeSpace(cmd); err != nil {
|
|
return err
|
|
}
|
|
limit, _ := cmd.Flags().GetInt("limit")
|
|
if cmd.Flags().Changed("page-size") {
|
|
limit, _ = cmd.Flags().GetInt("page-size")
|
|
}
|
|
if limit < 1 || limit > fileCommentMaxPageSize {
|
|
return &CLIError{
|
|
Code: CodeInvalidParam,
|
|
Message: fmt.Sprintf("--limit/--page-size 必须在 1-%d 之间", fileCommentMaxPageSize),
|
|
}
|
|
}
|
|
if cursor, _ := cmd.Flags().GetString("cursor"); strings.TrimSpace(cursor) != "" {
|
|
cursor = strings.TrimSpace(cursor)
|
|
if !allASCIIDigits(cursor) {
|
|
return &CLIError{Code: CodeInvalidParam, Message: "--cursor 必须是服务端返回的非负数字游标"}
|
|
}
|
|
if _, err := strconv.ParseInt(cursor, 10, 64); err != nil {
|
|
return &CLIError{Code: CodeInvalidParam, Message: "--cursor 超出 64 位整数范围"}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateFileCommentCreate(cmd *cobra.Command, _ []string) error {
|
|
if err := validateFileCommentNodeSpace(cmd); err != nil {
|
|
return err
|
|
}
|
|
content, _ := cmd.Flags().GetString("content")
|
|
if strings.TrimSpace(content) == "" {
|
|
return &CLIError{Code: CodeInvalidParam, Message: "--content 去除首尾空白后不能为空"}
|
|
}
|
|
length := fileCommentUTF16Length(content)
|
|
if length > fileCommentMaxContentLength {
|
|
return &CLIError{
|
|
Code: CodeInputTooLarge,
|
|
Message: fmt.Sprintf("--content 最多 %d 个 UTF-16 代码单元,当前为 %d", fileCommentMaxContentLength, length),
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateFileCommentNodeSpace(cmd *cobra.Command) error {
|
|
node := corecmd.EffectiveValue(cmd, fileCommentNodeFlag())
|
|
if !allASCIIDigits(node) {
|
|
return nil
|
|
}
|
|
if spaceID := corecmd.EffectiveValue(cmd, fileCommentSpaceIDFlag()); spaceID == "" {
|
|
return &CLIError{
|
|
Code: CodeInvalidParam,
|
|
Message: "--node 为数字 dentry ID 时必须同时提供 --space-id",
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func fileCommentUTF16Length(value string) int {
|
|
length := 0
|
|
for _, r := range value {
|
|
length++
|
|
if r > 0xffff {
|
|
length++
|
|
}
|
|
}
|
|
return length
|
|
}
|
|
|
|
func allASCIIDigits(value string) bool {
|
|
if value == "" {
|
|
return false
|
|
}
|
|
for _, r := range value {
|
|
if r < '0' || r > '9' {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func runFileCommentList(cmd *cobra.Command, tool string, args map[string]any) error {
|
|
fetchAll, _ := args["all"].(bool)
|
|
scope, _ := args["scope"].(string)
|
|
if scope == "" {
|
|
scope = "all"
|
|
}
|
|
request := fileCommentMCPArgs(args)
|
|
if fetchAll {
|
|
request["maxResults"] = fileCommentMaxPageSize
|
|
delete(request, "nextToken")
|
|
}
|
|
if deps != nil && deps.Caller != nil && deps.Caller.DryRun() {
|
|
return callMCPToolOnServer(fileCommentServer, tool, request)
|
|
}
|
|
|
|
if !fetchAll {
|
|
page, err := fetchFileCommentPage(tool, request)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := validateFileCommentNextCursor(page, stringArg(request, "nextToken")); err != nil {
|
|
return err
|
|
}
|
|
page.comments = filterFileCommentsByScope(page.comments, scope)
|
|
return output.WriteCommandPayload(cmd, fileCommentListPayload(page, scope), output.FormatJSON)
|
|
}
|
|
|
|
var aggregate fileCommentPage
|
|
aggregate.comments = make([]map[string]any, 0)
|
|
seenCursors := map[string]bool{}
|
|
currentCursor := ""
|
|
for pageNumber := 0; pageNumber < fileCommentMaxAutoPages; pageNumber++ {
|
|
if currentCursor == "" {
|
|
delete(request, "nextToken")
|
|
} else {
|
|
request["nextToken"] = currentCursor
|
|
}
|
|
page, err := fetchFileCommentPage(tool, request)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if pageNumber == 0 {
|
|
aggregate.nodeID = page.nodeID
|
|
aggregate.total = page.total
|
|
}
|
|
aggregate.comments = append(aggregate.comments, filterFileCommentsByScope(page.comments, scope)...)
|
|
if !page.hasMore {
|
|
aggregate.hasMore = false
|
|
aggregate.nextCursor = ""
|
|
return output.WriteCommandPayload(cmd, fileCommentListPayload(aggregate, scope), output.FormatJSON)
|
|
}
|
|
if err := validateFileCommentNextCursor(page, currentCursor); err != nil {
|
|
return err
|
|
}
|
|
if seenCursors[page.nextCursor] {
|
|
return fileCommentPaginationError("分页游标发生循环,结果可能不完整")
|
|
}
|
|
seenCursors[page.nextCursor] = true
|
|
currentCursor = page.nextCursor
|
|
}
|
|
return fileCommentPaginationError(fmt.Sprintf("自动翻页达到 %d 页上限但服务端仍返回 hasMore=true,结果可能不完整", fileCommentMaxAutoPages))
|
|
}
|
|
|
|
func runFileCommentCreate(cmd *cobra.Command, tool string, args map[string]any) error {
|
|
request := fileCommentMCPArgs(args)
|
|
if deps != nil && deps.Caller != nil && deps.Caller.DryRun() {
|
|
return callMCPToolOnServer(fileCommentServer, tool, request)
|
|
}
|
|
raw, err := callMCPToolReturnTextOnServer(context.Background(), fileCommentServer, tool, request)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
payload, err := decodeFileCommentPayload(tool, raw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
nodeID, ok := nonEmptyStringField(payload, "fileId")
|
|
if !ok {
|
|
return invalidFileCommentResponse(tool, "缺少 fileId", nil)
|
|
}
|
|
comment, ok := payload["comment"].(map[string]any)
|
|
if !ok {
|
|
return invalidFileCommentResponse(tool, "缺少 comment 对象", nil)
|
|
}
|
|
if err := validateFileCommentItem(tool, "comment", comment); err != nil {
|
|
return err
|
|
}
|
|
out := map[string]any{"nodeId": nodeID}
|
|
for key, value := range projectFileComment(comment) {
|
|
out[key] = value
|
|
}
|
|
return output.WriteCommandPayload(cmd, out, output.FormatJSON)
|
|
}
|
|
|
|
func fileCommentMCPArgs(args map[string]any) map[string]any {
|
|
out := make(map[string]any, len(args))
|
|
for key, value := range args {
|
|
switch key {
|
|
case "all", "scope":
|
|
continue
|
|
default:
|
|
out[key] = value
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func fetchFileCommentPage(tool string, request map[string]any) (fileCommentPage, error) {
|
|
raw, err := callMCPToolReturnTextOnServer(context.Background(), fileCommentServer, tool, request)
|
|
if err != nil {
|
|
return fileCommentPage{}, err
|
|
}
|
|
payload, err := decodeFileCommentPayload(tool, raw)
|
|
if err != nil {
|
|
return fileCommentPage{}, err
|
|
}
|
|
nodeID, ok := nonEmptyStringField(payload, "fileId")
|
|
if !ok {
|
|
return fileCommentPage{}, invalidFileCommentResponse(tool, "缺少 fileId", nil)
|
|
}
|
|
total, ok := payload["total"]
|
|
if !ok {
|
|
return fileCommentPage{}, invalidFileCommentResponse(tool, "缺少 total", nil)
|
|
}
|
|
hasMore, ok := payload["hasMore"].(bool)
|
|
if !ok {
|
|
return fileCommentPage{}, invalidFileCommentResponse(tool, "缺少布尔字段 hasMore", nil)
|
|
}
|
|
nextCursor := ""
|
|
if value, exists := payload["nextToken"]; exists && value != nil {
|
|
var stringValue bool
|
|
nextCursor, stringValue = value.(string)
|
|
if !stringValue {
|
|
return fileCommentPage{}, invalidFileCommentResponse(tool, "nextToken 不是字符串", nil)
|
|
}
|
|
}
|
|
rawItems, exists := payload["items"]
|
|
if !exists {
|
|
return fileCommentPage{}, invalidFileCommentResponse(tool, "缺少 items", nil)
|
|
}
|
|
items, ok := rawItems.([]any)
|
|
if rawItems == nil {
|
|
items = []any{}
|
|
ok = true
|
|
}
|
|
if !ok {
|
|
return fileCommentPage{}, invalidFileCommentResponse(tool, "items 不是数组", nil)
|
|
}
|
|
comments := make([]map[string]any, 0, len(items))
|
|
for index, item := range items {
|
|
comment, ok := item.(map[string]any)
|
|
if !ok {
|
|
return fileCommentPage{}, invalidFileCommentResponse(tool, fmt.Sprintf("items[%d] 不是对象", index), nil)
|
|
}
|
|
if err := validateFileCommentItem(tool, fmt.Sprintf("items[%d]", index), comment); err != nil {
|
|
return fileCommentPage{}, err
|
|
}
|
|
comments = append(comments, projectFileComment(comment))
|
|
}
|
|
return fileCommentPage{
|
|
nodeID: nodeID, total: total, hasMore: hasMore,
|
|
nextCursor: strings.TrimSpace(nextCursor), comments: comments,
|
|
}, nil
|
|
}
|
|
|
|
func decodeFileCommentPayload(tool, raw string) (map[string]any, error) {
|
|
if strings.TrimSpace(raw) == "" {
|
|
return nil, invalidFileCommentResponse(tool, "返回为空", nil)
|
|
}
|
|
var payload map[string]any
|
|
if err := json.Unmarshal([]byte(raw), &payload); err != nil {
|
|
return nil, invalidFileCommentResponse(tool, "返回不是有效 JSON", err)
|
|
}
|
|
for depth := 0; depth < 2; depth++ {
|
|
if _, ok := payload["fileId"]; ok {
|
|
break
|
|
}
|
|
var nested map[string]any
|
|
for _, key := range []string{"result", "data"} {
|
|
if value, ok := payload[key].(map[string]any); ok {
|
|
nested = value
|
|
break
|
|
}
|
|
}
|
|
if nested == nil {
|
|
break
|
|
}
|
|
payload = nested
|
|
}
|
|
return payload, nil
|
|
}
|
|
|
|
func projectFileComment(comment map[string]any) map[string]any {
|
|
out := map[string]any{}
|
|
for _, key := range []string{"commentId", "parentCommentId", "content", "createdAt", "updatedAt", "options", "anchor"} {
|
|
if value, ok := comment[key]; ok {
|
|
out[key] = value
|
|
}
|
|
}
|
|
if value, ok := comment["commentCustomType"]; ok {
|
|
out["customType"] = value
|
|
}
|
|
creator := map[string]any{}
|
|
for source, target := range map[string]string{
|
|
"creatorId": "userId", "creatorName": "name", "creatorAvatar": "avatar",
|
|
} {
|
|
if value, ok := comment[source]; ok {
|
|
creator[target] = value
|
|
}
|
|
}
|
|
if len(creator) > 0 {
|
|
out["creator"] = creator
|
|
}
|
|
return out
|
|
}
|
|
|
|
func validateFileCommentItem(tool, path string, comment map[string]any) error {
|
|
if _, ok := nonEmptyStringField(comment, "commentId"); !ok {
|
|
return invalidFileCommentResponse(tool, path+" 缺少 commentId", nil)
|
|
}
|
|
if _, ok := comment["anchor"].(map[string]any); !ok {
|
|
return invalidFileCommentResponse(tool, path+" 缺少 anchor 对象", nil)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func filterFileCommentsByScope(comments []map[string]any, scope string) []map[string]any {
|
|
if scope == "" || scope == "all" {
|
|
return comments
|
|
}
|
|
out := make([]map[string]any, 0, len(comments))
|
|
for _, comment := range comments {
|
|
anchor, _ := comment["anchor"].(map[string]any)
|
|
commentScope, _ := anchor["scope"].(string)
|
|
if commentScope == scope {
|
|
out = append(out, comment)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func fileCommentListPayload(page fileCommentPage, scope string) map[string]any {
|
|
comments := page.comments
|
|
if comments == nil {
|
|
comments = make([]map[string]any, 0)
|
|
}
|
|
nextCursor := any(nil)
|
|
if page.nextCursor != "" {
|
|
nextCursor = page.nextCursor
|
|
}
|
|
return map[string]any{
|
|
"nodeId": page.nodeID,
|
|
"total": page.total,
|
|
"count": len(page.comments),
|
|
"hasMore": page.hasMore,
|
|
"nextCursor": nextCursor,
|
|
"complete": !page.hasMore,
|
|
"scope": scope,
|
|
"comments": comments,
|
|
}
|
|
}
|
|
|
|
func validateFileCommentNextCursor(page fileCommentPage, currentCursor string) error {
|
|
if !page.hasMore {
|
|
return nil
|
|
}
|
|
if page.nextCursor == "" {
|
|
return fileCommentPaginationError("服务端返回 hasMore=true 但 nextCursor 为空,结果可能不完整")
|
|
}
|
|
if !allASCIIDigits(page.nextCursor) {
|
|
return fileCommentPaginationError("服务端返回的 nextCursor 不是非负数字游标,结果可能不完整")
|
|
}
|
|
if _, err := strconv.ParseInt(page.nextCursor, 10, 64); err != nil {
|
|
return fileCommentPaginationError("服务端返回的 nextCursor 超出 64 位整数范围,结果可能不完整")
|
|
}
|
|
if page.nextCursor == currentCursor {
|
|
return fileCommentPaginationError("服务端分页游标未前进,结果可能不完整")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func fileCommentPaginationError(message string) error {
|
|
return &CLIError{
|
|
Code: CodeContentTruncated,
|
|
Message: message,
|
|
Suggestion: "请稍后重试,或去掉 --all 后使用服务端返回的 --cursor 分页读取",
|
|
Operation: fileCommentServer + "/" + listFileCommentsTool,
|
|
}
|
|
}
|
|
|
|
func invalidFileCommentResponse(tool, reason string, cause error) error {
|
|
return &CLIError{
|
|
Code: CodeMCPToolError,
|
|
Message: fmt.Sprintf("%s 返回结构异常:%s", tool, reason),
|
|
Suggestion: "请确认 doc-comment MCP 与当前 DWS 契约一致",
|
|
Operation: fileCommentServer + "/" + tool,
|
|
Cause: cause,
|
|
}
|
|
}
|
|
|
|
func nonEmptyStringField(payload map[string]any, key string) (string, bool) {
|
|
value, ok := payload[key].(string)
|
|
value = strings.TrimSpace(value)
|
|
return value, ok && value != ""
|
|
}
|
|
|
|
func stringArg(args map[string]any, key string) string {
|
|
value, _ := args[key].(string)
|
|
return strings.TrimSpace(value)
|
|
}
|