pixivflow 2.25.0 → 2.27.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.
@@ -168,7 +168,14 @@
168
168
  "limit": 1,
169
169
  "storageMode": "cache",
170
170
  "delivery": {
171
- "target": "telepost-bot1"
171
+ "target": "telepost-bot1",
172
+ "richNovelPreview": {
173
+ "url": "http://127.0.0.1:8000/publish/rich-novel",
174
+ "headers": {
175
+ "Authorization": "Bearer ${TELEPRESS_API_KEY}"
176
+ },
177
+ "field": "novel_preview_url"
178
+ }
172
179
  }
173
180
  },
174
181
  {
@@ -43,6 +43,8 @@ export interface TargetDeliveryConfig {
43
43
  target: string;
44
44
  /** 覆盖该交付目标的表单字段,支持 {{title}} 等模板变量 */
45
45
  fields?: Record<string, DeliveryFieldValue>;
46
+ /** Optional rich-novel preview enrichment before the submission. */
47
+ richNovelPreview?: RichNovelPreviewConfig;
46
48
  /**
47
49
  * Schedule slot provenance injected at runtime for scheduled/external runs
48
50
  * (not authored in config). Rendered as {{slotId}}/{{slotName}}/{{slotDate}}.
@@ -538,6 +540,23 @@ export interface HttpMultipartSuccessConfig {
538
540
  /** jsonPath 对应的期望值 */
539
541
  equals?: string | number | boolean | null;
540
542
  }
543
+ /**
544
+ * Optional rich-novel preview publish step performed BEFORE the multipart
545
+ * submission is enqueued. Widget is TelePress (/publish/rich-novel): the novel
546
+ * markdown sidecar + inline images are posted there and the returned Telegraph
547
+ * URL is injected into the submission's `fields` under `field`. It is an
548
+ * enrichment — a failure never fails the submission; TXT/ZIP stay authoritative.
549
+ */
550
+ export interface RichNovelPreviewConfig {
551
+ /** TelePress `/publish/rich-novel` endpoint (http(s), supports ${ENV_NAME}). */
552
+ url: string;
553
+ /** Request headers (e.g. Authorization: Bearer ${TELEPRESS_API_KEY}). */
554
+ headers?: Record<string, string>;
555
+ /** Per-request timeout in milliseconds (default 60000). */
556
+ timeoutMs?: number;
557
+ /** Submission field receiving the Telegraph URL (default novel_preview_url). */
558
+ field?: string;
559
+ }
541
560
  export interface HttpMultipartDeliveryConfig {
542
561
  type: 'httpMultipart';
543
562
  url: string;
@@ -269,6 +269,21 @@ function validateConfig(config, location, databasePath) {
269
269
  errors.push(`targets[${index}].delivery.target: Unknown delivery target "${deliveryTarget}"`);
270
270
  }
271
271
  }
272
+ if (target.delivery?.richNovelPreview) {
273
+ if (!target.delivery.richNovelPreview.url?.trim()) {
274
+ errors.push(`targets[${index}].delivery.richNovelPreview.url: Required field is missing or empty`);
275
+ }
276
+ else {
277
+ try {
278
+ const url = new URL(target.delivery.richNovelPreview.url);
279
+ if (!['http:', 'https:'].includes(url.protocol))
280
+ throw new Error('unsupported protocol');
281
+ }
282
+ catch {
283
+ errors.push(`targets[${index}].delivery.richNovelPreview.url: Must be a valid HTTP or HTTPS URL`);
284
+ }
285
+ }
286
+ }
272
287
  });
273
288
  }
