@wps365/openclaw-wpsxiezuo 1.11.7 → 1.11.8-beta.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.
Files changed (56) hide show
  1. package/dist/channel/event-crypto.js +35 -0
  2. package/dist/channel/event-handlers.js +206 -0
  3. package/dist/channel/event-types.js +6 -0
  4. package/dist/channel/inbound-context-cache.js +27 -0
  5. package/dist/channel/index.js +8 -0
  6. package/dist/channel/plugin.js +948 -0
  7. package/dist/channel/register-tool-bridge.js +15 -0
  8. package/dist/channel/sdk-client-options.js +17 -0
  9. package/dist/channel/sdk-helpers.js +54 -0
  10. package/dist/channel/sdk-provider.js +71 -0
  11. package/dist/channel/types.js +8 -0
  12. package/dist/channel/wps-rich-text-walk.js +288 -0
  13. package/dist/core/config.js +239 -0
  14. package/dist/core/delegated-scope-refresh.js +75 -0
  15. package/dist/core/errors.js +79 -0
  16. package/dist/core/index.js +14 -0
  17. package/dist/core/lazy-client-store.js +111 -0
  18. package/dist/core/media-utils.js +53 -0
  19. package/dist/core/reaction.js +51 -0
  20. package/dist/core/scope-checker.js +56 -0
  21. package/dist/core/scope-persist.js +57 -0
  22. package/dist/core/token-persist.js +119 -0
  23. package/dist/core/token-store.js +112 -0
  24. package/dist/core/tool-scopes.js +19 -0
  25. package/dist/core/user-token-store.js +259 -0
  26. package/dist/core/wps-client.js +122 -0
  27. package/dist/messaging/handler.js +264 -0
  28. package/dist/messaging/inbound/content-parser.js +526 -0
  29. package/dist/messaging/inbound/handler.js +717 -0
  30. package/dist/messaging/inbound/index.js +3 -0
  31. package/dist/messaging/inbound/media-prefetch.js +257 -0
  32. package/dist/messaging/inbound/pairing-allow-store.js +47 -0
  33. package/dist/messaging/index.js +2 -0
  34. package/dist/messaging/outbound.js +72 -0
  35. package/dist/messaging/synthetic-message.js +41 -0
  36. package/dist/session/idempotency.js +24 -0
  37. package/dist/session/index.js +4 -0
  38. package/dist/session/resolver.js +134 -0
  39. package/dist/session/types.js +13 -0
  40. package/dist/tools/oapi/airpage.js +772 -0
  41. package/dist/tools/oapi/airsheet.js +672 -0
  42. package/dist/tools/oapi/auth-card-cooldown.js +81 -0
  43. package/dist/tools/oapi/calendar.js +1537 -0
  44. package/dist/tools/oapi/chat.js +321 -0
  45. package/dist/tools/oapi/cloud-doc.js +1211 -0
  46. package/dist/tools/oapi/dbsheet.js +669 -0
  47. package/dist/tools/oapi/delegated-auth.js +158 -0
  48. package/dist/tools/oapi/index.js +88 -0
  49. package/dist/tools/oapi/media.js +326 -0
  50. package/dist/tools/oapi/meeting-room.js +534 -0
  51. package/dist/tools/oapi/messaging.js +378 -0
  52. package/dist/tools/oapi/todo.js +805 -0
  53. package/dist/tools/oapi/tool-helpers.js +96 -0
  54. package/dist/tools/oapi/user.js +111 -0
  55. package/dist/tools/oauth/index.js +426 -0
  56. package/package.json +100 -100
