feat(core): ✨ add Lsky Pro image upload worker
在 cf_worker 目录下新增 lsky-telegram-worker.js 脚本,实现将 Telegram 发送的图片上传至 Lsky Pro 图床并返回 Markdown 链接代码块。 同时对项目目录结构进行调整与重构: - 将原 telegram/ 目录下的脚本与文档移动并整合至 cf_worker/ 目录 - 更新项目主 README.md 以及子目录文档,完善 Cloudflare Workers 相关脚本的用途说明
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Script Collection
|
# Script Collection
|
||||||
|
|
||||||
一个面向日常自动化的小型脚本集合,当前包含 Homebrew 升级管理、Sub-Store 节点重命名处理、Telegram Bot Worker 三类脚本。
|
一个面向日常自动化的小型脚本集合,当前包含 Homebrew 升级管理、Sub-Store 节点重命名处理、Cloudflare Worker 脚本。
|
||||||
|
|
||||||
## 目录
|
## 目录
|
||||||
|
|
||||||
@@ -12,8 +12,9 @@ script/
|
|||||||
├── substore/
|
├── substore/
|
||||||
│ ├── README.md
|
│ ├── README.md
|
||||||
│ └── sntp-rename.js
|
│ └── sntp-rename.js
|
||||||
├── telegram/
|
├── cf_worker/
|
||||||
│ ├── README.md
|
│ ├── README.md
|
||||||
|
│ ├── lsky-telegram-worker.js
|
||||||
│ └── tg-bot.js
|
│ └── tg-bot.js
|
||||||
├── LICENSE
|
├── LICENSE
|
||||||
└── README.md
|
└── README.md
|
||||||
@@ -25,7 +26,8 @@ script/
|
|||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `homebrew/` | `brew-upgrade-manager.sh` | 自动更新 Homebrew、升级 Formula/Cask、清理缓存 | macOS、Bash、Homebrew |
|
| `homebrew/` | `brew-upgrade-manager.sh` | 自动更新 Homebrew、升级 Formula/Cask、清理缓存 | macOS、Bash、Homebrew |
|
||||||
| `substore/` | `sntp-rename.js` | 守候网络节点命名专用,在 Sub-Store 代理节点名称中注入线路标签 | Sub-Store 脚本处理器 |
|
| `substore/` | `sntp-rename.js` | 守候网络节点命名专用,在 Sub-Store 代理节点名称中注入线路标签 | Sub-Store 脚本处理器 |
|
||||||
| `telegram/` | `tg-bot.js` | 基于 Cloudflare Workers 的 Telegram 私聊转发与管理后台 | Cloudflare Workers、D1、Telegram Bot |
|
| `cf_worker/` | `lsky-telegram-worker.js` | Telegram 收图后上传到 Lsky Pro,并返回 Markdown 图片链接代码块 | Cloudflare Workers、D1、Telegram Bot、Lsky Pro |
|
||||||
|
| `cf_worker/` | `tg-bot.js` | 基于 Cloudflare Workers 的 Telegram 私聊转发与管理后台 | Cloudflare Workers、D1、Telegram Bot |
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
@@ -52,17 +54,20 @@ HB_TERMINAL_WIDTH=130 ./brew-upgrade-manager.sh
|
|||||||
|
|
||||||
详细规则见 [substore/README.md](substore/README.md)。
|
详细规则见 [substore/README.md](substore/README.md)。
|
||||||
|
|
||||||
### Telegram Bot Worker
|
### Cloudflare Worker 脚本
|
||||||
|
|
||||||
把 [telegram/tg-bot.js](telegram/tg-bot.js) 部署为 Cloudflare Worker,并绑定 Telegram Bot Token、管理员 ID、管理群 ID 和 D1 数据库。Worker 根路径用于健康检查,Telegram Webhook POST 到 Worker 地址。
|
`cf_worker/` 目录包含两个 Worker:
|
||||||
|
|
||||||
详细部署说明见 [telegram/README.md](telegram/README.md)。
|
- [cf_worker/lsky-telegram-worker.js](cf_worker/lsky-telegram-worker.js):接收 Telegram 图片,上传到 Lsky Pro,返回 Markdown 图片链接代码块。默认单文件上限 `500.00 MB`,上传队列最多 `500` 张。
|
||||||
|
- [cf_worker/tg-bot.js](cf_worker/tg-bot.js):Telegram 私聊转发与管理后台,绑定 Telegram Bot Token、管理员 ID、管理群 ID 和 D1 数据库。
|
||||||
|
|
||||||
|
详细部署说明见 [cf_worker/README.md](cf_worker/README.md)。
|
||||||
|
|
||||||
## 安全提示
|
## 安全提示
|
||||||
|
|
||||||
- 不要把 Bot Token、Cloudflare 密钥、Telegram 群组 ID、管理员 ID 或 sudo 密码提交到公开仓库。
|
- 不要把 Bot Token、Cloudflare 密钥、Telegram 群组 ID、管理员 ID 或 sudo 密码提交到公开仓库。
|
||||||
- `homebrew/brew-upgrade-manager.sh` 当前包含 `SUDO_PWD` 占位变量,若确需使用自动输入 sudo 密码,请只在本机私有副本中填写。
|
- `homebrew/brew-upgrade-manager.sh` 当前包含 `SUDO_PWD` 占位变量,若确需使用自动输入 sudo 密码,请只在本机私有副本中填写。
|
||||||
- Telegram Worker 会保存用户状态、消息映射和配置项到 D1,请按实际隐私要求控制数据库访问权限。
|
- Cloudflare Worker 脚本会保存用户状态、消息映射、上传队列或配置项到 D1,请按实际隐私要求控制数据库访问权限。
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,64 @@
|
|||||||
yi'xia# Telegram Bot Worker
|
# Cloudflare Worker Scripts
|
||||||
|
|
||||||
|
这个目录保存 Cloudflare Workers 相关脚本:
|
||||||
|
|
||||||
|
| 脚本 | 用途 |
|
||||||
|
| --- | --- |
|
||||||
|
| `lsky-telegram-worker.js` | Telegram 收图后上传到 Lsky Pro,并返回 Markdown 图片链接代码块 |
|
||||||
|
| `tg-bot.js` | Telegram 双向私聊中继 Bot,包含管理面板、验证、过滤、封禁、备份等功能 |
|
||||||
|
|
||||||
|
## Lsky Telegram Upload Worker
|
||||||
|
|
||||||
|
`lsky-telegram-worker.js` 用于把 Telegram 私聊发送的图片或图片文件上传到 Lsky Pro。上传成功后,Bot 会回复 Markdown 图片链接,并使用 Telegram HTML `<pre>` 代码块渲染,便于 Android 客户端点击复制。
|
||||||
|
|
||||||
|
### 功能
|
||||||
|
|
||||||
|
- 支持 Telegram `photo` 和图片类 `document`。
|
||||||
|
- 支持 Telegram `media_group_id` 相册收集后批量上传。
|
||||||
|
- 默认单文件大小限制为 `500.00 MB`,可用 `MAX_FILE_MB` 覆盖。
|
||||||
|
- 默认上传队列最多收 `500` 张,可用 `MAX_UPLOAD_QUEUE_IMAGES` 覆盖。
|
||||||
|
- 返回格式为 ``,并关闭网页预览。
|
||||||
|
- 通过 `ALLOWED_TELEGRAM_USER_IDS` 限制可用用户。
|
||||||
|
|
||||||
|
### 必需绑定和环境变量
|
||||||
|
|
||||||
|
| 名称 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| `DB` | Cloudflare D1 数据库绑定名,用于暂存相册队列 |
|
||||||
|
| `TELEGRAM_BOT_TOKEN` | Telegram Bot Token |
|
||||||
|
| `TELEGRAM_WEBHOOK_SECRET` | Telegram Webhook secret token,同时用于 `/init` 初始化鉴权 |
|
||||||
|
| `ALLOWED_TELEGRAM_USER_IDS` | 允许使用的 Telegram user id,多个用英文逗号分隔 |
|
||||||
|
| `LSKY_API_URL` | Lsky Pro 上传 API 地址 |
|
||||||
|
| `LSKY_TOKEN` | Lsky Pro API token |
|
||||||
|
|
||||||
|
### 可选环境变量
|
||||||
|
|
||||||
|
| 名称 | 默认值 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `MAX_FILE_MB` | `500.00` | 单个 Telegram 图片/文件大小上限,单位 MB |
|
||||||
|
| `MAX_UPLOAD_QUEUE_IMAGES` | `500` | 单个上传队列最多暂存图片数量 |
|
||||||
|
| `ALBUM_WAIT_SECONDS` | `5` | 相册收集等待秒数 |
|
||||||
|
| `LSKY_PERMISSION` | `1` | 传给 Lsky Pro 的 `permission` |
|
||||||
|
| `LSKY_STRATEGY_ID` | 空 | 传给 Lsky Pro 的 `strategy_id` |
|
||||||
|
|
||||||
|
### 部署流程
|
||||||
|
|
||||||
|
1. 创建 Telegram Bot,拿到 `TELEGRAM_BOT_TOKEN`。
|
||||||
|
2. 创建 Cloudflare D1 数据库,并在 Worker 中绑定为 `DB`。
|
||||||
|
3. 创建 Worker,把 [lsky-telegram-worker.js](lsky-telegram-worker.js) 作为 Worker 代码。
|
||||||
|
4. 配置上方环境变量。
|
||||||
|
5. 访问 `https://<WORKER_URL>/init?secret=<TELEGRAM_WEBHOOK_SECRET>` 初始化 D1 表。
|
||||||
|
6. 设置 Telegram Webhook:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://api.telegram.org/bot<TELEGRAM_BOT_TOKEN>/setWebhook" \
|
||||||
|
-d "url=<WORKER_URL>/webhook" \
|
||||||
|
-d "secret_token=<TELEGRAM_WEBHOOK_SECRET>"
|
||||||
|
```
|
||||||
|
|
||||||
|
7. 访问 Worker 根路径,若返回 `Lsky Telegram Worker OK - code-block-max500mb-queue500-2026-05-27`,说明 Worker 基本可用。
|
||||||
|
|
||||||
|
## Telegram Bot Worker
|
||||||
|
|
||||||
`tg-bot.js` 是一个部署在 Cloudflare Workers 上的 Telegram 双向私聊中继 Bot(版本 v3.68)。它会把用户私聊消息转发到管理员论坛群的独立 topic 中,管理员在对应 topic 内回复即可把消息发回用户,同时提供验证、过滤、封禁、备注、自动回复、备份和 Telegram 内联管理面板。
|
`tg-bot.js` 是一个部署在 Cloudflare Workers 上的 Telegram 双向私聊中继 Bot(版本 v3.68)。它会把用户私聊消息转发到管理员论坛群的独立 topic 中,管理员在对应 topic 内回复即可把消息发回用户,同时提供验证、过滤、封禁、备注、自动回复、备份和 Telegram 内联管理面板。
|
||||||
|
|
||||||
@@ -0,0 +1,415 @@
|
|||||||
|
const DEFAULT_MAX_FILE_MB = 500;
|
||||||
|
const DEFAULT_MAX_UPLOAD_QUEUE_IMAGES = 500;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async fetch(request, env, ctx) {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
if (request.method === "GET" && url.pathname === "/init") {
|
||||||
|
if (url.searchParams.get("secret") !== env.TELEGRAM_WEBHOOK_SECRET) {
|
||||||
|
return new Response("Unauthorized", { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
await initDb(env);
|
||||||
|
return new Response("DB initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.method === "GET") {
|
||||||
|
return new Response("Lsky Telegram Worker OK - code-block-max500mb-queue500-2026-05-27");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.pathname !== "/webhook" || request.method !== "POST") {
|
||||||
|
return new Response("Not Found", { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const secret = request.headers.get("X-Telegram-Bot-Api-Secret-Token");
|
||||||
|
if (secret !== env.TELEGRAM_WEBHOOK_SECRET) {
|
||||||
|
return new Response("Unauthorized", { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const update = await request.json();
|
||||||
|
ctx.waitUntil(handleUpdate(update, env));
|
||||||
|
return new Response("OK");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async function initDb(env) {
|
||||||
|
await env.DB.prepare(`
|
||||||
|
CREATE TABLE IF NOT EXISTS album_groups (
|
||||||
|
group_key TEXT PRIMARY KEY,
|
||||||
|
chat_id TEXT NOT NULL,
|
||||||
|
reply_to INTEGER NOT NULL,
|
||||||
|
status TEXT NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
`).run();
|
||||||
|
|
||||||
|
await env.DB.prepare(`
|
||||||
|
CREATE TABLE IF NOT EXISTS album_messages (
|
||||||
|
group_key TEXT NOT NULL,
|
||||||
|
message_id INTEGER NOT NULL,
|
||||||
|
payload TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (group_key, message_id)
|
||||||
|
)
|
||||||
|
`).run();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUpdate(update, env) {
|
||||||
|
const message = update.message;
|
||||||
|
if (!message) return;
|
||||||
|
|
||||||
|
const chatId = message.chat?.id;
|
||||||
|
const userId = message.from?.id;
|
||||||
|
const messageId = message.message_id;
|
||||||
|
|
||||||
|
if (!isAllowedUser(userId, env.ALLOWED_TELEGRAM_USER_IDS)) {
|
||||||
|
await sendTelegramText(env, chatId, "未授权。", messageId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.text?.startsWith("/start")) {
|
||||||
|
await sendTelegramText(env, chatId, "发图片或图片文件给我,我会上传到 Lsky Pro。", messageId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.media_group_id) {
|
||||||
|
const groupKey = `${chatId}:${message.media_group_id}`;
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
await env.DB.prepare(`
|
||||||
|
INSERT OR IGNORE INTO album_groups (group_key, chat_id, reply_to, status, updated_at)
|
||||||
|
VALUES (?, ?, ?, 'collecting', ?)
|
||||||
|
`).bind(groupKey, String(chatId), messageId, now).run();
|
||||||
|
|
||||||
|
await env.DB.prepare(`
|
||||||
|
UPDATE album_groups
|
||||||
|
SET updated_at = ?
|
||||||
|
WHERE group_key = ? AND status = 'collecting'
|
||||||
|
`).bind(now, groupKey).run();
|
||||||
|
|
||||||
|
const maxQueueImages = getMaxUploadQueueImages(env);
|
||||||
|
const queued = await env.DB.prepare(`
|
||||||
|
SELECT COUNT(*) AS count
|
||||||
|
FROM album_messages
|
||||||
|
WHERE group_key = ?
|
||||||
|
`).bind(groupKey).first();
|
||||||
|
|
||||||
|
if (Number(queued?.count || 0) >= maxQueueImages) {
|
||||||
|
await sendTelegramText(env, chatId, `上传队列已满,最多 ${maxQueueImages} 张。`, messageId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await env.DB.prepare(`
|
||||||
|
INSERT OR IGNORE INTO album_messages (group_key, message_id, payload)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
`).bind(groupKey, messageId, JSON.stringify(message)).run();
|
||||||
|
|
||||||
|
await sleep((Number(env.ALBUM_WAIT_SECONDS || "5") + 1) * 1000);
|
||||||
|
await processAlbum(env, groupKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const image = await uploadTelegramMessageToLsky(env, message);
|
||||||
|
await sendTelegramText(env, chatId, toTelegramCodeBlock(toMarkdownImage(image.url, image.filename)), messageId, "HTML");
|
||||||
|
} catch (err) {
|
||||||
|
await sendTelegramText(env, chatId, `上传失败:${err.message}`, messageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processAlbum(env, groupKey) {
|
||||||
|
const waitMs = Number(env.ALBUM_WAIT_SECONDS || "5") * 1000;
|
||||||
|
const group = await env.DB.prepare(`
|
||||||
|
SELECT * FROM album_groups WHERE group_key = ?
|
||||||
|
`).bind(groupKey).first();
|
||||||
|
|
||||||
|
if (!group || group.status !== "collecting") return;
|
||||||
|
if (Date.now() - group.updated_at < waitMs) return;
|
||||||
|
|
||||||
|
const lock = await env.DB.prepare(`
|
||||||
|
UPDATE album_groups
|
||||||
|
SET status = 'processing'
|
||||||
|
WHERE group_key = ? AND status = 'collecting'
|
||||||
|
`).bind(groupKey).run();
|
||||||
|
|
||||||
|
if (!lock.meta || lock.meta.changes !== 1) return;
|
||||||
|
|
||||||
|
const rows = await env.DB.prepare(`
|
||||||
|
SELECT message_id, payload
|
||||||
|
FROM album_messages
|
||||||
|
WHERE group_key = ?
|
||||||
|
ORDER BY message_id ASC
|
||||||
|
`).bind(groupKey).all();
|
||||||
|
|
||||||
|
const messages = rows.results.map((row) => JSON.parse(row.payload));
|
||||||
|
const images = [];
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < messages.length; i++) {
|
||||||
|
try {
|
||||||
|
images.push(await uploadTelegramMessageToLsky(env, messages[i]));
|
||||||
|
} catch (err) {
|
||||||
|
errors.push(`${i + 1}. ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = images.length
|
||||||
|
? "上传完成:\n\n" + images.map((image, i) => `${i + 1}.\n${toTelegramCodeBlock(toMarkdownImage(image.url, image.filename))}`).join("\n")
|
||||||
|
: "这一组图片全部上传失败。";
|
||||||
|
|
||||||
|
if (errors.length) {
|
||||||
|
text += "\n\n失败:\n" + errors.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendTelegramLongText(env, group.chat_id, text, group.reply_to, images.length ? "HTML" : undefined);
|
||||||
|
|
||||||
|
await env.DB.prepare(`DELETE FROM album_messages WHERE group_key = ?`).bind(groupKey).run();
|
||||||
|
await env.DB.prepare(`DELETE FROM album_groups WHERE group_key = ?`).bind(groupKey).run();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAllowedUser(userId, allowList) {
|
||||||
|
return String(allowList || "")
|
||||||
|
.split(",")
|
||||||
|
.map((x) => x.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.includes(String(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toMarkdownImage(url, filename = "image") {
|
||||||
|
return ``;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeMarkdownAlt(value) {
|
||||||
|
return String(value || "image")
|
||||||
|
.replaceAll("\\", "\\\\")
|
||||||
|
.replaceAll("]", "\\]");
|
||||||
|
}
|
||||||
|
|
||||||
|
function toTelegramCodeBlock(text) {
|
||||||
|
return `<pre>${escapeHtml(text)}</pre>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value || "")
|
||||||
|
.replaceAll("&", "&")
|
||||||
|
.replaceAll("<", "<")
|
||||||
|
.replaceAll(">", ">");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMaxFileMb(env) {
|
||||||
|
const value = Number(env.MAX_FILE_MB || DEFAULT_MAX_FILE_MB);
|
||||||
|
return Number.isFinite(value) && value > 0 ? value : DEFAULT_MAX_FILE_MB;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFileMb(value) {
|
||||||
|
return value.toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMaxUploadQueueImages(env) {
|
||||||
|
const value = Number(env.MAX_UPLOAD_QUEUE_IMAGES || DEFAULT_MAX_UPLOAD_QUEUE_IMAGES);
|
||||||
|
return Number.isFinite(value) && value > 0 ? Math.floor(value) : DEFAULT_MAX_UPLOAD_QUEUE_IMAGES;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadTelegramMessageToLsky(env, message) {
|
||||||
|
const image = extractImage(message, env);
|
||||||
|
|
||||||
|
const fileInfo = await telegramApi(env, "getFile", {
|
||||||
|
file_id: image.fileId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileUrl = `https://api.telegram.org/file/bot${env.TELEGRAM_BOT_TOKEN}/${fileInfo.file_path}`;
|
||||||
|
const fileResp = await fetch(fileUrl);
|
||||||
|
|
||||||
|
if (!fileResp.ok) {
|
||||||
|
throw new Error(`下载 Telegram 文件失败: HTTP ${fileResp.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bytes = await fileResp.arrayBuffer();
|
||||||
|
const filename = image.filename || guessFilename(fileInfo.file_path, image.mimeType);
|
||||||
|
const url = await uploadToLsky(env, bytes, filename, image.mimeType);
|
||||||
|
|
||||||
|
return { url, filename };
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractImage(message, env) {
|
||||||
|
const maxFileMb = getMaxFileMb(env);
|
||||||
|
const maxBytes = maxFileMb * 1024 * 1024;
|
||||||
|
|
||||||
|
if (message.photo?.length) {
|
||||||
|
const photo = message.photo[message.photo.length - 1];
|
||||||
|
|
||||||
|
if (photo.file_size && photo.file_size > maxBytes) {
|
||||||
|
throw new Error(`图片超过 ${formatFileMb(maxFileMb)} MB`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
fileId: photo.file_id,
|
||||||
|
filename: `${photo.file_unique_id || photo.file_id}.jpg`,
|
||||||
|
mimeType: "image/jpeg",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.document) {
|
||||||
|
const doc = message.document;
|
||||||
|
const mimeType = doc.mime_type || "application/octet-stream";
|
||||||
|
|
||||||
|
if (!mimeType.startsWith("image/")) {
|
||||||
|
throw new Error("请发送图片,或以文件形式发送图片");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc.file_size && doc.file_size > maxBytes) {
|
||||||
|
throw new Error(`文件超过 ${formatFileMb(maxFileMb)} MB`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
fileId: doc.file_id,
|
||||||
|
filename: doc.file_name || `${doc.file_unique_id || doc.file_id}`,
|
||||||
|
mimeType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("请发送图片,或以文件形式发送图片");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadToLsky(env, bytes, filename, mimeType) {
|
||||||
|
const form = new FormData();
|
||||||
|
form.append("file", new File([bytes], filename, { type: mimeType }));
|
||||||
|
form.append("permission", env.LSKY_PERMISSION || "1");
|
||||||
|
|
||||||
|
if (env.LSKY_STRATEGY_ID) {
|
||||||
|
form.append("strategy_id", env.LSKY_STRATEGY_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp = await fetch(env.LSKY_API_URL, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${env.LSKY_TOKEN}`,
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
body: form,
|
||||||
|
});
|
||||||
|
|
||||||
|
const raw = await resp.text();
|
||||||
|
let result;
|
||||||
|
|
||||||
|
try {
|
||||||
|
result = JSON.parse(raw);
|
||||||
|
} catch {
|
||||||
|
throw new Error(`Lsky 返回非 JSON: HTTP ${resp.status} ${raw.slice(0, 200)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resp.ok || result.status !== true) {
|
||||||
|
throw new Error(`Lsky 上传失败: HTTP ${resp.status} ${JSON.stringify(result)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = result.data || {};
|
||||||
|
const links = data.links || {};
|
||||||
|
const url = links.url || data.url || data.pathname || data.path;
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
throw new Error(`Lsky 返回中找不到图片链接`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function telegramApi(env, method, payload) {
|
||||||
|
const resp = await fetch(`https://api.telegram.org/bot${env.TELEGRAM_BOT_TOKEN}/${method}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload || {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const raw = await resp.text();
|
||||||
|
const result = JSON.parse(raw);
|
||||||
|
|
||||||
|
if (!resp.ok || result.ok !== true) {
|
||||||
|
throw new Error(`Telegram API ${method} 失败: ${JSON.stringify(result)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendTelegramText(env, chatId, text, replyToMessageId, parseMode) {
|
||||||
|
if (!chatId) return;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
chat_id: chatId,
|
||||||
|
text,
|
||||||
|
disable_web_page_preview: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (parseMode) {
|
||||||
|
payload.parse_mode = parseMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (replyToMessageId) {
|
||||||
|
payload.reply_to_message_id = replyToMessageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
await telegramApi(env, "sendMessage", payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendTelegramLongText(env, chatId, text, replyToMessageId, parseMode) {
|
||||||
|
const limit = 3900;
|
||||||
|
const chunks = splitTelegramText(text, limit);
|
||||||
|
|
||||||
|
for (let i = 0; i < chunks.length; i++) {
|
||||||
|
await sendTelegramText(
|
||||||
|
env,
|
||||||
|
chatId,
|
||||||
|
chunks[i],
|
||||||
|
i === 0 ? replyToMessageId : undefined,
|
||||||
|
parseMode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitTelegramText(text, limit) {
|
||||||
|
const chunks = [];
|
||||||
|
let current = "";
|
||||||
|
|
||||||
|
for (const line of String(text || "").split("\n")) {
|
||||||
|
const next = current ? `${current}\n${line}` : line;
|
||||||
|
|
||||||
|
if (next.length <= limit) {
|
||||||
|
current = next;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current) {
|
||||||
|
chunks.push(current);
|
||||||
|
current = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.length <= limit) {
|
||||||
|
current = line;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < line.length; i += limit) {
|
||||||
|
chunks.push(line.slice(i, i + limit));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current) {
|
||||||
|
chunks.push(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunks.length ? chunks : [""];
|
||||||
|
}
|
||||||
|
|
||||||
|
function guessFilename(filePath, mimeType) {
|
||||||
|
const base = filePath.split("/").pop() || "image";
|
||||||
|
|
||||||
|
if (base.includes(".")) return base;
|
||||||
|
if (mimeType === "image/png") return `${base}.png`;
|
||||||
|
if (mimeType === "image/webp") return `${base}.webp`;
|
||||||
|
if (mimeType === "image/gif") return `${base}.gif`;
|
||||||
|
|
||||||
|
return `${base}.jpg`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user