moltbot-channel-feishu 0.0.8
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/LICENSE +21 -0
- package/README.md +68 -0
- package/clawdbot.plugin.json +33 -0
- package/moltbot.plugin.json +33 -0
- package/package.json +86 -0
- package/src/api/client.ts +140 -0
- package/src/api/directory.ts +186 -0
- package/src/api/media.ts +335 -0
- package/src/api/messages.ts +290 -0
- package/src/api/reactions.ts +155 -0
- package/src/config/schema.ts +183 -0
- package/src/core/dispatcher.ts +227 -0
- package/src/core/gateway.ts +202 -0
- package/src/core/handler.ts +231 -0
- package/src/core/parser.ts +112 -0
- package/src/core/policy.ts +199 -0
- package/src/core/reply-dispatcher.ts +151 -0
- package/src/core/runtime.ts +27 -0
- package/src/index.ts +108 -0
- package/src/plugin/channel.ts +367 -0
- package/src/plugin/index.ts +28 -0
- package/src/plugin/onboarding.ts +378 -0
- package/src/types/clawdbot.d.ts +377 -0
- package/src/types/events.ts +72 -0
- package/src/types/index.ts +6 -0
- package/src/types/messages.ts +172 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 samzong
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# moltbot-channel-feishu
|
|
2
|
+
|
|
3
|
+
**Turn Feishu into your AI super-gateway.** A production-grade Feishu/Lark channel plugin for [Moltbot](https://molt.bot) — the brilliant AI agent framework.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# npm
|
|
9
|
+
moltbot plugin install moltbot-channel-feishu
|
|
10
|
+
|
|
11
|
+
# GitHub (for testing)
|
|
12
|
+
moltbot plugin install github:samzong/moltbot-channel-feishu
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Configure
|
|
16
|
+
|
|
17
|
+
Edit `~/.clawdbot/clawdbot.json`:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"channels": {
|
|
22
|
+
"feishu": {
|
|
23
|
+
"enabled": true,
|
|
24
|
+
"appId": "cli_xxx",
|
|
25
|
+
"appSecret": "xxx",
|
|
26
|
+
"domain": "feishu",
|
|
27
|
+
"dmPolicy": "pairing",
|
|
28
|
+
"groupPolicy": "open"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Or use environment variables (takes precedence if config values are empty):
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
export FEISHU_APP_ID="cli_xxx"
|
|
38
|
+
export FEISHU_APP_SECRET="xxx"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Configuration Options
|
|
42
|
+
|
|
43
|
+
| Field | Type | Default | Description |
|
|
44
|
+
|-------|------|---------|-------------|
|
|
45
|
+
| `enabled` | boolean | `false` | Enable/disable the channel |
|
|
46
|
+
| `appId` | string | - | Feishu App ID |
|
|
47
|
+
| `appSecret` | string | - | Feishu App Secret |
|
|
48
|
+
| `domain` | `"feishu"` \| `"lark"` | `"feishu"` | API domain (China / International) |
|
|
49
|
+
| `dmPolicy` | `"open"` \| `"pairing"` \| `"allowlist"` | `"pairing"` | DM access policy |
|
|
50
|
+
| `allowFrom` | string[] | `[]` | User IDs allowed for DM (when `dmPolicy: "allowlist"`) |
|
|
51
|
+
| `groupPolicy` | `"open"` \| `"allowlist"` \| `"disabled"` | `"allowlist"` | Group chat access policy |
|
|
52
|
+
| `groupAllowFrom` | string[] | `[]` | Group IDs allowed (when `groupPolicy: "allowlist"`) |
|
|
53
|
+
| `requireMention` | boolean | `true` | Require @mention in groups |
|
|
54
|
+
|
|
55
|
+
## Feishu App Setup
|
|
56
|
+
|
|
57
|
+
1. Go to [Feishu Open Platform](https://open.feishu.cn)
|
|
58
|
+
2. Create a self-built app
|
|
59
|
+
3. Enable permissions: `im:message`, `im:chat`, `contact:user.base:readonly`
|
|
60
|
+
4. Events → Use **Long Connection** mode
|
|
61
|
+
5. Subscribe to event: `im.message.receive_v1`
|
|
62
|
+
6. Get App ID and App Secret from **Credentials** page
|
|
63
|
+
7. Publish the app
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
[MIT](LICENSE)
|
|
68
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "feishu",
|
|
3
|
+
"name": "Feishu",
|
|
4
|
+
"description": "Feishu/Lark channel plugin for Moltbot",
|
|
5
|
+
"version": "0.0.8",
|
|
6
|
+
"channels": [
|
|
7
|
+
"feishu"
|
|
8
|
+
],
|
|
9
|
+
"configSchema": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"additionalProperties": false,
|
|
12
|
+
"properties": {
|
|
13
|
+
"appId": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"description": "Feishu App ID"
|
|
16
|
+
},
|
|
17
|
+
"appSecret": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"description": "Feishu App Secret"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"uiHints": {
|
|
24
|
+
"appId": {
|
|
25
|
+
"label": "App ID",
|
|
26
|
+
"placeholder": "cli_xxxxxxxx"
|
|
27
|
+
},
|
|
28
|
+
"appSecret": {
|
|
29
|
+
"label": "App Secret",
|
|
30
|
+
"sensitive": true
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "feishu",
|
|
3
|
+
"name": "Feishu",
|
|
4
|
+
"description": "Feishu/Lark channel plugin for Moltbot",
|
|
5
|
+
"version": "0.0.8",
|
|
6
|
+
"channels": [
|
|
7
|
+
"feishu"
|
|
8
|
+
],
|
|
9
|
+
"configSchema": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"additionalProperties": false,
|
|
12
|
+
"properties": {
|
|
13
|
+
"appId": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"description": "Feishu App ID"
|
|
16
|
+
},
|
|
17
|
+
"appSecret": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"description": "Feishu App Secret"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"uiHints": {
|
|
24
|
+
"appId": {
|
|
25
|
+
"label": "App ID",
|
|
26
|
+
"placeholder": "cli_xxxxxxxx"
|
|
27
|
+
},
|
|
28
|
+
"appSecret": {
|
|
29
|
+
"label": "App Secret",
|
|
30
|
+
"sensitive": true
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "moltbot-channel-feishu",
|
|
3
|
+
"version": "0.0.8",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Production-grade Feishu/Lark channel plugin for Clawdbot",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/samzong/moltbot-channel-feishu.git"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"clawdbot",
|
|
13
|
+
"feishu",
|
|
14
|
+
"lark",
|
|
15
|
+
"plugin",
|
|
16
|
+
"channel"
|
|
17
|
+
],
|
|
18
|
+
"main": "./src/index.ts",
|
|
19
|
+
"types": "./src/index.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": "./src/index.ts"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"src",
|
|
25
|
+
"moltbot.plugin.json",
|
|
26
|
+
"clawdbot.plugin.json"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc",
|
|
30
|
+
"dev": "tsc --watch",
|
|
31
|
+
"lint": "eslint src/",
|
|
32
|
+
"lint:fix": "eslint src/ --fix",
|
|
33
|
+
"format": "prettier --write src/",
|
|
34
|
+
"format:check": "prettier --check src/",
|
|
35
|
+
"typecheck": "tsc --noEmit",
|
|
36
|
+
"test": "npm run build && vitest run",
|
|
37
|
+
"test:watch": "vitest",
|
|
38
|
+
"clean": "rm -rf dist",
|
|
39
|
+
"prerelease": "npm run lint && npm run build && npm run test && node --check dist/index.js && echo '✅ All checks passed'",
|
|
40
|
+
"dev:local": "tsx scripts/local-test.ts",
|
|
41
|
+
"version": "node scripts/sync-version.js && git add moltbot.plugin.json clawdbot.plugin.json"
|
|
42
|
+
},
|
|
43
|
+
"clawdbot": {
|
|
44
|
+
"extensions": [
|
|
45
|
+
"./src/index.ts"
|
|
46
|
+
],
|
|
47
|
+
"channel": {
|
|
48
|
+
"id": "feishu",
|
|
49
|
+
"label": "Feishu",
|
|
50
|
+
"selectionLabel": "Feishu",
|
|
51
|
+
"docsPath": "/channels/feishu",
|
|
52
|
+
"docsLabel": "feishu",
|
|
53
|
+
"blurb": "Feishu enterprise messaging.",
|
|
54
|
+
"aliases": [
|
|
55
|
+
"lark"
|
|
56
|
+
],
|
|
57
|
+
"order": 70
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"@larksuiteoapi/node-sdk": "^1.30.0",
|
|
62
|
+
"zod": "^3.23.0"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@eslint/js": "^9.39.2",
|
|
66
|
+
"@types/node": "^22.0.0",
|
|
67
|
+
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
68
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
69
|
+
"@vitest/coverage-v8": "^2.1.9",
|
|
70
|
+
"clawdbot": "^2026.1.24",
|
|
71
|
+
"dotenv": "^17.2.3",
|
|
72
|
+
"eslint": "^9.0.0",
|
|
73
|
+
"prettier": "^3.8.1",
|
|
74
|
+
"tsx": "^4.21.0",
|
|
75
|
+
"typescript": "^5.7.0",
|
|
76
|
+
"typescript-eslint": "^8.54.0",
|
|
77
|
+
"vite-tsconfig-paths": "^6.0.5",
|
|
78
|
+
"vitest": "^2.0.0"
|
|
79
|
+
},
|
|
80
|
+
"peerDependencies": {
|
|
81
|
+
"clawdbot": ">=2026.1.24"
|
|
82
|
+
},
|
|
83
|
+
"engines": {
|
|
84
|
+
"node": ">=20.0.0"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feishu API client wrapper.
|
|
3
|
+
* Provides singleton access to the Lark SDK client with connection pooling.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as Lark from "@larksuiteoapi/node-sdk";
|
|
7
|
+
import type { Config, Credentials } from "../config/schema.js";
|
|
8
|
+
import { resolveCredentials } from "../config/schema.js";
|
|
9
|
+
import type { ProbeResult } from "../types/index.js";
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Client Cache (Singleton Pattern)
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
interface CachedClient {
|
|
16
|
+
client: Lark.Client;
|
|
17
|
+
credentials: Credentials;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let cachedClient: CachedClient | null = null;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolve Lark domain enum from config.
|
|
24
|
+
*/
|
|
25
|
+
function resolveDomain(domain: "feishu" | "lark"): Lark.Domain {
|
|
26
|
+
return domain === "lark" ? Lark.Domain.Lark : Lark.Domain.Feishu;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create or retrieve the Feishu API client.
|
|
31
|
+
* Uses singleton pattern with credential-based cache invalidation.
|
|
32
|
+
*
|
|
33
|
+
* @throws Error if credentials are not configured
|
|
34
|
+
*/
|
|
35
|
+
export function getApiClient(config: Config): Lark.Client {
|
|
36
|
+
const credentials = resolveCredentials(config);
|
|
37
|
+
if (!credentials) {
|
|
38
|
+
throw new Error("Feishu credentials not configured (appId, appSecret required)");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Return cached client if credentials match
|
|
42
|
+
if (
|
|
43
|
+
cachedClient &&
|
|
44
|
+
cachedClient.credentials.appId === credentials.appId &&
|
|
45
|
+
cachedClient.credentials.appSecret === credentials.appSecret &&
|
|
46
|
+
cachedClient.credentials.domain === credentials.domain
|
|
47
|
+
) {
|
|
48
|
+
return cachedClient.client;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Create new client
|
|
52
|
+
const client = new Lark.Client({
|
|
53
|
+
appId: credentials.appId,
|
|
54
|
+
appSecret: credentials.appSecret,
|
|
55
|
+
appType: Lark.AppType.SelfBuild,
|
|
56
|
+
domain: resolveDomain(credentials.domain),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
cachedClient = { client, credentials };
|
|
60
|
+
return client;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Create a WebSocket client for real-time events.
|
|
65
|
+
*
|
|
66
|
+
* @throws Error if credentials are not configured
|
|
67
|
+
*/
|
|
68
|
+
export function createWsClient(config: Config): Lark.WSClient {
|
|
69
|
+
const credentials = resolveCredentials(config);
|
|
70
|
+
if (!credentials) {
|
|
71
|
+
throw new Error("Feishu credentials not configured (appId, appSecret required)");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return new Lark.WSClient({
|
|
75
|
+
appId: credentials.appId,
|
|
76
|
+
appSecret: credentials.appSecret,
|
|
77
|
+
domain: resolveDomain(credentials.domain),
|
|
78
|
+
loggerLevel: Lark.LoggerLevel.info,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Clear the client cache.
|
|
86
|
+
* Useful for testing or when credentials change.
|
|
87
|
+
*/
|
|
88
|
+
export function clearClientCache(): void {
|
|
89
|
+
cachedClient = null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Probe the Feishu API to verify credentials and get bot info.
|
|
94
|
+
*/
|
|
95
|
+
export async function probeConnection(config: Config | undefined): Promise<ProbeResult> {
|
|
96
|
+
if (!config) {
|
|
97
|
+
return { ok: false, error: "Configuration not provided" };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const credentials = resolveCredentials(config);
|
|
101
|
+
if (!credentials) {
|
|
102
|
+
return { ok: false, error: "Credentials not configured" };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const client = getApiClient(config);
|
|
107
|
+
|
|
108
|
+
// Use bot info endpoint to verify credentials
|
|
109
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
110
|
+
const response = (await (client as any).bot.botInfo()) as {
|
|
111
|
+
code?: number;
|
|
112
|
+
msg?: string;
|
|
113
|
+
bot?: {
|
|
114
|
+
app_name?: string;
|
|
115
|
+
open_id?: string;
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
if (response.code !== 0) {
|
|
120
|
+
return {
|
|
121
|
+
ok: false,
|
|
122
|
+
error: response.msg ?? `API error code ${response.code}`,
|
|
123
|
+
appId: credentials.appId,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
ok: true,
|
|
129
|
+
appId: credentials.appId,
|
|
130
|
+
botName: response.bot?.app_name,
|
|
131
|
+
botOpenId: response.bot?.open_id,
|
|
132
|
+
};
|
|
133
|
+
} catch (err) {
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
error: err instanceof Error ? err.message : String(err),
|
|
137
|
+
appId: credentials.appId,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User and group directory operations.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Config } from "../config/schema.js";
|
|
6
|
+
import type { DirectoryUser, DirectoryGroup, ListDirectoryParams } from "../types/index.js";
|
|
7
|
+
import { getApiClient } from "./client.js";
|
|
8
|
+
import { resolveCredentials } from "../config/schema.js";
|
|
9
|
+
import { normalizeTarget } from "./messages.js";
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Static Directory (from config)
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* List users from config allowlists.
|
|
17
|
+
* Used as fallback when API access is unavailable.
|
|
18
|
+
*/
|
|
19
|
+
export function listUsersFromConfig(config: Config, params: ListDirectoryParams): DirectoryUser[] {
|
|
20
|
+
const query = params.query?.trim().toLowerCase() ?? "";
|
|
21
|
+
const ids = new Set<string>();
|
|
22
|
+
|
|
23
|
+
// Collect from DM allowlist
|
|
24
|
+
for (const entry of config.allowFrom ?? []) {
|
|
25
|
+
const trimmed = String(entry).trim();
|
|
26
|
+
if (trimmed && trimmed !== "*") ids.add(trimmed);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Collect from DM configs
|
|
30
|
+
for (const userId of Object.keys(config.dms ?? {})) {
|
|
31
|
+
const trimmed = userId.trim();
|
|
32
|
+
if (trimmed) ids.add(trimmed);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return Array.from(ids)
|
|
36
|
+
.map((raw) => normalizeTarget(raw) ?? raw)
|
|
37
|
+
.filter((id) => !query || id.toLowerCase().includes(query))
|
|
38
|
+
.slice(0, params.limit && params.limit > 0 ? params.limit : undefined)
|
|
39
|
+
.map((id) => ({ kind: "user" as const, id }));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* List groups from config allowlists.
|
|
44
|
+
* Used as fallback when API access is unavailable.
|
|
45
|
+
*/
|
|
46
|
+
export function listGroupsFromConfig(
|
|
47
|
+
config: Config,
|
|
48
|
+
params: ListDirectoryParams
|
|
49
|
+
): DirectoryGroup[] {
|
|
50
|
+
const query = params.query?.trim().toLowerCase() ?? "";
|
|
51
|
+
const ids = new Set<string>();
|
|
52
|
+
|
|
53
|
+
// Collect from group configs
|
|
54
|
+
for (const groupId of Object.keys(config.groups ?? {})) {
|
|
55
|
+
const trimmed = groupId.trim();
|
|
56
|
+
if (trimmed && trimmed !== "*") ids.add(trimmed);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Collect from group allowlist
|
|
60
|
+
for (const entry of config.groupAllowFrom ?? []) {
|
|
61
|
+
const trimmed = String(entry).trim();
|
|
62
|
+
if (trimmed && trimmed !== "*") ids.add(trimmed);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return Array.from(ids)
|
|
66
|
+
.filter((id) => !query || id.toLowerCase().includes(query))
|
|
67
|
+
.slice(0, params.limit && params.limit > 0 ? params.limit : undefined)
|
|
68
|
+
.map((id) => ({ kind: "group" as const, id }));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// Live Directory (from API)
|
|
73
|
+
// ============================================================================
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* List users from Feishu API.
|
|
77
|
+
* Falls back to config-based listing if API unavailable.
|
|
78
|
+
*/
|
|
79
|
+
export async function listUsers(
|
|
80
|
+
config: Config,
|
|
81
|
+
params: ListDirectoryParams
|
|
82
|
+
): Promise<DirectoryUser[]> {
|
|
83
|
+
const credentials = resolveCredentials(config);
|
|
84
|
+
if (!credentials) {
|
|
85
|
+
return listUsersFromConfig(config, params);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const client = getApiClient(config);
|
|
90
|
+
const limit = params.limit ?? 50;
|
|
91
|
+
const query = params.query?.trim().toLowerCase() ?? "";
|
|
92
|
+
|
|
93
|
+
const users: DirectoryUser[] = [];
|
|
94
|
+
|
|
95
|
+
// Use SDK's iterator for automatic pagination
|
|
96
|
+
const iterator = await client.contact.user.listWithIterator({
|
|
97
|
+
params: { page_size: Math.min(limit, 50) },
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
for await (const page of iterator) {
|
|
101
|
+
const items = page?.items;
|
|
102
|
+
if (!items) continue;
|
|
103
|
+
|
|
104
|
+
for (const user of items) {
|
|
105
|
+
if (!user.open_id) continue;
|
|
106
|
+
|
|
107
|
+
const name = user.name ?? "";
|
|
108
|
+
const matchesQuery =
|
|
109
|
+
!query || user.open_id.toLowerCase().includes(query) || name.toLowerCase().includes(query);
|
|
110
|
+
|
|
111
|
+
if (matchesQuery) {
|
|
112
|
+
users.push({
|
|
113
|
+
kind: "user",
|
|
114
|
+
id: user.open_id,
|
|
115
|
+
name: name || undefined,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (users.length >= limit) break;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (users.length >= limit) break;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return users;
|
|
126
|
+
} catch {
|
|
127
|
+
return listUsersFromConfig(config, params);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* List groups from Feishu API.
|
|
134
|
+
* Falls back to config-based listing if API unavailable.
|
|
135
|
+
*/
|
|
136
|
+
export async function listGroups(
|
|
137
|
+
config: Config,
|
|
138
|
+
params: ListDirectoryParams
|
|
139
|
+
): Promise<DirectoryGroup[]> {
|
|
140
|
+
const credentials = resolveCredentials(config);
|
|
141
|
+
if (!credentials) {
|
|
142
|
+
return listGroupsFromConfig(config, params);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const client = getApiClient(config);
|
|
147
|
+
const limit = params.limit ?? 50;
|
|
148
|
+
const query = params.query?.trim().toLowerCase() ?? "";
|
|
149
|
+
|
|
150
|
+
const groups: DirectoryGroup[] = [];
|
|
151
|
+
|
|
152
|
+
// Use SDK's iterator for automatic pagination
|
|
153
|
+
const iterator = await client.im.chat.listWithIterator({
|
|
154
|
+
params: { page_size: Math.min(limit, 100) },
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
for await (const page of iterator) {
|
|
158
|
+
const items = page?.items;
|
|
159
|
+
if (!items) continue;
|
|
160
|
+
|
|
161
|
+
for (const chat of items) {
|
|
162
|
+
if (!chat.chat_id) continue;
|
|
163
|
+
|
|
164
|
+
const name = chat.name ?? "";
|
|
165
|
+
const matchesQuery =
|
|
166
|
+
!query || chat.chat_id.toLowerCase().includes(query) || name.toLowerCase().includes(query);
|
|
167
|
+
|
|
168
|
+
if (matchesQuery) {
|
|
169
|
+
groups.push({
|
|
170
|
+
kind: "group",
|
|
171
|
+
id: chat.chat_id,
|
|
172
|
+
name: name || undefined,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (groups.length >= limit) break;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (groups.length >= limit) break;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return groups;
|
|
183
|
+
} catch {
|
|
184
|
+
return listGroupsFromConfig(config, params);
|
|
185
|
+
}
|
|
186
|
+
}
|