libero3 0.3.0

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.
Files changed (192) hide show
  1. package/COLD_START.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +43 -0
  4. package/data/migrations/001_initial_schema.sql +43 -0
  5. package/data/migrations/002_seed_categories.sql +18 -0
  6. package/data/migrations/003_add_rating.sql +4 -0
  7. package/data/migrations/004_timer_sessions.sql +21 -0
  8. package/data/migrations/005_profile_settings.sql +5 -0
  9. package/data/migrations/006_add_timeblock_status.sql +1 -0
  10. package/data/migrations/007_reminders.sql +17 -0
  11. package/data/migrations/008_timer_target_ms.sql +5 -0
  12. package/data/migrations/009_tasks.sql +22 -0
  13. package/data/migrations/010_timer_task_id.sql +4 -0
  14. package/data/migrations/011_profile_default_confirmed.sql +4 -0
  15. package/data/migrations/012_identity_tables.sql +68 -0
  16. package/data/migrations/013_identity_managed_by.sql +3 -0
  17. package/data/migrations/014_task_difficulty_priority.sql +6 -0
  18. package/data/migrations/015_timeblock_asked_at.sql +5 -0
  19. package/data/migrations/016_intent_columns.sql +8 -0
  20. package/data/migrations/017_ap_wallet.sql +25 -0
  21. package/data/migrations/018_ap_rewards.sql +21 -0
  22. package/data/migrations/019_ap_bounty.sql +10 -0
  23. package/data/migrations/020_chat_turns.sql +10 -0
  24. package/data/migrations/021_portrait.sql +4 -0
  25. package/data/migrations/022_family_emoji_single.sql +3 -0
  26. package/data/migrations/023_ap_balance_view_log.sql +9 -0
  27. package/data/schema.sql +75 -0
  28. package/dist/data/db.js +83 -0
  29. package/dist/data/migrations/001_initial_schema.sql +43 -0
  30. package/dist/data/migrations/002_seed_categories.sql +18 -0
  31. package/dist/data/migrations/003_add_rating.sql +4 -0
  32. package/dist/data/migrations/004_timer_sessions.sql +21 -0
  33. package/dist/data/migrations/005_profile_settings.sql +5 -0
  34. package/dist/data/migrations/006_add_timeblock_status.sql +1 -0
  35. package/dist/data/migrations/007_reminders.sql +17 -0
  36. package/dist/data/migrations/008_timer_target_ms.sql +5 -0
  37. package/dist/data/migrations/009_tasks.sql +22 -0
  38. package/dist/data/migrations/010_timer_task_id.sql +4 -0
  39. package/dist/data/migrations/011_profile_default_confirmed.sql +4 -0
  40. package/dist/data/migrations/012_identity_tables.sql +68 -0
  41. package/dist/data/migrations/013_identity_managed_by.sql +3 -0
  42. package/dist/data/migrations/014_task_difficulty_priority.sql +6 -0
  43. package/dist/data/migrations/015_timeblock_asked_at.sql +5 -0
  44. package/dist/data/migrations/016_intent_columns.sql +8 -0
  45. package/dist/data/migrations/017_ap_wallet.sql +25 -0
  46. package/dist/data/migrations/018_ap_rewards.sql +21 -0
  47. package/dist/data/migrations/019_ap_bounty.sql +10 -0
  48. package/dist/data/migrations/020_chat_turns.sql +10 -0
  49. package/dist/data/migrations/021_portrait.sql +4 -0
  50. package/dist/data/migrations/022_family_emoji_single.sql +3 -0
  51. package/dist/data/migrations/023_ap_balance_view_log.sql +9 -0
  52. package/dist/data/schema.sql +75 -0
  53. package/dist/knowledge/_registry/dimensions.yaml +10 -0
  54. package/dist/knowledge/_registry/personas.yaml +7 -0
  55. package/dist/knowledge/_schema/analysis-model.schema.js +32 -0
  56. package/dist/knowledge/_schema/entry.schema.js +45 -0
  57. package/dist/knowledge/_schema/persona.schema.js +23 -0
  58. package/dist/knowledge/_schema/scenario-pack.schema.js +29 -0
  59. package/dist/knowledge/analysis-models/review-default.yaml +16 -0
  60. package/dist/knowledge/analysis-models/review-summer-plan.yaml +15 -0
  61. package/dist/knowledge/brain-science/brain-deep-warmup.yaml +22 -0
  62. package/dist/knowledge/brain-science/brain-emotion-granularity.yaml +22 -0
  63. package/dist/knowledge/brain-science/brain-env-engineering.yaml +22 -0
  64. package/dist/knowledge/brain-science/brain-extended-mind.yaml +22 -0
  65. package/dist/knowledge/brain-science/brain-friction-barrier.yaml +22 -0
  66. package/dist/knowledge/brain-science/brain-peak-end.yaml +29 -0
  67. package/dist/knowledge/brain-science/brain-pomodoro.yaml +31 -0
  68. package/dist/knowledge/brain-science/brain-revenge-bedtime.yaml +22 -0
  69. package/dist/knowledge/brain-science/brain-single-variable.yaml +15 -0
  70. package/dist/knowledge/brain-science/brain-tiny-habits.yaml +22 -0
  71. package/dist/knowledge/energy/energy-active-recovery.yaml +22 -0
  72. package/dist/knowledge/energy/energy-deliberate-rest.yaml +22 -0
  73. package/dist/knowledge/energy/energy-exercise-embedded.yaml +22 -0
  74. package/dist/knowledge/energy/energy-fixed-wake-time.yaml +22 -0
  75. package/dist/knowledge/energy/energy-four-energy-radar.yaml +22 -0
  76. package/dist/knowledge/energy/energy-intensity-recovery.yaml +22 -0
  77. package/dist/knowledge/scenario-packs/pack-summer-kid.yaml +12 -0
  78. package/dist/knowledge/src/repo/fs-yaml.js +252 -0
  79. package/dist/knowledge/src/repo/types.js +1 -0
  80. package/dist/knowledge/time/time-batch-windows.yaml +22 -0
  81. package/dist/knowledge/time/time-conscious-abandon.yaml +22 -0
  82. package/dist/knowledge/time/time-deep-work-block.yaml +22 -0
  83. package/dist/knowledge/time/time-finite-commitment.yaml +22 -0
  84. package/dist/knowledge/time/time-forced-whitespace.yaml +22 -0
  85. package/dist/knowledge/time/time-gtd-next-action.yaml +22 -0
  86. package/dist/knowledge/time/time-three-tier-list.yaml +22 -0
  87. package/dist/mcp/cold-start.js +43 -0
  88. package/dist/mcp/contract/v1/rest.js +56 -0
  89. package/dist/mcp/rest-server.js +52 -0
  90. package/dist/mcp/server-core.js +1334 -0
  91. package/dist/mcp/server.js +34 -0
  92. package/dist/mcp/src/ap/index.js +14 -0
  93. package/dist/mcp/src/ap/rewards.js +44 -0
  94. package/dist/mcp/src/categories/index.js +12 -0
  95. package/dist/mcp/src/chat/from-env.js +111 -0
  96. package/dist/mcp/src/chat/history.js +35 -0
  97. package/dist/mcp/src/chat/loop.js +131 -0
  98. package/dist/mcp/src/chat/portrait.js +133 -0
  99. package/dist/mcp/src/chat/system-prompt.js +23 -0
  100. package/dist/mcp/src/chat/tool-names.js +27 -0
  101. package/dist/mcp/src/chat/tool-schema.js +20 -0
  102. package/dist/mcp/src/chat/types.js +1 -0
  103. package/dist/mcp/src/clock/index.js +78 -0
  104. package/dist/mcp/src/coachmod/index.js +135 -0
  105. package/dist/mcp/src/host-adapters/hermes-cron.js +185 -0
  106. package/dist/mcp/src/host-adapters/index.js +61 -0
  107. package/dist/mcp/src/host-adapters/types.js +7 -0
  108. package/dist/mcp/src/identity/equivalent-ids.js +41 -0
  109. package/dist/mcp/src/identity/in-memory.js +100 -0
  110. package/dist/mcp/src/identity/resolve.js +60 -0
  111. package/dist/mcp/src/identity/types.js +4 -0
  112. package/dist/mcp/src/matrix/index.js +15 -0
  113. package/dist/mcp/src/profile/confirm-default.js +48 -0
  114. package/dist/mcp/src/profile/index.js +298 -0
  115. package/dist/mcp/src/profile-confirm.js +28 -0
  116. package/dist/mcp/src/profile-helpers.js +109 -0
  117. package/dist/mcp/src/reminder/hermes-channel.js +40 -0
  118. package/dist/mcp/src/reminder/index.js +70 -0
  119. package/dist/mcp/src/reminder/push-channel.js +10 -0
  120. package/dist/mcp/src/repo/factory.js +34 -0
  121. package/dist/mcp/src/repo/in-memory.js +469 -0
  122. package/dist/mcp/src/repo/sqlite-ap.js +188 -0
  123. package/dist/mcp/src/repo/sqlite-helpers.js +39 -0
  124. package/dist/mcp/src/repo/sqlite-identity.js +129 -0
  125. package/dist/mcp/src/repo/sqlite.js +754 -0
  126. package/dist/mcp/src/repo/types.js +4 -0
  127. package/dist/mcp/src/rest/app.js +121 -0
  128. package/dist/mcp/src/rest/invoke-tool.js +98 -0
  129. package/dist/mcp/src/rest/short-token.js +38 -0
  130. package/dist/mcp/src/rest/tokens.js +55 -0
  131. package/dist/mcp/src/scheduler/index.js +319 -0
  132. package/dist/mcp/src/stats/index.js +29 -0
  133. package/dist/mcp/src/stickiness/report.js +174 -0
  134. package/dist/mcp/src/stickiness/types.js +1 -0
  135. package/dist/mcp/src/task/index.js +412 -0
  136. package/dist/mcp/src/timeblock/index.js +206 -0
  137. package/dist/mcp/src/timeline/index.js +705 -0
  138. package/dist/mcp/src/timer/index.js +234 -0
  139. package/dist/mcp/src/user-id-resolver.js +30 -0
  140. package/dist/mcp/src/weixin/app.js +97 -0
  141. package/dist/mcp/src/weixin/custom-send.js +82 -0
  142. package/dist/mcp/src/weixin/handle-text.js +49 -0
  143. package/dist/mcp/src/weixin/reminder-loop.js +39 -0
  144. package/dist/mcp/src/weixin/signature.js +13 -0
  145. package/dist/mcp/src/weixin/tencent-asr.js +101 -0
  146. package/dist/mcp/src/weixin/xml.js +37 -0
  147. package/dist/mcp/weixin-server.js +158 -0
  148. package/knowledge/_registry/dimensions.yaml +10 -0
  149. package/knowledge/_registry/personas.yaml +7 -0
  150. package/knowledge/_schema/analysis-model.schema.ts +36 -0
  151. package/knowledge/_schema/entry.schema.ts +50 -0
  152. package/knowledge/_schema/persona.schema.ts +30 -0
  153. package/knowledge/_schema/scenario-pack.schema.ts +33 -0
  154. package/knowledge/analysis-models/review-default.yaml +16 -0
  155. package/knowledge/analysis-models/review-summer-plan.yaml +15 -0
  156. package/knowledge/brain-science/brain-deep-warmup.yaml +22 -0
  157. package/knowledge/brain-science/brain-emotion-granularity.yaml +22 -0
  158. package/knowledge/brain-science/brain-env-engineering.yaml +22 -0
  159. package/knowledge/brain-science/brain-extended-mind.yaml +22 -0
  160. package/knowledge/brain-science/brain-friction-barrier.yaml +22 -0
  161. package/knowledge/brain-science/brain-peak-end.yaml +29 -0
  162. package/knowledge/brain-science/brain-pomodoro.yaml +31 -0
  163. package/knowledge/brain-science/brain-revenge-bedtime.yaml +22 -0
  164. package/knowledge/brain-science/brain-single-variable.yaml +15 -0
  165. package/knowledge/brain-science/brain-tiny-habits.yaml +22 -0
  166. package/knowledge/energy/energy-active-recovery.yaml +22 -0
  167. package/knowledge/energy/energy-deliberate-rest.yaml +22 -0
  168. package/knowledge/energy/energy-exercise-embedded.yaml +22 -0
  169. package/knowledge/energy/energy-fixed-wake-time.yaml +22 -0
  170. package/knowledge/energy/energy-four-energy-radar.yaml +22 -0
  171. package/knowledge/energy/energy-intensity-recovery.yaml +22 -0
  172. package/knowledge/scenario-packs/pack-summer-kid.yaml +12 -0
  173. package/knowledge/src/repo/fs-yaml.ts +282 -0
  174. package/knowledge/src/repo/types.ts +24 -0
  175. package/knowledge/tests/fixtures/base/_registry/dimensions.yaml +4 -0
  176. package/knowledge/tests/fixtures/base/time/valid-entry.yaml +24 -0
  177. package/knowledge/tests/fixtures/dup/_registry/dimensions.yaml +4 -0
  178. package/knowledge/tests/fixtures/dup/time/entry-a.yaml +13 -0
  179. package/knowledge/tests/fixtures/dup/time/entry-b.yaml +13 -0
  180. package/knowledge/tests/fixtures/unregistered/_registry/dimensions.yaml +7 -0
  181. package/knowledge/tests/fixtures/unregistered/time/anxiety-entry.yaml +13 -0
  182. package/knowledge/tests/repo/fs-yaml-knowledge-repo.test.ts +333 -0
  183. package/knowledge/time/time-batch-windows.yaml +22 -0
  184. package/knowledge/time/time-conscious-abandon.yaml +22 -0
  185. package/knowledge/time/time-deep-work-block.yaml +22 -0
  186. package/knowledge/time/time-finite-commitment.yaml +22 -0
  187. package/knowledge/time/time-forced-whitespace.yaml +22 -0
  188. package/knowledge/time/time-gtd-next-action.yaml +22 -0
  189. package/knowledge/time/time-three-tier-list.yaml +22 -0
  190. package/knowledge/validate.ts +50 -0
  191. package/package.json +76 -0
  192. package/scripts/postinstall-cold-start.js +45 -0
