jowork 0.2.4 → 0.3.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 (38) hide show
  1. package/dist/{chunk-ROIINI33.js → chunk-4PIT2GZ4.js} +13 -1
  2. package/dist/{chunk-XLYRHKG6.js → chunk-54SD5GBF.js} +1 -1
  3. package/dist/chunk-63AMINQC.js +156 -0
  4. package/dist/{chunk-XAEGXSEO.js → chunk-74AHY7X6.js} +4 -0
  5. package/dist/{chunk-7U3SXINY.js → chunk-ATAUWJYD.js} +320 -50
  6. package/dist/chunk-DQW74UCN.js +671 -0
  7. package/dist/chunk-EYP6WMFF.js +153 -0
  8. package/dist/{chunk-JSTXMDXI.js → chunk-FCFZCZHR.js} +1 -1
  9. package/dist/chunk-FX6Z3QHV.js +34 -0
  10. package/dist/chunk-HENAABEL.js +419 -0
  11. package/dist/chunk-OXWWOKC7.js +201 -0
  12. package/dist/chunk-QGHJ45PL.js +661 -0
  13. package/dist/chunk-RO3KK5RC.js +132 -0
  14. package/dist/{chunk-JE6TOU7W.js → chunk-TFMF3EXE.js} +2 -7
  15. package/dist/{chunk-TN327MDF.js → chunk-VX662YLA.js} +3 -3
  16. package/dist/cli.js +338 -149
  17. package/dist/{config-AI6UIJJN.js → config-FH2XLN7A.js} +2 -2
  18. package/dist/content-reader-VPGTR2SF.js +10 -0
  19. package/dist/context-ZNI3WOB7.js +10 -0
  20. package/dist/{credential-store-ZRZCSRPC.js → credential-store-OS5ZY4OW.js} +2 -2
  21. package/dist/{feishu-A6YVFKEN.js → feishu-XW5T6ER2.js} +8 -3
  22. package/dist/{git-manager-N35XSG4Y.js → git-manager-RVWV2GSV.js} +2 -1
  23. package/dist/github-PQKAYTLO.js +11 -0
  24. package/dist/{paths-JXOMBYIT.js → paths-FFRET6F7.js} +7 -3
  25. package/dist/{server-5GVWN2NB.js → server-WEADPUST.js} +59 -66
  26. package/dist/{setup-IDQDPCEJ.js → setup-S2S2CHB2.js} +91 -32
  27. package/dist/sync-SRLFR5NA.js +21 -0
  28. package/dist/transport.js +6 -4
  29. package/package.json +1 -1
  30. package/src/dashboard/public/app.js +34 -8
  31. package/src/dashboard/public/style.css +14 -0
  32. package/dist/chunk-AIXKXEYS.js +0 -547
  33. package/dist/chunk-L5ZR7TSK.js +0 -82
  34. package/dist/chunk-LS2AJM5A.js +0 -163
  35. package/dist/chunk-QMOFQX7X.js +0 -612
  36. package/dist/chunk-YJWTKFWX.js +0 -451
  37. package/dist/github-SHWUFNYB.js +0 -10
  38. package/dist/sync-7V54N62M.js +0 -18
