@nextclaw/channel-runtime 0.4.0 → 0.4.1

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/index.d.ts CHANGED
@@ -102,6 +102,7 @@ declare class FeishuChannel extends BaseChannel<Config["channels"]["feishu"]> {
102
102
  private clients;
103
103
  private processedMessageIds;
104
104
  private processedSet;
105
+ private readonly inboundMediaResolver;
105
106
  constructor(config: Config["channels"]["feishu"], bus: MessageBus);
106
107
  start(): Promise<void>;
107
108
  stop(): Promise<void>;
package/dist/index.js CHANGED
@@ -1384,6 +1384,115 @@ import {
1384
1384
  getEnabledFeishuAccounts,
1385
1385
  LarkClient
1386
1386
  } from "@nextclaw/feishu-core";
1387
+
1388
+ // src/channels/feishu-inbound-media.ts
1389
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
1390
+ import { join as join2 } from "path";
1391
+ var DEFAULT_FEISHU_MEDIA_MAX_MB = 20;
1392
+ function sanitizeAttachmentName2(value) {
1393
+ return value.replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "") || "attachment";
1394
+ }
1395
+ function inferAttachmentExtension(resourceType, mimeType) {
1396
+ const normalizedMime = mimeType?.toLowerCase();
1397
+ if (normalizedMime === "image/jpeg") {
1398
+ return ".jpg";
1399
+ }
1400
+ if (normalizedMime === "image/png") {
1401
+ return ".png";
1402
+ }
1403
+ if (normalizedMime === "image/webp") {
1404
+ return ".webp";
1405
+ }
1406
+ if (normalizedMime === "image/gif") {
1407
+ return ".gif";
1408
+ }
1409
+ if (normalizedMime === "audio/ogg") {
1410
+ return ".ogg";
1411
+ }
1412
+ if (normalizedMime === "audio/mpeg") {
1413
+ return ".mp3";
1414
+ }
1415
+ if (normalizedMime === "application/pdf") {
1416
+ return ".pdf";
1417
+ }
1418
+ if (resourceType === "image") {
1419
+ return ".jpg";
1420
+ }
1421
+ if (resourceType === "audio") {
1422
+ return ".ogg";
1423
+ }
1424
+ if (resourceType === "sticker") {
1425
+ return ".webp";
1426
+ }
1427
+ return ".bin";
1428
+ }
1429
+ function buildAttachmentFileName(params) {
1430
+ const extension = inferAttachmentExtension(params.resource.type, params.mimeType);
1431
+ const resourceId = sanitizeAttachmentName2(params.resource.fileKey).slice(0, 64);
1432
+ const messageId = sanitizeAttachmentName2(params.messageId).slice(0, 48);
1433
+ const preferredName = params.resource.fileName?.trim() ? sanitizeAttachmentName2(params.resource.fileName.trim()) : `${params.resource.type}${extension}`;
1434
+ const baseName = preferredName.includes(".") ? preferredName : `${preferredName}${extension}`;
1435
+ return `feishu_${messageId}_${resourceId}_${baseName}`;
1436
+ }
1437
+ function resolveMessageResourceType(resourceType) {
1438
+ return resourceType === "image" ? "image" : "file";
1439
+ }
1440
+ var FeishuInboundMediaResolver = class {
1441
+ maxBytes;
1442
+ constructor(maxMb) {
1443
+ this.maxBytes = Math.max(1, maxMb ?? DEFAULT_FEISHU_MEDIA_MAX_MB) * 1024 * 1024;
1444
+ }
1445
+ async resolve(params) {
1446
+ const { client, messageId, resource } = params;
1447
+ const mimeType = inferFeishuResourceMimeType(resource.type);
1448
+ const baseAttachment = {
1449
+ id: resource.fileKey,
1450
+ name: resource.fileName,
1451
+ source: "feishu",
1452
+ status: "remote-only",
1453
+ mimeType
1454
+ };
1455
+ if (!messageId?.trim()) {
1456
+ return {
1457
+ ...baseAttachment,
1458
+ errorCode: "invalid_payload"
1459
+ };
1460
+ }
1461
+ try {
1462
+ const downloaded = await client.downloadMessageResource({
1463
+ messageId,
1464
+ fileKey: resource.fileKey,
1465
+ type: resolveMessageResourceType(resource.type)
1466
+ });
1467
+ if (downloaded.buffer.length > this.maxBytes) {
1468
+ return {
1469
+ ...baseAttachment,
1470
+ size: downloaded.buffer.length,
1471
+ errorCode: "too_large"
1472
+ };
1473
+ }
1474
+ const mediaDir = join2(getDataPath(), "media");
1475
+ mkdirSync2(mediaDir, { recursive: true });
1476
+ const fileName = buildAttachmentFileName({ messageId, resource, mimeType });
1477
+ const filePath = join2(mediaDir, fileName);
1478
+ writeFileSync2(filePath, downloaded.buffer);
1479
+ return {
1480
+ ...baseAttachment,
1481
+ name: fileName,
1482
+ path: filePath,
1483
+ size: downloaded.buffer.length,
1484
+ status: "ready"
1485
+ };
1486
+ } catch {
1487
+ return {
1488
+ ...baseAttachment,
1489
+ errorCode: "download_failed"
1490
+ };
1491
+ }
1492
+ }
1493
+ };
1494
+
1495
+ // src/channels/feishu.ts
1387
1496
  function isRecord(value) {
1388
1497
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1389
1498
  }
