pixivflow 2.35.0 → 2.37.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.
Files changed (54) hide show
  1. package/dist/download/novelMarkers.d.ts +18 -2
  2. package/dist/package.json +1 -1
  3. package/dist/version.js +1 -1
  4. package/dist/webui/package.json +1 -1
  5. package/dist/webui/routes/handlers/scheduler-handlers.d.ts +44 -0
  6. package/dist/webui/routes/handlers/scheduler-handlers.js +285 -8
  7. package/dist/webui/routes/scheduler.js +4 -0
  8. package/dist/webui/utils/error-codes.d.ts +2 -0
  9. package/dist/webui/utils/error-codes.js +2 -0
  10. package/package.json +1 -1
  11. package/webui-frontend/dist/assets/ActionButton-bmITMO8t.js +2 -0
  12. package/webui-frontend/dist/assets/{CheckCircleOutlined-BS_TxY3k.js → CheckCircleOutlined-DXxMdP08.js} +2 -2
  13. package/webui-frontend/dist/assets/{ClearOutlined-uIvWLBII.js → ClearOutlined-CVkP1OPo.js} +2 -2
  14. package/webui-frontend/dist/assets/{CloseCircleOutlined-B45wvGw1.js → CloseCircleOutlined-DGYEx-rx.js} +2 -2
  15. package/webui-frontend/dist/assets/{Config-UjgCA3Jq.js → Config-BFBZgSvI.js} +3 -3
  16. package/webui-frontend/dist/assets/{Dashboard-hIW1nAN9.js → Dashboard-CHiRV77D.js} +2 -2
  17. package/webui-frontend/dist/assets/{DeleteOutlined-ByQuZQbZ.js → DeleteOutlined-DwsLtl_I.js} +2 -2
  18. package/webui-frontend/dist/assets/Download-BY0RQ0yW.js +2 -0
  19. package/webui-frontend/dist/assets/{Files-DCMyqujj.js → Files-qrEyGaCr.js} +3 -3
  20. package/webui-frontend/dist/assets/{PictureOutlined-CSEKKDH9.js → PictureOutlined-B5UcfS3J.js} +2 -2
  21. package/webui-frontend/dist/assets/{SortDescendingOutlined-BS5Vnvjg.js → SortDescendingOutlined-CIPBhzNI.js} +2 -2
  22. package/webui-frontend/dist/assets/{Table-CvqrzZX2.js → Table-COdSkj3k.js} +7 -7
  23. package/webui-frontend/dist/assets/{ThunderboltOutlined-DhgtXoNM.js → ThunderboltOutlined-BlHGTLpd.js} +2 -2
  24. package/webui-frontend/dist/assets/{UrlDownload-BHF2qmgI.js → UrlDownload-Dxj1MDnw.js} +2 -2
  25. package/webui-frontend/dist/assets/{dateUtils-CEt5gAeU.js → dateUtils-Dh-zVUtr.js} +2 -2
  26. package/webui-frontend/dist/assets/{errorCodeTranslator-Xn_Z9qOh.js → errorCodeTranslator-D5vahyqQ.js} +2 -2
  27. package/webui-frontend/dist/assets/{index-DiwSYMD5.js → index-B84Vd9_M.js} +2 -2
  28. package/webui-frontend/dist/assets/index-BhmvYPFd.js +19 -0
  29. package/webui-frontend/dist/assets/{index-NSEOZ6G5.js → index-Bp5yfy4e.js} +3 -3
  30. package/webui-frontend/dist/assets/{index-HJggy9SZ.js → index-BtXc5-H2.js} +15 -15
  31. package/webui-frontend/dist/assets/{index-eiyXJZci.js → index-BvQQNYa5.js} +2 -2
  32. package/webui-frontend/dist/assets/index-C2qRwckx.js +2 -0
  33. package/webui-frontend/dist/assets/{index-ZRYuscJf.js → index-CFWcxkyB.js} +2 -2
  34. package/webui-frontend/dist/assets/{index-DpfbKoAX.js → index-CZ44NQeD.js} +2 -2
  35. package/webui-frontend/dist/assets/{index-Cg2Wi2pt.js → index-CZXbP5HB.js} +2 -2
  36. package/webui-frontend/dist/assets/{index-6mtxs9yc.js → index-CZlT-55J.js} +2 -2
  37. package/webui-frontend/dist/assets/index-Cfg0ICF_.js +20 -0
  38. package/webui-frontend/dist/assets/{index-D_qUS2GR.js → index-D1KRxkVR.js} +2 -2
  39. package/webui-frontend/dist/assets/{index-D4mbR-Vx.js → index-D8iUeVsP.js} +2 -2
  40. package/webui-frontend/dist/assets/{index-C66Xw4Fs.js → index-DcqvG64t.js} +2 -2
  41. package/webui-frontend/dist/assets/index-eeF42ACG.js +2 -0
  42. package/webui-frontend/dist/assets/{index-mHLzQpzS.js → index-xZubvNRw.js} +6 -6
  43. package/webui-frontend/dist/assets/row-DaMDSIar.js +2 -0
  44. package/webui-frontend/dist/assets/{useAuth-4erTEEjT.js → useAuth-BfM9bg0I.js} +3 -3
  45. package/webui-frontend/dist/assets/{useConfig-BmhXnAiM.js → useConfig-nFBmSW5k.js} +2 -2
  46. package/webui-frontend/dist/assets/{useDownload-DhuPPKqw.js → useDownload-ChmDqdbc.js} +2 -2
  47. package/webui-frontend/dist/assets/{useErrorHandler-Bcb3l7ju.js → useErrorHandler-MJ8OxkB4.js} +2 -2
  48. package/webui-frontend/dist/index.html +1 -1
  49. package/webui-frontend/dist/assets/Download-DdswOGH7.js +0 -2
  50. package/webui-frontend/dist/assets/index-B4zZ11c-.js +0 -20
  51. package/webui-frontend/dist/assets/index-BlXTpFS3.js +0 -2
  52. package/webui-frontend/dist/assets/index-BvgYmQcZ.js +0 -2
  53. package/webui-frontend/dist/assets/index-DV4iHo-9.js +0 -19
  54. package/webui-frontend/dist/assets/row-CQQO4x82.js +0 -2
