Files
Agent-System-Backend/internal/cmdbus/cmdbus.go
T
weichenandCursor b740f8b6f6 Add optional proofs for an exact human structure decision
Why: A model-visible flag cannot prove that a person confirmed one proposal revision and payload hash.
Evidence: go test ./pkg/extension ./internal/app ./internal/server -run 'HumanDecision|SurfaceSnapshot|ResolveHuman|NaturalLanguage' passed locally.
Impact: SurfaceVersion 2.22.0-experimental adds HumanDecisionVerifier. Existing Host methods stay unchanged, and the base does not consume the proof.
Refs: W4b human confirmation design r1
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-15 02:33:37 +00:00

632 lines
23 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.
// Package cmdbus 组合 Agent-Command 生态库:注册、四入口分发(button/card/slash/chat→api
// 归一)、审计信封落盘与危险命令的内联确认卡收口(提案 T3 / D27、D30、D33)。
// 与 intentgate 之于 agent-intent 同构:schema 权威在生态库,宿主只做装配与执行适配。
package cmdbus
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"strings"
"sync"
"time"
agentcommand "agent-command"
"agent-system-core"
)
// AuditStore 审计落盘端口(store 实现;v7 建表、v18 加 owner 列)。
// owner 是请求租户(req.Owner):审计行按租户归属,查询按 owner 过滤,
// 跨租户审计不可见(信封 Actor 是发起主体,不作归属依据)。
type AuditStore interface {
AppendAudit(owner string, env agentcommand.AuditEnvelope) error
}
// CardPort 确认卡所需卡片能力(cardsvc.Service 实现)。
type CardPort interface {
Create(owner string, card core.ActivityCard, ref string) (core.ActivityCard, error)
SetStatus(owner, id string, status core.CardStatus, tone, note string) (core.ActivityCard, error)
}
// Handler 宿主执行适配器:同步内置用例返回 payload;异步执行自行返回 accepted 回执。
type Handler func(ctx context.Context, req Request) (any, error)
// CommandTool EXT-14:带 exposeAsTool 声明的命令的工具投影视图(runmgr 消费)。
// Risk 取 low|medium|high(工具权限声明口径);ID 即工具名(命令 id)。
type CommandTool struct {
ID string
Title string
Description string
Params json.RawMessage
Risk string
}
// CommandTools 全部带 ExposeAsTool 的已启用命令(EXT-14 工具投影源)。
// ToolGate 是 owner 级工具注入闸(U3:禁用即不注入能力):返回 false 时该命令
// 不投影给该 owner 的模型。组合根按需装配(如 generative-ui 按 activation 台账)。
type ToolGate func(owner, commandID string) bool
// EffectiveAccountGuard checks the current account state before a command can
// create a confirmation or invoke a handler. The composition root supplies the
// check; nil keeps standalone and auth-off fixtures unchanged. The actor is
// derived only from trusted Request identity fields, never Params. This is an
// early check, not an atomic fence across handler databases or external calls.
type EffectiveAccountGuard func(ctx context.Context, actor string) error
// SetToolGate 装配 owner 级注入闸(组合根;启动期一次)。
func (b *Bus) SetToolGate(g ToolGate) { b.toolGate = g }
func (b *Bus) CommandTools() []CommandTool {
return b.CommandToolsFor("")
}
// CommandToolsFor 按 owner 过滤工具投影(gate 为 nil 时不过滤;owner 为空时
// gated 命令一律不投影——fail-closed)。
func (b *Bus) CommandToolsFor(owner string) []CommandTool {
out := []CommandTool{}
for _, r := range b.reg.List() {
if !r.Enabled || r.Spec.ExposeAsTool == nil {
continue
}
if b.toolGate != nil && !b.toolGate(owner, r.Spec.ID) {
continue
}
risk := r.Spec.ExposeAsTool.Risk
if risk == "" {
risk = "low"
}
desc := r.Spec.Description
if desc == "" {
desc = r.Spec.Title
}
out = append(out, CommandTool{ID: r.Spec.ID, Title: r.Spec.Title,
Description: desc, Params: r.Spec.ParamsSchema, Risk: risk})
}
return out
}
// Request 一次分发请求(入口归一后的统一形态)。
type Request struct {
ID string
Params map[string]any
Entrance string // button | card | slash | chat | api(归一)
Actor string
Owner string
ConversationID string
OperationID string // overwritten by the trusted bus before handler execution
AnchorMessageID string // 确认卡锚定的发起消息(内联渲染)
Confirmed bool // 危险命令经确认卡放行后的执行
HostProjectID string // EXT-18:分发所属基座项目(空=无项目上下文)
// HumanGrant is set only by the authenticated session path or the human
// decision resolver. Client JSON and agent tool dispatch leave it nil.
HumanGrant *HumanGrant
}
// HumanGrant ties this dispatch to one persisted human response.
// It is not a secret token and is not read from command params.
type HumanGrant struct {
CardID string
ProofID string
Actor string
Decision string
}
// Response 分发结果:回执 + 处理负载 + 可能的确认卡。
type Response struct {
Receipt agentcommand.ExecReceipt
Payload any
Card *core.ActivityCard // 高危未确认时创建的内联确认卡
}
// pending 确认卡挂起的执行(进程内;重启后确认返回 rejected expired——v1 限制)。
type pending struct {
req Request
cardID string
}
type Bus struct {
reg *agentcommand.Registry
audit AuditStore
cards CardPort
now func() time.Time
handlers map[string]Handler
slash map[string]string // "/x" → command id
pendMu sync.Mutex
pending map[string]pending
pendingStore PendingStore
intents *IntentStore // G2-2:意图声明表(自然语言路由)
toolGate ToolGate // U3:owner 级工具注入闸(nil = 不过滤)
accountGuard EffectiveAccountGuard // A1: optional command execution precheck
strictAccountWrites bool
allowImplicitLocal bool
}
// PendingStore 把危险命令的确认挂起态落盘。进程内 map 仍是热路径;重启后从存储取回。
type PendingStore interface {
SavePendingConfirm(cardID, owner, payload string) error
TakePendingConfirm(cardID string) (owner, payload string, ok bool, err error)
}
func New(audit AuditStore, cards CardPort, now func() time.Time) *Bus {
if now == nil {
now = time.Now
}
return &Bus{
reg: agentcommand.NewRegistry(), audit: audit, cards: cards, now: now,
handlers: map[string]Handler{}, slash: map[string]string{}, pending: map[string]pending{},
intents: NewIntentStore(),
}
}
// Registry 暴露注册表(列清单/插件 overlay 注册用)。
func (b *Bus) Registry() *agentcommand.Registry { return b.reg }
// SetPendingStore 注入确认挂起态的持久化实现(nil = 仅进程内)。
func (b *Bus) SetPendingStore(st PendingStore) { b.pendingStore = st }
// SetEffectiveAccountGuard installs the composition-root check at startup.
// Handler Store writes still need their own final transaction fence.
func (b *Bus) SetEffectiveAccountGuard(g EffectiveAccountGuard) { b.accountGuard = g }
// RequireAccountCheckedWrites makes every Bus-owned durable sink use a final
// account transaction. The composition root enables this after wiring Store
// and Cards; standalone in-memory fixtures retain their legacy ports.
func (b *Bus) RequireAccountCheckedWrites(allowImplicitLocal bool) {
b.strictAccountWrites = true
b.allowImplicitLocal = allowImplicitLocal
}
func (b *Bus) createConfirmCard(actor string, card core.ActivityCard, ref string) (core.ActivityCard, error) {
if !b.strictAccountWrites {
return b.cards.Create(actor, card, ref)
}
checked, ok := b.cards.(interface {
CreateAccountChecked(string, core.ActivityCard, string, bool) (core.ActivityCard, error)
})
if !ok {
return core.ActivityCard{}, fmt.Errorf("cmdbus: account-checked card port required")
}
return checked.CreateAccountChecked(actor, card, ref, b.allowImplicitLocal)
}
func (b *Bus) setConfirmCardStatus(actor, id string, status core.CardStatus, tone, note string) (core.ActivityCard, error) {
if !b.strictAccountWrites {
return b.cards.SetStatus(actor, id, status, tone, note)
}
checked, ok := b.cards.(interface {
SetStatusAccountChecked(string, string, core.CardStatus, string, string, bool) (core.ActivityCard, error)
})
if !ok {
return core.ActivityCard{}, fmt.Errorf("cmdbus: account-checked card port required")
}
return checked.SetStatusAccountChecked(actor, id, status, tone, note, b.allowImplicitLocal)
}
func (b *Bus) savePending(cardID, owner, payload string) error {
if !b.strictAccountWrites {
return b.pendingStore.SavePendingConfirm(cardID, owner, payload)
}
checked, ok := b.pendingStore.(interface {
SavePendingConfirmAccountChecked(string, string, string, bool) error
})
if !ok {
return fmt.Errorf("cmdbus: account-checked pending store required")
}
return checked.SavePendingConfirmAccountChecked(cardID, owner, payload, b.allowImplicitLocal)
}
func (b *Bus) takePending(cardID, owner string) (string, string, bool, error) {
if !b.strictAccountWrites {
return b.pendingStore.TakePendingConfirm(cardID)
}
checked, ok := b.pendingStore.(interface {
TakePendingConfirmAccountChecked(string, string, bool) (string, string, bool, error)
})
if !ok {
return "", "", false, fmt.Errorf("cmdbus: account-checked pending store required")
}
return checked.TakePendingConfirmAccountChecked(cardID, owner, b.allowImplicitLocal)
}
// RegisterBuiltin 登记内置命令并绑定宿主处理器与 `/` 别名。
func (b *Bus) RegisterBuiltin(spec agentcommand.CommandSpec, slash string, h Handler) error {
spec.Binding = agentcommand.Binding{Kind: agentcommand.BindBuiltin}
if err := b.reg.Register(spec, agentcommand.SourceBuiltin); err != nil {
return err
}
if h != nil {
b.handlers[spec.ID] = h
}
if slash != "" {
b.slash[strings.ToLower(slash)] = spec.ID
}
return nil
}
// RegisterUser 登记用户提示词命令。重复登记视为已存在。执行不走总线,发送时展开模板。
func (b *Bus) RegisterUser(spec agentcommand.CommandSpec, slash string) error {
spec.Binding = agentcommand.Binding{Kind: agentcommand.BindMessage}
if err := b.reg.Register(spec, agentcommand.SourceUser); err != nil && !errors.Is(err, agentcommand.ErrDuplicate) {
return err
}
if slash != "" {
b.slash[strings.ToLower(slash)] = spec.ID
}
return nil
}
// UnregisterUser 卸下用户命令。不存在时忽略。
func (b *Bus) UnregisterUser(id, slash string) {
_ = b.reg.Unregister(id)
if slash != "" {
delete(b.slash, strings.ToLower(slash))
}
}
// RegisterPluginCommands 插件命令 overlay(manifest provides.commands;失败 fail-closed 返回)。
func (b *Bus) RegisterPluginCommands(overlay []json.RawMessage) error {
for _, raw := range overlay {
var spec agentcommand.CommandSpec
if err := json.Unmarshal(raw, &spec); err != nil {
return fmt.Errorf("cmdbus: plugin command spec: %w", err)
}
if err := b.reg.Register(spec, agentcommand.SourcePlugin); err != nil {
return fmt.Errorf("cmdbus: register %s: %w", spec.ID, err)
}
b.bindPluginSlash(spec)
}
return nil
}
// bindPluginSlash EXT-10②:插件命令规格自带的 slash 别名绑定(与内置命令同一张
// 别名表——四入口同源:slash 命中即同一命令 id、同一审计信封)。
func (b *Bus) bindPluginSlash(spec agentcommand.CommandSpec) {
if spec.Slash != "" {
b.slash[strings.ToLower(spec.Slash)] = spec.ID
}
}
// RegisterPluginCommand 注册进程内插件命令(EXT-1):登记规格并绑定宿主处理器。
// 仅登记规格的声明式路径仍走 RegisterPluginCommands(无 handler,分发即 rejected)。
func (b *Bus) RegisterPluginCommand(spec agentcommand.CommandSpec, h Handler) error {
if err := b.reg.Register(spec, agentcommand.SourcePlugin); err != nil {
return fmt.Errorf("cmdbus: register %s: %w", spec.ID, err)
}
if h != nil {
b.handlers[spec.ID] = h
}
b.bindPluginSlash(spec)
return nil
}
// Intents 意图声明表(G2-2:注册面与路由查询共用)。
func (b *Bus) Intents() *IntentStore { return b.intents }
// ResolveSlash `/` 入口解析:返回命令 id 与原始字段(参数由宿主处理器按命令语义解析)。
func (b *Bus) ResolveSlash(cmd string) (string, bool) {
id, ok := b.slash[strings.ToLower(strings.TrimSpace(cmd))]
return id, ok
}
// SlashTable slash 别名表(清单组装用;只读副本)。
func (b *Bus) SlashTable() map[string]string {
out := make(map[string]string, len(b.slash))
for k, v := range b.slash {
out[k] = v
}
return out
}
// HasCommand 同源判定(intentgate 命中意图后查总线是否承接)。
func (b *Bus) HasCommand(id string) bool {
_, _, err := b.reg.Resolve(id)
return err == nil
}
// List 四入口共享的命令清单(`/` 菜单、按钮、卡片 action 提示用)。
func (b *Bus) List() []agentcommand.Registered { return b.reg.List() }
// Dispatch 统一分发:账号预检→校验→危险收口→执行→审计。账号预检拒绝只返回回执,
// 避免停用账号或检查故障时继续写入审计;其余路径维持四入口审计对称。
func (b *Bus) Dispatch(ctx context.Context, req Request) Response {
start := b.now()
req.Entrance = agentcommand.CanonicalEntrance(req.Entrance)
if err := b.checkEffectiveAccount(ctx, accountActor(req)); err != nil {
// A rejected account cannot create a card, pending row, audit row, or
// invoke a handler. A failed read is also a denial.
return b.accountRejected(req.ID, start)
}
receipt := func(state agentcommand.ResultState, op, reason string) Response {
return Response{Receipt: b.auditReceipt(req, state, op, reason, start)}
}
spec, _, err := b.reg.Resolve(req.ID)
if err != nil {
return receipt(agentcommand.StateRejected, "", "unknown or disabled command: "+req.ID)
}
if err := validateParams(spec.ParamsSchema, req.Params); err != nil {
return receipt(agentcommand.StateRejected, "", "params: "+err.Error())
}
// 高危收口:非确认入口先落内联确认卡(committed 语义 = 卡已创建;确认后二次分发执行)。
if spec.Danger == agentcommand.DangerHigh && !req.Confirmed {
if b.cards == nil || req.ConversationID == "" {
return receipt(agentcommand.StateRejected, "", "high-danger command requires conversation context for inline confirm")
}
if err := b.checkEffectiveAccount(ctx, accountActor(req)); err != nil {
return b.accountRejected(req.ID, start)
}
card, err := b.createConfirmCard(accountActor(req), confirmCard(spec, req), "cmd:"+spec.ID)
if err != nil {
return receipt(agentcommand.StateUnknown, "", "confirm card: "+err.Error())
}
if b.pendingStore != nil {
raw, _ := json.Marshal(req)
if err := b.savePending(card.ID, accountActor(req), string(raw)); err != nil {
return receipt(agentcommand.StateUnknown, "", "confirm pending: "+err.Error())
}
} else if b.strictAccountWrites {
return receipt(agentcommand.StateUnknown, "", "confirm pending: account-checked store required")
}
b.pendMu.Lock()
b.pending[card.ID] = pending{req: req, cardID: card.ID}
b.pendMu.Unlock()
r := b.auditReceipt(req, agentcommand.StateAccepted, card.ID, "awaiting explicit confirm", start)
return Response{Receipt: r, Card: &card}
}
h, ok := b.handlers[req.ID]
if !ok {
return receipt(agentcommand.StateRejected, "", "no host handler bound for "+req.ID)
}
if err := b.checkEffectiveAccount(ctx, accountActor(req)); err != nil {
return b.accountRejected(req.ID, start)
}
// The domain handler can persist this trusted ID with its transaction/outbox.
// Entropy, not the clock or caller Params, distinguishes concurrent operations.
// A handler failure still writes only a rejected audit receipt without an ID.
var opBytes [24]byte
if _, err := rand.Read(opBytes[:]); err != nil {
return receipt(agentcommand.StateUnknown, "", "operation ID unavailable")
}
req.OperationID = "op_" + hex.EncodeToString(opBytes[:])
payload, err := h(ctx, req)
if err != nil {
return receipt(agentcommand.StateRejected, "", "rejected: "+err.Error())
}
return Response{Receipt: b.auditReceipt(req, agentcommand.StateCommitted, req.OperationID, "", start), Payload: payload}
}
// Confirm 确认卡回道:actionID=confirm 执行挂起请求(entrance 归一 card),其余视为取消。
func (b *Bus) Confirm(owner, cardID, actionID string) Response {
return b.ConfirmContext(context.Background(), owner, cardID, actionID)
}
// ConfirmContext carries the request cancellation through the confirmed
// handler and repeats the account check before consuming durable state.
func (b *Bus) ConfirmContext(ctx context.Context, owner, cardID, actionID string) Response {
// The caller supplies the authenticated card owner. Check before even
// consuming a persisted pending request; Dispatch checks the stored
// request again immediately before executing its handler.
if err := b.checkEffectiveAccount(ctx, owner); err != nil {
return b.accountRejected("unknown", b.now())
}
b.pendMu.Lock()
p, ok := b.pending[cardID]
b.pendMu.Unlock()
if ok && p.req.Owner != "" && p.req.Owner != owner {
return Response{Receipt: agentcommand.ExecReceipt{CommandID: p.req.ID, State: agentcommand.StateRejected, Reason: "confirm owner mismatch", Time: b.now().UTC()}}
}
if b.pendingStore != nil {
storedOwner, payload, found, err := b.takePending(cardID, owner)
if err != nil {
return b.accountRejected("unknown", b.now())
}
if found {
var req Request
if storedOwner != owner || json.Unmarshal([]byte(payload), &req) != nil {
return Response{Receipt: agentcommand.ExecReceipt{CommandID: "unknown", State: agentcommand.StateRejected, Reason: "invalid pending confirm", Time: b.now().UTC()}}
}
p = pending{req: req, cardID: cardID}
ok = true
} else if b.strictAccountWrites {
ok = false
}
} else if b.strictAccountWrites {
ok = false
}
if !ok {
return Response{Receipt: agentcommand.ExecReceipt{CommandID: "unknown", State: agentcommand.StateRejected, Reason: "no pending confirm (expired or already resolved)", Time: b.now().UTC()}}
}
req := p.req
if req.Owner != "" && req.Owner != owner {
return Response{Receipt: agentcommand.ExecReceipt{CommandID: req.ID, State: agentcommand.StateRejected, Reason: "confirm owner mismatch", Time: b.now().UTC()}}
}
b.pendMu.Lock()
delete(b.pending, cardID)
b.pendMu.Unlock()
req.Confirmed = true
req.Entrance = "card"
if actionID != "confirm" {
if b.cards != nil {
_, _ = b.setConfirmCardStatus(owner, cardID, core.CardResolved, "muted", "cancelled")
}
start := b.now()
return Response{Receipt: b.auditReceipt(req, agentcommand.StateRejected, "", "cancelled at confirm card", start)}
}
resp := b.Dispatch(ctx, req)
if b.cards != nil {
if resp.Receipt.State == agentcommand.StateCommitted {
_, _ = b.setConfirmCardStatus(owner, cardID, core.CardResolved, "ok", "confirmed")
} else {
_, _ = b.setConfirmCardStatus(owner, cardID, core.CardFailed, "danger", resp.Receipt.Reason)
}
}
return resp
}
func accountActor(req Request) string {
// Plugin and delegated commands use Actor for an audit label such as a
// plugin ID; Owner is the effective account whose state controls effects.
if req.Owner != "" {
return req.Owner
}
return req.Actor
}
func (b *Bus) checkEffectiveAccount(ctx context.Context, actor string) error {
if err := ctx.Err(); err != nil {
return err
}
if b.accountGuard == nil {
return nil
}
return b.accountGuard(ctx, actor)
}
func (b *Bus) accountRejected(commandID string, start time.Time) Response {
return Response{Receipt: agentcommand.ExecReceipt{
CommandID: commandID, State: agentcommand.StateRejected,
Reason: "effective account unavailable", Time: start.UTC(),
}}
}
func (b *Bus) auditReceipt(req Request, state agentcommand.ResultState, op, reason string, start time.Time) agentcommand.ExecReceipt {
env := agentcommand.AuditEnvelope{
Time: start.UTC(), Actor: req.Actor, CommandID: req.ID, Entrance: agentcommand.CanonicalEntrance(req.Entrance),
ParamsDigest: digest(req.Params), State: state, DurationMs: float64(b.now().Sub(start).Milliseconds()),
OperationID: op, ConversationID: req.ConversationID,
}
if _, src, err := b.reg.Resolve(req.ID); err == nil {
env.Source = src
}
var err error
if b.strictAccountWrites {
checked, ok := b.audit.(interface {
AppendAuditAccountChecked(string, agentcommand.AuditEnvelope, bool) error
})
if !ok {
err = fmt.Errorf("cmdbus: account-checked audit store required")
} else {
err = checked.AppendAuditAccountChecked(accountActor(req), env, b.allowImplicitLocal)
}
} else {
err = b.audit.AppendAudit(req.Owner, env)
}
if err != nil {
// 审计不可用:结果标记未知,不臆断成功(四态语义)。
return agentcommand.ExecReceipt{CommandID: req.ID, State: agentcommand.StateUnknown, Reason: "audit unavailable: " + err.Error(), Time: b.now().UTC()}
}
return agentcommand.ExecReceipt{CommandID: req.ID, State: state, OperationID: op, Reason: reason, Time: b.now().UTC()}
}
func digest(params map[string]any) string {
if len(params) == 0 {
return ""
}
raw, _ := json.Marshal(params)
sum := sha256.Sum256(raw)
return hex.EncodeToString(sum[:8])
}
// confirmCard 构造内联确认卡:锚定发起消息、双现于收件箱、动作收口回总线。
func confirmCard(spec agentcommand.CommandSpec, req Request) core.ActivityCard {
body := []core.UINode{{Type: core.UIMarkdown, Markdown: "**" + spec.Title + "** 需要确认后执行。"}}
if spec.Description != "" {
body = append(body, core.UINode{Type: core.UIMarkdown, Markdown: spec.Description})
}
rows := [][2]string{{"入口", req.Entrance}}
for k, v := range req.Params {
rows = append(rows, [2]string{k, fmt.Sprintf("%v", v)})
}
body = append(body, core.UINode{Type: core.UIKV, KV: rows})
return core.ActivityCard{
Kind: core.CardKindConfirm, Status: core.CardActionRequired, Tone: "warn",
Title: "确认执行:" + spec.Title,
Body: body,
Actions: []core.UIAction{
{ID: "confirm", Label: "确认执行", Style: "primary"},
{ID: "cancel", Label: "取消", Style: "danger"},
},
ConversationID: core.ID(req.ConversationID),
AnchorMessageID: core.ID(req.AnchorMessageID),
}
}
// validateParams JSON Schema draft-07 子集(type:object + required + 标量类型),宿主侧校验。
func validateParams(schema json.RawMessage, params map[string]any) error {
if len(schema) == 0 {
return nil
}
var sc struct {
Type string `json:"type"`
Required []string `json:"required"`
Properties map[string]any `json:"properties"`
}
if err := json.Unmarshal(schema, &sc); err != nil {
return fmt.Errorf("bad schema: %w", err)
}
if sc.Type != "" && sc.Type != "object" {
return fmt.Errorf("schema type must be object, got %s", sc.Type)
}
for _, k := range sc.Required {
if _, ok := params[k]; !ok {
return fmt.Errorf("missing required param %s", k)
}
}
for k, v := range params {
decl, ok := sc.Properties[k]
if !ok {
continue // 未声明属性放行(宿主最小校验)
}
m, _ := decl.(map[string]any)
want, _ := m["type"].(string)
if want != "" && !scalarTypeOk(want, v) {
return fmt.Errorf("param %s: want %s", k, want)
}
}
return nil
}
func scalarTypeOk(want string, v any) bool {
switch want {
case "string":
_, ok := v.(string)
return ok
case "number":
return isNum(v)
case "boolean":
_, ok := v.(bool)
return ok
case "integer":
if !isNum(v) {
return false
}
return float64(toFloat(v)) == float64(int64(toFloat(v)))
}
return true
}
func isNum(v any) bool {
switch v.(type) {
case float64, float32, int, int64:
return true
}
return false
}
func toFloat(v any) float64 {
switch x := v.(type) {
case float64:
return x
case float32:
return float64(x)
case int:
return float64(x)
case int64:
return float64(x)
}
return 0
}