mirror of
https://github.com/zgs225/cliproxy-plugin-commandcode.git
synced 2026-09-26 11:42:48 +08:00
feat: initial implementation of CLIProxyAPI Command Code plugin (stage 1)
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
PluginID = "commandcode"
|
||||
PluginName = "commandcode"
|
||||
PluginVersion = "0.1.0"
|
||||
PluginAuthor = "zgs225"
|
||||
PluginRepo = "https://github.com/zgs225/cliproxy-plugin-commandcode"
|
||||
PluginLogo = "https://raw.githubusercontent.com/zgs225/cliproxy-plugin-commandcode/main/assets/logo.svg"
|
||||
SchemaVersion1 = 1
|
||||
)
|
||||
|
||||
// PluginConfig holds the runtime configuration parsed from YAML.
|
||||
type PluginConfig struct {
|
||||
mu sync.RWMutex
|
||||
SessionToken string `yaml:"session_token" json:"session_token"`
|
||||
APIBase string `yaml:"api_base" json:"api_base"`
|
||||
}
|
||||
|
||||
// UpdateFromYAML updates the configuration from raw YAML bytes.
|
||||
func (c *PluginConfig) UpdateFromYAML(raw []byte) error {
|
||||
if len(raw) == 0 {
|
||||
return nil
|
||||
}
|
||||
var tmp struct {
|
||||
SessionToken string `yaml:"session_token"`
|
||||
APIBase string `yaml:"api_base"`
|
||||
}
|
||||
if err := yaml.Unmarshal(raw, &tmp); err != nil {
|
||||
return fmt.Errorf("unmarshal config_yaml: %w", err)
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if tmp.SessionToken != "" {
|
||||
c.SessionToken = ExtractSessionToken(tmp.SessionToken)
|
||||
}
|
||||
if tmp.APIBase != "" {
|
||||
c.APIBase = strings.TrimRight(tmp.APIBase, "/")
|
||||
}
|
||||
if c.APIBase == "" {
|
||||
c.APIBase = DefaultAPIBase
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSessionToken safely returns the session token.
|
||||
func (c *PluginConfig) GetSessionToken() string {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.SessionToken
|
||||
}
|
||||
|
||||
// SetSessionToken safely sets the session token.
|
||||
func (c *PluginConfig) SetSessionToken(token string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.SessionToken = ExtractSessionToken(token)
|
||||
}
|
||||
|
||||
// GetAPIBase safely returns the API base URL.
|
||||
func (c *PluginConfig) GetAPIBase() string {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
if c.APIBase == "" {
|
||||
return DefaultAPIBase
|
||||
}
|
||||
return c.APIBase
|
||||
}
|
||||
|
||||
// Plugin encapsulates the Command Code plugin instance.
|
||||
type Plugin struct {
|
||||
config *PluginConfig
|
||||
}
|
||||
|
||||
var (
|
||||
defaultPlugin = NewPlugin()
|
||||
)
|
||||
|
||||
// DefaultPlugin returns the singleton plugin instance.
|
||||
func DefaultPlugin() *Plugin {
|
||||
return defaultPlugin
|
||||
}
|
||||
|
||||
// NewPlugin creates a new Plugin instance.
|
||||
func NewPlugin() *Plugin {
|
||||
return &Plugin{
|
||||
config: &PluginConfig{
|
||||
APIBase: DefaultAPIBase,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// HandleMethod dispatches an ABI call to the corresponding handler.
|
||||
func (p *Plugin) HandleMethod(method string, requestBytes []byte) ([]byte, error) {
|
||||
switch method {
|
||||
case "plugin.register":
|
||||
return p.handleRegister(requestBytes)
|
||||
case "plugin.reconfigure":
|
||||
return p.handleReconfigure(requestBytes)
|
||||
case "plugin.quiesce", "plugin.shutdown":
|
||||
return OkEnvelope(map[string]any{"shutdown": true})
|
||||
|
||||
case "auth.identifier":
|
||||
return OkEnvelope(IdentifierResponse{Identifier: PluginID})
|
||||
case "auth.parse":
|
||||
return p.handleAuthParse(requestBytes)
|
||||
case "auth.login.start":
|
||||
return p.handleAuthLoginStart()
|
||||
case "auth.login.poll":
|
||||
return p.handleAuthLoginPoll()
|
||||
case "auth.refresh":
|
||||
return p.handleAuthRefresh(requestBytes)
|
||||
|
||||
case "management.register":
|
||||
return p.handleManagementRegister()
|
||||
case "management.handle":
|
||||
return p.handleManagementHandle(requestBytes)
|
||||
|
||||
default:
|
||||
return ErrorEnvelope("unknown_method", "unknown method: "+method), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Plugin) handleRegister(raw []byte) ([]byte, error) {
|
||||
if len(raw) > 0 {
|
||||
var req LifecycleRequest
|
||||
if err := json.Unmarshal(raw, &req); err == nil && len(req.ConfigYAML) > 0 {
|
||||
_ = p.config.UpdateFromYAML(req.ConfigYAML)
|
||||
}
|
||||
}
|
||||
return OkEnvelope(Registration{
|
||||
SchemaVersion: SchemaVersion1,
|
||||
Metadata: Metadata{
|
||||
Name: PluginName,
|
||||
Version: PluginVersion,
|
||||
Author: PluginAuthor,
|
||||
GitHubRepository: PluginRepo,
|
||||
Logo: PluginLogo,
|
||||
ConfigFields: []ConfigField{
|
||||
{
|
||||
Name: "session_token",
|
||||
Type: "string",
|
||||
Description: "Command Code session token (__Secure-commandcode_prod_.session_token cookie value)",
|
||||
},
|
||||
{
|
||||
Name: "api_base",
|
||||
Type: "string",
|
||||
Description: "Command Code API base URL (default: https://api.commandcode.ai)",
|
||||
},
|
||||
},
|
||||
},
|
||||
Capabilities: RegistrationCapability{
|
||||
AuthProvider: true,
|
||||
ManagementAPI: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Plugin) handleReconfigure(raw []byte) ([]byte, error) {
|
||||
if len(raw) > 0 {
|
||||
var req LifecycleRequest
|
||||
if err := json.Unmarshal(raw, &req); err == nil && len(req.ConfigYAML) > 0 {
|
||||
_ = p.config.UpdateFromYAML(req.ConfigYAML)
|
||||
}
|
||||
}
|
||||
return p.handleRegister(raw)
|
||||
}
|
||||
|
||||
func (p *Plugin) handleAuthParse(raw []byte) ([]byte, error) {
|
||||
var req AuthParseRequest
|
||||
if len(raw) > 0 {
|
||||
if err := json.Unmarshal(raw, &req); err != nil {
|
||||
return ErrorEnvelope("invalid_request", "failed to parse AuthParseRequest: "+err.Error()), nil
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := ParseAuth(req)
|
||||
if err != nil {
|
||||
return ErrorEnvelope("auth_parse_error", err.Error()), nil
|
||||
}
|
||||
|
||||
// Cache token in config if config doesn't have one yet
|
||||
if resp.Handled && resp.Auth.Metadata != nil {
|
||||
if tok, ok := resp.Auth.Metadata["session_token"].(string); ok && tok != "" {
|
||||
if p.config.GetSessionToken() == "" {
|
||||
p.config.SetSessionToken(tok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return OkEnvelope(resp)
|
||||
}
|
||||
|
||||
func (p *Plugin) handleAuthLoginStart() ([]byte, error) {
|
||||
return OkEnvelope(map[string]any{
|
||||
"Provider": PluginID,
|
||||
"URL": "https://commandcode.ai",
|
||||
"State": "manual",
|
||||
"ExpiresAt": time.Now().Add(5 * time.Minute).UTC(),
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Plugin) handleAuthLoginPoll() ([]byte, error) {
|
||||
return OkEnvelope(map[string]any{
|
||||
"Status": "error",
|
||||
"Message": "Command Code interactive login is not supported; please configure session_token or provide a commandcode-*.json credential file",
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Plugin) handleAuthRefresh(raw []byte) ([]byte, error) {
|
||||
var req AuthRefreshRequest
|
||||
if len(raw) > 0 {
|
||||
_ = json.Unmarshal(raw, &req)
|
||||
}
|
||||
|
||||
authData := AuthData{
|
||||
Provider: PluginID,
|
||||
ID: req.AuthID,
|
||||
StorageJSON: req.StorageJSON,
|
||||
Metadata: req.Metadata,
|
||||
Attributes: req.Attributes,
|
||||
NextRefreshAfter: time.Now().Add(24 * time.Hour).UTC(),
|
||||
}
|
||||
return OkEnvelope(AuthRefreshResponse{
|
||||
Auth: authData,
|
||||
NextRefreshAfter: authData.NextRefreshAfter,
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Plugin) handleManagementRegister() ([]byte, error) {
|
||||
resp, err := RegisterManagement()
|
||||
if err != nil {
|
||||
return ErrorEnvelope("management_register_error", err.Error()), nil
|
||||
}
|
||||
return OkEnvelope(resp)
|
||||
}
|
||||
|
||||
func (p *Plugin) handleManagementHandle(raw []byte) ([]byte, error) {
|
||||
var req ManagementRequest
|
||||
if len(raw) > 0 {
|
||||
if err := json.Unmarshal(raw, &req); err != nil {
|
||||
return ErrorEnvelope("invalid_request", "failed to parse ManagementRequest: "+err.Error()), nil
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := HandleManagement(context.Background(), req, p.config)
|
||||
if err != nil {
|
||||
return ErrorEnvelope("management_handle_error", err.Error()), nil
|
||||
}
|
||||
return OkEnvelope(resp)
|
||||
}
|
||||
|
||||
// OkEnvelope builds a successful Envelope response.
|
||||
func OkEnvelope(v any) ([]byte, error) {
|
||||
raw, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(Envelope{OK: true, Result: raw})
|
||||
}
|
||||
|
||||
// ErrorEnvelope builds an error Envelope response.
|
||||
func ErrorEnvelope(code, message string) []byte {
|
||||
raw, _ := json.Marshal(Envelope{
|
||||
OK: false,
|
||||
Error: &EnvelopeError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
},
|
||||
})
|
||||
return raw
|
||||
}
|
||||
Reference in New Issue
Block a user