From 0a8fd6c8a25e491f53b2d23c42e7766e485b3b63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=96=8C?= <259278618@qq.com> Date: Thu, 13 Aug 2026 16:04:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20genasset=20=E7=A6=BB=E7=BA=BF=E7=B4=A0?= =?UTF-8?q?=E6=9D=90=E7=AE=A1=E7=BA=BF=EF=BC=88oMLX=20SVG=20=E7=94=9F?= =?UTF-8?q?=E6=88=90=20+=20svg2png=20=E6=89=93=E5=8C=85=20+=20=E5=AF=B9?= =?UTF-8?q?=E6=AF=94=E7=82=B9=E8=AF=84=E5=9B=9E=E5=A1=AB=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- cmd/genasset/main.go | 435 ++++++++++++++++++++++++ cmd/genasset/omlx.go | 97 ++++++ cmd/genasset/prompts.go | 35 ++ config.yml | 8 + ui-src/package-lock.json | 668 +++++++++++++++++++++++++++++++++++++ ui-src/package.json | 1 + ui-src/scripts/svg2png.mjs | 18 + 7 files changed, 1262 insertions(+) create mode 100644 cmd/genasset/main.go create mode 100644 cmd/genasset/omlx.go create mode 100644 cmd/genasset/prompts.go create mode 100644 ui-src/scripts/svg2png.mjs diff --git a/cmd/genasset/main.go b/cmd/genasset/main.go new file mode 100644 index 0000000..5f7a431 --- /dev/null +++ b/cmd/genasset/main.go @@ -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_ids(strategy<=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, "= 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 +} diff --git a/cmd/genasset/prompts.go b/cmd/genasset/prompts.go new file mode 100644 index 0000000..3afa795 --- /dev/null +++ b/cmd/genasset/prompts.go @@ -0,0 +1,35 @@ +package main + +// styleRule 素材风格约束:简笔卡通、无文字、暖色系、儿童向 +const styleRule = `要求:简笔卡通风格,粗线条,暖色调(米黄/橙/青绿),画面中禁止出现任何文字,圆润可爱,适合 4-8 岁儿童。 +只输出 JSON,格式:{"svg": ""}。SVG viewBox="0 0 400 300"(道具图标 0 0 200 200),只用基础形状(rect/circle/path/ellipse/line),不使用外部图片与字体,不用 。` + +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」(它的不足;若它是最佳选择可留空或写小小的代价)。` diff --git a/config.yml b/config.yml index 04b6cd8..10bf685 100644 --- a/config.yml +++ b/config.yml @@ -15,3 +15,11 @@ pool: auth: secret: "36wisdom-dev-secret-change-in-prod" expire: 604800 +# 离线素材生成(cmd/genasset,仅开发机运行;本机 oMLX:Qwen3.5-9B-MLX-4bit) +genasset: + endpoint: http://127.0.0.1:18080 + api_key: wenwu901 # 本机 oMLX 鉴权 key(rag-local model_config 表) + model: Qwen3.5-9B-MLX-4bit + timeout: 600 # 本地 ~5 tok/s,单张 SVG 1-3 分钟 + max_tokens: 24576 + retries: 3 diff --git a/ui-src/package-lock.json b/ui-src/package-lock.json index dc387f7..3162d97 100644 --- a/ui-src/package-lock.json +++ b/ui-src/package-lock.json @@ -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", diff --git a/ui-src/package.json b/ui-src/package.json index 0f0bf67..4142173 100644 --- a/ui-src/package.json +++ b/ui-src/package.json @@ -14,6 +14,7 @@ }, "devDependencies": { "@dcloudio/vite-plugin-uni": "3.0.0-5020420260813001", + "sharp": "^0.35.3", "vite": "5.2.8" } } diff --git a/ui-src/scripts/svg2png.mjs b/ui-src/scripts/svg2png.mjs new file mode 100644 index 0000000..031c629 --- /dev/null +++ b/ui-src/scripts/svg2png.mjs @@ -0,0 +1,18 @@ +// 用法: node svg2png.mjs [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`)