274
289
  for (const [name, delivery] of Object.entries(config.delivery?.targets ?? {})) {
@@ -0,0 +1,40 @@
1
+ import type { DownloadedArtifact } from './types';
2
+ export interface RichNovelPreviewResult {
3
+ /** Telegraph "read online" URL for the published page. */
4
+ url: string;
5
+ /** Per-asset diagnostic returned by TelePress ({local, remote, status}). */
6
+ assets?: Array<{
7
+ local: string;
8
+ remote?: string | null;
9
+ status: string;
10
+ }>;
11
+ }
12
+ export interface RichNovelPublishOptions {
13
+ url: string;
14
+ headers?: Record<string, string>;
15
+ timeoutMs?: number;
16
+ }
17
+ /**
18
+ * Locate the rich-markdown sidecar and its inline image directory for a novel
19
+ * artifact. Pure-text novels (no images dir / no sidecar) return undefined so
20
+ * the caller can skip enrichment with zero behaviour change.
21
+ */
22
+ export declare function interpolateEnv(value: string): string;
23
+ export declare function findRichNovelSources(artifact: DownloadedArtifact): {
24
+ txtPath: string;
25
+ mdPath: string;
26
+ imagePaths: string[];
27
+ } | undefined;
28
+ /**
29
+ * Publish a rich novel (markdown + inline images) to TelePress and return the
30
+ * Telegraph URL. Client-side only — TelePress owns rendering/Catbox/Telegraph.
31
+ *
32
+ * Failures classify as retryable when the endpoint was reached but answered
33
+ * non-2xx (Transient), and non-retryable when local input is unusable.
34
+ */
35
+ export declare function publishRichNovelPreview(artifact: DownloadedArtifact, options: RichNovelPublishOptions): Promise<{
36
+ url: string;
37
+ retryable: boolean;
38
+ operatorHint?: string;
39
+ }>;
40
+ //# sourceMappingURL=TelePressRichNovel.d.ts.map
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.interpolateEnv = interpolateEnv;
37
+ exports.findRichNovelSources = findRichNovelSources;
38
+ exports.publishRichNovelPreview = publishRichNovelPreview;
39
+ const fs = __importStar(require("node:fs"));
40
+ const path = __importStar(require("node:path"));
41
+ const node_crypto_1 = require("node:crypto");
42
+ const logger_1 = require("../logger");
43
+ const redact_1 = require("../utils/redact");
44
+ /**
45
+ * Locate the rich-markdown sidecar and its inline image directory for a novel
46
+ * artifact. Pure-text novels (no images dir / no sidecar) return undefined so
47
+ * the caller can skip enrichment with zero behaviour change.
48
+ */
49
+ function interpolateEnv(value) {
50
+ return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, name) => {
51
+ const resolved = process.env[name];
52
+ if (resolved === undefined) {
53
+ throw new Error(`Required TelePress environment variable is not set: ${name}`);
54
+ }
55
+ return resolved;
56
+ });
57
+ }
58
+ function findRichNovelSources(artifact) {
59
+ if (artifact.type !== 'novel')
60
+ return undefined;
61
+ const txtPath = artifact.files.find((f) => /\.txt$/i.test(f));
62
+ if (!txtPath)
63
+ return undefined;
64
+ const mdPath = txtPath.replace(/\.txt$/i, '.md');
65
+ if (!fs.existsSync(mdPath))
66
+ return undefined;
67
+ const imagesDir = path.join(path.dirname(txtPath), 'images');
68
+ let imagePaths = [];
69
+ if (fs.existsSync(imagesDir)) {
70
+ imagePaths = fs
71
+ .readdirSync(imagesDir)
72
+ .filter((name) => /\.(jpe?g|png|gif|webp|bmp)$/i.test(name))
73
+ .map((name) => path.join(imagesDir, name))
74
+ .sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
75
+ }
76
+ return { txtPath, mdPath, imagePaths };
77
+ }
78
+ /**
79
+ * Publish a rich novel (markdown + inline images) to TelePress and return the
80
+ * Telegraph URL. Client-side only — TelePress owns rendering/Catbox/Telegraph.
81
+ *
82
+ * Failures classify as retryable when the endpoint was reached but answered
83
+ * non-2xx (Transient), and non-retryable when local input is unusable.
84
+ */
85
+ async function publishRichNovelPreview(artifact, options) {
86
+ const sources = findRichNovelSources(artifact);
87
+ if (!sources)
88
+ return { url: '', retryable: false, operatorHint: 'no_rich_novel_assets' };
89
+ if (sources.imagePaths.length === 0) {
90
+ return { url: '', retryable: false, operatorHint: 'no_rich_novel_assets' };
91
+ }
92
+ const mdName = path.basename(sources.mdPath);
93
+ const boundary = `telepress-${(0, node_crypto_1.randomUUID)()}`;
94
+ const fields = [];
95
+ // Text part for the markdown file.
96
+ fields.push(Buffer.from(`--${boundary}\r\n` +
97
+ `Content-Disposition: form-data; name="md"; filename="${escape(mdName)}"\r\n` +
98
+ `Content-Type: text/markdown\r\n\r\n`));
99
+ fields.push(await fs.promises.readFile(sources.mdPath));
100
+ fields.push(Buffer.from('\r\n'));
101
+ // File parts for each inline image, named with the `images/` prefix so the
102
+ // relative markdown refs resolve on the receiving side.
103
+ for (const imagePath of sources.imagePaths) {
104
+ const name = `images/${path.basename(imagePath)}`;
105
+ const stat = await fs.promises.stat(imagePath);
106
+ if (stat.size <= 0)
107
+ continue;
108
+ const header = Buffer.from(`--${boundary}\r\n` +
109
+ `Content-Disposition: form-data; name="images"; filename="${escape(name)}"\r\n` +
110
+ `Content-Type: application/octet-stream\r\n\r\n`);
111
+ const data = await fs.promises.readFile(imagePath);
112
+ fields.push(header, data, Buffer.from('\r\n'));
113
+ }
114
+ fields.push(Buffer.from(`--${boundary}--\r\n`));
115
+ const timeoutMs = options.timeoutMs ?? 60_000;
116
+ const headers = {
117
+ 'Content-Type': `multipart/form-data; boundary=${boundary}`,
118
+ 'Content-Length': String(fields.reduce((n, b) => n + b.length, 0)),
119
+ ...options.headers,
120
+ };
121
+ const controller = new AbortController();
122
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
123
+ timer.unref?.();
124
+ try {
125
+ const response = await fetch(options.url, { method: 'POST', headers, body: Buffer.concat(fields), signal: controller.signal });
126
+ const text = await response.text();
127
+ let body = text;
128
+ try {
129
+ body = JSON.parse(text);
130
+ }
131
+ catch { /* plain text */ }
132
+ if (!response.ok) {
133
+ logger_1.logger.warn('TelePress rich novel publish returned an error status', {
134
+ url: (0, redact_1.redactUrl)(options.url),
135
+ status: response.status,
136
+ body: String(body).slice(0, 300),
137
+ });
138
+ return { url: '', retryable: response.status >= 500 || response.status === 408 || response.status === 429, operatorHint: `telepress_http_${response.status}` };
139
+ }
140
+ const data = (body && typeof body === 'object' ? body : undefined);
141
+ const url = typeof data?.url === 'string' ? data.url : '';
142
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
143
+ return { url: '', retryable: true, operatorHint: 'telepress_invalid_response' };
144
+ }
145
+ logger_1.logger.info('TelePress rich novel publish succeeded', {
146
+ url: (0, redact_1.redactUrl)(url),
147
+ images: sources.imagePaths.length,
148
+ });
149
+ return { url, retryable: false };
150
+ }
151
+ catch (error) {
152
+ const aborted = error?.name === 'AbortError';
153
+ logger_1.logger.warn('TelePress rich novel publish failed', {
154
+ url: (0, redact_1.redactUrl)(options.url),
155
+ retryable: !aborted,
156
+ reason: aborted ? 'timeout' : String(error),
157
+ });
158
+ return { url: '', retryable: !aborted, operatorHint: aborted ? 'telepress_timeout' : 'telepress_network_error' };
159
+ }
160
+ finally {
161
+ clearTimeout(timer);
162
+ }
163
+ }
164
+ function escape(value) {
165
+ return value.replace(/[\r\n]/g, ' ').replace(/"/g, '%22');
166
+ }
167
+ //# sourceMappingURL=TelePressRichNovel.js.map
@@ -38,6 +38,7 @@ const logger_1 = require("../logger");
38
38
  const node_path_1 = require("node:path");
39
39
  const language_detection_1 = require("../utils/language-detection");
40
40
  const novelMarkers_1 = require("./novelMarkers");
41
+ const zip_1 = require("../utils/zip");
41
42
  const LANGUAGE_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
42
43
  class NovelDownloader {
43
44
  client;
@@ -188,9 +189,11 @@ class NovelDownloader {
188
189
  // Rich-media markdown sidecar (RFC 1 Phase 2): same path as the .txt
189
190
  // (compat format stays), inline images become relative ![](images/x.jpg)
190
191
  // refs so a later TelePress/TelePost phase can render the Telegraph page.
192
+ let richMediaPath;
191
193
  if (assets.some((a) => a.status === 'downloaded')) {
192
194
  try {
193
195
  const mdPath = await this.fileService.saveText(`${header}\n${(0, novelMarkers_1.renderNovelMarkdown)(text, assets)}`, fileName.replace(/\.txt$/, '.md'), metadata);
196
+ richMediaPath = mdPath;
194
197
  logger_1.logger.info(`Saved novel ${detail.id} rich-media markdown sidecar`, { filePath: mdPath });
195
198
  }
196
199
  catch (error) {
@@ -243,6 +246,33 @@ class NovelDownloader {
243
246
  catch (error) {
244
247
  logger_1.logger.warn(`Failed to save metadata for novel ${detail.id}: ${error instanceof Error ? error.message : String(error)}`);
245
248
  }
249
+ // Rich-media ZIP archive (Phase 3): a save-only download package carrying
250
+ // txt + md + metadata + images. The md stays an internal render input, never
251
+ // a user-facing attachment; txt remains the authoritative/compat file.
252
+ let archivePath;
253
+ if (assets.some((a) => a.status === 'downloaded')) {
254
+ const zipName = this.fileService.sanitizeFileName(`${detail.id}_${detail.title}.zip`);
255
+ const dest = (0, node_path_1.join)((0, node_path_1.dirname)(filePath), zipName);
256
+ const entries = [
257
+ { name: fileName, sourcePath: filePath },
258
+ ];
259
+ if (richMediaPath)
260
+ entries.push({ name: `${fileName.replace(/\.txt$/, '.md')}`, sourcePath: richMediaPath });
261
+ if (metadataPath)
262
+ entries.push({ name: `${fileName}.json`, sourcePath: metadataPath });
263
+ for (const a of assets) {
264
+ if (a.status === 'downloaded' && a.localPath) {
265
+ entries.push({ name: `images/${(0, node_path_1.basename)(a.localPath)}`, sourcePath: a.localPath });
266
+ }
267
+ }
268
+ try {
269
+ archivePath = await (0, zip_1.createZipArchive)(dest, entries);
270
+ logger_1.logger.info(`Saved novel ${detail.id} zip archive`, { filePath: archivePath });
271
+ }
272
+ catch (error) {
273
+ logger_1.logger.warn(`Failed to save zip archive for novel ${detail.id}: ${error instanceof Error ? error.message : String(error)}`);
274
+ }
275
+ }
246
276
  this.database.insertDownload({
247
277
  pixivId: String(detail.id),
248
278
  type: 'novel',
@@ -264,7 +294,7 @@ class NovelDownloader {
264
294
  type: 'novel',
265
295
  title: detail.title,
266
296
  tags: tags.map((item) => item.name).filter(Boolean),
267
- files: [filePath],
297
+ files: archivePath ? [filePath, archivePath] : [filePath],
268
298
  cleanupFiles: metadataPath ? [metadataPath] : [],
269
299
  spoiler: (detail.x_restrict ?? 0) > 0,
270
300
  xRestrict: detail.x_restrict,
@@ -108,5 +108,11 @@ export declare class NovelTargetHandler {
108
108
  * second concurrent worker lose this race instead of double-submitting.
109
109
  */
110
110
  private recordArtifactOutcome;
111
+ /**
112
+ * Optional rich-novel preview enrichment (TelePress /publish/rich-novel).
113
+ * Non-fatal by design: a preview failure never blocks the TXT/ZIP
114
+ * submission, but it is surfaced with the operational result contract.
115
+ */
116
+ private enrichRichNovelPreview;
111
117
  }
112
118
  //# sourceMappingURL=NovelTargetHandler.d.ts.map
@@ -12,6 +12,7 @@ const observability_1 = require("../../observability");
12
12
  const context_1 = require("../../observability/context");
13
13
  const DownloadPlanner_1 = require("../plan/DownloadPlanner");
14
14
  const deliveryContext_1 = require("./deliveryContext");
15
+ const TelePressRichNovel_1 = require("../../delivery/TelePressRichNovel");
15
16
  class NovelTargetHandler {
16
17
  client;
17
18
  database;
@@ -678,7 +679,7 @@ class NovelTargetHandler {
678
679
  // candidate and the scan could never advance — the whole point of this
679
680
  // change. `releaseCellWork` itself refuses once the cell moved on
680
681
  // (delivery_pending/submitted), so a committed identity stays stable.
681
- const attempt = this.recordArtifactOutcome(artifact, target);
682
+ const attempt = await this.recordArtifactOutcome(artifact, target);
682
683
  if (attempt.kind === 'skipped' && execution && !recovering) {
683
684
  execution.release(workId);
684
685
  }
@@ -692,7 +693,7 @@ class NovelTargetHandler {
692
693
  * slot as a `duplicate`. The delivery idempotency ledger is what makes the
693
694
  * second concurrent worker lose this race instead of double-submitting.
694
695
  */
695
- recordArtifactOutcome(artifact, target) {
696
+ async recordArtifactOutcome(artifact, target) {
696
697
  const isDelivery = target.storageMode === 'cache' && target.delivery?.target?.trim();
697
698
  if (!isDelivery || !this.deliveryService) {
698
699
  this.outcomes.push({ kind: 'stored', workId: artifact.pixivId, workType: artifact.type });
@@ -710,9 +711,10 @@ class NovelTargetHandler {
710
711
  },
711
712
  };
712
713
  }
714
+ const fields = await this.enrichRichNovelPreview(artifact, target);
713
715
  const res = this.deliveryService.enqueue(artifact, target, {
714
716
  slotId,
715
- fields: target.delivery?.fields,
717
+ fields: fields ?? target.delivery?.fields,
716
718
  extraContext: (0, deliveryContext_1.deliveryContextFields)(target),
717
719
  });
718
720
  if (res.duplicate) {
@@ -733,6 +735,45 @@ class NovelTargetHandler {
733
735
  });
734
736
  return { kind: 'selected', workId: artifact.pixivId, workType: artifact.type };
735
737
  }
738
+ /**
739
+ * Optional rich-novel preview enrichment (TelePress /publish/rich-novel).
740
+ * Non-fatal by design: a preview failure never blocks the TXT/ZIP
741
+ * submission, but it is surfaced with the operational result contract.
742
+ */
743
+ async enrichRichNovelPreview(artifact, target) {
744
+ const delivery = target.delivery;
745
+ if (!delivery?.richNovelPreview?.url)
746
+ return undefined;
747
+ if (!(0, TelePressRichNovel_1.findRichNovelSources)(artifact))
748
+ return undefined;
749
+ const cfg = delivery.richNovelPreview;
750
+ const headers = {};
751
+ for (const [key, value] of Object.entries(cfg.headers ?? {})) {
752
+ headers[key] = (0, TelePressRichNovel_1.interpolateEnv)(String(value));
753
+ }
754
+ const result = await (0, TelePressRichNovel_1.publishRichNovelPreview)(artifact, {
755
+ url: (0, TelePressRichNovel_1.interpolateEnv)(cfg.url),
756
+ headers,
757
+ timeoutMs: cfg.timeoutMs,
758
+ });
759
+ const field = cfg.field ?? 'novel_preview_url';
760
+ if (result.url) {
761
+ logger_1.logger.info('Rich novel preview URL ready', {
762
+ pixivId: artifact.pixivId,
763
+ field,
764
+ url: result.url.slice(0, 120),
765
+ });
766
+ return { ...(target.delivery?.fields ?? {}), [field]: result.url };
767
+ }
768
+ logger_1.logger.warn('Rich novel preview skipped', {
769
+ code: 'NOVEL_PUBLISH_FAILED',
770
+ stage: 'telepress_publish',
771
+ retryable: result.retryable,
772
+ operator_hint: result.operatorHint ?? 'unknown',
773
+ pixivId: artifact.pixivId,
774
+ });
775
+ return undefined;
776
+ }
736
777
  }
737
778
  exports.NovelTargetHandler = NovelTargetHandler;
738
779
  //# sourceMappingURL=NovelTargetHandler.js.map
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "type": "commonjs",
3
3
  "name": "pixivflow",
4
- "version": "2.25.0",
4
+ "version": "2.27.0",
5
5
  "private": true
6
6
  }
@@ -0,0 +1,14 @@
1
+ export interface ArchiveEntry {
2
+ /** Path the file is stored under inside the zip (e.g. "images/001.jpg"). */
3
+ name: string;
4
+ /** Absolute path of the file on disk. */
5
+ sourcePath: string;
6
+ }
7
+ /**
8
+ * Create a zip archive at `destPath` containing `entries` (order preserved).
9
+ * Returns destPath on success; on failure removes a partial destination so a
10
+ * caller never hands a half-written archive to a delivery. Pure FS helper — no
11
+ * domain logic.
12
+ */
13
+ export declare function createZipArchive(destPath: string, entries: ArchiveEntry[]): Promise<string>;
14
+ //# sourceMappingURL=zip.d.ts.map
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createZipArchive = createZipArchive;
4
+ const node_fs_1 = require("node:fs");
5
+ const node_fs_2 = require("node:fs");
6
+ const node_module_1 = require("node:module");
7
+ // archiver ships no matching TS factory types; bind it at runtime via
8
+ // createRequire and keep a minimal local contract (nothing Pixiv-specific).
9
+ const archiver = (0, node_module_1.createRequire)(__filename)('archiver');
10
+ /**
11
+ * Create a zip archive at `destPath` containing `entries` (order preserved).
12
+ * Returns destPath on success; on failure removes a partial destination so a
13
+ * caller never hands a half-written archive to a delivery. Pure FS helper — no
14
+ * domain logic.
15
+ */
16
+ async function createZipArchive(destPath, entries) {
17
+ const output = (0, node_fs_1.createWriteStream)(destPath);
18
+ const archive = archiver('zip', { zlib: { level: 9 } });
19
+ const settled = new Promise((resolve, reject) => {
20
+ output.on('close', () => resolve(destPath));
21
+ output.on('error', reject);
22
+ archive.on('error', reject);
23
+ });
24
+ try {
25
+ archive.pipe(output);
26
+ for (const e of entries) {
27
+ archive.file(e.sourcePath, { name: e.name });
28
+ }
29
+ await archive.finalize();
30
+ await settled;
31
+ await node_fs_2.promises.access(destPath);
32
+ return destPath;
33
+ }
34
+ catch (error) {
35
+ try {
36
+ await node_fs_2.promises.rm(destPath, { force: true });
37
+ }
38
+ catch {
39
+ /* best-effort cleanup */
40
+ }
41
+ throw new Error(`Failed to create zip archive: ${error instanceof Error ? error.message : String(error)}`);
42
+ }
43
+ }
44
+ //# sourceMappingURL=zip.js.map
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.25.0', commit: '532ba3534889' };
5
+ exports.BUILD = { version: '2.27.0', commit: '491a2a8f7abd' };
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.25.0",
4
+ "version": "2.27.0",
5
5
  "description": "PixivFlow WebUI Backend - CommonJS module"
6
6
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pixivflow",
3
- "version": "2.25.0",
4
- "description": "\ud83c\udfa8 Pixiv \u4e0b\u8f7d\u3001\u7b5b\u9009\u4e0e\u81ea\u52a8\u6536\u96c6\u5de5\u5177 - \u6279\u91cf\u4e0b\u8f7d\u63d2\u753b\u548c\u5c0f\u8bf4\u3001\u6309\u6807\u7b7e/\u70ed\u5ea6/\u65e5\u671f\u7b5b\u9009\u3001\u5b9a\u65f6\u4efb\u52a1\u4e0e\u53ef\u9760 HTTP \u4ea4\u4ed8 | Pixiv downloader and automation toolkit with filtering, scheduling and reliable HTTP delivery",
3
+ "version": "2.27.0",
4
+ "description": "🎨 Pixiv 下载、筛选与自动收集工具 - 批量下载插画和小说、按标签/热度/日期筛选、定时任务与可靠 HTTP 交付 | Pixiv downloader and automation toolkit with filtering, scheduling and reliable HTTP delivery",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/redtidev1918/PixivFlow.git"
@@ -116,6 +116,7 @@
116
116
  },
117
117
  "dependencies": {
118
118
  "@redtidev/pixiv-client": "0.1.0",
119
+ "archiver": "^7.0.1",
119
120
  "axios": "^1.20.0",
120
121
  "cors": "^2.8.6",
121
122
  "cron-parser": "^4.9.0",