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.
- package/dist/{chunk-ROIINI33.js → chunk-4PIT2GZ4.js} +13 -1
- package/dist/{chunk-XLYRHKG6.js → chunk-54SD5GBF.js} +1 -1
- package/dist/chunk-63AMINQC.js +156 -0
- package/dist/{chunk-XAEGXSEO.js → chunk-74AHY7X6.js} +4 -0
- package/dist/{chunk-7U3SXINY.js → chunk-ATAUWJYD.js} +320 -50
- package/dist/chunk-DQW74UCN.js +671 -0
- package/dist/chunk-EYP6WMFF.js +153 -0
- package/dist/{chunk-JSTXMDXI.js → chunk-FCFZCZHR.js} +1 -1
- package/dist/chunk-FX6Z3QHV.js +34 -0
- package/dist/chunk-HENAABEL.js +419 -0
- package/dist/chunk-OXWWOKC7.js +201 -0
- package/dist/chunk-QGHJ45PL.js +661 -0
- package/dist/chunk-RO3KK5RC.js +132 -0
- package/dist/{chunk-JE6TOU7W.js → chunk-TFMF3EXE.js} +2 -7
- package/dist/{chunk-TN327MDF.js → chunk-VX662YLA.js} +3 -3
- package/dist/cli.js +338 -149
- package/dist/{config-AI6UIJJN.js → config-FH2XLN7A.js} +2 -2
- package/dist/content-reader-VPGTR2SF.js +10 -0
- package/dist/context-ZNI3WOB7.js +10 -0
- package/dist/{credential-store-ZRZCSRPC.js → credential-store-OS5ZY4OW.js} +2 -2
- package/dist/{feishu-A6YVFKEN.js → feishu-XW5T6ER2.js} +8 -3
- package/dist/{git-manager-N35XSG4Y.js → git-manager-RVWV2GSV.js} +2 -1
- package/dist/github-PQKAYTLO.js +11 -0
- package/dist/{paths-JXOMBYIT.js → paths-FFRET6F7.js} +7 -3
- package/dist/{server-5GVWN2NB.js → server-WEADPUST.js} +59 -66
- package/dist/{setup-IDQDPCEJ.js → setup-S2S2CHB2.js} +91 -32
- package/dist/sync-SRLFR5NA.js +21 -0
- package/dist/transport.js +6 -4
- package/package.json +1 -1
- package/src/dashboard/public/app.js +34 -8
- package/src/dashboard/public/style.css +14 -0
- package/dist/chunk-AIXKXEYS.js +0 -547
- package/dist/chunk-L5ZR7TSK.js +0 -82
- package/dist/chunk-LS2AJM5A.js +0 -163
- package/dist/chunk-QMOFQX7X.js +0 -612
- package/dist/chunk-YJWTKFWX.js +0 -451
- package/dist/github-SHWUFNYB.js +0 -10
- package/dist/sync-7V54N62M.js +0 -18
package/dist/chunk-QMOFQX7X.js
DELETED
|
@@ -1,612 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createId
|
|
3
|
-
} from "./chunk-JE6TOU7W.js";
|
|
4
|
-
import {
|
|
5
|
-
logError,
|
|
6
|
-
logInfo
|
|
7
|
-
} from "./chunk-MYDK7MWB.js";
|
|
8
|
-
|
|
9
|
-
// src/sync/feishu.ts
|
|
10
|
-
import { createHash } from "crypto";
|
|
11
|
-
|
|
12
|
-
// src/sync/formatters.ts
|
|
13
|
-
function formatMessages(chatName, chatId, date, messages) {
|
|
14
|
-
const frontmatter = `---
|
|
15
|
-
source: feishu
|
|
16
|
-
type: messages
|
|
17
|
-
chat: ${chatName}
|
|
18
|
-
chat_id: ${chatId}
|
|
19
|
-
date: ${date}
|
|
20
|
-
---
|
|
21
|
-
|
|
22
|
-
`;
|
|
23
|
-
const body = messages.map((m) => `## ${m.time} \u2014 ${m.sender}
|
|
24
|
-
${m.content}`).join("\n\n");
|
|
25
|
-
return frontmatter + body + "\n";
|
|
26
|
-
}
|
|
27
|
-
function formatIssue(opts) {
|
|
28
|
-
return [
|
|
29
|
-
"---",
|
|
30
|
-
`source: ${opts.source}`,
|
|
31
|
-
`type: issue`,
|
|
32
|
-
`repo: ${opts.repo}`,
|
|
33
|
-
`number: ${opts.number}`,
|
|
34
|
-
`state: ${opts.state}`,
|
|
35
|
-
`author: ${opts.author}`,
|
|
36
|
-
`labels: [${opts.labels.join(", ")}]`,
|
|
37
|
-
`created: ${opts.created}`,
|
|
38
|
-
`uri: ${opts.uri}`,
|
|
39
|
-
"---",
|
|
40
|
-
"",
|
|
41
|
-
`# ${opts.repo}#${opts.number}: ${opts.title}`,
|
|
42
|
-
"",
|
|
43
|
-
opts.body || "(no description)",
|
|
44
|
-
""
|
|
45
|
-
].join("\n");
|
|
46
|
-
}
|
|
47
|
-
function formatPullRequest(opts) {
|
|
48
|
-
const branchLine = opts.sourceBranch && opts.targetBranch ? `branch: ${opts.sourceBranch} -> ${opts.targetBranch}
|
|
49
|
-
` : "";
|
|
50
|
-
return [
|
|
51
|
-
"---",
|
|
52
|
-
`source: ${opts.source}`,
|
|
53
|
-
`type: pull_request`,
|
|
54
|
-
`repo: ${opts.repo}`,
|
|
55
|
-
`number: ${opts.number}`,
|
|
56
|
-
`state: ${opts.state}`,
|
|
57
|
-
`author: ${opts.author}`,
|
|
58
|
-
`labels: [${opts.labels.join(", ")}]`,
|
|
59
|
-
`created: ${opts.created}`,
|
|
60
|
-
`uri: ${opts.uri}`,
|
|
61
|
-
branchLine ? `${branchLine.trim()}` : null,
|
|
62
|
-
"---",
|
|
63
|
-
"",
|
|
64
|
-
`# ${opts.repo}#${opts.number}: ${opts.title}`,
|
|
65
|
-
"",
|
|
66
|
-
opts.body || "(no description)",
|
|
67
|
-
""
|
|
68
|
-
].filter((line) => line !== null).join("\n");
|
|
69
|
-
}
|
|
70
|
-
function formatCalendarEvent(opts) {
|
|
71
|
-
return [
|
|
72
|
-
"---",
|
|
73
|
-
`source: ${opts.source}`,
|
|
74
|
-
`type: calendar_event`,
|
|
75
|
-
`title: ${opts.title}`,
|
|
76
|
-
`start: ${opts.startTime}`,
|
|
77
|
-
`end: ${opts.endTime}`,
|
|
78
|
-
`attendees: [${opts.attendees.join(", ")}]`,
|
|
79
|
-
`uri: ${opts.uri}`,
|
|
80
|
-
"---",
|
|
81
|
-
"",
|
|
82
|
-
`# ${opts.title}`,
|
|
83
|
-
"",
|
|
84
|
-
`**Time:** ${opts.startTime} \u2014 ${opts.endTime}`,
|
|
85
|
-
`**Attendees:** ${opts.attendees.join(", ") || "none"}`,
|
|
86
|
-
"",
|
|
87
|
-
opts.description || "(no description)",
|
|
88
|
-
""
|
|
89
|
-
].join("\n");
|
|
90
|
-
}
|
|
91
|
-
function formatApproval(opts) {
|
|
92
|
-
const fieldRows = opts.fields.map((f) => `| ${f.name} | ${f.value} |`).join("\n");
|
|
93
|
-
const table = opts.fields.length > 0 ? `| Field | Value |
|
|
94
|
-
|-------|-------|
|
|
95
|
-
${fieldRows}
|
|
96
|
-
` : "";
|
|
97
|
-
return [
|
|
98
|
-
"---",
|
|
99
|
-
`source: ${opts.source}`,
|
|
100
|
-
`type: approval`,
|
|
101
|
-
`name: ${opts.name}`,
|
|
102
|
-
`status: ${opts.status}`,
|
|
103
|
-
`submitter: ${opts.submitter}`,
|
|
104
|
-
`uri: ${opts.uri}`,
|
|
105
|
-
"---",
|
|
106
|
-
"",
|
|
107
|
-
`# ${opts.name}`,
|
|
108
|
-
"",
|
|
109
|
-
`**Status:** ${opts.status}`,
|
|
110
|
-
`**Submitter:** ${opts.submitter}`,
|
|
111
|
-
"",
|
|
112
|
-
table,
|
|
113
|
-
""
|
|
114
|
-
].join("\n");
|
|
115
|
-
}
|
|
116
|
-
function formatDocument(opts) {
|
|
117
|
-
return [
|
|
118
|
-
"---",
|
|
119
|
-
`source: ${opts.source}`,
|
|
120
|
-
`type: document`,
|
|
121
|
-
`title: ${opts.title}`,
|
|
122
|
-
`uri: ${opts.uri}`,
|
|
123
|
-
"---",
|
|
124
|
-
"",
|
|
125
|
-
`# ${opts.title}`,
|
|
126
|
-
"",
|
|
127
|
-
opts.body || "(no content)",
|
|
128
|
-
""
|
|
129
|
-
].join("\n");
|
|
130
|
-
}
|
|
131
|
-
function formatAnalytics(data) {
|
|
132
|
-
return JSON.stringify(data, null, 2) + "\n";
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// src/sync/feishu.ts
|
|
136
|
-
var defaultLogger = {
|
|
137
|
-
info: (msg, ctx) => logInfo("sync", msg, ctx),
|
|
138
|
-
warn: (msg, ctx) => logError("sync", msg, ctx),
|
|
139
|
-
error: (msg, ctx) => logError("sync", msg, ctx)
|
|
140
|
-
};
|
|
141
|
-
function contentHash(str) {
|
|
142
|
-
return createHash("sha256").update(str).digest("hex").slice(0, 16);
|
|
143
|
-
}
|
|
144
|
-
async function getFeishuToken(appId, appSecret) {
|
|
145
|
-
const res = await fetch("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal", {
|
|
146
|
-
method: "POST",
|
|
147
|
-
headers: { "Content-Type": "application/json" },
|
|
148
|
-
body: JSON.stringify({ app_id: appId, app_secret: appSecret })
|
|
149
|
-
});
|
|
150
|
-
const data = await res.json();
|
|
151
|
-
if (data.code !== 0) throw new Error(`Feishu auth failed: code ${data.code}`);
|
|
152
|
-
return data.tenant_access_token;
|
|
153
|
-
}
|
|
154
|
-
async function syncFeishu(sqlite, data, logger = defaultLogger, fileWriter) {
|
|
155
|
-
const { appId, appSecret } = data;
|
|
156
|
-
if (!appId || !appSecret) throw new Error("Missing Feishu credentials");
|
|
157
|
-
const token = await getFeishuToken(appId, appSecret);
|
|
158
|
-
const chatsRes = await fetch("https://open.feishu.cn/open-apis/im/v1/chats?page_size=50", {
|
|
159
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
160
|
-
});
|
|
161
|
-
const chatsData = await chatsRes.json();
|
|
162
|
-
if (chatsData.code !== 0) throw new Error(`Failed to list chats: code ${chatsData.code}`);
|
|
163
|
-
const chats = chatsData.data?.items ?? [];
|
|
164
|
-
let totalMessages = 0;
|
|
165
|
-
let newMessages = 0;
|
|
166
|
-
const checkExists = sqlite.prepare("SELECT id FROM objects WHERE uri = ?");
|
|
167
|
-
const insertObj = sqlite.prepare(`
|
|
168
|
-
INSERT INTO objects (id, source, source_type, uri, title, summary, tags, content_hash, last_synced_at, created_at)
|
|
169
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
170
|
-
`);
|
|
171
|
-
const insertBody = sqlite.prepare(`
|
|
172
|
-
INSERT OR REPLACE INTO object_bodies (object_id, content, content_type, fetched_at)
|
|
173
|
-
VALUES (?, ?, ?, ?)
|
|
174
|
-
`);
|
|
175
|
-
const insertFts = sqlite.prepare(`
|
|
176
|
-
INSERT INTO objects_fts(rowid, title, summary, tags, source, source_type, body_excerpt)
|
|
177
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
178
|
-
`);
|
|
179
|
-
const getRowid = sqlite.prepare("SELECT rowid FROM objects WHERE id = ?");
|
|
180
|
-
const dayMessages = /* @__PURE__ */ new Map();
|
|
181
|
-
for (const chat of chats) {
|
|
182
|
-
const cursorRow = sqlite.prepare("SELECT cursor FROM sync_cursors WHERE connector_id = ?").get(`feishu:${chat.chat_id}`);
|
|
183
|
-
let pageToken = cursorRow?.cursor ?? void 0;
|
|
184
|
-
let hasMore = true;
|
|
185
|
-
while (hasMore) {
|
|
186
|
-
const url = new URL("https://open.feishu.cn/open-apis/im/v1/messages");
|
|
187
|
-
url.searchParams.set("container_id_type", "chat");
|
|
188
|
-
url.searchParams.set("container_id", chat.chat_id);
|
|
189
|
-
url.searchParams.set("page_size", "50");
|
|
190
|
-
url.searchParams.set("sort_type", "ByCreateTimeAsc");
|
|
191
|
-
if (pageToken) url.searchParams.set("page_token", pageToken);
|
|
192
|
-
const msgRes = await fetch(url.toString(), {
|
|
193
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
194
|
-
});
|
|
195
|
-
const msgData = await msgRes.json();
|
|
196
|
-
if (msgData.code !== 0) {
|
|
197
|
-
if (msgData.code === 99991400) {
|
|
198
|
-
logger.warn(`Rate limited on ${chat.name}, waiting 5s`);
|
|
199
|
-
await new Promise((r) => setTimeout(r, 5e3));
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
logger.warn(`Failed to get messages from "${chat.name}": code ${msgData.code}`);
|
|
203
|
-
break;
|
|
204
|
-
}
|
|
205
|
-
const messages = msgData.data?.items ?? [];
|
|
206
|
-
const batchInsert = sqlite.transaction((msgs) => {
|
|
207
|
-
for (const msg of msgs) {
|
|
208
|
-
if (msg.msg_type !== "text" && msg.msg_type !== "post") continue;
|
|
209
|
-
let content = "";
|
|
210
|
-
try {
|
|
211
|
-
const bodyContent = JSON.parse(msg.body?.content ?? "{}");
|
|
212
|
-
const raw = bodyContent.text ?? bodyContent.content ?? bodyContent;
|
|
213
|
-
content = typeof raw === "string" ? raw : JSON.stringify(raw);
|
|
214
|
-
} catch {
|
|
215
|
-
content = msg.body?.content ?? "";
|
|
216
|
-
}
|
|
217
|
-
if (!content || typeof content !== "string") continue;
|
|
218
|
-
const uri = `feishu://message/${msg.message_id}`;
|
|
219
|
-
const existing = checkExists.get(uri);
|
|
220
|
-
if (existing) continue;
|
|
221
|
-
const hash = contentHash(content);
|
|
222
|
-
const now = Date.now();
|
|
223
|
-
const id = createId("obj");
|
|
224
|
-
const createTime = msg.create_time ? parseInt(msg.create_time) : now;
|
|
225
|
-
const summary = content.length > 200 ? content.slice(0, 200) + "..." : content;
|
|
226
|
-
const tags = JSON.stringify(["feishu", "message"]);
|
|
227
|
-
insertObj.run(id, "feishu", "message", uri, chat.name, summary, tags, hash, now, createTime);
|
|
228
|
-
insertBody.run(id, content, "text/plain", now);
|
|
229
|
-
try {
|
|
230
|
-
const rowid = getRowid.get(id);
|
|
231
|
-
if (rowid) {
|
|
232
|
-
const excerpt = content.length > 500 ? content.slice(0, 500) : content;
|
|
233
|
-
insertFts.run(rowid.rowid, chat.name ?? "", summary ?? "", tags, "feishu", "message", excerpt);
|
|
234
|
-
}
|
|
235
|
-
} catch {
|
|
236
|
-
}
|
|
237
|
-
if (fileWriter) {
|
|
238
|
-
const date = new Date(createTime).toISOString().slice(0, 10);
|
|
239
|
-
const time = new Date(createTime).toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" });
|
|
240
|
-
const key = `${chat.chat_id}:${date}`;
|
|
241
|
-
let group = dayMessages.get(key);
|
|
242
|
-
if (!group) {
|
|
243
|
-
group = { chatName: chat.name, chatId: chat.chat_id, date, messages: [] };
|
|
244
|
-
dayMessages.set(key, group);
|
|
245
|
-
}
|
|
246
|
-
group.messages.push({ time, sender: msg.sender?.id ?? "unknown", content });
|
|
247
|
-
}
|
|
248
|
-
newMessages++;
|
|
249
|
-
}
|
|
250
|
-
});
|
|
251
|
-
for (let i = 0; i < messages.length; i += 100) {
|
|
252
|
-
batchInsert(messages.slice(i, i + 100));
|
|
253
|
-
}
|
|
254
|
-
totalMessages += messages.length;
|
|
255
|
-
hasMore = msgData.data.has_more;
|
|
256
|
-
pageToken = msgData.data.page_token;
|
|
257
|
-
if (pageToken) {
|
|
258
|
-
sqlite.prepare("INSERT OR REPLACE INTO sync_cursors (connector_id, cursor, last_synced_at) VALUES (?, ?, ?)").run(`feishu:${chat.chat_id}`, pageToken, Date.now());
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
if (fileWriter && dayMessages.size > 0) {
|
|
263
|
-
for (const group of dayMessages.values()) {
|
|
264
|
-
try {
|
|
265
|
-
const filePath = fileWriter.appendMessages(
|
|
266
|
-
"feishu",
|
|
267
|
-
group.chatName,
|
|
268
|
-
group.chatId,
|
|
269
|
-
group.date,
|
|
270
|
-
group.messages
|
|
271
|
-
);
|
|
272
|
-
sqlite.prepare(
|
|
273
|
-
`UPDATE objects SET file_path = ? WHERE source = 'feishu' AND source_type = 'message' AND title = ? AND file_path IS NULL`
|
|
274
|
-
).run(filePath, group.chatName);
|
|
275
|
-
} catch (err) {
|
|
276
|
-
logger.warn(`Failed to write messages file for ${group.chatName}/${group.date}: ${err}`);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
logger.info("Feishu sync complete", { totalMessages, newMessages, chats: chats.length });
|
|
281
|
-
return { totalMessages, newMessages, chats: chats.length };
|
|
282
|
-
}
|
|
283
|
-
async function syncFeishuMeetings(sqlite, data, logger = defaultLogger, fileWriter) {
|
|
284
|
-
const { appId, appSecret } = data;
|
|
285
|
-
if (!appId || !appSecret) throw new Error("Missing Feishu credentials");
|
|
286
|
-
const token = await getFeishuToken(appId, appSecret);
|
|
287
|
-
let meetings = 0, newObjects = 0;
|
|
288
|
-
try {
|
|
289
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
290
|
-
const weekAgo = now - 7 * 24 * 60 * 60;
|
|
291
|
-
const calRes = await fetch("https://open.feishu.cn/open-apis/calendar/v4/calendars?page_size=10", {
|
|
292
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
293
|
-
});
|
|
294
|
-
const calData = await calRes.json();
|
|
295
|
-
if (calData.code !== 0 || !calData.data?.items) {
|
|
296
|
-
logger.warn(`Calendar API returned code ${calData.code}`);
|
|
297
|
-
return { meetings, newObjects };
|
|
298
|
-
}
|
|
299
|
-
const checkExists = sqlite.prepare("SELECT id FROM objects WHERE uri = ?");
|
|
300
|
-
const insertObj = sqlite.prepare(`
|
|
301
|
-
INSERT INTO objects (id, source, source_type, uri, title, summary, tags, content_hash, last_synced_at, created_at)
|
|
302
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
303
|
-
`);
|
|
304
|
-
const insertBody = sqlite.prepare(`
|
|
305
|
-
INSERT OR REPLACE INTO object_bodies (object_id, content, content_type, fetched_at)
|
|
306
|
-
VALUES (?, ?, ?, ?)
|
|
307
|
-
`);
|
|
308
|
-
const insertFts = sqlite.prepare(`
|
|
309
|
-
INSERT INTO objects_fts(rowid, title, summary, tags, source, source_type, body_excerpt)
|
|
310
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
311
|
-
`);
|
|
312
|
-
const getRowid = sqlite.prepare("SELECT rowid FROM objects WHERE id = ?");
|
|
313
|
-
for (const cal of calData.data.items) {
|
|
314
|
-
const eventsRes = await fetch(
|
|
315
|
-
`https://open.feishu.cn/open-apis/calendar/v4/calendars/${cal.calendar_id}/events?start_time=${weekAgo}&end_time=${now}&page_size=50`,
|
|
316
|
-
{ headers: { Authorization: `Bearer ${token}` } }
|
|
317
|
-
);
|
|
318
|
-
const eventsData = await eventsRes.json();
|
|
319
|
-
if (eventsData.code !== 0 || !eventsData.data?.items) continue;
|
|
320
|
-
const batch = sqlite.transaction((events) => {
|
|
321
|
-
for (const event of events) {
|
|
322
|
-
const uri = `feishu://calendar/${cal.calendar_id}/event/${event.event_id}`;
|
|
323
|
-
if (checkExists.get(uri)) continue;
|
|
324
|
-
const nowMs = Date.now();
|
|
325
|
-
const id = createId("obj");
|
|
326
|
-
const attendees = event.attendees?.map((a) => a.display_name).join(", ") ?? "";
|
|
327
|
-
const startTs = parseInt(event.start_time?.timestamp ?? "0") * 1e3;
|
|
328
|
-
const startTime = new Date(startTs).toLocaleString("zh-CN");
|
|
329
|
-
const summary = `${event.summary} (${startTime}, ${attendees || "no attendees"})`;
|
|
330
|
-
const body = [
|
|
331
|
-
`Meeting: ${event.summary}`,
|
|
332
|
-
`Time: ${startTime}`,
|
|
333
|
-
`Attendees: ${attendees}`,
|
|
334
|
-
`Description: ${event.description || "(none)"}`
|
|
335
|
-
].join("\n");
|
|
336
|
-
const tags = JSON.stringify(["feishu", "calendar", "meeting"]);
|
|
337
|
-
insertObj.run(id, "feishu", "calendar_event", uri, event.summary || "Untitled meeting", summary, tags, contentHash(body), nowMs, startTs);
|
|
338
|
-
insertBody.run(id, body, "text/plain", nowMs);
|
|
339
|
-
try {
|
|
340
|
-
const rowid = getRowid.get(id);
|
|
341
|
-
if (rowid) {
|
|
342
|
-
const excerpt = body.length > 500 ? body.slice(0, 500) : body;
|
|
343
|
-
insertFts.run(rowid.rowid, event.summary ?? "", summary ?? "", tags, "feishu", "calendar_event", excerpt);
|
|
344
|
-
}
|
|
345
|
-
} catch {
|
|
346
|
-
}
|
|
347
|
-
if (fileWriter) {
|
|
348
|
-
try {
|
|
349
|
-
const attendeeNames = event.attendees?.map((a) => a.display_name) ?? [];
|
|
350
|
-
const endTs = parseInt(event.end_time?.timestamp ?? "0") * 1e3;
|
|
351
|
-
const endTime = new Date(endTs).toLocaleString("zh-CN");
|
|
352
|
-
const date = new Date(startTs).toISOString().slice(0, 10);
|
|
353
|
-
const fileContent = formatCalendarEvent({
|
|
354
|
-
source: "feishu",
|
|
355
|
-
title: event.summary || "Untitled meeting",
|
|
356
|
-
startTime,
|
|
357
|
-
endTime,
|
|
358
|
-
attendees: attendeeNames,
|
|
359
|
-
description: event.description || "",
|
|
360
|
-
uri
|
|
361
|
-
});
|
|
362
|
-
const filePath = fileWriter.writeObject("feishu", "calendar_event", {
|
|
363
|
-
id,
|
|
364
|
-
title: event.summary,
|
|
365
|
-
date
|
|
366
|
-
}, fileContent);
|
|
367
|
-
sqlite.prepare("UPDATE objects SET file_path = ? WHERE id = ?").run(filePath, id);
|
|
368
|
-
} catch {
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
meetings++;
|
|
372
|
-
newObjects++;
|
|
373
|
-
}
|
|
374
|
-
});
|
|
375
|
-
batch(eventsData.data.items);
|
|
376
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
377
|
-
}
|
|
378
|
-
} catch (err) {
|
|
379
|
-
logger.error(`Meeting sync error: ${err}`);
|
|
380
|
-
}
|
|
381
|
-
logger.info("Meeting sync complete", { meetings, newObjects });
|
|
382
|
-
return { meetings, newObjects };
|
|
383
|
-
}
|
|
384
|
-
async function syncFeishuApprovals(sqlite, data, logger = defaultLogger, fileWriter) {
|
|
385
|
-
const { appId, appSecret } = data;
|
|
386
|
-
if (!appId || !appSecret) throw new Error("Missing Feishu credentials");
|
|
387
|
-
const token = await getFeishuToken(appId, appSecret);
|
|
388
|
-
let approvals = 0, newObjects = 0;
|
|
389
|
-
try {
|
|
390
|
-
const res = await fetch("https://open.feishu.cn/open-apis/approval/v4/instances?page_size=50", {
|
|
391
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
392
|
-
});
|
|
393
|
-
if (!res.ok) {
|
|
394
|
-
logger.warn(`Approval API: ${res.status} (may need approval:approval:readonly scope)`);
|
|
395
|
-
return { approvals, newObjects };
|
|
396
|
-
}
|
|
397
|
-
const resData = await res.json();
|
|
398
|
-
if (resData.code !== 0 || !resData.data?.items) {
|
|
399
|
-
logger.warn(`Approval list returned code ${resData.code}`);
|
|
400
|
-
return { approvals, newObjects };
|
|
401
|
-
}
|
|
402
|
-
const checkExists = sqlite.prepare("SELECT id FROM objects WHERE uri = ?");
|
|
403
|
-
const insertObj = sqlite.prepare(`
|
|
404
|
-
INSERT INTO objects (id, source, source_type, uri, title, summary, tags, content_hash, last_synced_at, created_at)
|
|
405
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
406
|
-
`);
|
|
407
|
-
const insertBody = sqlite.prepare(`
|
|
408
|
-
INSERT OR REPLACE INTO object_bodies (object_id, content, content_type, fetched_at)
|
|
409
|
-
VALUES (?, ?, ?, ?)
|
|
410
|
-
`);
|
|
411
|
-
const insertFts = sqlite.prepare(`
|
|
412
|
-
INSERT INTO objects_fts(rowid, title, summary, tags, source, source_type, body_excerpt)
|
|
413
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
414
|
-
`);
|
|
415
|
-
const getRowid = sqlite.prepare("SELECT rowid FROM objects WHERE id = ?");
|
|
416
|
-
const batch = sqlite.transaction((items) => {
|
|
417
|
-
for (const approval of items) {
|
|
418
|
-
const uri = `feishu://approval/${approval.instance_id}`;
|
|
419
|
-
if (checkExists.get(uri)) continue;
|
|
420
|
-
const nowMs = Date.now();
|
|
421
|
-
const id = createId("obj");
|
|
422
|
-
const summary = `${approval.approval_name} [${approval.status}]`;
|
|
423
|
-
const tags = JSON.stringify(["feishu", "approval", approval.status]);
|
|
424
|
-
let formText = "";
|
|
425
|
-
try {
|
|
426
|
-
const formData = JSON.parse(approval.form || "[]");
|
|
427
|
-
formText = Array.isArray(formData) ? formData.map((f) => `${f.name}: ${f.value}`).join("\n") : JSON.stringify(formData);
|
|
428
|
-
} catch {
|
|
429
|
-
formText = approval.form || "";
|
|
430
|
-
}
|
|
431
|
-
const body = [
|
|
432
|
-
`Approval: ${approval.approval_name}`,
|
|
433
|
-
`Status: ${approval.status}`,
|
|
434
|
-
`Submitted: ${approval.start_time}`,
|
|
435
|
-
`Completed: ${approval.end_time || "pending"}`,
|
|
436
|
-
"",
|
|
437
|
-
formText
|
|
438
|
-
].join("\n");
|
|
439
|
-
const startTime = approval.start_time ? new Date(approval.start_time).getTime() : nowMs;
|
|
440
|
-
insertObj.run(id, "feishu", "approval", uri, approval.approval_name, summary, tags, contentHash(body), nowMs, startTime);
|
|
441
|
-
insertBody.run(id, body, "text/plain", nowMs);
|
|
442
|
-
try {
|
|
443
|
-
const rowid = getRowid.get(id);
|
|
444
|
-
if (rowid) {
|
|
445
|
-
const excerpt = body.length > 500 ? body.slice(0, 500) : body;
|
|
446
|
-
insertFts.run(rowid.rowid, approval.approval_name ?? "", summary ?? "", tags, "feishu", "approval", excerpt);
|
|
447
|
-
}
|
|
448
|
-
} catch {
|
|
449
|
-
}
|
|
450
|
-
if (fileWriter) {
|
|
451
|
-
try {
|
|
452
|
-
let formFields = [];
|
|
453
|
-
try {
|
|
454
|
-
const fd = JSON.parse(approval.form || "[]");
|
|
455
|
-
if (Array.isArray(fd)) formFields = fd;
|
|
456
|
-
} catch {
|
|
457
|
-
}
|
|
458
|
-
const fileContent = formatApproval({
|
|
459
|
-
source: "feishu",
|
|
460
|
-
name: approval.approval_name,
|
|
461
|
-
status: approval.status,
|
|
462
|
-
submitter: approval.user_id || "unknown",
|
|
463
|
-
fields: formFields,
|
|
464
|
-
uri
|
|
465
|
-
});
|
|
466
|
-
const filePath = fileWriter.writeObject("feishu", "approval", {
|
|
467
|
-
id,
|
|
468
|
-
title: approval.approval_name
|
|
469
|
-
}, fileContent);
|
|
470
|
-
sqlite.prepare("UPDATE objects SET file_path = ? WHERE id = ?").run(filePath, id);
|
|
471
|
-
} catch {
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
approvals++;
|
|
475
|
-
newObjects++;
|
|
476
|
-
}
|
|
477
|
-
});
|
|
478
|
-
batch(resData.data.items);
|
|
479
|
-
} catch (err) {
|
|
480
|
-
logger.error(`Approval sync error: ${err}`);
|
|
481
|
-
}
|
|
482
|
-
logger.info("Approval sync complete", { approvals, newObjects });
|
|
483
|
-
return { approvals, newObjects };
|
|
484
|
-
}
|
|
485
|
-
async function syncFeishuDocs(sqlite, data, logger = defaultLogger, fileWriter) {
|
|
486
|
-
const { appId, appSecret } = data;
|
|
487
|
-
if (!appId || !appSecret) throw new Error("Missing Feishu credentials");
|
|
488
|
-
const token = await getFeishuToken(appId, appSecret);
|
|
489
|
-
let docs = 0, newObjects = 0;
|
|
490
|
-
const checkExists = sqlite.prepare("SELECT id FROM objects WHERE uri = ?");
|
|
491
|
-
const insertObj = sqlite.prepare(`
|
|
492
|
-
INSERT INTO objects (id, source, source_type, uri, title, summary, tags, content_hash, last_synced_at, created_at)
|
|
493
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
494
|
-
`);
|
|
495
|
-
const insertBody = sqlite.prepare(`
|
|
496
|
-
INSERT OR REPLACE INTO object_bodies (object_id, content, content_type, fetched_at)
|
|
497
|
-
VALUES (?, ?, ?, ?)
|
|
498
|
-
`);
|
|
499
|
-
const insertFts = sqlite.prepare(`
|
|
500
|
-
INSERT INTO objects_fts(rowid, title, summary, tags, source, source_type, body_excerpt)
|
|
501
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
502
|
-
`);
|
|
503
|
-
const getRowid = sqlite.prepare("SELECT rowid FROM objects WHERE id = ?");
|
|
504
|
-
try {
|
|
505
|
-
const spacesRes = await fetch("https://open.feishu.cn/open-apis/wiki/v2/spaces?page_size=10", {
|
|
506
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
507
|
-
});
|
|
508
|
-
const spacesData = await spacesRes.json();
|
|
509
|
-
if (spacesData.code === 0 && spacesData.data?.items?.length) {
|
|
510
|
-
for (const space of spacesData.data.items) {
|
|
511
|
-
const nodesRes = await fetch(
|
|
512
|
-
`https://open.feishu.cn/open-apis/wiki/v2/spaces/${space.space_id}/nodes?page_size=50`,
|
|
513
|
-
{ headers: { Authorization: `Bearer ${token}` } }
|
|
514
|
-
);
|
|
515
|
-
const nodesData = await nodesRes.json();
|
|
516
|
-
if (nodesData.code !== 0 || !nodesData.data?.items) continue;
|
|
517
|
-
const batch = sqlite.transaction((nodes) => {
|
|
518
|
-
for (const node of nodes) {
|
|
519
|
-
const uri = `feishu://wiki/${space.space_id}/${node.node_token}`;
|
|
520
|
-
if (checkExists.get(uri)) continue;
|
|
521
|
-
const nowMs = Date.now();
|
|
522
|
-
const id = createId("obj");
|
|
523
|
-
const tags = JSON.stringify(["feishu", "document", node.obj_type]);
|
|
524
|
-
const docBody = `Wiki: ${node.title} (${node.obj_type}, space: ${space.name})`;
|
|
525
|
-
insertObj.run(id, "feishu", "document", uri, node.title, node.title, tags, contentHash(node.title + node.node_token), nowMs, nowMs);
|
|
526
|
-
insertBody.run(id, docBody, "text/plain", nowMs);
|
|
527
|
-
try {
|
|
528
|
-
const rowid = getRowid.get(id);
|
|
529
|
-
if (rowid) {
|
|
530
|
-
insertFts.run(rowid.rowid, node.title ?? "", node.title ?? "", tags, "feishu", "document", `Wiki: ${node.title}`);
|
|
531
|
-
}
|
|
532
|
-
} catch {
|
|
533
|
-
}
|
|
534
|
-
if (fileWriter) {
|
|
535
|
-
try {
|
|
536
|
-
const fileContent = formatDocument({ source: "feishu", title: node.title, uri, body: docBody });
|
|
537
|
-
const filePath = fileWriter.writeObject("feishu", "document", { id, title: node.title }, fileContent);
|
|
538
|
-
sqlite.prepare("UPDATE objects SET file_path = ? WHERE id = ?").run(filePath, id);
|
|
539
|
-
} catch {
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
docs++;
|
|
543
|
-
newObjects++;
|
|
544
|
-
}
|
|
545
|
-
});
|
|
546
|
-
batch(nodesData.data.items);
|
|
547
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
548
|
-
}
|
|
549
|
-
} else {
|
|
550
|
-
const searchRes = await fetch("https://open.feishu.cn/open-apis/wiki/v1/nodes/search", {
|
|
551
|
-
method: "POST",
|
|
552
|
-
headers: {
|
|
553
|
-
Authorization: `Bearer ${token}`,
|
|
554
|
-
"Content-Type": "application/json"
|
|
555
|
-
},
|
|
556
|
-
body: JSON.stringify({ query: "", page_size: 50 })
|
|
557
|
-
});
|
|
558
|
-
if (searchRes.ok) {
|
|
559
|
-
const searchData = await searchRes.json();
|
|
560
|
-
if (searchData.code === 0 && searchData.data?.items) {
|
|
561
|
-
const batch = sqlite.transaction((nodes) => {
|
|
562
|
-
for (const node of nodes) {
|
|
563
|
-
const uri = `feishu://wiki/${node.space_id}/${node.node_token}`;
|
|
564
|
-
if (checkExists.get(uri)) continue;
|
|
565
|
-
const nowMs = Date.now();
|
|
566
|
-
const id = createId("obj");
|
|
567
|
-
const tags = JSON.stringify(["feishu", "document", node.obj_type]);
|
|
568
|
-
const docBody = `Wiki: ${node.title} (${node.obj_type})`;
|
|
569
|
-
insertObj.run(id, "feishu", "document", uri, node.title, node.title, tags, contentHash(node.title + node.node_token), nowMs, nowMs);
|
|
570
|
-
insertBody.run(id, docBody, "text/plain", nowMs);
|
|
571
|
-
try {
|
|
572
|
-
const rowid = getRowid.get(id);
|
|
573
|
-
if (rowid) {
|
|
574
|
-
insertFts.run(rowid.rowid, node.title ?? "", node.title ?? "", tags, "feishu", "document", `Wiki: ${node.title}`);
|
|
575
|
-
}
|
|
576
|
-
} catch {
|
|
577
|
-
}
|
|
578
|
-
if (fileWriter) {
|
|
579
|
-
try {
|
|
580
|
-
const fileContent = formatDocument({ source: "feishu", title: node.title, uri, body: docBody });
|
|
581
|
-
const filePath = fileWriter.writeObject("feishu", "document", { id, title: node.title }, fileContent);
|
|
582
|
-
sqlite.prepare("UPDATE objects SET file_path = ? WHERE id = ?").run(filePath, id);
|
|
583
|
-
} catch {
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
docs++;
|
|
587
|
-
newObjects++;
|
|
588
|
-
}
|
|
589
|
-
});
|
|
590
|
-
batch(searchData.data.items);
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
} catch (err) {
|
|
595
|
-
logger.error(`Document sync error: ${err}`);
|
|
596
|
-
}
|
|
597
|
-
logger.info("Document sync complete", { docs, newObjects });
|
|
598
|
-
return { docs, newObjects };
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
export {
|
|
602
|
-
formatMessages,
|
|
603
|
-
formatIssue,
|
|
604
|
-
formatPullRequest,
|
|
605
|
-
formatAnalytics,
|
|
606
|
-
contentHash,
|
|
607
|
-
getFeishuToken,
|
|
608
|
-
syncFeishu,
|
|
609
|
-
syncFeishuMeetings,
|
|
610
|
-
syncFeishuApprovals,
|
|
611
|
-
syncFeishuDocs
|
|
612
|
-
};
|