memi-agent 1.0.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/memi-agent.js ADDED
@@ -0,0 +1,1364 @@
1
+ #!/usr/bin/env node
2
+ // ╔══════════════════════════════════════════════════════════╗
3
+ // ║ Memi Agent CLI ║
4
+ // ║ chat | status | config | skills | sessions | onboard ║
5
+ // ╚══════════════════════════════════════════════════════════╝
6
+
7
+ const readline = require("readline");
8
+ const fs = require("fs");
9
+ const path = require("path");
10
+
11
+ // ─── 路径 ────────────────────────────────────────────
12
+ const ROOT = __dirname;
13
+ const DIR = path.join(ROOT, "memi-config");
14
+ const CONFIG = path.join(DIR, "config.json");
15
+ const BACKUP_DIR = path.join(DIR, "backups");
16
+ const WORKSPACE = path.join(DIR, "workspace");
17
+ const SKILLS = path.join(DIR, "skills");
18
+ const SESSIONS = path.join(DIR, "sessions");
19
+ const AGENTS_DIR = path.join(DIR, "agents");
20
+ const LOGS = path.join(DIR, "logs");
21
+ const CACHE = path.join(DIR, "cache");
22
+ const ONBOARDED = path.join(DIR, ".onboarded");
23
+
24
+ [DIR, BACKUP_DIR, WORKSPACE, SKILLS, SESSIONS, AGENTS_DIR, LOGS, CACHE].forEach((d) => {
25
+ if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
26
+ });
27
+
28
+ // 初始化 workspace 文档(SOUL.md, MEMORY.md 等)
29
+ const initWorkspace = () => {
30
+ const docs = {
31
+ "SOUL.md": "# Memi Agent · Soul\n\n你是 Memi,一个机智高效的 AI 助手。拥有完整的系统访问权限。\n直接动手,不废话。",
32
+ "MEMORY.md": "# Memory\n\n> 在此记录用户偏好和重要上下文。\n",
33
+ "USER.md": "# User Profile\n\n> 用户习惯和偏好。\n",
34
+ "IDENTITY.md": "# Identity\n\n- Name: Memi\n- Version: 1.0\n- Built: 2025\n",
35
+ "TOOLS.md": "# Tools\n\n57 个内置工具:文件操作、系统管理、网络工具、编码转换等。\n",
36
+ };
37
+ Object.entries(docs).forEach(([name, content]) => {
38
+ const p = path.join(WORKSPACE, name);
39
+ if (!fs.existsSync(p)) fs.writeFileSync(p, content);
40
+ });
41
+ };
42
+ initWorkspace();
43
+
44
+ // ─── ANSI ────────────────────────────────────────────
45
+ const A = { r:"\x1b[0m", b:"\x1b[1m", d:"\x1b[2m", g:"\x1b[90m", rk:"\x1b[31m", gk:"\x1b[32m", yk:"\x1b[33m", bk:"\x1b[34m", mk:"\x1b[35m", ck:"\x1b[36m", wk:"\x1b[37m" };
46
+ function out(...a) { process.stdout.write(a.join("")); }
47
+ function log(...a) { console.log(a.join("") + A.r); }
48
+ function ok(t) { log(A.gk + " ✓ " + t + A.r); }
49
+ function warn(t) { log(A.yk + " ⚠ " + t + A.r); }
50
+ function fail(t) { log(A.rk + " ✗ " + t + A.r); }
51
+ function head(t) { log("\n" + A.b + A.wk + " " + t + A.r); }
52
+ function pair(k, v) { log(` ${A.g}${k}${A.r} ${A.wk}${v}`); }
53
+
54
+ // ─── 配置 ────────────────────────────────────────────
55
+ function loadCfg() {
56
+ try {
57
+ if (fs.existsSync(CONFIG)) {
58
+ const raw = JSON.parse(fs.readFileSync(CONFIG, "utf8"));
59
+ // 自动升级:确保有 models + agents 结构
60
+ if (!raw.models) raw.models = { primary: raw.api1 || {}, secondary: raw.api2 || {}, tertiary: raw.api3 || {} };
61
+ if (!raw.agents) raw.agents = [{ name: "default", model: (raw.api1||raw.models?.primary||{}).model || "?", systemPrompt: "" }];
62
+ // 保留旧字段兼容
63
+ if (!raw.api1) raw.api1 = raw.models?.primary || {};
64
+ if (!raw.api2) raw.api2 = raw.models?.secondary || {};
65
+ if (!raw.api3) raw.api3 = raw.models?.tertiary || {};
66
+ return raw;
67
+ }
68
+ } catch {}
69
+ return { api1:{}, api2:{}, api3:{}, models:{primary:{}}, agents:[{name:"default",model:"?",systemPrompt:""}] };
70
+ }
71
+ function saveCfg(c) {
72
+ try {
73
+ fs.mkdirSync(path.dirname(CONFIG), { recursive: true });
74
+ // 同步 models → api1/api2/api3
75
+ if (c.models) { c.api1 = c.models.primary || {}; c.api2 = c.models.secondary || {}; c.api3 = c.models.tertiary || {}; }
76
+ fs.writeFileSync(CONFIG, JSON.stringify(c, null, 2));
77
+ // 写一份到 memi-server/
78
+ try { fs.writeFileSync(path.join(__dirname, "memi-server", "config.json"), JSON.stringify({api1:c.api1,api2:c.api2,api3:c.api3},null,2)); } catch {}
79
+ } catch {}
80
+ }
81
+
82
+ // ─── 状态 ────────────────────────────────────────────
83
+ function showStatus() {
84
+ const c = loadCfg();
85
+ const api = c.api1 || {};
86
+ head("Status");
87
+ pair("Endpoint", api.baseUrl || "—");
88
+ pair("Model", api.model || "—");
89
+ pair("API Key", api.apiKey ? "***" + api.apiKey.slice(-4) : "—");
90
+ try {
91
+ const sess = fs.readdirSync(SESSIONS).filter((f) => f.endsWith(".json"));
92
+ const sk = fs.readdirSync(SKILLS).filter((f) => f.endsWith(".json"));
93
+ pair("Sessions", sess.length + " saved");
94
+ pair("Skills", sk.length + " local");
95
+ } catch {}
96
+ log("");
97
+ }
98
+
99
+ // ─── 新手引导 ────────────────────────────────────────
100
+ async function onboard() {
101
+ log(A.mk + A.b + "\n ╔══════════════════════════════════════════╗");
102
+ log(A.mk + A.b + " ║ 🦞 Memi Agent · Setup ║");
103
+ log(A.mk + A.b + " ║ v1.0 · 63 tools · 12 steps ║");
104
+ log(A.mk + A.b + " ╚══════════════════════════════════════════╝\n");
105
+ log(A.g + " 此引导将按以下顺序完成初始化配置。");
106
+ log(A.g + " Ctrl+C 任意步骤可退出,已完成配置会自动保存。\n");
107
+
108
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
109
+ const ask = (q, d, hidden = false) => new Promise((r) => {
110
+ const prompt = A.g + " > " + q + (d && !hidden ? A.g + ` [${d}]` : "") + ": " + A.r;
111
+ rl.question(prompt, (a) => {
112
+ const val = a.trim();
113
+ if (hidden && !val) { r(""); return; } // 空输入不覆盖旧值
114
+ r(val || d || "");
115
+ });
116
+ });
117
+ const choose = async (q, opts) => {
118
+ if (opts.length <= 10) {
119
+ log(A.g + " " + q);
120
+ opts.forEach((o, i) => log(` ${A.g}${i+1}.${A.r} ${A.wk}${o}${A.r}`));
121
+ const n = await ask("序号或搜索关键词", "1");
122
+ if (isNaN(parseInt(n))) {
123
+ const f = opts.filter(o => o.toLowerCase().includes(n.toLowerCase()));
124
+ if (f.length === 0) { warn("无匹配"); return opts[0]; }
125
+ log(A.g + " 匹配 " + f.length + " 项:");
126
+ f.forEach((o, i) => log(` ${A.g}${i+1}.${A.r} ${A.wk}${o}${A.r}`));
127
+ const m = await ask("序号", "1");
128
+ return f[parseInt(m)-1] || f[0];
129
+ }
130
+ return opts[parseInt(n)-1] || opts[0];
131
+ }
132
+ // 大列表:交互式带箭头上键选择
133
+ return new Promise((resolve) => {
134
+ const PS = 10;
135
+ let cur = 0, page = 0, input = "", filtered = opts;
136
+ const totalPages = Math.ceil(filtered.length / PS);
137
+
138
+ const draw = () => {
139
+ console.clear();
140
+ log(A.b + A.wk + " " + q + A.r);
141
+ log(A.d + " ↑↓选择 搜索 数字 Enter确认 Esc取消" + A.r);
142
+ const start = page * PS;
143
+ const subset = filtered.slice(start, start + PS);
144
+ subset.forEach((o, i) => {
145
+ const mark = (start + i === cur) ? A.b + A.gk + " ▶" + A.r : " ";
146
+ log(mark + ` ${A.g}${start+i+1}.${A.r} ${A.wk}${o}${A.r}`);
147
+ });
148
+ if (input) log(A.g + ` 搜索: ${A.wk}${input}${A.r}`);
149
+ log(A.d + ` [${page+1}/${totalPages}] 第${cur+1}项` + A.r);
150
+ };
151
+
152
+ // 先画初始界面再进入 raw mode
153
+ draw();
154
+ // (draw() already called above)
155
+
156
+ const wasRaw = process.stdin.isRaw;
157
+ process.stdin.setRawMode(true);
158
+ process.stdin.resume();
159
+
160
+ const onData = (key) => {
161
+ const s = key.toString();
162
+ if (s === "\u001b[A") { // Up
163
+ cur = Math.max(0, cur - 1);
164
+ if (cur < page * PS) { page = Math.max(0, page - 1); cur = page * PS + PS - 1; }
165
+ draw();
166
+ } else if (s === "\u001b[B") { // Down
167
+ cur = Math.min(filtered.length - 1, cur + 1);
168
+ if (cur >= (page + 1) * PS) { page = Math.min(totalPages - 1, page + 1); cur = page * PS; }
169
+ draw();
170
+ } else if (s === "\r" || s === "\n") { // Enter
171
+ if (input && isNaN(parseInt(input))) {
172
+ filtered = opts.filter(o => o.toLowerCase().includes(input.toLowerCase()));
173
+ input = ""; cur = 0; page = 0;
174
+ if (filtered.length === 0) { filtered = opts; }
175
+ draw();
176
+ return;
177
+ }
178
+ const num = parseInt(input) || (cur + 1);
179
+ cleanup(); process.stdout.write("\n"); resolve(filtered[Math.min(num - 1, filtered.length - 1)] || filtered[0]);
180
+ } else if (s === "\x03") { cleanup(); process.exit(0); } // Ctrl+C
181
+ else if (s === "\x1b") { // Esc
182
+ const num = parseInt(input);
183
+ cleanup(); process.stdout.write("\n"); resolve(filtered[num - 1] || filtered[0] || opts[0]);
184
+ } else if (s === "\b" || s === "\x7f") { // Backspace
185
+ input = input.slice(0, -1); draw();
186
+ } else if (s.length === 1 && s >= " ") {
187
+ input += s;
188
+ const match = filtered[cur] && filtered[cur].toLowerCase().includes(input.toLowerCase());
189
+ if (!match) {
190
+ filtered = opts.filter(o => o.toLowerCase().includes(input.toLowerCase()));
191
+ if (filtered.length === 0) { input = input.slice(0, -1); draw(); return; }
192
+ cur = 0; page = 0;
193
+ }
194
+ draw();
195
+ }
196
+ };
197
+
198
+ const cleanup = () => {
199
+ process.stdin.removeListener("data", onData);
200
+ process.stdin.setRawMode(wasRaw);
201
+ };
202
+
203
+ process.stdin.on("data", onData);
204
+ });
205
+ };
206
+ const confirm = async (q) => { const a = await ask(q + " (y/n)", "y"); return a.toLowerCase() === "y"; };
207
+ // 进度条
208
+ const progress = (current, total, label) => {
209
+ const w = 20; const p = Math.round((current / total) * w);
210
+ const bar = A.gk + "█".repeat(p) + A.g + "░".repeat(w - p) + A.r;
211
+ readline.clearLine(process.stdout, 0); readline.cursorTo(process.stdout, 0);
212
+ out(A.g + ` ${label || "进度"}: ${bar} ${current}/${total}` + A.r);
213
+ };
214
+
215
+ // 扫描 OpenClaw skills
216
+ const openclawSkillsDir = path.join(require("os").homedir(), ".openclaw", "skills");
217
+ let openclawSkillsFound = 0;
218
+ try { if (fs.existsSync(openclawSkillsDir)) {
219
+ openclawSkillsFound = fs.readdirSync(openclawSkillsDir, { withFileTypes: true })
220
+ .filter(d => d.isDirectory() && fs.existsSync(path.join(openclawSkillsDir, d.name, "SKILL.md"))).length;
221
+ }} catch {}
222
+
223
+ // 检测 OpenClaw 配置并自动导入
224
+ const openclawConfig = (() => {
225
+ try {
226
+ const ocPath = path.join(require("os").homedir(), ".openclaw", "openclaw.json");
227
+ if (fs.existsSync(ocPath)) {
228
+ const oc = JSON.parse(fs.readFileSync(ocPath, "utf8"));
229
+ const models = oc.models || {};
230
+ const firstModel = Object.values(models)[0] || {};
231
+ return {
232
+ baseUrl: firstModel.baseURL || firstModel.baseUrl || "https://api.openai.com/v1",
233
+ apiKey: firstModel.apiKey || "",
234
+ model: firstModel.model || (oc.agents?.[0]?.model) || "gpt-4o",
235
+ };
236
+ }
237
+ } catch {}
238
+ return null;
239
+ })();
240
+
241
+ if (openclawConfig) {
242
+ log(A.mk + " 🔍 检测到 OpenClaw 配置!\n");
243
+ log(A.g + ` Model: ${A.wk}${openclawConfig.model}${A.r}`);
244
+ log(A.g + ` Base URL: ${A.wk}${openclawConfig.baseUrl}${A.r}`);
245
+ log(A.g + ` API Key: ${A.wk}${openclawConfig.apiKey ? "已配置 (***" + openclawConfig.apiKey.slice(-4) + ")" : "未配置"}${A.r}`);
246
+ const use = await confirm("是否导入 OpenClaw 配置?");
247
+ if (use) {
248
+ cfg.api1 = cfg.api1 || {};
249
+ cfg.api1.baseUrl = openclawConfig.baseUrl;
250
+ cfg.api1.model = openclawConfig.model;
251
+ cfg.api1.apiKey = openclawConfig.apiKey || cfg.api1.apiKey;
252
+ saveCfg(cfg);
253
+ ok("配置已导入");
254
+ // 导入 skills
255
+ if (openclawSkillsFound > 0) {
256
+ const impSkills = await confirm(`检测到 ${openclawSkillsFound} 个 OpenClaw skill,是否导入?`);
257
+ if (impSkills) {
258
+ let imported = 0;
259
+ try {
260
+ const ocSkills = fs.readdirSync(openclawSkillsDir, { withFileTypes: true }).filter(d => d.isDirectory());
261
+ const total = ocSkills.length;
262
+ for (let i = 0; i < ocSkills.length; i++) {
263
+ const d = ocSkills[i];
264
+ const src = path.join(openclawSkillsDir, d.name);
265
+ const dest = path.join(SKILLS, d.name);
266
+ progress(i + 1, total, "导入 OpenClaw skill");
267
+ if (!fs.existsSync(dest)) { fs.cpSync(src, dest, { recursive: true }); imported++; }
268
+ }
269
+ log("");
270
+ ok(`已导入 ${imported}/${total} 个 skill`);
271
+ } catch { log(""); warn("skill 导入失败"); }
272
+ }
273
+ }
274
+ if (openclawConfig.apiKey) { rl.close(); return; }
275
+ }
276
+ }
277
+
278
+ const cfg = loadCfg();
279
+ cfg.api1 = cfg.api1 || {};
280
+ cfg.api2 = cfg.api2 || {};
281
+ cfg.api3 = cfg.api3 || {};
282
+
283
+ // ── 步骤 0:环境检查 ──
284
+ head("步骤 1/12 · 环境检查");
285
+ log(A.g + " 正在检查运行环境...\n");
286
+ const { execSync } = require("child_process");
287
+ const checks = [];
288
+
289
+ // Node.js
290
+ checks.push({ name: "Node.js", ok: true, ver: process.version });
291
+
292
+ // npm 依赖
293
+ const npmDeps = ["axios", "express", "ws", "cors"];
294
+ const serverNM = path.join(__dirname, "memi-server", "node_modules");
295
+ const missingNpm = npmDeps.filter(d => !fs.existsSync(path.join(serverNM, d)));
296
+ checks.push({ name: "npm 依赖", ok: missingNpm.length === 0, ver: missingNpm.length ? "缺 " + missingNpm.join(",") : "已安装" });
297
+
298
+ // Git
299
+ try { const v = execSync("git --version", { encoding: "utf8", timeout: 5000 }).trim(); checks.push({ name: "Git", ok: true, ver: v }); }
300
+ catch { checks.push({ name: "Git", ok: false, ver: "未安装 — 版本控制需要" }); }
301
+
302
+ // Python
303
+ try { const v = execSync("python --version 2>&1 || python3 --version 2>&1", { encoding: "utf8", shell: true, timeout: 5000 }).trim(); checks.push({ name: "Python", ok: true, ver: v }); }
304
+ catch { checks.push({ name: "Python", ok: false, ver: "未安装 — 脚本执行需要" }); }
305
+
306
+ // build tools (Windows: Visual Studio / Linux: build-essential)
307
+ if (process.platform === "win32") {
308
+ try { execSync("where nmake 2>nul || where msbuild 2>nul || echo notfound", { shell: true, timeout: 5000 }); checks.push({ name: "C++ 构建工具", ok: true, ver: "已安装" }); }
309
+ catch { checks.push({ name: "C++ 构建工具", ok: false, ver: "未安装 — 编译 native 模块需要" }); }
310
+ } else {
311
+ try { execSync("which make && which gcc", { shell: true, timeout: 5000 }); checks.push({ name: "build-essential", ok: true, ver: "已安装" }); }
312
+ catch { checks.push({ name: "build-essential", ok: false, ver: "未安装 — sudo apt install build-essential" }); }
313
+ }
314
+
315
+ checks.forEach(c => {
316
+ log(` ${c.ok ? A.gk + "✓" + A.r : A.yk + "⚠" + A.r} ${A.g}${c.name}${A.r} ${c.ok ? A.d : A.yk}${c.ver}${A.r}`);
317
+ });
318
+
319
+ const failed = checks.filter(c => !c.ok);
320
+ if (failed.length > 0) {
321
+ log(A.yk + "\n " + failed.length + " 项未就绪" + A.r);
322
+ if (missingNpm.length > 0) {
323
+ const doInstall = await confirm("自动安装 npm 依赖?");
324
+ if (doInstall) {
325
+ try {
326
+ execSync("npm install", { cwd: path.join(__dirname, "memi-server"), stdio: "pipe" });
327
+ ok("npm 依赖已安装");
328
+ } catch { warn("安装失败,请手动: npm install --prefix memi-server"); }
329
+ }
330
+ }
331
+ } else {
332
+ ok("所有环境已就绪");
333
+ }
334
+
335
+ // ── 步骤 1:工作区 ──
336
+ head("步骤 2/12 · 工作区路径");
337
+ log(A.g + ` 当前: ${A.wk}${DIR}${A.r}`);
338
+ const customDir = await ask("工作区路径(回车使用默认)", "");
339
+ if (customDir && customDir !== DIR) {
340
+ const newDir = path.resolve(customDir);
341
+ log(A.g + " 将使用: " + A.wk + newDir + A.r);
342
+ // 注意:此阶段还不能切换 DIR,因为后续步骤依赖它。提示后续生效。
343
+ log(A.g + " 配置将在所有步骤完成后迁移");
344
+ cfg.workspaceDir = newDir;
345
+ }
346
+
347
+ head("步骤 2/11 · 选择 AI 提供商");
348
+ const provider = await choose("选择 AI 提供商:", [
349
+ "OpenAI", "OpenAI-API", "OpenAI-Proxy", "OpenAI-SB", "CloseAI", "API2D", "OhMyGPT", "GPT-API", "AIGC2D",
350
+ "Anthropic", "Google Gemini", "Google Vertex AI", "Mistral AI", "Cohere", "AI21 Labs",
351
+ "xAI Grok", "Meta Llama", "Amazon Bedrock", "IBM Watsonx", "Aleph Alpha",
352
+ "Together AI", "Fireworks AI", "Groq", "OpenRouter", "Perplexity",
353
+ "Replicate", "DeepInfra", "Hugging Face", "NVIDIA NIM", "Cloudflare Workers AI",
354
+ "OctoAI", "Anyscale", "Lepton AI", "RunPod", "BentoML",
355
+ "DeepSeek", "DeepSeek(欧派中转)", "火山方舟", "智谱 AI", "阿里云百炼",
356
+ "腾讯云 TI-ONE", "百度文心一言", "讯飞星火", "腾讯混元", "字节豆包",
357
+ "Kimi", "MiniMax", "百川智能", "零一万物", "StepFun", "非线智能",
358
+ "商汤日日新", "云从科技", "昆仑万维", "面壁智能", "猎户星空",
359
+ "硅基流动", "AIHubMix", "MOMA", "OneAPI", "NewAPI",
360
+ "shturl", "诗云 API", "DMXAPI", "AI-API", "AI.LS", "Oaipro", "V2Gpt",
361
+ "Ollama", "LM Studio", "llama.cpp", "vLLM", "LocalAI", "TGI", "GPT4All", "KoboldCpp",
362
+ "自定义",
363
+ ]);
364
+ // 提供商URL + 默认模型映射表
365
+ const PROVIDERS = {
366
+ "OpenAI":{url:"https://api.openai.com/v1",models:"gpt-4o,gpt-4-turbo,gpt-4o-mini,o1,o3-mini,o3".split(",")},
367
+ "Anthropic":{url:"https://api.anthropic.com/v1",models:"claude-sonnet-4-20250514,claude-3-5-sonnet,claude-3-5-haiku".split(",")},
368
+ "Google Gemini":{url:"https://generativelanguage.googleapis.com/v1beta/openai",models:"gemini-2.5-pro,gemini-2.0-flash".split(",")},
369
+ "Mistral AI":{url:"https://api.mistral.ai/v1",models:"mistral-large-latest,mistral-small-latest,codestral-latest".split(",")},
370
+ "Cohere":{url:"https://api.cohere.ai/v1",models:"command-r-plus,command-r".split(",")},
371
+ "AI21 Labs":{url:"https://api.ai21.com/studio/v1",models:"jamba-1.5-large,jamba-1.5-mini".split(",")},
372
+ "Together AI":{url:"https://api.together.xyz/v1",models:"meta-llama/Llama-3.3-70B-Instruct-Turbo".split(",")},
373
+ "Fireworks AI":{url:"https://api.fireworks.ai/inference/v1",models:"accounts/fireworks/models/llama-v3p1-70b-instruct".split(",")},
374
+ "Groq":{url:"https://api.groq.com/openai/v1",models:"llama-3.3-70b-versatile,llama-3.1-8b-instant".split(",")},
375
+ "OpenRouter":{url:"https://openrouter.ai/api/v1",models:"openai/gpt-4o,anthropic/claude-sonnet,google/gemini-flash".split(",")},
376
+ "火山方舟":{url:"https://ark.cn-beijing.volces.com/api/v3",models:"doubao-pro-128k,doubao-lite-128k".split(",")},
377
+ "智谱 AI":{url:"https://open.bigmodel.cn/api/paas/v4",models:"glm-4-flash,glm-4-plus,glm-4-air".split(",")},
378
+ "阿里云百炼":{url:"https://dashscope.aliyuncs.com/compatible-mode/v1",models:"qwen-plus,qwen-max,qwen-turbo".split(",")},
379
+ "腾讯云 TI-ONE":{url:"https://api.hunyuan.cloud.tencent.com/v1",models:"hunyuan-lite".split(",")},
380
+ "百度文心一言":{url:"https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat",models:"ernie-speed-128k".split(",")},
381
+ "DeepSeek":{url:"https://api.deepseek.com/v1",models:"deepseek-chat,deepseek-reasoner,deepseek-v4-pro,deepseek-v4-flash".split(",")},
382
+ "Kimi":{url:"https://api.moonshot.cn/v1",models:"moonshot-v1-8k,moonshot-v1-32k,moonshot-v1-128k".split(",")},
383
+ "MiniMax":{url:"https://api.minimax.chat/v1",models:"abab6.5s-chat,abab5.5-chat".split(",")},
384
+ "百川智能":{url:"https://api.baichuan-ai.com/v1",models:"Baichuan4,Baichuan3-Turbo".split(",")},
385
+ "非线智能":{url:"https://api.feixian.ai/v1",models:"model".split(",")},
386
+ "硅基流动":{url:"https://api.siliconflow.cn/v1",models:"Qwen/Qwen2.5-7B-Instruct,deepseek-ai/DeepSeek-V3".split(",")},
387
+ "MOMA":{url:"https://api.momayun.com/v1",models:"gpt-4o".split(",")},
388
+ "OneAPI":{url:"https://your-oneapi.com/v1",models:"all".split(",")},
389
+ "NewAPI":{url:"https://api.newapi.com/v1",models:"all".split(",")},
390
+ "shturl":{url:"https://api.shturl.com/v1",models:"gpt-4o".split(",")},
391
+ "诗云 API":{url:"https://api.shiyunapi.com/v1",models:"gpt-4o".split(",")},
392
+ "DMXAPI":{url:"https://api.dmxapi.com/v1",models:"gpt-4o".split(",")},
393
+ "Ollama":{url:"http://localhost:11434/v1",models:"llama3:8b,qwen2.5:7b".split(",")},
394
+ "llama.cpp":{url:"http://localhost:8081/v1",models:"model".split(",")},
395
+ "vLLM":{url:"http://localhost:8000/v1",models:"model".split(",")},
396
+ "TGI":{url:"http://localhost:8080/v1",models:"model".split(",")},
397
+ "OpenAI-API":{url:"https://api.openai.com/v1",models:"gpt-4o".split(",")},
398
+ "OpenAI-Proxy":{url:"https://api.openai-proxy.com/v1",models:"all".split(",")},
399
+ "OpenAI-SB":{url:"https://api.openai-sb.com/v1",models:"all".split(",")},
400
+ "CloseAI":{url:"https://api.closeai-asia.com/v1",models:"all".split(",")},
401
+ "API2D":{url:"https://api.api2d.com/v1",models:"all".split(",")},
402
+ "OhMyGPT":{url:"https://api.ohmygpt.com/v1",models:"all".split(",")},
403
+ "GPT-API":{url:"https://api.gpt-api.com/v1",models:"all".split(",")},
404
+ "AIGC2D":{url:"https://api.aigc2d.com/v1",models:"all".split(",")},
405
+ "Google Vertex AI":{url:"https://REGION-aiplatform.googleapis.com/v1",models:"gemini-pro".split(",")},
406
+ "xAI Grok":{url:"https://api.x.ai/v1",models:"grok-2,grok-2-mini".split(",")},
407
+ "Meta Llama":{url:"https://api.llama-api.com",models:"llama3.1-70b".split(",")},
408
+ "Amazon Bedrock":{url:"https://bedrock-runtime.us-east-1.amazonaws.com",models:"anthropic.claude-3-sonnet".split(",")},
409
+ "IBM Watsonx":{url:"https://us-south.ml.cloud.ibm.com/ml/v1/text/generation",models:"ibm/granite-13b-chat-v2".split(",")},
410
+ "Aleph Alpha":{url:"https://api.aleph-alpha.com/v1",models:"luminous-base".split(",")},
411
+ "Perplexity":{url:"https://api.perplexity.ai",models:"llama-3.1-sonar-large-128k-online".split(",")},
412
+ "Replicate":{url:"https://api.replicate.com/v1",models:"meta/meta-llama-3-70b-instruct".split(",")},
413
+ "DeepInfra":{url:"https://api.deepinfra.com/v1/openai",models:"meta-llama/Meta-Llama-3.1-70B-Instruct".split(",")},
414
+ "Hugging Face":{url:"https://api-inference.huggingface.co/v1",models:"meta-llama/Meta-Llama-3-8B-Instruct".split(",")},
415
+ "NVIDIA NIM":{url:"https://integrate.api.nvidia.com/v1",models:"meta/llama-3.1-70b-instruct".split(",")},
416
+ "Cloudflare Workers AI":{url:"https://api.cloudflare.com/client/v4/accounts/YOUR_ID/ai/v1",models:"@cf/meta/llama-3-8b-instruct".split(",")},
417
+ "OctoAI":{url:"https://text.octoai.run/v1",models:"meta-llama-3.1-70b-instruct".split(",")},
418
+ "Anyscale":{url:"https://api.endpoints.anyscale.com/v1",models:"meta-llama/Llama-3-70b-chat-hf".split(",")},
419
+ "Lepton AI":{url:"https://api.lepton.ai/v1",models:"llama3-70b".split(",")},
420
+ "RunPod":{url:"https://api.runpod.ai/v2/YOUR_ENDPOINT/openai/v1",models:"model".split(",")},
421
+ "BentoML":{url:"https://api.bentoml.com/v1",models:"model".split(",")},
422
+ "DeepSeek(欧派中转)":{url:"https://api.deepseek.com/v1",models:"deepseek-chat".split(",")},
423
+ "讯飞星火":{url:"https://spark-api-open.xf-yun.com/v1",models:"generalv3.5".split(",")},
424
+ "腾讯混元":{url:"https://api.hunyuan.cloud.tencent.com/v1",models:"hunyuan-lite".split(",")},
425
+ "字节豆包":{url:"https://ark.cn-beijing.volces.com/api/v3",models:"doubao-lite-128k".split(",")},
426
+ "零一万物":{url:"https://api.lingyiwanwu.com/v1",models:"yi-large,yi-medium".split(",")},
427
+ "StepFun":{url:"https://api.stepfun.com/v1",models:"step-1-8k,step-1-32k".split(",")},
428
+ "商汤日日新":{url:"https://api.sensenova.cn/v1",models:"SenseChat-5".split(",")},
429
+ "云从科技":{url:"https://api.cloudwalk.com/v1",models:"model".split(",")},
430
+ "昆仑万维":{url:"https://api.kunlun.com/v1",models:"model".split(",")},
431
+ "面壁智能":{url:"https://api.modelbest.cn/v1",models:"MiniCPM".split(",")},
432
+ "猎户星空":{url:"https://api.orionstar.com/v1",models:"model".split(",")},
433
+ "AIHubMix":{url:"https://aihubmix.com/v1",models:"all".split(",")},
434
+ "AI-API":{url:"https://api.ai-api.com/v1",models:"all".split(",")},
435
+ "AI.LS":{url:"https://api.ai.ls/v1",models:"all".split(",")},
436
+ "Oaipro":{url:"https://api.oaipro.com/v1",models:"all".split(",")},
437
+ "V2Gpt":{url:"https://api.v2gpt.com/v1",models:"all".split(",")},
438
+ "LM Studio":{url:"http://localhost:1234/v1",models:"local-model".split(",")},
439
+ "LocalAI":{url:"http://localhost:8080/v1",models:"model".split(",")},
440
+ "GPT4All":{url:"http://localhost:4891/v1",models:"model".split(",")},
441
+ "KoboldCpp":{url:"http://localhost:5001/v1",models:"model".split(",")},
442
+ };
443
+ let modelChoices = [];
444
+ if (PROVIDERS[provider]) {
445
+ cfg.api1.baseUrl = PROVIDERS[provider].url;
446
+ modelChoices = PROVIDERS[provider].models;
447
+ } else if (provider.startsWith("──")) {
448
+ warn("请选择提供商,不是分类标题"); return;
449
+ } else {
450
+ cfg.api1.baseUrl = await ask("Base URL", cfg.api1.baseUrl);
451
+ cfg.api1.model = await ask("Model", cfg.api1.model || "deepseek-chat");
452
+ }
453
+
454
+ // ── 步骤 2:模型 ──
455
+ head("步骤 2/8 · 模型选择");
456
+ // 中转站给通用热门模型列表
457
+ if (modelChoices.length === 1 && (modelChoices[0] === "all" || modelChoices[0] === "model")) {
458
+ modelChoices = "gpt-4o,gpt-4o-mini,gpt-3.5-turbo,claude-3-5-sonnet,gemini-2.0-flash,deepseek-chat,deepseek-reasoner,qwen-plus,glm-4-flash".split(",");
459
+ }
460
+ if (modelChoices.length > 1) {
461
+ const chosen = await choose("选择模型:", modelChoices);
462
+ cfg.api1.model = chosen.trim();
463
+ } else if (modelChoices.length === 1) {
464
+ cfg.api1.model = modelChoices[0];
465
+ log(A.g + " 默认模型: " + A.wk + cfg.api1.model + A.r);
466
+ } else {
467
+ cfg.api1.model = await ask("模型名称", cfg.api1.model || "gpt-4o");
468
+ }
469
+ const keyInput = await ask("API Key(输入不可见,直接回车保持不变)", "", true);
470
+ if (keyInput) cfg.api1.apiKey = keyInput;
471
+
472
+ // ── 步骤 3:生图 API(可选)─
473
+ // ── Agent 身份 ──
474
+ head("步骤 3/10 · Agent 身份");
475
+ cfg.agents = cfg.agents || [];
476
+ if (cfg.agents.length > 0) {
477
+ const aName = await ask("Agent 名称", cfg.agents[0].name || "default");
478
+ const aPrompt = await ask("系统提示词(回车跳过)", cfg.agents[0].systemPrompt || "");
479
+ cfg.agents[0] = { name: aName, model: cfg.api1.model, systemPrompt: aPrompt };
480
+ }
481
+
482
+ // ── 搜索引擎 ──
483
+ head("步骤 4/10 · 搜索引擎");
484
+ const se = await choose("默认搜索引擎:", ["DuckDuckGo + Bing(自动切换)", "仅 DuckDuckGo", "仅 Bing"]);
485
+ cfg.searchEngine = ["auto", "ddg", "bing"][["DuckDuckGo", "仅", "仅"].findIndex(k => se.includes(k))] || "auto";
486
+ ok("搜索引擎: " + se);
487
+
488
+ // ── 网关端口 ──
489
+ head("步骤 5/10 · 网关端口");
490
+ const port = await ask("网关端口", String(process.env.PORT || 3001));
491
+ cfg.gatewayPort = parseInt(port) || 3001;
492
+ ok("端口: " + cfg.gatewayPort);
493
+
494
+ head("步骤 6/10 · 消息渠道(可选)");
495
+ log(A.g + " Memi 支持在聊天 App 里使用 AI:");
496
+ log(A.g + " " + A.wk + "Telegram / 飞书 / 企业微信 / QQ" + A.r);
497
+ const wantChannel = await confirm("是否配置消息渠道?");
498
+ if (wantChannel) {
499
+ const ch = await choose("选择渠道:", ["Telegram", "飞书", "企业微信", "QQ"]);
500
+ const ngrokUrl = "https://pyromania-strenuous-sinuous.ngrok-free.dev";
501
+ if (ch === "Telegram") {
502
+ const tToken = await ask("Telegram Bot Token (@BotFather 获取)", "");
503
+ if (tToken) {
504
+ try {
505
+ await fetch(`http://localhost:3001/api/gateway/telegram/${tToken}/setup`, {
506
+ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ baseUrl: ngrokUrl })
507
+ });
508
+ ok("Telegram webhook 已注册");
509
+ } catch { warn("注册失败,稍后运行 memi telegram " + tToken); }
510
+ }
511
+ } else if (ch === "飞书") {
512
+ log(A.gk + " ✓ Feishu 端点就绪" + A.r);
513
+ log(A.g + " 回调 URL: " + A.wk + ngrokUrl + "/api/gateway/feishu" + A.r);
514
+ log(A.g + " 请在飞书开放平台配置此地址");
515
+ } else if (ch === "企业微信") {
516
+ const wcKey = await ask("企业微信机器人 Webhook Key", "");
517
+ log(A.gk + " ✓ WeCom 端点就绪" + A.r);
518
+ log(A.g + " Webhook: " + A.wk + ngrokUrl + "/api/gateway/wecom/" + (wcKey || "KEY") + A.r);
519
+ } else if (ch === "QQ") {
520
+ const qqToken = await ask("QQ Bot Token", "");
521
+ log(A.gk + " ✓ QQ 端点就绪" + A.r);
522
+ log(A.g + " Webhook: " + A.wk + ngrokUrl + "/api/gateway/qq/" + (qqToken || "TOKEN") + A.r);
523
+ }
524
+ }
525
+
526
+ head("步骤 7/11 · 图片生成 API(可选)");
527
+ const wantImage = await confirm("是否配置生图 API?");
528
+ if (wantImage) {
529
+ cfg.api2.baseUrl = await ask("Base URL", cfg.api2.baseUrl || "https://api.deepseek.com/v1");
530
+ cfg.api2.model = await ask("Model", cfg.api2.model || "deepseek-chat");
531
+ const keyInput2 = await ask("API Key(输入不可见,直接回车保持不变)", "", true);
532
+ if (keyInput2) cfg.api2.apiKey = keyInput2;
533
+ ok("生图 API 已配置");
534
+ }
535
+
536
+ // ── 步骤 4:工作区 ──
537
+ head("步骤 8/11 · 工作区与技能");
538
+ log(A.g + ` 配置目录: ${A.wk}${DIR}${A.r}`);
539
+ log(A.g + ` 技能目录: ${A.wk}${SKILLS}${A.r}`);
540
+ log(A.g + ` 会话目录: ${A.wk}${SESSIONS}${A.r}`);
541
+ const wantSkills = await confirm("是否安装内置示例技能?");
542
+ if (wantSkills) {
543
+ try {
544
+ const examples = [
545
+ { name:"翻译助手", type:"llm", description:"中英互译", promptTemplate:"翻译以下内容: {input}" },
546
+ { name:"代码审查", type:"llm", description:"审查代码并给出建议", promptTemplate:"Review this code and suggest improvements: {input}" },
547
+ { name:"周报生成", type:"llm", description:"从工作记录生成周报", promptTemplate:"根据以下工作记录生成周报: {input}" },
548
+ ];
549
+ examples.forEach((s, i) => {
550
+ fs.writeFileSync(path.join(SKILLS, "example-" + (i + 1) + ".json"), JSON.stringify(s, null, 2));
551
+ });
552
+ ok(`已安装 ${examples.length} 个示例技能`);
553
+ } catch { warn("技能安装失败"); }
554
+ }
555
+
556
+ // ── 步骤 5:连接测试 ──
557
+ head("步骤 9/11 · 连接测试");
558
+ out(A.g + " 测试中... ");
559
+ try {
560
+ const r = await fetch("http://localhost:3001/api/v1/chat/completions", {
561
+ method: "POST", headers: { "Content-Type": "application/json" },
562
+ body: JSON.stringify({ model: cfg.api1.model, messages: [{ role: "user", content: "Hi" }], max_tokens: 5 }),
563
+ });
564
+ log(r.ok ? A.gk + "✓ 已连接" + A.r : A.rk + "✗ HTTP " + r.status + A.r);
565
+ } catch { log(A.rk + "✗ 无法连接 (memi-server 已启动?)" + A.r); }
566
+
567
+ // ── 步骤 6:完成 ──
568
+ // ── 守护进程 ──
569
+ head("步骤 10/11 · 守护进程");
570
+ log(A.g + " 开机自动启动 Memi 服务,无需手动 npm start");
571
+ const wantDaemon = await confirm("是否安装守护进程?");
572
+ if (wantDaemon) {
573
+ const osType = process.platform;
574
+ const serverScript = path.join(__dirname, "memi-server", "index.js");
575
+ if (osType === "win32") {
576
+ try {
577
+ require("child_process").execSync(`schtasks /create /tn "MemiAgent" /tr "node \\"${serverScript}\\"" /sc onstart /f`, { shell: true });
578
+ ok("已安装(schtasks)");
579
+ } catch {
580
+ try {
581
+ const startupDir = path.join(require("os").homedir(), "AppData", "Roaming", "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
582
+ fs.writeFileSync(path.join(startupDir, "memi-server.bat"), `@echo off\ncd /d "${path.join(__dirname, "memi-server")}"\nnode index.js\n`);
583
+ ok("已安装(启动文件夹)");
584
+ } catch { warn("安装失败,可稍后运行 memi daemon install"); }
585
+ }
586
+ } else {
587
+ log(A.g + " 请稍后手动运行: memi daemon install");
588
+ }
589
+ }
590
+
591
+ head("步骤 12/12 · 配置完成");
592
+ saveCfg(cfg);
593
+ fs.writeFileSync(ONBOARDED, "{}");
594
+
595
+ log(A.b + A.gk + "\n ✓ 配置完成!\n" + A.r);
596
+ log(A.g + " ──────────────────────────────────────");
597
+ pair("AI 模型", cfg.api1.model || "—");
598
+ pair(" Base URL", cfg.api1.baseUrl || "—");
599
+ pair(" API Key", cfg.api1.apiKey ? "***" + cfg.api1.apiKey.slice(-4) : "—");
600
+ pair("思考强度", cfg.thinkLevel || "high");
601
+ pair("会话存储", SESSIONS);
602
+ pair("技能目录", SKILLS);
603
+ log(A.g + " ──────────────────────────────────────");
604
+
605
+ log(A.b + "\n Quick Start:\n");
606
+ log(` ${A.ck}memi chat${A.r} 进入智能对话`);
607
+ log(` ${A.ck}memi status${A.r} 查看系统状态`);
608
+ log(` ${A.ck}memi skills${A.r} 查看已安装技能`);
609
+ log(` ${A.ck}memi dashboard${A.r} 打开网页管理面板`);
610
+ log(` ${A.ck}memi doctor${A.r} 运行系统诊断`);
611
+ log(` ${A.ck}memi update${A.r} 检查版本更新`);
612
+ log(` ${A.ck}memi help${A.r} 查看更多命令`);
613
+ log(` ${A.ck}memi onboard${A.r} 重新运行本引导`);
614
+ log("");
615
+ const goChat = await confirm("是否进入对话?");
616
+ rl.close();
617
+ if (goChat) { console.clear(); await chat(); return; }
618
+ }
619
+
620
+ // ─── 聊天 ────────────────────────────────────────────
621
+ async function chat() {
622
+ let msgs = [], session = "default", cfg = loadCfg(), currency = cfg.currency || "¥", apiBalance = "—";
623
+ // 多智能体支持
624
+ let agents = cfg.agents || [{ name: "default", model: (cfg.api1 && cfg.api1.model) || "?", systemPrompt: "" }];
625
+ let currentAgent = agents[0]?.name || "default";
626
+ if (!cfg.agents) { cfg.agents = agents; saveCfg(cfg); }
627
+
628
+ const md = (t) => {
629
+ // 表格检测与渲染
630
+ const lines = t.split("\n");
631
+ const out = [];
632
+ for (let i = 0; i < lines.length; i++) {
633
+ const line = lines[i];
634
+ if (!line.trim().startsWith("|")) { out.push(line); continue; }
635
+ // 收集连续表格行
636
+ const table = [line];
637
+ while (i + 1 < lines.length && lines[i + 1].trim().startsWith("|")) table.push(lines[++i]);
638
+ if (table.length < 2) { out.push(line); continue; }
639
+ // 解析列
640
+ const rows = table.map((r) => r.split("|").slice(1, -1).map((c) => c.trim()));
641
+ const widths = rows[0].map((_, ci) => Math.max(...rows.map((r) => (r[ci] || "").replace(/[\u4e00-\u9fff]/g, "XX").length)));
642
+ // 顶线
643
+ out.push(A.g + " +" + widths.map((w) => "-".repeat(w + 2)).join("+") + "+" + A.r);
644
+ rows.forEach((row, ri) => {
645
+ const cells = row.map((c, ci) => {
646
+ const w = (c || "").replace(/[\u4e00-\u9fff]/g, "XX").length;
647
+ return " " + c + " ".repeat(widths[ci] - w + 1);
648
+ });
649
+ if (ri === 0) out.push(A.b + A.g + " |" + cells.join("|") + "|" + A.r);
650
+ else out.push(A.g + " |" + A.wk + cells.join(A.g + "|" + A.wk) + A.g + "|" + A.r);
651
+ if (ri === 0) out.push(A.g + " +" + widths.map((w) => "-".repeat(w + 2)).join("+") + "+" + A.r);
652
+ });
653
+ // 底线
654
+ out.push(A.g + " +" + widths.map((w) => "-".repeat(w + 2)).join("+") + "+" + A.r);
655
+ }
656
+ return out.join("\n")
657
+ .replace(/\*\*(.+?)\*\*/g, A.b + "$1" + A.r)
658
+ .replace(/### (.+)/g, A.b + A.yk + "$1" + A.r)
659
+ .replace(/## (.+)/g, A.b + A.wk + "$1" + A.r)
660
+ .replace(/# (.+)/g, A.b + A.ck + "$1" + A.r)
661
+ .replace(/`{3}(\w*)\n?([\s\S]*?)`{3}/g, "\n" + A.d + "$2" + A.r)
662
+ .replace(/`([^`]+)`/g, A.d + "$1" + A.r);
663
+ };
664
+
665
+ async function send(input) {
666
+ msgs.push({ role: "user", content: input });
667
+ const start = Date.now();
668
+ let tools = 0, tOut = 0;
669
+
670
+ // 确保服务器在运行,不在则自动启动
671
+ let serverReady = false;
672
+ try { const h = await fetch("http://localhost:3001/health"); serverReady = h.ok; } catch {}
673
+ if (!serverReady) {
674
+ out(A.d + " 启动服务中... " + A.r);
675
+ try {
676
+ const serverPath = path.join(__dirname, "memi-server");
677
+ require("child_process").spawn("node", ["index.js"], { cwd: serverPath, detached: true, stdio: "ignore" }).unref();
678
+ await new Promise(r => setTimeout(r, 2000));
679
+ serverReady = true;
680
+ } catch {}
681
+ if (serverReady) out(A.gk + " ✓ 服务已启动\n" + A.r);
682
+ else out(A.yk + " ⚠ 启动失败,请手动: npm start --prefix memi-server\n" + A.r);
683
+ }
684
+
685
+ try {
686
+ const r = await fetch("http://localhost:3001/api/v1/chat/completions", {
687
+ method: "POST", headers: { "Content-Type": "application/json" },
688
+ body: JSON.stringify({ model: "memi-agent", messages: msgs, stream: true, thinking: cfg.thinkLevel || "high", systemPrompt: agentCfg.systemPrompt || "" }),
689
+ });
690
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
691
+
692
+ const reader = r.body.getReader();
693
+ const dec = new TextDecoder();
694
+ let full = "", buf = "", toolCalls = [];
695
+
696
+ out(A.d + " ..." + A.r);
697
+
698
+ while (true) {
699
+ const { done, value } = await reader.read();
700
+ if (done) break;
701
+ buf += dec.decode(value, { stream: true });
702
+ const lines = buf.split("\n");
703
+ buf = lines.pop() || "";
704
+ for (const line of lines) {
705
+ if (!line.startsWith("data: ")) continue;
706
+ const d = line.slice(6).trim();
707
+ if (d === "[DONE]") continue;
708
+ try {
709
+ const data = JSON.parse(d);
710
+ if (data.type === "tool_call") {
711
+ tools++;
712
+ const icons = { web_search:"🔍", list_files:"📂", read_file:"📖", write_file:"✏️", get_time:"🕐", get_location:"📍", calculate:"🧮", move_file:"📦", delete_file:"🗑", create_dir:"📁", run_command:"⚡", run_skill:"🔧", generate_image:"🎨", find_files:"🔎", search_in_files:"📋", http_request:"🌐", system_info:"🖥", clipboard_read:"📋", clipboard_write:"📝", open_url:"🔗", download_file:"⬇", zip_files:"📦", unzip:"📂", process_list:"📊", notify:"🔔", encode_decode:"🔣", git_status:"📋", git_log:"📜", random:"🎲", hash_text:"🔐", uuid:"🆔", count_text:"🔢", diff_files:"↔", disk_usage:"💾", network_info:"🌐", ping_host:"📡", dns_lookup:"🔍", sort_file:"📑", get_env:"🔧", take_screenshot:"📸", get_weather:"🌤", timer:"⏱", qr_generate:"📱", set_reminder:"📝" };
713
+ const name = (icons[data.tool] || "🔧") + " " + data.tool;
714
+ let args = ""; try { const a = typeof data.args === "string" ? JSON.parse(data.args) : data.args; args = Object.values(a || {}).join(", ").slice(0, 60); } catch { args = String(data.args || "").slice(0, 60); }
715
+ log(A.bk + " ⚙ " + name + A.r + " " + A.g + args);
716
+ if (data.result) log(A.g + " ↳ " + data.result.slice(0, 120).replace(/\n/g, " "));
717
+ msgs.push({ role: "assistant", type: "tool", content: name, args, result: (data.result||"").slice(0, 500) });
718
+ continue;
719
+ }
720
+ // 思考过程
721
+ const r = data.choices?.[0]?.delta?.reasoning_content || "";
722
+ if (r) process.stdout.write(A.d + r + A.r);
723
+ const t = data.choices?.[0]?.delta?.content || "";
724
+ if (t) { full += t; tOut++; }
725
+ } catch {}
726
+ }
727
+ }
728
+
729
+ // 流式收完,整体 Markdown 渲染后输出
730
+ readline.clearLine(process.stdout, 0);
731
+ readline.cursorTo(process.stdout, 0);
732
+ log(A.ck + A.b + " " + ((cfg.api1 && cfg.api1.model) || "agent") + A.r);
733
+ const rendered = md(full);
734
+ rendered.split("\n").forEach((l) => log(" " + l));
735
+
736
+ msgs.push({ role: "assistant", content: full });
737
+ const elapsed = ((Date.now() - start) / 1000).toFixed(1);
738
+ const tIn = Math.round(input.length / 3.5);
739
+ const cost = ((tIn + tOut) * 0.0000014).toFixed(5);
740
+
741
+ log(A.g + " " + elapsed + "s " + A.yk + tIn + "↑+" + tOut + "↓" + A.r + " " + A.g + "$" + cost + A.d + " | 余额 " + A.wk + apiBalance + A.r);
742
+ } catch (e) { msgs.pop(); fail(e.message); }
743
+ }
744
+
745
+ async function handleCmd(input) {
746
+ const [name, ...args] = input.slice(1).trim().split(/\s+/);
747
+ const a = args.join(" ");
748
+ switch (name) {
749
+ case "exit": case "quit": log(A.g + " 再见"); process.exit(0);
750
+ case "clear": msgs = []; ok("已清空"); break;
751
+ case "save":
752
+ try { fs.writeFileSync(path.join(SESSIONS, session + ".json"), JSON.stringify(msgs, null, 2)); ok("已保存 (" + msgs.length + " 条)"); } catch { fail("保存失败"); }
753
+ break;
754
+ case "load":
755
+ try {
756
+ const f = path.join(SESSIONS, (a || session) + ".json");
757
+ if (fs.existsSync(f)) {
758
+ msgs = JSON.parse(fs.readFileSync(f, "utf8")); session = a || session;
759
+ ok("已加载 " + msgs.length + " 条");
760
+ log(A.g + " " + "-".repeat(50));
761
+ msgs.filter(m => !m.type || m.type !== "tool").forEach((m) => {
762
+ if (m.role === "user") {
763
+ log(A.yk + " User" + A.r + ": " + m.content);
764
+ } else {
765
+ log(A.ck + " " + ((cfg.api1 && cfg.api1.model) || "agent") + A.r);
766
+ const rendered = md(m.content || "");
767
+ rendered.split("\n").forEach((l) => log(" " + l));
768
+ }
769
+ });
770
+ log(A.g + " " + "-".repeat(50));
771
+ } else warn("不存在");
772
+ } catch { fail("加载失败"); }
773
+ break;
774
+ case "status": showStatus(); break;
775
+ case "sessions":
776
+ try {
777
+ const files = fs.readdirSync(SESSIONS).filter((f) => f.endsWith(".json"));
778
+ if (files.length === 0) { log(A.g + " 无保存的会话"); break; }
779
+ log(A.b + "\n 已保存的会话 ( /load 名称 切换 ):\n");
780
+ files.forEach((f) => {
781
+ const n = f.replace(".json", "");
782
+ const len = JSON.parse(fs.readFileSync(path.join(SESSIONS, f), "utf8")).length;
783
+ log(` ${n === session ? A.gk + "●" : " "} ${A.wk}${n}${A.r} ${A.g}${len} 条`);
784
+ });
785
+ } catch { log(A.g + " 无"); }
786
+ break;
787
+ case "skills":
788
+ try { const files = fs.readdirSync(SKILLS).filter((f) => f.endsWith(".json")); files.forEach((f) => { try { const s = JSON.parse(fs.readFileSync(path.join(SKILLS, f), "utf8")); log(` ${A.ck}${s.name || f}${A.r} ${A.g}${s.type || ""}`); } catch { log(` ${A.yk}${f}${A.r}`); } }); } catch { log(A.g + " 无"); }
789
+ break;
790
+ case "history":
791
+ if (msgs.length === 0) { log(A.g + " 无消息"); break; }
792
+ const chatMsgs = msgs.filter(m => !m.type || m.type !== "tool");
793
+ log(A.b + "\n ═══ 历史 (" + chatMsgs.length + " 条对话, " + msgs.filter(m => m.type === "tool").length + " 条工具) ═══");
794
+ chatMsgs.forEach((m, i) => {
795
+ const r = m.role === "user" ? A.yk + "User" : A.ck + (cfg.api1 && cfg.api1.model || "agent");
796
+ log(` ${i+1}. ${r}${A.r}: ${A.d}${(m.content||"").slice(0, 100)}`);
797
+ });
798
+ log(A.g + " ════════════════════════");
799
+ break;
800
+ case "new":
801
+ // 保存旧会话,完全抹除上下文
802
+ try { fs.writeFileSync(path.join(SESSIONS, session + ".json"), JSON.stringify(msgs, null, 2)); } catch {}
803
+ msgs = [];
804
+ session = "s" + Date.now().toString(36).slice(-4);
805
+ ok("新建会话: " + session + " (上下文已清除)");
806
+ break;
807
+ case "rename":
808
+ if (!a) { warn("用法: /rename <名称>"); break; }
809
+ session = a; ok("已重命名: " + session);
810
+ break;
811
+ case "tools": {
812
+ const tMsgs = msgs.filter(m => m.type === "tool" || /^[🔍📂📖✏️🕐📍🧮📦🗑📁⚡🔧]/.test(m.content||""));
813
+ if (tMsgs.length === 0) { log(A.g + " 无工具调用"); break; }
814
+ log(A.b + "\n ═══ Tools (" + tMsgs.length + ") ═══");
815
+ tMsgs.forEach(m => {
816
+ log(` ${A.bk}${(m.content||"🔧").slice(0, 30)}${A.r} ${A.g}${(m.args||"").slice(0, 40)}`);
817
+ if (m.result) log(A.g + " " + (m.result||"").slice(0, 100).replace(/\n/g, " "));
818
+ });
819
+ log(A.g + " ═" + "═".repeat(40));
820
+ break;
821
+ }
822
+ case "stats": {
823
+ const tMsgs = msgs.filter(m => m.type === "tool" || /^[🔍📂📖✏️🕐📍🧮📦🗑📁⚡🔧]/.test(m.content||""));
824
+ let totalChars = 0; msgs.forEach(m => totalChars += (m.content||"").length);
825
+ const estT = Math.round(totalChars / 3.5);
826
+ const estC = (estT * 0.000002).toFixed(5);
827
+ log(A.b + "\n ═══ 会话统计 ═══");
828
+ log(` ${A.g}消息${A.r} ${A.wk}${msgs.length}${A.r} ${A.g}工具${A.r} ${A.wk}${tMsgs.length}${A.r}`);
829
+ log(` ${A.g}Token${A.r} ${A.wk}~${estT}${A.r} ${A.g}花费${A.r} ${A.wk}${currency}${estC}${A.r}`);
830
+ log(A.g + " ═" + "═".repeat(20));
831
+ break;
832
+ }
833
+ case "think":
834
+ if (!a || !["off","low","medium","high","max"].includes(a)) { warn("用法: /think off|low|medium|high|max"); break; }
835
+ const model = (cfg.api1 && cfg.api1.model) || "";
836
+ if (!/reasoner|r1|think|v4-pro|v4-flash/i.test(model)) { warn("当前模型 " + model + " 不支持思考模式"); break; }
837
+ cfg.thinkLevel = a;
838
+ saveCfg(cfg);
839
+ ok("思考强度: " + a);
840
+ break;
841
+ case "currency":
842
+ currency = currency === "¥" ? "$" : "¥";
843
+ cfg.currency = currency;
844
+ saveCfg(cfg);
845
+ ok("币种: " + currency);
846
+ break;
847
+ case "skill": {
848
+ if (a === "list") {
849
+ try {
850
+ const files = fs.readdirSync(SKILLS).filter(f => f.endsWith(".json"));
851
+ if (files.length === 0) { log(A.g + " 无本地技能"); break; }
852
+ log(A.b + "\n 本地技能 (" + files.length + "):");
853
+ files.forEach(f => {
854
+ try {
855
+ const s = JSON.parse(fs.readFileSync(path.join(SKILLS, f), "utf8"));
856
+ const hasHandler = !!s.handler;
857
+ log(` ${A.ck}${s.name || f}${A.r} ${A.g}${hasHandler ? "可执行" : "模板"} ${s.description || ""}`);
858
+ } catch { log(` ${A.yk}${f}${A.r} (格式错误)`); }
859
+ });
860
+ } catch { log(A.g + " 无"); }
861
+ } else if (a.startsWith("import ")) {
862
+ const url = a.slice(7).trim();
863
+ if (!url.startsWith("http")) { warn("用法: /skill import <url>"); break; }
864
+ out(A.g + " 下载中... ");
865
+ try {
866
+ const r = await fetch(url);
867
+ const text = await r.text();
868
+ let skill;
869
+ try { skill = JSON.parse(text); } catch {
870
+ // 尝试 markdown 格式
871
+ skill = { name: url.split("/").pop().replace(/\.\w+$/, ""), description: "来自 " + url, prompt: text };
872
+ }
873
+ if (!skill.name) skill.name = url.split("/").pop().replace(/\.\w+$/, "");
874
+ // 字段适配:将 OpenClaw 常见字段映射到我们的格式
875
+ if (!skill.handler && skill.code) skill.handler = skill.code;
876
+ if (!skill.handler && skill.script) skill.handler = skill.script;
877
+ if (!skill.handler && skill.run) skill.handler = skill.run;
878
+ if (!skill.description && skill.desc) skill.description = skill.desc;
879
+ const filename = (skill.name || "imported").replace(/[^a-zA-Z0-9\u4e00-\u9fff_-]/g, "_") + ".json";
880
+ fs.writeFileSync(path.join(SKILLS, filename), JSON.stringify(skill, null, 2));
881
+ readline.clearLine(process.stdout, 0); readline.cursorTo(process.stdout, 0);
882
+ ok("已导入: " + skill.name + " → skills/" + filename);
883
+ } catch { readline.clearLine(process.stdout, 0); readline.cursorTo(process.stdout, 0); fail("下载失败"); }
884
+ } else {
885
+ log(A.g + " /skill list 查看 /skill import <url> 从URL导入");
886
+ }
887
+ break;
888
+ }
889
+ case "balance":
890
+ try {
891
+ out(A.g + " 查询中... ");
892
+ const r = await fetch("http://localhost:3001/api/balance");
893
+ const d = await r.json();
894
+ readline.clearLine(process.stdout, 0);
895
+ readline.cursorTo(process.stdout, 0);
896
+ apiBalance = d.balance || "—";
897
+ ok("API 余额: " + apiBalance);
898
+ } catch { warn("无法获取余额"); }
899
+ break;
900
+ case "agent":
901
+ if (a === "list") {
902
+ log(A.b + "\n 智能体列表:");
903
+ agents.forEach((ag, i) => {
904
+ log(` ${ag.name === currentAgent ? A.gk + "●" + A.r : " "} ${A.wk}${ag.name}${A.r} ${A.g}${ag.model || "?"}${A.r}`);
905
+ });
906
+ log(A.g + "\n /agent use <名称> 切换");
907
+ } else if (a.startsWith("use ")) {
908
+ const name = a.slice(4).trim();
909
+ const found = agents.find(ag => ag.name === name);
910
+ if (found) { currentAgent = name; ok("已切换到: " + name); }
911
+ else { warn("智能体不存在: " + name); }
912
+ } else if (a === "add") {
913
+ const aName = await new Promise(r => rl.question(A.g + " 名称: " + A.r + " ", r));
914
+ if (!aName || !aName.trim()) { warn("名称必填"); break; }
915
+ const aModel = await new Promise(r => rl.question(A.g + " 模型 [" + ((cfg.api1&&cfg.api1.model)||"?") + "]: " + A.r + " ", r));
916
+ const aPrompt = await new Promise(r => rl.question(A.g + " 系统提示词 (可选): " + A.r + " ", r));
917
+ agents.push({ name: aName.trim(), model: aModel.trim() || (cfg.api1&&cfg.api1.model)||"?", systemPrompt: aPrompt.trim() || "" });
918
+ cfg.agents = agents; saveCfg(cfg);
919
+ ok("已添加: " + aName.trim());
920
+ } else {
921
+ log(A.g + " /agent list 查看 /agent use <名称> 切换 /agent add 新增");
922
+ }
923
+ break;
924
+ case "docs":
925
+ log(A.b + "\n ═══ API 文档 ═══\n");
926
+ log(A.wk + " POST /api/v1/chat/completions" + A.r);
927
+ log(A.g + " OpenAI 兼容,支持 stream/thinking/systemPrompt\n");
928
+ log(A.wk + " POST /api/gateway/telegram|wecom|qq/:token" + A.r);
929
+ log(A.g + " 消息渠道 webhook,返回对应平台格式\n");
930
+ log(A.wk + " POST /api/gateway/feishu" + A.r);
931
+ log(A.g + " 飞书事件回调 (url_verification + 消息接收)\n");
932
+ log(A.wk + " GET /api/config /api/balance /api/sessions/:name" + A.r);
933
+ log(A.g + " 管理接口:配置/余额/会话\n");
934
+ log(A.wk + " WebSocket: ws://localhost:3001/api/gateway/ws" + A.r);
935
+ log(A.g + " 发送 {\"message\":\"...\"} → 返回 {\"type\":\"response\",\"response\":\"...\"}");
936
+ break;
937
+ case "help": case "?":
938
+ log(A.b + "\n /help帮助 /history历史 /sessions会话 /new新建 /load加载 /save保存\n /tools工具 /stats统计 /think思考 /status状态 /clear清空 /docs文档 /exit退出\n");
939
+ break;
940
+ default: warn("未知: /" + name); break;
941
+ }
942
+ }
943
+
944
+ // 主循环
945
+ // 启动:默认新建会话,旧会话用 /load 恢复
946
+ session = "s" + Date.now().toString(36).slice(-4);
947
+ msgs = [];
948
+
949
+ const agentCfg = agents.find(a => a.name === currentAgent) || agents[0] || {};
950
+ const model = agentCfg.model || (cfg.api1 && cfg.api1.model) || "?";
951
+ const divider = A.g + " " + "-".repeat(50) + A.r;
952
+
953
+ // (sidebar removed — use /tools and /stats inline instead)
954
+
955
+ log("");
956
+ log(A.mk + A.b + " |\\ /| _____ | \\/ | |_ _|");
957
+ log(A.mk + A.b + " | \\/ | | ___| | \\ / | | | ");
958
+ log(A.rk + A.b + " | |\\/| | |___ | |\\/| | | | ");
959
+ log(A.yk + A.b + " | | | | ___| | | | | | | ");
960
+ log(A.gk + A.b + " | | | | |___ | | | | _| |_ ");
961
+ log(A.ck + A.b + " |_| |_|_____| |_| |_| |_____|");
962
+ log("");
963
+ log(A.mk + A.b + " +" + "-".repeat(42) + "+");
964
+ log(A.mk + A.b + " |" + A.r + " " + A.g + "Agent " + A.wk + currentAgent + " Model " + A.wk + model + " ".repeat(36 - model.length) + A.mk + A.b + "|");
965
+ log(A.mk + A.b + " |" + A.r + " " + A.g + "Session " + A.wk + session + " " + A.g + "Msgs " + A.wk + msgs.length + " ".repeat(22 - session.length) + A.mk + A.b + "|");
966
+ log(A.mk + A.b + " +" + "-".repeat(42) + "+");
967
+ log(A.g + " Commands: " + A.d + "/help /history /sessions /new /load /save /tools /stats /balance /think /currency /exit" + A.r);
968
+ log(divider);
969
+
970
+ // 异步获取余额,每5分钟刷新
971
+ const refreshBalance = async () => {
972
+ try { const r = await fetch("http://localhost:3001/api/balance"); const d = await r.json(); apiBalance = d.balance || "—"; } catch {}
973
+ };
974
+ refreshBalance();
975
+ setInterval(refreshBalance, 300000);
976
+
977
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: A.gk + A.b + " > " + A.r });
978
+ rl.setPrompt(A.gk + A.b + " > " + A.r);
979
+ rl.prompt();
980
+ rl.on("line", async (line) => {
981
+ const text = line.trim();
982
+ if (!text) { rl.prompt(); return; }
983
+ if (text.startsWith("/")) { await handleCmd(text); rl.prompt(); return; }
984
+ // 消息多了 logo 会滚出屏幕,在每条回复后重打紧凑标题
985
+ if (msgs.length > 0 && msgs.length % 4 === 0) {
986
+ log(A.g + " " + "-".repeat(50));
987
+ log(A.g + " " + A.b + A.wk + "Memi" + A.r + A.g + " " + ((cfg.api1 && cfg.api1.model) || "?") + " | msgs: " + msgs.length);
988
+ }
989
+ // @agent 协作: 检测 @agent名 调用指定 agent
990
+ const atMatch = text.match(/^@(\S+)\s+(.+)/);
991
+ if (atMatch) {
992
+ const targetAgent = agents.find(a => a.name === atMatch[1]);
993
+ if (targetAgent) {
994
+ log(A.mk + " → 委托给 " + atMatch[1] + A.r);
995
+ const origAgent = currentAgent;
996
+ currentAgent = targetAgent.name;
997
+ await send(atMatch[2]);
998
+ currentAgent = origAgent;
999
+ log(A.g + " " + "-".repeat(50));
1000
+ rl.prompt();
1001
+ return;
1002
+ }
1003
+ }
1004
+ log(A.yk + " User" + A.r + ": " + text);
1005
+ await send(text);
1006
+ try { fs.writeFileSync(path.join(SESSIONS, session + ".json"), JSON.stringify(msgs, null, 2)); } catch {}
1007
+ // 首次对话自动 AI 命名
1008
+ if (session.startsWith("s") && msgs.length === 2) autoNameSession();
1009
+ log(A.g + " " + "-".repeat(50));
1010
+ rl.prompt();
1011
+ });
1012
+
1013
+ async function autoNameSession() {
1014
+ try {
1015
+ const userMsgs = msgs.filter(m => m.role === "user").map(m => m.content).join("; ").slice(0, 200);
1016
+ const r = await fetch("http://localhost:3001/api/v1/chat/completions", {
1017
+ method: "POST", headers: { "Content-Type": "application/json" },
1018
+ body: JSON.stringify({ model: "memi-agent", messages: [
1019
+ { role: "user", content: "给这段对话起一个简短名称(3-6个汉字),直接描述对话主题,不要修饰、不要诗意、不要标点。\n\n对话:\n" + userMsgs }
1020
+ ], max_tokens: 20, temperature: 0.3 })
1021
+ });
1022
+ const d = await r.json();
1023
+ let name = (d.choices?.[0]?.message?.content || "").replace(/["\n\r]/g, "").trim();
1024
+ if (!name || name.length < 1 || name.length > 20 || !/[\u4e00-\u9fff]/.test(name)) return;
1025
+ const old = path.join(SESSIONS, session + ".json");
1026
+ const nu = path.join(SESSIONS, name + ".json");
1027
+ if (fs.existsSync(old)) {
1028
+ fs.renameSync(old, nu);
1029
+ session = name;
1030
+ }
1031
+ } catch {}
1032
+ }
1033
+
1034
+ rl.on("close", () => {
1035
+ try { fs.writeFileSync(path.join(SESSIONS, session + ".json"), JSON.stringify(msgs, null, 2)); } catch {}
1036
+ log(A.g + "\n 已断开\n"); process.exit(0);
1037
+ });
1038
+ }
1039
+
1040
+ // ─── 入口 ────────────────────────────────────────────
1041
+ const cmd = process.argv[2] || "chat";
1042
+
1043
+ (async () => {
1044
+ if (!fs.existsSync(ONBOARDED) && cmd !== "onboard") await onboard();
1045
+
1046
+ switch (cmd) {
1047
+ case "chat": case undefined: await chat(); break;
1048
+ case "status": showStatus(); break;
1049
+ case "onboard": await onboard(); break;
1050
+ case "config":
1051
+ if (process.argv[3] === "edit") await onboard();
1052
+ else showStatus();
1053
+ break;
1054
+ case "server":
1055
+ if (process.argv[3] === "start") {
1056
+ out(A.g + " 启动服务... ");
1057
+ try {
1058
+ require("child_process").execSync("npm start --prefix " + path.join(__dirname, "memi-server"), { stdio: "ignore", detached: true });
1059
+ log(A.gk + "已启动" + A.r);
1060
+ } catch { log(A.rk + "启动失败" + A.r); }
1061
+ } else if (process.argv[3] === "restart") {
1062
+ out(A.g + " 重启中... ");
1063
+ try {
1064
+ require("child_process").execSync("taskkill /f /im node.exe 2>nul & npm start --prefix " + path.join(__dirname, "memi-server"), { stdio: "ignore", shell: true });
1065
+ log(A.gk + "已重启" + A.r);
1066
+ } catch { log(A.rk + "重启失败" + A.r); }
1067
+ } else {
1068
+ out(A.g + " 检查服务... ");
1069
+ try { const r = await fetch("http://localhost:3001/health"); log(r.ok ? A.gk + "运行中" + A.r : A.rk + "未响应" + A.r); } catch { log(A.rk + "未启动" + A.r); }
1070
+ log(A.g + " memi server start 启动 memi server restart 重启");
1071
+ }
1072
+ break;
1073
+ case "dashboard":
1074
+ try { require("child_process").execSync("start http://localhost:3001/dashboard"); } catch {}
1075
+ break;
1076
+ case "version": case "--version": case "-v":
1077
+ log(A.ck + A.b + " memi v1.0.0" + A.r);
1078
+ break;
1079
+ case "update":
1080
+ out(A.g + " 检查更新... ");
1081
+ try {
1082
+ const r = await fetch("https://api.github.com/repos/YOURNAME/memi/releases/latest", { signal: AbortSignal.timeout(8000) });
1083
+ if (!r.ok) throw new Error("HTTP " + r.status);
1084
+ const d = await r.json();
1085
+ const latest = (d.tag_name || "").replace(/^v/, "");
1086
+ readline.clearLine(process.stdout, 0); readline.cursorTo(process.stdout, 0);
1087
+ if (latest && latest !== "1.0.0") {
1088
+ log(A.yk + " ⚡ 新版本可用: v" + latest + A.r);
1089
+ log(A.g + " 当前: v1.0.0 → 最新: v" + latest);
1090
+ log(A.g + " 下载: " + (d.html_url || ""));
1091
+ } else {
1092
+ ok("已是最新版本 (v1.0.0)");
1093
+ }
1094
+ } catch {
1095
+ readline.clearLine(process.stdout, 0); readline.cursorTo(process.stdout, 0);
1096
+ log(A.g + " v1.0.0 (检查更新失败,请稍后重试)");
1097
+ }
1098
+ break;
1099
+ case "skills":
1100
+ try {
1101
+ // 扫描 skills/ 目录:JSON 技能 + ClawHub 子目录
1102
+ const jsonSkills = fs.readdirSync(SKILLS).filter(f => f.endsWith(".json"));
1103
+ const clawhubDirs = fs.readdirSync(SKILLS, { withFileTypes: true })
1104
+ .filter(d => d.isDirectory() && fs.existsSync(path.join(SKILLS, d.name, "SKILL.md")));
1105
+
1106
+ if (jsonSkills.length === 0 && clawhubDirs.length === 0) {
1107
+ log(A.g + " 无。放 .json 到 memi-config/skills/ 或用 clawhub install <skill> 安装");
1108
+ log("");
1109
+ break;
1110
+ }
1111
+ head("Skills");
1112
+ jsonSkills.forEach((f) => { try {
1113
+ const s = JSON.parse(fs.readFileSync(path.join(SKILLS, f), "utf8"));
1114
+ log(` ${A.ck}${s.name || f}${A.r} ${A.g}${s.handler ? "可执行" : "模板"} ${s.description || ""}`);
1115
+ } catch { log(` ${A.yk}${f}${A.r}`); } });
1116
+ clawhubDirs.forEach(d => {
1117
+ try {
1118
+ const md = fs.readFileSync(path.join(SKILLS, d.name, "SKILL.md"), "utf8");
1119
+ const yamlMatch = md.match(/^---\n([\s\S]*?)\n---/);
1120
+ const meta = {};
1121
+ if (yamlMatch) {
1122
+ yamlMatch[1].split("\n").forEach(line => {
1123
+ const [k, ...v] = line.split(":");
1124
+ if (k && v.length) meta[k.trim()] = v.join(":").trim();
1125
+ });
1126
+ }
1127
+ log(` ${A.ck}${meta.name || d.name}${A.r} ${A.g}ClawHub ${meta.description || ""}`);
1128
+ } catch { log(` ${A.yk}${d.name}${A.r}`); }
1129
+ });
1130
+ log("");
1131
+ } catch { fail("无法读取"); }
1132
+ break;
1133
+
1134
+ case "skill-import": {
1135
+ const url = process.argv[3];
1136
+ if (!url || !url.startsWith("http")) { log(A.g + " 用法: memi skill-import <url>"); break; }
1137
+ try {
1138
+ const r = await fetch(url);
1139
+ const text = await r.text();
1140
+ let skill;
1141
+ try { skill = JSON.parse(text); } catch { skill = { name: url.split("/").pop()?.replace(/\.\w+$/, "") || "imported", description: "来自 " + url, prompt: text }; }
1142
+ if (!skill.name) skill.name = "imported_" + Date.now().toString(36);
1143
+ if (!skill.handler && skill.code) skill.handler = skill.code;
1144
+ if (!skill.handler && skill.script) skill.handler = skill.script;
1145
+ if (!skill.handler && skill.run) skill.handler = skill.run;
1146
+ if (!skill.description && skill.desc) skill.description = skill.desc;
1147
+ const filename = (skill.name || "skill").replace(/[^a-zA-Z0-9\u4e00-\u9fff_-]/g, "_") + ".json";
1148
+ fs.writeFileSync(path.join(SKILLS, filename), JSON.stringify(skill, null, 2));
1149
+ ok("已导入: " + skill.name + " → " + filename);
1150
+ } catch(e) { fail("导入失败: " + e.message); }
1151
+ break;
1152
+ }
1153
+ case "sessions":
1154
+ try {
1155
+ const files = fs.readdirSync(SESSIONS).filter((f) => f.endsWith(".json"));
1156
+ head("Sessions (" + SESSIONS + ")");
1157
+ files.forEach((f) => { const n = f.replace(".json", ""); const len = JSON.parse(fs.readFileSync(path.join(SESSIONS, f), "utf8")).length; log(` ${A.ck}${n}${A.r} ${A.g}(${len} msgs)`); });
1158
+ if (files.length === 0) log(A.g + " 无");
1159
+ log("");
1160
+ } catch { fail("无法读取"); }
1161
+ break;
1162
+ case "doctor": {
1163
+ let ok = true;
1164
+ log(A.b + "\n Memi Doctor — 诊断报告\n");
1165
+ const nv = process.version;
1166
+ log(` ${parseInt(nv.slice(1)) >= 18 ? A.gk + "✓" : A.rk + "✗"}${A.r} Node.js ${nv}`);
1167
+ const c = loadCfg();
1168
+ const hasApi = !!(c.api1?.baseUrl && c.api1?.apiKey);
1169
+ log(` ${hasApi ? A.gk + "✓" : A.yk + "⚠"}${A.r} API 配置`);
1170
+ try {
1171
+ const r = await fetch("http://localhost:3001/health");
1172
+ log(` ${r.ok ? A.gk + "✓" : A.rk + "✗"}${A.r} memi-server ${r.ok ? "运行中" : "异常"}`);
1173
+ } catch { log(` ${A.rk + "✗"}${A.r} memi-server 未启动`); }
1174
+ log(` ${A.gk + "✓"}${A.r} 配置目录 ${DIR}`);
1175
+ log("");
1176
+ log(A.gk + " 一切正常" + A.r);
1177
+ break;
1178
+ }
1179
+ case "reset":
1180
+ if (process.argv[3] === "--force" || process.argv[3] === "-f") {
1181
+ try { fs.rmSync(DIR, { recursive: true, force: true }); log(A.gk + " ✓ 已重置。运行 memi onboard 重新配置" + A.r); } catch { fail("重置失败"); }
1182
+ } else {
1183
+ log(A.yk + " ⚠ 此操作将清除所有配置和会话。确认: memi reset --force");
1184
+ }
1185
+ break;
1186
+ case "agent":
1187
+ log(A.b + "\n Memi Agent v1.0.0\n");
1188
+ const ac = loadCfg();
1189
+ pair("模型", (ac.api1 && ac.api1.model) || "未配置");
1190
+ pair("端点", (ac.api1 && ac.api1.baseUrl) || "未配置");
1191
+ pair("工具", "12 (文件/搜索/命令/计算/时间/技能/生图)");
1192
+ pair("迭代", "8 轮上限");
1193
+ log("");
1194
+ break;
1195
+ case "telegram": case "feishu": case "wecom": case "qq": {
1196
+ const ch = cmd === "telegram" ? "telegram" : cmd === "feishu" ? "feishu" : cmd === "wecom" ? "wecom" : "qq";
1197
+ const token = process.argv[3];
1198
+ if (!token) {
1199
+ log(A.g + ` 用法: memi ${ch} <token/key>`);
1200
+ if (ch === "telegram") log(A.g + " 1. @BotFather 创建机器人 → 获取 token");
1201
+ if (ch === "feishu") log(A.g + " 1. 飞书开放平台 → 创建应用 → 获取 App ID");
1202
+ if (ch === "wecom") log(A.g + " 1. 企业微信管理后台 → 创建机器人 → 获取 webhook key");
1203
+ if (ch === "qq") log(A.g + " 1. go-cqhttp 或官方 QQ Bot → 获取 token");
1204
+ log(A.g + ` 2. 运行: memi ${ch} <token>`);
1205
+ break;
1206
+ }
1207
+ const chNames = { telegram: "Telegram", feishu: "飞书", wecom: "企业微信", qq: "QQ" };
1208
+ out(A.g + ` 连接 ${chNames[ch]}... `);
1209
+ try {
1210
+ const ngrokUrl = "https://pyromania-strenuous-sinuous.ngrok-free.dev";
1211
+ if (ch === "telegram") {
1212
+ const r = await fetch(`http://localhost:3001/api/gateway/telegram/${token}/setup`, {
1213
+ method: "POST", headers: { "Content-Type": "application/json" },
1214
+ body: JSON.stringify({ baseUrl: ngrokUrl })
1215
+ });
1216
+ const d = await r.json();
1217
+ if (d.success) log(A.gk + `✓ Webhook: ${d.webhook}` + A.r);
1218
+ else fail("失败: " + (d.error || ""));
1219
+ } else if (ch === "feishu") {
1220
+ // 飞书需 AppID+AppSecret,提示用户用 webhook URL
1221
+ const webhookUrl = `${ngrokUrl}/api/gateway/feishu`;
1222
+ log(A.gk + "✓ Feishu 端点就绪" + A.r);
1223
+ log(A.g + " Webhook URL: " + A.wk + webhookUrl + A.r);
1224
+ log(A.g + " 请在飞书开放平台配置此地址为事件回调 URL");
1225
+ } else if (ch === "wecom") {
1226
+ const webhookUrl = `${ngrokUrl}/api/gateway/wecom/${token}`;
1227
+ log(A.gk + "✓ 企业微信端点就绪" + A.r);
1228
+ log(A.g + " Webhook URL: " + A.wk + webhookUrl + A.r);
1229
+ log(A.g + " 请在企业微信机器人配置中填入此地址");
1230
+ } else if (ch === "qq") {
1231
+ const webhookUrl = `${ngrokUrl}/api/gateway/qq/${token}`;
1232
+ log(A.gk + "✓ QQ 端点就绪" + A.r);
1233
+ log(A.g + " Webhook URL: " + A.wk + webhookUrl + A.r);
1234
+ log(A.g + " 请在 go-cqhttp 或 QQ Bot 后台配置此地址");
1235
+ }
1236
+ } catch { fail("连接失败,请确认 memi-server 已启动"); }
1237
+ break;
1238
+ }
1239
+ case "daemon": {
1240
+ const sub = process.argv[3] || "status";
1241
+ const osType = process.platform;
1242
+ const serverScript = path.join(__dirname, "memi-server", "index.js");
1243
+
1244
+ if (sub === "install") {
1245
+ if (osType === "win32") {
1246
+ try {
1247
+ const cmd = `schtasks /create /tn "MemiAgent" /tr "node \\"${serverScript}\\"" /sc onstart /ru System /f`;
1248
+ require("child_process").execSync(cmd, { shell: true });
1249
+ ok("守护进程已安装(开机自启)");
1250
+ } catch(e) { fail("安装失败: " + e.message.slice(0, 100)); }
1251
+ } else if (osType === "darwin") {
1252
+ try {
1253
+ const plist = path.join(require("os").homedir(), "Library", "LaunchAgents", "com.memi.agent.plist");
1254
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
1255
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1256
+ <plist version="1.0"><dict>
1257
+ <key>Label</key><string>com.memi.agent</string>
1258
+ <key>ProgramArguments</key><array><string>node</string><string>${serverScript}</string></array>
1259
+ <key>RunAtLoad</key><true/><key>KeepAlive</key><true/>
1260
+ </dict></plist>`;
1261
+ fs.writeFileSync(plist, xml);
1262
+ require("child_process").execSync(`launchctl load "${plist}"`, { shell: true });
1263
+ ok("守护进程已安装(开机自启)");
1264
+ } catch(e) { fail("安装失败: " + e.message.slice(0, 100)); }
1265
+ } else {
1266
+ try {
1267
+ const unit = `/etc/systemd/system/memi-agent.service`;
1268
+ const cfg = `[Unit]\nDescription=Memi Agent\nAfter=network.target\n\n[Service]\nExecStart=node ${serverScript}\nRestart=always\nUser=${require("os").userInfo().username}\n\n[Install]\nWantedBy=multi-user.target\n`;
1269
+ require("child_process").execSync(`echo "${cfg.replace(/"/g,'\\"')}" | sudo tee ${unit} && sudo systemctl daemon-reload && sudo systemctl enable memi-agent`, { shell: true });
1270
+ ok("守护进程已安装(开机自启)");
1271
+ } catch(e) { fail("安装失败(需要 sudo 权限): " + e.message.slice(0, 100)); }
1272
+ }
1273
+ } else if (sub === "uninstall") {
1274
+ if (osType === "win32") {
1275
+ try { require("child_process").execSync('schtasks /delete /tn "MemiAgent" /f', { shell: true }); ok("已卸载"); }
1276
+ catch { warn("卸载失败或无任务"); }
1277
+ } else if (osType === "darwin") {
1278
+ try {
1279
+ const plist = path.join(require("os").homedir(), "Library", "LaunchAgents", "com.memi.agent.plist");
1280
+ require("child_process").execSync(`launchctl unload "${plist}"`, { shell: true });
1281
+ if (fs.existsSync(plist)) fs.unlinkSync(plist);
1282
+ ok("已卸载");
1283
+ } catch { warn("卸载失败"); }
1284
+ } else {
1285
+ try { require("child_process").execSync('sudo systemctl disable memi-agent && sudo rm /etc/systemd/system/memi-agent.service', { shell: true }); ok("已卸载"); }
1286
+ catch { warn("卸载失败(需要 sudo)"); }
1287
+ }
1288
+ } else {
1289
+ // status
1290
+ try { out(A.g + " 服务状态: "); const r = await fetch("http://localhost:3001/health"); log(r.ok ? A.gk + "运行中" + A.r : A.rk + "未响应" + A.r); } catch { log(A.rk + "未运行" + A.r); }
1291
+ if (osType === "win32") {
1292
+ try { const r = require("child_process").execSync('schtasks /query /tn "MemiAgent" 2>nul', { shell: true, encoding: "utf8" }); log(A.g + " 开机自启: " + A.gk + "已配置" + A.r); } catch { log(A.g + " 开机自启: " + A.g + "未配置" + A.r); }
1293
+ }
1294
+ log(A.g + "\n memi daemon install 安装开机自启\n memi daemon uninstall 取消开机自启");
1295
+ }
1296
+ break;
1297
+ }
1298
+ case "team": {
1299
+ log(A.b + "\n ═══ 多 Agent 协作 ═══\n");
1300
+ const agentList = cfg.agents || [{ name: "default", model: cfg.api1?.model || "?", systemPrompt: "" }];
1301
+ log(A.g + " 已注册 Agent:");
1302
+ agentList.forEach(a => log(` ${A.ck}${a.name}${A.r} ${A.g}${a.model}${A.r}`));
1303
+ log("");
1304
+ log(A.g + " /agent use <名称> 切换当前 Agent");
1305
+ log(A.g + " /agent add 新增 Agent");
1306
+ log(A.g + " 协作: 在对话中 @agent名称 调用其他 Agent");
1307
+ break;
1308
+ }
1309
+ case "docs": {
1310
+ log(A.b + "\n ═══ API 文档 ═══\n");
1311
+ log(A.wk + " POST /api/v1/chat/completions" + A.r);
1312
+ log(A.g + " Body: { model, messages, stream, thinking, systemPrompt }");
1313
+ log(A.g + " 返回 OpenAI 兼容格式\n");
1314
+ log(A.wk + " POST /api/gateway/telegram/:token" + A.r);
1315
+ log(A.g + " Body: Telegram Update 格式");
1316
+ log(A.g + " 注册: POST /api/gateway/telegram/:token/setup\n");
1317
+ log(A.wk + " POST /api/gateway/wecom/:token" + A.r);
1318
+ log(A.g + " Body: { text: { content: \"...\" } }\n");
1319
+ log(A.wk + " POST /api/gateway/feishu" + A.r);
1320
+ log(A.g + " Body: 飞书事件回调格式\n");
1321
+ log(A.wk + " POST /api/gateway/qq/:token" + A.r);
1322
+ log(A.g + " Body: { message: \"...\" }\n");
1323
+ log(A.wk + " GET /api/config /api/balance /api/sessions" + A.r);
1324
+ log(A.g + " 管理接口: 配置/余额/会话 CRUD\n");
1325
+ log(A.g + " WebSocket: ws://localhost:3001/api/gateway/ws");
1326
+ break;
1327
+ }
1328
+ case "publish": {
1329
+ const skillName = process.argv[3];
1330
+ if (!skillName) { log(A.g + " 用法: memi publish <skill名称>"); break; }
1331
+ try {
1332
+ const srcDir = path.join(SKILLS, skillName);
1333
+ const dest = path.join(DIR, `${skillName}.zip`);
1334
+ if (!fs.existsSync(srcDir)) { warn("技能不存在: " + skillName); break; }
1335
+ const { execSync } = require("child_process");
1336
+ if (process.platform === "win32") {
1337
+ execSync(`powershell Compress-Archive -Path "${srcDir}" -DestinationPath "${dest}" -Force`, { shell: true });
1338
+ } else {
1339
+ execSync(`cd "${SKILLS}" && zip -r "${dest}" "${skillName}"`, { shell: true });
1340
+ }
1341
+ ok(`已打包: ${dest}`);
1342
+ log(A.g + " 可分享此 zip 文件,其他人用 memi skill-import <url> 安装");
1343
+ } catch(e) { fail("打包失败: " + e.message); }
1344
+ break;
1345
+ }
1346
+ case "help": case "--help": case "-h":
1347
+ log(A.b + "\n Memi Agent CLI\n");
1348
+ log(` ${A.ck}chat${A.r} 对话`);
1349
+ log(` ${A.ck}status${A.r} 状态 ${A.ck}doctor${A.r} 诊断`);
1350
+ log(` ${A.ck}agent${A.r} 信息 ${A.ck}onboard${A.r} 引导`);
1351
+ log(` ${A.ck}reset${A.r} 重置 ${A.ck}config${A.r} 配置`);
1352
+ log(` ${A.ck}server${A.r} 服务 ${A.ck}skills${A.r} 技能`);
1353
+ log(` ${A.ck}dashboard${A.r}面板 ${A.ck}sessions${A.r} 会话`);
1354
+ log(` ${A.ck}daemon${A.r} 守护进程 ${A.ck}update${A.r} 更新`);
1355
+ log(` ${A.ck}version${A.r} 版本 ${A.ck}help${A.r} 帮助`);
1356
+ log(` ${A.ck}help${A.r} 帮助`);
1357
+ log("");
1358
+ break;
1359
+ default:
1360
+ log(A.yk + " 未知命令: " + cmd);
1361
+ log(A.g + " 输入 memi help 查看所有命令");
1362
+ break;
1363
+ }
1364
+ })();