fix(web): use raw token for Pico media proxy and refresh chat attachment UI (#2618)
This commit is contained in:
parent
77b0c43392
commit
f367a9c010
3 changed files with 183 additions and 109 deletions
|
|
@ -181,17 +181,16 @@ func (h *Handler) handlePicoMediaProxy() http.HandlerFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
gateway.mu.Lock()
|
gateway.mu.Lock()
|
||||||
uiToken := gateway.picoToken
|
picoToken := gateway.picoToken
|
||||||
gateway.mu.Unlock()
|
gateway.mu.Unlock()
|
||||||
|
|
||||||
token := tokenPrefix + uiToken
|
if picoToken == "" {
|
||||||
if token == "" {
|
|
||||||
logger.Warnf("Missing Pico token for media proxy")
|
logger.Warnf("Missing Pico token for media proxy")
|
||||||
http.Error(w, "Invalid Pico token", http.StatusForbidden)
|
http.Error(w, "Invalid Pico token", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
h.createPicoHTTPProxy(token).ServeHTTP(w, r)
|
h.createPicoHTTPProxy(picoToken).ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -668,7 +668,7 @@ func TestCreatePicoHTTPProxyInjectsGatewayAuth(t *testing.T) {
|
||||||
t.Fatalf("SaveConfig() error = %v", err)
|
t.Fatalf("SaveConfig() error = %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
proxy := h.createPicoHTTPProxy(tokenPrefix + "test-token" + "ui-token")
|
proxy := h.createPicoHTTPProxy("ui-token")
|
||||||
var capturedPath string
|
var capturedPath string
|
||||||
var capturedAuth string
|
var capturedAuth string
|
||||||
proxy.Transport = roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
proxy.Transport = roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||||||
|
|
@ -692,12 +692,83 @@ func TestCreatePicoHTTPProxyInjectsGatewayAuth(t *testing.T) {
|
||||||
if capturedPath != "/pico/media/attachment-1" {
|
if capturedPath != "/pico/media/attachment-1" {
|
||||||
t.Fatalf("capturedPath = %q, want %q", capturedPath, "/pico/media/attachment-1")
|
t.Fatalf("capturedPath = %q, want %q", capturedPath, "/pico/media/attachment-1")
|
||||||
}
|
}
|
||||||
expected := "Bearer " + tokenPrefix + "test-token" + "ui-token"
|
expected := "Bearer ui-token"
|
||||||
if capturedAuth != expected {
|
if capturedAuth != expected {
|
||||||
t.Fatalf("Authorization = %q, want %q", capturedAuth, expected)
|
t.Fatalf("Authorization = %q, want %q", capturedAuth, expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHandlePicoMediaProxyUsesRawBearerToken(t *testing.T) {
|
||||||
|
home := t.TempDir()
|
||||||
|
t.Setenv("PICOCLAW_HOME", home)
|
||||||
|
|
||||||
|
configPath := filepath.Join(t.TempDir(), "config.json")
|
||||||
|
h := NewHandler(configPath)
|
||||||
|
handler := h.handlePicoMediaProxy()
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/pico/media/attachment-1" {
|
||||||
|
t.Fatalf("path = %q, want %q", r.URL.Path, "/pico/media/attachment-1")
|
||||||
|
}
|
||||||
|
if got := r.Header.Get("Authorization"); got != "Bearer ui-token" {
|
||||||
|
t.Fatalf("Authorization = %q, want %q", got, "Bearer ui-token")
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = io.WriteString(w, "proxied-media")
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.Gateway.Host = "127.0.0.1"
|
||||||
|
cfg.Gateway.Port = mustGatewayTestPort(t, server.URL)
|
||||||
|
bc := cfg.Channels["pico"]
|
||||||
|
bc.Enabled = true
|
||||||
|
decoded, err := bc.GetDecoded()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetDecoded() error = %v", err)
|
||||||
|
}
|
||||||
|
decoded.(*config.PicoSettings).SetToken("ui-token")
|
||||||
|
if err := config.SaveConfig(configPath, cfg); err != nil {
|
||||||
|
t.Fatalf("SaveConfig() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := startGatewayLikeProcess(t)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if cmd.Process != nil {
|
||||||
|
_ = cmd.Process.Kill()
|
||||||
|
}
|
||||||
|
_ = cmd.Wait()
|
||||||
|
})
|
||||||
|
|
||||||
|
origPidData := gateway.pidData
|
||||||
|
origPicoToken := gateway.picoToken
|
||||||
|
origCmd := gateway.cmd
|
||||||
|
t.Cleanup(func() {
|
||||||
|
gateway.mu.Lock()
|
||||||
|
gateway.pidData = origPidData
|
||||||
|
gateway.picoToken = origPicoToken
|
||||||
|
gateway.cmd = origCmd
|
||||||
|
gateway.mu.Unlock()
|
||||||
|
})
|
||||||
|
|
||||||
|
gateway.mu.Lock()
|
||||||
|
gateway.pidData = &ppid.PidFileData{PID: cmd.Process.Pid}
|
||||||
|
gateway.picoToken = "ui-token"
|
||||||
|
gateway.cmd = cmd
|
||||||
|
gateway.mu.Unlock()
|
||||||
|
|
||||||
|
req := newPicoProxyRequest(http.MethodGet, "/pico/media/attachment-1")
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
handler(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
|
||||||
|
}
|
||||||
|
if body := rec.Body.String(); body != "proxied-media" {
|
||||||
|
t.Fatalf("body = %q, want %q", body, "proxied-media")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestHandleWebSocketProxyRejectsStalePidDataAfterProcessExit(t *testing.T) {
|
func TestHandleWebSocketProxyRejectsStalePidDataAfterProcessExit(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
t.Setenv("HOME", tmpDir)
|
t.Setenv("HOME", tmpDir)
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@ export function AssistantMessage({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{(hasText || isThought) && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative overflow-hidden rounded-xl border",
|
"relative overflow-hidden rounded-xl border",
|
||||||
|
|
@ -112,53 +113,6 @@ export function AssistantMessage({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(imageAttachments.length > 0 || fileAttachments.length > 0) && (
|
|
||||||
<div
|
|
||||||
className={cn("flex flex-col gap-3", hasText ? "px-4 pb-4" : "p-4")}
|
|
||||||
>
|
|
||||||
{imageAttachments.length > 0 && (
|
|
||||||
<div className="flex flex-wrap gap-3">
|
|
||||||
{imageAttachments.map((attachment, index) => (
|
|
||||||
<a
|
|
||||||
key={`${attachment.url}-${index}`}
|
|
||||||
href={attachment.url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="overflow-hidden rounded-xl border"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={attachment.url}
|
|
||||||
alt={attachment.filename || "Attachment"}
|
|
||||||
className="max-h-72 max-w-full object-cover"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{fileAttachments.length > 0 && (
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
{fileAttachments.map((attachment, index) => (
|
|
||||||
<a
|
|
||||||
key={`${attachment.url}-${index}`}
|
|
||||||
href={attachment.url}
|
|
||||||
download={attachment.filename}
|
|
||||||
className="bg-background/70 hover:bg-background/90 flex items-center justify-between gap-3 rounded-xl border px-3 py-2 transition-colors"
|
|
||||||
>
|
|
||||||
<span className="flex min-w-0 items-center gap-2">
|
|
||||||
<IconFileText className="text-muted-foreground size-4 shrink-0" />
|
|
||||||
<span className="truncate text-sm">
|
|
||||||
{attachment.filename || "Download attachment"}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<IconDownload className="text-muted-foreground size-4 shrink-0" />
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isThought && hasText && (
|
{!isThought && hasText && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
@ -176,6 +130,56 @@ export function AssistantMessage({
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{imageAttachments.length > 0 && (
|
||||||
|
<div className="mt-1 flex flex-wrap gap-2">
|
||||||
|
{imageAttachments.map((attachment, index) => (
|
||||||
|
<a
|
||||||
|
key={`${attachment.url}-${index}`}
|
||||||
|
href={attachment.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="group/img relative overflow-hidden rounded-xl border border-border/50 bg-muted/30 shadow-sm transition-colors hover:border-border/80"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={attachment.url}
|
||||||
|
alt={attachment.filename || "Attached image"}
|
||||||
|
className="max-h-80 max-w-[280px] object-contain transition-transform duration-300 group-hover/img:scale-[1.02]"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-black/0 transition-colors group-hover/img:bg-black/10 dark:group-hover/img:bg-black/20" />
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{fileAttachments.length > 0 && (
|
||||||
|
<div className="mt-1 flex flex-wrap gap-3">
|
||||||
|
{fileAttachments.map((attachment, index) => (
|
||||||
|
<a
|
||||||
|
key={`${attachment.url}-${index}`}
|
||||||
|
href={attachment.url}
|
||||||
|
download={attachment.filename}
|
||||||
|
className="group/file flex w-fit min-w-[220px] max-w-sm items-center gap-3.5 rounded-xl border border-border/60 bg-card px-4 py-3 transition-all duration-300 hover:-translate-y-0.5 hover:border-violet-500/30 hover:shadow-sm dark:hover:border-violet-500/40"
|
||||||
|
>
|
||||||
|
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg text-violet-400 ring-1 ring-violet-500/10 dark:bg-violet-500/10 dark:text-violet-400 dark:ring-violet-500/30">
|
||||||
|
<IconFileText className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div className="flex min-w-0 flex-1 flex-col pr-1">
|
||||||
|
<span className="truncate text-[14px] font-medium leading-tight text-foreground/90 transition-colors group-hover/file:text-violet-600 dark:group-hover/file:text-violet-400">
|
||||||
|
{attachment.filename || "Download file"}
|
||||||
|
</span>
|
||||||
|
<span className="mt-1 text-[12px] font-medium text-muted-foreground/70">
|
||||||
|
{attachment.filename?.split(".").pop()?.toUpperCase() || "FILE"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted/60 text-muted-foreground/50 transition-all duration-300 group-hover/file:bg-violet-400 group-hover/file:text-white group-hover/file:shadow-sm dark:bg-muted/20 dark:group-hover/file:bg-violet-400">
|
||||||
|
<IconDownload className="h-4 w-4 transition-transform duration-300 group-hover/file:-translate-y-[1px]" />
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue