@w-xuefeng/cfib 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/.editorconfig ADDED
@@ -0,0 +1,13 @@
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ indent_size = 2
6
+ indent_style = space
7
+ insert_final_newline = true
8
+ trim_trailing_whitespace = true
9
+ end_of_line = lf
10
+
11
+
12
+ [*.md]
13
+ trim_trailing_whitespace = false
package/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # CFIB - Cloudflare Image Bed CLI & Agent Skills
2
+
3
+ > 💡 **特别说明与致敬**
4
+ >
5
+ > 本项目是对优秀的开源图床解决方案 **[CloudFlare-ImgBed](https://github.com/MarSeventh/CloudFlare-ImgBed)** 的生态扩展。十分感谢作者为大家提供如此出色的服务级支持!在此对原项目致以最诚挚的敬意。
6
+
7
+ **CFIB** 是原项目 **[CloudFlare-ImgBed](https://github.com/MarSeventh/CloudFlare-ImgBed)** 的一个由 [Bun](https://bun.sh/) 驱动的高效命令行工具实现,并且提供了 AI Agent Skills 集合,用于管理和操作搭建在 Cloudflare 上的图床资源。
8
+
9
+ 通过此项目,你可以在终端中快速实现图片的上传、远程文件的删除、随机图片的获取,同时它还在 `skills/` 目录下提供了标准化的 Agent Skills 描述文件,方便外部 AI Agent 直接学习并接入这些能力。
10
+
11
+ ## 📦 安装与使用
12
+
13
+ 确保您的环境中已安装 [Bun](https://bun.sh/) 运行时。
14
+
15
+ 你可以通过以下两种方式之一来使用本项目:
16
+
17
+ ### 选项 A:全局安装 (推荐)
18
+
19
+ 一键全局安装,方便你在系统的任意位置直接使用 `cfib` 唤起命令:
20
+
21
+ ```bash
22
+ bun install @w-xuefeng/cfib -g
23
+ ```
24
+
25
+ 安装成功后即可通过 `cfib -h` 查看帮助菜单。
26
+
27
+ ### 选项 B:免安装立即调用 (bunx)
28
+
29
+ 如果你不想进行全局安装,也可以利用 `bunx` 直接动态执行:
30
+
31
+ ```bash
32
+ bunx @w-xuefeng/cfib -h
33
+ ```
34
+
35
+ ### 3. 环境配置
36
+
37
+ 所有底层服务调用均需绑定目标图床地址和对应的凭证密钥。你可以通过创建 `.env` 文件或在每次使用命令时直接通过长参数(配合环境变量)覆盖。
38
+
39
+ **支持的全局参数(同时支持对应的环境变量):**
40
+ - `--origin` | `IB_ORIGIN` **(必配)**:图床服务的根 URL。
41
+ - `--upload-auth-code` | `IB_UPLOAD_AUTH_CODE` **(上传/删除 必配)**:访问及调用特权的身份验证 Cookie / Header 码。
42
+ - `--upload-token` | `IB_UPLOAD_TOKEN`:上传文件专用的 Token。
43
+ - `--delete-token` | `IB_DELETE_TOKEN`:删除远端资源的 Token。
44
+ - `--trace`:自定义日志与跨服务追踪的 `Trace-Id`。
45
+ - `--lang`:请求支持的 `Accept-Language`。
46
+ - `--log` / `--runtime-path` / `--log-root`:本地日志存储的路径设定。
47
+
48
+ ## 🛠 功能介绍
49
+
50
+ 获取详细介绍,通过执行:
51
+ ```bash
52
+ cfib -h
53
+ ```
54
+
55
+ ### 1. 图片上传 (`upload`)
56
+
57
+ 将本地文件流式上传至服务并直接从云端返回可用地址。支持定义高级上传设置如服务器压缩状态、存储频道(例如 telegram, S3 等)。
58
+
59
+ **用法:**
60
+ ```bash
61
+ cfib upload <file> [options]
62
+ ```
63
+ **示例:**
64
+ ```bash
65
+ cfib upload ./my-picture.png --origin "https://example.com" --upload-auth-code "xxxxxxxx"
66
+ ```
67
+
68
+ ### 2. 资源删除 (`remove`)
69
+
70
+ 移除图床中已保存的文件,或者配合 `--folder` 直接移除指定的文件夹。
71
+
72
+ **用法:**
73
+ ```bash
74
+ cfib remove <path> [options]
75
+ ```
76
+ **示例:**
77
+ ```bash
78
+ cfib remove "images/delete_this.png" --origin "https://example.com" --upload-auth-code "xxxxxxxx"
79
+ ```
80
+
81
+ ### 3. 获取随机图 (`random`)
82
+
83
+ 自图床中抽取一张随机媒体。既可以返回其对应的元数据(JSON),也可以通过 `--type img` 开启流响应将二进制内容直接落盘成你指定的本地实体图片:
84
+
85
+ **用法:**
86
+ ```bash
87
+ cfib random [dest] [options]
88
+ ```
89
+ **示例(直接下载):**
90
+ ```bash
91
+ cfib random output.jpg --type img --origin "https://example.com"
92
+ ```
93
+
94
+ ## 🤖 关于 AI Agent Skills
95
+
96
+ 在项目的 `skills/` 文件夹下包含了可以直接被 AI Agent(例如 Claude, AutoGPT 工具链)解析吸收的能力配置清单(`SKILL.md`)。
97
+
98
+ 这些说明文档内置了完整的调用说明,AI 助手在读取这些配置后便能安全、无障碍地为你执行上述所列的图床业务。
99
+
100
+ - `skills/upload/SKILL.md`
101
+ - `skills/remove/SKILL.md`
102
+ - `skills/random/SKILL.md`
package/bun.lock ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "configVersion": 1,
4
+ "workspaces": {
5
+ "": {
6
+ "name": "cloudflare-image-bed-skills",
7
+ "dependencies": {
8
+ "@w-xuefeng/bkit": "^0.0.5",
9
+ },
10
+ "devDependencies": {
11
+ "@types/bun": "latest",
12
+ "local-diamond": "^0.0.3",
13
+ },
14
+ "peerDependencies": {
15
+ "typescript": "^5",
16
+ },
17
+ },
18
+ },
19
+ "packages": {
20
+ "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
21
+
22
+ "@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
23
+
24
+ "@w-xuefeng/bkit": ["@w-xuefeng/bkit@0.0.5", "", { "dependencies": { "js-base64": "^3.7.8" }, "peerDependencies": { "typescript": "^5" } }, "sha512-r34CUMKuCHMeSqVVZWN0lKBeuAGXvftPk3JMfqtBPgumNkXYXx50xdNhE9JpcF5FXcLMXi1hUEUlBOgIxuml3A=="],
25
+
26
+ "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
27
+
28
+ "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
29
+
30
+ "hono": ["hono@4.12.9", "", {}, "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA=="],
31
+
32
+ "js-base64": ["js-base64@3.7.8", "", {}, "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow=="],
33
+
34
+ "local-diamond": ["local-diamond@0.0.3", "", { "dependencies": { "commander": "^14.0.3", "hono": "^4.12.2" }, "peerDependencies": { "typescript": "^5" }, "bin": { "lod": "src/bin.ts" } }, "sha512-xqia8ihwisnyRyxkQHekaS8xVElqYn7Pxb87s7OVexxb3EzmfIHxWflEC1ZwP7bvYkP01H5q8N2318W0e1s5pA=="],
35
+
36
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
37
+
38
+ "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
39
+ }
40
+ }
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@w-xuefeng/cfib",
3
+ "description": "cloudflare-image-bed CLI tools and Agent Skills",
4
+ "version": "0.0.1",
5
+ "module": "src/index.ts",
6
+ "type": "module",
7
+ "bin": {
8
+ "cfib": "src/index.ts"
9
+ },
10
+ "scripts": {
11
+ "test": "bun test/index.test.ts"
12
+ },
13
+ "devDependencies": {
14
+ "@types/bun": "latest",
15
+ "local-diamond": "^0.0.3"
16
+ },
17
+ "peerDependencies": {
18
+ "typescript": "^5"
19
+ },
20
+ "dependencies": {
21
+ "@w-xuefeng/bkit": "^0.0.5"
22
+ }
23
+ }
@@ -0,0 +1,48 @@
1
+ ---
2
+ description: Get a random image from the Cloudflare Image Bed
3
+ ---
4
+
5
+ # Random Image Skill
6
+
7
+ This tool fetches a random file or image from the Cloudflare Image Bed server. It can either return the JSON metadata or download the actual file.
8
+
9
+ ## Usage
10
+
11
+ Use the `run_command` tool to execute the cfib script. If the package is installed globally, you can use `cfib`. Otherwise, use `bunx cfib` or `npx cfib`:
12
+
13
+ ```bash
14
+ cfib random [options] [local_save_path]
15
+ # OR
16
+ bunx @w-xuefeng/cfib random [options] [local_save_path]
17
+ ```
18
+
19
+ ### Options (All Optional):
20
+ - `[local_save_path]`: If you specify `--type img`, you must provide a local filepath to save the binary image data (defaults to `random-image.jpg`).
21
+ - `--type <path|img>`: (default: path) If set to `img`, returns and saves the file directly. If set to `path`, returns JSON info.
22
+ - `--content <image|video>`: (default: image) The type of content to filter.
23
+ - `--form <json|text>`: (default: json) The response data format.
24
+ - `--dir <path>`: Filter random files only within a specific server directory.
25
+ - `--orientation <landscape|portrait|square|auto>`: Filter by image orientation.
26
+
27
+ ### Global Required Options:
28
+ - `--origin <url>`: The origin URL of the image bed server.
29
+
30
+ ### Global Optional Options:
31
+ - `--trace <id>`: Set an explicit Trace-Id.
32
+ - `--lang <language>`: Set an explicit Accept-Language.
33
+ - `--log <true|false>`: Enable or disable logging.
34
+
35
+ ## Examples
36
+
37
+ Get JSON metadata of a random image:
38
+ ```bash
39
+ cfib random --origin "https://my-image-bed.com"
40
+ ```
41
+
42
+ Download a random image directly to `test.jpg`:
43
+ ```bash
44
+ cfib random test.jpg --type img --origin "https://my-image-bed.com"
45
+ ```
46
+
47
+ ## Success Criteria
48
+ Normally outputs JSON metadata of the file. If `--type img` is used, the image is saved to the provided local path and outputs `Image saved to <path>`.
@@ -0,0 +1,48 @@
1
+ ---
2
+ description: Remove an image or folder from the Cloudflare Image Bed
3
+ ---
4
+
5
+ # Remove Image or Folder Skill
6
+
7
+ This tool allows you to delete an existing file or directory from the Cloudflare Image Bed server.
8
+
9
+ ## Usage
10
+
11
+ Use the `run_command` tool to execute the cfib script. If the package is installed globally, you can use `cfib`. Otherwise, use `bunx cfib` or `npx cfib`:
12
+
13
+ ```bash
14
+ cfib remove <remote_path> [options]
15
+ # OR
16
+ bunx @w-xuefeng/cfib remove <remote_path> [options]
17
+ ```
18
+
19
+ ### Required Arguments:
20
+ - `<remote_path>`: The path on the server referencing the file or folder.
21
+
22
+ ### Global Required Options:
23
+ - `--origin <url>`: The origin URL of the image bed server.
24
+ - `--upload-auth-code <code>`: The auth code cookie value.
25
+
26
+ ### Global Optional Options:
27
+ - `--delete-token <token>`: The authorization token required for deletion.
28
+ - `--trace <id>`: Set an explicit Trace-Id.
29
+ - `--lang <language>`: Set an explicit Accept-Language.
30
+ - `--log <true|false>`: Enable or disable logging.
31
+
32
+ ### Options (All Optional):
33
+ - `--folder`: Pass this flag if the `<remote_path>` is a folder and you want to delete the whole folder instead of a target file.
34
+
35
+ ## Example
36
+
37
+ Delete a single file:
38
+ ```bash
39
+ cfib remove "12345.png" --origin "https://my-image-bed.com" --upload-auth-code "my-auth-code" --delete-token "my-secret-token"
40
+ ```
41
+
42
+ Delete a full folder:
43
+ ```bash
44
+ cfib remove "my_folder_name" --folder --origin "https://my-image-bed.com" --upload-auth-code "my-auth-code"
45
+ ```
46
+
47
+ ## Success Criteria
48
+ The command outputs a JSON response with `success: true` when the file or folder is successfully deleted.
@@ -0,0 +1,45 @@
1
+ ---
2
+ description: Upload a local image to the Cloudflare Image Bed
3
+ ---
4
+
5
+ # Upload Image Skill
6
+
7
+ This tool allows you to upload a local generic file or image to a Cloudflare Image Bed instance.
8
+
9
+ ## Usage
10
+
11
+ Use the `run_command` tool to execute the cfib script. If the package is installed globally, you can use `cfib`. Otherwise, use `bunx cfib` or `npx cfib`:
12
+
13
+ ```bash
14
+ cfib upload <local_filepath> [options]
15
+ # OR
16
+ bunx @w-xuefeng/cfib upload <local_filepath> [options]
17
+ ```
18
+
19
+ ### Required Arguments:
20
+ - `<local_filepath>`: The absolute or relative path to the local file you wish to upload.
21
+
22
+ ### Global Required Options:
23
+ - `--origin <url>`: The origin URL of the image bed server.
24
+ - `--upload-auth-code <code>`: The auth code cookie value to bypass authentication.
25
+
26
+ ### Global Optional Environment & Tracing Options:
27
+ - `--upload-token <token>`: The authorization token for uploading.
28
+ - `--trace <id>`: Set an explicit Trace-Id.
29
+ - `--lang <language>`: Set an explicit Accept-Language.
30
+ - `--log <true|false>`: Enable or disable logging.
31
+
32
+ ### Upload Specific Options (All Optional):
33
+ - `--server-compress <true|false>`: Whether the server should compress the image.
34
+ - `--upload-channel <channel>`: The channel to use (telegram, cfr2, s3, discord, huggingface).
35
+ - `--upload-folder <folder>`: A specific folder on the server to upload the file to.
36
+ - `--auth-code <code>`: Specific authCode for upload.
37
+
38
+ ## Example
39
+
40
+ ```bash
41
+ cfib upload /path/to/my/image.png --origin "https://my-image-bed.com" --upload-auth-code "my-auth-code" --upload-token "my-token"
42
+ ```
43
+
44
+ ## Success Criteria
45
+ The command outputs a JSON response. A successful upload will contain a `success: true` and the `data.src` / `data.url` pointing to the uploaded file.
@@ -0,0 +1,194 @@
1
+ import { pathJoin, R } from "@w-xuefeng/bkit";
2
+ import { type Context, filterEmptyField, logger } from "./utils";
3
+
4
+ export interface IBUploadOptions {
5
+ authCode?: string;
6
+ serverCompress?: "true" | "false";
7
+ uploadChannel?: "telegram" | "cfr2" | "s3" | "discord" | "huggingface";
8
+ channelName?: string;
9
+ autoRetry?: "true" | "false";
10
+ uploadNameType?: "default" | "index" | "origin" | "short";
11
+ returnFormat?: "default" | "full";
12
+ uploadFolder?: string;
13
+ }
14
+
15
+ export interface IBRandomOptions {
16
+ /**
17
+ * @default "image"
18
+ * 文件类型过滤,可选值有 [image, video],多个使用 , 分隔
19
+ */
20
+ content?: "image" | "vidoe" | "image, video";
21
+ /**
22
+ * @default "path"
23
+ * 返回内容类型,设为 img 时直接返回图片(此时 form 不生效),设为 url 时返回完整 url 链接
24
+ */
25
+ type?: "path" | "img";
26
+ /**
27
+ * @default "json"
28
+ * 响应格式,设为 text 时直接返回文本
29
+ */
30
+ form?: "json" | "text";
31
+ /**
32
+ * 指定目录,使用相对路径,例如 img/test 会返回该目录以及所有子目录下的文件
33
+ */
34
+ dir?: string;
35
+ /**
36
+ * 图片方向筛选,可选值:landscape(横图)、portrait(竖图)、square(方图)、auto(自适应设备方向)
37
+ */
38
+ orientation?: "landscape" | "portrait" | "square" | "auto";
39
+ }
40
+
41
+ type IBUploadResponse = { src: string }[];
42
+
43
+ interface IBDeleteSingleFileResponse {
44
+ success: true;
45
+ fileId: string;
46
+ }
47
+
48
+ interface IBDeleteFolderResponse {
49
+ success: true;
50
+ deleted: string[];
51
+ failed: string[];
52
+ }
53
+
54
+ interface IBDeleteFolderErrorResponse {
55
+ success: false;
56
+ error: string;
57
+ }
58
+
59
+ type IBDeleteResponse =
60
+ | IBDeleteSingleFileResponse
61
+ | IBDeleteFolderResponse
62
+ | IBDeleteFolderErrorResponse;
63
+
64
+ export function upload(c: Context, file: File, options?: IBUploadOptions) {
65
+ const traceId = c.headers.get("Trace-Id");
66
+ const formData = new FormData();
67
+ formData.append("file", file);
68
+ const searchParams = new URLSearchParams(filterEmptyField({
69
+ uploadChannel: "telegram",
70
+ serverCompress: "true",
71
+ uploadNameType: "default",
72
+ returnFormat: "default",
73
+ autoRetry: "false",
74
+ ...options,
75
+ }));
76
+ const url = `${Bun.env.IB_ORIGIN}/upload?${searchParams.toString()}`;
77
+ logger.info(
78
+ `[trace-id: ${traceId}][image-bed upload params]: POST ${url} formData::[file:${file.name} ${file.size} ${file.type}]`,
79
+ );
80
+ return fetch(
81
+ url,
82
+ {
83
+ method: "POST",
84
+ body: formData,
85
+ headers: {
86
+ cookie: `authCode=${String(Bun.env.IB_UPLOAD_AUTH_CODE)}`,
87
+ authCode: String(Bun.env.IB_UPLOAD_AUTH_CODE),
88
+ referer: String(Bun.env.IB_ORIGIN),
89
+ Authorization: `Bearer ${Bun.env.IB_UPLOAD_TOKEN}`,
90
+ },
91
+ },
92
+ ).then((rs) => rs.json()).then((rs) => {
93
+ logger.info(
94
+ `[trace-id: ${traceId}][image-bed upload result]: ${JSON.stringify(rs)}`,
95
+ );
96
+ if (
97
+ !rs || !(rs as IBUploadResponse).length ||
98
+ !(rs as IBUploadResponse)[0]?.src
99
+ ) {
100
+ return R.unifail("FILE_UPLOAD_FAIL", rs, c.headers);
101
+ }
102
+ return R.unisuccess({
103
+ src: (rs as IBUploadResponse)[0]!.src,
104
+ url: pathJoin(
105
+ String(Bun.env.IB_ORIGIN),
106
+ (rs as IBUploadResponse)[0]!.src,
107
+ ),
108
+ }, c.headers);
109
+ }, (error) => {
110
+ logger.error(
111
+ `[trace-id: ${traceId}][image-bed upload error]: ${
112
+ JSON.stringify(error)
113
+ }`,
114
+ );
115
+ return R.unifail("SERVER_ERROR", error, c.headers);
116
+ });
117
+ }
118
+
119
+ export function remove(c: Context, path: string, folder: boolean = false) {
120
+ const traceId = c.headers?.get("Trace-Id");
121
+ if (path.startsWith(`${Bun.env.IB_ORIGIN}/`)) {
122
+ path = path.replace(`${Bun.env.IB_ORIGIN}/`, "");
123
+ }
124
+ if (path.startsWith("file/")) {
125
+ path = path.replace("file/", "");
126
+ }
127
+ if (path.startsWith("/file/")) {
128
+ path = path.replace("/file/", "");
129
+ }
130
+ return fetch(
131
+ `${Bun.env.IB_ORIGIN}/api/manage/delete/${path}${
132
+ folder ? "?folder=true" : ""
133
+ }`,
134
+ {
135
+ method: "DELETE",
136
+ headers: {
137
+ cookie: `authCode=${String(Bun.env.IB_UPLOAD_AUTH_CODE)}`,
138
+ authCode: String(Bun.env.IB_UPLOAD_AUTH_CODE),
139
+ referer: String(Bun.env.IB_ORIGIN),
140
+ Authorization: `Bearer ${Bun.env.IB_DELETE_TOKEN}`,
141
+ },
142
+ },
143
+ ).then((rs) => rs.json()).then((rs) => {
144
+ logger.info(
145
+ `[trace-id: ${traceId}][image-bed remove result]: ${JSON.stringify(rs)}`,
146
+ );
147
+ if (!rs || !(rs as IBDeleteResponse).success) {
148
+ return R.unifail("REQ_EXCEPTION", rs, c.headers);
149
+ }
150
+ return R.unisuccess(rs as IBDeleteResponse, c.headers);
151
+ }, (error) => {
152
+ logger.error(
153
+ `[trace-id: ${traceId}][image-bed remove error]: ${
154
+ JSON.stringify(error)
155
+ }`,
156
+ );
157
+ return R.unifail("SERVER_ERROR", error, c.headers);
158
+ });
159
+ }
160
+
161
+ export function random(c: Context, options?: IBRandomOptions) {
162
+ const traceId = c.headers.get("Trace-Id");
163
+ const searchParams = new URLSearchParams(filterEmptyField({
164
+ content: "image",
165
+ type: "path",
166
+ form: "json",
167
+ ...options,
168
+ }));
169
+ const url = `${Bun.env.IB_ORIGIN}/random?${searchParams.toString()}`;
170
+ logger.info(
171
+ `[trace-id: ${traceId}][image-bed random params]: GET ${url}`,
172
+ );
173
+ return fetch(url, {
174
+ method: "GET",
175
+ headers: {
176
+ referer: String(Bun.env.IB_ORIGIN),
177
+ },
178
+ }).then((rs) => {
179
+ if (options?.type === "img") {
180
+ return rs.blob();
181
+ }
182
+ if (options?.form === "text") {
183
+ return rs.text();
184
+ }
185
+ return rs.json();
186
+ }).catch((error) => {
187
+ logger.error(
188
+ `[trace-id: ${traceId}][image-bed random error]: ${
189
+ JSON.stringify(error)
190
+ }`,
191
+ );
192
+ return R.unifail("SERVER_ERROR", error, c.headers);
193
+ });
194
+ }
package/src/index.ts ADDED
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { parseArgs } from "node:util";
4
+ import { Context, initEvn, logger } from "./utils";
5
+ import {
6
+ type IBRandomOptions,
7
+ type IBUploadOptions,
8
+ random,
9
+ remove,
10
+ upload,
11
+ } from "./image-bed";
12
+
13
+ async function main() {
14
+ const args = process.argv.slice(2);
15
+ const { values, positionals } = parseArgs({
16
+ args,
17
+ options: {
18
+ origin: { type: "string" },
19
+ log: { type: "string" },
20
+ "runtime-path": { type: "string" },
21
+ "log-root": { type: "string" },
22
+ "upload-token": { type: "string" },
23
+ "delete-token": { type: "string" },
24
+ "upload-auth-code": { type: "string" },
25
+ trace: { type: "string" },
26
+ lang: { type: "string" },
27
+
28
+ // Upload Options
29
+ "auth-code": { type: "string" },
30
+ "server-compress": { type: "string" },
31
+ "upload-channel": { type: "string" },
32
+ "channel-name": { type: "string" },
33
+ "auto-retry": { type: "string" },
34
+ "upload-name-type": { type: "string" },
35
+ "return-format": { type: "string" },
36
+ "upload-folder": { type: "string" },
37
+
38
+ // Remove Options
39
+ folder: { type: "boolean" },
40
+
41
+ // Random Options
42
+ content: { type: "string" },
43
+ type: { type: "string" },
44
+ form: { type: "string" },
45
+ dir: { type: "string" },
46
+ orientation: { type: "string" },
47
+
48
+ help: { type: "boolean", short: "h" },
49
+ },
50
+ strict: false,
51
+ allowPositionals: true,
52
+ });
53
+
54
+ if (values.help || positionals.length === 0) {
55
+ console.log(`
56
+ Usage: cfib <command> [args] [options...]
57
+
58
+ Commands:
59
+ upload <file> (Required <file>) Upload a local file
60
+ remove <path> (Required <path>) Remove a remote file or folder
61
+ random [dest] (Optional [dest]) Get a random image and save it to [dest] if --type is img
62
+
63
+ Global Options:
64
+ --origin <url> (Required) Set IB_ORIGIN
65
+ --log <true|false> Set IB_LOG
66
+ --runtime-path <path> Set IB_RUNTIME_PATH
67
+ --log-root <path> Set IB_LOG_ROOT
68
+ --upload-token <token> Set IB_UPLOAD_TOKEN
69
+ --delete-token <token> Set IB_DELETE_TOKEN
70
+ --upload-auth-code <code> (Required for upload/remove) Set IB_UPLOAD_AUTH_CODE
71
+ --trace <id> Set Trace-Id
72
+ --lang <language> Set Accept-Language
73
+
74
+ Upload Options (All Optional):
75
+ --auth-code <code> Provide authCode for upload
76
+ --server-compress <true|false> Whether to compress image on server
77
+ --upload-channel <channel> Upload channel (telegram|cfr2|s3|discord|huggingface)
78
+ --channel-name <name> Channel target name
79
+ --auto-retry <true|false> Whether to auto-retry
80
+ --upload-name-type <type> Name type (default|index|origin|short)
81
+ --return-format <format> Return format (default|full)
82
+ --upload-folder <folder> Target upload folder
83
+
84
+ Remove Options (All Optional):
85
+ --folder Remove a folder instead of a single file
86
+
87
+ Random Options (All Optional):
88
+ --content <image|video> Filter content type
89
+ --type <path|img> Return type, use 'img' to return raw binary and save to local
90
+ --form <json|text> Response format
91
+ --dir <path> Filter by specific directory
92
+ --orientation <direction> Orientation (landscape|portrait|square|auto)
93
+ `.trim());
94
+ process.exit(0);
95
+ }
96
+
97
+ // Initialize env
98
+ initEvn({
99
+ IB_ORIGIN: values.origin as string | undefined,
100
+ IB_LOG: values.log as "true" | "false" | undefined,
101
+ IB_RUNTIME_PATH: values["runtime-path"] as string | undefined,
102
+ IB_LOG_ROOT: values["log-root"] as string | undefined,
103
+ IB_UPLOAD_TOKEN: values["upload-token"] as string | undefined,
104
+ IB_DELETE_TOKEN: values["delete-token"] as string | undefined,
105
+ IB_UPLOAD_AUTH_CODE: values["upload-auth-code"] as string | undefined,
106
+ });
107
+
108
+ if (!Bun.env.IB_ORIGIN) {
109
+ console.error(
110
+ "Error: Missing required global configuration: --origin. Please provide it via CLI or environment variables.",
111
+ );
112
+ process.exit(1);
113
+ }
114
+
115
+ const command = positionals[0];
116
+ if (
117
+ (command === "upload" || command === "remove") &&
118
+ !Bun.env.IB_UPLOAD_AUTH_CODE
119
+ ) {
120
+ console.error(
121
+ `Error: Missing required configuration: --upload-auth-code for the '${command}' command.`,
122
+ );
123
+ process.exit(1);
124
+ }
125
+ const c = new Context(
126
+ values.trace as string | undefined,
127
+ values.lang as string | undefined,
128
+ );
129
+
130
+ try {
131
+ if (command === "upload") {
132
+ const filepath = positionals[1];
133
+ if (!filepath) {
134
+ console.error("Error: Please provide a filepath to upload.");
135
+ process.exit(1);
136
+ }
137
+ const bunFile = Bun.file(filepath);
138
+ if (!(await bunFile.exists())) {
139
+ console.error(`Error: File not found at ${filepath}`);
140
+ process.exit(1);
141
+ }
142
+
143
+ const options: IBUploadOptions = {
144
+ authCode: values["auth-code"] as string | undefined,
145
+ serverCompress:
146
+ values["server-compress"] as IBUploadOptions["serverCompress"],
147
+ uploadChannel:
148
+ values["upload-channel"] as IBUploadOptions["uploadChannel"],
149
+ channelName: values["channel-name"] as string | undefined,
150
+ autoRetry: values["auto-retry"] as IBUploadOptions["autoRetry"],
151
+ uploadNameType:
152
+ values["upload-name-type"] as IBUploadOptions["uploadNameType"],
153
+ returnFormat:
154
+ values["return-format"] as IBUploadOptions["returnFormat"],
155
+ uploadFolder: values["upload-folder"] as string | undefined,
156
+ };
157
+
158
+ const result = await upload(c, bunFile as unknown as File, options);
159
+ if (result.success === false) {
160
+ console.error(JSON.stringify(result, null, 2));
161
+ process.exit(1);
162
+ }
163
+ console.log(JSON.stringify(result, null, 2));
164
+ } else if (command === "remove") {
165
+ const pathToRemove = positionals[1];
166
+ if (!pathToRemove) {
167
+ console.error("Error: Please provide a path to remove.");
168
+ process.exit(1);
169
+ }
170
+
171
+ const isFolder = Boolean(values.folder);
172
+ const result = await remove(c, pathToRemove, isFolder);
173
+ if (result.success === false) {
174
+ console.error(JSON.stringify(result, null, 2));
175
+ process.exit(1);
176
+ }
177
+ console.log(JSON.stringify(result, null, 2));
178
+ } else if (command === "random") {
179
+ const options: IBRandomOptions = {
180
+ content: values.content as IBRandomOptions["content"],
181
+ type: values.type as IBRandomOptions["type"],
182
+ form: values.form as IBRandomOptions["form"],
183
+ dir: values.dir as string | undefined,
184
+ orientation: values.orientation as IBRandomOptions["orientation"],
185
+ };
186
+
187
+ const result = await random(c, options);
188
+ if (options.type === "img") {
189
+ const dest = positionals[1] || "random-image.jpg";
190
+ if (result instanceof Blob) {
191
+ await Bun.write(dest, result);
192
+ console.log(`Image saved to ${dest}`);
193
+ } else {
194
+ console.error("Error: Expected Blob, got", result);
195
+ process.exit(1);
196
+ }
197
+ } else if (options.form === "text") {
198
+ console.log(result);
199
+ } else {
200
+ if (result && (result as any).success === false) {
201
+ console.error(JSON.stringify(result, null, 2));
202
+ process.exit(1);
203
+ }
204
+ console.log(JSON.stringify(result, null, 2));
205
+ }
206
+ } else {
207
+ console.error(`Error: Unknown command "${command}"`);
208
+ process.exit(1);
209
+ }
210
+ } catch (err: any) {
211
+ console.error("Execution error:", err.message || err);
212
+ process.exit(1);
213
+ }
214
+ }
215
+
216
+ main();
package/src/utils.ts ADDED
@@ -0,0 +1,83 @@
1
+ import { Logger, pathJoin } from "@w-xuefeng/bkit";
2
+
3
+ interface IBEnv {
4
+ IB_ORIGIN: string;
5
+ IB_LOG: "true" | "false";
6
+ IB_RUNTIME_PATH: string;
7
+ IB_LOG_ROOT: string;
8
+ IB_UPLOAD_TOKEN: string;
9
+ IB_DELETE_TOKEN: string;
10
+ IB_UPLOAD_AUTH_CODE: string;
11
+ }
12
+
13
+ const IB_RUNTIME_PATH = Bun.env.IB_RUNTIME_PATH || "runtime";
14
+ const IB_LOG_ROOT = Bun.env.IB_LOG_ROOT || "logs";
15
+
16
+ const getLogConfig = () => {
17
+ return {
18
+ enabled: Bun.env.IB_LOG === "true",
19
+ type: "both" as const,
20
+ root: pathJoin(IB_RUNTIME_PATH, IB_LOG_ROOT),
21
+ };
22
+ };
23
+
24
+ export const logger = new Logger(getLogConfig());
25
+ const initLogger = () => {
26
+ const loggerConfig = getLogConfig();
27
+ logger.enabled = loggerConfig.enabled;
28
+ logger.root = loggerConfig.root;
29
+ };
30
+
31
+ export function initEvn(env?: Partial<IBEnv>) {
32
+ Bun.env.IB_ORIGIN = Bun.env.IB_ORIGIN || env?.IB_ORIGIN;
33
+ Bun.env.IB_LOG = Bun.env.IB_LOG || env?.IB_LOG;
34
+ Bun.env.IB_RUNTIME_PATH = Bun.env.IB_RUNTIME_PATH || env?.IB_RUNTIME_PATH;
35
+ Bun.env.IB_LOG_ROOT = Bun.env.IB_LOG_ROOT || env?.IB_LOG_ROOT;
36
+ Bun.env.IB_UPLOAD_TOKEN = Bun.env.IB_UPLOAD_TOKEN || env?.IB_UPLOAD_TOKEN;
37
+ Bun.env.IB_DELETE_TOKEN = Bun.env.IB_DELETE_TOKEN || env?.IB_DELETE_TOKEN;
38
+ Bun.env.IB_UPLOAD_AUTH_CODE = Bun.env.IB_UPLOAD_AUTH_CODE ||
39
+ env?.IB_UPLOAD_AUTH_CODE;
40
+ initLogger();
41
+ }
42
+
43
+ export class Context {
44
+ headers: Headers;
45
+ constructor(traceId?: string, acceptLanguage = "en") {
46
+ this.headers = new Headers();
47
+ this.headers.set("Trace-Id", traceId || Bun.randomUUIDv7());
48
+ this.headers.set("Accept-Language", acceptLanguage);
49
+ }
50
+ setLanguage(lang: string) {
51
+ this.headers.set("Accept-Language", lang);
52
+ return this;
53
+ }
54
+ setTraceId(traceId?: string) {
55
+ this.headers.set("Trace-Id", traceId || Bun.randomUUIDv7());
56
+ return this;
57
+ }
58
+ setHeaders(headers: Headers) {
59
+ this.headers = headers;
60
+ return this;
61
+ }
62
+ }
63
+
64
+ export function filterEmptyField<T extends Record<string, any>>(
65
+ data?: Record<string, any>,
66
+ deep = false,
67
+ emptyArray = ["", void 0, null],
68
+ ) {
69
+ if (!data) {
70
+ return {} as T;
71
+ }
72
+ return Object.keys(data).reduce((res, key) => {
73
+ if (
74
+ data[key] && typeof data[key] === "object" && !Array.isArray(data[key]) &&
75
+ deep
76
+ ) {
77
+ res[key as keyof T] = filterEmptyField(data[key]);
78
+ } else if (!emptyArray.includes(data[key])) {
79
+ res[key as keyof T] = data[key];
80
+ }
81
+ return res;
82
+ }, {} as T);
83
+ }
Binary file
@@ -0,0 +1,82 @@
1
+ import { beforeAll, describe, expect, it } from "bun:test";
2
+ import { random, remove, upload } from "../src/image-bed";
3
+ import { Context, initEvn } from "../src/utils";
4
+
5
+ /**
6
+ * 在执行本测试脚本之前,
7
+ * 请确保您本地安装了 local-diamond,并且使用下列命令配置好了环境变量
8
+ * lod set cfib/origin <origin>
9
+ * lod set cfib/upload-auth-code <upload-auth-code>
10
+ * lod set cfib/upload-token <upload-token>
11
+ * lod set cfib/delete-token <delete-token>
12
+ */
13
+
14
+ function getLodConfig(key: string): string {
15
+ const proc = Bun.spawnSync(["bunx", "lod", "get", key]);
16
+ if (!proc.success) {
17
+ throw new Error(
18
+ `Failed to fetch ${key} from lod. Make sure local-diamond is configured properly.`,
19
+ );
20
+ }
21
+ return proc.stdout.toString().trim();
22
+ }
23
+
24
+ describe("Cloudflare Image Bed API Tests", () => {
25
+ let c: Context;
26
+ let testImagePath = "";
27
+
28
+ beforeAll(() => {
29
+ const origin = getLodConfig("cfib/origin");
30
+ const uploadAuthCode = getLodConfig("cfib/upload-auth-code");
31
+ const uploadToken = getLodConfig("cfib/upload-token");
32
+ const deleteToken = getLodConfig("cfib/delete-token");
33
+
34
+ initEvn({
35
+ IB_ORIGIN: origin,
36
+ IB_UPLOAD_AUTH_CODE: uploadAuthCode,
37
+ IB_UPLOAD_TOKEN: uploadToken,
38
+ IB_DELETE_TOKEN: deleteToken,
39
+ IB_LOG: "true",
40
+ });
41
+
42
+ c = new Context();
43
+ });
44
+
45
+ it("should upload a local image", async () => {
46
+ const file = Bun.file(`${import.meta.dir}/banner.png`);
47
+ expect(await file.exists()).toBe(true);
48
+ const buffer = await file.arrayBuffer();
49
+ const webFile = new File([buffer], "banner.png", { type: file.type });
50
+ const result = await upload(c, webFile, { uploadFolder: "wallpaper/test" });
51
+
52
+ if (!result.success) {
53
+ console.error(
54
+ "Upload failed with result:",
55
+ JSON.stringify(result, null, 2),
56
+ );
57
+ }
58
+
59
+ expect(result.success).toBe(true);
60
+ expect(result.data).toBeDefined();
61
+ expect(result.data?.src).toBeDefined();
62
+ expect(result.data?.url).toBeDefined();
63
+ expect(typeof result.data?.src).toBe("string");
64
+ expect(typeof result.data?.url).toBe("string");
65
+
66
+ testImagePath = result.data!.url;
67
+ });
68
+
69
+ it("should remove the uploaded image", async () => {
70
+ expect(testImagePath).not.toBe("");
71
+ const result = await remove(c, testImagePath, false);
72
+ expect(result.success).toBe(true);
73
+ });
74
+
75
+ it("should get a random image", async () => {
76
+ const result = await random(c, { type: "path", dir: "wallpaper" });
77
+ expect(result).toBeDefined();
78
+ if ((result as any).success !== undefined) {
79
+ expect((result as any).success).toBe(true);
80
+ }
81
+ });
82
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "lib": [
4
+ "ESNext"
5
+ ],
6
+ "target": "ESNext",
7
+ "module": "Preserve",
8
+ "moduleDetection": "force",
9
+ "jsx": "react-jsx",
10
+ "allowJs": true,
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "noEmit": true,
15
+ "strict": true,
16
+ "skipLibCheck": true,
17
+ "noFallthroughCasesInSwitch": true,
18
+ "noUncheckedIndexedAccess": true,
19
+ "noImplicitOverride": true,
20
+ "noUnusedLocals": false,
21
+ "noUnusedParameters": false,
22
+ "noPropertyAccessFromIndexSignature": false
23
+ }
24
+ }