@bobotu/feishu-fork 0.1.0

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 (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +922 -0
  3. package/index.ts +65 -0
  4. package/openclaw.plugin.json +10 -0
  5. package/package.json +72 -0
  6. package/skills/feishu-doc/SKILL.md +161 -0
  7. package/skills/feishu-doc/references/block-types.md +102 -0
  8. package/skills/feishu-drive/SKILL.md +96 -0
  9. package/skills/feishu-perm/SKILL.md +90 -0
  10. package/skills/feishu-task/SKILL.md +210 -0
  11. package/skills/feishu-wiki/SKILL.md +96 -0
  12. package/src/accounts.ts +140 -0
  13. package/src/bitable-tools/actions.ts +199 -0
  14. package/src/bitable-tools/common.ts +90 -0
  15. package/src/bitable-tools/index.ts +1 -0
  16. package/src/bitable-tools/meta.ts +80 -0
  17. package/src/bitable-tools/register.ts +195 -0
  18. package/src/bitable-tools/schemas.ts +221 -0
  19. package/src/bot.ts +1125 -0
  20. package/src/channel.ts +334 -0
  21. package/src/client.ts +114 -0
  22. package/src/config-schema.ts +237 -0
  23. package/src/dedup.ts +54 -0
  24. package/src/directory.ts +165 -0
  25. package/src/doc-tools/actions.ts +341 -0
  26. package/src/doc-tools/common.ts +33 -0
  27. package/src/doc-tools/index.ts +2 -0
  28. package/src/doc-tools/register.ts +90 -0
  29. package/src/doc-tools/schemas.ts +85 -0
  30. package/src/doc-write-service.ts +711 -0
  31. package/src/drive-tools/actions.ts +182 -0
  32. package/src/drive-tools/common.ts +18 -0
  33. package/src/drive-tools/index.ts +2 -0
  34. package/src/drive-tools/register.ts +71 -0
  35. package/src/drive-tools/schemas.ts +67 -0
  36. package/src/dynamic-agent.ts +135 -0
  37. package/src/external-keys.ts +19 -0
  38. package/src/media.ts +510 -0
  39. package/src/mention.ts +121 -0
  40. package/src/monitor.ts +323 -0
  41. package/src/onboarding.ts +449 -0
  42. package/src/outbound.ts +40 -0
  43. package/src/perm-tools/actions.ts +111 -0
  44. package/src/perm-tools/common.ts +18 -0
  45. package/src/perm-tools/index.ts +2 -0
  46. package/src/perm-tools/register.ts +65 -0
  47. package/src/perm-tools/schemas.ts +52 -0
  48. package/src/policy.ts +117 -0
  49. package/src/probe.ts +147 -0
  50. package/src/reactions.ts +160 -0
  51. package/src/reply-dispatcher.ts +240 -0
  52. package/src/runtime.ts +14 -0
  53. package/src/send.ts +391 -0
  54. package/src/streaming-card.ts +211 -0
  55. package/src/targets.ts +58 -0
  56. package/src/task-tools/actions.ts +590 -0
  57. package/src/task-tools/common.ts +18 -0
  58. package/src/task-tools/constants.ts +13 -0
  59. package/src/task-tools/index.ts +1 -0
  60. package/src/task-tools/register.ts +263 -0
  61. package/src/task-tools/schemas.ts +567 -0
  62. package/src/text/markdown-links.ts +104 -0
  63. package/src/tools-common/feishu-api.ts +184 -0
  64. package/src/tools-common/tool-context.ts +23 -0
  65. package/src/tools-common/tool-exec.ts +73 -0
  66. package/src/tools-config.ts +22 -0
  67. package/src/types.ts +79 -0
  68. package/src/typing.ts +75 -0
  69. package/src/wiki-tools/actions.ts +166 -0
  70. package/src/wiki-tools/common.ts +18 -0
  71. package/src/wiki-tools/index.ts +2 -0
  72. package/src/wiki-tools/register.ts +66 -0
  73. package/src/wiki-tools/schemas.ts +55 -0
@@ -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 ADDED
@@ -0,0 +1,58 @@
1
+ import type { FeishuIdType } from "./types.js";
2
+
3
+ const CHAT_ID_PREFIX = "oc_";
4
+ const OPEN_ID_PREFIX = "ou_";
5
+ const USER_ID_REGEX = /^[a-zA-Z0-9_-]+$/;
6
+
7
+ export function detectIdType(id: string): FeishuIdType | null {
8
+ const trimmed = id.trim();
9
+ if (trimmed.startsWith(CHAT_ID_PREFIX)) return "chat_id";
10
+ if (trimmed.startsWith(OPEN_ID_PREFIX)) return "open_id";
11
+ if (USER_ID_REGEX.test(trimmed)) return "user_id";
12
+ return null;
13
+ }
14
+
15
+ export function normalizeFeishuTarget(raw: string): string | null {
16
+ const trimmed = raw.trim();
17
+ if (!trimmed) return null;
18
+
19
+ const lowered = trimmed.toLowerCase();
20
+ if (lowered.startsWith("chat:")) {
21
+ return trimmed.slice("chat:".length).trim() || null;
22
+ }
23
+ if (lowered.startsWith("user:")) {
24
+ return trimmed.slice("user:".length).trim() || null;
25
+ }
26
+ if (lowered.startsWith("open_id:")) {
27
+ return trimmed.slice("open_id:".length).trim() || null;
28
+ }
29
+
30
+ return trimmed;
31
+ }
32
+
33
+ export function formatFeishuTarget(id: string, type?: FeishuIdType): string {
34
+ const trimmed = id.trim();
35
+ if (type === "chat_id" || trimmed.startsWith(CHAT_ID_PREFIX)) {
36
+ return `chat:${trimmed}`;
37
+ }
38
+ if (type === "open_id" || trimmed.startsWith(OPEN_ID_PREFIX)) {
39
+ return `user:${trimmed}`;
40
+ }
41
+ return trimmed;
42
+ }
43
+
44
+ export function resolveReceiveIdType(id: string): "chat_id" | "open_id" | "user_id" {
45
+ const trimmed = id.trim();
46
+ if (trimmed.startsWith(CHAT_ID_PREFIX)) return "chat_id";
47
+ if (trimmed.startsWith(OPEN_ID_PREFIX)) return "open_id";
48
+ return "user_id";
49
+ }
50
+
51
+ export function looksLikeFeishuId(raw: string): boolean {
52
+ const trimmed = raw.trim();
53
+ if (!trimmed) return false;
54
+ if (/^(chat|user|open_id):/i.test(trimmed)) return true;
55
+ if (trimmed.startsWith(CHAT_ID_PREFIX)) return true;
56
+ if (trimmed.startsWith(OPEN_ID_PREFIX)) return true;
57
+ return false;
58
+ }