feat: 实现基于 Cloudflare Worker 的 WPS 表单数据转发至 GoToSocial 功能
This commit is contained in:
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" },
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user