feishu-card (schema 2.0)
Regras Fundamentais
- Todos os cartões usam schema 2.0, elementos dentro de
body.elements[], não no nível superiorelements[] - Enviar cartões deve usar
--as bot,--msg-type interactive,--contentpassando uma string JSON - Marcar (@) pessoas dentro do cartão use
<at id="ou_xxx">Nome</at>(não<at user_id>) - Botões JSON 2.0 não possuem mais container
action, botões vão diretamente embody.elements, interação usando arraybehaviors - Primeiro envie um cartão "Pensando..." para obter o
message_id, depois que Claude responder, faça PATCH para atualizar no lugar, evitando flood - Tag
notenão é mais suportada no schema 2.0 (erro 200861), usediv + lark_mdpara notas (<font color=grey>Texto da nota</font>) - Construir JSON do cartão deve usar Python heredoc ou escrever em arquivo, proibido usar
$(python3 -c "...")— conteúdo com crases ou caracteres especiais causará erro de sintaxe no bash
Estrutura completa do nível superior
{
"schema": "2.0",
"config": {
"streaming_mode": false,
"enable_forward": true,
"update_multi": true,
"width_mode": "default"
},
"card_link": {
"url": "https://...",
"pc_url": "https://...",
"ios_url": "https://...",
"android_url": "https://..."
},
"header": { "...": "..." },
"body": {
"direction": "vertical",
"padding": "0px",
"elements": []
}
}
Campos config
| Campo | Tipo | Padrão | Descrição |
|---|---|---|---|
streaming_mode | bool | false | Estado de streaming (placeholder de pensamento) |
enable_forward | bool | true | Permitir encaminhar cartão |
update_multi | bool | false | Modo de cartão compartilhado JSON 2.0 (definir true para atualização síncrona de múltiplos) |
width_mode | string | default (600px) | compact (400px) / fill (largura total responsiva) / default |
Campos Header
{
"header": {
"title": {"tag": "plain_text", "content": "Título"},
"subtitle": {"tag": "plain_text", "content": "Subtítulo (opcional)"},
"template": "blue",
"icon": {
"tag": "standard_icon",
"token": "robot-outlined",
"color": "blue"
},
"text_tag_list": [
{"tag": "text_tag", "text": {"tag": "plain_text", "content": "Novo"}, "color": "blue"}
]
}
}
Cores template: blue / wathet / turquoise / green / yellow / orange / red / carmine / violet / purple / indigo / grey
Referência rápida de cores Feishu
Todos os campos que suportam enumeração de cores (background_color, text_color, icon color, etc.) usam o seguinte formato, não use apenas o nome básico da cor (como "blue" sozinho pode causar erro em alguns componentes), precisa do sufixo de nível:
| Tom | Claro (recomendado para fundo) | Médio | Escuro (recomendado para texto) |
|---|---|---|---|
| Azul blue | blue-50 blue-100 blue-200 | blue-300~blue-500 | blue-600~blue-900 |
| Água wathet | wathet-50 wathet-100 wathet-200 | wathet-300~wathet-500 | wathet-600~wathet-900 |
| Turquesa turquoise | turquoise-50 turquoise-100 | turquoise-300~turquoise-500 | turquoise-600~turquoise-900 |
| Verde green | green-50 green-100 green-200 | green-300~green-500 | green-600~green-900 |
| Verde-limão lime | lime-50 lime-100 | lime-300~lime-500 | lime-600~lime-900 |
| Amarelo yellow | yellow-50 yellow-100 | yellow-300~yellow-500 | yellow-600~yellow-900 |
| Laranja orange | orange-50 orange-100 | orange-300~orange-500 | orange-600~orange-900 |
| Vermelho red | red-50 red-100 | red-300~red-500 | red-600~red-900 |
| Rosa carmine | carmine-50 carmine-100 | carmine-300~carmine-500 | carmine-600~carmine-900 |
| Roxo purple | purple-50 purple-100 | purple-300~purple-500 | purple-600~purple-900 |
| Azul-violeta violet | violet-50 violet-100 | violet-300~violet-500 | violet-600~violet-900 |
| Anil indigo | indigo-50 indigo-100 | indigo-300~indigo-500 | indigo-600~indigo-900 |
| Cinza grey | grey-00 grey-50 grey-100 | grey-300~grey-500 | grey-600~grey-1000 |
| Branco | white bg-white | — | — |
Fundo claro recomendado (para collapsible_panel, column, etc.):
blue-100 wathet-100 green-100 yellow-100
orange-100 red-100 purple-100 carmine-100
turquoise-100 violet-100 indigo-100 lime-100
Tabela de componentes
| tag | Tipo | Descrição |
|---|---|---|
div | Conteúdo | Bloco de texto (plain_text / lark_md), suporta ícone prefixo |
markdown | Conteúdo | Texto Markdown puro (não suporta @, use div+lark_md para @) |
img | Conteúdo | Imagem |
table | Conteúdo | Tabela de dados (máx. 5 por cartão, não aninhável) |
hr | Layout | Linha divisória |
note | Layout | ❌ Obsoleto no schema 2.0, usar div + lark_md + <font color=grey> |
column_set | Container | Layout de múltiplas colunas |
collapsible_panel | Container | Painel recolhível |
button | Interação | Botão (JSON 2.0 colocado diretamente em elements, sem container action) |
input | Interação | Caixa de entrada de texto (deve ser usada dentro de form) |
select_static | Interação | Seletor estático suspenso |
div (Bloco de texto)
{
"tag": "div",
"text": {
"tag": "lark_md",
"content": "**Negrito** _Itálico_ `Código`",
"text_size": "normal",
"text_color": "default",
"text_align": "left",
"lines": 3
},
"icon": {"tag": "standard_icon", "token": "robot-outlined", "color": "blue"}
}
text.tag: plain_text (texto simples) ou lark_md (Markdown + @)
text_size: heading-0 / heading / normal (padrão) / notation / x-small
text_align: left / center / right
img (Imagem)
{
"tag": "img",
"img_key": "img_xxx",
"alt": {"tag": "plain_text", "content": "Texto ao passar o mouse"},
"title": {"tag": "plain_text", "content": "Título da imagem"},
"scale_type": "crop_center",
"size": "medium",
"corner_radius": "4px",
"preview": true
}
scale_type: crop_center / crop_top / fit_horizontal
size: stretch (largura total) / large / medium / small / tiny / "300px 200px" (personalizado)
JSON 2.0 não suporta
stretch_without_padding, para efeito de largura total use margem negativa
column_set (Layout de múltiplas colunas)
{
"tag": "column_set",
"flex_mode": "bisect",
"horizontal_spacing": "medium",
"background_style": "default",
"columns": [
{
"tag": "column",
"width": "weighted",
"weight": 1,
"vertical_align": "top",
"elements": [{"tag": "div", "text": {"tag": "plain_text", "content": "Coluna esquerda"}}]
},
{
"tag": "column",
"width": "weighted",
"weight": 1,
"elements": [{"tag": "div", "text": {"tag": "plain_text", "content": "Coluna direita"}}]
}
]
}
flex_mode: none (comprimir proporcionalmente) / stretch (empilhar vertical em tela estreita) / flow (quebra automática) / bisect (duas colunas iguais) / trisect (três colunas iguais)
horizontal_spacing: small(4px) / medium(8px) / large(12px) / extra_large(16px)
column.width: auto / weighted (com weight 1-5) / "200px"
Máximo de 5 níveis de aninhamento, não colocar container form ou table dentro de coluna
collapsible_panel (Painel recolhível)
{
"tag": "collapsible_panel",
"expanded": false,
"background_color": "default",
"header": {
"title": {"tag": "plain_text", "content": "Clique para expandir"},
"background_color": "grey",
"icon": {"tag": "standard_icon", "token": "arrow-down-outlined"},
"icon_expanded_angle": -180
},
"elements": [
{"tag": "div", "text": {"tag": "plain_text", "content": "Conteúdo recolhido"}}
]
}
table (Tabela de dados)
{
"tag": "table",
"page_size": 5,
"row_height": "low",
"freeze_first_column": false,
"header_style": {
"text_align": "left",
"text_size": "normal",
"background_style": "grey",
"bold": true
},
"columns": [
{"name": "name", "display_name": "Nome", "data_type": "text", "width": "auto"},
{"name": "status", "display_name": "Status", "data_type": "options", "width": "120px"},
{"name": "count", "display_name": "Quantidade", "data_type": "number",
"format": {"precision": 0, "separator": true}},
{"name": "ts", "display_name": "Data", "data_type": "date",
"format": "YYYY/MM/DD HH:mm"}
],
"rows": [
{"name": "João", "status": [{"name": "Em andamento", "color": "blue"}], "count": 42, "ts": 1700000000000}
]
}
data_type: text / lark_md / markdown / options / number / persons / date
row_height: low / middle / high / auto / "48px" (32-124px)
Máximo de 5 tabelas por cartão, não aninhável
button (JSON 2.0)
{
"tag": "button",
"text": {"tag": "plain_text", "content": "Confirmar"},
"type": "primary",
"size": "medium",
"width": "default",
"disabled": false,
"behaviors": [
{
"type": "callback",
"value": {"action": "confirm", "data": "payload"}
}
],
"confirm": {
"title": {"tag": "plain_text", "content": "Confirmação secundária"},
"text": {"tag": "plain_text", "content": "Confirmar execução desta ação?"}
}
}
type: default (cinza) / primary (azul) / danger (vermelho) / text / primary_text / danger_text / primary_filled / danger_filled / laser
size: tiny / small / medium / large
behaviors[].type: callback (disparar evento) / open_url (link)
Mudança JSON 2.0: Não há mais container
action, botão colocado diretamente embody.elementsoucolumn.elements; interação usabehaviorsem vez do antigovalue
input (Entrada de texto)
{
"tag": "input",
"name": "field_name",
"placeholder": {"tag": "plain_text", "content": "Digite..."},
"default_value": "",
"input_type": "text",
"max_length": 200,
"required": false,
"disabled": false,
"label": {"tag": "plain_text", "content": "Rótulo"},
"label_position": "top",
"width": "fill"
}
input_type: text / multiline_text / password
O envio do usuário dispara evento card.action.trigger, contendo input_value
Sintaxe de @ (Menção)
| Tipo de mensagem | Sintaxe @pessoa | @todos |
|---|---|---|
Cartão interactive lark_md | <at id="ou_xxx">Nome</at> | <at id="all">Todos</at> |
Texto text / post | <at user_id="ou_xxx">Nome</at> | <at user_id="all"></at> |
Estrutura de elemento validada para @mention dentro do cartão (div + lark_md, não usar tag markdown nua):
{
"tag": "div",
"text": {
"tag": "lark_md",
"content": "<at id=\"ou_xxx\">Nome</at> Mensagem de conteúdo"
}
}
Referência rápida de sintaxe Markdown (dentro de lark_md / tag markdown)
Estilos de texto
**Negrito** *Itálico* ~~Tachado~~ `Código inline`
Texto colorido
<font color=red>Vermelho</font>
<font color=green>Verde</font>
<font color=grey>Cinza</font>
<font color=blue>Azul</font>
<font color=orange>Laranja</font>
<font color=wathet>Água</font>
<font color=purple>Roxo</font>
Cores completas: red / green / grey / blue / orange / wathet / purple / yellow / turquoise / indigo / violet / lime
Tags / Emblemas numéricos
<text_tag color='red'>Tag</text_tag>
<number_tag>99</number_tag>
Horário localizado
<local_datetime millisecond='1700000000000' format_type='date_num'></local_datetime>
format_type: date_num (2023/11/15) / date_full (15 de novembro de 2023) / datetime (com horas, minutos, segundos)
Títulos / Listas / Citações
# H1 ## H2 ### H3 #### H4 ##### H5 ###### H6
- Não ordenada 1. Ordenada > Citação
Bloco de código
```python
print("olá")
```
Imagem (dentro do Markdown)

