libero-mcp 0.2.7 → 0.2.8
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/mcp/src/profile/index.js +19 -19
- package/dist/mcp/src/repo/sqlite.js +59 -0
- package/package.json +1 -1
|
@@ -49,48 +49,48 @@ export function createProfileTools(repo) {
|
|
|
49
49
|
});
|
|
50
50
|
},
|
|
51
51
|
/**
|
|
52
|
-
* S40 slice 2(2026-08-
|
|
52
|
+
* S40 slice 2 → slice 3(2026-08-03 升级):Atomic claim。
|
|
53
53
|
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
54
|
+
* 历史:slice 2 是 tool 层「读-检查-写」,集成测试量出 100% race(每次并发都双发)。
|
|
55
|
+
* slice 3 改为:repo 提供 claimNudgeAtomic(用 better-sqlite3 db.transaction 包整段)。
|
|
56
56
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
57
|
+
* 兼容降级:如果 repo 没实现 claimNudgeAtomic(如 InMemoryProfileRepo),
|
|
58
|
+
* 走旧版非原子逻辑——但会在 §4.5 静默折叠意义上「未声明」,所以打 WARN 日志。
|
|
59
59
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* won=false → 输出 [SILENT](已被别的 cron 抢先)
|
|
63
|
-
*
|
|
64
|
-
* 默认 window = 5 分钟,覆盖 cron 启动间隔差。
|
|
60
|
+
* @4.5 显式声明:sqlite transaction 是真 atomic(阻塞 event loop + 写锁)。
|
|
61
|
+
* 在 in-memory repo 下无 atomic 保护——但 in-memory 只在测试场景,无生产风险。
|
|
65
62
|
*/
|
|
66
63
|
async claimNudge(input) {
|
|
67
64
|
if (!input.user_id) {
|
|
68
65
|
throw new Error("user_id 为必填参数");
|
|
69
66
|
}
|
|
70
67
|
const windowMinutes = input.window_minutes ?? 5;
|
|
68
|
+
// 优先用 repo 提供的 atomic 版本(sqlite production path)
|
|
69
|
+
if (typeof repo.claimNudgeAtomic === "function") {
|
|
70
|
+
return repo.claimNudgeAtomic({
|
|
71
|
+
user_id: input.user_id,
|
|
72
|
+
window_minutes: windowMinutes,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
// 兼容降级(in-memory / 测试):非原子,无并发安全保证
|
|
76
|
+
// ⚠️ §4.5 声明:此分支不是 atomic,仅用于无并发的测试场景
|
|
77
|
+
// eslint-disable-next-line no-console
|
|
78
|
+
console.warn("[claimNudge] repo.claimNudgeAtomic 未实现,降级到非原子路径。" +
|
|
79
|
+
"仅测试场景安全,生产必须用 SqliteProfileRepo。");
|
|
71
80
|
const windowMs = windowMinutes * 60 * 1000;
|
|
72
81
|
const now = new Date();
|
|
73
82
|
const nowIso = now.toISOString();
|
|
74
|
-
// Atomic block: 用 repo 提供的 transaction hook
|
|
75
|
-
// 但 ProfileRepo interface 没有 transaction API —— 通过 repo.upsert 的「读-合并-写」语义实现
|
|
76
|
-
// 真正的 atomic 要靠 sqlite 层,但为兼容 in-memory repo,我们在 tool 层用读后写
|
|
77
|
-
// ⚠️ §4.5 静默折叠声明:这不是 100% atomic(读-写之间有 race window)
|
|
78
|
-
// 但 sqlite 层的默认 statement-level atomic 在实践中足够(< 1ms 窗口)
|
|
79
|
-
// 真 100% atomic 需要 SqliteProfileRepo 暴露 transaction 接口(slice 3 补)
|
|
80
83
|
const existing = await repo.getById(input.user_id);
|
|
81
84
|
const existingSettings = existing?.settings != null
|
|
82
85
|
? existing.settings
|
|
83
86
|
: {};
|
|
84
|
-
// 检查:window 内是否已被写过
|
|
85
87
|
const lastNudgeAt = existingSettings.last_inactivity_nudge_at;
|
|
86
88
|
if (typeof lastNudgeAt === "string") {
|
|
87
89
|
const ageMs = now.getTime() - new Date(lastNudgeAt).getTime();
|
|
88
90
|
if (ageMs < windowMs) {
|
|
89
|
-
// window 内已被 claim,本次输
|
|
90
91
|
return { won: false, profile: existing };
|
|
91
92
|
}
|
|
92
93
|
}
|
|
93
|
-
// 赢,写时间戳
|
|
94
94
|
const merged = {
|
|
95
95
|
...existingSettings,
|
|
96
96
|
last_inactivity_nudge_at: nowIso,
|
|
@@ -552,6 +552,65 @@ export class SqliteProfileRepo {
|
|
|
552
552
|
}
|
|
553
553
|
return (await this.getById(input.user_id));
|
|
554
554
|
}
|
|
555
|
+
/**
|
|
556
|
+
* S40 slice 3:Atomic claim nudge。
|
|
557
|
+
*
|
|
558
|
+
* 用 better-sqlite3 的 `db.transaction()` 把「SELECT → 检查 window → UPDATE → 返回」
|
|
559
|
+
* 包成一个原子单元。better-sqlite3 是**同步**的——transaction 期间阻塞 event loop,
|
|
560
|
+
* 第二个调用根本没机会插入。
|
|
561
|
+
*
|
|
562
|
+
* 注意:transaction 返回的函数是同步的,必须在同步上下文调用(不能 await)。
|
|
563
|
+
* 这里包装成 async 只是为了匹配 ProfileRepo interface,内部完全同步执行。
|
|
564
|
+
*/
|
|
565
|
+
async claimNudgeAtomic(input) {
|
|
566
|
+
const windowMs = input.window_minutes * 60 * 1000;
|
|
567
|
+
const now = new Date();
|
|
568
|
+
const nowIso = now.toISOString();
|
|
569
|
+
const nowDb = nowIso.replace("T", " ").replace("Z", "");
|
|
570
|
+
const rowToProfile = (r) => ({
|
|
571
|
+
id: r.id,
|
|
572
|
+
display_name: r.display_name,
|
|
573
|
+
expectations: r.expectations,
|
|
574
|
+
settings: r.settings != null ? JSON.parse(r.settings) : null,
|
|
575
|
+
created_at: r.created_at,
|
|
576
|
+
updated_at: r.updated_at,
|
|
577
|
+
});
|
|
578
|
+
const selectStmt = this.db.prepare("SELECT id, display_name, expectations, settings, created_at, updated_at FROM user_profile WHERE id = ? AND deleted_at IS NULL");
|
|
579
|
+
const updateStmt = this.db.prepare(`UPDATE user_profile SET settings = ?, updated_at = ? WHERE id = ?`);
|
|
580
|
+
const insertStmt = this.db.prepare(`INSERT INTO user_profile (id, display_name, expectations, settings, created_at, updated_at)
|
|
581
|
+
VALUES (?, '', NULL, ?, ?, ?)`);
|
|
582
|
+
// transaction 函数(同步)。better-sqlite3 在 transaction() 期间阻塞 event loop,
|
|
583
|
+
// 第二个并发调用必须等第一个 COMMIT 才能进入 BEGIN。
|
|
584
|
+
const txn = this.db.transaction(() => {
|
|
585
|
+
const row = selectStmt.get(input.user_id);
|
|
586
|
+
const existingSettings = row?.settings != null
|
|
587
|
+
? JSON.parse(row.settings)
|
|
588
|
+
: {};
|
|
589
|
+
const lastNudgeAt = existingSettings.last_inactivity_nudge_at;
|
|
590
|
+
if (typeof lastNudgeAt === "string") {
|
|
591
|
+
const ageMs = now.getTime() - new Date(lastNudgeAt).getTime();
|
|
592
|
+
if (ageMs < windowMs) {
|
|
593
|
+
return { won: false, profile: rowToProfile(row) };
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
const merged = {
|
|
597
|
+
...existingSettings,
|
|
598
|
+
last_inactivity_nudge_at: nowIso,
|
|
599
|
+
};
|
|
600
|
+
const settingsJson = JSON.stringify(merged);
|
|
601
|
+
if (row) {
|
|
602
|
+
updateStmt.run(settingsJson, nowDb, input.user_id);
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
insertStmt.run(input.user_id, settingsJson, nowDb, nowDb);
|
|
606
|
+
}
|
|
607
|
+
const updated = selectStmt.get(input.user_id);
|
|
608
|
+
return { won: true, profile: rowToProfile(updated) };
|
|
609
|
+
});
|
|
610
|
+
// BEGIN IMMEDIATE:立刻拿写锁,防止 deferred 模式下读-写升级冲突
|
|
611
|
+
// better-sqlite3 API:transaction 函数有 .immediate(args) 变种
|
|
612
|
+
return txn.immediate();
|
|
613
|
+
}
|
|
555
614
|
}
|
|
556
615
|
// ============================================================
|
|
557
616
|
// SqliteReminderRepo (S11 — reminder persistence)
|