@nextclaw/channel-runtime 0.3.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 +1 -0
- package/dist/index.js +142 -25
- package/package.json +2 -2
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(
|
|
1577
|
-
return {
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1612
|
-
import { mkdirSync as
|
|
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 =
|
|
1632
|
-
cursorPath =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3141
|
-
import { mkdirSync as
|
|
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 =
|
|
3565
|
-
|
|
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.
|
|
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.
|
|
31
|
+
"@nextclaw/core": "0.11.1",
|
|
32
32
|
"@nextclaw/feishu-core": "0.2.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|