@tencent-weixin/openclaw-weixin 2.4.2 → 2.4.4
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 +45 -0
- package/CHANGELOG.zh_CN.md +45 -0
- package/dist/src/api/api.js +84 -8
- package/dist/src/api/api.js.map +1 -1
- package/dist/src/api/types.js +2 -0
- package/dist/src/api/types.js.map +1 -1
- package/dist/src/auth/login-qr.js +1 -0
- package/dist/src/auth/login-qr.js.map +1 -1
- package/dist/src/channel.js +15 -1
- package/dist/src/channel.js.map +1 -1
- package/dist/src/config/config-schema.js +1 -0
- package/dist/src/config/config-schema.js.map +1 -1
- package/dist/src/config/reply-progress.js +5 -0
- package/dist/src/config/reply-progress.js.map +1 -0
- package/dist/src/media/voice-outbound.js +98 -1
- package/dist/src/media/voice-outbound.js.map +1 -1
- package/dist/src/messaging/batch-session.js +271 -0
- package/dist/src/messaging/batch-session.js.map +1 -0
- package/dist/src/messaging/error-notice.js +1 -0
- package/dist/src/messaging/error-notice.js.map +1 -1
- package/dist/src/messaging/outbound-hooks.js +2 -0
- package/dist/src/messaging/outbound-hooks.js.map +1 -1
- package/dist/src/messaging/process-message.js +32 -7
- package/dist/src/messaging/process-message.js.map +1 -1
- package/dist/src/messaging/reply-progress-sender.js +93 -0
- package/dist/src/messaging/reply-progress-sender.js.map +1 -0
- package/dist/src/messaging/run-context.js +9 -0
- package/dist/src/messaging/run-context.js.map +1 -0
- package/dist/src/messaging/run-report-session.js +123 -0
- package/dist/src/messaging/run-report-session.js.map +1 -0
- package/dist/src/messaging/send.js +40 -2
- package/dist/src/messaging/send.js.map +1 -1
- package/dist/src/monitor/monitor.js +3 -0
- package/dist/src/monitor/monitor.js.map +1 -1
- package/dist/src/streaming/batch-session.js +271 -0
- package/dist/src/streaming/batch-session.js.map +1 -0
- package/dist/src/streaming/stream-piece.js +54 -0
- package/dist/src/streaming/stream-piece.js.map +1 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/api/api.ts +92 -8
- package/src/api/types.ts +16 -0
- package/src/auth/login-qr.ts +8 -0
- package/src/channel.ts +16 -1
- package/src/config/config-schema.ts +1 -0
- package/src/config/reply-progress.ts +10 -0
- package/src/messaging/error-notice.ts +2 -0
- package/src/messaging/outbound-hooks.ts +4 -0
- package/src/messaging/process-message.ts +32 -7
- package/src/messaging/reply-progress-sender.ts +122 -0
- package/src/messaging/send-media.ts +1 -1
- package/src/messaging/send.ts +60 -7
- package/src/monitor/monitor.ts +3 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { onAgentEvent, } from "openclaw/plugin-sdk/agent-harness-runtime";
|
|
3
|
+
import { MessageItemType } from "../api/types.js";
|
|
4
|
+
import { logger } from "../util/logger.js";
|
|
5
|
+
/** Max chars for a tool result summary before truncation. */
|
|
6
|
+
const TOOL_OUTPUT_CHAR_LIMIT = 120_000;
|
|
7
|
+
/** Default size budget for a single batch sendMessage (bytes, JSON estimate). */
|
|
8
|
+
export const BATCH_SIZE_LIMIT_BYTES = 200 * 1024;
|
|
9
|
+
function estimateSizeBytes(items) {
|
|
10
|
+
try {
|
|
11
|
+
return Buffer.byteLength(JSON.stringify(items), "utf8");
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function truncateText(text, limit) {
|
|
18
|
+
if (text.length <= limit)
|
|
19
|
+
return text;
|
|
20
|
+
return `${text.slice(0, limit)}\n\n… truncated (${text.length} chars, showing first ${limit}).`;
|
|
21
|
+
}
|
|
22
|
+
function formatToolOutput(value) {
|
|
23
|
+
if (value === null || value === undefined)
|
|
24
|
+
return "";
|
|
25
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
26
|
+
return String(value);
|
|
27
|
+
let text;
|
|
28
|
+
if (typeof value === "string") {
|
|
29
|
+
text = value;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
if (typeof value === "object") {
|
|
33
|
+
const rec = value;
|
|
34
|
+
if (typeof rec.text === "string") {
|
|
35
|
+
text = rec.text;
|
|
36
|
+
}
|
|
37
|
+
else if (Array.isArray(rec.content)) {
|
|
38
|
+
const parts = rec.content
|
|
39
|
+
.map((item) => {
|
|
40
|
+
if (item && typeof item === "object") {
|
|
41
|
+
const e = item;
|
|
42
|
+
if (e.type === "text" && typeof e.text === "string")
|
|
43
|
+
return e.text;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
})
|
|
47
|
+
.filter((p) => p !== null);
|
|
48
|
+
text = parts.length > 0 ? parts.join("\n") : JSON.stringify(value, null, 2);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
text = JSON.stringify(value, null, 2);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
text = String(value);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return truncateText(text, TOOL_OUTPUT_CHAR_LIMIT);
|
|
59
|
+
}
|
|
60
|
+
function tryStringifyArgs(args) {
|
|
61
|
+
if (args === undefined)
|
|
62
|
+
return undefined;
|
|
63
|
+
try {
|
|
64
|
+
return JSON.stringify(args);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* BatchSession — accumulates thinking, tool calls, and text payloads during
|
|
72
|
+
* an AI turn and sends them all in a single sendMessage at finalize time.
|
|
73
|
+
*
|
|
74
|
+
* Drop priority when item_list > BATCH_SIZE_LIMIT_BYTES:
|
|
75
|
+
* 1. Drop ThinkingItem (largest, optional for client UX)
|
|
76
|
+
* 2. Drop all ToolCallStart/Result items
|
|
77
|
+
* 3. Always keep TextItem(s) — the actual reply
|
|
78
|
+
*
|
|
79
|
+
* Usage:
|
|
80
|
+
* const session = new BatchSession({ onSendBatch });
|
|
81
|
+
* // spread replyCallbacks + runId into replyOptions
|
|
82
|
+
* replyOptions = { ...replyOptions, runId: session.runId, ...session.replyCallbacks }
|
|
83
|
+
* // in deliver(): session.addTextPayload(text)
|
|
84
|
+
* // after dispatchReplyFromConfig: await session.finalize()
|
|
85
|
+
* // on error: await session.abort()
|
|
86
|
+
*/
|
|
87
|
+
export class BatchSession {
|
|
88
|
+
/** Run id this session listens for on the global agent event bus. */
|
|
89
|
+
runId;
|
|
90
|
+
thinkingText = null;
|
|
91
|
+
toolStarts = [];
|
|
92
|
+
toolResults = [];
|
|
93
|
+
textPayloads = [];
|
|
94
|
+
done = false;
|
|
95
|
+
unsubscribeAgentEvent;
|
|
96
|
+
onSendBatch;
|
|
97
|
+
constructor(deps) {
|
|
98
|
+
this.runId = deps.runId ?? crypto.randomUUID();
|
|
99
|
+
this.onSendBatch = deps.onSendBatch;
|
|
100
|
+
this.unsubscribeAgentEvent = onAgentEvent((evt) => this.handleRawAgentEvent(evt));
|
|
101
|
+
logger.debug(`BatchSession.ctor: runId=${this.runId} agentEventListener=registered`);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Spread into replyOptions so the SDK delivers thinking/tool events here.
|
|
105
|
+
* Also set `replyOptions.runId = session.runId` so agent events carry the
|
|
106
|
+
* matching runId.
|
|
107
|
+
*/
|
|
108
|
+
get replyCallbacks() {
|
|
109
|
+
return {
|
|
110
|
+
onReasoningStream: (payload) => this.handleReasoningStream(payload),
|
|
111
|
+
onReasoningEnd: () => { },
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Store a text payload from deliver(). Each call appends one TextItem to the
|
|
116
|
+
* final item_list in call order. Skips empty strings.
|
|
117
|
+
*/
|
|
118
|
+
addTextPayload(text) {
|
|
119
|
+
if (!text.trim())
|
|
120
|
+
return;
|
|
121
|
+
this.textPayloads.push(text);
|
|
122
|
+
logger.debug(`BatchSession.addTextPayload: len=${text.length} total=${this.textPayloads.length}`);
|
|
123
|
+
}
|
|
124
|
+
// ---- private callbacks --------------------------------------------------
|
|
125
|
+
handleReasoningStream(payload) {
|
|
126
|
+
if (this.done)
|
|
127
|
+
return;
|
|
128
|
+
const text = typeof payload.text === "string" ? payload.text : "";
|
|
129
|
+
if (text) {
|
|
130
|
+
this.thinkingText = text;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
handleRawAgentEvent(evt) {
|
|
134
|
+
if (this.done)
|
|
135
|
+
return;
|
|
136
|
+
if (evt.runId !== this.runId)
|
|
137
|
+
return;
|
|
138
|
+
if (evt.stream !== "tool")
|
|
139
|
+
return;
|
|
140
|
+
const data = evt.data;
|
|
141
|
+
if (typeof data.toolCallId !== "string" || !data.toolCallId)
|
|
142
|
+
return;
|
|
143
|
+
if (data.phase === "start") {
|
|
144
|
+
this.toolStarts.push({
|
|
145
|
+
tool_call_id: data.toolCallId,
|
|
146
|
+
tool_name: typeof data.name === "string" && data.name ? data.name : "tool",
|
|
147
|
+
args_json: tryStringifyArgs(data.args),
|
|
148
|
+
});
|
|
149
|
+
logger.debug(`BatchSession: tool_call_start tool=${data.name ?? "?"} id=${data.toolCallId}` +
|
|
150
|
+
` totalStarts=${this.toolStarts.length}`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (data.phase === "result") {
|
|
154
|
+
this.toolResults.push({
|
|
155
|
+
tool_call_id: data.toolCallId,
|
|
156
|
+
summary: formatToolOutput(data.result),
|
|
157
|
+
});
|
|
158
|
+
logger.debug(`BatchSession: tool_call_result id=${data.toolCallId}` +
|
|
159
|
+
` totalResults=${this.toolResults.length}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// ---- item_list assembly -------------------------------------------------
|
|
163
|
+
/**
|
|
164
|
+
* Build the item_list applying the 200 KB budget.
|
|
165
|
+
*
|
|
166
|
+
* Order: [ThinkingItem?, ToolCallStart+Result pairs..., TextItem(s)...]
|
|
167
|
+
*
|
|
168
|
+
* If full list > maxBytes:
|
|
169
|
+
* - drop ThinkingItem and retry
|
|
170
|
+
* - if still > maxBytes, drop all tool items too
|
|
171
|
+
* - TextItem(s) are always kept
|
|
172
|
+
*/
|
|
173
|
+
buildItemList(maxBytes = BATCH_SIZE_LIMIT_BYTES) {
|
|
174
|
+
const textItems = this.textPayloads.map((t) => ({
|
|
175
|
+
type: MessageItemType.TEXT,
|
|
176
|
+
text_item: { text: t },
|
|
177
|
+
}));
|
|
178
|
+
const toolItems = [];
|
|
179
|
+
for (const start of this.toolStarts) {
|
|
180
|
+
toolItems.push({
|
|
181
|
+
type: MessageItemType.TOOL_CALL_START,
|
|
182
|
+
tool_call_start_item: {
|
|
183
|
+
tool_name: start.tool_name,
|
|
184
|
+
tool_call_id: start.tool_call_id,
|
|
185
|
+
args_json: start.args_json,
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
const result = this.toolResults.find((r) => r.tool_call_id === start.tool_call_id);
|
|
189
|
+
if (result) {
|
|
190
|
+
toolItems.push({
|
|
191
|
+
type: MessageItemType.TOOL_CALL_RESULT,
|
|
192
|
+
tool_call_result_item: {
|
|
193
|
+
tool_call_id: result.tool_call_id,
|
|
194
|
+
summary: result.summary,
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const thinkingItem = this.thinkingText
|
|
200
|
+
? { type: MessageItemType.THINKING, thinking_item: { text: this.thinkingText } }
|
|
201
|
+
: null;
|
|
202
|
+
// Attempt 1: full list
|
|
203
|
+
const full = [
|
|
204
|
+
...(thinkingItem ? [thinkingItem] : []),
|
|
205
|
+
...toolItems,
|
|
206
|
+
...textItems,
|
|
207
|
+
];
|
|
208
|
+
if (full.length === 0)
|
|
209
|
+
return [];
|
|
210
|
+
const fullSize = estimateSizeBytes(full);
|
|
211
|
+
logger.debug(`BatchSession.buildItemList: items=${full.length} size=${fullSize}B` +
|
|
212
|
+
` thinking=${thinkingItem !== null} tools=${this.toolStarts.length}` +
|
|
213
|
+
` texts=${this.textPayloads.length}`);
|
|
214
|
+
if (fullSize <= maxBytes)
|
|
215
|
+
return full;
|
|
216
|
+
// Attempt 2: drop thinking
|
|
217
|
+
logger.warn(`BatchSession.buildItemList: size=${fullSize}B > limit=${maxBytes}B,` +
|
|
218
|
+
` dropping ThinkingItem (len=${this.thinkingText?.length ?? 0} chars).` +
|
|
219
|
+
` Full thinking content is in the log: ${logger.getLogFilePath()}`);
|
|
220
|
+
const withoutThinking = [...toolItems, ...textItems];
|
|
221
|
+
if (withoutThinking.length === 0)
|
|
222
|
+
return [];
|
|
223
|
+
const noThinkingSize = estimateSizeBytes(withoutThinking);
|
|
224
|
+
if (noThinkingSize <= maxBytes)
|
|
225
|
+
return withoutThinking;
|
|
226
|
+
// Attempt 3: drop tool items too
|
|
227
|
+
logger.warn(`BatchSession.buildItemList: size=${noThinkingSize}B still > limit=${maxBytes}B,` +
|
|
228
|
+
` dropping ${this.toolStarts.length} tool call(s) (${toolItems.length} items).` +
|
|
229
|
+
` Full tool content is in the log: ${logger.getLogFilePath()}`);
|
|
230
|
+
return textItems.length > 0 ? textItems : [];
|
|
231
|
+
}
|
|
232
|
+
// ---- lifecycle ----------------------------------------------------------
|
|
233
|
+
/**
|
|
234
|
+
* Assemble item_list, call onSendBatch once, then clean up. Idempotent.
|
|
235
|
+
*/
|
|
236
|
+
async finalize() {
|
|
237
|
+
if (this.done)
|
|
238
|
+
return;
|
|
239
|
+
this.done = true;
|
|
240
|
+
this.cleanup();
|
|
241
|
+
const items = this.buildItemList();
|
|
242
|
+
if (items.length === 0) {
|
|
243
|
+
logger.debug("BatchSession.finalize: item_list empty, skipping send");
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
logger.debug(`BatchSession.finalize: sending ${items.length} item(s)` +
|
|
247
|
+
` thinking=${this.thinkingText !== null}` +
|
|
248
|
+
` toolStarts=${this.toolStarts.length}` +
|
|
249
|
+
` texts=${this.textPayloads.length}`);
|
|
250
|
+
await this.onSendBatch(items);
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Discard all buffered data, release event listener. Does not send. Idempotent.
|
|
254
|
+
*/
|
|
255
|
+
async abort() {
|
|
256
|
+
if (this.done)
|
|
257
|
+
return;
|
|
258
|
+
this.done = true;
|
|
259
|
+
this.cleanup();
|
|
260
|
+
logger.debug("BatchSession.abort: discarded without send");
|
|
261
|
+
}
|
|
262
|
+
cleanup() {
|
|
263
|
+
try {
|
|
264
|
+
this.unsubscribeAgentEvent();
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
logger.warn(`BatchSession.cleanup: unsubscribe failed err=${String(err)}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
//# sourceMappingURL=batch-session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"batch-session.js","sourceRoot":"","sources":["../../../src/streaming/batch-session.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stream piece JSON schema — what gets base64-encoded into PieceItem.piece_data
|
|
3
|
+
* and pushed onto the ilink uplink stream.
|
|
4
|
+
*
|
|
5
|
+
* The protocol distinguishes 7 `kind`s:
|
|
6
|
+
*
|
|
7
|
+
* Lifecycle (streaming, accumulative — multiple pieces per node):
|
|
8
|
+
* - "result" agent's final reply text stream
|
|
9
|
+
* - "thinking" agent's reasoning block stream
|
|
10
|
+
*
|
|
11
|
+
* Discrete (single-frame — exactly 1 piece per occurrence):
|
|
12
|
+
* - "tool_call_start" tool invocation start snapshot
|
|
13
|
+
* - "tool_call_result" tool invocation result snapshot
|
|
14
|
+
*
|
|
15
|
+
* Piece-only (no companion sendMessage / KV write):
|
|
16
|
+
* - "tool_progress" command stdout slices / patch file list
|
|
17
|
+
* - "compaction_start" context compaction begin signal
|
|
18
|
+
* - "compaction_end" context compaction end signal
|
|
19
|
+
*
|
|
20
|
+
* Each piece carries `client_id` (the renderer node id; equals the companion
|
|
21
|
+
* sendMessage's client_id when one exists), `root_id` (= the stream_start
|
|
22
|
+
* signal's client_id, shared across the whole stream), and an optional
|
|
23
|
+
* `parent_id` (subagent only, unused this iteration).
|
|
24
|
+
*
|
|
25
|
+
* Field naming follows pb conventions (snake_case) so the same wire format is
|
|
26
|
+
* readable on every consumer language.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Parse the SDK's `itemId` (as seen on `onItemEvent`) and extract the
|
|
30
|
+
* `tool_call_id` + `item_kind` so the channel can build a `tool_progress` piece.
|
|
31
|
+
*
|
|
32
|
+
* The SDK encodes the source kind as a prefix:
|
|
33
|
+
* - "command:<callId>" → command output progress
|
|
34
|
+
* - "patch:<callId>" → apply_patch file list summary
|
|
35
|
+
*
|
|
36
|
+
* Other prefixes (e.g. "tool:..." / "search:..." / "analysis:...") and
|
|
37
|
+
* unprefixed values return null — the caller MUST then no-op (skip the piece)
|
|
38
|
+
* to honor the "only command + patch" filter described in the plan.
|
|
39
|
+
*/
|
|
40
|
+
export function parseToolCallIdFromItemId(itemId) {
|
|
41
|
+
if (typeof itemId !== "string" || itemId.length === 0)
|
|
42
|
+
return null;
|
|
43
|
+
const m = /^(command|patch):(.+)$/.exec(itemId);
|
|
44
|
+
if (!m)
|
|
45
|
+
return null;
|
|
46
|
+
return { item_kind: m[1], tool_call_id: m[2] };
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Encode a `StreamPiece` to the base64 string consumed by `PieceItem.piece_data`.
|
|
50
|
+
*/
|
|
51
|
+
export function encodeStreamPiece(piece) {
|
|
52
|
+
return Buffer.from(JSON.stringify(piece), "utf-8").toString("base64");
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=stream-piece.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stream-piece.js","sourceRoot":"","sources":["../../../src/streaming/stream-piece.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AA2HH;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,yBAAyB,CAAC,MAAc;IACtD,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACnE,MAAM,CAAC,GAAG,wBAAwB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAChD,IAAI,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACpB,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,CAAwB,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AACxE,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAkB;IAClD,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AACxE,CAAC"}
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/src/api/api.ts
CHANGED
|
@@ -38,14 +38,49 @@ interface PackageJson {
|
|
|
38
38
|
ilink_appid?: string;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Identify whether a parsed package.json belongs to this plugin.
|
|
43
|
+
*
|
|
44
|
+
* The walk-up search may pass through unrelated `package.json` files
|
|
45
|
+
* (e.g. nested `node_modules/<dep>/package.json`); only ours is accepted.
|
|
46
|
+
*/
|
|
47
|
+
function isOwnPackageJson(parsed: PackageJson): boolean {
|
|
48
|
+
if (parsed.ilink_appid !== undefined) return true;
|
|
49
|
+
return typeof parsed.name === "string" && parsed.name.includes("openclaw-weixin");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Walk up from `startDir` searching for the plugin's own `package.json`.
|
|
54
|
+
*
|
|
55
|
+
* Resilient to differing layouts between dev (TS source under `src/`) and
|
|
56
|
+
* publish (compiled output under `dist/src/`) by not assuming a fixed depth.
|
|
57
|
+
*/
|
|
58
|
+
export function readPackageJsonFromDir(startDir: string): PackageJson {
|
|
42
59
|
try {
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
60
|
+
let dir = startDir;
|
|
61
|
+
const { root } = path.parse(dir);
|
|
62
|
+
while (dir && dir !== root) {
|
|
63
|
+
const candidate = path.join(dir, "package.json");
|
|
64
|
+
if (fs.existsSync(candidate)) {
|
|
65
|
+
try {
|
|
66
|
+
const parsed = JSON.parse(fs.readFileSync(candidate, "utf-8")) as PackageJson;
|
|
67
|
+
if (isOwnPackageJson(parsed)) {
|
|
68
|
+
return parsed;
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
// Malformed package.json — keep walking up.
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
dir = path.dirname(dir);
|
|
75
|
+
}
|
|
46
76
|
} catch {
|
|
47
|
-
|
|
77
|
+
// Fall through to empty default.
|
|
48
78
|
}
|
|
79
|
+
return {};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readPackageJson(): PackageJson {
|
|
83
|
+
return readPackageJsonFromDir(path.dirname(fileURLToPath(import.meta.url)));
|
|
49
84
|
}
|
|
50
85
|
|
|
51
86
|
const pkg = readPackageJson();
|
|
@@ -260,11 +295,42 @@ export async function apiGetFetch(params: {
|
|
|
260
295
|
}
|
|
261
296
|
}
|
|
262
297
|
|
|
298
|
+
/**
|
|
299
|
+
* Combine an internal timeout controller with an optional external abort signal.
|
|
300
|
+
* This lets gateway channel-stop aborts cancel in-flight long-poll requests
|
|
301
|
+
* immediately while preserving the existing timeout-driven AbortError path.
|
|
302
|
+
*/
|
|
303
|
+
function combineAbortSignals(params: {
|
|
304
|
+
internal?: AbortController;
|
|
305
|
+
external?: AbortSignal;
|
|
306
|
+
}): { signal?: AbortSignal; cleanup: () => void } {
|
|
307
|
+
const { internal, external } = params;
|
|
308
|
+
if (!external) {
|
|
309
|
+
return { signal: internal?.signal, cleanup: () => {} };
|
|
310
|
+
}
|
|
311
|
+
if (!internal) {
|
|
312
|
+
return { signal: external, cleanup: () => {} };
|
|
313
|
+
}
|
|
314
|
+
if (external.aborted) {
|
|
315
|
+
internal.abort();
|
|
316
|
+
return { signal: internal.signal, cleanup: () => {} };
|
|
317
|
+
}
|
|
318
|
+
const onExternalAbort = () => internal.abort();
|
|
319
|
+
external.addEventListener("abort", onExternalAbort, { once: true });
|
|
320
|
+
return {
|
|
321
|
+
signal: internal.signal,
|
|
322
|
+
cleanup: () => external.removeEventListener("abort", onExternalAbort),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
263
326
|
/**
|
|
264
327
|
* Common fetch wrapper: POST JSON to a Weixin API endpoint.
|
|
265
328
|
* When `timeoutMs` is provided, the request is aborted after that many milliseconds.
|
|
266
329
|
* When omitted, no client-side timeout is applied (relies on OS/TCP stack).
|
|
267
330
|
* Returns the raw response text on success; throws on HTTP error or timeout.
|
|
331
|
+
*
|
|
332
|
+
* When `abortSignal` is provided, an external abort (e.g. gateway channel stop)
|
|
333
|
+
* cancels the in-flight request immediately instead of waiting for `timeoutMs`.
|
|
268
334
|
*/
|
|
269
335
|
export async function apiPostFetch(params: {
|
|
270
336
|
baseUrl: string;
|
|
@@ -273,6 +339,7 @@ export async function apiPostFetch(params: {
|
|
|
273
339
|
token?: string;
|
|
274
340
|
timeoutMs?: number;
|
|
275
341
|
label: string;
|
|
342
|
+
abortSignal?: AbortSignal;
|
|
276
343
|
}): Promise<string> {
|
|
277
344
|
const base = ensureTrailingSlash(params.baseUrl);
|
|
278
345
|
const url = new URL(params.endpoint, base);
|
|
@@ -285,12 +352,16 @@ export async function apiPostFetch(params: {
|
|
|
285
352
|
controller != null && params.timeoutMs !== undefined
|
|
286
353
|
? setTimeout(() => controller.abort(), params.timeoutMs)
|
|
287
354
|
: undefined;
|
|
355
|
+
const { signal, cleanup } = combineAbortSignals({
|
|
356
|
+
internal: controller,
|
|
357
|
+
external: params.abortSignal,
|
|
358
|
+
});
|
|
288
359
|
try {
|
|
289
360
|
const res = await fetch(url.toString(), {
|
|
290
361
|
method: "POST",
|
|
291
362
|
headers: hdrs,
|
|
292
363
|
body: params.body,
|
|
293
|
-
...(
|
|
364
|
+
...(signal ? { signal } : {}),
|
|
294
365
|
});
|
|
295
366
|
if (t !== undefined) clearTimeout(t);
|
|
296
367
|
const rawText = await res.text();
|
|
@@ -302,6 +373,8 @@ export async function apiPostFetch(params: {
|
|
|
302
373
|
} catch (err) {
|
|
303
374
|
if (t !== undefined) clearTimeout(t);
|
|
304
375
|
throw err;
|
|
376
|
+
} finally {
|
|
377
|
+
cleanup();
|
|
305
378
|
}
|
|
306
379
|
}
|
|
307
380
|
|
|
@@ -316,6 +389,11 @@ export async function getUpdates(
|
|
|
316
389
|
baseUrl: string;
|
|
317
390
|
token?: string;
|
|
318
391
|
timeoutMs?: number;
|
|
392
|
+
/**
|
|
393
|
+
* Optional external abort signal from the gateway. When stopping the channel,
|
|
394
|
+
* this aborts the in-flight long-poll immediately so hot reload can restart.
|
|
395
|
+
*/
|
|
396
|
+
abortSignal?: AbortSignal;
|
|
319
397
|
},
|
|
320
398
|
): Promise<GetUpdatesResp> {
|
|
321
399
|
const timeout = params.timeoutMs ?? DEFAULT_LONG_POLL_TIMEOUT_MS;
|
|
@@ -330,13 +408,19 @@ export async function getUpdates(
|
|
|
330
408
|
token: params.token,
|
|
331
409
|
timeoutMs: timeout,
|
|
332
410
|
label: "getUpdates",
|
|
411
|
+
abortSignal: params.abortSignal,
|
|
333
412
|
});
|
|
334
413
|
const resp: GetUpdatesResp = JSON.parse(rawText);
|
|
335
414
|
return resp;
|
|
336
415
|
} catch (err) {
|
|
337
|
-
// Long-poll timeout
|
|
416
|
+
// Long-poll timeout or external abort are both normal control-flow exits.
|
|
417
|
+
// The monitor loop checks abortSignal after return and exits when needed.
|
|
338
418
|
if (err instanceof Error && err.name === "AbortError") {
|
|
339
|
-
|
|
419
|
+
if (params.abortSignal?.aborted) {
|
|
420
|
+
logger.debug(`getUpdates: aborted by external signal`);
|
|
421
|
+
} else {
|
|
422
|
+
logger.debug(`getUpdates: client-side timeout after ${timeout}ms, returning empty response`);
|
|
423
|
+
}
|
|
340
424
|
return { ret: 0, msgs: [], get_updates_buf: params.get_updates_buf };
|
|
341
425
|
}
|
|
342
426
|
throw err;
|
package/src/api/types.ts
CHANGED
|
@@ -74,6 +74,8 @@ export const MessageItemType = {
|
|
|
74
74
|
VOICE: 3,
|
|
75
75
|
FILE: 4,
|
|
76
76
|
VIDEO: 5,
|
|
77
|
+
TOOL_CALL_START: 11,
|
|
78
|
+
TOOL_CALL_RESULT: 12,
|
|
77
79
|
} as const;
|
|
78
80
|
|
|
79
81
|
export const MessageState = {
|
|
@@ -147,6 +149,17 @@ export interface RefMessage {
|
|
|
147
149
|
title?: string; // 摘要
|
|
148
150
|
}
|
|
149
151
|
|
|
152
|
+
export interface ToolCallStartItem {
|
|
153
|
+
tool_name?: string;
|
|
154
|
+
tool_call_id?: string;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface ToolCallResultItem {
|
|
158
|
+
tool_name?: string;
|
|
159
|
+
tool_call_id?: string;
|
|
160
|
+
status?: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
150
163
|
export interface MessageItem {
|
|
151
164
|
type?: number;
|
|
152
165
|
create_time_ms?: number;
|
|
@@ -159,6 +172,8 @@ export interface MessageItem {
|
|
|
159
172
|
voice_item?: VoiceItem;
|
|
160
173
|
file_item?: FileItem;
|
|
161
174
|
video_item?: VideoItem;
|
|
175
|
+
tool_call_start_item?: ToolCallStartItem;
|
|
176
|
+
tool_call_result_item?: ToolCallResultItem;
|
|
162
177
|
}
|
|
163
178
|
|
|
164
179
|
/** Unified message (proto: WeixinMessage). Replaces the old split Message + MessageContent + FullMessage. */
|
|
@@ -177,6 +192,7 @@ export interface WeixinMessage {
|
|
|
177
192
|
message_state?: number;
|
|
178
193
|
item_list?: MessageItem[];
|
|
179
194
|
context_token?: string;
|
|
195
|
+
run_id?: string;
|
|
180
196
|
}
|
|
181
197
|
|
|
182
198
|
/** GetUpdates request: bytes fields are base64 strings in JSON. */
|
package/src/auth/login-qr.ts
CHANGED
|
@@ -159,6 +159,13 @@ export type WeixinQrStartResult = {
|
|
|
159
159
|
|
|
160
160
|
export type WeixinQrWaitResult = {
|
|
161
161
|
connected: boolean;
|
|
162
|
+
/**
|
|
163
|
+
* Server reported `binded_redirect`: the scanned bot is already bound to
|
|
164
|
+
* this OpenClaw instance, so no new credentials are issued and existing
|
|
165
|
+
* local credentials remain valid. Callers should treat this as a successful
|
|
166
|
+
* outcome (semantically "already done") rather than a login failure.
|
|
167
|
+
*/
|
|
168
|
+
alreadyConnected?: boolean;
|
|
162
169
|
botToken?: string;
|
|
163
170
|
accountId?: string;
|
|
164
171
|
baseUrl?: string;
|
|
@@ -385,6 +392,7 @@ export async function waitForWeixinLogin(opts: {
|
|
|
385
392
|
activeLogins.delete(opts.sessionKey);
|
|
386
393
|
return {
|
|
387
394
|
connected: false,
|
|
395
|
+
alreadyConnected: true,
|
|
388
396
|
message: "已连接过此 OpenClaw,无需重复连接。",
|
|
389
397
|
};
|
|
390
398
|
}
|
package/src/channel.ts
CHANGED
|
@@ -167,7 +167,13 @@ export const weixinPlugin: ChannelPlugin<ResolvedWeixinAccount> = {
|
|
|
167
167
|
schema: {
|
|
168
168
|
type: "object",
|
|
169
169
|
additionalProperties: false,
|
|
170
|
-
properties: {
|
|
170
|
+
properties: {
|
|
171
|
+
replyProgressMessages: {
|
|
172
|
+
type: "boolean",
|
|
173
|
+
default: true,
|
|
174
|
+
description: "Send structured tool-call progress messages.",
|
|
175
|
+
},
|
|
176
|
+
},
|
|
171
177
|
},
|
|
172
178
|
},
|
|
173
179
|
capabilities: {
|
|
@@ -373,6 +379,15 @@ export const weixinPlugin: ChannelPlugin<ResolvedWeixinAccount> = {
|
|
|
373
379
|
);
|
|
374
380
|
log(`⚠️ 保存账号数据失败: ${String(err)}`);
|
|
375
381
|
}
|
|
382
|
+
} else if (waitResult.alreadyConnected) {
|
|
383
|
+
// Server confirmed this OpenClaw is already bound to the scanned bot;
|
|
384
|
+
// local credentials are intact, nothing to persist. Exit successfully
|
|
385
|
+
// so that automated installers don't treat re-runs as login failures.
|
|
386
|
+
// The QR poller already wrote the user-facing message to stdout, so
|
|
387
|
+
// we deliberately do NOT echo it again via `log(...)`.
|
|
388
|
+
logger.info(
|
|
389
|
+
`auth.login: bot already connected to this OpenClaw accountId=${account.accountId}`,
|
|
390
|
+
);
|
|
376
391
|
} else {
|
|
377
392
|
logger.warn(
|
|
378
393
|
`auth.login: login did not complete accountId=${account.accountId} message=${waitResult.message}`,
|
|
@@ -17,6 +17,7 @@ const weixinAccountSchema = z.object({
|
|
|
17
17
|
/** Top-level weixin config schema (token is stored in credentials file, not config). */
|
|
18
18
|
export const WeixinConfigSchema = weixinAccountSchema.extend({
|
|
19
19
|
accounts: z.record(z.string(), weixinAccountSchema).optional(),
|
|
20
|
+
replyProgressMessages: z.boolean().default(true),
|
|
20
21
|
/** ISO 8601; bumped on each successful login to refresh gateway config from disk. */
|
|
21
22
|
channelConfigUpdatedAt: z.string().optional(),
|
|
22
23
|
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
2
|
+
|
|
3
|
+
type WeixinChannelConfig = {
|
|
4
|
+
replyProgressMessages?: boolean;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export function resolveReplyProgressMessagesEnabled(cfg: OpenClawConfig): boolean {
|
|
8
|
+
const section = cfg.channels?.["openclaw-weixin"] as WeixinChannelConfig | undefined;
|
|
9
|
+
return section?.replyProgressMessages !== false;
|
|
10
|
+
}
|
|
@@ -12,6 +12,7 @@ export async function sendWeixinErrorNotice(params: {
|
|
|
12
12
|
message: string;
|
|
13
13
|
baseUrl: string;
|
|
14
14
|
token?: string;
|
|
15
|
+
runId?: string;
|
|
15
16
|
errLog: (m: string) => void;
|
|
16
17
|
}): Promise<void> {
|
|
17
18
|
if (!params.contextToken) {
|
|
@@ -22,6 +23,7 @@ export async function sendWeixinErrorNotice(params: {
|
|
|
22
23
|
baseUrl: params.baseUrl,
|
|
23
24
|
token: params.token,
|
|
24
25
|
contextToken: params.contextToken,
|
|
26
|
+
...(params.runId ? { runId: params.runId } : {}),
|
|
25
27
|
}});
|
|
26
28
|
logger.debug(`sendWeixinErrorNotice: sent to=${params.to}`);
|
|
27
29
|
} catch (err) {
|