@m1heng-clawd/feishu 0.1.8 → 0.1.10

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,137 @@
1
+ import type { TaskClient } from "./common.js";
2
+ import type {
3
+ CreateTaskParams,
4
+ GetTaskParams,
5
+ TaskUpdateTask,
6
+ UpdateTaskParams,
7
+ } from "./schemas.js";
8
+ import { runTaskApiCall } from "./common.js";
9
+
10
+ const SUPPORTED_PATCH_FIELDS = new Set<keyof TaskUpdateTask>([
11
+ "summary",
12
+ "description",
13
+ "due",
14
+ "start",
15
+ "extra",
16
+ "completed_at",
17
+ "repeat_rule",
18
+ "mode",
19
+ "is_milestone",
20
+ ]);
21
+
22
+ function omitUndefined<T extends Record<string, unknown>>(obj: T): T {
23
+ return Object.fromEntries(
24
+ Object.entries(obj).filter(([, value]) => value !== undefined),
25
+ ) as T;
26
+ }
27
+
28
+ function inferUpdateFields(task: TaskUpdateTask): string[] {
29
+ return Object.keys(task).filter((field) =>
30
+ SUPPORTED_PATCH_FIELDS.has(field as keyof TaskUpdateTask),
31
+ );
32
+ }
33
+
34
+ function formatTask(task: Record<string, unknown> | undefined) {
35
+ if (!task) return undefined;
36
+ return {
37
+ guid: task.guid,
38
+ task_id: task.task_id,
39
+ summary: task.summary,
40
+ description: task.description,
41
+ status: task.status,
42
+ url: task.url,
43
+ created_at: task.created_at,
44
+ updated_at: task.updated_at,
45
+ completed_at: task.completed_at,
46
+ due: task.due,
47
+ start: task.start,
48
+ is_milestone: task.is_milestone,
49
+ members: task.members,
50
+ tasklists: task.tasklists,
51
+ };
52
+ }
53
+
54
+ export async function createTask(client: TaskClient, params: CreateTaskParams) {
55
+ const res = await runTaskApiCall("task.v2.task.create", () =>
56
+ client.task.v2.task.create({
57
+ data: omitUndefined({
58
+ summary: params.summary,
59
+ description: params.description,
60
+ due: params.due,
61
+ start: params.start,
62
+ extra: params.extra,
63
+ completed_at: params.completed_at,
64
+ members: params.members,
65
+ repeat_rule: params.repeat_rule,
66
+ tasklists: params.tasklists,
67
+ mode: params.mode,
68
+ is_milestone: params.is_milestone,
69
+ }),
70
+ params: omitUndefined({
71
+ user_id_type: params.user_id_type,
72
+ }),
73
+ }),
74
+ );
75
+
76
+ return {
77
+ task: formatTask((res.data?.task ?? undefined) as Record<string, unknown> | undefined),
78
+ };
79
+ }
80
+
81
+ export async function deleteTask(client: TaskClient, taskGuid: string) {
82
+ await runTaskApiCall("task.v2.task.delete", () =>
83
+ client.task.v2.task.delete({
84
+ path: { task_guid: taskGuid },
85
+ }),
86
+ );
87
+
88
+ return {
89
+ success: true,
90
+ task_guid: taskGuid,
91
+ };
92
+ }
93
+
94
+ export async function getTask(client: TaskClient, params: GetTaskParams) {
95
+ const res = await runTaskApiCall("task.v2.task.get", () =>
96
+ client.task.v2.task.get({
97
+ path: { task_guid: params.task_guid },
98
+ params: omitUndefined({
99
+ user_id_type: params.user_id_type,
100
+ }),
101
+ }),
102
+ );
103
+
104
+ return {
105
+ task: formatTask((res.data?.task ?? undefined) as Record<string, unknown> | undefined),
106
+ };
107
+ }
108
+
109
+ export async function updateTask(client: TaskClient, params: UpdateTaskParams) {
110
+ const task = omitUndefined(params.task as Record<string, unknown>) as TaskUpdateTask;
111
+ const updateFields = params.update_fields?.length ? params.update_fields : inferUpdateFields(task);
112
+
113
+ if (Object.keys(task).length === 0) {
114
+ throw new Error("task update payload is empty");
115
+ }
116
+ if (updateFields.length === 0) {
117
+ throw new Error("no valid update_fields provided or inferred from task payload");
118
+ }
119
+
120
+ const res = await runTaskApiCall("task.v2.task.patch", () =>
121
+ client.task.v2.task.patch({
122
+ path: { task_guid: params.task_guid },
123
+ data: {
124
+ task,
125
+ update_fields: updateFields,
126
+ },
127
+ params: omitUndefined({
128
+ user_id_type: params.user_id_type,
129
+ }),
130
+ }),
131
+ );
132
+
133
+ return {
134
+ task: formatTask((res.data?.task ?? undefined) as Record<string, unknown> | undefined),
135
+ update_fields: updateFields,
136
+ };
137
+ }
@@ -0,0 +1,18 @@
1
+ import { createFeishuClient } from "../client.js";
2
+ import {
3
+ errorResult,
4
+ json,
5
+ runFeishuApiCall,
6
+ type FeishuApiResponse,
7
+ } from "../tools-common/feishu-api.js";
8
+
9
+ export type TaskClient = ReturnType<typeof createFeishuClient>;
10
+
11
+ export { json, errorResult };
12
+
13
+ export async function runTaskApiCall<T extends FeishuApiResponse>(
14
+ context: string,
15
+ fn: () => Promise<T>,
16
+ ): Promise<T> {
17
+ return runFeishuApiCall(context, fn);
18
+ }
@@ -0,0 +1,101 @@
1
+ import type { TSchema } from "@sinclair/typebox";
2
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
3
+ import { hasFeishuToolEnabledForAnyAccount, withFeishuToolClient } from "../tools-common/tool-exec.js";
4
+ import { createTask, deleteTask, getTask, updateTask } from "./actions.js";
5
+ import { errorResult, json, type TaskClient } from "./common.js";
6
+ import {
7
+ CreateTaskSchema,
8
+ type CreateTaskParams,
9
+ DeleteTaskSchema,
10
+ type DeleteTaskParams,
11
+ GetTaskSchema,
12
+ type GetTaskParams,
13
+ UpdateTaskSchema,
14
+ type UpdateTaskParams,
15
+ } from "./schemas.js";
16
+
17
+ type ToolSpec<P> = {
18
+ name: string;
19
+ label: string;
20
+ description: string;
21
+ parameters: TSchema;
22
+ run: (client: TaskClient, params: P) => Promise<unknown>;
23
+ };
24
+
25
+ function registerTaskTool<P>(
26
+ api: OpenClawPluginApi,
27
+ spec: ToolSpec<P>,
28
+ ) {
29
+ api.registerTool(
30
+ {
31
+ name: spec.name,
32
+ label: spec.label,
33
+ description: spec.description,
34
+ parameters: spec.parameters,
35
+ async execute(_toolCallId, params) {
36
+ try {
37
+ return await withFeishuToolClient({
38
+ api,
39
+ toolName: spec.name,
40
+ requiredTool: "task",
41
+ run: async ({ client }) => json(await spec.run(client as TaskClient, params as P)),
42
+ });
43
+ } catch (err) {
44
+ return errorResult(err);
45
+ }
46
+ },
47
+ },
48
+ { name: spec.name },
49
+ );
50
+ }
51
+
52
+ export function registerFeishuTaskTools(api: OpenClawPluginApi) {
53
+ if (!api.config) {
54
+ api.logger.debug?.("feishu_task: No config available, skipping task tools");
55
+ return;
56
+ }
57
+
58
+ if (!hasFeishuToolEnabledForAnyAccount(api.config)) {
59
+ api.logger.debug?.("feishu_task: No Feishu accounts configured, skipping task tools");
60
+ return;
61
+ }
62
+
63
+ if (!hasFeishuToolEnabledForAnyAccount(api.config, "task")) {
64
+ api.logger.debug?.("feishu_task: task tools disabled in config");
65
+ return;
66
+ }
67
+
68
+ registerTaskTool<CreateTaskParams>(api, {
69
+ name: "feishu_task_create",
70
+ label: "Feishu Task Create",
71
+ description: "Create a Feishu task (task v2)",
72
+ parameters: CreateTaskSchema,
73
+ run: (client, params) => createTask(client, params),
74
+ });
75
+
76
+ registerTaskTool<DeleteTaskParams>(api, {
77
+ name: "feishu_task_delete",
78
+ label: "Feishu Task Delete",
79
+ description: "Delete a Feishu task by task_guid (task v2)",
80
+ parameters: DeleteTaskSchema,
81
+ run: (client, { task_guid }) => deleteTask(client, task_guid),
82
+ });
83
+
84
+ registerTaskTool<GetTaskParams>(api, {
85
+ name: "feishu_task_get",
86
+ label: "Feishu Task Get",
87
+ description: "Get Feishu task details by task_guid (task v2)",
88
+ parameters: GetTaskSchema,
89
+ run: (client, params) => getTask(client, params),
90
+ });
91
+
92
+ registerTaskTool<UpdateTaskParams>(api, {
93
+ name: "feishu_task_update",
94
+ label: "Feishu Task Update",
95
+ description: "Update a Feishu task by task_guid (task v2 patch)",
96
+ parameters: UpdateTaskSchema,
97
+ run: (client, params) => updateTask(client, params),
98
+ });
99
+
100
+ api.logger.debug?.("feishu_task: Registered 4 task tools");
101
+ }
@@ -0,0 +1,138 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import type { TaskClient } from "./common.js";
3
+
4
+ type TaskCreatePayload = NonNullable<Parameters<TaskClient["task"]["v2"]["task"]["create"]>[0]>;
5
+ type TaskUpdatePayload = NonNullable<Parameters<TaskClient["task"]["v2"]["task"]["patch"]>[0]>;
6
+ type TaskDeletePayload = NonNullable<Parameters<TaskClient["task"]["v2"]["task"]["delete"]>[0]>;
7
+ type TaskGetPayload = NonNullable<Parameters<TaskClient["task"]["v2"]["task"]["get"]>[0]>;
8
+
9
+ export type TaskCreateData = TaskCreatePayload["data"];
10
+ export type TaskUpdateData = TaskUpdatePayload["data"];
11
+ export type TaskUpdateTask = NonNullable<TaskUpdateData["task"]>;
12
+
13
+ export type CreateTaskParams = {
14
+ summary: TaskCreateData["summary"];
15
+ description?: TaskCreateData["description"];
16
+ due?: TaskCreateData["due"];
17
+ start?: TaskCreateData["start"];
18
+ extra?: TaskCreateData["extra"];
19
+ completed_at?: TaskCreateData["completed_at"];
20
+ members?: TaskCreateData["members"];
21
+ repeat_rule?: TaskCreateData["repeat_rule"];
22
+ tasklists?: TaskCreateData["tasklists"];
23
+ mode?: TaskCreateData["mode"];
24
+ is_milestone?: TaskCreateData["is_milestone"];
25
+ user_id_type?: NonNullable<TaskCreatePayload["params"]>["user_id_type"];
26
+ };
27
+
28
+ export type DeleteTaskParams = {
29
+ task_guid: TaskDeletePayload["path"]["task_guid"];
30
+ };
31
+
32
+ export type GetTaskParams = {
33
+ task_guid: TaskGetPayload["path"]["task_guid"];
34
+ user_id_type?: NonNullable<TaskGetPayload["params"]>["user_id_type"];
35
+ };
36
+
37
+ export type UpdateTaskParams = {
38
+ task_guid: TaskUpdatePayload["path"]["task_guid"];
39
+ task: TaskUpdateTask;
40
+ update_fields?: TaskUpdateData["update_fields"];
41
+ user_id_type?: NonNullable<TaskUpdatePayload["params"]>["user_id_type"];
42
+ };
43
+
44
+ const TaskDateSchema = Type.Object({
45
+ timestamp: Type.Optional(
46
+ Type.String({
47
+ description:
48
+ "Unix timestamp in milliseconds (string), e.g. \"1735689600000\" (13-digit ms)",
49
+ }),
50
+ ),
51
+ is_all_day: Type.Optional(Type.Boolean({ description: "Whether this is an all-day date" })),
52
+ });
53
+
54
+ const TaskMemberSchema = Type.Object({
55
+ id: Type.String({ description: "Member ID (with type controlled by user_id_type)" }),
56
+ type: Type.Optional(Type.String({ description: "Member type (usually \"user\")" })),
57
+ role: Type.String({ description: "Member role, e.g. \"assignee\"" }),
58
+ name: Type.Optional(Type.String({ description: "Optional display name" })),
59
+ });
60
+
61
+ const TasklistRefSchema = Type.Object({
62
+ tasklist_guid: Type.Optional(Type.String({ description: "Tasklist GUID" })),
63
+ section_guid: Type.Optional(Type.String({ description: "Section GUID in tasklist" })),
64
+ });
65
+
66
+ export const CreateTaskSchema = Type.Object({
67
+ summary: Type.String({ description: "Task title/summary" }),
68
+ description: Type.Optional(Type.String({ description: "Task description" })),
69
+ due: Type.Optional(TaskDateSchema),
70
+ start: Type.Optional(TaskDateSchema),
71
+ extra: Type.Optional(Type.String({ description: "Custom opaque metadata string" })),
72
+ completed_at: Type.Optional(
73
+ Type.String({
74
+ description: "Completion time as Unix timestamp in milliseconds (string, 13-digit ms)",
75
+ }),
76
+ ),
77
+ members: Type.Optional(Type.Array(TaskMemberSchema, { description: "Initial task members" })),
78
+ repeat_rule: Type.Optional(Type.String({ description: "Task repeat rule" })),
79
+ tasklists: Type.Optional(
80
+ Type.Array(TasklistRefSchema, { description: "Attach the task to tasklists/sections" }),
81
+ ),
82
+ mode: Type.Optional(Type.Number({ description: "Task mode value from Feishu Task API" })),
83
+ is_milestone: Type.Optional(Type.Boolean({ description: "Whether task is a milestone" })),
84
+ user_id_type: Type.Optional(
85
+ Type.String({
86
+ description: "User ID type for member IDs, e.g. open_id/user_id/union_id",
87
+ }),
88
+ ),
89
+ });
90
+
91
+ export const DeleteTaskSchema = Type.Object({
92
+ task_guid: Type.String({ description: "Task GUID to delete" }),
93
+ });
94
+
95
+ export const GetTaskSchema = Type.Object({
96
+ task_guid: Type.String({ description: "Task GUID to retrieve" }),
97
+ user_id_type: Type.Optional(
98
+ Type.String({
99
+ description: "User ID type in returned members, e.g. open_id/user_id/union_id",
100
+ }),
101
+ ),
102
+ });
103
+
104
+ const TaskUpdateContentSchema = Type.Object(
105
+ {
106
+ summary: Type.Optional(Type.String({ description: "Updated summary" })),
107
+ description: Type.Optional(Type.String({ description: "Updated description" })),
108
+ due: Type.Optional(TaskDateSchema),
109
+ start: Type.Optional(TaskDateSchema),
110
+ extra: Type.Optional(Type.String({ description: "Updated extra metadata" })),
111
+ completed_at: Type.Optional(
112
+ Type.String({
113
+ description: "Updated completion time (Unix timestamp in milliseconds, string, 13-digit ms)",
114
+ }),
115
+ ),
116
+ repeat_rule: Type.Optional(Type.String({ description: "Updated repeat rule" })),
117
+ mode: Type.Optional(Type.Number({ description: "Updated task mode" })),
118
+ is_milestone: Type.Optional(Type.Boolean({ description: "Updated milestone flag" })),
119
+ },
120
+ { minProperties: 1 },
121
+ );
122
+
123
+ export const UpdateTaskSchema = Type.Object({
124
+ task_guid: Type.String({ description: "Task GUID to update" }),
125
+ task: TaskUpdateContentSchema,
126
+ update_fields: Type.Optional(
127
+ Type.Array(Type.String(), {
128
+ description:
129
+ "Fields to update. If omitted, this tool infers from keys in task (e.g. summary, description, due, start)",
130
+ minItems: 1,
131
+ }),
132
+ ),
133
+ user_id_type: Type.Optional(
134
+ Type.String({
135
+ description: "User ID type when task body contains user-related fields",
136
+ }),
137
+ ),
138
+ });
package/src/task.ts ADDED
@@ -0,0 +1 @@
1
+ export { registerFeishuTaskTools } from "./task-tools/register.js";
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Minimal response shape shared by Feishu OpenAPI endpoints.
3
+ * Most endpoints return success when `code` is `0` (or omitted).
4
+ */
5
+ export type FeishuApiResponse = {
6
+ code?: number;
7
+ msg?: string;
8
+ log_id?: string;
9
+ logId?: string;
10
+ };
11
+
12
+ type FeishuErrorInfo = {
13
+ code?: number;
14
+ msg?: string;
15
+ logId?: string;
16
+ };
17
+
18
+ type RunFeishuApiCallOptions = {
19
+ /** Feishu error codes that should be treated as transient and retried. */
20
+ retryableCodes?: Iterable<number>;
21
+ /** Retry delays in milliseconds. Number of entries controls retry attempts. */
22
+ backoffMs?: number[];
23
+ };
24
+
25
+ /**
26
+ * Standard tool result payload:
27
+ * - `content` for model-visible text output
28
+ * - `details` for structured downstream access
29
+ */
30
+ export function json(data: unknown) {
31
+ return {
32
+ content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
33
+ details: data,
34
+ };
35
+ }
36
+
37
+ /** Convert any thrown value into the standard JSON error envelope. */
38
+ export function errorResult(err: unknown) {
39
+ return json({ error: err instanceof Error ? err.message : String(err) });
40
+ }
41
+
42
+ /** Small async sleep utility used by retry backoff. */
43
+ function sleep(ms: number) {
44
+ return new Promise((resolve) => setTimeout(resolve, ms));
45
+ }
46
+
47
+ /**
48
+ * Extract Feishu error fields (`code`, `msg`, `log_id`) from different throw shapes.
49
+ * Handles nested SDK error arrays and axios-style `response.data`.
50
+ */
51
+ function extractFeishuErrorInfo(err: unknown): FeishuErrorInfo | null {
52
+ if (!err) return null;
53
+
54
+ // Feishu SDK may throw nested array structures like:
55
+ // [axiosError, { code, msg, log_id, ... }]
56
+ if (Array.isArray(err)) {
57
+ for (let i = err.length - 1; i >= 0; i -= 1) {
58
+ const info = extractFeishuErrorInfo(err[i]);
59
+ if (info) return info;
60
+ }
61
+ return null;
62
+ }
63
+
64
+ if (typeof err !== "object") return null;
65
+
66
+ const obj = err as Record<string, unknown>;
67
+ const codeValue = obj.code;
68
+ const msgValue = obj.msg ?? obj.message;
69
+ const logIdValue = obj.log_id ?? obj.logId;
70
+
71
+ const hasCode = typeof codeValue === "number";
72
+ const hasMsg = typeof msgValue === "string";
73
+ const hasLogId = typeof logIdValue === "string";
74
+
75
+ if (hasCode || hasMsg || hasLogId) {
76
+ return {
77
+ code: hasCode ? codeValue : undefined,
78
+ msg: hasMsg ? (msgValue as string) : undefined,
79
+ logId: hasLogId ? (logIdValue as string) : undefined,
80
+ };
81
+ }
82
+
83
+ const responseData = (obj.response as { data?: unknown } | undefined)?.data;
84
+ if (responseData) return extractFeishuErrorInfo(responseData);
85
+
86
+ return null;
87
+ }
88
+
89
+ function assertFeishuOk<T extends FeishuApiResponse>(response: T, context: string): T {
90
+ if (response.code === undefined || response.code === 0) return response;
91
+
92
+ const message = response.msg || `code ${response.code}`;
93
+ const detail = response.log_id ?? response.logId;
94
+ const error = new Error(
95
+ detail
96
+ ? `${context} failed: ${message}, code=${response.code}, log_id=${detail}`
97
+ : `${context} failed: ${message}, code=${response.code}`,
98
+ ) as Error & { code?: number; log_id?: string; logId?: string };
99
+ error.code = response.code;
100
+ if (detail) {
101
+ error.log_id = detail;
102
+ error.logId = detail;
103
+ }
104
+ throw error;
105
+ }
106
+
107
+ /**
108
+ * Normalize unknown errors to a readable, context-aware Error message.
109
+ * Preserves Feishu `code/log_id` details when available.
110
+ */
111
+ function toError(err: unknown, context: string): Error {
112
+ if (err instanceof Error) {
113
+ const info = extractFeishuErrorInfo(err);
114
+ if (!info) return err;
115
+ const details = [
116
+ info.msg || `code ${info.code}`,
117
+ info.code !== undefined ? `code=${info.code}` : undefined,
118
+ info.logId ? `log_id=${info.logId}` : undefined,
119
+ ]
120
+ .filter(Boolean)
121
+ .join(", ");
122
+ return new Error(`${context} failed: ${details}`);
123
+ }
124
+
125
+ const info = extractFeishuErrorInfo(err);
126
+ if (info) {
127
+ const details = [
128
+ info.msg || `code ${info.code}`,
129
+ info.code !== undefined ? `code=${info.code}` : undefined,
130
+ info.logId ? `log_id=${info.logId}` : undefined,
131
+ ]
132
+ .filter(Boolean)
133
+ .join(", ");
134
+ return new Error(`${context} failed: ${details}`);
135
+ }
136
+
137
+ return new Error(`${context} failed: ${String(err)}`);
138
+ }
139
+
140
+ /**
141
+ * Execute a Feishu API call with shared success/error handling.
142
+ *
143
+ * Behavior:
144
+ * - Treats `code === 0` (or undefined) as success.
145
+ * - Converts non-zero responses and thrown values into normalized Errors.
146
+ * - Optionally retries only for configured transient error codes.
147
+ *
148
+ * Retry model:
149
+ * - Attempts = `backoffMs.length + 1`
150
+ * - Delay before each retry uses the corresponding `backoffMs` entry.
151
+ */
152
+ export async function runFeishuApiCall<T extends FeishuApiResponse>(
153
+ context: string,
154
+ fn: () => Promise<T>,
155
+ options?: RunFeishuApiCallOptions,
156
+ ): Promise<T> {
157
+ const retryableCodes = new Set(options?.retryableCodes ?? []);
158
+ const backoffMs = options?.backoffMs ?? [];
159
+ const maxAttempts = backoffMs.length + 1;
160
+ let attempt = 0;
161
+ let lastErr: unknown = null;
162
+
163
+ while (attempt < maxAttempts) {
164
+ try {
165
+ const response = await fn();
166
+ return assertFeishuOk(response, context);
167
+ } catch (err) {
168
+ lastErr = err;
169
+ const info = extractFeishuErrorInfo(err);
170
+ const retryable =
171
+ retryableCodes.size > 0 && info?.code !== undefined && retryableCodes.has(info.code);
172
+ const exhausted = attempt >= maxAttempts - 1;
173
+ if (!retryable || exhausted) {
174
+ throw toError(err, context);
175
+ }
176
+
177
+ const waitMs = backoffMs[Math.min(attempt, backoffMs.length - 1)];
178
+ await sleep(waitMs);
179
+ attempt += 1;
180
+ }
181
+ }
182
+
183
+ throw toError(lastErr, context);
184
+ }
@@ -0,0 +1,23 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+
3
+ export type FeishuToolContext = {
4
+ channel: "feishu";
5
+ accountId: string;
6
+ sessionKey?: string;
7
+ };
8
+
9
+ const toolContextStorage = new AsyncLocalStorage<FeishuToolContext>();
10
+
11
+ export function runWithFeishuToolContext<T>(
12
+ context: FeishuToolContext,
13
+ fn: () => T,
14
+ ): T {
15
+ // Propagate the active Feishu account through async boundaries so tool execution
16
+ // can resolve the correct account without changing OpenClaw core APIs.
17
+ return toolContextStorage.run(context, fn);
18
+ }
19
+
20
+ export function getCurrentFeishuToolContext(): FeishuToolContext | undefined {
21
+ // Returns undefined when execution is outside a message-dispatch context.
22
+ return toolContextStorage.getStore();
23
+ }
@@ -0,0 +1,73 @@
1
+ import type * as Lark from "@larksuiteoapi/node-sdk";
2
+ import type { ClawdbotConfig, OpenClawPluginApi } from "openclaw/plugin-sdk";
3
+ import {
4
+ listEnabledFeishuAccounts,
5
+ resolveDefaultFeishuAccountId,
6
+ resolveFeishuAccount,
7
+ } from "../accounts.js";
8
+ import { createFeishuClient } from "../client.js";
9
+ import { resolveToolsConfig } from "../tools-config.js";
10
+ import { getCurrentFeishuToolContext } from "./tool-context.js";
11
+ import type { FeishuToolsConfig, ResolvedFeishuAccount } from "../types.js";
12
+
13
+ export type FeishuToolFlag = keyof Required<FeishuToolsConfig>;
14
+
15
+ export function hasFeishuToolEnabledForAnyAccount(
16
+ cfg: ClawdbotConfig,
17
+ requiredTool?: FeishuToolFlag,
18
+ ): boolean {
19
+ // Tool registration is global (one definition), so we only need to know whether
20
+ // at least one enabled account can use the tool.
21
+ const accounts = listEnabledFeishuAccounts(cfg);
22
+ if (accounts.length === 0) {
23
+ return false;
24
+ }
25
+ if (!requiredTool) {
26
+ return true;
27
+ }
28
+ return accounts.some((account) => resolveToolsConfig(account.config.tools)[requiredTool]);
29
+ }
30
+
31
+ export function resolveToolAccount(cfg: ClawdbotConfig): ResolvedFeishuAccount {
32
+ const context = getCurrentFeishuToolContext();
33
+ if (context?.channel === "feishu" && context.accountId) {
34
+ // Message-driven path: use the account from AsyncLocalStorage context.
35
+ return resolveFeishuAccount({ cfg, accountId: context.accountId });
36
+ }
37
+ // Non-session path (e.g. background/manual invocation): fall back to default account.
38
+ return resolveFeishuAccount({ cfg, accountId: resolveDefaultFeishuAccountId(cfg) });
39
+ }
40
+
41
+ export async function withFeishuToolClient<T>(params: {
42
+ api: OpenClawPluginApi;
43
+ toolName: string;
44
+ requiredTool?: FeishuToolFlag;
45
+ run: (args: { client: Lark.Client; account: ResolvedFeishuAccount }) => Promise<T>;
46
+ }): Promise<T> {
47
+ if (!params.api.config) {
48
+ throw new Error("Feishu config is not available");
49
+ }
50
+
51
+ // Resolve account at execution time (not registration time).
52
+ const account = resolveToolAccount(params.api.config);
53
+
54
+ if (!account.enabled) {
55
+ throw new Error(`Feishu account "${account.accountId}" is disabled`);
56
+ }
57
+ if (!account.configured) {
58
+ throw new Error(`Feishu account "${account.accountId}" is not configured`);
59
+ }
60
+
61
+ if (params.requiredTool) {
62
+ // Enforce per-account tool toggles, even though the tool is registered globally.
63
+ const toolsCfg = resolveToolsConfig(account.config.tools);
64
+ if (!toolsCfg[params.requiredTool]) {
65
+ throw new Error(
66
+ `Feishu tool "${params.toolName}" is disabled for account "${account.accountId}"`,
67
+ );
68
+ }
69
+ }
70
+
71
+ const client = createFeishuClient(account);
72
+ return params.run({ client, account });
73
+ }
@@ -2,7 +2,7 @@ import type { FeishuToolsConfig } from "./types.js";
2
2
 
3
3
  /**
4
4
  * Default tool configuration.
5
- * - doc, wiki, drive, scopes: enabled by default
5
+ * - doc, wiki, drive, scopes, task: enabled by default
6
6
  * - perm: disabled by default (sensitive operation)
7
7
  */
8
8
  export const DEFAULT_TOOLS_CONFIG: Required<FeishuToolsConfig> = {
@@ -11,6 +11,7 @@ export const DEFAULT_TOOLS_CONFIG: Required<FeishuToolsConfig> = {
11
11
  drive: true,
12
12
  perm: false,
13
13
  scopes: true,
14
+ task: true,
14
15
  };
15
16
 
16
17
  /**