@zeyiy/openclaw-channel 0.3.5 → 0.3.7
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/README.md +29 -1
- package/README.zh-CN.md +29 -1
- package/dist/config.js +3 -0
- package/dist/inbound.d.ts +3 -1
- package/dist/inbound.js +199 -6
- package/dist/media.js +2 -2
- package/dist/portal.js +1285 -4
- package/dist/tools.js +154 -113
- package/dist/types.d.ts +63 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,6 +14,9 @@ Chinese documentation: [README.zh-CN.md](https://github.com/ZeyiY/openclaw-chann
|
|
|
14
14
|
- Quote/reply message parsing for inbound context
|
|
15
15
|
- Multi-account login via `channels.openim.accounts.<id>`
|
|
16
16
|
- Group trigger policy with optional mention-only mode
|
|
17
|
+
- Auto read-receipt for direct messages
|
|
18
|
+
- Per-user session isolation (direct chat) / shared session (group chat)
|
|
19
|
+
- Agent Portal Bridge — persistent WebSocket connection to agent-portal cloud service
|
|
17
20
|
- Interactive setup command: `openclaw openim setup`
|
|
18
21
|
|
|
19
22
|
## Installation
|
|
@@ -58,7 +61,9 @@ openclaw openim setup
|
|
|
58
61
|
"enabled": true,
|
|
59
62
|
"token": "your_token",
|
|
60
63
|
"wsAddr": "ws://127.0.0.1:10001",
|
|
61
|
-
"apiAddr": "http://127.0.0.1:10002"
|
|
64
|
+
"apiAddr": "http://127.0.0.1:10002",
|
|
65
|
+
"botId": "my-bot-001",
|
|
66
|
+
"portalWsAddr": "wss://portal.example.com/ws"
|
|
62
67
|
}
|
|
63
68
|
}
|
|
64
69
|
}
|
|
@@ -75,6 +80,8 @@ If set, only these users can trigger processing:
|
|
|
75
80
|
- direct messages to the account
|
|
76
81
|
- group messages where they `@` the account
|
|
77
82
|
|
|
83
|
+
`botId` and `portalWsAddr` are optional. When both are set, the plugin establishes a WebSocket connection to the agent-portal cloud service, enabling remote management of agents, files, and models.
|
|
84
|
+
|
|
78
85
|
Single-account fallback (without `accounts`) is supported.
|
|
79
86
|
|
|
80
87
|
Environment fallback is supported for the `default` account:
|
|
@@ -113,6 +120,27 @@ Optional env overrides:
|
|
|
113
120
|
- `name` (optional): override filename for URL input
|
|
114
121
|
- `accountId` (optional): select sending account
|
|
115
122
|
|
|
123
|
+
## Agent Portal Bridge
|
|
124
|
+
|
|
125
|
+
When `botId` and `portalWsAddr` are configured, the plugin connects to the agent-portal cloud service via WebSocket. The portal can remotely invoke the following methods:
|
|
126
|
+
|
|
127
|
+
| Method | Description |
|
|
128
|
+
|---|---|
|
|
129
|
+
| `bot.agent.get` | Resolve the agentId bound to the current bot |
|
|
130
|
+
| `models.list` | List available models from config |
|
|
131
|
+
| `agents.list` | List all configured agents |
|
|
132
|
+
| `agents.create` | Create a new agent with workspace |
|
|
133
|
+
| `agents.files.list` | List workspace files for an agent |
|
|
134
|
+
| `agents.files.get` | Read a single workspace file |
|
|
135
|
+
| `agents.files.set` | Write a file to agent workspace |
|
|
136
|
+
| `tools.catalog` | List available tools |
|
|
137
|
+
| `skills.status` | List installed skills/plugins status |
|
|
138
|
+
| `skills.search` | Search ClawHub for skills (placeholder) |
|
|
139
|
+
| `skills.detail` | Get detail for a specific skill |
|
|
140
|
+
| `cron.list` | List configured cron jobs |
|
|
141
|
+
|
|
142
|
+
The connection features automatic reconnect with exponential backoff and heartbeat keepalive.
|
|
143
|
+
|
|
116
144
|
## Development
|
|
117
145
|
|
|
118
146
|
```bash
|
package/README.zh-CN.md
CHANGED
|
@@ -14,6 +14,9 @@ English documentation: [README.md](https://github.com/ZeyiY/openclaw-channel/blo
|
|
|
14
14
|
- 支持引用消息解析(用于入站上下文)
|
|
15
15
|
- 支持多账号并发(`channels.openim.accounts.<id>`)
|
|
16
16
|
- 支持群聊仅 @ 触发
|
|
17
|
+
- 私聊消息自动标记已读(已读回执)
|
|
18
|
+
- 每用户独立会话(私聊)/ 同群共享会话(群聊)
|
|
19
|
+
- Agent Portal Bridge — 与 agent-portal 云服务保持 WebSocket 长连接,支持远程管理
|
|
17
20
|
- 提供交互式配置命令:`openclaw openim setup`
|
|
18
21
|
|
|
19
22
|
## 安装
|
|
@@ -58,7 +61,9 @@ openclaw openim setup
|
|
|
58
61
|
"enabled": true,
|
|
59
62
|
"token": "your_token",
|
|
60
63
|
"wsAddr": "ws://127.0.0.1:10001",
|
|
61
|
-
"apiAddr": "http://127.0.0.1:10002"
|
|
64
|
+
"apiAddr": "http://127.0.0.1:10002",
|
|
65
|
+
"botId": "my-bot-001",
|
|
66
|
+
"portalWsAddr": "wss://portal.example.com/ws"
|
|
62
67
|
}
|
|
63
68
|
}
|
|
64
69
|
}
|
|
@@ -74,6 +79,8 @@ openclaw openim setup
|
|
|
74
79
|
- 给账号发单聊消息
|
|
75
80
|
- 在群里 @ 账号的消息
|
|
76
81
|
|
|
82
|
+
`botId` 和 `portalWsAddr` 为可选项。同时配置后,插件会与 agent-portal 云服务建立 WebSocket 连接,支持远程管理 agent、文件和模型。
|
|
83
|
+
|
|
77
84
|
支持单账号兜底写法(不使用 `accounts`)。
|
|
78
85
|
|
|
79
86
|
`default` 账号支持环境变量兜底:
|
|
@@ -112,6 +119,27 @@ openclaw openim setup
|
|
|
112
119
|
- `name`(可选):URL 输入时覆盖文件名
|
|
113
120
|
- `accountId`(可选):指定发送账号
|
|
114
121
|
|
|
122
|
+
## Agent Portal Bridge
|
|
123
|
+
|
|
124
|
+
配置 `botId` 和 `portalWsAddr` 后,插件会通过 WebSocket 连接到 agent-portal 云服务。Portal 可远程调用以下方法:
|
|
125
|
+
|
|
126
|
+
| 方法 | 说明 |
|
|
127
|
+
|---|---|
|
|
128
|
+
| `bot.agent.get` | 获取当前 bot 绑定的 agentId |
|
|
129
|
+
| `models.list` | 列出配置中的可用模型 |
|
|
130
|
+
| `agents.list` | 列出所有已配置的 agent |
|
|
131
|
+
| `agents.create` | 创建新 agent 及工作空间 |
|
|
132
|
+
| `agents.files.list` | 列出 agent 工作空间文件 |
|
|
133
|
+
| `agents.files.get` | 读取单个工作空间文件 |
|
|
134
|
+
| `agents.files.set` | 写入文件到 agent 工作空间 |
|
|
135
|
+
| `tools.catalog` | 列出可用工具 |
|
|
136
|
+
| `skills.status` | 列出已安装技能/插件状态 |
|
|
137
|
+
| `skills.search` | 搜索 ClawHub 技能(占位) |
|
|
138
|
+
| `skills.detail` | 获取特定技能详情 |
|
|
139
|
+
| `cron.list` | 列出已配置的定时任务 |
|
|
140
|
+
|
|
141
|
+
连接支持指数退避自动重连和心跳保活。
|
|
142
|
+
|
|
115
143
|
## 开发
|
|
116
144
|
|
|
117
145
|
```bash
|
package/dist/config.js
CHANGED
|
@@ -62,6 +62,7 @@ function envDefaultAccount() {
|
|
|
62
62
|
platformID,
|
|
63
63
|
enabled: true,
|
|
64
64
|
requireMention: true,
|
|
65
|
+
historyLimit: 20,
|
|
65
66
|
botId,
|
|
66
67
|
portalWsAddr,
|
|
67
68
|
};
|
|
@@ -89,6 +90,7 @@ function normalizeAccount(accountId, raw) {
|
|
|
89
90
|
const enabled = raw.enabled !== false;
|
|
90
91
|
const requireMention = raw.requireMention !== false;
|
|
91
92
|
const inboundWhitelist = normalizeInboundWhitelist(raw.inboundWhitelist);
|
|
93
|
+
const historyLimit = Math.max(1, Math.min(100, toFiniteNumber(raw.historyLimit, 20)));
|
|
92
94
|
const botId = String(raw.botId ?? "").trim() || undefined;
|
|
93
95
|
const portalWsAddr = String(raw.portalWsAddr ?? "").trim() || undefined;
|
|
94
96
|
if (!userID)
|
|
@@ -103,6 +105,7 @@ function normalizeAccount(accountId, raw) {
|
|
|
103
105
|
platformID,
|
|
104
106
|
requireMention,
|
|
105
107
|
inboundWhitelist,
|
|
108
|
+
historyLimit,
|
|
106
109
|
botId,
|
|
107
110
|
portalWsAddr,
|
|
108
111
|
};
|
package/dist/inbound.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import { type MessageItem } from "@openim/client-sdk";
|
|
2
|
-
import type { OpenIMClientState } from "./types";
|
|
2
|
+
import type { GroupInfoCacheEntry, GroupMemberCacheEntry, OpenIMClientState } from "./types";
|
|
3
|
+
export declare function fetchGroupInfo(client: OpenIMClientState, groupID: string, logger?: any, forceRefresh?: boolean): Promise<GroupInfoCacheEntry | null>;
|
|
4
|
+
export declare function fetchGroupMembers(client: OpenIMClientState, groupID: string, logger?: any, forceRefresh?: boolean): Promise<GroupMemberCacheEntry | null>;
|
|
3
5
|
export declare function processInboundMessage(api: any, client: OpenIMClientState, msg: MessageItem): Promise<void>;
|
package/dist/inbound.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SessionType } from "@openim/client-sdk";
|
|
1
|
+
import { GroupMemberFilter, SessionType } from "@openim/client-sdk";
|
|
2
2
|
import { appendFileSync } from "node:fs";
|
|
3
3
|
import { sendTextToTarget } from "./media";
|
|
4
4
|
import { formatSdkError } from "./utils";
|
|
@@ -14,6 +14,106 @@ const inboundDedup = new Map();
|
|
|
14
14
|
const INBOUND_DEDUP_TTL_MS = 5 * 60 * 1000;
|
|
15
15
|
const MAX_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
16
16
|
const IMAGE_FETCH_TIMEOUT_MS = 15000;
|
|
17
|
+
// ── Group context caches ──────────────────────────────────────────────
|
|
18
|
+
const groupInfoCache = new Map();
|
|
19
|
+
const groupMemberCache = new Map();
|
|
20
|
+
const groupHistories = new Map();
|
|
21
|
+
const GROUP_CACHE_TTL = 60 * 60 * 1000; // 1 hour
|
|
22
|
+
const DEFAULT_HISTORY_LIMIT = 20;
|
|
23
|
+
const DEFAULT_HISTORY_PROMPT_TEMPLATE = `[Group Chat History] Below are messages from others since your last reply (sender is user ID, body is message content):\n\`\`\`json\n{messages}\n\`\`\`\nPlease respond to the current @mention based on this context.\n\n`;
|
|
24
|
+
export async function fetchGroupInfo(client, groupID, logger, forceRefresh = false) {
|
|
25
|
+
const cached = groupInfoCache.get(groupID);
|
|
26
|
+
if (!forceRefresh && cached && Date.now() - cached.fetchedAt < GROUP_CACHE_TTL)
|
|
27
|
+
return cached;
|
|
28
|
+
try {
|
|
29
|
+
const res = await client.sdk.getSpecifiedGroupsInfo([groupID]);
|
|
30
|
+
const groups = res?.data;
|
|
31
|
+
if (!Array.isArray(groups) || groups.length === 0)
|
|
32
|
+
return cached ?? null;
|
|
33
|
+
const g = groups[0];
|
|
34
|
+
const entry = {
|
|
35
|
+
groupName: g.groupName ?? "",
|
|
36
|
+
notification: g.notification ?? "",
|
|
37
|
+
introduction: g.introduction ?? "",
|
|
38
|
+
memberCount: g.memberCount ?? 0,
|
|
39
|
+
fetchedAt: Date.now(),
|
|
40
|
+
};
|
|
41
|
+
groupInfoCache.set(groupID, entry);
|
|
42
|
+
logger?.info?.(`[openim] group info cached: ${groupID} name=${entry.groupName} members=${entry.memberCount}`);
|
|
43
|
+
return entry;
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
logger?.warn?.(`[openim] fetchGroupInfo failed for ${groupID}: ${formatSdkError(e)}`);
|
|
47
|
+
return cached ?? null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export async function fetchGroupMembers(client, groupID, logger, forceRefresh = false) {
|
|
51
|
+
const cached = groupMemberCache.get(groupID);
|
|
52
|
+
if (!forceRefresh && cached && Date.now() - cached.fetchedAt < GROUP_CACHE_TTL)
|
|
53
|
+
return cached;
|
|
54
|
+
try {
|
|
55
|
+
const uidToName = new Map();
|
|
56
|
+
let offset = 0;
|
|
57
|
+
const pageSize = 200;
|
|
58
|
+
// Paginate until we have all members
|
|
59
|
+
for (;;) {
|
|
60
|
+
const res = await client.sdk.getGroupMemberList({ groupID, filter: GroupMemberFilter.All, offset, count: pageSize });
|
|
61
|
+
const members = res?.data;
|
|
62
|
+
if (!Array.isArray(members) || members.length === 0)
|
|
63
|
+
break;
|
|
64
|
+
for (const m of members) {
|
|
65
|
+
if (m.userID) {
|
|
66
|
+
uidToName.set(m.userID, m.nickname || m.userID);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (members.length < pageSize)
|
|
70
|
+
break;
|
|
71
|
+
offset += pageSize;
|
|
72
|
+
}
|
|
73
|
+
if (uidToName.size === 0)
|
|
74
|
+
return cached ?? null;
|
|
75
|
+
const entry = { uidToName, fetchedAt: Date.now() };
|
|
76
|
+
groupMemberCache.set(groupID, entry);
|
|
77
|
+
logger?.info?.(`[openim] group members cached: ${groupID} count=${uidToName.size}`);
|
|
78
|
+
return entry;
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
logger?.warn?.(`[openim] fetchGroupMembers failed for ${groupID}: ${formatSdkError(e)}`);
|
|
82
|
+
return cached ?? null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function buildGroupSystemPrompt(info) {
|
|
86
|
+
const parts = [];
|
|
87
|
+
parts.push(`## 群聊上下文(Group Chat Context)\n`);
|
|
88
|
+
parts.push(`你当前在 OpenIM 群:\`${info.groupName}\`。`);
|
|
89
|
+
parts.push(`普通回复会自动发到该群,不需要额外操作。`);
|
|
90
|
+
parts.push(`只在需要对方回复你时才使用 @,仅仅提及某人时直接说名字即可。\n`);
|
|
91
|
+
if (info.notification) {
|
|
92
|
+
parts.push(`群公告:${info.notification}`);
|
|
93
|
+
}
|
|
94
|
+
if (info.introduction) {
|
|
95
|
+
parts.push(`群简介:${info.introduction}`);
|
|
96
|
+
}
|
|
97
|
+
return parts.join("\n");
|
|
98
|
+
}
|
|
99
|
+
function buildMemberListPrefix(uidToName) {
|
|
100
|
+
if (uidToName.size === 0)
|
|
101
|
+
return "";
|
|
102
|
+
const members = Array.from(uidToName.entries());
|
|
103
|
+
const memberLines = members
|
|
104
|
+
.map(([uid, name]) => ` ${name} (${uid})`)
|
|
105
|
+
.join("\n");
|
|
106
|
+
return `[Group Members]\n${memberLines}\n\nWhen mentioning a group member, use @userID (e.g. @${members[0][0]}). Do NOT use @displayName — only the numeric/alphanumeric userID works.\n\n`;
|
|
107
|
+
}
|
|
108
|
+
function buildHistoryPrefix(entries, uidToName) {
|
|
109
|
+
if (entries.length === 0)
|
|
110
|
+
return "";
|
|
111
|
+
const messagesJson = JSON.stringify(entries.map((e) => ({
|
|
112
|
+
sender: e.senderName ? `${e.senderName}(${e.sender})` : e.sender,
|
|
113
|
+
body: e.body,
|
|
114
|
+
})), null, 2);
|
|
115
|
+
return DEFAULT_HISTORY_PROMPT_TEMPLATE.replace("{messages}", messagesJson);
|
|
116
|
+
}
|
|
17
117
|
function normalizeImageMimeType(value) {
|
|
18
118
|
const mime = String(value ?? "").trim().toLowerCase();
|
|
19
119
|
return mime.startsWith("image/") ? mime : undefined;
|
|
@@ -340,9 +440,95 @@ export async function processInboundMessage(api, client, msg) {
|
|
|
340
440
|
return;
|
|
341
441
|
}
|
|
342
442
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
443
|
+
// ── Group context: fetch group info & members (cached) ────────────
|
|
444
|
+
let groupSystemPrompt;
|
|
445
|
+
let memberListPrefix = "";
|
|
446
|
+
let historyPrefix = "";
|
|
447
|
+
const historyLimit = client.config.historyLimit ?? DEFAULT_HISTORY_LIMIT;
|
|
448
|
+
if (group && msg.groupID) {
|
|
449
|
+
// Fetch group info → SystemPrompt (fire-and-forget-safe, uses cache)
|
|
450
|
+
const groupInfo = await fetchGroupInfo(client, msg.groupID, api.logger);
|
|
451
|
+
if (groupInfo) {
|
|
452
|
+
groupSystemPrompt = buildGroupSystemPrompt(groupInfo);
|
|
453
|
+
}
|
|
454
|
+
// Fetch group members → member list prefix (cached)
|
|
455
|
+
const memberData = await fetchGroupMembers(client, msg.groupID, api.logger);
|
|
456
|
+
if (memberData) {
|
|
457
|
+
memberListPrefix = buildMemberListPrefix(memberData.uidToName);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
// ── requireMention history gating ─────────────────────────────────
|
|
461
|
+
if (group && client.config.requireMention) {
|
|
462
|
+
if (!mentioned) {
|
|
463
|
+
// Not @mentioned → cache message for later history injection, don't trigger LLM
|
|
464
|
+
const groupID = String(msg.groupID);
|
|
465
|
+
if (!groupHistories.has(groupID)) {
|
|
466
|
+
groupHistories.set(groupID, []);
|
|
467
|
+
}
|
|
468
|
+
const entries = groupHistories.get(groupID);
|
|
469
|
+
entries.push({
|
|
470
|
+
sender: String(msg.sendID),
|
|
471
|
+
senderName: String(msg.senderNickname || ""),
|
|
472
|
+
body: inbound.body,
|
|
473
|
+
timestamp: msg.sendTime || Date.now(),
|
|
474
|
+
});
|
|
475
|
+
// Sliding window
|
|
476
|
+
while (entries.length > historyLimit) {
|
|
477
|
+
entries.shift();
|
|
478
|
+
}
|
|
479
|
+
api.logger?.info?.(`[openim] [HISTORY] cached non-mention msg | group=${groupID} | sender=${msg.sendID} | cached=${entries.length}`);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
// @mentioned → build history prefix from cached + API messages
|
|
483
|
+
const groupID = String(msg.groupID);
|
|
484
|
+
let entries = groupHistories.get(groupID) ?? [];
|
|
485
|
+
// If cache is insufficient, supplement from API
|
|
486
|
+
const cacheInsufficient = entries.length < Math.ceil(historyLimit / 2);
|
|
487
|
+
if (cacheInsufficient) {
|
|
488
|
+
api.logger?.info?.(`[openim] [MENTION] cache insufficient (${entries.length}/${historyLimit}), fetching from API...`);
|
|
489
|
+
try {
|
|
490
|
+
const conversationID = `sg_${msg.groupID}`;
|
|
491
|
+
const res = await client.sdk.getAdvancedHistoryMessageList({
|
|
492
|
+
conversationID,
|
|
493
|
+
startClientMsgID: "",
|
|
494
|
+
count: historyLimit,
|
|
495
|
+
});
|
|
496
|
+
const apiMessages = res?.data?.messageList;
|
|
497
|
+
if (Array.isArray(apiMessages) && apiMessages.length > 0) {
|
|
498
|
+
// Filter out empty/meaningless messages
|
|
499
|
+
const filtered = apiMessages
|
|
500
|
+
.filter((m) => {
|
|
501
|
+
const body = String(m.textElem?.content ?? m.atTextElem?.text ?? m.content ?? "").trim();
|
|
502
|
+
return !!body;
|
|
503
|
+
})
|
|
504
|
+
.slice(-historyLimit);
|
|
505
|
+
entries = filtered.map((m) => ({
|
|
506
|
+
sender: String(m.sendID),
|
|
507
|
+
senderName: String(m.senderNickname || ""),
|
|
508
|
+
body: String(m.textElem?.content ?? m.atTextElem?.text ?? m.content ?? "[消息]").trim(),
|
|
509
|
+
timestamp: m.sendTime || Date.now(),
|
|
510
|
+
}));
|
|
511
|
+
api.logger?.info?.(`[openim] [MENTION] fetched ${entries.length} history messages from API`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
catch (e) {
|
|
515
|
+
api.logger?.warn?.(`[openim] [MENTION] API history fetch failed: ${formatSdkError(e)}`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
// Take last N entries
|
|
519
|
+
if (entries.length > historyLimit) {
|
|
520
|
+
entries = entries.slice(-historyLimit);
|
|
521
|
+
groupHistories.set(groupID, entries);
|
|
522
|
+
}
|
|
523
|
+
const memberUidToName = groupMemberCache.get(groupID)?.uidToName ?? new Map();
|
|
524
|
+
historyPrefix = buildHistoryPrefix(entries, memberUidToName);
|
|
525
|
+
if (historyPrefix) {
|
|
526
|
+
api.logger?.info?.(`[openim] [MENTION] injected history context | ${entries.length} messages | ${historyPrefix.length} chars`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
else if (group && !client.config.requireMention && !mentioned) {
|
|
530
|
+
// requireMention=false: all messages trigger LLM, no history gating needed
|
|
531
|
+
// (no return, fall through to dispatch)
|
|
346
532
|
}
|
|
347
533
|
// 会话隔离:群聊按 groupID 分 session,私聊按发送者 ID 分 session
|
|
348
534
|
const baseSessionKey = group ? `openim:group:${msg.groupID}`.toLowerCase() : `openim:dm:${msg.sendID}`.toLowerCase();
|
|
@@ -367,7 +553,12 @@ export async function processInboundMessage(api, client, msg) {
|
|
|
367
553
|
const mediaResult = await materializeInboundMedia(inbound.media);
|
|
368
554
|
const warningText = mediaResult.warnings.map((warning) => `[Media fetch failed] ${warning}`).join("\n");
|
|
369
555
|
const rawBody = warningText ? `${inbound.body}\n${warningText}` : inbound.body;
|
|
370
|
-
|
|
556
|
+
// Envelope wraps only the raw message text (with sender prefix)
|
|
557
|
+
const envelopedBody = buildTextEnvelope(runtime, cfg, fromLabel, senderId, timestamp, rawBody, chatType);
|
|
558
|
+
// Prepend member list + history BEFORE the envelope so they appear cleanly above the sender line
|
|
559
|
+
const body = (memberListPrefix || historyPrefix)
|
|
560
|
+
? (memberListPrefix + historyPrefix + envelopedBody)
|
|
561
|
+
: envelopedBody;
|
|
371
562
|
if (mediaResult.warnings.length > 0) {
|
|
372
563
|
for (const warning of mediaResult.warnings) {
|
|
373
564
|
api.logger?.warn?.(`[openim] inbound media fetch failed: ${warning}`);
|
|
@@ -375,13 +566,14 @@ export async function processInboundMessage(api, client, msg) {
|
|
|
375
566
|
}
|
|
376
567
|
const ctxPayload = {
|
|
377
568
|
Body: body,
|
|
569
|
+
BodyForAgent: body,
|
|
378
570
|
RawBody: rawBody,
|
|
379
571
|
From: group ? `openim:group:${msg.groupID}` : `openim:${msg.sendID}`,
|
|
380
572
|
To: `openim:${client.config.userID}`,
|
|
381
573
|
SessionKey: sessionKey,
|
|
382
574
|
AccountId: client.config.accountId,
|
|
383
575
|
ChatType: chatType,
|
|
384
|
-
ConversationLabel: group ? `openim:g-${msg.groupID}` : `openim:${senderId}`,
|
|
576
|
+
ConversationLabel: group ? `openim:g-${msg.groupID}` : `openim:${senderId}`,
|
|
385
577
|
SenderName: fromLabel,
|
|
386
578
|
SenderId: senderId,
|
|
387
579
|
Provider: "openim",
|
|
@@ -391,6 +583,7 @@ export async function processInboundMessage(api, client, msg) {
|
|
|
391
583
|
OriginatingChannel: "openim",
|
|
392
584
|
OriginatingTo: `openim:${client.config.userID}`,
|
|
393
585
|
CommandAuthorized: true,
|
|
586
|
+
GroupSystemPrompt: groupSystemPrompt,
|
|
394
587
|
_openim: {
|
|
395
588
|
accountId: client.config.accountId,
|
|
396
589
|
isGroup: group,
|
package/dist/media.js
CHANGED
|
@@ -78,8 +78,8 @@ export async function sendTextToTarget(client, target, text) {
|
|
|
78
78
|
if (target.kind === "group") {
|
|
79
79
|
// 统一 <@ID> 和 @ID 两种格式,收集去重后的被 @ 用户 ID
|
|
80
80
|
const atIDs = new Set();
|
|
81
|
-
const normalizedText = text.replace(/<@(
|
|
82
|
-
for (const m of normalizedText.matchAll(/@(
|
|
81
|
+
const normalizedText = text.replace(/<@([a-zA-Z0-9_]{4,})>/g, (_m, id) => { atIDs.add(id); return `@${id}`; });
|
|
82
|
+
for (const m of normalizedText.matchAll(/@([a-zA-Z0-9_]{4,})/g))
|
|
83
83
|
atIDs.add(m[1]);
|
|
84
84
|
if (atIDs.size > 0) {
|
|
85
85
|
const atUserIDList = [...atIDs];
|