midou 0.1.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.
- package/LICENSE +21 -0
- package/README.md +241 -0
- package/midou.config.js +76 -0
- package/package.json +44 -0
- package/src/boot.js +145 -0
- package/src/chat.js +187 -0
- package/src/heartbeat.js +119 -0
- package/src/index.js +438 -0
- package/src/init.js +258 -0
- package/src/llm.js +239 -0
- package/src/mcp.js +376 -0
- package/src/memory.js +123 -0
- package/src/mode.js +223 -0
- package/src/scheduler.js +186 -0
- package/src/skills.js +150 -0
- package/src/soul.js +203 -0
- package/src/tools.js +571 -0
package/src/memory.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 记忆系统 — midou 延续自我的方式
|
|
3
|
+
*
|
|
4
|
+
* 两层记忆架构:
|
|
5
|
+
* - 日记 (memory/YYYY-MM-DD.md): 每日对话记录,追加式
|
|
6
|
+
* - 长期记忆 (MEMORY.md): 从日记中提炼的重要信息
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import dayjs from 'dayjs';
|
|
10
|
+
import { readFile, writeFile, appendFile, listDir } from './soul.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 获取今天的日期字符串
|
|
14
|
+
*/
|
|
15
|
+
export function today() {
|
|
16
|
+
return dayjs().format('YYYY-MM-DD');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 获取今日日记的路径
|
|
21
|
+
*/
|
|
22
|
+
export function todayJournalPath() {
|
|
23
|
+
return `memory/${today()}.md`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 写入今日日记(追加)
|
|
28
|
+
*/
|
|
29
|
+
export async function writeJournal(content) {
|
|
30
|
+
const journalPath = todayJournalPath();
|
|
31
|
+
const existing = await readFile(journalPath);
|
|
32
|
+
|
|
33
|
+
if (!existing) {
|
|
34
|
+
// 创建新的日记,带标题
|
|
35
|
+
const header = `# ${today()} 日记\n\n`;
|
|
36
|
+
await writeFile(journalPath, header + content + '\n\n');
|
|
37
|
+
} else {
|
|
38
|
+
await appendFile(journalPath, content + '\n\n');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 记录一次对话到日记
|
|
44
|
+
*/
|
|
45
|
+
export async function logConversation(userMessage, assistantMessage) {
|
|
46
|
+
const time = dayjs().format('HH:mm');
|
|
47
|
+
const entry = `### ${time}\n\n**主人**: ${userMessage}\n\n**midou**: ${assistantMessage}\n`;
|
|
48
|
+
await writeJournal(entry);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 读取最近几天的日记
|
|
53
|
+
*/
|
|
54
|
+
export async function getRecentMemories(days = 2) {
|
|
55
|
+
const memories = [];
|
|
56
|
+
|
|
57
|
+
for (let i = 0; i < days; i++) {
|
|
58
|
+
const date = dayjs().subtract(i, 'day').format('YYYY-MM-DD');
|
|
59
|
+
const journal = await readFile(`memory/${date}.md`);
|
|
60
|
+
if (journal) {
|
|
61
|
+
memories.push(journal);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return memories.join('\n\n---\n\n');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 读取长期记忆
|
|
70
|
+
*/
|
|
71
|
+
export async function getLongTermMemory() {
|
|
72
|
+
return await readFile('MEMORY.md') || '';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 写入长期记忆(追加一条新记忆)
|
|
77
|
+
*/
|
|
78
|
+
export async function addLongTermMemory(content) {
|
|
79
|
+
const timestamp = dayjs().format('YYYY-MM-DD HH:mm');
|
|
80
|
+
const entry = `\n### ${timestamp}\n\n${content}\n`;
|
|
81
|
+
await appendFile('MEMORY.md', entry);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 获取所有日记文件列表
|
|
86
|
+
*/
|
|
87
|
+
export async function listJournals() {
|
|
88
|
+
const files = await listDir('memory');
|
|
89
|
+
return files
|
|
90
|
+
.filter(f => f.endsWith('.md'))
|
|
91
|
+
.sort()
|
|
92
|
+
.reverse();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 会话记忆管理器 — 管理当前会话中的对话历史
|
|
97
|
+
*/
|
|
98
|
+
export class SessionMemory {
|
|
99
|
+
constructor(maxMessages = 50) {
|
|
100
|
+
this.messages = [];
|
|
101
|
+
this.maxMessages = maxMessages;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
add(role, content) {
|
|
105
|
+
this.messages.push({ role, content });
|
|
106
|
+
|
|
107
|
+
// 如果消息过多,保留系统消息和最近的对话
|
|
108
|
+
if (this.messages.length > this.maxMessages) {
|
|
109
|
+
const systemMsg = this.messages.find(m => m.role === 'system');
|
|
110
|
+
const recent = this.messages.slice(-this.maxMessages + 1);
|
|
111
|
+
this.messages = systemMsg ? [systemMsg, ...recent] : recent;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
getMessages() {
|
|
116
|
+
return [...this.messages];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
clear() {
|
|
120
|
+
const systemMsg = this.messages.find(m => m.role === 'system');
|
|
121
|
+
this.messages = systemMsg ? [systemMsg] : [];
|
|
122
|
+
}
|
|
123
|
+
}
|
package/src/mode.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 功耗模式管理器 — midou 的能量调节
|
|
3
|
+
*
|
|
4
|
+
* 三种模式,像猫咪的三种状态:
|
|
5
|
+
*
|
|
6
|
+
* 🐱 eco — 打盹模式:省 token,轻量提示词,核心工具,快速回复
|
|
7
|
+
* 🐱 normal — 日常模式:平衡功耗,标准提示词,全部工具
|
|
8
|
+
* 🐱 full — 全能模式:深度思考,完整上下文,大 token 预算
|
|
9
|
+
*
|
|
10
|
+
* 切换方式:
|
|
11
|
+
* 对话中输入 /mode eco | /mode normal | /mode full
|
|
12
|
+
* 环境变量 MIDOU_MODE=eco
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// ── 模式定义 ──────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
const MODES = {
|
|
18
|
+
eco: {
|
|
19
|
+
name: 'eco',
|
|
20
|
+
label: '🌙 低功耗',
|
|
21
|
+
description: '省 token 模式 — 简洁提示词 + 核心工具 + 短回复',
|
|
22
|
+
maxTokens: 1024,
|
|
23
|
+
temperature: 0.5,
|
|
24
|
+
// 系统提示词策略
|
|
25
|
+
prompt: {
|
|
26
|
+
includeSoul: true, // SOUL.md(总是需要)
|
|
27
|
+
includeIdentity: false, // IDENTITY.md(省略)
|
|
28
|
+
includeUser: true, // USER.md(保留核心关系)
|
|
29
|
+
includeMemory: false, // MEMORY.md(省略长期记忆)
|
|
30
|
+
includeJournals: false, // 日记(省略)
|
|
31
|
+
includeSkills: false, // 技能列表(省略)
|
|
32
|
+
includeMCP: false, // MCP 状态(省略)
|
|
33
|
+
includeReminders: true, // 活跃提醒(保留)
|
|
34
|
+
toolDescStyle: 'minimal', // 工具描述风格
|
|
35
|
+
journalDays: 0, // 加载日记天数
|
|
36
|
+
},
|
|
37
|
+
// 工具策略:只保留核心工具
|
|
38
|
+
coreToolsOnly: true,
|
|
39
|
+
coreTools: [
|
|
40
|
+
'read_file', 'write_file', 'list_dir',
|
|
41
|
+
'write_memory', 'write_journal',
|
|
42
|
+
'set_reminder', 'list_reminders', 'cancel_reminder',
|
|
43
|
+
'run_command', 'read_system_file',
|
|
44
|
+
],
|
|
45
|
+
// 心跳策略
|
|
46
|
+
heartbeat: {
|
|
47
|
+
maxTokens: 256,
|
|
48
|
+
skipIfBusy: true,
|
|
49
|
+
},
|
|
50
|
+
// 日记记录策略
|
|
51
|
+
journal: {
|
|
52
|
+
truncateResponse: 200, // 回复截断长度
|
|
53
|
+
logToolCalls: false, // 不记录工具调用细节
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
normal: {
|
|
58
|
+
name: 'normal',
|
|
59
|
+
label: '☀️ 标准',
|
|
60
|
+
description: '平衡模式 — 完整提示词 + 全部工具',
|
|
61
|
+
maxTokens: 4096,
|
|
62
|
+
temperature: 0.7,
|
|
63
|
+
prompt: {
|
|
64
|
+
includeSoul: true,
|
|
65
|
+
includeIdentity: true,
|
|
66
|
+
includeUser: true,
|
|
67
|
+
includeMemory: true,
|
|
68
|
+
includeJournals: true,
|
|
69
|
+
includeSkills: true,
|
|
70
|
+
includeMCP: true,
|
|
71
|
+
includeReminders: true,
|
|
72
|
+
toolDescStyle: 'normal',
|
|
73
|
+
journalDays: 2,
|
|
74
|
+
},
|
|
75
|
+
coreToolsOnly: false,
|
|
76
|
+
coreTools: [],
|
|
77
|
+
heartbeat: {
|
|
78
|
+
maxTokens: 512,
|
|
79
|
+
skipIfBusy: false,
|
|
80
|
+
},
|
|
81
|
+
journal: {
|
|
82
|
+
truncateResponse: 500,
|
|
83
|
+
logToolCalls: true,
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
full: {
|
|
88
|
+
name: 'full',
|
|
89
|
+
label: '🔥 全能',
|
|
90
|
+
description: '全能模式 — 深度上下文 + 大 token 预算 + 完整日记',
|
|
91
|
+
maxTokens: 8192,
|
|
92
|
+
temperature: 0.8,
|
|
93
|
+
prompt: {
|
|
94
|
+
includeSoul: true,
|
|
95
|
+
includeIdentity: true,
|
|
96
|
+
includeUser: true,
|
|
97
|
+
includeMemory: true,
|
|
98
|
+
includeJournals: true,
|
|
99
|
+
includeSkills: true,
|
|
100
|
+
includeMCP: true,
|
|
101
|
+
includeReminders: true,
|
|
102
|
+
toolDescStyle: 'detailed',
|
|
103
|
+
journalDays: 5, // 加载更多天的日记
|
|
104
|
+
},
|
|
105
|
+
coreToolsOnly: false,
|
|
106
|
+
coreTools: [],
|
|
107
|
+
heartbeat: {
|
|
108
|
+
maxTokens: 1024,
|
|
109
|
+
skipIfBusy: false,
|
|
110
|
+
},
|
|
111
|
+
journal: {
|
|
112
|
+
truncateResponse: 0, // 不截断
|
|
113
|
+
logToolCalls: true,
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// ── 当前模式 ──────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
let currentMode = null;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 初始化模式(从环境变量或默认 normal)
|
|
124
|
+
*/
|
|
125
|
+
export function initMode(modeName) {
|
|
126
|
+
const name = modeName || process.env.MIDOU_MODE || 'normal';
|
|
127
|
+
if (!MODES[name]) {
|
|
128
|
+
console.warn(`未知模式 "${name}",使用 normal`);
|
|
129
|
+
currentMode = MODES.normal;
|
|
130
|
+
} else {
|
|
131
|
+
currentMode = MODES[name];
|
|
132
|
+
}
|
|
133
|
+
return currentMode;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 获取当前模式
|
|
138
|
+
*/
|
|
139
|
+
export function getMode() {
|
|
140
|
+
if (!currentMode) initMode();
|
|
141
|
+
return currentMode;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 切换模式
|
|
146
|
+
*/
|
|
147
|
+
export function setMode(modeName) {
|
|
148
|
+
if (!MODES[modeName]) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
currentMode = MODES[modeName];
|
|
152
|
+
return currentMode;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 列出所有模式
|
|
157
|
+
*/
|
|
158
|
+
export function listModes() {
|
|
159
|
+
return Object.values(MODES).map(m => ({
|
|
160
|
+
name: m.name,
|
|
161
|
+
label: m.label,
|
|
162
|
+
description: m.description,
|
|
163
|
+
maxTokens: m.maxTokens,
|
|
164
|
+
temperature: m.temperature,
|
|
165
|
+
active: m === currentMode,
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 获取当前模式的 maxTokens
|
|
171
|
+
*/
|
|
172
|
+
export function getModeMaxTokens() {
|
|
173
|
+
return getMode().maxTokens;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* 获取当前模式的 temperature
|
|
178
|
+
*/
|
|
179
|
+
export function getModeTemperature() {
|
|
180
|
+
return getMode().temperature;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 获取模式下的提示词策略
|
|
185
|
+
*/
|
|
186
|
+
export function getPromptStrategy() {
|
|
187
|
+
return getMode().prompt;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 获取模式下要使用的工具列表
|
|
192
|
+
* @param {Array} allTools - 完整工具定义列表
|
|
193
|
+
*/
|
|
194
|
+
export function filterToolsByMode(allTools) {
|
|
195
|
+
const mode = getMode();
|
|
196
|
+
if (!mode.coreToolsOnly) return allTools;
|
|
197
|
+
|
|
198
|
+
return allTools.filter(t => {
|
|
199
|
+
const name = t.function?.name || t._mcpToolName;
|
|
200
|
+
return mode.coreTools.includes(name);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* 获取心跳参数
|
|
206
|
+
*/
|
|
207
|
+
export function getHeartbeatParams() {
|
|
208
|
+
return getMode().heartbeat;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 获取日记策略
|
|
213
|
+
*/
|
|
214
|
+
export function getJournalStrategy() {
|
|
215
|
+
return getMode().journal;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* 检测是否是核心工具模式
|
|
220
|
+
*/
|
|
221
|
+
export function isCoreToolsOnly() {
|
|
222
|
+
return getMode().coreToolsOnly;
|
|
223
|
+
}
|
package/src/scheduler.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 定时任务系统 — midou 的闹钟
|
|
3
|
+
*
|
|
4
|
+
* 让 midou 可以设定提醒和定时任务,
|
|
5
|
+
* 就像猫咪的生物钟一样精准。
|
|
6
|
+
*
|
|
7
|
+
* 提醒存储在 ~/.midou/reminders.json
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'fs/promises';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import dayjs from 'dayjs';
|
|
13
|
+
import config from '../midou.config.js';
|
|
14
|
+
|
|
15
|
+
const REMINDERS_FILE = path.join(config.workspace.root, 'reminders.json');
|
|
16
|
+
|
|
17
|
+
let reminders = [];
|
|
18
|
+
let schedulerTimer = null;
|
|
19
|
+
let nextId = 1;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 加载提醒列表
|
|
23
|
+
*/
|
|
24
|
+
async function loadReminders() {
|
|
25
|
+
try {
|
|
26
|
+
const data = await fs.readFile(REMINDERS_FILE, 'utf-8');
|
|
27
|
+
const parsed = JSON.parse(data);
|
|
28
|
+
reminders = parsed.reminders || [];
|
|
29
|
+
nextId = parsed.nextId || (reminders.length > 0 ? Math.max(...reminders.map(r => r.id)) + 1 : 1);
|
|
30
|
+
} catch {
|
|
31
|
+
reminders = [];
|
|
32
|
+
nextId = 1;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 持久化提醒列表
|
|
38
|
+
*/
|
|
39
|
+
async function saveReminders() {
|
|
40
|
+
await fs.writeFile(REMINDERS_FILE, JSON.stringify({ reminders, nextId }, null, 2), 'utf-8');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 添加提醒
|
|
45
|
+
* @param {string} text - 提醒内容
|
|
46
|
+
* @param {number} intervalMinutes - 间隔(分钟)
|
|
47
|
+
* @param {boolean} repeat - 是否重复
|
|
48
|
+
* @param {string} [triggerAt] - 指定触发时间 (ISO 字符串),如果设置则 intervalMinutes 被忽略
|
|
49
|
+
* @returns {object} 创建的提醒对象
|
|
50
|
+
*/
|
|
51
|
+
export async function addReminder(text, intervalMinutes, repeat = false, triggerAt = null) {
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
const reminder = {
|
|
54
|
+
id: nextId++,
|
|
55
|
+
text,
|
|
56
|
+
intervalMinutes,
|
|
57
|
+
repeat,
|
|
58
|
+
createdAt: new Date(now).toISOString(),
|
|
59
|
+
nextTrigger: triggerAt || new Date(now + intervalMinutes * 60 * 1000).toISOString(),
|
|
60
|
+
firedCount: 0,
|
|
61
|
+
active: true,
|
|
62
|
+
};
|
|
63
|
+
reminders.push(reminder);
|
|
64
|
+
await saveReminders();
|
|
65
|
+
return reminder;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 移除提醒
|
|
70
|
+
*/
|
|
71
|
+
export async function removeReminder(id) {
|
|
72
|
+
const idx = reminders.findIndex(r => r.id === id);
|
|
73
|
+
if (idx === -1) return false;
|
|
74
|
+
reminders.splice(idx, 1);
|
|
75
|
+
await saveReminders();
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 暂停/恢复提醒
|
|
81
|
+
*/
|
|
82
|
+
export async function toggleReminder(id) {
|
|
83
|
+
const reminder = reminders.find(r => r.id === id);
|
|
84
|
+
if (!reminder) return null;
|
|
85
|
+
reminder.active = !reminder.active;
|
|
86
|
+
await saveReminders();
|
|
87
|
+
return reminder;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 列出所有提醒
|
|
92
|
+
*/
|
|
93
|
+
export function listReminders() {
|
|
94
|
+
return reminders.map(r => ({
|
|
95
|
+
id: r.id,
|
|
96
|
+
text: r.text,
|
|
97
|
+
intervalMinutes: r.intervalMinutes,
|
|
98
|
+
repeat: r.repeat,
|
|
99
|
+
active: r.active,
|
|
100
|
+
nextTrigger: r.nextTrigger,
|
|
101
|
+
firedCount: r.firedCount,
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 检查并触发到期的提醒
|
|
107
|
+
* @param {function} onFire - 触发时的回调 (reminder) => void
|
|
108
|
+
*/
|
|
109
|
+
async function checkReminders(onFire) {
|
|
110
|
+
const now = Date.now();
|
|
111
|
+
let changed = false;
|
|
112
|
+
|
|
113
|
+
for (const reminder of reminders) {
|
|
114
|
+
if (!reminder.active) continue;
|
|
115
|
+
|
|
116
|
+
const triggerTime = new Date(reminder.nextTrigger).getTime();
|
|
117
|
+
if (now >= triggerTime) {
|
|
118
|
+
reminder.firedCount++;
|
|
119
|
+
|
|
120
|
+
// 通知
|
|
121
|
+
if (onFire) {
|
|
122
|
+
onFire(reminder);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (reminder.repeat) {
|
|
126
|
+
// 重复提醒:设置下一次触发时间
|
|
127
|
+
reminder.nextTrigger = new Date(now + reminder.intervalMinutes * 60 * 1000).toISOString();
|
|
128
|
+
} else {
|
|
129
|
+
// 一次性提醒:标记为非活跃
|
|
130
|
+
reminder.active = false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
changed = true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (changed) {
|
|
138
|
+
// 清除已完成的非重复提醒(保留记录最多 50 条)
|
|
139
|
+
const inactive = reminders.filter(r => !r.active);
|
|
140
|
+
if (inactive.length > 50) {
|
|
141
|
+
reminders = [
|
|
142
|
+
...reminders.filter(r => r.active),
|
|
143
|
+
...inactive.slice(-50),
|
|
144
|
+
];
|
|
145
|
+
}
|
|
146
|
+
await saveReminders();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 启动调度器(每 30 秒检查一次)
|
|
152
|
+
*/
|
|
153
|
+
export async function startScheduler(onFire) {
|
|
154
|
+
await loadReminders();
|
|
155
|
+
|
|
156
|
+
// 每 30 秒检查一次提醒
|
|
157
|
+
schedulerTimer = setInterval(() => checkReminders(onFire), 30 * 1000);
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
stop: stopScheduler,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 停止调度器
|
|
166
|
+
*/
|
|
167
|
+
export function stopScheduler() {
|
|
168
|
+
if (schedulerTimer) {
|
|
169
|
+
clearInterval(schedulerTimer);
|
|
170
|
+
schedulerTimer = null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 格式化提醒列表为可读字符串
|
|
176
|
+
*/
|
|
177
|
+
export function formatReminders() {
|
|
178
|
+
const active = reminders.filter(r => r.active);
|
|
179
|
+
if (active.length === 0) return '当前没有活跃的提醒';
|
|
180
|
+
|
|
181
|
+
return active.map(r => {
|
|
182
|
+
const next = dayjs(r.nextTrigger).format('HH:mm:ss');
|
|
183
|
+
const type = r.repeat ? `每 ${r.intervalMinutes} 分钟` : '一次性';
|
|
184
|
+
return `[${r.id}] ${r.text} — ${type},下次: ${next}`;
|
|
185
|
+
}).join('\n');
|
|
186
|
+
}
|
package/src/skills.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 技能系统 — midou 的学习能力
|
|
3
|
+
*
|
|
4
|
+
* 扫描 .claude/skills/ 和 ~/.midou/skills/ 目录,
|
|
5
|
+
* 发现并加载技能描述,让 midou 知道自己有哪些扩展能力。
|
|
6
|
+
*
|
|
7
|
+
* 技能通过 SKILL.md 文件定义,包含:
|
|
8
|
+
* - 技能名称
|
|
9
|
+
* - 技能描述
|
|
10
|
+
* - 使用场景
|
|
11
|
+
* - 详细指令(按需加载)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from 'fs/promises';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import os from 'os';
|
|
17
|
+
import config from '../midou.config.js';
|
|
18
|
+
|
|
19
|
+
// 技能搜索路径
|
|
20
|
+
const SKILL_SEARCH_PATHS = [
|
|
21
|
+
path.join(os.homedir(), '.claude', 'skills'), // Claude 官方技能
|
|
22
|
+
path.join(os.homedir(), '.agents', 'skills'), // agents 技能
|
|
23
|
+
path.join(config.workspace.root, 'skills'), // midou 自定义技能
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 技能描述缓存
|
|
28
|
+
*/
|
|
29
|
+
let skillsCache = null;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 发现所有可用技能
|
|
33
|
+
* @returns {Array<{name, description, path, source}>}
|
|
34
|
+
*/
|
|
35
|
+
export async function discoverSkills() {
|
|
36
|
+
if (skillsCache) return skillsCache;
|
|
37
|
+
|
|
38
|
+
const skills = [];
|
|
39
|
+
|
|
40
|
+
for (const searchPath of SKILL_SEARCH_PATHS) {
|
|
41
|
+
try {
|
|
42
|
+
const entries = await fs.readdir(searchPath, { withFileTypes: true });
|
|
43
|
+
const source = searchPath.includes('.claude') ? 'claude'
|
|
44
|
+
: searchPath.includes('.agents') ? 'agents'
|
|
45
|
+
: 'midou';
|
|
46
|
+
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
if (!entry.isDirectory()) continue;
|
|
49
|
+
|
|
50
|
+
const skillMdPath = path.join(searchPath, entry.name, 'SKILL.md');
|
|
51
|
+
try {
|
|
52
|
+
await fs.access(skillMdPath);
|
|
53
|
+
|
|
54
|
+
// 读取 SKILL.md 获取描述
|
|
55
|
+
const content = await fs.readFile(skillMdPath, 'utf-8');
|
|
56
|
+
const description = extractDescription(content);
|
|
57
|
+
|
|
58
|
+
skills.push({
|
|
59
|
+
name: entry.name,
|
|
60
|
+
description,
|
|
61
|
+
path: skillMdPath,
|
|
62
|
+
source,
|
|
63
|
+
});
|
|
64
|
+
} catch {
|
|
65
|
+
// SKILL.md 不存在,跳过
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
// 目录不存在,跳过
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
skillsCache = skills;
|
|
74
|
+
return skills;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 从 SKILL.md 内容中提取简短描述
|
|
79
|
+
* 通常是 <description> 标签内的内容,或者第一段文字
|
|
80
|
+
*/
|
|
81
|
+
function extractDescription(content) {
|
|
82
|
+
// 尝试提取 <description> 标签
|
|
83
|
+
const descMatch = content.match(/<description>([\s\S]*?)<\/description>/);
|
|
84
|
+
if (descMatch) {
|
|
85
|
+
return descMatch[1].trim();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 尝试提取 description 字段(YAML 风格)
|
|
89
|
+
const yamlMatch = content.match(/description:\s*(.+)/i);
|
|
90
|
+
if (yamlMatch) {
|
|
91
|
+
return yamlMatch[1].trim();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 回退:取第一段非标题文字
|
|
95
|
+
const lines = content.split('\n');
|
|
96
|
+
for (const line of lines) {
|
|
97
|
+
const trimmed = line.trim();
|
|
98
|
+
if (trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('<') && !trimmed.startsWith('---')) {
|
|
99
|
+
return trimmed.slice(0, 200);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return '(无描述)';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 读取技能的完整指令
|
|
108
|
+
*/
|
|
109
|
+
export async function loadSkillContent(skillName) {
|
|
110
|
+
const skills = await discoverSkills();
|
|
111
|
+
const skill = skills.find(s => s.name === skillName);
|
|
112
|
+
if (!skill) return null;
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
return await fs.readFile(skill.path, 'utf-8');
|
|
116
|
+
} catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 构建技能部分的系统提示词
|
|
123
|
+
*/
|
|
124
|
+
export async function buildSkillsPrompt() {
|
|
125
|
+
const skills = await discoverSkills();
|
|
126
|
+
if (skills.length === 0) return '';
|
|
127
|
+
|
|
128
|
+
const lines = ['你拥有以下技能,可以在需要时使用 `load_skill` 工具加载详细指令:\n'];
|
|
129
|
+
|
|
130
|
+
for (const skill of skills) {
|
|
131
|
+
lines.push(`- **${skill.name}** (${skill.source}): ${skill.description}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return lines.join('\n');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 清除技能缓存(需要重新扫描时)
|
|
139
|
+
*/
|
|
140
|
+
export function clearSkillsCache() {
|
|
141
|
+
skillsCache = null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 列出技能(用于工具调用)
|
|
146
|
+
*/
|
|
147
|
+
export async function listSkillNames() {
|
|
148
|
+
const skills = await discoverSkills();
|
|
149
|
+
return skills.map(s => `${s.name} (${s.source}): ${s.description}`);
|
|
150
|
+
}
|