#!/usr/bin/env python3 """ 更新飞书文档:ai-proj 项目可见性手册 """ import requests import os 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}) return response.json().get("code") == 0 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(), 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(), 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 generate_visibility_diagram(): """生成可见性示意图""" output_path = "/tmp/visibility_diagram.png" img = Image.new('RGB', (800, 400), color='#ffffff') draw = ImageDraw.Draw(img) # 标题 draw.rectangle([0, 0, 800, 50], fill='#1890ff') draw.text((400, 25), "项目可见性示意图", fill='white', anchor='mm') # 私有项目 draw.rectangle([50, 80, 370, 350], fill='#fff7e6', outline='#fa8c16', width=2) draw.text((210, 100), "🔒 私有项目 (private)", fill='#fa8c16', anchor='mm') draw.text((210, 140), "仅创建者可见", fill='#666666', anchor='mm') # 私有项目内的人员图标 draw.ellipse([180, 180, 240, 240], fill='#fa8c16') draw.text((210, 210), "👤", fill='white', anchor='mm') draw.text((210, 270), "项目所有者", fill='#333333', anchor='mm') draw.text((210, 300), "管理员", fill='#999999', anchor='mm') # 企业项目 draw.rectangle([430, 80, 750, 350], fill='#e6f7ff', outline='#1890ff', width=2) draw.text((590, 100), "👥 企业项目 (enterprise)", fill='#1890ff', anchor='mm') draw.text((590, 140), "企业内所有成员可见", fill='#666666', anchor='mm') # 企业项目内的人员图标组 positions = [(520, 190), (590, 190), (660, 190), (555, 250), (625, 250)] for x, y in positions: draw.ellipse([x-20, y-20, x+20, y+20], fill='#1890ff') draw.text((x, y), "👤", fill='white', anchor='mm') draw.text((590, 300), "企业所有成员", fill='#333333', anchor='mm') draw.text((590, 330), "管理员", fill='#999999', anchor='mm') # 箭头 draw.text((400, 200), "→", fill='#333333', anchor='mm') 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', (600, 200), color='#fafafa') draw = ImageDraw.Draw(img) # 标题 draw.text((20, 20), "项目可见性选择", fill='#333333') # Radio Group 模拟 # 私有项目选项 draw.rectangle([20, 60, 280, 100], fill='#fff7e6', outline='#fa8c16', width=2) draw.ellipse([30, 70, 50, 90], fill='#fa8c16') draw.text((60, 80), "🔒 私有项目", fill='#fa8c16', anchor='lm') # 企业项目选项 draw.rectangle([300, 60, 560, 100], fill='#ffffff', outline='#d9d9d9', width=1) draw.ellipse([310, 70, 330, 90], outline='#d9d9d9', width=1) draw.text((340, 80), "👥 企业项目", fill='#666666', anchor='lm') # 说明文字 draw.text((20, 130), "私有项目仅创建者可见,企业项目对企业内所有成员可见", fill='#999999') # 默认标签 draw.rectangle([200, 65, 270, 85], fill='#52c41a') draw.text((235, 75), "默认", fill='white', anchor='mm') 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="json"): return { "block_type": 14, "code": { "elements": [{"text_run": {"content": content}}], "language": 1 if language == "json" else 0 } } 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: 追加手册内容 ---") print("\n--- Step 3: 创建手册内容 ---") # 定义手册内容 manual_blocks = [ heading1("ai-proj 项目可见性手册"), text_block("本手册介绍 ai-proj 系统中项目可见性功能的使用方法。"), divider(), heading2("1. 功能概述"), text_block("项目可见性用于控制谁可以查看和访问您的项目。ai-proj 支持两种可见性级别:"), bullet("私有项目 (private):仅项目创建者和管理员可见"), bullet("企业项目 (enterprise):企业内所有成员可见"), text_block("新创建的项目默认为「私有项目」,保护您的隐私。"), divider(), heading2("2. 可见性对比"), ] # 创建第一批内容块 created = create_blocks(DOCUMENT_ID, DOCUMENT_ID, manual_blocks) print(f" 创建了 {len(created)} 个内容块") # Step 4: 插入可见性示意图 print("\n--- Step 4: 插入可见性示意图 ---") img_block_id = create_image_block(DOCUMENT_ID, DOCUMENT_ID) print(f" 图片块: {img_block_id}") file_token = upload_image(diagram_path, img_block_id) print(f" file_token: {file_token}") bind_image(DOCUMENT_ID, img_block_id, file_token) print(" 图片绑定成功!") # Step 5: 继续添加内容 print("\n--- Step 5: 添加更多内容 ---") more_blocks = [ divider(), heading2("3. 设置项目可见性"), heading3("3.1 创建项目时设置"), ordered("点击「创建项目」按钮"), ordered("在弹出的表单中找到「项目可见性」选项"), ordered("选择「私有项目」或「企业项目」"), ordered("填写其他信息后点击「确定」"), ] create_blocks(DOCUMENT_ID, DOCUMENT_ID, more_blocks) # Step 6: 插入 UI 示意图 print("\n--- Step 6: 插入 UI 示意图 ---") ui_block_id = create_image_block(DOCUMENT_ID, DOCUMENT_ID) ui_file_token = upload_image(ui_path, ui_block_id) bind_image(DOCUMENT_ID, ui_block_id, ui_file_token) print(" UI 示意图上传成功!") # Step 7: 添加剩余内容 print("\n--- Step 7: 添加剩余内容 ---") final_blocks = [ 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, final_blocks) print("\n" + "=" * 60) print("文档更新完成!") print(f"\n文档地址: https://zhiyuncai.feishu.cn/docx/{DOCUMENT_ID}") print("=" * 60) if __name__ == "__main__": main()