feat: publish sanitized custom.97 source

This commit is contained in:
cneicy committed 2026-10-27 20:43:39 +08:00
1 parent 8e6ed0317b
commit 86c0276156
1022 files changed
+69903 -19207

No files matched your search

+91 -37
View File
@@ -1,12 +1,14 @@
package jsplugin
import (
"encoding/json"
"fmt"
"maps"
"net/http"
"regexp"
"slices"
"sort"
"strconv"
"strings"
"time"
@@ -29,6 +31,11 @@ type Route struct {
Decode string `json:"decode,omitempty"`
Render string `json:"render,omitempty"`
TaskIDParam string `json:"taskIdParam,omitempty"`
// RetainResult, when explicitly false on a submit or dynamic route, tells
// the host not to persist the upstream snapshot of an immediate terminal
// result and to treat the task as not found on every retrieval surface
// afterwards. nil means the route did not declare it (retain).
RetainResult *bool `json:"retainResult,omitempty"`
// Models restricts this route to the listed models. The host matches the
// canonical top-level "model" body field before any JS hook runs; empty
// means unrestricted. Must be a subset of meta.models.
@@ -88,8 +95,19 @@ var hostProtocols = []HostProtocolDefinition{
{Name: "retrieve", Methods: []string{http.MethodGet}, Path: "/v1/videos/:task_id", BodyKinds: []BodyKind{BodyNone}, RequiredProtocolMembers: []string{"render"}},
{Name: "content", Methods: []string{http.MethodGet, http.MethodHead}, Path: "/v1/videos/:task_id/content", BodyKinds: []BodyKind{BodyNone}, RequiredDriverHooks: []string{"listArtifacts", "buildContentRequest"}},
}},
// The OpenAI Images API is synchronous: both operations create a task and
// the host answers with the rendered image response once the task is
// terminal, so there is no retrieve operation and no request modes.
{Name: ProtocolOpenAIImage, Operations: []HostProtocolOperation{
{Name: "generate", Methods: []string{http.MethodPost}, Path: "/v1/images/generations", BodyKinds: []BodyKind{BodyJSON}, ModelField: "model", RequiredProtocolMembers: []string{"decodeRequest", "render"}},
{Name: "edit", Methods: []string{http.MethodPost}, Path: "/v1/images/edits", BodyKinds: []BodyKind{BodyJSON, BodyMultipart}, ModelField: "model", RequiredProtocolMembers: []string{"decodeRequest", "render"}},
}},
}
// ProtocolOpenAIImage is the host protocol that serves the OpenAI Images API
// (`POST /v1/images/generations` and `POST /v1/images/edits`) from a plugin.
const ProtocolOpenAIImage = "openai_image"
func HostProtocol(name string) (HostProtocolDefinition, bool) {
for _, definition := range hostProtocols {
if definition.Name == name {
@@ -201,6 +219,10 @@ const (
ContextKeyPinnedEndpoint = "task_plugin_pinned_endpoint"
ContextKeyRouteRequest = "task_plugin_route_request"
ContextKeyProtocolRequest = "task_plugin_protocol_request"
// ContextKeyRequestBodyText holds, for a plugin that preserves JSON
// order, the decoded requestBody as JSON text (json.RawMessage) beside
// the Go value in task_request.
ContextKeyRequestBodyText = "task_plugin_request_body_text"
)
type PinnedPlugin struct {
@@ -228,6 +250,38 @@ type PinnedEndpoint struct {
Candidates []ProtocolBinding
}
// FileReference returns the opaque ref of the index-th uploaded file in a
// multipart field. The first file keeps the historical `request_file:<field>`
// spelling; later files in the same repeated field (`image[]`, `image[]`)
// append `#<index>` so a plugin can address each of them.
func FileReference(field string, index int) string {
if index <= 0 {
return "request_file:" + field
}
return "request_file:" + field + "#" + strconv.Itoa(index)
}
// ParseFileReference resolves a ref produced by FileReference back to the
// multipart field and the zero-based file index within that field.
func ParseFileReference(ref string) (field string, index int, ok bool) {
rest, found := strings.CutPrefix(ref, "request_file:")
if !found || rest == "" {
return "", 0, false
}
field, suffix, hasIndex := strings.Cut(rest, "#")
if field == "" {
return "", 0, false
}
if !hasIndex {
return field, 0, true
}
parsed, err := strconv.Atoi(suffix)
if err != nil || parsed < 0 || strconv.Itoa(parsed) != suffix {
return "", 0, false
}
return field, parsed, true
}
// RouteRequestContext is the canonical request view exposed to declarative
// routing hooks. RequestBody contains decoded JSON or multipart text fields;
// raw binary and multipart file bytes remain host-owned.
@@ -239,55 +293,44 @@ type RouteRequestContext struct {
Body any `json:"body"`
Files []map[string]any `json:"-"`
RequestBody any `json:"-"`
// BodyText is the JSON body as the client sent it, which JSValueFor
// gives to plugins that preserve JSON order; Body stays the Go value the
// host reads. Body storage never changes these bytes, which the engine
// parses in place.
BodyText json.RawMessage `json:"-"`
}
// JSValue shares the request with hooks without copying it: the engine never
// writes JavaScript changes back into Go values, so the request must only stay
// unchanged while a hook runs. Missing params and query still reach hooks as
// empty objects rather than null.
func (r RouteRequestContext) JSValue() map[string]any {
params := make(map[string]string, len(r.Params))
maps.Copy(params, r.Params)
query := make(map[string][]string, len(r.Query))
for key, values := range r.Query {
query[key] = append([]string(nil), values...)
params, query := r.Params, r.Query
if params == nil {
params = map[string]string{}
}
if query == nil {
query = map[string][]string{}
}
return map[string]any{
"path": r.Path,
"method": r.Method,
"params": params,
"query": query,
"body": clonePluginRequestValue(r.Body),
"body": r.Body,
}
}
func clonePluginRequestValue(value any) any {
switch typed := value.(type) {
case map[string]any:
cloned := make(map[string]any, len(typed))
for key, item := range typed {
cloned[key] = clonePluginRequestValue(item)
}
return cloned
case []any:
cloned := make([]any, len(typed))
for index, item := range typed {
cloned[index] = clonePluginRequestValue(item)
}
return cloned
case []string:
return append([]string(nil), typed...)
case map[string][]string:
cloned := make(map[string][]string, len(typed))
for key, values := range typed {
cloned[key] = append([]string(nil), values...)
}
return cloned
case []map[string]any:
cloned := make([]map[string]any, len(typed))
for index, item := range typed {
cloned[index] = clonePluginRequestValue(item).(map[string]any)
}
return cloned
default:
return value
// JSValueFor is JSValue for the decode hooks of the plugin meta describes. A
// plugin that preserves JSON order receives a JSON body as its text, which the
// engine parses in place, members in the client's order: a decoder reads the
// body, so it is parsed for every call.
func (r RouteRequestContext) JSValueFor(meta Meta) map[string]any {
value := r.JSValue()
if len(r.BodyText) > 0 && meta.PreservesJSONOrder() {
value["body"] = map[string]any{"kind": string(BodyJSON), "value": RawJSON(r.BodyText)}
}
return value
}
type ProtocolRequestContext struct {
@@ -303,7 +346,15 @@ type ProtocolRequestContext struct {
}
func (p ProtocolRequestContext) JSValue() map[string]any {
value := p.RouteRequestContext.JSValue()
return p.withProtocol(p.RouteRequestContext.JSValue())
}
// JSValueFor is RouteRequestContext.JSValueFor with the protocol fields.
func (p ProtocolRequestContext) JSValueFor(meta Meta) map[string]any {
return p.withProtocol(p.RouteRequestContext.JSValueFor(meta))
}
func (p ProtocolRequestContext) withProtocol(value map[string]any) map[string]any {
value["protocol"] = p.Protocol
value["operation"] = p.Operation
value["model"] = p.Model
@@ -712,6 +763,9 @@ func validateRoute(route *Route) error {
if route.Action != "" {
return fmt.Errorf("query route %s %s must not declare action", route.Method, route.Path)
}
if route.RetainResult != nil {
return fmt.Errorf("query route %s %s must not declare retainResult", route.Method, route.Path)
}
if route.TaskIDParam == "" {
route.TaskIDParam = "task_id"
}