-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.go
More file actions
543 lines (478 loc) · 15.6 KB
/
app.go
File metadata and controls
543 lines (478 loc) · 15.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
package main
import (
"bufio"
"context"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/google/uuid"
wailsRuntime "github.com/wailsapp/wails/v2/pkg/runtime"
"network-log-formatter/internal/agent"
"network-log-formatter/internal/config"
"network-log-formatter/internal/executor"
"network-log-formatter/internal/model"
"network-log-formatter/internal/project"
"network-log-formatter/internal/pyenv"
)
// App is the main controller bridging the Wails frontend and Go backend.
type App struct {
ctx context.Context
configDir string
sampleAnalyzer *agent.SampleAnalyzer
codeValidator *agent.CodeValidator
batchExecutor *executor.BatchExecutor
envManager *pyenv.PythonEnvManager
projectManager *project.ProjectManager
settingsManager *config.SettingsManager
llmClient *agent.LLMClient
mu sync.Mutex // protects pyenvReady and pyenvError
pyenvReady bool
pyenvError string
}
// NewApp creates a new App with SettingsManager and ProjectManager initialized.
// LLM-dependent components are initialized lazily after settings are loaded.
func NewApp(configDir string) *App {
settingsMgr := config.NewSettingsManager(filepath.Join(configDir, "settings.json"))
projectsDir := filepath.Join(configDir, "projects")
projectMgr, err := project.NewProjectManager(projectsDir)
if err != nil {
// Log but don't fail — projectManager will be nil and methods will return errors
fmt.Printf("warning: failed to initialize project manager: %v\n", err)
}
return &App{
configDir: configDir,
settingsManager: settingsMgr,
projectManager: projectMgr,
}
}
// startup is called by Wails when the application starts.
// It loads settings and initializes LLM-dependent components.
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
settings, err := a.settingsManager.Load()
if err != nil {
fmt.Printf("warning: failed to load settings: %v\n", err)
// Still initialize env manager with defaults so the app is usable
envPath := filepath.Join(a.configDir, "pyenv")
a.envManager = pyenv.NewPythonEnvManager("uv", envPath)
return
}
// Initialize Python environment manager
uvPath := settings.UvPath
if uvPath == "" {
uvPath = "uv"
}
envPath := filepath.Join(a.configDir, "pyenv")
a.envManager = pyenv.NewPythonEnvManager(uvPath, envPath)
// Auto-initialize Python environment in background
go func() {
if err := a.envManager.EnsureEnv(a.ctx); err != nil {
a.mu.Lock()
a.pyenvError = err.Error()
a.mu.Unlock()
fmt.Printf("warning: auto python env init failed: %v\n", err)
} else {
a.mu.Lock()
a.pyenvReady = true
a.mu.Unlock()
}
}()
// Initialize LLM components if configured
if settings.LLM.BaseURL != "" && settings.LLM.APIKey != "" && settings.LLM.ModelName != "" {
_ = a.initLLMComponents(settings.LLM)
}
}
// initLLMComponents initializes or reinitializes the LLM client and all
// components that depend on it (SampleAnalyzer, CodeValidator, BatchExecutor).
func (a *App) initLLMComponents(cfg model.LLMConfig) error {
llmClient, err := agent.NewLLMClient(cfg)
if err != nil {
return fmt.Errorf("failed to create LLM client: %w", err)
}
a.llmClient = llmClient
a.sampleAnalyzer = agent.NewSampleAnalyzer(llmClient)
a.codeValidator = agent.NewCodeValidator(a.envManager, llmClient, 3)
a.batchExecutor = executor.NewBatchExecutor(
a.envManager,
&llmRepairerAdapter{llmClient: llmClient},
3,
)
return nil
}
// AnalyzeSample analyzes sample log text: calls SampleAnalyzer → CodeValidator → ProjectManager.
func (a *App) AnalyzeSample(projectName string, sampleText string) (*model.GenerateResult, error) {
if a.sampleAnalyzer == nil {
return nil, fmt.Errorf("LLM is not configured. Please configure LLM settings first")
}
if strings.TrimSpace(projectName) == "" {
return nil, fmt.Errorf("请输入项目名称")
}
// 1. Analyze sample to generate Python code
analyzeCtx, analyzeCancel := context.WithTimeout(a.ctx, 2*time.Minute)
defer analyzeCancel()
code, err := a.sampleAnalyzer.Analyze(analyzeCtx, sampleText)
if err != nil {
return nil, fmt.Errorf("sample analysis failed: %w", err)
}
// 2. Validate the generated code
var validationResult *agent.ValidationResult
if a.codeValidator != nil {
validateCtx, validateCancel := context.WithTimeout(a.ctx, 3*time.Minute)
defer validateCancel()
validationResult, err = a.codeValidator.Validate(validateCtx, code)
if err != nil {
return nil, fmt.Errorf("code validation failed: %w", err)
}
code = validationResult.Code
}
// 3. Determine project status
status := "draft"
valid := false
var errors []string
if validationResult != nil {
valid = validationResult.Valid
errors = validationResult.Errors
if valid {
status = "validated"
}
}
// 4. Create project
projectID := uuid.New().String()
now := time.Now()
p := model.Project{
ID: projectID,
Name: strings.TrimSpace(projectName),
SampleData: sampleText,
Code: code,
CreatedAt: now,
UpdatedAt: now,
Status: status,
}
if a.projectManager != nil {
if err := a.projectManager.Create(p); err != nil {
return nil, fmt.Errorf("failed to save project: %w", err)
}
}
return &model.GenerateResult{
ProjectID: projectID,
Code: code,
Valid: valid,
Errors: errors,
}, nil
}
// RunBatch starts batch processing in a background goroutine so it doesn't block the UI.
func (a *App) RunBatch(projectID string, inputDir string, outputDir string, outputFileName string) error {
if a.batchExecutor == nil {
return fmt.Errorf("LLM is not configured. Please configure LLM settings first")
}
if a.projectManager == nil {
return fmt.Errorf("project manager is not initialized")
}
a.mu.Lock()
envReady := a.pyenvReady
a.mu.Unlock()
if !envReady {
return fmt.Errorf("Python 环境尚未就绪,请等待初始化完成")
}
p, err := a.projectManager.Get(projectID)
if err != nil {
return fmt.Errorf("failed to get project: %w", err)
}
if strings.TrimSpace(p.Code) == "" {
return fmt.Errorf("项目代码为空,无法执行")
}
go func() {
_, execErr := a.batchExecutor.Execute(a.ctx, p.Code, inputDir, outputDir, outputFileName)
// Update project status based on result
status := "executed"
if execErr != nil {
status = "failed"
}
statusStr := status
_ = a.projectManager.Update(projectID, model.ProjectUpdate{Status: &statusStr})
}()
return nil
}
// GetBatchProgress returns the current batch processing progress.
func (a *App) GetBatchProgress() (*model.BatchProgress, error) {
if a.batchExecutor == nil {
return &model.BatchProgress{Status: "idle"}, nil
}
return a.batchExecutor.GetProgress(), nil
}
// ListProjects returns all projects sorted by creation time descending.
func (a *App) ListProjects() ([]model.Project, error) {
if a.projectManager == nil {
return nil, fmt.Errorf("project manager is not initialized")
}
return a.projectManager.List()
}
// GetProject returns a single project by ID.
func (a *App) GetProject(id string) (*model.Project, error) {
if a.projectManager == nil {
return nil, fmt.Errorf("project manager is not initialized")
}
return a.projectManager.Get(id)
}
// UpdateProjectCode updates the Python code for a project.
func (a *App) UpdateProjectCode(id string, code string) error {
if a.projectManager == nil {
return fmt.Errorf("project manager is not initialized")
}
return a.projectManager.Update(id, model.ProjectUpdate{Code: &code})
}
// DeleteProject removes a project by ID.
func (a *App) DeleteProject(id string) error {
if a.projectManager == nil {
return fmt.Errorf("project manager is not initialized")
}
return a.projectManager.Delete(id)
}
// RerunProject starts batch processing using an existing project's code.
func (a *App) RerunProject(id string, inputDir string, outputDir string, outputFileName string) error {
return a.RunBatch(id, inputDir, outputDir, outputFileName)
}
// BrowseLogFile opens a file picker for log files, reads the first N lines
// (configured by SampleLines setting, default 5), and returns the sample text
// along with a project name derived from the file name (without extension).
func (a *App) BrowseLogFile() (*model.LogFileSample, error) {
filePath, err := wailsRuntime.OpenFileDialog(a.ctx, wailsRuntime.OpenDialogOptions{
Title: "选择日志文件",
Filters: []wailsRuntime.FileFilter{
{DisplayName: "日志文件", Pattern: "*.log;*.txt;*.csv;*.json;*.xml"},
{DisplayName: "所有文件", Pattern: "*.*"},
},
})
if err != nil {
return nil, err
}
if filePath == "" {
return nil, nil // user cancelled
}
// Determine sample lines from settings
sampleLines := 5
if settings, err := a.settingsManager.Load(); err == nil && settings.SampleLines > 0 {
sampleLines = settings.SampleLines
}
// Read first N lines
f, err := os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("无法打开文件: %w", err)
}
defer f.Close()
var lines []string
scanner := bufio.NewScanner(f)
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024) // support long log lines up to 1MB
for scanner.Scan() {
lines = append(lines, scanner.Text())
if len(lines) >= sampleLines {
break
}
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("读取文件失败: %w", err)
}
baseName := filepath.Base(filePath)
ext := filepath.Ext(baseName)
projectName := strings.TrimSuffix(baseName, ext)
return &model.LogFileSample{
FileName: baseName,
ProjectName: projectName,
SampleText: strings.Join(lines, "\n"),
}, nil
}
// SelectDirectory opens a native directory picker dialog and returns the selected path.
func (a *App) SelectDirectory(title string) (string, error) {
if title == "" {
title = "选择目录"
}
dir, err := wailsRuntime.OpenDirectoryDialog(a.ctx, wailsRuntime.OpenDialogOptions{
Title: title,
})
if err != nil {
return "", err
}
return dir, nil
}
// OpenDirectory opens the given directory in the system file explorer.
func (a *App) OpenDirectory(dir string) error {
if dir == "" {
return fmt.Errorf("directory path must not be empty")
}
absDir, err := filepath.Abs(dir)
if err != nil {
return fmt.Errorf("invalid directory path: %w", err)
}
return openFileExplorer(absDir)
}
// GetSettings returns the current application settings.
func (a *App) GetSettings() (*model.Settings, error) {
return a.settingsManager.Load()
}
// SaveSettings saves settings and reinitializes LLM-dependent components.
func (a *App) SaveSettings(settings model.Settings) error {
if err := a.settingsManager.Save(settings); err != nil {
return fmt.Errorf("failed to save settings: %w", err)
}
// Reinitialize Python environment manager with new uv path
uvPath := settings.UvPath
if uvPath == "" {
uvPath = "uv"
}
envPath := filepath.Join(a.configDir, "pyenv")
newEnvMgr := pyenv.NewPythonEnvManager(uvPath, envPath)
a.mu.Lock()
a.envManager = newEnvMgr
// Reset pyenv status so it can be re-initialized
a.pyenvReady = false
a.pyenvError = ""
a.mu.Unlock()
// Re-initialize Python environment in background
go func() {
if err := newEnvMgr.EnsureEnv(a.ctx); err != nil {
a.mu.Lock()
a.pyenvError = err.Error()
a.mu.Unlock()
fmt.Printf("warning: python env re-init failed: %v\n", err)
} else {
a.mu.Lock()
a.pyenvReady = true
a.mu.Unlock()
}
}()
// Reinitialize LLM components if configured
if settings.LLM.BaseURL != "" && settings.LLM.APIKey != "" && settings.LLM.ModelName != "" {
if err := a.initLLMComponents(settings.LLM); err != nil {
return fmt.Errorf("failed to reinitialize LLM components: %w", err)
}
}
return nil
}
// EnsurePythonEnv ensures the Python virtual environment is set up.
func (a *App) EnsurePythonEnv() error {
if a.envManager == nil {
return fmt.Errorf("Python environment manager is not initialized. Please check settings")
}
return a.envManager.EnsureEnv(a.ctx)
}
// GetEnvStatus returns the current Python environment status.
func (a *App) GetEnvStatus() (*pyenv.EnvStatus, error) {
if a.envManager == nil {
return nil, fmt.Errorf("Python environment manager is not initialized")
}
return a.envManager.GetStatus(), nil
}
// GetPythonEnvReady returns whether the auto-init has completed and any error message.
func (a *App) GetPythonEnvReady() map[string]interface{} {
a.mu.Lock()
defer a.mu.Unlock()
return map[string]interface{}{
"ready": a.pyenvReady,
"error": a.pyenvError,
}
}
// IsLLMConfigured returns true if LLM settings are filled and the client is initialized.
func (a *App) IsLLMConfigured() bool {
return a.llmClient != nil
}
// GetShowWizard returns whether the startup wizard should be shown.
func (a *App) GetShowWizard() bool {
settings, err := a.settingsManager.Load()
if err != nil {
return true
}
if settings.ShowWizard == nil {
return true
}
return *settings.ShowWizard
}
// SetShowWizard updates the show_wizard setting.
func (a *App) SetShowWizard(show bool) error {
settings, err := a.settingsManager.Load()
if err != nil {
return err
}
settings.ShowWizard = &show
return a.settingsManager.Save(*settings)
}
// TestLLM tests the LLM connection by sending a simple message and checking for a response.
func (a *App) TestLLM() error {
settings, err := a.settingsManager.Load()
if err != nil {
return fmt.Errorf("无法加载设置: %w", err)
}
if settings.LLM.BaseURL == "" || settings.LLM.APIKey == "" || settings.LLM.ModelName == "" {
return fmt.Errorf("LLM 配置不完整,请填写 Base URL、API Key 和 Model Name")
}
// Create a temporary client to test
testClient, err := agent.NewLLMClient(settings.LLM)
if err != nil {
return fmt.Errorf("创建 LLM 客户端失败: %w", err)
}
// Use a timeout context for the test request
testCtx, cancel := context.WithTimeout(a.ctx, 30*time.Second)
defer cancel()
_, err = testClient.Chat(testCtx, []model.Message{
{Role: "user", Content: "请回复 OK"},
})
if err != nil {
return fmt.Errorf("LLM 连接测试失败: %w", err)
}
// Test passed — initialize components with this config
if initErr := a.initLLMComponents(settings.LLM); initErr != nil {
return fmt.Errorf("初始化 LLM 组件失败: %w", initErr)
}
return nil
}
// llmRepairerAdapter adapts LLMClient to the executor.LLMRepairer interface.
type llmRepairerAdapter struct {
llmClient *agent.LLMClient
}
func (a *llmRepairerAdapter) RepairCode(ctx context.Context, code string, errorMsg string) (string, error) {
messages := []model.Message{
{
Role: "system",
Content: "You are an expert Python developer. Fix the runtime error in the given Python code. " +
"Return the complete fixed Python code inside a single ```python code block. " +
"Do not explain the changes, just return the corrected code.",
},
{
Role: "user",
Content: fmt.Sprintf("The following Python code encountered a runtime error:\n\n```python\n%s\n```\n\n"+
"Error message:\n```\n%s\n```\n\nPlease fix the error and return the complete corrected code.",
code, errorMsg),
},
}
resp, err := a.llmClient.Chat(ctx, messages)
if err != nil {
return "", err
}
return extractRepairCode(resp), nil
}
// extractRepairCode extracts Python code from an LLM repair response.
func extractRepairCode(response string) string {
// Try ```python block
if start := strings.Index(response, "```python"); start >= 0 {
rest := response[start:]
if nlIdx := strings.Index(rest, "\n"); nlIdx >= 0 {
content := rest[nlIdx+1:]
if endIdx := strings.Index(content, "```"); endIdx >= 0 {
return strings.TrimSpace(content[:endIdx])
}
}
}
// Try any ``` block
if start := strings.Index(response, "```"); start >= 0 {
rest := response[start:]
if nlIdx := strings.Index(rest, "\n"); nlIdx >= 0 {
content := rest[nlIdx+1:]
if endIdx := strings.Index(content, "```"); endIdx >= 0 {
return strings.TrimSpace(content[:endIdx])
}
}
}
return response
}