@chbo297/infoflow 2026.5.7 → 2026.5.9-beta.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/README.md +32 -17
- package/dist/src/actions.js +100 -10
- package/dist/src/agent-tools.js +102 -0
- package/dist/src/bot.js +182 -15
- package/dist/src/channel.js +13 -1
- package/dist/src/inbound-context.js +61 -0
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -43,22 +43,30 @@ BAIDU_NPM_REGISTRY=http://registry.npm.baidu-int.com bash scripts/deploy.sh
|
|
|
43
43
|
|
|
44
44
|
方式 A:通过独立 tools 包安装并部署(推荐,支持 `update` 子命令)
|
|
45
45
|
|
|
46
|
+
<!-- sync:infoflow-plugin-version -->
|
|
46
47
|
```bash
|
|
47
48
|
# 正式版(latest)
|
|
48
|
-
npx -y @chbo297/infoflow-openclaw-tools update --version 2026.5.
|
|
49
|
+
npx -y @chbo297/infoflow-openclaw-tools update --version 2026.5.9-beta.1 --registry https://registry.npmjs.org
|
|
50
|
+
```
|
|
51
|
+
<!-- /sync:infoflow-plugin-version -->
|
|
49
52
|
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
```bash
|
|
54
|
+
# Beta 版(示例,版本号请按实际预发包替换)
|
|
55
|
+
npx -y @chbo297/infoflow-openclaw-tools@beta update --version 2026.5.8-beta.1 --registry https://registry.npmjs.org
|
|
52
56
|
```
|
|
53
57
|
|
|
54
58
|
方式 B:通过 OpenClaw 插件命令安装
|
|
55
59
|
|
|
60
|
+
<!-- sync:infoflow-plugin-version -->
|
|
56
61
|
```bash
|
|
57
62
|
# 正式版
|
|
58
|
-
openclaw plugins install @chbo297/infoflow@2026.5.
|
|
63
|
+
openclaw plugins install @chbo297/infoflow@2026.5.9-beta.1
|
|
64
|
+
```
|
|
65
|
+
<!-- /sync:infoflow-plugin-version -->
|
|
59
66
|
|
|
60
|
-
|
|
61
|
-
|
|
67
|
+
```bash
|
|
68
|
+
# Beta 版(示例,版本号请按实际预发包替换)
|
|
69
|
+
openclaw plugins install @chbo297/infoflow@2026.5.8-beta.1
|
|
62
70
|
```
|
|
63
71
|
|
|
64
72
|
安装后建议检查插件状态:
|
|
@@ -72,9 +80,11 @@ openclaw plugins inspect infoflow
|
|
|
72
80
|
|
|
73
81
|
发布到 npm 后,可直接通过独立 tools 包执行安装/升级:
|
|
74
82
|
|
|
83
|
+
<!-- sync:infoflow-plugin-version -->
|
|
75
84
|
```bash
|
|
76
|
-
npx -y @chbo297/infoflow-openclaw-tools update --version 2026.5.
|
|
85
|
+
npx -y @chbo297/infoflow-openclaw-tools update --version 2026.5.9-beta.1 --registry https://registry.npmjs.org
|
|
77
86
|
```
|
|
87
|
+
<!-- /sync:infoflow-plugin-version -->
|
|
78
88
|
|
|
79
89
|
常用参数:
|
|
80
90
|
|
|
@@ -97,29 +107,34 @@ npx -y @chbo297/infoflow-openclaw-tools update --version 2026.5.6 --registry htt
|
|
|
97
107
|
|
|
98
108
|
### 版本升级、打 tag、推送与 npm 发布流程
|
|
99
109
|
|
|
100
|
-
|
|
110
|
+
每次发布新版本时,先将 `package.json` 的 `version` 设为待发版本号,再按下述顺序执行。上文「首次安装 / npx 更新」与下文发版流程中,各 bash 代码块外侧有一对用于自动替换的 HTML 注释标记;发版前请执行 **`npm run sync-readme-install-version`**,脚本会按当前 `package.json` 的 `version` 更新这些块内的版本号,以免 README 与 npm 不一致。
|
|
101
111
|
|
|
112
|
+
<!-- sync:infoflow-plugin-version -->
|
|
102
113
|
```bash
|
|
103
114
|
# 1) 修改版本号(会同步 package-lock.json)
|
|
104
|
-
npm version 2026.5.
|
|
115
|
+
npm version 2026.5.9-beta.1 --no-git-tag-version
|
|
116
|
+
|
|
117
|
+
# 2) 同步 README 中标记块内的推荐安装命令与下文中的 tag / commit 示例版本号
|
|
118
|
+
npm run sync-readme-install-version
|
|
105
119
|
|
|
106
|
-
#
|
|
120
|
+
# 3) 发布前校验
|
|
107
121
|
npm run typecheck
|
|
108
122
|
npm run test
|
|
109
123
|
npm run build
|
|
110
124
|
|
|
111
|
-
#
|
|
112
|
-
git add package.json package-lock.json README.md scripts src
|
|
113
|
-
git commit -m "2026.5.
|
|
125
|
+
# 4) 提交版本变更(含 README、CHANGELOG 等)
|
|
126
|
+
git add package.json package-lock.json README.md CHANGELOG.md scripts src
|
|
127
|
+
git commit -m "2026.5.9-beta.1"
|
|
114
128
|
|
|
115
|
-
#
|
|
116
|
-
git tag 2026.5.
|
|
129
|
+
# 5) 打 tag 并推送代码与 tag
|
|
130
|
+
git tag 2026.5.9-beta.1
|
|
117
131
|
git push origin main
|
|
118
|
-
git push origin 2026.5.
|
|
132
|
+
git push origin 2026.5.9-beta.1
|
|
119
133
|
|
|
120
|
-
#
|
|
134
|
+
# 6) 发布 npm(可按需指定 registry)
|
|
121
135
|
npm publish
|
|
122
136
|
# 或
|
|
123
137
|
# npm publish --registry https://registry.npmjs.org
|
|
124
138
|
```
|
|
139
|
+
<!-- /sync:infoflow-plugin-version -->
|
|
125
140
|
|
package/dist/src/actions.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { jsonResult, readStringParam } from "openclaw/plugin-sdk/core";
|
|
7
7
|
import { extractToolSend } from "openclaw/plugin-sdk/tool-send";
|
|
8
8
|
import { resolveInfoflowAccount } from "./accounts.js";
|
|
9
|
+
import { lookupInboundContext } from "./inbound-context.js";
|
|
9
10
|
import { logVerbose } from "./logging.js";
|
|
10
11
|
import { prepareInfoflowImageBase64, sendInfoflowImageMessage } from "./media.js";
|
|
11
12
|
import { sendInfoflowMessage, recallInfoflowGroupMessage, recallInfoflowPrivateMessage, } from "./send.js";
|
|
@@ -15,12 +16,52 @@ import { normalizeInfoflowTarget } from "./targets.js";
|
|
|
15
16
|
const RECALL_OK_HINT = "Recall succeeded. output only NO_REPLY with no other text.";
|
|
16
17
|
const RECALL_FAIL_HINT = "Recall failed. Send a brief reply stating only the failure reason.";
|
|
17
18
|
const RECALL_PARTIAL_HINT = "Some recalls failed. Send a brief reply stating only the failure reason(s).";
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the inbound replyToMessageId from the action ctx + inbound-context map.
|
|
21
|
+
* Returns the bot-sent messageId the user is quote-replying to (if any), so we
|
|
22
|
+
* can recover when the LLM accidentally passes the inbound user-message id as
|
|
23
|
+
* the delete target.
|
|
24
|
+
*/
|
|
25
|
+
function resolveInboundReplyToMessageId(params) {
|
|
26
|
+
const currentMessageId = params.currentMessageId != null ? String(params.currentMessageId) : undefined;
|
|
27
|
+
if (!currentMessageId)
|
|
28
|
+
return undefined;
|
|
29
|
+
const ctx = lookupInboundContext(currentMessageId);
|
|
30
|
+
if (!ctx)
|
|
31
|
+
return undefined;
|
|
32
|
+
// Scope match: same account + target (avoid using a stale context from another chat).
|
|
33
|
+
if (ctx.accountId !== params.accountId)
|
|
34
|
+
return undefined;
|
|
35
|
+
if (ctx.target !== params.target)
|
|
36
|
+
return undefined;
|
|
37
|
+
return ctx.replyToMessageId;
|
|
38
|
+
}
|
|
39
|
+
/** Format up to N recent sent messages for an error-path hint to the LLM. */
|
|
40
|
+
function formatRecentCandidatesForError(records, limit = 5) {
|
|
41
|
+
if (!records || !Array.isArray(records) || records.length === 0)
|
|
42
|
+
return "";
|
|
43
|
+
const lines = records.slice(0, limit).map((r) => {
|
|
44
|
+
const previewText = r.digest || "(no preview)";
|
|
45
|
+
return `messageId=${r.messageid} preview="${previewText}"`;
|
|
46
|
+
});
|
|
47
|
+
return lines.join("; ");
|
|
48
|
+
}
|
|
49
|
+
/** Safe candidate lookup that never throws (errors → empty string). */
|
|
50
|
+
function safeRecentCandidates(accountId, target) {
|
|
51
|
+
try {
|
|
52
|
+
const records = querySentMessages(accountId, { target, count: 5 });
|
|
53
|
+
return formatRecentCandidatesForError(records);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
18
59
|
export const infoflowMessageActions = {
|
|
19
60
|
describeMessageTool: () => ({
|
|
20
61
|
actions: ["send", "delete"],
|
|
21
62
|
}),
|
|
22
63
|
extractToolSend: ({ args }) => extractToolSend(args, "sendMessage"),
|
|
23
|
-
handleAction: async ({ action, params, cfg, accountId }) => {
|
|
64
|
+
handleAction: async ({ action, params, cfg, accountId, toolContext }) => {
|
|
24
65
|
// -----------------------------------------------------------------------
|
|
25
66
|
// delete (群消息撤回) — Mode A: by messageId, Mode B: by count
|
|
26
67
|
// -----------------------------------------------------------------------
|
|
@@ -46,25 +87,49 @@ export const infoflowMessageActions = {
|
|
|
46
87
|
const groupId = Number(groupMatch[1]);
|
|
47
88
|
// Mode A: single message recall by messageId
|
|
48
89
|
if (messageId) {
|
|
90
|
+
// Resolve msgseqid (group recall requires it). If the LLM-passed messageId
|
|
91
|
+
// is unknown to the store, fall back to the inbound replyToMessageId — the
|
|
92
|
+
// common failure mode is the LLM passing the inbound user-message id as
|
|
93
|
+
// the delete target instead of the bot-message id it's quote-replying to.
|
|
94
|
+
let effectiveMessageId = messageId;
|
|
49
95
|
let msgseqid = readStringParam(params, "msgseqid") ?? "";
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
96
|
+
let stored = findSentMessage(account.accountId, effectiveMessageId);
|
|
97
|
+
if (!stored && !msgseqid) {
|
|
98
|
+
const fallbackId = resolveInboundReplyToMessageId({
|
|
99
|
+
accountId: account.accountId,
|
|
100
|
+
target: `group:${groupId}`,
|
|
101
|
+
currentMessageId: toolContext?.currentMessageId,
|
|
102
|
+
});
|
|
103
|
+
if (fallbackId && fallbackId !== effectiveMessageId) {
|
|
104
|
+
const fallbackStored = findSentMessage(account.accountId, fallbackId);
|
|
105
|
+
if (fallbackStored) {
|
|
106
|
+
logVerbose(`[infoflow:delete] LLM passed unknown messageId=${effectiveMessageId}, falling back to replyToMessageId=${fallbackId}`);
|
|
107
|
+
effectiveMessageId = fallbackId;
|
|
108
|
+
stored = fallbackStored;
|
|
109
|
+
}
|
|
54
110
|
}
|
|
55
111
|
}
|
|
112
|
+
if (!msgseqid && stored?.msgseqid) {
|
|
113
|
+
msgseqid = stored.msgseqid;
|
|
114
|
+
}
|
|
56
115
|
if (!msgseqid) {
|
|
57
|
-
|
|
116
|
+
const candidates = safeRecentCandidates(account.accountId, `group:${groupId}`);
|
|
117
|
+
logVerbose(`[infoflow:delete] unknown messageId=${effectiveMessageId}, no fallback available, returning candidates to LLM`);
|
|
118
|
+
throw new Error(`delete: messageId=${effectiveMessageId} is not a known bot-sent message in this chat (msgseqid not found in store). ` +
|
|
119
|
+
`It looks like you may have passed the inbound (user) message id instead of the bot's. ` +
|
|
120
|
+
(candidates
|
|
121
|
+
? `Recent bot-sent messages here: ${candidates}. Pick the right messageId and retry.`
|
|
122
|
+
: `No recent bot-sent messages on file for this chat. Aborting to avoid wrong recall.`));
|
|
58
123
|
}
|
|
59
124
|
const result = await recallInfoflowGroupMessage({
|
|
60
125
|
account,
|
|
61
126
|
groupId,
|
|
62
|
-
messageid:
|
|
127
|
+
messageid: effectiveMessageId,
|
|
63
128
|
msgseqid,
|
|
64
129
|
});
|
|
65
130
|
if (result.ok) {
|
|
66
131
|
try {
|
|
67
|
-
removeRecalledMessages(account.accountId, [
|
|
132
|
+
removeRecalledMessages(account.accountId, [effectiveMessageId]);
|
|
68
133
|
}
|
|
69
134
|
catch {
|
|
70
135
|
// ignore cleanup errors
|
|
@@ -74,6 +139,7 @@ export const infoflowMessageActions = {
|
|
|
74
139
|
ok: result.ok,
|
|
75
140
|
channel: "infoflow",
|
|
76
141
|
to,
|
|
142
|
+
messageId: effectiveMessageId,
|
|
77
143
|
...(result.error ? { error: result.error } : {}),
|
|
78
144
|
_hint: result.ok ? RECALL_OK_HINT : RECALL_FAIL_HINT,
|
|
79
145
|
});
|
|
@@ -157,14 +223,37 @@ export const infoflowMessageActions = {
|
|
|
157
223
|
}
|
|
158
224
|
// Mode A: single message recall by messageId (msgkey)
|
|
159
225
|
if (messageId) {
|
|
226
|
+
// Attempt the inbound-context fallback when the LLM-passed messageId is
|
|
227
|
+
// unknown to the store. If we can swap it for a verified bot-message id
|
|
228
|
+
// from the inbound replyTo, do so. Otherwise PRESERVE the original
|
|
229
|
+
// permissive behavior (pass the LLM id straight to the API and let
|
|
230
|
+
// Infoflow's backend judge) — the store may legitimately not contain
|
|
231
|
+
// every recallable DM message (e.g., after the 7-day retention sweep
|
|
232
|
+
// or for messages sent before this plugin started recording).
|
|
233
|
+
let effectiveMessageId = messageId;
|
|
234
|
+
const stored = findSentMessage(account.accountId, effectiveMessageId);
|
|
235
|
+
if (!stored) {
|
|
236
|
+
const fallbackId = resolveInboundReplyToMessageId({
|
|
237
|
+
accountId: account.accountId,
|
|
238
|
+
target,
|
|
239
|
+
currentMessageId: toolContext?.currentMessageId,
|
|
240
|
+
});
|
|
241
|
+
if (fallbackId && fallbackId !== effectiveMessageId) {
|
|
242
|
+
const fallbackStored = findSentMessage(account.accountId, fallbackId);
|
|
243
|
+
if (fallbackStored) {
|
|
244
|
+
logVerbose(`[infoflow:delete] LLM passed unknown messageId=${effectiveMessageId}, falling back to replyToMessageId=${fallbackId}`);
|
|
245
|
+
effectiveMessageId = fallbackId;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
160
249
|
const result = await recallInfoflowPrivateMessage({
|
|
161
250
|
account,
|
|
162
|
-
msgkey:
|
|
251
|
+
msgkey: effectiveMessageId,
|
|
163
252
|
appAgentId,
|
|
164
253
|
});
|
|
165
254
|
if (result.ok) {
|
|
166
255
|
try {
|
|
167
|
-
removeRecalledMessages(account.accountId, [
|
|
256
|
+
removeRecalledMessages(account.accountId, [effectiveMessageId]);
|
|
168
257
|
}
|
|
169
258
|
catch {
|
|
170
259
|
// ignore cleanup errors
|
|
@@ -174,6 +263,7 @@ export const infoflowMessageActions = {
|
|
|
174
263
|
ok: result.ok,
|
|
175
264
|
channel: "infoflow",
|
|
176
265
|
to,
|
|
266
|
+
messageId: effectiveMessageId,
|
|
177
267
|
...(result.error ? { error: result.error } : {}),
|
|
178
268
|
_hint: result.ok ? RECALL_OK_HINT : RECALL_FAIL_HINT,
|
|
179
269
|
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM-callable tools exposed by the Infoflow channel plugin.
|
|
3
|
+
*
|
|
4
|
+
* Currently exposes: `infoflow_list_sent_messages` — lets the LLM look up
|
|
5
|
+
* bot-sent messages by target / time window / content substring when the
|
|
6
|
+
* push-injected recent list (in bot.ts) isn't enough (e.g., older messages,
|
|
7
|
+
* search-by-content).
|
|
8
|
+
*/
|
|
9
|
+
import { jsonResult } from "openclaw/plugin-sdk/core";
|
|
10
|
+
import { Type } from "typebox";
|
|
11
|
+
import { resolveDefaultInfoflowAccountId } from "./accounts.js";
|
|
12
|
+
import { logVerbose } from "./logging.js";
|
|
13
|
+
import { querySentMessages } from "./sent-message-store.js";
|
|
14
|
+
const listSentMessagesSchema = Type.Object({
|
|
15
|
+
target: Type.String({
|
|
16
|
+
description: "Chat target to query. Format: 'group:<groupId>' for groups or '<username>' for private chats. " +
|
|
17
|
+
"MUST be the current chat target — do not query other chats.",
|
|
18
|
+
}),
|
|
19
|
+
count: Type.Optional(Type.Integer({
|
|
20
|
+
minimum: 1,
|
|
21
|
+
maximum: 50,
|
|
22
|
+
default: 20,
|
|
23
|
+
description: "Maximum number of messages to return, newest first.",
|
|
24
|
+
})),
|
|
25
|
+
withinHours: Type.Optional(Type.Integer({
|
|
26
|
+
minimum: 1,
|
|
27
|
+
maximum: 168,
|
|
28
|
+
description: "Only include messages sent within the last N hours (1-168, i.e. up to 7 days, which matches the store's retention). Omit for no time filter.",
|
|
29
|
+
})),
|
|
30
|
+
containsText: Type.Optional(Type.String({
|
|
31
|
+
description: "Case-insensitive substring filter against the message digest (first ~100 chars of body). Use this to find a message by content, e.g. containsText='会议改到3点'.",
|
|
32
|
+
})),
|
|
33
|
+
accountId: Type.Optional(Type.String({
|
|
34
|
+
description: "Account id to query against (only needed when multiple Infoflow accounts are configured). Defaults to the configured default account.",
|
|
35
|
+
})),
|
|
36
|
+
});
|
|
37
|
+
const TOOL_DESCRIPTION = [
|
|
38
|
+
"List messages the bot previously sent to a given Infoflow chat, with optional time-window and content-substring filters.",
|
|
39
|
+
"Use this BEFORE action='delete' when:",
|
|
40
|
+
" (a) the message you need to recall is older than the recent list already injected into the message body, or",
|
|
41
|
+
" (b) you need to find a bot-sent message by its content (e.g. 'the joke about programmers', '会议通知').",
|
|
42
|
+
"Returns: target, count, and an array of { messageId, sentAt, ageMinutes, preview }.",
|
|
43
|
+
"Feed the chosen messageId back into action='delete' to recall it.",
|
|
44
|
+
].join("\n");
|
|
45
|
+
export function createListSentMessagesTool(deps) {
|
|
46
|
+
return {
|
|
47
|
+
name: "infoflow_list_sent_messages",
|
|
48
|
+
label: "infoflow_list_sent_messages",
|
|
49
|
+
description: TOOL_DESCRIPTION,
|
|
50
|
+
parameters: listSentMessagesSchema,
|
|
51
|
+
execute: async (_toolCallId, rawParams) => {
|
|
52
|
+
const p = (rawParams ?? {});
|
|
53
|
+
if (typeof p.target !== "string" || !p.target.trim()) {
|
|
54
|
+
throw new Error("infoflow_list_sent_messages: 'target' is required.");
|
|
55
|
+
}
|
|
56
|
+
const target = p.target.trim();
|
|
57
|
+
const limit = Math.min(Math.max(p.count ?? 20, 1), 50);
|
|
58
|
+
const hasContains = typeof p.containsText === "string" && p.containsText.trim().length > 0;
|
|
59
|
+
const needle = hasContains ? p.containsText.trim().toLowerCase() : undefined;
|
|
60
|
+
let accountId = p.accountId?.trim();
|
|
61
|
+
if (!accountId) {
|
|
62
|
+
try {
|
|
63
|
+
accountId = resolveDefaultInfoflowAccountId(deps.getConfig());
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
accountId = undefined;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (!accountId) {
|
|
70
|
+
throw new Error("infoflow_list_sent_messages: cannot resolve account id. Pass accountId explicitly or configure a default account.");
|
|
71
|
+
}
|
|
72
|
+
// Over-fetch when filtering by content so the post-filter slice still has up to `limit` rows.
|
|
73
|
+
const fetchCount = needle ? Math.max(limit * 4, 50) : limit;
|
|
74
|
+
let records;
|
|
75
|
+
try {
|
|
76
|
+
records = querySentMessages(accountId, { target, count: fetchCount });
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
throw new Error(`infoflow_list_sent_messages: store query failed: ${err?.message ?? String(err)}`);
|
|
80
|
+
}
|
|
81
|
+
if (p.withinHours) {
|
|
82
|
+
const cutoff = Date.now() - p.withinHours * 60 * 60 * 1000;
|
|
83
|
+
records = records.filter((r) => r.sentAt >= cutoff);
|
|
84
|
+
}
|
|
85
|
+
if (needle) {
|
|
86
|
+
records = records.filter((r) => (r.digest ?? "").toLowerCase().includes(needle));
|
|
87
|
+
}
|
|
88
|
+
const sliced = records.slice(0, limit);
|
|
89
|
+
logVerbose(`[infoflow:tool:list_sent_messages] target=${target} accountId=${accountId} count=${sliced.length} (limit=${limit}, withinHours=${p.withinHours ?? "none"}, containsText=${hasContains ? "yes" : "no"})`);
|
|
90
|
+
return jsonResult({
|
|
91
|
+
target,
|
|
92
|
+
count: sliced.length,
|
|
93
|
+
messages: sliced.map((r) => ({
|
|
94
|
+
messageId: r.messageid,
|
|
95
|
+
sentAt: new Date(r.sentAt).toISOString(),
|
|
96
|
+
ageMinutes: Math.max(0, Math.round((Date.now() - r.sentAt) / 60000)),
|
|
97
|
+
preview: r.digest || "",
|
|
98
|
+
})),
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
package/dist/src/bot.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { buildAgentMediaPayload, getAgentScopedMediaLocalRoots, } from "openclaw/plugin-sdk/agent-media-payload";
|
|
2
2
|
import { buildPendingHistoryContextFromMap, clearHistoryEntriesIfEnabled, DEFAULT_GROUP_HISTORY_LIMIT, recordPendingHistoryEntryIfEnabled, } from "openclaw/plugin-sdk/reply-history";
|
|
3
3
|
import { resolveInfoflowAccount } from "./accounts.js";
|
|
4
|
+
import { registerInboundContext } from "./inbound-context.js";
|
|
4
5
|
import { getInfoflowBotLog, formatInfoflowError, logVerbose } from "./logging.js";
|
|
5
6
|
import { createInfoflowReplyDispatcher } from "./reply-dispatcher.js";
|
|
6
7
|
import { getInfoflowRuntime } from "./runtime.js";
|
|
7
|
-
import { findSentMessage } from "./sent-message-store.js";
|
|
8
|
+
import { findSentMessage, querySentMessages } from "./sent-message-store.js";
|
|
8
9
|
/**
|
|
9
10
|
* Check if the bot was @mentioned in the message body.
|
|
10
11
|
* Matches appAgentId, robotName, or robotId against AT items (same order as Baidu reference plugin).
|
|
@@ -183,14 +184,18 @@ function resolveFollowUpOtherMentioned(params) {
|
|
|
183
184
|
logVerbose(`[infoflow:bot] skip dispatch: from=${fromuser}, group=${groupId}, reason=followUp-other-mentioned (record only, no LLM)`);
|
|
184
185
|
return "record_only";
|
|
185
186
|
}
|
|
186
|
-
// ---------------------------------------------------------------------------
|
|
187
|
-
// Reply-to-bot detection (引用回复机器人消息)
|
|
188
|
-
// ---------------------------------------------------------------------------
|
|
189
187
|
/**
|
|
190
|
-
*
|
|
191
|
-
*
|
|
188
|
+
* Resolve all replyData targets from inbound body items, including each target's
|
|
189
|
+
* messageid + body preview + isBotMessage (via sent-message-store lookup).
|
|
190
|
+
*
|
|
191
|
+
* This is the structured form behind `checkReplyToBot`: callers who only want a
|
|
192
|
+
* boolean can compute it as `targets.some((t) => t.isBotMessage)`. Returning the
|
|
193
|
+
* full list lets us surface the messageid to the LLM and resolve the right
|
|
194
|
+
* recall target — the previous bool-only API was the root cause of the LLM
|
|
195
|
+
* passing the inbound (user) messageId to action=delete.
|
|
192
196
|
*/
|
|
193
|
-
function
|
|
197
|
+
function resolveReplyTargets(bodyItems, accountId) {
|
|
198
|
+
const out = [];
|
|
194
199
|
for (const item of bodyItems) {
|
|
195
200
|
if (item.type !== "replyData")
|
|
196
201
|
continue;
|
|
@@ -200,16 +205,91 @@ function checkReplyToBot(bodyItems, accountId) {
|
|
|
200
205
|
const msgIdStr = String(msgId);
|
|
201
206
|
if (!msgIdStr)
|
|
202
207
|
continue;
|
|
208
|
+
let isBotMessage = false;
|
|
203
209
|
try {
|
|
204
|
-
|
|
205
|
-
if (found)
|
|
206
|
-
return true;
|
|
210
|
+
isBotMessage = Boolean(findSentMessage(accountId, msgIdStr));
|
|
207
211
|
}
|
|
208
212
|
catch {
|
|
209
213
|
// DB lookup failure should not block message processing
|
|
210
214
|
}
|
|
215
|
+
out.push({
|
|
216
|
+
messageid: msgIdStr,
|
|
217
|
+
preview: (item.content ?? "").trim(),
|
|
218
|
+
isBotMessage,
|
|
219
|
+
});
|
|
211
220
|
}
|
|
212
|
-
return
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// Sent-message context injection (push) — solves both the "AI uses inbound id
|
|
225
|
+
// as delete target" bug and the "DM-triggered cross-context send is invisible
|
|
226
|
+
// in the target group" bug. sent-messages.db tracks all bot-sent messages
|
|
227
|
+
// keyed by target, so a single push surfaces messages sent from any session.
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
/** Detect inbound text that semantically asks the bot to recall/delete messages. */
|
|
230
|
+
const RECALL_INTENT_REGEX = /(撤回|收回|删[掉了除]|取消|清除|recall|unsend|undo\s*send|delete\s+(?:that|those|the\s+(?:last|previous(?:\s+\d+)?)))/i;
|
|
231
|
+
function looksLikeRecallIntent(text) {
|
|
232
|
+
if (!text)
|
|
233
|
+
return false;
|
|
234
|
+
return RECALL_INTENT_REGEX.test(text);
|
|
235
|
+
}
|
|
236
|
+
const RECENT_BOT_AMBIENT_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
237
|
+
const RECENT_BOT_AMBIENT_COUNT = 5;
|
|
238
|
+
const RECENT_BOT_DETAIL_COUNT = 10;
|
|
239
|
+
/**
|
|
240
|
+
* Build a system-style section listing the bot's recent messages to this chat.
|
|
241
|
+
* Two modes — ambient (compact, awareness only) and detail (longer, with
|
|
242
|
+
* explicit instruction for recall). Returns undefined when there's nothing
|
|
243
|
+
* recent to report so unrelated chats stay token-cheap.
|
|
244
|
+
*/
|
|
245
|
+
function buildBotRecentMessagesSection(params) {
|
|
246
|
+
const detail = params.inboundLooksLikeRecall || params.isReplyToBot;
|
|
247
|
+
const count = detail ? RECENT_BOT_DETAIL_COUNT : RECENT_BOT_AMBIENT_COUNT;
|
|
248
|
+
let records;
|
|
249
|
+
try {
|
|
250
|
+
records = querySentMessages(params.accountId, { target: params.target, count });
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
if (records.length === 0)
|
|
256
|
+
return undefined;
|
|
257
|
+
const cutoff = Date.now() - RECENT_BOT_AMBIENT_WINDOW_MS;
|
|
258
|
+
const recent = records.filter((r) => r.sentAt >= cutoff);
|
|
259
|
+
if (recent.length === 0)
|
|
260
|
+
return undefined;
|
|
261
|
+
const header = detail
|
|
262
|
+
? "[System: Recent messages you (the bot) sent to this chat (newest first). Use these messageIds when recalling/deleting your own messages. NEVER pass the current inbound message_id as the delete target — that is the USER's message, not a bot message.]"
|
|
263
|
+
: "[System: Your recent messages to this chat (for awareness — these may have been sent from another session/context):]";
|
|
264
|
+
const lines = recent.map((r, i) => {
|
|
265
|
+
const ageMin = Math.max(0, Math.round((Date.now() - r.sentAt) / 60000));
|
|
266
|
+
const previewText = r.digest || "(no preview)";
|
|
267
|
+
return ` ${i + 1}. messageId=${r.messageid} sent=${ageMin}m ago preview="${previewText}"`;
|
|
268
|
+
});
|
|
269
|
+
return {
|
|
270
|
+
text: [header, ...lines].join("\n"),
|
|
271
|
+
mode: detail ? "detail" : "ambient",
|
|
272
|
+
count: recent.length,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Build a section describing each quoted-reply target on the inbound message,
|
|
277
|
+
* with messageid + isBotMessage. Only emitted when there's at least one target.
|
|
278
|
+
* This is the missing piece for the original bug — the LLM needs to know which
|
|
279
|
+
* id belongs to the quoted bot message, distinct from the inbound message's id.
|
|
280
|
+
*/
|
|
281
|
+
function formatQuotedReplyTargetsSection(targets) {
|
|
282
|
+
if (!targets.length)
|
|
283
|
+
return undefined;
|
|
284
|
+
const lines = targets.map((t, i) => {
|
|
285
|
+
const previewText = t.preview ? t.preview.slice(0, 200) : "(no preview)";
|
|
286
|
+
return ` ${i + 1}. messageId=${t.messageid} sentByBot=${t.isBotMessage} preview="${previewText}"`;
|
|
287
|
+
});
|
|
288
|
+
return [
|
|
289
|
+
"[System: This message is a quoted reply to:",
|
|
290
|
+
...lines,
|
|
291
|
+
"If the user is asking to act on (recall/edit/quote) a referenced message and sentByBot=true, use that messageId as the action target.]",
|
|
292
|
+
].join("\n");
|
|
213
293
|
}
|
|
214
294
|
/** Shared judgment rules and reply format requirements for all conditional-reply prompts */
|
|
215
295
|
function buildReplyJudgmentRules(options) {
|
|
@@ -309,6 +389,21 @@ function buildProactivePrompt() {
|
|
|
309
389
|
buildReplyJudgmentRules(),
|
|
310
390
|
].join("\n");
|
|
311
391
|
}
|
|
392
|
+
/**
|
|
393
|
+
* Appended last to every group-chat GroupSystemPrompt. Keeps the visible group reply
|
|
394
|
+
* conclusion-oriented: no raw tool transcripts or retrieval dumps; multi-step work
|
|
395
|
+
* stays in subagent (or equivalent) with only a synthesized answer to the group.
|
|
396
|
+
*/
|
|
397
|
+
function buildGroupOutputHygienePrompt() {
|
|
398
|
+
return [
|
|
399
|
+
"# Group chat output (hard constraint)",
|
|
400
|
+
"",
|
|
401
|
+
"- Reply to the group with **only the final user-facing answer** in one concise message (or a few short messages if the channel requires splitting).",
|
|
402
|
+
"- **Do not** include tool-call traces, raw intermediate search/retrieval payloads, or long scratchpad-style reasoning in the group-visible text.",
|
|
403
|
+
"- If the task needs exploration across multiple steps, do that work in a **subagent** (or equivalent isolated context) and return **only** the merged conclusion to the group.",
|
|
404
|
+
"- If the user explicitly asks for your reasoning or steps, satisfy that with a **brief** numbered or bulleted summary **inside the same** conclusion-style reply — still **not** raw tool logs or full intermediate dumps.",
|
|
405
|
+
].join("\n");
|
|
406
|
+
}
|
|
312
407
|
// ---------------------------------------------------------------------------
|
|
313
408
|
// Group reply tracking (in-memory) for follow-up window
|
|
314
409
|
// ---------------------------------------------------------------------------
|
|
@@ -550,8 +645,13 @@ export async function handleGroupChatMessage(params) {
|
|
|
550
645
|
const bodyForAgent = agentVisibleText.trim() || rawMes || mes;
|
|
551
646
|
// Extract sender name from header or fallback to fromuser
|
|
552
647
|
const senderName = String(header?.username ?? header?.nickname ?? msgData.username ?? fromuser);
|
|
553
|
-
//
|
|
554
|
-
|
|
648
|
+
// Resolve all replyData targets (id + preview + isBotMessage). We need the
|
|
649
|
+
// structured form (not just a boolean) so we can surface the bot-message
|
|
650
|
+
// messageId to the LLM for correct recall.
|
|
651
|
+
const replyTargets = Array.isArray(bodyItems) && bodyItems.length > 0
|
|
652
|
+
? resolveReplyTargets(bodyItems, accountId)
|
|
653
|
+
: [];
|
|
654
|
+
const isReplyToBot = replyTargets.some((t) => t.isBotMessage);
|
|
555
655
|
// Delegate to the common message handler (group chat)
|
|
556
656
|
await handleInfoflowMessage({
|
|
557
657
|
cfg,
|
|
@@ -570,6 +670,7 @@ export async function handleGroupChatMessage(params) {
|
|
|
570
670
|
mentionIds: mentionIds.userIds.length > 0 || mentionIds.agentIds.length > 0 ? mentionIds : undefined,
|
|
571
671
|
replyContext,
|
|
572
672
|
isReplyToBot: isReplyToBot || undefined,
|
|
673
|
+
replyTargets: replyTargets.length > 0 ? replyTargets : undefined,
|
|
573
674
|
imageUrls: imageUrls.length > 0 ? imageUrls : undefined,
|
|
574
675
|
},
|
|
575
676
|
accountId,
|
|
@@ -751,6 +852,54 @@ export async function handleInfoflowMessage(params) {
|
|
|
751
852
|
ctxPayload.BodyForAgent = bodyForAgent;
|
|
752
853
|
logVerbose(`[infoflow] group: BodyForAgent set for LLM (${bodyForAgent.length} chars, includes @/robotid)`);
|
|
753
854
|
}
|
|
855
|
+
// ---------------------------------------------------------------------------
|
|
856
|
+
// Inject sent-message context into the LLM body. Two sections:
|
|
857
|
+
// 1. Quoted-reply targets (when this inbound is a 引用回复): exposes the
|
|
858
|
+
// bot-message messageId so the LLM doesn't mistake the inbound id for
|
|
859
|
+
// the recall target.
|
|
860
|
+
// 2. Recent bot-sent messages (always, when records exist within 24h):
|
|
861
|
+
// gives the LLM both ambient awareness (for cross-context-sent messages
|
|
862
|
+
// that aren't in this session's history) and a candidate list for
|
|
863
|
+
// semantic recall ("撤回刚才那条笑话").
|
|
864
|
+
// ---------------------------------------------------------------------------
|
|
865
|
+
const ctxTarget = isGroup && groupId !== undefined ? `group:${groupId}` : fromuser;
|
|
866
|
+
{
|
|
867
|
+
const sections = [];
|
|
868
|
+
const quotedSection = formatQuotedReplyTargetsSection(event.replyTargets ?? []);
|
|
869
|
+
if (quotedSection)
|
|
870
|
+
sections.push(quotedSection);
|
|
871
|
+
const recallTextSource = `${bodyForAgent ?? ""}\n${event.replyContext?.join(" ") ?? ""}`.trim();
|
|
872
|
+
const recentSection = buildBotRecentMessagesSection({
|
|
873
|
+
accountId,
|
|
874
|
+
target: ctxTarget,
|
|
875
|
+
inboundLooksLikeRecall: looksLikeRecallIntent(recallTextSource),
|
|
876
|
+
isReplyToBot: event.isReplyToBot === true,
|
|
877
|
+
});
|
|
878
|
+
if (recentSection) {
|
|
879
|
+
sections.push(recentSection.text);
|
|
880
|
+
logVerbose(`[infoflow:ctx] injected recent-bot-messages section (mode=${recentSection.mode}, count=${recentSection.count}, target=${ctxTarget})`);
|
|
881
|
+
}
|
|
882
|
+
if (sections.length > 0) {
|
|
883
|
+
const existing = String(ctxPayload.Body ?? "");
|
|
884
|
+
ctxPayload.Body =
|
|
885
|
+
`${existing}\n\n${sections.join("\n\n")}`.trim();
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
// Register inbound context so the delete action handler can fall back to the
|
|
889
|
+
// bot-message id the inbound is quote-replying to (when present) — only used
|
|
890
|
+
// when the LLM otherwise passes an unknown id. We intentionally only pick a
|
|
891
|
+
// bot-sent reply target: falling back to a non-bot reply id would never help
|
|
892
|
+
// (it can't be in sent-messages.db) and only adds noise.
|
|
893
|
+
if (event.messageId) {
|
|
894
|
+
registerInboundContext({
|
|
895
|
+
accountId,
|
|
896
|
+
target: ctxTarget,
|
|
897
|
+
inboundMessageId: event.messageId,
|
|
898
|
+
replyToMessageId: event.replyTargets?.find((t) => t.isBotMessage)?.messageid,
|
|
899
|
+
replyTargets: event.replyTargets,
|
|
900
|
+
registeredAt: Date.now(),
|
|
901
|
+
});
|
|
902
|
+
}
|
|
754
903
|
// Record session using recordInboundSession for proper session tracking
|
|
755
904
|
await core.channel.session.recordInboundSession({
|
|
756
905
|
storePath,
|
|
@@ -922,6 +1071,12 @@ export async function handleInfoflowMessage(params) {
|
|
|
922
1071
|
? `${existing}\n\n---\n\n${groupCfg.systemPrompt}`
|
|
923
1072
|
: groupCfg.systemPrompt;
|
|
924
1073
|
}
|
|
1074
|
+
// Default output hygiene for every dispatched group message (always last)
|
|
1075
|
+
const hygiene = buildGroupOutputHygienePrompt();
|
|
1076
|
+
const beforeHygiene = ctxPayload.GroupSystemPrompt ?? "";
|
|
1077
|
+
ctxPayload.GroupSystemPrompt = beforeHygiene
|
|
1078
|
+
? `${beforeHygiene}\n\n---\n\n${hygiene}`
|
|
1079
|
+
: hygiene;
|
|
925
1080
|
}
|
|
926
1081
|
// Build unified target: "group:<id>" for group chat, username for private chat
|
|
927
1082
|
const to = isGroup && groupId !== undefined ? `group:${groupId}` : fromuser;
|
|
@@ -998,5 +1153,17 @@ export const _checkWatchMentioned = checkWatchMentioned;
|
|
|
998
1153
|
export const _extractMentionIds = extractMentionIds;
|
|
999
1154
|
/** @internal — Check if message matches any watchRegex pattern (dotAll). Only exported for tests. */
|
|
1000
1155
|
export const _checkWatchRegex = checkWatchRegex;
|
|
1001
|
-
/** @internal —
|
|
1002
|
-
export const
|
|
1156
|
+
/** @internal — Resolve structured replyData targets (id + preview + isBotMessage). Only exported for tests. */
|
|
1157
|
+
export const _resolveReplyTargets = resolveReplyTargets;
|
|
1158
|
+
/** @internal — Back-compat boolean form used by older tests; prefer _resolveReplyTargets. */
|
|
1159
|
+
export function _checkReplyToBot(bodyItems, accountId) {
|
|
1160
|
+
return resolveReplyTargets(bodyItems, accountId).some((t) => t.isBotMessage);
|
|
1161
|
+
}
|
|
1162
|
+
/** @internal — Group output hygiene fragment appended to GroupSystemPrompt. Only exported for tests. */
|
|
1163
|
+
export const _buildGroupOutputHygienePrompt = buildGroupOutputHygienePrompt;
|
|
1164
|
+
/** @internal — Recall intent regex. Only exported for tests. */
|
|
1165
|
+
export const _looksLikeRecallIntent = looksLikeRecallIntent;
|
|
1166
|
+
/** @internal — Sent-messages section builder. Only exported for tests. */
|
|
1167
|
+
export const _buildBotRecentMessagesSection = buildBotRecentMessagesSection;
|
|
1168
|
+
/** @internal — Quoted-reply targets section builder. Only exported for tests. */
|
|
1169
|
+
export const _formatQuotedReplyTargetsSection = formatQuotedReplyTargetsSection;
|
package/dist/src/channel.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { applyAccountNameToChannelSection, DEFAULT_ACCOUNT_ID, deleteAccountFromConfigSection, formatPairingApproveHint, migrateBaseNameToDefaultAccount, normalizeAccountId, setAccountEnabledInConfigSection, } from "openclaw/plugin-sdk/core";
|
|
2
2
|
import { getChannelSection, listInfoflowAccountIds, resolveDefaultInfoflowAccountId, resolveInfoflowAccount, } from "./accounts.js";
|
|
3
3
|
import { infoflowMessageActions } from "./actions.js";
|
|
4
|
+
import { createListSentMessagesTool } from "./agent-tools.js";
|
|
4
5
|
import { logVerbose } from "./logging.js";
|
|
5
6
|
import { parseMarkdownForLocalImages } from "./markdown-local-images.js";
|
|
6
7
|
import { prepareInfoflowImageBase64, sendInfoflowImageMessage } from "./media.js";
|
|
@@ -66,10 +67,21 @@ export const infoflowPlugin = {
|
|
|
66
67
|
},
|
|
67
68
|
reload: { configPrefixes: ["channels.infoflow"] },
|
|
68
69
|
actions: infoflowMessageActions,
|
|
70
|
+
agentTools: (params) => [
|
|
71
|
+
createListSentMessagesTool({
|
|
72
|
+
getConfig: () => params.cfg ?? {},
|
|
73
|
+
}),
|
|
74
|
+
],
|
|
69
75
|
agentPrompt: {
|
|
70
76
|
messageToolHints: () => [
|
|
71
77
|
'Infoflow group @mentions: set atAll=true to @all members, or mentionUserIds="user1,user2" (comma-separated uuapName) to @mention specific users. Only effective for group targets (group:<id>).',
|
|
72
|
-
'Infoflow
|
|
78
|
+
'Infoflow message recall (撤回): use action="delete" to recall a bot-sent message.',
|
|
79
|
+
' - To recall a specific message, pass messageId=<the bot message id>. Get the id from (a) the "Recent messages you (the bot) sent" section that may be injected into the body, (b) the "quoted reply target" block when sentByBot=true, or (c) the infoflow_list_sent_messages tool.',
|
|
80
|
+
' - To recall the most recent message without specifying id, omit messageId (defaults to count=1).',
|
|
81
|
+
' - For batch recall use count=<N>.',
|
|
82
|
+
' - NEVER pass the current inbound message_id (the user-sent message you are replying to) as the delete target — that is the USER\'s message, not a bot message; the call will fail.',
|
|
83
|
+
' - When a quoted reply target is present with sentByBot=true, that messageId is the most likely recall target.',
|
|
84
|
+
' - For messages older than the injected recent window, or hard-to-identify ones, call infoflow_list_sent_messages first (use containsText / withinHours filters) and then pass the chosen messageId to action="delete".',
|
|
73
85
|
],
|
|
74
86
|
},
|
|
75
87
|
config: {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory registry of inbound message context, used by the delete action handler
|
|
3
|
+
* to recover when the LLM passes a wrong messageId.
|
|
4
|
+
*
|
|
5
|
+
* Why: openclaw's ChannelMessageActionContext exposes the inbound trigger's
|
|
6
|
+
* currentMessageId (via ctx.toolContext) but does NOT carry the inbound's
|
|
7
|
+
* replyToMessageId / resolved reply targets. We register that context here on
|
|
8
|
+
* inbound, keyed by the inbound messageId, and consume it from the action
|
|
9
|
+
* handler.
|
|
10
|
+
*
|
|
11
|
+
* Entries auto-expire after RETENTION_MS to keep the map bounded.
|
|
12
|
+
*/
|
|
13
|
+
import { logVerbose } from "./logging.js";
|
|
14
|
+
const RETENTION_MS = 10 * 60 * 1000; // 10 minutes — same order of magnitude as followUp window
|
|
15
|
+
const MAX_ENTRIES = 500;
|
|
16
|
+
const store = new Map();
|
|
17
|
+
function evictExpired() {
|
|
18
|
+
if (store.size === 0)
|
|
19
|
+
return;
|
|
20
|
+
const cutoff = Date.now() - RETENTION_MS;
|
|
21
|
+
let count = 0;
|
|
22
|
+
for (const [key, entry] of store) {
|
|
23
|
+
if (entry.registeredAt < cutoff) {
|
|
24
|
+
store.delete(key);
|
|
25
|
+
count++;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (count > 0) {
|
|
29
|
+
logVerbose(`[infoflow:inbound-ctx] evicted ${count} expired entries`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function registerInboundContext(record) {
|
|
33
|
+
evictExpired();
|
|
34
|
+
// Cap the map size; if we're over, drop the oldest entries.
|
|
35
|
+
if (store.size >= MAX_ENTRIES) {
|
|
36
|
+
const sorted = Array.from(store.entries()).sort((a, b) => a[1].registeredAt - b[1].registeredAt);
|
|
37
|
+
const dropCount = store.size - MAX_ENTRIES + 1;
|
|
38
|
+
for (let i = 0; i < dropCount; i++) {
|
|
39
|
+
store.delete(sorted[i][0]);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
store.set(record.inboundMessageId, record);
|
|
43
|
+
}
|
|
44
|
+
export function lookupInboundContext(inboundMessageId) {
|
|
45
|
+
const entry = store.get(inboundMessageId);
|
|
46
|
+
if (!entry)
|
|
47
|
+
return undefined;
|
|
48
|
+
if (Date.now() - entry.registeredAt > RETENTION_MS) {
|
|
49
|
+
store.delete(inboundMessageId);
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
return entry;
|
|
53
|
+
}
|
|
54
|
+
/** @internal — for tests */
|
|
55
|
+
export function _resetInboundContext() {
|
|
56
|
+
store.clear();
|
|
57
|
+
}
|
|
58
|
+
/** @internal — for tests */
|
|
59
|
+
export function _inboundContextSize() {
|
|
60
|
+
return store.size;
|
|
61
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chbo297/infoflow",
|
|
3
|
-
"version": "2026.5.
|
|
3
|
+
"version": "2026.5.9-beta.1",
|
|
4
4
|
"description": "OpenClaw Infoflow (如流) channel plugin for Baidu enterprise messaging",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
"@baidu/infoflow-sdk-nodejs": ">=0.1.0",
|
|
23
23
|
"openclaw": ">=2026.5.4"
|
|
24
24
|
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"typebox": "^1.1.0"
|
|
27
|
+
},
|
|
25
28
|
"peerDependenciesMeta": {
|
|
26
29
|
"@baidu/infoflow-sdk-nodejs": {
|
|
27
30
|
"optional": true
|
|
@@ -47,9 +50,11 @@
|
|
|
47
50
|
"scripts": {
|
|
48
51
|
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
49
52
|
"typecheck": "tsc --noEmit",
|
|
50
|
-
"test": "vitest run"
|
|
53
|
+
"test": "vitest run",
|
|
54
|
+
"sync-readme-install-version": "node scripts/sync-readme-install-version.mjs"
|
|
51
55
|
},
|
|
52
56
|
"devDependencies": {
|
|
57
|
+
"typebox": "^1.1.38",
|
|
53
58
|
"typescript": "^6.0.3",
|
|
54
59
|
"vitest": "^4.1.5"
|
|
55
60
|
},
|