openclaw-home-pc/workspace/create_feishu_doc_api.py
2026-03-21 15:31:06 +08:00

198 lines
10 KiB
Python
Raw Permalink 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
"""
使用飞书 API 和用户访问令牌创建云文档
"""
import requests
import json
import sys
# 飞书应用凭证
APP_ID = "cli_a93815b250b9dcb5"
APP_SECRET = "NogaPY8DiMHMyadOKDW26bqkGPnrOkND"
# 刷新令牌
REFRESH_TOKEN = "eyJhbGciOiJFUzI1NiIsImZlYXR1cmVfY29kZSI6IkZlYXR1cmVPQXV0aEpXVFNpZ25fQ04iLCJraWQiOiI3NjE2ODk0MzA1NzE3OTE0NTczIiwidHlwIjoiSldUIn0.eyJqdGkiOiI3NjE4Mjk3MjkyNDUzMzE3NTgxIiwiaWF0IjoxNzczNzczMDYyLCJleHAiOjE3NzQzNzc4NjIsInZlciI6InYxIiwidHlwIjoicmVmcmVzaF90b2tlbiIsImNsaWVudF9pZCI6ImNsaV9hOTM4MTViMjUwYjlkY2I1Iiwic2NvcGUiOiJhdXRoOnVzZXIuaWQ6cmVhZCBiYXNlOmFwcDpjb3B5IGJhc2U6YXBwOmNyZWF0ZSBiYXNlOmFwcDpyZWFkIGJhc2U6YXBwOnVwZGF0ZSBiYXNlOmZpZWxkOmNyZWF0ZSBiYXNlOmZpZWxkOmRlbGV0ZSBiYXNlOmZpZWxkOnJlYWQgYmFzZTpmaWVsZDp1cGRhdGUgYmFzZTpyZWNvcmQ6Y3JlYXRlIGJhc2U6cmVjb3JkOmRlbGV0ZSBiYXNlOnJlY29yZDpyZXRyaWV2ZSBiYXNlOnJlY29yZDp1cGRhdGUgYmFzZTp0YWJsZTpjcmVhdGUgYmFzZTp0YWJsZTpkZWxldGUgYmFzZTp0YWJsZTpyZWFkIGJhc2U6dGFibGU6dXBkYXRlIGJhc2U6dmlldzpyZWFkIGJhc2U6dmlldzp3cml0ZV9vbmx5IGJvYXJkOndoaXRlYm9hcmQ6bm9kZTpjcmVhdGUgYm9hcmQ6d2hpdGVib2FyZDpub2RlOnJlYWQgY2FsZW5kYXI6Y2FsZW5kYXIuZXZlbnQ6Y3JlYXRlIGNhbGVuZGFyOmNhbGVuZGFyLmV2ZW50OmRlbGV0ZSBjYWxlbmRhcjpjYWxlbmRhci5ldmVudDpyZWFkIGNhbGVuZGFyOmNhbGVuZGFyLmV2ZW50OnJlcGx5IGNhbGVuZGFyOmNhbGVuZGFyLmV2ZW50OnVwZGF0ZSBjYWxlbmRhcjpjYWxlbmRhci5mcmVlX2J1c3k6cmVhZCBjYWxlbmRhcjpjYWxlbmRhcjpyZWFkIGNvbnRhY3Q6Y29udGFjdC5iYXNlOnJlYWRvbmx5IGNvbnRhY3Q6dXNlci5iYXNlOnJlYWRvbmx5IGNvbnRhY3Q6dXNlci5iYXNpY19wcm9maWxlOnJlYWRvbmx5IGNvbnRhY3Q6dXNlci5lbXBsb3llZV9pZDpyZWFkb25seSBjb250YWN0OnVzZXI6c2VhcmNoIGRvY3M6ZG9jdW1lbnQuY29tbWVudDpjcmVhdGUgZG9jczpkb2N1bWVudC5jb21tZW50OnJlYWQgZG9jczpkb2N1bWVudC5jb21tZW50OnVwZGF0ZSBkb2NzOmRvY3VtZW50Lm1lZGlhOmRvd25sb2FkIGRvY3M6ZG9jdW1lbnQubWVkaWE6dXBsb2FkIGRvY3M6ZG9jdW1lbnQ6Y29weSBkb2NzOmRvY3VtZW50OmV4cG9ydCBkb2N4OmRvY3VtZW50OmNyZWF0ZSBkb2N4OmRvY3VtZW50OnJlYWRvbmx5IGRvY3g6ZG9jdW1lbnQ6d3JpdGVfb25seSBkcml2ZTpkcml2ZS5tZXRhZGF0YTpyZWFkb25seSBkcml2ZTpmaWxlOmRvd25sb2FkIGRyaXZlOmZpbGU6dXBsb2FkIGltOmNoYXQubWVtYmVyczpyZWFkIGltOmNoYXQ6cmVhZCBpbTptZXNzYWdlIGltOm1lc3NhZ2UuZ3JvdXBfbXNnOmdldF9hc191c2VyIGltOm1lc3NhZ2UucDJwX21zZzpnZXRfYXNfdXNlciBpbTptZXNzYWdlOnJlYWRvbmx5IHNlYXJjaDpkb2NzOnJlYWQgc2VhcmNoOm1lc3NhZ2Ugc2hlZXRzOnNwcmVhZHNoZWV0Lm1ldGE6cmVhZCBzaGVldHM6c3ByZWFkc2hlZXQ6Y3JlYXRlIHNoZWV0czpzcHJlYWRzaGVldDpyZWFkIHNoZWV0czpzcHJlYWRzaGVldDp3cml0ZV9vbmx5IHNwYWNlOmRvY3VtZW50OmRlbGV0ZSBzcGFjZTpkb2N1bWVudDptb3ZlIHNwYWNlOmRvY3VtZW50OnJldHJpZXZlIHRhc2s6Y29tbWVudDpyZWFkIHRhc2s6Y29tbWVudDp3cml0ZSB0YXNrOnRhc2s6cmVhZCB0YXNrOnRhc2s6d3JpdGUgdGFzazp0YXNrOndyaXRlb25seSB0YXNrOnRhc2tsaXN0OnJlYWQgdGFzazp0YXNrbGlzdDp3cml0ZSB3aWtpOm5vZGU6Y29weSB3aWtpOm5vZGU6Y3JlYXRlIHdpa2k6bm9kZTptb3ZlIHdpa2k6bm9kZTpyZWFkIHdpa2k6bm9kZTpyZXRyaWV2ZSB3aWtpOnNwYWNlOnJlYWQgd2lraTpzcGFjZTpyZXRyaWV2ZSB3aWtpOnNwYWNlOndyaXRlX29ubHkgb2ZmbGluZV9hY2Nlc3MiLCJhdXRoX2lkIjoiNzYxODI5NzI2NjgwOTM1OTU1MyIsImF1dGhfdGltZSI6MTc3Mzc3MzA2MiwiYXV0aF9leHAiOjE4MDUzMDkwNjIsImF0X2lkIjoiNzYxODI5NzI5MjQzNjU3MzExNSIsImF0X2V4cCI6MTc3Mzc4MDI2MiwidW5pdCI6ImV1X25jIiwidGVuYW50X3VuaXQiOiJldV9uYyIsIm9wYXF1ZSI6dHJ1ZSwiZW5jIjoiQWlRa0FRRUNBTUlEQUFFQkF3QUNBUTBBQXdzTEFBQUFBd0FBQUFkR1pXRjBkWEpsQUFBQUVHOWhkWFJvWDI5d1lYRjFaVjlxZDNRQUFBQUlWR1Z1WVc1MFNXUUFBQUFCTUFBQUFBUlVhVzFsQUFBQUNqRTNOek0yTVRreU1EQVBBQVFNQUFBQUFRb0FBV0xGSWxtdmdBQWlDd0FDQUFBQURFNkdsUjZPN0VpWjJQeGh0UXNBQXdBQUFEQmFqSDRQSmx6d2lISDZybXlCZVg4TWRjSHAxc2t0U3R6c3h2U1F5NUtSL2JEYlJhMnhoeVA5RHVYWDFvUTRvaFVBQ3dBRkFBQUFCV1YxWDI1akFKZlhUKzBCWk1PZU5oYVpyVEpIN0JPSTNoYWtLRmdWL2N5S2p2U294eE9nVmxDdStlclZUY2ZscGJFclMxd2hCcVBWU0MvcW1KSThVcGIvc2VlWHpwRTg0QkNRYXM3cHFTY2VuUm9kWit2Rk9nd1dlVC9XazJ6T0ZEZ1hUNmZLbVFnMFBabERJNWhDTHpzYUo0Sjl2dkR4YzNXbXMwdGpTZ201SnlZVHd0dUN1aUt0NCtGSWIxd00rVHJxdGxEeWhNeGxqZz09IiwiZW5jX3ZlciI6InYxIn0.QfPh3o5AW2ekxVLTGG993OwytuQxYkQ3qzCQTSwtSXqxoDwtAFjP2mP0_kPteBGlIHsjBHsAWmfA-HvFYy4SdQ"
def refresh_access_token():
"""使用刷新令牌获取新的访问令牌"""
url = "https://open.feishu.cn/open-apis/authen/v1/refresh_access_token"
headers = {
"Content-Type": "application/json"
}
payload = {
"grant_type": "refresh_token",
"refresh_token": REFRESH_TOKEN,
"client_id": APP_ID,
"client_secret": APP_SECRET
}
print("正在刷新访问令牌...")
resp = requests.post(url, json=payload, headers=headers, timeout=10)
print(f"HTTP 状态码:{resp.status_code}")
print(f"响应内容:{resp.text[:500]}")
try:
data = resp.json()
except:
data = {"raw": resp.text, "code": -1}
if data.get("code") != 0:
raise Exception(f"刷新令牌失败:{data}")
access_token = data["data"]["access_token"]
print(f"✓ 刷新成功,新令牌:{access_token[:30]}...")
return access_token
def create_document(token, title, markdown):
"""使用飞书 API 创建云文档"""
url = "https://open.feishu.cn/open-apis/docx/v1/documents"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
payload = {
"title": title,
"folder_token": "", # 空表示创建在个人空间根目录
}
print(f"正在创建文档:{title}")
resp = requests.post(url, json=payload, headers=headers, timeout=10)
print(f"HTTP 状态码:{resp.status_code}")
data = resp.json()
print(f"响应:{json.dumps(data, indent=2, ensure_ascii=False)[:500]}")
if data.get("code") != 0:
raise Exception(f"创建文档失败:{data}")
doc_token = data["data"]["document"]["token"]
doc_id = data["data"]["document"]["doc_id"]
doc_url = f"https://www.feishu.cn/docx/{doc_token}"
# 更新文档内容
print(f"正在更新文档内容...")
update_document_content(token, doc_token, markdown)
return doc_token, doc_id, doc_url
def update_document_content(token, doc_token, markdown):
"""更新文档内容(使用批量更新 API"""
url = f"https://open.feishu.cn/open-apis/docx/v1/documents/{doc_token}/batch_update"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
# 简单处理:将 Markdown 作为纯文本插入
# 飞书 API 需要特定的块格式,这里简化处理
blocks = []
# 解析 Markdown转换为飞书文档块
lines = markdown.split('\n')
current_text = []
for line in lines:
if line.startswith('# '):
if current_text:
blocks.append({"text": {"elements": [{"text_run": {"content": "\n".join(current_text)}}]}})
current_text = []
blocks.append({"heading1": {"elements": [{"text_run": {"content": line[2:]}}]}})
elif line.startswith('## '):
if current_text:
blocks.append({"text": {"elements": [{"text_run": {"content": "\n".join(current_text)}}]}})
current_text = []
blocks.append({"heading2": {"elements": [{"text_run": {"content": line[3:]}}]}})
elif line.startswith('### '):
if current_text:
blocks.append({"text": {"elements": [{"text_run": {"content": "\n".join(current_text)}}]}})
current_text = []
blocks.append({"heading3": {"elements": [{"text_run": {"content": line[4:]}}]}})
elif line.startswith('> '):
if current_text:
blocks.append({"text": {"elements": [{"text_run": {"content": "\n".join(current_text)}}]}})
current_text = []
blocks.append({"quote": {"elements": [{"text_run": {"content": line[2:]}}]}})
elif line.startswith('- ') or line.startswith('* '):
if current_text:
blocks.append({"text": {"elements": [{"text_run": {"content": "\n".join(current_text)}}]}})
current_text = []
blocks.append({"bullet": {"elements": [{"text_run": {"content": line[2:]}}]}})
elif line.startswith('1. ') or line.startswith('2. '):
if current_text:
blocks.append({"text": {"elements": [{"text_run": {"content": "\n".join(current_text)}}]}})
current_text = []
content_text = line.split('. ', 1)[1] if '. ' in line else line
blocks.append({"ordered": {"elements": [{"text_run": {"content": content_text}}]}})
elif line.startswith('```'):
if current_text:
blocks.append({"text": {"elements": [{"text_run": {"content": "\n".join(current_text)}}]}})
current_text = []
elif line == '---':
if current_text:
blocks.append({"text": {"elements": [{"text_run": {"content": "\n".join(current_text)}}]}})
current_text = []
blocks.append({"divider": {}})
elif line.strip() == '':
if current_text:
blocks.append({"text": {"elements": [{"text_run": {"content": "\n".join(current_text)}}]}})
current_text = []
else:
current_text.append(line)
if current_text:
blocks.append({"text": {"elements": [{"text_run": {"content": "\n".join(current_text)}}]}})
# 构建请求体
requests_body = {
"requests": []
}
for i, block in enumerate(blocks):
requests_body["requests"].append({
"action": "append_block",
"args": {
"block": block,
"parent_block_id": "",
"index": i
}
})
resp = requests.post(url, json=requests_body, headers=headers, timeout=30)
print(f"更新内容 HTTP 状态码:{resp.status_code}")
data = resp.json()
if data.get("code") != 0:
print(f"警告:更新文档内容部分失败:{data}")
else:
print(f"✓ 成功更新 {len(blocks)} 个块")
def main():
if len(sys.argv) < 3:
print("用法python3 create_feishu_doc_api.py <title> <content_file>")
sys.exit(1)
title = sys.argv[1]
content_file = sys.argv[2]
# 读取文档内容
with open(content_file, 'r', encoding='utf-8') as f:
markdown = f.read()
print(f"创建文档:{title}")
print(f"内容来源:{content_file}")
print(f"内容长度:{len(markdown)} 字符")
# 刷新访问令牌
access_token = refresh_access_token()
# 创建文档
doc_token, doc_id, doc_url = create_document(access_token, title, markdown)
print(f"\n✓ 文档创建成功!")
print(f"文档 Token: {doc_token}")
print(f"文档 ID: {doc_id}")
print(f"文档 URL: {doc_url}")
return doc_url
if __name__ == "__main__":
main()