pixivflow 2.16.1 → 2.18.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 (143) hide show
  1. package/{README_EN.md → README.en.md} +8 -6
  2. package/README.md +8 -6
  3. package/config/examples/README.md +218 -0
  4. package/config/examples/multi-tag-or-limit.zh.json +49 -0
  5. package/config/examples/specific-download.example.json +173 -0
  6. package/config/examples/standalone.config.example.json +167 -0
  7. package/config/examples/standalone.config.novel-chinese.json +124 -0
  8. package/config/examples/standalone.config.ranking.json +135 -0
  9. package/config/examples/standalone.config.simple.json +52 -0
  10. package/config/examples/yesterday-popular-novel.zh.json +48 -0
  11. package/config/examples/yesterday-ranking-illustration.json +43 -0
  12. package/dist/auth/PixivAuth.d.ts +12 -0
  13. package/dist/auth/PixivAuth.d.ts.map +1 -1
  14. package/dist/auth/PixivAuth.js +26 -0
  15. package/dist/auth/PixivAuth.js.map +1 -1
  16. package/dist/batch/executionResult.d.ts +85 -0
  17. package/dist/batch/executionResult.d.ts.map +1 -0
  18. package/dist/batch/executionResult.js +102 -0
  19. package/dist/batch/executionResult.js.map +1 -0
  20. package/dist/batch/runDiagnostics.d.ts +48 -0
  21. package/dist/batch/runDiagnostics.d.ts.map +1 -0
  22. package/dist/batch/runDiagnostics.js +103 -0
  23. package/dist/batch/runDiagnostics.js.map +1 -0
  24. package/dist/commands/Command.d.ts +5 -0
  25. package/dist/commands/Command.d.ts.map +1 -1
  26. package/dist/commands/Command.js +7 -0
  27. package/dist/commands/Command.js.map +1 -1
  28. package/dist/commands/DiagnoseEgressCommand.d.ts +98 -0
  29. package/dist/commands/DiagnoseEgressCommand.d.ts.map +1 -0
  30. package/dist/commands/DiagnoseEgressCommand.js +327 -0
  31. package/dist/commands/DiagnoseEgressCommand.js.map +1 -0
  32. package/dist/commands/ExecuteSlotCommand.d.ts +37 -0
  33. package/dist/commands/ExecuteSlotCommand.d.ts.map +1 -0
  34. package/dist/commands/ExecuteSlotCommand.js +267 -0
  35. package/dist/commands/ExecuteSlotCommand.js.map +1 -0
  36. package/dist/commands/MaintainCommand.d.ts.map +1 -1
  37. package/dist/commands/MaintainCommand.js +10 -15
  38. package/dist/commands/MaintainCommand.js.map +1 -1
  39. package/dist/commands/index.d.ts +3 -1
  40. package/dist/commands/index.d.ts.map +1 -1
  41. package/dist/commands/index.js +7 -1
  42. package/dist/commands/index.js.map +1 -1
  43. package/dist/commands/scheduler-runtime.d.ts +37 -1
  44. package/dist/commands/scheduler-runtime.d.ts.map +1 -1
  45. package/dist/commands/scheduler-runtime.js +52 -19
  46. package/dist/commands/scheduler-runtime.js.map +1 -1
  47. package/dist/commands/types.d.ts +6 -0
  48. package/dist/commands/types.d.ts.map +1 -1
  49. package/dist/config/types.d.ts +33 -1
  50. package/dist/config/types.d.ts.map +1 -1
  51. package/dist/config/validation.d.ts +14 -1
  52. package/dist/config/validation.d.ts.map +1 -1
  53. package/dist/config/validation.js +109 -33
  54. package/dist/config/validation.js.map +1 -1
  55. package/dist/config.d.ts +1 -1
  56. package/dist/config.d.ts.map +1 -1
  57. package/dist/config.js.map +1 -1
  58. package/dist/delivery/DeliveryDispatcher.d.ts.map +1 -1
  59. package/dist/delivery/DeliveryDispatcher.js +7 -0
  60. package/dist/delivery/DeliveryDispatcher.js.map +1 -1
  61. package/dist/delivery/HttpMultipartDelivery.d.ts +12 -0
  62. package/dist/delivery/HttpMultipartDelivery.d.ts.map +1 -1
  63. package/dist/delivery/HttpMultipartDelivery.js +89 -70
  64. package/dist/delivery/HttpMultipartDelivery.js.map +1 -1
  65. package/dist/delivery/LegacyOutboxMigration.js +1 -1
  66. package/dist/delivery/LegacyOutboxMigration.js.map +1 -1
  67. package/dist/delivery/TelegramReviewDelivery.d.ts +49 -0
  68. package/dist/delivery/TelegramReviewDelivery.d.ts.map +1 -0
  69. package/dist/delivery/TelegramReviewDelivery.js +307 -0
  70. package/dist/delivery/TelegramReviewDelivery.js.map +1 -0
  71. package/dist/download/DownloadManager.d.ts +10 -0
  72. package/dist/download/DownloadManager.d.ts.map +1 -1
  73. package/dist/download/DownloadManager.js +21 -0
  74. package/dist/download/DownloadManager.js.map +1 -1
  75. package/dist/download/plan/DownloadPlanner.d.ts +10 -0
  76. package/dist/download/plan/DownloadPlanner.d.ts.map +1 -1
  77. package/dist/download/plan/DownloadPlanner.js +19 -0
  78. package/dist/download/plan/DownloadPlanner.js.map +1 -1
  79. package/dist/index.js +5 -3
  80. package/dist/index.js.map +1 -1
  81. package/dist/notification/NotificationPolicy.d.ts.map +1 -1
  82. package/dist/notification/NotificationPolicy.js +4 -1
  83. package/dist/notification/NotificationPolicy.js.map +1 -1
  84. package/dist/package.json +1 -1
  85. package/dist/pixiv-token-getter-adapter.d.ts +27 -7
  86. package/dist/pixiv-token-getter-adapter.d.ts.map +1 -1
  87. package/dist/pixiv-token-getter-adapter.js +44 -27
  88. package/dist/pixiv-token-getter-adapter.js.map +1 -1
  89. package/dist/puppeteer-login-adapter/auth-code.d.ts +1 -1
  90. package/dist/puppeteer-login-adapter/auth-code.d.ts.map +1 -1
  91. package/dist/puppeteer-login-adapter/browser-launch.d.ts +21 -0
  92. package/dist/puppeteer-login-adapter/browser-launch.d.ts.map +1 -0
  93. package/dist/puppeteer-login-adapter/browser-launch.js +46 -0
  94. package/dist/puppeteer-login-adapter/browser-launch.js.map +1 -0
  95. package/dist/puppeteer-login-adapter/environment.js +2 -2
  96. package/dist/puppeteer-login-adapter/environment.js.map +1 -1
  97. package/dist/puppeteer-login-adapter/login-headless.d.ts.map +1 -1
  98. package/dist/puppeteer-login-adapter/login-headless.js +2 -5
  99. package/dist/puppeteer-login-adapter/login-headless.js.map +1 -1
  100. package/dist/puppeteer-login-adapter/login-interactive.d.ts.map +1 -1
  101. package/dist/puppeteer-login-adapter/login-interactive.js +2 -5
  102. package/dist/puppeteer-login-adapter/login-interactive.js.map +1 -1
  103. package/dist/storage/Database.d.ts +2 -0
  104. package/dist/storage/Database.d.ts.map +1 -1
  105. package/dist/storage/Database.js +7 -6
  106. package/dist/storage/Database.js.map +1 -1
  107. package/dist/storage/DatabaseMigration.d.ts +2 -2
  108. package/dist/storage/DatabaseMigration.d.ts.map +1 -1
  109. package/dist/storage/DatabaseMigration.js.map +1 -1
  110. package/dist/storage/drivers/NodeSqliteDriver.d.ts +14 -0
  111. package/dist/storage/drivers/NodeSqliteDriver.d.ts.map +1 -0
  112. package/dist/storage/drivers/NodeSqliteDriver.js +123 -0
  113. package/dist/storage/drivers/NodeSqliteDriver.js.map +1 -0
  114. package/dist/storage/drivers/SqliteDriver.d.ts +55 -0
  115. package/dist/storage/drivers/SqliteDriver.d.ts.map +1 -0
  116. package/dist/storage/drivers/SqliteDriver.js +21 -0
  117. package/dist/storage/drivers/SqliteDriver.js.map +1 -0
  118. package/dist/storage/repositories/BaseRepository.d.ts +4 -4
  119. package/dist/storage/repositories/BaseRepository.d.ts.map +1 -1
  120. package/dist/storage/repositories/BaseRepository.js.map +1 -1
  121. package/dist/storage/repositories/DownloadRepository.d.ts +2 -1
  122. package/dist/storage/repositories/DownloadRepository.d.ts.map +1 -1
  123. package/dist/storage/repositories/DownloadRepository.js.map +1 -1
  124. package/dist/storage/repositories/RateLimitStateRepository.d.ts +1 -1
  125. package/dist/storage/repositories/RateLimitStateRepository.js +1 -1
  126. package/dist/utils/concurrency.d.ts.map +1 -1
  127. package/dist/utils/concurrency.js +9 -2
  128. package/dist/utils/concurrency.js.map +1 -1
  129. package/dist/utils/config-validator-unified.d.ts.map +1 -1
  130. package/dist/utils/config-validator-unified.js +15 -1
  131. package/dist/utils/config-validator-unified.js.map +1 -1
  132. package/dist/utils/token-maintenance.d.ts +0 -4
  133. package/dist/utils/token-maintenance.d.ts.map +1 -1
  134. package/dist/utils/token-maintenance.js +5 -25
  135. package/dist/utils/token-maintenance.js.map +1 -1
  136. package/dist/version.js +1 -1
  137. package/dist/webui/package.json +1 -1
  138. package/node_modules/@redtidev/pixiv-client/README.en.md +105 -0
  139. package/node_modules/@redtidev/pixiv-client/README.md +67 -73
  140. package/node_modules/@redtidev/pixiv-client/dist/rate-limit/RateLimitGate.d.ts +1 -1
  141. package/node_modules/@redtidev/pixiv-client/dist/rate-limit/RateLimitGate.js +1 -1
  142. package/node_modules/@redtidev/pixiv-client/src/rate-limit/RateLimitGate.ts +1 -1
  143. package/package.json +7 -8
