Skip to content

Commit 05d8137

Browse files
authored
feat(drive): extend +add-comment to support slides targets (#674)
Change-Id: Id87ecce098d87f7db82389a73f3134b66fcd4814
1 parent 17a85d3 commit 05d8137

4 files changed

Lines changed: 681 additions & 41 deletions

File tree

shortcuts/drive/drive_add_comment.go

Lines changed: 137 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ const (
6464
var DriveAddComment = common.Shortcut{
6565
Service: "drive",
6666
Command: "+add-comment",
67-
Description: "Add a full-document or local comment to doc/docx/sheet, also supports wiki URL resolving to doc/docx/sheet",
67+
Description: "Add a comment to doc/docx/sheet/slides, also supports wiki URL resolving to doc/docx/sheet/slides",
6868
Risk: "write",
6969
Scopes: []string{
7070
"docx:document:readonly",
@@ -73,12 +73,12 @@ var DriveAddComment = common.Shortcut{
7373
},
7474
AuthTypes: []string{"user", "bot"},
7575
Flags: []common.Flag{
76-
{Name: "doc", Desc: "document URL/token, sheet URL, or wiki URL that resolves to doc/docx/sheet", Required: true},
77-
{Name: "type", Desc: "document type: doc, docx, sheet (required when --doc is a bare token; auto-detected for URLs)", Enum: []string{"doc", "docx", "sheet"}},
76+
{Name: "doc", Desc: "document URL/token, sheet/slides URL, or wiki URL that resolves to doc/docx/sheet/slides", Required: true},
77+
{Name: "type", Desc: "document type: doc, docx, sheet, slides (required when --doc is a bare token; auto-detected for URLs)", Enum: []string{"doc", "docx", "sheet", "slides"}},
7878
{Name: "content", Desc: "reply_elements JSON string", Required: true},
7979
{Name: "full-comment", Type: "bool", Desc: "create a full-document comment; also the default when no location is provided"},
8080
{Name: "selection-with-ellipsis", Desc: "target content locator (plain text or 'start...end')"},
81-
{Name: "block-id", Desc: "for docx: anchor block ID; for sheet: <sheetId>!<cell> (e.g. a281f9!D6)"},
81+
{Name: "block-id", Desc: "for docx: anchor block ID; for sheet: <sheetId>!<cell> (e.g. a281f9!D6); for slides: <slide-block-type>!<xml-id> (e.g. shape!bPq)"},
8282
},
8383
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
8484
docRef, err := parseCommentDocRef(runtime.Str("doc"), runtime.Str("type"))
@@ -104,6 +104,18 @@ var DriveAddComment = common.Shortcut{
104104
}
105105
return nil
106106
}
107+
if docRef.Kind == "slides" {
108+
if _, _, err := parseSlidesBlockRef(runtime.Str("block-id")); err != nil {
109+
return err
110+
}
111+
if runtime.Bool("full-comment") {
112+
return output.ErrValidation("--full-comment is not applicable for slide comments; use --block-id <slide-block-type>!<xml-id>")
113+
}
114+
if strings.TrimSpace(runtime.Str("selection-with-ellipsis")) != "" {
115+
return output.ErrValidation("--selection-with-ellipsis is not applicable for slide comments; use --block-id <slide-block-type>!<xml-id>")
116+
}
117+
return nil
118+
}
107119

108120
selection := runtime.Str("selection-with-ellipsis")
109121
blockID := strings.TrimSpace(runtime.Str("block-id"))
@@ -116,15 +128,17 @@ var DriveAddComment = common.Shortcut{
116128

117129
mode := resolveCommentMode(runtime.Bool("full-comment"), selection, blockID)
118130
if mode == commentModeLocal && docRef.Kind == "doc" {
119-
return output.ErrValidation("local comments only support docx and sheet; old doc format only supports full comments")
131+
return output.ErrValidation("local comments only support docx, sheet, and slides; old doc format only supports full comments")
120132
}
121133

122134
return nil
123135
},
124136
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
125137
docRef, _ := parseCommentDocRef(runtime.Str("doc"), runtime.Str("type"))
126138
replyElements, _ := parseCommentReplyElements(runtime.Str("content"))
139+
selection := runtime.Str("selection-with-ellipsis")
127140
blockID := strings.TrimSpace(runtime.Str("block-id"))
141+
mode := resolveCommentMode(runtime.Bool("full-comment"), selection, blockID)
128142

129143
// For wiki URLs, resolve the actual target type via API so dry-run
130144
// matches real execution behavior instead of guessing from --block-id.
@@ -133,11 +147,12 @@ var DriveAddComment = common.Shortcut{
133147
isWiki := false
134148
if docRef.Kind == "wiki" {
135149
isWiki = true
136-
target, err := resolveCommentTarget(ctx, runtime, runtime.Str("doc"), commentModeFull)
137-
if err == nil {
138-
resolvedKind = target.FileType
139-
resolvedToken = target.FileToken
150+
target, err := resolveCommentTarget(ctx, runtime, runtime.Str("doc"), mode)
151+
if err != nil {
152+
return common.NewDryRunAPI().Set("error", err.Error())
140153
}
154+
resolvedKind = target.FileType
155+
resolvedToken = target.FileToken
141156
}
142157

143158
// Sheet comment dry-run.
@@ -146,7 +161,7 @@ var DriveAddComment = common.Shortcut{
146161
if anchor == nil {
147162
anchor = &sheetAnchor{SheetID: "<sheetId>", Col: 0, Row: 0}
148163
}
149-
commentBody := buildCommentCreateV2Request("sheet", "", replyElements, anchor)
164+
commentBody := buildCommentCreateV2Request("sheet", "", "", replyElements, anchor)
150165
desc := "1-step request: create sheet comment"
151166
if isWiki {
152167
desc = "2-step orchestration: resolve wiki -> create sheet comment"
@@ -157,15 +172,28 @@ var DriveAddComment = common.Shortcut{
157172
Body(commentBody).
158173
Set("file_token", resolvedToken)
159174
}
175+
if resolvedKind == "slides" {
176+
slideAnchorBlockID, slideBlockType, err := parseSlidesBlockRef(blockID)
177+
if err != nil {
178+
return common.NewDryRunAPI().Set("error", err.Error())
179+
}
180+
commentBody := buildCommentCreateV2Request("slides", slideAnchorBlockID, slideBlockType, replyElements, nil)
181+
desc := "1-step request: create slide block comment"
182+
if isWiki {
183+
desc = "2-step orchestration: resolve wiki -> create slide block comment"
184+
}
185+
return common.NewDryRunAPI().
186+
Desc(desc).
187+
POST("/open-apis/drive/v1/files/:file_token/new_comments").
188+
Body(commentBody).
189+
Set("file_token", resolvedToken)
190+
}
160191

161192
// Doc/docx comment dry-run.
162-
selection := runtime.Str("selection-with-ellipsis")
163-
mode := resolveCommentMode(runtime.Bool("full-comment"), selection, blockID)
164-
165193
createPath := "/open-apis/drive/v1/files/:file_token/new_comments"
166-
commentBody := buildCommentCreateV2Request(resolvedKind, "", replyElements, nil)
194+
commentBody := buildCommentCreateV2Request(resolvedKind, "", "", replyElements, nil)
167195
if mode == commentModeLocal {
168-
commentBody = buildCommentCreateV2Request(resolvedKind, anchorBlockIDForDryRun(blockID), replyElements, nil)
196+
commentBody = buildCommentCreateV2Request(resolvedKind, anchorBlockIDForDryRun(blockID), "", replyElements, nil)
169197
}
170198

171199
mcpEndpoint := common.MCPEndpoint(runtime.Config.Brand)
@@ -240,6 +268,9 @@ var DriveAddComment = common.Shortcut{
240268
if docRef.Kind == "sheet" {
241269
return executeSheetComment(runtime, docRef)
242270
}
271+
if docRef.Kind == "slides" {
272+
return executeSlidesComment(runtime, docRef)
273+
}
243274

244275
selection := runtime.Str("selection-with-ellipsis")
245276
blockID := strings.TrimSpace(runtime.Str("block-id"))
@@ -254,6 +285,9 @@ var DriveAddComment = common.Shortcut{
254285
if target.FileType == "sheet" {
255286
return executeSheetComment(runtime, commentDocRef{Kind: "sheet", Token: target.FileToken})
256287
}
288+
if target.FileType == "slides" {
289+
return executeSlidesComment(runtime, commentDocRef{Kind: "slides", Token: target.FileToken})
290+
}
257291

258292
replyElements, err := parseCommentReplyElements(runtime.Str("content"))
259293
if err != nil {
@@ -283,9 +317,9 @@ var DriveAddComment = common.Shortcut{
283317
}
284318

285319
requestPath := fmt.Sprintf("/open-apis/drive/v1/files/%s/new_comments", validate.EncodePathSegment(target.FileToken))
286-
requestBody := buildCommentCreateV2Request(target.FileType, "", replyElements, nil)
320+
requestBody := buildCommentCreateV2Request(target.FileType, "", "", replyElements, nil)
287321
if mode == commentModeLocal {
288-
requestBody = buildCommentCreateV2Request(target.FileType, blockID, replyElements, nil)
322+
requestBody = buildCommentCreateV2Request(target.FileType, blockID, "", replyElements, nil)
289323
}
290324

291325
if mode == commentModeLocal {
@@ -358,14 +392,17 @@ func parseCommentDocRef(input, docType string) (commentDocRef, error) {
358392
if token, ok := extractURLToken(raw, "/sheets/"); ok {
359393
return commentDocRef{Kind: "sheet", Token: token}, nil
360394
}
395+
if token, ok := extractURLToken(raw, "/slides/"); ok {
396+
return commentDocRef{Kind: "slides", Token: token}, nil
397+
}
361398
if token, ok := extractURLToken(raw, "/docx/"); ok {
362399
return commentDocRef{Kind: "docx", Token: token}, nil
363400
}
364401
if token, ok := extractURLToken(raw, "/doc/"); ok {
365402
return commentDocRef{Kind: "doc", Token: token}, nil
366403
}
367404
if strings.Contains(raw, "://") {
368-
return commentDocRef{}, output.ErrValidation("unsupported --doc input %q: use a doc/docx/sheet URL, a token with --type, or a wiki URL that resolves to doc/docx/sheet", raw)
405+
return commentDocRef{}, output.ErrValidation("unsupported --doc input %q: use a doc/docx/sheet/slides URL, a token with --type, or a wiki URL that resolves to doc/docx/sheet/slides", raw)
369406
}
370407
if strings.ContainsAny(raw, "/?#") {
371408
return commentDocRef{}, output.ErrValidation("unsupported --doc input %q: use a token with --type, or a wiki URL", raw)
@@ -374,7 +411,7 @@ func parseCommentDocRef(input, docType string) (commentDocRef, error) {
374411
// Bare token: --type is required.
375412
docType = strings.TrimSpace(docType)
376413
if docType == "" {
377-
return commentDocRef{}, output.ErrValidation("--type is required when --doc is a bare token (allowed values: doc, docx, sheet)")
414+
return commentDocRef{}, output.ErrValidation("--type is required when --doc is a bare token (allowed values: doc, docx, sheet, slides)")
378415
}
379416
return commentDocRef{Kind: docType, Token: raw}, nil
380417
}
@@ -385,9 +422,9 @@ func resolveCommentTarget(ctx context.Context, runtime *common.RuntimeContext, i
385422
return resolvedCommentTarget{}, err
386423
}
387424

388-
if docRef.Kind == "docx" || docRef.Kind == "doc" || docRef.Kind == "sheet" {
389-
if mode == commentModeLocal && docRef.Kind != "docx" && docRef.Kind != "sheet" {
390-
return resolvedCommentTarget{}, output.ErrValidation("local comments only support docx and sheet; old doc format only supports full comments")
425+
if docRef.Kind == "docx" || docRef.Kind == "doc" || docRef.Kind == "sheet" || docRef.Kind == "slides" {
426+
if mode == commentModeLocal && docRef.Kind != "docx" && docRef.Kind != "sheet" && docRef.Kind != "slides" {
427+
return resolvedCommentTarget{}, output.ErrValidation("local comments only support docx, sheet, and slides; old doc format only supports full comments")
391428
}
392429
return resolvedCommentTarget{
393430
DocID: docRef.Token,
@@ -414,6 +451,12 @@ func resolveCommentTarget(ctx context.Context, runtime *common.RuntimeContext, i
414451
if objType == "" || objToken == "" {
415452
return resolvedCommentTarget{}, output.Errorf(output.ExitAPI, "api_error", "wiki get_node returned incomplete node data")
416453
}
454+
if objType == "slides" && mode == commentModeFull {
455+
return resolvedCommentTarget{}, output.ErrValidation("wiki resolved to %q, but slide comments require --block-id <slide-block-type>!<xml-id>; --full-comment is not applicable", objType)
456+
}
457+
if objType == "slides" && strings.TrimSpace(runtime.Str("selection-with-ellipsis")) != "" {
458+
return resolvedCommentTarget{}, output.ErrValidation("wiki resolved to %q, but --selection-with-ellipsis is not applicable for slide comments; use --block-id <slide-block-type>!<xml-id>", objType)
459+
}
417460
if objType == "sheet" {
418461
// Sheet comments are handled via the sheet fast path in Execute.
419462
fmt.Fprintf(runtime.IO().ErrOut, "Resolved wiki to %s: %s\n", objType, common.MaskToken(objToken))
@@ -425,11 +468,21 @@ func resolveCommentTarget(ctx context.Context, runtime *common.RuntimeContext, i
425468
WikiToken: docRef.Token,
426469
}, nil
427470
}
471+
if objType == "slides" {
472+
fmt.Fprintf(runtime.IO().ErrOut, "Resolved wiki to %s: %s\n", objType, common.MaskToken(objToken))
473+
return resolvedCommentTarget{
474+
DocID: objToken,
475+
FileToken: objToken,
476+
FileType: "slides",
477+
ResolvedBy: "wiki",
478+
WikiToken: docRef.Token,
479+
}, nil
480+
}
428481
if mode == commentModeLocal && objType != "docx" {
429-
return resolvedCommentTarget{}, output.ErrValidation("wiki resolved to %q, but local comments only support docx and sheet; for sheet use --block-id <sheetId>!<cell>", objType)
482+
return resolvedCommentTarget{}, output.ErrValidation("wiki resolved to %q, but local comments only support docx, sheet, and slides; for sheet use --block-id <sheetId>!<cell>, for slides use --block-id <slide-block-type>!<xml-id>", objType)
430483
}
431484
if mode == commentModeFull && objType != "docx" && objType != "doc" {
432-
return resolvedCommentTarget{}, output.ErrValidation("wiki resolved to %q, but comments only support doc/docx/sheet", objType)
485+
return resolvedCommentTarget{}, output.ErrValidation("wiki resolved to %q, but comments only support doc/docx/sheet/slides", objType)
433486
}
434487

435488
fmt.Fprintf(runtime.IO().ErrOut, "Resolved wiki to %s: %s\n", objType, common.MaskToken(objToken))
@@ -606,7 +659,7 @@ type sheetAnchor struct {
606659
Row int
607660
}
608661

609-
func buildCommentCreateV2Request(fileType, blockID string, replyElements []map[string]interface{}, sheet *sheetAnchor) map[string]interface{} {
662+
func buildCommentCreateV2Request(fileType, blockID, slideBlockType string, replyElements []map[string]interface{}, sheet *sheetAnchor) map[string]interface{} {
610663
body := map[string]interface{}{
611664
"file_type": fileType,
612665
"reply_elements": replyElements,
@@ -621,6 +674,9 @@ func buildCommentCreateV2Request(fileType, blockID string, replyElements []map[s
621674
body["anchor"] = map[string]interface{}{
622675
"block_id": blockID,
623676
}
677+
if strings.TrimSpace(slideBlockType) != "" {
678+
body["anchor"].(map[string]interface{})["slide_block_type"] = strings.TrimSpace(slideBlockType)
679+
}
624680
}
625681
return body
626682
}
@@ -632,6 +688,24 @@ func anchorBlockIDForDryRun(blockID string) string {
632688
return "<anchor_block_id>"
633689
}
634690

691+
func parseSlidesBlockRef(blockID string) (string, string, error) {
692+
blockID = strings.TrimSpace(blockID)
693+
if blockID == "" {
694+
return "", "", output.ErrValidation("slide comments require --block-id in <slide-block-type>!<xml-id> format")
695+
}
696+
697+
parts := strings.SplitN(blockID, "!", 2)
698+
if len(parts) != 2 {
699+
return "", "", output.ErrValidation("slide --block-id must be <slide-block-type>!<xml-id> (e.g. shape!bPq), got %q", blockID)
700+
}
701+
parsedType := strings.TrimSpace(parts[0])
702+
parsedID := strings.TrimSpace(parts[1])
703+
if parsedType == "" || parsedID == "" {
704+
return "", "", output.ErrValidation("slide --block-id must be <slide-block-type>!<xml-id> (e.g. shape!bPq), got %q", blockID)
705+
}
706+
return parsedID, parsedType, nil
707+
}
708+
635709
func firstNonEmptyString(values ...string) string {
636710
for _, value := range values {
637711
if strings.TrimSpace(value) != "" {
@@ -703,7 +777,7 @@ func executeSheetComment(runtime *common.RuntimeContext, docRef commentDocRef) e
703777
}
704778

705779
requestPath := fmt.Sprintf("/open-apis/drive/v1/files/%s/new_comments", validate.EncodePathSegment(docRef.Token))
706-
requestBody := buildCommentCreateV2Request("sheet", "", replyElements, anchor)
780+
requestBody := buildCommentCreateV2Request("sheet", "", "", replyElements, anchor)
707781

708782
fmt.Fprintf(runtime.IO().ErrOut, "Creating sheet comment in %s (sheet=%s, col=%d, row=%d)\n",
709783
common.MaskToken(docRef.Token), anchor.SheetID, anchor.Col, anchor.Row)
@@ -727,6 +801,43 @@ func executeSheetComment(runtime *common.RuntimeContext, docRef commentDocRef) e
727801
return nil
728802
}
729803

804+
func executeSlidesComment(runtime *common.RuntimeContext, docRef commentDocRef) error {
805+
replyElements, err := parseCommentReplyElements(runtime.Str("content"))
806+
if err != nil {
807+
return err
808+
}
809+
810+
blockID, slideBlockType, err := parseSlidesBlockRef(runtime.Str("block-id"))
811+
if err != nil {
812+
return err
813+
}
814+
815+
requestPath := fmt.Sprintf("/open-apis/drive/v1/files/%s/new_comments", validate.EncodePathSegment(docRef.Token))
816+
requestBody := buildCommentCreateV2Request("slides", blockID, slideBlockType, replyElements, nil)
817+
818+
fmt.Fprintf(runtime.IO().ErrOut, "Creating slide block comment in %s (block_id=%s, slide_block_type=%s)\n",
819+
common.MaskToken(docRef.Token), blockID, slideBlockType)
820+
821+
data, err := runtime.CallAPI("POST", requestPath, nil, requestBody)
822+
if err != nil {
823+
return err
824+
}
825+
826+
out := map[string]interface{}{
827+
"comment_id": data["comment_id"],
828+
"file_token": docRef.Token,
829+
"file_type": "slides",
830+
"comment_mode": "slide_block",
831+
"anchor_block_id": blockID,
832+
"slide_block_type": slideBlockType,
833+
}
834+
if createdAt := firstPresentValue(data, "created_at", "create_time"); createdAt != nil {
835+
out["created_at"] = createdAt
836+
}
837+
runtime.Out(out, nil)
838+
return nil
839+
}
840+
730841
func extractURLToken(raw, marker string) (string, bool) {
731842
idx := strings.Index(raw, marker)
732843
if idx < 0 {

0 commit comments

Comments
 (0)