@larksuite/openclaw-lark 2026.3.15 → 2026.3.17-beta.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.
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execFileSync } from "node:child_process";
4
+ import { dirname, join } from "node:path";
5
+
6
+ // --tools-version <ver> lets the user pin a specific version
7
+ const args = process.argv.slice(2);
8
+ let version = "latest";
9
+
10
+ const vIdx = args.indexOf("--tools-version");
11
+ if (vIdx !== -1) {
12
+ version = args[vIdx + 1];
13
+ // Remove --tools-version <ver> from forwarded args
14
+ args.splice(vIdx, 2);
15
+ }
16
+
17
+ const allArgs = ["--yes", `@larksuite/openclaw-lark-tools@${version}`, ...args];
18
+
19
+ try {
20
+ if (process.platform === "win32") {
21
+ // On Windows, npx is a .cmd shim that can be broken or trigger
22
+ // DEP0190. Bypass it entirely: run node with the npx-cli.js
23
+ // script located next to the running node binary.
24
+ const npxCli = join(
25
+ dirname(process.execPath),
26
+ "node_modules",
27
+ "npm",
28
+ "bin",
29
+ "npx-cli.js",
30
+ );
31
+ execFileSync(process.execPath, [npxCli, ...allArgs], {
32
+ stdio: "inherit",
33
+ env: {
34
+ ...process.env,
35
+ NODE_OPTIONS: [
36
+ process.env.NODE_OPTIONS,
37
+ "--disable-warning=DEP0190",
38
+ ]
39
+ .filter(Boolean)
40
+ .join(" "),
41
+ },
42
+ });
43
+ } else {
44
+ execFileSync("npx", allArgs, { stdio: "inherit" });
45
+ }
46
+ } catch (error) {
47
+ process.exit(error.status ?? 1);
48
+ }
package/index.d.ts CHANGED
@@ -30,7 +30,7 @@ declare const plugin: {
30
30
  id: string;
31
31
  name: string;
32
32
  description: string;
33
- configSchema: any;
33
+ configSchema: import("openclaw/plugin-sdk").OpenClawPluginConfigSchema;
34
34
  register(api: OpenClawPluginApi): void;
35
35
  };
36
36
  export default plugin;
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@larksuite/openclaw-lark",
3
- "version": "2026.3.15",
3
+ "version": "2026.3.17-beta.0",
4
4
  "description": "OpenClaw Lark/Feishu channel plugin",
5
5
  "type": "module",
6
+ "bin": {
7
+ "openclaw-lark": "bin/openclaw-lark.js"
8
+ },
6
9
  "files": [
7
10
  "**/*"
8
11
  ],
@@ -13,7 +13,7 @@ import { getTicket } from '../core/lark-ticket';
13
13
  import { getLarkAccount } from '../core/accounts';
14
14
  import { LarkClient } from '../core/lark-client';
15
15
  import { getAppInfo, getAppGrantedScopes } from '../core/app-scope-checker';
16
- import { getStoredToken } from '../core/token-store';
16
+ import { getStoredToken, tokenStatus } from '../core/token-store';
17
17
  import { filterSensitiveScopes } from '../core/tool-scopes';
18
18
  import { assertOwnerAccessStrict, OwnerAccessDeniedError } from '../core/owner-policy';
19
19
  import { openPlatformDomain } from '../core/domains';
@@ -124,7 +124,8 @@ async function executeFeishuAuth(config) {
124
124
  return { kind: 'no_user_scopes' };
125
125
  }
126
126
  const existing = await getStoredToken(appId, senderOpenId);
127
- const grantedScopes = new Set(existing?.scope?.split(/\s+/).filter(Boolean) ?? []);
127
+ const tokenValid = existing && tokenStatus(existing) !== 'expired';
128
+ const grantedScopes = new Set(tokenValid ? (existing.scope?.split(/\s+/).filter(Boolean) ?? []) : []);
128
129
  const missingScopes = appScopes.filter((s) => !grantedScopes.has(s));
