libero-mcp 0.2.9 → 0.2.11

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.
@@ -300,6 +300,7 @@ export class InMemoryTimelineRepo {
300
300
  title: b.title,
301
301
  category_emoji: cat?.emoji ?? null,
302
302
  category_name: cat?.name ?? null,
303
+ category_color: cat?.color ?? null,
303
304
  };
304
305
  });
305
306
  const timers = this.seed.timer_sessions
@@ -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 "实绩";
@@ -250,6 +336,15 @@ export function renderTimelineCells(input) {
250
336
  }
251
337
  if (boundary)
252
338
  pushEllipsis();
339
+ // S9 v0.2: 末尾拼图例(按本次实际出现的色)
340
+ // 设计:图例短一行,只列实际出现过的符号;颜色块统一标"计划·X 类"
341
+ if (Object.keys(legend).length > 1) {
342
+ const legendParts = [];
343
+ for (const [sym, meaning] of Object.entries(legend)) {
344
+ legendParts.push(`${sym} ${meaning}`);
345
+ }
346
+ textLines.push(`图例:${legendParts.join(" · ")}`);
347
+ }
253
348
  return { rows, rendered_text: textLines.join("\n"), legend };
254
349
  }
255
350
  export function computeDefaultWindow(now) {
@@ -272,6 +367,18 @@ function cellFromBlock(b) {
272
367
  label: b.title,
273
368
  };
274
369
  }
370
+ // S9 v0.2: planned 块按分类上色(不再统一 🟦)
371
+ // unverified 保持 ❓ 不上色(状态优先于分类)
372
+ if (b.status === "planned") {
373
+ const ascii = categoryAsciiToken(b.category_name);
374
+ const tile = colorToTile(b.category_color);
375
+ return {
376
+ kind: "planned",
377
+ tile,
378
+ ascii,
379
+ label: b.title,
380
+ };
381
+ }
275
382
  return { kind: b.status, label: b.title };
276
383
  }
277
384
  function cellFromTimer(t) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libero-mcp",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "description": "AI 时间管理教练 MCP 工具集——时间块记录、统计、矩阵诊断、计时、分类、profile",
5
5
  "license": "MIT",
6
6
  "author": "amosyuan",