@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.
Files changed (73) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +8 -8
  3. package/.next/build-manifest.json +2 -2
  4. package/.next/prerender-manifest.json +3 -3
  5. package/.next/required-server-files.js +1 -1
  6. package/.next/required-server-files.json +1 -1
  7. package/.next/server/app/_global-error.html +1 -1
  8. package/.next/server/app/_global-error.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  14. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  15. package/.next/server/app/_not-found.html +1 -1
  16. package/.next/server/app/_not-found.rsc +1 -1
  17. package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  18. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  19. package/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  20. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  21. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  22. package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  23. package/.next/server/app/api/im/turn/route.js +1 -1
  24. package/.next/server/app/api/internal/runtime/route.js +1 -1
  25. package/.next/server/app/api/version/route.js +1 -1
  26. package/.next/server/app/docs/changelog.html +2 -2
  27. package/.next/server/app/docs/changelog.rsc +1 -1
  28. package/.next/server/app/docs/changelog.segments/_full.segment.rsc +1 -1
  29. package/.next/server/app/docs/changelog.segments/_head.segment.rsc +1 -1
  30. package/.next/server/app/docs/changelog.segments/_index.segment.rsc +1 -1
  31. package/.next/server/app/docs/changelog.segments/_tree.segment.rsc +1 -1
  32. package/.next/server/app/docs/changelog.segments/docs/changelog/__PAGE__.segment.rsc +1 -1
  33. package/.next/server/app/docs/changelog.segments/docs/changelog.segment.rsc +1 -1
  34. package/.next/server/app/docs/changelog.segments/docs.segment.rsc +1 -1
  35. package/.next/server/app/index.html +1 -1
  36. package/.next/server/app/index.rsc +1 -1
  37. package/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  38. package/.next/server/app/index.segments/_full.segment.rsc +1 -1
  39. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  40. package/.next/server/app/index.segments/_index.segment.rsc +1 -1
  41. package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  42. package/.next/server/app/login.html +1 -1
  43. package/.next/server/app/login.rsc +1 -1
  44. package/.next/server/app/login.segments/_full.segment.rsc +1 -1
  45. package/.next/server/app/login.segments/_head.segment.rsc +1 -1
  46. package/.next/server/app/login.segments/_index.segment.rsc +1 -1
  47. package/.next/server/app/login.segments/_tree.segment.rsc +1 -1
  48. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +1 -1
  49. package/.next/server/app/login.segments/login.segment.rsc +1 -1
  50. package/.next/server/app/workspace/page.js +2 -2
  51. package/.next/server/app/workspace/page_client-reference-manifest.js +1 -1
  52. package/.next/server/app/workspace.html +1 -1
  53. package/.next/server/app/workspace.rsc +2 -2
  54. package/.next/server/app/workspace.segments/_full.segment.rsc +2 -2
  55. package/.next/server/app/workspace.segments/_head.segment.rsc +1 -1
  56. package/.next/server/app/workspace.segments/_index.segment.rsc +1 -1
  57. package/.next/server/app/workspace.segments/_tree.segment.rsc +1 -1
  58. package/.next/server/app/workspace.segments/workspace/__PAGE__.segment.rsc +2 -2
  59. package/.next/server/app/workspace.segments/workspace.segment.rsc +1 -1
  60. package/.next/server/app-paths-manifest.json +8 -8
  61. package/.next/server/chunks/6983.js +1 -1
  62. package/.next/server/middleware-build-manifest.js +1 -1
  63. package/.next/server/next-font-manifest.js +1 -1
  64. package/.next/server/next-font-manifest.json +1 -1
  65. package/.next/server/pages/404.html +1 -1
  66. package/.next/server/pages/500.html +1 -1
  67. package/.next/server/server-reference-manifest.json +1 -1
  68. package/.next/static/chunks/app/workspace/{page-9879349ec16c1136.js → page-d5360de46c9f4386.js} +2 -2
  69. package/bin/annodex-im-gateway.js +173 -3
  70. package/lib/im-media.js +171 -0
  71. package/package.json +2 -1
  72. /package/.next/static/{9ja2nMxYXszS_1ydk3Gb9 → rKBVKaVLsaySHUHU-dLb7}/_buildManifest.js +0 -0
  73. /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
- reply.schedulePush(text);
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 text = typeof result.reply === "string" && result.reply.trim()
824
+ const rawReply = typeof result.reply === "string" && result.reply.trim()
662
825
  ? result.reply.trim()
663
826
  : "No reply from annodex.";
664
- await reply.finish(text);
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)}`);
@@ -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.72",
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"