Add 'server/' from commit 'e64421295fff83acbb6d6ab3d3b27f3ef8368f00'

git-subtree-dir: server
git-subtree-mainline: c4e617ada7
git-subtree-split: e64421295f
This commit is contained in:
2026-08-04 15:02:35 +08:00
176 changed files with 13532 additions and 0 deletions
+713
View File
@@ -0,0 +1,713 @@
{
"name": "avatar-render",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "avatar-render",
"version": "1.0.0",
"dependencies": {
"gl": "^9.0.0-rc.10",
"pngjs": "^7.0.0",
"three": "0.162.0"
}
},
"node_modules/@isaacs/fs-minipass": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
"dependencies": {
"minipass": "^7.0.4"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/abbrev": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz",
"integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==",
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"dependencies": {
"file-uri-to-path": "1.0.0"
}
},
"node_modules/bit-twiddle": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bit-twiddle/-/bit-twiddle-1.0.2.tgz",
"integrity": "sha512-B9UhK0DKFZhoTFcfvAzhqsjStvGJp9vYWf3+6SNTtdSQnvIgfkHbgHrg/e4+TH71N2GDu8tpmCVoyfrL1d7ntA=="
},
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"dependencies": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/chownr": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
"engines": {
"node": ">=18"
}
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"dependencies": {
"mimic-response": "^3.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"engines": {
"node": ">=4.0.0"
}
},
"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==",
"engines": {
"node": ">=8"
}
},
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/env-paths": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
"engines": {
"node": ">=6"
}
},
"node_modules/expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
"engines": {
"node": ">=6"
}
},
"node_modules/exponential-backoff": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz",
"integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="
},
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="
},
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
},
"node_modules/github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="
},
"node_modules/gl": {
"version": "9.0.0-rc.10",
"resolved": "https://registry.npmjs.org/gl/-/gl-9.0.0-rc.10.tgz",
"integrity": "sha512-G6lYaWoan0d2d8UO0UmaSS8zqyyZwYt6q2dFVJ2hD62sRWUwkMIH+Jp7gEoQesLlo28Nzelh+GIYz1qOUa5WmQ==",
"hasInstallScript": true,
"dependencies": {
"bindings": "^1.5.0",
"bit-twiddle": "^1.0.2",
"glsl-tokenizer": "^2.1.5",
"nan": "^2.26.2",
"node-gyp": "^12.2.0",
"prebuild-install": "^7.1.3"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/glsl-tokenizer": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/glsl-tokenizer/-/glsl-tokenizer-2.1.5.tgz",
"integrity": "sha512-XSZEJ/i4dmz3Pmbnpsy3cKh7cotvFlBiZnDOwnj/05EwNp2XrhQ4XKJxT7/pDt4kp4YcpRSKz8eTV7S+mwV6MA==",
"dependencies": {
"through2": "^0.6.3"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
},
"node_modules/isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="
},
"node_modules/isexe": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz",
"integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==",
"engines": {
"node": ">=20"
}
},
"node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/minipass": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/minizlib": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
"integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
"dependencies": {
"minipass": "^7.1.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
},
"node_modules/nan": {
"version": "2.28.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.28.0.tgz",
"integrity": "sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ=="
},
"node_modules/napi-build-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="
},
"node_modules/node-abi": {
"version": "3.94.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz",
"integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==",
"dependencies": {
"semver": "^7.3.5"
},
"engines": {
"node": ">=10"
}
},
"node_modules/node-gyp": {
"version": "12.4.0",
"resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz",
"integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==",
"dependencies": {
"env-paths": "^2.2.0",
"exponential-backoff": "^3.1.1",
"graceful-fs": "^4.2.6",
"nopt": "^9.0.0",
"proc-log": "^6.0.0",
"semver": "^7.3.5",
"tar": "^7.5.4",
"tinyglobby": "^0.2.12",
"undici": "^6.25.0",
"which": "^6.0.0"
},
"bin": {
"node-gyp": "bin/node-gyp.js"
},
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/nopt": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz",
"integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==",
"dependencies": {
"abbrev": "^4.0.0"
},
"bin": {
"nopt": "bin/nopt.js"
},
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/picomatch": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pngjs": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
"engines": {
"node": ">=14.19.0"
}
},
"node_modules/prebuild-install": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
"deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
"dependencies": {
"detect-libc": "^2.0.0",
"expand-template": "^2.0.3",
"github-from-package": "0.0.0",
"minimist": "^1.2.3",
"mkdirp-classic": "^0.5.3",
"napi-build-utils": "^2.0.0",
"node-abi": "^3.3.0",
"pump": "^3.0.0",
"rc": "^1.2.7",
"simple-get": "^4.0.0",
"tar-fs": "^2.0.0",
"tunnel-agent": "^0.6.0"
},
"bin": {
"prebuild-install": "bin.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/proc-log": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz",
"integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==",
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/pump": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"dependencies": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
"minimist": "^1.2.0",
"strip-json-comments": "~2.0.1"
},
"bin": {
"rc": "cli.js"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/semver": {
"version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/simple-concat": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/simple-get": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"dependencies": {
"decompress-response": "^6.0.0",
"once": "^1.3.1",
"simple-concat": "^1.0.0"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/tar": {
"version": "7.5.22",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz",
"integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",
"chownr": "^3.0.0",
"minipass": "^7.1.2",
"minizlib": "^3.1.0",
"yallist": "^5.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/tar-fs": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz",
"integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==",
"dependencies": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.1.4"
}
},
"node_modules/tar-fs/node_modules/chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
},
"node_modules/tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/three": {
"version": "0.162.0",
"resolved": "https://registry.npmjs.org/three/-/three-0.162.0.tgz",
"integrity": "sha512-xfCYj4RnlozReCmUd+XQzj6/5OjDNHBy5nT6rVwrOKGENAvpXe2z1jL+DZYaMu4/9pNsjH/4Os/VvS9IrH7IOQ=="
},
"node_modules/through2": {
"version": "0.6.5",
"resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz",
"integrity": "sha512-RkK/CCESdTKQZHdmKICijdKKsCRVHs5KsLZ6pACAmF/1GPUQhonHSXWNERctxEp7RmvjdNbZTL5z9V7nSCXKcg==",
"dependencies": {
"readable-stream": ">=1.0.33-1 <1.1.0-0",
"xtend": ">=4.0.0 <4.1.0-0"
}
},
"node_modules/through2/node_modules/readable-stream": {
"version": "1.0.34",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
"integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.1",
"isarray": "0.0.1",
"string_decoder": "~0.10.x"
}
},
"node_modules/through2/node_modules/string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="
},
"node_modules/tinyglobby": {
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.4"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"dependencies": {
"safe-buffer": "^5.0.1"
},
"engines": {
"node": "*"
}
},
"node_modules/undici": {
"version": "6.28.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz",
"integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==",
"engines": {
"node": ">=18.17"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/which": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz",
"integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==",
"dependencies": {
"isexe": "^4.0.0"
},
"bin": {
"node-which": "bin/which.js"
},
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"engines": {
"node": ">=0.4"
}
},
"node_modules/yallist": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
"engines": {
"node": ">=18"
}
}
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"name": "avatar-render",
"version": "1.0.0",
"private": true,
"description": "服务端 3D 化身预渲染:GLB -> 36 帧旋转 PNGthree.js + headless-gl + pngjs",
"type": "module",
"dependencies": {
"gl": "^9.0.0-rc.10",
"pngjs": "^7.0.0",
"three": "0.162.0"
}
}
+115
View File
@@ -0,0 +1,115 @@
// 化身 GLB -> 36 帧旋转 PNG(绕 Y 轴 10° 步进),服务端预渲染。
// 用法: node render.js --glb <path> --out <dir> [--frames 36] [--size 256x512]
import { argv } from 'node:process';
import fs from 'node:fs';
import path from 'node:path';
import createGL from 'gl';
import { PNG } from 'pngjs';
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
function parseArgs() {
const a = {};
for (let i = 2; i < argv.length; i++) {
if (argv[i].startsWith('--')) {
const key = argv[i].slice(2);
const val = argv[i + 1] !== undefined && !argv[i + 1].startsWith('--') ? argv[++i] : true;
a[key] = val;
}
}
if (!a.glb || !a.out) {
console.error('用法: node render.js --glb <path> --out <dir> [--frames 36] [--size 256x512]');
process.exit(1);
}
a.frames = a.frames === true ? 36 : parseInt(a.frames, 10) || 36;
const [w, h] = (a.size === true ? '256x512' : String(a.size)).split('x').map(Number);
a.width = w || 256;
a.height = h || 512;
return a;
}
const args = parseArgs();
const { width, height, frames } = args;
const gl = createGL(width, height, { preserveDrawingBuffer: true });
if (!gl) {
console.error('headless-gl 初始化失败(容器内需 mesa/libglvnd');
process.exit(2);
}
const canvas = {
width,
height,
style: {},
addEventListener: () => {},
removeEventListener: () => {},
clientWidth: width,
clientHeight: height,
getContext: () => gl,
};
const renderer = new THREE.WebGLRenderer({ canvas, context: gl, antialias: false });
renderer.setClearColor(0xffffff, 1);
renderer.setSize(width, height, false);
const scene = new THREE.Scene();
scene.add(new THREE.AmbientLight(0xffffff, 1.1));
const dirLight = new THREE.DirectionalLight(0xffffff, 1.4);
dirLight.position.set(3, 6, 4);
scene.add(dirLight);
scene.add(new THREE.DirectionalLight(0xffffff, 0.5).translateY(-4).translateX(-3));
const camera = new THREE.PerspectiveCamera(35, width / height, 0.1, 100);
const loader = new GLTFLoader();
const loadGlb = () =>
new Promise((resolve, reject) => {
// Buffer 需转成 ArrayBuffer 才能触发 GLB 头解析
const bin = fs.readFileSync(args.glb);
const ab = bin.buffer.slice(bin.byteOffset, bin.byteOffset + bin.byteLength);
loader.parse(ab, '', (gltf) => resolve(gltf.scene), (err) => reject(err));
});
loadGlb()
.then((object) => {
scene.add(object);
render(object);
})
.catch((err) => {
console.error('GLB 解析失败:', err && err.message ? err.message : err);
process.exit(3);
});
function render(object) {
// 包围盒 -> 相机半径与观测高度
const box = new THREE.Box3().setFromObject(object);
const center = box.getCenter(new THREE.Vector3());
const size = box.getSize(new THREE.Vector3());
const radius = Math.max(size.x, size.z) * 1.6 + 0.6;
const lookY = center.y + size.y * 0.35;
const cameraY = center.y + size.y * 0.35;
fs.mkdirSync(args.out, { recursive: true });
const pixels = new Uint8Array(width * height * 4);
const rowSize = width * 4;
const png = new PNG({ width, height });
for (let i = 0; i < frames; i++) {
const angle = (i / frames) * Math.PI * 2;
camera.position.set(Math.sin(angle) * radius, cameraY, Math.cos(angle) * radius);
camera.lookAt(0, lookY, 0);
renderer.render(scene, camera);
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
const buf = Buffer.from(pixels.buffer);
for (let y = 0; y < height; y++) {
buf.copy(png.data, y * rowSize, (height - 1 - y) * rowSize, (height - y) * rowSize);
}
const out = path.join(args.out, `frame_${String(i).padStart(3, '0')}.png`);
fs.writeFileSync(out, PNG.sync.write(png));
}
console.log(`rendered ${frames} frames -> ${args.out}`);
process.exit(0);
}
+232
View File
@@ -0,0 +1,232 @@
package main
// 为 wenwu901 真实生成一套 AI 穿搭方案(真实调用 imagegen 与 LLM,非 mock):
// go run scripts/gen_outfit_plan/main.go
// 步骤:补衣橱(8 件单品,imagegen 生成服装图)→ 调 OutfitService.Generate →
// 轮询任务到 done → 选主方案(触发效果图异步生成)→ 等 3 张效果图完成。
// 前置:config.yml 已配置 geo.amap_key + weather.qweather_key(天气硬依赖)。
import (
"context"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
"github.com/gogf/gf/v2/frame/g"
"slogan-agent/styleagent/agent"
"slogan-agent/styleagent/consts"
"slogan-agent/styleagent/dao"
"slogan-agent/styleagent/model/dto"
"slogan-agent/styleagent/model/entity"
"slogan-agent/styleagent/service"
)
const username = "wenwu901"
type garment struct {
name string
category string
style string
color string
prompt string
}
var garments = []garment{
{"白色长袖衬衫", "上衣", "休闲", "白色", "纯白背景的白色长袖衬衫商品图,正面展示,高清,电商风格"},
{"灰色圆领T恤", "上衣", "休闲", "灰色", "纯白背景的灰色圆领T恤商品图,正面展示,高清,电商风格"},
{"深蓝夹克外套", "上衣", "外套", "深蓝", "纯白背景的深蓝色夹克外套商品图,正面展示,高清,电商风格"},
{"深灰休闲长裤", "下装", "休闲", "深灰", "纯白背景的深灰色休闲长裤商品图,正面展示,高清,电商风格"},
{"蓝色牛仔裤", "下装", "休闲", "蓝色", "纯白背景的蓝色牛仔裤商品图,正面展示,高清,电商风格"},
{"白色运动鞋", "鞋", "休闲", "白色", "纯白背景的白色运动鞋商品图,侧面展示,高清,电商风格"},
{"棕色皮鞋", "鞋", "商务", "棕色", "纯白背景的棕色皮鞋商品图,侧面展示,高清,电商风格"},
{"黑色双肩背包", "配饰", "休闲", "黑色", "纯白背景的黑色双肩背包商品图,正面展示,高清,电商风格"},
}
func main() {
ctx := context.Background()
var user entity.User
if err := g.DB().Model(consts.TableNameUser).Ctx(ctx).
Where("username", username).Scan(&user); err != nil || user.Id == 0 {
panic(fmt.Sprintf("用户 %s 不存在: %v", username, err))
}
fmt.Printf("用户: %s (id=%d)\n", username, user.Id)
if err := ensureWardrobe(ctx, user.Id); err != nil {
panic(err)
}
// 幂等:已有方案则只重试效果图(主方案 → select-main → 等 3 张 done
var mainPlan *entity.OutfitPlan
existing, err := dao.OutfitPlan.ListByUser(ctx, user.Id)
if err != nil {
panic(fmt.Sprintf("读取方案列表失败: %v", err))
}
for _, p := range existing {
if p.MainFlag == 1 {
mainPlan = p
}
}
if len(existing) == 0 {
taskId, err := service.OutfitService.Generate(ctx, user.Id, &dto.OutfitGenerateReq{
StartDate: "2026-08-01", EndDate: "2026-08-07", Location: "上海", Occasion: "通勤",
})
if err != nil {
if strings.Contains(err.Error(), "未配置") {
panic(fmt.Sprintf("%v\n请先在 config.yml 配置 geo.amap_key / weather.qweather_key 后重跑", err))
}
panic(fmt.Sprintf("发起方案生成失败: %v", err))
}
fmt.Printf("生成任务已提交: task_id=%d,轮询中...\n", taskId)
waitTaskDone(ctx, taskId, user.Id)
plans, err := dao.OutfitPlan.ListByTask(ctx, taskId)
if err != nil || len(plans) == 0 {
panic(fmt.Sprintf("任务完成但无方案: %v", err))
}
mainPlan = plans[0]
for _, p := range plans {
if p.MainFlag == 1 {
mainPlan = p
}
}
} else if mainPlan == nil {
panic("已有方案但无主方案,请先选主方案")
}
fmt.Printf("主方案: id=%d %s 评分=%d\n", mainPlan.Id, mainPlan.Title, mainPlan.Score)
if err := service.OutfitPlanService.SelectMain(ctx, user.Id, mainPlan.Id); err != nil {
panic(fmt.Sprintf("选主方案失败: %v", err))
}
waitEffects(ctx, mainPlan.Id)
all, err := dao.OutfitPlan.ListByUser(ctx, user.Id)
if err != nil {
panic(fmt.Sprintf("读取方案列表失败: %v", err))
}
fmt.Printf("完成!wenwu901 现有 %d 套方案:\n", len(all))
for _, p := range all {
fmt.Printf(" - plan %d: %s(评分 %d%s\n", p.Id, p.Title, p.Score, p.Source)
}
}
// ensureWardrobe 为指定用户补齐 8 件衣橱单品(同 Category 已有则跳过该分类),服装图用 imagegen 生成
func ensureWardrobe(ctx context.Context, userId int64) error {
existing, err := dao.WardrobeItem.ListAllByUser(ctx, userId)
if err != nil {
return err
}
have := map[string]bool{}
for _, it := range existing {
have[it.Category] = true
}
need := make([]garment, 0, len(garments))
for _, ga := range garments {
if !have[ga.category] {
need = append(need, ga)
}
}
if len(need) == 0 {
fmt.Println("衣橱 4 类已齐,跳过补衣橱")
return nil
}
client, err := agent.NewClient(g.Cfg().MustGet(ctx, "imagegen.supplier", "wanx").String())
if err != nil {
return err
}
dir := filepath.Join("workspace", fmt.Sprintf("user_%d", userId), "wardrobe")
if err := os.MkdirAll(dir, 0o755); err != nil {
return err
}
for _, ga := range need {
fmt.Printf("生成服装图: %s...\n", ga.name)
url, err := client.Generate(ctx, &agent.GenerateReq{
Prompt: ga.prompt, Seed: time.Now().UnixNano() % 1_000_000,
})
if err != nil {
return fmt.Errorf("生成 %s 服装图失败: %w", ga.name, err)
}
path := filepath.Join(dir, fmt.Sprintf("%d_%s.png", time.Now().UnixNano(), ga.name))
if err := download(url, path); err != nil {
return fmt.Errorf("保存 %s 失败: %w", ga.name, err)
}
if _, err := dao.WardrobeItem.Insert(ctx, &entity.WardrobeItem{
UserId: userId, PhotoUrl: "/" + filepath.ToSlash(path),
Name: ga.name, Category: ga.category, Season: "四季", StyleTags: ga.style,
ColorInfo: ga.color, Status: 1,
}); err != nil {
return fmt.Errorf("入库 %s 失败: %w", ga.name, err)
}
fmt.Printf("%s 完成: %s\n", ga.name, path)
}
fmt.Println("衣橱补齐完毕")
return nil
}
func waitTaskDone(ctx context.Context, taskId, userId int64) {
for i := 0; i < 30; i++ {
task, err := dao.OutfitGenTask.GetOne(ctx, taskId, userId)
if err != nil || task == nil {
panic(fmt.Sprintf("读取任务失败: %v", err))
}
switch task.Status {
case consts.TaskStatusDone:
fmt.Println("方案生成完成")
return
case consts.TaskStatusFailed:
panic(fmt.Sprintf("方案生成失败: %s", task.Error))
}
time.Sleep(10 * time.Second)
}
panic("方案生成超时(5 分钟)")
}
// waitEffects 等主方案的 3 张效果图(正面/侧面/背面)全部 done
func waitEffects(ctx context.Context, planId int64) {
for i := 0; i < 20; i++ {
images, err := dao.PlanEffectImage.ListByPlan(ctx, planId)
if err != nil {
panic(fmt.Sprintf("读取效果图列表失败: %v", err))
}
done := 0
for _, im := range images {
if im.Status == consts.EffectStatusDone {
done++
}
}
if done >= 3 {
fmt.Printf("效果图 3 张完成\n")
return
}
if i == 19 {
fmt.Printf("警告: 效果图超时(完成 %d/3),可稍后查看\n", done)
return
}
time.Sleep(15 * time.Second)
}
}
func download(url, dest string) error {
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("下载失败: http %d", resp.StatusCode)
}
out, err := os.Create(dest)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, resp.Body)
return err
}
+114
View File
@@ -0,0 +1,114 @@
package main
// 为指定用户生成一套三视角全身照(真实调用 imagegen,非 mock):
// go run scripts/gen_user_photos/main.go [username]
// 默认用户 wenwu901。已存在同视角照片时跳过;图片存 workspace/user_{id}/photos/,记录写入 slogan_user_photo。
import (
"context"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"time"
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
"github.com/gogf/gf/v2/frame/g"
"slogan-agent/styleagent/agent"
"slogan-agent/styleagent/consts"
"slogan-agent/styleagent/dao"
"slogan-agent/styleagent/model/entity"
)
const personDesc = "一位穿浅蓝色衬衫与深灰色西裤的亚洲年轻女性,干净利落的黑色短发,身材匀称"
var views = []struct {
angle string
photoT int
prompt string
}{
{angle: "front", photoT: consts.PhotoTypeFullFront, prompt: personDesc + ",全身正面照,站直面对镜头,双手自然下垂,纯白背景,高清写实,全身入镜"},
{angle: "side", photoT: consts.PhotoTypeFullSide, prompt: personDesc + ",全身侧面照,侧身站立目视前方,纯白背景,高清写实,全身入镜"},
{angle: "back", photoT: consts.PhotoTypeFullBack, prompt: personDesc + ",全身背面照,背对镜头站立,纯白背景,高清写实,全身入镜"},
}
func main() {
username := "wenwu901"
if len(os.Args) > 1 {
username = os.Args[1]
}
ctx := context.Background()
var user entity.User
if err := g.DB().Model(consts.TableNameUser).Ctx(ctx).
Where("username", username).Scan(&user); err != nil || user.Id == 0 {
panic(fmt.Sprintf("用户 %s 不存在: %v", username, err))
}
fmt.Printf("用户: %s (id=%d)\n", username, user.Id)
existing, err := dao.UserPhoto.ListByUser(ctx, user.Id, 0)
if err != nil {
panic(err)
}
have := map[int]bool{}
for _, p := range existing {
have[p.Type] = true
}
client, err := agent.NewClient(g.Cfg().MustGet(ctx, "imagegen.supplier", "wanx").String())
if err != nil {
panic(err)
}
dir := filepath.Join("workspace", fmt.Sprintf("user_%d", user.Id), "photos")
if err := os.MkdirAll(dir, 0o755); err != nil {
panic(err)
}
// 三视角用同一 seed,保证人物一致
seed := time.Now().UnixNano() % 1_000_000
for _, v := range views {
if have[v.photoT] {
fmt.Printf("视角 %s 已有照片,跳过\n", v.angle)
continue
}
fmt.Printf("生成 %s 视角...\n", v.angle)
url, err := client.Generate(ctx, &agent.GenerateReq{
Prompt: v.prompt, Angle: v.angle, Seed: seed,
})
if err != nil {
panic(fmt.Sprintf("生成 %s 失败: %v", v.angle, err))
}
path := filepath.Join(dir, fmt.Sprintf("%d_%s.png", time.Now().UnixNano(), v.angle))
if err := download(url, path); err != nil {
panic(fmt.Sprintf("保存 %s 失败: %v", v.angle, err))
}
if _, err := dao.UserPhoto.Insert(ctx, &entity.UserPhoto{
UserId: user.Id, Type: v.photoT, Url: "/" + filepath.ToSlash(path), Status: 1,
}); err != nil {
panic(fmt.Sprintf("入库 %s 失败: %v", v.angle, err))
}
fmt.Printf("%s 完成: %s\n", v.angle, path)
}
fmt.Println("照片套生成完毕")
}
func download(url, dest string) error {
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("下载失败: http %d", resp.StatusCode)
}
out, err := os.Create(dest)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, resp.Body)
return err
}
+11
View File
@@ -0,0 +1,11 @@
#!/bin/bash
# 路由快照:输出 /api.json 的全部路径(排序去重)
# 用法:bash scripts/routes.sh > /tmp/routes-before.txt
set -e
BASE="${BASE:-http://localhost:3007}"
curl -s "$BASE/api.json" | python3 -c "
import json, sys
d = json.load(sys.stdin)
for p in sorted(d.get('paths', {}).keys()):
print(p)
"
+88
View File
@@ -0,0 +1,88 @@
#!/bin/bash
# 全路径冒烟:register/login → token → 依次打全部接口,断言 HTTP 200 + code 符合预期
# 用法:bash scripts/smoke.sh
# 约定:check_code 第三参 = 允许的降级 code 列表(逗号分隔,默认只许 0)
set -e
BASE="${BASE:-http://localhost:3007}"
FAIL=0
say() { echo "[smoke] $*"; }
fail() { echo "[smoke] FAIL: $*"; FAIL=1; }
check_code() {
local name="$1" body="$2" allow="$3"
local code
code=$(echo "$body" | python3 -c "import json,sys; print(json.load(sys.stdin).get('code','?'))" 2>/dev/null || echo "?")
if [ "$code" = "0" ] || echo ",$allow," | grep -q ",$code,"; then
say "OK: $name"
else
fail "$name: unexpected code=$code (allow: $allow) body=$(echo "$body" | head -c 200)"
fi
}
# 1. 注册 + 登录拿 token
USER="smoke_$(date +%s)"
REG=$(curl -s -X POST "$BASE/user/register" -H 'Content-Type: application/json' -d "{\"account\":\"$USER\",\"password\":\"smoketest123\"}")
say "register: $(echo "$REG" | head -c 120)"
LOGIN=$(curl -s -X POST "$BASE/user/login" -H 'Content-Type: application/json' -d "{\"account\":\"$USER\",\"password\":\"smoketest123\"}")
TOKEN=$(echo "$LOGIN" | python3 -c "import json,sys; print(json.load(sys.stdin).get('data',{}).get('token',''))" 2>/dev/null)
if [ -z "$TOKEN" ]; then
fail "login: no token in $(echo "$LOGIN" | head -c 200)"
exit 1
fi
say "login OK, token len=${#TOKEN}"
AUTH="Authorization: Bearer $TOKEN"
# 2. GET 接口(第三参允许的 code:50=未开通/降级/无数据)
for item in \
"GET /user/profile 0" \
"GET /user-photo/list 0" \
"GET /wardrobe/list 0" \
"GET /body-measurement/get 0" \
"GET /avatar/get 0" \
"GET /hairstyle/list 0" \
"GET /outfit/task/status?task_id=0 50" \
"GET /outfit/plan/list 0" \
"GET /partner-store/list 0" \
"GET /member/plan/list 0" \
"GET /member/status 0" \
"GET /cps/category/list 0" \
"GET /cps/product/list?source=meituan_ota&category_code=beauty 0" \
"GET /cps/plan/recommend?plan_id=0&scene=haircut 50" \
"GET /cps/wardrobe/upgrade?item_id=0 50" \
"GET /cps/my/recent 0" ; do
set -- $item
METHOD="$1"; PATH_="$2"; ALLOW="${3:-0}"
RESP=$(curl -s -X "$METHOD" "$BASE$PATH_" -H "$AUTH")
check_code "$PATH_" "$RESP" "$ALLOW"
done
# 3. POST 接口
post_check() {
local name="$1" json="$2" allow="${3:-0}"
local resp
resp=$(curl -s -X POST "$BASE$name" -H "$AUTH" -H 'Content-Type: application/json' -d "$json")
check_code "$name" "$resp" "$allow"
}
post_check "/body-measurement/save" '{"height_cm":175,"weight_kg":65}'
post_check "/outfit/plan/review" '{"plan_id":0,"action":"fav"}' "50"
post_check "/member/order/create" '{"plan_id":1}' "50"
post_check "/ad/reward/claim" '{"ad_type":"effect_extra"}' "50"
post_check "/outfit/generate" '{"start_date":"2026-08-01","end_date":"2026-08-07","location":"上海"}' "50"
post_check "/cps/product/link" '{"product_id":0,"scene":"item_buy"}' "50,51"
post_check "/user/change-password" '{"old_password":"smoketest123","new_password":"smoketest456"}'
# 4. 裸回调(无鉴权;no_order 预期返回 fail
NOTIFY=$(curl -s -X POST "$BASE/member/order/notify" -H 'Content-Type: application/x-www-form-urlencoded' -d 'out_trade_no=nonexist&trade_no=x&amount=0&status=paid')
say "notify(no_order)=$NOTIFY"
# 5. workspace 静态文件
WS=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/workspace/nonexist.png" -H "$AUTH")
say "workspace/404: $WS"
if [ "$FAIL" = "0" ]; then
say "ALL SMOKE PASS"
else
say "SMOKE HAS FAILURES"
exit 1
fi
+238
View File
@@ -0,0 +1,238 @@
// 一次性迁移工具:把单文件 slogan.db 拆分为 4 个 SQLite 库
//
// slogan.db 主库(用户域 + 低频配置)
// slogan_plan.db 穿搭方案域
// slogan_pay.db 会员/支付域
// slogan_cps.db CPS 联盟域
//
// 用法:在 slogan-agent 目录执行 `go run ./scripts/split_db`
package main
import (
"database/sql"
"fmt"
"os"
"path/filepath"
"strings"
"time"
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
)
const mainDB = "slogan.db"
var groups = []struct {
file string
tables []string
}{
{"slogan_plan.db", []string{
"slogan_outfit_generation_task", "slogan_outfit_plan", "slogan_plan_outfit_item",
"slogan_plan_effect_image", "slogan_plan_review", "slogan_hairstyle_asset",
}},
{"slogan_pay.db", []string{
"slogan_member_plan", "slogan_user_member", "slogan_payment_order",
"slogan_pay_notify_log", "slogan_ad_reward_log",
}},
{"slogan_cps.db", []string{
"slogan_cps_category", "slogan_cps_product", "slogan_cps_click_log",
"slogan_scene_category_map",
}},
}
func main() {
dir, err := os.Getwd()
if err != nil {
fatal("getwd: %v", err)
}
mainPath := filepath.Join(dir, mainDB)
if _, err := os.Stat(mainPath); err != nil {
fatal("slogan.db 不存在(请在 slogan-agent 目录执行): %v", err)
}
backup := filepath.Join(dir, "slogan_backup_"+time.Now().Format("20060102_150405")+".db")
if err := copyFile(mainPath, backup); err != nil {
fatal("备份失败: %v", err)
}
fmt.Printf("已备份 -> %s\n", backup)
src, err := sql.Open("sqlite", mainPath)
if err != nil {
fatal("open %s: %v", mainDB, err)
}
src.SetMaxOpenConns(1)
defer src.Close()
ddl, err := loadDDL(src)
if err != nil {
fatal("读取 DDL: %v", err)
}
srcAbs, err := filepath.Abs(mainPath)
if err != nil {
fatal("abs: %v", err)
}
failed := false
for _, g := range groups {
if err := migrateGroup(dir, src, srcAbs, g.file, g.tables, ddl); err != nil {
fmt.Printf("❌ %s 迁移失败: %v\n", g.file, err)
failed = true
}
}
if failed {
os.Exit(1)
}
// 主库删除已迁出的表(连带索引),清理自增序列残留
for _, g := range groups {
for _, t := range g.tables {
if _, err := src.Exec("DROP TABLE IF EXISTS " + t); err != nil {
fatal("DROP %s: %v", t, err)
}
}
names := quoteList(g.tables)
if _, err := src.Exec("DELETE FROM sqlite_sequence WHERE name IN (" + names + ")"); err != nil {
fmt.Printf("⚠ 清理 sqlite_sequence 失败(可忽略): %v\n", err)
}
}
fmt.Printf("✅ %s 主库已清理,剩余表:\n", mainDB)
if err := listTables(src, mainDB); err != nil {
fatal("list: %v", err)
}
fmt.Println("✅ 拆分完成")
}
// migrateGroup 新建目标库文件并拷贝表 + 索引 + 校验行数
func migrateGroup(dir string, src *sql.DB, srcAbs, file string, tables []string, ddl map[string][]string) error {
dstPath := filepath.Join(dir, file)
_ = os.Remove(dstPath) // 覆盖上次失败残留
dst, err := sql.Open("sqlite", dstPath)
if err != nil {
return err
}
dst.SetMaxOpenConns(1)
defer dst.Close()
if _, err := dst.Exec(fmt.Sprintf("ATTACH DATABASE %q AS src", srcAbs)); err != nil {
return fmt.Errorf("attach: %w", err)
}
defer dst.Exec("DETACH DATABASE src")
for _, t := range tables {
createDDL, ok := firstByType(ddl[t], "table")
if !ok {
return fmt.Errorf("表 %s 未找到建表 DDL", t)
}
if _, err := dst.Exec(createDDL); err != nil {
return fmt.Errorf("create %s: %w", t, err)
}
if _, err := dst.Exec(fmt.Sprintf("INSERT INTO %s SELECT * FROM src.%s", t, t)); err != nil {
return fmt.Errorf("copy %s: %w", t, err)
}
}
for _, t := range tables {
for _, idx := range ddl[t] {
if !strings.HasPrefix(idx, "CREATE INDEX") && !strings.HasPrefix(idx, "CREATE UNIQUE INDEX") {
continue
}
if _, err := dst.Exec(idx); err != nil {
return fmt.Errorf("index %s: %w", idx, err)
}
}
}
// 校验行数
for _, t := range tables {
srcN, dstN, err := countPair(src, dst, t)
if err != nil {
return err
}
if srcN != dstN {
return fmt.Errorf("%s 行数不一致: src=%d dst=%d", t, srcN, dstN)
}
fmt.Printf("✅ %-24s %8d 行\n", t, dstN)
}
return nil
}
func loadDDL(db *sql.DB) (map[string][]string, error) {
rows, err := db.Query("SELECT type, name, sql FROM sqlite_master WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%' AND type IN ('table','index') ORDER BY type DESC")
if err != nil {
return nil, err
}
defer rows.Close()
out := map[string][]string{}
for rows.Next() {
var typ, name, sqlText string
if err := rows.Scan(&typ, &name, &sqlText); err != nil {
return nil, err
}
if strings.HasPrefix(sqlText, "CREATE TABLE") {
out[name] = append([]string{sqlText}, out[name]...) // table 放最前
} else {
out[name] = append(out[name], sqlText)
}
}
return out, rows.Err()
}
func firstByType(ddls []string, prefix string) (string, bool) {
for _, d := range ddls {
if strings.HasPrefix(d, "CREATE TABLE") {
return d, true
}
}
return "", false
}
func countPair(src, dst *sql.DB, table string) (int, int, error) {
srcN, err := count(src, table)
if err != nil {
return 0, 0, err
}
dstN, err := count(dst, table)
if err != nil {
return 0, 0, err
}
return srcN, dstN, nil
}
func count(db *sql.DB, table string) (int, error) {
var n int
err := db.QueryRow("SELECT COUNT(*) FROM " + table).Scan(&n)
return n, err
}
func listTables(db *sql.DB, file string) error {
rows, err := db.Query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name")
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
return err
}
fmt.Printf(" %s\n", name)
}
return rows.Err()
}
func quoteList(items []string) string {
q := make([]string, len(items))
for i, s := range items {
q[i] = "'" + s + "'"
}
return strings.Join(q, ",")
}
func copyFile(src, dst string) error {
data, err := os.ReadFile(src)
if err != nil {
return err
}
return os.WriteFile(dst, data, 0o644)
}
func fatal(format string, args ...any) {
fmt.Printf("❌ "+format+"\n", args...)
os.Exit(1)
}