pixivflow 2.20.3 → 2.20.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/scheduler-runtime.js +24 -17
- package/dist/delivery/OutboxWorker.d.ts +3 -0
- package/dist/delivery/OutboxWorker.js +4 -0
- package/dist/notification/NotificationPolicy.d.ts +2 -0
- package/dist/notification/NotificationPolicy.js +31 -5
- package/dist/package.json +1 -1
- package/dist/storage/repositories/SlotRepository.d.ts +2 -0
- package/dist/storage/repositories/SlotRepository.js +12 -0
- package/dist/version.js +1 -1
- package/dist/webui/package.json +1 -1
- package/package.json +1 -1
|
@@ -252,6 +252,7 @@ async function createSchedulerRuntime(configPathArg) {
|
|
|
252
252
|
const deliveryDispatcher = new DeliveryDispatcher_1.DeliveryDispatcher(config.delivery, buildProxyUrl(config.network));
|
|
253
253
|
const notificationPolicy = new NotificationPolicy_1.NotificationPolicy(database, config);
|
|
254
254
|
const outboxWorker = new OutboxWorker_1.OutboxWorker(database, deliveryDispatcher, {
|
|
255
|
+
beforeDrain: () => notificationPolicy.reconcileScheduleSummaries(),
|
|
255
256
|
retryBaseMs: config.delivery?.outboxRetryBaseMs,
|
|
256
257
|
retryMaxMs: config.delivery?.outboxRetryMaxMs,
|
|
257
258
|
// A confirmed ACK settles the owning Slot cell (submitted / duplicate /
|
|
@@ -462,12 +463,13 @@ async function createSchedulerRuntime(configPathArg) {
|
|
|
462
463
|
// as a degraded (partial) terminal result.
|
|
463
464
|
const maxFallbackStages = Math.max(0, Math.min(10, Number(runtimeConfig.download?.maxFallbackStages ?? 3)));
|
|
464
465
|
const boostScanLimit = fallbackScanLimit;
|
|
465
|
-
const boostedTargets = (list
|
|
466
|
-
?
|
|
467
|
-
|
|
466
|
+
const boostedTargets = (list) => list.map((t) => {
|
|
467
|
+
const stage = scheduleSlot && t.id ? coordinator.cellFallbackStage(scheduleSlot.slotId, t.id) : 0;
|
|
468
|
+
return stage === 0 ? t : {
|
|
468
469
|
...t,
|
|
469
470
|
candidateScanLimit: boostScanLimit(t.candidateScanLimit ?? runtimeConfig.download?.candidateScanLimit, stage),
|
|
470
|
-
}
|
|
471
|
+
};
|
|
472
|
+
});
|
|
471
473
|
const buildManager = (list) => {
|
|
472
474
|
const scoped = {
|
|
473
475
|
...runtimeConfig,
|
|
@@ -534,7 +536,7 @@ async function createSchedulerRuntime(configPathArg) {
|
|
|
534
536
|
notificationPolicy.noteRefetchOutcome(scheduleSlot, schedule, target, scheduleSlot.manualRequestId, outcome);
|
|
535
537
|
}
|
|
536
538
|
};
|
|
537
|
-
let downloadManager = buildManager(boostedTargets(runTargets
|
|
539
|
+
let downloadManager = buildManager(boostedTargets(runTargets));
|
|
538
540
|
activeDownloadManager = downloadManager;
|
|
539
541
|
await downloadManager.initialise();
|
|
540
542
|
// Apply initial delay if configured
|
|
@@ -573,7 +575,7 @@ async function createSchedulerRuntime(configPathArg) {
|
|
|
573
575
|
p.cell.fallback_stage < maxFallbackStages);
|
|
574
576
|
if (pendingFallback.length === 0)
|
|
575
577
|
break;
|
|
576
|
-
downloadManager = buildManager(boostedTargets(pendingFallback.map((p) => p.target)
|
|
578
|
+
downloadManager = buildManager(boostedTargets(pendingFallback.map((p) => p.target)));
|
|
577
579
|
activeDownloadManager = downloadManager;
|
|
578
580
|
await downloadManager.initialise();
|
|
579
581
|
}
|
|
@@ -619,17 +621,22 @@ async function createSchedulerRuntime(configPathArg) {
|
|
|
619
621
|
for (const target of targets)
|
|
620
622
|
if (target.id)
|
|
621
623
|
notificationPolicy.noteTerminalRefetchCell(slotCtx.slotId, target.id);
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
624
|
+
// Terminal summaries are for SCHEDULED occurrences (P0-B). Remote manual
|
|
625
|
+
// replacements already report through their own verdict channel
|
|
626
|
+
// (refetchOutcomeUrl); a schedule summary would double-notify the group.
|
|
627
|
+
if (!slotCtx.manualRequestId) {
|
|
628
|
+
notificationPolicy.sendSlotSummary(slotCtx, schedule, summary.cells.map((c) => {
|
|
629
|
+
const t = targets.find((x) => x.id === c.targetId);
|
|
630
|
+
return {
|
|
631
|
+
targetId: c.targetId,
|
|
632
|
+
label: c.targetId,
|
|
633
|
+
workType: t?.type ?? 'unknown',
|
|
634
|
+
status: c.status,
|
|
635
|
+
workId: c.workId,
|
|
636
|
+
error: c.error ?? null,
|
|
637
|
+
};
|
|
638
|
+
}));
|
|
639
|
+
}
|
|
633
640
|
releaseLease();
|
|
634
641
|
}
|
|
635
642
|
if (!slotCtx && allTargetsFailed)
|
|
@@ -3,6 +3,8 @@ import { OutboxRow } from '../storage/repositories/OutboxRepository';
|
|
|
3
3
|
import { DeliveryDispatcher } from './DeliveryDispatcher';
|
|
4
4
|
import { DeliveryAck } from './DeliveryAck';
|
|
5
5
|
export interface OutboxWorkerOptions {
|
|
6
|
+
/** Reconcile durable notification intents before consuming due rows. */
|
|
7
|
+
beforeDrain?: () => void;
|
|
6
8
|
/** How often to scan for due rows. */
|
|
7
9
|
pollIntervalMs?: number;
|
|
8
10
|
/** Row lease duration while one attempt is in flight. */
|
|
@@ -45,6 +47,7 @@ export declare function backoffDelayMs(attempt: number, base: number, max: numbe
|
|
|
45
47
|
export declare class OutboxWorker {
|
|
46
48
|
private readonly database;
|
|
47
49
|
private readonly dispatcher;
|
|
50
|
+
private readonly options;
|
|
48
51
|
private timer;
|
|
49
52
|
private running;
|
|
50
53
|
private stopped;
|
|
@@ -23,6 +23,7 @@ function backoffDelayMs(attempt, base, max) {
|
|
|
23
23
|
class OutboxWorker {
|
|
24
24
|
database;
|
|
25
25
|
dispatcher;
|
|
26
|
+
options;
|
|
26
27
|
timer = null;
|
|
27
28
|
running = false;
|
|
28
29
|
stopped = false;
|
|
@@ -37,6 +38,7 @@ class OutboxWorker {
|
|
|
37
38
|
constructor(database, dispatcher, options = {}) {
|
|
38
39
|
this.database = database;
|
|
39
40
|
this.dispatcher = dispatcher;
|
|
41
|
+
this.options = options;
|
|
40
42
|
this.pollIntervalMs = options.pollIntervalMs ?? 10_000;
|
|
41
43
|
this.leaseMs = options.leaseMs ?? 120_000;
|
|
42
44
|
this.batchSize = options.batchSize ?? 4;
|
|
@@ -73,6 +75,7 @@ class OutboxWorker {
|
|
|
73
75
|
let retried = 0;
|
|
74
76
|
let dead = 0;
|
|
75
77
|
for (let round = 0; round < maxRounds; round++) {
|
|
78
|
+
this.options.beforeDrain?.();
|
|
76
79
|
const rows = this.database.outbox.claimDue(this.owner, this.leaseMs, this.batchSize);
|
|
77
80
|
if (rows.length === 0)
|
|
78
81
|
break;
|
|
@@ -104,6 +107,7 @@ class OutboxWorker {
|
|
|
104
107
|
return;
|
|
105
108
|
this.running = true;
|
|
106
109
|
try {
|
|
110
|
+
this.options.beforeDrain?.();
|
|
107
111
|
const rows = this.database.outbox.claimDue(this.owner, this.leaseMs, this.batchSize);
|
|
108
112
|
for (const row of rows) {
|
|
109
113
|
if (this.stopped) {
|
|
@@ -51,6 +51,8 @@ export declare class NotificationPolicy {
|
|
|
51
51
|
* use `refetchOutcomeUrl`; generic notifications use `notificationUrl`.
|
|
52
52
|
*/
|
|
53
53
|
private targetsWithUrl;
|
|
54
|
+
/** The durable cells remain the notification intent across crashes and late ACKs. */
|
|
55
|
+
reconcileScheduleSummaries(): void;
|
|
54
56
|
private send;
|
|
55
57
|
/**
|
|
56
58
|
* Report the terminal verdict of a REMOTE MANUAL replacement ("重抓") back to
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.NotificationPolicy = void 0;
|
|
4
4
|
const DeliveryService_1 = require("../delivery/DeliveryService");
|
|
5
|
+
const SlotCoordinator_1 = require("../scheduler/SlotCoordinator");
|
|
5
6
|
const logger_1 = require("../logger");
|
|
6
7
|
/**
|
|
7
8
|
* Central policy for all operational notifications. Handlers/scheduler do not
|
|
@@ -76,8 +77,11 @@ class NotificationPolicy {
|
|
|
76
77
|
}
|
|
77
78
|
/** One consolidated summary per slot, delivered to every notifying target's endpoint. */
|
|
78
79
|
sendSlotSummary(slot, schedule, rows) {
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
if (slot.manualRequestId || rows.length === 0 || rows.some((r) => !['submitted', 'no_candidate', 'duplicate', 'failed'].includes(r.status)))
|
|
81
|
+
return;
|
|
82
|
+
const memberIds = new Set(rows.map((r) => r.targetId));
|
|
83
|
+
const targets = this.targetsWithUrl('scheduleOutcomeUrl', memberIds);
|
|
84
|
+
if (targets.size === 0)
|
|
81
85
|
return;
|
|
82
86
|
const icon = (s) => s === 'submitted' ? '✅' : s === 'no_candidate' ? '⚠️' : s === 'duplicate' ? '♱' : s === 'delivery_pending' ? '🕓' : '❌';
|
|
83
87
|
const lines = rows.map((r) => `${icon(r.status)} ${r.label}(${r.workType === 'novel' ? '小说' : '插画'})` +
|
|
@@ -93,8 +97,8 @@ class NotificationPolicy {
|
|
|
93
97
|
].join('\n');
|
|
94
98
|
const outcomeStatus = submitted === rows.length ? 'success' : submitted > 0 ? 'partial' : 'failed';
|
|
95
99
|
const service = new DeliveryService_1.DeliveryService(this.database);
|
|
96
|
-
for (const name of targets) {
|
|
97
|
-
service.enqueueNotification(name, text, NotificationPolicy.keys.summary(slot.slotId), undefined, {
|
|
100
|
+
for (const [index, name] of [...targets].entries()) {
|
|
101
|
+
service.enqueueNotification(name, text, NotificationPolicy.keys.summary(slot.slotId) + (index === 0 ? '' : `:${name}`), undefined, {
|
|
98
102
|
scheduleId: schedule.id,
|
|
99
103
|
slotId: slot.slotId,
|
|
100
104
|
status: outcomeStatus,
|
|
@@ -112,9 +116,11 @@ class NotificationPolicy {
|
|
|
112
116
|
* Schedule summaries require `scheduleOutcomeUrl`; manual refetch outcomes
|
|
113
117
|
* use `refetchOutcomeUrl`; generic notifications use `notificationUrl`.
|
|
114
118
|
*/
|
|
115
|
-
targetsWithUrl(urlKey) {
|
|
119
|
+
targetsWithUrl(urlKey, memberIds) {
|
|
116
120
|
const result = new Set();
|
|
117
121
|
for (const target of this.config.targets ?? []) {
|
|
122
|
+
if (memberIds && !memberIds.has(target.id ?? ''))
|
|
123
|
+
continue;
|
|
118
124
|
const deliveryTarget = target.delivery?.target;
|
|
119
125
|
if (!deliveryTarget)
|
|
120
126
|
continue;
|
|
@@ -125,6 +131,26 @@ class NotificationPolicy {
|
|
|
125
131
|
}
|
|
126
132
|
return result;
|
|
127
133
|
}
|
|
134
|
+
/** The durable cells remain the notification intent across crashes and late ACKs. */
|
|
135
|
+
reconcileScheduleSummaries() {
|
|
136
|
+
for (const record of this.database.slots.getUnreportedTerminalSchedules()) {
|
|
137
|
+
const slot = {
|
|
138
|
+
slotId: record.id, scheduleId: record.scheduleId,
|
|
139
|
+
occurrenceAt: record.occurrenceAt ?? 0, occurrenceDate: record.occurrenceDate,
|
|
140
|
+
occurrenceLabel: record.occurrenceLabel, timezone: record.timezone,
|
|
141
|
+
triggerSource: 'http', slotName: record.slotName, slotDate: record.slotDate,
|
|
142
|
+
};
|
|
143
|
+
const schedule = this.config.schedules?.find((item) => item.id === record.scheduleId)
|
|
144
|
+
?? { id: record.scheduleId };
|
|
145
|
+
const targets = this.config.targets.filter((target) => record.targetIds.includes(target.id ?? ''));
|
|
146
|
+
const summary = new SlotCoordinator_1.SlotCoordinator(this.database).finish(slot, schedule, targets);
|
|
147
|
+
this.sendSlotSummary(slot, schedule, summary.cells.map((cell) => ({
|
|
148
|
+
...cell, label: cell.targetId,
|
|
149
|
+
workType: targets.find((target) => target.id === cell.targetId)?.type ?? 'unknown',
|
|
150
|
+
error: cell.error ?? null,
|
|
151
|
+
})));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
128
154
|
send(targetName, key, text) {
|
|
129
155
|
try {
|
|
130
156
|
new DeliveryService_1.DeliveryService(this.database).enqueueNotification(targetName, text, key);
|
package/dist/package.json
CHANGED
|
@@ -89,6 +89,8 @@ export declare class SlotRepository extends BaseRepository {
|
|
|
89
89
|
/** Target ids materialized for a slot (stable membership; falls back to []). */
|
|
90
90
|
getSlotTargetIds(id: string): string[];
|
|
91
91
|
getRecentSlots(limit?: number): SlotRecord[];
|
|
92
|
+
/** Completed cells whose summary enqueue or aggregate rollup was interrupted. */
|
|
93
|
+
getUnreportedTerminalSchedules(): SlotRecord[];
|
|
92
94
|
markSlotStatus(id: string, status: SlotStatus, error?: string): void;
|
|
93
95
|
/** All cells for a slot (one per target). */
|
|
94
96
|
getCells(slotId: string): SlotItemRecord[];
|
|
@@ -71,6 +71,18 @@ class SlotRepository extends BaseRepository_1.BaseRepository {
|
|
|
71
71
|
.all(limit);
|
|
72
72
|
return rows.map((r) => this.toSlot(r));
|
|
73
73
|
}
|
|
74
|
+
/** Completed cells whose summary enqueue or aggregate rollup was interrupted. */
|
|
75
|
+
getUnreportedTerminalSchedules() {
|
|
76
|
+
const rows = this.db.prepare(`SELECT s.* FROM schedule_slots s
|
|
77
|
+
WHERE s.manual_request_id IS NULL AND s.status != 'expired'
|
|
78
|
+
AND EXISTS (SELECT 1 FROM schedule_slot_items c WHERE c.slot_id = s.id)
|
|
79
|
+
AND NOT EXISTS (SELECT 1 FROM schedule_slot_items c WHERE c.slot_id = s.id
|
|
80
|
+
AND c.status NOT IN ('submitted', 'no_candidate', 'duplicate', 'failed'))
|
|
81
|
+
AND (s.status IN ('pending', 'running') OR NOT EXISTS (
|
|
82
|
+
SELECT 1 FROM outbox o WHERE o.idempotency_key = 'notification:' || s.id || ':summary'
|
|
83
|
+
))`).all();
|
|
84
|
+
return rows.map((row) => this.toSlot(row));
|
|
85
|
+
}
|
|
74
86
|
markSlotStatus(id, status, error) {
|
|
75
87
|
const stamp = status === 'running' ? 'started_at' : status === 'success' || status === 'partial' || status === 'failed' ? 'completed_at' : null;
|
|
76
88
|
const sets = ['status = @status', 'last_error = @error'];
|
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.20.
|
|
5
|
+
exports.BUILD = { version: '2.20.4', commit: '5bb73436b670' };
|
|
6
6
|
//# sourceMappingURL=version.js.map
|
package/dist/webui/package.json
CHANGED
package/package.json
CHANGED