@openclaw/feishu 2026.2.25 → 2026.3.2
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/index.ts +2 -0
- package/package.json +2 -1
- package/skills/feishu-doc/SKILL.md +109 -3
- package/src/accounts.test.ts +161 -0
- package/src/accounts.ts +76 -8
- package/src/async.ts +62 -0
- package/src/bitable.ts +189 -215
- package/src/bot.card-action.test.ts +63 -0
- package/src/bot.checkBotMentioned.test.ts +56 -1
- package/src/bot.test.ts +1271 -56
- package/src/bot.ts +499 -215
- package/src/card-action.ts +79 -0
- package/src/channel.ts +26 -4
- package/src/chat-schema.ts +24 -0
- package/src/chat.test.ts +89 -0
- package/src/chat.ts +130 -0
- package/src/client.test.ts +121 -0
- package/src/client.ts +13 -0
- package/src/config-schema.test.ts +101 -1
- package/src/config-schema.ts +66 -11
- package/src/dedup.ts +47 -1
- package/src/doc-schema.ts +135 -0
- package/src/docx-batch-insert.ts +190 -0
- package/src/docx-color-text.ts +149 -0
- package/src/docx-table-ops.ts +298 -0
- package/src/docx.account-selection.test.ts +70 -0
- package/src/docx.test.ts +331 -9
- package/src/docx.ts +996 -72
- package/src/drive.ts +38 -33
- package/src/media.test.ts +227 -7
- package/src/media.ts +52 -11
- package/src/mention.ts +1 -1
- package/src/monitor.account.ts +534 -0
- package/src/monitor.reaction.test.ts +578 -0
- package/src/monitor.startup.test.ts +203 -0
- package/src/monitor.startup.ts +51 -0
- package/src/monitor.state.defaults.test.ts +46 -0
- package/src/monitor.state.ts +152 -0
- package/src/monitor.test-mocks.ts +12 -0
- package/src/monitor.transport.ts +163 -0
- package/src/monitor.ts +44 -346
- package/src/monitor.webhook-security.test.ts +53 -10
- package/src/onboarding.status.test.ts +25 -0
- package/src/onboarding.ts +144 -52
- package/src/outbound.test.ts +181 -0
- package/src/outbound.ts +94 -7
- package/src/perm.ts +37 -30
- package/src/policy.test.ts +56 -1
- package/src/policy.ts +5 -1
- package/src/post.test.ts +105 -0
- package/src/post.ts +274 -0
- package/src/probe.test.ts +271 -0
- package/src/probe.ts +131 -19
- package/src/reply-dispatcher.test.ts +300 -0
- package/src/reply-dispatcher.ts +159 -46
- package/src/secret-input.ts +19 -0
- package/src/send-target.test.ts +74 -0
- package/src/send-target.ts +6 -2
- package/src/send.reply-fallback.test.ts +105 -0
- package/src/send.test.ts +168 -0
- package/src/send.ts +143 -18
- package/src/streaming-card.ts +131 -43
- package/src/targets.test.ts +55 -1
- package/src/targets.ts +32 -7
- package/src/tool-account-routing.test.ts +129 -0
- package/src/tool-account.ts +70 -0
- package/src/tool-factory-test-harness.ts +76 -0
- package/src/tools-config.test.ts +21 -0
- package/src/tools-config.ts +2 -1
- package/src/types.ts +10 -1
- package/src/typing.test.ts +144 -0
- package/src/typing.ts +140 -10
- package/src/wiki.ts +55 -50
package/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
|
2
2
|
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
3
3
|
import { registerFeishuBitableTools } from "./src/bitable.js";
|
|
4
4
|
import { feishuPlugin } from "./src/channel.js";
|
|
5
|
+
import { registerFeishuChatTools } from "./src/chat.js";
|
|
5
6
|
import { registerFeishuDocTools } from "./src/docx.js";
|
|
6
7
|
import { registerFeishuDriveTools } from "./src/drive.js";
|
|
7
8
|
import { registerFeishuPermTools } from "./src/perm.js";
|
|
@@ -53,6 +54,7 @@ const plugin = {
|
|
|
53
54
|
setFeishuRuntime(api.runtime);
|
|
54
55
|
api.registerChannel({ plugin: feishuPlugin });
|
|
55
56
|
registerFeishuDocTools(api);
|
|
57
|
+
registerFeishuChatTools(api);
|
|
56
58
|
registerFeishuWikiTools(api);
|
|
57
59
|
registerFeishuDriveTools(api);
|
|
58
60
|
registerFeishuPermTools(api);
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/feishu",
|
|
3
|
-
"version": "2026.2
|
|
3
|
+
"version": "2026.3.2",
|
|
4
4
|
"description": "OpenClaw Feishu/Lark channel plugin (community maintained by @m1heng)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@larksuiteoapi/node-sdk": "^1.59.0",
|
|
8
8
|
"@sinclair/typebox": "0.34.48",
|
|
9
|
+
"https-proxy-agent": "^7.0.6",
|
|
9
10
|
"zod": "^4.3.6"
|
|
10
11
|
},
|
|
11
12
|
"openclaw": {
|
|
@@ -6,7 +6,7 @@ description: |
|
|
|
6
6
|
|
|
7
7
|
# Feishu Document Tool
|
|
8
8
|
|
|
9
|
-
Single tool `feishu_doc` with action parameter for all document operations.
|
|
9
|
+
Single tool `feishu_doc` with action parameter for all document operations, including table creation for Docx.
|
|
10
10
|
|
|
11
11
|
## Token Extraction
|
|
12
12
|
|
|
@@ -43,15 +43,22 @@ Appends markdown to end of document.
|
|
|
43
43
|
### Create Document
|
|
44
44
|
|
|
45
45
|
```json
|
|
46
|
-
{ "action": "create", "title": "New Document" }
|
|
46
|
+
{ "action": "create", "title": "New Document", "owner_open_id": "ou_xxx" }
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
With folder:
|
|
50
50
|
|
|
51
51
|
```json
|
|
52
|
-
{
|
|
52
|
+
{
|
|
53
|
+
"action": "create",
|
|
54
|
+
"title": "New Document",
|
|
55
|
+
"folder_token": "fldcnXXX",
|
|
56
|
+
"owner_open_id": "ou_xxx"
|
|
57
|
+
}
|
|
53
58
|
```
|
|
54
59
|
|
|
60
|
+
**Important:** Always pass `owner_open_id` with the requesting user's `open_id` (from inbound metadata `sender_id`) so the user automatically gets `full_access` permission on the created document. Without this, only the bot app has access.
|
|
61
|
+
|
|
55
62
|
### List Blocks
|
|
56
63
|
|
|
57
64
|
```json
|
|
@@ -83,6 +90,105 @@ Returns full block data including tables, images. Use this to read structured co
|
|
|
83
90
|
{ "action": "delete_block", "doc_token": "ABC123def", "block_id": "doxcnXXX" }
|
|
84
91
|
```
|
|
85
92
|
|
|
93
|
+
### Create Table (Docx Table Block)
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"action": "create_table",
|
|
98
|
+
"doc_token": "ABC123def",
|
|
99
|
+
"row_size": 2,
|
|
100
|
+
"column_size": 2,
|
|
101
|
+
"column_width": [200, 200]
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Optional: `parent_block_id` to insert under a specific block.
|
|
106
|
+
|
|
107
|
+
### Write Table Cells
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"action": "write_table_cells",
|
|
112
|
+
"doc_token": "ABC123def",
|
|
113
|
+
"table_block_id": "doxcnTABLE",
|
|
114
|
+
"values": [
|
|
115
|
+
["A1", "B1"],
|
|
116
|
+
["A2", "B2"]
|
|
117
|
+
]
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Create Table With Values (One-step)
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"action": "create_table_with_values",
|
|
126
|
+
"doc_token": "ABC123def",
|
|
127
|
+
"row_size": 2,
|
|
128
|
+
"column_size": 2,
|
|
129
|
+
"column_width": [200, 200],
|
|
130
|
+
"values": [
|
|
131
|
+
["A1", "B1"],
|
|
132
|
+
["A2", "B2"]
|
|
133
|
+
]
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Optional: `parent_block_id` to insert under a specific block.
|
|
138
|
+
|
|
139
|
+
### Upload Image to Docx (from URL or local file)
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
{
|
|
143
|
+
"action": "upload_image",
|
|
144
|
+
"doc_token": "ABC123def",
|
|
145
|
+
"url": "https://example.com/image.png"
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Or local path with position control:
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{
|
|
153
|
+
"action": "upload_image",
|
|
154
|
+
"doc_token": "ABC123def",
|
|
155
|
+
"file_path": "/tmp/image.png",
|
|
156
|
+
"parent_block_id": "doxcnParent",
|
|
157
|
+
"index": 5
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Optional `index` (0-based) inserts the image at a specific position among sibling blocks. Omit to append at end.
|
|
162
|
+
|
|
163
|
+
**Note:** Image display size is determined by the uploaded image's pixel dimensions. For small images (e.g. 480x270 GIFs), scale to 800px+ width before uploading to ensure proper display.
|
|
164
|
+
|
|
165
|
+
### Upload File Attachment to Docx (from URL or local file)
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{
|
|
169
|
+
"action": "upload_file",
|
|
170
|
+
"doc_token": "ABC123def",
|
|
171
|
+
"url": "https://example.com/report.pdf"
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Or local path:
|
|
176
|
+
|
|
177
|
+
```json
|
|
178
|
+
{
|
|
179
|
+
"action": "upload_file",
|
|
180
|
+
"doc_token": "ABC123def",
|
|
181
|
+
"file_path": "/tmp/report.pdf",
|
|
182
|
+
"filename": "Q1-report.pdf"
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Rules:
|
|
187
|
+
|
|
188
|
+
- exactly one of `url` / `file_path`
|
|
189
|
+
- optional `filename` override
|
|
190
|
+
- optional `parent_block_id`
|
|
191
|
+
|
|
86
192
|
## Reading Workflow
|
|
87
193
|
|
|
88
194
|
1. Start with `action: "read"` - get plain text + statistics
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
resolveDefaultFeishuAccountId,
|
|
4
|
+
resolveDefaultFeishuAccountSelection,
|
|
5
|
+
resolveFeishuAccount,
|
|
6
|
+
} from "./accounts.js";
|
|
7
|
+
|
|
8
|
+
describe("resolveDefaultFeishuAccountId", () => {
|
|
9
|
+
it("prefers channels.feishu.defaultAccount when configured", () => {
|
|
10
|
+
const cfg = {
|
|
11
|
+
channels: {
|
|
12
|
+
feishu: {
|
|
13
|
+
defaultAccount: "router-d",
|
|
14
|
+
accounts: {
|
|
15
|
+
default: { appId: "cli_default", appSecret: "secret_default" },
|
|
16
|
+
"router-d": { appId: "cli_router", appSecret: "secret_router" },
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
expect(resolveDefaultFeishuAccountId(cfg as never)).toBe("router-d");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("normalizes configured defaultAccount before lookup", () => {
|
|
26
|
+
const cfg = {
|
|
27
|
+
channels: {
|
|
28
|
+
feishu: {
|
|
29
|
+
defaultAccount: "Router D",
|
|
30
|
+
accounts: {
|
|
31
|
+
"router-d": { appId: "cli_router", appSecret: "secret_router" },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
expect(resolveDefaultFeishuAccountId(cfg as never)).toBe("router-d");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("keeps configured defaultAccount even when not present in accounts map", () => {
|
|
41
|
+
const cfg = {
|
|
42
|
+
channels: {
|
|
43
|
+
feishu: {
|
|
44
|
+
defaultAccount: "router-d",
|
|
45
|
+
accounts: {
|
|
46
|
+
default: { appId: "cli_default", appSecret: "secret_default" },
|
|
47
|
+
zeta: { appId: "cli_zeta", appSecret: "secret_zeta" },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
expect(resolveDefaultFeishuAccountId(cfg as never)).toBe("router-d");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("falls back to literal default account id when present", () => {
|
|
57
|
+
const cfg = {
|
|
58
|
+
channels: {
|
|
59
|
+
feishu: {
|
|
60
|
+
accounts: {
|
|
61
|
+
default: { appId: "cli_default", appSecret: "secret_default" },
|
|
62
|
+
zeta: { appId: "cli_zeta", appSecret: "secret_zeta" },
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
expect(resolveDefaultFeishuAccountId(cfg as never)).toBe("default");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("reports selection source for configured defaults and mapped defaults", () => {
|
|
72
|
+
const explicitDefaultCfg = {
|
|
73
|
+
channels: {
|
|
74
|
+
feishu: {
|
|
75
|
+
defaultAccount: "router-d",
|
|
76
|
+
accounts: {},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
expect(resolveDefaultFeishuAccountSelection(explicitDefaultCfg as never)).toEqual({
|
|
81
|
+
accountId: "router-d",
|
|
82
|
+
source: "explicit-default",
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const mappedDefaultCfg = {
|
|
86
|
+
channels: {
|
|
87
|
+
feishu: {
|
|
88
|
+
accounts: {
|
|
89
|
+
default: { appId: "cli_default", appSecret: "secret_default" },
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
expect(resolveDefaultFeishuAccountSelection(mappedDefaultCfg as never)).toEqual({
|
|
95
|
+
accountId: "default",
|
|
96
|
+
source: "mapped-default",
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("resolveFeishuAccount", () => {
|
|
102
|
+
it("uses top-level credentials with configured default account id even without account map entry", () => {
|
|
103
|
+
const cfg = {
|
|
104
|
+
channels: {
|
|
105
|
+
feishu: {
|
|
106
|
+
defaultAccount: "router-d",
|
|
107
|
+
appId: "top_level_app",
|
|
108
|
+
appSecret: "top_level_secret",
|
|
109
|
+
accounts: {
|
|
110
|
+
default: { appId: "cli_default", appSecret: "secret_default" },
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const account = resolveFeishuAccount({ cfg: cfg as never, accountId: undefined });
|
|
117
|
+
expect(account.accountId).toBe("router-d");
|
|
118
|
+
expect(account.selectionSource).toBe("explicit-default");
|
|
119
|
+
expect(account.configured).toBe(true);
|
|
120
|
+
expect(account.appId).toBe("top_level_app");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("uses configured default account when accountId is omitted", () => {
|
|
124
|
+
const cfg = {
|
|
125
|
+
channels: {
|
|
126
|
+
feishu: {
|
|
127
|
+
defaultAccount: "router-d",
|
|
128
|
+
accounts: {
|
|
129
|
+
default: { enabled: true },
|
|
130
|
+
"router-d": { appId: "cli_router", appSecret: "secret_router", enabled: true },
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const account = resolveFeishuAccount({ cfg: cfg as never, accountId: undefined });
|
|
137
|
+
expect(account.accountId).toBe("router-d");
|
|
138
|
+
expect(account.selectionSource).toBe("explicit-default");
|
|
139
|
+
expect(account.configured).toBe(true);
|
|
140
|
+
expect(account.appId).toBe("cli_router");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("keeps explicit accountId selection", () => {
|
|
144
|
+
const cfg = {
|
|
145
|
+
channels: {
|
|
146
|
+
feishu: {
|
|
147
|
+
defaultAccount: "router-d",
|
|
148
|
+
accounts: {
|
|
149
|
+
default: { appId: "cli_default", appSecret: "secret_default" },
|
|
150
|
+
"router-d": { appId: "cli_router", appSecret: "secret_router" },
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const account = resolveFeishuAccount({ cfg: cfg as never, accountId: "default" });
|
|
157
|
+
expect(account.accountId).toBe("default");
|
|
158
|
+
expect(account.selectionSource).toBe("explicit");
|
|
159
|
+
expect(account.appId).toBe("cli_default");
|
|
160
|
+
});
|
|
161
|
+
});
|
package/src/accounts.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
2
2
|
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
3
|
+
import { normalizeResolvedSecretInputString, normalizeSecretInputString } from "./secret-input.js";
|
|
3
4
|
import type {
|
|
4
5
|
FeishuConfig,
|
|
5
6
|
FeishuAccountConfig,
|
|
7
|
+
FeishuDefaultAccountSelectionSource,
|
|
6
8
|
FeishuDomain,
|
|
7
9
|
ResolvedFeishuAccount,
|
|
8
10
|
} from "./types.js";
|
|
@@ -32,14 +34,38 @@ export function listFeishuAccountIds(cfg: ClawdbotConfig): string[] {
|
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
/**
|
|
35
|
-
* Resolve the default account
|
|
37
|
+
* Resolve the default account selection and its source.
|
|
36
38
|
*/
|
|
37
|
-
export function
|
|
39
|
+
export function resolveDefaultFeishuAccountSelection(cfg: ClawdbotConfig): {
|
|
40
|
+
accountId: string;
|
|
41
|
+
source: FeishuDefaultAccountSelectionSource;
|
|
42
|
+
} {
|
|
43
|
+
const preferredRaw = (cfg.channels?.feishu as FeishuConfig | undefined)?.defaultAccount?.trim();
|
|
44
|
+
const preferred = preferredRaw ? normalizeAccountId(preferredRaw) : undefined;
|
|
45
|
+
if (preferred) {
|
|
46
|
+
return {
|
|
47
|
+
accountId: preferred,
|
|
48
|
+
source: "explicit-default",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
38
51
|
const ids = listFeishuAccountIds(cfg);
|
|
39
52
|
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
|
40
|
-
return
|
|
53
|
+
return {
|
|
54
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
55
|
+
source: "mapped-default",
|
|
56
|
+
};
|
|
41
57
|
}
|
|
42
|
-
return
|
|
58
|
+
return {
|
|
59
|
+
accountId: ids[0] ?? DEFAULT_ACCOUNT_ID,
|
|
60
|
+
source: "fallback",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolve the default account ID.
|
|
66
|
+
*/
|
|
67
|
+
export function resolveDefaultFeishuAccountId(cfg: ClawdbotConfig): string {
|
|
68
|
+
return resolveDefaultFeishuAccountSelection(cfg).accountId;
|
|
43
69
|
}
|
|
44
70
|
|
|
45
71
|
/**
|
|
@@ -64,7 +90,7 @@ function mergeFeishuAccountConfig(cfg: ClawdbotConfig, accountId: string): Feish
|
|
|
64
90
|
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
65
91
|
|
|
66
92
|
// Extract base config (exclude accounts field to avoid recursion)
|
|
67
|
-
const { accounts: _ignored, ...base } = feishuCfg ?? {};
|
|
93
|
+
const { accounts: _ignored, defaultAccount: _ignoredDefaultAccount, ...base } = feishuCfg ?? {};
|
|
68
94
|
|
|
69
95
|
// Get account-specific overrides
|
|
70
96
|
const account = resolveAccountConfig(cfg, accountId) ?? {};
|
|
@@ -82,9 +108,34 @@ export function resolveFeishuCredentials(cfg?: FeishuConfig): {
|
|
|
82
108
|
encryptKey?: string;
|
|
83
109
|
verificationToken?: string;
|
|
84
110
|
domain: FeishuDomain;
|
|
111
|
+
} | null;
|
|
112
|
+
export function resolveFeishuCredentials(
|
|
113
|
+
cfg: FeishuConfig | undefined,
|
|
114
|
+
options: { allowUnresolvedSecretRef?: boolean },
|
|
115
|
+
): {
|
|
116
|
+
appId: string;
|
|
117
|
+
appSecret: string;
|
|
118
|
+
encryptKey?: string;
|
|
119
|
+
verificationToken?: string;
|
|
120
|
+
domain: FeishuDomain;
|
|
121
|
+
} | null;
|
|
122
|
+
export function resolveFeishuCredentials(
|
|
123
|
+
cfg?: FeishuConfig,
|
|
124
|
+
options?: { allowUnresolvedSecretRef?: boolean },
|
|
125
|
+
): {
|
|
126
|
+
appId: string;
|
|
127
|
+
appSecret: string;
|
|
128
|
+
encryptKey?: string;
|
|
129
|
+
verificationToken?: string;
|
|
130
|
+
domain: FeishuDomain;
|
|
85
131
|
} | null {
|
|
86
132
|
const appId = cfg?.appId?.trim();
|
|
87
|
-
const appSecret =
|
|
133
|
+
const appSecret = options?.allowUnresolvedSecretRef
|
|
134
|
+
? normalizeSecretInputString(cfg?.appSecret)
|
|
135
|
+
: normalizeResolvedSecretInputString({
|
|
136
|
+
value: cfg?.appSecret,
|
|
137
|
+
path: "channels.feishu.appSecret",
|
|
138
|
+
});
|
|
88
139
|
if (!appId || !appSecret) {
|
|
89
140
|
return null;
|
|
90
141
|
}
|
|
@@ -92,7 +143,13 @@ export function resolveFeishuCredentials(cfg?: FeishuConfig): {
|
|
|
92
143
|
appId,
|
|
93
144
|
appSecret,
|
|
94
145
|
encryptKey: cfg?.encryptKey?.trim() || undefined,
|
|
95
|
-
verificationToken:
|
|
146
|
+
verificationToken:
|
|
147
|
+
(options?.allowUnresolvedSecretRef
|
|
148
|
+
? normalizeSecretInputString(cfg?.verificationToken)
|
|
149
|
+
: normalizeResolvedSecretInputString({
|
|
150
|
+
value: cfg?.verificationToken,
|
|
151
|
+
path: "channels.feishu.verificationToken",
|
|
152
|
+
})) || undefined,
|
|
96
153
|
domain: cfg?.domain ?? "feishu",
|
|
97
154
|
};
|
|
98
155
|
}
|
|
@@ -104,7 +161,17 @@ export function resolveFeishuAccount(params: {
|
|
|
104
161
|
cfg: ClawdbotConfig;
|
|
105
162
|
accountId?: string | null;
|
|
106
163
|
}): ResolvedFeishuAccount {
|
|
107
|
-
const
|
|
164
|
+
const hasExplicitAccountId =
|
|
165
|
+
typeof params.accountId === "string" && params.accountId.trim() !== "";
|
|
166
|
+
const defaultSelection = hasExplicitAccountId
|
|
167
|
+
? null
|
|
168
|
+
: resolveDefaultFeishuAccountSelection(params.cfg);
|
|
169
|
+
const accountId = hasExplicitAccountId
|
|
170
|
+
? normalizeAccountId(params.accountId)
|
|
171
|
+
: (defaultSelection?.accountId ?? DEFAULT_ACCOUNT_ID);
|
|
172
|
+
const selectionSource = hasExplicitAccountId
|
|
173
|
+
? "explicit"
|
|
174
|
+
: (defaultSelection?.source ?? "fallback");
|
|
108
175
|
const feishuCfg = params.cfg.channels?.feishu as FeishuConfig | undefined;
|
|
109
176
|
|
|
110
177
|
// Base enabled state (top-level)
|
|
@@ -122,6 +189,7 @@ export function resolveFeishuAccount(params: {
|
|
|
122
189
|
|
|
123
190
|
return {
|
|
124
191
|
accountId,
|
|
192
|
+
selectionSource,
|
|
125
193
|
enabled,
|
|
126
194
|
configured: Boolean(creds),
|
|
127
195
|
name: (merged as FeishuAccountConfig).name?.trim() || undefined,
|
package/src/async.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const RACE_TIMEOUT = Symbol("race-timeout");
|
|
2
|
+
const RACE_ABORT = Symbol("race-abort");
|
|
3
|
+
|
|
4
|
+
export type RaceWithTimeoutAndAbortResult<T> =
|
|
5
|
+
| { status: "resolved"; value: T }
|
|
6
|
+
| { status: "timeout" }
|
|
7
|
+
| { status: "aborted" };
|
|
8
|
+
|
|
9
|
+
export async function raceWithTimeoutAndAbort<T>(
|
|
10
|
+
promise: Promise<T>,
|
|
11
|
+
options: {
|
|
12
|
+
timeoutMs?: number;
|
|
13
|
+
abortSignal?: AbortSignal;
|
|
14
|
+
} = {},
|
|
15
|
+
): Promise<RaceWithTimeoutAndAbortResult<T>> {
|
|
16
|
+
if (options.abortSignal?.aborted) {
|
|
17
|
+
return { status: "aborted" };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (options.timeoutMs === undefined && !options.abortSignal) {
|
|
21
|
+
return { status: "resolved", value: await promise };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
|
25
|
+
let abortHandler: (() => void) | undefined;
|
|
26
|
+
const contenders: Array<Promise<T | typeof RACE_TIMEOUT | typeof RACE_ABORT>> = [promise];
|
|
27
|
+
|
|
28
|
+
if (options.timeoutMs !== undefined) {
|
|
29
|
+
contenders.push(
|
|
30
|
+
new Promise((resolve) => {
|
|
31
|
+
timeoutHandle = setTimeout(() => resolve(RACE_TIMEOUT), options.timeoutMs);
|
|
32
|
+
}),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (options.abortSignal) {
|
|
37
|
+
contenders.push(
|
|
38
|
+
new Promise((resolve) => {
|
|
39
|
+
abortHandler = () => resolve(RACE_ABORT);
|
|
40
|
+
options.abortSignal?.addEventListener("abort", abortHandler, { once: true });
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const result = await Promise.race(contenders);
|
|
47
|
+
if (result === RACE_TIMEOUT) {
|
|
48
|
+
return { status: "timeout" };
|
|
49
|
+
}
|
|
50
|
+
if (result === RACE_ABORT) {
|
|
51
|
+
return { status: "aborted" };
|
|
52
|
+
}
|
|
53
|
+
return { status: "resolved", value: result };
|
|
54
|
+
} finally {
|
|
55
|
+
if (timeoutHandle) {
|
|
56
|
+
clearTimeout(timeoutHandle);
|
|
57
|
+
}
|
|
58
|
+
if (abortHandler) {
|
|
59
|
+
options.abortSignal?.removeEventListener("abort", abortHandler);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|