libero-mcp 0.2.24 → 0.2.26

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,8 @@
1
+ -- MV-COACH-5 slice 1 (2026-08-12): reminders 表升格为意图队列
2
+ -- 用于 3F 架构:脑说意图 → 手记意图 + 调 adapter → adapter 翻译成宿主机制
3
+ -- 复用现有列:remind_at(时间)、content(action_prompt)、timeblock_id(schedule_id)、status
4
+ ALTER TABLE reminders ADD COLUMN intent_type TEXT; -- pre_start_reminder / missed_check / daily_review / one_off_reminder
5
+ ALTER TABLE reminders ADD COLUMN trigger_kind TEXT; -- relative / absolute / recurring
6
+ ALTER TABLE reminders ADD COLUMN trigger_payload TEXT; -- JSON: {offset:"-15m", relative_to:"schedule.start"} 等
7
+ ALTER TABLE reminders ADD COLUMN host_job_id TEXT; -- adapter 写回(hermes cron job id)
8
+ ALTER TABLE reminders ADD COLUMN host_kind TEXT; -- "hermes_cron" / "openclaw" / "cc_pending"
package/data/schema.sql CHANGED
@@ -68,3 +68,8 @@ CREATE TABLE IF NOT EXISTS user_platform_alias (
68
68
  );
69
69
 
70
70
  CREATE INDEX IF NOT EXISTS idx_alias_libero ON user_platform_alias(libero_user_id);
71
+
72
+ -- reminders 表由 007 migration 建,此处不显式列(IF NOT EXISTS 不重建表)
73
+ -- MV-COACH-5 slice 1(migration 016):reminders 升格为意图队列,加 5 列:
74
+ -- intent_type / trigger_kind / trigger_payload / host_job_id / host_kind
75
+ -- 详见 data/migrations/016_intent_columns.sql
@@ -0,0 +1,8 @@
1
+ -- MV-COACH-5 slice 1 (2026-08-12): reminders 表升格为意图队列
2
+ -- 用于 3F 架构:脑说意图 → 手记意图 + 调 adapter → adapter 翻译成宿主机制
3
+ -- 复用现有列:remind_at(时间)、content(action_prompt)、timeblock_id(schedule_id)、status
4
+ ALTER TABLE reminders ADD COLUMN intent_type TEXT; -- pre_start_reminder / missed_check / daily_review / one_off_reminder
5
+ ALTER TABLE reminders ADD COLUMN trigger_kind TEXT; -- relative / absolute / recurring
6
+ ALTER TABLE reminders ADD COLUMN trigger_payload TEXT; -- JSON: {offset:"-15m", relative_to:"schedule.start"} 等
7
+ ALTER TABLE reminders ADD COLUMN host_job_id TEXT; -- adapter 写回(hermes cron job id)
8
+ ALTER TABLE reminders ADD COLUMN host_kind TEXT; -- "hermes_cron" / "openclaw" / "cc_pending"
@@ -68,3 +68,8 @@ CREATE TABLE IF NOT EXISTS user_platform_alias (
68
68
  );
69
69
 
70
70
  CREATE INDEX IF NOT EXISTS idx_alias_libero ON user_platform_alias(libero_user_id);
71
+
72
+ -- reminders 表由 007 migration 建,此处不显式列(IF NOT EXISTS 不重建表)
73
+ -- MV-COACH-5 slice 1(migration 016):reminders 升格为意图队列,加 5 列:
74
+ -- intent_type / trigger_kind / trigger_payload / host_job_id / host_kind
75
+ -- 详见 data/migrations/016_intent_columns.sql
@@ -11,6 +11,7 @@ import { createProfileTools } from "./src/profile/index.js";
11
11
  import { createProfileConfirmTools } from "./src/profile/confirm-default.js";
12
12
  import { createReminderTools } from "./src/reminder/index.js";
13
13
  import { createSchedulerTools } from "./src/scheduler/index.js";
14
+ import { createAdaptersWithDb, selectAdapter } from "./src/host-adapters/index.js";
14
15
  import { createTaskTools } from "./src/task/index.js";
15
16
  import { createCoachModTools } from "./src/coachmod/index.js";
