@m1heng-clawd/feishu 0.1.15 → 0.1.17
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 +28 -0
- package/index.ts +4 -0
- package/package.json +2 -1
- package/skills/feishu-message/SKILL.md +111 -0
- package/skills/feishu-reaction/SKILL.md +120 -0
- package/src/bot.ts +4 -0
- package/src/channel.ts +2 -0
- package/src/chat-tools/actions.ts +102 -33
- package/src/chat-tools/schemas.ts +57 -62
- package/src/config-schema.ts +2 -0
- package/src/doc-tools/actions.ts +63 -14
- package/src/doc-tools/schemas.ts +41 -77
- package/src/doc-write-service.ts +19 -0
- package/src/drive-tools/actions.ts +18 -6
- package/src/drive-tools/register.ts +1 -1
- package/src/drive-tools/schemas.ts +45 -58
- package/src/media-duration.ts +43 -8
- package/src/media.ts +31 -53
- package/src/message-tools/actions.ts +154 -0
- package/src/message-tools/index.ts +2 -0
- package/src/message-tools/register.ts +58 -0
- package/src/message-tools/schemas.ts +56 -0
- package/src/outbound.ts +2 -2
- package/src/perm-tools/actions.ts +27 -3
- package/src/perm-tools/schemas.ts +39 -45
- package/src/reaction-tools/actions.ts +127 -0
- package/src/reaction-tools/index.ts +2 -0
- package/src/reaction-tools/register.ts +51 -0
- package/src/reaction-tools/schemas.ts +34 -0
- package/src/reply-dispatcher.ts +23 -33
- package/src/streaming-card.ts +34 -11
- package/src/task-tools/actions.ts +145 -10
- package/src/task-tools/register.ts +56 -1
- package/src/task-tools/schemas.ts +23 -18
- package/src/tools-common/tool-context.ts +2 -0
- package/src/tools-config.ts +2 -0
- package/src/types.ts +4 -0
- package/src/wiki-tools/actions.ts +24 -6
- package/src/wiki-tools/schemas.ts +32 -51
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type * as Lark from "@larksuiteoapi/node-sdk";
|
|
2
|
+
import { runFeishuApiCall, type FeishuApiResponse } from "../tools-common/feishu-api.js";
|
|
3
|
+
import type { FeishuReactionParams } from "./schemas.js";
|
|
4
|
+
|
|
5
|
+
interface ReactionCreateResponse extends FeishuApiResponse {
|
|
6
|
+
data?: { reaction_id?: string };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface ReactionListResponse extends FeishuApiResponse {
|
|
10
|
+
data?: {
|
|
11
|
+
items?: Array<{
|
|
12
|
+
reaction_id?: string;
|
|
13
|
+
reaction_type?: { emoji_type?: string };
|
|
14
|
+
operator_type?: string;
|
|
15
|
+
operator_id?: { open_id?: string; user_id?: string; union_id?: string };
|
|
16
|
+
}>;
|
|
17
|
+
has_more?: boolean;
|
|
18
|
+
page_token?: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function runReactionAction(
|
|
23
|
+
client: Lark.Client,
|
|
24
|
+
params: FeishuReactionParams,
|
|
25
|
+
): Promise<unknown> {
|
|
26
|
+
switch (params.action) {
|
|
27
|
+
case "add":
|
|
28
|
+
return addReaction(client, params);
|
|
29
|
+
case "remove":
|
|
30
|
+
return removeReaction(client, params);
|
|
31
|
+
case "list":
|
|
32
|
+
return listReactions(client, params);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function addReaction(client: Lark.Client, params: FeishuReactionParams) {
|
|
37
|
+
if (!params.emoji_type) {
|
|
38
|
+
throw new Error("emoji_type is required for action=add");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const response = await runFeishuApiCall<ReactionCreateResponse>(
|
|
42
|
+
"Feishu add reaction",
|
|
43
|
+
() =>
|
|
44
|
+
client.im.messageReaction.create({
|
|
45
|
+
path: { message_id: params.message_id },
|
|
46
|
+
data: { reaction_type: { emoji_type: params.emoji_type! } },
|
|
47
|
+
}) as Promise<ReactionCreateResponse>,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
ok: true,
|
|
52
|
+
action: "add",
|
|
53
|
+
message_id: params.message_id,
|
|
54
|
+
emoji_type: params.emoji_type,
|
|
55
|
+
reaction_id: response.data?.reaction_id ?? null,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function removeReaction(client: Lark.Client, params: FeishuReactionParams) {
|
|
60
|
+
if (!params.reaction_id) {
|
|
61
|
+
throw new Error("reaction_id is required for action=remove");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
await runFeishuApiCall<FeishuApiResponse>(
|
|
65
|
+
"Feishu remove reaction",
|
|
66
|
+
() =>
|
|
67
|
+
client.im.messageReaction.delete({
|
|
68
|
+
path: {
|
|
69
|
+
message_id: params.message_id,
|
|
70
|
+
reaction_id: params.reaction_id!,
|
|
71
|
+
},
|
|
72
|
+
}) as Promise<FeishuApiResponse>,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
ok: true,
|
|
77
|
+
action: "remove",
|
|
78
|
+
message_id: params.message_id,
|
|
79
|
+
reaction_id: params.reaction_id,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function listReactions(client: Lark.Client, params: FeishuReactionParams) {
|
|
84
|
+
const allItems: Array<{
|
|
85
|
+
reaction_id: string;
|
|
86
|
+
emoji_type: string;
|
|
87
|
+
operator_type: string;
|
|
88
|
+
operator_id: string;
|
|
89
|
+
}> = [];
|
|
90
|
+
|
|
91
|
+
let pageToken: string | undefined;
|
|
92
|
+
do {
|
|
93
|
+
const response = await runFeishuApiCall<ReactionListResponse>(
|
|
94
|
+
"Feishu list reactions",
|
|
95
|
+
() =>
|
|
96
|
+
client.im.messageReaction.list({
|
|
97
|
+
path: { message_id: params.message_id },
|
|
98
|
+
params: {
|
|
99
|
+
...(params.emoji_type ? { reaction_type: params.emoji_type } : {}),
|
|
100
|
+
...(pageToken ? { page_token: pageToken } : {}),
|
|
101
|
+
page_size: 50,
|
|
102
|
+
},
|
|
103
|
+
}) as Promise<ReactionListResponse>,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
for (const item of response.data?.items ?? []) {
|
|
107
|
+
allItems.push({
|
|
108
|
+
reaction_id: item.reaction_id ?? "",
|
|
109
|
+
emoji_type: item.reaction_type?.emoji_type ?? "",
|
|
110
|
+
operator_type: item.operator_type ?? "user",
|
|
111
|
+
operator_id:
|
|
112
|
+
item.operator_id?.open_id ?? item.operator_id?.user_id ?? item.operator_id?.union_id ?? "",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
pageToken = response.data?.has_more ? (response.data.page_token ?? undefined) : undefined;
|
|
117
|
+
} while (pageToken);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
ok: true,
|
|
121
|
+
action: "list",
|
|
122
|
+
message_id: params.message_id,
|
|
123
|
+
emoji_type_filter: params.emoji_type ?? null,
|
|
124
|
+
total: allItems.length,
|
|
125
|
+
reactions: allItems,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import { hasFeishuToolEnabledForAnyAccount, withFeishuToolClient } from "../tools-common/tool-exec.js";
|
|
3
|
+
import { errorResult, json } from "../tools-common/feishu-api.js";
|
|
4
|
+
import { runReactionAction } from "./actions.js";
|
|
5
|
+
import { FeishuReactionSchema, type FeishuReactionParams } from "./schemas.js";
|
|
6
|
+
|
|
7
|
+
export function registerFeishuReactionTools(api: OpenClawPluginApi) {
|
|
8
|
+
if (!api.config) {
|
|
9
|
+
api.logger.debug?.("feishu_reaction: No config available, skipping");
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (!hasFeishuToolEnabledForAnyAccount(api.config)) {
|
|
14
|
+
api.logger.debug?.("feishu_reaction: No Feishu accounts configured, skipping");
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!hasFeishuToolEnabledForAnyAccount(api.config, "reaction")) {
|
|
19
|
+
api.logger.debug?.("feishu_reaction: reaction tool disabled in config");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
api.registerTool(
|
|
24
|
+
{
|
|
25
|
+
name: "feishu_reaction",
|
|
26
|
+
label: "Feishu Reaction",
|
|
27
|
+
description:
|
|
28
|
+
"Add, remove, or list emoji reactions on Feishu messages. " +
|
|
29
|
+
"Supported actions: add (add an emoji reaction), remove (remove a reaction by ID), list (list all reactions). " +
|
|
30
|
+
"Common emoji types: THUMBSUP, THUMBSDOWN, HEART, SMILE, GRINNING, FIRE, CLAP, OK, CHECK, CROSS, PARTY, PRAY. " +
|
|
31
|
+
"To react to a previous message, first use feishu_message list to find the target message_id.",
|
|
32
|
+
parameters: FeishuReactionSchema,
|
|
33
|
+
async execute(_toolCallId, params) {
|
|
34
|
+
const p = params as FeishuReactionParams;
|
|
35
|
+
try {
|
|
36
|
+
return await withFeishuToolClient({
|
|
37
|
+
api,
|
|
38
|
+
toolName: "feishu_reaction",
|
|
39
|
+
requiredTool: "reaction",
|
|
40
|
+
run: async ({ client }) => json(await runReactionAction(client, p)),
|
|
41
|
+
});
|
|
42
|
+
} catch (err) {
|
|
43
|
+
return errorResult(err);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{ name: "feishu_reaction" },
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
api.logger.debug?.("feishu_reaction: Registered feishu_reaction tool");
|
|
51
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Type, type Static } from "@sinclair/typebox";
|
|
2
|
+
|
|
3
|
+
function stringEnum<T extends readonly string[]>(
|
|
4
|
+
values: T,
|
|
5
|
+
options: { description?: string; default?: T[number] } = {},
|
|
6
|
+
) {
|
|
7
|
+
return Type.Unsafe<T[number]>({ type: "string", enum: [...values], ...options });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const ACTION_VALUES = ["add", "remove", "list"] as const;
|
|
11
|
+
|
|
12
|
+
export const FeishuReactionSchema = Type.Object({
|
|
13
|
+
action: stringEnum(ACTION_VALUES, {
|
|
14
|
+
description:
|
|
15
|
+
"Action to perform: add (add emoji reaction to a message), remove (remove a reaction by reaction_id), list (list all reactions on a message).",
|
|
16
|
+
}),
|
|
17
|
+
message_id: Type.String({
|
|
18
|
+
description: "Feishu message ID (e.g. om_xxx). Use feishu_message list to find message_ids from chat history.",
|
|
19
|
+
}),
|
|
20
|
+
emoji_type: Type.Optional(
|
|
21
|
+
Type.String({
|
|
22
|
+
description:
|
|
23
|
+
"Emoji type to add or filter by (e.g. THUMBSUP, HEART, SMILE, GRINNING, FIRE, CLAP, OK, CHECK, CROSS). " +
|
|
24
|
+
"Required for action=add. Optional filter for action=list.",
|
|
25
|
+
}),
|
|
26
|
+
),
|
|
27
|
+
reaction_id: Type.Optional(
|
|
28
|
+
Type.String({
|
|
29
|
+
description: "Reaction ID to remove. Required for action=remove. Obtained from add or list results.",
|
|
30
|
+
}),
|
|
31
|
+
),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export type FeishuReactionParams = Static<typeof FeishuReactionSchema>;
|
package/src/reply-dispatcher.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { buildMentionedCardContent, type MentionTarget } from "./mention.js";
|
|
|
12
12
|
import { normalizeFeishuMarkdownLinks } from "./text/markdown-links.js";
|
|
13
13
|
import { getFeishuRuntime } from "./runtime.js";
|
|
14
14
|
import { sendMarkdownCardFeishu, sendMessageFeishu } from "./send.js";
|
|
15
|
-
import { FeishuStreamingSession } from "./streaming-card.js";
|
|
15
|
+
import { FeishuStreamingSession, mergeStreamingText } from "./streaming-card.js";
|
|
16
16
|
import { resolveReceiveIdType } from "./targets.js";
|
|
17
17
|
import { addTypingIndicator, removeTypingIndicator, type TypingIndicatorState } from "./typing.js";
|
|
18
18
|
|
|
@@ -119,39 +119,15 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
|
|
119
119
|
let streaming: FeishuStreamingSession | null = null;
|
|
120
120
|
let streamText = "";
|
|
121
121
|
let lastPartial = "";
|
|
122
|
+
let streamingCompleted = false;
|
|
122
123
|
let partialUpdateQueue: Promise<void> = Promise.resolve();
|
|
123
124
|
let streamingStartPromise: Promise<void> | null = null;
|
|
124
125
|
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
streamText = nextText;
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
if (nextText.startsWith(streamText)) {
|
|
131
|
-
// Handle cumulative partial payloads where nextText already includes prior text.
|
|
132
|
-
streamText = nextText;
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
if (streamText.endsWith(nextText)) {
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
streamText += nextText;
|
|
126
|
+
const mergeIntoStreamText = (nextText: string) => {
|
|
127
|
+
streamText = mergeStreamingText(streamText, nextText);
|
|
139
128
|
};
|
|
140
129
|
|
|
141
|
-
const
|
|
142
|
-
nextText: string,
|
|
143
|
-
options?: { dedupeWithLastPartial?: boolean },
|
|
144
|
-
) => {
|
|
145
|
-
if (!nextText) {
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
if (options?.dedupeWithLastPartial && nextText === lastPartial) {
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
if (options?.dedupeWithLastPartial) {
|
|
152
|
-
lastPartial = nextText;
|
|
153
|
-
}
|
|
154
|
-
mergeStreamingText(nextText);
|
|
130
|
+
const enqueueStreamingFlush = () => {
|
|
155
131
|
partialUpdateQueue = partialUpdateQueue.then(async () => {
|
|
156
132
|
if (streamingStartPromise) {
|
|
157
133
|
await streamingStartPromise;
|
|
@@ -163,7 +139,7 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
|
|
163
139
|
};
|
|
164
140
|
|
|
165
141
|
const startStreaming = () => {
|
|
166
|
-
if (!streamingEnabled || streamingStartPromise || streaming) {
|
|
142
|
+
if (!streamingEnabled || streamingStartPromise || streaming || streamingCompleted) {
|
|
167
143
|
return;
|
|
168
144
|
}
|
|
169
145
|
streamingStartPromise = (async () => {
|
|
@@ -198,6 +174,7 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
|
|
198
174
|
text = buildMentionedCardContent(mentionTargets, text);
|
|
199
175
|
}
|
|
200
176
|
await streaming.close(normalizeFeishuMarkdownLinks(text));
|
|
177
|
+
streamingCompleted = true;
|
|
201
178
|
}
|
|
202
179
|
streaming = null;
|
|
203
180
|
streamingStartPromise = null;
|
|
@@ -247,7 +224,8 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
|
|
247
224
|
if (info?.kind === "block") {
|
|
248
225
|
// Some runtimes emit block payloads without onPartial/final callbacks.
|
|
249
226
|
// Mirror block text into streamText so onIdle close still sends content.
|
|
250
|
-
|
|
227
|
+
mergeIntoStreamText(text);
|
|
228
|
+
enqueueStreamingFlush();
|
|
251
229
|
}
|
|
252
230
|
if (info?.kind === "final") {
|
|
253
231
|
streamText = text;
|
|
@@ -256,6 +234,13 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
|
|
256
234
|
return;
|
|
257
235
|
}
|
|
258
236
|
|
|
237
|
+
// Streaming card already delivered the content — skip regular send to
|
|
238
|
+
// avoid a duplicate card when the runtime delivers a second payload
|
|
239
|
+
// after the streaming session has closed (#399).
|
|
240
|
+
if (streamingCompleted) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
259
244
|
let first = true;
|
|
260
245
|
if (useCard) {
|
|
261
246
|
for (const chunk of core.channel.text.chunkTextWithMode(text, textChunkLimit, chunkMode)) {
|
|
@@ -312,10 +297,15 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
|
|
312
297
|
onPartialReply: streamingEnabled
|
|
313
298
|
? (payload: ReplyPayload) => {
|
|
314
299
|
const partialText = normalizeFeishuMarkdownLinks(payload.text ?? "");
|
|
315
|
-
if (!partialText) {
|
|
300
|
+
if (!partialText || partialText === lastPartial) {
|
|
316
301
|
return;
|
|
317
302
|
}
|
|
318
|
-
|
|
303
|
+
lastPartial = partialText;
|
|
304
|
+
// Partials are cumulative — replace streamText directly instead of
|
|
305
|
+
// merging, which avoids duplication when segment boundaries shift
|
|
306
|
+
// (e.g. after a tool call mid-stream).
|
|
307
|
+
streamText = partialText;
|
|
308
|
+
enqueueStreamingFlush();
|
|
319
309
|
}
|
|
320
310
|
: undefined,
|
|
321
311
|
},
|
package/src/streaming-card.ts
CHANGED
|
@@ -44,6 +44,21 @@ async function getToken(creds: Credentials): Promise<string> {
|
|
|
44
44
|
return data.tenant_access_token;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Find the length of the longest suffix of `a` that equals a prefix of `b`.
|
|
49
|
+
* Used to detect partial overlaps between consecutive streaming segments.
|
|
50
|
+
* Capped at 500 chars to keep cost bounded.
|
|
51
|
+
*/
|
|
52
|
+
function findOverlapLength(a: string, b: string): number {
|
|
53
|
+
const max = Math.min(a.length, b.length, 500);
|
|
54
|
+
for (let len = max; len > 0; len--) {
|
|
55
|
+
if (a.endsWith(b.slice(0, len))) {
|
|
56
|
+
return len;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
47
62
|
export function mergeStreamingText(
|
|
48
63
|
previousText: string | undefined,
|
|
49
64
|
nextText: string | undefined,
|
|
@@ -59,7 +74,14 @@ export function mergeStreamingText(
|
|
|
59
74
|
if (previous.includes(next)) {
|
|
60
75
|
return previous;
|
|
61
76
|
}
|
|
62
|
-
//
|
|
77
|
+
// Detect partial overlap: the end of previous matches the start of next.
|
|
78
|
+
// This happens when partials are cumulative within a segment but don't
|
|
79
|
+
// include prior segments (e.g. after a tool call mid-stream).
|
|
80
|
+
const overlap = findOverlapLength(previous, next);
|
|
81
|
+
if (overlap > 0) {
|
|
82
|
+
return previous + next.slice(overlap);
|
|
83
|
+
}
|
|
84
|
+
// No overlap — truly incremental chunk, just append.
|
|
63
85
|
return `${previous}${next}`;
|
|
64
86
|
}
|
|
65
87
|
|
|
@@ -147,30 +169,30 @@ export class FeishuStreamingSession {
|
|
|
147
169
|
}
|
|
148
170
|
|
|
149
171
|
async update(text: string): Promise<void> {
|
|
150
|
-
if (!this.state || this.closed) {
|
|
172
|
+
if (!this.state || this.closed || !text) {
|
|
151
173
|
return;
|
|
152
174
|
}
|
|
153
|
-
|
|
154
|
-
if (
|
|
175
|
+
// Caller sends cumulative full text — no merge needed, just throttle.
|
|
176
|
+
if (text === this.state.currentText) {
|
|
155
177
|
return;
|
|
156
178
|
}
|
|
157
179
|
const now = Date.now();
|
|
158
180
|
if (now - this.lastUpdateTime < this.updateThrottleMs) {
|
|
159
|
-
this.pendingText =
|
|
181
|
+
this.pendingText = text;
|
|
160
182
|
return;
|
|
161
183
|
}
|
|
162
184
|
this.pendingText = null;
|
|
163
185
|
this.lastUpdateTime = now;
|
|
164
186
|
|
|
187
|
+
const toSend = text;
|
|
165
188
|
this.queue = this.queue.then(async () => {
|
|
166
189
|
if (!this.state || this.closed) {
|
|
167
190
|
return;
|
|
168
191
|
}
|
|
169
|
-
|
|
170
|
-
if (!mergedText || mergedText === this.state.currentText) {
|
|
192
|
+
if (toSend === this.state.currentText) {
|
|
171
193
|
return;
|
|
172
194
|
}
|
|
173
|
-
this.state.currentText =
|
|
195
|
+
this.state.currentText = toSend;
|
|
174
196
|
this.state.sequence += 1;
|
|
175
197
|
const apiBase = resolveApiBase(this.creds.domain);
|
|
176
198
|
await fetch(`${apiBase}/cardkit/v1/cards/${this.state.cardId}/elements/content/content`, {
|
|
@@ -180,7 +202,7 @@ export class FeishuStreamingSession {
|
|
|
180
202
|
"Content-Type": "application/json",
|
|
181
203
|
},
|
|
182
204
|
body: JSON.stringify({
|
|
183
|
-
content:
|
|
205
|
+
content: toSend,
|
|
184
206
|
sequence: this.state.sequence,
|
|
185
207
|
uuid: `s_${this.state.cardId}_${this.state.sequence}`,
|
|
186
208
|
}),
|
|
@@ -196,8 +218,9 @@ export class FeishuStreamingSession {
|
|
|
196
218
|
this.closed = true;
|
|
197
219
|
await this.queue;
|
|
198
220
|
|
|
199
|
-
|
|
200
|
-
|
|
221
|
+
// finalText is authoritative when provided (from deliver kind=final).
|
|
222
|
+
// Fall back to pending or current text when closing via onIdle.
|
|
223
|
+
const text = finalText || this.pendingText || this.state.currentText;
|
|
201
224
|
const apiBase = resolveApiBase(this.creds.domain);
|
|
202
225
|
|
|
203
226
|
if (text && text !== this.state.currentText) {
|
|
@@ -3,26 +3,33 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import type { TaskClient } from "./common.js";
|
|
5
5
|
import {
|
|
6
|
+
TASK_COMMENT_UPDATE_FIELD_VALUES,
|
|
6
7
|
TASKLIST_UPDATE_FIELD_VALUES,
|
|
7
8
|
TASK_UPDATE_FIELD_VALUES,
|
|
8
9
|
type AddTaskToTasklistParams,
|
|
9
10
|
type AddTasklistMembersParams,
|
|
11
|
+
type CreateTaskCommentParams,
|
|
10
12
|
type CreateTasklistParams,
|
|
11
|
-
CreateSubtaskParams,
|
|
12
|
-
CreateTaskParams,
|
|
13
|
-
DeleteTaskAttachmentParams,
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
type CreateSubtaskParams,
|
|
14
|
+
type CreateTaskParams,
|
|
15
|
+
type DeleteTaskAttachmentParams,
|
|
16
|
+
type DeleteTaskCommentParams,
|
|
17
|
+
type GetTaskAttachmentParams,
|
|
18
|
+
type GetTaskCommentParams,
|
|
19
|
+
type GetTaskParams,
|
|
16
20
|
type GetTasklistParams,
|
|
17
|
-
ListTaskAttachmentsParams,
|
|
21
|
+
type ListTaskAttachmentsParams,
|
|
22
|
+
type ListTaskCommentsParams,
|
|
18
23
|
type ListTasklistsParams,
|
|
19
24
|
type RemoveTaskFromTasklistParams,
|
|
20
25
|
type RemoveTasklistMembersParams,
|
|
21
|
-
|
|
26
|
+
type TaskCommentPatchComment,
|
|
27
|
+
type TaskUpdateTask,
|
|
22
28
|
type TasklistPatchTasklist,
|
|
23
|
-
UploadTaskAttachmentParams,
|
|
29
|
+
type UploadTaskAttachmentParams,
|
|
30
|
+
type UpdateTaskCommentParams,
|
|
24
31
|
type UpdateTasklistParams,
|
|
25
|
-
UpdateTaskParams,
|
|
32
|
+
type UpdateTaskParams,
|
|
26
33
|
} from "./schemas.js";
|
|
27
34
|
import {
|
|
28
35
|
DEFAULT_TASK_ATTACHMENT_FILENAME,
|
|
@@ -36,6 +43,7 @@ import { getFeishuRuntime } from "../runtime.js";
|
|
|
36
43
|
import { runTaskApiCall } from "./common.js";
|
|
37
44
|
|
|
38
45
|
const SUPPORTED_PATCH_FIELDS = new Set<string>(TASK_UPDATE_FIELD_VALUES);
|
|
46
|
+
const SUPPORTED_COMMENT_PATCH_FIELDS = new Set<string>(TASK_COMMENT_UPDATE_FIELD_VALUES);
|
|
39
47
|
const SUPPORTED_TASKLIST_PATCH_FIELDS = new Set<string>(TASKLIST_UPDATE_FIELD_VALUES);
|
|
40
48
|
|
|
41
49
|
function omitUndefined<T extends Record<string, unknown>>(obj: T): T {
|
|
@@ -50,10 +58,16 @@ function inferUpdateFields(task: TaskUpdateTask): string[] {
|
|
|
50
58
|
);
|
|
51
59
|
}
|
|
52
60
|
|
|
61
|
+
function inferCommentUpdateFields(comment: TaskCommentPatchComment): string[] {
|
|
62
|
+
return Object.keys(comment).filter((field) =>
|
|
63
|
+
SUPPORTED_COMMENT_PATCH_FIELDS.has(field),
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
53
67
|
function ensureSupportedUpdateFields(
|
|
54
68
|
updateFields: string[],
|
|
55
69
|
supported: Set<string>,
|
|
56
|
-
resource: "task" | "tasklist",
|
|
70
|
+
resource: "task" | "comment" | "tasklist",
|
|
57
71
|
) {
|
|
58
72
|
const invalid = updateFields.filter((field) => !supported.has(field));
|
|
59
73
|
if (invalid.length > 0) {
|
|
@@ -102,6 +116,20 @@ function formatTasklist(tasklist: Record<string, unknown> | undefined) {
|
|
|
102
116
|
};
|
|
103
117
|
}
|
|
104
118
|
|
|
119
|
+
function formatComment(comment: Record<string, unknown> | undefined) {
|
|
120
|
+
if (!comment) return undefined;
|
|
121
|
+
return {
|
|
122
|
+
id: comment.id,
|
|
123
|
+
content: comment.content,
|
|
124
|
+
creator: comment.creator,
|
|
125
|
+
reply_to_comment_id: comment.reply_to_comment_id,
|
|
126
|
+
created_at: comment.created_at,
|
|
127
|
+
updated_at: comment.updated_at,
|
|
128
|
+
resource_type: comment.resource_type,
|
|
129
|
+
resource_id: comment.resource_id,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
105
133
|
function formatAttachment(attachment: Record<string, unknown> | undefined) {
|
|
106
134
|
if (!attachment) return undefined;
|
|
107
135
|
return {
|
|
@@ -307,6 +335,113 @@ export async function getTask(client: TaskClient, params: GetTaskParams) {
|
|
|
307
335
|
};
|
|
308
336
|
}
|
|
309
337
|
|
|
338
|
+
export async function createTaskComment(client: TaskClient, params: CreateTaskCommentParams) {
|
|
339
|
+
const res = await runTaskApiCall("task.v2.comment.create", () =>
|
|
340
|
+
client.task.v2.comment.create({
|
|
341
|
+
data: omitUndefined({
|
|
342
|
+
content: params.content,
|
|
343
|
+
reply_to_comment_id: params.reply_to_comment_id,
|
|
344
|
+
resource_type: "task",
|
|
345
|
+
resource_id: params.task_guid,
|
|
346
|
+
}),
|
|
347
|
+
params: omitUndefined({
|
|
348
|
+
user_id_type: params.user_id_type,
|
|
349
|
+
}),
|
|
350
|
+
}),
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
comment: formatComment((res.data?.comment ?? undefined) as Record<string, unknown> | undefined),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export async function listTaskComments(client: TaskClient, params: ListTaskCommentsParams) {
|
|
359
|
+
const res = await runTaskApiCall("task.v2.comment.list", () =>
|
|
360
|
+
client.task.v2.comment.list({
|
|
361
|
+
params: omitUndefined({
|
|
362
|
+
resource_type: "task",
|
|
363
|
+
resource_id: params.task_guid,
|
|
364
|
+
page_size: params.page_size,
|
|
365
|
+
page_token: params.page_token,
|
|
366
|
+
direction: params.direction,
|
|
367
|
+
user_id_type: params.user_id_type,
|
|
368
|
+
}),
|
|
369
|
+
}),
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
const items = (res.data?.items ?? []) as Record<string, unknown>[];
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
items: items.map((item) => formatComment(item)),
|
|
376
|
+
page_token: res.data?.page_token,
|
|
377
|
+
has_more: res.data?.has_more,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export async function getTaskComment(client: TaskClient, params: GetTaskCommentParams) {
|
|
382
|
+
const res = await runTaskApiCall("task.v2.comment.get", () =>
|
|
383
|
+
client.task.v2.comment.get({
|
|
384
|
+
path: { comment_id: params.comment_id },
|
|
385
|
+
params: omitUndefined({
|
|
386
|
+
user_id_type: params.user_id_type,
|
|
387
|
+
}),
|
|
388
|
+
}),
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
comment: formatComment((res.data?.comment ?? undefined) as Record<string, unknown> | undefined),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export async function updateTaskComment(client: TaskClient, params: UpdateTaskCommentParams) {
|
|
397
|
+
const comment = omitUndefined(params.comment as Record<string, unknown>) as TaskCommentPatchComment;
|
|
398
|
+
const updateFields = params.update_fields?.length
|
|
399
|
+
? params.update_fields
|
|
400
|
+
: inferCommentUpdateFields(comment);
|
|
401
|
+
|
|
402
|
+
if (params.update_fields?.length) {
|
|
403
|
+
ensureSupportedUpdateFields(updateFields, SUPPORTED_COMMENT_PATCH_FIELDS, "comment");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (Object.keys(comment).length === 0) {
|
|
407
|
+
throw new Error("comment update payload is empty");
|
|
408
|
+
}
|
|
409
|
+
if (updateFields.length === 0) {
|
|
410
|
+
throw new Error("no valid update_fields provided or inferred from comment payload");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const res = await runTaskApiCall("task.v2.comment.patch", () =>
|
|
414
|
+
client.task.v2.comment.patch({
|
|
415
|
+
path: { comment_id: params.comment_id },
|
|
416
|
+
data: {
|
|
417
|
+
comment,
|
|
418
|
+
update_fields: updateFields,
|
|
419
|
+
},
|
|
420
|
+
params: omitUndefined({
|
|
421
|
+
user_id_type: params.user_id_type,
|
|
422
|
+
}),
|
|
423
|
+
}),
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
return {
|
|
427
|
+
comment: formatComment((res.data?.comment ?? undefined) as Record<string, unknown> | undefined),
|
|
428
|
+
update_fields: updateFields,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export async function deleteTaskComment(client: TaskClient, params: DeleteTaskCommentParams) {
|
|
433
|
+
await runTaskApiCall("task.v2.comment.delete", () =>
|
|
434
|
+
client.task.v2.comment.delete({
|
|
435
|
+
path: { comment_id: params.comment_id },
|
|
436
|
+
}),
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
success: true,
|
|
441
|
+
comment_id: params.comment_id,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
310
445
|
export async function getTasklist(client: TaskClient, params: GetTasklistParams) {
|
|
311
446
|
const res = await runTaskApiCall("task.v2.tasklist.get", () =>
|
|
312
447
|
client.task.v2.tasklist.get({
|