@tencent-weixin/openclaw-weixin 2.4.1 → 2.4.3
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/index.js +0 -4
- package/dist/index.js.map +1 -1
- package/dist/src/api/api.js +41 -7
- package/dist/src/api/api.js.map +1 -1
- package/dist/src/auth/accounts.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 +19 -0
- package/dist/src/channel.js.map +1 -1
- package/dist/src/media/voice-outbound.js +177 -0
- package/dist/src/media/voice-outbound.js.map +1 -0
- package/dist/src/messaging/abort-fence.js +70 -0
- package/dist/src/messaging/abort-fence.js.map +1 -0
- package/dist/src/messaging/buttons.js +117 -0
- package/dist/src/messaging/buttons.js.map +1 -0
- package/dist/src/messaging/lane-key.js +66 -0
- package/dist/src/messaging/lane-key.js.map +1 -0
- package/dist/src/messaging/merged-record.js +149 -0
- package/dist/src/messaging/merged-record.js.map +1 -0
- package/dist/src/messaging/model-buttons.js +182 -0
- package/dist/src/messaging/model-buttons.js.map +1 -0
- package/dist/src/messaging/model-callback-handler.js +133 -0
- package/dist/src/messaging/model-callback-handler.js.map +1 -0
- package/dist/src/monitor/lane-scheduler.js +46 -0
- package/dist/src/monitor/lane-scheduler.js.map +1 -0
- package/dist/src/monitor/monitor.js +5 -12
- package/dist/src/monitor/monitor.js.map +1 -1
- package/dist/src/streaming/stream-pipeline.js +431 -0
- package/dist/src/streaming/stream-pipeline.js.map +1 -0
- package/dist/src/streaming/stream-session.js +260 -0
- package/dist/src/streaming/stream-session.js.map +1 -0
- package/dist/src/streaming/stream.js +239 -0
- package/dist/src/streaming/stream.js.map +1 -0
- package/dist/src/util/markdown-fences.js +54 -0
- package/dist/src/util/markdown-fences.js.map +1 -0
- package/index.ts +0 -5
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/api/api.ts +42 -8
- package/src/auth/accounts.ts +0 -1
- package/src/auth/login-qr.ts +8 -0
- package/src/channel.ts +22 -1
- package/src/monitor/monitor.ts +11 -10
- package/src/runtime.ts +0 -70
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();
|
|
@@ -201,11 +236,10 @@ function buildCommonHeaders(): Record<string, string> {
|
|
|
201
236
|
return headers;
|
|
202
237
|
}
|
|
203
238
|
|
|
204
|
-
function buildHeaders(opts: { token?: string
|
|
239
|
+
function buildHeaders(opts: { token?: string }): Record<string, string> {
|
|
205
240
|
const headers: Record<string, string> = {
|
|
206
241
|
"Content-Type": "application/json",
|
|
207
242
|
AuthorizationType: "ilink_bot_token",
|
|
208
|
-
"Content-Length": String(Buffer.byteLength(opts.body, "utf-8")),
|
|
209
243
|
"X-WECHAT-UIN": randomWechatUin(),
|
|
210
244
|
...buildCommonHeaders(),
|
|
211
245
|
};
|
|
@@ -277,7 +311,7 @@ export async function apiPostFetch(params: {
|
|
|
277
311
|
}): Promise<string> {
|
|
278
312
|
const base = ensureTrailingSlash(params.baseUrl);
|
|
279
313
|
const url = new URL(params.endpoint, base);
|
|
280
|
-
const hdrs = buildHeaders({ token: params.token
|
|
314
|
+
const hdrs = buildHeaders({ token: params.token });
|
|
281
315
|
logger.debug(`POST ${redactUrl(url.toString())} body=${redactBody(params.body)}`);
|
|
282
316
|
|
|
283
317
|
const controller =
|
package/src/auth/accounts.ts
CHANGED
|
@@ -4,7 +4,6 @@ import path from "node:path";
|
|
|
4
4
|
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
5
5
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
6
6
|
|
|
7
|
-
import { getWeixinRuntime } from "../runtime.js";
|
|
8
7
|
import { resolveStateDir } from "../storage/state-dir.js";
|
|
9
8
|
import { resolveFrameworkAllowFromPath } from "./pairing.js";
|
|
10
9
|
import { logger } from "../util/logger.js";
|
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
|
-
import type { ChannelPlugin, OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
3
|
+
import type { ChannelPlugin, OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/core";
|
|
4
4
|
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
5
5
|
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/infra-runtime";
|
|
6
6
|
|
|
@@ -373,6 +373,15 @@ export const weixinPlugin: ChannelPlugin<ResolvedWeixinAccount> = {
|
|
|
373
373
|
);
|
|
374
374
|
log(`⚠️ 保存账号数据失败: ${String(err)}`);
|
|
375
375
|
}
|
|
376
|
+
} else if (waitResult.alreadyConnected) {
|
|
377
|
+
// Server confirmed this OpenClaw is already bound to the scanned bot;
|
|
378
|
+
// local credentials are intact, nothing to persist. Exit successfully
|
|
379
|
+
// so that automated installers don't treat re-runs as login failures.
|
|
380
|
+
// The QR poller already wrote the user-facing message to stdout, so
|
|
381
|
+
// we deliberately do NOT echo it again via `log(...)`.
|
|
382
|
+
logger.info(
|
|
383
|
+
`auth.login: bot already connected to this OpenClaw accountId=${account.accountId}`,
|
|
384
|
+
);
|
|
376
385
|
} else {
|
|
377
386
|
logger.warn(
|
|
378
387
|
`auth.login: login did not complete accountId=${account.accountId} message=${waitResult.message}`,
|
|
@@ -428,6 +437,17 @@ export const weixinPlugin: ChannelPlugin<ResolvedWeixinAccount> = {
|
|
|
428
437
|
const logPath = aLog.getLogFilePath();
|
|
429
438
|
ctx.log?.info?.(`[${account.accountId}] weixin logs: ${logPath}`);
|
|
430
439
|
|
|
440
|
+
// The gateway injects the channel runtime surface per-call (task-scoped). We require it:
|
|
441
|
+
// it carries reply/routing/session/media/commands helpers used by processOneMessage.
|
|
442
|
+
// Available on hosts >= 2026.2.19 (our peerDependency is >= 2026.3.22).
|
|
443
|
+
if (!ctx.channelRuntime) {
|
|
444
|
+
const msg = `ctx.channelRuntime missing — host too old or plugin SDK contract violated`;
|
|
445
|
+
aLog.error(msg);
|
|
446
|
+
ctx.log?.error?.(`[${account.accountId}] ${msg}`);
|
|
447
|
+
ctx.setStatus?.({ accountId: account.accountId, running: false });
|
|
448
|
+
throw new Error(msg);
|
|
449
|
+
}
|
|
450
|
+
|
|
431
451
|
const { monitorWeixinProvider } = await import("./monitor/monitor.js");
|
|
432
452
|
return monitorWeixinProvider({
|
|
433
453
|
baseUrl: account.baseUrl,
|
|
@@ -436,6 +456,7 @@ export const weixinPlugin: ChannelPlugin<ResolvedWeixinAccount> = {
|
|
|
436
456
|
accountId: account.accountId,
|
|
437
457
|
config: ctx.cfg,
|
|
438
458
|
runtime: ctx.runtime,
|
|
459
|
+
channelRuntime: ctx.channelRuntime as unknown as PluginRuntime["channel"],
|
|
439
460
|
abortSignal: ctx.abortSignal,
|
|
440
461
|
setStatus: ctx.setStatus,
|
|
441
462
|
});
|
package/src/monitor/monitor.ts
CHANGED
|
@@ -5,7 +5,6 @@ import { getUpdates } from "../api/api.js";
|
|
|
5
5
|
import { WeixinConfigManager } from "../api/config-cache.js";
|
|
6
6
|
import { SESSION_EXPIRED_ERRCODE, pauseSession, getRemainingPauseMs } from "../api/session-guard.js";
|
|
7
7
|
import { processOneMessage } from "../messaging/process-message.js";
|
|
8
|
-
import { getWeixinRuntime, waitForWeixinRuntime } from "../runtime.js";
|
|
9
8
|
import { getSyncBufFilePath, loadGetUpdatesBuf, saveGetUpdatesBuf } from "../storage/sync-buf.js";
|
|
10
9
|
import { logger } from "../util/logger.js";
|
|
11
10
|
import type { Logger } from "../util/logger.js";
|
|
@@ -25,6 +24,11 @@ export type MonitorWeixinOpts = {
|
|
|
25
24
|
allowFrom?: string[];
|
|
26
25
|
config: import("openclaw/plugin-sdk/core").OpenClawConfig;
|
|
27
26
|
runtime?: { log?: (msg: string) => void; error?: (msg: string) => void };
|
|
27
|
+
/**
|
|
28
|
+
* Gateway-injected channel runtime surface (reply/routing/session/media/commands/...).
|
|
29
|
+
* Required for inbound message processing; provided by `ChannelGatewayContext.channelRuntime`.
|
|
30
|
+
*/
|
|
31
|
+
channelRuntime: PluginRuntime["channel"];
|
|
28
32
|
abortSignal?: AbortSignal;
|
|
29
33
|
longPollTimeoutMs?: number;
|
|
30
34
|
/** Gateway status callback — called on each successful poll and inbound message. */
|
|
@@ -42,6 +46,7 @@ export async function monitorWeixinProvider(opts: MonitorWeixinOpts): Promise<vo
|
|
|
42
46
|
token,
|
|
43
47
|
accountId,
|
|
44
48
|
config,
|
|
49
|
+
channelRuntime,
|
|
45
50
|
abortSignal,
|
|
46
51
|
longPollTimeoutMs,
|
|
47
52
|
setStatus,
|
|
@@ -50,15 +55,11 @@ export async function monitorWeixinProvider(opts: MonitorWeixinOpts): Promise<vo
|
|
|
50
55
|
const errLog = opts.runtime?.error ?? ((m: string) => log(m));
|
|
51
56
|
const aLog: Logger = logger.withAccount(accountId);
|
|
52
57
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
aLog.info(`Weixin runtime acquired, channelRuntime type: ${typeof channelRuntime}`);
|
|
59
|
-
} catch (err) {
|
|
60
|
-
aLog.error(`waitForWeixinRuntime() failed: ${String(err)}`);
|
|
61
|
-
throw err;
|
|
58
|
+
if (!channelRuntime) {
|
|
59
|
+
const msg =
|
|
60
|
+
"channelRuntime missing on monitor opts; gateway must inject ChannelGatewayContext.channelRuntime";
|
|
61
|
+
aLog.error(msg);
|
|
62
|
+
throw new Error(msg);
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
log(`weixin monitor started (${baseUrl}, account=${accountId})`);
|
package/src/runtime.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import type { PluginRuntime } from "openclaw/plugin-sdk/core";
|
|
2
|
-
|
|
3
|
-
import { logger } from "./util/logger.js";
|
|
4
|
-
|
|
5
|
-
let pluginRuntime: PluginRuntime | null = null;
|
|
6
|
-
|
|
7
|
-
export type PluginChannelRuntime = PluginRuntime["channel"];
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Sets the global Weixin runtime (called from plugin register).
|
|
11
|
-
*/
|
|
12
|
-
export function setWeixinRuntime(next: PluginRuntime): void {
|
|
13
|
-
pluginRuntime = next;
|
|
14
|
-
logger.info(`[runtime] setWeixinRuntime called, runtime set successfully`);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Gets the global Weixin runtime (throws if not initialized).
|
|
19
|
-
*/
|
|
20
|
-
export function getWeixinRuntime(): PluginRuntime {
|
|
21
|
-
if (!pluginRuntime) {
|
|
22
|
-
throw new Error("Weixin runtime not initialized");
|
|
23
|
-
}
|
|
24
|
-
return pluginRuntime;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const WAIT_INTERVAL_MS = 100;
|
|
28
|
-
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Waits for the Weixin runtime to be initialized (async polling).
|
|
32
|
-
*/
|
|
33
|
-
export async function waitForWeixinRuntime(
|
|
34
|
-
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
35
|
-
): Promise<PluginRuntime> {
|
|
36
|
-
const start = Date.now();
|
|
37
|
-
while (!pluginRuntime) {
|
|
38
|
-
if (Date.now() - start > timeoutMs) {
|
|
39
|
-
throw new Error("Weixin runtime initialization timeout");
|
|
40
|
-
}
|
|
41
|
-
await new Promise((resolve) => setTimeout(resolve, WAIT_INTERVAL_MS));
|
|
42
|
-
}
|
|
43
|
-
return pluginRuntime;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Resolves `PluginRuntime["channel"]` for the long-poll monitor.
|
|
48
|
-
*
|
|
49
|
-
* Prefer the gateway-injected `channelRuntime` on `ChannelGatewayContext` when present (avoids
|
|
50
|
-
* races with the module-global from `register()`). Fall back to the global set by `setWeixinRuntime()`,
|
|
51
|
-
* then to a short wait for legacy hosts.
|
|
52
|
-
*/
|
|
53
|
-
export async function resolveWeixinChannelRuntime(params: {
|
|
54
|
-
channelRuntime?: PluginChannelRuntime;
|
|
55
|
-
waitTimeoutMs?: number;
|
|
56
|
-
}): Promise<PluginChannelRuntime> {
|
|
57
|
-
if (params.channelRuntime) {
|
|
58
|
-
logger.debug("[runtime] channelRuntime from gateway context");
|
|
59
|
-
return params.channelRuntime;
|
|
60
|
-
}
|
|
61
|
-
if (pluginRuntime) {
|
|
62
|
-
logger.debug("[runtime] channelRuntime from register() global");
|
|
63
|
-
return pluginRuntime.channel;
|
|
64
|
-
}
|
|
65
|
-
logger.warn(
|
|
66
|
-
"[runtime] no channelRuntime on ctx and no global runtime yet; waiting for register()",
|
|
67
|
-
);
|
|
68
|
-
const pr = await waitForWeixinRuntime(params.waitTimeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
69
|
-
return pr.channel;
|
|
70
|
-
}
|