@seqyuan/annodex 0.1.72 → 0.1.74
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/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +8 -8
- package/.next/build-manifest.json +2 -2
- package/.next/prerender-manifest.json +3 -3
- package/.next/required-server-files.js +1 -1
- package/.next/required-server-files.json +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +1 -1
- package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/api/im/turn/route.js +1 -1
- package/.next/server/app/api/internal/runtime/route.js +1 -1
- package/.next/server/app/api/version/route.js +1 -1
- package/.next/server/app/docs/changelog.html +2 -2
- package/.next/server/app/docs/changelog.rsc +1 -1
- package/.next/server/app/docs/changelog.segments/_full.segment.rsc +1 -1
- package/.next/server/app/docs/changelog.segments/_head.segment.rsc +1 -1
- package/.next/server/app/docs/changelog.segments/_index.segment.rsc +1 -1
- package/.next/server/app/docs/changelog.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/docs/changelog.segments/docs/changelog/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/docs/changelog.segments/docs/changelog.segment.rsc +1 -1
- package/.next/server/app/docs/changelog.segments/docs.segment.rsc +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +1 -1
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/login.html +1 -1
- package/.next/server/app/login.rsc +1 -1
- package/.next/server/app/login.segments/_full.segment.rsc +1 -1
- package/.next/server/app/login.segments/_head.segment.rsc +1 -1
- package/.next/server/app/login.segments/_index.segment.rsc +1 -1
- package/.next/server/app/login.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/login.segments/login.segment.rsc +1 -1
- package/.next/server/app/workspace/page.js +2 -2
- package/.next/server/app/workspace/page_client-reference-manifest.js +1 -1
- package/.next/server/app/workspace.html +1 -1
- package/.next/server/app/workspace.rsc +2 -2
- package/.next/server/app/workspace.segments/_full.segment.rsc +2 -2
- package/.next/server/app/workspace.segments/_head.segment.rsc +1 -1
- package/.next/server/app/workspace.segments/_index.segment.rsc +1 -1
- package/.next/server/app/workspace.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/workspace.segments/workspace/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/workspace.segments/workspace.segment.rsc +1 -1
- package/.next/server/app-paths-manifest.json +8 -8
- package/.next/server/chunks/6983.js +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/next-font-manifest.js +1 -1
- package/.next/server/next-font-manifest.json +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/app/workspace/{page-9879349ec16c1136.js → page-d5360de46c9f4386.js} +2 -2
- package/bin/annodex-im-gateway.js +173 -3
- package/lib/im-media.js +171 -0
- package/package.json +2 -1
- /package/.next/static/{9ja2nMxYXszS_1ydk3Gb9 → rKBVKaVLsaySHUHU-dLb7}/_buildManifest.js +0 -0
- /package/.next/static/{9ja2nMxYXszS_1ydk3Gb9 → rKBVKaVLsaySHUHU-dLb7}/_ssgManifest.js +0 -0
|
@@ -6,7 +6,12 @@ const http = require("http");
|
|
|
6
6
|
const https = require("https");
|
|
7
7
|
const os = require("os");
|
|
8
8
|
const path = require("path");
|
|
9
|
+
const crypto = require("crypto");
|
|
9
10
|
const WebSocket = require("ws");
|
|
11
|
+
const imMedia = require("../lib/im-media.js");
|
|
12
|
+
|
|
13
|
+
const UPLOAD_CHUNK_SIZE = 512 * 1024;
|
|
14
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
10
15
|
|
|
11
16
|
const REVISION_DIGITS = ["\u200b", "\u200c", "\u200d", "\u2060", "\ufeff"];
|
|
12
17
|
const REVISION_SUFFIX_PATTERN = /[\u200b\u200c\u200d\u2060\ufeff]+$/u;
|
|
@@ -340,6 +345,7 @@ class WeComWsClient {
|
|
|
340
345
|
this.ws = null;
|
|
341
346
|
this.heartbeatTimer = null;
|
|
342
347
|
this.pendingAcks = new Map();
|
|
348
|
+
this.pendingResponses = new Map();
|
|
343
349
|
this.authenticated = createDeferred();
|
|
344
350
|
}
|
|
345
351
|
|
|
@@ -408,6 +414,90 @@ class WeComWsClient {
|
|
|
408
414
|
}).catch(() => undefined);
|
|
409
415
|
}
|
|
410
416
|
|
|
417
|
+
async sendRequest(cmd, body, timeoutMs = REQUEST_TIMEOUT_MS, reqId = `${cmd}_${crypto.randomUUID()}`) {
|
|
418
|
+
return new Promise((resolve, reject) => {
|
|
419
|
+
const timeout = setTimeout(() => {
|
|
420
|
+
this.pendingResponses.delete(reqId);
|
|
421
|
+
reject(new Error(`wecom request timeout for ${cmd}`));
|
|
422
|
+
}, timeoutMs);
|
|
423
|
+
this.pendingResponses.set(reqId, { resolve, reject, timeout });
|
|
424
|
+
void this.sendFrame({ cmd, headers: { req_id: reqId }, body }).catch((error) => {
|
|
425
|
+
clearTimeout(timeout);
|
|
426
|
+
this.pendingResponses.delete(reqId);
|
|
427
|
+
reject(error);
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
raiseForWeComError(frame, context) {
|
|
433
|
+
if ((frame.errcode ?? 0) === 0) return;
|
|
434
|
+
const error = new Error(frame.errmsg || `wecom ${context} failed (${frame.errcode})`);
|
|
435
|
+
error.errcode = frame.errcode;
|
|
436
|
+
error.errmsg = frame.errmsg;
|
|
437
|
+
throw error;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async uploadMediaFile(filePath, options = {}) {
|
|
441
|
+
const data = fs.readFileSync(filePath);
|
|
442
|
+
const fileName = (options.fileName || path.basename(filePath) || "file").trim();
|
|
443
|
+
const mediaType = (options.mediaType || "file").trim();
|
|
444
|
+
const totalSize = data.length;
|
|
445
|
+
const totalChunks = Math.ceil(totalSize / UPLOAD_CHUNK_SIZE);
|
|
446
|
+
if (totalChunks > 100) {
|
|
447
|
+
throw new Error(`File too large for WeCom upload (${totalChunks} chunks)`);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const initResponse = await this.sendRequest("aibot_upload_media_init", {
|
|
451
|
+
type: mediaType,
|
|
452
|
+
filename: fileName,
|
|
453
|
+
total_size: totalSize,
|
|
454
|
+
total_chunks: totalChunks,
|
|
455
|
+
md5: crypto.createHash("md5").update(data).digest("hex"),
|
|
456
|
+
});
|
|
457
|
+
this.raiseForWeComError(initResponse, "media upload init");
|
|
458
|
+
|
|
459
|
+
const initBody = asRecord(initResponse.body);
|
|
460
|
+
const uploadId = resolveOptionalString(initBody?.upload_id);
|
|
461
|
+
if (!uploadId) throw new Error("media upload init failed: missing upload_id");
|
|
462
|
+
|
|
463
|
+
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex += 1) {
|
|
464
|
+
const start = chunkIndex * UPLOAD_CHUNK_SIZE;
|
|
465
|
+
const chunk = data.subarray(start, start + UPLOAD_CHUNK_SIZE);
|
|
466
|
+
const chunkResponse = await this.sendRequest("aibot_upload_media_chunk", {
|
|
467
|
+
upload_id: uploadId,
|
|
468
|
+
chunk_index: chunkIndex,
|
|
469
|
+
base64_data: chunk.toString("base64"),
|
|
470
|
+
});
|
|
471
|
+
this.raiseForWeComError(chunkResponse, `media upload chunk ${chunkIndex}`);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const finishResponse = await this.sendRequest("aibot_upload_media_finish", { upload_id: uploadId });
|
|
475
|
+
this.raiseForWeComError(finishResponse, "media upload finish");
|
|
476
|
+
|
|
477
|
+
const finishBody = asRecord(finishResponse.body);
|
|
478
|
+
const mediaId = resolveOptionalString(finishBody?.media_id);
|
|
479
|
+
if (!mediaId) throw new Error("media upload finish failed: missing media_id");
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
mediaId,
|
|
483
|
+
mediaType: resolveOptionalString(finishBody?.type) || mediaType,
|
|
484
|
+
fileName,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async replyMediaMessage(reqId, mediaType, mediaId) {
|
|
489
|
+
const response = await this.sendRequest(
|
|
490
|
+
"aibot_respond_msg",
|
|
491
|
+
{
|
|
492
|
+
msgtype: mediaType,
|
|
493
|
+
[mediaType]: { media_id: mediaId },
|
|
494
|
+
},
|
|
495
|
+
REQUEST_TIMEOUT_MS,
|
|
496
|
+
reqId,
|
|
497
|
+
);
|
|
498
|
+
this.raiseForWeComError(response, "send reply media");
|
|
499
|
+
}
|
|
500
|
+
|
|
411
501
|
async disconnect() {
|
|
412
502
|
this.stopHeartbeat();
|
|
413
503
|
this.rejectPendingAcks(new Error("wecom websocket disconnected"));
|
|
@@ -424,6 +514,23 @@ class WeComWsClient {
|
|
|
424
514
|
const frame = JSON.parse(frameText);
|
|
425
515
|
const reqId = frame.headers?.req_id?.trim();
|
|
426
516
|
|
|
517
|
+
if (reqId && this.pendingResponses.has(reqId)) {
|
|
518
|
+
const pending = this.pendingResponses.get(reqId);
|
|
519
|
+
if (pending) {
|
|
520
|
+
this.pendingResponses.delete(reqId);
|
|
521
|
+
clearTimeout(pending.timeout);
|
|
522
|
+
if ((frame.errcode ?? 0) !== 0) {
|
|
523
|
+
pending.reject(Object.assign(new Error(frame.errmsg || "wecom request failed"), {
|
|
524
|
+
errcode: frame.errcode,
|
|
525
|
+
errmsg: frame.errmsg,
|
|
526
|
+
}));
|
|
527
|
+
} else {
|
|
528
|
+
pending.resolve(frame);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
427
534
|
if (reqId && this.pendingAcks.has(reqId)) {
|
|
428
535
|
const pending = this.pendingAcks.get(reqId);
|
|
429
536
|
if (pending) {
|
|
@@ -485,6 +592,11 @@ class WeComWsClient {
|
|
|
485
592
|
clearTimeout(pending.timeout);
|
|
486
593
|
pending.reject(error);
|
|
487
594
|
}
|
|
595
|
+
for (const [reqId, pending] of this.pendingResponses) {
|
|
596
|
+
this.pendingResponses.delete(reqId);
|
|
597
|
+
clearTimeout(pending.timeout);
|
|
598
|
+
pending.reject(error);
|
|
599
|
+
}
|
|
488
600
|
}
|
|
489
601
|
}
|
|
490
602
|
|
|
@@ -575,6 +687,7 @@ class ProjectWeComBridge {
|
|
|
575
687
|
this.client = null;
|
|
576
688
|
this.stopped = false;
|
|
577
689
|
this.reconnectTimer = null;
|
|
690
|
+
this.mediaUploadDisabled = false;
|
|
578
691
|
}
|
|
579
692
|
|
|
580
693
|
async start() {
|
|
@@ -620,6 +733,53 @@ class ProjectWeComBridge {
|
|
|
620
733
|
this.client = null;
|
|
621
734
|
}
|
|
622
735
|
|
|
736
|
+
async deliverMedia(reqId, mediaPaths) {
|
|
737
|
+
const fallbacks = [];
|
|
738
|
+
if (!mediaPaths.length || !this.client) return fallbacks;
|
|
739
|
+
|
|
740
|
+
for (const rawPath of mediaPaths) {
|
|
741
|
+
const resolved = imMedia.resolveMediaPath(rawPath, this.project.cwd);
|
|
742
|
+
if (!resolved.ok) {
|
|
743
|
+
console.error(`[im] skip media ${rawPath}: ${resolved.error}`);
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (this.mediaUploadDisabled) {
|
|
748
|
+
fallbacks.push(imMedia.formatMediaUploadFallback(resolved.path, "企业微信机器人未开通图片/文件上传权限"));
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const mediaType = imMedia.detectWeComMediaType(resolved.path, resolved.size);
|
|
753
|
+
try {
|
|
754
|
+
const upload = await this.client.uploadMediaFile(resolved.path, { mediaType });
|
|
755
|
+
await this.client.replyMediaMessage(reqId, upload.mediaType, upload.mediaId);
|
|
756
|
+
console.error(`[im] delivered media ${resolved.path} as ${upload.mediaType}`);
|
|
757
|
+
} catch (error) {
|
|
758
|
+
const errcode = error && typeof error === "object" ? error.errcode : undefined;
|
|
759
|
+
const errmsg = error && typeof error === "object" ? error.errmsg : undefined;
|
|
760
|
+
if (imMedia.isWeComMediaPermissionError(errcode, errmsg || String(error))) {
|
|
761
|
+
this.mediaUploadDisabled = true;
|
|
762
|
+
console.error(`[im] media upload disabled for ${this.project.cwd}: ${errmsg || String(error)}`);
|
|
763
|
+
fallbacks.push(imMedia.formatMediaUploadFallback(
|
|
764
|
+
resolved.path,
|
|
765
|
+
"企业微信机器人未开通图片/文件上传权限",
|
|
766
|
+
));
|
|
767
|
+
} else {
|
|
768
|
+
console.error(`[im] media delivery failed for ${resolved.path}: ${String(error)}`);
|
|
769
|
+
fallbacks.push(imMedia.formatMediaUploadFallback(resolved.path, String(error)));
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
return fallbacks;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
async sendFallbackText(reqId, text) {
|
|
778
|
+
if (!text.trim() || !this.client) return;
|
|
779
|
+
const streamId = `${reqId}-fallback-${Date.now()}`;
|
|
780
|
+
await this.client.replyStream(reqId, streamId, text, true);
|
|
781
|
+
}
|
|
782
|
+
|
|
623
783
|
async handleFrame(frame) {
|
|
624
784
|
const inbound = extractWeComInboundMessage(frame);
|
|
625
785
|
if (!inbound) return;
|
|
@@ -629,6 +789,7 @@ class ProjectWeComBridge {
|
|
|
629
789
|
const startedAt = Date.now();
|
|
630
790
|
let refreshTimer = null;
|
|
631
791
|
let toolsRunning = false;
|
|
792
|
+
let lastCleanedText = "";
|
|
632
793
|
|
|
633
794
|
try {
|
|
634
795
|
await reply.beginProcessing();
|
|
@@ -650,7 +811,9 @@ class ProjectWeComBridge {
|
|
|
650
811
|
},
|
|
651
812
|
onDelta: (text) => {
|
|
652
813
|
toolsRunning = false;
|
|
653
|
-
|
|
814
|
+
const cleaned = imMedia.cleanImReplyText(text).cleaned;
|
|
815
|
+
lastCleanedText = cleaned;
|
|
816
|
+
reply.schedulePush(cleaned);
|
|
654
817
|
},
|
|
655
818
|
});
|
|
656
819
|
|
|
@@ -658,10 +821,17 @@ class ProjectWeComBridge {
|
|
|
658
821
|
console.error(`[im] turn failed for ${this.project.cwd} user ${inbound.userId}: ${result.reply || "unknown error"}`);
|
|
659
822
|
}
|
|
660
823
|
|
|
661
|
-
const
|
|
824
|
+
const rawReply = typeof result.reply === "string" && result.reply.trim()
|
|
662
825
|
? result.reply.trim()
|
|
663
826
|
: "No reply from annodex.";
|
|
664
|
-
|
|
827
|
+
const { cleaned, mediaPaths } = imMedia.cleanImReplyText(rawReply);
|
|
828
|
+
const finishText = cleaned || lastCleanedText || "No reply from annodex.";
|
|
829
|
+
await reply.finish(finishText);
|
|
830
|
+
|
|
831
|
+
const fallbacks = await this.deliverMedia(inbound.reqId, mediaPaths);
|
|
832
|
+
if (fallbacks.length) {
|
|
833
|
+
await this.sendFallbackText(inbound.reqId, fallbacks.join("\n\n"));
|
|
834
|
+
}
|
|
665
835
|
} catch (error) {
|
|
666
836
|
console.error(`[im] turn request failed for ${this.project.cwd} user ${inbound.userId}: ${String(error)}`);
|
|
667
837
|
await reply.fail(`annodex IM error: ${String(error)}`);
|
package/lib/im-media.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const os = require("os");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
const MEDIA_DELIVERY_EXTS = [
|
|
8
|
+
"png", "jpg", "jpeg", "gif", "webp", "bmp", "svg",
|
|
9
|
+
"pdf", "csv", "tsv", "txt", "md", "json", "yaml", "yml",
|
|
10
|
+
"html", "htm", "xml",
|
|
11
|
+
"xlsx", "xls", "docx", "doc", "pptx", "ppt",
|
|
12
|
+
"zip", "tar", "gz", "tgz", "7z",
|
|
13
|
+
"mp4", "mov", "webm", "mp3", "wav", "ogg", "amr",
|
|
14
|
+
].join("|");
|
|
15
|
+
|
|
16
|
+
const MEDIA_TAG_RE = /\bMEDIA:\s*(`[^`\n]+`|"[^"\n]+"|'[^'\n]+'|(?:~\/|\/|[A-Za-z]:[/\\])[^\s"'`,;:)\]}]+)/gi;
|
|
17
|
+
|
|
18
|
+
const SHOW_WIDGET_FENCE_RE = /```\s*show-widget[\s\S]*?```/gi;
|
|
19
|
+
const SHOW_WIDGET_PARTIAL_RE = /```\s*show-widget[\s\S]*$/;
|
|
20
|
+
|
|
21
|
+
const IMAGE_EXTS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".svg"]);
|
|
22
|
+
const VIDEO_EXTS = new Set([".mp4", ".mov", ".webm", ".avi", ".mkv"]);
|
|
23
|
+
const AUDIO_EXTS = new Set([".mp3", ".wav", ".ogg", ".amr"]);
|
|
24
|
+
|
|
25
|
+
const ABSOLUTE_MAX_BYTES = 20 * 1024 * 1024;
|
|
26
|
+
const IMAGE_MAX_BYTES = 10 * 1024 * 1024;
|
|
27
|
+
|
|
28
|
+
const WECOM_MEDIA_PERMISSION_ERRCODES = new Set([48002, 60020, 81013]);
|
|
29
|
+
|
|
30
|
+
function collapseBlankLines(text) {
|
|
31
|
+
return text.replace(/\n{3,}/g, "\n\n").trim();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function stripShowWidgetFences(text) {
|
|
35
|
+
let cleaned = text.replace(SHOW_WIDGET_FENCE_RE, "");
|
|
36
|
+
cleaned = cleaned.replace(SHOW_WIDGET_PARTIAL_RE, "");
|
|
37
|
+
return cleaned;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeMediaTagPath(rawPath) {
|
|
41
|
+
let value = String(rawPath || "").trim();
|
|
42
|
+
if (!value) return "";
|
|
43
|
+
if (value.length >= 2 && value[0] === value[value.length - 1] && "`\"'".includes(value[0])) {
|
|
44
|
+
value = value.slice(1, -1).trim();
|
|
45
|
+
}
|
|
46
|
+
return value.replace(/^[`"'\\]+|[`"'\\.,;:)\]}]+$/g, "").trim();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function extractMediaFromText(text) {
|
|
50
|
+
const media = [];
|
|
51
|
+
const seen = new Set();
|
|
52
|
+
let cleaned = text;
|
|
53
|
+
|
|
54
|
+
for (const match of text.matchAll(MEDIA_TAG_RE)) {
|
|
55
|
+
const rawPath = normalizeMediaTagPath(match[1] || "");
|
|
56
|
+
if (!rawPath || seen.has(rawPath)) continue;
|
|
57
|
+
if (!hasDeliverableExtension(rawPath)) continue;
|
|
58
|
+
seen.add(rawPath);
|
|
59
|
+
media.push(rawPath);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
cleaned = cleaned.replace(MEDIA_TAG_RE, "");
|
|
63
|
+
cleaned = collapseBlankLines(cleaned);
|
|
64
|
+
return { mediaPaths: media, cleaned };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function hasDeliverableExtension(filePath) {
|
|
68
|
+
const ext = path.extname(filePath).slice(1).toLowerCase();
|
|
69
|
+
if (!ext) return false;
|
|
70
|
+
return MEDIA_DELIVERY_EXTS.split("|").includes(ext);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function resolveMediaPath(rawPath, cwd) {
|
|
74
|
+
const trimmed = String(rawPath || "").trim();
|
|
75
|
+
if (!trimmed) {
|
|
76
|
+
return { ok: false, error: "empty path" };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let resolved;
|
|
80
|
+
try {
|
|
81
|
+
if (trimmed.startsWith("~")) {
|
|
82
|
+
resolved = path.resolve(trimmed.replace(/^~(?=$|[/\\])/, os.homedir()));
|
|
83
|
+
} else if (path.isAbsolute(trimmed)) {
|
|
84
|
+
resolved = path.resolve(trimmed);
|
|
85
|
+
} else if (cwd) {
|
|
86
|
+
resolved = path.resolve(cwd, trimmed);
|
|
87
|
+
} else {
|
|
88
|
+
return { ok: false, error: "relative path requires cwd" };
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
return { ok: false, error: String(error) };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let stat;
|
|
95
|
+
try {
|
|
96
|
+
stat = fs.statSync(resolved);
|
|
97
|
+
} catch {
|
|
98
|
+
return { ok: false, error: `file not found: ${resolved}` };
|
|
99
|
+
}
|
|
100
|
+
if (!stat.isFile()) {
|
|
101
|
+
return { ok: false, error: `not a file: ${resolved}` };
|
|
102
|
+
}
|
|
103
|
+
if (stat.size > ABSOLUTE_MAX_BYTES) {
|
|
104
|
+
return { ok: false, error: `file exceeds 20MB WeCom limit: ${resolved}` };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { ok: true, path: resolved, size: stat.size };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function detectWeComMediaType(filePath, sizeBytes) {
|
|
111
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
112
|
+
if (IMAGE_EXTS.has(ext)) {
|
|
113
|
+
if (sizeBytes > IMAGE_MAX_BYTES) return "file";
|
|
114
|
+
return "image";
|
|
115
|
+
}
|
|
116
|
+
if (VIDEO_EXTS.has(ext)) return "video";
|
|
117
|
+
if (AUDIO_EXTS.has(ext)) return "voice";
|
|
118
|
+
return "file";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function cleanImReplyText(text) {
|
|
122
|
+
let cleaned = stripShowWidgetFences(String(text || ""));
|
|
123
|
+
const extracted = extractMediaFromText(cleaned);
|
|
124
|
+
cleaned = extracted.cleaned;
|
|
125
|
+
if (stripShowWidgetFences(String(text || "")).includes("show-widget") && !cleaned) {
|
|
126
|
+
cleaned = "(visual diagram — see attachment below if available)";
|
|
127
|
+
}
|
|
128
|
+
return { cleaned, mediaPaths: extracted.mediaPaths };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isWeComMediaPermissionError(errcode, errmsg) {
|
|
132
|
+
if (WECOM_MEDIA_PERMISSION_ERRCODES.has(Number(errcode))) return true;
|
|
133
|
+
const msg = String(errmsg || "").toLowerCase();
|
|
134
|
+
return (
|
|
135
|
+
msg.includes("permission")
|
|
136
|
+
|| msg.includes("privilege")
|
|
137
|
+
|| msg.includes("auth")
|
|
138
|
+
|| msg.includes("权限")
|
|
139
|
+
|| msg.includes("上传")
|
|
140
|
+
|| msg.includes("upload")
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function formatMediaUploadFallback(filePath, reason) {
|
|
145
|
+
const label = reason || "企业微信机器人可能未开通图片/文件上传权限";
|
|
146
|
+
return `附件未能上传(${label})。文件路径:\`${filePath}\``;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function buildImMediaGuidance() {
|
|
150
|
+
return [
|
|
151
|
+
"WeCom IM cannot render show-widget, HTML, or interactive charts in chat.",
|
|
152
|
+
"For diagrams, charts, or exports: save PNG/PDF (or other files) under the project directory, then include one line per file:",
|
|
153
|
+
"MEDIA:/absolute/path/to/file.png",
|
|
154
|
+
"Always mention the saved file path in plain text as well — some enterprises disable bot file/image upload.",
|
|
155
|
+
"Do not use show-widget fences in IM replies.",
|
|
156
|
+
].join("\n");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = {
|
|
160
|
+
ABSOLUTE_MAX_BYTES,
|
|
161
|
+
IMAGE_MAX_BYTES,
|
|
162
|
+
cleanImReplyText,
|
|
163
|
+
collapseBlankLines,
|
|
164
|
+
detectWeComMediaType,
|
|
165
|
+
extractMediaFromText,
|
|
166
|
+
formatMediaUploadFallback,
|
|
167
|
+
isWeComMediaPermissionError,
|
|
168
|
+
resolveMediaPath,
|
|
169
|
+
stripShowWidgetFences,
|
|
170
|
+
buildImMediaGuidance,
|
|
171
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seqyuan/annodex",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.74",
|
|
4
4
|
"description": "AI-native bioinformatics workspace by Annoroad",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"lib/macos-codex-security.js",
|
|
24
24
|
"lib/default-SOUL.md",
|
|
25
25
|
"lib/default-HARNESS.md",
|
|
26
|
+
"lib/im-media.js",
|
|
26
27
|
"public",
|
|
27
28
|
"next.config.ts",
|
|
28
29
|
"package.json"
|
|
File without changes
|
|
File without changes
|