@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hpplay-lebo/cluster-hub",
3
- "version": "3.0.0",
3
+ "version": "3.2.0",
4
4
  "description": "OpenClaw Hub cluster plugin — cross-network node collaboration, chat, and task orchestration",
5
5
  "author": "HPPlay",
6
6
  "license": "MIT",
@@ -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
- return {
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=false`, {
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: 'Feishu document operations. Actions: read, write, append, create, list_blocks, get_block, update_block, delete_block',
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: 'Feishu permission management. Actions: list, add, remove',
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] ✅ 已注册 5 个飞书工具(feishu_doc/wiki/drive/perm/app_scopes)');
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);