feat: you can summary screen shot directly
This commit is contained in:
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/mmmy/snapgo/internal/infrastructure/config"
|
||||
"github.com/mmmy/snapgo/internal/infrastructure/display"
|
||||
"github.com/mmmy/snapgo/internal/infrastructure/hotkey"
|
||||
llmpkg "github.com/mmmy/snapgo/internal/infrastructure/llm"
|
||||
"github.com/mmmy/snapgo/internal/infrastructure/oss"
|
||||
"github.com/mmmy/snapgo/internal/infrastructure/screencapture"
|
||||
sshpkg "github.com/mmmy/snapgo/internal/infrastructure/ssh"
|
||||
@@ -96,6 +97,15 @@ type CaptureActionResult struct {
|
||||
Path string `json:"path,omitempty"`
|
||||
}
|
||||
|
||||
// OperationStatusPayload drives the frontend status HUD and mirrors the
|
||||
// native macOS HUD state.
|
||||
type OperationStatusPayload struct {
|
||||
Operation string `json:"operation"`
|
||||
Phase string `json:"phase"`
|
||||
Message string `json:"message"`
|
||||
State string `json:"state"`
|
||||
}
|
||||
|
||||
// NewApp creates a new App with collaborators already initialised.
|
||||
func NewApp() *App {
|
||||
store, err := config.NewFileStore()
|
||||
@@ -110,6 +120,7 @@ func NewApp() *App {
|
||||
slog.Warn("config load failed, using defaults", "err", lerr)
|
||||
}
|
||||
}
|
||||
cfg.Normalize()
|
||||
return &App{
|
||||
cfg: cfg,
|
||||
configFile: store,
|
||||
@@ -297,7 +308,13 @@ func (a *App) runUploadPipeline(provider domain.OSSProvider, pngBytes []byte) er
|
||||
FallbackDir: filepath.Join(userPicturesDir(), "SnapGo"),
|
||||
PathPrefix: cfg.S3.PathPrefix,
|
||||
}
|
||||
return svc.ExecuteWithBytes(a.ctx, pngBytes)
|
||||
a.emitOperationStatus("upload", "上传中", "正在上传截图到 S3", "running")
|
||||
if err := svc.ExecuteWithBytes(a.ctx, pngBytes); err != nil {
|
||||
a.emitOperationStatus("upload", "上传失败", err.Error(), "error")
|
||||
return err
|
||||
}
|
||||
a.emitOperationStatus("upload", "上传完成", "链接已复制到剪贴板", "success")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) runCopyImagePipeline(pngBytes []byte) error {
|
||||
@@ -332,6 +349,7 @@ func (a *App) runSaveRemotePipeline(pngBytes []byte) error {
|
||||
err := fmt.Errorf("SSH host/user is not configured")
|
||||
slog.Warn("save-remote rejected: ssh not configured",
|
||||
"host", cfg.SSH.Host, "user", cfg.SSH.User)
|
||||
a.emitOperationStatus("save-remote", "需要配置 SSH", err.Error(), "error")
|
||||
wruntime.EventsEmit(a.ctx, "upload:failure", err.Error())
|
||||
return err
|
||||
}
|
||||
@@ -355,7 +373,78 @@ func (a *App) runSaveRemotePipeline(pngBytes []byte) error {
|
||||
Notifier: &runtimeNotifier{ctx: a.ctx},
|
||||
Cfg: cfg.SSH,
|
||||
}
|
||||
return svc.ExecuteWithBytes(a.ctx, pngBytes)
|
||||
a.emitOperationStatus("save-remote", "保存中", "正在保存截图到远端 SSH", "running")
|
||||
if err := svc.ExecuteWithBytes(a.ctx, pngBytes); err != nil {
|
||||
a.emitOperationStatus("save-remote", "保存失败", err.Error(), "error")
|
||||
return err
|
||||
}
|
||||
a.emitOperationStatus("save-remote", "保存完成", "远端路径已复制到剪贴板", "success")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) runSummaryPipeline(pngBytes []byte) error {
|
||||
a.mu.RLock()
|
||||
cfg := a.cfg
|
||||
a.mu.RUnlock()
|
||||
|
||||
if !cfg.IsS3Configured() {
|
||||
err := fmt.Errorf("请先在 S3 配置页填写可用的对象存储配置")
|
||||
a.emitOperationStatus("summary", "需要配置 S3", err.Error(), "error")
|
||||
wruntime.EventsEmit(a.ctx, "upload:failure", err.Error())
|
||||
return err
|
||||
}
|
||||
if !cfg.IsLLMConfigured() {
|
||||
err := fmt.Errorf("请先在 LLM 配置页填写 API Key 和模型")
|
||||
a.emitOperationStatus("summary", "需要配置 LLM", err.Error(), "error")
|
||||
wruntime.EventsEmit(a.ctx, "upload:failure", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
s3Provider, err := oss.NewS3Provider(cfg.S3)
|
||||
if err != nil {
|
||||
a.emitOperationStatus("summary", "S3 配置错误", err.Error(), "error")
|
||||
wruntime.EventsEmit(a.ctx, "upload:failure", err.Error())
|
||||
return err
|
||||
}
|
||||
providerID, llmCfg, _ := cfg.ActiveLLMProvider()
|
||||
visionClient, err := llmpkg.NewVisionClient(providerID, llmCfg)
|
||||
if err != nil {
|
||||
a.emitOperationStatus("summary", "LLM 配置错误", err.Error(), "error")
|
||||
wruntime.EventsEmit(a.ctx, "upload:failure", err.Error())
|
||||
return err
|
||||
}
|
||||
svc := &application.CaptureSummaryService{
|
||||
Provider: s3Provider,
|
||||
Summarizer: visionClient,
|
||||
Clipboard: a.clip,
|
||||
Prompt: cfg.LLM.Prompt,
|
||||
PathPrefix: cfg.S3.PathPrefix,
|
||||
}
|
||||
|
||||
a.emitOperationStatus("summary", "上传中", "正在上传截图供模型读取", "running")
|
||||
uploaded, err := svc.UploadImage(a.ctx, pngBytes)
|
||||
if err != nil {
|
||||
a.emitOperationStatus("summary", "上传失败", err.Error(), "error")
|
||||
wruntime.EventsEmit(a.ctx, "upload:failure", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
a.emitOperationStatus("summary", "识别中", "图片已上传,正在请求多模态模型", "running")
|
||||
summary, err := svc.Summarize(a.ctx, uploaded.URL)
|
||||
if err != nil {
|
||||
a.emitOperationStatus("summary", "识别失败", err.Error(), "error")
|
||||
wruntime.EventsEmit(a.ctx, "upload:failure", err.Error())
|
||||
return err
|
||||
}
|
||||
if err := svc.CopySummary(a.ctx, summary); err != nil {
|
||||
a.emitOperationStatus("summary", "复制失败", err.Error(), "error")
|
||||
wruntime.EventsEmit(a.ctx, "upload:failure", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
a.emitOperationStatus("summary", "识别完成", "总结已复制到剪贴板", "success")
|
||||
wruntime.EventsEmit(a.ctx, "upload:success", "summary copied to clipboard")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) consumePendingCapture() (*pendingCapture, error) {
|
||||
@@ -408,6 +497,7 @@ func (a *App) GetConfig() domain.AppConfig {
|
||||
// SaveConfig persists the supplied configuration and re-registers the hotkey
|
||||
// if it changed.
|
||||
func (a *App) SaveConfig(cfg domain.AppConfig) error {
|
||||
cfg.Normalize()
|
||||
a.mu.Lock()
|
||||
prev := a.cfg
|
||||
a.cfg = cfg
|
||||
@@ -705,6 +795,35 @@ func (a *App) SaveRegionToRemote(result CaptureResult) error {
|
||||
return a.runSaveRemotePipeline(cropped)
|
||||
}
|
||||
|
||||
// SummarizeRegion uploads the selected screenshot to S3, sends the public URL
|
||||
// to the configured multimodal LLM, and copies the resulting summary.
|
||||
func (a *App) SummarizeRegion(result CaptureResult) error {
|
||||
pc, err := a.consumePendingCapture()
|
||||
if err != nil {
|
||||
slog.Warn("SummarizeRegion: no pending capture", "err", err)
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
a.capturing.Store(false)
|
||||
a.dismissOverlay()
|
||||
}()
|
||||
|
||||
a.dismissOverlay()
|
||||
flushFrame()
|
||||
|
||||
slog.Info("SummarizeRegion: capturing region",
|
||||
"x", result.Rect.X, "y", result.Rect.Y,
|
||||
"w", result.Rect.W, "h", result.Rect.H,
|
||||
"annotations", len(result.Annotations))
|
||||
cropped, err := a.captureSelectedPNG(result, pc)
|
||||
if err != nil {
|
||||
slog.Error("SummarizeRegion: capture failed", "err", err)
|
||||
wruntime.EventsEmit(a.ctx, "upload:failure", err.Error())
|
||||
return err
|
||||
}
|
||||
return a.runSummaryPipeline(cropped)
|
||||
}
|
||||
|
||||
// SaveNativeRegionToRemote is the macOS-native overlay equivalent of
|
||||
// SaveRegionToRemote. The AppKit panel is already closed by the time this
|
||||
// runs (see saveRemoteSelection in native_overlay_darwin.go), so we only
|
||||
@@ -735,6 +854,33 @@ func (a *App) SaveNativeRegionToRemote(result CaptureResult) error {
|
||||
return a.runSaveRemotePipeline(cropped)
|
||||
}
|
||||
|
||||
// SummarizeNativeRegion is the macOS-native overlay equivalent of
|
||||
// SummarizeRegion. The AppKit panel is already closed before this runs.
|
||||
func (a *App) SummarizeNativeRegion(result CaptureResult) error {
|
||||
pc, err := a.consumePendingCapture()
|
||||
if err != nil {
|
||||
slog.Warn("SummarizeNativeRegion: no pending capture", "err", err)
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
a.capturing.Store(false)
|
||||
hideDockIcon()
|
||||
}()
|
||||
|
||||
flushFrame()
|
||||
slog.Info("SummarizeNativeRegion: capturing region",
|
||||
"x", result.Rect.X, "y", result.Rect.Y,
|
||||
"w", result.Rect.W, "h", result.Rect.H,
|
||||
"annotations", len(result.Annotations))
|
||||
cropped, err := a.captureSelectedPNG(result, pc)
|
||||
if err != nil {
|
||||
slog.Error("SummarizeNativeRegion: capture failed", "err", err)
|
||||
wruntime.EventsEmit(a.ctx, "upload:failure", err.Error())
|
||||
return err
|
||||
}
|
||||
return a.runSummaryPipeline(cropped)
|
||||
}
|
||||
|
||||
func parseNativeAnnotations(raw string) []application.Annotation {
|
||||
if raw == "" {
|
||||
return nil
|
||||
@@ -784,6 +930,27 @@ func (a *App) QuitApp() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func (a *App) emitOperationStatus(operation, phase, message, state string) {
|
||||
if a.ctx != nil {
|
||||
wruntime.EventsEmit(a.ctx, "operation:status", OperationStatusPayload{
|
||||
Operation: operation,
|
||||
Phase: phase,
|
||||
Message: message,
|
||||
State: state,
|
||||
})
|
||||
}
|
||||
switch state {
|
||||
case "success":
|
||||
showOperationStatus(phase, message, operationStatusSuccess)
|
||||
hideOperationStatusAfter(1400 * time.Millisecond)
|
||||
case "error":
|
||||
showOperationStatus(phase, message, operationStatusError)
|
||||
hideOperationStatusAfter(2600 * time.Millisecond)
|
||||
default:
|
||||
showOperationStatus(phase, message, operationStatusRunning)
|
||||
}
|
||||
}
|
||||
|
||||
// runtimeNotifier emits success / failure events via the Wails runtime.
|
||||
type runtimeNotifier struct{ ctx context.Context }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user