openclaw/test_scripts/get_bitable_info.py
2026-01-27 21:49:13 +08:00

263 lines
8.3 KiB
Python
Executable File
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
"""
获取飞书多维表格的正确 app_token 和 table_id
飞书多维表格有两种访问方式:
1. 独立的多维表格https://xxx.feishu.cn/base/{app_token}?table={table_id}
2. 知识库中的多维表格https://xxx.feishu.cn/wiki/{wiki_token}?table={table_id}
对于知识库中的多维表格,需要通过 API 获取实际的 app_token。
使用方法:
export FEISHU_APP_ID="cli_xxxxxxxxxxxxxxxx"
export FEISHU_APP_SECRET="xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
python scripts/feishu/get_bitable_info.py <feishu_url>
"""
import os
import sys
import re
from urllib.parse import urlparse, parse_qs
import requests
FEISHU_API_BASE = "https://open.feishu.cn/open-apis"
def get_tenant_access_token(app_id: str, app_secret: str) -> str:
"""获取租户访问令牌"""
url = f"{FEISHU_API_BASE}/auth/v3/tenant_access_token/internal"
payload = {
"app_id": app_id,
"app_secret": app_secret,
}
response = requests.post(url, json=payload, timeout=30)
response.raise_for_status()
data = response.json()
if data.get("code") != 0:
raise RuntimeError(f"获取访问令牌失败: {data.get('msg')}")
return data["tenant_access_token"]
def parse_feishu_url(url: str) -> dict:
"""解析飞书多维表格 URL"""
parsed = urlparse(url)
path_parts = parsed.path.strip('/').split('/')
query_params = parse_qs(parsed.query)
result = {
"url_type": None,
"token": None,
"table_id": query_params.get('table', [None])[0],
"view_id": query_params.get('view', [None])[0],
}
if 'wiki' in path_parts:
result["url_type"] = "wiki"
wiki_idx = path_parts.index('wiki')
if len(path_parts) > wiki_idx + 1:
result["token"] = path_parts[wiki_idx + 1]
elif 'base' in path_parts:
result["url_type"] = "base"
base_idx = path_parts.index('base')
if len(path_parts) > base_idx + 1:
result["token"] = path_parts[base_idx + 1]
return result
def get_wiki_node_info(token: str, wiki_token: str) -> dict:
"""获取知识库节点信息"""
url = f"{FEISHU_API_BASE}/wiki/v2/spaces/get_node"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json; charset=utf-8",
}
params = {"token": wiki_token}
response = requests.get(url, headers=headers, params=params, timeout=30)
try:
data = response.json()
except Exception:
response.raise_for_status()
raise RuntimeError(f"无法解析飞书 API 响应: {response.text[:200]}")
if data.get("code") != 0:
error_msg = data.get("msg", "未知错误")
error_code = data.get("code")
raise RuntimeError(
f"获取知识库节点信息失败 (错误码: {error_code}): {error_msg}"
)
return data.get("data", {}).get("node", {})
def list_tables_in_base(token: str, app_token: str) -> list:
"""列出多维表格中的所有数据表"""
url = f"{FEISHU_API_BASE}/bitable/v1/apps/{app_token}/tables"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json; charset=utf-8",
}
response = requests.get(url, headers=headers, timeout=30)
try:
data = response.json()
except Exception:
response.raise_for_status()
raise RuntimeError(f"无法解析飞书 API 响应: {response.text[:200]}")
if data.get("code") != 0:
error_msg = data.get("msg", "未知错误")
error_code = data.get("code")
raise RuntimeError(
f"获取数据表列表失败 (错误码: {error_code}): {error_msg}"
)
return data.get("data", {}).get("items", [])
def main():
"""主函数"""
if len(sys.argv) < 2:
print("错误: 缺少飞书多维表格 URL 参数")
print("\n使用方法:")
print(" python scripts/feishu/get_bitable_info.py <feishu_url>")
print("\n示例:")
print(" python scripts/feishu/get_bitable_info.py 'https://xxx.feishu.cn/wiki/xxx?table=xxx'")
sys.exit(1)
feishu_url = sys.argv[1]
# 检查环境变量
app_id = os.getenv("FEISHU_APP_ID")
app_secret = os.getenv("FEISHU_APP_SECRET")
if not app_id or not app_secret:
print("错误: 缺少飞书 API 凭证")
print("\n请设置环境变量:")
print(" export FEISHU_APP_ID='your_app_id'")
print(" export FEISHU_APP_SECRET='your_app_secret'")
sys.exit(1)
print("=" * 80)
print("飞书多维表格信息获取工具")
print("=" * 80)
print(f"\n输入 URL: {feishu_url}\n")
# 解析 URL
print("[1/4] 解析 URL...")
parsed = parse_feishu_url(feishu_url)
print(f" URL 类型: {parsed['url_type']}")
print(f" Token: {parsed['token']}")
print(f" Table ID: {parsed['table_id']}")
print(f" View ID: {parsed['view_id']}")
if not parsed['token']:
print("\n错误: 无法从 URL 中解析出 token")
print("请确保 URL 格式正确")
sys.exit(1)
# 获取访问令牌
print("\n[2/4] 获取访问令牌...")
try:
token = get_tenant_access_token(app_id, app_secret)
print("✓ 成功获取访问令牌")
except Exception as e:
print(f"✗ 获取访问令牌失败: {e}")
sys.exit(1)
# 获取实际的 app_token
app_token = None
if parsed['url_type'] == 'wiki':
print("\n[3/4] 从知识库节点获取多维表格信息...")
try:
node_info = get_wiki_node_info(token, parsed['token'])
obj_type = node_info.get('obj_type')
if obj_type == 'bitable':
# 知识库中的多维表格
app_token = node_info.get('obj_token')
print(f"✓ 找到多维表格")
print(f" 节点类型: {obj_type}")
print(f" App Token: {app_token}")
else:
print(f"✗ 该节点不是多维表格,类型为: {obj_type}")
sys.exit(1)
except Exception as e:
print(f"✗ 获取节点信息失败: {e}")
print("\n提示: 如果是独立的多维表格,请直接访问多维表格页面并复制 URL")
sys.exit(1)
elif parsed['url_type'] == 'base':
print("\n[3/4] 使用独立多维表格的 app_token...")
app_token = parsed['token']
print(f"✓ App Token: {app_token}")
else:
print(f"\n错误: 不支持的 URL 类型: {parsed['url_type']}")
sys.exit(1)
if not app_token:
print("\n错误: 无法获取 app_token")
sys.exit(1)
# 列出数据表
print("\n[4/4] 列出多维表格中的数据表...")
try:
tables = list_tables_in_base(token, app_token)
print(f"✓ 找到 {len(tables)} 个数据表:\n")
for i, table in enumerate(tables, 1):
table_id = table.get('table_id')
table_name = table.get('name')
is_target = table_id == parsed['table_id']
marker = " ← 目标表格" if is_target else ""
print(f" {i}. {table_name}")
print(f" Table ID: {table_id}{marker}")
except Exception as e:
print(f"✗ 获取数据表列表失败: {e}")
sys.exit(1)
# 输出配置信息
print("\n" + "=" * 80)
print("配置信息")
print("=" * 80)
print("\n请将以下配置更新到脚本中:\n")
print(f"FEISHU_APP_TOKEN = \"{app_token}\"")
if parsed['table_id']:
print(f"FEISHU_TABLE_ID = \"{parsed['table_id']}\"")
else:
print(f"# 请从上面的列表中选择一个 table_id")
print(f"FEISHU_TABLE_ID = \"<table_id>\"")
if parsed['view_id']:
print(f"FEISHU_VIEW_ID = \"{parsed['view_id']}\" # 可选,建议不使用")
print("\n" + "=" * 80)
print("完成!")
print("=" * 80)
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\n\n用户中断操作")
sys.exit(1)
except Exception as e:
print(f"\n\n未预期的错误: {e}")
import traceback
traceback.print_exc()
sys.exit(1)