@lixiongwei/n8n-nodes-feishu 1.0.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -33
- package/credentials/FeishuApi.credentials.js +22 -4
- package/nodes/Feishu/Feishu.node.js +604 -171
- package/nodes/Feishu/FeishuTrigger.node.js +62 -38
- package/nodes/Feishu/errors.d.ts +4 -0
- package/nodes/Feishu/errors.js +42 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,66 +1,138 @@
|
|
|
1
1
|
# @lixiongwei/n8n-nodes-feishu
|
|
2
2
|
|
|
3
|
-
> Feishu / Lark integration nodes for n8n
|
|
3
|
+
> **Feishu / Lark** integration nodes for n8n — messaging, Bitable CRUD, approvals, calendar & contacts.
|
|
4
|
+
> Free tier + Pro License. Includes workflow templates.
|
|
4
5
|
|
|
5
6
|
---
|
|
6
7
|
|
|
7
|
-
## Install
|
|
8
|
+
## 📦 Install
|
|
8
9
|
|
|
9
10
|
```bash
|
|
10
11
|
npm install @lixiongwei/n8n-nodes-feishu
|
|
11
12
|
```
|
|
12
13
|
|
|
14
|
+
Requires: n8n self-hosted. (n8n Cloud does not support community nodes.)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 🎮 Capabilities
|
|
19
|
+
|
|
20
|
+
### 🆓 Free
|
|
21
|
+
|
|
22
|
+
| Category | Operation | Description |
|
|
23
|
+
|----------|-----------|-------------|
|
|
24
|
+
| **Messaging** | Send Text Message | Send plain text to users or groups |
|
|
25
|
+
| **Trigger** | Message Received | Webhook — fires when the bot receives a message |
|
|
26
|
+
|
|
27
|
+
### ⭐ Pro — $49 one-time ([get a key](https://1717465779306.gumroad.com/l/feishu-pro))
|
|
28
|
+
|
|
29
|
+
| Category | Operations |
|
|
30
|
+
|----------|------------|
|
|
31
|
+
| **Messaging** | Send Card Message (with Markdown & color), Send Image, Send File, Batch Send to multiple recipients |
|
|
32
|
+
| **Bitable** | Read Records, Create Record, Update Record, Delete Record, Search Records, List Tables |
|
|
33
|
+
| **Approvals** | List Pending, Get Detail, Approve, Reject |
|
|
34
|
+
| **Calendar** | List Events, Create Event |
|
|
35
|
+
| **Contacts** | Get User Info (by Open ID / Email / Mobile), Search Users |
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 🚀 Quick Start (3 steps)
|
|
40
|
+
|
|
41
|
+
### 1. Create a Feishu/Lark app
|
|
42
|
+
|
|
43
|
+
1. Go to [Feishu Dev Console](https://open.feishu.cn/) (or [Lark Dev Console](https://open.larksuite.com/))
|
|
44
|
+
2. Create an **Enterprise Internal App**
|
|
45
|
+
3. Enable **Bot** capability (App Capabilities → Bot → Enable)
|
|
46
|
+
4. Go to **Permissions** → add at minimum:
|
|
47
|
+
- `im:message:send_as_bot` (send messages)
|
|
48
|
+
- `contact:contact.base:readonly` (for Contacts operations)
|
|
49
|
+
5. Go to **Version Management** → Create version → **Publish**
|
|
50
|
+
6. Copy your **App ID** and **App Secret** from Credentials & Basic Info
|
|
51
|
+
|
|
52
|
+
### 2. Configure n8n
|
|
53
|
+
|
|
54
|
+
1. n8n → **Credentials** → New → search `Feishu` → select **Feishu / Lark API**
|
|
55
|
+
2. Choose your platform (Feishu for China, Lark for International)
|
|
56
|
+
3. Enter App ID + App Secret
|
|
57
|
+
4. (Optional) Enter License Key for Pro features
|
|
58
|
+
|
|
59
|
+
### 3. Start building
|
|
60
|
+
|
|
61
|
+
Drag the **Feishu / Lark** node onto your canvas → select a Resource → select an Operation → configure → test.
|
|
62
|
+
|
|
13
63
|
---
|
|
14
64
|
|
|
15
|
-
##
|
|
65
|
+
## 📋 Included Workflow Templates
|
|
16
66
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
|
20
|
-
|
|
67
|
+
Import these from `node_modules/@lixiongwei/n8n-nodes-feishu/workflows/` or from the [GitHub repo](https://github.com/lixiongwei/n8n-nodes-feishu):
|
|
68
|
+
|
|
69
|
+
| # | File | What it does |
|
|
70
|
+
|---|------|-------------|
|
|
71
|
+
| 1 | `template-01-send-message.json` | External Webhook → Feishu text message (CI/CD alerts, form notifications) |
|
|
72
|
+
| 2 | `template-02-cron-reminder.json` | Scheduled → Feishu reminder (daily standup, deadline alerts) |
|
|
73
|
+
| 3 | `template-03-website-monitor.json` | Monitor website health → Feishu alert on downtime |
|
|
74
|
+
| 4 | `template-04-form-notify.json` | Form submission → Feishu card notification |
|
|
75
|
+
| 5 | `template-05-api-to-feishu.json` | Pull external API data → format → push to Feishu |
|
|
21
76
|
|
|
22
77
|
---
|
|
23
78
|
|
|
24
|
-
##
|
|
79
|
+
## 🔧 Common Patterns
|
|
25
80
|
|
|
26
|
-
|
|
|
27
|
-
|
|
28
|
-
|
|
|
29
|
-
|
|
|
30
|
-
|
|
|
31
|
-
|
|
|
81
|
+
| I want to... | Use this setup |
|
|
82
|
+
|-------------|---------------|
|
|
83
|
+
| Get alerts when my site goes down | Template 03 — Cron + HTTP Check + If + Feishu Message |
|
|
84
|
+
| Send a daily report to my team | Template 02 — Cron + Feishu Card Message |
|
|
85
|
+
| Track form submissions in Bitable | Webhook → Feishu Bitable Create Record → Feishu Card to notify team |
|
|
86
|
+
| Auto-approve simple requests | Feishu Trigger (Approval) → If (conditions) → Approve Instance |
|
|
87
|
+
| Search users for open_ids | Contacts → Search Users → use output in subsequent Message node |
|
|
88
|
+
| Build a bot that replies to messages | Feishu Trigger (Message) → AI/Logic → Feishu Send Text Message |
|
|
32
89
|
|
|
33
90
|
---
|
|
34
91
|
|
|
35
|
-
##
|
|
92
|
+
## ⚠️ Troubleshooting
|
|
36
93
|
|
|
37
|
-
|
|
94
|
+
| Error | Cause | Fix |
|
|
95
|
+
|-------|-------|-----|
|
|
96
|
+
| `Bot ability not activated (230006)` | Bot not enabled in app | App Capabilities → Enable Bot → Republish |
|
|
97
|
+
| `Permission denied (99991672)` | Missing API scope | Permissions → Add required scope → Republish |
|
|
98
|
+
| `Recipient ID not found (99992351)` | Wrong ID type or not in app scope | Use Contacts → Get User Info to find the correct ID |
|
|
99
|
+
| `Invalid tenant access token` | Wrong App ID or Secret | Double-check from Developer Console → Credentials |
|
|
100
|
+
| `This is a Pro feature` | No license key | Get one at the Gumroad link, or use a Free operation |
|
|
101
|
+
| Workflow stuck spinning | Method is GET instead of POST | Check the node configuration — content-sending operations need POST |
|
|
102
|
+
| Webhook URL not receiving events | App not configured for events | Add Request URL in Feishu Dev Console → Event Subscriptions |
|
|
38
103
|
|
|
39
104
|
---
|
|
40
105
|
|
|
41
|
-
##
|
|
106
|
+
## 🌍 Feishu vs Lark
|
|
107
|
+
|
|
108
|
+
Feishu (飞书) and Lark share the same API but use **different domains**:
|
|
42
109
|
|
|
43
|
-
|
|
110
|
+
| Platform | API Base | Used in |
|
|
111
|
+
|----------|----------|---------|
|
|
112
|
+
| Feishu | `open.feishu.cn` | China |
|
|
113
|
+
| Lark | `open.larksuite.com` | International (Singapore, US, etc.) |
|
|
44
114
|
|
|
45
|
-
|
|
46
|
-
|----------|----------|
|
|
47
|
-
| `template-01-send-message.json` | Webhook → Feishu message |
|
|
48
|
-
| `template-02-cron-reminder.json` | Scheduled → Feishu reminder |
|
|
49
|
-
| `template-03-website-monitor.json` | Website monitor → Feishu alert |
|
|
50
|
-
| `template-04-form-notify.json` | Form submission → Card notification |
|
|
51
|
-
| `template-05-api-to-feishu.json` | API data → Feishu push |
|
|
115
|
+
Choose the correct platform in your n8n credentials. If you get `99991663` errors, you likely have the wrong platform selected.
|
|
52
116
|
|
|
53
117
|
---
|
|
54
118
|
|
|
55
|
-
##
|
|
119
|
+
## 💰 License
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
npm install → FREE features work immediately
|
|
123
|
+
↓
|
|
124
|
+
Try Pro operation → prompted for License Key
|
|
125
|
+
↓
|
|
126
|
+
Get Key: https://1717465779306.gumroad.com/l/feishu-pro
|
|
127
|
+
↓
|
|
128
|
+
Enter Key in credentials → ALL features unlocked forever
|
|
129
|
+
```
|
|
56
130
|
|
|
57
|
-
|
|
58
|
-
2. Enable **Bot** capability
|
|
59
|
-
3. Add permissions: `im:message:send_as_bot`
|
|
60
|
-
4. Publish the app
|
|
61
|
-
5. In n8n credentials, fill in App ID + App Secret + License Key
|
|
131
|
+
One-time payment. No subscription. Lifetime access.
|
|
62
132
|
|
|
63
133
|
---
|
|
64
134
|
|
|
65
|
-
- n8n version
|
|
66
|
-
- Feishu API
|
|
135
|
+
- **n8n version**: 1.x+ (v2 recommended)
|
|
136
|
+
- **Feishu API**: v3
|
|
137
|
+
- **License**: MIT
|
|
138
|
+
- **Support**: Reply on [n8n Community Forum](https://community.n8n.io/c/community-nodes/11)
|
|
@@ -4,15 +4,34 @@ exports.FeishuApi = void 0;
|
|
|
4
4
|
class FeishuApi {
|
|
5
5
|
constructor() {
|
|
6
6
|
this.name = 'feishuApi';
|
|
7
|
-
this.displayName = 'Feishu
|
|
7
|
+
this.displayName = 'Feishu / Lark API';
|
|
8
8
|
this.documentationUrl = 'https://open.feishu.cn/document/home/getting-started';
|
|
9
9
|
this.properties = [
|
|
10
|
+
{
|
|
11
|
+
displayName: 'Platform',
|
|
12
|
+
name: 'platform',
|
|
13
|
+
type: 'options',
|
|
14
|
+
options: [
|
|
15
|
+
{
|
|
16
|
+
name: 'Feishu (飞书) — China',
|
|
17
|
+
value: 'feishu',
|
|
18
|
+
description: 'API domain: open.feishu.cn',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'Lark — International',
|
|
22
|
+
value: 'lark',
|
|
23
|
+
description: 'API domain: open.larksuite.com',
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
default: 'feishu',
|
|
27
|
+
description: 'Choose Feishu for China, Lark for international users',
|
|
28
|
+
},
|
|
10
29
|
{
|
|
11
30
|
displayName: 'App ID',
|
|
12
31
|
name: 'appId',
|
|
13
32
|
type: 'string',
|
|
14
33
|
default: '',
|
|
15
|
-
description: '
|
|
34
|
+
description: 'From Developer Console → Credentials & Basic Info',
|
|
16
35
|
required: true,
|
|
17
36
|
},
|
|
18
37
|
{
|
|
@@ -21,7 +40,6 @@ class FeishuApi {
|
|
|
21
40
|
type: 'string',
|
|
22
41
|
typeOptions: { password: true },
|
|
23
42
|
default: '',
|
|
24
|
-
description: 'Feishu App Secret',
|
|
25
43
|
required: true,
|
|
26
44
|
},
|
|
27
45
|
{
|
|
@@ -29,7 +47,7 @@ class FeishuApi {
|
|
|
29
47
|
name: 'licenseKey',
|
|
30
48
|
type: 'string',
|
|
31
49
|
default: '',
|
|
32
|
-
description: 'Pro License Key
|
|
50
|
+
description: 'Pro License Key → Unlock all features. Leave empty for free features. Get one: https://1717465779306.gumroad.com/l/feishu-pro',
|
|
33
51
|
required: false,
|
|
34
52
|
},
|
|
35
53
|
];
|
|
@@ -3,31 +3,43 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.Feishu = void 0;
|
|
4
4
|
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
5
|
const license_1 = require("./license");
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
const errors_1 = require("./errors");
|
|
7
|
+
// ─── Operation groups ───────────────────────────────────────────────
|
|
8
|
+
const FREE_OPS = ['sendTextMessage'];
|
|
9
|
+
const PRO_OPS = [
|
|
10
|
+
'sendCardMessage', 'sendImage', 'sendFile', 'batchSend',
|
|
11
|
+
'readBitable', 'createBitable', 'updateBitable', 'deleteBitable',
|
|
12
|
+
'searchBitable', 'listBitableTables',
|
|
13
|
+
'listApprovals', 'getApprovalDetail', 'approveInstance', 'rejectInstance',
|
|
14
|
+
'listCalendarEvents', 'createCalendarEvent',
|
|
15
|
+
'getUserInfo', 'searchUsers',
|
|
16
|
+
];
|
|
17
|
+
const API_HOST = {
|
|
18
|
+
feishu: 'https://open.feishu.cn',
|
|
19
|
+
lark: 'https://open.larksuite.com',
|
|
20
|
+
};
|
|
21
|
+
// ─── Token cache ─────────────────────────────────────────────────────
|
|
22
|
+
let cachedToken = '';
|
|
23
|
+
let cachedHost = null;
|
|
11
24
|
let tokenExpiry = 0;
|
|
12
|
-
async function
|
|
13
|
-
if (cachedToken && Date.now() < tokenExpiry)
|
|
25
|
+
async function getToken(ctx, appId, appSecret, baseUrl) {
|
|
26
|
+
if (cachedToken && cachedHost === baseUrl && Date.now() < tokenExpiry)
|
|
14
27
|
return cachedToken;
|
|
15
|
-
|
|
16
|
-
const response = await this.helpers.request({
|
|
28
|
+
const res = await ctx.helpers.request({
|
|
17
29
|
method: 'POST',
|
|
18
|
-
url:
|
|
30
|
+
url: `${baseUrl}/open-apis/auth/v3/tenant_access_token/internal`,
|
|
19
31
|
headers: { 'Content-Type': 'application/json' },
|
|
20
32
|
body: { app_id: appId, app_secret: appSecret },
|
|
21
33
|
json: true,
|
|
22
34
|
});
|
|
23
|
-
if (
|
|
24
|
-
throw new n8n_workflow_1.NodeOperationError(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
tokenExpiry = Date.now() + (
|
|
35
|
+
if (res.code !== 0)
|
|
36
|
+
throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), (0, errors_1.explainError)(res.code, res.msg));
|
|
37
|
+
cachedToken = res.tenant_access_token;
|
|
38
|
+
cachedHost = baseUrl;
|
|
39
|
+
tokenExpiry = Date.now() + (res.expire - 300) * 1000;
|
|
28
40
|
return cachedToken;
|
|
29
41
|
}
|
|
30
|
-
//
|
|
42
|
+
// ─── Node Description ────────────────────────────────────────────────
|
|
31
43
|
class Feishu {
|
|
32
44
|
constructor() {
|
|
33
45
|
this.description = {
|
|
@@ -35,31 +47,46 @@ class Feishu {
|
|
|
35
47
|
name: 'feishu',
|
|
36
48
|
icon: 'file:feishu.svg',
|
|
37
49
|
group: ['transform'],
|
|
38
|
-
version:
|
|
39
|
-
subtitle: '={{ $parameter["operation"] }}',
|
|
40
|
-
description: 'Feishu/Lark integration
|
|
50
|
+
version: 2,
|
|
51
|
+
subtitle: '={{ $parameter["resource"] }} - {{ $parameter["operation"] }}',
|
|
52
|
+
description: 'Feishu/Lark integration — messaging, Bitable CRUD, approvals, calendar, contacts',
|
|
41
53
|
defaults: { name: 'Feishu / Lark' },
|
|
42
|
-
inputs: [
|
|
43
|
-
outputs: [
|
|
54
|
+
inputs: ['main'],
|
|
55
|
+
outputs: ['main'],
|
|
44
56
|
credentials: [{ name: 'feishuApi', required: true }],
|
|
45
57
|
properties: [
|
|
46
|
-
//
|
|
58
|
+
// ── Resource ──
|
|
59
|
+
{
|
|
60
|
+
displayName: 'Resource',
|
|
61
|
+
name: 'resource',
|
|
62
|
+
type: 'options',
|
|
63
|
+
noDataExpression: true,
|
|
64
|
+
options: [
|
|
65
|
+
{ name: '📨 Messaging', value: 'messaging' },
|
|
66
|
+
{ name: '📊 Bitable', value: 'bitable' },
|
|
67
|
+
{ name: '✅ Approvals', value: 'approvals' },
|
|
68
|
+
{ name: '📅 Calendar', value: 'calendar' },
|
|
69
|
+
{ name: '👤 Contacts', value: 'contacts' },
|
|
70
|
+
],
|
|
71
|
+
default: 'messaging',
|
|
72
|
+
},
|
|
73
|
+
// ═══════════ MESSAGING ═══════════
|
|
47
74
|
{
|
|
48
75
|
displayName: 'Operation',
|
|
49
76
|
name: 'operation',
|
|
50
77
|
type: 'options',
|
|
51
78
|
noDataExpression: true,
|
|
79
|
+
displayOptions: { show: { resource: ['messaging'] } },
|
|
52
80
|
options: [
|
|
53
|
-
{ name: '🆓 Send Text Message
|
|
54
|
-
{ name: '⭐ Send Card Message
|
|
55
|
-
{ name: '⭐
|
|
56
|
-
{ name: '⭐
|
|
57
|
-
{ name: '⭐
|
|
81
|
+
{ name: '🆓 Send Text Message', value: 'sendTextMessage' },
|
|
82
|
+
{ name: '⭐ Send Card Message', value: 'sendCardMessage' },
|
|
83
|
+
{ name: '⭐ Send Image', value: 'sendImage' },
|
|
84
|
+
{ name: '⭐ Send File', value: 'sendFile' },
|
|
85
|
+
{ name: '⭐ Batch Send to Multiple Users', value: 'batchSend' },
|
|
58
86
|
],
|
|
59
87
|
default: 'sendTextMessage',
|
|
60
|
-
description: 'Pro features require a License Key',
|
|
61
88
|
},
|
|
62
|
-
//
|
|
89
|
+
// -- messaging shared --
|
|
63
90
|
{
|
|
64
91
|
displayName: 'Recipient ID Type',
|
|
65
92
|
name: 'receiveIdType',
|
|
@@ -68,10 +95,10 @@ class Feishu {
|
|
|
68
95
|
{ name: 'Open ID', value: 'open_id' },
|
|
69
96
|
{ name: 'User ID', value: 'user_id' },
|
|
70
97
|
{ name: 'Email', value: 'email' },
|
|
71
|
-
{ name: 'Chat ID', value: 'chat_id' },
|
|
98
|
+
{ name: 'Chat ID (Group)', value: 'chat_id' },
|
|
72
99
|
],
|
|
73
100
|
default: 'open_id',
|
|
74
|
-
displayOptions: { show: {
|
|
101
|
+
displayOptions: { show: { resource: ['messaging'] } },
|
|
75
102
|
},
|
|
76
103
|
{
|
|
77
104
|
displayName: 'Recipient',
|
|
@@ -79,10 +106,11 @@ class Feishu {
|
|
|
79
106
|
type: 'string',
|
|
80
107
|
default: '',
|
|
81
108
|
required: true,
|
|
82
|
-
displayOptions: { show: { operation: ['sendTextMessage', 'sendCardMessage'] } },
|
|
83
|
-
placeholder: 'ou_xxx or chat_xxx or
|
|
84
|
-
description: 'Recipient
|
|
109
|
+
displayOptions: { show: { resource: ['messaging'], operation: ['sendTextMessage', 'sendCardMessage', 'sendImage', 'sendFile'] } },
|
|
110
|
+
placeholder: 'ou_xxx or chat_xxx or user@example.com',
|
|
111
|
+
description: 'Recipient ID matching the type above. Use Contacts → Get User Info to find IDs.',
|
|
85
112
|
},
|
|
113
|
+
// -- text --
|
|
86
114
|
{
|
|
87
115
|
displayName: 'Message Content',
|
|
88
116
|
name: 'textContent',
|
|
@@ -90,36 +118,118 @@ class Feishu {
|
|
|
90
118
|
typeOptions: { rows: 4 },
|
|
91
119
|
default: '',
|
|
92
120
|
required: true,
|
|
93
|
-
displayOptions: { show: { operation: ['sendTextMessage'] } },
|
|
121
|
+
displayOptions: { show: { resource: ['messaging'], operation: ['sendTextMessage'] } },
|
|
94
122
|
placeholder: 'Type your message...',
|
|
95
123
|
},
|
|
96
|
-
//
|
|
124
|
+
// -- card --
|
|
97
125
|
{
|
|
98
126
|
displayName: 'Card Title',
|
|
99
127
|
name: 'cardTitle',
|
|
100
128
|
type: 'string',
|
|
101
129
|
default: '📢 Notification',
|
|
102
|
-
displayOptions: { show: { operation: ['sendCardMessage'] } },
|
|
130
|
+
displayOptions: { show: { resource: ['messaging'], operation: ['sendCardMessage'] } },
|
|
103
131
|
},
|
|
104
132
|
{
|
|
105
|
-
displayName: 'Card Body (Markdown
|
|
106
|
-
name: '
|
|
133
|
+
displayName: 'Card Body (Markdown)',
|
|
134
|
+
name: 'cardBody',
|
|
107
135
|
type: 'string',
|
|
108
136
|
typeOptions: { rows: 6 },
|
|
109
137
|
default: '',
|
|
110
138
|
required: true,
|
|
111
|
-
displayOptions: { show: { operation: ['sendCardMessage'] } },
|
|
112
|
-
placeholder: '**
|
|
139
|
+
displayOptions: { show: { resource: ['messaging'], operation: ['sendCardMessage'] } },
|
|
140
|
+
placeholder: '**Heading**\nBody text with _formatting_\n[Link](https://example.com)',
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
displayName: 'Card Color',
|
|
144
|
+
name: 'cardColor',
|
|
145
|
+
type: 'options',
|
|
146
|
+
options: [
|
|
147
|
+
{ name: 'Blue', value: 'blue' },
|
|
148
|
+
{ name: 'Green', value: 'green' },
|
|
149
|
+
{ name: 'Red', value: 'red' },
|
|
150
|
+
{ name: 'Yellow', value: 'yellow' },
|
|
151
|
+
{ name: 'Purple', value: 'purple' },
|
|
152
|
+
{ name: 'Default', value: 'default' },
|
|
153
|
+
],
|
|
154
|
+
default: 'default',
|
|
155
|
+
displayOptions: { show: { resource: ['messaging'], operation: ['sendCardMessage'] } },
|
|
156
|
+
},
|
|
157
|
+
// -- image --
|
|
158
|
+
{
|
|
159
|
+
displayName: 'Image URL',
|
|
160
|
+
name: 'imageUrl',
|
|
161
|
+
type: 'string',
|
|
162
|
+
default: '',
|
|
163
|
+
required: true,
|
|
164
|
+
displayOptions: { show: { resource: ['messaging'], operation: ['sendImage'] } },
|
|
165
|
+
placeholder: 'https://example.com/image.png',
|
|
166
|
+
description: 'Publicly accessible image URL (jpg, png, gif, webp)',
|
|
167
|
+
},
|
|
168
|
+
// -- file --
|
|
169
|
+
{
|
|
170
|
+
displayName: 'File URL',
|
|
171
|
+
name: 'fileUrl',
|
|
172
|
+
type: 'string',
|
|
173
|
+
default: '',
|
|
174
|
+
required: true,
|
|
175
|
+
displayOptions: { show: { resource: ['messaging'], operation: ['sendFile'] } },
|
|
176
|
+
placeholder: 'https://example.com/document.pdf',
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
displayName: 'File Name',
|
|
180
|
+
name: 'fileName',
|
|
181
|
+
type: 'string',
|
|
182
|
+
default: '',
|
|
183
|
+
required: true,
|
|
184
|
+
displayOptions: { show: { resource: ['messaging'], operation: ['sendFile'] } },
|
|
185
|
+
description: 'Display name of the file (including extension)',
|
|
186
|
+
},
|
|
187
|
+
// -- batch send --
|
|
188
|
+
{
|
|
189
|
+
displayName: 'Recipients (one per line)',
|
|
190
|
+
name: 'batchRecipients',
|
|
191
|
+
type: 'string',
|
|
192
|
+
typeOptions: { rows: 5 },
|
|
193
|
+
default: '',
|
|
194
|
+
required: true,
|
|
195
|
+
displayOptions: { show: { resource: ['messaging'], operation: ['batchSend'] } },
|
|
196
|
+
placeholder: 'open_id\nou_xxx\nuser@example.com',
|
|
197
|
+
description: 'List of recipient IDs, one per line. Each ID type is auto-detected.',
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
displayName: 'Batch Message',
|
|
201
|
+
name: 'batchMessage',
|
|
202
|
+
type: 'string',
|
|
203
|
+
typeOptions: { rows: 3 },
|
|
204
|
+
default: '',
|
|
205
|
+
required: true,
|
|
206
|
+
displayOptions: { show: { resource: ['messaging'], operation: ['batchSend'] } },
|
|
207
|
+
},
|
|
208
|
+
// ═══════════ BITABLE ═══════════
|
|
209
|
+
{
|
|
210
|
+
displayName: 'Operation',
|
|
211
|
+
name: 'operation',
|
|
212
|
+
type: 'options',
|
|
213
|
+
noDataExpression: true,
|
|
214
|
+
displayOptions: { show: { resource: ['bitable'] } },
|
|
215
|
+
options: [
|
|
216
|
+
{ name: '⭐ Read Records', value: 'readBitable' },
|
|
217
|
+
{ name: '⭐ Create Record', value: 'createBitable' },
|
|
218
|
+
{ name: '⭐ Update Record', value: 'updateBitable' },
|
|
219
|
+
{ name: '⭐ Delete Record', value: 'deleteBitable' },
|
|
220
|
+
{ name: '⭐ Search Records', value: 'searchBitable' },
|
|
221
|
+
{ name: '⭐ List Tables', value: 'listBitableTables' },
|
|
222
|
+
],
|
|
223
|
+
default: 'readBitable',
|
|
113
224
|
},
|
|
114
|
-
// --- Read Bitable (Pro) ---
|
|
115
225
|
{
|
|
116
226
|
displayName: 'Bitable App Token',
|
|
117
|
-
name: '
|
|
227
|
+
name: 'appToken',
|
|
118
228
|
type: 'string',
|
|
119
229
|
default: '',
|
|
120
230
|
required: true,
|
|
121
|
-
displayOptions: { show: { operation: ['readBitable', '
|
|
122
|
-
description: '
|
|
231
|
+
displayOptions: { show: { resource: ['bitable'], operation: ['readBitable', 'createBitable', 'updateBitable', 'deleteBitable', 'searchBitable', 'listBitableTables'] } },
|
|
232
|
+
description: 'From the Bitable URL: https://xxx.feishu.cn/base/{AppToken}?...',
|
|
123
233
|
},
|
|
124
234
|
{
|
|
125
235
|
displayName: 'Table ID',
|
|
@@ -127,154 +237,241 @@ class Feishu {
|
|
|
127
237
|
type: 'string',
|
|
128
238
|
default: '',
|
|
129
239
|
required: true,
|
|
130
|
-
displayOptions: { show: { operation: ['readBitable', '
|
|
240
|
+
displayOptions: { show: { resource: ['bitable'], operation: ['readBitable', 'createBitable', 'updateBitable', 'deleteBitable', 'searchBitable'] } },
|
|
131
241
|
placeholder: 'tblXXXXXXXXXXXXX',
|
|
242
|
+
description: 'Use "List Tables" to find table IDs',
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
displayName: 'Page Size',
|
|
246
|
+
name: 'pageSize',
|
|
247
|
+
type: 'number',
|
|
248
|
+
default: 50,
|
|
249
|
+
displayOptions: { show: { resource: ['bitable'], operation: ['readBitable', 'searchBitable'] } },
|
|
250
|
+
description: 'Max records per page (1-500)',
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
displayName: 'Search Filter',
|
|
254
|
+
name: 'searchFilter',
|
|
255
|
+
type: 'string',
|
|
256
|
+
default: '',
|
|
257
|
+
displayOptions: { show: { resource: ['bitable'], operation: ['searchBitable'] } },
|
|
258
|
+
placeholder: 'Field Name = "value"',
|
|
259
|
+
description: 'Field name and value to search for (exact match)',
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
displayName: 'Record ID',
|
|
263
|
+
name: 'recordId',
|
|
264
|
+
type: 'string',
|
|
265
|
+
default: '',
|
|
266
|
+
required: true,
|
|
267
|
+
displayOptions: { show: { resource: ['bitable'], operation: ['updateBitable', 'deleteBitable'] } },
|
|
268
|
+
placeholder: 'recXXXXXXXXXXXXX',
|
|
269
|
+
description: 'Record ID from a previous Read operation',
|
|
132
270
|
},
|
|
133
|
-
// --- Write Bitable (Pro) ---
|
|
134
271
|
{
|
|
135
272
|
displayName: 'Fields (JSON)',
|
|
136
|
-
name: '
|
|
273
|
+
name: 'fields',
|
|
137
274
|
type: 'json',
|
|
138
|
-
default: '{"
|
|
275
|
+
default: '{\n "Name": "value",\n "Status": "done"\n}',
|
|
276
|
+
required: true,
|
|
277
|
+
displayOptions: { show: { resource: ['bitable'], operation: ['createBitable', 'updateBitable'] } },
|
|
278
|
+
},
|
|
279
|
+
// ═══════════ APPROVALS ═══════════
|
|
280
|
+
{
|
|
281
|
+
displayName: 'Operation',
|
|
282
|
+
name: 'operation',
|
|
283
|
+
type: 'options',
|
|
284
|
+
noDataExpression: true,
|
|
285
|
+
displayOptions: { show: { resource: ['approvals'] } },
|
|
286
|
+
options: [
|
|
287
|
+
{ name: '⭐ List Pending Approvals', value: 'listApprovals' },
|
|
288
|
+
{ name: '⭐ Get Approval Detail', value: 'getApprovalDetail' },
|
|
289
|
+
{ name: '⭐ Approve Instance', value: 'approveInstance' },
|
|
290
|
+
{ name: '⭐ Reject Instance', value: 'rejectInstance' },
|
|
291
|
+
],
|
|
292
|
+
default: 'listApprovals',
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
displayName: 'Approval Code',
|
|
296
|
+
name: 'approvalCode',
|
|
297
|
+
type: 'string',
|
|
298
|
+
default: '',
|
|
299
|
+
displayOptions: { show: { resource: ['approvals'], operation: ['listApprovals', 'getApprovalDetail', 'approveInstance', 'rejectInstance'] } },
|
|
300
|
+
placeholder: 'Optional — filter by approval type',
|
|
301
|
+
description: 'Leave empty to list all pending. Available from Feishu Admin → Approvals.',
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
displayName: 'Instance Code',
|
|
305
|
+
name: 'instanceCode',
|
|
306
|
+
type: 'string',
|
|
307
|
+
default: '',
|
|
308
|
+
required: true,
|
|
309
|
+
displayOptions: { show: { resource: ['approvals'], operation: ['getApprovalDetail', 'approveInstance', 'rejectInstance'] } },
|
|
310
|
+
description: 'The specific approval instance. Get from "List Pending Approvals".',
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
displayName: 'Comment (optional)',
|
|
314
|
+
name: 'approvalComment',
|
|
315
|
+
type: 'string',
|
|
316
|
+
default: '',
|
|
317
|
+
displayOptions: { show: { resource: ['approvals'], operation: ['approveInstance', 'rejectInstance'] } },
|
|
318
|
+
},
|
|
319
|
+
// ═══════════ CALENDAR ═══════════
|
|
320
|
+
{
|
|
321
|
+
displayName: 'Operation',
|
|
322
|
+
name: 'operation',
|
|
323
|
+
type: 'options',
|
|
324
|
+
noDataExpression: true,
|
|
325
|
+
displayOptions: { show: { resource: ['calendar'] } },
|
|
326
|
+
options: [
|
|
327
|
+
{ name: '⭐ List Events', value: 'listCalendarEvents' },
|
|
328
|
+
{ name: '⭐ Create Event', value: 'createCalendarEvent' },
|
|
329
|
+
],
|
|
330
|
+
default: 'listCalendarEvents',
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
displayName: 'Calendar ID',
|
|
334
|
+
name: 'calendarId',
|
|
335
|
+
type: 'string',
|
|
336
|
+
default: 'primary',
|
|
337
|
+
displayOptions: { show: { resource: ['calendar'], operation: ['listCalendarEvents', 'createCalendarEvent'] } },
|
|
338
|
+
description: 'Calendar ID. Use "primary" for the default calendar.',
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
displayName: 'Start Time',
|
|
342
|
+
name: 'startTime',
|
|
343
|
+
type: 'string',
|
|
344
|
+
default: '',
|
|
345
|
+
required: true,
|
|
346
|
+
displayOptions: { show: { resource: ['calendar'], operation: ['listCalendarEvents'] } },
|
|
347
|
+
placeholder: '2024-01-01T00:00:00+08:00',
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
displayName: 'End Time',
|
|
351
|
+
name: 'endTime',
|
|
352
|
+
type: 'string',
|
|
353
|
+
default: '',
|
|
354
|
+
required: true,
|
|
355
|
+
displayOptions: { show: { resource: ['calendar'], operation: ['listCalendarEvents'] } },
|
|
356
|
+
placeholder: '2024-01-31T23:59:59+08:00',
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
displayName: 'Event Title',
|
|
360
|
+
name: 'eventTitle',
|
|
361
|
+
type: 'string',
|
|
362
|
+
default: '',
|
|
363
|
+
required: true,
|
|
364
|
+
displayOptions: { show: { resource: ['calendar'], operation: ['createCalendarEvent'] } },
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
displayName: 'Event Start',
|
|
368
|
+
name: 'eventStart',
|
|
369
|
+
type: 'string',
|
|
370
|
+
default: '',
|
|
371
|
+
required: true,
|
|
372
|
+
displayOptions: { show: { resource: ['calendar'], operation: ['createCalendarEvent'] } },
|
|
373
|
+
placeholder: '2024-06-21T14:00:00',
|
|
374
|
+
description: 'ISO 8601 datetime string',
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
displayName: 'Event End',
|
|
378
|
+
name: 'eventEnd',
|
|
379
|
+
type: 'string',
|
|
380
|
+
default: '',
|
|
381
|
+
required: true,
|
|
382
|
+
displayOptions: { show: { resource: ['calendar'], operation: ['createCalendarEvent'] } },
|
|
383
|
+
placeholder: '2024-06-21T15:00:00',
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
displayName: 'Description (optional)',
|
|
387
|
+
name: 'eventDesc',
|
|
388
|
+
type: 'string',
|
|
389
|
+
typeOptions: { rows: 3 },
|
|
390
|
+
default: '',
|
|
391
|
+
displayOptions: { show: { resource: ['calendar'], operation: ['createCalendarEvent'] } },
|
|
392
|
+
},
|
|
393
|
+
// ═══════════ CONTACTS ═══════════
|
|
394
|
+
{
|
|
395
|
+
displayName: 'Operation',
|
|
396
|
+
name: 'operation',
|
|
397
|
+
type: 'options',
|
|
398
|
+
noDataExpression: true,
|
|
399
|
+
displayOptions: { show: { resource: ['contacts'] } },
|
|
400
|
+
options: [
|
|
401
|
+
{ name: '⭐ Get User Info', value: 'getUserInfo' },
|
|
402
|
+
{ name: '⭐ Search Users', value: 'searchUsers' },
|
|
403
|
+
],
|
|
404
|
+
default: 'getUserInfo',
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
displayName: 'Lookup By',
|
|
408
|
+
name: 'lookupBy',
|
|
409
|
+
type: 'options',
|
|
410
|
+
options: [
|
|
411
|
+
{ name: 'Open ID', value: 'open_id' },
|
|
412
|
+
{ name: 'Email', value: 'email' },
|
|
413
|
+
{ name: 'Mobile', value: 'mobile' },
|
|
414
|
+
],
|
|
415
|
+
default: 'open_id',
|
|
416
|
+
displayOptions: { show: { resource: ['contacts'], operation: ['getUserInfo'] } },
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
displayName: 'User ID / Email / Mobile',
|
|
420
|
+
name: 'userLookup',
|
|
421
|
+
type: 'string',
|
|
422
|
+
default: '',
|
|
423
|
+
required: true,
|
|
424
|
+
displayOptions: { show: { resource: ['contacts'], operation: ['getUserInfo'] } },
|
|
425
|
+
placeholder: 'ou_xxx or user@example.com',
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
displayName: 'Search Query',
|
|
429
|
+
name: 'userSearch',
|
|
430
|
+
type: 'string',
|
|
431
|
+
default: '',
|
|
139
432
|
required: true,
|
|
140
|
-
displayOptions: { show: { operation: ['
|
|
141
|
-
|
|
433
|
+
displayOptions: { show: { resource: ['contacts'], operation: ['searchUsers'] } },
|
|
434
|
+
placeholder: 'john',
|
|
435
|
+
description: 'Search by name or email prefix. Returns up to 50 matches.',
|
|
142
436
|
},
|
|
143
437
|
],
|
|
144
438
|
};
|
|
145
439
|
}
|
|
146
|
-
//
|
|
440
|
+
// ═══════════════ EXECUTE ═══════════════
|
|
147
441
|
async execute() {
|
|
148
442
|
const items = this.getInputData();
|
|
149
443
|
const returnData = [];
|
|
150
|
-
const
|
|
151
|
-
const
|
|
152
|
-
const
|
|
153
|
-
const
|
|
444
|
+
const cred = await this.getCredentials('feishuApi');
|
|
445
|
+
const platform = cred.platform || 'feishu';
|
|
446
|
+
const appId = cred.appId;
|
|
447
|
+
const appSecret = cred.appSecret;
|
|
448
|
+
const licenseKey = cred.licenseKey || '';
|
|
449
|
+
const baseUrl = API_HOST[platform];
|
|
450
|
+
// Check license for all resources except messaging-free
|
|
451
|
+
// (License check is per-operation, not per-resource)
|
|
154
452
|
for (let i = 0; i < items.length; i++) {
|
|
155
453
|
try {
|
|
156
454
|
const operation = this.getNodeParameter('operation', i);
|
|
157
|
-
// License
|
|
158
|
-
if (
|
|
455
|
+
// ── License gate ──
|
|
456
|
+
if (PRO_OPS.includes(operation)) {
|
|
159
457
|
if (!licenseKey) {
|
|
160
|
-
throw new n8n_workflow_1.NodeOperationError(this.getNode(), '⭐
|
|
161
|
-
'👉 Get a License Key: https://1717465779306.gumroad.com/l/feishu-pro');
|
|
162
|
-
}
|
|
163
|
-
const validation = await license_1.validateLicense.call(this, licenseKey, appId);
|
|
164
|
-
if (!validation.valid) {
|
|
165
|
-
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `License Key invalid or expired: ${validation.reason}`);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
const token = await getFeishuToken.call(this, appId, appSecret);
|
|
169
|
-
switch (operation) {
|
|
170
|
-
case 'sendTextMessage': {
|
|
171
|
-
const receiveIdType = this.getNodeParameter('receiveIdType', i);
|
|
172
|
-
const receiveId = this.getNodeParameter('receiveId', i);
|
|
173
|
-
const textContent = this.getNodeParameter('textContent', i);
|
|
174
|
-
const response = await this.helpers.request({
|
|
175
|
-
method: 'POST',
|
|
176
|
-
url: `https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=${receiveIdType}`,
|
|
177
|
-
headers: {
|
|
178
|
-
'Content-Type': 'application/json',
|
|
179
|
-
Authorization: `Bearer ${token}`,
|
|
180
|
-
},
|
|
181
|
-
body: {
|
|
182
|
-
receive_id: receiveId,
|
|
183
|
-
msg_type: 'text',
|
|
184
|
-
content: JSON.stringify({ text: textContent }),
|
|
185
|
-
},
|
|
186
|
-
json: true,
|
|
187
|
-
});
|
|
188
|
-
returnData.push({ json: response });
|
|
189
|
-
break;
|
|
190
|
-
}
|
|
191
|
-
case 'sendCardMessage': {
|
|
192
|
-
const receiveIdType = this.getNodeParameter('receiveIdType', i);
|
|
193
|
-
const receiveId = this.getNodeParameter('receiveId', i);
|
|
194
|
-
const cardTitle = this.getNodeParameter('cardTitle', i);
|
|
195
|
-
const cardContent = this.getNodeParameter('cardContent', i);
|
|
196
|
-
const response = await this.helpers.request({
|
|
197
|
-
method: 'POST',
|
|
198
|
-
url: `https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=${receiveIdType}`,
|
|
199
|
-
headers: {
|
|
200
|
-
'Content-Type': 'application/json',
|
|
201
|
-
Authorization: `Bearer ${token}`,
|
|
202
|
-
},
|
|
203
|
-
body: {
|
|
204
|
-
receive_id: receiveId,
|
|
205
|
-
msg_type: 'interactive',
|
|
206
|
-
content: JSON.stringify({
|
|
207
|
-
config: { wide_screen_mode: true },
|
|
208
|
-
header: {
|
|
209
|
-
title: { tag: 'plain_text', content: cardTitle },
|
|
210
|
-
},
|
|
211
|
-
elements: [
|
|
212
|
-
{
|
|
213
|
-
tag: 'div',
|
|
214
|
-
text: { tag: 'lark_md', content: cardContent },
|
|
215
|
-
},
|
|
216
|
-
],
|
|
217
|
-
}),
|
|
218
|
-
},
|
|
219
|
-
json: true,
|
|
220
|
-
});
|
|
221
|
-
returnData.push({ json: response });
|
|
222
|
-
break;
|
|
223
|
-
}
|
|
224
|
-
case 'readBitable': {
|
|
225
|
-
const appToken = this.getNodeParameter('bitableAppToken', i);
|
|
226
|
-
const tableId = this.getNodeParameter('tableId', i);
|
|
227
|
-
const response = await this.helpers.request({
|
|
228
|
-
method: 'GET',
|
|
229
|
-
url: `https://open.feishu.cn/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/records?page_size=100`,
|
|
230
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
231
|
-
json: true,
|
|
232
|
-
});
|
|
233
|
-
const records = (response.data?.items || []).map((item) => item.fields);
|
|
234
|
-
returnData.push({ json: { total: response.data?.total || 0, records } });
|
|
235
|
-
break;
|
|
236
|
-
}
|
|
237
|
-
case 'writeBitable': {
|
|
238
|
-
const appToken = this.getNodeParameter('bitableAppToken', i);
|
|
239
|
-
const tableId = this.getNodeParameter('tableId', i);
|
|
240
|
-
const fields = this.getNodeParameter('bitableFields', i);
|
|
241
|
-
const response = await this.helpers.request({
|
|
242
|
-
method: 'POST',
|
|
243
|
-
url: `https://open.feishu.cn/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/records`,
|
|
244
|
-
headers: {
|
|
245
|
-
'Content-Type': 'application/json',
|
|
246
|
-
Authorization: `Bearer ${token}`,
|
|
247
|
-
},
|
|
248
|
-
body: { fields: typeof fields === 'string' ? JSON.parse(fields) : fields },
|
|
249
|
-
json: true,
|
|
250
|
-
});
|
|
251
|
-
returnData.push({ json: response });
|
|
252
|
-
break;
|
|
253
|
-
}
|
|
254
|
-
case 'getApprovals': {
|
|
255
|
-
const response = await this.helpers.request({
|
|
256
|
-
method: 'GET',
|
|
257
|
-
url: 'https://open.feishu.cn/open-apis/approval/v4/instances?page_size=20&status=PENDING',
|
|
258
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
259
|
-
json: true,
|
|
260
|
-
});
|
|
261
|
-
const instances = (response.data?.instance_list || []).map((inst) => ({
|
|
262
|
-
instanceCode: inst.instance_code,
|
|
263
|
-
approvalName: inst.approval_name,
|
|
264
|
-
startTime: inst.start_time,
|
|
265
|
-
status: inst.status,
|
|
266
|
-
}));
|
|
267
|
-
returnData.push({ json: { pending: instances.length, instances } });
|
|
268
|
-
break;
|
|
458
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), '⭐ Pro feature. Add your License Key in credentials.\n👉 https://1717465779306.gumroad.com/l/feishu-pro');
|
|
269
459
|
}
|
|
460
|
+
const v = await license_1.validateLicense.call(this, licenseKey, appId);
|
|
461
|
+
if (!v.valid)
|
|
462
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `License Key invalid: ${v.reason}`);
|
|
270
463
|
}
|
|
464
|
+
const token = await getToken(this, appId, appSecret, baseUrl);
|
|
465
|
+
// ── Route ──
|
|
466
|
+
const result = await executeOperation.call(this, operation, token, baseUrl, i);
|
|
467
|
+
returnData.push({ json: result });
|
|
271
468
|
}
|
|
272
|
-
catch (
|
|
273
|
-
if (
|
|
274
|
-
returnData.push({ json: { error:
|
|
469
|
+
catch (err) {
|
|
470
|
+
if (err instanceof n8n_workflow_1.NodeOperationError) {
|
|
471
|
+
returnData.push({ json: { error: err.message }, error: err });
|
|
275
472
|
}
|
|
276
473
|
else {
|
|
277
|
-
returnData.push({ json: { error:
|
|
474
|
+
returnData.push({ json: { error: err.message || 'Unknown error' } });
|
|
278
475
|
}
|
|
279
476
|
}
|
|
280
477
|
}
|
|
@@ -282,3 +479,239 @@ class Feishu {
|
|
|
282
479
|
}
|
|
283
480
|
}
|
|
284
481
|
exports.Feishu = Feishu;
|
|
482
|
+
// ─── Operation router ─────────────────────────────────────────────────
|
|
483
|
+
async function executeOperation(operation, token, baseUrl, i) {
|
|
484
|
+
const hdr = { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` };
|
|
485
|
+
switch (operation) {
|
|
486
|
+
// ── MESSAGING ──────────────────────────────────────────
|
|
487
|
+
case 'sendTextMessage': {
|
|
488
|
+
const idType = this.getNodeParameter('receiveIdType', i);
|
|
489
|
+
const rid = this.getNodeParameter('receiveId', i);
|
|
490
|
+
const text = this.getNodeParameter('textContent', i);
|
|
491
|
+
return await this.helpers.request({
|
|
492
|
+
method: 'POST', url: `${baseUrl}/open-apis/im/v1/messages?receive_id_type=${idType}`,
|
|
493
|
+
headers: hdr,
|
|
494
|
+
body: { receive_id: rid, msg_type: 'text', content: JSON.stringify({ text }) },
|
|
495
|
+
json: true,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
case 'sendCardMessage': {
|
|
499
|
+
const idType = this.getNodeParameter('receiveIdType', i);
|
|
500
|
+
const rid = this.getNodeParameter('receiveId', i);
|
|
501
|
+
const title = this.getNodeParameter('cardTitle', i);
|
|
502
|
+
const body = this.getNodeParameter('cardBody', i);
|
|
503
|
+
const color = this.getNodeParameter('cardColor', i);
|
|
504
|
+
const colorMap = { blue: 'blue', green: 'green', red: 'red', yellow: 'yellow', purple: 'purple', default: 'default' };
|
|
505
|
+
return await this.helpers.request({
|
|
506
|
+
method: 'POST', url: `${baseUrl}/open-apis/im/v1/messages?receive_id_type=${idType}`,
|
|
507
|
+
headers: hdr,
|
|
508
|
+
body: {
|
|
509
|
+
receive_id: rid, msg_type: 'interactive',
|
|
510
|
+
content: JSON.stringify({
|
|
511
|
+
config: { wide_screen_mode: true },
|
|
512
|
+
header: {
|
|
513
|
+
title: { tag: 'plain_text', content: title },
|
|
514
|
+
...(color !== 'default' ? { template: colorMap[color] } : {}),
|
|
515
|
+
},
|
|
516
|
+
elements: [{ tag: 'div', text: { tag: 'lark_md', content: body } }],
|
|
517
|
+
}),
|
|
518
|
+
},
|
|
519
|
+
json: true,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
case 'sendImage': {
|
|
523
|
+
const idType = this.getNodeParameter('receiveIdType', i);
|
|
524
|
+
const rid = this.getNodeParameter('receiveId', i);
|
|
525
|
+
const imgUrl = this.getNodeParameter('imageUrl', i);
|
|
526
|
+
return await this.helpers.request({
|
|
527
|
+
method: 'POST', url: `${baseUrl}/open-apis/im/v1/messages?receive_id_type=${idType}`,
|
|
528
|
+
headers: hdr,
|
|
529
|
+
body: { receive_id: rid, msg_type: 'image', content: JSON.stringify({ image_key: imgUrl }) },
|
|
530
|
+
json: true,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
case 'sendFile': {
|
|
534
|
+
const idType = this.getNodeParameter('receiveIdType', i);
|
|
535
|
+
const rid = this.getNodeParameter('receiveId', i);
|
|
536
|
+
const fileUrl = this.getNodeParameter('fileUrl', i);
|
|
537
|
+
const fileName = this.getNodeParameter('fileName', i);
|
|
538
|
+
return await this.helpers.request({
|
|
539
|
+
method: 'POST', url: `${baseUrl}/open-apis/im/v1/messages?receive_id_type=${idType}`,
|
|
540
|
+
headers: hdr,
|
|
541
|
+
body: { receive_id: rid, msg_type: 'file', content: JSON.stringify({ file_key: fileUrl, file_name: fileName }) },
|
|
542
|
+
json: true,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
case 'batchSend': {
|
|
546
|
+
const raw = this.getNodeParameter('batchRecipients', i);
|
|
547
|
+
const msg = this.getNodeParameter('batchMessage', i);
|
|
548
|
+
const ids = raw.split('\n').map(s => s.trim()).filter(Boolean);
|
|
549
|
+
const results = [];
|
|
550
|
+
for (const rid of ids) {
|
|
551
|
+
const idType = rid.includes('@') ? 'email' : (rid.startsWith('ou_') ? 'open_id' : 'open_id');
|
|
552
|
+
try {
|
|
553
|
+
const r = await this.helpers.request({
|
|
554
|
+
method: 'POST', url: `${baseUrl}/open-apis/im/v1/messages?receive_id_type=${idType}`,
|
|
555
|
+
headers: hdr,
|
|
556
|
+
body: { receive_id: rid, msg_type: 'text', content: JSON.stringify({ text: msg }) },
|
|
557
|
+
json: true,
|
|
558
|
+
});
|
|
559
|
+
results.push({ recipient: rid, status: 'sent', messageId: r.data?.message_id });
|
|
560
|
+
}
|
|
561
|
+
catch (e) {
|
|
562
|
+
results.push({ recipient: rid, status: 'failed', error: e.message });
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return { results, sent: results.filter(r => r.status === 'sent').length, failed: results.filter(r => r.status === 'failed').length };
|
|
566
|
+
}
|
|
567
|
+
// ── BITABLE ────────────────────────────────────────────
|
|
568
|
+
case 'readBitable': {
|
|
569
|
+
const appToken = this.getNodeParameter('appToken', i);
|
|
570
|
+
const tableId = this.getNodeParameter('tableId', i);
|
|
571
|
+
const ps = this.getNodeParameter('pageSize', i);
|
|
572
|
+
const res = await this.helpers.request({
|
|
573
|
+
method: 'GET', url: `${baseUrl}/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/records?page_size=${ps}`,
|
|
574
|
+
headers: { Authorization: `Bearer ${token}` }, json: true,
|
|
575
|
+
});
|
|
576
|
+
return { total: res.data?.total || 0, pageSize: ps, records: (res.data?.items || []).map((it) => ({ recordId: it.record_id, ...it.fields })) };
|
|
577
|
+
}
|
|
578
|
+
case 'createBitable': {
|
|
579
|
+
const appToken = this.getNodeParameter('appToken', i);
|
|
580
|
+
const tableId = this.getNodeParameter('tableId', i);
|
|
581
|
+
const fields = this.getNodeParameter('fields', i);
|
|
582
|
+
return await this.helpers.request({
|
|
583
|
+
method: 'POST', url: `${baseUrl}/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/records`,
|
|
584
|
+
headers: hdr,
|
|
585
|
+
body: { fields: typeof fields === 'string' ? JSON.parse(fields) : fields },
|
|
586
|
+
json: true,
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
case 'updateBitable': {
|
|
590
|
+
const appToken = this.getNodeParameter('appToken', i);
|
|
591
|
+
const tableId = this.getNodeParameter('tableId', i);
|
|
592
|
+
const recId = this.getNodeParameter('recordId', i);
|
|
593
|
+
const fields = this.getNodeParameter('fields', i);
|
|
594
|
+
return await this.helpers.request({
|
|
595
|
+
method: 'PUT', url: `${baseUrl}/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/records/${recId}`,
|
|
596
|
+
headers: hdr,
|
|
597
|
+
body: { fields: typeof fields === 'string' ? JSON.parse(fields) : fields },
|
|
598
|
+
json: true,
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
case 'deleteBitable': {
|
|
602
|
+
const appToken = this.getNodeParameter('appToken', i);
|
|
603
|
+
const tableId = this.getNodeParameter('tableId', i);
|
|
604
|
+
const recId = this.getNodeParameter('recordId', i);
|
|
605
|
+
return await this.helpers.request({
|
|
606
|
+
method: 'DELETE', url: `${baseUrl}/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/records/${recId}`,
|
|
607
|
+
headers: { Authorization: `Bearer ${token}` }, json: true,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
case 'searchBitable': {
|
|
611
|
+
const appToken = this.getNodeParameter('appToken', i);
|
|
612
|
+
const tableId = this.getNodeParameter('tableId', i);
|
|
613
|
+
const filter = this.getNodeParameter('searchFilter', i);
|
|
614
|
+
const ps = this.getNodeParameter('pageSize', i);
|
|
615
|
+
// Use search filter with the bitable filter parameter
|
|
616
|
+
let url = `${baseUrl}/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/records?page_size=${ps}`;
|
|
617
|
+
if (filter)
|
|
618
|
+
url += `&filter=${encodeURIComponent(filter)}`;
|
|
619
|
+
const res = await this.helpers.request({
|
|
620
|
+
method: 'GET', url, headers: { Authorization: `Bearer ${token}` }, json: true,
|
|
621
|
+
});
|
|
622
|
+
return { total: res.data?.total || 0, matched: (res.data?.items || []).map((it) => ({ recordId: it.record_id, ...it.fields })) };
|
|
623
|
+
}
|
|
624
|
+
case 'listBitableTables': {
|
|
625
|
+
const appToken = this.getNodeParameter('appToken', i);
|
|
626
|
+
const res = await this.helpers.request({
|
|
627
|
+
method: 'GET', url: `${baseUrl}/open-apis/bitable/v1/apps/${appToken}/tables`,
|
|
628
|
+
headers: { Authorization: `Bearer ${token}` }, json: true,
|
|
629
|
+
});
|
|
630
|
+
return { tables: (res.data?.items || []).map((t) => ({ tableId: t.table_id, name: t.name, revision: t.revision })) };
|
|
631
|
+
}
|
|
632
|
+
// ── APPROVALS ──────────────────────────────────────────
|
|
633
|
+
case 'listApprovals': {
|
|
634
|
+
const ac = this.getNodeParameter('approvalCode', i);
|
|
635
|
+
let url = `${baseUrl}/open-apis/approval/v4/instances?page_size=20&status=PENDING`;
|
|
636
|
+
if (ac)
|
|
637
|
+
url += `&approval_code=${ac}`;
|
|
638
|
+
const res = await this.helpers.request({ method: 'GET', url, headers: { Authorization: `Bearer ${token}` }, json: true });
|
|
639
|
+
return { pending: (res.data?.instance_list || []).map((inst) => ({ instanceCode: inst.instance_code, approvalName: inst.approval_name, startTime: inst.start_time, status: inst.status })) };
|
|
640
|
+
}
|
|
641
|
+
case 'getApprovalDetail': {
|
|
642
|
+
const instCode = this.getNodeParameter('instanceCode', i);
|
|
643
|
+
const res = await this.helpers.request({
|
|
644
|
+
method: 'GET', url: `${baseUrl}/open-apis/approval/v4/instances/${instCode}`,
|
|
645
|
+
headers: { Authorization: `Bearer ${token}` }, json: true,
|
|
646
|
+
});
|
|
647
|
+
return { instanceCode: res.data?.instance_code, approvalName: res.data?.approval_name, status: res.data?.status, form: res.data?.form, timeline: res.data?.timeline };
|
|
648
|
+
}
|
|
649
|
+
case 'approveInstance': {
|
|
650
|
+
const instCode = this.getNodeParameter('instanceCode', i);
|
|
651
|
+
const comment = this.getNodeParameter('approvalComment', i);
|
|
652
|
+
return await this.helpers.request({
|
|
653
|
+
method: 'POST', url: `${baseUrl}/open-apis/approval/v4/instances/${instCode}/approve`,
|
|
654
|
+
headers: hdr, body: { comment }, json: true,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
case 'rejectInstance': {
|
|
658
|
+
const instCode = this.getNodeParameter('instanceCode', i);
|
|
659
|
+
const comment = this.getNodeParameter('approvalComment', i);
|
|
660
|
+
return await this.helpers.request({
|
|
661
|
+
method: 'POST', url: `${baseUrl}/open-apis/approval/v4/instances/${instCode}/reject`,
|
|
662
|
+
headers: hdr, body: { comment }, json: true,
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
// ── CALENDAR ───────────────────────────────────────────
|
|
666
|
+
case 'listCalendarEvents': {
|
|
667
|
+
const calId = this.getNodeParameter('calendarId', i);
|
|
668
|
+
const st = this.getNodeParameter('startTime', i);
|
|
669
|
+
const et = this.getNodeParameter('endTime', i);
|
|
670
|
+
const res = await this.helpers.request({
|
|
671
|
+
method: 'GET', url: `${baseUrl}/open-apis/calendar/v4/calendars/${calId}/events?start_time=${encodeURIComponent(st)}&end_time=${encodeURIComponent(et)}`,
|
|
672
|
+
headers: { Authorization: `Bearer ${token}` }, json: true,
|
|
673
|
+
});
|
|
674
|
+
return { events: (res.data?.items || []).map((ev) => ({ eventId: ev.event_id, summary: ev.summary, start: ev.start?.date_time, end: ev.end?.date_time, status: ev.status })) };
|
|
675
|
+
}
|
|
676
|
+
case 'createCalendarEvent': {
|
|
677
|
+
const calId = this.getNodeParameter('calendarId', i);
|
|
678
|
+
const title = this.getNodeParameter('eventTitle', i);
|
|
679
|
+
const st = this.getNodeParameter('eventStart', i);
|
|
680
|
+
const et = this.getNodeParameter('eventEnd', i);
|
|
681
|
+
const desc = this.getNodeParameter('eventDesc', i);
|
|
682
|
+
return await this.helpers.request({
|
|
683
|
+
method: 'POST', url: `${baseUrl}/open-apis/calendar/v4/calendars/${calId}/events`,
|
|
684
|
+
headers: hdr,
|
|
685
|
+
body: { summary: title, start: { date_time: st }, end: { date_time: et }, ...(desc ? { description: desc } : {}) },
|
|
686
|
+
json: true,
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
// ── CONTACTS ───────────────────────────────────────────
|
|
690
|
+
case 'getUserInfo': {
|
|
691
|
+
const by = this.getNodeParameter('lookupBy', i);
|
|
692
|
+
const val = this.getNodeParameter('userLookup', i);
|
|
693
|
+
let url = `${baseUrl}/open-apis/contact/v3/users`;
|
|
694
|
+
if (by === 'open_id')
|
|
695
|
+
url += `/${val}`;
|
|
696
|
+
else if (by === 'email')
|
|
697
|
+
url += `?email=${encodeURIComponent(val)}`;
|
|
698
|
+
else
|
|
699
|
+
url += `?mobile=${encodeURIComponent(val)}`;
|
|
700
|
+
const res = await this.helpers.request({ method: 'GET', url, headers: { Authorization: `Bearer ${token}` }, json: true });
|
|
701
|
+
const user = (res.data?.user || res.data?.items?.[0] || {});
|
|
702
|
+
return { openId: user.open_id, name: user.name, email: user.email, mobile: user.mobile, departmentIds: user.department_ids, avatar: user.avatar_url };
|
|
703
|
+
}
|
|
704
|
+
case 'searchUsers': {
|
|
705
|
+
const q = this.getNodeParameter('userSearch', i);
|
|
706
|
+
const res = await this.helpers.request({
|
|
707
|
+
method: 'GET', url: `${baseUrl}/open-apis/contact/v3/users?page_size=50`,
|
|
708
|
+
headers: { Authorization: `Bearer ${token}` }, json: true,
|
|
709
|
+
});
|
|
710
|
+
const all = (res.data?.items || []).map((u) => ({ openId: u.open_id, name: u.name, email: u.email, mobile: u.mobile, departmentIds: u.department_ids }));
|
|
711
|
+
const lower = q.toLowerCase();
|
|
712
|
+
return { query: q, results: all.filter((u) => (u.name || '').toLowerCase().includes(lower) || (u.email || '').toLowerCase().includes(lower)) };
|
|
713
|
+
}
|
|
714
|
+
default:
|
|
715
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unknown operation: ${operation}`);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
@@ -8,39 +8,39 @@ class FeishuTrigger {
|
|
|
8
8
|
name: 'feishuTrigger',
|
|
9
9
|
icon: 'file:feishu.svg',
|
|
10
10
|
group: ['trigger'],
|
|
11
|
-
version:
|
|
11
|
+
version: 2,
|
|
12
12
|
subtitle: '={{ $parameter["event"] }}',
|
|
13
|
-
description: 'Listen for Feishu events:
|
|
13
|
+
description: 'Listen for Feishu/Lark events: incoming messages, approval changes, Bitable updates',
|
|
14
14
|
defaults: { name: 'Feishu / Lark Trigger' },
|
|
15
15
|
inputs: [],
|
|
16
|
-
outputs: [
|
|
16
|
+
outputs: ['main'],
|
|
17
17
|
credentials: [{ name: 'feishuApi', required: true }],
|
|
18
|
-
webhooks: [
|
|
19
|
-
{
|
|
20
|
-
name: 'default',
|
|
21
|
-
httpMethod: 'POST',
|
|
22
|
-
responseMode: 'onReceived',
|
|
23
|
-
path: 'feishu-webhook',
|
|
24
|
-
},
|
|
25
|
-
],
|
|
18
|
+
webhooks: [{ name: 'default', httpMethod: 'POST', responseMode: 'onReceived', path: 'feishu' }],
|
|
26
19
|
properties: [
|
|
27
20
|
{
|
|
28
21
|
displayName: 'Event Type',
|
|
29
22
|
name: 'event',
|
|
30
23
|
type: 'options',
|
|
31
24
|
options: [
|
|
32
|
-
{ name: '🆓 Message Received (Free)', value: 'message_receive' },
|
|
33
|
-
{ name: '⭐ Approval Status Changed (Pro)', value: 'approval_change' },
|
|
25
|
+
{ name: '🆓 Message Received (Free)', value: 'message_receive', description: 'When the bot receives a message' },
|
|
26
|
+
{ name: '⭐ Approval Status Changed (Pro)', value: 'approval_change', description: 'When an approval is submitted/approved/rejected' },
|
|
27
|
+
{ name: '⭐ Bitable Record Changed (Pro)', value: 'bitable_change', description: 'When a record is created/updated in a Bitable' },
|
|
34
28
|
],
|
|
35
29
|
default: 'message_receive',
|
|
36
|
-
description: 'Pro events require a License Key',
|
|
37
30
|
},
|
|
38
31
|
{
|
|
39
|
-
displayName: 'Verification Token',
|
|
32
|
+
displayName: 'Verification Token (optional)',
|
|
40
33
|
name: 'verificationToken',
|
|
41
34
|
type: 'string',
|
|
42
35
|
default: '',
|
|
43
|
-
description: 'Feishu Event
|
|
36
|
+
description: 'From Feishu Developer Console → Event Subscriptions. For verifying callback source.',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
displayName: 'Webhook URL',
|
|
40
|
+
name: 'webhookUrl',
|
|
41
|
+
type: 'notice',
|
|
42
|
+
default: 'Copy the production URL above and paste it into Feishu Developer Console → Event Subscriptions → Request URL.',
|
|
43
|
+
displayOptions: { show: { event: ['message_receive', 'approval_change', 'bitable_change'] } },
|
|
44
44
|
},
|
|
45
45
|
],
|
|
46
46
|
};
|
|
@@ -48,33 +48,57 @@ class FeishuTrigger {
|
|
|
48
48
|
async webhook() {
|
|
49
49
|
const req = this.getRequestObject();
|
|
50
50
|
const body = req.body;
|
|
51
|
-
//
|
|
51
|
+
// URL verification challenge
|
|
52
52
|
if (body?.type === 'url_verification') {
|
|
53
|
-
return {
|
|
54
|
-
webhookResponse: { challenge: body.challenge },
|
|
55
|
-
workflowData: [],
|
|
56
|
-
};
|
|
53
|
+
return { webhookResponse: { challenge: body.challenge }, workflowData: [] };
|
|
57
54
|
}
|
|
55
|
+
// Extract event data
|
|
58
56
|
const event = body?.event || {};
|
|
59
57
|
const header = body?.header || {};
|
|
58
|
+
const output = {
|
|
59
|
+
eventType: header.event_type || body.event_type || 'unknown',
|
|
60
|
+
eventId: header.event_id || '',
|
|
61
|
+
appId: header.app_id || '',
|
|
62
|
+
tenantKey: header.tenant_key || '',
|
|
63
|
+
timestamp: header.create_time || new Date().toISOString(),
|
|
64
|
+
};
|
|
65
|
+
// Message event
|
|
66
|
+
if (event.sender?.sender_id) {
|
|
67
|
+
output.senderOpenId = event.sender.sender_id.open_id || '';
|
|
68
|
+
output.senderUserId = event.sender.sender_id.user_id || '';
|
|
69
|
+
output.chatId = event.message?.chat_id || '';
|
|
70
|
+
output.chatType = event.message?.chat_type || '';
|
|
71
|
+
output.messageId = event.message?.message_id || '';
|
|
72
|
+
output.messageType = event.message?.msg_type || '';
|
|
73
|
+
// Parse message content if text
|
|
74
|
+
if (event.message?.content) {
|
|
75
|
+
try {
|
|
76
|
+
const content = JSON.parse(event.message.content);
|
|
77
|
+
output.messageText = content.text || '';
|
|
78
|
+
output.rawContent = content;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
output.messageText = event.message.content;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Approval event
|
|
86
|
+
if (event.approval_code) {
|
|
87
|
+
output.approvalCode = event.approval_code;
|
|
88
|
+
output.instanceCode = event.instance_code;
|
|
89
|
+
output.approvalStatus = event.status;
|
|
90
|
+
}
|
|
91
|
+
// Bitable event
|
|
92
|
+
if (event.table_id) {
|
|
93
|
+
output.tableId = event.table_id;
|
|
94
|
+
output.recordId = event.record_id;
|
|
95
|
+
output.action = event.action; // 'insert' or 'update'
|
|
96
|
+
}
|
|
97
|
+
// Include raw event for advanced use
|
|
98
|
+
output.rawEvent = event;
|
|
60
99
|
return {
|
|
61
|
-
webhookResponse: { code: 0, msg: '
|
|
62
|
-
workflowData: [
|
|
63
|
-
[
|
|
64
|
-
{
|
|
65
|
-
json: {
|
|
66
|
-
eventType: header.event_type || 'unknown',
|
|
67
|
-
eventId: header.event_id || '',
|
|
68
|
-
appId: header.app_id || '',
|
|
69
|
-
tenantKey: header.tenant_key || '',
|
|
70
|
-
senderOpenId: event.sender?.sender_id?.open_id || '',
|
|
71
|
-
messageType: event.message?.msg_type || '',
|
|
72
|
-
messageContent: event.message?.content || '',
|
|
73
|
-
sourceData: event,
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
],
|
|
77
|
-
],
|
|
100
|
+
webhookResponse: { code: 0, msg: 'success' },
|
|
101
|
+
workflowData: [[{ json: output }]],
|
|
78
102
|
};
|
|
79
103
|
}
|
|
80
104
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.explainError = explainError;
|
|
4
|
+
/**
|
|
5
|
+
* Feishu / Lark API error codes → English explanations
|
|
6
|
+
*/
|
|
7
|
+
const ERROR_MAP = {
|
|
8
|
+
// Auth & Token
|
|
9
|
+
99991672: 'Permission denied. Add the required scope in Feishu Developer Console → Permissions.',
|
|
10
|
+
99991668: 'App not published. Publish your app in Developer Console → Version Management.',
|
|
11
|
+
99991663: 'Invalid tenant access token. App ID or App Secret may be wrong.',
|
|
12
|
+
// Bot & Messaging
|
|
13
|
+
230006: 'Bot ability not activated. Enable Bot in Developer Console → App Capabilities.',
|
|
14
|
+
230001: 'Invalid recipient. The user/chat ID does not exist or app is not in the chat.',
|
|
15
|
+
99992351: 'Recipient ID not found. Check the ID type (open_id vs email vs chat_id). Use "Get User Info" operation to find the correct ID.',
|
|
16
|
+
230002: 'Message content is empty or malformed.',
|
|
17
|
+
// Bitable
|
|
18
|
+
1740010: 'Bitable App Token not found. Copy it from the Bitable URL: /base/{AppToken}/...',
|
|
19
|
+
1740011: 'Table ID not found. Use "List Tables" operation to find valid table IDs.',
|
|
20
|
+
1740012: 'Field not found in the table. Check that field names match exactly (case-sensitive).',
|
|
21
|
+
1740013: 'Invalid field value type. Check the expected field type (text/number/date/etc).',
|
|
22
|
+
// Approval
|
|
23
|
+
1820000: 'No pending approvals found.',
|
|
24
|
+
1820001: 'Approval instance not found or already processed.',
|
|
25
|
+
// Rate Limit
|
|
26
|
+
99991400: 'Rate limited. Too many requests — wait and retry.',
|
|
27
|
+
// General
|
|
28
|
+
10003: 'Missing required parameter. Check all fields are filled.',
|
|
29
|
+
10013: 'Invalid token. Try re-entering your App credentials.',
|
|
30
|
+
// Lark-specific
|
|
31
|
+
99991600: 'API domain mismatch. If using Lark, ensure Platform is set to "Lark" in credentials.',
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Get a human-readable error message for a Feishu error code.
|
|
35
|
+
*/
|
|
36
|
+
function explainError(code, msg) {
|
|
37
|
+
const explanation = ERROR_MAP[code];
|
|
38
|
+
if (explanation) {
|
|
39
|
+
return `${explanation} [Feishu code: ${code}]`;
|
|
40
|
+
}
|
|
41
|
+
return `${msg || 'Unknown error'} [Feishu code: ${code}]`;
|
|
42
|
+
}
|
package/package.json
CHANGED