Files
yetone 2f135ed4d5 plugin, gui, docs: Plugins says which plugins are providers and which are middleware, lists the community's ready-made middleware, and edits a middleware's options in its row (the owner) — the Plugins page still read as if every plugin were a subscription, and nothing showed what a middleware does or let you set its options without editing plugins.json. Every Discover card and Installed row now carries a Provider or Middleware chip (both for a plugin that is both), also for one that is off (isMiddleware/middlewareOnly from the server, read from its files); Discover splits into Subscriptions and Gateway middleware ("runs in magpie's gateway on what every agent sends and gets back, whichever provider serves it"), the latter from market entries with "kind": "middleware" and no providers or icon (the built-in market.json lists the five new @magpie-community/middleware-* packages: param-override, model-map, system-prompt, word-guard, think-tags, mostly New API's channel settings with the same JSON). A middleware row has Options: a JSON editor in the row, starting from the package's magpie.options example when none are set, which validates it is an object, saves (POST /api/plugins/options) for the next request, clears, and cancels with Esc. magpie plugin options takes a short name (model-map for @magpie-community/middleware-model-map; plugin.ShortName, also in SetOptions). en/zh/ja/de. Docs: usemagpie.ai/docs/plugins (en/zh/ja) gains Ready-made middleware and Options and the registry's kind; reference.md, both READMEs and docs/subsystems/gateway-middleware.md likewise. Tested with a real DeepSeek account and real Claude Code through magpie web --gateway in a sandbox HOME, with all five installed and options set by short name: a mapped model named back, max_tokens capped, a prompt appended for claude only, word-guard's 400 and param-override's 403 in the agent's shape, <think> stripped from Chat and Responses streams and a whole Anthropic reply, reasoning_content moved into the text; /api/plugins showed the kinds and the example. TestSetOptionsShortName is new; TestBuiltinMarket checks a middleware entry has no providers or icon and no other kind. plugin-middleware.test.cjs (Chromium and WebKit, en and zh: the chips, Discover's two sections, the options editor with its errors, no select, no scroll, no left border) fails without the change; the other plugin tests and click-scroll pass in WebKit (plugin-market's section name updated; plugin-move fails only under parallel load and passes alone). go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.
2026-10-05 20:50:09 +08:00

65 lines
1.8 KiB
Go

package plugin
import (
"encoding/json"
"os"
"path/filepath"
"strings"
)
// Middleware is the gateway middleware a plugin at target has: the file
// its package.json's magpie.middleware names, or the file itself for a
// path to a *.middleware.js (or .mjs). only is whether it is nothing
// else: no OpenCode plugin for the host to load (no main, no exports).
func Middleware(target string) (file string, only bool) {
st, err := os.Stat(target)
if err != nil {
return "", false
}
if !st.IsDir() {
base := strings.ToLower(filepath.Base(target))
if strings.HasSuffix(base, ".middleware.js") || strings.HasSuffix(base, ".middleware.mjs") {
return target, true
}
return "", false
}
b, err := os.ReadFile(filepath.Join(target, "package.json"))
if err != nil {
return "", false
}
var pkg struct {
Main string `json:"main"`
Exports json.RawMessage `json:"exports"`
Magpie struct {
Middleware string `json:"middleware"`
} `json:"magpie"`
}
if json.Unmarshal(b, &pkg) != nil || strings.TrimSpace(pkg.Magpie.Middleware) == "" {
return "", false
}
file = filepath.Join(target, filepath.FromSlash(strings.TrimSpace(pkg.Magpie.Middleware)))
return file, pkg.Main == "" && len(pkg.Exports) == 0
}
// ListStamp changes when plugins.json does.
func ListStamp() string { return listStamp() }
// OptionsExample is the options a plugin at target suggests, from its
// package.json's magpie.options: what its options editor starts from
// before any are set.
func OptionsExample(target string) map[string]any {
b, err := os.ReadFile(filepath.Join(target, "package.json"))
if err != nil {
return nil
}
var pkg struct {
Magpie struct {
Options map[string]any `json:"options"`
} `json:"magpie"`
}
if json.Unmarshal(b, &pkg) != nil {
return nil
}
return pkg.Magpie.Options
}