@@ -0,0 +1,35 @@
1
+ /**
2
+ * WPS 事件加解密与签名校验。
3
+ *
4
+ * Webhook 事件使用 HMAC-SHA256 签名 + AES-256-CBC 加密。
5
+ * 从 v1 monitor.ts 中抽取,对标飞书 src/channel/event-handlers.ts 的加解密层。
6
+ */
7
+ import * as crypto from "node:crypto";
8
+ export function isEncryptedEvent(body) {
9
+ if (!body || typeof body !== "object")
10
+ return false;
11
+ const b = body;
12
+ return (typeof b.encrypted_data === "string" &&
13
+ typeof b.nonce === "string" &&
14
+ typeof b.signature === "string" &&
15
+ typeof b.topic === "string" &&
16
+ typeof b.time === "number");
17
+ }
18
+ export function verifySignature(params) {
19
+ const content = `${params.appId}:${params.topic}:${params.nonce}:${params.time}:${params.encryptedData}`;
20
+ const hmac = crypto.createHmac("sha256", params.appSecret);
21
+ hmac.update(content);
22
+ const expected = hmac.digest("base64url").replace(/=+$/, "");
23
+ return expected === params.signature;
24
+ }
25
+ export function decryptEventData(encryptedData, appSecret, nonce) {
26
+ const cipherHex = crypto.createHash("md5").update(appSecret).digest("hex");
27
+ const key = Buffer.from(cipherHex, "utf-8");
28
+ const iv = Buffer.from(nonce, "utf-8").subarray(0, 16);
29
+ const encrypted = Buffer.from(encryptedData, "base64");
30
+ const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv);
31
+ let decrypted = decipher.update(encrypted);
32
+ decrypted = Buffer.concat([decrypted, decipher.final()]);
33
+ return decrypted.toString("utf-8");
34
+ }
35
+ //# sourceMappingURL=event-crypto.js.map
@@ -0,0 +1,206 @@
1
+ /**
2
+ * WPS 入站事件处理 — Webhook HTTP + SDK 长连接双模式。
3
+ *
4
+ * 对标飞书 src/channel/event-handlers.ts:
5
+ * - Webhook: Express POST → challenge / 加解密 / 分发
6
+ * - SDK: open-event-sdk 映射为统一 WpsEventPayload
7
+ *
8
+ * 不包含业务逻辑(session / routing / reply),仅做事件基础设施。
9
+ */
10
+ import { MESSAGE_TOPIC } from "./event-types.js";
11
+ import { isEncryptedEvent, verifySignature, decryptEventData } from "./event-crypto.js";
12
+ // ── 公共工具 ──────────────────────────────────────────────────
13
+ export function shouldSkipNonMessageTopic(topic) {
14
+ return typeof topic === "string" && topic.trim() !== "" && topic !== MESSAGE_TOPIC;
15
+ }
16
+ function toRecord(value) {
17
+ return value && typeof value === "object" ? value : {};
18
+ }
19
+ function firstString(...values) {
20
+ for (const v of values) {
21
+ if (typeof v === "string" && v.trim())
22
+ return v.trim();
23
+ if (typeof v === "number" && Number.isFinite(v))
24
+ return String(v);
25
+ }
26
+ return "";
27
+ }
28
+ export function buildMessageContent(msg) {
29
+ const c = msg.content;
30
+ if (!c || typeof c !== "object")
31
+ return {};
32
+ const raw = c;
33
+ const nestedText = raw.text;
34
+ if (nestedText && typeof nestedText === "object" && nestedText !== null && "content" in nestedText) {
35
+ const cVal = nestedText.content;
36
+ const textPreview = typeof cVal === "string"
37
+ ? cVal.trim()
38
+ : typeof cVal === "number" && Number.isFinite(cVal)
39
+ ? String(cVal)
40
+ : "";
41
+ // WPS 常在 rich_text 旁挂空的 text.content;若此处无条件 return raw,会跳过下面的 rich_text 抽取
42
+ if (textPreview !== "") {
43
+ return raw;
44
+ }
45
+ }
46
+ if (typeof raw.text === "string") {
47
+ return { text: { content: raw.text, type: "plain" } };
48
+ }
49
+ const image = raw.image;
50
+ if (image && typeof image === "object") {
51
+ const imgFileId = typeof image.file_id === "string" ? image.file_id : "";
52
+ const imgStorageKey = typeof image.storage_key === "string" ? image.storage_key : "";
53
+ const imgKey = typeof image.key === "string" ? image.key : "";
54
+ const resolvedSk = imgFileId || imgStorageKey || imgKey;
55
+ if (resolvedSk) {
56
+ return { image: { ...image, storage_key: resolvedSk } };
57
+ }
58
+ return raw;
59
+ }
60
+ const file = raw.file;
61
+ if (file && typeof file === "object") {
62
+ const fileType = typeof file.type === "string" ? file.type : "";
63
+ const cloud = file.cloud;
64
+ if ((fileType === "cloud" || cloud) && cloud && typeof cloud === "object") {
65
+ return { file: { type: "cloud", cloud } };
66
+ }
67
+ const fileId = file.file_id;
68
+ if (fileId) {
69
+ return { file: { type: "local", local: { storage_key: fileId, name: file.name ?? "file" } } };
70
+ }
71
+ const local = file.local;
72
+ if (local && typeof local === "object") {
73
+ return { file: { type: "local", local } };
74
+ }
75
+ }
76
+ const richText = raw.rich_text;
77
+ if (richText && typeof richText === "object") {
78
+ return raw;
79
+ }
80
+ return raw;
81
+ }
82
+ function mapMentions(input) {
83
+ if (!Array.isArray(input))
84
+ return undefined;
85
+ const mapped = input
86
+ .map((item) => toRecord(item))
87
+ .map((r) => {
88
+ const identity = toRecord(r.identity);
89
+ const identityMapped = {
90
+ id: typeof identity.id === "string" ? identity.id : undefined,
91
+ app_id: typeof identity.app_id === "string" ? identity.app_id : undefined,
92
+ name: typeof identity.name === "string" ? identity.name : undefined,
93
+ type: typeof identity.type === "string" ? identity.type : undefined,
94
+ company_id: typeof identity.company_id === "string" ? identity.company_id : undefined,
95
+ };
96
+ const hasIdentity = Object.values(identityMapped).some((v) => v !== undefined);
97
+ return {
98
+ type: typeof r.type === "string" ? r.type : undefined,
99
+ id: typeof r.id === "string" ? r.id : undefined,
100
+ user_id: typeof r.user_id === "string" ? r.user_id : undefined,
101
+ userId: typeof r.userId === "string" ? r.userId : undefined,
102
+ offset: typeof r.offset === "number" ? r.offset : undefined,
103
+ length: typeof r.length === "number" ? r.length : undefined,
104
+ ...(hasIdentity ? { identity: identityMapped } : {}),
105
+ };
106
+ })
107
+ .filter((m) => Boolean(m.id || m.user_id || m.userId || m.type || m.identity));
108
+ return mapped.length > 0 ? mapped : undefined;
109
+ }
110
+ function mapReplyTo(input) {
111
+ const record = toRecord(input);
112
+ const sender = toRecord(record.sender);
113
+ const senderId = typeof record.sender_id === "string" ? record.sender_id : undefined;
114
+ const senderInnerId = typeof sender.id === "string" ? sender.id : undefined;
115
+ if (!senderId && !senderInnerId)
116
+ return undefined;
117
+ return {
118
+ sender_id: senderId,
119
+ sender: senderInnerId ? { id: senderInnerId } : undefined,
120
+ };
121
+ }
122
+ /** 将 open-event-sdk 消息事件转为统一 WpsEventPayload */
123
+ export function mapSdkMessageToPayload(data) {
124
+ const rawData = toRecord(data);
125
+ const rawMsg = toRecord(data.message);
126
+ const sendMs = data.send_time < 1e12 ? Math.floor(data.send_time * 1000) : data.send_time;
127
+ const chatId = String(data.chat.id);
128
+ const messageId = String(data.message.id);
129
+ const senderId = String(data.sender.id);
130
+ return {
131
+ topic: typeof rawData.topic === "string" ? rawData.topic : MESSAGE_TOPIC,
132
+ event_id: firstString(rawData.event_id, rawData.id, messageId) || undefined,
133
+ company_id: firstString(rawData.company_id, rawData.companyId, rawData.corp_id) || undefined,
134
+ chat: { id: chatId, type: data.chat.type },
135
+ message: {
136
+ id: messageId,
137
+ type: data.message.type,
138
+ content: buildMessageContent(data.message),
139
+ sender: { id: senderId, type: data.sender.type },
140
+ mentions: mapMentions(rawMsg.mentions),
141
+ reply_to: mapReplyTo(rawMsg.reply_to),
142
+ },
143
+ quote_msg_id: firstString(rawData.quote_msg_id, rawData.quote_message_id, typeof rawMsg.quote_msg_id === "string" ? rawMsg.quote_msg_id : undefined) || undefined,
144
+ sender: {
145
+ id: senderId,
146
+ type: data.sender.type,
147
+ name: data.sender.extended_attribute?.name,
148
+ },
149
+ send_time: sendMs,
150
+ };
151
+ }
152
+ /**
153
+ * 处理单个 Webhook POST body:
154
+ * - challenge 验证 → 返回 { challenge }
155
+ * - 加密事件 → 签名校验 + 解密
156
+ * - 明文事件 → 直接透传
157
+ */
158
+ export function processWebhookBody(body, creds, enableEncryption, log) {
159
+ const record = toRecord(body);
160
+ if (typeof record.challenge === "string") {
161
+ log?.info?.("WPS webhook challenge verification");
162
+ return { challenge: record.challenge };
163
+ }
164
+ if (enableEncryption && isEncryptedEvent(body)) {
165
+ const enc = body;
166
+ if (enc.topic !== MESSAGE_TOPIC) {
167
+ log?.info?.(`WPS webhook skip non-message topic=${enc.topic}`);
168
+ return { ok: false, error: "skip", status: 200 };
169
+ }
170
+ const valid = verifySignature({
171
+ appId: creds.appId,
172
+ appSecret: creds.appSecret,
173
+ topic: enc.topic,
174
+ nonce: enc.nonce,
175
+ time: enc.time,
176
+ encryptedData: enc.encrypted_data,
177
+ signature: enc.signature,
178
+ });
179
+ if (!valid) {
180
+ log?.error?.("WPS signature verification failed");
181
+ return { ok: false, error: "Invalid signature", status: 403 };
182
+ }
183
+ try {
184
+ const decrypted = decryptEventData(enc.encrypted_data, creds.appSecret, enc.nonce);
185
+ const payload = JSON.parse(decrypted);
186
+ payload.topic = enc.topic;
187
+ if (!payload.event_id)
188
+ payload.event_id = payload.message?.id;
189
+ log?.info?.(`WPS decrypted event: topic=${enc.topic}, operation=${enc.operation}`);
190
+ return { ok: true, payload };
191
+ }
192
+ catch (err) {
193
+ log?.error?.("WPS decryption failed:", err);
194
+ return { ok: false, error: "Decryption failed", status: 400 };
195
+ }
196
+ }
197
+ const payload = body;
198
+ if (shouldSkipNonMessageTopic(payload.topic)) {
199
+ log?.info?.(`WPS webhook skip non-message topic=${String(payload.topic)}`);
200
+ return { ok: false, error: "skip", status: 200 };
201
+ }
202
+ if (!payload.event_id)
203
+ payload.event_id = payload.message?.id;
204
+ return { ok: true, payload };
205
+ }
206
+ //# sourceMappingURL=event-handlers.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * WPS 入站事件类型定义。
3
+ * 对标飞书 src/channel/event-types.ts。
4
+ */
5
+ export const MESSAGE_TOPIC = "kso.app_chat.message";
6
+ //# sourceMappingURL=event-types.js.map
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Per-account 最近入站上下文缓存(纯内存)。
3
+ *
4
+ * 在 handleInbound 阶段写入,在 handleAction 阶段读取作为 fallback,
5
+ * 作为防御层:当 agent 意外传入 AK(应用 ID)作为 to 时自动修正。
6
+ *
7
+ * 正常路径下 OriginatingTo 已设为正确的 user:/chat: 目标,
8
+ * 此缓存仅在极端场景下被触发。
9
+ *
10
+ * 独立模块,避免 handler.ts ↔ plugin.ts 循环依赖。
11
+ */
12
+ const lastInboundCtxMap = new Map();
13
+ const INBOUND_CTX_TTL_MS = 30 * 60 * 1000;
14
+ export function setLastInboundContext(accountId, ctx) {
15
+ lastInboundCtxMap.set(accountId, { ...ctx, ts: Date.now() });
16
+ }
17
+ export function getLastInboundContext(accountId) {
18
+ const entry = lastInboundCtxMap.get(accountId);
19
+ if (!entry)
20
+ return undefined;
21
+ if (Date.now() - entry.ts > INBOUND_CTX_TTL_MS) {
22
+ lastInboundCtxMap.delete(accountId);
23
+ return undefined;
24
+ }
25
+ return entry;
26
+ }
27
+ //# sourceMappingURL=inbound-context-cache.js.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Channel module exports
3
+ */
4
+ export * from "./event-types.js";
5
+ export * from "./event-crypto.js";
6
+ export * from "./event-handlers.js";
7
+ export * from "./sdk-provider.js";
8
+ //# sourceMappingURL=index.js.map