@sireai/optimus 0.1.44 → 0.1.46
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/cli/feedback-delivery.js +13 -8
- package/dist/cli/feedback-delivery.js.map +1 -1
- package/dist/cli/optimus.js +370 -63
- package/dist/cli/optimus.js.map +1 -1
- package/dist/integrations/feishu/feishu-client.d.ts +13 -0
- package/dist/integrations/feishu/feishu-client.js +111 -6
- package/dist/integrations/feishu/feishu-client.js.map +1 -1
- package/dist/integrations/feishu/feishu-reference-material-downloader.d.ts +34 -0
- package/dist/integrations/feishu/feishu-reference-material-downloader.js +572 -0
- package/dist/integrations/feishu/feishu-reference-material-downloader.js.map +1 -0
- package/dist/integrations/feishu/feishu-token-store.d.ts +25 -1
- package/dist/integrations/feishu/feishu-token-store.js +148 -4
- package/dist/integrations/feishu/feishu-token-store.js.map +1 -1
- package/dist/integrations/feishu/feishu-user-auth-service.d.ts +39 -0
- package/dist/integrations/feishu/feishu-user-auth-service.js +228 -0
- package/dist/integrations/feishu/feishu-user-auth-service.js.map +1 -0
- package/dist/integrations/jira/jira-cli.js +127 -19
- package/dist/integrations/jira/jira-cli.js.map +1 -1
- package/dist/task-environment/delivery/commit-message/coder-commit-message-template.d.ts +12 -0
- package/dist/task-environment/delivery/commit-message/coder-commit-message-template.js +105 -0
- package/dist/task-environment/delivery/commit-message/coder-commit-message-template.js.map +1 -0
- package/dist/task-environment/delivery/commit-message/commit-message-builder.js +2 -1
- package/dist/task-environment/delivery/commit-message/commit-message-builder.js.map +1 -1
- package/dist/task-environment/delivery/feishu-analysis-doc-service.d.ts +6 -0
- package/dist/task-environment/delivery/feishu-analysis-doc-service.js +106 -34
- package/dist/task-environment/delivery/feishu-analysis-doc-service.js.map +1 -1
- package/dist/task-environment/delivery/feishu-content/feishu-content-renderer.js +16 -1
- package/dist/task-environment/delivery/feishu-content/feishu-content-renderer.js.map +1 -1
- package/dist/task-environment/delivery/feishu-content/feishu-copy-config.d.ts +6 -0
- package/dist/task-environment/delivery/feishu-content/feishu-copy-config.js +19 -0
- package/dist/task-environment/delivery/feishu-content/feishu-copy-config.js.map +1 -1
- package/dist/task-environment/delivery/feishu-notifier.js +13 -11
- package/dist/task-environment/delivery/feishu-notifier.js.map +1 -1
- package/dist/task-environment/delivery/feishu-templates/coder-message-template.d.ts +6 -0
- package/dist/task-environment/delivery/feishu-templates/coder-message-template.js +58 -0
- package/dist/task-environment/delivery/feishu-templates/coder-message-template.js.map +1 -0
- package/dist/task-environment/delivery/feishu-templates/template-registry.js +2 -0
- package/dist/task-environment/delivery/feishu-templates/template-registry.js.map +1 -1
- package/dist/task-environment/delivery/task-delivery-dispatcher.js +6 -0
- package/dist/task-environment/delivery/task-delivery-dispatcher.js.map +1 -1
- package/dist/task-environment/delivery/task-delivery-service.d.ts +1 -0
- package/dist/task-environment/delivery/task-delivery-service.js +124 -8
- package/dist/task-environment/delivery/task-delivery-service.js.map +1 -1
- package/dist/task-environment/delivery/task-publication-service.js +9 -6
- package/dist/task-environment/delivery/task-publication-service.js.map +1 -1
- package/dist/task-environment/document-input/document-structure.d.ts +13 -0
- package/dist/task-environment/document-input/document-structure.js +438 -0
- package/dist/task-environment/document-input/document-structure.js.map +1 -0
- package/dist/task-environment/intake/manual-problem-intake.js +36 -0
- package/dist/task-environment/intake/manual-problem-intake.js.map +1 -1
- package/dist/task-environment/observability/logger.d.ts +1 -0
- package/dist/task-environment/observability/logger.js +26 -0
- package/dist/task-environment/observability/logger.js.map +1 -1
- package/dist/task-environment/observability/runtime-panel.js +10 -1
- package/dist/task-environment/observability/runtime-panel.js.map +1 -1
- package/dist/task-environment/orchestration/reference-material-relocator.d.ts +2 -0
- package/dist/task-environment/orchestration/reference-material-relocator.js +69 -0
- package/dist/task-environment/orchestration/reference-material-relocator.js.map +1 -0
- package/dist/task-environment/orchestration/task-orchestrator.d.ts +3 -0
- package/dist/task-environment/orchestration/task-orchestrator.js +182 -8
- package/dist/task-environment/orchestration/task-orchestrator.js.map +1 -1
- package/dist/task-environment/orchestration/task-package-inputs.js +7 -1
- package/dist/task-environment/orchestration/task-package-inputs.js.map +1 -1
- package/dist/task-environment/orchestration/task-runtime-policy.js +11 -0
- package/dist/task-environment/orchestration/task-runtime-policy.js.map +1 -1
- package/dist/task-environment/orchestration/triage-runner.js +3 -0
- package/dist/task-environment/orchestration/triage-runner.js.map +1 -1
- package/dist/task-environment/runtime/optimus-runtime.js +3 -0
- package/dist/task-environment/runtime/optimus-runtime.js.map +1 -1
- package/dist/task-environment/storage/sqlite-task-store.d.ts +4 -0
- package/dist/task-environment/storage/sqlite-task-store.js +70 -1
- package/dist/task-environment/storage/sqlite-task-store.js.map +1 -1
- package/dist/task-environment/task-handler-descriptor.d.ts +5 -0
- package/dist/task-environment/task-handler-descriptor.js +15 -0
- package/dist/task-environment/task-handler-descriptor.js.map +1 -0
- package/dist/types.d.ts +47 -1
- package/embedded-skills/shared/feishu-task-inputs/SKILL.md +56 -0
- package/embedded-skills/shared/feishu-task-inputs/scripts/fetch-feishu-doc.mjs +756 -0
- package/embedded-skills/shared/feishu-task-inputs/skill.json +5 -0
- package/package.json +4 -1
- package/task-harnesses/coder/ACCEPT.md +73 -0
- package/task-harnesses/coder/CONSTRAINTS.md +72 -0
- package/task-harnesses/coder/CONTEXT.md +36 -0
- package/task-harnesses/coder/EVOLUTION.md +83 -0
- package/task-harnesses/coder/ROLE.md +39 -0
- package/task-harnesses/coder/STANDARD.md +258 -0
- package/task-harnesses/coder/manifest.json +13 -0
- package/task-harnesses/pm/ACCEPT.md +7 -0
- package/task-harnesses/pm/CONSTRAINTS.md +5 -0
- package/task-harnesses/pm/ROLE.md +5 -8
- package/task-harnesses/pm/STANDARD.md +83 -124
- package/task-harnesses/registry.json +4 -0
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { basename, extname, join } from "node:path";
|
|
5
|
+
|
|
6
|
+
const CREDENTIAL_XOR_KEY = "feishu@2026!Mi";
|
|
7
|
+
const DEFAULT_FEISHU_BASE_URL = "https://open.feishu.cn";
|
|
8
|
+
const BUILTIN_APP_ID = decodeCredential("0509002c09147854030b5044285e005c50100b47");
|
|
9
|
+
const BUILTIN_APP_SECRET = decodeCredential("2c5513430a307864685d574a391a2d0e1f0629010754634a5e5b0151042a0604");
|
|
10
|
+
const FEISHU_DOCUMENT_HOST_PATTERN = /^(?:[\w-]+\.)?(?:feishu\.cn|larksuite\.com)$/iu;
|
|
11
|
+
const DOCX_PATH_PATTERN = /\/docx\/([A-Za-z0-9]+)/u;
|
|
12
|
+
const WIKI_PATH_PATTERN = /\/wiki\/([A-Za-z0-9]+)/u;
|
|
13
|
+
const CHILDREN_PAGE_SIZE = "200";
|
|
14
|
+
const KNOWN_BLOCK_KEYS = [
|
|
15
|
+
"page", "heading1", "heading2", "heading3", "heading4", "heading5", "heading6",
|
|
16
|
+
"heading7", "heading8", "heading9", "text", "bullet", "ordered", "todo", "code",
|
|
17
|
+
"quote", "callout", "equation", "divider", "image", "whiteboard", "file", "sheet",
|
|
18
|
+
"bitable", "table", "table_cell", "diagram", "mindnote", "embed", "iframe", "grid",
|
|
19
|
+
"grid_column", "view"
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export async function fetchFeishuDoc({
|
|
23
|
+
url,
|
|
24
|
+
outputDir,
|
|
25
|
+
fetchImpl = globalThis.fetch?.bind(globalThis),
|
|
26
|
+
appId = process.env.OPTIMUS_FEISHU_AUTH_APP_ID?.trim() || BUILTIN_APP_ID,
|
|
27
|
+
appSecret = process.env.OPTIMUS_FEISHU_AUTH_APP_SECRET?.trim() || BUILTIN_APP_SECRET,
|
|
28
|
+
baseUrl = normalizeBaseUrl(process.env.OPTIMUS_FEISHU_BASE_URL?.trim() || process.env.FEISHU_BASE_URL?.trim() || DEFAULT_FEISHU_BASE_URL)
|
|
29
|
+
}) {
|
|
30
|
+
if (!fetchImpl) {
|
|
31
|
+
throw new Error("Global fetch is unavailable.");
|
|
32
|
+
}
|
|
33
|
+
if (!url?.trim()) {
|
|
34
|
+
throw new Error("Missing --url.");
|
|
35
|
+
}
|
|
36
|
+
if (!outputDir?.trim()) {
|
|
37
|
+
throw new Error("Missing --output-dir.");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const parsed = parseFeishuDocumentUrl(url);
|
|
41
|
+
if (!parsed) {
|
|
42
|
+
throw new Error("Only Feishu/Lark docx and wiki URLs are supported.");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
await mkdir(outputDir, { recursive: true });
|
|
46
|
+
const accessToken = await getTenantAccessToken(fetchImpl, baseUrl, appId, appSecret);
|
|
47
|
+
const client = createClient(fetchImpl, baseUrl, accessToken);
|
|
48
|
+
const target = await resolveTarget(client, parsed);
|
|
49
|
+
const document = await client.get(`/open-apis/docx/v1/documents/${encodeURIComponent(target.documentToken)}`);
|
|
50
|
+
const title = readStringDeep(document, ["data.document.title"]) || target.title;
|
|
51
|
+
const blocks = await readDocumentTree(client, target.documentToken);
|
|
52
|
+
const rendered = renderDocument(blocks, target.documentToken, title);
|
|
53
|
+
const localizedReferences = await downloadReferenceMaterials(client, rendered.referenceMaterials ?? [], join(outputDir, "attachments"));
|
|
54
|
+
const manifest = {
|
|
55
|
+
title: title || null,
|
|
56
|
+
sourceUrl: url,
|
|
57
|
+
sourceType: "feishu_doc",
|
|
58
|
+
sourceDocumentType: target.sourceDocumentType,
|
|
59
|
+
contentPath: join(outputDir, "content.md"),
|
|
60
|
+
referenceMaterials: localizedReferences
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
await writeFile(join(outputDir, "content.md"), `${rendered.content.trim()}\n`, "utf8");
|
|
64
|
+
await writeFile(join(outputDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
65
|
+
return manifest;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function main(argv = process.argv.slice(2)) {
|
|
69
|
+
const args = parseArgs(argv);
|
|
70
|
+
if (args.help || args.h) {
|
|
71
|
+
console.log(usage());
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const manifest = await fetchFeishuDoc({
|
|
75
|
+
url: args.url,
|
|
76
|
+
outputDir: args["output-dir"]
|
|
77
|
+
});
|
|
78
|
+
console.log(JSON.stringify({
|
|
79
|
+
title: manifest.title,
|
|
80
|
+
sourceUrl: manifest.sourceUrl,
|
|
81
|
+
sourceDocumentType: manifest.sourceDocumentType,
|
|
82
|
+
contentPath: manifest.contentPath,
|
|
83
|
+
attachmentCount: manifest.referenceMaterials.filter((entry) => entry.localPath).length
|
|
84
|
+
}, null, 2));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function usage() {
|
|
88
|
+
return [
|
|
89
|
+
"Usage:",
|
|
90
|
+
" fetch-feishu-doc --url <feishu-doc-or-wiki-url> --output-dir <dir>"
|
|
91
|
+
].join("\n");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function parseArgs(argv) {
|
|
95
|
+
const parsed = {};
|
|
96
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
97
|
+
const token = argv[index];
|
|
98
|
+
if (!token?.startsWith("--")) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const key = token.slice(2);
|
|
102
|
+
const next = argv[index + 1];
|
|
103
|
+
if (!next || next.startsWith("--")) {
|
|
104
|
+
parsed[key] = "true";
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
parsed[key] = next;
|
|
108
|
+
index += 1;
|
|
109
|
+
}
|
|
110
|
+
return parsed;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function createClient(fetchImpl, baseUrl, accessToken) {
|
|
114
|
+
return {
|
|
115
|
+
async get(path, query) {
|
|
116
|
+
const url = buildUrl(baseUrl, path, query);
|
|
117
|
+
const response = await fetchImpl(url, {
|
|
118
|
+
method: "GET",
|
|
119
|
+
headers: {
|
|
120
|
+
authorization: `Bearer ${accessToken}`
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
return await readJsonResponse(response);
|
|
124
|
+
},
|
|
125
|
+
async getBinary(path, query) {
|
|
126
|
+
const url = buildUrl(baseUrl, path, query);
|
|
127
|
+
const response = await fetchImpl(url, {
|
|
128
|
+
method: "GET",
|
|
129
|
+
headers: {
|
|
130
|
+
authorization: `Bearer ${accessToken}`
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
throw new Error(`Binary download failed: ${response.status} ${response.statusText}`.trim());
|
|
135
|
+
}
|
|
136
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
137
|
+
return {
|
|
138
|
+
buffer,
|
|
139
|
+
contentType: response.headers.get("content-type")?.trim() || undefined,
|
|
140
|
+
fileName: readFileNameFromHeaders(response.headers)
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function getTenantAccessToken(fetchImpl, baseUrl, appId, appSecret) {
|
|
147
|
+
const response = await fetchImpl(`${baseUrl}/open-apis/auth/v3/tenant_access_token/internal`, {
|
|
148
|
+
method: "POST",
|
|
149
|
+
headers: {
|
|
150
|
+
"content-type": "application/json; charset=utf-8"
|
|
151
|
+
},
|
|
152
|
+
body: JSON.stringify({
|
|
153
|
+
app_id: appId,
|
|
154
|
+
app_secret: appSecret
|
|
155
|
+
})
|
|
156
|
+
});
|
|
157
|
+
const payload = await readJsonResponse(response);
|
|
158
|
+
const token = readStringDeep(payload, ["tenant_access_token"]);
|
|
159
|
+
if (!response.ok || !token) {
|
|
160
|
+
throw new Error(`Failed to acquire Feishu tenant token: ${JSON.stringify(payload)}`);
|
|
161
|
+
}
|
|
162
|
+
return token;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function buildUrl(baseUrl, path, query) {
|
|
166
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
167
|
+
const url = new URL(`${baseUrl}${normalizedPath}`);
|
|
168
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
169
|
+
if (typeof value === "string" && value.trim()) {
|
|
170
|
+
url.searchParams.set(key, value.trim());
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return url.toString();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function readJsonResponse(response) {
|
|
177
|
+
const text = await response.text();
|
|
178
|
+
if (!text.trim()) {
|
|
179
|
+
return {};
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
return JSON.parse(text);
|
|
183
|
+
} catch {
|
|
184
|
+
return { raw: text };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function parseFeishuDocumentUrl(input) {
|
|
189
|
+
const trimmed = input?.trim();
|
|
190
|
+
if (!trimmed) {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
let url;
|
|
194
|
+
try {
|
|
195
|
+
url = new URL(trimmed);
|
|
196
|
+
} catch {
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
if (!FEISHU_DOCUMENT_HOST_PATTERN.test(url.hostname)) {
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
const docxMatch = url.pathname.match(DOCX_PATH_PATTERN);
|
|
203
|
+
if (docxMatch?.[1]) {
|
|
204
|
+
return { kind: "docx", token: docxMatch[1], url: trimmed };
|
|
205
|
+
}
|
|
206
|
+
const wikiMatch = url.pathname.match(WIKI_PATH_PATTERN);
|
|
207
|
+
if (wikiMatch?.[1]) {
|
|
208
|
+
return { kind: "wiki", token: wikiMatch[1], url: trimmed };
|
|
209
|
+
}
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function resolveTarget(client, parsed) {
|
|
214
|
+
if (parsed.kind === "docx") {
|
|
215
|
+
return {
|
|
216
|
+
sourceUrl: parsed.url,
|
|
217
|
+
sourceDocumentType: "docx",
|
|
218
|
+
documentToken: parsed.token
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
const payload = await client.get("/open-apis/wiki/v2/spaces/get_node", { token: parsed.token });
|
|
222
|
+
const target = extractWikiDocTarget(payload);
|
|
223
|
+
if (!target?.documentToken || target.documentType !== "docx") {
|
|
224
|
+
throw new Error("Failed to resolve wiki docx target.");
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
sourceUrl: parsed.url,
|
|
228
|
+
sourceDocumentType: target.documentType,
|
|
229
|
+
documentToken: target.documentToken,
|
|
230
|
+
title: target.title
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function extractWikiDocTarget(payload) {
|
|
235
|
+
if (!payload || typeof payload !== "object") {
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
if (Array.isArray(payload)) {
|
|
239
|
+
for (const entry of payload) {
|
|
240
|
+
const target = extractWikiDocTarget(entry);
|
|
241
|
+
if (target?.documentToken || target?.documentType) {
|
|
242
|
+
return target;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
const documentType = readStringDeep(payload, ["obj_type", "objType", "object_type", "objectType"]);
|
|
248
|
+
const documentToken = readStringDeep(payload, ["obj_token", "objToken", "document_id", "documentId", "doc_token", "docToken"]);
|
|
249
|
+
const title = readStringDeep(payload, ["title", "name"]);
|
|
250
|
+
if (documentType || documentToken || title) {
|
|
251
|
+
return { documentType, documentToken, title };
|
|
252
|
+
}
|
|
253
|
+
for (const value of Object.values(payload)) {
|
|
254
|
+
const nested = extractWikiDocTarget(value);
|
|
255
|
+
if (nested?.documentToken || nested?.documentType) {
|
|
256
|
+
return nested;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function readDocumentTree(client, documentToken) {
|
|
263
|
+
const blocks = new Map();
|
|
264
|
+
blocks.set(documentToken, {
|
|
265
|
+
blockId: documentToken,
|
|
266
|
+
children: [],
|
|
267
|
+
hasChild: true,
|
|
268
|
+
raw: {}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const queue = [documentToken];
|
|
272
|
+
while (queue.length > 0) {
|
|
273
|
+
const parentId = queue.shift();
|
|
274
|
+
const childBlocks = await fetchChildren(client, documentToken, parentId);
|
|
275
|
+
const orderedIds = [];
|
|
276
|
+
for (const block of childBlocks) {
|
|
277
|
+
orderedIds.push(block.blockId);
|
|
278
|
+
blocks.set(block.blockId, block);
|
|
279
|
+
if (block.hasChild) {
|
|
280
|
+
queue.push(block.blockId);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const parent = blocks.get(parentId);
|
|
284
|
+
if (parent) {
|
|
285
|
+
parent.children = orderedIds;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return blocks;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function fetchChildren(client, documentToken, parentId) {
|
|
292
|
+
const blocks = [];
|
|
293
|
+
let pageToken;
|
|
294
|
+
while (true) {
|
|
295
|
+
const payload = await client.get(
|
|
296
|
+
`/open-apis/docx/v1/documents/${encodeURIComponent(documentToken)}/blocks/${encodeURIComponent(parentId)}/children`,
|
|
297
|
+
{
|
|
298
|
+
page_size: CHILDREN_PAGE_SIZE,
|
|
299
|
+
...(pageToken ? { page_token: pageToken } : {})
|
|
300
|
+
}
|
|
301
|
+
);
|
|
302
|
+
const items = Array.isArray(payload?.data?.items) ? payload.data.items : [];
|
|
303
|
+
for (const item of items) {
|
|
304
|
+
const block = normalizeBlockRecord(item);
|
|
305
|
+
if (block) {
|
|
306
|
+
blocks.push(block);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (!payload?.data?.has_more || !payload?.data?.page_token?.trim()) {
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
pageToken = payload.data.page_token.trim();
|
|
313
|
+
}
|
|
314
|
+
return blocks;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function normalizeBlockRecord(value) {
|
|
318
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
const blockId = typeof value.block_id === "string" ? value.block_id.trim() : "";
|
|
322
|
+
if (!blockId) {
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
const parentId = typeof value.parent_id === "string" && value.parent_id.trim() ? value.parent_id.trim() : undefined;
|
|
326
|
+
const children = Array.isArray(value.children)
|
|
327
|
+
? value.children.filter((entry) => typeof entry === "string" && entry.trim().length > 0).map((entry) => entry.trim())
|
|
328
|
+
: [];
|
|
329
|
+
return {
|
|
330
|
+
blockId,
|
|
331
|
+
parentId,
|
|
332
|
+
children,
|
|
333
|
+
hasChild: value.has_child === true || children.length > 0,
|
|
334
|
+
raw: value
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function renderDocument(blocks, rootId, title) {
|
|
339
|
+
const state = {
|
|
340
|
+
noteReferences: [],
|
|
341
|
+
seenReferenceKeys: new Set(),
|
|
342
|
+
containerCounters: {}
|
|
343
|
+
};
|
|
344
|
+
const lines = [];
|
|
345
|
+
if (title) {
|
|
346
|
+
lines.push(`# ${title}`, "");
|
|
347
|
+
}
|
|
348
|
+
lines.push(...renderChildren(rootId, blocks, state, 0));
|
|
349
|
+
return {
|
|
350
|
+
content: lines.join("\n").replace(/\n{3,}/gu, "\n\n").trim(),
|
|
351
|
+
referenceMaterials: state.noteReferences
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function renderChildren(parentId, blocks, state, depth) {
|
|
356
|
+
const parent = blocks.get(parentId);
|
|
357
|
+
if (!parent) {
|
|
358
|
+
return [];
|
|
359
|
+
}
|
|
360
|
+
const lines = [];
|
|
361
|
+
parent.children.forEach((childId, index) => {
|
|
362
|
+
const child = blocks.get(childId);
|
|
363
|
+
if (!child) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const rendered = renderBlock(child, blocks, state, depth, index);
|
|
367
|
+
if (!rendered.length) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (lines.length > 0 && rendered[0] !== "" && lines.at(-1) !== "") {
|
|
371
|
+
lines.push("");
|
|
372
|
+
}
|
|
373
|
+
lines.push(...rendered);
|
|
374
|
+
});
|
|
375
|
+
return lines;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function renderBlock(block, blocks, state, depth, index) {
|
|
379
|
+
const kind = detectBlockKind(block.raw);
|
|
380
|
+
const childLines = block.children.length > 0 ? renderChildren(block.blockId, blocks, state, depth + 1) : [];
|
|
381
|
+
switch (kind) {
|
|
382
|
+
case "page":
|
|
383
|
+
case "view":
|
|
384
|
+
case "grid":
|
|
385
|
+
case "grid_column":
|
|
386
|
+
return childLines;
|
|
387
|
+
case "heading1":
|
|
388
|
+
case "heading2":
|
|
389
|
+
case "heading3":
|
|
390
|
+
case "heading4":
|
|
391
|
+
case "heading5":
|
|
392
|
+
case "heading6":
|
|
393
|
+
case "heading7":
|
|
394
|
+
case "heading8":
|
|
395
|
+
case "heading9": {
|
|
396
|
+
const level = Number(kind.replace("heading", "")) || 1;
|
|
397
|
+
return [`${"#".repeat(Math.min(6, level))} ${renderRichBlockText(block.raw[kind])}`];
|
|
398
|
+
}
|
|
399
|
+
case "text": {
|
|
400
|
+
const text = renderRichBlockText(block.raw.text);
|
|
401
|
+
return text ? [text] : childLines;
|
|
402
|
+
}
|
|
403
|
+
case "bullet":
|
|
404
|
+
return renderListItem("-", renderRichBlockText(block.raw.bullet), childLines);
|
|
405
|
+
case "ordered":
|
|
406
|
+
return renderListItem(`${index + 1}.`, renderRichBlockText(block.raw.ordered), childLines);
|
|
407
|
+
case "todo": {
|
|
408
|
+
const done = readBooleanDeep(block.raw.todo, ["checked", "is_checked", "done"]);
|
|
409
|
+
return renderListItem(done ? "- [x]" : "- [ ]", renderRichBlockText(block.raw.todo), childLines);
|
|
410
|
+
}
|
|
411
|
+
case "quote":
|
|
412
|
+
return prefixLines(`> ${renderRichBlockText(block.raw.quote)}`.trimEnd(), childLines, "> ");
|
|
413
|
+
case "callout":
|
|
414
|
+
return prefixLines(`> [!NOTE] ${renderRichBlockText(block.raw.callout)}`.trimEnd(), childLines, "> ");
|
|
415
|
+
case "code": {
|
|
416
|
+
const language = readStringDeep(block.raw.code, ["language", "lang"]) || "";
|
|
417
|
+
const content = renderRichBlockText(block.raw.code, { preserveNewlines: true });
|
|
418
|
+
return [`\`\`\`${language}`, content || "", "```", ...indentLines(childLines, " ")];
|
|
419
|
+
}
|
|
420
|
+
case "equation":
|
|
421
|
+
return [`$$${renderRichBlockText(block.raw.equation, { preserveNewlines: true })}$$`];
|
|
422
|
+
case "divider":
|
|
423
|
+
return ["---"];
|
|
424
|
+
case "image":
|
|
425
|
+
return [renderMediaTag("img", block.raw.image, state, { titlePrefix: "Embedded image", defaultType: "image" })];
|
|
426
|
+
case "whiteboard":
|
|
427
|
+
return [renderMediaTag("whiteboard", block.raw.whiteboard, state, { titlePrefix: "Embedded whiteboard", defaultType: "whiteboard" })];
|
|
428
|
+
case "file":
|
|
429
|
+
return [renderMediaTag("file", block.raw.file, state, { titlePrefix: "Embedded file", defaultType: "file" })];
|
|
430
|
+
case "sheet":
|
|
431
|
+
return [renderMediaTag("sheet", block.raw.sheet, state, { titlePrefix: "Embedded sheet", defaultType: "sheet" })];
|
|
432
|
+
case "bitable":
|
|
433
|
+
return [renderMediaTag("bitable", block.raw.bitable, state, { titlePrefix: "Embedded bitable", defaultType: "bitable" })];
|
|
434
|
+
default:
|
|
435
|
+
return childLines;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function renderMediaTag(tagName, payload, state, options) {
|
|
440
|
+
const title = readStringDeep(payload, ["name", "title"]) || nextContainerTitle(state, tagName, options.titlePrefix);
|
|
441
|
+
const token = readStringDeep(payload, ["token", "file_token", "image_token", "src"]);
|
|
442
|
+
const url = readStringDeep(payload, ["url", "link"]);
|
|
443
|
+
const mimeType = readStringDeep(payload, ["mime_type", "mime", "type"]);
|
|
444
|
+
if (payload && typeof payload === "object") {
|
|
445
|
+
pushNoteReference(state, {
|
|
446
|
+
type: options.defaultType,
|
|
447
|
+
title,
|
|
448
|
+
...(token ? { token } : {}),
|
|
449
|
+
...(url ? { url } : {}),
|
|
450
|
+
...(mimeType ? { mimeType } : {}),
|
|
451
|
+
sourceType: "feishu_block"
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
const attributes = [
|
|
455
|
+
`name="${escapeHtmlAttribute(title)}"`,
|
|
456
|
+
...(token ? [tagName === "img" ? `src="${escapeHtmlAttribute(token)}"` : `token="${escapeHtmlAttribute(token)}"`] : []),
|
|
457
|
+
...(url ? [`url="${escapeHtmlAttribute(url)}"`] : []),
|
|
458
|
+
...(mimeType ? [`mime="${escapeHtmlAttribute(mimeType)}"`] : [])
|
|
459
|
+
];
|
|
460
|
+
return `<${tagName} ${attributes.join(" ")}/>`;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function renderRichBlockText(payload, options = {}) {
|
|
464
|
+
const elements = findElementsArray(payload);
|
|
465
|
+
if (elements.length > 0) {
|
|
466
|
+
const rendered = elements.map((element) => renderTextElement(element)).join("");
|
|
467
|
+
return options.preserveNewlines ? rendered : rendered.replace(/\s+\n/gu, "\n").trim();
|
|
468
|
+
}
|
|
469
|
+
return readStringDeep(payload, ["content", "text", "title", "caption"]) || "";
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function renderTextElement(element) {
|
|
473
|
+
if (!element || typeof element !== "object" || Array.isArray(element)) {
|
|
474
|
+
return "";
|
|
475
|
+
}
|
|
476
|
+
if (element.text_run && typeof element.text_run === "object") {
|
|
477
|
+
const content = typeof element.text_run.content === "string" ? element.text_run.content : "";
|
|
478
|
+
const style = element.text_run.text_element_style;
|
|
479
|
+
const link = readStringDeep(element.text_run, ["link.url", "href", "url"]) || readStringDeep(style, ["link.url", "href", "url"]);
|
|
480
|
+
return applyInlineStyle(link ? `[${content}](${link})` : content, style);
|
|
481
|
+
}
|
|
482
|
+
if (element.mention_doc && typeof element.mention_doc === "object") {
|
|
483
|
+
const title = readStringDeep(element.mention_doc, ["title", "name"]) || "document";
|
|
484
|
+
const url = readStringDeep(element.mention_doc, ["url", "link"]);
|
|
485
|
+
return url ? `[${title}](${url})` : title;
|
|
486
|
+
}
|
|
487
|
+
if (element.mention_user && typeof element.mention_user === "object") {
|
|
488
|
+
return `@${readStringDeep(element.mention_user, ["name", "title", "display_name"]) || "user"}`;
|
|
489
|
+
}
|
|
490
|
+
return readStringDeep(element, ["text", "content", "title"]) || "";
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function pushNoteReference(state, reference) {
|
|
494
|
+
const key = [reference.type, reference.title, reference.token || "", reference.url || "", reference.content || ""].join("|");
|
|
495
|
+
if (state.seenReferenceKeys.has(key)) {
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
state.seenReferenceKeys.add(key);
|
|
499
|
+
state.noteReferences.push(reference);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function nextContainerTitle(state, counterKey, prefix) {
|
|
503
|
+
const next = (state.containerCounters[counterKey] || 0) + 1;
|
|
504
|
+
state.containerCounters[counterKey] = next;
|
|
505
|
+
return `${prefix} ${next}`;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async function downloadReferenceMaterials(client, materials, attachmentsDir) {
|
|
509
|
+
await mkdir(attachmentsDir, { recursive: true });
|
|
510
|
+
const output = [];
|
|
511
|
+
for (const material of materials) {
|
|
512
|
+
if (material.type !== "file" && material.type !== "image") {
|
|
513
|
+
output.push(material);
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
const token = material.token?.trim();
|
|
517
|
+
if (!token) {
|
|
518
|
+
output.push({ ...material, downloadStatus: "skipped", downloadError: "download_token_missing" });
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
const candidatePaths = material.type === "image"
|
|
522
|
+
? [`/open-apis/drive/v1/medias/${encodeURIComponent(token)}/download`, `/open-apis/drive/v1/files/${encodeURIComponent(token)}/download`]
|
|
523
|
+
: [`/open-apis/drive/v1/files/${encodeURIComponent(token)}/download`, `/open-apis/drive/v1/medias/${encodeURIComponent(token)}/download`];
|
|
524
|
+
let lastError = "download_failed";
|
|
525
|
+
let downloaded = false;
|
|
526
|
+
for (const path of candidatePaths) {
|
|
527
|
+
try {
|
|
528
|
+
const binary = await client.getBinary(path);
|
|
529
|
+
const fileName = ensureExtension(resolvePreferredFileName(material, binary.fileName, binary.contentType), material, binary.contentType);
|
|
530
|
+
const outputPath = join(attachmentsDir, fileName);
|
|
531
|
+
await writeFile(outputPath, binary.buffer);
|
|
532
|
+
output.push({
|
|
533
|
+
...material,
|
|
534
|
+
localPath: outputPath,
|
|
535
|
+
downloadStatus: "downloaded",
|
|
536
|
+
...(binary.contentType ? { mimeType: material.mimeType || binary.contentType } : {})
|
|
537
|
+
});
|
|
538
|
+
downloaded = true;
|
|
539
|
+
break;
|
|
540
|
+
} catch (error) {
|
|
541
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (!downloaded) {
|
|
545
|
+
output.push({ ...material, downloadStatus: "failed", downloadError: lastError });
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return output;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function detectBlockKind(raw) {
|
|
552
|
+
return KNOWN_BLOCK_KEYS.find((key) => key in raw);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function findElementsArray(value) {
|
|
556
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
557
|
+
return [];
|
|
558
|
+
}
|
|
559
|
+
if (Array.isArray(value.elements)) {
|
|
560
|
+
return value.elements;
|
|
561
|
+
}
|
|
562
|
+
for (const nested of Object.values(value)) {
|
|
563
|
+
const elements = findElementsArray(nested);
|
|
564
|
+
if (elements.length > 0) {
|
|
565
|
+
return elements;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return [];
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function readStringDeep(value, keys) {
|
|
572
|
+
for (const key of keys) {
|
|
573
|
+
const direct = readDottedString(value, key);
|
|
574
|
+
if (direct) {
|
|
575
|
+
return direct;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return undefined;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function readDottedString(value, dottedKey) {
|
|
582
|
+
const parts = dottedKey.split(".");
|
|
583
|
+
let current = value;
|
|
584
|
+
for (const part of parts) {
|
|
585
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) {
|
|
586
|
+
return undefined;
|
|
587
|
+
}
|
|
588
|
+
current = current[part];
|
|
589
|
+
}
|
|
590
|
+
return typeof current === "string" && current.trim() ? current.trim() : undefined;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function readBooleanDeep(value, keys) {
|
|
594
|
+
for (const key of keys) {
|
|
595
|
+
const parts = key.split(".");
|
|
596
|
+
let current = value;
|
|
597
|
+
for (const part of parts) {
|
|
598
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) {
|
|
599
|
+
current = undefined;
|
|
600
|
+
break;
|
|
601
|
+
}
|
|
602
|
+
current = current[part];
|
|
603
|
+
}
|
|
604
|
+
if (typeof current === "boolean") {
|
|
605
|
+
return current;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function applyInlineStyle(text, style) {
|
|
612
|
+
if (!text) {
|
|
613
|
+
return "";
|
|
614
|
+
}
|
|
615
|
+
const bold = readBooleanDeep(style, ["bold"]);
|
|
616
|
+
const italic = readBooleanDeep(style, ["italic"]);
|
|
617
|
+
const inlineCode = readBooleanDeep(style, ["inline_code"]);
|
|
618
|
+
const strikethrough = readBooleanDeep(style, ["strikethrough"]);
|
|
619
|
+
const underline = readBooleanDeep(style, ["underline"]);
|
|
620
|
+
let output = text;
|
|
621
|
+
if (inlineCode) {
|
|
622
|
+
output = `\`${output.replace(/`/gu, "\\`")}\``;
|
|
623
|
+
}
|
|
624
|
+
if (bold) {
|
|
625
|
+
output = `**${output}**`;
|
|
626
|
+
}
|
|
627
|
+
if (italic) {
|
|
628
|
+
output = `*${output}*`;
|
|
629
|
+
}
|
|
630
|
+
if (strikethrough) {
|
|
631
|
+
output = `~~${output}~~`;
|
|
632
|
+
}
|
|
633
|
+
if (underline) {
|
|
634
|
+
output = `<u>${output}</u>`;
|
|
635
|
+
}
|
|
636
|
+
return output;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function renderListItem(prefix, text, childLines) {
|
|
640
|
+
const line = text ? `${prefix} ${text}` : prefix;
|
|
641
|
+
return childLines.length === 0 ? [line] : [line, ...indentLines(childLines, " ")];
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function indentLines(lines, indent) {
|
|
645
|
+
return lines.map((line) => line.length > 0 ? `${indent}${line}` : line);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function prefixLines(firstLine, childLines, prefix) {
|
|
649
|
+
return childLines.length === 0
|
|
650
|
+
? [firstLine]
|
|
651
|
+
: [firstLine, ...childLines.map((line) => line.length > 0 ? `${prefix}${line}` : prefix.trimEnd())];
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function escapeHtmlAttribute(value) {
|
|
655
|
+
return value.replace(/&/gu, "&").replace(/"/gu, """).replace(/</gu, "<").replace(/>/gu, ">");
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function resolvePreferredFileName(material, responseFileName, contentType) {
|
|
659
|
+
const stableName = buildStableReferenceMaterialName(material, responseFileName, contentType);
|
|
660
|
+
if (stableName) {
|
|
661
|
+
return stableName;
|
|
662
|
+
}
|
|
663
|
+
const candidates = [responseFileName, material.title, material.token ? `${material.type}-${material.token}` : undefined];
|
|
664
|
+
for (const candidate of candidates) {
|
|
665
|
+
const normalized = sanitizeFileName(candidate);
|
|
666
|
+
if (normalized) {
|
|
667
|
+
return normalized;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return `${material.type}${extensionFromMimeType(contentType) || extensionFromMimeType(material.mimeType) || ""}`;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function buildStableReferenceMaterialName(material, sourceFileName, contentType) {
|
|
674
|
+
if (material.type !== "image") {
|
|
675
|
+
return undefined;
|
|
676
|
+
}
|
|
677
|
+
const extension = extensionFromFileName(sourceFileName) || extensionFromMimeType(contentType) || extensionFromMimeType(material.mimeType) || ".png";
|
|
678
|
+
const identity = sanitizeFileName(material.assetId) || (Number.isFinite(material.order) ? String(material.order).padStart(3, "0") : undefined) || sanitizeFileName(material.token);
|
|
679
|
+
return identity ? `image-${identity}${extension}` : `image${extension}`;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function extensionFromFileName(fileName) {
|
|
683
|
+
const extension = extname(fileName?.trim() || "").toLowerCase();
|
|
684
|
+
return [".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg", ".html", ".md", ".json", ".txt"].includes(extension) ? extension : undefined;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function ensureExtension(fileName, material, contentType) {
|
|
688
|
+
if (extname(fileName)) {
|
|
689
|
+
return fileName;
|
|
690
|
+
}
|
|
691
|
+
const extension = extensionFromMimeType(contentType) || extensionFromMimeType(material.mimeType) || (material.type === "image" ? ".png" : undefined);
|
|
692
|
+
return extension ? `${fileName}${extension}` : fileName;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function extensionFromMimeType(mimeType) {
|
|
696
|
+
const normalized = mimeType?.trim().toLowerCase();
|
|
697
|
+
switch (normalized) {
|
|
698
|
+
case "image/png": return ".png";
|
|
699
|
+
case "image/jpeg": return ".jpeg";
|
|
700
|
+
case "image/webp": return ".webp";
|
|
701
|
+
case "image/gif": return ".gif";
|
|
702
|
+
case "image/svg+xml": return ".svg";
|
|
703
|
+
case "text/html": return ".html";
|
|
704
|
+
case "text/markdown": return ".md";
|
|
705
|
+
case "application/json": return ".json";
|
|
706
|
+
case "text/plain": return ".txt";
|
|
707
|
+
default: return undefined;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function sanitizeFileName(value) {
|
|
712
|
+
const normalized = value?.trim().replace(/[\\/:*?"<>|]+/gu, "-").replace(/\s+/gu, " ");
|
|
713
|
+
return normalized ? basename(normalized) : undefined;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function readFileNameFromHeaders(headers) {
|
|
717
|
+
const contentDisposition = headers.get("content-disposition")?.trim();
|
|
718
|
+
if (!contentDisposition) {
|
|
719
|
+
return undefined;
|
|
720
|
+
}
|
|
721
|
+
const utf8Match = contentDisposition.match(/filename\*\s*=\s*UTF-8''([^;]+)/iu);
|
|
722
|
+
if (utf8Match?.[1]) {
|
|
723
|
+
try {
|
|
724
|
+
return decodeURIComponent(utf8Match[1]).trim();
|
|
725
|
+
} catch {
|
|
726
|
+
return utf8Match[1].trim();
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
const quotedMatch = contentDisposition.match(/filename\s*=\s*"([^"]+)"/iu);
|
|
730
|
+
if (quotedMatch?.[1]) {
|
|
731
|
+
return quotedMatch[1].trim();
|
|
732
|
+
}
|
|
733
|
+
const plainMatch = contentDisposition.match(/filename\s*=\s*([^;]+)/iu);
|
|
734
|
+
return plainMatch?.[1]?.trim();
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function normalizeBaseUrl(value) {
|
|
738
|
+
return value.replace(/\/+$/u, "");
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function decodeCredential(encoded) {
|
|
742
|
+
const source = Buffer.from(encoded, "hex");
|
|
743
|
+
const key = Buffer.from(CREDENTIAL_XOR_KEY, "utf8");
|
|
744
|
+
const output = Buffer.alloc(source.length);
|
|
745
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
746
|
+
output[index] = source[index] ^ key[index % key.length];
|
|
747
|
+
}
|
|
748
|
+
return output.toString("utf8");
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (import.meta.url === new URL(`file://${process.argv[1]}`).toString()) {
|
|
752
|
+
main().catch((error) => {
|
|
753
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
754
|
+
process.exitCode = 1;
|
|
755
|
+
});
|
|
756
|
+
}
|