Files

486 lines
14 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
"""
Feishu Smart Doc Writer
飞书智能文档写入器 - 自动分段、分批写入
支持首次使用自动引导配置
核心功能:
1. 智能分块写入 - 解决飞书API字数限制导致的空白文档
2. 自动转移所有权 - 创建文档后自动转移给用户
3. 首次使用引导 - 自动询问OpenID并配置
"""
import json
import os
from typing import Dict, Optional
from dataclasses import dataclass, asdict
# 配置文件路径
CONFIG_PATH = os.path.expanduser("~/.openclaw/workspace/skills/feishu-smart-doc-writer/user_config.json")
@dataclass
class UserConfig:
"""用户配置"""
owner_openid: str = ""
permission_noted: bool = False # 用户是否已确认权限
first_time: bool = True # 是否首次使用
def save(self):
"""保存配置"""
try:
os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
with open(CONFIG_PATH, 'w', encoding='utf-8') as f:
json.dump(asdict(self), f, ensure_ascii=False, indent=2)
return True
except Exception as e:
print(f"保存配置失败: {e}")
return False
@classmethod
def load(cls) -> 'UserConfig':
"""加载配置"""
try:
if os.path.exists(CONFIG_PATH):
with open(CONFIG_PATH, 'r', encoding='utf-8') as f:
data = json.load(f)
return cls(**data)
except Exception as e:
print(f"加载配置失败: {e}")
return cls()
# 引导消息模板
FIRST_TIME_GUIDE = """👋 **欢迎使用 Feishu Smart Doc Writer**
本 Skill 可以帮助你:
✅ **智能分块写入** - 解决长文档写入时因API限制导致的空白问题
✅ **自动转移所有权** - 创建文档后自动转移给你,拥有完全控制权
---
## 🔧 首次使用配置
### 第1步:获取你的 OpenID
**详细步骤(精确路径):**
1. **登录飞书开放平台**
- 网址:https://open.feishu.cn
2. **进入权限管理并前往调试台**
- 进入你的**相关应用**
- 点击 **"权限管理"**
- 搜索权限:`im:message`
- 鼠标移动到 **"相关API事件"**
- 选择:**【API】发送消息**
- 点击右下角:**"前往API调试台"**
3. **找到 "快速复制 open_id"**
- 在页面中找到 **蓝色文字** "快速复制 open_id"
- 点击这个链接
4. **选择用户并复制**
- 在弹出的选择框中,**选择你的账号**
- 点击 **"复制"** 按钮
- 得到格式如:`ou_5b921cba0fd6e7c885276a02d730ec19`
💡 **提示**OpenID 是以 `ou_` 开头的一串字符,不是数字ID
---
### 第2步:开通并发布权限
⚠️ **重要:需要开通权限并发布应用新版本**
**开通权限步骤:**
1. **进入权限管理**
- 登录 https://open.feishu.cn
- 进入你的应用
- 点击左侧菜单 **"权限管理"**
2. **搜索并开通权限**
- 在搜索框输入:`docs:permission.member:transfer`
- 找到权限 **"转移云文档的所有权"**
- 点击 **"开通"** 按钮
3. **发布新版本(关键!)**
- 开通后,点击页面右上角的 **"发布"** 按钮
- 等待发布完成(显示"已发布"状态)
- ⚠️ **不发布的话,权限不会生效!**
---
## 💬 请回复配置信息
请按以下格式回复:
```
配置OpenIDou_你的OpenID
权限已开通并发布:是
```
例如:
```
配置OpenIDou_5b921cba0fd6e7c885276a02d730ec19
权限已开通并发布:是
```
配置完成后,本 Skill 将自动保存配置,之后创建文档会自动转移所有权给你!
"""
async def write_smart(ctx, args: dict) -> dict:
"""
智能创建飞书文档(自动分块 + 自动转移所有权)
首次使用时会自动引导配置。
Args:
ctx: OpenClaw 上下文
args: 包含 title, content, folder_token, chunk_size, show_progress
Returns:
{"doc_url": "...", "doc_token": "...", "chunks_count": N, "owner_transferred": True/False}
"""
# 加载用户配置
config = UserConfig.load()
# 首次使用或未完成配置,显示引导
if config.first_time or not config.owner_openid:
return {
"doc_url": None,
"doc_token": None,
"chunks_count": 0,
"owner_transferred": False,
"need_config": True,
"message": FIRST_TIME_GUIDE
}
# 已配置,正常执行
title = args.get("title")
content = args.get("content", "")
folder_token = args.get("folder_token")
chunk_size = args.get("chunk_size", 2000)
show_progress = args.get("show_progress", True)
if not title:
raise ValueError("必须提供 title 参数")
from .feishu_smart_doc_writer import FeishuDocWriter, ChunkConfig
chunk_config = ChunkConfig(
chunk_size=chunk_size,
show_progress=show_progress
)
writer = FeishuDocWriter(ctx, chunk_config)
try:
# 使用配置的 owner_openid 自动转移
result = await writer.write_document_with_transfer(
title=title,
content=content,
folder_token=folder_token,
owner_openid=config.owner_openid
)
transfer_msg = ",所有权已转移" if result.get("owner_transferred") else ""
return {
"doc_url": result["doc_url"],
"doc_token": result["doc_token"],
"chunks_count": result["chunks_count"],
"owner_transferred": result["owner_transferred"],
"need_config": False,
"message": f"✅ 文档创建成功,共分 {result['chunks_count']} 块写入{transfer_msg}"
}
except Exception as e:
return {
"doc_url": None,
"doc_token": None,
"chunks_count": 0,
"owner_transferred": False,
"need_config": False,
"message": f"❌ 创建失败: {e}"
}
async def configure(ctx, args: dict) -> dict:
"""
配置 Skill
用户首次使用时,通过此工具配置 OpenID。
Args:
ctx: OpenClaw 上下文
args: 包含 openid, permission_checked
Returns:
{"success": True/False, "message": "..."}
"""
openid = args.get("openid", "").strip()
permission_checked = args.get("permission_checked", False)
# 验证 OpenID 格式
if not openid:
return {
"success": False,
"message": "❌ 请提供 OpenID"
}
if not openid.startswith("ou_"):
return {
"success": False,
"message": "❌ OpenID 格式错误,应以 'ou_' 开头,请检查"
}
# 保存配置
config = UserConfig()
config.owner_openid = openid
config.permission_noted = permission_checked
config.first_time = False
if config.save():
return {
"success": True,
"openid": openid,
"message": f"✅ 配置成功!\n\n你的 OpenID{openid}\n\n配置已保存,现在可以使用 write_smart 创建文档,所有权会自动转移给你。"
}
else:
return {
"success": False,
"message": "❌ 配置保存失败"
}
async def append_smart(ctx, args: dict) -> dict:
"""
智能追加内容到飞书文档(自动分块)
Args:
ctx: OpenClaw 上下文
args: 包含 doc_url, content, chunk_size, show_progress
Returns:
{"success": True/False, "chunks_count": N}
"""
doc_url = args.get("doc_url")
content = args.get("content", "")
chunk_size = args.get("chunk_size", 2000)
show_progress = args.get("show_progress", True)
if not doc_url:
raise ValueError("必须提供 doc_url 参数")
from .feishu_smart_doc_writer import FeishuDocWriter, ChunkConfig, ContentChunker
config = ChunkConfig(
chunk_size=chunk_size,
show_progress=show_progress
)
writer = FeishuDocWriter(ctx, config)
try:
success = await writer.append_to_document(doc_url, content)
# 计算分块数
chunks = ContentChunker(config).chunk_content(content)
return {
"success": success,
"chunks_count": len(chunks),
"message": f"{'✅' if success else '❌'} 追加 {'成功' if success else '失败'},共分 {len(chunks)} 块"
}
except Exception as e:
return {
"success": False,
"chunks_count": 0,
"message": f"❌ 追加失败: {e}"
}
async def transfer_ownership(ctx, args: dict) -> dict:
"""
转移文档所有权
Args:
ctx: OpenClaw 上下文
args: 包含 doc_url, owner_openid
Returns:
{"success": True/False, "message": "..."}
"""
doc_url = args.get("doc_url")
owner_openid = args.get("owner_openid")
if not doc_url or not owner_openid:
raise ValueError("必须提供 doc_url 和 owner_openid 参数")
from .feishu_smart_doc_writer import FeishuDocWriter, ChunkConfig
config = ChunkConfig(show_progress=False)
writer = FeishuDocWriter(ctx, config)
try:
success = await writer.transfer_ownership(doc_url, owner_openid)
return {
"success": success,
"message": f"{'✅' if success else '❌'} 所有权转移{'成功' if success else '失败'}"
}
except Exception as e:
return {
"success": False,
"message": f"❌ 转移失败: {e}"
}
async def get_config_status(ctx, args: dict) -> dict:
"""
获取当前配置状态
Returns:
{"configured": True/False, "openid": "...", "message": "..."}
"""
config = UserConfig.load()
if config.owner_openid:
return {
"configured": True,
"openid": config.owner_openid,
"first_time": config.first_time,
"message": f"✅ 已配置\nOpenID: {config.owner_openid}"
}
else:
return {
"configured": False,
"openid": None,
"first_time": config.first_time,
"message": "⚠️ 未配置\n请使用 configure 工具进行配置"
}
async def search_docs(ctx, args: dict) -> dict:
"""
搜索本地索引中的文档
Args:
ctx: OpenClaw 上下文
args: 包含 keyword, search_in(可选)
Returns:
{"results": [...], "count": N, "message": "..."}
"""
keyword = args.get("keyword", "").strip()
search_in = args.get("search_in", ["name", "summary", "tags"])
if not keyword:
return {
"results": [],
"count": 0,
"message": "❌ 请提供搜索关键词"
}
try:
from .index_manager import IndexManager
manager = IndexManager()
results = manager.search_docs(keyword, search_in)
# 格式化结果
formatted_results = []
for doc in results:
formatted_results.append({
"name": doc.get("name", ""),
"type": doc.get("type", ""),
"link": doc.get("link", ""),
"summary": doc.get("summary", ""),
"status": doc.get("status", ""),
"tags": doc.get("tags", ""),
"updated": doc.get("updated", "")
})
return {
"results": formatted_results,
"count": len(formatted_results),
"message": f"✅ 找到 {len(formatted_results)} 个结果" if formatted_results else f"⚠️ 未找到包含 '{keyword}' 的文档"
}
except Exception as e:
return {
"results": [],
"count": 0,
"message": f"❌ 搜索失败: {e}"
}
async def list_docs(ctx, args: dict) -> dict:
"""
列出所有文档(支持筛选)
Args:
ctx: OpenClaw 上下文
args: 包含 tag(可选), status(可选), limit(可选)
Returns:
{"results": [...], "count": N, "message": "..."}
"""
tag = args.get("tag")
status = args.get("status")
limit = args.get("limit", 50)
try:
from .index_manager import IndexManager
manager = IndexManager()
results = manager.list_docs(tag=tag, status=status, limit=limit)
# 格式化结果
formatted_results = []
for doc in results:
formatted_results.append({
"name": doc.get("name", ""),
"type": doc.get("type", ""),
"link": doc.get("link", ""),
"summary": doc.get("summary", ""),
"status": doc.get("status", ""),
"tags": doc.get("tags", ""),
"updated": doc.get("updated", "")
})
# 构建消息
filter_desc = []
if tag:
filter_desc.append(f"标签 '{tag}'")
if status:
filter_desc.append(f"状态 '{status}'")
filter_text = "".join(filter_desc) if filter_desc else "全部"
return {
"results": formatted_results,
"count": len(formatted_results),
"message": f"✅ {filter_text}文档共 {len(formatted_results)} 个"
}
except Exception as e:
return {
"results": [],
"count": 0,
"message": f"❌ 列出文档失败: {e}"
}
# 版本信息
__version__ = "1.3.0"
__all__ = [
"write_smart",
"append_smart",
"transfer_ownership",
"configure",
"get_config_status",
"search_docs",
"list_docs",
"UserConfig",
"FIRST_TIME_GUIDE"
]