@core-workspace/infoflow-openclaw-plugin 2026.3.8 → 2026.3.31
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/CHANGELOG.md +21 -0
- package/README.md +142 -50
- package/docs/qa-feature-list.md +413 -0
- package/index.ts +25 -1
- package/openclaw.plugin.json +22 -1
- package/package.json +19 -4
- package/publish.sh +221 -0
- package/scripts/deploy.sh +1 -1
- package/scripts/npm-tools/README.md +70 -0
- package/scripts/npm-tools/cli.js +262 -0
- package/scripts/npm-tools/package.json +21 -0
- package/src/adapter/inbound/ws-receiver.ts +71 -29
- package/src/adapter/outbound/reply-dispatcher.ts +12 -19
- package/src/channel/accounts.ts +26 -6
- package/src/channel/channel.ts +5 -4
- package/src/channel/media.ts +8 -0
- package/src/channel/outbound.ts +15 -7
- package/src/commands/changelog.ts +53 -0
- package/src/commands/doctor.ts +391 -0
- package/src/commands/logs.ts +212 -0
- package/src/handler/message-handler.ts +77 -82
- package/src/security/group-policy.ts +2 -0
- package/src/types.ts +20 -4
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
* SDK event system (@core-workspace/infoflow-sdk-nodejs):
|
|
13
13
|
* - Group messages → "group.*" (covers group.text, group.mixed, group.image, etc.)
|
|
14
14
|
* - Private messages → "private.*" (covers private.text, private.image, etc.)
|
|
15
|
-
* - event.data is normalized
|
|
16
|
-
*
|
|
15
|
+
* - SDK 0.1.1: event.data is normalized with fromUserId, groupId, body, originalMessage, ...
|
|
16
|
+
* - SDK 0.1.5+: event.data is { chatType, msgType, raw } — raw is the full original payload
|
|
17
17
|
*
|
|
18
18
|
* SDK Bug Workaround:
|
|
19
19
|
* SDK's normalizePrivateMessage() drops PicUrl/MsgId fields from private messages.
|
|
@@ -28,7 +28,8 @@ import { WSClient } from "@core-workspace/infoflow-sdk-nodejs";
|
|
|
28
28
|
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
29
29
|
import { handleGroupChatMessage, handlePrivateChatMessage } from "../../handler/message-handler.js";
|
|
30
30
|
import { isDuplicateMessage } from "./webhook-parser.js";
|
|
31
|
-
import { formatInfoflowError, logVerbose } from "../../logging.js";
|
|
31
|
+
import { formatInfoflowError, logVerbose, getInfoflowWebhookLog } from "../../logging.js";
|
|
32
|
+
import { extractIdFromRawJson } from "../../channel/outbound.js";
|
|
32
33
|
import type { ResolvedInfoflowAccount } from "../../types.js";
|
|
33
34
|
|
|
34
35
|
// ---------------------------------------------------------------------------
|
|
@@ -79,6 +80,22 @@ export class InfoflowWSReceiver {
|
|
|
79
80
|
return normalized;
|
|
80
81
|
};
|
|
81
82
|
}
|
|
83
|
+
|
|
84
|
+
// Patch frameCodec.parsePayload to attach _rawJson to parsed objects.
|
|
85
|
+
// This preserves large integer precision (e.g. messageid) that JSON.parse loses.
|
|
86
|
+
const frameCodec = (this.wsClient as any).frameCodec;
|
|
87
|
+
if (frameCodec && typeof frameCodec.parsePayload === "function") {
|
|
88
|
+
const originalParse = frameCodec.parsePayload.bind(frameCodec);
|
|
89
|
+
frameCodec.parsePayload = function (frame: any) {
|
|
90
|
+
const result = originalParse(frame);
|
|
91
|
+
if (result && typeof result === "object" && frame.payload) {
|
|
92
|
+
try {
|
|
93
|
+
result._rawJson = frame.payload.toString("utf-8");
|
|
94
|
+
} catch { /* ignore */ }
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
82
99
|
}
|
|
83
100
|
|
|
84
101
|
/** Connect and start receiving messages. */
|
|
@@ -106,7 +123,10 @@ export class InfoflowWSReceiver {
|
|
|
106
123
|
this.wsClient.on("private.*", handlePrivateEvent);
|
|
107
124
|
|
|
108
125
|
// Connect (two-phase: endpoint allocation → WS handshake)
|
|
126
|
+
const wsGateway = this.options.account.config.wsGateway ?? "infoflow-open-gateway.weiyun.baidu.com";
|
|
127
|
+
getInfoflowWebhookLog().info(`[ws:connect] connecting to ${wsGateway}`);
|
|
109
128
|
await this.wsClient.connect();
|
|
129
|
+
getInfoflowWebhookLog().info(`[ws:connect] connected to ${wsGateway}`);
|
|
110
130
|
|
|
111
131
|
// Listen for abortSignal to gracefully disconnect
|
|
112
132
|
this.options.abortSignal.addEventListener(
|
|
@@ -122,6 +142,7 @@ export class InfoflowWSReceiver {
|
|
|
122
142
|
stop(): void {
|
|
123
143
|
if (this.stopped) return;
|
|
124
144
|
this.stopped = true;
|
|
145
|
+
getInfoflowWebhookLog().info(`[ws:disconnect] stopping ws receiver`);
|
|
125
146
|
try {
|
|
126
147
|
this.wsClient.disconnect();
|
|
127
148
|
} catch {
|
|
@@ -131,8 +152,10 @@ export class InfoflowWSReceiver {
|
|
|
131
152
|
|
|
132
153
|
/**
|
|
133
154
|
* Handle a group message event from the new SDK.
|
|
134
|
-
*
|
|
135
|
-
*
|
|
155
|
+
*
|
|
156
|
+
* SDK 0.1.1: normalizes to { chatType, msgType, fromUserId, groupId, body, originalMessage, ... }
|
|
157
|
+
* SDK 0.1.5: normalizes to { chatType, msgType, raw } — raw is the original payload
|
|
158
|
+
*
|
|
136
159
|
* We reconstruct the raw msgData shape expected by handleGroupChatMessage.
|
|
137
160
|
*/
|
|
138
161
|
private async handleGroupEvent(data: any): Promise<void> {
|
|
@@ -140,37 +163,54 @@ export class InfoflowWSReceiver {
|
|
|
140
163
|
|
|
141
164
|
this.options.statusSink?.({ lastInboundAt: Date.now() });
|
|
142
165
|
|
|
166
|
+
// SDK 0.1.5 puts everything in data.raw; 0.1.1 inlines fields directly.
|
|
167
|
+
const payload = data.raw ?? data;
|
|
168
|
+
|
|
143
169
|
// Reconstruct raw msgData compatible with handleGroupChatMessage expectations:
|
|
144
170
|
// { message: { header: { fromuserid, servertime, messageid, ... }, body }, groupid, ... }
|
|
145
|
-
const originalMessage =
|
|
146
|
-
const
|
|
171
|
+
const originalMessage = payload.originalMessage ?? payload;
|
|
172
|
+
const message = payload.message ?? originalMessage.message ?? {};
|
|
173
|
+
const header = message.header ?? originalMessage.header ?? {};
|
|
174
|
+
|
|
175
|
+
// Extract messageid with full precision from raw JSON string (avoids JS Number precision loss
|
|
176
|
+
// for large int64 IDs like 1859821262633816373 which JSON.parse rounds to ...300).
|
|
177
|
+
const rawJson: string | undefined = payload._rawJson;
|
|
178
|
+
const preciseMessageId =
|
|
179
|
+
(rawJson && extractIdFromRawJson(rawJson, "messageid")) ??
|
|
180
|
+
(header.messageid != null ? String(header.messageid) : undefined);
|
|
181
|
+
const preciseClientMsgId =
|
|
182
|
+
(rawJson && extractIdFromRawJson(rawJson, "clientmsgid")) ??
|
|
183
|
+
(header.clientmsgid != null ? String(header.clientmsgid) : undefined);
|
|
184
|
+
|
|
147
185
|
const msgData: Record<string, unknown> = {
|
|
148
186
|
eventtype: "MESSAGE_RECEIVE",
|
|
149
|
-
groupid:
|
|
187
|
+
groupid: payload.groupid ?? payload.groupId ?? data.groupId,
|
|
188
|
+
fromid: payload.fromid,
|
|
150
189
|
message: {
|
|
151
190
|
header: {
|
|
152
|
-
fromuserid:
|
|
153
|
-
toid:
|
|
191
|
+
fromuserid: header.fromuserid ?? payload.fromUserId ?? data.fromUserId ?? "",
|
|
192
|
+
toid: payload.groupid ?? payload.groupId ?? data.groupId,
|
|
154
193
|
totype: "GROUP",
|
|
155
|
-
msgtype:
|
|
156
|
-
messageid:
|
|
157
|
-
clientmsgid:
|
|
194
|
+
msgtype: header.msgtype ?? data.msgType ?? "text",
|
|
195
|
+
messageid: preciseMessageId,
|
|
196
|
+
clientmsgid: preciseClientMsgId,
|
|
158
197
|
servertime: header.servertime,
|
|
159
198
|
clienttime: header.clienttime,
|
|
160
199
|
at: header.at ?? { atrobotids: [] },
|
|
161
200
|
},
|
|
162
|
-
body:
|
|
201
|
+
body: message.body ?? payload.body ?? data.body ??
|
|
202
|
+
(data.content ? [{ type: "TEXT", content: data.content }] : []),
|
|
163
203
|
},
|
|
164
204
|
};
|
|
165
205
|
|
|
166
|
-
// Dedup check using clientmsgid or messageid as key
|
|
167
|
-
const dedupKey =
|
|
206
|
+
// Dedup check using clientmsgid or messageid as key (use precise string IDs)
|
|
207
|
+
const dedupKey = preciseClientMsgId ?? preciseMessageId;
|
|
168
208
|
if (dedupKey && isDuplicateMessage({ CreateTime: String(dedupKey) })) {
|
|
169
209
|
logVerbose("[infoflow:ws] duplicate group message, skipping");
|
|
170
210
|
return;
|
|
171
211
|
}
|
|
172
212
|
|
|
173
|
-
logVerbose(`[infoflow:ws] group message: from=${
|
|
213
|
+
logVerbose(`[infoflow:ws] group message: from=${header.fromuserid}, msgType=${header.msgtype}, groupId=${payload.groupid ?? payload.groupId}`);
|
|
174
214
|
|
|
175
215
|
await handleGroupChatMessage({
|
|
176
216
|
cfg: this.options.config,
|
|
@@ -182,7 +222,10 @@ export class InfoflowWSReceiver {
|
|
|
182
222
|
|
|
183
223
|
/**
|
|
184
224
|
* Handle a private message event from the new SDK.
|
|
185
|
-
*
|
|
225
|
+
*
|
|
226
|
+
* SDK 0.1.1: normalizes to { chatType, msgType, fromUserId, content, createTime, originalMessage, ... }
|
|
227
|
+
* SDK 0.1.5: normalizes to { chatType, msgType, raw } — raw is the original payload
|
|
228
|
+
*
|
|
186
229
|
* We reconstruct the raw msgData shape expected by handlePrivateChatMessage.
|
|
187
230
|
*/
|
|
188
231
|
private async handlePrivateEvent(data: any): Promise<void> {
|
|
@@ -190,20 +233,21 @@ export class InfoflowWSReceiver {
|
|
|
190
233
|
|
|
191
234
|
this.options.statusSink?.({ lastInboundAt: Date.now() });
|
|
192
235
|
|
|
193
|
-
|
|
236
|
+
// SDK 0.1.5 puts everything in data.raw; 0.1.1 inlines fields directly.
|
|
237
|
+
const payload = data.raw ?? data;
|
|
194
238
|
|
|
195
239
|
// Reconstruct raw msgData compatible with handlePrivateChatMessage expectations:
|
|
196
240
|
// { FromUserId, Content, MsgType, CreateTime, PicUrl, MsgId, ... }
|
|
197
|
-
const originalMessage =
|
|
241
|
+
const originalMessage = payload.originalMessage ?? payload;
|
|
198
242
|
const msgData: Record<string, unknown> = {
|
|
199
|
-
FromUserId:
|
|
200
|
-
FromUserName:
|
|
201
|
-
Content:
|
|
202
|
-
MsgType:
|
|
203
|
-
CreateTime:
|
|
243
|
+
FromUserId: payload.FromUserId ?? payload.fromUserId ?? data.fromUserId ?? originalMessage.FromUserId ?? "",
|
|
244
|
+
FromUserName: payload.FromUserName ?? payload.fromUserName ?? data.fromUserName ?? originalMessage.FromUserName,
|
|
245
|
+
Content: payload.Content ?? payload.content ?? data.content ?? originalMessage.Content ?? "",
|
|
246
|
+
MsgType: payload.MsgType ?? payload.msgType ?? data.msgType ?? originalMessage.MsgType ?? "text",
|
|
247
|
+
CreateTime: payload.CreateTime ?? payload.createTime ?? data.createTime ?? originalMessage.CreateTime ?? String(Date.now()),
|
|
204
248
|
// 图片消息字段
|
|
205
|
-
PicUrl:
|
|
206
|
-
MsgId:
|
|
249
|
+
PicUrl: payload.PicUrl ?? payload.picUrl ?? data.picUrl ?? originalMessage.PicUrl ?? "",
|
|
250
|
+
MsgId: payload.MsgId ?? payload.msgId ?? data.msgId ?? originalMessage.MsgId,
|
|
207
251
|
};
|
|
208
252
|
|
|
209
253
|
// Dedup check
|
|
@@ -222,5 +266,3 @@ export class InfoflowWSReceiver {
|
|
|
222
266
|
});
|
|
223
267
|
}
|
|
224
268
|
}
|
|
225
|
-
|
|
226
|
-
|
|
@@ -45,6 +45,11 @@ export type CreateInfoflowReplyDispatcherParams = {
|
|
|
45
45
|
* Default: "text"
|
|
46
46
|
*/
|
|
47
47
|
messageFormat?: "text" | "markdown";
|
|
48
|
+
/**
|
|
49
|
+
* Maximum character limit per outbound message chunk.
|
|
50
|
+
* Default: 1800
|
|
51
|
+
*/
|
|
52
|
+
textChunkLimit?: number;
|
|
48
53
|
};
|
|
49
54
|
|
|
50
55
|
/**
|
|
@@ -64,6 +69,7 @@ export function createInfoflowReplyDispatcher(params: CreateInfoflowReplyDispatc
|
|
|
64
69
|
replyToPreview,
|
|
65
70
|
replyToImid,
|
|
66
71
|
messageFormat = "text",
|
|
72
|
+
textChunkLimit = 1800,
|
|
67
73
|
} = params;
|
|
68
74
|
const core = getInfoflowRuntime();
|
|
69
75
|
|
|
@@ -93,8 +99,6 @@ export function createInfoflowReplyDispatcher(params: CreateInfoflowReplyDispatc
|
|
|
93
99
|
|
|
94
100
|
// Build replyTo context (only used for the first outbound message)
|
|
95
101
|
// Note: replyTo is suppressed when messageFormat is "markdown" (not supported by API)
|
|
96
|
-
logVerbose(`[DEBUG reply-dispatcher] isGroup=${isGroup}, replyToMessageId=${replyToMessageId}, replyToImid=${replyToImid}, replyToPreview=${replyToPreview?.slice(0, 50)}`);
|
|
97
|
-
|
|
98
102
|
const replyTo: InfoflowOutboundReply | undefined =
|
|
99
103
|
isGroup && effectiveReplyToMessageId
|
|
100
104
|
? {
|
|
@@ -106,17 +110,8 @@ export function createInfoflowReplyDispatcher(params: CreateInfoflowReplyDispatc
|
|
|
106
110
|
: undefined;
|
|
107
111
|
let replyApplied = false;
|
|
108
112
|
|
|
109
|
-
// Debug: Log constructed replyTo
|
|
110
|
-
logVerbose(`[DEBUG reply-dispatcher] replyTo constructed: ${JSON.stringify(replyTo)}`);
|
|
111
|
-
if (replyTo) {
|
|
112
|
-
logVerbose(`[DEBUG reply-dispatcher] Creating reply context with messageid=${replyTo.messageid}`);
|
|
113
|
-
} else {
|
|
114
|
-
logVerbose(`[DEBUG reply-dispatcher] replyTo is undefined - not creating reply context`);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
113
|
const deliver = async (payload: ReplyPayload) => {
|
|
118
114
|
const text = payload.text ?? "";
|
|
119
|
-
logVerbose(`[infoflow] deliver called: to=${to}, text=${text}`);
|
|
120
115
|
|
|
121
116
|
// Normalize media URL list (same pattern as Feishu reply-dispatcher)
|
|
122
117
|
const mediaList =
|
|
@@ -126,7 +121,6 @@ export function createInfoflowReplyDispatcher(params: CreateInfoflowReplyDispatc
|
|
|
126
121
|
? [payload.mediaUrl]
|
|
127
122
|
: [];
|
|
128
123
|
|
|
129
|
-
logVerbose(`[infoflow] deliver called: to=${to}, text=${text}, mediaList=${JSON.stringify(mediaList)}`);
|
|
130
124
|
if (!text.trim() && mediaList.length === 0) {
|
|
131
125
|
return;
|
|
132
126
|
}
|
|
@@ -169,8 +163,8 @@ export function createInfoflowReplyDispatcher(params: CreateInfoflowReplyDispatc
|
|
|
169
163
|
// AT nodes will be added to contents for notification, but text stays clean
|
|
170
164
|
let messageText = text;
|
|
171
165
|
|
|
172
|
-
// Chunk text
|
|
173
|
-
const chunks = core.channel.text.
|
|
166
|
+
// Chunk text using markdown-aware chunker with configured limit
|
|
167
|
+
const chunks = core.channel.text.chunkMarkdownText(messageText, textChunkLimit);
|
|
174
168
|
// Only include @mentions in the first chunk (avoid duplicate @s)
|
|
175
169
|
let isFirstChunk = true;
|
|
176
170
|
|
|
@@ -196,10 +190,6 @@ export function createInfoflowReplyDispatcher(params: CreateInfoflowReplyDispatc
|
|
|
196
190
|
// Only include replyTo on the first outbound message
|
|
197
191
|
const chunkReplyTo = !replyApplied ? replyTo : undefined;
|
|
198
192
|
|
|
199
|
-
// Debug: Log when sending with replyTo
|
|
200
|
-
logVerbose(`[DEBUG deliver] chunkReplyTo: ${JSON.stringify(chunkReplyTo)}`);
|
|
201
|
-
logVerbose(`[DEBUG deliver] replyApplied=${replyApplied}, replyTo exists=${!!replyTo}`);
|
|
202
|
-
|
|
203
193
|
const result = await sendInfoflowMessage({
|
|
204
194
|
cfg,
|
|
205
195
|
to,
|
|
@@ -210,10 +200,13 @@ export function createInfoflowReplyDispatcher(params: CreateInfoflowReplyDispatc
|
|
|
210
200
|
if (chunkReplyTo) replyApplied = true;
|
|
211
201
|
|
|
212
202
|
if (result.ok) {
|
|
203
|
+
getInfoflowSendLog().info(
|
|
204
|
+
`[outbound] to=${to}, len=${chunk.length}, msgId=${result.messageId ?? "N/A"}, replyTo=${chunkReplyTo?.messageid ?? "none"}`,
|
|
205
|
+
);
|
|
213
206
|
statusSink?.({ lastOutboundAt: Date.now() });
|
|
214
207
|
} else if (result.error) {
|
|
215
208
|
getInfoflowSendLog().error(
|
|
216
|
-
`[
|
|
209
|
+
`[outbound:error] to=${to}, accountId=${accountId}: ${result.error}`,
|
|
217
210
|
);
|
|
218
211
|
}
|
|
219
212
|
}
|
package/src/channel/accounts.ts
CHANGED
|
@@ -77,10 +77,16 @@ function mergeInfoflowAccountConfig(
|
|
|
77
77
|
appAgentId?: number;
|
|
78
78
|
dmMessageFormat?: "text" | "markdown";
|
|
79
79
|
groupMessageFormat?: "text" | "markdown";
|
|
80
|
-
|
|
80
|
+
replyMode?: import("../types.js").InfoflowReplyMode;
|
|
81
|
+
groupSessionMode?: import("../types.js").InfoflowGroupSessionMode;
|
|
82
|
+
followUp?: boolean;
|
|
83
|
+
followUpWindow?: number;
|
|
84
|
+
groups?: Record<string, import("../types.js").InfoflowGroupConfig>;
|
|
85
|
+
dmPolicy?: import("../types.js").InfoflowDmPolicy;
|
|
81
86
|
allowFrom?: string[];
|
|
82
|
-
groupPolicy?:
|
|
87
|
+
groupPolicy?: import("../types.js").InfoflowGroupPolicy;
|
|
83
88
|
groupAllowFrom?: string[];
|
|
89
|
+
textChunkLimit?: number;
|
|
84
90
|
} {
|
|
85
91
|
const raw = getChannelSection(cfg) ?? {};
|
|
86
92
|
const { accounts: _ignored, defaultAccount: _ignored2, ...base } = raw;
|
|
@@ -102,9 +108,14 @@ function mergeInfoflowAccountConfig(
|
|
|
102
108
|
appAgentId?: number;
|
|
103
109
|
dmMessageFormat?: "text" | "markdown";
|
|
104
110
|
groupMessageFormat?: "text" | "markdown";
|
|
105
|
-
|
|
111
|
+
replyMode?: import("../types.js").InfoflowReplyMode;
|
|
112
|
+
groupSessionMode?: import("../types.js").InfoflowGroupSessionMode;
|
|
113
|
+
followUp?: boolean;
|
|
114
|
+
followUpWindow?: number;
|
|
115
|
+
groups?: Record<string, import("../types.js").InfoflowGroupConfig>;
|
|
116
|
+
dmPolicy?: import("../types.js").InfoflowDmPolicy;
|
|
106
117
|
allowFrom?: string[];
|
|
107
|
-
groupPolicy?:
|
|
118
|
+
groupPolicy?: import("../types.js").InfoflowGroupPolicy;
|
|
108
119
|
groupAllowFrom?: string[];
|
|
109
120
|
};
|
|
110
121
|
}
|
|
@@ -125,13 +136,16 @@ export function resolveInfoflowAccount(params: {
|
|
|
125
136
|
const merged = mergeInfoflowAccountConfig(params.cfg, accountId);
|
|
126
137
|
const accountEnabled = merged.enabled !== false;
|
|
127
138
|
const enabled = baseEnabled && accountEnabled;
|
|
128
|
-
const apiHost = merged.apiHost ?? "";
|
|
139
|
+
const apiHost = merged.apiHost ?? "http://apiin.im.baidu.com";
|
|
129
140
|
const checkToken = merged.checkToken ?? "";
|
|
130
141
|
const encodingAESKey = merged.encodingAESKey ?? "";
|
|
131
142
|
const appKey = merged.appKey ?? "";
|
|
132
143
|
const appSecret = merged.appSecret ?? "";
|
|
144
|
+
const effectiveConnectionMode = merged.connectionMode ?? "websocket";
|
|
133
145
|
const configured =
|
|
134
|
-
|
|
146
|
+
effectiveConnectionMode === "websocket"
|
|
147
|
+
? Boolean(appKey) && Boolean(appSecret)
|
|
148
|
+
: Boolean(checkToken) && Boolean(encodingAESKey) && Boolean(appKey) && Boolean(appSecret);
|
|
135
149
|
|
|
136
150
|
return {
|
|
137
151
|
accountId,
|
|
@@ -155,10 +169,16 @@ export function resolveInfoflowAccount(params: {
|
|
|
155
169
|
appAgentId: merged.appAgentId,
|
|
156
170
|
dmMessageFormat: merged.dmMessageFormat,
|
|
157
171
|
groupMessageFormat: merged.groupMessageFormat,
|
|
172
|
+
replyMode: merged.replyMode,
|
|
173
|
+
groupSessionMode: merged.groupSessionMode,
|
|
174
|
+
followUp: merged.followUp,
|
|
175
|
+
followUpWindow: merged.followUpWindow,
|
|
176
|
+
groups: merged.groups,
|
|
158
177
|
dmPolicy: merged.dmPolicy,
|
|
159
178
|
allowFrom: merged.allowFrom,
|
|
160
179
|
groupPolicy: merged.groupPolicy,
|
|
161
180
|
groupAllowFrom: merged.groupAllowFrom,
|
|
181
|
+
textChunkLimit: merged.textChunkLimit,
|
|
162
182
|
},
|
|
163
183
|
};
|
|
164
184
|
}
|
package/src/channel/channel.ts
CHANGED
|
@@ -205,10 +205,11 @@ export const infoflowPlugin: ChannelPlugin<ResolvedInfoflowAccount> = {
|
|
|
205
205
|
},
|
|
206
206
|
},
|
|
207
207
|
outbound: {
|
|
208
|
-
deliveryMode: "
|
|
208
|
+
deliveryMode: "gateway",
|
|
209
209
|
chunkerMode: "markdown",
|
|
210
|
-
textChunkLimit:
|
|
211
|
-
|
|
210
|
+
textChunkLimit: 1800,
|
|
211
|
+
resolveTextChunkLimit: (account) => account.config.textChunkLimit,
|
|
212
|
+
chunker: (text, limit) => getInfoflowRuntime().channel.text.chunkMarkdownText(text, limit),
|
|
212
213
|
sendText: async ({ cfg, to, text, accountId }) => {
|
|
213
214
|
logVerbose(`[infoflow:sendText] to=${to}, accountId=${accountId}`);
|
|
214
215
|
// Use "markdown" type even though param is named `text`: LLM outputs are often markdown,
|
|
@@ -322,7 +323,7 @@ export const infoflowPlugin: ChannelPlugin<ResolvedInfoflowAccount> = {
|
|
|
322
323
|
gateway: {
|
|
323
324
|
startAccount: async (ctx) => {
|
|
324
325
|
const account = ctx.account;
|
|
325
|
-
const connectionMode = account.config.connectionMode ?? "
|
|
326
|
+
const connectionMode = account.config.connectionMode ?? "websocket";
|
|
326
327
|
ctx.log?.info(`[${account.accountId}] starting Infoflow ${connectionMode}`);
|
|
327
328
|
ctx.setStatus({
|
|
328
329
|
accountId: account.accountId,
|
package/src/channel/media.ts
CHANGED
|
@@ -298,6 +298,14 @@ export async function sendInfoflowPrivateImage(params: {
|
|
|
298
298
|
const data = JSON.parse(responseText) as Record<string, unknown>;
|
|
299
299
|
logVerbose(`[infoflow:sendPrivateImage] response: status=${res.status}, data=${responseText}`);
|
|
300
300
|
|
|
301
|
+
// Check outer code first (same format as text message API)
|
|
302
|
+
const code = typeof data.code === "string" ? data.code : "";
|
|
303
|
+
if (code && code !== "ok") {
|
|
304
|
+
const errMsg = String(data.message ?? data.errmsg ?? `code=${code}`);
|
|
305
|
+
getInfoflowSendLog().error(`[infoflow:sendPrivateImage] failed: ${errMsg}`);
|
|
306
|
+
return { ok: false, error: errMsg };
|
|
307
|
+
}
|
|
308
|
+
// Also check inner errcode
|
|
301
309
|
if (data.errcode && data.errcode !== 0) {
|
|
302
310
|
const errMsg = String(data.errmsg ?? `errcode ${data.errcode}`);
|
|
303
311
|
getInfoflowSendLog().error(`[infoflow:sendPrivateImage] failed: ${errMsg}`);
|
package/src/channel/outbound.ts
CHANGED
|
@@ -283,8 +283,9 @@ export async function sendInfoflowPrivateMessage(params: {
|
|
|
283
283
|
|
|
284
284
|
const bodyStr = JSON.stringify(payload);
|
|
285
285
|
|
|
286
|
-
// Log request
|
|
287
|
-
|
|
286
|
+
// Log request
|
|
287
|
+
getInfoflowSendLog().info(`[outbound:dm] to=${toUser}, msgtype=${payload.msgtype}, bodyLen=${bodyStr.length}`);
|
|
288
|
+
logVerbose(`[outbound:dm] POST body: ${bodyStr}`);
|
|
288
289
|
|
|
289
290
|
const res = await fetch(`${ensureHttps(apiHost)}${INFOFLOW_PRIVATE_SEND_PATH}`, {
|
|
290
291
|
method: "POST",
|
|
@@ -295,7 +296,8 @@ export async function sendInfoflowPrivateMessage(params: {
|
|
|
295
296
|
|
|
296
297
|
const responseText = await res.text();
|
|
297
298
|
const data = JSON.parse(responseText) as Record<string, unknown>;
|
|
298
|
-
|
|
299
|
+
getInfoflowSendLog().info(`[outbound:dm] response: status=${res.status}, code=${data.code ?? "?"}, msgkey=${(data.data as any)?.msgkey ?? "?"}`);
|
|
300
|
+
logVerbose(`[outbound:dm] response body: ${responseText}`);
|
|
299
301
|
|
|
300
302
|
// Check outer code first
|
|
301
303
|
const code = typeof data.code === "string" ? data.code : "";
|
|
@@ -465,10 +467,10 @@ export async function sendInfoflowGroupMessage(params: {
|
|
|
465
467
|
...(replyTo
|
|
466
468
|
? {
|
|
467
469
|
reply: {
|
|
468
|
-
messageid: String(replyTo.messageid),
|
|
470
|
+
messageid: String(replyTo.messageid),
|
|
469
471
|
preview: replyTo.preview ?? "",
|
|
470
|
-
...(replyTo.imid ? { imid: replyTo.imid } : {}),
|
|
471
|
-
|
|
472
|
+
...(replyTo.imid ? { imid: replyTo.imid } : {}),
|
|
473
|
+
replytype: replyTo.replytype ?? "1",
|
|
472
474
|
},
|
|
473
475
|
}
|
|
474
476
|
: {}),
|
|
@@ -478,6 +480,9 @@ export async function sendInfoflowGroupMessage(params: {
|
|
|
478
480
|
// Build request body
|
|
479
481
|
const bodyStr = JSON.stringify(payload);
|
|
480
482
|
|
|
483
|
+
getInfoflowSendLog().info(`[outbound:group] groupId=${groupId}, msgtype=${msgtype}, bodyLen=${bodyStr.length}, hasReply=${!!replyTo}`);
|
|
484
|
+
logVerbose(`[outbound:group] POST body: ${bodyStr}`);
|
|
485
|
+
|
|
481
486
|
const res = await fetch(`${ensureHttps(apiHost)}${INFOFLOW_GROUP_SEND_PATH}`, {
|
|
482
487
|
method: "POST",
|
|
483
488
|
headers,
|
|
@@ -487,7 +492,8 @@ export async function sendInfoflowGroupMessage(params: {
|
|
|
487
492
|
|
|
488
493
|
const responseText = await res.text();
|
|
489
494
|
const data = JSON.parse(responseText) as Record<string, unknown>;
|
|
490
|
-
|
|
495
|
+
getInfoflowSendLog().info(`[outbound:group] response: status=${res.status}, code=${data.code ?? "?"}, messageid=${extractIdFromRawJson(responseText, "messageid") ?? "?"}`);
|
|
496
|
+
logVerbose(`[outbound:group] response body: ${responseText}`);
|
|
491
497
|
|
|
492
498
|
const code = typeof data.code === "string" ? data.code : "";
|
|
493
499
|
if (code !== "ok") {
|
|
@@ -861,6 +867,8 @@ export async function sendInfoflowMessage(params: {
|
|
|
861
867
|
// Parse target: remove "infoflow:" prefix if present
|
|
862
868
|
const target = to.replace(/^infoflow:/i, "");
|
|
863
869
|
|
|
870
|
+
getInfoflowSendLog().info(`[outbound] sendMessage: to=${target}, items=${resolvedContents.length}, types=[${resolvedContents.map(c => c.type).join(",")}]`);
|
|
871
|
+
|
|
864
872
|
// Check if target is a group (format: group:123)
|
|
865
873
|
const groupMatch = target.match(/^group:(\d+)/i);
|
|
866
874
|
if (groupMatch) {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /infoflow-changelog command
|
|
3
|
+
*
|
|
4
|
+
* 读取 CHANGELOG.md,展示最近几次更新内容。
|
|
5
|
+
*
|
|
6
|
+
* 用法:
|
|
7
|
+
* /infoflow-changelog — 最近 5 次更新
|
|
8
|
+
* /infoflow-changelog all — 全部更新记录
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
12
|
+
import { resolve, dirname } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const CHANGELOG_PATH = resolve(__dirname, "../../CHANGELOG.md");
|
|
17
|
+
const DEFAULT_SHOW = 5;
|
|
18
|
+
|
|
19
|
+
export async function runChangelogCommand(ctx: {
|
|
20
|
+
args?: string;
|
|
21
|
+
}): Promise<{ text: string }> {
|
|
22
|
+
const showAll = ctx.args?.trim().toLowerCase() === "all";
|
|
23
|
+
|
|
24
|
+
if (!existsSync(CHANGELOG_PATH)) {
|
|
25
|
+
return { text: "**更新日志**\n\n暂无更新记录。" };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let content: string;
|
|
29
|
+
try {
|
|
30
|
+
content = readFileSync(CHANGELOG_PATH, "utf-8");
|
|
31
|
+
} catch (err: unknown) {
|
|
32
|
+
return {
|
|
33
|
+
text: `**更新日志**\n\n读取更新日志失败:${err instanceof Error ? err.message : String(err)}`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 按 "## " 版本标题分割,跳过第一个(文件标题行)
|
|
38
|
+
const sections = content.split(/^## /m).filter(Boolean);
|
|
39
|
+
|
|
40
|
+
if (sections.length === 0) {
|
|
41
|
+
return { text: "**Infoflow 更新日志**\n\n暂无更新记录。" };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const toShow = showAll ? sections : sections.slice(0, DEFAULT_SHOW);
|
|
45
|
+
const body = toShow.map((s) => `## ${s.trimEnd()}`).join("\n\n");
|
|
46
|
+
|
|
47
|
+
const hint =
|
|
48
|
+
!showAll && sections.length > DEFAULT_SHOW
|
|
49
|
+
? `\n\n> 仅显示最近 ${toShow.length} 次更新,发送 \`/infoflow-changelog all\` 查看全部 ${sections.length} 次更新记录。`
|
|
50
|
+
: "";
|
|
51
|
+
|
|
52
|
+
return { text: `**Infoflow 更新日志**\n\n${body}${hint}` };
|
|
53
|
+
}
|