feature/gemini-web-provider #1
|
|
@ -0,0 +1,28 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cursor-api-proxy/internal/config"
|
||||||
|
"cursor-api-proxy/internal/env"
|
||||||
|
"cursor-api-proxy/internal/providers/geminiweb"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
accountName := ""
|
||||||
|
if len(os.Args) > 1 {
|
||||||
|
accountName = os.Args[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
e := env.OsEnvToMap()
|
||||||
|
loaded := env.LoadEnvConfig(e, "")
|
||||||
|
cfg := config.LoadBridgeConfig(e, "")
|
||||||
|
|
||||||
|
cfg.GeminiAccountDir = loaded.GeminiAccountDir
|
||||||
|
cfg.GeminiBrowserVisible = true
|
||||||
|
|
||||||
|
if err := geminiweb.RunLogin(cfg, accountName); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
6
go.mod
6
go.mod
|
|
@ -9,9 +9,15 @@ require (
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/go-rod/rod v0.116.2 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
github.com/ysmood/fetchup v0.2.3 // indirect
|
||||||
|
github.com/ysmood/goob v0.4.0 // indirect
|
||||||
|
github.com/ysmood/got v0.40.0 // indirect
|
||||||
|
github.com/ysmood/gson v0.7.3 // indirect
|
||||||
|
github.com/ysmood/leakless v0.9.0 // indirect
|
||||||
golang.org/x/sys v0.42.0 // indirect
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
modernc.org/libc v1.70.0 // indirect
|
modernc.org/libc v1.70.0 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
|
|
|
||||||
13
go.sum
13
go.sum
|
|
@ -1,5 +1,7 @@
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA=
|
||||||
|
github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
|
@ -12,6 +14,17 @@ github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOF
|
||||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ=
|
||||||
|
github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns=
|
||||||
|
github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ=
|
||||||
|
github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18=
|
||||||
|
github.com/ysmood/got v0.40.0 h1:ZQk1B55zIvS7zflRrkGfPDrPG3d7+JOza1ZkNxcc74Q=
|
||||||
|
github.com/ysmood/got v0.40.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg=
|
||||||
|
github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM=
|
||||||
|
github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE=
|
||||||
|
github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg=
|
||||||
|
github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU=
|
||||||
|
github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ=
|
||||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
package apitypes
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
Role string
|
||||||
|
Content string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tool struct {
|
||||||
|
Type string
|
||||||
|
Function ToolFunction
|
||||||
|
}
|
||||||
|
|
||||||
|
type ToolFunction struct {
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
Parameters interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ToolCall struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
Arguments string
|
||||||
|
}
|
||||||
|
|
||||||
|
type StreamChunk struct {
|
||||||
|
Type ChunkType
|
||||||
|
Text string
|
||||||
|
Thinking string
|
||||||
|
ToolCall *ToolCall
|
||||||
|
Done bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChunkType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ChunkText ChunkType = iota
|
||||||
|
ChunkThinking
|
||||||
|
ChunkToolCall
|
||||||
|
ChunkDone
|
||||||
|
)
|
||||||
|
|
@ -5,50 +5,58 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type BridgeConfig struct {
|
type BridgeConfig struct {
|
||||||
AgentBin string
|
AgentBin string
|
||||||
Host string
|
Host string
|
||||||
Port int
|
Port int
|
||||||
RequiredKey string
|
RequiredKey string
|
||||||
DefaultModel string
|
DefaultModel string
|
||||||
Mode string
|
Mode string
|
||||||
Force bool
|
Provider string
|
||||||
ApproveMcps bool
|
Force bool
|
||||||
StrictModel bool
|
ApproveMcps bool
|
||||||
Workspace string
|
StrictModel bool
|
||||||
TimeoutMs int
|
Workspace string
|
||||||
TLSCertPath string
|
TimeoutMs int
|
||||||
TLSKeyPath string
|
TLSCertPath string
|
||||||
SessionsLogPath string
|
TLSKeyPath string
|
||||||
ChatOnlyWorkspace bool
|
SessionsLogPath string
|
||||||
Verbose bool
|
ChatOnlyWorkspace bool
|
||||||
MaxMode bool
|
Verbose bool
|
||||||
ConfigDirs []string
|
MaxMode bool
|
||||||
MultiPort bool
|
ConfigDirs []string
|
||||||
WinCmdlineMax int
|
MultiPort bool
|
||||||
|
WinCmdlineMax int
|
||||||
|
GeminiAccountDir string
|
||||||
|
GeminiBrowserVisible bool
|
||||||
|
GeminiMaxSessions int
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadBridgeConfig(e env.EnvSource, cwd string) BridgeConfig {
|
func LoadBridgeConfig(e env.EnvSource, cwd string) BridgeConfig {
|
||||||
loaded := env.LoadEnvConfig(e, cwd)
|
loaded := env.LoadEnvConfig(e, cwd)
|
||||||
return BridgeConfig{
|
return BridgeConfig{
|
||||||
AgentBin: loaded.AgentBin,
|
AgentBin: loaded.AgentBin,
|
||||||
Host: loaded.Host,
|
Host: loaded.Host,
|
||||||
Port: loaded.Port,
|
Port: loaded.Port,
|
||||||
RequiredKey: loaded.RequiredKey,
|
RequiredKey: loaded.RequiredKey,
|
||||||
DefaultModel: loaded.DefaultModel,
|
DefaultModel: loaded.DefaultModel,
|
||||||
Mode: "ask",
|
Mode: "ask",
|
||||||
Force: loaded.Force,
|
Provider: loaded.Provider,
|
||||||
ApproveMcps: loaded.ApproveMcps,
|
Force: loaded.Force,
|
||||||
StrictModel: loaded.StrictModel,
|
ApproveMcps: loaded.ApproveMcps,
|
||||||
Workspace: loaded.Workspace,
|
StrictModel: loaded.StrictModel,
|
||||||
TimeoutMs: loaded.TimeoutMs,
|
Workspace: loaded.Workspace,
|
||||||
TLSCertPath: loaded.TLSCertPath,
|
TimeoutMs: loaded.TimeoutMs,
|
||||||
TLSKeyPath: loaded.TLSKeyPath,
|
TLSCertPath: loaded.TLSCertPath,
|
||||||
SessionsLogPath: loaded.SessionsLogPath,
|
TLSKeyPath: loaded.TLSKeyPath,
|
||||||
ChatOnlyWorkspace: loaded.ChatOnlyWorkspace,
|
SessionsLogPath: loaded.SessionsLogPath,
|
||||||
Verbose: loaded.Verbose,
|
ChatOnlyWorkspace: loaded.ChatOnlyWorkspace,
|
||||||
MaxMode: loaded.MaxMode,
|
Verbose: loaded.Verbose,
|
||||||
ConfigDirs: loaded.ConfigDirs,
|
MaxMode: loaded.MaxMode,
|
||||||
MultiPort: loaded.MultiPort,
|
ConfigDirs: loaded.ConfigDirs,
|
||||||
WinCmdlineMax: loaded.WinCmdlineMax,
|
MultiPort: loaded.MultiPort,
|
||||||
|
WinCmdlineMax: loaded.WinCmdlineMax,
|
||||||
|
GeminiAccountDir: loaded.GeminiAccountDir,
|
||||||
|
GeminiBrowserVisible: loaded.GeminiBrowserVisible,
|
||||||
|
GeminiMaxSessions: loaded.GeminiMaxSessions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,28 +12,32 @@ import (
|
||||||
type EnvSource map[string]string
|
type EnvSource map[string]string
|
||||||
|
|
||||||
type LoadedEnv struct {
|
type LoadedEnv struct {
|
||||||
AgentBin string
|
AgentBin string
|
||||||
AgentNode string
|
AgentNode string
|
||||||
AgentScript string
|
AgentScript string
|
||||||
CommandShell string
|
CommandShell string
|
||||||
Host string
|
Host string
|
||||||
Port int
|
Port int
|
||||||
RequiredKey string
|
RequiredKey string
|
||||||
DefaultModel string
|
DefaultModel string
|
||||||
Force bool
|
Provider string
|
||||||
ApproveMcps bool
|
Force bool
|
||||||
StrictModel bool
|
ApproveMcps bool
|
||||||
Workspace string
|
StrictModel bool
|
||||||
TimeoutMs int
|
Workspace string
|
||||||
TLSCertPath string
|
TimeoutMs int
|
||||||
TLSKeyPath string
|
TLSCertPath string
|
||||||
SessionsLogPath string
|
TLSKeyPath string
|
||||||
ChatOnlyWorkspace bool
|
SessionsLogPath string
|
||||||
Verbose bool
|
ChatOnlyWorkspace bool
|
||||||
MaxMode bool
|
Verbose bool
|
||||||
ConfigDirs []string
|
MaxMode bool
|
||||||
MultiPort bool
|
ConfigDirs []string
|
||||||
WinCmdlineMax int
|
MultiPort bool
|
||||||
|
WinCmdlineMax int
|
||||||
|
GeminiAccountDir string
|
||||||
|
GeminiBrowserVisible bool
|
||||||
|
GeminiMaxSessions int
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentCommand struct {
|
type AgentCommand struct {
|
||||||
|
|
@ -256,29 +260,40 @@ func LoadEnvConfig(e EnvSource, cwd string) LoadedEnv {
|
||||||
workspace = cwd
|
workspace = cwd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
geminiAccountDir := getEnvVal(e, []string{"GEMINI_ACCOUNT_DIR"})
|
||||||
|
if geminiAccountDir == "" {
|
||||||
|
geminiAccountDir = filepath.Join(home, ".cursor-api-proxy", "gemini-accounts")
|
||||||
|
} else {
|
||||||
|
geminiAccountDir = resolveAbs(geminiAccountDir, cwd)
|
||||||
|
}
|
||||||
|
|
||||||
return LoadedEnv{
|
return LoadedEnv{
|
||||||
AgentBin: agentBin,
|
AgentBin: agentBin,
|
||||||
AgentNode: getEnvVal(e, []string{"CURSOR_AGENT_NODE"}),
|
AgentNode: getEnvVal(e, []string{"CURSOR_AGENT_NODE"}),
|
||||||
AgentScript: getEnvVal(e, []string{"CURSOR_AGENT_SCRIPT"}),
|
AgentScript: getEnvVal(e, []string{"CURSOR_AGENT_SCRIPT"}),
|
||||||
CommandShell: commandShell,
|
CommandShell: commandShell,
|
||||||
Host: host,
|
Host: host,
|
||||||
Port: port,
|
Port: port,
|
||||||
RequiredKey: getEnvVal(e, []string{"CURSOR_BRIDGE_API_KEY"}),
|
RequiredKey: getEnvVal(e, []string{"CURSOR_BRIDGE_API_KEY"}),
|
||||||
DefaultModel: normalizeModelId(getEnvVal(e, []string{"CURSOR_BRIDGE_DEFAULT_MODEL"})),
|
DefaultModel: normalizeModelId(getEnvVal(e, []string{"CURSOR_BRIDGE_DEFAULT_MODEL"})),
|
||||||
Force: envBool(e, []string{"CURSOR_BRIDGE_FORCE"}, false),
|
Provider: getEnvVal(e, []string{"CURSOR_BRIDGE_PROVIDER"}),
|
||||||
ApproveMcps: envBool(e, []string{"CURSOR_BRIDGE_APPROVE_MCPS"}, false),
|
Force: envBool(e, []string{"CURSOR_BRIDGE_FORCE"}, false),
|
||||||
StrictModel: envBool(e, []string{"CURSOR_BRIDGE_STRICT_MODEL"}, true),
|
ApproveMcps: envBool(e, []string{"CURSOR_BRIDGE_APPROVE_MCPS"}, false),
|
||||||
Workspace: workspace,
|
StrictModel: envBool(e, []string{"CURSOR_BRIDGE_STRICT_MODEL"}, true),
|
||||||
TimeoutMs: envInt(e, []string{"CURSOR_BRIDGE_TIMEOUT_MS"}, 300000),
|
Workspace: workspace,
|
||||||
TLSCertPath: resolveAbs(getEnvVal(e, []string{"CURSOR_BRIDGE_TLS_CERT"}), cwd),
|
TimeoutMs: envInt(e, []string{"CURSOR_BRIDGE_TIMEOUT_MS"}, 300000),
|
||||||
TLSKeyPath: resolveAbs(getEnvVal(e, []string{"CURSOR_BRIDGE_TLS_KEY"}), cwd),
|
TLSCertPath: resolveAbs(getEnvVal(e, []string{"CURSOR_BRIDGE_TLS_CERT"}), cwd),
|
||||||
SessionsLogPath: sessionsLogPath,
|
TLSKeyPath: resolveAbs(getEnvVal(e, []string{"CURSOR_BRIDGE_TLS_KEY"}), cwd),
|
||||||
ChatOnlyWorkspace: envBool(e, []string{"CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE"}, true),
|
SessionsLogPath: sessionsLogPath,
|
||||||
Verbose: envBool(e, []string{"CURSOR_BRIDGE_VERBOSE"}, false),
|
ChatOnlyWorkspace: envBool(e, []string{"CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE"}, true),
|
||||||
MaxMode: envBool(e, []string{"CURSOR_BRIDGE_MAX_MODE"}, false),
|
Verbose: envBool(e, []string{"CURSOR_BRIDGE_VERBOSE"}, false),
|
||||||
ConfigDirs: configDirs,
|
MaxMode: envBool(e, []string{"CURSOR_BRIDGE_MAX_MODE"}, false),
|
||||||
MultiPort: envBool(e, []string{"CURSOR_BRIDGE_MULTI_PORT"}, false),
|
ConfigDirs: configDirs,
|
||||||
WinCmdlineMax: winMax,
|
MultiPort: envBool(e, []string{"CURSOR_BRIDGE_MULTI_PORT"}, false),
|
||||||
|
WinCmdlineMax: winMax,
|
||||||
|
GeminiAccountDir: geminiAccountDir,
|
||||||
|
GeminiBrowserVisible: envBool(e, []string{"GEMINI_BROWSER_VISIBLE"}, false),
|
||||||
|
GeminiMaxSessions: envInt(e, []string{"GEMINI_MAX_SESSIONS"}, 3),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package cursor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"cursor-api-proxy/internal/apitypes"
|
||||||
|
"cursor-api-proxy/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Provider struct {
|
||||||
|
cfg config.BridgeConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProvider(cfg config.BridgeConfig) *Provider {
|
||||||
|
return &Provider{cfg: cfg}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) Name() string {
|
||||||
|
return "cursor"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) Generate(ctx context.Context, model string, messages []apitypes.Message, tools []apitypes.Tool, cb func(apitypes.StreamChunk)) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"cursor-api-proxy/internal/apitypes"
|
||||||
|
"cursor-api-proxy/internal/config"
|
||||||
|
"cursor-api-proxy/internal/providers/cursor"
|
||||||
|
"cursor-api-proxy/internal/providers/geminiweb"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Provider interface {
|
||||||
|
Name() string
|
||||||
|
Close() error
|
||||||
|
Generate(ctx context.Context, model string, messages []apitypes.Message, tools []apitypes.Tool, cb func(apitypes.StreamChunk)) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProvider(cfg config.BridgeConfig) (Provider, error) {
|
||||||
|
providerType := cfg.Provider
|
||||||
|
if providerType == "" {
|
||||||
|
providerType = "cursor"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch providerType {
|
||||||
|
case "cursor":
|
||||||
|
return cursor.NewProvider(cfg), nil
|
||||||
|
case "gemini-web":
|
||||||
|
return geminiweb.NewProvider(cfg), nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown provider: %s", providerType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
package geminiweb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-rod/rod"
|
||||||
|
"github.com/go-rod/rod/lib/launcher"
|
||||||
|
"github.com/go-rod/rod/lib/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Browser struct {
|
||||||
|
browser *rod.Browser
|
||||||
|
visible bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBrowser(visible bool) (*Browser, error) {
|
||||||
|
l := launcher.New()
|
||||||
|
if visible {
|
||||||
|
l = l.Headless(false)
|
||||||
|
} else {
|
||||||
|
l = l.Headless(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
url, err := l.Launch()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to launch browser: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b := rod.New().ControlURL(url)
|
||||||
|
if err := b.Connect(); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to connect browser: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Browser{browser: b, visible: visible}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Browser) Close() error {
|
||||||
|
if b.browser != nil {
|
||||||
|
return b.browser.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Browser) NewPage() (*rod.Page, error) {
|
||||||
|
return b.browser.Page(proto.TargetCreateTarget{URL: "about:blank"})
|
||||||
|
}
|
||||||
|
|
||||||
|
type Cookie struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Expires float64 `json:"expires"`
|
||||||
|
HTTPOnly bool `json:"httpOnly"`
|
||||||
|
Secure bool `json:"secure"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadCookiesFromFile(cookieFile string) ([]Cookie, error) {
|
||||||
|
data, err := os.ReadFile(cookieFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read cookies: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cookies []Cookie
|
||||||
|
if err := json.Unmarshal(data, &cookies); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse cookies: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cookies, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveCookiesToFile(cookies []Cookie, cookieFile string) error {
|
||||||
|
data, err := json.MarshalIndent(cookies, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal cookies: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := filepath.Dir(cookieFile)
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create cookie dir: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(cookieFile, data, 0644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write cookies: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetCookiesOnPage(page *rod.Page, cookies []Cookie) error {
|
||||||
|
var protoCookies []*proto.NetworkCookieParam
|
||||||
|
for _, c := range cookies {
|
||||||
|
p := &proto.NetworkCookieParam{
|
||||||
|
Name: c.Name,
|
||||||
|
Value: c.Value,
|
||||||
|
Domain: c.Domain,
|
||||||
|
Path: c.Path,
|
||||||
|
HTTPOnly: c.HTTPOnly,
|
||||||
|
Secure: c.Secure,
|
||||||
|
}
|
||||||
|
if c.Expires > 0 {
|
||||||
|
exp := proto.TimeSinceEpoch(c.Expires)
|
||||||
|
p.Expires = exp
|
||||||
|
}
|
||||||
|
protoCookies = append(protoCookies, p)
|
||||||
|
}
|
||||||
|
return page.SetCookies(protoCookies)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WaitForElement(page *rod.Page, selector string, timeout time.Duration) (*rod.Element, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
return page.Context(ctx).Element(selector)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WaitForElements(page *rod.Page, selector string, timeout time.Duration) (rod.Elements, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
return page.Context(ctx).Elements(selector)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,194 @@
|
||||||
|
package geminiweb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-rod/rod"
|
||||||
|
"github.com/go-rod/rod/lib/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
const geminiURL = "https://gemini.google.com/app"
|
||||||
|
|
||||||
|
var modelSelectors = map[string]string{
|
||||||
|
"gemini-2.0-flash": "Flash",
|
||||||
|
"gemini-2.5-pro": "Pro",
|
||||||
|
"gemini-2.5-pro-thinking": "Thinking",
|
||||||
|
}
|
||||||
|
|
||||||
|
func NormalizeModel(model string) string {
|
||||||
|
if strings.HasPrefix(model, "gemini-") {
|
||||||
|
return model
|
||||||
|
}
|
||||||
|
return "gemini-" + model
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetModelDisplayName(model string) string {
|
||||||
|
if name, ok := modelSelectors[model]; ok {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return "Flash"
|
||||||
|
}
|
||||||
|
|
||||||
|
func NavigateToGemini(page *rod.Page) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := page.Context(ctx).Navigate(geminiURL); err != nil {
|
||||||
|
return fmt.Errorf("failed to navigate to gemini: %w", err)
|
||||||
|
}
|
||||||
|
return page.Context(ctx).WaitLoad()
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsLoggedIn(page *rod.Page) bool {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_, err := page.Context(ctx).Element(`[aria-label*="New chat"], [data-test-id*="new-chat"], button[aria-label*="chat"]`)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SelectModel(page *rod.Page, model string) error {
|
||||||
|
displayName := GetModelDisplayName(model)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
modelSwitcher, err := page.Context(ctx).Element(`button[aria-label*="model"], [data-test-id="model-selector"], button[aria-haspopup="listbox"]`)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("model selector not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := modelSwitcher.Click(proto.InputMouseButtonLeft, 1); err != nil {
|
||||||
|
return fmt.Errorf("failed to click model selector: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
|
option, err := page.Context(ctx).Element(fmt.Sprintf(`[aria-label*="%s"], [data-value="%s"]`, displayName, displayName))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("model option %s not found: %w", displayName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return option.Click(proto.InputMouseButtonLeft, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendPrompt(page *rod.Page, prompt string) error {
|
||||||
|
textarea, err := page.Element(`textarea[aria-label*="message"], textarea[placeholder*="message"], rich-textarea, .ql-editor, div[contenteditable="true"]`)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("input field not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := textarea.Input(prompt); err != nil {
|
||||||
|
return fmt.Errorf("failed to input prompt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(300 * time.Millisecond)
|
||||||
|
|
||||||
|
sendBtn, err := page.Element(`button[aria-label*="Send"], button[aria-label*="submit"], button[type="submit"]`)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("send button not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sendBtn.Click(proto.InputMouseButtonLeft, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WaitForResponse(page *rod.Page, onChunk func(text string), onThinking func(thinking string), onComplete func()) error {
|
||||||
|
lastText := ""
|
||||||
|
lastThinking := ""
|
||||||
|
responseComplete := false
|
||||||
|
|
||||||
|
timeout := time.NewTimer(120 * time.Second)
|
||||||
|
defer timeout.Stop()
|
||||||
|
|
||||||
|
ticker := time.NewTicker(500 * time.Millisecond)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-timeout.C:
|
||||||
|
return fmt.Errorf("response timeout")
|
||||||
|
case <-ticker.C:
|
||||||
|
textChanged := false
|
||||||
|
|
||||||
|
responseEls, err := page.Elements(`.response-text, message-content, .model-response, div[data-test-id="response"]`)
|
||||||
|
if err == nil && len(responseEls) > 0 {
|
||||||
|
for _, el := range responseEls {
|
||||||
|
text, _ := el.Text()
|
||||||
|
text = strings.TrimSpace(text)
|
||||||
|
if text != "" && text != lastText {
|
||||||
|
if strings.Contains(text, lastText) {
|
||||||
|
newPart := strings.TrimPrefix(text, lastText)
|
||||||
|
if newPart != "" {
|
||||||
|
onChunk(newPart)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onChunk(text)
|
||||||
|
}
|
||||||
|
lastText = text
|
||||||
|
textChanged = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
thinkingEls, err := page.Elements(`.thinking-content, .thought-text, div[data-test-id="thinking"]`)
|
||||||
|
if err == nil && len(thinkingEls) > 0 {
|
||||||
|
for _, el := range thinkingEls {
|
||||||
|
thinking, _ := el.Text()
|
||||||
|
thinking = strings.TrimSpace(thinking)
|
||||||
|
if thinking != "" && thinking != lastThinking {
|
||||||
|
if strings.Contains(thinking, lastThinking) {
|
||||||
|
newPart := strings.TrimPrefix(thinking, lastThinking)
|
||||||
|
if newPart != "" {
|
||||||
|
onThinking(newPart)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onThinking(thinking)
|
||||||
|
}
|
||||||
|
lastThinking = thinking
|
||||||
|
textChanged = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
doneBtn, err := page.Element(`button[aria-label*="stop"], button[aria-label*="regenerate"]`)
|
||||||
|
if err == nil && doneBtn != nil {
|
||||||
|
ariaLabel, _ := doneBtn.Attribute("aria-label")
|
||||||
|
if ariaLabel != nil && (*ariaLabel == "Stop" || strings.Contains(*ariaLabel, "regenerate")) {
|
||||||
|
if !responseComplete && lastText != "" {
|
||||||
|
responseComplete = true
|
||||||
|
onComplete()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !textChanged && responseComplete {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsRateLimited(page *rod.Page) bool {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
el, err := page.Context(ctx).Element(`[class*="rate-limit"], [class*="quota"], [data-test-id="rate-limited"]`)
|
||||||
|
return err == nil && el != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRateLimitMessage(page *rod.Page) string {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
el, err := page.Context(ctx).Element(`[class*="rate-limit"], [class*="quota"], [class*="error-message"]`)
|
||||||
|
if err != nil || el == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
text, _ := el.Text()
|
||||||
|
return strings.TrimSpace(text)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,169 @@
|
||||||
|
package geminiweb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GeminiSession struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
CookieFile string `json:"cookie_file"`
|
||||||
|
LastUsed int64 `json:"last_used"`
|
||||||
|
ActiveCount int `json:"active_count"`
|
||||||
|
RateLimitEnd int64 `json:"rate_limit_end"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SessionPool struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
sessions []*GeminiSession
|
||||||
|
dir string
|
||||||
|
maxCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSessionPool(dir string, maxSessions int) (*SessionPool, error) {
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create session dir: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sessions, err := loadSessions(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load sessions: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &SessionPool{
|
||||||
|
sessions: sessions,
|
||||||
|
dir: dir,
|
||||||
|
maxCount: maxSessions,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadSessions(dir string) ([]*GeminiSession, error) {
|
||||||
|
entries, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var sessions []*GeminiSession
|
||||||
|
for _, entry := range entries {
|
||||||
|
if !entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := entry.Name()
|
||||||
|
metaPath := filepath.Join(dir, name, "session.json")
|
||||||
|
data, err := os.ReadFile(metaPath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var s GeminiSession
|
||||||
|
if err := json.Unmarshal(data, &s); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sessions = append(sessions, &s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sessions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SessionPool) Count() int {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
return len(p.sessions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SessionPool) GetAvailable() *GeminiSession {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
|
now := time.Now().UnixMilli()
|
||||||
|
|
||||||
|
var available []*GeminiSession
|
||||||
|
for _, s := range p.sessions {
|
||||||
|
if s.RateLimitEnd < now {
|
||||||
|
available = append(available, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(available) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var best *GeminiSession
|
||||||
|
for _, s := range available {
|
||||||
|
if best == nil || s.ActiveCount < best.ActiveCount {
|
||||||
|
best = s
|
||||||
|
} else if s.ActiveCount == best.ActiveCount && s.LastUsed < best.LastUsed {
|
||||||
|
best = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return best
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SessionPool) StartSession(s *GeminiSession) {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
s.ActiveCount++
|
||||||
|
s.LastUsed = time.Now().UnixMilli()
|
||||||
|
p.saveSession(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SessionPool) EndSession(s *GeminiSession) {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
if s.ActiveCount > 0 {
|
||||||
|
s.ActiveCount--
|
||||||
|
}
|
||||||
|
p.saveSession(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SessionPool) RateLimitSession(s *GeminiSession, durationMs int64) {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
s.RateLimitEnd = time.Now().UnixMilli() + durationMs
|
||||||
|
p.saveSession(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SessionPool) saveSession(s *GeminiSession) {
|
||||||
|
metaPath := filepath.Join(p.dir, s.Name, "session.json")
|
||||||
|
data, err := json.MarshalIndent(s, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = os.WriteFile(metaPath, data, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SessionPool) CreateSession(name string) (*GeminiSession, error) {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
|
sessionDir := filepath.Join(p.dir, name)
|
||||||
|
if err := os.MkdirAll(sessionDir, 0755); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create session dir: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &GeminiSession{
|
||||||
|
Name: name,
|
||||||
|
CookieFile: filepath.Join(sessionDir, "cookies.json"),
|
||||||
|
LastUsed: time.Now().UnixMilli(),
|
||||||
|
}
|
||||||
|
|
||||||
|
p.sessions = append(p.sessions, s)
|
||||||
|
p.saveSession(s)
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SessionPool) GetSessionNames() []string {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
names := make([]string, len(p.sessions))
|
||||||
|
for i, s := range p.sessions {
|
||||||
|
names[i] = s.Name
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,197 @@
|
||||||
|
package geminiweb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"cursor-api-proxy/internal/apitypes"
|
||||||
|
"cursor-api-proxy/internal/config"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-rod/rod"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Provider struct {
|
||||||
|
cfg config.BridgeConfig
|
||||||
|
pool *SessionPool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProvider(cfg config.BridgeConfig) *Provider {
|
||||||
|
return &Provider{cfg: cfg}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) Name() string {
|
||||||
|
return "gemini-web"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) initPool() error {
|
||||||
|
if p.pool != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
pool, err := NewSessionPool(p.cfg.GeminiAccountDir, p.cfg.GeminiMaxSessions)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to init session pool: %w", err)
|
||||||
|
}
|
||||||
|
p.pool = pool
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) Generate(ctx context.Context, model string, messages []apitypes.Message, tools []apitypes.Tool, cb func(apitypes.StreamChunk)) error {
|
||||||
|
if err := p.initPool(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
session := p.pool.GetAvailable()
|
||||||
|
if session == nil {
|
||||||
|
return fmt.Errorf("no available sessions")
|
||||||
|
}
|
||||||
|
|
||||||
|
p.pool.StartSession(session)
|
||||||
|
defer p.pool.EndSession(session)
|
||||||
|
|
||||||
|
browser, err := NewBrowser(p.cfg.GeminiBrowserVisible)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create browser: %w", err)
|
||||||
|
}
|
||||||
|
defer browser.Close()
|
||||||
|
|
||||||
|
page, err := browser.NewPage()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create page: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if session.CookieFile != "" {
|
||||||
|
cookies, err := LoadCookiesFromFile(session.CookieFile)
|
||||||
|
if err == nil {
|
||||||
|
if err := SetCookiesOnPage(page, cookies); err != nil {
|
||||||
|
return fmt.Errorf("failed to set cookies: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := NavigateToGemini(page); err != nil {
|
||||||
|
return fmt.Errorf("failed to navigate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
|
if !IsLoggedIn(page) {
|
||||||
|
return fmt.Errorf("session not logged in, please run gemini-login first")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := SelectModel(page, model); err != nil {
|
||||||
|
return fmt.Errorf("failed to select model: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
|
prompt := buildPromptFromMessages(messages)
|
||||||
|
if err := SendPrompt(page, prompt); err != nil {
|
||||||
|
return fmt.Errorf("failed to send prompt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return WaitForResponse(page,
|
||||||
|
func(text string) {
|
||||||
|
cb(apitypes.StreamChunk{Type: apitypes.ChunkText, Text: text})
|
||||||
|
},
|
||||||
|
func(thinking string) {
|
||||||
|
cb(apitypes.StreamChunk{Type: apitypes.ChunkThinking, Thinking: thinking})
|
||||||
|
},
|
||||||
|
func() {
|
||||||
|
cb(apitypes.StreamChunk{Type: apitypes.ChunkDone, Done: true})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildPromptFromMessages(messages []apitypes.Message) string {
|
||||||
|
var prompt string
|
||||||
|
for _, m := range messages {
|
||||||
|
switch m.Role {
|
||||||
|
case "system":
|
||||||
|
prompt += "System: " + m.Content + "\n\n"
|
||||||
|
case "user":
|
||||||
|
prompt += m.Content + "\n\n"
|
||||||
|
case "assistant":
|
||||||
|
prompt += "Assistant: " + m.Content + "\n\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunLogin(cfg config.BridgeConfig, sessionName string) error {
|
||||||
|
if sessionName == "" {
|
||||||
|
sessionName = fmt.Sprintf("session-%d", time.Now().Unix())
|
||||||
|
}
|
||||||
|
|
||||||
|
pool, err := NewSessionPool(cfg.GeminiAccountDir, cfg.GeminiMaxSessions)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to init pool: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
session, err := pool.CreateSession(sessionName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create session: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Starting browser for login. Session: %s\n", sessionName)
|
||||||
|
fmt.Println("Please log in to your Gemini account in the browser window.")
|
||||||
|
fmt.Println("Press Ctrl+C when you have completed the login...")
|
||||||
|
|
||||||
|
browser, err := NewBrowser(true)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create browser: %w", err)
|
||||||
|
}
|
||||||
|
defer browser.Close()
|
||||||
|
|
||||||
|
page, err := browser.NewPage()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create page: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := NavigateToGemini(page); err != nil {
|
||||||
|
return fmt.Errorf("failed to navigate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sigChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
||||||
|
<-sigChan
|
||||||
|
|
||||||
|
cookies, err := GetPageCookies(page)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get cookies: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := SaveCookiesToFile(cookies, session.CookieFile); err != nil {
|
||||||
|
return fmt.Errorf("failed to save cookies: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Session saved successfully: %s\n", sessionName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPageCookies(page *rod.Page) ([]Cookie, error) {
|
||||||
|
cookies, err := page.Cookies([]string{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get cookies: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []Cookie
|
||||||
|
for _, c := range cookies {
|
||||||
|
result = append(result, Cookie{
|
||||||
|
Name: c.Name,
|
||||||
|
Value: c.Value,
|
||||||
|
Domain: c.Domain,
|
||||||
|
Path: c.Path,
|
||||||
|
HTTPOnly: c.HTTPOnly,
|
||||||
|
Secure: c.Secure,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue