move claude-marketplace to ai-proj-helper
This commit is contained in:
423
plugins/feishu-plugin/create_visibility_manual.py
Normal file
423
plugins/feishu-plugin/create_visibility_manual.py
Normal file
@@ -0,0 +1,423 @@
|
||||
#!/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"
|
||||
|
||||
_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 create_document(title: str):
|
||||
"""创建文档"""
|
||||
url = f"{BASE_URL}/docx/v1/documents"
|
||||
response = requests.post(url, headers=headers(), json={"title": title})
|
||||
data = response.json()
|
||||
if data.get("code") != 0:
|
||||
raise Exception(f"创建文档失败: {data}")
|
||||
doc = data["data"]["document"]
|
||||
document_id = doc["document_id"]
|
||||
print(f"[OK] 文档创建成功: {document_id}")
|
||||
return document_id
|
||||
|
||||
|
||||
def set_document_permission(document_id: str):
|
||||
"""设置文档权限为组织内可编辑"""
|
||||
url = f"{BASE_URL}/drive/v1/permissions/{document_id}/public"
|
||||
payload = {
|
||||
"external_access_entity": "open",
|
||||
"security_entity": "anyone_can_view",
|
||||
"comment_entity": "anyone_can_view",
|
||||
"share_entity": "anyone",
|
||||
"link_share_entity": "tenant_editable",
|
||||
"invite_external": False
|
||||
}
|
||||
response = requests.patch(url, headers=headers(), params={"type": "docx"}, json=payload)
|
||||
result = response.json()
|
||||
if result.get("code") == 0:
|
||||
print("[OK] 权限设置成功: 组织内可编辑")
|
||||
return True
|
||||
else:
|
||||
print(f"[WARN] 权限设置: {result.get('msg')}")
|
||||
return False
|
||||
|
||||
|
||||
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 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}")
|
||||
|
||||
# Step 2: 上传图片
|
||||
file_token = upload_image(file_path, block_id)
|
||||
print(f" file_token: {file_token}")
|
||||
|
||||
# Step 3: 绑定图片到块
|
||||
bind_image(document_id, block_id, file_token)
|
||||
print(f" 绑定成功!")
|
||||
|
||||
# 等待处理
|
||||
time.sleep(0.5)
|
||||
|
||||
return block_id, file_token
|
||||
|
||||
|
||||
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: 创建新文档 ---")
|
||||
doc_id = create_document("ai-proj 项目可见性手册")
|
||||
set_document_permission(doc_id)
|
||||
|
||||
# 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(doc_id, doc_id, blocks_part1)
|
||||
print(" 第一部分内容创建完成")
|
||||
|
||||
# 插入可见性示意图
|
||||
print("\n--- Step 4: 插入可见性示意图 ---")
|
||||
insert_image(doc_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(doc_id, doc_id, blocks_part2)
|
||||
print(" 设置说明创建完成")
|
||||
|
||||
# 插入 UI 示意图
|
||||
print("\n--- Step 6: 插入 UI 示意图 ---")
|
||||
insert_image(doc_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(doc_id, doc_id, blocks_part3)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("文档创建完成!")
|
||||
print(f"\n文档地址: https://zhiyuncai.feishu.cn/docx/{doc_id}")
|
||||
print("=" * 60)
|
||||
|
||||
# 验证图片
|
||||
print("\n--- 验证图片状态 ---")
|
||||
time.sleep(2) # 等待服务器处理
|
||||
blocks = get_document_blocks(doc_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", "")
|
||||
width = image_data.get("width", 0)
|
||||
height = image_data.get("height", 0)
|
||||
status = "✅ 有效" if token else "❌ 空"
|
||||
print(f"图片 #{image_count}: {status} (token: {token[:15]}..., 尺寸: {width}x{height})")
|
||||
|
||||
if image_count == 0:
|
||||
print("⚠️ 警告:文档中没有图片块")
|
||||
elif image_count == 2:
|
||||
print(f"\n✅ 成功上传 {image_count} 张图片")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user