@m1heng-clawd/feishu 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +199 -31
- package/index.ts +3 -1
- package/package.json +4 -4
- package/skills/feishu-doc/SKILL.md +24 -0
- package/src/bitable-tools/actions.ts +199 -0
- package/src/bitable-tools/common.ts +90 -0
- package/src/bitable-tools/meta.ts +80 -0
- package/src/bitable-tools/register.ts +195 -0
- package/src/bitable-tools/schemas.ts +221 -0
- package/src/bitable.ts +1 -441
- package/src/bot.ts +130 -68
- package/src/channel.ts +6 -8
- package/src/config-schema.ts +7 -0
- package/src/dedup.ts +31 -0
- package/src/doc-schema.ts +8 -0
- package/src/doc-write-service.ts +711 -0
- package/src/docx.ts +83 -287
- package/src/drive-schema.ts +20 -0
- package/src/drive.ts +57 -27
- package/src/dynamic-agent.ts +8 -4
- package/src/media.ts +25 -40
- package/src/monitor.ts +29 -2
- package/src/onboarding.ts +196 -105
- package/src/perm.ts +23 -24
- package/src/probe.ts +108 -4
- package/src/reply-dispatcher.ts +137 -73
- package/src/streaming-card.ts +211 -0
- package/src/targets.ts +1 -1
- package/src/task-tools/actions.ts +137 -0
- package/src/task-tools/common.ts +18 -0
- package/src/task-tools/register.ts +101 -0
- package/src/task-tools/schemas.ts +138 -0
- package/src/task.ts +1 -0
- package/src/tools-common/feishu-api.ts +184 -0
- package/src/tools-common/tool-context.ts +23 -0
- package/src/tools-common/tool-exec.ts +73 -0
- package/src/tools-config.ts +2 -1
- package/src/types.ts +1 -0
- package/src/wiki.ts +42 -43
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import type { Client } from "@larksuiteoapi/node-sdk";
|
|
2
|
+
import type { FeishuDomain } from "./types.js";
|
|
3
|
+
|
|
4
|
+
type Credentials = { appId: string; appSecret: string; domain?: FeishuDomain };
|
|
5
|
+
type CardState = { cardId: string; messageId: string; sequence: number; currentText: string };
|
|
6
|
+
|
|
7
|
+
const tokenCache = new Map<string, { token: string; expiresAt: number }>();
|
|
8
|
+
|
|
9
|
+
function resolveApiBase(domain?: FeishuDomain): string {
|
|
10
|
+
if (domain === "lark") {
|
|
11
|
+
return "https://open.larksuite.com/open-apis";
|
|
12
|
+
}
|
|
13
|
+
if (domain && domain !== "feishu" && domain.startsWith("http")) {
|
|
14
|
+
return `${domain.replace(/\/+$/, "")}/open-apis`;
|
|
15
|
+
}
|
|
16
|
+
return "https://open.feishu.cn/open-apis";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function getToken(creds: Credentials): Promise<string> {
|
|
20
|
+
const key = `${creds.domain ?? "feishu"}|${creds.appId}`;
|
|
21
|
+
const cached = tokenCache.get(key);
|
|
22
|
+
if (cached && cached.expiresAt > Date.now() + 60000) {
|
|
23
|
+
return cached.token;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const res = await fetch(`${resolveApiBase(creds.domain)}/auth/v3/tenant_access_token/internal`, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: { "Content-Type": "application/json" },
|
|
29
|
+
body: JSON.stringify({ app_id: creds.appId, app_secret: creds.appSecret }),
|
|
30
|
+
});
|
|
31
|
+
const data = (await res.json()) as {
|
|
32
|
+
code: number;
|
|
33
|
+
msg: string;
|
|
34
|
+
tenant_access_token?: string;
|
|
35
|
+
expire?: number;
|
|
36
|
+
};
|
|
37
|
+
if (data.code !== 0 || !data.tenant_access_token) {
|
|
38
|
+
throw new Error(`Token error: ${data.msg}`);
|
|
39
|
+
}
|
|
40
|
+
tokenCache.set(key, {
|
|
41
|
+
token: data.tenant_access_token,
|
|
42
|
+
expiresAt: Date.now() + (data.expire ?? 7200) * 1000,
|
|
43
|
+
});
|
|
44
|
+
return data.tenant_access_token;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function truncateSummary(text: string, max = 50): string {
|
|
48
|
+
if (!text) {
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
const clean = text.replace(/\n/g, " ").trim();
|
|
52
|
+
return clean.length <= max ? clean : clean.slice(0, max - 3) + "...";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class FeishuStreamingSession {
|
|
56
|
+
private client: Client;
|
|
57
|
+
private creds: Credentials;
|
|
58
|
+
private state: CardState | null = null;
|
|
59
|
+
private queue: Promise<void> = Promise.resolve();
|
|
60
|
+
private closed = false;
|
|
61
|
+
private log?: (msg: string) => void;
|
|
62
|
+
private lastUpdateTime = 0;
|
|
63
|
+
private pendingText: string | null = null;
|
|
64
|
+
private updateThrottleMs = 100;
|
|
65
|
+
|
|
66
|
+
constructor(client: Client, creds: Credentials, log?: (msg: string) => void) {
|
|
67
|
+
this.client = client;
|
|
68
|
+
this.creds = creds;
|
|
69
|
+
this.log = log;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async start(
|
|
73
|
+
receiveId: string,
|
|
74
|
+
receiveIdType: "open_id" | "user_id" | "union_id" | "email" | "chat_id" = "chat_id",
|
|
75
|
+
): Promise<void> {
|
|
76
|
+
if (this.state) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const apiBase = resolveApiBase(this.creds.domain);
|
|
81
|
+
const cardJson = {
|
|
82
|
+
schema: "2.0",
|
|
83
|
+
config: {
|
|
84
|
+
streaming_mode: true,
|
|
85
|
+
summary: { content: "[Generating...]" },
|
|
86
|
+
streaming_config: { print_frequency_ms: { default: 50 }, print_step: { default: 2 } },
|
|
87
|
+
},
|
|
88
|
+
body: {
|
|
89
|
+
elements: [{ tag: "markdown", content: "Thinking...", element_id: "content" }],
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const createRes = await fetch(`${apiBase}/cardkit/v1/cards`, {
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers: {
|
|
96
|
+
Authorization: `Bearer ${await getToken(this.creds)}`,
|
|
97
|
+
"Content-Type": "application/json",
|
|
98
|
+
},
|
|
99
|
+
body: JSON.stringify({ type: "card_json", data: JSON.stringify(cardJson) }),
|
|
100
|
+
});
|
|
101
|
+
const createData = (await createRes.json()) as {
|
|
102
|
+
code: number;
|
|
103
|
+
msg: string;
|
|
104
|
+
data?: { card_id: string };
|
|
105
|
+
};
|
|
106
|
+
if (createData.code !== 0 || !createData.data?.card_id) {
|
|
107
|
+
throw new Error(`Create card failed: ${createData.msg}`);
|
|
108
|
+
}
|
|
109
|
+
const cardId = createData.data.card_id;
|
|
110
|
+
|
|
111
|
+
const sendRes = await this.client.im.message.create({
|
|
112
|
+
params: { receive_id_type: receiveIdType },
|
|
113
|
+
data: {
|
|
114
|
+
receive_id: receiveId,
|
|
115
|
+
msg_type: "interactive",
|
|
116
|
+
content: JSON.stringify({ type: "card", data: { card_id: cardId } }),
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
if (sendRes.code !== 0 || !sendRes.data?.message_id) {
|
|
120
|
+
throw new Error(`Send card failed: ${sendRes.msg}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this.state = { cardId, messageId: sendRes.data.message_id, sequence: 1, currentText: "" };
|
|
124
|
+
this.log?.(`Started streaming: cardId=${cardId}, messageId=${sendRes.data.message_id}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async update(text: string): Promise<void> {
|
|
128
|
+
if (!this.state || this.closed) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
if (now - this.lastUpdateTime < this.updateThrottleMs) {
|
|
133
|
+
this.pendingText = text;
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this.pendingText = null;
|
|
137
|
+
this.lastUpdateTime = now;
|
|
138
|
+
|
|
139
|
+
this.queue = this.queue.then(async () => {
|
|
140
|
+
if (!this.state || this.closed) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
this.state.currentText = text;
|
|
144
|
+
this.state.sequence += 1;
|
|
145
|
+
const apiBase = resolveApiBase(this.creds.domain);
|
|
146
|
+
await fetch(`${apiBase}/cardkit/v1/cards/${this.state.cardId}/elements/content/content`, {
|
|
147
|
+
method: "PUT",
|
|
148
|
+
headers: {
|
|
149
|
+
Authorization: `Bearer ${await getToken(this.creds)}`,
|
|
150
|
+
"Content-Type": "application/json",
|
|
151
|
+
},
|
|
152
|
+
body: JSON.stringify({
|
|
153
|
+
content: text,
|
|
154
|
+
sequence: this.state.sequence,
|
|
155
|
+
uuid: `s_${this.state.cardId}_${this.state.sequence}`,
|
|
156
|
+
}),
|
|
157
|
+
}).catch((e) => this.log?.(`Update failed: ${String(e)}`));
|
|
158
|
+
});
|
|
159
|
+
await this.queue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async close(finalText?: string): Promise<void> {
|
|
163
|
+
if (!this.state || this.closed) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
this.closed = true;
|
|
167
|
+
await this.queue;
|
|
168
|
+
|
|
169
|
+
const text = finalText ?? this.pendingText ?? this.state.currentText;
|
|
170
|
+
const apiBase = resolveApiBase(this.creds.domain);
|
|
171
|
+
|
|
172
|
+
if (text && text !== this.state.currentText) {
|
|
173
|
+
this.state.sequence += 1;
|
|
174
|
+
await fetch(`${apiBase}/cardkit/v1/cards/${this.state.cardId}/elements/content/content`, {
|
|
175
|
+
method: "PUT",
|
|
176
|
+
headers: {
|
|
177
|
+
Authorization: `Bearer ${await getToken(this.creds)}`,
|
|
178
|
+
"Content-Type": "application/json",
|
|
179
|
+
},
|
|
180
|
+
body: JSON.stringify({
|
|
181
|
+
content: text,
|
|
182
|
+
sequence: this.state.sequence,
|
|
183
|
+
uuid: `s_${this.state.cardId}_${this.state.sequence}`,
|
|
184
|
+
}),
|
|
185
|
+
}).catch(() => {});
|
|
186
|
+
this.state.currentText = text;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
this.state.sequence += 1;
|
|
190
|
+
await fetch(`${apiBase}/cardkit/v1/cards/${this.state.cardId}/settings`, {
|
|
191
|
+
method: "PATCH",
|
|
192
|
+
headers: {
|
|
193
|
+
Authorization: `Bearer ${await getToken(this.creds)}`,
|
|
194
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
195
|
+
},
|
|
196
|
+
body: JSON.stringify({
|
|
197
|
+
settings: JSON.stringify({
|
|
198
|
+
config: { streaming_mode: false, summary: { content: truncateSummary(text) } },
|
|
199
|
+
}),
|
|
200
|
+
sequence: this.state.sequence,
|
|
201
|
+
uuid: `c_${this.state.cardId}_${this.state.sequence}`,
|
|
202
|
+
}),
|
|
203
|
+
}).catch((e) => this.log?.(`Close failed: ${String(e)}`));
|
|
204
|
+
|
|
205
|
+
this.log?.(`Closed streaming: cardId=${this.state.cardId}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
isActive(): boolean {
|
|
209
|
+
return this.state !== null && !this.closed;
|
|
210
|
+
}
|
|
211
|
+
}
|
package/src/targets.ts
CHANGED
|
@@ -45,7 +45,7 @@ export function resolveReceiveIdType(id: string): "chat_id" | "open_id" | "user_
|
|
|
45
45
|
const trimmed = id.trim();
|
|
46
46
|
if (trimmed.startsWith(CHAT_ID_PREFIX)) return "chat_id";
|
|
47
47
|
if (trimmed.startsWith(OPEN_ID_PREFIX)) return "open_id";
|
|
48
|
-
return "
|
|
48
|
+
return "user_id";
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
export function looksLikeFeishuId(raw: string): boolean {
|
|
@@ -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";
|