@@ -1,4 +1,17 @@
1
- import type { PixivNovelTextResponse } from '@redtidev/pixiv-client';
1
+ /**
2
+ * Structural shape used by the webview v2 rich-novel text payload. The shipped
3
+ * `@redtidev/pixiv-client` dist.d.ts lags the package source and does not
4
+ * declare these fields, so we keep a minimal local view instead of depending on
5
+ * a dist export that does not exist in published builds.
6
+ */
7
+ export interface NovelUploadedImage {
8
+ urls: Record<string, string | undefined>;
9
+ }
10
+ export interface NovelIllustRef {
11
+ illust: {
12
+ images: Record<string, string | undefined>;
13
+ };
14
+ }
2
15
  /**
3
16
  * Single-pass novel text tokenizer, ported from PixEz
4
17
  * (Notsfsssf/pixez-flutter lib/page/novel/viewer/image_text.dart,
@@ -48,7 +61,10 @@ export declare function scanNovelMarkers(source: string): NovelMarker[];
48
61
  * `unavailable` = the source is missing in the webview payload (deleted/no
49
62
  * permission) so it will never succeed; `pending` = resolvable + downloadable.
50
63
  */
51
- export declare function extractNovelAssets(text: string, response?: Pick<PixivNovelTextResponse, 'images' | 'illusts'>): NovelAsset[];
64
+ export declare function extractNovelAssets(text: string, response?: {
65
+ images?: Record<string, NovelUploadedImage | undefined>;
66
+ illusts?: Record<string, NovelIllustRef | null>;
67
+ }): NovelAsset[];
52
68
  /** Minimal self-check for the scanner's double-bracket handling. */
53
69
  export declare function demo(): void;
54
70
  /**
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "type": "commonjs",
3
3
  "name": "pixivflow",
4
- "version": "2.35.0",
4
+ "version": "2.37.0",
5
5
  "private": true
6
6
  }
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.35.0', commit: '209644ebd185' };
5
+ exports.BUILD = { version: '2.37.0', commit: 'a17343519198' };
6
6
  //# sourceMappingURL=version.js.map
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "type": "commonjs",
3
3
  "name": "pixivflow-webui-backend",
4
- "version": "2.35.0",
4
+ "version": "2.37.0",
5
5
  "description": "PixivFlow WebUI Backend - CommonJS module"
6
6
  }
@@ -5,4 +5,48 @@ import { Request, Response } from 'express';
5
5
  * projection of the existing Slot Ledger, not a second state system.
6
6
  */
7
7
  export declare function listRecentSlots(req: Request, res: Response): Promise<void>;
