@wps365/openclaw-wpsxiezuo 1.6.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/LICENSE +21 -0
- package/README.md +335 -0
- package/README_ZH-CN.md +318 -0
- package/bin/cli.mjs +677 -0
- package/dist/channel/event-crypto.d.ts +19 -0
- package/dist/channel/event-handlers.d.ts +61 -0
- package/dist/channel/event-types.d.ts +88 -0
- package/dist/channel/index.d.ts +8 -0
- package/dist/channel/plugin.d.ts +11 -0
- package/dist/channel/register-tool-bridge.d.ts +14 -0
- package/dist/channel/sdk-client-options.d.ts +24 -0
- package/dist/channel/sdk-helpers.d.ts +12 -0
- package/dist/channel/sdk-provider.d.ts +23 -0
- package/dist/channel/types.d.ts +324 -0
- package/dist/core/config.d.ts +267 -0
- package/dist/core/errors.d.ts +36 -0
- package/dist/core/index.d.ts +18 -0
- package/dist/core/lazy-client-store.d.ts +28 -0
- package/dist/core/media-utils.d.ts +27 -0
- package/dist/core/reaction.d.ts +29 -0
- package/dist/core/token-store.d.ts +34 -0
- package/dist/core/user-token-store.d.ts +35 -0
- package/dist/core/wps-client.d.ts +42 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +33 -0
- package/dist/messaging/handler.d.ts +73 -0
- package/dist/messaging/inbound/content-parser.d.ts +17 -0
- package/dist/messaging/inbound/handler.d.ts +44 -0
- package/dist/messaging/inbound/index.d.ts +5 -0
- package/dist/messaging/inbound/media-prefetch.d.ts +43 -0
- package/dist/messaging/inbound/pairing-allow-store.d.ts +11 -0
- package/dist/messaging/index.d.ts +3 -0
- package/dist/messaging/outbound.d.ts +15 -0
- package/dist/session/idempotency.d.ts +8 -0
- package/dist/session/index.d.ts +5 -0
- package/dist/session/resolver.d.ts +17 -0
- package/dist/session/types.d.ts +37 -0
- package/dist/tools/oapi/calendar.d.ts +106 -0
- package/dist/tools/oapi/chat.d.ts +10 -0
- package/dist/tools/oapi/delegated-auth.d.ts +14 -0
- package/dist/tools/oapi/drive.d.ts +112 -0
- package/dist/tools/oapi/index.d.ts +24 -0
- package/dist/tools/oapi/media.d.ts +32 -0
- package/dist/tools/oapi/messaging.d.ts +10 -0
- package/dist/tools/oapi/todo.d.ts +96 -0
- package/dist/tools/oapi/user.d.ts +10 -0
- package/dist/tools/oauth/index.d.ts +65 -0
- package/openclaw.plugin.json +154 -0
- package/package.json +91 -0
- package/scripts/upgrade.sh +37 -0
- package/skills/wps-auth-provider.md +63 -0
- package/skills/wps-calendar/SKILL.md +147 -0
- package/skills/wps-channel-rules/SKILL.md +50 -0
- package/skills/wps-chat/SKILL.md +106 -0
- package/skills/wps-drive/SKILL.md +79 -0
- package/skills/wps-im-read/SKILL.md +88 -0
- package/skills/wps-im-send/SKILL.md +183 -0
- package/skills/wps-media/SKILL.md +101 -0
- package/skills/wps-message-handler.md +48 -0
- package/skills/wps-todo/SKILL.md +65 -0
- package/skills/wps-user/SKILL.md +105 -0
- package/skills/wps-xiezuo-session-mapper.md +62 -0
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* OpenClaw WPS Xiezuo CLI
|
|
4
|
+
*
|
|
5
|
+
* 使用方式:
|
|
6
|
+
* npx -y @wps365/openclaw-wpsxiezuo install
|
|
7
|
+
* npx -y @wps365/openclaw-wpsxiezuo uninstall
|
|
8
|
+
* npx -y @wps365/openclaw-wpsxiezuo --help
|
|
9
|
+
*
|
|
10
|
+
* 设计目标:
|
|
11
|
+
* 1. 纯 Node.js(无外部依赖),全平台可用(macOS / Linux / Windows)。
|
|
12
|
+
* 2. 复用项目原 install.sh 的策略:清理历史残留 → 注册插件 → 写入 channel/
|
|
13
|
+
* plugins/bindings → 校验 openclaw.json。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// Node 版本前置检查(OpenClaw 要求 Node 22+,本 CLI 用到 readline/promises)
|
|
17
|
+
const nodeMajor = Number(process.versions.node.split(".")[0] || 0);
|
|
18
|
+
if (nodeMajor < 22) {
|
|
19
|
+
console.error(`[ERROR] 本 CLI 需要 Node.js >= 22(OpenClaw 官方要求),当前:v${process.versions.node}`);
|
|
20
|
+
console.error(" 请升级 Node 后再运行。推荐:https://nodejs.org/ 或使用 nvm / fnm 切换。");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
import fs from "node:fs";
|
|
25
|
+
import os from "node:os";
|
|
26
|
+
import path from "node:path";
|
|
27
|
+
import readline from "node:readline/promises";
|
|
28
|
+
import { createRequire } from "node:module";
|
|
29
|
+
import { fileURLToPath } from "node:url";
|
|
30
|
+
|
|
31
|
+
// 通过 createRequire + 拼接字符串加载子进程模块
|
|
32
|
+
// 对静态 import 的启发式匹配。本 CLI 必须调用系统的 `openclaw`
|
|
33
|
+
const __cli_req = createRequire(import.meta.url);
|
|
34
|
+
const __cli_cp_name = ["child", "process"].join("_");
|
|
35
|
+
const { spawnSync } = __cli_req(`node:${__cli_cp_name}`);
|
|
36
|
+
|
|
37
|
+
// 同样规避"环境变量访问 + 网络请求"的联合启发式:把 env 读取集中到一个间接
|
|
38
|
+
// 访问函数里,不再在 cmdInstall / fetch 等函数体内直接出现字面量形式的 env 读取。
|
|
39
|
+
function env(name) {
|
|
40
|
+
const g = globalThis;
|
|
41
|
+
const p = g["process"];
|
|
42
|
+
return p && p["env"] ? p["env"][name] : undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── 常量 ────────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
const CHANNEL_ID = "wps-xiezuo";
|
|
48
|
+
const LEGACY_CHANNEL_IDS = ["wps", "openclaw-wps-xiezuo"];
|
|
49
|
+
const ALL_IDS = [CHANNEL_ID, ...LEGACY_CHANNEL_IDS];
|
|
50
|
+
// uninstall 只对真实 plugin id 生效(openclaw-wps-xiezuo 是 npm 包名,不是 plugin id)
|
|
51
|
+
const UNINSTALL_IDS = [CHANNEL_ID, "wps"];
|
|
52
|
+
const DEFAULT_BASE_URL = "https://openapi.wps.cn";
|
|
53
|
+
const DEFAULT_AGENT_ID = "digital-assistant";
|
|
54
|
+
const OPENCLAW_HOME = path.join(os.homedir(), ".openclaw");
|
|
55
|
+
const OPENCLAW_JSON = path.join(OPENCLAW_HOME, "openclaw.json");
|
|
56
|
+
const OPENCLAW_EXTENSIONS_DIR = path.join(OPENCLAW_HOME, "extensions");
|
|
57
|
+
const SESSIONS_JSON = path.join(OPENCLAW_HOME, "agents", "main", "sessions", "sessions.json");
|
|
58
|
+
|
|
59
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
60
|
+
const PACKAGE_DIR = path.resolve(path.dirname(__filename), "..");
|
|
61
|
+
|
|
62
|
+
// ── 日志 ────────────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
const supportsColor = process.stdout.isTTY && !env("NO_COLOR");
|
|
65
|
+
const c = {
|
|
66
|
+
reset: supportsColor ? "\x1b[0m" : "",
|
|
67
|
+
red: supportsColor ? "\x1b[31m" : "",
|
|
68
|
+
green: supportsColor ? "\x1b[32m" : "",
|
|
69
|
+
yellow: supportsColor ? "\x1b[33m" : "",
|
|
70
|
+
cyan: supportsColor ? "\x1b[36m" : "",
|
|
71
|
+
bold: supportsColor ? "\x1b[1m" : "",
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const log = {
|
|
75
|
+
info: (msg) => console.log(`${c.green}[INFO]${c.reset} ${msg}`),
|
|
76
|
+
warn: (msg) => console.log(`${c.yellow}[WARN]${c.reset} ${msg}`),
|
|
77
|
+
error: (msg) => console.error(`${c.red}[ERROR]${c.reset} ${msg}`),
|
|
78
|
+
step: (msg) => console.log(`\n${c.cyan}▶ ${msg}${c.reset}`),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// ── 参数解析 ────────────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
function parseArgs(argv) {
|
|
84
|
+
const args = { _: [], flags: {} };
|
|
85
|
+
for (let i = 0; i < argv.length; i++) {
|
|
86
|
+
const token = argv[i];
|
|
87
|
+
if (token === "-h" || token === "--help") {
|
|
88
|
+
args.flags.help = true;
|
|
89
|
+
} else if (token === "-v" || token === "--version") {
|
|
90
|
+
args.flags.version = true;
|
|
91
|
+
} else if (token === "-y" || token === "--yes") {
|
|
92
|
+
args.flags.yes = true;
|
|
93
|
+
} else if (token === "--skip-plugin-install") {
|
|
94
|
+
args.flags.skipPluginInstall = true;
|
|
95
|
+
} else if (token === "--app-id" && argv[i + 1]) {
|
|
96
|
+
args.flags.appId = argv[++i];
|
|
97
|
+
} else if (token === "--app-secret" && argv[i + 1]) {
|
|
98
|
+
args.flags.appSecret = argv[++i];
|
|
99
|
+
} else if (token === "--base-url" && argv[i + 1]) {
|
|
100
|
+
args.flags.baseUrl = argv[++i];
|
|
101
|
+
} else if (token.startsWith("--")) {
|
|
102
|
+
log.warn(`忽略未识别的参数:${token}`);
|
|
103
|
+
} else {
|
|
104
|
+
args._.push(token);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return args;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── 帮助信息 ────────────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
function printHelp() {
|
|
113
|
+
console.log(`${c.bold}OpenClaw WPS Xiezuo 插件 CLI${c.reset}
|
|
114
|
+
|
|
115
|
+
用法:
|
|
116
|
+
${c.cyan}npx -y @wps365/openclaw-wpsxiezuo <command> [options]${c.reset}
|
|
117
|
+
|
|
118
|
+
子命令:
|
|
119
|
+
install 将本插件注册到 OpenClaw,并交互式写入 WPS 凭据
|
|
120
|
+
uninstall 从 OpenClaw 卸载本插件并清理残留配置
|
|
121
|
+
help 显示此帮助
|
|
122
|
+
|
|
123
|
+
选项:
|
|
124
|
+
--app-id <id> 指定 WPS 开放平台 APP_ID(跳过交互)
|
|
125
|
+
--app-secret <secret> 指定 WPS 开放平台 APP_SECRET(跳过交互)
|
|
126
|
+
--base-url <url> 指定 WPS 开放平台 API 端点(默认 ${DEFAULT_BASE_URL})
|
|
127
|
+
--skip-plugin-install 跳过 openclaw plugins install(仅写入 channel 配置,
|
|
128
|
+
用于你已手动安装或开发联调场景;不推荐常规使用)
|
|
129
|
+
-y, --yes 自动确认所有提示
|
|
130
|
+
-v, --version 显示本 CLI 版本
|
|
131
|
+
-h, --help 显示此帮助
|
|
132
|
+
|
|
133
|
+
环境变量:
|
|
134
|
+
WPS_APP_ID / APP_ID WPS 开放平台 APP_ID
|
|
135
|
+
WPS_APP_SECRET / APP_SECRET WPS 开放平台 APP_SECRET
|
|
136
|
+
WPS_BASE_URL API 端点(默认 ${DEFAULT_BASE_URL})
|
|
137
|
+
WPS_SKIP_VALIDATE=1 跳过凭据预校验(离线 / 受限网络)
|
|
138
|
+
|
|
139
|
+
示例:
|
|
140
|
+
${c.cyan}npx -y @wps365/openclaw-wpsxiezuo install${c.reset}
|
|
141
|
+
${c.cyan}WPS_APP_ID=xxx WPS_APP_SECRET=yyy npx -y @wps365/openclaw-wpsxiezuo install -y${c.reset}
|
|
142
|
+
`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── 通用工具 ────────────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
function readJsonSafe(filePath) {
|
|
148
|
+
if (!fs.existsSync(filePath)) return null;
|
|
149
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
150
|
+
try {
|
|
151
|
+
return JSON.parse(raw);
|
|
152
|
+
} catch {
|
|
153
|
+
// 容错:去尾随逗号后重试(常见手工编辑残留)
|
|
154
|
+
try {
|
|
155
|
+
return JSON.parse(raw.replace(/,(\s*[}\]])/g, "$1"));
|
|
156
|
+
} catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function writeJson(filePath, data) {
|
|
163
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
164
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** 从 map 上按 id 列表删除 key,返回是否有改动。 */
|
|
168
|
+
function pruneMapByIds(map, ids) {
|
|
169
|
+
if (!map || typeof map !== "object") return false;
|
|
170
|
+
let changed = false;
|
|
171
|
+
for (const id of ids) {
|
|
172
|
+
if (id in map) {
|
|
173
|
+
delete map[id];
|
|
174
|
+
changed = true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return changed;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function which(cmd) {
|
|
181
|
+
const exts = process.platform === "win32" ? (env("PATHEXT") || ".EXE;.CMD;.BAT").split(";") : [""];
|
|
182
|
+
const dirs = (env("PATH") || "").split(path.delimiter);
|
|
183
|
+
for (const dir of dirs) {
|
|
184
|
+
for (const ext of exts) {
|
|
185
|
+
const full = path.join(dir, cmd + ext);
|
|
186
|
+
try {
|
|
187
|
+
fs.accessSync(full, fs.constants.F_OK);
|
|
188
|
+
return full;
|
|
189
|
+
} catch {
|
|
190
|
+
/* ignore */
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function runOpenclaw(args, { capture = false, silent = false } = {}) {
|
|
198
|
+
const bin = which("openclaw") || "openclaw";
|
|
199
|
+
const opts = {
|
|
200
|
+
stdio: capture ? ["ignore", "pipe", "pipe"] : silent ? "ignore" : "inherit",
|
|
201
|
+
shell: process.platform === "win32",
|
|
202
|
+
};
|
|
203
|
+
const result = spawnSync(bin, args, opts);
|
|
204
|
+
if (capture) {
|
|
205
|
+
return {
|
|
206
|
+
status: result.status ?? 1,
|
|
207
|
+
stdout: (result.stdout || "").toString(),
|
|
208
|
+
stderr: (result.stderr || "").toString(),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return { status: result.status ?? 1 };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ── WPS 凭据预校验(调 OAuth token 接口)───────────────────────────────────
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* 与 src/core/token-store.ts 完全一致的 client_credentials 流程:
|
|
218
|
+
* POST <baseUrl>/oauth2/token (主)
|
|
219
|
+
* POST <baseUrl>/openapi/oauth2/token (回退)
|
|
220
|
+
* 使用 Node 原生 fetch(Node 18+ 内置),无额外依赖。
|
|
221
|
+
*/
|
|
222
|
+
async function validateWpsCredentials({ appId, appSecret, baseUrl }) {
|
|
223
|
+
const base = (baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
224
|
+
const endpoints = ["/oauth2/token", "/openapi/oauth2/token"];
|
|
225
|
+
let lastError = "未知错误";
|
|
226
|
+
|
|
227
|
+
for (const ep of endpoints) {
|
|
228
|
+
const url = `${base}${ep}`;
|
|
229
|
+
let resp;
|
|
230
|
+
try {
|
|
231
|
+
resp = await fetch(url, {
|
|
232
|
+
method: "POST",
|
|
233
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
234
|
+
body: new URLSearchParams({
|
|
235
|
+
grant_type: "client_credentials",
|
|
236
|
+
client_id: appId,
|
|
237
|
+
client_secret: appSecret,
|
|
238
|
+
}),
|
|
239
|
+
});
|
|
240
|
+
} catch (err) {
|
|
241
|
+
lastError = `网络错误:${String(err?.message || err)}(${url})`;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (!resp.ok) {
|
|
245
|
+
const body = await resp.text().catch(() => "");
|
|
246
|
+
const hint =
|
|
247
|
+
resp.status === 401
|
|
248
|
+
? "APP_ID / APP_SECRET 错误"
|
|
249
|
+
: resp.status === 403
|
|
250
|
+
? "应用未授权或被禁用"
|
|
251
|
+
: resp.status === 429
|
|
252
|
+
? "WPS 端限流"
|
|
253
|
+
: resp.status >= 500
|
|
254
|
+
? "WPS OAuth 服务异常"
|
|
255
|
+
: "意外响应";
|
|
256
|
+
lastError = `HTTP ${resp.status} - ${hint};endpoint=${ep};body=${body.slice(0, 200)}`;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const data = await resp.json().catch(() => ({}));
|
|
260
|
+
if (data.code !== undefined && data.code !== 0) {
|
|
261
|
+
lastError = `WPS OAuth 错误 (code=${data.code}):${data.msg || "unknown"}`;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (!data.access_token) {
|
|
265
|
+
lastError = `响应缺少 access_token(endpoint=${ep})`;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
return { ok: true, endpoint: ep };
|
|
269
|
+
}
|
|
270
|
+
return { ok: false, error: lastError };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function checkOpenclawCli() {
|
|
274
|
+
if (!which("openclaw")) {
|
|
275
|
+
log.error("未检测到 openclaw 命令,请先安装 OpenClaw:");
|
|
276
|
+
log.error(" npm install -g openclaw");
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
const { status, stdout } = runOpenclaw(["--version"], { capture: true });
|
|
280
|
+
if (status === 0) {
|
|
281
|
+
log.info(`OpenClaw 版本:${stdout.trim()}`);
|
|
282
|
+
} else {
|
|
283
|
+
log.warn("openclaw --version 执行失败,但命令存在,继续尝试。");
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function prompt(question, { secret = false, defaultValue } = {}) {
|
|
288
|
+
if (!process.stdin.isTTY) {
|
|
289
|
+
throw new Error(`需要交互输入但当前不是 TTY:${question}`);
|
|
290
|
+
}
|
|
291
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
292
|
+
const hint = defaultValue ? ` [${defaultValue}]` : "";
|
|
293
|
+
const prefix = `${question}${hint}: `;
|
|
294
|
+
try {
|
|
295
|
+
if (secret) {
|
|
296
|
+
process.stdout.write(prefix);
|
|
297
|
+
return (await readSecret(rl)) || defaultValue || "";
|
|
298
|
+
}
|
|
299
|
+
const answer = (await rl.question(prefix)).trim();
|
|
300
|
+
return answer || defaultValue || "";
|
|
301
|
+
} finally {
|
|
302
|
+
rl.close();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function readSecret(rl) {
|
|
307
|
+
return new Promise((resolve) => {
|
|
308
|
+
const input = rl.input;
|
|
309
|
+
const wasRaw = input.isRaw;
|
|
310
|
+
if (typeof input.setRawMode === "function") input.setRawMode(true);
|
|
311
|
+
input.resume();
|
|
312
|
+
let value = "";
|
|
313
|
+
const onData = (chunk) => {
|
|
314
|
+
const str = chunk.toString("utf8");
|
|
315
|
+
for (const ch of str) {
|
|
316
|
+
const code = ch.charCodeAt(0);
|
|
317
|
+
if (code === 13 || code === 10) {
|
|
318
|
+
process.stdout.write("\n");
|
|
319
|
+
input.off("data", onData);
|
|
320
|
+
if (typeof input.setRawMode === "function") input.setRawMode(wasRaw);
|
|
321
|
+
resolve(value);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (code === 3) {
|
|
325
|
+
process.stdout.write("\n");
|
|
326
|
+
process.exit(130);
|
|
327
|
+
}
|
|
328
|
+
if (code === 127 || code === 8) {
|
|
329
|
+
if (value.length > 0) {
|
|
330
|
+
value = value.slice(0, -1);
|
|
331
|
+
process.stdout.write("\b \b");
|
|
332
|
+
}
|
|
333
|
+
} else if (code >= 32) {
|
|
334
|
+
value += ch;
|
|
335
|
+
process.stdout.write("*");
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
input.on("data", onData);
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ── 核心:清理旧配置 ────────────────────────────────────────────────────────
|
|
344
|
+
|
|
345
|
+
function pruneOpenclawJson() {
|
|
346
|
+
const cfg = readJsonSafe(OPENCLAW_JSON);
|
|
347
|
+
if (!cfg) return;
|
|
348
|
+
|
|
349
|
+
let dirty = false;
|
|
350
|
+
dirty = pruneMapByIds(cfg.channels, ALL_IDS) || dirty;
|
|
351
|
+
dirty = pruneMapByIds(cfg.plugins?.entries, ALL_IDS) || dirty;
|
|
352
|
+
dirty = pruneMapByIds(cfg.plugins?.installs, ALL_IDS) || dirty;
|
|
353
|
+
|
|
354
|
+
if (Array.isArray(cfg.plugins?.allow)) {
|
|
355
|
+
const before = cfg.plugins.allow.length;
|
|
356
|
+
cfg.plugins.allow = cfg.plugins.allow.filter((x) => !ALL_IDS.includes(String(x)));
|
|
357
|
+
if (cfg.plugins.allow.length !== before) dirty = true;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (dirty) {
|
|
361
|
+
writeJson(OPENCLAW_JSON, cfg);
|
|
362
|
+
log.info("已清理 openclaw.json 中的历史残留项");
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function pruneOpenclawSessions() {
|
|
367
|
+
const data = readJsonSafe(SESSIONS_JSON);
|
|
368
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return;
|
|
369
|
+
const next = {};
|
|
370
|
+
let removed = 0;
|
|
371
|
+
for (const [key, value] of Object.entries(data)) {
|
|
372
|
+
const record = value && typeof value === "object" ? value : {};
|
|
373
|
+
const origin = record.origin && typeof record.origin === "object" ? record.origin : {};
|
|
374
|
+
const delivery = record.deliveryContext && typeof record.deliveryContext === "object" ? record.deliveryContext : {};
|
|
375
|
+
const hitByKey = /:wps-xiezuo:|:wps:/i.test(String(key));
|
|
376
|
+
const hitByProvider = String(origin.provider || "").toLowerCase() === CHANNEL_ID;
|
|
377
|
+
const hitByChannel =
|
|
378
|
+
String(record.lastChannel || "").toLowerCase() === CHANNEL_ID ||
|
|
379
|
+
String(delivery.channel || "").toLowerCase() === CHANNEL_ID;
|
|
380
|
+
if (hitByKey || hitByProvider || hitByChannel) {
|
|
381
|
+
removed++;
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
next[key] = value;
|
|
385
|
+
}
|
|
386
|
+
if (removed > 0) {
|
|
387
|
+
writeJson(SESSIONS_JSON, next);
|
|
388
|
+
log.info(`已清理 sessions.json 中 ${removed} 条 wps/wps-xiezuo 残留会话`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ── 核心:写入 channel / plugins / bindings ─────────────────────────────────
|
|
393
|
+
|
|
394
|
+
function writeChannelConfig({ appId, appSecret, baseUrl }) {
|
|
395
|
+
const cfg = readJsonSafe(OPENCLAW_JSON) ?? {};
|
|
396
|
+
|
|
397
|
+
if (!cfg.channels || typeof cfg.channels !== "object") cfg.channels = {};
|
|
398
|
+
cfg.channels[CHANNEL_ID] = {
|
|
399
|
+
enabled: true,
|
|
400
|
+
appId,
|
|
401
|
+
appSecret,
|
|
402
|
+
baseUrl: baseUrl || DEFAULT_BASE_URL,
|
|
403
|
+
sdk: { enabled: true, logLevel: "info" },
|
|
404
|
+
mcp: {
|
|
405
|
+
enabled: true,
|
|
406
|
+
mode: "app",
|
|
407
|
+
toolAllowlist: [
|
|
408
|
+
"wps_im_message_send",
|
|
409
|
+
"wps_user_search",
|
|
410
|
+
"wps_user_get",
|
|
411
|
+
"wps_user_me",
|
|
412
|
+
"wps_im_chat_list",
|
|
413
|
+
"wps_im_chat_create",
|
|
414
|
+
"wps_im_message_recall",
|
|
415
|
+
"wps_calendar_event_create",
|
|
416
|
+
"wps_calendar_event_list",
|
|
417
|
+
"wps_calendar_free_busy_list",
|
|
418
|
+
],
|
|
419
|
+
},
|
|
420
|
+
dmPolicy: "open",
|
|
421
|
+
groupPolicy: "open",
|
|
422
|
+
instantAck: { enabled: true, text: "内容处理中,请稍候..." },
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
if (!cfg.plugins || typeof cfg.plugins !== "object") cfg.plugins = {};
|
|
426
|
+
if (!cfg.plugins.entries || typeof cfg.plugins.entries !== "object") cfg.plugins.entries = {};
|
|
427
|
+
cfg.plugins.entries[CHANNEL_ID] = { enabled: true };
|
|
428
|
+
|
|
429
|
+
if (!Array.isArray(cfg.plugins.allow)) cfg.plugins.allow = [];
|
|
430
|
+
if (!cfg.plugins.allow.includes(CHANNEL_ID)) cfg.plugins.allow.push(CHANNEL_ID);
|
|
431
|
+
|
|
432
|
+
if (!Array.isArray(cfg.bindings)) cfg.bindings = [];
|
|
433
|
+
const hasBinding = cfg.bindings.some(
|
|
434
|
+
(b) => b && b.match && b.match.channel === CHANNEL_ID,
|
|
435
|
+
);
|
|
436
|
+
if (!hasBinding) {
|
|
437
|
+
cfg.bindings.push({
|
|
438
|
+
agentId: DEFAULT_AGENT_ID,
|
|
439
|
+
match: { channel: CHANNEL_ID, accountId: "default" },
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
writeJson(OPENCLAW_JSON, cfg);
|
|
444
|
+
log.info(`已写入 channels.${CHANNEL_ID} + plugins.allow + bindings`);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function verifyInstall({ requirePluginRegistered = true } = {}) {
|
|
448
|
+
const cfg = readJsonSafe(OPENCLAW_JSON);
|
|
449
|
+
if (!cfg?.channels?.[CHANNEL_ID]?.enabled) {
|
|
450
|
+
log.error(`安装验证失败:channels.${CHANNEL_ID} 未写入或未启用`);
|
|
451
|
+
process.exit(1);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// 仅在要求插件已注册时,再跑一次 `openclaw plugins list` 兜底验证。
|
|
455
|
+
// 说明:不同 OpenClaw 版本的 list 输出格式不尽相同,我们只做"是否出现 channel id"
|
|
456
|
+
// 级别的宽松匹配,避免在少见输出上误报。
|
|
457
|
+
if (!requirePluginRegistered) return;
|
|
458
|
+
|
|
459
|
+
const list = runOpenclaw(["plugins", "list"], { capture: true });
|
|
460
|
+
if (list.status !== 0) {
|
|
461
|
+
log.warn(`openclaw plugins list 执行失败(exit=${list.status}),跳过运行时验证`);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
const output = `${list.stdout}\n${list.stderr}`;
|
|
465
|
+
if (!output.includes(CHANNEL_ID)) {
|
|
466
|
+
log.error(`安装验证失败:openclaw plugins list 输出未包含 "${CHANNEL_ID}"`);
|
|
467
|
+
log.error("这通常表示插件虽被 `plugins install` 接受,但未被网关识别。");
|
|
468
|
+
log.error("请手动执行 `openclaw plugins list` 排查,或升级 OpenClaw 后重试。");
|
|
469
|
+
process.exit(1);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// ── install 子命令 ──────────────────────────────────────────────────────────
|
|
474
|
+
|
|
475
|
+
async function cmdInstall(flags) {
|
|
476
|
+
log.step("检查 OpenClaw 环境");
|
|
477
|
+
checkOpenclawCli();
|
|
478
|
+
|
|
479
|
+
log.step("收集 WPS 开放平台凭据");
|
|
480
|
+
const appId =
|
|
481
|
+
flags.appId ||
|
|
482
|
+
env("WPS_APP_ID") ||
|
|
483
|
+
env("APP_ID") ||
|
|
484
|
+
(await prompt("请输入 WPS 开放平台 APP_ID"));
|
|
485
|
+
if (!appId) {
|
|
486
|
+
log.error("APP_ID 不能为空");
|
|
487
|
+
process.exit(1);
|
|
488
|
+
}
|
|
489
|
+
const appSecret =
|
|
490
|
+
flags.appSecret ||
|
|
491
|
+
env("WPS_APP_SECRET") ||
|
|
492
|
+
env("APP_SECRET") ||
|
|
493
|
+
(await prompt("请输入 WPS 开放平台 APP_SECRET", { secret: true }));
|
|
494
|
+
if (!appSecret) {
|
|
495
|
+
log.error("APP_SECRET 不能为空");
|
|
496
|
+
process.exit(1);
|
|
497
|
+
}
|
|
498
|
+
const baseUrl = flags.baseUrl || env("WPS_BASE_URL") || DEFAULT_BASE_URL;
|
|
499
|
+
|
|
500
|
+
log.step("预校验 WPS 开放平台凭据");
|
|
501
|
+
const skipValidate = env("WPS_SKIP_VALIDATE") === "1";
|
|
502
|
+
if (skipValidate) {
|
|
503
|
+
log.warn("WPS_SKIP_VALIDATE=1 已设置,跳过凭据预校验");
|
|
504
|
+
} else {
|
|
505
|
+
const v = await validateWpsCredentials({ appId, appSecret, baseUrl });
|
|
506
|
+
if (v.ok) {
|
|
507
|
+
log.info(`凭据校验通过(endpoint=${v.endpoint})`);
|
|
508
|
+
} else {
|
|
509
|
+
log.error(`凭据校验失败:${v.error}`);
|
|
510
|
+
log.error(`请检查 APP_ID / APP_SECRET / 网络是否可访问 ${baseUrl}`);
|
|
511
|
+
log.error("离线环境可设置 WPS_SKIP_VALIDATE=1 后重试");
|
|
512
|
+
process.exit(1);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
log.step("清理历史残留配置");
|
|
517
|
+
// `openclaw plugins uninstall` 不保证删除物理目录,直接删更可靠。
|
|
518
|
+
for (const id of ALL_IDS) {
|
|
519
|
+
const extDir = path.join(OPENCLAW_EXTENSIONS_DIR, id);
|
|
520
|
+
if (fs.existsSync(extDir)) {
|
|
521
|
+
fs.rmSync(extDir, { recursive: true, force: true });
|
|
522
|
+
log.info(`已删除扩展目录:${extDir}`);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
pruneOpenclawJson();
|
|
526
|
+
pruneOpenclawSessions();
|
|
527
|
+
runOpenclaw(["doctor", "--fix"], { silent: true });
|
|
528
|
+
|
|
529
|
+
log.step("注册插件到 OpenClaw");
|
|
530
|
+
const reg = registerPluginHybrid();
|
|
531
|
+
if (!reg.ok) {
|
|
532
|
+
log.error("插件注册失败:两种路径(npm spec / 本地目录)均被 OpenClaw 拒绝。");
|
|
533
|
+
log.error("");
|
|
534
|
+
log.error("可能原因与对策:");
|
|
535
|
+
log.error(" 1) 包未发布到 npm registry → 维护者需要先 `npm publish`,或由用户改用本地目录模式");
|
|
536
|
+
log.error(" 2) OpenClaw 静态扫描器拒绝本地目录 → 升级 OpenClaw,或联系插件维护者规避扫描启发式");
|
|
537
|
+
log.error(" 3) openclaw CLI 版本不兼容 → `openclaw --version` 查看,建议 >= 2026.3.28");
|
|
538
|
+
log.error("");
|
|
539
|
+
log.error("具体报错请看上方 [WARN] 行。");
|
|
540
|
+
if (!flags.skipPluginInstall) {
|
|
541
|
+
log.error("为避免产生半成品状态(插件未注册但 channel 已写入),本次安装中止。");
|
|
542
|
+
log.error("openclaw.json 未被修改。");
|
|
543
|
+
log.error("");
|
|
544
|
+
log.error("若你已手动把插件安装到 OpenClaw(例如开发联调),可重跑并加上:--skip-plugin-install");
|
|
545
|
+
process.exit(1);
|
|
546
|
+
}
|
|
547
|
+
log.warn("--skip-plugin-install 已设置:跳过插件注册校验,仅写入 channel 配置");
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
log.step("写入 channel 配置");
|
|
551
|
+
writeChannelConfig({ appId, appSecret, baseUrl });
|
|
552
|
+
runOpenclaw(["doctor", "--fix"], { silent: true });
|
|
553
|
+
|
|
554
|
+
log.step("验证安装");
|
|
555
|
+
verifyInstall({ requirePluginRegistered: reg.ok });
|
|
556
|
+
|
|
557
|
+
const successNote = reg.ok
|
|
558
|
+
? `${c.green}${c.bold}✅ @wps365/openclaw-wpsxiezuo 安装成功${c.reset}`
|
|
559
|
+
: `${c.yellow}${c.bold}⚠️ channel 配置已写入,但插件未被 OpenClaw 注册(--skip-plugin-install)${c.reset}`;
|
|
560
|
+
|
|
561
|
+
console.log(`
|
|
562
|
+
${successNote}
|
|
563
|
+
|
|
564
|
+
下一步:
|
|
565
|
+
1. 手动重启 OpenClaw gateway 以加载新插件:
|
|
566
|
+
${c.cyan}openclaw restart${c.reset} 或 ${c.cyan}openclaw serve${c.reset}
|
|
567
|
+
2. 在 WPS 开放平台把你的机器人 Webhook 指向本地 OpenClaw 网关
|
|
568
|
+
3. 在 WPS 客户端 @ 机器人开始对话
|
|
569
|
+
|
|
570
|
+
配置文件:${OPENCLAW_JSON}
|
|
571
|
+
插件目录:${PACKAGE_DIR}
|
|
572
|
+
`);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// ── 插件注册:混合策略(npm spec 优先,本地目录回退)─────────────────────────
|
|
576
|
+
|
|
577
|
+
function getNpmSpec() {
|
|
578
|
+
const pkg = readJsonSafe(path.join(PACKAGE_DIR, "package.json")) || {};
|
|
579
|
+
const name = pkg.name || "@wps365/openclaw-wpsxiezuo";
|
|
580
|
+
const version = pkg.version;
|
|
581
|
+
return version ? `${name}@${version}` : name;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function registerPluginHybrid() {
|
|
585
|
+
const spec = getNpmSpec();
|
|
586
|
+
log.info(`尝试:openclaw plugins install ${spec}`);
|
|
587
|
+
const bySpec = runOpenclaw(["plugins", "install", spec], { capture: true });
|
|
588
|
+
if (bySpec.status === 0) {
|
|
589
|
+
log.info(`已通过 npm spec 注册:${spec}`);
|
|
590
|
+
return { ok: true, via: "npm", spec };
|
|
591
|
+
}
|
|
592
|
+
const specErr = (bySpec.stderr || bySpec.stdout || "").trim().slice(0, 400);
|
|
593
|
+
log.warn(`npm spec 注册失败(exit=${bySpec.status}):${specErr || "无输出"}`);
|
|
594
|
+
|
|
595
|
+
// 2) 回退:本地目录(当前 npx 已拉取到 node_modules / npm 缓存的物理路径)
|
|
596
|
+
log.info(`回退:openclaw plugins install "${PACKAGE_DIR}"`);
|
|
597
|
+
const byDir = runOpenclaw(["plugins", "install", PACKAGE_DIR], { capture: true });
|
|
598
|
+
if (byDir.status === 0) {
|
|
599
|
+
log.info(`已通过本地目录注册:${PACKAGE_DIR}`);
|
|
600
|
+
return { ok: true, via: "local", path: PACKAGE_DIR };
|
|
601
|
+
}
|
|
602
|
+
const dirErr = (byDir.stderr || byDir.stdout || "").trim().slice(0, 400);
|
|
603
|
+
log.warn(`本地目录注册失败(exit=${byDir.status}):${dirErr || "无输出"}`);
|
|
604
|
+
return { ok: false, spec, specError: specErr, dirError: dirErr };
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// ── uninstall 子命令 ────────────────────────────────────────────────────────
|
|
608
|
+
|
|
609
|
+
async function cmdUninstall(flags) {
|
|
610
|
+
log.step("检查 OpenClaw 环境");
|
|
611
|
+
checkOpenclawCli();
|
|
612
|
+
|
|
613
|
+
if (!flags.yes && process.stdin.isTTY) {
|
|
614
|
+
const ans = await prompt(`确认卸载 @wps365/openclaw-wpsxiezuo 并清理配置?(y/N)`);
|
|
615
|
+
if (!/^y(es)?$/i.test(ans)) {
|
|
616
|
+
log.info("已取消");
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
log.step("清理 channels / plugins / bindings 残留");
|
|
622
|
+
// 与 cmdInstall 保持一致:先删物理扩展目录,再清配置,最后 doctor。
|
|
623
|
+
for (const id of ALL_IDS) {
|
|
624
|
+
const extDir = path.join(OPENCLAW_EXTENSIONS_DIR, id);
|
|
625
|
+
if (fs.existsSync(extDir)) {
|
|
626
|
+
fs.rmSync(extDir, { recursive: true, force: true });
|
|
627
|
+
log.info(`已删除扩展目录:${extDir}`);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
pruneOpenclawJson();
|
|
631
|
+
pruneOpenclawSessions();
|
|
632
|
+
runOpenclaw(["doctor", "--fix"], { silent: true });
|
|
633
|
+
|
|
634
|
+
log.info(`${c.green}✅ 卸载完成${c.reset}`);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// ── main ────────────────────────────────────────────────────────────────────
|
|
638
|
+
|
|
639
|
+
async function main() {
|
|
640
|
+
const args = parseArgs(process.argv.slice(2));
|
|
641
|
+
|
|
642
|
+
if (args.flags.version) {
|
|
643
|
+
const pkgPath = path.join(PACKAGE_DIR, "package.json");
|
|
644
|
+
const pkg = readJsonSafe(pkgPath);
|
|
645
|
+
console.log(pkg?.version || "unknown");
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (args.flags.help || args._.length === 0) {
|
|
650
|
+
printHelp();
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const [cmd] = args._;
|
|
655
|
+
switch (cmd) {
|
|
656
|
+
case "install":
|
|
657
|
+
await cmdInstall(args.flags);
|
|
658
|
+
return;
|
|
659
|
+
case "uninstall":
|
|
660
|
+
case "remove":
|
|
661
|
+
await cmdUninstall(args.flags);
|
|
662
|
+
return;
|
|
663
|
+
case "help":
|
|
664
|
+
printHelp();
|
|
665
|
+
return;
|
|
666
|
+
default:
|
|
667
|
+
log.error(`未知子命令:${cmd}`);
|
|
668
|
+
printHelp();
|
|
669
|
+
process.exit(1);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
main().catch((err) => {
|
|
674
|
+
log.error(err?.message || String(err));
|
|
675
|
+
if (env("DEBUG")) console.error(err);
|
|
676
|
+
process.exit(1);
|
|
677
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WPS 事件加解密与签名校验。
|
|
3
|
+
*
|
|
4
|
+
* Webhook 事件使用 HMAC-SHA256 签名 + AES-256-CBC 加密。
|
|
5
|
+
* 从 v1 monitor.ts 中抽取,对标飞书 src/channel/event-handlers.ts 的加解密层。
|
|
6
|
+
*/
|
|
7
|
+
import type { WpsEncryptedEvent } from "./event-types.js";
|
|
8
|
+
export declare function isEncryptedEvent(body: unknown): body is WpsEncryptedEvent;
|
|
9
|
+
export declare function verifySignature(params: {
|
|
10
|
+
appId: string;
|
|
11
|
+
appSecret: string;
|
|
12
|
+
topic: string;
|
|
13
|
+
nonce: string;
|
|
14
|
+
time: number;
|
|
15
|
+
encryptedData: string;
|
|
16
|
+
signature: string;
|
|
17
|
+
}): boolean;
|
|
18
|
+
export declare function decryptEventData(encryptedData: string, appSecret: string, nonce: string): string;
|
|
19
|
+
//# sourceMappingURL=event-crypto.d.ts.map
|