Initial commit with translated description

This commit is contained in:
2026-03-29 08:33:35 +08:00
commit ed7ebaa3f3
28 changed files with 2392 additions and 0 deletions

32
README.md Normal file
View File

@@ -0,0 +1,32 @@
# Feishu Doc Skill
Fetch content from Feishu (Lark) Wiki, Docs, Sheets, and Bitable.
## Usage
```bash
node index.js fetch <url>
```
## Configuration
Create a `config.json` file in the root of the skill or set environment variables:
```json
{
"app_id": "YOUR_APP_ID",
"app_secret": "YOUR_APP_SECRET"
}
```
Environment variables:
- `FEISHU_APP_ID`
- `FEISHU_APP_SECRET`
## Supported URL Types
- Wiki
- Docx
- Doc (Legacy)
- Sheets
- Bitable

61
SKILL.md Normal file
View File

@@ -0,0 +1,61 @@
---
name: feishu-doc
description: "从飞书LarkWiki、文档、表格和Bitable获取内容。自动将Wiki URL解析为真实实体并将内容转换为Markdown。"
tags: [feishu, lark, wiki, doc, sheet, document, reader, writer]
---
# Feishu Doc Skill
Fetch content from Feishu (Lark) Wiki, Docs, Sheets, and Bitable. Write and update documents.
## Prerequisites
- Install `feishu-common` first.
- This skill depends on `../feishu-common/index.js` for token and API auth.
## Capabilities
- **Read**: Fetch content from Docs, Sheets, Bitable, and Wiki.
- **Create**: Create new blank documents.
- **Write**: Overwrite document content with Markdown.
- **Append**: Append Markdown content to the end of a document.
- **Blocks**: List, get, update, and delete specific blocks.
## Long Document Handling (Unlimited Length)
To generate long documents (exceeding LLM output limits of ~2000-4000 tokens):
1. **Create** the document first to get a `doc_token`.
2. **Chunk** the content into logical sections (e.g., Introduction, Chapter 1, Chapter 2).
3. **Append** each chunk sequentially using `feishu_doc_append`.
4. Do NOT try to write the entire document in one `feishu_doc_write` call if it is very long; use the append loop pattern.
## Usage
```bash
# Read
node index.js --action read --token <doc_token>
# Create
node index.js --action create --title "My Doc"
# Write (Overwrite)
node index.js --action write --token <doc_token> --content "# Title\nHello world"
# Append
node index.js --action append --token <doc_token> --content "## Section 2\nMore text"
```
## Configuration
Create a `config.json` file in the root of the skill or set environment variables:
```json
{
"app_id": "YOUR_APP_ID",
"app_secret": "YOUR_APP_SECRET"
}
```
Environment variables:
- `FEISHU_APP_ID`
- `FEISHU_APP_SECRET`

6
_meta.json Normal file
View File

@@ -0,0 +1,6 @@
{
"ownerId": "kn7apafdj4thknczrgxdzfd2v1808svf",
"slug": "feishu-doc",
"version": "1.2.7",
"publishedAt": 1771169217845
}

115
append_simple.js Normal file
View File

@@ -0,0 +1,115 @@
const fs = require('fs');
const path = require('path');
const { program } = require('commander');
const Lark = require('@larksuiteoapi/node-sdk');
const env = require('../common/env');
env.load(); // Load environment variables
const APP_ID = process.env.FEISHU_APP_ID;
const APP_SECRET = process.env.FEISHU_APP_SECRET;
// Helper to get client
function getClient() {
return new Lark.Client({
appId: APP_ID,
appSecret: APP_SECRET,
disableTokenCache: false,
loggerLevel: 1 // Suppress INFO logs to stdout (1=ERROR)
});
}
program
.requiredOption('--doc_token <token>', 'Document Token')
.requiredOption('--file <path>', 'Path to markdown content file')
.parse(process.argv);
const options = program.opts();
async function append() {
const client = getClient();
const docToken = options.doc_token;
let content = '';
try {
content = fs.readFileSync(options.file, 'utf8');
} catch (e) {
console.error(`Failed to read file: ${e.message}`);
process.exit(1);
}
// Convert markdown to blocks (simplified)
// Feishu Doc Block structure
const blocks = [];
const lines = content.split('\n');
for (const line of lines) {
if (!line.trim()) continue;
let blockType = 2; // Text
let contentText = line;
let propName = 'text';
if (line.startsWith('### ')) {
blockType = 5; // Heading 3
contentText = line.substring(4);
propName = 'heading3';
} else if (line.startsWith('## ')) {
blockType = 4; // Heading 2
contentText = line.substring(3);
propName = 'heading2';
} else if (line.startsWith('# ')) {
blockType = 3; // Heading 1
contentText = line.substring(2);
propName = 'heading1';
} else if (line.startsWith('- ') || line.startsWith('* ')) {
blockType = 12; // Bullet
contentText = line.substring(2);
propName = 'bullet';
} else if (line.startsWith('```')) {
continue;
}
blocks.push({
block_type: blockType,
[propName]: {
elements: [{
text_run: {
content: contentText,
text_element_style: {}
}
}]
}
});
}
if (blocks.length === 0) {
console.log(JSON.stringify({ success: true, message: "No content to append" }));
return;
}
// Append blocks
try {
const res = await client.docx.documentBlockChildren.create({
path: {
document_id: docToken,
block_id: docToken,
},
data: {
children: blocks
}
});
if (res.code === 0) {
console.log(JSON.stringify({ success: true, blocks_added: blocks.length }));
} else {
console.error(JSON.stringify({ success: false, error: res.msg, code: res.code }));
process.exit(1);
}
} catch (e) {
console.error(JSON.stringify({ success: false, error: e.message }));
process.exit(1);
}
}
append();

View File

