package controlplane import ( "database/sql" "encoding/base64" "errors" "strings" ) const ( maxTaskImages = 10 maxTaskImageBytes = 12 * 1024 * 1024 ) // TaskImageInput is a browser data URL supplied only when creating a task. // It is deliberately never copied to task output or audit fields. type TaskImageInput struct { Name string `json:"name"` DataURL string `json:"dataURL"` } type taskImage struct { Name string `json:"name"` MediaType string `json:"media_type"` DataURL string `json:"data_url"` } // TaskAttachment is the deliberately small preview returned only by the local // task-detail view. It is never used for audit records, task output, or logs. // The data URL is needed by the local WebView to render a safe thumbnail. type TaskAttachment struct { Name string `json:"name"` MediaType string `json:"mediaType"` DataURL string `json:"dataURL"` } func normalizeTaskImages(inputs []TaskImageInput) ([]taskImage, error) { if len(inputs) > maxTaskImages { return nil, errors.New("每项任务最多附加 10 张图片") } images := make([]taskImage, 0, len(inputs)) total := 0 for index, input := range inputs { parts := strings.SplitN(input.DataURL, ",", 2) if len(parts) != 2 || !strings.HasPrefix(parts[0], "data:") || !strings.HasSuffix(parts[0], ";base64") { return nil, errors.New("图片格式无效") } mediaType := strings.TrimSuffix(strings.TrimPrefix(parts[0], "data:"), ";base64") if mediaType != "image/png" && mediaType != "image/jpeg" && mediaType != "image/gif" && mediaType != "image/webp" { return nil, errors.New("只支持 PNG、JPEG、GIF 或 WebP 图片") } decoded, err := base64.StdEncoding.DecodeString(parts[1]) if err != nil || len(decoded) == 0 { return nil, errors.New("图片内容无效") } total += len(decoded) if total > maxTaskImageBytes { return nil, errors.New("一项任务的图片总大小不能超过 12 MB") } name := strings.TrimSpace(input.Name) if name == "" { name = "图片" + stringLine(index+1) } images = append(images, taskImage{Name: name[:min(len(name), 120)], MediaType: mediaType, DataURL: input.DataURL}) } return images, nil } func (c *ControlPlane) taskImages(taskID string) ([]taskImage, error) { rows, err := c.db.Query("SELECT name, media_type, data_url FROM task_images WHERE task_id = ? ORDER BY position", taskID) if err != nil { return nil, errors.New("无法读取任务图片") } defer rows.Close() images := make([]taskImage, 0) for rows.Next() { var image taskImage if err := rows.Scan(&image.Name, &image.MediaType, &image.DataURL); err != nil { return nil, errors.New("无法读取任务图片") } images = append(images, image) } if err := rows.Err(); err != nil && err != sql.ErrNoRows { return nil, errors.New("无法读取任务图片") } return images, nil } func (c *ControlPlane) taskAttachmentPreviews(taskID string) ([]TaskAttachment, error) { images, err := c.taskImages(taskID) if err != nil { return nil, err } previews := make([]TaskAttachment, 0, len(images)) for _, image := range images { previews = append(previews, TaskAttachment{Name: image.Name, MediaType: image.MediaType, DataURL: image.DataURL}) } return previews, nil }