gewe-openclaw 2026.2.4 → 2026.3.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 +23 -2
- package/gewe-openclaw-2026.2.4.tgz +0 -0
- package/package.json +3 -1
- package/src/config-schema.ts +43 -0
- package/src/delivery.ts +279 -47
- package/src/onboarding.ts +104 -3
- package/src/s3.ts +149 -0
- package/src/types.ts +12 -0
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ openclaw plugins install ./gewe-openclaw.tgz
|
|
|
42
42
|
openclaw onboard
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
在通道列表中选择 **GeWe**,按提示填写 `token`、`appId`、`webhook
|
|
45
|
+
在通道列表中选择 **GeWe**,按提示填写 `token`、`appId`、`webhook`,以及可选的 `mediaPublicUrl`/`S3` 媒体配置。
|
|
46
46
|
|
|
47
47
|
### 方式 B:直接编辑配置文件
|
|
48
48
|
|
|
@@ -66,6 +66,15 @@ openclaw onboard
|
|
|
66
66
|
"mediaPort": 4400,
|
|
67
67
|
"mediaPath": "/gewe-media",
|
|
68
68
|
"mediaPublicUrl": "https://your-public-domain/gewe-media",
|
|
69
|
+
"s3Enabled": true,
|
|
70
|
+
"s3Endpoint": "https://s3.amazonaws.com",
|
|
71
|
+
"s3Region": "us-east-1",
|
|
72
|
+
"s3Bucket": "your-bucket",
|
|
73
|
+
"s3AccessKeyId": "<access-key-id>",
|
|
74
|
+
"s3SecretAccessKey": "<secret-access-key>",
|
|
75
|
+
"s3UrlMode": "public",
|
|
76
|
+
"s3PublicBaseUrl": "https://cdn.example.com/gewe-media",
|
|
77
|
+
"s3KeyPrefix": "gewe-openclaw/outbound",
|
|
69
78
|
"allowFrom": ["wxid_xxx"]
|
|
70
79
|
}
|
|
71
80
|
}
|
|
@@ -75,7 +84,15 @@ openclaw onboard
|
|
|
75
84
|
完整参数说明:
|
|
76
85
|
- `webhookHost/webhookPort/webhookPath`:GeWe 回调入口(需公网可达,常配合 FRP)。
|
|
77
86
|
- `mediaPath`:本地媒体服务的路由前缀(默认 `/gewe-media`)。
|
|
78
|
-
- `mediaPublicUrl
|
|
87
|
+
- `mediaPublicUrl`:本地反代回退时的公网地址前缀(可选)。配置后会自动拼接媒体 ID;通常应与 `mediaPath` 对齐。
|
|
88
|
+
- `s3Enabled`:是否启用 S3 兼容上传。
|
|
89
|
+
- `s3Endpoint/s3Region/s3Bucket/s3AccessKeyId/s3SecretAccessKey`:S3 兼容服务连接参数。
|
|
90
|
+
- `s3SessionToken`:临时凭证可选字段。
|
|
91
|
+
- `s3ForcePathStyle`:是否启用 path-style(部分 S3 兼容服务需要)。
|
|
92
|
+
- `s3UrlMode`:`public` 或 `presigned`(默认 `public`)。
|
|
93
|
+
- `s3PublicBaseUrl`:`public` 模式下用于拼接可访问 URL(必填)。
|
|
94
|
+
- `s3PresignExpiresSec`:`presigned` 模式签名有效期(默认 3600 秒)。
|
|
95
|
+
- `s3KeyPrefix`:对象 key 前缀(默认 `gewe-openclaw/outbound`)。
|
|
79
96
|
- `allowFrom`:允许私聊触发的微信 ID(或在群里走 allowlist 规则)。
|
|
80
97
|
- `voiceAutoConvert`:自动将音频转为 silk(默认开启;设为 `false` 可关闭)。
|
|
81
98
|
- `silkAutoDownload`:自动下载 `rust-silk`(默认开启;可关闭后自行配置 `voiceSilkPath` / `voiceDecodePath`)。
|
|
@@ -94,6 +111,10 @@ openclaw onboard
|
|
|
94
111
|
- `mediaMaxMb`:上传媒体大小上限(默认 20MB)。
|
|
95
112
|
- `downloadMinDelayMs`/`downloadMaxDelayMs`:入站媒体下载节流。
|
|
96
113
|
|
|
114
|
+
发送媒体时的 URL 策略:
|
|
115
|
+
- 本地文件:优先上传 S3,失败回退 `mediaPublicUrl` 本地反代。
|
|
116
|
+
- 公网 URL:先尝试原 URL 发送,失败后再尝试上传 S3,仍失败回退本地反代。
|
|
117
|
+
|
|
97
118
|
> 配置变更后需重启 Gateway。
|
|
98
119
|
|
|
99
120
|
## 高级用法:让未安装插件也出现在 onboarding 列表
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gewe-openclaw",
|
|
3
|
-
"version": "2026.2
|
|
3
|
+
"version": "2026.3.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenClaw GeWe channel plugin",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
+
"@aws-sdk/client-s3": "^3.922.0",
|
|
36
|
+
"@aws-sdk/s3-request-presigner": "^3.922.0",
|
|
35
37
|
"zod": "^4.3.6"
|
|
36
38
|
},
|
|
37
39
|
"peerDependencies": {
|
package/src/config-schema.ts
CHANGED
|
@@ -40,6 +40,18 @@ export const GeweAccountSchemaBase = z
|
|
|
40
40
|
mediaPath: z.string().optional(),
|
|
41
41
|
mediaPublicUrl: z.string().optional(),
|
|
42
42
|
mediaMaxMb: z.number().positive().optional(),
|
|
43
|
+
s3Enabled: z.boolean().optional(),
|
|
44
|
+
s3Endpoint: z.string().optional(),
|
|
45
|
+
s3Region: z.string().optional(),
|
|
46
|
+
s3Bucket: z.string().optional(),
|
|
47
|
+
s3AccessKeyId: z.string().optional(),
|
|
48
|
+
s3SecretAccessKey: z.string().optional(),
|
|
49
|
+
s3SessionToken: z.string().optional(),
|
|
50
|
+
s3ForcePathStyle: z.boolean().optional(),
|
|
51
|
+
s3PublicBaseUrl: z.string().optional(),
|
|
52
|
+
s3KeyPrefix: z.string().optional(),
|
|
53
|
+
s3UrlMode: z.enum(["public", "presigned"]).optional(),
|
|
54
|
+
s3PresignExpiresSec: z.number().int().positive().optional(),
|
|
43
55
|
voiceAutoConvert: z.boolean().optional(),
|
|
44
56
|
voiceFfmpegPath: z.string().optional(),
|
|
45
57
|
voiceSilkPath: z.string().optional(),
|
|
@@ -85,6 +97,37 @@ export const GeweAccountSchemaBase = z
|
|
|
85
97
|
message: "downloadMaxDelayMs must be >= downloadMinDelayMs",
|
|
86
98
|
});
|
|
87
99
|
}
|
|
100
|
+
|
|
101
|
+
if (value.s3Enabled === true) {
|
|
102
|
+
const required: Array<keyof typeof value> = [
|
|
103
|
+
"s3Endpoint",
|
|
104
|
+
"s3Region",
|
|
105
|
+
"s3Bucket",
|
|
106
|
+
"s3AccessKeyId",
|
|
107
|
+
"s3SecretAccessKey",
|
|
108
|
+
];
|
|
109
|
+
for (const key of required) {
|
|
110
|
+
const raw = value[key];
|
|
111
|
+
if (typeof raw !== "string" || !raw.trim()) {
|
|
112
|
+
ctx.addIssue({
|
|
113
|
+
code: z.ZodIssueCode.custom,
|
|
114
|
+
path: [key],
|
|
115
|
+
message: `${String(key)} is required when s3Enabled=true`,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const mode = value.s3UrlMode ?? "public";
|
|
120
|
+
if (mode === "public") {
|
|
121
|
+
const base = value.s3PublicBaseUrl?.trim();
|
|
122
|
+
if (!base) {
|
|
123
|
+
ctx.addIssue({
|
|
124
|
+
code: z.ZodIssueCode.custom,
|
|
125
|
+
path: ["s3PublicBaseUrl"],
|
|
126
|
+
message: "s3PublicBaseUrl is required when s3UrlMode=public",
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
88
131
|
});
|
|
89
132
|
|
|
90
133
|
export const GeweAccountSchema = GeweAccountSchemaBase.superRefine((value, ctx) => {
|
package/src/delivery.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { OpenClawConfig, ReplyPayload } from "openclaw/plugin-sdk";
|
|
|
9
9
|
import { extractOriginalFilename, extensionForMime } from "openclaw/plugin-sdk";
|
|
10
10
|
import { CHANNEL_ID } from "./constants.js";
|
|
11
11
|
import { getGeweRuntime } from "./runtime.js";
|
|
12
|
+
import { resolveS3Config, uploadToS3 } from "./s3.js";
|
|
12
13
|
import { ensureRustSilkBinary } from "./silk.js";
|
|
13
14
|
import {
|
|
14
15
|
sendFileGewe,
|
|
@@ -43,6 +44,9 @@ type ResolvedMedia = {
|
|
|
43
44
|
contentType?: string;
|
|
44
45
|
fileName?: string;
|
|
45
46
|
localPath?: string;
|
|
47
|
+
sourceKind: "remote" | "local";
|
|
48
|
+
sourceUrl: string;
|
|
49
|
+
provider: "direct" | "s3" | "proxy";
|
|
46
50
|
};
|
|
47
51
|
|
|
48
52
|
const LINK_THUMB_MAX_BYTES = 50 * 1024;
|
|
@@ -94,6 +98,21 @@ function buildPublicUrl(baseUrl: string, id: string): string {
|
|
|
94
98
|
return `${trimmed}/${encodeURIComponent(id)}`;
|
|
95
99
|
}
|
|
96
100
|
|
|
101
|
+
function hasProxyBase(account: ResolvedGeweAccount): boolean {
|
|
102
|
+
return Boolean(account.config.mediaPublicUrl?.trim());
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function hasS3(account: ResolvedGeweAccount): boolean {
|
|
106
|
+
return account.config.s3Enabled === true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function resolveFallbackProviders(account: ResolvedGeweAccount): Array<"s3" | "proxy"> {
|
|
110
|
+
const providers: Array<"s3" | "proxy"> = [];
|
|
111
|
+
if (hasS3(account)) providers.push("s3");
|
|
112
|
+
if (hasProxyBase(account)) providers.push("proxy");
|
|
113
|
+
return providers;
|
|
114
|
+
}
|
|
115
|
+
|
|
97
116
|
function resolveMediaMaxBytes(account: ResolvedGeweAccount): number {
|
|
98
117
|
const maxMb = account.config.mediaMaxMb;
|
|
99
118
|
if (typeof maxMb === "number" && maxMb > 0) return Math.floor(maxMb * 1024 * 1024);
|
|
@@ -553,11 +572,6 @@ async function stageThumbBuffer(params: {
|
|
|
553
572
|
fileName?: string;
|
|
554
573
|
}): Promise<string> {
|
|
555
574
|
const core = getGeweRuntime();
|
|
556
|
-
const publicBase = params.account.config.mediaPublicUrl?.trim();
|
|
557
|
-
if (!publicBase) {
|
|
558
|
-
throw new Error("mediaPublicUrl not configured (required for link thumbnails)");
|
|
559
|
-
}
|
|
560
|
-
|
|
561
575
|
const normalized = await normalizeThumbBuffer({
|
|
562
576
|
buffer: params.buffer,
|
|
563
577
|
contentType: params.contentType,
|
|
@@ -566,6 +580,31 @@ async function stageThumbBuffer(params: {
|
|
|
566
580
|
throw new Error("link thumbnail exceeds 50KB after resize");
|
|
567
581
|
}
|
|
568
582
|
|
|
583
|
+
if (hasS3(params.account)) {
|
|
584
|
+
try {
|
|
585
|
+
const s3Config = resolveS3Config(params.account.config);
|
|
586
|
+
if (!s3Config) throw new Error("s3 not configured");
|
|
587
|
+
const uploaded = await uploadToS3({
|
|
588
|
+
config: s3Config,
|
|
589
|
+
accountId: params.account.accountId,
|
|
590
|
+
buffer: normalized.buffer,
|
|
591
|
+
contentType: normalized.contentType,
|
|
592
|
+
fileName: params.fileName,
|
|
593
|
+
});
|
|
594
|
+
return uploaded.url;
|
|
595
|
+
} catch (err) {
|
|
596
|
+
if (!hasProxyBase(params.account)) {
|
|
597
|
+
throw new Error(`s3 thumb upload failed and proxy fallback unavailable: ${String(err)}`);
|
|
598
|
+
}
|
|
599
|
+
const logger = core.logging.getChildLogger({ channel: CHANNEL_ID, module: "thumb" });
|
|
600
|
+
logger.warn?.(`gewe thumb s3 upload failed, fallback proxy: ${String(err)}`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const publicBase = params.account.config.mediaPublicUrl?.trim();
|
|
605
|
+
if (!publicBase) {
|
|
606
|
+
throw new Error("mediaPublicUrl not configured (required for thumbnail fallback)");
|
|
607
|
+
}
|
|
569
608
|
const saved = await core.channel.media.saveMediaBuffer(
|
|
570
609
|
normalized.buffer,
|
|
571
610
|
normalized.contentType,
|
|
@@ -638,26 +677,27 @@ async function stageMedia(params: {
|
|
|
638
677
|
const core = getGeweRuntime();
|
|
639
678
|
const rawUrl = normalizeMediaToken(params.mediaUrl);
|
|
640
679
|
if (!rawUrl) throw new Error("mediaUrl is empty");
|
|
641
|
-
|
|
642
|
-
if (
|
|
680
|
+
const isRemote = looksLikeHttpUrl(rawUrl);
|
|
681
|
+
if (isRemote && params.allowRemote) {
|
|
643
682
|
const contentType = await core.media.detectMime({ filePath: rawUrl });
|
|
644
683
|
const fileName = path.basename(new URL(rawUrl).pathname || "");
|
|
645
|
-
return {
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
684
|
+
return {
|
|
685
|
+
publicUrl: rawUrl,
|
|
686
|
+
contentType: contentType ?? undefined,
|
|
687
|
+
fileName,
|
|
688
|
+
sourceKind: "remote",
|
|
689
|
+
sourceUrl: rawUrl,
|
|
690
|
+
provider: "direct",
|
|
691
|
+
};
|
|
653
692
|
}
|
|
654
693
|
|
|
655
694
|
const maxBytes = resolveMediaMaxBytes(params.account);
|
|
656
695
|
let buffer: Buffer;
|
|
657
696
|
let contentType: string | undefined;
|
|
658
697
|
let fileName: string | undefined;
|
|
698
|
+
let sourceLocalPath: string | undefined;
|
|
659
699
|
|
|
660
|
-
if (
|
|
700
|
+
if (isRemote) {
|
|
661
701
|
const fetched = await core.channel.media.fetchRemoteMedia({
|
|
662
702
|
url: rawUrl,
|
|
663
703
|
maxBytes,
|
|
@@ -671,6 +711,41 @@ async function stageMedia(params: {
|
|
|
671
711
|
buffer = await fs.readFile(localPath);
|
|
672
712
|
contentType = await core.media.detectMime({ buffer, filePath: localPath });
|
|
673
713
|
fileName = path.basename(localPath);
|
|
714
|
+
sourceLocalPath = localPath;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (hasS3(params.account)) {
|
|
718
|
+
try {
|
|
719
|
+
const s3Config = resolveS3Config(params.account.config);
|
|
720
|
+
if (!s3Config) throw new Error("s3 not configured");
|
|
721
|
+
const uploaded = await uploadToS3({
|
|
722
|
+
config: s3Config,
|
|
723
|
+
accountId: params.account.accountId,
|
|
724
|
+
buffer,
|
|
725
|
+
contentType,
|
|
726
|
+
fileName,
|
|
727
|
+
});
|
|
728
|
+
return {
|
|
729
|
+
publicUrl: uploaded.url,
|
|
730
|
+
contentType,
|
|
731
|
+
fileName,
|
|
732
|
+
localPath: sourceLocalPath,
|
|
733
|
+
sourceKind: isRemote ? "remote" : "local",
|
|
734
|
+
sourceUrl: rawUrl,
|
|
735
|
+
provider: "s3",
|
|
736
|
+
};
|
|
737
|
+
} catch (err) {
|
|
738
|
+
if (!hasProxyBase(params.account)) {
|
|
739
|
+
throw new Error(`s3 upload failed and proxy fallback unavailable: ${String(err)}`);
|
|
740
|
+
}
|
|
741
|
+
const logger = core.logging.getChildLogger({ channel: CHANNEL_ID, module: "media" });
|
|
742
|
+
logger.warn?.(`gewe s3 upload failed, fallback to proxy: ${String(err)}`);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const publicBase = params.account.config.mediaPublicUrl?.trim();
|
|
747
|
+
if (!publicBase) {
|
|
748
|
+
throw new Error("mediaPublicUrl not configured (required for proxy fallback)");
|
|
674
749
|
}
|
|
675
750
|
|
|
676
751
|
const saved = await core.channel.media.saveMediaBuffer(
|
|
@@ -684,8 +759,7 @@ async function stageMedia(params: {
|
|
|
684
759
|
let resolvedId = saved.id;
|
|
685
760
|
let resolvedPath = saved.path;
|
|
686
761
|
const desiredExt =
|
|
687
|
-
extensionForMime(contentType ?? saved.contentType) ||
|
|
688
|
-
path.extname(resolvedFileName);
|
|
762
|
+
extensionForMime(contentType ?? saved.contentType) || path.extname(resolvedFileName);
|
|
689
763
|
if (desiredExt && !path.extname(resolvedId)) {
|
|
690
764
|
const nextId = `${resolvedId}${desiredExt}`;
|
|
691
765
|
const nextPath = path.join(path.dirname(saved.path), nextId);
|
|
@@ -697,7 +771,10 @@ async function stageMedia(params: {
|
|
|
697
771
|
publicUrl: buildPublicUrl(publicBase, resolvedId),
|
|
698
772
|
contentType: contentType ?? saved.contentType,
|
|
699
773
|
fileName: resolvedFileName || resolvedId,
|
|
700
|
-
localPath: resolvedPath,
|
|
774
|
+
localPath: sourceLocalPath ?? resolvedPath,
|
|
775
|
+
sourceKind: isRemote ? "remote" : "local",
|
|
776
|
+
sourceUrl: rawUrl,
|
|
777
|
+
provider: "proxy",
|
|
701
778
|
};
|
|
702
779
|
}
|
|
703
780
|
|
|
@@ -716,6 +793,29 @@ async function resolvePublicUrl(params: {
|
|
|
716
793
|
return staged.publicUrl;
|
|
717
794
|
}
|
|
718
795
|
|
|
796
|
+
function shouldRetryWithStagedFallback(params: {
|
|
797
|
+
originalUrl: string;
|
|
798
|
+
staged: ResolvedMedia;
|
|
799
|
+
account: ResolvedGeweAccount;
|
|
800
|
+
}): boolean {
|
|
801
|
+
if (!looksLikeHttpUrl(params.originalUrl)) return false;
|
|
802
|
+
if (params.staged.provider !== "direct") return false;
|
|
803
|
+
return resolveFallbackProviders(params.account).length > 0;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
async function stageFallbackFromRemote(params: {
|
|
807
|
+
account: ResolvedGeweAccount;
|
|
808
|
+
cfg: OpenClawConfig;
|
|
809
|
+
originalUrl: string;
|
|
810
|
+
}): Promise<ResolvedMedia> {
|
|
811
|
+
return await stageMedia({
|
|
812
|
+
account: params.account,
|
|
813
|
+
cfg: params.cfg,
|
|
814
|
+
mediaUrl: params.originalUrl,
|
|
815
|
+
allowRemote: false,
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
|
|
719
819
|
export async function deliverGewePayload(params: {
|
|
720
820
|
payload: ReplyPayload;
|
|
721
821
|
account: ResolvedGeweAccount;
|
|
@@ -774,12 +874,34 @@ export async function deliverGewePayload(params: {
|
|
|
774
874
|
const declaredDuration = resolveVoiceDurationMs(geweData);
|
|
775
875
|
if (isSilkAudio({ contentType, fileName })) {
|
|
776
876
|
if (declaredDuration) {
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
877
|
+
let result: GeweSendResult;
|
|
878
|
+
try {
|
|
879
|
+
result = await sendVoiceGewe({
|
|
880
|
+
account,
|
|
881
|
+
toWxid,
|
|
882
|
+
voiceUrl: staged.publicUrl,
|
|
883
|
+
voiceDuration: declaredDuration,
|
|
884
|
+
});
|
|
885
|
+
} catch (err) {
|
|
886
|
+
if (!shouldRetryWithStagedFallback({
|
|
887
|
+
originalUrl: normalizedMediaUrl,
|
|
888
|
+
staged,
|
|
889
|
+
account,
|
|
890
|
+
})) {
|
|
891
|
+
throw err;
|
|
892
|
+
}
|
|
893
|
+
const fallback = await stageFallbackFromRemote({
|
|
894
|
+
account,
|
|
895
|
+
cfg,
|
|
896
|
+
originalUrl: normalizedMediaUrl,
|
|
897
|
+
});
|
|
898
|
+
result = await sendVoiceGewe({
|
|
899
|
+
account,
|
|
900
|
+
toWxid,
|
|
901
|
+
voiceUrl: fallback.publicUrl,
|
|
902
|
+
voiceDuration: declaredDuration,
|
|
903
|
+
});
|
|
904
|
+
}
|
|
783
905
|
core.channel.activity.record({
|
|
784
906
|
channel: CHANNEL_ID,
|
|
785
907
|
accountId: account.accountId,
|
|
@@ -795,21 +917,56 @@ export async function deliverGewePayload(params: {
|
|
|
795
917
|
});
|
|
796
918
|
if (converted) {
|
|
797
919
|
const voiceDuration = declaredDuration ?? converted.durationMs;
|
|
798
|
-
|
|
799
|
-
if (
|
|
800
|
-
|
|
920
|
+
let voiceUrl: string;
|
|
921
|
+
if (hasS3(account)) {
|
|
922
|
+
try {
|
|
923
|
+
const s3Config = resolveS3Config(account.config);
|
|
924
|
+
if (!s3Config) throw new Error("s3 not configured");
|
|
925
|
+
const uploaded = await uploadToS3({
|
|
926
|
+
config: s3Config,
|
|
927
|
+
accountId: account.accountId,
|
|
928
|
+
buffer: converted.buffer,
|
|
929
|
+
contentType: "audio/silk",
|
|
930
|
+
fileName: "voice.silk",
|
|
931
|
+
});
|
|
932
|
+
voiceUrl = uploaded.url;
|
|
933
|
+
} catch (err) {
|
|
934
|
+
if (!hasProxyBase(account)) {
|
|
935
|
+
throw new Error(
|
|
936
|
+
`s3 silk upload failed and proxy fallback unavailable: ${String(err)}`,
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
const publicBase = account.config.mediaPublicUrl?.trim();
|
|
940
|
+
if (!publicBase) {
|
|
941
|
+
throw new Error("mediaPublicUrl not configured (required for silk voice)");
|
|
942
|
+
}
|
|
943
|
+
const saved = await core.channel.media.saveMediaBuffer(
|
|
944
|
+
converted.buffer,
|
|
945
|
+
"audio/silk",
|
|
946
|
+
"outbound",
|
|
947
|
+
resolveMediaMaxBytes(account),
|
|
948
|
+
"voice.silk",
|
|
949
|
+
);
|
|
950
|
+
voiceUrl = buildPublicUrl(publicBase, saved.id);
|
|
951
|
+
}
|
|
952
|
+
} else {
|
|
953
|
+
const publicBase = account.config.mediaPublicUrl?.trim();
|
|
954
|
+
if (!publicBase) {
|
|
955
|
+
throw new Error("mediaPublicUrl not configured (required for silk voice)");
|
|
956
|
+
}
|
|
957
|
+
const saved = await core.channel.media.saveMediaBuffer(
|
|
958
|
+
converted.buffer,
|
|
959
|
+
"audio/silk",
|
|
960
|
+
"outbound",
|
|
961
|
+
resolveMediaMaxBytes(account),
|
|
962
|
+
"voice.silk",
|
|
963
|
+
);
|
|
964
|
+
voiceUrl = buildPublicUrl(publicBase, saved.id);
|
|
801
965
|
}
|
|
802
|
-
const saved = await core.channel.media.saveMediaBuffer(
|
|
803
|
-
converted.buffer,
|
|
804
|
-
"audio/silk",
|
|
805
|
-
"outbound",
|
|
806
|
-
resolveMediaMaxBytes(account),
|
|
807
|
-
"voice.silk",
|
|
808
|
-
);
|
|
809
966
|
const result = await sendVoiceGewe({
|
|
810
967
|
account,
|
|
811
968
|
toWxid,
|
|
812
|
-
voiceUrl
|
|
969
|
+
voiceUrl,
|
|
813
970
|
voiceDuration,
|
|
814
971
|
});
|
|
815
972
|
core.channel.activity.record({
|
|
@@ -824,11 +981,32 @@ export async function deliverGewePayload(params: {
|
|
|
824
981
|
}
|
|
825
982
|
|
|
826
983
|
if (!forceFile && kind === "image") {
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
984
|
+
let result: GeweSendResult;
|
|
985
|
+
try {
|
|
986
|
+
result = await sendImageGewe({
|
|
987
|
+
account,
|
|
988
|
+
toWxid,
|
|
989
|
+
imgUrl: staged.publicUrl,
|
|
990
|
+
});
|
|
991
|
+
} catch (err) {
|
|
992
|
+
if (!shouldRetryWithStagedFallback({
|
|
993
|
+
originalUrl: normalizedMediaUrl,
|
|
994
|
+
staged,
|
|
995
|
+
account,
|
|
996
|
+
})) {
|
|
997
|
+
throw err;
|
|
998
|
+
}
|
|
999
|
+
const fallback = await stageFallbackFromRemote({
|
|
1000
|
+
account,
|
|
1001
|
+
cfg,
|
|
1002
|
+
originalUrl: normalizedMediaUrl,
|
|
1003
|
+
});
|
|
1004
|
+
result = await sendImageGewe({
|
|
1005
|
+
account,
|
|
1006
|
+
toWxid,
|
|
1007
|
+
imgUrl: fallback.publicUrl,
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
832
1010
|
core.channel.activity.record({
|
|
833
1011
|
channel: CHANNEL_ID,
|
|
834
1012
|
accountId: account.accountId,
|
|
@@ -902,6 +1080,11 @@ export async function deliverGewePayload(params: {
|
|
|
902
1080
|
url: thumbUrl,
|
|
903
1081
|
allowRemote: true,
|
|
904
1082
|
});
|
|
1083
|
+
const canRetryMediaFallback = shouldRetryWithStagedFallback({
|
|
1084
|
+
originalUrl: normalizedMediaUrl,
|
|
1085
|
+
staged: stagedVideo,
|
|
1086
|
+
account,
|
|
1087
|
+
});
|
|
905
1088
|
try {
|
|
906
1089
|
const result = await sendVideoGewe({
|
|
907
1090
|
account,
|
|
@@ -918,6 +1101,33 @@ export async function deliverGewePayload(params: {
|
|
|
918
1101
|
statusSink?.({ lastOutboundAt: Date.now() });
|
|
919
1102
|
return result;
|
|
920
1103
|
} catch (err) {
|
|
1104
|
+
if (canRetryMediaFallback) {
|
|
1105
|
+
const fallbackVideo = await stageFallbackFromRemote({
|
|
1106
|
+
account,
|
|
1107
|
+
cfg,
|
|
1108
|
+
originalUrl: normalizedMediaUrl,
|
|
1109
|
+
});
|
|
1110
|
+
const fallbackThumb = await resolvePublicUrl({
|
|
1111
|
+
account,
|
|
1112
|
+
cfg,
|
|
1113
|
+
url: thumbUrl,
|
|
1114
|
+
allowRemote: false,
|
|
1115
|
+
});
|
|
1116
|
+
const result = await sendVideoGewe({
|
|
1117
|
+
account,
|
|
1118
|
+
toWxid,
|
|
1119
|
+
videoUrl: fallbackVideo.publicUrl,
|
|
1120
|
+
thumbUrl: fallbackThumb,
|
|
1121
|
+
videoDuration: Math.floor(videoDuration),
|
|
1122
|
+
});
|
|
1123
|
+
core.channel.activity.record({
|
|
1124
|
+
channel: CHANNEL_ID,
|
|
1125
|
+
accountId: account.accountId,
|
|
1126
|
+
direction: "outbound",
|
|
1127
|
+
});
|
|
1128
|
+
statusSink?.({ lastOutboundAt: Date.now() });
|
|
1129
|
+
return result;
|
|
1130
|
+
}
|
|
921
1131
|
if (fallbackThumbUrl && fallbackThumbUrl !== thumbUrl) {
|
|
922
1132
|
logger.warn?.(
|
|
923
1133
|
`gewe video send failed with primary thumb, retrying fallback: ${String(err)}`,
|
|
@@ -952,12 +1162,34 @@ export async function deliverGewePayload(params: {
|
|
|
952
1162
|
geweData?.fileName ||
|
|
953
1163
|
fileName ||
|
|
954
1164
|
(contentType ? `file${contentType.includes("/") ? `.${contentType.split("/")[1]}` : ""}` : "file");
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1165
|
+
let result: GeweSendResult;
|
|
1166
|
+
try {
|
|
1167
|
+
result = await sendFileGewe({
|
|
1168
|
+
account,
|
|
1169
|
+
toWxid,
|
|
1170
|
+
fileUrl: staged.publicUrl,
|
|
1171
|
+
fileName: fallbackName,
|
|
1172
|
+
});
|
|
1173
|
+
} catch (err) {
|
|
1174
|
+
if (!shouldRetryWithStagedFallback({
|
|
1175
|
+
originalUrl: normalizedMediaUrl,
|
|
1176
|
+
staged,
|
|
1177
|
+
account,
|
|
1178
|
+
})) {
|
|
1179
|
+
throw err;
|
|
1180
|
+
}
|
|
1181
|
+
const fallback = await stageFallbackFromRemote({
|
|
1182
|
+
account,
|
|
1183
|
+
cfg,
|
|
1184
|
+
originalUrl: normalizedMediaUrl,
|
|
1185
|
+
});
|
|
1186
|
+
result = await sendFileGewe({
|
|
1187
|
+
account,
|
|
1188
|
+
toWxid,
|
|
1189
|
+
fileUrl: fallback.publicUrl,
|
|
1190
|
+
fileName: fallbackName,
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
961
1193
|
core.channel.activity.record({
|
|
962
1194
|
channel: CHANNEL_ID,
|
|
963
1195
|
accountId: account.accountId,
|
package/src/onboarding.ts
CHANGED
|
@@ -171,7 +171,7 @@ export const geweOnboarding: GeweOnboardingAdapter = {
|
|
|
171
171
|
"You will need:",
|
|
172
172
|
"- GeWe token + appId",
|
|
173
173
|
"- Public webhook endpoint (FRP or reverse proxy)",
|
|
174
|
-
"- Public media base URL (
|
|
174
|
+
"- Public media base URL (optional proxy fallback)",
|
|
175
175
|
].join("\n"),
|
|
176
176
|
"GeWe setup",
|
|
177
177
|
);
|
|
@@ -217,9 +217,109 @@ export const geweOnboarding: GeweOnboardingAdapter = {
|
|
|
217
217
|
message: "Media public URL (prefix)",
|
|
218
218
|
placeholder: "https://your-domain/gewe-media",
|
|
219
219
|
initialValue: existing.mediaPublicUrl,
|
|
220
|
-
validate: (value) => (value.trim() ? undefined : "Required"),
|
|
221
220
|
});
|
|
222
221
|
|
|
222
|
+
const enableS3 = await ctx.prompter.confirm({
|
|
223
|
+
message: "Enable S3-compatible media delivery?",
|
|
224
|
+
initialValue: existing.s3Enabled === true,
|
|
225
|
+
});
|
|
226
|
+
let s3Patch: Partial<GeweAccountConfig> = {};
|
|
227
|
+
if (enableS3) {
|
|
228
|
+
const s3Endpoint = await ctx.prompter.text({
|
|
229
|
+
message: "S3 endpoint",
|
|
230
|
+
placeholder: "https://s3.amazonaws.com",
|
|
231
|
+
initialValue: existing.s3Endpoint,
|
|
232
|
+
validate: (value) => (value.trim() ? undefined : "Required"),
|
|
233
|
+
});
|
|
234
|
+
const s3Region = await ctx.prompter.text({
|
|
235
|
+
message: "S3 region",
|
|
236
|
+
placeholder: "us-east-1",
|
|
237
|
+
initialValue: existing.s3Region,
|
|
238
|
+
validate: (value) => (value.trim() ? undefined : "Required"),
|
|
239
|
+
});
|
|
240
|
+
const s3Bucket = await ctx.prompter.text({
|
|
241
|
+
message: "S3 bucket",
|
|
242
|
+
initialValue: existing.s3Bucket,
|
|
243
|
+
validate: (value) => (value.trim() ? undefined : "Required"),
|
|
244
|
+
});
|
|
245
|
+
const s3AccessKeyId = await ctx.prompter.text({
|
|
246
|
+
message: "S3 access key id",
|
|
247
|
+
initialValue: existing.s3AccessKeyId,
|
|
248
|
+
validate: (value) => (value.trim() ? undefined : "Required"),
|
|
249
|
+
});
|
|
250
|
+
const s3SecretAccessKey = await ctx.prompter.text({
|
|
251
|
+
message: "S3 secret access key",
|
|
252
|
+
initialValue: existing.s3SecretAccessKey,
|
|
253
|
+
validate: (value) => (value.trim() ? undefined : "Required"),
|
|
254
|
+
});
|
|
255
|
+
const s3SessionToken = await ctx.prompter.text({
|
|
256
|
+
message: "S3 session token (optional)",
|
|
257
|
+
initialValue: existing.s3SessionToken,
|
|
258
|
+
});
|
|
259
|
+
const s3ForcePathStyle = await ctx.prompter.confirm({
|
|
260
|
+
message: "Use path-style for S3 endpoint?",
|
|
261
|
+
initialValue: existing.s3ForcePathStyle === true,
|
|
262
|
+
});
|
|
263
|
+
const s3KeyPrefix = await ctx.prompter.text({
|
|
264
|
+
message: "S3 key prefix (optional)",
|
|
265
|
+
placeholder: "gewe-openclaw/outbound",
|
|
266
|
+
initialValue: existing.s3KeyPrefix,
|
|
267
|
+
});
|
|
268
|
+
const s3UrlMode = await ctx.prompter.select({
|
|
269
|
+
message: "S3 URL mode",
|
|
270
|
+
options: [
|
|
271
|
+
{ value: "public", label: "public (default)" },
|
|
272
|
+
{ value: "presigned", label: "presigned" },
|
|
273
|
+
],
|
|
274
|
+
initialValue: existing.s3UrlMode ?? "public",
|
|
275
|
+
});
|
|
276
|
+
const s3PublicBaseUrl =
|
|
277
|
+
s3UrlMode === "public"
|
|
278
|
+
? await ctx.prompter.text({
|
|
279
|
+
message: "S3 public base URL",
|
|
280
|
+
placeholder: "https://cdn.example.com/gewe-media",
|
|
281
|
+
initialValue: existing.s3PublicBaseUrl,
|
|
282
|
+
validate: (value) => (value.trim() ? undefined : "Required"),
|
|
283
|
+
})
|
|
284
|
+
: await ctx.prompter.text({
|
|
285
|
+
message: "S3 public base URL (optional in presigned mode)",
|
|
286
|
+
initialValue: existing.s3PublicBaseUrl,
|
|
287
|
+
});
|
|
288
|
+
const s3PresignExpiresSecRaw =
|
|
289
|
+
s3UrlMode === "presigned"
|
|
290
|
+
? await ctx.prompter.text({
|
|
291
|
+
message: "Presigned URL expire seconds",
|
|
292
|
+
initialValue: String(existing.s3PresignExpiresSec ?? 3600),
|
|
293
|
+
validate: (value) => {
|
|
294
|
+
const parsed = Number(value);
|
|
295
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
296
|
+
return "Must be a positive integer";
|
|
297
|
+
}
|
|
298
|
+
return undefined;
|
|
299
|
+
},
|
|
300
|
+
})
|
|
301
|
+
: "";
|
|
302
|
+
s3Patch = {
|
|
303
|
+
s3Enabled: true,
|
|
304
|
+
s3Endpoint: s3Endpoint.trim(),
|
|
305
|
+
s3Region: s3Region.trim(),
|
|
306
|
+
s3Bucket: s3Bucket.trim(),
|
|
307
|
+
s3AccessKeyId: s3AccessKeyId.trim(),
|
|
308
|
+
s3SecretAccessKey: s3SecretAccessKey.trim(),
|
|
309
|
+
s3SessionToken: s3SessionToken.trim() || undefined,
|
|
310
|
+
s3ForcePathStyle,
|
|
311
|
+
s3KeyPrefix: s3KeyPrefix.trim() || undefined,
|
|
312
|
+
s3UrlMode,
|
|
313
|
+
s3PublicBaseUrl: s3PublicBaseUrl.trim() || undefined,
|
|
314
|
+
s3PresignExpiresSec:
|
|
315
|
+
s3UrlMode === "presigned" ? Number(s3PresignExpiresSecRaw) : undefined,
|
|
316
|
+
};
|
|
317
|
+
} else {
|
|
318
|
+
s3Patch = {
|
|
319
|
+
s3Enabled: false,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
223
323
|
let allowFrom = existing.allowFrom;
|
|
224
324
|
let dmPolicy: GeweAccountConfig["dmPolicy"] | undefined;
|
|
225
325
|
if (ctx.forceAllowFrom) {
|
|
@@ -255,7 +355,8 @@ export const geweOnboarding: GeweOnboardingAdapter = {
|
|
|
255
355
|
mediaHost: existing.mediaHost ?? DEFAULT_MEDIA_HOST,
|
|
256
356
|
mediaPort: existing.mediaPort ?? DEFAULT_MEDIA_PORT,
|
|
257
357
|
mediaPath: existing.mediaPath ?? DEFAULT_MEDIA_PATH,
|
|
258
|
-
mediaPublicUrl: mediaPublicUrl.trim(),
|
|
358
|
+
mediaPublicUrl: mediaPublicUrl.trim() || undefined,
|
|
359
|
+
...s3Patch,
|
|
259
360
|
...(allowFrom ? { allowFrom } : {}),
|
|
260
361
|
...(dmPolicy ? { dmPolicy } : {}),
|
|
261
362
|
});
|
package/src/s3.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
|
5
|
+
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
6
|
+
import { extensionForMime } from "openclaw/plugin-sdk";
|
|
7
|
+
|
|
8
|
+
import type { GeweAccountConfig } from "./types.js";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_S3_KEY_PREFIX = "gewe-openclaw/outbound";
|
|
11
|
+
const DEFAULT_PRESIGN_EXPIRES_SEC = 3600;
|
|
12
|
+
|
|
13
|
+
export type ResolvedS3Config = {
|
|
14
|
+
endpoint: string;
|
|
15
|
+
region: string;
|
|
16
|
+
bucket: string;
|
|
17
|
+
accessKeyId: string;
|
|
18
|
+
secretAccessKey: string;
|
|
19
|
+
sessionToken?: string;
|
|
20
|
+
forcePathStyle: boolean;
|
|
21
|
+
publicBaseUrl?: string;
|
|
22
|
+
keyPrefix: string;
|
|
23
|
+
urlMode: "public" | "presigned";
|
|
24
|
+
presignExpiresSec: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function trimTrailingSlash(value: string): string {
|
|
28
|
+
return value.replace(/\/+$/, "");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizePrefix(value?: string): string {
|
|
32
|
+
const raw = value?.trim() || DEFAULT_S3_KEY_PREFIX;
|
|
33
|
+
return raw.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function resolveS3Config(config: GeweAccountConfig): ResolvedS3Config | null {
|
|
37
|
+
if (config.s3Enabled !== true) return null;
|
|
38
|
+
const endpoint = config.s3Endpoint?.trim();
|
|
39
|
+
const region = config.s3Region?.trim();
|
|
40
|
+
const bucket = config.s3Bucket?.trim();
|
|
41
|
+
const accessKeyId = config.s3AccessKeyId?.trim();
|
|
42
|
+
const secretAccessKey = config.s3SecretAccessKey?.trim();
|
|
43
|
+
if (!endpoint || !region || !bucket || !accessKeyId || !secretAccessKey) {
|
|
44
|
+
throw new Error("s3Enabled=true but S3 credentials or endpoint is incomplete");
|
|
45
|
+
}
|
|
46
|
+
const urlMode = config.s3UrlMode ?? "public";
|
|
47
|
+
const publicBaseUrl = config.s3PublicBaseUrl?.trim();
|
|
48
|
+
if (urlMode === "public" && !publicBaseUrl) {
|
|
49
|
+
throw new Error("s3PublicBaseUrl is required when s3UrlMode=public");
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
endpoint: trimTrailingSlash(endpoint),
|
|
53
|
+
region,
|
|
54
|
+
bucket,
|
|
55
|
+
accessKeyId,
|
|
56
|
+
secretAccessKey,
|
|
57
|
+
sessionToken: config.s3SessionToken?.trim() || undefined,
|
|
58
|
+
forcePathStyle: config.s3ForcePathStyle === true,
|
|
59
|
+
publicBaseUrl: publicBaseUrl ? trimTrailingSlash(publicBaseUrl) : undefined,
|
|
60
|
+
keyPrefix: normalizePrefix(config.s3KeyPrefix),
|
|
61
|
+
urlMode,
|
|
62
|
+
presignExpiresSec:
|
|
63
|
+
config.s3PresignExpiresSec && config.s3PresignExpiresSec > 0
|
|
64
|
+
? Math.floor(config.s3PresignExpiresSec)
|
|
65
|
+
: DEFAULT_PRESIGN_EXPIRES_SEC,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function createClient(config: ResolvedS3Config): S3Client {
|
|
70
|
+
return new S3Client({
|
|
71
|
+
endpoint: config.endpoint,
|
|
72
|
+
region: config.region,
|
|
73
|
+
forcePathStyle: config.forcePathStyle,
|
|
74
|
+
credentials: {
|
|
75
|
+
accessKeyId: config.accessKeyId,
|
|
76
|
+
secretAccessKey: config.secretAccessKey,
|
|
77
|
+
...(config.sessionToken ? { sessionToken: config.sessionToken } : {}),
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function inferExtension(fileName?: string, contentType?: string): string {
|
|
83
|
+
const byName = fileName ? path.extname(fileName).toLowerCase() : "";
|
|
84
|
+
if (byName) return byName;
|
|
85
|
+
const byMime = contentType ? extensionForMime(contentType) : "";
|
|
86
|
+
return byMime || "";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function buildS3ObjectKey(params: {
|
|
90
|
+
accountId: string;
|
|
91
|
+
config: ResolvedS3Config;
|
|
92
|
+
fileName?: string;
|
|
93
|
+
contentType?: string;
|
|
94
|
+
}): string {
|
|
95
|
+
const now = new Date();
|
|
96
|
+
const yyyy = String(now.getUTCFullYear());
|
|
97
|
+
const mm = String(now.getUTCMonth() + 1).padStart(2, "0");
|
|
98
|
+
const dd = String(now.getUTCDate()).padStart(2, "0");
|
|
99
|
+
const ext = inferExtension(params.fileName, params.contentType);
|
|
100
|
+
return [
|
|
101
|
+
params.config.keyPrefix,
|
|
102
|
+
params.accountId,
|
|
103
|
+
yyyy,
|
|
104
|
+
mm,
|
|
105
|
+
dd,
|
|
106
|
+
`${randomUUID()}${ext}`,
|
|
107
|
+
].join("/");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function buildPublicUrl(config: ResolvedS3Config, key: string): string {
|
|
111
|
+
if (!config.publicBaseUrl) {
|
|
112
|
+
throw new Error("s3PublicBaseUrl missing");
|
|
113
|
+
}
|
|
114
|
+
return `${config.publicBaseUrl}/${key.split("/").map(encodeURIComponent).join("/")}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function uploadToS3(params: {
|
|
118
|
+
config: ResolvedS3Config;
|
|
119
|
+
accountId: string;
|
|
120
|
+
buffer: Buffer;
|
|
121
|
+
contentType?: string;
|
|
122
|
+
fileName?: string;
|
|
123
|
+
}): Promise<{ key: string; url: string }> {
|
|
124
|
+
const key = buildS3ObjectKey({
|
|
125
|
+
accountId: params.accountId,
|
|
126
|
+
config: params.config,
|
|
127
|
+
fileName: params.fileName,
|
|
128
|
+
contentType: params.contentType,
|
|
129
|
+
});
|
|
130
|
+
const client = createClient(params.config);
|
|
131
|
+
const put = new PutObjectCommand({
|
|
132
|
+
Bucket: params.config.bucket,
|
|
133
|
+
Key: key,
|
|
134
|
+
Body: params.buffer,
|
|
135
|
+
...(params.contentType ? { ContentType: params.contentType } : {}),
|
|
136
|
+
});
|
|
137
|
+
await client.send(put);
|
|
138
|
+
if (params.config.urlMode === "public") {
|
|
139
|
+
return { key, url: buildPublicUrl(params.config, key) };
|
|
140
|
+
}
|
|
141
|
+
const getCommand = new GetObjectCommand({
|
|
142
|
+
Bucket: params.config.bucket,
|
|
143
|
+
Key: key,
|
|
144
|
+
});
|
|
145
|
+
const url = await getSignedUrl(client, getCommand, {
|
|
146
|
+
expiresIn: params.config.presignExpiresSec,
|
|
147
|
+
});
|
|
148
|
+
return { key, url };
|
|
149
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -33,6 +33,18 @@ export type GeweAccountConfig = {
|
|
|
33
33
|
mediaPath?: string;
|
|
34
34
|
mediaPublicUrl?: string;
|
|
35
35
|
mediaMaxMb?: number;
|
|
36
|
+
s3Enabled?: boolean;
|
|
37
|
+
s3Endpoint?: string;
|
|
38
|
+
s3Region?: string;
|
|
39
|
+
s3Bucket?: string;
|
|
40
|
+
s3AccessKeyId?: string;
|
|
41
|
+
s3SecretAccessKey?: string;
|
|
42
|
+
s3SessionToken?: string;
|
|
43
|
+
s3ForcePathStyle?: boolean;
|
|
44
|
+
s3PublicBaseUrl?: string;
|
|
45
|
+
s3KeyPrefix?: string;
|
|
46
|
+
s3UrlMode?: "public" | "presigned";
|
|
47
|
+
s3PresignExpiresSec?: number;
|
|
36
48
|
voiceAutoConvert?: boolean;
|
|
37
49
|
voiceFfmpegPath?: string;
|
|
38
50
|
voiceSilkPath?: string;
|