@sunnoy/wecom 1.7.1 → 1.8.0

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/index.js CHANGED
@@ -26,7 +26,7 @@ const plugin = {
26
26
  id: "wecom",
27
27
  name: "Enterprise WeChat",
28
28
  description: "Enterprise WeChat AI Bot channel plugin for OpenClaw",
29
- configSchema: { type: "object", additionalProperties: false, properties: {} },
29
+ configSchema: { type: "object", additionalProperties: true, properties: {} },
30
30
  register(api) {
31
31
  logger.info("WeCom plugin registering...");
32
32
 
@@ -7,7 +7,7 @@
7
7
  ],
8
8
  "configSchema": {
9
9
  "type": "object",
10
- "additionalProperties": false,
10
+ "additionalProperties": true,
11
11
  "properties": {}
12
12
  }
13
13
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sunnoy/wecom",
3
- "version": "1.7.1",
3
+ "version": "1.8.0",
4
4
  "description": "Enterprise WeChat AI Bot channel plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/wecom/accounts.js CHANGED
@@ -56,6 +56,15 @@ const RESERVED_KEYS = new Set([
56
56
  "allowFrom",
57
57
  "commandAllowlist",
58
58
  "commandBlockMessage",
59
+ // Top-level config keys that are NOT account IDs (issue #79).
60
+ "network",
61
+ "commands",
62
+ "dynamicAgents",
63
+ "dm",
64
+ "groupChat",
65
+ "adminUsers",
66
+ "workspaceTemplate",
67
+ "instances",
59
68
  ]);
60
69
 
61
70
  // ── Helpers ─────────────────────────────────────────────────────────
@@ -13,6 +13,7 @@ import { resolveWecomTarget } from "./target.js";
13
13
  import { webhookSendImage, webhookSendText, webhookUploadFile, webhookSendFile } from "./webhook-bot.js";
14
14
  import { normalizeWebhookPath, registerWebhookTarget } from "./webhook-targets.js";
15
15
  import { wecomFetch, setConfigProxyUrl } from "./http.js";
16
+ import { setApiBaseUrl } from "./constants.js";
16
17
 
17
18
 
18
19
  const AGENT_IMAGE_EXTS = new Set(["jpg", "jpeg", "png", "gif", "bmp"]);
@@ -164,6 +165,10 @@ export const wecomChannelPlugin = {
164
165
  type: "string",
165
166
  description: "HTTP(S) proxy URL for outbound WeCom API requests (e.g. http://proxy:8080). Env var WECOM_EGRESS_PROXY_URL takes precedence.",
166
167
  },
168
+ apiBaseUrl: {
169
+ type: "string",
170
+ description: "Custom WeCom API base URL (default: https://qyapi.weixin.qq.com). Use when routing through a reverse-proxy or API gateway. Env var WECOM_API_BASE_URL takes precedence.",
171
+ },
167
172
  },
168
173
  },
