Çŕéàţîñĝ Ţööļš ŵîţĥ Þàŕàḿéţéŕ Šçĥéḿàš
Ţĥîš ĝüîđé çöṽéŕš ĥöŵ ţö çŕéàţé à ţööļ ŵîţĥ à þàŕàḿéţéŕ šçĥéḿà šö ţĥàţ ţĥé ÜÎ àñđ ÇĻÎ çàñ àüţö-ĝéñéŕàţé çöñƒîĝüŕàţîöñ ƒöŕḿš àñđ ṽàļîđàţé üšéŕ îñþüţ.
Ţööļ ƃàšîçš
Éṽéŕý ţööļ îš ƃüîļţ öñ tool.BaseTool. Ƒöŕ Ƃļöçķš (ţĥé ţŕàñšļàţàƃļé üñîţ) à
ţööļ šéţš éẋàçţļý öñé çàþàƃîļîţý-ţýþéđ ĥàñđļéŕ, àñđ ţĥé ṽîéŵ îţ ŕéçéîṽéš ƃöüñđš
ŵĥàţ îţ ḿàý ŵŕîţé (É-03): Annotate(BlockView) ŕéàđš šöüŕçé àñđ ţàŕĝéţ àñđ
ŵŕîţéš öñļý öṽéŕļàýš, àññöţàţîöñš, àñđ þŕöþéŕţîéš; Produce(VariantView)
ŵŕîţéš ţĥé ţàŕĝéţ; Transform(BlockView) ŕéţüŕñš àñ éđîţ þļàñ ţĥàţ ţĥé
ƒŕàḿéŵöŕķ àþþļîéŕ üšéš ţö ŕéŵŕîţé ţĥé šöüŕçé. Ţĥé ŵŕöñĝ ŵŕîţéš
àŕé ñöţ öñ ţĥé ṽîéŵ: àñ àññöţàţöŕ ĥàš ñö ţàŕĝéţ šéţţéŕ ţö çàļļ, àñđ à
ţŕàñšƒöŕḿéŕ ĥöļđš ñö šöüŕçé šéţţéŕ àţ àļļ. Öţĥéŕ
Þàŕţ ţýþéš (Đàţà, Ḿéđîà, Ļàýéŕ, Ĝŕöüþ) üšé ţĥé üñţýþéđ Handle*Fn ƒîéļđš. Þàŕţš
ýöü đöñ'ţ ĥàñđļé þàšš ţĥŕöüĝĥ üñçĥàñĝéđ; à ĥàñđļéŕ ŕéţüŕñš àñ error (àñđ ḿàý
çàļļ v.Drop() ţö ŕéḿöṽé ţĥé ƃļöçķ ƒŕöḿ ţĥé šţŕéàḿ).
package mytool
import (
"strings"
"github.com/neokapi/neokapi/core/model"
"github.com/neokapi/neokapi/core/tool"
)
func NewMyTool(cfg *MyToolConfig) *tool.BaseTool {
t := &tool.BaseTool{
ToolName: "my-tool",
ToolDescription: "Does something useful",
Cfg: cfg,
}
// A tool declares its capability by which block handler it sets: the
// parameter type bounds what it may write (E-03):
// Annotate(BlockView): read-only: overlays / annotations / properties
// Produce(VariantView): writes the target; source stays read-only
// Transform(BlockView): edit producer: returns an EditPlan the
// framework applier applies to the source
// This tool writes a target, so it sets Produce.
t.Produce = func(v tool.VariantView) error {
if !v.Translatable() {
return nil // pass through
}
conf := t.Cfg.(*MyToolConfig)
text := v.SourceText()
if conf.Uppercase {
text = strings.ToUpper(text)
}
v.SetTargetText(model.LocaleID(conf.TargetLocale), text)
return nil
}
return t
}
Đéçļàŕîñĝ à þàŕàḿéţéŕ šçĥéḿà ŵîţĥ šţŕüçţ ţàĝš
Đéƒîñé à çöñƒîĝ šţŕüçţ ŵîţĥ éẋþöŕţéđ ƒîéļđš. Ţĥé schema šţŕüçţ ţàĝ çöñţŕöļš ĥöŵ éàçĥ ƒîéļđ àþþéàŕš îñ ţĥé ĝéñéŕàţéđ šçĥéḿà:
type MyToolConfig struct {
TargetLocale string `json:"targetLocale" schema:"description=Target locale for output"`
Uppercase bool `json:"uppercase" schema:"description=Convert text to uppercase,default=false"`
MaxLength int `json:"maxLength" schema:"description=Maximum output length (0 = unlimited),default=0"`
Mode string `json:"mode" schema:"description=Processing mode,enum=fast|thorough|balanced,default=balanced"`
}
Šüþþöŕţéđ šţŕüçţ ţàĝ ķéýš
| Ķéý | Éẋàḿþļé | Þüŕþöšé |
|---|---|---|
description | description=Target locale | Ĥüḿàñ-ŕéàđàƃļé ƒîéļđ đéšçŕîþţîöñ |
default | default=true | Đéƒàüļţ ṽàļüé |
enum | enum=fast|thorough | Àļļöŵéđ ṽàļüéš (þîþé-šéþàŕàţéđ) |
min | min=0 | Ḿîñîḿüḿ ñüḿéŕîç ṽàļüé |
max | max=100 | Ḿàẋîḿüḿ ñüḿéŕîç ṽàļüé |
widget | widget=regexBuilder | ÜÎ ŵîđĝéţ ĥîñţ |
placeholder | placeholder=en | Îñþüţ þļàçéĥöļđéŕ ţéẋţ |
group | group=validation | Þàŕàḿéţéŕ ĝŕöüþ ÎĐ |
Ĝö ţýþé ţö ĴŠÖÑ Šçĥéḿà ţýþé ḿàþþîñĝ
| Ĝö ţýþé | ĴŠÖÑ Šçĥéḿà ţýþé |
|---|---|
bool | boolean |
string | string |
int, int64, uint, éţç. | integer |
float32, float64 | number |
[]T | array |
map, struct | object |
Îñţéŕƒàçé, ƒüñçţîöñ, àñđ çĥàññéļ ƒîéļđš àŕé àüţöḿàţîçàļļý šķîþþéđ.
Ĥöŵ šçĥéḿà.ƑŕöḿŠţŕüçţ() ŵöŕķš
Ţĥé schema.FromStruct() ƒüñçţîöñ üšéš Ĝö ŕéƒļéçţîöñ ţö îñšþéçţ à çöñƒîĝ šţŕüçţ àñđ þŕöđüçé à ComponentSchema:
import "github.com/neokapi/neokapi/core/schema"
s := schema.FromStruct(&MyToolConfig{}, schema.ToolMeta{
ID: "my-tool",
Category: "transform",
DisplayName: "My Tool",
})
Ţĥé ƒüñçţîöñ:
- Îţéŕàţéš öṽéŕ éẋþöŕţéđ šţŕüçţ ƒîéļđš
- Ḿàþš Ĝö ţýþéš ţö ĴŠÖÑ Šçĥéḿà ţýþéš
- Þàŕšéš
schemašţŕüçţ ţàĝš ƒöŕ ḿéţàđàţà (đéšçŕîþţîöñ, đéƒàüļţ, éñüḿ, ŵîđĝéţ, éţç.) - Éẋţŕàçţš
groupţàĝš ţö ƃüîļđui:groupsƒöŕ ţĥé ÜÎ - Üšéš
jsonšţŕüçţ ţàĝš ƒöŕ ƒîéļđ ñàḿéš (ƒàļļš ƃàçķ ţö çàḿéļÇàšé çöñṽéŕšîöñ) - Ĝéñéŕàţéš à
ComponentSchemaŵîţĥtoolMetaḿéţàđàţà
Þàŕàḿéţéŕ ĝŕöüþš
Ƒîéļđš ŵîţĥ à group ţàĝ àŕé öŕĝàñîžéđ îñţö çöļļàþšîƃļé šéçţîöñš îñ ţĥé ÜÎ:
type CheckConfig struct {
CheckLeadingWS bool `schema:"description=Check leading whitespace,default=true,group=whitespace"`
CheckTrailingWS bool `schema:"description=Check trailing whitespace,default=true,group=whitespace"`
CheckEmptyTarget bool `schema:"description=Check empty translations,default=true,group=content"`
}
Ţĥîš þŕöđüçéš ţŵö çöļļàþšîƃļé ĝŕöüþš ("Ŵĥîţéšþàçé" àñđ "Çöñţéñţ") îñ ţĥé ĝéñéŕàţéđ ƒöŕḿ.
Ŕéĝîšţéŕîñĝ ŵîţĥ ŔéĝîšţéŕŴîţĥŠçĥéḿà()
Üšé RegisterWithSchema() îñšţéàđ öƒ Register() ţö îñçļüđé ţĥé šçĥéḿà îñ ţĥé
ŕéĝîšţŕý, àñđ þàîŕ îţ ŵîţĥ SetConfigFactory() šö à ÝÀḾĻ šţéþ'š config: ḿàþ
ŕéàçĥéš ţĥé ţööļ. Ţĥé þļàîñ ƒàçţöŕý ƃüîļđš à ţööļ ŵîţĥ đéƒàüļţ çöñƒîĝ; ţĥé
çöñƒîĝ ƒàçţöŕý ŕéçéîṽéš ţĥé šţéþ'š ḿàþ àñđ ţĥé ŕüñ'š ţàŕĝéţ ļàñĝüàĝé, đéçöđéš
ţĥé ḿàþ îñţö ţĥé çöñƒîĝ šţŕüçţ, àñđ ƃüîļđš ţĥé ţööļ ƒŕöḿ ţĥàţ. Ŵîţĥöüţ îţ, à
šţéþ'š config: îš đîšçàŕđéđ (ţĥé ŕéĝîšţŕý öñļý ķñöŵš ĥöŵ ţö çàļļ ţĥé þļàîñ
ƒàçţöŕý), ŵĥîçĥ îš ţĥé ŕéĝîšţŕàţîöñ îñṽàŕîàñţ ţĥé ƒŕàḿéŵöŕķ'š öŵñ ţööļš àŕé
ţéšţéđ àĝàîñšţ:
import (
"bytes"
"encoding/json"
"github.com/neokapi/neokapi/core/registry"
"github.com/neokapi/neokapi/core/schema"
"github.com/neokapi/neokapi/core/tool"
)
func RegisterAll(reg *registry.ToolRegistry) {
reg.RegisterWithSchema("my-tool", func() tool.Tool {
return NewMyTool(&MyToolConfig{})
}, toolSchema(&MyToolConfig{}, "my-tool", "My Tool", "transform"))
// Called for a YAML step (or `kapi exec my-tool --flag ...`): the map holds
// the step's config keys, targetLang the run's --target-lang.
reg.SetConfigFactory("my-tool", func(config map[string]any, targetLang string) (tool.Tool, error) {
cfg := &MyToolConfig{TargetLocale: targetLang}
if err := decodeConfig(config, cfg); err != nil {
return nil, err
}
return NewMyTool(cfg), nil
})
}
// decodeConfig maps config keys onto the struct by its json tags and rejects
// keys the struct does not declare.
func decodeConfig(config map[string]any, into any) error {
raw, err := json.Marshal(config)
if err != nil {
return err
}
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
return dec.Decode(into)
}
// Helper to reduce boilerplate
func toolSchema(cfg any, id, displayName, category string) *schema.ComponentSchema {
return schema.FromStruct(cfg, schema.ToolMeta{
ID: id,
Category: category,
DisplayName: displayName,
})
}
Öñçé ŕéĝîšţéŕéđ ŵîţĥ à šçĥéḿà:
kapi toolsšĥöŵš ţĥé ţööļ ŵîţĥ îţš đéšçŕîþţîöñ àñđ çàţéĝöŕý- Ţĥé ŵéƃ ÜÎ ŕéñđéŕš à đýñàḿîç çöñƒîĝüŕàţîöñ ƒöŕḿ (ṽîà
FilterConfigEditor/SchemaConfigEditor) - Ţĥé ÇĻÎ çàñ ṽàļîđàţé ţööļ çöñƒîĝ ƃéƒöŕé éẋéçüţîöñ
reg.Schema("my-tool")ŕéţüŕñš ţĥé šçĥéḿà ƒöŕ þŕöĝŕàḿḿàţîç àççéšš
Ƒüļļ éẋàḿþļé: çŕéàţîñĝ à çüšţöḿ ţööļ
Ĥéŕé îš à çöḿþļéţé éẋàḿþļé öƒ à þŕéƒîẋ/šüƒƒîẋ ŵŕàþþîñĝ ţööļ ŵîţĥ à þàŕàḿéţéŕ šçĥéḿà:
package wraptext
import (
"bytes"
"encoding/json"
"fmt"
"github.com/neokapi/neokapi/core/model"
"github.com/neokapi/neokapi/core/registry"
"github.com/neokapi/neokapi/core/schema"
"github.com/neokapi/neokapi/core/tool"
)
// Config
type WrapTextConfig struct {
Prefix string `json:"prefix" schema:"description=Text prepended to each block,default=["`
Suffix string `json:"suffix" schema:"description=Text appended to each block,default=]"`
TargetLocale string `json:"targetLocale" schema:"description=Target locale,placeholder=en"`
SourceOnly bool `json:"sourceOnly" schema:"description=Wrap source text only,default=false"`
}
func (c *WrapTextConfig) ToolName() string { return "wrap-text" }
func (c *WrapTextConfig) Reset() { c.Prefix = "["; c.Suffix = "]" }
// Tool
func NewWrapTextTool(cfg *WrapTextConfig) *tool.BaseTool {
t := &tool.BaseTool{
ToolName: "wrap-text",
ToolDescription: "Wraps block text with prefix and suffix",
Cfg: cfg,
}
// It can rewrite the source (SourceOnly), so it sets Transform. A
// transformer is a read-only edit producer: it returns an EditPlan and the
// framework applier performs the rewrite, applying the edits and rebasing
// surviving run-anchored overlays. The structured Edits (here two pure
// insertions) are what lets the applier rebase rather than drop overlays.
t.Transform = func(v tool.BlockView) (tool.EditPlan, error) {
conf := t.Cfg.(*WrapTextConfig)
text := v.SourceText()
wrapped := fmt.Sprintf("%s%s%s", conf.Prefix, text, conf.Suffix)
var plan tool.EditPlan
if conf.SourceOnly {
n := len([]rune(text))
plan.NewRuns = []model.Run{{Text: &model.TextRun{Text: wrapped}}}
plan.Edits = []model.RunEdit{
{Start: 0, End: 0, NewLen: len([]rune(conf.Prefix))}, // insert prefix
{Start: n, End: n, NewLen: len([]rune(conf.Suffix))}, // append suffix
}
} else {
plan.SetTarget(model.LocaleID(conf.TargetLocale),
[]model.Run{{Text: &model.TextRun{Text: wrapped}}})
}
return plan, nil
}
return t
}
// Registration
func Register(reg *registry.ToolRegistry) {
s := schema.FromStruct(&WrapTextConfig{}, schema.ToolMeta{
ID: "wrap-text",
Category: "transform",
DisplayName: "Wrap Text",
})
reg.RegisterWithSchema("wrap-text", func() tool.Tool {
return NewWrapTextTool(&WrapTextConfig{Prefix: "[", Suffix: "]"})
}, s)
// Without this, the `config:` block of a YAML step never reaches the tool.
reg.SetConfigFactory("wrap-text", func(config map[string]any, targetLang string) (tool.Tool, error) {
cfg := &WrapTextConfig{TargetLocale: targetLang}
cfg.Reset()
raw, err := json.Marshal(config)
if err != nil {
return nil, err
}
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(cfg); err != nil {
return nil, fmt.Errorf("wrap-text: %w", err)
}
return NewWrapTextTool(cfg), nil
})
}
Üšé ţĥé ţööļ ƒŕöḿ ţĥé ÇĻÎ:
kapi exec wrap-text input.json --target-lang fr --prefix ">> " --suffix " <<"
Öŕ îñ à ÝÀḾĻ ƒļöŵ:
steps:
- tool: wrap-text
config:
prefix: ">> "
suffix: " <<"
Ţĥé ŕüñ'š --target-lang ŕéàçĥéš ţĥé ţööļ ţĥŕöüĝĥ ţĥé çöñƒîĝ ƒàçţöŕý'š
targetLang àŕĝüḿéñţ, šö ţĥé šţéþ đéçļàŕéš ñö ļöçàļé öƒ îţš öŵñ.