memi-agent 1.0.0 → 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 CHANGED
@@ -1,90 +1,239 @@
1
- # Memi Agent
2
-
3
- 本地 AI 助手,终端 + 网页双模式,支持 DeepSeek/OpenAI/Anthropic 等模型。
4
-
5
- ## 安装
6
-
7
- ```bash
8
- git clone https://github.com/yourname/memi.git
9
- cd memi
10
- npm install --prefix memi-server
11
- npm install --prefix memi-client
12
- ```
13
-
14
- ## 启动
15
-
16
- ```bash
17
- # 启动后端
18
- npm start --prefix memi-server
19
-
20
- # 启动前端(可选)
21
- npm run dev --prefix memi-client
22
-
23
- # 启动 CLI
24
- node memi-agent.js
25
- # 或用 .bat
26
- memi chat
27
- ```
28
-
29
- ## 配置
30
-
31
- ```bash
32
- memi onboard # 6 步新手引导
33
- ```
34
-
35
- ## 命令
36
-
37
- | 命令 | 说明 |
38
- |---|---|
39
- | `memi chat` | 进入对话 |
40
- | `memi status` | 查看配置 |
41
- | `memi skills` | 技能列表 |
42
- | `memi sessions` | 会话管理 |
43
- | `memi doctor` | 系统诊断 |
44
- | `memi agent` | Agent 信息 |
45
- | `memi dashboard` | 打开网页版 |
46
- | `memi onboard` | 重新配置 |
47
- | `memi update` | 检查更新 |
48
- | `memi version` | 版本 |
49
- | `memi help` | 帮助 |
50
-
51
- ## 对话内命令
52
-
53
- | 命令 | 说明 |
54
- |---|---|
55
- | `/help` | 帮助 |
56
- | `/history` | 对话历史 |
57
- | `/sessions` | 会话列表 |
58
- | `/new` | 新建会话 |
59
- | `/load 名称` | 加载会话 |
60
- | `/save` | 保存会话 |
61
- | `/tools` | 工具调用记录 |
62
- | `/stats` | 会话统计 |
63
- | `/balance` | API 余额 |
64
- | `/think` | 思考强度 |
65
- | `/currency` | 切换币种 |
66
- | `/agent` | 智能体管理 |
67
-
68
- ## ClawHub 兼容
69
-
70
- ```bash
71
- npm i -g clawhub
72
- clawhub install weather # 安装 OpenClaw 技能
73
- memi skills # 自动识别
74
- ```
75
-
76
- ## 网页版
77
-
78
- `http://localhost:3001/dashboard`
79
-
80
- ## 目录结构
81
-
82
- ```
83
- memi/
84
- ├── memi-agent.js # CLI 入口
85
- ├── memi.bat # Windows 快捷启动
86
- ├── memi-server/ # Express 后端
87
- ├── memi-client/ # React 前端
88
- ├── memi-config/ # 配置 & 技能 & 会话
89
- └── README.md
90
- ```
1
+
2
+ <p align="center">
3
+ <pre style="font-size: 12px; line-height: 1.2;">
4
+ __ __ ___ __ __ ___
5
+ | \/ || __|| \/ ||_ _|
6
+ | |\/| || _| | |\/| | | |
7
+ |_| |_||___||_| |_||___|
8
+ </pre>
9
+ </p>
10
+
11
+ <p align="center">
12
+ <strong>你的本地 AI 助手 — 终端、网页、聊天软件,无处不在。</strong>
13
+ </p>
14
+
15
+ <p align="center">
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
+ <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
+ <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>
20
+ </p>
21
+
22
+ **Memi** 是一个运行在你本机的个人 AI 助手。它在终端里跟你聊天,在网页上给你看板,还打通了 Telegram / 飞书 / 企业微信 / QQ —— 所有渠道共享同一个会话和记忆。
23
+
24
+ 支持 80+ AI 模型商(DeepSeek / OpenAI / Anthropic / 通义千问 / Moonshot / 智谱 …),自带 60+ 工具,兼容 ClawHub 技能生态。
25
+
26
+ ---
27
+
28
+ ## 快速开始
29
+
30
+ ```bash
31
+ # 全局安装
32
+ npm install -g memi-agent
33
+ memi onboard
34
+
35
+ # 或者一行搞定(无需安装)
36
+ npx memi-agent onboard
37
+ ```
38
+
39
+ `memi onboard` 会引导你完成模型配置、工作区初始化、渠道接入,**macOS / Linux / Windows** 都支持。
40
+
41
+ 启动后:
42
+
43
+ ```bash
44
+ memi chat # 终端对话
45
+ memi dashboard # 打开网页控制台 http://localhost:3001/dashboard
46
+ memi status # 查看当前状态
47
+ ```
48
+
49
+ ---
50
+
51
+ ## 亮点
52
+
53
+ - **双界面** 终端 CLI + 网页 Dashboard,同一个后端,无缝切换。
54
+ - **多渠道收件箱** — Telegram、飞书/Lark、企业微信、QQ,消息统一路由到 Agent 处理。
55
+ - **60+ 工具** 文件读写、命令执行、网络搜索、图片生成、HTTP 请求、系统信息、向量记忆搜索、浏览器自动化、网页抓取、定时提醒……
56
+ - **ClawHub 兼容** 直接安装 OpenClaw 社区的 Skill,`clawhub install` 即装即用。
57
+ - **多 Agent 协作** — `@agent` 语法切换/协作,每个 Agent 可以有独立的 system prompt 和模型。
58
+ - **12 步新手引导** 交互式 onboard,配模型、装守护进程、接渠道,一条龙。
59
+ - **系统守护进程** schtasks (Windows) / launchd (macOS) / systemd (Linux) 一键安装,开机自启。
60
+ - **网关安全** — `MEMI_GATEWAY_TOKEN` 鉴权,DM 白名单,避免未授权访问。
61
+ - **工作区文档** SOUL.md / MEMORY.md / USER.md / IDENTITY.md / TOOLS.md 每日注入 system prompt,保持记忆连续性。
62
+ - **浏览器自动化** Agent 可操控真实浏览器,打开网页、点击、截图。基于 Playwright。
63
+ - **图片管道** 文生图 视觉审查 → 自动重试,直到满意。
64
+ - **会话管理** — 保存/加载/重命名会话,支持 `/stats` 统计 Token 用量和费用。
65
+ - **80+ 模型商** 兼容 OpenAI API 格式的所有提供商,一键切换。
66
+
67
+ ---
68
+
69
+ ## 渠道支持
70
+
71
+ | 渠道 | 接入方式 | 配置命令 |
72
+ |---|---|---|
73
+ | Telegram | Webhook | `memi telegram <token>` |
74
+ | 飞书 / Lark | Webhook + WebSocket 长连接 | `memi feishu <token>` |
75
+ | 企业微信 | Webhook | `memi wecom <key>` |
76
+ | QQ | Webhook (go-cqhttp) | `memi qq <token>` |
77
+
78
+ 所有渠道共享同一个 Agent 会话,在 Dashboard 里可以实时看到每条消息和工具调用。
79
+
80
+ > **安全提示**:对外暴露前务必设置 `MEMI_GATEWAY_TOKEN` 环境变量,并配置渠道白名单。
81
+
82
+ ---
83
+
84
+ ## 命令参考
85
+
86
+ ### CLI 命令
87
+
88
+ | 命令 | 说明 |
89
+ |---|---|
90
+ | `memi chat` | 进入交互对话 |
91
+ | `memi onboard` | 12 步新手引导(模型/守护/渠道) |
92
+ | `memi dashboard` | 打开网页控制台 |
93
+ | `memi status` | 查看模型、端点、会话数、技能数 |
94
+ | `memi skills` | 列出已安装技能 |
95
+ | `memi sessions` | 列出所有会话 |
96
+ | `memi doctor` | 系统诊断(Node 版本、API 连通性、服务状态) |
97
+ | `memi agent` | 显示当前 Agent 信息 |
98
+ | `memi config` | 查看配置;`memi config edit` 重新配置 |
99
+ | `memi update` | 检查 GitHub Release 更新 |
100
+ | `memi server start` | 启动后端服务 |
101
+ | `memi browser install` | 安装 Playwright + Chromium |
102
+ | `memi rag index` | 索引工作区文档为向量库 |
103
+ | `memi rag search <query>` | 语义搜索工作区记忆 |
104
+ | `memi daemon install` | 安装系统守护进程(开机自启) |
105
+ | `memi version` | 显示版本号 |
106
+
107
+ ### 对话内斜杠命令
108
+
109
+ | 命令 | 说明 |
110
+ |---|---|
111
+ | `/help` | 帮助信息 |
112
+ | `/history` | 查看对话历史 |
113
+ | `/sessions` | 会话列表 |
114
+ | `/new` | 新建会话 |
115
+ | `/load <name>` | 加载会话 |
116
+ | `/save` | 保存当前会话 |
117
+ | `/rename <name>` | 重命名会话 |
118
+ | `/tools` | 工具调用记录 |
119
+ | `/stats` | Token 用量与费用估算 |
120
+ | `/balance` | API 余额查询 |
121
+ | `/think off\|low\|medium\|high\|max` | 思考强度 |
122
+ | `/currency` | 切换币种 (¥/$) |
123
+ | `/clear` | 清空当前会话 |
124
+ | `/agent list\|use\|add` | 多 Agent 管理 |
125
+ | `/exit` | 退出对话 |
126
+
127
+ ---
128
+
129
+ ## 配置
130
+
131
+ 最小配置(`memi-config/config.json`):
132
+
133
+ ```json5
134
+ {
135
+ endpoint: "https://api.deepseek.com/v1",
136
+ apiKey: "sk-...",
137
+ model: "deepseek-chat",
138
+ port: 3001
139
+ }
140
+ ```
141
+
142
+ `memi onboard` 会交互式生成完整配置,包括渠道 Token、技能目录、工作区路径等。
143
+
144
+ ---
145
+
146
+ ## ClawHub 技能
147
+
148
+ Memi 兼容 [ClawHub](https://clawhub.ai) 技能生态:
149
+
150
+ ```bash
151
+ npm install -g clawhub
152
+ clawhub install weather # 安装天气技能
153
+ memi skills # 自动识别并加载
154
+ ```
155
+
156
+ 也可以在对话中用 `/import_skill <url>` 从 GitHub 直接导入。
157
+
158
+ ---
159
+
160
+ ## 工作区文档
161
+
162
+ 这些文件放在 `memi-config/workspace/`,每天自动注入 Agent 的 system prompt,并支持向量搜索:
163
+
164
+ | 文件 | 作用 |
165
+ |---|---|
166
+ | `SOUL.md` | Agent 人格定义 |
167
+ | `MEMORY.md` | 长期记忆 |
168
+ | `USER.md` | 用户偏好 |
169
+ | `IDENTITY.md` | 身份设定 |
170
+ | `TOOLS.md` | 工具使用说明 |
171
+
172
+ Agent 可通过 `rag_search` 工具随时检索这些文档,无需占用每次对话的上下文窗口。
173
+
174
+ ---
175
+
176
+ ## 从源码运行
177
+
178
+ ```bash
179
+ git clone https://github.com/memi-ai/memi.git
180
+ cd memi
181
+
182
+ npm install --prefix memi-server
183
+
184
+ # 启动后端
185
+ npm start --prefix memi-server
186
+
187
+ # 新开终端,启动 CLI
188
+ node memi-agent.js chat
189
+ ```
190
+
191
+ ---
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
+
212
+ ## 技术栈
213
+
214
+ | 层 | 技术 |
215
+ |---|---|
216
+ | CLI | Node.js (CommonJS) |
217
+ | 后端 | Express + WebSocket |
218
+ | 前端 | React (memi-client) + 原生 HTML Dashboard |
219
+ | AI 协议 | OpenAI-compatible `/v1/chat/completions` |
220
+ | 浏览器 | Playwright (Chromium) — 可选,按需安装 |
221
+ | 图片 | pollinations.ai + 视觉审查循环 |
222
+ | 平台 | Windows / macOS / Linux |
223
+
224
+ ---
225
+
226
+ ## Star History
227
+
228
+ [![Star History Chart](https://api.star-history.com/svg?repos=memi-ai/memi&type=date&legend=top-left)](https://www.star-history.com/#memi-ai/memi&type=date&legend=top-left)
229
+
230
+ ---
231
+
232
+ ## 社区
233
+
234
+ - Issues & PR: [github.com/memi-ai/memi](https://github.com/memi-ai/memi)
235
+ - AI/vibe-coded PRs welcome! 🤖
236
+
237
+ ---
238
+
239
+ MIT © 2025 Memi
package/memi-agent.js CHANGED
@@ -1079,7 +1079,7 @@ const cmd = process.argv[2] || "chat";
1079
1079
  case "update":
1080
1080
  out(A.g + " 检查更新... ");
1081
1081
  try {
1082
- const r = await fetch("https://api.github.com/repos/YOURNAME/memi/releases/latest", { signal: AbortSignal.timeout(8000) });
1082
+ const r = await fetch("https://api.github.com/repos/memi-ai/memi/releases/latest", { signal: AbortSignal.timeout(8000) });
1083
1083
  if (!r.ok) throw new Error("HTTP " + r.status);
1084
1084
  const d = await r.json();
1085
1085
  const latest = (d.tag_name || "").replace(/^v/, "");
@@ -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}daemon${A.r} 守护进程 ${A.ck}update${A.r} 更新`);
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:
@@ -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,8 +1,11 @@
1
1
  {
2
2
  "name": "memi-agent",
3
- "version": "1.0.0",
4
- "description": "本地 AI 助手 — 终端 + 网页双模式,63 个工具,80+ 提供商,4 个消息渠道",
5
- "bin": { "memi": "memi-agent.js" },
3
+ "version": "1.0.2",
4
+ "description": "本地 AI 助手 — 终端 + 网页双模式,65+ 工具(含浏览器自动化、向量记忆搜索),80+ 模型商,4 个消息渠道,ClawHub 兼容,Docker 支持",
5
+ "bin": {
6
+ "memi": "memi-agent.js",
7
+ "memi-agent": "memi-agent.js"
8
+ },
6
9
  "files": [
7
10
  "memi-agent.js",
8
11
  "README.md",
@@ -15,13 +18,32 @@
15
18
  "memi-server/utils/agent.js",
16
19
  "memi-server/utils/aiProxy.js",
17
20
  "memi-server/utils/importSkill.js",
21
+ "memi-server/utils/vectorStore.js",
22
+ "memi-server/utils/browser.js",
18
23
  "memi-client/dist/",
19
24
  "memi-config/workspace/"
20
25
  ],
21
26
  "scripts": {
22
27
  "postinstall": "cd memi-server && npm install --omit=dev"
23
28
  },
24
- "keywords": ["ai", "agent", "cli", "llm", "deepseek", "openai"],
29
+ "keywords": [
30
+ "ai",
31
+ "agent",
32
+ "cli",
33
+ "llm",
34
+ "deepseek",
35
+ "openai"
36
+ ],
25
37
  "license": "MIT",
26
- "engines": { "node": ">=18" }
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/memi-ai/memi.git"
41
+ },
42
+ "bugs": {
43
+ "url": "https://github.com/memi-ai/memi/issues"
44
+ },
45
+ "homepage": "https://github.com/memi-ai/memi#readme",
46
+ "engines": {
47
+ "node": ">=18"
48
+ }
27
49
  }