arcrun — AI workflow execution engine (clean history)
Self-hosted 開源:WASM 零件 + recipe + cypher-executor,跑在你自己的 Cloudflare。 此為重建的乾淨歷史起點(移除曾誤 commit 的 GCP SA 金鑰,舊歷史保留在 richblack/arcrun 與本地 backup 分支)。含: - acr init --self-hosted installer(建 KV/R2 + codeload 拉預編譯 wasm + wrangler deploy + seed recipe) - recipe push 把關(資料外流提醒 + 打通檢查) - 19 個正當零件預編譯 wasm(claude_api/km_writer/kbdb_upsert_block 排除:違反 DECISIONS §1) - CLI / cypher-executor / registry / 完整 SDD Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
canonical_id: "claude_api"
|
||||
display_name: "Claude AI 對話"
|
||||
category: "ai"
|
||||
version: "v2"
|
||||
wasi_target: "preview1"
|
||||
stability: "floating"
|
||||
runtime_compat:
|
||||
- "cf-workers"
|
||||
- "workerd"
|
||||
- "wazero"
|
||||
constraints:
|
||||
max_size_kb: 2048
|
||||
max_cold_start_ms: 80
|
||||
no_network_syscall: false
|
||||
no_filesystem_syscall: true
|
||||
io_model: "stdin_stdout_json"
|
||||
input_schema:
|
||||
type: object
|
||||
required: [mira_token, prompt]
|
||||
properties:
|
||||
mira_token:
|
||||
type: string
|
||||
description: Mira daemon Bearer token(Hetzner cloud-cto Mira daemon 的 MIRA_TOKEN)
|
||||
prompt:
|
||||
type: string
|
||||
description: 要送給 Mira 的訊息(已內建 Mira 副駕 persona,不需重複設角色)
|
||||
mira_url:
|
||||
type: string
|
||||
description: Mira daemon URL,預設 https://mira.uncle6.me
|
||||
default: "https://mira.uncle6.me"
|
||||
timeout_ms:
|
||||
type: integer
|
||||
description: Daemon 協商模式 timeout,預設 25000ms(協商上限)
|
||||
default: 25000
|
||||
output_schema:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
data:
|
||||
type: object
|
||||
description: 同步完成時的回應
|
||||
properties:
|
||||
text: { type: string, description: Mira 的回覆文字 }
|
||||
task_id: { type: string }
|
||||
model: { type: string, description: 「實際 routing 用的模型(haiku / sonnet)」 }
|
||||
pending:
|
||||
type: boolean
|
||||
description: 「true 時表示 daemon 切到非同步模式,task 還在跑,需 polling」
|
||||
task_id:
|
||||
type: string
|
||||
description: pending=true 時用此 id polling
|
||||
poll_url:
|
||||
type: string
|
||||
description: GET 此 URL 查詢任務進度 / 結果
|
||||
error:
|
||||
type: string
|
||||
gherkin_tests:
|
||||
- scenario: "缺 mira_token"
|
||||
given: '{"prompt":"hi"}'
|
||||
then_contains: '{"success":false'
|
||||
- scenario: "簡短對話 25s 內回完"
|
||||
given: '{"mira_token":"...","prompt":"1+1=?"}'
|
||||
then_contains: 'success'
|
||||
tags: [ai, llm, claude, mira, primitive]
|
||||
description: "呼叫 Mira daemon (Hetzner cloud-cto) 進行 AI 對話。Daemon 內部用 Claude Agent SDK,內建 Mira 副駕 persona,可長執行任務。所有 mira-app 的 AI workflow(自動回覆、wiki 合成、新聞註解)都用此零件。"
|
||||
config_example: |
|
||||
ai_reply:
|
||||
mira_token: "{{secret.mira_token}}"
|
||||
prompt: |
|
||||
用戶 leo 在 mira 河道發了這則貼文:
|
||||
「{{trigger.post_content}}」
|
||||
|
||||
請以副駕 AI 的身份留言回應,簡短繁中,務實。
|
||||
timeout_ms: 25000
|
||||
@@ -0,0 +1,3 @@
|
||||
module claude_api
|
||||
|
||||
go 1.21
|
||||
@@ -0,0 +1,180 @@
|
||||
// claude_api — 呼叫 Mira daemon(Hetzner 上跑的 Claude Agent SDK 服務)
|
||||
//
|
||||
// 架構決策(2026-05-06):
|
||||
// 不直打 Anthropic Messages API(OAuth token 限制 system prompt 角色 → rate_limit_error)
|
||||
// 改透過已部署的 cloud-cto Mira daemon (https://mira.uncle6.me/mira/execute)
|
||||
// 該 daemon 用 Claude Agent SDK,已內建 Mira persona,可長執行任務
|
||||
//
|
||||
// SDD: polaris/mira/.agents/specs/mira-app/design.md §6(五個 P0 零件)
|
||||
//
|
||||
//go:build tinygo
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
//go:wasmimport u6u http_request
|
||||
func hostHttpRequest(
|
||||
urlPtr uintptr, urlLen uint32,
|
||||
methodPtr uintptr, methodLen uint32,
|
||||
headersPtr uintptr, headersLen uint32,
|
||||
bodyPtr uintptr, bodyLen uint32,
|
||||
outPtr uintptr, outLenPtr uintptr,
|
||||
) uint32
|
||||
|
||||
type Input struct {
|
||||
MiraURL string `json:"mira_url"` // 預設 https://mira.uncle6.me
|
||||
MiraToken string `json:"mira_token"` // Mira daemon Bearer token
|
||||
Prompt string `json:"prompt"` // 必填:要傳給 Mira 的訊息
|
||||
TimeoutMS int `json:"timeout_ms"` // 預設 25000(daemon 協商模式上限)
|
||||
Model string `json:"model"` // 'haiku' / 'sonnet' / 'opus',預設 haiku(daemon 端)
|
||||
CallbackURL string `json:"callback_url"` // optional:daemon 完成 task 時 POST 此 URL 通知(Resumable workflow,SDD: resumable-workflow)
|
||||
}
|
||||
|
||||
var dummy [1]byte
|
||||
|
||||
func safePtr(b []byte) (uintptr, uint32) {
|
||||
if len(b) == 0 {
|
||||
return uintptr(unsafe.Pointer(&dummy[0])), 0
|
||||
}
|
||||
return uintptr(unsafe.Pointer(&b[0])), uint32(len(b))
|
||||
}
|
||||
|
||||
func main() {
|
||||
raw, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
writeError("failed to read stdin: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var input Input
|
||||
if err := json.Unmarshal(raw, &input); err != nil {
|
||||
writeError("invalid input JSON: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if input.MiraToken == "" {
|
||||
writeError("mira_token 必填(Mira daemon Bearer token)")
|
||||
return
|
||||
}
|
||||
if input.Prompt == "" {
|
||||
writeError("prompt 必填")
|
||||
return
|
||||
}
|
||||
|
||||
miraURL := input.MiraURL
|
||||
if miraURL == "" {
|
||||
miraURL = "https://mira.uncle6.me"
|
||||
}
|
||||
timeoutMS := input.TimeoutMS
|
||||
if timeoutMS <= 0 {
|
||||
// 預設 120s:daemon 協商期會在 25s 切非同步 + callback;
|
||||
// callback_url 存在時,timeout 上限不重要(daemon 會 fire callback 不論多久)
|
||||
timeoutMS = 120000
|
||||
}
|
||||
|
||||
// Mira daemon /execute 介面
|
||||
body := map[string]interface{}{
|
||||
"prompt": input.Prompt,
|
||||
"timeout_ms": timeoutMS,
|
||||
}
|
||||
if input.Model != "" {
|
||||
body["model"] = input.Model
|
||||
}
|
||||
if input.CallbackURL != "" {
|
||||
body["callback_url"] = input.CallbackURL
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
|
||||
headers := map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer " + input.MiraToken,
|
||||
}
|
||||
headersBytes, _ := json.Marshal(headers)
|
||||
|
||||
url := miraURL + "/mira/execute"
|
||||
urlBytes := []byte(url)
|
||||
methodBytes := []byte("POST")
|
||||
|
||||
outBuf := make([]byte, 1024*1024) // 1MB
|
||||
var outLen uint32
|
||||
|
||||
urlPtr, urlLen := safePtr(urlBytes)
|
||||
methodPtr, methodLen := safePtr(methodBytes)
|
||||
headersPtr, headersLen := safePtr(headersBytes)
|
||||
bodyPtr, bodyLenU := safePtr(bodyBytes)
|
||||
|
||||
result := hostHttpRequest(
|
||||
urlPtr, urlLen,
|
||||
methodPtr, methodLen,
|
||||
headersPtr, headersLen,
|
||||
bodyPtr, bodyLenU,
|
||||
uintptr(unsafe.Pointer(&outBuf[0])), uintptr(unsafe.Pointer(&outLen)),
|
||||
)
|
||||
|
||||
if result != 0 {
|
||||
writeError("Mira daemon request failed (host_http_request returned non-zero)")
|
||||
return
|
||||
}
|
||||
|
||||
respStr := string(outBuf[:outLen])
|
||||
var resp map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(respStr), &resp); err != nil {
|
||||
writeError("Mira returned non-JSON: " + respStr)
|
||||
return
|
||||
}
|
||||
|
||||
// 偵測錯誤回應
|
||||
if errObj, hasErr := resp["error"]; hasErr {
|
||||
errBytes, _ := json.Marshal(errObj)
|
||||
writeError("Mira error: " + string(errBytes))
|
||||
return
|
||||
}
|
||||
|
||||
// daemon 回應格式:
|
||||
// 同步完成: {"task_id":"...","status":"done","output":"...","model":"..."}
|
||||
// 非同步: {"task_id":"...","status":"running","estimated_seconds":N}
|
||||
|
||||
status, _ := resp["status"].(string)
|
||||
if status == "running" {
|
||||
// 還沒完成,回傳 task_id 給 caller 自己 polling
|
||||
out, _ := json.Marshal(map[string]interface{}{
|
||||
"success": true,
|
||||
"pending": true,
|
||||
"task_id": resp["task_id"],
|
||||
"estimated_seconds": resp["estimated_seconds"],
|
||||
"poll_url": miraURL + "/mira/execute/" + toString(resp["task_id"]),
|
||||
})
|
||||
os.Stdout.Write(out)
|
||||
return
|
||||
}
|
||||
|
||||
// status == "done" 的場景
|
||||
out := map[string]interface{}{
|
||||
"success": true,
|
||||
"data": map[string]interface{}{
|
||||
"text": resp["output"],
|
||||
"task_id": resp["task_id"],
|
||||
"model": resp["model"],
|
||||
},
|
||||
}
|
||||
outJSON, _ := json.Marshal(out)
|
||||
os.Stdout.Write(outJSON)
|
||||
}
|
||||
|
||||
func toString(v interface{}) string {
|
||||
if s, ok := v.(string); ok {
|
||||
return s
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func writeError(msg string) {
|
||||
out, _ := json.Marshal(map[string]interface{}{"success": false, "error": msg})
|
||||
os.Stdout.Write(out)
|
||||
}
|
||||
Reference in New Issue
Block a user