forked from Selig/openclaw-skill
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.
This commit is contained in:
114
skills/dispatch-webhook/SKILL.md
Normal file
114
skills/dispatch-webhook/SKILL.md
Normal file
@@ -0,0 +1,114 @@
|
||||
---
|
||||
name: dispatch-webhook
|
||||
description: 發送任務 Payload 到指定 VPS 的 Webhook endpoint,支援同步等待與非同步模式。
|
||||
triggers: []
|
||||
tools:
|
||||
- web_fetch
|
||||
- exec
|
||||
internal: true
|
||||
---
|
||||
|
||||
# Dispatch Webhook Skill
|
||||
|
||||
## 功能說明
|
||||
|
||||
底層 Webhook 發送工具,由 `assign-task` 呼叫。
|
||||
負責發送任務到 VPS-A 或 VPS-B,處理認證與錯誤重試。
|
||||
|
||||
## Webhook 協議
|
||||
|
||||
### 請求格式(OpenClaw → VPS)
|
||||
|
||||
```http
|
||||
POST <VPS_WEBHOOK_URL>
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
X-OpenClaw-Version: 1.0
|
||||
|
||||
{
|
||||
"task_id": "uuid",
|
||||
"type": "project_development",
|
||||
"description": "任務描述",
|
||||
"priority": "high|normal|low",
|
||||
"callback_url": "https://oclaw.nature.edu.kg/webhook/callback",
|
||||
"created_at": "ISO8601"
|
||||
}
|
||||
```
|
||||
|
||||
### 即時回應格式(VPS → OpenClaw,HTTP 202)
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "uuid",
|
||||
"status": "accepted",
|
||||
"message": "任務已接受,開始執行"
|
||||
}
|
||||
```
|
||||
|
||||
### Callback 格式(任務完成後,VPS → OpenClaw)
|
||||
|
||||
```http
|
||||
POST <callback_url>
|
||||
Authorization: Bearer <callback_token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"task_id": "uuid",
|
||||
"status": "completed|failed",
|
||||
"summary": "執行摘要",
|
||||
"artifacts": ["file1.py", "docs/README.md"],
|
||||
"error": null
|
||||
}
|
||||
```
|
||||
|
||||
## VPS 端接收端實作範例
|
||||
|
||||
### Node.js / Express
|
||||
|
||||
```javascript
|
||||
app.post('/webhook/openclaw', (req, res) => {
|
||||
const token = req.headers.authorization?.replace('Bearer ', '');
|
||||
if (token !== process.env.OPENCLAW_WEBHOOK_TOKEN) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const task = req.body;
|
||||
res.status(202).json({ task_id: task.task_id, status: 'accepted' });
|
||||
|
||||
// 非同步執行任務
|
||||
processTask(task).then(result => {
|
||||
// 回傳結果給 OpenClaw
|
||||
fetch(task.callback_url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${CALLBACK_TOKEN}` },
|
||||
body: JSON.stringify({ task_id: task.task_id, status: 'completed', ...result }),
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 給 Claude Code 的提示詞(VPS-A)
|
||||
|
||||
任務接收後,PM agent 用這個提示詞啟動工作:
|
||||
|
||||
```
|
||||
你是專案經理,收到來自 OpenClaw 的任務指派。
|
||||
Task ID: {task_id}
|
||||
任務描述: {description}
|
||||
優先級: {priority}
|
||||
|
||||
請:
|
||||
1. 分析需求,制定實作計劃
|
||||
2. 分派子任務給適合的模型(Sonnet 寫程式,Haiku 做測試)
|
||||
3. 整合結果,確保品質
|
||||
4. 完成後透過 callback_url 回報結果
|
||||
```
|
||||
|
||||
## 錯誤處理
|
||||
|
||||
| 錯誤 | 處理方式 |
|
||||
|------|---------|
|
||||
| 連線失敗 | 重試 3 次,間隔 5s |
|
||||
| 401 Unauthorized | 立即失敗,提示設定 token |
|
||||
| 超時(> 30s) | 返回 accepted,等待 callback |
|
||||
| VPS 回傳 500 | 記錄錯誤,通知使用者 |
|
||||
86
skills/dispatch-webhook/handler.ts
Normal file
86
skills/dispatch-webhook/handler.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* 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 發送失敗');
|
||||
}
|
||||
Reference in New Issue
Block a user