技能备份 - 2026-04-15 (40个技能)
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.4.1] - 2026-02-23
|
||||
|
||||
### Fixed
|
||||
- **Skill.json tool count**: Fixed missing tools display on Clawhub
|
||||
- Verified all 7 tools are properly declared: write_smart, configure, append_smart, transfer_ownership, get_config_status, search_docs, list_docs
|
||||
|
||||
## [1.4.0] - 2026-02-23
|
||||
|
||||
### Added
|
||||
- **Document Search Functionality** (`search_docs`)
|
||||
- Search local index by keywords
|
||||
- Search in document name, summary, and tags
|
||||
- Return matching documents with links
|
||||
|
||||
- **Document List Functionality** (`list_docs`)
|
||||
- List all created Feishu documents
|
||||
- Filter by tags (AI Technology, E-commerce, Health & Sports, etc.)
|
||||
- Filter by status
|
||||
|
||||
- **Automatic Index Management** (`index_manager.py`)
|
||||
- Auto-update local index at `memory/feishu-docs-index.md` after document creation
|
||||
- Auto-generate document summary (first 100 characters)
|
||||
- Intelligent auto-categorization tags:
|
||||
- AI Technology (AI, artificial intelligence, model, GPT, LLM)
|
||||
- OpenClaw (OpenClaw, skill, agent)
|
||||
- Feishu Docs (Feishu, document, docx)
|
||||
- E-commerce (e-commerce, TikTok, Alibaba)
|
||||
- Health & Sports (Garmin, Strava, cycling, health)
|
||||
- Daily Archive (conversation, archive, chat history)
|
||||
|
||||
- **Tool Declarations**
|
||||
- Officially declare `search_docs` and `list_docs` tools in `package.json` and `skill.json`
|
||||
|
||||
### Changed
|
||||
- Updated `SKILL.md` documentation with detailed descriptions of new features
|
||||
- Improved usage examples and configuration instructions
|
||||
|
||||
### Technical Details (Ownership Transfer Fix)
|
||||
- **Before**: Used `ctx.invoke_tool("exec", ...)` which failed when ctx was None or unavailable
|
||||
- **After**: Direct HTTP API calls with `aiohttp`, fully independent of OpenClaw context
|
||||
- **API Endpoint**: `POST /drive/v1/permissions/{token}/members/transfer_owner?type=docx`
|
||||
|
||||
### Optimized Ownership Transfer Workflow
|
||||
- Automatically transfer ownership to user after document creation
|
||||
- Automatically obtain tenant_access_token without manual configuration
|
||||
- Fixed "permission denied" errors caused by ctx passing issues
|
||||
|
||||
## [1.3.0] - 2026-02-22
|
||||
|
||||
### Fixed
|
||||
- **Fixed Ownership Transfer Functionality** (`transfer_ownership`)
|
||||
- **BREAKING CHANGE**: Replaced ctx-dependent implementation with independent API calls
|
||||
- Added `_get_tenant_access_token()` method for independent token retrieval using aiohttp
|
||||
- **Removed dependency**: No longer relies on `ctx.invoke_tool("exec", ...)` to call curl commands
|
||||
- **More stable**: Directly use `aiohttp` to call Feishu API, eliminating context passing issues
|
||||
- **Better error handling**: Returns detailed error messages on failure
|
||||
- Reads Feishu app credentials from OpenClaw config independently
|
||||
|
||||
### Technical Details
|
||||
- **Before**: Used `ctx.invoke_tool("exec", ...)` which failed when ctx was None or unavailable
|
||||
- **After**: Direct HTTP API calls with `aiohttp`, fully independent of OpenClaw context
|
||||
- **API Endpoint**: `POST /drive/v1/permissions/{token}/members/transfer_owner?type=docx`
|
||||
|
||||
### Changed
|
||||
- **Optimized Ownership Transfer Workflow**
|
||||
- Automatically transfer ownership to user after document creation
|
||||
- Automatically obtain tenant_access_token without manual configuration
|
||||
- Fixed "permission denied" errors caused by ctx passing issues
|
||||
|
||||
## [1.2.0] - 2026-02-21
|
||||
|
||||
### Added
|
||||
- **Intelligent Chunk Writing Functionality**
|
||||
- Automatically split long content into chunks (default 2000 characters)
|
||||
- Avoid blank document issues caused by Feishu API character limits
|
||||
- Smart paragraph segmentation while preserving heading structure
|
||||
- Support automatic table-to-text conversion
|
||||
|
||||
- **First-Time User Guide**
|
||||
- Auto-detect first-time usage
|
||||
- Guide users to obtain OpenID
|
||||
- Auto-save configuration to `user_config.json`
|
||||
|
||||
- **Basic Document Operations**
|
||||
- `write_smart` - Smart document creation
|
||||
- `append_smart` - Append content to existing documents
|
||||
- `transfer_ownership` - Transfer document ownership
|
||||
- `configure` - Configure OpenID
|
||||
- `get_config_status` - Check configuration status
|
||||
|
||||
## [1.1.0] - 2026-02-20
|
||||
|
||||
### Added
|
||||
- Initial release
|
||||
- Basic Feishu document creation and writing functionality
|
||||
- Support Markdown format (headings, lists, code blocks, etc.)
|
||||
- Support automatic image upload
|
||||
|
||||
---
|
||||
|
||||
## Version Summary
|
||||
|
||||
- **v1.4.0** - Added search, list, and automatic index management
|
||||
- **v1.3.0** - Fixed ownership transfer using independent API calls (removed ctx dependency)
|
||||
- **v1.2.0** - Added intelligent chunk writing and first-time user guide
|
||||
- **v1.1.0** - Initial release with basic document operations
|
||||
|
||||
## Key Technical Updates
|
||||
|
||||
### v1.4.0 Core Technology
|
||||
- Local index management system
|
||||
- Intelligent content categorization algorithm
|
||||
- Automatic document metadata extraction
|
||||
|
||||
### v1.3.0 Core Technology (Critical Fix)
|
||||
- **Independent tenant_access_token retrieval** using aiohttp
|
||||
- **Direct Feishu API calls** without OpenClaw context dependency
|
||||
- **Removed**: `ctx.invoke_tool("exec", ...)` approach
|
||||
- **Added**: `_get_tenant_access_token()` for standalone token acquisition
|
||||
- Reads app credentials from `~/.openclaw/openclaw.json` independently
|
||||
|
||||
### v1.2.0 Core Technology
|
||||
- ContentChunker content segmentation
|
||||
- Intelligent paragraph segmentation algorithm
|
||||
- User configuration persistence
|
||||
@@ -0,0 +1,143 @@
|
||||
# Feishu Smart Doc Writer
|
||||
|
||||
> **English**: Feishu/Lark Smart Document Writer. Solves API content limits by auto-chunking long documents and auto-transferring ownership. Guides OpenID config on first use.
|
||||
>
|
||||
> **中文**: 飞书智能文档写入器,解决长文档API限制导致的空白问题,自动转移所有权。首次使用自动引导配置。
|
||||
|
||||
飞书智能文档写入器 - 解决长文档写入时的 API 限制问题,支持自动转移所有权。
|
||||
|
||||
## 认证说明
|
||||
|
||||
本 Skill 使用 OpenClaw 内置的飞书工具集,**无需手动获取 token**。
|
||||
|
||||
- `tenant_access_token` 由 OpenClaw 自动管理(通过配置好的 `appId` 和 `appSecret` 自动换取)
|
||||
- 你**不需要**去飞书后台点击"获取 token"
|
||||
- 本 Skill 唯一需要配置的是 **用户 OpenID**(用于文档所有权转移,见下方配置步骤)
|
||||
|
||||
## 核心功能
|
||||
|
||||
### 1. 智能分块写入
|
||||
飞书 API 对单次写入有限制(创建~4000字符,追加~2000字符),超过会导致文档空白。本 Skill 自动将长内容分块写入,确保完整无丢失。
|
||||
|
||||
### 2. 自动转移所有权
|
||||
应用创建的文档默认所有权属于应用,用户无法编辑。本 Skill 在创建文档后自动转移所有权给用户,用户拥有完全控制权。
|
||||
|
||||
### 3. 首次使用自动引导
|
||||
首次使用时自动引导用户配置 OpenID,无需查阅文档。
|
||||
|
||||
## 使用流程
|
||||
|
||||
### 第1步:首次使用(自动引导)
|
||||
|
||||
当你第一次使用 `write_smart` 时,Skill 会自动提示配置指南。
|
||||
|
||||
### 第2步:获取你的 OpenID
|
||||
|
||||
**详细步骤(精确路径):**
|
||||
|
||||
1. **登录飞书开放平台**
|
||||
- 网址:https://open.feishu.cn
|
||||
|
||||
2. **进入权限管理并前往调试台**
|
||||
- 进入你的**相关应用**
|
||||
- 点击 **"权限管理"**
|
||||
- 搜索权限:`im:message`
|
||||
- 鼠标移动到 **"相关API事件"**
|
||||
- 选择:**【API】发送消息**
|
||||
- 点击右下角:**"前往API调试台"**
|
||||
|
||||
3. **找到 "快速复制 open_id"**
|
||||
- 在页面中找到 **蓝色文字** "快速复制 open_id"
|
||||
- 点击这个链接
|
||||
|
||||
4. **选择用户并复制**
|
||||
- 在弹出的选择框中,**选择你的账号**
|
||||
- 点击 **"复制"** 按钮
|
||||
- 得到格式如:`ou_xxxxxxxx`
|
||||
|
||||
### 第3步:开通并发布权限
|
||||
|
||||
⚠️ **重要:需要开通权限并发布应用新版本**
|
||||
|
||||
1. **进入权限管理**
|
||||
- 登录 https://open.feishu.cn
|
||||
- 进入你的应用 → **权限管理**
|
||||
|
||||
2. **搜索并开通权限**
|
||||
- 搜索:`docs:permission.member:transfer`
|
||||
- 点击 **"开通"**
|
||||
|
||||
3. **发布新版本(关键!)**
|
||||
- 点击页面右上角的 **"发布"** 按钮
|
||||
- 等待发布完成
|
||||
- ⚠️ **不发布的话,权限不会生效!**
|
||||
|
||||
### 第4步:配置 Skill
|
||||
|
||||
根据引导,使用 `configure` 工具配置:
|
||||
|
||||
```python
|
||||
await ctx.invoke_tool("feishu_smart_doc_writer.configure", {
|
||||
"openid": "ou_5b921cba0fd6e7c885276a02d730ec19",
|
||||
"permission_checked": true
|
||||
})
|
||||
```
|
||||
|
||||
### 第5步:创建文档(自动转移所有权)
|
||||
|
||||
配置完成后,直接创建文档:
|
||||
|
||||
```python
|
||||
result = await ctx.invoke_tool("feishu_smart_doc_writer.write_smart", {
|
||||
"title": "项目报告",
|
||||
"content": "# 项目报告\n\n很长很长的内容...(支持10000+字)"
|
||||
})
|
||||
|
||||
# 返回:
|
||||
# {
|
||||
# "doc_url": "https://feishu.cn/docx/xxx",
|
||||
# "chunks_count": 5,
|
||||
# "owner_transferred": true,
|
||||
# "message": "✅ 文档创建成功,共分 5 块写入,所有权已转移"
|
||||
# }
|
||||
```
|
||||
|
||||
**配置一次,永久生效!** 之后创建文档会自动转移所有权。
|
||||
|
||||
## 工具列表
|
||||
|
||||
| 工具名 | 功能 |
|
||||
|--------|------|
|
||||
| `write_smart` | 创建文档,自动分块写入,自动转移所有权(首次使用引导配置) |
|
||||
| `configure` | 配置 OpenID 和确认权限 |
|
||||
| `append_smart` | 追加内容到已有文档(自动分块) |
|
||||
| `transfer_ownership` | 转移已有文档的所有权 |
|
||||
| `get_config_status` | 查看当前配置状态 |
|
||||
|
||||
## 为什么需要这个 Skill?
|
||||
|
||||
### 原生 feishu_doc 的问题
|
||||
|
||||
```python
|
||||
# ❌ 原生方式 - 长内容会失败
|
||||
feishu_doc.create(
|
||||
title="项目报告",
|
||||
content="# 很长很长的内容..." * 1000 # 超过4000字符
|
||||
)
|
||||
# 结果:文档创建,但内容空白或报错
|
||||
```
|
||||
|
||||
### 使用本 Skill
|
||||
|
||||
```python
|
||||
# ✅ 自动处理 - 内容分块写入,自动转移所有权
|
||||
write_smart(
|
||||
title="项目报告",
|
||||
content="# 很长很长的内容..." * 1000, # 10000字符
|
||||
)
|
||||
# 结果:自动分为5块写入,所有权转移给用户,文档完整
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,300 @@
|
||||
---
|
||||
name: feishu-smart-doc-writer
|
||||
description: |
|
||||
Feishu/Lark Smart Document Writer - 飞书智能文档写入器.
|
||||
|
||||
Core Features / 核心功能:
|
||||
1. Smart Chunk Writing / 智能分块写入 - Solve API limit blank doc issues / 解决长文档API限制导致的空白问题
|
||||
2. Auto Ownership Transfer / 自动转移所有权 - Transfer to user after creation / 创建文档后自动转移给用户
|
||||
3. Auto Index Management / 自动索引管理 - Update local index with search support / 自动更新本地文档索引,支持搜索
|
||||
4. First-time Guide / 首次使用引导 - Auto guide for OpenID config / 自动引导配置OpenID
|
||||
---
|
||||
|
||||
# Feishu Smart Doc Writer v1.4.1
|
||||
|
||||
## 🚀 Core Features / 核心功能
|
||||
|
||||
### 1. Smart Document Creation / 智能文档创建
|
||||
- **Auto-chunk Writing / 自动分块**: Split long content into chunks to avoid API limit blank docs / 长内容自动分割成小块,避免API限制导致的空白文档
|
||||
- **Auto Ownership Transfer / 自动转移所有权**: Transfer to user using OpenID after creation / 创建后自动使用 OpenID 转移给用户
|
||||
- **Auto Index Update / 自动索引更新**: Add doc info to local index `memory/feishu-docs-index.md` / 文档信息自动添加到本地索引
|
||||
- **Smart Categorization / 智能分类**: Auto-tag based on content (AI Tech, E-commerce, Health, etc.) / 根据内容自动打标签(AI技术、电商、健康运动等)
|
||||
|
||||
### 2. Document Management / 文档管理
|
||||
- **Search Documents / 搜索文档**: Search local index by keywords / 按关键词搜索本地索引
|
||||
- **List Documents / 列出文档**: Filter by tags and status / 按标签、状态筛选文档列表
|
||||
- **Append Content / 追加内容**: Append to existing docs (auto-chunk) / 向现有文档追加内容(自动分块)
|
||||
|
||||
---
|
||||
|
||||
## 📋 Tools / 工具列表
|
||||
|
||||
### write_smart - Smart Document Creation / 智能创建文档
|
||||
Create document with auto-chunk writing, ownership transfer, and index update.
|
||||
创建文档,自动完成分块写入、所有权转移、索引更新。
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Document Title / 文档标题",
|
||||
"content": "Content (long content supported) / 文档内容(支持长内容)",
|
||||
"folder_token": "Optional folder token / 可选的文件夹token"
|
||||
}
|
||||
```
|
||||
|
||||
**Returns / 返回:**
|
||||
```json
|
||||
{
|
||||
"doc_url": "https://feishu.cn/docx/xxx",
|
||||
"doc_token": "xxx",
|
||||
"chunks_count": 3,
|
||||
"owner_transferred": true,
|
||||
"index_updated": true
|
||||
}
|
||||
```
|
||||
|
||||
### append_smart - Append Content / 追加内容
|
||||
Append content to existing document with auto-chunk.
|
||||
向现有文档追加内容(自动分块)。
|
||||
|
||||
```json
|
||||
{
|
||||
"doc_url": "https://feishu.cn/docx/xxx",
|
||||
"content": "Content to append / 要追加的内容"
|
||||
}
|
||||
```
|
||||
|
||||
### search_docs - Search Documents / 搜索文档
|
||||
Search documents in local index.
|
||||
搜索本地索引中的文档。
|
||||
|
||||
```json
|
||||
{
|
||||
"keyword": "Search keyword / 搜索关键词"
|
||||
}
|
||||
```
|
||||
|
||||
**Returns / 返回:**
|
||||
```json
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"name": "Document Name / 文档名",
|
||||
"link": "https://...",
|
||||
"summary": "Summary / 摘要",
|
||||
"tags": "AI Tech, OpenClaw / AI技术, OpenClaw"
|
||||
}
|
||||
],
|
||||
"count": 1
|
||||
}
|
||||
```
|
||||
|
||||
### list_docs - List Documents / 列出文档
|
||||
List all documents with optional filters.
|
||||
列出所有文档,支持筛选。
|
||||
|
||||
```json
|
||||
{
|
||||
"tag": "AI Tech / AI技术",
|
||||
"status": "Completed / 已完成"
|
||||
}
|
||||
```
|
||||
|
||||
### transfer_ownership - Transfer Ownership / 转移所有权
|
||||
Manually transfer document ownership (usually auto-handled by write_smart).
|
||||
手动转移文档所有权(通常不需要,write_smart 已自动处理)。
|
||||
|
||||
```json
|
||||
{
|
||||
"doc_url": "https://feishu.cn/docx/xxx",
|
||||
"owner_openid": "ou_xxxxxxxx"
|
||||
}
|
||||
```
|
||||
|
||||
**Note / 注意:** Only provide OpenID, tenant_access_token is auto-obtained by Skill.
|
||||
只需要提供 OpenID,tenant_access_token 由 Skill 自动获取。
|
||||
|
||||
### configure - Configure OpenID / 配置 OpenID
|
||||
Configure OpenID for first-time use.
|
||||
首次使用时配置 OpenID。
|
||||
|
||||
```json
|
||||
{
|
||||
"openid": "ou_xxxxxxxx",
|
||||
"permission_checked": true
|
||||
}
|
||||
```
|
||||
|
||||
### get_config_status - Get Config Status / 查看配置状态
|
||||
View current configuration status.
|
||||
查看当前配置状态。
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start / 快速开始
|
||||
|
||||
### First-time Setup (3 Steps) / 首次使用(3步配置)
|
||||
|
||||
**Step 1 / 第1步: Call write_smart / 调用 write_smart**
|
||||
```
|
||||
/feishu-smart-doc-writer write_smart
|
||||
title: Test Document / 测试文档
|
||||
content: This is a test document. / 这是一个测试文档内容
|
||||
```
|
||||
|
||||
**Step 2 / 第2步: Get OpenID / 获取 OpenID**
|
||||
If not configured, follow the guide:
|
||||
如果未配置,会显示引导:
|
||||
1. Login / 登录 https://open.feishu.cn
|
||||
2. Go to / 进入 Application → Permission Management / 应用 → 权限管理 → Search / 搜索 `im:message`
|
||||
3. Click / 点击【API】Send Message / 发送消息 → Go to API Debug Console / 前往API调试台
|
||||
4. Click / 点击"Quick Copy open_id" / 快速复制 open_id", select your account / 选择你的账号, copy / 复制
|
||||
|
||||
**Step 3 / 第3步: Configure and Enable Permissions / 配置并开通权限**
|
||||
```
|
||||
/feishu-smart-doc-writer configure
|
||||
openid: ou_your_openid / ou_你的OpenID
|
||||
permission_checked: true
|
||||
```
|
||||
|
||||
Then go to Permission Management / 然后到权限管理:
|
||||
1. Search / 搜索 `docs:permission.member:transfer`
|
||||
2. Click / 点击"Enable / 开通"
|
||||
3. **Important / 重要**: Click / 点击"Publish / 发布" button to publish new version / 按钮发布新版本
|
||||
|
||||
After setup, future document creation will automatically:
|
||||
配置完成后,以后创建文档会自动:
|
||||
- ✅ Chunk write content / 分块写入内容
|
||||
- ✅ Transfer ownership to you / 转移所有权给你
|
||||
- ✅ Update local index / 更新本地索引
|
||||
|
||||
---
|
||||
|
||||
## 📊 Index Management / 索引管理
|
||||
|
||||
### Auto Index Workflow / 自动索引流程
|
||||
```
|
||||
write_smart creates document / 创建文档
|
||||
↓
|
||||
Write content (auto-chunk) / 写入内容(自动分块)
|
||||
↓
|
||||
Transfer ownership / 转移所有权
|
||||
↓
|
||||
Auto update index → memory/feishu-docs-index.md / 自动更新索引
|
||||
↓
|
||||
Done! / 完成!
|
||||
```
|
||||
|
||||
### Auto-categorization Tags / 自动分类标签
|
||||
Auto-identified based on content / 根据内容自动识别:
|
||||
- **AI Tech / AI技术** - AI, artificial intelligence / 人工智能, model / 模型, GPT, LLM
|
||||
- **OpenClaw** - OpenClaw, skill, agent
|
||||
- **Feishu Docs / 飞书文档** - Feishu / 飞书, document / 文档, feishu
|
||||
- **E-commerce / 电商** - e-commerce / 电商, TikTok, Alibaba / 阿里巴巴
|
||||
- **Health & Sports / 健康运动** - Garmin, Strava, cycling / 骑行, health / 健康
|
||||
- **Daily Archive / 每日归档** - conversation / 对话, archive / 归档, chat history / 聊天记录
|
||||
|
||||
### Index File Location / 索引文件位置
|
||||
`memory/feishu-docs-index.md`
|
||||
|
||||
Format / 格式: Markdown table with / Markdown 表格,包含 index / 序号, name / 名称, type / 类型, link / 链接, summary / 摘要, status / 状态, tags / 标签, owner / 所有者
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Usage Examples / 使用示例
|
||||
|
||||
### Example 1 / 示例1: Create Tech Document / 创建技术文档
|
||||
```
|
||||
/feishu-smart-doc-writer write_smart
|
||||
title: AI Tech Research Report / AI技术调研报告
|
||||
content: # AI Overview / AI技术概述\n\nAI is... / 人工智能(AI)是...
|
||||
```
|
||||
|
||||
Result / 结果:
|
||||
- Document created successfully / 文档创建成功
|
||||
- Auto-tagged "AI Tech / AI技术" / 自动打上"AI技术"标签
|
||||
- Index updated / 索引已更新
|
||||
|
||||
### Example 2 / 示例2: Search Documents / 搜索文档
|
||||
```
|
||||
/feishu-smart-doc-writer search_docs
|
||||
keyword: AI Tech / AI技术
|
||||
```
|
||||
|
||||
### Example 3 / 示例3: List All Tech Documents / 列出所有技术文档
|
||||
```
|
||||
/feishu-smart-doc-writer list_docs
|
||||
tag: AI Tech / AI技术
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Configuration / 配置说明
|
||||
|
||||
### User Config File / 用户配置文件
|
||||
Location / 位置: `skills/feishu-smart-doc-writer/user_config.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"owner_openid": "ou_5b921cba0fd6e7c885276a02d730ec19",
|
||||
"permission_noted": true,
|
||||
"first_time": false
|
||||
}
|
||||
```
|
||||
|
||||
### Required Permissions / 必需权限
|
||||
- `docx:document:create` - Create document / 创建文档
|
||||
- `docx:document:write` - Write content / 写入内容
|
||||
- `docs:permission.member:transfer` - Transfer ownership ⚠️ Critical / 转移所有权 ⚠️ 关键权限
|
||||
|
||||
---
|
||||
|
||||
## 📝 Version History / 版本历史
|
||||
|
||||
### v1.4.1 (2026-02-23)
|
||||
- ✅ Fixed description inconsistency between skill.json and package.json / 修复 skill.json 和 package.json 描述不一致问题
|
||||
- ✅ Unified all file versions to v1.4.1 / 统一所有文件版本号为 v1.4.1
|
||||
- ✅ Verified all 7 tools properly declared / 确认所有 7 个工具正确声明
|
||||
|
||||
### v1.4.0 (2026-02-23)
|
||||
- ✅ Added auto index management (index_manager.py) / 新增自动索引管理(index_manager.py)
|
||||
- ✅ Added search_docs tool (search local index) / 新增 search_docs 工具(搜索本地索引)
|
||||
- ✅ Added list_docs tool (list documents) / 新增 list_docs 工具(列出文档)
|
||||
- ✅ Smart auto-categorization tags / 智能自动分类标签
|
||||
- ✅ Fixed ownership transfer (independent API calls, no ctx dependency) / 修复所有权转移(独立 API 调用,不依赖 ctx)
|
||||
|
||||
### v1.2.0
|
||||
- ✅ Auto-chunk writing / 自动分块写入
|
||||
- ✅ Auto ownership transfer / 自动转移所有权
|
||||
- ✅ First-time user guide / 首次使用引导
|
||||
|
||||
### v1.1.0
|
||||
- ✅ Basic document creation and append / 基础文档创建和追加
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Troubleshooting / 故障排除
|
||||
|
||||
### "open_id is not exist" Error / 错误
|
||||
**Cause / 原因**: Used user_id instead of openid / 使用了 user_id 而不是 openid
|
||||
**Solution / 解决**: Use openid starting with `ou_` / 使用以 `ou_` 开头的 openid
|
||||
|
||||
### "Permission Denied" Error / "权限不足" 错误
|
||||
**Cause / 原因**: `docs:permission.member:transfer` not enabled or app not published / 未开通权限,或未发布应用
|
||||
**Solution / 解决**:
|
||||
1. Permission Management → Search `docs:permission.member:transfer` → Enable / 权限管理 → 搜索 → 开通
|
||||
2. Click "Publish" button to publish new version (Critical!) / 点击"发布"按钮(关键!)
|
||||
|
||||
### Index Not Updated / 索引未更新
|
||||
**Check / 检查**:
|
||||
1. Check if `memory/feishu-docs-index.md` exists / 查看文件是否存在
|
||||
2. Check `index_updated` field in write_smart return / 检查返回字段
|
||||
3. Check error logs / 查看错误日志
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support / 支持
|
||||
|
||||
If issues, please check / 如有问题,请检查:
|
||||
1. OpenID format correct (starts with ou_) / 格式是否正确(ou_ 开头)
|
||||
2. Permissions enabled and published / 权限是否已开通并发布
|
||||
3. Index file path correct / 索引文件路径是否正确
|
||||
@@ -0,0 +1,486 @@
|
||||
#!/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"
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ownerId": "kn75sh5zhpf0spta18sqc7bd4s81dvr9",
|
||||
"slug": "feishu-smart-doc-writer",
|
||||
"version": "1.4.1",
|
||||
"publishedAt": 1771834474198
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"openid": "ou_5b921cba0fd6e7c885276a02d730ec19", "permission_checked": true}
|
||||
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
使用示例 - 展示如何在 OpenClaw Skill 中使用 feishu_smart_doc_writer
|
||||
"""
|
||||
|
||||
# 示例 1: 在 Skill 中写入长文档
|
||||
async def example_write_long_doc(ctx):
|
||||
"""
|
||||
在 OpenClaw Skill 中写入长文档
|
||||
|
||||
ctx 是 OpenClaw 提供的上下文对象
|
||||
"""
|
||||
from feishu_smart_doc_writer import FeishuDocWriter, ChunkConfig
|
||||
|
||||
# 创建写入器,传入 ctx
|
||||
config = ChunkConfig(
|
||||
chunk_size=2000, # 每块2000字符
|
||||
show_progress=True, # 显示进度
|
||||
max_retries=3 # 失败重试3次
|
||||
)
|
||||
writer = FeishuDocWriter(ctx, config)
|
||||
|
||||
# 准备长内容
|
||||
long_content = """
|
||||
# 项目策划文档
|
||||
|
||||
## 一、项目背景
|
||||
|
||||
这是项目的详细背景介绍...
|
||||
|
||||
## 二、技术方案
|
||||
|
||||
| 技术 | 说明 | 优势 |
|
||||
|------|------|------|
|
||||
| Python | 编程语言 | 生态丰富 |
|
||||
| OpenClaw | 自动化框架 | 功能强大 |
|
||||
|
||||
## 三、实施计划
|
||||
|
||||
1. 第一阶段...
|
||||
2. 第二阶段...
|
||||
3. 第三阶段...
|
||||
|
||||
""" * 100 # 重复100次,生成约50000字符的长内容
|
||||
|
||||
# 写入文档
|
||||
doc_url = await writer.write_document(
|
||||
title="项目策划 - 完整版",
|
||||
content=long_content,
|
||||
folder_token=None # 可选:指定文件夹
|
||||
)
|
||||
|
||||
print(f"✅ 文档创建成功: {doc_url}")
|
||||
return doc_url
|
||||
|
||||
|
||||
# 示例 2: 追加内容到现有文档
|
||||
async def example_append_to_doc(ctx, doc_url):
|
||||
"""
|
||||
向已有文档追加内容
|
||||
"""
|
||||
from feishu_smart_doc_writer import FeishuDocWriter
|
||||
|
||||
writer = FeishuDocWriter(ctx)
|
||||
|
||||
# 追加内容
|
||||
append_content = """
|
||||
## 四、补充说明
|
||||
|
||||
这是后来补充的内容...
|
||||
""" * 50
|
||||
|
||||
success = await writer.append_to_document(
|
||||
doc_url=doc_url,
|
||||
content=append_content
|
||||
)
|
||||
|
||||
if success:
|
||||
print("✅ 内容追加成功")
|
||||
else:
|
||||
print("❌ 内容追加失败")
|
||||
|
||||
return success
|
||||
|
||||
|
||||
# 示例 3: 在 Skill 定义中使用
|
||||
SKILL_DEFINITION = {
|
||||
"name": "my_skill",
|
||||
"tools": [
|
||||
{
|
||||
"name": "create_project_doc",
|
||||
"description": "创建项目文档",
|
||||
"handler": "create_project_doc_handler"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
async def create_project_doc_handler(ctx, args):
|
||||
"""
|
||||
Skill 工具处理函数
|
||||
"""
|
||||
from feishu_smart_doc_writer import FeishuDocWriter
|
||||
|
||||
title = args.get("title", "未命名文档")
|
||||
content = args.get("content", "")
|
||||
|
||||
writer = FeishuDocWriter(ctx)
|
||||
doc_url = await writer.write_document(title, content)
|
||||
|
||||
return {
|
||||
"doc_url": doc_url,
|
||||
"message": "文档创建成功"
|
||||
}
|
||||
|
||||
|
||||
# 示例 4: 直接使用同步函数(简化版)
|
||||
def example_sync_usage(ctx):
|
||||
"""
|
||||
同步方式使用(在不需要async的环境中)
|
||||
"""
|
||||
from feishu_smart_doc_writer import write_document_sync
|
||||
|
||||
doc_url = write_document_sync(
|
||||
ctx=ctx,
|
||||
title="同步创建的文档",
|
||||
content="# 标题\n\n大量内容..." * 1000
|
||||
)
|
||||
|
||||
return doc_url
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Feishu Smart Doc Writer 使用示例")
|
||||
print("=" * 50)
|
||||
print()
|
||||
print("在 OpenClaw Skill 中使用:")
|
||||
print()
|
||||
print("1. 导入:")
|
||||
print(" from feishu_smart_doc_writer import FeishuDocWriter")
|
||||
print()
|
||||
print("2. 创建写入器:")
|
||||
print(" writer = FeishuDocWriter(ctx)")
|
||||
print()
|
||||
print("3. 写入长文档:")
|
||||
print(" doc_url = await writer.write_document(title, content)")
|
||||
print()
|
||||
print("4. 追加内容:")
|
||||
print(" await writer.append_to_document(doc_url, content)")
|
||||
@@ -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))
|
||||
@@ -0,0 +1,349 @@
|
||||
"""
|
||||
Feishu 文档索引管理器
|
||||
负责管理 memory/feishu-docs-index.md 文件
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Optional, Tuple
|
||||
|
||||
|
||||
class IndexManager:
|
||||
"""飞书文档索引管理器"""
|
||||
|
||||
DEFAULT_INDEX_PATH = os.path.expanduser("~/.openclaw/workspace/memory/feishu-docs-index.md")
|
||||
|
||||
def __init__(self, index_path: str = None):
|
||||
self.index_path = index_path or self.DEFAULT_INDEX_PATH
|
||||
self._ensure_index_exists()
|
||||
|
||||
def _ensure_index_exists(self):
|
||||
"""确保索引文件存在"""
|
||||
if not os.path.exists(self.index_path):
|
||||
os.makedirs(os.path.dirname(self.index_path), exist_ok=True)
|
||||
self._create_empty_index()
|
||||
|
||||
def _create_empty_index(self):
|
||||
"""创建空索引文件"""
|
||||
content = """# 飞书云文档索引
|
||||
|
||||
**用途:** 快速定位和管理所有飞书云文档
|
||||
|
||||
---
|
||||
|
||||
## 📊 文档列表
|
||||
|
||||
| 序号 | 文档名 | 类型 | 链接 | 内容摘要 | 状态 | 最后更新 | 标签 | 所有者 |
|
||||
|------|--------|------|------|----------|------|----------|------|--------|
|
||||
|
||||
---
|
||||
|
||||
## 📂 按类型分类
|
||||
|
||||
### 项目管理
|
||||
|
||||
### 技术文档
|
||||
|
||||
### 每日归档
|
||||
|
||||
### 更新记录
|
||||
|
||||
### 数据分析
|
||||
|
||||
---
|
||||
|
||||
## 🔍 快速查找
|
||||
|
||||
*暂无关键词索引*
|
||||
|
||||
---
|
||||
|
||||
## 📝 使用说明
|
||||
|
||||
**添加新文档时:**
|
||||
1. 复制表格中的一行
|
||||
2. 填写所有字段
|
||||
3. 更新分类索引
|
||||
|
||||
**查找文档时:**
|
||||
1. 先搜索本文档中的关键词
|
||||
2. 找到对应链接
|
||||
3. 用 `feishu_doc` 工具读取内容
|
||||
|
||||
---
|
||||
*创建时间:{date}*
|
||||
*最后更新:{date}*
|
||||
""".format(date=datetime.now().strftime("%Y-%m-%d"))
|
||||
|
||||
with open(self.index_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
def add_or_update_doc(self, name: str, doc_type: str, url: str, token: str,
|
||||
summary: str = "", status: str = "已完成",
|
||||
tags: List[str] = None, owner: str = "") -> bool:
|
||||
"""
|
||||
添加或更新文档到索引
|
||||
|
||||
Args:
|
||||
name: 文档名称
|
||||
doc_type: 文档类型 (docx, sheet, bitable 等)
|
||||
url: 文档链接
|
||||
token: 文档token
|
||||
summary: 内容摘要
|
||||
status: 文档状态
|
||||
tags: 标签列表
|
||||
owner: 所有者
|
||||
|
||||
Returns:
|
||||
是否成功
|
||||
"""
|
||||
try:
|
||||
# 读取现有索引
|
||||
with open(self.index_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# 检查文档是否已存在(通过token判断)
|
||||
existing_index = self._find_doc_index(content, token)
|
||||
|
||||
# 获取当前序号
|
||||
if existing_index:
|
||||
doc_index = existing_index
|
||||
else:
|
||||
doc_index = self._get_next_index(content)
|
||||
|
||||
# 准备标签字符串
|
||||
tags_str = ", ".join(tags) if tags else ""
|
||||
|
||||
# 准备表格行
|
||||
now = datetime.now().strftime("%Y-%m-%d")
|
||||
new_row = f"| {doc_index} | {name} | {doc_type} | {url} | {summary} | {status} | {now} | {tags_str} | {owner} |"
|
||||
|
||||
# 更新表格
|
||||
if existing_index:
|
||||
# 更新现有行
|
||||
content = self._replace_table_row(content, token, new_row)
|
||||
else:
|
||||
# 添加新行
|
||||
content = self._insert_table_row(content, new_row)
|
||||
|
||||
# 更新分类(如果新文档)
|
||||
if not existing_index and tags:
|
||||
content = self._update_categories(content, name, tags)
|
||||
|
||||
# 更新关键词索引
|
||||
if not existing_index:
|
||||
content = self._update_keywords(content, name, summary, tags)
|
||||
|
||||
# 更新最后更新时间
|
||||
content = self._update_last_modified(content)
|
||||
|
||||
# 写回文件
|
||||
with open(self.index_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"更新索引失败: {e}")
|
||||
return False
|
||||
|
||||
def search_docs(self, keyword: str, search_in: List[str] = None) -> List[Dict]:
|
||||
"""
|
||||
搜索文档
|
||||
|
||||
Args:
|
||||
keyword: 搜索关键词
|
||||
search_in: 在哪些字段中搜索 (name, summary, tags)
|
||||
|
||||
Returns:
|
||||
匹配的文档列表
|
||||
"""
|
||||
if search_in is None:
|
||||
search_in = ["name", "summary", "tags"]
|
||||
|
||||
results = []
|
||||
docs = self._parse_table()
|
||||
|
||||
keyword_lower = keyword.lower()
|
||||
|
||||
for doc in docs:
|
||||
match = False
|
||||
if "name" in search_in and keyword_lower in doc.get("name", "").lower():
|
||||
match = True
|
||||
if "summary" in search_in and keyword_lower in doc.get("summary", "").lower():
|
||||
match = True
|
||||
if "tags" in search_in and keyword_lower in doc.get("tags", "").lower():
|
||||
match = True
|
||||
|
||||
if match:
|
||||
results.append(doc)
|
||||
|
||||
return results
|
||||
|
||||
def list_docs(self, tag: str = None, status: str = None, limit: int = None) -> List[Dict]:
|
||||
"""
|
||||
列出文档
|
||||
|
||||
Args:
|
||||
tag: 按标签筛选
|
||||
status: 按状态筛选
|
||||
limit: 限制数量
|
||||
|
||||
Returns:
|
||||
文档列表
|
||||
"""
|
||||
docs = self._parse_table()
|
||||
|
||||
# 筛选
|
||||
if tag:
|
||||
docs = [d for d in docs if tag in d.get("tags", "")]
|
||||
|
||||
if status:
|
||||
docs = [d for d in docs if d.get("status") == status]
|
||||
|
||||
# 限制数量
|
||||
if limit:
|
||||
docs = docs[:limit]
|
||||
|
||||
return docs
|
||||
|
||||
def get_doc_by_token(self, token: str) -> Optional[Dict]:
|
||||
"""通过token获取文档信息"""
|
||||
docs = self._parse_table()
|
||||
for doc in docs:
|
||||
if token in doc.get("link", ""):
|
||||
return doc
|
||||
return None
|
||||
|
||||
def _parse_table(self) -> List[Dict]:
|
||||
"""解析索引表格"""
|
||||
docs = []
|
||||
|
||||
try:
|
||||
with open(self.index_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# 找到表格部分
|
||||
lines = content.split('\n')
|
||||
in_table = False
|
||||
|
||||
for line in lines:
|
||||
if line.startswith('| 序号 '):
|
||||
in_table = True
|
||||
continue
|
||||
if in_table and line.startswith('|---'):
|
||||
continue
|
||||
if in_table and line.startswith('|') and not line.startswith('|------'):
|
||||
# 解析表格行
|
||||
parts = [p.strip() for p in line.split('|')[1:-1]]
|
||||
if len(parts) >= 8:
|
||||
# 兼容8列表格格式(序号、文档名、类型、链接、摘要、状态、更新时间、备注)
|
||||
docs.append({
|
||||
"index": parts[0],
|
||||
"name": parts[1],
|
||||
"type": parts[2],
|
||||
"link": parts[3],
|
||||
"summary": parts[4],
|
||||
"status": parts[5],
|
||||
"updated": parts[6],
|
||||
"tags": parts[7], # 使用"备注"列作为标签
|
||||
"owner": "" # 所有者信息暂时为空
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"解析索引失败: {e}")
|
||||
|
||||
return docs
|
||||
|
||||
def _find_doc_index(self, content: str, token: str) -> Optional[str]:
|
||||
"""查找文档是否已存在,返回序号"""
|
||||
pattern = r'\| (\d+) \| [^|]+ \| [^|]+ \| [^/]+/docx/' + re.escape(token) + r'[^|]* \|'
|
||||
match = re.search(pattern, content)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return None
|
||||
|
||||
def _get_next_index(self, content: str) -> int:
|
||||
"""获取下一个序号"""
|
||||
pattern = r'\| (\d+) \|'
|
||||
matches = re.findall(pattern, content)
|
||||
if matches:
|
||||
return max([int(m) for m in matches]) + 1
|
||||
return 1
|
||||
|
||||
def _replace_table_row(self, content: str, token: str, new_row: str) -> str:
|
||||
"""替换表格中的某一行"""
|
||||
pattern = r'(\| \d+ \| [^|]+ \| [^|]+ \| [^/]+/docx/' + re.escape(token) + r'[^|]* \|[^\n]+)'
|
||||
return re.sub(pattern, new_row, content)
|
||||
|
||||
def _insert_table_row(self, content: str, new_row: str) -> str:
|
||||
"""插入新行到表格"""
|
||||
# 在表格头部后插入
|
||||
lines = content.split('\n')
|
||||
insert_index = -1
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
if line.startswith('| 序号 '):
|
||||
insert_index = i + 2 # 跳过表头和分隔线
|
||||
break
|
||||
|
||||
if insert_index > 0:
|
||||
lines.insert(insert_index, new_row)
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
def _update_categories(self, content: str, name: str, tags: List[str]) -> str:
|
||||
"""更新分类列表"""
|
||||
# 简单实现:在对应分类下添加文档名
|
||||
# 这里可以扩展更复杂的逻辑
|
||||
return content
|
||||
|
||||
def _update_keywords(self, content: str, name: str, summary: str, tags: List[str]) -> str:
|
||||
"""更新关键词索引"""
|
||||
# 提取关键词
|
||||
keywords = []
|
||||
if tags:
|
||||
keywords.extend(tags)
|
||||
|
||||
# 这里可以添加更智能的关键词提取
|
||||
# 暂时保持简单
|
||||
|
||||
return content
|
||||
|
||||
def _update_last_modified(self, content: str) -> str:
|
||||
"""更新最后修改时间"""
|
||||
now = datetime.now().strftime("%Y-%m-%d")
|
||||
pattern = r'\*最后更新:[^*]+\*'
|
||||
replacement = f'*最后更新:{now}*'
|
||||
return re.sub(pattern, replacement, content)
|
||||
|
||||
|
||||
# 便捷函数
|
||||
def add_doc_to_index(name: str, url: str, token: str, summary: str = "",
|
||||
tags: List[str] = None, owner: str = "") -> bool:
|
||||
"""便捷函数:添加文档到索引"""
|
||||
manager = IndexManager()
|
||||
return manager.add_or_update_doc(
|
||||
name=name,
|
||||
doc_type="docx",
|
||||
url=url,
|
||||
token=token,
|
||||
summary=summary,
|
||||
status="已完成",
|
||||
tags=tags,
|
||||
owner=owner
|
||||
)
|
||||
|
||||
|
||||
def search_docs(keyword: str) -> List[Dict]:
|
||||
"""便捷函数:搜索文档"""
|
||||
manager = IndexManager()
|
||||
return manager.search_docs(keyword)
|
||||
|
||||
|
||||
def list_all_docs(tag: str = None) -> List[Dict]:
|
||||
"""便捷函数:列出所有文档"""
|
||||
manager = IndexManager()
|
||||
return manager.list_docs(tag=tag)
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "feishu-smart-doc-writer",
|
||||
"version": "1.4.1",
|
||||
"description": "Feishu/Lark Smart Document Writer v1.4.1 - 飞书智能文档写入器. Auto-chunk writing, auto ownership transfer, document index management with search/list. Config OpenID on first use.",
|
||||
"author": "OpenClaw User",
|
||||
"license": "MIT",
|
||||
"tags": [
|
||||
"feishu",
|
||||
"lark",
|
||||
"飞书",
|
||||
"document",
|
||||
"docx",
|
||||
"writer",
|
||||
"chunk",
|
||||
"smart",
|
||||
"ownership-transfer",
|
||||
"文档",
|
||||
"写入"
|
||||
],
|
||||
"homepage": "https://github.com/openclaw/openclaw",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/openclaw/openclaw"
|
||||
},
|
||||
"keywords": [
|
||||
"feishu",
|
||||
"lark",
|
||||
"飞书",
|
||||
"document",
|
||||
"writer",
|
||||
"chunk",
|
||||
"split",
|
||||
"api-limit",
|
||||
"ownership-transfer",
|
||||
"文档写入",
|
||||
"长文档"
|
||||
],
|
||||
"openclaw": {
|
||||
"minVersion": "2026.2.0",
|
||||
"tools": [
|
||||
"write_smart",
|
||||
"append_smart",
|
||||
"search_docs",
|
||||
"list_docs",
|
||||
"transfer_ownership",
|
||||
"configure",
|
||||
"get_config_status"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
{
|
||||
"name": "feishu-smart-doc-writer",
|
||||
"version": "1.4.1",
|
||||
"description": "Feishu/Lark Smart Document Writer v1.4.1 - 飞书智能文档写入器. Auto-chunk writing, auto ownership transfer, document index management with search/list. Config OpenID on first use.",
|
||||
"author": "OpenClaw",
|
||||
"license": "MIT",
|
||||
"tools": [
|
||||
{
|
||||
"name": "write_smart",
|
||||
"description": "Smart create Feishu/Lark doc with auto-chunk writing and ownership transfer. Guides OpenID config on first use. 智能创建飞书文档,自动分块写入,自动转移所有权。首次使用时引导配置OpenID。",
|
||||
"handler": "feishu_smart_doc_writer.write_smart",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Document title / 文档标题"
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "Document content (long content auto-chunked) / 文档内容(支持长内容,会自动分块处理)"
|
||||
},
|
||||
"folder_token": {
|
||||
"type": "string",
|
||||
"description": "Optional folder token / 可选的文件夹token"
|
||||
},
|
||||
"chunk_size": {
|
||||
"type": "integer",
|
||||
"description": "Max chars per chunk, default 2000 / 每块最大字符数,默认2000",
|
||||
"default": 2000
|
||||
},
|
||||
"show_progress": {
|
||||
"type": "boolean",
|
||||
"description": "Show progress / 是否显示写入进度",
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"required": ["title", "content"]
|
||||
},
|
||||
"returns": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"doc_url": {"type": "string"},
|
||||
"doc_token": {"type": "string"},
|
||||
"chunks_count": {"type": "integer"},
|
||||
"owner_transferred": {"type": "boolean"},
|
||||
"need_config": {"type": "boolean", "description": "Need config / 是否需要配置"},
|
||||
"message": {"type": "string"}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "configure",
|
||||
"description": "Configure Skill. Provide OpenID and confirm permission on first use. 配置Skill。首次使用时提供OpenID并确认权限已开通。",
|
||||
"handler": "feishu_smart_doc_writer.configure",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"openid": {
|
||||
"type": "string",
|
||||
"description": "Your Feishu/Lark OpenID (format: ou_xxxxxxxx) / 你的飞书OpenID(格式:ou_xxxxxxxx)"
|
||||
},
|
||||
"permission_checked": {
|
||||
"type": "boolean",
|
||||
"description": "Confirmed docs:permission.member:transfer permission granted / 是否已确认开通docs:permission.member:transfer权限",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"required": ["openid"]
|
||||
},
|
||||
"returns": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {"type": "boolean"},
|
||||
"openid": {"type": "string"},
|
||||
"message": {"type": "string"}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "append_smart",
|
||||
"description": "Smart append content to Feishu/Lark doc with auto-chunk. 智能追加内容到飞书文档,自动分块。",
|
||||
"handler": "feishu_smart_doc_writer.append_smart",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"doc_url": {"type": "string"},
|
||||
"content": {"type": "string"},
|
||||
"chunk_size": {"type": "integer", "default": 2000},
|
||||
"show_progress": {"type": "boolean", "default": true}
|
||||
},
|
||||
"required": ["doc_url", "content"]
|
||||
},
|
||||
"returns": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {"type": "boolean"},
|
||||
"chunks_count": {"type": "integer"},
|
||||
"message": {"type": "string"}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "transfer_ownership",
|
||||
"description": "Transfer Feishu/Lark doc ownership. Requires docs:permission.member:transfer permission. 转移飞书文档所有权。需要docs:permission.member:transfer权限。",
|
||||
"handler": "feishu_smart_doc_writer.transfer_ownership",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"doc_url": {"type": "string"},
|
||||
"owner_openid": {"type": "string"}
|
||||
},
|
||||
"required": ["doc_url", "owner_openid"]
|
||||
},
|
||||
"returns": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {"type": "boolean"},
|
||||
"message": {"type": "string"}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "get_config_status",
|
||||
"description": "Get current config status to check if OpenID is configured. 获取当前配置状态,查看是否已配置OpenID。",
|
||||
"handler": "feishu_smart_doc_writer.get_config_status",
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
"returns": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"configured": {"type": "boolean"},
|
||||
"openid": {"type": "string"},
|
||||
"first_time": {"type": "boolean"},
|
||||
"message": {"type": "string"}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "search_docs",
|
||||
"description": "Search local document index by keywords. Searches in document name, summary, and tags. 搜索本地文档索引,支持关键词搜索。",
|
||||
"handler": "index_manager.search_docs",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"keyword": {
|
||||
"type": "string",
|
||||
"description": "Search keyword / 搜索关键词"
|
||||
}
|
||||
},
|
||||
"required": ["keyword"]
|
||||
},
|
||||
"returns": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"link": {"type": "string"},
|
||||
"summary": {"type": "string"},
|
||||
"tags": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "list_docs",
|
||||
"description": "List all Feishu documents from local index. Optionally filter by tag. 列出所有飞书文档,可按标签筛选。",
|
||||
"handler": "index_manager.list_all_docs",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tag": {
|
||||
"type": "string",
|
||||
"description": "Optional tag filter / 可选的标签筛选"
|
||||
}
|
||||
}
|
||||
},
|
||||
"returns": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"link": {"type": "string"},
|
||||
"summary": {"type": "string"},
|
||||
"tags": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"dependencies": []
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"owner_openid": "ou_5b921cba0fd6e7c885276a02d730ec19",
|
||||
"permission_noted": true,
|
||||
"first_time": false
|
||||
}
|
||||
Reference in New Issue
Block a user