Skip to content

Commit d5b2d94

Browse files
authored
Simplify Feishu result cards (#7)
* fix: stabilize forwarded message timestamps * fix lint * Simplify Feishu result cards * fix lint
1 parent 45d1503 commit d5b2d94

4 files changed

Lines changed: 334 additions & 62 deletions

File tree

.claude/settings.local.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
11
{
22
"permissions": {
33
"allow": [
4-
"Bash(codex exec:*)"
4+
"Bash(codex exec:*)",
5+
"Bash(defuddle parse:*)",
6+
"Bash(curl -s \"https://raw.githubusercontent.com/hetaoBackend/agentara/main/user-home/.claude/skills/daily-hunt/SKILL.md\")",
7+
"Bash(curl:*)",
8+
"Bash(agent-browser --version)",
9+
"Bash(agent-browser open:*)",
10+
"Bash(agent-browser snapshot:*)",
11+
"Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); c=d[''''current_condition''''][0]; print\\(c[''''weatherDesc''''][0][''''value''''], c[''''temp_C'''']+''''°C'''', c[''''humidity'''']+''''%''''\\)\")",
12+
"Bash(grep -v \"^$\\\\|^\\\\s*$\")",
13+
"WebFetch(domain:wttr.in)",
14+
"WebFetch(domain:richerculture.cn)",
15+
"Skill(agent-browser)"
516
]
617
}
718
}

channels/feishu_channel.py

Lines changed: 189 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import threading
1818
from datetime import datetime, timedelta, timezone
1919
from pathlib import Path
20-
from typing import TYPE_CHECKING, Optional
20+
from typing import TYPE_CHECKING, Any, Optional
2121

2222
from taskboard_bus import Channel, MessageBus, OutboundMessage, OutboundMessageType
2323

@@ -210,6 +210,13 @@ def send(self, msg: OutboundMessage) -> None:
210210
]
211211
content = error_text
212212

213+
card = self._build_notification_card(
214+
task_id=task_id,
215+
task=task,
216+
is_completed=is_completed,
217+
body_text=content,
218+
)
219+
213220
# Try to reply in thread if we have an origin message
214221
with self._origin_lock:
215222
origin = self._task_origin.get(task_id)
@@ -220,13 +227,13 @@ def send(self, msg: OutboundMessage) -> None:
220227
# Add emoji reaction to the message that triggered the task (or resume)
221228
emoji = "DONE" if is_completed else "Cry"
222229
self._add_reaction(reaction_msg_id, emoji)
223-
sent_id = self._reply_message(root_msg_id, content)
230+
sent_id = self._reply_message(root_msg_id, content, card=card)
224231

225232
# Fallback: send to default chat if no origin or reply failed
226233
if not sent_id:
227234
chat_id = self.db.get_setting("feishu_default_chat_id")
228235
if chat_id:
229-
sent_id = self._send_message(chat_id, content)
236+
sent_id = self._send_message(chat_id, content, card=card, fallback_content=content)
230237

231238
if sent_id:
232239
print(f"[Feishu] Notification sent successfully, message_id: {sent_id}")
@@ -246,95 +253,216 @@ def _on_outbound(self, msg: OutboundMessage) -> None:
246253

247254
# ── outbound: low-level send ──────────────────────────────────
248255

