@nextclaw/channel-plugin-feishu 0.2.29-beta.0 → 0.2.29-beta.1

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.
Files changed (114) hide show
  1. package/dist/index.d.ts +23 -0
  2. package/dist/index.js +45 -0
  3. package/dist/src/accounts.js +141 -0
  4. package/dist/src/app-scope-checker.js +36 -0
  5. package/dist/src/async.js +34 -0
  6. package/dist/src/auth-errors.js +72 -0
  7. package/dist/src/bitable.js +495 -0
  8. package/dist/src/bot.d.ts +35 -0
  9. package/dist/src/bot.js +941 -0
  10. package/dist/src/calendar-calendar.js +54 -0
  11. package/dist/src/calendar-event-attendee.js +98 -0
  12. package/dist/src/calendar-event.js +193 -0
  13. package/dist/src/calendar-freebusy.js +40 -0
  14. package/dist/src/calendar-shared.js +23 -0
  15. package/dist/src/calendar.js +16 -0
  16. package/dist/src/card-action.js +49 -0
  17. package/dist/src/channel.d.ts +7 -0
  18. package/dist/src/channel.js +413 -0
  19. package/dist/src/chat-schema.js +25 -0
  20. package/dist/src/chat.js +87 -0
  21. package/dist/src/client.d.ts +16 -0
  22. package/dist/src/client.js +112 -0
  23. package/dist/src/config-schema.d.ts +357 -0
  24. package/dist/src/dedup.js +126 -0
  25. package/dist/src/device-flow.js +109 -0
  26. package/dist/src/directory.js +101 -0
  27. package/dist/src/doc-schema.js +148 -0
  28. package/dist/src/docx-batch-insert.js +104 -0
  29. package/dist/src/docx-color-text.js +80 -0
  30. package/dist/src/docx-table-ops.js +197 -0
  31. package/dist/src/docx.js +858 -0
  32. package/dist/src/domains.js +14 -0
  33. package/dist/src/drive-schema.js +41 -0
  34. package/dist/src/drive.js +126 -0
  35. package/dist/src/dynamic-agent.js +93 -0
  36. package/dist/src/external-keys.js +13 -0
  37. package/dist/src/feishu-fetch.js +12 -0
  38. package/dist/src/identity.js +92 -0
  39. package/dist/src/lark-ticket.js +11 -0
  40. package/dist/src/media.d.ts +75 -0
  41. package/dist/src/media.js +304 -0
  42. package/dist/src/mention.d.ts +52 -0
  43. package/dist/src/mention.js +82 -0
  44. package/dist/src/monitor.account.d.ts +1 -0
  45. package/dist/src/monitor.account.js +393 -0
  46. package/dist/src/monitor.d.ts +11 -0
  47. package/dist/src/monitor.js +58 -0
  48. package/dist/src/monitor.startup.js +24 -0
  49. package/dist/src/monitor.state.d.ts +1 -0
  50. package/dist/src/monitor.state.js +80 -0
  51. package/dist/src/monitor.transport.js +167 -0
  52. package/dist/src/nextclaw-sdk/account-id.js +15 -0
  53. package/dist/src/nextclaw-sdk/core-channel.js +150 -0
  54. package/dist/src/nextclaw-sdk/core-pairing.js +151 -0
  55. package/dist/src/nextclaw-sdk/dedupe.js +164 -0
  56. package/dist/src/nextclaw-sdk/feishu.d.ts +1 -0
  57. package/dist/src/nextclaw-sdk/feishu.js +14 -0
  58. package/dist/src/nextclaw-sdk/history.js +69 -0
  59. package/dist/src/nextclaw-sdk/network-body.js +180 -0
  60. package/dist/src/nextclaw-sdk/network-fetch.js +63 -0
  61. package/dist/src/nextclaw-sdk/network-webhook.js +126 -0
  62. package/dist/src/nextclaw-sdk/network.js +4 -0
  63. package/dist/src/nextclaw-sdk/runtime-store.js +21 -0
  64. package/dist/src/nextclaw-sdk/secrets-config.js +65 -0
  65. package/dist/src/nextclaw-sdk/secrets-core.d.ts +1 -0
  66. package/dist/src/nextclaw-sdk/secrets-core.js +68 -0
  67. package/dist/src/nextclaw-sdk/secrets-prompt.js +193 -0
  68. package/dist/src/nextclaw-sdk/secrets.d.ts +1 -0
  69. package/dist/src/nextclaw-sdk/secrets.js +4 -0
  70. package/dist/src/nextclaw-sdk/types.d.ts +242 -0
  71. package/dist/src/oauth.js +171 -0
  72. package/dist/src/onboarding.js +381 -0
  73. package/dist/src/outbound.js +150 -0
  74. package/dist/src/perm-schema.js +49 -0
  75. package/dist/src/perm.js +90 -0
  76. package/dist/src/policy.js +61 -0
  77. package/dist/src/post.js +160 -0
  78. package/dist/src/probe.d.ts +11 -0
  79. package/dist/src/probe.js +85 -0
  80. package/dist/src/raw-request.js +24 -0
  81. package/dist/src/reactions.d.ts +67 -0
  82. package/dist/src/reactions.js +91 -0
  83. package/dist/src/reply-dispatcher.js +250 -0
  84. package/dist/src/runtime.js +5 -0
  85. package/dist/src/secret-input.js +3 -0
  86. package/dist/src/send-result.js +12 -0
  87. package/dist/src/send-target.js +22 -0
  88. package/dist/src/send.d.ts +51 -0
  89. package/dist/src/send.js +265 -0
  90. package/dist/src/sheets-shared.js +193 -0
  91. package/dist/src/sheets.js +95 -0
  92. package/dist/src/streaming-card.js +263 -0
  93. package/dist/src/targets.js +39 -0
  94. package/dist/src/task-comment.js +76 -0
  95. package/dist/src/task-shared.js +13 -0
  96. package/dist/src/task-subtask.js +79 -0
  97. package/dist/src/task-task.js +144 -0
  98. package/dist/src/task-tasklist.js +136 -0
  99. package/dist/src/task.js +16 -0
  100. package/dist/src/token-store.js +154 -0
  101. package/dist/src/tool-account.js +65 -0
  102. package/dist/src/tool-result.js +18 -0
  103. package/dist/src/tool-scopes.js +62 -0
  104. package/dist/src/tools-config.js +30 -0
  105. package/dist/src/types.d.ts +43 -0
  106. package/dist/src/typing.js +145 -0
  107. package/dist/src/uat-client.js +102 -0
  108. package/dist/src/user-tool-client.js +132 -0
  109. package/dist/src/user-tool-helpers.js +110 -0
  110. package/dist/src/user-tool-result.js +10 -0
  111. package/dist/src/wiki-schema.js +45 -0
  112. package/dist/src/wiki.js +144 -0
  113. package/package.json +8 -4
  114. package/index.ts +0 -75
