larkin 0.4.13 → 0.4.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/agent-state-store.cjs +237 -0
- package/dist/agent/agent-state-store.mjs +238 -1
- package/dist/agent/context-prompt.cjs +2 -1
- package/dist/agent/context-prompt.mjs +2 -1
- package/dist/agent/host-reminder-orchestrator.cjs +134 -7
- package/dist/agent/host-reminder-orchestrator.mjs +134 -7
- package/dist/agent/inbox-projection.cjs +5 -0
- package/dist/agent/inbox-projection.mjs +4 -0
- package/dist/agent/reminder-delivery-audit.cjs +86 -0
- package/dist/agent/reminder-delivery-audit.mjs +50 -0
- package/dist/agent/reminder-routes.cjs +83 -3
- package/dist/agent/reminder-routes.mjs +83 -3
- package/dist/agent/reminder-store.cjs +5 -2
- package/dist/agent/reminder-store.mjs +5 -2
- package/dist/agent/transport-business-context.cjs +26 -2
- package/dist/agent/transport-business-context.mjs +26 -2
- package/dist/app/agent-cli.mjs +33 -2
- package/dist/app/lark-cli.mjs +79 -11
- package/dist/feishu/host-business-state.cjs +35 -14
- package/dist/feishu/host-business-state.mjs +35 -14
- package/dist/feishu/interaction-orchestrator.cjs +8 -1
- package/dist/feishu/interaction-orchestrator.mjs +8 -1
- package/dist/feishu/outbound-transport.cjs +37 -3
- package/dist/feishu/outbound-transport.mjs +37 -3
- package/dist/runtime/runtime-host.cjs +37 -0
- package/dist/runtime/runtime-host.mjs +37 -0
- package/package.json +1 -1
|
@@ -105,25 +105,150 @@ class HostReminderOrchestrator {
|
|
|
105
105
|
}
|
|
106
106
|
deliver(agent, reminder, overdueMs) {
|
|
107
107
|
const recurrence = reminder.repeat ? this.store.parseRepeat(reminder.repeat, reminder.tz) : null;
|
|
108
|
-
const
|
|
108
|
+
const projected = this.options.envelopeProjector.createReminderEnvelope(agent.agentId, {
|
|
109
109
|
reminderId: reminder.reminderId,
|
|
110
110
|
title: reminder.title,
|
|
111
111
|
repeat: reminder.repeat ? String(reminder.repeat) : null,
|
|
112
112
|
fireAt: reminder.fireAt,
|
|
113
113
|
msgRef: reminder.msgRef,
|
|
114
114
|
channel: reminder.channel ? String(reminder.channel) : null,
|
|
115
|
-
|
|
115
|
+
deliveryTarget: reminder.deliveryTarget ?? null,
|
|
116
|
+
deliveryAnchor: reminder.deliveryAnchor ?? null,
|
|
117
|
+
}, overdueMs, reminder.repeat && recurrence && !("error" in recurrence) ? (recurrence.description || null) : null);
|
|
118
|
+
const envelope = withCanonicalTarget({ ...projected,
|
|
119
|
+
// The projector also derives safe routing for pre-0.4.14 records; do not
|
|
120
|
+
// overwrite that migration result with the absent fields from the record.
|
|
121
|
+
deliveryTarget: reminder.deliveryTarget ?? projected.deliveryTarget ?? null,
|
|
122
|
+
deliveryAnchor: reminder.deliveryAnchor ?? projected.deliveryAnchor ?? null });
|
|
123
|
+
const auditableDelivery = typeof envelope.deliveryTarget === "string" && envelope.deliveryTarget.length > 0;
|
|
124
|
+
// Persist the pending audit marker before the Inbox row becomes pollable.
|
|
125
|
+
// An already-active Agent can poll and answer the wake immediately, and
|
|
126
|
+
// RuntimeHost.deliver may execute the whole turn before its promise settles
|
|
127
|
+
// (notably with Pi); without a durable pending marker that outbound write
|
|
128
|
+
// would be invisible to the audit. Internal/no-delivery reminders still
|
|
129
|
+
// wake the Runtime, but have no user-facing delivery to audit.
|
|
130
|
+
if (auditableDelivery && !this.recordDeliveryOutcome(agent, reminder.reminderId, "delivery_pending", {
|
|
131
|
+
outcome: "awaiting_runtime", deliveryTarget: envelope.deliveryTarget, occurrenceId: envelope.message_id,
|
|
132
|
+
})) {
|
|
133
|
+
this.log(`reminder delivery pending 审计写失败 id=#${reminder.reminderId.slice(0, 8)}(拒绝唤醒)`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
let stateStore;
|
|
137
|
+
let deliveryAnchorBound = false;
|
|
116
138
|
try {
|
|
117
|
-
this.options.stateStore(agent)
|
|
139
|
+
stateStore = this.options.stateStore(agent);
|
|
140
|
+
if (envelope.deliveryTarget && envelope.deliveryAnchor) {
|
|
141
|
+
stateStore.bindInboxDeliveryAnchor(envelope.deliveryAnchor, envelope.deliveryTarget);
|
|
142
|
+
deliveryAnchorBound = true;
|
|
143
|
+
}
|
|
144
|
+
stateStore.appendNdjson("inbox", envelope);
|
|
118
145
|
}
|
|
119
146
|
catch (error) {
|
|
147
|
+
if (deliveryAnchorBound && stateStore?.unbindInboxDeliveryAnchor && envelope.deliveryTarget && envelope.deliveryAnchor) {
|
|
148
|
+
try {
|
|
149
|
+
stateStore.unbindInboxDeliveryAnchor(envelope.deliveryAnchor, envelope.deliveryTarget);
|
|
150
|
+
}
|
|
151
|
+
catch (rollbackError) {
|
|
152
|
+
this.log("reminder Inbox anchor 回滚失败", rollbackError.message);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
120
155
|
this.log("reminder inbox 写失败", error.message);
|
|
156
|
+
// The wake row never became durable, so the pending marker must not stay
|
|
157
|
+
// open awaiting a Runtime turn that can never poll this occurrence.
|
|
158
|
+
if (auditableDelivery) {
|
|
159
|
+
this.recordDeliveryOutcome(agent, reminder.reminderId, "delivery_failed", {
|
|
160
|
+
outcome: "inbox_write_failed", reason: String(error.message || error),
|
|
161
|
+
deliveryTarget: envelope.deliveryTarget, occurrenceId: envelope.message_id,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
121
164
|
return;
|
|
122
165
|
}
|
|
123
|
-
if (this.options.deliveryTarget)
|
|
124
|
-
void this.options.deliveryTarget.deliver(agent.agentId, envelope);
|
|
125
|
-
else
|
|
166
|
+
if (!this.options.deliveryTarget) {
|
|
126
167
|
this.log(`reminder 触发但 Runtime Host 未就绪 id=#${reminder.reminderId.slice(0, 8)}(仅入 inbox)`);
|
|
168
|
+
if (auditableDelivery) {
|
|
169
|
+
this.recordDeliveryOutcome(agent, reminder.reminderId, "delivery_failed", {
|
|
170
|
+
outcome: "runtime_unavailable", occurrenceId: envelope.message_id,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
const recordReceipt = (receipt) => {
|
|
177
|
+
if (!auditableDelivery)
|
|
178
|
+
return;
|
|
179
|
+
const status = receipt && typeof receipt === "object" ? String(receipt.status || "") : "";
|
|
180
|
+
const reason = receipt && typeof receipt === "object" ? String(receipt.reason || "") : "";
|
|
181
|
+
// RuntimeHost.deliver only acknowledges Inbox persistence/wake submission.
|
|
182
|
+
// It does not prove that the Agent later committed an outbound Feishu
|
|
183
|
+
// message, so accepted, duplicate, and an empty receipt remain pending.
|
|
184
|
+
if (!status || status === "accepted" || status === "duplicate") {
|
|
185
|
+
this.recordDeliveryOutcome(agent, reminder.reminderId, "delivery_pending", {
|
|
186
|
+
outcome: status || "empty", deliveryTarget: envelope.deliveryTarget, occurrenceId: envelope.message_id,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
else if (status === "deferred") {
|
|
190
|
+
this.recordDeliveryOutcome(agent, reminder.reminderId, "delivery_pending", {
|
|
191
|
+
outcome: "deferred", ...(reason ? { reason } : {}), deliveryTarget: envelope.deliveryTarget, occurrenceId: envelope.message_id,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
this.recordDeliveryOutcome(agent, reminder.reminderId, "delivery_failed", {
|
|
196
|
+
outcome: "error", ...(reason ? { reason } : {}), deliveryTarget: envelope.deliveryTarget, occurrenceId: envelope.message_id,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
const result = this.options.deliveryTarget.deliver(agent.agentId, envelope);
|
|
201
|
+
if (result && typeof result.then === "function") {
|
|
202
|
+
void Promise.resolve(result).then(recordReceipt, (error) => this.recordDeliveryOutcome(agent, reminder.reminderId, "delivery_failed", {
|
|
203
|
+
outcome: "rejected", reason: String(error?.message || error), deliveryTarget: envelope.deliveryTarget, occurrenceId: envelope.message_id,
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
recordReceipt(result);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
if (typeof envelope.deliveryTarget === "string" && envelope.deliveryTarget.length > 0) {
|
|
212
|
+
this.recordDeliveryOutcome(agent, reminder.reminderId, "delivery_failed", {
|
|
213
|
+
outcome: "rejected", reason: String(error.message || error), deliveryTarget: envelope.deliveryTarget,
|
|
214
|
+
occurrenceId: envelope.message_id,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
this.log(`reminder delivery 失败 id=#${reminder.reminderId.slice(0, 8)}: ${error.message}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
recordDeliveryOutcome(agent, reminderId, eventType, metadata) {
|
|
221
|
+
try {
|
|
222
|
+
return this.store.mutate(this.options.stateStore(agent).paths.reminders, (store) => {
|
|
223
|
+
const reminder = store.reminders.find((candidate) => candidate.reminderId === reminderId);
|
|
224
|
+
if (!reminder)
|
|
225
|
+
return false;
|
|
226
|
+
const occurrenceId = typeof metadata.occurrenceId === "string" ? metadata.occurrenceId : null;
|
|
227
|
+
const occurrenceEvents = occurrenceId
|
|
228
|
+
? reminder.events?.filter((event) => event.metadata?.occurrenceId === occurrenceId)
|
|
229
|
+
: undefined;
|
|
230
|
+
const hasOccurrenceMetadata = reminder.events?.some((event) => typeof event.metadata?.occurrenceId === "string");
|
|
231
|
+
const last = occurrenceId
|
|
232
|
+
? (occurrenceEvents?.at(-1) ?? (hasOccurrenceMetadata && reminder.events?.at(-1)?.eventType !== "fired"
|
|
233
|
+
? undefined : reminder.events?.at(-1)))
|
|
234
|
+
: reminder.events?.at(-1);
|
|
235
|
+
if (!last || !["fired", "delivery_pending", "delivery_failed"].includes(last.eventType))
|
|
236
|
+
return false;
|
|
237
|
+
// A late Runtime receipt must not resurrect an occurrence finalized as failed.
|
|
238
|
+
if (eventType === "delivery_pending" && last.eventType === "delivery_failed")
|
|
239
|
+
return false;
|
|
240
|
+
// A synchronous outbound can finish inside deliveryTarget.deliver().
|
|
241
|
+
// Never let its later Runtime receipt callback downgrade that success.
|
|
242
|
+
if (eventType !== "delivery_succeeded" && last.eventType === "delivery_succeeded")
|
|
243
|
+
return false;
|
|
244
|
+
this.store.appendEvent(reminder, eventType, "system", null, reminder.status === "scheduled" ? reminder.fireAt : null, this.now(), metadata);
|
|
245
|
+
return true;
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
this.log(`reminder delivery audit 写失败 id=#${reminderId.slice(0, 8)}: ${error.message}`);
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
127
252
|
}
|
|
128
253
|
handleFire(message) {
|
|
129
254
|
const agent = this.options.agents.find((candidate) => candidate.agentId === message.agentId);
|
|
@@ -163,7 +288,9 @@ class HostReminderOrchestrator {
|
|
|
163
288
|
reminder.firedAt = this.store.nowIso(now);
|
|
164
289
|
}
|
|
165
290
|
reminder.version = Number(reminder.version) + 1;
|
|
166
|
-
this.store.appendEvent(reminder, "fired", "system", null, reminder.status === "scheduled" ? reminder.fireAt : null, now
|
|
291
|
+
this.store.appendEvent(reminder, "fired", "system", null, reminder.status === "scheduled" ? reminder.fireAt : null, now, {
|
|
292
|
+
deliveryTarget: reminder.deliveryTarget ?? null, deliveryAnchor: reminder.deliveryAnchor ?? null,
|
|
293
|
+
});
|
|
167
294
|
store.reminders = store.reminders.filter((candidate) => candidate.status === "scheduled" || now - Date.parse(candidate.firedAt || candidate.createdAt || "") < 30 * 86_400_000);
|
|
168
295
|
action = "deliver";
|
|
169
296
|
fired = { ...reminder };
|
|
@@ -69,25 +69,150 @@ export class HostReminderOrchestrator {
|
|
|
69
69
|
}
|
|
70
70
|
deliver(agent, reminder, overdueMs) {
|
|
71
71
|
const recurrence = reminder.repeat ? this.store.parseRepeat(reminder.repeat, reminder.tz) : null;
|
|
72
|
-
const
|
|
72
|
+
const projected = this.options.envelopeProjector.createReminderEnvelope(agent.agentId, {
|
|
73
73
|
reminderId: reminder.reminderId,
|
|
74
74
|
title: reminder.title,
|
|
75
75
|
repeat: reminder.repeat ? String(reminder.repeat) : null,
|
|
76
76
|
fireAt: reminder.fireAt,
|
|
77
77
|
msgRef: reminder.msgRef,
|
|
78
78
|
channel: reminder.channel ? String(reminder.channel) : null,
|
|
79
|
-
|
|
79
|
+
deliveryTarget: reminder.deliveryTarget ?? null,
|
|
80
|
+
deliveryAnchor: reminder.deliveryAnchor ?? null,
|
|
81
|
+
}, overdueMs, reminder.repeat && recurrence && !("error" in recurrence) ? (recurrence.description || null) : null);
|
|
82
|
+
const envelope = withCanonicalTarget({ ...projected,
|
|
83
|
+
// The projector also derives safe routing for pre-0.4.14 records; do not
|
|
84
|
+
// overwrite that migration result with the absent fields from the record.
|
|
85
|
+
deliveryTarget: reminder.deliveryTarget ?? projected.deliveryTarget ?? null,
|
|
86
|
+
deliveryAnchor: reminder.deliveryAnchor ?? projected.deliveryAnchor ?? null });
|
|
87
|
+
const auditableDelivery = typeof envelope.deliveryTarget === "string" && envelope.deliveryTarget.length > 0;
|
|
88
|
+
// Persist the pending audit marker before the Inbox row becomes pollable.
|
|
89
|
+
// An already-active Agent can poll and answer the wake immediately, and
|
|
90
|
+
// RuntimeHost.deliver may execute the whole turn before its promise settles
|
|
91
|
+
// (notably with Pi); without a durable pending marker that outbound write
|
|
92
|
+
// would be invisible to the audit. Internal/no-delivery reminders still
|
|
93
|
+
// wake the Runtime, but have no user-facing delivery to audit.
|
|
94
|
+
if (auditableDelivery && !this.recordDeliveryOutcome(agent, reminder.reminderId, "delivery_pending", {
|
|
95
|
+
outcome: "awaiting_runtime", deliveryTarget: envelope.deliveryTarget, occurrenceId: envelope.message_id,
|
|
96
|
+
})) {
|
|
97
|
+
this.log(`reminder delivery pending 审计写失败 id=#${reminder.reminderId.slice(0, 8)}(拒绝唤醒)`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
let stateStore;
|
|
101
|
+
let deliveryAnchorBound = false;
|
|
80
102
|
try {
|
|
81
|
-
this.options.stateStore(agent)
|
|
103
|
+
stateStore = this.options.stateStore(agent);
|
|
104
|
+
if (envelope.deliveryTarget && envelope.deliveryAnchor) {
|
|
105
|
+
stateStore.bindInboxDeliveryAnchor(envelope.deliveryAnchor, envelope.deliveryTarget);
|
|
106
|
+
deliveryAnchorBound = true;
|
|
107
|
+
}
|
|
108
|
+
stateStore.appendNdjson("inbox", envelope);
|
|
82
109
|
}
|
|
83
110
|
catch (error) {
|
|
111
|
+
if (deliveryAnchorBound && stateStore?.unbindInboxDeliveryAnchor && envelope.deliveryTarget && envelope.deliveryAnchor) {
|
|
112
|
+
try {
|
|
113
|
+
stateStore.unbindInboxDeliveryAnchor(envelope.deliveryAnchor, envelope.deliveryTarget);
|
|
114
|
+
}
|
|
115
|
+
catch (rollbackError) {
|
|
116
|
+
this.log("reminder Inbox anchor 回滚失败", rollbackError.message);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
84
119
|
this.log("reminder inbox 写失败", error.message);
|
|
120
|
+
// The wake row never became durable, so the pending marker must not stay
|
|
121
|
+
// open awaiting a Runtime turn that can never poll this occurrence.
|
|
122
|
+
if (auditableDelivery) {
|
|
123
|
+
this.recordDeliveryOutcome(agent, reminder.reminderId, "delivery_failed", {
|
|
124
|
+
outcome: "inbox_write_failed", reason: String(error.message || error),
|
|
125
|
+
deliveryTarget: envelope.deliveryTarget, occurrenceId: envelope.message_id,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
85
128
|
return;
|
|
86
129
|
}
|
|
87
|
-
if (this.options.deliveryTarget)
|
|
88
|
-
void this.options.deliveryTarget.deliver(agent.agentId, envelope);
|
|
89
|
-
else
|
|
130
|
+
if (!this.options.deliveryTarget) {
|
|
90
131
|
this.log(`reminder 触发但 Runtime Host 未就绪 id=#${reminder.reminderId.slice(0, 8)}(仅入 inbox)`);
|
|
132
|
+
if (auditableDelivery) {
|
|
133
|
+
this.recordDeliveryOutcome(agent, reminder.reminderId, "delivery_failed", {
|
|
134
|
+
outcome: "runtime_unavailable", occurrenceId: envelope.message_id,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
const recordReceipt = (receipt) => {
|
|
141
|
+
if (!auditableDelivery)
|
|
142
|
+
return;
|
|
143
|
+
const status = receipt && typeof receipt === "object" ? String(receipt.status || "") : "";
|
|
144
|
+
const reason = receipt && typeof receipt === "object" ? String(receipt.reason || "") : "";
|
|
145
|
+
// RuntimeHost.deliver only acknowledges Inbox persistence/wake submission.
|
|
146
|
+
// It does not prove that the Agent later committed an outbound Feishu
|
|
147
|
+
// message, so accepted, duplicate, and an empty receipt remain pending.
|
|
148
|
+
if (!status || status === "accepted" || status === "duplicate") {
|
|
149
|
+
this.recordDeliveryOutcome(agent, reminder.reminderId, "delivery_pending", {
|
|
150
|
+
outcome: status || "empty", deliveryTarget: envelope.deliveryTarget, occurrenceId: envelope.message_id,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
else if (status === "deferred") {
|
|
154
|
+
this.recordDeliveryOutcome(agent, reminder.reminderId, "delivery_pending", {
|
|
155
|
+
outcome: "deferred", ...(reason ? { reason } : {}), deliveryTarget: envelope.deliveryTarget, occurrenceId: envelope.message_id,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
this.recordDeliveryOutcome(agent, reminder.reminderId, "delivery_failed", {
|
|
160
|
+
outcome: "error", ...(reason ? { reason } : {}), deliveryTarget: envelope.deliveryTarget, occurrenceId: envelope.message_id,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
const result = this.options.deliveryTarget.deliver(agent.agentId, envelope);
|
|
165
|
+
if (result && typeof result.then === "function") {
|
|
166
|
+
void Promise.resolve(result).then(recordReceipt, (error) => this.recordDeliveryOutcome(agent, reminder.reminderId, "delivery_failed", {
|
|
167
|
+
outcome: "rejected", reason: String(error?.message || error), deliveryTarget: envelope.deliveryTarget, occurrenceId: envelope.message_id,
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
recordReceipt(result);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
if (typeof envelope.deliveryTarget === "string" && envelope.deliveryTarget.length > 0) {
|
|
176
|
+
this.recordDeliveryOutcome(agent, reminder.reminderId, "delivery_failed", {
|
|
177
|
+
outcome: "rejected", reason: String(error.message || error), deliveryTarget: envelope.deliveryTarget,
|
|
178
|
+
occurrenceId: envelope.message_id,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
this.log(`reminder delivery 失败 id=#${reminder.reminderId.slice(0, 8)}: ${error.message}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
recordDeliveryOutcome(agent, reminderId, eventType, metadata) {
|
|
185
|
+
try {
|
|
186
|
+
return this.store.mutate(this.options.stateStore(agent).paths.reminders, (store) => {
|
|
187
|
+
const reminder = store.reminders.find((candidate) => candidate.reminderId === reminderId);
|
|
188
|
+
if (!reminder)
|
|
189
|
+
return false;
|
|
190
|
+
const occurrenceId = typeof metadata.occurrenceId === "string" ? metadata.occurrenceId : null;
|
|
191
|
+
const occurrenceEvents = occurrenceId
|
|
192
|
+
? reminder.events?.filter((event) => event.metadata?.occurrenceId === occurrenceId)
|
|
193
|
+
: undefined;
|
|
194
|
+
const hasOccurrenceMetadata = reminder.events?.some((event) => typeof event.metadata?.occurrenceId === "string");
|
|
195
|
+
const last = occurrenceId
|
|
196
|
+
? (occurrenceEvents?.at(-1) ?? (hasOccurrenceMetadata && reminder.events?.at(-1)?.eventType !== "fired"
|
|
197
|
+
? undefined : reminder.events?.at(-1)))
|
|
198
|
+
: reminder.events?.at(-1);
|
|
199
|
+
if (!last || !["fired", "delivery_pending", "delivery_failed"].includes(last.eventType))
|
|
200
|
+
return false;
|
|
201
|
+
// A late Runtime receipt must not resurrect an occurrence finalized as failed.
|
|
202
|
+
if (eventType === "delivery_pending" && last.eventType === "delivery_failed")
|
|
203
|
+
return false;
|
|
204
|
+
// A synchronous outbound can finish inside deliveryTarget.deliver().
|
|
205
|
+
// Never let its later Runtime receipt callback downgrade that success.
|
|
206
|
+
if (eventType !== "delivery_succeeded" && last.eventType === "delivery_succeeded")
|
|
207
|
+
return false;
|
|
208
|
+
this.store.appendEvent(reminder, eventType, "system", null, reminder.status === "scheduled" ? reminder.fireAt : null, this.now(), metadata);
|
|
209
|
+
return true;
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
this.log(`reminder delivery audit 写失败 id=#${reminderId.slice(0, 8)}: ${error.message}`);
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
91
216
|
}
|
|
92
217
|
handleFire(message) {
|
|
93
218
|
const agent = this.options.agents.find((candidate) => candidate.agentId === message.agentId);
|
|
@@ -127,7 +252,9 @@ export class HostReminderOrchestrator {
|
|
|
127
252
|
reminder.firedAt = this.store.nowIso(now);
|
|
128
253
|
}
|
|
129
254
|
reminder.version = Number(reminder.version) + 1;
|
|
130
|
-
this.store.appendEvent(reminder, "fired", "system", null, reminder.status === "scheduled" ? reminder.fireAt : null, now
|
|
255
|
+
this.store.appendEvent(reminder, "fired", "system", null, reminder.status === "scheduled" ? reminder.fireAt : null, now, {
|
|
256
|
+
deliveryTarget: reminder.deliveryTarget ?? null, deliveryAnchor: reminder.deliveryAnchor ?? null,
|
|
257
|
+
});
|
|
131
258
|
store.reminders = store.reminders.filter((candidate) => candidate.status === "scheduled" || now - Date.parse(candidate.firedAt || candidate.createdAt || "") < 30 * 86_400_000);
|
|
132
259
|
action = "deliver";
|
|
133
260
|
fired = { ...reminder };
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.RUNTIME_REDELIVERY_TARGET = exports.RUNTIME_REMINDER_TARGET = void 0;
|
|
4
4
|
exports.isCanonicalInboxTarget = isCanonicalInboxTarget;
|
|
5
|
+
exports.isUserDeliveryTarget = isUserDeliveryTarget;
|
|
5
6
|
exports.targetKeyOfInboxEnvelope = targetKeyOfInboxEnvelope;
|
|
6
7
|
exports.projectInboxEnvelope = projectInboxEnvelope;
|
|
7
8
|
exports.targetOfInboxEnvelope = targetOfInboxEnvelope;
|
|
@@ -50,6 +51,10 @@ function isCanonicalInboxTarget(target) {
|
|
|
50
51
|
|| target === exports.RUNTIME_REDELIVERY_TARGET
|
|
51
52
|
|| isDocumentCommentTarget(target);
|
|
52
53
|
}
|
|
54
|
+
/** Targets that can safely receive a user-facing reminder. Runtime wake targets are not delivery destinations. */
|
|
55
|
+
function isUserDeliveryTarget(target) {
|
|
56
|
+
return isChatTarget(target) || isThreadTarget(target) || isDocumentCommentTarget(target);
|
|
57
|
+
}
|
|
53
58
|
function invalidTarget(target) {
|
|
54
59
|
return new Error(`Invalid canonical Inbox target ${JSON.stringify(target)}; expected chat:<nonempty>, thread:<nonempty>, runtime:reminder, runtime:redelivery, or document-comment:<nonempty>`);
|
|
55
60
|
}
|
|
@@ -41,6 +41,10 @@ export function isCanonicalInboxTarget(target) {
|
|
|
41
41
|
|| target === RUNTIME_REDELIVERY_TARGET
|
|
42
42
|
|| isDocumentCommentTarget(target);
|
|
43
43
|
}
|
|
44
|
+
/** Targets that can safely receive a user-facing reminder. Runtime wake targets are not delivery destinations. */
|
|
45
|
+
export function isUserDeliveryTarget(target) {
|
|
46
|
+
return isChatTarget(target) || isThreadTarget(target) || isDocumentCommentTarget(target);
|
|
47
|
+
}
|
|
44
48
|
function invalidTarget(target) {
|
|
45
49
|
return new Error(`Invalid canonical Inbox target ${JSON.stringify(target)}; expected chat:<nonempty>, thread:<nonempty>, runtime:reminder, runtime:redelivery, or document-comment:<nonempty>`);
|
|
46
50
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.auditReminderDelivery = auditReminderDelivery;
|
|
37
|
+
const ReminderStore = __importStar(require("./reminder-store.cjs"));
|
|
38
|
+
/** Record only committed, non-dry-run outbound attempts for the consumed reminder. */
|
|
39
|
+
function auditReminderDelivery(input) {
|
|
40
|
+
if (input.dryRun)
|
|
41
|
+
return;
|
|
42
|
+
const current = (input.stateStore.resolveCurrentReminders?.() ?? [input.stateStore.resolveCurrentReminder()]).filter((candidate) => Boolean(candidate)).find((candidate) => {
|
|
43
|
+
if (input.reminderId && candidate.reminderId !== input.reminderId)
|
|
44
|
+
return false;
|
|
45
|
+
if (input.deliveryAnchor && candidate.deliveryAnchor !== input.deliveryAnchor)
|
|
46
|
+
return false;
|
|
47
|
+
const currentChatId = candidate.deliveryTarget.match(/^chat:(oc_[A-Za-z0-9_-]+)$/)?.[1];
|
|
48
|
+
const outboundChatId = currentChatId && input.resolveChatId ? input.resolveChatId(input.target) : null;
|
|
49
|
+
return candidate.deliveryTarget === input.target || outboundChatId === currentChatId;
|
|
50
|
+
});
|
|
51
|
+
if (!current)
|
|
52
|
+
return;
|
|
53
|
+
// Match events against the consumed occurrence even when the outbound did
|
|
54
|
+
// not name one: a snooze/update/cancel appended after polling must not hide
|
|
55
|
+
// the pending occurrence that this committed write actually fulfilled.
|
|
56
|
+
const anchor = input.deliveryAnchor || current.deliveryAnchor;
|
|
57
|
+
let persisted = false;
|
|
58
|
+
ReminderStore.mutate(input.stateStore.paths.reminders, (store) => {
|
|
59
|
+
const reminder = store.reminders.find((candidate) => candidate.reminderId === current.reminderId);
|
|
60
|
+
const occurrenceEvents = anchor
|
|
61
|
+
? reminder?.events?.filter((event) => event.metadata?.occurrenceId === anchor)
|
|
62
|
+
: undefined;
|
|
63
|
+
const hasOccurrenceMetadata = reminder?.events?.some((event) => typeof event.metadata?.occurrenceId === "string");
|
|
64
|
+
const last = anchor
|
|
65
|
+
? (occurrenceEvents?.at(-1) ?? (hasOccurrenceMetadata ? undefined : reminder?.events?.at(-1)))
|
|
66
|
+
: reminder?.events?.at(-1);
|
|
67
|
+
if (!reminder || !last || !["delivery_pending", "delivery_failed"].includes(last.eventType))
|
|
68
|
+
return;
|
|
69
|
+
if (input.finalize && !input.succeeded && last.eventType === "delivery_failed")
|
|
70
|
+
return;
|
|
71
|
+
if (!input.succeeded && last.eventType === "delivery_failed")
|
|
72
|
+
return;
|
|
73
|
+
ReminderStore.appendEvent(reminder, input.succeeded ? "delivery_succeeded" : "delivery_failed", "agent", input.agentId, reminder.status === "scheduled" ? reminder.fireAt : null, Date.now(), {
|
|
74
|
+
deliveryTarget: input.target,
|
|
75
|
+
occurrenceId: current.deliveryAnchor,
|
|
76
|
+
...((input.messageId || current.deliveryMessageId) ? { messageId: input.messageId || current.deliveryMessageId } : {}),
|
|
77
|
+
...(input.reason ? { reason: input.reason } : {}),
|
|
78
|
+
});
|
|
79
|
+
persisted = true;
|
|
80
|
+
});
|
|
81
|
+
// Clear only after delivery_succeeded was durably appended; otherwise keep
|
|
82
|
+
// the occurrence context so a later reconciliation can still record the
|
|
83
|
+
// committed write instead of silently dropping its audit trail.
|
|
84
|
+
if (input.succeeded && persisted)
|
|
85
|
+
input.stateStore.clearCurrentReminder(current.reminderId, current.deliveryAnchor);
|
|
86
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as ReminderStore from "./reminder-store.mjs";
|
|
2
|
+
/** Record only committed, non-dry-run outbound attempts for the consumed reminder. */
|
|
3
|
+
export function auditReminderDelivery(input) {
|
|
4
|
+
if (input.dryRun)
|
|
5
|
+
return;
|
|
6
|
+
const current = (input.stateStore.resolveCurrentReminders?.() ?? [input.stateStore.resolveCurrentReminder()]).filter((candidate) => Boolean(candidate)).find((candidate) => {
|
|
7
|
+
if (input.reminderId && candidate.reminderId !== input.reminderId)
|
|
8
|
+
return false;
|
|
9
|
+
if (input.deliveryAnchor && candidate.deliveryAnchor !== input.deliveryAnchor)
|
|
10
|
+
return false;
|
|
11
|
+
const currentChatId = candidate.deliveryTarget.match(/^chat:(oc_[A-Za-z0-9_-]+)$/)?.[1];
|
|
12
|
+
const outboundChatId = currentChatId && input.resolveChatId ? input.resolveChatId(input.target) : null;
|
|
13
|
+
return candidate.deliveryTarget === input.target || outboundChatId === currentChatId;
|
|
14
|
+
});
|
|
15
|
+
if (!current)
|
|
16
|
+
return;
|
|
17
|
+
// Match events against the consumed occurrence even when the outbound did
|
|
18
|
+
// not name one: a snooze/update/cancel appended after polling must not hide
|
|
19
|
+
// the pending occurrence that this committed write actually fulfilled.
|
|
20
|
+
const anchor = input.deliveryAnchor || current.deliveryAnchor;
|
|
21
|
+
let persisted = false;
|
|
22
|
+
ReminderStore.mutate(input.stateStore.paths.reminders, (store) => {
|
|
23
|
+
const reminder = store.reminders.find((candidate) => candidate.reminderId === current.reminderId);
|
|
24
|
+
const occurrenceEvents = anchor
|
|
25
|
+
? reminder?.events?.filter((event) => event.metadata?.occurrenceId === anchor)
|
|
26
|
+
: undefined;
|
|
27
|
+
const hasOccurrenceMetadata = reminder?.events?.some((event) => typeof event.metadata?.occurrenceId === "string");
|
|
28
|
+
const last = anchor
|
|
29
|
+
? (occurrenceEvents?.at(-1) ?? (hasOccurrenceMetadata ? undefined : reminder?.events?.at(-1)))
|
|
30
|
+
: reminder?.events?.at(-1);
|
|
31
|
+
if (!reminder || !last || !["delivery_pending", "delivery_failed"].includes(last.eventType))
|
|
32
|
+
return;
|
|
33
|
+
if (input.finalize && !input.succeeded && last.eventType === "delivery_failed")
|
|
34
|
+
return;
|
|
35
|
+
if (!input.succeeded && last.eventType === "delivery_failed")
|
|
36
|
+
return;
|
|
37
|
+
ReminderStore.appendEvent(reminder, input.succeeded ? "delivery_succeeded" : "delivery_failed", "agent", input.agentId, reminder.status === "scheduled" ? reminder.fireAt : null, Date.now(), {
|
|
38
|
+
deliveryTarget: input.target,
|
|
39
|
+
occurrenceId: current.deliveryAnchor,
|
|
40
|
+
...((input.messageId || current.deliveryMessageId) ? { messageId: input.messageId || current.deliveryMessageId } : {}),
|
|
41
|
+
...(input.reason ? { reason: input.reason } : {}),
|
|
42
|
+
});
|
|
43
|
+
persisted = true;
|
|
44
|
+
});
|
|
45
|
+
// Clear only after delivery_succeeded was durably appended; otherwise keep
|
|
46
|
+
// the occurrence context so a later reconciliation can still record the
|
|
47
|
+
// committed write instead of silently dropping its audit trail.
|
|
48
|
+
if (input.succeeded && persisted)
|
|
49
|
+
input.stateStore.clearCurrentReminder(current.reminderId, current.deliveryAnchor);
|
|
50
|
+
}
|
|
@@ -35,9 +35,32 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.createReminderRoutes = createReminderRoutes;
|
|
37
37
|
const ReminderStore = __importStar(require("./reminder-store.cjs"));
|
|
38
|
+
const inbox_projection_js_1 = require("./inbox-projection.cjs");
|
|
38
39
|
function versionOf(reminder) {
|
|
39
40
|
return Number(reminder.version || 0);
|
|
40
41
|
}
|
|
42
|
+
const VALID_IM_ANCHOR = /^om_[A-Za-z0-9_-]+$/;
|
|
43
|
+
const VALID_COMMENT_ANCHOR = /^doc_comment_[A-Za-z0-9_-]+$/;
|
|
44
|
+
const VALID_CHAT_TARGET = /^chat:oc_[A-Za-z0-9_-]+$/;
|
|
45
|
+
const VALID_THREAD_TARGET = /^thread:oc_[A-Za-z0-9_-]+:omt_[A-Za-z0-9_-]+$/;
|
|
46
|
+
const VALID_DOCUMENT_COMMENT_TARGET = /^document-comment:(doc|docx|sheet|file):[A-Za-z0-9_-]+:[A-Za-z0-9_-]+:(in-thread|top-level)$/;
|
|
47
|
+
function userTarget(value, field) {
|
|
48
|
+
const raw = String(value ?? "").trim();
|
|
49
|
+
// --channel historically accepted an oc_ chat id; retain that unambiguous form.
|
|
50
|
+
const target = /^oc_[A-Za-z0-9_-]+$/.test(raw) ? `chat:${raw}` : raw;
|
|
51
|
+
const complete = VALID_CHAT_TARGET.test(target) || VALID_THREAD_TARGET.test(target) || VALID_DOCUMENT_COMMENT_TARGET.test(target);
|
|
52
|
+
if (!complete || !(0, inbox_projection_js_1.isCanonicalInboxTarget)(target) || !(0, inbox_projection_js_1.isUserDeliveryTarget)(target)) {
|
|
53
|
+
throw new Error(`${field} 必须是完整可投递的 canonical target(chat:oc_<id>、thread:oc_<chat>:omt_<thread> 或完整 document-comment locator)`);
|
|
54
|
+
}
|
|
55
|
+
return target;
|
|
56
|
+
}
|
|
57
|
+
function validAnchor(value, field = "message-id", target) {
|
|
58
|
+
const anchor = String(value ?? "").trim();
|
|
59
|
+
const valid = VALID_IM_ANCHOR.test(anchor) || (target?.startsWith("document-comment:") === true && VALID_COMMENT_ANCHOR.test(anchor));
|
|
60
|
+
if (!valid)
|
|
61
|
+
throw new Error(`${field} 必须是有效的 Feishu om_ message_id,或 document-comment source 的 doc_comment_ anchor`);
|
|
62
|
+
return anchor;
|
|
63
|
+
}
|
|
41
64
|
function createReminderRoutes(options) {
|
|
42
65
|
const now = options.now ?? Date.now;
|
|
43
66
|
const timeZone = options.timeZone ?? (() => Intl.DateTimeFormat().resolvedOptions().timeZone);
|
|
@@ -54,6 +77,60 @@ function createReminderRoutes(options) {
|
|
|
54
77
|
return { ok: false, status: 400, error: "title 不能为空" };
|
|
55
78
|
const current = now();
|
|
56
79
|
const tz = typeof body.tz === "string" && body.tz ? body.tz : timeZone();
|
|
80
|
+
const internal = body.internal === true || body.noDelivery === true;
|
|
81
|
+
let deliveryTarget = null;
|
|
82
|
+
let deliveryAnchor = null;
|
|
83
|
+
if (internal) {
|
|
84
|
+
if (body.deliveryTarget !== undefined || body.channel !== undefined || body.msgId !== undefined) {
|
|
85
|
+
return { ok: false, status: 400, error: "internal/no-delivery reminder 不应携带 user-facing delivery target" };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
try {
|
|
90
|
+
const hasExplicitTarget = body.deliveryTarget !== undefined && body.deliveryTarget !== null && Boolean(String(body.deliveryTarget).trim());
|
|
91
|
+
const hasExplicitChannel = body.channel !== undefined && body.channel !== null && Boolean(String(body.channel).trim());
|
|
92
|
+
// deliveryTarget and channel are destination aliases. Preferring one
|
|
93
|
+
// over a contradicting other could route to the wrong conversation,
|
|
94
|
+
// so a disagreement fails closed instead of being silently resolved.
|
|
95
|
+
if (hasExplicitTarget && hasExplicitChannel
|
|
96
|
+
&& userTarget(body.deliveryTarget, "delivery target") !== userTarget(body.channel, "channel")) {
|
|
97
|
+
return { ok: false, status: 400, error: "deliveryTarget 与 channel 指向不同目的地;请只提供其中一个" };
|
|
98
|
+
}
|
|
99
|
+
const explicit = hasExplicitTarget ? body.deliveryTarget : hasExplicitChannel ? body.channel : undefined;
|
|
100
|
+
if (explicit !== undefined)
|
|
101
|
+
deliveryTarget = userTarget(explicit, "delivery target");
|
|
102
|
+
if (body.msgId !== undefined && body.msgId !== null && String(body.msgId).trim()) {
|
|
103
|
+
const anchorTarget = options.resolveMessageTarget?.(String(body.msgId).trim()) || null;
|
|
104
|
+
deliveryAnchor = validAnchor(body.msgId, "message-id", anchorTarget || deliveryTarget);
|
|
105
|
+
if (deliveryTarget && !anchorTarget) {
|
|
106
|
+
return { ok: false, status: 400, error: `message-id ${String(body.msgId).trim()} 无法验证其所属 Inbox target;请省略 anchor 或提供可验证的 anchor` };
|
|
107
|
+
}
|
|
108
|
+
if (!deliveryTarget && !anchorTarget) {
|
|
109
|
+
return { ok: false, status: 400, error: `message-id ${deliveryAnchor} 没有当前 Inbox 的 canonical delivery target` };
|
|
110
|
+
}
|
|
111
|
+
if (anchorTarget) {
|
|
112
|
+
const normalizedAnchorTarget = userTarget(anchorTarget, "message-id target");
|
|
113
|
+
if (deliveryTarget && deliveryTarget !== normalizedAnchorTarget) {
|
|
114
|
+
return { ok: false, status: 400, error: "delivery target 与 message-id 所属 Inbox target 冲突" };
|
|
115
|
+
}
|
|
116
|
+
deliveryTarget = deliveryTarget || normalizedAnchorTarget;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (deliveryTarget && (deliveryTarget.startsWith("thread:") || deliveryTarget.startsWith("document-comment:")) && !deliveryAnchor) {
|
|
120
|
+
return { ok: false, status: 400, error: `${deliveryTarget} 必须同时提供可验证的 message-id anchor` };
|
|
121
|
+
}
|
|
122
|
+
if (!deliveryTarget) {
|
|
123
|
+
const source = options.currentInboxSource?.() || null;
|
|
124
|
+
if (!source)
|
|
125
|
+
return { ok: false, status: 400, error: "user-facing reminder 必须显式指定 delivery target,或从当前 Inbox source 派生" };
|
|
126
|
+
deliveryTarget = userTarget(source.deliveryTarget, "current Inbox target");
|
|
127
|
+
deliveryAnchor = validAnchor(source.deliveryAnchor, "current Inbox anchor", deliveryTarget);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
return { ok: false, status: 400, error: error.message };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
57
134
|
let recurrence = null;
|
|
58
135
|
if (body.repeat !== undefined) {
|
|
59
136
|
recurrence = ReminderStore.parseRepeat(body.repeat, tz);
|
|
@@ -78,14 +155,17 @@ function createReminderRoutes(options) {
|
|
|
78
155
|
const reminder = {
|
|
79
156
|
reminderId: ReminderStore.newId(), ownerAgentId: options.agentId, title,
|
|
80
157
|
fireAt: ReminderStore.nowIso(fireAtMs), firedAt: null, createdAt: ReminderStore.nowIso(current),
|
|
81
|
-
status: "scheduled", msgRef:
|
|
158
|
+
status: "scheduled", msgRef: deliveryAnchor, msgPermalink: null,
|
|
159
|
+
deliveryTarget, deliveryAnchor, deliveryMode: internal ? "internal" : "user",
|
|
82
160
|
repeat: body.repeat ?? null, tz: body.repeat !== undefined ? tz : null,
|
|
83
161
|
channel: body.channel ?? null, payload: body.payload ?? null,
|
|
84
162
|
version: 1, events: [],
|
|
85
163
|
};
|
|
86
|
-
ReminderStore.appendEvent(reminder, "scheduled", "agent", options.agentId, reminder.fireAt, current
|
|
164
|
+
ReminderStore.appendEvent(reminder, "scheduled", "agent", options.agentId, reminder.fireAt, current, {
|
|
165
|
+
deliveryTarget, deliveryAnchor, deliveryMode: internal ? "internal" : "user",
|
|
166
|
+
});
|
|
87
167
|
ReminderStore.mutate(options.stateFile, (store) => store.reminders.push(reminder));
|
|
88
|
-
options.log(`reminder 已建 #${reminder.reminderId.slice(0, 8)} fireAt=${reminder.fireAt}${reminder.repeat ? ` repeat=${String(reminder.repeat)}` : ""}`);
|
|
168
|
+
options.log(`reminder 已建 #${reminder.reminderId.slice(0, 8)} fireAt=${reminder.fireAt}${reminder.repeat ? ` repeat=${String(reminder.repeat)}` : ""} deliveryTarget=${deliveryTarget || "none"}`);
|
|
89
169
|
return { ok: true, status: 200, data: { reminder: ReminderStore.toSummary(reminder), ...(warning ? { warning } : {}) } };
|
|
90
170
|
}
|
|
91
171
|
if (method === "GET" && !reminderId) {
|