forked from Selig/openclaw-skill
6 custom skills (assign-task, dispatch-webhook, daily-briefing, task-capture, qmd-brain, tts-voice) with technical documentation. Compatible with Claude Code, OpenClaw, Codex CLI, and OpenCode.
87 lines
2.6 KiB
TypeScript
87 lines
2.6 KiB
TypeScript
/**
|
||
* dispatch-webhook skill
|
||
* 發送 Webhook 到 VPS,處理重試與錯誤
|
||
*/
|
||
|
||
interface DispatchInput {
|
||
target: 'vps-a' | 'vps-b';
|
||
payload: Record<string, unknown>;
|
||
webhookUrl: string;
|
||
webhookToken: string;
|
||
timeoutMs?: number;
|
||
retries?: number;
|
||
}
|
||
|
||
async function fetchWithTimeout(url: string, options: RequestInit, timeoutMs: number): Promise<Response> {
|
||
const controller = new AbortController();
|
||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||
try {
|
||
return await fetch(url, { ...options, signal: controller.signal });
|
||
} finally {
|
||
clearTimeout(timer);
|
||
}
|
||
}
|
||
|
||
export async function handler(ctx: any) {
|
||
const input: DispatchInput = ctx.input || ctx.params;
|
||
|
||
if (!input.webhookUrl) {
|
||
throw new Error(`${input.target.toUpperCase()} Webhook URL 未設定。請在環境變數設定 VPS_A_WEBHOOK_URL 或 VPS_B_WEBHOOK_URL`);
|
||
}
|
||
if (!input.webhookToken) {
|
||
throw new Error(`${input.target.toUpperCase()} Webhook Token 未設定`);
|
||
}
|
||
|
||
const timeoutMs = input.timeoutMs ?? 30000;
|
||
const maxRetries = input.retries ?? 3;
|
||
let lastError: Error | null = null;
|
||
|
||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||
try {
|
||
const response = await fetchWithTimeout(
|
||
input.webhookUrl,
|
||
{
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${input.webhookToken}`,
|
||
'X-OpenClaw-Version': '1.0',
|
||
'X-OpenClaw-Task-Id': String(input.payload.task_id || ''),
|
||
},
|
||
body: JSON.stringify(input.payload),
|
||
},
|
||
timeoutMs
|
||
);
|
||
|
||
if (response.status === 401) {
|
||
throw new Error('Webhook Token 驗證失敗(401),請確認 VPS_WEBHOOK_TOKEN 設定正確');
|
||
}
|
||
|
||
if (!response.ok && response.status !== 202) {
|
||
const body = await response.text().catch(() => '');
|
||
throw new Error(`VPS 回應錯誤 ${response.status}:${body.slice(0, 200)}`);
|
||
}
|
||
|
||
const result = await response.json().catch(() => ({ status: 'accepted' }));
|
||
return {
|
||
success: true,
|
||
status: result.status || 'accepted',
|
||
task_id: input.payload.task_id,
|
||
target: input.target,
|
||
attempt,
|
||
};
|
||
|
||
} catch (err: any) {
|
||
lastError = err;
|
||
if (err.message?.includes('401') || err.message?.includes('Token')) {
|
||
break; // 認證錯誤不重試
|
||
}
|
||
if (attempt < maxRetries) {
|
||
await new Promise(r => setTimeout(r, 5000 * attempt)); // 指數退避
|
||
}
|
||
}
|
||
}
|
||
|
||
throw lastError || new Error('Webhook 發送失敗');
|
||
}
|