16
17
  import { FsYamlKnowledgeRepo } from "../knowledge/src/repo/fs-yaml.js";
@@ -885,6 +886,75 @@ function buildToolRegistry(ctx) {
885
886
  },
886
887
  });
887
888
  // ============================================================
889
+ // scheduler MV-COACH-5 (2 tools — 3F 架构意图 API)
890
+ // ============================================================
891
+ tools.push({
892
+ name: "scheduler.setIntent",
893
+ description: "MV-COACH-5 3F 架构 §4.1:注册一个意图(提前提醒/追问/复盘/一次性提醒)。" +
894
+ "脑层(skill)调这个,不直接调宿主的 cronjob——通用核心记意图到 DB,调宿主适配层翻译成该平台机制。" +
895
+ "hermes 上 → 翻译成 cron job;无推送能力的平台 → 标 pending 等用户开口补推(slice 4 实装)。" +
896
+ "⚠️ slice 1 纯加法:现有 skill 继续直调 cronjob(v4/v5 路径不破坏),slice 3 才迁移 skill 调本工具。" +
897
+ "intent_type ∈ {pre_start_reminder, missed_check, daily_review, one_off_reminder};" +
898
+ "pre_start/missed_check 必填 schedule_id。trigger.kind ∈ {relative, absolute, recurring}。",
899
+ inputSchema: {
900
+ user_id: z.string().optional(),
901
+ target_user_id: z.string().min(1).optional(),
902
+ intent_type: z.enum([
903
+ "pre_start_reminder",
904
+ "missed_check",
905
+ "daily_review",
906
+ "one_off_reminder",
907
+ ]),
908
+ schedule_id: z.string().optional(),
909
+ trigger: z.object({
910
+ kind: z.enum(["relative", "absolute", "recurring"]),
911
+ offset: z.string().optional(),
912
+ relative_to: z.enum(["schedule.start", "now"]).optional(),
913
+ at: z.string().optional(),
914
+ cron_expr: z.string().optional(),
915
+ }),
916
+ action_prompt: z.string().min(1),
917
+ },
918
+ needsUserIdInjection: true,
919
+ handler: async (rawInput) => {
920
+ const r = injector(rawInput);
921
+ if ("error" in r)
922
+ return errorContent(r.error);
923
+ try {
924
+ const result = await scheduler.setIntent({
925
+ user_id: r.user_id,
926
+ intent_type: rawInput.intent_type,
927
+ schedule_id: rawInput.schedule_id,
928
+ trigger: rawInput.trigger,
929
+ action_prompt: rawInput.action_prompt,
930
+ });
931
+ return asContent(result);
932
+ }
933
+ catch (e) {
934
+ return errorContent(e.message);
935
+ }
936
+ },
937
+ });
938
+ tools.push({
939
+ name: "scheduler.cancelIntent",
940
+ description: "MV-COACH-5 3F 架构 §4.2:撤销一个意图(用户删日程时连带撤提醒/追问)。" +
941
+ "intent_id 必填(从 setIntent 返回值拿)。",
942
+ inputSchema: {
943
+ intent_id: z.string().min(1),
944
+ },
945
+ handler: async (rawInput) => {
946
+ try {
947
+ const result = await scheduler.cancelIntent({
948
+ intent_id: rawInput.intent_id,
949
+ });
950
+ return asContent(result);
951
+ }
952
+ catch (e) {
953
+ return errorContent(e.message);
954
+ }
955
+ },
956
+ });
957
+ // ============================================================
888
958
  // task (6 tools — S29 P2-3 + 事一 draft/commit, takes raw db)
889
959
  // ============================================================
