feat: 实现基于 Cloudflare Worker 的 WPS 表单数据转发至 GoToSocial 功能

This commit is contained in:
2026-01-20 11:16:00 +08:00
commit 2d3be3e2af
4 changed files with 317 additions and 0 deletions

67
README.md Normal file
View File

@@ -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。

14
package.json Normal file
View File

@@ -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"
}
}

218
src/worker.js Normal file
View File

@@ -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" },
});
}

18
wrangler.toml Normal file
View File

@@ -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