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.
- package/{README_EN.md → README.en.md} +8 -6
- package/README.md +8 -6
- package/config/examples/README.md +218 -0
- package/config/examples/multi-tag-or-limit.zh.json +49 -0
- package/config/examples/specific-download.example.json +173 -0
- package/config/examples/standalone.config.example.json +167 -0
- package/config/examples/standalone.config.novel-chinese.json +124 -0
- package/config/examples/standalone.config.ranking.json +135 -0
- package/config/examples/standalone.config.simple.json +52 -0
- package/config/examples/yesterday-popular-novel.zh.json +48 -0
- package/config/examples/yesterday-ranking-illustration.json +43 -0
- package/dist/auth/PixivAuth.d.ts +12 -0
- package/dist/auth/PixivAuth.d.ts.map +1 -1
- package/dist/auth/PixivAuth.js +26 -0
- package/dist/auth/PixivAuth.js.map +1 -1
- package/dist/batch/executionResult.d.ts +85 -0
- package/dist/batch/executionResult.d.ts.map +1 -0
- package/dist/batch/executionResult.js +102 -0
- package/dist/batch/executionResult.js.map +1 -0
- package/dist/batch/runDiagnostics.d.ts +48 -0
- package/dist/batch/runDiagnostics.d.ts.map +1 -0
- package/dist/batch/runDiagnostics.js +103 -0
- package/dist/batch/runDiagnostics.js.map +1 -0
- package/dist/commands/Command.d.ts +5 -0
- package/dist/commands/Command.d.ts.map +1 -1
- package/dist/commands/Command.js +7 -0
- package/dist/commands/Command.js.map +1 -1
- package/dist/commands/DiagnoseEgressCommand.d.ts +98 -0
- package/dist/commands/DiagnoseEgressCommand.d.ts.map +1 -0
- package/dist/commands/DiagnoseEgressCommand.js +327 -0
- package/dist/commands/DiagnoseEgressCommand.js.map +1 -0
- package/dist/commands/ExecuteSlotCommand.d.ts +37 -0
- package/dist/commands/ExecuteSlotCommand.d.ts.map +1 -0
- package/dist/commands/ExecuteSlotCommand.js +267 -0
- package/dist/commands/ExecuteSlotCommand.js.map +1 -0
- package/dist/commands/MaintainCommand.d.ts.map +1 -1
- package/dist/commands/MaintainCommand.js +10 -15
- package/dist/commands/MaintainCommand.js.map +1 -1
- package/dist/commands/index.d.ts +3 -1
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +7 -1
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/scheduler-runtime.d.ts +37 -1
- package/dist/commands/scheduler-runtime.d.ts.map +1 -1
- package/dist/commands/scheduler-runtime.js +52 -19
- package/dist/commands/scheduler-runtime.js.map +1 -1
- package/dist/commands/types.d.ts +6 -0
- package/dist/commands/types.d.ts.map +1 -1
- package/dist/config/types.d.ts +33 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/validation.d.ts +14 -1
- package/dist/config/validation.d.ts.map +1 -1
- package/dist/config/validation.js +109 -33
- package/dist/config/validation.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/delivery/DeliveryDispatcher.d.ts.map +1 -1
- package/dist/delivery/DeliveryDispatcher.js +7 -0
- package/dist/delivery/DeliveryDispatcher.js.map +1 -1
- package/dist/delivery/HttpMultipartDelivery.d.ts +12 -0
- package/dist/delivery/HttpMultipartDelivery.d.ts.map +1 -1
- package/dist/delivery/HttpMultipartDelivery.js +89 -70
- package/dist/delivery/HttpMultipartDelivery.js.map +1 -1
- package/dist/delivery/LegacyOutboxMigration.js +1 -1
- package/dist/delivery/LegacyOutboxMigration.js.map +1 -1
- package/dist/delivery/TelegramReviewDelivery.d.ts +49 -0
- package/dist/delivery/TelegramReviewDelivery.d.ts.map +1 -0
- package/dist/delivery/TelegramReviewDelivery.js +307 -0
- package/dist/delivery/TelegramReviewDelivery.js.map +1 -0
- package/dist/download/DownloadManager.d.ts +10 -0
- package/dist/download/DownloadManager.d.ts.map +1 -1
- package/dist/download/DownloadManager.js +21 -0
- package/dist/download/DownloadManager.js.map +1 -1
- package/dist/download/plan/DownloadPlanner.d.ts +10 -0
- package/dist/download/plan/DownloadPlanner.d.ts.map +1 -1
- package/dist/download/plan/DownloadPlanner.js +19 -0
- package/dist/download/plan/DownloadPlanner.js.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/notification/NotificationPolicy.d.ts.map +1 -1
- package/dist/notification/NotificationPolicy.js +4 -1
- package/dist/notification/NotificationPolicy.js.map +1 -1
- package/dist/package.json +1 -1
- package/dist/pixiv-token-getter-adapter.d.ts +27 -7
- package/dist/pixiv-token-getter-adapter.d.ts.map +1 -1
- package/dist/pixiv-token-getter-adapter.js +44 -27
- package/dist/pixiv-token-getter-adapter.js.map +1 -1
- package/dist/puppeteer-login-adapter/auth-code.d.ts +1 -1
- package/dist/puppeteer-login-adapter/auth-code.d.ts.map +1 -1
- package/dist/puppeteer-login-adapter/browser-launch.d.ts +21 -0
- package/dist/puppeteer-login-adapter/browser-launch.d.ts.map +1 -0
- package/dist/puppeteer-login-adapter/browser-launch.js +46 -0
- package/dist/puppeteer-login-adapter/browser-launch.js.map +1 -0
- package/dist/puppeteer-login-adapter/environment.js +2 -2
- package/dist/puppeteer-login-adapter/environment.js.map +1 -1
- package/dist/puppeteer-login-adapter/login-headless.d.ts.map +1 -1
- package/dist/puppeteer-login-adapter/login-headless.js +2 -5
- package/dist/puppeteer-login-adapter/login-headless.js.map +1 -1
- package/dist/puppeteer-login-adapter/login-interactive.d.ts.map +1 -1
- package/dist/puppeteer-login-adapter/login-interactive.js +2 -5
- package/dist/puppeteer-login-adapter/login-interactive.js.map +1 -1
- package/dist/storage/Database.d.ts +2 -0
- package/dist/storage/Database.d.ts.map +1 -1
- package/dist/storage/Database.js +7 -6
- package/dist/storage/Database.js.map +1 -1
- package/dist/storage/DatabaseMigration.d.ts +2 -2
- package/dist/storage/DatabaseMigration.d.ts.map +1 -1
- package/dist/storage/DatabaseMigration.js.map +1 -1
- package/dist/storage/drivers/NodeSqliteDriver.d.ts +14 -0
- package/dist/storage/drivers/NodeSqliteDriver.d.ts.map +1 -0
- package/dist/storage/drivers/NodeSqliteDriver.js +123 -0
- package/dist/storage/drivers/NodeSqliteDriver.js.map +1 -0
- package/dist/storage/drivers/SqliteDriver.d.ts +55 -0
- package/dist/storage/drivers/SqliteDriver.d.ts.map +1 -0
- package/dist/storage/drivers/SqliteDriver.js +21 -0
- package/dist/storage/drivers/SqliteDriver.js.map +1 -0
- package/dist/storage/repositories/BaseRepository.d.ts +4 -4
- package/dist/storage/repositories/BaseRepository.d.ts.map +1 -1
- package/dist/storage/repositories/BaseRepository.js.map +1 -1
- package/dist/storage/repositories/DownloadRepository.d.ts +2 -1
- package/dist/storage/repositories/DownloadRepository.d.ts.map +1 -1
- package/dist/storage/repositories/DownloadRepository.js.map +1 -1
- package/dist/storage/repositories/RateLimitStateRepository.d.ts +1 -1
- package/dist/storage/repositories/RateLimitStateRepository.js +1 -1
- package/dist/utils/concurrency.d.ts.map +1 -1
- package/dist/utils/concurrency.js +9 -2
- package/dist/utils/concurrency.js.map +1 -1
- package/dist/utils/config-validator-unified.d.ts.map +1 -1
- package/dist/utils/config-validator-unified.js +15 -1
- package/dist/utils/config-validator-unified.js.map +1 -1
- package/dist/utils/token-maintenance.d.ts +0 -4
- package/dist/utils/token-maintenance.d.ts.map +1 -1
- package/dist/utils/token-maintenance.js +5 -25
- package/dist/utils/token-maintenance.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/webui/package.json +1 -1
- package/node_modules/@redtidev/pixiv-client/README.en.md +105 -0
- package/node_modules/@redtidev/pixiv-client/README.md +67 -73
- package/node_modules/@redtidev/pixiv-client/dist/rate-limit/RateLimitGate.d.ts +1 -1
- package/node_modules/@redtidev/pixiv-client/dist/rate-limit/RateLimitGate.js +1 -1
- package/node_modules/@redtidev/pixiv-client/src/rate-limit/RateLimitGate.ts +1 -1
- 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
|