mirror of
https://github.com/larksuite/cli.git
synced 2026-09-14 18:42:53 +08:00
501bf539af
Block 1 — field completion: audio renders <audio key="..." duration="Xs"/> (falls back to [Voice: Xs]/[Voice]); post renders emotion -> :emoji_type:, applies text.style (bold/italic/underline/lineThrough), passes through md; sticker unchanged. Block 2 — opt-in --download-resources (default off) on +chat-messages-list, +messages-mget, +threads-messages-list: extract downloadable resource refs during formatting (image/file/audio/video/media + post-embedded; sticker excluded; merge_forward sub-items carry the top-level container message_id, since the resources endpoint rejects sub-item ids with "234003 File not in msg" and can only fetch a forwarded resource through the container; thread replies get their own block), then download each distinct (message_id, file_key) once into ./lark-im-resources/ with bounded concurrency (3), filling back local_path/size_bytes; single-resource failures are isolated (error:true + stderr warning). Path safety reuses normalizeDownloadOutputPath + ResolveSavePath. Batch download keys each file on disk by its unique file_key basename and only appends an extension (from the Content-Disposition filename or MIME type) — it does NOT substitute the server's Content-Disposition filename. Otherwise two resources whose servers return the same filename (e.g. download.bin) would resolve to the same ./lark-im-resources/ path and clobber each other concurrently. The friendly "adopt the server filename" behavior is kept only for an explicit +messages-resources-download with no --output. Resource ref extraction guards against self-referential / cyclic merge_forward prefetch maps (a real API sub-item list can include the container's own id or a back-pointing merge_forward) via a visited set, so extraction terminates instead of overflowing the stack. The container message_id is threaded through nested merge_forwards as the download owner. Also: document the feature (including the im:message:readonly scope requirement) in skills/lark-im — SKILL.md is generated from skill-template/domains/im.md (edit the source), plus the hand-written message-enrichment + 3 command references. Change-Id: I3a71d7d1b193130f551aaa2ec180ac1500d59ac4 Meego: https://meego.larkoffice.com/5e96d7bff4e7c525510f9156/story/detail/7331555925
86 lines
2.4 KiB
Go
86 lines
2.4 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package convertlib
|
|
|
|
import "fmt"
|
|
|
|
type imageConverter struct{}
|
|
|
|
func (imageConverter) Convert(ctx *ConvertContext) string {
|
|
parsed, err := ParseJSONObject(ctx.RawContent)
|
|
if err != nil {
|
|
return invalidJSONPlaceholder("image")
|
|
}
|
|
if key, _ := parsed["image_key"].(string); key != "" {
|
|
return fmt.Sprintf("[Image: %s]", key)
|
|
}
|
|
return "[Image]"
|
|
}
|
|
|
|
type fileConverter struct{}
|
|
|
|
func (fileConverter) Convert(ctx *ConvertContext) string {
|
|
parsed, err := ParseJSONObject(ctx.RawContent)
|
|
if err != nil {
|
|
return invalidJSONPlaceholder("file")
|
|
}
|
|
key, _ := parsed["file_key"].(string)
|
|
if key == "" {
|
|
return "[File]"
|
|
}
|
|
name, _ := parsed["file_name"].(string)
|
|
if name == "" {
|
|
name = key
|
|
}
|
|
return fmt.Sprintf(`<file key="%s" name="%s"/>`, cardEscapeAttr(key), cardEscapeAttr(name))
|
|
}
|
|
|
|
type audioMsgConverter struct{}
|
|
|
|
// Convert renders an audio message: when body.content carries a file_key it
|
|
// emits <audio key="..." duration="Xs"/> (duration omitted when absent);
|
|
// otherwise it falls back to [Voice: Xs] (duration only) or [Voice].
|
|
func (audioMsgConverter) Convert(ctx *ConvertContext) string {
|
|
parsed, err := ParseJSONObject(ctx.RawContent)
|
|
if err != nil {
|
|
return invalidJSONPlaceholder("audio")
|
|
}
|
|
if key, _ := parsed["file_key"].(string); key != "" {
|
|
result := fmt.Sprintf(`<audio key="%s"`, cardEscapeAttr(key))
|
|
if dur, ok := parsed["duration"].(float64); ok && dur > 0 {
|
|
result += fmt.Sprintf(` duration="%.0fs"`, dur/1000)
|
|
}
|
|
return result + "/>"
|
|
}
|
|
if dur, ok := parsed["duration"].(float64); ok && dur > 0 {
|
|
return fmt.Sprintf("[Voice: %.0fs]", dur/1000)
|
|
}
|
|
return "[Voice]"
|
|
}
|
|
|
|
type videoMsgConverter struct{}
|
|
|
|
func (videoMsgConverter) Convert(ctx *ConvertContext) string {
|
|
parsed, err := ParseJSONObject(ctx.RawContent)
|
|
if err != nil {
|
|
return invalidJSONPlaceholder("video")
|
|
}
|
|
key, _ := parsed["file_key"].(string)
|
|
if key == "" {
|
|
return "[Video]"
|
|
}
|
|
name, _ := parsed["file_name"].(string)
|
|
if name == "" {
|
|
name = key
|
|
}
|
|
result := fmt.Sprintf(`<video key="%s" name="%s"`, cardEscapeAttr(key), cardEscapeAttr(name))
|
|
if dur, ok := parsed["duration"].(float64); ok && dur > 0 {
|
|
result += fmt.Sprintf(` duration="%.0fs"`, dur/1000)
|
|
}
|
|
if coverKey, _ := parsed["image_key"].(string); coverKey != "" {
|
|
result += fmt.Sprintf(` cover_image_key="%s"`, cardEscapeAttr(coverKey))
|
|
}
|
|
return result + "/>"
|
|
}
|