feat: 提交详情能看 diff,也能丢给 AI 审这一次改动。文件检查可单独排除路径,TODO 只认大写,避免把 todo 表和模块当成待办标记。

热力图按容器宽度铺满一年,不再横向滚动。托盘右键换成可换皮肤的弹层;更新下载显示进度,退出后再由脚本拉起安装器,避免还占着 exe。
This commit is contained in:
李琦
2026-08-19 15:46:03 +08:00
parent 1694397245
commit 7c4109f687
38 changed files with 2618 additions and 129 deletions

View File

@@ -3,7 +3,6 @@ package main
// admin.go 云端管理员id=1运营后台TOTP/stepup、统计、用户/团队、发版。
import (
"bytes"
"encoding/json"
"errors"
"io"
@@ -253,38 +252,59 @@ func (a *App) AdminUploadRelease(version, channel, changelog, filePath string) (
}
defer f.Close()
var buf bytes.Buffer
w := multipart.NewWriter(&buf)
_ = w.WriteField("version", strings.TrimSpace(version))
_ = w.WriteField("channel", strings.TrimSpace(channel))
_ = w.WriteField("changelog", changelog)
part, e := w.CreateFormFile("file", filepath.Base(filePath))
if e != nil {
return nil, e
}
if _, e = io.Copy(part, f); e != nil {
return nil, errors.New("SAVE_FAILED")
}
_ = w.Close()
base := a.apiBaseURL()
if base == "" {
return nil, errors.New("SYNC_NOT_CONFIGURED")
}
req, e := http.NewRequest(http.MethodPost, base+"/api/v1/admin/releases", &buf)
pr, pw := io.Pipe()
w := multipart.NewWriter(pw)
go func() {
var copyErr error
defer func() {
_ = w.Close()
if copyErr != nil {
_ = pw.CloseWithError(copyErr)
} else {
_ = pw.Close()
}
}()
if e := w.WriteField("version", strings.TrimSpace(version)); e != nil {
copyErr = e
return
}
if e := w.WriteField("channel", strings.TrimSpace(channel)); e != nil {
copyErr = e
return
}
if e := w.WriteField("changelog", changelog); e != nil {
copyErr = e
return
}
part, e := w.CreateFormFile("file", filepath.Base(filePath))
if e != nil {
copyErr = e
return
}
if _, e = io.Copy(part, f); e != nil {
copyErr = errors.New("SAVE_FAILED")
}
}()
req, e := http.NewRequest(http.MethodPost, base+"/api/v1/admin/releases", pr)
if e != nil {
_ = pw.Close()
return nil, errors.New("SYNC_OFFLINE")
}
req.Header.Set("Content-Type", w.FormDataContentType())
tok := strings.TrimSpace(a.store.Meta("sync_access_token"))
if tok == "" {
_ = pw.Close()
return nil, errors.New("SYNC_NOT_LOGGED_IN")
}
req.Header.Set("Authorization", "Bearer "+tok)
for k, v := range headers {
req.Header.Set(k, v)
}
client := &http.Client{Timeout: 10 * time.Minute}
client := &http.Client{Timeout: 20 * time.Minute}
resp, e := client.Do(req)
if e != nil {
return nil, errors.New("SYNC_OFFLINE")