Files
ai-proj-helper/skills-integration/feishu-plugin/update_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

376 lines
12 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
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()