From 2d3be3e2af9e6b6c6bc88a3ec1144ee58c2b1399 Mon Sep 17 00:00:00 2001 From: seaHi Date: Tue, 20 Jan 2026 11:16:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E5=9F=BA=E4=BA=8E=20?= =?UTF-8?q?Cloudflare=20Worker=20=E7=9A=84=20WPS=20=E8=A1=A8=E5=8D=95?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E8=BD=AC=E5=8F=91=E8=87=B3=20GoToSocial=20?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 67 ++++++++++++++++ package.json | 14 ++++ src/worker.js | 218 ++++++++++++++++++++++++++++++++++++++++++++++++++ wrangler.toml | 18 +++++ 4 files changed, 317 insertions(+) create mode 100644 README.md create mode 100644 package.json create mode 100644 src/worker.js create mode 100644 wrangler.toml diff --git a/README.md b/README.md new file mode 100644 index 0000000..e5c46b2 --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +# WPS 表单数据推送 -> GoToSocial 机器人 + +一个 Cloudflare Worker,用于将 WPS 表单收集的数据自动推送到 GoToSocial 发布动态。 + +## 工作流程 + +用户填写 WPS 表单 → WPS 推送数据 → Cloudflare Worker (本应用) → GoToSocial 发布动态 + +## 部署步骤 + +### 1. 安装依赖 + +本项目使用 [Bun](https://bun.sh/) 作为包管理器。 + +```bash +bun install +``` + +### 2. 登录 Cloudflare + +执行以下命令,将根据提示在浏览器中登录您的 Cloudflare 账号。 + +```bash +bun x wrangler login +``` + +### 3. 设置密钥 (Secrets) + +为了安全,GoToSocial 的地址和访问令牌需要作为密钥存储在 Cloudflare 中。 + +```bash +# 1. 设置你的 GoToSocial 实例 API 地址 +# 示例: https://your-domain.com/api/v1/statuses +bun x wrangler secret put GOTOSOCIAL_URL + +# 2. 设置你的 GoToSocial Bearer Token +# 这是用于 API 认证的访问令牌 +bun x wrangler secret put GOTOSOCIAL_TOKEN +``` + +### 4. (可选) 本地配置 + +`wrangler.toml` 文件包含本地开发时的配置。 + +- `GOTOSOCIAL_URL`: 你的 GoToSocial 实例 API 地址。 +- `GOTOSOCIAL_VISIBILITY`: 动态的可见性,默认为 `private`。可选值:`public`, `unlisted`, `private`, `direct`。 + +> **注意**: 在生产环境(部署后),配置的密钥 (`secrets`) 会覆盖 `wrangler.toml` 文件中的 `vars` 变量。 + +### 5. 部署 + +```bash +bun x wrangler deploy +``` + +部署成功后,Cloudflare 会提供一个 Worker URL,格式通常为: +`https://wps-gotosocial-bot.你的用户名.workers.dev` + +### 6. 配置 WPS 表单 + +1. 打开你的 WPS 表单,进入 **设置 -> 数据推送**。 +2. 在 **URL** 输入框中,填入你部署好的 Worker URL,并在末尾加上 `/webhook`。最终的 URL 应该像这样: + ``` + https://wps-gotosocial-bot.你的用户名.workers.dev/webhook + ``` +3. 点击 **“校验并绑定”**。 +4. 绑定成功后,WPS 表单的任何新提交都将自动推送到你的 GoToSocial。 diff --git a/package.json b/package.json new file mode 100644 index 0000000..e0c69cb --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "wps-gotosocial-bot", + "version": "1.0.0", + "description": "WPS表单数据推送到GoToSocial的Cloudflare Worker", + "main": "src/worker.js", + "scripts": { + "dev": "wrangler dev", + "deploy": "wrangler deploy", + "tail": "wrangler tail" + }, + "devDependencies": { + "wrangler": "^3.0.0" + } +} diff --git a/src/worker.js b/src/worker.js new file mode 100644 index 0000000..bc83c88 --- /dev/null +++ b/src/worker.js @@ -0,0 +1,218 @@ +/** + * WPS 表单数据推送 -> GoToSocial 机器人 + * + * 严格按照WPS表单推送开发说明文档(无签名版)实现 + * + * 主要逻辑: + * 1. 接收 /webhook 路径的 POST 请求。 + * 2. 判断请求体中的 `event` 字段。 + * 3. 如果 event 是 'bind',处理WPS的验证绑定请求。 + * 4. 如果 event 是 'create_answer',格式化表单内容并推送到GoToSocial。 + * 5. 提供 /health 路径用于健康检查。 + */ +export default { + async fetch(request, env) { + const url = new URL(request.url); + + // 1. 健康检查端点 + if (url.pathname === "/health") { + return jsonResponse({ + status: "ok", + timestamp: new Date().toISOString(), + }); + } + + // 2. 只处理到 /webhook 的 POST 请求 + if (url.pathname !== "/webhook") { + return new Response("Not Found", { status: 404 }); + } + if (request.method !== "POST") { + return new Response("Method Not Allowed", { status: 405 }); + } + + try { + const body = await request.json(); + console.log("Received WPS push:", JSON.stringify(body, null, 2)); + + // 3. 根据 event 字段分发任务 + switch (body.event) { + case "bind": + // 最终方案: WPS的bind_code需要用户在后台看到,并手动配置为secret。 + // 代码从环境变量中读取这个值并返回。 + const bindCodeFromEnv = env.BIND_CODE; + + if (bindCodeFromEnv) { + console.log( + `Handling bind event by returning BIND_CODE from environment secret.`, + ); + return jsonResponse({ bind_code: bindCodeFromEnv }); + } else { + console.error( + 'BIND_CODE secret is not configured. Please set it via "wrangler secret put BIND_CODE".', + ); + return jsonResponse( + { status: 500, msg: "BIND_CODE secret is not configured." }, + 500, + ); + } + + case "create_answer": + // 处理新的答卷数据 + console.log("Handling create_answer event."); + return await handleDataPush(body, env); + + default: + // 对于 update_answer, delete_answer 等其他事件,暂时只记录日志,不处理 + console.log( + `Received unhandled event type: "${body.event}". Ignoring.`, + ); + return jsonResponse({ + status: 200, + msg: `Received and ignored event: ${body.event}`, + }); + } + } catch (error) { + console.error("Worker error:", error); + return jsonResponse({ status: 500, msg: `Error: ${error.message}` }, 500); + } + }, +}; + +/** + * 处理数据推送 (格式化并发送) + */ +async function handleDataPush(data, env) { + // 格式化消息内容 + const message = formatMessage(data); + + // 发送到 GoToSocial + const result = await sendToGoToSocial(message, env); + + if (result.success) { + console.log("Successfully sent to GoToSocial."); + return jsonResponse({ status: 200, msg: "success" }); + } else { + console.error("Failed to send to GoToSocial:", result.error); + // 即使发送失败,也返回200避免WPS重复推送 + return jsonResponse({ + status: 200, + msg: `Received but forward failed: ${result.error}`, + }); + } +} + +/** + * 格式化WPS表单数据为消息文本 + */ +function formatMessage(data) { + const lines = []; + + // 标题 + lines.push(`📋 ${data.formTitle || "无标题表单"}`); + + // 表单元数据 + const meta = []; + if (data.aid) meta.push(`答卷ID: ${data.aid}`); + if (data.creatorName) meta.push(`提交人: ${data.creatorName}`); + if (meta.length > 0) lines.push(meta.join(" | ")); + + lines.push("---"); + + // 答案内容 + if (data.answerContents && Array.isArray(data.answerContents)) { + for (const item of data.answerContents) { + // 检查 value 是否存在,不存在则不显示该行 + if ( + item.value !== null && + item.value !== undefined && + item.value !== "" + ) { + const title = item.title || `(无标题问题)`; + // 对图片/文件等特殊类型进行处理 + let answerText; + if (item.type === "newMultiImage" || item.type === "multiFile") { + if (Array.isArray(item.value)) { + answerText = item.value + .map( + (file) => ` + - ${file.fileName || "文件"}: ${file.fileShareLink || "无链接"}`, + ) + .join(""); + } else { + answerText = "附件格式错误"; + } + } else { + answerText = Array.isArray(item.value) + ? item.value.join(", ") + : item.value; + } + lines.push(`**${title}**: ${answerText}`); + } + } + } + + // 提交时间 + const time = data.eventTs + ? new Date(data.eventTs).toLocaleString("zh-CN", { + timeZone: "Asia/Shanghai", + }) + : ""; + if (time) { + lines.push("---"); + lines.push(`⏰ ${time}`); + } + + return lines.join("\n"); +} + +/** + * 发送消息到 GoToSocial + */ +async function sendToGoToSocial(message, env) { + const url = env.GOTOSOCIAL_URL; + const token = env.GOTOSOCIAL_TOKEN; + + if (!url || !token) { + return { + success: false, + error: + "GOTOSOCIAL_URL or GOTOSOCIAL_TOKEN not configured in environment variables.", + }; + } + + try { + const response = await fetch(url, { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + status: message, + visibility: env.GOTOSOCIAL_VISIBILITY || "private", // 可在环境变量中配置,默认为私密 + }), + }); + + if (response.ok) { + const result = await response.json(); + return { success: true, data: result }; + } else { + const errorText = await response.text(); + console.error(`GoToSocial API error: ${response.status} ${errorText}`); + return { success: false, error: `HTTP ${response.status}: ${errorText}` }; + } + } catch (error) { + console.error("Fetch to GoToSocial failed:", error); + return { success: false, error: error.message }; + } +} + +/** + * JSON响应辅助函数 + */ +function jsonResponse(data, status = 200) { + return new Response(JSON.stringify(data, null, 2), { + status, + headers: { "Content-Type": "application/json;charset=utf-8" }, + }); +} diff --git a/wrangler.toml b/wrangler.toml new file mode 100644 index 0000000..d1200aa --- /dev/null +++ b/wrangler.toml @@ -0,0 +1,18 @@ +name = "wps-gotosocial-bot" +main = "src/worker.js" +compatibility_date = "2024-01-01" + +[vars] +# GOTOSOCIAL_URL: GoToSocial 实例的 API 地址 +# GOTOSOCIAL_TOKEN: GoToSocial 的 API Bearer Token +# GOTOSOCIAL_VISIBILITY: 可选, toot 的可见性, 默认 'private' (private, direct, unlisted, public) +# +# 部署前, 请务必在 Cloudflare Dashboard 中设置好 GOTOSOCIAL_URL 和 GOTOSOCIAL_TOKEN 的 secrets +# 例如: npx wrangler secret put GOTOSOCIAL_TOKEN +# GOTOSOCIAL_URL = "https://your-gotosocial-instance.com/api/v1/statuses" +GOTOSOCIAL_VISIBILITY = "private" + +[observability] +[observability.logs] +enabled = true +invocation_logs = true