gewe-openclaw 2026.1.31 → 2026.2.2
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 +19 -4
- package/package.json +1 -1
- package/src/accounts.ts +1 -1
- package/src/channel.ts +2 -0
- package/src/config-schema.ts +6 -0
- package/src/delivery.ts +25 -5
- package/src/download.ts +1 -1
- package/src/inbound.ts +21 -2
- package/src/monitor.ts +2 -2
- package/src/onboarding.ts +265 -0
- package/src/send.ts +1 -1
- package/src/silk.ts +465 -0
- package/src/types.ts +6 -0
package/README.md
CHANGED
|
@@ -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,16 @@ 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
|
+
- `voiceAutoConvert`:自动将音频转为 silk(默认开启;设为 `false` 可关闭)。
|
|
76
|
+
- `silkAutoDownload`:自动下载 `rust-silk`(默认开启;可关闭后自行配置 `voiceSilkPath` / `voiceDecodePath`)。
|
|
77
|
+
- `silkVersion`:自动下载的 `rust-silk` 版本(`latest` 会自动清理旧版本)。
|
|
78
|
+
- `silkBaseUrl`:自定义下载源(默认 GitHub Releases)。
|
|
79
|
+
- `silkInstallDir`:自定义安装目录(默认 `~/.openclaw/tools/rust-silk/<version>`)。
|
|
80
|
+
- `silkAllowUnverified`:校验文件缺失时是否允许继续(默认 `false`)。
|
|
81
|
+
- `silkSha256`:手动指定下载包 SHA256(用于私有源或校验文件缺失场景)。
|
|
69
82
|
|
|
70
83
|
> 配置变更后需重启 Gateway。
|
|
71
84
|
|
|
@@ -107,6 +120,8 @@ OpenClaw 支持外部插件目录(catalog)。放置到以下路径即可被
|
|
|
107
120
|
}
|
|
108
121
|
```
|
|
109
122
|
|
|
123
|
+
> 现在插件已支持 onboarding:选择 GeWe 通道后会提示填写 token/appId/webhook/mediaPublicUrl 等配置。
|
|
124
|
+
|
|
110
125
|
## 依赖
|
|
111
126
|
|
|
112
127
|
### npm 依赖
|
|
@@ -120,8 +135,8 @@ OpenClaw 支持外部插件目录(catalog)。放置到以下路径即可被
|
|
|
120
135
|
### 系统级工具
|
|
121
136
|
|
|
122
137
|
- `ffmpeg` / `ffprobe`(用于视频缩略图与时长)
|
|
123
|
-
- `silk
|
|
124
|
-
- `silk-decoder
|
|
138
|
+
- `rust-silk`(出站语音转 silk + 入站语音解码;支持自动下载)
|
|
139
|
+
- 或者自行安装 `silk-encoder` / `silk-decoder` 并在配置中指定路径
|
|
125
140
|
|
|
126
141
|
### 网络/服务依赖
|
|
127
142
|
|
package/package.json
CHANGED
package/src/accounts.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk";
|
|
|
5
5
|
import { CHANNEL_CONFIG_KEY } from "./constants.js";
|
|
6
6
|
import type { CoreConfig, GeweAccountConfig, GeweAppIdSource, GeweTokenSource } from "./types.js";
|
|
7
7
|
|
|
8
|
-
const DEFAULT_API_BASE_URL = "
|
|
8
|
+
const DEFAULT_API_BASE_URL = "https://www.geweapi.com";
|
|
9
9
|
|
|
10
10
|
export type ResolvedGeweAccount = {
|
|
11
11
|
accountId: string;
|
package/src/channel.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { resolveGeweGroupToolPolicy, resolveGeweRequireMention } from "./policy.
|
|
|
30
30
|
import { getGeweRuntime } from "./runtime.js";
|
|
31
31
|
import { sendTextGewe } from "./send.js";
|
|
32
32
|
import type { CoreConfig, ResolvedGeweAccount } from "./types.js";
|
|
33
|
+
import { geweOnboarding } from "./onboarding.js";
|
|
33
34
|
|
|
34
35
|
const meta = {
|
|
35
36
|
id: CHANNEL_ID,
|
|
@@ -55,6 +56,7 @@ type GeweSetupInput = ChannelSetupInput & {
|
|
|
55
56
|
export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
|
|
56
57
|
id: CHANNEL_ID,
|
|
57
58
|
meta,
|
|
59
|
+
onboarding: geweOnboarding,
|
|
58
60
|
pairing: {
|
|
59
61
|
idLabel: "wechatUserId",
|
|
60
62
|
normalizeAllowEntry: (entry) => stripChannelPrefix(entry),
|
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(),
|
package/src/delivery.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { OpenClawConfig, ReplyPayload } from "openclaw/plugin-sdk";
|
|
|
7
7
|
import { extractOriginalFilename, extensionForMime } from "openclaw/plugin-sdk";
|
|
8
8
|
import { CHANNEL_ID } from "./constants.js";
|
|
9
9
|
import { getGeweRuntime } from "./runtime.js";
|
|
10
|
+
import { ensureRustSilkBinary } from "./silk.js";
|
|
10
11
|
import {
|
|
11
12
|
sendFileGewe,
|
|
12
13
|
sendImageGewe,
|
|
@@ -251,20 +252,39 @@ async function convertAudioToSilk(params: {
|
|
|
251
252
|
}): Promise<{ buffer: Buffer; durationMs: number } | null> {
|
|
252
253
|
const core = getGeweRuntime();
|
|
253
254
|
const logger = core.logging.getChildLogger({ channel: CHANNEL_ID, module: "voice" });
|
|
254
|
-
if (
|
|
255
|
+
if (params.account.config.voiceAutoConvert === false) return null;
|
|
255
256
|
|
|
256
257
|
const sampleRate = resolveVoiceSampleRate(params.account);
|
|
257
258
|
const ffmpegPath = params.account.config.voiceFfmpegPath?.trim() || DEFAULT_VOICE_FFMPEG;
|
|
258
|
-
const silkPath = params.account.config.voiceSilkPath?.trim() || DEFAULT_VOICE_SILK;
|
|
259
|
-
const customArgs =
|
|
260
|
-
params.account.config.voiceSilkArgs?.length ? [params.account.config.voiceSilkArgs] : [];
|
|
261
259
|
const fallbackArgs = [
|
|
262
260
|
["-i", "{input}", "-o", "{output}", "-rate", "{sampleRate}"],
|
|
263
261
|
["{input}", "{output}", "-rate", "{sampleRate}"],
|
|
264
262
|
["{input}", "{output}", "{sampleRate}"],
|
|
265
263
|
["{input}", "{output}"],
|
|
266
264
|
];
|
|
267
|
-
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
|
+
}
|
|
268
288
|
|
|
269
289
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-gewe-voice-"));
|
|
270
290
|
const pcmPath = path.join(tmpDir, "voice.pcm");
|
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: {
|
package/src/inbound.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { GeweDownloadQueue } from "./download-queue.js";
|
|
|
9
9
|
import { downloadGeweFile, downloadGeweImage, downloadGeweVideo, downloadGeweVoice } from "./download.js";
|
|
10
10
|
import { deliverGewePayload } from "./delivery.js";
|
|
11
11
|
import { getGeweRuntime } from "./runtime.js";
|
|
12
|
+
import { ensureRustSilkBinary } from "./silk.js";
|
|
12
13
|
import {
|
|
13
14
|
normalizeGeweAllowlist,
|
|
14
15
|
resolveGeweAllowlistMatch,
|
|
@@ -116,10 +117,28 @@ async function decodeSilkVoice(params: {
|
|
|
116
117
|
["{input}", "-o", "{output}"],
|
|
117
118
|
["-i", "{input}", "{output}"],
|
|
118
119
|
];
|
|
119
|
-
const
|
|
120
|
+
const rustArgs = [
|
|
121
|
+
"decode",
|
|
122
|
+
"-i",
|
|
123
|
+
"{input}",
|
|
124
|
+
"-o",
|
|
125
|
+
"{output}",
|
|
126
|
+
"--sample-rate",
|
|
127
|
+
"{sampleRate}",
|
|
128
|
+
"--quiet",
|
|
129
|
+
];
|
|
130
|
+
if (decodeOutput === "wav") rustArgs.push("--wav");
|
|
131
|
+
const rustSilk = customPath ? null : await ensureRustSilkBinary(params.account);
|
|
132
|
+
const argTemplates = customArgs.length
|
|
133
|
+
? customArgs
|
|
134
|
+
: rustSilk
|
|
135
|
+
? [rustArgs]
|
|
136
|
+
: fallbackArgs;
|
|
120
137
|
const candidates = customPath
|
|
121
138
|
? [customPath]
|
|
122
|
-
:
|
|
139
|
+
: rustSilk
|
|
140
|
+
? [rustSilk]
|
|
141
|
+
: ["silk-decoder", "silk-v3-decoder", "decoder"];
|
|
123
142
|
|
|
124
143
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gewe-voice-in-"));
|
|
125
144
|
const silkPath = path.join(tmpDir, "voice.silk");
|
package/src/monitor.ts
CHANGED
|
@@ -15,9 +15,9 @@ import type {
|
|
|
15
15
|
ResolvedGeweAccount,
|
|
16
16
|
} from "./types.js";
|
|
17
17
|
|
|
18
|
-
const DEFAULT_WEBHOOK_PORT =
|
|
18
|
+
const DEFAULT_WEBHOOK_PORT = 4399;
|
|
19
19
|
const DEFAULT_WEBHOOK_HOST = "0.0.0.0";
|
|
20
|
-
const DEFAULT_WEBHOOK_PATH = "/
|
|
20
|
+
const DEFAULT_WEBHOOK_PATH = "/webhook";
|
|
21
21
|
const HEALTH_PATH = "/healthz";
|
|
22
22
|
const DEDUPE_TTL_MS = 12 * 60 * 60 * 1000;
|
|
23
23
|
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import type { ChannelPlugin, OpenClawConfig, WizardPrompter } from "openclaw/plugin-sdk";
|
|
2
|
+
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk";
|
|
3
|
+
|
|
4
|
+
import type { CoreConfig, GeweAccountConfig, ResolvedGeweAccount } from "./types.js";
|
|
5
|
+
import { resolveGeweAccount, resolveDefaultGeweAccountId, listGeweAccountIds } from "./accounts.js";
|
|
6
|
+
import { CHANNEL_CONFIG_KEY, CHANNEL_ID, stripChannelPrefix } from "./constants.js";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_WEBHOOK_HOST = "0.0.0.0";
|
|
9
|
+
const DEFAULT_WEBHOOK_PORT = 4399;
|
|
10
|
+
const DEFAULT_WEBHOOK_PATH = "/webhook";
|
|
11
|
+
const DEFAULT_MEDIA_HOST = "0.0.0.0";
|
|
12
|
+
const DEFAULT_MEDIA_PORT = 4400;
|
|
13
|
+
const DEFAULT_MEDIA_PATH = "/gewe-media";
|
|
14
|
+
const DEFAULT_API_BASE_URL = "https://www.geweapi.com";
|
|
15
|
+
|
|
16
|
+
type GeweOnboardingAdapter = NonNullable<
|
|
17
|
+
ChannelPlugin<ResolvedGeweAccount>["onboarding"]
|
|
18
|
+
>;
|
|
19
|
+
|
|
20
|
+
type AccountSelection = {
|
|
21
|
+
accountId: string;
|
|
22
|
+
label: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function listAccountChoices(cfg: OpenClawConfig): AccountSelection[] {
|
|
26
|
+
const ids = listGeweAccountIds(cfg as CoreConfig);
|
|
27
|
+
return ids.map((accountId) => ({
|
|
28
|
+
accountId,
|
|
29
|
+
label: accountId === DEFAULT_ACCOUNT_ID ? "default (primary)" : accountId,
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function promptAccountId(params: {
|
|
34
|
+
cfg: OpenClawConfig;
|
|
35
|
+
prompter: WizardPrompter;
|
|
36
|
+
currentId?: string;
|
|
37
|
+
}): Promise<string> {
|
|
38
|
+
const choices = listAccountChoices(params.cfg);
|
|
39
|
+
const defaultId = resolveDefaultGeweAccountId(params.cfg as CoreConfig);
|
|
40
|
+
const initial = params.currentId?.trim() || defaultId || DEFAULT_ACCOUNT_ID;
|
|
41
|
+
const selection = await params.prompter.select({
|
|
42
|
+
message: "GeWe account",
|
|
43
|
+
options: [
|
|
44
|
+
...choices.map((item) => ({ value: item.accountId, label: item.label })),
|
|
45
|
+
{ value: "__new__", label: "Add a new account" },
|
|
46
|
+
],
|
|
47
|
+
initialValue: initial,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (selection !== "__new__") {
|
|
51
|
+
return normalizeAccountId(selection) ?? DEFAULT_ACCOUNT_ID;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const entered = await params.prompter.text({
|
|
55
|
+
message: "New GeWe account id",
|
|
56
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
57
|
+
});
|
|
58
|
+
const normalized = normalizeAccountId(String(entered));
|
|
59
|
+
if (String(entered).trim() !== normalized) {
|
|
60
|
+
await params.prompter.note(`Normalized account id to "${normalized}".`, "GeWe account");
|
|
61
|
+
}
|
|
62
|
+
return normalized;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseAllowFrom(raw: string): string[] {
|
|
66
|
+
return raw
|
|
67
|
+
.split(/[\n,;]+/g)
|
|
68
|
+
.map((entry) => stripChannelPrefix(entry.trim()))
|
|
69
|
+
.filter(Boolean);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function promptAllowFrom(params: {
|
|
73
|
+
prompter: WizardPrompter;
|
|
74
|
+
existing?: Array<string | number>;
|
|
75
|
+
required?: boolean;
|
|
76
|
+
}): Promise<string[]> {
|
|
77
|
+
const initial = (params.existing ?? []).map((entry) => String(entry)).join(", ");
|
|
78
|
+
const value = await params.prompter.text({
|
|
79
|
+
message: "Allowlist wxid (comma or newline separated)",
|
|
80
|
+
placeholder: "wxid_xxx, wxid_yyy",
|
|
81
|
+
initialValue: initial || undefined,
|
|
82
|
+
validate: params.required
|
|
83
|
+
? (input) => (parseAllowFrom(input).length > 0 ? undefined : "Required")
|
|
84
|
+
: undefined,
|
|
85
|
+
});
|
|
86
|
+
return parseAllowFrom(String(value));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function applyAccountPatch(
|
|
90
|
+
cfg: OpenClawConfig,
|
|
91
|
+
accountId: string,
|
|
92
|
+
patch: GeweAccountConfig,
|
|
93
|
+
): OpenClawConfig {
|
|
94
|
+
const existing = (cfg.channels?.[CHANNEL_CONFIG_KEY] ?? {}) as GeweAccountConfig & {
|
|
95
|
+
accounts?: Record<string, GeweAccountConfig>;
|
|
96
|
+
};
|
|
97
|
+
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
98
|
+
return {
|
|
99
|
+
...cfg,
|
|
100
|
+
channels: {
|
|
101
|
+
...cfg.channels,
|
|
102
|
+
[CHANNEL_CONFIG_KEY]: {
|
|
103
|
+
...existing,
|
|
104
|
+
...patch,
|
|
105
|
+
enabled: patch.enabled ?? existing.enabled ?? true,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
...cfg,
|
|
113
|
+
channels: {
|
|
114
|
+
...cfg.channels,
|
|
115
|
+
[CHANNEL_CONFIG_KEY]: {
|
|
116
|
+
...existing,
|
|
117
|
+
accounts: {
|
|
118
|
+
...(existing.accounts ?? {}),
|
|
119
|
+
[accountId]: {
|
|
120
|
+
...(existing.accounts?.[accountId] ?? {}),
|
|
121
|
+
...patch,
|
|
122
|
+
enabled:
|
|
123
|
+
patch.enabled ??
|
|
124
|
+
existing.accounts?.[accountId]?.enabled ??
|
|
125
|
+
existing.enabled ??
|
|
126
|
+
true,
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function readAccountConfig(cfg: OpenClawConfig, accountId: string): GeweAccountConfig {
|
|
135
|
+
const channelCfg = (cfg.channels?.[CHANNEL_CONFIG_KEY] ?? {}) as GeweAccountConfig & {
|
|
136
|
+
accounts?: Record<string, GeweAccountConfig>;
|
|
137
|
+
};
|
|
138
|
+
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
139
|
+
return channelCfg;
|
|
140
|
+
}
|
|
141
|
+
return channelCfg.accounts?.[accountId] ?? {};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export const geweOnboarding: GeweOnboardingAdapter = {
|
|
145
|
+
channel: CHANNEL_ID,
|
|
146
|
+
async getStatus(ctx) {
|
|
147
|
+
const accountId =
|
|
148
|
+
ctx.accountOverrides?.[CHANNEL_ID] ??
|
|
149
|
+
resolveDefaultGeweAccountId(ctx.cfg as CoreConfig);
|
|
150
|
+
const account = resolveGeweAccount({ cfg: ctx.cfg as CoreConfig, accountId });
|
|
151
|
+
const configured = Boolean(account.token?.trim() && account.appId?.trim());
|
|
152
|
+
const label = configured ? "configured" : "not configured";
|
|
153
|
+
const status = `GeWe (${accountId}): ${label}`;
|
|
154
|
+
return {
|
|
155
|
+
channel: CHANNEL_ID,
|
|
156
|
+
configured,
|
|
157
|
+
statusLines: [status],
|
|
158
|
+
selectionHint: label,
|
|
159
|
+
quickstartScore: configured ? 2 : 0,
|
|
160
|
+
};
|
|
161
|
+
},
|
|
162
|
+
async configure(ctx) {
|
|
163
|
+
const accountId = ctx.shouldPromptAccountIds
|
|
164
|
+
? await promptAccountId({ cfg: ctx.cfg, prompter: ctx.prompter })
|
|
165
|
+
: resolveDefaultGeweAccountId(ctx.cfg as CoreConfig);
|
|
166
|
+
const resolved = resolveGeweAccount({ cfg: ctx.cfg as CoreConfig, accountId });
|
|
167
|
+
const existing = readAccountConfig(ctx.cfg, accountId);
|
|
168
|
+
|
|
169
|
+
await ctx.prompter.note(
|
|
170
|
+
[
|
|
171
|
+
"You will need:",
|
|
172
|
+
"- GeWe token + appId",
|
|
173
|
+
"- Public webhook endpoint (FRP or reverse proxy)",
|
|
174
|
+
"- Public media base URL (for sending voice/media)",
|
|
175
|
+
].join("\n"),
|
|
176
|
+
"GeWe setup",
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const token = await ctx.prompter.text({
|
|
180
|
+
message: "GeWe token",
|
|
181
|
+
initialValue: resolved.tokenSource !== "none" ? resolved.token : existing.token,
|
|
182
|
+
validate: (value) => (value.trim() ? undefined : "Required"),
|
|
183
|
+
});
|
|
184
|
+
const appId = await ctx.prompter.text({
|
|
185
|
+
message: "GeWe appId",
|
|
186
|
+
initialValue: resolved.appIdSource !== "none" ? resolved.appId : existing.appId,
|
|
187
|
+
validate: (value) => (value.trim() ? undefined : "Required"),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const apiBaseUrl = await ctx.prompter.text({
|
|
191
|
+
message: "GeWe API base URL",
|
|
192
|
+
initialValue: existing.apiBaseUrl ?? DEFAULT_API_BASE_URL,
|
|
193
|
+
validate: (value) => (value.trim() ? undefined : "Required"),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const webhookHost = await ctx.prompter.text({
|
|
197
|
+
message: "Webhook host",
|
|
198
|
+
initialValue: existing.webhookHost ?? DEFAULT_WEBHOOK_HOST,
|
|
199
|
+
validate: (value) => (value.trim() ? undefined : "Required"),
|
|
200
|
+
});
|
|
201
|
+
const webhookPortRaw = await ctx.prompter.text({
|
|
202
|
+
message: "Webhook port",
|
|
203
|
+
initialValue: String(existing.webhookPort ?? DEFAULT_WEBHOOK_PORT),
|
|
204
|
+
validate: (value) => {
|
|
205
|
+
const parsed = Number(value);
|
|
206
|
+
if (!Number.isInteger(parsed) || parsed <= 0) return "Must be a positive integer";
|
|
207
|
+
return undefined;
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
const webhookPath = await ctx.prompter.text({
|
|
211
|
+
message: "Webhook path",
|
|
212
|
+
initialValue: existing.webhookPath ?? DEFAULT_WEBHOOK_PATH,
|
|
213
|
+
validate: (value) => (value.trim() ? undefined : "Required"),
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const mediaPublicUrl = await ctx.prompter.text({
|
|
217
|
+
message: "Media public URL (prefix)",
|
|
218
|
+
placeholder: "https://your-domain/gewe-media",
|
|
219
|
+
initialValue: existing.mediaPublicUrl,
|
|
220
|
+
validate: (value) => (value.trim() ? undefined : "Required"),
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
let allowFrom = existing.allowFrom;
|
|
224
|
+
let dmPolicy: GeweAccountConfig["dmPolicy"] | undefined;
|
|
225
|
+
if (ctx.forceAllowFrom) {
|
|
226
|
+
allowFrom = await promptAllowFrom({
|
|
227
|
+
prompter: ctx.prompter,
|
|
228
|
+
existing: existing.allowFrom,
|
|
229
|
+
required: true,
|
|
230
|
+
});
|
|
231
|
+
dmPolicy = "allowlist";
|
|
232
|
+
} else {
|
|
233
|
+
const wantsAllowlist = await ctx.prompter.confirm({
|
|
234
|
+
message: "Set a DM allowlist now? (optional)",
|
|
235
|
+
initialValue: false,
|
|
236
|
+
});
|
|
237
|
+
if (wantsAllowlist) {
|
|
238
|
+
allowFrom = await promptAllowFrom({
|
|
239
|
+
prompter: ctx.prompter,
|
|
240
|
+
existing: existing.allowFrom,
|
|
241
|
+
required: true,
|
|
242
|
+
});
|
|
243
|
+
dmPolicy = "allowlist";
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let nextCfg = applyAccountPatch(ctx.cfg, accountId, {
|
|
248
|
+
enabled: true,
|
|
249
|
+
token: token.trim(),
|
|
250
|
+
appId: appId.trim(),
|
|
251
|
+
apiBaseUrl: apiBaseUrl.trim().replace(/\/$/, ""),
|
|
252
|
+
webhookHost: webhookHost.trim(),
|
|
253
|
+
webhookPort: Number(webhookPortRaw),
|
|
254
|
+
webhookPath: webhookPath.trim(),
|
|
255
|
+
mediaHost: existing.mediaHost ?? DEFAULT_MEDIA_HOST,
|
|
256
|
+
mediaPort: existing.mediaPort ?? DEFAULT_MEDIA_PORT,
|
|
257
|
+
mediaPath: existing.mediaPath ?? DEFAULT_MEDIA_PATH,
|
|
258
|
+
mediaPublicUrl: mediaPublicUrl.trim(),
|
|
259
|
+
...(allowFrom ? { allowFrom } : {}),
|
|
260
|
+
...(dmPolicy ? { dmPolicy } : {}),
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
return { cfg: nextCfg, accountId };
|
|
264
|
+
},
|
|
265
|
+
};
|
package/src/send.ts
CHANGED
|
@@ -8,7 +8,7 @@ type GeweSendContext = {
|
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
function buildContext(account: ResolvedGeweAccount): GeweSendContext {
|
|
11
|
-
const baseUrl = account.config.apiBaseUrl?.trim() || "
|
|
11
|
+
const baseUrl = account.config.apiBaseUrl?.trim() || "https://www.geweapi.com";
|
|
12
12
|
return { baseUrl, token: account.token, appId: account.appId };
|
|
13
13
|
}
|
|
14
14
|
|
package/src/silk.ts
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { createReadStream, createWriteStream, existsSync } from "node:fs";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { Readable } from "node:stream";
|
|
7
|
+
import { pipeline } from "node:stream/promises";
|
|
8
|
+
|
|
9
|
+
import type { ResolvedGeweAccount } from "./types.js";
|
|
10
|
+
import { getGeweRuntime } from "./runtime.js";
|
|
11
|
+
import { CHANNEL_ID } from "./constants.js";
|
|
12
|
+
|
|
13
|
+
const DEFAULT_SILK_VERSION = "latest";
|
|
14
|
+
const DEFAULT_SILK_BASE_URL =
|
|
15
|
+
"https://github.com/Wangnov/rust-silk/releases/download";
|
|
16
|
+
const DEFAULT_DOWNLOAD_TIMEOUT_MS = 120_000;
|
|
17
|
+
const DEFAULT_EXTRACT_TIMEOUT_MS = 60_000;
|
|
18
|
+
|
|
19
|
+
type RustSilkAsset = {
|
|
20
|
+
name: string;
|
|
21
|
+
archive: "tar.xz" | "zip";
|
|
22
|
+
binary: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type ResolvedVersion = {
|
|
26
|
+
tag: string;
|
|
27
|
+
folder: string;
|
|
28
|
+
isLatest: boolean;
|
|
29
|
+
resolvedTag?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const installCache = new Map<string, Promise<string | null>>();
|
|
33
|
+
|
|
34
|
+
export function buildRustSilkEncodeArgs(params: {
|
|
35
|
+
input: string;
|
|
36
|
+
output: string;
|
|
37
|
+
sampleRate: number;
|
|
38
|
+
tencent?: boolean;
|
|
39
|
+
}): string[] {
|
|
40
|
+
const args = [
|
|
41
|
+
"encode",
|
|
42
|
+
"-i",
|
|
43
|
+
params.input,
|
|
44
|
+
"-o",
|
|
45
|
+
params.output,
|
|
46
|
+
"--sample-rate",
|
|
47
|
+
String(params.sampleRate),
|
|
48
|
+
"--quiet",
|
|
49
|
+
];
|
|
50
|
+
if (params.tencent ?? true) {
|
|
51
|
+
args.push("--tencent");
|
|
52
|
+
}
|
|
53
|
+
return args;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function buildRustSilkDecodeArgs(params: {
|
|
57
|
+
input: string;
|
|
58
|
+
output: string;
|
|
59
|
+
sampleRate: number;
|
|
60
|
+
wav?: boolean;
|
|
61
|
+
}): string[] {
|
|
62
|
+
const args = [
|
|
63
|
+
"decode",
|
|
64
|
+
"-i",
|
|
65
|
+
params.input,
|
|
66
|
+
"-o",
|
|
67
|
+
params.output,
|
|
68
|
+
"--sample-rate",
|
|
69
|
+
String(params.sampleRate),
|
|
70
|
+
"--quiet",
|
|
71
|
+
];
|
|
72
|
+
if (params.wav) args.push("--wav");
|
|
73
|
+
return args;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function ensureRustSilkBinary(
|
|
77
|
+
account: ResolvedGeweAccount,
|
|
78
|
+
): Promise<string | null> {
|
|
79
|
+
if (account.config.silkAutoDownload === false) return null;
|
|
80
|
+
|
|
81
|
+
const asset = resolveRustSilkAsset(process.platform, process.arch);
|
|
82
|
+
if (!asset) return null;
|
|
83
|
+
|
|
84
|
+
const versionInput = account.config.silkVersion?.trim() || DEFAULT_SILK_VERSION;
|
|
85
|
+
const baseUrl = account.config.silkBaseUrl?.trim() || DEFAULT_SILK_BASE_URL;
|
|
86
|
+
const resolved = await resolveRequestedVersion(versionInput, baseUrl);
|
|
87
|
+
const { tag, folder } = resolved;
|
|
88
|
+
const cacheKey = [
|
|
89
|
+
baseUrl,
|
|
90
|
+
tag,
|
|
91
|
+
folder,
|
|
92
|
+
asset.name,
|
|
93
|
+
process.platform,
|
|
94
|
+
process.arch,
|
|
95
|
+
].join("|");
|
|
96
|
+
|
|
97
|
+
if (installCache.has(cacheKey)) {
|
|
98
|
+
return installCache.get(cacheKey) ?? null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const installPromise = installRustSilk({
|
|
102
|
+
account,
|
|
103
|
+
asset,
|
|
104
|
+
baseUrl,
|
|
105
|
+
tag,
|
|
106
|
+
folder,
|
|
107
|
+
isLatest: resolved.isLatest,
|
|
108
|
+
resolvedTag: resolved.resolvedTag,
|
|
109
|
+
}).finally(() => {
|
|
110
|
+
installCache.delete(cacheKey);
|
|
111
|
+
});
|
|
112
|
+
installCache.set(cacheKey, installPromise);
|
|
113
|
+
return installPromise;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function installRustSilk(params: {
|
|
117
|
+
account: ResolvedGeweAccount;
|
|
118
|
+
asset: RustSilkAsset;
|
|
119
|
+
baseUrl: string;
|
|
120
|
+
tag: string;
|
|
121
|
+
folder: string;
|
|
122
|
+
isLatest: boolean;
|
|
123
|
+
resolvedTag?: string;
|
|
124
|
+
}): Promise<string | null> {
|
|
125
|
+
const core = getGeweRuntime();
|
|
126
|
+
const logger = core.logging.getChildLogger({ channel: CHANNEL_ID, module: "silk" });
|
|
127
|
+
const customInstall = params.account.config.silkInstallDir?.trim();
|
|
128
|
+
const installRoot = customInstall
|
|
129
|
+
? resolveUserPath(customInstall)
|
|
130
|
+
: path.join(resolveConfigDir(), "tools", "rust-silk");
|
|
131
|
+
const installDir = path.join(installRoot, params.folder);
|
|
132
|
+
const binaryPath = path.join(installDir, params.asset.binary);
|
|
133
|
+
|
|
134
|
+
if (existsSync(binaryPath)) {
|
|
135
|
+
if (params.isLatest && params.folder !== "latest") {
|
|
136
|
+
await cleanupOldVersions(installRoot, params.folder);
|
|
137
|
+
}
|
|
138
|
+
return binaryPath;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gewe-silk-"));
|
|
142
|
+
try {
|
|
143
|
+
const archivePath = path.join(tmpDir, params.asset.name);
|
|
144
|
+
const archiveUrl = `${params.baseUrl}/${params.tag}/${params.asset.name}`;
|
|
145
|
+
await downloadFile(archiveUrl, archivePath);
|
|
146
|
+
|
|
147
|
+
const expectedHash = await resolveChecksum({
|
|
148
|
+
account: params.account,
|
|
149
|
+
baseUrl: params.baseUrl,
|
|
150
|
+
tag: params.tag,
|
|
151
|
+
assetName: params.asset.name,
|
|
152
|
+
});
|
|
153
|
+
if (!expectedHash && params.account.config.silkAllowUnverified !== true) {
|
|
154
|
+
throw new Error("missing checksum for rust-silk download");
|
|
155
|
+
}
|
|
156
|
+
if (expectedHash) {
|
|
157
|
+
const actual = await sha256File(archivePath);
|
|
158
|
+
if (actual !== expectedHash) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`checksum mismatch for ${params.asset.name}: expected ${expectedHash} got ${actual}`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
await extractArchive(core, archivePath, tmpDir, params.asset.archive);
|
|
166
|
+
const extracted = await findBinary(tmpDir, params.asset.binary);
|
|
167
|
+
if (!extracted) {
|
|
168
|
+
throw new Error(`rust-silk binary not found in ${params.asset.name}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
await fs.mkdir(installDir, { recursive: true });
|
|
172
|
+
await fs.copyFile(extracted, binaryPath);
|
|
173
|
+
await fs.chmod(binaryPath, 0o755).catch(() => {});
|
|
174
|
+
await fs.writeFile(
|
|
175
|
+
path.join(installDir, "install.json"),
|
|
176
|
+
JSON.stringify(
|
|
177
|
+
{
|
|
178
|
+
version: params.folder,
|
|
179
|
+
tag: params.tag,
|
|
180
|
+
resolvedTag: params.resolvedTag ?? null,
|
|
181
|
+
asset: params.asset.name,
|
|
182
|
+
installedAt: new Date().toISOString(),
|
|
183
|
+
},
|
|
184
|
+
null,
|
|
185
|
+
2,
|
|
186
|
+
),
|
|
187
|
+
"utf8",
|
|
188
|
+
);
|
|
189
|
+
if (params.isLatest && params.folder !== "latest") {
|
|
190
|
+
await cleanupOldVersions(installRoot, params.folder);
|
|
191
|
+
}
|
|
192
|
+
return binaryPath;
|
|
193
|
+
} catch (err) {
|
|
194
|
+
logger.warn?.(`rust-silk install failed: ${String(err)}`);
|
|
195
|
+
return null;
|
|
196
|
+
} finally {
|
|
197
|
+
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function resolveRequestedVersion(
|
|
202
|
+
versionInput: string,
|
|
203
|
+
baseUrl: string,
|
|
204
|
+
): Promise<ResolvedVersion> {
|
|
205
|
+
const trimmed = versionInput.trim();
|
|
206
|
+
if (!trimmed || trimmed === "latest") {
|
|
207
|
+
const latestTag = await resolveLatestTag(baseUrl).catch(() => null);
|
|
208
|
+
if (latestTag) {
|
|
209
|
+
const folder = latestTag.startsWith("v") ? latestTag.slice(1) : latestTag;
|
|
210
|
+
return {
|
|
211
|
+
tag: latestTag,
|
|
212
|
+
folder,
|
|
213
|
+
isLatest: true,
|
|
214
|
+
resolvedTag: latestTag,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
return { tag: "latest", folder: "latest", isLatest: true };
|
|
218
|
+
}
|
|
219
|
+
const { tag, folder } = normalizeVersion(trimmed);
|
|
220
|
+
return { tag, folder, isLatest: false };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function normalizeVersion(version: string): { tag: string; folder: string } {
|
|
224
|
+
const trimmed = version.trim();
|
|
225
|
+
const tag = trimmed.startsWith("v") ? trimmed : `v${trimmed}`;
|
|
226
|
+
const folder = trimmed.startsWith("v") ? trimmed.slice(1) : trimmed;
|
|
227
|
+
return { tag, folder };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function resolveLatestTag(baseUrl: string): Promise<string | null> {
|
|
231
|
+
const repoUrl = deriveRepoUrl(baseUrl);
|
|
232
|
+
if (!repoUrl) return null;
|
|
233
|
+
const response = await fetchWithTimeout(
|
|
234
|
+
`${repoUrl}/releases/latest`,
|
|
235
|
+
DEFAULT_DOWNLOAD_TIMEOUT_MS,
|
|
236
|
+
);
|
|
237
|
+
const finalUrl = response.url || "";
|
|
238
|
+
const match = finalUrl.match(/\/tag\/([^/?#]+)/);
|
|
239
|
+
if (!match) return null;
|
|
240
|
+
return decodeURIComponent(match[1]);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function deriveRepoUrl(baseUrl: string): string | null {
|
|
244
|
+
const trimmed = baseUrl.replace(/\/+$/, "");
|
|
245
|
+
const match = trimmed.match(
|
|
246
|
+
/^(https?:\/\/github\.com\/[^/]+\/[^/]+)(?:\/releases\/download)?$/i,
|
|
247
|
+
);
|
|
248
|
+
if (match) return match[1];
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function resolveRustSilkAsset(
|
|
253
|
+
platform: NodeJS.Platform,
|
|
254
|
+
arch: string,
|
|
255
|
+
): RustSilkAsset | null {
|
|
256
|
+
if (platform === "darwin" && arch === "arm64") {
|
|
257
|
+
return {
|
|
258
|
+
name: "rust-silk-aarch64-apple-darwin.tar.xz",
|
|
259
|
+
archive: "tar.xz",
|
|
260
|
+
binary: "rust-silk",
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
if (platform === "darwin" && (arch === "x64" || arch === "amd64")) {
|
|
264
|
+
return {
|
|
265
|
+
name: "rust-silk-x86_64-apple-darwin.tar.xz",
|
|
266
|
+
archive: "tar.xz",
|
|
267
|
+
binary: "rust-silk",
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
if (platform === "linux" && arch === "arm64") {
|
|
271
|
+
return {
|
|
272
|
+
name: "rust-silk-aarch64-unknown-linux-gnu.tar.xz",
|
|
273
|
+
archive: "tar.xz",
|
|
274
|
+
binary: "rust-silk",
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
if (platform === "linux" && (arch === "x64" || arch === "amd64")) {
|
|
278
|
+
return {
|
|
279
|
+
name: "rust-silk-x86_64-unknown-linux-gnu.tar.xz",
|
|
280
|
+
archive: "tar.xz",
|
|
281
|
+
binary: "rust-silk",
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
if (platform === "win32" && (arch === "x64" || arch === "amd64")) {
|
|
285
|
+
return {
|
|
286
|
+
name: "rust-silk-x86_64-pc-windows-msvc.zip",
|
|
287
|
+
archive: "zip",
|
|
288
|
+
binary: "rust-silk.exe",
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function resolveChecksum(params: {
|
|
295
|
+
account: ResolvedGeweAccount;
|
|
296
|
+
baseUrl: string;
|
|
297
|
+
tag: string;
|
|
298
|
+
assetName: string;
|
|
299
|
+
}): Promise<string | null> {
|
|
300
|
+
if (params.account.config.silkSha256?.trim()) {
|
|
301
|
+
return params.account.config.silkSha256.trim().toLowerCase();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const sumUrl = `${params.baseUrl}/${params.tag}/sha256.sum`;
|
|
305
|
+
const sum = await fetchText(sumUrl).catch(() => "");
|
|
306
|
+
if (sum) {
|
|
307
|
+
const parsed = parseChecksum(sum, params.assetName);
|
|
308
|
+
if (parsed) return parsed;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const assetSumUrl = `${params.baseUrl}/${params.tag}/${params.assetName}.sha256`;
|
|
312
|
+
const assetSum = await fetchText(assetSumUrl).catch(() => "");
|
|
313
|
+
if (assetSum) {
|
|
314
|
+
const parsed = parseChecksum(assetSum, params.assetName);
|
|
315
|
+
if (parsed) return parsed;
|
|
316
|
+
const fallback = assetSum.trim().split(/\s+/)[0];
|
|
317
|
+
if (/^[a-f0-9]{64}$/i.test(fallback)) return fallback.toLowerCase();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function parseChecksum(contents: string, assetName: string): string | null {
|
|
324
|
+
const lines = contents.split(/\r?\n/);
|
|
325
|
+
for (const line of lines) {
|
|
326
|
+
const trimmed = line.trim();
|
|
327
|
+
if (!trimmed) continue;
|
|
328
|
+
const match = trimmed.match(/^([a-f0-9]{64})\s+\*?(.+)$/i);
|
|
329
|
+
if (!match) continue;
|
|
330
|
+
const [, hash, name] = match;
|
|
331
|
+
if (name.trim() === assetName) return hash.toLowerCase();
|
|
332
|
+
}
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function downloadFile(url: string, dest: string): Promise<void> {
|
|
337
|
+
const response = await fetchWithTimeout(url, DEFAULT_DOWNLOAD_TIMEOUT_MS);
|
|
338
|
+
if (!response.ok) {
|
|
339
|
+
throw new Error(`download failed: ${response.status} ${response.statusText}`);
|
|
340
|
+
}
|
|
341
|
+
if (!response.body) {
|
|
342
|
+
throw new Error("download failed: empty response body");
|
|
343
|
+
}
|
|
344
|
+
const stream = Readable.fromWeb(response.body as unknown as ReadableStream);
|
|
345
|
+
await pipeline(stream, createWriteStream(dest));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function fetchText(url: string): Promise<string> {
|
|
349
|
+
const response = await fetchWithTimeout(url, DEFAULT_DOWNLOAD_TIMEOUT_MS);
|
|
350
|
+
if (!response.ok) {
|
|
351
|
+
throw new Error(`download failed: ${response.status} ${response.statusText}`);
|
|
352
|
+
}
|
|
353
|
+
return response.text();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function fetchWithTimeout(url: string, timeoutMs: number): Promise<Response> {
|
|
357
|
+
const controller = new AbortController();
|
|
358
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
359
|
+
try {
|
|
360
|
+
return await fetch(url, { signal: controller.signal });
|
|
361
|
+
} finally {
|
|
362
|
+
clearTimeout(timer);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function sha256File(filePath: string): Promise<string> {
|
|
367
|
+
const hash = createHash("sha256");
|
|
368
|
+
const fileStream = createReadStream(filePath);
|
|
369
|
+
for await (const chunk of fileStream) {
|
|
370
|
+
hash.update(chunk as Buffer);
|
|
371
|
+
}
|
|
372
|
+
return hash.digest("hex");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function extractArchive(
|
|
376
|
+
core: ReturnType<typeof getGeweRuntime>,
|
|
377
|
+
archivePath: string,
|
|
378
|
+
destDir: string,
|
|
379
|
+
archiveType: RustSilkAsset["archive"],
|
|
380
|
+
): Promise<void> {
|
|
381
|
+
const args =
|
|
382
|
+
archiveType === "zip"
|
|
383
|
+
? ["-xf", archivePath, "-C", destDir]
|
|
384
|
+
: ["-xJf", archivePath, "-C", destDir];
|
|
385
|
+
let result = await core.system.runCommandWithTimeout(["tar", ...args], {
|
|
386
|
+
timeoutMs: DEFAULT_EXTRACT_TIMEOUT_MS,
|
|
387
|
+
});
|
|
388
|
+
if (result.code === 0) return;
|
|
389
|
+
|
|
390
|
+
if (archiveType === "zip") {
|
|
391
|
+
result = await core.system.runCommandWithTimeout(
|
|
392
|
+
["powershell", "-Command", `Expand-Archive -Path "${archivePath}" -DestinationPath "${destDir}" -Force`],
|
|
393
|
+
{ timeoutMs: DEFAULT_EXTRACT_TIMEOUT_MS },
|
|
394
|
+
);
|
|
395
|
+
if (result.code === 0) return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
throw new Error(result.stderr.trim() || `extract failed with code ${result.code ?? "?"}`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function findBinary(root: string, fileName: string): Promise<string | null> {
|
|
402
|
+
const entries = await fs.readdir(root, { withFileTypes: true });
|
|
403
|
+
for (const entry of entries) {
|
|
404
|
+
const fullPath = path.join(root, entry.name);
|
|
405
|
+
if (entry.isFile() && entry.name === fileName) {
|
|
406
|
+
return fullPath;
|
|
407
|
+
}
|
|
408
|
+
if (entry.isDirectory()) {
|
|
409
|
+
const nested = await findBinary(fullPath, fileName);
|
|
410
|
+
if (nested) return nested;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function cleanupOldVersions(
|
|
417
|
+
installRoot: string,
|
|
418
|
+
keepFolder: string,
|
|
419
|
+
): Promise<void> {
|
|
420
|
+
const entries = await fs.readdir(installRoot, { withFileTypes: true }).catch(() => []);
|
|
421
|
+
const tasks: Promise<void>[] = [];
|
|
422
|
+
for (const entry of entries) {
|
|
423
|
+
if (!entry.isDirectory()) continue;
|
|
424
|
+
if (entry.name === keepFolder) continue;
|
|
425
|
+
const fullPath = path.join(installRoot, entry.name);
|
|
426
|
+
tasks.push(fs.rm(fullPath, { recursive: true, force: true }).then(() => {}));
|
|
427
|
+
}
|
|
428
|
+
if (tasks.length) await Promise.all(tasks);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function resolveConfigDir(
|
|
432
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
433
|
+
homedir: () => string = os.homedir,
|
|
434
|
+
): string {
|
|
435
|
+
const override = env.OPENCLAW_STATE_DIR?.trim() || env.CLAWDBOT_STATE_DIR?.trim();
|
|
436
|
+
if (override) return resolveUserPath(override);
|
|
437
|
+
const legacyDirs = [".clawdbot", ".moltbot", ".moldbot"].map((dir) =>
|
|
438
|
+
path.join(homedir(), dir),
|
|
439
|
+
);
|
|
440
|
+
const newDir = path.join(homedir(), ".openclaw");
|
|
441
|
+
try {
|
|
442
|
+
if (existsSync(newDir)) return newDir;
|
|
443
|
+
const existingLegacy = legacyDirs.find((dir) => {
|
|
444
|
+
try {
|
|
445
|
+
return existsSync(dir);
|
|
446
|
+
} catch {
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
if (existingLegacy) return existingLegacy;
|
|
451
|
+
} catch {
|
|
452
|
+
// best-effort
|
|
453
|
+
}
|
|
454
|
+
return newDir;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function resolveUserPath(input: string): string {
|
|
458
|
+
const trimmed = input.trim();
|
|
459
|
+
if (!trimmed) return trimmed;
|
|
460
|
+
if (trimmed.startsWith("~")) {
|
|
461
|
+
const expanded = trimmed.replace(/^~(?=$|[\\/])/, os.homedir());
|
|
462
|
+
return path.resolve(expanded);
|
|
463
|
+
}
|
|
464
|
+
return path.resolve(trimmed);
|
|
465
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -42,6 +42,12 @@ export type GeweAccountConfig = {
|
|
|
42
42
|
voiceDecodeArgs?: string[];
|
|
43
43
|
voiceDecodeSampleRate?: number;
|
|
44
44
|
voiceDecodeOutput?: "pcm" | "wav";
|
|
45
|
+
silkAutoDownload?: boolean;
|
|
46
|
+
silkVersion?: string;
|
|
47
|
+
silkBaseUrl?: string;
|
|
48
|
+
silkSha256?: string;
|
|
49
|
+
silkAllowUnverified?: boolean;
|
|
50
|
+
silkInstallDir?: string;
|
|
45
51
|
videoFfmpegPath?: string;
|
|
46
52
|
videoFfprobePath?: string;
|
|
47
53
|
videoThumbUrl?: string;
|