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
|
@@ -1,7 +1,30 @@
|
|
|
1
1
|
import * as ReminderStore from "./reminder-store.mjs";
|
|
2
|
+
import { isCanonicalInboxTarget, isUserDeliveryTarget } from "./inbox-projection.mjs";
|
|
2
3
|
function versionOf(reminder) {
|
|
3
4
|
return Number(reminder.version || 0);
|
|
4
5
|
}
|
|
6
|
+
const VALID_IM_ANCHOR = /^om_[A-Za-z0-9_-]+$/;
|
|
7
|
+
const VALID_COMMENT_ANCHOR = /^doc_comment_[A-Za-z0-9_-]+$/;
|
|
8
|
+
const VALID_CHAT_TARGET = /^chat:oc_[A-Za-z0-9_-]+$/;
|
|
9
|
+
const VALID_THREAD_TARGET = /^thread:oc_[A-Za-z0-9_-]+:omt_[A-Za-z0-9_-]+$/;
|
|
10
|
+
const VALID_DOCUMENT_COMMENT_TARGET = /^document-comment:(doc|docx|sheet|file):[A-Za-z0-9_-]+:[A-Za-z0-9_-]+:(in-thread|top-level)$/;
|
|
11
|
+
function userTarget(value, field) {
|
|
12
|
+
const raw = String(value ?? "").trim();
|
|
13
|
+
// --channel historically accepted an oc_ chat id; retain that unambiguous form.
|
|
14
|
+
const target = /^oc_[A-Za-z0-9_-]+$/.test(raw) ? `chat:${raw}` : raw;
|
|
15
|
+
const complete = VALID_CHAT_TARGET.test(target) || VALID_THREAD_TARGET.test(target) || VALID_DOCUMENT_COMMENT_TARGET.test(target);
|
|
16
|
+
if (!complete || !isCanonicalInboxTarget(target) || !isUserDeliveryTarget(target)) {
|
|
17
|
+
throw new Error(`${field} 必须是完整可投递的 canonical target(chat:oc_<id>、thread:oc_<chat>:omt_<thread> 或完整 document-comment locator)`);
|
|
18
|
+
}
|
|
19
|
+
return target;
|
|
20
|
+
}
|
|
21
|
+
function validAnchor(value, field = "message-id", target) {
|
|
22
|
+
const anchor = String(value ?? "").trim();
|
|
23
|
+
const valid = VALID_IM_ANCHOR.test(anchor) || (target?.startsWith("document-comment:") === true && VALID_COMMENT_ANCHOR.test(anchor));
|
|
24
|
+
if (!valid)
|
|
25
|
+
throw new Error(`${field} 必须是有效的 Feishu om_ message_id,或 document-comment source 的 doc_comment_ anchor`);
|
|
26
|
+
return anchor;
|
|
27
|
+
}
|
|
5
28
|
export function createReminderRoutes(options) {
|
|
6
29
|
const now = options.now ?? Date.now;
|
|
7
30
|
const timeZone = options.timeZone ?? (() => Intl.DateTimeFormat().resolvedOptions().timeZone);
|
|
@@ -18,6 +41,60 @@ export function createReminderRoutes(options) {
|
|
|
18
41
|
return { ok: false, status: 400, error: "title 不能为空" };
|
|
19
42
|
const current = now();
|
|
20
43
|
const tz = typeof body.tz === "string" && body.tz ? body.tz : timeZone();
|
|
44
|
+
const internal = body.internal === true || body.noDelivery === true;
|
|
45
|
+
let deliveryTarget = null;
|
|
46
|
+
let deliveryAnchor = null;
|
|
47
|
+
if (internal) {
|
|
48
|
+
if (body.deliveryTarget !== undefined || body.channel !== undefined || body.msgId !== undefined) {
|
|
49
|
+
return { ok: false, status: 400, error: "internal/no-delivery reminder 不应携带 user-facing delivery target" };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
try {
|
|
54
|
+
const hasExplicitTarget = body.deliveryTarget !== undefined && body.deliveryTarget !== null && Boolean(String(body.deliveryTarget).trim());
|
|
55
|
+
const hasExplicitChannel = body.channel !== undefined && body.channel !== null && Boolean(String(body.channel).trim());
|
|
56
|
+
// deliveryTarget and channel are destination aliases. Preferring one
|
|
57
|
+
// over a contradicting other could route to the wrong conversation,
|
|
58
|
+
// so a disagreement fails closed instead of being silently resolved.
|
|
59
|
+
if (hasExplicitTarget && hasExplicitChannel
|
|
60
|
+
&& userTarget(body.deliveryTarget, "delivery target") !== userTarget(body.channel, "channel")) {
|
|
61
|
+
return { ok: false, status: 400, error: "deliveryTarget 与 channel 指向不同目的地;请只提供其中一个" };
|
|
62
|
+
}
|
|
63
|
+
const explicit = hasExplicitTarget ? body.deliveryTarget : hasExplicitChannel ? body.channel : undefined;
|
|
64
|
+
if (explicit !== undefined)
|
|
65
|
+
deliveryTarget = userTarget(explicit, "delivery target");
|
|
66
|
+
if (body.msgId !== undefined && body.msgId !== null && String(body.msgId).trim()) {
|
|
67
|
+
const anchorTarget = options.resolveMessageTarget?.(String(body.msgId).trim()) || null;
|
|
68
|
+
deliveryAnchor = validAnchor(body.msgId, "message-id", anchorTarget || deliveryTarget);
|
|
69
|
+
if (deliveryTarget && !anchorTarget) {
|
|
70
|
+
return { ok: false, status: 400, error: `message-id ${String(body.msgId).trim()} 无法验证其所属 Inbox target;请省略 anchor 或提供可验证的 anchor` };
|
|
71
|
+
}
|
|
72
|
+
if (!deliveryTarget && !anchorTarget) {
|
|
73
|
+
return { ok: false, status: 400, error: `message-id ${deliveryAnchor} 没有当前 Inbox 的 canonical delivery target` };
|
|
74
|
+
}
|
|
75
|
+
if (anchorTarget) {
|
|
76
|
+
const normalizedAnchorTarget = userTarget(anchorTarget, "message-id target");
|
|
77
|
+
if (deliveryTarget && deliveryTarget !== normalizedAnchorTarget) {
|
|
78
|
+
return { ok: false, status: 400, error: "delivery target 与 message-id 所属 Inbox target 冲突" };
|
|
79
|
+
}
|
|
80
|
+
deliveryTarget = deliveryTarget || normalizedAnchorTarget;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (deliveryTarget && (deliveryTarget.startsWith("thread:") || deliveryTarget.startsWith("document-comment:")) && !deliveryAnchor) {
|
|
84
|
+
return { ok: false, status: 400, error: `${deliveryTarget} 必须同时提供可验证的 message-id anchor` };
|
|
85
|
+
}
|
|
86
|
+
if (!deliveryTarget) {
|
|
87
|
+
const source = options.currentInboxSource?.() || null;
|
|
88
|
+
if (!source)
|
|
89
|
+
return { ok: false, status: 400, error: "user-facing reminder 必须显式指定 delivery target,或从当前 Inbox source 派生" };
|
|
90
|
+
deliveryTarget = userTarget(source.deliveryTarget, "current Inbox target");
|
|
91
|
+
deliveryAnchor = validAnchor(source.deliveryAnchor, "current Inbox anchor", deliveryTarget);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
return { ok: false, status: 400, error: error.message };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
21
98
|
let recurrence = null;
|
|
22
99
|
if (body.repeat !== undefined) {
|
|
23
100
|
recurrence = ReminderStore.parseRepeat(body.repeat, tz);
|
|
@@ -42,14 +119,17 @@ export function createReminderRoutes(options) {
|
|
|
42
119
|
const reminder = {
|
|
43
120
|
reminderId: ReminderStore.newId(), ownerAgentId: options.agentId, title,
|
|
44
121
|
fireAt: ReminderStore.nowIso(fireAtMs), firedAt: null, createdAt: ReminderStore.nowIso(current),
|
|
45
|
-
status: "scheduled", msgRef:
|
|
122
|
+
status: "scheduled", msgRef: deliveryAnchor, msgPermalink: null,
|
|
123
|
+
deliveryTarget, deliveryAnchor, deliveryMode: internal ? "internal" : "user",
|
|
46
124
|
repeat: body.repeat ?? null, tz: body.repeat !== undefined ? tz : null,
|
|
47
125
|
channel: body.channel ?? null, payload: body.payload ?? null,
|
|
48
126
|
version: 1, events: [],
|
|
49
127
|
};
|
|
50
|
-
ReminderStore.appendEvent(reminder, "scheduled", "agent", options.agentId, reminder.fireAt, current
|
|
128
|
+
ReminderStore.appendEvent(reminder, "scheduled", "agent", options.agentId, reminder.fireAt, current, {
|
|
129
|
+
deliveryTarget, deliveryAnchor, deliveryMode: internal ? "internal" : "user",
|
|
130
|
+
});
|
|
51
131
|
ReminderStore.mutate(options.stateFile, (store) => store.reminders.push(reminder));
|
|
52
|
-
options.log(`reminder 已建 #${reminder.reminderId.slice(0, 8)} fireAt=${reminder.fireAt}${reminder.repeat ? ` repeat=${String(reminder.repeat)}` : ""}`);
|
|
132
|
+
options.log(`reminder 已建 #${reminder.reminderId.slice(0, 8)} fireAt=${reminder.fireAt}${reminder.repeat ? ` repeat=${String(reminder.repeat)}` : ""} deliveryTarget=${deliveryTarget || "none"}`);
|
|
53
133
|
return { ok: true, status: 200, data: { reminder: ReminderStore.toSummary(reminder), ...(warning ? { warning } : {}) } };
|
|
54
134
|
}
|
|
55
135
|
if (method === "GET" && !reminderId) {
|
|
@@ -201,10 +201,13 @@ function toSummary(reminder) {
|
|
|
201
201
|
status: reminder.status,
|
|
202
202
|
msgRef: reminder.msgRef ?? null,
|
|
203
203
|
msgPermalink: reminder.msgPermalink ?? null,
|
|
204
|
+
deliveryTarget: reminder.deliveryTarget ?? null,
|
|
205
|
+
deliveryAnchor: reminder.deliveryAnchor ?? null,
|
|
206
|
+
deliveryMode: reminder.deliveryMode === "internal" ? "internal" : "user",
|
|
204
207
|
recurrence,
|
|
205
208
|
};
|
|
206
209
|
}
|
|
207
|
-
function appendEvent(reminder, eventType, actorType, actorId, nextFireAt, ms) {
|
|
210
|
+
function appendEvent(reminder, eventType, actorType, actorId, nextFireAt, ms, metadata) {
|
|
208
211
|
if (!Array.isArray(reminder.events))
|
|
209
212
|
reminder.events = [];
|
|
210
213
|
reminder.events.push({
|
|
@@ -215,6 +218,6 @@ function appendEvent(reminder, eventType, actorType, actorId, nextFireAt, ms) {
|
|
|
215
218
|
actorId: actorId ?? null,
|
|
216
219
|
occurredAt: (0, exports.nowIso)(ms),
|
|
217
220
|
nextFireAt: nextFireAt ?? null,
|
|
218
|
-
metadata: null,
|
|
221
|
+
metadata: metadata ?? null,
|
|
219
222
|
});
|
|
220
223
|
}
|
|
@@ -186,10 +186,13 @@ export function toSummary(reminder) {
|
|
|
186
186
|
status: reminder.status,
|
|
187
187
|
msgRef: reminder.msgRef ?? null,
|
|
188
188
|
msgPermalink: reminder.msgPermalink ?? null,
|
|
189
|
+
deliveryTarget: reminder.deliveryTarget ?? null,
|
|
190
|
+
deliveryAnchor: reminder.deliveryAnchor ?? null,
|
|
191
|
+
deliveryMode: reminder.deliveryMode === "internal" ? "internal" : "user",
|
|
189
192
|
recurrence,
|
|
190
193
|
};
|
|
191
194
|
}
|
|
192
|
-
export function appendEvent(reminder, eventType, actorType, actorId, nextFireAt, ms) {
|
|
195
|
+
export function appendEvent(reminder, eventType, actorType, actorId, nextFireAt, ms, metadata) {
|
|
193
196
|
if (!Array.isArray(reminder.events))
|
|
194
197
|
reminder.events = [];
|
|
195
198
|
reminder.events.push({
|
|
@@ -200,6 +203,6 @@ export function appendEvent(reminder, eventType, actorType, actorId, nextFireAt,
|
|
|
200
203
|
actorId: actorId ?? null,
|
|
201
204
|
occurredAt: nowIso(ms),
|
|
202
205
|
nextFireAt: nextFireAt ?? null,
|
|
203
|
-
metadata: null,
|
|
206
|
+
metadata: metadata ?? null,
|
|
204
207
|
});
|
|
205
208
|
}
|
|
@@ -46,6 +46,7 @@ const inbox_projection_js_1 = require("./inbox-projection.cjs");
|
|
|
46
46
|
const message_policy_js_1 = require("../feishu/message-policy.cjs");
|
|
47
47
|
const outbound_transport_js_1 = require("../feishu/outbound-transport.cjs");
|
|
48
48
|
const reminder_routes_js_1 = require("./reminder-routes.cjs");
|
|
49
|
+
const reminder_delivery_audit_js_1 = require("./reminder-delivery-audit.cjs");
|
|
49
50
|
const transport_shell_js_1 = require("./transport-shell.cjs");
|
|
50
51
|
const agent_lark_cli_workspace_js_1 = require("../app/agent-lark-cli-workspace.cjs");
|
|
51
52
|
function formatFeishuError(error, grantLink = null) {
|
|
@@ -152,6 +153,24 @@ function createTransportBusinessContext(env = process.env) {
|
|
|
152
153
|
}
|
|
153
154
|
};
|
|
154
155
|
const resolveChatId = (target) => (0, transport_shell_js_1.resolveMappedChatId)(loadMap(), target);
|
|
156
|
+
const recordReminderDelivery = ({ target, succeeded, messageId, reason }) => {
|
|
157
|
+
const current = stateStore.resolveCurrentReminder();
|
|
158
|
+
try {
|
|
159
|
+
(0, reminder_delivery_audit_js_1.auditReminderDelivery)({ stateStore, agentId, target, succeeded, ...(messageId ? { messageId } : {}),
|
|
160
|
+
...(reason ? { reason } : {}), resolveChatId });
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
if (succeeded && current && stateStore.markCurrentReminderDeliveryCommitted) {
|
|
164
|
+
try {
|
|
165
|
+
stateStore.markCurrentReminderDeliveryCommitted(current.reminderId, current.deliveryAnchor, messageId);
|
|
166
|
+
}
|
|
167
|
+
catch (markerError) {
|
|
168
|
+
log(`reminder committed marker 写失败 id=#${current.reminderId.slice(0, 8)}: ${markerError.message}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
log(`reminder outbound audit 写失败 id=#${current?.reminderId.slice(0, 8) || "unknown"}: ${error.message}`);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
155
174
|
const larkArgs = (...rest) => rest;
|
|
156
175
|
const larkCli = (args, cwd) => {
|
|
157
176
|
const managed = (0, agent_lark_cli_workspace_js_1.managedOfficialLarkCli)(selectedAgent, process.env);
|
|
@@ -307,7 +326,8 @@ function createTransportBusinessContext(env = process.env) {
|
|
|
307
326
|
const outbound = (0, outbound_transport_js_1.createOutboundTransport)({
|
|
308
327
|
attachmentDir, attachmentIndexFile, resolveChatId, replyContextFor,
|
|
309
328
|
sendText: larkSend, replyText: larkReply, sendMedia: larkSendMedia,
|
|
310
|
-
appendConversation,
|
|
329
|
+
appendConversation, onDeliveryOutcome: recordReminderDelivery,
|
|
330
|
+
botDisplayName: () => botIdentity()?.name, agentName, dryRun, log,
|
|
311
331
|
});
|
|
312
332
|
const liveGrantLink = () => {
|
|
313
333
|
try {
|
|
@@ -469,7 +489,11 @@ function createTransportBusinessContext(env = process.env) {
|
|
|
469
489
|
},
|
|
470
490
|
mappedChannels: loadMap, larkJsonOut, feishuError, botDisplayName, feishuAppInfo, log,
|
|
471
491
|
});
|
|
472
|
-
const reminderRoutes = (0, reminder_routes_js_1.createReminderRoutes)({
|
|
492
|
+
const reminderRoutes = (0, reminder_routes_js_1.createReminderRoutes)({
|
|
493
|
+
stateFile: paths.reminders, agentId, query, log,
|
|
494
|
+
currentInboxSource: () => stateStore.resolveCurrentInboxSource(),
|
|
495
|
+
resolveMessageTarget: (messageId) => stateStore.resolveInboxMessageTarget(messageId),
|
|
496
|
+
});
|
|
473
497
|
try {
|
|
474
498
|
fs.appendFileSync(transportLogFile, `${new Date().toISOString()} INIT profile=${selectedAgent.feishuProfile} larkCfgDir=${selectedAgent.larkConfigDir} inbox=${paths.inbox}\n`);
|
|
475
499
|
}
|
|
@@ -9,6 +9,7 @@ import { projectInboxEvents } from "./inbox-projection.mjs";
|
|
|
9
9
|
import { signatureDescription } from "../feishu/message-policy.mjs";
|
|
10
10
|
import { createOutboundTransport } from "../feishu/outbound-transport.mjs";
|
|
11
11
|
import { createReminderRoutes } from "./reminder-routes.mjs";
|
|
12
|
+
import { auditReminderDelivery } from "./reminder-delivery-audit.mjs";
|
|
12
13
|
import { isImageMime, looksLikeMarkdown, resolveMappedChatId, safeConversationExcerpt } from "./transport-shell.mjs";
|
|
13
14
|
import { managedOfficialLarkCli } from "../app/agent-lark-cli-workspace.mjs";
|
|
14
15
|
export function formatFeishuError(error, grantLink = null) {
|
|
@@ -115,6 +116,24 @@ export function createTransportBusinessContext(env = process.env) {
|
|
|
115
116
|
}
|
|
116
117
|
};
|
|
117
118
|
const resolveChatId = (target) => resolveMappedChatId(loadMap(), target);
|
|
119
|
+
const recordReminderDelivery = ({ target, succeeded, messageId, reason }) => {
|
|
120
|
+
const current = stateStore.resolveCurrentReminder();
|
|
121
|
+
try {
|
|
122
|
+
auditReminderDelivery({ stateStore, agentId, target, succeeded, ...(messageId ? { messageId } : {}),
|
|
123
|
+
...(reason ? { reason } : {}), resolveChatId });
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (succeeded && current && stateStore.markCurrentReminderDeliveryCommitted) {
|
|
127
|
+
try {
|
|
128
|
+
stateStore.markCurrentReminderDeliveryCommitted(current.reminderId, current.deliveryAnchor, messageId);
|
|
129
|
+
}
|
|
130
|
+
catch (markerError) {
|
|
131
|
+
log(`reminder committed marker 写失败 id=#${current.reminderId.slice(0, 8)}: ${markerError.message}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
log(`reminder outbound audit 写失败 id=#${current?.reminderId.slice(0, 8) || "unknown"}: ${error.message}`);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
118
137
|
const larkArgs = (...rest) => rest;
|
|
119
138
|
const larkCli = (args, cwd) => {
|
|
120
139
|
const managed = managedOfficialLarkCli(selectedAgent, process.env);
|
|
@@ -270,7 +289,8 @@ export function createTransportBusinessContext(env = process.env) {
|
|
|
270
289
|
const outbound = createOutboundTransport({
|
|
271
290
|
attachmentDir, attachmentIndexFile, resolveChatId, replyContextFor,
|
|
272
291
|
sendText: larkSend, replyText: larkReply, sendMedia: larkSendMedia,
|
|
273
|
-
appendConversation,
|
|
292
|
+
appendConversation, onDeliveryOutcome: recordReminderDelivery,
|
|
293
|
+
botDisplayName: () => botIdentity()?.name, agentName, dryRun, log,
|
|
274
294
|
});
|
|
275
295
|
const liveGrantLink = () => {
|
|
276
296
|
try {
|
|
@@ -432,7 +452,11 @@ export function createTransportBusinessContext(env = process.env) {
|
|
|
432
452
|
},
|
|
433
453
|
mappedChannels: loadMap, larkJsonOut, feishuError, botDisplayName, feishuAppInfo, log,
|
|
434
454
|
});
|
|
435
|
-
const reminderRoutes = createReminderRoutes({
|
|
455
|
+
const reminderRoutes = createReminderRoutes({
|
|
456
|
+
stateFile: paths.reminders, agentId, query, log,
|
|
457
|
+
currentInboxSource: () => stateStore.resolveCurrentInboxSource(),
|
|
458
|
+
resolveMessageTarget: (messageId) => stateStore.resolveInboxMessageTarget(messageId),
|
|
459
|
+
});
|
|
436
460
|
try {
|
|
437
461
|
fs.appendFileSync(transportLogFile, `${new Date().toISOString()} INIT profile=${selectedAgent.feishuProfile} larkCfgDir=${selectedAgent.larkConfigDir} inbox=${paths.inbox}\n`);
|
|
438
462
|
}
|
package/dist/app/agent-cli.mjs
CHANGED
|
@@ -262,7 +262,17 @@ function reminderRequest(groupArgv, stateStore, agentId, deps) {
|
|
|
262
262
|
if (!fs.existsSync(stateStore.paths.reminders))
|
|
263
263
|
stateStore.writeJson("reminders", { reminders: [] });
|
|
264
264
|
const [operation, ...rest] = groupArgv;
|
|
265
|
-
const options = parseOptions(rest, new Set(["--all", "--json"]));
|
|
265
|
+
const options = parseOptions(rest, new Set(["--all", "--json", "--no-delivery", "--internal"]));
|
|
266
|
+
const assertOnlyFlags = (valueFlags, booleanFlags = []) => {
|
|
267
|
+
const allowedValues = new Set(valueFlags);
|
|
268
|
+
const allowedBooleans = new Set(booleanFlags);
|
|
269
|
+
const unsupported = [
|
|
270
|
+
...[...options.values.keys()].filter((flag) => !allowedValues.has(flag)),
|
|
271
|
+
...[...options.booleans].filter((flag) => !allowedBooleans.has(flag)),
|
|
272
|
+
];
|
|
273
|
+
if (unsupported.length)
|
|
274
|
+
throw new Error(`reminder ${operation || ""} 不支持参数:${unsupported.join(", ")}`);
|
|
275
|
+
};
|
|
266
276
|
if (options.positionals.length)
|
|
267
277
|
throw new Error(`reminder ${operation || ""} 不接受位置参数:${options.positionals.join(" ")}`);
|
|
268
278
|
const id = options.values.get("--id");
|
|
@@ -270,7 +280,8 @@ function reminderRequest(groupArgv, stateStore, agentId, deps) {
|
|
|
270
280
|
let requestPath = "/reminders";
|
|
271
281
|
const body = {};
|
|
272
282
|
switch (operation) {
|
|
273
|
-
case "schedule":
|
|
283
|
+
case "schedule": {
|
|
284
|
+
assertOnlyFlags(["--title", "--fire-at", "--delay-seconds", "--repeat", "--tz", "--message-id", "--delivery-target", "--target", "--channel"], ["--json", "--no-delivery", "--internal"]);
|
|
274
285
|
method = "POST";
|
|
275
286
|
body.title = options.values.get("--title") || "";
|
|
276
287
|
if (options.values.has("--fire-at"))
|
|
@@ -283,10 +294,24 @@ function reminderRequest(groupArgv, stateStore, agentId, deps) {
|
|
|
283
294
|
body.tz = options.values.get("--tz");
|
|
284
295
|
if (options.values.has("--message-id"))
|
|
285
296
|
body.msgId = options.values.get("--message-id");
|
|
297
|
+
// The destination controls the recipient. Aliases must never silently
|
|
298
|
+
// overwrite each other, so any combination fails closed at parse time.
|
|
299
|
+
const destinationFlags = ["--delivery-target", "--target", "--channel"].filter((flag) => options.values.has(flag));
|
|
300
|
+
if (destinationFlags.length > 1) {
|
|
301
|
+
throw new Error(`reminder schedule 收到多个 delivery 目的地 flag(${destinationFlags.join("、")});它们互为别名且可能指向不同会话,只能指定其中一个`);
|
|
302
|
+
}
|
|
286
303
|
if (options.values.has("--channel"))
|
|
287
304
|
body.channel = options.values.get("--channel");
|
|
305
|
+
if (options.values.has("--delivery-target"))
|
|
306
|
+
body.deliveryTarget = options.values.get("--delivery-target");
|
|
307
|
+
if (options.values.has("--target"))
|
|
308
|
+
body.deliveryTarget = options.values.get("--target");
|
|
309
|
+
if (options.booleans.has("--no-delivery") || options.booleans.has("--internal"))
|
|
310
|
+
body.noDelivery = true;
|
|
288
311
|
break;
|
|
312
|
+
}
|
|
289
313
|
case "list": {
|
|
314
|
+
assertOnlyFlags(["--status"], ["--all", "--json"]);
|
|
290
315
|
const search = new URLSearchParams();
|
|
291
316
|
if (options.values.has("--status"))
|
|
292
317
|
search.set("status", options.values.get("--status"));
|
|
@@ -296,6 +321,7 @@ function reminderRequest(groupArgv, stateStore, agentId, deps) {
|
|
|
296
321
|
break;
|
|
297
322
|
}
|
|
298
323
|
case "snooze":
|
|
324
|
+
assertOnlyFlags(["--id", "--delay-seconds"], ["--json"]);
|
|
299
325
|
if (!id)
|
|
300
326
|
throw new Error("reminder snooze 需要 --id");
|
|
301
327
|
method = "POST";
|
|
@@ -303,6 +329,7 @@ function reminderRequest(groupArgv, stateStore, agentId, deps) {
|
|
|
303
329
|
body.delaySeconds = numberOption(options, "--delay-seconds");
|
|
304
330
|
break;
|
|
305
331
|
case "update":
|
|
332
|
+
assertOnlyFlags(["--id", "--title", "--fire-at", "--delay-seconds", "--repeat", "--tz"], ["--json"]);
|
|
306
333
|
if (!id)
|
|
307
334
|
throw new Error("reminder update 需要 --id");
|
|
308
335
|
method = "PATCH";
|
|
@@ -319,12 +346,14 @@ function reminderRequest(groupArgv, stateStore, agentId, deps) {
|
|
|
319
346
|
body.tz = options.values.get("--tz");
|
|
320
347
|
break;
|
|
321
348
|
case "cancel":
|
|
349
|
+
assertOnlyFlags(["--id"], ["--json"]);
|
|
322
350
|
if (!id)
|
|
323
351
|
throw new Error("reminder cancel 需要 --id");
|
|
324
352
|
method = "DELETE";
|
|
325
353
|
requestPath += `/${encodeURIComponent(id)}`;
|
|
326
354
|
break;
|
|
327
355
|
case "log":
|
|
356
|
+
assertOnlyFlags(["--id"], ["--json"]);
|
|
328
357
|
if (!id)
|
|
329
358
|
throw new Error("reminder log 需要 --id");
|
|
330
359
|
requestPath += `/${encodeURIComponent(id)}/log`;
|
|
@@ -339,6 +368,8 @@ function reminderRequest(groupArgv, stateStore, agentId, deps) {
|
|
|
339
368
|
log: () => undefined,
|
|
340
369
|
...(deps.now ? { now: deps.now } : {}),
|
|
341
370
|
...(deps.timeZone ? { timeZone: deps.timeZone } : {}),
|
|
371
|
+
currentInboxSource: () => stateStore.resolveCurrentInboxSource(),
|
|
372
|
+
resolveMessageTarget: (messageId) => stateStore.resolveInboxMessageTarget(messageId),
|
|
342
373
|
});
|
|
343
374
|
return routes.handle({ path: requestPath, pathNoQuery: requestPath.split("?")[0], method, body });
|
|
344
375
|
}
|
package/dist/app/lark-cli.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { createHash } from "node:crypto";
|
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { createAgentStateStore } from "../agent/agent-state-store.mjs";
|
|
7
|
+
import { auditReminderDelivery } from "../agent/reminder-delivery-audit.mjs";
|
|
7
8
|
import { evaluateFreshness } from "../agent/freshness-gate.mjs";
|
|
8
9
|
import { feishuImFreshnessAdapter, feishuImTarget, mergeFeishuImCursor, serializeFeishuImTarget, } from "../feishu/im-freshness-adapter.mjs";
|
|
9
10
|
import * as larkinConfig from "../platform/config.mjs";
|
|
@@ -281,21 +282,22 @@ const IM_WRITE_MEMO_LIMIT = 512;
|
|
|
281
282
|
// 只标注、不拦截:每次成功写把「实际生效的幂等 key → 服务端返回的 message_id」记进备忘。
|
|
282
283
|
// 同 key 再次成功且服务端返回同一个 message_id,说明服务端走了幂等去重(没有产生新消息),
|
|
283
284
|
// 返回 true 供输出标注 duplicate。拦截权始终在服务端,备忘不会吞掉任何发送。
|
|
284
|
-
function recordImWriteMemo(store, key, messageId) {
|
|
285
|
-
|
|
286
|
-
store.mutateJson("freshnessState", { version: 1, cursors: {} }, (state) => {
|
|
285
|
+
function recordImWriteMemo(store, key, messageId, sourceMessageId) {
|
|
286
|
+
return store.mutateJson("freshnessState", { version: 1, cursors: {} }, (state) => {
|
|
287
287
|
state.im_write_memo ??= {};
|
|
288
288
|
const prior = state.im_write_memo[key];
|
|
289
|
-
if (prior && prior.message_id === messageId)
|
|
290
|
-
duplicate
|
|
291
|
-
|
|
289
|
+
if (prior && prior.message_id === messageId) {
|
|
290
|
+
return { duplicate: true, ...(prior.source_message_id ? { priorSourceMessageId: prior.source_message_id } : {}) };
|
|
291
|
+
}
|
|
292
|
+
state.im_write_memo[key] = { message_id: messageId, updated_at: new Date().toISOString(),
|
|
293
|
+
...(sourceMessageId ? { source_message_id: sourceMessageId } : {}) };
|
|
292
294
|
const keys = Object.keys(state.im_write_memo);
|
|
293
295
|
for (const stale of keys.slice(0, Math.max(0, keys.length - IM_WRITE_MEMO_LIMIT)))
|
|
294
296
|
delete state.im_write_memo[stale];
|
|
297
|
+
return { duplicate: false };
|
|
295
298
|
});
|
|
296
|
-
return duplicate;
|
|
297
299
|
}
|
|
298
|
-
function runCommentReply(argv, privateEnv, io, dependencies, store) {
|
|
300
|
+
function runCommentReply(argv, privateEnv, io, dependencies, store, agentId) {
|
|
299
301
|
const input = parseCommentReply(argv);
|
|
300
302
|
const targetKey = store.resolveInboxMessageTarget(input.messageId);
|
|
301
303
|
const target = targetKey ? parseDocumentCommentTarget(targetKey) : null;
|
|
@@ -324,6 +326,18 @@ function runCommentReply(argv, privateEnv, io, dependencies, store) {
|
|
|
324
326
|
return "ready";
|
|
325
327
|
});
|
|
326
328
|
if (claim === "sent") {
|
|
329
|
+
const currentReminder = store.resolveCurrentReminders().filter((reminder) => reminder.deliveryTarget === targetKey).at(-1) ?? null;
|
|
330
|
+
try {
|
|
331
|
+
auditReminderDelivery({ stateStore: store, agentId, target: targetKey, deliveryAnchor: currentReminder?.deliveryAnchor,
|
|
332
|
+
succeeded: !currentReminder, ...(currentReminder ? { reason: "document comment ledger already sent this text to the anchor on an earlier reminder firing" } : {}) });
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
io.stderr(`lark-cli: reminder delivery audit failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
336
|
+
}
|
|
337
|
+
if (currentReminder) {
|
|
338
|
+
io.stderr("comment reply was already sent for an earlier reminder firing; no new delivery was committed\n");
|
|
339
|
+
return 2;
|
|
340
|
+
}
|
|
327
341
|
io.stdout(`${JSON.stringify({ ok: true, identity: "bot", committed: true, duplicate: true, target: targetKey })}\n`);
|
|
328
342
|
return 0;
|
|
329
343
|
}
|
|
@@ -362,6 +376,25 @@ function runCommentReply(argv, privateEnv, io, dependencies, store) {
|
|
|
362
376
|
};
|
|
363
377
|
});
|
|
364
378
|
}
|
|
379
|
+
const committedWrite = !result.error && result.status === 0;
|
|
380
|
+
const currentReminder = committedWrite
|
|
381
|
+
? store.resolveCurrentReminders().filter((reminder) => reminder.deliveryTarget === targetKey).at(-1) ?? null
|
|
382
|
+
: null;
|
|
383
|
+
try {
|
|
384
|
+
auditReminderDelivery({ stateStore: store, agentId, target: targetKey, succeeded: committedWrite,
|
|
385
|
+
...(committedWrite ? {} : { reason: (result.stderr || result.stdout || "comment provider failed").trim().split("\n")[0] }) });
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
if (committedWrite && currentReminder) {
|
|
389
|
+
try {
|
|
390
|
+
store.markCurrentReminderDeliveryCommitted(currentReminder.reminderId, currentReminder.deliveryAnchor);
|
|
391
|
+
}
|
|
392
|
+
catch (markerError) {
|
|
393
|
+
io.stderr(`lark-cli: reminder committed marker failed: ${markerError instanceof Error ? markerError.message : String(markerError)}\n`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
io.stderr(`lark-cli: reminder delivery audit failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
397
|
+
}
|
|
365
398
|
return emitNativeResult(result, io);
|
|
366
399
|
}
|
|
367
400
|
function definitiveProviderRejection(result) {
|
|
@@ -1031,7 +1064,7 @@ export function runLarkCli(argv, env = process.env, dependencies = {}) {
|
|
|
1031
1064
|
}
|
|
1032
1065
|
catch { /* telemetry is failure-isolated */ }
|
|
1033
1066
|
}
|
|
1034
|
-
return telemetry.externalPhase(agent.agentId, store.paths.root, "document.comment.reply", SpanKind.CLIENT, () => runCommentReply(argv, privateEnv, io, nativeDependencies, store), "comment_cli");
|
|
1067
|
+
return telemetry.externalPhase(agent.agentId, store.paths.root, "document.comment.reply", SpanKind.CLIENT, () => runCommentReply(argv, privateEnv, io, nativeDependencies, store, agent.agentId), "comment_cli");
|
|
1035
1068
|
}
|
|
1036
1069
|
catch (error) {
|
|
1037
1070
|
io.stderr(`lark-cli: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
@@ -1084,8 +1117,43 @@ export function runLarkCli(argv, env = process.env, dependencies = {}) {
|
|
|
1084
1117
|
return emitNativeResult(write, io);
|
|
1085
1118
|
}
|
|
1086
1119
|
const writeMessage = writeResponseMessage(write);
|
|
1087
|
-
const
|
|
1088
|
-
?
|
|
1120
|
+
const deliveryTarget = decision.operation === "send"
|
|
1121
|
+
? `chat:${policyFlagValue(effectiveArgv, "--chat-id")}`
|
|
1122
|
+
: store.resolveInboxMessageTarget(policyFlagValue(effectiveArgv, "--message-id") || "") || targetKey;
|
|
1123
|
+
const matchingReminderContexts = store.resolveCurrentReminders().filter((reminder) => reminder.deliveryTarget === deliveryTarget);
|
|
1124
|
+
// When recurring firings overlap, attach this write to one occurrence rather
|
|
1125
|
+
// than dropping audit state merely because reminder_id is shared. Reuse this
|
|
1126
|
+
// exact anchor for both the memo and audit so they cannot select different
|
|
1127
|
+
// occurrences.
|
|
1128
|
+
const currentReminder = matchingReminderContexts.at(-1) ?? null;
|
|
1129
|
+
const deliveryAnchor = currentReminder?.deliveryAnchor;
|
|
1130
|
+
const memo = !write.error && write.status === 0 && writeMessage
|
|
1131
|
+
? recordImWriteMemo(store, intentKey, writeMessage.message_id, deliveryAnchor) : { duplicate: false };
|
|
1132
|
+
const duplicateOfEarlierReminder = memo.duplicate && Boolean(currentReminder)
|
|
1133
|
+
&& memo.priorSourceMessageId !== currentReminder?.deliveryAnchor;
|
|
1134
|
+
const committedWrite = !write.error && write.status === 0 && !duplicateOfEarlierReminder;
|
|
1135
|
+
try {
|
|
1136
|
+
auditReminderDelivery({ stateStore: store, agentId: agent.agentId, target: deliveryTarget, deliveryAnchor,
|
|
1137
|
+
succeeded: committedWrite,
|
|
1138
|
+
...(!committedWrite ? {
|
|
1139
|
+
reason: duplicateOfEarlierReminder
|
|
1140
|
+
? "provider deduplicated this write to an earlier reminder firing"
|
|
1141
|
+
: (write.stderr || write.stdout || "provider write failed").trim().split("\n")[0],
|
|
1142
|
+
} : {}),
|
|
1143
|
+
...(writeMessage?.message_id ? { messageId: writeMessage.message_id } : {}) });
|
|
1144
|
+
}
|
|
1145
|
+
catch (error) {
|
|
1146
|
+
if (committedWrite && currentReminder) {
|
|
1147
|
+
try {
|
|
1148
|
+
store.markCurrentReminderDeliveryCommitted(currentReminder.reminderId, currentReminder.deliveryAnchor, writeMessage?.message_id);
|
|
1149
|
+
}
|
|
1150
|
+
catch (markerError) {
|
|
1151
|
+
io.stderr(`lark-cli: reminder committed marker failed: ${markerError instanceof Error ? markerError.message : String(markerError)}\n`);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
io.stderr(`lark-cli: reminder delivery audit failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1155
|
+
}
|
|
1156
|
+
const duplicate = memo.duplicate;
|
|
1089
1157
|
if (duplicate) {
|
|
1090
1158
|
observeSuccessfulWrite(write, target, targetKey, store, generation);
|
|
1091
1159
|
emitDuplicatedWrite(write, io, { target: targetKey });
|
|
@@ -316,31 +316,50 @@ class HostEnvelopeProjector {
|
|
|
316
316
|
}
|
|
317
317
|
createReminderEnvelope(agentId, reminder, overdueMs, repeatDescription) {
|
|
318
318
|
const seq = this.nextSequence(agentId);
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
319
|
+
const legacyChannel = typeof reminder.channel === "string" ? reminder.channel.trim() : "";
|
|
320
|
+
const legacyChatTarget = /^oc_[A-Za-z0-9_-]+$/.test(legacyChannel) ? `chat:${legacyChannel}` : null;
|
|
321
|
+
const deliveryTarget = typeof reminder.deliveryTarget === "string" && reminder.deliveryTarget
|
|
322
|
+
? reminder.deliveryTarget : legacyChatTarget;
|
|
323
|
+
const anchorMessageId = typeof reminder.deliveryAnchor === "string" && /^om_[A-Za-z0-9_-]+$/.test(reminder.deliveryAnchor)
|
|
324
|
+
? reminder.deliveryAnchor
|
|
325
|
+
: typeof reminder.msgRef === "string" && /^om_[A-Za-z0-9_-]+$/.test(reminder.msgRef)
|
|
326
|
+
? reminder.msgRef
|
|
327
|
+
: null;
|
|
328
|
+
const commentAnchorId = deliveryTarget?.startsWith("document-comment:") && typeof reminder.deliveryAnchor === "string"
|
|
329
|
+
&& /^doc_comment_[A-Za-z0-9_-]+$/.test(reminder.deliveryAnchor) ? reminder.deliveryAnchor : null;
|
|
330
|
+
const anchorId = anchorMessageId || commentAnchorId;
|
|
331
|
+
const chatId = deliveryTarget?.match(/^chat:(oc_[A-Za-z0-9_-]+)$/)?.[1] || null;
|
|
322
332
|
const lines = [
|
|
323
333
|
`[定时提醒触发] ${reminder.title}`,
|
|
324
334
|
`提醒ID: #${reminder.reminderId.slice(0, 8)}` + (reminder.repeat && repeatDescription
|
|
325
335
|
? ` 重复: ${repeatDescription}(下次已自动排在 ${reminder.fireAt})`
|
|
326
336
|
: " 类型: 一次性"),
|
|
327
337
|
overdueMs > 120_000 ? `注意: 原定时间已过 ${Math.round(overdueMs / 60_000)} 分钟(Runtime Host 离线期间错过,现补触发)` : null,
|
|
328
|
-
|
|
338
|
+
deliveryTarget ? `原始 deliveryTarget: ${deliveryTarget}`
|
|
339
|
+
: anchorId ? "这是升级前存量 user-facing reminder,优先回复其安全锚点;不得向标题中的任何人或第三方发送消息"
|
|
340
|
+
: "本条为 internal/no-delivery reminder,不得向标题中的任何人或第三方发送消息",
|
|
341
|
+
anchorId ? `锚定消息: ${anchorId}` : reminder.msgRef ? `历史锚点 ${String(reminder.msgRef)} 不是可用的 delivery anchor,不能用于回复` : null,
|
|
329
342
|
anchorMessageId
|
|
330
|
-
?
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
:
|
|
335
|
-
?
|
|
336
|
-
:
|
|
337
|
-
?
|
|
338
|
-
|
|
343
|
+
? [
|
|
344
|
+
`回复原会话: ${this.larkCommand(`im +messages-reply --message-id ${anchorMessageId}${deliveryTarget?.startsWith("thread:") ? " --reply-in-thread" : ""} ...`)}`,
|
|
345
|
+
chatId ? `若 ${anchorMessageId} 无法由当前 Inbox 解析(例如 interaction_* 卡片锚点或已淘汰的历史消息),改用聊天兜底发送: ${this.larkCommand(`im +messages-send --chat-id ${chatId} ...`)};仅使用这个持久化 chat_id,不得猜测收件人` : null,
|
|
346
|
+
].filter((line) => Boolean(line)).join("\n")
|
|
347
|
+
: commentAnchorId
|
|
348
|
+
? `回复原文档评论: ${this.agentCommand(`comment reply --message-id ${commentAnchorId} --text ...`)}`
|
|
349
|
+
: deliveryTarget
|
|
350
|
+
? chatId
|
|
351
|
+
? `发送到原始 target: ${deliveryTarget}: ${this.larkCommand(`im +messages-send --chat-id ${chatId} ...`)}(不得从提醒标题推断收件人)`
|
|
352
|
+
: `发送到原始 target: ${deliveryTarget}(不得从提醒标题推断收件人)`
|
|
353
|
+
: null,
|
|
339
354
|
`这是你之前用 ${this.agentCommand("reminder schedule")} 设置的提醒,请按标题执行相应动作。管理: ${this.agentCommand("reminder list")} / ${this.agentCommand("reminder snooze")} / ${this.agentCommand("reminder cancel")}`,
|
|
340
355
|
].filter((line) => Boolean(line));
|
|
341
356
|
const envelope = {
|
|
342
357
|
kind: "reminder",
|
|
343
|
-
|
|
358
|
+
// The random suffix keeps each firing's occurrence identity unique across
|
|
359
|
+
// Host restarts: the in-memory seq restarts at 1 and would otherwise let a
|
|
360
|
+
// recurring reminder reuse a prior occurrenceId in the delivery audit.
|
|
361
|
+
message_id: `rem_${reminder.reminderId.slice(0, 16)}_${seq}_${this.randomHex(6)}`,
|
|
362
|
+
reminderId: reminder.reminderId,
|
|
344
363
|
seq,
|
|
345
364
|
sender_name: "定时提醒",
|
|
346
365
|
sender_type: "system",
|
|
@@ -350,6 +369,8 @@ class HostEnvelopeProjector {
|
|
|
350
369
|
timestamp: this.now().toISOString(),
|
|
351
370
|
thread_id: null,
|
|
352
371
|
wake: true,
|
|
372
|
+
deliveryTarget,
|
|
373
|
+
deliveryAnchor: anchorId,
|
|
353
374
|
};
|
|
354
375
|
return { ...envelope, target: inbox_projection_js_1.RUNTIME_REMINDER_TARGET };
|
|
355
376
|
}
|