chore: initial commit (sources + docs, ignore build artifacts and secrets)
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user