技能备份 - 2026-04-15 (40个技能)

This commit is contained in:
root
2026-04-15 18:53:15 +08:00
parent f62f14814f
commit c65fce24e4
791 changed files with 190773 additions and 0 deletions
@@ -0,0 +1,536 @@
#!/usr/bin/env python3
"""
Feishu Smart Doc Writer - 改进版
自动分块写入 + 自动转移所有权 + 自动更新索引
"""
import re
import time
import json
import asyncio
from typing import List, Optional
from dataclasses import dataclass
# 导入索引管理器
try:
from .index_manager import IndexManager, add_doc_to_index
except ImportError:
from index_manager import IndexManager, add_doc_to_index
@dataclass
class ChunkConfig:
"""分块配置"""
chunk_size: int = 2000 # 每块最大字符数
max_retries: int = 3 # 最大重试次数
retry_delay: float = 1.0 # 重试间隔(秒)
show_progress: bool = True # 显示进度
convert_tables: bool = True # 转换表格为文本
class ContentChunker:
"""内容分块器"""
def __init__(self, config: ChunkConfig = None):
self.config = config or ChunkConfig()
def chunk_content(self, content: str) -> List[str]:
"""
将长内容分割成多个小块
策略:按段落分割,如果段落超过限制,按句子分割
"""
chunks = []
current_chunk = ""
# 先处理表格
if self.config.convert_tables:
content = self._convert_tables(content)
# 按段落分割
paragraphs = self._split_paragraphs(content)
for para in paragraphs:
# 如果当前块加上新段落会超限
if len(current_chunk) + len(para) > self.config.chunk_size:
# 保存当前块
if current_chunk.strip():
chunks.append(current_chunk.strip())
# 如果单个段落就超限,需要进一步分割
if len(para) > self.config.chunk_size:
sub_chunks = self._split_large_paragraph(para)
chunks.extend(sub_chunks)
current_chunk = ""
else:
current_chunk = para
else:
current_chunk += "\n\n" + para if current_chunk else para
# 保存最后一块
if current_chunk.strip():
chunks.append(current_chunk.strip())
return chunks
def _convert_tables(self, content: str) -> str:
"""将Markdown表格转换为文本列表"""
table_pattern = r'\|[^\n]+\|\n\|[-:| ]+\|\n((?:\|[^\n]+\|\n)+)'
def convert_table(match):
table_text = match.group(0)
lines = table_text.strip().split('\n')
# 提取表头
header = [cell.strip() for cell in lines[0].split('|')[1:-1]]
# 提取数据行
result = ["【表格内容】"]
for line in lines[2:]: # 跳过表头和分隔线
cells = [cell.strip() for cell in line.split('|')[1:-1]]
if cells and any(cells): # 确保不是空行
row_text = ", ".join([f"{h}: {c}" for h, c in zip(header, cells)])
result.append(f"- {row_text}")
return "\n".join(result)
return re.sub(table_pattern, convert_table, content)
def _split_paragraphs(self, content: str) -> List[str]:
"""按段落分割,保留标题结构"""
lines = content.split('\n')
paragraphs = []
current_para = ""
for line in lines:
stripped = line.strip()
if not stripped:
if current_para.strip():
paragraphs.append(current_para.strip())
current_para = ""
continue
# 如果是标题,单独成段
if stripped.startswith('#'):
if current_para.strip():
paragraphs.append(current_para.strip())
current_para = ""
paragraphs.append(stripped)
else:
current_para += line + "\n"
if current_para.strip():
paragraphs.append(current_para.strip())
return paragraphs
def _split_large_paragraph(self, para: str) -> List[str]:
"""分割大段落(按句子)"""
chunks = []
sentences = re.split(r'([。!?.\n])', para)
current = ""
for i in range(0, len(sentences), 2):
sentence = sentences[i]
if i + 1 < len(sentences):
sentence += sentences[i + 1] # 加上标点
if len(current) + len(sentence) > self.config.chunk_size:
if current.strip():
chunks.append(current.strip())
current = sentence
else:
current += sentence
if current.strip():
chunks.append(current.strip())
return chunks
class FeishuDocWriter:
"""
飞书文档智能写入器
使用 OpenClaw 官方工具调用方式
"""
def __init__(self, ctx=None, config: ChunkConfig = None):
"""
初始化
Args:
ctx: OpenClaw 上下文对象(在 Skill 中传入)
config: 分块配置
"""
self.ctx = ctx
self.config = config or ChunkConfig()
self.chunker = ContentChunker(config)
async def write_document(self, title: str, content: str, folder_token: str = None) -> str:
"""
创建新文档并写入内容(自动分块)
Args:
title: 文档标题
content: 文档内容(支持长内容,自动分块)
folder_token: 可选的文件夹token
Returns:
文档URL
"""
if not self.ctx:
raise ValueError("需要提供 OpenClaw 上下文对象 (ctx)")
# 第一步:创建空文档(只传标题)
doc_token = await self._create_empty_doc(title, folder_token)
doc_url = f"https://feishu.cn/docx/{doc_token}"
if self.config.show_progress:
print(f"✅ 文档创建成功: {doc_url}")
# 第二步:分批追加内容
success = await self._write_content_in_chunks(doc_token, content)
if not success:
raise Exception("写入内容失败")
return doc_url
async def append_to_document(self, doc_url: str, content: str) -> bool:
"""
追加内容到现有文档(自动分块)
Args:
doc_url: 文档URL
content: 要追加的内容
Returns:
是否成功
"""
if not self.ctx:
raise ValueError("需要提供 OpenClaw 上下文对象 (ctx)")
doc_token = self._extract_token_from_url(doc_url)
return await self._write_content_in_chunks(doc_token, content)
async def _create_empty_doc(self, title: str, folder_token: str = None) -> str:
"""创建空文档,只传标题"""
try:
# 使用 OpenClaw 官方工具调用方式
result = await self.ctx.invoke_tool("feishu_doc.create", {
"title": title,
"folder_token": folder_token
})
# 提取 doc_token
if isinstance(result, dict):
doc_token = result.get("document_id") or result.get("doc_token")
if doc_token:
return doc_token
# 如果是字符串,尝试提取
if isinstance(result, str):
import re
match = re.search(r'docx/([a-zA-Z0-9]+)', result)
if match:
return match.group(1)
return result
raise Exception(f"无法解析文档token: {result}")
except Exception as e:
raise Exception(f"创建文档失败: {e}")
async def _write_content_in_chunks(self, doc_token: str, content: str) -> bool:
"""分批写入内容"""
chunks = self.chunker.chunk_content(content)
if self.config.show_progress:
print(f"📝 内容已分割为 {len(chunks)} 块,开始写入...")
for i, chunk in enumerate(chunks, 1):
if self.config.show_progress:
print(f" 写入第 {i}/{len(chunks)} 块 ({len(chunk)} 字符)...")
success = await self._append_chunk_with_retry(doc_token, chunk)
if not success:
print(f"❌ 第 {i} 块写入失败")
return False
# 添加小延迟,避免API限流
if i < len(chunks):
await asyncio.sleep(0.5)
if self.config.show_progress:
print(f"✅ 全部 {len(chunks)} 块写入完成")
return True
async def _append_chunk_with_retry(self, doc_token: str, chunk: str) -> bool:
"""带重试的追加内容"""
for attempt in range(self.config.max_retries):
try:
return await self._append_chunk(doc_token, chunk)
except Exception as e:
if self.config.show_progress:
print(f" 尝试 {attempt + 1}/{self.config.max_retries} 失败: {e}")
if attempt < self.config.max_retries - 1:
await asyncio.sleep(self.config.retry_delay * (attempt + 1))
else:
return False
return False
async def _append_chunk(self, doc_token: str, chunk: str) -> bool:
"""追加单块内容"""
try:
# 使用 OpenClaw 官方工具调用方式
await self.ctx.invoke_tool("feishu_doc.append", {
"doc_token": doc_token,
"content": chunk
})
return True
except Exception as e:
raise Exception(f"API调用失败: {e}")
def _extract_token_from_url(self, url: str) -> str:
"""从URL中提取doc_token"""
import re
match = re.search(r'docx/([a-zA-Z0-9]+)', url)
if match:
return match.group(1)
raise ValueError(f"无法从URL提取token: {url}")
async def _get_tenant_access_token(self) -> str:
"""获取飞书 tenant_access_token"""
import aiohttp
import json
import os
# 尝试从 OpenClaw 配置读取 App ID 和 Secret
app_id, app_secret = None, None
# 方法1: 尝试从环境变量读取
app_id = os.environ.get("FEISHU_APP_ID")
app_secret = os.environ.get("FEISHU_APP_SECRET")
# 方法2: 尝试从 OpenClaw 配置文件读取
if not app_id or not app_secret:
try:
config_paths = [
os.path.expanduser("~/.openclaw/openclaw.json"),
os.path.expanduser("~/.openclaw/config.json"),
]
for config_path in config_paths:
if os.path.exists(config_path):
with open(config_path, 'r') as f:
config = json.load(f)
feishu_config = config.get("channels", {}).get("feishu", {})
app_id = feishu_config.get("appId")
app_secret = feishu_config.get("appSecret")
if app_id and app_secret:
break
except Exception:
pass
if not app_id or not app_secret:
return ""
# 调用飞书 API 获取 token
url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"
payload = {
"app_id": app_id,
"app_secret": app_secret
}
try:
async with aiohttp.ClientSession() as session:
async with session.post(url, json=payload) as resp:
result = await resp.json()
if result.get("code") == 0:
return result.get("tenant_access_token", "")
except Exception:
pass
return ""
async def transfer_ownership(self, doc_url: str, owner_openid: str) -> bool:
"""
转移文档所有权 - 直接调用飞书 API
API端点: POST /drive/v1/permissions/{token}/members/transfer_owner?type=docx
Args:
doc_url: 文档URL
owner_openid: 新所有者的openid (例如: ou_xxxxxxxx)
Returns:
是否成功
"""
import aiohttp
import json
doc_token = self._extract_token_from_url(doc_url)
# 获取 tenant_access_token
token = await self._get_tenant_access_token()
if not token:
if self.config.show_progress:
print(f"⚠️ 无法获取 tenant_access_token")
return False
# 调用飞书 API 转移所有权
url = f"https://open.feishu.cn/open-apis/drive/v1/permissions/{doc_token}/members/transfer_owner?type=docx"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
payload = {
"member_type": "openid",
"member_id": owner_openid
}
try:
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers, json=payload) as resp:
result = await resp.json()
if result.get("code") == 0:
if self.config.show_progress:
print(f"✅ 文档所有权已转移给 {owner_openid}")
return True
else:
error_msg = result.get("msg", "未知错误")
if self.config.show_progress:
print(f"⚠️ 所有权转移失败: {error_msg}")
return False
except Exception as e:
if self.config.show_progress:
print(f"⚠️ 所有权转移失败: {e}")
return False
async def write_document_with_transfer(
self,
title: str,
content: str,
folder_token: str = None,
owner_openid: str = None
) -> dict:
"""
创建文档并写入内容,完成后自动转移所有权并更新本地索引
Args:
title: 文档标题
content: 文档内容
folder_token: 可选的文件夹token
owner_openid: 新所有者的openid,如果提供则自动转移所有权
Returns:
{
"doc_url": "...",
"doc_token": "...",
"chunks_count": N,
"owner_transferred": True/False,
"index_updated": True/False
}
"""
# 1. 创建并写入文档
doc_url = await self.write_document(title, content, folder_token)
# 2. 提取 doc_token
doc_token = self._extract_token_from_url(doc_url)
# 3. 计算分块数
chunks = self.chunker.chunk_content(content)
# 4. 转移所有权(如果提供了 owner_openid
owner_transferred = False
if owner_openid:
owner_transferred = await self.transfer_ownership(doc_url, owner_openid)
# 5. 【关键】自动更新本地索引
index_updated = False
try:
# 生成摘要(取前100字)
summary = content[:100].replace('\n', ' ') + "..." if len(content) > 100 else content
# 自动分类标签
tags = self._auto_classify_content(content, title)
# 更新索引
index_updated = add_doc_to_index(
name=title,
url=doc_url,
token=doc_token,
summary=summary,
tags=tags,
owner=owner_openid or ""
)
if self.config.show_progress and index_updated:
print(f"✅ 文档索引已更新")
elif self.config.show_progress:
print(f"⚠️ 文档索引更新失败(不影响文档创建)")
except Exception as e:
if self.config.show_progress:
print(f"⚠️ 索引更新失败: {e}(不影响文档创建)")
return {
"doc_url": doc_url,
"doc_token": doc_token,
"chunks_count": len(chunks),
"owner_transferred": owner_transferred,
"index_updated": index_updated
}
def _auto_classify_content(self, content: str, title: str) -> List[str]:
"""根据内容自动分类"""
tags = []
text = (title + " " + content).lower()
# 关键词映射到标签
if any(k in text for k in ["ai", "人工智能", "模型", "gpt", "llm"]):
tags.append("AI技术")
if any(k in text for k in ["openclaw", "skill", "agent"]):
tags.append("OpenClaw")
if any(k in text for k in ["飞书", "文档", "feishu", "docx"]):
tags.append("飞书文档")
if any(k in text for k in ["电商", "tiktok", "alibaba", "玩具"]):
tags.append("电商")
if any(k in text for k in ["garmin", "strava", "骑行", "健康", "运动"]):
tags.append("健康运动")
if any(k in text for k in ["对话", "归档", "聊天记录"]):
tags.append("每日归档")
# 如果没有匹配到特定标签,添加通用标签
if not tags:
tags.append("其他")
return tags
# 同步包装函数(方便非异步环境使用)
def write_document_sync(ctx, title: str, content: str, folder_token: str = None, config: ChunkConfig = None) -> str:
"""同步方式写入文档"""
writer = FeishuDocWriter(ctx, config)
return asyncio.run(writer.write_document(title, content, folder_token))
def write_document_with_transfer_sync(
ctx,
title: str,
content: str,
folder_token: str = None,
owner_openid: str = None,
config: ChunkConfig = None
) -> dict:
"""同步方式写入文档并转移所有权"""
writer = FeishuDocWriter(ctx, config)
return asyncio.run(writer.write_document_with_transfer(title, content, folder_token, owner_openid))
def append_to_document_sync(ctx, doc_url: str, content: str, config: ChunkConfig = None) -> bool:
"""同步方式追加文档"""
writer = FeishuDocWriter(ctx, config)
return asyncio.run(writer.append_to_document(doc_url, content))