@@ -0,0 +1,671 @@
1
+ import {
2
+ formatApproval,
3
+ formatCalendarEvent,
4
+ formatDocument
5
+ } from "./chunk-RO3KK5RC.js";
6
+ import {
7
+ logError,
8
+ logInfo
9
+ } from "./chunk-MYDK7MWB.js";
10
+ import {
11
+ contentHash
12
+ } from "./chunk-OXWWOKC7.js";
13
+
14
+ // src/sync/feishu.ts
15
+ var defaultLogger = {
16
+ info: (msg, ctx) => logInfo("sync", msg, ctx),
17
+ warn: (msg, ctx) => logError("sync", msg, ctx),
18
+ error: (msg, ctx) => logError("sync", msg, ctx)
19
+ };
20
+ async function getFeishuToken(appId, appSecret) {
21
+ const res = await fetch("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal", {
22
+ method: "POST",
23
+ headers: { "Content-Type": "application/json" },
24
+ body: JSON.stringify({ app_id: appId, app_secret: appSecret })
25
+ });
26
+ const data = await res.json();
27
+ if (data.code !== 0) throw new Error(`Feishu auth failed: code ${data.code}`);
28
+ return data.tenant_access_token;
29
+ }
30
+ async function syncFeishu(ctx, data, logger = defaultLogger) {
31
+ const { appId, appSecret } = data;
32
+ if (!appId || !appSecret) throw new Error("Missing Feishu credentials");
33
+ const token = await getFeishuToken(appId, appSecret);
34
+ const chats = [];
35
+ let chatPageToken;
36
+ let hasMoreChats = true;
37
+ while (hasMoreChats) {
38
+ const chatUrl = new URL("https://open.feishu.cn/open-apis/im/v1/chats");
39
+ chatUrl.searchParams.set("page_size", "100");
40
+ if (chatPageToken) chatUrl.searchParams.set("page_token", chatPageToken);
41
+ const chatsRes = await fetch(chatUrl.toString(), {
42
+ headers: { Authorization: `Bearer ${token}` }
43
+ });
44
+ const chatsData = await chatsRes.json();
45
+ if (chatsData.code !== 0) throw new Error(`Failed to list chats: code ${chatsData.code}`);
46
+ chats.push(...chatsData.data?.items ?? []);
47
+ hasMoreChats = chatsData.data?.has_more ?? false;
48
+ chatPageToken = chatsData.data?.page_token;
49
+ }
50
+ let totalMessages = 0;
51
+ let newMessages = 0;
52
+ const sqlite = ctx.getSqlite();
53
+ const fileWriter = ctx.writer;
54
+ const dayMessages = /* @__PURE__ */ new Map();
55
+ for (const chat of chats) {
56
+ const cursor = ctx.getCursor(`feishu:${chat.chat_id}`);
57
+ let pageToken = cursor?.pageToken;
58
+ let hasMore = true;
59
+ while (hasMore) {
60
+ const url = new URL("https://open.feishu.cn/open-apis/im/v1/messages");
61
+ url.searchParams.set("container_id_type", "chat");
62
+ url.searchParams.set("container_id", chat.chat_id);
63
+ url.searchParams.set("page_size", "50");
64
+ url.searchParams.set("sort_type", "ByCreateTimeAsc");
65
+ if (pageToken) url.searchParams.set("page_token", pageToken);
66
+ const msgRes = await fetch(url.toString(), {
67
+ headers: { Authorization: `Bearer ${token}` }
68
+ });
69
+ const msgData = await msgRes.json();
70
+ if (msgData.code !== 0) {
71
+ if (msgData.code === 99991400) {
72
+ logger.warn(`Rate limited on ${chat.name}, waiting 5s`);
73
+ await new Promise((r) => setTimeout(r, 5e3));
74
+ continue;
75
+ }
76
+ logger.warn(`Failed to get messages from "${chat.name}": code ${msgData.code}`);
77
+ break;
78
+ }
79
+ const messages = msgData.data?.items ?? [];
80
+ for (let i = 0; i < messages.length; i += 100) {
81
+ const batch = messages.slice(i, i + 100);
82
+ const items = [];
83
+ for (const msg of batch) {
84
+ if (msg.msg_type !== "text" && msg.msg_type !== "post") continue;
85
+ let content = "";
86
+ try {
87
+ const bodyContent = JSON.parse(msg.body?.content ?? "{}");
88
+ const raw = bodyContent.text ?? bodyContent.content ?? bodyContent;
89
+ content = typeof raw === "string" ? raw : JSON.stringify(raw);
90
+ } catch {
91
+ content = msg.body?.content ?? "";
92
+ }
93
+ if (!content || typeof content !== "string") continue;
94
+ const uri = `feishu://message/${msg.message_id}`;
95
+ const createTime = msg.create_time ? parseInt(msg.create_time) : Date.now();
96
+ items.push({
97
+ source: "feishu",
98
+ sourceType: "message",
99
+ uri,
100
+ title: chat.name,
101
+ content,
102
+ contentType: "text/plain",
103
+ createdAt: createTime,
104
+ tags: ["feishu", "message"]
105
+ });
106
+ if (fileWriter) {
107
+ const date = new Date(createTime).toISOString().slice(0, 10);
108
+ const time = new Date(createTime).toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" });
109
+ const key = `${chat.chat_id}:${date}`;
110
+ let group = dayMessages.get(key);
111
+ if (!group) {
112
+ group = { chatName: chat.name, chatId: chat.chat_id, date, messages: [] };
113
+ dayMessages.set(key, group);
114
+ }
115
+ group.messages.push({ time, sender: msg.sender?.id ?? "unknown", content });
116
+ }
117
+ }
118
+ const result = ctx.batchUpsert(items);
119
+ newMessages += result.inserted;
120
+ }
121
+ totalMessages += messages.length;
122
+ hasMore = msgData.data.has_more;
123
+ pageToken = msgData.data.page_token;
124
+ if (pageToken) {
125
+ ctx.saveCursor(`feishu:${chat.chat_id}`, { pageToken });
126
+ }
127
+ }
128
+ }
129
+ if (fileWriter && dayMessages.size > 0) {
130
+ for (const group of dayMessages.values()) {
131
+ try {
132
+ const filePath = fileWriter.appendMessages(
133
+ "feishu",
134
+ group.chatName,
135
+ group.chatId,
136
+ group.date,
137
+ group.messages
138
+ );
139
+ sqlite.prepare(
140
+ `UPDATE objects SET file_path = ? WHERE source = 'feishu' AND source_type = 'message' AND title = ? AND file_path IS NULL`
141
+ ).run(filePath, group.chatName);
142
+ } catch (err) {
143
+ logger.warn(`Failed to write messages file for ${group.chatName}/${group.date}: ${err}`);
144
+ }
145
+ }
146
+ }
147
+ logger.info("Feishu sync complete", { totalMessages, newMessages, chats: chats.length });
148
+ return { totalMessages, newMessages, chats: chats.length };
149
+ }
150
+ async function syncFeishuMeetings(ctx, data, logger = defaultLogger) {
151
+ const { appId, appSecret } = data;
152
+ if (!appId || !appSecret) throw new Error("Missing Feishu credentials");
153
+ const token = await getFeishuToken(appId, appSecret);
154
+ let meetings = 0, newObjects = 0;
155
+ const fileWriter = ctx.writer;
156
+ try {
157
+ const now = Math.floor(Date.now() / 1e3);
158
+ const weekAgo = now - 30 * 24 * 60 * 60;
159
+ const calRes = await fetch("https://open.feishu.cn/open-apis/calendar/v4/calendars?page_size=50", {
160
+ headers: { Authorization: `Bearer ${token}` }
161
+ });
162
+ const calData = await calRes.json();
163
+ if (calData.code !== 0 || !calData.data?.calendar_list?.length) {
164
+ if (calData.code === 99992402) {
165
+ logger.warn("Calendar sync requires calendar:calendar:readonly scope. Add it at https://open.feishu.cn/app \u2192 Permissions");
166
+ } else if (calData.code !== 0) {
167
+ logger.warn(`Calendar API returned code ${calData.code}`);
168
+ }
169
+ return { meetings, newObjects };
170
+ }
171
+ for (const cal of calData.data.calendar_list) {
172
+ const eventsRes = await fetch(
173
+ `https://open.feishu.cn/open-apis/calendar/v4/calendars/${cal.calendar_id}/events?start_time=${weekAgo}&end_time=${now}&page_size=50`,
174
+ { headers: { Authorization: `Bearer ${token}` } }
175
+ );
176
+ const eventsData = await eventsRes.json();
177
+ if (eventsData.code !== 0 || !eventsData.data?.items) continue;
178
+ const items = [];
179
+ for (const event of eventsData.data.items) {
180
+ const uri = `feishu://calendar/${cal.calendar_id}/event/${event.event_id}`;
181
+ const attendees = event.attendees?.map((a) => a.display_name).join(", ") ?? "";
182
+ const startTs = parseInt(event.start_time?.timestamp ?? "0") * 1e3;
183
+ const startTime = new Date(startTs).toLocaleString("zh-CN");
184
+ const summary = `${event.summary} (${startTime}, ${attendees || "no attendees"})`;
185
+ const body = [
186
+ `Meeting: ${event.summary}`,
187
+ `Time: ${startTime}`,
188
+ `Attendees: ${attendees}`,
189
+ `Description: ${event.description || "(none)"}`
190
+ ].join("\n");
191
+ const attendeeNames = event.attendees?.map((a) => a.display_name) ?? [];
192
+ const endTs = parseInt(event.end_time?.timestamp ?? "0") * 1e3;
193
+ const endTime = new Date(endTs).toLocaleString("zh-CN");
194
+ const date = new Date(startTs).toISOString().slice(0, 10);
195
+ const fileContent = formatCalendarEvent({
196
+ source: "feishu",
197
+ title: event.summary || "Untitled meeting",
198
+ startTime,
199
+ endTime,
200
+ attendees: attendeeNames,
201
+ description: event.description || "",
202
+ uri
203
+ });
204
+ items.push({
205
+ source: "feishu",
206
+ sourceType: "calendar_event",
207
+ uri,
208
+ title: event.summary || "Untitled meeting",
209
+ summary,
210
+ tags: ["feishu", "calendar", "meeting"],
211
+ content: body,
212
+ contentType: "text/plain",
213
+ createdAt: startTs,
214
+ fileContent,
215
+ fileMeta: { date }
216
+ });
217
+ }
218
+ const result = ctx.batchUpsert(items);
219
+ meetings += result.inserted;
220
+ newObjects += result.inserted;
221
+ await new Promise((r) => setTimeout(r, 200));
222
+ }
223
+ } catch (err) {
224
+ logger.error(`Meeting sync error: ${err}`);
225
+ }
226
+ logger.info("Meeting sync complete", { meetings, newObjects });
227
+ return { meetings, newObjects };
228
+ }
229
+ async function syncFeishuApprovals(ctx, data, logger = defaultLogger) {
230
+ const { appId, appSecret } = data;
231
+ if (!appId || !appSecret) throw new Error("Missing Feishu credentials");
232
+ const token = await getFeishuToken(appId, appSecret);
233
+ let approvals = 0, newObjects = 0;
234
+ try {
235
+ const res = await fetch("https://open.feishu.cn/open-apis/approval/v4/instances?page_size=50", {
236
+ headers: { Authorization: `Bearer ${token}` }
237
+ });
238
+ if (!res.ok || res.status === 400) {
239
+ logger.warn("Approval sync requires approval:approval:readonly scope. Add it at https://open.feishu.cn/app \u2192 Permissions");
240
+ return { approvals, newObjects };
241
+ }
242
+ const resData = await res.json();
243
+ if (resData.code !== 0 || !resData.data?.items) {
244
+ logger.warn(`Approval list returned code ${resData.code}`);
245
+ return { approvals, newObjects };
246
+ }
247
+ const items = [];
248
+ for (const approval of resData.data.items) {
249
+ const uri = `feishu://approval/${approval.instance_id}`;
250
+ const summary = `${approval.approval_name} [${approval.status}]`;
251
+ let formText = "";
252
+ try {
253
+ const formData = JSON.parse(approval.form || "[]");
254
+ formText = Array.isArray(formData) ? formData.map((f) => `${f.name}: ${f.value}`).join("\n") : JSON.stringify(formData);
255
+ } catch {
256
+ formText = approval.form || "";
257
+ }
258
+ const body = [
259
+ `Approval: ${approval.approval_name}`,
260
+ `Status: ${approval.status}`,
261
+ `Submitted: ${approval.start_time}`,
262
+ `Completed: ${approval.end_time || "pending"}`,
263
+ "",
264
+ formText
265
+ ].join("\n");
266
+ const startTime = approval.start_time ? new Date(approval.start_time).getTime() : Date.now();
267
+ let formFields = [];
268
+ try {
269
+ const fd = JSON.parse(approval.form || "[]");
270
+ if (Array.isArray(fd)) formFields = fd;
271
+ } catch {
272
+ }
273
+ const fileContent = formatApproval({
274
+ source: "feishu",
275
+ name: approval.approval_name,
276
+ status: approval.status,
277
+ submitter: approval.user_id || "unknown",
278
+ fields: formFields,
279
+ uri
280
+ });
281
+ items.push({
282
+ source: "feishu",
283
+ sourceType: "approval",
284
+ uri,
285
+ title: approval.approval_name,
286
+ summary,
287
+ tags: ["feishu", "approval", approval.status],
288
+ content: body,
289
+ contentType: "text/plain",
290
+ createdAt: startTime,
291
+ fileContent
292
+ });
293
+ }
294
+ const result = ctx.batchUpsert(items);
295
+ approvals += result.inserted;
296
+ newObjects += result.inserted;
297
+ } catch (err) {
298
+ logger.error(`Approval sync error: ${err}`);
299
+ }
300
+ logger.info("Approval sync complete", { approvals, newObjects });
301
+ return { approvals, newObjects };
302
+ }
303
+ async function syncFeishuDocs(ctx, data, logger = defaultLogger) {
304
+ const { appId, appSecret } = data;
305
+ if (!appId || !appSecret) throw new Error("Missing Feishu credentials");
306
+ const token = await getFeishuToken(appId, appSecret);
307
+ let docs = 0, newObjects = 0;
308
+ const sqlite = ctx.getSqlite();
309
+ async function fetchDocContent(docToken) {
310
+ try {
311
+ const res = await fetch(
312
+ `https://open.feishu.cn/open-apis/docx/v1/documents/${docToken}/raw_content`,
313
+ { headers: { Authorization: `Bearer ${token}` } }
314
+ );
315
+ if (!res.ok) return null;
316
+ const d = await res.json();
317
+ return d.data?.content ?? null;
318
+ } catch {
319
+ return null;
320
+ }
321
+ }
322
+ function insertDoc(uri, title, docBody, objType, createdTs) {
323
+ if (ctx.exists(uri)) return false;
324
+ const fileContent = formatDocument({ source: "feishu", title, uri, body: docBody });
325
+ const result = ctx.upsertObject({
326
+ source: "feishu",
327
+ sourceType: "document",
328
+ uri,
329
+ title,
330
+ tags: ["feishu", "document", objType],
331
+ content: docBody,
332
+ contentType: "text/plain",
333
+ createdAt: createdTs,
334
+ fileContent
335
+ });
336
+ if (result.action === "inserted") {
337
+ docs++;
338
+ newObjects++;
339
+ return true;
340
+ }
341
+ return false;
342
+ }
343
+ try {
344
+ let wikiFound = 0;
345
+ try {
346
+ const spacesRes = await fetch("https://open.feishu.cn/open-apis/wiki/v2/spaces?page_size=50", {
347
+ headers: { Authorization: `Bearer ${token}` }
348
+ });
349
+ const spacesData = await spacesRes.json();
350
+ if (spacesData.code === 0 && spacesData.data?.items?.length) {
351
+ for (const space of spacesData.data.items) {
352
+ let nodePageToken;
353
+ let hasMoreNodes = true;
354
+ while (hasMoreNodes) {
355
+ const url = new URL(`https://open.feishu.cn/open-apis/wiki/v2/spaces/${space.space_id}/nodes`);
356
+ url.searchParams.set("page_size", "50");
357
+ if (nodePageToken) url.searchParams.set("page_token", nodePageToken);
358
+ const nodesRes = await fetch(url.toString(), { headers: { Authorization: `Bearer ${token}` } });
359
+ const nodesData = await nodesRes.json();
360
+ if (nodesData.code !== 0 || !nodesData.data?.items) break;
361
+ for (const node of nodesData.data.items) {
362
+ const uri = `feishu://wiki/${space.space_id}/${node.node_token}`;
363
+ let docBody = `Wiki: ${node.title} (${node.obj_type}, space: ${space.name})`;
364
+ if (node.obj_type === "docx" || node.obj_type === "doc") {
365
+ const content = await fetchDocContent(node.obj_token);
366
+ if (content) docBody = content;
367
+ }
368
+ if (insertDoc(uri, node.title, docBody, node.obj_type)) wikiFound++;
369
+ await new Promise((r) => setTimeout(r, 100));
370
+ }
371
+ hasMoreNodes = nodesData.data?.has_more ?? false;
372
+ nodePageToken = nodesData.data?.page_token;
373
+ }
374
+ }
375
+ }
376
+ } catch (err) {
377
+ logger.warn(`Wiki sync error (non-fatal): ${err}`);
378
+ }
379
+ let driveFound = 0;
380
+ try {
381
+ let drivePageToken;
382
+ let hasMoreFiles = true;
383
+ while (hasMoreFiles) {
384
+ const url = new URL("https://open.feishu.cn/open-apis/drive/v1/files");
385
+ url.searchParams.set("page_size", "50");
386
+ if (drivePageToken) url.searchParams.set("page_token", drivePageToken);
387
+ const driveRes = await fetch(url.toString(), { headers: { Authorization: `Bearer ${token}` } });
388
+ const driveData = await driveRes.json();
389
+ if (driveData.code !== 0) {
390
+ if (driveData.code === 99992402) {
391
+ logger.warn("Drive sync requires drive:drive:readonly scope. Add it at https://open.feishu.cn/app \u2192 Permissions");
392
+ }
393
+ break;
394
+ }
395
+ for (const file of driveData.data?.files ?? []) {
396
+ const uri = `feishu://drive/${file.token}`;
397
+ let docBody = `Drive: ${file.name} (${file.type})`;
398
+ if (file.type === "docx" || file.type === "doc") {
399
+ const content = await fetchDocContent(file.token);
400
+ if (content) docBody = content;
401
+ }
402
+ const createdTs = file.created_time ? parseInt(file.created_time) * 1e3 : void 0;
403
+ if (insertDoc(uri, file.name, docBody, file.type, createdTs)) driveFound++;
404
+ await new Promise((r) => setTimeout(r, 100));
405
+ }
406
+ hasMoreFiles = driveData.data?.has_more ?? false;
407
+ drivePageToken = driveData.data?.next_page_token;
408
+ }
409
+ } catch (err) {
410
+ logger.warn(`Drive sync error (non-fatal): ${err}`);
411
+ }
412
+ let msgDocFound = 0;
413
+ try {
414
+ const msgObjs = sqlite.prepare(`
415
+ SELECT o.id, o.file_path FROM objects o
416
+ WHERE o.source = 'feishu' AND o.source_type = 'message'
417
+ AND o.summary LIKE '%feishu.cn%'
418
+ `).all();
419
+ const { readObjectContents } = await import("./content-reader-VPGTR2SF.js");
420
+ const contentMap = readObjectContents(
421
+ sqlite,
422
+ msgObjs.map((o) => ({ id: o.id, filePath: o.file_path }))
423
+ );
424
+ const msgRows = [...contentMap.values()].filter((c) => c.includes("feishu.cn")).map((c) => ({ content: c }));
425
+ const docTokens = /* @__PURE__ */ new Map();
426
+ const feishuUrlPattern = /https?:\/\/[a-z0-9]+\.feishu\.cn\/(docx|doc|wiki|sheets|bitable|mindnote)\/([A-Za-z0-9]+)/g;
427
+ for (const row of msgRows) {
428
+ for (const match of row.content.matchAll(feishuUrlPattern)) {
429
+ const docType = match[1];
430
+ const docToken = match[2];
431
+ if (!docTokens.has(docToken)) {
432
+ docTokens.set(docToken, docType);
433
+ }
434
+ }
435
+ }
436
+ if (docTokens.size > 0) {
437
+ logger.info(`Found ${docTokens.size} feishu doc links in messages`);
438
+ }
439
+ for (const [docToken, docType] of docTokens) {
440
+ const uri = `feishu://doc/${docToken}`;
441
+ if (ctx.exists(uri)) continue;
442
+ let title = `Feishu ${docType} (${docToken})`;
443
+ let docBody = "";
444
+ if (docType === "docx" || docType === "doc") {
445
+ try {
446
+ const metaRes = await fetch(
447
+ `https://open.feishu.cn/open-apis/docx/v1/documents/${docToken}`,
448
+ { headers: { Authorization: `Bearer ${token}` } }
449
+ );
450
+ if (metaRes.ok) {
451
+ const metaData = await metaRes.json();
452
+ if (metaData.data?.document?.title) title = metaData.data.document.title;
453
+ }
454
+ } catch {
455
+ }
456
+ const content = await fetchDocContent(docToken);
457
+ if (content) {
458
+ docBody = content;
459
+ } else {
460
+ docBody = `(Content not accessible \u2014 doc may not be shared with the app)`;
461
+ }
462
+ } else if (docType === "wiki") {
463
+ try {
464
+ const nodeRes = await fetch(
465
+ `https://open.feishu.cn/open-apis/wiki/v2/spaces/get_node?token=${docToken}`,
466
+ { headers: { Authorization: `Bearer ${token}` } }
467
+ );
468
+ if (nodeRes.ok) {
469
+ const nodeData = await nodeRes.json();
470
+ if (nodeData.data?.node?.title) title = nodeData.data.node.title;
471
+ if (nodeData.data?.node?.obj_token && (nodeData.data.node.obj_type === "docx" || nodeData.data.node.obj_type === "doc")) {
472
+ const content = await fetchDocContent(nodeData.data.node.obj_token);
473
+ if (content) docBody = content;
474
+ }
475
+ }
476
+ } catch {
477
+ }
478
+ if (!docBody) docBody = `Wiki: ${title}`;
479
+ } else {
480
+ docBody = `Feishu ${docType}: ${title} (non-docx format, content extraction not supported)`;
481
+ }
482
+ if (insertDoc(uri, title, docBody, docType)) msgDocFound++;
483
+ await new Promise((r) => setTimeout(r, 200));
484
+ }
485
+ if (msgDocFound > 0) {
486
+ logger.info(`Extracted ${msgDocFound} docs from message links`);
487
+ }
488
+ } catch (err) {
489
+ logger.warn(`Message doc extraction error (non-fatal): ${err}`);
490
+ }
491
+ if (wikiFound === 0 && driveFound === 0 && msgDocFound === 0) {
492
+ const existingDocs = sqlite.prepare("SELECT COUNT(*) as cnt FROM objects WHERE source='feishu' AND source_type='document'").get().cnt;
493
+ if (existingDocs === 0) {
494
+ logger.warn("No documents found. Ensure docs are shared with the app, or wiki spaces include the bot as member.");
495
+ }
496
+ }
497
+ } catch (err) {
498
+ logger.error(`Document sync error: ${err}`);
499
+ }
500
+ logger.info("Document sync complete", { docs, newObjects });
501
+ return { docs, newObjects };
502
+ }
503
+ var SKIP_URL_PATTERNS = [
504
+ /email\.quail-mail\.com/,
505
+ // Newsletter tracking redirects
506
+ /\.feishu\.cn\//,
507
+ // Feishu internal links (handled by doc sync)
508
+ /mp\.weixin\.qq\.com/,
509
+ // WeChat articles (need cookies)
510
+ /open\.weixin\.qq\.com/,
511
+ // WeChat OAuth
512
+ /xhslink\.com/,
513
+ // Xiaohongshu short links (need app)
514
+ /b23\.tv/,
515
+ // Bilibili short links
516
+ /t\.co\//,
517
+ // Twitter short links
518
+ /luma\.com\/event/
519
+ // Luma events (dynamic SPA)
520
+ ];
521
+ function extractUrlsFromContent(content) {
522
+ const urls = [];
523
+ try {
524
+ const parsed = JSON.parse(content);
525
+ const rows = Array.isArray(parsed) ? parsed : [parsed];
526
+ for (const row of rows) {
527
+ if (!Array.isArray(row)) continue;
528
+ for (const element of row) {
529
+ if (element?.tag === "a" && element?.href) {
530
+ const href = String(element.href);
531
+ if (/\.(png|jpg|jpeg|gif|webp|svg|mp4|mp3|pdf|zip|rar)(\?|$)/i.test(href)) continue;
532
+ if (href.includes("/file/") || href.includes("/image/")) continue;
533
+ if (SKIP_URL_PATTERNS.some((p) => p.test(href))) continue;
534
+ urls.push({ url: href, text: element.text ?? href });
535
+ }
536
+ }
537
+ }
538
+ } catch {
539
+ }
540
+ if (urls.length === 0) {
541
+ const matches = content.match(/https?:\/\/[^\s"'<>\]]+/g);
542
+ if (matches) {
543
+ for (const url of matches) {
544
+ if (/\.(png|jpg|jpeg|gif|webp|svg|mp4|mp3|pdf|zip|rar)(\?|$)/i.test(url)) continue;
545
+ if (SKIP_URL_PATTERNS.some((p) => p.test(url))) continue;
546
+ urls.push({ url, text: url });
547
+ }
548
+ }
549
+ }
550
+ return urls;
551
+ }
552
+ async function fetchUrlContent(url) {
553
+ try {
554
+ const res = await fetch(`https://r.jina.ai/${url}`, {
555
+ headers: {
556
+ "Accept": "text/markdown",
557
+ "X-No-Cache": "true"
558
+ },
559
+ signal: AbortSignal.timeout(8e3)
560
+ });
561
+ if (!res.ok) return null;
562
+ const text = await res.text();
563
+ if (!text || text.length < 50) return null;
564
+ const titleMatch = text.match(/^Title:\s*(.+)/m);
565
+ let fallbackHost = url;
566
+ try {
567
+ fallbackHost = new URL(url).hostname;
568
+ } catch {
569
+ }
570
+ const title = titleMatch?.[1]?.trim() ?? fallbackHost;
571
+ return { title, content: text };
572
+ } catch {
573
+ return null;
574
+ }
575
+ }
576
+ async function syncFeishuLinks(ctx, _data, logger = defaultLogger) {
577
+ let extracted = 0, fetched = 0, failed = 0;
578
+ const sqlite = ctx.getSqlite();
579
+ try {
580
+ const { readObjectContents } = await import("./content-reader-VPGTR2SF.js");
581
+ const msgObjs = sqlite.prepare(`
582
+ SELECT o.id, o.file_path FROM objects o
583
+ WHERE o.source = 'feishu' AND o.source_type = 'message'
584
+ AND o.summary LIKE '%http%'
585
+ `).all();
586
+ const contentMap = readObjectContents(
587
+ sqlite,
588
+ msgObjs.map((o) => ({ id: o.id, filePath: o.file_path }))
589
+ );
590
+ const messageRows = [...contentMap.values()].filter((c) => c.includes("http")).map((c) => ({ content: c }));
591
+ const urlSet = /* @__PURE__ */ new Map();
592
+ for (const row of messageRows) {
593
+ const urls = extractUrlsFromContent(row.content);
594
+ for (const { url, text } of urls) {
595
+ if (!urlSet.has(url)) urlSet.set(url, text);
596
+ }
597
+ }
598
+ extracted = urlSet.size;
599
+ if (extracted === 0) {
600
+ logger.info("No URLs found in messages");
601
+ return { extracted, fetched, failed };
602
+ }
603
+ logger.info(`Found ${extracted} unique URLs in messages`);
604
+ let i = 0;
605
+ for (const [url, linkText] of urlSet) {
606
+ i++;
607
+ try {
608
+ new URL(url);
609
+ } catch {
610
+ failed++;
611
+ continue;
612
+ }
613
+ const uri = `feishu://link/${contentHash(url)}`;
614
+ if (ctx.exists(uri)) continue;
615
+ const result = await fetchUrlContent(url);
616
+ if (!result) {
617
+ failed++;
618
+ continue;
619
+ }
620
+ const title = result.title || linkText;
621
+ let hostname = "unknown";
622
+ try {
623
+ hostname = new URL(url).hostname;
624
+ } catch {
625
+ }
626
+ const fileContent = [
627
+ "---",
628
+ `source: feishu`,
629
+ `type: link`,
630
+ `url: ${url}`,
631
+ `title: "${title.replace(/"/g, '\\"')}"`,
632
+ `fetched: ${(/* @__PURE__ */ new Date()).toISOString()}`,
633
+ "---",
634
+ "",
635
+ result.content,
636
+ ""
637
+ ].join("\n");
638
+ const upsertResult = ctx.upsertObject({
639
+ source: "feishu",
640
+ sourceType: "link",
641
+ uri,
642
+ title,
643
+ tags: ["feishu", "link", hostname],
644
+ content: result.content,
645
+ contentType: "text/markdown",
646
+ fileContent,
647
+ fileMeta: { url }
648
+ });
649
+ if (upsertResult.action === "inserted") {
650
+ fetched++;
651
+ }
652
+ if (i % 10 === 0) {
653
+ logger.info(`Links progress: ${i}/${extracted} (${fetched} fetched, ${failed} failed)`);
654
+ }
655
+ await new Promise((r) => setTimeout(r, 500));
656
+ }
657
+ } catch (err) {
658
+ logger.error(`Link sync error: ${err}`);
659
+ }
660
+ logger.info("Link sync complete", { extracted, fetched, failed });
661
+ return { extracted, fetched, failed };
662
+ }
663
+
664
+ export {
665
+ getFeishuToken,
666
+ syncFeishu,
667
+ syncFeishuMeetings,
668
+ syncFeishuApprovals,
669
+ syncFeishuDocs,
670
+ syncFeishuLinks
671
+ };