mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-15 22:29:04 +02:00
* feat(cli): add central error translation layer (PR1) Introduce server/internal/cli/errors.go, a single user-facing error translation layer that collapses raw transport errors, HTTP status errors, and internal verb-wrapped chains into clear, localized messages. - ErrorKind classification (network timeout/DNS/refused/TLS/offline, 401/403/404/409/400+422/429/5xx, unknown) - NetworkError wraps transport errors and strips the raw URL from the user-facing message; classifyNetworkError categorizes via errors.As/Is with string fallbacks - HTTPError.Kind() maps status codes onto ErrorKind - FormatError: bilingual output (English default, auto-switch to Chinese on a zh LC_ALL/LC_MESSAGES/LANG locale), validation errors surface the server message; --debug / MULTICA_DEBUG appends the full raw chain - ExitCodeFor: tiered exit codes (network=2, auth=3, 404=4, validation=5, other=1) - client.go: default HTTP timeout 15s -> 30s, overridable via MULTICA_HTTP_TIMEOUT; wrap every transport Do() error as *NetworkError - main.go: route errors through FormatError + ExitCodeFor, add persistent --debug flag Unit tests cover every ErrorKind, classification, language detection, exit codes, server-message extraction, and timeout parsing. Refs MUL-3104. PR1 of 3; PR2/PR3 (status-code copy refinement and per-command customization) follow separately. Co-authored-by: multica-agent <github@multica.ai> * fix(cli): address review — unify command timeouts and classify all helper errors Must-fix 1: command-level contexts no longer truncate MULTICA_HTTP_TIMEOUT. Added cli.APITimeout/AtLeastAPITimeout/APIContext (budget = transport timeout + small grace, honoring MULTICA_HTTP_TIMEOUT) and replaced the hardcoded 15s context.WithTimeout in every API command (14 files, 92 sites) with cli.APIContext. The issue-create/comment path now uses APITimeout() with a 60s floor for attachment uploads. Must-fix 2: all API helpers now return *HTTPError on status >= 400. Added a shared newHTTPError(method, path, resp) and routed GetJSON, GetJSONWithHeaders, PostJSON, PutJSON, PatchJSON, DeleteJSON, DeleteJSONWithBody, UploadFile, UploadFileWithURL, DownloadFile (and HealthCheck) through it, so issue update/status/metadata (PUT), comment list (GetJSONWithHeaders), project/label/ comment delete (DELETE) and agent/workspace/autopilot update (PUT/PATCH) all get HTTPError.Kind() classification, friendly copy, and the tiered exit code instead of the raw string + exit 1. Tests: new errors_integration_test.go drives the real helpers against a fake server and asserts FormatError copy + ExitCodeFor for 401/403/404/422/500 across all 10 helpers, plus a slow-server test proving the command context does not cancel before the transport timeout. Updated the UploadFileWithURL assertion to check for *HTTPError. Refs MUL-3104, PR #3892. Co-authored-by: multica-agent <github@multica.ai> * fix(cli): make remaining fixed-timeout API commands honor MULTICA_HTTP_TIMEOUT Closes out the timeout work: the last API command paths still used a hardcoded context deadline that capped MULTICA_HTTP_TIMEOUT. Converted them to cli.AtLeastAPITimeout(<original floor>) so the env override scales them up while preserving each original lower bound: - cmd_autopilot.go autopilot trigger 30s -> AtLeastAPITimeout(30s) - cmd_attachment.go attachment download 60s -> AtLeastAPITimeout(60s) - cmd_agent.go avatar upload 60s -> AtLeastAPITimeout(60s) - cmd_skill.go skill import / search 60s -> AtLeastAPITimeout(60s) - cmd_runtime.go runtime update 150s -> AtLeastAPITimeout(150s) - cmd_login.go workspace-creation poll 10s -> AtLeastAPITimeout(10s) The login poll keeps a short 10s floor to stay responsive within its 5-minute loop, but it is NOT a silent exception: AtLeastAPITimeout means it still scales with MULTICA_HTTP_TIMEOUT. Documented in code and covered by a new subtest in TestAPITimeoutRespectsEnv. Refs MUL-3104, PR #3892. Co-authored-by: multica-agent <github@multica.ai> * style(cli): gofmt cmd_attachment.go to unblock backend CI Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai>
93 lines
2.3 KiB
Go
93 lines
2.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/multica-ai/multica/server/internal/cli"
|
|
)
|
|
|
|
var attachmentCmd = &cobra.Command{
|
|
Use: "attachment",
|
|
Short: "Work with attachments",
|
|
}
|
|
|
|
var attachmentDownloadCmd = &cobra.Command{
|
|
Use: "download <attachment-id>",
|
|
Short: "Download an attachment to a local file",
|
|
Long: "Download an attachment by its ID to a local file.",
|
|
Example: ` # Download an image attachment to the current directory
|
|
$ multica attachment download abc123
|
|
|
|
# Download to a specific directory
|
|
$ multica attachment download abc123 -o /tmp/images`,
|
|
Args: exactArgs(1),
|
|
RunE: runAttachmentDownload,
|
|
}
|
|
|
|
func init() {
|
|
attachmentCmd.AddCommand(attachmentDownloadCmd)
|
|
|
|
attachmentDownloadCmd.Flags().StringP("output-dir", "o", ".", "Directory to save the downloaded file")
|
|
}
|
|
|
|
func runAttachmentDownload(cmd *cobra.Command, args []string) error {
|
|
client, err := newAPIClient(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), cli.AtLeastAPITimeout(60*time.Second))
|
|
defer cancel()
|
|
|
|
// Fetch attachment metadata (includes signed download_url).
|
|
var att map[string]any
|
|
if err := client.GetJSON(ctx, "/api/attachments/"+args[0], &att); err != nil {
|
|
return fmt.Errorf("get attachment: %w", err)
|
|
}
|
|
|
|
downloadURL := strVal(att, "download_url")
|
|
if downloadURL == "" {
|
|
return fmt.Errorf("attachment has no download URL")
|
|
}
|
|
|
|
filename := filepath.Base(strVal(att, "filename"))
|
|
if filename == "" || filename == "." {
|
|
filename = args[0]
|
|
}
|
|
|
|
// Download the file content.
|
|
data, err := client.DownloadFile(ctx, downloadURL)
|
|
if err != nil {
|
|
return fmt.Errorf("download file: %w", err)
|
|
}
|
|
|
|
// Write to the output directory.
|
|
outputDir, _ := cmd.Flags().GetString("output-dir")
|
|
destPath := filepath.Join(outputDir, filename)
|
|
|
|
if err := os.WriteFile(destPath, data, 0o644); err != nil {
|
|
return fmt.Errorf("write file: %w", err)
|
|
}
|
|
|
|
// Print the absolute path so agents can reference the file.
|
|
abs, err := filepath.Abs(destPath)
|
|
if err != nil {
|
|
abs = destPath
|
|
}
|
|
fmt.Fprintln(os.Stderr, "Downloaded:", abs)
|
|
|
|
// Also print as JSON for --output json compatibility.
|
|
return cli.PrintJSON(os.Stdout, map[string]any{
|
|
"id": strVal(att, "id"),
|
|
"filename": filename,
|
|
"path": abs,
|
|
"size": strVal(att, "size_bytes"),
|
|
})
|
|
}
|