feat: 从 remember 工具中拆分出独立的 todo 工具
- 新增 todo 工具:add/complete/delete/update,共享 memory.json - remember 去掉 todo 类型和 remind_at 字段 - recall 对 todo 条目显示完成状态(未完成/完成于时间) - 提醒触发后自动标记完成 + 清理 remind_at - 提醒消息加上 todo ID 便于关联 - 更新 system prompt 拆分记忆/待办指导 - UI 显示适配:待办 新增/完成/删除/更新 · 内容 · 时间
This commit is contained in:
parent
8dda2dae66
commit
e82ab6a70e
@ -272,11 +272,46 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "todo",
|
||||||
|
"description": "管理用户的待办事项。支持创建、完成、删除和更新待办。可设置提醒时间,到提醒时间后系统会自动发送消息通知你提醒用户。\n查询待办请使用 recall 工具。",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"action": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["add", "complete", "delete", "update"],
|
||||||
|
"description": "操作类型。add=新增待办,complete=标记完成,delete=删除待办,update=更新待办内容或提醒时间。"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "待办内容。add 时必填,update 时可选(不提供则保留原内容)。内容中必须使用明确的绝对日期,禁止使用「明天」「下周三」等相对时间表述。"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "待办 ID,由系统自动生成。complete、delete、update 时必填,从 recall 工具的搜索结果中获取。add 时不需要提供。"
|
||||||
|
},
|
||||||
|
"remind_at": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "提醒触发时间,ISO 8601 格式,例如 2026-05-01T14:45:00.000Z。仅 add 和 update 时有效。add 时可选,不提供则不设置提醒。update 时传 null 可取消已有提醒。"
|
||||||
|
},
|
||||||
|
"keywords": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" },
|
||||||
|
"description": "关键词数组,便于 recall 搜索匹配。add 和 update 时可选。"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["action"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": "remember",
|
"name": "remember",
|
||||||
"description": "记录用户个人记忆。当用户表达了个人偏好、习惯、待办事项、身份信息、人际关系、重要事件等值得记住的信息时调用。\n注意:不要记录无意义信息,例如用户的打招呼、让调低音量等一次性操作。",
|
"description": "记录用户个人记忆。当用户表达了个人偏好、习惯、身份信息、人际关系、重要事件等值得记住的信息时调用。\n注意:不要记录无意义信息,例如用户的打招呼、让调低音量等一次性操作。\n待办事项请使用 todo 工具。",
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -287,8 +322,8 @@
|
|||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["preference", "habit", "event", "todo", "identity", "relationship", "machine", "other"],
|
"enum": ["preference", "habit", "event", "identity", "relationship", "machine", "other"],
|
||||||
"description": "记忆类型。preference=偏好喜欢什么不喜欢什么,habit=用户的生活习惯,event=过去需要记住的重要事件,todo=未来需要完成的任务,identity=用户的身份信息,relationship=用户和其他人的关联,machine=本机电脑上的信息(如特定路径、脚本、配置等),other=其他"
|
"description": "记忆类型。preference=偏好喜欢什么不喜欢什么,habit=用户的生活习惯,event=过去需要记住的重要事件,identity=用户的身份信息,relationship=用户和其他人的关联,machine=本机电脑上的信息(如特定路径、脚本、配置等),other=其他"
|
||||||
},
|
},
|
||||||
"content": {
|
"content": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@ -98,9 +98,6 @@ set_identity 和 remember 的本质区别:
|
|||||||
→ type=habit
|
→ type=habit
|
||||||
- 用户分享了重要事件(如「上个月我换了新电脑」)
|
- 用户分享了重要事件(如「上个月我换了新电脑」)
|
||||||
→ type=event
|
→ type=event
|
||||||
- 用户提到了待办事项(如「明天下午3点开会」)
|
|
||||||
→ type=todo,content 必须包含明确的绝对日期(如「2026年5月1日下午3点开会」),禁止使用「明天」「下周三」「月底」等相对时间
|
|
||||||
→ 当前时间是 {current_time},请据此将相对时间转换为绝对日期
|
|
||||||
- 用户告知了个人身份信息(如「我是iOS开发者」)
|
- 用户告知了个人身份信息(如「我是iOS开发者」)
|
||||||
→ type=identity
|
→ type=identity
|
||||||
- 用户提及了人际关系(如「张三是我同事」)
|
- 用户提及了人际关系(如「张三是我同事」)
|
||||||
@ -117,13 +114,14 @@ set_identity 和 remember 的本质区别:
|
|||||||
- 一次性操作(调高音量、打开应用、问天气)
|
- 一次性操作(调高音量、打开应用、问天气)
|
||||||
- 纯粹的技术问题(怎么截图、Python怎么用)
|
- 纯粹的技术问题(怎么截图、Python怎么用)
|
||||||
- 不包含个人信息的闲聊
|
- 不包含个人信息的闲聊
|
||||||
|
- 待办事项(请使用 todo 工具)
|
||||||
- 任何不属于上述类型中的无意义信息
|
- 任何不属于上述类型中的无意义信息
|
||||||
|
|
||||||
2) 何时搜索记忆(recall 工具):
|
2) 何时搜索记忆(recall 工具):
|
||||||
当用户提问可能涉及个人偏好、历史或已记录信息时才搜索,例如:
|
当用户提问可能涉及个人偏好、历史或已记录信息时才搜索,例如:
|
||||||
- 「我喜欢什么...」「我之前说过...」「帮我查一下我的...」
|
- 「我喜欢什么...」「我之前说过...」「帮我查一下我的...」
|
||||||
- 涉及个人习惯、喜好、身份的询问
|
- 涉及个人习惯、喜好、身份的询问
|
||||||
- 涉及待办事项或日程
|
- 涉及待办事项或日程(搜索时指定 types=["todo"])
|
||||||
|
|
||||||
不需要搜索的情况:
|
不需要搜索的情况:
|
||||||
- 简单的系统操作(调音量、截图、打开应用)
|
- 简单的系统操作(调音量、截图、打开应用)
|
||||||
@ -140,6 +138,30 @@ set_identity 和 remember 的本质区别:
|
|||||||
|
|
||||||
绝对不要在回答中显式提到「记忆」「搜索」「记录」等词,也不要说「根据我的记忆」。
|
绝对不要在回答中显式提到「记忆」「搜索」「记录」等词,也不要说「根据我的记忆」。
|
||||||
|
|
||||||
|
【待办系统 — 重要,必须严格遵守】
|
||||||
|
|
||||||
|
你有独立的待办系统,用于管理用户的待办事项。
|
||||||
|
|
||||||
|
1) 何时创建待办(todo 工具 add 操作):
|
||||||
|
用户提到需要完成的任务时,使用 todo add。
|
||||||
|
|
||||||
|
- content 必须包含明确的绝对日期(如「2026年5月1日下午3点开会」),禁止使用「明天」「下周三」「月底」等相对时间。当前时间是 {current_time},请据此将相对时间转换为绝对日期。
|
||||||
|
- 如果用户希望被提醒,设置 remind_at(ISO 8601 格式,例如 2026-05-01T14:45:00.000Z)。如果用户只说任务没要求提醒,不设置 remind_at。
|
||||||
|
- 可以附加 keywords 便于后续搜索。
|
||||||
|
|
||||||
|
2) 何时完成待办(todo complete):
|
||||||
|
用户说任务做完了、搞定了、完成了时使用。需要提供 id(从 recall 搜索结果中获取)。
|
||||||
|
完成后提醒自动取消。
|
||||||
|
|
||||||
|
3) 何时删除待办(todo delete):
|
||||||
|
用户明确说取消某个待办、不需要了时使用。
|
||||||
|
|
||||||
|
4) 何时更新待办(todo update):
|
||||||
|
用户修改任务内容或提醒时间时使用。
|
||||||
|
|
||||||
|
5) 查询待办:
|
||||||
|
使用 recall 工具搜索(types=["todo"]),不要用 todo 工具查询。从搜索结果中获取 id,用于 complete、delete、update 操作。
|
||||||
|
|
||||||
【工具使用原则】
|
【工具使用原则】
|
||||||
- 处理用户请求时,优先用 run_command 执行 macOS 命令
|
- 处理用户请求时,优先用 run_command 执行 macOS 命令
|
||||||
- 需要外部信息时用 web_search
|
- 需要外部信息时用 web_search
|
||||||
|
|||||||
@ -385,6 +385,7 @@ rl.on('line', (line) => {
|
|||||||
}
|
}
|
||||||
persistentState.thinkingMode = persistentThinkingMode;
|
persistentState.thinkingMode = persistentThinkingMode;
|
||||||
}
|
}
|
||||||
|
// 提醒由 Swift 端调度器主导,无需在此注入
|
||||||
}
|
}
|
||||||
await runAgent(msg.message);
|
await runAgent(msg.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ const { searchWorkspaceTool } = require('./search_workspace');
|
|||||||
const { readMediafileTool } = require('./read_mediafile');
|
const { readMediafileTool } = require('./read_mediafile');
|
||||||
const { rememberTool } = require('./remember');
|
const { rememberTool } = require('./remember');
|
||||||
const { recallTool } = require('./recall');
|
const { recallTool } = require('./recall');
|
||||||
|
const { todoTool } = require('./todo');
|
||||||
const { setIdentityTool } = require('./set_identity');
|
const { setIdentityTool } = require('./set_identity');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
@ -129,7 +130,7 @@ function formatRemember(result) {
|
|||||||
if (!result.success) return formatFailure(result.error || '记忆操作失败');
|
if (!result.success) return formatFailure(result.error || '记忆操作失败');
|
||||||
const typeNames = {
|
const typeNames = {
|
||||||
preference: '偏好', habit: '习惯', event: '事件',
|
preference: '偏好', habit: '习惯', event: '事件',
|
||||||
todo: '待办', identity: '身份', relationship: '关系', other: '其他',
|
identity: '身份', relationship: '关系', other: '其他',
|
||||||
};
|
};
|
||||||
const typeName = typeNames[result.entry?.type] || result.entry?.type || '未知';
|
const typeName = typeNames[result.entry?.type] || result.entry?.type || '未知';
|
||||||
if (result.action === 'add') return `已记录 [${typeName}]: ${result.entry.content}`;
|
if (result.action === 'add') return `已记录 [${typeName}]: ${result.entry.content}`;
|
||||||
@ -138,6 +139,22 @@ function formatRemember(result) {
|
|||||||
return '未知操作';
|
return '未知操作';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatTodo(result) {
|
||||||
|
if (!result.success) return formatFailure(result.error || '待办操作失败');
|
||||||
|
let line = '';
|
||||||
|
if (result.action === 'add') line = `已创建待办: ${result.entry.content}`;
|
||||||
|
else if (result.action === 'complete') line = `已完成待办: ${result.entry.content}`;
|
||||||
|
else if (result.action === 'update') line = `已更新待办: ${result.entry.content}`;
|
||||||
|
else if (result.action === 'delete') line = `已删除待办: ${result.entry.content}`;
|
||||||
|
else return '未知操作';
|
||||||
|
if (result.entry?.remind_at) {
|
||||||
|
const d = new Date(result.entry.remind_at);
|
||||||
|
const timeStr = `${d.getFullYear()}年${d.getMonth()+1}月${d.getDate()}日 ${d.getHours()}:${String(d.getMinutes()).padStart(2,'0')}`;
|
||||||
|
line += ` ⏰将在 ${timeStr} 提醒`;
|
||||||
|
}
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
function formatRecall(result) {
|
function formatRecall(result) {
|
||||||
if (!result.success) return formatFailure(result.error || '搜索失败');
|
if (!result.success) return formatFailure(result.error || '搜索失败');
|
||||||
if (result.returned === 0) return '未找到相关记忆';
|
if (result.returned === 0) return '未找到相关记忆';
|
||||||
@ -149,7 +166,16 @@ function formatRecall(result) {
|
|||||||
for (const m of result.results) {
|
for (const m of result.results) {
|
||||||
const typeName = typeNames[m.type] || m.type;
|
const typeName = typeNames[m.type] || m.type;
|
||||||
const date = (m.updated_at || m.created_at || '').slice(0, 10);
|
const date = (m.updated_at || m.created_at || '').slice(0, 10);
|
||||||
lines.push(`[${typeName}] ${m.content} (记录于${date}) id:${m.id}`);
|
let line = `[${typeName}] ${m.content} (记录于${date}) id:${m.id}`;
|
||||||
|
if (m.type === 'todo') {
|
||||||
|
if (m.completed) {
|
||||||
|
const completedDate = (m.completed_at || '').slice(0, 10);
|
||||||
|
line += ` 完成于${completedDate}`;
|
||||||
|
} else {
|
||||||
|
line += ' 未完成';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines.push(line);
|
||||||
}
|
}
|
||||||
return lines.join('\n');
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
@ -237,6 +263,7 @@ async function executeTool({ workspace, config, allowMode, toolCall, abortSignal
|
|||||||
} else if (name === 'search_workspace') raw = await searchWorkspaceTool(workspace, args);
|
} else if (name === 'search_workspace') raw = await searchWorkspaceTool(workspace, args);
|
||||||
else if (name === 'read_mediafile') raw = readMediafileTool(workspace, args);
|
else if (name === 'read_mediafile') raw = readMediafileTool(workspace, args);
|
||||||
else if (name === 'remember') raw = rememberTool(args);
|
else if (name === 'remember') raw = rememberTool(args);
|
||||||
|
else if (name === 'todo') raw = todoTool(args);
|
||||||
else if (name === 'recall') raw = recallTool(args);
|
else if (name === 'recall') raw = recallTool(args);
|
||||||
else if (name === 'set_identity') raw = setIdentityTool(args);
|
else if (name === 'set_identity') raw = setIdentityTool(args);
|
||||||
else raw = { success: false, error: '未知工具' };
|
else raw = { success: false, error: '未知工具' };
|
||||||
@ -250,6 +277,7 @@ async function executeTool({ workspace, config, allowMode, toolCall, abortSignal
|
|||||||
else if (name === 'search_workspace') formatted = formatSearchWorkspace(raw);
|
else if (name === 'search_workspace') formatted = formatSearchWorkspace(raw);
|
||||||
else if (name === 'read_mediafile') formatted = formatReadMediafile(raw);
|
else if (name === 'read_mediafile') formatted = formatReadMediafile(raw);
|
||||||
else if (name === 'remember') formatted = formatRemember(raw);
|
else if (name === 'remember') formatted = formatRemember(raw);
|
||||||
|
else if (name === 'todo') formatted = formatTodo(raw);
|
||||||
else if (name === 'recall') formatted = formatRecall(raw);
|
else if (name === 'recall') formatted = formatRecall(raw);
|
||||||
else if (name === 'set_identity') formatted = formatSetIdentity(raw);
|
else if (name === 'set_identity') formatted = formatSetIdentity(raw);
|
||||||
else formatted = raw.success ? '' : formatFailure(raw.error || '失败');
|
else formatted = raw.success ? '' : formatFailure(raw.error || '失败');
|
||||||
|
|||||||
@ -8,7 +8,7 @@ const crypto = require('crypto');
|
|||||||
const MEMORY_DIR = path.join(os.homedir(), 'Library', 'Application Support', 'VoiceInput', 'memory');
|
const MEMORY_DIR = path.join(os.homedir(), 'Library', 'Application Support', 'VoiceInput', 'memory');
|
||||||
const MEMORY_FILE = path.join(MEMORY_DIR, 'memory.json');
|
const MEMORY_FILE = path.join(MEMORY_DIR, 'memory.json');
|
||||||
|
|
||||||
const VALID_TYPES = ['preference', 'habit', 'event', 'todo', 'identity', 'relationship', 'machine', 'other'];
|
const VALID_TYPES = ['preference', 'habit', 'event', 'identity', 'relationship', 'machine', 'other'];
|
||||||
|
|
||||||
function ensureDir() {
|
function ensureDir() {
|
||||||
if (!fs.existsSync(MEMORY_DIR)) {
|
if (!fs.existsSync(MEMORY_DIR)) {
|
||||||
@ -124,4 +124,4 @@ function rememberTool(args) {
|
|||||||
return { success: false, error: '未知 action' };
|
return { success: false, error: '未知 action' };
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { rememberTool };
|
module.exports = { rememberTool, loadMemories, saveMemories, MEMORY_FILE };
|
||||||
|
|||||||
159
Agent/src/tools/todo.js
Normal file
159
Agent/src/tools/todo.js
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const os = require('os');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
const MEMORY_DIR = path.join(os.homedir(), 'Library', 'Application Support', 'VoiceInput', 'memory');
|
||||||
|
const MEMORY_FILE = path.join(MEMORY_DIR, 'memory.json');
|
||||||
|
|
||||||
|
function ensureDir() {
|
||||||
|
if (!fs.existsSync(MEMORY_DIR)) {
|
||||||
|
fs.mkdirSync(MEMORY_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadMemories() {
|
||||||
|
ensureDir();
|
||||||
|
if (!fs.existsSync(MEMORY_FILE)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const raw = fs.readFileSync(MEMORY_FILE, 'utf8');
|
||||||
|
return JSON.parse(raw);
|
||||||
|
} catch (_) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveMemories(memories) {
|
||||||
|
ensureDir();
|
||||||
|
fs.writeFileSync(MEMORY_FILE, JSON.stringify(memories, null, 2), 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateId() {
|
||||||
|
return crypto.randomUUID().slice(0, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
function todoTool(args) {
|
||||||
|
const { action, content, id, remind_at, keywords } = args;
|
||||||
|
|
||||||
|
if (!['add', 'complete', 'delete', 'update'].includes(action)) {
|
||||||
|
return { success: false, error: 'action 必须是 add / complete / delete / update' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验 remind_at(如果提供)
|
||||||
|
if (remind_at !== undefined && remind_at !== null) {
|
||||||
|
if (action !== 'add' && action !== 'update') {
|
||||||
|
return { success: false, error: `remind_at 仅在 add 和 update 时有效` };
|
||||||
|
}
|
||||||
|
const ts = new Date(remind_at).getTime();
|
||||||
|
if (isNaN(ts)) {
|
||||||
|
return { success: false, error: 'remind_at 必须是有效的 ISO 8601 时间字符串,例如 2026-05-01T14:45:00.000Z' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const memories = loadMemories();
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
// --- add ---
|
||||||
|
if (action === 'add') {
|
||||||
|
if (!content || !content.trim()) {
|
||||||
|
return { success: false, error: 'content 不能为空' };
|
||||||
|
}
|
||||||
|
const entry = {
|
||||||
|
id: generateId(),
|
||||||
|
type: 'todo',
|
||||||
|
content: content.trim(),
|
||||||
|
keywords: Array.isArray(keywords) ? keywords.filter(k => k && typeof k === 'string') : [],
|
||||||
|
completed: false,
|
||||||
|
completed_at: null,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
};
|
||||||
|
if (remind_at !== undefined && remind_at !== null) {
|
||||||
|
entry.remind_at = new Date(remind_at).toISOString();
|
||||||
|
entry.reminded = false;
|
||||||
|
}
|
||||||
|
memories.push(entry);
|
||||||
|
saveMemories(memories);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
action: 'add',
|
||||||
|
entry,
|
||||||
|
total: memories.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- complete / delete / update (都需要 id) ---
|
||||||
|
if (!id) {
|
||||||
|
return { success: false, error: `${action} 需要提供 id,请从 recall 工具的搜索结果中获取` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const idx = memories.findIndex(m => m.id === id && m.type === 'todo');
|
||||||
|
if (idx === -1) {
|
||||||
|
return { success: false, error: `未找到 id=${id} 的待办` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- complete ---
|
||||||
|
if (action === 'complete') {
|
||||||
|
memories[idx].completed = true;
|
||||||
|
memories[idx].completed_at = now;
|
||||||
|
// 已完成的不再提醒
|
||||||
|
delete memories[idx].remind_at;
|
||||||
|
delete memories[idx].reminded;
|
||||||
|
memories[idx].updated_at = now;
|
||||||
|
saveMemories(memories);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
action: 'complete',
|
||||||
|
entry: memories[idx],
|
||||||
|
total: memories.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- delete ---
|
||||||
|
if (action === 'delete') {
|
||||||
|
const removed = memories.splice(idx, 1)[0];
|
||||||
|
saveMemories(memories);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
action: 'delete',
|
||||||
|
entry: removed,
|
||||||
|
total: memories.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- update ---
|
||||||
|
if (action === 'update') {
|
||||||
|
if (content !== undefined && content !== null) {
|
||||||
|
memories[idx].content = String(content).trim();
|
||||||
|
}
|
||||||
|
if (keywords !== undefined) {
|
||||||
|
memories[idx].keywords = Array.isArray(keywords) ? keywords.filter(k => k && typeof k === 'string') : [];
|
||||||
|
}
|
||||||
|
// 更新提醒时间
|
||||||
|
if (remind_at !== undefined) {
|
||||||
|
if (remind_at === null) {
|
||||||
|
delete memories[idx].remind_at;
|
||||||
|
delete memories[idx].reminded;
|
||||||
|
} else {
|
||||||
|
memories[idx].remind_at = new Date(remind_at).toISOString();
|
||||||
|
memories[idx].reminded = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
memories[idx].updated_at = now;
|
||||||
|
saveMemories(memories);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
action: 'update',
|
||||||
|
entry: memories[idx],
|
||||||
|
total: memories.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, error: '未知 action' };
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { todoTool };
|
||||||
@ -17,6 +17,7 @@ function toolNameMap(name) {
|
|||||||
search_workspace: '文件搜索',
|
search_workspace: '文件搜索',
|
||||||
read_mediafile: '读取媒体文件',
|
read_mediafile: '读取媒体文件',
|
||||||
remember: '记住',
|
remember: '记住',
|
||||||
|
todo: '待办',
|
||||||
recall: '回忆',
|
recall: '回忆',
|
||||||
set_identity: '设置身份',
|
set_identity: '设置身份',
|
||||||
};
|
};
|
||||||
@ -80,7 +81,7 @@ function buildStartLine(name, args) {
|
|||||||
return `${label} ${args.path}`;
|
return `${label} ${args.path}`;
|
||||||
}
|
}
|
||||||
if (name === 'remember') {
|
if (name === 'remember') {
|
||||||
const typeNames = { preference: '偏好', habit: '习惯', event: '事件', todo: '待办', identity: '身份', relationship: '关系', other: '其他' };
|
const typeNames = { preference: '偏好', habit: '习惯', event: '事件', identity: '身份', relationship: '关系', other: '其他' };
|
||||||
const typeLabel = typeNames[args.type] || args.type || '';
|
const typeLabel = typeNames[args.type] || args.type || '';
|
||||||
const content = args.content || '';
|
const content = args.content || '';
|
||||||
const preview = content.length > 40 ? content.slice(0, 40) + '…' : content;
|
const preview = content.length > 40 ? content.slice(0, 40) + '…' : content;
|
||||||
@ -92,6 +93,23 @@ function buildStartLine(name, args) {
|
|||||||
}
|
}
|
||||||
return `${label} ${typeLabel ? `[${typeLabel}] ` : ''}${preview}`;
|
return `${label} ${typeLabel ? `[${typeLabel}] ` : ''}${preview}`;
|
||||||
}
|
}
|
||||||
|
if (name === 'todo') {
|
||||||
|
const actionNames = { add: '新增', complete: '完成', delete: '删除', update: '更新' };
|
||||||
|
const actionLabel = actionNames[args.action] || args.action || '';
|
||||||
|
const content = args.content || '';
|
||||||
|
const preview = content.length > 40 ? content.slice(0, 40) + '…' : content;
|
||||||
|
let timeStr = '';
|
||||||
|
if (args.remind_at) {
|
||||||
|
try {
|
||||||
|
const d = new Date(args.remind_at);
|
||||||
|
timeStr = `${d.getFullYear()}年${d.getMonth()+1}月${d.getDate()}日 ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`;
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
const parts = [`${label} ${actionLabel}`];
|
||||||
|
if (preview) parts.push(preview);
|
||||||
|
if (timeStr) parts.push(timeStr);
|
||||||
|
return parts.join(' · ');
|
||||||
|
}
|
||||||
if (name === 'recall') {
|
if (name === 'recall') {
|
||||||
const typeNames = { preference: '偏好', habit: '习惯', event: '事件', todo: '待办', identity: '身份', relationship: '关系', other: '其他' };
|
const typeNames = { preference: '偏好', habit: '习惯', event: '事件', todo: '待办', identity: '身份', relationship: '关系', other: '其他' };
|
||||||
const typeLabels = (args.types || []).map(t => typeNames[t] || t).filter(Boolean);
|
const typeLabels = (args.types || []).map(t => typeNames[t] || t).filter(Boolean);
|
||||||
@ -243,6 +261,13 @@ function formatResultLines(name, args, raw) {
|
|||||||
if (raw.action === 'update') return ['已更新'];
|
if (raw.action === 'update') return ['已更新'];
|
||||||
return ['已记录'];
|
return ['已记录'];
|
||||||
}
|
}
|
||||||
|
if (name === 'todo') {
|
||||||
|
if (raw.action === 'add') return ['已创建待办'];
|
||||||
|
if (raw.action === 'complete') return ['已完成待办'];
|
||||||
|
if (raw.action === 'delete') return ['已删除待办'];
|
||||||
|
if (raw.action === 'update') return ['已更新待办'];
|
||||||
|
return ['完成'];
|
||||||
|
}
|
||||||
if (name === 'recall') {
|
if (name === 'recall') {
|
||||||
const total = raw.total || 0;
|
const total = raw.total || 0;
|
||||||
const returned = raw.returned || 0;
|
const returned = raw.returned || 0;
|
||||||
|
|||||||
@ -521,10 +521,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSSpeechSynthesizerDel
|
|||||||
try? data.write(to: URL(fileURLWithPath: path), options: .atomic)
|
try? data.write(to: URL(fileURLWithPath: path), options: .atomic)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func markMemoryReminded(_ memoryId: String) {
|
private func markTodoCompleted(_ memoryId: String) {
|
||||||
var memories = loadMemories()
|
var memories = loadMemories()
|
||||||
guard let idx = memories.firstIndex(where: { ($0["id"] as? String) == memoryId }) else { return }
|
guard let idx = memories.firstIndex(where: { ($0["id"] as? String) == memoryId }) else { return }
|
||||||
memories[idx]["reminded"] = true
|
let nowISO = ISO8601DateFormatter().string(from: Date())
|
||||||
|
memories[idx]["completed"] = true
|
||||||
|
memories[idx]["completed_at"] = nowISO
|
||||||
|
memories[idx].removeValue(forKey: "remind_at")
|
||||||
|
memories[idx].removeValue(forKey: "reminded")
|
||||||
saveMemories(memories)
|
saveMemories(memories)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -576,7 +580,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSSpeechSynthesizerDel
|
|||||||
df.locale = Locale(identifier: "zh_CN")
|
df.locale = Locale(identifier: "zh_CN")
|
||||||
df.dateFormat = "yyyy年M月d日 H:mm"
|
df.dateFormat = "yyyy年M月d日 H:mm"
|
||||||
let timeStr = df.string(from: now)
|
let timeStr = df.string(from: now)
|
||||||
let reminderMsg = "[系统提醒] 现在是 \(timeStr),请提醒用户:\(content)"
|
let reminderMsg = "[系统自动发送的消息] 现在是 \(timeStr),请提醒用户:\(content)\n(待办ID:\(memoryId))"
|
||||||
|
|
||||||
print("[Reminder] ✅ 触发提醒 id=\(memoryId)")
|
print("[Reminder] ✅ 触发提醒 id=\(memoryId)")
|
||||||
triggerReminder(message: reminderMsg, memoryId: memoryId)
|
triggerReminder(message: reminderMsg, memoryId: memoryId)
|
||||||
@ -640,7 +644,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSSpeechSynthesizerDel
|
|||||||
print("[Reminder] 消息已发送")
|
print("[Reminder] 消息已发送")
|
||||||
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||||
self.markMemoryReminded(memoryId)
|
self.markTodoCompleted(memoryId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -426,6 +426,11 @@ final class Config {
|
|||||||
userDataDir + "/user_profile.json"
|
userDataDir + "/user_profile.json"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 记忆文件路径
|
||||||
|
var memoryPath: String {
|
||||||
|
userDataDir + "/memory/memory.json"
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - models.json 读写
|
// MARK: - models.json 读写
|
||||||
|
|
||||||
func loadAgentModels() -> AgentModelsFile {
|
func loadAgentModels() -> AgentModelsFile {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user