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

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