@@ -0,0 +1,135 @@
1
+ /** Read profile settings.focus_areas as extra matching tokens (deterministic). */
2
+ function readProfileTokens(db, userId) {
3
+ const row = db
4
+ .prepare(`SELECT settings FROM user_profile WHERE id = ? AND deleted_at IS NULL`)
5
+ .get(userId);
6
+ if (!row?.settings)
7
+ return [];
8
+ try {
9
+ const parsed = JSON.parse(row.settings);
10
+ if (Array.isArray(parsed.focus_areas)) {
11
+ return parsed.focus_areas.filter((x) => typeof x === "string");
12
+ }
13
+ }
14
+ catch {
15
+ // §4.5:settings 非法 JSON 不阻断 suggest(画像线索只是加分项,非必需)——
16
+ // 明确声明此折叠:坏 settings 退化为"无 profile 线索",仍返回默认模型 + 空候选。
17
+ return [];
18
+ }
19
+ return [];
20
+ }
21
+ export function createCoachModTools(knowledgeRepo, db) {
22
+ return {
23
+ /** §4.1 确定性粗筛候选 + 始终附默认基线模型。 */
24
+ async suggest(input) {
25
+ const defaultModel = await knowledgeRepo.getDefaultAnalysisModel();
26
+ const packs = await knowledgeRepo.loadScenarioPacks();
27
+ // 构造匹配 haystack:context_hint + profile focus_areas
28
+ const tokens = [];
29
+ if (input.context_hint)
30
+ tokens.push(input.context_hint);
31
+ if (input.user_id)
32
+ tokens.push(...readProfileTokens(db, input.user_id));
33
+ const haystack = tokens.join(" ");
34
+ const candidates = [];
35
+ for (const pack of packs) {
36
+ const matched = pack.triggers.context_tags.filter((tag) => haystack.includes(tag));
37
+ if (matched.length > 0) {
38
+ candidates.push({
39
+ type: "scenario-pack",
40
+ id: pack.id,
41
+ title: pack.title,
42
+ why_matched: `命中标签[${matched.join(",")}]`,
43
+ });
44
+ }
45
+ }
46
+ const result = {
47
+ default_model: { id: defaultModel.id, title: defaultModel.title },
48
+ candidates,
49
+ };
50
+ if (candidates.length === 0) {
51
+ result.note = "无场景包命中,用默认模型";
52
+ }
53
+ return result;
54
+ },
55
+ /** §4.2 渐进披露:只返回请求的 ids;场景包展开 recommends;坏 id 抛错。 */
56
+ async load(input) {
57
+ if (!input.ids || input.ids.length === 0) {
58
+ throw new Error("ids 不能为空");
59
+ }
60
+ const [models, packs, personas] = await Promise.all([
61
+ knowledgeRepo.loadAnalysisModels(),
62
+ knowledgeRepo.loadScenarioPacks(),
63
+ knowledgeRepo.loadPersonas(),
64
+ ]);
65
+ const modelMap = new Map(models.map((m) => [m.id, m]));
66
+ const packMap = new Map(packs.map((p) => [p.id, p]));
67
+ const personaMap = new Map(personas.map((p) => [p.id, p]));
68
+ const toLoadedModel = (m) => ({
69
+ id: m.id,
70
+ title: m.title,
71
+ description: m.description,
72
+ method: m.method,
73
+ output_shape: m.output_shape,
74
+ });
75
+ const toLoadedPersona = (p) => ({
76
+ id: p.id,
77
+ label: p.label,
78
+ traits: p.traits,
79
+ constraints: p.constraints,
80
+ tone_adjust: p.tone_adjust,
81
+ });
82
+ const loaded = [];
83
+ for (const id of input.ids) {
84
+ const pack = packMap.get(id);
85
+ if (pack) {
86
+ const persona = personaMap.get(pack.recommends.persona_id);
87
+ if (!persona) {
88
+ throw new Error(`场景包 "${id}" 引用的画像 "${pack.recommends.persona_id}" 不存在`);
89
+ }
90
+ const packModels = pack.recommends.analysis_model_ids.map((mid) => {
91
+ const m = modelMap.get(mid);
92
+ if (!m) {
93
+ throw new Error(`场景包 "${id}" 引用的分析模型 "${mid}" 不存在`);
94
+ }
95
+ return toLoadedModel(m);
96
+ });
97
+ loaded.push({
98
+ id: pack.id,
99
+ kind: "scenario-pack",
100
+ title: pack.title,
101
+ content: {
102
+ subskill_prompt: pack.recommends.subskill_prompt,
103
+ persona: toLoadedPersona(persona),
104
+ analysis_models: packModels,
105
+ },
106
+ });
107
+ continue;
108
+ }
109
+ const model = modelMap.get(id);
110
+ if (model) {
111
+ loaded.push({
112
+ id: model.id,
113
+ kind: "analysis-model",
114
+ title: model.title,
115
+ content: toLoadedModel(model),
116
+ });
117
+ continue;
118
+ }
119
+ const persona = personaMap.get(id);
120
+ if (persona) {
121
+ loaded.push({
122
+ id: persona.id,
123
+ kind: "persona",
124
+ title: persona.label,
125
+ content: toLoadedPersona(persona),
126
+ });
127
+ continue;
128
+ }
129
+ // §4.5:不存在的 id 报错,不静默返回空
130
+ throw new Error(`未找到 id "${id}"(不是场景包 / 分析模型 / 画像)`);
131
+ }
132
+ return { loaded };
133
+ },
134
+ };
135
+ }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * MV-COACH-5 §5.2/§5.4 — HermesCronAdapter(hermes 平台适配层)。
3
+ *
4
+ * spike 结论(2026-08-12):libero MCP 是 hermes 的子进程,cronjob 工具是 hermes
5
+ * 给自己 LLM 用的,不反向暴露给子进程。adapter 必须走 spawn CLI:
6
+ * - publish: `hermes cron create <schedule> <prompt> --name ... --deliver origin [--repeat 1]`
7
+ * - cancel: `hermes cron remove <job_id>`
8
+ *
9
+ * 本文件先实装纯函数 translateTriggerToHermesSchedule(核心可测逻辑),
10
+ * publish/cancel/detect 的 spawn 部分在 slice 1 后续补(需带 mock 测试)。
11
+ */
12
+ import { execFileSync } from "node:child_process";
13
+ /**
14
+ * §5.4 trigger → hermes cron schedule 翻译表(纯函数)。
15
+ *
16
+ * | trigger | hermes cron schedule |
17
+ * |---|---|
18
+ * | relative -15m from schedule.start | schedule.start-15min 的 cron(一次性,repeat=1)|
19
+ * | relative +5m from now | "+5m"(hermes 原生相对)|
20
+ * | relative +30m from schedule.start | schedule.start+offset 的 cron |
21
+ * | absolute at=ISO | 锁日期的 cron(一次性)|
22
+ * | recurring cron_expr | 直接用 |
23
+ *
24
+ * §4.5 静默折叠声明:
25
+ * - relative_to=schedule.start 但缺 scheduleStartIso → 抛错(不静默返回空)
26
+ * - offset 格式非法 → 抛错
27
+ * - absolute 过去时间(> 120s 前)→ 抛错(hermes 会拒)
28
+ */
29
+ export function translateTriggerToHermesSchedule(trigger, ctx = {}) {
30
+ switch (trigger.kind) {
31
+ case "relative":
32
+ return translateRelative(trigger, ctx);
33
+ case "absolute":
34
+ return translateAbsolute(trigger);
35
+ case "recurring":
36
+ return { schedule: trigger.cron_expr }; // repeat=undefined = 循环
37
+ }
38
+ }
39
+ /** relative 翻译:算出触发时刻 → 转 cron。 */
40
+ function translateRelative(trigger, ctx) {
41
+ const offsetMs = parseOffset(trigger.offset); // 正负均可
42
+ if (trigger.relative_to === "now") {
43
+ // hermes 原生支持 "+5m" / "+30m" 相对表达式,不用算绝对时间
44
+ // 注意:hermes 的相对格式是 "+5m"(带 + 号),offset 已是这个格式直接透传
45
+ return { schedule: trigger.offset, repeat: 1 };
46
+ }
47
+ // relative_to === "schedule.start"
48
+ if (!ctx.scheduleStartIso) {
49
+ throw new Error("relative_to=schedule.start 需要 scheduleStartIso(从 DB 查的 schedule 开始时间)");
50
+ }
51
+ const startMs = new Date(ctx.scheduleStartIso).getTime();
52
+ if (Number.isNaN(startMs)) {
53
+ throw new Error(`scheduleStartIso 格式非法: ${ctx.scheduleStartIso}`);
54
+ }
55
+ const triggerMs = startMs + offsetMs;
56
+ const triggerDate = new Date(triggerMs);
57
+ // 转本地时区的 cron(分 时 日 月 *,不填 year)
58
+ // 注意:相对 schedule 的具体某天 = 一次性,用 day/month 锁定日期
59
+ const cron = `${triggerDate.getMinutes()} ${triggerDate.getHours()} ${triggerDate.getDate()} ${triggerDate.getMonth() + 1} *`;
60
+ return { schedule: cron, repeat: 1 };
61
+ }
62
+ /** absolute 翻译:ISO → 锁日期的 cron(一次性)。 */
63
+ function translateAbsolute(trigger) {
64
+ const ms = new Date(trigger.at).getTime();
65
+ if (Number.isNaN(ms)) {
66
+ throw new Error(`absolute at 格式非法: ${trigger.at}`);
67
+ }
68
+ // §4.5:hermes 拒绝 > 120s 过去的时间
69
+ const nowMs = Date.now();
70
+ if (ms < nowMs - 120 * 1000) {
71
+ throw new Error(`absolute at 已过去 > 120s(hermes 会拒):${trigger.at}`);
72
+ }
73
+ const d = new Date(ms);
74
+ const cron = `${d.getMinutes()} ${d.getHours()} ${d.getDate()} ${d.getMonth() + 1} *`;
75
+ return { schedule: cron, repeat: 1 };
76
+ }
77
+ /**
78
+ * 解析 offset 字符串 → 毫秒。
79
+ * 格式:[+-]数字+单位(m/h),如 "-15m" / "+5m" / "+2h" / "30m"(无符号 = 正)
80
+ */
81
+ export function parseOffset(offset) {
82
+ const m = offset.match(/^([+-]?)(\d+)([mh])$/i);
83
+ if (!m) {
84
+ throw new Error(`offset 格式非法: "${offset}"(应为 [+-]数字+m/h,如 -15m / +5m / +2h)`);
85
+ }
86
+ const sign = m[1] === "-" ? -1 : 1;
87
+ const n = parseInt(m[2], 10);
88
+ const unitMs = m[3].toLowerCase() === "h" ? 60 * 60 * 1000 : 60 * 1000;
89
+ return sign * n * unitMs;
90
+ }
91
+ /**
92
+ * publish 前解析相对 schedule.start 所需的 ctx(可单测,不 spawn CLI)。
93
+ * §4.5:缺 schedule_id / 缺 db / 行不存在 → 抛错,不静默空。
94
+ */
95
+ export function resolvePublishTranslateContext(intent, db) {
96
+ if (intent.trigger.kind !== "relative" ||
97
+ intent.trigger.relative_to !== "schedule.start") {
98
+ return {};
99
+ }
100
+ if (!intent.schedule_id) {
101
+ throw new Error("relative_to=schedule.start 需要 schedule_id 才能算触发时间");
102
+ }
103
+ if (!db) {
104
+ throw new Error("HermesCronAdapter 未注入 db,无法查 schedule.start_time");
105
+ }
106
+ const row = db
107
+ .prepare("SELECT start_time FROM time_blocks WHERE id=?")
108
+ .get(intent.schedule_id);
109
+ if (!row) {
110
+ throw new Error(`schedule ${intent.schedule_id} 不存在`);
111
+ }
112
+ return { scheduleStartIso: row.start_time };
113
+ }
114
+ // ─────────────────────────────────────────────────────────────
115
+ // HermesCronAdapter(detect + publish + cancel 的 spawn 部分)
116
+ // ─────────────────────────────────────────────────────────────
117
+ /**
118
+ * HermesCronAdapter——hermes 平台的意图适配层。
119
+ *
120
+ * detect 三探(任一命中):
121
+ * ① env LIBERO_HOST === "hermes"(显式指定,最高优先级)
122
+ * ② env HERMES_HOME 存在
123
+ * ③ `hermes cron list` CLI 可调用
124
+ *
125
+ * publish/cancel 走 spawn CLI(spike 结论)。
126
+ */
127
+ export class HermesCronAdapter {
128
+ db;
129
+ kind = "hermes_cron";
130
+ /**
131
+ * @param db 可选——relative_to=schedule.start 时查 schedule.start_time 用。
132
+ * slice 1 单测不传(只测 trigger 纯函数);server-core 实装时传生产 db。
133
+ */
134
+ constructor(db) {
135
+ this.db = db;
136
+ }
137
+ detect() {
138
+ // ① 显式指定
139
+ if (process.env.LIBERO_HOST === "hermes")
140
+ return true;
141
+ // ② HERMES_HOME 存在
142
+ if (process.env.HERMES_HOME)
143
+ return true;
144
+ // ③ CLI 可调用(保守:只在她真的能跑时才认)
145
+ try {
146
+ execFileSync("hermes", ["cron", "list"], {
147
+ stdio: ["ignore", "pipe", "ignore"],
148
+ timeout: 5000,
149
+ });
150
+ return true;
151
+ }
152
+ catch {
153
+ return false;
154
+ }
155
+ }
156
+ async publish(intent) {
157
+ // relative_to=schedule.start 时查 DB 拿 start_time(slice 1 单测只覆盖纯函数,
158
+ // 这里是 publish 时真查 DB——tracer 暴露的必要修正)
159
+ const ctx = resolvePublishTranslateContext(intent, this.db);
160
+ const { schedule, repeat } = translateTriggerToHermesSchedule(intent.trigger, ctx);
161
+ const args = ["cron", "create", schedule, intent.action_prompt];
162
+ args.push("--name", `${intent.intent_type}:${intent.id.slice(0, 8)}`);
163
+ args.push("--deliver", "origin");
164
+ if (repeat !== undefined) {
165
+ args.push("--repeat", String(repeat));
166
+ }
167
+ const stdout = execFileSync("hermes", args, {
168
+ encoding: "utf-8",
169
+ stdio: ["ignore", "pipe", "pipe"],
170
+ timeout: 15000,
171
+ });
172
+ // 解析 "Created job: <job_id>"
173
+ const m = stdout.match(/Created job:\s*([a-f0-9]+)/i);
174
+ if (!m) {
175
+ throw new Error(`hermes cron create 未返回 job_id,stdout: ${stdout}`);
176
+ }
177
+ return { host_job_id: m[1] };
178
+ }
179
+ async cancel(host_job_id) {
180
+ execFileSync("hermes", ["cron", "remove", host_job_id], {
181
+ stdio: ["ignore", "pipe", "pipe"],
182
+ timeout: 10000,
183
+ });
184
+ }
185
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * MV-COACH-5 §5.3 — adapter 注册与选择。
3
+ *
4
+ * MCP 启动时调一次 selectAdapter(),结果缓存。
5
+ * 后续 setIntent / cancelIntent 用这个 active adapter。
6
+ *
7
+ * §4.5 静默折叠声明:
8
+ * - 所有 adapter detect() 都 false → 返回 null(降级:意图只写 DB status=pending,
9
+ * 等用户开口时补推,slice 4 实装)。这是产品意图(无推送平台接受降级),非缺陷。
10
+ */
11
+ import { HermesCronAdapter } from "./hermes-cron.js";
12
+ /** 所有内置 adapter(按 detect 优先级排序)。 */
13
+ export const ADAPTERS = [
14
+ // 注:这里 new 的 adapter 不带 db——relative_to=schedule.start 查不了
15
+ // server-core 实装时用 createAdaptersWithDb(db) 拿带 db 的实例
16
+ new HermesCronAdapter(),
17
+ // slice 2 之后加:new OpenclawAdapter(),
18
+ ];
19
+ /** server-core 实装时用这个——传入 db,adapter 才能查 schedule.start_time。 */
20
+ export function createAdaptersWithDb(db) {
21
+ return [
22
+ new HermesCronAdapter(db),
23
+ // slice 2 之后加:new OpenclawAdapter(),
24
+ ];
25
+ }
26
+ /**
27
+ * 选当前环境能用的 adapter。
28
+ * 1. env LIBERO_HOST 显式指定 → 直接匹配 kind
29
+ * 2. 否则按顺序 detect(),第一个 true 的胜出
30
+ * 3. 都不命中 → null(降级)
31
+ */
32
+ export function selectAdapter(adapters = ADAPTERS) {
33
+ // ① env 显式指定
34
+ const envHint = process.env.LIBERO_HOST;
35
+ if (envHint) {
36
+ const matched = adapters.find((a) => a.kind === envHint);
37
+ if (matched && matched.detect())
38
+ return matched;
39
+ // env 指定了但 detect 不过 → 继续走自动探测(不强行用不工作的 adapter)
40
+ }
41
+ // ② 自动探测
42
+ for (const adapter of adapters) {
43
+ if (adapter.detect())
44
+ return adapter;
45
+ }
46
+ // ③ 都不命中
47
+ return null;
48
+ }
49
+ /** 缓存的 active adapter(MCP 启动时调 selectAdapter 缓存结果)。 */
50
+ let _activeAdapter;
51
+ /** 获取(或首次解析)active adapter。 */
52
+ export function getActiveAdapter() {
53
+ if (_activeAdapter === undefined) {
54
+ _activeAdapter = selectAdapter();
55
+ }
56
+ return _activeAdapter;
57
+ }
58
+ /** 测试用:重置缓存。 */
59
+ export function _resetActiveAdapterForTest() {
60
+ _activeAdapter = undefined;
61
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * MV-COACH-5 §5 — HostAdapter 接口(3F 架构的宿主适配层)。
3
+ *
4
+ * 脑(skill)说意图 → 手(MCP)记意图 + 调 adapter → adapter 翻译成该平台的机制。
5
+ * slice 1 只实装 HermesCronAdapter;后续 slice 加 OpenclawAdapter / cc_pending 降级。
6
+ */
7
+ export {};
@@ -0,0 +1,41 @@
1
+ export function equivalentUserIds(db, input) {
2
+ const ids = new Set([input]);
3
+ if (!input)
4
+ return [...ids];
5
+ const canonicalRow = db
6
+ .prepare(`SELECT libero_user_id AS id FROM user_platform_alias
7
+ WHERE platform_user_id = ? OR chat_id = ?
8
+ UNION
9
+ SELECT libero_user_id AS id FROM user_identity WHERE libero_user_id = ?
10
+ LIMIT 1`)
11
+ .get(input, input, input);
12
+ if (!canonicalRow)
13
+ return [...ids];
14
+ const canonical = canonicalRow.id;
15
+ ids.add(canonical);
16
+ const aliases = db
17
+ .prepare(`SELECT platform_user_id, chat_id FROM user_platform_alias WHERE libero_user_id = ?`)
18
+ .all(canonical);
19
+ for (const a of aliases) {
20
+ ids.add(a.platform_user_id);
21
+ if (a.chat_id)
22
+ ids.add(a.chat_id);
23
+ }
24
+ return [...ids];
25
+ }
26
+ /** 写入用:等价集里已有的 user_profile.id;没有则 null(让上层走确认/原样)。 */
27
+ export function profileIdForWrite(db, input) {
28
+ const uid = userIdIn(db, "id", input);
29
+ const row = db
30
+ .prepare(`SELECT id FROM user_profile WHERE ${uid.clause} AND deleted_at IS NULL LIMIT 1`)
31
+ .get(...uid.params);
32
+ return row?.id ?? null;
33
+ }
34
+ /** `column IN (?,?,…)` + params. Always at least the original id. */
35
+ export function userIdIn(db, column, userId) {
36
+ const ids = equivalentUserIds(db, userId);
37
+ return {
38
+ clause: `${column} IN (${ids.map(() => "?").join(",")})`,
39
+ params: ids,
40
+ };
41
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * In-memory implementation of IdentityRepo — for tests and embedded scenarios.
3
+ *
4
+ * §4.5 静默折叠声明:
5
+ * - 错误(duplicate / FK violation)一律 throw,不静默吞
6
+ * - PK 校验在内存层用 Map.set 覆盖语义,会污染 — 故显式 if-exist check + throw
7
+ */
8
+ export class InMemoryIdentityRepo {
9
+ identities = new Map();
10
+ aliases = new Map(); // key = `${platform}:${platform_user_id}`
11
+ managers = new Map(); // user_id → manager_user_id
12
+ async createIdentity(input) {
13
+ if (this.identities.has(input.libero_user_id)) {
14
+ throw new Error(`identity already exists: libero_user_id=${input.libero_user_id}`);
15
+ }
16
+ const now = new Date().toISOString();
17
+ const row = {
18
+ libero_user_id: input.libero_user_id,
19
+ display_name: input.display_name ?? null,
20
+ created_at: now,
21
+ updated_at: now,
22
+ };
23
+ this.identities.set(input.libero_user_id, row);
24
+ return input.libero_user_id;
25
+ }
26
+ async getIdentity(libero_user_id) {
27
+ return this.identities.get(libero_user_id) ?? null;
28
+ }
29
+ async bindAlias(input) {
30
+ // FK check
31
+ if (!this.identities.has(input.libero_user_id)) {
32
+ throw new Error(`bindAlias FK violation: libero_user_id not found: ${input.libero_user_id}`);
33
+ }
34
+ // PK check
35
+ const key = `${input.platform}:${input.platform_user_id}`;
36
+ if (this.aliases.has(key)) {
37
+ throw new Error(`alias already bound: platform=${input.platform} platform_user_id=${input.platform_user_id}`);
38
+ }
39
+ const row = {
40
+ libero_user_id: input.libero_user_id,
41
+ platform: input.platform,
42
+ platform_user_id: input.platform_user_id,
43
+ chat_id: input.chat_id ?? input.platform_user_id,
44
+ bound_at: new Date().toISOString(),
45
+ };
46
+ this.aliases.set(key, row);
47
+ }
48
+ async listAliases(libero_user_id) {
49
+ const out = [];
50
+ for (const alias of this.aliases.values()) {
51
+ if (alias.libero_user_id === libero_user_id)
52
+ out.push(alias);
53
+ }
54
+ return out;
55
+ }
56
+ async findAliasByPlatformUserId(platform_user_id) {
57
+ for (const alias of this.aliases.values()) {
58
+ if (alias.platform_user_id === platform_user_id)
59
+ return alias;
60
+ }
61
+ return null;
62
+ }
63
+ async listAllIdentities() {
64
+ return Array.from(this.identities.values()).map((i) => ({
65
+ ...i,
66
+ managed_by: this.managers.get(i.libero_user_id) ?? null,
67
+ }));
68
+ }
69
+ async getManager(libero_user_id) {
70
+ const id = this.identities.get(libero_user_id);
71
+ if (!id) {
72
+ throw new Error(`getManager: libero_user_id not found: ${libero_user_id}`);
73
+ }
74
+ const managedBy = this.managers.get(libero_user_id) ?? null;
75
+ const mgr = managedBy ? this.identities.get(managedBy) : null;
76
+ return {
77
+ libero_user_id,
78
+ managed_by: managedBy,
79
+ manager_display_name: mgr?.display_name ?? null,
80
+ };
81
+ }
82
+ async setManager(input) {
83
+ if (!this.identities.has(input.libero_user_id)) {
84
+ throw new Error(`setManager: libero_user_id not found: ${input.libero_user_id}`);
85
+ }
86
+ if (input.managed_by !== null) {
87
+ if (!this.identities.has(input.managed_by)) {
88
+ throw new Error(`setManager: manager libero_user_id not found: ${input.managed_by}`);
89
+ }
90
+ this.managers.set(input.libero_user_id, input.managed_by);
91
+ }
92
+ else {
93
+ this.managers.delete(input.libero_user_id);
94
+ }
95
+ return {
96
+ libero_user_id: input.libero_user_id,
97
+ managed_by: input.managed_by,
98
+ };
99
+ }
100
+ }
@@ -0,0 +1,60 @@
1
+ import { randomUUID } from "node:crypto";
2
+ /**
3
+ * S40 兼容层核心:把任意 user_id 输入(libero_user_id / platform chat_id / 旧数据 chat_id)
4
+ * 解析成稳定的 libero_user_id。
5
+ *
6
+ * 三段查找:
7
+ * 1. user_platform_alias 表:input 是某平台的 platform_user_id?→ 返回 libero_user_id
8
+ * 2. user_identity 表:input 本身就是 libero_user_id?→ 直接返回
9
+ * 3. 都不是 → 视为旧数据 chat_id,自动建对应 identity + alias 兜底行(platform='unknown')
10
+ *
11
+ * §4.5 静默折叠声明:
12
+ * - "自动建 identity" 是显式声明的折叠——意图是兼容旧数据(slice 1 不全量 update 业务表)
13
+ * - 副作用:未知 chat_id 第一次出现会建一个新 libero user,永远不会跟已有用户合并
14
+ * - 主会话需通过 profile.bindAlias 主动声明合并(用户首接触多设备时引导)
15
+ *
16
+ * 幂等:第二次同 chat_id 调用会命中步骤 1,不会重复建 identity。
17
+ */
18
+ export async function resolveUserId(repo, input) {
19
+ if (!input) {
20
+ throw new Error("resolveUserId: input is required");
21
+ }
22
+ // 1. platform_user_id?
23
+ const alias = await repo.findAliasByPlatformUserId(input);
24
+ if (alias)
25
+ return alias.libero_user_id;
26
+ // 2. libero_user_id?
27
+ const identity = await repo.getIdentity(input);
28
+ if (identity)
29
+ return input;
30
+ // 3. legacy chat_id — auto-create
31
+ const newLiberoId = `u_libero_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
32
+ const displayName = input.length <= 16 ? input : input.slice(0, 16);
33
+ await repo.createIdentity({
34
+ libero_user_id: newLiberoId,
35
+ display_name: displayName,
36
+ });
37
+ await repo.bindAlias({
38
+ libero_user_id: newLiberoId,
39
+ platform: detectPlatformFromId(input),
40
+ platform_user_id: input,
41
+ chat_id: input,
42
+ });
43
+ return newLiberoId;
44
+ }
45
+ /**
46
+ * 平台探测(heuristic):
47
+ * - slice 1 不强求精确;返回 'unknown' 是安全默认,主会话首次接触时让用户确认
48
+ * - 未来可扩展为更精确的模式匹配
49
+ */
50
+ export function detectPlatformFromId(id) {
51
+ // wecom chat_id 通常 'wm_' / 'woqoaa' 开头,长度 30-32
52
+ if (/^(wm_|woqoaa|wm_)/.test(id) || (id.length >= 30 && id.length <= 32 && /^[A-Za-z0-9_-]+$/.test(id))) {
53
+ return "wecom";
54
+ }
55
+ // feishu open_id 通常 'ou_' 开头
56
+ if (/^ou_[a-f0-9]{40,}$/i.test(id)) {
57
+ return "feishu";
58
+ }
59
+ return "unknown";
60
+ }
@@ -0,0 +1,4 @@
1
+ // ============================================================
2
+ // S40 cross-platform identity domain types
3
+ // ============================================================
4
+ export {};
@@ -0,0 +1,15 @@
1
+ export function createMatrixTools(repo) {
2
+ return {
3
+ async renderMatrix(filters) {
4
+ if (!filters.user_id) {
5
+ throw new Error("user_id 为必填参数");
6
+ }
7
+ if (filters.from !== undefined &&
8
+ filters.to !== undefined &&
9
+ filters.from > filters.to) {
10
+ throw new Error("开始时间不能晚于结束时间");
11
+ }
12
+ return repo.renderMatrix(filters);
13
+ },
14
+ };
15
+ }
@@ -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
+ }