#!/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. **发布新版本(关键!)** - 开通后,点击页面右上角的 **"发布"** 按钮 - 等待发布完成(显示"已发布"状态) - ⚠️ **不发布的话,权限不会生效!** --- ## 💬 请回复配置信息 请按以下格式回复: ``` 配置OpenID:ou_你的OpenID 权限已开通并发布:是 ``` 例如: ``` 配置OpenID:ou_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" ]