Skip to content

Commit c435f78

Browse files
authored
Merge pull request #594 from Yumiue/html_progress
feat:web端接入代码回退按键和功能,修复代码回退功能,还有优化界面
2 parents de9e4b7 + 379f4ba commit c435f78

54 files changed

Lines changed: 4815 additions & 318 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

internal/checkpoint/checkpoint_manager.go

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,29 @@ type SQLiteCheckpointStore struct {
5858
ownsDB bool // true 表示本实例打开的连接,Close 时需释放
5959
}
6060

61+
type workspaceFingerprintRow struct {
62+
WorkspaceKey string
63+
FingerprintPayload string
64+
UpdatedAtMS int64
65+
}
66+
67+
// WorkspaceCheckpointState 保存工作区当前权威代码基线与文件指纹。
68+
type WorkspaceCheckpointState struct {
69+
WorkspaceKey string
70+
CurrentCheckpointID string
71+
FingerprintPayload string
72+
UpdatedAt time.Time
73+
}
74+
75+
// RunCheckpointBaseline 保存单次 run 的权威回退基线,供异步 diff 查询跨 run 结束后读取。
76+
type RunCheckpointBaseline struct {
77+
SessionID string
78+
RunID string
79+
CheckpointID string
80+
Drifted bool
81+
UpdatedAt time.Time
82+
}
83+
6184
// NewSQLiteCheckpointStore 创建 checkpoint 存储实例。
6285
// dbPath 为 session.db 文件路径,可通过 session.DatabasePath 获取。
6386
func NewSQLiteCheckpointStore(dbPath string) *SQLiteCheckpointStore {
@@ -114,6 +137,184 @@ func (s *SQLiteCheckpointStore) ensureDB(ctx context.Context) (*sql.DB, error) {
114137
return db, nil
115138
}
116139

140+
// SaveWorkspaceFingerprint 把 workspace 最新指纹保存到 SQLite,用于跨会话/重启后的 drift 检测。
141+
func (s *SQLiteCheckpointStore) SaveWorkspaceFingerprint(
142+
ctx context.Context,
143+
workspaceKey string,
144+
fingerprintPayload string,
145+
updatedAt time.Time,
146+
) error {
147+
if err := ctx.Err(); err != nil {
148+
return err
149+
}
150+
db, err := s.ensureDB(ctx)
151+
if err != nil {
152+
return err
153+
}
154+
if err := ensureWorkspaceFingerprintTable(ctx, db); err != nil {
155+
return err
156+
}
157+
_, err = db.ExecContext(ctx, `
158+
INSERT INTO workspace_fingerprints(workspace_key, fingerprint_payload, updated_at_ms)
159+
VALUES(?, ?, ?)
160+
ON CONFLICT(workspace_key) DO UPDATE SET
161+
fingerprint_payload=excluded.fingerprint_payload,
162+
updated_at_ms=excluded.updated_at_ms
163+
`, workspaceKey, fingerprintPayload, toUnixMillis(updatedAt))
164+
if err != nil {
165+
return fmt.Errorf("checkpoint: save workspace fingerprint %s: %w", workspaceKey, err)
166+
}
167+
return nil
168+
}
169+
170+
// LoadWorkspaceFingerprint 从 SQLite 读取 workspace 最近指纹,不存在时 ok=false。
171+
func (s *SQLiteCheckpointStore) LoadWorkspaceFingerprint(
172+
ctx context.Context,
173+
workspaceKey string,
174+
) (string, bool, error) {
175+
if err := ctx.Err(); err != nil {
176+
return "", false, err
177+
}
178+
db, err := s.ensureDB(ctx)
179+
if err != nil {
180+
return "", false, err
181+
}
182+
if err := ensureWorkspaceFingerprintTable(ctx, db); err != nil {
183+
return "", false, err
184+
}
185+
var row workspaceFingerprintRow
186+
err = db.QueryRowContext(ctx, `
187+
SELECT workspace_key, fingerprint_payload, updated_at_ms
188+
FROM workspace_fingerprints
189+
WHERE workspace_key = ?
190+
`, workspaceKey).Scan(&row.WorkspaceKey, &row.FingerprintPayload, &row.UpdatedAtMS)
191+
if err != nil {
192+
if errors.Is(err, sql.ErrNoRows) {
193+
return "", false, nil
194+
}
195+
return "", false, fmt.Errorf("checkpoint: load workspace fingerprint %s: %w", workspaceKey, err)
196+
}
197+
return row.FingerprintPayload, true, nil
198+
}
199+
200+
// SaveWorkspaceCheckpointState 持久化工作区当前代码基线与文件指纹。
201+
func (s *SQLiteCheckpointStore) SaveWorkspaceCheckpointState(ctx context.Context, state WorkspaceCheckpointState) error {
202+
if err := ctx.Err(); err != nil {
203+
return err
204+
}
205+
if state.WorkspaceKey == "" {
206+
return fmt.Errorf("checkpoint: workspace key required")
207+
}
208+
db, err := s.ensureDB(ctx)
209+
if err != nil {
210+
return err
211+
}
212+
if err := ensureWorkspaceCheckpointStateTable(ctx, db); err != nil {
213+
return err
214+
}
215+
_, err = db.ExecContext(ctx, `
216+
INSERT INTO workspace_checkpoint_states(workspace_key, current_checkpoint_id, fingerprint_payload, updated_at_ms)
217+
VALUES(?, ?, ?, ?)
218+
ON CONFLICT(workspace_key) DO UPDATE SET
219+
current_checkpoint_id=excluded.current_checkpoint_id,
220+
fingerprint_payload=excluded.fingerprint_payload,
221+
updated_at_ms=excluded.updated_at_ms
222+
`, state.WorkspaceKey, state.CurrentCheckpointID, state.FingerprintPayload, toUnixMillis(state.UpdatedAt))
223+
if err != nil {
224+
return fmt.Errorf("checkpoint: save workspace checkpoint state %s: %w", state.WorkspaceKey, err)
225+
}
226+
return nil
227+
}
228+
229+
// LoadWorkspaceCheckpointState 读取工作区当前代码基线与文件指纹,不存在时 ok=false。
230+
func (s *SQLiteCheckpointStore) LoadWorkspaceCheckpointState(ctx context.Context, workspaceKey string) (WorkspaceCheckpointState, bool, error) {
231+
if err := ctx.Err(); err != nil {
232+
return WorkspaceCheckpointState{}, false, err
233+
}
234+
db, err := s.ensureDB(ctx)
235+
if err != nil {
236+
return WorkspaceCheckpointState{}, false, err
237+
}
238+
if err := ensureWorkspaceCheckpointStateTable(ctx, db); err != nil {
239+
return WorkspaceCheckpointState{}, false, err
240+
}
241+
var state WorkspaceCheckpointState
242+
var updatedAtMS int64
243+
err = db.QueryRowContext(ctx, `
244+
SELECT workspace_key, current_checkpoint_id, fingerprint_payload, updated_at_ms
245+
FROM workspace_checkpoint_states
246+
WHERE workspace_key = ?
247+
`, workspaceKey).Scan(&state.WorkspaceKey, &state.CurrentCheckpointID, &state.FingerprintPayload, &updatedAtMS)
248+
if err != nil {
249+
if errors.Is(err, sql.ErrNoRows) {
250+
return WorkspaceCheckpointState{}, false, nil
251+
}
252+
return WorkspaceCheckpointState{}, false, fmt.Errorf("checkpoint: load workspace checkpoint state %s: %w", workspaceKey, err)
253+
}
254+
state.UpdatedAt = fromUnixMillis(updatedAtMS)
255+
return state, true, nil
256+
}
257+
258+
// SaveRunCheckpointBaseline 持久化单次 run 的权威回退基线。
259+
func (s *SQLiteCheckpointStore) SaveRunCheckpointBaseline(ctx context.Context, baseline RunCheckpointBaseline) error {
260+
if err := ctx.Err(); err != nil {
261+
return err
262+
}
263+
if baseline.SessionID == "" || baseline.RunID == "" {
264+
return fmt.Errorf("checkpoint: session_id and run_id required for run baseline")
265+
}
266+
db, err := s.ensureDB(ctx)
267+
if err != nil {
268+
return err
269+
}
270+
if err := ensureRunCheckpointBaselineTable(ctx, db); err != nil {
271+
return err
272+
}
273+
_, err = db.ExecContext(ctx, `
274+
INSERT INTO run_checkpoint_baselines(session_id, run_id, checkpoint_id, drifted, updated_at_ms)
275+
VALUES(?, ?, ?, ?, ?)
276+
ON CONFLICT(session_id, run_id) DO UPDATE SET
277+
checkpoint_id=excluded.checkpoint_id,
278+
drifted=excluded.drifted,
279+
updated_at_ms=excluded.updated_at_ms
280+
`, baseline.SessionID, baseline.RunID, baseline.CheckpointID, boolToInt(baseline.Drifted), toUnixMillis(baseline.UpdatedAt))
281+
if err != nil {
282+
return fmt.Errorf("checkpoint: save run baseline %s/%s: %w", baseline.SessionID, baseline.RunID, err)
283+
}
284+
return nil
285+
}
286+
287+
// LoadRunCheckpointBaseline 读取单次 run 的权威回退基线,不存在时 ok=false。
288+
func (s *SQLiteCheckpointStore) LoadRunCheckpointBaseline(ctx context.Context, sessionID, runID string) (RunCheckpointBaseline, bool, error) {
289+
if err := ctx.Err(); err != nil {
290+
return RunCheckpointBaseline{}, false, err
291+
}
292+
db, err := s.ensureDB(ctx)
293+
if err != nil {
294+
return RunCheckpointBaseline{}, false, err
295+
}
296+
if err := ensureRunCheckpointBaselineTable(ctx, db); err != nil {
297+
return RunCheckpointBaseline{}, false, err
298+
}
299+
var baseline RunCheckpointBaseline
300+
var drifted int
301+
var updatedAtMS int64
302+
err = db.QueryRowContext(ctx, `
303+
SELECT session_id, run_id, checkpoint_id, drifted, updated_at_ms
304+
FROM run_checkpoint_baselines
305+
WHERE session_id = ? AND run_id = ?
306+
`, sessionID, runID).Scan(&baseline.SessionID, &baseline.RunID, &baseline.CheckpointID, &drifted, &updatedAtMS)
307+
if err != nil {
308+
if errors.Is(err, sql.ErrNoRows) {
309+
return RunCheckpointBaseline{}, false, nil
310+
}
311+
return RunCheckpointBaseline{}, false, fmt.Errorf("checkpoint: load run baseline %s/%s: %w", sessionID, runID, err)
312+
}
313+
baseline.Drifted = drifted != 0
314+
baseline.UpdatedAt = fromUnixMillis(updatedAtMS)
315+
return baseline, true, nil
316+
}
317+
117318
// CreateCheckpoint 在单一事务内写入 checkpoint record + session checkpoint。
118319
// 事务内完成 record INSERT → session_cp INSERT → record UPDATE(设置 session_checkpoint_ref + status=available)。
119320
func (s *SQLiteCheckpointStore) CreateCheckpoint(ctx context.Context, input CreateCheckpointInput) (session.CheckpointRecord, error) {
@@ -663,3 +864,52 @@ func rollbackTx(tx *sql.Tx) {
663864
_ = tx.Rollback()
664865
}
665866
}
867+
868+
func ensureWorkspaceFingerprintTable(ctx context.Context, db *sql.DB) error {
869+
if db == nil {
870+
return fmt.Errorf("checkpoint: workspace fingerprint db is nil")
871+
}
872+
if _, err := db.ExecContext(ctx, `
873+
CREATE TABLE IF NOT EXISTS workspace_fingerprints (
874+
workspace_key TEXT PRIMARY KEY,
875+
fingerprint_payload TEXT NOT NULL,
876+
updated_at_ms INTEGER NOT NULL
877+
)`); err != nil {
878+
return fmt.Errorf("checkpoint: ensure workspace_fingerprints table: %w", err)
879+
}
880+
return nil
881+
}
882+
883+
func ensureWorkspaceCheckpointStateTable(ctx context.Context, db *sql.DB) error {
884+
if db == nil {
885+
return fmt.Errorf("checkpoint: workspace checkpoint state db is nil")
886+
}
887+
if _, err := db.ExecContext(ctx, `
888+
CREATE TABLE IF NOT EXISTS workspace_checkpoint_states (
889+
workspace_key TEXT PRIMARY KEY,
890+
current_checkpoint_id TEXT NOT NULL,
891+
fingerprint_payload TEXT NOT NULL,
892+
updated_at_ms INTEGER NOT NULL
893+
)`); err != nil {
894+
return fmt.Errorf("checkpoint: ensure workspace_checkpoint_states table: %w", err)
895+
}
896+
return nil
897+
}
898+
899+
func ensureRunCheckpointBaselineTable(ctx context.Context, db *sql.DB) error {
900+
if db == nil {
901+
return fmt.Errorf("checkpoint: run checkpoint baseline db is nil")
902+
}
903+
if _, err := db.ExecContext(ctx, `
904+
CREATE TABLE IF NOT EXISTS run_checkpoint_baselines (
905+
session_id TEXT NOT NULL,
906+
run_id TEXT NOT NULL,
907+
checkpoint_id TEXT NOT NULL,
908+
drifted INTEGER NOT NULL,
909+
updated_at_ms INTEGER NOT NULL,
910+
PRIMARY KEY(session_id, run_id)
911+
)`); err != nil {
912+
return fmt.Errorf("checkpoint: ensure run_checkpoint_baselines table: %w", err)
913+
}
914+
return nil
915+
}

internal/checkpoint/checkpoint_manager_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,3 +588,81 @@ func TestNewSQLiteCheckpointStoreWithNilDBClose(t *testing.T) {
588588
t.Fatalf("Close(nil db) error = %v", err)
589589
}
590590
}
591+
592+
func TestSQLiteCheckpointStoreWorkspaceFingerprintPersistence(t *testing.T) {
593+
fixture := newCheckpointStoreFixture(t)
594+
db, err := fixture.sessionStore.InitDB(context.Background())
595+
if err != nil {
596+
t.Fatalf("InitDB() error = %v", err)
597+
}
598+
shared := NewSQLiteCheckpointStoreWithDB(db)
599+
600+
workspaceKey := session.WorkspacePathKey(fixture.workspaceRoot)
601+
payload := `{"a.txt":{"size":1,"mod_time":"2026-01-01T00:00:00Z","head_hash":"abc"}}`
602+
if err := shared.SaveWorkspaceFingerprint(context.Background(), workspaceKey, payload, time.Now()); err != nil {
603+
t.Fatalf("SaveWorkspaceFingerprint() error = %v", err)
604+
}
605+
606+
got, ok, err := shared.LoadWorkspaceFingerprint(context.Background(), workspaceKey)
607+
if err != nil {
608+
t.Fatalf("LoadWorkspaceFingerprint() error = %v", err)
609+
}
610+
if !ok {
611+
t.Fatal("expected persisted fingerprint to exist")
612+
}
613+
if got != payload {
614+
t.Fatalf("fingerprint payload = %q, want %q", got, payload)
615+
}
616+
617+
missing, ok, err := shared.LoadWorkspaceFingerprint(context.Background(), "workspace/missing")
618+
if err != nil {
619+
t.Fatalf("LoadWorkspaceFingerprint(missing) error = %v", err)
620+
}
621+
if ok || missing != "" {
622+
t.Fatalf("expected missing fingerprint, got ok=%v payload=%q", ok, missing)
623+
}
624+
}
625+
626+
func TestSQLiteCheckpointStoreWorkspaceStateAndRunBaselinePersistence(t *testing.T) {
627+
fixture := newCheckpointStoreFixture(t)
628+
db, err := fixture.sessionStore.InitDB(context.Background())
629+
if err != nil {
630+
t.Fatalf("InitDB() error = %v", err)
631+
}
632+
shared := NewSQLiteCheckpointStoreWithDB(db)
633+
634+
workspaceKey := session.WorkspacePathKey(fixture.workspaceRoot)
635+
payload := `{"tracked.txt":{"size":7}}`
636+
if err := shared.SaveWorkspaceCheckpointState(context.Background(), WorkspaceCheckpointState{
637+
WorkspaceKey: workspaceKey,
638+
CurrentCheckpointID: "cp-current",
639+
FingerprintPayload: payload,
640+
UpdatedAt: time.Now(),
641+
}); err != nil {
642+
t.Fatalf("SaveWorkspaceCheckpointState() error = %v", err)
643+
}
644+
state, ok, err := shared.LoadWorkspaceCheckpointState(context.Background(), workspaceKey)
645+
if err != nil {
646+
t.Fatalf("LoadWorkspaceCheckpointState() error = %v", err)
647+
}
648+
if !ok || state.CurrentCheckpointID != "cp-current" || state.FingerprintPayload != payload {
649+
t.Fatalf("workspace state = %#v ok=%v", state, ok)
650+
}
651+
652+
if err := shared.SaveRunCheckpointBaseline(context.Background(), RunCheckpointBaseline{
653+
SessionID: "session-1",
654+
RunID: "run-1",
655+
CheckpointID: "cp-current",
656+
Drifted: true,
657+
UpdatedAt: time.Now(),
658+
}); err != nil {
659+
t.Fatalf("SaveRunCheckpointBaseline() error = %v", err)
660+
}
661+
baseline, ok, err := shared.LoadRunCheckpointBaseline(context.Background(), "session-1", "run-1")
662+
if err != nil {
663+
t.Fatalf("LoadRunCheckpointBaseline() error = %v", err)
664+
}
665+
if !ok || baseline.CheckpointID != "cp-current" || !baseline.Drifted {
666+
t.Fatalf("run baseline = %#v ok=%v", baseline, ok)
667+
}
668+
}

0 commit comments

Comments
 (0)