Commit 53886bde authored by xuchentao's avatar xuchentao

feat: import Word articles with embedded images

parent ec14e5cc
Pipeline #435 passed with stage
in 16 seconds
......@@ -44,7 +44,22 @@ CMS_DATA_DIR=/root/jiayun/shared/cms-data
CMS_BUILD_LOCK=/root/jiayun/shared/site-build.lock
```
生产环境必须修改示例密码和密钥。`CMS_API_KEY` 为可选的 Bearer API 访问密钥。
生产环境必须修改示例密码和密钥。`CMS_API_KEY` 为可选的 Bearer API 访问密钥。Word 导入接口只接受 API Key,不接受后台 Cookie 会话:
```http
POST /api/cms/imports/word
Authorization: Bearer <CMS_API_KEY>
Content-Type: application/json
{
"fileName": "文章.docx",
"category": "入行指南",
"title": "可选标题",
"dataUrl": "data:application/vnd.openxmlformats-officedocument.wordprocessingml.document;base64,..."
}
```
接口仅支持 20MB 以内的 `.docx`,会提取 PNG/JPG/WEBP/GIF 图片,将首个一级标题作为标题(请求中的 `title` 优先),并创建未发布草稿。响应包含 `slug``publishStatus``imageCount` 和 Mammoth 转换警告 `warnings`
## SEO 与部署
......
......@@ -12,10 +12,16 @@
"@astrojs/sitemap": "^3.7.3",
"astro": "5.18.2",
"gray-matter": "^4.0.3",
"marked": "^14.1.4"
"mammoth": "^1.12.0",
"marked": "^14.1.4",
"turndown": "^7.2.4",
"turndown-plugin-gfm": "^1.0.2"
},
"devDependencies": {
"@astrojs/check": "^0.9.6",
"@types/turndown": "^5.0.6",
"docx": "^9.7.1",
"tsx": "^4.23.1",
"typescript": "^5.9.3"
},
"engines": {
......@@ -828,9 +834,6 @@
"cpu": [
"arm"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
......@@ -847,9 +850,6 @@
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
......@@ -866,9 +866,6 @@
"cpu": [
"ppc64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
......@@ -885,9 +882,6 @@
"cpu": [
"riscv64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
......@@ -904,9 +898,6 @@
"cpu": [
"s390x"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
......@@ -923,9 +914,6 @@
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
......@@ -942,9 +930,6 @@
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
......@@ -961,9 +946,6 @@
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
......@@ -980,9 +962,6 @@
"cpu": [
"arm"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
......@@ -1005,9 +984,6 @@
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
......@@ -1030,9 +1006,6 @@
"cpu": [
"ppc64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
......@@ -1055,9 +1028,6 @@
"cpu": [
"riscv64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
......@@ -1080,9 +1050,6 @@
"cpu": [
"s390x"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
......@@ -1105,9 +1072,6 @@
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
......@@ -1130,9 +1094,6 @@
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "Apache-2.0",
"optional": true,
"os": [
......@@ -1155,9 +1116,6 @@
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "Apache-2.0",
"optional": true,
"os": [
......@@ -1255,6 +1213,12 @@
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"license": "MIT"
},
"node_modules/@mixmark-io/domino": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz",
"integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==",
"license": "BSD-2-Clause"
},
"node_modules/@oslojs/encoding": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz",
......@@ -1374,9 +1338,6 @@
"cpu": [
"arm"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
......@@ -1390,9 +1351,6 @@
"cpu": [
"arm"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
......@@ -1406,9 +1364,6 @@
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
......@@ -1422,9 +1377,6 @@
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
......@@ -1438,9 +1390,6 @@
"cpu": [
"loong64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
......@@ -1454,9 +1403,6 @@
"cpu": [
"loong64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
......@@ -1470,9 +1416,6 @@
"cpu": [
"ppc64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
......@@ -1486,9 +1429,6 @@
"cpu": [
"ppc64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
......@@ -1502,9 +1442,6 @@
"cpu": [
"riscv64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
......@@ -1518,9 +1455,6 @@
"cpu": [
"riscv64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
......@@ -1534,9 +1468,6 @@
"cpu": [
"s390x"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
......@@ -1550,9 +1481,6 @@
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
......@@ -1566,9 +1494,6 @@
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
......@@ -1786,6 +1711,13 @@
"@types/node": "*"
}
},
"node_modules/@types/turndown": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/@types/turndown/-/turndown-5.0.6.tgz",
"integrity": "sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
......@@ -1903,6 +1835,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/@xmldom/xmldom": {
"version": "0.8.13",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
"integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/acorn": {
"version": "8.17.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
......@@ -2176,6 +2117,32 @@
"integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==",
"license": "MIT"
},
"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"
}
],
"license": "MIT"
},
"node_modules/bluebird": {
"version": "3.4.7",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
"integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==",
"license": "MIT"
},
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
......@@ -2478,6 +2445,12 @@
"integrity": "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==",
"license": "MIT"
},
"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==",
"license": "MIT"
},
"node_modules/crossws": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz",
......@@ -2683,12 +2656,72 @@
"node": ">=0.3.1"
}
},
"node_modules/dingbat-to-unicode": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz",
"integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==",
"license": "BSD-2-Clause"
},
"node_modules/dlv": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
"license": "MIT"
},
"node_modules/docx": {
"version": "9.7.1",
"resolved": "https://registry.npmjs.org/docx/-/docx-9.7.1.tgz",
"integrity": "sha512-ilXFf9Moz47ABjFpDiA5s1w9lpb4EFSp7+5iiJSbfyYDM+bpZdAgLlSr7fW4aXhVe/E+F6QCv0EvRVFEd5CsWg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "^25.2.3",
"hash.js": "^1.1.7",
"jszip": "^3.10.1",
"nanoid": "^5.1.3",
"xml": "^1.0.1",
"xml-js": "^1.6.8"
},
"engines": {
"node": ">=10"
}
},
"node_modules/docx/node_modules/@types/node": {
"version": "25.9.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.5.tgz",
"integrity": "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": ">=7.24.0 <7.24.7"
}
},
"node_modules/docx/node_modules/nanoid": {
"version": "5.1.16",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz",
"integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.js"
},
"engines": {
"node": "^18 || >=20"
}
},
"node_modules/docx/node_modules/undici-types": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
"dev": true,
"license": "MIT"
},
"node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
......@@ -2765,6 +2798,15 @@
"node": ">=4"
}
},
"node_modules/duck": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz",
"integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==",
"license": "BSD",
"dependencies": {
"underscore": "^1.13.1"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
......@@ -3112,6 +3154,17 @@
"uncrypto": "^0.1.3"
}
},
"node_modules/hash.js": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"minimalistic-assert": "^1.0.1"
}
},
"node_modules/hast-util-from-html": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz",
......@@ -3331,6 +3384,12 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
"node_modules/import-meta-resolve": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz",
......@@ -3434,6 +3493,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
......@@ -3470,6 +3535,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"license": "(MIT OR GPL-3.0-or-later)",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"setimmediate": "^1.0.5"
}
},
"node_modules/kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
......@@ -3489,6 +3566,15 @@
"node": ">=6"
}
},
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"license": "MIT",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/longest-streak": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
......@@ -3499,6 +3585,17 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/lop": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz",
"integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==",
"license": "BSD-2-Clause",
"dependencies": {
"duck": "^0.1.12",
"option": "~0.2.1",
"underscore": "^1.13.1"
}
},
"node_modules/lru-cache": {
"version": "11.5.2",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
......@@ -3528,6 +3625,39 @@
"source-map-js": "^1.2.1"
}
},
"node_modules/mammoth": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.12.0.tgz",
"integrity": "sha512-cwnK1RIcRdDMi2HRx2EXGYlxqIEh0Oo3bLhorgnsVJi2UkbX1+jKxuBNR9PC5+JaX7EkmJxFPmo6mjLpqShI2w==",
"license": "BSD-2-Clause",
"dependencies": {
"@xmldom/xmldom": "^0.8.6",
"argparse": "~1.0.3",
"base64-js": "^1.5.1",
"bluebird": "~3.4.0",
"dingbat-to-unicode": "^1.0.1",
"jszip": "^3.7.1",
"lop": "^0.4.2",
"path-is-absolute": "^1.0.0",
"underscore": "^1.13.1",
"xmlbuilder": "^10.0.0"
},
"bin": {
"mammoth": "bin/mammoth"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/mammoth/node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"license": "MIT",
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/markdown-table": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
......@@ -4344,6 +4474,13 @@
],
"license": "MIT"
},
"node_modules/minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
"dev": true,
"license": "ISC"
},
"node_modules/mrmime": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
......@@ -4485,6 +4622,12 @@
"regex-recursion": "^6.0.2"
}
},
"node_modules/option": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz",
"integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==",
"license": "BSD-2-Clause"
},
"node_modules/p-limit": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-6.2.0.tgz",
......@@ -4534,6 +4677,12 @@
"integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==",
"license": "MIT"
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/parse-latin": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz",
......@@ -4571,6 +4720,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/piccolore": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz",
......@@ -4648,6 +4806,12 @@
"node": ">=6"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/prompts": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
......@@ -4695,6 +4859,21 @@
"node": ">= 0.6"
}
},
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
......@@ -5007,6 +5186,12 @@
"fsevents": "~2.3.2"
}
},
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/sax": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz",
......@@ -5107,6 +5292,12 @@
"integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==",
"license": "ISC"
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"license": "MIT"
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
......@@ -5251,6 +5442,15 @@
"integrity": "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==",
"license": "MIT"
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
......@@ -5413,64 +5613,592 @@
"license": "0BSD",
"optional": true
},
"node_modules/type-fest": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
"license": "(MIT OR CC0-1.0)",
"node_modules/tsx": {
"version": "4.23.1",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz",
"integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.28.0"
},
"bin": {
"tsx": "dist/cli.mjs"
},
"engines": {
"node": ">=16"
"node": ">=18.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"optionalDependencies": {
"fsevents": "~2.3.3"
}
},
"node_modules/typesafe-path": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/typesafe-path/-/typesafe-path-0.2.2.tgz",
"integrity": "sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==",
"node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
"integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT"
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=14.17"
"node": ">=18"
}
},
"node_modules/typescript-auto-import-cache": {
"version": "0.3.6",
"resolved": "https://registry.npmjs.org/typescript-auto-import-cache/-/typescript-auto-import-cache-0.3.6.tgz",
"integrity": "sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ==",
"node_modules/tsx/node_modules/@esbuild/android-arm": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"dependencies": {
"semver": "^7.3.8"
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/ufo": {
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz",
"integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==",
"license": "MIT"
"node_modules/tsx/node_modules/@esbuild/android-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/ultrahtml": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.7.0.tgz",
"integrity": "sha512-2xRd0VHoAQE4M+vF/DvFFB7pUV0ZxTW1TLi7lHQWnF/Sb5TPeEUV/l+hxcNnGO00ZXGnR0voCMmYRKQf+rvJ2g==",
"license": "MIT"
"node_modules/tsx/node_modules/@esbuild/android-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/uncrypto": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz",
"integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==",
"node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/darwin-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
"integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
"integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-arm": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-ia32": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-loong64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
"integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-s390x": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
"integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/netbsd-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/openbsd-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
"integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/openharmony-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/sunos-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/win32-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/win32-ia32": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/win32-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
"integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/esbuild": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
"integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
"devOptional": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.28.1",
"@esbuild/android-arm": "0.28.1",
"@esbuild/android-arm64": "0.28.1",
"@esbuild/android-x64": "0.28.1",
"@esbuild/darwin-arm64": "0.28.1",
"@esbuild/darwin-x64": "0.28.1",
"@esbuild/freebsd-arm64": "0.28.1",
"@esbuild/freebsd-x64": "0.28.1",
"@esbuild/linux-arm": "0.28.1",
"@esbuild/linux-arm64": "0.28.1",
"@esbuild/linux-ia32": "0.28.1",
"@esbuild/linux-loong64": "0.28.1",
"@esbuild/linux-mips64el": "0.28.1",
"@esbuild/linux-ppc64": "0.28.1",
"@esbuild/linux-riscv64": "0.28.1",
"@esbuild/linux-s390x": "0.28.1",
"@esbuild/linux-x64": "0.28.1",
"@esbuild/netbsd-arm64": "0.28.1",
"@esbuild/netbsd-x64": "0.28.1",
"@esbuild/openbsd-arm64": "0.28.1",
"@esbuild/openbsd-x64": "0.28.1",
"@esbuild/openharmony-arm64": "0.28.1",
"@esbuild/sunos-x64": "0.28.1",
"@esbuild/win32-arm64": "0.28.1",
"@esbuild/win32-ia32": "0.28.1",
"@esbuild/win32-x64": "0.28.1"
}
},
"node_modules/turndown": {
"version": "7.2.4",
"resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.4.tgz",
"integrity": "sha512-I8yFsfRzmzK0WV1pNNOA4A7y4RDfFxPRxb3t+e3ui14qSGOxGtiSP6GjeX+Y6CHb7HYaFj7ECUD7VE5kQMZWGQ==",
"license": "MIT",
"dependencies": {
"@mixmark-io/domino": "^2.2.0"
},
"engines": {
"node": ">=18",
"npm": ">=9"
}
},
"node_modules/turndown-plugin-gfm": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/turndown-plugin-gfm/-/turndown-plugin-gfm-1.0.2.tgz",
"integrity": "sha512-vwz9tfvF7XN/jE0dGoBei3FXWuvll78ohzCZQuOb+ZjWrs3a0XhQVomJEb2Qh4VHTPNRO4GPZh0V7VRbiWwkRg==",
"license": "MIT"
},
"node_modules/type-fest": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/typesafe-path": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/typesafe-path/-/typesafe-path-0.2.2.tgz",
"integrity": "sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==",
"dev": true,
"license": "MIT"
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/typescript-auto-import-cache": {
"version": "0.3.6",
"resolved": "https://registry.npmjs.org/typescript-auto-import-cache/-/typescript-auto-import-cache-0.3.6.tgz",
"integrity": "sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"semver": "^7.3.8"
}
},
"node_modules/ufo": {
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz",
"integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==",
"license": "MIT"
},
"node_modules/ultrahtml": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.7.0.tgz",
"integrity": "sha512-2xRd0VHoAQE4M+vF/DvFFB7pUV0ZxTW1TLi7lHQWnF/Sb5TPeEUV/l+hxcNnGO00ZXGnR0voCMmYRKQf+rvJ2g==",
"license": "MIT"
},
"node_modules/uncrypto": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz",
"integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==",
"license": "MIT"
},
"node_modules/underscore": {
"version": "1.13.8",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz",
"integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==",
"license": "MIT"
},
"node_modules/undici-types": {
......@@ -5756,6 +6484,12 @@
"url": "https://paulmillr.com/funding/"
}
},
"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==",
"license": "MIT"
},
"node_modules/vfile": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
......@@ -6733,6 +7467,35 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/xml": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
"integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==",
"dev": true,
"license": "MIT"
},
"node_modules/xml-js": {
"version": "1.6.11",
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
"integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"sax": "^1.2.4"
},
"bin": {
"xml-js": "bin/cli.js"
}
},
"node_modules/xmlbuilder": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz",
"integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==",
"license": "MIT",
"engines": {
"node": ">=4.0"
}
},
"node_modules/xxhash-wasm": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz",
......@@ -6909,4 +7672,3 @@
}
}
}
......@@ -15,17 +15,24 @@
"check": "astro check",
"start": "node --env-file-if-exists=.env server.mjs",
"admin": "npm start",
"serve": "npm run build && npm start"
"serve": "npm run build && npm start",
"test": "tsx --test test/*.test.ts"
},
"dependencies": {
"@astrojs/node": "9.5.5",
"@astrojs/sitemap": "^3.7.3",
"astro": "5.18.2",
"gray-matter": "^4.0.3",
"marked": "^14.1.4"
"mammoth": "^1.12.0",
"marked": "^14.1.4",
"turndown": "^7.2.4",
"turndown-plugin-gfm": "^1.0.2"
},
"devDependencies": {
"@astrojs/check": "^0.9.6",
"@types/turndown": "^5.0.6",
"docx": "^9.7.1",
"tsx": "^4.23.1",
"typescript": "^5.9.3"
}
}
import fs from "node:fs/promises";
import path from "node:path";
import {
AlignmentType,
Document,
HeadingLevel,
ImageRun,
Packer,
Paragraph,
TextRun,
} from "docx";
const root = process.cwd();
const outputDir = path.join(root, "test-artifacts");
const imagePath = "/Users/mac/Desktop/test.png";
const image = await fs.readFile(imagePath);
const date = new Date().toISOString().slice(0, 10).replaceAll("-", "");
const marker = `CMSUPLOADTEST${date}`;
const title = "天津新人跑网约车:从方案评估到上岗运营的四步指南";
const sections = [
{
heading: "第一步:先说清预算、时间与跑车目标",
paragraphs: [
"准备入行时,不要急着先选车。全职还是兼职、每天可投入的时间、现有预算与风险承受能力,都会影响租期、车型与平台选择。",
"嘉运网约车建议到店时带上自己的预算、时间安排和驾驶经验,先把用车方式与运营目标说清楚。",
],
},
{
heading: "第二步:同步规划车辆、证照与平台",
paragraphs: [
"网约车运营不只是租车或买车。车型是否符合平台准入、驾驶员证与车辆营运证如何办理,需要在签订长期合同前一起核对。",
"嘉运可提供新能源车辆销售、短租、月租、长租与以租代购等选择,并协助办理网约车驾驶员证和车辆营运证,对接曹操出行、滴滴、高德等主流平台。",
],
},
{
heading: "第三步:签约前核对总成本与合同责任",
paragraphs: [
"不要只看月租或月供。押金、保险、维保、违约责任、车辆残值与合同期限,都应列入总成本。所有价格、金融与租赁条件,应以门店当期政策和双方签署的正式合同为准。",
],
},
{
heading: "第四步:完成培训与持续运营",
paragraphs: [
"证照和平台注册完成后,新人还需要熟悉接单规则、日常车辆检查和成本记录。嘉运提供免费岗前培训,并由运营顾问跟进跑车过程中的账号、车辆与运营问题。",
"实际营收会受在线时长、平台规则与市场供需影响,应根据自己的运营数据持续复盘,不宜依赖固定收益承诺。",
],
},
];
const closing = "嘉运网约车是天津本地一站式网约车综合服务商,提供车辆租售、合规办证、平台入驻、司机培训与售后维保服务。咨询电话:022-87940507;门店地址:天津市南开区咸阳路45号小园新厦。";
await fs.mkdir(outputDir, { recursive: true });
await fs.copyFile(imagePath, path.join(outputDir, "test.png"));
const markdown = [
`# ${title}(Markdown 版)`,
"",
"准备在天津跑网约车,可以按照“沟通需求—匹配方案—办证入驻—培训上岗”的顺序推进。把车辆、证照、平台与运营放在同一个方案中考虑,更容易提前发现成本与合规问题。",
"",
"![Test image](./test.png)",
"",
...sections.flatMap((section) => [
`## ${section.heading}`,
"",
...section.paragraphs.flatMap((paragraph) => [paragraph, ""]),
]),
"## 到店前建议准备",
"",
"- 每天可投入的跑车时间",
"- 购车或租车的预算上限",
"- 现有驾驶证、车辆与网约车证照情况",
"- 计划入驻的平台和主要运营区域",
"",
closing,
"",
`内容校验标记:${marker}MD。`,
"",
].join("\n");
await fs.writeFile(path.join(outputDir, `cms-markdown-upload-test-${date}.md`), markdown);
const document = new Document({
sections: [{
properties: {
page: {
margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 },
},
},
children: [
new Paragraph({
text: `${title}(Word 版)`,
heading: HeadingLevel.HEADING_1,
spacing: { before: 320, after: 160 },
}),
new Paragraph({
children: [new TextRun({ text: "准备在天津跑网约车,可以按照“沟通需求—匹配方案—办证入驻—培训上岗”的顺序推进。把车辆、证照、平台与运营放在同一个方案中考虑,更容易提前发现成本与合规问题。", font: "Arial Unicode MS" })],
spacing: { after: 120, line: 280 },
}),
new Paragraph({
children: [new ImageRun({ data: image, transformation: { width: 320, height: 210 }, type: "png" })],
alignment: AlignmentType.CENTER,
spacing: { before: 120 },
}),
...sections.flatMap((section) => [
new Paragraph({ text: section.heading, heading: HeadingLevel.HEADING_2 }),
...section.paragraphs.map((paragraph) => new Paragraph({ text: paragraph })),
]),
new Paragraph({ text: "到店前建议准备", heading: HeadingLevel.HEADING_2 }),
...[
"每天可投入的跑车时间",
"购车或租车的预算上限",
"现有驾驶证、车辆与网约车证照情况",
"计划入驻的平台和主要运营区域",
].map((text) => new Paragraph({ text, bullet: { level: 0 } })),
new Paragraph({ text: closing }),
new Paragraph({ text: `内容校验标记:${marker}DOCX。` }),
],
}],
styles: {
default: {
document: {
run: { font: "Calibri", size: 22 },
paragraph: { spacing: { after: 120, line: 264 } },
},
heading1: {
run: { font: "Calibri", size: 32, bold: true, color: "2E74B5" },
paragraph: { spacing: { before: 320, after: 160 }, outlineLevel: 0 },
},
heading2: {
run: { font: "Arial Unicode MS", size: 26, bold: true, color: "2E74B5" },
paragraph: { spacing: { before: 240, after: 100 }, outlineLevel: 1 },
},
},
},
});
await fs.writeFile(path.join(outputDir, `cms-word-import-test-${date}.docx`), await Packer.toBuffer(document));
console.log(JSON.stringify({ outputDir, date, marker }));
import fs from "node:fs/promises";
import path from "node:path";
const baseUrl = String(process.argv[2] || "http://101.126.10.129:8790").replace(/\/$/, "");
const apiKey = process.env.CMS_API_KEY || "";
const artifactsDir = path.join(process.cwd(), "test-artifacts");
if (!apiKey) throw new Error("请先在 .env 中配置 CMS_API_KEY");
const headers = { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" };
async function cms(route, options = {}) {
const response = await fetch(`${baseUrl}/api/cms${route}`, {
method: options.method || "GET",
headers,
body: options.body === undefined ? undefined : JSON.stringify(options.body),
});
const payload = await response.json().catch(() => ({}));
if (response.status !== options.expectedStatus) {
throw new Error(`${route} 预期 ${options.expectedStatus},实际 ${response.status}${payload.error || "未知错误"}`);
}
return payload;
}
function assert(condition, message) {
if (!condition) throw new Error(message);
}
const files = await fs.readdir(artifactsDir);
const markdownName = files.find((file) => /^cms-markdown-upload-test-\d+\.md$/.test(file));
const wordName = files.find((file) => /^cms-word-import-test-\d+\.docx$/.test(file));
assert(markdownName && wordName, "缺少 Markdown 或 Word 测试文件");
const image = await fs.readFile(path.join(artifactsDir, "test.png"));
const uploadedImage = await cms("/uploads", {
method: "POST",
expectedStatus: 200,
body: { dataUrl: `data:image/png;base64,${image.toString("base64")}` },
});
assert(/^\/uploads\/[a-zA-Z0-9._-]+$/.test(uploadedImage.url), "Markdown 图片上传地址无效");
const markdownSource = await fs.readFile(path.join(artifactsDir, markdownName), "utf8");
const markdownTitle = markdownSource.match(/^#\s+(.+)$/m)?.[1]?.trim();
const markdownBody = markdownSource
.replace(/^#\s+.+\n+/, "")
.replace("./test.png", uploadedImage.url)
.trim();
assert(markdownTitle && markdownBody.includes("CMSUPLOADTEST"), "Markdown 文件缺少标题或校验标记");
const markdownCreated = await cms("/articles", {
method: "POST",
expectedStatus: 201,
body: { title: markdownTitle, category: "入行指南", body: markdownBody },
});
assert(markdownCreated.publishStatus === "new-draft", "Markdown 文章不是新草稿");
const markdownRead = await cms(`/articles/${markdownCreated.slug}`, { expectedStatus: 200 });
assert(markdownRead.body.includes("CMSUPLOADTEST"), "Markdown 草稿回读缺少校验标记");
assert(markdownRead.body.includes(uploadedImage.url), "Markdown 草稿回读缺少图片地址");
const markdownImageResponse = await fetch(`${baseUrl}${uploadedImage.url}`);
assert(markdownImageResponse.status === 200, "Markdown 图片无法访问");
assert(markdownImageResponse.headers.get("content-type") === "image/png", "Markdown 图片 Content-Type 不是 image/png");
const word = await fs.readFile(path.join(artifactsDir, wordName));
const wordCreated = await cms("/imports/word", {
method: "POST",
expectedStatus: 201,
body: {
fileName: wordName,
category: "入行指南",
dataUrl: `data:application/vnd.openxmlformats-officedocument.wordprocessingml.document;base64,${word.toString("base64")}`,
},
});
assert(wordCreated.publishStatus === "new-draft", "Word 文章不是新草稿");
assert(wordCreated.imageCount === 1, "Word 导入图片数量不是 1");
const wordRead = await cms(`/articles/${wordCreated.slug}`, { expectedStatus: 200 });
assert(wordRead.body.includes("CMSUPLOADTEST"), "Word 草稿回读缺少校验标记");
assert(!/^#\s+/m.test(wordRead.body), "Word 一级标题在 Markdown 正文中重复");
const wordImageUrl = wordRead.body.match(/!\[[^\]]*\]\((\/uploads\/[^)]+)\)/)?.[1];
assert(wordImageUrl, "Word 草稿缺少图片地址");
const wordImageResponse = await fetch(`${baseUrl}${wordImageUrl}`);
assert(wordImageResponse.status === 200, "Word 提取图片无法访问");
assert(wordImageResponse.headers.get("content-type") === "image/png", "Word 提取图片 Content-Type 不是 image/png");
const result = {
uploadedAt: new Date().toISOString(),
baseUrl,
markdown: { slug: markdownCreated.slug, title: markdownRead.title, imageUrl: uploadedImage.url },
word: { slug: wordCreated.slug, title: wordRead.title, imageUrl: wordImageUrl, warnings: wordCreated.warnings },
};
await fs.writeFile(path.join(artifactsDir, "upload-result.json"), `${JSON.stringify(result, null, 2)}\n`);
console.log(`CMS 上传验证通过:Markdown=${result.markdown.slug} Word=${result.word.slug}`);
......@@ -470,8 +470,22 @@ export async function saveUpload(dataUrl: unknown): Promise<string> {
const match = String(dataUrl || "").match(/^data:([^;]+);base64,(.+)$/s);
if (!match || !extensions[match[1]]) throw new StoreError("仅支持 PNG、JPG、WEBP 或 GIF 图片");
const buffer = Buffer.from(match[2], "base64");
if (buffer.length > 8 * 1024 * 1024) throw new StoreError("图片不能超过 8MB", 413);
const name = `${Date.now()}-${crypto.randomBytes(4).toString("hex")}.${extensions[match[1]]}`;
if (buffer.length > 20 * 1024 * 1024) throw new StoreError("图片不能超过 20MB", 413);
return saveUploadBuffer(buffer, match[1]);
}
export async function saveUploadBuffer(buffer: Buffer, contentType: string): Promise<string> {
await ensureStore();
const extensions: Record<string, string> = { "image/png": "png", "image/jpeg": "jpg", "image/webp": "webp", "image/gif": "gif" };
const extension = extensions[contentType];
if (!extension) throw new StoreError("仅支持 PNG、JPG、WEBP 或 GIF 图片");
if (buffer.length > 20 * 1024 * 1024) throw new StoreError("图片不能超过 20MB", 413);
const name = `${Date.now()}-${crypto.randomBytes(4).toString("hex")}.${extension}`;
await writeAtomic(path.join(UPLOADS_DIR, name), buffer);
return `/uploads/${name}`;
}
export async function removeUpload(url: string): Promise<void> {
const filename = url.match(/^\/uploads\/([a-zA-Z0-9._-]+)$/)?.[1];
if (filename) await fs.unlink(path.join(UPLOADS_DIR, filename)).catch(() => {});
}
......@@ -21,6 +21,7 @@ import {
writeArticle,
} from "./article-store";
import { buildSiteAtomic, tryAcquireSiteBuildLock, withSiteBuildLock } from "../../scripts/site-build.mjs";
import { importWordArticle } from "./word-import";
const PASSWORD = process.env.CMS_PASSWORD || "";
const SECRET = process.env.CMS_SECRET || crypto.randomBytes(32).toString("hex");
......@@ -290,6 +291,11 @@ export async function handleCmsApi(request: Request, routeValue: string, clientA
return json({ ok: true, url: await saveUpload(body.dataUrl) });
}
if (route === "imports/word" && method === "POST") {
if (!apiKeyValid(request)) throw new StoreError("Word 导入需要有效的 CMS API Key", 401);
return json(await importWordArticle(await bodyOf(request)), 201);
}
return json({ error: "接口不存在" }, 404);
} catch (error) {
if (error instanceof StoreError) return json({ error: error.message, ...error.details }, error.status);
......
import mammoth from "mammoth";
import TurndownService from "turndown";
import { gfm } from "turndown-plugin-gfm";
import {
StoreError,
generateSlug,
getPublishStatus,
removeUpload,
saveUploadBuffer,
writeArticle,
} from "./article-store";
import { withSiteBuildLock } from "../../scripts/site-build.mjs";
const DOCX_MIME = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
const MAX_FILE_BYTES = 20 * 1024 * 1024;
interface WordImportInput {
fileName?: unknown;
category?: unknown;
title?: unknown;
dataUrl?: unknown;
}
export interface WordImportResult {
ok: true;
slug: string;
title: string;
publishStatus: "new-draft" | "edited-draft" | "live";
imageCount: number;
warnings: string[];
}
function decodeDocx(input: WordImportInput): Buffer {
const fileName = String(input.fileName || "").trim();
if (!fileName.toLowerCase().endsWith(".docx")) {
throw new StoreError("仅支持 .docx Word 文件,不支持旧版 .doc 格式");
}
const match = String(input.dataUrl || "").match(/^data:([^;,]+);base64,([a-zA-Z0-9+/=\s]+)$/);
if (!match || match[1].toLowerCase() !== DOCX_MIME) {
throw new StoreError("Word 文件数据格式不正确");
}
const compactBase64 = match[2].replace(/\s/g, "");
if (!compactBase64 || compactBase64.length > Math.ceil(MAX_FILE_BYTES / 3) * 4 + 4) {
throw new StoreError("Word 文件不能超过 20MB", 413);
}
const buffer = Buffer.from(compactBase64, "base64");
if (buffer.length > MAX_FILE_BYTES) throw new StoreError("Word 文件不能超过 20MB", 413);
if (buffer.length < 4 || buffer.subarray(0, 2).toString("binary") !== "PK") {
throw new StoreError("Word 文件内容无效");
}
return buffer;
}
function titleFromHeading(html: string): { title: string; htmlWithoutHeading: string } {
const heading = html.match(/<h1(?:\s[^>]*)?>([\s\S]*?)<\/h1>/i);
if (!heading) return { title: "", htmlWithoutHeading: html };
const titleService = new TurndownService();
const title = titleService.turndown(heading[1]).replace(/\s+/g, " ").trim();
return {
title,
htmlWithoutHeading: `${html.slice(0, heading.index)}${html.slice((heading.index || 0) + heading[0].length)}`,
};
}
export async function importWordArticle(input: WordImportInput): Promise<WordImportResult> {
const buffer = decodeDocx(input);
const uploadedUrls: string[] = [];
try {
const converted = await mammoth.convertToHtml(
{ buffer },
{
convertImage: mammoth.images.imgElement(async (image) => {
const content = await image.readAsBuffer();
const url = await saveUploadBuffer(content, image.contentType.toLowerCase());
uploadedUrls.push(url);
return { src: url };
}),
},
);
const extracted = titleFromHeading(converted.value);
const title = String(input.title || "").replace(/\s+/g, " ").trim() || extracted.title;
if (!title) throw new StoreError("请在 Word 中添加一级标题,或在请求中提供 title");
const turndown = new TurndownService({ headingStyle: "atx", bulletListMarker: "-" });
turndown.use(gfm);
const body = turndown.turndown(extracted.htmlWithoutHeading).trim();
if (!body) throw new StoreError("Word 文件没有可导入的正文");
const { slug, article } = await withSiteBuildLock(async () => {
const slug = await generateSlug(input.category);
const article = await writeArticle(slug, { title, category: input.category, body }, true);
return { slug, article };
});
return {
ok: true,
slug,
title,
publishStatus: await getPublishStatus(article),
imageCount: uploadedUrls.length,
warnings: converted.messages.map((message) => message.message),
};
} catch (error) {
await Promise.all(uploadedUrls.map(removeUpload));
throw error;
}
}
declare module "turndown-plugin-gfm" {
import type TurndownService from "turndown";
export const gfm: TurndownService.Plugin;
export const highlightedCodeBlock: TurndownService.Plugin;
export const strikethrough: TurndownService.Plugin;
export const tables: TurndownService.Plugin;
export const taskListItems: TurndownService.Plugin;
}
# 天津新人跑网约车:从方案评估到上岗运营的四步指南(Markdown 版)
准备在天津跑网约车,可以按照“沟通需求—匹配方案—办证入驻—培训上岗”的顺序推进。把车辆、证照、平台与运营放在同一个方案中考虑,更容易提前发现成本与合规问题。
![Test image](./test.png)
## 第一步:先说清预算、时间与跑车目标
准备入行时,不要急着先选车。全职还是兼职、每天可投入的时间、现有预算与风险承受能力,都会影响租期、车型与平台选择。
嘉运网约车建议到店时带上自己的预算、时间安排和驾驶经验,先把用车方式与运营目标说清楚。
## 第二步:同步规划车辆、证照与平台
网约车运营不只是租车或买车。车型是否符合平台准入、驾驶员证与车辆营运证如何办理,需要在签订长期合同前一起核对。
嘉运可提供新能源车辆销售、短租、月租、长租与以租代购等选择,并协助办理网约车驾驶员证和车辆营运证,对接曹操出行、滴滴、高德等主流平台。
## 第三步:签约前核对总成本与合同责任
不要只看月租或月供。押金、保险、维保、违约责任、车辆残值与合同期限,都应列入总成本。所有价格、金融与租赁条件,应以门店当期政策和双方签署的正式合同为准。
## 第四步:完成培训与持续运营
证照和平台注册完成后,新人还需要熟悉接单规则、日常车辆检查和成本记录。嘉运提供免费岗前培训,并由运营顾问跟进跑车过程中的账号、车辆与运营问题。
实际营收会受在线时长、平台规则与市场供需影响,应根据自己的运营数据持续复盘,不宜依赖固定收益承诺。
## 到店前建议准备
- 每天可投入的跑车时间
- 购车或租车的预算上限
- 现有驾驶证、车辆与网约车证照情况
- 计划入驻的平台和主要运营区域
嘉运网约车是天津本地一站式网约车综合服务商,提供车辆租售、合规办证、平台入驻、司机培训与售后维保服务。咨询电话:022-87940507;门店地址:天津市南开区咸阳路45号小园新厦。
内容校验标记:CMSUPLOADTEST20260731MD。
import assert from "node:assert/strict";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { Document, HeadingLevel, ImageRun, Packer, Paragraph } from "docx";
const API_KEY = "word-import-test-key";
const MARKER = `WORDIMPORTTEST${Date.now()}`;
const ONE_PIXEL_PNG = Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M/wHwAF/gL+XPrlGQAAAABJRU5ErkJggg==",
"base64",
);
async function createDocx(): Promise<Buffer> {
const document = new Document({
sections: [{
children: [
new Paragraph({ text: "Word 导入集成测试", heading: HeadingLevel.HEADING_1 }),
new Paragraph(`这是唯一正文标记:${MARKER}`),
new Paragraph({ children: [new ImageRun({ data: ONE_PIXEL_PNG, transformation: { width: 1, height: 1 }, type: "png" })] }),
],
}],
});
return Packer.toBuffer(document);
}
test("Word 导入会建立带图片的未发布草稿", async () => {
const dataRoot = await fs.mkdtemp(path.join(os.tmpdir(), "jiayun-word-import-"));
process.env.CMS_DATA_DIR = dataRoot;
process.env.CMS_BUILD_LOCK = path.join(dataRoot, "site-build.lock");
process.env.CMS_API_KEY = API_KEY;
const [{ handleCmsApi }, { UPLOADS_DIR }, { GET: getUpload }] = await Promise.all([
import("../src/lib/cms-api"),
import("../src/lib/article-store"),
import("../src/pages/uploads/[file]"),
]);
const docx = await createDocx();
const payload = {
fileName: "集成测试.docx",
category: "入行指南",
dataUrl: `data:application/vnd.openxmlformats-officedocument.wordprocessingml.document;base64,${docx.toString("base64")}`,
};
const request = (key: string, body = payload) => new Request("http://localhost/api/cms/imports/word", {
method: "POST",
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const invalidKey = await handleCmsApi(request("wrong-key"), "imports/word");
assert.equal(invalidKey.status, 401);
const oldDoc = await handleCmsApi(request(API_KEY, { ...payload, fileName: "旧版.doc" }), "imports/word");
assert.equal(oldDoc.status, 400);
assert.match((await oldDoc.json()).error, /\.docx/);
const imported = await handleCmsApi(request(API_KEY), "imports/word");
assert.equal(imported.status, 201);
const result = await imported.json();
assert.equal(result.title, "Word 导入集成测试");
assert.equal(result.publishStatus, "new-draft");
assert.equal(result.imageCount, 1);
const articleResponse = await handleCmsApi(
new Request(`http://localhost/api/cms/articles/${result.slug}`, { headers: { Authorization: `Bearer ${API_KEY}` } }),
`articles/${result.slug}`,
);
assert.equal(articleResponse.status, 200);
const article = await articleResponse.json();
assert.match(article.body, new RegExp(MARKER));
assert.doesNotMatch(article.body, /^#\s+Word 导入集成测试/m);
const imageUrl = article.body.match(/!\[[^\]]*\]\((\/uploads\/[^)]+)\)/)?.[1];
assert.ok(imageUrl);
const imageName = path.basename(imageUrl);
const imageResponse = await getUpload({ params: { file: imageName } } as never);
assert.equal(imageResponse.status, 200);
assert.equal(imageResponse.headers.get("content-type"), "image/png");
assert.deepEqual(Buffer.from(await imageResponse.arrayBuffer()), ONE_PIXEL_PNG);
assert.equal((await fs.readdir(UPLOADS_DIR)).length, 1);
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment