Files
ai-proj-helper/skills-integration/feishu-plugin/rebuild_visibility_manual.py
John Qiu 712063071c refactor: 通用技能按类别拆分为独立目录
skills/ → skills-dev(9), skills-req(10), skills-ops(4),
skills-integration(8), skills-biz(4), skills-workflow(7)

generate-marketplace.py 改为自动扫描所有 skills-* 目录。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 11:31:58 +10:30

450 lines
16 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
"""
重建飞书文档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()