memi-agent 1.0.1 → 1.0.2
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/README.md +33 -6
- package/memi-agent.js +74 -2
- package/memi-server/index.js +8 -0
- package/memi-server/utils/agent.js +114 -0
- package/memi-server/utils/browser.js +123 -0
- package/memi-server/utils/vectorStore.js +218 -0
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
<a href="https://www.npmjs.com/package/memi-agent"><img src="https://img.shields.io/npm/v/memi-agent?style=for-the-badge&color=6366f1" alt="npm version"></a>
|
|
17
17
|
<a href="https://github.com/memi-ai/memi/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge" alt="MIT License"></a>
|
|
18
18
|
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-18+-green.svg?style=for-the-badge" alt="Node.js 18+"></a>
|
|
19
|
+
<a href="#docker"><img src="https://img.shields.io/badge/Docker-ready-2496ED?logo=docker&logoColor=white&style=for-the-badge" alt="Docker"></a>
|
|
19
20
|
</p>
|
|
20
21
|
|
|
21
22
|
**Memi** 是一个运行在你本机的个人 AI 助手。它在终端里跟你聊天,在网页上给你看板,还打通了 Telegram / 飞书 / 企业微信 / QQ —— 所有渠道共享同一个会话和记忆。
|
|
@@ -27,12 +28,12 @@
|
|
|
27
28
|
## 快速开始
|
|
28
29
|
|
|
29
30
|
```bash
|
|
31
|
+
# 全局安装
|
|
30
32
|
npm install -g memi-agent
|
|
31
33
|
memi onboard
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
npm i -g memi-agent
|
|
34
|
+
|
|
35
|
+
# 或者一行搞定(无需安装)
|
|
36
|
+
npx memi-agent onboard
|
|
36
37
|
```
|
|
37
38
|
|
|
38
39
|
`memi onboard` 会引导你完成模型配置、工作区初始化、渠道接入,**macOS / Linux / Windows** 都支持。
|
|
@@ -51,13 +52,14 @@ memi status # 查看当前状态
|
|
|
51
52
|
|
|
52
53
|
- **双界面** — 终端 CLI + 网页 Dashboard,同一个后端,无缝切换。
|
|
53
54
|
- **多渠道收件箱** — Telegram、飞书/Lark、企业微信、QQ,消息统一路由到 Agent 处理。
|
|
54
|
-
- **60+ 工具** — 文件读写、命令执行、网络搜索、图片生成、HTTP
|
|
55
|
+
- **60+ 工具** — 文件读写、命令执行、网络搜索、图片生成、HTTP 请求、系统信息、向量记忆搜索、浏览器自动化、网页抓取、定时提醒……
|
|
55
56
|
- **ClawHub 兼容** — 直接安装 OpenClaw 社区的 Skill,`clawhub install` 即装即用。
|
|
56
57
|
- **多 Agent 协作** — `@agent` 语法切换/协作,每个 Agent 可以有独立的 system prompt 和模型。
|
|
57
58
|
- **12 步新手引导** — 交互式 onboard,配模型、装守护进程、接渠道,一条龙。
|
|
58
59
|
- **系统守护进程** — schtasks (Windows) / launchd (macOS) / systemd (Linux) 一键安装,开机自启。
|
|
59
60
|
- **网关安全** — `MEMI_GATEWAY_TOKEN` 鉴权,DM 白名单,避免未授权访问。
|
|
60
61
|
- **工作区文档** — SOUL.md / MEMORY.md / USER.md / IDENTITY.md / TOOLS.md 每日注入 system prompt,保持记忆连续性。
|
|
62
|
+
- **浏览器自动化** — Agent 可操控真实浏览器,打开网页、点击、截图。基于 Playwright。
|
|
61
63
|
- **图片管道** — 文生图 → 视觉审查 → 自动重试,直到满意。
|
|
62
64
|
- **会话管理** — 保存/加载/重命名会话,支持 `/stats` 统计 Token 用量和费用。
|
|
63
65
|
- **80+ 模型商** — 兼容 OpenAI API 格式的所有提供商,一键切换。
|
|
@@ -96,6 +98,9 @@ memi status # 查看当前状态
|
|
|
96
98
|
| `memi config` | 查看配置;`memi config edit` 重新配置 |
|
|
97
99
|
| `memi update` | 检查 GitHub Release 更新 |
|
|
98
100
|
| `memi server start` | 启动后端服务 |
|
|
101
|
+
| `memi browser install` | 安装 Playwright + Chromium |
|
|
102
|
+
| `memi rag index` | 索引工作区文档为向量库 |
|
|
103
|
+
| `memi rag search <query>` | 语义搜索工作区记忆 |
|
|
99
104
|
| `memi daemon install` | 安装系统守护进程(开机自启) |
|
|
100
105
|
| `memi version` | 显示版本号 |
|
|
101
106
|
|
|
@@ -154,7 +159,7 @@ memi skills # 自动识别并加载
|
|
|
154
159
|
|
|
155
160
|
## 工作区文档
|
|
156
161
|
|
|
157
|
-
这些文件放在 `memi-config/workspace/`,每天自动注入 Agent 的 system prompt
|
|
162
|
+
这些文件放在 `memi-config/workspace/`,每天自动注入 Agent 的 system prompt,并支持向量搜索:
|
|
158
163
|
|
|
159
164
|
| 文件 | 作用 |
|
|
160
165
|
|---|---|
|
|
@@ -164,6 +169,8 @@ memi skills # 自动识别并加载
|
|
|
164
169
|
| `IDENTITY.md` | 身份设定 |
|
|
165
170
|
| `TOOLS.md` | 工具使用说明 |
|
|
166
171
|
|
|
172
|
+
Agent 可通过 `rag_search` 工具随时检索这些文档,无需占用每次对话的上下文窗口。
|
|
173
|
+
|
|
167
174
|
---
|
|
168
175
|
|
|
169
176
|
## 从源码运行
|
|
@@ -183,6 +190,25 @@ node memi-agent.js chat
|
|
|
183
190
|
|
|
184
191
|
---
|
|
185
192
|
|
|
193
|
+
## Docker
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
# 克隆仓库
|
|
197
|
+
git clone https://github.com/memi-ai/memi.git
|
|
198
|
+
cd memi
|
|
199
|
+
|
|
200
|
+
# 一键启动
|
|
201
|
+
docker compose up -d
|
|
202
|
+
|
|
203
|
+
# 或者单独构建
|
|
204
|
+
docker build -t memi-agent .
|
|
205
|
+
docker run -d -p 3001:3001 -v memi-config:/app/memi-config memi-agent
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
访问 `http://localhost:3001/dashboard`。
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
186
212
|
## 技术栈
|
|
187
213
|
|
|
188
214
|
| 层 | 技术 |
|
|
@@ -191,6 +217,7 @@ node memi-agent.js chat
|
|
|
191
217
|
| 后端 | Express + WebSocket |
|
|
192
218
|
| 前端 | React (memi-client) + 原生 HTML Dashboard |
|
|
193
219
|
| AI 协议 | OpenAI-compatible `/v1/chat/completions` |
|
|
220
|
+
| 浏览器 | Playwright (Chromium) — 可选,按需安装 |
|
|
194
221
|
| 图片 | pollinations.ai + 视觉审查循环 |
|
|
195
222
|
| 平台 | Windows / macOS / Linux |
|
|
196
223
|
|
package/memi-agent.js
CHANGED
|
@@ -1159,6 +1159,53 @@ const cmd = process.argv[2] || "chat";
|
|
|
1159
1159
|
log("");
|
|
1160
1160
|
} catch { fail("无法读取"); }
|
|
1161
1161
|
break;
|
|
1162
|
+
case "rag": {
|
|
1163
|
+
const sub = process.argv[3];
|
|
1164
|
+
if (sub === "index") {
|
|
1165
|
+
log(A.b + " 索引中..." + A.r);
|
|
1166
|
+
try {
|
|
1167
|
+
const { indexWorkspace } = require("./memi-server/utils/vectorStore");
|
|
1168
|
+
const c = loadCfg();
|
|
1169
|
+
const r = await indexWorkspace(c);
|
|
1170
|
+
if (r) ok(`已索引 ${r.chunks} 个文本块` + (r.embedded > 0 ? ` (${r.embedded} 已嵌入)` : ""));
|
|
1171
|
+
else fail("索引失败");
|
|
1172
|
+
} catch(e) { fail("索引失败: " + e.message); }
|
|
1173
|
+
} else if (sub === "search") {
|
|
1174
|
+
const query = process.argv.slice(4).join(" ");
|
|
1175
|
+
if (!query) { log(A.g + " 用法: memi rag search <查询语句>"); break; }
|
|
1176
|
+
try {
|
|
1177
|
+
const { search } = require("./memi-server/utils/vectorStore");
|
|
1178
|
+
const c = loadCfg();
|
|
1179
|
+
const result = await search(query, c);
|
|
1180
|
+
log(A.b + "\n RAG 搜索: " + query + "\n" + A.r);
|
|
1181
|
+
log(result);
|
|
1182
|
+
} catch(e) { fail("搜索失败: " + e.message); }
|
|
1183
|
+
} else if (sub === "stats") {
|
|
1184
|
+
try {
|
|
1185
|
+
const { stats } = require("./memi-server/utils/vectorStore");
|
|
1186
|
+
const s = stats();
|
|
1187
|
+
log(A.b + "\n 向量库统计\n" + A.r);
|
|
1188
|
+
log(` 文档块: ${s.chunks}`);
|
|
1189
|
+
log(` 来源文件: ${s.sources.join(", ") || "无"}`);
|
|
1190
|
+
log(` 总字符: ${s.totalChars}`);
|
|
1191
|
+
log(` 向量嵌入: ${s.hasEmbeddings ? "✓" : "✗ (使用关键词匹配)"}`);
|
|
1192
|
+
log("");
|
|
1193
|
+
} catch(e) { fail("统计失败: " + e.message); }
|
|
1194
|
+
} else if (sub === "clear") {
|
|
1195
|
+
try {
|
|
1196
|
+
const { clearIndex } = require("./memi-server/utils/vectorStore");
|
|
1197
|
+
clearIndex();
|
|
1198
|
+
ok("向量索引已清除");
|
|
1199
|
+
} catch(e) { fail("清除失败: " + e.message); }
|
|
1200
|
+
} else {
|
|
1201
|
+
log(A.b + " memi rag <子命令>\n" + A.r);
|
|
1202
|
+
log(` ${A.ck}index${A.r} 索引工作区文档`);
|
|
1203
|
+
log(` ${A.ck}search${A.r} 搜索记忆 ${A.g}memi rag search <查询语句>${A.r}`);
|
|
1204
|
+
log(` ${A.ck}stats${A.r} 向量库统计`);
|
|
1205
|
+
log(` ${A.ck}clear${A.r} 清除索引`);
|
|
1206
|
+
}
|
|
1207
|
+
break;
|
|
1208
|
+
}
|
|
1162
1209
|
case "doctor": {
|
|
1163
1210
|
let ok = true;
|
|
1164
1211
|
log(A.b + "\n Memi Doctor — 诊断报告\n");
|
|
@@ -1343,6 +1390,31 @@ const cmd = process.argv[2] || "chat";
|
|
|
1343
1390
|
} catch(e) { fail("打包失败: " + e.message); }
|
|
1344
1391
|
break;
|
|
1345
1392
|
}
|
|
1393
|
+
case "browser": {
|
|
1394
|
+
const sub = process.argv[3];
|
|
1395
|
+
if (sub === "install") {
|
|
1396
|
+
log(A.b + " 安装 Playwright + Chromium..." + A.r);
|
|
1397
|
+
try {
|
|
1398
|
+
const { installBrowser } = require("./memi-server/utils/browser");
|
|
1399
|
+
const result = await installBrowser();
|
|
1400
|
+
ok(result);
|
|
1401
|
+
} catch(e) { fail("安装失败: " + e.message); }
|
|
1402
|
+
} else if (sub === "test") {
|
|
1403
|
+
try {
|
|
1404
|
+
const { navigate, closeBrowser } = require("./memi-server/utils/browser");
|
|
1405
|
+
const result = await navigate("https://example.com");
|
|
1406
|
+
log(A.b + "\n Browser Test\n" + A.r);
|
|
1407
|
+
log(result.slice(0, 500));
|
|
1408
|
+
await closeBrowser();
|
|
1409
|
+
ok("浏览器测试通过 ✓");
|
|
1410
|
+
} catch(e) { fail("测试失败: " + e.message); }
|
|
1411
|
+
} else {
|
|
1412
|
+
log(A.b + " memi browser <子命令>\n" + A.r);
|
|
1413
|
+
log(` ${A.ck}install${A.r} 安装 Playwright + Chromium`);
|
|
1414
|
+
log(` ${A.ck}test${A.r} 测试浏览器是否可用`);
|
|
1415
|
+
}
|
|
1416
|
+
break;
|
|
1417
|
+
}
|
|
1346
1418
|
case "help": case "--help": case "-h":
|
|
1347
1419
|
log(A.b + "\n Memi Agent CLI\n");
|
|
1348
1420
|
log(` ${A.ck}chat${A.r} 对话`);
|
|
@@ -1351,9 +1423,9 @@ const cmd = process.argv[2] || "chat";
|
|
|
1351
1423
|
log(` ${A.ck}reset${A.r} 重置 ${A.ck}config${A.r} 配置`);
|
|
1352
1424
|
log(` ${A.ck}server${A.r} 服务 ${A.ck}skills${A.r} 技能`);
|
|
1353
1425
|
log(` ${A.ck}dashboard${A.r}面板 ${A.ck}sessions${A.r} 会话`);
|
|
1354
|
-
log(` ${A.ck}
|
|
1426
|
+
log(` ${A.ck}rag${A.r} 向量记忆 ${A.ck}daemon${A.r} 守护`);
|
|
1427
|
+
log(` ${A.ck}browser${A.r} 浏览器 ${A.ck}update${A.r} 更新`);
|
|
1355
1428
|
log(` ${A.ck}version${A.r} 版本 ${A.ck}help${A.r} 帮助`);
|
|
1356
|
-
log(` ${A.ck}help${A.r} 帮助`);
|
|
1357
1429
|
log("");
|
|
1358
1430
|
break;
|
|
1359
1431
|
default:
|
package/memi-server/index.js
CHANGED
|
@@ -149,4 +149,12 @@ wss.on("connection", (ws) => {
|
|
|
149
149
|
server.listen(PORT, () => {
|
|
150
150
|
console.log(`memi-server is running on port ${PORT}`);
|
|
151
151
|
console.log(`WebSocket: ws://localhost:${PORT}/api/gateway/ws`);
|
|
152
|
+
|
|
153
|
+
// 自动索引工作区文档(异步,不阻塞启动)
|
|
154
|
+
try {
|
|
155
|
+
const { indexWorkspace } = require("./utils/vectorStore");
|
|
156
|
+
indexWorkspace().then(r => {
|
|
157
|
+
if (r && r.chunks > 0) console.log(`[RAG] 向量索引完成: ${r.chunks} 块, ${r.embedded} 已嵌入`);
|
|
158
|
+
}).catch(() => {});
|
|
159
|
+
} catch {}
|
|
152
160
|
});
|
|
@@ -871,6 +871,120 @@ const TOOLS = [
|
|
|
871
871
|
}
|
|
872
872
|
},
|
|
873
873
|
},
|
|
874
|
+
{
|
|
875
|
+
name: "rag_search",
|
|
876
|
+
description:
|
|
877
|
+
"在工作区文档中执行语义搜索。传入查询语句,返回最相关的文档片段(SOUL.md / MEMORY.md / USER.md / IDENTITY.md / TOOLS.md)。\n" +
|
|
878
|
+
"适用场景:用户问「我之前说过什么」「我的偏好是什么」「Agent 的人设是什么」等问题时,用此工具检索长期记忆。",
|
|
879
|
+
parameters: {
|
|
880
|
+
type: "object",
|
|
881
|
+
properties: {
|
|
882
|
+
query: { type: "string", description: "搜索查询语句" },
|
|
883
|
+
},
|
|
884
|
+
required: ["query"],
|
|
885
|
+
},
|
|
886
|
+
handler: async (args) => {
|
|
887
|
+
try {
|
|
888
|
+
const configPath = path.join(__dirname, "..", "..", "memi-config", "config.json");
|
|
889
|
+
let config = {};
|
|
890
|
+
try { config = JSON.parse(fs.readFileSync(configPath, "utf8")); } catch {}
|
|
891
|
+
const { search } = require("./vectorStore");
|
|
892
|
+
return await search(args.query, config);
|
|
893
|
+
} catch (e) {
|
|
894
|
+
return "RAG 搜索失败: " + e.message;
|
|
895
|
+
}
|
|
896
|
+
},
|
|
897
|
+
},
|
|
898
|
+
{
|
|
899
|
+
name: "rag_index",
|
|
900
|
+
description:
|
|
901
|
+
"重建工作区文档的向量索引。当工作区文档(SOUL.md 等)被修改后,需要重新索引。此工具会读取所有工作区文档并建立向量搜索库。",
|
|
902
|
+
parameters: {
|
|
903
|
+
type: "object",
|
|
904
|
+
properties: {},
|
|
905
|
+
required: [],
|
|
906
|
+
},
|
|
907
|
+
handler: async () => {
|
|
908
|
+
try {
|
|
909
|
+
const configPath = path.join(__dirname, "..", "..", "memi-config", "config.json");
|
|
910
|
+
let config = {};
|
|
911
|
+
try { config = JSON.parse(fs.readFileSync(configPath, "utf8")); } catch {}
|
|
912
|
+
const { indexWorkspace } = require("./vectorStore");
|
|
913
|
+
const r = await indexWorkspace(config);
|
|
914
|
+
return `向量索引已重建:${r.chunks} 个文本块,${r.embedded} 个已嵌入向量。`;
|
|
915
|
+
} catch (e) {
|
|
916
|
+
return "索引失败: " + e.message;
|
|
917
|
+
}
|
|
918
|
+
},
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
name: "browser_navigate",
|
|
922
|
+
description:
|
|
923
|
+
"打开一个网页并获取其文本内容。url 是完整的网页地址(带 https://),返回页面标题和文本摘要。" +
|
|
924
|
+
"适用场景:用户要求查看某个网页、获取在线文档内容、抓取信息等。",
|
|
925
|
+
parameters: {
|
|
926
|
+
type: "object",
|
|
927
|
+
properties: {
|
|
928
|
+
url: { type: "string", description: "完整网页地址,例如 https://example.com" },
|
|
929
|
+
},
|
|
930
|
+
required: ["url"],
|
|
931
|
+
},
|
|
932
|
+
handler: async (args) => {
|
|
933
|
+
try {
|
|
934
|
+
const { navigate } = require("./browser");
|
|
935
|
+
return await navigate(args.url);
|
|
936
|
+
} catch (e) {
|
|
937
|
+
if (e.message && e.message.includes("Playwright 未安装")) {
|
|
938
|
+
return e.message;
|
|
939
|
+
}
|
|
940
|
+
return "浏览器导航失败: " + e.message;
|
|
941
|
+
}
|
|
942
|
+
},
|
|
943
|
+
},
|
|
944
|
+
{
|
|
945
|
+
name: "browser_screenshot",
|
|
946
|
+
description:
|
|
947
|
+
"对当前浏览器页面截图。返回截图文件路径。适用场景:用户要求看某个页面的样子、验证页面显示等。",
|
|
948
|
+
parameters: {
|
|
949
|
+
type: "object",
|
|
950
|
+
properties: {},
|
|
951
|
+
required: [],
|
|
952
|
+
},
|
|
953
|
+
handler: async () => {
|
|
954
|
+
try {
|
|
955
|
+
const { screenshot } = require("./browser");
|
|
956
|
+
return await screenshot();
|
|
957
|
+
} catch (e) {
|
|
958
|
+
if (e.message && e.message.includes("Playwright 未安装")) {
|
|
959
|
+
return e.message;
|
|
960
|
+
}
|
|
961
|
+
return "截图失败: " + e.message;
|
|
962
|
+
}
|
|
963
|
+
},
|
|
964
|
+
},
|
|
965
|
+
{
|
|
966
|
+
name: "browser_click",
|
|
967
|
+
description:
|
|
968
|
+
"在浏览器页面上点击一个元素。selector 是 CSS 选择器(例如 '#submit' 或 '.btn-primary')。点击后返回新页面内容。",
|
|
969
|
+
parameters: {
|
|
970
|
+
type: "object",
|
|
971
|
+
properties: {
|
|
972
|
+
selector: { type: "string", description: "CSS 选择器,如 '#login' 或 'button.submit'" },
|
|
973
|
+
},
|
|
974
|
+
required: ["selector"],
|
|
975
|
+
},
|
|
976
|
+
handler: async (args) => {
|
|
977
|
+
try {
|
|
978
|
+
const { click } = require("./browser");
|
|
979
|
+
return await click(args.selector);
|
|
980
|
+
} catch (e) {
|
|
981
|
+
if (e.message && e.message.includes("Playwright 未安装")) {
|
|
982
|
+
return e.message;
|
|
983
|
+
}
|
|
984
|
+
return "点击失败: " + e.message;
|
|
985
|
+
}
|
|
986
|
+
},
|
|
987
|
+
},
|
|
874
988
|
];
|
|
875
989
|
|
|
876
990
|
// ─── Agent 循环(提示词驱动工具调用)─────────────
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// ╔══════════════════════════════════════════════════════════╗
|
|
2
|
+
// ║ Memi Browser — Playwright 浏览器自动化 ║
|
|
3
|
+
// ║ Agent 可操控真实浏览器:导航、点击、输入、截图 ║
|
|
4
|
+
// ╚══════════════════════════════════════════════════════════╝
|
|
5
|
+
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
|
|
9
|
+
let browser = null;
|
|
10
|
+
let page = null;
|
|
11
|
+
let playwright = null;
|
|
12
|
+
|
|
13
|
+
// ─── 懒加载 Playwright ───────────────────────────────────
|
|
14
|
+
async function ensurePlaywright() {
|
|
15
|
+
if (playwright) return playwright;
|
|
16
|
+
try {
|
|
17
|
+
playwright = require("playwright");
|
|
18
|
+
} catch {
|
|
19
|
+
throw new Error(
|
|
20
|
+
"Playwright 未安装。请运行: npm install playwright && npx playwright install chromium\n" +
|
|
21
|
+
"或运行: memi browser install"
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
return playwright;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ─── 启动浏览器 ──────────────────────────────────────────
|
|
28
|
+
async function ensureBrowser(headless = true) {
|
|
29
|
+
const pw = await ensurePlaywright();
|
|
30
|
+
if (!browser || !browser.isConnected()) {
|
|
31
|
+
browser = await pw.chromium.launch({
|
|
32
|
+
headless,
|
|
33
|
+
args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"],
|
|
34
|
+
});
|
|
35
|
+
page = await browser.newPage();
|
|
36
|
+
await page.setViewportSize({ width: 1280, height: 800 });
|
|
37
|
+
}
|
|
38
|
+
return { browser, page };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─── 关闭浏览器 ──────────────────────────────────────────
|
|
42
|
+
async function closeBrowser() {
|
|
43
|
+
try {
|
|
44
|
+
if (page) { await page.close().catch(() => {}); page = null; }
|
|
45
|
+
if (browser) { await browser.close().catch(() => {}); browser = null; }
|
|
46
|
+
} catch {}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── 导航 ────────────────────────────────────────────────
|
|
50
|
+
async function navigate(url, timeout = 30000) {
|
|
51
|
+
const { page } = await ensureBrowser();
|
|
52
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout });
|
|
53
|
+
const title = await page.title();
|
|
54
|
+
const text = await page.evaluate(() => document.body.innerText.slice(0, 5000));
|
|
55
|
+
return `**${title}**\n${text}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ─── 点击 ────────────────────────────────────────────────
|
|
59
|
+
async function click(selector) {
|
|
60
|
+
const { page } = await ensureBrowser();
|
|
61
|
+
await page.waitForSelector(selector, { timeout: 10000 });
|
|
62
|
+
await page.click(selector);
|
|
63
|
+
await page.waitForLoadState("domcontentloaded").catch(() => {});
|
|
64
|
+
const title = await page.title();
|
|
65
|
+
const text = await page.evaluate(() => document.body.innerText.slice(0, 3000));
|
|
66
|
+
return `点击后页面: **${title}**\n${text}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ─── 输入 ────────────────────────────────────────────────
|
|
70
|
+
async function typeText(selector, text) {
|
|
71
|
+
const { page } = await ensureBrowser();
|
|
72
|
+
await page.waitForSelector(selector, { timeout: 10000 });
|
|
73
|
+
await page.fill(selector, text);
|
|
74
|
+
return `已在 ${selector} 中输入: ${text.slice(0, 200)}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ─── 截图 ────────────────────────────────────────────────
|
|
78
|
+
async function screenshot(fullPage = true) {
|
|
79
|
+
const { page } = await ensureBrowser();
|
|
80
|
+
const buf = await page.screenshot({ fullPage, type: "png" });
|
|
81
|
+
// 保存到 memi-config
|
|
82
|
+
const dir = path.join(__dirname, "..", "..", "memi-config");
|
|
83
|
+
const fname = `screenshot_${Date.now()}.png`;
|
|
84
|
+
const fpath = path.join(dir, fname);
|
|
85
|
+
fs.writeFileSync(fpath, buf);
|
|
86
|
+
return `截图已保存: ${fpath} (${(buf.length / 1024).toFixed(1)} KB)`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── 获取页面内容 ────────────────────────────────────────
|
|
90
|
+
async function getContent() {
|
|
91
|
+
const { page } = await ensureBrowser();
|
|
92
|
+
const title = await page.title();
|
|
93
|
+
const url = page.url();
|
|
94
|
+
const text = await page.evaluate(() => document.body.innerText.slice(0, 8000));
|
|
95
|
+
return `**${title}**\nURL: ${url}\n\n${text}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ─── 执行 JS ─────────────────────────────────────────────
|
|
99
|
+
async function evaluate(script) {
|
|
100
|
+
const { page } = await ensureBrowser();
|
|
101
|
+
const result = await page.evaluate(script);
|
|
102
|
+
return JSON.stringify(result).slice(0, 5000);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ─── 安装 Playwright ─────────────────────────────────────
|
|
106
|
+
async function installBrowser() {
|
|
107
|
+
const { execSync } = require("child_process");
|
|
108
|
+
const cwd = path.join(__dirname, "..");
|
|
109
|
+
try {
|
|
110
|
+
console.log("安装 playwright...");
|
|
111
|
+
execSync("npm install playwright", { cwd, stdio: "inherit" });
|
|
112
|
+
console.log("安装 Chromium...");
|
|
113
|
+
execSync("npx playwright install chromium", { cwd, stdio: "inherit" });
|
|
114
|
+
return "Playwright + Chromium 安装完成!现在可以使用浏览器工具了。";
|
|
115
|
+
} catch (e) {
|
|
116
|
+
throw new Error("安装失败: " + e.message);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = {
|
|
121
|
+
navigate, click, typeText, screenshot, getContent, evaluate,
|
|
122
|
+
closeBrowser, installBrowser, ensurePlaywright
|
|
123
|
+
};
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// ╔══════════════════════════════════════════════════════════╗
|
|
2
|
+
// ║ Memi Vector Store — 向量记忆 / RAG 引擎 ║
|
|
3
|
+
// ║ 嵌入工作区文档,支持语义搜索 ║
|
|
4
|
+
// ╚══════════════════════════════════════════════════════════╝
|
|
5
|
+
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
|
|
9
|
+
const CONFIG_DIR = path.join(__dirname, "..", "..", "memi-config");
|
|
10
|
+
const VECTOR_FILE = path.join(CONFIG_DIR, "vectors.json");
|
|
11
|
+
const CHUNK_SIZE = 500; // 每块最多 500 字符
|
|
12
|
+
const TOP_K = 5;
|
|
13
|
+
|
|
14
|
+
// ─── 读取配置 ────────────────────────────────────────────
|
|
15
|
+
function readConfig() {
|
|
16
|
+
try {
|
|
17
|
+
const raw = fs.readFileSync(path.join(CONFIG_DIR, "config.json"), "utf8");
|
|
18
|
+
return JSON.parse(raw);
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ─── 获取嵌入向量 ───────────────────────────────────────
|
|
25
|
+
async function getEmbedding(text, config) {
|
|
26
|
+
if (!config) config = readConfig();
|
|
27
|
+
if (!config || !config.apiKey) throw new Error("未配置 API Key");
|
|
28
|
+
|
|
29
|
+
const endpoint = (config.api1?.baseUrl || "https://api.deepseek.com/v1").replace(/\/$/, "");
|
|
30
|
+
const apiKey = config.api1?.apiKey || config.apiKey;
|
|
31
|
+
// 优先用 embedding 模型,否则用对话模型(DeepSeek 不支持 embedding,用 OpenAI 兼容端点)
|
|
32
|
+
const model = config.embeddingModel || "text-embedding-3-small";
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const axios = require("axios");
|
|
36
|
+
const resp = await axios.post(
|
|
37
|
+
`${endpoint}/embeddings`,
|
|
38
|
+
{ model, input: text },
|
|
39
|
+
{ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" }, timeout: 30000 }
|
|
40
|
+
);
|
|
41
|
+
return resp.data?.data?.[0]?.embedding;
|
|
42
|
+
} catch (e) {
|
|
43
|
+
// 如果 embedding API 失败,回退到简单的关键词匹配模式
|
|
44
|
+
console.warn("Embedding API 不可用,使用关键词匹配模式:", e.message);
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── 余弦相似度 ─────────────────────────────────────────
|
|
50
|
+
function cosineSimilarity(a, b) {
|
|
51
|
+
if (!a || !b || a.length !== b.length) return 0;
|
|
52
|
+
let dot = 0, na = 0, nb = 0;
|
|
53
|
+
for (let i = 0; i < a.length; i++) {
|
|
54
|
+
dot += a[i] * b[i];
|
|
55
|
+
na += a[i] * a[i];
|
|
56
|
+
nb += b[i] * b[i];
|
|
57
|
+
}
|
|
58
|
+
const denom = Math.sqrt(na) * Math.sqrt(nb);
|
|
59
|
+
return denom === 0 ? 0 : dot / denom;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── 简单关键词匹配(无 embedding API 时的回退)─────────
|
|
63
|
+
function keywordMatch(query, chunks) {
|
|
64
|
+
const terms = query.toLowerCase().split(/\s+/).filter(t => t.length > 1);
|
|
65
|
+
return chunks.map((chunk, idx) => {
|
|
66
|
+
const text = chunk.text.toLowerCase();
|
|
67
|
+
let score = 0;
|
|
68
|
+
for (const term of terms) {
|
|
69
|
+
if (text.includes(term)) score += 1;
|
|
70
|
+
// 部分匹配加分
|
|
71
|
+
for (let i = 0; i < term.length - 1; i++) {
|
|
72
|
+
if (text.includes(term.slice(i, i + 2))) score += 0.1;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { idx, score, chunk };
|
|
76
|
+
}).sort((a, b) => b.score - a.score);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── 分块 ───────────────────────────────────────────────
|
|
80
|
+
function chunkText(text, source) {
|
|
81
|
+
const chunks = [];
|
|
82
|
+
const paragraphs = text.split(/\n\n+/);
|
|
83
|
+
let current = "";
|
|
84
|
+
|
|
85
|
+
for (const para of paragraphs) {
|
|
86
|
+
if ((current + para).length > CHUNK_SIZE && current.length > 0) {
|
|
87
|
+
chunks.push({ text: current.trim(), source });
|
|
88
|
+
current = para;
|
|
89
|
+
} else {
|
|
90
|
+
current += (current ? "\n\n" : "") + para;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (current.trim()) chunks.push({ text: current.trim(), source });
|
|
94
|
+
|
|
95
|
+
return chunks;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ─── 索引工作区文档 ─────────────────────────────────────
|
|
99
|
+
async function indexWorkspace(config) {
|
|
100
|
+
if (!config) config = readConfig();
|
|
101
|
+
const wsDir = path.join(CONFIG_DIR, "workspace");
|
|
102
|
+
const files = ["SOUL.md", "MEMORY.md", "USER.md", "IDENTITY.md", "TOOLS.md"];
|
|
103
|
+
|
|
104
|
+
const allChunks = [];
|
|
105
|
+
for (const file of files) {
|
|
106
|
+
const fpath = path.join(wsDir, file);
|
|
107
|
+
if (!fs.existsSync(fpath)) continue;
|
|
108
|
+
const text = fs.readFileSync(fpath, "utf8");
|
|
109
|
+
const chunks = chunkText(text, file);
|
|
110
|
+
for (const c of chunks) allChunks.push(c);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 尝试获取嵌入
|
|
114
|
+
const needsEmbedding = config && config.apiKey;
|
|
115
|
+
let vectors = [];
|
|
116
|
+
|
|
117
|
+
if (needsEmbedding) {
|
|
118
|
+
for (let i = 0; i < allChunks.length; i++) {
|
|
119
|
+
const emb = await getEmbedding(allChunks[i].text, config);
|
|
120
|
+
vectors.push({ id: i, text: allChunks[i].text, source: allChunks[i].source, embedding: emb });
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
vectors = allChunks.map((c, i) => ({ id: i, text: c.text, source: c.source, embedding: null }));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 写入文件
|
|
127
|
+
try {
|
|
128
|
+
fs.writeFileSync(VECTOR_FILE, JSON.stringify(vectors, null, 2));
|
|
129
|
+
} catch {}
|
|
130
|
+
|
|
131
|
+
return { chunks: vectors.length, embedded: vectors.filter(v => v.embedding).length };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ─── 搜索 ───────────────────────────────────────────────
|
|
135
|
+
async function search(query, config, topK = TOP_K) {
|
|
136
|
+
if (!config) config = readConfig();
|
|
137
|
+
|
|
138
|
+
// 加载向量库
|
|
139
|
+
let vectors = [];
|
|
140
|
+
if (fs.existsSync(VECTOR_FILE)) {
|
|
141
|
+
try {
|
|
142
|
+
vectors = JSON.parse(fs.readFileSync(VECTOR_FILE, "utf8"));
|
|
143
|
+
} catch {}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (vectors.length === 0) {
|
|
147
|
+
return "向量库为空。请先运行 `memi rag index` 索引工作区文档。";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 如果有 embedding,用向量搜索
|
|
151
|
+
if (vectors[0]?.embedding) {
|
|
152
|
+
const qEmb = await getEmbedding(query, config);
|
|
153
|
+
if (qEmb) {
|
|
154
|
+
const scored = vectors
|
|
155
|
+
.map(v => ({ ...v, score: cosineSimilarity(qEmb, v.embedding) }))
|
|
156
|
+
.sort((a, b) => b.score - a.score)
|
|
157
|
+
.slice(0, topK);
|
|
158
|
+
|
|
159
|
+
return scored.map((s, i) =>
|
|
160
|
+
`**${s.source}** (相似度: ${(s.score * 100).toFixed(1)}%)\n${s.text.slice(0, 500)}`
|
|
161
|
+
).join("\n\n---\n\n");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 回退:关键词匹配
|
|
166
|
+
const results = keywordMatch(query, vectors).slice(0, topK);
|
|
167
|
+
return results.map((r, i) =>
|
|
168
|
+
`**${r.chunk.source}** (匹配度: ${r.score.toFixed(1)})\n${r.chunk.text.slice(0, 500)}`
|
|
169
|
+
).join("\n\n---\n\n");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ─── 添加文档 ───────────────────────────────────────────
|
|
173
|
+
async function addDocument(name, text, config) {
|
|
174
|
+
if (!config) config = readConfig();
|
|
175
|
+
|
|
176
|
+
const chunks = chunkText(text, name);
|
|
177
|
+
let vectors = [];
|
|
178
|
+
|
|
179
|
+
if (fs.existsSync(VECTOR_FILE)) {
|
|
180
|
+
try { vectors = JSON.parse(fs.readFileSync(VECTOR_FILE, "utf8")); } catch {}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const startId = vectors.length;
|
|
184
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
185
|
+
const emb = config?.apiKey ? await getEmbedding(chunks[i].text, config) : null;
|
|
186
|
+
vectors.push({ id: startId + i, text: chunks[i].text, source: name, embedding: emb });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
fs.writeFileSync(VECTOR_FILE, JSON.stringify(vectors, null, 2));
|
|
191
|
+
} catch {}
|
|
192
|
+
|
|
193
|
+
return chunks.length;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ─── 清除索引 ───────────────────────────────────────────
|
|
197
|
+
function clearIndex() {
|
|
198
|
+
if (fs.existsSync(VECTOR_FILE)) fs.unlinkSync(VECTOR_FILE);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ─── 统计 ───────────────────────────────────────────────
|
|
202
|
+
function stats() {
|
|
203
|
+
if (!fs.existsSync(VECTOR_FILE)) return { chunks: 0, sources: [], totalChars: 0, hasEmbeddings: false };
|
|
204
|
+
try {
|
|
205
|
+
const vectors = JSON.parse(fs.readFileSync(VECTOR_FILE, "utf8"));
|
|
206
|
+
const sources = [...new Set(vectors.map(v => v.source))];
|
|
207
|
+
return {
|
|
208
|
+
chunks: vectors.length,
|
|
209
|
+
sources,
|
|
210
|
+
totalChars: vectors.reduce((sum, v) => sum + v.text.length, 0),
|
|
211
|
+
hasEmbeddings: vectors.some(v => v.embedding),
|
|
212
|
+
};
|
|
213
|
+
} catch {
|
|
214
|
+
return { chunks: 0, sources: [], totalChars: 0, hasEmbeddings: false };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
module.exports = { indexWorkspace, search, addDocument, clearIndex, stats, getEmbedding };
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memi-agent",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "本地 AI 助手 — 终端 + 网页双模式,
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "本地 AI 助手 — 终端 + 网页双模式,65+ 工具(含浏览器自动化、向量记忆搜索),80+ 模型商,4 个消息渠道,ClawHub 兼容,Docker 支持",
|
|
5
5
|
"bin": {
|
|
6
|
-
"memi": "memi-agent.js"
|
|
6
|
+
"memi": "memi-agent.js",
|
|
7
|
+
"memi-agent": "memi-agent.js"
|
|
7
8
|
},
|
|
8
9
|
"files": [
|
|
9
10
|
"memi-agent.js",
|
|
@@ -17,6 +18,8 @@
|
|
|
17
18
|
"memi-server/utils/agent.js",
|
|
18
19
|
"memi-server/utils/aiProxy.js",
|
|
19
20
|
"memi-server/utils/importSkill.js",
|
|
21
|
+
"memi-server/utils/vectorStore.js",
|
|
22
|
+
"memi-server/utils/browser.js",
|
|
20
23
|
"memi-client/dist/",
|
|
21
24
|
"memi-config/workspace/"
|
|
22
25
|
],
|