diff --git a/plugins/ztools-rss/.gitignore b/plugins/ztools-rss/.gitignore new file mode 100644 index 000000000..a547bf36d --- /dev/null +++ b/plugins/ztools-rss/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/plugins/ztools-rss/README.md b/plugins/ztools-rss/README.md new file mode 100644 index 000000000..0c96e74fe --- /dev/null +++ b/plugins/ztools-rss/README.md @@ -0,0 +1,75 @@ +# ztools-rss + +> 在 ZTools 内阅读 RSS 文章,支持 FreshRSS / Google Reader API 兼容服务端。 + +## 功能 + +- **双栏布局**:左栏为订阅源树 + 文章列表(按时间逆序),右栏为文章正文。 +- **Google Reader API**:兼容 FreshRSS(默认)、Miniflux、Inoreader、The Old Reader 等服务端。 +- **订阅源树**:按分类(文件夹)分组,显示未读计数,支持折叠。 +- **文章阅读**:HTML 正文渲染(经 DOMPurify 清洗)、加星、标记已读、原文跳转。 +- **明暗主题**自适应。 + +## 使用方式 + +1. 在 ZTools 主输入框输入 `rss` / `RSS` / `订阅` 进入插件。 +2. 首次使用点击右上角 ⚙,填写账号配置: + - **API 地址**:FreshRSS 的 `greader.php` 地址,例如 `https://your-host/api/greader.php`(不带尾斜杠)。 + - **用户名**:FreshRSS 用户名。 + - **API 密码**:在 FreshRSS「账户管理」中设置的**专用 API 密码**(非登录密码)。 +3. 连接成功后,左侧选择订阅源即可阅读。 + +## 开发 + +```bash +npm install +npm run dev # 开发服务器 http://localhost:5173 +npm run build # 类型检查 + 生产构建,输出到 dist/ +``` + +## 项目结构 + +``` +. +├── public/ +│ ├── logo.png # 插件图标 +│ ├── plugin.json # 插件配置 +│ └── preload/ +│ ├── package.json +│ └── services.js # Node.js 层:rssFetch(规避渲染进程 CORS) +└── src/ + ├── main.ts # 入口 + ├── main.css # 全局样式 / 主题变量 + ├── App.vue # 路由 + ├── env.d.ts # 类型声明 + └── Reader/ + ├── index.vue # 双栏布局容器 + ├── types.ts # Google Reader API 类型 + ├── api.ts # API 客户端(登录/订阅/文章/标记) + ├── store.ts # 响应式状态管理 + ├── utils.ts # 日期等工具函数 + ├── FeedTree.vue # 订阅源树 + ├── ArticleList.vue # 文章列表 + ├── ArticleView.vue # 文章正文 + └── Settings.vue # 账号配置弹窗 +``` + +## Google Reader API 端点 + +基于 [FreshRSS GReader API](https://freshrss.github.io/FreshRSS/en/developers/06_GoogleReader_API.html): + +| 功能 | 方法 & 路径 | +| --- | --- | +| 登录 | `POST /accounts/ClientLogin` | +| 订阅列表 | `GET /reader/api/0/subscription/list` | +| 未读计数 | `GET /reader/api/0/unread-count` | +| 文章流 | `GET /reader/api/0/stream/contents/` | +| 修改令牌 | `GET /reader/api/0/token` | +| 标记已读/加星 | `POST /reader/api/0/edit-tag` | +| 整源已读 | `POST /reader/api/0/mark-all-as-read` | + +所有网络请求经 preload 层(Node.js `http`/`https`)发起,以规避渲染进程 CORS 限制。 + +## 开源协议 + +MIT License diff --git a/plugins/ztools-rss/index.html b/plugins/ztools-rss/index.html new file mode 100644 index 000000000..eaa17316c --- /dev/null +++ b/plugins/ztools-rss/index.html @@ -0,0 +1,11 @@ + + + + + + + +
+ + + diff --git a/plugins/ztools-rss/package-lock.json b/plugins/ztools-rss/package-lock.json new file mode 100644 index 000000000..efdca18c3 --- /dev/null +++ b/plugins/ztools-rss/package-lock.json @@ -0,0 +1,1534 @@ +{ + "name": "ztools-rss", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ztools-rss", + "version": "1.0.0", + "dependencies": { + "dompurify": "^3.4.12", + "vue": "^3.5.13" + }, + "devDependencies": { + "@types/dompurify": "^3.0.5", + "@vitejs/plugin-vue": "^5.2.1", + "@ztools-center/ztools-api-types": "^1.0.1", + "typescript": "^5.3.0", + "vite": "^6.0.11", + "vue-tsc": "^2.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/trusted-types": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.15", + "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.15.tgz", + "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.15" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.15", + "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.15.tgz", + "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.15", + "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.15.tgz", + "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.40", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.40.tgz", + "integrity": "sha512-39E8IgOhTbVDnoJFMKc2DvYnypcZwUqgUhQkccva/0m6FUwtIKSGV7n1hpVmYcFaoRAwf9pBcwnKlCEsN63ZEQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.40", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.40", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.40.tgz", + "integrity": "sha512-pwkx4vqlqOspFstrcmzwkKLePVMD3PT65imRzLhanU2V1Fj4K13g6OXjanOyzw3aTAuRk84BOmY8f3rEHqPaVA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.40", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.40.tgz", + "integrity": "sha512-gIf497P4kpuALcvs5n3AEg1Vdn0pSY4XbjASIfHNYF1/MP3T2Mf2STERTubysBxCRxzJGJYtF/O7vwJrxFB3Vw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.40", + "@vue/compiler-dom": "3.5.40", + "@vue/compiler-ssr": "3.5.40", + "@vue/shared": "3.5.40", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.19", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.40", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.40.tgz", + "integrity": "sha512-rrE5xiXG663+vHCHa3J9p2z5OcBRjXmoqenprJxAFQxg5pSshzeBiCE6pu46axapRJ2Adk0YDA2BRZVjiHXnhg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmmirror.com/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/@vue/language-core": { + "version": "2.2.12", + "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-2.2.12.tgz", + "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^1.0.3", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.40", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.40.tgz", + "integrity": "sha512-B7ot9UlUZOi1zbq61/LvE88ZLTV8IlajTdiZTAEiDQgrnIMIZoPr9kGw0Zw46ObW62O9+H/Be3kMbfb7kYPQZA==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.40", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.40.tgz", + "integrity": "sha512-KAZLweuZ6uUJPK1PMSQPgBU5gCjgrrfjUhSglmU9NhH+Zjepa8cnwSydPWDWHDwOgY4g3VcZ+PljbiHlURNCbw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.40", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.40.tgz", + "integrity": "sha512-ZfrX8ssZQds900L9pr8AuK05ddnMsR4MPMZr8cPN9GoqoPWcXLhjvvbIA2SMv+7a97sJ1vv9pj/zxK0Cq/eEFQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.40", + "@vue/runtime-core": "3.5.40", + "@vue/shared": "3.5.40", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.40", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.40.tgz", + "integrity": "sha512-XNJym9WpevhTVt1HuwOrCRJ5Q+9z4BjTMrDtjTrvx74SmUll8spNTw6whWJa9mEkO4PKn5TihI/bm/8ds2QVJw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.40", + "@vue/runtime-dom": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.40", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.40.tgz", + "integrity": "sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg==", + "license": "MIT" + }, + "node_modules/@ztools-center/ztools-api-types": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@ztools-center/ztools-api-types/-/ztools-api-types-1.0.3.tgz", + "integrity": "sha512-dv1eOAIasAupqKaQL/gESk1i2+RebdM/1gvZhrvH2D/bo3enCUsAGJ8nrHnlCLBSOGB81eC/SU0IH8BNsUlmvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/alien-signals": { + "version": "1.0.13", + "resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-1.0.13.tgz", + "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dompurify": { + "version": "3.4.12", + "resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-3.4.12.tgz", + "integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.19", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmmirror.com/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.40", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.40.tgz", + "integrity": "sha512-+8PJ4SJXdn/cHGImF4CKdxlWHIN5Dkt7DoufRREM6h6uVCx2m7QxgcEQmmzyOK8A9mcafg7sFbJFYsdFVubTig==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.40", + "@vue/compiler-sfc": "3.5.40", + "@vue/runtime-dom": "3.5.40", + "@vue/server-renderer": "3.5.40", + "@vue/shared": "3.5.40" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-tsc": { + "version": "2.2.12", + "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.2.12.tgz", + "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.15", + "@vue/language-core": "2.2.12" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + } + } +} diff --git a/plugins/ztools-rss/package.json b/plugins/ztools-rss/package.json new file mode 100644 index 000000000..1c74f9cdf --- /dev/null +++ b/plugins/ztools-rss/package.json @@ -0,0 +1,22 @@ +{ + "name": "ztools-rss", + "version": "1.0.0", + "description": "在ZTools内查看RSS", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc && vite build" + }, + "dependencies": { + "dompurify": "^3.4.12", + "vue": "^3.5.13" + }, + "devDependencies": { + "@types/dompurify": "^3.0.5", + "@vitejs/plugin-vue": "^5.2.1", + "@ztools-center/ztools-api-types": "^1.0.1", + "typescript": "^5.3.0", + "vite": "^6.0.11", + "vue-tsc": "^2.0.0" + } +} diff --git a/plugins/ztools-rss/public/logo.png b/plugins/ztools-rss/public/logo.png new file mode 100644 index 000000000..f195f8779 Binary files /dev/null and b/plugins/ztools-rss/public/logo.png differ diff --git a/plugins/ztools-rss/public/plugin.json b/plugins/ztools-rss/public/plugin.json new file mode 100644 index 000000000..94600310a --- /dev/null +++ b/plugins/ztools-rss/public/plugin.json @@ -0,0 +1,32 @@ +{ + "$schema": "node_modules/@ztools-center/ztools-api-types/resource/ztools.schema.json", + "name": "ztools-rss", + "title": "RSS 阅读器", + "description": "在 ZTools 内阅读 RSS,支持 FreshRSS / Google Reader API", + "author": "Kitsune", + "version": "1.0.0", + "main": "index.html", + "preload": "preload/services.js", + "logo": "logo.png", + "development": { + "main": "http://localhost:5173" + }, + "features": [ + { + "code": "rss", + "explain": "阅读 RSS 订阅文章", + "icon": "logo.png", + "cmds": [ + "rss", + "RSS", + "订阅", + "freshrss" + ] + } + ], + "platform": [ + "darwin", + "win32", + "linux" + ] +} diff --git a/plugins/ztools-rss/public/preload/package.json b/plugins/ztools-rss/public/preload/package.json new file mode 100644 index 000000000..5bbefffba --- /dev/null +++ b/plugins/ztools-rss/public/preload/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/plugins/ztools-rss/public/preload/services.js b/plugins/ztools-rss/public/preload/services.js new file mode 100644 index 000000000..86629250d --- /dev/null +++ b/plugins/ztools-rss/public/preload/services.js @@ -0,0 +1,146 @@ +const https = require('node:https') +const http = require('node:http') +const { URL } = require('node:url') + +/** + * 在 Node.js 侧发起 HTTP(S) 请求,规避渲染进程的 CORS 限制。 + * @param {string} urlStr 完整 URL + * @param {object} [opts] + * @param {string} [opts.method] HTTP 方法 + * @param {object} [opts.headers] 请求头 + * @param {string|null} [opts.body] 请求体(字符串) + * @param {number} [opts.timeout] 超时毫秒,默认 30000 + * @returns {Promise<{ status: number, headers: object, text: string }>} + */ +function httpRequest(urlStr, opts = {}) { + const { method = 'GET', headers = {}, body = null, timeout = 30000 } = opts + return new Promise((resolve, reject) => { + let u + try { + u = new URL(urlStr) + } catch (e) { + reject(new Error('无效的 URL:' + urlStr)) + return + } + const lib = u.protocol === 'https:' ? https : http + const reqOpts = { + method, + hostname: u.hostname, + port: u.port || (u.protocol === 'https:' ? 443 : 80), + path: u.pathname + u.search, + headers: { ...headers } + } + + const req = lib.request(reqOpts, (res) => { + if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + let loc = res.headers.location + if (loc.startsWith('/')) loc = u.protocol + '//' + u.host + loc + res.resume() + httpRequest(loc, { method, headers, body, timeout }).then(resolve, reject) + return + } + const chunks = [] + res.on('data', (c) => chunks.push(c)) + res.on('end', () => { + resolve({ + status: res.statusCode, + headers: res.headers, + text: Buffer.concat(chunks).toString('utf-8') + }) + }) + res.on('error', reject) + }) + + req.on('error', reject) + req.setTimeout(timeout, () => { + req.destroy(new Error('请求超时')) + }) + if (body != null) req.write(body) + req.end() + }) +} + +const UA = + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36' + +/** + * 通过 Node 下载图片(可带 Referer 绕过防盗链),返回 data URL。 + * 独立实现以正确处理二进制数据。 + * @param {string} urlStr 图片地址 + * @param {string} [referer] Referer 提示,用于防盗链 + * @returns {Promise} data:image/...;base64,xxxx + */ +function fetchImageAsDataUrl(urlStr, referer) { + return new Promise((resolve, reject) => { + let u + try { + u = new URL(urlStr) + } catch (e) { + reject(new Error('无效的图片 URL')) + return + } + const lib = u.protocol === 'https:' ? https : http + const headers = { 'User-Agent': UA, Accept: 'image/*,*/*;q=0.8' } + if (referer) { + headers['Referer'] = referer + try { + headers['Origin'] = new URL(referer).origin + } catch (_) { + /* ignore */ + } + } + const req = lib.request( + { + hostname: u.hostname, + port: u.port || (u.protocol === 'https:' ? 443 : 80), + path: u.pathname + u.search, + headers + }, + (res) => { + if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + let loc = res.headers.location + if (loc.startsWith('/')) loc = u.protocol + '//' + u.host + loc + res.resume() + fetchImageAsDataUrl(loc, referer).then(resolve, reject) + return + } + if (res.statusCode !== 200) { + res.resume() + reject(new Error('图片下载失败:HTTP ' + res.statusCode)) + return + } + const ct = (res.headers['content-type'] || 'image/jpeg').split(';')[0].trim() + const chunks = [] + res.on('data', (c) => chunks.push(c)) + res.on('end', () => { + const b64 = Buffer.concat(chunks).toString('base64') + resolve('data:' + ct + ';base64,' + b64) + }) + res.on('error', reject) + } + ) + req.on('error', reject) + req.setTimeout(20000, () => req.destroy(new Error('图片下载超时'))) + req.end() + }) +} + +// 通过 window 对象向渲染进程注入能力 +window.services = { + /** + * RSS HTTP 请求 + * @param {string} url + * @param {{ method?: string, headers?: object, body?: string|null, timeout?: number }} [opts] + */ + async rssFetch(url, opts) { + const res = await httpRequest(url, opts) + return { + status: res.status, + ok: res.status >= 200 && res.status < 300, + headers: res.headers, + text: res.text + } + }, + /** 下载图片为 data URL(绕过防盗链) */ + fetchImageAsDataUrl +} diff --git a/plugins/ztools-rss/src/App.vue b/plugins/ztools-rss/src/App.vue new file mode 100644 index 000000000..f0421e8d7 --- /dev/null +++ b/plugins/ztools-rss/src/App.vue @@ -0,0 +1,7 @@ + + + diff --git a/plugins/ztools-rss/src/Reader/ArticleList.vue b/plugins/ztools-rss/src/Reader/ArticleList.vue new file mode 100644 index 000000000..7c6b5328c --- /dev/null +++ b/plugins/ztools-rss/src/Reader/ArticleList.vue @@ -0,0 +1,110 @@ + + + + + diff --git a/plugins/ztools-rss/src/Reader/ArticleView.vue b/plugins/ztools-rss/src/Reader/ArticleView.vue new file mode 100644 index 000000000..34c847188 --- /dev/null +++ b/plugins/ztools-rss/src/Reader/ArticleView.vue @@ -0,0 +1,231 @@ + + + + + diff --git a/plugins/ztools-rss/src/Reader/FeedTree.vue b/plugins/ztools-rss/src/Reader/FeedTree.vue new file mode 100644 index 000000000..26e3bbdf6 --- /dev/null +++ b/plugins/ztools-rss/src/Reader/FeedTree.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/plugins/ztools-rss/src/Reader/Settings.vue b/plugins/ztools-rss/src/Reader/Settings.vue new file mode 100644 index 000000000..74f441c5f --- /dev/null +++ b/plugins/ztools-rss/src/Reader/Settings.vue @@ -0,0 +1,177 @@ + + + + + diff --git a/plugins/ztools-rss/src/Reader/api.ts b/plugins/ztools-rss/src/Reader/api.ts new file mode 100644 index 000000000..080113cc3 --- /dev/null +++ b/plugins/ztools-rss/src/Reader/api.ts @@ -0,0 +1,221 @@ +import DOMPurify from 'dompurify' +import { + type GReaderConfig, + type SubscriptionListResponse, + type UnreadCountResponse, + type StreamContentsResponse, + type Article, + type StreamItem, + STATE_READ, + STATE_STARRED, + STATE_READING_LIST +} from './types' + +// 给所有 加 referrerpolicy / loading,规避部分防盗链(空 Referer 放行的 CDN) +DOMPurify.addHook('afterSanitizeAttributes', (node) => { + if (node.nodeName === 'IMG') { + node.setAttribute('referrerpolicy', 'no-referrer') + node.setAttribute('loading', 'lazy') + } +}) + +const READ_TAG = STATE_READ +const STAR_TAG = STATE_STARRED + +let authToken: string | null = null +let editToken: string | null = null + +function normalizeBase(baseUrl: string): string { + return (baseUrl || '').trim().replace(/\/+$/, '') +} + +/** ClientLogin 登录,获取 Auth token */ +export async function login(config: GReaderConfig): Promise { + const base = normalizeBase(config.baseUrl) + if (!base) throw new Error('未配置 API 地址') + const body = new URLSearchParams({ + service: 'reader', + Email: config.username, + Passwd: config.password + }).toString() + const res = await window.services.rssFetch(`${base}/accounts/ClientLogin`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body + }) + if (!res.ok && res.status !== 200) { + throw new Error('登录失败:' + (res.status === 401 ? '用户名或密码错误' : `HTTP ${res.status}`)) + } + const match = res.text.match(/^Auth=(.+)$/m) + if (!match) throw new Error('登录失败:返回数据格式异常') + authToken = match[1].trim() + editToken = null +} + +export function isAuthed(): boolean { + return !!authToken +} + +export function logout(): void { + authToken = null + editToken = null +} + +async function apiGet(config: GReaderConfig, pathAndQuery: string): Promise { + return request(config, pathAndQuery, 'GET', null) +} + +async function apiPostForm( + config: GReaderConfig, + path: string, + params: Record +): Promise { + const parts: string[] = [] + for (const [k, v] of Object.entries(params)) { + const arr = Array.isArray(v) ? v : [v] + for (const item of arr) parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(item)}`) + } + return request(config, path, 'POST', parts.join('&')) +} + +async function request( + config: GReaderConfig, + pathAndQuery: string, + method: 'GET' | 'POST', + body: string | null +): Promise { + if (!authToken) throw new Error('尚未登录') + const base = normalizeBase(config.baseUrl) + const url = `${base}/reader/api/0/${pathAndQuery.replace(/^\/+/, '')}` + const headers: Record = { + Authorization: `GoogleLogin auth=${authToken}` + } + if (method === 'POST') headers['Content-Type'] = 'application/x-www-form-urlencoded' + const res = await window.services.rssFetch(url, { method, headers, body }) + if (res.status === 401) { + authToken = null + editToken = null + throw new Error('登录已失效,请重新登录') + } + if (!res.ok) throw new Error(`请求失败:HTTP ${res.status}`) + return res.text +} + +/** 获取修改操作所需的 session token(FreshRSS 实际为长效) */ +async function getEditToken(config: GReaderConfig): Promise { + if (editToken) return editToken + editToken = (await apiGet(config, 'token')).trim() + return editToken +} + +export async function fetchSubscriptionList(config: GReaderConfig): Promise { + const text = await apiGet(config, 'subscription/list?output=json') + return JSON.parse(text) +} + +export async function fetchUnreadCount(config: GReaderConfig): Promise { + const text = await apiGet(config, 'unread-count?output=json&all=true') + return JSON.parse(text) +} + +function encodeStream(streamId: string): string { + // streamId 作为路径的一部分,保留斜杠,仅编码空格等字符 + return streamId.split('/').map(encodeURIComponent).join('/') +} + +export interface FetchArticlesOptions { + /** 每页数量,默认 40 */ + n?: number + /** 仅未读 */ + onlyUnread?: boolean + /** 续接 token */ + continuation?: string +} + +export async function fetchArticles( + config: GReaderConfig, + streamId: string, + opts: FetchArticlesOptions = {} +): Promise { + const { n = 40, onlyUnread = true, continuation } = opts + const params = [`n=${n}`, 'output=json'] + if (onlyUnread) params.push(`xt=${encodeURIComponent(STATE_READ)}`) + if (continuation) params.push(`c=${encodeURIComponent(continuation)}`) + const path = `stream/contents/${encodeStream(streamId)}?${params.join('&')}` + const text = await apiGet(config, path) + return JSON.parse(text) +} + +function parseArticle(item: StreamItem): Article { + const cats = item.categories || [] + const content = item.content?.content || item.summary?.content || '' + const link = item.canonical?.[0]?.href || item.alternate?.[0]?.href + return { + id: item.id, + title: item.title || '(无标题)', + content, + author: item.author, + published: (item.published || 0) * 1000, + link, + feedId: item.origin?.streamId, + feedTitle: item.origin?.title, + feedUrl: item.origin?.htmlUrl || item.origin?.url, + read: cats.includes(READ_TAG), + starred: cats.includes(STAR_TAG) + } +} + +export function mapArticles(resp: StreamContentsResponse): Article[] { + return (resp.items || []).map(parseArticle).sort((a, b) => b.published - a.published) +} + +/** 标记单篇文章 已读/未读 */ +export async function markItemRead( + config: GReaderConfig, + itemId: string, + read: boolean +): Promise { + const token = await getEditToken(config) + const params: Record = { i: itemId, T: token } + if (read) params.a = READ_TAG + else params.r = READ_TAG + await apiPostForm(config, 'edit-tag', params) +} + +/** 标记单篇文章 加星/取消加星 */ +export async function markItemStarred( + config: GReaderConfig, + itemId: string, + starred: boolean +): Promise { + const token = await getEditToken(config) + const params: Record = { i: itemId, T: token } + if (starred) params.a = STAR_TAG + else params.r = STAR_TAG + await apiPostForm(config, 'edit-tag', params) +} + +/** 标记整个流为已读 */ +export async function markStreamRead( + config: GReaderConfig, + streamId: string +): Promise { + const token = await getEditToken(config) + await apiPostForm(config, 'mark-all-as-read', { + s: streamId, + T: token + }) +} + +export function sanitizeHtml(html: string): string { + return DOMPurify.sanitize(html, { + ADD_ATTR: ['target'], + FORBID_TAGS: ['style', 'script'], + FORBID_ATTR: ['style', 'onload', 'onerror'] + }) +} + +export const StreamIds = { + readingList: STATE_READING_LIST, + starred: STATE_STARRED +} diff --git a/plugins/ztools-rss/src/Reader/index.vue b/plugins/ztools-rss/src/Reader/index.vue new file mode 100644 index 000000000..ddffa88cc --- /dev/null +++ b/plugins/ztools-rss/src/Reader/index.vue @@ -0,0 +1,268 @@ + + + + + diff --git a/plugins/ztools-rss/src/Reader/store.ts b/plugins/ztools-rss/src/Reader/store.ts new file mode 100644 index 000000000..e46f4dfdc --- /dev/null +++ b/plugins/ztools-rss/src/Reader/store.ts @@ -0,0 +1,365 @@ +import { reactive, computed, readonly } from 'vue' +import { + type GReaderConfig, + type Subscription, + type TreeNode, + type Article, + SPECIAL_ALL, + SPECIAL_STARRED, + STATE_READING_LIST, + STATE_STARRED +} from './types' +import * as api from './api' + +const CONFIG_KEY = 'ztools-rss:config' + +interface ReaderState { + config: GReaderConfig | null + authed: boolean + loading: boolean + error: string + tree: TreeNode[] + selectedStreamId: string + selectedNodeId: string + articles: Article[] + articlesContinuation: string | null + selectedArticleId: string + loadingMore: boolean + unreadMap: Record +} + +const state = reactive({ + config: null, + authed: false, + loading: false, + error: '', + tree: [], + selectedStreamId: '', + selectedNodeId: '', + articles: [], + articlesContinuation: null, + selectedArticleId: '', + loadingMore: false, + unreadMap: {} +}) + +function loadConfig(): GReaderConfig | null { + try { + const raw = window.ztools.dbStorage.getItem(CONFIG_KEY) + if (raw && raw.baseUrl && raw.username) return raw + } catch (_) { + // ignore + } + return null +} + +export function saveConfig(config: GReaderConfig): void { + window.ztools.dbStorage.setItem(CONFIG_KEY, config) + state.config = { ...config } +} + +function countUnread(id: string): number { + return state.unreadMap[id] || 0 +} + +function resolveIcon(baseUrl: string, iconUrl?: string): string | undefined { + if (!iconUrl) return undefined + if (/^https?:\/\//i.test(iconUrl)) return iconUrl + try { + const u = new URL(baseUrl) + return u.origin + (iconUrl.startsWith('/') ? iconUrl : '/' + iconUrl) + } catch (_) { + return undefined + } +} + +export function buildTree( + subscriptions: Subscription[], + unreadMap: Record, + baseUrl = '' +): TreeNode[] { + const totalUnread = unreadMap[STATE_READING_LIST] || 0 + const starredUnread = unreadMap[STATE_STARRED] || 0 + + const specials: TreeNode[] = [ + { + type: 'special', + id: SPECIAL_ALL, + title: '全部文章', + unread: totalUnread, + streamId: STATE_READING_LIST, + collapsed: false + }, + { + type: 'special', + id: SPECIAL_STARRED, + title: '星标文章', + unread: starredUnread, + streamId: STATE_STARRED, + collapsed: false + } + ] + + const folders = new Map() + const orphans: TreeNode[] = [] + const UNCATEGORIZED = '未分类' + + const subs = [...subscriptions].sort((a, b) => + (a.title || '').localeCompare(b.title || '', 'zh') + ) + + for (const sub of subs) { + const node: TreeNode = { + type: 'feed', + id: sub.id, + title: sub.title || sub.url || sub.id, + unread: unreadMap[sub.id] || 0, + streamId: sub.id, + iconUrl: resolveIcon(baseUrl, sub.iconUrl) + } + if (sub.categories && sub.categories.length > 0) { + const label = sub.categories[0].label || UNCATEGORIZED + const folderId = 'folder:' + label + let folder = folders.get(folderId) + if (!folder) { + folder = { + type: 'folder', + id: folderId, + title: label, + unread: 0, + streamId: 'user/-/label/' + label, + collapsed: false, + children: [] + } + folders.set(folderId, folder) + } + folder.children!.push(node) + folder.unread += node.unread + } else { + orphans.push(node) + } + } + + const folderNodes = [...folders.values()].sort((a, b) => + a.title.localeCompare(b.title, 'zh') + ) + return [...specials, ...folderNodes, ...orphans] +} + +export async function init(): Promise { + if (typeof window === 'undefined' || !window.ztools) { + state.error = '请在 ZTools 中打开本插件' + return + } + state.config = loadConfig() + if (state.config) { + await ensureAuthed() + } +} + +export async function ensureAuthed(): Promise { + if (!state.config) return false + if (state.authed && api.isAuthed()) return true + state.loading = true + state.error = '' + try { + await api.login(state.config) + state.authed = true + await refreshTree() + return true + } catch (e: any) { + state.error = e.message || String(e) + state.authed = false + return false + } finally { + state.loading = false + } +} + +export async function refreshTree(): Promise { + if (!state.config) return + try { + const [subResp, unreadResp] = await Promise.all([ + api.fetchSubscriptionList(state.config), + api.fetchUnreadCount(state.config) + ]) + const unreadMap: Record = {} + for (const u of unreadResp.unreadcounts || []) { + unreadMap[u.id] = (unreadMap[u.id] || 0) + u.count + } + state.unreadMap = unreadMap + const prevSelected = state.selectedStreamId + state.tree = buildTree(subResp.subscriptions || [], unreadMap, state.config.baseUrl) + // 默认选中“全部文章” + if (!prevSelected || !nodeExists(state.tree, prevSelected)) { + await selectNode(SPECIAL_ALL) + } else { + await selectStream(state.selectedNodeId, prevSelected) + } + } catch (e: any) { + state.error = e.message || String(e) + } +} + +/** 刷新当前订阅源:保留选中节点,重载树(未读计数)与当前流的文章 */ +export async function refreshCurrent(): Promise { + if (!state.config || !state.selectedStreamId) return + state.loading = true + state.error = '' + try { + const [subResp, unreadResp] = await Promise.all([ + api.fetchSubscriptionList(state.config), + api.fetchUnreadCount(state.config) + ]) + const unreadMap: Record = {} + for (const u of unreadResp.unreadcounts || []) { + unreadMap[u.id] = (unreadMap[u.id] || 0) + u.count + } + state.unreadMap = unreadMap + state.tree = buildTree(subResp.subscriptions || [], unreadMap, state.config.baseUrl) + // 保留当前选中流,仅重载文章列表 + state.articles = [] + state.articlesContinuation = null + state.selectedArticleId = '' + await loadArticles(true) + } catch (e: any) { + if (e.message && /登录已失效/.test(e.message)) state.authed = false + state.error = e.message || String(e) + } finally { + state.loading = false + } +} + +function nodeExists(tree: TreeNode[], streamId: string): boolean { + for (const n of tree) { + if (n.streamId === streamId) return true + if (n.children && nodeExists(n.children, streamId)) return true + } + return false +} + +export async function selectNode(nodeId: string): Promise { + const node = findNode(state.tree, nodeId) + if (!node) return + await selectStream(nodeId, node.streamId) +} + +async function selectStream(nodeId: string, streamId: string): Promise { + state.selectedNodeId = nodeId + state.selectedStreamId = streamId + state.selectedArticleId = '' + state.articles = [] + state.articlesContinuation = null + await loadArticles(true) +} + +function findNode(tree: TreeNode[], id: string): TreeNode | null { + for (const n of tree) { + if (n.id === id) return n + if (n.children) { + const r = findNode(n.children, id) + if (r) return r + } + } + return null +} + +export async function loadArticles(first: boolean): Promise { + if (!state.config || !state.selectedStreamId) return + if (first) state.loading = true + else state.loadingMore = true + state.error = '' + try { + const resp = await api.fetchArticles(state.config, state.selectedStreamId, { + onlyUnread: false, + n: first ? 40 : 40, + continuation: first ? undefined : state.articlesContinuation || undefined + }) + const mapped = api.mapArticles(resp) + if (first) state.articles = mapped + else { + const ids = new Set(state.articles.map((a) => a.id)) + state.articles.push(...mapped.filter((a) => !ids.has(a.id))) + } + state.articlesContinuation = resp.continuation || null + // 默认选中第一篇 + if (first && state.articles.length > 0 && !state.selectedArticleId) { + state.selectedArticleId = state.articles[0].id + void markCurrentRead() + } + } catch (e: any) { + if (e.message && /登录已失效/.test(e.message)) state.authed = false + state.error = e.message || String(e) + } finally { + state.loading = false + state.loadingMore = false + } +} + +export function selectArticle(id: string): void { + state.selectedArticleId = id + void markCurrentRead() +} + +export const currentArticle = computed
(() => { + return state.articles.find((a) => a.id === state.selectedArticleId) || null +}) + +async function markCurrentRead(): Promise { + if (!state.config || !state.selectedArticleId) return + const article = currentArticle.value + if (!article || article.read) return + article.read = true + // 更新未读计数 + const node = findNode(state.tree, state.selectedNodeId) + if (node && node.unread > 0) node.unread -= 1 + try { + await api.markItemRead(state.config, article.id, true) + } catch (_) { + // 静默失败,不打断阅读 + } +} + +export async function toggleStar(article: Article): Promise { + if (!state.config) return + const next = !article.starred + article.starred = next + try { + await api.markItemStarred(state.config, article.id, next) + } catch (_) { + article.starred = !next + } +} + +export async function markAllReadInCurrent(): Promise { + if (!state.config || !state.selectedStreamId) return + try { + await api.markStreamRead(state.config, state.selectedStreamId) + for (const a of state.articles) a.read = true + const node = findNode(state.tree, state.selectedNodeId) + if (node) node.unread = 0 + } catch (e: any) { + state.error = e.message || String(e) + } +} + +export function toggleFolder(nodeId: string): void { + const node = findNode(state.tree, nodeId) + if (node && node.type === 'folder') node.collapsed = !node.collapsed +} + +export function resetError(): void { + state.error = '' +} + +export function logout(): void { + api.logout() + state.authed = false + state.tree = [] + state.articles = [] + state.selectedStreamId = '' + state.selectedNodeId = '' + state.selectedArticleId = '' +} + +export const store = readonly(state) diff --git a/plugins/ztools-rss/src/Reader/types.ts b/plugins/ztools-rss/src/Reader/types.ts new file mode 100644 index 000000000..94e2b03a5 --- /dev/null +++ b/plugins/ztools-rss/src/Reader/types.ts @@ -0,0 +1,128 @@ +// Google Reader API 类型定义 + +export interface GReaderConfig { + /** API 根地址,如 https://example.com/api/greader.php(不带尾斜杠) */ + baseUrl: string + /** 用户名 */ + username: string + /** API 专用密码 */ + password: string +} + +export interface FeedCategory { + id: string + label: string +} + +export interface Subscription { + id: string + title: string + categories: FeedCategory[] + url?: string + htmlUrl?: string + iconUrl?: string +} + +export interface SubscriptionListResponse { + subscriptions: Subscription[] +} + +export interface UnreadCount { + id: string + count: number + newestItemTimestampUsec?: string +} + +export interface UnreadCountResponse { + max: number + unreadcounts: UnreadCount[] +} + +export interface ItemAlternate { + href: string + type?: string +} + +export interface ItemSummary { + content: string + direction?: string +} + +export interface ItemOrigin { + streamId?: string + title?: string + htmlUrl?: string + url?: string +} + +export interface StreamItem { + id: string + title?: string + summary?: ItemSummary + content?: ItemSummary + author?: string + published?: number + updated?: number + crawlTimeMsec?: string + timestampUsec?: string + categories?: string[] + alternate?: ItemAlternate[] + canonical?: ItemAlternate[] + origin?: ItemOrigin +} + +export interface StreamContentsResponse { + id: string + title?: string + updated?: number + continuation?: string + items: StreamItem[] +} + +export interface Tag { + id: string + type?: string +} + +export interface TagListResponse { + tags: Tag[] +} + +// 系统标签常量 +export const STATE_READING_LIST = 'user/-/state/com.google/reading-list' +export const STATE_STARRED = 'user/-/state/com.google/starred' +export const STATE_READ = 'user/-/state/com.google/read' +export const STATE_KEPT_UNREAD = 'user/-/state/com.google/kept-unread' + +// 特殊节点标识(用于树形结构) +export const SPECIAL_ALL = '__all__' +export const SPECIAL_STARRED = '__starred__' + +/** 统一的文章结构 */ +export interface Article { + id: string + title: string + content: string + author?: string + published: number + link?: string + feedId?: string + feedTitle?: string + feedUrl?: string + read: boolean + starred: boolean +} + +/** 树节点类型 */ +export type TreeNodeType = 'special' | 'folder' | 'feed' + +export interface TreeNode { + type: TreeNodeType + id: string + title: string + unread: number + streamId: string + iconUrl?: string + collapsed?: boolean + children?: TreeNode[] +} diff --git a/plugins/ztools-rss/src/Reader/utils.ts b/plugins/ztools-rss/src/Reader/utils.ts new file mode 100644 index 000000000..09dd7729f --- /dev/null +++ b/plugins/ztools-rss/src/Reader/utils.ts @@ -0,0 +1,34 @@ +export function formatDate(ts: number, withTime = false): string { + if (!ts) return '' + const d = new Date(ts) + const y = d.getFullYear() + const m = String(d.getMonth() + 1).padStart(2, '0') + const day = String(d.getDate()).padStart(2, '0') + if (!withTime) return `${y}-${m}-${day}` + const hh = String(d.getHours()).padStart(2, '0') + const mm = String(d.getMinutes()).padStart(2, '0') + return `${m}-${day} ${hh}:${mm}` +} + +export function isToday(ts: number): boolean { + if (!ts) return false + const d = new Date(ts) + const n = new Date() + return ( + d.getFullYear() === n.getFullYear() && + d.getMonth() === n.getMonth() && + d.getDate() === n.getDate() + ) +} + +export function relativeTime(ts: number): string { + if (!ts) return '' + const diff = Date.now() - ts + const min = 60 * 1000 + const hour = 60 * min + const day = 24 * hour + if (diff < hour) return Math.max(1, Math.floor(diff / min)) + ' 分钟前' + if (diff < day) return Math.floor(diff / hour) + ' 小时前' + if (diff < 7 * day) return Math.floor(diff / day) + ' 天前' + return formatDate(ts) +} diff --git a/plugins/ztools-rss/src/env.d.ts b/plugins/ztools-rss/src/env.d.ts new file mode 100644 index 000000000..b9b6ec465 --- /dev/null +++ b/plugins/ztools-rss/src/env.d.ts @@ -0,0 +1,36 @@ +/// +/// + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent, Record, unknown> + export default component +} + +interface RssFetchOptions { + method?: 'GET' | 'POST' + headers?: Record + body?: string | null + timeout?: number +} + +interface RssFetchResponse { + status: number + ok: boolean + headers: Record + text: string +} + +// Preload services 类型声明(对应 public/preload/services.js) +interface Services { + rssFetch: (url: string, opts?: RssFetchOptions) => Promise + fetchImageAsDataUrl: (url: string, referer?: string) => Promise +} + +declare global { + interface Window { + services: Services + } +} + +export {} diff --git a/plugins/ztools-rss/src/main.css b/plugins/ztools-rss/src/main.css new file mode 100644 index 000000000..b3a86bad8 --- /dev/null +++ b/plugins/ztools-rss/src/main.css @@ -0,0 +1,69 @@ +html, +body { + margin: 0; + padding: 0; + height: 100%; +} + +button { + border: none; + cursor: pointer; +} + +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +/* 主题变量 */ +@media (prefers-color-scheme: light) { + :root { + --rss-bg: #f5f6f7; + --rss-panel: #ffffff; + --rss-bar: #fafafa; + --rss-text: #1f2329; + --rss-text-soft: #4b5158; + --rss-text-dim: #9aa0a6; + --rss-border: #e4e6eb; + --rss-border-soft: #eef0f2; + --rss-hover: #f0f2f5; + --rss-input: #ffffff; + --rss-accent: #2e7df6; + --rss-accent-soft: #e8f1ff; + --rss-code-bg: #f5f6f7; + } + + ::-webkit-scrollbar-track-piece { + background-color: var(--rss-bg); + } + ::-webkit-scrollbar-thumb { + background-color: #c8ccd1; + border-radius: 4px; + } +} + +@media (prefers-color-scheme: dark) { + :root { + --rss-bg: #1c1d1f; + --rss-panel: #25262a; + --rss-bar: #202124; + --rss-text: #e8eaed; + --rss-text-soft: #b6b9bd; + --rss-text-dim: #7a7d82; + --rss-border: #36383d; + --rss-border-soft: #2c2d31; + --rss-hover: #2f3136; + --rss-input: #2a2b2f; + --rss-accent: #6ea8ff; + --rss-accent-soft: #2a3a55; + --rss-code-bg: #1c1d1f; + } + + ::-webkit-scrollbar-track-piece { + background-color: var(--rss-bg); + } + ::-webkit-scrollbar-thumb { + background-color: #4a4d52; + border-radius: 4px; + } +} diff --git a/plugins/ztools-rss/src/main.ts b/plugins/ztools-rss/src/main.ts new file mode 100644 index 000000000..8a3c77bc5 --- /dev/null +++ b/plugins/ztools-rss/src/main.ts @@ -0,0 +1,5 @@ +import { createApp } from 'vue' +import './main.css' +import App from './App.vue' + +createApp(App).mount('#app') diff --git a/plugins/ztools-rss/tsconfig.json b/plugins/ztools-rss/tsconfig.json new file mode 100644 index 000000000..39b73bc6f --- /dev/null +++ b/plugins/ztools-rss/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "strict": false, + "noImplicitAny": false, + "types": ["@ztools-center/ztools-api-types"] + }, + "include": ["src"] +} diff --git a/plugins/ztools-rss/vite.config.js b/plugins/ztools-rss/vite.config.js new file mode 100644 index 000000000..e2753e5ae --- /dev/null +++ b/plugins/ztools-rss/vite.config.js @@ -0,0 +1,30 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { readFileSync, writeFileSync } from 'node:fs' +import { resolve } from 'node:path' + +// 构建时剥离 plugin.json 的 development 字段 +// development.main 指向 dev server,仅用于 npm run dev;打包产物不应携带 +function stripDevConfig() { + return { + name: 'strip-plugin-dev-config', + closeBundle() { + const file = resolve(__dirname, 'dist/plugin.json') + try { + const json = JSON.parse(readFileSync(file, 'utf-8')) + if (json.development) { + delete json.development + writeFileSync(file, JSON.stringify(json, null, 2), 'utf-8') + } + } catch (_) { + /* ignore */ + } + } + } +} + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [vue(), stripDevConfig()], + base: './' +})