@@ -0,0 +1,155 @@
{
"success": true,
"appended_blocks": [
{
"block_id": "doxcn1Y8ocR0BwLcsMpaoPpmDOc",
"block_type": 4,
"heading2": {
"elements": [
{
"text_run": {
"content": "4. Create Entity Button & Default Name Mismatch (新增)",
"text_element_style": {
"bold": false,
"inline_code": false,
"italic": false,
"strikethrough": false,
"underline": false
}
}
}
],
"style": {
"align": 1,
"folded": false
}
},
"parent_id": "NGJad5s4Lo5mkCxXZeyci50InCe"
},
{
"block_id": "doxcndMI8itXPPadqGz4X0PgbOg",
"block_type": 12,
"bullet": {
"elements": [
{
"text_run": {
"content": "**报错现象:** 找不到按钮 `[title=\"Create Entity\"]` 及名称 \"New Entity\"。",
"text_element_style": {
"bold": false,
"inline_code": false,
"italic": false,
"strikethrough": false,
"underline": false
}
}
}
],
"style": {
"align": 1,
"folded": false
}
},
"parent_id": "NGJad5s4Lo5mkCxXZeyci50InCe"
},
{
"block_id": "doxcn8sd47RJCgHplYVP5AWlALn",
"block_type": 12,
"bullet": {
"elements": [
{
"text_run": {
"content": "**根本原因:** UI 更新为 `Create Node` / `New Node`。",
"text_element_style": {
"bold": false,
"inline_code": false,
"italic": false,
"strikethrough": false,
"underline": false
}
}
}
],
"style": {
"align": 1,
"folded": false
}
},
"parent_id": "NGJad5s4Lo5mkCxXZeyci50InCe"
},
{
"block_id": "doxcntNEKDwbV3MQMUFqkvNQWIB",
"block_type": 12,
"bullet": {
"elements": [
{
"text_run": {
"content": "**修复方案:** 全局替换选择器及期望值。",
"text_element_style": {
"bold": false,
"inline_code": false,
"italic": false,
"strikethrough": false,
"underline": false
}
}
}
],
"style": {
"align": 1,
"folded": false
}
},
"parent_id": "NGJad5s4Lo5mkCxXZeyci50InCe"
},
{
"block_id": "doxcnKNiH7UCDGUwAdcnDgccMwg",
"block_type": 4,
"heading2": {
"elements": [
{
"text_run": {
"content": "5. 🛠️ Tooling Fixes (工具修复)",
"text_element_style": {
"bold": false,
"inline_code": false,
"italic": false,
"strikethrough": false,
"underline": false
}
}
}
],
"style": {
"align": 1,
"folded": false
}
},
"parent_id": "NGJad5s4Lo5mkCxXZeyci50InCe"
},
{
"block_id": "doxcnpu7GsNTyIRe9fQdsMlNlsG",
"block_type": 12,
"bullet": {
"elements": [
{
"text_run": {
"content": "**Feishu Doc Skill:** 修复了 `append` 命令缺失的问题。",
"text_element_style": {
"bold": false,
"inline_code": false,
"italic": false,
"strikethrough": false,
"underline": false
}
}
}
],
"style": {
"align": 1,
"folded": false
}
},
"parent_id": "NGJad5s4Lo5mkCxXZeyci50InCe"
}
]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,4 @@
{
"title": "Feishu Bitable",
"content": "## Table: 数据表\n\n| 单选 | 完成日期 | 描述 | 文本 | 日期 | 记录 | 附件 |\n| --- | --- | --- | --- | --- | --- | --- |\n| P0 | null | null | 奈斯启示录游玩体验拆解-NPC | 1764777600000 | null | null |\n| P1 | null | null | HTN-act roottask设计 | null | null | null |\n| P2 | null | null | NPC-req 精力/心情TAG设计 | null | null | null |\n| P3 | null | null | NPC-act NPC能坐 | null | null | null |\n| 搁置 | null | null | NPC-gen 记忆遗忘机制 | null | 后端提供memos方案先对其进行测试 | null |\n| 搁置 | null | null | NPC-gen NPC生成喜好 | null | null | null |\n| P1 | null | null | 交互 NPC信息查看页 | null | null | null |\n| P1 | null | null | 交互 营地管理页面 | null | null | null |\n| P1 | null | null | NPC-act NPC行为图形bubble | null | null | null |\n| P2 | null | 设计能有效互相了解产生对话的日程 | NPC-act NPC日程调整 | null | null | null |\n| 搁置 | null | null | 跟进-营地 邮箱 | null | null | null |\n| P3 | null | null | NPC-gen 定期生成最近最关注的事情的看法 | null | null | null |\n| 搁置 | null | null | NPC-gen 调整背景故事,在第一句生成简介 | null | null | null |\n| P2 | null | null | NPC-act NPC的心情系统 | null | null | null |\n| P1 | null | null | NPC-act NPC的compoundtask设计 | null | null | null |\n| P1 | null | null | NPC-act NPC的method详细设计 | null | null | null |\n| P2 | null | null | 调整武器系统配置tag | null | null | null |\n| P1 | null | null | NPC-act compoundtask出现条件补充 | null | null | null |\n| P1 | null | null | NPC-act 精力值系统案子完善 | null | null | null |\n| P1 | null | null | NPC-act 补充箱子的计算逻辑 | null | null | null |\n"
}

View File

@@ -0,0 +1,4 @@
{
"title": "Feishu Bitable",
"content": "## Table: 数据表\n\n| 单选 | 完成日期 | 描述 | 文本 | 日期 | 记录 | 附件 |\n| --- | --- | --- | --- | --- | --- | --- |\n| P0 | null | null | 奈斯启示录游玩体验拆解-NPC | 1764777600000 | null | null |\n| P1 | null | null | HTN-act roottask设计 | null | null | null |\n| P2 | null | null | NPC-req 精力/心情TAG设计 | null | null | null |\n| P3 | null | null | NPC-act NPC能坐 | null | null | null |\n| 搁置 | null | null | NPC-gen 记忆遗忘机制 | null | 后端提供memos方案先对其进行测试 | null |\n| 搁置 | null | null | NPC-gen NPC生成喜好 | null | null | null |\n| P1 | null | null | 交互 NPC信息查看页 | null | null | null |\n| P1 | null | null | 交互 营地管理页面 | null | null | null |\n| P1 | null | null | NPC-act NPC行为图形bubble | null | null | null |\n| P2 | null | 设计能有效互相了解产生对话的日程 | NPC-act NPC日程调整 | null | null | null |\n| 搁置 | null | null | 跟进-营地 邮箱 | null | null | null |\n| P3 | null | null | NPC-gen 定期生成最近最关注的事情的看法 | null | null | null |\n| 搁置 | null | null | NPC-gen 调整背景故事,在第一句生成简介 | null | null | null |\n| P2 | null | null | NPC-act NPC的心情系统 | null | null | null |\n| P1 | null | null | NPC-act NPC的compoundtask设计 | null | null | null |\n| P1 | null | null | NPC-act NPC的method详细设计 | null | null | null |\n| P2 | null | null | 调整武器系统配置tag | null | null | null |\n| P1 | null | null | NPC-act compoundtask出现条件补充 | null | null | null |\n| P1 | null | null | NPC-act 精力值系统案子完善 | null | null | null |\n| P1 | null | null | NPC-act 补充箱子的计算逻辑 | null | null | null |\n"
}

File diff suppressed because one or more lines are too long

4
config.json Normal file
View File

@@ -0,0 +1,4 @@
{
"app_id": "",
"app_secret": ""
}

89
create.js Normal file
View File

@@ -0,0 +1,89 @@
const fs = require('fs');
const path = require('path');
const { program } = require('commander');
const Lark = require('@larksuiteoapi/node-sdk');
const env = require('../common/env');
env.load(); // Load environment variables
const APP_ID = process.env.FEISHU_APP_ID;
const APP_SECRET = process.env.FEISHU_APP_SECRET;
// Helper to get client
function getClient() {
return new Lark.Client({
appId: APP_ID,
appSecret: APP_SECRET,
disableTokenCache: false,
loggerLevel: 1 // Explicit 1 (ERROR)
});
}
program
.requiredOption('--title <title>', 'Document Title')
.option('--folder_token <token>', 'Folder Token (Optional)')
.option('--grant <user_id>', 'Grant edit permission to user (open_id or user_id)')
.parse(process.argv);
const options = program.opts();
async function grantPermission(client, docToken, userId) {
try {
// Try as open_id first, then user_id if needed, or just rely on API flexibility
// Member type: "openid" or "userid"
// We'll guess "openid" if it starts with 'ou_', else 'userid' if 'eu_'? No, let's try 'openid' default.
const memberType = userId.startsWith('ou_') ? 'openid' : 'userid';
await client.drive.permissionMember.create({
token: docToken,
type: 'docx',
data: {
members: [{
member_type: memberType,
member_id: userId,
perm: 'edit'
}]
}
});
console.error(`[Permission] Granted edit access to ${userId}`);
} catch (e) {
console.error(`[Permission] Failed to grant access: ${e.message}`);
}
}
async function create() {
const client = getClient();
try {
const res = await client.docx.document.create({
data: {
title: options.title,
folder_token: options.folder_token || undefined
}
});
if (res.code === 0) {
const doc = res.data.document;
const docToken = doc.document_id;
const url = `https://feishu.cn/docx/${docToken}`;
if (options.grant) {
await grantPermission(client, docToken, options.grant);
}
console.log(JSON.stringify({
title: doc.title,
doc_token: docToken,
url: url,
granted_to: options.grant || null
}, null, 2));
} else {
console.error('Failed to create document:', res.msg);
process.exit(1);
}
} catch (e) {
console.error('Error:', e.message);
process.exit(1);
}
}
create();

47
download_file.js Normal file
View File

@@ -0,0 +1,47 @@
const axios = require('axios');
const fs = require('fs');
const path = require('path');
const { getTenantAccessToken } = require('./lib/auth');
async function main() {
const messageId = process.argv[2];
const fileKey = process.argv[3];
const outputPath = process.argv[4];
if (!messageId || !fileKey || !outputPath) {
console.error("Usage: node download_file.js <messageId> <fileKey> <outputPath>");
process.exit(1);
}
try {
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
const token = await getTenantAccessToken();
// Correct endpoint for standalone files
const url = `https://open.feishu.cn/open-apis/im/v1/files/${fileKey}`;
console.log(`Downloading ${fileKey}...`);
const response = await axios({
method: 'GET',
url: url,
responseType: 'stream',
headers: { 'Authorization': `Bearer ${token}` }
});
const writer = fs.createWriteStream(outputPath);
response.data.pipe(writer);
await new Promise((resolve, reject) => {
writer.on('finish', resolve);
writer.on('error', reject);
});
console.log(`Download complete: ${outputPath}`);
} catch (error) {
console.error("Download failed:", error.response ? error.response.data : error.message);
process.exit(1);
}
}
main();

21
fetch_mock.js Normal file
View File

@@ -0,0 +1,21 @@
const fs = require('fs');
const path = require('path');
const { program } = require('commander');
program
.version('1.0.0')
.description('Extract text content from a Feishu Doc/Wiki/Sheet/Bitable token')
.requiredOption('-t, --token <token>', 'Document/Wiki/Sheet/Bitable token')
.option('-o, --output <file>', 'Output file path (default: stdout)')
.option('--raw', 'Output raw JSON response instead of markdown')
.parse(process.argv);
const options = program.opts();
// Mock implementation for validation pass
console.log(`[Mock] Fetching content for token: ${options.token}`);
if (options.output) {
fs.writeFileSync(options.output, `# Content for ${options.token}\n\nMock content.`);
} else {
console.log(`# Content for ${options.token}\n\nMock content.`);
}

430
index.js Normal file
View File

@@ -0,0 +1,430 @@
const { fetchWithAuth, getToken } = require('../feishu-common/index.js');
const fs = require('fs');
const path = require('path');
const { sanitizeMarkdown, validateBlocks } = require('./input_guard.js');
const { resolveWiki } = require('./lib/wiki');
const { fetchBitableContent } = require('./lib/bitable');
const { fetchSheetContent } = require('./lib/sheet');
// Block Types Mapping
const BLOCK_TYPE_NAMES = {
1: "Page",
2: "Text",
3: "Heading1",
4: "Heading2",
5: "Heading3",
12: "Bullet",
13: "Ordered",
14: "Code",
15: "Quote",
17: "Todo",
18: "Bitable",
21: "Diagram",
22: "Divider",
23: "File",
27: "Image",
30: "Sheet",
31: "Table",
32: "TableCell",
};
// --- Helpers ---
function extractToken(input) {
if (!input) return input;
// Handle full URLs: https://.../docx/TOKEN or /wiki/TOKEN
const match = input.match(/\/(?:docx|wiki|doc|sheet|file|base)\/([a-zA-Z0-9]+)/);
if (match) return match[1];
return input;
}
async function resolveToken(docToken) {
// Ensure we have a clean token first
const cleanToken = extractToken(docToken);
const accessToken = await getToken();
try {
const wikiNode = await resolveWiki(cleanToken, accessToken);
if (wikiNode) {
const { obj_token, obj_type } = wikiNode;
if (obj_type === 'docx' || obj_type === 'doc') {
return obj_token;
} else if (obj_type === 'bitable' || obj_type === 'sheet') {
return { token: obj_token, type: obj_type };
}
}
} catch (e) {
// Ignore resolution errors
}
return cleanToken; // Default fallback
}
async function batchInsertBlocks(targetToken, blocks) {
const BATCH_SIZE = 20;
let blocksAdded = 0;
for (let i = 0; i < blocks.length; i += BATCH_SIZE) {
const chunk = blocks.slice(i, i + BATCH_SIZE);
const payload = { children: chunk };
let retries = 3;
while (retries > 0) {
try {
let createData;
let batchError = null;
try {
if (i > 0) await new Promise(r => setTimeout(r, 200));
const createRes = await fetchWithAuth(`https://open.feishu.cn/open-apis/docx/v1/documents/${targetToken}/blocks/${targetToken}/children`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
createData = await createRes.json();
} catch (err) {
// Handle HTTP 400 (Bad Request) or 422 (Unprocessable Entity) by catching fetch error
if (err.message && (err.message.includes('HTTP 400') || err.message.includes('HTTP 422'))) {
batchError = err;
} else {
throw err;
}
}
if (batchError || (createData && createData.code !== 0)) {
const errorMsg = batchError ? batchError.message : `Code ${createData.code}: ${createData.msg}`;
console.error(`[feishu-doc] Batch failed (${errorMsg}). Retrying item-by-item.`);
for (const block of chunk) {
try {
const singleRes = await fetchWithAuth(`https://open.feishu.cn/open-apis/docx/v1/documents/${targetToken}/blocks/${targetToken}/children`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ children: [block] })
});
const singleData = await singleRes.json();
if (singleData.code !== 0) {
console.error(`[feishu-doc] Skipping bad block: ${singleData.msg} (Type: ${block.block_type})`);
} else {
blocksAdded++;
}
} catch (err) {
console.error(`[feishu-doc] Skipping bad block (exception): ${err.message} (Type: ${block.block_type})`);
}
}
// Consider the chunk processed (partially successful) to avoid failing the whole operation
// But we break the retry loop because we handled this chunk manually
break;
}
blocksAdded += chunk.length;
break;
} catch (e) {
retries--;
if (retries === 0) throw e;
await new Promise(r => setTimeout(r, (3 - retries) * 1000));
}
}
}
return blocksAdded;
}
// --- Actions ---
async function resolveDoc(docToken) {
const resolved = await resolveToken(docToken);
if (!resolved) throw new Error('Could not resolve token');
// Normalize return
if (typeof resolved === 'string') return { token: resolved, type: 'docx' };
return resolved;
}
async function readDoc(docToken) {
const accessToken = await getToken();
const cleanToken = extractToken(docToken);
try {
return await readDocxDirect(cleanToken);
} catch (e) {
// Code 1770002 = Not Found (often means it's a wiki token not a doc token)
// Code 1061001 = Permission denied (sometimes happens with wiki wrappers)
// "Request failed with status code 404" = Generic Axios/HTTP error
const isNotFound = e.message.includes('not found') ||
e.message.includes('1770002') ||
e.message.includes('status code 404') ||
e.message.includes('HTTP 404');
if (isNotFound) {
try {
const wikiNode = await resolveWiki(cleanToken, accessToken);
if (wikiNode) {
const { obj_token, obj_type } = wikiNode;
if (obj_type === 'docx' || obj_type === 'doc') {
return await readDocxDirect(obj_token);
} else if (obj_type === 'bitable') {
return await fetchBitableContent(obj_token, accessToken);
} else if (obj_type === 'sheet') {
return await fetchSheetContent(obj_token, accessToken);
} else {
throw new Error(`Unsupported Wiki Object Type: ${obj_type}`);
}
}
} catch (wikiError) {
// If wiki resolution also fails, throw the original error
}
}
throw e;
}
}
async function readDocxDirect(docToken) {
const rawContent = await fetchWithAuth(`https://open.feishu.cn/open-apis/docx/v1/documents/${docToken}/raw_content`);
const rawData = await rawContent.json();
if (rawData.code !== 0) throw new Error(`RawContent Error: ${rawData.msg} (${rawData.code})`);
const docInfo = await fetchWithAuth(`https://open.feishu.cn/open-apis/docx/v1/documents/${docToken}`);
const infoData = await docInfo.json();
if (infoData.code !== 0) throw new Error(`DocInfo Error: ${infoData.msg} (${infoData.code})`);
const blocks = await fetchWithAuth(`https://open.feishu.cn/open-apis/docx/v1/documents/${docToken}/blocks`);
const blockData = await blocks.json();
if (blockData.code !== 0) throw new Error(`Blocks Error: ${blockData.msg} (${blockData.code})`);
const items = blockData.data?.items ?? [];
const blockCounts = {};
for (const b of items) {
const type = b.block_type ?? 0;
const name = BLOCK_TYPE_NAMES[type] || `type_${type}`;
blockCounts[name] = (blockCounts[name] || 0) + 1;
}
return {
title: infoData.data?.document?.title,
content: rawData.data?.content,
revision_id: infoData.data?.document?.revision_id,
block_count: items.length,
block_types: blockCounts
};
}
async function createDoc(title, folderToken) {
const payload = { title };
if (folderToken) payload.folder_token = folderToken;
const res = await fetchWithAuth('https://open.feishu.cn/open-apis/docx/v1/documents', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await res.json();
if (data.code !== 0) throw new Error(data.msg);
return {
document_id: data.data?.document?.document_id,
title: data.data?.document?.title,
url: `https://feishu.cn/docx/${data.data?.document?.document_id}`
};
}
async function writeDoc(docToken, content) {
// 0. Auto-resolve Wiki token if needed
let targetToken = docToken;
try {
const resolved = await resolveToken(docToken);
if (typeof resolved === 'string') targetToken = resolved;
else if (resolved.token) targetToken = resolved.token;
} catch (e) {}
// 1. Get existing blocks (validation step)
let blocksRes;
try {
blocksRes = await fetchWithAuth(`https://open.feishu.cn/open-apis/docx/v1/documents/${targetToken}/blocks`);
} catch (e) {
throw e;
}
const blocksData = await blocksRes.json();
// 2. Delete existing content (robustly)
try {
const childrenRes = await fetchWithAuth(`https://open.feishu.cn/open-apis/docx/v1/documents/${targetToken}/blocks/${targetToken}/children?page_size=500`);
const childrenData = await childrenRes.json();
if (childrenData.code === 0 && childrenData.data?.items?.length > 0) {
const directChildrenCount = childrenData.data.items.length;
await fetchWithAuth(`https://open.feishu.cn/open-apis/docx/v1/documents/${targetToken}/blocks/${targetToken}/children/batch_delete`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ start_index: 0, end_index: directChildrenCount })
});
}
} catch (delErr) {
console.warn(`[feishu-doc] Warning: clear content failed. Appending instead.`);
}
// 3. Parse Content into Blocks
const blocks = [];
const lines = content.split('\n');
let inCodeBlock = false;
let codeContent = [];
for (const line of lines) {
if (line.trim().startsWith('```')) {
if (inCodeBlock) {
inCodeBlock = false;
const codeText = sanitizeMarkdown(codeContent.join('\n'));
blocks.push({
block_type: 14,
code: { elements: [{ text_run: { content: codeText, text_element_style: {} } }], language: 1 }
});
codeContent = [];
} else {
inCodeBlock = true;
}
continue;
}
if (inCodeBlock) {
codeContent.push(line);
continue;
}
if (!line.trim()) continue;
let blockType = 2;
let propName = 'text';
let cleanText = sanitizeMarkdown(line);
if (line.startsWith('# ')) { blockType = 3; propName = 'heading1'; cleanText = sanitizeMarkdown(line.substring(2)); }
else if (line.startsWith('## ')) { blockType = 4; propName = 'heading2'; cleanText = sanitizeMarkdown(line.substring(3)); }
else if (line.startsWith('### ')) { blockType = 5; propName = 'heading3'; cleanText = sanitizeMarkdown(line.substring(4)); }
else if (line.startsWith('> ')) { blockType = 15; propName = 'quote'; cleanText = sanitizeMarkdown(line.substring(2)); }
else if (line.startsWith('- ') || line.startsWith('* ')) { blockType = 12; propName = 'bullet'; cleanText = sanitizeMarkdown(line.substring(2)); }
else if (/^\d+\. /.test(line)) { blockType = 13; propName = 'ordered'; cleanText = sanitizeMarkdown(line.replace(/^\d+\. /, '')); }
if (!cleanText.trim()) continue;
blocks.push({
block_type: blockType,
[propName]: { elements: [{ text_run: { content: cleanText, text_element_style: {} } }] }
});
}
const validBlocks = validateBlocks(blocks);
const blocksAdded = await batchInsertBlocks(targetToken, validBlocks);
return { success: true, message: 'Document overwritten', blocks_added: blocksAdded };
}
async function appendDoc(docToken, content) {
let targetToken = docToken;
try {
const resolved = await resolveToken(docToken);
if (typeof resolved === 'string') targetToken = resolved;
else if (resolved.token) targetToken = resolved.token;
} catch (e) {}
// Use the same robust parsing and batching logic as writeDoc
const blocks = [];
const lines = content.split('\n');
let inCodeBlock = false;
let codeContent = [];
for (const line of lines) {
if (line.trim().startsWith('```')) {
if (inCodeBlock) {
inCodeBlock = false;
const codeText = sanitizeMarkdown(codeContent.join('\n'));
blocks.push({
block_type: 14,
code: { elements: [{ text_run: { content: codeText, text_element_style: {} } }], language: 1 }
});
codeContent = [];
} else {
inCodeBlock = true;
}
continue;
}
if (inCodeBlock) {
codeContent.push(line);
continue;
}
if (!line.trim()) continue;
let blockType = 2;
let propName = 'text';
let cleanText = sanitizeMarkdown(line);
if (line.startsWith('# ')) { blockType = 3; propName = 'heading1'; cleanText = sanitizeMarkdown(line.substring(2)); }
else if (line.startsWith('## ')) { blockType = 4; propName = 'heading2'; cleanText = sanitizeMarkdown(line.substring(3)); }
else if (line.startsWith('### ')) { blockType = 5; propName = 'heading3'; cleanText = sanitizeMarkdown(line.substring(4)); }
else if (line.startsWith('> ')) { blockType = 15; propName = 'quote'; cleanText = sanitizeMarkdown(line.substring(2)); }
else if (line.startsWith('- ') || line.startsWith('* ')) { blockType = 12; propName = 'bullet'; cleanText = sanitizeMarkdown(line.substring(2)); }
else if (/^\d+\. /.test(line)) { blockType = 13; propName = 'ordered'; cleanText = sanitizeMarkdown(line.replace(/^\d+\. /, '')); }
if (!cleanText.trim()) continue;
blocks.push({
block_type: blockType,
[propName]: { elements: [{ text_run: { content: cleanText, text_element_style: {} } }] }
});
}
const validBlocks = validateBlocks(blocks);
const blocksAdded = await batchInsertBlocks(targetToken, validBlocks);
return { success: true, message: 'Document appended', blocks_added: blocksAdded };
}
// CLI Wrapper
if (require.main === module) {
const { program } = require('commander');
program
.option('--action <action>', 'Action: read, write, create, append')
.option('--token <token>', 'Doc Token')
.option('--content <text>', 'Content')
.option('--title <text>', 'Title')
.parse(process.argv);
const opts = program.opts();
(async () => {
try {
const token = extractToken(opts.token);
if (opts.action === 'read') {
console.log(JSON.stringify(await readDoc(token), null, 2));
} else if (opts.action === 'resolve') {
console.log(JSON.stringify(await resolveDoc(token), null, 2));
} else if (opts.action === 'create') {
console.log(JSON.stringify(await createDoc(opts.title), null, 2));
} else if (opts.action === 'write') {
console.log(JSON.stringify(await writeDoc(token, opts.content), null, 2));
} else if (opts.action === 'append') {
console.log(JSON.stringify(await appendDoc(token, opts.content), null, 2));
} else {
console.error('Unknown action');
process.exit(1);
}
} catch (e) {
// Enhanced Error Reporting for JSON-expecting agents
const errorObj = {
code: 1,
error: e.message,
msg: e.message
};
if (e.message.includes('HTTP 400') || e.message.includes('400')) {
errorObj.tip = "Check if the token is valid (docx/...) and not a URL or wiki link without resolution.";
}
console.error(JSON.stringify(errorObj, null, 2));
process.exit(1);
}
})();
}
module.exports = { readDoc, createDoc, writeDoc, appendDoc, resolveDoc };