249-
def _send_message(self, chat_id: str, content: str) -> Optional[str]:
250-
"""Send a markdown card to chat_id. Returns the sent message_id or None."""
256+
def _send_message(
257+
self,
258+
chat_id: str,
259+
content: str,
260+
card: Optional[dict[str, Any]] = None,
261+
fallback_content: Optional[str] = None,
262+
) -> Optional[str]:
263+
"""Send a card to chat_id. Falls back to the legacy markdown card on failure."""
251264
print(f"[Feishu] _send_message called, chat_id: {chat_id}, content length: {len(content)}")
252265
if not self._client:
253266
print("[Feishu] Client not initialized in _send_message")
254267
return None
255268
try:
256269
receive_id_type = "chat_id" if chat_id.startswith("oc_") else "open_id"
257270
print(f"[Feishu] receive_id_type: {receive_id_type}")
258-
card = {
259-
"config": {"wide_screen_mode": True},
260-
"elements": [{"tag": "markdown", "content": content}],
261-
}
262-
print("[Feishu] Building CreateMessageRequest...")
263-
request = (
264-
CreateMessageRequest.builder()
265-
.receive_id_type(receive_id_type)
266-
.request_body(
267-
CreateMessageRequestBody.builder()
268-
.receive_id(chat_id)
269-
.msg_type("interactive")
270-
.content(json.dumps(card, ensure_ascii=False))
271-
.build()
272-
)
273-
.build()
271+
card_payload = card or self._build_legacy_markdown_card(content)
272+
message_id = self._create_message(
273+
receive_id_type=receive_id_type,
274+
chat_id=chat_id,
275+
card=card_payload,
274276
)
275-
print("[Feishu] Calling im.v1.message.create()...")
276-
response = self._client.im.v1.message.create(request)
277-
print(
278-
f"[Feishu] Response received: success={response.success()}, code={response.code}, msg={response.msg}"
279-
)
280-
if response.success():
281-
message_id = response.data.message_id
282-
print(f"[Feishu] Message sent successfully, message_id: {message_id}")
277+
if message_id:
283278
return message_id
284-
else:
285-
print(f"[Feishu] Send failed: {response.code} {response.msg}")
286-
return None
279+
280+
if card is not None:
281+
legacy_content = fallback_content or content
282+
print("[Feishu] Structured card send failed, retrying with legacy markdown card")
283+
return self._create_message(
284+
receive_id_type=receive_id_type,
285+
chat_id=chat_id,
286+
card=self._build_legacy_markdown_card(legacy_content),
287+
)
288+
return None
287289
except Exception as e:
288290
print(f"[Feishu] Error sending message: {e}")
289291
import traceback
290292

291293
traceback.print_exc()
292294
return None
293295

294-
def _reply_message(self, parent_message_id: str, content: str) -> Optional[str]:
295-
"""Reply to a specific message (thread-style). Returns the sent message_id or None."""
296+
def _reply_message(
297+
self,
298+
parent_message_id: str,
299+
content: str,
300+
card: Optional[dict[str, Any]] = None,
301+
) -> Optional[str]:
302+
"""Reply to a specific message (thread-style). Falls back to the legacy markdown card."""
296303
print(
297304
f"[Feishu] _reply_message called, parent_message_id: {parent_message_id}, content length: {len(content)}"
298305
)
299306
if not self._client:
300307
print("[Feishu] Client not initialized in _reply_message")
301308
return None
302309
try:
303-
card = {
304-
"config": {"wide_screen_mode": True},
305-
"elements": [{"tag": "markdown", "content": content}],
306-
}
307-
request = (
308-
ReplyMessageRequest.builder()
309-
.message_id(parent_message_id)
310-
.request_body(
311-
ReplyMessageRequestBody.builder()
312-
.msg_type("interactive")
313-
.content(json.dumps(card, ensure_ascii=False))
314-
.reply_in_thread(True)
315-
.build()
316-
)
317-
.build()
318-
)
319-
print("[Feishu] Calling im.v1.message.reply()...")
320-
response = self._client.im.v1.message.reply(request)
321-
print(
322-
f"[Feishu] Reply response: success={response.success()}, code={response.code}, msg={response.msg}"
323-
)
324-
if response.success():
325-
message_id = response.data.message_id
326-
print(f"[Feishu] Reply sent successfully, message_id: {message_id}")
310+
reply_card = card or self._build_legacy_markdown_card(content)
311+
message_id = self._create_reply(parent_message_id=parent_message_id, card=reply_card)
312+
if message_id:
327313
return message_id
328-
else:
329-
print(f"[Feishu] Reply failed: {response.code} {response.msg}")
330-
return None
314+
315+
if card is not None:
316+
print("[Feishu] Structured card reply failed, retrying with legacy markdown card")
317+
return self._create_reply(
318+
parent_message_id=parent_message_id,
319+
card=self._build_legacy_markdown_card(content),
320+
)
321+
return None
331322
except Exception as e:
332323
print(f"[Feishu] Error replying to message: {e}")
333324
import traceback
334325

335326
traceback.print_exc()
336327
return None
337328

329+
def _create_message(
330+
self, receive_id_type: str, chat_id: str, card: dict[str, Any]
331+
) -> Optional[str]:
332+
print("[Feishu] Building CreateMessageRequest...")
333+
request = (
334+
CreateMessageRequest.builder()
335+
.receive_id_type(receive_id_type)
336+
.request_body(
337+
CreateMessageRequestBody.builder()
338+
.receive_id(chat_id)
339+
.msg_type("interactive")
340+
.content(json.dumps(card, ensure_ascii=False))
341+
.build()
342+
)
343+
.build()
344+
)
345+
print("[Feishu] Calling im.v1.message.create()...")
346+
response = self._client.im.v1.message.create(request)
347+
print(
348+
f"[Feishu] Response received: success={response.success()}, code={response.code}, msg={response.msg}"
349+
)
350+
if response.success():
351+
message_id = response.data.message_id
352+
print(f"[Feishu] Message sent successfully, message_id: {message_id}")
353+
return message_id
354+
355+
print(f"[Feishu] Send failed: {response.code} {response.msg}")
356+
return None
357+
358+
def _create_reply(self, parent_message_id: str, card: dict[str, Any]) -> Optional[str]:
359+
request = (
360+
ReplyMessageRequest.builder()
361+
.message_id(parent_message_id)
362+
.request_body(
363+
ReplyMessageRequestBody.builder()
364+
.msg_type("interactive")
365+
.content(json.dumps(card, ensure_ascii=False))
366+
.reply_in_thread(True)
367+
.build()
368+
)
369+
.build()
370+
)
371+
print("[Feishu] Calling im.v1.message.reply()...")
372+
response = self._client.im.v1.message.reply(request)
373+
print(
374+
f"[Feishu] Reply response: success={response.success()}, code={response.code}, msg={response.msg}"
375+
)
376+
if response.success():
377+
message_id = response.data.message_id
378+
print(f"[Feishu] Reply sent successfully, message_id: {message_id}")
379+
return message_id
380+
381+
print(f"[Feishu] Reply failed: {response.code} {response.msg}")
382+
return None
383+
384+
def _build_notification_card(
385+
self,
386+
task_id: int,
387+
task: dict[str, Any],
388+
is_completed: bool,
389+
body_text: str,
390+
) -> dict[str, Any]:
391+
clean_body = (body_text or "").strip() or ("Done." if is_completed else "Unknown error")
392+
summary = self._truncate_text(clean_body.splitlines()[0], 120) if clean_body else ""
393+
elements = self._build_result_elements(body_text=clean_body)
394+
395+
if not is_completed:
396+
elements.append(
397+
{
398+
"tag": "markdown",
399+
"content": f"`/status {task_id}` for full details",
400+
}
401+
)
402+
403+
return {
404+
"schema": "2.0",
405+
"config": {
406+
"wide_screen_mode": True,
407+
"enable_forward": True,
408+
"width_mode": "fill",
409+
"summary": {"content": summary},
410+
},
411+
"body": {
412+
"elements": elements,
413+
},
414+
}
415+
416+
def _build_result_elements(self, body_text: str) -> list[dict[str, Any]]:
417+
clean_body = (body_text or "").strip() or "Done."
418+
if len(clean_body) <= 1200:
419+
return [
420+
{
421+
"tag": "markdown",
422+
"content": clean_body,
423+
}
424+
]
425+
426+
preview = self._truncate_text(clean_body, 500)
427+
full_text = self._truncate_text(clean_body, 8000)
428+
return [
429+
{
430+
"tag": "markdown",
431+
"content": preview,
432+
},
433+
{
434+
"tag": "collapsible_panel",
435+
"expanded": False,
436+
"header": {
437+
"title": {
438+
"tag": "plain_text",
439+
"content": "展开查看完整结果",
440+
}
441+
},
442+
"elements": [
443+
{
444+
"tag": "markdown",
445+
"content": full_text,
446+
}
447+
],
448+
},
449+
]
450+
451+
def _build_legacy_markdown_card(self, content: str) -> dict[str, Any]:
452+
return {
453+
"config": {"wide_screen_mode": True},
454+
"elements": [{"tag": "markdown", "content": content}],
455+
}
456+
457+
def _truncate_text(self, text: str, limit: int) -> str:
458+
normalized = text.replace("\r\n", "\n").strip()
459+
if len(normalized) <= limit:
460+
return normalized
461+
return normalized[:limit].rstrip() + "\n…(truncated)"
462+
463+
def _escape_feishu_markdown(self, text: str) -> str:
464+
return text.replace("\\", "\\\\")
465+
338466
def _add_reaction(self, message_id: str, emoji_type: str = "THUMBSUP"):
339467
"""Add an emoji reaction in a background thread (non-blocking)."""
340468
if not self._client or not FEISHU_AVAILABLE:

docs/todo.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## In Progress
44

5+
- [ ] **修复 Feishu 默认通知接收人 `open_id cross app`** — 排查默认通知 fallback 使用 `open_id` 发送时的跨应用问题,并补充更安全的接收人解析与提示文案
6+
- [x] **优化 Feishu 结果展示** — 参考 `agentara` 的消息渲染实现,梳理当前 Feishu 输出格式并改进可读性与结构化展示
7+
- ✅ 已完成:`channels/feishu_channel.py` 改为生成更简洁的结构化 Feishu 卡片,成功时直接展示最终结果,长文本使用折叠面板承载完整内容
8+
- ✅ 已完成:发送/回复链路增加 legacy markdown 卡片回退,避免新卡片不兼容时通知直接失败
9+
- ✅ 验证:`uv run pytest tests/test_feishu_message_rendering.py tests/test_feishu_forwarded_messages.py -q` 通过,`19 passed`
510
- [x] **修复转发消息时间格式测试失败** — 统一 Feishu / Telegram 转发时间按北京时间 `UTC+8` 格式化
611
- ✅ 已修复:`channels/feishu_channel.py``channels/telegram_channel.py` 不再依赖进程本地时区
712
- ✅ 验证:相关 3 个失败用例通过;`tests/test_feishu_forwarded_messages.py``tests/test_telegram_forwarded_messages.py` 全部通过

0 commit comments

Comments
 (0)