Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions openclaw.plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,69 @@
"type": "object",
"additionalProperties": false,
"properties": {}
},
"channelConfigs": {
"openclaw-weixin": {
"label": "openclaw-weixin",
"description": "getUpdates long-poll upstream, sendMessage downstream; token auth.",
"schema": {
"type": "object",
"additionalProperties": false,
"properties": {
"name": {
"type": "string"
},
"enabled": {
"type": "boolean"
},
"baseUrl": {
"type": "string",
"default": "https://ilinkai.weixin.qq.com"
},
"cdnBaseUrl": {
"type": "string",
"default": "https://novac2c.cdn.weixin.qq.com/c2c"
},
"routeTag": {
"type": "number"
},
"blockStreaming": {
"type": "boolean"
},
"channelConfigUpdatedAt": {
"type": "string"
},
"accounts": {
"type": "object",
"additionalProperties": {
"type": "object",
"additionalProperties": false,
"properties": {
"name": {
"type": "string"
},
"enabled": {
"type": "boolean"
},
"baseUrl": {
"type": "string",
"default": "https://ilinkai.weixin.qq.com"
},
"cdnBaseUrl": {
"type": "string",
"default": "https://novac2c.cdn.weixin.qq.com/c2c"
},
"routeTag": {
"type": "number"
},
"blockStreaming": {
"type": "boolean"
}
}
}
}
}
}
}
}
}
2 changes: 2 additions & 0 deletions src/auth/accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,8 @@ type WeixinAccountConfig = {
name?: string;
enabled?: boolean;
cdnBaseUrl?: string;
/** Enable block replies before final turn completion. */
blockStreaming?: boolean;
/** Optional SKRouteTag source; read from openclaw.json when `accountId` is passed to `loadConfigRouteTag`. */
routeTag?: number | string;
};
Expand Down
10 changes: 3 additions & 7 deletions src/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import path from "node:path";

import type { ChannelPlugin, OpenClawConfig } from "openclaw/plugin-sdk/core";
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/infra-runtime";

import {
Expand All @@ -16,6 +17,7 @@ import {
} from "./auth/accounts.js";
import type { ResolvedWeixinAccount } from "./auth/accounts.js";
import { assertSessionActive } from "./api/session-guard.js";
import { WeixinConfigSchema } from "./config/config-schema.js";
import { getContextToken, findAccountIdsByContextToken, restoreContextTokens, clearContextTokensForAccount } from "./messaging/inbound.js";
import { logger } from "./util/logger.js";
import {
Expand Down Expand Up @@ -143,13 +145,7 @@ export const weixinPlugin: ChannelPlugin<ResolvedWeixinAccount> = {
blurb: "getUpdates long-poll upstream, sendMessage downstream; token auth.",
order: 75,
},
configSchema: {
schema: {
type: "object",
additionalProperties: false,
properties: {},
},
},
configSchema: buildChannelConfigSchema(WeixinConfigSchema),
capabilities: {
chatTypes: ["direct"],
media: true,
Expand Down
7 changes: 5 additions & 2 deletions src/config/config-schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,26 @@ describe("WeixinConfigSchema", () => {
expect(result.cdnBaseUrl).toBe("https://custom.cdn.com");
});

it("accepts optional name and enabled fields", () => {
it("accepts optional name, enabled, and blockStreaming fields", () => {
const result = WeixinConfigSchema.parse({
name: "my-bot",
enabled: false,
blockStreaming: true,
});
expect(result.name).toBe("my-bot");
expect(result.enabled).toBe(false);
expect(result.blockStreaming).toBe(true);
});

it("accepts accounts map", () => {
const result = WeixinConfigSchema.parse({
accounts: {
"acc1": { name: "Bot 1", enabled: true },
"acc1": { name: "Bot 1", enabled: true, blockStreaming: true },
"acc2": { name: "Bot 2" },
},
});
expect(result.accounts?.acc1?.name).toBe("Bot 1");
expect(result.accounts?.acc1?.blockStreaming).toBe(true);
expect(result.accounts?.acc2?.name).toBe("Bot 2");
});

Expand Down
1 change: 1 addition & 0 deletions src/config/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const weixinAccountSchema = z.object({
baseUrl: z.string().default(DEFAULT_BASE_URL),
cdnBaseUrl: z.string().default(CDN_BASE_URL),
routeTag: z.number().optional(),
blockStreaming: z.boolean().optional(),
});

/** Top-level weixin config schema (token is stored in credentials file, not config). */
Expand Down
28 changes: 28 additions & 0 deletions src/messaging/process-message.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { describe, expect, it } from "vitest";

import type { OpenClawConfig } from "openclaw/plugin-sdk/core";

import { resolveWeixinBlockStreamingEnabled } from "./process-message.js";

function config(openclawWeixin: unknown): OpenClawConfig {
return { channels: { "openclaw-weixin": openclawWeixin } } as OpenClawConfig;
}

describe("resolveWeixinBlockStreamingEnabled", () => {
it("defaults to false", () => {
expect(resolveWeixinBlockStreamingEnabled(config({}), "acc1")).toBe(false);
});

it("uses channel-level blockStreaming", () => {
expect(resolveWeixinBlockStreamingEnabled(config({ blockStreaming: true }), "acc1")).toBe(true);
});

it("lets account-level blockStreaming override channel-level value", () => {
expect(
resolveWeixinBlockStreamingEnabled(
config({ blockStreaming: true, accounts: { acc1: { blockStreaming: false } } }),
"acc1",
),
).toBe(false);
});
});
18 changes: 17 additions & 1 deletion src/messaging/process-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ import { handleSlashCommand } from "./slash-commands.js";

const MEDIA_OUTBOUND_TEMP_DIR = path.join(resolvePreferredOpenClawTmpDir(), "weixin/media/outbound-temp");

type WeixinChannelBlockStreamingConfig = {
blockStreaming?: boolean;
accounts?: Record<string, { blockStreaming?: boolean }>;
};

export function resolveWeixinBlockStreamingEnabled(
cfg: import("openclaw/plugin-sdk/core").OpenClawConfig,
accountId: string,
): boolean {
const section = cfg.channels?.["openclaw-weixin"] as WeixinChannelBlockStreamingConfig | undefined;
return section?.accounts?.[accountId]?.blockStreaming ?? section?.blockStreaming ?? false;
}

/** Dependencies for processOneMessage, injected by the monitor loop. */
export type ProcessMessageDeps = {
accountId: string;
Expand Down Expand Up @@ -419,7 +432,10 @@ export async function processOneMessage(
ctx: finalized,
cfg: deps.config,
dispatcher,
replyOptions: { ...replyOptions, disableBlockStreaming: true },
replyOptions: {
...replyOptions,
disableBlockStreaming: !resolveWeixinBlockStreamingEnabled(deps.config, deps.accountId),
},
}),
});
logger.debug(`dispatchReplyFromConfig: done agentId=${route.agentId ?? "(none)"}`);
Expand Down