@rei-standard/amsg-server 2.6.0-next.8 → 2.6.0-next.9
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 +45 -0
- package/dist/{chunk-HQHRXSQA.cjs → chunk-3QRVP4JE.cjs} +127 -3
- package/dist/{chunk-LEE5KQ7Y.mjs → chunk-S6O3MXQO.mjs} +127 -3
- package/dist/{cloudflare-CH3dBl5b.d.cts → cloudflare-kUY6z-k5.d.cts} +234 -9
- package/dist/{cloudflare-CH3dBl5b.d.ts → cloudflare-kUY6z-k5.d.ts} +234 -9
- package/dist/cloudflare.cjs +2 -2
- package/dist/cloudflare.d.cts +1 -1
- package/dist/cloudflare.d.ts +1 -1
- package/dist/cloudflare.mjs +1 -1
- package/dist/index.cjs +11 -11
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -121,6 +121,51 @@ const { bytes, remainingBytes, withinLimit } = measurePushPayload(JSON.stringify
|
|
|
121
121
|
|
|
122
122
|
装不下的内容(长文、附件详情)建议走旁路:正文存进 `client_state`,push 里只带一个引用键,客户端上线后用 `GET /client-state` 取回。单用户 Worker 的 fire-time hook 用 `ctx.writeState()` 写,见 [`examples/cloudflare-single-user/README.md`](https://github.com/Tosd0/ReiStandard/blob/main/packages/rei-standard-amsg/server/examples/cloudflare-single-user/README.md)。
|
|
123
123
|
|
|
124
|
+
## Fire 时刻 hooks
|
|
125
|
+
|
|
126
|
+
配上 `hooks: { onBeforeFire, onLLMOutput, executeToolCalls }` 之后,AI 类任务的 prompt 不再是排程那一刻冻结的文本,而是 cron 触发时现场组装,工具也在服务端就地跑完,全程不需要客户端在线。完整用法见 [`examples/cloudflare-single-user/README.md`](https://github.com/Tosd0/ReiStandard/blob/main/packages/rei-standard-amsg/server/examples/cloudflare-single-user/README.md) 的「Fire 时刻 hooks」。
|
|
127
|
+
|
|
128
|
+
三个 hook 拿到的 ctx 上都有这几个口子:
|
|
129
|
+
|
|
130
|
+
| ctx 上的口子 | 干什么 |
|
|
131
|
+
|---|---|
|
|
132
|
+
| `readState(ns)` / `writeState(ns, entries)` | 读写 `client_state`,和客户端 `GET/PUT /client-state` 是同一份数据 |
|
|
133
|
+
| `scheduleTask(options)` | 给同一个用户再建一条定时任务 |
|
|
134
|
+
| `scratch` | 本次 fire 的便签对象,三个 hook 共享同一个引用,fire 结束即丢弃 |
|
|
135
|
+
|
|
136
|
+
### `ctx.scheduleTask(options)`
|
|
137
|
+
|
|
138
|
+
角色在这次 fire 里给自己排一条后续任务:「这条发完,一个半小时后我再接着说一句」。建出来的是一条正常的任务行,到点由 cron 触发,用户全程离线也不影响。
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
const result = await ctx.scheduleTask({
|
|
142
|
+
firstSendTime: new Date(Date.now() + 90 * 60_000).toISOString(), // 必填,ISO 字符串
|
|
143
|
+
messageType: 'auto', // 可选,默认继承当前任务
|
|
144
|
+
recurrenceType: 'none', // 可选,默认 none
|
|
145
|
+
metadata: { beat: 'followup' }, // 可选,整体替换当前任务的 metadata(不深合并)
|
|
146
|
+
uuid: `fire-${ctx.task.id}-${ctx.task.nextSendAt}`, // 可选,默认随机
|
|
147
|
+
});
|
|
148
|
+
// → { created: true, id, uuid, nextSendAt }
|
|
149
|
+
// 或 { created: false, reason: 'duplicate', uuid }
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
凭据和投递配置(`pushSubscription` / `apiUrl` / `apiKey` / `primaryModel` / `maxTokens` / `temperature` / `splitPattern`)以及 `contactName` / `avatarUrl` / `messageSubtype` / `userMessage` 从当前任务继承,宿主只说「什么时候、说什么方向」——hook 全程看不到凭据。`completePrompt` / `messages` 不继承(都置 `null`):hook 每次现场重组 prompt,把排程时冻结的旧 prompt 带过去,新任务万一走回冻结 prompt 老链路就会静默发出一条谁也没打算发的文案。
|
|
153
|
+
|
|
154
|
+
护栏:
|
|
155
|
+
|
|
156
|
+
| 护栏 | 阈值 / 规则 | 不满足时 | 为什么 |
|
|
157
|
+
|---|---|---|---|
|
|
158
|
+
| `firstSendTime` | 必填、能解析成合法时间、至少比现在晚 **60 秒** | `RangeError` | cron 一分钟一跳,排在 60 秒内等于让下一跳立刻捡走,容易变成自己触发自己的紧密循环 |
|
|
159
|
+
| `messageType` | 只收 `auto` / `prompted` / `fixed` | `TypeError` | `instant` 的语义是「建行的那一刻就投递」,那条路径归 `POST /schedule-message` 管;从 fire 里造这么一行,投递时机反而说不清 |
|
|
160
|
+
| `messageType: 'fixed'` | 必须有 `userMessage`(自己传或继承到) | `TypeError` | 固定文本任务没有正文,就是一条永远发空的任务 |
|
|
161
|
+
| 单次 fire 的建任务条数 | 默认 **2 条**,factory 配置 `maxScheduledTasksPerFire` 可调(`0` = 不许自排) | `RangeError` | 模型自排后续本质上是条能无限延伸的链,没有上限就没人按停止键 |
|
|
162
|
+
| `uuid` 撞车 | 不当错误处理 | 返回 `{ created: false, reason: 'duplicate', uuid }` | fire 失败会整条重跑,宿主传一个由「任务 id + 触发时刻」推出来的确定性 uuid 就天然幂等 |
|
|
163
|
+
| 数据库适配器没有 `createTask` | — | 抛 `AGENTIC_SCHEDULE_UNSUPPORTED` | 静默成功会让宿主以为后续那条排上了,其实谁也不会触发它 |
|
|
164
|
+
|
|
165
|
+
`recurrenceType` 沿用排程接口那套 `none` / `daily` / `weekly`,别的值抛 `TypeError`。参数不合法的调用不占建任务额度;uuid 撞车占(那条任务其实已经建出来了)。
|
|
166
|
+
|
|
167
|
+
`GET /capabilities` 的 features 里有 `agentic-schedule-task`,前端可以据此判断部署的 worker 认不认这条链路。
|
|
168
|
+
|
|
124
169
|
## 导出(新增)
|
|
125
170
|
|
|
126
171
|
- `validateLlmMessagesArray(messages)` — 同步预校验 messages 数组,返回 `string | null`(错误信息 / 通过)。和 `@rei-standard/amsg-instant` 的校验规则字节级一致。
|
|
@@ -641,6 +641,10 @@ async function writeClientStateEntries({ db, userId, userKey, entries }) {
|
|
|
641
641
|
// src/server/lib/agentic-fire.js
|
|
642
642
|
var DEFAULT_MAX_TOOL_ITERATIONS = 5;
|
|
643
643
|
var DEFAULT_TOTAL_TIMEOUT_MS = 24e4;
|
|
644
|
+
var DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE = 2;
|
|
645
|
+
var MIN_SCHEDULE_LEAD_MS = 6e4;
|
|
646
|
+
var SCHEDULABLE_MESSAGE_TYPES = /* @__PURE__ */ new Set(["auto", "prompted", "fixed"]);
|
|
647
|
+
var SCHEDULABLE_RECURRENCE_TYPES = /* @__PURE__ */ new Set(["none", "daily", "weekly"]);
|
|
644
648
|
var SLEEP_BETWEEN_MESSAGES_MS = 1500;
|
|
645
649
|
var CREDENTIAL_PAYLOAD_KEYS = /* @__PURE__ */ new Set(["apiKey", "pushSubscription"]);
|
|
646
650
|
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -674,6 +678,9 @@ function normalizeBeforeFireResult(result) {
|
|
|
674
678
|
messages: result.messages,
|
|
675
679
|
maxToolIterations: result.maxToolIterations,
|
|
676
680
|
totalTimeoutMs: result.totalTimeoutMs,
|
|
681
|
+
// 归一成「有就是非空数组,否则 undefined」,让下面每轮的透传判断能直接
|
|
682
|
+
// 用真值。真正决定请求体带不带 tools 的规则住在 llm.js 的
|
|
683
|
+
// buildAiRequestBody(那边有测试钉着),这层只是不把空数组往下传。
|
|
677
684
|
tools: Array.isArray(result.tools) && result.tools.length > 0 ? result.tools : void 0,
|
|
678
685
|
toolChoice: result.toolChoice
|
|
679
686
|
};
|
|
@@ -765,12 +772,123 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
765
772
|
});
|
|
766
773
|
return writeClientStateEntries({ db: ctx.db, userId: task.user_id, userKey, entries: normalized2 });
|
|
767
774
|
};
|
|
775
|
+
const maxScheduledTasksPerFire = Number.isInteger(ctx.maxScheduledTasksPerFire) && ctx.maxScheduledTasksPerFire >= 0 ? ctx.maxScheduledTasksPerFire : DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE;
|
|
776
|
+
let scheduledTaskCount = 0;
|
|
777
|
+
const scheduleTask = async (options) => {
|
|
778
|
+
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
779
|
+
throw new TypeError("scheduleTask(options) \u9700\u8981\u4E00\u4E2A\u5BF9\u8C61\uFF0C\u81F3\u5C11\u5305\u542B { firstSendTime }");
|
|
780
|
+
}
|
|
781
|
+
if (typeof options.firstSendTime !== "string" || !options.firstSendTime.trim()) {
|
|
782
|
+
throw new RangeError("scheduleTask: firstSendTime \u5FC5\u586B\uFF0C\u4E14\u5FC5\u987B\u662F ISO 8601 \u5B57\u7B26\u4E32");
|
|
783
|
+
}
|
|
784
|
+
const firstSendAt = new Date(options.firstSendTime);
|
|
785
|
+
if (Number.isNaN(firstSendAt.getTime())) {
|
|
786
|
+
throw new RangeError(`scheduleTask: firstSendTime \u89E3\u6790\u4E0D\u51FA\u5408\u6CD5\u65F6\u95F4\uFF1A${options.firstSendTime}`);
|
|
787
|
+
}
|
|
788
|
+
const earliest = nowFn() + MIN_SCHEDULE_LEAD_MS;
|
|
789
|
+
if (firstSendAt.getTime() < earliest) {
|
|
790
|
+
throw new RangeError(
|
|
791
|
+
`scheduleTask: firstSendTime \u81F3\u5C11\u8981\u6BD4\u73B0\u5728\u665A ${MIN_SCHEDULE_LEAD_MS / 1e3} \u79D2\uFF08\u6700\u65E9\u53EF\u6392 ${new Date(earliest).toISOString()}\uFF0C\u6536\u5230 ${firstSendAt.toISOString()}\uFF09\u2014\u2014cron \u4E00\u5206\u949F\u4E00\u8DF3\uFF0C\u6392\u5F97\u66F4\u8FD1\u7B49\u4E8E\u8BA9\u4E0B\u4E00\u8DF3\u7ACB\u523B\u6361\u8D70`
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
const nextSendAt = firstSendAt.toISOString();
|
|
795
|
+
const inheritedType = options.messageType == null;
|
|
796
|
+
const messageType = inheritedType ? decryptedPayload.messageType : options.messageType;
|
|
797
|
+
if (messageType === "instant") {
|
|
798
|
+
throw new TypeError(
|
|
799
|
+
"scheduleTask: messageType \u4E0D\u80FD\u662F 'instant'\u2014\u2014instant \u7684\u8BED\u4E49\u662F\u300C\u5EFA\u884C\u7684\u90A3\u4E00\u523B\u5C31\u6295\u9012\u300D\uFF0C\u90A3\u6761\u8DEF\u5F84\u5F52 POST /schedule-message \u7BA1\uFF1B\u4ECE fire \u91CC\u5EFA\u4E00\u6761 instant\uFF0C\u6295\u9012\u65F6\u673A\u53CD\u800C\u8BF4\u4E0D\u6E05\u3002" + (inheritedType ? "\uFF08\u8FD9\u4E2A instant \u662F\u4ECE\u5F53\u524D\u4EFB\u52A1\u7EE7\u627F\u6765\u7684\uFF0C\u663E\u5F0F\u4F20 auto / prompted / fixed \u8986\u76D6\u5B83\u3002\uFF09" : "")
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
if (!SCHEDULABLE_MESSAGE_TYPES.has(messageType)) {
|
|
803
|
+
throw new TypeError(
|
|
804
|
+
`scheduleTask: messageType \u53EA\u80FD\u662F auto / prompted / fixed\uFF0C\u6536\u5230 ${JSON.stringify(messageType)}`
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
const recurrenceType = options.recurrenceType == null ? "none" : options.recurrenceType;
|
|
808
|
+
if (!SCHEDULABLE_RECURRENCE_TYPES.has(recurrenceType)) {
|
|
809
|
+
throw new TypeError(
|
|
810
|
+
`scheduleTask: recurrenceType \u53EA\u80FD\u662F none / daily / weekly\uFF0C\u6536\u5230 ${JSON.stringify(recurrenceType)}`
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
const contactName = options.contactName === void 0 ? decryptedPayload.contactName : options.contactName;
|
|
814
|
+
if (typeof contactName !== "string" || !contactName.trim()) {
|
|
815
|
+
throw new TypeError("scheduleTask: contactName \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\uFF08\u9ED8\u8BA4\u7EE7\u627F\u5F53\u524D\u4EFB\u52A1\uFF09");
|
|
816
|
+
}
|
|
817
|
+
const userMessage = options.userMessage === void 0 ? _nullishCoalesce(decryptedPayload.userMessage, () => ( null)) : options.userMessage;
|
|
818
|
+
if (messageType === "fixed" && (typeof userMessage !== "string" || !userMessage.trim())) {
|
|
819
|
+
throw new TypeError(
|
|
820
|
+
"scheduleTask: messageType 'fixed' \u5FC5\u987B\u6709 userMessage\uFF08\u81EA\u5DF1\u4F20\uFF0C\u6216\u4ECE\u5F53\u524D\u4EFB\u52A1\u7EE7\u627F\u5230\uFF09\u2014\u2014\u56FA\u5B9A\u6587\u672C\u4EFB\u52A1\u6CA1\u6709\u6B63\u6587\uFF0C\u5C31\u662F\u4E00\u6761\u6C38\u8FDC\u53D1\u7A7A\u7684\u4EFB\u52A1"
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
const metadata = options.metadata === void 0 ? decryptedPayload.metadata || {} : options.metadata;
|
|
824
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
825
|
+
throw new TypeError("scheduleTask: metadata \u5FC5\u987B\u662F\u666E\u901A\u5BF9\u8C61\uFF08\u6574\u4F53\u66FF\u6362\u5F53\u524D\u4EFB\u52A1\u7684 metadata\uFF0C\u4E0D\u505A\u6DF1\u5408\u5E76\uFF09");
|
|
826
|
+
}
|
|
827
|
+
const uuid = options.uuid == null ? randomUUID() : options.uuid;
|
|
828
|
+
if (typeof uuid !== "string" || !uuid.trim()) {
|
|
829
|
+
throw new TypeError("scheduleTask: uuid \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
|
|
830
|
+
}
|
|
831
|
+
if (scheduledTaskCount >= maxScheduledTasksPerFire) {
|
|
832
|
+
throw new RangeError(
|
|
833
|
+
`scheduleTask: \u5355\u6B21 fire \u6700\u591A\u5EFA ${maxScheduledTasksPerFire} \u6761\u4EFB\u52A1\uFF08factory \u914D\u7F6E maxScheduledTasksPerFire \u53EF\u8C03\uFF09\uFF0C\u8FD9\u662F\u7B2C ${scheduledTaskCount + 1} \u6761`
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
if (!ctx.db || typeof ctx.db.createTask !== "function") {
|
|
837
|
+
throw new Error("AGENTIC_SCHEDULE_UNSUPPORTED: \u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301\u5EFA\u4EFB\u52A1\uFF08\u7F3A createTask\uFF09");
|
|
838
|
+
}
|
|
839
|
+
scheduledTaskCount++;
|
|
840
|
+
const fullTaskData = {
|
|
841
|
+
contactName,
|
|
842
|
+
avatarUrl: options.avatarUrl === void 0 ? decryptedPayload.avatarUrl || null : options.avatarUrl || null,
|
|
843
|
+
messageType,
|
|
844
|
+
messageSubtype: (options.messageSubtype === void 0 ? decryptedPayload.messageSubtype : options.messageSubtype) || "chat",
|
|
845
|
+
userMessage: userMessage || null,
|
|
846
|
+
firstSendTime: nextSendAt,
|
|
847
|
+
recurrenceType,
|
|
848
|
+
apiUrl: decryptedPayload.apiUrl || null,
|
|
849
|
+
apiKey: decryptedPayload.apiKey || null,
|
|
850
|
+
primaryModel: decryptedPayload.primaryModel || null,
|
|
851
|
+
// 见文件头:fire-time hook 每次现场重组 prompt,旧 prompt 带过去只会在新
|
|
852
|
+
// 任务走回老链路时顶替宿主的意图。
|
|
853
|
+
completePrompt: null,
|
|
854
|
+
messages: null,
|
|
855
|
+
maxTokens: _nullishCoalesce(decryptedPayload.maxTokens, () => ( null)),
|
|
856
|
+
temperature: _nullishCoalesce(decryptedPayload.temperature, () => ( null)),
|
|
857
|
+
splitPattern: _nullishCoalesce(decryptedPayload.splitPattern, () => ( null)),
|
|
858
|
+
pushSubscription: decryptedPayload.pushSubscription,
|
|
859
|
+
metadata
|
|
860
|
+
};
|
|
861
|
+
const encryptedPayload = await encryptForStorage(JSON.stringify(fullTaskData), userKey);
|
|
862
|
+
let created;
|
|
863
|
+
try {
|
|
864
|
+
created = await ctx.db.createTask({
|
|
865
|
+
user_id: task.user_id,
|
|
866
|
+
uuid,
|
|
867
|
+
encrypted_payload: encryptedPayload,
|
|
868
|
+
next_send_at: nextSendAt,
|
|
869
|
+
message_type: messageType
|
|
870
|
+
});
|
|
871
|
+
} catch (error) {
|
|
872
|
+
if (isUniqueViolation(error)) return { created: false, reason: "duplicate", uuid };
|
|
873
|
+
throw error;
|
|
874
|
+
}
|
|
875
|
+
if (!created) {
|
|
876
|
+
throw new Error("AGENTIC_SCHEDULE_FAILED: createTask \u6CA1\u6709\u8FD4\u56DE\u65B0\u5EFA\u7684\u4EFB\u52A1\u884C");
|
|
877
|
+
}
|
|
878
|
+
return {
|
|
879
|
+
created: true,
|
|
880
|
+
id: _nullishCoalesce(created.id, () => ( null)),
|
|
881
|
+
uuid: _nullishCoalesce(created.uuid, () => ( uuid)),
|
|
882
|
+
nextSendAt: _nullishCoalesce(created.next_send_at, () => ( nextSendAt))
|
|
883
|
+
};
|
|
884
|
+
};
|
|
768
885
|
const scratch = {};
|
|
769
886
|
const fireCtx = Object.freeze({
|
|
770
887
|
task: buildHookTask(task, decryptedPayload),
|
|
771
888
|
userId: task.user_id,
|
|
772
889
|
readState,
|
|
773
890
|
writeState,
|
|
891
|
+
scheduleTask,
|
|
774
892
|
now: new Date(nowFn()),
|
|
775
893
|
scratch
|
|
776
894
|
});
|
|
@@ -819,7 +937,8 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
819
937
|
scratch
|
|
820
938
|
}),
|
|
821
939
|
readState,
|
|
822
|
-
writeState
|
|
940
|
+
writeState,
|
|
941
|
+
scheduleTask
|
|
823
942
|
});
|
|
824
943
|
const decision = await hooks.onLLMOutput(sessionCtx);
|
|
825
944
|
_amsgshared.assertValidDecision.call(void 0, decision, { inlineToolCalls: true });
|
|
@@ -2277,7 +2396,7 @@ function createClientStateHandler(ctx) {
|
|
|
2277
2396
|
}
|
|
2278
2397
|
|
|
2279
2398
|
// src/server/lib/version.js
|
|
2280
|
-
var SERVER_VERSION = true ? "2.6.0-next.
|
|
2399
|
+
var SERVER_VERSION = true ? "2.6.0-next.9" : "0.0.0-dev";
|
|
2281
2400
|
|
|
2282
2401
|
// src/server/handlers/capabilities.js
|
|
2283
2402
|
var SERVER_FEATURES = Object.freeze([
|
|
@@ -2288,6 +2407,7 @@ var SERVER_FEATURES = Object.freeze([
|
|
|
2288
2407
|
"agentic-scratch",
|
|
2289
2408
|
"agentic-write-state",
|
|
2290
2409
|
"agentic-fire-tools",
|
|
2410
|
+
"agentic-schedule-task",
|
|
2291
2411
|
"vapid-public-key"
|
|
2292
2412
|
]);
|
|
2293
2413
|
function createCapabilitiesHandler(ctx) {
|
|
@@ -2329,7 +2449,9 @@ function createSingleUserServer(config) {
|
|
|
2329
2449
|
hooks: config.hooks || null,
|
|
2330
2450
|
maxToolIterations: config.maxToolIterations,
|
|
2331
2451
|
totalTimeoutMs: config.totalTimeoutMs,
|
|
2332
|
-
maxStateValueBytes: config.maxStateValueBytes
|
|
2452
|
+
maxStateValueBytes: config.maxStateValueBytes,
|
|
2453
|
+
// hook 的 ctx.scheduleTask() 单次 fire 建任务的条数上限(默认 2)。
|
|
2454
|
+
maxScheduledTasksPerFire: config.maxScheduledTasksPerFire
|
|
2333
2455
|
};
|
|
2334
2456
|
return {
|
|
2335
2457
|
ctx,
|
|
@@ -2668,6 +2790,8 @@ function createSingleUserCloudflareWorker(buildConfig) {
|
|
|
2668
2790
|
totalTimeoutMs: cfg.totalTimeoutMs,
|
|
2669
2791
|
// hook 的 ctx.writeState() 用它判断单条 value 的上限,和 PUT /client-state 同一个配置。
|
|
2670
2792
|
maxStateValueBytes: cfg.maxStateValueBytes,
|
|
2793
|
+
// hook 的 ctx.scheduleTask() 单次 fire 建任务的条数上限(默认 2)。
|
|
2794
|
+
maxScheduledTasksPerFire: cfg.maxScheduledTasksPerFire,
|
|
2671
2795
|
// 任务占位租期(默认 10 分钟,随 totalTimeoutMs 抬高)。
|
|
2672
2796
|
claimLeaseMs: cfg.claimLeaseMs
|
|
2673
2797
|
});
|
|
@@ -641,6 +641,10 @@ async function writeClientStateEntries({ db, userId, userKey, entries }) {
|
|
|
641
641
|
// src/server/lib/agentic-fire.js
|
|
642
642
|
var DEFAULT_MAX_TOOL_ITERATIONS = 5;
|
|
643
643
|
var DEFAULT_TOTAL_TIMEOUT_MS = 24e4;
|
|
644
|
+
var DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE = 2;
|
|
645
|
+
var MIN_SCHEDULE_LEAD_MS = 6e4;
|
|
646
|
+
var SCHEDULABLE_MESSAGE_TYPES = /* @__PURE__ */ new Set(["auto", "prompted", "fixed"]);
|
|
647
|
+
var SCHEDULABLE_RECURRENCE_TYPES = /* @__PURE__ */ new Set(["none", "daily", "weekly"]);
|
|
644
648
|
var SLEEP_BETWEEN_MESSAGES_MS = 1500;
|
|
645
649
|
var CREDENTIAL_PAYLOAD_KEYS = /* @__PURE__ */ new Set(["apiKey", "pushSubscription"]);
|
|
646
650
|
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -674,6 +678,9 @@ function normalizeBeforeFireResult(result) {
|
|
|
674
678
|
messages: result.messages,
|
|
675
679
|
maxToolIterations: result.maxToolIterations,
|
|
676
680
|
totalTimeoutMs: result.totalTimeoutMs,
|
|
681
|
+
// 归一成「有就是非空数组,否则 undefined」,让下面每轮的透传判断能直接
|
|
682
|
+
// 用真值。真正决定请求体带不带 tools 的规则住在 llm.js 的
|
|
683
|
+
// buildAiRequestBody(那边有测试钉着),这层只是不把空数组往下传。
|
|
677
684
|
tools: Array.isArray(result.tools) && result.tools.length > 0 ? result.tools : void 0,
|
|
678
685
|
toolChoice: result.toolChoice
|
|
679
686
|
};
|
|
@@ -765,12 +772,123 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
765
772
|
});
|
|
766
773
|
return writeClientStateEntries({ db: ctx.db, userId: task.user_id, userKey, entries: normalized2 });
|
|
767
774
|
};
|
|
775
|
+
const maxScheduledTasksPerFire = Number.isInteger(ctx.maxScheduledTasksPerFire) && ctx.maxScheduledTasksPerFire >= 0 ? ctx.maxScheduledTasksPerFire : DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE;
|
|
776
|
+
let scheduledTaskCount = 0;
|
|
777
|
+
const scheduleTask = async (options) => {
|
|
778
|
+
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
779
|
+
throw new TypeError("scheduleTask(options) \u9700\u8981\u4E00\u4E2A\u5BF9\u8C61\uFF0C\u81F3\u5C11\u5305\u542B { firstSendTime }");
|
|
780
|
+
}
|
|
781
|
+
if (typeof options.firstSendTime !== "string" || !options.firstSendTime.trim()) {
|
|
782
|
+
throw new RangeError("scheduleTask: firstSendTime \u5FC5\u586B\uFF0C\u4E14\u5FC5\u987B\u662F ISO 8601 \u5B57\u7B26\u4E32");
|
|
783
|
+
}
|
|
784
|
+
const firstSendAt = new Date(options.firstSendTime);
|
|
785
|
+
if (Number.isNaN(firstSendAt.getTime())) {
|
|
786
|
+
throw new RangeError(`scheduleTask: firstSendTime \u89E3\u6790\u4E0D\u51FA\u5408\u6CD5\u65F6\u95F4\uFF1A${options.firstSendTime}`);
|
|
787
|
+
}
|
|
788
|
+
const earliest = nowFn() + MIN_SCHEDULE_LEAD_MS;
|
|
789
|
+
if (firstSendAt.getTime() < earliest) {
|
|
790
|
+
throw new RangeError(
|
|
791
|
+
`scheduleTask: firstSendTime \u81F3\u5C11\u8981\u6BD4\u73B0\u5728\u665A ${MIN_SCHEDULE_LEAD_MS / 1e3} \u79D2\uFF08\u6700\u65E9\u53EF\u6392 ${new Date(earliest).toISOString()}\uFF0C\u6536\u5230 ${firstSendAt.toISOString()}\uFF09\u2014\u2014cron \u4E00\u5206\u949F\u4E00\u8DF3\uFF0C\u6392\u5F97\u66F4\u8FD1\u7B49\u4E8E\u8BA9\u4E0B\u4E00\u8DF3\u7ACB\u523B\u6361\u8D70`
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
const nextSendAt = firstSendAt.toISOString();
|
|
795
|
+
const inheritedType = options.messageType == null;
|
|
796
|
+
const messageType = inheritedType ? decryptedPayload.messageType : options.messageType;
|
|
797
|
+
if (messageType === "instant") {
|
|
798
|
+
throw new TypeError(
|
|
799
|
+
"scheduleTask: messageType \u4E0D\u80FD\u662F 'instant'\u2014\u2014instant \u7684\u8BED\u4E49\u662F\u300C\u5EFA\u884C\u7684\u90A3\u4E00\u523B\u5C31\u6295\u9012\u300D\uFF0C\u90A3\u6761\u8DEF\u5F84\u5F52 POST /schedule-message \u7BA1\uFF1B\u4ECE fire \u91CC\u5EFA\u4E00\u6761 instant\uFF0C\u6295\u9012\u65F6\u673A\u53CD\u800C\u8BF4\u4E0D\u6E05\u3002" + (inheritedType ? "\uFF08\u8FD9\u4E2A instant \u662F\u4ECE\u5F53\u524D\u4EFB\u52A1\u7EE7\u627F\u6765\u7684\uFF0C\u663E\u5F0F\u4F20 auto / prompted / fixed \u8986\u76D6\u5B83\u3002\uFF09" : "")
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
if (!SCHEDULABLE_MESSAGE_TYPES.has(messageType)) {
|
|
803
|
+
throw new TypeError(
|
|
804
|
+
`scheduleTask: messageType \u53EA\u80FD\u662F auto / prompted / fixed\uFF0C\u6536\u5230 ${JSON.stringify(messageType)}`
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
const recurrenceType = options.recurrenceType == null ? "none" : options.recurrenceType;
|
|
808
|
+
if (!SCHEDULABLE_RECURRENCE_TYPES.has(recurrenceType)) {
|
|
809
|
+
throw new TypeError(
|
|
810
|
+
`scheduleTask: recurrenceType \u53EA\u80FD\u662F none / daily / weekly\uFF0C\u6536\u5230 ${JSON.stringify(recurrenceType)}`
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
const contactName = options.contactName === void 0 ? decryptedPayload.contactName : options.contactName;
|
|
814
|
+
if (typeof contactName !== "string" || !contactName.trim()) {
|
|
815
|
+
throw new TypeError("scheduleTask: contactName \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\uFF08\u9ED8\u8BA4\u7EE7\u627F\u5F53\u524D\u4EFB\u52A1\uFF09");
|
|
816
|
+
}
|
|
817
|
+
const userMessage = options.userMessage === void 0 ? decryptedPayload.userMessage ?? null : options.userMessage;
|
|
818
|
+
if (messageType === "fixed" && (typeof userMessage !== "string" || !userMessage.trim())) {
|
|
819
|
+
throw new TypeError(
|
|
820
|
+
"scheduleTask: messageType 'fixed' \u5FC5\u987B\u6709 userMessage\uFF08\u81EA\u5DF1\u4F20\uFF0C\u6216\u4ECE\u5F53\u524D\u4EFB\u52A1\u7EE7\u627F\u5230\uFF09\u2014\u2014\u56FA\u5B9A\u6587\u672C\u4EFB\u52A1\u6CA1\u6709\u6B63\u6587\uFF0C\u5C31\u662F\u4E00\u6761\u6C38\u8FDC\u53D1\u7A7A\u7684\u4EFB\u52A1"
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
const metadata = options.metadata === void 0 ? decryptedPayload.metadata || {} : options.metadata;
|
|
824
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
825
|
+
throw new TypeError("scheduleTask: metadata \u5FC5\u987B\u662F\u666E\u901A\u5BF9\u8C61\uFF08\u6574\u4F53\u66FF\u6362\u5F53\u524D\u4EFB\u52A1\u7684 metadata\uFF0C\u4E0D\u505A\u6DF1\u5408\u5E76\uFF09");
|
|
826
|
+
}
|
|
827
|
+
const uuid = options.uuid == null ? randomUUID() : options.uuid;
|
|
828
|
+
if (typeof uuid !== "string" || !uuid.trim()) {
|
|
829
|
+
throw new TypeError("scheduleTask: uuid \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
|
|
830
|
+
}
|
|
831
|
+
if (scheduledTaskCount >= maxScheduledTasksPerFire) {
|
|
832
|
+
throw new RangeError(
|
|
833
|
+
`scheduleTask: \u5355\u6B21 fire \u6700\u591A\u5EFA ${maxScheduledTasksPerFire} \u6761\u4EFB\u52A1\uFF08factory \u914D\u7F6E maxScheduledTasksPerFire \u53EF\u8C03\uFF09\uFF0C\u8FD9\u662F\u7B2C ${scheduledTaskCount + 1} \u6761`
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
if (!ctx.db || typeof ctx.db.createTask !== "function") {
|
|
837
|
+
throw new Error("AGENTIC_SCHEDULE_UNSUPPORTED: \u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301\u5EFA\u4EFB\u52A1\uFF08\u7F3A createTask\uFF09");
|
|
838
|
+
}
|
|
839
|
+
scheduledTaskCount++;
|
|
840
|
+
const fullTaskData = {
|
|
841
|
+
contactName,
|
|
842
|
+
avatarUrl: options.avatarUrl === void 0 ? decryptedPayload.avatarUrl || null : options.avatarUrl || null,
|
|
843
|
+
messageType,
|
|
844
|
+
messageSubtype: (options.messageSubtype === void 0 ? decryptedPayload.messageSubtype : options.messageSubtype) || "chat",
|
|
845
|
+
userMessage: userMessage || null,
|
|
846
|
+
firstSendTime: nextSendAt,
|
|
847
|
+
recurrenceType,
|
|
848
|
+
apiUrl: decryptedPayload.apiUrl || null,
|
|
849
|
+
apiKey: decryptedPayload.apiKey || null,
|
|
850
|
+
primaryModel: decryptedPayload.primaryModel || null,
|
|
851
|
+
// 见文件头:fire-time hook 每次现场重组 prompt,旧 prompt 带过去只会在新
|
|
852
|
+
// 任务走回老链路时顶替宿主的意图。
|
|
853
|
+
completePrompt: null,
|
|
854
|
+
messages: null,
|
|
855
|
+
maxTokens: decryptedPayload.maxTokens ?? null,
|
|
856
|
+
temperature: decryptedPayload.temperature ?? null,
|
|
857
|
+
splitPattern: decryptedPayload.splitPattern ?? null,
|
|
858
|
+
pushSubscription: decryptedPayload.pushSubscription,
|
|
859
|
+
metadata
|
|
860
|
+
};
|
|
861
|
+
const encryptedPayload = await encryptForStorage(JSON.stringify(fullTaskData), userKey);
|
|
862
|
+
let created;
|
|
863
|
+
try {
|
|
864
|
+
created = await ctx.db.createTask({
|
|
865
|
+
user_id: task.user_id,
|
|
866
|
+
uuid,
|
|
867
|
+
encrypted_payload: encryptedPayload,
|
|
868
|
+
next_send_at: nextSendAt,
|
|
869
|
+
message_type: messageType
|
|
870
|
+
});
|
|
871
|
+
} catch (error) {
|
|
872
|
+
if (isUniqueViolation(error)) return { created: false, reason: "duplicate", uuid };
|
|
873
|
+
throw error;
|
|
874
|
+
}
|
|
875
|
+
if (!created) {
|
|
876
|
+
throw new Error("AGENTIC_SCHEDULE_FAILED: createTask \u6CA1\u6709\u8FD4\u56DE\u65B0\u5EFA\u7684\u4EFB\u52A1\u884C");
|
|
877
|
+
}
|
|
878
|
+
return {
|
|
879
|
+
created: true,
|
|
880
|
+
id: created.id ?? null,
|
|
881
|
+
uuid: created.uuid ?? uuid,
|
|
882
|
+
nextSendAt: created.next_send_at ?? nextSendAt
|
|
883
|
+
};
|
|
884
|
+
};
|
|
768
885
|
const scratch = {};
|
|
769
886
|
const fireCtx = Object.freeze({
|
|
770
887
|
task: buildHookTask(task, decryptedPayload),
|
|
771
888
|
userId: task.user_id,
|
|
772
889
|
readState,
|
|
773
890
|
writeState,
|
|
891
|
+
scheduleTask,
|
|
774
892
|
now: new Date(nowFn()),
|
|
775
893
|
scratch
|
|
776
894
|
});
|
|
@@ -819,7 +937,8 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
819
937
|
scratch
|
|
820
938
|
}),
|
|
821
939
|
readState,
|
|
822
|
-
writeState
|
|
940
|
+
writeState,
|
|
941
|
+
scheduleTask
|
|
823
942
|
});
|
|
824
943
|
const decision = await hooks.onLLMOutput(sessionCtx);
|
|
825
944
|
assertValidDecision(decision, { inlineToolCalls: true });
|
|
@@ -2277,7 +2396,7 @@ function createClientStateHandler(ctx) {
|
|
|
2277
2396
|
}
|
|
2278
2397
|
|
|
2279
2398
|
// src/server/lib/version.js
|
|
2280
|
-
var SERVER_VERSION = true ? "2.6.0-next.
|
|
2399
|
+
var SERVER_VERSION = true ? "2.6.0-next.9" : "0.0.0-dev";
|
|
2281
2400
|
|
|
2282
2401
|
// src/server/handlers/capabilities.js
|
|
2283
2402
|
var SERVER_FEATURES = Object.freeze([
|
|
@@ -2288,6 +2407,7 @@ var SERVER_FEATURES = Object.freeze([
|
|
|
2288
2407
|
"agentic-scratch",
|
|
2289
2408
|
"agentic-write-state",
|
|
2290
2409
|
"agentic-fire-tools",
|
|
2410
|
+
"agentic-schedule-task",
|
|
2291
2411
|
"vapid-public-key"
|
|
2292
2412
|
]);
|
|
2293
2413
|
function createCapabilitiesHandler(ctx) {
|
|
@@ -2329,7 +2449,9 @@ function createSingleUserServer(config) {
|
|
|
2329
2449
|
hooks: config.hooks || null,
|
|
2330
2450
|
maxToolIterations: config.maxToolIterations,
|
|
2331
2451
|
totalTimeoutMs: config.totalTimeoutMs,
|
|
2332
|
-
maxStateValueBytes: config.maxStateValueBytes
|
|
2452
|
+
maxStateValueBytes: config.maxStateValueBytes,
|
|
2453
|
+
// hook 的 ctx.scheduleTask() 单次 fire 建任务的条数上限(默认 2)。
|
|
2454
|
+
maxScheduledTasksPerFire: config.maxScheduledTasksPerFire
|
|
2333
2455
|
};
|
|
2334
2456
|
return {
|
|
2335
2457
|
ctx,
|
|
@@ -2668,6 +2790,8 @@ function createSingleUserCloudflareWorker(buildConfig) {
|
|
|
2668
2790
|
totalTimeoutMs: cfg.totalTimeoutMs,
|
|
2669
2791
|
// hook 的 ctx.writeState() 用它判断单条 value 的上限,和 PUT /client-state 同一个配置。
|
|
2670
2792
|
maxStateValueBytes: cfg.maxStateValueBytes,
|
|
2793
|
+
// hook 的 ctx.scheduleTask() 单次 fire 建任务的条数上限(默认 2)。
|
|
2794
|
+
maxScheduledTasksPerFire: cfg.maxScheduledTasksPerFire,
|
|
2671
2795
|
// 任务占位租期(默认 10 分钟,随 totalTimeoutMs 抬高)。
|
|
2672
2796
|
claimLeaseMs: cfg.claimLeaseMs
|
|
2673
2797
|
});
|
|
@@ -1183,6 +1183,32 @@ async function writeClientStateEntries({ db, userId, userKey, entries }) {
|
|
|
1183
1183
|
* 补完那轮模型仍可能再发起调用。库不看 tools 的内容,执行照旧是
|
|
1184
1184
|
* executeToolCalls 的事。
|
|
1185
1185
|
*
|
|
1186
|
+
* 自排后续任务:fireCtx 和每轮的 sessionCtx 上都挂着 scheduleTask,宿主用它
|
|
1187
|
+
* 在这次 fire 里给同一个用户再建一条定时任务(「这条发完,一个半小时后我再
|
|
1188
|
+
* 接着说一句」)。写口在 sessionCtx 上也给,是因为要不要接着说往往是看完
|
|
1189
|
+
* LLM 输出才定的,那时 onBeforeFire 早已返回。凭据(pushSubscription /
|
|
1190
|
+
* apiKey)和投递配置从当前任务继承、宿主全程看不到(与 buildHookTask 屏蔽
|
|
1191
|
+
* 凭据同一个原则),宿主只提供「什么时候、说什么方向」。
|
|
1192
|
+
*
|
|
1193
|
+
* 新任务不继承 completePrompt / messages,两者都置 null:fire-time hook 每次
|
|
1194
|
+
* 现场重组 prompt,把排程时冻结的旧 prompt 带过去,会在新任务万一走回老链路
|
|
1195
|
+
* (onBeforeFire 返回 null、或那次部署没配 hooks)时静默顶替宿主的意图——
|
|
1196
|
+
* 与其发一条来路不明的旧文案,不如让它当场失败、走既有的重试/标记逻辑。
|
|
1197
|
+
*
|
|
1198
|
+
* 护栏(几条都是这个能力能不能上线的关键):
|
|
1199
|
+
* - firstSendTime 至少比现在晚 MIN_SCHEDULE_LEAD_MS(60 秒)。cron 每分钟
|
|
1200
|
+
* 一跳,排在 60 秒内等于让下一跳立刻捡走,容易变成自己触发自己的紧密循环。
|
|
1201
|
+
* - messageType 只收 auto / prompted / fixed,不收 instant。instant 的语义是
|
|
1202
|
+
* 「建行的那一刻就投递」,那条路径归 POST /schedule-message 管;从 fire 里
|
|
1203
|
+
* 造这么一行,投递时机反而说不清(这里不投,要等 cron 捞到才发)。
|
|
1204
|
+
* - 单次 fire 的建任务上限默认 DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE 条,
|
|
1205
|
+
* factory 配置 ctx.maxScheduledTasksPerFire 可覆盖(0 表示不许自排)。模型
|
|
1206
|
+
* 自排后续本质上是个可以无限延伸的链:没有上限的话,一次 fire 里连排十条、
|
|
1207
|
+
* 或者每条任务都排下一条,就成了谁也没按下停止键的循环。
|
|
1208
|
+
* - uuid 冲突不抛错,返回 { created: false, reason: 'duplicate' }。fire 失败
|
|
1209
|
+
* 会整条重跑(run-tick 的重试语义),宿主传一个由「任务 id + 触发时刻」推
|
|
1210
|
+
* 出来的确定性 uuid 就天然幂等,不会每重试一次多排一条。
|
|
1211
|
+
*
|
|
1186
1212
|
* Budget guards, both factory-level (ctx.maxToolIterations /
|
|
1187
1213
|
* ctx.totalTimeoutMs) and per-fire (onBeforeFire may return
|
|
1188
1214
|
* { messages, maxToolIterations?, totalTimeoutMs? } to override for one
|
|
@@ -1197,6 +1223,17 @@ async function writeClientStateEntries({ db, userId, userKey, entries }) {
|
|
|
1197
1223
|
const DEFAULT_MAX_TOOL_ITERATIONS = 5;
|
|
1198
1224
|
const DEFAULT_TOTAL_TIMEOUT_MS = 240_000;
|
|
1199
1225
|
|
|
1226
|
+
// 一次 fire 里最多能给自己排几条后续任务(ctx.maxScheduledTasksPerFire 可覆盖)。
|
|
1227
|
+
const DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE = 2;
|
|
1228
|
+
// 自排任务的最小提前量:cron 一分钟一跳,比这更近等于让下一跳立刻捡走。
|
|
1229
|
+
const MIN_SCHEDULE_LEAD_MS = 60_000;
|
|
1230
|
+
|
|
1231
|
+
// 自排任务允许的类型。instant 归 POST /schedule-message 那条同步路径管。
|
|
1232
|
+
const SCHEDULABLE_MESSAGE_TYPES = new Set(['auto', 'prompted', 'fixed']);
|
|
1233
|
+
// 与 validateScheduleMessagePayload 同一套;run-tick 只认得这三种,别的值会让
|
|
1234
|
+
// 任务发完之后推进不到下一次。
|
|
1235
|
+
const SCHEDULABLE_RECURRENCE_TYPES = new Set(['none', 'daily', 'weekly']);
|
|
1236
|
+
|
|
1200
1237
|
// Same pacing as the legacy path / amsg-instant.
|
|
1201
1238
|
const SLEEP_BETWEEN_MESSAGES_MS$1 = 1500;
|
|
1202
1239
|
|
|
@@ -1251,6 +1288,9 @@ function normalizeBeforeFireResult(result) {
|
|
|
1251
1288
|
messages: result.messages,
|
|
1252
1289
|
maxToolIterations: result.maxToolIterations,
|
|
1253
1290
|
totalTimeoutMs: result.totalTimeoutMs,
|
|
1291
|
+
// 归一成「有就是非空数组,否则 undefined」,让下面每轮的透传判断能直接
|
|
1292
|
+
// 用真值。真正决定请求体带不带 tools 的规则住在 llm.js 的
|
|
1293
|
+
// buildAiRequestBody(那边有测试钉着),这层只是不把空数组往下传。
|
|
1254
1294
|
tools: Array.isArray(result.tools) && result.tools.length > 0 ? result.tools : undefined,
|
|
1255
1295
|
toolChoice: result.toolChoice,
|
|
1256
1296
|
};
|
|
@@ -1281,7 +1321,7 @@ function firstPositiveNumber(values, fallback) {
|
|
|
1281
1321
|
* @param {import('../adapters/interface.js').TaskRow} args.task
|
|
1282
1322
|
* @param {Object} args.decryptedPayload - decrypted task payload (has credentials; they stop here)
|
|
1283
1323
|
* @param {string} args.userKey - per-user storage key (for readState decryption)
|
|
1284
|
-
* @param {Object} args.ctx - processor ctx ({ db, webpush, vapid, hooks, maxToolIterations, totalTimeoutMs })
|
|
1324
|
+
* @param {Object} args.ctx - processor ctx ({ db, webpush, vapid, hooks, maxToolIterations, totalTimeoutMs, maxScheduledTasksPerFire })
|
|
1285
1325
|
* @returns {Promise<{ handled: false } | { handled: true, result: { success: true, messagesSent: number, status: 'finished'|'skipped', iterations: number } }>}
|
|
1286
1326
|
* `handled: false` → caller falls back to the legacy frozen-prompt path
|
|
1287
1327
|
* (onBeforeFire returned null).
|
|
@@ -1400,6 +1440,174 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
1400
1440
|
return writeClientStateEntries({ db: ctx.db, userId: task.user_id, userKey, entries: normalized });
|
|
1401
1441
|
};
|
|
1402
1442
|
|
|
1443
|
+
const maxScheduledTasksPerFire =
|
|
1444
|
+
Number.isInteger(ctx.maxScheduledTasksPerFire) && ctx.maxScheduledTasksPerFire >= 0
|
|
1445
|
+
? ctx.maxScheduledTasksPerFire
|
|
1446
|
+
: DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE;
|
|
1447
|
+
// 本次 fire 已经用掉的建任务额度。校验通过、真的要写库时才 +1,参数不合法的
|
|
1448
|
+
// 调用不占额度;uuid 撞车(那条任务其实已经建出来了)照样占。
|
|
1449
|
+
let scheduledTaskCount = 0;
|
|
1450
|
+
|
|
1451
|
+
/**
|
|
1452
|
+
* scheduleTask(options) —— 在这次 fire 里给**同一个用户**再建一条定时任务。
|
|
1453
|
+
*
|
|
1454
|
+
* 典型用法:角色发完这条,想过一个半小时再接着说一句。以前只能写进
|
|
1455
|
+
* client_state 等客户端上线重放,用户一直不上线这条链就断了;建成任务行之后,
|
|
1456
|
+
* 到点由 cron 直接触发,跟别的定时消息一样。
|
|
1457
|
+
*
|
|
1458
|
+
* 凭据与投递配置(pushSubscription / apiUrl / apiKey / primaryModel /
|
|
1459
|
+
* maxTokens / temperature / splitPattern)以及 contactName / avatarUrl /
|
|
1460
|
+
* messageSubtype / userMessage 从当前任务继承,宿主只说「什么时候、说什么
|
|
1461
|
+
* 方向」——hook 全程看不到凭据。completePrompt / messages 不继承(都置 null),
|
|
1462
|
+
* 见文件头。
|
|
1463
|
+
*
|
|
1464
|
+
* @param {Object} options
|
|
1465
|
+
* @param {string} options.firstSendTime - 必填,ISO 8601;至少比现在晚 60 秒
|
|
1466
|
+
* @param {'none'|'daily'|'weekly'} [options.recurrenceType='none']
|
|
1467
|
+
* @param {'auto'|'prompted'|'fixed'} [options.messageType] - 默认继承当前任务
|
|
1468
|
+
* @param {Object} [options.metadata] - 整体替换(不是深合并)当前任务的 metadata
|
|
1469
|
+
* @param {string} [options.contactName] - 默认继承
|
|
1470
|
+
* @param {string|null} [options.avatarUrl] - 默认继承
|
|
1471
|
+
* @param {string} [options.messageSubtype] - 默认继承
|
|
1472
|
+
* @param {string|null} [options.userMessage] - 默认继承;messageType 'fixed' 时必须有
|
|
1473
|
+
* @param {string} [options.uuid] - 默认 randomUUID();传确定性 uuid 可做重试幂等
|
|
1474
|
+
* @returns {Promise<{ created: true, id: number|null, uuid: string, nextSendAt: string }
|
|
1475
|
+
* | { created: false, reason: 'duplicate', uuid: string }>}
|
|
1476
|
+
*/
|
|
1477
|
+
const scheduleTask = async (options) => {
|
|
1478
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
1479
|
+
throw new TypeError('scheduleTask(options) 需要一个对象,至少包含 { firstSendTime }');
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
// ---- 触发时刻 ----
|
|
1483
|
+
if (typeof options.firstSendTime !== 'string' || !options.firstSendTime.trim()) {
|
|
1484
|
+
throw new RangeError('scheduleTask: firstSendTime 必填,且必须是 ISO 8601 字符串');
|
|
1485
|
+
}
|
|
1486
|
+
const firstSendAt = new Date(options.firstSendTime);
|
|
1487
|
+
if (Number.isNaN(firstSendAt.getTime())) {
|
|
1488
|
+
throw new RangeError(`scheduleTask: firstSendTime 解析不出合法时间:${options.firstSendTime}`);
|
|
1489
|
+
}
|
|
1490
|
+
const earliest = nowFn() + MIN_SCHEDULE_LEAD_MS;
|
|
1491
|
+
if (firstSendAt.getTime() < earliest) {
|
|
1492
|
+
throw new RangeError(
|
|
1493
|
+
`scheduleTask: firstSendTime 至少要比现在晚 ${MIN_SCHEDULE_LEAD_MS / 1000} 秒` +
|
|
1494
|
+
`(最早可排 ${new Date(earliest).toISOString()},收到 ${firstSendAt.toISOString()})` +
|
|
1495
|
+
'——cron 一分钟一跳,排得更近等于让下一跳立刻捡走'
|
|
1496
|
+
);
|
|
1497
|
+
}
|
|
1498
|
+
const nextSendAt = firstSendAt.toISOString();
|
|
1499
|
+
|
|
1500
|
+
// ---- 类型 ----
|
|
1501
|
+
const inheritedType = options.messageType == null;
|
|
1502
|
+
const messageType = inheritedType ? decryptedPayload.messageType : options.messageType;
|
|
1503
|
+
if (messageType === 'instant') {
|
|
1504
|
+
throw new TypeError(
|
|
1505
|
+
"scheduleTask: messageType 不能是 'instant'——instant 的语义是「建行的那一刻就投递」," +
|
|
1506
|
+
'那条路径归 POST /schedule-message 管;从 fire 里建一条 instant,投递时机反而说不清。' +
|
|
1507
|
+
(inheritedType ? '(这个 instant 是从当前任务继承来的,显式传 auto / prompted / fixed 覆盖它。)' : '')
|
|
1508
|
+
);
|
|
1509
|
+
}
|
|
1510
|
+
if (!SCHEDULABLE_MESSAGE_TYPES.has(messageType)) {
|
|
1511
|
+
throw new TypeError(
|
|
1512
|
+
`scheduleTask: messageType 只能是 auto / prompted / fixed,收到 ${JSON.stringify(messageType)}`
|
|
1513
|
+
);
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
const recurrenceType = options.recurrenceType == null ? 'none' : options.recurrenceType;
|
|
1517
|
+
if (!SCHEDULABLE_RECURRENCE_TYPES.has(recurrenceType)) {
|
|
1518
|
+
throw new TypeError(
|
|
1519
|
+
`scheduleTask: recurrenceType 只能是 none / daily / weekly,收到 ${JSON.stringify(recurrenceType)}`
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// ---- 继承 + 覆盖 ----
|
|
1524
|
+
const contactName = options.contactName === undefined ? decryptedPayload.contactName : options.contactName;
|
|
1525
|
+
if (typeof contactName !== 'string' || !contactName.trim()) {
|
|
1526
|
+
throw new TypeError('scheduleTask: contactName 必须是非空字符串(默认继承当前任务)');
|
|
1527
|
+
}
|
|
1528
|
+
const userMessage = options.userMessage === undefined
|
|
1529
|
+
? (decryptedPayload.userMessage ?? null)
|
|
1530
|
+
: options.userMessage;
|
|
1531
|
+
if (messageType === 'fixed' && (typeof userMessage !== 'string' || !userMessage.trim())) {
|
|
1532
|
+
throw new TypeError(
|
|
1533
|
+
"scheduleTask: messageType 'fixed' 必须有 userMessage(自己传,或从当前任务继承到)" +
|
|
1534
|
+
'——固定文本任务没有正文,就是一条永远发空的任务'
|
|
1535
|
+
);
|
|
1536
|
+
}
|
|
1537
|
+
const metadata = options.metadata === undefined ? (decryptedPayload.metadata || {}) : options.metadata;
|
|
1538
|
+
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
|
|
1539
|
+
throw new TypeError('scheduleTask: metadata 必须是普通对象(整体替换当前任务的 metadata,不做深合并)');
|
|
1540
|
+
}
|
|
1541
|
+
const uuid = options.uuid == null ? randomUUID() : options.uuid;
|
|
1542
|
+
if (typeof uuid !== 'string' || !uuid.trim()) {
|
|
1543
|
+
throw new TypeError('scheduleTask: uuid 必须是非空字符串');
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
// ---- 额度 ----
|
|
1547
|
+
if (scheduledTaskCount >= maxScheduledTasksPerFire) {
|
|
1548
|
+
throw new RangeError(
|
|
1549
|
+
`scheduleTask: 单次 fire 最多建 ${maxScheduledTasksPerFire} 条任务` +
|
|
1550
|
+
`(factory 配置 maxScheduledTasksPerFire 可调),这是第 ${scheduledTaskCount + 1} 条`
|
|
1551
|
+
);
|
|
1552
|
+
}
|
|
1553
|
+
// readState 在适配器不支持时降级成空数组;建任务不一样,静默成功会让宿主
|
|
1554
|
+
// 以为后续那条已经排上了,其实谁也不会触发它。
|
|
1555
|
+
if (!ctx.db || typeof ctx.db.createTask !== 'function') {
|
|
1556
|
+
throw new Error('AGENTIC_SCHEDULE_UNSUPPORTED: 当前数据库适配器不支持建任务(缺 createTask)');
|
|
1557
|
+
}
|
|
1558
|
+
scheduledTaskCount++;
|
|
1559
|
+
|
|
1560
|
+
// 字段构成与 POST /schedule-message 落库的那份保持一致,只是走库内部、不经 HTTP。
|
|
1561
|
+
const fullTaskData = {
|
|
1562
|
+
contactName,
|
|
1563
|
+
avatarUrl: options.avatarUrl === undefined ? (decryptedPayload.avatarUrl || null) : (options.avatarUrl || null),
|
|
1564
|
+
messageType,
|
|
1565
|
+
messageSubtype: (options.messageSubtype === undefined ? decryptedPayload.messageSubtype : options.messageSubtype) || 'chat',
|
|
1566
|
+
userMessage: userMessage || null,
|
|
1567
|
+
firstSendTime: nextSendAt,
|
|
1568
|
+
recurrenceType,
|
|
1569
|
+
apiUrl: decryptedPayload.apiUrl || null,
|
|
1570
|
+
apiKey: decryptedPayload.apiKey || null,
|
|
1571
|
+
primaryModel: decryptedPayload.primaryModel || null,
|
|
1572
|
+
// 见文件头:fire-time hook 每次现场重组 prompt,旧 prompt 带过去只会在新
|
|
1573
|
+
// 任务走回老链路时顶替宿主的意图。
|
|
1574
|
+
completePrompt: null,
|
|
1575
|
+
messages: null,
|
|
1576
|
+
maxTokens: decryptedPayload.maxTokens ?? null,
|
|
1577
|
+
temperature: decryptedPayload.temperature ?? null,
|
|
1578
|
+
splitPattern: decryptedPayload.splitPattern ?? null,
|
|
1579
|
+
pushSubscription: decryptedPayload.pushSubscription,
|
|
1580
|
+
metadata,
|
|
1581
|
+
};
|
|
1582
|
+
|
|
1583
|
+
const encryptedPayload = await encryptForStorage(JSON.stringify(fullTaskData), userKey);
|
|
1584
|
+
|
|
1585
|
+
let created;
|
|
1586
|
+
try {
|
|
1587
|
+
created = await ctx.db.createTask({
|
|
1588
|
+
user_id: task.user_id,
|
|
1589
|
+
uuid,
|
|
1590
|
+
encrypted_payload: encryptedPayload,
|
|
1591
|
+
next_send_at: nextSendAt,
|
|
1592
|
+
message_type: messageType,
|
|
1593
|
+
});
|
|
1594
|
+
} catch (error) {
|
|
1595
|
+
// 撞 uuid 不算错:fire 失败会整条重跑,宿主用确定性 uuid(任务 id + 触发
|
|
1596
|
+
// 时刻推出来的那种)就天然幂等,重试不会多排一条。
|
|
1597
|
+
if (isUniqueViolation(error)) return { created: false, reason: 'duplicate', uuid };
|
|
1598
|
+
throw error;
|
|
1599
|
+
}
|
|
1600
|
+
if (!created) {
|
|
1601
|
+
throw new Error('AGENTIC_SCHEDULE_FAILED: createTask 没有返回新建的任务行');
|
|
1602
|
+
}
|
|
1603
|
+
return {
|
|
1604
|
+
created: true,
|
|
1605
|
+
id: created.id ?? null,
|
|
1606
|
+
uuid: created.uuid ?? uuid,
|
|
1607
|
+
nextSendAt: created.next_send_at ?? nextSendAt,
|
|
1608
|
+
};
|
|
1609
|
+
};
|
|
1610
|
+
|
|
1403
1611
|
// 单次 fire 的宿主便签:onBeforeFire 的 fireCtx 和同一次 fire 每轮的
|
|
1404
1612
|
// sessionCtx(onLLMOutput / executeToolCalls)拿到同一个对象引用,fire 结束
|
|
1405
1613
|
// (finish / skip-push / 抛错 / 轮数超限)随调用栈丢弃。库自己不读不写、
|
|
@@ -1411,6 +1619,7 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
1411
1619
|
userId: task.user_id,
|
|
1412
1620
|
readState,
|
|
1413
1621
|
writeState,
|
|
1622
|
+
scheduleTask,
|
|
1414
1623
|
now: new Date(nowFn()),
|
|
1415
1624
|
scratch,
|
|
1416
1625
|
});
|
|
@@ -1463,9 +1672,10 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
1463
1672
|
const assistantMessage = extractAssistantMessage(llmResponse);
|
|
1464
1673
|
messages = [...messages, assistantMessage];
|
|
1465
1674
|
|
|
1466
|
-
// 共享的 SessionContext(与 amsg-instant
|
|
1467
|
-
//
|
|
1468
|
-
// onLLMOutput / executeToolCalls 的位置,
|
|
1675
|
+
// 共享的 SessionContext(与 amsg-instant 同形状)之上,再挂两个状态访问器
|
|
1676
|
+
// 和 scheduleTask:大内容要不要旁路存、要不要给自己排条后续,往往到工具跑完、
|
|
1677
|
+
// 组 push 时才知道,而那正是 onLLMOutput / executeToolCalls 的位置,
|
|
1678
|
+
// onBeforeFire 早就返回了。
|
|
1469
1679
|
const sessionCtx = Object.freeze({
|
|
1470
1680
|
...buildSessionContext({
|
|
1471
1681
|
sessionId,
|
|
@@ -1480,6 +1690,7 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
1480
1690
|
}),
|
|
1481
1691
|
readState,
|
|
1482
1692
|
writeState,
|
|
1693
|
+
scheduleTask,
|
|
1483
1694
|
});
|
|
1484
1695
|
|
|
1485
1696
|
const decision = await hooks.onLLMOutput(sessionCtx);
|
|
@@ -1536,6 +1747,13 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
1536
1747
|
// same round, both need their id on the assistant turn, or the half
|
|
1537
1748
|
// that's missing leaves an orphan role:'tool' and a strict relay
|
|
1538
1749
|
// rejects the next round.
|
|
1750
|
+
//
|
|
1751
|
+
// Contract for onLLMOutput: the toolCalls it returns must cover every
|
|
1752
|
+
// native tool_call the model declared. A native call left out still
|
|
1753
|
+
// gets declared on the assistant turn (it came from the model) but has
|
|
1754
|
+
// no matching role:'tool' result, and a strict relay rejects the next
|
|
1755
|
+
// round over the unanswered id. Drop a call on purpose → return a
|
|
1756
|
+
// result for it saying so, don't just omit it.
|
|
1539
1757
|
const nativeCalls = Array.isArray(assistantMessage.tool_calls) ? assistantMessage.tool_calls : [];
|
|
1540
1758
|
const nativeIds = new Set(nativeCalls.map((tc) => tc && tc.id));
|
|
1541
1759
|
const synthesized = toolCalls.filter((tc) => !nativeIds.has(tc && tc.id));
|
|
@@ -3573,6 +3791,7 @@ const SERVER_FEATURES = Object.freeze([
|
|
|
3573
3791
|
'agentic-scratch',
|
|
3574
3792
|
'agentic-write-state',
|
|
3575
3793
|
'agentic-fire-tools',
|
|
3794
|
+
'agentic-schedule-task',
|
|
3576
3795
|
'vapid-public-key',
|
|
3577
3796
|
]);
|
|
3578
3797
|
|
|
@@ -3612,6 +3831,7 @@ function createCapabilitiesHandler(ctx) {
|
|
|
3612
3831
|
* @param {number} [config.maxToolIterations] - factory default LLM-round cap for the agentic loop (default 5).
|
|
3613
3832
|
* @param {number} [config.totalTimeoutMs] - factory default wall-time ceiling for the agentic loop (default 240000).
|
|
3614
3833
|
* @param {number} [config.maxStateValueBytes] - client_state 单条 value 的总上限(默认 5MB)。超过 200KB 的值由服务端透明分块存储(见 lib/state-chunks.js)。
|
|
3834
|
+
* @param {number} [config.maxScheduledTasksPerFire] - 一次 fire 里 hook 用 ctx.scheduleTask() 最多能建几条后续任务(默认 2,0 表示不许自排)。
|
|
3615
3835
|
* @returns {{ handlers: Object, ctx: Object }}
|
|
3616
3836
|
*/
|
|
3617
3837
|
|
|
@@ -3641,7 +3861,9 @@ function createSingleUserServer(config) {
|
|
|
3641
3861
|
hooks: config.hooks || null,
|
|
3642
3862
|
maxToolIterations: config.maxToolIterations,
|
|
3643
3863
|
totalTimeoutMs: config.totalTimeoutMs,
|
|
3644
|
-
maxStateValueBytes: config.maxStateValueBytes
|
|
3864
|
+
maxStateValueBytes: config.maxStateValueBytes,
|
|
3865
|
+
// hook 的 ctx.scheduleTask() 单次 fire 建任务的条数上限(默认 2)。
|
|
3866
|
+
maxScheduledTasksPerFire: config.maxScheduledTasksPerFire
|
|
3645
3867
|
};
|
|
3646
3868
|
|
|
3647
3869
|
return {
|
|
@@ -4078,10 +4300,11 @@ function createWebCryptoWebPush(vapid = {}, { ttl = SCHEDULED_DEFAULT_TTL } = {}
|
|
|
4078
4300
|
* Access-Control-* on responses. With no `cors` the Worker stays same-origin.
|
|
4079
4301
|
*
|
|
4080
4302
|
* Fire-time hooks are opt-in too: pass `hooks: { onBeforeFire, onLLMOutput,
|
|
4081
|
-
* executeToolCalls }` (+ optional `maxToolIterations` / `totalTimeoutMs`
|
|
4082
|
-
* config to let scheduled AI tasks assemble
|
|
4083
|
-
* tool loop at fire time. Omit them and AI
|
|
4084
|
-
* prompt exactly as before. See
|
|
4303
|
+
* executeToolCalls }` (+ optional `maxToolIterations` / `totalTimeoutMs` /
|
|
4304
|
+
* `maxScheduledTasksPerFire`) in the config to let scheduled AI tasks assemble
|
|
4305
|
+
* their prompt and run a server-side tool loop at fire time. Omit them and AI
|
|
4306
|
+
* tasks replay the schedule-time frozen prompt exactly as before. See
|
|
4307
|
+
* lib/agentic-fire.js.
|
|
4085
4308
|
*
|
|
4086
4309
|
* scheduled() 每次触发都会先给任务占位(在行的 lease_until 上写租约),同一
|
|
4087
4310
|
* 条任务不会被相邻两跳重复触发(见 lib/run-tick.js)。租期默认 10 分钟,可以
|
|
@@ -4221,6 +4444,8 @@ function createSingleUserCloudflareWorker(buildConfig) {
|
|
|
4221
4444
|
totalTimeoutMs: cfg.totalTimeoutMs,
|
|
4222
4445
|
// hook 的 ctx.writeState() 用它判断单条 value 的上限,和 PUT /client-state 同一个配置。
|
|
4223
4446
|
maxStateValueBytes: cfg.maxStateValueBytes,
|
|
4447
|
+
// hook 的 ctx.scheduleTask() 单次 fire 建任务的条数上限(默认 2)。
|
|
4448
|
+
maxScheduledTasksPerFire: cfg.maxScheduledTasksPerFire,
|
|
4224
4449
|
// 任务占位租期(默认 10 分钟,随 totalTimeoutMs 抬高)。
|
|
4225
4450
|
claimLeaseMs: cfg.claimLeaseMs
|
|
4226
4451
|
});
|
|
@@ -1183,6 +1183,32 @@ async function writeClientStateEntries({ db, userId, userKey, entries }) {
|
|
|
1183
1183
|
* 补完那轮模型仍可能再发起调用。库不看 tools 的内容,执行照旧是
|
|
1184
1184
|
* executeToolCalls 的事。
|
|
1185
1185
|
*
|
|
1186
|
+
* 自排后续任务:fireCtx 和每轮的 sessionCtx 上都挂着 scheduleTask,宿主用它
|
|
1187
|
+
* 在这次 fire 里给同一个用户再建一条定时任务(「这条发完,一个半小时后我再
|
|
1188
|
+
* 接着说一句」)。写口在 sessionCtx 上也给,是因为要不要接着说往往是看完
|
|
1189
|
+
* LLM 输出才定的,那时 onBeforeFire 早已返回。凭据(pushSubscription /
|
|
1190
|
+
* apiKey)和投递配置从当前任务继承、宿主全程看不到(与 buildHookTask 屏蔽
|
|
1191
|
+
* 凭据同一个原则),宿主只提供「什么时候、说什么方向」。
|
|
1192
|
+
*
|
|
1193
|
+
* 新任务不继承 completePrompt / messages,两者都置 null:fire-time hook 每次
|
|
1194
|
+
* 现场重组 prompt,把排程时冻结的旧 prompt 带过去,会在新任务万一走回老链路
|
|
1195
|
+
* (onBeforeFire 返回 null、或那次部署没配 hooks)时静默顶替宿主的意图——
|
|
1196
|
+
* 与其发一条来路不明的旧文案,不如让它当场失败、走既有的重试/标记逻辑。
|
|
1197
|
+
*
|
|
1198
|
+
* 护栏(几条都是这个能力能不能上线的关键):
|
|
1199
|
+
* - firstSendTime 至少比现在晚 MIN_SCHEDULE_LEAD_MS(60 秒)。cron 每分钟
|
|
1200
|
+
* 一跳,排在 60 秒内等于让下一跳立刻捡走,容易变成自己触发自己的紧密循环。
|
|
1201
|
+
* - messageType 只收 auto / prompted / fixed,不收 instant。instant 的语义是
|
|
1202
|
+
* 「建行的那一刻就投递」,那条路径归 POST /schedule-message 管;从 fire 里
|
|
1203
|
+
* 造这么一行,投递时机反而说不清(这里不投,要等 cron 捞到才发)。
|
|
1204
|
+
* - 单次 fire 的建任务上限默认 DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE 条,
|
|
1205
|
+
* factory 配置 ctx.maxScheduledTasksPerFire 可覆盖(0 表示不许自排)。模型
|
|
1206
|
+
* 自排后续本质上是个可以无限延伸的链:没有上限的话,一次 fire 里连排十条、
|
|
1207
|
+
* 或者每条任务都排下一条,就成了谁也没按下停止键的循环。
|
|
1208
|
+
* - uuid 冲突不抛错,返回 { created: false, reason: 'duplicate' }。fire 失败
|
|
1209
|
+
* 会整条重跑(run-tick 的重试语义),宿主传一个由「任务 id + 触发时刻」推
|
|
1210
|
+
* 出来的确定性 uuid 就天然幂等,不会每重试一次多排一条。
|
|
1211
|
+
*
|
|
1186
1212
|
* Budget guards, both factory-level (ctx.maxToolIterations /
|
|
1187
1213
|
* ctx.totalTimeoutMs) and per-fire (onBeforeFire may return
|
|
1188
1214
|
* { messages, maxToolIterations?, totalTimeoutMs? } to override for one
|
|
@@ -1197,6 +1223,17 @@ async function writeClientStateEntries({ db, userId, userKey, entries }) {
|
|
|
1197
1223
|
const DEFAULT_MAX_TOOL_ITERATIONS = 5;
|
|
1198
1224
|
const DEFAULT_TOTAL_TIMEOUT_MS = 240_000;
|
|
1199
1225
|
|
|
1226
|
+
// 一次 fire 里最多能给自己排几条后续任务(ctx.maxScheduledTasksPerFire 可覆盖)。
|
|
1227
|
+
const DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE = 2;
|
|
1228
|
+
// 自排任务的最小提前量:cron 一分钟一跳,比这更近等于让下一跳立刻捡走。
|
|
1229
|
+
const MIN_SCHEDULE_LEAD_MS = 60_000;
|
|
1230
|
+
|
|
1231
|
+
// 自排任务允许的类型。instant 归 POST /schedule-message 那条同步路径管。
|
|
1232
|
+
const SCHEDULABLE_MESSAGE_TYPES = new Set(['auto', 'prompted', 'fixed']);
|
|
1233
|
+
// 与 validateScheduleMessagePayload 同一套;run-tick 只认得这三种,别的值会让
|
|
1234
|
+
// 任务发完之后推进不到下一次。
|
|
1235
|
+
const SCHEDULABLE_RECURRENCE_TYPES = new Set(['none', 'daily', 'weekly']);
|
|
1236
|
+
|
|
1200
1237
|
// Same pacing as the legacy path / amsg-instant.
|
|
1201
1238
|
const SLEEP_BETWEEN_MESSAGES_MS$1 = 1500;
|
|
1202
1239
|
|
|
@@ -1251,6 +1288,9 @@ function normalizeBeforeFireResult(result) {
|
|
|
1251
1288
|
messages: result.messages,
|
|
1252
1289
|
maxToolIterations: result.maxToolIterations,
|
|
1253
1290
|
totalTimeoutMs: result.totalTimeoutMs,
|
|
1291
|
+
// 归一成「有就是非空数组,否则 undefined」,让下面每轮的透传判断能直接
|
|
1292
|
+
// 用真值。真正决定请求体带不带 tools 的规则住在 llm.js 的
|
|
1293
|
+
// buildAiRequestBody(那边有测试钉着),这层只是不把空数组往下传。
|
|
1254
1294
|
tools: Array.isArray(result.tools) && result.tools.length > 0 ? result.tools : undefined,
|
|
1255
1295
|
toolChoice: result.toolChoice,
|
|
1256
1296
|
};
|
|
@@ -1281,7 +1321,7 @@ function firstPositiveNumber(values, fallback) {
|
|
|
1281
1321
|
* @param {import('../adapters/interface.js').TaskRow} args.task
|
|
1282
1322
|
* @param {Object} args.decryptedPayload - decrypted task payload (has credentials; they stop here)
|
|
1283
1323
|
* @param {string} args.userKey - per-user storage key (for readState decryption)
|
|
1284
|
-
* @param {Object} args.ctx - processor ctx ({ db, webpush, vapid, hooks, maxToolIterations, totalTimeoutMs })
|
|
1324
|
+
* @param {Object} args.ctx - processor ctx ({ db, webpush, vapid, hooks, maxToolIterations, totalTimeoutMs, maxScheduledTasksPerFire })
|
|
1285
1325
|
* @returns {Promise<{ handled: false } | { handled: true, result: { success: true, messagesSent: number, status: 'finished'|'skipped', iterations: number } }>}
|
|
1286
1326
|
* `handled: false` → caller falls back to the legacy frozen-prompt path
|
|
1287
1327
|
* (onBeforeFire returned null).
|
|
@@ -1400,6 +1440,174 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
1400
1440
|
return writeClientStateEntries({ db: ctx.db, userId: task.user_id, userKey, entries: normalized });
|
|
1401
1441
|
};
|
|
1402
1442
|
|
|
1443
|
+
const maxScheduledTasksPerFire =
|
|
1444
|
+
Number.isInteger(ctx.maxScheduledTasksPerFire) && ctx.maxScheduledTasksPerFire >= 0
|
|
1445
|
+
? ctx.maxScheduledTasksPerFire
|
|
1446
|
+
: DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE;
|
|
1447
|
+
// 本次 fire 已经用掉的建任务额度。校验通过、真的要写库时才 +1,参数不合法的
|
|
1448
|
+
// 调用不占额度;uuid 撞车(那条任务其实已经建出来了)照样占。
|
|
1449
|
+
let scheduledTaskCount = 0;
|
|
1450
|
+
|
|
1451
|
+
/**
|
|
1452
|
+
* scheduleTask(options) —— 在这次 fire 里给**同一个用户**再建一条定时任务。
|
|
1453
|
+
*
|
|
1454
|
+
* 典型用法:角色发完这条,想过一个半小时再接着说一句。以前只能写进
|
|
1455
|
+
* client_state 等客户端上线重放,用户一直不上线这条链就断了;建成任务行之后,
|
|
1456
|
+
* 到点由 cron 直接触发,跟别的定时消息一样。
|
|
1457
|
+
*
|
|
1458
|
+
* 凭据与投递配置(pushSubscription / apiUrl / apiKey / primaryModel /
|
|
1459
|
+
* maxTokens / temperature / splitPattern)以及 contactName / avatarUrl /
|
|
1460
|
+
* messageSubtype / userMessage 从当前任务继承,宿主只说「什么时候、说什么
|
|
1461
|
+
* 方向」——hook 全程看不到凭据。completePrompt / messages 不继承(都置 null),
|
|
1462
|
+
* 见文件头。
|
|
1463
|
+
*
|
|
1464
|
+
* @param {Object} options
|
|
1465
|
+
* @param {string} options.firstSendTime - 必填,ISO 8601;至少比现在晚 60 秒
|
|
1466
|
+
* @param {'none'|'daily'|'weekly'} [options.recurrenceType='none']
|
|
1467
|
+
* @param {'auto'|'prompted'|'fixed'} [options.messageType] - 默认继承当前任务
|
|
1468
|
+
* @param {Object} [options.metadata] - 整体替换(不是深合并)当前任务的 metadata
|
|
1469
|
+
* @param {string} [options.contactName] - 默认继承
|
|
1470
|
+
* @param {string|null} [options.avatarUrl] - 默认继承
|
|
1471
|
+
* @param {string} [options.messageSubtype] - 默认继承
|
|
1472
|
+
* @param {string|null} [options.userMessage] - 默认继承;messageType 'fixed' 时必须有
|
|
1473
|
+
* @param {string} [options.uuid] - 默认 randomUUID();传确定性 uuid 可做重试幂等
|
|
1474
|
+
* @returns {Promise<{ created: true, id: number|null, uuid: string, nextSendAt: string }
|
|
1475
|
+
* | { created: false, reason: 'duplicate', uuid: string }>}
|
|
1476
|
+
*/
|
|
1477
|
+
const scheduleTask = async (options) => {
|
|
1478
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
1479
|
+
throw new TypeError('scheduleTask(options) 需要一个对象,至少包含 { firstSendTime }');
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
// ---- 触发时刻 ----
|
|
1483
|
+
if (typeof options.firstSendTime !== 'string' || !options.firstSendTime.trim()) {
|
|
1484
|
+
throw new RangeError('scheduleTask: firstSendTime 必填,且必须是 ISO 8601 字符串');
|
|
1485
|
+
}
|
|
1486
|
+
const firstSendAt = new Date(options.firstSendTime);
|
|
1487
|
+
if (Number.isNaN(firstSendAt.getTime())) {
|
|
1488
|
+
throw new RangeError(`scheduleTask: firstSendTime 解析不出合法时间:${options.firstSendTime}`);
|
|
1489
|
+
}
|
|
1490
|
+
const earliest = nowFn() + MIN_SCHEDULE_LEAD_MS;
|
|
1491
|
+
if (firstSendAt.getTime() < earliest) {
|
|
1492
|
+
throw new RangeError(
|
|
1493
|
+
`scheduleTask: firstSendTime 至少要比现在晚 ${MIN_SCHEDULE_LEAD_MS / 1000} 秒` +
|
|
1494
|
+
`(最早可排 ${new Date(earliest).toISOString()},收到 ${firstSendAt.toISOString()})` +
|
|
1495
|
+
'——cron 一分钟一跳,排得更近等于让下一跳立刻捡走'
|
|
1496
|
+
);
|
|
1497
|
+
}
|
|
1498
|
+
const nextSendAt = firstSendAt.toISOString();
|
|
1499
|
+
|
|
1500
|
+
// ---- 类型 ----
|
|
1501
|
+
const inheritedType = options.messageType == null;
|
|
1502
|
+
const messageType = inheritedType ? decryptedPayload.messageType : options.messageType;
|
|
1503
|
+
if (messageType === 'instant') {
|
|
1504
|
+
throw new TypeError(
|
|
1505
|
+
"scheduleTask: messageType 不能是 'instant'——instant 的语义是「建行的那一刻就投递」," +
|
|
1506
|
+
'那条路径归 POST /schedule-message 管;从 fire 里建一条 instant,投递时机反而说不清。' +
|
|
1507
|
+
(inheritedType ? '(这个 instant 是从当前任务继承来的,显式传 auto / prompted / fixed 覆盖它。)' : '')
|
|
1508
|
+
);
|
|
1509
|
+
}
|
|
1510
|
+
if (!SCHEDULABLE_MESSAGE_TYPES.has(messageType)) {
|
|
1511
|
+
throw new TypeError(
|
|
1512
|
+
`scheduleTask: messageType 只能是 auto / prompted / fixed,收到 ${JSON.stringify(messageType)}`
|
|
1513
|
+
);
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
const recurrenceType = options.recurrenceType == null ? 'none' : options.recurrenceType;
|
|
1517
|
+
if (!SCHEDULABLE_RECURRENCE_TYPES.has(recurrenceType)) {
|
|
1518
|
+
throw new TypeError(
|
|
1519
|
+
`scheduleTask: recurrenceType 只能是 none / daily / weekly,收到 ${JSON.stringify(recurrenceType)}`
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// ---- 继承 + 覆盖 ----
|
|
1524
|
+
const contactName = options.contactName === undefined ? decryptedPayload.contactName : options.contactName;
|
|
1525
|
+
if (typeof contactName !== 'string' || !contactName.trim()) {
|
|
1526
|
+
throw new TypeError('scheduleTask: contactName 必须是非空字符串(默认继承当前任务)');
|
|
1527
|
+
}
|
|
1528
|
+
const userMessage = options.userMessage === undefined
|
|
1529
|
+
? (decryptedPayload.userMessage ?? null)
|
|
1530
|
+
: options.userMessage;
|
|
1531
|
+
if (messageType === 'fixed' && (typeof userMessage !== 'string' || !userMessage.trim())) {
|
|
1532
|
+
throw new TypeError(
|
|
1533
|
+
"scheduleTask: messageType 'fixed' 必须有 userMessage(自己传,或从当前任务继承到)" +
|
|
1534
|
+
'——固定文本任务没有正文,就是一条永远发空的任务'
|
|
1535
|
+
);
|
|
1536
|
+
}
|
|
1537
|
+
const metadata = options.metadata === undefined ? (decryptedPayload.metadata || {}) : options.metadata;
|
|
1538
|
+
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
|
|
1539
|
+
throw new TypeError('scheduleTask: metadata 必须是普通对象(整体替换当前任务的 metadata,不做深合并)');
|
|
1540
|
+
}
|
|
1541
|
+
const uuid = options.uuid == null ? randomUUID() : options.uuid;
|
|
1542
|
+
if (typeof uuid !== 'string' || !uuid.trim()) {
|
|
1543
|
+
throw new TypeError('scheduleTask: uuid 必须是非空字符串');
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
// ---- 额度 ----
|
|
1547
|
+
if (scheduledTaskCount >= maxScheduledTasksPerFire) {
|
|
1548
|
+
throw new RangeError(
|
|
1549
|
+
`scheduleTask: 单次 fire 最多建 ${maxScheduledTasksPerFire} 条任务` +
|
|
1550
|
+
`(factory 配置 maxScheduledTasksPerFire 可调),这是第 ${scheduledTaskCount + 1} 条`
|
|
1551
|
+
);
|
|
1552
|
+
}
|
|
1553
|
+
// readState 在适配器不支持时降级成空数组;建任务不一样,静默成功会让宿主
|
|
1554
|
+
// 以为后续那条已经排上了,其实谁也不会触发它。
|
|
1555
|
+
if (!ctx.db || typeof ctx.db.createTask !== 'function') {
|
|
1556
|
+
throw new Error('AGENTIC_SCHEDULE_UNSUPPORTED: 当前数据库适配器不支持建任务(缺 createTask)');
|
|
1557
|
+
}
|
|
1558
|
+
scheduledTaskCount++;
|
|
1559
|
+
|
|
1560
|
+
// 字段构成与 POST /schedule-message 落库的那份保持一致,只是走库内部、不经 HTTP。
|
|
1561
|
+
const fullTaskData = {
|
|
1562
|
+
contactName,
|
|
1563
|
+
avatarUrl: options.avatarUrl === undefined ? (decryptedPayload.avatarUrl || null) : (options.avatarUrl || null),
|
|
1564
|
+
messageType,
|
|
1565
|
+
messageSubtype: (options.messageSubtype === undefined ? decryptedPayload.messageSubtype : options.messageSubtype) || 'chat',
|
|
1566
|
+
userMessage: userMessage || null,
|
|
1567
|
+
firstSendTime: nextSendAt,
|
|
1568
|
+
recurrenceType,
|
|
1569
|
+
apiUrl: decryptedPayload.apiUrl || null,
|
|
1570
|
+
apiKey: decryptedPayload.apiKey || null,
|
|
1571
|
+
primaryModel: decryptedPayload.primaryModel || null,
|
|
1572
|
+
// 见文件头:fire-time hook 每次现场重组 prompt,旧 prompt 带过去只会在新
|
|
1573
|
+
// 任务走回老链路时顶替宿主的意图。
|
|
1574
|
+
completePrompt: null,
|
|
1575
|
+
messages: null,
|
|
1576
|
+
maxTokens: decryptedPayload.maxTokens ?? null,
|
|
1577
|
+
temperature: decryptedPayload.temperature ?? null,
|
|
1578
|
+
splitPattern: decryptedPayload.splitPattern ?? null,
|
|
1579
|
+
pushSubscription: decryptedPayload.pushSubscription,
|
|
1580
|
+
metadata,
|
|
1581
|
+
};
|
|
1582
|
+
|
|
1583
|
+
const encryptedPayload = await encryptForStorage(JSON.stringify(fullTaskData), userKey);
|
|
1584
|
+
|
|
1585
|
+
let created;
|
|
1586
|
+
try {
|
|
1587
|
+
created = await ctx.db.createTask({
|
|
1588
|
+
user_id: task.user_id,
|
|
1589
|
+
uuid,
|
|
1590
|
+
encrypted_payload: encryptedPayload,
|
|
1591
|
+
next_send_at: nextSendAt,
|
|
1592
|
+
message_type: messageType,
|
|
1593
|
+
});
|
|
1594
|
+
} catch (error) {
|
|
1595
|
+
// 撞 uuid 不算错:fire 失败会整条重跑,宿主用确定性 uuid(任务 id + 触发
|
|
1596
|
+
// 时刻推出来的那种)就天然幂等,重试不会多排一条。
|
|
1597
|
+
if (isUniqueViolation(error)) return { created: false, reason: 'duplicate', uuid };
|
|
1598
|
+
throw error;
|
|
1599
|
+
}
|
|
1600
|
+
if (!created) {
|
|
1601
|
+
throw new Error('AGENTIC_SCHEDULE_FAILED: createTask 没有返回新建的任务行');
|
|
1602
|
+
}
|
|
1603
|
+
return {
|
|
1604
|
+
created: true,
|
|
1605
|
+
id: created.id ?? null,
|
|
1606
|
+
uuid: created.uuid ?? uuid,
|
|
1607
|
+
nextSendAt: created.next_send_at ?? nextSendAt,
|
|
1608
|
+
};
|
|
1609
|
+
};
|
|
1610
|
+
|
|
1403
1611
|
// 单次 fire 的宿主便签:onBeforeFire 的 fireCtx 和同一次 fire 每轮的
|
|
1404
1612
|
// sessionCtx(onLLMOutput / executeToolCalls)拿到同一个对象引用,fire 结束
|
|
1405
1613
|
// (finish / skip-push / 抛错 / 轮数超限)随调用栈丢弃。库自己不读不写、
|
|
@@ -1411,6 +1619,7 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
1411
1619
|
userId: task.user_id,
|
|
1412
1620
|
readState,
|
|
1413
1621
|
writeState,
|
|
1622
|
+
scheduleTask,
|
|
1414
1623
|
now: new Date(nowFn()),
|
|
1415
1624
|
scratch,
|
|
1416
1625
|
});
|
|
@@ -1463,9 +1672,10 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
1463
1672
|
const assistantMessage = extractAssistantMessage(llmResponse);
|
|
1464
1673
|
messages = [...messages, assistantMessage];
|
|
1465
1674
|
|
|
1466
|
-
// 共享的 SessionContext(与 amsg-instant
|
|
1467
|
-
//
|
|
1468
|
-
// onLLMOutput / executeToolCalls 的位置,
|
|
1675
|
+
// 共享的 SessionContext(与 amsg-instant 同形状)之上,再挂两个状态访问器
|
|
1676
|
+
// 和 scheduleTask:大内容要不要旁路存、要不要给自己排条后续,往往到工具跑完、
|
|
1677
|
+
// 组 push 时才知道,而那正是 onLLMOutput / executeToolCalls 的位置,
|
|
1678
|
+
// onBeforeFire 早就返回了。
|
|
1469
1679
|
const sessionCtx = Object.freeze({
|
|
1470
1680
|
...buildSessionContext({
|
|
1471
1681
|
sessionId,
|
|
@@ -1480,6 +1690,7 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
1480
1690
|
}),
|
|
1481
1691
|
readState,
|
|
1482
1692
|
writeState,
|
|
1693
|
+
scheduleTask,
|
|
1483
1694
|
});
|
|
1484
1695
|
|
|
1485
1696
|
const decision = await hooks.onLLMOutput(sessionCtx);
|
|
@@ -1536,6 +1747,13 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
1536
1747
|
// same round, both need their id on the assistant turn, or the half
|
|
1537
1748
|
// that's missing leaves an orphan role:'tool' and a strict relay
|
|
1538
1749
|
// rejects the next round.
|
|
1750
|
+
//
|
|
1751
|
+
// Contract for onLLMOutput: the toolCalls it returns must cover every
|
|
1752
|
+
// native tool_call the model declared. A native call left out still
|
|
1753
|
+
// gets declared on the assistant turn (it came from the model) but has
|
|
1754
|
+
// no matching role:'tool' result, and a strict relay rejects the next
|
|
1755
|
+
// round over the unanswered id. Drop a call on purpose → return a
|
|
1756
|
+
// result for it saying so, don't just omit it.
|
|
1539
1757
|
const nativeCalls = Array.isArray(assistantMessage.tool_calls) ? assistantMessage.tool_calls : [];
|
|
1540
1758
|
const nativeIds = new Set(nativeCalls.map((tc) => tc && tc.id));
|
|
1541
1759
|
const synthesized = toolCalls.filter((tc) => !nativeIds.has(tc && tc.id));
|
|
@@ -3573,6 +3791,7 @@ const SERVER_FEATURES = Object.freeze([
|
|
|
3573
3791
|
'agentic-scratch',
|
|
3574
3792
|
'agentic-write-state',
|
|
3575
3793
|
'agentic-fire-tools',
|
|
3794
|
+
'agentic-schedule-task',
|
|
3576
3795
|
'vapid-public-key',
|
|
3577
3796
|
]);
|
|
3578
3797
|
|
|
@@ -3612,6 +3831,7 @@ function createCapabilitiesHandler(ctx) {
|
|
|
3612
3831
|
* @param {number} [config.maxToolIterations] - factory default LLM-round cap for the agentic loop (default 5).
|
|
3613
3832
|
* @param {number} [config.totalTimeoutMs] - factory default wall-time ceiling for the agentic loop (default 240000).
|
|
3614
3833
|
* @param {number} [config.maxStateValueBytes] - client_state 单条 value 的总上限(默认 5MB)。超过 200KB 的值由服务端透明分块存储(见 lib/state-chunks.js)。
|
|
3834
|
+
* @param {number} [config.maxScheduledTasksPerFire] - 一次 fire 里 hook 用 ctx.scheduleTask() 最多能建几条后续任务(默认 2,0 表示不许自排)。
|
|
3615
3835
|
* @returns {{ handlers: Object, ctx: Object }}
|
|
3616
3836
|
*/
|
|
3617
3837
|
|
|
@@ -3641,7 +3861,9 @@ function createSingleUserServer(config) {
|
|
|
3641
3861
|
hooks: config.hooks || null,
|
|
3642
3862
|
maxToolIterations: config.maxToolIterations,
|
|
3643
3863
|
totalTimeoutMs: config.totalTimeoutMs,
|
|
3644
|
-
maxStateValueBytes: config.maxStateValueBytes
|
|
3864
|
+
maxStateValueBytes: config.maxStateValueBytes,
|
|
3865
|
+
// hook 的 ctx.scheduleTask() 单次 fire 建任务的条数上限(默认 2)。
|
|
3866
|
+
maxScheduledTasksPerFire: config.maxScheduledTasksPerFire
|
|
3645
3867
|
};
|
|
3646
3868
|
|
|
3647
3869
|
return {
|
|
@@ -4078,10 +4300,11 @@ function createWebCryptoWebPush(vapid = {}, { ttl = SCHEDULED_DEFAULT_TTL } = {}
|
|
|
4078
4300
|
* Access-Control-* on responses. With no `cors` the Worker stays same-origin.
|
|
4079
4301
|
*
|
|
4080
4302
|
* Fire-time hooks are opt-in too: pass `hooks: { onBeforeFire, onLLMOutput,
|
|
4081
|
-
* executeToolCalls }` (+ optional `maxToolIterations` / `totalTimeoutMs`
|
|
4082
|
-
* config to let scheduled AI tasks assemble
|
|
4083
|
-
* tool loop at fire time. Omit them and AI
|
|
4084
|
-
* prompt exactly as before. See
|
|
4303
|
+
* executeToolCalls }` (+ optional `maxToolIterations` / `totalTimeoutMs` /
|
|
4304
|
+
* `maxScheduledTasksPerFire`) in the config to let scheduled AI tasks assemble
|
|
4305
|
+
* their prompt and run a server-side tool loop at fire time. Omit them and AI
|
|
4306
|
+
* tasks replay the schedule-time frozen prompt exactly as before. See
|
|
4307
|
+
* lib/agentic-fire.js.
|
|
4085
4308
|
*
|
|
4086
4309
|
* scheduled() 每次触发都会先给任务占位(在行的 lease_until 上写租约),同一
|
|
4087
4310
|
* 条任务不会被相邻两跳重复触发(见 lib/run-tick.js)。租期默认 10 分钟,可以
|
|
@@ -4221,6 +4444,8 @@ function createSingleUserCloudflareWorker(buildConfig) {
|
|
|
4221
4444
|
totalTimeoutMs: cfg.totalTimeoutMs,
|
|
4222
4445
|
// hook 的 ctx.writeState() 用它判断单条 value 的上限,和 PUT /client-state 同一个配置。
|
|
4223
4446
|
maxStateValueBytes: cfg.maxStateValueBytes,
|
|
4447
|
+
// hook 的 ctx.scheduleTask() 单次 fire 建任务的条数上限(默认 2)。
|
|
4448
|
+
maxScheduledTasksPerFire: cfg.maxScheduledTasksPerFire,
|
|
4224
4449
|
// 任务占位租期(默认 10 分钟,随 totalTimeoutMs 抬高)。
|
|
4225
4450
|
claimLeaseMs: cfg.claimLeaseMs
|
|
4226
4451
|
});
|
package/dist/cloudflare.cjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
var
|
|
15
|
+
var _chunk3QRVP4JEcjs = require('./chunk-3QRVP4JE.cjs');
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
|
|
@@ -27,4 +27,4 @@ var _chunkHQHRXSQAcjs = require('./chunk-HQHRXSQA.cjs');
|
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
exports.MAX_PUSH_PAYLOAD_BYTES =
|
|
30
|
+
exports.MAX_PUSH_PAYLOAD_BYTES = _chunk3QRVP4JEcjs.MAX_PUSH_PAYLOAD_BYTES; exports.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES = _chunk3QRVP4JEcjs.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES; exports.WEB_PUSH_MAX_BODY_BYTES = _chunk3QRVP4JEcjs.WEB_PUSH_MAX_BODY_BYTES; exports.createD1Adapter = _chunk3QRVP4JEcjs.createD1Adapter; exports.createSingleUserCloudflareWorker = _chunk3QRVP4JEcjs.createSingleUserCloudflareWorker; exports.createSingleUserServer = _chunk3QRVP4JEcjs.createSingleUserServer; exports.createWebCryptoWebPush = _chunk3QRVP4JEcjs.createWebCryptoWebPush; exports.decryptFromStorage = _chunk3QRVP4JEcjs.decryptFromStorage; exports.decryptPayload = _chunk3QRVP4JEcjs.decryptPayload; exports.deriveUserEncryptionKey = _chunk3QRVP4JEcjs.deriveUserEncryptionKey; exports.encryptForStorage = _chunk3QRVP4JEcjs.encryptForStorage; exports.measurePushPayload = _chunk3QRVP4JEcjs.measurePushPayload; exports.runScheduledTick = _chunk3QRVP4JEcjs.runScheduledTick;
|
package/dist/cloudflare.d.cts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { M as MAX_PUSH_PAYLOAD_BYTES, W as WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES, f as WEB_PUSH_MAX_BODY_BYTES, h as createD1Adapter, j as createSingleUserCloudflareWorker, k as createSingleUserServer, l as createWebCryptoWebPush, m as decryptFromStorage, n as decryptPayload, o as deriveUserEncryptionKey, q as encryptForStorage, v as measurePushPayload, r as runScheduledTick } from './cloudflare-
|
|
1
|
+
export { M as MAX_PUSH_PAYLOAD_BYTES, W as WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES, f as WEB_PUSH_MAX_BODY_BYTES, h as createD1Adapter, j as createSingleUserCloudflareWorker, k as createSingleUserServer, l as createWebCryptoWebPush, m as decryptFromStorage, n as decryptPayload, o as deriveUserEncryptionKey, q as encryptForStorage, v as measurePushPayload, r as runScheduledTick } from './cloudflare-kUY6z-k5.cjs';
|
|
2
2
|
import '@rei-standard/amsg-shared';
|
package/dist/cloudflare.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { M as MAX_PUSH_PAYLOAD_BYTES, W as WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES, f as WEB_PUSH_MAX_BODY_BYTES, h as createD1Adapter, j as createSingleUserCloudflareWorker, k as createSingleUserServer, l as createWebCryptoWebPush, m as decryptFromStorage, n as decryptPayload, o as deriveUserEncryptionKey, q as encryptForStorage, v as measurePushPayload, r as runScheduledTick } from './cloudflare-
|
|
1
|
+
export { M as MAX_PUSH_PAYLOAD_BYTES, W as WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES, f as WEB_PUSH_MAX_BODY_BYTES, h as createD1Adapter, j as createSingleUserCloudflareWorker, k as createSingleUserServer, l as createWebCryptoWebPush, m as decryptFromStorage, n as decryptPayload, o as deriveUserEncryptionKey, q as encryptForStorage, v as measurePushPayload, r as runScheduledTick } from './cloudflare-kUY6z-k5.js';
|
|
2
2
|
import '@rei-standard/amsg-shared';
|
package/dist/cloudflare.mjs
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
var
|
|
30
|
+
var _chunk3QRVP4JEcjs = require('./chunk-3QRVP4JE.cjs');
|
|
31
31
|
|
|
32
32
|
// src/server/adapters/factory.js
|
|
33
33
|
async function createAdapter(config) {
|
|
@@ -60,7 +60,7 @@ async function createAdapter(config) {
|
|
|
60
60
|
// src/server/handlers/init-tenant.js
|
|
61
61
|
function createInitTenantHandler(ctx) {
|
|
62
62
|
async function POST(headers, body) {
|
|
63
|
-
const initSecret =
|
|
63
|
+
const initSecret = _chunk3QRVP4JEcjs.getHeader.call(void 0, headers, "x-init-secret");
|
|
64
64
|
if (ctx.tenant.initSecret && initSecret !== ctx.tenant.initSecret) {
|
|
65
65
|
return {
|
|
66
66
|
status: 401,
|
|
@@ -73,7 +73,7 @@ function createInitTenantHandler(ctx) {
|
|
|
73
73
|
}
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
|
-
const parsed =
|
|
76
|
+
const parsed = _chunk3QRVP4JEcjs.parseJsonBody.call(void 0, body);
|
|
77
77
|
if (!parsed.ok) {
|
|
78
78
|
return {
|
|
79
79
|
status: 400,
|
|
@@ -87,7 +87,7 @@ function createInitTenantHandler(ctx) {
|
|
|
87
87
|
const tenantId = payload.tenantId || void 0;
|
|
88
88
|
const driver = payload.driver;
|
|
89
89
|
const databaseUrl = payload.databaseUrl || payload.connectionString;
|
|
90
|
-
if (tenantId && !
|
|
90
|
+
if (tenantId && !_chunk3QRVP4JEcjs.isValidUUIDv4.call(void 0, tenantId)) {
|
|
91
91
|
return {
|
|
92
92
|
status: 400,
|
|
93
93
|
body: {
|
|
@@ -207,7 +207,7 @@ function createSendNotificationsHandler(ctx) {
|
|
|
207
207
|
}
|
|
208
208
|
};
|
|
209
209
|
}
|
|
210
|
-
const data = await
|
|
210
|
+
const data = await _chunk3QRVP4JEcjs.runScheduledTick.call(void 0, { ...ctx, db, masterKey });
|
|
211
211
|
return { status: 200, body: { success: true, data } };
|
|
212
212
|
}
|
|
213
213
|
return { POST };
|
|
@@ -579,12 +579,12 @@ async function createReiServer(config) {
|
|
|
579
579
|
return {
|
|
580
580
|
handlers: {
|
|
581
581
|
initTenant: createInitTenantHandler(ctx),
|
|
582
|
-
getUserKey:
|
|
583
|
-
scheduleMessage:
|
|
582
|
+
getUserKey: _chunk3QRVP4JEcjs.createGetUserKeyHandler.call(void 0, ctx),
|
|
583
|
+
scheduleMessage: _chunk3QRVP4JEcjs.createScheduleMessageHandler.call(void 0, ctx),
|
|
584
584
|
sendNotifications: createSendNotificationsHandler(ctx),
|
|
585
|
-
updateMessage:
|
|
586
|
-
cancelMessage:
|
|
587
|
-
messages:
|
|
585
|
+
updateMessage: _chunk3QRVP4JEcjs.createUpdateMessageHandler.call(void 0, ctx),
|
|
586
|
+
cancelMessage: _chunk3QRVP4JEcjs.createCancelMessageHandler.call(void 0, ctx),
|
|
587
|
+
messages: _chunk3QRVP4JEcjs.createMessagesHandler.call(void 0, ctx)
|
|
588
588
|
}
|
|
589
589
|
};
|
|
590
590
|
}
|
|
@@ -614,4 +614,4 @@ async function createReiServer(config) {
|
|
|
614
614
|
|
|
615
615
|
|
|
616
616
|
|
|
617
|
-
exports.MAX_PUSH_PAYLOAD_BYTES =
|
|
617
|
+
exports.MAX_PUSH_PAYLOAD_BYTES = _chunk3QRVP4JEcjs.MAX_PUSH_PAYLOAD_BYTES; exports.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES = _chunk3QRVP4JEcjs.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES; exports.WEB_PUSH_MAX_BODY_BYTES = _chunk3QRVP4JEcjs.WEB_PUSH_MAX_BODY_BYTES; exports.createAdapter = createAdapter; exports.createD1Adapter = _chunk3QRVP4JEcjs.createD1Adapter; exports.createReiServer = createReiServer; exports.createSingleUserCloudflareWorker = _chunk3QRVP4JEcjs.createSingleUserCloudflareWorker; exports.createSingleUserServer = _chunk3QRVP4JEcjs.createSingleUserServer; exports.createTenantToken = createTenantToken; exports.createWebCryptoWebPush = _chunk3QRVP4JEcjs.createWebCryptoWebPush; exports.decryptFromStorage = _chunk3QRVP4JEcjs.decryptFromStorage; exports.decryptPayload = _chunk3QRVP4JEcjs.decryptPayload; exports.deriveUserEncryptionKey = _chunk3QRVP4JEcjs.deriveUserEncryptionKey; exports.encryptForStorage = _chunk3QRVP4JEcjs.encryptForStorage; exports.isValidISO8601 = _chunk3QRVP4JEcjs.isValidISO8601; exports.isValidUUID = _chunk3QRVP4JEcjs.isValidUUID; exports.isValidUUIDv4 = _chunk3QRVP4JEcjs.isValidUUIDv4; exports.isValidUrl = _chunk3QRVP4JEcjs.isValidUrl; exports.measurePushPayload = _chunk3QRVP4JEcjs.measurePushPayload; exports.runScheduledTick = _chunk3QRVP4JEcjs.runScheduledTick; exports.validateAvatarUrl = _chunk3QRVP4JEcjs.validateAvatarUrl; exports.validateLlmMessagesArray = _chunk3QRVP4JEcjs.validateLlmMessagesArray; exports.validateScheduleMessagePayload = _chunk3QRVP4JEcjs.validateScheduleMessagePayload; exports.validateSplitPattern = _chunk3QRVP4JEcjs.validateSplitPattern; exports.verifyTenantToken = verifyTenantToken;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { g as getHeader, p as parseJsonBody, i as isValidUUIDv4, r as runScheduledTick, c as createMessagesHandler, a as createCancelMessageHandler, b as createUpdateMessageHandler, d as createScheduleMessageHandler, e as createGetUserKeyHandler } from './cloudflare-
|
|
2
|
-
export { M as MAX_PUSH_PAYLOAD_BYTES, W as WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES, f as WEB_PUSH_MAX_BODY_BYTES, h as createD1Adapter, j as createSingleUserCloudflareWorker, k as createSingleUserServer, l as createWebCryptoWebPush, m as decryptFromStorage, n as decryptPayload, o as deriveUserEncryptionKey, q as encryptForStorage, s as isValidISO8601, t as isValidUUID, u as isValidUrl, v as measurePushPayload, w as validateLlmMessagesArray, x as validateScheduleMessagePayload, y as validateSplitPattern } from './cloudflare-
|
|
1
|
+
import { g as getHeader, p as parseJsonBody, i as isValidUUIDv4, r as runScheduledTick, c as createMessagesHandler, a as createCancelMessageHandler, b as createUpdateMessageHandler, d as createScheduleMessageHandler, e as createGetUserKeyHandler } from './cloudflare-kUY6z-k5.cjs';
|
|
2
|
+
export { M as MAX_PUSH_PAYLOAD_BYTES, W as WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES, f as WEB_PUSH_MAX_BODY_BYTES, h as createD1Adapter, j as createSingleUserCloudflareWorker, k as createSingleUserServer, l as createWebCryptoWebPush, m as decryptFromStorage, n as decryptPayload, o as deriveUserEncryptionKey, q as encryptForStorage, s as isValidISO8601, t as isValidUUID, u as isValidUrl, v as measurePushPayload, w as validateLlmMessagesArray, x as validateScheduleMessagePayload, y as validateSplitPattern } from './cloudflare-kUY6z-k5.cjs';
|
|
3
3
|
import { createHash, randomBytes, createCipheriv, createDecipheriv, timingSafeEqual, createHmac, randomUUID } from 'crypto';
|
|
4
4
|
import { normalizeVapidSubject } from '@rei-standard/amsg-shared';
|
|
5
5
|
export { validateAvatarUrl } from '@rei-standard/amsg-shared';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { g as getHeader, p as parseJsonBody, i as isValidUUIDv4, r as runScheduledTick, c as createMessagesHandler, a as createCancelMessageHandler, b as createUpdateMessageHandler, d as createScheduleMessageHandler, e as createGetUserKeyHandler } from './cloudflare-
|
|
2
|
-
export { M as MAX_PUSH_PAYLOAD_BYTES, W as WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES, f as WEB_PUSH_MAX_BODY_BYTES, h as createD1Adapter, j as createSingleUserCloudflareWorker, k as createSingleUserServer, l as createWebCryptoWebPush, m as decryptFromStorage, n as decryptPayload, o as deriveUserEncryptionKey, q as encryptForStorage, s as isValidISO8601, t as isValidUUID, u as isValidUrl, v as measurePushPayload, w as validateLlmMessagesArray, x as validateScheduleMessagePayload, y as validateSplitPattern } from './cloudflare-
|
|
1
|
+
import { g as getHeader, p as parseJsonBody, i as isValidUUIDv4, r as runScheduledTick, c as createMessagesHandler, a as createCancelMessageHandler, b as createUpdateMessageHandler, d as createScheduleMessageHandler, e as createGetUserKeyHandler } from './cloudflare-kUY6z-k5.js';
|
|
2
|
+
export { M as MAX_PUSH_PAYLOAD_BYTES, W as WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES, f as WEB_PUSH_MAX_BODY_BYTES, h as createD1Adapter, j as createSingleUserCloudflareWorker, k as createSingleUserServer, l as createWebCryptoWebPush, m as decryptFromStorage, n as decryptPayload, o as deriveUserEncryptionKey, q as encryptForStorage, s as isValidISO8601, t as isValidUUID, u as isValidUrl, v as measurePushPayload, w as validateLlmMessagesArray, x as validateScheduleMessagePayload, y as validateSplitPattern } from './cloudflare-kUY6z-k5.js';
|
|
3
3
|
import { createHash, randomBytes, createCipheriv, createDecipheriv, timingSafeEqual, createHmac, randomUUID } from 'crypto';
|
|
4
4
|
import { normalizeVapidSubject } from '@rei-standard/amsg-shared';
|
|
5
5
|
export { validateAvatarUrl } from '@rei-standard/amsg-shared';
|
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rei-standard/amsg-server",
|
|
3
|
-
"version": "2.6.0-next.
|
|
3
|
+
"version": "2.6.0-next.9",
|
|
4
4
|
"description": "ReiStandard Active Messaging server SDK with pluggable database adapters. Three-axis push schema (messageKind / messageType / messageSubtype) from @rei-standard/amsg-shared. Auto-emits ReasoningPush when the LLM response carries reasoning_content.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|