129
130
  if (missingScopes.length === 0) {
130
131
  return { kind: 'all_authorized', count: appScopes.length };
@@ -18,22 +18,24 @@ export declare const LARK_ERROR: {
18
18
  /** access_token 无效 */
19
19
  readonly TOKEN_INVALID: 99991668;
20
20
  /** access_token 已过期 */
21
- readonly TOKEN_EXPIRED: 99991669;
22
- /** refresh_token 无效 */
23
- readonly REFRESH_TOKEN_INVALID: 20003;
24
- /** refresh_token 已过期 */
25
- readonly REFRESH_TOKEN_EXPIRED: 20004;
26
- /** refresh_token 缺失 */
27
- readonly REFRESH_TOKEN_MISSING: 20024;
21
+ readonly TOKEN_EXPIRED: 99991677;
22
+ /** refresh_token 本身无效(格式非法或来自 v1 API) */
23
+ readonly REFRESH_TOKEN_INVALID: 20026;
24
+ /** refresh_token 已过期(超过 365 天) */
25
+ readonly REFRESH_TOKEN_EXPIRED: 20037;
28
26
  /** refresh_token 已被吊销 */
29
- readonly REFRESH_TOKEN_REVOKED: 20063;
27
+ readonly REFRESH_TOKEN_REVOKED: 20064;
28
+ /** refresh_token 已被使用(单次消费,rotation 场景) */
29
+ readonly REFRESH_TOKEN_ALREADY_USED: 20073;
30
+ /** refresh token 端点服务端内部错误,可重试 */
31
+ readonly REFRESH_SERVER_ERROR: 20050;
30
32
  /** 消息已被撤回 */
31
33
  readonly MESSAGE_RECALLED: 230011;
32
34
  /** 消息已被删除 */
33
35
  readonly MESSAGE_DELETED: 231003;
34
36
  };
35
- /** 不可恢复的 refresh_token 错误码集合,遇到后需要重新授权。 */
36
- export declare const REFRESH_TOKEN_IRRECOVERABLE: ReadonlySet<number>;
37
+ /** refresh token 端点可重试的错误码集合(服务端瞬时故障)。遇到后重试一次,仍失败则清 token。 */
38
+ export declare const REFRESH_TOKEN_RETRYABLE: ReadonlySet<number>;
37
39
  /** 消息终止错误码集合(撤回/删除),遇到后应停止对该消息的后续操作。 */
38
40
  export declare const MESSAGE_TERMINAL_CODES: ReadonlySet<number>;
39
41
  /** access_token 失效相关的错误码集合,遇到后可尝试刷新重试。 */
@@ -22,26 +22,25 @@ export const LARK_ERROR = {
22
22
  /** access_token 无效 */
23
23
  TOKEN_INVALID: 99991668,
24
24
  /** access_token 已过期 */
25
- TOKEN_EXPIRED: 99991669,
26
- /** refresh_token 无效 */
27
- REFRESH_TOKEN_INVALID: 20003,
28
- /** refresh_token 已过期 */
29
- REFRESH_TOKEN_EXPIRED: 20004,
30
- /** refresh_token 缺失 */
31
- REFRESH_TOKEN_MISSING: 20024,
25
+ TOKEN_EXPIRED: 99991677,
26
+ /** refresh_token 本身无效(格式非法或来自 v1 API) */
27
+ REFRESH_TOKEN_INVALID: 20026,
28
+ /** refresh_token 已过期(超过 365 天) */
29
+ REFRESH_TOKEN_EXPIRED: 20037,
32
30
  /** refresh_token 已被吊销 */
33
- REFRESH_TOKEN_REVOKED: 20063,
31
+ REFRESH_TOKEN_REVOKED: 20064,
32
+ /** refresh_token 已被使用(单次消费,rotation 场景) */
33
+ REFRESH_TOKEN_ALREADY_USED: 20073,
34
+ /** refresh token 端点服务端内部错误,可重试 */
35
+ REFRESH_SERVER_ERROR: 20050,
34
36
  /** 消息已被撤回 */
35
37
  MESSAGE_RECALLED: 230011,
36
38
  /** 消息已被删除 */
37
39
  MESSAGE_DELETED: 231003,
38
40
  };
39
- /** 不可恢复的 refresh_token 错误码集合,遇到后需要重新授权。 */
40
- export const REFRESH_TOKEN_IRRECOVERABLE = new Set([
41
- LARK_ERROR.REFRESH_TOKEN_INVALID,
42
- LARK_ERROR.REFRESH_TOKEN_EXPIRED,
43
- LARK_ERROR.REFRESH_TOKEN_MISSING,
44
- LARK_ERROR.REFRESH_TOKEN_REVOKED,
41
+ /** refresh token 端点可重试的错误码集合(服务端瞬时故障)。遇到后重试一次,仍失败则清 token。 */
42
+ export const REFRESH_TOKEN_RETRYABLE = new Set([
43
+ LARK_ERROR.REFRESH_SERVER_ERROR,
45
44
  ]);
46
45
  /** 消息终止错误码集合(撤回/删除),遇到后应停止对该消息的后续操作。 */
47
46
  export const MESSAGE_TERMINAL_CODES = new Set([
@@ -9,10 +9,435 @@
9
9
  */
10
10
  import { z } from 'zod';
11
11
  export { z };
12
- export declare const UATConfigSchema: any;
13
- export declare const FeishuGroupSchema: any;
14
- export declare const FeishuAccountConfigSchema: any;
15
- export declare const FeishuConfigSchema: any;
12
+ export declare const UATConfigSchema: z.ZodOptional<z.ZodObject<{
13
+ enabled: z.ZodOptional<z.ZodBoolean>;
14
+ allowedScopes: z.ZodOptional<z.ZodArray<z.ZodString>>;
15
+ blockedScopes: z.ZodOptional<z.ZodArray<z.ZodString>>;
16
+ }, z.core.$strip>>;
17
+ export declare const FeishuGroupSchema: z.ZodObject<{
18
+ groupPolicy: z.ZodOptional<z.ZodEnum<{
19
+ allowlist: "allowlist";
20
+ open: "open";
21
+ disabled: "disabled";
22
+ }>>;
23
+ requireMention: z.ZodOptional<z.ZodBoolean>;
24
+ tools: z.ZodOptional<z.ZodObject<{
25
+ allow: z.ZodOptional<z.ZodArray<z.ZodString>>;
26
+ deny: z.ZodOptional<z.ZodArray<z.ZodString>>;
27
+ }, z.core.$strip>>;
28
+ skills: z.ZodOptional<z.ZodArray<z.ZodString>>;
29
+ enabled: z.ZodOptional<z.ZodBoolean>;
30
+ allowFrom: z.ZodPipe<z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>, z.ZodTransform<string[] | undefined, string | string[] | undefined>>;
31
+ systemPrompt: z.ZodOptional<z.ZodString>;
32
+ }, z.core.$strip>;
33
+ export declare const FeishuAccountConfigSchema: z.ZodObject<{
34
+ appId: z.ZodOptional<z.ZodString>;
35
+ appSecret: z.ZodOptional<z.ZodString>;
36
+ encryptKey: z.ZodOptional<z.ZodString>;
37
+ verificationToken: z.ZodOptional<z.ZodString>;
38
+ name: z.ZodOptional<z.ZodString>;
39
+ enabled: z.ZodOptional<z.ZodBoolean>;
40
+ domain: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"feishu">, z.ZodLiteral<"lark">, z.ZodString]>>;
41
+ connectionMode: z.ZodOptional<z.ZodEnum<{
42
+ websocket: "websocket";
43
+ webhook: "webhook";
44
+ }>>;
45
+ webhookPath: z.ZodOptional<z.ZodString>;
46
+ webhookPort: z.ZodOptional<z.ZodNumber>;
47
+ dmPolicy: z.ZodOptional<z.ZodEnum<{
48
+ allowlist: "allowlist";
49
+ open: "open";
50
+ pairing: "pairing";
51
+ disabled: "disabled";
52
+ }>>;
53
+ allowFrom: z.ZodPipe<z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>, z.ZodTransform<string[] | undefined, string | string[] | undefined>>;
54
+ groupPolicy: z.ZodOptional<z.ZodEnum<{
55
+ allowlist: "allowlist";
56
+ open: "open";
57
+ disabled: "disabled";
58
+ }>>;
59
+ groupAllowFrom: z.ZodPipe<z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>, z.ZodTransform<string[] | undefined, string | string[] | undefined>>;
60
+ requireMention: z.ZodOptional<z.ZodBoolean>;
61
+ groups: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
62
+ groupPolicy: z.ZodOptional<z.ZodEnum<{
63
+ allowlist: "allowlist";
64
+ open: "open";
65
+ disabled: "disabled";
66
+ }>>;
67
+ requireMention: z.ZodOptional<z.ZodBoolean>;
68
+ tools: z.ZodOptional<z.ZodObject<{
69
+ allow: z.ZodOptional<z.ZodArray<z.ZodString>>;
70
+ deny: z.ZodOptional<z.ZodArray<z.ZodString>>;
71
+ }, z.core.$strip>>;
72
+ skills: z.ZodOptional<z.ZodArray<z.ZodString>>;
73
+ enabled: z.ZodOptional<z.ZodBoolean>;
74
+ allowFrom: z.ZodPipe<z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>, z.ZodTransform<string[] | undefined, string | string[] | undefined>>;
75
+ systemPrompt: z.ZodOptional<z.ZodString>;
76
+ }, z.core.$strip>>>;
77
+ historyLimit: z.ZodOptional<z.ZodNumber>;
78
+ dmHistoryLimit: z.ZodOptional<z.ZodNumber>;
79
+ dms: z.ZodOptional<z.ZodObject<{
80
+ historyLimit: z.ZodOptional<z.ZodNumber>;
81
+ }, z.core.$strip>>;
82
+ textChunkLimit: z.ZodOptional<z.ZodNumber>;
83
+ chunkMode: z.ZodOptional<z.ZodEnum<{
84
+ newline: "newline";
85
+ paragraph: "paragraph";
86
+ none: "none";
87
+ }>>;
88
+ blockStreamingCoalesce: z.ZodOptional<z.ZodObject<{
89
+ minChars: z.ZodOptional<z.ZodNumber>;
90
+ maxChars: z.ZodOptional<z.ZodNumber>;
91
+ idleMs: z.ZodOptional<z.ZodNumber>;
92
+ }, z.core.$strip>>;
93
+ mediaMaxMb: z.ZodOptional<z.ZodNumber>;
94
+ heartbeat: z.ZodOptional<z.ZodObject<{
95
+ every: z.ZodOptional<z.ZodString>;
96
+ activeHours: z.ZodOptional<z.ZodObject<{
97
+ start: z.ZodOptional<z.ZodString>;
98
+ end: z.ZodOptional<z.ZodString>;
99
+ timezone: z.ZodOptional<z.ZodString>;
100
+ }, z.core.$strip>>;
101
+ target: z.ZodOptional<z.ZodString>;
102
+ to: z.ZodOptional<z.ZodString>;
103
+ prompt: z.ZodOptional<z.ZodString>;
104
+ accountId: z.ZodOptional<z.ZodString>;
105
+ }, z.core.$strip>>;
106
+ replyMode: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
107
+ streaming: "streaming";
108
+ auto: "auto";
109
+ static: "static";
110
+ }>, z.ZodObject<{
111
+ default: z.ZodOptional<z.ZodEnum<{
112
+ streaming: "streaming";
113
+ auto: "auto";
114
+ static: "static";
115
+ }>>;
116
+ group: z.ZodOptional<z.ZodEnum<{
117
+ streaming: "streaming";
118
+ auto: "auto";
119
+ static: "static";
120
+ }>>;
121
+ direct: z.ZodOptional<z.ZodEnum<{
122
+ streaming: "streaming";
123
+ auto: "auto";
124
+ static: "static";
125
+ }>>;
126
+ }, z.core.$strip>]>>;
127
+ streaming: z.ZodOptional<z.ZodBoolean>;
128
+ blockStreaming: z.ZodOptional<z.ZodBoolean>;
129
+ tools: z.ZodOptional<z.ZodObject<{
130
+ doc: z.ZodOptional<z.ZodBoolean>;
131
+ wiki: z.ZodOptional<z.ZodBoolean>;
132
+ drive: z.ZodOptional<z.ZodBoolean>;
133
+ perm: z.ZodOptional<z.ZodBoolean>;
134
+ scopes: z.ZodOptional<z.ZodBoolean>;
135
+ }, z.core.$strip>>;
136
+ footer: z.ZodOptional<z.ZodObject<{
137
+ status: z.ZodOptional<z.ZodBoolean>;
138
+ elapsed: z.ZodOptional<z.ZodBoolean>;
139
+ }, z.core.$strip>>;
140
+ markdown: z.ZodOptional<z.ZodObject<{
141
+ tables: z.ZodOptional<z.ZodEnum<{
142
+ code: "code";
143
+ off: "off";
144
+ bullets: "bullets";
145
+ }>>;
146
+ }, z.core.$strip>>;
147
+ configWrites: z.ZodOptional<z.ZodBoolean>;
148
+ capabilities: z.ZodOptional<z.ZodObject<{
149
+ image: z.ZodOptional<z.ZodBoolean>;
150
+ audio: z.ZodOptional<z.ZodBoolean>;
151
+ video: z.ZodOptional<z.ZodBoolean>;
152
+ }, z.core.$strip>>;
153
+ dedup: z.ZodOptional<z.ZodObject<{
154
+ ttlMs: z.ZodOptional<z.ZodNumber>;
155
+ maxEntries: z.ZodOptional<z.ZodNumber>;
156
+ }, z.core.$strip>>;
157
+ reactionNotifications: z.ZodOptional<z.ZodEnum<{
158
+ all: "all";
159
+ off: "off";
160
+ own: "own";
161
+ }>>;
162
+ threadSession: z.ZodOptional<z.ZodBoolean>;
163
+ uat: z.ZodOptional<z.ZodObject<{
164
+ enabled: z.ZodOptional<z.ZodBoolean>;
165
+ allowedScopes: z.ZodOptional<z.ZodArray<z.ZodString>>;
166
+ blockedScopes: z.ZodOptional<z.ZodArray<z.ZodString>>;
167
+ }, z.core.$strip>>;
168
+ }, z.core.$strip>;
169
+ export declare const FeishuConfigSchema: z.ZodObject<{
170
+ appId: z.ZodOptional<z.ZodString>;
171
+ appSecret: z.ZodOptional<z.ZodString>;
172
+ encryptKey: z.ZodOptional<z.ZodString>;
173
+ verificationToken: z.ZodOptional<z.ZodString>;
174
+ name: z.ZodOptional<z.ZodString>;
175
+ enabled: z.ZodOptional<z.ZodBoolean>;
176
+ domain: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"feishu">, z.ZodLiteral<"lark">, z.ZodString]>>;
177
+ connectionMode: z.ZodOptional<z.ZodEnum<{
178
+ websocket: "websocket";
179
+ webhook: "webhook";
180
+ }>>;
181
+ webhookPath: z.ZodOptional<z.ZodString>;
182
+ webhookPort: z.ZodOptional<z.ZodNumber>;
183
+ dmPolicy: z.ZodOptional<z.ZodEnum<{
184
+ allowlist: "allowlist";
185
+ open: "open";
186
+ pairing: "pairing";
187
+ disabled: "disabled";
188
+ }>>;
189
+ allowFrom: z.ZodPipe<z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>, z.ZodTransform<string[] | undefined, string | string[] | undefined>>;
190
+ groupPolicy: z.ZodOptional<z.ZodEnum<{
191
+ allowlist: "allowlist";
192
+ open: "open";
193
+ disabled: "disabled";
194
+ }>>;
195
+ groupAllowFrom: z.ZodPipe<z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>, z.ZodTransform<string[] | undefined, string | string[] | undefined>>;
196
+ requireMention: z.ZodOptional<z.ZodBoolean>;
197
+ groups: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
198
+ groupPolicy: z.ZodOptional<z.ZodEnum<{
199
+ allowlist: "allowlist";
200
+ open: "open";
201
+ disabled: "disabled";
202
+ }>>;
203
+ requireMention: z.ZodOptional<z.ZodBoolean>;
204
+ tools: z.ZodOptional<z.ZodObject<{
205
+ allow: z.ZodOptional<z.ZodArray<z.ZodString>>;
206
+ deny: z.ZodOptional<z.ZodArray<z.ZodString>>;
207
+ }, z.core.$strip>>;
208
+ skills: z.ZodOptional<z.ZodArray<z.ZodString>>;
209
+ enabled: z.ZodOptional<z.ZodBoolean>;
210
+ allowFrom: z.ZodPipe<z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>, z.ZodTransform<string[] | undefined, string | string[] | undefined>>;
211
+ systemPrompt: z.ZodOptional<z.ZodString>;
212
+ }, z.core.$strip>>>;
213
+ historyLimit: z.ZodOptional<z.ZodNumber>;
214
+ dmHistoryLimit: z.ZodOptional<z.ZodNumber>;
215
+ dms: z.ZodOptional<z.ZodObject<{
216
+ historyLimit: z.ZodOptional<z.ZodNumber>;
217
+ }, z.core.$strip>>;
218
+ textChunkLimit: z.ZodOptional<z.ZodNumber>;
219
+ chunkMode: z.ZodOptional<z.ZodEnum<{
220
+ newline: "newline";
221
+ paragraph: "paragraph";
222
+ none: "none";
223
+ }>>;
224
+ blockStreamingCoalesce: z.ZodOptional<z.ZodObject<{
225
+ minChars: z.ZodOptional<z.ZodNumber>;
226
+ maxChars: z.ZodOptional<z.ZodNumber>;
227
+ idleMs: z.ZodOptional<z.ZodNumber>;
228
+ }, z.core.$strip>>;
229
+ mediaMaxMb: z.ZodOptional<z.ZodNumber>;
230
+ heartbeat: z.ZodOptional<z.ZodObject<{
231
+ every: z.ZodOptional<z.ZodString>;
232
+ activeHours: z.ZodOptional<z.ZodObject<{
233
+ start: z.ZodOptional<z.ZodString>;
234
+ end: z.ZodOptional<z.ZodString>;
235
+ timezone: z.ZodOptional<z.ZodString>;
236
+ }, z.core.$strip>>;
237
+ target: z.ZodOptional<z.ZodString>;
238
+ to: z.ZodOptional<z.ZodString>;
239
+ prompt: z.ZodOptional<z.ZodString>;
240
+ accountId: z.ZodOptional<z.ZodString>;
241
+ }, z.core.$strip>>;
242
+ replyMode: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
243
+ streaming: "streaming";
244
+ auto: "auto";
245
+ static: "static";
246
+ }>, z.ZodObject<{
247
+ default: z.ZodOptional<z.ZodEnum<{
248
+ streaming: "streaming";
249
+ auto: "auto";
250
+ static: "static";
251
+ }>>;
252
+ group: z.ZodOptional<z.ZodEnum<{
253
+ streaming: "streaming";
254
+ auto: "auto";
255
+ static: "static";
256
+ }>>;
257
+ direct: z.ZodOptional<z.ZodEnum<{
258
+ streaming: "streaming";
259
+ auto: "auto";
260
+ static: "static";
261
+ }>>;
262
+ }, z.core.$strip>]>>;
263
+ streaming: z.ZodOptional<z.ZodBoolean>;
264
+ blockStreaming: z.ZodOptional<z.ZodBoolean>;
265
+ tools: z.ZodOptional<z.ZodObject<{
266
+ doc: z.ZodOptional<z.ZodBoolean>;
267
+ wiki: z.ZodOptional<z.ZodBoolean>;
268
+ drive: z.ZodOptional<z.ZodBoolean>;
269
+ perm: z.ZodOptional<z.ZodBoolean>;
270
+ scopes: z.ZodOptional<z.ZodBoolean>;
271
+ }, z.core.$strip>>;
272
+ footer: z.ZodOptional<z.ZodObject<{
273
+ status: z.ZodOptional<z.ZodBoolean>;
274
+ elapsed: z.ZodOptional<z.ZodBoolean>;
275
+ }, z.core.$strip>>;
276
+ markdown: z.ZodOptional<z.ZodObject<{
277
+ tables: z.ZodOptional<z.ZodEnum<{
278
+ code: "code";
279
+ off: "off";
280
+ bullets: "bullets";
281
+ }>>;
282
+ }, z.core.$strip>>;
283
+ configWrites: z.ZodOptional<z.ZodBoolean>;
284
+ capabilities: z.ZodOptional<z.ZodObject<{
285
+ image: z.ZodOptional<z.ZodBoolean>;
286
+ audio: z.ZodOptional<z.ZodBoolean>;
287
+ video: z.ZodOptional<z.ZodBoolean>;
288
+ }, z.core.$strip>>;
289
+ dedup: z.ZodOptional<z.ZodObject<{
290
+ ttlMs: z.ZodOptional<z.ZodNumber>;
291
+ maxEntries: z.ZodOptional<z.ZodNumber>;
292
+ }, z.core.$strip>>;
293
+ reactionNotifications: z.ZodOptional<z.ZodEnum<{
294
+ all: "all";
295
+ off: "off";
296
+ own: "own";
297
+ }>>;
298
+ threadSession: z.ZodOptional<z.ZodBoolean>;
299
+ uat: z.ZodOptional<z.ZodObject<{
300
+ enabled: z.ZodOptional<z.ZodBoolean>;
301
+ allowedScopes: z.ZodOptional<z.ZodArray<z.ZodString>>;
302
+ blockedScopes: z.ZodOptional<z.ZodArray<z.ZodString>>;
303
+ }, z.core.$strip>>;
304
+ accounts: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
305
+ appId: z.ZodOptional<z.ZodString>;
306
+ appSecret: z.ZodOptional<z.ZodString>;
307
+ encryptKey: z.ZodOptional<z.ZodString>;
308
+ verificationToken: z.ZodOptional<z.ZodString>;
309
+ name: z.ZodOptional<z.ZodString>;
310
+ enabled: z.ZodOptional<z.ZodBoolean>;
311
+ domain: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"feishu">, z.ZodLiteral<"lark">, z.ZodString]>>;
312
+ connectionMode: z.ZodOptional<z.ZodEnum<{
313
+ websocket: "websocket";
314
+ webhook: "webhook";
315
+ }>>;
316
+ webhookPath: z.ZodOptional<z.ZodString>;
317
+ webhookPort: z.ZodOptional<z.ZodNumber>;
318
+ dmPolicy: z.ZodOptional<z.ZodEnum<{
319
+ allowlist: "allowlist";
320
+ open: "open";
321
+ pairing: "pairing";
322
+ disabled: "disabled";
323
+ }>>;
324
+ allowFrom: z.ZodPipe<z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>, z.ZodTransform<string[] | undefined, string | string[] | undefined>>;
325
+ groupPolicy: z.ZodOptional<z.ZodEnum<{
326
+ allowlist: "allowlist";
327
+ open: "open";
328
+ disabled: "disabled";
329
+ }>>;
330
+ groupAllowFrom: z.ZodPipe<z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>, z.ZodTransform<string[] | undefined, string | string[] | undefined>>;
331
+ requireMention: z.ZodOptional<z.ZodBoolean>;
332
+ groups: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
333
+ groupPolicy: z.ZodOptional<z.ZodEnum<{
334
+ allowlist: "allowlist";
335
+ open: "open";
336
+ disabled: "disabled";
337
+ }>>;
338
+ requireMention: z.ZodOptional<z.ZodBoolean>;
339
+ tools: z.ZodOptional<z.ZodObject<{
340
+ allow: z.ZodOptional<z.ZodArray<z.ZodString>>;
341
+ deny: z.ZodOptional<z.ZodArray<z.ZodString>>;
342
+ }, z.core.$strip>>;
343
+ skills: z.ZodOptional<z.ZodArray<z.ZodString>>;
344
+ enabled: z.ZodOptional<z.ZodBoolean>;
345
+ allowFrom: z.ZodPipe<z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>, z.ZodTransform<string[] | undefined, string | string[] | undefined>>;
346
+ systemPrompt: z.ZodOptional<z.ZodString>;
347
+ }, z.core.$strip>>>;
348
+ historyLimit: z.ZodOptional<z.ZodNumber>;
349
+ dmHistoryLimit: z.ZodOptional<z.ZodNumber>;
350
+ dms: z.ZodOptional<z.ZodObject<{
351
+ historyLimit: z.ZodOptional<z.ZodNumber>;
352
+ }, z.core.$strip>>;
353
+ textChunkLimit: z.ZodOptional<z.ZodNumber>;
354
+ chunkMode: z.ZodOptional<z.ZodEnum<{
355
+ newline: "newline";
356
+ paragraph: "paragraph";
357
+ none: "none";
358
+ }>>;
359
+ blockStreamingCoalesce: z.ZodOptional<z.ZodObject<{
360
+ minChars: z.ZodOptional<z.ZodNumber>;
361
+ maxChars: z.ZodOptional<z.ZodNumber>;
362
+ idleMs: z.ZodOptional<z.ZodNumber>;
363
+ }, z.core.$strip>>;
364
+ mediaMaxMb: z.ZodOptional<z.ZodNumber>;
365
+ heartbeat: z.ZodOptional<z.ZodObject<{
366
+ every: z.ZodOptional<z.ZodString>;
367
+ activeHours: z.ZodOptional<z.ZodObject<{
368
+ start: z.ZodOptional<z.ZodString>;
369
+ end: z.ZodOptional<z.ZodString>;
370
+ timezone: z.ZodOptional<z.ZodString>;
371
+ }, z.core.$strip>>;
372
+ target: z.ZodOptional<z.ZodString>;
373
+ to: z.ZodOptional<z.ZodString>;
374
+ prompt: z.ZodOptional<z.ZodString>;
375
+ accountId: z.ZodOptional<z.ZodString>;
376
+ }, z.core.$strip>>;
377
+ replyMode: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
378
+ streaming: "streaming";
379
+ auto: "auto";
380
+ static: "static";
381
+ }>, z.ZodObject<{
382
+ default: z.ZodOptional<z.ZodEnum<{
383
+ streaming: "streaming";
384
+ auto: "auto";
385
+ static: "static";
386
+ }>>;
387
+ group: z.ZodOptional<z.ZodEnum<{
388
+ streaming: "streaming";
389
+ auto: "auto";
390
+ static: "static";
391
+ }>>;
392
+ direct: z.ZodOptional<z.ZodEnum<{
393
+ streaming: "streaming";
394
+ auto: "auto";
395
+ static: "static";
396
+ }>>;
397
+ }, z.core.$strip>]>>;
398
+ streaming: z.ZodOptional<z.ZodBoolean>;
399
+ blockStreaming: z.ZodOptional<z.ZodBoolean>;
400
+ tools: z.ZodOptional<z.ZodObject<{
401
+ doc: z.ZodOptional<z.ZodBoolean>;
402
+ wiki: z.ZodOptional<z.ZodBoolean>;
403
+ drive: z.ZodOptional<z.ZodBoolean>;
404
+ perm: z.ZodOptional<z.ZodBoolean>;
405
+ scopes: z.ZodOptional<z.ZodBoolean>;
406
+ }, z.core.$strip>>;
407
+ footer: z.ZodOptional<z.ZodObject<{
408
+ status: z.ZodOptional<z.ZodBoolean>;
409
+ elapsed: z.ZodOptional<z.ZodBoolean>;
410
+ }, z.core.$strip>>;
411
+ markdown: z.ZodOptional<z.ZodObject<{
412
+ tables: z.ZodOptional<z.ZodEnum<{
413
+ code: "code";
414
+ off: "off";
415
+ bullets: "bullets";
416
+ }>>;
417
+ }, z.core.$strip>>;
418
+ configWrites: z.ZodOptional<z.ZodBoolean>;
419
+ capabilities: z.ZodOptional<z.ZodObject<{
420
+ image: z.ZodOptional<z.ZodBoolean>;
421
+ audio: z.ZodOptional<z.ZodBoolean>;
422
+ video: z.ZodOptional<z.ZodBoolean>;
423
+ }, z.core.$strip>>;
424
+ dedup: z.ZodOptional<z.ZodObject<{
425
+ ttlMs: z.ZodOptional<z.ZodNumber>;
426
+ maxEntries: z.ZodOptional<z.ZodNumber>;
427
+ }, z.core.$strip>>;
428
+ reactionNotifications: z.ZodOptional<z.ZodEnum<{
429
+ all: "all";
430
+ off: "off";
431
+ own: "own";
432
+ }>>;
433
+ threadSession: z.ZodOptional<z.ZodBoolean>;
434
+ uat: z.ZodOptional<z.ZodObject<{
435
+ enabled: z.ZodOptional<z.ZodBoolean>;
436
+ allowedScopes: z.ZodOptional<z.ZodArray<z.ZodString>>;
437
+ blockedScopes: z.ZodOptional<z.ZodArray<z.ZodString>>;
438
+ }, z.core.$strip>>;
439
+ }, z.core.$strip>>>;
440
+ }, z.core.$strip>;
16
441
  /**
17
442
  * JSON Schema derived from FeishuConfigSchema.
18
443
  *
@@ -53,7 +53,7 @@
53
53
  *
54
54
  * 总计:98 个工具动作
55
55
  */
