711af5dbf2
之前只支援 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 正在組)。
212 lines
5.6 KiB
Go
212 lines
5.6 KiB
Go
// kbdb_get — 從 KBDB 讀 block(GET /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"` // 模式 B:page_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
|
||
}
|
||
|
||
// 構造 URL:block_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) // 1MB(list 可能很大)
|
||
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 != "" {
|
||
// 單一 block:KBDB 直接回 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)
|
||
}
|