8
+ /**
9
+ * Recovery admission projection (read-only). Retry semantics still live in the
10
+ * scheduler's own admission rules; this only labels what the WebUI may offer.
11
+ * - system `failed` → retryable (normal / relaxed)
12
+ * - `no_candidate`/`duplicate` → normal business outcome; relaxed retry is the
13
+ * only semantically useful action (soft-scope widening), never a blind retry
14
+ * - `submitted`/`pending`/`running` → not retryable
15
+ */
16
+ export declare function recoveryAdmission(status: string, terminalReasonCode: string | null): {
17
+ retryable: boolean;
18
+ relaxedRetryAllowed: boolean;
19
+ retryableReason: string;
20
+ };
21
+ /**
22
+ * GET /api/scheduler/executions — read-only Execution projection over the
23
+ * durable Slot Ledger. Execution Truth stays in schedule_slots + items; this
24
+ * endpoint only shapes it for the WebUI (no new state source).
25
+ */
26
+ export declare function listExecutions(req: Request, res: Response): Promise<void>;
27
+ /**
28
+ * GET /api/scheduler/slots/:slotId/logs
29
+ *
30
+ * Correlated log view: filters the process log file by the slot id and its
31
+ * target ids so operators can replay exactly the lines for one occurrence.
32
+ * Pure filtering of existing structured logger output, no new store.
33
+ */
34
+ export declare function getSlotLogs(req: Request, res: Response): Promise<void>;
35
+ /**
36
+ * POST /api/scheduler/targets/:targetId/recover
37
+ *
38
+ * WebUI Control Center Recovery action. This is a thin server-side proxy to the
39
+ * EXISTING authenticated scheduler dispatcher (`POST /internal/targets/:targetId/recover`),
40
+ * so the browser never sees the trigger token and the business admission rules
41
+ * live exactly where scheduled/manual recovery already lives. Deliberately
42
+ * disabled with a clear 503 when the proxy URL/token env vars are absent —
43
+ * read-only Scheduler remains fully usable.
44
+ */
45
+ export declare function recoverTarget(req: Request, res: Response): Promise<void>;
46
+ /**
47
+ * GET /api/scheduler/targets/:targetId/recover/:requestId
48
+ *
49
+ * Poll the durable recovery slot outcome (same contract the review chain uses).
50
+ */
51
+ export declare function recoverStatus(req: Request, res: Response): Promise<void>;
8
52
  //# sourceMappingURL=scheduler-handlers.d.ts.map
@@ -1,10 +1,65 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.listRecentSlots = listRecentSlots;
7
+ exports.recoveryAdmission = recoveryAdmission;
8
+ exports.listExecutions = listExecutions;
9
+ exports.getSlotLogs = getSlotLogs;
10
+ exports.recoverTarget = recoverTarget;
11
+ exports.recoverStatus = recoverStatus;
12
+ const fs_1 = require("fs");
13
+ const path_1 = __importDefault(require("path"));
4
14
  const Database_1 = require("../../../storage/Database");
5
15
  const config_1 = require("../../../config");
6
16
  const logger_1 = require("../../../logger");
7
17
  const error_codes_1 = require("../../utils/error-codes");
