@hpplay-lebo/cluster-hub 3.0.0 → 3.2.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 +2 -0
- package/docs/GUIDE.md +13 -0
- package/package.json +1 -1
- package/src/feishu-tools.ts +197 -5
- package/src/index.ts +4 -1
package/README.md
CHANGED
|
@@ -141,6 +141,8 @@ openclaw hub tasks
|
|
|
141
141
|
| `feishu_wiki` | 知识库操作(空间/节点/创建) |
|
|
142
142
|
| `feishu_drive` | 云空间管理(文件列表/创建文件夹/移动/删除) |
|
|
143
143
|
| `feishu_perm` | 权限管理(查看/添加/移除协作者) |
|
|
144
|
+
| `feishu_contact` | 通讯录查询(按姓名搜索/按邮箱查 open_id) |
|
|
145
|
+
| `feishu_message` | 发送飞书消息(文本/富文本/批量发送) |
|
|
144
146
|
| `feishu_app_scopes` | 查看应用权限 |
|
|
145
147
|
|
|
146
148
|
> **冲突处理**: 如果节点已安装并启用了 OpenClaw 飞书插件,Hub 下发的工具会自动跳过,不会冲突。
|
package/docs/GUIDE.md
CHANGED
|
@@ -42,6 +42,19 @@ openclaw plugins list
|
|
|
42
42
|
# 应看到 cluster-hub 状态为 loaded
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
### 更新插件
|
|
46
|
+
|
|
47
|
+
已安装过的用户,拉取最新版本即可:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
cd ~/.openclaw/extensions/cluster-hub
|
|
51
|
+
git pull
|
|
52
|
+
|
|
53
|
+
# 重启 Gateway 加载新代码
|
|
54
|
+
kill -9 $(pgrep -f "openclaw.*gateway")
|
|
55
|
+
openclaw gateway start
|
|
56
|
+
```
|
|
57
|
+
|
|
45
58
|
---
|
|
46
59
|
|
|
47
60
|
## 第二步:创建集群(根节点)
|
package/package.json
CHANGED
package/src/feishu-tools.ts
CHANGED
|
@@ -13,7 +13,13 @@ interface FeishuCredentials {
|
|
|
13
13
|
domain?: string;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
interface OwnerInfo {
|
|
17
|
+
openId: string;
|
|
18
|
+
name?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
16
21
|
let _credentials: FeishuCredentials | null = null;
|
|
22
|
+
let _owner: OwnerInfo | null = null;
|
|
17
23
|
let _tenantToken: string | null = null;
|
|
18
24
|
let _tokenExpiresAt = 0;
|
|
19
25
|
|
|
@@ -29,6 +35,10 @@ export function setCredentials(creds: FeishuCredentials) {
|
|
|
29
35
|
_tokenExpiresAt = 0;
|
|
30
36
|
}
|
|
31
37
|
|
|
38
|
+
export function setOwner(owner: OwnerInfo | undefined) {
|
|
39
|
+
if (owner?.openId) _owner = owner;
|
|
40
|
+
}
|
|
41
|
+
|
|
32
42
|
export function hasCredentials(): boolean {
|
|
33
43
|
return !!_credentials?.appId && !!_credentials?.appSecret;
|
|
34
44
|
}
|
|
@@ -181,11 +191,23 @@ async function docCreate(title: string, folderToken?: string) {
|
|
|
181
191
|
folder_token: folderToken,
|
|
182
192
|
});
|
|
183
193
|
const doc = data?.document;
|
|
184
|
-
|
|
194
|
+
const result: any = {
|
|
185
195
|
document_id: doc?.document_id,
|
|
186
196
|
title: doc?.title,
|
|
187
197
|
url: `https://feishu.cn/docx/${doc?.document_id}`,
|
|
188
198
|
};
|
|
199
|
+
|
|
200
|
+
// 自动给 owner 加编辑权限
|
|
201
|
+
if (_owner?.openId && doc?.document_id) {
|
|
202
|
+
try {
|
|
203
|
+
await permAdd(doc.document_id, 'docx', 'openid', _owner.openId, 'full_access');
|
|
204
|
+
result.owner_permission = 'full_access';
|
|
205
|
+
} catch (e: any) {
|
|
206
|
+
result.owner_permission_error = e.message;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return result;
|
|
189
211
|
}
|
|
190
212
|
|
|
191
213
|
async function docWrite(docToken: string, markdown: string) {
|
|
@@ -369,7 +391,7 @@ async function permList(token: string, type: string) {
|
|
|
369
391
|
}
|
|
370
392
|
|
|
371
393
|
async function permAdd(token: string, type: string, memberType: string, memberId: string, perm: string) {
|
|
372
|
-
const data = await feishuPost(`/open-apis/drive/v1/permissions/${token}/members?type=${type}&need_notification=
|
|
394
|
+
const data = await feishuPost(`/open-apis/drive/v1/permissions/${token}/members?type=${type}&need_notification=true`, {
|
|
373
395
|
member_type: memberType, member_id: memberId, perm,
|
|
374
396
|
});
|
|
375
397
|
return { success: true, member: data?.member };
|
|
@@ -382,6 +404,88 @@ async function permRemove(token: string, type: string, memberType: string, membe
|
|
|
382
404
|
return { success: true };
|
|
383
405
|
}
|
|
384
406
|
|
|
407
|
+
// ============================================================
|
|
408
|
+
// Contact API
|
|
409
|
+
// ============================================================
|
|
410
|
+
|
|
411
|
+
async function contactSearch(query: string) {
|
|
412
|
+
const data = await feishuPost('/open-apis/search/v1/user', { query });
|
|
413
|
+
return {
|
|
414
|
+
users: (data?.users || []).map((u: any) => ({
|
|
415
|
+
open_id: u.open_id,
|
|
416
|
+
name: u.name,
|
|
417
|
+
en_name: u.en_name,
|
|
418
|
+
email: u.email,
|
|
419
|
+
avatar: u.avatar?.avatar_72,
|
|
420
|
+
department: u.department_name,
|
|
421
|
+
})),
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function contactBatchGetId(emails?: string[], mobiles?: string[]) {
|
|
426
|
+
const body: any = {};
|
|
427
|
+
if (emails?.length) body.emails = emails;
|
|
428
|
+
if (mobiles?.length) body.mobiles = mobiles;
|
|
429
|
+
const data = await feishuPost('/open-apis/contact/v3/users/batch_get_id?user_id_type=open_id', body);
|
|
430
|
+
return {
|
|
431
|
+
email_users: data?.email_users || {},
|
|
432
|
+
mobile_users: data?.mobile_users || {},
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async function contactGetUser(userId: string) {
|
|
437
|
+
const data = await feishuGet(`/open-apis/contact/v3/users/${userId}`, { user_id_type: 'open_id' });
|
|
438
|
+
const u = data?.user;
|
|
439
|
+
return {
|
|
440
|
+
open_id: u?.open_id,
|
|
441
|
+
name: u?.name,
|
|
442
|
+
en_name: u?.en_name,
|
|
443
|
+
email: u?.email,
|
|
444
|
+
mobile: u?.mobile,
|
|
445
|
+
avatar: u?.avatar?.avatar_72,
|
|
446
|
+
department_ids: u?.department_ids,
|
|
447
|
+
status: u?.status,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ============================================================
|
|
452
|
+
// Message API
|
|
453
|
+
// ============================================================
|
|
454
|
+
|
|
455
|
+
async function messageSend(receiveId: string, receiveIdType: string, msgType: string, content: any) {
|
|
456
|
+
const data = await feishuPost(`/open-apis/im/v1/messages?receive_id_type=${receiveIdType}`, {
|
|
457
|
+
receive_id: receiveId,
|
|
458
|
+
msg_type: msgType,
|
|
459
|
+
content: typeof content === 'string' ? content : JSON.stringify(content),
|
|
460
|
+
});
|
|
461
|
+
return {
|
|
462
|
+
message_id: data?.message_id,
|
|
463
|
+
create_time: data?.create_time,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async function messageSendText(receiveId: string, receiveIdType: string, text: string) {
|
|
468
|
+
return messageSend(receiveId, receiveIdType, 'text', JSON.stringify({ text }));
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async function messageSendRichText(receiveId: string, receiveIdType: string, title: string, contentParts: any[][]) {
|
|
472
|
+
const post = { zh_cn: { title, content: contentParts } };
|
|
473
|
+
return messageSend(receiveId, receiveIdType, 'post', JSON.stringify(post));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async function messageSendBatch(receiveIds: string[], receiveIdType: string, msgType: string, content: any) {
|
|
477
|
+
const results: any[] = [];
|
|
478
|
+
for (const id of receiveIds) {
|
|
479
|
+
try {
|
|
480
|
+
const r = await messageSend(id, receiveIdType, msgType, content);
|
|
481
|
+
results.push({ receiveId: id, success: true, message_id: r.message_id });
|
|
482
|
+
} catch (e: any) {
|
|
483
|
+
results.push({ receiveId: id, success: false, error: e.message });
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return { sent: results.filter(r => r.success).length, failed: results.filter(r => !r.success).length, results };
|
|
487
|
+
}
|
|
488
|
+
|
|
385
489
|
// ============================================================
|
|
386
490
|
// App Scopes API
|
|
387
491
|
// ============================================================
|
|
@@ -447,10 +551,14 @@ export function registerFeishuTools(api: any, logger: any): boolean {
|
|
|
447
551
|
} catch {}
|
|
448
552
|
|
|
449
553
|
// ---- feishu_doc ----
|
|
554
|
+
const ownerHint = _owner?.openId
|
|
555
|
+
? ` Documents created via API are owned by the app bot. Owner (openid: ${_owner.openId}) is auto-granted full_access on create.`
|
|
556
|
+
: '';
|
|
557
|
+
|
|
450
558
|
api.registerTool({
|
|
451
559
|
name: 'feishu_doc',
|
|
452
560
|
label: 'Feishu Doc',
|
|
453
|
-
description:
|
|
561
|
+
description: `Feishu document operations. Actions: read, write, append, create, list_blocks, get_block, update_block, delete_block.${ownerHint}`,
|
|
454
562
|
parameters: {
|
|
455
563
|
type: 'object',
|
|
456
564
|
properties: {
|
|
@@ -540,10 +648,14 @@ export function registerFeishuTools(api: any, logger: any): boolean {
|
|
|
540
648
|
}, { name: 'feishu_drive' });
|
|
541
649
|
|
|
542
650
|
// ---- feishu_perm ----
|
|
651
|
+
const permOwnerHint = _owner?.openId
|
|
652
|
+
? ` Owner open_id: ${_owner.openId}`
|
|
653
|
+
: '';
|
|
654
|
+
|
|
543
655
|
api.registerTool({
|
|
544
656
|
name: 'feishu_perm',
|
|
545
657
|
label: 'Feishu Perm',
|
|
546
|
-
description:
|
|
658
|
+
description: `Feishu permission management. Actions: list, add, remove.${permOwnerHint}`,
|
|
547
659
|
parameters: {
|
|
548
660
|
type: 'object',
|
|
549
661
|
properties: {
|
|
@@ -568,6 +680,86 @@ export function registerFeishuTools(api: any, logger: any): boolean {
|
|
|
568
680
|
},
|
|
569
681
|
}, { name: 'feishu_perm' });
|
|
570
682
|
|
|
683
|
+
// ---- feishu_contact ----
|
|
684
|
+
api.registerTool({
|
|
685
|
+
name: 'feishu_contact',
|
|
686
|
+
label: 'Feishu Contact',
|
|
687
|
+
description: 'Feishu contact/user lookup. Actions: search (by name), batch_get_id (by emails/mobiles), get (by open_id). Use search to find a person\'s open_id by name.',
|
|
688
|
+
parameters: {
|
|
689
|
+
type: 'object',
|
|
690
|
+
properties: {
|
|
691
|
+
action: { type: 'string', enum: ['search', 'batch_get_id', 'get'], description: 'Action' },
|
|
692
|
+
query: { type: 'string', description: 'Search keyword (name) for search action' },
|
|
693
|
+
emails: { type: 'array', items: { type: 'string' }, description: 'Email list for batch_get_id' },
|
|
694
|
+
mobiles: { type: 'array', items: { type: 'string' }, description: 'Mobile list for batch_get_id' },
|
|
695
|
+
user_id: { type: 'string', description: 'User open_id for get action' },
|
|
696
|
+
},
|
|
697
|
+
required: ['action'],
|
|
698
|
+
},
|
|
699
|
+
async execute(_id: string, p: any) {
|
|
700
|
+
try {
|
|
701
|
+
switch (p.action) {
|
|
702
|
+
case 'search': return json(await contactSearch(p.query));
|
|
703
|
+
case 'batch_get_id': return json(await contactBatchGetId(p.emails, p.mobiles));
|
|
704
|
+
case 'get': return json(await contactGetUser(p.user_id));
|
|
705
|
+
default: return json({ error: `Unknown action: ${p.action}` });
|
|
706
|
+
}
|
|
707
|
+
} catch (err: any) { return json({ error: err.message }); }
|
|
708
|
+
},
|
|
709
|
+
}, { name: 'feishu_contact' });
|
|
710
|
+
|
|
711
|
+
// ---- feishu_message ----
|
|
712
|
+
api.registerTool({
|
|
713
|
+
name: 'feishu_message',
|
|
714
|
+
label: 'Feishu Message',
|
|
715
|
+
description: 'Send Feishu messages. Actions: send (single), send_batch (multiple). Supports text and rich text. Use feishu_contact to look up open_id first if needed.',
|
|
716
|
+
parameters: {
|
|
717
|
+
type: 'object',
|
|
718
|
+
properties: {
|
|
719
|
+
action: { type: 'string', enum: ['send', 'send_batch'], description: 'Action' },
|
|
720
|
+
receive_id: { type: 'string', description: 'Recipient ID (open_id or email)' },
|
|
721
|
+
receive_id_type: { type: 'string', enum: ['open_id', 'email', 'user_id', 'chat_id'], description: 'Recipient ID type (default: open_id)' },
|
|
722
|
+
receive_ids: { type: 'array', items: { type: 'string' }, description: 'Multiple recipient IDs for send_batch' },
|
|
723
|
+
msg_type: { type: 'string', enum: ['text', 'post'], description: 'Message type: text (plain) or post (rich text with links). Default: text' },
|
|
724
|
+
text: { type: 'string', description: 'Message text content (for text type)' },
|
|
725
|
+
title: { type: 'string', description: 'Rich text title (for post type)' },
|
|
726
|
+
content: {
|
|
727
|
+
type: 'array',
|
|
728
|
+
description: 'Rich text content blocks (for post type). Array of lines, each line is array of elements like {tag:"text",text:"..."} or {tag:"a",text:"click",href:"https://..."}',
|
|
729
|
+
items: { type: 'array', items: { type: 'object' } },
|
|
730
|
+
},
|
|
731
|
+
},
|
|
732
|
+
required: ['action'],
|
|
733
|
+
},
|
|
734
|
+
async execute(_id: string, p: any) {
|
|
735
|
+
try {
|
|
736
|
+
const idType = p.receive_id_type || 'open_id';
|
|
737
|
+
const msgType = p.msg_type || 'text';
|
|
738
|
+
|
|
739
|
+
if (p.action === 'send') {
|
|
740
|
+
if (msgType === 'text') {
|
|
741
|
+
return json(await messageSendText(p.receive_id, idType, p.text));
|
|
742
|
+
} else if (msgType === 'post') {
|
|
743
|
+
return json(await messageSendRichText(p.receive_id, idType, p.title || '', p.content || []));
|
|
744
|
+
} else {
|
|
745
|
+
return json(await messageSend(p.receive_id, idType, msgType, p.text || p.content));
|
|
746
|
+
}
|
|
747
|
+
} else if (p.action === 'send_batch') {
|
|
748
|
+
const ids = p.receive_ids || [];
|
|
749
|
+
if (msgType === 'text') {
|
|
750
|
+
const content = JSON.stringify({ text: p.text });
|
|
751
|
+
return json(await messageSendBatch(ids, idType, 'text', content));
|
|
752
|
+
} else if (msgType === 'post') {
|
|
753
|
+
const post = { zh_cn: { title: p.title || '', content: p.content || [] } };
|
|
754
|
+
return json(await messageSendBatch(ids, idType, 'post', JSON.stringify(post)));
|
|
755
|
+
}
|
|
756
|
+
return json({ error: 'Unsupported msg_type for batch' });
|
|
757
|
+
}
|
|
758
|
+
return json({ error: `Unknown action: ${p.action}` });
|
|
759
|
+
} catch (err: any) { return json({ error: err.message }); }
|
|
760
|
+
},
|
|
761
|
+
}, { name: 'feishu_message' });
|
|
762
|
+
|
|
571
763
|
// ---- feishu_app_scopes ----
|
|
572
764
|
api.registerTool({
|
|
573
765
|
name: 'feishu_app_scopes',
|
|
@@ -581,6 +773,6 @@ export function registerFeishuTools(api: any, logger: any): boolean {
|
|
|
581
773
|
}, { name: 'feishu_app_scopes' });
|
|
582
774
|
|
|
583
775
|
_registered = true;
|
|
584
|
-
logger.info('[feishu-tools] ✅ 已注册
|
|
776
|
+
logger.info('[feishu-tools] ✅ 已注册 7 个飞书工具(feishu_doc/wiki/drive/perm/contact/message/app_scopes)');
|
|
585
777
|
return true;
|
|
586
778
|
}
|
package/src/index.ts
CHANGED
|
@@ -17,7 +17,7 @@ import path from 'path';
|
|
|
17
17
|
import fs from 'fs';
|
|
18
18
|
import { HubClient } from './hub-client.js';
|
|
19
19
|
import { TaskStore, ChatStore, NodeEventStore } from './store.js';
|
|
20
|
-
import { setCredentials, registerFeishuTools, hasCredentials } from './feishu-tools.js';
|
|
20
|
+
import { setCredentials, setOwner, registerFeishuTools, hasCredentials } from './feishu-tools.js';
|
|
21
21
|
import type {
|
|
22
22
|
HubPluginConfig, DEFAULT_CONFIG, ResultPayload, WSMessage,
|
|
23
23
|
QueuedTask, ChatConfig, StoredTask, StoredChatMessage, StoredNodeEvent,
|
|
@@ -671,6 +671,9 @@ const plugin = {
|
|
|
671
671
|
// Hub 下发共享配置 → 注册飞书工具
|
|
672
672
|
client.onSharedConfig = (config: any) => {
|
|
673
673
|
api.logger.info(`[cluster-hub] 收到共享配置: ${JSON.stringify(Object.keys(config))}`);
|
|
674
|
+
if (config.owner) {
|
|
675
|
+
setOwner(config.owner);
|
|
676
|
+
}
|
|
674
677
|
if (config.feishu?.appId && config.feishu?.appSecret) {
|
|
675
678
|
setCredentials(config.feishu);
|
|
676
679
|
registerFeishuTools(api, api.logger);
|