@@ -0,0 +1,30 @@
1
+ //#region src/tools-config.ts
2
+ /**
3
+ * Default tool configuration.
4
+ * - doc, chat, wiki, drive, scopes: enabled by default
5
+ * - perm: disabled by default (sensitive operation)
6
+ */
7
+ const DEFAULT_TOOLS_CONFIG = {
8
+ doc: true,
9
+ chat: true,
10
+ wiki: true,
11
+ drive: true,
12
+ perm: false,
13
+ scopes: true,
14
+ calendar: true,
15
+ task: true,
16
+ sheets: true,
17
+ oauth: true,
18
+ identity: true
19
+ };
20
+ /**
21
+ * Resolve tools config with defaults.
22
+ */
23
+ function resolveToolsConfig(cfg) {
24
+ return {
25
+ ...DEFAULT_TOOLS_CONFIG,
26
+ ...cfg
27
+ };
28
+ }
29
+ //#endregion
30
+ export { resolveToolsConfig };
@@ -0,0 +1,43 @@
1
+ import { BaseProbeResult } from "./nextclaw-sdk/types.js";
2
+ import { FeishuConfigSchema, z } from "./config-schema.js";
3
+ //#region src/types.d.ts
4
+ type FeishuConfig = z.infer<typeof FeishuConfigSchema>;
5
+ type FeishuDomain = "feishu" | "lark" | (string & {});
6
+ type FeishuDefaultAccountSelectionSource = "explicit-default" | "mapped-default" | "fallback";
7
+ type FeishuAccountSelectionSource = "explicit" | FeishuDefaultAccountSelectionSource;
8
+ type ResolvedFeishuAccount = {
9
+ accountId: string;
10
+ selectionSource: FeishuAccountSelectionSource;
11
+ enabled: boolean;
12
+ configured: boolean;
13
+ name?: string;
14
+ appId?: string;
15
+ appSecret?: string;
16
+ encryptKey?: string;
17
+ verificationToken?: string;
18
+ domain: FeishuDomain; /** Merged config (top-level defaults + account-specific overrides) */
19
+ config: FeishuConfig;
20
+ };
21
+ type FeishuSendResult = {
22
+ messageId: string;
23
+ chatId: string;
24
+ };
25
+ type FeishuChatType = "p2p" | "group" | "private";
26
+ type FeishuMessageInfo = {
27
+ messageId: string;
28
+ chatId: string;
29
+ chatType?: FeishuChatType;
30
+ senderId?: string;
31
+ senderOpenId?: string;
32
+ senderType?: string;
33
+ content: string;
34
+ contentType: string;
35
+ createTime?: number;
36
+ };
37
+ type FeishuProbeResult = BaseProbeResult<string> & {
38
+ appId?: string;
39
+ botName?: string;
40
+ botOpenId?: string;
41
+ };
42
+ //#endregion
43
+ export { FeishuConfig, FeishuDomain, FeishuMessageInfo, FeishuProbeResult, FeishuSendResult, ResolvedFeishuAccount };
@@ -0,0 +1,145 @@
1
+ import { resolveFeishuAccount } from "./accounts.js";
2
+ import { createFeishuClient } from "./client.js";
3
+ import { getFeishuRuntime } from "./runtime.js";
4
+ //#region src/typing.ts
5
+ const TYPING_EMOJI = "THUMBSUP";
6
+ /**
7
+ * Feishu API error codes that indicate the caller should back off.
8
+ * These must propagate to the typing circuit breaker so the keepalive loop
9
+ * can trip and stop retrying.
10
+ *
11
+ * - 99991400: Rate limit (too many requests per second)
12
+ * - 99991403: Monthly API call quota exceeded
13
+ * - 429: Standard HTTP 429 returned as a Feishu SDK error code
14
+ *
15
+ * @see https://open.feishu.cn/document/server-docs/api-call-guide/generic-error-code
16
+ */
17
+ const FEISHU_BACKOFF_CODES = new Set([
18
+ 99991400,
19
+ 99991403,
20
+ 429
21
+ ]);
22
+ /**
23
+ * Custom error class for Feishu backoff conditions detected from non-throwing
24
+ * SDK responses. Carries a numeric `.code` so that `isFeishuBackoffError()`
25
+ * recognises it when the error is caught downstream.
26
+ */
27
+ var FeishuBackoffError = class extends Error {
28
+ code;
29
+ constructor(code) {
30
+ super(`Feishu API backoff: code ${code}`);
31
+ this.name = "FeishuBackoffError";
32
+ this.code = code;
33
+ }
34
+ };
35
+ /**
36
+ * Check whether an error represents a rate-limit or quota-exceeded condition
37
+ * from the Feishu API that should stop the typing keepalive loop.
38
+ *
39
+ * Handles two shapes:
40
+ * 1. AxiosError with `response.status` and `response.data.code`
41
+ * 2. Feishu SDK error with a top-level `code` property
42
+ */
43
+ function isFeishuBackoffError(err) {
44
+ if (typeof err !== "object" || err === null) return false;
45
+ const response = err.response;
46
+ if (response) {
47
+ if (response.status === 429) return true;
48
+ if (typeof response.data?.code === "number" && FEISHU_BACKOFF_CODES.has(response.data.code)) return true;
49
+ }
50
+ const code = err.code;
51
+ if (typeof code === "number" && FEISHU_BACKOFF_CODES.has(code)) return true;
52
+ return false;
53
+ }
54
+ /**
55
+ * Check whether a Feishu SDK response object contains a backoff error code.
56
+ *
57
+ * The Feishu SDK sometimes returns a normal response (no throw) with an
58
+ * API-level error code in the response body. This must be detected so the
59
+ * circuit breaker can trip. See codex review on #28157.
60
+ */
61
+ function getBackoffCodeFromResponse(response) {
62
+ if (typeof response !== "object" || response === null) return;
63
+ const code = response.code;
64
+ if (typeof code === "number" && FEISHU_BACKOFF_CODES.has(code)) return code;
65
+ }
66
+ /**
67
+ * Add a typing indicator (reaction) to a message.
68
+ *
69
+ * Rate-limit and quota errors are re-thrown so the circuit breaker in
70
+ * `createTypingCallbacks` (typing-start-guard) can trip and stop the
71
+ * keepalive loop. See #28062.
72
+ *
73
+ * Also checks for backoff codes in non-throwing SDK responses (#28157).
74
+ */
75
+ async function addTypingIndicator(params) {
76
+ const { cfg, messageId, accountId, runtime } = params;
77
+ const account = resolveFeishuAccount({
78
+ cfg,
79
+ accountId
80
+ });
81
+ if (!account.configured) return {
82
+ messageId,
83
+ reactionId: null
84
+ };
85
+ const client = createFeishuClient(account);
86
+ try {
87
+ const response = await client.im.messageReaction.create({
88
+ path: { message_id: messageId },
89
+ data: { reaction_type: { emoji_type: TYPING_EMOJI } }
90
+ });
91
+ const backoffCode = getBackoffCodeFromResponse(response);
92
+ if (backoffCode !== void 0) {
93
+ if (getFeishuRuntime().logging.shouldLogVerbose()) runtime?.log?.(`[feishu] typing indicator response contains backoff code ${backoffCode}, stopping keepalive`);
94
+ throw new FeishuBackoffError(backoffCode);
95
+ }
96
+ return {
97
+ messageId,
98
+ reactionId: response?.data?.reaction_id ?? null
99
+ };
100
+ } catch (err) {
101
+ if (isFeishuBackoffError(err)) {
102
+ if (getFeishuRuntime().logging.shouldLogVerbose()) runtime?.log?.("[feishu] typing indicator hit rate-limit/quota, stopping keepalive");
103
+ throw err;
104
+ }
105
+ if (getFeishuRuntime().logging.shouldLogVerbose()) runtime?.log?.(`[feishu] failed to add typing indicator: ${String(err)}`);
106
+ return {
107
+ messageId,
108
+ reactionId: null
109
+ };
110
+ }
111
+ }
112
+ /**
113
+ * Remove a typing indicator (reaction) from a message.
114
+ *
115
+ * Rate-limit and quota errors are re-thrown for the same reason as above.
116
+ */
117
+ async function removeTypingIndicator(params) {
118
+ if (TYPING_EMOJI !== "Typing") return;
119
+ const { cfg, state, accountId, runtime } = params;
120
+ if (!state.reactionId) return;
121
+ const account = resolveFeishuAccount({
122
+ cfg,
123
+ accountId
124
+ });
125
+ if (!account.configured) return;
126
+ const client = createFeishuClient(account);
127
+ try {
128
+ const backoffCode = getBackoffCodeFromResponse(await client.im.messageReaction.delete({ path: {
129
+ message_id: state.messageId,
130
+ reaction_id: state.reactionId
131
+ } }));
132
+ if (backoffCode !== void 0) {
133
+ if (getFeishuRuntime().logging.shouldLogVerbose()) runtime?.log?.(`[feishu] typing indicator removal response contains backoff code ${backoffCode}, stopping keepalive`);
134
+ throw new FeishuBackoffError(backoffCode);
135
+ }
136
+ } catch (err) {
137
+ if (isFeishuBackoffError(err)) {
138
+ if (getFeishuRuntime().logging.shouldLogVerbose()) runtime?.log?.("[feishu] typing indicator removal hit rate-limit/quota, stopping keepalive");
139
+ throw err;
140
+ }
141
+ if (getFeishuRuntime().logging.shouldLogVerbose()) runtime?.log?.(`[feishu] failed to remove typing indicator: ${String(err)}`);
142
+ }
143
+ }
144
+ //#endregion
145
+ export { addTypingIndicator, removeTypingIndicator };
@@ -0,0 +1,102 @@
1
+ import { NeedAuthorizationError, REFRESH_TOKEN_RETRYABLE, TOKEN_RETRY_CODES } from "./auth-errors.js";
2
+ import { feishuFetch } from "./feishu-fetch.js";
3
+ import { resolveOAuthEndpoints } from "./device-flow.js";
4
+ import { getStoredToken, removeStoredToken, setStoredToken, tokenStatus } from "./token-store.js";
5
+ //#region src/uat-client.ts
6
+ const refreshLocks = /* @__PURE__ */ new Map();
7
+ async function doRefreshToken(opts, stored) {
8
+ if (Date.now() >= stored.refreshExpiresAt) {
9
+ await removeStoredToken(opts.appId, opts.userOpenId);
10
+ return null;
11
+ }
12
+ const endpoints = resolveOAuthEndpoints(opts.domain);
13
+ const requestBody = new URLSearchParams({
14
+ grant_type: "refresh_token",
15
+ refresh_token: stored.refreshToken,
16
+ client_id: opts.appId,
17
+ client_secret: opts.appSecret
18
+ }).toString();
19
+ const callEndpoint = async () => {
20
+ return await (await feishuFetch(endpoints.token, {
21
+ method: "POST",
22
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
23
+ body: requestBody
24
+ })).json();
25
+ };
26
+ let data = await callEndpoint();
27
+ const code = typeof data.code === "number" ? data.code : void 0;
28
+ const error = typeof data.error === "string" ? data.error : void 0;
29
+ if (code !== void 0 && code !== 0 || error) if (code !== void 0 && REFRESH_TOKEN_RETRYABLE.has(code)) {
30
+ data = await callEndpoint();
31
+ const retryCode = typeof data.code === "number" ? data.code : void 0;
32
+ const retryError = typeof data.error === "string" ? data.error : void 0;
33
+ if (retryCode !== void 0 && retryCode !== 0 || retryError) {
34
+ await removeStoredToken(opts.appId, opts.userOpenId);
35
+ return null;
36
+ }
37
+ } else {
38
+ await removeStoredToken(opts.appId, opts.userOpenId);
39
+ return null;
40
+ }
41
+ if (!data.access_token) throw new Error("Token refresh returned no access_token");
42
+ const now = Date.now();
43
+ const updated = {
44
+ userOpenId: stored.userOpenId,
45
+ appId: opts.appId,
46
+ accessToken: String(data.access_token),
47
+ refreshToken: String(data.refresh_token ?? stored.refreshToken),
48
+ expiresAt: now + Number(data.expires_in ?? 7200) * 1e3,
49
+ refreshExpiresAt: data.refresh_token_expires_in ? now + Number(data.refresh_token_expires_in) * 1e3 : stored.refreshExpiresAt,
50
+ scope: String(data.scope ?? stored.scope),
51
+ grantedAt: stored.grantedAt
52
+ };
53
+ await setStoredToken(updated);
54
+ return updated;
55
+ }
56
+ async function refreshWithLock(opts, stored) {
57
+ const key = `${opts.appId}:${opts.userOpenId}`;
58
+ const existing = refreshLocks.get(key);
59
+ if (existing) {
60
+ await existing;
61
+ return getStoredToken(opts.appId, opts.userOpenId);
62
+ }
63
+ const promise = doRefreshToken(opts, stored);
64
+ refreshLocks.set(key, promise);
65
+ try {
66
+ return await promise;
67
+ } finally {
68
+ refreshLocks.delete(key);
69
+ }
70
+ }
71
+ async function getValidAccessToken(opts) {
72
+ const stored = await getStoredToken(opts.appId, opts.userOpenId);
73
+ if (!stored) throw new NeedAuthorizationError(opts.userOpenId);
74
+ const status = tokenStatus(stored);
75
+ if (status === "valid") return stored.accessToken;
76
+ if (status === "needs_refresh") {
77
+ const refreshed = await refreshWithLock(opts, stored);
78
+ if (!refreshed) throw new NeedAuthorizationError(opts.userOpenId);
79
+ return refreshed.accessToken;
80
+ }
81
+ await removeStoredToken(opts.appId, opts.userOpenId);
82
+ throw new NeedAuthorizationError(opts.userOpenId);
83
+ }
84
+ async function callWithUAT(opts, apiCall) {
85
+ const accessToken = await getValidAccessToken(opts);
86
+ try {
87
+ return await apiCall(accessToken);
88
+ } catch (error) {
89
+ const code = error.code ?? error.response?.data?.code;
90
+ if (!TOKEN_RETRY_CODES.has(Number(code))) throw error;
91
+ const stored = await getStoredToken(opts.appId, opts.userOpenId);
92
+ if (!stored) throw new NeedAuthorizationError(opts.userOpenId);
93
+ const refreshed = await refreshWithLock(opts, stored);
94
+ if (!refreshed) throw new NeedAuthorizationError(opts.userOpenId);
95
+ return apiCall(refreshed.accessToken);
96
+ }
97
+ }
98
+ async function revokeUAT(appId, userOpenId) {
99
+ await removeStoredToken(appId, userOpenId);
100
+ }
101
+ //#endregion
102
+ export { callWithUAT, revokeUAT };
@@ -0,0 +1,132 @@
1
+ import { AppScopeMissingError, LARK_ERROR, NeedAuthorizationError, UserAuthRequiredError, UserScopeInsufficientError } from "./auth-errors.js";
2
+ import { getRequiredScopes } from "./tool-scopes.js";
3
+ import { listEnabledFeishuAccounts, resolveFeishuAccount } from "./accounts.js";
4
+ import { getAppGrantedScopes, invalidateAppScopeCache, missingScopes } from "./app-scope-checker.js";
5
+ import { createFeishuClient } from "./client.js";
6
+ import { getTicket } from "./lark-ticket.js";
7
+ import { rawLarkRequest } from "./raw-request.js";
8
+ import { getStoredToken } from "./token-store.js";
9
+ import { callWithUAT } from "./uat-client.js";
10
+ import * as Lark from "@larksuiteoapi/node-sdk";
11
+ //#region src/user-tool-client.ts
12
+ function assertConfiguredAccount(account) {
13
+ if (!account.enabled) throw new Error(`Feishu account "${account.accountId}" is disabled.`);
14
+ if (!account.configured || !account.appId || !account.appSecret) throw new Error(`Feishu account "${account.accountId}" is not configured.`);
15
+ return account;
16
+ }
17
+ function resolveConfiguredAccount(config, accountIndex = 0) {
18
+ const ticket = getTicket();
19
+ if (ticket?.accountId) return assertConfiguredAccount(resolveFeishuAccount({
20
+ cfg: config,
21
+ accountId: ticket.accountId
22
+ }));
23
+ const accounts = listEnabledFeishuAccounts(config);
24
+ if (accounts.length === 0) throw new Error("No enabled Feishu accounts configured.");
25
+ return assertConfiguredAccount(accounts[Math.min(accountIndex, accounts.length - 1)]);
26
+ }
27
+ var UserToolClient = class {
28
+ account;
29
+ senderOpenId;
30
+ sdk;
31
+ constructor(config, accountIndex = 0) {
32
+ this.config = config;
33
+ this.account = resolveConfiguredAccount(config, accountIndex);
34
+ this.senderOpenId = getTicket()?.senderOpenId;
35
+ this.sdk = createFeishuClient(this.account);
36
+ }
37
+ async invoke(toolAction, fn, options) {
38
+ const requiredScopes = getRequiredScopes(toolAction);
39
+ const tokenType = options?.as ?? "user";
40
+ const appScopeVerified = await this.verifyAppScopes(requiredScopes, tokenType, toolAction);
41
+ if (tokenType === "tenant") try {
42
+ return await fn(this.sdk);
43
+ } catch (error) {
44
+ this.rethrowStructuredError(error, toolAction, requiredScopes);
45
+ throw error;
46
+ }
47
+ const userOpenId = options?.userOpenId ?? this.senderOpenId;
48
+ if (!userOpenId) throw new UserAuthRequiredError("unknown", {
49
+ apiName: toolAction,
50
+ scopes: requiredScopes,
51
+ appScopeVerified,
52
+ appId: this.account.appId
53
+ });
54
+ const stored = await getStoredToken(this.account.appId, userOpenId);
55
+ if (!stored) throw new UserAuthRequiredError(userOpenId, {
56
+ apiName: toolAction,
57
+ scopes: requiredScopes,
58
+ appScopeVerified,
59
+ appId: this.account.appId
60
+ });
61
+ if (appScopeVerified && stored.scope && requiredScopes.length > 0) {
62
+ const granted = new Set(stored.scope.split(/\s+/).filter(Boolean));
63
+ const missingUserScopes = requiredScopes.filter((scope) => !granted.has(scope));
64
+ if (missingUserScopes.length > 0) throw new UserAuthRequiredError(userOpenId, {
65
+ apiName: toolAction,
66
+ scopes: missingUserScopes,
67
+ appScopeVerified,
68
+ appId: this.account.appId
69
+ });
70
+ }
71
+ try {
72
+ return await callWithUAT({
73
+ userOpenId,
74
+ appId: this.account.appId,
75
+ appSecret: this.account.appSecret,
76
+ domain: this.account.domain
77
+ }, (accessToken) => fn(this.sdk, Lark.withUserAccessToken(accessToken), accessToken));
78
+ } catch (error) {
79
+ if (error instanceof NeedAuthorizationError) throw new UserAuthRequiredError(userOpenId, {
80
+ apiName: toolAction,
81
+ scopes: requiredScopes,
82
+ appScopeVerified,
83
+ appId: this.account.appId
84
+ });
85
+ this.rethrowStructuredError(error, toolAction, requiredScopes, userOpenId);
86
+ throw error;
87
+ }
88
+ }
89
+ async invokeByPath(toolAction, path, options) {
90
+ return this.invoke(toolAction, async (_sdk, _opts, uat) => rawLarkRequest({
91
+ domain: this.account.domain,
92
+ path,
93
+ method: options?.method,
94
+ body: options?.body,
95
+ query: options?.query,
96
+ headers: options?.headers,
97
+ accessToken: uat
98
+ }), options);
99
+ }
100
+ async verifyAppScopes(requiredScopes, tokenType, toolAction) {
101
+ if (requiredScopes.length === 0) return true;
102
+ const appGrantedScopes = await getAppGrantedScopes(this.sdk, this.account.appId, tokenType);
103
+ if (appGrantedScopes.length === 0) return false;
104
+ const missingAppScopes = missingScopes(appGrantedScopes, tokenType === "user" ? [...new Set([...requiredScopes, "offline_access"])] : requiredScopes);
105
+ if (missingAppScopes.length > 0) throw new AppScopeMissingError({
106
+ apiName: toolAction,
107
+ scopes: missingAppScopes,
108
+ appId: this.account.appId
109
+ });
110
+ return true;
111
+ }
112
+ rethrowStructuredError(error, toolAction, requiredScopes, userOpenId) {
113
+ const code = error.code ?? error.response?.data?.code;
114
+ if (code === LARK_ERROR.APP_SCOPE_MISSING) {
115
+ invalidateAppScopeCache(this.account.appId);
116
+ throw new AppScopeMissingError({
117
+ apiName: toolAction,
118
+ scopes: requiredScopes,
119
+ appId: this.account.appId
120
+ });
121
+ }
122
+ if (code === LARK_ERROR.USER_SCOPE_INSUFFICIENT && userOpenId) throw new UserScopeInsufficientError(userOpenId, {
123
+ apiName: toolAction,
124
+ scopes: requiredScopes
125
+ });
126
+ }
127
+ };
128
+ function createUserToolClient(config, accountIndex = 0) {
129
+ return new UserToolClient(config, accountIndex);
130
+ }
131
+ //#endregion
132
+ export { createUserToolClient };
@@ -0,0 +1,110 @@
1
+ import { AppScopeMissingError, UserAuthRequiredError, UserScopeInsufficientError } from "./auth-errors.js";
2
+ import { openPlatformDomain } from "./domains.js";
3
+ import { formatLarkError } from "./user-tool-result.js";
4
+ import { getAllKnownScopes } from "./tool-scopes.js";
5
+ import { createUserToolClient } from "./user-tool-client.js";
6
+ import { jsonToolResult } from "./tool-result.js";
7
+ import { Type } from "@sinclair/typebox";
8
+ //#region src/user-tool-helpers.ts
9
+ function json(data) {
10
+ return jsonToolResult(data);
11
+ }
12
+ function createToolContext(api, toolName, accountIndex = 0) {
13
+ const logPrefix = `${toolName}:`;
14
+ return {
15
+ toolClient: () => createUserToolClient(api.config, accountIndex),
16
+ log: {
17
+ info: (message) => api.logger.info?.(`${logPrefix} ${message}`),
18
+ warn: (message) => api.logger.warn?.(`${logPrefix} ${message}`),
19
+ error: (message) => api.logger.error?.(`${logPrefix} ${message}`),
20
+ debug: (message) => api.logger.debug?.(`${logPrefix} ${message}`)
21
+ }
22
+ };
23
+ }
24
+ function registerTool(api, tool, opts) {
25
+ api.registerTool(tool, opts);
26
+ }
27
+ function assertLarkOk(res) {
28
+ if (!res.code || res.code === 0) return;
29
+ throw new Error(res.msg ?? `Feishu API error (code=${res.code})`);
30
+ }
31
+ function StringEnum(values, options) {
32
+ return Type.Union(values.map((value) => Type.Literal(value)), options);
33
+ }
34
+ function parseTimeToTimestamp(input) {
35
+ const date = parseDateLike(input);
36
+ return date ? Math.floor(date.getTime() / 1e3).toString() : null;
37
+ }
38
+ function parseTimeToTimestampMs(input) {
39
+ const date = parseDateLike(input);
40
+ return date ? date.getTime().toString() : null;
41
+ }
42
+ function parseTimeToRFC3339(input) {
43
+ const trimmed = input.trim();
44
+ if (/[Zz]$|[+-]\d{2}:\d{2}$/.test(trimmed)) return new Date(trimmed).toString() === "Invalid Date" ? null : trimmed;
45
+ const match = trimmed.replace(" ", "T").match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?$/);
46
+ if (!match) {
47
+ const date = new Date(trimmed);
48
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
49
+ }
50
+ const [, y, m, d, hh, mm, ss] = match;
51
+ return `${y}-${m}-${d}T${hh}:${mm}:${ss ?? "00"}+08:00`;
52
+ }
53
+ function unixTimestampToISO8601(raw) {
54
+ if (raw === void 0 || raw === null || raw === "") return null;
55
+ const value = Number(raw);
56
+ if (!Number.isFinite(value)) return null;
57
+ const ms = value > 0xe8d4a51000 ? value : value * 1e3;
58
+ return new Date(ms).toISOString();
59
+ }
60
+ function parseDateLike(input) {
61
+ const trimmed = input.trim();
62
+ if (/[Zz]$|[+-]\d{2}:\d{2}$/.test(trimmed)) {
63
+ const date = new Date(trimmed);
64
+ return Number.isNaN(date.getTime()) ? null : date;
65
+ }
66
+ const match = trimmed.replace("T", " ").match(/^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2})(?::(\d{2}))?$/);
67
+ if (!match) {
68
+ const date = new Date(trimmed);
69
+ return Number.isNaN(date.getTime()) ? null : date;
70
+ }
71
+ const [, year, month, day, hour, minute, second] = match;
72
+ return new Date(Date.UTC(Number(year), Number(month) - 1, Number(day), Number(hour) - 8, Number(minute), Number(second ?? "0")));
73
+ }
74
+ async function handleInvokeError(error, _api) {
75
+ if (error instanceof AppScopeMissingError) return json({
76
+ error: "app_scope_missing",
77
+ message: error.message,
78
+ missing_scopes: error.missingScopes,
79
+ app_id: error.appId,
80
+ open_platform: openPlatformDomain("feishu")
81
+ });
82
+ if (error instanceof UserAuthRequiredError) return json({
83
+ error: "need_user_authorization",
84
+ message: "当前用户尚未完成飞书 OAuth 授权,或授权范围不足。",
85
+ required_scopes: error.requiredScopes,
86
+ next_tool_call: {
87
+ tool: "feishu_oauth",
88
+ params: {
89
+ action: "authorize",
90
+ scope: error.requiredScopes.join(" ")
91
+ }
92
+ },
93
+ default_scope_suggestion: getAllKnownScopes().join(" ")
94
+ });
95
+ if (error instanceof UserScopeInsufficientError) return json({
96
+ error: "user_scope_insufficient",
97
+ message: "当前用户授权范围不足,请重新授权补齐缺失 scope。",
98
+ missing_scopes: error.missingScopes,
99
+ next_tool_call: {
100
+ tool: "feishu_oauth",
101
+ params: {
102
+ action: "authorize",
103
+ scope: error.missingScopes.join(" ")
104
+ }
105
+ }
106
+ });
107
+ return json({ error: formatLarkError(error) });
108
+ }
109
+ //#endregion
110
+ export { StringEnum, assertLarkOk, createToolContext, handleInvokeError, json, parseTimeToRFC3339, parseTimeToTimestamp, parseTimeToTimestampMs, registerTool, unixTimestampToISO8601 };
@@ -0,0 +1,10 @@
1
+ //#region src/user-tool-result.ts
2
+ function formatLarkError(error) {
3
+ if (!error || typeof error !== "object") return String(error);
4
+ const typed = error;
5
+ if (typeof typed.code === "number" && typed.msg) return typed.msg;
6
+ if (typed.response?.data?.msg) return typed.response.data.msg;
7
+ return typed.message ?? String(error);
8
+ }
9
+ //#endregion
10
+ export { formatLarkError };
@@ -0,0 +1,45 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ //#region src/wiki-schema.ts
3
+ const FeishuWikiSchema = Type.Union([
4
+ Type.Object({ action: Type.Literal("spaces") }),
5
+ Type.Object({
6
+ action: Type.Literal("nodes"),
7
+ space_id: Type.String({ description: "Knowledge space ID" }),
8
+ parent_node_token: Type.Optional(Type.String({ description: "Parent node token (optional, omit for root)" }))
9
+ }),
10
+ Type.Object({
11
+ action: Type.Literal("get"),
12
+ token: Type.String({ description: "Wiki node token (from URL /wiki/XXX)" })
13
+ }),
14
+ Type.Object({
15
+ action: Type.Literal("search"),
16
+ query: Type.String({ description: "Search query" }),
17
+ space_id: Type.Optional(Type.String({ description: "Limit search to this space (optional)" }))
18
+ }),
19
+ Type.Object({
20
+ action: Type.Literal("create"),
21
+ space_id: Type.String({ description: "Knowledge space ID" }),
22
+ title: Type.String({ description: "Node title" }),
23
+ obj_type: Type.Optional(Type.Union([
24
+ Type.Literal("docx"),
25
+ Type.Literal("sheet"),
26
+ Type.Literal("bitable")
27
+ ], { description: "Object type (default: docx)" })),
28
+ parent_node_token: Type.Optional(Type.String({ description: "Parent node token (optional, omit for root)" }))
29
+ }),
30
+ Type.Object({
31
+ action: Type.Literal("move"),
32
+ space_id: Type.String({ description: "Source knowledge space ID" }),
33
+ node_token: Type.String({ description: "Node token to move" }),
34
+ target_space_id: Type.Optional(Type.String({ description: "Target space ID (optional, same space if omitted)" })),
35
+ target_parent_token: Type.Optional(Type.String({ description: "Target parent node token (optional, root if omitted)" }))
36
+ }),
37
+ Type.Object({
38
+ action: Type.Literal("rename"),
39
+ space_id: Type.String({ description: "Knowledge space ID" }),
40
+ node_token: Type.String({ description: "Node token to rename" }),
41
+ title: Type.String({ description: "New title" })
42
+ })
43
+ ]);
44
+ //#endregion
45
+ export { FeishuWikiSchema };