koishi-plugin-tx-cos 0.0.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/lib/index.d.ts +14 -0
- package/lib/index.js +130 -0
- package/package.json +34 -0
- package/readme.md +152 -0
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import Assets from '@koishijs/assets';
|
|
2
|
+
import { Context, Schema } from 'koishi';
|
|
3
|
+
export declare const name = "tx-cos";
|
|
4
|
+
export declare const inject: string[];
|
|
5
|
+
export interface Config extends Assets.Config {
|
|
6
|
+
secretId: string;
|
|
7
|
+
secretKey: string;
|
|
8
|
+
bucket: string;
|
|
9
|
+
region: string;
|
|
10
|
+
baseUrl: string;
|
|
11
|
+
prefix?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare const Config: Schema<Config>;
|
|
14
|
+
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name2 in all)
|
|
10
|
+
__defProp(target, name2, { get: all[name2], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
Config: () => Config,
|
|
34
|
+
apply: () => apply,
|
|
35
|
+
inject: () => inject,
|
|
36
|
+
name: () => name
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(src_exports);
|
|
39
|
+
var import_assets = __toESM(require("@koishijs/assets"));
|
|
40
|
+
var import_cos_nodejs_sdk_v5 = __toESM(require("cos-nodejs-sdk-v5"));
|
|
41
|
+
var import_koishi = require("koishi");
|
|
42
|
+
var import_path = require("path");
|
|
43
|
+
var name = "tx-cos";
|
|
44
|
+
var inject = ["http"];
|
|
45
|
+
var Config = import_koishi.Schema.intersect([
|
|
46
|
+
import_koishi.Schema.object({
|
|
47
|
+
secretId: import_koishi.Schema.string().required().role("secret").description("腾讯云 COS SecretId。"),
|
|
48
|
+
secretKey: import_koishi.Schema.string().required().role("secret").description("腾讯云 COS SecretKey。"),
|
|
49
|
+
bucket: import_koishi.Schema.string().required().description("腾讯云 COS 存储桶名称,格式为 bucket-appid。"),
|
|
50
|
+
region: import_koishi.Schema.string().required().description("腾讯云 COS 存储桶地域,例如 ap-guangzhou。"),
|
|
51
|
+
baseUrl: import_koishi.Schema.string().required().role("link").description("CDN 或 COS 公网访问根地址。"),
|
|
52
|
+
prefix: import_koishi.Schema.string().default("koishi-assets").description("上传到 COS 时使用的对象前缀。")
|
|
53
|
+
}),
|
|
54
|
+
import_assets.default.Config
|
|
55
|
+
]);
|
|
56
|
+
function apply(ctx, config) {
|
|
57
|
+
ctx.plugin(TxCosAssets, config);
|
|
58
|
+
}
|
|
59
|
+
__name(apply, "apply");
|
|
60
|
+
var TxCosAssets = class extends import_assets.default {
|
|
61
|
+
static {
|
|
62
|
+
__name(this, "TxCosAssets");
|
|
63
|
+
}
|
|
64
|
+
static inject = ["http"];
|
|
65
|
+
client;
|
|
66
|
+
baseUrl;
|
|
67
|
+
prefix;
|
|
68
|
+
uploadedKeys = /* @__PURE__ */ new Set();
|
|
69
|
+
_stats = {
|
|
70
|
+
assetCount: 0,
|
|
71
|
+
assetSize: 0
|
|
72
|
+
};
|
|
73
|
+
constructor(ctx, config) {
|
|
74
|
+
super(ctx, {
|
|
75
|
+
...config,
|
|
76
|
+
whitelist: config.whitelist ?? []
|
|
77
|
+
});
|
|
78
|
+
this.client = new import_cos_nodejs_sdk_v5.default({
|
|
79
|
+
SecretId: config.secretId,
|
|
80
|
+
SecretKey: config.secretKey
|
|
81
|
+
});
|
|
82
|
+
this.baseUrl = (0, import_koishi.trimSlash)(config.baseUrl);
|
|
83
|
+
this.prefix = this.normalizePrefix(config.prefix || "koishi-assets");
|
|
84
|
+
}
|
|
85
|
+
normalizePrefix(prefix) {
|
|
86
|
+
return prefix.split("/").map((segment) => this.safeName(segment)).filter(Boolean).join("/");
|
|
87
|
+
}
|
|
88
|
+
safeName(name2) {
|
|
89
|
+
return name2.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
90
|
+
}
|
|
91
|
+
safeFile(file) {
|
|
92
|
+
const name2 = this.safeName((0, import_path.basename)(file || ""));
|
|
93
|
+
return name2 || void 0;
|
|
94
|
+
}
|
|
95
|
+
getKey(filename) {
|
|
96
|
+
return this.prefix ? `${this.prefix}/${filename}` : filename;
|
|
97
|
+
}
|
|
98
|
+
getUrl(key) {
|
|
99
|
+
return `${this.baseUrl}/${key}`;
|
|
100
|
+
}
|
|
101
|
+
async upload(url, file = "") {
|
|
102
|
+
if (url.startsWith(`${this.baseUrl}/`)) return url;
|
|
103
|
+
const { buffer, filename, type } = await this.analyze(url, this.safeFile(file));
|
|
104
|
+
const key = this.getKey(filename);
|
|
105
|
+
const publicUrl = this.getUrl(key);
|
|
106
|
+
if (this.uploadedKeys.has(key)) return publicUrl;
|
|
107
|
+
await this.client.putObject({
|
|
108
|
+
Bucket: this.config.bucket,
|
|
109
|
+
Region: this.config.region,
|
|
110
|
+
Key: key,
|
|
111
|
+
Body: buffer,
|
|
112
|
+
ContentLength: buffer.byteLength,
|
|
113
|
+
ContentType: type
|
|
114
|
+
});
|
|
115
|
+
this.uploadedKeys.add(key);
|
|
116
|
+
this._stats.assetCount += 1;
|
|
117
|
+
this._stats.assetSize += buffer.byteLength;
|
|
118
|
+
return publicUrl;
|
|
119
|
+
}
|
|
120
|
+
async stats() {
|
|
121
|
+
return this._stats;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
125
|
+
0 && (module.exports = {
|
|
126
|
+
Config,
|
|
127
|
+
apply,
|
|
128
|
+
inject,
|
|
129
|
+
name
|
|
130
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "koishi-plugin-tx-cos",
|
|
3
|
+
"description": "来用腾讯cos作为assets服务吧",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"typings": "lib/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"lib",
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"chatbot",
|
|
14
|
+
"koishi",
|
|
15
|
+
"plugin"
|
|
16
|
+
],
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"koishi": "4.18.11"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@koishijs/assets": "^1.1.2",
|
|
22
|
+
"cos-nodejs-sdk-v5": "^2.15.4"
|
|
23
|
+
},
|
|
24
|
+
"koishi": {
|
|
25
|
+
"service": {
|
|
26
|
+
"required": [
|
|
27
|
+
"http"
|
|
28
|
+
],
|
|
29
|
+
"implements": [
|
|
30
|
+
"assets"
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# koishi-plugin-tx-cos
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/koishi-plugin-tx-cos)
|
|
4
|
+
|
|
5
|
+
使用腾讯云对象存储(COS)作为 Koishi 的 `assets` 服务。插件会将消息中的图片、音频和视频上传到指定存储桶,并把资源地址替换为可公开访问的 COS 或 CDN URL。
|
|
6
|
+
|
|
7
|
+
## 功能特点
|
|
8
|
+
|
|
9
|
+
- 实现 Koishi `assets` 服务,可直接配合各类图片、音频和视频消息使用
|
|
10
|
+
- 支持 HTTP URL、Data URL 等 Koishi HTTP 服务能够读取的资源
|
|
11
|
+
- 使用文件内容的 SHA-1 摘要生成对象名,相同内容会得到稳定的存储路径
|
|
12
|
+
- 支持自定义对象路径前缀、COS 公网域名或 CDN 域名
|
|
13
|
+
- 支持 URL 白名单,指定资源可跳过上传
|
|
14
|
+
- 自动跳过已经指向当前 `baseUrl` 的资源,避免重复处理
|
|
15
|
+
|
|
16
|
+
## 使用要求
|
|
17
|
+
|
|
18
|
+
- Koishi 4.18
|
|
19
|
+
- 腾讯云 COS 存储桶
|
|
20
|
+
- 对目标存储桶具有对象写入权限的腾讯云 API 密钥
|
|
21
|
+
- 可公开读取对象的 COS 访问地址,或已正确配置回源的 CDN 域名
|
|
22
|
+
- 已启用 Koishi `http` 服务
|
|
23
|
+
|
|
24
|
+
插件返回的是不带临时签名的公开 URL,因此存储桶或 CDN 必须允许客户端读取对应对象。建议为插件使用权限最小化的独立密钥,不要将密钥提交到代码仓库。
|
|
25
|
+
|
|
26
|
+
## 安装
|
|
27
|
+
|
|
28
|
+
在 Koishi 插件市场搜索 `tx-cos`,或通过包管理器安装:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
yarn add koishi-plugin-tx-cos
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
启用插件并填写 COS 配置后,它会注册为当前 Koishi 实例的 `assets` 服务。一个 Koishi 实例通常只应启用一个 `assets` 服务实现,以免服务冲突。
|
|
35
|
+
|
|
36
|
+
## 腾讯云 COS 准备
|
|
37
|
+
|
|
38
|
+
1. 创建 COS 存储桶,并记下完整存储桶名称和地域。
|
|
39
|
+
2. 创建具有目标存储桶对象写入权限的 API 密钥,获取 `SecretId` 和 `SecretKey`。
|
|
40
|
+
3. 配置存储桶公开读取,或配置能够公开访问对象的 CDN 域名。
|
|
41
|
+
4. 将公网访问根地址填写到 `baseUrl`,不要包含对象路径前缀。
|
|
42
|
+
|
|
43
|
+
例如:
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
bucket: example-1250000000
|
|
47
|
+
region: ap-guangzhou
|
|
48
|
+
baseUrl: https://example-1250000000.cos.ap-guangzhou.myqcloud.com
|
|
49
|
+
prefix: koishi-assets
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
上传后的资源地址类似:
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
https://example-1250000000.cos.ap-guangzhou.myqcloud.com/koishi-assets/6c6f0e...a1.png
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
使用自定义 CDN 域名时,`baseUrl` 也可以填写为 `https://static.example.com`,但该域名必须正确回源到配置的存储桶和路径。
|
|
59
|
+
|
|
60
|
+
## 配置项
|
|
61
|
+
|
|
62
|
+
| 配置项 | 类型 | 默认值 | 说明 |
|
|
63
|
+
| --- | --- | --- | --- |
|
|
64
|
+
| `secretId` | `string` | 必填 | 腾讯云 API 密钥的 SecretId |
|
|
65
|
+
| `secretKey` | `string` | 必填 | 腾讯云 API 密钥的 SecretKey,控制台中会按敏感字段显示 |
|
|
66
|
+
| `bucket` | `string` | 必填 | 完整存储桶名称,格式为 `bucket-appid` |
|
|
67
|
+
| `region` | `string` | 必填 | 存储桶地域,例如 `ap-guangzhou` |
|
|
68
|
+
| `baseUrl` | `string` | 必填 | COS 公网访问或 CDN 根地址 |
|
|
69
|
+
| `prefix` | `string` | `koishi-assets` | 上传对象的路径前缀 |
|
|
70
|
+
| `whitelist` | `string[]` | `[]` | 不进行上传和地址替换的 URL 前缀列表 |
|
|
71
|
+
|
|
72
|
+
`baseUrl` 末尾的 `/` 会被自动处理。`prefix` 中不安全的字符会替换为 `-`,多级路径可以使用 `/` 分隔。
|
|
73
|
+
|
|
74
|
+
## 配置示例
|
|
75
|
+
|
|
76
|
+
推荐通过环境变量保存密钥:
|
|
77
|
+
|
|
78
|
+
```yaml
|
|
79
|
+
plugins:
|
|
80
|
+
tx-cos:
|
|
81
|
+
secretId: ${{env.COS_SECRET_ID}}
|
|
82
|
+
secretKey: ${{env.COS_SECRET_KEY}}
|
|
83
|
+
bucket: ${{env.COS_BUCKET}}
|
|
84
|
+
region: ${{env.COS_REGION}}
|
|
85
|
+
baseUrl: ${{env.COS_BASE_URL}}
|
|
86
|
+
prefix: koishi-assets
|
|
87
|
+
whitelist:
|
|
88
|
+
- https://trusted.example.com/static/
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
对应的环境变量示例:
|
|
92
|
+
|
|
93
|
+
```dotenv
|
|
94
|
+
COS_SECRET_ID=AKIDxxxxxxxxxxxxxxxx
|
|
95
|
+
COS_SECRET_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
96
|
+
COS_BUCKET=example-1250000000
|
|
97
|
+
COS_REGION=ap-guangzhou
|
|
98
|
+
COS_BASE_URL=https://static.example.com
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
请根据实际 Koishi 配置结构放置插件配置;控制台安装时可直接在插件配置页面填写这些字段。
|
|
102
|
+
|
|
103
|
+
## 工作方式
|
|
104
|
+
|
|
105
|
+
当 Koishi 处理消息中的 `<img>`、`<audio>` 或 `<video>` 元素时,插件会:
|
|
106
|
+
|
|
107
|
+
1. 检查资源 URL 是否命中 `whitelist`,或是否已经使用当前 `baseUrl`。
|
|
108
|
+
2. 通过 Koishi `http` 服务读取资源内容。
|
|
109
|
+
3. 根据内容计算 SHA-1 摘要,并结合传入的文件名或检测到的扩展名生成对象名。
|
|
110
|
+
4. 将对象上传到 `bucket` 的 `prefix` 路径下,同时保留资源的 Content-Type。
|
|
111
|
+
5. 返回公开 URL,替换消息中的原始资源地址。
|
|
112
|
+
|
|
113
|
+
同一进程内成功上传过的对象会记录在内存中,再次遇到同一对象时直接返回公开 URL。Bot 重启后这份记录会清空;由于对象名由内容生成,重复上传仍会写入同一个对象键。
|
|
114
|
+
|
|
115
|
+
## 白名单
|
|
116
|
+
|
|
117
|
+
`whitelist` 使用 URL 前缀匹配。命中白名单的资源会保留原地址,不会下载或上传到 COS。
|
|
118
|
+
|
|
119
|
+
```yaml
|
|
120
|
+
whitelist:
|
|
121
|
+
- https://q.qlogo.cn/
|
|
122
|
+
- https://third-party.example.com/public/
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
只有确认目标平台可以直接访问这些 URL 时才应加入白名单。
|
|
126
|
+
|
|
127
|
+
## 常见问题
|
|
128
|
+
|
|
129
|
+
### 上传成功但消息中的资源无法访问
|
|
130
|
+
|
|
131
|
+
检查以下项目:
|
|
132
|
+
|
|
133
|
+
- `baseUrl` 是否能公开访问,而不是腾讯云控制台内部地址。
|
|
134
|
+
- 存储桶是否允许读取对象,或 CDN 是否已正确配置回源和访问权限。
|
|
135
|
+
- CDN 回源路径是否与 `prefix` 一致。
|
|
136
|
+
- `bucket` 是否包含 AppId,`region` 是否与存储桶实际地域一致。
|
|
137
|
+
|
|
138
|
+
### 出现 `AccessDenied` 或签名错误
|
|
139
|
+
|
|
140
|
+
检查 `secretId`、`secretKey` 是否正确,以及密钥是否拥有向目标存储桶执行对象写入操作的权限。还应确认服务器时间准确,且 `bucket` 和 `region` 没有填错。
|
|
141
|
+
|
|
142
|
+
### 为什么统计数量与存储桶中的对象数不一致
|
|
143
|
+
|
|
144
|
+
插件的 `stats()` 只统计当前进程启动后成功执行的上传次数和字节数,不会读取 COS 中已有对象,也不会在 Bot 重启后保留统计结果。
|
|
145
|
+
|
|
146
|
+
### 是否会自动删除 COS 中的旧文件
|
|
147
|
+
|
|
148
|
+
不会。插件只负责上传和返回资源地址,不会清理对象。需要自动清理时,请在腾讯云 COS 中配置生命周期规则,并确认生命周期策略不会误删仍在使用的资源。
|
|
149
|
+
|
|
150
|
+
## 许可证
|
|
151
|
+
|
|
152
|
+
MIT
|