初始化仓库
This commit is contained in:
+310
@@ -0,0 +1,310 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Card represents a structured rich message that can be rendered as
|
||||
// platform-specific cards (Feishu Interactive Card, Telegram message, etc.)
|
||||
// or degraded to plain text for platforms without card support.
|
||||
type Card struct {
|
||||
Header *CardHeader
|
||||
Elements []CardElement
|
||||
}
|
||||
|
||||
// CardHeader is the optional colored title bar of a card.
|
||||
type CardHeader struct {
|
||||
Title string
|
||||
Color string // blue, green, red, orange, purple, grey, turquoise, violet, indigo, wathet, yellow, carmine
|
||||
}
|
||||
|
||||
// CardElement is the interface satisfied by all card content elements.
|
||||
type CardElement interface {
|
||||
cardElement()
|
||||
}
|
||||
|
||||
// CardMarkdown renders markdown-formatted text.
|
||||
type CardMarkdown struct{ Content string }
|
||||
|
||||
// CardDivider renders a horizontal rule.
|
||||
type CardDivider struct{}
|
||||
|
||||
// CardActions renders a row of clickable buttons.
|
||||
type CardActions struct {
|
||||
Buttons []CardButton
|
||||
Layout CardActionLayout
|
||||
}
|
||||
|
||||
// CardNote renders small footnote text at the bottom.
|
||||
// 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.
|
||||
type CardListItem struct {
|
||||
Text string // left-side description
|
||||
BtnText string // button label
|
||||
BtnType string // "primary", "default", "danger"
|
||||
BtnValue string // callback data
|
||||
Extra map[string]string // additional key-value pairs carried in the callback
|
||||
}
|
||||
|
||||
// CardSelect renders a dropdown selector.
|
||||
// On Feishu this maps to select_static; on other platforms it degrades to text.
|
||||
type CardSelect struct {
|
||||
Placeholder string
|
||||
Options []CardSelectOption
|
||||
InitValue string // pre-selected option value (empty = none)
|
||||
}
|
||||
|
||||
// CardSelectOption is one item in a CardSelect dropdown.
|
||||
type CardSelectOption struct {
|
||||
Text string
|
||||
Value string
|
||||
}
|
||||
|
||||
func (CardMarkdown) cardElement() {}
|
||||
func (CardDivider) cardElement() {}
|
||||
func (CardActions) cardElement() {}
|
||||
func (CardNote) cardElement() {}
|
||||
func (CardListItem) cardElement() {}
|
||||
func (CardSelect) cardElement() {}
|
||||
|
||||
// CardButton represents a clickable button inside a CardActions element.
|
||||
type CardButton struct {
|
||||
Text string // display label
|
||||
Type string // "primary", "default", "danger"
|
||||
Value string // callback data, e.g. "cmd:/new", "cmd:/switch 3"
|
||||
Extra map[string]string // additional key-value pairs carried in the callback (platform-specific)
|
||||
}
|
||||
|
||||
// CardActionLayout controls how a CardActions row should be rendered by
|
||||
// platforms with richer layout capabilities.
|
||||
type CardActionLayout string
|
||||
|
||||
const (
|
||||
CardActionLayoutRow CardActionLayout = "row"
|
||||
CardActionLayoutEqualColumns CardActionLayout = "equal_columns"
|
||||
)
|
||||
|
||||
// Btn is a shorthand constructor for CardButton.
|
||||
func Btn(text, typ, value string) CardButton {
|
||||
return CardButton{Text: text, Type: typ, Value: value}
|
||||
}
|
||||
|
||||
// PrimaryBtn creates a primary-styled button.
|
||||
func PrimaryBtn(text, value string) CardButton {
|
||||
return CardButton{Text: text, Type: "primary", Value: value}
|
||||
}
|
||||
|
||||
// DefaultBtn creates a default-styled button.
|
||||
func DefaultBtn(text, value string) CardButton {
|
||||
return CardButton{Text: text, Type: "default", Value: value}
|
||||
}
|
||||
|
||||
// DangerBtn creates a danger-styled button.
|
||||
func DangerBtn(text, value string) CardButton {
|
||||
return CardButton{Text: text, Type: "danger", Value: value}
|
||||
}
|
||||
|
||||
// --- Builder API ---
|
||||
|
||||
// CardBuilder provides a fluent API for constructing Card instances.
|
||||
type CardBuilder struct {
|
||||
card Card
|
||||
}
|
||||
|
||||
// NewCard returns a new CardBuilder.
|
||||
func NewCard() *CardBuilder {
|
||||
return &CardBuilder{}
|
||||
}
|
||||
|
||||
// Title sets the card header with a title and color.
|
||||
func (b *CardBuilder) Title(title, color string) *CardBuilder {
|
||||
b.card.Header = &CardHeader{Title: title, Color: color}
|
||||
return b
|
||||
}
|
||||
|
||||
// Markdown appends a markdown text element.
|
||||
func (b *CardBuilder) Markdown(content string) *CardBuilder {
|
||||
if content != "" {
|
||||
b.card.Elements = append(b.card.Elements, CardMarkdown{Content: content})
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Markdownf appends a formatted markdown text element.
|
||||
func (b *CardBuilder) Markdownf(format string, args ...any) *CardBuilder {
|
||||
return b.Markdown(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Divider appends a horizontal divider.
|
||||
func (b *CardBuilder) Divider() *CardBuilder {
|
||||
b.card.Elements = append(b.card.Elements, CardDivider{})
|
||||
return b
|
||||
}
|
||||
|
||||
// Buttons appends an action row with the given buttons.
|
||||
func (b *CardBuilder) Buttons(buttons ...CardButton) *CardBuilder {
|
||||
if len(buttons) > 0 {
|
||||
b.card.Elements = append(b.card.Elements, CardActions{Buttons: buttons, Layout: CardActionLayoutRow})
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// ButtonsEqual appends an action row where each button should take equal width
|
||||
// on platforms that support richer layouts.
|
||||
func (b *CardBuilder) ButtonsEqual(buttons ...CardButton) *CardBuilder {
|
||||
if len(buttons) > 0 {
|
||||
b.card.Elements = append(b.card.Elements, CardActions{Buttons: buttons, Layout: CardActionLayoutEqualColumns})
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// ListItem appends a list row: description on the left, button on the right.
|
||||
func (b *CardBuilder) ListItem(desc, btnText, btnValue string) *CardBuilder {
|
||||
b.card.Elements = append(b.card.Elements, CardListItem{
|
||||
Text: desc, BtnText: btnText, BtnType: "default", BtnValue: btnValue,
|
||||
})
|
||||
return b
|
||||
}
|
||||
|
||||
// ListItemBtn is like ListItem but allows specifying the button type.
|
||||
func (b *CardBuilder) ListItemBtn(desc, btnText, btnType, btnValue string) *CardBuilder {
|
||||
b.card.Elements = append(b.card.Elements, CardListItem{
|
||||
Text: desc, BtnText: btnText, BtnType: btnType, BtnValue: btnValue,
|
||||
})
|
||||
return b
|
||||
}
|
||||
|
||||
// ListItemBtnExtra is like ListItemBtn but with extra callback data.
|
||||
func (b *CardBuilder) ListItemBtnExtra(desc, btnText, btnType, btnValue string, extra map[string]string) *CardBuilder {
|
||||
b.card.Elements = append(b.card.Elements, CardListItem{
|
||||
Text: desc, BtnText: btnText, BtnType: btnType, BtnValue: btnValue, Extra: extra,
|
||||
})
|
||||
return b
|
||||
}
|
||||
|
||||
// Select appends a dropdown selector element.
|
||||
func (b *CardBuilder) Select(placeholder string, options []CardSelectOption, initValue string) *CardBuilder {
|
||||
if len(options) > 0 {
|
||||
b.card.Elements = append(b.card.Elements, CardSelect{
|
||||
Placeholder: placeholder, Options: options, InitValue: initValue,
|
||||
})
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Note appends a footnote element.
|
||||
func (b *CardBuilder) Note(text string) *CardBuilder {
|
||||
if text != "" {
|
||||
b.card.Elements = append(b.card.Elements, CardNote{Text: text})
|
||||
}
|
||||
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
|
||||
return &c
|
||||
}
|
||||
|
||||
// --- Text Fallback ---
|
||||
|
||||
// RenderText converts the card to a plain-text representation for platforms
|
||||
// that do not support rich cards.
|
||||
func (c *Card) RenderText() string {
|
||||
var sb strings.Builder
|
||||
|
||||
if c.Header != nil && c.Header.Title != "" {
|
||||
sb.WriteString("**")
|
||||
sb.WriteString(c.Header.Title)
|
||||
sb.WriteString("**\n\n")
|
||||
}
|
||||
|
||||
for _, elem := range c.Elements {
|
||||
switch e := elem.(type) {
|
||||
case CardMarkdown:
|
||||
sb.WriteString(e.Content)
|
||||
sb.WriteString("\n\n")
|
||||
case CardDivider:
|
||||
sb.WriteString("---\n\n")
|
||||
case CardActions:
|
||||
// Render buttons as a hint line
|
||||
for i, btn := range e.Buttons {
|
||||
if i > 0 {
|
||||
sb.WriteString(" ")
|
||||
}
|
||||
sb.WriteString("[")
|
||||
sb.WriteString(btn.Text)
|
||||
sb.WriteString("]")
|
||||
}
|
||||
sb.WriteString("\n\n")
|
||||
case CardListItem:
|
||||
sb.WriteString(e.Text)
|
||||
sb.WriteString(" [")
|
||||
sb.WriteString(e.BtnText)
|
||||
sb.WriteString("]\n")
|
||||
case CardSelect:
|
||||
sb.WriteString(e.Placeholder)
|
||||
sb.WriteString(": ")
|
||||
for i, opt := range e.Options {
|
||||
if i > 0 {
|
||||
sb.WriteString(" | ")
|
||||
}
|
||||
sb.WriteString(opt.Text)
|
||||
}
|
||||
sb.WriteString("\n\n")
|
||||
case CardNote:
|
||||
sb.WriteString(e.Text)
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
return strings.TrimRight(sb.String(), "\n")
|
||||
}
|
||||
|
||||
// HasButtons returns true if the card contains any interactive elements.
|
||||
func (c *Card) HasButtons() bool {
|
||||
for _, elem := range c.Elements {
|
||||
switch elem.(type) {
|
||||
case CardActions, CardListItem, CardSelect:
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// CollectButtons extracts all buttons from the card as a 2D slice
|
||||
// (one inner slice per CardActions element), suitable for InlineButtonSender.
|
||||
// CardListItem elements are collected as single-button rows.
|
||||
func (c *Card) CollectButtons() [][]ButtonOption {
|
||||
var rows [][]ButtonOption
|
||||
for _, elem := range c.Elements {
|
||||
switch e := elem.(type) {
|
||||
case CardActions:
|
||||
var row []ButtonOption
|
||||
for _, btn := range e.Buttons {
|
||||
row = append(row, ButtonOption{Text: btn.Text, Data: btn.Value})
|
||||
}
|
||||
if len(row) > 0 {
|
||||
rows = append(rows, row)
|
||||
}
|
||||
case CardListItem:
|
||||
rows = append(rows, []ButtonOption{{Text: e.BtnText, Data: e.BtnValue}})
|
||||
}
|
||||
}
|
||||
return rows
|
||||
}
|
||||
Reference in New Issue
Block a user