feat: genasset 离线素材管线(oMLX SVG 生成 + svg2png 打包 + 对比点评回填)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-08-13 16:04:54 +08:00
co-authored by Claude Opus 4.7
parent 950ca9194b
commit 0a8fd6c8a2
7 changed files with 1262 additions and 0 deletions
+435
View File
@@ -0,0 +1,435 @@
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/glog"
"36wisdom/biz/consts"
)
// genasset:离线生成视觉素材(SVG→PNG 打包入客户端)与对比点评(回填数据库)。
// 用法:
//
// go run ./cmd/genasset --list # 打印生成清单
// go run ./cmd/genasset --only=comments # 只生成对比点评
// go run ./cmd/genasset --only=visuals --strategy=1 # 只生成第 1 计素材
// go run ./cmd/genasset --force # 覆盖已有产物
//
// 幂等:产物存在即跳过(--force 覆盖);点评按 feedback_pros 是否为空判定。
var (
ctx = context.Background()
genClient omlx
assetDir = "workspace/genasset" // SVG 源 + 转换缓存
staticDir = "ui-src/static/generated" // 打包入客户端的 PNG
)
type genFlags struct {
listOnly bool
only string // comments | visuals
strategy int // 0 = 全部计策
force bool
}
type visualItem struct {
kind string // scene | char | prop | strategy
key string
id int64
name string
desc string
}
func main() {
fl := parseFlags()
genClient = omlx{
endpoint: g.Cfg().MustGet(ctx, "genasset.endpoint", "http://127.0.0.1:18080").String(),
apiKey: g.Cfg().MustGet(ctx, "genasset.api_key", "wenwu901").String(),
model: g.Cfg().MustGet(ctx, "genasset.model", "Qwen3.5-9B-MLX-4bit").String(),
timeout: g.Cfg().MustGet(ctx, "genasset.timeout", 600).Int(),
maxTokens: g.Cfg().MustGet(ctx, "genasset.max_tokens", 24576).Int(),
retries: g.Cfg().MustGet(ctx, "genasset.retries", 3).Int(),
}
if fl.listOnly {
printInventory(fl)
return
}
if fl.only == "" || fl.only == "comments" {
genComments(fl)
}
if fl.only == "" || fl.only == "visuals" {
genVisuals(fl)
}
fmt.Println("[genasset] 完成")
}
func parseFlags() genFlags {
var fl genFlags
flag.BoolVar(&fl.listOnly, "list", false, "只打印生成清单")
flag.StringVar(&fl.only, "only", "", "comments | visuals,默认都生成")
flag.IntVar(&fl.strategy, "strategy", 0, "只处理指定计策(strategy_id),0=全部")
flag.BoolVar(&fl.force, "force", false, "覆盖已存在产物")
flag.Parse()
return fl
}
// ---------- 清单 ----------
func printInventory(fl genFlags) {
comments := pendingComments(fl)
fmt.Printf("待生成点评选项:%d(按节点分组 %d 个请求)\n", len(comments), distinctCount(comments))
for _, it := range visualInventory(fl) {
fmt.Printf("%-9s %-16s %s\n", it.kind, it.key, it.name)
}
}
// ---------- 对比点评 ----------
func pendingComments(fl genFlags) []gdb.Record {
m := g.DB().Model(consts.TableNodeOption).Where("feedback_pros IS NULL OR feedback_pros = ''")
if fl.strategy > 0 {
_, nodeIDs := strategyScope(fl)
m = m.WhereIn("node_id", nodeIDs)
}
rows, err := m.Ctx(ctx).All()
if err != nil {
glog.Fatal(ctx, err)
}
return rows
}
func distinctCount(rows []gdb.Record) int {
seen := map[int64]bool{}
for _, r := range rows {
seen[r["node_id"].Int64()] = true
}
return len(seen)
}
// strategyScope 返回某计策涉及的 level_ids 与 node_idsstrategy<=0 时返回 nil
func strategyScope(fl genFlags) ([]int64, []int64) {
if fl.strategy <= 0 {
return nil, nil
}
var levelIDs []int64
arr, err := g.DB().Model(consts.TableLevel).Where("strategy_id", fl.strategy).Array("id")
if err != nil {
glog.Fatal(ctx, err)
}
for _, v := range arr {
levelIDs = append(levelIDs, v.Int64())
}
var nodeIDs []int64
arr, err = g.DB().Model(consts.TableSceneNode).WhereIn("level_id", levelIDs).Array("id")
if err != nil {
glog.Fatal(ctx, err)
}
for _, v := range arr {
nodeIDs = append(nodeIDs, v.Int64())
}
return levelIDs, nodeIDs
}
func genComments(fl genFlags) {
rows := pendingComments(fl)
if len(rows) == 0 {
fmt.Println("[comments] 无待生成选项(全部已回填)")
return
}
byNode := map[int64][]gdb.Record{}
var nodeIDs []int64
for _, r := range rows {
nid := r["node_id"].Int64()
if _, ok := byNode[nid]; !ok {
nodeIDs = append(nodeIDs, nid)
}
byNode[nid] = append(byNode[nid], r)
}
nodeRows, err := g.DB().Model(consts.TableSceneNode).WhereIn("id", nodeIDs).Ctx(ctx).All()
if err != nil {
glog.Fatal(ctx, err)
}
nodes := map[int64]gdb.Record{}
for _, r := range nodeRows {
nodes[r["id"].Int64()] = r
}
sem := make(chan struct{}, 2) // 本地 9B 模型慢,并发 2 防排队打爆
var wg sync.WaitGroup
for _, nid := range nodeIDs {
wg.Add(1)
sem <- struct{}{}
go func(nid int64) {
defer wg.Done()
defer func() { <-sem }()
if err := genNodeComments(nid, nodes[nid], byNode[nid]); err != nil {
glog.Errorf(ctx, "[comments] node %d 失败: %v", nid, err)
}
}(nid)
}
wg.Wait()
}
func genNodeComments(nid int64, node gdb.Record, opts []gdb.Record) error {
var lines []string
for _, o := range opts {
lines = append(lines, fmt.Sprintf("%d. %s", o["id"].Int64(), o["text"].String()))
}
out, err := genClient.chat(ctx, commentSystem, fmt.Sprintf(commentUser, node["content"].String(), strings.Join(lines, "\n")))
if err != nil {
return err
}
var resp struct {
Comments []struct {
OptionID int64 `json:"option_id"`
Pros string `json:"pros"`
Cons string `json:"cons"`
} `json:"comments"`
}
if err := json.Unmarshal([]byte(out), &resp); err != nil {
return fmt.Errorf("点评 JSON 解析失败: %v(输出: %s", err, truncate(out, 200))
}
if len(resp.Comments) == 0 {
return fmt.Errorf("点评空结果: %s", truncate(out, 200))
}
for _, c := range resp.Comments {
// 注意:Update 的变参是 dataAndWhere,直接 Update(ctx) 会把 ctx 当作 Data 覆盖掉,必须用 .Ctx(ctx)
_, err := g.DB().Model(consts.TableNodeOption).
Ctx(ctx).
Data(g.Map{"feedback_pros": c.Pros, "feedback_cons": c.Cons}).
Where("id", c.OptionID).Update()
if err != nil {
return err
}
}
fmt.Printf("[comments] ok node %d%d 个选项)\n", nid, len(resp.Comments))
return nil
}
// ---------- 视觉素材 ----------
func visualInventory(fl genFlags) []visualItem {
var items []visualItem
items = append(items, elementItems("scene", sceneIDs(fl))...)
items = append(items, elementItems("char", charIDs(fl))...)
items = append(items, elementItems("prop", propIDs(fl))...)
// 计策卡图标
m := g.DB().Model(consts.TableStrategy)
if fl.strategy > 0 {
m = m.Where("id", fl.strategy)
}
rows, err := m.Ctx(ctx).All()
if err != nil {
glog.Fatal(ctx, err)
}
for _, r := range rows {
items = append(items, visualItem{
kind: "strategy", key: fmt.Sprintf("strategy_%d", r["id"].Int64()),
id: r["id"].Int64(), name: r["name"].String(), desc: r["meaning"].String(),
})
}
return items
}
func sceneIDs(fl genFlags) []int64 {
m := g.DB().Model(consts.TableLevel).Where("scene_id > 0").Fields("DISTINCT scene_id")
if fl.strategy > 0 {
m = m.Where("strategy_id", fl.strategy)
}
var ids []int64
arr, err := m.Array("scene_id")
if err != nil {
glog.Fatal(ctx, err)
}
for _, v := range arr {
ids = append(ids, v.Int64())
}
return ids
}
func charIDs(fl genFlags) []int64 {
m := g.DB().Model(consts.TableSceneNode).Where("character_id > 0").Fields("DISTINCT character_id")
if fl.strategy > 0 {
levelIDs, _ := strategyScope(fl)
m = m.WhereIn("level_id", levelIDs)
}
var ids []int64
arr, err := m.Array("character_id")
if err != nil {
glog.Fatal(ctx, err)
}
for _, v := range arr {
ids = append(ids, v.Int64())
}
return ids
}
func propIDs(fl genFlags) []int64 {
m := g.DB().Model(consts.TableNodeOption).Where("prop_id > 0").Fields("DISTINCT prop_id")
if fl.strategy > 0 {
_, nodeIDs := strategyScope(fl)
m = m.WhereIn("node_id", nodeIDs)
}
var ids []int64
arr, err := m.Array("prop_id")
if err != nil {
glog.Fatal(ctx, err)
}
for _, v := range arr {
ids = append(ids, v.Int64())
}
return ids
}
func elementItems(kind string, ids []int64) []visualItem {
if len(ids) == 0 {
return nil
}
rows, err := g.DB().Model(consts.TableElement).WhereIn("id", ids).Ctx(ctx).All()
if err != nil {
glog.Fatal(ctx, err)
}
var items []visualItem
for _, r := range rows {
items = append(items, visualItem{
kind: kind, key: fmt.Sprintf("%s_%d", kind, r["id"].Int64()),
id: r["id"].Int64(), name: r["name"].String(), desc: r["description"].String(),
})
}
return items
}
func genVisuals(fl genFlags) {
items := visualInventory(fl)
if len(items) == 0 {
fmt.Println("[visuals] 清单为空")
return
}
fmt.Printf("[visuals] 共 %d 个素材\n", len(items))
sem := make(chan struct{}, 2)
var wg sync.WaitGroup
for _, it := range items {
wg.Add(1)
sem <- struct{}{}
go func(it visualItem) {
defer wg.Done()
defer func() { <-sem }()
if err := genOneVisual(fl, it); err != nil {
glog.Errorf(ctx, "[visuals] %s %s 失败: %v", it.kind, it.key, err)
}
}(it)
}
wg.Wait()
// 统一转 PNG(每个 kind 一个目录)
for _, kind := range []string{"scene", "char", "prop", "strategy"} {
src := filepath.Join(assetDir, kind)
if _, err := os.Stat(src); err != nil {
continue
}
if err := svg2png(src, filepath.Join(staticDir, kind)); err != nil {
glog.Errorf(ctx, "[visuals] svg2png %s: %v", kind, err)
}
}
// 回填 image 路径(前端按 /static/... 直接引用);PNG 未生成成功的不回填(避免前端 404)
for _, it := range items {
if !fileExists(filepath.Join(staticDir, it.kind, it.key+".png")) {
glog.Errorf(ctx, "[visuals] 回填 %s 跳过:PNG 不存在", it.key)
continue
}
img := pngPath(it)
var err error
if it.kind == "strategy" {
_, err = g.DB().Model(consts.TableStrategy).Ctx(ctx).Data(g.Map{"icon": img}).Where("id", it.id).Update()
} else {
_, err = g.DB().Model(consts.TableElement).Ctx(ctx).Data(g.Map{"image": img}).Where("id", it.id).Update()
}
if err != nil {
glog.Errorf(ctx, "[visuals] 回填 %s 失败: %v", it.key, err)
}
}
}
func genOneVisual(fl genFlags, it visualItem) error {
svgPath := filepath.Join(assetDir, it.kind, it.key+".svg")
if !fl.force && fileExists(svgPath) {
return nil // 幂等:已有产物跳过
}
var user string
switch it.kind {
case "scene":
user = fmt.Sprintf("场景名称:%s\n场景说明:%s\n%s", it.name, it.desc, styleRule)
case "char":
user = fmt.Sprintf("角色名称:%s\n角色说明:%s\n%s", it.name, it.desc, styleRule)
case "prop":
user = fmt.Sprintf("道具名称:%s\n道具说明:%s\n%s", it.name, it.desc, styleRule)
case "strategy":
user = fmt.Sprintf("计策名称:%s\n计策释义:%s\n%s", it.name, it.desc, styleRule)
}
out, err := genClient.chat(ctx, "", user)
if err != nil {
return err
}
svg, err := extractSVG(out)
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(svgPath), 0o755); err != nil {
return err
}
if err := os.WriteFile(svgPath, []byte(svg), 0o644); err != nil {
return err
}
fmt.Printf("[visuals] ok %s/%s\n", it.kind, it.key)
return nil
}
func extractSVG(out string) (string, error) {
var box struct {
SVG string `json:"svg"`
}
if err := json.Unmarshal([]byte(out), &box); err != nil {
return "", fmt.Errorf("模型输出不是 JSON: %v(输出: %s", err, truncate(out, 200))
}
if !strings.Contains(box.SVG, "<svg") {
return "", fmt.Errorf("输出缺少 svg 内容: %s", truncate(out, 200))
}
return box.SVG, nil
}
func pngPath(it visualItem) string {
return "/static/generated/" + it.kind + "/" + it.key + ".png"
}
// svg2png 调 ui-src/scripts/svg2png.mjssharp)批量转换
func svg2png(src, dst string) error {
cmd := exec.CommandContext(ctx, "node", "ui-src/scripts/svg2png.mjs", src, dst)
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("%s: %v", strings.TrimSpace(string(out)), err)
}
fmt.Print(string(out))
return nil
}
func fileExists(p string) bool {
_, err := os.Stat(p)
return err == nil
}
func truncate(s string, n int) string {
r := []rune(s)
if len(r) <= n {
return s
}
return string(r[:n]) + "…"
}
+97
View File
@@ -0,0 +1,97 @@
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
// omlx 调本机 oMLXOpenAI 兼容 /v1/chat/completions)。
// 注意:Qwen3.5 思考链默认开启且思考文本混入 content,必须传 chat_template_kwargs.enable_thinking=false。
type omlx struct {
endpoint string
apiKey string
model string
timeout int // 秒
maxTokens int
retries int
}
type chatRequest struct {
Model string `json:"model"`
Messages []chatMessage `json:"messages"`
Stream bool `json:"stream"`
MaxTokens int `json:"max_tokens"`
ChatTemplateKwargs map[string]any `json:"chat_template_kwargs"`
}
type chatMessage struct {
Role string `json:"role"`
Content string `json:"content"`
}
type chatResponse struct {
Choices []struct {
Message struct {
Content string `json:"content"`
} `json:"message"`
} `json:"choices"`
}
func (m *omlx) chat(ctx context.Context, system, user string) (string, error) {
payload := chatRequest{
Model: m.model,
Messages: []chatMessage{
{Role: "system", Content: system},
{Role: "user", Content: user},
},
Stream: false,
MaxTokens: m.maxTokens,
ChatTemplateKwargs: map[string]any{"enable_thinking": false},
}
buf, err := json.Marshal(payload)
if err != nil {
return "", err
}
var lastErr error
for i := 0; i <= m.retries; i++ {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, m.endpoint+"/v1/chat/completions", bytes.NewReader(buf))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+m.apiKey)
resp, err := (&http.Client{Timeout: time.Duration(m.timeout) * time.Second}).Do(req)
if err != nil {
lastErr = fmt.Errorf("oMLX 请求失败: %w", err)
time.Sleep(2 * time.Second)
continue
}
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
lastErr = err
continue
}
if resp.StatusCode >= 400 {
lastErr = fmt.Errorf("oMLX %d: %s", resp.StatusCode, string(body))
time.Sleep(2 * time.Second)
continue
}
var cr chatResponse
if err := json.Unmarshal(body, &cr); err != nil {
lastErr = fmt.Errorf("oMLX 响应解析失败: %w", err)
continue
}
if len(cr.Choices) == 0 {
lastErr = fmt.Errorf("oMLX 空响应")
continue
}
return cr.Choices[0].Message.Content, nil
}
return "", lastErr
}
+35
View File
@@ -0,0 +1,35 @@
package main
// styleRule 素材风格约束:简笔卡通、无文字、暖色系、儿童向
const styleRule = `要求:简笔卡通风格,粗线条,暖色调(米黄/橙/青绿),画面中禁止出现任何文字,圆润可爱,适合 4-8 岁儿童。
只输出 JSON,格式:{"svg": "<svg ...></svg>"}。SVG viewBox="0 0 400 300"(道具图标 0 0 200 200),只用基础形状(rect/circle/path/ellipse/line),不使用外部图片与字体,不用 <text>。`
const scenePrompt = `你是儿童绘本插画师。为儿童学习游戏绘制一张场景插画。
场景名称:%s
场景说明:%s
%s`
const charPrompt = `你是儿童绘本插画师。绘制一个 Q 版儿童角色立绘(全身像,头身比 1:2,圆脸大眼)。
角色名称:%s
角色说明:%s
%s`
const propPrompt = `你是儿童绘本插画师。绘制一个道具图标(单一主体居中,底部浅色圆角底衬)。
道具名称:%s
道具说明:%s
%s`
const cardPrompt = `你是儿童绘本插画师。绘制一个圆形徽章图标(占满画布,无文字)。
计策名称:%s
计策释义:%s
%s`
const commentSystem = `你是给 4-8 岁儿童写闯关游戏点评的老师。语言儿童化、口语化、温和,禁止责备,多用"因为/所以"。每条 20-40 字。
只输出 JSON{"comments": [{"option_id": 1, "pros": "选它的好处", "cons": "它的不足,或错过的更好选择"}, ...]},与输入的选项一一对应。`
const commentUser = `情境:%s
选项:
%s
请为每个选项写「好处 pros」(选它的收获、为什么好)与「坏处 cons」(它的不足;若它是最佳选择可留空或写小小的代价)。`
+8
View File
@@ -15,3 +15,11 @@ pool:
auth:
secret: "36wisdom-dev-secret-change-in-prod"
expire: 604800
# 离线素材生成(cmd/genasset,仅开发机运行;本机 oMLXQwen3.5-9B-MLX-4bit
genasset:
endpoint: http://127.0.0.1:18080
api_key: wenwu901 # 本机 oMLX 鉴权 keyrag-local model_config 表)
model: Qwen3.5-9B-MLX-4bit
timeout: 600 # 本地 ~5 tok/s,单张 SVG 1-3 分钟
max_tokens: 24576
retries: 3
+668
View File
@@ -15,6 +15,7 @@
},
"devDependencies": {
"@dcloudio/vite-plugin-uni": "3.0.0-5020420260813001",
"sharp": "^0.35.3",
"vite": "5.2.8"
}
},
@@ -2308,6 +2309,17 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
"integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
@@ -2676,6 +2688,581 @@
"node": ">=12"
}
},
"node_modules/@img/colour": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
"integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.35.3",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz",
"integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.3.2"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.35.3",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz",
"integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.3.2"
}
},
"node_modules/@img/sharp-freebsd-wasm32": {
"version": "0.35.3",
"resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz",
"integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==",
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"freebsd"
],
"dependencies": {
"@img/sharp-wasm32": "0.35.3"
},
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz",
"integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz",
"integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==",
"cpu": [
"x64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz",
"integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==",
"cpu": [
"arm"
],
"dev": true,
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz",
"integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-ppc64": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz",
"integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==",
"cpu": [
"ppc64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-riscv64": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz",
"integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==",
"cpu": [
"riscv64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz",
"integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==",
"cpu": [
"s390x"
],
"dev": true,
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz",
"integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz",
"integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz",
"integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.35.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz",
"integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==",
"cpu": [
"arm"
],
"dev": true,
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.3.2"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.35.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz",
"integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.3.2"
}
},
"node_modules/@img/sharp-linux-ppc64": {
"version": "0.35.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz",
"integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==",
"cpu": [
"ppc64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-ppc64": "1.3.2"
}
},
"node_modules/@img/sharp-linux-riscv64": {
"version": "0.35.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz",
"integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==",
"cpu": [
"riscv64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-riscv64": "1.3.2"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.35.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz",
"integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==",
"cpu": [
"s390x"
],
"dev": true,
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.3.2"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.35.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz",
"integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.3.2"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.35.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz",
"integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.3.2"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.35.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz",
"integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.3.2"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.35.3",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz",
"integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==",
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
"@emnapi/runtime": "^1.11.1"
},
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-webcontainers-wasm32": {
"version": "0.35.3",
"resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz",
"integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==",
"cpu": [
"wasm32"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"@img/sharp-wasm32": "0.35.3"
},
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-arm64": {
"version": "0.35.3",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz",
"integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.35.3",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz",
"integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==",
"cpu": [
"ia32"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.35.3",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz",
"integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@intlify/core-base": {
"version": "9.1.9",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.1.9.tgz",
@@ -4288,6 +4875,16 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -6114,6 +6711,69 @@
"dev": true,
"license": "ISC"
},
"node_modules/sharp": {
"version": "0.35.3",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz",
"integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@img/colour": "^1.1.0",
"detect-libc": "^2.1.2",
"semver": "^7.8.5"
},
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "0.35.3",
"@img/sharp-darwin-x64": "0.35.3",
"@img/sharp-freebsd-wasm32": "0.35.3",
"@img/sharp-libvips-darwin-arm64": "1.3.2",
"@img/sharp-libvips-darwin-x64": "1.3.2",
"@img/sharp-libvips-linux-arm": "1.3.2",
"@img/sharp-libvips-linux-arm64": "1.3.2",
"@img/sharp-libvips-linux-ppc64": "1.3.2",
"@img/sharp-libvips-linux-riscv64": "1.3.2",
"@img/sharp-libvips-linux-s390x": "1.3.2",
"@img/sharp-libvips-linux-x64": "1.3.2",
"@img/sharp-libvips-linuxmusl-arm64": "1.3.2",
"@img/sharp-libvips-linuxmusl-x64": "1.3.2",
"@img/sharp-linux-arm": "0.35.3",
"@img/sharp-linux-arm64": "0.35.3",
"@img/sharp-linux-ppc64": "0.35.3",
"@img/sharp-linux-riscv64": "0.35.3",
"@img/sharp-linux-s390x": "0.35.3",
"@img/sharp-linux-x64": "0.35.3",
"@img/sharp-linuxmusl-arm64": "0.35.3",
"@img/sharp-linuxmusl-x64": "0.35.3",
"@img/sharp-webcontainers-wasm32": "0.35.3",
"@img/sharp-win32-arm64": "0.35.3",
"@img/sharp-win32-ia32": "0.35.3",
"@img/sharp-win32-x64": "0.35.3"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/sharp/node_modules/semver": {
"version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/side-channel": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
@@ -6353,6 +7013,14 @@
"node": ">=0.6"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD",
"optional": true
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+1
View File
@@ -14,6 +14,7 @@
},
"devDependencies": {
"@dcloudio/vite-plugin-uni": "3.0.0-5020420260813001",
"sharp": "^0.35.3",
"vite": "5.2.8"
}
}
+18
View File
@@ -0,0 +1,18 @@
// 用法: node svg2png.mjs <srcDir> <outDir> [width]
// 把 srcDir 下所有 .svg 转为 outDir 下同名 .png(默认 800 宽,2x 高清)
import sharp from 'sharp'
import { readdirSync, mkdirSync } from 'fs'
import { join } from 'path'
const [srcDir, outDir, widthArg] = process.argv.slice(2)
const width = Number(widthArg) || 800
mkdirSync(outDir, { recursive: true })
let n = 0
for (const f of readdirSync(srcDir)) {
if (!f.endsWith('.svg')) continue
const out = join(outDir, f.replace(/\.svg$/, '.png'))
await sharp(join(srcDir, f)).resize({ width }).png().toFile(out)
n++
console.log('png:', out)
}
console.log(`done: ${n} files`)