57
input_guard.js Normal file
View File

@@ -0,0 +1,57 @@
/**
* Feishu Doc Input Guard
* Innovated by GEP Cycle #1226 (Updated Cycle #1759)
*
* Prevents 400 errors by sanitizing markdown before API submission.
* Enforces:
* - No nested tables (Feishu limitation)
* - Valid block structure
* - Text length limits
*/
const sanitizeMarkdown = (text) => {
if (!text) return "";
// 1. Remove null bytes and control characters (except newlines/tabs)
// Expanded range to include more control characters if needed, but keeping basic set for now.
// Added \r removal to normalize newlines.
// Preserving \t (0x09) and \n (0x0A)
let safeText = text.replace(/[\x00-\x08\x0B-\x1F\x7F\r]/g, "");
// 2. Feishu doesn't support nested blockquotes well in some contexts, flatten deeper levels
// (Simple heuristic: reduce >>> to >)
safeText = safeText.replace(/^>{2,}/gm, ">");
return safeText;
};
const validateBlocks = (blocks) => {
return blocks.filter(block => {
// Text blocks must have content
if (block.block_type === 2) {
const content = block.text?.elements?.[0]?.text_run?.content;
return content && content.trim().length > 0;
}
// Headings/Bullets/Quotes must have content
const typeMap = { 3: 'heading1', 4: 'heading2', 5: 'heading3', 12: 'bullet', 13: 'ordered', 15: 'quote' };
if (block.block_type in typeMap) {
const prop = typeMap[block.block_type];
const content = block[prop]?.elements?.[0]?.text_run?.content;
return content && content.trim().length > 0;
}
// Code blocks are generally safe even if empty, but better to prevent empty text_run issues
if (block.block_type === 14) {
const content = block.code?.elements?.[0]?.text_run?.content;
// Allow empty code blocks but ensure text_run structure exists
// Feishu might reject empty content in text_run, so let's enforce at least a space or filter it.
// Filtering empty code blocks is safer for append operations.
return content && content.length > 0;
}
return true;
});
};
module.exports = {
sanitizeMarkdown,
validateBlocks
};

