mirror of
https://github.com/chenhg5/cc-connect.git
synced 2026-10-25 17:36:43 +08:00
feat(card): add tagged notes and improve delete mode flow
Add Tag field to CardNote struct and TaggedNote method to CardBuilder for programmatic note handling. Simplify delete mode toggle logic by returning updated cards instead of nil and using native Feishu form checkers. Improve Discord command description truncation using runes. generated by llmgit Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
dc5cc3d0df
commit
3f438f0055
6 files changed
+38
-43
No files matched your search
+13
-1
@@ -37,7 +37,12 @@ type CardActions struct {
|
||||
}
|
||||
|
||||
// CardNote renders small footnote text at the bottom.
|
||||
type CardNote struct{ Text string }
|
||||
// Tag is an optional machine-readable identifier (not displayed) used by
|
||||
// platform renderers to recognize and handle specific notes programmatically.
|
||||
type CardNote struct {
|
||||
Text string
|
||||
Tag string
|
||||
}
|
||||
|
||||
// CardListItem renders a row with description text on the left and a button on the right.
|
||||
// On Feishu this maps to div+extra; on other platforms it degrades to a text line.
|
||||
@@ -203,6 +208,13 @@ func (b *CardBuilder) Note(text string) *CardBuilder {
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *CardBuilder) TaggedNote(tag, text string) *CardBuilder {
|
||||
if text != "" {
|
||||
b.card.Elements = append(b.card.Elements, CardNote{Text: text, Tag: tag})
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Build returns the constructed Card.
|
||||
func (b *CardBuilder) Build() *Card {
|
||||
c := b.card
|
||||
|
||||
+1
-6
@@ -4353,11 +4353,6 @@ func (e *Engine) handleCardNav(action string, sessionKey string) *Card {
|
||||
case "/switch":
|
||||
return e.renderListCardSafe(sessionKey, 1)
|
||||
case "/delete-mode":
|
||||
if prefix == "act" {
|
||||
if fields := strings.Fields(args); len(fields) > 0 && fields[0] == "toggle" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(args, "cancel") {
|
||||
return e.renderListCardSafe(sessionKey, 1)
|
||||
}
|
||||
@@ -4684,7 +4679,7 @@ func (e *Engine) renderDeleteModeSelectCard(sessionKey string, sessions *Session
|
||||
action,
|
||||
)
|
||||
}
|
||||
cb.Note(e.i18n.Tf(MsgDeleteModeSelectedCount, selectedCount))
|
||||
cb.TaggedNote("delete-mode-selected-count", e.i18n.Tf(MsgDeleteModeSelectedCount, selectedCount))
|
||||
if dm.hint != "" {
|
||||
cb.Note(dm.hint)
|
||||
}
|
||||
|
||||
+6
-3
@@ -1071,7 +1071,7 @@ func TestCmdDelete_NoArgsOnCardPlatformShowsDeleteModeCard(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteMode_ToggleSelectionDoesNotReturnCardButPersistsSelection(t *testing.T) {
|
||||
func TestDeleteMode_ToggleSelectionReturnsUpdatedCard(t *testing.T) {
|
||||
p := &stubCardPlatform{stubPlatformEngine: stubPlatformEngine{n: "feishu"}}
|
||||
agent := &stubDeleteAgent{stubListAgent: stubListAgent{sessions: []AgentSessionInfo{
|
||||
{ID: "session-1", Summary: "One"},
|
||||
@@ -1082,8 +1082,11 @@ func TestDeleteMode_ToggleSelectionDoesNotReturnCardButPersistsSelection(t *test
|
||||
|
||||
e.cmdDelete(p, msg, nil)
|
||||
card := e.handleCardNav("act:/delete-mode toggle session-2", msg.SessionKey)
|
||||
if card != nil {
|
||||
t.Fatal("expected no card update after toggle")
|
||||
if card == nil {
|
||||
t.Fatal("expected card update after toggle")
|
||||
}
|
||||
if !strings.Contains(card.RenderText(), "1 selected") {
|
||||
t.Fatalf("card text = %q, want selected count", card.RenderText())
|
||||
}
|
||||
|
||||
confirmCard := e.handleCardNav("act:/delete-mode confirm", msg.SessionKey)
|
||||
|
||||
@@ -79,7 +79,7 @@ func (p *Platform) makeSessionKey(channelID string, userID string) string {
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterCommands registers bot commands with Telegram for the command menu.
|
||||
// RegisterCommands registers bot commands with Discord for the slash command menu.
|
||||
func (p *Platform) RegisterCommands(commands []core.BotCommandInfo) error {
|
||||
// Wait for Ready event to ensure appID is populated
|
||||
select {
|
||||
@@ -95,8 +95,8 @@ func (p *Platform) RegisterCommands(commands []core.BotCommandInfo) error {
|
||||
continue
|
||||
}
|
||||
desc := c.Description
|
||||
if len(desc) > 100 {
|
||||
desc = desc[:97] + "..."
|
||||
if runes := []rune(desc); len(runes) > 100 {
|
||||
desc = string(runes[:97]) + "..."
|
||||
}
|
||||
cmds = append(cmds, &discordgo.ApplicationCommand{
|
||||
Name: c.Command,
|
||||
|
||||
+5
-22
@@ -5,7 +5,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/chenhg5/cc-connect/core"
|
||||
@@ -246,7 +245,7 @@ func renderDeleteModeCheckerCard(card *core.Card, base map[string]any) (map[stri
|
||||
|
||||
rows := make([]deleteModeCheckerRow, 0)
|
||||
formRowElements := make([]map[string]any, 0)
|
||||
notes := make([]string, 0)
|
||||
notes := make([]core.CardNote, 0)
|
||||
navRows := make([]core.CardActions, 0)
|
||||
submitText := ""
|
||||
cancelText := ""
|
||||
@@ -282,7 +281,7 @@ func renderDeleteModeCheckerCard(card *core.Card, base map[string]any) (map[stri
|
||||
},
|
||||
})
|
||||
case core.CardNote:
|
||||
notes = append(notes, e.Text)
|
||||
notes = append(notes, e)
|
||||
case core.CardActions:
|
||||
remaining := make([]core.CardButton, 0, len(e.Buttons))
|
||||
for _, btn := range e.Buttons {
|
||||
@@ -309,15 +308,15 @@ func renderDeleteModeCheckerCard(card *core.Card, base map[string]any) (map[stri
|
||||
|
||||
elements := make([]map[string]any, 0, len(notes)+1+len(navRows))
|
||||
for _, n := range notes {
|
||||
if n == "" {
|
||||
if n.Text == "" {
|
||||
continue
|
||||
}
|
||||
if isDeleteModeSelectedCountNote(n) {
|
||||
if n.Tag == "delete-mode-selected-count" {
|
||||
continue
|
||||
}
|
||||
elements = append(elements, map[string]any{
|
||||
"tag": "note",
|
||||
"elements": []map[string]any{plainText(n)},
|
||||
"elements": []map[string]any{plainText(n.Text)},
|
||||
})
|
||||
}
|
||||
formElements := append([]map[string]any{}, formRowElements...)
|
||||
@@ -429,22 +428,6 @@ func parseDeleteModeListItemAction(action string) (id string, selectable bool, o
|
||||
}
|
||||
}
|
||||
|
||||
func isDeleteModeSelectedCountNote(text string) bool {
|
||||
t := strings.TrimSpace(text)
|
||||
return regexpDeleteModeSelectedCountEn.MatchString(t) ||
|
||||
regexpDeleteModeSelectedCountZh.MatchString(t) ||
|
||||
regexpDeleteModeSelectedCountZhTW.MatchString(t) ||
|
||||
regexpDeleteModeSelectedCountJa.MatchString(t) ||
|
||||
regexpDeleteModeSelectedCountEs.MatchString(t)
|
||||
}
|
||||
|
||||
var (
|
||||
regexpDeleteModeSelectedCountEn = regexp.MustCompile(`^\d+\s+selected$`)
|
||||
regexpDeleteModeSelectedCountZh = regexp.MustCompile(`^已选\s+\d+\s+项$`)
|
||||
regexpDeleteModeSelectedCountZhTW = regexp.MustCompile(`^已選\s+\d+\s+項$`)
|
||||
regexpDeleteModeSelectedCountJa = regexp.MustCompile(`^\d+\s+件を選択中$`)
|
||||
regexpDeleteModeSelectedCountEs = regexp.MustCompile(`^\d+\s+seleccionadas$`)
|
||||
)
|
||||
|
||||
// renderCard converts a core.Card into the Feishu Interactive Card JSON string.
|
||||
func renderCard(card *core.Card) string {
|
||||
|
||||
@@ -238,6 +238,16 @@ func (p *Platform) onCardAction(event *callback.CardActionTriggerEvent) (*callba
|
||||
|
||||
// nav: / act: — synchronous card update
|
||||
if strings.HasPrefix(actionVal, "nav:") || strings.HasPrefix(actionVal, "act:") {
|
||||
// Feishu uses native form checker for delete-mode toggle,
|
||||
// so return a toast without calling cardNavHandler to avoid a full card refresh.
|
||||
if strings.HasPrefix(actionVal, "act:/delete-mode toggle ") {
|
||||
return &callback.CardActionTriggerResponse{
|
||||
Toast: &callback.Toast{
|
||||
Type: "info",
|
||||
Content: "已记录选择(Selection recorded)",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
if p.cardNavHandler != nil {
|
||||
card := p.cardNavHandler(actionVal, sessionKey)
|
||||
if card != nil {
|
||||
@@ -250,14 +260,6 @@ func (p *Platform) onCardAction(event *callback.CardActionTriggerEvent) (*callba
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(actionVal, "act:") {
|
||||
if strings.HasPrefix(actionVal, "act:/delete-mode toggle ") {
|
||||
return &callback.CardActionTriggerResponse{
|
||||
Toast: &callback.Toast{
|
||||
Type: "info",
|
||||
Content: "已记录选择(Selection recorded)",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
slog.Debug(p.tag()+": card action produced no card update", "action", actionVal)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user