refactor: 合并 claude-marketplace,重构目录结构为单一仓库
- 重命名 plugins/ → skills/,个人插件迁移到 skills-personal/(gitignore) - 更新 generate-marketplace.py 支持 config 读取和 skills-personal 扫描 - 新增 claude-config.yaml(技能启用/禁用 + MCP 配置) - 新增 init.sh(交互式 MCP 初始化,支持 stdio/SSE 模式) - 新增 CLAUDE.md 项目说明 - 重写 README.md 反映新结构 - 删除过时脚本:PUSH.sh、generate-marketplace.sh、convert-skills.sh Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
375
skills/feishu-plugin/update_visibility_manual.py
Normal file
375
skills/feishu-plugin/update_visibility_manual.py
Normal file
@@ -0,0 +1,375 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user