larkin 0.2.70
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/CONTRIBUTING.md +48 -0
- package/LICENSE +201 -0
- package/README.md +122 -0
- package/SECURITY.md +14 -0
- package/artifacts/release/THIRD_PARTY_NOTICES.txt +5488 -0
- package/dist/agent/agent-cli-capabilities.cjs +43 -0
- package/dist/agent/agent-cli-capabilities.mjs +35 -0
- package/dist/agent/agent-state-store.cjs +961 -0
- package/dist/agent/agent-state-store.mjs +923 -0
- package/dist/agent/agent-transport.cjs +201 -0
- package/dist/agent/agent-transport.mjs +194 -0
- package/dist/agent/config-cli-contract.cjs +19 -0
- package/dist/agent/config-cli-contract.mjs +16 -0
- package/dist/agent/context-prompt.cjs +159 -0
- package/dist/agent/context-prompt.mjs +155 -0
- package/dist/agent/freshness-gate.mjs +24 -0
- package/dist/agent/history-projection.cjs +72 -0
- package/dist/agent/history-projection.mjs +65 -0
- package/dist/agent/host-reminder-orchestrator.cjs +289 -0
- package/dist/agent/host-reminder-orchestrator.mjs +252 -0
- package/dist/agent/inbox-projection.cjs +92 -0
- package/dist/agent/inbox-projection.mjs +85 -0
- package/dist/agent/interaction-state-machine.cjs +710 -0
- package/dist/agent/interaction-state-machine.mjs +705 -0
- package/dist/agent/reminder-routes.cjs +192 -0
- package/dist/agent/reminder-routes.mjs +156 -0
- package/dist/agent/reminder-store.cjs +220 -0
- package/dist/agent/reminder-store.mjs +205 -0
- package/dist/agent/transport-business-context.cjs +486 -0
- package/dist/agent/transport-business-context.mjs +446 -0
- package/dist/agent/transport-shell.cjs +45 -0
- package/dist/agent/transport-shell.mjs +39 -0
- package/dist/app/agent-cli.mjs +596 -0
- package/dist/app/agent-config.mjs +633 -0
- package/dist/app/agent-lark-cli-workspace.cjs +95 -0
- package/dist/app/agent-lark-cli-workspace.mjs +85 -0
- package/dist/app/agent-readiness.cjs +42 -0
- package/dist/app/agent-readiness.mjs +38 -0
- package/dist/app/binary-entry.mjs +86 -0
- package/dist/app/cli.mjs +176 -0
- package/dist/app/dashboard.mjs +205 -0
- package/dist/app/internal-command.cjs +57 -0
- package/dist/app/internal-command.mjs +48 -0
- package/dist/app/lark-channel-secret.mjs +22 -0
- package/dist/app/lark-cli.mjs +887 -0
- package/dist/app/lark.mjs +45 -0
- package/dist/app/local-control.mjs +728 -0
- package/dist/app/official-lark-cli.cjs +138 -0
- package/dist/app/official-lark-cli.mjs +128 -0
- package/dist/app/pi-auth-cli.mjs +67 -0
- package/dist/app/run.mjs +364 -0
- package/dist/app/runtime-agent-config.mjs +416 -0
- package/dist/app/runtime-model-directory.mjs +78 -0
- package/dist/app/runtime-process.mjs +113 -0
- package/dist/app/session-cli.mjs +83 -0
- package/dist/app/setup-dashboard.mjs +26 -0
- package/dist/app/setup.mjs +166 -0
- package/dist/app/telemetry.mjs +83 -0
- package/dist/dashboard/dashboard-assets.mjs +21 -0
- package/dist/dashboard/dashboard-avatar.mjs +128 -0
- package/dist/dashboard/dashboard-config-controller.mjs +525 -0
- package/dist/dashboard/dashboard-lifecycle.cjs +97 -0
- package/dist/dashboard/dashboard-lifecycle.mjs +88 -0
- package/dist/dashboard/dashboard-template.mjs +29 -0
- package/dist/dashboard/dashboard-view-model.mjs +556 -0
- package/dist/dashboard/dashboard-workspace.mjs +104 -0
- package/dist/dashboard/web/assets/dashboard.css +3 -0
- package/dist/dashboard/web/assets/dashboard.js +49 -0
- package/dist/feishu/channel-transport.cjs +151 -0
- package/dist/feishu/channel-transport.mjs +148 -0
- package/dist/feishu/document-comment.cjs +120 -0
- package/dist/feishu/document-comment.mjs +109 -0
- package/dist/feishu/host-business-state.cjs +477 -0
- package/dist/feishu/host-business-state.mjs +462 -0
- package/dist/feishu/host-channel-business.cjs +161 -0
- package/dist/feishu/host-channel-business.mjs +157 -0
- package/dist/feishu/host-processing-eye.cjs +185 -0
- package/dist/feishu/host-processing-eye.mjs +181 -0
- package/dist/feishu/host-shell.cjs +1425 -0
- package/dist/feishu/host-shell.mjs +1385 -0
- package/dist/feishu/im-freshness-adapter.mjs +89 -0
- package/dist/feishu/interaction-orchestrator.cjs +279 -0
- package/dist/feishu/interaction-orchestrator.mjs +242 -0
- package/dist/feishu/lark-passthrough.mjs +70 -0
- package/dist/feishu/message-policy.cjs +111 -0
- package/dist/feishu/message-policy.mjs +98 -0
- package/dist/feishu/outbound-transport.cjs +221 -0
- package/dist/feishu/outbound-transport.mjs +215 -0
- package/dist/platform/build-info.cjs +25 -0
- package/dist/platform/build-info.mjs +18 -0
- package/dist/platform/callback-capability.cjs +301 -0
- package/dist/platform/callback-capability.mjs +252 -0
- package/dist/platform/check-bun-version.cjs +12 -0
- package/dist/platform/config.cjs +751 -0
- package/dist/platform/config.mjs +694 -0
- package/dist/platform/process-inspect.cjs +89 -0
- package/dist/platform/process-state.cjs +237 -0
- package/dist/platform/process-state.mjs +223 -0
- package/dist/platform/release-artifacts.mjs +61 -0
- package/dist/platform/root-layout.cjs +111 -0
- package/dist/platform/root-layout.mjs +72 -0
- package/dist/platform/telemetry-config.cjs +64 -0
- package/dist/platform/telemetry-config.mjs +56 -0
- package/dist/platform/telemetry-spool.cjs +661 -0
- package/dist/platform/telemetry-spool.mjs +653 -0
- package/dist/platform/telemetry-tracing.cjs +602 -0
- package/dist/platform/telemetry-tracing.mjs +596 -0
- package/dist/platform/telemetry-uploader.cjs +150 -0
- package/dist/platform/telemetry-uploader.mjs +146 -0
- package/dist/platform/workspace-service.cjs +469 -0
- package/dist/platform/workspace-service.mjs +430 -0
- package/dist/runtime/builtin-pi-assets.mjs +33 -0
- package/dist/runtime/claude-model-catalog.cjs +102 -0
- package/dist/runtime/claude-model-catalog.mjs +99 -0
- package/dist/runtime/codex-model-catalog.cjs +108 -0
- package/dist/runtime/codex-model-catalog.mjs +105 -0
- package/dist/runtime/pi-bash-timeout-extension.mjs +60 -0
- package/dist/runtime/pi-bash-timeout-injection.mjs +60 -0
- package/dist/runtime/pi-bash-timeout.bundle.js +43 -0
- package/dist/runtime/pi-model-catalog.cjs +116 -0
- package/dist/runtime/pi-model-catalog.mjs +75 -0
- package/dist/runtime/pi-official-auth.mjs +480 -0
- package/dist/runtime/pi-provider-config.cjs +252 -0
- package/dist/runtime/pi-provider-config.mjs +239 -0
- package/dist/runtime/pi-rpc-client.cjs +255 -0
- package/dist/runtime/pi-rpc-client.mjs +251 -0
- package/dist/runtime/pi-subagent-injection.mjs +129 -0
- package/dist/runtime/pi-subagents.bundle.js +8990 -0
- package/dist/runtime/runtime-adapters.mjs +1019 -0
- package/dist/runtime/runtime-contracts.cjs +2 -0
- package/dist/runtime/runtime-contracts.mjs +1 -0
- package/dist/runtime/runtime-host.cjs +930 -0
- package/dist/runtime/runtime-host.mjs +922 -0
- package/dist/runtime/runtime-model-catalog.cjs +43 -0
- package/dist/runtime/runtime-model-catalog.mjs +40 -0
- package/dist/runtime/runtime-readiness.cjs +177 -0
- package/dist/runtime/runtime-readiness.mjs +133 -0
- package/dist/setup/bot-register.mjs +655 -0
- package/dist/setup/grant-scopes.mjs +133 -0
- package/dist/setup/run-credential-preflight.mjs +89 -0
- package/dist/setup/setup-agent-choice.mjs +172 -0
- package/dist/setup/setup-bind.mjs +337 -0
- package/dist/setup/setup-binding.mjs +52 -0
- package/package.json +112 -0
|
@@ -0,0 +1,192 @@
|
|
|
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.createReminderRoutes = createReminderRoutes;
|
|
37
|
+
const ReminderStore = __importStar(require("./reminder-store.cjs"));
|
|
38
|
+
function versionOf(reminder) {
|
|
39
|
+
return Number(reminder.version || 0);
|
|
40
|
+
}
|
|
41
|
+
function createReminderRoutes(options) {
|
|
42
|
+
const now = options.now ?? Date.now;
|
|
43
|
+
const timeZone = options.timeZone ?? (() => Intl.DateTimeFormat().resolvedOptions().timeZone);
|
|
44
|
+
const handle = ({ path, pathNoQuery, method, body }) => {
|
|
45
|
+
if (!options.stateFile)
|
|
46
|
+
return { ok: false, status: 500, error: "state dir 未配置,无法持久化 reminder" };
|
|
47
|
+
const match = pathNoQuery.match(/\/reminders\/([^/]+)(\/(snooze|log))?$/);
|
|
48
|
+
const reminderId = match ? decodeURIComponent(match[1]) : null;
|
|
49
|
+
const subroute = match ? match[3] || null : null;
|
|
50
|
+
const find = (store) => store.reminders.find((record) => record.reminderId === reminderId);
|
|
51
|
+
if (method === "POST" && !reminderId) {
|
|
52
|
+
const title = String(body.title || "").trim();
|
|
53
|
+
if (!title)
|
|
54
|
+
return { ok: false, status: 400, error: "title 不能为空" };
|
|
55
|
+
const current = now();
|
|
56
|
+
const tz = typeof body.tz === "string" && body.tz ? body.tz : timeZone();
|
|
57
|
+
let recurrence = null;
|
|
58
|
+
if (body.repeat !== undefined) {
|
|
59
|
+
recurrence = ReminderStore.parseRepeat(body.repeat, tz);
|
|
60
|
+
if ("error" in recurrence)
|
|
61
|
+
return { ok: false, status: 400, error: recurrence.error };
|
|
62
|
+
}
|
|
63
|
+
let fireAtMs = null;
|
|
64
|
+
let warning = null;
|
|
65
|
+
if (body.delaySeconds != null)
|
|
66
|
+
fireAtMs = current + Number(body.delaySeconds) * 1_000;
|
|
67
|
+
else if (body.fireAt !== undefined) {
|
|
68
|
+
fireAtMs = Date.parse(String(body.fireAt));
|
|
69
|
+
if (Number.isNaN(fireAtMs))
|
|
70
|
+
return { ok: false, status: 400, error: `fireAt 不是合法时间:${String(body.fireAt)}` };
|
|
71
|
+
if (fireAtMs <= current)
|
|
72
|
+
warning = "fireAt 已是过去时间,将立即触发";
|
|
73
|
+
}
|
|
74
|
+
else if (recurrence && !("error" in recurrence))
|
|
75
|
+
fireAtMs = recurrence.next(current);
|
|
76
|
+
if (!fireAtMs)
|
|
77
|
+
return { ok: false, status: 400, error: "无法确定首次触发时间" };
|
|
78
|
+
const reminder = {
|
|
79
|
+
reminderId: ReminderStore.newId(), ownerAgentId: options.agentId, title,
|
|
80
|
+
fireAt: ReminderStore.nowIso(fireAtMs), firedAt: null, createdAt: ReminderStore.nowIso(current),
|
|
81
|
+
status: "scheduled", msgRef: body.msgId ?? null, msgPermalink: null,
|
|
82
|
+
repeat: body.repeat ?? null, tz: body.repeat !== undefined ? tz : null,
|
|
83
|
+
channel: body.channel ?? null, payload: body.payload ?? null,
|
|
84
|
+
version: 1, events: [],
|
|
85
|
+
};
|
|
86
|
+
ReminderStore.appendEvent(reminder, "scheduled", "agent", options.agentId, reminder.fireAt, current);
|
|
87
|
+
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)}` : ""}`);
|
|
89
|
+
return { ok: true, status: 200, data: { reminder: ReminderStore.toSummary(reminder), ...(warning ? { warning } : {}) } };
|
|
90
|
+
}
|
|
91
|
+
if (method === "GET" && !reminderId) {
|
|
92
|
+
const store = ReminderStore.load(options.stateFile);
|
|
93
|
+
const all = options.query(path, "all") === "true";
|
|
94
|
+
const statuses = (options.query(path, "status") || "scheduled,fired").split(",").map((status) => status.trim());
|
|
95
|
+
return { ok: true, status: 200, data: { reminders: store.reminders.filter((record) => all || statuses.includes(record.status)).map(ReminderStore.toSummary) } };
|
|
96
|
+
}
|
|
97
|
+
if (!reminderId)
|
|
98
|
+
return { ok: false, status: 404, error: "unknown reminders route" };
|
|
99
|
+
if (method === "GET" && subroute === "log") {
|
|
100
|
+
const reminder = find(ReminderStore.load(options.stateFile));
|
|
101
|
+
if (!reminder)
|
|
102
|
+
return { ok: false, status: 404, error: `reminder 不存在:${reminderId}` };
|
|
103
|
+
return { ok: true, status: 200, data: { events: reminder.events || [] } };
|
|
104
|
+
}
|
|
105
|
+
if (method === "POST" && subroute === "snooze") {
|
|
106
|
+
const delay = Number(body.delaySeconds);
|
|
107
|
+
if (!Number.isFinite(delay) || delay <= 0)
|
|
108
|
+
return { ok: false, status: 400, error: "delaySeconds 必须为正数" };
|
|
109
|
+
const output = ReminderStore.mutate(options.stateFile, (store) => {
|
|
110
|
+
const reminder = find(store);
|
|
111
|
+
if (!reminder)
|
|
112
|
+
return null;
|
|
113
|
+
const current = now();
|
|
114
|
+
reminder.fireAt = ReminderStore.nowIso(current + delay * 1_000);
|
|
115
|
+
reminder.status = "scheduled";
|
|
116
|
+
reminder.version = versionOf(reminder) + 1;
|
|
117
|
+
ReminderStore.appendEvent(reminder, "snoozed", "agent", options.agentId, reminder.fireAt, current);
|
|
118
|
+
return ReminderStore.toSummary(reminder);
|
|
119
|
+
});
|
|
120
|
+
return output
|
|
121
|
+
? { ok: true, status: 200, data: { reminder: output } }
|
|
122
|
+
: { ok: false, status: 404, error: `reminder 不存在:${reminderId}` };
|
|
123
|
+
}
|
|
124
|
+
if (method === "PATCH" && !subroute) {
|
|
125
|
+
let warning = null;
|
|
126
|
+
if (body.fireAt !== undefined && Number.isNaN(Date.parse(String(body.fireAt)))) {
|
|
127
|
+
return { ok: false, status: 400, error: `fireAt 不是合法时间:${String(body.fireAt)}` };
|
|
128
|
+
}
|
|
129
|
+
const patchTz = typeof body.tz === "string" && body.tz ? body.tz : timeZone();
|
|
130
|
+
if (body.repeat !== undefined) {
|
|
131
|
+
const recurrence = ReminderStore.parseRepeat(body.repeat, patchTz);
|
|
132
|
+
if ("error" in recurrence)
|
|
133
|
+
return { ok: false, status: 400, error: recurrence.error };
|
|
134
|
+
}
|
|
135
|
+
const output = ReminderStore.mutate(options.stateFile, (store) => {
|
|
136
|
+
const reminder = find(store);
|
|
137
|
+
if (!reminder)
|
|
138
|
+
return null;
|
|
139
|
+
const current = now();
|
|
140
|
+
if (body.title !== undefined)
|
|
141
|
+
reminder.title = String(body.title);
|
|
142
|
+
if (body.delaySeconds != null) {
|
|
143
|
+
reminder.fireAt = ReminderStore.nowIso(current + Number(body.delaySeconds) * 1_000);
|
|
144
|
+
reminder.status = "scheduled";
|
|
145
|
+
}
|
|
146
|
+
if (body.fireAt !== undefined) {
|
|
147
|
+
const parsed = Date.parse(String(body.fireAt));
|
|
148
|
+
if (parsed <= current)
|
|
149
|
+
warning = "fireAt 已是过去时间,将立即触发";
|
|
150
|
+
reminder.fireAt = ReminderStore.nowIso(parsed);
|
|
151
|
+
reminder.status = "scheduled";
|
|
152
|
+
}
|
|
153
|
+
if (body.repeat !== undefined) {
|
|
154
|
+
const tz = (typeof body.tz === "string" && body.tz) || reminder.tz || patchTz;
|
|
155
|
+
reminder.repeat = body.repeat;
|
|
156
|
+
reminder.tz = tz;
|
|
157
|
+
reminder.status = "scheduled";
|
|
158
|
+
const recurrence = ReminderStore.parseRepeat(body.repeat, tz);
|
|
159
|
+
if (!("error" in recurrence) && Date.parse(reminder.fireAt) <= current) {
|
|
160
|
+
const next = recurrence.next(current, Date.parse(reminder.fireAt));
|
|
161
|
+
if (next)
|
|
162
|
+
reminder.fireAt = ReminderStore.nowIso(next);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
reminder.version = versionOf(reminder) + 1;
|
|
166
|
+
ReminderStore.appendEvent(reminder, "updated", "agent", options.agentId, reminder.status === "scheduled" ? reminder.fireAt : null, current);
|
|
167
|
+
return ReminderStore.toSummary(reminder);
|
|
168
|
+
});
|
|
169
|
+
return output
|
|
170
|
+
? { ok: true, status: 200, data: { reminder: output, ...(warning ? { warning } : {}) } }
|
|
171
|
+
: { ok: false, status: 404, error: `reminder 不存在:${reminderId}` };
|
|
172
|
+
}
|
|
173
|
+
if (method === "DELETE" && !subroute) {
|
|
174
|
+
const output = ReminderStore.mutate(options.stateFile, (store) => {
|
|
175
|
+
const reminder = find(store);
|
|
176
|
+
if (!reminder)
|
|
177
|
+
return null;
|
|
178
|
+
const current = now();
|
|
179
|
+
reminder.status = "canceled";
|
|
180
|
+
reminder.version = versionOf(reminder) + 1;
|
|
181
|
+
ReminderStore.appendEvent(reminder, "canceled", "agent", options.agentId, null, current);
|
|
182
|
+
return ReminderStore.toSummary(reminder);
|
|
183
|
+
});
|
|
184
|
+
if (!output)
|
|
185
|
+
return { ok: false, status: 404, error: `reminder 不存在:${reminderId}` };
|
|
186
|
+
options.log(`reminder 已取消 #${reminderId.slice(0, 8)}`);
|
|
187
|
+
return { ok: true, status: 200, data: { reminder: output } };
|
|
188
|
+
}
|
|
189
|
+
return { ok: false, status: 404, error: `unknown reminders route: ${method} ${pathNoQuery}` };
|
|
190
|
+
};
|
|
191
|
+
return { handle };
|
|
192
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import * as ReminderStore from "./reminder-store.mjs";
|
|
2
|
+
function versionOf(reminder) {
|
|
3
|
+
return Number(reminder.version || 0);
|
|
4
|
+
}
|
|
5
|
+
export function createReminderRoutes(options) {
|
|
6
|
+
const now = options.now ?? Date.now;
|
|
7
|
+
const timeZone = options.timeZone ?? (() => Intl.DateTimeFormat().resolvedOptions().timeZone);
|
|
8
|
+
const handle = ({ path, pathNoQuery, method, body }) => {
|
|
9
|
+
if (!options.stateFile)
|
|
10
|
+
return { ok: false, status: 500, error: "state dir 未配置,无法持久化 reminder" };
|
|
11
|
+
const match = pathNoQuery.match(/\/reminders\/([^/]+)(\/(snooze|log))?$/);
|
|
12
|
+
const reminderId = match ? decodeURIComponent(match[1]) : null;
|
|
13
|
+
const subroute = match ? match[3] || null : null;
|
|
14
|
+
const find = (store) => store.reminders.find((record) => record.reminderId === reminderId);
|
|
15
|
+
if (method === "POST" && !reminderId) {
|
|
16
|
+
const title = String(body.title || "").trim();
|
|
17
|
+
if (!title)
|
|
18
|
+
return { ok: false, status: 400, error: "title 不能为空" };
|
|
19
|
+
const current = now();
|
|
20
|
+
const tz = typeof body.tz === "string" && body.tz ? body.tz : timeZone();
|
|
21
|
+
let recurrence = null;
|
|
22
|
+
if (body.repeat !== undefined) {
|
|
23
|
+
recurrence = ReminderStore.parseRepeat(body.repeat, tz);
|
|
24
|
+
if ("error" in recurrence)
|
|
25
|
+
return { ok: false, status: 400, error: recurrence.error };
|
|
26
|
+
}
|
|
27
|
+
let fireAtMs = null;
|
|
28
|
+
let warning = null;
|
|
29
|
+
if (body.delaySeconds != null)
|
|
30
|
+
fireAtMs = current + Number(body.delaySeconds) * 1_000;
|
|
31
|
+
else if (body.fireAt !== undefined) {
|
|
32
|
+
fireAtMs = Date.parse(String(body.fireAt));
|
|
33
|
+
if (Number.isNaN(fireAtMs))
|
|
34
|
+
return { ok: false, status: 400, error: `fireAt 不是合法时间:${String(body.fireAt)}` };
|
|
35
|
+
if (fireAtMs <= current)
|
|
36
|
+
warning = "fireAt 已是过去时间,将立即触发";
|
|
37
|
+
}
|
|
38
|
+
else if (recurrence && !("error" in recurrence))
|
|
39
|
+
fireAtMs = recurrence.next(current);
|
|
40
|
+
if (!fireAtMs)
|
|
41
|
+
return { ok: false, status: 400, error: "无法确定首次触发时间" };
|
|
42
|
+
const reminder = {
|
|
43
|
+
reminderId: ReminderStore.newId(), ownerAgentId: options.agentId, title,
|
|
44
|
+
fireAt: ReminderStore.nowIso(fireAtMs), firedAt: null, createdAt: ReminderStore.nowIso(current),
|
|
45
|
+
status: "scheduled", msgRef: body.msgId ?? null, msgPermalink: null,
|
|
46
|
+
repeat: body.repeat ?? null, tz: body.repeat !== undefined ? tz : null,
|
|
47
|
+
channel: body.channel ?? null, payload: body.payload ?? null,
|
|
48
|
+
version: 1, events: [],
|
|
49
|
+
};
|
|
50
|
+
ReminderStore.appendEvent(reminder, "scheduled", "agent", options.agentId, reminder.fireAt, current);
|
|
51
|
+
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)}` : ""}`);
|
|
53
|
+
return { ok: true, status: 200, data: { reminder: ReminderStore.toSummary(reminder), ...(warning ? { warning } : {}) } };
|
|
54
|
+
}
|
|
55
|
+
if (method === "GET" && !reminderId) {
|
|
56
|
+
const store = ReminderStore.load(options.stateFile);
|
|
57
|
+
const all = options.query(path, "all") === "true";
|
|
58
|
+
const statuses = (options.query(path, "status") || "scheduled,fired").split(",").map((status) => status.trim());
|
|
59
|
+
return { ok: true, status: 200, data: { reminders: store.reminders.filter((record) => all || statuses.includes(record.status)).map(ReminderStore.toSummary) } };
|
|
60
|
+
}
|
|
61
|
+
if (!reminderId)
|
|
62
|
+
return { ok: false, status: 404, error: "unknown reminders route" };
|
|
63
|
+
if (method === "GET" && subroute === "log") {
|
|
64
|
+
const reminder = find(ReminderStore.load(options.stateFile));
|
|
65
|
+
if (!reminder)
|
|
66
|
+
return { ok: false, status: 404, error: `reminder 不存在:${reminderId}` };
|
|
67
|
+
return { ok: true, status: 200, data: { events: reminder.events || [] } };
|
|
68
|
+
}
|
|
69
|
+
if (method === "POST" && subroute === "snooze") {
|
|
70
|
+
const delay = Number(body.delaySeconds);
|
|
71
|
+
if (!Number.isFinite(delay) || delay <= 0)
|
|
72
|
+
return { ok: false, status: 400, error: "delaySeconds 必须为正数" };
|
|
73
|
+
const output = ReminderStore.mutate(options.stateFile, (store) => {
|
|
74
|
+
const reminder = find(store);
|
|
75
|
+
if (!reminder)
|
|
76
|
+
return null;
|
|
77
|
+
const current = now();
|
|
78
|
+
reminder.fireAt = ReminderStore.nowIso(current + delay * 1_000);
|
|
79
|
+
reminder.status = "scheduled";
|
|
80
|
+
reminder.version = versionOf(reminder) + 1;
|
|
81
|
+
ReminderStore.appendEvent(reminder, "snoozed", "agent", options.agentId, reminder.fireAt, current);
|
|
82
|
+
return ReminderStore.toSummary(reminder);
|
|
83
|
+
});
|
|
84
|
+
return output
|
|
85
|
+
? { ok: true, status: 200, data: { reminder: output } }
|
|
86
|
+
: { ok: false, status: 404, error: `reminder 不存在:${reminderId}` };
|
|
87
|
+
}
|
|
88
|
+
if (method === "PATCH" && !subroute) {
|
|
89
|
+
let warning = null;
|
|
90
|
+
if (body.fireAt !== undefined && Number.isNaN(Date.parse(String(body.fireAt)))) {
|
|
91
|
+
return { ok: false, status: 400, error: `fireAt 不是合法时间:${String(body.fireAt)}` };
|
|
92
|
+
}
|
|
93
|
+
const patchTz = typeof body.tz === "string" && body.tz ? body.tz : timeZone();
|
|
94
|
+
if (body.repeat !== undefined) {
|
|
95
|
+
const recurrence = ReminderStore.parseRepeat(body.repeat, patchTz);
|
|
96
|
+
if ("error" in recurrence)
|
|
97
|
+
return { ok: false, status: 400, error: recurrence.error };
|
|
98
|
+
}
|
|
99
|
+
const output = ReminderStore.mutate(options.stateFile, (store) => {
|
|
100
|
+
const reminder = find(store);
|
|
101
|
+
if (!reminder)
|
|
102
|
+
return null;
|
|
103
|
+
const current = now();
|
|
104
|
+
if (body.title !== undefined)
|
|
105
|
+
reminder.title = String(body.title);
|
|
106
|
+
if (body.delaySeconds != null) {
|
|
107
|
+
reminder.fireAt = ReminderStore.nowIso(current + Number(body.delaySeconds) * 1_000);
|
|
108
|
+
reminder.status = "scheduled";
|
|
109
|
+
}
|
|
110
|
+
if (body.fireAt !== undefined) {
|
|
111
|
+
const parsed = Date.parse(String(body.fireAt));
|
|
112
|
+
if (parsed <= current)
|
|
113
|
+
warning = "fireAt 已是过去时间,将立即触发";
|
|
114
|
+
reminder.fireAt = ReminderStore.nowIso(parsed);
|
|
115
|
+
reminder.status = "scheduled";
|
|
116
|
+
}
|
|
117
|
+
if (body.repeat !== undefined) {
|
|
118
|
+
const tz = (typeof body.tz === "string" && body.tz) || reminder.tz || patchTz;
|
|
119
|
+
reminder.repeat = body.repeat;
|
|
120
|
+
reminder.tz = tz;
|
|
121
|
+
reminder.status = "scheduled";
|
|
122
|
+
const recurrence = ReminderStore.parseRepeat(body.repeat, tz);
|
|
123
|
+
if (!("error" in recurrence) && Date.parse(reminder.fireAt) <= current) {
|
|
124
|
+
const next = recurrence.next(current, Date.parse(reminder.fireAt));
|
|
125
|
+
if (next)
|
|
126
|
+
reminder.fireAt = ReminderStore.nowIso(next);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
reminder.version = versionOf(reminder) + 1;
|
|
130
|
+
ReminderStore.appendEvent(reminder, "updated", "agent", options.agentId, reminder.status === "scheduled" ? reminder.fireAt : null, current);
|
|
131
|
+
return ReminderStore.toSummary(reminder);
|
|
132
|
+
});
|
|
133
|
+
return output
|
|
134
|
+
? { ok: true, status: 200, data: { reminder: output, ...(warning ? { warning } : {}) } }
|
|
135
|
+
: { ok: false, status: 404, error: `reminder 不存在:${reminderId}` };
|
|
136
|
+
}
|
|
137
|
+
if (method === "DELETE" && !subroute) {
|
|
138
|
+
const output = ReminderStore.mutate(options.stateFile, (store) => {
|
|
139
|
+
const reminder = find(store);
|
|
140
|
+
if (!reminder)
|
|
141
|
+
return null;
|
|
142
|
+
const current = now();
|
|
143
|
+
reminder.status = "canceled";
|
|
144
|
+
reminder.version = versionOf(reminder) + 1;
|
|
145
|
+
ReminderStore.appendEvent(reminder, "canceled", "agent", options.agentId, null, current);
|
|
146
|
+
return ReminderStore.toSummary(reminder);
|
|
147
|
+
});
|
|
148
|
+
if (!output)
|
|
149
|
+
return { ok: false, status: 404, error: `reminder 不存在:${reminderId}` };
|
|
150
|
+
options.log(`reminder 已取消 #${reminderId.slice(0, 8)}`);
|
|
151
|
+
return { ok: true, status: 200, data: { reminder: output } };
|
|
152
|
+
}
|
|
153
|
+
return { ok: false, status: 404, error: `unknown reminders route: ${method} ${pathNoQuery}` };
|
|
154
|
+
};
|
|
155
|
+
return { handle };
|
|
156
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.newId = exports.nowIso = void 0;
|
|
7
|
+
exports.withLock = withLock;
|
|
8
|
+
exports.load = load;
|
|
9
|
+
exports.save = save;
|
|
10
|
+
exports.mutate = mutate;
|
|
11
|
+
exports.parseRepeat = parseRepeat;
|
|
12
|
+
exports.toSummary = toSummary;
|
|
13
|
+
exports.appendEvent = appendEvent;
|
|
14
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
15
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
16
|
+
const nowIso = (ms) => new Date(ms ?? Date.now()).toISOString();
|
|
17
|
+
exports.nowIso = nowIso;
|
|
18
|
+
const newId = () => node_crypto_1.default.randomUUID().replace(/-/g, "");
|
|
19
|
+
exports.newId = newId;
|
|
20
|
+
// Host and Agent transport are separate processes. mkdir is the cross-process mutex;
|
|
21
|
+
// the timeout and stale-lock ages intentionally preserve the established production contract.
|
|
22
|
+
function withLock(file, fn, timeoutMs = 2_000) {
|
|
23
|
+
const lockDir = `${file}.lock`;
|
|
24
|
+
const deadline = Date.now() + timeoutMs;
|
|
25
|
+
for (;;) {
|
|
26
|
+
try {
|
|
27
|
+
node_fs_1.default.mkdirSync(lockDir);
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error.code !== "EEXIST")
|
|
32
|
+
throw error;
|
|
33
|
+
try {
|
|
34
|
+
if (Date.now() - node_fs_1.default.statSync(lockDir).mtimeMs > 10_000) {
|
|
35
|
+
node_fs_1.default.rmdirSync(lockDir);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch { /* another process may be reclaiming the same stale directory */ }
|
|
40
|
+
if (Date.now() > deadline)
|
|
41
|
+
throw new Error("reminders.json 锁等待超时");
|
|
42
|
+
const until = Date.now() + 15;
|
|
43
|
+
while (Date.now() < until) { /* preserve the existing short synchronous critical section */ }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
return fn();
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
try {
|
|
51
|
+
node_fs_1.default.rmdirSync(lockDir);
|
|
52
|
+
}
|
|
53
|
+
catch { /* best effort: preserve existing crash-recovery semantics */ }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function load(file) {
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(node_fs_1.default.readFileSync(file, "utf8"));
|
|
59
|
+
if (parsed && Array.isArray(parsed.reminders))
|
|
60
|
+
return parsed;
|
|
61
|
+
}
|
|
62
|
+
catch { /* missing and damaged stores both project to an empty store */ }
|
|
63
|
+
return { reminders: [] };
|
|
64
|
+
}
|
|
65
|
+
function save(file, store) {
|
|
66
|
+
const temporary = `${file}.tmp`;
|
|
67
|
+
node_fs_1.default.writeFileSync(temporary, `${JSON.stringify(store, null, 2)}\n`);
|
|
68
|
+
node_fs_1.default.renameSync(temporary, file);
|
|
69
|
+
}
|
|
70
|
+
function mutate(file, fn, lockTimeoutMs = 2_000) {
|
|
71
|
+
return withLock(file, () => {
|
|
72
|
+
const store = load(file);
|
|
73
|
+
const result = fn(store);
|
|
74
|
+
save(file, store);
|
|
75
|
+
return result;
|
|
76
|
+
}, lockTimeoutMs);
|
|
77
|
+
}
|
|
78
|
+
const WEEKDAYS = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
|
|
79
|
+
function validTz(tz) {
|
|
80
|
+
try {
|
|
81
|
+
new Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function wallParts(ms, tz) {
|
|
89
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
90
|
+
timeZone: tz,
|
|
91
|
+
year: "numeric",
|
|
92
|
+
month: "2-digit",
|
|
93
|
+
day: "2-digit",
|
|
94
|
+
hour: "2-digit",
|
|
95
|
+
minute: "2-digit",
|
|
96
|
+
second: "2-digit",
|
|
97
|
+
weekday: "short",
|
|
98
|
+
hour12: false,
|
|
99
|
+
});
|
|
100
|
+
const parts = {};
|
|
101
|
+
for (const part of formatter.formatToParts(ms))
|
|
102
|
+
parts[part.type] = part.value;
|
|
103
|
+
return {
|
|
104
|
+
y: Number(parts.year),
|
|
105
|
+
mo: Number(parts.month),
|
|
106
|
+
d: Number(parts.day),
|
|
107
|
+
hh: parts.hour === "24" ? 0 : Number(parts.hour),
|
|
108
|
+
mm: Number(parts.minute),
|
|
109
|
+
ss: Number(parts.second),
|
|
110
|
+
wd: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].indexOf(parts.weekday),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function utcOffsetMs(ms, tz) {
|
|
114
|
+
const wall = wallParts(ms, tz);
|
|
115
|
+
return Date.UTC(wall.y, wall.mo - 1, wall.d, wall.hh, wall.mm, wall.ss) - Math.floor(ms / 1_000) * 1_000;
|
|
116
|
+
}
|
|
117
|
+
function wallToUtc(y, mo, d, hh, mm, tz) {
|
|
118
|
+
const firstGuess = Date.UTC(y, mo - 1, d, hh, mm);
|
|
119
|
+
const secondGuess = firstGuess - utcOffsetMs(firstGuess, tz);
|
|
120
|
+
return firstGuess - utcOffsetMs(secondGuess, tz);
|
|
121
|
+
}
|
|
122
|
+
function parseRepeat(rule, tz) {
|
|
123
|
+
const normalized = String(rule || "").trim();
|
|
124
|
+
const normalizedTz = tz || undefined;
|
|
125
|
+
const zone = validTz(normalizedTz) ? normalizedTz : Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
126
|
+
let match = normalized.match(/^every:(\d+)(s|m|h|d)$/);
|
|
127
|
+
if (match) {
|
|
128
|
+
const multipliers = { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000 };
|
|
129
|
+
const unit = match[2];
|
|
130
|
+
const period = Number(match[1]) * multipliers[unit];
|
|
131
|
+
if (period < 60_000)
|
|
132
|
+
return { error: "间隔太短:every 至少 1 分钟(如 every:15m)" };
|
|
133
|
+
return {
|
|
134
|
+
kind: "interval",
|
|
135
|
+
description: `every ${match[1]}${unit}`,
|
|
136
|
+
next: (afterMs, anchorMs) => {
|
|
137
|
+
const base = Number.isFinite(anchorMs) ? Number(anchorMs) : afterMs;
|
|
138
|
+
const count = Math.max(1, Math.floor((afterMs - base) / period) + 1);
|
|
139
|
+
return base + count * period;
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
match = normalized.match(/^daily@(\d{1,2}):(\d{2})$/);
|
|
144
|
+
if (match) {
|
|
145
|
+
const [hours, minutes] = [Number(match[1]), Number(match[2])];
|
|
146
|
+
if (hours > 23 || minutes > 59)
|
|
147
|
+
return { error: `无效时间 ${match[1]}:${match[2]}` };
|
|
148
|
+
return {
|
|
149
|
+
kind: "daily",
|
|
150
|
+
description: `daily@${String(hours).padStart(2, "0")}:${match[2]} (${zone})`,
|
|
151
|
+
next: (afterMs) => {
|
|
152
|
+
const wall = wallParts(afterMs, zone);
|
|
153
|
+
for (let offset = 0; offset <= 2; offset += 1) {
|
|
154
|
+
const candidate = wallToUtc(wall.y, wall.mo, wall.d + offset, hours, minutes, zone);
|
|
155
|
+
if (candidate > afterMs)
|
|
156
|
+
return candidate;
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
match = normalized.match(/^weekly:([a-z,]+)@(\d{1,2}):(\d{2})$/);
|
|
163
|
+
if (match) {
|
|
164
|
+
const days = match[1].split(",").map((day) => WEEKDAYS.indexOf(day.trim()));
|
|
165
|
+
const [hours, minutes] = [Number(match[2]), Number(match[3])];
|
|
166
|
+
if (days.some((day) => day < 0))
|
|
167
|
+
return { error: `无效星期(可用: ${WEEKDAYS.join(",")}):${match[1]}` };
|
|
168
|
+
if (hours > 23 || minutes > 59)
|
|
169
|
+
return { error: `无效时间 ${match[2]}:${match[3]}` };
|
|
170
|
+
return {
|
|
171
|
+
kind: "weekly",
|
|
172
|
+
description: `weekly:${match[1]}@${String(hours).padStart(2, "0")}:${match[3]} (${zone})`,
|
|
173
|
+
next: (afterMs) => {
|
|
174
|
+
const wall = wallParts(afterMs, zone);
|
|
175
|
+
for (let offset = 0; offset <= 8; offset += 1) {
|
|
176
|
+
const candidate = wallToUtc(wall.y, wall.mo, wall.d + offset, hours, minutes, zone);
|
|
177
|
+
if (candidate > afterMs && days.includes(wallParts(candidate, zone).wd))
|
|
178
|
+
return candidate;
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return { error: `无法识别的 repeat 规则:${JSON.stringify(normalized)}。支持 every:<N><s|m|h|d>、daily@HH:MM、weekly:mon,fri@HH:MM` };
|
|
185
|
+
}
|
|
186
|
+
function toSummary(reminder) {
|
|
187
|
+
let recurrence = null;
|
|
188
|
+
if (reminder.repeat) {
|
|
189
|
+
const parsed = parseRepeat(reminder.repeat, reminder.tz || undefined);
|
|
190
|
+
recurrence = "error" in parsed
|
|
191
|
+
? { kind: "unsupported", description: String(reminder.repeat) }
|
|
192
|
+
: { kind: parsed.kind, description: parsed.description };
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
reminderId: reminder.reminderId,
|
|
196
|
+
ownerAgentId: reminder.ownerAgentId,
|
|
197
|
+
title: reminder.title,
|
|
198
|
+
fireAt: reminder.fireAt,
|
|
199
|
+
firedAt: reminder.firedAt ?? null,
|
|
200
|
+
createdAt: reminder.createdAt,
|
|
201
|
+
status: reminder.status,
|
|
202
|
+
msgRef: reminder.msgRef ?? null,
|
|
203
|
+
msgPermalink: reminder.msgPermalink ?? null,
|
|
204
|
+
recurrence,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function appendEvent(reminder, eventType, actorType, actorId, nextFireAt, ms) {
|
|
208
|
+
if (!Array.isArray(reminder.events))
|
|
209
|
+
reminder.events = [];
|
|
210
|
+
reminder.events.push({
|
|
211
|
+
eventId: (0, exports.newId)(),
|
|
212
|
+
reminderId: reminder.reminderId,
|
|
213
|
+
eventType,
|
|
214
|
+
actorType,
|
|
215
|
+
actorId: actorId ?? null,
|
|
216
|
+
occurredAt: (0, exports.nowIso)(ms),
|
|
217
|
+
nextFireAt: nextFireAt ?? null,
|
|
218
|
+
metadata: null,
|
|
219
|
+
});
|
|
220
|
+
}
|