agent-loft 0.2.30 → 0.2.31

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.
@@ -0,0 +1,281 @@
1
+ // 定时任务:到点让 agent 按一段提示词干活,结果回到一条对话里;agent 自己判断要不要提醒用户。
2
+ //
3
+ // 和「定时发送」(scheduled.ts)的区别:那个是替用户把一条消息晚点发出去,一次性;这个可以周期运行,
4
+ // 每次都带着「这是定时任务、没事就别打扰用户」的说明,结果按 agent 的判断决定推不推送。
5
+ //
6
+ // 按人隔离:每个任务记着主人(owner,用户 id),网页接口和 agent 的工具都只碰得到自己的。
7
+ // 到点时还要再核对一次主人和对话(见 task-runner.ts)。这里只管存取和算时间,不依赖 hub。
8
+ import crypto from "node:crypto";
9
+ import path from "node:path";
10
+ import { EventEmitter } from "node:events";
11
+ import { dataDir, readJson, writeJsonAtomic } from "../home.js";
12
+ import { describeSchedule, nextRun, normalizeSchedule, type TaskSchedule } from "./task-schedule.js";
13
+ import type { ConversationMode } from "./types.js";
14
+
15
+ /**
16
+ * 结果回到哪:
17
+ * conv:一条已有的对话(agent 在对话里帮你建的,默认回到那条对话),模式 / agent / 目录都跟着那条对话;
18
+ * dedicated:任务自己的对话,第一次运行时新建(key 记下来,之后续用),模式等按任务里的设置。
19
+ */
20
+ export type TaskTarget = { kind: "conv"; key: string } | { kind: "dedicated"; key?: string };
21
+
22
+ export type TaskRunStatus = "running" | "notified" | "silent" | "error" | "skipped" | "interrupted";
23
+
24
+ export type TaskRun = {
25
+ id: string;
26
+ at: number;
27
+ endedAt?: number;
28
+ status: TaskRunStatus;
29
+ /** 跳过 / 出错的原因,或者「手动运行」 */
30
+ note?: string;
31
+ convKey?: string;
32
+ /** 回复开头一句,列表里显示 */
33
+ preview?: string;
34
+ };
35
+
36
+ export type Task = {
37
+ id: string;
38
+ owner: string;
39
+ title: string;
40
+ prompt: string;
41
+ schedule: TaskSchedule;
42
+ /** smart:只在需要时提醒(agent 判断);always:每次都提醒 */
43
+ notify: "smart" | "always";
44
+ target: TaskTarget;
45
+ /** 专用对话的设置(target 是 conv 时不用,以那条对话为准) */
46
+ agent: string;
47
+ cwd?: string;
48
+ mode: ConversationMode;
49
+ model?: string;
50
+ effort?: string;
51
+ enabled: boolean;
52
+ createdAt: number;
53
+ updatedAt: number;
54
+ /** 谁建的:用户在页面上建的,还是 agent 在对话里建的 */
55
+ createdBy: "user" | "agent";
56
+ nextRunAt?: number;
57
+ /** 最近的在前 */
58
+ runs: TaskRun[];
59
+ };
60
+
61
+ /** 每人最多几个任务(暂停的也算) */
62
+ export const TASKS_PER_USER = 30;
63
+ const RUNS_KEPT = 20;
64
+ export const PROMPT_MAX = 4000;
65
+ export const TITLE_MAX = 60;
66
+
67
+ export const taskEvents = new EventEmitter<{ changed: [owner: string] }>();
68
+
69
+ const storePath = path.join(dataDir, "tasks.json");
70
+ let cache: Task[] | null = null;
71
+
72
+ function load(): Task[] {
73
+ cache ??= readJson<Task[]>(storePath, []).filter((t) => t && typeof t.id === "string" && typeof t.owner === "string" && t.schedule);
74
+ return cache;
75
+ }
76
+
77
+ function save(owner?: string): void {
78
+ writeJsonAtomic(storePath, load());
79
+ if (owner) taskEvents.emit("changed", owner);
80
+ }
81
+
82
+ export function resetTasksForTest(): void {
83
+ cache = null;
84
+ }
85
+
86
+ // ---------- 查询 ----------
87
+
88
+ export function allTasks(): Task[] {
89
+ return load();
90
+ }
91
+
92
+ export function listTasks(owner: string): Task[] {
93
+ return load()
94
+ .filter((t) => t.owner === owner)
95
+ .sort((a, b) => b.createdAt - a.createdAt);
96
+ }
97
+
98
+ /** 只认主人:别人的任务一律当不存在 */
99
+ export function getTask(owner: string, id: string): Task | undefined {
100
+ return load().find((t) => t.id === id && t.owner === owner);
101
+ }
102
+
103
+ export function findTask(id: string): Task | undefined {
104
+ return load().find((t) => t.id === id);
105
+ }
106
+
107
+ // ---------- 增删改 ----------
108
+
109
+ export type TaskInput = {
110
+ title?: unknown;
111
+ prompt?: unknown;
112
+ schedule?: unknown;
113
+ notify?: unknown;
114
+ target?: unknown;
115
+ agent?: unknown;
116
+ cwd?: unknown;
117
+ mode?: unknown;
118
+ model?: unknown;
119
+ effort?: unknown;
120
+ enabled?: unknown;
121
+ };
122
+
123
+ const str = (v: unknown) => (typeof v === "string" ? v.trim() : "");
124
+
125
+ function cleanTitle(v: unknown, prompt: string): string {
126
+ const t = str(v).replace(/\s+/g, " ");
127
+ return (t || prompt.replace(/\s+/g, " ")).slice(0, TITLE_MAX);
128
+ }
129
+
130
+ function cleanPrompt(v: unknown): string {
131
+ const p = str(v);
132
+ if (!p) throw new Error("要写清楚让 agent 做什么");
133
+ if (p.length > PROMPT_MAX) throw new Error(`提示词最多 ${PROMPT_MAX} 字`);
134
+ return p;
135
+ }
136
+
137
+ function cleanTarget(v: unknown, prev?: TaskTarget): TaskTarget {
138
+ const r = (v ?? {}) as Record<string, unknown>;
139
+ if (r.kind === "conv" && typeof r.key === "string" && r.key) return { kind: "conv", key: r.key };
140
+ // 改成专用对话时沿用之前那条专用对话
141
+ return prev?.kind === "dedicated" ? prev : { kind: "dedicated" };
142
+ }
143
+
144
+ export function createTask(owner: string, input: TaskInput, by: "user" | "agent", now = Date.now()): Task {
145
+ if (listTasks(owner).length >= TASKS_PER_USER) throw new Error(`每人最多 ${TASKS_PER_USER} 个定时任务,先删掉几个不用的`);
146
+ const prompt = cleanPrompt(input.prompt);
147
+ const schedule = normalizeSchedule(input.schedule, now);
148
+ const agent = str(input.agent) || "claude";
149
+ const task: Task = {
150
+ id: `t_${crypto.randomBytes(5).toString("hex")}`,
151
+ owner,
152
+ title: cleanTitle(input.title, prompt),
153
+ prompt,
154
+ schedule,
155
+ notify: input.notify === "always" ? "always" : "smart",
156
+ target: cleanTarget(input.target),
157
+ agent,
158
+ ...(str(input.cwd) ? { cwd: str(input.cwd) } : {}),
159
+ mode: input.mode === "work" ? "work" : "chat",
160
+ ...(str(input.model) ? { model: str(input.model) } : {}),
161
+ ...(str(input.effort) ? { effort: str(input.effort) } : {}),
162
+ enabled: input.enabled !== false,
163
+ createdAt: now,
164
+ updatedAt: now,
165
+ createdBy: by,
166
+ runs: [],
167
+ };
168
+ task.nextRunAt = task.enabled ? nextRun(schedule, now) : undefined;
169
+ load().push(task);
170
+ save(owner);
171
+ return task;
172
+ }
173
+
174
+ /** 只改给了的字段。改了频率或重新启用都重算下次时间 */
175
+ export function updateTask(owner: string, id: string, patch: TaskInput, now = Date.now()): Task {
176
+ const task = getTask(owner, id);
177
+ if (!task) throw new Error("这个任务不存在");
178
+ const next: Task = { ...task };
179
+ if (patch.prompt !== undefined) next.prompt = cleanPrompt(patch.prompt);
180
+ if (patch.title !== undefined) next.title = cleanTitle(patch.title, next.prompt);
181
+ if (patch.schedule !== undefined) next.schedule = normalizeSchedule(patch.schedule, now);
182
+ if (patch.notify !== undefined) next.notify = patch.notify === "always" ? "always" : "smart";
183
+ if (patch.target !== undefined) next.target = cleanTarget(patch.target, task.target);
184
+ if (patch.agent !== undefined && str(patch.agent)) next.agent = str(patch.agent);
185
+ if (patch.cwd !== undefined) {
186
+ if (str(patch.cwd)) next.cwd = str(patch.cwd);
187
+ else delete next.cwd;
188
+ }
189
+ if (patch.mode !== undefined) next.mode = patch.mode === "work" ? "work" : "chat";
190
+ for (const k of ["model", "effort"] as const) {
191
+ if (patch[k] === undefined) continue;
192
+ if (str(patch[k])) next[k] = str(patch[k]);
193
+ else delete next[k];
194
+ }
195
+ if (patch.enabled !== undefined) next.enabled = patch.enabled !== false;
196
+ // 专用对话的 agent / 目录 / 模式变了:旧的那条对话对不上了,下次新开一条
197
+ if (
198
+ next.target.kind === "dedicated" &&
199
+ next.target.key &&
200
+ (next.agent !== task.agent || next.cwd !== task.cwd || next.mode !== task.mode)
201
+ ) {
202
+ next.target = { kind: "dedicated" };
203
+ }
204
+ next.updatedAt = now;
205
+ next.nextRunAt = next.enabled ? nextRun(next.schedule, now) : undefined;
206
+ // 「一次」的时间已经过了还要启用:没有下一次
207
+ if (next.enabled && next.nextRunAt === undefined) throw new Error("这个任务已经没有下一次运行时间了,改一下时间");
208
+ // 整个换成 next:Object.assign 不会删掉 next 里去掉了的字段(清空目录 / 模型时)
209
+ for (const k of Object.keys(task) as (keyof Task)[]) if (!(k in next) || next[k] === undefined) delete task[k];
210
+ Object.assign(task, next);
211
+ save(owner);
212
+ return task;
213
+ }
214
+
215
+ export function deleteTask(owner: string, id: string): Task | undefined {
216
+ const list = load();
217
+ const i = list.findIndex((t) => t.id === id && t.owner === owner);
218
+ if (i < 0) return undefined;
219
+ const [removed] = list.splice(i, 1);
220
+ save(owner);
221
+ return removed;
222
+ }
223
+
224
+ /** 删用户时:他的任务一起删掉,不转给管理员 */
225
+ export function deleteUserTasks(owner: string): number {
226
+ const list = load();
227
+ const kept = list.filter((t) => t.owner !== owner);
228
+ const n = list.length - kept.length;
229
+ if (n === 0) return 0;
230
+ cache = kept;
231
+ save(owner);
232
+ return n;
233
+ }
234
+
235
+ // ---------- 运行记录(task-runner 用) ----------
236
+
237
+ /** 暂停并说明原因(对话被删了、换了主人……) */
238
+ export function pauseTask(task: Task, note: string, now = Date.now()): void {
239
+ task.enabled = false;
240
+ delete task.nextRunAt;
241
+ pushRun(task, { id: runId(), at: now, endedAt: now, status: "skipped", note });
242
+ }
243
+
244
+ export function runId(): string {
245
+ return `r_${crypto.randomBytes(4).toString("hex")}`;
246
+ }
247
+
248
+ /** 记一条运行(最新的在前,留最近 RUNS_KEPT 条)并落盘 */
249
+ export function pushRun(task: Task, run: TaskRun): TaskRun {
250
+ task.runs = [run, ...task.runs.filter((r) => r.id !== run.id)].slice(0, RUNS_KEPT);
251
+ save(task.owner);
252
+ return run;
253
+ }
254
+
255
+ export function finishRun(taskId: string, id: string, patch: Partial<TaskRun>): Task | undefined {
256
+ const task = findTask(taskId);
257
+ const run = task?.runs.find((r) => r.id === id);
258
+ if (!task || !run) return undefined;
259
+ Object.assign(run, patch);
260
+ save(task.owner);
261
+ return task;
262
+ }
263
+
264
+ /** 到点了:算出再下一次(「一次」跑完就停用) */
265
+ export function advance(task: Task, now = Date.now()): void {
266
+ task.nextRunAt = nextRun(task.schedule, now);
267
+ if (task.nextRunAt === undefined) task.enabled = false;
268
+ save(task.owner);
269
+ }
270
+
271
+ export function persistTask(task: Task): void {
272
+ save(task.owner);
273
+ }
274
+
275
+ // ---------- 给页面 / agent 看的 ----------
276
+
277
+ export type TaskView = Task & { summary: string };
278
+
279
+ export function viewOf(task: Task): TaskView {
280
+ return { ...task, summary: describeSchedule(task.schedule) };
281
+ }
package/src/core/types.ts CHANGED
@@ -188,6 +188,8 @@ export type SessionOptions = {
188
188
  };
