@unlinearity/cli-wechat-bridge 1.0.1 → 1.0.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.
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runJsEntry } from "./_run-entry.mjs";
4
+
5
+ runJsEntry("dist/wechat/setup.js");
@@ -314,6 +314,13 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
314
314
  }
315
315
  return this.isNativePanelMode() ? Boolean(this.nativeProcess) : Boolean(this.pty);
316
316
  }
317
+ shouldPollSessionLog() {
318
+ return (this.isCodexClientRunning() ||
319
+ this.pendingTurnStart ||
320
+ Boolean(this.activeTurn) ||
321
+ Boolean(this.state.activeTurnId) ||
322
+ Boolean(this.sessionFilePath));
323
+ }
317
324
  async startNativeClient() {
318
325
  this.setStatus("starting", `Starting ${this.options.kind} adapter...`);
319
326
  let spawnTarget = null;
@@ -450,7 +457,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
450
457
  this.nextSessionFallbackScanAtMs = 0;
451
458
  }
452
459
  async pollSessionLog() {
453
- if (!this.isCodexClientRunning()) {
460
+ if (!this.shouldPollSessionLog()) {
454
461
  return;
455
462
  }
456
463
  this.maybeApplyRecentSessionFallback();
@@ -98,6 +98,11 @@ export class LocalCompanionProxyAdapter {
98
98
  sessionId,
99
99
  });
100
100
  }