@@ -0,0 +1,327 @@
1
+ "use strict";
2
+ /**
3
+ * `pixivflow diagnose egress` — minimal Pixiv data-plane egress probe.
4
+ *
5
+ * Reachability is not suitability: a datacenter egress can pass OAuth and still be
6
+ * rate-limit-starved on the production search workload (2026-09-11, GitHub-hosted
7
+ * runners). This command answers, with a handful of tiny requests, three INDEPENDENT
8
+ * questions about the network it runs on:
9
+ *
10
+ * 1. OAuth oauth.secure.pixiv.net — one real refresh-token exchange
11
+ * 2. App API app-api.pixiv.net — 1 small fixed request, status + Retry-After
12
+ * 3. Media i.pximg.net — 64 KiB Range request with the app Referer
13
+ *
14
+ * It never runs the business workload (no 12 tags / 100 samples / pagination), so it
15
+ * cannot by itself drive the account into a penalty. Output is a findings list plus a
16
+ * uniform verdict (PASS / DEGRADED / FAIL) suitable for a provider-qualification matrix.
17
+ *
18
+ * Exit codes: 0 = no findings above warn, 1 = warn (degraded evidence), 2 = critical
19
+ * (a data plane is unusable from this egress).
20
+ *
21
+ * The OAuth probe performs ONE real token refresh. PixivAuth persists a rotated refresh
22
+ * token (database + unified storage + config), which is the desired durable behaviour —
23
+ * run this probe only when no production execution holds the credential, exactly like
24
+ * `account rotate`.
25
+ */
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.DiagnoseEgressCommand = void 0;
28
+ const Command_1 = require("./Command");
29
+ const metadata_1 = require("./metadata");
30
+ const Database_1 = require("../storage/Database");
31
+ const PixivAuth_1 = require("../auth/PixivAuth");
32
+ const errors_1 = require("../utils/errors");
33
+ /** The documented placeholder rules: empty, the template string, or implausibly short. */
34
+ function isPlaceholderToken(token) {
35
+ const value = typeof token === 'string' ? token.trim() : '';
36
+ return value.length === 0 || value === 'YOUR_REFRESH_TOKEN' || value.length < 10;
37
+ }
38
+ const PROBE_TIMEOUT_MS = 15_000;
39
+ /** this-safe fetch wrapper (Cloudflare/undici reject `this.fetch(...)`). */
40
+ const defaultFetch = (input, init) => globalThis.fetch(input, init);
41
+ class DiagnoseEgressCommand extends Command_1.BaseCommand {
42
+ deps;
43
+ name = 'diagnose';
44
+ description = 'Minimal Pixiv data-plane egress probe (usage: diagnose egress)';
45
+ aliases = ['diag'];
46
+ requiresToken = false;
47
+ metadata = {
48
+ category: metadata_1.CommandCategory.MONITORING,
49
+ requiresAuth: false,
50
+ longRunning: false,
51
+ examples: ['pixivflow diagnose egress', 'pixivflow diagnose egress --json', 'pixivflow diagnose egress --illust-id 12345'],
52
+ relatedCommands: ['health', 'doctor'],
53
+ };
54
+ constructor(
55
+ /** Injectable for tests; defaults to a this-safe global fetch. */
56
+ deps = {}) {
57
+ super();
58
+ this.deps = deps;
59
+ }
60
+ getUsage() {
61
+ return [
62
+ 'diagnose egress [--json] [--illust-id <id>] [--provider <name>] [--region <code>]',
63
+ ' Minimal Pixiv egress probe: OAuth refresh, one small App API request,',
64
+ ' one 64 KiB media Range request. Exit 0/1/2 = ok/warn/critical.',
65
+ ].join('\n');
66
+ }
67
+ async execute(context, args) {
68
+ const sub = args.positional[0] || 'egress';
69
+ if (sub !== 'egress') {
70
+ return this.failure(`Unknown diagnose target: ${sub}. Try: pixivflow diagnose egress`);
71
+ }
72
+ const findings = [];
73
+ const fetchImpl = this.deps.fetchImpl ?? defaultFetch;
74
+ const provider = String(args.options.provider ?? this.deps.provider ?? 'local');
75
+ const region = String(args.options.region ?? this.deps.region ?? 'unknown');
76
+ const illustId = String(args.options['illust-id'] ?? '92421724');
77
+ const proxy = context.config?.network?.proxy;
78
+ // --no-proxy tests the DIRECT egress even when a proxy is configured: a
79
+ // qualification matrix wants both paths, and a stale proxy setting must not
80
+ // mask what the network itself can do.
81
+ const noProxy = args.options['no-proxy'] === true;
82
+ const useProxy = !noProxy && !!(proxy?.enabled && proxy.host && proxy.port);
83
+ // The OAuth probe builds its own client from config.network — hand it the same
84
+ // overridden view so all three probes test the SAME egress path.
85
+ const networkForAuth = noProxy
86
+ ? { ...(context.config?.network ?? {}), proxy: undefined }
87
+ : context.config?.network ?? {};
88
+ // --- 1. General reachability (pixiv.net answers 403 to probes; that IS a pass) ---
89
+ await this.probeReachable(fetchImpl, useProxy ? proxy : undefined, findings);
90
+ // --- 2. OAuth: one real refresh exchange ---
91
+ let accessToken = null;
92
+ let db = null;
93
+ const refreshToken = context.config?.pixiv?.refreshToken;
94
+ if (isPlaceholderToken(refreshToken)) {
95
+ findings.push({
96
+ level: 'skipped',
97
+ code: 'oauth-skipped',
98
+ message: 'no usable refresh token in config; OAuth data plane not tested',
99
+ });
100
+ }
101
+ else {
102
+ try {
103
+ db = new Database_1.Database(context.config.storage?.databasePath ?? './data/pixiv-downloader.db');
104
+ db.migrate();
105
+ const auth = new PixivAuth_1.PixivAuth(context.config.pixiv, networkForAuth, db, context.configPath);
106
+ const started = performance.now();
107
+ accessToken = (0, PixivAuth_1.isAuthReadOnly)()
108
+ ? await auth.getAccessToken() // readonly: cache only; the token endpoint is NOT tested
109
+ : await auth.refreshAccessTokenForClient();
110
+ const latencyMs = Math.round(performance.now() - started);
111
+ const fromCache = (0, PixivAuth_1.isAuthReadOnly)();
112
+ findings.push({
113
+ level: 'ok',
114
+ code: fromCache ? 'oauth-cache-ok' : 'oauth-refresh-ok',
115
+ message: fromCache
116
+ ? `OAuth access token served from cache in ${latencyMs}ms (PIXIV_AUTH_READONLY: the token endpoint was NOT tested)`
117
+ : `OAuth refresh succeeded in ${latencyMs}ms`,
118
+ data: { latencyMs },
119
+ });
120
+ }
121
+ catch (error) {
122
+ if ((0, PixivAuth_1.isAuthReadOnly)() || error instanceof errors_1.AuthenticationError) {
123
+ findings.push({
124
+ level: 'warn',
125
+ code: 'oauth-readonly-blocked',
126
+ message: 'PIXIV_AUTH_READONLY blocks the token endpoint; OAuth data plane NOT tested. ' +
127
+ 'Re-run without the flag (and with no production execution holding the credential).',
128
+ data: { error: error instanceof Error ? error.message : String(error) },
129
+ });
130
+ }
131
+ else {
132
+ findings.push({
133
+ level: 'critical',
134
+ code: 'oauth-failed',
135
+ message: `OAuth refresh failed: ${error instanceof Error ? error.message : String(error)}`,
136
+ });
137
+ }
138
+ }
139
+ }
140
+ // --- 3. App API: one small fixed request ---
141
+ let mediaUrl = null;
142
+ const appApi = await this.probeAppApi(fetchImpl, accessToken, illustId, findings);
143
+ if (appApi?.mediaUrl)
144
+ mediaUrl = appApi.mediaUrl;
145
+ // --- 4. Media CDN: 64 KiB Range request with the app Referer ---
146
+ const media = await this.probeMedia(fetchImpl, mediaUrl, findings);
147
+ if (db)
148
+ db.close();
149
+ const summary = this.summarize(provider, region, findings, appApi, media);
150
+ this.print(context, summary, args.options.json === true);
151
+ const exitCode = findings.some((f) => f.level === 'critical') ? 2 : findings.some((f) => f.level === 'warn') ? 1 : 0;
152
+ return this.withExitCode(this.success(`egress verdict: ${summary.verdict}`, summary), exitCode);
153
+ }
154
+ /** pixiv.net answers 403 to anonymous probes; any HTTP response proves reachability. */
155
+ async probeReachable(fetchImpl, proxy, findings) {
156
+ try {
157
+ const init = { signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) };
158
+ if (proxy) {
159
+ // Same proxy path the download pipeline uses (see HealthCommand.checkNetwork).
160
+ const undici = require('undici');
161
+ const protocol = (proxy.protocol || 'http').toLowerCase();
162
+ const auth = proxy.username && proxy.password
163
+ ? `${encodeURIComponent(proxy.username)}:${encodeURIComponent(proxy.password)}@`
164
+ : '';
165
+ init.dispatcher = new undici.ProxyAgent(`${protocol}://${auth}${proxy.host}:${proxy.port}`);
166
+ }
167
+ const started = performance.now();
168
+ const res = await fetchImpl('https://www.pixiv.net/', init);
169
+ const latencyMs = Math.round(performance.now() - started);
170
+ findings.push({
171
+ level: 'ok',
172
+ code: 'pixiv-net-ok',
173
+ message: `pixiv.net reachable (HTTP ${res.status}) in ${latencyMs}ms`,
174
+ data: { status: res.status, latencyMs },
175
+ });
176
+ }
177
+ catch (error) {
178
+ findings.push({
179
+ level: 'critical',
180
+ code: 'pixiv-net-unreachable',
181
+ message: `pixiv.net unreachable: ${error instanceof Error ? error.message : String(error)}`,
182
+ });
183
+ }
184
+ }
185
+ /**
186
+ * One small App API request. Any HTTP response proves the data plane is reachable;
187
+ * 429 is the starvation signal this probe exists to surface.
188
+ */
189
+ async probeAppApi(fetchImpl, accessToken, illustId, findings) {
190
+ if (!accessToken) {
191
+ findings.push({
192
+ level: 'skipped',
193
+ code: 'appapi-skipped',
194
+ message: 'no access token (OAuth skipped/failed); App API data plane not tested authenticated',
195
+ });
196
+ return null;
197
+ }
198
+ const url = `https://app-api.pixiv.net/v1/illust/detail?illust_id=${encodeURIComponent(illustId)}`;
199
+ try {
200
+ const started = performance.now();
201
+ const res = await fetchImpl(url, {
202
+ headers: {
203
+ authorization: `Bearer ${accessToken}`,
204
+ // The App API rejects requests without an app User-Agent.
205
+ 'user-agent': 'PixivAndroidApp/5.0.234 (Android 11; Pixel 5)',
206
+ 'app-os': 'android',
207
+ 'app-os-version': '11',
208
+ },
209
+ signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
210
+ });
211
+ const ttfbMs = Math.round(performance.now() - started);
212
+ const retryAfter = res.headers.get('retry-after');
213
+ const retryAfterMs = retryAfter !== null ? Number(retryAfter) * 1000 || null : null;
214
+ let mediaUrl = null;
215
+ try {
216
+ const body = (await res.json());
217
+ mediaUrl =
218
+ body.illust?.meta_single_page?.original_image_url ??
219
+ body.illust?.meta_pages?.[0]?.image_urls?.original ??
220
+ null;
221
+ }
222
+ catch {
223
+ // A non-JSON body is recorded via status; the media probe then reports skipped.
224
+ }
225
+ const rateLimited = res.status === 429;
226
+ findings.push({
227
+ level: rateLimited ? 'warn' : 'ok',
228
+ code: rateLimited ? 'appapi-429' : 'appapi-ok',
229
+ message: `App API responded HTTP ${res.status} in ${ttfbMs}ms${retryAfter ? ` (retry-after ${retryAfter}s)` : ''}`,
230
+ data: { status: res.status, ttfbMs, retryAfterMs },
231
+ });
232
+ return { status: res.status, ttfbMs, durationMs: ttfbMs, rateLimited, retryAfterMs, mediaUrl };
233
+ }
234
+ catch (error) {
235
+ findings.push({
236
+ level: 'critical',
237
+ code: 'appapi-failed',
238
+ message: `App API request failed: ${error instanceof Error ? error.message : String(error)}`,
239
+ });
240
+ return null;
241
+ }
242
+ }
243
+ /** 64 KiB Range request — never a full media download. */
244
+ async probeMedia(fetchImpl, mediaUrl, findings) {
245
+ if (!mediaUrl) {
246
+ findings.push({
247
+ level: 'skipped',
248
+ code: 'media-skipped',
249
+ message: 'no media URL from the App API response; media CDN not tested',
250
+ });
251
+ return { status: null, ttfbMs: null, bytes64kDurationMs: null, timedOut: false };
252
+ }
253
+ try {
254
+ const started = performance.now();
255
+ const res = await fetchImpl(mediaUrl, {
256
+ headers: {
257
+ range: 'bytes=0-65535',
258
+ // i.pximg.net requires the app referer or it answers 403.
259
+ referer: 'https://app-api.pixiv.net/',
260
+ },
261
+ signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
262
+ });
263
+ const durationMs = Math.round(performance.now() - started);
264
+ await res.arrayBuffer().catch(() => null);
265
+ const ok = res.status === 200 || res.status === 206;
266
+ findings.push({
267
+ level: ok ? 'ok' : res.status === 429 ? 'warn' : 'critical',
268
+ code: ok ? 'media-ok' : res.status === 429 ? 'media-429' : 'media-blocked',
269
+ message: `media CDN responded HTTP ${res.status} in ${durationMs}ms (64 KiB Range)`,
270
+ data: { status: res.status, bytes64kDurationMs: durationMs },
271
+ });
272
+ return { status: res.status, ttfbMs: durationMs, bytes64kDurationMs: durationMs, timedOut: false };
273
+ }
274
+ catch (error) {
275
+ const timedOut = error instanceof Error && /timeout|abort/i.test(error.message);
276
+ findings.push({
277
+ level: 'critical',
278
+ code: timedOut ? 'media-timeout' : 'media-failed',
279
+ message: `media CDN request failed: ${error instanceof Error ? error.message : String(error)}`,
280
+ });
281
+ return { status: null, ttfbMs: null, bytes64kDurationMs: null, timedOut };
282
+ }
283
+ }
284
+ summarize(provider, region, findings, appApi, media) {
285
+ const oauth = findings.find((f) => f.code.startsWith('oauth-'));
286
+ const incomplete = findings.some((f) => f.level === 'skipped');
287
+ const critical = findings.some((f) => f.level === 'critical');
288
+ const degraded = findings.some((f) => f.level === 'warn');
289
+ const verdict = critical ? 'FAIL' : degraded ? 'DEGRADED' : incomplete ? 'INCOMPLETE' : 'PASS';
290
+ return {
291
+ provider,
292
+ region,
293
+ network: 'datacenter-egress',
294
+ oauth: {
295
+ status: oauth ? oauth.code : 'oauth-skipped',
296
+ latencyMs: oauth?.data?.latencyMs ?? null,
297
+ refreshed: oauth?.code === 'oauth-refresh-ok',
298
+ },
299
+ appApi: {
300
+ status: appApi?.status ?? null,
301
+ ttfbMs: appApi?.ttfbMs ?? null,
302
+ durationMs: appApi?.durationMs ?? null,
303
+ rateLimited: appApi?.rateLimited ?? false,
304
+ retryAfterMs: appApi?.retryAfterMs ?? null,
305
+ },
306
+ media,
307
+ verdict,
308
+ incomplete,
309
+ findings,
310
+ };
311
+ }
312
+ print(context, summary, asJson) {
313
+ if (asJson) {
314
+ context.logger.info(JSON.stringify(summary, null, 2));
315
+ return;
316
+ }
317
+ context.logger.info('━━━ Pixiv egress probe ━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
318
+ for (const f of summary.findings) {
319
+ const mark = f.level === 'ok' ? '✓' : f.level === 'warn' ? '⚠' : f.level === 'critical' ? '✗' : '·';
320
+ context.logger.info(` ${mark} [${f.level.toUpperCase()}] ${f.code}: ${f.message}`);
321
+ }
322
+ context.logger.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
323
+ context.logger.info(` verdict: ${summary.verdict}${summary.incomplete ? ' (incomplete: some probes were skipped)' : ''}`);
324
+ }
325
+ }
326
+ exports.DiagnoseEgressCommand = DiagnoseEgressCommand;
327
+ //# sourceMappingURL=DiagnoseEgressCommand.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DiagnoseEgressCommand.js","sourceRoot":"","sources":["../../src/commands/DiagnoseEgressCommand.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * execute-slot: run ONE canonical occurrence as a disposable batch job.
3
+ *
4
+ * This is the execution plane of the serverless production architecture. The
5
+ * control plane has already decided *which* occurrence is due and hands the runner
6
+ * its canonical identity; the runner never re-derives "what should run today" and
7
+ * never opens a long-lived daemon:
8
+ *
9
+ * control plane → workflow_dispatch(slot_id, schedule_id, occurrence_at, ...)
10
+ * → this command runs exactly that slot's targets once
11
+ * → writes one machine-readable result document
12
+ * → exits with a status-bearing code
13
+ *
14
+ * Deliberately NOT `pixivflow scheduler`: no cron, no slots, no leases, no local
15
+ * ledger requirement. Duplicate history still comes from the local database when
16
+ * one is present, and the control plane owns cross-runner duplicate state.
17
+ */
18
+ import { BaseCommand } from './Command';
19
+ import { CommandCategory } from './metadata';
20
+ import { CommandContext, CommandArgs, CommandResult } from './types';
21
+ export declare class ExecuteSlotCommand extends BaseCommand {
22
+ readonly name = "execute-slot";
23
+ readonly description = "Execute one canonical occurrence once (batch/CI execution plane)";
24
+ readonly aliases: string[];
25
+ readonly metadata: {
26
+ category: CommandCategory;
27
+ requiresAuth: boolean;
28
+ longRunning: boolean;
29
+ };
30
+ validate(args: CommandArgs): {
31
+ valid: boolean;
32
+ errors: string[];
33
+ };
34
+ execute(context: CommandContext, args: CommandArgs): Promise<CommandResult>;
35
+ getUsage(): string;
36
+ }
37
+ //# sourceMappingURL=ExecuteSlotCommand.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExecuteSlotCommand.d.ts","sourceRoot":"","sources":["../../src/commands/ExecuteSlotCommand.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAiCrE,qBAAa,kBAAmB,SAAQ,WAAW;IACjD,QAAQ,CAAC,IAAI,kBAAkB;IAC/B,QAAQ,CAAC,WAAW,sEAAsE;IAC1F,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,CAAa;IACvC,QAAQ,CAAC,QAAQ;;;;MAIf;IAEF,QAAQ,CAAC,IAAI,EAAE,WAAW;;;;IAqBpB,OAAO,CAAC,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,aAAa,CAAC;IA+IjF,QAAQ,IAAI,MAAM;CAqCnB"}
@@ -0,0 +1,267 @@
1
+ "use strict";
2
+ /**
3
+ * execute-slot: run ONE canonical occurrence as a disposable batch job.
4
+ *
5
+ * This is the execution plane of the serverless production architecture. The
6
+ * control plane has already decided *which* occurrence is due and hands the runner
7
+ * its canonical identity; the runner never re-derives "what should run today" and
8
+ * never opens a long-lived daemon:
9
+ *
10
+ * control plane → workflow_dispatch(slot_id, schedule_id, occurrence_at, ...)
11
+ * → this command runs exactly that slot's targets once
12
+ * → writes one machine-readable result document
13
+ * → exits with a status-bearing code
14
+ *
15
+ * Deliberately NOT `pixivflow scheduler`: no cron, no slots, no leases, no local
16
+ * ledger requirement. Duplicate history still comes from the local database when
17
+ * one is present, and the control plane owns cross-runner duplicate state.
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.ExecuteSlotCommand = void 0;
21
+ const node_fs_1 = require("node:fs");
22
+ const Command_1 = require("./Command");
23
+ const metadata_1 = require("./metadata");
24
+ const schedules_1 = require("../scheduler/schedules");
25
+ const Scheduler_1 = require("../scheduler/Scheduler");
26
+ const scheduler_runtime_1 = require("./scheduler-runtime");
27
+ const runDiagnostics_1 = require("../batch/runDiagnostics");
28
+ const executionResult_1 = require("../batch/executionResult");
29
+ function stringOption(args, ...names) {
30
+ for (const name of names) {
31
+ const value = args.options[name];
32
+ if (typeof value === 'string' && value.trim().length > 0)
33
+ return value.trim();
34
+ }
35
+ return undefined;
36
+ }
37
+ class ExecuteSlotCommand extends Command_1.BaseCommand {
38
+ name = 'execute-slot';
39
+ description = 'Execute one canonical occurrence once (batch/CI execution plane)';
40
+ aliases = ['batch'];
41
+ metadata = {
42
+ category: metadata_1.CommandCategory.DOWNLOAD,
43
+ requiresAuth: true,
44
+ longRunning: false,
45
+ };
46
+ validate(args) {
47
+ const errors = [];
48
+ const scheduleId = stringOption(args, 'schedule-id', 'scheduleId');
49
+ if (!scheduleId)
50
+ errors.push('--schedule-id is required (which schedule to execute)');
51
+ const slotId = stringOption(args, 'slot-id', 'slotId');
52
+ if (!slotId)
53
+ errors.push('--slot-id is required (canonical occurrence identity)');
54
+ const mode = stringOption(args, 'mode') ?? 'live';
55
+ if (!scheduler_runtime_1.EXECUTION_MODES.includes(mode)) {
56
+ errors.push(`--mode must be one of ${scheduler_runtime_1.EXECUTION_MODES.join(', ')}`);
57
+ }
58
+ const attempt = stringOption(args, 'attempt');
59
+ if (attempt && (!Number.isInteger(Number(attempt)) || Number(attempt) < 1)) {
60
+ errors.push('--attempt must be a positive integer');
61
+ }
62
+ const timeout = stringOption(args, 'timeout-ms', 'timeoutMs');
63
+ if (timeout && (!Number.isFinite(Number(timeout)) || Number(timeout) <= 0)) {
64
+ errors.push('--timeout-ms must be a positive number');
65
+ }
66
+ return { valid: errors.length === 0, errors };
67
+ }
68
+ async execute(context, args) {
69
+ const scheduleId = stringOption(args, 'schedule-id', 'scheduleId');
70
+ const slotId = stringOption(args, 'slot-id', 'slotId');
71
+ const mode = (stringOption(args, 'mode') ?? 'live');
72
+ const attempt = Number(stringOption(args, 'attempt') ?? '1');
73
+ const botId = stringOption(args, 'bot-id', 'botId');
74
+ const occurrenceAtRaw = stringOption(args, 'occurrence-at', 'occurrenceAt');
75
+ const occurrenceAt = occurrenceAtRaw ? Number(occurrenceAtRaw) : undefined;
76
+ const resultFile = stringOption(args, 'result-file', 'resultFile');
77
+ const excludeFile = stringOption(args, 'exclude-work-ids', 'excludeWorkIds');
78
+ const timeoutMs = Number(stringOption(args, 'timeout-ms', 'timeoutMs') ?? Scheduler_1.DEFAULT_SCHEDULE_TIMEOUT_MS);
79
+ const targetFilter = (stringOption(args, 'targets') ?? '')
80
+ .split(',')
81
+ .map((value) => value.trim())
82
+ .filter(Boolean);
83
+ const startedAt = new Date();
84
+ const startedMs = Date.now();
85
+ // Counts must not leak between runs (the scheduler calls this in-process too).
86
+ (0, runDiagnostics_1.resetRunDiagnostics)();
87
+ const outcomes = [];
88
+ let runtime;
89
+ let expectedTargetIds = [];
90
+ let outbox;
91
+ let fatal;
92
+ try {
93
+ runtime = await (0, scheduler_runtime_1.createSchedulerRuntime)(args.options.config);
94
+ const schedule = (0, schedules_1.resolveSchedules)(runtime.config).find((plan) => plan.id === scheduleId);
95
+ if (!schedule) {
96
+ throw new Error(`unknown schedule: ${scheduleId}`);
97
+ }
98
+ if (schedule.enabled === false) {
99
+ throw new Error(`schedule is disabled: ${scheduleId}`);
100
+ }
101
+ // The canonical target list comes from the schedule (and may be narrowed by
102
+ // the dispatcher). It is the expectation set: anything missing from the
103
+ // outcomes is reported as `missing`, never silently dropped.
104
+ const scheduleTargets = schedule.targetIds ?? [];
105
+ expectedTargetIds = targetFilter.length > 0
106
+ ? (scheduleTargets.length > 0
107
+ ? scheduleTargets.filter((id) => targetFilter.includes(id))
108
+ : targetFilter)
109
+ : scheduleTargets;
110
+ if (expectedTargetIds.length === 0) {
111
+ throw new Error(`no targets resolved for schedule ${scheduleId}`);
112
+ }
113
+ context.logger.info('Executing canonical occurrence', {
114
+ slotId,
115
+ scheduleId,
116
+ attempt,
117
+ mode,
118
+ targets: expectedTargetIds,
119
+ });
120
+ // Durable duplicate history: works this bot already handled, fetched by the
121
+ // caller (the batch runner asks the control plane). Without it a disposable
122
+ // runner re-selects work that was delivered weeks ago.
123
+ const excludedWorkIds = excludeFile ? readExcludedWorkIds(excludeFile, context) : undefined;
124
+ await (0, scheduler_runtime_1.runWithTimeout)(runtime.runJob(runtime.config, schedule, {
125
+ // Ad-hoc by design: the control plane owns the occurrence ledger now, so
126
+ // this run must not open or resume a local scheduled Slot.
127
+ adhoc: true,
128
+ triggerSource: 'http',
129
+ deliveryMode: mode,
130
+ onTargetOutcome: (targetId, outcome) => {
131
+ outcomes.push((0, executionResult_1.outcomeToTargetResult)(targetId, outcome));
132
+ // The typed Pixiv errors are recorded where they are raised; this catches
133
+ // the paths that only keep a message.
134
+ if (outcome.kind === 'failed' && outcome.error)
135
+ (0, runDiagnostics_1.recordRunError)(outcome.error);
136
+ },
137
+ ...(excludedWorkIds ? { excludedWorkIds } : {}),
138
+ }), timeoutMs, () => runtime.cancelActive(`batch timeout after ${timeoutMs}ms`), `slot ${slotId}`);
139
+ // Bounded drain: deliveries created by this run get one chance to confirm
140
+ // before we report. Rows that cannot finish now stay durable and are
141
+ // resolved by the control plane's reconciliation, never by a blind resend.
142
+ outbox = await runtime.drainOutbox();
143
+ }
144
+ catch (error) {
145
+ fatal = error instanceof Error ? error.message : String(error);
146
+ context.logger.error('Batch execution failed', { slotId, error: fatal });
147
+ }
148
+ finally {
149
+ try {
150
+ runtime?.close();
151
+ }
152
+ catch {
153
+ // Closing must never mask the real failure above.
154
+ }
155
+ }
156
+ const summary = (0, executionResult_1.summarizeExecution)(outcomes, expectedTargetIds);
157
+ const finishedAt = new Date();
158
+ if (fatal)
159
+ (0, runDiagnostics_1.recordRunError)(fatal);
160
+ const diagnostics = (0, runDiagnostics_1.runDiagnostics)();
161
+ const result = {
162
+ slotId,
163
+ scheduleId,
164
+ ...(botId ? { botId } : {}),
165
+ attempt,
166
+ mode,
167
+ ...(occurrenceAt !== undefined && Number.isFinite(occurrenceAt) ? { occurrenceAt } : {}),
168
+ status: fatal ? 'failed' : summary.status,
169
+ exitCode: fatal ? executionResult_1.EXIT_ERROR : summary.exitCode,
170
+ startedAt: startedAt.toISOString(),
171
+ finishedAt: finishedAt.toISOString(),
172
+ durationMs: finishedAt.getTime() - startedMs,
173
+ targets: summary.targets,
174
+ ...(outbox ? { outbox } : {}),
175
+ ...(fatal ? { error: fatal } : {}),
176
+ // What went wrong, in a form the control plane can act on: a rate-limited
177
+ // account needs to wait for the server, a broken provider must not be retried
178
+ // on the same schedule, and the exit code cannot tell them apart.
179
+ ...(diagnostics.dominantCategory ? { errorClass: diagnostics.dominantCategory } : {}),
180
+ ...(diagnostics.maxRetryAfterMs !== null ? { retryAfterMs: diagnostics.maxRetryAfterMs } : {}),
181
+ ...(diagnostics.rateLimitHits > 0 ? { rateLimitHits: diagnostics.rateLimitHits } : {}),
182
+ };
183
+ const serialized = JSON.stringify(result, null, 2);
184
+ if (resultFile) {
185
+ (0, node_fs_1.writeFileSync)(resultFile, serialized, 'utf8');
186
+ context.logger.info('Wrote batch result', { resultFile });
187
+ }
188
+ else {
189
+ // stdout is the machine interface when no file is requested; keep it clean
190
+ // (all human logging goes to stderr/the logger).
191
+ process.stdout.write(`${serialized}\n`);
192
+ }
193
+ if (fatal) {
194
+ return this.withExitCode(this.failure(fatal, result), executionResult_1.EXIT_ERROR);
195
+ }
196
+ return this.withExitCode(this.success(`slot ${slotId}: ${result.status}`, result), result.exitCode);
197
+ }
198
+ getUsage() {
199
+ return `execute-slot
200
+
201
+ Execute ONE canonical occurrence once and exit. This is the batch/CI execution
202
+ plane: the control plane decides which occurrence is due, this command runs it.
203
+
204
+ Required:
205
+ --schedule-id <id> Schedule to execute (e.g. bot1-daily)
206
+ --slot-id <id> Canonical occurrence (e.g. bot1-daily@2026-09-11T1800)
207
+
208
+ Optional:
209
+ --occurrence-at <ms> Canonical occurrence instant (epoch ms), for audit
210
+ --attempt <n> Attempt number, echoed into the result (default 1)
211
+ --bot-id <id> Bot identity, echoed into the result
212
+ --targets <a,b> Restrict to these target ids (default: schedule targets)
213
+ --mode live|shadow|dry-run Downstream publishing mode (default live)
214
+ --result-file <path> Write the result JSON here
215
+ --timeout-ms <n> Hard bound for the run (default: schedule timeout)
216
+ --exclude-work-ids <f> JSON file of works this bot already handled:
217
+ {"illustration":["id",...],"novel":[...]} (or the control
218
+ plane's {"works":{...}} response). Durable duplicate
219
+ history a disposable runner cannot know by itself.
220
+
221
+ Machine interface: pass --result-file and read that file. Stdout also carries the
222
+ process logger's lines, so it is NOT a strict JSON channel.
223
+
224
+ Exit codes (the exit code IS the execution status):
225
+ 0 success every target was delivered/stored/deduplicated
226
+ 2 partial some targets delivered, some did not (incl. all-no-candidate)
227
+ 3 failed nothing was delivered
228
+ 4 uncertain a send was not confirmed; never auto-retried
229
+ 1 error the process itself could not run the slot
230
+
231
+ Examples:
232
+ pixivflow execute-slot --schedule-id bot1-daily --slot-id bot1-daily@2026-09-11T1800 \\
233
+ --mode shadow --result-file /tmp/slot-result.json`;
234
+ }
235
+ }
236
+ exports.ExecuteSlotCommand = ExecuteSlotCommand;
237
+ /**
238
+ * Read durable duplicate history. Accepts either the bare map or the control
239
+ * plane's envelope, and tolerates a missing/garbled file by logging loudly rather
240
+ * than silently running without history — that would re-post old work.
241
+ */
242
+ function readExcludedWorkIds(path, context) {
243
+ try {
244
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)(path, 'utf8'));
245
+ const source = (parsed.works && typeof parsed.works === 'object' ? parsed.works : parsed);
246
+ const toIds = (value) => Array.isArray(value) ? value.filter((id) => typeof id === 'string') : undefined;
247
+ const illustration = toIds(source.illustration);
248
+ const novel = toIds(source.novel);
249
+ if (!illustration && !novel) {
250
+ context.logger.warn('Duplicate-history file had no usable ids', { path });
251
+ return undefined;
252
+ }
253
+ context.logger.info('Loaded durable duplicate history', {
254
+ illustration: illustration?.length ?? 0,
255
+ novel: novel?.length ?? 0,
256
+ });
257
+ return { ...(illustration ? { illustration } : {}), ...(novel ? { novel } : {}) };
258
+ }
259
+ catch (error) {
260
+ context.logger.warn('Could not read the duplicate-history file; continuing without it', {
261
+ path,
262
+ error: error instanceof Error ? error.message : String(error),
263
+ });
264
+ return undefined;
265
+ }
266
+ }
267
+ //# sourceMappingURL=ExecuteSlotCommand.js.map