gewe-openclaw 2026.3.13 → 2026.3.23
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/README.md +455 -3
- package/index.ts +39 -1
- package/package.json +12 -1
- package/skills/gewe-agent-tools/SKILL.md +113 -0
- package/skills/gewe-channel-rules/SKILL.md +7 -0
- package/src/accounts.ts +51 -5
- package/src/api-tools.ts +1264 -0
- package/src/api.ts +37 -2
- package/src/binary-command.ts +65 -0
- package/src/channel-actions.ts +536 -0
- package/src/channel-allowlist.ts +150 -0
- package/src/channel-directory.ts +419 -0
- package/src/channel-status.ts +186 -0
- package/src/channel.ts +155 -58
- package/src/config-edit.ts +94 -0
- package/src/config-schema.ts +78 -3
- package/src/contacts-api.ts +113 -0
- package/src/delivery.ts +502 -62
- package/src/directory-cache.ts +164 -0
- package/src/gewe-account-api.ts +27 -0
- package/src/group-allowlist-tool.ts +242 -0
- package/src/group-binding-tool.ts +154 -0
- package/src/group-binding.ts +405 -0
- package/src/groups-api.ts +146 -0
- package/src/inbound-batch.ts +5 -2
- package/src/inbound.ts +248 -41
- package/src/media-server.ts +73 -93
- package/src/moments-api.ts +138 -0
- package/src/monitor.ts +81 -24
- package/src/onboarding.ts +9 -4
- package/src/openclaw-compat.ts +1070 -0
- package/src/pairing-store.ts +478 -0
- package/src/personal-api.ts +45 -0
- package/src/policy.ts +130 -22
- package/src/quote-context-cache.ts +97 -0
- package/src/reply-options.ts +101 -2
- package/src/s3.ts +1 -1
- package/src/send.ts +235 -16
- package/src/setup-wizard-types.ts +162 -0
- package/src/setup-wizard.ts +464 -0
- package/src/silk.ts +2 -1
- package/src/state-paths.ts +55 -14
- package/src/types.ts +66 -7
- package/src/xml.ts +158 -0
package/src/api.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
import type { ResolvedGeweAccount } from "./types.js";
|
|
2
|
+
|
|
1
3
|
export type GeweApiResponse<T> = {
|
|
2
4
|
ret: number;
|
|
3
5
|
msg: string;
|
|
4
6
|
data?: T;
|
|
5
7
|
};
|
|
6
8
|
|
|
9
|
+
const GEWE_LARGE_ID_FIELD_PATTERN =
|
|
10
|
+
/("(?:msgId|newMsgId|MsgId|NewMsgId)"\s*:\s*)(-?\d{16,})(?=[,\}])/g;
|
|
11
|
+
|
|
7
12
|
export function buildGeweUrl(baseUrl: string, path: string): string {
|
|
8
13
|
const base = baseUrl.replace(/\/$/, "");
|
|
9
14
|
const suffix = path.startsWith("/") ? path : `/${path}`;
|
|
@@ -18,6 +23,11 @@ async function readResponseText(res: Response): Promise<string> {
|
|
|
18
23
|
}
|
|
19
24
|
}
|
|
20
25
|
|
|
26
|
+
export function parseGeweJsonText<T>(text: string): T {
|
|
27
|
+
const normalized = text.replace(GEWE_LARGE_ID_FIELD_PATTERN, '$1"$2"');
|
|
28
|
+
return JSON.parse(normalized) as T;
|
|
29
|
+
}
|
|
30
|
+
|
|
21
31
|
export async function postGeweJson<T>(params: {
|
|
22
32
|
baseUrl: string;
|
|
23
33
|
token: string;
|
|
@@ -40,8 +50,14 @@ export async function postGeweJson<T>(params: {
|
|
|
40
50
|
throw new Error(`GeWe API request failed (${res.status})${detail}`);
|
|
41
51
|
}
|
|
42
52
|
|
|
43
|
-
const
|
|
44
|
-
|
|
53
|
+
const text = await readResponseText(res);
|
|
54
|
+
try {
|
|
55
|
+
return parseGeweJsonText<GeweApiResponse<T>>(text);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
const detail = text ? `: ${text}` : "";
|
|
58
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
59
|
+
throw new Error(`GeWe API returned invalid JSON (${reason})${detail}`);
|
|
60
|
+
}
|
|
45
61
|
}
|
|
46
62
|
|
|
47
63
|
export function assertGeweOk<T>(resp: GeweApiResponse<T>, context: string): T | undefined {
|
|
@@ -51,3 +67,22 @@ export function assertGeweOk<T>(resp: GeweApiResponse<T>, context: string): T |
|
|
|
51
67
|
}
|
|
52
68
|
return resp.data;
|
|
53
69
|
}
|
|
70
|
+
|
|
71
|
+
export async function postGeweAccountJson<T>(params: {
|
|
72
|
+
account: ResolvedGeweAccount;
|
|
73
|
+
path: string;
|
|
74
|
+
body?: Record<string, unknown>;
|
|
75
|
+
context?: string;
|
|
76
|
+
}): Promise<T | undefined> {
|
|
77
|
+
const baseUrl = params.account.config.apiBaseUrl?.trim() || "https://www.geweapi.com";
|
|
78
|
+
const resp = await postGeweJson<T>({
|
|
79
|
+
baseUrl,
|
|
80
|
+
token: params.account.token,
|
|
81
|
+
path: params.path,
|
|
82
|
+
body: {
|
|
83
|
+
appId: params.account.appId,
|
|
84
|
+
...(params.body ?? {}),
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
return assertGeweOk(resp, params.context ?? params.path);
|
|
88
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export type BinaryCommandResult = {
|
|
4
|
+
stdout: Buffer;
|
|
5
|
+
stderr: string;
|
|
6
|
+
code: number | null;
|
|
7
|
+
signal: NodeJS.Signals | null;
|
|
8
|
+
timedOut: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export async function runBinaryCommand(params: {
|
|
12
|
+
argv: string[];
|
|
13
|
+
timeoutMs: number;
|
|
14
|
+
input?: Buffer;
|
|
15
|
+
}): Promise<BinaryCommandResult> {
|
|
16
|
+
const [command, ...args] = params.argv;
|
|
17
|
+
if (!command) {
|
|
18
|
+
throw new Error("missing command");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return await new Promise((resolve, reject) => {
|
|
22
|
+
const child = spawn(command, args, {
|
|
23
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
24
|
+
});
|
|
25
|
+
const stdoutChunks: Buffer[] = [];
|
|
26
|
+
const stderrChunks: Buffer[] = [];
|
|
27
|
+
let settled = false;
|
|
28
|
+
let timedOut = false;
|
|
29
|
+
const timer = setTimeout(() => {
|
|
30
|
+
timedOut = true;
|
|
31
|
+
child.kill("SIGKILL");
|
|
32
|
+
}, params.timeoutMs);
|
|
33
|
+
|
|
34
|
+
child.stdout?.on("data", (chunk: Buffer | string) => {
|
|
35
|
+
stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
36
|
+
});
|
|
37
|
+
child.stderr?.on("data", (chunk: Buffer | string) => {
|
|
38
|
+
stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
39
|
+
});
|
|
40
|
+
child.on("error", (err) => {
|
|
41
|
+
if (settled) return;
|
|
42
|
+
settled = true;
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
reject(err);
|
|
45
|
+
});
|
|
46
|
+
child.on("close", (code, signal) => {
|
|
47
|
+
if (settled) return;
|
|
48
|
+
settled = true;
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
resolve({
|
|
51
|
+
stdout: Buffer.concat(stdoutChunks),
|
|
52
|
+
stderr: Buffer.concat(stderrChunks).toString("utf8"),
|
|
53
|
+
code,
|
|
54
|
+
signal,
|
|
55
|
+
timedOut,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (params.input && params.input.length) {
|
|
60
|
+
child.stdin?.end(params.input);
|
|
61
|
+
} else {
|
|
62
|
+
child.stdin?.end();
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
import { Type, type TSchema } from "@sinclair/typebox";
|
|
2
|
+
import type {
|
|
3
|
+
ChannelMessageActionAdapter,
|
|
4
|
+
ChannelMessageActionName,
|
|
5
|
+
ChannelMessageToolSchemaContribution,
|
|
6
|
+
} from "openclaw/plugin-sdk/channel-runtime";
|
|
7
|
+
|
|
8
|
+
import { listEnabledGeweAccounts, resolveGeweAccount } from "./accounts.js";
|
|
9
|
+
import { deliverGewePayload } from "./delivery.js";
|
|
10
|
+
import { normalizeGeweMessagingTarget } from "./normalize.js";
|
|
11
|
+
import type { OpenClawConfig, ReplyPayload } from "./openclaw-compat.js";
|
|
12
|
+
import type { CoreConfig } from "./types.js";
|
|
13
|
+
|
|
14
|
+
type JsonResultDetails = Record<string, unknown>;
|
|
15
|
+
type JsonResult = {
|
|
16
|
+
content: Array<{ type: "text"; text: string }>;
|
|
17
|
+
details: JsonResultDetails;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type GeweActionScopedPayload = {
|
|
21
|
+
quoteReply?: {
|
|
22
|
+
svrid?: string;
|
|
23
|
+
atWxid?: string;
|
|
24
|
+
partialText?: {
|
|
25
|
+
text?: string;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
emoji?: {
|
|
29
|
+
emojiMd5?: string;
|
|
30
|
+
emojiSize?: number;
|
|
31
|
+
};
|
|
32
|
+
nameCard?: {
|
|
33
|
+
nickName?: string;
|
|
34
|
+
nameCardWxid?: string;
|
|
35
|
+
};
|
|
36
|
+
miniApp?: {
|
|
37
|
+
miniAppId?: string;
|
|
38
|
+
displayName?: string;
|
|
39
|
+
pagePath?: string;
|
|
40
|
+
coverImgUrl?: string;
|
|
41
|
+
title?: string;
|
|
42
|
+
userName?: string;
|
|
43
|
+
};
|
|
44
|
+
forward?: {
|
|
45
|
+
kind?: "image" | "video" | "file" | "link" | "miniApp";
|
|
46
|
+
xml?: string;
|
|
47
|
+
coverImgUrl?: string;
|
|
48
|
+
};
|
|
49
|
+
revoke?: {
|
|
50
|
+
msgId?: string;
|
|
51
|
+
newMsgId?: string;
|
|
52
|
+
createTime?: string;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const SUPPORTED_ACTIONS = new Set<ChannelMessageActionName>(["send", "reply", "unsend"]);
|
|
57
|
+
|
|
58
|
+
function jsonResult(details: JsonResultDetails): JsonResult {
|
|
59
|
+
return {
|
|
60
|
+
content: [{ type: "text", text: JSON.stringify(details, null, 2) }],
|
|
61
|
+
details,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function extractToolSend(args: Record<string, unknown>, expectedAction = "sendMessage") {
|
|
66
|
+
const action = typeof args.action === "string" ? args.action.trim() : "";
|
|
67
|
+
if (action !== expectedAction) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const to = typeof args.to === "string" ? args.to.trim() : "";
|
|
71
|
+
if (!to) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const accountId = typeof args.accountId === "string" ? args.accountId.trim() : undefined;
|
|
75
|
+
const threadIdRaw =
|
|
76
|
+
typeof args.threadId === "string"
|
|
77
|
+
? args.threadId.trim()
|
|
78
|
+
: typeof args.threadId === "number"
|
|
79
|
+
? String(args.threadId)
|
|
80
|
+
: "";
|
|
81
|
+
return {
|
|
82
|
+
to,
|
|
83
|
+
...(accountId ? { accountId } : {}),
|
|
84
|
+
...(threadIdRaw ? { threadId: threadIdRaw } : {}),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function optionalString(description: string): TSchema {
|
|
89
|
+
return Type.Optional(Type.String({ description }));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function createGeweSchemaContribution(): ChannelMessageToolSchemaContribution[] {
|
|
93
|
+
return [
|
|
94
|
+
{
|
|
95
|
+
properties: {
|
|
96
|
+
messageId: optionalString("GeWe reply/unsend 目标消息 ID。reply 未显式提供时会回退到当前消息。"),
|
|
97
|
+
newMessageId: optionalString("GeWe 撤回所需的 new message id。"),
|
|
98
|
+
createTime: optionalString("GeWe 撤回所需的消息创建时间。"),
|
|
99
|
+
gewe: Type.Optional(
|
|
100
|
+
Type.Object(
|
|
101
|
+
{
|
|
102
|
+
quote: Type.Optional(
|
|
103
|
+
Type.Object(
|
|
104
|
+
{
|
|
105
|
+
partialText: optionalString("GeWe 部分引用文本。"),
|
|
106
|
+
atWxid: optionalString("GeWe 引用回复里 @ 的目标 wxid。"),
|
|
107
|
+
atSender: Type.Optional(
|
|
108
|
+
Type.Boolean({
|
|
109
|
+
description: "在当前群会话里自动 @ 当前可信发言人。",
|
|
110
|
+
}),
|
|
111
|
+
),
|
|
112
|
+
},
|
|
113
|
+
{ additionalProperties: false },
|
|
114
|
+
),
|
|
115
|
+
),
|
|
116
|
+
emoji: Type.Optional(
|
|
117
|
+
Type.Object(
|
|
118
|
+
{
|
|
119
|
+
emojiMd5: optionalString("GeWe emoji md5。"),
|
|
120
|
+
emojiSize: Type.Optional(Type.Number()),
|
|
121
|
+
},
|
|
122
|
+
{ additionalProperties: false },
|
|
123
|
+
),
|
|
124
|
+
),
|
|
125
|
+
nameCard: Type.Optional(
|
|
126
|
+
Type.Object(
|
|
127
|
+
{
|
|
128
|
+
nickName: optionalString("名片昵称。"),
|
|
129
|
+
nameCardWxid: optionalString("名片 wxid。"),
|
|
130
|
+
},
|
|
131
|
+
{ additionalProperties: false },
|
|
132
|
+
),
|
|
133
|
+
),
|
|
134
|
+
miniApp: Type.Optional(
|
|
135
|
+
Type.Object(
|
|
136
|
+
{
|
|
137
|
+
miniAppId: optionalString("小程序 appId。"),
|
|
138
|
+
displayName: optionalString("小程序展示名。"),
|
|
139
|
+
pagePath: optionalString("小程序页面路径。"),
|
|
140
|
+
coverImgUrl: optionalString("小程序封面图。"),
|
|
141
|
+
title: optionalString("小程序标题。"),
|
|
142
|
+
userName: optionalString("小程序原始 userName。"),
|
|
143
|
+
},
|
|
144
|
+
{ additionalProperties: false },
|
|
145
|
+
),
|
|
146
|
+
),
|
|
147
|
+
forward: Type.Optional(
|
|
148
|
+
Type.Object(
|
|
149
|
+
{
|
|
150
|
+
kind: Type.Optional(
|
|
151
|
+
Type.Union([
|
|
152
|
+
Type.Literal("image"),
|
|
153
|
+
Type.Literal("video"),
|
|
154
|
+
Type.Literal("file"),
|
|
155
|
+
Type.Literal("link"),
|
|
156
|
+
Type.Literal("miniApp"),
|
|
157
|
+
]),
|
|
158
|
+
),
|
|
159
|
+
xml: optionalString("GeWe 转发消息 XML。"),
|
|
160
|
+
coverImgUrl: optionalString("转发 miniApp 时需要的封面图。"),
|
|
161
|
+
},
|
|
162
|
+
{ additionalProperties: false },
|
|
163
|
+
),
|
|
164
|
+
),
|
|
165
|
+
},
|
|
166
|
+
{ additionalProperties: false },
|
|
167
|
+
),
|
|
168
|
+
),
|
|
169
|
+
},
|
|
170
|
+
visibility: "all-configured",
|
|
171
|
+
},
|
|
172
|
+
];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function readStringParam(
|
|
176
|
+
params: Record<string, unknown>,
|
|
177
|
+
key: string,
|
|
178
|
+
opts?: { allowEmpty?: boolean },
|
|
179
|
+
): string | undefined {
|
|
180
|
+
const raw = params[key];
|
|
181
|
+
if (typeof raw !== "string") {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
const value = opts?.allowEmpty ? raw : raw.trim();
|
|
185
|
+
return opts?.allowEmpty ? value : value || undefined;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function readGeweObject(params: Record<string, unknown>): Record<string, unknown> | undefined {
|
|
189
|
+
const value = params.gewe;
|
|
190
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
return value as Record<string, unknown>;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function readScopedObject(
|
|
197
|
+
parent: Record<string, unknown> | undefined,
|
|
198
|
+
key: string,
|
|
199
|
+
): Record<string, unknown> | undefined {
|
|
200
|
+
const value = parent?.[key];
|
|
201
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
return value as Record<string, unknown>;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function resolveCurrentTarget(raw: string | undefined | null): string | undefined {
|
|
208
|
+
if (!raw) {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
return normalizeGeweMessagingTarget(raw) ?? undefined;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function resolveTargetFromAction(params: {
|
|
215
|
+
args: Record<string, unknown>;
|
|
216
|
+
currentChannelId?: string | null;
|
|
217
|
+
required: boolean;
|
|
218
|
+
}): string | undefined {
|
|
219
|
+
const explicit =
|
|
220
|
+
readStringParam(params.args, "to") ??
|
|
221
|
+
readStringParam(params.args, "target") ??
|
|
222
|
+
readStringParam(params.args, "chatId");
|
|
223
|
+
const target = resolveCurrentTarget(explicit ?? params.currentChannelId);
|
|
224
|
+
if (!target && params.required) {
|
|
225
|
+
throw new Error("GeWe action requires a target conversation.");
|
|
226
|
+
}
|
|
227
|
+
return target;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function resolveReplyMessageId(params: {
|
|
231
|
+
args: Record<string, unknown>;
|
|
232
|
+
currentMessageId?: string | number | null;
|
|
233
|
+
}): string | undefined {
|
|
234
|
+
const explicit =
|
|
235
|
+
readStringParam(params.args, "replyTo") ??
|
|
236
|
+
readStringParam(params.args, "messageId") ??
|
|
237
|
+
(typeof params.currentMessageId === "number"
|
|
238
|
+
? String(params.currentMessageId)
|
|
239
|
+
: params.currentMessageId?.trim() || undefined);
|
|
240
|
+
return explicit?.trim() || undefined;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function resolveTextPayload(params: Record<string, unknown>): string | undefined {
|
|
244
|
+
const message = readStringParam(params, "message", { allowEmpty: true });
|
|
245
|
+
const text = readStringParam(params, "text", { allowEmpty: true });
|
|
246
|
+
const caption = readStringParam(params, "caption", { allowEmpty: true });
|
|
247
|
+
const chosen = message ?? text ?? caption;
|
|
248
|
+
if (typeof chosen !== "string") {
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
return chosen;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function resolveMediaPayload(params: Record<string, unknown>): string | undefined {
|
|
255
|
+
return (
|
|
256
|
+
readStringParam(params, "media") ??
|
|
257
|
+
readStringParam(params, "path") ??
|
|
258
|
+
readStringParam(params, "filePath")
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function buildScopedPayloadData(params: {
|
|
263
|
+
args: Record<string, unknown>;
|
|
264
|
+
replyToId?: string;
|
|
265
|
+
requesterSenderId?: string | null;
|
|
266
|
+
isGroupTarget: boolean;
|
|
267
|
+
}): GeweActionScopedPayload | undefined {
|
|
268
|
+
const gewe = readGeweObject(params.args);
|
|
269
|
+
if (!gewe && !params.replyToId) {
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const quote = readScopedObject(gewe, "quote");
|
|
274
|
+
const emoji = readScopedObject(gewe, "emoji");
|
|
275
|
+
const nameCard = readScopedObject(gewe, "nameCard");
|
|
276
|
+
const miniApp = readScopedObject(gewe, "miniApp");
|
|
277
|
+
const forward = readScopedObject(gewe, "forward");
|
|
278
|
+
|
|
279
|
+
const scoped: GeweActionScopedPayload = {};
|
|
280
|
+
|
|
281
|
+
if (params.replyToId || quote) {
|
|
282
|
+
const partialText = readStringParam(quote ?? {}, "partialText", { allowEmpty: true });
|
|
283
|
+
let atWxid = readStringParam(quote ?? {}, "atWxid");
|
|
284
|
+
const atSender = (quote?.atSender as boolean | undefined) === true;
|
|
285
|
+
if (atSender) {
|
|
286
|
+
if (!params.isGroupTarget || !params.requesterSenderId?.trim()) {
|
|
287
|
+
throw new Error("GeWe gewe.quote.atSender requires a current group message sender.");
|
|
288
|
+
}
|
|
289
|
+
atWxid = params.requesterSenderId.trim();
|
|
290
|
+
}
|
|
291
|
+
scoped.quoteReply = {
|
|
292
|
+
svrid: params.replyToId,
|
|
293
|
+
...(atWxid ? { atWxid } : {}),
|
|
294
|
+
...(partialText != null ? { partialText: { text: partialText } } : {}),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (emoji) {
|
|
299
|
+
const emojiMd5 = readStringParam(emoji, "emojiMd5");
|
|
300
|
+
const emojiSizeRaw = emoji.emojiSize;
|
|
301
|
+
if (emojiMd5 && typeof emojiSizeRaw === "number") {
|
|
302
|
+
scoped.emoji = {
|
|
303
|
+
emojiMd5,
|
|
304
|
+
emojiSize: emojiSizeRaw,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (nameCard) {
|
|
310
|
+
const nickName = readStringParam(nameCard, "nickName");
|
|
311
|
+
const nameCardWxid = readStringParam(nameCard, "nameCardWxid");
|
|
312
|
+
if (nickName && nameCardWxid) {
|
|
313
|
+
scoped.nameCard = {
|
|
314
|
+
nickName,
|
|
315
|
+
nameCardWxid,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (miniApp) {
|
|
321
|
+
const miniAppId = readStringParam(miniApp, "miniAppId");
|
|
322
|
+
const displayName = readStringParam(miniApp, "displayName");
|
|
323
|
+
const pagePath = readStringParam(miniApp, "pagePath");
|
|
324
|
+
const coverImgUrl = readStringParam(miniApp, "coverImgUrl");
|
|
325
|
+
const title = readStringParam(miniApp, "title");
|
|
326
|
+
const userName = readStringParam(miniApp, "userName");
|
|
327
|
+
if (miniAppId && displayName && pagePath && coverImgUrl && title && userName) {
|
|
328
|
+
scoped.miniApp = {
|
|
329
|
+
miniAppId,
|
|
330
|
+
displayName,
|
|
331
|
+
pagePath,
|
|
332
|
+
coverImgUrl,
|
|
333
|
+
title,
|
|
334
|
+
userName,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (forward) {
|
|
340
|
+
const kind = forward.kind;
|
|
341
|
+
const xml = readStringParam(forward, "xml", { allowEmpty: true });
|
|
342
|
+
const coverImgUrl = readStringParam(forward, "coverImgUrl");
|
|
343
|
+
if (
|
|
344
|
+
xml &&
|
|
345
|
+
(kind === "image" ||
|
|
346
|
+
kind === "video" ||
|
|
347
|
+
kind === "file" ||
|
|
348
|
+
kind === "link" ||
|
|
349
|
+
kind === "miniApp")
|
|
350
|
+
) {
|
|
351
|
+
scoped.forward = {
|
|
352
|
+
kind,
|
|
353
|
+
xml,
|
|
354
|
+
...(coverImgUrl ? { coverImgUrl } : {}),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return Object.keys(scoped).length > 0 ? scoped : undefined;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function buildReplyPayload(params: {
|
|
363
|
+
args: Record<string, unknown>;
|
|
364
|
+
replyToId?: string;
|
|
365
|
+
requesterSenderId?: string | null;
|
|
366
|
+
isGroupTarget: boolean;
|
|
367
|
+
}): ReplyPayload {
|
|
368
|
+
const text = resolveTextPayload(params.args);
|
|
369
|
+
const mediaUrl = resolveMediaPayload(params.args);
|
|
370
|
+
const asVoice = params.args.asVoice === true;
|
|
371
|
+
const scoped = buildScopedPayloadData(params);
|
|
372
|
+
|
|
373
|
+
const payload: ReplyPayload = {
|
|
374
|
+
...(typeof text === "string" ? { text } : {}),
|
|
375
|
+
...(mediaUrl ? { mediaUrl } : {}),
|
|
376
|
+
...(params.replyToId ? { replyToId: params.replyToId } : {}),
|
|
377
|
+
...(asVoice ? { audioAsVoice: true } : {}),
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
if (scoped) {
|
|
381
|
+
payload.channelData = {
|
|
382
|
+
"gewe-openclaw": scoped,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const hasContent =
|
|
387
|
+
Boolean(text?.trim()) ||
|
|
388
|
+
Boolean(mediaUrl) ||
|
|
389
|
+
Boolean(scoped?.quoteReply) ||
|
|
390
|
+
Boolean(scoped?.emoji) ||
|
|
391
|
+
Boolean(scoped?.nameCard) ||
|
|
392
|
+
Boolean(scoped?.miniApp) ||
|
|
393
|
+
Boolean(scoped?.forward);
|
|
394
|
+
if (!hasContent) {
|
|
395
|
+
throw new Error("GeWe action requires message, media, or a gewe payload.");
|
|
396
|
+
}
|
|
397
|
+
return payload;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function resolveConfiguredAccount(params: {
|
|
401
|
+
cfg: OpenClawConfig;
|
|
402
|
+
accountId?: string | null;
|
|
403
|
+
}) {
|
|
404
|
+
return resolveGeweAccount({
|
|
405
|
+
cfg: params.cfg as CoreConfig,
|
|
406
|
+
accountId: params.accountId,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async function executeSendLikeAction(params: Parameters<
|
|
411
|
+
NonNullable<ChannelMessageActionAdapter["handleAction"]>
|
|
412
|
+
>[0] & {
|
|
413
|
+
replyToId?: string;
|
|
414
|
+
target: string;
|
|
415
|
+
}): Promise<JsonResult> {
|
|
416
|
+
const account = resolveConfiguredAccount({
|
|
417
|
+
cfg: params.cfg,
|
|
418
|
+
accountId: params.accountId,
|
|
419
|
+
});
|
|
420
|
+
const payload = buildReplyPayload({
|
|
421
|
+
args: params.params,
|
|
422
|
+
replyToId: params.replyToId,
|
|
423
|
+
requesterSenderId: params.requesterSenderId,
|
|
424
|
+
isGroupTarget: params.target.endsWith("@chatroom"),
|
|
425
|
+
});
|
|
426
|
+
const result = await deliverGewePayload({
|
|
427
|
+
payload,
|
|
428
|
+
account,
|
|
429
|
+
cfg: params.cfg,
|
|
430
|
+
toWxid: params.target,
|
|
431
|
+
});
|
|
432
|
+
return jsonResult({
|
|
433
|
+
ok: true,
|
|
434
|
+
action: params.action,
|
|
435
|
+
to: params.target,
|
|
436
|
+
messageId: result?.messageId ?? null,
|
|
437
|
+
newMessageId: result?.newMessageId ?? null,
|
|
438
|
+
timestamp: result?.timestamp ?? null,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export const geweMessageActions: ChannelMessageActionAdapter = {
|
|
443
|
+
describeMessageTool: ({ cfg }) => {
|
|
444
|
+
const accounts = listEnabledGeweAccounts(cfg as CoreConfig).filter(
|
|
445
|
+
(account) => account.token.trim() && account.appId.trim(),
|
|
446
|
+
);
|
|
447
|
+
if (accounts.length === 0) {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
return {
|
|
451
|
+
actions: ["send", "reply", "unsend"],
|
|
452
|
+
schema: createGeweSchemaContribution(),
|
|
453
|
+
};
|
|
454
|
+
},
|
|
455
|
+
supportsAction: ({ action }) => SUPPORTED_ACTIONS.has(action),
|
|
456
|
+
extractToolSend: ({ args }) => extractToolSend(args, "sendMessage"),
|
|
457
|
+
handleAction: async (ctx) => {
|
|
458
|
+
if (ctx.action === "send") {
|
|
459
|
+
const target = resolveTargetFromAction({
|
|
460
|
+
args: ctx.params,
|
|
461
|
+
currentChannelId: ctx.toolContext?.currentChannelId,
|
|
462
|
+
required: true,
|
|
463
|
+
});
|
|
464
|
+
return await executeSendLikeAction({
|
|
465
|
+
...ctx,
|
|
466
|
+
target: target!,
|
|
467
|
+
replyToId: readStringParam(ctx.params, "replyTo"),
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (ctx.action === "reply") {
|
|
472
|
+
const target = resolveTargetFromAction({
|
|
473
|
+
args: ctx.params,
|
|
474
|
+
currentChannelId: ctx.toolContext?.currentChannelId,
|
|
475
|
+
required: true,
|
|
476
|
+
});
|
|
477
|
+
const replyToId = resolveReplyMessageId({
|
|
478
|
+
args: ctx.params,
|
|
479
|
+
currentMessageId: ctx.toolContext?.currentMessageId,
|
|
480
|
+
});
|
|
481
|
+
if (!replyToId) {
|
|
482
|
+
throw new Error("GeWe reply requires replyTo/messageId or a current inbound message.");
|
|
483
|
+
}
|
|
484
|
+
return await executeSendLikeAction({
|
|
485
|
+
...ctx,
|
|
486
|
+
target: target!,
|
|
487
|
+
replyToId,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (ctx.action === "unsend") {
|
|
492
|
+
const target = resolveTargetFromAction({
|
|
493
|
+
args: ctx.params,
|
|
494
|
+
currentChannelId: ctx.toolContext?.currentChannelId,
|
|
495
|
+
required: true,
|
|
496
|
+
});
|
|
497
|
+
const messageId = readStringParam(ctx.params, "messageId");
|
|
498
|
+
const newMessageId =
|
|
499
|
+
readStringParam(ctx.params, "newMessageId") ?? readStringParam(ctx.params, "newMsgId");
|
|
500
|
+
const createTime = readStringParam(ctx.params, "createTime");
|
|
501
|
+
if (!messageId || !newMessageId || !createTime) {
|
|
502
|
+
throw new Error("GeWe unsend requires messageId, newMessageId, and createTime.");
|
|
503
|
+
}
|
|
504
|
+
const account = resolveConfiguredAccount({
|
|
505
|
+
cfg: ctx.cfg,
|
|
506
|
+
accountId: ctx.accountId,
|
|
507
|
+
});
|
|
508
|
+
const result = await deliverGewePayload({
|
|
509
|
+
payload: {
|
|
510
|
+
channelData: {
|
|
511
|
+
"gewe-openclaw": {
|
|
512
|
+
revoke: {
|
|
513
|
+
msgId: messageId,
|
|
514
|
+
newMsgId: newMessageId,
|
|
515
|
+
createTime,
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
},
|
|
519
|
+
},
|
|
520
|
+
account,
|
|
521
|
+
cfg: ctx.cfg,
|
|
522
|
+
toWxid: target!,
|
|
523
|
+
});
|
|
524
|
+
return jsonResult({
|
|
525
|
+
ok: true,
|
|
526
|
+
action: ctx.action,
|
|
527
|
+
to: target,
|
|
528
|
+
messageId: result?.messageId ?? null,
|
|
529
|
+
newMessageId: result?.newMessageId ?? null,
|
|
530
|
+
timestamp: result?.timestamp ?? null,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
throw new Error(`Unsupported GeWe action: ${ctx.action}`);
|
|
535
|
+
},
|
|
536
|
+
};
|