Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions shortcuts/doc/docs_content_compat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package doc

import (
"fmt"
"regexp"
"strings"
)

var fetchedVideoTagRe = regexp.MustCompile(`<video\s+([^>]*?)></video>`)

// normalizeDocInputContent keeps the current docs_ai request path intact while
// accepting a small compatibility surface from the legacy converter pipeline.
// Today this is intentionally limited to translating exported <video ...>
// elements back into file blocks that docs_ai already understands.
func normalizeDocInputContent(format, content string) string {
if strings.TrimSpace(content) == "" {
return content
}
switch strings.TrimSpace(format) {
case "xml", "markdown", "":
return normalizeInputVideoTags(content)
default:
return content
}
}

func normalizeInputVideoTags(content string) string {
return fetchedVideoTagRe.ReplaceAllStringFunc(content, func(tag string) string {
src := strings.TrimSpace(fetchedAttrValue(tag, "src"))
name := strings.TrimSpace(fetchedAttrValue(tag, "data-name"))
if src == "" || !strings.HasPrefix(src, "feishu://media/") {
return tag
}
token := strings.TrimPrefix(src, "feishu://media/")
if token == "" {
return tag
}

attrs := []string{fmt.Sprintf(`token="%s"`, token)}
if name != "" {
attrs = append(attrs, fmt.Sprintf(`name="%s"`, name))
}
if viewType := strings.TrimSpace(fetchedAttrValue(tag, "data-view-type")); viewType != "" {
attrs = append(attrs, fmt.Sprintf(`view-type="%s"`, viewType))
}
return fmt.Sprintf("<file %s/>", strings.Join(attrs, " "))
})
}
66 changes: 66 additions & 0 deletions shortcuts/doc/docs_content_compat_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package doc

import (
"testing"

"github.com/spf13/cobra"

"github.com/larksuite/cli/shortcuts/common"
)

func newDocCompatRuntime(t *testing.T, stringFlags map[string]string) *common.RuntimeContext {
t.Helper()
cmd := &cobra.Command{Use: "docs-test"}
for name := range stringFlags {
cmd.Flags().String(name, "", "")
}
for name, value := range stringFlags {
if err := cmd.Flags().Set(name, value); err != nil {
t.Fatalf("set flag %s: %v", name, err)
}
}
return common.TestNewRuntimeContext(cmd, nil)
}

func TestNormalizeInputVideoTags(t *testing.T) {
t.Parallel()

input := `<video controls src="feishu://media/file_video_123" data-name="demo.mp4"></video>`
got := normalizeInputVideoTags(input)
want := `<file token="file_video_123" name="demo.mp4"/>`
if got != want {
t.Fatalf("normalizeInputVideoTags() = %q, want %q", got, want)
}
}

func TestNormalizeInputVideoTagsPreservesViewType(t *testing.T) {
t.Parallel()

input := `<video controls src="feishu://media/file_video_123" data-name="demo.mp4" data-view-type="2"></video>`
got := normalizeInputVideoTags(input)
want := `<file token="file_video_123" name="demo.mp4" view-type="2"/>`
if got != want {
t.Fatalf("normalizeInputVideoTags() = %q, want %q", got, want)
}
}

func TestNormalizeInputVideoTagsLeavesLocalSourceAlone(t *testing.T) {
t.Parallel()

input := `<video controls src="./demo.mp4"></video>`
if got := normalizeInputVideoTags(input); got != input {
t.Fatalf("normalizeInputVideoTags() = %q, want unchanged %q", got, input)
}
}

func TestNormalizeDocInputContentSkipsText(t *testing.T) {
t.Parallel()

input := `<video controls src="feishu://media/file_video_123"></video>`
if got := normalizeDocInputContent("text", input); got != input {
t.Fatalf("normalizeDocInputContent(text) = %q, want unchanged %q", got, input)
}
}
14 changes: 14 additions & 0 deletions shortcuts/doc/docs_create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,20 @@ func TestDocsCreateV2BotAutoGrantFailureDoesNotFailCreate(t *testing.T) {
}
}

func TestBuildCreateBodyV2NormalizesVideoCompatMarkup(t *testing.T) {
t.Parallel()

runtime := newDocCompatRuntime(t, map[string]string{
"doc-format": "xml",
"content": `<video controls src="feishu://media/file_video_123" data-name="demo.mp4" data-view-type="2"></video>`,
})

body := buildCreateBody(runtime)
if got := body["content"]; got != `<file token="file_video_123" name="demo.mp4" view-type="2"/>` {
t.Fatalf("buildCreateBody().content = %#v", got)
}
}

// ── V1 (MCP) tests ──

