@rei-standard/amsg-server 2.6.0-next.11 → 2.6.0-next.13
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.md +68 -3
- package/dist/adapters/d1.d.ts +10 -2
- package/dist/adapters/interface.d.ts +12 -5
- package/dist/adapters/neon.d.ts +9 -2
- package/dist/adapters/pg.d.ts +9 -2
- package/dist/adapters/schema.d.ts +12 -1
- package/dist/adapters/schema.sqlite.d.ts +4 -1
- package/dist/{chunk-QGJSWMQP.cjs → chunk-5IOYCCFD.cjs} +302 -38
- package/dist/{chunk-WAFOSZL3.mjs → chunk-B2FDV7YK.mjs} +21 -0
- package/dist/{chunk-HZDUURIR.mjs → chunk-TV7N3RTH.mjs} +301 -37
- package/dist/{chunk-4SIN2R66.cjs → chunk-V2SDWGUB.cjs} +21 -0
- package/dist/cloudflare.cjs +2 -2
- package/dist/cloudflare.mjs +1 -1
- package/dist/handlers/get-message.d.ts +3 -0
- package/dist/index.cjs +23 -21
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.mjs +5 -3
- package/dist/lib/agentic-fire.d.ts +20 -0
- package/dist/lib/run-tick.d.ts +15 -0
- package/dist/lib/task-projection.d.ts +14 -1
- package/dist/{neon-2CS4TLNS.cjs → neon-3ASBLLCM.cjs} +37 -15
- package/dist/{neon-YUKD2B77.mjs → neon-CUIGAB2T.mjs} +29 -7
- package/dist/{pg-PM5RYRWH.cjs → pg-IEZTAR43.cjs} +37 -15
- package/dist/{pg-JNQ6VMXC.mjs → pg-KMJPL6QV.mjs} +29 -7
- package/dist/single-user.d.ts +4 -0
- package/package.json +1 -1
|
@@ -767,7 +767,7 @@ function createStateAccessors({ db, userId, userKey, maxStateValueBytes, now })
|
|
|
767
767
|
}
|
|
768
768
|
|
|
769
769
|
// src/server/lib/task-projection.js
|
|
770
|
-
function projectTask(row, decryptedPayload) {
|
|
770
|
+
function projectTask(row, decryptedPayload, options = {}) {
|
|
771
771
|
const payload = decryptedPayload || {};
|
|
772
772
|
const metadata = payload.metadata && typeof payload.metadata === "object" ? payload.metadata : {};
|
|
773
773
|
return {
|
|
@@ -786,10 +786,11 @@ function projectTask(row, decryptedPayload) {
|
|
|
786
786
|
createdAt: _nullishCoalesce(row.created_at, () => ( null)),
|
|
787
787
|
updatedAt: _nullishCoalesce(row.updated_at, () => ( null)),
|
|
788
788
|
// 角色归属 / 客户端侧的任务身份,取自排程方写进 metadata 的字段,宿主
|
|
789
|
-
// 靠它按角色过滤(contactName
|
|
790
|
-
// 宿主私有数据,留在服务端。缺席 → null。
|
|
789
|
+
// 靠它按角色过滤(contactName 会跨角色重名)。缺席 → null。
|
|
791
790
|
charId: _nullishCoalesce(metadata.charId, () => ( null)),
|
|
792
791
|
clientTaskId: _nullishCoalesce(metadata.amsgClientTaskId, () => ( null)),
|
|
792
|
+
// 整份 metadata 只在单条查询里给(见上面的 includeMetadata)。
|
|
793
|
+
...options.includeMetadata ? { metadata: _nullishCoalesce(payload.metadata, () => ( null)) } : {},
|
|
793
794
|
// 上一次没发出去的原因(run-tick 记进 payload 的 lastError)。
|
|
794
795
|
// reason 'stale' 表示错过触发时刻太久被判定不再补发;其余是投递失败的
|
|
795
796
|
// 错误信息。没有记录 → null。
|
|
@@ -1027,9 +1028,64 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
1027
1028
|
now: new Date(nowFn()),
|
|
1028
1029
|
scratch
|
|
1029
1030
|
});
|
|
1031
|
+
const progress = { sentCount: 0, total: 0, iterations: 0, skipReason: null };
|
|
1032
|
+
let settledStatus = "failed";
|
|
1033
|
+
let settledError = null;
|
|
1034
|
+
try {
|
|
1035
|
+
const outcome = await runFireChain({
|
|
1036
|
+
task,
|
|
1037
|
+
decryptedPayload,
|
|
1038
|
+
userKey,
|
|
1039
|
+
ctx,
|
|
1040
|
+
hooks,
|
|
1041
|
+
nowFn,
|
|
1042
|
+
sleep,
|
|
1043
|
+
readState,
|
|
1044
|
+
writeState,
|
|
1045
|
+
scratch,
|
|
1046
|
+
scheduleTask,
|
|
1047
|
+
fireCtx,
|
|
1048
|
+
progress
|
|
1049
|
+
});
|
|
1050
|
+
settledStatus = !outcome.handled ? "not-handled" : outcome.result.status === "skipped" ? "skipped" : "sent";
|
|
1051
|
+
return outcome;
|
|
1052
|
+
} catch (error) {
|
|
1053
|
+
settledError = error;
|
|
1054
|
+
throw error;
|
|
1055
|
+
} finally {
|
|
1056
|
+
await notifyFireSettled(ctx, {
|
|
1057
|
+
task,
|
|
1058
|
+
status: settledStatus,
|
|
1059
|
+
skipReason: settledStatus === "skipped" ? progress.skipReason : null,
|
|
1060
|
+
sentCount: progress.sentCount,
|
|
1061
|
+
total: progress.total,
|
|
1062
|
+
iterations: progress.iterations,
|
|
1063
|
+
error: settledError,
|
|
1064
|
+
scratch,
|
|
1065
|
+
readState,
|
|
1066
|
+
writeState
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
async function runFireChain({
|
|
1071
|
+
task,
|
|
1072
|
+
decryptedPayload,
|
|
1073
|
+
userKey,
|
|
1074
|
+
ctx,
|
|
1075
|
+
hooks,
|
|
1076
|
+
nowFn,
|
|
1077
|
+
sleep,
|
|
1078
|
+
readState,
|
|
1079
|
+
writeState,
|
|
1080
|
+
scratch,
|
|
1081
|
+
scheduleTask,
|
|
1082
|
+
fireCtx,
|
|
1083
|
+
progress
|
|
1084
|
+
}) {
|
|
1030
1085
|
const before = await hooks.onBeforeFire(fireCtx);
|
|
1031
1086
|
if (before == null) return { handled: false };
|
|
1032
1087
|
if (typeof before === "object" && before.skip === true) {
|
|
1088
|
+
progress.skipReason = "before-fire";
|
|
1033
1089
|
return { handled: true, result: { success: true, messagesSent: 0, status: "skipped", iterations: 0 } };
|
|
1034
1090
|
}
|
|
1035
1091
|
const normalized = normalizeBeforeFireResult(before);
|
|
@@ -1049,6 +1105,7 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
1049
1105
|
if (nowFn() >= deadline) {
|
|
1050
1106
|
throw new Error(`AGENTIC_TOTAL_TIMEOUT: fire chain exceeded ${totalTimeoutMs}ms after ${iteration} LLM round(s)`);
|
|
1051
1107
|
}
|
|
1108
|
+
progress.iterations = iteration + 1;
|
|
1052
1109
|
const roundTimeoutMs = Math.max(1, Math.min(3e5, deadline - nowFn()));
|
|
1053
1110
|
const { response: llmResponse } = await _amsgshared.callLlm.call(void 0,
|
|
1054
1111
|
{
|
|
@@ -1086,6 +1143,7 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
1086
1143
|
continue;
|
|
1087
1144
|
}
|
|
1088
1145
|
if (decision.decision === "skip-push") {
|
|
1146
|
+
progress.skipReason = "skip-push";
|
|
1089
1147
|
return { handled: true, result: { success: true, messagesSent: 0, status: "skipped", iterations: iteration + 1 } };
|
|
1090
1148
|
}
|
|
1091
1149
|
if (decision.decision === "finish") {
|
|
@@ -1100,7 +1158,8 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
1100
1158
|
sleep,
|
|
1101
1159
|
scratch,
|
|
1102
1160
|
readState,
|
|
1103
|
-
writeState
|
|
1161
|
+
writeState,
|
|
1162
|
+
progress
|
|
1104
1163
|
});
|
|
1105
1164
|
return { handled: true, result: { success: true, messagesSent, status: "finished", iterations: iteration + 1 } };
|
|
1106
1165
|
}
|
|
@@ -1143,6 +1202,14 @@ async function notifyAfterSend(ctx, info) {
|
|
|
1143
1202
|
console.warn("[amsg-server] onAfterSend hook \u629B\u9519\uFF08\u5DF2\u5FFD\u7565\uFF09:", hookError && hookError.message);
|
|
1144
1203
|
}
|
|
1145
1204
|
}
|
|
1205
|
+
async function notifyFireSettled(ctx, info) {
|
|
1206
|
+
if (typeof ctx.onFireSettled !== "function") return;
|
|
1207
|
+
try {
|
|
1208
|
+
await ctx.onFireSettled(info);
|
|
1209
|
+
} catch (hookError) {
|
|
1210
|
+
console.warn("[amsg-server] onFireSettled hook \u629B\u9519\uFF08\u5DF2\u5FFD\u7565\uFF09:", hookError && hookError.message);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1146
1213
|
function stampTaskIdentity(push, task, decryptedPayload, occurrenceMs) {
|
|
1147
1214
|
push.taskId = _nullishCoalesce(task.id, () => ( null));
|
|
1148
1215
|
push.taskUuid = _nullishCoalesce(task.uuid, () => ( null));
|
|
@@ -1160,10 +1227,12 @@ async function sendHookPushPayloads({
|
|
|
1160
1227
|
sleep,
|
|
1161
1228
|
scratch,
|
|
1162
1229
|
readState,
|
|
1163
|
-
writeState
|
|
1230
|
+
writeState,
|
|
1231
|
+
progress
|
|
1164
1232
|
}) {
|
|
1165
1233
|
const total = pushPayloads.length;
|
|
1166
1234
|
let sentCount = 0;
|
|
1235
|
+
progress.total = total;
|
|
1167
1236
|
const afterSendBase = { task, total, scratch, readState, writeState };
|
|
1168
1237
|
try {
|
|
1169
1238
|
if (!ctx.vapid || !ctx.vapid.email || !ctx.vapid.publicKey || !ctx.vapid.privateKey) {
|
|
@@ -1185,6 +1254,7 @@ async function sendHookPushPayloads({
|
|
|
1185
1254
|
stampTaskIdentity(push, task, decryptedPayload, occurrenceMs);
|
|
1186
1255
|
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(push));
|
|
1187
1256
|
sentCount++;
|
|
1257
|
+
progress.sentCount = sentCount;
|
|
1188
1258
|
if (i < total - 1) await sleep(SLEEP_BETWEEN_MESSAGES_MS);
|
|
1189
1259
|
}
|
|
1190
1260
|
} catch (error) {
|
|
@@ -1579,10 +1649,14 @@ function positiveNumber(value) {
|
|
|
1579
1649
|
function resolveClaimLeaseMs(ctx) {
|
|
1580
1650
|
return positiveNumber(ctx.claimLeaseMs) || Math.max(DEFAULT_CLAIM_LEASE_MS, positiveNumber(ctx.totalTimeoutMs) + CLAIM_LEASE_MARGIN_MS);
|
|
1581
1651
|
}
|
|
1652
|
+
async function deriveSerializeGroup(userKey, rawKey) {
|
|
1653
|
+
return _amsgshared.bytesToBase64Url.call(void 0, await _amsgshared.hmacSha256.call(void 0, _amsgshared.utf8.call(void 0, userKey), _amsgshared.utf8.call(void 0, rawKey)));
|
|
1654
|
+
}
|
|
1582
1655
|
async function runScheduledTick(ctx) {
|
|
1583
1656
|
const db = ctx.db;
|
|
1584
1657
|
const masterKey = ctx.masterKey;
|
|
1585
1658
|
const claimLeaseMs = resolveClaimLeaseMs(ctx);
|
|
1659
|
+
const serializeBy = typeof ctx.serializeBy === "function" ? ctx.serializeBy : null;
|
|
1586
1660
|
const startTime = Date.now();
|
|
1587
1661
|
const tasks = await db.getPendingTasks(50);
|
|
1588
1662
|
const MAX_CONCURRENT = 8;
|
|
@@ -1591,19 +1665,47 @@ async function runScheduledTick(ctx) {
|
|
|
1591
1665
|
successCount: 0,
|
|
1592
1666
|
failedCount: 0,
|
|
1593
1667
|
claimSkippedTasks: 0,
|
|
1668
|
+
serializeSkippedTasks: 0,
|
|
1594
1669
|
deletedOnceOffTasks: 0,
|
|
1595
1670
|
updatedRecurringTasks: 0,
|
|
1596
1671
|
staleTasks: [],
|
|
1597
1672
|
failedTasks: []
|
|
1598
1673
|
};
|
|
1674
|
+
const groupsTakenThisTick = /* @__PURE__ */ new Set();
|
|
1599
1675
|
const supportsClaim = typeof db.claimTask === "function";
|
|
1600
|
-
async function claimForThisTick(task) {
|
|
1676
|
+
async function claimForThisTick(task, serializeGroup) {
|
|
1601
1677
|
if (!supportsClaim) return true;
|
|
1602
1678
|
const leaseUntil = new Date(Date.now() + claimLeaseMs).toISOString();
|
|
1603
|
-
return !!await db.claimTask(task.id, task.next_send_at, leaseUntil);
|
|
1679
|
+
return !!await db.claimTask(task.id, task.next_send_at, leaseUntil, serializeGroup);
|
|
1604
1680
|
}
|
|
1605
1681
|
async function updateAndRelease(taskId, fields) {
|
|
1606
|
-
return db.updateTaskById(
|
|
1682
|
+
return db.updateTaskById(
|
|
1683
|
+
taskId,
|
|
1684
|
+
supportsClaim ? { ...fields, lease_until: null, retry_after: null } : fields
|
|
1685
|
+
);
|
|
1686
|
+
}
|
|
1687
|
+
async function decryptTask(task) {
|
|
1688
|
+
try {
|
|
1689
|
+
const userKey = await deriveUserEncryptionKey(task.user_id, masterKey);
|
|
1690
|
+
const payload = JSON.parse(await decryptFromStorage(task.encrypted_payload, userKey));
|
|
1691
|
+
return { ok: true, userKey, payload };
|
|
1692
|
+
} catch (error) {
|
|
1693
|
+
return { ok: false, error };
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
function reserveSerializeGroup(task, decryptedPayload) {
|
|
1697
|
+
if (!serializeBy || !decryptedPayload) return { taken: false, rawKey: null };
|
|
1698
|
+
let rawKey;
|
|
1699
|
+
try {
|
|
1700
|
+
rawKey = serializeBy(buildHookTask(task, decryptedPayload));
|
|
1701
|
+
} catch (hookError) {
|
|
1702
|
+
console.warn("[amsg-server] serializeBy \u629B\u9519\uFF0C\u672C\u8DF3\u8DF3\u8FC7\u8FD9\u6761\u4EFB\u52A1:", hookError && hookError.message);
|
|
1703
|
+
return { taken: true, rawKey: null };
|
|
1704
|
+
}
|
|
1705
|
+
if (typeof rawKey !== "string" || !rawKey) return { taken: false, rawKey: null };
|
|
1706
|
+
if (groupsTakenThisTick.has(rawKey)) return { taken: true, rawKey };
|
|
1707
|
+
groupsTakenThisTick.add(rawKey);
|
|
1708
|
+
return { taken: false, rawKey };
|
|
1607
1709
|
}
|
|
1608
1710
|
async function encryptPayloadWithLastError(task, decryptedPayload, userKey, reason, extra) {
|
|
1609
1711
|
if (!decryptedPayload || !userKey) return null;
|
|
@@ -1653,7 +1755,11 @@ async function runScheduledTick(ctx) {
|
|
|
1653
1755
|
} else {
|
|
1654
1756
|
const nextRetryTime = new Date(Date.now() + (task.retry_count + 1) * 2 * 60 * 1e3);
|
|
1655
1757
|
if (supportsClaim) {
|
|
1656
|
-
await db.updateTaskById(task.id, {
|
|
1758
|
+
await db.updateTaskById(task.id, {
|
|
1759
|
+
retry_after: nextRetryTime.toISOString(),
|
|
1760
|
+
lease_until: null,
|
|
1761
|
+
retry_count: task.retry_count + 1
|
|
1762
|
+
});
|
|
1657
1763
|
} else {
|
|
1658
1764
|
await db.updateTaskById(task.id, { next_send_at: nextRetryTime.toISOString(), retry_count: task.retry_count + 1 });
|
|
1659
1765
|
}
|
|
@@ -1697,10 +1803,13 @@ async function runScheduledTick(ctx) {
|
|
|
1697
1803
|
messageDelivered: true
|
|
1698
1804
|
});
|
|
1699
1805
|
}
|
|
1700
|
-
async function processTask(task) {
|
|
1806
|
+
async function processTask({ task, decrypted, serializeKey }) {
|
|
1807
|
+
const decryptedPayload = decrypted.ok ? decrypted.payload : null;
|
|
1808
|
+
const userKey = decrypted.ok ? decrypted.userKey : null;
|
|
1809
|
+
const serializeGroup = serializeKey ? await deriveSerializeGroup(userKey, serializeKey) : null;
|
|
1701
1810
|
let claimed;
|
|
1702
1811
|
try {
|
|
1703
|
-
claimed = await claimForThisTick(task);
|
|
1812
|
+
claimed = await claimForThisTick(task, serializeGroup);
|
|
1704
1813
|
} catch (error) {
|
|
1705
1814
|
results.failedCount++;
|
|
1706
1815
|
results.failedTasks.push({ taskId: task.id, reason: error.message || "\u4EFB\u52A1\u5360\u4F4D\u5931\u8D25", status: "claim_failed" });
|
|
@@ -1710,13 +1819,8 @@ async function runScheduledTick(ctx) {
|
|
|
1710
1819
|
results.claimSkippedTasks++;
|
|
1711
1820
|
return;
|
|
1712
1821
|
}
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
try {
|
|
1716
|
-
userKey = await deriveUserEncryptionKey(task.user_id, masterKey);
|
|
1717
|
-
decryptedPayload = JSON.parse(await decryptFromStorage(task.encrypted_payload, userKey));
|
|
1718
|
-
} catch (error) {
|
|
1719
|
-
await handleDeliveryFailure(task, error.message || "\u4EFB\u52A1\u8F7D\u8377\u89E3\u5BC6\u5931\u8D25", null, null, null);
|
|
1822
|
+
if (!decrypted.ok) {
|
|
1823
|
+
await handleDeliveryFailure(task, decrypted.error && decrypted.error.message || "\u4EFB\u52A1\u8F7D\u8377\u89E3\u5BC6\u5931\u8D25", null, null, null);
|
|
1720
1824
|
return;
|
|
1721
1825
|
}
|
|
1722
1826
|
const recurrenceType = decryptedPayload.recurrenceType;
|
|
@@ -1812,12 +1916,21 @@ async function runScheduledTick(ctx) {
|
|
|
1812
1916
|
await handlePostSendPersistenceFailure(task, error.message || "\u53D1\u9001\u540E\u72B6\u6001\u66F4\u65B0\u5931\u8D25", recurrenceType, tzId);
|
|
1813
1917
|
}
|
|
1814
1918
|
}
|
|
1815
|
-
const taskQueue = [
|
|
1919
|
+
const taskQueue = [];
|
|
1920
|
+
for (const task of tasks) {
|
|
1921
|
+
const decrypted = await decryptTask(task);
|
|
1922
|
+
const reserved = reserveSerializeGroup(task, decrypted.ok ? decrypted.payload : null);
|
|
1923
|
+
if (reserved.taken) {
|
|
1924
|
+
results.serializeSkippedTasks++;
|
|
1925
|
+
continue;
|
|
1926
|
+
}
|
|
1927
|
+
taskQueue.push({ task, decrypted, serializeKey: reserved.rawKey });
|
|
1928
|
+
}
|
|
1816
1929
|
const processing = [];
|
|
1817
1930
|
while (taskQueue.length > 0 || processing.length > 0) {
|
|
1818
1931
|
while (processing.length < MAX_CONCURRENT && taskQueue.length > 0) {
|
|
1819
|
-
const
|
|
1820
|
-
const promise = processTask(
|
|
1932
|
+
const entry = taskQueue.shift();
|
|
1933
|
+
const promise = processTask(entry);
|
|
1821
1934
|
processing.push(promise);
|
|
1822
1935
|
promise.finally(() => {
|
|
1823
1936
|
const index = processing.indexOf(promise);
|
|
@@ -1838,6 +1951,9 @@ async function runScheduledTick(ctx) {
|
|
|
1838
1951
|
executionTime,
|
|
1839
1952
|
details: {
|
|
1840
1953
|
claimSkippedTasks: results.claimSkippedTasks,
|
|
1954
|
+
// 分组串行拦下的条数:本跳里同分组已经放行过别的任务。跨跳被拦下的
|
|
1955
|
+
// (分组在别的 tick 里正忙)走的是占位那一步,计在 claimSkippedTasks。
|
|
1956
|
+
serializeSkippedTasks: results.serializeSkippedTasks,
|
|
1841
1957
|
deletedOnceOffTasks: results.deletedOnceOffTasks,
|
|
1842
1958
|
updatedRecurringTasks: results.updatedRecurringTasks,
|
|
1843
1959
|
staleTasks: results.staleTasks,
|
|
@@ -1894,6 +2010,9 @@ function createUpdateMessageHandler(ctx) {
|
|
|
1894
2010
|
if (updates.nextSendAt && !isValidISO8601(updates.nextSendAt)) {
|
|
1895
2011
|
return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["nextSendAt"] } } } };
|
|
1896
2012
|
}
|
|
2013
|
+
if (Object.prototype.hasOwnProperty.call(updates, "contactName") && (typeof updates.contactName !== "string" || !updates.contactName.trim())) {
|
|
2014
|
+
return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "contactName \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32", details: { invalidFields: ["contactName"] } } } };
|
|
2015
|
+
}
|
|
1897
2016
|
if (updates.recurrenceType && !["none", "daily", "weekly"].includes(updates.recurrenceType)) {
|
|
1898
2017
|
return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["recurrenceType"] } } } };
|
|
1899
2018
|
}
|
|
@@ -1951,9 +2070,15 @@ function createUpdateMessageHandler(ctx) {
|
|
|
1951
2070
|
const updatedData = {
|
|
1952
2071
|
...existingData,
|
|
1953
2072
|
...promptUpdates,
|
|
2073
|
+
...Object.prototype.hasOwnProperty.call(updates, "contactName") && { contactName: updates.contactName },
|
|
1954
2074
|
...updates.userMessage && { userMessage: updates.userMessage },
|
|
1955
2075
|
...updates.recurrenceType && { recurrenceType: updates.recurrenceType },
|
|
1956
2076
|
...Object.prototype.hasOwnProperty.call(updates, "tzId") && { tzId: _nullishCoalesce(updates.tzId, () => ( null)) },
|
|
2077
|
+
// avatarUrl 走 truthy spread:显式传 null 只是「不改」,不是「清空」。
|
|
2078
|
+
// 与 tzId / splitPattern 那几个 hasOwnProperty 的字段不一样,是因为
|
|
2079
|
+
// §6.2 的软清空策略要求非法头像从 patch 里被摘掉、旧头像原样保留,而
|
|
2080
|
+
// 「摘掉」和「传了个 null」在这一层看起来是同一件事。真要支持清空,得
|
|
2081
|
+
// 先把这两件事区分开,不能只把判断换成 hasOwnProperty。
|
|
1957
2082
|
...updates.avatarUrl && { avatarUrl: updates.avatarUrl },
|
|
1958
2083
|
...updates.metadata && { metadata: updates.metadata },
|
|
1959
2084
|
...updates.apiUrl && { apiUrl: updates.apiUrl },
|
|
@@ -2082,6 +2207,57 @@ function createMessagesHandler(ctx) {
|
|
|
2082
2207
|
return { GET };
|
|
2083
2208
|
}
|
|
2084
2209
|
|
|
2210
|
+
// src/server/handlers/get-message.js
|
|
2211
|
+
function createGetMessageHandler(ctx) {
|
|
2212
|
+
async function GET(url, headers) {
|
|
2213
|
+
const tenantResult = await ctx.tenantManager.resolveTenant(headers, { url });
|
|
2214
|
+
if (!tenantResult.ok) {
|
|
2215
|
+
return tenantResult.error;
|
|
2216
|
+
}
|
|
2217
|
+
const tenantCtx = tenantResult.context;
|
|
2218
|
+
const db = tenantCtx.db;
|
|
2219
|
+
const masterKey = tenantCtx.masterKey;
|
|
2220
|
+
const userId = getHeader(headers, "x-user-id");
|
|
2221
|
+
if (!userId) {
|
|
2222
|
+
return {
|
|
2223
|
+
status: 400,
|
|
2224
|
+
body: { success: false, error: { code: "USER_ID_REQUIRED", message: "\u5FC5\u987B\u63D0\u4F9B X-User-Id \u8BF7\u6C42\u5934" } }
|
|
2225
|
+
};
|
|
2226
|
+
}
|
|
2227
|
+
if (!isValidUUIDv4(userId)) {
|
|
2228
|
+
return {
|
|
2229
|
+
status: 400,
|
|
2230
|
+
body: { success: false, error: { code: "INVALID_USER_ID_FORMAT", message: "X-User-Id \u5FC5\u987B\u662F UUID v4 \u683C\u5F0F" } }
|
|
2231
|
+
};
|
|
2232
|
+
}
|
|
2233
|
+
const taskUuid = new URL(url, "https://dummy").searchParams.get("id");
|
|
2234
|
+
if (!taskUuid) {
|
|
2235
|
+
return { status: 400, body: { success: false, error: { code: "TASK_ID_REQUIRED", message: "\u7F3A\u5C11\u4EFB\u52A1ID" } } };
|
|
2236
|
+
}
|
|
2237
|
+
const row = await db.getTaskByUuid(taskUuid, userId);
|
|
2238
|
+
if (!row) {
|
|
2239
|
+
const taskStatus = await db.getTaskStatus(taskUuid, userId);
|
|
2240
|
+
if (!taskStatus) {
|
|
2241
|
+
return { status: 404, body: { success: false, error: { code: "TASK_NOT_FOUND", message: "\u6307\u5B9A\u7684\u4EFB\u52A1\u4E0D\u5B58\u5728\u6216\u5DF2\u88AB\u5220\u9664" } } };
|
|
2242
|
+
}
|
|
2243
|
+
return { status: 409, body: { success: false, error: { code: "TASK_ALREADY_COMPLETED", message: "\u4EFB\u52A1\u5DF2\u5B8C\u6210\u6216\u5DF2\u5931\u8D25\uFF0C\u65E0\u6CD5\u66F4\u65B0" } } };
|
|
2244
|
+
}
|
|
2245
|
+
const userKey = await deriveUserEncryptionKey(userId, masterKey);
|
|
2246
|
+
const decrypted = JSON.parse(await decryptFromStorage(row.encrypted_payload, userKey));
|
|
2247
|
+
const task = projectTask(row, decrypted, { includeMetadata: true });
|
|
2248
|
+
return {
|
|
2249
|
+
status: 200,
|
|
2250
|
+
body: {
|
|
2251
|
+
success: true,
|
|
2252
|
+
encrypted: true,
|
|
2253
|
+
version: 1,
|
|
2254
|
+
data: await encryptPayload({ task }, userKey)
|
|
2255
|
+
}
|
|
2256
|
+
};
|
|
2257
|
+
}
|
|
2258
|
+
return { GET };
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2085
2261
|
// src/server/handlers/push-subscription.js
|
|
2086
2262
|
function err(status, code, message, details) {
|
|
2087
2263
|
const error = details === void 0 ? { code, message } : { code, message, details };
|
|
@@ -2196,6 +2372,8 @@ var SQLITE_TABLE_SQL = `
|
|
|
2196
2372
|
message_type TEXT NOT NULL CHECK (message_type IN ('fixed', 'prompted', 'auto', 'instant')),
|
|
2197
2373
|
next_send_at TEXT NOT NULL,
|
|
2198
2374
|
lease_until TEXT,
|
|
2375
|
+
retry_after TEXT,
|
|
2376
|
+
serialize_group TEXT,
|
|
2199
2377
|
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'sent', 'failed')),
|
|
2200
2378
|
retry_count INTEGER NOT NULL DEFAULT 0,
|
|
2201
2379
|
created_at TEXT NOT NULL,
|
|
@@ -2207,6 +2385,16 @@ var SQLITE_MIGRATIONS = [
|
|
|
2207
2385
|
name: "add_lease_until",
|
|
2208
2386
|
sql: "ALTER TABLE scheduled_messages ADD COLUMN lease_until TEXT",
|
|
2209
2387
|
description: "Task claim lease (2.6.0)"
|
|
2388
|
+
},
|
|
2389
|
+
{
|
|
2390
|
+
name: "add_retry_after",
|
|
2391
|
+
sql: "ALTER TABLE scheduled_messages ADD COLUMN retry_after TEXT",
|
|
2392
|
+
description: "Retry backoff, held apart from the claim lease (2.6.0)"
|
|
2393
|
+
},
|
|
2394
|
+
{
|
|
2395
|
+
name: "add_serialize_group",
|
|
2396
|
+
sql: "ALTER TABLE scheduled_messages ADD COLUMN serialize_group TEXT",
|
|
2397
|
+
description: "Serialization group for runScheduledTick serializeBy (2.6.0)"
|
|
2210
2398
|
}
|
|
2211
2399
|
];
|
|
2212
2400
|
var SQLITE_INDEXES = [
|
|
@@ -2248,6 +2436,14 @@ var SQLITE_INDEXES = [
|
|
|
2248
2436
|
WHERE uuid IS NOT NULL`,
|
|
2249
2437
|
description: "UUID uniqueness guard",
|
|
2250
2438
|
critical: true
|
|
2439
|
+
},
|
|
2440
|
+
{
|
|
2441
|
+
name: "idx_serialize_group_lease",
|
|
2442
|
+
sql: `CREATE INDEX IF NOT EXISTS idx_serialize_group_lease
|
|
2443
|
+
ON scheduled_messages (serialize_group, lease_until)
|
|
2444
|
+
WHERE serialize_group IS NOT NULL AND status = 'pending'`,
|
|
2445
|
+
description: "Serialization group busy-check index (claimTask)",
|
|
2446
|
+
critical: false
|
|
2251
2447
|
}
|
|
2252
2448
|
];
|
|
2253
2449
|
var CLIENT_STATE_TABLE_SQL = `
|
|
@@ -2276,6 +2472,8 @@ var UPDATABLE_COLUMNS = /* @__PURE__ */ new Set([
|
|
|
2276
2472
|
"message_type",
|
|
2277
2473
|
"next_send_at",
|
|
2278
2474
|
"lease_until",
|
|
2475
|
+
"retry_after",
|
|
2476
|
+
"serialize_group",
|
|
2279
2477
|
"status",
|
|
2280
2478
|
"retry_count",
|
|
2281
2479
|
"created_at",
|
|
@@ -2329,7 +2527,7 @@ var D1Adapter = class {
|
|
|
2329
2527
|
);
|
|
2330
2528
|
}
|
|
2331
2529
|
return {
|
|
2332
|
-
columnsCreated:
|
|
2530
|
+
columnsCreated: 13,
|
|
2333
2531
|
indexesCreated: indexResults.filter((r) => r.status === "success").length,
|
|
2334
2532
|
indexesFailed: indexResults.filter((r) => r.status === "failed").length,
|
|
2335
2533
|
columns: [],
|
|
@@ -2356,7 +2554,7 @@ var D1Adapter = class {
|
|
|
2356
2554
|
}
|
|
2357
2555
|
async getTaskByUuid(uuid, userId) {
|
|
2358
2556
|
return this._db.prepare(
|
|
2359
|
-
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
|
|
2557
|
+
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count, created_at, updated_at
|
|
2360
2558
|
FROM scheduled_messages
|
|
2361
2559
|
WHERE uuid = ? AND user_id = ? AND status = 'pending'
|
|
2362
2560
|
LIMIT 1`
|
|
@@ -2428,9 +2626,10 @@ var D1Adapter = class {
|
|
|
2428
2626
|
FROM scheduled_messages
|
|
2429
2627
|
WHERE status = 'pending' AND next_send_at <= ?
|
|
2430
2628
|
AND (lease_until IS NULL OR lease_until <= ?)
|
|
2629
|
+
AND (retry_after IS NULL OR retry_after <= ?)
|
|
2431
2630
|
ORDER BY next_send_at ASC
|
|
2432
2631
|
LIMIT ?`
|
|
2433
|
-
).bind(now, now, limit).all();
|
|
2632
|
+
).bind(now, now, now, limit).all();
|
|
2434
2633
|
return res.results || [];
|
|
2435
2634
|
}
|
|
2436
2635
|
/**
|
|
@@ -2454,19 +2653,46 @@ var D1Adapter = class {
|
|
|
2454
2653
|
* 着非归一化写法的行(如 +08:00 结尾),归一化后反而对不上,那条任务会永
|
|
2455
2654
|
* 远领不到。
|
|
2456
2655
|
*
|
|
2656
|
+
* 带 serializeGroup 时多一道分组门:同一分组里已经有别的行拿着未到期的租
|
|
2657
|
+
* 约,这条就领不走(同一分组同时只跑一条)。判定和写租约在同一条 UPDATE
|
|
2658
|
+
* 里完成,「先查再占」的空档天然不存在——两个 tick 同时来,只有一个改得动
|
|
2659
|
+
* 行。分组门只看租约,不看 `retry_after`:等着重试的任务其实闲着,不该把
|
|
2660
|
+
* 同分组的其他任务一起堵住。
|
|
2661
|
+
*
|
|
2457
2662
|
* @param {number} taskId
|
|
2458
2663
|
* @param {string} expectedNextSendAt - 读这行时拿到的 next_send_at 原值
|
|
2459
2664
|
* @param {string|Date} leaseUntil - 租期末尾
|
|
2460
|
-
* @
|
|
2665
|
+
* @param {string|null} [serializeGroup] - 串行分组标识;空表示不参与分组串行
|
|
2666
|
+
* @returns {Promise<boolean>} true = 领到了;false = 别人正拿着租约、同分组
|
|
2667
|
+
* 有任务正在跑、排期被改过、或行已不是 pending
|
|
2461
2668
|
*/
|
|
2462
|
-
async claimTask(taskId, expectedNextSendAt, leaseUntil) {
|
|
2669
|
+
async claimTask(taskId, expectedNextSendAt, leaseUntil, serializeGroup = null) {
|
|
2463
2670
|
const expected = typeof expectedNextSendAt === "string" ? expectedNextSendAt : this._iso(expectedNextSendAt);
|
|
2671
|
+
const grouped = typeof serializeGroup === "string" && serializeGroup.length > 0;
|
|
2672
|
+
const now = this._now();
|
|
2673
|
+
const sets = ["lease_until = ?"];
|
|
2674
|
+
const values = [this._iso(leaseUntil)];
|
|
2675
|
+
if (grouped) {
|
|
2676
|
+
sets.push("serialize_group = ?");
|
|
2677
|
+
values.push(serializeGroup);
|
|
2678
|
+
}
|
|
2679
|
+
sets.push("updated_at = ?");
|
|
2680
|
+
values.push(now);
|
|
2681
|
+
let where = `id = ? AND status = 'pending' AND next_send_at = ?
|
|
2682
|
+
AND (lease_until IS NULL OR lease_until <= ?)`;
|
|
2683
|
+
values.push(taskId, expected, now);
|
|
2684
|
+
if (grouped) {
|
|
2685
|
+
where += `
|
|
2686
|
+
AND NOT EXISTS (
|
|
2687
|
+
SELECT 1 FROM scheduled_messages busy
|
|
2688
|
+
WHERE busy.serialize_group = ? AND busy.id <> ?
|
|
2689
|
+
AND busy.status = 'pending' AND busy.lease_until > ?
|
|
2690
|
+
)`;
|
|
2691
|
+
values.push(serializeGroup, taskId, now);
|
|
2692
|
+
}
|
|
2464
2693
|
const res = await this._db.prepare(
|
|
2465
|
-
`UPDATE scheduled_messages
|
|
2466
|
-
|
|
2467
|
-
WHERE id = ? AND status = 'pending' AND next_send_at = ?
|
|
2468
|
-
AND (lease_until IS NULL OR lease_until <= ?)`
|
|
2469
|
-
).bind(this._iso(leaseUntil), this._now(), taskId, expected, this._now()).run();
|
|
2694
|
+
`UPDATE scheduled_messages SET ${sets.join(", ")} WHERE ${where}`
|
|
2695
|
+
).bind(...values).run();
|
|
2470
2696
|
return (res.meta.changes || 0) > 0;
|
|
2471
2697
|
}
|
|
2472
2698
|
async listTasks(userId, opts = {}) {
|
|
@@ -2889,7 +3115,7 @@ function createClientStateHandler(ctx) {
|
|
|
2889
3115
|
}
|
|
2890
3116
|
|
|
2891
3117
|
// src/server/lib/version.js
|
|
2892
|
-
var SERVER_VERSION = true ? "2.6.0-next.
|
|
3118
|
+
var SERVER_VERSION = true ? "2.6.0-next.13" : "0.0.0-dev";
|
|
2893
3119
|
|
|
2894
3120
|
// src/server/handlers/capabilities.js
|
|
2895
3121
|
var SERVER_FEATURES = Object.freeze([
|
|
@@ -2924,7 +3150,15 @@ var SERVER_FEATURES = Object.freeze([
|
|
|
2924
3150
|
// 任务行支持 tzId:daily / weekly 按该时区的墙钟推进。
|
|
2925
3151
|
"task-timezone",
|
|
2926
3152
|
// 推送订阅是用户级的一份(PUT/GET/DELETE /push-subscription),任务不携带。
|
|
2927
|
-
"user-push-subscription"
|
|
3153
|
+
"user-push-subscription",
|
|
3154
|
+
// GET /message?id=<uuid>:单条任务,带完整 metadata(列表只给两个子字段)。
|
|
3155
|
+
"get-message-detail",
|
|
3156
|
+
// PUT /update-message 认 contactName(改了角色名之后旧任务的通知标题跟着改)。
|
|
3157
|
+
"update-message-contact-name",
|
|
3158
|
+
// runScheduledTick 认 serializeBy:同一分组的任务同时只跑一条,跨跳也算。
|
|
3159
|
+
"tick-serialize-by",
|
|
3160
|
+
// 一次 fire 无论什么结局都调 onFireSettled(发完 / 跳过 / 抛错)。
|
|
3161
|
+
"fire-settled-hook"
|
|
2928
3162
|
]);
|
|
2929
3163
|
function createCapabilitiesHandler(ctx) {
|
|
2930
3164
|
async function GET(url, headers) {
|
|
@@ -2968,6 +3202,9 @@ function createSingleUserServer(config) {
|
|
|
2968
3202
|
maxStateValueBytes: config.maxStateValueBytes,
|
|
2969
3203
|
// 推送发出(或发挂)之后的 hook(见 lib/agentic-fire.js 的 notifyAfterSend)。
|
|
2970
3204
|
onAfterSend: config.onAfterSend,
|
|
3205
|
+
// 一次 fire 收尾的 hook,什么结局都会调一次(见 lib/agentic-fire.js 的
|
|
3206
|
+
// notifyFireSettled)。
|
|
3207
|
+
onFireSettled: config.onFireSettled,
|
|
2971
3208
|
// hook 的 ctx.scheduleTask() 单次 fire 建任务的条数上限(默认 2)。
|
|
2972
3209
|
maxScheduledTasksPerFire: config.maxScheduledTasksPerFire
|
|
2973
3210
|
};
|
|
@@ -2980,6 +3217,7 @@ function createSingleUserServer(config) {
|
|
|
2980
3217
|
updateMessage: createUpdateMessageHandler(ctx),
|
|
2981
3218
|
cancelMessage: createCancelMessageHandler(ctx),
|
|
2982
3219
|
messages: createMessagesHandler(ctx),
|
|
3220
|
+
getMessage: createGetMessageHandler(ctx),
|
|
2983
3221
|
vapidPublicKey: createVapidPublicKeyHandler(ctx),
|
|
2984
3222
|
clientState: createClientStateHandler(ctx),
|
|
2985
3223
|
pushSubscription: createPushSubscriptionHandler(ctx),
|
|
@@ -3071,6 +3309,12 @@ function corsHeadersFor(cors, requestOrigin) {
|
|
|
3071
3309
|
if (allowOrigin !== "*") headers["Vary"] = "Origin";
|
|
3072
3310
|
return headers;
|
|
3073
3311
|
}
|
|
3312
|
+
function degradedCorsHeaders(requestOrigin) {
|
|
3313
|
+
return corsHeadersFor({ origin: requestOrigin, maxAge: 0 }, requestOrigin);
|
|
3314
|
+
}
|
|
3315
|
+
function internalErrorResponse(cors) {
|
|
3316
|
+
return jsonResponse(500, { success: false, error: { code: "INTERNAL_ERROR", message: "\u670D\u52A1\u5668\u5185\u90E8\u9519\u8BEF" } }, cors);
|
|
3317
|
+
}
|
|
3074
3318
|
function createSingleUserCloudflareWorker(buildConfig) {
|
|
3075
3319
|
async function resolveConfig(env) {
|
|
3076
3320
|
const cfg = await buildConfig(env);
|
|
@@ -3078,10 +3322,19 @@ function createSingleUserCloudflareWorker(buildConfig) {
|
|
|
3078
3322
|
return cfg;
|
|
3079
3323
|
}
|
|
3080
3324
|
async function fetch(request, env) {
|
|
3325
|
+
const requestOrigin = request.headers.get("origin") || "";
|
|
3326
|
+
const method = request.method.toUpperCase();
|
|
3327
|
+
let cfg;
|
|
3328
|
+
try {
|
|
3329
|
+
cfg = await resolveConfig(env);
|
|
3330
|
+
} catch (error) {
|
|
3331
|
+
console.error("[amsg single-user] fetch() config build failed:", error && error.message);
|
|
3332
|
+
const degraded = degradedCorsHeaders(requestOrigin);
|
|
3333
|
+
if (method === "OPTIONS" && degraded) return new Response(null, { status: 204, headers: degraded });
|
|
3334
|
+
return internalErrorResponse(degraded);
|
|
3335
|
+
}
|
|
3336
|
+
const cors = corsHeadersFor(cfg.cors, requestOrigin);
|
|
3081
3337
|
try {
|
|
3082
|
-
const cfg = await resolveConfig(env);
|
|
3083
|
-
const cors = corsHeadersFor(cfg.cors, request.headers.get("origin") || "");
|
|
3084
|
-
const method = request.method.toUpperCase();
|
|
3085
3338
|
if (method === "OPTIONS") {
|
|
3086
3339
|
return cors ? new Response(null, { status: 204, headers: cors }) : jsonResponse(404, { success: false, error: { code: "NOT_FOUND", message: "Unknown route" } });
|
|
3087
3340
|
}
|
|
@@ -3098,6 +3351,8 @@ function createSingleUserCloudflareWorker(buildConfig) {
|
|
|
3098
3351
|
result = await server.handlers.scheduleMessage.POST(headers, await request.text());
|
|
3099
3352
|
} else if (method === "GET" && pathname.endsWith("/messages")) {
|
|
3100
3353
|
result = await server.handlers.messages.GET(url, headers);
|
|
3354
|
+
} else if (method === "GET" && pathname.endsWith("/message")) {
|
|
3355
|
+
result = await server.handlers.getMessage.GET(url, headers);
|
|
3101
3356
|
} else if (method === "PUT" && pathname.endsWith("/update-message")) {
|
|
3102
3357
|
result = await server.handlers.updateMessage.PUT(url, headers, await request.text());
|
|
3103
3358
|
} else if (method === "DELETE" && pathname.endsWith("/cancel-message")) {
|
|
@@ -3124,7 +3379,7 @@ function createSingleUserCloudflareWorker(buildConfig) {
|
|
|
3124
3379
|
return jsonResponse(result.status, result.body, cors);
|
|
3125
3380
|
} catch (error) {
|
|
3126
3381
|
console.error("[amsg single-user] fetch() unhandled error:", error && error.message);
|
|
3127
|
-
return
|
|
3382
|
+
return internalErrorResponse(cors);
|
|
3128
3383
|
}
|
|
3129
3384
|
}
|
|
3130
3385
|
async function scheduled(event, env) {
|
|
@@ -3158,6 +3413,14 @@ function createSingleUserCloudflareWorker(buildConfig) {
|
|
|
3158
3413
|
// 推送发出(或发挂)之后的 hook:{ task, sentCount, total, error,
|
|
3159
3414
|
// scratch, readState, writeState }(best-effort,见 lib/agentic-fire.js)。
|
|
3160
3415
|
onAfterSend: cfg.onAfterSend,
|
|
3416
|
+
// 一次 fire 收尾的 hook:{ task, status, skipReason, sentCount, total,
|
|
3417
|
+
// iterations, error, scratch, readState, writeState }。发完 / 跳过 /
|
|
3418
|
+
// 抛错都会调,宿主用它做「开始时占点什么、结束时放掉」那类收尾
|
|
3419
|
+
//(best-effort,见 lib/agentic-fire.js)。
|
|
3420
|
+
onFireSettled: cfg.onFireSettled,
|
|
3421
|
+
// 分组串行:(task) => 分组标识 | null。同一分组的任务同时只跑一条,
|
|
3422
|
+
// 跨跳也算(见 lib/run-tick.js)。不配 = 全并发,与以前一致。
|
|
3423
|
+
serializeBy: cfg.serializeBy,
|
|
3161
3424
|
// 任务占位租期(默认 10 分钟,随 totalTimeoutMs 抬高)。
|
|
3162
3425
|
claimLeaseMs: cfg.claimLeaseMs
|
|
3163
3426
|
});
|
|
@@ -3208,4 +3471,5 @@ function createSingleUserCloudflareWorker(buildConfig) {
|
|
|
3208
3471
|
|
|
3209
3472
|
|
|
3210
3473
|
|
|
3211
|
-
|
|
3474
|
+
|
|
3475
|
+
exports.isValidTimeZoneId = isValidTimeZoneId; exports.advanceOccurrence = advanceOccurrence; exports.planNextOccurrence = planNextOccurrence; exports.nextFutureOccurrence = nextFutureOccurrence; exports.validateAvatarUrl = _amsgshared.validateAvatarUrl; exports.isValidISO8601 = isValidISO8601; exports.isValidUrl = isValidUrl; exports.isValidUUID = isValidUUID; exports.isValidUUIDv4 = isValidUUIDv4; exports.validateSplitPattern = validateSplitPattern; exports.validateLlmMessagesArray = validateLlmMessagesArray; exports.validateScheduleMessagePayload = validateScheduleMessagePayload; exports.parseJsonBody = parseJsonBody; exports.getHeader = getHeader; exports.base64UrlToBytes = _amsgshared.base64UrlToBytes; exports.utf8 = _amsgshared.utf8; exports.utf8Decode = _amsgshared.utf8Decode; exports.bytesToBase64Url = _amsgshared.bytesToBase64Url; exports.timingSafeEqualBytes = _amsgshared.timingSafeEqualBytes; exports.deriveUserEncryptionKey = deriveUserEncryptionKey; exports.decryptPayload = decryptPayload; exports.encryptForStorage = encryptForStorage; exports.decryptFromStorage = decryptFromStorage; exports.createGetUserKeyHandler = createGetUserKeyHandler; exports.createScheduleMessageHandler = createScheduleMessageHandler; exports.runScheduledTick = runScheduledTick; exports.createUpdateMessageHandler = createUpdateMessageHandler; exports.createCancelMessageHandler = createCancelMessageHandler; exports.createMessagesHandler = createMessagesHandler; exports.createGetMessageHandler = createGetMessageHandler; exports.createPushSubscriptionHandler = createPushSubscriptionHandler; exports.createD1Adapter = createD1Adapter; exports.createSingleUserServer = createSingleUserServer; exports.WEB_PUSH_MAX_BODY_BYTES = WEB_PUSH_MAX_BODY_BYTES; exports.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES = WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES; exports.MAX_PUSH_PAYLOAD_BYTES = MAX_PUSH_PAYLOAD_BYTES; exports.PUSH_ENVELOPE_RESERVED_BYTES = PUSH_ENVELOPE_RESERVED_BYTES; exports.measurePushPayload = measurePushPayload; exports.createWebCryptoWebPush = createWebCryptoWebPush; exports.createSingleUserCloudflareWorker = createSingleUserCloudflareWorker;
|
|
@@ -8,6 +8,8 @@ var TABLE_SQL = `
|
|
|
8
8
|
message_type VARCHAR(50) NOT NULL CHECK (message_type IN ('fixed', 'prompted', 'auto', 'instant')),
|
|
9
9
|
next_send_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
10
10
|
lease_until TIMESTAMP WITH TIME ZONE,
|
|
11
|
+
retry_after TIMESTAMP WITH TIME ZONE,
|
|
12
|
+
serialize_group VARCHAR(64),
|
|
11
13
|
status VARCHAR(50) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'sent', 'failed')),
|
|
12
14
|
retry_count INTEGER DEFAULT 0,
|
|
13
15
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
@@ -19,6 +21,16 @@ var MIGRATIONS = [
|
|
|
19
21
|
name: "add_lease_until",
|
|
20
22
|
sql: "ALTER TABLE scheduled_messages ADD COLUMN IF NOT EXISTS lease_until TIMESTAMP WITH TIME ZONE",
|
|
21
23
|
description: "Task claim lease (2.6.0)"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "add_retry_after",
|
|
27
|
+
sql: "ALTER TABLE scheduled_messages ADD COLUMN IF NOT EXISTS retry_after TIMESTAMP WITH TIME ZONE",
|
|
28
|
+
description: "Retry backoff, held apart from the claim lease (2.6.0)"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: "add_serialize_group",
|
|
32
|
+
sql: "ALTER TABLE scheduled_messages ADD COLUMN IF NOT EXISTS serialize_group VARCHAR(64)",
|
|
33
|
+
description: "Serialization group for runScheduledTick serializeBy (2.6.0)"
|
|
22
34
|
}
|
|
23
35
|
];
|
|
24
36
|
var INDEXES = [
|
|
@@ -56,6 +68,13 @@ var INDEXES = [
|
|
|
56
68
|
WHERE uuid IS NOT NULL`,
|
|
57
69
|
description: "UUID uniqueness guard",
|
|
58
70
|
critical: true
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "idx_serialize_group_lease",
|
|
74
|
+
sql: `CREATE INDEX IF NOT EXISTS idx_serialize_group_lease
|
|
75
|
+
ON scheduled_messages (serialize_group, lease_until)
|
|
76
|
+
WHERE serialize_group IS NOT NULL AND status = 'pending'`,
|
|
77
|
+
description: "Serialization group busy-check index (claimTask)"
|
|
59
78
|
}
|
|
60
79
|
];
|
|
61
80
|
var PUSH_SUBSCRIPTION_TABLE_SQL = `
|
|
@@ -85,6 +104,8 @@ var UPDATABLE_COLUMNS = /* @__PURE__ */ new Set([
|
|
|
85
104
|
"message_type",
|
|
86
105
|
"next_send_at",
|
|
87
106
|
"lease_until",
|
|
107
|
+
"retry_after",
|
|
108
|
+
"serialize_group",
|
|
88
109
|
"status",
|
|
89
110
|
"retry_count",
|
|
90
111
|
"created_at",
|