opencode-copilot-account-switcher 0.13.5 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/common-settings-actions.d.ts +1 -1
- package/dist/common-settings-actions.js +36 -0
- package/dist/common-settings-store.d.ts +4 -0
- package/dist/common-settings-store.js +32 -9
- package/dist/menu-runtime.js +12 -1
- package/dist/plugin-hooks.d.ts +8 -0
- package/dist/plugin-hooks.js +96 -0
- package/dist/providers/codex-menu-adapter.js +8 -1
- package/dist/providers/copilot-menu-adapter.js +7 -0
- package/dist/store-paths.d.ts +1 -0
- package/dist/store-paths.js +3 -0
- package/dist/ui/menu.d.ts +33 -0
- package/dist/ui/menu.js +85 -0
- package/dist/wechat/bridge.d.ts +69 -0
- package/dist/wechat/bridge.js +180 -0
- package/dist/wechat/broker-client.d.ts +33 -0
- package/dist/wechat/broker-client.js +257 -0
- package/dist/wechat/broker-entry.d.ts +17 -0
- package/dist/wechat/broker-entry.js +182 -0
- package/dist/wechat/broker-launcher.d.ts +27 -0
- package/dist/wechat/broker-launcher.js +191 -0
- package/dist/wechat/broker-server.d.ts +25 -0
- package/dist/wechat/broker-server.js +540 -0
- package/dist/wechat/command-parser.d.ts +7 -0
- package/dist/wechat/command-parser.js +16 -0
- package/dist/wechat/compat/openclaw-guided-smoke.d.ts +178 -0
- package/dist/wechat/compat/openclaw-guided-smoke.js +1133 -0
- package/dist/wechat/compat/openclaw-public-helpers.d.ts +101 -0
- package/dist/wechat/compat/openclaw-public-helpers.js +207 -0
- package/dist/wechat/compat/openclaw-smoke.d.ts +48 -0
- package/dist/wechat/compat/openclaw-smoke.js +100 -0
- package/dist/wechat/compat/slash-guard.d.ts +11 -0
- package/dist/wechat/compat/slash-guard.js +24 -0
- package/dist/wechat/handle.d.ts +8 -0
- package/dist/wechat/handle.js +46 -0
- package/dist/wechat/ipc-auth.d.ts +6 -0
- package/dist/wechat/ipc-auth.js +39 -0
- package/dist/wechat/operator-store.d.ts +8 -0
- package/dist/wechat/operator-store.js +59 -0
- package/dist/wechat/protocol.d.ts +29 -0
- package/dist/wechat/protocol.js +75 -0
- package/dist/wechat/request-store.d.ts +41 -0
- package/dist/wechat/request-store.js +215 -0
- package/dist/wechat/session-digest.d.ts +41 -0
- package/dist/wechat/session-digest.js +134 -0
- package/dist/wechat/state-paths.d.ts +14 -0
- package/dist/wechat/state-paths.js +45 -0
- package/dist/wechat/status-format.d.ts +14 -0
- package/dist/wechat/status-format.js +174 -0
- package/dist/wechat/token-store.d.ts +18 -0
- package/dist/wechat/token-store.js +100 -0
- package/dist/wechat/wechat-status-runtime.d.ts +24 -0
- package/dist/wechat/wechat-status-runtime.js +238 -0
- package/package.json +8 -3
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Message, Part, PermissionRequest, QuestionRequest, Session, SessionStatus, Todo } from "@opencode-ai/sdk/v2";
|
|
2
|
+
import { connect } from "./broker-client.js";
|
|
3
|
+
import { connectOrSpawnBroker } from "./broker-launcher.js";
|
|
4
|
+
import { type SessionDigest } from "./session-digest.js";
|
|
5
|
+
type SessionMessages = Array<{
|
|
6
|
+
info: Message;
|
|
7
|
+
parts: Part[];
|
|
8
|
+
}>;
|
|
9
|
+
type SessionLite = Pick<Session, "id" | "title" | "directory" | "time">;
|
|
10
|
+
type WechatBridgeClient = {
|
|
11
|
+
session: {
|
|
12
|
+
list: () => Promise<SessionLite[]>;
|
|
13
|
+
status: () => Promise<Record<string, SessionStatus | undefined>>;
|
|
14
|
+
todo: (sessionID: string) => Promise<Todo[]>;
|
|
15
|
+
messages: (sessionID: string) => Promise<SessionMessages>;
|
|
16
|
+
};
|
|
17
|
+
question: {
|
|
18
|
+
list: () => Promise<QuestionRequest[]>;
|
|
19
|
+
};
|
|
20
|
+
permission: {
|
|
21
|
+
list: () => Promise<PermissionRequest[]>;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
export type InstanceUnavailableKind = "sessionStatus" | "questionList" | "permissionList";
|
|
25
|
+
export type WechatInstanceStatusSnapshot = {
|
|
26
|
+
instanceID: string;
|
|
27
|
+
instanceName: string;
|
|
28
|
+
pid: number;
|
|
29
|
+
projectName?: string;
|
|
30
|
+
directory: string;
|
|
31
|
+
collectedAt: number;
|
|
32
|
+
sessions: SessionDigest[];
|
|
33
|
+
unavailable?: InstanceUnavailableKind[];
|
|
34
|
+
};
|
|
35
|
+
export type WechatBridgeInput = {
|
|
36
|
+
instanceID: string;
|
|
37
|
+
instanceName: string;
|
|
38
|
+
pid: number;
|
|
39
|
+
projectName?: string;
|
|
40
|
+
directory: string;
|
|
41
|
+
client: WechatBridgeClient;
|
|
42
|
+
liveReadTimeoutMs?: number;
|
|
43
|
+
};
|
|
44
|
+
export type WechatBridge = {
|
|
45
|
+
collectStatusSnapshot: () => Promise<WechatInstanceStatusSnapshot>;
|
|
46
|
+
};
|
|
47
|
+
export type WechatBridgeLifecycleInput = {
|
|
48
|
+
client: WechatBridgeClient;
|
|
49
|
+
project?: {
|
|
50
|
+
id?: string;
|
|
51
|
+
name?: string;
|
|
52
|
+
};
|
|
53
|
+
directory?: string;
|
|
54
|
+
serverUrl?: URL;
|
|
55
|
+
statusCollectionEnabled?: boolean;
|
|
56
|
+
heartbeatIntervalMs?: number;
|
|
57
|
+
};
|
|
58
|
+
export type WechatBridgeLifecycle = {
|
|
59
|
+
close: () => Promise<void>;
|
|
60
|
+
};
|
|
61
|
+
type WechatBridgeLifecycleDeps = {
|
|
62
|
+
connectOrSpawnBrokerImpl?: typeof connectOrSpawnBroker;
|
|
63
|
+
connectImpl?: typeof connect;
|
|
64
|
+
setIntervalImpl?: typeof setInterval;
|
|
65
|
+
clearIntervalImpl?: typeof clearInterval;
|
|
66
|
+
};
|
|
67
|
+
export declare function createWechatBridge(input: WechatBridgeInput): WechatBridge;
|
|
68
|
+
export declare function createWechatBridgeLifecycle(input: WechatBridgeLifecycleInput, deps?: WechatBridgeLifecycleDeps): Promise<WechatBridgeLifecycle>;
|
|
69
|
+
export {};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { connect } from "./broker-client.js";
|
|
2
|
+
import { connectOrSpawnBroker } from "./broker-launcher.js";
|
|
3
|
+
import { buildSessionDigest, groupPermissionsBySession, groupQuestionsBySession, pickRecentSessions, } from "./session-digest.js";
|
|
4
|
+
const DEFAULT_LIVE_READ_TIMEOUT_MS = 2_000;
|
|
5
|
+
const DEFAULT_HEARTBEAT_INTERVAL_MS = 10_000;
|
|
6
|
+
function toSafeInstanceID(input) {
|
|
7
|
+
const normalized = input.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
8
|
+
if (normalized.length === 0) {
|
|
9
|
+
return `wechat-${process.pid}`;
|
|
10
|
+
}
|
|
11
|
+
return normalized.slice(0, 64);
|
|
12
|
+
}
|
|
13
|
+
function toProjectName(project) {
|
|
14
|
+
if (typeof project?.name === "string" && project.name.trim().length > 0) {
|
|
15
|
+
return project.name.trim();
|
|
16
|
+
}
|
|
17
|
+
if (typeof project?.id === "string" && project.id.trim().length > 0) {
|
|
18
|
+
return project.id.trim();
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
function toDirectory(inputDirectory) {
|
|
23
|
+
if (typeof inputDirectory === "string" && inputDirectory.trim().length > 0) {
|
|
24
|
+
return inputDirectory;
|
|
25
|
+
}
|
|
26
|
+
return process.cwd();
|
|
27
|
+
}
|
|
28
|
+
function toInstanceName(projectName, directory) {
|
|
29
|
+
if (projectName) {
|
|
30
|
+
return projectName;
|
|
31
|
+
}
|
|
32
|
+
const parts = directory.split(/[\\/]+/).filter((part) => part.length > 0);
|
|
33
|
+
return parts.at(-1) ?? `wechat-${process.pid}`;
|
|
34
|
+
}
|
|
35
|
+
function toInstanceID(projectName, directory) {
|
|
36
|
+
const seed = projectName ?? directory;
|
|
37
|
+
return toSafeInstanceID(seed);
|
|
38
|
+
}
|
|
39
|
+
function withTimeout(task, timeoutMs, name) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
let settled = false;
|
|
42
|
+
const timer = setTimeout(() => {
|
|
43
|
+
if (settled) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
settled = true;
|
|
47
|
+
reject(new Error(`${name} timed out in ${timeoutMs}ms`));
|
|
48
|
+
}, timeoutMs);
|
|
49
|
+
void Promise.resolve()
|
|
50
|
+
.then(task)
|
|
51
|
+
.then((value) => {
|
|
52
|
+
if (settled) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
settled = true;
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
resolve(value);
|
|
58
|
+
})
|
|
59
|
+
.catch((error) => {
|
|
60
|
+
if (settled) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
settled = true;
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
reject(error);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
export function createWechatBridge(input) {
|
|
70
|
+
const collectStatusSnapshot = async () => {
|
|
71
|
+
const liveReadTimeoutMs = typeof input.liveReadTimeoutMs === "number" && Number.isFinite(input.liveReadTimeoutMs)
|
|
72
|
+
? Math.max(1, Math.floor(input.liveReadTimeoutMs))
|
|
73
|
+
: DEFAULT_LIVE_READ_TIMEOUT_MS;
|
|
74
|
+
const unavailable = new Set();
|
|
75
|
+
const [sessionListResult, statusResult, questionResult, permissionResult] = await Promise.allSettled([
|
|
76
|
+
withTimeout(() => input.client.session.list(), liveReadTimeoutMs, "session.list"),
|
|
77
|
+
withTimeout(() => input.client.session.status(), liveReadTimeoutMs, "session.status"),
|
|
78
|
+
withTimeout(() => input.client.question.list(), liveReadTimeoutMs, "question.list"),
|
|
79
|
+
withTimeout(() => input.client.permission.list(), liveReadTimeoutMs, "permission.list"),
|
|
80
|
+
]);
|
|
81
|
+
const sessions = sessionListResult.status === "fulfilled" ? sessionListResult.value : [];
|
|
82
|
+
const recentSessions = pickRecentSessions(sessions, 3);
|
|
83
|
+
if (sessionListResult.status === "rejected") {
|
|
84
|
+
unavailable.add("sessionStatus");
|
|
85
|
+
}
|
|
86
|
+
const statusBySession = statusResult.status === "fulfilled"
|
|
87
|
+
? statusResult.value
|
|
88
|
+
: (unavailable.add("sessionStatus"), {});
|
|
89
|
+
const questionsBySession = questionResult.status === "fulfilled"
|
|
90
|
+
? groupQuestionsBySession(questionResult.value)
|
|
91
|
+
: (unavailable.add("questionList"), groupQuestionsBySession([]));
|
|
92
|
+
const permissionsBySession = permissionResult.status === "fulfilled"
|
|
93
|
+
? groupPermissionsBySession(permissionResult.value)
|
|
94
|
+
: (unavailable.add("permissionList"), groupPermissionsBySession([]));
|
|
95
|
+
const sessionDigests = await Promise.all(recentSessions.map(async (session) => {
|
|
96
|
+
const [todoResult, messagesResult] = await Promise.allSettled([
|
|
97
|
+
withTimeout(() => input.client.session.todo(session.id), liveReadTimeoutMs, `session.todo:${session.id}`),
|
|
98
|
+
withTimeout(() => input.client.session.messages(session.id), liveReadTimeoutMs, `session.messages:${session.id}`),
|
|
99
|
+
]);
|
|
100
|
+
const sessionUnavailable = [];
|
|
101
|
+
const todos = todoResult.status === "fulfilled" ? todoResult.value : (sessionUnavailable.push("todo"), []);
|
|
102
|
+
const messages = messagesResult.status === "fulfilled"
|
|
103
|
+
? messagesResult.value
|
|
104
|
+
: (sessionUnavailable.push("messages"), []);
|
|
105
|
+
return buildSessionDigest({
|
|
106
|
+
session,
|
|
107
|
+
statusBySession,
|
|
108
|
+
questionsBySession,
|
|
109
|
+
permissionsBySession,
|
|
110
|
+
todos,
|
|
111
|
+
messages,
|
|
112
|
+
unavailable: sessionUnavailable,
|
|
113
|
+
});
|
|
114
|
+
}));
|
|
115
|
+
return {
|
|
116
|
+
instanceID: input.instanceID,
|
|
117
|
+
instanceName: input.instanceName,
|
|
118
|
+
pid: input.pid,
|
|
119
|
+
projectName: input.projectName,
|
|
120
|
+
directory: input.directory,
|
|
121
|
+
collectedAt: Date.now(),
|
|
122
|
+
sessions: sessionDigests,
|
|
123
|
+
unavailable: unavailable.size > 0 ? [...unavailable] : undefined,
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
return {
|
|
127
|
+
collectStatusSnapshot,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
export async function createWechatBridgeLifecycle(input, deps = {}) {
|
|
131
|
+
if (input.statusCollectionEnabled !== true) {
|
|
132
|
+
return {
|
|
133
|
+
close: async () => { },
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const connectOrSpawnBrokerImpl = deps.connectOrSpawnBrokerImpl ?? connectOrSpawnBroker;
|
|
137
|
+
const connectImpl = deps.connectImpl ?? connect;
|
|
138
|
+
const setIntervalImpl = deps.setIntervalImpl ?? setInterval;
|
|
139
|
+
const clearIntervalImpl = deps.clearIntervalImpl ?? clearInterval;
|
|
140
|
+
const directory = toDirectory(input.directory);
|
|
141
|
+
const projectName = toProjectName(input.project);
|
|
142
|
+
const instanceID = toInstanceID(projectName, directory);
|
|
143
|
+
const bridge = createWechatBridge({
|
|
144
|
+
instanceID,
|
|
145
|
+
instanceName: toInstanceName(projectName, directory),
|
|
146
|
+
pid: process.pid,
|
|
147
|
+
projectName,
|
|
148
|
+
directory,
|
|
149
|
+
client: input.client,
|
|
150
|
+
});
|
|
151
|
+
const broker = await connectOrSpawnBrokerImpl();
|
|
152
|
+
const brokerClient = await connectImpl(broker.endpoint, { bridge });
|
|
153
|
+
try {
|
|
154
|
+
await brokerClient.registerInstance({
|
|
155
|
+
instanceID,
|
|
156
|
+
pid: process.pid,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
await brokerClient.close().catch(() => { });
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
const heartbeatIntervalMs = typeof input.heartbeatIntervalMs === "number" && Number.isFinite(input.heartbeatIntervalMs)
|
|
164
|
+
? Math.max(1_000, Math.floor(input.heartbeatIntervalMs))
|
|
165
|
+
: DEFAULT_HEARTBEAT_INTERVAL_MS;
|
|
166
|
+
const timer = setIntervalImpl(() => {
|
|
167
|
+
void brokerClient.heartbeat().catch(() => { });
|
|
168
|
+
}, heartbeatIntervalMs);
|
|
169
|
+
let closed = false;
|
|
170
|
+
return {
|
|
171
|
+
close: async () => {
|
|
172
|
+
if (closed) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
closed = true;
|
|
176
|
+
clearIntervalImpl(timer);
|
|
177
|
+
await brokerClient.close().catch(() => { });
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type BrokerEnvelope } from "./protocol.js";
|
|
2
|
+
import type { WechatBridge } from "./bridge.js";
|
|
3
|
+
type RegisterMeta = {
|
|
4
|
+
instanceID: string;
|
|
5
|
+
pid: number;
|
|
6
|
+
};
|
|
7
|
+
export type RegisterAck = {
|
|
8
|
+
sessionToken: string;
|
|
9
|
+
registeredAt: number;
|
|
10
|
+
brokerPid: number;
|
|
11
|
+
};
|
|
12
|
+
type SessionSnapshot = {
|
|
13
|
+
instanceID: string;
|
|
14
|
+
sessionToken: string;
|
|
15
|
+
registeredAt: number;
|
|
16
|
+
brokerPid: number;
|
|
17
|
+
};
|
|
18
|
+
type BrokerClient = {
|
|
19
|
+
ping: () => Promise<BrokerEnvelope>;
|
|
20
|
+
registerInstance: (meta: RegisterMeta) => Promise<RegisterAck>;
|
|
21
|
+
heartbeat: () => Promise<BrokerEnvelope>;
|
|
22
|
+
getSessionSnapshot: () => SessionSnapshot | null;
|
|
23
|
+
close: () => Promise<void>;
|
|
24
|
+
};
|
|
25
|
+
export type CollectStatusInput = {
|
|
26
|
+
requestId: string;
|
|
27
|
+
};
|
|
28
|
+
export type BrokerClientOptions = {
|
|
29
|
+
onCollectStatus?: (input: CollectStatusInput) => Promise<unknown> | unknown;
|
|
30
|
+
bridge?: WechatBridge;
|
|
31
|
+
};
|
|
32
|
+
export declare function connect(endpoint: string, options?: BrokerClientOptions): Promise<BrokerClient>;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
import { parseEnvelopeLine, serializeEnvelope, } from "./protocol.js";
|
|
3
|
+
function isNonEmptyString(value) {
|
|
4
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
5
|
+
}
|
|
6
|
+
function isFiniteNumber(value) {
|
|
7
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
8
|
+
}
|
|
9
|
+
function isResponseForRequest(response, requestId) {
|
|
10
|
+
if (response.id === requestId) {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
if (response.id.endsWith(`-${requestId}`)) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
if (response.type === "error") {
|
|
17
|
+
const payload = response.payload;
|
|
18
|
+
return payload.requestId === requestId;
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
export async function connect(endpoint, options = {}) {
|
|
23
|
+
if (options.bridge && options.onCollectStatus) {
|
|
24
|
+
throw new Error("broker client options are ambiguous: provide either bridge or onCollectStatus");
|
|
25
|
+
}
|
|
26
|
+
const socket = net.createConnection(endpoint);
|
|
27
|
+
let sequence = 0;
|
|
28
|
+
let pendingResolve = null;
|
|
29
|
+
let pendingReject = null;
|
|
30
|
+
let pendingRequestId = null;
|
|
31
|
+
let buffer = "";
|
|
32
|
+
let connected = false;
|
|
33
|
+
let closed = false;
|
|
34
|
+
let session = null;
|
|
35
|
+
const connectedReady = new Promise((resolve, reject) => {
|
|
36
|
+
socket.once("connect", () => {
|
|
37
|
+
connected = true;
|
|
38
|
+
resolve();
|
|
39
|
+
});
|
|
40
|
+
socket.once("error", reject);
|
|
41
|
+
});
|
|
42
|
+
socket.on("data", (chunk) => {
|
|
43
|
+
buffer += chunk.toString("utf8");
|
|
44
|
+
while (true) {
|
|
45
|
+
const newlineIndex = buffer.indexOf("\n");
|
|
46
|
+
if (newlineIndex === -1) {
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
const frame = buffer.slice(0, newlineIndex + 1);
|
|
50
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
51
|
+
if (pendingResolve) {
|
|
52
|
+
try {
|
|
53
|
+
const parsed = parseEnvelopeLine(frame);
|
|
54
|
+
if (parsed.type === "collectStatus") {
|
|
55
|
+
handleCollectStatus(parsed);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (pendingRequestId && !isResponseForRequest(parsed, pendingRequestId)) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const resolve = pendingResolve;
|
|
62
|
+
pendingResolve = null;
|
|
63
|
+
pendingReject = null;
|
|
64
|
+
pendingRequestId = null;
|
|
65
|
+
resolve(parsed);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
const reject = pendingReject;
|
|
69
|
+
pendingResolve = null;
|
|
70
|
+
pendingReject = null;
|
|
71
|
+
pendingRequestId = null;
|
|
72
|
+
reject?.(error);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
try {
|
|
77
|
+
const parsed = parseEnvelopeLine(frame);
|
|
78
|
+
if (parsed.type === "collectStatus") {
|
|
79
|
+
handleCollectStatus(parsed);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// ignore unsolicited invalid frames when no pending request exists
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
socket.on("error", (error) => {
|
|
89
|
+
if (pendingReject) {
|
|
90
|
+
const reject = pendingReject;
|
|
91
|
+
pendingResolve = null;
|
|
92
|
+
pendingReject = null;
|
|
93
|
+
pendingRequestId = null;
|
|
94
|
+
reject(error);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
socket.on("close", () => {
|
|
98
|
+
connected = false;
|
|
99
|
+
closed = true;
|
|
100
|
+
session = null;
|
|
101
|
+
if (pendingReject) {
|
|
102
|
+
const reject = pendingReject;
|
|
103
|
+
pendingResolve = null;
|
|
104
|
+
pendingReject = null;
|
|
105
|
+
pendingRequestId = null;
|
|
106
|
+
reject(new Error("broker connection closed"));
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
await connectedReady;
|
|
110
|
+
function nextRequestId(prefix) {
|
|
111
|
+
sequence += 1;
|
|
112
|
+
return `${prefix}-${Date.now()}-${sequence}`;
|
|
113
|
+
}
|
|
114
|
+
function sendStatusSnapshot(requestId, snapshot) {
|
|
115
|
+
if (!session) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const envelope = {
|
|
119
|
+
id: nextRequestId("statusSnapshot"),
|
|
120
|
+
type: "statusSnapshot",
|
|
121
|
+
instanceID: session.instanceID,
|
|
122
|
+
sessionToken: session.sessionToken,
|
|
123
|
+
payload: {
|
|
124
|
+
requestId,
|
|
125
|
+
snapshot,
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
socket.write(serializeEnvelope(envelope));
|
|
129
|
+
}
|
|
130
|
+
function handleCollectStatus(envelope) {
|
|
131
|
+
const payload = envelope.payload;
|
|
132
|
+
if (!isNonEmptyString(payload.requestId)) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const hasBridge = options.bridge !== undefined;
|
|
136
|
+
const hasHook = options.onCollectStatus !== undefined;
|
|
137
|
+
if (!hasBridge && !hasHook) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const collectPromise = hasBridge
|
|
141
|
+
? options.bridge.collectStatusSnapshot()
|
|
142
|
+
: options.onCollectStatus({ requestId: payload.requestId });
|
|
143
|
+
void Promise.resolve(collectPromise)
|
|
144
|
+
.then((snapshot) => {
|
|
145
|
+
sendStatusSnapshot(payload.requestId, snapshot);
|
|
146
|
+
})
|
|
147
|
+
.catch(() => {
|
|
148
|
+
// swallow collect handler errors to keep socket alive
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
async function send(envelope) {
|
|
152
|
+
if (!connected || closed) {
|
|
153
|
+
throw new Error("broker connection closed");
|
|
154
|
+
}
|
|
155
|
+
if (pendingResolve) {
|
|
156
|
+
throw new Error("broker client has pending request");
|
|
157
|
+
}
|
|
158
|
+
return new Promise((resolve, reject) => {
|
|
159
|
+
pendingResolve = resolve;
|
|
160
|
+
pendingReject = reject;
|
|
161
|
+
pendingRequestId = envelope.id;
|
|
162
|
+
socket.write(serializeEnvelope(envelope));
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
async ping() {
|
|
167
|
+
return send({
|
|
168
|
+
id: nextRequestId("ping"),
|
|
169
|
+
type: "ping",
|
|
170
|
+
payload: {},
|
|
171
|
+
});
|
|
172
|
+
},
|
|
173
|
+
async registerInstance(meta) {
|
|
174
|
+
const instanceID = meta.instanceID;
|
|
175
|
+
if (!isNonEmptyString(instanceID)) {
|
|
176
|
+
throw new Error("invalid instanceID");
|
|
177
|
+
}
|
|
178
|
+
if (!isFiniteNumber(meta.pid)) {
|
|
179
|
+
throw new Error("invalid pid");
|
|
180
|
+
}
|
|
181
|
+
const response = await send({
|
|
182
|
+
id: nextRequestId("register"),
|
|
183
|
+
type: "registerInstance",
|
|
184
|
+
instanceID,
|
|
185
|
+
payload: { pid: meta.pid },
|
|
186
|
+
});
|
|
187
|
+
if (response.type !== "registerAck") {
|
|
188
|
+
throw new Error("register failed");
|
|
189
|
+
}
|
|
190
|
+
const payload = response.payload;
|
|
191
|
+
if (!isNonEmptyString(payload.sessionToken)) {
|
|
192
|
+
throw new Error("registerAck missing sessionToken");
|
|
193
|
+
}
|
|
194
|
+
if (!isFiniteNumber(payload.registeredAt)) {
|
|
195
|
+
throw new Error("registerAck missing registeredAt");
|
|
196
|
+
}
|
|
197
|
+
if (!isFiniteNumber(payload.brokerPid)) {
|
|
198
|
+
throw new Error("registerAck missing brokerPid");
|
|
199
|
+
}
|
|
200
|
+
session = {
|
|
201
|
+
instanceID,
|
|
202
|
+
sessionToken: payload.sessionToken,
|
|
203
|
+
registeredAt: payload.registeredAt,
|
|
204
|
+
brokerPid: payload.brokerPid,
|
|
205
|
+
};
|
|
206
|
+
return {
|
|
207
|
+
sessionToken: session.sessionToken,
|
|
208
|
+
registeredAt: session.registeredAt,
|
|
209
|
+
brokerPid: session.brokerPid,
|
|
210
|
+
};
|
|
211
|
+
},
|
|
212
|
+
async heartbeat() {
|
|
213
|
+
if (!session) {
|
|
214
|
+
throw new Error("missing broker session");
|
|
215
|
+
}
|
|
216
|
+
return send({
|
|
217
|
+
id: nextRequestId("heartbeat"),
|
|
218
|
+
type: "heartbeat",
|
|
219
|
+
instanceID: session.instanceID,
|
|
220
|
+
sessionToken: session.sessionToken,
|
|
221
|
+
payload: {},
|
|
222
|
+
});
|
|
223
|
+
},
|
|
224
|
+
getSessionSnapshot() {
|
|
225
|
+
if (!session) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
return { ...session };
|
|
229
|
+
},
|
|
230
|
+
async close() {
|
|
231
|
+
if (closed) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (socket.destroyed) {
|
|
235
|
+
closed = true;
|
|
236
|
+
connected = false;
|
|
237
|
+
session = null;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const closePromise = new Promise((resolve) => {
|
|
241
|
+
socket.once("close", () => resolve());
|
|
242
|
+
});
|
|
243
|
+
socket.end();
|
|
244
|
+
await Promise.race([
|
|
245
|
+
closePromise,
|
|
246
|
+
new Promise((resolve) => {
|
|
247
|
+
setTimeout(() => {
|
|
248
|
+
if (!socket.destroyed) {
|
|
249
|
+
socket.destroy();
|
|
250
|
+
}
|
|
251
|
+
resolve();
|
|
252
|
+
}, 200);
|
|
253
|
+
}),
|
|
254
|
+
]);
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type WechatStatusRuntime } from "./wechat-status-runtime.js";
|
|
2
|
+
type BrokerWechatStatusRuntimeLifecycle = {
|
|
3
|
+
start: () => Promise<void>;
|
|
4
|
+
close: () => Promise<void>;
|
|
5
|
+
};
|
|
6
|
+
type BrokerWechatStatusRuntimeLifecycleDeps = {
|
|
7
|
+
createStatusRuntime?: (deps: {
|
|
8
|
+
onSlashCommand: (input: {
|
|
9
|
+
command: import("./command-parser.js").WechatSlashCommand;
|
|
10
|
+
}) => Promise<string>;
|
|
11
|
+
}) => WechatStatusRuntime;
|
|
12
|
+
handleWechatSlashCommand?: (command: import("./command-parser.js").WechatSlashCommand) => Promise<string>;
|
|
13
|
+
onRuntimeError?: (error: unknown) => void;
|
|
14
|
+
};
|
|
15
|
+
export declare function shouldEnableBrokerWechatStatusRuntime(env?: NodeJS.ProcessEnv): boolean;
|
|
16
|
+
export declare function createBrokerWechatStatusRuntimeLifecycle(deps?: BrokerWechatStatusRuntimeLifecycleDeps): BrokerWechatStatusRuntimeLifecycle;
|
|
17
|
+
export {};
|