You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
99 lines
3.2 KiB
99 lines
3.2 KiB
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
|
|
}
|
|
|