@rei-standard/amsg-server 3.0.0 → 3.1.0-next.1
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 +110 -11
- package/dist/adapters/d1.d.ts +8 -3
- package/dist/adapters/interface.d.ts +6 -3
- package/dist/{chunk-YBDQJVFQ.cjs → chunk-D4SKSCIY.cjs} +461 -140
- package/dist/{chunk-RL7V7VSS.mjs → chunk-EIM3PKFY.mjs} +413 -92
- package/dist/cloudflare.cjs +4 -2
- package/dist/cloudflare.d.ts +1 -1
- package/dist/cloudflare.mjs +3 -1
- package/dist/index.cjs +27 -25
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +3 -1
- package/dist/lib/agentic-fire.d.ts +1 -0
- package/dist/lib/attachments.d.ts +39 -0
- package/dist/lib/client-state-store.d.ts +6 -4
- package/dist/lib/state-accessors.d.ts +12 -8
- package/dist/lib/task-projection.d.ts +2 -2
- package/package.json +1 -1
|
@@ -147,14 +147,33 @@ import {
|
|
|
147
147
|
// src/server/lib/encryption.js
|
|
148
148
|
var base64ToBytes = base64UrlToBytes;
|
|
149
149
|
var TAG_LENGTH_BYTES = 16;
|
|
150
|
+
var AES_KEY_CACHE_MAX = 128;
|
|
151
|
+
var aesKeyCache = /* @__PURE__ */ new Map();
|
|
150
152
|
function importAesKey(hexKey, usage) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
153
|
+
const cacheKey = `${usage}:${hexKey}`;
|
|
154
|
+
const cached = aesKeyCache.get(cacheKey);
|
|
155
|
+
if (cached) return cached;
|
|
156
|
+
let promise;
|
|
157
|
+
try {
|
|
158
|
+
promise = globalThis.crypto.subtle.importKey(
|
|
159
|
+
"raw",
|
|
160
|
+
hexToBytes(hexKey),
|
|
161
|
+
{ name: "AES-GCM" },
|
|
162
|
+
false,
|
|
163
|
+
[usage]
|
|
164
|
+
).catch((err6) => {
|
|
165
|
+
aesKeyCache.delete(cacheKey);
|
|
166
|
+
throw err6;
|
|
167
|
+
});
|
|
168
|
+
} catch (err6) {
|
|
169
|
+
return Promise.reject(err6);
|
|
170
|
+
}
|
|
171
|
+
if (aesKeyCache.size >= AES_KEY_CACHE_MAX) {
|
|
172
|
+
const oldest = aesKeyCache.keys().next().value;
|
|
173
|
+
if (oldest !== void 0) aesKeyCache.delete(oldest);
|
|
174
|
+
}
|
|
175
|
+
aesKeyCache.set(cacheKey, promise);
|
|
176
|
+
return promise;
|
|
158
177
|
}
|
|
159
178
|
async function aesGcmSeal(hexKey, iv, plaintextBytes) {
|
|
160
179
|
const key = await importAesKey(hexKey, "encrypt");
|
|
@@ -428,6 +447,9 @@ function taskPayloadByteLength(serializedPayload) {
|
|
|
428
447
|
return taskPayloadEncoder.encode(serializedPayload).length;
|
|
429
448
|
}
|
|
430
449
|
function validateScheduleMessagePayload(payload) {
|
|
450
|
+
if (payload.taskInput != null && (typeof payload.taskInput !== "object" || Array.isArray(payload.taskInput))) {
|
|
451
|
+
return { valid: false, errorCode: "INVALID_TASK_INPUT", errorMessage: "taskInput must be an object or null" };
|
|
452
|
+
}
|
|
431
453
|
if (!payload.contactName || typeof payload.contactName !== "string") {
|
|
432
454
|
return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { missingFields: ["contactName"] } };
|
|
433
455
|
}
|
|
@@ -982,6 +1004,40 @@ function createWebCryptoWebPush(vapid = {}, { ttl = SCHEDULED_DEFAULT_TTL } = {}
|
|
|
982
1004
|
};
|
|
983
1005
|
}
|
|
984
1006
|
|
|
1007
|
+
// src/server/lib/attachments.js
|
|
1008
|
+
var ATTACHMENT_NAMESPACE = "amsg:attachments";
|
|
1009
|
+
var ATTACHMENT_RETENTION_DAYS = 30;
|
|
1010
|
+
var MAX_ATTACHMENT_TTL_MS = ATTACHMENT_RETENTION_DAYS * 24 * 60 * 60 * 1e3;
|
|
1011
|
+
function createAttachmentStore({ task, occurrenceMs, writeState, now }) {
|
|
1012
|
+
return async function storeAttachment(value, { ttlMs = MAX_ATTACHMENT_TTL_MS, owner, kind = "attachment", ownerGeneration } = {}) {
|
|
1013
|
+
if (typeof value !== "string") throw new TypeError("storeAttachment value must be a serialized string");
|
|
1014
|
+
if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0 || ttlMs > MAX_ATTACHMENT_TTL_MS) {
|
|
1015
|
+
throw new RangeError("storeAttachment ttlMs must be between 1 millisecond and 30 days");
|
|
1016
|
+
}
|
|
1017
|
+
if (typeof task.uuid !== "string" || !task.uuid || !Number.isFinite(occurrenceMs)) {
|
|
1018
|
+
throw new TypeError("storeAttachment requires a task UUID and a valid occurrence");
|
|
1019
|
+
}
|
|
1020
|
+
const at = now();
|
|
1021
|
+
const ref = { version: (
|
|
1022
|
+
/** @type {1} */
|
|
1023
|
+
1
|
|
1024
|
+
), namespace: ATTACHMENT_NAMESPACE, key: randomUUID(), taskUuid: task.uuid, occurrenceMs, expiresAt: at + ttlMs };
|
|
1025
|
+
let result;
|
|
1026
|
+
try {
|
|
1027
|
+
result = await writeState(ATTACHMENT_NAMESPACE, [{ key: ref.key, value: JSON.stringify({ ...ref, owner, kind, ownerGeneration, value }), updatedAt: at, owner, kind, ownerGeneration }]);
|
|
1028
|
+
} catch (error) {
|
|
1029
|
+
if (error?.code === "AGENTIC_STATE_WRITE_UNSUPPORTED") {
|
|
1030
|
+
throw new DeploymentConfigError("ATTACHMENT_STORAGE_UNSUPPORTED: attachments require a client_state adapter (built-in support: D1)", { code: "ATTACHMENT_STORAGE_UNSUPPORTED" });
|
|
1031
|
+
}
|
|
1032
|
+
throw error;
|
|
1033
|
+
}
|
|
1034
|
+
if (result.upserted !== 1 || result.skipped !== 0) {
|
|
1035
|
+
throw new Error("ATTACHMENT_WRITE_INCOMPLETE: attachment was not stored");
|
|
1036
|
+
}
|
|
1037
|
+
return ref;
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
|
|
985
1041
|
// src/server/lib/agentic-fire.js
|
|
986
1042
|
import {
|
|
987
1043
|
assertValidDecision,
|
|
@@ -1192,6 +1248,7 @@ async function writeClientStateEntries({ db, userId, userKey, entries, now }) {
|
|
|
1192
1248
|
let upserted = 0;
|
|
1193
1249
|
let skipped = 0;
|
|
1194
1250
|
let deleted = 0;
|
|
1251
|
+
let unknown = 0;
|
|
1195
1252
|
const skippedEntries = [];
|
|
1196
1253
|
if (Array.isArray(result.outcomes) && result.outcomes.length === physicalRows.length) {
|
|
1197
1254
|
for (let i = 0; i < rootRowIndexes.length; i++) {
|
|
@@ -1203,9 +1260,10 @@ async function writeClientStateEntries({ db, userId, userKey, entries, now }) {
|
|
|
1203
1260
|
skippedEntries.push({ namespace: entry.namespace, key: entry.key });
|
|
1204
1261
|
}
|
|
1205
1262
|
}
|
|
1263
|
+
} else if (result.upserted === physicalRows.length && result.skipped === 0) {
|
|
1264
|
+
upserted = rootRowIndexes.length;
|
|
1206
1265
|
} else {
|
|
1207
|
-
|
|
1208
|
-
skipped = result.skipped;
|
|
1266
|
+
unknown = rootRowIndexes.length;
|
|
1209
1267
|
}
|
|
1210
1268
|
const cleanupOutcomes = Array.isArray(result.cleanupOutcomes) && result.cleanupOutcomes.length === cleanups.length ? result.cleanupOutcomes : null;
|
|
1211
1269
|
for (const { cleanupIndex, entry } of deletions) {
|
|
@@ -1216,22 +1274,26 @@ async function writeClientStateEntries({ db, userId, userKey, entries, now }) {
|
|
|
1216
1274
|
deleted++;
|
|
1217
1275
|
}
|
|
1218
1276
|
}
|
|
1219
|
-
return { upserted, skipped, deleted, skippedEntries };
|
|
1277
|
+
return { upserted, skipped, deleted, skippedEntries, ...unknown > 0 ? { unknown } : {} };
|
|
1220
1278
|
}
|
|
1221
1279
|
|
|
1222
1280
|
// src/server/lib/state-accessors.js
|
|
1223
1281
|
function createStateAccessors({ db, userId, userKey, maxStateValueBytes, now }) {
|
|
1224
1282
|
const nowFn = typeof now === "function" ? now : Date.now;
|
|
1225
1283
|
const valueCeiling = Number.isInteger(maxStateValueBytes) && maxStateValueBytes > 0 ? maxStateValueBytes : DEFAULT_MAX_STATE_VALUE_BYTES;
|
|
1226
|
-
const readState = async (namespace) => {
|
|
1284
|
+
const readState = async (namespace, { key } = {}) => {
|
|
1227
1285
|
if (typeof namespace !== "string" || !namespace.trim()) {
|
|
1228
1286
|
throw new TypeError("readState(namespace) requires a non-empty string");
|
|
1229
1287
|
}
|
|
1288
|
+
if (key !== void 0 && (typeof key !== "string" || !key.trim() || key.length > MAX_KEY_CHARS || INTERNAL_STATE_CHAR_RE.test(key))) {
|
|
1289
|
+
throw new TypeError(`readState key must be a non-empty string of at most ${MAX_KEY_CHARS} characters without control characters`);
|
|
1290
|
+
}
|
|
1230
1291
|
if (!db || typeof db.getClientState !== "function") return [];
|
|
1231
|
-
const rows = await db.getClientState(userId, namespace);
|
|
1292
|
+
const rows = await db.getClientState(userId, namespace, key === void 0 ? void 0 : { key });
|
|
1232
1293
|
return resolveClientStateEntries(
|
|
1233
|
-
|
|
1234
|
-
() =>
|
|
1294
|
+
// 老适配器可能忽略第三个参数,仍在解密前过滤根行,保持返回契约。
|
|
1295
|
+
key === void 0 ? rows : rows.filter((row) => row.key === key),
|
|
1296
|
+
() => db.getClientState(userId, chunkNamespaceFor(namespace), key === void 0 ? void 0 : { keyPrefix: chunkKeyPrefixFor(key) }),
|
|
1235
1297
|
(value) => decryptFromStorage(value, userKey)
|
|
1236
1298
|
);
|
|
1237
1299
|
};
|
|
@@ -1287,6 +1349,9 @@ function createStateAccessors({ db, userId, userKey, maxStateValueBytes, now })
|
|
|
1287
1349
|
namespace,
|
|
1288
1350
|
key: entry.key,
|
|
1289
1351
|
value: entry.value,
|
|
1352
|
+
owner: entry.owner,
|
|
1353
|
+
kind: entry.kind,
|
|
1354
|
+
ownerGeneration: entry.ownerGeneration,
|
|
1290
1355
|
updatedAt: entry.updatedAt ?? at,
|
|
1291
1356
|
...entry.version !== void 0 ? { version: entry.version } : {}
|
|
1292
1357
|
};
|
|
@@ -1618,7 +1683,7 @@ function projectTask(row, decryptedPayload, options = {}) {
|
|
|
1618
1683
|
// 凭据引用只是名字(cred_id),本体在 llm_credentials 表里,凭据字段本身
|
|
1619
1684
|
// 照旧被白名单挡在外面。
|
|
1620
1685
|
credRefs: payload.credRefs ?? null,
|
|
1621
|
-
// 整份 metadata
|
|
1686
|
+
// 整份 metadata 由 includeMetadata 控制,单条查询与 hook 回执会带。
|
|
1622
1687
|
...options.includeMetadata ? { metadata: payload.metadata ?? null } : {},
|
|
1623
1688
|
// 上一次没发出去的原因。reason 'stale' 表示错过触发时刻太久被判定不再补
|
|
1624
1689
|
// 发;其余是投递失败的错误信息。没有记录 → null。
|
|
@@ -1709,6 +1774,9 @@ function normalizeBeforeFireResult(result) {
|
|
|
1709
1774
|
return { messages: result };
|
|
1710
1775
|
}
|
|
1711
1776
|
if (result && typeof result === "object" && Array.isArray(result.messages)) {
|
|
1777
|
+
if (result.allowedToolNames !== void 0 && (!Array.isArray(result.allowedToolNames) || result.allowedToolNames.some((name) => typeof name !== "string" || !name.trim()))) {
|
|
1778
|
+
throw markPermanent(new TypeError("allowedToolNames must be an array of non-empty strings"), "AGENTIC_BAD_BEFORE_FIRE");
|
|
1779
|
+
}
|
|
1712
1780
|
return {
|
|
1713
1781
|
messages: result.messages,
|
|
1714
1782
|
maxToolIterations: result.maxToolIterations,
|
|
@@ -1717,7 +1785,8 @@ function normalizeBeforeFireResult(result) {
|
|
|
1717
1785
|
// 用真值。真正决定请求体带不带 tools 的规则住在 llm.js 的
|
|
1718
1786
|
// buildAiRequestBody(那边有测试钉着),这层只是不把空数组往下传。
|
|
1719
1787
|
tools: Array.isArray(result.tools) && result.tools.length > 0 ? result.tools : void 0,
|
|
1720
|
-
toolChoice: result.toolChoice
|
|
1788
|
+
toolChoice: result.toolChoice,
|
|
1789
|
+
allowedToolNames: result.allowedToolNames === void 0 ? void 0 : new Set(result.allowedToolNames)
|
|
1721
1790
|
};
|
|
1722
1791
|
}
|
|
1723
1792
|
throw markPermanent(new TypeError(BAD_BEFORE_FIRE_MESSAGE), "AGENTIC_BAD_BEFORE_FIRE");
|
|
@@ -1769,6 +1838,8 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
1769
1838
|
const sessionId = task.id != null ? `sess_task_${task.id}${occurrenceSuffix(task)}` : `sess_${randomUUID()}`;
|
|
1770
1839
|
const messageIdBase = task.id != null ? `msg_task_${task.id}${occurrenceSuffix(task)}` : `msg_${randomUUID()}`;
|
|
1771
1840
|
const occurrenceMs = occurrenceMsOf(task);
|
|
1841
|
+
const storeAttachment = createAttachmentStore({ task, occurrenceMs, writeState, now: nowFn });
|
|
1842
|
+
const storeSettledAttachment = createAttachmentStore({ task, occurrenceMs, writeState: writeSettledState, now: nowFn });
|
|
1772
1843
|
const { emitResult } = createResultEmitter({
|
|
1773
1844
|
db: ctx.db,
|
|
1774
1845
|
task,
|
|
@@ -1791,17 +1862,37 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
1791
1862
|
const row = await ctx.db.getTaskByUuid(uuid, task.user_id);
|
|
1792
1863
|
if (!row) return null;
|
|
1793
1864
|
const payload = JSON.parse(await decryptFromStorage(row.encrypted_payload, userKey));
|
|
1794
|
-
return projectTask(row, payload);
|
|
1865
|
+
return projectTask(row, payload, { includeMetadata: true });
|
|
1795
1866
|
} catch (error) {
|
|
1796
1867
|
console.warn("[amsg-server] scheduleTask: \u8BFB\u53D6\u649E\u8F66\u4EFB\u52A1\u5931\u8D25\uFF08\u5DF2\u5FFD\u7565\uFF09:", error && error.message);
|
|
1797
1868
|
return null;
|
|
1798
1869
|
}
|
|
1799
1870
|
}
|
|
1871
|
+
const listTasks = async (options = {}) => {
|
|
1872
|
+
throwIfCancelled();
|
|
1873
|
+
const { status = "pending", limit = 100, offset = 0, includeMetadata = true } = options;
|
|
1874
|
+
if (!["pending", "sent", "failed", "all"].includes(status) || !Number.isInteger(limit) || limit < 1 || limit > 100 || !Number.isInteger(offset) || offset < 0) {
|
|
1875
|
+
throw new TypeError("listTasks requires a valid status, limit 1-100, and non-negative offset");
|
|
1876
|
+
}
|
|
1877
|
+
if (typeof ctx.db?.listTasks !== "function") {
|
|
1878
|
+
throw new DeploymentConfigError("AGENTIC_TASK_VIEW_UNSUPPORTED: adapter has no listTasks", { code: "AGENTIC_TASK_VIEW_UNSUPPORTED" });
|
|
1879
|
+
}
|
|
1880
|
+
const { tasks, total } = await ctx.db.listTasks(task.user_id, { status, limit, offset });
|
|
1881
|
+
const projected = await Promise.all(tasks.map(async (row) => projectTask(
|
|
1882
|
+
row,
|
|
1883
|
+
JSON.parse(await decryptFromStorage(row.encrypted_payload, userKey)),
|
|
1884
|
+
{ includeMetadata }
|
|
1885
|
+
)));
|
|
1886
|
+
return { tasks: projected, pagination: { total, limit, offset, hasMore: offset + tasks.length < total } };
|
|
1887
|
+
};
|
|
1800
1888
|
const scheduleTask = async (options) => {
|
|
1801
1889
|
throwIfCancelled();
|
|
1802
1890
|
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
1803
1891
|
throw new TypeError("scheduleTask(options) \u9700\u8981\u4E00\u4E2A\u5BF9\u8C61\uFF0C\u81F3\u5C11\u5305\u542B { firstSendTime }");
|
|
1804
1892
|
}
|
|
1893
|
+
if (options.taskInput != null && (typeof options.taskInput !== "object" || Array.isArray(options.taskInput))) {
|
|
1894
|
+
throw new TypeError("scheduleTask: taskInput must be an object or null");
|
|
1895
|
+
}
|
|
1805
1896
|
if (typeof options.firstSendTime !== "string" || !options.firstSendTime.trim()) {
|
|
1806
1897
|
throw new RangeError("scheduleTask: firstSendTime \u5FC5\u586B\uFF0C\u4E14\u5FC5\u987B\u662F ISO 8601 \u5B57\u7B26\u4E32");
|
|
1807
1898
|
}
|
|
@@ -1896,7 +1987,8 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
1896
1987
|
// 与凭据同列的投递配置:中转的非标准参数(thinking 之类)跟着任务走,
|
|
1897
1988
|
// 自排的后续任务不该突然掉档。
|
|
1898
1989
|
llmExtraBody: decryptedPayload.llmExtraBody ?? null,
|
|
1899
|
-
metadata
|
|
1990
|
+
metadata,
|
|
1991
|
+
taskInput: options.taskInput ?? null
|
|
1900
1992
|
};
|
|
1901
1993
|
const serializedTaskData = JSON.stringify(fullTaskData);
|
|
1902
1994
|
const sizeError = validateTaskPayloadSize(serializedTaskData);
|
|
@@ -1950,7 +2042,8 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
1950
2042
|
created: true,
|
|
1951
2043
|
id: created.id ?? null,
|
|
1952
2044
|
uuid: created.uuid ?? uuid,
|
|
1953
|
-
nextSendAt: created.next_send_at ?? nextSendAt
|
|
2045
|
+
nextSendAt: created.next_send_at ?? nextSendAt,
|
|
2046
|
+
task: projectTask({ status: "pending", next_send_at: nextSendAt, ...created }, fullTaskData, { includeMetadata: true })
|
|
1954
2047
|
};
|
|
1955
2048
|
};
|
|
1956
2049
|
const cancelTask = async (uuid) => {
|
|
@@ -2018,7 +2111,11 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
2018
2111
|
const stillPending = await ctx.db.getTaskByUuid(uuid, task.user_id);
|
|
2019
2112
|
return { renewed: false, reason: stillPending ? "in_flight" : "not_found" };
|
|
2020
2113
|
}
|
|
2021
|
-
return { renewed: true, uuid, nextSendAt: nextSendAtIso
|
|
2114
|
+
return { renewed: true, uuid, nextSendAt: nextSendAtIso, task: projectTask(
|
|
2115
|
+
{ ...row, next_send_at: nextSendAtIso, retry_count: 0 },
|
|
2116
|
+
payload,
|
|
2117
|
+
{ includeMetadata: true }
|
|
2118
|
+
) };
|
|
2022
2119
|
};
|
|
2023
2120
|
const resolveLlmCredential2 = async (credId) => {
|
|
2024
2121
|
if (typeof credId !== "string" || !credId.trim()) {
|
|
@@ -2035,7 +2132,9 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
2035
2132
|
readState,
|
|
2036
2133
|
writeState,
|
|
2037
2134
|
emitResult,
|
|
2135
|
+
storeAttachment,
|
|
2038
2136
|
scheduleTask,
|
|
2137
|
+
listTasks,
|
|
2039
2138
|
cancelTask,
|
|
2040
2139
|
renewTask,
|
|
2041
2140
|
resolveLlmCredential: resolveLlmCredential2,
|
|
@@ -2068,8 +2167,10 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
2068
2167
|
readState,
|
|
2069
2168
|
writeState,
|
|
2070
2169
|
emitResult,
|
|
2170
|
+
storeAttachment,
|
|
2071
2171
|
scratch,
|
|
2072
2172
|
scheduleTask,
|
|
2173
|
+
listTasks,
|
|
2073
2174
|
cancelTask,
|
|
2074
2175
|
renewTask,
|
|
2075
2176
|
resolveLlmCredential: resolveLlmCredential2,
|
|
@@ -2128,7 +2229,8 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
2128
2229
|
scratch,
|
|
2129
2230
|
readState,
|
|
2130
2231
|
writeState: writeSettledState,
|
|
2131
|
-
emitResult
|
|
2232
|
+
emitResult,
|
|
2233
|
+
storeAttachment: storeSettledAttachment
|
|
2132
2234
|
});
|
|
2133
2235
|
}
|
|
2134
2236
|
}
|
|
@@ -2143,8 +2245,10 @@ async function runFireChain({
|
|
|
2143
2245
|
readState,
|
|
2144
2246
|
writeState,
|
|
2145
2247
|
emitResult,
|
|
2248
|
+
storeAttachment,
|
|
2146
2249
|
scratch,
|
|
2147
2250
|
scheduleTask,
|
|
2251
|
+
listTasks,
|
|
2148
2252
|
cancelTask,
|
|
2149
2253
|
renewTask,
|
|
2150
2254
|
resolveLlmCredential: resolveLlmCredential2,
|
|
@@ -2243,15 +2347,35 @@ async function runFireChain({
|
|
|
2243
2347
|
readState,
|
|
2244
2348
|
writeState,
|
|
2245
2349
|
emitResult,
|
|
2350
|
+
storeAttachment,
|
|
2246
2351
|
scheduleTask,
|
|
2352
|
+
listTasks,
|
|
2247
2353
|
cancelTask,
|
|
2248
2354
|
renewTask,
|
|
2249
2355
|
resolveLlmCredential: resolveLlmCredential2
|
|
2250
2356
|
});
|
|
2251
2357
|
const decision = await hooks.onLLMOutput(sessionCtx);
|
|
2252
2358
|
ctx.throwIfCancelled();
|
|
2359
|
+
const rawCalls = decision?.decision === "tool-request" ? extractToolCallsFromDecision(decision) : [];
|
|
2360
|
+
const validId = (call) => typeof call?.id === "string" && call.id.trim().length > 0;
|
|
2361
|
+
const nativeCalls = Array.isArray(assistantMessage.tool_calls) ? assistantMessage.tool_calls : [];
|
|
2362
|
+
const usedIds = new Set([...rawCalls, ...nativeCalls].filter(validId).map((call) => call.id));
|
|
2363
|
+
const malformedIds = /* @__PURE__ */ new Set();
|
|
2364
|
+
const toolCalls = rawCalls.map((call, index) => {
|
|
2365
|
+
let id = validId(call) ? call.id : `call_${iteration + 1}_${index + 1}`;
|
|
2366
|
+
if (!validId(call)) {
|
|
2367
|
+
while (usedIds.has(id)) id += "_";
|
|
2368
|
+
usedIds.add(id);
|
|
2369
|
+
}
|
|
2370
|
+
if (typeof call?.function?.name !== "string" || !call.function.name.trim()) {
|
|
2371
|
+
malformedIds.add(id);
|
|
2372
|
+
return { id, type: "function", function: { name: "invalid_tool_call", arguments: "{}" } };
|
|
2373
|
+
}
|
|
2374
|
+
return { ...call, id };
|
|
2375
|
+
});
|
|
2376
|
+
const validationDecision = decision?.decision === "tool-request" && Array.isArray(decision.toolCalls) && decision.toolCalls.length > 0 ? { ...decision, toolCalls } : decision;
|
|
2253
2377
|
try {
|
|
2254
|
-
assertValidDecision(
|
|
2378
|
+
assertValidDecision(validationDecision, { inlineToolCalls: true });
|
|
2255
2379
|
} catch (error) {
|
|
2256
2380
|
throw markPermanent(error, "AGENTIC_BAD_DECISION");
|
|
2257
2381
|
}
|
|
@@ -2282,14 +2406,19 @@ async function runFireChain({
|
|
|
2282
2406
|
});
|
|
2283
2407
|
return { handled: true, result: { success: true, messagesSent, status: "finished", iterations: iteration + 1 } };
|
|
2284
2408
|
}
|
|
2285
|
-
const toolCalls = extractToolCallsFromDecision(decision);
|
|
2286
2409
|
if (toolCalls.length === 0) {
|
|
2287
2410
|
throw taggedRetryableError(
|
|
2288
2411
|
"AGENTIC_EMPTY_TOOL_REQUEST: tool-request decision carried no toolCalls (neither decision.toolCalls nor pushPayloads[].toolCalls)",
|
|
2289
2412
|
"AGENTIC_EMPTY_TOOL_REQUEST"
|
|
2290
2413
|
);
|
|
2291
2414
|
}
|
|
2292
|
-
|
|
2415
|
+
const permittedCalls = toolCalls.filter((call) => !malformedIds.has(call.id) && (normalized.allowedToolNames === void 0 || normalized.allowedToolNames.has(call.function.name)));
|
|
2416
|
+
const deniedResults = toolCalls.filter((call) => !permittedCalls.includes(call)).map((call) => ({
|
|
2417
|
+
role: "tool",
|
|
2418
|
+
tool_call_id: call.id,
|
|
2419
|
+
content: JSON.stringify({ error: malformedIds.has(call.id) ? { code: "INVALID_TOOL_CALL", message: "Tool call requires a function name" } : { code: "TOOL_NOT_ALLOWED", message: "Tool execution is not allowed for this fire" } })
|
|
2420
|
+
}));
|
|
2421
|
+
if (permittedCalls.length > 0 && typeof hooks.executeToolCalls !== "function") {
|
|
2293
2422
|
throw new DeploymentConfigError(
|
|
2294
2423
|
"AGENTIC_CONFIG_ERROR: onLLMOutput returned tool-request but hooks.executeToolCalls is not configured",
|
|
2295
2424
|
{ code: "AGENTIC_CONFIG_ERROR" }
|
|
@@ -2301,7 +2430,7 @@ async function runFireChain({
|
|
|
2301
2430
|
let toolResults;
|
|
2302
2431
|
try {
|
|
2303
2432
|
ctx.throwIfCancelled();
|
|
2304
|
-
toolResults = await hooks.executeToolCalls(
|
|
2433
|
+
toolResults = permittedCalls.length > 0 ? await hooks.executeToolCalls(permittedCalls, sessionCtx) : [];
|
|
2305
2434
|
ctx.throwIfCancelled();
|
|
2306
2435
|
if (!Array.isArray(toolResults)) {
|
|
2307
2436
|
throw new TypeError('executeToolCalls must resolve to an array of { tool_call_id, role: "tool", content }');
|
|
@@ -2309,16 +2438,17 @@ async function runFireChain({
|
|
|
2309
2438
|
} catch (error) {
|
|
2310
2439
|
ctx.throwIfCancelled();
|
|
2311
2440
|
if (isTaskCancelledError(error)) throw error;
|
|
2312
|
-
toolResults =
|
|
2441
|
+
toolResults = permittedCalls.map((toolCall) => ({
|
|
2313
2442
|
tool_call_id: toolCall && typeof toolCall === "object" && typeof toolCall.id === "string" ? toolCall.id : "",
|
|
2314
2443
|
role: "tool",
|
|
2315
2444
|
content: `Tool execution failed: ${error?.message ?? String(error)}`
|
|
2316
2445
|
}));
|
|
2317
2446
|
}
|
|
2318
|
-
|
|
2319
|
-
const
|
|
2320
|
-
const
|
|
2321
|
-
const
|
|
2447
|
+
toolResults = [...toolResults, ...deniedResults];
|
|
2448
|
+
const declared = nativeCalls.filter(validId).map((call) => toolCalls.find((item) => item.id === call.id) || call);
|
|
2449
|
+
const nativeIds = new Set(declared.map((call) => call.id));
|
|
2450
|
+
const synthesized = toolCalls.filter((call) => !nativeIds.has(call.id));
|
|
2451
|
+
const assistantWithTools = { ...assistantMessage, tool_calls: [...declared, ...synthesized] };
|
|
2322
2452
|
messages = [...messages.slice(0, -1), assistantWithTools, ...toolResults];
|
|
2323
2453
|
}
|
|
2324
2454
|
throw taggedRetryableError(
|
|
@@ -3392,6 +3522,9 @@ function createScheduleMessageHandler(ctx) {
|
|
|
3392
3522
|
owner: payload.owner,
|
|
3393
3523
|
kind: payload.kind,
|
|
3394
3524
|
ownerGeneration: payload.ownerGeneration,
|
|
3525
|
+
// Input is persisted with the task in the same create/supersede transaction.
|
|
3526
|
+
// Shared credentials remain references and continue to resolve at fire time.
|
|
3527
|
+
taskInput: payload.taskInput ?? null,
|
|
3395
3528
|
metadata: payload.metadata || {}
|
|
3396
3529
|
};
|
|
3397
3530
|
const serializedTaskData = JSON.stringify(fullTaskData);
|
|
@@ -3599,7 +3732,7 @@ async function runScheduledTick(ctx) {
|
|
|
3599
3732
|
}
|
|
3600
3733
|
async function cleanupExpiredClientState(ctx) {
|
|
3601
3734
|
if (typeof ctx.db.cleanupClientState !== "function") return;
|
|
3602
|
-
const targets = planClientStateCleanup(ctx.clientStateTtl, Date.now());
|
|
3735
|
+
const targets = planClientStateCleanup({ [ATTACHMENT_NAMESPACE]: ATTACHMENT_RETENTION_DAYS, ...ctx.clientStateTtl }, Date.now());
|
|
3603
3736
|
if (targets.length === 0) return;
|
|
3604
3737
|
try {
|
|
3605
3738
|
await ctx.db.cleanupClientState(targets);
|
|
@@ -4259,6 +4392,9 @@ function createUpdateMessageHandler(ctx) {
|
|
|
4259
4392
|
if (!isPlainObject(updates)) {
|
|
4260
4393
|
return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF" } } };
|
|
4261
4394
|
}
|
|
4395
|
+
if (Object.prototype.hasOwnProperty.call(updates, "taskInput")) {
|
|
4396
|
+
return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "taskInput \u53EA\u80FD\u5728\u521B\u5EFA\u6216\u66FF\u6362\u4EFB\u52A1\u65F6\u8BBE\u7F6E", details: { invalidFields: ["taskInput"] } } } };
|
|
4397
|
+
}
|
|
4262
4398
|
if (updates.nextSendAt && !isValidISO8601(updates.nextSendAt)) {
|
|
4263
4399
|
return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["nextSendAt"] } } } };
|
|
4264
4400
|
}
|
|
@@ -5334,12 +5470,37 @@ var D1Adapter = class {
|
|
|
5334
5470
|
const row = await this._db.prepare("SELECT encrypted_value FROM cloud_resource_metadata WHERE user_id=? AND resource_key=?").bind(userId, resourceKey).first();
|
|
5335
5471
|
return row?.encrypted_value ?? null;
|
|
5336
5472
|
}
|
|
5473
|
+
async getOutboxPayload(userId, messageId) {
|
|
5474
|
+
const row = await this._db.prepare(
|
|
5475
|
+
"SELECT payload FROM message_outbox WHERE user_id = ? AND message_id = ? LIMIT 1"
|
|
5476
|
+
).bind(userId, messageId).first();
|
|
5477
|
+
return row?.payload ?? null;
|
|
5478
|
+
}
|
|
5337
5479
|
async listCloudResourceRows(userId) {
|
|
5338
|
-
const
|
|
5480
|
+
const queries = {
|
|
5481
|
+
task: "SELECT * FROM scheduled_messages WHERE user_id = ?",
|
|
5482
|
+
state: `SELECT user_id, namespace, key,
|
|
5483
|
+
CASE WHEN LENGTH(value) <= 1024 THEN value ELSE NULL END AS value,
|
|
5484
|
+
LENGTH(CAST(value AS BLOB)) AS byte_size,
|
|
5485
|
+
substr(value, 1, 128) AS value_prefix,
|
|
5486
|
+
CASE WHEN LENGTH(value) > 128 THEN substr(value, -64) ELSE '' END AS value_suffix,
|
|
5487
|
+
updated_at
|
|
5488
|
+
FROM client_state WHERE user_id = ?`,
|
|
5489
|
+
credential: "SELECT * FROM llm_credentials WHERE user_id = ?",
|
|
5490
|
+
outbox: `SELECT id, user_id, message_id, task_uuid, session_id, message_index, total_messages,
|
|
5491
|
+
CASE WHEN LENGTH(payload) <= 1024 THEN payload ELSE NULL END AS payload,
|
|
5492
|
+
LENGTH(CAST(payload AS BLOB)) AS byte_size,
|
|
5493
|
+
substr(payload, 1, 128) AS payload_prefix,
|
|
5494
|
+
CASE WHEN LENGTH(payload) > 128 THEN substr(payload, -64) ELSE '' END AS payload_suffix,
|
|
5495
|
+
created_at, delivered_at, acked_at
|
|
5496
|
+
FROM message_outbox WHERE user_id = ?`,
|
|
5497
|
+
subscription: "SELECT * FROM push_subscriptions WHERE user_id = ?",
|
|
5498
|
+
metadata: "SELECT * FROM cloud_resource_metadata WHERE user_id = ?"
|
|
5499
|
+
};
|
|
5339
5500
|
const result = { gaps: [] };
|
|
5340
|
-
await Promise.all(Object.entries(
|
|
5501
|
+
await Promise.all(Object.entries(queries).map(async ([type, sql]) => {
|
|
5341
5502
|
try {
|
|
5342
|
-
const response = await this._db.prepare(
|
|
5503
|
+
const response = await this._db.prepare(sql).bind(userId).all();
|
|
5343
5504
|
result[type] = response.results || [];
|
|
5344
5505
|
} catch {
|
|
5345
5506
|
result[type] = [];
|
|
@@ -5404,7 +5565,10 @@ var D1Adapter = class {
|
|
|
5404
5565
|
if (idempotencyKey) {
|
|
5405
5566
|
return this._cloudRecord(await this._db.prepare(`SELECT * FROM cloud_data_records WHERE user_id=? AND kind=? AND idempotency_key=?`).bind(userId, kind, idempotencyKey).first());
|
|
5406
5567
|
}
|
|
5407
|
-
|
|
5568
|
+
if (createOnly) {
|
|
5569
|
+
return this.getCloudDataRecord(userId, kind, id);
|
|
5570
|
+
}
|
|
5571
|
+
return { id, userId, kind, data, updatedAt: now };
|
|
5408
5572
|
}
|
|
5409
5573
|
async getCloudDataRecord(userId, kind, id) {
|
|
5410
5574
|
return this._cloudRecord(await this._db.prepare("SELECT * FROM cloud_data_records WHERE user_id=? AND kind=? AND id=?").bind(userId, kind, id).first());
|
|
@@ -5613,6 +5777,29 @@ var D1Adapter = class {
|
|
|
5613
5777
|
const row = await this._db.prepare(`SELECT ${condition.sql} AS valid`).bind(...condition.args).first();
|
|
5614
5778
|
return !!row?.valid;
|
|
5615
5779
|
}
|
|
5780
|
+
_rowSnapshotMatch(spec, row) {
|
|
5781
|
+
const clauses = [];
|
|
5782
|
+
const args = [];
|
|
5783
|
+
for (const column of spec.columns) {
|
|
5784
|
+
if (!Object.hasOwn(row, column)) continue;
|
|
5785
|
+
if ((column === "value" || column === "payload") && row[column] === null && typeof row.byte_size === "number") {
|
|
5786
|
+
clauses.push(
|
|
5787
|
+
`LENGTH(CAST(${column} AS BLOB)) IS ?`,
|
|
5788
|
+
`substr(${column}, 1, 128) IS ?`,
|
|
5789
|
+
`(CASE WHEN LENGTH(${column}) > 128 THEN substr(${column}, -64) ELSE '' END) IS ?`
|
|
5790
|
+
);
|
|
5791
|
+
args.push(
|
|
5792
|
+
row.byte_size,
|
|
5793
|
+
row[`${column}_prefix`] ?? null,
|
|
5794
|
+
row[`${column}_suffix`] ?? ""
|
|
5795
|
+
);
|
|
5796
|
+
} else {
|
|
5797
|
+
clauses.push(`${column} IS ?`);
|
|
5798
|
+
args.push(row[column]);
|
|
5799
|
+
}
|
|
5800
|
+
}
|
|
5801
|
+
return { predicate: clauses.join(" AND "), values: args };
|
|
5802
|
+
}
|
|
5616
5803
|
/** Delete only the exact physical snapshot; no untrusted SQL identifiers. */
|
|
5617
5804
|
async deleteCloudResourceRows(userId, locators, { operationId, leaseToken } = {}) {
|
|
5618
5805
|
const specs = {
|
|
@@ -5660,9 +5847,7 @@ var D1Adapter = class {
|
|
|
5660
5847
|
).bind(userId, namespace, key, "amsg-chunks" + namespace, key + "", key + " ", identities));
|
|
5661
5848
|
}
|
|
5662
5849
|
for (const row of locator.rows) {
|
|
5663
|
-
const
|
|
5664
|
-
const predicate = columns.map((column) => `${column} IS ?`).join(" AND ");
|
|
5665
|
-
const values = columns.map((column) => row[column]);
|
|
5850
|
+
const { predicate, values } = this._rowSnapshotMatch(spec, row);
|
|
5666
5851
|
const identity = spec.keys.map((column) => `${column} IS ?`).join(" AND ");
|
|
5667
5852
|
statements.push(this._db.prepare(
|
|
5668
5853
|
`INSERT INTO cloud_guard_assertions (allowed)
|
|
@@ -5671,8 +5856,8 @@ var D1Adapter = class {
|
|
|
5671
5856
|
).bind(...spec.keys.map((column) => row[column]), ...values));
|
|
5672
5857
|
}
|
|
5673
5858
|
for (const row of locator.rows) {
|
|
5674
|
-
const
|
|
5675
|
-
statements.push(this._db.prepare(`DELETE FROM ${spec.table} WHERE ${
|
|
5859
|
+
const { predicate, values } = this._rowSnapshotMatch(spec, row);
|
|
5860
|
+
statements.push(this._db.prepare(`DELETE FROM ${spec.table} WHERE ${predicate}`).bind(...values));
|
|
5676
5861
|
}
|
|
5677
5862
|
if (locator.metadataKey) statements.push(this._db.prepare("DELETE FROM cloud_resource_metadata WHERE user_id=? AND resource_key=?").bind(userId, locator.metadataKey));
|
|
5678
5863
|
try {
|
|
@@ -6175,24 +6360,33 @@ var D1Adapter = class {
|
|
|
6175
6360
|
return result;
|
|
6176
6361
|
}
|
|
6177
6362
|
/**
|
|
6178
|
-
*
|
|
6363
|
+
* Entries of one namespace (values still encrypted); optional exact/prefix selection.
|
|
6179
6364
|
*
|
|
6180
6365
|
* @param {string} userId
|
|
6181
6366
|
* @param {string} namespace
|
|
6182
6367
|
* @returns {Promise<Array<{ namespace: string, key: string, value: string, updated_at: number }>>}
|
|
6183
6368
|
*/
|
|
6184
|
-
async getClientState(userId, namespace) {
|
|
6369
|
+
async getClientState(userId, namespace, options = {}) {
|
|
6370
|
+
const params = [userId, namespace];
|
|
6371
|
+
let selector = "";
|
|
6372
|
+
if (options.key !== void 0) {
|
|
6373
|
+
selector = " AND key = ?";
|
|
6374
|
+
params.push(options.key);
|
|
6375
|
+
} else if (options.keyPrefix !== void 0) {
|
|
6376
|
+
selector = " AND key >= ? AND key < ?";
|
|
6377
|
+
params.push(options.keyPrefix, prefixRangeEnd(options.keyPrefix));
|
|
6378
|
+
}
|
|
6185
6379
|
const res = await this._db.prepare(
|
|
6186
6380
|
`SELECT namespace, key, value, updated_at
|
|
6187
6381
|
FROM client_state
|
|
6188
|
-
WHERE user_id = ? AND namespace =
|
|
6382
|
+
WHERE user_id = ? AND namespace = ?${selector}
|
|
6189
6383
|
ORDER BY key ASC`
|
|
6190
|
-
).bind(
|
|
6384
|
+
).bind(...params).all();
|
|
6191
6385
|
return res.results || [];
|
|
6192
6386
|
}
|
|
6193
6387
|
/**
|
|
6194
6388
|
* 例行清理:把指定命名空间下太久没更新的条目删掉(run-tick 每跳顺手调,
|
|
6195
|
-
*
|
|
6389
|
+
* 附件默认保留 30 天,宿主 `clientStateTtl` 可覆盖或配置其他命名空间)。
|
|
6196
6390
|
*
|
|
6197
6391
|
* 不限用户——「这个命名空间只留最近 N 天」是命名空间级的约定,单用户部署
|
|
6198
6392
|
* 下也就是这一个用户的行。指令由 lib/client-state-store.js 的
|
|
@@ -6924,8 +7118,9 @@ function createClientStateHandler(ctx) {
|
|
|
6924
7118
|
if (typeof db.upsertClientState !== "function") {
|
|
6925
7119
|
return err3(501, "CLIENT_STATE_NOT_SUPPORTED", "\u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301 client_state");
|
|
6926
7120
|
}
|
|
6927
|
-
const { upserted, skipped, deleted, skippedEntries } = await writeClientStateEntries({ db, userId, userKey, entries: accepted });
|
|
7121
|
+
const { upserted, skipped, deleted, skippedEntries, unknown } = await writeClientStateEntries({ db, userId, userKey, entries: accepted });
|
|
6928
7122
|
const data = { upserted, skipped };
|
|
7123
|
+
if (unknown > 0) data.unknown = unknown;
|
|
6929
7124
|
if (deleted > 0) data.deleted = deleted;
|
|
6930
7125
|
if (skippedEntries.length > 0) data.skippedEntries = skippedEntries;
|
|
6931
7126
|
if (rejected.length > 0) data.rejected = rejected;
|
|
@@ -6938,7 +7133,12 @@ function createClientStateHandler(ctx) {
|
|
|
6938
7133
|
const gate = requireUserId(headers);
|
|
6939
7134
|
if (gate.error) return gate.error;
|
|
6940
7135
|
const { userId } = gate;
|
|
6941
|
-
const
|
|
7136
|
+
const query = new URL(url, "https://dummy").searchParams;
|
|
7137
|
+
const namespace = query.get("namespace") || "";
|
|
7138
|
+
const key = query.get("key");
|
|
7139
|
+
if (key !== null && (!key.trim() || key.length > MAX_KEY_CHARS || INTERNAL_STATE_CHAR_RE.test(key))) {
|
|
7140
|
+
return err3(400, "INVALID_STATE_KEY", "key must be non-empty, at most 256 characters, without control characters");
|
|
7141
|
+
}
|
|
6942
7142
|
if (!namespace.trim()) return err3(400, "NAMESPACE_REQUIRED", "\u5FC5\u987B\u63D0\u4F9B namespace \u67E5\u8BE2\u53C2\u6570");
|
|
6943
7143
|
if (INTERNAL_STATE_CHAR_RE.test(namespace)) {
|
|
6944
7144
|
return err3(400, "INVALID_STATE_NAMESPACE", "namespace \u4E0D\u80FD\u5305\u542B\u63A7\u5236\u5B57\u7B26\uFF08\\u0000-\\u001f \u4E3A\u5E93\u5185\u90E8\u4FDD\u7559\uFF09");
|
|
@@ -6947,10 +7147,10 @@ function createClientStateHandler(ctx) {
|
|
|
6947
7147
|
return err3(501, "CLIENT_STATE_NOT_SUPPORTED", "\u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301 client_state");
|
|
6948
7148
|
}
|
|
6949
7149
|
const userKey = await deriveUserEncryptionKey(userId, masterKey);
|
|
6950
|
-
const rows = await db.getClientState(userId, namespace);
|
|
7150
|
+
const rows = await db.getClientState(userId, namespace, key === null ? void 0 : { key });
|
|
6951
7151
|
const decrypted = await resolveClientStateEntries(
|
|
6952
|
-
rows,
|
|
6953
|
-
() => db.getClientState(userId, chunkNamespaceFor(namespace)),
|
|
7152
|
+
key === null ? rows : rows.filter((row) => row.key === key),
|
|
7153
|
+
() => db.getClientState(userId, chunkNamespaceFor(namespace), key === null ? void 0 : { keyPrefix: chunkKeyPrefixFor(key) }),
|
|
6954
7154
|
(value) => decryptFromStorage(value, userKey)
|
|
6955
7155
|
);
|
|
6956
7156
|
const encryptedResponse = await encryptPayload({ namespace, entries: decrypted }, userKey);
|
|
@@ -7028,7 +7228,7 @@ function createClientStateNamespacesHandler(ctx) {
|
|
|
7028
7228
|
}
|
|
7029
7229
|
|
|
7030
7230
|
// src/server/lib/version.js
|
|
7031
|
-
var SERVER_VERSION = true ? "3.0.
|
|
7231
|
+
var SERVER_VERSION = true ? "3.1.0-next.1" : "0.0.0-dev";
|
|
7032
7232
|
|
|
7033
7233
|
// src/server/handlers/capabilities.js
|
|
7034
7234
|
var SERVER_FEATURES = Object.freeze([
|
|
@@ -7144,7 +7344,12 @@ var SERVER_FEATURES = Object.freeze([
|
|
|
7144
7344
|
"max-generation-retries",
|
|
7145
7345
|
// onBeforeFire 认 { defer: { afterMs } }:这次不生成,过一会儿再来问(不占重试
|
|
7146
7346
|
// 次数);onFireSettled 的 status 多一种 'deferred',带 retryAfter。
|
|
7147
|
-
"before-fire-defer"
|
|
7347
|
+
"before-fire-defer",
|
|
7348
|
+
"task-input",
|
|
7349
|
+
"client-state-key",
|
|
7350
|
+
"agentic-list-tasks",
|
|
7351
|
+
"agentic-tool-allowlist",
|
|
7352
|
+
"attachments"
|
|
7148
7353
|
]);
|
|
7149
7354
|
function createCapabilitiesHandler(ctx) {
|
|
7150
7355
|
async function GET(url, headers) {
|
|
@@ -7191,6 +7396,39 @@ function objectOrText(value) {
|
|
|
7191
7396
|
return value;
|
|
7192
7397
|
}
|
|
7193
7398
|
}
|
|
7399
|
+
function utf8ByteLength(text) {
|
|
7400
|
+
if (typeof text !== "string" || text.length === 0) return 0;
|
|
7401
|
+
return /[^\x00-\x7f]/.test(text) ? encoder.encode(text).length : text.length;
|
|
7402
|
+
}
|
|
7403
|
+
function rowByteSize(row) {
|
|
7404
|
+
if (typeof row.byte_size === "number") return row.byte_size;
|
|
7405
|
+
return utf8ByteLength(
|
|
7406
|
+
String(
|
|
7407
|
+
row.value ?? row.encrypted_payload ?? row.encrypted_value ?? row.payload ?? row.subscription ?? ""
|
|
7408
|
+
)
|
|
7409
|
+
);
|
|
7410
|
+
}
|
|
7411
|
+
var STORAGE_PREFIX_RE = /^[0-9a-f]{32}:[0-9a-f]{24,32}:[0-9a-f]*$/i;
|
|
7412
|
+
var HEX_RE = /^[0-9a-f]*$/i;
|
|
7413
|
+
function isValidStorageCiphertext(fullText, prefix, suffix, byteSize) {
|
|
7414
|
+
if (typeof fullText === "string") {
|
|
7415
|
+
if (!STORAGE_PREFIX_RE.test(fullText)) return false;
|
|
7416
|
+
const secondColon = fullText.indexOf(":", 33);
|
|
7417
|
+
return secondColon > 32 && (fullText.length - secondColon - 1) % 2 === 0;
|
|
7418
|
+
}
|
|
7419
|
+
if (typeof prefix === "string" && typeof byteSize === "number") {
|
|
7420
|
+
if (!STORAGE_PREFIX_RE.test(prefix)) return false;
|
|
7421
|
+
if (typeof suffix === "string" && !HEX_RE.test(suffix)) return false;
|
|
7422
|
+
const secondColon = prefix.indexOf(":", 33);
|
|
7423
|
+
return secondColon > 32 && (byteSize - secondColon - 1) % 2 === 0;
|
|
7424
|
+
}
|
|
7425
|
+
return false;
|
|
7426
|
+
}
|
|
7427
|
+
function hasCompleteStoredMetadata(stored) {
|
|
7428
|
+
return Boolean(
|
|
7429
|
+
stored && safeOwner(stored.owner) !== null && typeof stored.kind === "string" && stored.kind.length > 0
|
|
7430
|
+
);
|
|
7431
|
+
}
|
|
7194
7432
|
async function buildCloudDataInventory({
|
|
7195
7433
|
db,
|
|
7196
7434
|
userId,
|
|
@@ -7203,6 +7441,20 @@ async function buildCloudDataInventory({
|
|
|
7203
7441
|
const metadata = new Map(
|
|
7204
7442
|
(raw.metadata || []).map((row) => [row.resource_key, row.encrypted_value])
|
|
7205
7443
|
);
|
|
7444
|
+
const readStoredMetadata = async (metadataKey) => {
|
|
7445
|
+
const encryptedMetadata = metadata.get(metadataKey) || null;
|
|
7446
|
+
if (!encryptedMetadata) {
|
|
7447
|
+
return { encryptedMetadata: null, stored: null, unreadable: false };
|
|
7448
|
+
}
|
|
7449
|
+
try {
|
|
7450
|
+
const stored = JSON.parse(
|
|
7451
|
+
await decryptFromStorage(encryptedMetadata, userKey)
|
|
7452
|
+
);
|
|
7453
|
+
return { encryptedMetadata, stored, unreadable: false };
|
|
7454
|
+
} catch {
|
|
7455
|
+
return { encryptedMetadata, stored: null, unreadable: true };
|
|
7456
|
+
}
|
|
7457
|
+
};
|
|
7206
7458
|
const taskPayloads = /* @__PURE__ */ new Map();
|
|
7207
7459
|
const taskByUuid = /* @__PURE__ */ new Map();
|
|
7208
7460
|
for (const row of raw.task || []) {
|
|
@@ -7227,23 +7479,19 @@ async function buildCloudDataInventory({
|
|
|
7227
7479
|
status = null,
|
|
7228
7480
|
namespace,
|
|
7229
7481
|
key,
|
|
7230
|
-
credId
|
|
7482
|
+
credId,
|
|
7483
|
+
metadataInfo
|
|
7231
7484
|
}) => {
|
|
7232
7485
|
const metadataKey = JSON.stringify([type, ...identity]);
|
|
7233
7486
|
const id = await cloudDataHash(JSON.stringify([userId, metadataKey]));
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
);
|
|
7241
|
-
} catch {
|
|
7242
|
-
unreadable = true;
|
|
7243
|
-
}
|
|
7244
|
-
}
|
|
7487
|
+
const {
|
|
7488
|
+
encryptedMetadata,
|
|
7489
|
+
stored,
|
|
7490
|
+
unreadable: metadataUnreadable
|
|
7491
|
+
} = metadataInfo ?? await readStoredMetadata(metadataKey);
|
|
7492
|
+
if (metadataUnreadable) unreadable = true;
|
|
7245
7493
|
let resolved = null;
|
|
7246
|
-
if (resolveOwner) {
|
|
7494
|
+
if (resolveOwner && !hasCompleteStoredMetadata(stored)) {
|
|
7247
7495
|
try {
|
|
7248
7496
|
resolved = await resolveOwner({
|
|
7249
7497
|
type,
|
|
@@ -7266,14 +7514,7 @@ async function buildCloudDataInventory({
|
|
|
7266
7514
|
const owner = safeOwner(stored?.owner) || safeOwner(payload?.__cloudOwnerGuard?.owner) || safeOwner(payload?.metadata?.cloudOwner) || safeOwner(payload?.metadata?.owner) || safeOwner(payload?.owner) || safeOwner(task?.__cloudOwnerGuard?.owner) || safeOwner(task?.metadata?.cloudOwner) || safeOwner(task?.metadata?.owner) || safeOwner(task?.owner) || safeOwner(resolved?.owner);
|
|
7267
7515
|
const kind = stored?.kind ?? payload?.metadata?.kind ?? payload?.kind ?? resolved?.kind ?? null;
|
|
7268
7516
|
const times = rows.map((row) => timestamp(row.updated_at ?? row.created_at)).filter((time) => time !== null);
|
|
7269
|
-
const byteSize = rows.reduce(
|
|
7270
|
-
(sum, row) => sum + encoder.encode(
|
|
7271
|
-
String(
|
|
7272
|
-
row.value ?? row.encrypted_payload ?? row.encrypted_value ?? row.payload ?? row.subscription ?? ""
|
|
7273
|
-
)
|
|
7274
|
-
).length,
|
|
7275
|
-
0
|
|
7276
|
-
);
|
|
7517
|
+
const byteSize = rows.reduce((sum, row) => sum + rowByteSize(row), 0);
|
|
7277
7518
|
const label = type === "state" ? `${namespace}/${key}` : type === "credential" ? credId : type === "task" ? rows[0].uuid || `task row ${rows[0].id}` : type === "outbox" ? rows[0].message_id : "Web Push subscription";
|
|
7278
7519
|
entries.push({
|
|
7279
7520
|
resource: {
|
|
@@ -7328,22 +7569,75 @@ async function buildCloudDataInventory({
|
|
|
7328
7569
|
const slices = chunks.get(group) || [];
|
|
7329
7570
|
chunks.delete(group);
|
|
7330
7571
|
const rows = [row, ...slices.sort((a, b) => a.key.localeCompare(b.key))];
|
|
7331
|
-
|
|
7572
|
+
const metadataKey = JSON.stringify(["state", row.namespace, row.key]);
|
|
7573
|
+
const metadataInfo = await readStoredMetadata(metadataKey);
|
|
7574
|
+
const skipFullDecrypt = !metadataInfo.unreadable && hasCompleteStoredMetadata(metadataInfo.stored);
|
|
7575
|
+
let value, unreadable = metadataInfo.unreadable;
|
|
7332
7576
|
try {
|
|
7333
|
-
const
|
|
7334
|
-
|
|
7335
|
-
|
|
7577
|
+
const rootMarker = row.value ?? row.value_prefix ?? "";
|
|
7578
|
+
const count = parseChunkedRootCount(rootMarker);
|
|
7579
|
+
if (count === null) {
|
|
7580
|
+
if (typeof row.value === "string" && (!skipFullDecrypt || row.value.length <= 1024)) {
|
|
7581
|
+
value = await decryptFromStorage(row.value, userKey);
|
|
7582
|
+
} else if (skipFullDecrypt) {
|
|
7583
|
+
if (!isValidStorageCiphertext(
|
|
7584
|
+
row.value,
|
|
7585
|
+
row.value_prefix,
|
|
7586
|
+
row.value_suffix,
|
|
7587
|
+
row.byte_size
|
|
7588
|
+
)) {
|
|
7589
|
+
throw new Error("Invalid state ciphertext");
|
|
7590
|
+
}
|
|
7591
|
+
} else {
|
|
7592
|
+
const fetched = typeof db.getClientState === "function" ? await db.getClientState(userId, row.namespace, { key: row.key }) : [];
|
|
7593
|
+
const rawValue = fetched?.[0]?.value;
|
|
7594
|
+
if (typeof rawValue !== "string") {
|
|
7595
|
+
throw new Error("Missing state value");
|
|
7596
|
+
}
|
|
7597
|
+
value = await decryptFromStorage(rawValue, userKey);
|
|
7598
|
+
}
|
|
7599
|
+
} else {
|
|
7336
7600
|
if (count > slices.length) throw new Error("Missing state chunks");
|
|
7337
|
-
const
|
|
7601
|
+
const sliceByKey = new Map(
|
|
7602
|
+
slices.map((candidate) => [candidate.key, candidate])
|
|
7603
|
+
);
|
|
7604
|
+
const orderedSlices = [];
|
|
7338
7605
|
for (let i = 0; i < count; i++) {
|
|
7339
|
-
const slice =
|
|
7340
|
-
(candidate) => candidate.key === chunkKeyFor(row.key, i)
|
|
7341
|
-
);
|
|
7606
|
+
const slice = sliceByKey.get(chunkKeyFor(row.key, i));
|
|
7342
7607
|
if (!slice || slice.updated_at !== row.updated_at)
|
|
7343
7608
|
throw new Error("Mismatched state chunks");
|
|
7344
|
-
|
|
7609
|
+
if (!isValidStorageCiphertext(
|
|
7610
|
+
slice.value,
|
|
7611
|
+
slice.value_prefix,
|
|
7612
|
+
slice.value_suffix,
|
|
7613
|
+
slice.byte_size
|
|
7614
|
+
)) {
|
|
7615
|
+
throw new Error("Corrupt state chunk");
|
|
7616
|
+
}
|
|
7617
|
+
orderedSlices.push(slice);
|
|
7618
|
+
}
|
|
7619
|
+
if (!skipFullDecrypt) {
|
|
7620
|
+
let fetchedByKey = null;
|
|
7621
|
+
if (orderedSlices.some((slice) => typeof slice.value !== "string")) {
|
|
7622
|
+
const fetchedSlices = typeof db.getClientState === "function" ? await db.getClientState(
|
|
7623
|
+
userId,
|
|
7624
|
+
CHUNK_NAMESPACE_PREFIX + row.namespace,
|
|
7625
|
+
{ keyPrefix: `${row.key}` }
|
|
7626
|
+
) : [];
|
|
7627
|
+
fetchedByKey = new Map(
|
|
7628
|
+
fetchedSlices.map((candidate) => [candidate.key, candidate.value])
|
|
7629
|
+
);
|
|
7630
|
+
}
|
|
7631
|
+
const parts = [];
|
|
7632
|
+
for (const slice of orderedSlices) {
|
|
7633
|
+
const rawSliceValue = typeof slice.value === "string" ? slice.value : fetchedByKey?.get(slice.key);
|
|
7634
|
+
if (typeof rawSliceValue !== "string") {
|
|
7635
|
+
throw new Error("Missing state chunk value");
|
|
7636
|
+
}
|
|
7637
|
+
parts.push(await decryptFromStorage(rawSliceValue, userKey));
|
|
7638
|
+
}
|
|
7639
|
+
value = parts.join("");
|
|
7345
7640
|
}
|
|
7346
|
-
value = parts.join("");
|
|
7347
7641
|
}
|
|
7348
7642
|
} catch {
|
|
7349
7643
|
unreadable = true;
|
|
@@ -7356,7 +7650,8 @@ async function buildCloudDataInventory({
|
|
|
7356
7650
|
rows,
|
|
7357
7651
|
value,
|
|
7358
7652
|
unreadable,
|
|
7359
|
-
status: null
|
|
7653
|
+
status: null,
|
|
7654
|
+
metadataInfo
|
|
7360
7655
|
});
|
|
7361
7656
|
}
|
|
7362
7657
|
for (const [group, rows] of chunks) {
|
|
@@ -7380,9 +7675,29 @@ async function buildCloudDataInventory({
|
|
|
7380
7675
|
});
|
|
7381
7676
|
}
|
|
7382
7677
|
for (const row of raw.outbox || []) {
|
|
7383
|
-
|
|
7678
|
+
const metadataKey = JSON.stringify(["outbox", row.message_id]);
|
|
7679
|
+
const metadataInfo = await readStoredMetadata(metadataKey);
|
|
7680
|
+
const skipFullDecrypt = !metadataInfo.unreadable && hasCompleteStoredMetadata(metadataInfo.stored);
|
|
7681
|
+
let payload, unreadable = metadataInfo.unreadable;
|
|
7384
7682
|
try {
|
|
7385
|
-
payload
|
|
7683
|
+
if (typeof row.payload === "string" && (!skipFullDecrypt || row.payload.length <= 1024)) {
|
|
7684
|
+
payload = objectOrText(await decryptFromStorage(row.payload, userKey));
|
|
7685
|
+
} else if (skipFullDecrypt) {
|
|
7686
|
+
if (!isValidStorageCiphertext(
|
|
7687
|
+
row.payload,
|
|
7688
|
+
row.payload_prefix,
|
|
7689
|
+
row.payload_suffix,
|
|
7690
|
+
row.byte_size
|
|
7691
|
+
)) {
|
|
7692
|
+
throw new Error("Invalid outbox ciphertext");
|
|
7693
|
+
}
|
|
7694
|
+
} else {
|
|
7695
|
+
const rawPayload = typeof db.getOutboxPayload === "function" ? await db.getOutboxPayload(userId, row.message_id) : null;
|
|
7696
|
+
if (typeof rawPayload !== "string") {
|
|
7697
|
+
throw new Error("Missing outbox payload");
|
|
7698
|
+
}
|
|
7699
|
+
payload = objectOrText(await decryptFromStorage(rawPayload, userKey));
|
|
7700
|
+
}
|
|
7386
7701
|
} catch {
|
|
7387
7702
|
unreadable = true;
|
|
7388
7703
|
}
|
|
@@ -7393,7 +7708,8 @@ async function buildCloudDataInventory({
|
|
|
7393
7708
|
payload,
|
|
7394
7709
|
task: taskByUuid.get(row.task_uuid),
|
|
7395
7710
|
unreadable,
|
|
7396
|
-
status: row.acked_at != null ? "acked" : row.delivered_at != null ? "delivered" : "pending"
|
|
7711
|
+
status: row.acked_at != null ? "acked" : row.delivered_at != null ? "delivered" : "pending",
|
|
7712
|
+
metadataInfo
|
|
7397
7713
|
});
|
|
7398
7714
|
}
|
|
7399
7715
|
for (const row of raw.subscription || []) {
|
|
@@ -7516,10 +7832,12 @@ async function makeCleanupPlan(ctx, db, userId, key, input) {
|
|
|
7516
7832
|
"\u4E91\u7AEF\u6E05\u5355\u4E0D\u5B8C\u6574\uFF0C\u6682\u4E0D\u80FD\u751F\u6210\u6E05\u7406\u8BA1\u5212",
|
|
7517
7833
|
409
|
|
7518
7834
|
);
|
|
7835
|
+
const resourceIdSet = resourceIds ? new Set(resourceIds) : null;
|
|
7836
|
+
const typeSet = types ? new Set(types) : null;
|
|
7519
7837
|
const entries = inventory.entries.filter(
|
|
7520
|
-
(e) => (!owner || ownerEquals(e.resource.owner, owner)) && (!
|
|
7838
|
+
(e) => (!owner || ownerEquals(e.resource.owner, owner)) && (!resourceIdSet || resourceIdSet.has(e.resource.id)) && (!typeSet || typeSet.has(e.resource.type))
|
|
7521
7839
|
);
|
|
7522
|
-
if (
|
|
7840
|
+
if (resourceIdSet && resourceIdSet.size !== entries.length)
|
|
7523
7841
|
throw cloudError(
|
|
7524
7842
|
"CLOUD_RESOURCE_NOT_FOUND",
|
|
7525
7843
|
"\u90E8\u5206\u9009\u4E2D\u8D44\u6E90\u4E0D\u5B58\u5728\u6216\u4E0D\u5C5E\u4E8E\u5F53\u524D\u8303\u56F4",
|
|
@@ -7677,6 +7995,7 @@ async function advanceCleanup(ctx, db, userId, key, operation) {
|
|
|
7677
7995
|
if (operation.status === "completed" || operation.status === "failed")
|
|
7678
7996
|
return operation;
|
|
7679
7997
|
operation.status = "running";
|
|
7998
|
+
let retirementInventory = null;
|
|
7680
7999
|
if (operation.mode === "retire-owner") {
|
|
7681
8000
|
if (operation.fencedGeneration == null) {
|
|
7682
8001
|
const state = await db.setCloudOwnerActive(
|
|
@@ -7704,6 +8023,7 @@ async function advanceCleanup(ctx, db, userId, key, operation) {
|
|
|
7704
8023
|
"\u65E0\u6CD5\u786E\u8BA4\u5F85\u6E05\u7406\u8D44\u6E90",
|
|
7705
8024
|
503
|
|
7706
8025
|
);
|
|
8026
|
+
retirementInventory = inventory;
|
|
7707
8027
|
const known = new Set(operation.knownResourceIds || []);
|
|
7708
8028
|
operation.knownResourceVersions ||= {};
|
|
7709
8029
|
operation.entries = inventory.entries.filter((entry) => {
|
|
@@ -7730,7 +8050,7 @@ async function advanceCleanup(ctx, db, userId, key, operation) {
|
|
|
7730
8050
|
).length;
|
|
7731
8051
|
await save(db, userId, key, "operation", operation, { leaseToken });
|
|
7732
8052
|
}
|
|
7733
|
-
const beforeBatch = await cloudInventory(ctx, db, userId, key);
|
|
8053
|
+
const beforeBatch = retirementInventory ?? await cloudInventory(ctx, db, userId, key);
|
|
7734
8054
|
if (!beforeBatch.complete)
|
|
7735
8055
|
throw cloudError("CLOUD_INVENTORY_INCOMPLETE", "\u65E0\u6CD5\u786E\u8BA4\u5F85\u6E05\u7406\u8D44\u6E90", 503);
|
|
7736
8056
|
const currentById = new Map(
|
|
@@ -7768,7 +8088,6 @@ async function advanceCleanup(ctx, db, userId, key, operation) {
|
|
|
7768
8088
|
delete entry.locator;
|
|
7769
8089
|
operation.cursor++;
|
|
7770
8090
|
operation.updatedAt = Date.now();
|
|
7771
|
-
await save(db, userId, key, "operation", operation, { leaseToken });
|
|
7772
8091
|
}
|
|
7773
8092
|
await renewLease();
|
|
7774
8093
|
const current = await cloudInventory(ctx, db, userId, key);
|
|
@@ -7779,8 +8098,9 @@ async function advanceCleanup(ctx, db, userId, key, operation) {
|
|
|
7779
8098
|
503
|
|
7780
8099
|
);
|
|
7781
8100
|
const selected = new Set(operation.entries.map((e) => e.resource.id));
|
|
8101
|
+
const knownIds = new Set(operation.knownResourceIds || []);
|
|
7782
8102
|
const remaining = current.entries.filter(
|
|
7783
|
-
(e) => operation.mode === "retire-owner" ? ownerEquals(e.resource.owner, operation.owner) ||
|
|
8103
|
+
(e) => operation.mode === "retire-owner" ? ownerEquals(e.resource.owner, operation.owner) || knownIds.has(e.resource.id) : selected.has(e.resource.id)
|
|
7784
8104
|
);
|
|
7785
8105
|
for (const count of operation.counts)
|
|
7786
8106
|
count.remaining = remaining.filter(
|
|
@@ -8718,6 +9038,7 @@ export {
|
|
|
8718
9038
|
timingSafeEqualBytes,
|
|
8719
9039
|
deriveUserEncryptionKey,
|
|
8720
9040
|
decryptPayload,
|
|
9041
|
+
encryptPayload,
|
|
8721
9042
|
encryptForStorage,
|
|
8722
9043
|
decryptFromStorage,
|
|
8723
9044
|
CRED_ID_MAX_LENGTH,
|