Skip to content

Commit 843a882

Browse files
zhumiaoxinseemslike
authored andcommitted
feat: add flag shortcuts for im
Change-Id: I8f87f0eadf83fb59b024a3b9fe67b23d363abe0a
1 parent 05d8137 commit 843a882

14 files changed

Lines changed: 2103 additions & 1 deletion

internal/registry/service_descriptions.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@
2727
"en": { "title": "Event", "description": "Event subscription management" },
2828
"zh": { "title": "事件订阅", "description": "WebSocket 实时推送" }
2929
},
30+
"flag": {
31+
"en": { "title": "Flag", "description": "Bookmark on messages and threads" },
32+
"zh": { "title": "标记", "description": "消息与 thread 标记的创建、取消、列表" }
33+
},
3034
"im": {
3135
"en": { "title": "Messenger", "description": "Message and group chat management" },
3236
"zh": { "title": "消息与群组", "description": "消息发送、群聊管理" }

shortcuts/im/helpers.go

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1432,3 +1432,171 @@ func uploadFileFromReader(ctx context.Context, runtime *common.RuntimeContext, r
14321432
}
14331433
return fileKey, nil
14341434
}
1435+
1436+
// FlagType enumerates the kind of bookmark.
1437+
// Aligned with server-side constants: Unknown=0, Feed=1, Message=2.
1438+
type FlagType int
1439+
1440+
const (
1441+
FlagTypeUnknown FlagType = 0
1442+
FlagTypeFeed FlagType = 1
1443+
FlagTypeMessage FlagType = 2
1444+
)
1445+
1446+
// ItemType enumerates the kind of thing being bookmarked.
1447+
// Server-side constants (only the types used by IM flags):
1448+
//
1449+
// default=0, thread=4, msg_thread=11.
1450+
//
1451+
// Note on the two thread-shaped item types:
1452+
// - ItemTypeThread (4) — thread inside a topic-style chat
1453+
// - ItemTypeMsgThread (11) — thread inside a regular chat
1454+
type ItemType int
1455+
1456+
const (
1457+
ItemTypeDefault ItemType = 0
1458+
ItemTypeThread ItemType = 4 // thread in a topic-style chat
1459+
ItemTypeMsgThread ItemType = 11 // thread in a regular chat
1460+
)
1461+
1462+
// flagItem is one entry in the flags API body. The server expects numeric
1463+
// enums serialized as strings.
1464+
type flagItem struct {
1465+
ItemID string `json:"item_id"`
1466+
ItemType string `json:"item_type"`
1467+
FlagType string `json:"flag_type"`
1468+
}
1469+
1470+
// parseItemID inspects an om_/omt_ prefix and returns a best-guess
1471+
// (itemType, flagType) pair. Used when the user omits the explicit enums.
1472+
// - om_xxx → (default, message)
1473+
// - omt_xxx → (thread, feed)
1474+
func parseItemID(id string) (ItemType, FlagType, error) {
1475+
id = strings.TrimSpace(id)
1476+
switch {
1477+
case strings.HasPrefix(id, "omt_"):
1478+
return ItemTypeThread, FlagTypeFeed, nil
1479+
case strings.HasPrefix(id, "om_"):
1480+
return ItemTypeDefault, FlagTypeMessage, nil
1481+
case id == "":
1482+
return 0, 0, output.ErrValidation("--item-id / --message-id / --thread-id cannot be empty")
1483+
default:
1484+
return 0, 0, output.ErrValidation(
1485+
"cannot infer item type from id %q: expected om_ (message) or omt_ (thread) prefix; "+
1486+
"pass --item-type and --flag-type explicitly if you are using a different id format", id)
1487+
}
1488+
}
1489+
1490+
// parseItemType converts a user-facing string to the server enum.
1491+
func parseItemType(s string) (ItemType, error) {
1492+
switch strings.ToLower(strings.TrimSpace(s)) {
1493+
case "", "default", "message":
1494+
return ItemTypeDefault, nil
1495+
case "thread":
1496+
return ItemTypeThread, nil
1497+
case "msg_thread":
1498+
return ItemTypeMsgThread, nil
1499+
}
1500+
return 0, output.ErrValidation("invalid --item-type %q: expected one of default|thread|msg_thread", s)
1501+
}
1502+
1503+
// parseFlagType converts a user-facing string to the server enum.
1504+
func parseFlagType(s string) (FlagType, error) {
1505+
switch strings.ToLower(strings.TrimSpace(s)) {
1506+
case "", "message":
1507+
return FlagTypeMessage, nil
1508+
case "feed":
1509+
return FlagTypeFeed, nil
1510+
case "unknown":
1511+
return FlagTypeUnknown, nil
1512+
}
1513+
return 0, output.ErrValidation("invalid --flag-type %q: expected one of message|feed", s)
1514+
}
1515+
1516+
// isValidCombo checks if the (ItemType, FlagType) pair is accepted by the server.
1517+
// Valid combinations are:
1518+
// - (default, message) — regular chat message
1519+
// - (thread, feed) — thread as feed-layer flag
1520+
// - (msg_thread, feed) — message-thread as feed-layer flag
1521+
func isValidCombo(it ItemType, ft FlagType) bool {
1522+
return (it == ItemTypeDefault && ft == FlagTypeMessage) ||
1523+
(it == ItemTypeThread && ft == FlagTypeFeed) ||
1524+
(it == ItemTypeMsgThread && ft == FlagTypeFeed)
1525+
}
1526+
1527+
// newFlagItem builds a payload entry with numeric-stringified enums.
1528+
func newFlagItem(itemID string, it ItemType, ft FlagType) flagItem {
1529+
return flagItem{
1530+
ItemID: itemID,
1531+
ItemType: fmt.Sprintf("%d", int(it)),
1532+
FlagType: fmt.Sprintf("%d", int(ft)),
1533+
}
1534+
}
1535+
1536+
// checkIsThreadRoot queries the message API to check if a message is a thread root.
1537+
// Returns (isThreadRoot, chatID, error). chatID is empty when the message is not a thread root.
1538+
func checkIsThreadRoot(rt *common.RuntimeContext, messageID string) (bool, string, error) {
1539+
data, err := rt.DoAPIJSON("GET", "/open-apis/im/v1/messages/"+validate.EncodePathSegment(messageID), nil, nil)
1540+
if err != nil {
1541+
return false, "", err
1542+
}
1543+
1544+
// Check items array in response
1545+
items, _ := data["items"].([]any)
1546+
if len(items) == 0 {
1547+
return false, "", nil
1548+
}
1549+
1550+
msg, _ := items[0].(map[string]any)
1551+
threadID, _ := msg["thread_id"].(string)
1552+
chatID, _ := msg["chat_id"].(string)
1553+
return threadID != "", chatID, nil
1554+
}
1555+
1556+
// resolveThreadFeedItemType determines the correct feed-layer ItemType for a thread
1557+
// by querying the chat API for chat_mode.
1558+
// - topic-style chat → ItemTypeThread
1559+
// - regular chat → ItemTypeMsgThread
1560+
//
1561+
// Falls back to ItemTypeMsgThread if the chat query fails.
1562+
func resolveThreadFeedItemType(rt *common.RuntimeContext, chatID string) ItemType {
1563+
data, err := rt.DoAPIJSON("GET", "/open-apis/im/v1/chats/"+validate.EncodePathSegment(chatID), nil, nil)
1564+
if err != nil {
1565+
return ItemTypeMsgThread
1566+
}
1567+
1568+
// DoAPIJSON returns envelope.Data, so chat_mode is at the top level
1569+
chatMode, _ := data["chat_mode"].(string)
1570+
if chatMode == "topic" {
1571+
return ItemTypeThread
1572+
}
1573+
return ItemTypeMsgThread
1574+
}
1575+
1576+
// resolveThreadFeedItemTypeFromThread resolves the feed-layer ItemType for an omt_ thread ID.
1577+
// It queries the messages API with container_id_type=thread to get the chat_id,
1578+
// then queries the chat API for chat_mode.
1579+
// Falls back to ItemTypeMsgThread if any query fails.
1580+
func resolveThreadFeedItemTypeFromThread(rt *common.RuntimeContext, threadID string) ItemType {
1581+
data, err := rt.DoAPIJSON("GET", "/open-apis/im/v1/messages", larkcore.QueryParams{
1582+
"container_id_type": []string{"thread"},
1583+
"container_id": []string{threadID},
1584+
"page_size": []string{"1"},
1585+
}, nil)
1586+
if err != nil {
1587+
return ItemTypeMsgThread
1588+
}
1589+
1590+
items, _ := data["items"].([]any)
1591+
if len(items) == 0 {
1592+
return ItemTypeMsgThread
1593+
}
1594+
1595+
msg, _ := items[0].(map[string]any)
1596+
chatID, _ := msg["chat_id"].(string)
1597+
if chatID == "" {
1598+
return ItemTypeMsgThread
1599+
}
1600+
1601+
return resolveThreadFeedItemType(rt, chatID)
1602+
}

shortcuts/im/helpers_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -868,6 +868,9 @@ func TestShortcuts(t *testing.T) {
868868
"+messages-search",
869869
"+messages-send",
870870
"+threads-messages-list",
871+
"+flag-create",
872+
"+flag-cancel",
873+
"+flag-list",
871874
}
872875
if !reflect.DeepEqual(commands, want) {
873876
t.Fatalf("Shortcuts() commands = %#v, want %#v", commands, want)

0 commit comments

Comments
 (0)