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>
This commit is contained in:
2026-03-14 11:31:58 +10:30
parent ea266e9cce
commit 712063071c
170 changed files with 341 additions and 346 deletions

View File

@@ -0,0 +1,530 @@
#!/usr/bin/env python3
"""
ai-proj 与飞书项目同步脚本
功能:
- 初始化:创建飞书根目录、多维表格
- 全量同步:同步所有项目到飞书
- 增量同步:只同步新增项目
- 更新同步:更新现有项目的任务统计等信息
使用方法:
python aiproj_sync.py init # 首次初始化
python aiproj_sync.py sync # 增量同步(推荐日常使用)
python aiproj_sync.py sync-all # 全量同步
python aiproj_sync.py update # 更新统计信息
"""
import os
import sys
import json
import requests
from datetime import datetime
from typing import Optional, Dict, List, Any
# ============================================
# 配置
# ============================================
FEISHU_APP_ID = os.getenv("FEISHU_APP_ID", "cli_a9f29dca82b9dbef")
FEISHU_APP_SECRET = os.getenv("FEISHU_APP_SECRET", "sDfhjG7QT1S4gfHiMVYSygmPQPN1R2Ho")
# 默认协作者 open_id邱栋梁@智云采购)
# 应用创建的文件夹所有者是应用本身,需要显式添加用户为协作者
DEFAULT_COLLABORATOR_OPENID = "ou_43784ff7c819ac000095fb52a4c3d1c7"
AIPROJ_API_BASE = "https://ai.pipexerp.com/api/v1"
AIPROJ_TOKEN = os.getenv("AIPROJ_TOKEN", "aiproj_pk_b455c91607414c22a0f3d8f09785969f1aa2144f33f1336fbb12450ecebfdb64")
CONFIG_PATH = os.path.expanduser("~/.config/aiproj-feishu-sync.json")
# ============================================
# 飞书 API
# ============================================
class FeishuAPI:
def __init__(self):
self.token = None
def get_token(self) -> str:
"""获取 tenant_access_token"""
if self.token:
return self.token
url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"
response = requests.post(url, json={
"app_id": FEISHU_APP_ID,
"app_secret": FEISHU_APP_SECRET
})
data = response.json()
if data.get("code") == 0:
self.token = data["tenant_access_token"]
return self.token
else:
raise Exception(f"获取飞书 token 失败: {data}")
def _headers(self, content_type: bool = False) -> Dict:
headers = {"Authorization": f"Bearer {self.get_token()}"}
if content_type:
headers["Content-Type"] = "application/json"
return headers
def get_root_folder(self) -> str:
"""获取云空间根文件夹"""
url = "https://open.feishu.cn/open-apis/drive/explorer/v2/root_folder/meta"
response = requests.get(url, headers=self._headers())
data = response.json()
if data.get("code") == 0:
return data["data"]["token"]
raise Exception(f"获取根文件夹失败: {data}")
def create_folder(self, name: str, parent_token: str, add_collaborator: bool = True) -> str:
"""创建文件夹"""
url = "https://open.feishu.cn/open-apis/drive/v1/files/create_folder"
response = requests.post(url, headers=self._headers(True), json={
"name": name,
"folder_token": parent_token
})
data = response.json()
if data.get("code") == 0:
folder_token = data["data"]["token"]
# 自动添加协作者(应用创建的文件夹默认只有应用能访问)
if add_collaborator and DEFAULT_COLLABORATOR_OPENID:
self.add_collaborator(folder_token, "folder", DEFAULT_COLLABORATOR_OPENID)
return folder_token
raise Exception(f"创建文件夹失败: {data}")
def add_collaborator(self, file_token: str, file_type: str, user_open_id: str, perm: str = "full_access") -> bool:
"""添加协作者
应用通过 API 创建的文件/文件夹,所有者是应用本身。
组织内用户默认无法访问,需要显式添加为协作者。
Args:
file_token: 文件/文件夹 token
file_type: 类型 (folder, doc, sheet, bitable 等)
user_open_id: 用户 open_id
perm: 权限级别 (full_access, edit, view)
"""
url = f"https://open.feishu.cn/open-apis/drive/v1/permissions/{file_token}/members"
params = {"type": file_type, "need_notification": "false"}
payload = {
"member_type": "openid",
"member_id": user_open_id,
"perm": perm
}
response = requests.post(url, headers=self._headers(True), params=params, json=payload)
return response.json().get("code") == 0
def create_bitable(self, name: str, folder_token: str) -> Dict:
"""创建多维表格"""
url = "https://open.feishu.cn/open-apis/bitable/v1/apps"
response = requests.post(url, headers=self._headers(True), json={
"name": name,
"folder_token": folder_token
})
data = response.json()
if data.get("code") == 0:
return data["data"]["app"]
raise Exception(f"创建多维表格失败: {data}")
def get_bitable_tables(self, app_token: str) -> List[Dict]:
"""获取数据表列表"""
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables"
response = requests.get(url, headers=self._headers())
data = response.json()
if data.get("code") == 0:
return data["data"]["items"]
raise Exception(f"获取数据表失败: {data}")
def create_field(self, app_token: str, table_id: str, field: Dict) -> Optional[Dict]:
"""创建字段"""
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/fields"
response = requests.post(url, headers=self._headers(True), json=field)
data = response.json()
if data.get("code") == 0:
return data["data"]["field"]
print(f" 字段创建失败: {field.get('field_name')} - {data.get('msg')}")
return None
def update_field(self, app_token: str, table_id: str, field_id: str, updates: Dict) -> bool:
"""更新字段"""
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/fields/{field_id}"
response = requests.put(url, headers=self._headers(True), json=updates)
return response.json().get("code") == 0
def get_fields(self, app_token: str, table_id: str) -> List[Dict]:
"""获取字段列表"""
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/fields"
response = requests.get(url, headers=self._headers())
data = response.json()
if data.get("code") == 0:
return data["data"]["items"]
return []
def add_record(self, app_token: str, table_id: str, fields: Dict) -> Optional[Dict]:
"""添加记录"""
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records"
response = requests.post(url, headers=self._headers(True), json={"fields": fields})
data = response.json()
if data.get("code") == 0:
return data["data"]["record"]
print(f" 记录添加失败: {data.get('msg')}")
return None
def get_records(self, app_token: str, table_id: str, filter_str: str = None) -> List[Dict]:
"""获取记录列表"""
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records"
params = {"page_size": 500}
if filter_str:
params["filter"] = filter_str
response = requests.get(url, headers=self._headers(), params=params)
data = response.json()
if data.get("code") == 0:
return data["data"].get("items", [])
return []
def update_record(self, app_token: str, table_id: str, record_id: str, fields: Dict) -> bool:
"""更新记录"""
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/{record_id}"
response = requests.put(url, headers=self._headers(True), json={"fields": fields})
return response.json().get("code") == 0
# ============================================
# ai-proj API
# ============================================
class AIProjAPI:
@staticmethod
def get_projects() -> List[Dict]:
"""获取项目列表"""
url = f"{AIPROJ_API_BASE}/projects?page_size=100"
headers = {"Authorization": f"Bearer {AIPROJ_TOKEN}"}
response = requests.get(url, headers=headers)
data = response.json()
if data.get("success"):
return data["data"]["data"]
raise Exception(f"获取项目列表失败: {data}")
# ============================================
# 配置管理
# ============================================
def load_config() -> Optional[Dict]:
"""加载配置"""
if os.path.exists(CONFIG_PATH):
with open(CONFIG_PATH) as f:
return json.load(f)
return None
def save_config(config: Dict):
"""保存配置"""
os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
with open(CONFIG_PATH, "w") as f:
json.dump(config, f, indent=2, ensure_ascii=False)
# ============================================
# 同步逻辑
# ============================================
def init():
"""初始化:创建飞书目录结构"""
print("=" * 50)
print("飞书 ai-proj 项目同步 - 初始化")
print("=" * 50)
if load_config():
print("\n已存在配置文件,是否重新初始化?(y/N)")
if input().lower() != 'y':
print("取消初始化")
return
feishu = FeishuAPI()
# 1. 获取根目录
print("\n[1/5] 获取云空间根目录...")
space_root = feishu.get_root_folder()
print(f" 根目录: {space_root}")
# 2. 创建 ai-proj 文件夹
print("\n[2/5] 创建 ai-proj 根文件夹...")
root_folder = feishu.create_folder("ai-proj", space_root)
print(f" 创建成功: {root_folder}")
# 3. 创建多维表格
print("\n[3/5] 创建项目目录多维表格...")
bitable = feishu.create_bitable("项目目录", root_folder)
app_token = bitable["app_token"]
print(f" 创建成功: {app_token}")
tables = feishu.get_bitable_tables(app_token)
table_id = tables[0]["table_id"]
# 4. 创建字段
print("\n[4/5] 创建数据表字段...")
# 先修改第一列名称
fields = feishu.get_fields(app_token, table_id)
if fields:
feishu.update_field(app_token, table_id, fields[0]["field_id"], {
"field_name": "项目名称",
"type": fields[0]["type"]
})
print(" 修改字段: 项目名称")
# 创建其他字段
field_definitions = [
{"field_name": "项目ID", "type": 2},
{"field_name": "项目编号", "type": 1},
{"field_name": "飞书文件夹", "type": 15},
{"field_name": "状态", "type": 3, "property": {"options": [
{"name": "active", "color": 0},
{"name": "on_hold", "color": 1},
{"name": "planning", "color": 2},
{"name": "completed", "color": 3},
{"name": "archived", "color": 4}
]}},
{"field_name": "优先级", "type": 3, "property": {"options": [
{"name": "high", "color": 0},
{"name": "medium", "color": 1},
{"name": "low", "color": 2}
]}},
{"field_name": "所属企业", "type": 1},
{"field_name": "任务总数", "type": 2},
{"field_name": "描述", "type": 1},
{"field_name": "创建时间", "type": 5},
{"field_name": "最后同步", "type": 5},
]
for field in field_definitions:
if feishu.create_field(app_token, table_id, field):
print(f" 创建字段: {field['field_name']}")
# 5. 保存配置
print("\n[5/5] 保存配置...")
config = {
"root_folder_token": root_folder,
"bitable_app_token": app_token,
"bitable_table_id": table_id,
"created_at": datetime.now().isoformat(),
"synced_projects": {}
}
save_config(config)
print("\n" + "=" * 50)
print("初始化完成!")
print("=" * 50)
print(f"\n根文件夹: https://feishu.cn/drive/folder/{root_folder}")
print(f"多维表格: https://feishu.cn/base/{app_token}")
print(f"\n配置文件: {CONFIG_PATH}")
print("\n运行 'python aiproj_sync.py sync' 同步项目")
def sync(full: bool = False):
"""同步项目"""
config = load_config()
if not config:
print("未找到配置,请先运行 init")
return
print("=" * 50)
print(f"飞书 ai-proj 项目同步 - {'全量' if full else '增量'}同步")
print("=" * 50)
feishu = FeishuAPI()
app_token = config["bitable_app_token"]
table_id = config["bitable_table_id"]
root_folder = config["root_folder_token"]
synced = config.get("synced_projects", {})
# 获取 ai-proj 项目
print("\n获取 ai-proj 项目列表...")
projects = AIProjAPI.get_projects()
print(f"{len(projects)} 个项目")
# 筛选需要同步的项目
if full:
to_sync = projects
else:
to_sync = [p for p in projects if str(p["id"]) not in synced]
if not to_sync:
print("\n没有新项目需要同步")
return
print(f"\n需要同步 {len(to_sync)} 个项目...")
success = 0
for project in to_sync:
project_id = str(project["id"])
project_name = project["name"]
folder_name = f"{project_name}_{project_id}"
print(f"\n 处理: {project_name} (ID: {project_id})")
# 创建文件夹(如果不存在)
folder_token = synced.get(project_id, {}).get("folder_token")
if not folder_token:
try:
folder_token = feishu.create_folder(folder_name, root_folder)
print(f" 创建文件夹: {folder_token}")
except Exception as e:
print(f" 文件夹创建失败: {e}")
folder_token = ""
folder_url = f"https://feishu.cn/drive/folder/{folder_token}" if folder_token else ""
# 添加/更新记录
record_fields = {
"项目名称": project_name,
"项目ID": int(project_id),
"项目编号": project.get("project_number", ""),
"飞书文件夹": {"link": folder_url, "text": folder_name} if folder_url else None,
"状态": project.get("status", "active"),
"优先级": project.get("priority", "medium"),
"所属企业": project.get("company_name", ""),
"任务总数": project.get("task_count", 0),
"描述": (project.get("description", "") or "")[:500],
"创建时间": int(datetime.fromisoformat(
project["created_at"].replace("Z", "+00:00")
).timestamp() * 1000),
"最后同步": int(datetime.now().timestamp() * 1000)
}
record_id = synced.get(project_id, {}).get("record_id")
if record_id and not full:
# 更新现有记录
if feishu.update_record(app_token, table_id, record_id, record_fields):
print(f" 更新记录成功")
success += 1
else:
# 添加新记录
record = feishu.add_record(app_token, table_id, record_fields)
if record:
record_id = record["record_id"]
print(f" 添加记录成功")
success += 1
# 更新同步记录
synced[project_id] = {
"folder_token": folder_token,
"folder_url": folder_url,
"record_id": record_id,
"synced_at": datetime.now().isoformat()
}
# 保存配置
config["synced_projects"] = synced
config["last_sync"] = datetime.now().isoformat()
save_config(config)
print("\n" + "=" * 50)
print(f"同步完成: {success}/{len(to_sync)}")
print("=" * 50)
print(f"\n多维表格: https://feishu.cn/base/{app_token}")
def update_stats():
"""更新任务统计信息"""
config = load_config()
if not config:
print("未找到配置,请先运行 init")
return
print("=" * 50)
print("飞书 ai-proj 项目同步 - 更新统计")
print("=" * 50)
feishu = FeishuAPI()
app_token = config["bitable_app_token"]
table_id = config["bitable_table_id"]
synced = config.get("synced_projects", {})
# 获取最新项目数据
print("\n获取 ai-proj 项目列表...")
projects = AIProjAPI.get_projects()
project_map = {str(p["id"]): p for p in projects}
updated = 0
for project_id, sync_info in synced.items():
record_id = sync_info.get("record_id")
if not record_id:
continue
project = project_map.get(project_id)
if not project:
continue
# 只更新统计字段
update_fields = {
"任务总数": project.get("task_count", 0),
"状态": project.get("status", "active"),
"最后同步": int(datetime.now().timestamp() * 1000)
}
if feishu.update_record(app_token, table_id, record_id, update_fields):
print(f" 更新: {project['name']} - 任务数: {project.get('task_count', 0)}")
updated += 1
config["last_sync"] = datetime.now().isoformat()
save_config(config)
print(f"\n更新完成: {updated} 个项目")
def show_status():
"""显示同步状态"""
config = load_config()
if not config:
print("未初始化,请先运行: python aiproj_sync.py init")
return
print("=" * 50)
print("ai-proj 飞书同步状态")
print("=" * 50)
print(f"\n根文件夹: https://feishu.cn/drive/folder/{config['root_folder_token']}")
print(f"多维表格: https://feishu.cn/base/{config['bitable_app_token']}")
print(f"已同步项目: {len(config.get('synced_projects', {}))}")
print(f"上次同步: {config.get('last_sync', '从未')}")
print(f"配置文件: {CONFIG_PATH}")
# ============================================
# 主入口
# ============================================
def main():
if len(sys.argv) < 2:
print("用法: python aiproj_sync.py <command>")
print("\n命令:")
print(" init 首次初始化(创建目录和多维表格)")
print(" sync 增量同步(只同步新项目)")
print(" sync-all 全量同步(同步所有项目)")
print(" update 更新统计信息")
print(" status 查看同步状态")
return
command = sys.argv[1]
if command == "init":
init()
elif command == "sync":
sync(full=False)
elif command == "sync-all":
sync(full=True)
elif command == "update":
update_stats()
elif command == "status":
show_status()
else:
print(f"未知命令: {command}")
if __name__ == "__main__":
main()