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.
- hermes_feishu-0.2.0/PKG-INFO +153 -0
- hermes_feishu-0.2.0/README.md +131 -0
- hermes_feishu-0.2.0/pyproject.toml +44 -0
- hermes_feishu-0.2.0/setup.cfg +4 -0
- hermes_feishu-0.2.0/src/hermes_feishu/__init__.py +85 -0
- hermes_feishu-0.2.0/src/hermes_feishu/card_builder.py +241 -0
- hermes_feishu-0.2.0/src/hermes_feishu/schemas.py +116 -0
- hermes_feishu-0.2.0/src/hermes_feishu/sender.py +136 -0
- hermes_feishu-0.2.0/src/hermes_feishu/table_parser.py +233 -0
- hermes_feishu-0.2.0/src/hermes_feishu/tools.py +134 -0
- hermes_feishu-0.2.0/src/hermes_feishu.egg-info/PKG-INFO +153 -0
- hermes_feishu-0.2.0/src/hermes_feishu.egg-info/SOURCES.txt +17 -0
- hermes_feishu-0.2.0/src/hermes_feishu.egg-info/dependency_links.txt +1 -0
- hermes_feishu-0.2.0/src/hermes_feishu.egg-info/entry_points.txt +2 -0
- hermes_feishu-0.2.0/src/hermes_feishu.egg-info/requires.txt +5 -0
- hermes_feishu-0.2.0/src/hermes_feishu.egg-info/top_level.txt +1 -0
- hermes_feishu-0.2.0/tests/test_card_builder.py +152 -0
- hermes_feishu-0.2.0/tests/test_table_parser.py +186 -0
- hermes_feishu-0.2.0/tests/test_tools.py +147 -0
|
@@ -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,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=(",", ":"))
|