opencode-copilot-account-switcher 0.13.6 → 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 +26 -0
- 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,101 @@
|
|
|
1
|
+
export declare const OPENCLAW_WEIXIN_JITI_SRC_HELPER_MODULES: {
|
|
2
|
+
readonly stateDir: "@tencent-weixin/openclaw-weixin/src/storage/state-dir.ts";
|
|
3
|
+
readonly syncBuf: "@tencent-weixin/openclaw-weixin/src/storage/sync-buf.ts";
|
|
4
|
+
readonly getUpdates: "@tencent-weixin/openclaw-weixin/src/api/api.ts";
|
|
5
|
+
readonly sendMessageWeixin: "@tencent-weixin/openclaw-weixin/src/messaging/send.ts";
|
|
6
|
+
};
|
|
7
|
+
type WeixinQrGateway = {
|
|
8
|
+
loginWithQrStart: (input?: unknown) => unknown;
|
|
9
|
+
loginWithQrWait: (input?: unknown) => unknown;
|
|
10
|
+
};
|
|
11
|
+
export type OpenClawWeixinPublicEntry = {
|
|
12
|
+
packageJsonPath: string;
|
|
13
|
+
packageRoot: string;
|
|
14
|
+
extensions: string[];
|
|
15
|
+
entryRelativePath: string;
|
|
16
|
+
entryAbsolutePath: string;
|
|
17
|
+
};
|
|
18
|
+
declare function resolveOpenClawWeixinPublicEntry(): Promise<OpenClawWeixinPublicEntry>;
|
|
19
|
+
declare function loadLatestWeixinAccountState(): Promise<{
|
|
20
|
+
accountId: string;
|
|
21
|
+
token: string;
|
|
22
|
+
baseUrl: string;
|
|
23
|
+
getUpdatesBuf?: string;
|
|
24
|
+
} | null>;
|
|
25
|
+
type PublicWeixinMessageItem = {
|
|
26
|
+
type?: number;
|
|
27
|
+
text_item?: {
|
|
28
|
+
text?: string;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
export type PublicWeixinMessage = {
|
|
32
|
+
message_id?: number;
|
|
33
|
+
from_user_id?: string;
|
|
34
|
+
context_token?: string;
|
|
35
|
+
create_time_ms?: number;
|
|
36
|
+
item_list?: PublicWeixinMessageItem[];
|
|
37
|
+
};
|
|
38
|
+
export type PublicWeixinSendMessage = (params: {
|
|
39
|
+
to: string;
|
|
40
|
+
text: string;
|
|
41
|
+
opts: {
|
|
42
|
+
baseUrl: string;
|
|
43
|
+
token: string;
|
|
44
|
+
contextToken?: string;
|
|
45
|
+
};
|
|
46
|
+
}) => Promise<{
|
|
47
|
+
messageId: string;
|
|
48
|
+
}>;
|
|
49
|
+
export type PublicWeixinPersistGetUpdatesBuf = (params: {
|
|
50
|
+
accountId: string;
|
|
51
|
+
getUpdatesBuf: string;
|
|
52
|
+
}) => Promise<void>;
|
|
53
|
+
export type OpenClawWeixinPublicHelpers = {
|
|
54
|
+
entry: OpenClawWeixinPublicEntry;
|
|
55
|
+
pluginId: string;
|
|
56
|
+
qrGateway: WeixinQrGateway;
|
|
57
|
+
latestAccountState: {
|
|
58
|
+
accountId: string;
|
|
59
|
+
token: string;
|
|
60
|
+
baseUrl: string;
|
|
61
|
+
getUpdatesBuf?: string;
|
|
62
|
+
} | null;
|
|
63
|
+
getUpdates: (params: {
|
|
64
|
+
baseUrl: string;
|
|
65
|
+
token?: string;
|
|
66
|
+
get_updates_buf?: string;
|
|
67
|
+
timeoutMs?: number;
|
|
68
|
+
}) => Promise<{
|
|
69
|
+
msgs?: PublicWeixinMessage[];
|
|
70
|
+
get_updates_buf?: string;
|
|
71
|
+
}>;
|
|
72
|
+
sendMessageWeixin: PublicWeixinSendMessage;
|
|
73
|
+
persistGetUpdatesBuf?: PublicWeixinPersistGetUpdatesBuf;
|
|
74
|
+
};
|
|
75
|
+
type OpenClawWeixinPublicHelpersLoaders = {
|
|
76
|
+
resolveOpenClawWeixinPublicEntry?: typeof resolveOpenClawWeixinPublicEntry;
|
|
77
|
+
loadPublicWeixinQrGateway?: () => Promise<{
|
|
78
|
+
gateway: WeixinQrGateway;
|
|
79
|
+
pluginId?: string;
|
|
80
|
+
}>;
|
|
81
|
+
loadLatestWeixinAccountState?: typeof loadLatestWeixinAccountState;
|
|
82
|
+
loadPublicWeixinHelpers?: typeof loadPublicWeixinHelpers;
|
|
83
|
+
loadPublicWeixinSendHelper?: typeof loadPublicWeixinSendHelper;
|
|
84
|
+
};
|
|
85
|
+
declare function loadPublicWeixinHelpers(): Promise<{
|
|
86
|
+
getUpdates: (params: {
|
|
87
|
+
baseUrl: string;
|
|
88
|
+
token?: string;
|
|
89
|
+
get_updates_buf?: string;
|
|
90
|
+
timeoutMs?: number;
|
|
91
|
+
}) => Promise<{
|
|
92
|
+
msgs?: PublicWeixinMessage[];
|
|
93
|
+
get_updates_buf?: string;
|
|
94
|
+
}>;
|
|
95
|
+
}>;
|
|
96
|
+
declare function loadPublicWeixinSendHelper(): Promise<{
|
|
97
|
+
sendMessageWeixin: PublicWeixinSendMessage;
|
|
98
|
+
}>;
|
|
99
|
+
export declare function loadOpenClawWeixinPublicHelpers(loaders?: OpenClawWeixinPublicHelpersLoaders): Promise<OpenClawWeixinPublicHelpers>;
|
|
100
|
+
export type OpenClawWeixinPublicHelpersLoaderOptions = OpenClawWeixinPublicHelpersLoaders;
|
|
101
|
+
export {};
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createJiti } from "jiti";
|
|
5
|
+
export const OPENCLAW_WEIXIN_JITI_SRC_HELPER_MODULES = {
|
|
6
|
+
stateDir: "@tencent-weixin/openclaw-weixin/src/storage/state-dir.ts",
|
|
7
|
+
syncBuf: "@tencent-weixin/openclaw-weixin/src/storage/sync-buf.ts",
|
|
8
|
+
getUpdates: "@tencent-weixin/openclaw-weixin/src/api/api.ts",
|
|
9
|
+
sendMessageWeixin: "@tencent-weixin/openclaw-weixin/src/messaging/send.ts",
|
|
10
|
+
};
|
|
11
|
+
let publicJitiLoader = null;
|
|
12
|
+
function requireField(condition, message) {
|
|
13
|
+
if (!condition) {
|
|
14
|
+
throw new Error(`[wechat-compat] ${message}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function getPublicJiti() {
|
|
18
|
+
if (publicJitiLoader) {
|
|
19
|
+
return publicJitiLoader;
|
|
20
|
+
}
|
|
21
|
+
publicJitiLoader = createJiti(import.meta.url, {
|
|
22
|
+
interopDefault: true,
|
|
23
|
+
extensions: [".ts", ".tsx", ".mts", ".cts", ".js", ".mjs", ".cjs", ".json"],
|
|
24
|
+
});
|
|
25
|
+
return publicJitiLoader;
|
|
26
|
+
}
|
|
27
|
+
function hasQrLoginMethods(value) {
|
|
28
|
+
if (!value || typeof value !== "object") {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
const candidate = value;
|
|
32
|
+
return typeof candidate.loginWithQrStart === "function" && typeof candidate.loginWithQrWait === "function";
|
|
33
|
+
}
|
|
34
|
+
async function resolveOpenClawWeixinPublicEntry() {
|
|
35
|
+
const require = createRequire(import.meta.url);
|
|
36
|
+
const packageName = "@tencent-weixin/openclaw-weixin";
|
|
37
|
+
const packageJsonPath = require.resolve(`${packageName}/package.json`);
|
|
38
|
+
const packageJsonRaw = await readFile(packageJsonPath, "utf8");
|
|
39
|
+
const packageJson = JSON.parse(packageJsonRaw);
|
|
40
|
+
const extensions = Array.isArray(packageJson.openclaw?.extensions)
|
|
41
|
+
? packageJson.openclaw?.extensions.filter((it) => typeof it === "string")
|
|
42
|
+
: [];
|
|
43
|
+
requireField(extensions.length > 0, `${packageName} openclaw.extensions[0] is required`);
|
|
44
|
+
const entryRelativePath = extensions[0];
|
|
45
|
+
requireField(Boolean(entryRelativePath?.startsWith("./")), `${packageName} openclaw.extensions[0] must start with ./`);
|
|
46
|
+
const packageRoot = path.dirname(packageJsonPath);
|
|
47
|
+
const entryAbsolutePath = path.resolve(packageRoot, entryRelativePath);
|
|
48
|
+
return {
|
|
49
|
+
packageJsonPath,
|
|
50
|
+
packageRoot,
|
|
51
|
+
extensions,
|
|
52
|
+
entryRelativePath,
|
|
53
|
+
entryAbsolutePath,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
async function loadOpenClawWeixinDefaultExport() {
|
|
57
|
+
const entry = await resolveOpenClawWeixinPublicEntry();
|
|
58
|
+
const moduleNamespace = getPublicJiti()(entry.entryAbsolutePath);
|
|
59
|
+
const plugin = moduleNamespace.default;
|
|
60
|
+
if (!plugin || typeof plugin !== "object" || typeof plugin.register !== "function") {
|
|
61
|
+
throw new Error("[wechat-compat] @tencent-weixin/openclaw-weixin public entry default export is missing register(api)");
|
|
62
|
+
}
|
|
63
|
+
return plugin;
|
|
64
|
+
}
|
|
65
|
+
async function loadPublicWeixinQrGateway() {
|
|
66
|
+
const registeredPayloads = [];
|
|
67
|
+
const compatHostApi = {
|
|
68
|
+
runtime: {
|
|
69
|
+
channelRuntime: {
|
|
70
|
+
mode: "guided-smoke",
|
|
71
|
+
},
|
|
72
|
+
gateway: {
|
|
73
|
+
startAccount: {
|
|
74
|
+
source: "guided-smoke",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
registerChannel(payload) {
|
|
79
|
+
registeredPayloads.push(payload);
|
|
80
|
+
},
|
|
81
|
+
registerCli() { },
|
|
82
|
+
};
|
|
83
|
+
const plugin = await loadOpenClawWeixinDefaultExport();
|
|
84
|
+
plugin.register(compatHostApi);
|
|
85
|
+
for (const payload of registeredPayloads) {
|
|
86
|
+
const payloadPlugin = payload?.plugin;
|
|
87
|
+
const gateway = payloadPlugin && typeof payloadPlugin === "object" ? payloadPlugin.gateway : null;
|
|
88
|
+
if (hasQrLoginMethods(gateway)) {
|
|
89
|
+
return { gateway, pluginId: plugin.id ?? "unknown" };
|
|
90
|
+
}
|
|
91
|
+
if (hasQrLoginMethods(payloadPlugin)) {
|
|
92
|
+
return { gateway: payloadPlugin, pluginId: plugin.id ?? "unknown" };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
throw new Error("registerChannel did not expose weixin gateway loginWithQrStart/loginWithQrWait");
|
|
96
|
+
}
|
|
97
|
+
async function loadLatestWeixinAccountState() {
|
|
98
|
+
const require = createRequire(import.meta.url);
|
|
99
|
+
const stateDirModulePath = require.resolve(OPENCLAW_WEIXIN_JITI_SRC_HELPER_MODULES.stateDir);
|
|
100
|
+
const stateDirModule = getPublicJiti()(stateDirModulePath);
|
|
101
|
+
const stateDir = stateDirModule.resolveStateDir?.();
|
|
102
|
+
if (!stateDir) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const accountsIndexPath = path.join(stateDir, "openclaw-weixin", "accounts.json");
|
|
106
|
+
let accountIds = [];
|
|
107
|
+
try {
|
|
108
|
+
const raw = await readFile(accountsIndexPath, "utf8");
|
|
109
|
+
const parsed = JSON.parse(raw);
|
|
110
|
+
if (Array.isArray(parsed)) {
|
|
111
|
+
accountIds = parsed.filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
const accountId = accountIds.at(-1);
|
|
118
|
+
if (!accountId) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const accountFilePath = path.join(stateDir, "openclaw-weixin", "accounts", `${accountId}.json`);
|
|
123
|
+
const accountRaw = await readFile(accountFilePath, "utf8");
|
|
124
|
+
const account = JSON.parse(accountRaw);
|
|
125
|
+
const syncBufModulePath = require.resolve(OPENCLAW_WEIXIN_JITI_SRC_HELPER_MODULES.syncBuf);
|
|
126
|
+
const syncBufModule = getPublicJiti()(syncBufModulePath);
|
|
127
|
+
if (typeof account.token !== "string" || account.token.trim().length === 0) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
const syncBufFilePath = syncBufModule.getSyncBufFilePath?.(accountId);
|
|
131
|
+
const persistedGetUpdatesBuf = syncBufFilePath ? syncBufModule.loadGetUpdatesBuf?.(syncBufFilePath) : undefined;
|
|
132
|
+
return {
|
|
133
|
+
accountId,
|
|
134
|
+
token: account.token,
|
|
135
|
+
baseUrl: typeof account.baseUrl === "string" && account.baseUrl.trim().length > 0 ? account.baseUrl : "https://ilinkai.weixin.qq.com",
|
|
136
|
+
getUpdatesBuf: typeof persistedGetUpdatesBuf === "string" ? persistedGetUpdatesBuf : undefined,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function missingHelperError(helperName) {
|
|
144
|
+
return new Error(`[wechat-compat] required helper missing: ${helperName}`);
|
|
145
|
+
}
|
|
146
|
+
async function loadPublicWeixinHelpers() {
|
|
147
|
+
const require = createRequire(import.meta.url);
|
|
148
|
+
const getUpdatesModulePath = require.resolve(OPENCLAW_WEIXIN_JITI_SRC_HELPER_MODULES.getUpdates);
|
|
149
|
+
const getUpdatesModule = getPublicJiti()(getUpdatesModulePath);
|
|
150
|
+
if (typeof getUpdatesModule.getUpdates !== "function") {
|
|
151
|
+
throw new Error("public getUpdates helper unavailable");
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
getUpdates: getUpdatesModule.getUpdates,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
async function loadPublicWeixinSendHelper() {
|
|
158
|
+
const require = createRequire(import.meta.url);
|
|
159
|
+
const sendModulePath = require.resolve(OPENCLAW_WEIXIN_JITI_SRC_HELPER_MODULES.sendMessageWeixin);
|
|
160
|
+
const sendModule = getPublicJiti()(sendModulePath);
|
|
161
|
+
if (typeof sendModule.sendMessageWeixin !== "function") {
|
|
162
|
+
throw new Error("public sendMessageWeixin helper unavailable");
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
sendMessageWeixin: sendModule.sendMessageWeixin,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
async function loadPublicWeixinSyncBufHelpers() {
|
|
169
|
+
const require = createRequire(import.meta.url);
|
|
170
|
+
const syncBufModulePath = require.resolve(OPENCLAW_WEIXIN_JITI_SRC_HELPER_MODULES.syncBuf);
|
|
171
|
+
const syncBufModule = getPublicJiti()(syncBufModulePath);
|
|
172
|
+
if (typeof syncBufModule.getSyncBufFilePath !== "function" || typeof syncBufModule.saveGetUpdatesBuf !== "function") {
|
|
173
|
+
return {};
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
persistGetUpdatesBuf: async ({ accountId, getUpdatesBuf }) => {
|
|
177
|
+
const filePath = syncBufModule.getSyncBufFilePath(accountId);
|
|
178
|
+
syncBufModule.saveGetUpdatesBuf(filePath, getUpdatesBuf);
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
export async function loadOpenClawWeixinPublicHelpers(loaders = {}) {
|
|
183
|
+
const entry = await (loaders.resolveOpenClawWeixinPublicEntry ?? resolveOpenClawWeixinPublicEntry)();
|
|
184
|
+
const qrGatewayResult = await (loaders.loadPublicWeixinQrGateway ?? loadPublicWeixinQrGateway)();
|
|
185
|
+
const latestAccountState = await (loaders.loadLatestWeixinAccountState ?? loadLatestWeixinAccountState)();
|
|
186
|
+
const publicHelpers = await (loaders.loadPublicWeixinHelpers ?? loadPublicWeixinHelpers)();
|
|
187
|
+
const sendHelper = await (loaders.loadPublicWeixinSendHelper ?? loadPublicWeixinSendHelper)();
|
|
188
|
+
const syncBufHelpers = await loadPublicWeixinSyncBufHelpers();
|
|
189
|
+
if (typeof qrGatewayResult?.gateway?.loginWithQrStart !== "function" || typeof qrGatewayResult?.gateway?.loginWithQrWait !== "function") {
|
|
190
|
+
throw missingHelperError("qrGateway");
|
|
191
|
+
}
|
|
192
|
+
if (typeof publicHelpers?.getUpdates !== "function") {
|
|
193
|
+
throw missingHelperError("getUpdates");
|
|
194
|
+
}
|
|
195
|
+
if (typeof sendHelper?.sendMessageWeixin !== "function") {
|
|
196
|
+
throw missingHelperError("sendMessageWeixin");
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
entry,
|
|
200
|
+
pluginId: typeof qrGatewayResult.pluginId === "string" && qrGatewayResult.pluginId.length > 0 ? qrGatewayResult.pluginId : "unknown",
|
|
201
|
+
qrGateway: qrGatewayResult.gateway,
|
|
202
|
+
latestAccountState,
|
|
203
|
+
getUpdates: publicHelpers.getUpdates,
|
|
204
|
+
sendMessageWeixin: sendHelper.sendMessageWeixin,
|
|
205
|
+
persistGetUpdatesBuf: syncBufHelpers.persistGetUpdatesBuf,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type SlashOnlyCommand } from "./slash-guard.js";
|
|
2
|
+
import { type OpenClawWeixinPublicHelpers, type OpenClawWeixinPublicHelpersLoaderOptions } from "./openclaw-public-helpers.js";
|
|
3
|
+
type SmokeMode = "self-test" | "real-account";
|
|
4
|
+
type OpenClawSmokeHarnessOptions = {
|
|
5
|
+
mode: SmokeMode;
|
|
6
|
+
};
|
|
7
|
+
type PublicHelpersLoader = (options?: OpenClawWeixinPublicHelpersLoaderOptions) => Promise<OpenClawWeixinPublicHelpers>;
|
|
8
|
+
type RunOpenClawSmokeOptions = {
|
|
9
|
+
loadOpenClawWeixinPublicHelpers?: PublicHelpersLoader;
|
|
10
|
+
publicHelpersOptions?: OpenClawWeixinPublicHelpersLoaderOptions;
|
|
11
|
+
inputs?: string[];
|
|
12
|
+
dryRun?: boolean;
|
|
13
|
+
argv?: string[];
|
|
14
|
+
};
|
|
15
|
+
type SmokeGuardRejectResult = {
|
|
16
|
+
route: "guard-reject";
|
|
17
|
+
message: string;
|
|
18
|
+
};
|
|
19
|
+
type SmokeHostSelfTestResult = {
|
|
20
|
+
route: "public-self-test";
|
|
21
|
+
status: "loaded";
|
|
22
|
+
pluginId: string;
|
|
23
|
+
};
|
|
24
|
+
type SmokeStubResult = {
|
|
25
|
+
route: "stub";
|
|
26
|
+
command: SlashOnlyCommand;
|
|
27
|
+
argument: string;
|
|
28
|
+
stubReason: "stage-a-command-stub";
|
|
29
|
+
mode: SmokeMode;
|
|
30
|
+
};
|
|
31
|
+
type SmokeRealAccountDryRunResult = {
|
|
32
|
+
route: "real-account-dry-run";
|
|
33
|
+
binding: "skipped";
|
|
34
|
+
requiredEnvVars: readonly string[];
|
|
35
|
+
missingEnvVars: readonly string[];
|
|
36
|
+
manualSteps: readonly string[];
|
|
37
|
+
artifactPaths: readonly string[];
|
|
38
|
+
};
|
|
39
|
+
export type OpenClawSmokeHandleResult = SmokeGuardRejectResult | SmokeHostSelfTestResult | SmokeStubResult | SmokeRealAccountDryRunResult;
|
|
40
|
+
export type OpenClawSmokeHarness = {
|
|
41
|
+
handleIncomingText(input: string): Promise<OpenClawSmokeHandleResult>;
|
|
42
|
+
};
|
|
43
|
+
export declare function resolveRealAccountDryRunFlag(options?: Pick<RunOpenClawSmokeOptions, "dryRun" | "argv">): boolean;
|
|
44
|
+
export declare function createRealAccountDryRunPreparation(): SmokeRealAccountDryRunResult;
|
|
45
|
+
export declare function sanitizeOpenClawEvidenceSample(input: string): string;
|
|
46
|
+
export declare function createOpenClawSmokeHarness(options: OpenClawSmokeHarnessOptions): OpenClawSmokeHarness;
|
|
47
|
+
export declare function runOpenClawSmoke(mode: SmokeMode, options?: RunOpenClawSmokeOptions): Promise<OpenClawSmokeHandleResult[]>;
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { guardSlashOnlyInput } from "./slash-guard.js";
|
|
2
|
+
import { loadOpenClawWeixinPublicHelpers, } from "./openclaw-public-helpers.js";
|
|
3
|
+
const REAL_ACCOUNT_REQUIRED_ENV_VARS = [
|
|
4
|
+
"WECHAT_REAL_ACCOUNT_ID",
|
|
5
|
+
"WECHAT_DEVICE_ID",
|
|
6
|
+
"WECHAT_CONTEXT_TOKEN",
|
|
7
|
+
"WECHAT_BOT_TOKEN",
|
|
8
|
+
];
|
|
9
|
+
const REAL_ACCOUNT_MANUAL_STEPS = [
|
|
10
|
+
"执行 npm run wechat:smoke:real-account -- --dry-run,确认仅输出准备信息",
|
|
11
|
+
"确认所有必填环境变量已配置,再执行 npm run wechat:smoke:guided 完成真实账号手测",
|
|
12
|
+
"按 evidence/README.md 记录 blocked 或 known-unknown,并产出脱敏样本",
|
|
13
|
+
];
|
|
14
|
+
const REAL_ACCOUNT_ARTIFACT_PATHS = [
|
|
15
|
+
"docs/superpowers/wechat-stage-a/evidence/README.md",
|
|
16
|
+
"docs/superpowers/wechat-stage-a/api-samples-sanitized.md",
|
|
17
|
+
"docs/superpowers/wechat-stage-a/go-no-go.md",
|
|
18
|
+
];
|
|
19
|
+
export function resolveRealAccountDryRunFlag(options = {}) {
|
|
20
|
+
if (options.dryRun === true) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
const argv = options.argv ?? process.argv.slice(2);
|
|
24
|
+
return argv.includes("--dry-run");
|
|
25
|
+
}
|
|
26
|
+
export function createRealAccountDryRunPreparation() {
|
|
27
|
+
const missingEnvVars = REAL_ACCOUNT_REQUIRED_ENV_VARS.filter((name) => !process.env[name]?.trim());
|
|
28
|
+
return {
|
|
29
|
+
route: "real-account-dry-run",
|
|
30
|
+
binding: "skipped",
|
|
31
|
+
requiredEnvVars: [...REAL_ACCOUNT_REQUIRED_ENV_VARS],
|
|
32
|
+
missingEnvVars,
|
|
33
|
+
manualSteps: [...REAL_ACCOUNT_MANUAL_STEPS],
|
|
34
|
+
artifactPaths: [...REAL_ACCOUNT_ARTIFACT_PATHS],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function sanitizeOpenClawEvidenceSample(input) {
|
|
38
|
+
return input
|
|
39
|
+
.replace(/(contextToken\s*[=:]\s*)([^\s\n]+)/g, "$1[REDACTED_CONTEXT_TOKEN]")
|
|
40
|
+
.replace(/("contextToken"\s*:\s*")([^"]+)(")/g, "$1[REDACTED_CONTEXT_TOKEN]$3")
|
|
41
|
+
.replace(/(context_token\s*[=:]\s*)([^\s\n]+)/gi, "$1[REDACTED_CONTEXT_TOKEN]")
|
|
42
|
+
.replace(/("context_token"\s*:\s*")([^"]+)(")/gi, "$1[REDACTED_CONTEXT_TOKEN]$3")
|
|
43
|
+
.replace(/(bot_token\s*[=:]\s*)([^\s\n]+)/gi, "$1[REDACTED_BOT_TOKEN]")
|
|
44
|
+
.replace(/("bot_token"\s*:\s*")([^"]+)(")/gi, "$1[REDACTED_BOT_TOKEN]$3")
|
|
45
|
+
.replace(/(authorization\s*:\s*bearer\s+)([^\s\n]+)/gi, "$1[REDACTED_AUTHORIZATION]")
|
|
46
|
+
.replace(/("authorization"\s*:\s*")Bearer\s+([^"]+)(")/gi, "$1Bearer [REDACTED_AUTHORIZATION]$3")
|
|
47
|
+
.replace(/(userId\s*[=:]\s*)([^\s\n]+)/gi, "$1[REDACTED_USER_ID]")
|
|
48
|
+
.replace(/("userId"\s*:\s*")([^"]+)(")/gi, "$1[REDACTED_USER_ID]$3")
|
|
49
|
+
.replace(/(botId\s*[=:]\s*)([^\s\n]+)/gi, "$1[REDACTED_BOT_ID]")
|
|
50
|
+
.replace(/("botId"\s*:\s*")([^"]+)(")/gi, "$1[REDACTED_BOT_ID]$3")
|
|
51
|
+
.replace(/(qrCode\s*[=:]\s*)([^\s\n]+)/gi, "$1[REDACTED_QR_CODE]")
|
|
52
|
+
.replace(/("qrCode"\s*:\s*")([^"]+)(")/gi, "$1[REDACTED_QR_CODE]$3")
|
|
53
|
+
.replace(/(deviceId\s*[=:]\s*)([^\s\n]+)/gi, "$1[REDACTED_DEVICE_ID]")
|
|
54
|
+
.replace(/("deviceId"\s*:\s*")([^"]+)(")/gi, "$1[REDACTED_DEVICE_ID]$3")
|
|
55
|
+
.replace(/(messageId\s*[=:]\s*)([^\s\n]+)/gi, "$1[REDACTED_MESSAGE_ID]")
|
|
56
|
+
.replace(/("messageId"\s*:\s*")([^"]+)(")/gi, "$1[REDACTED_MESSAGE_ID]$3")
|
|
57
|
+
.replace(/(requestId\s*[=:]\s*)([^\s\n]+)/gi, "$1[REDACTED_REQUEST_ID]")
|
|
58
|
+
.replace(/("requestId"\s*:\s*")([^"]+)(")/gi, "$1[REDACTED_REQUEST_ID]$3");
|
|
59
|
+
}
|
|
60
|
+
export function createOpenClawSmokeHarness(options) {
|
|
61
|
+
return {
|
|
62
|
+
async handleIncomingText(input) {
|
|
63
|
+
const guarded = guardSlashOnlyInput(input);
|
|
64
|
+
if (!guarded.accepted) {
|
|
65
|
+
return {
|
|
66
|
+
route: "guard-reject",
|
|
67
|
+
message: guarded.message,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
route: "stub",
|
|
72
|
+
command: guarded.command,
|
|
73
|
+
argument: guarded.argument,
|
|
74
|
+
stubReason: "stage-a-command-stub",
|
|
75
|
+
mode: options.mode,
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export async function runOpenClawSmoke(mode, options = {}) {
|
|
81
|
+
const results = [];
|
|
82
|
+
if (mode === "self-test") {
|
|
83
|
+
const helpers = await (options.loadOpenClawWeixinPublicHelpers ?? loadOpenClawWeixinPublicHelpers)(options.publicHelpersOptions);
|
|
84
|
+
results.push({
|
|
85
|
+
route: "public-self-test",
|
|
86
|
+
status: "loaded",
|
|
87
|
+
pluginId: helpers.pluginId,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (mode === "real-account") {
|
|
91
|
+
results.push(createRealAccountDryRunPreparation());
|
|
92
|
+
return results;
|
|
93
|
+
}
|
|
94
|
+
const harness = createOpenClawSmokeHarness({ mode });
|
|
95
|
+
const inputs = options.inputs ?? ["hello", "/status", "/reply smoke", "/allow once"];
|
|
96
|
+
for (const input of inputs) {
|
|
97
|
+
results.push(await harness.handleIncomingText(input));
|
|
98
|
+
}
|
|
99
|
+
return results;
|
|
100
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const STAGE_A_SLASH_ONLY_MESSAGE = "\u5F53\u524D\u9636\u6BB5\u4EC5\u652F\u6301\u547D\u4EE4\u578B\u4EA4\u4E92\uFF0C\u8BF7\u53D1\u9001 /status\u3001/reply \u6216 /allow\u3002";
|
|
2
|
+
export type SlashOnlyCommand = "status" | "reply" | "allow";
|
|
3
|
+
export type SlashGuardResult = {
|
|
4
|
+
accepted: true;
|
|
5
|
+
command: SlashOnlyCommand;
|
|
6
|
+
argument: string;
|
|
7
|
+
} | {
|
|
8
|
+
accepted: false;
|
|
9
|
+
message: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function guardSlashOnlyInput(input: string): SlashGuardResult;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const STAGE_A_SLASH_ONLY_MESSAGE = "当前阶段仅支持命令型交互,请发送 /status、/reply 或 /allow。";
|
|
2
|
+
const ALLOWED_COMMANDS = new Set(["status", "reply", "allow"]);
|
|
3
|
+
export function guardSlashOnlyInput(input) {
|
|
4
|
+
const normalized = input.trim();
|
|
5
|
+
if (!normalized.startsWith("/")) {
|
|
6
|
+
return {
|
|
7
|
+
accepted: false,
|
|
8
|
+
message: STAGE_A_SLASH_ONLY_MESSAGE,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
const [commandSegment = "", ...rest] = normalized.slice(1).trim().split(/\s+/);
|
|
12
|
+
const command = commandSegment.toLowerCase();
|
|
13
|
+
if (!ALLOWED_COMMANDS.has(command)) {
|
|
14
|
+
return {
|
|
15
|
+
accepted: false,
|
|
16
|
+
message: STAGE_A_SLASH_ONLY_MESSAGE,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
accepted: true,
|
|
21
|
+
command,
|
|
22
|
+
argument: rest.join(" "),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { WechatRequestKind } from "./state-paths.js";
|
|
2
|
+
export declare function createRouteKey(input: {
|
|
3
|
+
kind: WechatRequestKind;
|
|
4
|
+
requestID: string;
|
|
5
|
+
}): string;
|
|
6
|
+
export declare function normalizeHandle(input: string): string;
|
|
7
|
+
export declare function assertValidHandleInput(input: string): void;
|
|
8
|
+
export declare function createHandle(kind: WechatRequestKind, existingHandles: Iterable<string>): string;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
const HANDLE_PREFIX = {
|
|
3
|
+
question: "q",
|
|
4
|
+
permission: "p",
|
|
5
|
+
};
|
|
6
|
+
function normalizeRequestID(requestID) {
|
|
7
|
+
return requestID.trim().toLowerCase();
|
|
8
|
+
}
|
|
9
|
+
export function createRouteKey(input) {
|
|
10
|
+
const normalized = normalizeRequestID(input.requestID);
|
|
11
|
+
const digest = crypto.createHash("sha1").update(`${input.kind}:${normalized}`).digest("hex").slice(0, 12);
|
|
12
|
+
return `${input.kind}-${digest}`;
|
|
13
|
+
}
|
|
14
|
+
export function normalizeHandle(input) {
|
|
15
|
+
const value = input.trim().toLowerCase();
|
|
16
|
+
if (!/^[a-z][a-z0-9]*$/.test(value)) {
|
|
17
|
+
throw new Error("invalid handle format");
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
function isRawRequestIDLike(input) {
|
|
22
|
+
return /^req(?:uest)?[-_]/i.test(input.trim());
|
|
23
|
+
}
|
|
24
|
+
export function assertValidHandleInput(input) {
|
|
25
|
+
if (isRawRequestIDLike(input)) {
|
|
26
|
+
throw new Error("raw requestID cannot be used as handle");
|
|
27
|
+
}
|
|
28
|
+
normalizeHandle(input);
|
|
29
|
+
}
|
|
30
|
+
export function createHandle(kind, existingHandles) {
|
|
31
|
+
const prefix = HANDLE_PREFIX[kind];
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
for (const item of existingHandles) {
|
|
34
|
+
try {
|
|
35
|
+
seen.add(normalizeHandle(item));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// ignore invalid historical values
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
let index = 1;
|
|
42
|
+
while (seen.has(`${prefix}${index}`)) {
|
|
43
|
+
index += 1;
|
|
44
|
+
}
|
|
45
|
+
return `${prefix}${index}`;
|
|
46
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { BrokerMessageType } from "./protocol.js";
|
|
2
|
+
export declare function createSessionToken(): string;
|
|
3
|
+
export declare function registerConnection(instanceID: string, connectionRef: unknown): string;
|
|
4
|
+
export declare function validateSessionToken(instanceID: string, token: string): boolean;
|
|
5
|
+
export declare function revokeSessionToken(instanceID: string): void;
|
|
6
|
+
export declare function isAuthRequired(type: BrokerMessageType): boolean;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
const sessionByInstanceID = new Map();
|
|
3
|
+
const TOKEN_FREE_TYPES = ["registerInstance", "ping"];
|
|
4
|
+
function isNonEmptyString(value) {
|
|
5
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
6
|
+
}
|
|
7
|
+
export function createSessionToken() {
|
|
8
|
+
return randomUUID();
|
|
9
|
+
}
|
|
10
|
+
export function registerConnection(instanceID, connectionRef) {
|
|
11
|
+
if (!isNonEmptyString(instanceID)) {
|
|
12
|
+
throw new Error("invalid instanceID");
|
|
13
|
+
}
|
|
14
|
+
const token = createSessionToken();
|
|
15
|
+
sessionByInstanceID.set(instanceID, { token, connectionRef });
|
|
16
|
+
return token;
|
|
17
|
+
}
|
|
18
|
+
export function validateSessionToken(instanceID, token) {
|
|
19
|
+
if (!isNonEmptyString(instanceID) || !isNonEmptyString(token)) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const current = sessionByInstanceID.get(instanceID);
|
|
23
|
+
if (!current) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
return current.token === token;
|
|
27
|
+
}
|
|
28
|
+
export function revokeSessionToken(instanceID) {
|
|
29
|
+
if (!isNonEmptyString(instanceID)) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
sessionByInstanceID.delete(instanceID);
|
|
33
|
+
}
|
|
34
|
+
export function isAuthRequired(type) {
|
|
35
|
+
if (TOKEN_FREE_TYPES.includes(type)) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type OperatorBinding = {
|
|
2
|
+
wechatAccountId: string;
|
|
3
|
+
userId: string;
|
|
4
|
+
boundAt: number;
|
|
5
|
+
};
|
|
6
|
+
export declare function readOperatorBinding(): Promise<OperatorBinding | undefined>;
|
|
7
|
+
export declare function bindOperator(input: OperatorBinding): Promise<OperatorBinding>;
|
|
8
|
+
export declare function resetOperatorBinding(): Promise<void>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { WECHAT_FILE_MODE, ensureWechatStateLayout, operatorStatePath } from "./state-paths.js";
|
|
4
|
+
function isNonEmptyString(value) {
|
|
5
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
6
|
+
}
|
|
7
|
+
function isValidBinding(input) {
|
|
8
|
+
return (isNonEmptyString(input.wechatAccountId) &&
|
|
9
|
+
isNonEmptyString(input.userId) &&
|
|
10
|
+
typeof input.boundAt === "number" &&
|
|
11
|
+
Number.isFinite(input.boundAt));
|
|
12
|
+
}
|
|
13
|
+
function toBinding(input) {
|
|
14
|
+
return {
|
|
15
|
+
wechatAccountId: input.wechatAccountId,
|
|
16
|
+
userId: input.userId,
|
|
17
|
+
boundAt: input.boundAt,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export async function readOperatorBinding() {
|
|
21
|
+
try {
|
|
22
|
+
const raw = await readFile(operatorStatePath(), "utf8");
|
|
23
|
+
const parsed = JSON.parse(raw);
|
|
24
|
+
if (!isValidBinding(parsed)) {
|
|
25
|
+
throw new Error("invalid operator binding format");
|
|
26
|
+
}
|
|
27
|
+
return toBinding(parsed);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
const issue = error;
|
|
31
|
+
if (issue.code === "ENOENT")
|
|
32
|
+
return undefined;
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export async function bindOperator(input) {
|
|
37
|
+
const next = toBinding(input);
|
|
38
|
+
if (!isValidBinding(next)) {
|
|
39
|
+
throw new Error("invalid operator binding format");
|
|
40
|
+
}
|
|
41
|
+
const existing = await readOperatorBinding();
|
|
42
|
+
if (existing && (existing.userId !== next.userId || existing.wechatAccountId !== next.wechatAccountId)) {
|
|
43
|
+
throw new Error("operator already bound to another user");
|
|
44
|
+
}
|
|
45
|
+
await ensureWechatStateLayout();
|
|
46
|
+
await mkdir(path.dirname(operatorStatePath()), { recursive: true });
|
|
47
|
+
await writeFile(operatorStatePath(), JSON.stringify(next, null, 2), { mode: WECHAT_FILE_MODE });
|
|
48
|
+
return next;
|
|
49
|
+
}
|
|
50
|
+
export async function resetOperatorBinding() {
|
|
51
|
+
try {
|
|
52
|
+
await unlink(operatorStatePath());
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
const issue = error;
|
|
56
|
+
if (issue.code !== "ENOENT")
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|