pixivflow 3.4.2 → 3.5.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/delivery/DeliveryService.js +2 -0
- package/dist/delivery/HttpMultipartDelivery.js +18 -1
- package/dist/delivery/refetchProvenance.d.ts +38 -0
- package/dist/delivery/refetchProvenance.js +55 -0
- package/dist/delivery/types.d.ts +13 -0
- package/dist/download/NovelDownloader.js +1 -0
- package/dist/download/handlers/deliveryContext.d.ts +5 -1
- package/dist/download/handlers/deliveryContext.js +26 -2
- package/dist/package.json +1 -1
- package/dist/version.js +1 -1
- package/dist/webui/package.json +1 -1
- package/package.json +1 -1
|
@@ -148,6 +148,7 @@ class DeliveryService {
|
|
|
148
148
|
pixivId: artifact.pixivId,
|
|
149
149
|
type: artifact.type,
|
|
150
150
|
title: artifact.title,
|
|
151
|
+
...(artifact.seriesTitle !== undefined ? { seriesTitle: artifact.seriesTitle } : {}),
|
|
151
152
|
...(artifact.spoiler !== undefined ? { spoiler: artifact.spoiler } : {}),
|
|
152
153
|
},
|
|
153
154
|
files,
|
|
@@ -288,6 +289,7 @@ class DeliveryService {
|
|
|
288
289
|
return {
|
|
289
290
|
deliveryTarget,
|
|
290
291
|
title: artifact.title,
|
|
292
|
+
...(artifact.seriesTitle !== undefined ? { seriesTitle: artifact.seriesTitle } : {}),
|
|
291
293
|
pixivId: artifact.pixivId,
|
|
292
294
|
type: artifact.type,
|
|
293
295
|
targetId: target.id,
|
|
@@ -43,6 +43,7 @@ const node_crypto_1 = require("node:crypto");
|
|
|
43
43
|
const node_stream_1 = require("node:stream");
|
|
44
44
|
const logger_1 = require("../logger");
|
|
45
45
|
const DeliveryAck_1 = require("./DeliveryAck");
|
|
46
|
+
const refetchProvenance_1 = require("./refetchProvenance");
|
|
46
47
|
const redact_1 = require("../utils/redact");
|
|
47
48
|
/** Render an ISO timestamp to YYYY-MM-DD (create_date is JST). */
|
|
48
49
|
function formatPublishedDate(iso) {
|
|
@@ -424,8 +425,21 @@ function buildTemplateVariables(request) {
|
|
|
424
425
|
return 'R18G';
|
|
425
426
|
return `XRestrict${xRestrict}`;
|
|
426
427
|
})();
|
|
428
|
+
// A series novel's `seriesTitle` names its parent series while `title` is the
|
|
429
|
+
// chapter title. Compose the card headline `《系列名》 章节名` when the work
|
|
430
|
+
// belongs to a series; fall back to the bare title otherwise (illustrations
|
|
431
|
+
// and non-series novels never carry a seriesTitle).
|
|
432
|
+
const displayTitle = c.type === 'novel' && c.seriesTitle
|
|
433
|
+
? `《${c.seriesTitle}》 ${c.title}`
|
|
434
|
+
: c.title;
|
|
427
435
|
return {
|
|
428
436
|
title: c.title,
|
|
437
|
+
// Composed card headline: `《系列名》 章节名` for series novels, else the
|
|
438
|
+
// bare work title. Templates should render {{displayTitle}}.
|
|
439
|
+
displayTitle,
|
|
440
|
+
// Parent series name for a series novel chapter; empty otherwise. Tempo
|
|
441
|
+
// templates can render `《{{seriesTitle}}》 {{title}}` themselves.
|
|
442
|
+
seriesTitle: c.seriesTitle ?? '',
|
|
429
443
|
pixivId: c.pixivId,
|
|
430
444
|
type: c.type,
|
|
431
445
|
targetId: c.targetId ?? '',
|
|
@@ -476,7 +490,10 @@ function buildTemplateVariables(request) {
|
|
|
476
490
|
// Remote manual replacement ("重抓") request UUID; empty for scheduled runs.
|
|
477
491
|
// Carried through the delivery payload context (extraContext) so the
|
|
478
492
|
// receiving service can correlate the review with its refetch attempt.
|
|
479
|
-
|
|
493
|
+
// Canonicalized at this last boundary before the wire: the receiver accepts
|
|
494
|
+
// only a canonical dashed lowercase UUID and rejects the whole submission
|
|
495
|
+
// otherwise (see delivery/refetchProvenance.ts).
|
|
496
|
+
refetchRequestId: (0, refetchProvenance_1.canonicalRefetchRequestId)(c.refetchRequestId),
|
|
480
497
|
};
|
|
481
498
|
}
|
|
482
499
|
/**
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `refetch_request_id` wire contract.
|
|
3
|
+
*
|
|
4
|
+
* The receiving service (TelePost) validates this field as a *canonical* dashed
|
|
5
|
+
* lowercase UUID — `utils/api_server.py:706-715` answers HTTP 400
|
|
6
|
+
* `invalid_refetch_provenance` ("refetch_request_id 必须是 UUID") for anything
|
|
7
|
+
* else — and it rejects the whole submission, permanently, before any review
|
|
8
|
+
* exists. It is strict on purpose: the stored value must later match the
|
|
9
|
+
* refetch attempt's own `request_id`, which TelePost generates as
|
|
10
|
+
* `str(uuid.uuid4())`.
|
|
11
|
+
*
|
|
12
|
+
* Callers hand us their key in whatever spelling they generated it. Production
|
|
13
|
+
* incident 2026-09-27: a manual refetch arrived at the delivery with the bare
|
|
14
|
+
* 32-hex key `1e22b55cb33e47289f30c62e8ee1e11f` — a v4 UUID whose dashes were
|
|
15
|
+
* stripped — and the run ended in
|
|
16
|
+
* `permanent delivery failure: delivery endpoint HTTP 400:
|
|
17
|
+
* {"code":"invalid_refetch_provenance","message":"refetch_request_id 必须是 UUID"}`.
|
|
18
|
+
*
|
|
19
|
+
* So the boundary canonicalizes instead of forwarding the raw key:
|
|
20
|
+
*
|
|
21
|
+
* - a UUID in any spelling we can recognize (dashed, undashed, UPPER, braces,
|
|
22
|
+
* `urn:uuid:` prefix) becomes the canonical dashed lowercase form, which
|
|
23
|
+
* keeps the receiving service's identity match intact;
|
|
24
|
+
* - a value that is not a UUID at all becomes `''` — an unusable provenance
|
|
25
|
+
* must never fail an otherwise valid delivery, and the empty value is
|
|
26
|
+
* already the documented shape of a scheduled (non-refetch) delivery.
|
|
27
|
+
*
|
|
28
|
+
* Identity is never touched here: the consumer's own idempotency key keeps its
|
|
29
|
+
* exact spelling as the durable `manual_request_id` (slot identity), and only
|
|
30
|
+
* the payload field that crosses the wire is normalized.
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* Canonical dashed lowercase UUID for `value`, or `''` when `value` is not a
|
|
34
|
+
* UUID. Mirrors Python's `uuid.UUID()` leniency exactly: only hyphens may be
|
|
35
|
+
* removed, and what remains must be 32 hex digits.
|
|
36
|
+
*/
|
|
37
|
+
export declare function canonicalRefetchRequestId(value: unknown): string;
|
|
38
|
+
//# sourceMappingURL=refetchProvenance.d.ts.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The `refetch_request_id` wire contract.
|
|
4
|
+
*
|
|
5
|
+
* The receiving service (TelePost) validates this field as a *canonical* dashed
|
|
6
|
+
* lowercase UUID — `utils/api_server.py:706-715` answers HTTP 400
|
|
7
|
+
* `invalid_refetch_provenance` ("refetch_request_id 必须是 UUID") for anything
|
|
8
|
+
* else — and it rejects the whole submission, permanently, before any review
|
|
9
|
+
* exists. It is strict on purpose: the stored value must later match the
|
|
10
|
+
* refetch attempt's own `request_id`, which TelePost generates as
|
|
11
|
+
* `str(uuid.uuid4())`.
|
|
12
|
+
*
|
|
13
|
+
* Callers hand us their key in whatever spelling they generated it. Production
|
|
14
|
+
* incident 2026-09-27: a manual refetch arrived at the delivery with the bare
|
|
15
|
+
* 32-hex key `1e22b55cb33e47289f30c62e8ee1e11f` — a v4 UUID whose dashes were
|
|
16
|
+
* stripped — and the run ended in
|
|
17
|
+
* `permanent delivery failure: delivery endpoint HTTP 400:
|
|
18
|
+
* {"code":"invalid_refetch_provenance","message":"refetch_request_id 必须是 UUID"}`.
|
|
19
|
+
*
|
|
20
|
+
* So the boundary canonicalizes instead of forwarding the raw key:
|
|
21
|
+
*
|
|
22
|
+
* - a UUID in any spelling we can recognize (dashed, undashed, UPPER, braces,
|
|
23
|
+
* `urn:uuid:` prefix) becomes the canonical dashed lowercase form, which
|
|
24
|
+
* keeps the receiving service's identity match intact;
|
|
25
|
+
* - a value that is not a UUID at all becomes `''` — an unusable provenance
|
|
26
|
+
* must never fail an otherwise valid delivery, and the empty value is
|
|
27
|
+
* already the documented shape of a scheduled (non-refetch) delivery.
|
|
28
|
+
*
|
|
29
|
+
* Identity is never touched here: the consumer's own idempotency key keeps its
|
|
30
|
+
* exact spelling as the durable `manual_request_id` (slot identity), and only
|
|
31
|
+
* the payload field that crosses the wire is normalized.
|
|
32
|
+
*/
|
|
33
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
|
+
exports.canonicalRefetchRequestId = canonicalRefetchRequestId;
|
|
35
|
+
/**
|
|
36
|
+
* Canonical dashed lowercase UUID for `value`, or `''` when `value` is not a
|
|
37
|
+
* UUID. Mirrors Python's `uuid.UUID()` leniency exactly: only hyphens may be
|
|
38
|
+
* removed, and what remains must be 32 hex digits.
|
|
39
|
+
*/
|
|
40
|
+
function canonicalRefetchRequestId(value) {
|
|
41
|
+
if (typeof value !== 'string')
|
|
42
|
+
return '';
|
|
43
|
+
let candidate = value.trim().toLowerCase();
|
|
44
|
+
if (!candidate)
|
|
45
|
+
return '';
|
|
46
|
+
if (candidate.startsWith('urn:uuid:'))
|
|
47
|
+
candidate = candidate.slice('urn:uuid:'.length);
|
|
48
|
+
if (candidate.startsWith('{') && candidate.endsWith('}'))
|
|
49
|
+
candidate = candidate.slice(1, -1);
|
|
50
|
+
const hex = candidate.replace(/-/g, '');
|
|
51
|
+
if (!/^[0-9a-f]{32}$/.test(hex))
|
|
52
|
+
return '';
|
|
53
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=refetchProvenance.js.map
|
package/dist/delivery/types.d.ts
CHANGED
|
@@ -14,6 +14,12 @@ export interface DownloadedArtifact {
|
|
|
14
14
|
pixivId: string;
|
|
15
15
|
type: DeliveryItemType;
|
|
16
16
|
title: string;
|
|
17
|
+
/**
|
|
18
|
+
* Parent series name for a series-novel chapter, when the API response
|
|
19
|
+
* carried one. This is distinct from `title`, which holds the chapter title
|
|
20
|
+
* only. Empty when the work is not part of a Pixiv series.
|
|
21
|
+
*/
|
|
22
|
+
seriesTitle?: string;
|
|
17
23
|
/**
|
|
18
24
|
* Pixiv author (artist) display name, when the API response carried one.
|
|
19
25
|
*
|
|
@@ -57,6 +63,13 @@ export interface DownloadedArtifact {
|
|
|
57
63
|
export declare function deliveryFilePaths(artifact: DownloadedArtifact): string[];
|
|
58
64
|
export interface DeliveryContext {
|
|
59
65
|
title: string;
|
|
66
|
+
/**
|
|
67
|
+
* Series (series novel) name, when the work belongs to a Pixiv series.
|
|
68
|
+
* A series novel's `title` holds the chapter title only; this carries the
|
|
69
|
+
* parent series name so templates can render e.g. `《系列名》 章节名`.
|
|
70
|
+
* Empty when the work is not part of a series.
|
|
71
|
+
*/
|
|
72
|
+
seriesTitle?: string;
|
|
60
73
|
pixivId: string;
|
|
61
74
|
type: DeliveryItemType;
|
|
62
75
|
/**
|
|
@@ -386,6 +386,7 @@ class NovelDownloader {
|
|
|
386
386
|
pixivId: workId,
|
|
387
387
|
type: 'novel',
|
|
388
388
|
title: detail.title,
|
|
389
|
+
seriesTitle: detail.series?.title || undefined,
|
|
389
390
|
// Attribution travels with the artifact: a publishing provider (e.g. the
|
|
390
391
|
// TelePost submission target) renders it into its own fields, so the
|
|
391
392
|
// downloader must not be the only place that knows the author.
|
|
@@ -10,7 +10,11 @@ import { TargetConfig } from '../../config';
|
|
|
10
10
|
* `refetchRequestId` is the request UUID of a remote manual replacement ("重抓")
|
|
11
11
|
* that produced this delivery; it is empty for scheduled occurrences. The
|
|
12
12
|
* receiving service (TelePost) correlates the incoming review with the refetch
|
|
13
|
-
* attempt on this id
|
|
13
|
+
* attempt on this id, and validates it as a canonical UUID — see
|
|
14
|
+
* `delivery/refetchProvenance.ts` for why an unusable key becomes empty instead
|
|
15
|
+
* of failing the whole delivery. The consumer's idempotency key itself keeps its
|
|
16
|
+
* exact spelling as the durable slot identity; only this wire field is
|
|
17
|
+
* normalized.
|
|
14
18
|
*/
|
|
15
19
|
export declare function deliveryContextFields(target: TargetConfig): Record<string, unknown>;
|
|
16
20
|
//# sourceMappingURL=deliveryContext.d.ts.map
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.deliveryContextFields = deliveryContextFields;
|
|
4
|
+
const logger_1 = require("../../logger");
|
|
5
|
+
const refetchProvenance_1 = require("../../delivery/refetchProvenance");
|
|
4
6
|
/**
|
|
5
7
|
* Delivery payload context for one target run, merging schedule/slot provenance
|
|
6
8
|
* with the optional remote manual replacement identity.
|
|
@@ -12,12 +14,34 @@ exports.deliveryContextFields = deliveryContextFields;
|
|
|
12
14
|
* `refetchRequestId` is the request UUID of a remote manual replacement ("重抓")
|
|
13
15
|
* that produced this delivery; it is empty for scheduled occurrences. The
|
|
14
16
|
* receiving service (TelePost) correlates the incoming review with the refetch
|
|
15
|
-
* attempt on this id
|
|
17
|
+
* attempt on this id, and validates it as a canonical UUID — see
|
|
18
|
+
* `delivery/refetchProvenance.ts` for why an unusable key becomes empty instead
|
|
19
|
+
* of failing the whole delivery. The consumer's idempotency key itself keeps its
|
|
20
|
+
* exact spelling as the durable slot identity; only this wire field is
|
|
21
|
+
* normalized.
|
|
16
22
|
*/
|
|
17
23
|
function deliveryContextFields(target) {
|
|
18
24
|
const ec = target.delivery;
|
|
19
25
|
const slotContext = ec?.slotContext;
|
|
20
26
|
const executionContext = ec?.executionContext;
|
|
27
|
+
const manualRequestId = slotContext?.manualRequestId ?? '';
|
|
28
|
+
const refetchRequestId = (0, refetchProvenance_1.canonicalRefetchRequestId)(manualRequestId);
|
|
29
|
+
if (manualRequestId && !refetchRequestId) {
|
|
30
|
+
// Visible, not silent: the delivery still goes out (the receiving service
|
|
31
|
+
// treats an empty provenance as "not a refetch"), but the operator must be
|
|
32
|
+
// able to see that this manual run cannot be correlated remotely.
|
|
33
|
+
logger_1.logger.warn('Manual refetch request id is not a UUID; sending an empty refetch_request_id so the delivery is not rejected', {
|
|
34
|
+
slotId: slotContext?.slotId ?? '',
|
|
35
|
+
manualRequestIdLength: manualRequestId.length,
|
|
36
|
+
manualRequestIdPrefix: manualRequestId.slice(0, 8),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
else if (manualRequestId && manualRequestId !== refetchRequestId) {
|
|
40
|
+
logger_1.logger.info('Canonicalized the manual refetch request id for the delivery payload', {
|
|
41
|
+
slotId: slotContext?.slotId ?? '',
|
|
42
|
+
canonicalRefetchRequestId: refetchRequestId,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
21
45
|
return {
|
|
22
46
|
scheduleId: executionContext?.scheduleId ?? '',
|
|
23
47
|
executionId: executionContext?.slotId ?? '',
|
|
@@ -26,7 +50,7 @@ function deliveryContextFields(target) {
|
|
|
26
50
|
slotId: slotContext?.slotId ?? executionContext?.slotId ?? '',
|
|
27
51
|
slotName: slotContext?.slotName ?? executionContext?.slotName ?? '',
|
|
28
52
|
slotDate: slotContext?.slotDate ?? executionContext?.slotDate ?? '',
|
|
29
|
-
refetchRequestId
|
|
53
|
+
refetchRequestId,
|
|
30
54
|
};
|
|
31
55
|
}
|
|
32
56
|
//# sourceMappingURL=deliveryContext.js.map
|
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: '3.
|
|
5
|
+
exports.BUILD = { version: '3.5.0', commit: '644b3dfb5551' };
|
|
6
6
|
//# sourceMappingURL=version.js.map
|
package/dist/webui/package.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pixivflow",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.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",
|