libero-mcp 0.2.4 → 0.2.6

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.
@@ -358,6 +358,20 @@ export function buildServer(db) {
358
358
  return errorContent(r.error);
359
359
  return asContent(await profile.upsert(r));
360
360
  });
361
+ server.registerTool("profile.markInactivityNudge", {
362
+ description: "MV-COACH-3 slice 2(防重复失效专用):触发「5 天没记录轻触」问候后必调。" +
363
+ "专门写 settings.last_inactivity_nudge_at = now。" +
364
+ "⚠️ 不要用 profile.upsert 写这个字段——upsert 是 whole-replace 会清掉其它 settings key。" +
365
+ "本工具 deep-merge,只更新 last_inactivity_nudge_at 一个 key。",
366
+ inputSchema: {
367
+ user_id: z.string().optional(),
368
+ },
369
+ }, async (rawInput) => {
370
+ const r = injector(rawInput);
371
+ if ("error" in r)
372
+ return errorContent(r.error);
373
+ return asContent(await profile.markInactivityNudge({ user_id: r.user_id }));
374
+ });
361
375
  server.registerTool("profile.confirm_default", {
362
376
  description: "首次写入前确认默认用户:写入 user_profile,设置 display_name 与 default_confirmed_at。display_name 重名则拒绝。",
363
377
  inputSchema: {
@@ -3,6 +3,12 @@ export function nowUTC() {
3
3
  }
4
4
  export function nowLocal(offsetMinutes) {
5
5
  const d = new Date();
6
+ // Bug 4 修复(2026-07-31):旧实现多算了一次 getTimezoneOffset():
7
+ // 旧代码 utc = d.getTime() + d.getTimezoneOffset()*60000 // ← 把 UTC 毫秒减回本地墙钟当 UTC
8
+ // 旧代码 local = utc + offset*60000 // ← 又加 offset
9
+ // 在生产 TZ=+08 服务器不传参时 offset=480,被双偏移抵消,返回 UTC 墙钟+08:00 后缀
10
+ // (错位 8 小时)。实测:7/30 22:02:51 北京时间触发的复盘 cron 收到 "14:02:51+08:00"。
11
+ // 正确逻辑:UTC 毫秒 + offset = 该时区墙钟时间(视为 UTC 格式化)+ 拼后缀。
6
12
  const offset = offsetMinutes ?? -d.getTimezoneOffset();
7
13
  if (offset < -720 || offset > 720) {
8
14
  throw new Error(`Timezone offset ${offset} out of range (must be ±720 minutes)`);
@@ -11,9 +17,8 @@ export function nowLocal(offsetMinutes) {
11
17
  const abs = Math.abs(offset);
12
18
  const h = String(Math.floor(abs / 60)).padStart(2, "0");
13
19
  const m = String(abs % 60).padStart(2, "0");
14
- const utc = new Date(d.getTime() + d.getTimezoneOffset() * 60000);
15
- const local = new Date(utc.getTime() + offset * 60000);
16
- const iso = local.toISOString().replace("Z", "");
20
+ const shifted = new Date(d.getTime() + offset * 60000);
21
+ const iso = shifted.toISOString().replace("Z", "");
17
22
  return `${iso}${sign}${h}:${m}`;
18
23
  }
19
24
  export function formatTime(isoString, format) {
@@ -14,5 +14,35 @@ export function createProfileTools(repo) {
14
14
  }
15
15
  return repo.upsert(input);
16
16
  },
17
+ /**
18
+ * MV-COACH-3 slice 2(2026-07-31):专门写 last_inactivity_nudge_at 时间戳。
19
+ *
20
+ * 解决问题:原本 LLM 触发"5 天没记录问候"后要自己调 profile.upsert 写这个时间戳,
21
+ * 但 LLM 经常忘调 → 5 天窗口防重复失效,可能 nag 多次。
22
+ * 专用工具 = 唯一入口,调别的没用,让"忘了写时间戳"在工具层不再可能。
23
+ *
24
+ * 关键:DEEP-MERGE 进 settings(不是 whole-replace),保留其它 settings key。
25
+ * (profile.upsert 的 settings 是 whole-replace per S13 §4,所以必须用专门工具。)
26
+ *
27
+ * nonexistent user_id 也会 auto-create profile row(防御 race / skill 顺序问题)。
28
+ */
29
+ async markInactivityNudge(input) {
30
+ if (!input.user_id) {
31
+ throw new Error("user_id 为必填参数");
32
+ }
33
+ const now = new Date().toISOString();
34
+ const existing = await repo.getById(input.user_id);
35
+ const existingSettings = existing?.settings != null
36
+ ? existing.settings
37
+ : {};
38
+ const merged = {
39
+ ...existingSettings,
40
+ last_inactivity_nudge_at: now,
41
+ };
42
+ return repo.upsert({
43
+ user_id: input.user_id,
44
+ settings: merged,
45
+ });
46
+ },
17
47
  };
18
48
  }
@@ -143,7 +143,7 @@ export function createSchedulerTools(repo, reminderRepo, categoryRepo) {
143
143
  ` action="create",\n` +
144
144
  ` name="${title} 追问",\n` +
145
145
  ` schedule="<追问时间的 '分 时 * * *'>",\n` +
146
- ` prompt="[Libero 教练闭环 - 追问] 用户 ${startTime} 的 ${title} 到现在没确认执行。\\n执行 2 步:\\n1. 温和追问一次(不带评判):看到 ${startTime} 那段好像还没动——是忘了、卡住、还是计划需要调整?\\n2. 调 timeblock.list({user_id:'${userId}', status:'unverified'}) 找到 start_time 是 ${startTime} 这条,查用户回应:答执行了→timeblock.update(status:'completed')+回'太好了,记下了';答没执行→timeblock.remove(id)软删+回'了解,已撤掉这条';不答→保持 unverified,什么都不做。\\n ⚠️ user_id 必须用 '${userId}'。\\n ⚠️ Bug 1 教训(2026-07-31):如果 timeblock.list 找不到 start_time 是 ${startTime} 的 unverified 记录(说明用户已确认执行升 completed,或已软删)→ **静默退出,绝对不要 timeblock.update 任何东西**。后端代码层也已加守卫拒绝 completed→unverified,但你仍应避免发起这种调用。\\n⚠️ 这是唯一一次追问,无论用户回不回,不再追加,也不再建任何新 cron。",\n` +
146
+ ` prompt="[Libero 教练闭环 - 追问] 用户 ${startTime} 的 ${title} 到现在没确认执行。\\n执行 2 步:\\n1. 温和追问一次(不带评判):看到 ${startTime} 那段好像还没动——是忘了、卡住、还是计划需要调整?\\n2. 调 timeblock.list({user_id:'${userId}', status:'unverified'}) 找到 start_time 是 ${startTime} 这条,查用户回应:答执行了→timeblock.update(status:'completed')+回'👍 记下了。要不要起个番茄钟?'(⚠️ Bug 3:只升 status,禁止自动调 timer.startFocus,"开始了"是确认执行不是请求开计时);答没执行→timeblock.remove(id)软删+回'了解,已撤掉这条';不答→保持 unverified,什么都不做。\\n ⚠️ user_id 必须用 '${userId}'。\\n ⚠️ Bug 1 教训(2026-07-31):如果 timeblock.list 找不到 start_time 是 ${startTime} 的 unverified 记录(说明用户已确认执行升 completed,或已软删)→ **静默退出,绝对不要 timeblock.update 任何东西**。后端代码层也已加守卫拒绝 completed→unverified,但你仍应避免发起这种调用。\\n⚠️ 这是唯一一次追问,无论用户回不回,不再追加,也不再建任何新 cron。",\n` +
147
147
  ` deliver="origin"${repeatClause}\n` +
148
148
  `)${repeatNote}`,
149
149
  reminder_note: "⚠️ 提醒 ≠ 计时。开工 cron 只推提醒('时间快到了'),绝对不帮用户开番茄钟。" +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libero-mcp",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "AI 时间管理教练 MCP 工具集——时间块记录、统计、矩阵诊断、计时、分类、profile",
5
5
  "license": "MIT",
6
6
  "author": "amosyuan",