libero-mcp 0.2.13 → 0.2.14
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.
|
@@ -164,10 +164,14 @@ export class SqliteTimeBlockRepo {
|
|
|
164
164
|
return (await this.getById(id));
|
|
165
165
|
}
|
|
166
166
|
async findOverlapping(input) {
|
|
167
|
+
// ⚠️ 时区安全:用 SQLite datetime() 规范化所有时间字段后再比较。
|
|
168
|
+
// datetime() 能解析多种 ISO 格式(含 +08:00 / Z),输出统一 'YYYY-MM-DD HH:MM:SS' UTC。
|
|
169
|
+
// 不能用裸字符串比较('2026-08-07T09:00:00+08:00' vs '2026-08-07T01:00:00Z' 字符串比较会误判)。
|
|
167
170
|
return this.db
|
|
168
171
|
.prepare(`SELECT * FROM time_blocks
|
|
169
172
|
WHERE user_id = ? AND status = 'planned' AND deleted_at IS NULL
|
|
170
|
-
AND start_time < ?
|
|
173
|
+
AND datetime(start_time) < datetime(?)
|
|
174
|
+
AND datetime(end_time) > datetime(?)
|
|
171
175
|
ORDER BY start_time`)
|
|
172
176
|
.all(input.user_id, input.end, input.start);
|
|
173
177
|
}
|
|
@@ -287,69 +287,45 @@ export function renderTimelineCells(input) {
|
|
|
287
287
|
i = j;
|
|
288
288
|
}
|
|
289
289
|
}
|
|
290
|
-
// Build text lines:
|
|
291
|
-
//
|
|
290
|
+
// Build text lines: 明细在前(用户先看具体内容),矩阵在后(时间分布概览)
|
|
291
|
+
// S9 v0.5(2026-08-07):表头移到矩阵上方,不再夹在明细前面
|
|
292
292
|
const textLines = [];
|
|
293
|
-
|
|
294
|
-
//
|
|
295
|
-
// S9 v0.
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
//
|
|
293
|
+
// ── 明细段(在前)──
|
|
294
|
+
// S9 v0.3: 按 symbol 分组紧凑展示(合并同 symbol 到一行)
|
|
295
|
+
// S9 v0.4: completed → ✓ + ~~划线~~;unverified → ❓ 后缀;插入「📍 现在」分隔线
|
|
296
|
+
// S9 v0.5(2026-08-07): 改为每条独占一行(用户反馈"挤在一起乱")
|
|
297
|
+
// 格式:`{status} {symbol} {time} {title}`
|
|
298
|
+
// status: completed → "✓ ";unverified → "? ";planned/timer_* → " "(空格占位对齐)
|
|
299
299
|
const details = input.details ?? [];
|
|
300
300
|
if (details.length > 0) {
|
|
301
301
|
textLines.push("📋 明细");
|
|
302
302
|
const inWin = details.filter((d) => d.in_window);
|
|
303
303
|
const outWin = details.filter((d) => !d.in_window);
|
|
304
|
-
// S9 v0.
|
|
305
|
-
// - completed: "✓ {time} ~~{title}~~"
|
|
306
|
-
// - unverified: "{time} {title} ❓"
|
|
307
|
-
// - planned / timer_running / timer_paused: "{time} {title}"
|
|
304
|
+
// S9 v0.5: 每条独占一行的 entry 构造
|
|
308
305
|
const buildEntry = (d) => {
|
|
306
|
+
let statusMark = " "; // 默认 2 空格占位(对齐 ✓ 和 ?)
|
|
307
|
+
let titlePart = d.title;
|
|
309
308
|
if (d.status === "completed") {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
if (d.status === "unverified") {
|
|
313
|
-
return `${d.time} ${d.title} ❓`;
|
|
309
|
+
statusMark = "✓ ";
|
|
310
|
+
titlePart = `~~${d.title}~~`;
|
|
314
311
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
// 按 symbol 分组(保留 inWin 内的原始顺序)
|
|
318
|
-
const groupBySymbol = (list) => {
|
|
319
|
-
const groups = [];
|
|
320
|
-
const symbolToGroup = new Map();
|
|
321
|
-
for (const d of list) {
|
|
322
|
-
mark(d.symbol);
|
|
323
|
-
const entry = buildEntry(d);
|
|
324
|
-
const idx = symbolToGroup.get(d.symbol);
|
|
325
|
-
if (idx === undefined) {
|
|
326
|
-
symbolToGroup.set(d.symbol, groups.length);
|
|
327
|
-
groups.push({ symbol: d.symbol, entries: [entry] });
|
|
328
|
-
}
|
|
329
|
-
else {
|
|
330
|
-
groups[idx].entries.push(entry);
|
|
331
|
-
}
|
|
312
|
+
else if (d.status === "unverified") {
|
|
313
|
+
statusMark = "? ";
|
|
332
314
|
}
|
|
333
|
-
return
|
|
315
|
+
return `${statusMark}${d.symbol} ${d.time} ${titlePart}`;
|
|
334
316
|
};
|
|
335
|
-
// S9 v0.4:
|
|
336
|
-
// 在 inWin 列表中,找到第一条 start_time > now 的明细位置
|
|
337
|
-
// 时间从 detail.time "HH:MM-HH:MM" 取 start
|
|
317
|
+
// S9 v0.4: 「📍 现在」分隔线判断(v0.5 保留同规则)
|
|
338
318
|
const parseDetailStart = (time) => {
|
|
339
319
|
const startStr = time.split("-")[0].trim();
|
|
340
320
|
const [h, m] = startStr.split(":").map(Number);
|
|
341
321
|
return h * 60 + m;
|
|
342
322
|
};
|
|
343
|
-
const nowMs = new Date(input.nowIso).getTime();
|
|
344
323
|
const nowDate = new Date(input.nowIso);
|
|
345
324
|
const nowHM = nowDate.getHours() * 60 + nowDate.getMinutes();
|
|
346
325
|
const nowLabel = `${String(nowDate.getHours()).padStart(2, "0")}:${String(nowDate.getMinutes()).padStart(2, "0")}`;
|
|
347
|
-
// 判断是否需要插「📍 现在」
|
|
348
|
-
// 仅当 inWin 中存在 start > now 且存在 start <= now 时才插(即跨 now)
|
|
349
326
|
const inWinAllPast = inWin.every((d) => parseDetailStart(d.time) <= nowHM);
|
|
350
327
|
const inWinAllFuture = inWin.every((d) => parseDetailStart(d.time) > nowHM);
|
|
351
328
|
const needNowMarker = inWin.length > 0 && !inWinAllPast && !inWinAllFuture;
|
|
352
|
-
// 找到第一个 start > now 的 index(在该位置前插入分隔线)
|
|
353
329
|
let firstFutureIdx = inWin.length;
|
|
354
330
|
if (needNowMarker) {
|
|
355
331
|
for (let i = 0; i < inWin.length; i++) {
|
|
@@ -359,30 +335,17 @@ export function renderTimelineCells(input) {
|
|
|
359
335
|
}
|
|
360
336
|
}
|
|
361
337
|
}
|
|
362
|
-
// S9 v0.
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
const past = inWin.slice(0, firstFutureIdx);
|
|
367
|
-
const future = inWin.slice(firstFutureIdx);
|
|
368
|
-
for (const g of groupBySymbol(past)) {
|
|
369
|
-
textLines.push(`${g.symbol} ${g.entries.join(" · ")}`);
|
|
370
|
-
}
|
|
371
|
-
textLines.push(`─── 📍 现在 ${nowLabel} ──`);
|
|
372
|
-
for (const g of groupBySymbol(future)) {
|
|
373
|
-
textLines.push(`${g.symbol} ${g.entries.join(" · ")}`);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
else {
|
|
377
|
-
// 无需分隔线(全过 / 全未到),原逻辑
|
|
378
|
-
for (const g of groupBySymbol(inWin)) {
|
|
379
|
-
textLines.push(`${g.symbol} ${g.entries.join(" · ")}`);
|
|
338
|
+
// S9 v0.5: 每条独占一行,在 firstFutureIdx 处插分隔线
|
|
339
|
+
for (let i = 0; i < inWin.length; i++) {
|
|
340
|
+
if (needNowMarker && i === firstFutureIdx) {
|
|
341
|
+
textLines.push(`─── 📍 现在 ${nowLabel} ──`);
|
|
380
342
|
}
|
|
343
|
+
textLines.push(buildEntry(inWin[i]));
|
|
381
344
|
}
|
|
382
345
|
if (outWin.length > 0) {
|
|
383
346
|
textLines.push("(窗口外)");
|
|
384
|
-
for (const
|
|
385
|
-
textLines.push(
|
|
347
|
+
for (const d of outWin) {
|
|
348
|
+
textLines.push(buildEntry(d));
|
|
386
349
|
}
|
|
387
350
|
}
|
|
388
351
|
}
|
|
@@ -397,6 +360,8 @@ export function renderTimelineCells(input) {
|
|
|
397
360
|
};
|
|
398
361
|
if (boundary)
|
|
399
362
|
pushEllipsis();
|
|
363
|
+
// S9 v0.5: 时间表头紧贴矩阵上方(之前在明细前,造成"表头夹中间"错觉)
|
|
364
|
+
textLines.push(" 00 10 20 30 40 50");
|
|
400
365
|
// 2026-07-27 UX 反馈:矩阵上下行距太挤,每行后加一个空行透气
|
|
401
366
|
for (let k = 0; k < matrixLines.length; k++) {
|
|
402
367
|
const line = matrixLines[k];
|
|
@@ -116,8 +116,41 @@ export function createTimerTools(repo, timeBlockRepo, db) {
|
|
|
116
116
|
ended_at: endedAt,
|
|
117
117
|
});
|
|
118
118
|
let timeblockId;
|
|
119
|
-
//
|
|
120
|
-
|
|
119
|
+
// S42 (2026-08-07): 回查匹配的 planned timeblock → 改 completed
|
|
120
|
+
// 用户场景:先排了 planned timeblock(如「羽毛球 9:00-11:00」),后开计时;
|
|
121
|
+
// 停计时期望自动标完成(带 ✓ + 划线),而不是 create 新的。
|
|
122
|
+
// 匹配规则:同 user_id + 同 title + 时间重叠 + status=planned
|
|
123
|
+
//
|
|
124
|
+
// ⚠️ 时区陷阱:planned timeblock 的 start_time/end_time 可能是 +08:00 格式(用户传入),
|
|
125
|
+
// 而 timer.session 的 started_at 是 Z 格式(nowISO())。findOverlapping 用字符串比较,
|
|
126
|
+
// 混合时区会误判("2026-08-07T09:00:00+08:00" vs "2026-08-07T05:00:00Z" 字符串比较 09 > 05)。
|
|
127
|
+
// 解法:调用前把 started_at/endedAt 都转 ISO UTC(Z 格式),与 line 578-581 其它 list 查询姿势一致。
|
|
128
|
+
if (timeBlockRepo) {
|
|
129
|
+
const startUtc = new Date(active.started_at).toISOString();
|
|
130
|
+
const endUtc = new Date(endedAt).toISOString();
|
|
131
|
+
const candidates = await timeBlockRepo.findOverlapping({
|
|
132
|
+
user_id: active.user_id,
|
|
133
|
+
start: startUtc,
|
|
134
|
+
end: endUtc,
|
|
135
|
+
});
|
|
136
|
+
const match = candidates.find((tb) => tb.status === "planned" &&
|
|
137
|
+
(tb.title === active.event ||
|
|
138
|
+
tb.title.includes(active.event) ||
|
|
139
|
+
active.event.includes(tb.title)));
|
|
140
|
+
if (match) {
|
|
141
|
+
const updated = await timeBlockRepo.update(match.id, {
|
|
142
|
+
end_time: endedAt,
|
|
143
|
+
status: "completed",
|
|
144
|
+
...(options?.rating !== undefined ? { rating: options.rating } : {}),
|
|
145
|
+
...(options?.rating_reason !== undefined
|
|
146
|
+
? { rating_reason: options.rating_reason }
|
|
147
|
+
: {}),
|
|
148
|
+
});
|
|
149
|
+
timeblockId = updated.id;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// 没匹配到 planned → 走原 create 路径(仅 rating 时)
|
|
153
|
+
if (!timeblockId && options?.rating !== undefined && timeBlockRepo) {
|
|
121
154
|
const tb = await timeBlockRepo.create({
|
|
122
155
|
user_id: active.user_id,
|
|
123
156
|
title: active.event,
|