18
+ const RECOVERY_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
19
+ const TARGET_ID_SAFE = /^[A-Za-z0-9._-]{1,80}$/;
20
+ function recoveryBaseUrl() {
21
+ return process.env.SCHEDULER_TRIGGER_URL?.trim() || process.env.PIXIVFLOW_TRIGGER_BASE_URL?.trim() || null;
22
+ }
23
+ function recoveryToken() {
24
+ return process.env.SCHEDULER_TRIGGER_TOKEN?.trim() || null;
25
+ }
26
+ async function forwardRecovery(res, targetId, pathAndQuery, init) {
27
+ const base = recoveryBaseUrl();
28
+ const token = recoveryToken();
29
+ if (!base || !token) {
30
+ res503();
31
+ return;
32
+ }
33
+ try {
34
+ const upstream = await fetch(`${base.replace(/\/+$/, '')}${pathAndQuery}`, {
35
+ method: init.method,
36
+ headers: {
37
+ Authorization: `Bearer ${token}`,
38
+ ...(init.body ? { 'Content-Type': 'application/json' } : {}),
39
+ },
40
+ body: init.body,
41
+ });
42
+ const body = await upstream.json().catch(() => null);
43
+ // Bubble the upstream status + body up unchanged; the client only ever
44
+ // talks to the same honest dispatcher the external clock talks to.
45
+ res.status(upstream.status).json(body && typeof body === 'object' ? body : { status: upstream.ok ? 'ok' : 'error' });
46
+ }
47
+ catch (error) {
48
+ const message = error instanceof Error ? error.message : String(error);
49
+ logger_1.logger.error('WebUI recovery proxy failed', { targetId, error: { message } });
50
+ res.status(502).json({
51
+ errorCode: error_codes_1.ErrorCode.SCHEDULER_RECOVERY_FAILED,
52
+ message: 'recovery proxy request failed',
53
+ });
54
+ }
55
+ function res503() {
56
+ res.status(503).json({
57
+ errorCode: error_codes_1.ErrorCode.SCHEDULER_RECOVERY_UNAVAILABLE,
58
+ message: 'Configure SCHEDULER_TRIGGER_URL (or PIXIVFLOW_TRIGGER_BASE_URL) and ' +
59
+ 'SCHEDULER_TRIGGER_TOKEN to enable WebUI recovery actions.',
60
+ });
61
+ }
62
+ }
8
63
  /**
9
64
  * GET /api/scheduler — read-only recent slot occurrences (WebUI Control Center
10
65
  * Phase 1: Scheduler panel). Deliberately no writes and no secrets: this is a
@@ -32,16 +87,11 @@ async function listRecentSlots(req, res) {
32
87
  timezone: slot.timezone,
33
88
  triggerSource: slot.triggerSource,
34
89
  recoveryRequestId: slot.recoveryRequestId ?? null,
90
+ recoveryMode: slot.recoveryMode ?? null,
35
91
  startedAt: slot.startedAt,
36
92
  completedAt: slot.completedAt,
37
- targets: database.slots.getCells(slot.id).map((cell) => ({
38
- targetId: cell.targetId,
39
- workType: cell.workType,
40
- status: cell.status,
41
- workId: cell.workId,
42
- terminalReasonCode: cell.terminalReasonCode,
43
- reason: cell.terminalReasonMessage ?? null,
44
- })),
93
+ lastError: slot.lastError,
94
+ targets: database.slots.getCells(slot.id).map(cellProjection),
45
95
  }));
46
96
  database.close();
47
97
  database = null;
@@ -61,4 +111,231 @@ async function listRecentSlots(req, res) {
61
111
  res.status(500).json({ errorCode: error_codes_1.ErrorCode.SCHEDULER_LIST_FAILED });
62
112
  }
63
113
  }
114
+ function parseCandidateReport(report) {
115
+ return report && typeof report === 'object' ? report : null;
116
+ }
117
+ function cellProjection(cell) {
118
+ return {
119
+ targetId: cell.targetId,
120
+ workType: cell.workType,
121
+ status: cell.status,
122
+ workId: cell.workId,
123
+ terminalReasonCode: cell.terminalReasonCode,
124
+ reason: cell.terminalReasonMessage ?? null,
125
+ candidateReport: parseCandidateReport(cell.candidateReport),
126
+ attemptCount: cell.attemptCount,
127
+ fallbackStage: cell.fallback_stage,
128
+ completedAt: cell.completedAt,
129
+ };
130
+ }
131
+ /**
132
+ * Recovery admission projection (read-only). Retry semantics still live in the
133
+ * scheduler's own admission rules; this only labels what the WebUI may offer.
134
+ * - system `failed` → retryable (normal / relaxed)
135
+ * - `no_candidate`/`duplicate` → normal business outcome; relaxed retry is the
136
+ * only semantically useful action (soft-scope widening), never a blind retry
137
+ * - `submitted`/`pending`/`running` → not retryable
138
+ */
139
+ function recoveryAdmission(status, terminalReasonCode) {
140
+ const t = (terminalReasonCode ?? '').toLowerCase();
141
+ if (status === 'failed') {
142
+ return { retryable: true, relaxedRetryAllowed: true, retryableReason: 'system failure' };
143
+ }
144
+ if (status === 'no_candidate' || status === 'duplicate') {
145
+ const noContent = t === 'duplicate_exhausted' || t === 'no_candidate' || t === 'duplicate';
146
+ return {
147
+ retryable: !noContent,
148
+ relaxedRetryAllowed: true,
149
+ retryableReason: noContent
150
+ ? 'non-retryable business outcome (no new content)'
151
+ : 'normal retry applies',
152
+ };
153
+ }
154
+ return { retryable: false, relaxedRetryAllowed: false, retryableReason: 'non-terminal or already-submitted' };
155
+ }
156
+ /**
157
+ * GET /api/scheduler/executions — read-only Execution projection over the
158
+ * durable Slot Ledger. Execution Truth stays in schedule_slots + items; this
159
+ * endpoint only shapes it for the WebUI (no new state source).
160
+ */
161
+ async function listExecutions(req, res) {
162
+ const limit = Math.min(Math.max(Number(req.query.limit ?? 20) || 20, 1), 100);
163
+ const targetFilter = typeof req.query.targetId === 'string' ? req.query.targetId.trim() : '';
164
+ const statusFilter = typeof req.query.status === 'string' ? req.query.status.trim().toLowerCase() : '';
165
+ let database = null;
166
+ try {
167
+ const configPath = (0, config_1.getConfigPath)();
168
+ const config = (0, config_1.loadConfig)(configPath);
169
+ if (!config.storage?.databasePath) {
170
+ res.status(400).json({ errorCode: error_codes_1.ErrorCode.SCHEDULER_LIST_FAILED, message: 'database not configured' });
171
+ return;
172
+ }
173
+ database = new Database_1.Database(config.storage.databasePath);
174
+ database.migrate();
175
+ const slots = database.slots.getRecentSlots(Math.max(limit, 50));
176
+ const executions = slots
177
+ .flatMap((slot) => database.slots.getCells(slot.id).map((cell) => {
178
+ const cellView = cellProjection(cell);
179
+ const admission = recoveryAdmission(cell.status, cell.terminalReasonCode);
180
+ return {
181
+ executionId: `${slot.id}:${cell.targetId}`,
182
+ slotId: slot.id,
183
+ scheduleId: slot.scheduleId,
184
+ targetId: cell.targetId,
185
+ workType: cell.workType,
186
+ status: cell.status,
187
+ terminalReasonCode: cell.terminalReasonCode,
188
+ message: cell.terminalReasonMessage,
189
+ startedAt: slot.startedAt,
190
+ endedAt: cell.completedAt ?? slot.completedAt,
191
+ triggerSource: slot.triggerSource,
192
+ recoveryRequestId: slot.recoveryRequestId ?? null,
193
+ recoveryMode: slot.recoveryMode ?? null,
194
+ occurrenceAt: slot.occurrenceAt,
195
+ candidateReport: cellView.candidateReport,
196
+ attemptCount: cell.attemptCount,
197
+ fallbackStage: cell.fallback_stage,
198
+ recovery: admission,
199
+ operatorHint: admission.retryable
200
+ ? '可重试'
201
+ : admission.relaxedRetryAllowed
202
+ ? '正常完成的无新内容结果,仅在人工判断后可放宽条件重试'
203
+ : '非终态或已成功,无需重试',
204
+ };
205
+ }))
206
+ .filter((e) => {
207
+ if (targetFilter && e.targetId !== targetFilter)
208
+ return false;
209
+ if (statusFilter && e.status.toLowerCase() !== statusFilter)
210
+ return false;
211
+ return true;
212
+ })
213
+ .slice(0, limit);
214
+ database.close();
215
+ database = null;
216
+ res.json({ data: { executions } });
217
+ }
218
+ catch (error) {
219
+ if (database) {
220
+ try {
221
+ database.close();
222
+ }
223
+ catch { /* ignore */ }
224
+ }
225
+ const message = error instanceof Error ? error.message : String(error);
226
+ logger_1.logger.error('Failed to list scheduler executions', { error: { message } });
227
+ res.status(500).json({ errorCode: error_codes_1.ErrorCode.SCHEDULER_LIST_FAILED });
228
+ }
229
+ }
230
+ /**
231
+ * GET /api/scheduler/slots/:slotId/logs
232
+ *
233
+ * Correlated log view: filters the process log file by the slot id and its
234
+ * target ids so operators can replay exactly the lines for one occurrence.
235
+ * Pure filtering of existing structured logger output, no new store.
236
+ */
237
+ async function getSlotLogs(req, res) {
238
+ const slotId = req.params.slotId;
239
+ if (!slotId || slotId.length > 200) {
240
+ res.status(400).json({ errorCode: error_codes_1.ErrorCode.SCHEDULER_LIST_FAILED, message: 'invalid slotId' });
241
+ return;
242
+ }
243
+ let database = null;
244
+ try {
245
+ const configPath = (0, config_1.getConfigPath)();
246
+ const config = (0, config_1.loadConfig)(configPath);
247
+ if (!config.storage?.databasePath) {
248
+ res.status(400).json({ errorCode: error_codes_1.ErrorCode.SCHEDULER_LIST_FAILED, message: 'database not configured' });
249
+ return;
250
+ }
251
+ database = new Database_1.Database(config.storage.databasePath);
252
+ database.migrate();
253
+ const slot = database.slots.getSlot(slotId);
254
+ const targets = slot ? database.slots.getCells(slotId).map((c) => c.targetId) : [];
255
+ database.close();
256
+ database = null;
257
+ let logFile = '';
258
+ const dataDir = path_1.default.dirname(config.storage.databasePath);
259
+ for (const candidate of [
260
+ path_1.default.join(dataDir, 'pixiv-downloader.log'),
261
+ path_1.default.resolve(process.cwd(), 'data', 'pixiv-downloader.log'),
262
+ ]) {
263
+ if ((0, fs_1.existsSync)(candidate)) {
264
+ logFile = candidate;
265
+ break;
266
+ }
267
+ }
268
+ if (!logFile) {
269
+ res.json({ data: { logs: [], total: 0, slotId, targets } });
270
+ return;
271
+ }
272
+ const keywords = [slotId, ...targets];
273
+ const lines = (0, fs_1.readFileSync)(logFile, 'utf-8')
274
+ .split('\n')
275
+ .filter((line) => line.trim() && keywords.some((k) => line.toLowerCase().includes(k.toLowerCase())));
276
+ res.json({ data: { logs: lines.slice(-500), total: lines.length, slotId, targets } });
277
+ }
278
+ catch (error) {
279
+ if (database) {
280
+ try {
281
+ database.close();
282
+ }
283
+ catch { /* ignore */ }
284
+ }
285
+ const message = error instanceof Error ? error.message : String(error);
286
+ logger_1.logger.error('Failed to read slot logs', { slotId, error: { message } });
287
+ res.status(500).json({ errorCode: error_codes_1.ErrorCode.LOGS_GET_FAILED });
288
+ }
289
+ }
290
+ /**
291
+ * POST /api/scheduler/targets/:targetId/recover
292
+ *
293
+ * WebUI Control Center Recovery action. This is a thin server-side proxy to the
294
+ * EXISTING authenticated scheduler dispatcher (`POST /internal/targets/:targetId/recover`),
295
+ * so the browser never sees the trigger token and the business admission rules
296
+ * live exactly where scheduled/manual recovery already lives. Deliberately
297
+ * disabled with a clear 503 when the proxy URL/token env vars are absent —
298
+ * read-only Scheduler remains fully usable.
299
+ */
300
+ async function recoverTarget(req, res) {
301
+ const targetId = req.params.targetId;
302
+ if (!TARGET_ID_SAFE.test(targetId)) {
303
+ res.status(400).json({ status: 'error', error: 'invalid targetId' });
304
+ return;
305
+ }
306
+ const requestId = req.body?.requestId;
307
+ if (typeof requestId !== 'string' || !RECOVERY_UUID.test(requestId)) {
308
+ res.status(400).json({ status: 'error', error: 'requestId must be a UUID' });
309
+ return;
310
+ }
311
+ const retryMode = req.body?.retryMode === 'relaxed' ? 'relaxed' : 'normal';
312
+ const correlationId = req.body?.correlationId;
313
+ if (correlationId !== undefined && (typeof correlationId !== 'string' || correlationId.length > 200)) {
314
+ res.status(400).json({ status: 'error', error: 'correlationId must be a string of at most 200 chars' });
315
+ return;
316
+ }
317
+ await forwardRecovery(res, targetId, `/internal/targets/${encodeURIComponent(targetId)}/recover`, {
318
+ method: 'POST',
319
+ body: JSON.stringify({
320
+ requestId,
321
+ retryMode,
322
+ ...(correlationId ? { correlationId } : {}),
323
+ }),
324
+ });
325
+ }
326
+ /**
327
+ * GET /api/scheduler/targets/:targetId/recover/:requestId
328
+ *
329
+ * Poll the durable recovery slot outcome (same contract the review chain uses).
330
+ */
331
+ async function recoverStatus(req, res) {
332
+ const { targetId, requestId } = req.params;
333
+ if (!TARGET_ID_SAFE.test(targetId) || !RECOVERY_UUID.test(requestId ?? '')) {
334
+ res.status(400).json({ status: 'error', error: 'invalid targetId or requestId' });
335
+ return;
336
+ }
337
+ await forwardRecovery(res, targetId, `/internal/targets/${encodeURIComponent(targetId)}/recover/${encodeURIComponent(requestId)}`, {
338
+ method: 'GET',
339
+ });
340
+ }
64
341
  //# sourceMappingURL=scheduler-handlers.js.map