169
174
  webhooks: {
@@ -739,6 +744,8 @@ export const wecomChannelPlugin = {
739
744
  // Wire proxy URL from config (env var takes precedence inside http.js).
740
745
  const wecomCfg = ctx.cfg?.channels?.wecom ?? {};
741
746
  setConfigProxyUrl(wecomCfg.network?.egressProxyUrl ?? "");
747
+ // Wire API base URL override (env var WECOM_API_BASE_URL takes precedence).
748
+ setApiBaseUrl(wecomCfg.network?.apiBaseUrl ?? "");
742
749
 
743
750
  // Conflict detection: warn about duplicate tokens / agent IDs.
744
751
  const conflicts = detectAccountConflicts(ctx.cfg);
@@ -40,13 +40,36 @@ export const MAIN_RESPONSE_IDLE_CLOSE_MS = 30 * 1000;
40
40
  export const SAFETY_NET_IDLE_CLOSE_MS = 90 * 1000;
41
41
  export const RESPONSE_URL_ERROR_BODY_PREVIEW_MAX = 300;
42
42
 
43
+ // Default Agent API base URL (self-built application mode).
44
+ // Can be overridden via `channels.wecom.network.apiBaseUrl` config or
45
+ // `WECOM_API_BASE_URL` env var for users behind a reverse-proxy gateway
46
+ // that relays requests to qyapi.weixin.qq.com (issue #79).
47
+ const DEFAULT_API_BASE = "https://qyapi.weixin.qq.com";
48
+
49
+ let _apiBase = DEFAULT_API_BASE;
50
+
51
+ /**
52
+ * Set the API base URL from plugin config (called during plugin load).
53
+ * @param {string} url
54
+ */
55
+ export function setApiBaseUrl(url) {
56
+ const trimmed = (url || "").trim().replace(/\/+$/, "");
57
+ _apiBase = trimmed || DEFAULT_API_BASE;
58
+ }
59
+
60
+ function apiBase() {
61
+ // Env var takes precedence over config.
62
+ const env = (process.env.WECOM_API_BASE_URL || "").trim().replace(/\/+$/, "");
63
+ return env || _apiBase;
64
+ }
65
+
43
66
  // Agent API endpoints (self-built application mode).
44
67
  export const AGENT_API_ENDPOINTS = {
45
- GET_TOKEN: "https://qyapi.weixin.qq.com/cgi-bin/gettoken",
46
- SEND_MESSAGE: "https://qyapi.weixin.qq.com/cgi-bin/message/send",
47
- SEND_APPCHAT: "https://qyapi.weixin.qq.com/cgi-bin/appchat/send",
48
- UPLOAD_MEDIA: "https://qyapi.weixin.qq.com/cgi-bin/media/upload",
49
- DOWNLOAD_MEDIA: "https://qyapi.weixin.qq.com/cgi-bin/media/get",
68
+ get GET_TOKEN() { return `${apiBase()}/cgi-bin/gettoken`; },
69
+ get SEND_MESSAGE() { return `${apiBase()}/cgi-bin/message/send`; },
70
+ get SEND_APPCHAT() { return `${apiBase()}/cgi-bin/appchat/send`; },
71
+ get UPLOAD_MEDIA() { return `${apiBase()}/cgi-bin/media/upload`; },
72
+ get DOWNLOAD_MEDIA() { return `${apiBase()}/cgi-bin/media/get`; },
50
73
  };
51
74
 
52
75
  export const TOKEN_REFRESH_BUFFER_MS = 60 * 1000;
@@ -54,5 +77,13 @@ export const AGENT_API_REQUEST_TIMEOUT_MS = 15 * 1000;
54
77
  export const MAX_REQUEST_BODY_SIZE = 1024 * 1024; // 1 MB
55
78
 
56
79
  // Webhook Bot endpoints (group robot notifications).
57
- export const WEBHOOK_BOT_SEND_URL = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send";
58
- export const WEBHOOK_BOT_UPLOAD_URL = "https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media";
80
+ export const WEBHOOK_BOT_SEND_URL_DEFAULT = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send";
81
+ export const WEBHOOK_BOT_UPLOAD_URL_DEFAULT = "https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media";
82
+
83
+ // Dynamic getters so apiBaseUrl override applies to webhook bot too.
84
+ export function getWebhookBotSendUrl() {
85
+ return `${apiBase()}/cgi-bin/webhook/send`;
86
+ }
87
+ export function getWebhookBotUploadUrl() {
88
+ return `${apiBase()}/cgi-bin/webhook/upload_media`;
89
+ }
@@ -48,7 +48,13 @@ export async function wecomHttpHandler(req, res) {
48
48
  const targets = webhookTargets.get(path);
49
49
 
50
50
  if (!targets || targets.length === 0) {
51
- return false; // Not handled by this plugin
51
+ // Return a proper HTTP response instead of `false`. Returning false tells
52
+ // OpenClaw 3.x "not handled", which causes the SPA catch-all to serve the
53
+ // chat UI on webhook paths (issue #81).
54
+ logger.debug("WeCom: no webhook target registered for path", { path });
55
+ res.writeHead(404, { "Content-Type": "text/plain" });
56
+ res.end(`No WeCom webhook target configured for ${path}`);
57
+ return true;
52
58
  }
53
59
 
54
60
  const query = Object.fromEntries(url.searchParams);
package/wecom/http.js CHANGED
@@ -12,6 +12,7 @@
12
12
  * 3. Config: `channels.wecom.network.egressProxyUrl`
13
13
  */
14
14
 
15
+ import { logger } from "../logger.js";
15
16
  import { AGENT_API_REQUEST_TIMEOUT_MS } from "./constants.js";
16
17
 
17
18
  // ── Lazy-loaded undici (optional dependency) ──────────────────────────
@@ -64,6 +65,7 @@ function mergeAbortSignal({ signal, timeoutMs }) {
64
65
  // ── Proxy URL resolution ──────────────────────────────────────────────
65
66
 
66
67
  let _configProxyUrl = "";
68
+ let _proxyWarningLogged = false;
67
69
 
68
70
  /**
69
71
  * Set the proxy URL from plugin config (called once during plugin load).
@@ -118,7 +120,16 @@ export async function wecomFetch(input, init, opts) {
118
120
  dispatcher,
119
121
  });
120
122
  }
121
- // undici not available — fall through to native fetch (no proxy)
123
+ // undici not available — log warning and fall through to native fetch (no proxy).
124
+ // This is a common cause of proxy misconfiguration (issue #79).
125
+ if (!_proxyWarningLogged) {
126
+ _proxyWarningLogged = true;
127
+ logger.error(
128
+ "[wecom/http] Proxy configured but undici is not available — requests will go DIRECT without proxy. " +
129
+ "Install undici (npm install undici) to enable proxy support.",
130
+ { proxyUrl },
131
+ );
132
+ }
122
133
  }
123
134
 
124
135
  // Native fetch (no proxy)
package/wecom/state.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
- import { WEBHOOK_BOT_SEND_URL } from "./constants.js";
2
+ import { getWebhookBotSendUrl } from "./constants.js";
3
3
  import { resolveAgentConfigForAccount, resolveAccount } from "./accounts.js";
4
4
 
5
5
  const runtimeState = {
@@ -81,5 +81,5 @@ export function resolveWebhookUrl(name, accountId) {
81
81
  if (!webhooks || !webhooks[name]) return null;
82
82
  const value = webhooks[name];
83
83
  if (value.startsWith("http")) return value;
84
- return `${WEBHOOK_BOT_SEND_URL}?key=${value}`;
84
+ return `${getWebhookBotSendUrl()}?key=${value}`;
85
85
  }
@@ -10,6 +10,7 @@
10
10
  import crypto from "node:crypto";
11
11
  import { logger } from "../logger.js";
12
12
  import { AGENT_API_REQUEST_TIMEOUT_MS } from "./constants.js";
13
+ import { getWebhookBotUploadUrl } from "./constants.js";
13
14
  import { wecomFetch } from "./http.js";
14
15
 
15
16
  /**
@@ -76,7 +77,7 @@ export async function webhookSendImage({ url, base64, md5 }) {
76
77
  */
77
78
  export async function webhookUploadFile({ url, buffer, filename }) {
78
79
  const key = extractKey(url);
79
- const uploadUrl = `https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key=${encodeURIComponent(key)}&type=file`;
80
+ const uploadUrl = `${getWebhookBotUploadUrl()}?key=${encodeURIComponent(key)}&type=file`;
80
81
 
81
82
  const boundary = `----WebKitFormBoundary${crypto.randomBytes(16).toString("hex")}`;
82
83
  const header = Buffer.from(