Files
Arcrun/registry/components/kbdb_get/main.go
T
Leo 711af5dbf2 feat(arcrun): kbdb_get 加 type/source/user_id filter
之前只支援 block_id / page_name,撈「source=km-writer-direct 的 note」這類
跨 page 查詢做不到。Wiki UI 7B.3g 跟 mira_feed_watcher 都要用 client-side
filter 繞,違反「邊用 arcrun 邊修」原則。

擴 contract:保留既有 block_id (mode A) + page_name (mode B),新增純 filter
mode C:type / source / user_id 任意組合。同時 page_name + filter 也允許組合。

驗證:source=km-writer-direct&type=note&limit=5 撈到 leo 5 筆未處理河道貼文。

對應 SDD: arcrun.md 三-B 新零件 checklist + tasks.md 7B.3h(mira_feed_watcher
正在組)。
2026-05-14 14:18:43 +08:00

212 lines
5.6 KiB
Go
Raw 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.
// kbdb_get — 從 KBDB 讀 blockGET /blocks?page_name=... 或 GET /blocks/:id
// thin wrapper:透過 host function http_request 呼叫 KBDB API
//
//go:build tinygo
package main
import (
"encoding/json"
"io"
"os"
"strconv"
"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 {
KBDBUrl string `json:"kbdb_url"` // optional
APIKey string `json:"api_key"` // 必填
BlockID string `json:"block_id"` // 模式 A:單一 block by id(最優先)
PageName string `json:"page_name"` // 模式 Bpage_name 為主,可疊 type/source/user_id filter
// 模式 C:純 filter 查詢,至少要有 type / source / user_id 其中一個(page_name 不必填)
Type string `json:"type"` // 例:wiki-page / wiki-paragraph / triplet / note
Source string `json:"source"` // 例:km-writer-direct / ai-canon-wiki
UserID string `json:"user_id"` // 例:inkstone_mira_tools
Limit int `json:"limit"` // optional, default 50
}
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 writeError(msg string) {
out, _ := json.Marshal(map[string]interface{}{"success": false, "error": msg})
os.Stdout.Write(out)
}
// urlEncode:簡易 query string encoder(只處理 KBDB 會用到的字元)
// 避免引入 net/url(白名單外)
func urlEncode(s string) string {
var out []byte
for i := 0; i < len(s); i++ {
c := s[i]
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') ||
c == '-' || c == '_' || c == '.' || c == '~' {
out = append(out, c)
} else {
const hex = "0123456789ABCDEF"
out = append(out, '%', hex[c>>4], hex[c&0x0f])
}
}
return string(out)
}
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.APIKey == "" {
writeError("api_key 必填")
return
}
// 至少要有一個查詢條件
hasFilter := input.Type != "" || input.Source != "" || input.UserID != ""
if input.BlockID == "" && input.PageName == "" && !hasFilter {
writeError("至少要有一個查詢條件:block_id / page_name / type / source / user_id")
return
}
kbdbURL := input.KBDBUrl
if kbdbURL == "" {
kbdbURL = "https://kbdb.finally.click"
}
limit := input.Limit
if limit <= 0 {
limit = 50
}
// 構造 URLblock_id 模式走 /blocks/:id(單一),其餘走 /blocks?...query 列表
var url string
if input.BlockID != "" {
url = kbdbURL + "/blocks/" + urlEncode(input.BlockID)
} else {
// list 模式:page_name / type / source / user_id 任意組合
params := []string{}
if input.PageName != "" {
params = append(params, "page_name="+urlEncode(input.PageName))
}
if input.Type != "" {
params = append(params, "type="+urlEncode(input.Type))
}
if input.Source != "" {
params = append(params, "source="+urlEncode(input.Source))
}
if input.UserID != "" {
params = append(params, "user_id="+urlEncode(input.UserID))
}
params = append(params, "limit="+strconv.Itoa(limit))
url = kbdbURL + "/blocks?"
for i, p := range params {
if i > 0 {
url += "&"
}
url += p
}
}
headers := map[string]string{
"Authorization": "Bearer " + input.APIKey,
}
headersBytes, _ := json.Marshal(headers)
method := "GET"
urlBytes := []byte(url)
methodBytes := []byte(method)
outBuf := make([]byte, 1<<20) // 1MBlist 可能很大)
var outLen uint32
urlPtr, urlLen := safePtr(urlBytes)
methodPtr, methodLen := safePtr(methodBytes)
headersPtr, headersLenU := safePtr(headersBytes)
bodyPtr, bodyLenU := safePtr(nil)
result := hostHttpRequest(
urlPtr, urlLen,
methodPtr, methodLen,
headersPtr, headersLenU,
bodyPtr, bodyLenU,
uintptr(unsafe.Pointer(&outBuf[0])), uintptr(unsafe.Pointer(&outLen)),
)
if result != 0 {
writeError("KBDB GET request failed (host_http_request returned non-zero)")
return
}
respStr := string(outBuf[:outLen])
// 解析回應
if input.BlockID != "" {
// 單一 blockKBDB 直接回 block 物件,包成 array 給下游 foreach
var block map[string]interface{}
if err := json.Unmarshal([]byte(respStr), &block); err != nil {
writeError("KBDB returned non-JSON: " + respStr)
return
}
if _, hasErr := block["error"]; hasErr {
out, _ := json.Marshal(map[string]interface{}{
"success": false, "error": block["error"],
})
os.Stdout.Write(out)
return
}
out, _ := json.Marshal(map[string]interface{}{
"success": true,
"blocks": []map[string]interface{}{block},
"count": 1,
})
os.Stdout.Write(out)
return
}
// page_name 列表模式:KBDB 回 {"blocks": [...], "count": N}
var listResp struct {
Blocks []map[string]interface{} `json:"blocks"`
Count int `json:"count"`
Error interface{} `json:"error"`
}
if err := json.Unmarshal([]byte(respStr), &listResp); err != nil {
writeError("KBDB returned non-JSON: " + respStr)
return
}
if listResp.Error != nil {
out, _ := json.Marshal(map[string]interface{}{
"success": false, "error": listResp.Error,
})
os.Stdout.Write(out)
return
}
out, _ := json.Marshal(map[string]interface{}{
"success": true,
"blocks": listResp.Blocks,
"count": listResp.Count,
})
os.Stdout.Write(out)
}