@@ -4,5 +4,9 @@ const express_1 = require("express");
4
4
  const scheduler_handlers_1 = require("./handlers/scheduler-handlers");
5
5
  const router = (0, express_1.Router)();
6
6
  router.get('/', scheduler_handlers_1.listRecentSlots);
7
+ router.get('/executions', scheduler_handlers_1.listExecutions);
8
+ router.get('/slots/:slotId/logs', scheduler_handlers_1.getSlotLogs);
9
+ router.post('/targets/:targetId/recover', scheduler_handlers_1.recoverTarget);
10
+ router.get('/targets/:targetId/recover/:requestId', scheduler_handlers_1.recoverStatus);
7
11
  exports.default = router;
8
12
  //# sourceMappingURL=scheduler.js.map
@@ -63,6 +63,8 @@ export declare enum ErrorCode {
63
63
  FILE_DELETE_FAILED = "FILE_DELETE_FAILED",
64
64
  FILE_NORMALIZE_FAILED = "FILE_NORMALIZE_FAILED",
65
65
  SCHEDULER_LIST_FAILED = "SCHEDULER_LIST_FAILED",
66
+ SCHEDULER_RECOVERY_UNAVAILABLE = "SCHEDULER_RECOVERY_UNAVAILABLE",
67
+ SCHEDULER_RECOVERY_FAILED = "SCHEDULER_RECOVERY_FAILED",
66
68
  AUTH_STATUS_FAILED = "AUTH_STATUS_FAILED",
67
69
  AUTH_USERNAME_PASSWORD_REQUIRED = "AUTH_USERNAME_PASSWORD_REQUIRED",
68
70
  AUTH_LOGIN_SUCCESS = "AUTH_LOGIN_SUCCESS",
@@ -72,6 +72,8 @@ var ErrorCode;
72
72
  ErrorCode["FILE_NORMALIZE_FAILED"] = "FILE_NORMALIZE_FAILED";
73
73
  // Scheduler errors
74
74
  ErrorCode["SCHEDULER_LIST_FAILED"] = "SCHEDULER_LIST_FAILED";
75
+ ErrorCode["SCHEDULER_RECOVERY_UNAVAILABLE"] = "SCHEDULER_RECOVERY_UNAVAILABLE";
76
+ ErrorCode["SCHEDULER_RECOVERY_FAILED"] = "SCHEDULER_RECOVERY_FAILED";
75
77
  // Auth errors
76
78
  ErrorCode["AUTH_STATUS_FAILED"] = "AUTH_STATUS_FAILED";
77
79
  ErrorCode["AUTH_USERNAME_PASSWORD_REQUIRED"] = "AUTH_USERNAME_PASSWORD_REQUIRED";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixivflow",
3
- "version": "2.35.0",
3
+ "version": "2.37.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",
@@ -0,0 +1,2 @@
1
+ import{r as s,bk as g,B as x,a_ as E}from"./index-BtXc5-H2.js";const a=r=>typeof r?.then=="function",P=r=>{const{type:p,children:v,prefixCls:m,buttonProps:b,close:l,autoFocus:f,emitEvent:h,isSilent:i,quitOnNullishReturnValue:k,actionFn:o}=r,n=s.useRef(!1),d=s.useRef(null),[y,c]=g(!1),u=(...t)=>{l?.apply(void 0,t)};s.useEffect(()=>{let t=null;return f&&(t=setTimeout(()=>{var e;(e=d.current)===null||e===void 0||e.focus({preventScroll:!0})})),()=>{t&&clearTimeout(t)}},[f]);const C=t=>{a(t)&&(c(!0),t.then((...e)=>{c(!1,!0),u.apply(void 0,e),n.current=!1},e=>{if(c(!1,!0),n.current=!1,!i?.())return Promise.reject(e)}))},R=t=>{if(n.current)return;if(n.current=!0,!o){u();return}let e;if(h){if(e=o(t),k&&!a(e)){n.current=!1,u(t);return}}else if(o.length)e=o(l),n.current=!1;else if(e=o(),!a(e)){u();return}C(e)};return s.createElement(x,Object.assign({},E(p),{onClick:R,loading:y,prefixCls:m},b,{ref:d}),v)};export{P as A};
2
+ //# sourceMappingURL=ActionButton-bmITMO8t.js.map
@@ -1,2 +1,2 @@
1
- import{r as e,I as a,_ as r}from"./index-HJggy9SZ.js";var l={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M699 353h-46.9c-10.2 0-19.9 4.9-25.9 13.3L469 584.3l-71.2-98.8c-6-8.3-15.6-13.3-25.9-13.3H325c-6.5 0-10.3 7.4-6.5 12.7l124.6 172.8a31.8 31.8 0 0051.7 0l210.6-292c3.9-5.3.1-12.7-6.4-12.7z"}},{tag:"path",attrs:{d:"M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"}}]},name:"check-circle",theme:"outlined"},n=function(t,c){return e.createElement(a,r({},t,{ref:c,icon:l}))},o=e.forwardRef(n);export{o as R};
2
- //# sourceMappingURL=CheckCircleOutlined-BS_TxY3k.js.map
1
+ import{r as e,I as a,_ as r}from"./index-BtXc5-H2.js";var l={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M699 353h-46.9c-10.2 0-19.9 4.9-25.9 13.3L469 584.3l-71.2-98.8c-6-8.3-15.6-13.3-25.9-13.3H325c-6.5 0-10.3 7.4-6.5 12.7l124.6 172.8a31.8 31.8 0 0051.7 0l210.6-292c3.9-5.3.1-12.7-6.4-12.7z"}},{tag:"path",attrs:{d:"M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"}}]},name:"check-circle",theme:"outlined"},n=function(t,c){return e.createElement(a,r({},t,{ref:c,icon:l}))},o=e.forwardRef(n);export{o as R};
2
+ //# sourceMappingURL=CheckCircleOutlined-DXxMdP08.js.map
@@ -1,2 +1,2 @@
1
- import{r as e,I as r,_ as c}from"./index-HJggy9SZ.js";var l={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"defs",attrs:{},children:[{tag:"style",attrs:{}}]},{tag:"path",attrs:{d:"M899.1 869.6l-53-305.6H864c14.4 0 26-11.6 26-26V346c0-14.4-11.6-26-26-26H618V138c0-14.4-11.6-26-26-26H432c-14.4 0-26 11.6-26 26v182H160c-14.4 0-26 11.6-26 26v192c0 14.4 11.6 26 26 26h17.9l-53 305.6a25.95 25.95 0 0025.6 30.4h723c1.5 0 3-.1 4.4-.4a25.88 25.88 0 0021.2-30zM204 390h272V182h72v208h272v104H204V390zm468 440V674c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v156H416V674c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v156H202.8l45.1-260H776l45.1 260H672z"}}]},name:"clear",theme:"outlined"},n=function(a,t){return e.createElement(r,c({},a,{ref:t,icon:l}))},h=e.forwardRef(n);export{h as R};
2
- //# sourceMappingURL=ClearOutlined-uIvWLBII.js.map
1
+ import{r as e,I as r,_ as c}from"./index-BtXc5-H2.js";var l={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"defs",attrs:{},children:[{tag:"style",attrs:{}}]},{tag:"path",attrs:{d:"M899.1 869.6l-53-305.6H864c14.4 0 26-11.6 26-26V346c0-14.4-11.6-26-26-26H618V138c0-14.4-11.6-26-26-26H432c-14.4 0-26 11.6-26 26v182H160c-14.4 0-26 11.6-26 26v192c0 14.4 11.6 26 26 26h17.9l-53 305.6a25.95 25.95 0 0025.6 30.4h723c1.5 0 3-.1 4.4-.4a25.88 25.88 0 0021.2-30zM204 390h272V182h72v208h272v104H204V390zm468 440V674c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v156H416V674c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v156H202.8l45.1-260H776l45.1 260H672z"}}]},name:"clear",theme:"outlined"},n=function(a,t){return e.createElement(r,c({},a,{ref:t,icon:l}))},h=e.forwardRef(n);export{h as R};
2
+ //# sourceMappingURL=ClearOutlined-CVkP1OPo.js.map
@@ -1,2 +1,2 @@
1
- import{r as e,I as l,_ as r}from"./index-HJggy9SZ.js";var t={icon:{tag:"svg",attrs:{"fill-rule":"evenodd",viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M512 64c247.4 0 448 200.6 448 448S759.4 960 512 960 64 759.4 64 512 264.6 64 512 64zm0 76c-205.4 0-372 166.6-372 372s166.6 372 372 372 372-166.6 372-372-166.6-372-372-372zm128.01 198.83c.03 0 .05.01.09.06l45.02 45.01a.2.2 0 01.05.09.12.12 0 010 .07c0 .02-.01.04-.05.08L557.25 512l127.87 127.86a.27.27 0 01.05.06v.02a.12.12 0 010 .07c0 .03-.01.05-.05.09l-45.02 45.02a.2.2 0 01-.09.05.12.12 0 01-.07 0c-.02 0-.04-.01-.08-.05L512 557.25 384.14 685.12c-.04.04-.06.05-.08.05a.12.12 0 01-.07 0c-.03 0-.05-.01-.09-.05l-45.02-45.02a.2.2 0 01-.05-.09.12.12 0 010-.07c0-.02.01-.04.06-.08L466.75 512 338.88 384.14a.27.27 0 01-.05-.06l-.01-.02a.12.12 0 010-.07c0-.03.01-.05.05-.09l45.02-45.02a.2.2 0 01.09-.05.12.12 0 01.07 0c.02 0 .04.01.08.06L512 466.75l127.86-127.86c.04-.05.06-.06.08-.06a.12.12 0 01.07 0z"}}]},name:"close-circle",theme:"outlined"},o=function(a,c){return e.createElement(l,r({},a,{ref:c,icon:t}))},i=e.forwardRef(o);export{i as R};
2
- //# sourceMappingURL=CloseCircleOutlined-B45wvGw1.js.map
1
+ import{r as e,I as l,_ as r}from"./index-BtXc5-H2.js";var t={icon:{tag:"svg",attrs:{"fill-rule":"evenodd",viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M512 64c247.4 0 448 200.6 448 448S759.4 960 512 960 64 759.4 64 512 264.6 64 512 64zm0 76c-205.4 0-372 166.6-372 372s166.6 372 372 372 372-166.6 372-372-166.6-372-372-372zm128.01 198.83c.03 0 .05.01.09.06l45.02 45.01a.2.2 0 01.05.09.12.12 0 010 .07c0 .02-.01.04-.05.08L557.25 512l127.87 127.86a.27.27 0 01.05.06v.02a.12.12 0 010 .07c0 .03-.01.05-.05.09l-45.02 45.02a.2.2 0 01-.09.05.12.12 0 01-.07 0c-.02 0-.04-.01-.08-.05L512 557.25 384.14 685.12c-.04.04-.06.05-.08.05a.12.12 0 01-.07 0c-.03 0-.05-.01-.09-.05l-45.02-45.02a.2.2 0 01-.05-.09.12.12 0 010-.07c0-.02.01-.04.06-.08L466.75 512 338.88 384.14a.27.27 0 01-.05-.06l-.01-.02a.12.12 0 010-.07c0-.03.01-.05.05-.09l45.02-45.02a.2.2 0 01.09-.05.12.12 0 01.07 0c.02 0 .04.01.08.06L512 466.75l127.86-127.86c.04-.05.06-.06.08-.06a.12.12 0 01.07 0z"}}]},name:"close-circle",theme:"outlined"},o=function(a,c){return e.createElement(l,r({},a,{ref:c,icon:t}))},i=e.forwardRef(o);export{i as R};
2
+ //# sourceMappingURL=CloseCircleOutlined-DGYEx-rx.js.map