cli-wechat-bridge 1.1.6 → 1.1.7

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.
Files changed (34) hide show
  1. package/README.md +163 -198
  2. package/bin/wecom-check-update.mjs +5 -0
  3. package/bin/wecom-claude.mjs +5 -0
  4. package/bin/wecom-codex.mjs +5 -0
  5. package/bin/wecom-daemon.mjs +5 -0
  6. package/bin/wecom-opencode.mjs +5 -0
  7. package/bin/wecom-pi.mjs +5 -0
  8. package/bin/wecom-setup.mjs +5 -0
  9. package/dist/bridge/bridge-adapters.claude.js +126 -3
  10. package/dist/bridge/bridge-process-reaper.js +35 -2
  11. package/dist/bridge/bridge-state.js +10 -3
  12. package/dist/bridge/wechat-bridge.js +277 -44
  13. package/dist/channels/wechat/wechat-outbound-queue.js +10 -3
  14. package/dist/channels/wecom/bridge-final-reply.js +26 -0
  15. package/dist/channels/wecom/setup-cli.js +6 -0
  16. package/dist/channels/wecom/setup.js +176 -0
  17. package/dist/channels/wecom/wecom-channel-port.js +45 -0
  18. package/dist/channels/wecom/wecom-config.js +66 -0
  19. package/dist/channels/wecom/wecom-media.js +141 -0
  20. package/dist/channels/wecom/wecom-message.js +149 -0
  21. package/dist/channels/wecom/wecom-transport.js +308 -0
  22. package/dist/companion/codex-remote-client.js +2 -1
  23. package/dist/companion/local-companion-start.js +49 -9
  24. package/dist/companion/local-companion.js +2 -1
  25. package/dist/core/channel-types.js +3 -1
  26. package/dist/core/direct-run.js +17 -0
  27. package/dist/core/inbound-message-claims.js +61 -0
  28. package/dist/daemon/daemon-link.js +3 -0
  29. package/dist/daemon/wechat-daemon.js +265 -36
  30. package/dist/i18n/messages-en.js +2 -0
  31. package/dist/i18n/messages-zh.js +2 -0
  32. package/dist/utils/doctor.js +30 -10
  33. package/dist/wechat/setup.js +113 -13
  34. package/package.json +20 -10
@@ -74,11 +74,11 @@ function defaultGetWindowsCodePage() {
74
74
  return null;
75
75
  }
76
76
  }
