feat: 飞书知识库读写工具:增删查改节点、写入页面、创建早报、记录踩坑,基于 tenant_access_token。
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python3
|
||||
"""写入 Wiki 页面内容。支持 Markdown 文件和 Block JSON。"""
|
||||
import json, sys, os, argparse
|
||||
import subprocess
|
||||
|
||||
SPACE_ID = os.environ.get('FEISHU_SPACE_ID', '7664817589230570761')
|
||||
|
||||
def get_token():
|
||||
r = subprocess.run(['python3', os.path.join(os.path.dirname(__file__), 'init_token.py')],
|
||||
capture_output=True, text=True)
|
||||
return json.loads(r.stdout)['token']
|
||||
|
||||
# Block helpers
|
||||
def te(t, **kw):
|
||||
d = {}
|
||||
d.update(kw)
|
||||
return {'text_run': {'content': t, 'text_element_style': d}}
|
||||
|
||||
def link_el(t, u):
|
||||
return {'text_run': {'content': t, 'text_element_style': {'link': {'url': u}}}}
|
||||
|
||||
H1 = lambda t: {'block_type': 3, 'heading1': {'elements': [te(t, bold=True)], 'style': {}}}
|
||||
H2 = lambda t: {'block_type': 4, 'heading2': {'elements': [te(t, bold=True)], 'style': {}}}
|
||||
H3 = lambda t: {'block_type': 5, 'heading3': {'elements': [te(t, bold=True)], 'style': {}}}
|
||||
DIV = lambda: {'block_type': 22, 'divider': {}}
|
||||
TXT = lambda t: {'block_type': 2, 'text': {'elements': [te(t)], 'style': {}}}
|
||||
BLD = lambda t: {'block_type': 2, 'text': {'elements': [te(t, bold=True)], 'style': {}}}
|
||||
BUL = lambda t, lt='', lu='': {'block_type': 12, 'bullet': {'elements': [te(t)] + ([link_el(' ' + lt, lu)] if lt and lu else []), 'style': {}}}
|
||||
|
||||
def json_to_blocks(data):
|
||||
"""Convert JSON block data to Feishu block objects"""
|
||||
if isinstance(data, list):
|
||||
return [json_to_blocks(d) for d in data]
|
||||
bt = data.get('type', 'text')
|
||||
text = data.get('text', '')
|
||||
|
||||
type_map = {
|
||||
'h1': lambda: H1(text),
|
||||
'h2': lambda: H2(text),
|
||||
'h3': lambda: H3(text),
|
||||
'text': lambda: TXT(text),
|
||||
'bold': lambda: BLD(text),
|
||||
'divider': lambda: DIV(),
|
||||
'bullet': lambda: BUL(text, data.get('link_text', ''), data.get('link_url', '')),
|
||||
}
|
||||
return type_map.get(bt, lambda: TXT(text))()
|
||||
|
||||
def write_blocks(node_token, blocks):
|
||||
import requests
|
||||
token = get_token()
|
||||
h = {'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json'}
|
||||
|
||||
# Get node detail to find obj_token
|
||||
r = requests.get(
|
||||
'https://open.feishu.cn/open-apis/wiki/v2/spaces/get_node',
|
||||
params={'token': node_token}, headers=h
|
||||
)
|
||||
nd = r.json()
|
||||
if nd.get('code') != 0:
|
||||
raise Exception(f"Get node failed: {nd.get('msg')}")
|
||||
doc_id = nd['data']['node']['obj_token']
|
||||
|
||||
# Clear defaults
|
||||
r = requests.get(
|
||||
f'https://open.feishu.cn/open-apis/docx/v1/documents/{doc_id}/blocks/{doc_id}',
|
||||
headers=h
|
||||
)
|
||||
kids = r.json().get('data', {}).get('block', {}).get('children', [])
|
||||
if kids:
|
||||
cids = [c['block_id'] for c in kids]
|
||||
requests.delete(
|
||||
f'https://open.feishu.cn/open-apis/docx/v1/documents/{doc_id}/blocks/{doc_id}/children/batch_delete',
|
||||
headers=h, json={'start_index': 0, 'end_index': len(cids)}
|
||||
)
|
||||
|
||||
# Write in batches
|
||||
batch_size = 25
|
||||
for i in range(0, len(blocks), batch_size):
|
||||
batch = blocks[i:i + batch_size]
|
||||
r = requests.post(
|
||||
f'https://open.feishu.cn/open-apis/docx/v1/documents/{doc_id}/blocks/{doc_id}/children',
|
||||
headers=h, json={'children': batch, 'index': -1}
|
||||
)
|
||||
if r.json().get('code') != 0:
|
||||
raise Exception(f"Write batch {i // batch_size} failed: {r.json().get('msg')}")
|
||||
return True
|
||||
|
||||
if __name__ == '__main__':
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument('--node', required=True, help='Node token')
|
||||
p.add_argument('--file', default=None, help='Content file (JSON block array or Markdown)')
|
||||
p.add_argument('--json', default=None, help='Inline JSON block array')
|
||||
p.add_argument('--markdown', action='store_true', help='Treat --file as Markdown')
|
||||
args = p.parse_args()
|
||||
|
||||
try:
|
||||
if args.json:
|
||||
data = json.loads(args.json)
|
||||
elif args.file:
|
||||
if args.markdown or args.file.endswith('.md'):
|
||||
import subprocess
|
||||
r = subprocess.run(
|
||||
['python3', os.path.join(os.path.dirname(__file__), 'md_to_blocks.py'), args.file],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
if r.returncode != 0:
|
||||
raise Exception(f"md_to_blocks failed: {r.stderr}")
|
||||
data = json.loads(r.stdout)
|
||||
else:
|
||||
with open(args.file) as f:
|
||||
data = json.load(f)
|
||||
else:
|
||||
data = json.load(sys.stdin)
|
||||
|
||||
if isinstance(data, list) and not isinstance(data, str):
|
||||
blocks = data
|
||||
else:
|
||||
blocks = json_to_blocks(data)
|
||||
write_blocks(args.node, blocks)
|
||||
print('OK')
|
||||
except Exception as e:
|
||||
print(f'Error: {e}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
Reference in New Issue
Block a user