libero-mcp 0.2.3 → 0.2.4
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/data/migrations/011_profile_default_confirmed.sql +4 -0
- package/data/schema.sql +4 -0
- package/dist/data/migrations/011_profile_default_confirmed.sql +4 -0
- package/dist/data/schema.sql +4 -0
- package/dist/mcp/server-core.js +263 -55
- package/dist/mcp/src/profile/confirm-default.js +48 -0
- package/dist/mcp/src/profile-confirm.js +28 -0
- package/dist/mcp/src/profile-helpers.js +109 -0
- package/dist/mcp/src/reminder/index.js +20 -6
- package/dist/mcp/src/repo/in-memory.js +74 -1
- package/dist/mcp/src/repo/sqlite.js +74 -1
- package/dist/mcp/src/scheduler/index.js +46 -1
- package/dist/mcp/src/stats/index.js +5 -2
- package/dist/mcp/src/task/index.js +13 -10
- package/dist/mcp/src/timeblock/index.js +69 -14
- package/dist/mcp/src/timeline/index.js +429 -0
- package/dist/mcp/src/timer/index.js +20 -9
- package/dist/mcp/src/user-id-resolver.js +30 -0
- package/package.json +7 -2
package/data/schema.sql
CHANGED
|
@@ -5,6 +5,10 @@ CREATE TABLE IF NOT EXISTS user_profile (
|
|
|
5
5
|
id TEXT PRIMARY KEY,
|
|
6
6
|
display_name TEXT NOT NULL DEFAULT '',
|
|
7
7
|
expectations TEXT,
|
|
8
|
+
-- 005_profile_settings: 结构化约定存为 JSON
|
|
9
|
+
settings TEXT,
|
|
10
|
+
-- 011_profile_default_confirmed: MV-COACH-1 首次确认时间戳
|
|
11
|
+
default_confirmed_at TEXT,
|
|
8
12
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
9
13
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
10
14
|
deleted_at TEXT
|
package/dist/data/schema.sql
CHANGED
|
@@ -5,6 +5,10 @@ CREATE TABLE IF NOT EXISTS user_profile (
|
|
|
5
5
|
id TEXT PRIMARY KEY,
|
|
6
6
|
display_name TEXT NOT NULL DEFAULT '',
|
|
7
7
|
expectations TEXT,
|
|
8
|
+
-- 005_profile_settings: 结构化约定存为 JSON
|
|
9
|
+
settings TEXT,
|
|
10
|
+
-- 011_profile_default_confirmed: MV-COACH-1 首次确认时间戳
|
|
11
|
+
default_confirmed_at TEXT,
|
|
8
12
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
9
13
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
10
14
|
deleted_at TEXT
|
package/dist/mcp/server-core.js
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { SqliteCategoryRepo, SqliteTimeBlockRepo, SqliteStatsRepo, SqliteTimerRepo, SqliteProfileRepo, SqliteReminderRepo, } from "./src/repo/sqlite.js";
|
|
3
|
+
import { SqliteCategoryRepo, SqliteTimeBlockRepo, SqliteStatsRepo, SqliteTimerRepo, SqliteProfileRepo, SqliteReminderRepo, SqliteTimelineRepo, } from "./src/repo/sqlite.js";
|
|
4
4
|
import { createCategoryTools } from "./src/categories/index.js";
|
|
5
5
|
import { createTimeBlockTools } from "./src/timeblock/index.js";
|
|
6
6
|
import { createStatsTools } from "./src/stats/index.js";
|
|
7
7
|
import { createMatrixTools } from "./src/matrix/index.js";
|
|
8
|
+
import { createTimelineTools } from "./src/timeline/index.js";
|
|
8
9
|
import { createTimerTools } from "./src/timer/index.js";
|
|
9
10
|
import { createProfileTools } from "./src/profile/index.js";
|
|
11
|
+
import { createProfileConfirmTools } from "./src/profile/confirm-default.js";
|
|
10
12
|
import { createReminderTools } from "./src/reminder/index.js";
|
|
11
13
|
import { createSchedulerTools } from "./src/scheduler/index.js";
|
|
12
14
|
import { createTaskTools } from "./src/task/index.js";
|
|
13
15
|
import { nowLocal, formatTime, parseTime } from "./src/clock/index.js";
|
|
14
16
|
import { buildServerInstructions } from "./cold-start.js";
|
|
17
|
+
import { resolveDefaultUserId, logResolvedDefaultUserId, } from "./src/user-id-resolver.js";
|
|
15
18
|
/** Wrap any tool result as MCP text content (JSON). Void results become {ok:true}. */
|
|
16
19
|
function asContent(result) {
|
|
17
20
|
return {
|
|
@@ -23,12 +26,34 @@ function asContent(result) {
|
|
|
23
26
|
],
|
|
24
27
|
};
|
|
25
28
|
}
|
|
29
|
+
/** Inject default user_id when LLM omits it; otherwise return isError payload. */
|
|
30
|
+
export function makeUserIdInjector(defaultUserId) {
|
|
31
|
+
return function inject(input) {
|
|
32
|
+
if (input.user_id)
|
|
33
|
+
return input;
|
|
34
|
+
if (defaultUserId)
|
|
35
|
+
return { ...input, user_id: defaultUserId };
|
|
36
|
+
return {
|
|
37
|
+
error: "user_id is required (no default configured: set LIBERO_DEFAULT_USER_ID or have at least one non-system user_profile)",
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function errorContent(msg) {
|
|
42
|
+
return {
|
|
43
|
+
content: [{ type: "text", text: JSON.stringify({ error: msg }) }],
|
|
44
|
+
isError: true,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
26
47
|
/**
|
|
27
48
|
* Build the libero MCP server on a given DB connection.
|
|
28
49
|
* Tools throw Error on business violations; handlers let them propagate —
|
|
29
50
|
* the SDK turns thrown errors into MCP tool errors (isError) preserving the message.
|
|
30
51
|
*/
|
|
31
52
|
export function buildServer(db) {
|
|
53
|
+
// MV-COACH-3 Slice 2: resolve default user_id once at server build
|
|
54
|
+
const defaultUser = resolveDefaultUserId({ db, env: process.env });
|
|
55
|
+
logResolvedDefaultUserId(defaultUser);
|
|
56
|
+
const injector = makeUserIdInjector(defaultUser.userId);
|
|
32
57
|
// ── construct repos (S10 §3.3) ──
|
|
33
58
|
const categoryRepo = new SqliteCategoryRepo(db);
|
|
34
59
|
const timeblockRepo = new SqliteTimeBlockRepo(db);
|
|
@@ -36,14 +61,17 @@ export function buildServer(db) {
|
|
|
36
61
|
const timerRepo = new SqliteTimerRepo(db);
|
|
37
62
|
const profileRepo = new SqliteProfileRepo(db);
|
|
38
63
|
const reminderRepo = new SqliteReminderRepo(db);
|
|
64
|
+
const timelineRepo = new SqliteTimelineRepo(db);
|
|
39
65
|
// ── inject tool factories ──
|
|
40
66
|
const categories = createCategoryTools(categoryRepo);
|
|
41
|
-
const tb = createTimeBlockTools(timeblockRepo);
|
|
67
|
+
const tb = createTimeBlockTools(timeblockRepo, db);
|
|
42
68
|
const stats = createStatsTools(statsRepo);
|
|
43
69
|
const matrix = createMatrixTools(statsRepo); // matrix reuses StatsRepo
|
|
44
|
-
const
|
|
70
|
+
const timeline = createTimelineTools(timelineRepo);
|
|
71
|
+
const timer = createTimerTools(timerRepo, timeblockRepo, db);
|
|
45
72
|
const profile = createProfileTools(profileRepo);
|
|
46
|
-
const
|
|
73
|
+
const profileConfirm = createProfileConfirmTools(db);
|
|
74
|
+
const reminder = createReminderTools(reminderRepo, undefined, db);
|
|
47
75
|
const scheduler = createSchedulerTools(timeblockRepo, reminderRepo, categoryRepo);
|
|
48
76
|
const task = createTaskTools(db);
|
|
49
77
|
// instructions 必须放在第 2 参 ServerOptions(不是 serverInfo)—— SDK 只从 options 读
|
|
@@ -59,49 +87,94 @@ export function buildServer(db) {
|
|
|
59
87
|
server.registerTool("timer.start", {
|
|
60
88
|
description: "开始一个计时会话",
|
|
61
89
|
inputSchema: {
|
|
62
|
-
user_id: z.string().
|
|
90
|
+
user_id: z.string().optional(),
|
|
91
|
+
target_user_id: z.string().min(1).optional(),
|
|
63
92
|
event: z.string().min(1),
|
|
64
93
|
categoryId: z.string().optional(),
|
|
65
94
|
task_id: z.string().optional(),
|
|
66
95
|
},
|
|
67
|
-
}, async (
|
|
68
|
-
|
|
69
|
-
|
|
96
|
+
}, async (rawInput) => {
|
|
97
|
+
const r = injector(rawInput);
|
|
98
|
+
if ("error" in r)
|
|
99
|
+
return errorContent(r.error);
|
|
100
|
+
return asContent(await timer.start(r.user_id, r.event, r.categoryId, r.task_id, r.target_user_id));
|
|
101
|
+
});
|
|
102
|
+
server.registerTool("timer.pause", { description: "暂停当前计时", inputSchema: { user_id: z.string().optional() } }, async (rawInput) => {
|
|
103
|
+
const r = injector(rawInput);
|
|
104
|
+
if ("error" in r)
|
|
105
|
+
return errorContent(r.error);
|
|
106
|
+
return asContent(await timer.pause(r.user_id));
|
|
107
|
+
});
|
|
108
|
+
server.registerTool("timer.resume", { description: "恢复已暂停的计时", inputSchema: { user_id: z.string().optional() } }, async (rawInput) => {
|
|
109
|
+
const r = injector(rawInput);
|
|
110
|
+
if ("error" in r)
|
|
111
|
+
return errorContent(r.error);
|
|
112
|
+
return asContent(await timer.resume(r.user_id));
|
|
113
|
+
});
|
|
70
114
|
server.registerTool("timer.stop", {
|
|
71
115
|
description: "停止计时并返回记录(传 rating 时自动落盘 timeblock)",
|
|
72
116
|
inputSchema: {
|
|
73
|
-
user_id: z.string().
|
|
117
|
+
user_id: z.string().optional(),
|
|
74
118
|
rating: z.number().min(1).max(5).optional(),
|
|
75
119
|
rating_reason: z.string().optional(),
|
|
76
120
|
},
|
|
77
|
-
}, async (
|
|
78
|
-
|
|
79
|
-
|
|
121
|
+
}, async (rawInput) => {
|
|
122
|
+
const r = injector(rawInput);
|
|
123
|
+
if ("error" in r)
|
|
124
|
+
return errorContent(r.error);
|
|
125
|
+
return asContent(await timer.stop(r.user_id, {
|
|
126
|
+
rating: r.rating,
|
|
127
|
+
rating_reason: r.rating_reason,
|
|
128
|
+
}));
|
|
129
|
+
});
|
|
130
|
+
server.registerTool("timer.status", { description: "查询某用户当前计时状态", inputSchema: { user_id: z.string().optional() } }, async (rawInput) => {
|
|
131
|
+
const r = injector(rawInput);
|
|
132
|
+
if ("error" in r)
|
|
133
|
+
return errorContent(r.error);
|
|
134
|
+
return asContent(await timer.status(r.user_id));
|
|
135
|
+
});
|
|
136
|
+
server.registerTool("timer.elapsed", { description: "查询当前计时已过毫秒数", inputSchema: { user_id: z.string().optional() } }, async (rawInput) => {
|
|
137
|
+
const r = injector(rawInput);
|
|
138
|
+
if ("error" in r)
|
|
139
|
+
return errorContent(r.error);
|
|
140
|
+
return asContent(await timer.elapsed(r.user_id));
|
|
141
|
+
});
|
|
80
142
|
server.registerTool("timer.statusAll", { description: "查询所有用户的当前计时状态(调试/兜底用)", inputSchema: {} }, async () => asContent(await timer.statusAll()));
|
|
81
143
|
// ── S26: pomodoro v1 tools ──
|
|
82
144
|
server.registerTool("timer.startFocus", {
|
|
83
145
|
description: "开始一个专注计时(番茄钟),带目标时长",
|
|
84
146
|
inputSchema: {
|
|
85
|
-
user_id: z.string().
|
|
147
|
+
user_id: z.string().optional(),
|
|
86
148
|
event: z.string().min(1),
|
|
87
149
|
focusMin: z.number().positive().optional(),
|
|
88
150
|
categoryId: z.string().optional(),
|
|
89
151
|
task_id: z.string().optional(),
|
|
90
152
|
},
|
|
91
|
-
}, async (
|
|
153
|
+
}, async (rawInput) => {
|
|
154
|
+
const r = injector(rawInput);
|
|
155
|
+
if ("error" in r)
|
|
156
|
+
return errorContent(r.error);
|
|
157
|
+
return asContent(await timer.startFocus(r.user_id, r.event, r.focusMin, r.categoryId, r.task_id));
|
|
158
|
+
});
|
|
92
159
|
server.registerTool("timer.remaining", {
|
|
93
160
|
description: "查询专注计时剩余时间(负数=超时)",
|
|
94
161
|
inputSchema: {
|
|
95
|
-
user_id: z.string().
|
|
162
|
+
user_id: z.string().optional(),
|
|
96
163
|
},
|
|
97
|
-
}, async (
|
|
164
|
+
}, async (rawInput) => {
|
|
165
|
+
const r = injector(rawInput);
|
|
166
|
+
if ("error" in r)
|
|
167
|
+
return errorContent(r.error);
|
|
168
|
+
return asContent(await timer.remaining(r.user_id));
|
|
169
|
+
});
|
|
98
170
|
// ============================================================
|
|
99
171
|
// timeblock (5 tools — single-object args)
|
|
100
172
|
// ============================================================
|
|
101
173
|
server.registerTool("timeblock.create", {
|
|
102
174
|
description: "创建一条时间记录",
|
|
103
175
|
inputSchema: {
|
|
104
|
-
user_id: z.string().
|
|
176
|
+
user_id: z.string().optional(),
|
|
177
|
+
target_user_id: z.string().min(1).optional(),
|
|
105
178
|
title: z.string().min(1),
|
|
106
179
|
category_id: z.string().optional(),
|
|
107
180
|
start_time: z.string(),
|
|
@@ -111,12 +184,24 @@ export function buildServer(db) {
|
|
|
111
184
|
rating_reason: z.string().optional(),
|
|
112
185
|
notes: z.string().optional(),
|
|
113
186
|
},
|
|
114
|
-
}, async (
|
|
187
|
+
}, async (rawInput) => {
|
|
188
|
+
const r = injector(rawInput);
|
|
189
|
+
if ("error" in r)
|
|
190
|
+
return errorContent(r.error);
|
|
191
|
+
return asContent(await tb.create(r));
|
|
192
|
+
});
|
|
115
193
|
server.registerTool("timeblock.getById", { description: "按 id 获取单条时间记录", inputSchema: { id: z.string().min(1) } }, async ({ id }) => asContent(await tb.getById(id)));
|
|
116
194
|
server.registerTool("timeblock.update", {
|
|
117
|
-
description: "
|
|
195
|
+
description: "更新一条时间记录(标题/时间/评分/status 等)。" +
|
|
196
|
+
"MV-COACH-3 Slice 4 用户主权状态机:" +
|
|
197
|
+
"① 开工 cron 触发到点 → status='unverified'(等用户核实,**不计入实绩**);" +
|
|
198
|
+
"② 用户确认执行(主动开始计时 / 回报「执行了」/ 追问后答执行了)→ status='completed'(**唯一计入实绩**);" +
|
|
199
|
+
"③ 用户答「没执行」→ 不改 status,直接 timeblock.remove 软删(留 deleted_at 可复盘,不进实绩统计);" +
|
|
200
|
+
"④ 用户一直不答 → 保持 unverified,永远等(不自动判死)。" +
|
|
201
|
+
"⚠️ LLM 不能擅自把 planned/unverified 升级到 completed——只有用户确认才能升。",
|
|
118
202
|
inputSchema: {
|
|
119
203
|
id: z.string().min(1),
|
|
204
|
+
target_user_id: z.string().min(1).optional(),
|
|
120
205
|
title: z.string().optional(),
|
|
121
206
|
category_id: z.string().optional(),
|
|
122
207
|
start_time: z.string().optional(),
|
|
@@ -125,14 +210,22 @@ export function buildServer(db) {
|
|
|
125
210
|
rating: z.number().min(1).max(5).optional(),
|
|
126
211
|
rating_reason: z.string().optional(),
|
|
127
212
|
notes: z.string().optional(),
|
|
213
|
+
status: z.enum(["planned", "unverified", "completed"]).optional(),
|
|
128
214
|
},
|
|
129
215
|
}, async ({ id, ...rest }) => asContent(await tb.update(id, rest)));
|
|
130
|
-
server.registerTool("timeblock.remove", {
|
|
216
|
+
server.registerTool("timeblock.remove", {
|
|
217
|
+
description: "软删除一条时间记录",
|
|
218
|
+
inputSchema: {
|
|
219
|
+
id: z.string().min(1),
|
|
220
|
+
target_user_id: z.string().min(1).optional(),
|
|
221
|
+
},
|
|
222
|
+
}, async ({ id, target_user_id }) => asContent(await tb.remove(id, { target_user_id })));
|
|
131
223
|
server.registerTool("timeblock.restore", { description: "恢复一条已软删除的时间记录", inputSchema: { id: z.string().min(1) } }, async ({ id }) => asContent(await tb.restore(id)));
|
|
132
224
|
server.registerTool("timeblock.list", {
|
|
133
225
|
description: "按条件查询时间记录列表",
|
|
134
226
|
inputSchema: {
|
|
135
|
-
user_id: z.string().
|
|
227
|
+
user_id: z.string().optional(),
|
|
228
|
+
target_user_id: z.string().min(1).optional(),
|
|
136
229
|
date: z.string().optional(),
|
|
137
230
|
from: z.string().optional(),
|
|
138
231
|
to: z.string().optional(),
|
|
@@ -141,39 +234,81 @@ export function buildServer(db) {
|
|
|
141
234
|
limit: z.number().optional(),
|
|
142
235
|
offset: z.number().optional(),
|
|
143
236
|
},
|
|
144
|
-
}, async (
|
|
237
|
+
}, async (rawInput) => {
|
|
238
|
+
const r = injector(rawInput);
|
|
239
|
+
if ("error" in r)
|
|
240
|
+
return errorContent(r.error);
|
|
241
|
+
return asContent(await tb.list(r));
|
|
242
|
+
});
|
|
145
243
|
// ============================================================
|
|
146
244
|
// stats (2 tools — single-object filters)
|
|
147
245
|
// ============================================================
|
|
148
246
|
server.registerTool("stats.summarize", {
|
|
149
247
|
description: "按分类汇总时间统计(占比/记录数)",
|
|
150
248
|
inputSchema: {
|
|
151
|
-
user_id: z.string().
|
|
249
|
+
user_id: z.string().optional(),
|
|
250
|
+
target_user_id: z.string().min(1).optional(),
|
|
152
251
|
from: z.string().optional(),
|
|
153
252
|
to: z.string().optional(),
|
|
154
253
|
category_id: z.string().optional(),
|
|
155
254
|
},
|
|
156
|
-
}, async (
|
|
255
|
+
}, async (rawInput) => {
|
|
256
|
+
const r = injector(rawInput);
|
|
257
|
+
if ("error" in r)
|
|
258
|
+
return errorContent(r.error);
|
|
259
|
+
return asContent(await stats.summarize(r));
|
|
260
|
+
});
|
|
157
261
|
server.registerTool("stats.dailyBreakdown", {
|
|
158
262
|
description: "按天×分类拆分时间明细",
|
|
159
263
|
inputSchema: {
|
|
160
|
-
user_id: z.string().
|
|
264
|
+
user_id: z.string().optional(),
|
|
265
|
+
target_user_id: z.string().min(1).optional(),
|
|
161
266
|
from: z.string().optional(),
|
|
162
267
|
to: z.string().optional(),
|
|
163
268
|
category_id: z.string().optional(),
|
|
164
269
|
},
|
|
165
|
-
}, async (
|
|
270
|
+
}, async (rawInput) => {
|
|
271
|
+
const r = injector(rawInput);
|
|
272
|
+
if ("error" in r)
|
|
273
|
+
return errorContent(r.error);
|
|
274
|
+
return asContent(await stats.dailyBreakdown(r));
|
|
275
|
+
});
|
|
166
276
|
// ============================================================
|
|
167
277
|
// matrix (1 tool — single-object filters, reuses StatsRepo)
|
|
168
278
|
// ============================================================
|
|
169
279
|
server.registerTool("matrix.renderMatrix", {
|
|
170
280
|
description: "时间×评分四象限矩阵(能耗黑洞/保持区/亮点区/可放弃)",
|
|
171
281
|
inputSchema: {
|
|
172
|
-
user_id: z.string().
|
|
282
|
+
user_id: z.string().optional(),
|
|
283
|
+
from: z.string().optional(),
|
|
284
|
+
to: z.string().optional(),
|
|
285
|
+
},
|
|
286
|
+
}, async (rawInput) => {
|
|
287
|
+
const r = injector(rawInput);
|
|
288
|
+
if ("error" in r)
|
|
289
|
+
return errorContent(r.error);
|
|
290
|
+
return asContent(await matrix.renderMatrix(r));
|
|
291
|
+
});
|
|
292
|
+
// ============================================================
|
|
293
|
+
// schedule.timeline (emoji matrix + detail table)
|
|
294
|
+
// ============================================================
|
|
295
|
+
server.registerTool("schedule.timeline", {
|
|
296
|
+
description: "看日程/复盘的首选视图:emoji 矩阵+细项表格,含计划/计时中/未回应/实绩全状态。" +
|
|
297
|
+
"用户说「看今天/看日程/在干啥/复盘/接下来 N 小时」时调本工具,并直接 echo rendered_text(不重写、不改格式)。" +
|
|
298
|
+
"不要调 scheduler.listSchedules(那是内部排程查询,不是用户视图)。",
|
|
299
|
+
inputSchema: {
|
|
300
|
+
user_id: z.string().optional(),
|
|
301
|
+
target_user_id: z.string().min(1).optional(),
|
|
173
302
|
from: z.string().optional(),
|
|
174
303
|
to: z.string().optional(),
|
|
304
|
+
format: z.enum(["emoji", "ascii"]).optional(),
|
|
175
305
|
},
|
|
176
|
-
}, async (
|
|
306
|
+
}, async (rawInput) => {
|
|
307
|
+
const r = injector(rawInput);
|
|
308
|
+
if ("error" in r)
|
|
309
|
+
return errorContent(r.error);
|
|
310
|
+
return asContent(await timeline.renderTimeline(r));
|
|
311
|
+
});
|
|
177
312
|
// ============================================================
|
|
178
313
|
// clock (3 tools — pure functions, no repo)
|
|
179
314
|
// ============================================================
|
|
@@ -199,12 +334,17 @@ export function buildServer(db) {
|
|
|
199
334
|
// ============================================================
|
|
200
335
|
server.registerTool("profile.get", {
|
|
201
336
|
description: "获取用户 profile(含结构化约定)",
|
|
202
|
-
inputSchema: { user_id: z.string().
|
|
203
|
-
}, async (
|
|
337
|
+
inputSchema: { user_id: z.string().optional() },
|
|
338
|
+
}, async (rawInput) => {
|
|
339
|
+
const r = injector(rawInput);
|
|
340
|
+
if ("error" in r)
|
|
341
|
+
return errorContent(r.error);
|
|
342
|
+
return asContent(await profile.get(r.user_id));
|
|
343
|
+
});
|
|
204
344
|
server.registerTool("profile.upsert", {
|
|
205
|
-
description: "创建或更新用户 profile。
|
|
345
|
+
description: "创建或更新用户 profile(display_name / expectations / settings)。首次确认请用 profile.confirm_default。",
|
|
206
346
|
inputSchema: {
|
|
207
|
-
user_id: z.string().
|
|
347
|
+
user_id: z.string().optional(),
|
|
208
348
|
display_name: z.string().optional(),
|
|
209
349
|
expectations: z.string().optional(),
|
|
210
350
|
settings: z
|
|
@@ -212,20 +352,40 @@ export function buildServer(db) {
|
|
|
212
352
|
.passthrough()
|
|
213
353
|
.optional(),
|
|
214
354
|
},
|
|
215
|
-
}, async (
|
|
355
|
+
}, async (rawInput) => {
|
|
356
|
+
const r = injector(rawInput);
|
|
357
|
+
if ("error" in r)
|
|
358
|
+
return errorContent(r.error);
|
|
359
|
+
return asContent(await profile.upsert(r));
|
|
360
|
+
});
|
|
361
|
+
server.registerTool("profile.confirm_default", {
|
|
362
|
+
description: "首次写入前确认默认用户:写入 user_profile,设置 display_name 与 default_confirmed_at。display_name 重名则拒绝。",
|
|
363
|
+
inputSchema: {
|
|
364
|
+
platform_user_id: z.string().min(1),
|
|
365
|
+
display_name: z.string().min(1),
|
|
366
|
+
},
|
|
367
|
+
}, async (input) => asContent(await profileConfirm.confirmDefault(input)));
|
|
216
368
|
// ============================================================
|
|
217
369
|
// reminder (6 tools — S11)
|
|
218
370
|
// ============================================================
|
|
219
371
|
server.registerTool("reminder.create", {
|
|
220
|
-
description: "
|
|
372
|
+
description: "⚠️ 仅写 DB,不主动推送(libero-mcp 无 background sender)。" +
|
|
373
|
+
"在 hermes 等有 cronjob 工具的平台上,用户要'X 时间提醒我 Y'请调宿主 cronjob(action=create, deliver=origin),不要用本工具。" +
|
|
374
|
+
"本工具仅用于:① 无 cron 能力的平台兜底;② 查询场景配合 getDue/getMissed。",
|
|
221
375
|
inputSchema: {
|
|
222
|
-
user_id: z.string().
|
|
376
|
+
user_id: z.string().optional(),
|
|
377
|
+
target_user_id: z.string().min(1).optional(),
|
|
223
378
|
content: z.string().min(1),
|
|
224
379
|
remind_at: z.string().min(1),
|
|
225
380
|
channel: z.string().optional(),
|
|
226
381
|
timeblock_id: z.string().optional(),
|
|
227
382
|
},
|
|
228
|
-
}, async (
|
|
383
|
+
}, async (rawInput) => {
|
|
384
|
+
const r = injector(rawInput);
|
|
385
|
+
if ("error" in r)
|
|
386
|
+
return errorContent(r.error);
|
|
387
|
+
return asContent(await reminder.create(r));
|
|
388
|
+
});
|
|
229
389
|
server.registerTool("reminder.getDue", {
|
|
230
390
|
description: "获取到期且待发送的提醒列表",
|
|
231
391
|
inputSchema: {
|
|
@@ -242,9 +402,14 @@ export function buildServer(db) {
|
|
|
242
402
|
server.registerTool("reminder.getMissed", {
|
|
243
403
|
description: "获取过期但未送达的提醒(被动补发用)",
|
|
244
404
|
inputSchema: {
|
|
245
|
-
user_id: z.string().
|
|
405
|
+
user_id: z.string().optional(),
|
|
246
406
|
},
|
|
247
|
-
}, async (
|
|
407
|
+
}, async (rawInput) => {
|
|
408
|
+
const r = injector(rawInput);
|
|
409
|
+
if ("error" in r)
|
|
410
|
+
return errorContent(r.error);
|
|
411
|
+
return asContent(await reminder.getMissedReminders({ user_id: r.user_id }));
|
|
412
|
+
});
|
|
248
413
|
server.registerTool("reminder.markSent", {
|
|
249
414
|
description: "标记提醒为已发送(幂等)",
|
|
250
415
|
inputSchema: { id: z.string().min(1) },
|
|
@@ -257,9 +422,15 @@ export function buildServer(db) {
|
|
|
257
422
|
// scheduler (3 tools — S27, reuses timeblockRepo)
|
|
258
423
|
// ============================================================
|
|
259
424
|
server.registerTool("scheduler.createSchedule", {
|
|
260
|
-
description: "创建一个日程(planned
|
|
425
|
+
description: "创建一个日程(planned 时间块)。" +
|
|
426
|
+
"⚠️ MV-COACH-3 Slice 4 v2 教练闭环(用户主权模型):建完 schedule 后,**必须**在同一回复里完成:" +
|
|
427
|
+
"① 调 cronjob(action=create, deliver=origin) 建开工 cron(schedule=日程开始时间),cron prompt 必须用 libero-coach skill 的'开工 cron 标准 prompt 模板'——**不允许自创结构**。" +
|
|
428
|
+
"② 那条标准 prompt 里写死了 3 步:推提醒 + 调 timeblock.update(status=**unverified**,不是 in_progress) + 建追问 cron(时间动态算:有耗时→过半;无耗时→+30min)。" +
|
|
429
|
+
"③ 顺带问用户'要不要每天 22:00 做个简短复盘?'(B 类推送必须先问再建)。" +
|
|
430
|
+
"关键:状态机用户主权——LLM 只能到点标 unverified 等用户核实,**不能擅自升级到 completed**。" +
|
|
431
|
+
"详见 libero-coach skill 的'推送抓手 + 状态机一条龙'段。",
|
|
261
432
|
inputSchema: {
|
|
262
|
-
user_id: z.string().
|
|
433
|
+
user_id: z.string().optional(),
|
|
263
434
|
title: z.string().min(1),
|
|
264
435
|
category_id: z.string().optional(),
|
|
265
436
|
start_time: z.string(),
|
|
@@ -269,23 +440,40 @@ export function buildServer(db) {
|
|
|
269
440
|
rating_reason: z.string().optional(),
|
|
270
441
|
notes: z.string().optional(),
|
|
271
442
|
},
|
|
272
|
-
}, async (
|
|
443
|
+
}, async (rawInput) => {
|
|
444
|
+
const r = injector(rawInput);
|
|
445
|
+
if ("error" in r)
|
|
446
|
+
return errorContent(r.error);
|
|
447
|
+
return asContent(await scheduler.createSchedule(r));
|
|
448
|
+
});
|
|
273
449
|
server.registerTool("scheduler.listSchedules", {
|
|
274
|
-
description: "
|
|
450
|
+
description: "内部工具:查询 planned 日程块用于排程冲突检查。" +
|
|
451
|
+
"⚠️ 用户「看今天/看日程/复盘/在干啥」请调 schedule.timeline(含矩阵+表格+全状态视图,不要调本工具)。" +
|
|
452
|
+
"本工具仅用于排程编排时的内部查询。",
|
|
275
453
|
inputSchema: {
|
|
276
|
-
user_id: z.string().
|
|
454
|
+
user_id: z.string().optional(),
|
|
277
455
|
from: z.string().optional(),
|
|
278
456
|
to: z.string().optional(),
|
|
279
457
|
},
|
|
280
|
-
}, async (
|
|
458
|
+
}, async (rawInput) => {
|
|
459
|
+
const r = injector(rawInput);
|
|
460
|
+
if ("error" in r)
|
|
461
|
+
return errorContent(r.error);
|
|
462
|
+
return asContent(await scheduler.listSchedules(r));
|
|
463
|
+
});
|
|
281
464
|
server.registerTool("scheduler.checkConflict", {
|
|
282
465
|
description: "检查时间段是否与已有日程冲突",
|
|
283
466
|
inputSchema: {
|
|
284
|
-
user_id: z.string().
|
|
467
|
+
user_id: z.string().optional(),
|
|
285
468
|
start_time: z.string(),
|
|
286
469
|
end_time: z.string(),
|
|
287
470
|
},
|
|
288
|
-
}, async (
|
|
471
|
+
}, async (rawInput) => {
|
|
472
|
+
const r = injector(rawInput);
|
|
473
|
+
if ("error" in r)
|
|
474
|
+
return errorContent(r.error);
|
|
475
|
+
return asContent(await scheduler.checkConflict(r));
|
|
476
|
+
});
|
|
289
477
|
// ============================================================
|
|
290
478
|
// task (7 tools — S29 P2-3 + 事一 draft/commit, takes raw db)
|
|
291
479
|
// ============================================================
|
|
@@ -296,12 +484,18 @@ export function buildServer(db) {
|
|
|
296
484
|
// 底层 createTask 仍保留 parent_id 能力(task.test.ts 单测不破)。
|
|
297
485
|
inputSchema: z
|
|
298
486
|
.object({
|
|
299
|
-
user_id: z.string().
|
|
487
|
+
user_id: z.string().optional(),
|
|
488
|
+
target_user_id: z.string().min(1).optional(),
|
|
300
489
|
title: z.string().min(1),
|
|
301
490
|
est_minutes: z.number().optional(),
|
|
302
491
|
})
|
|
303
492
|
.strict(),
|
|
304
|
-
}, async (
|
|
493
|
+
}, async (rawInput) => {
|
|
494
|
+
const r = injector(rawInput);
|
|
495
|
+
if ("error" in r)
|
|
496
|
+
return errorContent(r.error);
|
|
497
|
+
return asContent(await task.createTask(r));
|
|
498
|
+
});
|
|
305
499
|
server.registerTool("task.get", {
|
|
306
500
|
description: "按 id 获取任务(含递归子树)",
|
|
307
501
|
inputSchema: { id: z.string().min(1) },
|
|
@@ -309,10 +503,16 @@ export function buildServer(db) {
|
|
|
309
503
|
server.registerTool("task.list", {
|
|
310
504
|
description: "列出任务(parent_id=null 仅根;给定则该父的子;省略则全部)",
|
|
311
505
|
inputSchema: {
|
|
312
|
-
user_id: z.string().
|
|
506
|
+
user_id: z.string().optional(),
|
|
507
|
+
target_user_id: z.string().min(1).optional(),
|
|
313
508
|
parent_id: z.string().nullable().optional(),
|
|
314
509
|
},
|
|
315
|
-
}, async (
|
|
510
|
+
}, async (rawInput) => {
|
|
511
|
+
const r = injector(rawInput);
|
|
512
|
+
if ("error" in r)
|
|
513
|
+
return errorContent(r.error);
|
|
514
|
+
return asContent(await task.listTasks(r));
|
|
515
|
+
});
|
|
316
516
|
server.registerTool("task.update", {
|
|
317
517
|
description: "更新任务(title/est_minutes/status;状态机校验非法流转)",
|
|
318
518
|
inputSchema: {
|
|
@@ -332,10 +532,15 @@ export function buildServer(db) {
|
|
|
332
532
|
server.registerTool("task.findPlan", {
|
|
333
533
|
description: "按事件名 exact-match 查拆解计划:返回根任务 + 第一块未完成子任务(record 开工时链 task_id,驱动闭环正反馈)",
|
|
334
534
|
inputSchema: {
|
|
335
|
-
user_id: z.string().
|
|
535
|
+
user_id: z.string().optional(),
|
|
336
536
|
title: z.string().min(1),
|
|
337
537
|
},
|
|
338
|
-
}, async (
|
|
538
|
+
}, async (rawInput) => {
|
|
539
|
+
const r = injector(rawInput);
|
|
540
|
+
if ("error" in r)
|
|
541
|
+
return errorContent(r.error);
|
|
542
|
+
return asContent(await task.findPlan(r));
|
|
543
|
+
});
|
|
339
544
|
// S33 v3: task.draftPlan / task.commitPlan 从 MCP 工具列表移除(不再暴露给 mimo)。
|
|
340
545
|
// 拆解唯一入口 = decompose.split(见下方 decompose 段)。
|
|
341
546
|
// 底层 draftPlan/commitPlan 方法保留在 mcp/src/task/index.ts(被 decompose.split 复用 + task.test.ts 单测覆盖)。
|
|
@@ -347,7 +552,7 @@ export function buildServer(db) {
|
|
|
347
552
|
server.registerTool("decompose.split", {
|
|
348
553
|
description: "拆解 timeblock 为结构化子任务树(拆解唯一入口)。第一次调用出方案(不写库),第二次带 confirm_token 落地。状态机强制 draft→confirm→commit。⚠️ 拆子任务请用本工具,不要用 task.create。",
|
|
349
554
|
inputSchema: {
|
|
350
|
-
user_id: z.string().
|
|
555
|
+
user_id: z.string().optional(),
|
|
351
556
|
timeblock_id: z.string().min(1),
|
|
352
557
|
children: z
|
|
353
558
|
.array(z.object({
|
|
@@ -357,8 +562,11 @@ export function buildServer(db) {
|
|
|
357
562
|
.min(1),
|
|
358
563
|
confirm_token: z.string().optional(),
|
|
359
564
|
},
|
|
360
|
-
}, async (
|
|
361
|
-
const
|
|
565
|
+
}, async (rawInput) => {
|
|
566
|
+
const r = injector(rawInput);
|
|
567
|
+
if ("error" in r)
|
|
568
|
+
return errorContent(r.error);
|
|
569
|
+
const result = await task.splitDecompose(r);
|
|
362
570
|
// DecomposeErrorResult → isError content(不 throw,避 hermes 熔断器)
|
|
363
571
|
if ("ok" in result && result.ok === false) {
|
|
364
572
|
return {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export function createProfileConfirmTools(db) {
|
|
2
|
+
return {
|
|
3
|
+
async confirmDefault(input) {
|
|
4
|
+
if (!input.platform_user_id || !input.platform_user_id.trim()) {
|
|
5
|
+
throw new Error("platform_user_id 为必填参数");
|
|
6
|
+
}
|
|
7
|
+
if (!input.display_name || !input.display_name.trim()) {
|
|
8
|
+
throw new Error("display_name 为必填参数");
|
|
9
|
+
}
|
|
10
|
+
const displayName = input.display_name.trim();
|
|
11
|
+
const platformUserId = input.platform_user_id.trim();
|
|
12
|
+
const taken = db
|
|
13
|
+
.prepare(`SELECT id FROM user_profile
|
|
14
|
+
WHERE display_name = ? AND id != ? AND deleted_at IS NULL`)
|
|
15
|
+
.get(displayName, platformUserId);
|
|
16
|
+
if (taken) {
|
|
17
|
+
return { status: "rejected", reason: "display_name_taken" };
|
|
18
|
+
}
|
|
19
|
+
const now = new Date().toISOString().replace("T", " ").replace("Z", "");
|
|
20
|
+
const existing = db
|
|
21
|
+
.prepare(`SELECT id FROM user_profile WHERE id = ? AND deleted_at IS NULL`)
|
|
22
|
+
.get(platformUserId);
|
|
23
|
+
if (existing) {
|
|
24
|
+
db.prepare(`UPDATE user_profile
|
|
25
|
+
SET display_name = ?, default_confirmed_at = ?, updated_at = ?
|
|
26
|
+
WHERE id = ?`).run(displayName, now, now, platformUserId);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
db.prepare(`INSERT INTO user_profile
|
|
30
|
+
(id, display_name, expectations, settings, default_confirmed_at, created_at, updated_at)
|
|
31
|
+
VALUES (?, ?, NULL, NULL, ?, ?, ?)`).run(platformUserId, displayName, now, now, now);
|
|
32
|
+
}
|
|
33
|
+
const row = db
|
|
34
|
+
.prepare(`SELECT id, display_name, expectations, settings, default_confirmed_at, created_at, updated_at
|
|
35
|
+
FROM user_profile WHERE id = ?`)
|
|
36
|
+
.get(platformUserId);
|
|
37
|
+
return {
|
|
38
|
+
id: row.id,
|
|
39
|
+
display_name: row.display_name,
|
|
40
|
+
default_confirmed_at: row.default_confirmed_at,
|
|
41
|
+
expectations: row.expectations,
|
|
42
|
+
settings: row.settings != null ? JSON.parse(row.settings) : null,
|
|
43
|
+
created_at: row.created_at,
|
|
44
|
+
updated_at: row.updated_at,
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|