189
189
  /** 模式换过、这个 agent 会话还不知道:换不了系统提示的 agent(Codex)要在第一条消息里说一声 */
190
190
  announceMode?: boolean;
191
+ /** 给 agent 定时任务工具(core/tasks-mcp.ts):替谁、在哪条对话里管理。只有有主人的对话才给 */
192
+ tasks?: { userId: string; convKey: string };
191
193
  };
192
194
 
193
195
  export type ConversationMode = "chat" | "work";
package/src/main.ts CHANGED
@@ -11,6 +11,8 @@ import { startWeb, stopWeb } from "./channels/web/server.js";
11
11
  import { home } from "./home.js";
12
12
  import { startMemoryCurator } from "./core/memory-curator.js";
13
13
  import { startScheduler, stopScheduler } from "./core/scheduled.js";
14
+ import { startTaskRunner, stopTaskRunner } from "./core/task-runner.js";
15
+ import { startTasksBridge, stopTasksBridge } from "./core/tasks-bridge.js";
14
16
 
15
17
  async function startDriver(id: "claude" | "codex"): Promise<void> {
16
18
  const driver = hub.driver(id);
@@ -52,6 +54,9 @@ async function main(): Promise<void> {
52
54
  hub.send(m.key, { text: m.text, attachments: [] }, { channel: "schedule", channelId: "schedule", name: "定时发送" });
53
55
  hub.emitEvent(m.key, { type: "meta" });
54
56
  });
