@modusensus/dsh-mneme 0.7.11 → 0.7.13
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.en.md +69 -0
- package/README.md +227 -21
- package/bin/cli.mjs +603 -0
- package/lib/api-standalone.js +264 -0
- package/lib/api.js +91 -2
- package/lib/client.js +243 -1
- package/lib/config.js +80 -0
- package/lib/index.js +43 -12
- package/lib/quality-filter.js +4 -1
- package/lib/service.js +38 -5
- package/lib/settings.js +40 -0
- package/lib/store.js +4 -1
- package/lib/summarize.js +197 -59
- package/lib/tools.js +3 -3
- package/package.json +7 -1
- package/src/api-standalone.js +264 -0
- package/src/api.js +91 -2
- package/src/config.js +80 -0
- package/src/index.js +43 -12
- package/src/quality-filter.js +4 -1
- package/src/service.js +38 -5
- package/src/settings.js +40 -0
- package/src/store.js +4 -1
- package/src/summarize.js +197 -59
- package/src/tools.js +3 -3
- package/test/api.test.js +44 -0
- package/test/helpers/peer-worker.mjs +10 -0
- package/test/peer-blockers.test.js +4 -9
- package/test/settings.test.js +24 -0
- package/test/standalone-api.test.js +326 -0
- package/test/summarize.test.js +116 -0
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* dsh-mneme CLI —— @modusensus/dsh-mneme 外部 API 命令行客户端
|
|
4
|
+
*
|
|
5
|
+
* 零依赖 ESM(Node >= 20,使用全局 fetch)。
|
|
6
|
+
* 配置优先级:命令行 --url/--token > 环境变量 DSH_MNEME_URL / DSH_MNEME_TOKEN
|
|
7
|
+
* > 配置文件 ~/.dsh-mneme/cli.json({"url","token"})
|
|
8
|
+
* 默认服务地址:http://127.0.0.1:8790
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import fsp from 'node:fs/promises';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import process from 'node:process';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
|
|
18
|
+
const CLI_NAME = 'dsh-mneme';
|
|
19
|
+
const DEFAULT_URL = 'http://127.0.0.1:8790';
|
|
20
|
+
const CONFIG_DIR = path.join(os.homedir(), '.dsh-mneme');
|
|
21
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, 'cli.json');
|
|
22
|
+
const MEMORY_TYPES = ['preference', 'project', 'decision', 'summary', 'history'];
|
|
23
|
+
const SEARCH_MODES = ['keyword', 'vector', 'auto'];
|
|
24
|
+
const REQUEST_TIMEOUT_MS = 10_000;
|
|
25
|
+
|
|
26
|
+
function readPkgVersion() {
|
|
27
|
+
try {
|
|
28
|
+
const pkgPath = path.resolve(
|
|
29
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
30
|
+
'..',
|
|
31
|
+
'package.json'
|
|
32
|
+
);
|
|
33
|
+
return JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version ?? '?';
|
|
34
|
+
} catch {
|
|
35
|
+
return '?';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const PKG_VERSION = readPkgVersion();
|
|
40
|
+
|
|
41
|
+
const USAGE = `${CLI_NAME} —— DSH 记忆插件(@modusensus/dsh-mneme)外部 API 命令行客户端
|
|
42
|
+
|
|
43
|
+
用法:
|
|
44
|
+
${CLI_NAME} <命令> [参数] [选项]
|
|
45
|
+
|
|
46
|
+
配置(优先级从高到低):
|
|
47
|
+
1. 命令行参数 --url <url> / --token <token>
|
|
48
|
+
2. 环境变量 DSH_MNEME_URL / DSH_MNEME_TOKEN
|
|
49
|
+
3. 配置文件 ~/.dsh-mneme/cli.json(由 \`config set\` 写入)
|
|
50
|
+
未配置时使用默认地址:${DEFAULT_URL}
|
|
51
|
+
|
|
52
|
+
命令:
|
|
53
|
+
config set <url> <token> 写入配置文件(token 省略时保留原值)
|
|
54
|
+
config show 查看当前配置(token 打码显示)
|
|
55
|
+
config path 打印配置文件路径
|
|
56
|
+
status 服务状态(版本 / 记忆统计 / 实体 / 运行时长)
|
|
57
|
+
list [--type t] [--min-importance n] [--source s]
|
|
58
|
+
[--limit n] [--offset n] [--json]
|
|
59
|
+
列出记忆(type: preference | project | decision | summary | history)
|
|
60
|
+
search <query> [--mode m] [--topk n] [--json]
|
|
61
|
+
搜索记忆(mode: keyword | vector | auto)
|
|
62
|
+
get <id> [--json] 查看单条记忆
|
|
63
|
+
add --type <t> --title <t> --content <c>
|
|
64
|
+
[--importance n] [--tags a,b] [--source s] [--json]
|
|
65
|
+
新增记忆(type: preference | project | decision | summary | history)
|
|
66
|
+
delete <id> 删除记忆
|
|
67
|
+
|
|
68
|
+
选项:
|
|
69
|
+
--url <url> 覆盖服务地址
|
|
70
|
+
--token <token> 覆盖访问 token
|
|
71
|
+
--json 输出原始 JSON
|
|
72
|
+
-h, --help 显示本帮助
|
|
73
|
+
-V, --version 显示版本号
|
|
74
|
+
|
|
75
|
+
说明:
|
|
76
|
+
token 在 DSH 面板「设置 → 外部访问」或插件设置中查看;
|
|
77
|
+
除 GET /health 外,所有外部 API 路由均需 Bearer token。
|
|
78
|
+
|
|
79
|
+
示例:
|
|
80
|
+
${CLI_NAME} config set ${DEFAULT_URL} my-token
|
|
81
|
+
${CLI_NAME} status
|
|
82
|
+
${CLI_NAME} list --type project --limit 10
|
|
83
|
+
${CLI_NAME} search "部署流程" --mode vector --topk 5
|
|
84
|
+
${CLI_NAME} add --type decision --title "采用 SQLite" --content "存储层使用 node:sqlite" --importance 4 --tags 存储,决策
|
|
85
|
+
${CLI_NAME} get 42
|
|
86
|
+
${CLI_NAME} delete 42`;
|
|
87
|
+
|
|
88
|
+
/* ---------------------------------- 参数解析 ---------------------------------- */
|
|
89
|
+
|
|
90
|
+
function parseArgv(argv) {
|
|
91
|
+
const positionals = [];
|
|
92
|
+
const flags = {};
|
|
93
|
+
for (let i = 0; i < argv.length; i++) {
|
|
94
|
+
const arg = argv[i];
|
|
95
|
+
if (arg === '--') {
|
|
96
|
+
positionals.push(...argv.slice(i + 1));
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
if (arg.startsWith('--')) {
|
|
100
|
+
let key = arg.slice(2);
|
|
101
|
+
let value;
|
|
102
|
+
const eq = key.indexOf('=');
|
|
103
|
+
if (eq >= 0) {
|
|
104
|
+
value = key.slice(eq + 1);
|
|
105
|
+
key = key.slice(0, eq);
|
|
106
|
+
} else if (i + 1 < argv.length && !argv[i + 1].startsWith('--') && argv[i + 1] !== '--') {
|
|
107
|
+
value = argv[++i];
|
|
108
|
+
} else {
|
|
109
|
+
value = true;
|
|
110
|
+
}
|
|
111
|
+
flags[key.toLowerCase()] = value;
|
|
112
|
+
} else {
|
|
113
|
+
positionals.push(arg);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { positionals, flags };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function flagValue(flags, ...names) {
|
|
120
|
+
for (const name of names) {
|
|
121
|
+
const value = flags[name.toLowerCase()];
|
|
122
|
+
if (value !== undefined && value !== true && value !== '') return value;
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function flagBool(flags, ...names) {
|
|
128
|
+
for (const name of names) {
|
|
129
|
+
const value = flags[name.toLowerCase()];
|
|
130
|
+
if (value !== undefined) return value === true || value === 'true' || value === '';
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* ---------------------------------- 配置解析 ---------------------------------- */
|
|
136
|
+
|
|
137
|
+
function readConfigFile() {
|
|
138
|
+
try {
|
|
139
|
+
const data = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
140
|
+
return data && typeof data === 'object' && !Array.isArray(data) ? data : {};
|
|
141
|
+
} catch {
|
|
142
|
+
return {};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function firstNonEmpty(...values) {
|
|
147
|
+
for (const value of values) {
|
|
148
|
+
if (typeof value === 'string' && value.trim() !== '') return value.trim();
|
|
149
|
+
}
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function resolveConfig(flags) {
|
|
154
|
+
const file = readConfigFile();
|
|
155
|
+
const url =
|
|
156
|
+
firstNonEmpty(flagValue(flags, 'url'), process.env.DSH_MNEME_URL, file.url) ?? DEFAULT_URL;
|
|
157
|
+
const token =
|
|
158
|
+
firstNonEmpty(flagValue(flags, 'token'), process.env.DSH_MNEME_TOKEN, file.token) ?? '';
|
|
159
|
+
return { url: url.replace(/\/+$/, ''), token, jsonMode: false };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function maskToken(token) {
|
|
163
|
+
if (!token) return '(未设置)';
|
|
164
|
+
if (token.length <= 8) return '****';
|
|
165
|
+
return `${token.slice(0, 4)}****${token.slice(-4)}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* ---------------------------------- 输出与错误 ---------------------------------- */
|
|
169
|
+
|
|
170
|
+
function stringify(value) {
|
|
171
|
+
return JSON.stringify(value, null, 2);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function fail(cfg, message, { rawJson } = {}) {
|
|
175
|
+
if (cfg.jsonMode) {
|
|
176
|
+
console.error(rawJson !== undefined ? rawJson : stringify({ error: message }));
|
|
177
|
+
} else {
|
|
178
|
+
console.error(message);
|
|
179
|
+
}
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function failHttp(cfg, status, pathname, data, text) {
|
|
184
|
+
const serverError =
|
|
185
|
+
data && typeof data === 'object' && data.error
|
|
186
|
+
? String(data.error)
|
|
187
|
+
: (text || '').slice(0, 200);
|
|
188
|
+
let hint = '';
|
|
189
|
+
if (status === 401) {
|
|
190
|
+
hint =
|
|
191
|
+
`\n访问 token 缺失或无效。token 可在 DSH 面板「设置 → 外部访问」或插件设置中查看,\n` +
|
|
192
|
+
`然后用 \`${CLI_NAME} config set <url> <token>\`、环境变量 DSH_MNEME_TOKEN 或 --token 配置。`;
|
|
193
|
+
} else if (status === 404) {
|
|
194
|
+
hint = `\n资源不存在:${pathname}`;
|
|
195
|
+
}
|
|
196
|
+
fail(cfg, `请求失败(HTTP ${status})${serverError ? `:${serverError}` : ''} [${pathname}]${hint}`, {
|
|
197
|
+
rawJson: cfg.jsonMode && text ? text : undefined,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function failNetwork(cfg, url, err) {
|
|
202
|
+
const timedOut = err && (err.name === 'TimeoutError' || err.name === 'AbortError');
|
|
203
|
+
const reason = timedOut
|
|
204
|
+
? `请求超时(${REQUEST_TIMEOUT_MS / 1000} 秒)`
|
|
205
|
+
: `无法连接到服务(${err?.code ?? err?.cause?.code ?? err?.message ?? '未知错误'})`;
|
|
206
|
+
fail(
|
|
207
|
+
cfg,
|
|
208
|
+
`${reason}:${url}\n` +
|
|
209
|
+
`请确认 DSH 已启动、插件外部 API 已开启,且地址正确(默认 ${DEFAULT_URL})。`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function requireToken(cfg) {
|
|
214
|
+
if (cfg.token) return;
|
|
215
|
+
fail(
|
|
216
|
+
cfg,
|
|
217
|
+
[
|
|
218
|
+
'未配置访问 token,无法调用该命令(外部 API 需要 Bearer 鉴权)。',
|
|
219
|
+
'',
|
|
220
|
+
'token 可在 DSH 面板「设置 → 外部访问」或插件设置中查看,然后任选其一配置:',
|
|
221
|
+
` 1. ${CLI_NAME} config set <url> <token>`,
|
|
222
|
+
' 2. 环境变量 DSH_MNEME_TOKEN=<token>',
|
|
223
|
+
' 3. 命令行参数 --token <token>',
|
|
224
|
+
].join('\n')
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function parseIntArg(cfg, value, label) {
|
|
229
|
+
if (value === undefined || value === true || value === '') return undefined;
|
|
230
|
+
const n = Number(value);
|
|
231
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
232
|
+
fail(cfg, `无效的 ${label}: ${value}(应为非负整数)`);
|
|
233
|
+
}
|
|
234
|
+
return n;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/* ---------------------------------- HTTP 请求 ---------------------------------- */
|
|
238
|
+
|
|
239
|
+
async function apiRequest(cfg, method, pathname, { query, body } = {}) {
|
|
240
|
+
let url = cfg.url + pathname;
|
|
241
|
+
if (query) {
|
|
242
|
+
const params = new URLSearchParams();
|
|
243
|
+
for (const [key, value] of Object.entries(query)) {
|
|
244
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
245
|
+
params.set(key, String(value));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const qs = params.toString();
|
|
249
|
+
if (qs) url += `?${qs}`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const headers = { Accept: 'application/json' };
|
|
253
|
+
if (cfg.token) headers.Authorization = `Bearer ${cfg.token}`;
|
|
254
|
+
if (body !== undefined) headers['Content-Type'] = 'application/json';
|
|
255
|
+
|
|
256
|
+
let res;
|
|
257
|
+
try {
|
|
258
|
+
res = await fetch(url, {
|
|
259
|
+
method,
|
|
260
|
+
headers,
|
|
261
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
262
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
263
|
+
});
|
|
264
|
+
} catch (err) {
|
|
265
|
+
failNetwork(cfg, url, err);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const text = await res.text();
|
|
269
|
+
let data = null;
|
|
270
|
+
try {
|
|
271
|
+
data = text ? JSON.parse(text) : null;
|
|
272
|
+
} catch {
|
|
273
|
+
// 非 JSON 响应体,保留原始文本用于报错展示
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (!res.ok) {
|
|
277
|
+
failHttp(cfg, res.status, pathname, data, text);
|
|
278
|
+
}
|
|
279
|
+
return data;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/* ---------------------------------- 结果格式化 ---------------------------------- */
|
|
283
|
+
|
|
284
|
+
function formatTimestamp(value) {
|
|
285
|
+
if (!value) return '';
|
|
286
|
+
const s = String(value);
|
|
287
|
+
return s.length >= 10 ? s.slice(0, 10) : s;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function formatMemoryLine(item) {
|
|
291
|
+
const id = item?.id ?? '?';
|
|
292
|
+
const type = item?.type ?? '?';
|
|
293
|
+
const importance = item?.importance != null ? ` ★${item.importance}` : '';
|
|
294
|
+
const title = item?.title ? ` ${item.title}` : '';
|
|
295
|
+
const tags =
|
|
296
|
+
Array.isArray(item?.tags) && item.tags.length ? ` [${item.tags.join(',')}]` : '';
|
|
297
|
+
const date = formatTimestamp(item?.created_at ?? item?.createdAt);
|
|
298
|
+
return ` #${id} [${type}]${importance}${title}${tags}${date ? ` (${date})` : ''}`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function printMemoryItems(payload, { withMode = false } = {}) {
|
|
302
|
+
const items = Array.isArray(payload?.items) ? payload.items : [];
|
|
303
|
+
const total = payload?.total ?? items.length;
|
|
304
|
+
if (withMode && payload?.mode) console.log(`搜索模式: ${payload.mode}`);
|
|
305
|
+
console.log(`共 ${total} 条,本次显示 ${items.length} 条`);
|
|
306
|
+
if (!items.length) return;
|
|
307
|
+
console.log('');
|
|
308
|
+
for (const item of items) {
|
|
309
|
+
const score = item?.score ?? item?.similarity;
|
|
310
|
+
const scoreText =
|
|
311
|
+
typeof score === 'number' && Number.isFinite(score) ? ` (score ${score.toFixed(3)})` : '';
|
|
312
|
+
console.log(formatMemoryLine(item) + scoreText);
|
|
313
|
+
const content =
|
|
314
|
+
typeof item?.content === 'string' ? item.content.replace(/\s+/g, ' ').trim() : '';
|
|
315
|
+
if (content) {
|
|
316
|
+
console.log(` ${content.length > 80 ? `${content.slice(0, 80)}…` : content}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function printMemoryDetail(item) {
|
|
322
|
+
if (!item || typeof item !== 'object') {
|
|
323
|
+
console.log(stringify(item ?? null));
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const fields = [];
|
|
327
|
+
if (item.id !== undefined) fields.push(`id: ${item.id}`);
|
|
328
|
+
if (item.type !== undefined) fields.push(`类型: ${item.type}`);
|
|
329
|
+
if (item.importance !== undefined) fields.push(`重要性: ${item.importance}`);
|
|
330
|
+
if (item.title !== undefined) fields.push(`标题: ${item.title}`);
|
|
331
|
+
if (item.tags !== undefined) {
|
|
332
|
+
fields.push(`标签: ${Array.isArray(item.tags) ? item.tags.join(', ') : item.tags}`);
|
|
333
|
+
}
|
|
334
|
+
if (item.source !== undefined) fields.push(`来源: ${item.source}`);
|
|
335
|
+
const created = item.created_at ?? item.createdAt;
|
|
336
|
+
if (created) fields.push(`创建时间: ${created}`);
|
|
337
|
+
for (const line of fields) console.log(line);
|
|
338
|
+
if (item.content !== undefined) console.log(`\n${item.content}`);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function printStatus(data) {
|
|
342
|
+
console.log(`服务版本: ${data?.version ?? '?'}`);
|
|
343
|
+
console.log(`运行时长: ${data?.uptime_s ?? data?.uptimeS ?? '?'} 秒`);
|
|
344
|
+
const memories = data?.memories ?? {};
|
|
345
|
+
console.log(`记忆总数: ${memories.total ?? '?'}`);
|
|
346
|
+
const byType = memories.byType;
|
|
347
|
+
if (Array.isArray(byType)) {
|
|
348
|
+
for (const row of byType) {
|
|
349
|
+
console.log(` - ${row?.type ?? row?.name ?? '?'}: ${row?.count ?? row?.total ?? '?'}`);
|
|
350
|
+
}
|
|
351
|
+
} else if (byType && typeof byType === 'object') {
|
|
352
|
+
for (const [type, count] of Object.entries(byType)) {
|
|
353
|
+
console.log(` - ${type}: ${count}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
const entities = data?.entities;
|
|
357
|
+
if (entities != null) {
|
|
358
|
+
const total =
|
|
359
|
+
typeof entities === 'object' && !Array.isArray(entities)
|
|
360
|
+
? entities.total
|
|
361
|
+
: entities;
|
|
362
|
+
console.log(`实体数量: ${total ?? '?'}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/* ---------------------------------- 子命令 ---------------------------------- */
|
|
367
|
+
|
|
368
|
+
async function cmdConfig(cfg, sub, rest) {
|
|
369
|
+
const subCmd = sub || 'show';
|
|
370
|
+
|
|
371
|
+
if (subCmd === 'set') {
|
|
372
|
+
const [url, tokenArg] = rest;
|
|
373
|
+
if (!url) {
|
|
374
|
+
fail(
|
|
375
|
+
cfg,
|
|
376
|
+
`用法: ${CLI_NAME} config set <url> <token>\n示例: ${CLI_NAME} config set ${DEFAULT_URL} my-token`
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
380
|
+
fail(cfg, `无效的服务地址: ${url}(应以 http:// 或 https:// 开头)`);
|
|
381
|
+
}
|
|
382
|
+
const existing = readConfigFile();
|
|
383
|
+
const token = tokenArg ?? existing.token ?? '';
|
|
384
|
+
await fsp.mkdir(CONFIG_DIR, { recursive: true });
|
|
385
|
+
await fsp.writeFile(CONFIG_PATH, `${JSON.stringify({ url, token }, null, 2)}\n`, 'utf8');
|
|
386
|
+
if (cfg.jsonMode) {
|
|
387
|
+
console.log(stringify({ configPath: CONFIG_PATH, url, tokenConfigured: Boolean(token) }));
|
|
388
|
+
} else {
|
|
389
|
+
console.log(`已写入配置文件: ${CONFIG_PATH}`);
|
|
390
|
+
console.log(` url: ${url}`);
|
|
391
|
+
console.log(` token: ${maskToken(token)}`);
|
|
392
|
+
}
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (subCmd === 'show') {
|
|
397
|
+
const hasFile = fs.existsSync(CONFIG_PATH);
|
|
398
|
+
const file = readConfigFile();
|
|
399
|
+
if (cfg.jsonMode) {
|
|
400
|
+
console.log(
|
|
401
|
+
stringify({
|
|
402
|
+
configPath: CONFIG_PATH,
|
|
403
|
+
configExists: hasFile,
|
|
404
|
+
url: file.url ?? DEFAULT_URL,
|
|
405
|
+
token: maskToken(file.token),
|
|
406
|
+
})
|
|
407
|
+
);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (!hasFile) {
|
|
411
|
+
console.log(`尚未创建配置文件(未配置): ${CONFIG_PATH}`);
|
|
412
|
+
console.log('');
|
|
413
|
+
console.log(`可运行 \`${CLI_NAME} config set <url> <token>\` 创建配置;`);
|
|
414
|
+
console.log('token 可在 DSH 面板「设置 → 外部访问」或插件设置中查看。');
|
|
415
|
+
console.log(
|
|
416
|
+
`未配置时使用默认地址 ${DEFAULT_URL},也可用环境变量 DSH_MNEME_URL / DSH_MNEME_TOKEN。`
|
|
417
|
+
);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
console.log(`配置文件: ${CONFIG_PATH}`);
|
|
421
|
+
console.log(` url: ${file.url ?? `(未设置,默认 ${DEFAULT_URL})`}`);
|
|
422
|
+
console.log(` token: ${maskToken(file.token)}`);
|
|
423
|
+
console.log('');
|
|
424
|
+
console.log('配置优先级: --url/--token > 环境变量 DSH_MNEME_URL / DSH_MNEME_TOKEN > 配置文件');
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (subCmd === 'path') {
|
|
429
|
+
console.log(CONFIG_PATH);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
fail(cfg, `未知的 config 子命令: ${subCmd}\n可用子命令: set / show / path`);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async function cmdStatus(cfg) {
|
|
437
|
+
requireToken(cfg);
|
|
438
|
+
const data = await apiRequest(cfg, 'GET', '/status');
|
|
439
|
+
if (cfg.jsonMode) {
|
|
440
|
+
console.log(stringify(data));
|
|
441
|
+
} else {
|
|
442
|
+
printStatus(data);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async function cmdList(cfg, flags) {
|
|
447
|
+
requireToken(cfg);
|
|
448
|
+
const query = {
|
|
449
|
+
type: flagValue(flags, 'type'),
|
|
450
|
+
minImportance: parseIntArg(cfg, flagValue(flags, 'min-importance', 'minImportance'), '--min-importance'),
|
|
451
|
+
source: flagValue(flags, 'source'),
|
|
452
|
+
limit: parseIntArg(cfg, flagValue(flags, 'limit'), '--limit'),
|
|
453
|
+
offset: parseIntArg(cfg, flagValue(flags, 'offset'), '--offset'),
|
|
454
|
+
};
|
|
455
|
+
const data = await apiRequest(cfg, 'GET', '/memories', { query });
|
|
456
|
+
if (cfg.jsonMode) {
|
|
457
|
+
console.log(stringify(data));
|
|
458
|
+
} else {
|
|
459
|
+
printMemoryItems(data);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async function cmdSearch(cfg, rest, flags) {
|
|
464
|
+
requireToken(cfg);
|
|
465
|
+
const q = rest[0];
|
|
466
|
+
if (!q) {
|
|
467
|
+
fail(
|
|
468
|
+
cfg,
|
|
469
|
+
`用法: ${CLI_NAME} search <query> [--mode m] [--topk n]\n` +
|
|
470
|
+
`示例: ${CLI_NAME} search "部署流程" --mode vector --topk 5`
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
const mode = flagValue(flags, 'mode');
|
|
474
|
+
if (mode !== undefined && !SEARCH_MODES.includes(mode)) {
|
|
475
|
+
fail(cfg, `无效的 --mode: ${mode}(可选: ${SEARCH_MODES.join(' | ')})`);
|
|
476
|
+
}
|
|
477
|
+
const topK = parseIntArg(cfg, flagValue(flags, 'topk', 'top-k', 'topK'), '--topk');
|
|
478
|
+
const data = await apiRequest(cfg, 'GET', '/search', { query: { q, mode, topK } });
|
|
479
|
+
if (cfg.jsonMode) {
|
|
480
|
+
console.log(stringify(data));
|
|
481
|
+
} else {
|
|
482
|
+
printMemoryItems(data, { withMode: true });
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async function cmdGet(cfg, rest) {
|
|
487
|
+
requireToken(cfg);
|
|
488
|
+
const id = rest[0];
|
|
489
|
+
if (!id) {
|
|
490
|
+
fail(cfg, `用法: ${CLI_NAME} get <id> [--json]`);
|
|
491
|
+
}
|
|
492
|
+
const data = await apiRequest(cfg, 'GET', `/memories/${encodeURIComponent(id)}`);
|
|
493
|
+
if (cfg.jsonMode) {
|
|
494
|
+
console.log(stringify(data));
|
|
495
|
+
} else {
|
|
496
|
+
printMemoryDetail(data);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async function cmdAdd(cfg, flags) {
|
|
501
|
+
requireToken(cfg);
|
|
502
|
+
const type = flagValue(flags, 'type');
|
|
503
|
+
const title = flagValue(flags, 'title');
|
|
504
|
+
const content = flagValue(flags, 'content');
|
|
505
|
+
if (!type || !title || !content) {
|
|
506
|
+
fail(
|
|
507
|
+
cfg,
|
|
508
|
+
`用法: ${CLI_NAME} add --type <${MEMORY_TYPES.join('|')}> --title <标题> --content <内容>\n` +
|
|
509
|
+
` [--importance n] [--tags a,b] [--source s]\n\n` +
|
|
510
|
+
`示例: ${CLI_NAME} add --type decision --title "采用 SQLite" --content "存储层使用 node:sqlite"`
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
if (!MEMORY_TYPES.includes(type)) {
|
|
514
|
+
fail(cfg, `无效的 --type: ${type}(可选: ${MEMORY_TYPES.join(' | ')})`);
|
|
515
|
+
}
|
|
516
|
+
const importanceRaw = flagValue(flags, 'importance');
|
|
517
|
+
let importance;
|
|
518
|
+
if (importanceRaw !== undefined) {
|
|
519
|
+
importance = Number(importanceRaw);
|
|
520
|
+
if (!Number.isInteger(importance) || importance < 1 || importance > 5) {
|
|
521
|
+
fail(cfg, `无效的 --importance: ${importanceRaw}(应为 1-5 的整数)`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
const tagsRaw = flagValue(flags, 'tags');
|
|
525
|
+
const tags =
|
|
526
|
+
tagsRaw !== undefined
|
|
527
|
+
? tagsRaw.split(',').map((t) => t.trim()).filter(Boolean)
|
|
528
|
+
: undefined;
|
|
529
|
+
const source = flagValue(flags, 'source');
|
|
530
|
+
|
|
531
|
+
const body = { type, title, content };
|
|
532
|
+
if (importance !== undefined) body.importance = importance;
|
|
533
|
+
if (tags) body.tags = tags;
|
|
534
|
+
if (source) body.source = source;
|
|
535
|
+
|
|
536
|
+
const data = await apiRequest(cfg, 'POST', '/memories', { body });
|
|
537
|
+
if (cfg.jsonMode) {
|
|
538
|
+
console.log(stringify(data));
|
|
539
|
+
} else {
|
|
540
|
+
console.log('已添加记忆:');
|
|
541
|
+
printMemoryDetail(data);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
async function cmdDelete(cfg, rest) {
|
|
546
|
+
requireToken(cfg);
|
|
547
|
+
const id = rest[0];
|
|
548
|
+
if (!id) {
|
|
549
|
+
fail(cfg, `用法: ${CLI_NAME} delete <id>`);
|
|
550
|
+
}
|
|
551
|
+
const data = await apiRequest(cfg, 'DELETE', `/memories/${encodeURIComponent(id)}`);
|
|
552
|
+
if (cfg.jsonMode) {
|
|
553
|
+
console.log(stringify(data));
|
|
554
|
+
} else {
|
|
555
|
+
console.log(`已删除记忆 ${id}`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/* ---------------------------------- 入口 ---------------------------------- */
|
|
560
|
+
|
|
561
|
+
async function main(argv) {
|
|
562
|
+
const { positionals, flags } = parseArgv(argv);
|
|
563
|
+
const cmd = positionals[0];
|
|
564
|
+
const rest = positionals.slice(1);
|
|
565
|
+
|
|
566
|
+
if (!cmd || cmd === '-h' || cmd === '--help' || cmd === 'help') {
|
|
567
|
+
console.log(USAGE);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (cmd === '-V' || cmd === '--version' || cmd === 'version') {
|
|
571
|
+
console.log(PKG_VERSION);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const cfg = resolveConfig(flags);
|
|
576
|
+
cfg.jsonMode = flagBool(flags, 'json');
|
|
577
|
+
|
|
578
|
+
switch (cmd) {
|
|
579
|
+
case 'config':
|
|
580
|
+
return cmdConfig(cfg, rest[0], rest.slice(1));
|
|
581
|
+
case 'status':
|
|
582
|
+
return cmdStatus(cfg);
|
|
583
|
+
case 'list':
|
|
584
|
+
case 'ls':
|
|
585
|
+
return cmdList(cfg, flags);
|
|
586
|
+
case 'search':
|
|
587
|
+
return cmdSearch(cfg, rest, flags);
|
|
588
|
+
case 'get':
|
|
589
|
+
return cmdGet(cfg, rest);
|
|
590
|
+
case 'add':
|
|
591
|
+
return cmdAdd(cfg, flags);
|
|
592
|
+
case 'delete':
|
|
593
|
+
case 'rm':
|
|
594
|
+
return cmdDelete(cfg, rest);
|
|
595
|
+
default:
|
|
596
|
+
fail(cfg, `未知命令: ${cmd}\n运行 \`${CLI_NAME} --help\` 查看帮助。`);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
main(process.argv.slice(2)).catch((err) => {
|
|
601
|
+
console.error(err?.stack ?? String(err));
|
|
602
|
+
process.exit(1);
|
|
603
|
+
});
|