libero-mcp 0.2.10 → 0.2.12
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.
|
@@ -401,7 +401,8 @@ export class SqliteTimelineRepo {
|
|
|
401
401
|
tb.status as status,
|
|
402
402
|
tb.title as title,
|
|
403
403
|
c.emoji as category_emoji,
|
|
404
|
-
c.name as category_name
|
|
404
|
+
c.name as category_name,
|
|
405
|
+
c.color as category_color
|
|
405
406
|
FROM time_blocks tb
|
|
406
407
|
LEFT JOIN categories c ON tb.category_id = c.id
|
|
407
408
|
WHERE tb.user_id = ?
|
|
@@ -42,6 +42,71 @@ export function categoryAsciiToken(name) {
|
|
|
42
42
|
return `[${latin[0].toUpperCase()}]`;
|
|
43
43
|
return `[${name.trim()[0]}]`;
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* S9 v0.2: hex 颜色 → 方块 emoji(粗色相映射,6-7 色)
|
|
47
|
+
*
|
|
48
|
+
* 设计意图(见 docs/specs/S9-v0.2-design-plan-colored-tiles.md):
|
|
49
|
+
* - 15 个分类压缩到 7 种方块色——保留视觉区分度,避免 15 色用户认不过来
|
|
50
|
+
* - 用 HSL hue 粗分(不用精确匹配 hex,避免脆弱)
|
|
51
|
+
* - 灰阶 / 深色 → ⬛(视觉暗色,区分显著)
|
|
52
|
+
* - 无效输入 → 🟦(默认蓝,跟原 spec v0.1 兼容)
|
|
53
|
+
*/
|
|
54
|
+
export function colorToTile(hex) {
|
|
55
|
+
if (!hex || typeof hex !== "string" || !hex.startsWith("#"))
|
|
56
|
+
return "🟦";
|
|
57
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
58
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
59
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
60
|
+
// 解析失败 → fallback
|
|
61
|
+
if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b))
|
|
62
|
+
return "🟦";
|
|
63
|
+
const { h, s, l } = rgbToHsl(r, g, b);
|
|
64
|
+
// 灰阶(饱和度 < 12%)或过暗(亮度 < 0.35)→ ⬛
|
|
65
|
+
// 设计:睡眠(#34495E l=0.29 s=0.29)、内务(#95A5A6 s=0.09)、闲散(#BDC3C7) 等
|
|
66
|
+
// 深色 / 中性色都归黑,区分显著
|
|
67
|
+
if (s < 0.12 || l < 0.35)
|
|
68
|
+
return "⬛";
|
|
69
|
+
// 色相分段(0-360)
|
|
70
|
+
if (h < 15 || h >= 345)
|
|
71
|
+
return "🟥"; // 红
|
|
72
|
+
if (h < 45)
|
|
73
|
+
return "🟧"; // 橙
|
|
74
|
+
if (h < 70)
|
|
75
|
+
return "🟨"; // 黄
|
|
76
|
+
if (h < 165)
|
|
77
|
+
return "🟩"; // 绿
|
|
78
|
+
if (h < 240)
|
|
79
|
+
return "🟦"; // 蓝(含青)
|
|
80
|
+
if (h < 315)
|
|
81
|
+
return "🟪"; // 紫(240-315)
|
|
82
|
+
return "🟥"; // 粉红归红
|
|
83
|
+
}
|
|
84
|
+
/** RGB → HSL(h: 0-360, s/l: 0-1) */
|
|
85
|
+
function rgbToHsl(r, g, b) {
|
|
86
|
+
const rN = r / 255;
|
|
87
|
+
const gN = g / 255;
|
|
88
|
+
const bN = b / 255;
|
|
89
|
+
const max = Math.max(rN, gN, bN);
|
|
90
|
+
const min = Math.min(rN, gN, bN);
|
|
91
|
+
const l = (max + min) / 2;
|
|
92
|
+
let h = 0;
|
|
93
|
+
let s = 0;
|
|
94
|
+
if (max !== min) {
|
|
95
|
+
const d = max - min;
|
|
96
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
97
|
+
switch (max) {
|
|
98
|
+
case rN:
|
|
99
|
+
h = ((gN - bN) / d + (gN < bN ? 6 : 0)) * 60;
|
|
100
|
+
break;
|
|
101
|
+
case gN:
|
|
102
|
+
h = ((bN - rN) / d + 2) * 60;
|
|
103
|
+
break;
|
|
104
|
+
default:
|
|
105
|
+
h = ((rN - gN) / d + 4) * 60;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return { h, s, l };
|
|
109
|
+
}
|
|
45
110
|
function timerKey(status) {
|
|
46
111
|
return status === "paused" ? "timer_paused" : "timer_running";
|
|
47
112
|
}
|
|
@@ -59,6 +124,17 @@ export function resolveCellSymbol(cell, format) {
|
|
|
59
124
|
const key = timerKey(cell.status);
|
|
60
125
|
return format === "emoji" ? FIXED_EMOJI[key] : FIXED_ASCII[key];
|
|
61
126
|
}
|
|
127
|
+
// S9 v0.2: planned 块带 tile 时优先用分类色方块,否则 fallback 到默认 🟦
|
|
128
|
+
if (cell.kind === "planned") {
|
|
129
|
+
if (format === "emoji") {
|
|
130
|
+
if (cell.tile && cell.tile.trim())
|
|
131
|
+
return cell.tile;
|
|
132
|
+
return FIXED_EMOJI.planned;
|
|
133
|
+
}
|
|
134
|
+
// ascii 用 category 首字母(如 [W]),否则默认 [#]
|
|
135
|
+
return cell.ascii ?? FIXED_ASCII.planned;
|
|
136
|
+
}
|
|
137
|
+
// unverified / empty
|
|
62
138
|
const key = cell.kind;
|
|
63
139
|
return format === "emoji" ? FIXED_EMOJI[key] : FIXED_ASCII[key];
|
|
64
140
|
}
|
|
@@ -92,6 +168,16 @@ export function mergeTimelineCells(candidates) {
|
|
|
92
168
|
function legendMeaning(symbol) {
|
|
93
169
|
if (LEGEND_ZH[symbol])
|
|
94
170
|
return LEGEND_ZH[symbol];
|
|
171
|
+
// S9 v0.2: 方块色 = 计划块(按分类上色)
|
|
172
|
+
if (symbol === "🟦" ||
|
|
173
|
+
symbol === "🟪" ||
|
|
174
|
+
symbol === "🟩" ||
|
|
175
|
+
symbol === "🟥" ||
|
|
176
|
+
symbol === "🟧" ||
|
|
177
|
+
symbol === "🟨" ||
|
|
178
|
+
symbol === "⬛") {
|
|
179
|
+
return "计划";
|
|
180
|
+
}
|
|
95
181
|
// completed category emoji / ascii letter
|
|
96
182
|
if (symbol.startsWith("[") && symbol.endsWith("]"))
|
|
97
183
|
return "实绩";
|
|
@@ -206,26 +292,45 @@ export function renderTimelineCells(input) {
|
|
|
206
292
|
const textLines = [];
|
|
207
293
|
textLines.push(" 00 10 20 30 40 50");
|
|
208
294
|
// ── 表格段(在前)──
|
|
295
|
+
// S9 v0.3: details 段按 symbol 分组紧凑展示
|
|
296
|
+
// 用户痛点:移动端窄屏,原格式 "{time} {symbol} {title} [planned]" 长标题折行后 [planned] 跑到第三行,诡异
|
|
297
|
+
// 新格式:同 symbol 合并到一行,用 · 分隔;移除 [status] 后缀(状态在矩阵方块色已表达)
|
|
209
298
|
const details = input.details ?? [];
|
|
210
299
|
if (details.length > 0) {
|
|
211
|
-
textLines.push("📋
|
|
300
|
+
textLines.push("📋 明细");
|
|
212
301
|
const inWin = details.filter((d) => d.in_window);
|
|
213
302
|
const outWin = details.filter((d) => !d.in_window);
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
303
|
+
// 按 symbol 分组(保留 inWin 内的原始顺序)
|
|
304
|
+
const groupBySymbol = (list) => {
|
|
305
|
+
const groups = [];
|
|
306
|
+
const symbolToGroup = new Map();
|
|
307
|
+
for (const d of list) {
|
|
308
|
+
mark(d.symbol);
|
|
309
|
+
const entry = `${d.time} ${d.title}`;
|
|
310
|
+
const idx = symbolToGroup.get(d.symbol);
|
|
311
|
+
if (idx === undefined) {
|
|
312
|
+
symbolToGroup.set(d.symbol, groups.length);
|
|
313
|
+
groups.push({ symbol: d.symbol, entries: [entry] });
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
groups[idx].entries.push(entry);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return groups;
|
|
320
|
+
};
|
|
321
|
+
for (const g of groupBySymbol(inWin)) {
|
|
322
|
+
textLines.push(`${g.symbol} ${g.entries.join(" · ")}`);
|
|
217
323
|
}
|
|
218
324
|
if (outWin.length > 0) {
|
|
219
|
-
textLines.push("
|
|
220
|
-
for (const
|
|
221
|
-
|
|
222
|
-
textLines.push(`${d.time} ${d.symbol} ${d.title} [${d.status}]`);
|
|
325
|
+
textLines.push("(窗口外)");
|
|
326
|
+
for (const g of groupBySymbol(outWin)) {
|
|
327
|
+
textLines.push(`${g.symbol} ${g.entries.join(" · ")}`);
|
|
223
328
|
}
|
|
224
329
|
}
|
|
225
330
|
}
|
|
226
331
|
else {
|
|
227
332
|
// pressure E-6: details 空 → 给"该时段无安排"提示,不留空段
|
|
228
|
-
textLines.push("📋
|
|
333
|
+
textLines.push("📋 明细:该时段无安排");
|
|
229
334
|
}
|
|
230
335
|
// ── 矩阵段(在后,作为时间分布概览)──
|
|
231
336
|
const pushEllipsis = () => {
|
|
@@ -250,6 +355,15 @@ export function renderTimelineCells(input) {
|
|
|
250
355
|
}
|
|
251
356
|
if (boundary)
|
|
252
357
|
pushEllipsis();
|
|
358
|
+
// S9 v0.2: 末尾拼图例(按本次实际出现的色)
|
|
359
|
+
// 设计:图例短一行,只列实际出现过的符号;颜色块统一标"计划·X 类"
|
|
360
|
+
if (Object.keys(legend).length > 1) {
|
|
361
|
+
const legendParts = [];
|
|
362
|
+
for (const [sym, meaning] of Object.entries(legend)) {
|
|
363
|
+
legendParts.push(`${sym} ${meaning}`);
|
|
364
|
+
}
|
|
365
|
+
textLines.push(`图例:${legendParts.join(" · ")}`);
|
|
366
|
+
}
|
|
253
367
|
return { rows, rendered_text: textLines.join("\n"), legend };
|
|
254
368
|
}
|
|
255
369
|
export function computeDefaultWindow(now) {
|
|
@@ -272,6 +386,18 @@ function cellFromBlock(b) {
|
|
|
272
386
|
label: b.title,
|
|
273
387
|
};
|
|
274
388
|
}
|
|
389
|
+
// S9 v0.2: planned 块按分类上色(不再统一 🟦)
|
|
390
|
+
// unverified 保持 ❓ 不上色(状态优先于分类)
|
|
391
|
+
if (b.status === "planned") {
|
|
392
|
+
const ascii = categoryAsciiToken(b.category_name);
|
|
393
|
+
const tile = colorToTile(b.category_color);
|
|
394
|
+
return {
|
|
395
|
+
kind: "planned",
|
|
396
|
+
tile,
|
|
397
|
+
ascii,
|
|
398
|
+
label: b.title,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
275
401
|
return { kind: b.status, label: b.title };
|
|
276
402
|
}
|
|
277
403
|
function cellFromTimer(t) {
|