func TestDocsCreateV1BotAutoGrantSuccess(t *testing.T) {
Expand Down
5 changes: 3 additions & 2 deletions shortcuts/doc/docs_create_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,10 @@ func executeCreateV2(_ context.Context, runtime *common.RuntimeContext) error {
}

func buildCreateBody(runtime *common.RuntimeContext) map[string]interface{} {
format := runtime.Str("doc-format")
body := map[string]interface{}{
"format": runtime.Str("doc-format"),
"content": runtime.Str("content"),
"format": format,
"content": normalizeDocInputContent(format, runtime.Str("content")),
}
if v := runtime.Str("parent-token"); v != "" {
body["parent_token"] = v
Expand Down
124 changes: 124 additions & 0 deletions shortcuts/doc/docs_fetch_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"context"
"fmt"
"io"
"regexp"
"strconv"
"strings"

Expand Down Expand Up @@ -67,6 +68,7 @@ func executeFetchV2(_ context.Context, runtime *common.RuntimeContext) error {
if err != nil {
return err
}
normalizeFetchedDocumentContent(data, runtime.Str("doc-format"))

runtime.OutFormatRaw(data, nil, func(w io.Writer) {
if doc, ok := data["document"].(map[string]interface{}); ok {
Expand All @@ -78,6 +80,128 @@ func executeFetchV2(_ context.Context, runtime *common.RuntimeContext) error {
return nil
}

var (
fetchedVideoFileTagRe = regexp.MustCompile(`<file\s+([^>]*\btoken="([^"]+)"[^>]*\bname="([^"]+)"[^>]*)/>`)
fetchedVideoFigureTagRe = regexp.MustCompile(`<figure\s+([^>]*?)><source\s+([^>]*?)token="([^"]+)"([^>]*?)/></figure>`)
fetchedSheetIDTagRe = regexp.MustCompile(`<sheet\s+([^>]*?)sheet-id="([^"]+)"([^>]*?)(?:></sheet>|/>)`)
)

// normalizeFetchedDocumentContent applies lightweight export rewrites so the
// current CLI can surface a couple of higher-level compatibility behaviors from
// the legacy converter pipeline without reintroducing that entire subsystem.
func normalizeFetchedDocumentContent(data map[string]interface{}, format string) {
if strings.TrimSpace(format) == "text" {
return
}
doc, _ := data["document"].(map[string]interface{})
if doc == nil {
return
}
content, _ := doc["content"].(string)
if content == "" {
return
}
content = normalizeFetchedVideoTags(content)
content = normalizeFetchedSheetTags(content)
doc["content"] = content
}

func normalizeFetchedVideoTags(content string) string {
content = fetchedVideoFigureTagRe.ReplaceAllStringFunc(content, func(tag string) string {
m := fetchedVideoFigureTagRe.FindStringSubmatch(tag)
if len(m) != 5 {
return tag
}
viewType := strings.TrimSpace(fetchedAttrValue(tag, "view-type"))
token := strings.TrimSpace(m[3])
mime := strings.TrimSpace(fetchedAttrValue(tag, "mime"))

attrs := []string{"controls"}
if token != "" {
attrs = append(attrs, fmt.Sprintf(`src="feishu://media/%s"`, token))
}
if mime != "" {
attrs = append(attrs, fmt.Sprintf(`data-mime="%s"`, mime))
}
if viewType != "" {
attrs = append(attrs, fmt.Sprintf(`data-view-type="%s"`, viewType))
}
return fmt.Sprintf("<video %s></video>", strings.Join(attrs, " "))
})

return fetchedVideoFileTagRe.ReplaceAllStringFunc(content, func(tag string) string {
m := fetchedVideoFileTagRe.FindStringSubmatch(tag)
if len(m) != 4 {
return tag
}
token := strings.TrimSpace(m[2])
name := strings.TrimSpace(m[3])
if !isFetchedVideoFilename(name) {
return tag
}

attrs := []string{"controls"}
if token != "" {
attrs = append(attrs, fmt.Sprintf(`src="feishu://media/%s"`, token))
}
if name != "" {
attrs = append(attrs, fmt.Sprintf(`data-name="%s"`, name))
}
if viewType := fetchedAttrValue(tag, "view-type"); viewType != "" {
attrs = append(attrs, fmt.Sprintf(`data-view-type="%s"`, viewType))
}
return fmt.Sprintf("<video %s></video>", strings.Join(attrs, " "))
})
}

func normalizeFetchedSheetTags(content string) string {
return fetchedSheetIDTagRe.ReplaceAllStringFunc(content, func(tag string) string {
m := fetchedSheetIDTagRe.FindStringSubmatch(tag)
if len(m) != 4 || strings.Contains(tag, ` id="`) {
return tag
}
sheetID := strings.TrimSpace(m[2])
if sheetID == "" {
return tag
}
return strings.Replace(tag, fmt.Sprintf(`sheet-id="%s"`, sheetID), fmt.Sprintf(`id="%s"`, sheetID), 1)
})
}

func fetchedAttrValue(tag, attr string) string {
needle := attr + `="`
idx := strings.Index(tag, needle)
if idx == -1 {
return ""
}
start := idx + len(needle)
end := strings.Index(tag[start:], `"`)
if end == -1 {
return ""
}
return tag[start : start+end]
}

func isFetchedVideoFilename(name string) bool {
name = strings.ToLower(strings.TrimSpace(name))
switch {
case strings.HasSuffix(name, ".mp4"):
return true
case strings.HasSuffix(name, ".mov"):
return true
case strings.HasSuffix(name, ".m4v"):
return true
case strings.HasSuffix(name, ".webm"):
return true
case strings.HasSuffix(name, ".avi"):
return true
case strings.HasSuffix(name, ".mkv"):
return true
default:
return false
}
}

func buildFetchBody(runtime *common.RuntimeContext) map[string]interface{} {
body := map[string]interface{}{
"format": runtime.Str("doc-format"),
Expand Down
Loading
Loading