77
- async function defaultFetchServerDate() {
77
+ async function defaultFetchServerDate(baseUrl = DEFAULT_BASE_URL) {
78
78
  const controller = new AbortController();
79
79
  const timer = setTimeout(() => controller.abort(), SERVER_DATE_TIMEOUT_MS);
80
80
  try {
81
- const res = await fetch(DEFAULT_BASE_URL, {
81
+ const res = await fetch(baseUrl, {
82
82
  method: "GET",
83
83
  signal: controller.signal,
84
84
  });
@@ -179,6 +179,7 @@ function isBridgeAdapterKind(value) {
179
179
  export function parseDoctorCliArgs(argv, mode = "generic", fallbackCwd = process.cwd()) {
180
180
  let cwd = fallbackCwd;
181
181
  let adapter;
182
+ let channelId = "wechat";
182
183
  for (let i = 0; i < argv.length; i += 1) {
183
184
  const arg = argv[i];
184
185
  const next = argv[i + 1];
@@ -195,11 +196,17 @@ export function parseDoctorCliArgs(argv, mode = "generic", fallbackCwd = process
195
196
  i += 1;
196
197
  continue;
197
198
  }
199
+ if (arg === "--channel" && (next === "wechat" || next === "wecom")) {
200
+ channelId = next;
201
+ i += 1;
202
+ continue;
203
+ }
198
204
  }
199
205
  return {
200
206
  mode,
201
207
  cwd: path.resolve(cwd),
202
208
  adapter,
209
+ ...(channelId === "wecom" ? { channelId } : {}),
203
210
  };
204
211
  }
205
212
  function statusTag(status) {
@@ -582,10 +589,15 @@ export async function buildDoctorReport(options = {}, rawDeps = {}) {
582
589
  }
583
590
  }
584
591
  const proxyUrl = pickProxyEnvValue(deps.env);
585
- const serverDate = await deps.fetchServerDate();
592
+ const connectivityUrl = doctorOptions.channelId === "wecom"
593
+ ? "https://open.work.weixin.qq.com"
594
+ : DEFAULT_BASE_URL;
595
+ const serverDate = await deps.fetchServerDate(connectivityUrl);
586
596
  if (serverDate) {
587
- lines.push(row(STATE_OK, msg("label.connectivity"), msg("connectivity.ok", {
588
- baseUrl: DEFAULT_BASE_URL,
597
+ lines.push(row(STATE_OK, msg(doctorOptions.channelId === "wecom"
598
+ ? "label.wecomConnectivity"
599
+ : "label.connectivity"), msg("connectivity.ok", {
600
+ baseUrl: connectivityUrl,
589
601
  })));
590
602
  const skewMs = deps.now() - serverDate.getTime();
591
603
  const skewSeconds = Math.round(Math.abs(skewMs) / 1000);
@@ -595,9 +607,11 @@ export async function buildDoctorReport(options = {}, rawDeps = {}) {
595
607
  : msg("clock.skewWarning", { skewSeconds })));
596
608
  }
597
609
  else {
598
- lines.push(row(STATE_WARN, msg("label.connectivity"), proxyUrl && deps.env.NODE_USE_ENV_PROXY !== "1"
599
- ? msg("connectivity.proxyHint", { baseUrl: DEFAULT_BASE_URL, proxy: proxyUrl })
600
- : msg("connectivity.unreachable", { baseUrl: DEFAULT_BASE_URL })));
610
+ lines.push(row(STATE_WARN, msg(doctorOptions.channelId === "wecom"
611
+ ? "label.wecomConnectivity"
612
+ : "label.connectivity"), proxyUrl && deps.env.NODE_USE_ENV_PROXY !== "1"
613
+ ? msg("connectivity.proxyHint", { baseUrl: connectivityUrl, proxy: proxyUrl })
614
+ : msg("connectivity.unreachable", { baseUrl: connectivityUrl })));
601
615
  }
602
616
  try {
603
617
  await deps.loadNodePty();
@@ -612,9 +626,15 @@ export async function buildDoctorReport(options = {}, rawDeps = {}) {
612
626
  const dataDir = deps.resolveDataDir();
613
627
  const dataDirExists = deps.exists(dataDir);
614
628
  lines.push(row(dataDirExists ? STATE_OK : STATE_FAIL, msg("label.dataDir"), dataDirExists ? dataDir : `${dataDir} ${msg("dataDirMissing")}`));
615
- const credFile = path.join(dataDir, "account.json");
629
+ const credFile = doctorOptions.channelId === "wecom"
630
+ ? path.join(dataDir, "wecom", "account.json")
631
+ : path.join(dataDir, "account.json");
616
632
  const credExists = deps.exists(credFile);
617
- lines.push(row(credExists ? STATE_OK : STATE_FAIL, msg("label.credentials"), credExists ? msg("credentialsFound") : msg("credentialsMissing")));
633
+ lines.push(row(credExists ? STATE_OK : STATE_FAIL, msg("label.credentials"), credExists
634
+ ? msg("credentialsFound")
635
+ : msg(doctorOptions.channelId === "wecom"
636
+ ? "credentialsMissingWecom"
637
+ : "credentialsMissing")));
618
638
  section(lines, msg("section.runtime"));
619
639
  lines.push(fieldRow(msg("label.mode"), formatMode(doctorOptions.mode)));
620
640
  lines.push(fieldRow(msg("label.cwd"), doctorOptions.cwd));
@@ -11,6 +11,7 @@ import fs from "node:fs";
11
11
  import readline from "node:readline";
12
12
  import { BOT_TYPE, CONTEXT_CACHE_FILE, CREDENTIALS_FILE, DEFAULT_BASE_URL, ensureChannelDataDir, migrateLegacyChannelFiles, SYNC_BUF_FILE, } from "./channel-config.js";
13
13
  import { isWechatSyncSessionTimeout } from "./wechat-transport.js";
14
+ import { isDirectModuleRun } from "../core/direct-run.js";
14
15
  const CHANNEL_VERSION = "0.3.0";
15
16
  export function loadExistingCredentials() {
16
17
  try {
@@ -130,18 +131,74 @@ async function askYesNo(prompt) {
130
131
  rl.close();
131
132
  }
132
133
  }
133
- async function printQRCode(qrContent, write) {
134
+ // eslint-disable-next-line no-control-regex
135
+ const ANSI_SGR_RE = /\u001b\[[0-9;]*m/g;
136
+ function resolveLoginTerminal() {
137
+ const columns = process.stdout.columns;
138
+ return {
139
+ platform: process.platform,
140
+ isTTY: process.stdout.isTTY === true,
141
+ columns: typeof columns === "number" && Number.isFinite(columns) && columns > 0
142
+ ? columns
143
+ : undefined,
144
+ };
145
+ }
146
+ export function getQRCodeVisibleWidth(qr) {
147
+ return Math.max(0, ...qr
148
+ .replace(/\r/g, "")
149
+ .split("\n")
150
+ .map((line) => Array.from(line.replace(ANSI_SGR_RE, "")).length));
151
+ }
152
+ export function selectQRCodePresentation(params) {
153
+ if (!params.isTTY ||
154
+ typeof params.columns !== "number" ||
155
+ !Number.isFinite(params.columns) ||
156
+ params.columns <= 0 ||
157
+ params.renderedWidth + 1 > params.columns) {
158
+ return "url_only";
159
+ }
160
+ return "qr_with_url";
161
+ }
162
+ export function formatQRCodeLoginInstruction(presentation) {
163
+ return presentation === "qr_with_url"
164
+ ? "Scan the QR code above with WeChat, then confirm the login on your phone. If scanning fails, open the browser URL above.\n"
165
+ : "The QR code was not rendered in this terminal. Open the browser URL above, then confirm the login in WeChat.\n";
166
+ }
167
+ async function generateQRCode(qrContent, options) {
168
+ const qrterm = await import("qrcode-terminal");
169
+ return await new Promise((resolve, reject) => {
170
+ try {
171
+ qrterm.default.generate(qrContent, options, resolve);
172
+ }
173
+ catch (error) {
174
+ reject(error);
175
+ }
176
+ });
177
+ }
178
+ export async function printQRCode(qrContent, write, terminal = resolveLoginTerminal(), qrMode = "small", qrGenerator = generateQRCode) {
179
+ if (!terminal.isTTY) {
180
+ return "url_only";
181
+ }
134
182
  try {
135
- const qrterm = await import("qrcode-terminal");
136
- await new Promise((resolve) => {
137
- qrterm.default.generate(qrContent, { small: true }, (qr) => {
138
- write(`${qr}\n`);
139
- resolve();
140
- });
183
+ const qr = await qrGenerator(qrContent, {
184
+ small: qrMode === "small",
141
185
  });
186
+ const presentation = selectQRCodePresentation({
187
+ isTTY: terminal.isTTY,
188
+ columns: terminal.columns,
189
+ renderedWidth: getQRCodeVisibleWidth(qr),
190
+ });
191
+ if (presentation === "qr_with_url") {
192
+ write(`${qr}\n`);
193
+ }
194
+ write(presentation === "qr_with_url"
195
+ ? `If scanning fails, open this short-lived WeChat login URL in a browser. Do not share it:\n${qrContent}\n\n`
196
+ : `Open this short-lived WeChat login URL in a browser. Do not share it:\n${qrContent}\n\n`);
197
+ return presentation;
142
198
  }
143
199
  catch {
144
- write(`Open this QR code URL in a browser: ${qrContent}\n\n`);
200
+ write(`The QR code could not be rendered. Open this short-lived WeChat login URL in a browser. Do not share it:\n${qrContent}\n\n`);
201
+ return "url_only";
145
202
  }
146
203
  }
147
204
  function saveCredentials(account) {
@@ -174,16 +231,54 @@ function printPostLoginHelp(log) {
174
231
  log("");
175
232
  log("Run wechat-setup again any time you need to refresh the login.");
176
233
  }
234
+ export function parseWechatSetupCliArgs(argv) {
235
+ let qrMode = "small";
236
+ for (let index = 0; index < argv.length; index += 1) {
237
+ const arg = argv[index];
238
+ if (arg === "--help" || arg === "-h") {
239
+ return { help: true, qrMode };
240
+ }
241
+ if (arg === "--qr-mode") {
242
+ const value = argv[index + 1];
243
+ if (value !== "small" && value !== "normal") {
244
+ throw new Error("--qr-mode requires small or normal.");
245
+ }
246
+ qrMode = value;
247
+ index += 1;
248
+ continue;
249
+ }
250
+ if (arg === "--qr-mode=small" || arg === "--qr-mode=normal") {
251
+ qrMode = arg.endsWith("=small") ? "small" : "normal";
252
+ continue;
253
+ }
254
+ throw new Error(`Unknown wechat-setup option: ${arg}`);
255
+ }
256
+ return { help: false, qrMode };
257
+ }
258
+ export function formatWechatSetupUsage() {
259
+ return [
260
+ "Usage: wechat-setup [--qr-mode <small|normal>]",
261
+ "",
262
+ "Log in to WeChat and save bridge credentials.",
263
+ "QR mode defaults to small; use --qr-mode normal when the terminal renders small QR blocks incorrectly.",
264
+ "",
265
+ ].join("\n");
266
+ }
177
267
  export async function runWechatLogin(options = {}) {
178
268
  const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
179
269
  const log = options.log ?? ((message) => console.log(message));
180
270
  const write = options.write ?? ((message) => process.stdout.write(message));
181
271
  const timeoutMs = options.timeoutMs ?? 480_000;
182
272
  const pollIntervalMs = options.pollIntervalMs ?? 1_000;
273
+ const terminal = options.terminal ?? resolveLoginTerminal();
274
+ const qrMode = options.qrMode ?? "small";
275
+ if (!terminal.isTTY) {
276
+ throw new Error("WeChat login requires an interactive terminal. Run wechat-setup directly in PowerShell, Command Prompt, or a terminal window.");
277
+ }
183
278
  log("Fetching WeChat login QR code...\n");
184
279
  const qrResp = await fetchQRCode(baseUrl);
185
- await printQRCode(qrResp.qrcode_img_content, write);
186
- log("Scan the QR code above with WeChat, then confirm the login on your phone.\n");
280
+ const presentation = await printQRCode(qrResp.qrcode_img_content, write, terminal, qrMode);
281
+ log(formatQRCodeLoginInstruction(presentation));
187
282
  const deadline = Date.now() + timeoutMs;
188
283
  let scannedPrinted = false;
189
284
  while (Date.now() < deadline) {
@@ -252,7 +347,12 @@ export async function ensureWechatCredentials(options = {}) {
252
347
  const login = options.login ?? (() => runWechatLogin(options));
253
348
  return login();
254
349
  }
255
- async function main() {
350
+ async function main(argv = process.argv.slice(2)) {
351
+ const cliOptions = parseWechatSetupCliArgs(argv);
352
+ if (cliOptions.help) {
353
+ console.log(formatWechatSetupUsage());
354
+ return;
355
+ }
256
356
  migrateLegacyChannelFiles((message) => console.log(message));
257
357
  const existing = loadExistingCredentials();
258
358
  if (existing) {
@@ -266,10 +366,10 @@ async function main() {
266
366
  return;
267
367
  }
268
368
  }
269
- await runWechatLogin({ requireUserId: true });
369
+ await runWechatLogin({ requireUserId: true, qrMode: cliOptions.qrMode });
270
370
  printPostLoginHelp((message) => console.log(message));
271
371
  }
272
- const isDirectRun = Boolean(import.meta.main);
372
+ const isDirectRun = isDirectModuleRun(import.meta.url, process.argv, import.meta.main);
273
373
  if (isDirectRun) {
274
374
  main().catch((err) => {
275
375
  const message = err instanceof Error ? err.message : String(err);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cli-wechat-bridge",
3
- "version": "1.1.6",
4
- "description": "CLI WeChat Bridge for Codex, Claude Code, OpenCode, and Pi over WeChat",
3
+ "version": "1.1.7",
4
+ "description": "CLI bridge for Codex, Claude Code, OpenCode, and Pi over WeChat and WeCom",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "bin/",
@@ -11,7 +11,8 @@
11
11
  "LICENSE.txt"
12
12
  ],
13
13
  "keywords": [
14
- "wechat",
14
+ "wechat",
15
+ "wecom",
15
16
  "cli",
16
17
  "bridge",
17
18
  "codex",
@@ -40,7 +41,14 @@
40
41
  "wechat-pi": "bin/wechat-pi.mjs",
41
42
  "wechat-daemon": "bin/wechat-daemon.mjs",
42
43
  "wechat-setup": "bin/wechat-setup.mjs",
43
- "wechat-check-update": "bin/wechat-check-update.mjs"
44
+ "wechat-check-update": "bin/wechat-check-update.mjs",
45
+ "wecom-codex": "bin/wecom-codex.mjs",
46
+ "wecom-claude": "bin/wecom-claude.mjs",
47
+ "wecom-opencode": "bin/wecom-opencode.mjs",
48
+ "wecom-pi": "bin/wecom-pi.mjs",
49
+ "wecom-daemon": "bin/wecom-daemon.mjs",
50
+ "wecom-setup": "bin/wecom-setup.mjs",
51
+ "wecom-check-update": "bin/wecom-check-update.mjs"
44
52
  },
45
53
  "scripts": {
46
54
  "postinstall": "node scripts/ensure-node-pty-permissions.mjs",
@@ -74,12 +82,14 @@
74
82
  "test": "bun test test",
75
83
  "test:bridge": "bun test test/bridge",
76
84
  "test:companion": "bun test test/companion",
77
- "test:wechat": "bun test test/wechat",
85
+ "test:wechat": "bun test test/wechat",
86
+ "test:wecom": "bun test test/wecom",
78
87
  "test:watch": "bun test --watch test"
79
88
  },
80
89
  "dependencies": {
81
90
  "@modelcontextprotocol/sdk": "^1.30.0",
82
91
  "@opencode-ai/sdk": "^1.18.0",
92
+ "@wecom/aibot-node-sdk": "1.0.7",
83
93
  "node-pty": "^1.1.0",
84
94
  "qrcode-terminal": "^0.12.0"
85
95
  },
@@ -102,8 +112,8 @@
102
112
  "typescript": "^6.0.2",
103
113
  "typescript-eslint": "^8.59.3"
104
114
  },
105
- "engines": {
106
- "bun": ">=1.0.0",
107
- "node": ">=22.13.0"
108
- }
109
- }
115
+ "engines": {
116
+ "bun": ">=1.0.0",
117
+ "node": ">=22.13.0"
118
+ }
119
+ }