Files
Selig 4c966a3ad2 Initial commit: OpenClaw Skill Collection
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.
2026-03-13 10:58:30 +08:00

87 lines
2.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 發送失敗');
}