Tabela (máximo 5 linhas de dados)
| Col1 | Col2 |
|-----|------|
| A | B |
Links
[Texto](https://url) [Telefone](tel://13800138000)
Linha divisória / Quebra de linha
--- ou <hr>
<br/> ou duas quebras de linha (parágrafo novo)
Emoji
⚠️ Regras estritas
- Use apenas keys da lista oficial, keys não listadas (como
:WAITING:,:PROCESSING:) serão renderizadas como texto simples, proibido. - Maiúsculas/minúsculas da key devem corresponder exatamente à oficial (ex.:
FirenãoFIRE,TrophynãoTROPHY,ShrugnãoSHRUG). - Dentro de
<text_tag>não renderiza:KEY:, deve usar Unicode emoji (ex.:✅). :KEY:funciona apenas em área de texto comumlark_md.
Comuns para notificações de release
:DONE: :OK: :THUMBSUP: :APPLAUSE:
:PARTY: :FIREWORKS: :MUSCLE: :THANKS:
:Trophy: :Fire: :Hundred: :CheckMark:
Trabalho & Status
:LGTM: :OnIt: :OneSecond: :Get:
:ERROR: :CrossMark: :Pin: :Alarm:
:Loudspeaker: :OKR: :Yes: :No:
:HEADSET: :Typing: :MeMeMe: :Sigh:
:Lemon: :AWESOMEN: :VRHeadset: :YouAreTheBest:
:SALUTE: :HIGHFIVE: :FISTBUMP: :FINGERHEART:
:WAVE: :ThumbsDown:
Emoticons - Positivos
:SMILE: :LAUGH: :LOL: :LOVE:
:WINK: :PROUD: :WITTY: :JOYFUL:
:WOW: :BLUSH: :HUG: :BeamingFace:
:Delighted: :ThanksFace: :SaluteFace: :PRAISE:
:CLAP: :JIAYI: :FACEPALM: :YEAH:
Emoticons - Negativos / Engraçados
:CRY: :SOB: :ANGRY: :SHOCKED:
:Shrug: :THINKING: :DIZZY: :SLEEP:
:YAWN: :SICK: :SWEAT: :EMBARRASSED:
:SKULL: :RAINBOWPUKE: :PUKE: :ClownFace:
:ColdSweat: :SMUG: :SILENT: :TERROR:
:PETRIFIED: :BETRAYED: :WRONGED:
Celebração & Festas
:PARTY: :FIREWORKS: :REDPACKET: :FORTUNE:
:FIRECRACKER: :XmasTree: :XmasHat: :Snowman:
:Pumpkin: :Mooncake: :MoonRabbit: :Partying:
:StickyRiceBalls: :JubilantRabbit: :LUCK:
:FullMoonFace: :GoGoGo: :HappyDragon:
Objetos & Comida
:ROSE: :HEART: :BEER: :CAKE:
:GIFT: :BOMB: :Fire: :Trophy:
:Coffee: :BubbleTea: :Drumstick: :Pepper:
:CUCUMBER: :CANDIEDHAWS: :Soccer: :Basketball:
:Music: :CLEAVER: :EatingFood: :LIPS:
Indicadores de status (não usar com frequência, apenas em contexto de status)
:GeneralDoNotDisturb: :GeneralInMeetingBusy:
:StatusReading: :GeneralWorkFromHome:
:GeneralBusinessTrip: :StatusInFlight:
:GeneralSun: :GeneralMoonRest:
:StatusFlashOfInspiration: :Status_PrivateMessage:
:GeneralTravellingCar: :StatusBus:
Unicode Emoji (usar em títulos, dentro de text_tag, subtítulos header)
🎉 ✅ ❌ ⚠️ 🚀 📦 💬 ⏳ 🔔 📝 🛠️ 🔍 👋 🎯 💡 🌟
👥 📅 🤖 🏆 🔥 💯 📣 🎁 ☕ 🍺
Estilo recomendado: Títulos com Unicode emoji para melhorar visual, status/feedback com sintaxe específica Feishu
:DONE::LGTM:
Enviar / Responder / Atualizar
Enviar para grupo (nova mensagem)
CARD=$(python3 -c "
import json
card = {
'schema': '2.0',
'header': {'title': {'tag': 'plain_text', 'content': 'Título'}, 'template': 'blue'},
'body': {'elements': [{'tag': 'markdown', 'content': 'Conteúdo'}]}
}
print(json.dumps(card))
")
lark-cli im +messages-send \
--chat-id oc_xxx \
--msg-type interactive \
--content "$CARD" \
--as bot
Responder a mensagem específica (obter message_id para atualização posterior)
MSG_ID=$(lark-cli im +messages-reply \
--message-id om_xxx \
--msg-type interactive \
--content "$CARD" \
--as bot \
--jq '.data.message_id')
Responder em thread (não aparece no fluxo principal do chat)
lark-cli im +messages-reply \
--message-id om_xxx \
--msg-type interactive \
--content "$CARD" \
--reply-in-thread \
--as bot
Evitar envio duplicado (mesma key só envia uma vez em 1 hora)
lark-cli im +messages-send \
--chat-id oc_xxx \
--msg-type interactive \
--content "$CARD" \
--idempotency-key "deploy-654.0-notify" \
--as bot
Atualizar cartão no lugar (PATCH)
PAYLOAD=$(jq -n --argjson card "$CARD" '{"msg_type":"interactive","content":($card|tojson)}')
lark-cli api PATCH "/open-apis/im/v1/messages/${MSG_ID}" \
--data "$PAYLOAD" \
--as bot
Modelos de cartão
Pensando (estado streaming, placeholder inicial)
make_thinking_card() {
local name="$1" question="$2"
jq -n --arg name "$name" --arg q "$question"
...[truncado para tradução]