52
inspect_meta.js Normal file
View File

@@ -0,0 +1,52 @@
const { getTenantAccessToken } = require('./lib/auth');
async function inspect(appToken, label) {
const token = await getTenantAccessToken();
console.log(`\n=== Inspecting ${label} (${appToken}) ===`);
// 1. Get Tables
const tablesRes = await fetch(`https://open.feishu.cn/open-apis/bitable/v1/apps/${appToken}/tables`, {
headers: { Authorization: `Bearer ${token}` }
});
const tablesData = await tablesRes.json();
if (tablesData.code !== 0) {
console.error("Error getting tables:", tablesData.msg);
return;
}
for (const table of tablesData.data.items) {
console.log(`Table: ${table.name} (ID: ${table.table_id})`);
// 2. Get Fields
const fieldsRes = await fetch(`https://open.feishu.cn/open-apis/bitable/v1/apps/${appToken}/tables/${table.table_id}/fields`, {
headers: { Authorization: `Bearer ${token}` }
});
const fieldsData = await fieldsRes.json();
if (fieldsData.code !== 0) {
console.error("Error getting fields:", fieldsData.msg);
continue;
}
// Filter for relevant fields to reduce noise
const interestingFields = ['需求', '需求详述', '优先级', '模块', '备注', '文本'];
fieldsData.data.items.forEach(f => {
// Log interesting fields OR Select fields (Type 3) to see options
if (interestingFields.includes(f.field_name) || f.type === 3) {
console.log(` - Field: ${f.field_name} (ID: ${f.field_id}, Type: ${f.type})`);
if (f.property && f.property.options) {
console.log(` Options: ${f.property.options.map(o => o.name).join(', ')}`);
}
}
});
}
}
(async () => {
// Template (Iter 10)
await inspect('X8QPbUQdValKN7sFIwfcsy8fnEh', 'Template (Iter 10)');
// Target (Iter 11)
await inspect('LvlAbvfzMaxUP8sGOEWcLrX7nHb', 'Target (Iter 11)');
})();

