@nextclaw/channel-runtime 0.2.12 → 0.3.0

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
@@ -99,8 +99,7 @@ declare class EmailChannel extends BaseChannel<Config["channels"]["email"]> {
99
99
 
100
100
  declare class FeishuChannel extends BaseChannel<Config["channels"]["feishu"]> {
101
101
  name: string;
102
- private client;
103
- private wsClient;
102
+ private clients;
104
103
  private processedMessageIds;
105
104
  private processedSet;
106
105
  constructor(config: Config["channels"]["feishu"], bus: MessageBus);
@@ -110,6 +109,11 @@ declare class FeishuChannel extends BaseChannel<Config["channels"]["feishu"]> {
110
109
  private handleIncoming;
111
110
  private isDuplicate;
112
111
  private addReaction;
112
+ private resolveOutboundAccount;
113
+ private isAllowedByPolicy;
114
+ private resolveMentionState;
115
+ private convertResource;
116
+ private buildInboundPayload;
113
117
  }
114
118
 
115
119
  declare class MochatChannel extends BaseChannel<Config["channels"]["mochat"]> {
package/dist/index.js CHANGED
@@ -1264,138 +1264,237 @@ var EmailChannel = class extends BaseChannel {
1264
1264
  }
1265
1265
  };
1266
1266
 
1267
- // src/channels/feishu.ts
1268
- import * as Lark from "@larksuiteoapi/node-sdk";
1269
- var MSG_TYPE_MAP = {
1270
- image: "[image]",
1271
- audio: "[audio]",
1272
- file: "[file]",
1273
- sticker: "[sticker]"
1274
- };
1267
+ // src/channels/feishu-message-support.ts
1275
1268
  var TABLE_RE = /((?:^[ \t]*\|.+\|[ \t]*\n)(?:^[ \t]*\|[-:\s|]+\|[ \t]*\n)(?:^[ \t]*\|.+\|[ \t]*\n?)+)/gm;
1269
+ function extractSenderInfo(sender) {
1270
+ const senderIdObj = sender.sender_id ?? {};
1271
+ const senderOpenId = senderIdObj.open_id || sender.open_id || "";
1272
+ const senderUserId = senderIdObj.user_id || sender.user_id || "";
1273
+ const senderUnionId = senderIdObj.union_id || sender.union_id || "";
1274
+ return {
1275
+ senderId: senderOpenId || senderUserId || senderUnionId || "",
1276
+ senderType: sender.sender_type ?? sender.senderType,
1277
+ senderOpenId: senderOpenId || void 0,
1278
+ senderUserId: senderUserId || void 0,
1279
+ senderUnionId: senderUnionId || void 0
1280
+ };
1281
+ }
1282
+ function extractMessageInfo(message) {
1283
+ const chatId = message.chat_id ?? "";
1284
+ const chatType = message.chat_type ?? "";
1285
+ return {
1286
+ chatId,
1287
+ chatType,
1288
+ isGroup: chatType === "group",
1289
+ msgType: message.msg_type ?? message.message_type ?? "",
1290
+ messageId: message.message_id ?? "",
1291
+ rawContent: typeof message.content === "string" ? message.content : ""
1292
+ };
1293
+ }
1294
+ function extractMentions(root, message) {
1295
+ if (Array.isArray(root.mentions)) {
1296
+ return root.mentions;
1297
+ }
1298
+ if (Array.isArray(message.mentions)) {
1299
+ return message.mentions;
1300
+ }
1301
+ return [];
1302
+ }
1303
+ function buildInboundMetadata(params) {
1304
+ return {
1305
+ account_id: params.accountId,
1306
+ accountId: params.accountId,
1307
+ message_id: params.messageInfo.messageId,
1308
+ chat_id: params.messageInfo.chatId,
1309
+ chat_type: params.messageInfo.chatType,
1310
+ msg_type: params.messageInfo.msgType,
1311
+ is_group: params.messageInfo.isGroup,
1312
+ peer_kind: params.messageInfo.isGroup ? "group" : "direct",
1313
+ peer_id: params.messageInfo.isGroup ? params.messageInfo.chatId : params.senderInfo.senderId,
1314
+ sender_open_id: params.senderInfo.senderOpenId,
1315
+ sender_user_id: params.senderInfo.senderUserId,
1316
+ sender_union_id: params.senderInfo.senderUnionId,
1317
+ was_mentioned: params.mentionState.wasMentioned,
1318
+ require_mention: params.mentionState.requireMention
1319
+ };
1320
+ }
1321
+ function inferFeishuResourceMimeType(resourceType) {
1322
+ if (resourceType === "image" || resourceType === "sticker") {
1323
+ return "image/*";
1324
+ }
1325
+ if (resourceType === "audio") {
1326
+ return "audio/*";
1327
+ }
1328
+ return void 0;
1329
+ }
1330
+ function buildFeishuCardElements(content) {
1331
+ const elements = [];
1332
+ let lastEnd = 0;
1333
+ for (const match of content.matchAll(TABLE_RE)) {
1334
+ const start = match.index ?? 0;
1335
+ const tableText = match[1] ?? "";
1336
+ const before = content.slice(lastEnd, start).trim();
1337
+ if (before) {
1338
+ elements.push({ tag: "markdown", content: before });
1339
+ }
1340
+ elements.push(parseMarkdownTable(tableText) ?? { tag: "markdown", content: tableText });
1341
+ lastEnd = start + tableText.length;
1342
+ }
1343
+ const remaining = content.slice(lastEnd).trim();
1344
+ if (remaining) {
1345
+ elements.push({ tag: "markdown", content: remaining });
1346
+ }
1347
+ if (!elements.length) {
1348
+ elements.push({ tag: "markdown", content });
1349
+ }
1350
+ return elements;
1351
+ }
1352
+ function parseMarkdownTable(tableText) {
1353
+ const lines = tableText.trim().split("\n").map((line) => line.trim()).filter(Boolean);
1354
+ if (lines.length < 3) {
1355
+ return null;
1356
+ }
1357
+ const split = (line) => line.replace(/^\|+|\|+$/g, "").split("|").map((item) => item.trim());
1358
+ const headers = split(lines[0]);
1359
+ const rows = lines.slice(2).map(split);
1360
+ return {
1361
+ tag: "table",
1362
+ page_size: rows.length + 1,
1363
+ columns: headers.map((header, index) => ({
1364
+ tag: "column",
1365
+ name: `c${index}`,
1366
+ display_name: header,
1367
+ width: "auto"
1368
+ })),
1369
+ rows: rows.map((row) => {
1370
+ const values = {};
1371
+ headers.forEach((_, index) => {
1372
+ values[`c${index}`] = row[index] ?? "";
1373
+ });
1374
+ return values;
1375
+ })
1376
+ };
1377
+ }
1378
+
1379
+ // src/channels/feishu.ts
1380
+ import {
1381
+ buildFeishuConvertContext,
1382
+ convertFeishuMessageContent,
1383
+ getDefaultFeishuAccountId,
1384
+ getEnabledFeishuAccounts,
1385
+ LarkClient
1386
+ } from "@nextclaw/feishu-core";
1276
1387
  function isRecord(value) {
1277
1388
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1278
1389
  }
1279
1390
  var FeishuChannel = class extends BaseChannel {
1280
1391
  name = "feishu";
1281
- client = null;
1282
- wsClient = null;
1392
+ clients = /* @__PURE__ */ new Map();
1283
1393
  processedMessageIds = [];
1284
1394
  processedSet = /* @__PURE__ */ new Set();
1285
1395
  constructor(config, bus) {
1286
1396
  super(config, bus);
1287
1397
  }
1288
1398
  async start() {
1289
- if (!this.config.appId || !this.config.appSecret) {
1399
+ const accounts = getEnabledFeishuAccounts(this.config);
1400
+ if (accounts.length === 0) {
1290
1401
  throw new Error("Feishu appId/appSecret not configured");
1291
1402
  }
1292
1403
  this.running = true;
1293
- this.client = new Lark.Client({ appId: this.config.appId, appSecret: this.config.appSecret });
1294
- const dispatcher = new Lark.EventDispatcher({
1295
- encryptKey: this.config.encryptKey || void 0,
1296
- verificationToken: this.config.verificationToken || void 0
1297
- }).register({
1298
- "im.message.receive_v1": async (data) => {
1299
- await this.handleIncoming(data);
1404
+ for (const account of accounts) {
1405
+ const client = LarkClient.fromAccount(account);
1406
+ const activeAccount = {
1407
+ accountId: account.accountId,
1408
+ client
1409
+ };
1410
+ const probe = await client.probe();
1411
+ if (probe.ok) {
1412
+ activeAccount.botOpenId = probe.botOpenId;
1413
+ activeAccount.botName = probe.botName;
1300
1414
  }
1301
- });
1302
- this.wsClient = new Lark.WSClient({
1303
- appId: this.config.appId,
1304
- appSecret: this.config.appSecret,
1305
- loggerLevel: Lark.LoggerLevel.info
1306
- });
1307
- this.wsClient.start({ eventDispatcher: dispatcher });
1415
+ client.startWebsocket(async (data) => {
1416
+ await this.handleIncoming(account.accountId, data);
1417
+ });
1418
+ this.clients.set(account.accountId, activeAccount);
1419
+ }
1308
1420
  }
1309
1421
  async stop() {
1310
1422
  this.running = false;
1311
- if (this.wsClient) {
1312
- this.wsClient.close();
1313
- this.wsClient = null;
1423
+ for (const account of this.clients.values()) {
1424
+ account.client.closeWebsocket();
1314
1425
  }
1426
+ this.clients.clear();
1315
1427
  }
1316
1428
  async send(msg) {
1317
- if (!this.client) {
1429
+ const account = this.resolveOutboundAccount(msg.metadata);
1430
+ if (!account) {
1318
1431
  return;
1319
1432
  }
1320
1433
  const receiveIdType = msg.chatId.startsWith("oc_") ? "chat_id" : "open_id";
1321
- const elements = buildCardElements(msg.content ?? "");
1434
+ const elements = buildFeishuCardElements(msg.content ?? "");
1322
1435
  const card = {
1323
1436
  config: { wide_screen_mode: true },
1324
1437
  elements
1325
1438
  };
1326
1439
  const content = JSON.stringify(card);
1327
- await this.client.im.message.create({
1328
- params: { receive_id_type: receiveIdType },
1329
- data: {
1330
- receive_id: msg.chatId,
1331
- msg_type: "interactive",
1332
- content
1333
- }
1440
+ await account.client.sendInteractiveCard({
1441
+ receiveId: msg.chatId,
1442
+ receiveIdType,
1443
+ content
1334
1444
  });
1335
1445
  }
1336
- async handleIncoming(data) {
1446
+ async handleIncoming(accountId, data) {
1447
+ const account = this.clients.get(accountId);
1448
+ if (!account) {
1449
+ return;
1450
+ }
1337
1451
  const root = isRecord(data.event) ? data.event : data;
1338
1452
  const message = root.message ?? data.message ?? {};
1339
1453
  const sender = root.sender ?? message.sender ?? data.sender ?? {};
1340
- const senderIdObj = sender.sender_id ?? {};
1341
- const senderOpenId = senderIdObj.open_id || sender.open_id || "";
1342
- const senderUserId = senderIdObj.user_id || sender.user_id || "";
1343
- const senderUnionId = senderIdObj.union_id || sender.union_id || "";
1344
- const senderId = senderOpenId || senderUserId || senderUnionId || "";
1345
- const senderType = sender.sender_type ?? sender.senderType;
1346
- if (senderType === "bot") {
1347
- return;
1348
- }
1349
- const chatId = message.chat_id ?? "";
1350
- const chatType = message.chat_type ?? "";
1351
- const isGroup = chatType === "group";
1352
- const msgType = message.msg_type ?? message.message_type ?? "";
1353
- const messageId = message.message_id ?? "";
1354
- if (!senderId || !chatId) {
1454
+ const senderInfo = extractSenderInfo(sender);
1455
+ if (senderInfo.senderType === "bot") {
1355
1456
  return;
1356
1457
  }
1357
- if (!this.isAllowed(String(senderId))) {
1458
+ const messageInfo = extractMessageInfo(message);
1459
+ if (!senderInfo.senderId || !messageInfo.chatId) {
1358
1460
  return;
1359
1461
  }
1360
- if (messageId && this.isDuplicate(messageId)) {
1462
+ if (!this.isAllowedByPolicy({ senderId: senderInfo.senderId, chatId: messageInfo.chatId, isGroup: messageInfo.isGroup })) {
1361
1463
  return;
1362
1464
  }
1363
- if (messageId) {
1364
- await this.addReaction(messageId, "THUMBSUP");
1465
+ if (messageInfo.messageId && this.isDuplicate(`${accountId}:${messageInfo.messageId}`)) {
1466
+ return;
1365
1467
  }
1366
- let content = "";
1367
- if (message.content) {
1368
- try {
1369
- const parsed = JSON.parse(String(message.content));
1370
- content = String(parsed.text ?? parsed.content ?? "");
1371
- } catch {
1372
- content = String(message.content);
1373
- }
1468
+ if (messageInfo.messageId) {
1469
+ await this.addReaction(account, messageInfo.messageId, "THUMBSUP");
1374
1470
  }
1375
- if (!content && MSG_TYPE_MAP[msgType]) {
1376
- content = MSG_TYPE_MAP[msgType];
1471
+ const mentions = extractMentions(root, message);
1472
+ const mentionState = this.resolveMentionState({
1473
+ account,
1474
+ mentions,
1475
+ chatId: messageInfo.chatId,
1476
+ isGroup: messageInfo.isGroup,
1477
+ rawContent: messageInfo.rawContent
1478
+ });
1479
+ if (mentionState.requireMention && !mentionState.wasMentioned) {
1480
+ return;
1377
1481
  }
1378
- if (!content) {
1482
+ const payload = this.buildInboundPayload(account, messageInfo, mentions);
1483
+ if (!payload) {
1379
1484
  return;
1380
1485
  }
1381
1486
  await this.handleMessage({
1382
- senderId: String(senderId),
1487
+ senderId: senderInfo.senderId,
1383
1488
  // Always route by Feishu chat_id so DM/group sessions are stable.
1384
- chatId,
1385
- content,
1386
- attachments: [],
1387
- metadata: {
1388
- message_id: messageId,
1389
- chat_id: chatId,
1390
- chat_type: chatType,
1391
- msg_type: msgType,
1392
- is_group: isGroup,
1393
- peer_kind: isGroup ? "group" : "direct",
1394
- peer_id: chatId,
1395
- sender_open_id: senderOpenId || void 0,
1396
- sender_user_id: senderUserId || void 0,
1397
- sender_union_id: senderUnionId || void 0
1398
- }
1489
+ chatId: messageInfo.chatId,
1490
+ content: payload.content,
1491
+ attachments: payload.attachments,
1492
+ metadata: buildInboundMetadata({
1493
+ accountId,
1494
+ messageInfo,
1495
+ senderInfo,
1496
+ mentionState
1497
+ })
1399
1498
  });
1400
1499
  }
1401
1500
  isDuplicate(messageId) {
@@ -1412,69 +1511,99 @@ var FeishuChannel = class extends BaseChannel {
1412
1511
  }
1413
1512
  return false;
1414
1513
  }
1415
- async addReaction(messageId, emojiType) {
1416
- if (!this.client) {
1417
- return;
1418
- }
1514
+ async addReaction(account, messageId, emojiType) {
1419
1515
  try {
1420
- await this.client.im.messageReaction.create({
1421
- path: { message_id: messageId },
1422
- data: { reaction_type: { emoji_type: emojiType } }
1423
- });
1516
+ await account.client.addReaction(messageId, emojiType);
1424
1517
  } catch {
1425
1518
  }
1426
1519
  }
1427
- };
1428
- function buildCardElements(content) {
1429
- const elements = [];
1430
- let lastEnd = 0;
1431
- for (const match of content.matchAll(TABLE_RE)) {
1432
- const start = match.index ?? 0;
1433
- const tableText = match[1] ?? "";
1434
- const before = content.slice(lastEnd, start).trim();
1435
- if (before) {
1436
- elements.push({ tag: "markdown", content: before });
1520
+ resolveOutboundAccount(metadata) {
1521
+ const accountId = typeof metadata?.accountId === "string" ? metadata.accountId : typeof metadata?.account_id === "string" ? metadata.account_id : getDefaultFeishuAccountId(this.config);
1522
+ return this.clients.get(accountId) ?? this.clients.get(getDefaultFeishuAccountId(this.config)) ?? null;
1523
+ }
1524
+ isAllowedByPolicy(params) {
1525
+ if (!params.isGroup) {
1526
+ if (this.config.dmPolicy === "disabled") {
1527
+ return false;
1528
+ }
1529
+ if (this.config.dmPolicy === "allowlist" || this.config.dmPolicy === "pairing") {
1530
+ return this.isAllowed(params.senderId);
1531
+ }
1532
+ const allowFrom = this.config.allowFrom ?? [];
1533
+ return allowFrom.length === 0 || allowFrom.includes("*") || this.isAllowed(params.senderId);
1437
1534
  }
1438
- elements.push(parseMdTable(tableText) ?? { tag: "markdown", content: tableText });
1439
- lastEnd = start + tableText.length;
1535
+ if (this.config.groupPolicy === "disabled") {
1536
+ return false;
1537
+ }
1538
+ if (this.config.groupPolicy === "allowlist") {
1539
+ const allowFrom = this.config.groupAllowFrom ?? [];
1540
+ return allowFrom.includes("*") || allowFrom.includes(params.chatId);
1541
+ }
1542
+ return true;
1440
1543
  }
1441
- const remaining = content.slice(lastEnd).trim();
1442
- if (remaining) {
1443
- elements.push({ tag: "markdown", content: remaining });
1544
+ resolveMentionState(params) {
1545
+ if (!params.isGroup) {
1546
+ return { wasMentioned: false, requireMention: false };
1547
+ }
1548
+ const groupRule = this.config.groups?.[params.chatId] ?? this.config.groups?.["*"];
1549
+ const requireMention = groupRule?.requireMention ?? this.config.requireMention ?? false;
1550
+ if (!requireMention) {
1551
+ return { wasMentioned: false, requireMention: false };
1552
+ }
1553
+ const patterns = [...this.config.mentionPatterns ?? [], ...groupRule?.mentionPatterns ?? []].map((pattern) => pattern.trim()).filter(Boolean);
1554
+ const rawText = params.rawContent.toLowerCase();
1555
+ const mentionedByPattern = patterns.some((pattern) => {
1556
+ try {
1557
+ return new RegExp(pattern, "i").test(rawText);
1558
+ } catch {
1559
+ return rawText.includes(pattern.toLowerCase());
1560
+ }
1561
+ });
1562
+ const mentionedByIds = params.mentions.some((entry) => {
1563
+ if (!entry || typeof entry !== "object") {
1564
+ return false;
1565
+ }
1566
+ const mention = entry;
1567
+ const openId = (typeof mention.open_id === "string" ? mention.open_id : "") || (mention.id && typeof mention.id === "object" && "open_id" in mention.id ? mention.id.open_id ?? "" : typeof mention.id === "string" ? mention.id : "");
1568
+ const name = typeof mention.name === "string" ? mention.name : "";
1569
+ return openId === params.account.botOpenId || (params.account.botName ? name === params.account.botName : false);
1570
+ });
1571
+ return {
1572
+ wasMentioned: mentionedByPattern || mentionedByIds,
1573
+ requireMention
1574
+ };
1444
1575
  }
1445
- if (!elements.length) {
1446
- elements.push({ tag: "markdown", content });
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
+ };
1447
1585
  }
1448
- return elements;
1449
- }
1450
- function parseMdTable(tableText) {
1451
- const lines = tableText.trim().split("\n").map((line) => line.trim()).filter(Boolean);
1452
- if (lines.length < 3) {
1453
- return null;
1586
+ buildInboundPayload(account, messageInfo, mentions) {
1587
+ const converted = convertFeishuMessageContent(
1588
+ messageInfo.rawContent,
1589
+ messageInfo.msgType,
1590
+ buildFeishuConvertContext({
1591
+ mentions,
1592
+ stripBotMentions: true,
1593
+ botOpenId: account.botOpenId,
1594
+ botName: account.botName
1595
+ })
1596
+ );
1597
+ const content = converted.content.trim();
1598
+ if (!content) {
1599
+ return null;
1600
+ }
1601
+ return {
1602
+ content,
1603
+ attachments: converted.resources.map((resource) => this.convertResource(resource))
1604
+ };
1454
1605
  }
1455
- const split = (line) => line.replace(/^\|+|\|+$/g, "").split("|").map((item) => item.trim());
1456
- const headers = split(lines[0]);
1457
- const rows = lines.slice(2).map(split);
1458
- const columns = headers.map((header, index) => ({
1459
- tag: "column",
1460
- name: `c${index}`,
1461
- display_name: header,
1462
- width: "auto"
1463
- }));
1464
- const tableRows = rows.map((row) => {
1465
- const values = {};
1466
- headers.forEach((_, index) => {
1467
- values[`c${index}`] = row[index] ?? "";
1468
- });
1469
- return values;
1470
- });
1471
- return {
1472
- tag: "table",
1473
- page_size: rows.length + 1,
1474
- columns,
1475
- rows: tableRows
1476
- };
1477
- }
1606
+ };
1478
1607
 
1479
1608
  // src/channels/mochat.ts
1480
1609
  import { io } from "socket.io-client";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/channel-runtime",
3
- "version": "0.2.12",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "description": "Runtime implementations for NextClaw builtin channel plugins.",
6
6
  "type": "module",
@@ -15,7 +15,6 @@
15
15
  "dist"
16
16
  ],
17
17
  "dependencies": {
18
- "@larksuiteoapi/node-sdk": "^1.58.0",
19
18
  "@slack/socket-mode": "^1.3.3",
20
19
  "@slack/web-api": "^7.6.0",
21
20
  "dingtalk-stream": "^2.1.4",
@@ -29,7 +28,8 @@
29
28
  "undici": "^6.21.0",
30
29
  "ws": "^8.18.0",
31
30
  "socket.io-msgpack-parser": "^3.0.2",
32
- "@nextclaw/core": "0.9.12"
31
+ "@nextclaw/core": "0.10.0",
32
+ "@nextclaw/feishu-core": "0.2.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/mailparser": "^3.4.6",