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.
- package/dist/download/novelMarkers.d.ts +18 -2
- package/dist/package.json +1 -1
- package/dist/version.js +1 -1
- package/dist/webui/package.json +1 -1
- package/dist/webui/routes/handlers/scheduler-handlers.d.ts +44 -0
- package/dist/webui/routes/handlers/scheduler-handlers.js +285 -8
- package/dist/webui/routes/scheduler.js +4 -0
- package/dist/webui/utils/error-codes.d.ts +2 -0
- package/dist/webui/utils/error-codes.js +2 -0
- package/package.json +1 -1
- package/webui-frontend/dist/assets/ActionButton-bmITMO8t.js +2 -0
- package/webui-frontend/dist/assets/{CheckCircleOutlined-BS_TxY3k.js → CheckCircleOutlined-DXxMdP08.js} +2 -2
- package/webui-frontend/dist/assets/{ClearOutlined-uIvWLBII.js → ClearOutlined-CVkP1OPo.js} +2 -2
- package/webui-frontend/dist/assets/{CloseCircleOutlined-B45wvGw1.js → CloseCircleOutlined-DGYEx-rx.js} +2 -2
- package/webui-frontend/dist/assets/{Config-UjgCA3Jq.js → Config-BFBZgSvI.js} +3 -3
- package/webui-frontend/dist/assets/{Dashboard-hIW1nAN9.js → Dashboard-CHiRV77D.js} +2 -2
- package/webui-frontend/dist/assets/{DeleteOutlined-ByQuZQbZ.js → DeleteOutlined-DwsLtl_I.js} +2 -2
- package/webui-frontend/dist/assets/Download-BY0RQ0yW.js +2 -0
- package/webui-frontend/dist/assets/{Files-DCMyqujj.js → Files-qrEyGaCr.js} +3 -3
- package/webui-frontend/dist/assets/{PictureOutlined-CSEKKDH9.js → PictureOutlined-B5UcfS3J.js} +2 -2
- package/webui-frontend/dist/assets/{SortDescendingOutlined-BS5Vnvjg.js → SortDescendingOutlined-CIPBhzNI.js} +2 -2
- package/webui-frontend/dist/assets/{Table-CvqrzZX2.js → Table-COdSkj3k.js} +7 -7
- package/webui-frontend/dist/assets/{ThunderboltOutlined-DhgtXoNM.js → ThunderboltOutlined-BlHGTLpd.js} +2 -2
- package/webui-frontend/dist/assets/{UrlDownload-BHF2qmgI.js → UrlDownload-Dxj1MDnw.js} +2 -2
- package/webui-frontend/dist/assets/{dateUtils-CEt5gAeU.js → dateUtils-Dh-zVUtr.js} +2 -2
- package/webui-frontend/dist/assets/{errorCodeTranslator-Xn_Z9qOh.js → errorCodeTranslator-D5vahyqQ.js} +2 -2
- package/webui-frontend/dist/assets/{index-DiwSYMD5.js → index-B84Vd9_M.js} +2 -2
- package/webui-frontend/dist/assets/index-BhmvYPFd.js +19 -0
- package/webui-frontend/dist/assets/{index-NSEOZ6G5.js → index-Bp5yfy4e.js} +3 -3
- package/webui-frontend/dist/assets/{index-HJggy9SZ.js → index-BtXc5-H2.js} +15 -15
- package/webui-frontend/dist/assets/{index-eiyXJZci.js → index-BvQQNYa5.js} +2 -2
- package/webui-frontend/dist/assets/index-C2qRwckx.js +2 -0
- package/webui-frontend/dist/assets/{index-ZRYuscJf.js → index-CFWcxkyB.js} +2 -2
- package/webui-frontend/dist/assets/{index-DpfbKoAX.js → index-CZ44NQeD.js} +2 -2
- package/webui-frontend/dist/assets/{index-Cg2Wi2pt.js → index-CZXbP5HB.js} +2 -2
- package/webui-frontend/dist/assets/{index-6mtxs9yc.js → index-CZlT-55J.js} +2 -2
- package/webui-frontend/dist/assets/index-Cfg0ICF_.js +20 -0
- package/webui-frontend/dist/assets/{index-D_qUS2GR.js → index-D1KRxkVR.js} +2 -2
- package/webui-frontend/dist/assets/{index-D4mbR-Vx.js → index-D8iUeVsP.js} +2 -2
- package/webui-frontend/dist/assets/{index-C66Xw4Fs.js → index-DcqvG64t.js} +2 -2
- package/webui-frontend/dist/assets/index-eeF42ACG.js +2 -0
- package/webui-frontend/dist/assets/{index-mHLzQpzS.js → index-xZubvNRw.js} +6 -6
- package/webui-frontend/dist/assets/row-DaMDSIar.js +2 -0
- package/webui-frontend/dist/assets/{useAuth-4erTEEjT.js → useAuth-BfM9bg0I.js} +3 -3
- package/webui-frontend/dist/assets/{useConfig-BmhXnAiM.js → useConfig-nFBmSW5k.js} +2 -2
- package/webui-frontend/dist/assets/{useDownload-DhuPPKqw.js → useDownload-ChmDqdbc.js} +2 -2
- package/webui-frontend/dist/assets/{useErrorHandler-Bcb3l7ju.js → useErrorHandler-MJ8OxkB4.js} +2 -2
- package/webui-frontend/dist/index.html +1 -1
- package/webui-frontend/dist/assets/Download-DdswOGH7.js +0 -2
- package/webui-frontend/dist/assets/index-B4zZ11c-.js +0 -20
- package/webui-frontend/dist/assets/index-BlXTpFS3.js +0 -2
- package/webui-frontend/dist/assets/index-BvgYmQcZ.js +0 -2
- package/webui-frontend/dist/assets/index-DV4iHo-9.js +0 -19
- package/webui-frontend/dist/assets/row-CQQO4x82.js +0 -2
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
|
|
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?:
|
|
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
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.37.0', commit: 'a17343519198' };
|
|
6
6
|
//# sourceMappingURL=version.js.map
|
package/dist/webui/package.json
CHANGED
|
@@ -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
|
-
|
|
38
|
-
|
|
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.
|
|
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-
|
|
2
|
-
//# sourceMappingURL=CheckCircleOutlined-
|
|
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-
|
|
2
|
-
//# sourceMappingURL=ClearOutlined-
|
|
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-
|
|
2
|
-
//# sourceMappingURL=CloseCircleOutlined-
|
|
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
|