@@ -1392,8 +1501,10 @@ var FeishuChannel = class extends BaseChannel {
1392
1501
  clients = /* @__PURE__ */ new Map();
1393
1502
  processedMessageIds = [];
1394
1503
  processedSet = /* @__PURE__ */ new Set();
1504
+ inboundMediaResolver;
1395
1505
  constructor(config, bus) {
1396
1506
  super(config, bus);
1507
+ this.inboundMediaResolver = new FeishuInboundMediaResolver(this.config.mediaMaxMb);
1397
1508
  }
1398
1509
  async start() {
1399
1510
  const accounts = getEnabledFeishuAccounts(this.config);
@@ -1479,7 +1590,7 @@ var FeishuChannel = class extends BaseChannel {
1479
1590
  if (mentionState.requireMention && !mentionState.wasMentioned) {
1480
1591
  return;
1481
1592
  }
1482
- const payload = this.buildInboundPayload(account, messageInfo, mentions);
1593
+ const payload = await this.buildInboundPayload(account, messageInfo, mentions);
1483
1594
  if (!payload) {
1484
1595
  return;
1485
1596
  }
@@ -1573,17 +1684,14 @@ var FeishuChannel = class extends BaseChannel {
1573
1684
  requireMention
1574
1685
  };
1575
1686
  }
1576
- convertResource(resource) {
1577
- return {
1578
- id: resource.fileKey,
1579
- name: resource.fileName,
1580
- source: "feishu",
1581
- status: "remote-only",
1582
- mimeType: inferFeishuResourceMimeType(resource.type),
1583
- url: resource.fileKey
1584
- };
1687
+ async convertResource(params) {
1688
+ return this.inboundMediaResolver.resolve({
1689
+ client: params.account.client,
1690
+ messageId: params.messageId,
1691
+ resource: params.resource
1692
+ });
1585
1693
  }
1586
- buildInboundPayload(account, messageInfo, mentions) {
1694
+ async buildInboundPayload(account, messageInfo, mentions) {
1587
1695
  const converted = convertFeishuMessageContent(
1588
1696
  messageInfo.rawContent,
1589
1697
  messageInfo.msgType,
@@ -1594,13 +1702,22 @@ var FeishuChannel = class extends BaseChannel {
1594
1702
  botName: account.botName
1595
1703
  })
1596
1704
  );
1597
- const content = converted.content.trim();
1598
- if (!content) {
1705
+ const content = converted.content.trim() || `[${messageInfo.msgType || "message"}]`;
1706
+ const attachments = await Promise.all(
1707
+ converted.resources.map(
1708
+ (resource) => this.convertResource({
1709
+ account,
1710
+ messageId: messageInfo.messageId,
1711
+ resource
1712
+ })
1713
+ )
1714
+ );
1715
+ if (!content && attachments.length === 0) {
1599
1716
  return null;
1600
1717
  }
1601
1718
  return {
1602
1719
  content,
1603
- attachments: converted.resources.map((resource) => this.convertResource(resource))
1720
+ attachments
1604
1721
  };
1605
1722
  }
1606
1723
  };
@@ -1608,8 +1725,8 @@ var FeishuChannel = class extends BaseChannel {
1608
1725
  // src/channels/mochat.ts
1609
1726
  import { io } from "socket.io-client";
1610
1727
  import { fetch as fetch3 } from "undici";
1611
- import { join as join2 } from "path";
1612
- import { mkdirSync as mkdirSync2, existsSync, readFileSync, writeFileSync as writeFileSync2 } from "fs";
1728
+ import { join as join3 } from "path";
1729
+ import { mkdirSync as mkdirSync3, existsSync, readFileSync, writeFileSync as writeFileSync3 } from "fs";
1613
1730
  var MAX_SEEN_MESSAGE_IDS = 2e3;
1614
1731
  var CURSOR_SAVE_DEBOUNCE_MS = 500;
1615
1732
  var AsyncLock = class {
@@ -1628,8 +1745,8 @@ var MochatChannel = class extends BaseChannel {
1628
1745
  socket = null;
1629
1746
  wsConnected = false;
1630
1747
  wsReady = false;
1631
- stateDir = join2(getDataPath(), "mochat");
1632
- cursorPath = join2(this.stateDir, "session_cursors.json");
1748
+ stateDir = join3(getDataPath(), "mochat");
1749
+ cursorPath = join3(this.stateDir, "session_cursors.json");
1633
1750
  sessionCursor = {};
1634
1751
  cursorSaveTimer = null;
1635
1752
  sessionSet = /* @__PURE__ */ new Set();
@@ -1655,7 +1772,7 @@ var MochatChannel = class extends BaseChannel {
1655
1772
  if (!this.config.clawToken) {
1656
1773
  throw new Error("Mochat clawToken not configured");
1657
1774
  }
1658
- mkdirSync2(this.stateDir, { recursive: true });
1775
+ mkdirSync3(this.stateDir, { recursive: true });
1659
1776
  await this.loadSessionCursors();
1660
1777
  this.seedTargetsFromConfig();
1661
1778
  await this.refreshTargets(false);
@@ -2338,13 +2455,13 @@ var MochatChannel = class extends BaseChannel {
2338
2455
  }
2339
2456
  async saveSessionCursors() {
2340
2457
  try {
2341
- mkdirSync2(this.stateDir, { recursive: true });
2458
+ mkdirSync3(this.stateDir, { recursive: true });
2342
2459
  const payload = {
2343
2460
  schemaVersion: 1,
2344
2461
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2345
2462
  cursors: this.sessionCursor
2346
2463
  };
2347
- writeFileSync2(this.cursorPath, JSON.stringify(payload, null, 2) + "\n");
2464
+ writeFileSync3(this.cursorPath, JSON.stringify(payload, null, 2) + "\n");
2348
2465
  } catch {
2349
2466
  return;
2350
2467
  }
@@ -3137,8 +3254,8 @@ var GroqTranscriptionProvider = class {
3137
3254
  };
3138
3255
 
3139
3256
  // src/channels/telegram.ts
3140
- import { join as join3 } from "path";
3141
- import { mkdirSync as mkdirSync3 } from "fs";
3257
+ import { join as join4 } from "path";
3258
+ import { mkdirSync as mkdirSync4 } from "fs";
3142
3259
  import {
3143
3260
  isAssistantStreamResetControlMessage,
3144
3261
  isTypingStopControlMessage as isTypingStopControlMessage2,
@@ -3561,8 +3678,8 @@ Just send me a text message to chat!`;
3561
3678
  }
3562
3679
  const { fileId, mediaType, mimeType } = resolveMedia(message);
3563
3680
  if (fileId && mediaType) {
3564
- const mediaDir = join3(getDataPath(), "media");
3565
- mkdirSync3(mediaDir, { recursive: true });
3681
+ const mediaDir = join4(getDataPath(), "media");
3682
+ mkdirSync4(mediaDir, { recursive: true });
3566
3683
  const extension = getExtension(mediaType, mimeType);
3567
3684
  const downloaded = await this.bot.downloadFile(fileId, mediaDir);
3568
3685
  const finalPath = extension && !downloaded.endsWith(extension) ? `${downloaded}${extension}` : downloaded;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/channel-runtime",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "private": false,
5
5
  "description": "Runtime implementations for NextClaw builtin channel plugins.",
6
6
  "type": "module",
@@ -28,7 +28,7 @@
28
28
  "undici": "^6.21.0",
29
29
  "ws": "^8.18.0",
30
30
  "socket.io-msgpack-parser": "^3.0.2",
31
- "@nextclaw/core": "0.11.0",
31
+ "@nextclaw/core": "0.11.1",
32
32
  "@nextclaw/feishu-core": "0.2.0"
33
33
  },
34
34
  "devDependencies": {