101
+ async createSession() {
102
+ await this.sendRequest({
103
+ command: "create_session",
104
+ });
105
+ }
101
106
  async interrupt() {
102
107
  const result = await this.sendRequest({
103
108
  command: "interrupt",
@@ -6,6 +6,7 @@ const OPENCODE_DEBUG_ENABLED = /^(1|true|yes|on)$/i.test(process.env.WECHAT_OPEN
6
6
  const OPENCODE_DUPLICATE_EVENT_TTL_MS = 150;
7
7
  const OPENCODE_WECHAT_MIRROR_SUPPRESSION_TTL_MS = 30_000;
8
8
  const OPENCODE_RECENT_LOCAL_PROMPT_TTL_MS = 10_000;
9
+ const OPENCODE_LOCAL_SESSION_CREATE_FOLLOW_TTL_MS = 5_000;
9
10
  /* ------------------------------------------------------------------ */
10
11
  /* Adapter */
11
12
  /* ------------------------------------------------------------------ */
@@ -37,9 +38,11 @@ export class OpenCodeServerAdapter {
37
38
  observedUserTextByPartId = new Map();
38
39
  observedUserMessagePartIds = new Map();
39
40
  pendingWechatPromptMirrorSuppressions = [];
41
+ recentWechatPromptMirrorSuppressions = [];
40
42
  recentSdkEventObservations = new Map();
41
43
  suppressedTuiSessionSelectId = null;
42
44
  lastMirroredLocalPrompt = null;
45
+ pendingLocalSessionCreateFollowUntilMs = 0;
43
46
  pendingPermission = null;
44
47
  constructor(options) {
45
48
  this.options = options;
@@ -118,6 +121,8 @@ export class OpenCodeServerAdapter {
118
121
  syncVisible: true,
119
122
  forceVisibleSync: true,
120
123
  });
124
+ this.recordPendingWechatPromptMirrorSuppression(session.id, normalized);
125
+ this.beginTrackedTurn(normalized, "wechat");
121
126
  try {
122
127
  const result = await this.client.session.promptAsync({
123
128
  sessionID: session.id,
@@ -130,10 +135,11 @@ export class OpenCodeServerAdapter {
130
135
  }
131
136
  }
132
137
  catch (err) {
138
+ this.settleTurnState();
139
+ this.clearObservedMessageTracking();
140
+ this.setStatus("idle");
133
141
  throw new Error(`Failed to send prompt: ${describeUnknownError(err)}`);
134
142
  }
135
- this.recordPendingWechatPromptMirrorSuppression(session.id, normalized);
136
- this.beginTrackedTurn(normalized, "wechat");
137
143
  }
138
144
  async listResumeSessions(limit = 10) {
139
145
  void limit;
@@ -143,6 +149,31 @@ export class OpenCodeServerAdapter {
143
149
  void sessionId;
144
150
  throw new Error('WeChat /resume is disabled in opencode mode. Use /session directly inside "wechat-opencode"; WeChat will follow the active local session.');
145
151
  }
152
+ async createSession() {
153
+ if (!this.client) {
154
+ throw new Error("OpenCode adapter is not running.");
155
+ }
156
+ if (this.state.status === "busy") {
157
+ throw new Error("OpenCode is still working. Wait for the current reply or use /stop.");
158
+ }
159
+ if (this.pendingPermission) {
160
+ throw new Error("An OpenCode approval request is pending. Reply with /confirm <code> or /deny.");
161
+ }
162
+ this.outputBatcher.clear();
163
+ this.clearStreamedPartState();
164
+ const session = this.unwrapOrThrow(await this.client.session.create({
165
+ directory: this.options.cwd,
166
+ workspace: this.activeWorkspaceId ?? undefined,
167
+ }));
168
+ this.switchSharedSession(session, {
169
+ source: "wechat",
170
+ reason: "wechat_resume",
171
+ notify: true,
172
+ clearTrackedTurn: true,
173
+ syncVisible: true,
174
+ forceVisibleSync: true,
175
+ });
176
+ }
146
177
  async interrupt() {
147
178
  if (!this.client || !this.activeSessionId) {
148
179
  return false;
@@ -167,6 +198,7 @@ export class OpenCodeServerAdapter {
167
198
  this.clearWechatWorkingNotice(true);
168
199
  this.pendingLocalPrompt = "";
169
200
  this.localPromptNoticeSent = false;
201
+ this.pendingLocalSessionCreateFollowUntilMs = 0;
170
202
  this.clearObservedMessageTracking();
171
203
  this.recentSdkEventObservations.clear();
172
204
  this.clearPendingPermissionState();
@@ -223,6 +255,7 @@ export class OpenCodeServerAdapter {
223
255
  this.clearWechatWorkingNotice(true);
224
256
  this.pendingLocalPrompt = "";
225
257
  this.localPromptNoticeSent = false;
258
+ this.pendingLocalSessionCreateFollowUntilMs = 0;
226
259
  this.clearObservedMessageTracking();
227
260
  this.recentSdkEventObservations.clear();
228
261
  this.outputBatcher.clear();
@@ -573,12 +606,15 @@ export class OpenCodeServerAdapter {
573
606
  if (this.isImplicitLocalCompanionUiEvent(type)) {
574
607
  return true;
575
608
  }
576
- return this.matchesCurrentDirectoryEvent(event);
609
+ return (this.matchesCurrentDirectoryEvent(event) ||
610
+ this.shouldAcceptUnscopedLocalSessionCreatedEvent(event));
577
611
  }
578
612
  if (type === "session.created" ||
579
613
  type === "session.updated" ||
580
614
  type === "session.deleted") {
581
- return this.matchesCurrentDirectoryEvent(event);
615
+ return (this.matchesCurrentDirectoryEvent(event) ||
616
+ (type === "session.created" &&
617
+ this.shouldAcceptUnscopedLocalSessionCreatedEvent(event)));
582
618
  }
583
619
  return false;
584
620
  }
@@ -831,9 +867,21 @@ export class OpenCodeServerAdapter {
831
867
  return;
832
868
  }
833
869
  const session = this.extractSessionReference(properties);
834
- if (!this.syncTrackedSessionFromEvent(session)) {
870
+ if (this.syncTrackedSessionFromEvent(session)) {
835
871
  return;
836
872
  }
873
+ if (!this.shouldFollowCreatedLocalSession(session)) {
874
+ return;
875
+ }
876
+ this.pendingLocalSessionCreateFollowUntilMs = 0;
877
+ this.logDebug(`[opencode-adapter:local-follow] session.created ${session.id}`);
878
+ this.switchSharedSession(session, {
879
+ source: "local",
880
+ reason: "local_follow",
881
+ notify: true,
882
+ clearTrackedTurn: true,
883
+ syncVisible: true,
884
+ });
837
885
  }
838
886
  handleSessionUpdated(properties) {
839
887
  if (!isRecord(properties)) {
@@ -907,6 +955,14 @@ export class OpenCodeServerAdapter {
907
955
  : delta
908
956
  ? this.consumeVisiblePartDelta(partId, delta)
909
957
  : "";
958
+ const observedText = typeof part.messageID === "string"
959
+ ? this.observedOpenCodeMessages.get(part.messageID)?.text
960
+ : undefined;
961
+ if (this.matchesRecentWechatPromptMirror(part.sessionID, observedText || partText || delta || text, {
962
+ allowPrefix: true,
963
+ })) {
964
+ return;
965
+ }
910
966
  this.pushVisibleOutput(text);
911
967
  }
912
968
  handleMessagePartDelta(properties) {
@@ -953,6 +1009,14 @@ export class OpenCodeServerAdapter {
953
1009
  return;
954
1010
  }
955
1011
  const text = this.consumeVisiblePartDelta(partId, delta);
1012
+ const observedText = typeof properties.messageID === "string"
1013
+ ? this.observedOpenCodeMessages.get(properties.messageID)?.text
1014
+ : undefined;
1015
+ if (this.matchesRecentWechatPromptMirror(sessionId, observedText || delta || text, {
1016
+ allowPrefix: true,
1017
+ })) {
1018
+ return;
1019
+ }
956
1020
  this.pushVisibleOutput(text);
957
1021
  }
958
1022
  handleMessagePartRemoved(properties) {
@@ -993,6 +1057,9 @@ export class OpenCodeServerAdapter {
993
1057
  this.handleLocalPromptSubmit();
994
1058
  return;
995
1059
  default:
1060
+ if (this.isLocalSessionNavigationCommand(command)) {
1061
+ this.markPendingLocalSessionCreateFollow();
1062
+ }
996
1063
  return;
997
1064
  }
998
1065
  }
@@ -1000,7 +1067,7 @@ export class OpenCodeServerAdapter {
1000
1067
  if (!isRecord(properties)) {
1001
1068
  return;
1002
1069
  }
1003
- const sessionId = typeof properties.sessionID === "string" ? properties.sessionID : undefined;
1070
+ const sessionId = this.extractSessionId(properties) ?? undefined;
1004
1071
  if (!sessionId) {
1005
1072
  return;
1006
1073
  }
@@ -1010,6 +1077,7 @@ export class OpenCodeServerAdapter {
1010
1077
  }
1011
1078
  this.pendingLocalPrompt = "";
1012
1079
  this.localPromptNoticeSent = false;
1080
+ this.pendingLocalSessionCreateFollowUntilMs = 0;
1013
1081
  this.logDebug(`[opencode-adapter:local-follow] tui.session.select ${sessionId}`);
1014
1082
  this.switchSharedSession({
1015
1083
  id: sessionId,
@@ -1043,7 +1111,9 @@ export class OpenCodeServerAdapter {
1043
1111
  clearTrackedTurn: true,
1044
1112
  syncVisible: true,
1045
1113
  });
1114
+ return;
1046
1115
  }
1116
+ this.markPendingLocalSessionCreateFollow();
1047
1117
  }
1048
1118
  handleSessionError(properties) {
1049
1119
  if (!isRecord(properties)) {
@@ -1138,7 +1208,8 @@ export class OpenCodeServerAdapter {
1138
1208
  return;
1139
1209
  }
1140
1210
  observed.emitted = true;
1141
- if (this.shouldSuppressWechatMirroredPrompt(observed.sessionId, text)) {
1211
+ if (this.shouldSuppressWechatMirroredPrompt(observed.sessionId, text) ||
1212
+ this.matchesRecentWechatPromptMirror(observed.sessionId, text)) {
1142
1213
  this.cleanupObservedOpenCodeMessage(messageId);
1143
1214
  return;
1144
1215
  }
@@ -1159,30 +1230,23 @@ export class OpenCodeServerAdapter {
1159
1230
  if (!sessionId || !normalizedText) {
1160
1231
  return;
1161
1232
  }
1162
- const cutoff = Date.now() - OPENCODE_WECHAT_MIRROR_SUPPRESSION_TTL_MS;
1163
- for (let index = this.pendingWechatPromptMirrorSuppressions.length - 1; index >= 0; index -= 1) {
1164
- if (this.pendingWechatPromptMirrorSuppressions[index].createdAtMs < cutoff) {
1165
- this.pendingWechatPromptMirrorSuppressions.splice(index, 1);
1166
- }
1167
- }
1168
- this.pendingWechatPromptMirrorSuppressions.push({
1233
+ this.pruneWechatPromptMirrorSuppressions();
1234
+ const suppression = {
1169
1235
  sessionId,
1170
1236
  text: normalizedText,
1171
1237
  createdAtMs: Date.now(),
1172
- });
1238
+ };
1239
+ this.pendingWechatPromptMirrorSuppressions.push(suppression);
1240
+ this.recentWechatPromptMirrorSuppressions.push(suppression);
1173
1241
  }
1174
1242
  shouldSuppressWechatMirroredPrompt(sessionId, text) {
1175
1243
  const normalizedText = normalizeOutput(text).trim();
1176
1244
  if (!sessionId || !normalizedText) {
1177
1245
  return false;
1178
1246
  }
1179
- const cutoff = Date.now() - OPENCODE_WECHAT_MIRROR_SUPPRESSION_TTL_MS;
1247
+ this.pruneWechatPromptMirrorSuppressions();
1180
1248
  for (let index = this.pendingWechatPromptMirrorSuppressions.length - 1; index >= 0; index -= 1) {
1181
1249
  const pending = this.pendingWechatPromptMirrorSuppressions[index];
1182
- if (pending.createdAtMs < cutoff) {
1183
- this.pendingWechatPromptMirrorSuppressions.splice(index, 1);
1184
- continue;
1185
- }
1186
1250
  if (pending.sessionId === sessionId && pending.text === normalizedText) {
1187
1251
  this.pendingWechatPromptMirrorSuppressions.splice(index, 1);
1188
1252
  return true;
@@ -1190,6 +1254,35 @@ export class OpenCodeServerAdapter {
1190
1254
  }
1191
1255
  return false;
1192
1256
  }
1257
+ matchesRecentWechatPromptMirror(sessionId, text, options = {}) {
1258
+ const normalizedText = normalizeOutput(text ?? "").trim();
1259
+ if (!sessionId || !normalizedText) {
1260
+ return false;
1261
+ }
1262
+ this.pruneWechatPromptMirrorSuppressions();
1263
+ return this.recentWechatPromptMirrorSuppressions.some((pending) => {
1264
+ if (pending.sessionId !== sessionId) {
1265
+ return false;
1266
+ }
1267
+ if (pending.text === normalizedText) {
1268
+ return true;
1269
+ }
1270
+ return options.allowPrefix === true && pending.text.startsWith(normalizedText);
1271
+ });
1272
+ }
1273
+ pruneWechatPromptMirrorSuppressions() {
1274
+ const cutoff = Date.now() - OPENCODE_WECHAT_MIRROR_SUPPRESSION_TTL_MS;
1275
+ for (let index = this.pendingWechatPromptMirrorSuppressions.length - 1; index >= 0; index -= 1) {
1276
+ if (this.pendingWechatPromptMirrorSuppressions[index].createdAtMs < cutoff) {
1277
+ this.pendingWechatPromptMirrorSuppressions.splice(index, 1);
1278
+ }
1279
+ }
1280
+ for (let index = this.recentWechatPromptMirrorSuppressions.length - 1; index >= 0; index -= 1) {
1281
+ if (this.recentWechatPromptMirrorSuppressions[index].createdAtMs < cutoff) {
1282
+ this.recentWechatPromptMirrorSuppressions.splice(index, 1);
1283
+ }
1284
+ }
1285
+ }
1193
1286
  wasRecentlyMirroredLocalPrompt(text) {
1194
1287
  const normalizedText = normalizeOutput(text).trim();
1195
1288
  if (!normalizedText) {
@@ -1276,6 +1369,7 @@ export class OpenCodeServerAdapter {
1276
1369
  this.observedUserTextByPartId.clear();
1277
1370
  this.observedUserMessagePartIds.clear();
1278
1371
  this.pendingWechatPromptMirrorSuppressions.length = 0;
1372
+ this.recentWechatPromptMirrorSuppressions.length = 0;
1279
1373
  this.lastMirroredLocalPrompt = null;
1280
1374
  }
1281
1375
  clearTrackedTurnForLocalSessionSwitch() {
@@ -1400,12 +1494,22 @@ export class OpenCodeServerAdapter {
1400
1494
  return false;
1401
1495
  }
1402
1496
  extractSessionReference(properties) {
1403
- if (typeof properties.sessionID === "string") {
1497
+ if (typeof properties.sessionID === "string" || typeof properties.sessionId === "string") {
1498
+ const id = typeof properties.sessionID === "string"
1499
+ ? properties.sessionID
1500
+ : properties.sessionId;
1404
1501
  return {
1405
- id: properties.sessionID,
1502
+ id,
1406
1503
  workspaceID: this.extractWorkspaceId(properties) ?? undefined,
1407
1504
  };
1408
1505
  }
1506
+ const session = isRecord(properties.session) ? properties.session : undefined;
1507
+ if (typeof session?.id === "string") {
1508
+ return {
1509
+ id: session.id,
1510
+ workspaceID: this.extractWorkspaceId(session) ?? this.extractWorkspaceId(properties) ?? undefined,
1511
+ };
1512
+ }
1409
1513
  const info = isRecord(properties.info) ? properties.info : undefined;
1410
1514
  if (typeof info?.id === "string") {
1411
1515
  return {
@@ -1422,6 +1526,10 @@ export class OpenCodeServerAdapter {
1422
1526
  if (typeof properties.workspaceID === "string") {
1423
1527
  return properties.workspaceID;
1424
1528
  }
1529
+ const session = isRecord(properties.session) ? properties.session : undefined;
1530
+ if (typeof session?.workspaceID === "string") {
1531
+ return session.workspaceID;
1532
+ }
1425
1533
  const info = isRecord(properties.info) ? properties.info : undefined;
1426
1534
  if (typeof info?.workspaceID === "string") {
1427
1535
  return info.workspaceID;
@@ -1433,6 +1541,46 @@ export class OpenCodeServerAdapter {
1433
1541
  this.state.activeTurnOrigin === "local" &&
1434
1542
  this.hasTrackedTurnState());
1435
1543
  }
1544
+ markPendingLocalSessionCreateFollow() {
1545
+ this.pendingLocalSessionCreateFollowUntilMs =
1546
+ Date.now() + OPENCODE_LOCAL_SESSION_CREATE_FOLLOW_TTL_MS;
1547
+ }
1548
+ hasPendingLocalSessionCreateFollow() {
1549
+ return Date.now() <= this.pendingLocalSessionCreateFollowUntilMs;
1550
+ }
1551
+ shouldAcceptUnscopedLocalSessionCreatedEvent(event) {
1552
+ if (this.normalizeEventType(event.type) !== "session.created") {
1553
+ return false;
1554
+ }
1555
+ if (this.options.renderMode !== "companion") {
1556
+ return false;
1557
+ }
1558
+ const payload = this.extractEventPayload(event);
1559
+ if (!isRecord(payload)) {
1560
+ return false;
1561
+ }
1562
+ const session = this.extractSessionReference(payload);
1563
+ if (!session?.id || session.id === this.activeSessionId) {
1564
+ return false;
1565
+ }
1566
+ if (typeof event.directory === "string" ||
1567
+ this.extractSessionDirectory(payload) ||
1568
+ this.extractWorkspaceId(payload)) {
1569
+ return false;
1570
+ }
1571
+ return this.hasPendingLocalSessionCreateFollow() || Boolean(this.activeSessionId);
1572
+ }
1573
+ shouldFollowCreatedLocalSession(session) {
1574
+ if (!session?.id || session.id === this.activeSessionId) {
1575
+ return false;
1576
+ }
1577
+ if (this.hasPendingLocalSessionCreateFollow()) {
1578
+ this.pendingLocalSessionCreateFollowUntilMs = 0;
1579
+ return true;
1580
+ }
1581
+ this.pendingLocalSessionCreateFollowUntilMs = 0;
1582
+ return Boolean(this.activeSessionId);
1583
+ }
1436
1584
  switchSharedSession(session, options) {
1437
1585
  const sessionId = typeof session === "string" ? session : session.id;
1438
1586
  if (!sessionId) {
@@ -1490,7 +1638,9 @@ export class OpenCodeServerAdapter {
1490
1638
  }
1491
1639
  const normalized = command.trim().toLowerCase();
1492
1640
  return (normalized === "session" ||
1641
+ normalized === "new" ||
1493
1642
  normalized === "/session" ||
1643
+ normalized === "/new" ||
1494
1644
  normalized.startsWith("session.") ||
1495
1645
  normalized.startsWith("/session "));
1496
1646
  }
@@ -1538,6 +1688,10 @@ export class OpenCodeServerAdapter {
1538
1688
  if (typeof properties.directory === "string") {
1539
1689
  return properties.directory;
1540
1690
  }
1691
+ const session = isRecord(properties.session) ? properties.session : undefined;
1692
+ if (typeof session?.directory === "string") {
1693
+ return session.directory;
1694
+ }
1541
1695
  const info = isRecord(properties.info) ? properties.info : undefined;
1542
1696
  if (typeof info?.directory === "string") {
1543
1697
  return info.directory;
@@ -139,6 +139,9 @@ export function parseSystemCommand(text) {
139
139
  return { type: "status" };
140
140
  case "/resume":
141
141
  return argument ? { type: "resume", target: argument } : { type: "resume" };
142
+ case "/new":
143
+ case "/new-session":
144
+ return { type: "new_session" };
142
145
  case "/stop":
143
146
  return { type: "stop" };
144
147
  case "/reset":
@@ -694,6 +697,7 @@ const OPENCODE_REASONING_LINE_RES = [
694
697
  /\bI need to (?:respond|reply|answer|tell the user)\b/i,
695
698
  /\bWe need to (?:respond|reply|answer)\b/i,
696
699
  /\bI should\b/i,
700
+ /\bI(?:'ll| will) (?:respond|reply|answer|tell the user|provide)\b/i,
697
701
  /\bI'll provide\b/i,
698
702
  /^Let me (?:directly )?(?:answer|respond)\b/i,
699
703
  /根据系统提示/i,
@@ -704,8 +708,27 @@ const OPENCODE_REASONING_LINE_RES = [
704
708
  /^我要直接(?:回答|回复)/,
705
709
  /^用户(?:说|问)了/,
706
710
  ];
711
+ const OPENCODE_INLINE_REASONING_MARKER_RE = /\b(?:The user\b|I need to\b|I should\b|I(?:'ll| will)\b|We need to\b|Let me\b)/i;
712
+ const OPENCODE_INLINE_REASONING_SENTENCE_RE = /^(?:The user\b|I need to\b|I should\b|I(?:'ll| will)\b|We need to\b|Let me\b)[^.!?\n]*(?:[.!?]+)\s*/i;
713
+ function stripInlineOpenCodeReasoningPrefix(text) {
714
+ let current = text.trim();
715
+ const markerIndex = current.search(OPENCODE_INLINE_REASONING_MARKER_RE);
716
+ if (markerIndex > 0 && markerIndex <= 80) {
717
+ current = current.slice(markerIndex).trimStart();
718
+ }
719
+ for (let index = 0; index < 6; index += 1) {
720
+ const match = current.match(OPENCODE_INLINE_REASONING_SENTENCE_RE);
721
+ if (!match) {
722
+ break;
723
+ }
724
+ current = current.slice(match[0].length).trimStart();
725
+ }
726
+ return current;
727
+ }
707
728
  export function sanitizeWechatFinalReplyText(adapter, text) {
708
- const normalized = cleanupVisibleWechatReplyText(text);
729
+ const normalized = adapter === "opencode"
730
+ ? cleanupVisibleWechatReplyText(stripInlineOpenCodeReasoningPrefix(text))
731
+ : cleanupVisibleWechatReplyText(text);
709
732
  if (!normalized || adapter !== "opencode") {
710
733
  return normalized;
711
734
  }
@@ -4,7 +4,7 @@ import { resolveDefaultAdapterCommand, } from "./bridge-adapters.js";
4
4
  import { delay } from "./bridge-adapters.shared.js";
5
5
  import { BridgeController } from "./bridge-controller.js";
6
6
  import { forwardWechatFinalReply } from "./bridge-final-reply.js";
7
- import { migrateLegacyChannelFiles } from "../wechat/channel-config.js";
7
+ import { ensureWechatCredentials } from "../wechat/setup.js";
8
8
  import { BridgeStateStore } from "./bridge-state.js";
9
9
  import { reapOrphanedOpencodeProcesses, reapPeerBridgeProcesses } from "./bridge-process-reaper.js";
10
10
  import { createRuntimeHost } from "../runtime/create-runtime-host.js";
@@ -196,8 +196,17 @@ function printUsageAndExit() {
196
196
  process.exit(0);
197
197
  }
198
198
  async function main() {
199
- migrateLegacyChannelFiles(log);
200
- // 非阻塞地检查更新(不影响启动速度)
199
+ const options = parseCliArgs(process.argv.slice(2));
200
+ const credentials = await ensureWechatCredentials({
201
+ requireUserId: true,
202
+ validateExisting: true,
203
+ log,
204
+ });
205
+ if (!credentials.userId) {
206
+ throw new Error("Saved WeChat credentials are missing userId.");
207
+ }
208
+ const transport = new WeChatTransport({ log, logError });
209
+ // 非阻塞地检查更新(不影响启动速度,也避免首次登录时打断二维码输出)
201
210
  setTimeout(async () => {
202
211
  try {
203
212
  const versionInfo = await checkForUpdate();
@@ -209,15 +218,6 @@ async function main() {
209
218
  // 静默失败,不影响正常使用
210
219
  }
211
220
  }, 3000); // 延迟3秒,确保不影响启动
212
- const options = parseCliArgs(process.argv.slice(2));
213
- const transport = new WeChatTransport({ log, logError });
214
- const credentials = transport.getCredentials();
215
- if (!credentials) {
216
- throw new Error('No saved WeChat credentials found. Run "bun run setup" first.');
217
- }
218
- if (!credentials.userId) {
219
- throw new Error('Saved WeChat credentials are missing userId. Run "bun run setup" again.');
220
- }
221
221
  const stateStore = new BridgeStateStore({
222
222
  ...options,
223
223
  authorizedUserId: credentials.userId,
@@ -641,12 +641,13 @@ function wireAdapterEvents(params) {
641
641
  }
642
642
  break;
643
643
  case "final_reply":
644
+ stateStore.appendLog(`final_reply: ${truncatePreview(event.text)}`);
644
645
  void outputBatcher.flushNow().then(async () => {
645
646
  await forwardWechatFinalReply({
646
647
  adapter: options.adapter,
647
648
  rawText: event.text,
648
649
  sender: {
649
- sendText: (text) => queueWechatMessage(authorizedUserId, text),
650
+ sendText: (text) => queueWechatMessage(authorizedUserId, text, "final_reply"),
650
651
  sendImage: (imagePath) => queueWechatAttachmentAction(() => transport.sendImage(imagePath, { recipientId: authorizedUserId })),
651
652
  sendFile: (filePath) => queueWechatAttachmentAction(() => transport.sendFile(filePath, { recipientId: authorizedUserId })),
652
653
  sendVoice: (voicePath) => queueWechatAttachmentAction(() => transport.sendVoice(voicePath, authorizedUserId)),
@@ -798,6 +799,19 @@ async function handleInboundMessage(params) {
798
799
  await queueWechatMessage(message.senderId, `/resume is not available in ${options.adapter} mode.`);
799
800
  return null;
800
801
  }
802
+ case "new_session": {
803
+ if (!adapter.createSession) {
804
+ await queueWechatMessage(message.senderId, `/new is not available in ${options.adapter} mode.`);
805
+ return null;
806
+ }
807
+ await outputBatcher.flushNow();
808
+ outputBatcher.clear();
809
+ stateStore.clearPendingConfirmation();
810
+ stateStore.clearSharedSessionId();
811
+ await adapter.createSession();
812
+ stateStore.appendLog(`New ${options.adapter} session requested by owner.`);
813
+ return null;
814
+ }
801
815
  case "stop": {
802
816
  const interrupted = await adapter.interrupt();
803
817
  await queueWechatMessage(message.senderId, interrupted
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import fs from "node:fs";
3
2
  import net from "node:net";
4
3
  import path from "node:path";
5
4
  import { spawn } from "node:child_process";
6
5
  import { fileURLToPath } from "node:url";
7
- import { BRIDGE_LOG_FILE, CREDENTIALS_FILE, migrateLegacyChannelFiles, } from "../wechat/channel-config.js";
6
+ import { BRIDGE_LOG_FILE } from "../wechat/channel-config.js";
7
+ import { ensureWechatCredentials } from "../wechat/setup.js";
8
8
  import { readBridgeLockFile, shouldAutoReclaimBridgeLock, } from "../bridge/bridge-state.js";
9
9
  import { clearLocalCompanionOccupancy, clearLocalCompanionEndpoint, readLocalCompanionEndpoint, } from "./local-companion-link.js";
10
10
  import { runCodexRemoteClient } from "./codex-remote-client.js";
@@ -54,9 +54,13 @@ export function decideLaunchAction(input) {
54
54
  failureMessage: formatSwitchFailureMessage(input.runningLock.cwd),
55
55
  };
56
56
  }
57
- if (input.endpoint &&
58
- input.endpointIsReachable &&
59
- input.companionIsAlive &&
57
+ if (!input.endpoint || !input.endpointIsReachable) {
58
+ return {
59
+ kind: "restart_unhealthy",
60
+ message: formatRestartUnhealthyMessage(input.requestedCwd),
61
+ };
62
+ }
63
+ if (input.companionIsAlive &&
60
64
  (input.endpoint.companionStatus === "stopped" ||
61
65
  input.endpoint.companionStatus === "error")) {
62
66
  return {
@@ -328,12 +332,16 @@ export async function runVisibleClient(options, runners = {}) {
328
332
  cliArgs: options.cliArgs,
329
333
  });
330
334
  }
335
+ export async function ensureCompanionStartWechatCredentials(adapter, ensureCredentials = ensureWechatCredentials) {
336
+ await ensureCredentials({
337
+ requireUserId: true,
338
+ validateExisting: true,
339
+ log: (message) => log(adapter, message),
340
+ });
341
+ }
331
342
  export async function runLocalCompanionStart(argv = process.argv.slice(2)) {
332
343
  const options = parseCliArgs(argv);
333
- migrateLegacyChannelFiles((message) => log(options.adapter, message));
334
- if (!fs.existsSync(CREDENTIALS_FILE)) {
335
- throw new Error(`Missing WeChat credentials. Run "bun run setup" first. (${CREDENTIALS_FILE})`);
336
- }
344
+ await ensureCompanionStartWechatCredentials(options.adapter);
337
345
  const ready = await ensureBridgeReady(options);
338
346
  if (!ready.shouldOpenVisibleClient) {
339
347
  return 0;
@@ -199,6 +199,14 @@ export async function runLocalCompanion(options) {
199
199
  publishState();
200
200
  sendResponse(socket, message.id, true);
201
201
  break;
202
+ case "create_session":
203
+ if (!adapter.createSession) {
204
+ throw new Error(`/${adapter.getState().kind} does not support creating sessions from WeChat.`);
205
+ }
206
+ await adapter.createSession();
207
+ publishState();
208
+ sendResponse(socket, message.id, true);
209
+ break;
202
210
  case "interrupt":
203
211
  sendResponse(socket, message.id, true, await adapter.interrupt());
204
212
  break;
@@ -19,6 +19,12 @@ export class LegacyAdapterRuntime {
19
19
  async resumeSession(sessionId) {
20
20
  await this.adapter.resumeSession(sessionId);
21
21
  }
22
+ async createSession() {
23
+ if (!this.adapter.createSession) {
24
+ throw new Error(`/${this.adapter.getState().kind} does not support creating sessions from WeChat.`);
25
+ }
26
+ await this.adapter.createSession();
27
+ }
22
28
  async interrupt() {
23
29
  return await this.adapter.interrupt();
24
30
  }
@@ -64,22 +64,38 @@ export function ensureWorkspaceChannelDir(cwd) {
64
64
  fs.mkdirSync(paths.workspaceDir, { recursive: true });
65
65
  return paths;
66
66
  }
67
+ function channelDataDirHasState() {
68
+ try {
69
+ if (!fs.existsSync(CHANNEL_DATA_DIR)) {
70
+ return false;
71
+ }
72
+ return fs.readdirSync(CHANNEL_DATA_DIR).length > 0;
73
+ }
74
+ catch {
75
+ return true;
76
+ }
77
+ }
78
+ export function shouldMigrateLegacyCredentials(params) {
79
+ return (params.legacyCredentialsExists &&
80
+ !params.credentialsExists &&
81
+ !params.channelDataDirHadState);
82
+ }
67
83
  export function migrateLegacyChannelFiles(log) {
68
84
  const migrated = [];
85
+ const channelDataDirHadState = channelDataDirHasState();
69
86
  if (!fs.existsSync(LEGACY_CREDENTIALS_FILE) &&
70
87
  !fs.existsSync(LEGACY_SYNC_BUF_FILE)) {
71
88
  return migrated;
72
89
  }
73
90
  ensureChannelDataDir();
74
- const copyIfNeeded = (fromPath, toPath, label) => {
75
- if (!fs.existsSync(fromPath) || fs.existsSync(toPath)) {
76
- return;
77
- }
78
- fs.copyFileSync(fromPath, toPath);
79
- migrated.push(label);
80
- };
81
- copyIfNeeded(LEGACY_CREDENTIALS_FILE, CREDENTIALS_FILE, "credentials");
82
- copyIfNeeded(LEGACY_SYNC_BUF_FILE, SYNC_BUF_FILE, "sync state");
91
+ if (shouldMigrateLegacyCredentials({
92
+ channelDataDirHadState,
93
+ credentialsExists: fs.existsSync(CREDENTIALS_FILE),
94
+ legacyCredentialsExists: fs.existsSync(LEGACY_CREDENTIALS_FILE),
95
+ })) {
96
+ fs.copyFileSync(LEGACY_CREDENTIALS_FILE, CREDENTIALS_FILE);
97
+ migrated.push("credentials");
98
+ }
83
99
  if (migrated.length && log) {
84
100
  log(`Migrated legacy ${migrated.join(" and ")} from ${LEGACY_CHANNEL_DATA_DIR} to ${CHANNEL_DATA_DIR}.`);
85
101
  }
@@ -2,13 +2,17 @@
2
2
  /**
3
3
  * CLI WeChat Bridge setup.
4
4
  *
5
- * Run this before starting CLI WeChat Bridge:
6
- * bun run setup
5
+ * Installed bridge commands run this automatically on first use.
6
+ * To force a relogin manually:
7
+ * wechat-setup
7
8
  */
9
+ import crypto from "node:crypto";
8
10
  import fs from "node:fs";
9
11
  import readline from "node:readline";
10
- import { BOT_TYPE, CREDENTIALS_FILE, DEFAULT_BASE_URL, ensureChannelDataDir, migrateLegacyChannelFiles, } from "./channel-config.js";
11
- function loadExistingCredentials() {
12
+ import { BOT_TYPE, CONTEXT_CACHE_FILE, CREDENTIALS_FILE, DEFAULT_BASE_URL, ensureChannelDataDir, migrateLegacyChannelFiles, SYNC_BUF_FILE, } from "./channel-config.js";
13
+ import { isWechatSyncSessionTimeout } from "./wechat-transport.js";
14
+ const CHANNEL_VERSION = "0.3.0";
15
+ export function loadExistingCredentials() {
12
16
  try {
13
17
  if (!fs.existsSync(CREDENTIALS_FILE)) {
14
18
  return null;
@@ -19,6 +23,65 @@ function loadExistingCredentials() {
19
23
  return null;
20
24
  }
21
25
  }
26
+ export function getWechatLoginRequiredReason(account, options = {}) {
27
+ if (!account) {
28
+ return "No saved WeChat credentials found.";
29
+ }
30
+ if (options.requireUserId && !account.userId) {
31
+ return "Saved WeChat credentials are missing userId.";
32
+ }
33
+ return null;
34
+ }
35
+ function randomWechatUin() {
36
+ const uint32 = crypto.randomBytes(4).readUInt32BE(0);
37
+ return Buffer.from(String(uint32), "utf-8").toString("base64");
38
+ }
39
+ export async function getStoredCredentialsInvalidReason(account, options = {}) {
40
+ const baseUrl = account.baseUrl || DEFAULT_BASE_URL;
41
+ const base = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
42
+ const url = `${base}ilink/bot/getupdates`;
43
+ const body = JSON.stringify({
44
+ get_updates_buf: "",
45
+ base_info: { channel_version: CHANNEL_VERSION },
46
+ });
47
+ const controller = new AbortController();
48
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 5_000);
49
+ try {
50
+ const res = await fetch(url, {
51
+ method: "POST",
52
+ headers: {
53
+ "Content-Type": "application/json",
54
+ "Content-Length": String(Buffer.byteLength(body, "utf-8")),
55
+ AuthorizationType: "ilink_bot_token",
56
+ Authorization: `Bearer ${account.token}`,
57
+ "X-WECHAT-UIN": randomWechatUin(),
58
+ },
59
+ body,
60
+ signal: controller.signal,
61
+ });
62
+ const text = await res.text();
63
+ if (res.status === 401 || res.status === 403) {
64
+ return "Saved WeChat credentials were rejected by the server.";
65
+ }
66
+ if (!res.ok) {
67
+ return null;
68
+ }
69
+ const response = JSON.parse(text);
70
+ if (isWechatSyncSessionTimeout(response)) {
71
+ return "Saved WeChat login has expired.";
72
+ }
73
+ }
74
+ catch (error) {
75
+ if (error instanceof Error && error.name === "AbortError") {
76
+ return null;
77
+ }
78
+ return null;
79
+ }
80
+ finally {
81
+ clearTimeout(timer);
82
+ }
83
+ return null;
84
+ }
22
85
  async function fetchQRCode(baseUrl) {
23
86
  const base = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
24
87
  const url = `${base}ilink/bot/get_bot_qrcode?bot_type=${BOT_TYPE}`;
@@ -67,118 +130,154 @@ async function askYesNo(prompt) {
67
130
  rl.close();
68
131
  }
69
132
  }
70
- async function printQRCode(qrContent) {
133
+ async function printQRCode(qrContent, write) {
71
134
  try {
72
135
  const qrterm = await import("qrcode-terminal");
73
136
  await new Promise((resolve) => {
74
137
  qrterm.default.generate(qrContent, { small: true }, (qr) => {
75
- console.log(qr);
138
+ write(`${qr}\n`);
76
139
  resolve();
77
140
  });
78
141
  });
79
142
  }
80
143
  catch {
81
- console.log(`Open this QR code URL in a browser: ${qrContent}\n`);
144
+ write(`Open this QR code URL in a browser: ${qrContent}\n\n`);
82
145
  }
83
146
  }
84
- async function main() {
85
- migrateLegacyChannelFiles((message) => console.log(message));
86
- const existing = loadExistingCredentials();
87
- if (existing) {
88
- console.log(`Found saved account: ${existing.accountId}`);
89
- console.log(`Saved at: ${existing.savedAt}`);
90
- console.log(`Credentials file: ${CREDENTIALS_FILE}`);
91
- console.log();
92
- const shouldRelogin = await askYesNo("Log in again? (y/N) ");
93
- if (!shouldRelogin) {
94
- console.log("Keeping existing credentials.");
95
- return;
147
+ function saveCredentials(account) {
148
+ ensureChannelDataDir();
149
+ fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(account, null, 2), "utf-8");
150
+ for (const staleStateFile of [SYNC_BUF_FILE, CONTEXT_CACHE_FILE]) {
151
+ try {
152
+ fs.rmSync(staleStateFile, { force: true });
153
+ }
154
+ catch {
155
+ // Best effort cleanup.
96
156
  }
97
157
  }
98
- console.log("Fetching WeChat login QR code...\n");
99
- const qrResp = await fetchQRCode(DEFAULT_BASE_URL);
100
- await printQRCode(qrResp.qrcode_img_content);
101
- console.log("Scan the QR code in WeChat, then confirm on your phone.\n");
102
- const deadline = Date.now() + 480_000;
158
+ try {
159
+ fs.chmodSync(CREDENTIALS_FILE, 0o600);
160
+ }
161
+ catch {
162
+ // Best effort on Windows.
163
+ }
164
+ }
165
+ function printPostLoginHelp(log) {
166
+ log("This WeChat account is now authorized for the bridge.");
167
+ log("");
168
+ log("Start from any project directory with one of:");
169
+ log(" wechat-codex-start");
170
+ log(" wechat-claude-start");
171
+ log(" wechat-opencode-start");
172
+ log(" wechat-bridge-shell");
173
+ log("");
174
+ log("Manual two-terminal mode is also available:");
175
+ log(" wechat-bridge-codex + wechat-codex");
176
+ log(" wechat-bridge-claude + wechat-claude");
177
+ log(" wechat-bridge-opencode + wechat-opencode");
178
+ log("");
179
+ log("Run wechat-setup again any time you need to refresh the login.");
180
+ }
181
+ export async function runWechatLogin(options = {}) {
182
+ const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
183
+ const log = options.log ?? ((message) => console.log(message));
184
+ const write = options.write ?? ((message) => process.stdout.write(message));
185
+ const timeoutMs = options.timeoutMs ?? 480_000;
186
+ const pollIntervalMs = options.pollIntervalMs ?? 1_000;
187
+ log("Fetching WeChat login QR code...\n");
188
+ const qrResp = await fetchQRCode(baseUrl);
189
+ await printQRCode(qrResp.qrcode_img_content, write);
190
+ log("Scan the QR code above with WeChat, then confirm the login on your phone.\n");
191
+ const deadline = Date.now() + timeoutMs;
103
192
  let scannedPrinted = false;
104
193
  while (Date.now() < deadline) {
105
- const status = await pollQRStatus(DEFAULT_BASE_URL, qrResp.qrcode);
194
+ const status = await pollQRStatus(baseUrl, qrResp.qrcode);
106
195
  switch (status.status) {
107
196
  case "wait":
108
- process.stdout.write(".");
197
+ write(".");
109
198
  break;
110
199
  case "scaned":
111
200
  if (!scannedPrinted) {
112
- console.log("\nQR code scanned. Confirm the login in WeChat...");
201
+ log("\nQR code scanned. Confirm the login in WeChat...");
113
202
  scannedPrinted = true;
114
203
  }
115
204
  break;
116
205
  case "expired":
117
- console.log("\nThe QR code expired. Run setup again.");
118
- process.exit(1);
119
- break;
206
+ throw new Error("The QR code expired. Run setup again.");
120
207
  case "confirmed": {
121
208
  if (!status.ilink_bot_id || !status.bot_token) {
122
- console.error("\nLogin failed: missing bot credentials from server.");
123
- process.exit(1);
209
+ throw new Error("Login failed: missing bot credentials from server.");
210
+ }
211
+ if (options.requireUserId && !status.ilink_user_id) {
212
+ throw new Error("Login failed: missing WeChat userId from server.");
124
213
  }
125
214
  const account = {
126
215
  token: status.bot_token,
127
- baseUrl: status.baseurl || DEFAULT_BASE_URL,
216
+ baseUrl: status.baseurl || baseUrl,
128
217
  accountId: status.ilink_bot_id,
129
218
  userId: status.ilink_user_id,
130
219
  savedAt: new Date().toISOString(),
131
220
  };
132
- ensureChannelDataDir();
133
- fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(account, null, 2), "utf-8");
134
- try {
135
- fs.chmodSync(CREDENTIALS_FILE, 0o600);
136
- }
137
- catch {
138
- // Best effort on Windows.
139
- }
140
- console.log("\nWeChat login completed.");
141
- console.log(`Account ID: ${account.accountId}`);
142
- console.log(`User ID: ${account.userId ?? "(unknown)"}`);
143
- console.log(`Credentials saved to: ${CREDENTIALS_FILE}`);
144
- console.log();
145
- console.log("No /pair step is required.");
146
- console.log("The logged-in WeChat account above becomes the only authorized bridge owner.");
147
- console.log();
148
- console.log("After installing the package globally (for example: npm install -g . or npm link),");
149
- console.log("start the WeChat bridge from any directory with:");
150
- console.log(" wechat-bridge-codex");
151
- console.log(" wechat-codex # start this in a second terminal in the same directory");
152
- console.log(" wechat-bridge-opencode");
153
- console.log(" wechat-opencode # start this in a second terminal in the same directory");
154
- console.log(" wechat-bridge-claude");
155
- console.log(" wechat-claude # start this in a second terminal in the same directory");
156
- console.log(" wechat-bridge-shell");
157
- console.log(" On Linux/macOS, wechat-bridge-shell defaults to pwsh, then bash, zsh, or sh.");
158
- console.log(" Shell mode is a headless remote executor for non-interactive commands and scripts.");
159
- console.log();
160
- console.log("Repo-local development entrypoints are still available:");
161
- console.log(" bun run bridge:codex");
162
- console.log(" bun run codex:panel");
163
- console.log(" bun run bridge:opencode");
164
- console.log(" bun run opencode:panel");
165
- console.log(" bun run opencode:start");
166
- console.log(" bun run bridge:claude");
167
- console.log(" bun run claude:companion");
168
- console.log(" bun run bridge:shell");
169
- console.log();
170
- console.log("Legacy MCP server entrypoint:");
171
- console.log(" bun run start");
172
- return;
221
+ saveCredentials(account);
222
+ log("\nWeChat login completed.");
223
+ log(`Account ID: ${account.accountId}`);
224
+ log(`User ID: ${account.userId ?? "(unknown)"}`);
225
+ log(`Credentials saved to: ${CREDENTIALS_FILE}`);
226
+ log("");
227
+ return account;
228
+ }
229
+ }
230
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
231
+ }
232
+ throw new Error("Login timed out. Run setup again.");
233
+ }
234
+ export async function ensureWechatCredentials(options = {}) {
235
+ const log = options.log ?? ((message) => console.log(message));
236
+ migrateLegacyChannelFiles(log);
237
+ const existing = loadExistingCredentials();
238
+ const loginReason = getWechatLoginRequiredReason(existing, {
239
+ requireUserId: options.requireUserId,
240
+ });
241
+ if (!loginReason) {
242
+ const account = existing;
243
+ if (options.validateExisting) {
244
+ const invalidReason = await getStoredCredentialsInvalidReason(account, {
245
+ timeoutMs: options.validationTimeoutMs,
246
+ });
247
+ if (invalidReason) {
248
+ log(`${invalidReason} Starting WeChat login...`);
249
+ const login = options.login ?? (() => runWechatLogin(options));
250
+ return login();
173
251
  }
174
252
  }
175
- await new Promise((resolve) => setTimeout(resolve, 1000));
253
+ return account;
254
+ }
255
+ log(`${loginReason} Starting WeChat login...`);
256
+ const login = options.login ?? (() => runWechatLogin(options));
257
+ return login();
258
+ }
259
+ async function main() {
260
+ migrateLegacyChannelFiles((message) => console.log(message));
261
+ const existing = loadExistingCredentials();
262
+ if (existing) {
263
+ console.log(`Found saved account: ${existing.accountId}`);
264
+ console.log(`Saved at: ${existing.savedAt}`);
265
+ console.log(`Credentials file: ${CREDENTIALS_FILE}`);
266
+ console.log();
267
+ const shouldRelogin = await askYesNo("Log in again? (y/N) ");
268
+ if (!shouldRelogin) {
269
+ console.log("Keeping existing credentials.");
270
+ return;
271
+ }
176
272
  }
177
- console.log("\nLogin timed out. Run setup again.");
178
- process.exit(1);
273
+ await runWechatLogin({ requireUserId: true });
274
+ printPostLoginHelp((message) => console.log(message));
275
+ }
276
+ const isDirectRun = Boolean(import.meta.main);
277
+ if (isDirectRun) {
278
+ main().catch((err) => {
279
+ const message = err instanceof Error ? err.message : String(err);
280
+ console.error(`Error: ${message}`);
281
+ process.exit(1);
282
+ });
179
283
  }
180
- main().catch((err) => {
181
- const message = err instanceof Error ? err.message : String(err);
182
- console.error(`Error: ${message}`);
183
- process.exit(1);
184
- });
@@ -12,6 +12,7 @@ const SEND_TIMEOUT_MS = 15_000;
12
12
  const CDN_MAX_RETRIES = 3;
13
13
  const ERROR_CAUSE_DEPTH_LIMIT = 4;
14
14
  const INBOUND_MESSAGE_CLAIM_TTL_MS = 10 * 60 * 1000;
15
+ const SYNC_SESSION_TIMEOUT_ERRCODE = -14;
15
16
  const MSG_TYPE_USER = 1;
16
17
  const MSG_TYPE_BOT = 2;
17
18
  const MSG_ITEM_TEXT = 1;
@@ -24,6 +25,10 @@ const UPLOAD_MEDIA_TYPE_IMAGE = 1;
24
25
  const UPLOAD_MEDIA_TYPE_VIDEO = 2;
25
26
  const UPLOAD_MEDIA_TYPE_FILE = 3;
26
27
  const UPLOAD_MEDIA_TYPE_VOICE = 4;
28
+ export function isWechatSyncSessionTimeout(response) {
29
+ return (response.errcode === SYNC_SESSION_TIMEOUT_ERRCODE &&
30
+ /session timeout/i.test(response.errmsg ?? ""));
31
+ }
27
32
  const DEFAULT_MEDIA_UPLOAD_LIMIT_MB = {
28
33
  image: 20,
29
34
  file: 50,
@@ -199,6 +204,10 @@ export function describeWechatTransportError(error) {
199
204
  }
200
205
  export function classifyWechatTransportError(error) {
201
206
  if (error instanceof Error) {
207
+ if (/WeChat session timed out/i.test(error.message) ||
208
+ /errcode=-14\b.*session timeout/i.test(error.message)) {
209
+ return { kind: "auth", retryable: false };
210
+ }
202
211
  if (error.name === "AbortError") {
203
212
  return { kind: "timeout", retryable: true };
204
213
  }
@@ -539,7 +548,15 @@ export class WeChatTransport {
539
548
  async pollMessages(options = {}) {
540
549
  const timeoutMs = options.timeoutMs ?? DEFAULT_LONG_POLL_TIMEOUT_MS;
541
550
  const account = this.requireAccount();
542
- const response = await this.getUpdates(account, timeoutMs);
551
+ let response = await this.getUpdates(account, timeoutMs);
552
+ if (isWechatSyncSessionTimeout(response) && this.syncBuffer) {
553
+ this.logger.log("WeChat sync session timed out. Clearing local sync cursor and retrying once.");
554
+ this.clearSyncBuffer();
555
+ response = await this.getUpdates(account, timeoutMs);
556
+ }
557
+ if (isWechatSyncSessionTimeout(response)) {
558
+ throw new Error('WeChat session timed out. Run "wechat-setup" to log in again.');
559
+ }
543
560
  const isError = (response.ret !== undefined && response.ret !== 0) ||
544
561
  (response.errcode !== undefined && response.errcode !== 0);
545
562
  if (isError) {
@@ -689,7 +706,7 @@ export class WeChatTransport {
689
706
  requireAccount() {
690
707
  const account = this.getCredentials();
691
708
  if (!account) {
692
- throw new Error(`No saved WeChat credentials found. Run "bun run setup" first. Expected file: ${CREDENTIALS_FILE}`);
709
+ throw new Error(`No saved WeChat credentials found. Start a bridge command in a terminal to log in automatically, or run "wechat-setup". Expected file: ${CREDENTIALS_FILE}`);
693
710
  }
694
711
  return account;
695
712
  }
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@unlinearity/cli-wechat-bridge",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "CLI WeChat Bridge for Codex, Claude Code, OpenCode, and PowerShell over WeChat",
5
5
  "type": "module",
6
- "files": [
7
- "bin/",
8
- "dist/",
9
- "README.md",
10
- "LICENSE.txt"
11
- ],
6
+ "files": [
7
+ "bin/",
8
+ "dist/",
9
+ "README.md",
10
+ "LICENSE.txt"
11
+ ],
12
12
  "keywords": [
13
13
  "wechat",
14
14
  "cli",
@@ -43,13 +43,14 @@
43
43
  "wechat-bridge-opencode": "bin/wechat-bridge-opencode.mjs",
44
44
  "wechat-opencode": "bin/wechat-opencode.mjs",
45
45
  "wechat-opencode-start": "bin/wechat-opencode-start.mjs",
46
+ "wechat-setup": "bin/wechat-setup.mjs",
46
47
  "wechat-check-update": "bin/wechat-check-update.mjs"
47
48
  },
48
- "scripts": {
49
- "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
50
- "build": "npm run clean && tsc -p tsconfig.build.json",
51
- "prepack": "npm run build",
52
- "setup": "node --no-warnings --experimental-strip-types src/wechat/setup.ts",
49
+ "scripts": {
50
+ "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
51
+ "build": "npm run clean && tsc -p tsconfig.build.json",
52
+ "prepack": "npm run build",
53
+ "setup": "node --no-warnings --experimental-strip-types src/wechat/setup.ts",
53
54
  "start": "node --no-warnings --experimental-strip-types src/wechat/wechat-channel.ts",
54
55
  "check": "node --no-warnings --experimental-strip-types src/wechat/wechat-channel.ts --check",
55
56
  "bridge": "node --no-warnings --experimental-strip-types src/bridge/wechat-bridge.ts",
@@ -82,8 +83,8 @@
82
83
  "@types/bun": "latest",
83
84
  "typescript": "^6.0.2"
84
85
  },
85
- "engines": {
86
- "bun": ">=1.0.0",
87
- "node": ">=24.0.0"
88
- }
89
- }
86
+ "engines": {
87
+ "bun": ">=1.0.0",
88
+ "node": ">=24.0.0"
89
+ }
90
+ }