56
- export type ToolActionKey = 'feishu_bitable_app.copy' | 'feishu_bitable_app.create' | 'feishu_bitable_app.get' | 'feishu_bitable_app.list' | 'feishu_bitable_app.patch' | 'feishu_bitable_app_table.batch_create' | 'feishu_bitable_app_table.batch_delete' | 'feishu_bitable_app_table.create' | 'feishu_bitable_app_table.delete' | 'feishu_bitable_app_table.list' | 'feishu_bitable_app_table.patch' | 'feishu_bitable_app_table_field.create' | 'feishu_bitable_app_table_field.delete' | 'feishu_bitable_app_table_field.list' | 'feishu_bitable_app_table_field.update' | 'feishu_bitable_app_table_record.batch_create' | 'feishu_bitable_app_table_record.batch_delete' | 'feishu_bitable_app_table_record.batch_update' | 'feishu_bitable_app_table_record.create' | 'feishu_bitable_app_table_record.delete' | 'feishu_bitable_app_table_record.list' | 'feishu_bitable_app_table_record.update' | 'feishu_bitable_app_table_view.create' | 'feishu_bitable_app_table_view.delete' | 'feishu_bitable_app_table_view.get' | 'feishu_bitable_app_table_view.list' | 'feishu_bitable_app_table_view.patch' | 'feishu_calendar_calendar.get' | 'feishu_calendar_calendar.list' | 'feishu_calendar_calendar.primary' | 'feishu_calendar_event.create' | 'feishu_calendar_event.delete' | 'feishu_calendar_event.get' | 'feishu_calendar_event.instance_view' | 'feishu_calendar_event.instances' | 'feishu_calendar_event.list' | 'feishu_calendar_event.patch' | 'feishu_calendar_event.reply' | 'feishu_calendar_event.search' | 'feishu_calendar_event_attendee.batch_delete' | 'feishu_calendar_event_attendee.create' | 'feishu_calendar_event_attendee.list' | 'feishu_calendar_freebusy.list' | 'feishu_chat.get' | 'feishu_chat.search' | 'feishu_chat_members.default' | 'feishu_create_doc.default' | 'feishu_doc_comments.create' | 'feishu_doc_comments.list' | 'feishu_doc_comments.patch' | 'feishu_doc_media.download' | 'feishu_doc_media.insert' | 'feishu_drive_file.copy' | 'feishu_drive_file.delete' | 'feishu_drive_file.download' | 'feishu_drive_file.get_meta' | 'feishu_drive_file.list' | 'feishu_drive_file.move' | 'feishu_drive_file.upload' | 'feishu_fetch_doc.default' | 'feishu_get_user.default' | 'feishu_im_user_fetch_resource.default' | 'feishu_im_user_get_messages.default' | 'feishu_im_user_message.reply' | 'feishu_im_user_message.send' | 'feishu_im_user_search_messages.default' | 'feishu_search_doc_wiki.search' | 'feishu_search_user.default' | 'feishu_task_comment.create' | 'feishu_task_comment.get' | 'feishu_task_comment.list' | 'feishu_task_subtask.create' | 'feishu_task_subtask.list' | 'feishu_task_task.create' | 'feishu_task_task.get' | 'feishu_task_task.list' | 'feishu_task_task.patch' | 'feishu_task_tasklist.add_members' | 'feishu_task_tasklist.create' | 'feishu_task_tasklist.delete' | 'feishu_task_tasklist.get' | 'feishu_task_tasklist.list' | 'feishu_task_tasklist.patch' | 'feishu_task_tasklist.remove_members' | 'feishu_task_tasklist.tasks' | 'feishu_update_doc.default' | 'feishu_wiki_space.create' | 'feishu_wiki_space.get' | 'feishu_wiki_space.list' | 'feishu_wiki_space_node.copy' | 'feishu_wiki_space_node.create' | 'feishu_wiki_space_node.get' | 'feishu_wiki_space_node.list' | 'feishu_wiki_space_node.move' | 'feishu_sheet.info' | 'feishu_sheet.read' | 'feishu_sheet.write' | 'feishu_sheet.append' | 'feishu_sheet.find' | 'feishu_sheet.create' | 'feishu_sheet.export';
56
+ export type ToolActionKey = 'feishu_bitable_app.copy' | 'feishu_bitable_app.create' | 'feishu_bitable_app.get' | 'feishu_bitable_app.list' | 'feishu_bitable_app.patch' | 'feishu_bitable_app_table.batch_create' | 'feishu_bitable_app_table.batch_delete' | 'feishu_bitable_app_table.create' | 'feishu_bitable_app_table.delete' | 'feishu_bitable_app_table.list' | 'feishu_bitable_app_table.patch' | 'feishu_bitable_app_table_field.create' | 'feishu_bitable_app_table_field.delete' | 'feishu_bitable_app_table_field.list' | 'feishu_bitable_app_table_field.update' | 'feishu_bitable_app_table_record.batch_create' | 'feishu_bitable_app_table_record.batch_delete' | 'feishu_bitable_app_table_record.batch_update' | 'feishu_bitable_app_table_record.create' | 'feishu_bitable_app_table_record.delete' | 'feishu_bitable_app_table_record.list' | 'feishu_bitable_app_table_record.update' | 'feishu_bitable_app_table_view.create' | 'feishu_bitable_app_table_view.delete' | 'feishu_bitable_app_table_view.get' | 'feishu_bitable_app_table_view.list' | 'feishu_bitable_app_table_view.patch' | 'feishu_calendar_calendar.get' | 'feishu_calendar_calendar.list' | 'feishu_calendar_calendar.primary' | 'feishu_calendar_event.create' | 'feishu_calendar_event.delete' | 'feishu_calendar_event.get' | 'feishu_calendar_event.instance_view' | 'feishu_calendar_event.instances' | 'feishu_calendar_event.list' | 'feishu_calendar_event.patch' | 'feishu_calendar_event.reply' | 'feishu_calendar_event.search' | 'feishu_calendar_event_attendee.batch_delete' | 'feishu_calendar_event_attendee.create' | 'feishu_calendar_event_attendee.list' | 'feishu_calendar_freebusy.list' | 'feishu_chat.get' | 'feishu_chat.search' | 'feishu_chat_members.default' | 'feishu_create_doc.default' | 'feishu_doc_comments.create' | 'feishu_doc_comments.list' | 'feishu_doc_comments.patch' | 'feishu_doc_media.download' | 'feishu_doc_media.insert' | 'feishu_drive_file.copy' | 'feishu_drive_file.delete' | 'feishu_drive_file.download' | 'feishu_drive_file.get_meta' | 'feishu_drive_file.list' | 'feishu_drive_file.move' | 'feishu_drive_file.upload' | 'feishu_fetch_doc.default' | 'feishu_get_user.basic_batch' | 'feishu_get_user.default' | 'feishu_im_user_fetch_resource.default' | 'feishu_im_user_get_messages.default' | 'feishu_im_user_message.reply' | 'feishu_im_user_message.send' | 'feishu_im_user_search_messages.default' | 'feishu_search_doc_wiki.search' | 'feishu_search_user.default' | 'feishu_task_comment.create' | 'feishu_task_comment.get' | 'feishu_task_comment.list' | 'feishu_task_subtask.create' | 'feishu_task_subtask.list' | 'feishu_task_task.create' | 'feishu_task_task.get' | 'feishu_task_task.list' | 'feishu_task_task.patch' | 'feishu_task_tasklist.add_members' | 'feishu_task_tasklist.create' | 'feishu_task_tasklist.delete' | 'feishu_task_tasklist.get' | 'feishu_task_tasklist.list' | 'feishu_task_tasklist.patch' | 'feishu_task_tasklist.remove_members' | 'feishu_task_tasklist.tasks' | 'feishu_update_doc.default' | 'feishu_wiki_space.create' | 'feishu_wiki_space.get' | 'feishu_wiki_space.list' | 'feishu_wiki_space_node.copy' | 'feishu_wiki_space_node.create' | 'feishu_wiki_space_node.get' | 'feishu_wiki_space_node.list' | 'feishu_wiki_space_node.move' | 'feishu_sheet.info' | 'feishu_sheet.read' | 'feishu_sheet.write' | 'feishu_sheet.append' | 'feishu_sheet.find' | 'feishu_sheet.create' | 'feishu_sheet.export';
57
57
  /**
58
58
  * Tool Scope 映射类型
59
59
  *
@@ -146,8 +146,8 @@ export type SensitiveScope = (typeof SENSITIVE_SCOPES)[number];
146
146
  */
