ai_member_xiaobian/scripts/feishu_sheet_writer.py
2026-05-15 10:57:05 +08:00

332 lines
11 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
"""
飞书电子表格剧本写入工具
将儿童互动英语剧本以8列格式写入飞书电子表格支持知识点富文本标注。
用法:
python scripts/feishu_sheet_writer.py create --title "U22 剧本" --credential xiaobian
python scripts/feishu_sheet_writer.py write --token <spreadsheet_token> --sheet <sheet_id> \
--data <json_file> --credential xiaobian
python scripts/feishu_sheet_writer.py append --token <spreadsheet_token> --sheet <sheet_id> \
--data <json_file> --credential xiaobian
python scripts/feishu_sheet_writer.py create-sheet --token <spreadsheet_token> \
--title "U22_L1_起" --credential xiaobian
"""
import json
import sys
import os
import argparse
import requests
BASE_URL = "https://open.feishu.cn/open-apis"
CRED_DIR = "/root/.openclaw/credentials"
def get_token(credential_name):
"""获取 Bot tenant_access_token"""
config_path = os.path.join(CRED_DIR, credential_name, "config.json")
if not os.path.exists(config_path):
print(f"❌ 凭证文件不存在: {config_path}", file=sys.stderr)
sys.exit(1)
with open(config_path) as f:
config = json.load(f)
app_id = config.get("apps", [{}])[0].get("appId", "")
app_secret = config.get("apps", [{}])[0].get("appSecret", "")
resp = requests.post(
f"{BASE_URL}/auth/v3/tenant_access_token/internal",
json={"app_id": app_id, "app_secret": app_secret},
timeout=10,
)
data = resp.json()
if data.get("code") != 0:
print(f"❌ 获取token失败: {data}", file=sys.stderr)
sys.exit(1)
return data["tenant_access_token"]
def format_cell_value(text, knowledge_points=None, is_user_line=False):
"""
将文本格式化为飞书富文本单元格值。
Args:
text: 单元格文本
knowledge_points: 知识点列表,如 ["afternoon", "adventure"]
is_user_line: 是否为User台词User台词知识点=红色+粗体NPC=粗体)
Returns:
飞书单元格值对象带textFormatRuns
"""
if not knowledge_points or not text:
return text
# 找出所有知识点的位置
format_runs = []
text_lower = text.lower()
# 去重并按在文本中的位置排序
found_kps = []
for kp in knowledge_points:
kp_lower = kp.lower()
idx = 0
while True:
idx = text_lower.find(kp_lower, idx)
if idx == -1:
break
found_kps.append({
"start": idx,
"end": idx + len(kp_lower),
"original": text[idx:idx + len(kp_lower)],
"kp": kp,
})
idx += len(kp_lower)
# 按位置排序,合并重叠区间
found_kps.sort(key=lambda x: x["start"])
if not found_kps:
return text
# 构建 textFormatRuns
for kp_info in found_kps:
fmt = {"bold": True}
if is_user_line:
fmt["foregroundColor"] = {
"red": 245, "green": 74, "blue": 69, "alpha": 255
} # #f54a45
format_runs.append({
"startIndex": kp_info["start"],
"format": fmt,
})
return {
"type": "text",
"text": text,
"textFormatRuns": format_runs,
}
def create_spreadsheet(title, token):
"""创建新的电子表格"""
resp = requests.post(
f"{BASE_URL}/sheets/v3/spreadsheets",
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
json={"title": title},
timeout=10,
)
data = resp.json()
if data.get("code") != 0:
print(f"❌ 创建表格失败: {data}", file=sys.stderr)
sys.exit(1)
spreadsheet = data["data"]["spreadsheet"]
print(json.dumps({
"token": spreadsheet["spreadsheet_token"],
"url": spreadsheet["url"],
"title": spreadsheet["title"],
}))
def create_sheet(spreadsheet_token, title, token):
"""在已有表格中创建新sheet"""
resp = requests.post(
f"{BASE_URL}/sheets/v2/spreadsheets/{spreadsheet_token}/sheets_batch_update",
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
json={
"requests": [{
"addSheet": {
"properties": {"title": title},
}
}]
},
timeout=10,
)
data = resp.json()
if data.get("code") != 0:
print(f"❌ 创建sheet失败: {data}", file=sys.stderr)
sys.exit(1)
reply = data["data"]["replies"][0]
props = reply["addSheet"]["properties"]
print(json.dumps({
"sheet_id": props["sheetId"],
"title": props["title"],
"index": props.get("index", -1),
}))
def write_data(spreadsheet_token, sheet_id, data_file, token, append=False, start_row=1):
"""写入或追加数据到表格"""
with open(data_file) as f:
data = json.load(f)
# data format: list of rows, each row is a list of column values
# Column values can be strings or objects with {text, knowledge_points, is_user}
rows = data.get("rows", [])
knowledge_points = data.get("knowledge_points", [])
# Convert rows to API format
values = []
header_written = False
for row_idx, row in enumerate(rows):
formatted_row = []
for col_idx, cell in enumerate(row):
if isinstance(cell, dict):
is_user = cell.get("is_user", False)
formatted = format_cell_value(
cell.get("text", ""),
knowledge_points,
is_user_line=is_user,
)
else:
formatted = str(cell) if cell else ""
formatted_row.append(formatted)
values.append(formatted_row)
if not values:
print("⚠️ 没有数据可写入", file=sys.stderr)
return
# Calculate range
num_rows = len(values)
num_cols = max(len(row) for row in values) if values else 1
col_letter = chr(64 + num_cols) if num_cols <= 26 else "Z"
if append:
# Append mode - use append API
resp = requests.post(
f"{BASE_URL}/sheets/v2/spreadsheets/{spreadsheet_token}/values_append",
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
json={
"valueRange": {
"range": f"{sheet_id}!A{start_row}:{col_letter}{start_row + num_rows - 1}",
"values": values,
},
"insertDataOption": "INSERT_ROWS",
},
timeout=30,
)
else:
# Overwrite mode
resp = requests.put(
f"{BASE_URL}/sheets/v2/spreadsheets/{spreadsheet_token}/values",
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
json={
"valueRange": {
"range": f"{sheet_id}!A1:{col_letter}{num_rows}",
"values": values,
},
},
timeout=30,
)
data_resp = resp.json()
if data_resp.get("code") != 0:
print(f"❌ 写入数据失败: {data_resp}", file=sys.stderr)
sys.exit(1)
print(json.dumps({
"status": "ok",
"rows": num_rows,
"columns": num_cols,
"mode": "append" if append else "overwrite",
}))
def set_styles(spreadsheet_token, sheet_id, ranges_styles, token):
"""设置单元格样式(粗体、颜色、背景色等)"""
resp = requests.put(
f"{BASE_URL}/sheets/v2/spreadsheets/{spreadsheet_token}/style",
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
json={
"appendStyle": {
"range": f"{sheet_id}!A1:H1",
"style": {
"font": {"bold": True},
"backColor": "#e8e8e8",
},
},
},
timeout=10,
)
data = resp.json()
if data.get("code") != 0:
print(f"⚠️ 设置样式失败(非致命): {data}", file=sys.stderr)
else:
print(json.dumps({"style_status": "ok"}))
def main():
parser = argparse.ArgumentParser(description="飞书电子表格剧本写入工具")
subparsers = parser.add_subparsers(dest="command", required=True)
# create command
create_parser = subparsers.add_parser("create", help="创建新的电子表格")
create_parser.add_argument("--title", required=True, help="表格标题")
create_parser.add_argument("--credential", default="xiaobian", help="凭证名称")
# create-sheet command
sheet_parser = subparsers.add_parser("create-sheet", help="在已有表格中创建新sheet")
sheet_parser.add_argument("--token", required=True, help="spreadsheet token")
sheet_parser.add_argument("--title", required=True, help="sheet标题")
sheet_parser.add_argument("--credential", default="xiaobian", help="凭证名称")
# write command
write_parser = subparsers.add_parser("write", help="写入数据(覆盖)")
write_parser.add_argument("--token", required=True, help="spreadsheet token")
write_parser.add_argument("--sheet", required=True, help="sheet ID")
write_parser.add_argument("--data", required=True, help="数据JSON文件路径")
write_parser.add_argument("--credential", default="xiaobian", help="凭证名称")
# append command
append_parser = subparsers.add_parser("append", help="追加数据")
append_parser.add_argument("--token", required=True, help="spreadsheet token")
append_parser.add_argument("--sheet", required=True, help="sheet ID")
append_parser.add_argument("--data", required=True, help="数据JSON文件路径")
append_parser.add_argument("--start-row", type=int, default=1, help="起始行号")
append_parser.add_argument("--credential", default="xiaobian", help="凭证名称")
# style command
style_parser = subparsers.add_parser("style", help="设置表头样式")
style_parser.add_argument("--token", required=True, help="spreadsheet token")
style_parser.add_argument("--sheet", required=True, help="sheet ID")
style_parser.add_argument("--credential", default="xiaobian", help="凭证名称")
args = parser.parse_args()
token = get_token(args.credential)
if args.command == "create":
create_spreadsheet(args.title, token)
elif args.command == "create-sheet":
create_sheet(args.token, args.title, token)
elif args.command == "write":
write_data(args.token, args.sheet, args.data, token, append=False)
set_styles(args.token, args.sheet, None, token)
elif args.command == "append":
write_data(args.token, args.sheet, args.data, token, append=True, start_row=args.start_row)
elif args.command == "style":
set_styles(args.token, args.sheet, None, token)
if __name__ == "__main__":
main()