57
+ // 定时任务:agent 用的工具通道先开,再开始按点运行(停机期间错过的补跑)
58
+ startTasksBridge();
59
+ startTaskRunner();
55
60
  // 先查一遍 agent 装没装、登没登录,网页打开时新对话里就只列出能用的
56
61
  void agentHealth().catch(() => {});
57
62
 
@@ -71,6 +76,9 @@ async function main(): Promise<void> {
71
76
  stopFeishu();
72
77
  await stopWeb();
73
78
  await hub.shutdown().catch(() => {});
79
+ // hub 收尾时会把正在跑的定时任务记成中断,所以放在它后面
80
+ stopTaskRunner();
81
+ stopTasksBridge();
74
82
  flushStore();
75
83
  process.exit(0);
76
84
  };
@@ -0,0 +1,206 @@
1
+ // 定时任务工具的 MCP 服务(stdio):给 agent 用的 list / create / update / delete。
2
+ //
3
+ // 这里只是个传话的:每次调用原样转给主进程(core/tasks-bridge.ts,经 unix socket),带上拉起时
4
+ // 定死的用户、对话和签名(见 core/tasks-mcp.ts)。校验、授权卡、真正的增删改都在主进程里做。
5
+ //
6
+ // 和记忆工具一样只实现 MCP 里用得到的那一小块;stdout 只能写协议消息,日志一律走 stderr。
7
+ import net from "node:net";
8
+
9
+ const socketPath = process.env.AGENT_LOFT_TASKS_SOCKET ?? "";
10
+ const caller = {
11
+ user: process.env.AGENT_LOFT_TASKS_USER ?? "",
12
+ conv: process.env.AGENT_LOFT_TASKS_CONV ?? "",
13
+ sig: process.env.AGENT_LOFT_TASKS_SIG ?? "",
14
+ };
15
+ const tz = process.env.AGENT_LOFT_TZ || "服务端本地时区";
16
+
17
+ type Request = { jsonrpc: "2.0"; id?: string | number | null; method: string; params?: Record<string, unknown> };
18
+
19
+ const SCHEDULE = {
20
+ type: "object",
21
+ description:
22
+ `什么时候运行,时间一律按 ${tz} 解释。kind 选一种:` +
23
+ "once(一次:at 写「YYYY-MM-DD HH:MM」,或者 in_minutes 写多少分钟后)、" +
24
+ "daily(每天 time)、weekdays(工作日 time)、weekly(每周 days 的 time,days 里 0=周日…6=周六)、" +
25
+ "monthly(每月 day 日的 time)、hourly(每 every 小时一次,在第 minute 分)、cron(5 段 cron 表达式 expr)。" +
26
+ "time 写「HH:MM」。两次运行至少隔 15 分钟。",
27
+ properties: {
28
+ kind: { type: "string", enum: ["once", "daily", "weekdays", "weekly", "monthly", "hourly", "cron"] },
29
+ at: { type: "string", description: "once:运行时间,「YYYY-MM-DD HH:MM」" },
30
+ in_minutes: { type: "number", description: "once:多少分钟之后(和 at 二选一)" },
31
+ time: { type: "string", description: "daily / weekdays / weekly / monthly:几点,「HH:MM」" },
32
+ days: { type: "array", items: { type: "integer" }, description: "weekly:星期几,0=周日 … 6=周六" },
33
+ day: { type: "integer", description: "monthly:几号(1-31)" },
34
+ every: { type: "integer", description: "hourly:每几小时(1-23)" },
35
+ minute: { type: "integer", description: "hourly:第几分(0-59)" },
36
+ expr: { type: "string", description: "cron:分 时 日 月 周" },
37
+ },
38
+ required: ["kind"],
39
+ additionalProperties: false,
40
+ };
41
+
42
+ const TOOLS = [
43
+ {
44
+ name: "list_tasks",
45
+ description: "列出用户的定时任务(id、名称、频率、下次运行时间、最近一次结果),同时返回现在的时间。改、删任务之前先用它拿到 id。",
46
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
47
+ annotations: { title: "查看定时任务", readOnlyHint: true, openWorldHint: false },
48
+ },
49
+ {
50
+ name: "create_task",
51
+ description:
52
+ "给用户建一个定时任务:到点 agent 会收到 prompt 并照着做,结果默认回到当前这条对话。用户说「每天 / 每周 / 明早 / 两小时后帮我……」时用。" +
53
+ "notify 默认 smart:每次运行时由 agent 自己判断有没有需要告诉用户的事,没有就不提醒(适合「有更新才告诉我」);always:每次都提醒。" +
54
+ "prompt 要写成到点时独立可执行的完整指令(那时用户不在,不能再问他),需要对比时写清楚怎么判断(比如「和上次的结果比」「新版本号大于当前版本才算更新」)。" +
55
+ "调用后会弹授权卡给用户确认,确认了才会建。",
56
+ inputSchema: {
57
+ type: "object",
58
+ properties: {
59
+ title: { type: "string", description: "简短的名称,比如「检查 agent-loft 更新」" },
60
+ prompt: { type: "string", description: "到点时发给 agent 的完整指令" },
61
+ schedule: SCHEDULE,
62
+ notify: { type: "string", enum: ["smart", "always"], description: "smart:有需要才提醒(默认);always:每次都提醒" },
63
+ reply_to: {
64
+ type: "string",
65
+ enum: ["this_conversation", "dedicated"],
66
+ description: "this_conversation:结果回到当前对话(默认);dedicated:给任务单独开一条对话(可以指定 mode)",
67
+ },
68
+ mode: { type: "string", enum: ["chat", "work"], description: "dedicated 时用:chat 只能联网搜索、开销小(默认);work 能执行命令、读写本机文件" },
69
+ },
70
+ required: ["prompt", "schedule"],
71
+ additionalProperties: false,
72
+ },
73
+ annotations: { title: "新建定时任务", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
74
+ },
75
+ {
76
+ name: "update_task",
77
+ description: "修改用户的一个定时任务:改名称、提示词、频率、提醒方式,或者暂停 / 恢复(enabled)。只给要改的字段。除了暂停以外的修改会弹授权卡让用户确认。",
78
+ inputSchema: {
79
+ type: "object",
80
+ properties: {
81
+ id: { type: "string", description: "任务 id(list_tasks 里的),形如 t_1a2b3c4d5e" },
82
+ title: { type: "string" },
83
+ prompt: { type: "string" },
84
+ schedule: SCHEDULE,
85
+ notify: { type: "string", enum: ["smart", "always"] },
86
+ enabled: { type: "boolean", description: "false 暂停,true 恢复" },
87
+ },
88
+ required: ["id"],
89
+ additionalProperties: false,
90
+ },
91
+ annotations: { title: "修改定时任务", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
92
+ },
93
+ {
94
+ name: "delete_task",
95
+ description: "删掉用户的一个定时任务(用户要求删除 / 取消时用;只是暂时不想运行的话用 update_task 暂停)。会弹授权卡让用户确认。",
96
+ inputSchema: {
97
+ type: "object",
98
+ properties: { id: { type: "string", description: "任务 id" } },
99
+ required: ["id"],
100
+ additionalProperties: false,
101
+ },
102
+ annotations: { title: "删除定时任务", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
103
+ },
104
+ ];
105
+
106
+ function text(t: string, isError = false) {
107
+ return { content: [{ type: "text", text: t }], ...(isError ? { isError: true } : {}) };
108
+ }
109
+
110
+ /** 转给主进程,等它回一行 JSON */
111
+ function forward(tool: string, args: Record<string, unknown>): Promise<{ text: string; isError?: boolean }> {
112
+ return new Promise((resolve) => {
113
+ if (!socketPath || !caller.user || !caller.conv) {
114
+ resolve({ text: "定时任务工具没有正确配置(缺少用户或对话)。", isError: true });
115
+ return;
116
+ }
117
+ const sock = net.createConnection(socketPath);
118
+ let buf = "";
119
+ let done = false;
120
+ const finish = (r: { text: string; isError?: boolean }) => {
121
+ if (done) return;
122
+ done = true;
123
+ sock.destroy();
124
+ resolve(r);
125
+ };
126
+ sock.setEncoding("utf8");
127
+ sock.on("connect", () => sock.write(`${JSON.stringify({ ...caller, tool, args })}\n`));
128
+ sock.on("data", (chunk: string) => {
129
+ buf += chunk;
130
+ const n = buf.indexOf("\n");
131
+ if (n < 0) return;
132
+ try {
133
+ const res = JSON.parse(buf.slice(0, n)) as { text?: string; isError?: boolean };
134
+ finish({ text: String(res.text ?? ""), ...(res.isError ? { isError: true } : {}) });
135
+ } catch {
136
+ finish({ text: "agent-loft 返回的内容看不懂。", isError: true });
137
+ }
138
+ });
139
+ sock.on("error", (err) => finish({ text: `连不上 agent-loft(${err.message})。它可能刚重启过,开个新会话再试。`, isError: true }));
140
+ sock.on("close", () => finish({ text: "agent-loft 断开了连接。", isError: true }));
141
+ });
142
+ }
143
+
144
+ async function handle(req: Request): Promise<unknown> {
145
+ switch (req.method) {
146
+ case "initialize":
147
+ return {
148
+ protocolVersion: typeof req.params?.protocolVersion === "string" ? req.params.protocolVersion : "2025-06-18",
149
+ capabilities: { tools: {} },
150
+ serverInfo: { name: "agent-loft-tasks", version: "1.0.0" },
151
+ };
152
+ case "ping":
153
+ return {};
154
+ case "tools/list":
155
+ return { tools: TOOLS };
156
+ case "tools/call": {
157
+ const params = req.params ?? {};
158
+ const name = String(params.name ?? "");
159
+ if (!TOOLS.some((t) => t.name === name)) return text(`没有工具 ${name}`, true);
160
+ const res = await forward(name, (params.arguments as Record<string, unknown> | undefined) ?? {});
161
+ return text(res.text, !!res.isError);
162
+ }
163
+ default:
164
+ throw Object.assign(new Error(`不支持 ${req.method}`), { code: -32601 });
165
+ }
166
+ }
167
+
168
+ function reply(message: unknown): void {
169
+ process.stdout.write(`${JSON.stringify(message)}\n`);
170
+ }
171
+
172
+ function onLine(line: string): void {
173
+ if (!line.trim()) return;
174
+ let req: Request;
175
+ try {
176
+ req = JSON.parse(line) as Request;
177
+ } catch {
178
+ reply({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "JSON 解析失败" } });
179
+ return;
180
+ }
181
+ const isRequest = req.id !== undefined && req.id !== null;
182
+ // 不按顺序等:一个调用在等授权卡的时候,ping 之类照样要回
183
+ handle(req).then(
184
+ (result) => {
185
+ if (isRequest) reply({ jsonrpc: "2.0", id: req.id, result });
186
+ },
187
+ (err) => {
188
+ if (!isRequest) return;
189
+ const code = (err as { code?: number }).code ?? -32603;
190
+ reply({ jsonrpc: "2.0", id: req.id, error: { code, message: err instanceof Error ? err.message : String(err) } });
191
+ },
192
+ );
193
+ }
194
+
195
+ let buf = "";
196
+ process.stdin.setEncoding("utf8");
197
+ process.stdin.on("data", (chunk: string) => {
198
+ buf += chunk;
199
+ let n: number;
200
+ while ((n = buf.indexOf("\n")) >= 0) {
201
+ const line = buf.slice(0, n);
202
+ buf = buf.slice(n + 1);
203
+ onLine(line);
204
+ }
205
+ });
206
+ process.stdin.on("end", () => process.exit(0));