#!/usr/bin/env python3 """ 重建飞书文档:ai-proj 项目可见性手册 清理现有内容后重新生成 """ import requests import os import time from datetime import datetime, timedelta from PIL import Image, ImageDraw, ImageFont ZHIYUN_APP_ID = "cli_a9f29dca82b9dbef" ZHIYUN_APP_SECRET = "sDfhjG7QT1S4gfHiMVYSygmPQPN1R2Ho" BASE_URL = "https://open.feishu.cn/open-apis" # 目标文档 DOCUMENT_ID = "Eqt2dpcpToVDxExFmhicqFa9nJf" _token = None _token_expires = None def get_token(): global _token, _token_expires if _token and _token_expires and datetime.now() < _token_expires: return _token url = f"{BASE_URL}/auth/v3/tenant_access_token/internal" response = requests.post(url, json={ "app_id": ZHIYUN_APP_ID, "app_secret": ZHIYUN_APP_SECRET }) data = response.json() if data.get("code") != 0: raise Exception(f"获取 token 失败: {data}") _token = data["tenant_access_token"] _token_expires = datetime.now() + timedelta(seconds=data.get("expire", 7200) - 60) return _token def headers(): return { "Authorization": f"Bearer {get_token()}", "Content-Type": "application/json" } def get_document_blocks(document_id: str): """获取文档所有块""" url = f"{BASE_URL}/docx/v1/documents/{document_id}/blocks" response = requests.get(url, headers=headers()) data = response.json() if data.get("code") != 0: raise Exception(f"获取块失败: {data}") return data["data"].get("items", []) def delete_block(document_id: str, block_id: str): """删除块""" url = f"{BASE_URL}/docx/v1/documents/{document_id}/blocks/{block_id}" response = requests.delete(url, headers=headers(), params={"document_revision_id": -1}) result = response.json() return result.get("code") == 0 def clear_document(document_id: str): """清空文档内容(保留根块)""" print("正在清空文档...") blocks = get_document_blocks(document_id) # 找出所有子块(排除根块本身) child_blocks = [b for b in blocks if b.get("block_id") != document_id and b.get("parent_id") == document_id] print(f" 发现 {len(child_blocks)} 个顶级子块需要删除") # 从后向前删除,避免索引问题 for block in reversed(child_blocks): block_id = block.get("block_id") if delete_block(document_id, block_id): print(f" 删除: {block_id[:20]}...") else: print(f" 删除失败: {block_id[:20]}...") time.sleep(0.1) # 避免请求过快 print(" 文档清空完成") def create_blocks(document_id: str, parent_id: str, blocks: list): """创建内容块""" url = f"{BASE_URL}/docx/v1/documents/{document_id}/blocks/{parent_id}/children" payload = {"children": blocks} response = requests.post(url, headers=headers(), params={"document_revision_id": -1}, json=payload) data = response.json() if data.get("code") != 0: raise Exception(f"创建块失败: {data}") return data["data"].get("children", []) def create_image_block(document_id: str, parent_id: str): """创建空图片块""" url = f"{BASE_URL}/docx/v1/documents/{document_id}/blocks/{parent_id}/children" payload = { "children": [{ "block_type": 27, "image": {} }] } response = requests.post(url, headers=headers(), params={"document_revision_id": -1}, json=payload) data = response.json() if data.get("code") != 0: raise Exception(f"创建图片块失败: {data}") return data["data"]["children"][0]["block_id"] def upload_image(file_path: str, block_id: str): """上传图片""" url = f"{BASE_URL}/drive/v1/medias/upload_all" file_name = os.path.basename(file_path) file_size = os.path.getsize(file_path) with open(file_path, 'rb') as f: files = {'file': (file_name, f, 'image/png')} data = { 'file_name': file_name, 'parent_type': 'docx_image', 'parent_node': block_id, 'size': str(file_size) } response = requests.post( url, headers={"Authorization": f"Bearer {get_token()}"}, files=files, data=data ) result = response.json() if result.get("code") != 0: raise Exception(f"上传失败: {result}") return result["data"]["file_token"] def bind_image(document_id: str, block_id: str, file_token: str): """绑定图片到图片块""" url = f"{BASE_URL}/docx/v1/documents/{document_id}/blocks/{block_id}" payload = {"replace_image": {"token": file_token}} response = requests.patch(url, headers=headers(), params={"document_revision_id": -1}, json=payload) data = response.json() if data.get("code") != 0: raise Exception(f"绑定失败: {data}") return True def insert_image(document_id: str, file_path: str, description: str = ""): """插入图片的完整流程""" print(f" 插入图片: {description}") # Step 1: 创建空图片块 block_id = create_image_block(document_id, document_id) print(f" block_id: {block_id[:20]}...") # Step 2: 上传图片 file_token = upload_image(file_path, block_id) print(f" file_token: {file_token[:20]}...") # Step 3: 绑定图片 bind_image(document_id, block_id, file_token) print(f" 绑定成功!") return block_id def generate_visibility_diagram(): """生成可见性示意图""" output_path = "/tmp/visibility_diagram.png" img = Image.new('RGB', (800, 400), color='#ffffff') draw = ImageDraw.Draw(img) # 尝试加载字体,如果失败则使用默认 try: font_large = ImageFont.truetype("/System/Library/Fonts/PingFang.ttc", 24) font_medium = ImageFont.truetype("/System/Library/Fonts/PingFang.ttc", 18) font_small = ImageFont.truetype("/System/Library/Fonts/PingFang.ttc", 14) except: font_large = ImageFont.load_default() font_medium = ImageFont.load_default() font_small = ImageFont.load_default() # 标题背景 draw.rectangle([0, 0, 800, 60], fill='#1890ff') draw.text((400, 30), "ai-proj 项目可见性示意图", fill='white', anchor='mm', font=font_large) # 私有项目区域 draw.rectangle([40, 80, 380, 370], fill='#fff7e6', outline='#fa8c16', width=3) draw.text((210, 110), "🔒 私有项目", fill='#fa8c16', anchor='mm', font=font_medium) draw.text((210, 140), "(private)", fill='#d48806', anchor='mm', font=font_small) # 私有项目说明 draw.text((210, 180), "仅项目创建者可见", fill='#666666', anchor='mm', font=font_small) # 私有项目图标 draw.ellipse([175, 210, 245, 280], fill='#fa8c16') draw.text((210, 245), "👤", fill='white', anchor='mm', font=font_large) draw.text((210, 310), "项目所有者", fill='#333333', anchor='mm', font=font_small) draw.text((210, 335), "管理员", fill='#999999', anchor='mm', font=font_small) # 企业项目区域 draw.rectangle([420, 80, 760, 370], fill='#e6f7ff', outline='#1890ff', width=3) draw.text((590, 110), "👥 企业项目", fill='#1890ff', anchor='mm', font=font_medium) draw.text((590, 140), "(enterprise)", fill='#096dd9', anchor='mm', font=font_small) # 企业项目说明 draw.text((590, 180), "企业内所有成员可见", fill='#666666', anchor='mm', font=font_small) # 企业项目图标组 positions = [(520, 230), (590, 230), (660, 230), (555, 290), (625, 290)] for x, y in positions: draw.ellipse([x-25, y-25, x+25, y+25], fill='#1890ff') draw.text((x, y), "👤", fill='white', anchor='mm', font=font_medium) draw.text((590, 340), "企业所有成员", fill='#333333', anchor='mm', font=font_small) # 中间箭头 draw.text((400, 225), "→", fill='#333333', anchor='mm', font=font_large) img.save(output_path) print(f"[OK] 生成可见性示意图: {output_path}") return output_path def generate_ui_screenshot(): """生成 UI 示意图""" output_path = "/tmp/visibility_ui.png" img = Image.new('RGB', (700, 250), color='#fafafa') draw = ImageDraw.Draw(img) try: font_medium = ImageFont.truetype("/System/Library/Fonts/PingFang.ttc", 16) font_small = ImageFont.truetype("/System/Library/Fonts/PingFang.ttc", 12) except: font_medium = ImageFont.load_default() font_small = ImageFont.load_default() # 标题 draw.text((30, 25), "创建项目 - 可见性选择", fill='#333333', font=font_medium) # 私有项目选项(选中状态) draw.rectangle([30, 70, 320, 140], fill='#fff7e6', outline='#fa8c16', width=2) draw.ellipse([45, 95, 65, 115], fill='#fa8c16') draw.text((80, 90), "🔒 私有项目", fill='#fa8c16', font=font_medium) draw.text((80, 115), "仅创建者可见", fill='#999999', font=font_small) # 默认标签 draw.rectangle([250, 85, 310, 110], fill='#52c41a') draw.text((280, 97), "默认", fill='white', anchor='mm', font=font_small) # 企业项目选项(未选中状态) draw.rectangle([350, 70, 640, 140], fill='#ffffff', outline='#d9d9d9', width=1) draw.ellipse([365, 95, 385, 115], outline='#d9d9d9', width=2) draw.text((400, 90), "👥 企业项目", fill='#666666', font=font_medium) draw.text((400, 115), "企业内所有成员可见", fill='#999999', font=font_small) # 底部说明 draw.rectangle([30, 170, 670, 220], fill='#f0f5ff', outline='#adc6ff', width=1) draw.text((50, 185), "💡 提示:新创建的项目默认为私有项目。", fill='#1890ff', font=font_small) draw.text((50, 205), "设置为企业项目后,项目需要先关联企业才能被企业成员访问。", fill='#666666', font=font_small) img.save(output_path) print(f"[OK] 生成 UI 示意图: {output_path}") return output_path # 文档内容块定义 def heading1(text): return { "block_type": 3, "heading1": {"elements": [{"text_run": {"content": text}}]} } def heading2(text): return { "block_type": 4, "heading2": {"elements": [{"text_run": {"content": text}}]} } def heading3(text): return { "block_type": 5, "heading3": {"elements": [{"text_run": {"content": text}}]} } def text_block(content): return { "block_type": 2, "text": {"elements": [{"text_run": {"content": content}}]} } def bullet(content): return { "block_type": 12, "bullet": {"elements": [{"text_run": {"content": content}}]} } def ordered(content): return { "block_type": 13, "ordered": {"elements": [{"text_run": {"content": content}}]} } def code_block(content, language=1): return { "block_type": 14, "code": { "elements": [{"text_run": {"content": content}}], "language": language } } def divider(): return {"block_type": 22, "divider": {}} def main(): print("\n" + "#" * 60) print("# 重建飞书文档:ai-proj 项目可见性手册") print("#" * 60) # Step 1: 生成示意图 print("\n--- Step 1: 生成示意图 ---") diagram_path = generate_visibility_diagram() ui_path = generate_ui_screenshot() # Step 2: 清空文档 print("\n--- Step 2: 清空文档 ---") clear_document(DOCUMENT_ID) time.sleep(1) # 等待清空完成 # Step 3: 创建手册内容 print("\n--- Step 3: 创建手册内容 ---") # 第一部分:标题和概述 blocks_part1 = [ heading1("ai-proj 项目可见性手册"), text_block("本手册介绍 ai-proj 系统中项目可见性功能的使用方法。"), divider(), heading2("1. 功能概述"), text_block("项目可见性用于控制谁可以查看和访问您的项目。ai-proj 支持两种可见性级别:"), bullet("私有项目 (private):仅项目创建者和管理员可见"), bullet("企业项目 (enterprise):企业内所有成员可见"), text_block("新创建的项目默认为「私有项目」,保护您的隐私。"), divider(), heading2("2. 可见性对比"), text_block("下图展示了两种可见性的区别:"), ] create_blocks(DOCUMENT_ID, DOCUMENT_ID, blocks_part1) print(" 第一部分内容创建完成") # 插入可见性示意图 print("\n--- Step 4: 插入可见性示意图 ---") insert_image(DOCUMENT_ID, diagram_path, "可见性示意图") # 第二部分:设置方法 print("\n--- Step 5: 添加设置说明 ---") blocks_part2 = [ divider(), heading2("3. 设置项目可见性"), heading3("3.1 创建项目时设置"), text_block("在创建项目时,您可以选择项目的可见性:"), ordered("点击「创建项目」按钮"), ordered("在弹出的表单中找到「项目可见性」选项"), ordered("选择「私有项目」或「企业项目」"), ordered("填写其他信息后点击「确定」"), text_block("UI 界面示意:"), ] create_blocks(DOCUMENT_ID, DOCUMENT_ID, blocks_part2) print(" 设置说明创建完成") # 插入 UI 示意图 print("\n--- Step 6: 插入 UI 示意图 ---") insert_image(DOCUMENT_ID, ui_path, "UI 示意图") # 第三部分:修改方法和规则 print("\n--- Step 7: 添加剩余内容 ---") blocks_part3 = [ heading3("3.2 修改已有项目"), ordered("在项目列表中找到目标项目"), ordered("点击项目卡片上的「编辑」按钮"), ordered("在编辑页面修改「项目可见性」"), ordered("点击「保存」"), divider(), heading2("4. 可见性规则"), bullet("私有项目:仅所有者和超级管理员可以查看"), bullet("企业项目:需要项目已关联企业,企业内所有成员可以查看"), bullet("只有项目所有者或管理员可以修改可见性"), bullet("设置为「企业项目」需要项目已关联企业"), divider(), heading2("5. API 参考"), text_block("修改项目可见性的 API:"), code_block('''PATCH /api/v1/projects/:id/visibility 请求体: { "visibility": "enterprise" } 可选值: "private" | "enterprise"'''), divider(), heading2("6. 注意事项"), bullet("新项目默认为私有,请根据需要调整可见性"), bullet("将私有项目改为企业项目后,企业所有成员都能看到"), bullet("将企业项目改为私有后,其他成员将无法访问"), bullet("项目成员权限不受可见性影响,被添加为成员的用户始终可以访问"), divider(), text_block(f"最后更新:{datetime.now().strftime('%Y-%m-%d %H:%M')}"), ] create_blocks(DOCUMENT_ID, DOCUMENT_ID, blocks_part3) print("\n" + "=" * 60) print("文档重建完成!") print(f"\n文档地址: https://zhiyuncai.feishu.cn/docx/{DOCUMENT_ID}") print("=" * 60) # 验证图片 print("\n--- 验证图片状态 ---") blocks = get_document_blocks(DOCUMENT_ID) image_count = 0 for block in blocks: if block.get("block_type") == 27: image_count += 1 image_data = block.get("image", {}) token = image_data.get("token", "") print(f"图片 #{image_count}: token={'有效' if token else '空'} ({token[:20] if token else 'N/A'}...)") if image_count == 0: print("警告:文档中没有图片块") if __name__ == "__main__": main()