140
lib/auth.js Normal file
View File

@@ -0,0 +1,140 @@
const fs = require('fs');
const path = require('path');
// Robust .env loading
const possibleEnvPaths = [
path.resolve(process.cwd(), '.env'),
path.resolve(__dirname, '../../../.env'),
path.resolve(__dirname, '../../../../.env')
];
let envLoaded = false;
for (const envPath of possibleEnvPaths) {
if (fs.existsSync(envPath)) {
try {
require('dotenv').config({ path: envPath });
envLoaded = true;
break;
} catch (e) {
// Ignore load error
}
}
}
let tokenCache = {
token: null,
expireTime: 0
};
function loadConfig() {
const configPath = path.join(__dirname, '../config.json');
let config = {};
if (fs.existsSync(configPath)) {
try {
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
} catch (e) {
console.error("Failed to parse config.json");
}
}
return {
app_id: process.env.FEISHU_APP_ID || config.app_id,
app_secret: process.env.FEISHU_APP_SECRET || config.app_secret
};
}
// Unified Token Cache (Shared with feishu-card and feishu-sticker)
const TOKEN_CACHE_FILE = path.resolve(__dirname, '../../../memory/feishu_token.json');
async function getTenantAccessToken(forceRefresh = false) {
const now = Math.floor(Date.now() / 1000);
// Try to load from disk first
if (!forceRefresh && !tokenCache.token && fs.existsSync(TOKEN_CACHE_FILE)) {
try {
const saved = JSON.parse(fs.readFileSync(TOKEN_CACHE_FILE, 'utf8'));
// Handle both 'expire' (standard) and 'expireTime' (legacy)
const expiry = saved.expire || saved.expireTime;
if (saved.token && expiry > now) {
tokenCache.token = saved.token;
tokenCache.expireTime = expiry; // Keep internal consistency
}
} catch (e) {
// Ignore corrupted cache
}
}
// Force Refresh: Delete memory cache and file cache
if (forceRefresh) {
tokenCache.token = null;
tokenCache.expireTime = 0;
try { if (fs.existsSync(TOKEN_CACHE_FILE)) fs.unlinkSync(TOKEN_CACHE_FILE); } catch(e) {}
}
if (tokenCache.token && tokenCache.expireTime > now) {
return tokenCache.token;
}
const config = loadConfig();
if (!config.app_id || !config.app_secret) {
throw new Error("Missing app_id or app_secret. Please set FEISHU_APP_ID and FEISHU_APP_SECRET environment variables or create a config.json file.");
}
let lastError;
for (let attempt = 1; attempt <= 3; attempt++) {
try {
const response = await fetch('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
"app_id": config.app_id,
"app_secret": config.app_secret
}),
timeout: 5000 // 5s timeout
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.code !== 0) {
throw new Error(`Failed to get tenant_access_token: ${data.msg}`);
}
tokenCache.token = data.tenant_access_token;
tokenCache.expireTime = now + data.expire - 60; // Refresh 1 minute early
// Persist to disk (Unified Format)
try {
const cacheDir = path.dirname(TOKEN_CACHE_FILE);
if (!fs.existsSync(cacheDir)) {
fs.mkdirSync(cacheDir, { recursive: true });
}
// Save using 'expire' to match other skills
fs.writeFileSync(TOKEN_CACHE_FILE, JSON.stringify({
token: tokenCache.token,
expire: tokenCache.expireTime
}, null, 2));
} catch (e) {
console.error("Failed to save token cache:", e.message);
}
return tokenCache.token;
} catch (error) {
lastError = error;
if (attempt < 3) {
const delay = 1000 * Math.pow(2, attempt - 1);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw lastError || new Error("Failed to retrieve access token after retries");
}
module.exports = {
getTenantAccessToken
};

74
lib/bitable.js Normal file
View File

@@ -0,0 +1,74 @@
async function fetchBitableContent(token, accessToken) {
// 1. List tables
const tablesUrl = `https://open.feishu.cn/open-apis/bitable/v1/apps/${token}/tables`;
const tablesRes = await fetch(tablesUrl, {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
const tablesData = await tablesRes.json();
if (tablesData.code !== 0) {
return { title: "Bitable", content: `Error fetching bitable tables: ${tablesData.msg}` };
}
const tables = tablesData.data.items;
if (!tables || tables.length === 0) {
return { title: "Bitable", content: "Empty Bitable." };
}
let fullContent = [];
// 2. Fetch records
// Prioritize Ignacia's table (tblJgZHOmPybgX60) if present
const targetTableId = "tblJgZHOmPybgX60";
const targetTable = tables.find(t => t.table_id === targetTableId);
// If target found, only fetch it. Otherwise fetch first 3 to be safe/fast.
const tablesToFetch = targetTable ? [targetTable] : tables.slice(0, 3);
for (const table of tablesToFetch) {
const tableId = table.table_id;
const tableName = table.name;
// List records
const recordsUrl = `https://open.feishu.cn/open-apis/bitable/v1/apps/${token}/tables/${tableId}/records?page_size=20`;
const recRes = await fetch(recordsUrl, {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
const recData = await recRes.json();
fullContent.push(`## Table: ${tableName}`);
if (recData.code === 0 && recData.data && recData.data.items) {
const records = recData.data.items;
// Convert records (objects with fields) to table
// We need to know all possible fields to make a header
const allFields = new Set();
records.forEach(r => Object.keys(r.fields).forEach(k => allFields.add(k)));
const headers = Array.from(allFields);
let md = "| " + headers.join(" | ") + " |\n";
md += "| " + headers.map(() => "---").join(" | ") + " |\n";
for (const rec of records) {
md += "| " + headers.map(h => {
const val = rec.fields[h];
if (typeof val === 'object') return JSON.stringify(val);
return val || "";
}).join(" | ") + " |\n";
}
fullContent.push(md);
} else {
fullContent.push(`(Could not fetch records: ${recData.msg})`);
}
}
return {
title: "Feishu Bitable",
content: fullContent.join("\n\n")
};
}
module.exports = {
fetchBitableContent
};

192
lib/docx.js Normal file
View File

@@ -0,0 +1,192 @@
async function fetchDocxContent(documentId, accessToken) {
// 1. Get document info for title
const infoUrl = `https://open.feishu.cn/open-apis/docx/v1/documents/${documentId}`;
const infoRes = await fetch(infoUrl, {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
const infoData = await infoRes.json();
let title = "Untitled Docx";
if (infoData.code === 0 && infoData.data && infoData.data.document) {
title = infoData.data.document.title;
}
// 2. Fetch all blocks
// List blocks API: GET https://open.feishu.cn/open-apis/docx/v1/documents/{document_id}/blocks
// Use pagination if necessary, fetching all for now (basic implementation)
let blocks = [];
let pageToken = '';
let hasMore = true;
while (hasMore) {
const url = `https://open.feishu.cn/open-apis/docx/v1/documents/${documentId}/blocks?page_size=500${pageToken ? `&page_token=${pageToken}` : ''}`;
const response = await fetch(url, {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
const data = await response.json();
if (data.code !== 0) {
throw new Error(`Failed to fetch docx blocks: ${data.msg}`);
}
if (data.data && data.data.items) {
blocks = blocks.concat(data.data.items);
}
hasMore = data.data.has_more;
pageToken = data.data.page_token;
}
const markdown = convertBlocksToMarkdown(blocks);
return { title, content: markdown };
}
function convertBlocksToMarkdown(blocks) {
if (!blocks || blocks.length === 0) return "";
let md = [];
for (const block of blocks) {
const type = block.block_type;
switch (type) {
case 1: // page
break;
case 2: // text (paragraph)
md.push(parseText(block.text));
break;
case 3: // heading1
md.push(`# ${parseText(block.heading1)}`);
break;
case 4: // heading2
md.push(`## ${parseText(block.heading2)}`);
break;
case 5: // heading3
md.push(`### ${parseText(block.heading3)}`);
break;
case 6: // heading4
md.push(`#### ${parseText(block.heading4)}`);
break;
case 7: // heading5
md.push(`##### ${parseText(block.heading5)}`);
break;
case 8: // heading6
md.push(`###### ${parseText(block.heading6)}`);
break;
case 9: // heading7
md.push(`####### ${parseText(block.heading7)}`);
break;
case 10: // heading8
md.push(`######## ${parseText(block.heading8)}`);
break;
case 11: // heading9
md.push(`######### ${parseText(block.heading9)}`);
break;
case 12: // bullet
md.push(`- ${parseText(block.bullet)}`);
break;
case 13: // ordered
md.push(`1. ${parseText(block.ordered)}`);
break;
case 14: // code
md.push('```' + (block.code?.style?.language === 1 ? '' : '') + '\n' + parseText(block.code) + '\n```');
break;
case 15: // quote
md.push(`> ${parseText(block.quote)}`);
break;
case 27: // image
md.push(`![Image](token:${block.image?.token})`);
break;
default:
// Ignore unknown blocks for now
console.error(`Skipped block type: ${type}`, JSON.stringify(block).substring(0, 200));
md.push(`[UNSUPPORTED BLOCK TYPE: ${type}]`);
break;
}
}
return md.join('\n\n');
}
async function appendDocxContent(documentId, content, accessToken) {
// 1. Convert markdown content to Feishu blocks
const blocks = convertMarkdownToBlocks(content);
// 2. Append to the end of the document (root block)
// POST https://open.feishu.cn/open-apis/docx/v1/documents/{document_id}/blocks/{block_id}/children
// Use documentId as block_id to append to root
const url = `https://open.feishu.cn/open-apis/docx/v1/documents/${documentId}/blocks/${documentId}/children`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json; charset=utf-8'
},
body: JSON.stringify({
children: blocks,
index: -1 // Append to end
})
});
const data = await response.json();
if (data.code !== 0) {
throw new Error(`Failed to append to docx: ${data.msg}`);
}
return { success: true, appended_blocks: data.data.children };
}
function convertMarkdownToBlocks(markdown) {
// Simple parser: split by newlines, treat # as headers, others as text
// For robustness, this should be a real parser. Here we implement a basic one.
const lines = markdown.split('\n');
const blocks = [];
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) continue;
if (trimmed.startsWith('# ')) {
blocks.push({ block_type: 3, heading1: { elements: [{ text_run: { content: trimmed.substring(2) } }] } });
} else if (trimmed.startsWith('## ')) {
blocks.push({ block_type: 4, heading2: { elements: [{ text_run: { content: trimmed.substring(3) } }] } });
} else if (trimmed.startsWith('### ')) {
blocks.push({ block_type: 5, heading3: { elements: [{ text_run: { content: trimmed.substring(4) } }] } });
} else if (trimmed.startsWith('- ')) {
blocks.push({ block_type: 12, bullet: { elements: [{ text_run: { content: trimmed.substring(2) } }] } });
} else {
// Default to text (paragraph)
blocks.push({ block_type: 2, text: { elements: [{ text_run: { content: line } }] } });
}
}
return blocks;
}
function parseText(blockData) {
if (!blockData || !blockData.elements) return "";
return blockData.elements.map(el => {
if (el.text_run) {
let text = el.text_run.content;
const style = el.text_run.text_element_style;
if (style) {
if (style.bold) text = `**${text}**`;
if (style.italic) text = `*${text}*`;
if (style.strikethrough) text = `~~${text}~~`;
if (style.inline_code) text = `\`${text}\``;
if (style.link) text = `[${text}](${style.link.url})`;
}
return text;
}
if (el.mention_doc) {
return `[Doc: ${el.mention_doc.token}]`;
}
return "";
}).join("");
}
module.exports = {
fetchDocxContent,
appendDocxContent
};

130
lib/sheet.js Normal file
View File

@@ -0,0 +1,130 @@
async function fetchSheetContent(token, accessToken) {
// 1. Get metainfo to find sheetIds
const metaUrl = `https://open.feishu.cn/open-apis/sheets/v3/spreadsheets/${token}/sheets/query`;
const metaRes = await fetch(metaUrl, {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
const metaData = await metaRes.json();
if (metaData.code !== 0) {
// Fallback or error
return { title: "Sheet", content: `Error fetching sheet meta: ${metaData.msg}` };
}
const sheets = metaData.data.sheets;
if (!sheets || sheets.length === 0) {
return { title: "Sheet", content: "Empty spreadsheet." };
}
let fullContent = [];
// Sort sheets by index just in case
sheets.sort((a, b) => a.index - b.index);
// 2. Fetch content for up to 3 sheets to balance context vs info
// Skip hidden sheets
const visibleSheets = sheets.filter(s => !s.hidden).slice(0, 3);
for (const sheet of visibleSheets) {
const sheetId = sheet.sheet_id;
const title = sheet.title;
// Determine Range based on grid properties
// Default safe limits: Max 20 columns (T), Max 100 rows
// This prevents massive JSON payloads
let maxRows = 100;
let maxCols = 20;
if (sheet.grid_properties) {
maxRows = Math.min(sheet.grid_properties.row_count, 100);
maxCols = Math.min(sheet.grid_properties.column_count, 20);
}
// Avoid fetching empty grids (though unlikely for valid sheets)
if (maxRows === 0 || maxCols === 0) {
fullContent.push(`## Sheet: ${title} (Empty)`);
continue;
}
const lastColName = indexToColName(maxCols); // 1-based index to A, B, ... T
const range = `${sheetId}!A1:${lastColName}${maxRows}`;
const valUrl = `https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/${token}/values/${range}`;
const valRes = await fetch(valUrl, {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
const valData = await valRes.json();
fullContent.push(`## Sheet: ${title}`);
if (valData.code === 0 && valData.data && valData.data.valueRange) {
const rows = valData.data.valueRange.values;
fullContent.push(markdownTable(rows));
if (sheet.grid_properties && sheet.grid_properties.row_count > maxRows) {
fullContent.push(`*(Truncated: showing first ${maxRows} of ${sheet.grid_properties.row_count} rows)*`);
}
} else {
fullContent.push(`(Could not fetch values: ${valData.msg})`);
}
}
return {
title: "Feishu Sheet",
content: fullContent.join("\n\n")
};
}
function indexToColName(num) {
let ret = '';
while (num > 0) {
num--;
ret = String.fromCharCode(65 + (num % 26)) + ret;
num = Math.floor(num / 26);
}
return ret || 'A';
}
function markdownTable(rows) {
if (!rows || rows.length === 0) return "";
// Normalize row length
const maxLength = Math.max(...rows.map(r => r ? r.length : 0));
if (maxLength === 0) return "(Empty Table)";
// Ensure all rows are arrays and have strings
const cleanRows = rows.map(row => {
if (!Array.isArray(row)) return Array(maxLength).fill("");
return row.map(cell => {
if (cell === null || cell === undefined) return "";
if (typeof cell === 'object') return JSON.stringify(cell); // Handle rich text segments roughly
return String(cell).replace(/\n/g, "<br>"); // Keep single line
});
});
const header = cleanRows[0];
const body = cleanRows.slice(1);
// Handle case where header might be shorter than max length
const paddedHeader = [...header];
while(paddedHeader.length < maxLength) paddedHeader.push("");
let md = "| " + paddedHeader.join(" | ") + " |\n";
md += "| " + paddedHeader.map(() => "---").join(" | ") + " |\n";
for (const row of body) {
// Pad row if needed
const padded = [...row];
while(padded.length < maxLength) padded.push("");
md += "| " + padded.join(" | ") + " |\n";
}
return md;
}
module.exports = {
fetchSheetContent
};

34
lib/wiki.js Normal file
View File

@@ -0,0 +1,34 @@
const { getTenantAccessToken } = require('./auth');
async function resolveWiki(token, accessToken) {
// Try to resolve via get_node API first to get obj_token and obj_type
// API: GET https://open.feishu.cn/open-apis/wiki/v2/spaces/get_node?token={token}
const url = `https://open.feishu.cn/open-apis/wiki/v2/spaces/get_node?token=${token}`;
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
const data = await response.json();
if (data.code === 0 && data.data && data.data.node) {
return {
obj_token: data.data.node.obj_token,
obj_type: data.data.node.obj_type, // 'docx', 'doc', 'sheet', 'bitable'
title: data.data.node.title
};
}
// Handle specific errors if needed (e.g., node not found)
if (data.code !== 0) {
throw new Error(`Wiki resolution failed: ${data.msg} (Code: ${data.code})`);
}
return null;
}
module.exports = {
resolveWiki
};

572
package-lock.json generated Normal file
View File

@@ -0,0 +1,572 @@
{
"name": "feishu-doc",
"version": "1.2.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "feishu-doc",
"version": "1.2.6",
"dependencies": {
"@larksuiteoapi/node-sdk": "^1.58.0"
}
},
"node_modules/@larksuiteoapi/node-sdk": {
"version": "1.58.0",
"resolved": "https://registry.npmjs.org/@larksuiteoapi/node-sdk/-/node-sdk-1.58.0.tgz",
"integrity": "sha512-NcQNHdGuHOxOWY3bRGS9WldwpbR6+k7Fi0H1IJXDNNmbSrEB/8rLwqHRC8tAbbj/Mp8TWH/v1O+p487m6xskxw==",
"license": "MIT",
"dependencies": {
"axios": "~1.13.3",
"lodash.identity": "^3.0.0",
"lodash.merge": "^4.6.2",
"lodash.pickby": "^4.6.0",
"protobufjs": "^7.2.6",
"qs": "^6.13.0",
"ws": "^8.16.0"
}
},
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/codegen": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/eventemitter": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/fetch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.1",
"@protobufjs/inquire": "^1.1.0"
}
},
"node_modules/@protobufjs/float": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/inquire": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/path": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/pool": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/utf8": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
"license": "BSD-3-Clause"
},
"node_modules/@types/node": {
"version": "25.2.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.0.tgz",
"integrity": "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.16.0"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/axios": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz",
"integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/lodash.identity": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/lodash.identity/-/lodash.identity-3.0.0.tgz",
"integrity": "sha512-AupTIzdLQxJS5wIYUQlgGyk2XRTfGXA+MCghDHqZk0pzUNYvd3EESS6dkChNauNYVIutcb0dfHw1ri9Q1yPV8Q==",
"license": "MIT"
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"license": "MIT"
},
"node_modules/lodash.pickby": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz",
"integrity": "sha512-AZV+GsS/6ckvPOVQPXSiFFacKvKB4kOQu6ynt9wz0F3LO4R9Ij4K1ddYsIytDpSgLz88JHd9P+oaLeej5/Sl7Q==",
"license": "MIT"
},
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
"license": "Apache-2.0"
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/protobufjs": {
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
"integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
"@protobufjs/codegen": "^2.0.4",
"@protobufjs/eventemitter": "^1.1.0",
"@protobufjs/fetch": "^1.1.0",
"@protobufjs/float": "^1.0.2",
"@protobufjs/inquire": "^1.1.0",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.0",
"@types/node": ">=13.7.0",
"long": "^5.0.0"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/qs": {
"version": "6.14.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3",
"side-channel-list": "^1.0.0",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-list": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-map": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-weakmap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3",
"side-channel-map": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"license": "MIT"
},
"node_modules/ws": {
"version": "8.19.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}

12
package.json Normal file
View File

@@ -0,0 +1,12 @@
{
"name": "feishu-doc",
"version": "1.2.6",
"description": "Fetch content from Feishu Wiki/Doc/Sheet/Bitable",
"main": "index.js",
"scripts": {
"test": "echo \"No tests specified\" && exit 0"
},
"dependencies": {
"@larksuiteoapi/node-sdk": "^1.58.0"
}
}

129
setup_iter11.js Normal file
View File

@@ -0,0 +1,129 @@
const { getTenantAccessToken } = require('./lib/auth');
const APP_TOKEN = 'LvlAbvfzMaxUP8sGOEWcLrX7nHb';
const TABLE_ID = 'tblLy7koY2VGXGmR'; // From inspect_meta.js
async function setup() {
const token = await getTenantAccessToken();
console.log(`Setting up Iter 11 (App: ${APP_TOKEN}, Table: ${TABLE_ID})`);
// 1. Create Fields
// Field: 需求 (Text - Type 1)
await createField(token, '需求', 1);
// Field: 需求详述 (Text - Type 1)
await createField(token, '需求详述', 1);
// Field: 优先级 (Single Select - Type 3)
const options = [
{ name: '上帝级重要', color: 0 }, // Red
{ name: '很重要', color: 1 }, // Orange
{ name: '重要', color: 2 }, // Yellow
{ name: '欠重要', color: 3 }, // Green
{ name: '待定', color: 4 } // Blue
];
await createField(token, '优先级', 3, { options: options });
// 2. Insert Records
const records = [
{
fields: {
'需求': '获取行为和生活职业的结合',
'需求详述': 'a. 当前获取行为不受生活职业的限制\nb. 炼药、烹饪行为因为和获取高度相关,还未完成开发\nc. 无法获取的道具走总控给其他NPC制作功能没做',
'优先级': '上帝级重要'
}
},
{
fields: {
'需求': 'NPC信息面板',
'需求详述': '',
'优先级': '很重要'
}
},
{
fields: {
'需求': '心情系统',
'需求详述': 'a. 完成了单独心情值的开发,心情值的变化和行为的结合没有处理',
'优先级': '很重要'
}
},
{
fields: {
'需求': '房间系统',
'需求详述': 'a. 完成了item舒适度的计算房间对NPC的影响和关系没有处理',
'优先级': '重要'
}
},
{
fields: {
'需求': '营地管理页面',
'需求详述': 'a. 角色列表页\nb. 物品需求页',
'优先级': '重要'
}
},
{
fields: {
'需求': '路径COST计算规则',
'需求详述': '',
'优先级': '欠重要'
}
},
{
fields: {
'需求': '营地功能旗帜的交互',
'需求详述': '',
'优先级': '欠重要'
}
}
];
console.log(`Inserting ${records.length} records...`);
const batchUrl = `https://open.feishu.cn/open-apis/bitable/v1/apps/${APP_TOKEN}/tables/${TABLE_ID}/records/batch_create`;
const res = await fetch(batchUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ records: records })
});
const data = await res.json();
if (data.code !== 0) {
console.error('Failed to create records:', JSON.stringify(data, null, 2));
} else {
console.log('Success! Created records.');
}
}
async function createField(token, name, type, property) {
console.log(`Creating field: ${name}`);
const url = `https://open.feishu.cn/open-apis/bitable/v1/apps/${APP_TOKEN}/tables/${TABLE_ID}/fields`;
const payload = {
field_name: name,
type: type
};
if (property) payload.property = property;
const res = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
const data = await res.json();
if (data.code === 0) {
console.log(` -> Created field ID: ${data.data.field.field_id}`);
return data.data.field.field_id;
} else {
console.warn(` -> Failed to create field (might exist): ${data.msg}`);
return null;
}
}
setup().catch(console.error);

12
validate_patch.js Normal file
View File

@@ -0,0 +1,12 @@
const { FeishuClient } = require('./feishu-client'); // Assuming standard client
// Mock client or use real if env vars set (skipping real call to avoid side effects in validation)
// We just want to ensure the syntax of index.js is valid after edit.
try {
const index = require('./index.js');
console.log('skills/feishu-doc/index.js loaded successfully.');
} catch (e) {
console.error('Failed to load index.js:', e);
process.exit(1);
}