feat: add ETag cache optimization, code-split WebUI, and refactor XML tool scanner

- Chat history: early 304 via Revision()/DetailRevision() to avoid full snapshot reads
- WebUI: lazy-load tab containers with Suspense fallback
- Toolstream: split tool_sieve_xml.go into tags.go and scan.go
- CI: trigger on main branch, guard cross-build to dev/main pushes only
- Docs: add DEVELOPER.md developer quick reference

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
CJACK
2026-04-27 14:37:23 +08:00
parent 1602c3a43c
commit 6959aa2982
10 changed files with 403 additions and 192 deletions

View File

@@ -16,6 +16,24 @@ func (h *Handler) getChatHistory(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusServiceUnavailable, map[string]any{"detail": "chat history store is not configured"})
return
}
ifNoneMatch := strings.TrimSpace(r.Header.Get("If-None-Match"))
if ifNoneMatch != "" {
revision, err := store.Revision()
if err != nil {
writeJSON(w, http.StatusServiceUnavailable, map[string]any{
"detail": err.Error(),
"path": store.Path(),
})
return
}
etag := chathistory.ListETag(revision)
w.Header().Set("ETag", etag)
w.Header().Set("Cache-Control", "no-cache")
if ifNoneMatch == etag {
w.WriteHeader(http.StatusNotModified)
return
}
}
snapshot, err := store.Snapshot()
if err != nil {
writeJSON(w, http.StatusServiceUnavailable, map[string]any{
@@ -27,7 +45,7 @@ func (h *Handler) getChatHistory(w http.ResponseWriter, r *http.Request) {
etag := chathistory.ListETag(snapshot.Revision)
w.Header().Set("ETag", etag)
w.Header().Set("Cache-Control", "no-cache")
if strings.TrimSpace(r.Header.Get("If-None-Match")) == etag {
if ifNoneMatch == etag {
w.WriteHeader(http.StatusNotModified)
return
}
@@ -51,6 +69,25 @@ func (h *Handler) getChatHistoryItem(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusBadRequest, map[string]any{"detail": "history id is required"})
return
}
ifNoneMatch := strings.TrimSpace(r.Header.Get("If-None-Match"))
if ifNoneMatch != "" {
revision, err := store.DetailRevision(id)
if err != nil {
status := http.StatusInternalServerError
if strings.Contains(strings.ToLower(err.Error()), "not found") {
status = http.StatusNotFound
}
writeJSON(w, status, map[string]any{"detail": err.Error()})
return
}
etag := chathistory.DetailETag(id, revision)
w.Header().Set("ETag", etag)
w.Header().Set("Cache-Control", "no-cache")
if ifNoneMatch == etag {
w.WriteHeader(http.StatusNotModified)
return
}
}
item, err := store.Get(id)
if err != nil {
status := http.StatusInternalServerError
@@ -63,7 +100,7 @@ func (h *Handler) getChatHistoryItem(w http.ResponseWriter, r *http.Request) {
etag := chathistory.DetailETag(item.ID, item.Revision)
w.Header().Set("ETag", etag)
w.Header().Set("Cache-Control", "no-cache")
if strings.TrimSpace(r.Header.Get("If-None-Match")) == etag {
if ifNoneMatch == etag {
w.WriteHeader(http.StatusNotModified)
return
}

View File

@@ -95,6 +95,15 @@ func TestGetChatHistoryAndUpdateSettings(t *testing.T) {
t.Fatalf("expected detail etag header")
}
notModifiedItemReq := httptest.NewRequest(http.MethodGet, "/chat-history/"+entry.ID, nil)
notModifiedItemReq.Header.Set("Authorization", "Bearer admin")
notModifiedItemReq.Header.Set("If-None-Match", itemRec.Header().Get("ETag"))
notModifiedItemRec := httptest.NewRecorder()
r.ServeHTTP(notModifiedItemRec, notModifiedItemReq)
if notModifiedItemRec.Code != http.StatusNotModified {
t.Fatalf("expected detail 304, got %d body=%s", notModifiedItemRec.Code, notModifiedItemRec.Body.String())
}
updateReq := httptest.NewRequest(http.MethodPut, "/chat-history/settings", bytes.NewReader([]byte(`{"limit":10}`)))
updateReq.Header.Set("Authorization", "Bearer admin")
updateRec := httptest.NewRecorder()