opencode-copilot-account-switcher 0.14.25 → 0.14.27

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.
@@ -31,7 +31,44 @@ const TOUCH_WRITE_CACHE_IDLE_TTL_MS = 30 * 60 * 1000;
31
31
  const MAX_TOUCH_WRITE_CACHE_ENTRIES = 2048;
32
32
  const INTERNAL_DEBUG_LINK_HEADER = "x-opencode-debug-link-id";
33
33
  let wechatBridgeLifecycleState;
34
+ let wechatBridgeSessionState;
34
35
  let wechatBridgeAutoCloseAttached = false;
36
+ function isNonEmptyString(value) {
37
+ return typeof value === "string" && value.trim().length > 0;
38
+ }
39
+ function ensureWechatBridgeSessionState(key) {
40
+ if (wechatBridgeSessionState?.key === key) {
41
+ return wechatBridgeSessionState;
42
+ }
43
+ const state = { key };
44
+ wechatBridgeSessionState = state;
45
+ return state;
46
+ }
47
+ function trackWechatBridgeSelectedSession(state, sessionID) {
48
+ if (!state || !isNonEmptyString(sessionID)) {
49
+ return;
50
+ }
51
+ state.selectedSessionID = sessionID;
52
+ }
53
+ function trackWechatBridgeInteractedSession(state, sessionID) {
54
+ if (!state || !isNonEmptyString(sessionID)) {
55
+ return;
56
+ }
57
+ state.interactedSessionID = sessionID;
58
+ }
59
+ function getWechatBridgeActiveSessionID(state) {
60
+ return state?.selectedSessionID ?? state?.interactedSessionID;
61
+ }
62
+ function handleWechatBridgeEvent(state, event) {
63
+ if (!state || typeof event !== "object" || event === null) {
64
+ return;
65
+ }
66
+ const payload = event;
67
+ if (payload.type !== "tui.session.select") {
68
+ return;
69
+ }
70
+ trackWechatBridgeSelectedSession(state, payload.properties?.sessionID);
71
+ }
35
72
  function buildWechatBridgeLifecycleKey(input) {
36
73
  const projectName = typeof input.project?.name === "string" ? input.project.name : "";
37
74
  const projectId = typeof input.project?.id === "string" ? input.project.id : "";
@@ -611,6 +648,16 @@ export function buildPluginHooks(input) {
611
648
  const ensureWechatBrokerStarted = input.ensureWechatBrokerStarted ?? (async () => connectOrSpawnBroker());
612
649
  const createWechatBridgeLifecycleImpl = input.createWechatBridgeLifecycleImpl ?? createWechatBridgeLifecycle;
613
650
  const wechatBridgeClient = toWechatBridgeClient(input.client);
651
+ const wechatBridgeLifecycleKey = input.serverUrl && wechatBridgeClient
652
+ ? buildWechatBridgeLifecycleKey({
653
+ directory: input.directory,
654
+ serverUrl: input.serverUrl,
655
+ project: input.project,
656
+ })
657
+ : undefined;
658
+ const wechatBridgeSessionContext = wechatBridgeLifecycleKey
659
+ ? ensureWechatBridgeSessionState(wechatBridgeLifecycleKey)
660
+ : undefined;
614
661
  if (wechatBridgeClient) {
615
662
  void showStatusToast({
616
663
  client: input.client,
@@ -624,15 +671,10 @@ export function buildPluginHooks(input) {
624
671
  .then(() => ensureWechatBrokerStarted())
625
672
  .catch(() => { });
626
673
  }
627
- if (input.serverUrl && wechatBridgeClient) {
628
- const lifecycleKey = buildWechatBridgeLifecycleKey({
629
- directory: input.directory,
630
- serverUrl: input.serverUrl,
631
- project: input.project,
632
- });
674
+ if (input.serverUrl && wechatBridgeClient && wechatBridgeLifecycleKey) {
633
675
  attachWechatBridgeAutoClose();
634
676
  void ensureWechatBridgeLifecycle({
635
- key: lifecycleKey,
677
+ key: wechatBridgeLifecycleKey,
636
678
  create: async () => {
637
679
  return createWechatBridgeLifecycleImpl({
638
680
  client: wechatBridgeClient,
@@ -640,6 +682,7 @@ export function buildPluginHooks(input) {
640
682
  directory: input.directory,
641
683
  serverUrl: input.serverUrl,
642
684
  statusCollectionEnabled: true,
685
+ getActiveSessionID: () => getWechatBridgeActiveSessionID(wechatBridgeSessionContext),
643
686
  });
644
687
  },
645
688
  }).catch(() => { });
@@ -1262,6 +1305,7 @@ export function buildPluginHooks(input) {
1262
1305
  })
1263
1306
  : Promise.resolve(async () => { });
1264
1307
  const chatHeaders = async (hookInput, output) => {
1308
+ trackWechatBridgeInteractedSession(wechatBridgeSessionContext, hookInput.sessionID);
1265
1309
  if (enableCodexAuthLoader) {
1266
1310
  if (hookInput.model.providerID !== authProvider)
1267
1311
  return;
@@ -1364,6 +1408,9 @@ export function buildPluginHooks(input) {
1364
1408
  output.headers["x-opencode-session-id"] = hookInput.sessionID;
1365
1409
  };
1366
1410
  return {
1411
+ event: async ({ event }) => {
1412
+ handleWechatBridgeEvent(wechatBridgeSessionContext, event);
1413
+ },
1367
1414
  auth: {
1368
1415
  ...input.auth,
1369
1416
  provider: authProvider,
@@ -1407,6 +1454,7 @@ export function buildPluginHooks(input) {
1407
1454
  }
1408
1455
  },
1409
1456
  "command.execute.before": async (hookInput) => {
1457
+ trackWechatBridgeInteractedSession(wechatBridgeSessionContext, hookInput.sessionID);
1410
1458
  const store = await loadMergedStore();
1411
1459
  if (hookInput.command === "copilot-inject") {
1412
1460
  if (!enableCopilotAuthLoader)
@@ -1475,6 +1523,7 @@ export function buildPluginHooks(input) {
1475
1523
  }
1476
1524
  },
1477
1525
  "tool.execute.before": async (hookInput) => {
1526
+ trackWechatBridgeInteractedSession(wechatBridgeSessionContext, hookInput.sessionID);
1478
1527
  if (!injectArmed)
1479
1528
  return;
1480
1529
  if (hookInput.tool !== "question")
@@ -1482,6 +1531,7 @@ export function buildPluginHooks(input) {
1482
1531
  injectArmed = false;
1483
1532
  },
1484
1533
  "tool.execute.after": async (hookInput, output) => {
1534
+ trackWechatBridgeInteractedSession(wechatBridgeSessionContext, hookInput.sessionID);
1485
1535
  if (hookInput.tool === "question") {
1486
1536
  injectArmed = false;
1487
1537
  return;
@@ -7,7 +7,6 @@ import { recoverInvalidCodexAccount } from "../codex-invalid-account.js";
7
7
  import { readAuth } from "../store.js";
8
8
  import { readCommonSettingsStore, writeCommonSettingsStore, } from "../common-settings-store.js";
9
9
  import { applyCommonSettingsAction } from "../common-settings-actions.js";
10
- import { runWechatBindFlow } from "../wechat/bind-flow.js";
11
10
  function pickName(input) {
12
11
  const accountId = input.accountId?.trim();
13
12
  if (accountId)
@@ -384,6 +383,7 @@ export function createCodexMenuAdapter(inputDeps) {
384
383
  return false;
385
384
  }
386
385
  if (action.name === "wechat-bind" || action.name === "wechat-rebind") {
386
+ const { runWechatBindFlow } = await import("../wechat/bind-flow.js");
387
387
  await runWechatBindFlow({
388
388
  action: action.name,
389
389
  readCommonSettings,
@@ -5,7 +5,6 @@ import { getGitHubToken, normalizeDomain } from "../copilot-api-helpers.js";
5
5
  import { listAssignableAccountsForModel, listKnownCopilotModels, rewriteModelAccountAssignments, } from "../model-account-map.js";
6
6
  import { applyMenuAction, persistAccountSwitch } from "../plugin-actions.js";
7
7
  import { readCommonSettingsStore, writeCommonSettingsStore, } from "../common-settings-store.js";
8
- import { runWechatBindFlow } from "../wechat/bind-flow.js";
9
8
  import { select, selectMany } from "../ui/select.js";
10
9
  import { authPath, readAuth, readStore } from "../store.js";
11
10
  const CLIENT_ID = "Ov23li8tweQw6odWQebz";
@@ -742,6 +741,7 @@ export function createCopilotMenuAdapter(inputDeps) {
742
741
  return false;
743
742
  }
744
743
  if (action.name === "wechat-bind" || action.name === "wechat-rebind") {
744
+ const { runWechatBindFlow } = await import("../wechat/bind-flow.js");
745
745
  await runWechatBindFlow({
746
746
  action: action.name,
747
747
  readCommonSettings,
@@ -1,8 +1,8 @@
1
1
  import { bindOperator, readOperatorBinding, rebindOperator, resetOperatorBinding } from "./operator-store.js";
2
2
  import { loadOpenClawWeixinPublicHelpers } from "./compat/openclaw-public-helpers.js";
3
3
  import { buildOpenClawMenuAccount } from "./openclaw-account-adapter.js";
4
+ import { loadQrCodeTerminal } from "./compat/qrcode-terminal-loader.js";
4
5
  import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
5
- import qrcodeTerminal from "qrcode-terminal";
6
6
  const DEFAULT_QR_WAIT_TIMEOUT_MS = 480000;
7
7
  function pickFirstNonEmptyString(...values) {
8
8
  for (const value of values) {
@@ -41,6 +41,7 @@ function isSameOperatorBinding(left, right) {
41
41
  return left.wechatAccountId === right.wechatAccountId && left.userId === right.userId && left.boundAt === right.boundAt;
42
42
  }
43
43
  async function renderQrTerminalDefault(input) {
44
+ const qrcodeTerminal = loadQrCodeTerminal();
44
45
  return await new Promise((resolve) => {
45
46
  qrcodeTerminal.generate(input.value, { small: true }, (output) => {
46
47
  resolve(typeof output === "string" && output.trim().length > 0 ? output : undefined);
@@ -52,6 +52,7 @@ export type WechatBridgeInput = {
52
52
  directory: string;
53
53
  client: WechatBridgeClient;
54
54
  liveReadTimeoutMs?: number;
55
+ getActiveSessionID?: () => string | undefined;
55
56
  onDiagnosticEvent?: (event: WechatBridgeDiagnosticEvent) => Promise<void> | void;
56
57
  };
57
58
  export type WechatBridge = {
@@ -67,6 +68,7 @@ export type WechatBridgeLifecycleInput = {
67
68
  serverUrl?: URL;
68
69
  statusCollectionEnabled?: boolean;
69
70
  heartbeatIntervalMs?: number;
71
+ getActiveSessionID?: () => string | undefined;
70
72
  };
71
73
  export type WechatBridgeLifecycle = {
72
74
  close: () => Promise<void>;
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import path from "node:path";
2
3
  import { appendFile, mkdir } from "node:fs/promises";
3
4
  import { connect } from "./broker-client.js";
@@ -6,6 +7,7 @@ import { WECHAT_FILE_MODE, wechatBridgeDiagnosticsPath } from "./state-paths.js"
6
7
  import { buildSessionDigest, groupPermissionsBySession, groupQuestionsBySession, pickRecentSessions, } from "./session-digest.js";
7
8
  const DEFAULT_LIVE_READ_TIMEOUT_MS = 2_000;
8
9
  const DEFAULT_HEARTBEAT_INTERVAL_MS = 10_000;
10
+ const PROCESS_INSTANCE_ID = toSafeInstanceID(`wechat-${process.pid}-${randomUUID().slice(0, 8)}`);
9
11
  function toSafeInstanceID(input) {
10
12
  const normalized = input.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
11
13
  if (normalized.length === 0) {
@@ -13,6 +15,9 @@ function toSafeInstanceID(input) {
13
15
  }
14
16
  return normalized.slice(0, 64);
15
17
  }
18
+ function isNonEmptyString(value) {
19
+ return typeof value === "string" && value.trim().length > 0;
20
+ }
16
21
  function toProjectName(project) {
17
22
  if (typeof project?.name === "string" && project.name.trim().length > 0) {
18
23
  return project.name.trim();
@@ -149,6 +154,27 @@ export function createWechatBridge(input) {
149
154
  : DEFAULT_LIVE_READ_TIMEOUT_MS;
150
155
  const unavailable = new Set();
151
156
  const onDiagnosticEvent = input.onDiagnosticEvent;
157
+ const activeSessionID = input.getActiveSessionID?.();
158
+ if (input.getActiveSessionID && !isNonEmptyString(activeSessionID)) {
159
+ const snapshot = {
160
+ instanceID: input.instanceID,
161
+ instanceName: input.instanceName,
162
+ pid: input.pid,
163
+ projectName: input.projectName,
164
+ directory: input.directory,
165
+ collectedAt: Date.now(),
166
+ sessions: [],
167
+ unavailable: undefined,
168
+ };
169
+ void Promise.resolve(onDiagnosticEvent?.({
170
+ type: "collectStatusCompleted",
171
+ instanceID: input.instanceID,
172
+ durationMs: Date.now() - startedAt,
173
+ sessionCount: 0,
174
+ unavailable: snapshot.unavailable,
175
+ })).catch(() => { });
176
+ return snapshot;
177
+ }
152
178
  const [sessionListResult, statusResult, questionResult, permissionResult] = await Promise.allSettled([
153
179
  wrapDiagnosticStage({ instanceID: input.instanceID, stage: "session.list", onDiagnosticEvent }, () => withTimeout(async () => unwrapSdkReadResult(await input.client.session.list(), "session.list"), liveReadTimeoutMs, "session.list")),
154
180
  wrapDiagnosticStage({ instanceID: input.instanceID, stage: "session.status", onDiagnosticEvent }, () => withTimeout(async () => unwrapSdkReadResult(await input.client.session.status(), "session.status"), liveReadTimeoutMs, "session.status")),
@@ -156,7 +182,9 @@ export function createWechatBridge(input) {
156
182
  wrapDiagnosticStage({ instanceID: input.instanceID, stage: "permission.list", onDiagnosticEvent }, () => withTimeout(async () => unwrapSdkReadResult(await input.client.permission.list(), "permission.list"), liveReadTimeoutMs, "permission.list")),
157
183
  ]);
158
184
  const sessions = sessionListResult.status === "fulfilled" ? sessionListResult.value : [];
159
- const recentSessions = pickRecentSessions(sessions, 3);
185
+ const recentSessions = isNonEmptyString(activeSessionID)
186
+ ? sessions.filter((session) => session.id === activeSessionID).slice(0, 1)
187
+ : pickRecentSessions(sessions, 3);
160
188
  if (sessionListResult.status === "rejected") {
161
189
  unavailable.add("sessionStatus");
162
190
  }
@@ -224,7 +252,7 @@ export async function createWechatBridgeLifecycle(input, deps = {}) {
224
252
  const clearIntervalImpl = deps.clearIntervalImpl ?? clearInterval;
225
253
  const directory = toDirectory(input.directory);
226
254
  const projectName = toProjectName(input.project);
227
- const instanceID = toInstanceID(projectName, directory);
255
+ const instanceID = PROCESS_INSTANCE_ID;
228
256
  const bridge = createWechatBridge({
229
257
  instanceID,
230
258
  instanceName: toInstanceName(projectName, directory),
@@ -232,6 +260,7 @@ export async function createWechatBridgeLifecycle(input, deps = {}) {
232
260
  projectName,
233
261
  directory,
234
262
  client: input.client,
263
+ getActiveSessionID: input.getActiveSessionID,
235
264
  onDiagnosticEvent: createWechatBridgeDiagnosticsWriter(),
236
265
  });
237
266
  const broker = await connectOrSpawnBrokerImpl();
@@ -0,0 +1,11 @@
1
+ export type JitiLoader = (path: string) => unknown;
2
+ type CreateJiti = (id: string | URL, options?: Record<string, unknown>) => JitiLoader;
3
+ type JitiNamespace = {
4
+ createJiti?: unknown;
5
+ default?: unknown;
6
+ };
7
+ export declare function resolveCreateJiti(namespace: JitiNamespace): CreateJiti;
8
+ export declare function loadJiti(requireImpl?: NodeRequire): {
9
+ createJiti: CreateJiti;
10
+ };
11
+ export {};
@@ -0,0 +1,27 @@
1
+ import { createRequire } from "node:module";
2
+ function isCreateJiti(value) {
3
+ return typeof value === "function";
4
+ }
5
+ export function resolveCreateJiti(namespace) {
6
+ if (isCreateJiti(namespace)) {
7
+ return namespace;
8
+ }
9
+ if (isCreateJiti(namespace.createJiti)) {
10
+ return namespace.createJiti;
11
+ }
12
+ if (isCreateJiti(namespace.default)) {
13
+ return namespace.default;
14
+ }
15
+ if (namespace.default &&
16
+ typeof namespace.default === "object" &&
17
+ isCreateJiti(namespace.default.createJiti)) {
18
+ return namespace.default.createJiti;
19
+ }
20
+ throw new Error("[wechat-compat] createJiti export unavailable");
21
+ }
22
+ export function loadJiti(requireImpl = createRequire(import.meta.url)) {
23
+ const namespace = requireImpl("jiti");
24
+ return {
25
+ createJiti: resolveCreateJiti(namespace),
26
+ };
27
+ }
@@ -1,12 +1,12 @@
1
1
  import { createRequire } from "node:module";
2
- import { createJiti } from "jiti";
2
+ import { loadJiti } from "./jiti-loader.js";
3
3
  const OPENCLAW_WEIXIN_ACCOUNTS_MODULE = "@tencent-weixin/openclaw-weixin/src/auth/accounts.ts";
4
4
  let accountJitiLoader = null;
5
5
  function getAccountJiti() {
6
6
  if (accountJitiLoader) {
7
7
  return accountJitiLoader;
8
8
  }
9
- accountJitiLoader = createJiti(import.meta.url, {
9
+ accountJitiLoader = loadJiti().createJiti(import.meta.url, {
10
10
  interopDefault: true,
11
11
  extensions: [".ts", ".tsx", ".mts", ".cts", ".js", ".mjs", ".cjs", ".json"],
12
12
  });
@@ -1,7 +1,7 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { createRequire } from "node:module";
3
3
  import path from "node:path";
4
- import { createJiti } from "jiti";
4
+ import { loadJiti } from "./jiti-loader.js";
5
5
  let publicJitiLoader = null;
6
6
  function requireField(condition, message) {
7
7
  if (!condition) {
@@ -12,7 +12,7 @@ function getPublicJiti() {
12
12
  if (publicJitiLoader) {
13
13
  return publicJitiLoader;
14
14
  }
15
- publicJitiLoader = createJiti(import.meta.url, {
15
+ publicJitiLoader = loadJiti().createJiti(import.meta.url, {
16
16
  interopDefault: true,
17
17
  extensions: [".ts", ".tsx", ".mts", ".cts", ".js", ".mjs", ".cjs", ".json"],
18
18
  });
@@ -1,7 +1,7 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { createRequire } from "node:module";
3
3
  import path from "node:path";
4
- import { createJiti } from "jiti";
4
+ import { loadJiti } from "./jiti-loader.js";
5
5
  const OPENCLAW_SYNC_BUF_MODULE = "@tencent-weixin/openclaw-weixin/src/storage/sync-buf.ts";
6
6
  const OPENCLAW_STATE_DIR_MODULE = "@tencent-weixin/openclaw-weixin/src/storage/state-dir.ts";
7
7
  let syncBufJitiLoader = null;
@@ -9,7 +9,7 @@ function getSyncBufJiti() {
9
9
  if (syncBufJitiLoader) {
10
10
  return syncBufJitiLoader;
11
11
  }
12
- syncBufJitiLoader = createJiti(import.meta.url, {
12
+ syncBufJitiLoader = loadJiti().createJiti(import.meta.url, {
13
13
  interopDefault: true,
14
14
  extensions: [".ts", ".tsx", ".mts", ".cts", ".js", ".mjs", ".cjs", ".json"],
15
15
  });
@@ -1,5 +1,5 @@
1
1
  import { createRequire } from "node:module";
2
- import { createJiti } from "jiti";
2
+ import { loadJiti } from "./jiti-loader.js";
3
3
  const OPENCLAW_UPDATES_MODULE = "@tencent-weixin/openclaw-weixin/src/api/api.ts";
4
4
  const OPENCLAW_SEND_MODULE = "@tencent-weixin/openclaw-weixin/src/messaging/send.ts";
5
5
  let updatesSendJitiLoader = null;
@@ -7,7 +7,7 @@ function getUpdatesSendJiti() {
7
7
  if (updatesSendJitiLoader) {
8
8
  return updatesSendJitiLoader;
9
9
  }
10
- updatesSendJitiLoader = createJiti(import.meta.url, {
10
+ updatesSendJitiLoader = loadJiti().createJiti(import.meta.url, {
11
11
  interopDefault: true,
12
12
  extensions: [".ts", ".tsx", ".mts", ".cts", ".js", ".mjs", ".cjs", ".json"],
13
13
  });
@@ -0,0 +1,12 @@
1
+ type QrCodeTerminal = {
2
+ generate(input: string, opts: {
3
+ small?: boolean;
4
+ }, cb?: (output: string) => void): void;
5
+ };
6
+ type QrCodeTerminalNamespace = {
7
+ default?: unknown;
8
+ generate?: unknown;
9
+ };
10
+ export declare function resolveQrCodeTerminal(namespace: QrCodeTerminalNamespace): QrCodeTerminal;
11
+ export declare function loadQrCodeTerminal(requireImpl?: NodeRequire): QrCodeTerminal;
12
+ export {};
@@ -0,0 +1,16 @@
1
+ import { createRequire } from "node:module";
2
+ function isQrCodeTerminal(value) {
3
+ return Boolean(value && typeof value === "object" && typeof value.generate === "function");
4
+ }
5
+ export function resolveQrCodeTerminal(namespace) {
6
+ if (isQrCodeTerminal(namespace)) {
7
+ return namespace;
8
+ }
9
+ if (isQrCodeTerminal(namespace.default)) {
10
+ return namespace.default;
11
+ }
12
+ throw new Error("[wechat-compat] qrcode-terminal export unavailable");
13
+ }
14
+ export function loadQrCodeTerminal(requireImpl = createRequire(import.meta.url)) {
15
+ return resolveQrCodeTerminal(requireImpl("qrcode-terminal"));
16
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-copilot-account-switcher",
3
- "version": "0.14.25",
3
+ "version": "0.14.27",
4
4
  "description": "GitHub Copilot account switcher plugin for OpenCode",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",