Files
colleague-skill/tools/feishu_mcp_client.py
titanwings 6a0b31aa6c refactor: restructure to official AgentSkills/Claude Code skill format
- Flatten colleague-creator/ to repo root (repo = skill directory)
- Update SKILL.md frontmatter with official fields: name, description,
  argument-hint, version, user-invocable, allowed-tools
- Move PRD.md → docs/PRD.md
- Add .gitignore, requirements.txt, LICENSE
- Update README and INSTALL docs to reflect new structure and git clone install

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 13:37:54 +08:00

311 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
飞书 MCP 客户端封装cso1z/Feishu-MCP 方案)
通过 Feishu MCP Server 读取文档、wiki、消息记录。
适合:公司已授权的文档、有 App token 权限的内容。
前置要求:
1. 安装 Feishu MCPnpm install -g feishu-mcp
2. 配置 App ID 和 App Secret飞书开放平台创建企业自建应用
3. 给应用开通必要权限(见下方 REQUIRED_PERMISSIONS
权限列表(飞书开放平台 → 权限管理 → 开通):
- docs:doc:readonly 读取文档
- wiki:wiki:readonly 读取知识库
- im:message:readonly 读取消息
- bitable:app:readonly 读取多维表格
- sheets:spreadsheet:readonly 读取表格
用法:
# 配置 token一次性
python3 feishu_mcp_client.py --setup
# 读取文档
python3 feishu_mcp_client.py --url "https://xxx.feishu.cn/wiki/xxx" --output out.txt
# 读取消息记录
python3 feishu_mcp_client.py --chat-id "oc_xxx" --target "张三" --output out.txt
# 列出某空间下的所有文档
python3 feishu_mcp_client.py --list-wiki --space-id "xxx"
"""
from __future__ import annotations
import os
import sys
import json
import argparse
import subprocess
from pathlib import Path
from typing import Optional
CONFIG_PATH = Path.home() / ".colleague-skill" / "feishu_config.json"
# ─── 配置管理 ────────────────────────────────────────────────────────────────
def load_config() -> dict:
if CONFIG_PATH.exists():
return json.loads(CONFIG_PATH.read_text())
return {}
def save_config(config: dict) -> None:
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
CONFIG_PATH.write_text(json.dumps(config, indent=2))
print(f"配置已保存到 {CONFIG_PATH}")
def setup_config() -> None:
print("=== 飞书 MCP 配置 ===")
print("请前往飞书开放平台open.feishu.cn创建企业自建应用获取以下信息\n")
app_id = input("App ID (cli_xxx): ").strip()
app_secret = input("App Secret: ").strip()
print("\n配置方式选择:")
print(" [1] App Token应用权限需要在飞书后台开通对应权限")
print(" [2] User Token个人权限能访问你本人有权限的所有内容需要定期刷新")
mode = input("选择 [1/2],默认 1").strip() or "1"
config = {
"app_id": app_id,
"app_secret": app_secret,
"mode": "app" if mode == "1" else "user",
}
if mode == "2":
print("\n获取 User Token飞书开放平台 → OAuth 2.0 → 获取 user_access_token")
user_token = input("User Access Token (u-xxx)").strip()
config["user_token"] = user_token
print("注意User Token 有效期约 2 小时,过期后需要重新配置")
save_config(config)
print("\n✅ 配置完成!")
# ─── MCP 调用封装 ─────────────────────────────────────────────────────────────
def call_mcp(tool: str, params: dict, config: dict) -> dict:
"""
通过 npx 调用 feishu-mcp 工具。
feishu-mcp 支持 stdio 模式,直接 JSON 通信。
"""
env = os.environ.copy()
env["FEISHU_APP_ID"] = config.get("app_id", "")
env["FEISHU_APP_SECRET"] = config.get("app_secret", "")
if config.get("mode") == "user" and config.get("user_token"):
env["FEISHU_USER_ACCESS_TOKEN"] = config["user_token"]
payload = json.dumps({
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": tool,
"arguments": params,
},
"id": 1,
})
try:
result = subprocess.run(
["npx", "-y", "feishu-mcp", "--stdio"],
input=payload,
capture_output=True,
text=True,
env=env,
timeout=30,
)
if result.returncode != 0:
raise RuntimeError(f"MCP 调用失败:{result.stderr}")
return json.loads(result.stdout)
except FileNotFoundError:
print("错误:未找到 npx请先安装 Node.js", file=sys.stderr)
print("安装 Feishu MCPnpm install -g feishu-mcp", file=sys.stderr)
sys.exit(1)
def extract_doc_token(url: str) -> tuple[str, str]:
"""从飞书 URL 中提取文档 token 和类型"""
import re
patterns = [
(r"/wiki/([A-Za-z0-9]+)", "wiki"),
(r"/docx/([A-Za-z0-9]+)", "docx"),
(r"/docs/([A-Za-z0-9]+)", "doc"),
(r"/sheets/([A-Za-z0-9]+)", "sheet"),
(r"/base/([A-Za-z0-9]+)", "base"),
]
for pattern, doc_type in patterns:
m = re.search(pattern, url)
if m:
return m.group(1), doc_type
raise ValueError(f"无法从 URL 解析文档 token{url}")
# ─── 功能函数 ─────────────────────────────────────────────────────────────────
def fetch_doc_via_mcp(url: str, config: dict) -> str:
"""通过 MCP 读取飞书文档或 Wiki"""
token, doc_type = extract_doc_token(url)
if doc_type == "wiki":
result = call_mcp("get_wiki_node", {"token": token}, config)
elif doc_type in ("docx", "doc"):
result = call_mcp("get_doc_content", {"doc_token": token}, config)
elif doc_type == "sheet":
result = call_mcp("get_spreadsheet_content", {"spreadsheet_token": token}, config)
else:
raise ValueError(f"不支持的文档类型:{doc_type}")
# 提取 MCP 返回的内容
if "result" in result:
content = result["result"]
if isinstance(content, list):
# MCP tool result 格式
for item in content:
if isinstance(item, dict) and item.get("type") == "text":
return item.get("text", "")
elif isinstance(content, str):
return content
elif "error" in result:
raise RuntimeError(f"MCP 返回错误:{result['error']}")
return json.dumps(result, ensure_ascii=False, indent=2)
def fetch_messages_via_mcp(
chat_id: str,
target_name: str,
limit: int,
config: dict,
) -> str:
"""通过 MCP 读取群聊消息记录"""
result = call_mcp(
"get_chat_messages",
{
"chat_id": chat_id,
"page_size": min(limit, 50), # 飞书 API 单次最多 50 条
},
config,
)
messages = []
raw = result.get("result", [])
if isinstance(raw, list):
messages = raw
elif isinstance(raw, str):
try:
messages = json.loads(raw)
except Exception:
return raw
# 过滤目标人物
if target_name:
messages = [
m for m in messages
if target_name in str(m.get("sender", {}).get("name", ""))
]
# 分类输出
long_msgs = [m for m in messages if len(str(m.get("content", ""))) > 50]
short_msgs = [m for m in messages if len(str(m.get("content", ""))) <= 50]
lines = [
"# 飞书消息记录MCP 方案)",
f"群聊 ID{chat_id}",
f"目标人物:{target_name or '全部'}",
f"{len(messages)}",
"",
"---",
"",
"## 长消息",
"",
]
for m in long_msgs:
sender = m.get("sender", {}).get("name", "")
content = m.get("content", "")
ts = m.get("create_time", "")
lines.append(f"[{ts}] {sender}{content}")
lines.append("")
lines += ["---", "", "## 日常消息", ""]
for m in short_msgs[:200]:
sender = m.get("sender", {}).get("name", "")
content = m.get("content", "")
lines.append(f"{sender}{content}")
return "\n".join(lines)
def list_wiki_docs(space_id: str, config: dict) -> str:
"""列出知识库空间下的所有文档"""
result = call_mcp("list_wiki_nodes", {"space_id": space_id}, config)
raw = result.get("result", "")
if isinstance(raw, str):
return raw
return json.dumps(raw, ensure_ascii=False, indent=2)
# ─── CLI ─────────────────────────────────────────────────────────────────────
def main() -> None:
parser = argparse.ArgumentParser(description="飞书 MCP 客户端")
parser.add_argument("--setup", action="store_true", help="初始化配置App ID / Secret")
parser.add_argument("--url", help="飞书文档/Wiki/表格链接")
parser.add_argument("--chat-id", help="群聊 IDoc_xxx 格式)")
parser.add_argument("--target", help="目标人物姓名")
parser.add_argument("--limit", type=int, default=500, help="最多获取消息数")
parser.add_argument("--list-wiki", action="store_true", help="列出知识库文档")
parser.add_argument("--space-id", help="知识库 Space ID")
parser.add_argument("--output", default=None, help="输出文件路径")
args = parser.parse_args()
if args.setup:
setup_config()
return
config = load_config()
if not config:
print("错误尚未配置请先运行python3 feishu_mcp_client.py --setup", file=sys.stderr)
sys.exit(1)
content = ""
if args.url:
print(f"通过 MCP 读取:{args.url}", file=sys.stderr)
content = fetch_doc_via_mcp(args.url, config)
elif args.chat_id:
print(f"通过 MCP 读取消息:{args.chat_id}", file=sys.stderr)
content = fetch_messages_via_mcp(
args.chat_id,
args.target or "",
args.limit,
config,
)
elif args.list_wiki:
if not args.space_id:
print("错误:--list-wiki 需要 --space-id", file=sys.stderr)
sys.exit(1)
content = list_wiki_docs(args.space_id, config)
else:
parser.print_help()
return
if args.output:
Path(args.output).write_text(content, encoding="utf-8")
print(f"✅ 已保存到 {args.output}", file=sys.stderr)
else:
print(content)
if __name__ == "__main__":
main()