gewe-openclaw 2026.1.30 → 2026.2.1
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 +24 -10
- package/openclaw.plugin.json +1 -1
- package/package.json +7 -6
- package/src/accounts.ts +6 -6
- package/src/channel.ts +50 -29
- package/src/config-schema.ts +10 -2
- package/src/constants.ts +10 -0
- package/src/delivery.ts +43 -20
- package/src/download.ts +1 -1
- package/src/inbound.ts +32 -12
- package/src/monitor.ts +2 -2
- package/src/normalize.ts +3 -4
- package/src/onboarding.ts +265 -0
- package/src/policy.ts +6 -5
- package/src/send.ts +1 -1
- package/src/silk.ts +465 -0
- package/src/types.ts +7 -1
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ openclaw plugins install ./gewe-openclaw.tgz
|
|
|
34
34
|
|
|
35
35
|
## 配置
|
|
36
36
|
|
|
37
|
-
插件配置放在 `~/.openclaw/openclaw.json` 的 `channels.gewe`,并确保插件开启:
|
|
37
|
+
插件配置放在 `~/.openclaw/openclaw.json` 的 `channels.gewe-openclaw`,并确保插件开启:
|
|
38
38
|
|
|
39
39
|
```json5
|
|
40
40
|
{
|
|
@@ -44,7 +44,7 @@ openclaw plugins install ./gewe-openclaw.tgz
|
|
|
44
44
|
}
|
|
45
45
|
},
|
|
46
46
|
"channels": {
|
|
47
|
-
"gewe": {
|
|
47
|
+
"gewe-openclaw": {
|
|
48
48
|
"enabled": true,
|
|
49
49
|
"apiBaseUrl": "https://www.geweapi.com",
|
|
50
50
|
"token": "<gewe-token>",
|
|
@@ -56,7 +56,12 @@ openclaw plugins install ./gewe-openclaw.tgz
|
|
|
56
56
|
"mediaPort": 4400,
|
|
57
57
|
"mediaPath": "/gewe-media",
|
|
58
58
|
"mediaPublicUrl": "https://your-public-domain/gewe-media",
|
|
59
|
-
"allowFrom": ["wxid_xxx"]
|
|
59
|
+
"allowFrom": ["wxid_xxx"],
|
|
60
|
+
"silkAutoDownload": true,
|
|
61
|
+
"silkVersion": "latest",
|
|
62
|
+
"silkBaseUrl": "https://github.com/Wangnov/rust-silk/releases/download",
|
|
63
|
+
"silkInstallDir": "~/.openclaw/tools/rust-silk",
|
|
64
|
+
"silkAllowUnverified": false
|
|
60
65
|
}
|
|
61
66
|
}
|
|
62
67
|
}
|
|
@@ -64,8 +69,15 @@ openclaw plugins install ./gewe-openclaw.tgz
|
|
|
64
69
|
|
|
65
70
|
说明:
|
|
66
71
|
- `webhookHost/webhookPort/webhookPath`:GeWe 回调入口(需公网可达,常配合 FRP)。
|
|
67
|
-
- `
|
|
72
|
+
- `mediaPath`:本地媒体服务的路由前缀(默认 `/gewe-media`)。
|
|
73
|
+
- `mediaPublicUrl`:公网访问地址的“基础前缀”,会自动拼接媒体 ID。通常应与 `mediaPath` 对齐,例如 `mediaPath="/gewe-media"` 时,`mediaPublicUrl` 也应包含 `/gewe-media`。
|
|
68
74
|
- `allowFrom`:允许私聊触发的微信 ID(或在群里走 allowlist 规则)。
|
|
75
|
+
- `silkAutoDownload`:自动下载 `rust-silk`(默认开启;可关闭后自行配置 `voiceSilkPath` / `voiceDecodePath`)。
|
|
76
|
+
- `silkVersion`:自动下载的 `rust-silk` 版本(`latest` 会自动清理旧版本)。
|
|
77
|
+
- `silkBaseUrl`:自定义下载源(默认 GitHub Releases)。
|
|
78
|
+
- `silkInstallDir`:自定义安装目录(默认 `~/.openclaw/tools/rust-silk/<version>`)。
|
|
79
|
+
- `silkAllowUnverified`:校验文件缺失时是否允许继续(默认 `false`)。
|
|
80
|
+
- `silkSha256`:手动指定下载包 SHA256(用于私有源或校验文件缺失场景)。
|
|
69
81
|
|
|
70
82
|
> 配置变更后需重启 Gateway。
|
|
71
83
|
|
|
@@ -86,14 +98,14 @@ OpenClaw 支持外部插件目录(catalog)。放置到以下路径即可被
|
|
|
86
98
|
"name": "gewe-openclaw",
|
|
87
99
|
"openclaw": {
|
|
88
100
|
"channel": {
|
|
89
|
-
"id": "gewe",
|
|
101
|
+
"id": "gewe-openclaw",
|
|
90
102
|
"label": "GeWe",
|
|
91
103
|
"selectionLabel": "WeChat (GeWe)",
|
|
92
104
|
"detailLabel": "WeChat (GeWe)",
|
|
93
|
-
"docsPath": "/channels/gewe",
|
|
94
|
-
"docsLabel": "gewe",
|
|
105
|
+
"docsPath": "/channels/gewe-openclaw",
|
|
106
|
+
"docsLabel": "gewe-openclaw",
|
|
95
107
|
"blurb": "WeChat channel via GeWe API and webhook callbacks.",
|
|
96
|
-
"aliases": ["
|
|
108
|
+
"aliases": ["gewe-openclaw", "gewe", "wechat", "wx"],
|
|
97
109
|
"order": 72,
|
|
98
110
|
"quickstartAllowFrom": true
|
|
99
111
|
},
|
|
@@ -107,6 +119,8 @@ OpenClaw 支持外部插件目录(catalog)。放置到以下路径即可被
|
|
|
107
119
|
}
|
|
108
120
|
```
|
|
109
121
|
|
|
122
|
+
> 现在插件已支持 onboarding:选择 GeWe 通道后会提示填写 token/appId/webhook/mediaPublicUrl 等配置。
|
|
123
|
+
|
|
110
124
|
## 依赖
|
|
111
125
|
|
|
112
126
|
### npm 依赖
|
|
@@ -120,8 +134,8 @@ OpenClaw 支持外部插件目录(catalog)。放置到以下路径即可被
|
|
|
120
134
|
### 系统级工具
|
|
121
135
|
|
|
122
136
|
- `ffmpeg` / `ffprobe`(用于视频缩略图与时长)
|
|
123
|
-
- `silk
|
|
124
|
-
- `silk-decoder
|
|
137
|
+
- `rust-silk`(出站语音转 silk + 入站语音解码;支持自动下载)
|
|
138
|
+
- 或者自行安装 `silk-encoder` / `silk-decoder` 并在配置中指定路径
|
|
125
139
|
|
|
126
140
|
### 网络/服务依赖
|
|
127
141
|
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gewe-openclaw",
|
|
3
|
-
"version": "2026.1
|
|
3
|
+
"version": "2026.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenClaw GeWe channel plugin",
|
|
6
6
|
"license": "MIT",
|
|
@@ -9,17 +9,18 @@
|
|
|
9
9
|
"./index.ts"
|
|
10
10
|
],
|
|
11
11
|
"channel": {
|
|
12
|
-
"id": "gewe",
|
|
12
|
+
"id": "gewe-openclaw",
|
|
13
13
|
"label": "GeWe",
|
|
14
14
|
"selectionLabel": "WeChat (GeWe)",
|
|
15
15
|
"detailLabel": "WeChat (GeWe)",
|
|
16
|
-
"docsPath": "/channels/gewe",
|
|
17
|
-
"docsLabel": "gewe",
|
|
16
|
+
"docsPath": "/channels/gewe-openclaw",
|
|
17
|
+
"docsLabel": "gewe-openclaw",
|
|
18
18
|
"blurb": "WeChat channel via GeWe API and webhook callbacks.",
|
|
19
19
|
"aliases": [
|
|
20
|
+
"gewe-openclaw",
|
|
21
|
+
"gewe",
|
|
20
22
|
"wechat",
|
|
21
|
-
"wx"
|
|
22
|
-
"gewe"
|
|
23
|
+
"wx"
|
|
23
24
|
],
|
|
24
25
|
"order": 72,
|
|
25
26
|
"quickstartAllowFrom": true
|
package/src/accounts.ts
CHANGED
|
@@ -2,9 +2,10 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
|
|
3
3
|
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk";
|
|
4
4
|
|
|
5
|
+
import { CHANNEL_CONFIG_KEY } from "./constants.js";
|
|
5
6
|
import type { CoreConfig, GeweAccountConfig, GeweAppIdSource, GeweTokenSource } from "./types.js";
|
|
6
7
|
|
|
7
|
-
const DEFAULT_API_BASE_URL = "
|
|
8
|
+
const DEFAULT_API_BASE_URL = "https://www.geweapi.com";
|
|
8
9
|
|
|
9
10
|
export type ResolvedGeweAccount = {
|
|
10
11
|
accountId: string;
|
|
@@ -18,7 +19,7 @@ export type ResolvedGeweAccount = {
|
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
function listConfiguredAccountIds(cfg: CoreConfig): string[] {
|
|
21
|
-
const accounts = cfg.channels?.
|
|
22
|
+
const accounts = cfg.channels?.[CHANNEL_CONFIG_KEY]?.accounts;
|
|
22
23
|
if (!accounts || typeof accounts !== "object") return [];
|
|
23
24
|
const ids = new Set<string>();
|
|
24
25
|
for (const key of Object.keys(accounts)) {
|
|
@@ -41,7 +42,7 @@ export function resolveDefaultGeweAccountId(cfg: CoreConfig): string {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
function resolveAccountConfig(cfg: CoreConfig, accountId: string): GeweAccountConfig | undefined {
|
|
44
|
-
const accounts = cfg.channels?.
|
|
45
|
+
const accounts = cfg.channels?.[CHANNEL_CONFIG_KEY]?.accounts;
|
|
45
46
|
if (!accounts || typeof accounts !== "object") return undefined;
|
|
46
47
|
const direct = accounts[accountId] as GeweAccountConfig | undefined;
|
|
47
48
|
if (direct) return direct;
|
|
@@ -51,7 +52,7 @@ function resolveAccountConfig(cfg: CoreConfig, accountId: string): GeweAccountCo
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
function mergeGeweAccountConfig(cfg: CoreConfig, accountId: string): GeweAccountConfig {
|
|
54
|
-
const { accounts: _ignored, ...base } = (cfg.channels?.
|
|
55
|
+
const { accounts: _ignored, ...base } = (cfg.channels?.[CHANNEL_CONFIG_KEY] ?? {}) as GeweAccountConfig & {
|
|
55
56
|
accounts?: unknown;
|
|
56
57
|
};
|
|
57
58
|
const account = resolveAccountConfig(cfg, accountId) ?? {};
|
|
@@ -117,7 +118,7 @@ export function resolveGeweAccount(params: {
|
|
|
117
118
|
accountId?: string | null;
|
|
118
119
|
}): ResolvedGeweAccount {
|
|
119
120
|
const hasExplicitAccountId = Boolean(params.accountId?.trim());
|
|
120
|
-
const baseEnabled = params.cfg.channels?.
|
|
121
|
+
const baseEnabled = params.cfg.channels?.[CHANNEL_CONFIG_KEY]?.enabled !== false;
|
|
121
122
|
|
|
122
123
|
const resolve = (accountId: string): ResolvedGeweAccount => {
|
|
123
124
|
const merged = mergeGeweAccountConfig(params.cfg, accountId);
|
|
@@ -161,4 +162,3 @@ export function listEnabledGeweAccounts(cfg: CoreConfig): ResolvedGeweAccount[]
|
|
|
161
162
|
.map((accountId) => resolveGeweAccount({ cfg, accountId }))
|
|
162
163
|
.filter((account) => account.enabled);
|
|
163
164
|
}
|
|
164
|
-
|
package/src/channel.ts
CHANGED
|
@@ -15,6 +15,14 @@ import {
|
|
|
15
15
|
|
|
16
16
|
import { resolveGeweAccount, resolveDefaultGeweAccountId, listGeweAccountIds } from "./accounts.js";
|
|
17
17
|
import { GeweConfigSchema } from "./config-schema.js";
|
|
18
|
+
import {
|
|
19
|
+
CHANNEL_ALIASES,
|
|
20
|
+
CHANNEL_CONFIG_KEY,
|
|
21
|
+
CHANNEL_DOCS_LABEL,
|
|
22
|
+
CHANNEL_DOCS_PATH,
|
|
23
|
+
CHANNEL_ID,
|
|
24
|
+
stripChannelPrefix,
|
|
25
|
+
} from "./constants.js";
|
|
18
26
|
import { deliverGewePayload } from "./delivery.js";
|
|
19
27
|
import { monitorGeweProvider } from "./monitor.js";
|
|
20
28
|
import { looksLikeGeweTargetId, normalizeGeweMessagingTarget } from "./normalize.js";
|
|
@@ -22,16 +30,17 @@ import { resolveGeweGroupToolPolicy, resolveGeweRequireMention } from "./policy.
|
|
|
22
30
|
import { getGeweRuntime } from "./runtime.js";
|
|
23
31
|
import { sendTextGewe } from "./send.js";
|
|
24
32
|
import type { CoreConfig, ResolvedGeweAccount } from "./types.js";
|
|
33
|
+
import { geweOnboarding } from "./onboarding.js";
|
|
25
34
|
|
|
26
35
|
const meta = {
|
|
27
|
-
id:
|
|
36
|
+
id: CHANNEL_ID,
|
|
28
37
|
label: "GeWe",
|
|
29
38
|
selectionLabel: "WeChat (GeWe)",
|
|
30
39
|
detailLabel: "WeChat (GeWe)",
|
|
31
|
-
docsPath:
|
|
32
|
-
docsLabel:
|
|
40
|
+
docsPath: CHANNEL_DOCS_PATH,
|
|
41
|
+
docsLabel: CHANNEL_DOCS_LABEL,
|
|
33
42
|
blurb: "WeChat channel via GeWe API and webhook callbacks.",
|
|
34
|
-
aliases: [
|
|
43
|
+
aliases: [...CHANNEL_ALIASES],
|
|
35
44
|
order: 72,
|
|
36
45
|
quickstartAllowFrom: true,
|
|
37
46
|
};
|
|
@@ -45,11 +54,12 @@ type GeweSetupInput = ChannelSetupInput & {
|
|
|
45
54
|
};
|
|
46
55
|
|
|
47
56
|
export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
|
|
48
|
-
id:
|
|
57
|
+
id: CHANNEL_ID,
|
|
49
58
|
meta,
|
|
59
|
+
onboarding: geweOnboarding,
|
|
50
60
|
pairing: {
|
|
51
61
|
idLabel: "wechatUserId",
|
|
52
|
-
normalizeAllowEntry: (entry) => entry
|
|
62
|
+
normalizeAllowEntry: (entry) => stripChannelPrefix(entry),
|
|
53
63
|
notifyApproval: async ({ cfg, id }) => {
|
|
54
64
|
const account = resolveGeweAccount({ cfg: cfg as CoreConfig });
|
|
55
65
|
if (!account.token || !account.appId) {
|
|
@@ -70,7 +80,7 @@ export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
|
|
|
70
80
|
nativeCommands: false,
|
|
71
81
|
blockStreaming: true,
|
|
72
82
|
},
|
|
73
|
-
reload: { configPrefixes: [
|
|
83
|
+
reload: { configPrefixes: [`channels.${CHANNEL_CONFIG_KEY}`] },
|
|
74
84
|
configSchema: buildChannelConfigSchema(GeweConfigSchema),
|
|
75
85
|
config: {
|
|
76
86
|
listAccountIds: (cfg) => listGeweAccountIds(cfg as CoreConfig),
|
|
@@ -79,7 +89,7 @@ export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
|
|
|
79
89
|
setAccountEnabled: ({ cfg, accountId, enabled }) =>
|
|
80
90
|
setAccountEnabledInConfigSection({
|
|
81
91
|
cfg,
|
|
82
|
-
sectionKey:
|
|
92
|
+
sectionKey: CHANNEL_CONFIG_KEY,
|
|
83
93
|
accountId,
|
|
84
94
|
enabled,
|
|
85
95
|
allowTopLevel: true,
|
|
@@ -87,7 +97,7 @@ export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
|
|
|
87
97
|
deleteAccount: ({ cfg, accountId }) =>
|
|
88
98
|
deleteAccountFromConfigSection({
|
|
89
99
|
cfg,
|
|
90
|
-
sectionKey:
|
|
100
|
+
sectionKey: CHANNEL_CONFIG_KEY,
|
|
91
101
|
accountId,
|
|
92
102
|
clearBaseFields: ["token", "tokenFile", "appId", "appIdFile", "name"],
|
|
93
103
|
}),
|
|
@@ -108,24 +118,24 @@ export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
|
|
|
108
118
|
allowFrom
|
|
109
119
|
.map((entry) => String(entry).trim())
|
|
110
120
|
.filter(Boolean)
|
|
111
|
-
.map((entry) => entry
|
|
121
|
+
.map((entry) => stripChannelPrefix(entry)),
|
|
112
122
|
},
|
|
113
123
|
security: {
|
|
114
124
|
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
|
115
125
|
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
|
|
116
126
|
const useAccountPath = Boolean(
|
|
117
|
-
cfg.channels?.
|
|
127
|
+
cfg.channels?.[CHANNEL_CONFIG_KEY]?.accounts?.[resolvedAccountId],
|
|
118
128
|
);
|
|
119
129
|
const basePath = useAccountPath
|
|
120
|
-
? `channels.
|
|
121
|
-
:
|
|
130
|
+
? `channels.${CHANNEL_CONFIG_KEY}.accounts.${resolvedAccountId}.`
|
|
131
|
+
: `channels.${CHANNEL_CONFIG_KEY}.`;
|
|
122
132
|
return {
|
|
123
133
|
policy: account.config.dmPolicy ?? "pairing",
|
|
124
134
|
allowFrom: account.config.allowFrom ?? [],
|
|
125
135
|
policyPath: `${basePath}dmPolicy`,
|
|
126
136
|
allowFromPath: basePath,
|
|
127
|
-
approveHint: formatPairingApproveHint(
|
|
128
|
-
normalizeEntry: (raw) => raw
|
|
137
|
+
approveHint: formatPairingApproveHint(CHANNEL_ID),
|
|
138
|
+
normalizeEntry: (raw) => stripChannelPrefix(raw),
|
|
129
139
|
};
|
|
130
140
|
},
|
|
131
141
|
collectWarnings: ({ account, cfg }) => {
|
|
@@ -136,11 +146,11 @@ export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
|
|
|
136
146
|
account.config.groups && Object.keys(account.config.groups).length > 0;
|
|
137
147
|
if (groupAllowlistConfigured) {
|
|
138
148
|
return [
|
|
139
|
-
`- GeWe groups: groupPolicy="open" allows any member in allowed groups to trigger (mention-gated). Set channels.
|
|
149
|
+
`- GeWe groups: groupPolicy="open" allows any member in allowed groups to trigger (mention-gated). Set channels.${CHANNEL_CONFIG_KEY}.groupPolicy="allowlist" + channels.${CHANNEL_CONFIG_KEY}.groupAllowFrom to restrict senders.`,
|
|
140
150
|
];
|
|
141
151
|
}
|
|
142
152
|
return [
|
|
143
|
-
`- GeWe groups: groupPolicy="open" with no channels.
|
|
153
|
+
`- GeWe groups: groupPolicy="open" with no channels.${CHANNEL_CONFIG_KEY}.groups allowlist; any group can add + ping (mention-gated). Set channels.${CHANNEL_CONFIG_KEY}.groupPolicy="allowlist" + channels.${CHANNEL_CONFIG_KEY}.groupAllowFrom or configure channels.${CHANNEL_CONFIG_KEY}.groups.`,
|
|
144
154
|
];
|
|
145
155
|
},
|
|
146
156
|
},
|
|
@@ -187,7 +197,10 @@ export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
|
|
|
187
197
|
}
|
|
188
198
|
return {
|
|
189
199
|
ok: false,
|
|
190
|
-
error: missingTargetError(
|
|
200
|
+
error: missingTargetError(
|
|
201
|
+
"GeWe",
|
|
202
|
+
`<wxid|@chatroom> or channels.${CHANNEL_CONFIG_KEY}.allowFrom[0]`,
|
|
203
|
+
),
|
|
191
204
|
};
|
|
192
205
|
}
|
|
193
206
|
return { ok: true, to: normalized };
|
|
@@ -198,7 +211,10 @@ export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
|
|
|
198
211
|
}
|
|
199
212
|
return {
|
|
200
213
|
ok: false,
|
|
201
|
-
error: missingTargetError(
|
|
214
|
+
error: missingTargetError(
|
|
215
|
+
"GeWe",
|
|
216
|
+
`<wxid|@chatroom> or channels.${CHANNEL_CONFIG_KEY}.allowFrom[0]`,
|
|
217
|
+
),
|
|
202
218
|
};
|
|
203
219
|
},
|
|
204
220
|
sendPayload: async ({ payload, cfg, to, accountId }) => {
|
|
@@ -210,7 +226,7 @@ export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
|
|
|
210
226
|
toWxid: to,
|
|
211
227
|
});
|
|
212
228
|
return {
|
|
213
|
-
channel:
|
|
229
|
+
channel: CHANNEL_ID,
|
|
214
230
|
messageId: result?.messageId ?? "ok",
|
|
215
231
|
timestamp: result?.timestamp,
|
|
216
232
|
meta: { newMessageId: result?.newMessageId },
|
|
@@ -225,7 +241,7 @@ export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
|
|
|
225
241
|
toWxid: to,
|
|
226
242
|
});
|
|
227
243
|
return {
|
|
228
|
-
channel:
|
|
244
|
+
channel: CHANNEL_ID,
|
|
229
245
|
messageId: result?.messageId ?? "ok",
|
|
230
246
|
timestamp: result?.timestamp,
|
|
231
247
|
meta: { newMessageId: result?.newMessageId },
|
|
@@ -240,7 +256,7 @@ export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
|
|
|
240
256
|
toWxid: to,
|
|
241
257
|
});
|
|
242
258
|
return {
|
|
243
|
-
channel:
|
|
259
|
+
channel: CHANNEL_ID,
|
|
244
260
|
messageId: result?.messageId ?? "ok",
|
|
245
261
|
timestamp: result?.timestamp,
|
|
246
262
|
meta: { newMessageId: result?.newMessageId },
|
|
@@ -307,7 +323,9 @@ export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
|
|
|
307
323
|
},
|
|
308
324
|
logoutAccount: async ({ accountId, cfg }) => {
|
|
309
325
|
const nextCfg = { ...cfg } as OpenClawConfig;
|
|
310
|
-
const nextSection = cfg.channels?.
|
|
326
|
+
const nextSection = cfg.channels?.[CHANNEL_CONFIG_KEY]
|
|
327
|
+
? { ...(cfg.channels?.[CHANNEL_CONFIG_KEY] as Record<string, unknown>) }
|
|
328
|
+
: undefined;
|
|
311
329
|
let cleared = false;
|
|
312
330
|
let changed = false;
|
|
313
331
|
|
|
@@ -377,7 +395,7 @@ export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
|
|
|
377
395
|
if (changed) {
|
|
378
396
|
nextCfg.channels = {
|
|
379
397
|
...nextCfg.channels,
|
|
380
|
-
|
|
398
|
+
[CHANNEL_CONFIG_KEY]: nextSection,
|
|
381
399
|
};
|
|
382
400
|
}
|
|
383
401
|
}
|
|
@@ -390,7 +408,7 @@ export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
|
|
|
390
408
|
applyAccountName: ({ cfg, accountId, name }) =>
|
|
391
409
|
applyAccountNameToChannelSection({
|
|
392
410
|
cfg: cfg as OpenClawConfig,
|
|
393
|
-
channelKey:
|
|
411
|
+
channelKey: CHANNEL_CONFIG_KEY,
|
|
394
412
|
accountId,
|
|
395
413
|
name,
|
|
396
414
|
}),
|
|
@@ -411,11 +429,14 @@ export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
|
|
|
411
429
|
const setupInput = input as GeweSetupInput;
|
|
412
430
|
const namedConfig = applyAccountNameToChannelSection({
|
|
413
431
|
cfg: cfg as OpenClawConfig,
|
|
414
|
-
channelKey:
|
|
432
|
+
channelKey: CHANNEL_CONFIG_KEY,
|
|
415
433
|
accountId,
|
|
416
434
|
name: setupInput.name,
|
|
417
435
|
});
|
|
418
|
-
const section = (namedConfig.channels?.
|
|
436
|
+
const section = (namedConfig.channels?.[CHANNEL_CONFIG_KEY] ?? {}) as Record<
|
|
437
|
+
string,
|
|
438
|
+
unknown
|
|
439
|
+
>;
|
|
419
440
|
const useAccountPath = accountId !== DEFAULT_ACCOUNT_ID;
|
|
420
441
|
const base = useAccountPath
|
|
421
442
|
? (section.accounts?.[accountId] as Record<string, unknown> | undefined) ?? {}
|
|
@@ -443,7 +464,7 @@ export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
|
|
|
443
464
|
...namedConfig,
|
|
444
465
|
channels: {
|
|
445
466
|
...namedConfig.channels,
|
|
446
|
-
|
|
467
|
+
[CHANNEL_CONFIG_KEY]: nextEntry,
|
|
447
468
|
},
|
|
448
469
|
};
|
|
449
470
|
}
|
|
@@ -451,7 +472,7 @@ export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
|
|
|
451
472
|
...namedConfig,
|
|
452
473
|
channels: {
|
|
453
474
|
...namedConfig.channels,
|
|
454
|
-
|
|
475
|
+
[CHANNEL_CONFIG_KEY]: {
|
|
455
476
|
...section,
|
|
456
477
|
accounts: {
|
|
457
478
|
...(section.accounts as Record<string, unknown> | undefined),
|
package/src/config-schema.ts
CHANGED
|
@@ -49,6 +49,12 @@ export const GeweAccountSchemaBase = z
|
|
|
49
49
|
voiceDecodeArgs: z.array(z.string()).optional(),
|
|
50
50
|
voiceDecodeSampleRate: z.number().int().positive().optional(),
|
|
51
51
|
voiceDecodeOutput: z.enum(["pcm", "wav"]).optional(),
|
|
52
|
+
silkAutoDownload: z.boolean().optional(),
|
|
53
|
+
silkVersion: z.string().optional(),
|
|
54
|
+
silkBaseUrl: z.string().optional(),
|
|
55
|
+
silkSha256: z.string().optional(),
|
|
56
|
+
silkAllowUnverified: z.boolean().optional(),
|
|
57
|
+
silkInstallDir: z.string().optional(),
|
|
52
58
|
videoFfmpegPath: z.string().optional(),
|
|
53
59
|
videoFfprobePath: z.string().optional(),
|
|
54
60
|
videoThumbUrl: z.string().optional(),
|
|
@@ -81,25 +87,27 @@ export const GeweAccountSchemaBase = z
|
|
|
81
87
|
});
|
|
82
88
|
|
|
83
89
|
export const GeweAccountSchema = GeweAccountSchemaBase.superRefine((value, ctx) => {
|
|
90
|
+
const pathHint = "channels.gewe-openclaw";
|
|
84
91
|
requireOpenAllowFrom({
|
|
85
92
|
policy: value.dmPolicy,
|
|
86
93
|
allowFrom: value.allowFrom,
|
|
87
94
|
ctx,
|
|
88
95
|
path: ["allowFrom"],
|
|
89
96
|
message:
|
|
90
|
-
|
|
97
|
+
`${pathHint}.dmPolicy="open" requires ${pathHint}.allowFrom to include "*"`,
|
|
91
98
|
});
|
|
92
99
|
});
|
|
93
100
|
|
|
94
101
|
export const GeweConfigSchema = GeweAccountSchemaBase.extend({
|
|
95
102
|
accounts: z.record(z.string(), GeweAccountSchema.optional()).optional(),
|
|
96
103
|
}).superRefine((value, ctx) => {
|
|
104
|
+
const pathHint = "channels.gewe-openclaw";
|
|
97
105
|
requireOpenAllowFrom({
|
|
98
106
|
policy: value.dmPolicy,
|
|
99
107
|
allowFrom: value.allowFrom,
|
|
100
108
|
ctx,
|
|
101
109
|
path: ["allowFrom"],
|
|
102
110
|
message:
|
|
103
|
-
|
|
111
|
+
`${pathHint}.dmPolicy="open" requires ${pathHint}.allowFrom to include "*"`,
|
|
104
112
|
});
|
|
105
113
|
});
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const CHANNEL_ID = "gewe-openclaw" as const;
|
|
2
|
+
export const CHANNEL_CONFIG_KEY = "gewe-openclaw" as const;
|
|
3
|
+
export const CHANNEL_DOCS_PATH = "/channels/gewe-openclaw" as const;
|
|
4
|
+
export const CHANNEL_DOCS_LABEL = "gewe-openclaw" as const;
|
|
5
|
+
export const CHANNEL_PREFIX_REGEX = /^(gewe-openclaw|gewe|wechat|wx):/i;
|
|
6
|
+
export const CHANNEL_ALIASES = ["gewe-openclaw", "gewe", "wechat", "wx"] as const;
|
|
7
|
+
|
|
8
|
+
export function stripChannelPrefix(value: string): string {
|
|
9
|
+
return value.replace(CHANNEL_PREFIX_REGEX, "");
|
|
10
|
+
}
|
package/src/delivery.ts
CHANGED
|
@@ -5,7 +5,9 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
|
|
6
6
|
import type { OpenClawConfig, ReplyPayload } from "openclaw/plugin-sdk";
|
|
7
7
|
import { extractOriginalFilename, extensionForMime } from "openclaw/plugin-sdk";
|
|
8
|
+
import { CHANNEL_ID } from "./constants.js";
|
|
8
9
|
import { getGeweRuntime } from "./runtime.js";
|
|
10
|
+
import { ensureRustSilkBinary } from "./silk.js";
|
|
9
11
|
import {
|
|
10
12
|
sendFileGewe,
|
|
11
13
|
sendImageGewe,
|
|
@@ -97,8 +99,10 @@ function resolveMediaMaxBytes(account: ResolvedGeweAccount): number {
|
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
function resolveGeweData(payload: ReplyPayload): GeweChannelData | undefined {
|
|
100
|
-
const data = payload.channelData as
|
|
101
|
-
|
|
102
|
+
const data = payload.channelData as
|
|
103
|
+
| { "gewe-openclaw"?: GeweChannelData; gewe?: GeweChannelData }
|
|
104
|
+
| undefined;
|
|
105
|
+
return data?.[CHANNEL_ID] ?? data?.gewe;
|
|
102
106
|
}
|
|
103
107
|
|
|
104
108
|
function isSilkAudio(opts: { contentType?: string; fileName?: string }): boolean {
|
|
@@ -145,7 +149,7 @@ async function probeVideoDurationSeconds(params: {
|
|
|
145
149
|
sourcePath: string;
|
|
146
150
|
}): Promise<number | null> {
|
|
147
151
|
const core = getGeweRuntime();
|
|
148
|
-
const logger = core.logging.getChildLogger({ channel:
|
|
152
|
+
const logger = core.logging.getChildLogger({ channel: CHANNEL_ID, module: "video" });
|
|
149
153
|
const ffmpegPath = resolveVideoFfmpegPath(params.account);
|
|
150
154
|
const ffprobePath = resolveVideoFfprobePath(params.account, ffmpegPath);
|
|
151
155
|
const args = [
|
|
@@ -180,7 +184,7 @@ async function generateVideoThumbBuffer(params: {
|
|
|
180
184
|
sourcePath: string;
|
|
181
185
|
}): Promise<Buffer | null> {
|
|
182
186
|
const core = getGeweRuntime();
|
|
183
|
-
const logger = core.logging.getChildLogger({ channel:
|
|
187
|
+
const logger = core.logging.getChildLogger({ channel: CHANNEL_ID, module: "video" });
|
|
184
188
|
const ffmpegPath = resolveVideoFfmpegPath(params.account);
|
|
185
189
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-gewe-video-"));
|
|
186
190
|
const thumbPath = path.join(tmpDir, "thumb.png");
|
|
@@ -247,21 +251,40 @@ async function convertAudioToSilk(params: {
|
|
|
247
251
|
sourcePath: string;
|
|
248
252
|
}): Promise<{ buffer: Buffer; durationMs: number } | null> {
|
|
249
253
|
const core = getGeweRuntime();
|
|
250
|
-
const logger = core.logging.getChildLogger({ channel:
|
|
254
|
+
const logger = core.logging.getChildLogger({ channel: CHANNEL_ID, module: "voice" });
|
|
251
255
|
if (!params.account.config.voiceAutoConvert) return null;
|
|
252
256
|
|
|
253
257
|
const sampleRate = resolveVoiceSampleRate(params.account);
|
|
254
258
|
const ffmpegPath = params.account.config.voiceFfmpegPath?.trim() || DEFAULT_VOICE_FFMPEG;
|
|
255
|
-
const silkPath = params.account.config.voiceSilkPath?.trim() || DEFAULT_VOICE_SILK;
|
|
256
|
-
const customArgs =
|
|
257
|
-
params.account.config.voiceSilkArgs?.length ? [params.account.config.voiceSilkArgs] : [];
|
|
258
259
|
const fallbackArgs = [
|
|
259
260
|
["-i", "{input}", "-o", "{output}", "-rate", "{sampleRate}"],
|
|
260
261
|
["{input}", "{output}", "-rate", "{sampleRate}"],
|
|
261
262
|
["{input}", "{output}", "{sampleRate}"],
|
|
262
263
|
["{input}", "{output}"],
|
|
263
264
|
];
|
|
264
|
-
const
|
|
265
|
+
const rustArgs = [
|
|
266
|
+
"encode",
|
|
267
|
+
"-i",
|
|
268
|
+
"{input}",
|
|
269
|
+
"-o",
|
|
270
|
+
"{output}",
|
|
271
|
+
"--sample-rate",
|
|
272
|
+
"{sampleRate}",
|
|
273
|
+
"--tencent",
|
|
274
|
+
"--quiet",
|
|
275
|
+
];
|
|
276
|
+
const customPath = params.account.config.voiceSilkPath?.trim();
|
|
277
|
+
const customArgs =
|
|
278
|
+
params.account.config.voiceSilkArgs?.length ? [params.account.config.voiceSilkArgs] : [];
|
|
279
|
+
let silkPath = customPath || DEFAULT_VOICE_SILK;
|
|
280
|
+
let argTemplates = customArgs.length ? customArgs : fallbackArgs;
|
|
281
|
+
if (!customPath) {
|
|
282
|
+
const rustSilk = await ensureRustSilkBinary(params.account);
|
|
283
|
+
if (rustSilk) {
|
|
284
|
+
silkPath = rustSilk;
|
|
285
|
+
argTemplates = [rustArgs];
|
|
286
|
+
}
|
|
287
|
+
}
|
|
265
288
|
|
|
266
289
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-gewe-voice-"));
|
|
267
290
|
const pcmPath = path.join(tmpDir, "voice.pcm");
|
|
@@ -441,7 +464,7 @@ async function resolveLinkThumbUrl(params: {
|
|
|
441
464
|
thumbUrl?: string;
|
|
442
465
|
}): Promise<string> {
|
|
443
466
|
const core = getGeweRuntime();
|
|
444
|
-
const logger = core.logging.getChildLogger({ channel:
|
|
467
|
+
const logger = core.logging.getChildLogger({ channel: CHANNEL_ID, module: "thumb" });
|
|
445
468
|
const fallbackBuffer = await fs.readFile(DEFAULT_LINK_THUMB_PATH);
|
|
446
469
|
const fallbackUrl = await stageThumbBuffer({
|
|
447
470
|
account: params.account,
|
|
@@ -591,7 +614,7 @@ export async function deliverGewePayload(params: {
|
|
|
591
614
|
thumbUrl,
|
|
592
615
|
});
|
|
593
616
|
core.channel.activity.record({
|
|
594
|
-
channel:
|
|
617
|
+
channel: CHANNEL_ID,
|
|
595
618
|
accountId: account.accountId,
|
|
596
619
|
direction: "outbound",
|
|
597
620
|
});
|
|
@@ -625,7 +648,7 @@ export async function deliverGewePayload(params: {
|
|
|
625
648
|
voiceDuration: declaredDuration,
|
|
626
649
|
});
|
|
627
650
|
core.channel.activity.record({
|
|
628
|
-
channel:
|
|
651
|
+
channel: CHANNEL_ID,
|
|
629
652
|
accountId: account.accountId,
|
|
630
653
|
direction: "outbound",
|
|
631
654
|
});
|
|
@@ -657,7 +680,7 @@ export async function deliverGewePayload(params: {
|
|
|
657
680
|
voiceDuration,
|
|
658
681
|
});
|
|
659
682
|
core.channel.activity.record({
|
|
660
|
-
channel:
|
|
683
|
+
channel: CHANNEL_ID,
|
|
661
684
|
accountId: account.accountId,
|
|
662
685
|
direction: "outbound",
|
|
663
686
|
});
|
|
@@ -674,7 +697,7 @@ export async function deliverGewePayload(params: {
|
|
|
674
697
|
imgUrl: staged.publicUrl,
|
|
675
698
|
});
|
|
676
699
|
core.channel.activity.record({
|
|
677
|
-
channel:
|
|
700
|
+
channel: CHANNEL_ID,
|
|
678
701
|
accountId: account.accountId,
|
|
679
702
|
direction: "outbound",
|
|
680
703
|
});
|
|
@@ -683,7 +706,7 @@ export async function deliverGewePayload(params: {
|
|
|
683
706
|
}
|
|
684
707
|
|
|
685
708
|
if (!forceFile && kind === "video") {
|
|
686
|
-
const logger = core.logging.getChildLogger({ channel:
|
|
709
|
+
const logger = core.logging.getChildLogger({ channel: CHANNEL_ID, module: "video" });
|
|
687
710
|
const video = geweData?.video;
|
|
688
711
|
let thumbUrl = video?.thumbUrl;
|
|
689
712
|
const fallbackThumbUrl = account.config.videoThumbUrl?.trim() || undefined;
|
|
@@ -755,7 +778,7 @@ export async function deliverGewePayload(params: {
|
|
|
755
778
|
videoDuration: Math.floor(videoDuration),
|
|
756
779
|
});
|
|
757
780
|
core.channel.activity.record({
|
|
758
|
-
channel:
|
|
781
|
+
channel: CHANNEL_ID,
|
|
759
782
|
accountId: account.accountId,
|
|
760
783
|
direction: "outbound",
|
|
761
784
|
});
|
|
@@ -780,7 +803,7 @@ export async function deliverGewePayload(params: {
|
|
|
780
803
|
videoDuration: Math.floor(videoDuration),
|
|
781
804
|
});
|
|
782
805
|
core.channel.activity.record({
|
|
783
|
-
channel:
|
|
806
|
+
channel: CHANNEL_ID,
|
|
784
807
|
accountId: account.accountId,
|
|
785
808
|
direction: "outbound",
|
|
786
809
|
});
|
|
@@ -803,7 +826,7 @@ export async function deliverGewePayload(params: {
|
|
|
803
826
|
fileName: fallbackName,
|
|
804
827
|
});
|
|
805
828
|
core.channel.activity.record({
|
|
806
|
-
channel:
|
|
829
|
+
channel: CHANNEL_ID,
|
|
807
830
|
accountId: account.accountId,
|
|
808
831
|
direction: "outbound",
|
|
809
832
|
});
|
|
@@ -814,7 +837,7 @@ export async function deliverGewePayload(params: {
|
|
|
814
837
|
if (trimmedText) {
|
|
815
838
|
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
|
816
839
|
cfg,
|
|
817
|
-
channel:
|
|
840
|
+
channel: CHANNEL_ID,
|
|
818
841
|
accountId: account.accountId,
|
|
819
842
|
});
|
|
820
843
|
const content = core.channel.text.convertMarkdownTables(trimmedText, tableMode);
|
|
@@ -825,7 +848,7 @@ export async function deliverGewePayload(params: {
|
|
|
825
848
|
ats: geweData?.ats,
|
|
826
849
|
});
|
|
827
850
|
core.channel.activity.record({
|
|
828
|
-
channel:
|
|
851
|
+
channel: CHANNEL_ID,
|
|
829
852
|
accountId: account.accountId,
|
|
830
853
|
direction: "outbound",
|
|
831
854
|
});
|
package/src/download.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { ResolvedGeweAccount } from "./types.js";
|
|
|
4
4
|
type DownloadResult = { fileUrl: string };
|
|
5
5
|
|
|
6
6
|
function resolveBaseUrl(account: ResolvedGeweAccount): string {
|
|
7
|
-
return account.config.apiBaseUrl?.trim() || "
|
|
7
|
+
return account.config.apiBaseUrl?.trim() || "https://www.geweapi.com";
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export async function downloadGeweImage(params: {
|