pixivflow 2.31.0 → 2.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -21
- package/dist/package.json +1 -1
- package/dist/version.js +1 -1
- package/dist/webui/package.json +1 -1
- package/dist/webui/server/server-static.d.ts +9 -2
- package/dist/webui/server/server-static.js +25 -21
- package/dist/webui/server/server.js +15 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,10 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
**Pixiv 下载、筛选与自动收集工具。**
|
|
6
6
|
|
|
7
|
-
可以直接下载单个 Pixiv
|
|
8
|
-
批量筛选,并通过 scheduler 定时自动收集。结果既能永久保存在本地,也能按需通过 HTTP
|
|
9
|
-
可靠交付给其他服务——下游是可选的,PixivFlow 自己就能跑完「发现 → 筛选 → 下载 → 保存」
|
|
10
|
-
的完整链路。
|
|
7
|
+
可以直接下载单个 Pixiv 作品(插画、小说、动图),也可以按标签、热度、日期和收藏数等条件批量筛选,并通过 scheduler 定时自动收集。结果既能永久保存在本地,也能按需通过 HTTP 可靠交付给其他服务——下游是可选的,PixivFlow 自己就能跑完「发现 → 筛选 → 下载 → 保存」的完整链路。
|
|
11
8
|
|
|
12
9
|
[](https://www.npmjs.com/package/pixivflow)
|
|
13
10
|
[](https://nodejs.org/)
|
|
@@ -191,33 +188,34 @@ Email、Telegram、Discord、ntfy 等渠道;PixivFlow 不实现这些通知协
|
|
|
191
188
|
热重载。完整双 Bot 缓存投递模板见
|
|
192
189
|
[`config/fly-two-bots.example.json`](config/fly-two-bots.example.json)。
|
|
193
190
|
|
|
194
|
-
###
|
|
191
|
+
### 投递事务发件箱(Outbox)
|
|
195
192
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
`maxAttempts` 进 dead 状态并在投递账记 `failed`。进程崩溃 / 机器挂起后重启,残留的
|
|
200
|
-
`processing` 租约过期即被新进程接管,重试同一个幂等意图——下游按
|
|
201
|
-
`idempotent_replay`(同键,ACK 丢失重试)或 `duplicate_existing`(历史重复)收敛,
|
|
202
|
-
频道里仍然只有一条消息。旧版文件型 `delivery-outbox/*.json` 清单在启动时自动一次性
|
|
203
|
-
迁移进 SQLite,迁移幂等。「今天没有可投稿内容」这类通知与内容投递走同一张表、独立泵送,
|
|
204
|
-
审核端暂时挂掉不会互相阻塞。
|
|
193
|
+
投递和通知都先写入 SQLite 发件箱(outbox),再由后台 worker 泵送:对外副作用
|
|
194
|
+
(一次内容投递、一条通知)在 `outbox` 表各占一行,带幂等键和行级租约,保证
|
|
195
|
+
至少一次执行、最终只产生一次可见效果。
|
|
205
196
|
|
|
206
|
-
|
|
207
|
-
|
|
197
|
+
- 失败会自动重试:指数退避(默认 5 分钟起步、最长 6 小时)。
|
|
198
|
+
- 超过 `maxAttempts` 进入 `dead` 状态,可用 `pixivflow outbox` 查看和重试。
|
|
199
|
+
- 进程崩溃/重启后,残留的 `processing` 租约过期后会被新进程接管,重复发送同一个
|
|
200
|
+
幂等意图;下游按幂等键收敛,不会在频道里出现重复消息。
|
|
201
|
+
- 旧的文件型 `delivery-outbox/*.json` 会在启动时自动一次性迁移进 SQLite(迁移幂等)。
|
|
202
|
+
- 「今天没有可投稿内容」这类通知与内容投递走同一张表、独立泵送,互不阻塞。
|
|
203
|
+
|
|
204
|
+
配置 `readinessUrl` 后,worker 每次认领都会先检查依赖 `/ready`;非 2xx 只把 row
|
|
205
|
+
放回 pending,不增加 attempt。dead letter 通过正式 CLI 管理:
|
|
208
206
|
|
|
209
207
|
```bash
|
|
210
208
|
pixivflow outbox list --status dead
|
|
211
209
|
pixivflow outbox inspect <id>
|
|
212
|
-
pixivflow outbox retry <id> #
|
|
210
|
+
pixivflow outbox retry <id> # 只重试 dead row,保留幂等键
|
|
213
211
|
pixivflow outbox retry --dead
|
|
214
212
|
pixivflow outbox cancel <id> # 只取消尚未执行的 row
|
|
215
213
|
```
|
|
216
214
|
|
|
217
|
-
`run-once` 会重新执行下载计划,不等价于 outbox replay
|
|
218
|
-
`next_attempt_at
|
|
219
|
-
租约、pending 投递、dead 行,`--repair` 收敛)与 `pixivflow reconcile
|
|
220
|
-
|
|
215
|
+
`run-once` 会重新执行下载计划,不等价于 outbox replay,不要手动改 SQLite 的
|
|
216
|
+
`next_attempt_at`。长期体检与收敛用 `pixivflow doctor`(卡住的 slot/outbox
|
|
217
|
+
租约、pending 投递、dead 行,`--repair` 收敛)与 `pixivflow reconcile`
|
|
218
|
+
(把下游已确认的历史重复登记进投递账本,默认 dry-run)。
|
|
221
219
|
|
|
222
220
|
## 常用命令
|
|
223
221
|
|
package/dist/package.json
CHANGED
package/dist/version.js
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.BUILD = void 0;
|
|
4
4
|
// GENERATED by scripts/write-version.js — do not edit manually.
|
|
5
|
-
exports.BUILD = { version: '2.
|
|
5
|
+
exports.BUILD = { version: '2.33.0', commit: 'ceb9c345fa1b' };
|
|
6
6
|
//# sourceMappingURL=version.js.map
|
package/dist/webui/package.json
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { Express } from 'express';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Inject a one-time dismissal security notice into the served SPA index.html
|
|
4
|
+
* when WebUI basic auth is NOT configured (WEBUI_USERNAME / WEBUI_PASSWORD).
|
|
5
|
+
* First local start needs no credentials; the banner only reminds operators
|
|
6
|
+
* to set them before exposing the server beyond localhost.
|
|
4
7
|
*/
|
|
5
|
-
export declare function
|
|
8
|
+
export declare function injectAuthBanner(indexHtml: string, basicAuthEnabled: boolean): string;
|
|
9
|
+
/**
|
|
10
|
+
* Setup static file serving for SPA frontend (injects auth-disabled notice when basic auth is off).
|
|
11
|
+
*/
|
|
12
|
+
export declare function setupStaticFiles(app: Express, staticPath?: string, basicAuthEnabled?: boolean): void;
|
|
6
13
|
//# sourceMappingURL=server-static.d.ts.map
|
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.injectAuthBanner = injectAuthBanner;
|
|
6
7
|
exports.setupStaticFiles = setupStaticFiles;
|
|
7
8
|
const express_1 = __importDefault(require("express"));
|
|
8
9
|
const path_1 = __importDefault(require("path"));
|
|
@@ -70,9 +71,27 @@ function getPackageVersion() {
|
|
|
70
71
|
return 'unknown';
|
|
71
72
|
}
|
|
72
73
|
/**
|
|
73
|
-
*
|
|
74
|
+
* Inject a one-time dismissal security notice into the served SPA index.html
|
|
75
|
+
* when WebUI basic auth is NOT configured (WEBUI_USERNAME / WEBUI_PASSWORD).
|
|
76
|
+
* First local start needs no credentials; the banner only reminds operators
|
|
77
|
+
* to set them before exposing the server beyond localhost.
|
|
74
78
|
*/
|
|
75
|
-
function
|
|
79
|
+
function injectAuthBanner(indexHtml, basicAuthEnabled) {
|
|
80
|
+
if (basicAuthEnabled || indexHtml.includes('pixivflow-auth-banner')) {
|
|
81
|
+
return indexHtml;
|
|
82
|
+
}
|
|
83
|
+
const banner = [
|
|
84
|
+
'<div id="pixivflow-auth-banner" style="position:sticky;top:0;z-index:9999;display:flex;align-items:center;gap:12px;padding:8px 16px;background:#fff7e0;border-bottom:1px solid #f0d99a;color:#6b5300;font:14px/1.5 -apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;">',
|
|
85
|
+
' <span>⚠️ <strong>安全提醒 / Security:</strong> WebUI 认证未启用(WEBUI_USERNAME / WEBUI_PASSWORD 未设置)。首次本地使用无需设置;若需对外暴露,请先设置用户名密码并重启服务。</span>',
|
|
86
|
+
' <button type="button" title="知道了 / Got it" style="margin-left:auto;border:0;background:transparent;color:#6b5300;cursor:pointer;font-size:18px;line-height:1;" onclick="this.parentElement.remove()">✕</button>',
|
|
87
|
+
'</div>',
|
|
88
|
+
].join('\n');
|
|
89
|
+
return indexHtml.replace('</body>', banner + '\n</body>');
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Setup static file serving for SPA frontend (injects auth-disabled notice when basic auth is off).
|
|
93
|
+
*/
|
|
94
|
+
function setupStaticFiles(app, staticPath, basicAuthEnabled = false) {
|
|
76
95
|
if (!staticPath) {
|
|
77
96
|
// Root path handler when static files are not configured
|
|
78
97
|
app.get('/', (req, res) => {
|
|
@@ -95,6 +114,7 @@ function setupStaticFiles(app, staticPath) {
|
|
|
95
114
|
}
|
|
96
115
|
const resolvedStaticPath = path_1.default.resolve(staticPath);
|
|
97
116
|
const indexPath = path_1.default.join(resolvedStaticPath, 'index.html');
|
|
117
|
+
let servedIndexHtml = null;
|
|
98
118
|
// Verify static path and index.html exist
|
|
99
119
|
if (!fs_1.default.existsSync(resolvedStaticPath)) {
|
|
100
120
|
logger_1.logger.warn('Static path does not exist', { path: resolvedStaticPath });
|
|
@@ -107,6 +127,7 @@ function setupStaticFiles(app, staticPath) {
|
|
|
107
127
|
}
|
|
108
128
|
else {
|
|
109
129
|
logger_1.logger.info('Serving static files', { path: resolvedStaticPath });
|
|
130
|
+
servedIndexHtml = injectAuthBanner(fs_1.default.readFileSync(indexPath, 'utf8'), basicAuthEnabled);
|
|
110
131
|
}
|
|
111
132
|
// Serve static files (CSS, JS, images, etc.)
|
|
112
133
|
app.use(express_1.default.static(resolvedStaticPath, {
|
|
@@ -116,15 +137,7 @@ function setupStaticFiles(app, staticPath) {
|
|
|
116
137
|
// Explicitly handle root path first
|
|
117
138
|
app.get('/', (req, res, next) => {
|
|
118
139
|
if (fs_1.default.existsSync(indexPath)) {
|
|
119
|
-
res.
|
|
120
|
-
if (err) {
|
|
121
|
-
logger_1.logger.error('Failed to send index.html for root path', {
|
|
122
|
-
error: err.message,
|
|
123
|
-
path: indexPath,
|
|
124
|
-
});
|
|
125
|
-
next(err);
|
|
126
|
-
}
|
|
127
|
-
});
|
|
140
|
+
res.type('html').send(servedIndexHtml);
|
|
128
141
|
}
|
|
129
142
|
else {
|
|
130
143
|
logger_1.logger.warn('index.html not found, cannot serve frontend', {
|
|
@@ -145,16 +158,7 @@ function setupStaticFiles(app, staticPath) {
|
|
|
145
158
|
}
|
|
146
159
|
// Send index.html for all other routes (SPA routing)
|
|
147
160
|
if (fs_1.default.existsSync(indexPath)) {
|
|
148
|
-
res.
|
|
149
|
-
if (err) {
|
|
150
|
-
logger_1.logger.error('Failed to send index.html for SPA route', {
|
|
151
|
-
error: err.message,
|
|
152
|
-
path: indexPath,
|
|
153
|
-
requestedPath: req.path,
|
|
154
|
-
});
|
|
155
|
-
next(err);
|
|
156
|
-
}
|
|
157
|
-
});
|
|
161
|
+
res.type('html').send(servedIndexHtml);
|
|
158
162
|
}
|
|
159
163
|
else {
|
|
160
164
|
next();
|
|
@@ -94,10 +94,24 @@ class WebUIServer {
|
|
|
94
94
|
else {
|
|
95
95
|
logger_1.logger.warn('WebUI authentication is DISABLED (set WEBUI_USERNAME and WEBUI_PASSWORD to enable basic auth)');
|
|
96
96
|
}
|
|
97
|
+
// Fail closed for accidental public exposure: a non-loopback bind without
|
|
98
|
+
// credentials would expose config secrets, downloaded files and arbitrary
|
|
99
|
+
// download/exec controls to anyone. Local bind (localhost/127.0.0.1/::1)
|
|
100
|
+
// remains credential-free. Operators who *really* want a public
|
|
101
|
+
// unauthenticated server must opt in explicitly.
|
|
102
|
+
const hostIsLoopback = (h) => h === 'localhost' ||
|
|
103
|
+
h === '127.0.0.1' ||
|
|
104
|
+
h === '::1' ||
|
|
105
|
+
h.startsWith('127.');
|
|
106
|
+
if (!this.basicAuthEnabled && !hostIsLoopback(this.host) && process.env.WEBUI_ALLOW_PUBLIC_NO_AUTH !== 'true') {
|
|
107
|
+
throw new Error(`Refusing to bind WebUI to non-loopback host "${this.host}" without authentication. ` +
|
|
108
|
+
`Set WEBUI_USERNAME and WEBUI_PASSWORD (recommended), or set ` +
|
|
109
|
+
`WEBUI_ALLOW_PUBLIC_NO_AUTH=true only if you fully understand the risk.`);
|
|
110
|
+
}
|
|
97
111
|
// Setup API routes
|
|
98
112
|
(0, server_routes_1.setupRoutes)(this.app);
|
|
99
113
|
// Setup static file serving
|
|
100
|
-
(0, server_static_1.setupStaticFiles)(this.app, options.staticPath);
|
|
114
|
+
(0, server_static_1.setupStaticFiles)(this.app, options.staticPath, this.basicAuthEnabled);
|
|
101
115
|
// Error handler (must be last)
|
|
102
116
|
this.app.use(server_middleware_1.errorHandler);
|
|
103
117
|
// Create HTTP or HTTPS server (TLS opt-in via WEBUI_TLS_CERT/KEY)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pixivflow",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.33.0",
|
|
4
4
|
"description": "🎨 Pixiv 下载、筛选与自动收集工具 - 批量下载插画和小说、按标签/热度/日期筛选、定时任务与可靠 HTTP 交付 | Pixiv downloader and automation toolkit with filtering, scheduling and reliable HTTP delivery",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|