feat: 实现基于 Cloudflare Worker 的 WPS 表单数据转发至 GoToSocial 功能
This commit is contained in:
67
README.md
Normal file
67
README.md
Normal 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
14
package.json
Normal 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
218
src/worker.js
Normal 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
18
wrangler.toml
Normal 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
|
||||
Reference in New Issue
Block a user