opencode-copilot-account-switcher 0.14.8 → 0.14.10

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/dist/ui/menu.js CHANGED
@@ -544,7 +544,7 @@ export async function showMenuWithDeps(accounts, input = {}, deps = {}) {
544
544
  return { type: "cancel" };
545
545
  if (result.type === "wechat-menu") {
546
546
  const [commonSettings, operatorBinding] = await Promise.all([
547
- input.wechatPrimaryBinding
547
+ input.wechatPrimaryBinding || input.wechatOperatorBinding
548
548
  ? Promise.resolve(undefined)
549
549
  : (deps.readCommonSettings ?? readCommonSettingsStore)().catch(() => undefined),
550
550
  input.wechatOperatorBinding
@@ -151,13 +151,8 @@ export async function runWechatBindFlow(input) {
151
151
  }
152
152
  catch (error) {
153
153
  if (shouldRollbackBinding) {
154
- if (input.action === "wechat-bind") {
155
- const currentOperatorBinding = await loadOperatorBinding().catch(() => undefined);
156
- if (isSameOperatorBinding(currentOperatorBinding, attemptedOperatorBinding)) {
157
- await rollbackBinding(input.action, previousOperatorBinding, persistOperatorRebinding, clearOperatorBinding);
158
- }
159
- }
160
- else {
154
+ const currentOperatorBinding = await loadOperatorBinding().catch(() => undefined);
155
+ if (isSameOperatorBinding(currentOperatorBinding, attemptedOperatorBinding)) {
161
156
  await rollbackBinding(input.action, previousOperatorBinding, persistOperatorRebinding, clearOperatorBinding);
162
157
  }
163
158
  }
@@ -1,5 +1,5 @@
1
1
  import { type QuestionAnswer } from "@opencode-ai/sdk/v2";
2
- import { type WechatStatusRuntime } from "./wechat-status-runtime.js";
2
+ import { type WechatStatusRuntime, type WechatStatusRuntimeDiagnosticEvent } from "./wechat-status-runtime.js";
3
3
  import type { WechatSlashCommand } from "./command-parser.js";
4
4
  type BrokerWechatStatusRuntimeLifecycle = {
5
5
  start: () => Promise<void>;
@@ -10,9 +10,12 @@ type BrokerWechatStatusRuntimeLifecycleDeps = {
10
10
  onSlashCommand: (input: {
11
11
  command: import("./command-parser.js").WechatSlashCommand;
12
12
  }) => Promise<string>;
13
+ onDiagnosticEvent: (event: WechatStatusRuntimeDiagnosticEvent) => void | Promise<void>;
13
14
  }) => WechatStatusRuntime;
14
15
  handleWechatSlashCommand?: (command: import("./command-parser.js").WechatSlashCommand) => Promise<string>;
15
16
  onRuntimeError?: (error: unknown) => void;
17
+ onDiagnosticEvent?: (event: WechatStatusRuntimeDiagnosticEvent) => void | Promise<void>;
18
+ stateRoot?: string;
16
19
  };
17
20
  export declare function shouldEnableBrokerWechatStatusRuntime(env?: NodeJS.ProcessEnv): boolean;
18
21
  type BrokerWechatSlashHandlerClient = {
@@ -1,12 +1,12 @@
1
1
  import path from "node:path";
2
2
  import process from "node:process";
3
3
  import { readFileSync, rmSync } from "node:fs";
4
- import { mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { createOpencodeClient as createOpencodeClientV2 } from "@opencode-ai/sdk/v2";
7
7
  import { startBrokerServer } from "./broker-server.js";
8
- import { WECHAT_FILE_MODE, wechatStateRoot } from "./state-paths.js";
9
- import { createWechatStatusRuntime } from "./wechat-status-runtime.js";
8
+ import { WECHAT_FILE_MODE, wechatStateRoot, wechatStatusRuntimeDiagnosticsPath } from "./state-paths.js";
9
+ import { createWechatStatusRuntime, } from "./wechat-status-runtime.js";
10
10
  const BROKER_WECHAT_RUNTIME_AUTOSTART_DELAY_MS = 1_000;
11
11
  async function readPackageVersion() {
12
12
  const packageJsonPath = new URL("../../package.json", import.meta.url);
@@ -52,6 +52,22 @@ async function writeBrokerState(state, stateRoot) {
52
52
  const filePath = brokerStatePathForRoot(stateRoot);
53
53
  await writeFile(filePath, JSON.stringify(state, null, 2), { mode: WECHAT_FILE_MODE });
54
54
  }
55
+ function createWechatStatusRuntimeDiagnosticsFileWriter(input) {
56
+ return async (event) => {
57
+ try {
58
+ await mkdir(input.stateRoot, { recursive: true, mode: 0o700 });
59
+ const filePath = wechatStatusRuntimeDiagnosticsPath(input.stateRoot);
60
+ const line = `${JSON.stringify({
61
+ timestamp: Date.now(),
62
+ ...event,
63
+ })}\n`;
64
+ await appendFile(filePath, line, { encoding: "utf8", mode: WECHAT_FILE_MODE });
65
+ }
66
+ catch (error) {
67
+ input.onRuntimeError(error);
68
+ }
69
+ };
70
+ }
55
71
  export function shouldEnableBrokerWechatStatusRuntime(env = process.env) {
56
72
  void env;
57
73
  return true;
@@ -103,6 +119,8 @@ export function createBrokerWechatSlashCommandHandler(input) {
103
119
  }
104
120
  export function createBrokerWechatStatusRuntimeLifecycle(deps = {}) {
105
121
  const onRuntimeError = deps.onRuntimeError ?? ((error) => console.error(error));
122
+ const stateRoot = deps.stateRoot ?? wechatStateRoot();
123
+ const onDiagnosticEvent = deps.onDiagnosticEvent ?? createWechatStatusRuntimeDiagnosticsFileWriter({ stateRoot, onRuntimeError });
106
124
  const v2Client = createOpencodeClientV2({
107
125
  baseUrl: "http://localhost:4096",
108
126
  directory: process.cwd(),
@@ -116,6 +134,7 @@ export function createBrokerWechatStatusRuntimeLifecycle(deps = {}) {
116
134
  ((statusRuntimeDeps) => createWechatStatusRuntime({
117
135
  onSlashCommand: async ({ command }) => statusRuntimeDeps.onSlashCommand({ command }),
118
136
  onRuntimeError,
137
+ onDiagnosticEvent: statusRuntimeDeps.onDiagnosticEvent,
119
138
  }));
120
139
  let runtime = null;
121
140
  return {
@@ -125,6 +144,7 @@ export function createBrokerWechatStatusRuntimeLifecycle(deps = {}) {
125
144
  }
126
145
  const created = createStatusRuntime({
127
146
  onSlashCommand: async ({ command }) => handleWechatSlashCommand(command),
147
+ onDiagnosticEvent,
128
148
  });
129
149
  runtime = created;
130
150
  try {
@@ -15,6 +15,7 @@ type LaunchOptions = {
15
15
  launchLockPath?: string;
16
16
  backoffMs?: number;
17
17
  maxAttempts?: number;
18
+ expectedVersion?: string;
18
19
  endpointFactory?: () => string;
19
20
  spawnImpl?: (endpoint: string, stateRoot: string) => {
20
21
  pid?: number | undefined;
@@ -17,6 +17,17 @@ function isFiniteNumber(value) {
17
17
  function delay(ms) {
18
18
  return new Promise((resolve) => setTimeout(resolve, ms));
19
19
  }
20
+ async function readCurrentPackageVersion() {
21
+ try {
22
+ const packageJsonPath = new URL("../../package.json", import.meta.url);
23
+ const raw = await readFile(packageJsonPath, "utf8");
24
+ const parsed = JSON.parse(raw);
25
+ return isNonEmptyString(parsed.version) ? parsed.version : "unknown";
26
+ }
27
+ catch {
28
+ return "unknown";
29
+ }
30
+ }
20
31
  function isProcessAlive(pid) {
21
32
  try {
22
33
  process.kill(pid, 0);
@@ -118,11 +129,14 @@ async function acquireLaunchLock(filePath) {
118
129
  return null;
119
130
  }
120
131
  }
121
- async function isBrokerAlive(brokerFilePath, pingImpl) {
132
+ async function isBrokerAlive(brokerFilePath, pingImpl, expectedVersion) {
122
133
  const metadata = await readBrokerMetadata(brokerFilePath);
123
134
  if (!metadata) {
124
135
  return null;
125
136
  }
137
+ if (isNonEmptyString(expectedVersion) && metadata.version !== expectedVersion) {
138
+ return null;
139
+ }
126
140
  const ok = await pingImpl(metadata.endpoint);
127
141
  if (!ok) {
128
142
  return null;
@@ -145,6 +159,7 @@ export async function connectOrSpawnBroker(options = {}) {
145
159
  const launchLockFile = options.launchLockPath ?? path.join(stateRoot, "launch.lock");
146
160
  const backoffMs = options.backoffMs ?? DEFAULT_BACKOFF_MS;
147
161
  const maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
162
+ const expectedVersion = options.expectedVersion ?? await readCurrentPackageVersion();
148
163
  const pingImpl = options.pingImpl ?? defaultPingImpl;
149
164
  const spawnImpl = options.spawnImpl ?? defaultSpawnImpl;
150
165
  const endpointFactory = options.endpointFactory ?? (() => {
@@ -156,7 +171,7 @@ export async function connectOrSpawnBroker(options = {}) {
156
171
  });
157
172
  await mkdir(stateRoot, { recursive: true, mode: 0o700 });
158
173
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
159
- const running = await isBrokerAlive(brokerJsonFile, pingImpl);
174
+ const running = await isBrokerAlive(brokerJsonFile, pingImpl, expectedVersion);
160
175
  if (running) {
161
176
  return running;
162
177
  }
@@ -167,7 +182,7 @@ export async function connectOrSpawnBroker(options = {}) {
167
182
  }
168
183
  options.onLockAcquired?.(lock);
169
184
  try {
170
- const secondCheck = await isBrokerAlive(brokerJsonFile, pingImpl);
185
+ const secondCheck = await isBrokerAlive(brokerJsonFile, pingImpl, expectedVersion);
171
186
  if (secondCheck) {
172
187
  return secondCheck;
173
188
  }
@@ -176,7 +191,7 @@ export async function connectOrSpawnBroker(options = {}) {
176
191
  void child?.unref?.();
177
192
  for (let n = 0; n < 20; n += 1) {
178
193
  await delay(100);
179
- const spawned = await isBrokerAlive(brokerJsonFile, pingImpl);
194
+ const spawned = await isBrokerAlive(brokerJsonFile, pingImpl, expectedVersion);
180
195
  if (spawned) {
181
196
  return spawned;
182
197
  }
@@ -3,6 +3,7 @@ export declare const WECHAT_FILE_MODE = 384;
3
3
  export type WechatRequestKind = "question" | "permission";
4
4
  export declare function wechatStateRoot(): string;
5
5
  export declare function brokerStatePath(): string;
6
+ export declare function wechatStatusRuntimeDiagnosticsPath(stateRoot?: string): string;
6
7
  export declare function launchLockPath(): string;
7
8
  export declare function operatorStatePath(): string;
8
9
  export declare function instancesDir(): string;
@@ -9,6 +9,9 @@ export function wechatStateRoot() {
9
9
  export function brokerStatePath() {
10
10
  return path.join(wechatStateRoot(), "broker.json");
11
11
  }
12
+ export function wechatStatusRuntimeDiagnosticsPath(stateRoot = wechatStateRoot()) {
13
+ return path.join(stateRoot, "wechat-status-runtime.diagnostics.jsonl");
14
+ }
12
15
  export function launchLockPath() {
13
16
  return path.join(wechatStateRoot(), "launch.lock");
14
17
  }
@@ -8,11 +8,28 @@ type SlashCommandHandlerInput = {
8
8
  text: string;
9
9
  message: PublicWeixinMessage;
10
10
  };
11
+ export type WechatStatusRuntimeDiagnosticEvent = {
12
+ type: "messageSkipped";
13
+ reason: "missingFromUserId" | "missingText";
14
+ hasFromUserId: boolean;
15
+ hasText: boolean;
16
+ } | {
17
+ type: "slashCommandRecognized";
18
+ command: WechatSlashCommand;
19
+ text: string;
20
+ to: string;
21
+ } | {
22
+ type: "replySendFailed";
23
+ to: string;
24
+ error: string;
25
+ commandType: WechatSlashCommand["type"] | null;
26
+ };
11
27
  type CreateWechatStatusRuntimeInput = {
12
28
  loadPublicHelpers?: (options?: OpenClawWeixinPublicHelpersLoaderOptions) => Promise<PublicHelpersForRuntime>;
13
29
  publicHelpersOptions?: OpenClawWeixinPublicHelpersLoaderOptions;
14
30
  onSlashCommand?: (input: SlashCommandHandlerInput) => Promise<string>;
15
31
  onRuntimeError?: (error: unknown) => void;
32
+ onDiagnosticEvent?: (event: WechatStatusRuntimeDiagnosticEvent) => void | Promise<void>;
16
33
  retryDelayMs?: number;
17
34
  longPollTimeoutMs?: number;
18
35
  shouldReloadState?: (state: {
@@ -88,6 +88,15 @@ function toNonEmptyString(value) {
88
88
  const trimmed = value.trim();
89
89
  return trimmed.length > 0 ? trimmed : null;
90
90
  }
91
+ function toErrorMessage(error) {
92
+ if (error instanceof Error) {
93
+ return error.message;
94
+ }
95
+ if (typeof error === "string") {
96
+ return error;
97
+ }
98
+ return String(error);
99
+ }
91
100
  export function createWechatStatusRuntime(input = {}) {
92
101
  const loadPublicHelpers = input.loadPublicHelpers ?? loadOpenClawWeixinPublicHelpers;
93
102
  const onSlashCommand = input.onSlashCommand ??
@@ -95,6 +104,7 @@ export function createWechatStatusRuntime(input = {}) {
95
104
  return "/status 处理中";
96
105
  });
97
106
  const onRuntimeError = input.onRuntimeError ?? (() => { });
107
+ const onDiagnosticEvent = input.onDiagnosticEvent ?? (() => { });
98
108
  const retryDelayMs = normalizePositiveInteger(input.retryDelayMs, DEFAULT_RETRY_DELAY_MS);
99
109
  const longPollTimeoutMs = normalizePositiveInteger(input.longPollTimeoutMs, DEFAULT_LONG_POLL_TIMEOUT_MS);
100
110
  const shouldReloadState = input.shouldReloadState ?? (() => false);
@@ -102,6 +112,13 @@ export function createWechatStatusRuntime(input = {}) {
102
112
  let closed = false;
103
113
  let stopController = null;
104
114
  let pollingTask = null;
115
+ const emitDiagnosticEvent = (event) => {
116
+ void Promise.resolve()
117
+ .then(() => onDiagnosticEvent(event))
118
+ .catch((error) => {
119
+ onRuntimeError(error);
120
+ });
121
+ };
105
122
  const poll = async (signal) => {
106
123
  let initialized = null;
107
124
  while (!signal.aborted) {
@@ -163,12 +180,34 @@ export function createWechatStatusRuntime(input = {}) {
163
180
  }
164
181
  const to = toNonEmptyString(message.from_user_id);
165
182
  const text = extractMessageText(message);
166
- if (!to || text.trim().length === 0) {
183
+ const hasText = text.trim().length > 0;
184
+ if (!to) {
185
+ emitDiagnosticEvent({
186
+ type: "messageSkipped",
187
+ reason: "missingFromUserId",
188
+ hasFromUserId: false,
189
+ hasText,
190
+ });
191
+ continue;
192
+ }
193
+ if (!hasText) {
194
+ emitDiagnosticEvent({
195
+ type: "messageSkipped",
196
+ reason: "missingText",
197
+ hasFromUserId: true,
198
+ hasText: false,
199
+ });
167
200
  continue;
168
201
  }
169
202
  const parsedCommand = parseWechatSlashCommand(text);
170
203
  let replyText = DEFAULT_NON_SLASH_REPLY_TEXT;
171
204
  if (parsedCommand) {
205
+ emitDiagnosticEvent({
206
+ type: "slashCommandRecognized",
207
+ command: parsedCommand,
208
+ text,
209
+ to,
210
+ });
172
211
  try {
173
212
  replyText = await onSlashCommand({
174
213
  command: parsedCommand,
@@ -196,6 +235,12 @@ export function createWechatStatusRuntime(input = {}) {
196
235
  if (isAbortError(error)) {
197
236
  return;
198
237
  }
238
+ emitDiagnosticEvent({
239
+ type: "replySendFailed",
240
+ to,
241
+ error: toErrorMessage(error),
242
+ commandType: parsedCommand?.type ?? null,
243
+ });
199
244
  onRuntimeError(error);
200
245
  }
201
246
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-copilot-account-switcher",
3
- "version": "0.14.8",
3
+ "version": "0.14.10",
4
4
  "description": "GitHub Copilot account switcher plugin for OpenCode",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",