pixivflow 2.19.4 → 2.19.5
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/config/defaults.d.ts +1 -0
- package/dist/config/defaults.js +4 -0
- package/dist/config/environment.js +19 -0
- package/dist/config/types.d.ts +21 -0
- package/dist/config/validation.js +15 -0
- package/dist/delivery/DeliveryService.d.ts +9 -0
- package/dist/delivery/DeliveryService.js +11 -0
- package/dist/download/DownloadManager.js +12 -1
- package/dist/download/handlers/IllustrationTargetHandler.d.ts +42 -1
- package/dist/download/handlers/IllustrationTargetHandler.js +238 -37
- package/dist/download/handlers/NovelTargetHandler.d.ts +47 -0
- package/dist/download/handlers/NovelTargetHandler.js +211 -27
- package/dist/download/pipeline/DownloadPipeline.d.ts +22 -3
- package/dist/download/pipeline/DownloadPipeline.js +109 -92
- package/dist/download/plan/DownloadPlanner.d.ts +47 -1
- package/dist/download/plan/DownloadPlanner.js +105 -15
- package/dist/package.json +1 -1
- package/dist/scheduler/TargetOutcome.d.ts +195 -2
- package/dist/scheduler/TargetOutcome.js +202 -3
- package/dist/storage/repositories/DeliveryRepository.d.ts +12 -0
- package/dist/storage/repositories/DeliveryRepository.js +18 -1
- package/dist/version.js +1 -1
- package/dist/webui/package.json +1 -1
- package/package.json +1 -1
package/dist/config/defaults.js
CHANGED
|
@@ -41,6 +41,10 @@ exports.DEFAULT_CONFIG = {
|
|
|
41
41
|
maxRetries: 3,
|
|
42
42
|
retryDelay: 2000,
|
|
43
43
|
timeout: 60000,
|
|
44
|
+
// Candidates one execution may ATTEMPT while looking for an eligible work.
|
|
45
|
+
// Bounded so a ranking page full of already-delivered works cannot loop,
|
|
46
|
+
// but large enough that a single duplicate no longer burns the slot.
|
|
47
|
+
candidateScanLimit: 5,
|
|
44
48
|
},
|
|
45
49
|
initialDelay: 0,
|
|
46
50
|
};
|
|
@@ -101,6 +101,25 @@ function applyEnvironmentOverrides(config) {
|
|
|
101
101
|
overridden.scheduler.enabled = process.env.PIXIV_SCHEDULER_ENABLED.toLowerCase() === 'true';
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
|
+
// Override the bounded candidate-scan window. A non-numeric or non-positive
|
|
105
|
+
// value is ignored (the config-file value stays authoritative) rather than
|
|
106
|
+
// silently becoming NaN, which would disable the bound entirely.
|
|
107
|
+
if (process.env.PIXIV_CANDIDATE_SCAN_LIMIT !== undefined) {
|
|
108
|
+
const parsed = Number.parseInt(process.env.PIXIV_CANDIDATE_SCAN_LIMIT, 10);
|
|
109
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
110
|
+
if (!overridden.download) {
|
|
111
|
+
overridden.download = { candidateScanLimit: parsed };
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
overridden.download.candidateScanLimit = parsed;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
logger_1.logger.warn('Ignoring PIXIV_CANDIDATE_SCAN_LIMIT: expected a positive integer', {
|
|
119
|
+
value: process.env.PIXIV_CANDIDATE_SCAN_LIMIT,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
104
123
|
// Override proxy from environment variables
|
|
105
124
|
// Priority: all_proxy > https_proxy > http_proxy
|
|
106
125
|
const proxyUrl = process.env.all_proxy || process.env.ALL_PROXY ||
|
package/dist/config/types.d.ts
CHANGED
|
@@ -81,6 +81,20 @@ export interface TargetConfig {
|
|
|
81
81
|
* Maximum works to download per execution for this tag.
|
|
82
82
|
*/
|
|
83
83
|
limit?: number;
|
|
84
|
+
/**
|
|
85
|
+
* Maximum CANDIDATE works one execution may attempt (not produce) while
|
|
86
|
+
* looking for an eligible work. A scheduled "one post per slot" target has
|
|
87
|
+
* `limit: 1`, so without this the run would hold a single candidate: if that
|
|
88
|
+
* one candidate is already delivered / deleted / private, the slot ends with
|
|
89
|
+
* nothing submitted. The scan walks up to N candidates in ranking order,
|
|
90
|
+
* skipping unusable ones, and is strictly bounded so a page full of
|
|
91
|
+
* duplicates cannot loop.
|
|
92
|
+
*
|
|
93
|
+
* Overrides `download.candidateScanLimit` / `PIXIV_CANDIDATE_SCAN_LIMIT`.
|
|
94
|
+
* Clamped to 1..100 and never below `limit`, so a multi-work target can still
|
|
95
|
+
* fill its own limit.
|
|
96
|
+
*/
|
|
97
|
+
candidateScanLimit?: number;
|
|
84
98
|
/**
|
|
85
99
|
* Search target parameter for Pixiv API.
|
|
86
100
|
*/
|
|
@@ -655,6 +669,13 @@ export interface StandaloneConfig {
|
|
|
655
669
|
* Default: 60000
|
|
656
670
|
*/
|
|
657
671
|
timeout?: number;
|
|
672
|
+
/**
|
|
673
|
+
* How many CANDIDATE works one execution may attempt while looking for an
|
|
674
|
+
* eligible one. Per-target `candidateScanLimit` overrides this.
|
|
675
|
+
*
|
|
676
|
+
* Default: 5 (clamped to 1..100)
|
|
677
|
+
*/
|
|
678
|
+
candidateScanLimit?: number;
|
|
658
679
|
};
|
|
659
680
|
}
|
|
660
681
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -469,6 +469,21 @@ function validateConfig(config, location, databasePath) {
|
|
|
469
469
|
if (config.download.maxRetries !== undefined && (config.download.maxRetries < 0 || config.download.maxRetries > 10)) {
|
|
470
470
|
warnings.push('download.maxRetries: Should be between 0 and 10');
|
|
471
471
|
}
|
|
472
|
+
if (config.download.candidateScanLimit !== undefined &&
|
|
473
|
+
(!Number.isInteger(config.download.candidateScanLimit) ||
|
|
474
|
+
config.download.candidateScanLimit < 1 ||
|
|
475
|
+
config.download.candidateScanLimit > 100)) {
|
|
476
|
+
warnings.push('download.candidateScanLimit: Should be an integer between 1 and 100');
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
// The bounded candidate scan is what stops a page full of duplicates from
|
|
480
|
+
// burning a whole scheduled slot, so a non-integer value is reported here.
|
|
481
|
+
for (const target of config.targets ?? []) {
|
|
482
|
+
if (target.candidateScanLimit === undefined)
|
|
483
|
+
continue;
|
|
484
|
+
if (!Number.isInteger(target.candidateScanLimit) || target.candidateScanLimit < 1 || target.candidateScanLimit > 100) {
|
|
485
|
+
warnings.push(`targets.${target.id ?? target.tag ?? '?'}.candidateScanLimit: Should be an integer between 1 and 100`);
|
|
486
|
+
}
|
|
472
487
|
}
|
|
473
488
|
// Validate log level
|
|
474
489
|
if (config.logLevel && !['debug', 'info', 'warn', 'error'].includes(config.logLevel)) {
|
|
@@ -35,6 +35,15 @@ export declare class DeliveryService {
|
|
|
35
35
|
isAlreadyDelivered(deliveryTarget: string, workType: string, pixivId: string): boolean;
|
|
36
36
|
/** Batch form for pre-lock candidate filtering. */
|
|
37
37
|
deliveredIds(deliveryTarget: string, workType: string, pixivIds: string[]): Set<string>;
|
|
38
|
+
/**
|
|
39
|
+
* Bulk CANDIDATE SELECTION dedupe: works already delivered to this target, or
|
|
40
|
+
* whose review submission is still PENDING an answer. A pending work is
|
|
41
|
+
* already submitted for review, so it must not be selected (and submitted)
|
|
42
|
+
* again; the scan moves on to the next candidate instead. Within-slot RESUME
|
|
43
|
+
* keeps using `isAlreadyDelivered` — a cell continuing its OWN pending work is
|
|
44
|
+
* resuming it, not duplicating it.
|
|
45
|
+
*/
|
|
46
|
+
submittedIds(deliveryTarget: string, workType: string, pixivIds: string[]): Set<string>;
|
|
38
47
|
/**
|
|
39
48
|
* Atomically create the delivery intent + outbox row and advance the cell.
|
|
40
49
|
* Missing local artifact files are treated as a recoverable error (the
|
|
@@ -33,6 +33,17 @@ class DeliveryService {
|
|
|
33
33
|
deliveredIds(deliveryTarget, workType, pixivIds) {
|
|
34
34
|
return this.database.deliveries.deliveredIds(deliveryTarget, workType, pixivIds);
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Bulk CANDIDATE SELECTION dedupe: works already delivered to this target, or
|
|
38
|
+
* whose review submission is still PENDING an answer. A pending work is
|
|
39
|
+
* already submitted for review, so it must not be selected (and submitted)
|
|
40
|
+
* again; the scan moves on to the next candidate instead. Within-slot RESUME
|
|
41
|
+
* keeps using `isAlreadyDelivered` — a cell continuing its OWN pending work is
|
|
42
|
+
* resuming it, not duplicating it.
|
|
43
|
+
*/
|
|
44
|
+
submittedIds(deliveryTarget, workType, pixivIds) {
|
|
45
|
+
return this.database.deliveries.submittedIds(deliveryTarget, workType, pixivIds);
|
|
46
|
+
}
|
|
36
47
|
/**
|
|
37
48
|
* Atomically create the delivery intent + outbox row and advance the cell.
|
|
38
49
|
* Missing local artifact files are treated as a recoverable error (the
|
|
@@ -128,6 +128,9 @@ class DownloadManager {
|
|
|
128
128
|
this.novelDownloader = new NovelDownloader_1.NovelDownloader(client, database, fileService, database);
|
|
129
129
|
this.planner = new DownloadPlanner_1.DownloadPlanner(database, {
|
|
130
130
|
deliveredIds: (target, type, ids) => this.deliveryService.deliveredIds(target, type, ids),
|
|
131
|
+
// CANDIDATE SELECTION dedupe: also treats a work whose review submission is
|
|
132
|
+
// still PENDING as taken, so it is skipped instead of submitted again.
|
|
133
|
+
submittedIds: (target, type, ids) => this.deliveryService.submittedIds(target, type, ids),
|
|
131
134
|
// Durable duplicate history handed in by the caller (the batch runner asks
|
|
132
135
|
// the control plane for it). Independent of any local delivery target, so it
|
|
133
136
|
// also works for a shadow run that delivers nowhere.
|
|
@@ -137,7 +140,10 @@ class DownloadManager {
|
|
|
137
140
|
return new Set();
|
|
138
141
|
return new Set(ids.filter((id) => known.has(id)));
|
|
139
142
|
},
|
|
140
|
-
|
|
143
|
+
// Global bounded candidate-scan window (`download.candidateScanLimit`).
|
|
144
|
+
// Per-target `candidateScanLimit` overrides it. Without this the pipeline
|
|
145
|
+
// would only ever see as many candidates as the target's own `limit`.
|
|
146
|
+
}, config.download?.candidateScanLimit);
|
|
141
147
|
this.executor = new DownloadExecutor_1.DownloadExecutor();
|
|
142
148
|
const downloadConfig = config.download ?? {};
|
|
143
149
|
const maxRetries = downloadConfig.maxRetries ?? 3;
|
|
@@ -166,6 +172,11 @@ class DownloadManager {
|
|
|
166
172
|
: config.storage?.databasePath, config.download?.requestDelay ?? 500, this.abortController.signal);
|
|
167
173
|
this.illustrationHandler = new IllustrationTargetHandler_1.IllustrationTargetHandler(client, database, this.rankingService, this.illustrationDownloader, this.pipeline, topicFactory, this.deliveryService);
|
|
168
174
|
this.novelHandler = new NovelTargetHandler_1.NovelTargetHandler(client, database, this.rankingService, this.pipeline, this.novelDownloader, topicFactory, this.deliveryService);
|
|
175
|
+
// The FETCH stage needs the same bound the planner applies to the attempt
|
|
176
|
+
// window, or a `limit: 1` target would ask the ranking API for one work and
|
|
177
|
+
// leave the bounded scan nothing to scan.
|
|
178
|
+
this.illustrationHandler.setDefaultCandidateScanLimit(config.download?.candidateScanLimit);
|
|
179
|
+
this.novelHandler.setDefaultCandidateScanLimit(config.download?.candidateScanLimit);
|
|
169
180
|
}
|
|
170
181
|
setProgressCallback(callback) {
|
|
171
182
|
this.progressReporter.setCallback(callback);
|
|
@@ -18,6 +18,29 @@ export declare class IllustrationTargetHandler {
|
|
|
18
18
|
private readonly deliveryService?;
|
|
19
19
|
/** Outcomes produced during this handle() call (deliveries + terminal non-matches). */
|
|
20
20
|
private outcomes;
|
|
21
|
+
/**
|
|
22
|
+
* Candidate scan of the last pipeline.run() of this handle() call: how many
|
|
23
|
+
* candidates were attempted, which were skipped and why, and whether any
|
|
24
|
+
* job-level outage appeared. This is what turns an empty result into an
|
|
25
|
+
* explicit verdict instead of an ambiguous "completed".
|
|
26
|
+
*/
|
|
27
|
+
private scan;
|
|
28
|
+
/**
|
|
29
|
+
* Global candidate-scan bound (`download.candidateScanLimit`), supplied by
|
|
30
|
+
* DownloadManager. This is what lets the FETCH stage ask for more than one
|
|
31
|
+
* candidate: a scheduled one-post-per-slot target has `limit: 1`, and asking
|
|
32
|
+
* the ranking API for exactly one work is the reason a single duplicate used
|
|
33
|
+
* to be unfixable downstream.
|
|
34
|
+
*/
|
|
35
|
+
private defaultCandidateScanLimit?;
|
|
36
|
+
/** Publish the global candidate-scan bound for the fetch stage. */
|
|
37
|
+
setDefaultCandidateScanLimit(limit: number | undefined): void;
|
|
38
|
+
/**
|
|
39
|
+
* How many candidates to FETCH so the bounded scan has something to choose
|
|
40
|
+
* from. Same rule the planner applies to the attempt window, so fetch and
|
|
41
|
+
* scan agree on one bound.
|
|
42
|
+
*/
|
|
43
|
+
private candidateFetchLimit;
|
|
21
44
|
/**
|
|
22
45
|
* Cell identity for this handle() call. Set only for a single-work cell of a
|
|
23
46
|
* scheduled occurrence; null for ad-hoc runs and N-works-per-run targets, which
|
|
@@ -26,7 +49,15 @@ export declare class IllustrationTargetHandler {
|
|
|
26
49
|
private execution;
|
|
27
50
|
constructor(client: IPixivClient, database: IDatabase, rankingService: RankingService, illustrationDownloader: IllustrationDownloader, pipeline: DownloadPipeline, topicPipelineFactory?: TopicPipelineFactory | undefined, deliveryService?: DeliveryService | undefined);
|
|
28
51
|
handle(target: TargetConfig, execution?: TargetExecutionContext): Promise<TargetOutcome>;
|
|
29
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Reduce the outcomes collected while processing one target to one cell
|
|
54
|
+
* result, including the bounded-scan bookkeeping.
|
|
55
|
+
*
|
|
56
|
+
* A `duplicate` is deliberately NOT a target verdict here: a duplicate is a
|
|
57
|
+
* CANDIDATE problem (skip it and try the next candidate), which is what the
|
|
58
|
+
* scan already did. Returning it as the target outcome is the bug that made a
|
|
59
|
+
* scheduled slot report success after submitting nothing.
|
|
60
|
+
*/
|
|
30
61
|
private summarize;
|
|
31
62
|
private classifyError;
|
|
32
63
|
private fetchIllustrations;
|
|
@@ -56,6 +87,11 @@ export declare class IllustrationTargetHandler {
|
|
|
56
87
|
/**
|
|
57
88
|
* Process ONE candidate work for this cell.
|
|
58
89
|
*
|
|
90
|
+
* Returns what happened to THIS candidate, so the pipeline can advance to the
|
|
91
|
+
* next one when it was unusable. Throwing is reserved for JOB-level failures
|
|
92
|
+
* (dead database / dead token / dead delivery provider): a candidate-level
|
|
93
|
+
* failure is reported as a `skipped` attempt and never as a job verdict.
|
|
94
|
+
*
|
|
59
95
|
* The cell is bound to `illust` BEFORE any side effect: it is the binding, not
|
|
60
96
|
* the candidate list, that decides what a later recovery resumes. The binding
|
|
61
97
|
* is only rolled back when the attempt produced no artifact at all, so in-run
|
|
@@ -67,6 +103,11 @@ export declare class IllustrationTargetHandler {
|
|
|
67
103
|
* delivery mode the DeliveryService creates the durable intent atomically and
|
|
68
104
|
* the result is 'delivery_pending' (NOT submitted — the OutboxWorker confirms
|
|
69
105
|
* the ACK). Persistent/download-only runs are 'stored'.
|
|
106
|
+
*
|
|
107
|
+
* An ALREADY-DELIVERED work is returned as a candidate SKIP, never as a
|
|
108
|
+
* target outcome: the run must try the next candidate instead of ending the
|
|
109
|
+
* slot as a `duplicate`. The delivery idempotency ledger is what makes the
|
|
110
|
+
* second concurrent worker lose this race instead of double-submitting.
|
|
70
111
|
*/
|
|
71
112
|
private recordArtifactOutcome;
|
|
72
113
|
private executionContextFields;
|