feat(server,cli): improve attachment support across issue and comment APIs

- Add --attachment flag to `multica issue create` CLI command
- Fix CreateComment response to include linked attachments instead of empty array
- Include attachments inline in GetIssue API response (matching Jira/ClickUp pattern)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
yushen 2026-04-01 18:10:43 +08:00
parent 09376dc879
commit 9c249f0770
3 changed files with 37 additions and 2 deletions

View file

@ -123,6 +123,7 @@ func init() {
issueCreateCmd.Flags().String("parent", "", "Parent issue ID")
issueCreateCmd.Flags().String("due-date", "", "Due date (RFC3339 format)")
issueCreateCmd.Flags().String("output", "json", "Output format: table or json")
issueCreateCmd.Flags().StringSlice("attachment", nil, "File path(s) to attach (can be specified multiple times)")
// issue update
issueUpdateCmd.Flags().String("title", "", "New title")
@ -276,7 +277,13 @@ func runIssueCreate(cmd *cobra.Command, _ []string) error {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
// Use a longer timeout when attachments are present (file uploads can be slow).
timeout := 15 * time.Second
attachments, _ := cmd.Flags().GetStringSlice("attachment")
if len(attachments) > 0 {
timeout = 60 * time.Second
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
body := map[string]any{"title": title}
@ -309,6 +316,19 @@ func runIssueCreate(cmd *cobra.Command, _ []string) error {
return fmt.Errorf("create issue: %w", err)
}
// Upload attachments and link them to the newly created issue.
issueID := strVal(result, "id")
for _, filePath := range attachments {
data, readErr := os.ReadFile(filePath)
if readErr != nil {
return fmt.Errorf("read attachment %s: %w", filePath, readErr)
}
if _, uploadErr := client.UploadFile(ctx, data, filePath, issueID); uploadErr != nil {
return fmt.Errorf("upload attachment %s: %w", filePath, uploadErr)
}
fmt.Fprintf(os.Stderr, "Uploaded %s\n", filePath)
}
output, _ := cmd.Flags().GetString("output")
if output == "table" {
headers := []string{"ID", "TITLE", "STATUS", "PRIORITY"}

View file

@ -148,7 +148,9 @@ func (h *Handler) CreateComment(w http.ResponseWriter, r *http.Request) {
h.linkAttachmentsByIDs(r.Context(), comment.ID, issue.ID, req.AttachmentIDs)
}
resp := commentToResponse(comment, nil, nil)
// Fetch linked attachments so the response includes them.
groupedAtt := h.groupAttachments(r, []pgtype.UUID{comment.ID})
resp := commentToResponse(comment, nil, groupedAtt[uuidToString(comment.ID)])
slog.Info("comment created", append(logger.RequestAttrs(r), "comment_id", uuidToString(comment.ID), "issue_id", issueID)...)
h.publish(protocol.EventCommentCreated, uuidToString(issue.WorkspaceID), authorType, authorID, map[string]any{
"comment": resp,

View file

@ -36,6 +36,7 @@ type IssueResponse struct {
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Reactions []IssueReactionResponse `json:"reactions,omitempty"`
Attachments []AttachmentResponse `json:"attachments,omitempty"`
}
type agentTriggerSnapshot struct {
@ -142,6 +143,18 @@ func (h *Handler) GetIssue(w http.ResponseWriter, r *http.Request) {
}
}
// Fetch issue-level attachments.
attachments, err := h.Queries.ListAttachmentsByIssue(r.Context(), db.ListAttachmentsByIssueParams{
IssueID: issue.ID,
WorkspaceID: issue.WorkspaceID,
})
if err == nil && len(attachments) > 0 {
resp.Attachments = make([]AttachmentResponse, len(attachments))
for i, a := range attachments {
resp.Attachments[i] = h.attachmentToResponse(a)
}
}
writeJSON(w, http.StatusOK, resp)
}