890
960
  tools.push({
@@ -1047,7 +1117,8 @@ export function buildServer(db) {
1047
1117
  const profile = createProfileTools(repos.profileRepo, repos.identityRepo);
1048
1118
  const profileConfirm = createProfileConfirmTools(db);
1049
1119
  const reminder = createReminderTools(repos.reminderRepo, undefined, db);
1050
- const scheduler = createSchedulerTools(repos.timeblockRepo, repos.reminderRepo, repos.categoryRepo);
1120
+ const scheduler = createSchedulerTools(repos.timeblockRepo, repos.reminderRepo, repos.categoryRepo, selectAdapter(createAdaptersWithDb(db)), // MV-COACH-5 slice 1: 3F 架构宿主适配层(带 db)
1121
+ db);
1051
1122
  const task = createTaskTools(db);
1052
1123
  // ── COACH-MOD slice 1: knowledge repo + coachmod tools ──
1053
1124
  const knowledgeRepo = new FsYamlKnowledgeRepo();
@@ -0,0 +1,178 @@
1
+ /**
2
+ * MV-COACH-5 §5.2/§5.4 — HermesCronAdapter(hermes 平台适配层)。
3
+ *
4
+ * spike 结论(2026-08-12):libero MCP 是 hermes 的子进程,cronjob 工具是 hermes
5
+ * 给自己 LLM 用的,不反向暴露给子进程。adapter 必须走 spawn CLI:
6
+ * - publish: `hermes cron create <schedule> <prompt> --name ... --deliver origin [--repeat 1]`
7
+ * - cancel: `hermes cron remove <job_id>`
8
+ *
9
+ * 本文件先实装纯函数 translateTriggerToHermesSchedule(核心可测逻辑),
10
+ * publish/cancel/detect 的 spawn 部分在 slice 1 后续补(需带 mock 测试)。
11
+ */
12
+ import { execFileSync } from "node:child_process";
13
+ /**
14
+ * §5.4 trigger → hermes cron schedule 翻译表(纯函数)。
15
+ *
16
+ * | trigger | hermes cron schedule |
17
+ * |---|---|
18
+ * | relative -15m from schedule.start | schedule.start-15min 的 cron(一次性,repeat=1)|
19
+ * | relative +5m from now | "+5m"(hermes 原生相对)|
20
+ * | relative +30m from schedule.start | schedule.start+offset 的 cron |
21
+ * | absolute at=ISO | 锁日期的 cron(一次性)|
22
+ * | recurring cron_expr | 直接用 |
23
+ *
24
+ * §4.5 静默折叠声明:
25
+ * - relative_to=schedule.start 但缺 scheduleStartIso → 抛错(不静默返回空)
26
+ * - offset 格式非法 → 抛错
27
+ * - absolute 过去时间(> 120s 前)→ 抛错(hermes 会拒)
28
+ */
29
+ export function translateTriggerToHermesSchedule(trigger, ctx = {}) {
30
+ switch (trigger.kind) {
31
+ case "relative":
32
+ return translateRelative(trigger, ctx);
33
+ case "absolute":
34
+ return translateAbsolute(trigger);
35
+ case "recurring":
36
+ return { schedule: trigger.cron_expr }; // repeat=undefined = 循环
37
+ }
38
+ }
39
+ /** relative 翻译:算出触发时刻 → 转 cron。 */
40
+ function translateRelative(trigger, ctx) {
41
+ const offsetMs = parseOffset(trigger.offset); // 正负均可
42
+ if (trigger.relative_to === "now") {
43
+ // hermes 原生支持 "+5m" / "+30m" 相对表达式,不用算绝对时间
44
+ // 注意:hermes 的相对格式是 "+5m"(带 + 号),offset 已是这个格式直接透传
45
+ return { schedule: trigger.offset, repeat: 1 };
46
+ }
47
+ // relative_to === "schedule.start"
48
+ if (!ctx.scheduleStartIso) {
49
+ throw new Error("relative_to=schedule.start 需要 scheduleStartIso(从 DB 查的 schedule 开始时间)");
50
+ }
51
+ const startMs = new Date(ctx.scheduleStartIso).getTime();
52
+ if (Number.isNaN(startMs)) {
53
+ throw new Error(`scheduleStartIso 格式非法: ${ctx.scheduleStartIso}`);
54
+ }
55
+ const triggerMs = startMs + offsetMs;
56
+ const triggerDate = new Date(triggerMs);
57
+ // 转本地时区的 cron(分 时 日 月 *,不填 year)
58
+ // 注意:相对 schedule 的具体某天 = 一次性,用 day/month 锁定日期
59
+ const cron = `${triggerDate.getMinutes()} ${triggerDate.getHours()} ${triggerDate.getDate()} ${triggerDate.getMonth() + 1} *`;
60
+ return { schedule: cron, repeat: 1 };
61
+ }
62
+ /** absolute 翻译:ISO → 锁日期的 cron(一次性)。 */
63
+ function translateAbsolute(trigger) {
64
+ const ms = new Date(trigger.at).getTime();
65
+ if (Number.isNaN(ms)) {
66
+ throw new Error(`absolute at 格式非法: ${trigger.at}`);
67
+ }
68
+ // §4.5:hermes 拒绝 > 120s 过去的时间
69
+ const nowMs = Date.now();
70
+ if (ms < nowMs - 120 * 1000) {
71
+ throw new Error(`absolute at 已过去 > 120s(hermes 会拒):${trigger.at}`);
72
+ }
73
+ const d = new Date(ms);
74
+ const cron = `${d.getMinutes()} ${d.getHours()} ${d.getDate()} ${d.getMonth() + 1} *`;
75
+ return { schedule: cron, repeat: 1 };
76
+ }
77
+ /**
78
+ * 解析 offset 字符串 → 毫秒。
79
+ * 格式:[+-]数字+单位(m/h),如 "-15m" / "+5m" / "+2h" / "30m"(无符号 = 正)
80
+ */
81
+ export function parseOffset(offset) {
82
+ const m = offset.match(/^([+-]?)(\d+)([mh])$/i);
83
+ if (!m) {
84
+ throw new Error(`offset 格式非法: "${offset}"(应为 [+-]数字+m/h,如 -15m / +5m / +2h)`);
85
+ }
86
+ const sign = m[1] === "-" ? -1 : 1;
87
+ const n = parseInt(m[2], 10);
88
+ const unitMs = m[3].toLowerCase() === "h" ? 60 * 60 * 1000 : 60 * 1000;
89
+ return sign * n * unitMs;
90
+ }
91
+ // ─────────────────────────────────────────────────────────────
92
+ // HermesCronAdapter(detect + publish + cancel 的 spawn 部分)
93
+ // ─────────────────────────────────────────────────────────────
94
+ /**
95
+ * HermesCronAdapter——hermes 平台的意图适配层。
96
+ *
97
+ * detect 三探(任一命中):
98
+ * ① env LIBERO_HOST === "hermes"(显式指定,最高优先级)
99
+ * ② env HERMES_HOME 存在
100
+ * ③ `hermes cron list` CLI 可调用
101
+ *
102
+ * publish/cancel 走 spawn CLI(spike 结论)。
103
+ */
104
+ export class HermesCronAdapter {
105
+ db;
106
+ kind = "hermes_cron";
107
+ /**
108
+ * @param db 可选——relative_to=schedule.start 时查 schedule.start_time 用。
109
+ * slice 1 单测不传(只测 trigger 纯函数);server-core 实装时传生产 db。
110
+ */
111
+ constructor(db) {
112
+ this.db = db;
113
+ }
114
+ detect() {
115
+ // ① 显式指定
116
+ if (process.env.LIBERO_HOST === "hermes")
117
+ return true;
118
+ // ② HERMES_HOME 存在
119
+ if (process.env.HERMES_HOME)
120
+ return true;
121
+ // ③ CLI 可调用(保守:只在她真的能跑时才认)
122
+ try {
123
+ execFileSync("hermes", ["cron", "list"], {
124
+ stdio: ["ignore", "pipe", "ignore"],
125
+ timeout: 5000,
126
+ });
127
+ return true;
128
+ }
129
+ catch {
130
+ return false;
131
+ }
132
+ }
133
+ async publish(intent) {
134
+ // relative_to=schedule.start 时查 DB 拿 start_time(slice 1 单测只覆盖纯函数,
135
+ // 这里是 publish 时真查 DB——tracer 暴露的必要修正)
136
+ const ctx = {};
137
+ if (intent.trigger.kind === "relative" &&
138
+ intent.trigger.relative_to === "schedule.start") {
139
+ if (!intent.schedule_id) {
140
+ throw new Error("relative_to=schedule.start 需要 schedule_id 才能算触发时间");
141
+ }
142
+ if (!this.db) {
143
+ throw new Error("HermesCronAdapter 未注入 db,无法查 schedule.start_time");
144
+ }
145
+ const row = this.db
146
+ .prepare("SELECT start_time FROM time_blocks WHERE id=?")
147
+ .get(intent.schedule_id);
148
+ if (!row) {
149
+ throw new Error(`schedule ${intent.schedule_id} 不存在`);
150
+ }
151
+ ctx.scheduleStartIso = row.start_time;
152
+ }
153
+ const { schedule, repeat } = translateTriggerToHermesSchedule(intent.trigger, ctx);
154
+ const args = ["cron", "create", schedule, intent.action_prompt];
155
+ args.push("--name", `${intent.intent_type}:${intent.id.slice(0, 8)}`);
156
+ args.push("--deliver", "origin");
157
+ if (repeat !== undefined) {
158
+ args.push("--repeat", String(repeat));
159
+ }
160
+ const stdout = execFileSync("hermes", args, {
161
+ encoding: "utf-8",
162
+ stdio: ["ignore", "pipe", "pipe"],
163
+ timeout: 15000,
164
+ });
165
+ // 解析 "Created job: <job_id>"
166
+ const m = stdout.match(/Created job:\s*([a-f0-9]+)/i);
167
+ if (!m) {
168
+ throw new Error(`hermes cron create 未返回 job_id,stdout: ${stdout}`);
169
+ }
170
+ return { host_job_id: m[1] };
171
+ }
172
+ async cancel(host_job_id) {
173
+ execFileSync("hermes", ["cron", "remove", host_job_id], {
174
+ stdio: ["ignore", "pipe", "pipe"],
175
+ timeout: 10000,
176
+ });
177
+ }
178
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * MV-COACH-5 §5.3 — adapter 注册与选择。
3
+ *
4
+ * MCP 启动时调一次 selectAdapter(),结果缓存。
5
+ * 后续 setIntent / cancelIntent 用这个 active adapter。
6
+ *
7
+ * §4.5 静默折叠声明:
8
+ * - 所有 adapter detect() 都 false → 返回 null(降级:意图只写 DB status=pending,
9
+ * 等用户开口时补推,slice 4 实装)。这是产品意图(无推送平台接受降级),非缺陷。
10
+ */
11
+ import { HermesCronAdapter } from "./hermes-cron.js";
12
+ /** 所有内置 adapter(按 detect 优先级排序)。 */
13
+ export const ADAPTERS = [
14
+ // 注:这里 new 的 adapter 不带 db——relative_to=schedule.start 查不了
15
+ // server-core 实装时用 createAdaptersWithDb(db) 拿带 db 的实例
16
+ new HermesCronAdapter(),
17
+ // slice 2 之后加:new OpenclawAdapter(),
18
+ ];
19
+ /** server-core 实装时用这个——传入 db,adapter 才能查 schedule.start_time。 */
20
+ export function createAdaptersWithDb(db) {
21
+ return [
22
+ new HermesCronAdapter(db),
23
+ // slice 2 之后加:new OpenclawAdapter(),
24
+ ];
25
+ }
26
+ /**
27
+ * 选当前环境能用的 adapter。
28
+ * 1. env LIBERO_HOST 显式指定 → 直接匹配 kind
29
+ * 2. 否则按顺序 detect(),第一个 true 的胜出
30
+ * 3. 都不命中 → null(降级)
31
+ */
32
+ export function selectAdapter(adapters = ADAPTERS) {
33
+ // ① env 显式指定
34
+ const envHint = process.env.LIBERO_HOST;
35
+ if (envHint) {
36
+ const matched = adapters.find((a) => a.kind === envHint);
37
+ if (matched && matched.detect())
38
+ return matched;
39
+ // env 指定了但 detect 不过 → 继续走自动探测(不强行用不工作的 adapter)
40
+ }
41
+ // ② 自动探测
42
+ for (const adapter of adapters) {
43
+ if (adapter.detect())
44
+ return adapter;
45
+ }
46
+ // ③ 都不命中
47
+ return null;
48
+ }
49
+ /** 缓存的 active adapter(MCP 启动时调 selectAdapter 缓存结果)。 */
50
+ let _activeAdapter;
51
+ /** 获取(或首次解析)active adapter。 */
52
+ export function getActiveAdapter() {
53
+ if (_activeAdapter === undefined) {
54
+ _activeAdapter = selectAdapter();
55
+ }
56
+ return _activeAdapter;
57
+ }
58
+ /** 测试用:重置缓存。 */
59
+ export function _resetActiveAdapterForTest() {
60
+ _activeAdapter = undefined;
61
+ }
62
+ void _resetActiveAdapterForTest;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * MV-COACH-5 §5 — HostAdapter 接口(3F 架构的宿主适配层)。
3
+ *
4
+ * 脑(skill)说意图 → 手(MCP)记意图 + 调 adapter → adapter 翻译成该平台的机制。
5
+ * slice 1 只实装 HermesCronAdapter;后续 slice 加 OpenclawAdapter / cc_pending 降级。
6
+ */
7
+ export {};
@@ -67,7 +67,17 @@ function subtractMinutes(iso, minutes) {
67
67
  const ms = new Date(iso).getTime() - minutes * 60_000;
68
68
  return new Date(ms).toISOString();
69
69
  }
70
- export function createSchedulerTools(repo, reminderRepo, categoryRepo) {
70
+ export function createSchedulerTools(repo, reminderRepo, categoryRepo,
71
+ /**
72
+ * MV-COACH-5 slice 1: setIntent/cancelIntent 用的宿主适配层。
73
+ * 可选——不传则 setIntent 写 DB status=pending(降级路径)。
74
+ */
75
+ activeAdapter,
76
+ /**
77
+ * MV-COACH-5 slice 1: raw DB 句柄,用于直接写 reminders 表的 intent_* 列
78
+ * (reminderRepo.create 不认 intent_* 字段也不接 cron_expr 占位 remind_at)。
79
+ */
80
+ intentDb) {
71
81
  return {
72
82
  async createSchedule(input) {
73
83
  // Reuse timeblock.create validation
@@ -167,5 +177,111 @@ export function createSchedulerTools(repo, reminderRepo, categoryRepo) {
167
177
  end: input.end_time,
168
178
  });
169
179
  },
180
+ /**
181
+ * MV-COACH-5 §4.1 setIntent——注册意图(提醒/追问/复盘等)。
182
+ * 脑层(skill)调这个,不直接调宿主的 cronjob。
183
+ * 通用核心记意图到 reminders 表(升格为意图队列),调 activeAdapter 翻译成宿主机制。
184
+ *
185
+ * §4.5 静默折叠声明:
186
+ * - activeAdapter=null(无宿主推送能力)→ 写 DB status=pending,不抛错(降级路径)
187
+ * - adapter.publish 失败 → status=failed,不静默(错误显式上报)
188
+ */
189
+ async setIntent(input) {
190
+ if (!reminderRepo) {
191
+ throw new Error("scheduler.setIntent 需要 reminderRepo(未注入)");
192
+ }
193
+ if (!intentDb) {
194
+ throw new Error("scheduler.setIntent 需要 intentDb(raw DB 句柄,用于写 intent_* 列)");
195
+ }
196
+ if (!input.action_prompt || input.action_prompt.trim() === "") {
197
+ throw new Error("action_prompt 不能为空");
198
+ }
199
+ if ((input.intent_type === "pre_start_reminder" ||
200
+ input.intent_type === "missed_check") &&
201
+ !input.schedule_id) {
202
+ throw new Error(`${input.intent_type} 需要 schedule_id(关联的 timeblock)`);
203
+ }
204
+ const nowIso = new Date().toISOString();
205
+ const intentId = `intent_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
206
+ const triggerPayload = JSON.stringify(input.trigger);
207
+ const triggerKind = input.trigger.kind;
208
+ // remind_at 给个合法 ISO 近似(intent 模式下真正触发靠 adapter;remind_at 在 reminders 表 NOT NULL)
209
+ const remindAt = nowIso;
210
+ // 直接 INSERT 整行(绕过 reminderRepo.create 的 remind_at ISO 校验 + 不认 intent_* 字段)
211
+ intentDb
212
+ .prepare(`INSERT INTO reminders
213
+ (id, user_id, timeblock_id, content, remind_at, channel, status, attempts, created_at, updated_at,
214
+ intent_type, trigger_kind, trigger_payload)
215
+ VALUES (?, ?, ?, ?, ?, '', 'pending', 0, ?, ?,
216
+ ?, ?, ?)`)
217
+ .run(intentId, input.user_id, input.schedule_id ?? null, input.action_prompt, remindAt, nowIso, nowIso, input.intent_type, triggerKind, triggerPayload);
218
+ const db = intentDb;
219
+ // 调 activeAdapter
220
+ if (!activeAdapter) {
221
+ return { intent_id: intentId, dispatched_via: "pending" };
222
+ }
223
+ try {
224
+ const intent = {
225
+ id: intentId,
226
+ user_id: input.user_id,
227
+ intent_type: input.intent_type,
228
+ schedule_id: input.schedule_id,
229
+ trigger: input.trigger,
230
+ action_prompt: input.action_prompt,
231
+ status: "pending",
232
+ created_at: nowIso,
233
+ updated_at: nowIso,
234
+ };
235
+ const { host_job_id } = await activeAdapter.publish(intent);
236
+ db.prepare(`UPDATE reminders SET status='dispatched', host_job_id=?, host_kind=?, updated_at=? WHERE id=?`).run(host_job_id, activeAdapter.kind, new Date().toISOString(), intentId);
237
+ return {
238
+ intent_id: intentId,
239
+ dispatched_via: activeAdapter.kind,
240
+ host_job_id,
241
+ };
242
+ }
243
+ catch (e) {
244
+ db.prepare(`UPDATE reminders SET status='failed', host_kind=?, updated_at=? WHERE id=?`).run(activeAdapter.kind, new Date().toISOString(), intentId);
245
+ throw new Error(`adapter.publish 失败(intent ${intentId} 标 failed):${e.message}`);
246
+ }
247
+ },
248
+ /**
249
+ * MV-COACH-5 §4.2 cancelIntent——撤销意图。
250
+ * 用户删日程时连带撤提醒/追问。
251
+ */
252
+ async cancelIntent(input) {
253
+ if (!intentDb) {
254
+ throw new Error("scheduler.cancelIntent 需要 intentDb");
255
+ }
256
+ const row = intentDb
257
+ .prepare("SELECT host_job_id, status FROM reminders WHERE id=?")
258
+ .get(input.intent_id);
259
+ if (!row) {
260
+ throw new Error(`intent ${input.intent_id} 不存在`);
261
+ }
262
+ // 调 adapter.cancel(有 host_job_id 才调)
263
+ if (row.host_job_id && activeAdapter) {
264
+ try {
265
+ await activeAdapter.cancel(row.host_job_id);
266
+ }
267
+ catch (e) {
268
+ console.warn(`[cancelIntent] adapter.cancel 失败(继续标本地 cancelled):${e.message}`);
269
+ }
270
+ }
271
+ intentDb
272
+ .prepare(`UPDATE reminders SET status='cancelled', updated_at=? WHERE id=?`)
273
+ .run(new Date().toISOString(), input.intent_id);
274
+ return { cancelled: true };
275
+ },
170
276
  };
171
277
  }
278
+ /**
279
+ * 从 trigger 算 remind_at(slice 1 简化:recurring 用 now 近似,真正触发靠 adapter)。
280
+ * reminders 表 remind_at NOT NULL,intent 模式下退化为元数据。
281
+ */
282
+ function computeRemindAt(trigger, scheduleId, _repo, nowIso) {
283
+ if (trigger.kind === "absolute")
284
+ return trigger.at;
285
+ // relative / recurring 都用 now 近似(slice 1:真正触发靠 adapter)
286
+ return nowIso;
287
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libero-mcp",
3
- "version": "0.2.24",
3
+ "version": "0.2.26",
4
4
  "description": "AI 时间管理教练 MCP 工具集——时间块记录、统计、矩阵诊断、计时、分类、profile",
5
5
  "license": "MIT",
6
6
  "author": "amosyuan",