@nextclaw/channel-plugin-feishu 0.2.29-beta.0 → 0.2.29-beta.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.
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,193 @@
1
+ import { DEFAULT_SECRET_PROVIDER_ALIAS, ENV_SECRET_REF_ID_RE, formatExecSecretRefIdValidationMessage, isValidExecSecretRefId, isValidFileSecretRefId } from "./secrets-core.js";
2
+ import fs from "node:fs";
3
+ //#region src/nextclaw-sdk/secrets-prompt.ts
4
+ function resolveDefaultSecretProviderAlias(cfg, source) {
5
+ const configured = source === "env" ? cfg.secrets?.defaults?.env : source === "file" ? cfg.secrets?.defaults?.file : cfg.secrets?.defaults?.exec;
6
+ if (configured?.trim()) return configured.trim();
7
+ const providers = cfg.secrets?.providers;
8
+ if (providers) {
9
+ for (const [name, provider] of Object.entries(providers)) if (provider?.source === source) return name;
10
+ }
11
+ return DEFAULT_SECRET_PROVIDER_ALIAS;
12
+ }
13
+ function isRecord(value) {
14
+ return typeof value === "object" && value !== null && !Array.isArray(value);
15
+ }
16
+ function readJsonPointerValue(payload, pointer) {
17
+ if (pointer === "value") return typeof payload === "string" ? payload : null;
18
+ const segments = pointer.slice(1).split("/").map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~"));
19
+ let cursor = payload;
20
+ for (const segment of segments) {
21
+ if (!isRecord(cursor) && !Array.isArray(cursor)) return null;
22
+ cursor = cursor[segment];
23
+ }
24
+ return typeof cursor === "string" ? cursor : null;
25
+ }
26
+ function tryResolveFileProviderValue(cfg, provider, id) {
27
+ const providerConfig = cfg.secrets?.providers?.[provider];
28
+ if (!providerConfig || providerConfig.source !== "file" || !providerConfig.path) return null;
29
+ try {
30
+ const raw = fs.readFileSync(providerConfig.path, "utf-8");
31
+ if (providerConfig.mode === "singleValue") return raw.trim();
32
+ return readJsonPointerValue(JSON.parse(raw), id);
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+ function promptSingleChannelToken(params) {
38
+ const promptToken = async () => String(await params.prompter.text({
39
+ message: params.inputPrompt,
40
+ validate: (value) => value?.trim() ? void 0 : "Required"
41
+ })).trim();
42
+ return (async () => {
43
+ if (params.canUseEnv) {
44
+ if (await params.prompter.confirm({
45
+ message: params.envPrompt,
46
+ initialValue: true
47
+ })) return {
48
+ useEnv: true,
49
+ token: null
50
+ };
51
+ return {
52
+ useEnv: false,
53
+ token: await promptToken()
54
+ };
55
+ }
56
+ if (params.hasConfigToken && params.accountConfigured) {
57
+ if (await params.prompter.confirm({
58
+ message: params.keepPrompt,
59
+ initialValue: true
60
+ })) return {
61
+ useEnv: false,
62
+ token: null
63
+ };
64
+ }
65
+ return {
66
+ useEnv: false,
67
+ token: await promptToken()
68
+ };
69
+ })();
70
+ }
71
+ async function resolveSecretInputMode(params) {
72
+ if (params.explicitMode) return params.explicitMode;
73
+ return await params.prompter.select({
74
+ message: `How do you want to provide this ${params.credentialLabel}?`,
75
+ initialValue: "plaintext",
76
+ options: [{
77
+ value: "plaintext",
78
+ label: `Enter ${params.credentialLabel}`,
79
+ hint: "Stores the credential directly in NextClaw config"
80
+ }, {
81
+ value: "ref",
82
+ label: "Use secret reference",
83
+ hint: "Stores a reference to env or configured secret providers"
84
+ }]
85
+ });
86
+ }
87
+ async function promptSecretRefForSetup(params) {
88
+ const providers = Object.entries(params.cfg.secrets?.providers ?? {}).filter(([, provider]) => provider?.source === "file" || provider?.source === "exec");
89
+ if (await params.prompter.select({
90
+ message: "Where is this secret stored?",
91
+ initialValue: "env",
92
+ options: [{
93
+ value: "env",
94
+ label: "Environment variable",
95
+ hint: "Reference an env var"
96
+ }, ...providers.length > 0 ? [{
97
+ value: "provider",
98
+ label: "Configured provider",
99
+ hint: "Reference file/exec provider"
100
+ }] : []]
101
+ }) === "env") {
102
+ const envVar = String(await params.prompter.text({
103
+ message: "Environment variable name",
104
+ initialValue: params.preferredEnvVar,
105
+ placeholder: params.preferredEnvVar ?? "NEXTCLAW_SECRET",
106
+ validate: (value) => {
107
+ const candidate = value.trim();
108
+ if (!ENV_SECRET_REF_ID_RE.test(candidate)) return "Use an env var name like \"OPENAI_API_KEY\".";
109
+ if (!process.env[candidate]?.trim()) return `Environment variable "${candidate}" is missing or empty in this session.`;
110
+ }
111
+ })).trim();
112
+ return {
113
+ value: {
114
+ source: "env",
115
+ provider: resolveDefaultSecretProviderAlias(params.cfg, "env"),
116
+ id: envVar
117
+ },
118
+ resolvedValue: process.env[envVar]?.trim() ?? ""
119
+ };
120
+ }
121
+ const provider = await params.prompter.select({
122
+ message: "Select secret provider",
123
+ initialValue: providers[0]?.[0],
124
+ options: providers.map(([name, providerConfig]) => ({
125
+ value: name,
126
+ label: name,
127
+ hint: providerConfig?.source === "exec" ? "Exec provider" : "File provider"
128
+ }))
129
+ });
130
+ const providerConfig = params.cfg.secrets?.providers?.[provider];
131
+ const id = String(await params.prompter.text({
132
+ message: providerConfig?.source === "file" ? "Secret id (JSON pointer, or 'value' for singleValue mode)" : "Secret id for the exec provider",
133
+ initialValue: providerConfig?.source === "file" ? "/providers/feishu/appSecret" : void 0,
134
+ validate: (value) => {
135
+ const candidate = value.trim();
136
+ if (!candidate) return "Required";
137
+ if (providerConfig?.source === "file") return isValidFileSecretRefId(candidate) ? void 0 : "Invalid file secret reference id.";
138
+ return isValidExecSecretRefId(candidate) ? void 0 : formatExecSecretRefIdValidationMessage();
139
+ }
140
+ })).trim();
141
+ const resolvedValue = providerConfig?.source === "file" ? tryResolveFileProviderValue(params.cfg, provider, id) ?? "" : "";
142
+ if (!resolvedValue && providerConfig?.source === "exec") await params.prompter.note("Exec provider reference saved. Connection probe will rely on runtime secret resolution later.", "Secret reference saved");
143
+ return {
144
+ value: {
145
+ source: providerConfig?.source === "exec" ? "exec" : "file",
146
+ provider,
147
+ id
148
+ },
149
+ resolvedValue
150
+ };
151
+ }
152
+ async function promptSingleChannelSecretInput(params) {
153
+ if (await resolveSecretInputMode({
154
+ prompter: params.prompter,
155
+ explicitMode: params.secretInputMode,
156
+ credentialLabel: params.credentialLabel
157
+ }) === "plaintext") {
158
+ const plainResult = await promptSingleChannelToken({
159
+ prompter: params.prompter,
160
+ accountConfigured: params.accountConfigured,
161
+ canUseEnv: params.canUseEnv,
162
+ hasConfigToken: params.hasConfigToken,
163
+ envPrompt: params.envPrompt,
164
+ keepPrompt: params.keepPrompt,
165
+ inputPrompt: params.inputPrompt
166
+ });
167
+ if (plainResult.useEnv) return { action: "use-env" };
168
+ if (plainResult.token) return {
169
+ action: "set",
170
+ value: plainResult.token,
171
+ resolvedValue: plainResult.token
172
+ };
173
+ return { action: "keep" };
174
+ }
175
+ if (params.hasConfigToken && params.accountConfigured) {
176
+ if (await params.prompter.confirm({
177
+ message: params.keepPrompt,
178
+ initialValue: true
179
+ })) return { action: "keep" };
180
+ }
181
+ const refResult = await promptSecretRefForSetup({
182
+ cfg: params.cfg,
183
+ prompter: params.prompter,
184
+ preferredEnvVar: params.preferredEnvVar
185
+ });
186
+ return {
187
+ action: "set",
188
+ value: refResult.value,
189
+ resolvedValue: refResult.resolvedValue
190
+ };
191
+ }
192
+ //#endregion
193
+ export { promptSingleChannelSecretInput };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,4 @@
1
+ import "./secrets-core.js";
2
+ import "./secrets-config.js";
3
+ import "./secrets-prompt.js";
4
+ export {};
@@ -0,0 +1,242 @@
1
+ //#region src/nextclaw-sdk/types.d.ts
2
+ type LogFn = (message: string) => void;
3
+ type SecretRefSource = "env" | "file" | "exec";
4
+ type DmPolicy = "open" | "pairing" | "allowlist";
5
+ type BaseProbeResult<TTarget = string> = {
6
+ ok: boolean;
7
+ target?: TTarget;
8
+ error?: string;
9
+ [key: string]: unknown;
10
+ };
11
+ type ChannelMeta = {
12
+ id: string;
13
+ label: string;
14
+ selectionLabel?: string;
15
+ docsPath?: string;
16
+ docsLabel?: string;
17
+ blurb?: string;
18
+ aliases?: string[];
19
+ order?: number;
20
+ [key: string]: unknown;
21
+ };
22
+ type ChannelPlugin<ResolvedAccount = unknown> = {
23
+ id: string;
24
+ meta: ChannelMeta;
25
+ config?: Record<string, unknown>;
26
+ onboarding?: ChannelOnboardingAdapter;
27
+ [key: string]: unknown;
28
+ __resolvedAccountType__?: ResolvedAccount;
29
+ };
30
+ type AnyAgentTool = {
31
+ name?: string;
32
+ description?: string;
33
+ parameters?: Record<string, unknown>;
34
+ execute?: (toolCallId: string, params: unknown) => Promise<unknown> | unknown;
35
+ [key: string]: unknown;
36
+ };
37
+ type ResolvedAgentRoute = {
38
+ agentId?: string;
39
+ sessionKey?: string;
40
+ [key: string]: unknown;
41
+ };
42
+ type InboundDebouncer<T> = {
43
+ run: (key: string, value: T, task: (value: T) => Promise<void>) => void;
44
+ clear: () => void;
45
+ };
46
+ type PluginRuntime = {
47
+ version?: string;
48
+ config: {
49
+ loadConfig?: () => OpenClawConfig;
50
+ writeConfigFile: (next: OpenClawConfig) => Promise<void>;
51
+ };
52
+ logging: {
53
+ shouldLogVerbose: () => boolean;
54
+ };
55
+ media: {
56
+ detectMime: (params: {
57
+ buffer: Buffer;
58
+ }) => Promise<string | undefined>;
59
+ loadWebMedia: (url: string, options?: Record<string, unknown>) => Promise<Record<string, unknown>>;
60
+ };
61
+ channel: {
62
+ media: {
63
+ fetchRemoteMedia: (params: {
64
+ url: string;
65
+ maxBytes?: number;
66
+ }) => Promise<Record<string, unknown>>;
67
+ saveMediaBuffer: (buffer: Buffer, contentType?: string, direction?: string, maxBytes?: number) => Promise<{
68
+ path: string;
69
+ contentType?: string;
70
+ }>;
71
+ };
72
+ text: {
73
+ chunkMarkdownText: (text: string, limit: number) => string[];
74
+ resolveMarkdownTableMode: (params: Record<string, unknown>) => unknown;
75
+ convertMarkdownTables: (text: string, mode?: unknown) => string;
76
+ resolveTextChunkLimit: (cfg: OpenClawConfig, channel: string, accountId?: string, options?: Record<string, unknown>) => number;
77
+ resolveChunkMode: (cfg: OpenClawConfig, channel: string) => string;
78
+ chunkTextWithMode: (text: string, limit: number, mode?: unknown) => string[];
79
+ hasControlCommand: (text: string, cfg: OpenClawConfig) => boolean;
80
+ };
81
+ reply: {
82
+ resolveEnvelopeFormatOptions: (cfg: OpenClawConfig) => Record<string, unknown>;
83
+ formatAgentEnvelope: (params: Record<string, unknown>) => string;
84
+ finalizeInboundContext: (params: Record<string, unknown>) => Record<string, unknown>;
85
+ withReplyDispatcher: (params: Record<string, unknown>) => Promise<Record<string, unknown>>;
86
+ dispatchReplyFromConfig: (params: Record<string, unknown>) => Promise<Record<string, unknown>>;
87
+ createReplyDispatcherWithTyping: (params: Record<string, unknown>) => {
88
+ dispatcher: unknown;
89
+ replyOptions: Record<string, unknown>;
90
+ markDispatchIdle: () => void;
91
+ };
92
+ resolveHumanDelayConfig: (cfg: OpenClawConfig, agentId: string) => unknown;
93
+ dispatchReplyWithBufferedBlockDispatcher?: (params: Record<string, unknown>) => Promise<void>;
94
+ };
95
+ routing: {
96
+ resolveAgentRoute: (params: Record<string, unknown>) => ResolvedAgentRoute | null;
97
+ };
98
+ pairing: {
99
+ readAllowFromStore: (params: {
100
+ channel: string;
101
+ accountId?: string;
102
+ }) => unknown;
103
+ upsertPairingRequest: (params: Record<string, unknown>) => Promise<{
104
+ code: string;
105
+ created: boolean;
106
+ }>;
107
+ };
108
+ commands: {
109
+ shouldComputeCommandAuthorized: (text: string) => boolean;
110
+ resolveCommandAuthorizedFromAuthorizers: (params: Record<string, unknown>) => Promise<boolean> | boolean;
111
+ };
112
+ debounce: {
113
+ resolveInboundDebounceMs: (params: Record<string, unknown>) => number;
114
+ createInboundDebouncer: <T>(params: Record<string, unknown>) => InboundDebouncer<T>;
115
+ };
116
+ };
117
+ [key: string]: unknown;
118
+ };
119
+ type RuntimeEnv = {
120
+ log?: (message: string) => void;
121
+ error?: (message: string) => void;
122
+ warn?: (message: string) => void;
123
+ debug?: (message: string) => void;
124
+ info?: (message: string) => void;
125
+ [key: string]: unknown;
126
+ };
127
+ type OpenClawPluginApi = {
128
+ config: ClawdbotConfig;
129
+ runtime: PluginRuntime;
130
+ logger: {
131
+ info: LogFn;
132
+ warn: LogFn;
133
+ error: LogFn;
134
+ debug?: LogFn;
135
+ };
136
+ registerTool: (tool: AnyAgentTool | ((ctx: Record<string, unknown>) => AnyAgentTool | AnyAgentTool[] | null | undefined), opts?: {
137
+ name?: string;
138
+ names?: string[];
139
+ optional?: boolean;
140
+ }) => void;
141
+ registerChannel: (registration: unknown) => void;
142
+ [key: string]: unknown;
143
+ };
144
+ type WizardSelectOption<T extends string = string> = {
145
+ value: T;
146
+ label: string;
147
+ hint?: string;
148
+ };
149
+ type WizardPrompter = {
150
+ note: (message: string, title?: string) => Promise<void>;
151
+ text: (params: {
152
+ message: string;
153
+ placeholder?: string;
154
+ initialValue?: string;
155
+ validate?: (value: string) => string | undefined;
156
+ }) => Promise<string>;
157
+ confirm: (params: {
158
+ message: string;
159
+ initialValue?: boolean;
160
+ }) => Promise<boolean>;
161
+ select: <T extends string = string>(params: {
162
+ message: string;
163
+ options: WizardSelectOption<T>[];
164
+ initialValue?: T | string;
165
+ }) => Promise<T>;
166
+ };
167
+ type ChannelOnboardingDmPolicy = {
168
+ label: string;
169
+ channel: string;
170
+ policyKey: string;
171
+ allowFromKey: string;
172
+ getCurrent: (cfg: ClawdbotConfig) => DmPolicy;
173
+ setPolicy: (cfg: ClawdbotConfig, policy: DmPolicy) => ClawdbotConfig;
174
+ promptAllowFrom: (params: {
175
+ cfg: ClawdbotConfig;
176
+ prompter: WizardPrompter;
177
+ }) => Promise<ClawdbotConfig>;
178
+ };
179
+ type ChannelOnboardingAdapter = {
180
+ channel: string;
181
+ getStatus: (params: {
182
+ cfg: ClawdbotConfig;
183
+ }) => Promise<{
184
+ channel: string;
185
+ configured: boolean;
186
+ statusLines: string[];
187
+ selectionHint?: string;
188
+ quickstartScore?: number;
189
+ }>;
190
+ configure: (params: {
191
+ cfg: ClawdbotConfig;
192
+ prompter: WizardPrompter;
193
+ }) => Promise<{
194
+ cfg: ClawdbotConfig;
195
+ accountId?: string;
196
+ }>;
197
+ dmPolicy?: ChannelOnboardingDmPolicy;
198
+ disable?: (cfg: ClawdbotConfig) => ClawdbotConfig;
199
+ };
200
+ type OpenClawConfig = {
201
+ channels?: Record<string, Record<string, unknown> | undefined>;
202
+ bindings?: Array<{
203
+ agentId?: string;
204
+ match?: {
205
+ channel?: string;
206
+ peer?: {
207
+ kind?: string;
208
+ id?: string;
209
+ };
210
+ };
211
+ }>;
212
+ agents?: {
213
+ list?: Array<{
214
+ id: string;
215
+ workspace?: string;
216
+ agentDir?: string;
217
+ [key: string]: unknown;
218
+ }>;
219
+ [key: string]: unknown;
220
+ };
221
+ broadcast?: Record<string, string[]>;
222
+ secrets?: {
223
+ defaults?: {
224
+ env?: string;
225
+ file?: string;
226
+ exec?: string;
227
+ };
228
+ providers?: Record<string, {
229
+ source?: SecretRefSource;
230
+ path?: string;
231
+ mode?: "singleValue" | "json";
232
+ command?: string;
233
+ args?: string[];
234
+ [key: string]: unknown;
235
+ }>;
236
+ [key: string]: unknown;
237
+ };
238
+ [key: string]: unknown;
239
+ };
240
+ type ClawdbotConfig = OpenClawConfig;
241
+ //#endregion
242
+ export { AnyAgentTool, BaseProbeResult, ChannelMeta, ChannelOnboardingAdapter, ChannelOnboardingDmPolicy, ChannelPlugin, ClawdbotConfig, DmPolicy, OpenClawConfig, OpenClawPluginApi, PluginRuntime, RuntimeEnv, WizardPrompter };
@@ -0,0 +1,171 @@
1
+ import { getAllKnownScopes } from "./tool-scopes.js";
2
+ import { getTicket } from "./lark-ticket.js";
3
+ import { feishuFetch } from "./feishu-fetch.js";
4
+ import { pollDeviceToken, requestDeviceAuthorization } from "./device-flow.js";
5
+ import { getStoredToken, setStoredToken, tokenStatus } from "./token-store.js";
6
+ import { revokeUAT } from "./uat-client.js";
7
+ import { createUserToolClient } from "./user-tool-client.js";
8
+ import { jsonToolResult } from "./tool-result.js";
9
+ import { resolveRegisteredFeishuToolsConfig } from "./tool-account.js";
10
+ import { Type } from "@sinclair/typebox";
11
+ //#region src/oauth.ts
12
+ const FeishuOAuthSchema = Type.Object({
13
+ action: Type.Union([
14
+ Type.Literal("authorize"),
15
+ Type.Literal("status"),
16
+ Type.Literal("revoke")
17
+ ]),
18
+ scope: Type.Optional(Type.String({ description: "可选,自定义 scope 空格串;不传时默认申请当前集成能力需要的全部高价值 scope。" })),
19
+ wait_seconds: Type.Optional(Type.Integer({
20
+ description: "authorize 后轮询等待秒数,默认 15,最大 60。",
21
+ minimum: 0,
22
+ maximum: 60
23
+ }))
24
+ });
25
+ function json(data) {
26
+ return jsonToolResult(data);
27
+ }
28
+ async function verifyTokenIdentity(domain, accessToken, expectedOpenId) {
29
+ const data = await (await feishuFetch(`${domain === "lark" ? "https://open.larksuite.com" : "https://open.feishu.cn"}/open-apis/authen/v1/user_info`, { headers: { Authorization: `Bearer ${accessToken}` } })).json();
30
+ const actualOpenId = data.data?.open_id;
31
+ return {
32
+ valid: data.code === 0 && actualOpenId === expectedOpenId,
33
+ actualOpenId
34
+ };
35
+ }
36
+ async function handleOAuthStatus(params) {
37
+ const stored = await getStoredToken(params.appId, params.senderOpenId);
38
+ if (!stored) return json({
39
+ authorized: false,
40
+ user_open_id: params.senderOpenId
41
+ });
42
+ return json({
43
+ authorized: true,
44
+ user_open_id: params.senderOpenId,
45
+ scope: stored.scope,
46
+ token_status: tokenStatus(stored),
47
+ granted_at: new Date(stored.grantedAt).toISOString(),
48
+ expires_at: new Date(stored.expiresAt).toISOString(),
49
+ refresh_expires_at: new Date(stored.refreshExpiresAt).toISOString()
50
+ });
51
+ }
52
+ async function handleOAuthRevoke(params) {
53
+ await revokeUAT(params.appId, params.senderOpenId);
54
+ return json({
55
+ success: true,
56
+ user_open_id: params.senderOpenId,
57
+ message: "当前用户的飞书 OAuth 授权已撤销。"
58
+ });
59
+ }
60
+ async function handleOAuthAuthorize(params) {
61
+ const scope = params.scope?.trim() || getAllKnownScopes().join(" ");
62
+ const waitSeconds = Math.min(Math.max(params.waitSeconds ?? 15, 0), 60);
63
+ const deviceFlow = await requestDeviceAuthorization({
64
+ appId: params.account.appId,
65
+ appSecret: params.account.appSecret,
66
+ domain: params.account.domain,
67
+ scope
68
+ });
69
+ const controller = new AbortController();
70
+ const timeout = setTimeout(() => controller.abort(), waitSeconds * 1e3);
71
+ try {
72
+ const result = await pollDeviceToken({
73
+ appId: params.account.appId,
74
+ appSecret: params.account.appSecret,
75
+ domain: params.account.domain,
76
+ deviceCode: deviceFlow.deviceCode,
77
+ interval: deviceFlow.interval,
78
+ expiresIn: deviceFlow.expiresIn,
79
+ signal: controller.signal
80
+ });
81
+ if (!result.ok) return json({
82
+ authorized: false,
83
+ status: result.error,
84
+ message: result.message,
85
+ user_open_id: params.senderOpenId,
86
+ verification_uri: deviceFlow.verificationUri,
87
+ verification_uri_complete: deviceFlow.verificationUriComplete,
88
+ user_code: deviceFlow.userCode,
89
+ requested_scope: scope
90
+ });
91
+ const verified = await verifyTokenIdentity(params.account.domain, result.token.accessToken, params.senderOpenId);
92
+ if (!verified.valid) return json({
93
+ authorized: false,
94
+ status: "identity_mismatch",
95
+ message: "完成授权的飞书账号与当前消息发送者不一致,已拒绝写入令牌。",
96
+ expected_open_id: params.senderOpenId,
97
+ actual_open_id: verified.actualOpenId
98
+ });
99
+ const now = Date.now();
100
+ await setStoredToken({
101
+ userOpenId: params.senderOpenId,
102
+ appId: params.account.appId,
103
+ accessToken: result.token.accessToken,
104
+ refreshToken: result.token.refreshToken,
105
+ expiresAt: now + result.token.expiresIn * 1e3,
106
+ refreshExpiresAt: now + result.token.refreshExpiresIn * 1e3,
107
+ scope: result.token.scope,
108
+ grantedAt: now
109
+ });
110
+ return json({
111
+ authorized: true,
112
+ user_open_id: params.senderOpenId,
113
+ scope: result.token.scope,
114
+ expires_at: new Date(now + result.token.expiresIn * 1e3).toISOString(),
115
+ message: "飞书 OAuth 授权成功,后续工具将按当前用户身份执行。"
116
+ });
117
+ } catch (error) {
118
+ if (!controller.signal.aborted) throw error;
119
+ return json({
120
+ authorized: false,
121
+ status: "authorization_pending",
122
+ message: "授权请求已创建,但在等待窗口内尚未完成。请打开链接完成授权后重新调用 feishu_oauth status 或再次 authorize。",
123
+ user_open_id: params.senderOpenId,
124
+ verification_uri: deviceFlow.verificationUri,
125
+ verification_uri_complete: deviceFlow.verificationUriComplete,
126
+ user_code: deviceFlow.userCode,
127
+ requested_scope: scope
128
+ });
129
+ } finally {
130
+ clearTimeout(timeout);
131
+ }
132
+ }
133
+ function registerFeishuOAuthTool(api) {
134
+ if (!api.config) return;
135
+ if (!resolveRegisteredFeishuToolsConfig(api.config).oauth) return;
136
+ api.registerTool({
137
+ name: "feishu_oauth",
138
+ label: "Feishu OAuth",
139
+ description: "管理当前消息发送者的飞书 OAuth 授权。支持 authorize/status/revoke,授权后 calendar/task/sheets/identity 工具即可按本人身份执行。",
140
+ parameters: FeishuOAuthSchema,
141
+ async execute(_toolCallId, params) {
142
+ const payload = params;
143
+ const senderOpenId = getTicket()?.senderOpenId;
144
+ if (!senderOpenId) return json({
145
+ error: "missing_sender_identity",
146
+ message: "当前不在飞书消息上下文中,无法按本人身份管理 OAuth。"
147
+ });
148
+ try {
149
+ const account = createUserToolClient(api.config).account;
150
+ if (payload.action === "status") return handleOAuthStatus({
151
+ appId: account.appId,
152
+ senderOpenId
153
+ });
154
+ if (payload.action === "revoke") return handleOAuthRevoke({
155
+ appId: account.appId,
156
+ senderOpenId
157
+ });
158
+ return handleOAuthAuthorize({
159
+ account,
160
+ senderOpenId,
161
+ scope: payload.scope,
162
+ waitSeconds: payload.wait_seconds
163
+ });
164
+ } catch (error) {
165
+ return json({ error: error instanceof Error ? error.message : String(error) });
166
+ }
167
+ }
168
+ }, { name: "feishu_oauth" });
169
+ }
170
+ //#endregion
171
+ export { registerFeishuOAuthTool };