hermes-feishu 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,153 @@
1
+ Metadata-Version: 2.4
2
+ Name: hermes-feishu
3
+ Version: 0.2.0
4
+ Summary: Enhanced Feishu/Lark messaging for Hermes Agent with card messages and table rendering
5
+ Author: hermes-feishu contributors
6
+ License: MIT
7
+ Keywords: hermes,feishu,lark,agent,plugin,card,table
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Communications :: Chat
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: lark-oapi<2,>=1.5.3
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest>=7.0; extra == "dev"
21
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
22
+
23
+ # hermes-feishu
24
+
25
+ 增强 Hermes Agent 飞书消息通道,支持卡片消息和表格渲染。
26
+
27
+ ## 问题背景
28
+
29
+ Hermes Agent 内置的飞书通道使用 `post` 消息类型 + `tag: "md"` 发送 Markdown 内容。但飞书的 Markdown 组件仅支持语法子集,**不支持表格语法** (`| col | col |`)。这导致 LLM 生成的表格在飞书中无法正常渲染。
30
+
31
+ ## 解决方案
32
+
33
+ 本插件通过以下方式解决:
34
+
35
+ 1. **`send_feishu_card` 工具** — 发送包含表格的飞书卡片消息。自动检测 Markdown 中的表格语法,转换为飞书卡片 Table 组件。
36
+ 2. **`send_feishu_table` 工具** — 直接发送结构化表格数据(headers + rows)。
37
+ 3. **`pre_llm_call` 钩子** — 当平台为飞书时,自动注入格式化指令,引导 LLM 使用卡片工具发送表格。
38
+
39
+ ## 快速安装
40
+
41
+ ### 1. 环境准备
42
+
43
+ - Python 3.10+
44
+ - Hermes Agent 已安装并配置飞书平台
45
+ - 飞书开放平台应用(需要 App ID 和 App Secret)
46
+
47
+ ### 2. 安装插件
48
+
49
+ ```bash
50
+ # 从源码安装(开发模式)
51
+ cd hermes-feishu
52
+ pip install -e .
53
+
54
+ # 或安装到 Hermes 插件目录
55
+ cp plugin.yaml ~/.hermes/plugins/hermes-feishu/
56
+ cp -r src/hermes_feishu/ ~/.hermes/plugins/hermes-feishu/
57
+ ```
58
+
59
+ ### 3. 配置环境变量
60
+
61
+ ```bash
62
+ export FEISHU_APP_ID="cli_xxxxxxxxxxxx"
63
+ export FEISHU_APP_SECRET="xxxxxxxxxxxxxxxxxxxxxxxx"
64
+ ```
65
+
66
+ 或在 Hermes 的 `.env` 文件中添加:
67
+
68
+ ```env
69
+ FEISHU_APP_ID=cli_xxxxxxxxxxxx
70
+ FEISHU_APP_SECRET=xxxxxxxxxxxxxxxxxxxxxxxx
71
+ ```
72
+
73
+ ### 4. 重启 Hermes
74
+
75
+ ```bash
76
+ hermes
77
+ ```
78
+
79
+ 启动后使用 `/plugins` 命令确认插件已加载。
80
+
81
+ ## 使用方式
82
+
83
+ 插件加载后,LLM 在飞书平台上会自动收到格式化指令。当需要展示表格时,LLM 会自动调用 `send_feishu_card` 或 `send_feishu_table` 工具。
84
+
85
+ ### 示例:Markdown 表格
86
+
87
+ LLM 生成包含表格的内容时会自动调用:
88
+
89
+ ```
90
+ 用户: 帮我对比一下这两个方案
91
+
92
+ LLM 调用 send_feishu_card:
93
+ content: |
94
+ | 对比项 | 方案A | 方案B |
95
+ | --- | --- | --- |
96
+ | 成本 | ¥1000 | ¥2000 |
97
+ | 周期 | 2周 | 1周 |
98
+ | 风险 | 低 | 中 |
99
+ ```
100
+
101
+ 飞书中会渲染为带颜色标题的卡片消息,表格使用飞书 Table 组件。
102
+
103
+ ### 示例:结构化表格
104
+
105
+ LLM 可以直接使用结构化数据:
106
+
107
+ ```
108
+ LLM 调用 send_feishu_table:
109
+ headers: ["指标", "当前值", "目标值"]
110
+ rows: [
111
+ ["日活用户", "10,000", "15,000"],
112
+ ["转化率", "3.2%", "5%"],
113
+ ["NPS", "42", "60"]
114
+ ]
115
+ ```
116
+
117
+ ## 插件架构
118
+
119
+ ```
120
+ src/hermes_feishu/
121
+ ├── __init__.py # 插件注册:工具 + 钩子
122
+ ├── schemas.py # 工具 Schema 定义
123
+ ├── tools.py # 工具处理器
124
+ ├── card_builder.py # 飞书卡片 JSON 构建
125
+ ├── table_parser.py # Markdown 表格解析
126
+ └── sender.py # 飞书 API 发送层
127
+ ```
128
+
129
+ ## 飞书应用权限
130
+
131
+ 插件需要以下飞书应用权限:
132
+
133
+ | 权限 | 权限标识 | 用途 |
134
+ | --- | --- | --- |
135
+ | 获取与发送单聊、群组消息 | `im:message` | 发送卡片消息 |
136
+ | 读取消息中的消息体内容 | `im:message:readonly` | 读取消息内容 |
137
+
138
+ ## 开发
139
+
140
+ ```bash
141
+ # 安装开发依赖
142
+ pip install -e ".[dev]"
143
+
144
+ # 运行测试
145
+ pytest tests/ -v
146
+
147
+ # 运行测试(带覆盖率)
148
+ pytest tests/ -v --cov=hermes_feishu
149
+ ```
150
+
151
+ ## 许可证
152
+
153
+ MIT License
@@ -0,0 +1,131 @@
1
+ # hermes-feishu
2
+
3
+ 增强 Hermes Agent 飞书消息通道,支持卡片消息和表格渲染。
4
+
5
+ ## 问题背景
6
+
7
+ Hermes Agent 内置的飞书通道使用 `post` 消息类型 + `tag: "md"` 发送 Markdown 内容。但飞书的 Markdown 组件仅支持语法子集,**不支持表格语法** (`| col | col |`)。这导致 LLM 生成的表格在飞书中无法正常渲染。
8
+
9
+ ## 解决方案
10
+
11
+ 本插件通过以下方式解决:
12
+
13
+ 1. **`send_feishu_card` 工具** — 发送包含表格的飞书卡片消息。自动检测 Markdown 中的表格语法,转换为飞书卡片 Table 组件。
14
+ 2. **`send_feishu_table` 工具** — 直接发送结构化表格数据(headers + rows)。
15
+ 3. **`pre_llm_call` 钩子** — 当平台为飞书时,自动注入格式化指令,引导 LLM 使用卡片工具发送表格。
16
+
17
+ ## 快速安装
18
+
19
+ ### 1. 环境准备
20
+
21
+ - Python 3.10+
22
+ - Hermes Agent 已安装并配置飞书平台
23
+ - 飞书开放平台应用(需要 App ID 和 App Secret)
24
+
25
+ ### 2. 安装插件
26
+
27
+ ```bash
28
+ # 从源码安装(开发模式)
29
+ cd hermes-feishu
30
+ pip install -e .
31
+
32
+ # 或安装到 Hermes 插件目录
33
+ cp plugin.yaml ~/.hermes/plugins/hermes-feishu/
34
+ cp -r src/hermes_feishu/ ~/.hermes/plugins/hermes-feishu/
35
+ ```
36
+
37
+ ### 3. 配置环境变量
38
+
39
+ ```bash
40
+ export FEISHU_APP_ID="cli_xxxxxxxxxxxx"
41
+ export FEISHU_APP_SECRET="xxxxxxxxxxxxxxxxxxxxxxxx"
42
+ ```
43
+
44
+ 或在 Hermes 的 `.env` 文件中添加:
45
+
46
+ ```env
47
+ FEISHU_APP_ID=cli_xxxxxxxxxxxx
48
+ FEISHU_APP_SECRET=xxxxxxxxxxxxxxxxxxxxxxxx
49
+ ```
50
+
51
+ ### 4. 重启 Hermes
52
+
53
+ ```bash
54
+ hermes
55
+ ```
56
+
57
+ 启动后使用 `/plugins` 命令确认插件已加载。
58
+
59
+ ## 使用方式
60
+
61
+ 插件加载后,LLM 在飞书平台上会自动收到格式化指令。当需要展示表格时,LLM 会自动调用 `send_feishu_card` 或 `send_feishu_table` 工具。
62
+
63
+ ### 示例:Markdown 表格
64
+
65
+ LLM 生成包含表格的内容时会自动调用:
66
+
67
+ ```
68
+ 用户: 帮我对比一下这两个方案
69
+
70
+ LLM 调用 send_feishu_card:
71
+ content: |
72
+ | 对比项 | 方案A | 方案B |
73
+ | --- | --- | --- |
74
+ | 成本 | ¥1000 | ¥2000 |
75
+ | 周期 | 2周 | 1周 |
76
+ | 风险 | 低 | 中 |
77
+ ```
78
+
79
+ 飞书中会渲染为带颜色标题的卡片消息,表格使用飞书 Table 组件。
80
+
81
+ ### 示例:结构化表格
82
+
83
+ LLM 可以直接使用结构化数据:
84
+
85
+ ```
86
+ LLM 调用 send_feishu_table:
87
+ headers: ["指标", "当前值", "目标值"]
88
+ rows: [
89
+ ["日活用户", "10,000", "15,000"],
90
+ ["转化率", "3.2%", "5%"],
91
+ ["NPS", "42", "60"]
92
+ ]
93
+ ```
94
+
95
+ ## 插件架构
96
+
97
+ ```
98
+ src/hermes_feishu/
99
+ ├── __init__.py # 插件注册:工具 + 钩子
100
+ ├── schemas.py # 工具 Schema 定义
101
+ ├── tools.py # 工具处理器
102
+ ├── card_builder.py # 飞书卡片 JSON 构建
103
+ ├── table_parser.py # Markdown 表格解析
104
+ └── sender.py # 飞书 API 发送层
105
+ ```
106
+
107
+ ## 飞书应用权限
108
+
109
+ 插件需要以下飞书应用权限:
110
+
111
+ | 权限 | 权限标识 | 用途 |
112
+ | --- | --- | --- |
113
+ | 获取与发送单聊、群组消息 | `im:message` | 发送卡片消息 |
114
+ | 读取消息中的消息体内容 | `im:message:readonly` | 读取消息内容 |
115
+
116
+ ## 开发
117
+
118
+ ```bash
119
+ # 安装开发依赖
120
+ pip install -e ".[dev]"
121
+
122
+ # 运行测试
123
+ pytest tests/ -v
124
+
125
+ # 运行测试(带覆盖率)
126
+ pytest tests/ -v --cov=hermes_feishu
127
+ ```
128
+
129
+ ## 许可证
130
+
131
+ MIT License
@@ -0,0 +1,44 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "hermes-feishu"
7
+ version = "0.2.0"
8
+ description = "Enhanced Feishu/Lark messaging for Hermes Agent with card messages and table rendering"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ {name = "hermes-feishu contributors"},
14
+ ]
15
+ keywords = ["hermes", "feishu", "lark", "agent", "plugin", "card", "table"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Topic :: Communications :: Chat",
25
+ ]
26
+ dependencies = [
27
+ "lark-oapi>=1.5.3,<2",
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ dev = [
32
+ "pytest>=7.0",
33
+ "pytest-cov>=4.0",
34
+ ]
35
+
36
+ [project.entry-points."hermes_agent.plugins"]
37
+ hermes-feishu = "hermes_feishu"
38
+
39
+ [tool.setuptools.packages.find]
40
+ where = ["src"]
41
+
42
+ [tool.pytest.ini_options]
43
+ testpaths = ["tests"]
44
+ python_files = ["test_*.py"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,85 @@
1
+ """Hermes Feishu Plugin - Enhanced Feishu messaging with card messages and table rendering.
2
+
3
+ This plugin enhances Hermes Agent's Feishu messaging capabilities by providing:
4
+ - send_feishu_card: Send rich card messages with table support
5
+ - send_feishu_table: Send structured tables as card messages
6
+ - pre_llm_call hook: Auto-inject formatting instructions for Feishu platform
7
+ """
8
+
9
+ from .schemas import SEND_FEISHU_CARD_SCHEMA, SEND_FEISHU_TABLE_SCHEMA
10
+ from .sender import _has_credentials
11
+ from .tools import send_feishu_card, send_feishu_table
12
+
13
+ __version__ = "0.2.0"
14
+
15
+ # Context injection for Feishu platform
16
+ _FEISHU_CONTEXT_INJECTION = (
17
+ "\n\n[System: Feishu Platform Formatting]\n"
18
+ "You are connected via Feishu (Lark). Feishu post messages do NOT support "
19
+ "Markdown table syntax. When your response contains tabular data, you MUST "
20
+ "use the `send_feishu_card` or `send_feishu_table` tool to render it properly.\n"
21
+ "- Use `send_feishu_card` for Markdown content that includes tables.\n"
22
+ "- Use `send_feishu_table` for structured data (headers + rows).\n"
23
+ "- Do NOT include Markdown tables in your regular text response.\n"
24
+ "- Other Markdown (bold, italic, lists, code blocks) works fine in normal messages.\n"
25
+ )
26
+
27
+
28
+ def register(ctx):
29
+ """Register plugin tools and hooks with Hermes Agent.
30
+
31
+ Args:
32
+ ctx: Plugin registration context provided by Hermes.
33
+ """
34
+ # Register tools with conditional availability
35
+ ctx.register_tool(
36
+ name="send_feishu_card",
37
+ schema=SEND_FEISHU_CARD_SCHEMA,
38
+ handler=send_feishu_card,
39
+ check_fn=_has_credentials,
40
+ )
41
+
42
+ ctx.register_tool(
43
+ name="send_feishu_table",
44
+ schema=SEND_FEISHU_TABLE_SCHEMA,
45
+ handler=send_feishu_table,
46
+ check_fn=_has_credentials,
47
+ )
48
+
49
+ # Register pre_llm_call hook for Feishu context injection
50
+ ctx.register_hook("pre_llm_call", _on_pre_llm_call)
51
+
52
+
53
+ def _on_pre_llm_call(
54
+ session_id: str = "",
55
+ user_message: str = "",
56
+ conversation_history=None,
57
+ is_first_turn: bool = False,
58
+ model: str = "",
59
+ platform: str = "",
60
+ **kwargs,
61
+ ):
62
+ """Inject Feishu formatting instructions when platform is Feishu.
63
+
64
+ Only injects on the first turn of a session to avoid repetition,
65
+ and only when the platform is 'feishu'.
66
+
67
+ Args:
68
+ session_id: Current session ID.
69
+ user_message: The user's message.
70
+ conversation_history: Conversation history.
71
+ is_first_turn: Whether this is the first turn.
72
+ model: Model name.
73
+ platform: Platform identifier (e.g., 'feishu').
74
+
75
+ Returns:
76
+ Context dict to inject, or None.
77
+ """
78
+ if not is_first_turn:
79
+ return None
80
+
81
+ # Normalize platform name (case-insensitive check)
82
+ if not platform or platform.lower() not in ("feishu", "lark"):
83
+ return None
84
+
85
+ return {"context": _FEISHU_CONTEXT_INJECTION}
@@ -0,0 +1,241 @@
1
+ """Feishu card JSON builder for Hermes Feishu plugin.
2
+
3
+ Builds Feishu interactive card JSON structures from parsed table data
4
+ and markdown content.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ from .table_parser import ParsedTable, TableColumn, TableCell, parse_table, split_table_and_text
13
+
14
+
15
+ def _build_table_columns(columns: List[TableColumn]) -> List[Dict[str, Any]]:
16
+ """Build Feishu Table column definitions.
17
+
18
+ Args:
19
+ columns: Parsed table column definitions.
20
+
21
+ Returns:
22
+ List of Feishu column spec dicts.
23
+ """
24
+ feishu_cols: List[Dict[str, Any]] = []
25
+ for col in columns:
26
+ spec: Dict[str, Any] = {
27
+ "field_type": col.field_type,
28
+ "name": col.name,
29
+ }
30
+ if col.width:
31
+ spec["width"] = col.width
32
+ feishu_cols.append(spec)
33
+ return feishu_cols
34
+
35
+
36
+ def _build_table_rows(
37
+ rows: List[List[TableCell]],
38
+ columns: List[TableColumn],
39
+ ) -> List[List[Dict[str, Any]]]:
40
+ """Build Feishu Table row data.
41
+
42
+ Args:
43
+ rows: Parsed table cell data.
44
+ columns: Column definitions (for type info).
45
+
46
+ Returns:
47
+ List of Feishu row dicts.
48
+ """
49
+ feishu_rows: List[List[Dict[str, Any]]] = []
50
+ for row in rows:
51
+ feishu_row: List[Dict[str, Any]] = []
52
+ for idx, cell in enumerate(row):
53
+ col_type = "text"
54
+ if idx < len(columns):
55
+ col_type = columns[idx].field_type
56
+
57
+ if col_type == "number":
58
+ # Try to parse as number for the value field
59
+ try:
60
+ cleaned = cell.text.replace(",", "").replace("%", "").strip()
61
+ num = float(cleaned)
62
+ if num == int(num):
63
+ num = int(num)
64
+ feishu_row.append({"text": cell.text, "value": num})
65
+ except (ValueError, OverflowError):
66
+ feishu_row.append({"text": cell.text})
67
+ else:
68
+ feishu_row.append({"text": cell.text})
69
+ feishu_rows.append(feishu_row)
70
+ return feishu_rows
71
+
72
+
73
+ def build_table_card(
74
+ table: ParsedTable,
75
+ title: str = "📊 数据表格",
76
+ template: str = "blue",
77
+ ) -> Dict[str, Any]:
78
+ """Build a Feishu interactive card containing a Table component.
79
+
80
+ Args:
81
+ table: A parsed table from table_parser.
82
+ title: Card header title.
83
+ template: Card header color template (blue, wathet, turquoise, green,
84
+ yellow, orange, red, carmine, violet, purple, indigo, grey).
85
+
86
+ Returns:
87
+ Complete Feishu card JSON dict.
88
+ """
89
+ columns = _build_table_columns(table.headers)
90
+ rows = _build_table_rows(table.rows, table.headers)
91
+
92
+ card: Dict[str, Any] = {
93
+ "config": {"wide_screen_mode": True},
94
+ "header": {
95
+ "title": {"content": title, "tag": "plain_text"},
96
+ "template": template,
97
+ },
98
+ "elements": [
99
+ {
100
+ "tag": "table",
101
+ "columns": columns,
102
+ "rows": rows,
103
+ }
104
+ ],
105
+ }
106
+
107
+ return card
108
+
109
+
110
+ def build_content_card(
111
+ content: str,
112
+ title: Optional[str] = None,
113
+ template: str = "blue",
114
+ ) -> Dict[str, Any]:
115
+ """Build a Feishu card with markdown content (no table).
116
+
117
+ Used for non-table content that should be sent as a card.
118
+
119
+ Args:
120
+ content: Markdown content for the card body.
121
+ title: Optional card header title.
122
+ template: Card header color template.
123
+
124
+ Returns:
125
+ Complete Feishu card JSON dict.
126
+ """
127
+ card: Dict[str, Any] = {
128
+ "config": {"wide_screen_mode": True},
129
+ }
130
+
131
+ if title:
132
+ card["header"] = {
133
+ "title": {"content": title, "tag": "plain_text"},
134
+ "template": template,
135
+ }
136
+
137
+ card["elements"] = [
138
+ {
139
+ "tag": "markdown",
140
+ "content": content,
141
+ }
142
+ ]
143
+
144
+ return card
145
+
146
+
147
+ def build_mixed_card(
148
+ markdown: str,
149
+ title: Optional[str] = None,
150
+ template: str = "blue",
151
+ ) -> Optional[Dict[str, Any]]:
152
+ """Build a Feishu card that handles mixed content (text + tables).
153
+
154
+ If the content contains tables, they are rendered as Table components.
155
+ Non-table text is rendered as markdown elements.
156
+
157
+ Args:
158
+ markdown: Full markdown content that may include tables.
159
+ title: Optional card header title.
160
+ template: Card header color template.
161
+
162
+ Returns:
163
+ Complete Feishu card JSON dict, or None if no tables found
164
+ (in which case use build_content_card or send as post message).
165
+ """
166
+ tables = parse_table(markdown)
167
+ if not tables:
168
+ return None
169
+
170
+ card: Dict[str, Any] = {
171
+ "config": {"wide_screen_mode": True},
172
+ }
173
+
174
+ if title:
175
+ card["header"] = {
176
+ "title": {"content": title, "tag": "plain_text"},
177
+ "template": template,
178
+ }
179
+
180
+ elements: List[Dict[str, Any]] = []
181
+ table_blocks, text_segments = split_table_and_text(markdown)
182
+
183
+ # Interleave text and table elements in original order
184
+ table_idx = 0
185
+ text_idx = 0
186
+
187
+ # Walk through the original markdown to maintain order
188
+ import re
189
+ _TABLE_BLOCK_RE = re.compile(
190
+ r"((?:^\|[^\n]+\|\s*\n"
191
+ r"^\|[\s:|-]+\|\s*\n"
192
+ r"(?:^\|[^\n]+\|\s*\n?)*)+)",
193
+ re.MULTILINE,
194
+ )
195
+
196
+ last_end = 0
197
+ for match in _TABLE_BLOCK_RE.finditer(markdown):
198
+ # Text before this table
199
+ before = markdown[last_end:match.start()].strip()
200
+ if before:
201
+ elements.append({
202
+ "tag": "markdown",
203
+ "content": before,
204
+ })
205
+
206
+ # Table element
207
+ if table_idx < len(tables):
208
+ table = tables[table_idx]
209
+ columns = _build_table_columns(table.headers)
210
+ rows = _build_table_rows(table.rows, table.headers)
211
+ elements.append({
212
+ "tag": "table",
213
+ "columns": columns,
214
+ "rows": rows,
215
+ })
216
+ table_idx += 1
217
+
218
+ last_end = match.end()
219
+
220
+ # Remaining text after last table
221
+ remaining = markdown[last_end:].strip()
222
+ if remaining:
223
+ elements.append({
224
+ "tag": "markdown",
225
+ "content": remaining,
226
+ })
227
+
228
+ card["elements"] = elements
229
+ return card
230
+
231
+
232
+ def card_to_json(card: Dict[str, Any]) -> str:
233
+ """Serialize a card dict to JSON string.
234
+
235
+ Args:
236
+ card: Feishu card JSON dict.
237
+
238
+ Returns:
239
+ Compact JSON string (ensure_ascii=False for CJK support).
240
+ """
241
+ return json.dumps(card, ensure_ascii=False, separators=(",", ":"))