feat(proxy) : Add proxy support for mainland

This commit is contained in:
Symb0x76
2025-10-15 16:51:44 +08:00
parent 0e091aede2
commit c4461787f9
6 changed files with 544 additions and 87 deletions
+359 -69
View File
@@ -2,13 +2,14 @@ package chatlog
import (
"fmt"
"net"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
"net"
"strings"
"github.com/sjzar/chatlog/internal/chatlog/conf"
"github.com/sjzar/chatlog/internal/chatlog/ctx"
"github.com/sjzar/chatlog/internal/ui/footer"
"github.com/sjzar/chatlog/internal/ui/form"
@@ -26,6 +27,21 @@ const (
RefreshInterval = 1000 * time.Millisecond
)
type settingsKey string
const (
settingKeyHTTPAddr settingsKey = "http_addr"
settingKeyToggleListen settingsKey = "toggle_listen"
settingKeyWorkDir settingsKey = "work_dir"
settingKeyDataDir settingsKey = "data_dir"
settingKeyDataKey settingsKey = "data_key"
settingKeyImgKey settingsKey = "img_key"
settingKeyOpenAIAPIKey settingsKey = "openai_api_key"
settingKeyOpenAIBaseURL settingsKey = "openai_base_url"
settingKeyOpenAIProxy settingsKey = "openai_proxy"
settingKeyOpenAITimeout settingsKey = "openai_timeout"
)
type App struct {
*tview.Application
@@ -40,26 +56,31 @@ type App struct {
footer *footer.Footer
// tab
menu *menu.Menu
help *help.Help
activeTab int
tabCount int
menu *menu.Menu
help *help.Help
settingsMenu *menu.SubMenu
settingsItems []*menu.Item
settingsItemMap map[settingsKey]*menu.Item
activeTab int
tabCount int
}
func NewApp(ctx *ctx.Context, m *Manager) *App {
app := &App{
ctx: ctx,
m: m,
Application: tview.NewApplication(),
mainPages: tview.NewPages(),
infoBar: infobar.New(),
tabPages: tview.NewPages(),
footer: footer.New(),
menu: menu.New("主菜单"),
help: help.New(),
ctx: ctx,
m: m,
Application: tview.NewApplication(),
mainPages: tview.NewPages(),
infoBar: infobar.New(),
tabPages: tview.NewPages(),
footer: footer.New(),
menu: menu.New("主菜单"),
help: help.New(),
settingsItemMap: make(map[settingsKey]*menu.Item),
}
app.initMenu()
app.initSettingsTab()
app.updateMenuItemsState()
@@ -79,7 +100,12 @@ func (a *App) Run() error {
a.tabPages.
AddPage("0", a.menu, true, true).
AddPage("1", a.help, true, false)
a.tabCount = 2
if a.settingsMenu != nil {
a.tabPages.AddPage("2", a.settingsMenu, true, false)
a.tabCount = 3
} else {
a.tabCount = 2
}
a.SetInputCapture(a.inputCapture)
@@ -124,6 +150,8 @@ func (a *App) updateMenuItemsState() {
item.Description = "启动本地 HTTP & MCP 服务器"
}
}
a.refreshSettingsMenu()
}
}
@@ -134,6 +162,26 @@ func (a *App) switchTab(step int) {
}
a.activeTab = index
a.tabPages.SwitchToPage(fmt.Sprint(a.activeTab))
switch a.activeTab {
case 0:
a.SetFocus(a.menu)
case 1:
a.SetFocus(a.help)
case 2:
if a.settingsMenu != nil {
a.SetFocus(a.settingsMenu)
}
}
}
func (a *App) focusSettingsTab() {
if a.settingsMenu == nil {
return
}
a.activeTab = 2
a.tabPages.SwitchToPage("2")
a.refreshSettingsMenu()
a.SetFocus(a.settingsMenu)
}
func (a *App) refresh() {
@@ -241,6 +289,7 @@ func (a *App) initMenu() {
} else {
// 解密成功
modal.SetText("获取密钥成功")
a.refreshSettingsMenu()
}
// 添加确认按钮
@@ -448,7 +497,9 @@ func (a *App) initMenu() {
Index: 6,
Name: "设置",
Description: "设置应用程序选项",
Selected: a.settingSelected,
Selected: func(*menu.Item) {
a.focusSettingsTab()
},
}
selectAccount := &menu.Item{
@@ -475,65 +526,271 @@ func (a *App) initMenu() {
})
}
// settingItem 表示一个设置项
type settingItem struct {
name string
description string
action func()
func (a *App) initSettingsTab() {
a.settingsMenu = menu.NewSubMenu("设置")
if a.settingsMenu == nil {
return
}
a.settingsMenu.SetCancelFunc(nil)
a.settingsItems = []*menu.Item{
a.newSettingsItem(1, "设置 HTTP 服务地址", settingKeyHTTPAddr, a.settingHTTPPort),
a.newSettingsItem(2, "切换局域网监听", settingKeyToggleListen, a.toggleListen),
a.newSettingsItem(3, "设置工作目录", settingKeyWorkDir, a.settingWorkDir),
a.newSettingsItem(4, "设置数据目录", settingKeyDataDir, a.settingDataDir),
a.newSettingsItem(5, "设置数据密钥", settingKeyDataKey, a.settingDataKey),
a.newSettingsItem(6, "设置图片密钥", settingKeyImgKey, a.settingImgKey),
a.newSettingsItem(7, "设置 OpenAI API Key", settingKeyOpenAIAPIKey, a.settingOpenAIAPIKey),
a.newSettingsItem(8, "设置 OpenAI Base URL", settingKeyOpenAIBaseURL, a.settingOpenAIBaseURL),
a.newSettingsItem(9, "设置 OpenAI 代理", settingKeyOpenAIProxy, a.settingOpenAIProxy),
a.newSettingsItem(10, "设置 OpenAI 请求超时", settingKeyOpenAITimeout, a.settingOpenAITimeout),
}
a.settingsMenu.SetItems(a.settingsItems)
a.refreshSettingsMenu()
}
func (a *App) settingSelected(i *menu.Item) {
settings := []settingItem{
{
name: "设置 HTTP 服务地址",
description: "配置 HTTP 服务监听的地址",
action: a.settingHTTPPort,
},
{
name: "切换局域网监听",
description: "切换局域网访问限制",
action: a.toggleListen,
},
{
name: "设置工作目录",
description: "配置数据解密后的存储目录",
action: a.settingWorkDir,
},
{
name: "设置数据密钥",
description: "配置数据解密密钥",
action: a.settingDataKey,
},
{
name: "设置图片密钥",
description: "配置图片解密密钥",
action: a.settingImgKey,
},
{
name: "设置数据目录",
description: "配置微信数据文件所在目录",
action: a.settingDataDir,
func (a *App) newSettingsItem(index int, name string, key settingsKey, action func()) *menu.Item {
item := &menu.Item{
Index: index,
Name: name,
Selected: func(*menu.Item) {
if action != nil {
action()
}
},
}
if a.settingsItemMap == nil {
a.settingsItemMap = make(map[settingsKey]*menu.Item)
}
a.settingsItemMap[key] = item
return item
}
subMenu := menu.NewSubMenu("设置")
for idx, setting := range settings {
item := &menu.Item{
Index: idx + 1,
Name: setting.name,
Description: setting.description,
Selected: func(action func()) func(*menu.Item) {
return func(*menu.Item) {
action()
}
}(setting.action),
func (a *App) refreshSettingsMenu() {
if a.settingsMenu == nil || len(a.settingsItems) == 0 {
return
}
if item := a.settingsItemMap[settingKeyHTTPAddr]; item != nil {
item.Description = fmt.Sprintf("配置 HTTP 服务监听的地址 (当前: %s)", formatPathWithFallback(a.ctx.GetHTTPAddr(), "未设置"))
}
if item := a.settingsItemMap[settingKeyToggleListen]; item != nil {
host := a.ctx.GetHTTPAddr()
if h, _, err := net.SplitHostPort(host); err == nil {
host = h
}
subMenu.AddItem(item)
if strings.TrimSpace(host) == "" {
host = "127.0.0.1"
}
item.Description = fmt.Sprintf("当前监听主机: %s", host)
}
a.mainPages.AddPage("submenu", subMenu, true, true)
a.SetFocus(subMenu)
if item := a.settingsItemMap[settingKeyWorkDir]; item != nil {
item.Description = fmt.Sprintf("配置数据解密后的存储目录 (当前: %s)", formatPathWithFallback(a.ctx.WorkDir, "未设置"))
}
if item := a.settingsItemMap[settingKeyDataDir]; item != nil {
item.Description = fmt.Sprintf("配置微信数据目录 (当前: %s)", formatPathWithFallback(a.ctx.DataDir, "未设置"))
}
if item := a.settingsItemMap[settingKeyDataKey]; item != nil {
item.Description = fmt.Sprintf("配置数据密钥 (当前: %s)", formatSecretSummary(a.ctx.DataKey))
}
if item := a.settingsItemMap[settingKeyImgKey]; item != nil {
item.Description = fmt.Sprintf("配置图片密钥 (当前: %s)", formatSecretSummary(a.ctx.ImgKey))
}
speech := a.ctx.GetSpeech()
var speechCfg conf.SpeechConfig
if speech != nil {
speechCfg = *speech
}
if item := a.settingsItemMap[settingKeyOpenAIAPIKey]; item != nil {
item.Description = fmt.Sprintf("配置 OpenAI API Key (当前: %s)", formatSecretSummary(speechCfg.APIKey))
}
if item := a.settingsItemMap[settingKeyOpenAIBaseURL]; item != nil {
item.Description = fmt.Sprintf("配置 OpenAI Base URL (当前: %s)", formatPathWithFallback(speechCfg.BaseURL, "官方默认"))
}
if item := a.settingsItemMap[settingKeyOpenAIProxy]; item != nil {
item.Description = fmt.Sprintf("配置 OpenAI 代理 (当前: %s)", formatPathWithFallback(speechCfg.Proxy, "未设置"))
}
if item := a.settingsItemMap[settingKeyOpenAITimeout]; item != nil {
item.Description = fmt.Sprintf("配置 OpenAI 请求超时 (当前: %s)", formatTimeoutSummary(speechCfg.RequestTimeoutSeconds))
}
a.settingsMenu.SetItems(a.settingsItems)
}
func (a *App) updateSpeechConfig(mutator func(*conf.SpeechConfig)) error {
current := a.ctx.GetSpeech()
cfg := conf.SpeechConfig{Enabled: true, Provider: "openai"}
if current != nil {
cfg = *current
}
if mutator != nil {
mutator(&cfg)
}
cfg.Normalize()
return a.m.SaveSpeechConfig(&cfg)
}
func (a *App) settingOpenAIAPIKey() {
formView := form.NewForm("设置 OpenAI API Key")
speech := a.ctx.GetSpeech()
currentValue := ""
if speech != nil {
currentValue = speech.APIKey
}
tempValue := currentValue
formView.AddInputField("API Key", tempValue, 0, nil, func(text string) {
tempValue = text
})
formView.AddButton("保存", func() {
if err := a.updateSpeechConfig(func(cfg *conf.SpeechConfig) {
cfg.APIKey = tempValue
}); err != nil {
a.showError(err)
return
}
a.mainPages.RemovePage("submenu2")
a.refreshSettingsMenu()
a.showInfo("OpenAI API Key 已更新")
})
formView.AddButton("取消", func() {
a.mainPages.RemovePage("submenu2")
})
a.mainPages.AddPage("submenu2", formView, true, true)
a.SetFocus(formView)
}
func (a *App) settingOpenAIBaseURL() {
formView := form.NewForm("设置 OpenAI Base URL")
speech := a.ctx.GetSpeech()
currentValue := ""
if speech != nil {
currentValue = speech.BaseURL
}
tempValue := currentValue
formView.AddInputField("Base URL", tempValue, 0, nil, func(text string) {
tempValue = text
})
formView.AddButton("保存", func() {
if err := a.updateSpeechConfig(func(cfg *conf.SpeechConfig) {
cfg.BaseURL = tempValue
}); err != nil {
a.showError(err)
return
}
a.mainPages.RemovePage("submenu2")
a.refreshSettingsMenu()
a.showInfo("OpenAI Base URL 已更新")
})
formView.AddButton("取消", func() {
a.mainPages.RemovePage("submenu2")
})
a.mainPages.AddPage("submenu2", formView, true, true)
a.SetFocus(formView)
}
func (a *App) settingOpenAIProxy() {
formView := form.NewForm("设置 OpenAI 代理")
speech := a.ctx.GetSpeech()
currentValue := ""
if speech != nil {
currentValue = speech.Proxy
}
tempValue := currentValue
formView.AddInputField("代理地址", tempValue, 0, nil, func(text string) {
tempValue = text
})
formView.AddButton("保存", func() {
if err := a.updateSpeechConfig(func(cfg *conf.SpeechConfig) {
cfg.Proxy = tempValue
}); err != nil {
a.showError(err)
return
}
a.mainPages.RemovePage("submenu2")
a.refreshSettingsMenu()
a.showInfo("OpenAI 代理已更新")
})
formView.AddButton("取消", func() {
a.mainPages.RemovePage("submenu2")
})
a.mainPages.AddPage("submenu2", formView, true, true)
a.SetFocus(formView)
}
func (a *App) settingOpenAITimeout() {
formView := form.NewForm("设置 OpenAI 请求超时")
speech := a.ctx.GetSpeech()
currentValue := ""
if speech != nil && speech.RequestTimeoutSeconds > 0 {
currentValue = strconv.Itoa(speech.RequestTimeoutSeconds)
}
tempValue := currentValue
acceptNumeric := func(text string, lastChar rune) bool {
if lastChar == 0 {
return true
}
return lastChar >= '0' && lastChar <= '9'
}
formView.AddInputField("超时(秒)", tempValue, 0, acceptNumeric, func(text string) {
tempValue = text
})
formView.AddButton("保存", func() {
trimmed := strings.TrimSpace(tempValue)
seconds := 0
if trimmed != "" {
v, err := strconv.Atoi(trimmed)
if err != nil {
a.showError(fmt.Errorf("请输入合法的非负整数"))
return
}
seconds = v
}
if err := a.updateSpeechConfig(func(cfg *conf.SpeechConfig) {
cfg.RequestTimeoutSeconds = seconds
}); err != nil {
a.showError(err)
return
}
a.mainPages.RemovePage("submenu2")
a.refreshSettingsMenu()
a.showInfo("OpenAI 请求超时已更新")
})
formView.AddButton("取消", func() {
a.mainPages.RemovePage("submenu2")
})
a.mainPages.AddPage("submenu2", formView, true, true)
a.SetFocus(formView)
}
// settingHTTPPort 设置 HTTP 端口
@@ -553,6 +810,7 @@ func (a *App) settingHTTPPort() {
formView.AddButton("保存", func() {
a.m.SetHTTPAddr(tempHTTPAddr) // 在这里设置HTTP地址
a.mainPages.RemovePage("submenu2")
a.refreshSettingsMenu()
a.showInfo("HTTP 地址已设置为 " + a.ctx.HTTPAddr)
})
@@ -601,6 +859,7 @@ func (a *App) toggleListen() {
if startErr != nil {
a.showError(fmt.Errorf("切换失败: %v", startErr))
} else {
a.refreshSettingsMenu()
a.showInfo("已切换监听地址为 " + newAddr)
}
})
@@ -617,6 +876,7 @@ func (a *App) toggleListen() {
// 服务未运行,仅更新配置
_ = a.m.SetHTTPAddr(newAddr)
a.refreshSettingsMenu()
a.showInfo("已切换监听地址为 " + newAddr)
}
@@ -637,6 +897,7 @@ func (a *App) settingWorkDir() {
formView.AddButton("保存", func() {
a.ctx.SetWorkDir(tempWorkDir) // 在这里设置工作目录
a.mainPages.RemovePage("submenu2")
a.refreshSettingsMenu()
a.showInfo("工作目录已设置为 " + a.ctx.WorkDir)
})
@@ -665,6 +926,7 @@ func (a *App) settingDataKey() {
formView.AddButton("保存", func() {
a.ctx.DataKey = tempDataKey // 设置数据密钥
a.mainPages.RemovePage("submenu2")
a.refreshSettingsMenu()
a.showInfo("数据密钥已设置")
})
@@ -689,6 +951,7 @@ func (a *App) settingImgKey() {
formView.AddButton("保存", func() {
a.ctx.SetImgKey(tempImgKey)
a.mainPages.RemovePage("submenu2")
a.refreshSettingsMenu()
a.showInfo("图片密钥已设置")
})
@@ -717,6 +980,7 @@ func (a *App) settingDataDir() {
formView.AddButton("保存", func() {
a.ctx.DataDir = tempDataDir // 设置数据目录
a.mainPages.RemovePage("submenu2")
a.refreshSettingsMenu()
a.showInfo("数据目录已设置为 " + a.ctx.DataDir)
})
@@ -917,3 +1181,29 @@ func (a *App) showInfo(text string) {
a.mainPages.RemovePage("modal")
})
}
func formatPathWithFallback(value, fallback string) string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return fallback
}
return trimmed
}
func formatSecretSummary(value string) string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return "未设置"
}
if len(trimmed) <= 6 {
return "已设置"
}
return fmt.Sprintf("已设置(长度 %d)", len(trimmed))
}
func formatTimeoutSummary(seconds int) string {
if seconds <= 0 {
return "默认"
}
return fmt.Sprintf("%d 秒", seconds)
}
+79 -14
View File
@@ -1,22 +1,87 @@
package conf
import "github.com/sjzar/chatlog/internal/whisper"
import (
"strings"
"github.com/sjzar/chatlog/internal/whisper"
)
// OpenAISettings captures provider-specific fields retained for backwards compatibility.
type OpenAISettings struct {
APIKey string `mapstructure:"api_key" json:"api_key"`
BaseURL string `mapstructure:"base_url" json:"base_url"`
Organization string `mapstructure:"organization" json:"organization"`
Project string `mapstructure:"project" json:"project"`
Proxy string `mapstructure:"proxy" json:"proxy"`
}
// SpeechConfig controls optional speech-to-text features.
type SpeechConfig struct {
Enabled bool `mapstructure:"enabled" json:"enabled"`
Model string `mapstructure:"model" json:"model"`
TranslateModel string `mapstructure:"translate_model" json:"translate_model"`
Threads int `mapstructure:"threads" json:"threads"`
Language string `mapstructure:"language" json:"language"`
Translate *bool `mapstructure:"translate" json:"translate"`
InitialPrompt string `mapstructure:"initial_prompt" json:"initial_prompt"`
Temperature *float64 `mapstructure:"temperature" json:"temperature"`
TemperatureFallback *float64 `mapstructure:"temperature_fallback" json:"temperature_fallback"`
APIKey string `mapstructure:"api_key" json:"api_key"`
BaseURL string `mapstructure:"base_url" json:"base_url"`
Organization string `mapstructure:"organization" json:"organization"`
RequestTimeoutSeconds int `mapstructure:"request_timeout_seconds" json:"request_timeout_seconds"`
Enabled bool `mapstructure:"enabled" json:"enabled"`
Provider string `mapstructure:"provider" json:"provider"`
Model string `mapstructure:"model" json:"model"`
TranslateModel string `mapstructure:"translate_model" json:"translate_model"`
Threads int `mapstructure:"threads" json:"threads"`
Language string `mapstructure:"language" json:"language"`
Translate *bool `mapstructure:"translate" json:"translate"`
InitialPrompt string `mapstructure:"initial_prompt" json:"initial_prompt"`
Temperature *float64 `mapstructure:"temperature" json:"temperature"`
TemperatureFallback *float64 `mapstructure:"temperature_fallback" json:"temperature_fallback"`
APIKey string `mapstructure:"api_key" json:"api_key"`
BaseURL string `mapstructure:"base_url" json:"base_url"`
Organization string `mapstructure:"organization" json:"organization"`
Project string `mapstructure:"project" json:"project"`
Proxy string `mapstructure:"proxy" json:"proxy"`
RequestTimeoutSeconds int `mapstructure:"request_timeout_seconds" json:"request_timeout_seconds"`
OpenAI OpenAISettings `mapstructure:"openai" json:"openai"`
}
// Normalize hydrates legacy OpenAI fields into the flattened structure and applies defaults.
func (c *SpeechConfig) Normalize() {
if c == nil {
return
}
if c.Provider == "" {
c.Provider = "openai"
}
if c.APIKey == "" {
c.APIKey = c.OpenAI.APIKey
}
if c.BaseURL == "" {
c.BaseURL = c.OpenAI.BaseURL
}
if c.Organization == "" {
c.Organization = c.OpenAI.Organization
}
if c.Project == "" {
c.Project = c.OpenAI.Project
}
if c.Proxy == "" {
c.Proxy = c.OpenAI.Proxy
}
c.APIKey = strings.TrimSpace(c.APIKey)
c.BaseURL = strings.TrimSpace(c.BaseURL)
c.Organization = strings.TrimSpace(c.Organization)
c.Project = strings.TrimSpace(c.Project)
c.Proxy = strings.TrimSpace(c.Proxy)
}
// PrepareForSave syncs the flattened OpenAI fields back into the legacy nested structure.
func (c *SpeechConfig) PrepareForSave() {
if c == nil {
return
}
if c.Provider == "" {
c.Provider = "openai"
}
c.OpenAI.APIKey = c.APIKey
c.OpenAI.BaseURL = c.BaseURL
c.OpenAI.Organization = c.Organization
c.OpenAI.Project = c.Project
c.OpenAI.Proxy = c.Proxy
}
// ToOptions converts the speech config into runtime options for a transcription backend.
+44 -3
View File
@@ -2,6 +2,7 @@ package ctx
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"sync"
@@ -25,8 +26,9 @@ type Context struct {
cm *config.Manager
mu sync.RWMutex
History map[string]conf.ProcessConfig
speech *conf.SpeechConfig
History map[string]conf.ProcessConfig
speech *conf.SpeechConfig
speechPath string
// 微信账号相关状态
Account string
@@ -83,21 +85,25 @@ func (c *Context) loadConfig() {
c.Refresh()
if c.cm != nil {
speechPath := filepath.Join(c.cm.Path, "whisper.json")
c.speechPath = speechPath
if data, err := os.ReadFile(speechPath); err == nil {
var sc conf.SpeechConfig
if err := json.Unmarshal(data, &sc); err != nil {
log.Debug().Err(err).Msg("failed to parse speech config")
} else {
sc.Normalize()
c.speech = &sc
}
} else if os.IsNotExist(err) {
defaultSpeech := conf.SpeechConfig{Enabled: true, Model: "whisper-1"}
defaultSpeech := conf.SpeechConfig{Enabled: true, Provider: "openai", Model: "whisper-1"}
defaultSpeech.PrepareForSave()
payload, marshalErr := json.MarshalIndent(defaultSpeech, "", " ")
if marshalErr != nil {
log.Error().Err(marshalErr).Msg("failed to marshal default speech config")
} else if writeErr := os.WriteFile(speechPath, payload, 0600); writeErr != nil {
log.Error().Err(writeErr).Msg("failed to write default speech config")
} else {
defaultSpeech.Normalize()
c.speech = &defaultSpeech
log.Info().Str("path", speechPath).Msg("created default speech config")
}
@@ -111,6 +117,41 @@ func (c *Context) loadConfig() {
}
}
func (c *Context) SaveSpeechConfig(cfg *conf.SpeechConfig) error {
if cfg == nil {
return errors.New("speech config is nil")
}
if c.cm == nil {
return errors.New("config manager unavailable")
}
cfg.Enabled = true
cfg.Normalize()
cfg.PrepareForSave()
path := c.speechPath
if path == "" {
path = filepath.Join(c.cm.Path, "whisper.json")
c.speechPath = path
}
payload, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return err
}
if err := os.WriteFile(path, payload, 0o600); err != nil {
return err
}
cfgCopy := *cfg
cfgCopy.Normalize()
c.mu.Lock()
defer c.mu.Unlock()
c.speech = &cfgCopy
return nil
}
func (c *Context) SwitchHistory(account string) {
c.mu.Lock()
defer c.mu.Unlock()
+12
View File
@@ -66,11 +66,18 @@ func NewService(conf Config, db *database.Service) *Service {
}
func (s *Service) initSpeech(cfg Config) {
if s.speechTranscriber != nil {
s.speechTranscriber.Close()
s.speechTranscriber = nil
}
speechCfg := cfg.GetSpeech()
if speechCfg == nil || !speechCfg.Enabled {
return
}
speechCfg.Normalize()
opts := speechCfg.ToOptions()
timeout := time.Duration(speechCfg.RequestTimeoutSeconds) * time.Second
transcriber, err := whisper.NewOpenAITranscriber(whisper.OpenAIConfig{
@@ -79,6 +86,7 @@ func (s *Service) initSpeech(cfg Config) {
APIKey: speechCfg.APIKey,
BaseURL: speechCfg.BaseURL,
Organization: speechCfg.Organization,
ProxyURL: speechCfg.Proxy,
RequestTimeout: timeout,
DefaultOptions: opts,
})
@@ -92,6 +100,10 @@ func (s *Service) initSpeech(cfg Config) {
log.Info().Str("model", transcriber.ModelName()).Msg("speech transcription backend initialised via openai whisper")
}
func (s *Service) ReloadSpeech() {
s.initSpeech(s.conf)
}
func (s *Service) Start() error {
s.server = &http.Server{
+13
View File
@@ -322,6 +322,19 @@ func (m *Manager) StopAutoDecrypt() error {
return nil
}
func (m *Manager) SaveSpeechConfig(cfg *conf.SpeechConfig) error {
if cfg == nil {
return fmt.Errorf("speech config is nil")
}
if err := m.ctx.SaveSpeechConfig(cfg); err != nil {
return err
}
if m.http != nil {
m.http.ReloadSpeech()
}
return nil
}
func (m *Manager) RefreshSession() error {
if m.db.GetDB() == nil {
if err := m.db.Start(); err != nil {
+37 -1
View File
@@ -6,8 +6,11 @@ import (
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
"math"
"net/http"
"net/url"
"strings"
"time"
@@ -26,6 +29,7 @@ type OpenAIConfig struct {
APIKey string
BaseURL string
Organization string
ProxyURL string
RequestTimeout time.Duration
DefaultOptions Options
}
@@ -56,7 +60,13 @@ func NewOpenAITranscriber(cfg OpenAIConfig) (*OpenAITranscriber, error) {
if cfg.BaseURL != "" {
opts = append(opts, option.WithBaseURL(cfg.BaseURL))
}
if cfg.RequestTimeout > 0 {
if cfg.ProxyURL != "" {
client, err := buildHTTPClient(cfg.ProxyURL, cfg.RequestTimeout)
if err != nil {
return nil, err
}
opts = append(opts, option.WithHTTPClient(client))
} else if cfg.RequestTimeout > 0 {
opts = append(opts, option.WithRequestTimeout(cfg.RequestTimeout))
}
@@ -71,6 +81,32 @@ func NewOpenAITranscriber(cfg OpenAIConfig) (*OpenAITranscriber, error) {
}, nil
}
func buildHTTPClient(proxyURL string, timeout time.Duration) (*http.Client, error) {
transport, ok := http.DefaultTransport.(*http.Transport)
var baseTransport *http.Transport
if ok {
baseTransport = transport.Clone()
} else {
baseTransport = &http.Transport{Proxy: http.ProxyFromEnvironment}
}
if proxyURL != "" {
parsed, err := url.Parse(proxyURL)
if err != nil {
return nil, fmt.Errorf("invalid proxy url: %w", err)
}
baseTransport.Proxy = http.ProxyURL(parsed)
}
client := &http.Client{
Transport: baseTransport,
}
if timeout > 0 {
client.Timeout = timeout
}
return client, nil
}
// Close releases resources held by the transcriber. No-op for the OpenAI backend.
func (t *OpenAITranscriber) Close() {}