147
147
  export declare function filterSensitiveScopes(scopes: string[]): string[];
148
148
  /**
149
- * 工具动作总数: 98
150
- * 唯一 scope 总数: 66
149
+ * 工具动作总数: 99
150
+ * 唯一 scope 总数: 67
151
151
  * 必需应用权限总数: 20
152
152
  * 高敏感权限总数: 1
153
153
  */
@@ -173,6 +173,7 @@ export const TOOL_SCOPES = {
173
173
  'search:message',
174
174
  ],
175
175
  'feishu_search_doc_wiki.search': ['search:docs:read'],
176
+ 'feishu_get_user.basic_batch': ['contact:user.basic_profile:readonly'],
176
177
  'feishu_get_user.default': ['contact:contact.base:readonly', 'contact:user.base:readonly'],
177
178
  'feishu_search_user.default': ['contact:user:search'],
178
179
  'feishu_create_doc.default': [
@@ -319,8 +320,8 @@ export function filterSensitiveScopes(scopes) {
319
320
  }
320
321
  // ===== 统计信息 =====
321
322
  /**
322
- * 工具动作总数: 98
323
- * 唯一 scope 总数: 66
323
+ * 工具动作总数: 99
324
+ * 唯一 scope 总数: 67
324
325
  * 必需应用权限总数: 20
325
326
  * 高敏感权限总数: 1
326
327
  */
@@ -14,7 +14,7 @@ import { resolveOAuthEndpoints } from './device-flow';
14
14
  import { larkLogger } from './lark-logger';
15
15
  const log = larkLogger('core/uat-client');
16
16
  import { feishuFetch } from './feishu-fetch';
17
- import { REFRESH_TOKEN_IRRECOVERABLE, TOKEN_RETRY_CODES, NeedAuthorizationError } from './auth-errors';
17
+ import { REFRESH_TOKEN_RETRYABLE, TOKEN_RETRY_CODES, NeedAuthorizationError } from './auth-errors';
18
18
  // Re-export for backward compatibility
19
19
  export { NeedAuthorizationError };
20
20
  // ---------------------------------------------------------------------------
@@ -39,31 +39,46 @@ async function doRefreshToken(opts, stored) {
39
39
  return null;
40
40
  }
41
41
  const endpoints = resolveOAuthEndpoints(opts.domain);
42
- const resp = await feishuFetch(endpoints.token, {
43
- method: 'POST',
44
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
45
- body: new URLSearchParams({
46
- grant_type: 'refresh_token',
47
- refresh_token: stored.refreshToken,
48
- client_id: opts.appId,
49
- client_secret: opts.appSecret,
50
- }).toString(),
51
- });
52
- const data = (await resp.json());
42
+ const requestBody = new URLSearchParams({
43
+ grant_type: 'refresh_token',
44
+ refresh_token: stored.refreshToken,
45
+ client_id: opts.appId,
46
+ client_secret: opts.appSecret,
47
+ }).toString();
48
+ const callEndpoint = async () => {
49
+ const resp = await feishuFetch(endpoints.token, {
50
+ method: 'POST',
51
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
52
+ body: requestBody,
53
+ });
54
+ return (await resp.json());
55
+ };
56
+ let data = await callEndpoint();
53
57
  // Feishu v2 token endpoint returns `code: 0` on success.
54
58
  // Some responses use `error` field instead (standard OAuth).
55
59
  const code = data.code;
56
60
  const error = data.error;
57
61
  if ((code !== undefined && code !== 0) || error) {
58
62
  const errCode = code ?? error;
59
- const errMsg = data.error_description ?? data.msg ?? 'unknown';
60
- // Known irrecoverable codes: invalid/expired/missing refresh_token
61
- if (REFRESH_TOKEN_IRRECOVERABLE.has(code)) {
63
+ // Transient server error: retry once, then clear.
64
+ if (REFRESH_TOKEN_RETRYABLE.has(code)) {
65
+ log.warn(`refresh transient error (code=${errCode}) for ${opts.userOpenId}, retrying once`);
66
+ data = await callEndpoint();
67
+ const retryCode = data.code;
68
+ const retryError = data.error;
69
+ if ((retryCode !== undefined && retryCode !== 0) || retryError) {
70
+ const retryErrCode = retryCode ?? retryError;
71
+ log.warn(`refresh failed after retry (code=${retryErrCode}), clearing token for ${opts.userOpenId}`);
72
+ await removeStoredToken(opts.appId, opts.userOpenId);
73
+ return null;
74
+ }
75
+ }
76
+ else {
77
+ // Any other error (invalid/expired/revoked token, or unknown): clear and force re-auth.
62
78
  log.warn(`refresh failed (code=${errCode}), clearing token for ${opts.userOpenId}`);
63
79
  await removeStoredToken(opts.appId, opts.userOpenId);
64
80
  return null;
65
81
  }
66
- throw new Error(`Token refresh failed (code=${errCode}): ${errMsg}`);
67
82
  }
68
83
  if (!data.access_token) {
69
84
  throw new Error('Token refresh returned no access_token');
@@ -75,11 +75,12 @@ export async function resolveReactionContext(params) {
75
75
  log(`feishu[${accountId}]: reacted message ${messageId} not found or timed out, skipping`);
76
76
  return null;
77
77
  }
78
- // The mget API returns app_id (cli_xxx) as sender.id for bot messages,
79
- // not the bot's open_id (ou_xxx). Match against the account's appId.
78
+ // mget API returns app_id (cli_xxx) as sender.id for bot messages.
80
79
  const isBotMessage = msg.senderType === 'app' && msg.senderId === account.appId;
81
- if (reactionMode === 'own' && !isBotMessage) {
82
- log(`feishu[${accountId}]: reaction on non-bot message ${messageId}, skipping (senderId=${msg.senderId}, senderType=${msg.senderType}, botOpenId=${botOpenId}, appId=${account.appId})`);
80
+ const isOtherBotMessage = msg.senderType === 'app' && account.appId && msg.senderId !== account.appId;
81
+ // 'own': only react to this bot's messages; 'all': also skip other bots' messages.
82
+ if ((reactionMode === 'own' && !isBotMessage) || (reactionMode === 'all' && isOtherBotMessage)) {
83
+ log(`feishu[${accountId}]: reaction on ${isOtherBotMessage ? 'other bot' : 'non-bot'} message ${messageId}, skipping`);
83
84
  return null;
84
85
  }
85
86
  // ---- Resolve effective chatId ----
@@ -179,4 +179,4 @@ import type { SchemaOptions } from '@sinclair/typebox';
179
179
  * 本函数生成 `{ type: 'string', enum: ['a', 'b'] }` 格式,
180
180
  * 兼容性更好。
181
181
  */
182
- export declare function StringEnum<T extends string>(values: T[], options?: SchemaOptions): any;
182
+ export declare function StringEnum<T extends string>(values: T[], options?: SchemaOptions): import("@sinclair/typebox").TUnsafe<T>;
@@ -140,7 +140,7 @@ function registerGetMessages(api) {
140
140
  as: 'user',
141
141
  });
142
142
  assertLarkOk(res);
143
- return formatAndReturn(res, config, log, client);
143
+ return await formatAndReturn(res, config, log, client);
144
144
  }
145
145
  catch (err) {
146
146
  return await handleInvokeErrorWithAutoAuth(err, config);
@@ -192,7 +192,7 @@ function registerGetThreadMessages(api) {
192
192
  as: 'user',
193
193
  });
194
194
  assertLarkOk(res);
195
- return formatAndReturn(res, config, log, client);
195
+ return await formatAndReturn(res, config, log, client);
196
196
  }
197
197
  catch (err) {
198
198
  return await handleInvokeErrorWithAutoAuth(err, config);
@@ -10,6 +10,9 @@
10
10
  * 设计动机:TAT 调用 contact/v3/users/batch 缺少权限导致返回的用户
11
11
  * 条目不含 name 字段,而工具层搜索消息等场景运行在 UAT 上下文中,
12
12
  * 用户 token 可以读取其他用户的名称。
13
+ *
14
+ * 底层使用 contact/v3/users/basic_batch 接口(scope: contact:user.basic_profile:readonly),
15
+ * 每次最多查询 10 个用户。
13
16
  */
14
17
  import type { ToolClient } from '../../../core/tool-client';
15
18
  /** 从 UAT 缓存中获取用户名 */
@@ -11,6 +11,9 @@
11
11
  * 设计动机:TAT 调用 contact/v3/users/batch 缺少权限导致返回的用户
12
12
  * 条目不含 name 字段,而工具层搜索消息等场景运行在 UAT 上下文中,
13
13
  * 用户 token 可以读取其他用户的名称。
14
+ *
15
+ * 底层使用 contact/v3/users/basic_batch 接口(scope: contact:user.basic_profile:readonly),
16
+ * 每次最多查询 10 个用户。
14
17
  */
15
18
  import { isInvokeError } from '../helpers';
16
19
  // ---------------------------------------------------------------------------
@@ -64,7 +67,7 @@ export function setUATUserNames(accountId, entries) {
64
67
  // ---------------------------------------------------------------------------
65
68
  // 以 UAT 身份批量解析用户名
66
69
  // ---------------------------------------------------------------------------
67
- const BATCH_SIZE = 50;
70
+ const BATCH_SIZE = 10; // basic_batch API 限制每次最多 10 个
68
71
  export async function batchResolveUserNamesAsUser(params) {
69
72
  const { client, openIds, log } = params;
70
73
  if (openIds.length === 0)
@@ -90,31 +93,41 @@ export async function batchResolveUserNamesAsUser(params) {
90
93
  const uniqueMissing = [...new Set(missing)];
91
94
  if (uniqueMissing.length === 0)
92
95
  return result;
93
- // 2. 分批通过 SDK 调用 contact/v3/users/batch(UAT)
96
+ // 2. 分批通过 SDK 调用 contact/v3/users/basic_batch(UAT)
97
+ const totalBatches = Math.ceil(uniqueMissing.length / BATCH_SIZE);
98
+ log(`batchResolveUserNamesAsUser: resolving ${uniqueMissing.length} user(s) in ${totalBatches} batch(es), ${result.size} cache hit(s)`);
94
99
  for (let i = 0; i < uniqueMissing.length; i += BATCH_SIZE) {
95
100
  const chunk = uniqueMissing.slice(i, i + BATCH_SIZE);
101
+ const batchIndex = Math.floor(i / BATCH_SIZE) + 1;
96
102
  try {
97
103
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
98
- const res = await client.invoke('feishu_get_user.default', // 注:实际调用 batch API,共享 get_user 的 scope
99
- (sdk, opts) => sdk.contact.user.batch({
100
- params: {
101
- user_ids: chunk,
102
- user_id_type: 'open_id',
103
- },
104
+ const res = await client.invoke('feishu_get_user.basic_batch', (sdk, opts) => sdk.request({
105
+ method: 'POST',
106
+ url: '/open-apis/contact/v3/users/basic_batch',
107
+ data: { user_ids: chunk },
108
+ params: { user_id_type: 'open_id' },
104
109
  }, opts), {
105
110
  as: 'user',
106
111
  });
107
112
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
108
- const items = res?.data?.items ?? [];
109
- for (const item of items) {
110
- const openId = item.open_id;
111
- const name = item.name || item.display_name || item.nickname || item.en_name;
113
+ const users = res?.data?.users ?? [];
114
+ let resolved = 0;
115
+ for (const user of users) {
116
+ const openId = user.user_id;
117
+ // 实际返回 name 为字符串,兼容文档中 name.value 的对象结构
118
+ const rawName = user.name;
119
+ const name = typeof rawName === 'string' ? rawName : rawName?.value;
112
120
  if (openId && name) {
113
121
  cache.delete(openId);
114
122
  cache.set(openId, { name, expireAt: Date.now() + UAT_TTL_MS });
115
123
  result.set(openId, name);
124
+ resolved++;
116
125
  }
117
126
  }
127
+ const unresolvedCount = chunk.length - resolved;
128
+ if (unresolvedCount > 0) {
129
+ log(`batchResolveUserNamesAsUser: batch ${batchIndex}/${totalBatches}: ${resolved} resolved, ${unresolvedCount} missing name`);
130
+ }
118
131
  }
119
132
  catch (err) {
120
133
  // 授权/权限错误向上冒泡,由上层 handleInvokeErrorWithAutoAuth 处理自动授权
@@ -41,13 +41,12 @@ const FeishuOAuthSchema = Type.Object({
41
41
  // Type.Literal("authorize"), // 已由 auto-auth 自动处理,不再对外暴露
42
42
  Type.Literal('revoke'),
43
43
  ], {
44
- description: 'revoke: 撤销当前用户的授权',
44
+ description: 'revoke: 撤销当前用户已保存的授权凭据',
45
45
  }),
46
46
  }, {
47
- description: '飞书用户授权管理工具。' +
48
- '【注意】授权流程由系统自动发起,不要主动调用此工具触发授权!' +
49
- '此工具仅用于撤销授权(revoke)。' +
50
- '不需要传入 user_open_id,系统自动识别当前用户。',
47
+ description: '飞书用户撤销授权工具。' +
48
+ '仅在用户明确说"撤销授权"、"取消授权"、"退出登录"、"清除授权"时调用。' +
49
+ '【严禁调用场景】用户说"重新授权"、"发起授权"、"重新发起"、"授权失败"、"授权过期"时,绝对不要调用此工具,授权流程由系统自动处理。',
51
50
  });
52
51
  const pendingFlows = new Map();
53
52
  // ---------------------------------------------------------------------------
@@ -96,11 +95,10 @@ export function registerFeishuOAuthTool(api) {
96
95
  registerTool(api, {
97
96
  name: 'feishu_oauth',
98
97
  label: 'Feishu OAuth',
99
- description: '飞书用户授权(OAuth)管理工具。' +
100
- '【注意】授权流程由系统自动发起,不要主动调用此工具触发授权!' +
101
- '此工具仅用于 revoke(撤销当前用户的授权)。' +
102
- '不需要传入 user_open_id,系统自动从消息上下文获取当前用户。' +
103
- '【Token 过期处理】当返回 token_expired 错误时,调用 revoke 撤销后,系统会自动重新发起授权流程。',
98
+ description: '飞书用户撤销授权工具。' +
99
+ '仅在用户明确说"撤销授权"、"取消授权"、"退出登录"、"清除授权"时调用 revoke。' +
100
+ '【严禁调用场景】用户说"重新授权"、"发起授权"、"重新发起"、"授权失败"、"授权过期"时,绝对不要调用此工具,授权流程由系统自动处理,无需人工干预。' +
101
+ '不需要传入 user_open_id,系统自动从消息上下文获取当前用户。',
104
102
  parameters: FeishuOAuthSchema,
105
103
  async execute(_toolCallId, params) {
106
104
  const p = params;