@huicai/cli 2026.711.0 → 2026.711.24617
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/LICENSE +43 -21
- package/package.json +4 -3
- package/src/advisor.js +1 -46
- package/src/agent.js +1 -518
- package/src/api.js +1 -359
- package/src/autofix.js +1 -90
- package/src/banner.js +1 -83
- package/src/checkpoints.js +1 -99
- package/src/clipboard.js +1 -34
- package/src/codeindex.js +1 -229
- package/src/colors.js +1 -47
- package/src/commands.js +1 -1294
- package/src/compaction.js +1 -144
- package/src/config.js +1 -86
- package/src/consult.js +1 -65
- package/src/context-report.js +1 -119
- package/src/context.js +1 -152
- package/src/cron.js +1 -64
- package/src/daemon.js +1 -62
- package/src/device.js +1 -88
- package/src/diffview.js +1 -115
- package/src/doctor.js +1 -72
- package/src/executors.js +1 -391
- package/src/forks.js +1 -59
- package/src/gitflow.js +1 -140
- package/src/hooks.js +1 -207
- package/src/ide.js +1 -53
- package/src/input.js +1 -182
- package/src/insights.js +1 -102
- package/src/kbsync.js +1 -141
- package/src/markdown.js +0 -0
- package/src/markup-filter.js +1 -85
- package/src/mcp.js +1 -207
- package/src/memory.js +1 -161
- package/src/permissions.js +1 -120
- package/src/render.js +1 -138
- package/src/repl.js +1 -271
- package/src/scan.js +1 -84
- package/src/schedule.js +1 -48
- package/src/session.js +1 -140
- package/src/settings.js +0 -0
- package/src/skills.js +1 -248
- package/src/subagent.js +1 -183
- package/src/todo.js +1 -75
- package/src/tools.js +1 -147
- package/src/transcript.js +1 -94
- package/src/tui/app.js +1 -666
- package/src/usage.js +1 -43
- package/src/watch.js +1 -74
package/src/kbsync.js
CHANGED
|
@@ -1,141 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// 检测策略:默认对比工作区与 HEAD(已跟踪改动 + 未跟踪新文件);给定 ref 则 git diff --name-status <ref>。
|
|
3
|
-
import fs from 'node:fs';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { execFileSync } from 'node:child_process';
|
|
6
|
-
import { syncKnowledge } from './api.js';
|
|
7
|
-
import { IGNORE, MAX_FILE_BYTES } from './codeindex.js';
|
|
8
|
-
|
|
9
|
-
const BATCH = 20; // 每批提交的文件数(服务端逐文件分块向量化,分批避免单请求过大/超时)
|
|
10
|
-
|
|
11
|
-
function git(cwd, args) {
|
|
12
|
-
return execFileSync('git', args, { cwd, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }).trim();
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function isGitRepo(cwd) {
|
|
16
|
-
try {
|
|
17
|
-
git(cwd, ['rev-parse', '--is-inside-work-tree']);
|
|
18
|
-
return true;
|
|
19
|
-
} catch {
|
|
20
|
-
return false;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** 路径是否落在被忽略目录下(node_modules/.git/dist…),避免把依赖/产物灌进知识库。 */
|
|
25
|
-
function inIgnoredDir(rel) {
|
|
26
|
-
return rel.split('/').some((seg) => IGNORE.has(seg));
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** 空字节嗅探:判定二进制(不入库)。 */
|
|
30
|
-
function isBinary(buf) {
|
|
31
|
-
const n = Math.min(buf.length, 4096);
|
|
32
|
-
for (let i = 0; i < n; i++) if (buf[i] === 0) return true;
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* 用 git 检测变更文件。
|
|
38
|
-
* - 指定 base(ref/范围,如 origin/main、HEAD~3、abc123):git diff --name-status <base>
|
|
39
|
-
* - 不指定:工作区相对 HEAD 的改动(已跟踪 A/M/D/R)+ 未跟踪新文件(视为新增)
|
|
40
|
-
* @returns {{changed:string[], deleted:string[]}}
|
|
41
|
-
*/
|
|
42
|
-
export function detectChanges(cwd, base) {
|
|
43
|
-
const changed = new Set();
|
|
44
|
-
const deleted = new Set();
|
|
45
|
-
const parse = (out) => {
|
|
46
|
-
for (const line of out.split('\n')) {
|
|
47
|
-
if (!line.trim()) continue;
|
|
48
|
-
const parts = line.split('\t');
|
|
49
|
-
const code = parts[0][0];
|
|
50
|
-
if (code === 'D') {
|
|
51
|
-
deleted.add(parts[1]);
|
|
52
|
-
} else if (code === 'R' || code === 'C') {
|
|
53
|
-
// Rxxx old new:重命名——旧路径删档、新路径入档(复制 C 只入新档)
|
|
54
|
-
const oldP = parts[1];
|
|
55
|
-
const newP = parts[2] || parts[1];
|
|
56
|
-
if (code === 'R') deleted.add(oldP);
|
|
57
|
-
changed.add(newP);
|
|
58
|
-
} else {
|
|
59
|
-
// A/M/T…:取末列为路径
|
|
60
|
-
changed.add(parts[parts.length - 1]);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
if (base) {
|
|
65
|
-
parse(git(cwd, ['diff', '--name-status', base]));
|
|
66
|
-
} else {
|
|
67
|
-
parse(git(cwd, ['diff', '--name-status', 'HEAD']));
|
|
68
|
-
const others = git(cwd, ['ls-files', '--others', '--exclude-standard']);
|
|
69
|
-
for (const p of others.split('\n')) if (p.trim()) changed.add(p.trim());
|
|
70
|
-
}
|
|
71
|
-
// 删除后又重新出现的以变更为准
|
|
72
|
-
for (const p of changed) deleted.delete(p);
|
|
73
|
-
return { changed: [...changed], deleted: [...deleted] };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* 把检测到的变更组装为同步条目(读取内容、过滤忽略目录/二进制/超大文件)。
|
|
78
|
-
* @returns {{files:Array<{path,content?,deleted?}>, skipped:number, skips:string[]}}
|
|
79
|
-
*/
|
|
80
|
-
export function buildEntries(cwd, changed, deleted, emit = () => {}) {
|
|
81
|
-
const files = [];
|
|
82
|
-
let skipped = 0;
|
|
83
|
-
for (const rel of changed) {
|
|
84
|
-
if (inIgnoredDir(rel)) { skipped++; continue; }
|
|
85
|
-
const abs = path.join(cwd, rel);
|
|
86
|
-
let st;
|
|
87
|
-
try {
|
|
88
|
-
st = fs.statSync(abs);
|
|
89
|
-
} catch {
|
|
90
|
-
skipped++; // 已不在(diff 后又被删)
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
if (!st.isFile()) { skipped++; continue; }
|
|
94
|
-
if (st.size > MAX_FILE_BYTES) {
|
|
95
|
-
emit(`跳过(过大 ${(st.size / 1024).toFixed(0)}KB):${rel}`);
|
|
96
|
-
skipped++;
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
const buf = fs.readFileSync(abs);
|
|
100
|
-
if (isBinary(buf)) { skipped++; continue; }
|
|
101
|
-
files.push({ path: rel, content: buf.toString('utf8'), deleted: false });
|
|
102
|
-
}
|
|
103
|
-
for (const rel of deleted) {
|
|
104
|
-
if (inIgnoredDir(rel)) continue;
|
|
105
|
-
files.push({ path: rel, deleted: true });
|
|
106
|
-
}
|
|
107
|
-
return { files, skipped };
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* 执行 kb-sync:检测变更 → 组装条目 → 分批调用平台增量同步。
|
|
112
|
-
* @param {{cwd:string, config:{baseUrl:string,apiKey:string}, kbId:string|number, base?:string, dry?:boolean, emit?:(t:string)=>void}} opts
|
|
113
|
-
* @returns {Promise<{ok:boolean, empty?:boolean, dry?:boolean, added:number, removed:number, skipped:number, accepted?:number, message:string, preview?:any[]}>}
|
|
114
|
-
*/
|
|
115
|
-
export async function runKbSync({ cwd, config, kbId, base, dry = false, emit = () => {} }) {
|
|
116
|
-
if (!isGitRepo(cwd)) throw new Error('当前目录不是 git 仓库;kb-sync 依赖 git diff 检测变更');
|
|
117
|
-
const { changed, deleted } = detectChanges(cwd, base);
|
|
118
|
-
const { files, skipped } = buildEntries(cwd, changed, deleted, emit);
|
|
119
|
-
|
|
120
|
-
const added = files.filter((f) => !f.deleted).length;
|
|
121
|
-
const removed = files.filter((f) => f.deleted).length;
|
|
122
|
-
if (!files.length) {
|
|
123
|
-
return { ok: true, empty: true, added: 0, removed: 0, skipped, message: base ? `相对 ${base} 无可同步的变更文件` : '工作区无可同步的变更文件' };
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
emit(`检测到 ${added} 个新增/修改、${removed} 个删除${skipped ? `(跳过 ${skipped})` : ''}${base ? `(相对 ${base})` : ''}`);
|
|
127
|
-
if (dry) {
|
|
128
|
-
return { ok: true, dry: true, added, removed, skipped, message: '试运行(未提交)', preview: files.map((f) => ({ path: f.path, deleted: !!f.deleted })) };
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
let accepted = 0;
|
|
132
|
-
const msgs = [];
|
|
133
|
-
for (let i = 0; i < files.length; i += BATCH) {
|
|
134
|
-
const batch = files.slice(i, i + BATCH);
|
|
135
|
-
if (files.length > BATCH) emit(`同步中… ${Math.min(i + BATCH, files.length)}/${files.length}`);
|
|
136
|
-
const r = await syncKnowledge(config.baseUrl, config.apiKey, kbId, batch);
|
|
137
|
-
accepted += r?.accepted || 0;
|
|
138
|
-
if (r?.message) msgs.push(r.message);
|
|
139
|
-
}
|
|
140
|
-
return { ok: true, added, removed, skipped, accepted, message: msgs.join(';') || '同步完成' };
|
|
141
|
-
}
|
|
1
|
+
import e from"node:fs";import t from"node:path";import{execFileSync as n}from"node:child_process";import{syncKnowledge as i}from"./api.js";import{IGNORE as o,MAX_FILE_BYTES as s}from"./codeindex.js";function r(e,t){return n("git",t,{cwd:e,encoding:"utf8",maxBuffer:67108864}).trim()}function d(e){return e.split("/").some(e=>o.has(e))}function c(e){const t=Math.min(e.length,4096);for(let n=0;n<t;n++)if(0===e[n])return!0;return!1}export function detectChanges(e,t){const n=new Set,i=new Set,o=e=>{for(const t of e.split("\n")){if(!t.trim())continue;const e=t.split("\t"),o=e[0][0];if("D"===o)i.add(e[1]);else if("R"===o||"C"===o){const t=e[1],s=e[2]||e[1];"R"===o&&i.add(t),n.add(s)}else n.add(e[e.length-1])}};if(t)o(r(e,["diff","--name-status",t]));else{o(r(e,["diff","--name-status","HEAD"]));const t=r(e,["ls-files","--others","--exclude-standard"]);for(const e of t.split("\n"))e.trim()&&n.add(e.trim())}for(const e of n)i.delete(e);return{changed:[...n],deleted:[...i]}}export function buildEntries(n,i,o,r=()=>{}){const f=[];let a=0;for(const o of i){if(d(o)){a++;continue}const i=t.join(n,o);let l;try{l=e.statSync(i)}catch{a++;continue}if(!l.isFile()){a++;continue}if(l.size>s){r(`跳过(过大 ${(l.size/1024).toFixed(0)}KB):${o}`),a++;continue}const p=e.readFileSync(i);c(p)?a++:f.push({path:o,content:p.toString("utf8"),deleted:!1})}for(const e of o)d(e)||f.push({path:e,deleted:!0});return{files:f,skipped:a}}export async function runKbSync({cwd:e,config:t,kbId:n,base:o,dry:s=!1,emit:d=()=>{}}){if(!function(e){try{return r(e,["rev-parse","--is-inside-work-tree"]),!0}catch{return!1}}(e))throw new Error("当前目录不是 git 仓库;kb-sync 依赖 git diff 检测变更");const{changed:c,deleted:f}=detectChanges(e,o),{files:a,skipped:l}=buildEntries(e,c,f,d),p=a.filter(e=>!e.deleted).length,u=a.filter(e=>e.deleted).length;if(!a.length)return{ok:!0,empty:!0,added:0,removed:0,skipped:l,message:o?`相对 ${o} 无可同步的变更文件`:"工作区无可同步的变更文件"};if(d(`检测到 ${p} 个新增/修改、${u} 个删除${l?`(跳过 ${l})`:""}${o?`(相对 ${o})`:""}`),s)return{ok:!0,dry:!0,added:p,removed:u,skipped:l,message:"试运行(未提交)",preview:a.map(e=>({path:e.path,deleted:!!e.deleted}))};let m=0;const h=[];for(let e=0;e<a.length;e+=20){const o=a.slice(e,e+20);a.length>20&&d(`同步中… ${Math.min(e+20,a.length)}/${a.length}`);const s=await i(t.baseUrl,t.apiKey,n,o);m+=s?.accepted||0,s?.message&&h.push(s.message)}return{ok:!0,added:p,removed:u,skipped:l,accepted:m,message:h.join(";")||"同步完成"}}
|
package/src/markdown.js
CHANGED
|
Binary file
|
package/src/markup-filter.js
CHANGED
|
@@ -1,85 +1 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
// 部分模型(如 DeepSeek 系)在长上下文/上下文压缩后会格式退化:不走 function calling 特殊通道,
|
|
4
|
-
// 而把工具调用以纯文本标记写进正文(如 <||DSML||tool_calls>…、<|tool▁calls▁begin|>…)。
|
|
5
|
-
// 这种「调用」不会出现在 tool_calls 里、也不会被执行;若照原样显示,终端/扩展面板会出现成片协议乱码,
|
|
6
|
-
// 且答非所问。CLI 直连原始模型(平台 /v1 为透明代理,不代过滤),须在客户端做同样的拦截。
|
|
7
|
-
//
|
|
8
|
-
// 流式状态机:正常文本原样放行;遇疑似起始符 <| / <| 先持有,在确认窗口内命中工具标记特征
|
|
9
|
-
// (dsml / tool_call / tool▁call / function_call…)→ 进入吞噬模式,本轮剩余内容全部丢弃
|
|
10
|
-
// (此类输出一旦开始,其后即整段协议块,无用户价值);窗口内未命中 → 原样放行
|
|
11
|
-
// (不误杀正文中的 <|x|> 字面量)。每轮模型输出使用独立实例。
|
|
12
|
-
|
|
13
|
-
/** 起始符确认窗口:起始符后至多累计这么多字符仍未命中特征则放行。 */
|
|
14
|
-
const CONFIRM_WINDOW = 48;
|
|
15
|
-
|
|
16
|
-
/** 工具标记特征(起始符后窗口内匹配):DSML / tool_calls / tool▁calls / function_call 等变体。 */
|
|
17
|
-
const MARKUP_RE = /dsml|tool[_▁]?call|function[_▁]?call/i;
|
|
18
|
-
|
|
19
|
-
/** 找到最早的疑似起始符(<| 或 <|)下标,无则 -1。 */
|
|
20
|
-
function markerStart(s) {
|
|
21
|
-
for (let i = 0; i < s.length - 1; i++) {
|
|
22
|
-
if (s[i] === '<' && (s[i + 1] === '|' || s[i + 1] === '|')) return i;
|
|
23
|
-
}
|
|
24
|
-
return -1;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export class MarkupFilter {
|
|
28
|
-
constructor() {
|
|
29
|
-
this.held = '';
|
|
30
|
-
this.swallowing = false;
|
|
31
|
-
/** 本轮是否检出过文本化工具标记。 */
|
|
32
|
-
this.sawMarkup = false;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** 喂入一段增量,返回可安全下发的文本(可能为空串)。 */
|
|
36
|
-
feed(delta) {
|
|
37
|
-
if (!delta || this.swallowing) return '';
|
|
38
|
-
this.held += delta;
|
|
39
|
-
let out = '';
|
|
40
|
-
for (;;) {
|
|
41
|
-
const idx = markerStart(this.held);
|
|
42
|
-
if (idx < 0) {
|
|
43
|
-
// 无疑似起始:保留可能被增量边界截断的尾部 '<',其余放行
|
|
44
|
-
const keep = this.held.endsWith('<') ? 1 : 0;
|
|
45
|
-
out += this.held.slice(0, this.held.length - keep);
|
|
46
|
-
this.held = keep ? '<' : '';
|
|
47
|
-
return out;
|
|
48
|
-
}
|
|
49
|
-
out += this.held.slice(0, idx);
|
|
50
|
-
this.held = this.held.slice(idx);
|
|
51
|
-
// held 现以 <| 或 <| 开头:在确认窗口内判定
|
|
52
|
-
if (MARKUP_RE.test(this.held.slice(0, CONFIRM_WINDOW))) {
|
|
53
|
-
this.swallowing = true;
|
|
54
|
-
this.sawMarkup = true;
|
|
55
|
-
this.held = '';
|
|
56
|
-
return out;
|
|
57
|
-
}
|
|
58
|
-
if (this.held.length < CONFIRM_WINDOW) return out; // 证据不足:持有等待更多增量
|
|
59
|
-
// 窗口已满仍非标记:放行起始两字符,继续扫描其后内容
|
|
60
|
-
out += this.held.slice(0, 2);
|
|
61
|
-
this.held = this.held.slice(2);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** 流结束:放行仍在持有的未确认内容(吞噬中则丢弃)。 */
|
|
66
|
-
flush() {
|
|
67
|
-
if (this.swallowing) {
|
|
68
|
-
this.held = '';
|
|
69
|
-
return '';
|
|
70
|
-
}
|
|
71
|
-
const rest = this.held;
|
|
72
|
-
this.held = '';
|
|
73
|
-
return rest;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* 一次性清洗全量文本(等价于流式 feed+flush)。
|
|
79
|
-
* @returns {{text:string, sawMarkup:boolean}} text 为清洗后的安全文本(null/空入参返回空串)
|
|
80
|
-
*/
|
|
81
|
-
export function stripMarkup(content) {
|
|
82
|
-
if (!content) return { text: '', sawMarkup: false };
|
|
83
|
-
const f = new MarkupFilter();
|
|
84
|
-
return { text: f.feed(content) + f.flush(), sawMarkup: f.sawMarkup };
|
|
85
|
-
}
|
|
1
|
+
const t=/dsml|tool[_▁]?call|function[_▁]?call/i;function s(t){for(let s=0;s<t.length-1;s++)if("<"===t[s]&&("|"===t[s+1]||"|"===t[s+1]))return s;return-1}export class MarkupFilter{constructor(){this.held="",this.swallowing=!1,this.sawMarkup=!1}feed(e){if(!e||this.swallowing)return"";this.held+=e;let h="";for(;;){const e=s(this.held);if(e<0){const t=this.held.endsWith("<")?1:0;return h+=this.held.slice(0,this.held.length-t),this.held=t?"<":"",h}if(h+=this.held.slice(0,e),this.held=this.held.slice(e),t.test(this.held.slice(0,48)))return this.swallowing=!0,this.sawMarkup=!0,this.held="",h;if(this.held.length<48)return h;h+=this.held.slice(0,2),this.held=this.held.slice(2)}}flush(){if(this.swallowing)return this.held="","";const t=this.held;return this.held="",t}}export function stripMarkup(t){if(!t)return{text:"",sawMarkup:!1};const s=new MarkupFilter;return{text:s.feed(t)+s.flush(),sawMarkup:s.sawMarkup}}
|
package/src/mcp.js
CHANGED
|
@@ -1,207 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// 换行分隔),握手后拉取其工具,暴露给 Agent 循环调用。配置形状为通用 mcpServers 格式:
|
|
3
|
-
// { "mcpServers": { "<名>": { "command": "npx", "args": ["-y","@modelcontextprotocol/server-filesystem","<路径>"], "env": {} } } }
|
|
4
|
-
// 未配置该文件时完全无副作用(返回空工具集)。
|
|
5
|
-
|
|
6
|
-
import fs from 'node:fs';
|
|
7
|
-
import path from 'node:path';
|
|
8
|
-
import { spawn } from 'node:child_process';
|
|
9
|
-
import { CONFIG_DIR } from './config.js';
|
|
10
|
-
import { c } from './colors.js';
|
|
11
|
-
|
|
12
|
-
export const MCP_CONFIG_PATH = path.join(CONFIG_DIR, 'mcp.json');
|
|
13
|
-
|
|
14
|
-
const INIT_TIMEOUT = 20_000;
|
|
15
|
-
const CALL_TIMEOUT = 120_000;
|
|
16
|
-
const PROTOCOL_VERSION = '2024-11-05';
|
|
17
|
-
|
|
18
|
-
/** 单个 MCP 服务的 stdio JSON-RPC 客户端。 */
|
|
19
|
-
class McpStdioClient {
|
|
20
|
-
constructor(name, spec) {
|
|
21
|
-
this.name = name;
|
|
22
|
-
this.spec = spec;
|
|
23
|
-
this.child = null;
|
|
24
|
-
this.buffer = '';
|
|
25
|
-
this.nextId = 1;
|
|
26
|
-
this.pending = new Map(); // id -> {resolve, reject, timer}
|
|
27
|
-
this.closed = false;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async start() {
|
|
31
|
-
const env = { ...process.env, ...(this.spec.env || {}) };
|
|
32
|
-
this.child = spawn(this.spec.command, this.spec.args || [], {
|
|
33
|
-
env,
|
|
34
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
35
|
-
});
|
|
36
|
-
this.child.on('error', (e) => this._failAll(new Error(`MCP[${this.name}] 进程错误:${e.message}`)));
|
|
37
|
-
this.child.on('exit', () => {
|
|
38
|
-
this.closed = true;
|
|
39
|
-
this._failAll(new Error(`MCP[${this.name}] 进程已退出`));
|
|
40
|
-
});
|
|
41
|
-
this.child.stdout.setEncoding('utf8');
|
|
42
|
-
this.child.stdout.on('data', (chunk) => this._onData(chunk));
|
|
43
|
-
// stderr 仅调试用,静默丢弃避免污染 TUI
|
|
44
|
-
this.child.stderr.on('data', () => {});
|
|
45
|
-
|
|
46
|
-
// 握手:initialize -> notifications/initialized
|
|
47
|
-
await this._request('initialize', {
|
|
48
|
-
protocolVersion: PROTOCOL_VERSION,
|
|
49
|
-
capabilities: {},
|
|
50
|
-
clientInfo: { name: 'huicai-cli', version: '0.2.0' },
|
|
51
|
-
}, INIT_TIMEOUT);
|
|
52
|
-
this._notify('notifications/initialized', {});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async listTools() {
|
|
56
|
-
const res = await this._request('tools/list', {}, INIT_TIMEOUT);
|
|
57
|
-
return Array.isArray(res?.tools) ? res.tools : [];
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async callTool(toolName, args) {
|
|
61
|
-
const res = await this._request('tools/call', { name: toolName, arguments: args || {} }, CALL_TIMEOUT);
|
|
62
|
-
// 结果内容为 content 数组(text/其它);拼接 text 回填模型
|
|
63
|
-
const parts = Array.isArray(res?.content) ? res.content : [];
|
|
64
|
-
const text = parts
|
|
65
|
-
.map((p) => (p?.type === 'text' ? p.text : p?.text || JSON.stringify(p)))
|
|
66
|
-
.filter(Boolean)
|
|
67
|
-
.join('\n');
|
|
68
|
-
if (res?.isError) return `工具报错:${text || '(无详情)'}`;
|
|
69
|
-
return text || '(无输出)';
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
close() {
|
|
73
|
-
this.closed = true;
|
|
74
|
-
try {
|
|
75
|
-
this.child?.kill('SIGTERM');
|
|
76
|
-
} catch {
|
|
77
|
-
/* ignore */
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
_onData(chunk) {
|
|
82
|
-
this.buffer += chunk;
|
|
83
|
-
let idx;
|
|
84
|
-
while ((idx = this.buffer.indexOf('\n')) >= 0) {
|
|
85
|
-
const line = this.buffer.slice(0, idx).trim();
|
|
86
|
-
this.buffer = this.buffer.slice(idx + 1);
|
|
87
|
-
if (!line) continue;
|
|
88
|
-
let msg;
|
|
89
|
-
try {
|
|
90
|
-
msg = JSON.parse(line);
|
|
91
|
-
} catch {
|
|
92
|
-
continue; // 非 JSON 行(部分服务打印日志到 stdout)忽略
|
|
93
|
-
}
|
|
94
|
-
if (msg.id != null && this.pending.has(msg.id)) {
|
|
95
|
-
const { resolve, reject, timer } = this.pending.get(msg.id);
|
|
96
|
-
clearTimeout(timer);
|
|
97
|
-
this.pending.delete(msg.id);
|
|
98
|
-
if (msg.error) reject(new Error(msg.error.message || 'MCP 错误'));
|
|
99
|
-
else resolve(msg.result);
|
|
100
|
-
}
|
|
101
|
-
// 服务端通知(无匹配 id)忽略
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
_request(method, params, timeoutMs) {
|
|
106
|
-
if (this.closed) return Promise.reject(new Error(`MCP[${this.name}] 已关闭`));
|
|
107
|
-
const id = this.nextId++;
|
|
108
|
-
const payload = JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n';
|
|
109
|
-
return new Promise((resolve, reject) => {
|
|
110
|
-
const timer = setTimeout(() => {
|
|
111
|
-
this.pending.delete(id);
|
|
112
|
-
reject(new Error(`MCP[${this.name}] ${method} 超时`));
|
|
113
|
-
}, timeoutMs);
|
|
114
|
-
this.pending.set(id, { resolve, reject, timer });
|
|
115
|
-
try {
|
|
116
|
-
this.child.stdin.write(payload);
|
|
117
|
-
} catch (e) {
|
|
118
|
-
clearTimeout(timer);
|
|
119
|
-
this.pending.delete(id);
|
|
120
|
-
reject(e);
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
_notify(method, params) {
|
|
126
|
-
try {
|
|
127
|
-
this.child.stdin.write(JSON.stringify({ jsonrpc: '2.0', method, params }) + '\n');
|
|
128
|
-
} catch {
|
|
129
|
-
/* ignore */
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
_failAll(err) {
|
|
134
|
-
for (const { reject, timer } of this.pending.values()) {
|
|
135
|
-
clearTimeout(timer);
|
|
136
|
-
reject(err);
|
|
137
|
-
}
|
|
138
|
-
this.pending.clear();
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** 规整为合法 OpenAI 函数名。 */
|
|
143
|
-
function sanitize(name) {
|
|
144
|
-
const s = String(name).replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
145
|
-
return s.length > 60 ? s.slice(0, 60) : s;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function readConfig() {
|
|
149
|
-
try {
|
|
150
|
-
const j = JSON.parse(fs.readFileSync(MCP_CONFIG_PATH, 'utf8'));
|
|
151
|
-
return j?.mcpServers && typeof j.mcpServers === 'object' ? j.mcpServers : {};
|
|
152
|
-
} catch {
|
|
153
|
-
return {};
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* 启动所有已配置的 MCP 服务并汇集其工具。
|
|
159
|
-
* @returns {Promise<{tools:{spec:object,run:Function,readOnly:boolean}[], clients:McpStdioClient[]}>}
|
|
160
|
-
*/
|
|
161
|
-
export async function loadMcpTools() {
|
|
162
|
-
const servers = readConfig();
|
|
163
|
-
const names = Object.keys(servers);
|
|
164
|
-
const tools = [];
|
|
165
|
-
const clients = [];
|
|
166
|
-
if (names.length === 0) return { tools, clients };
|
|
167
|
-
|
|
168
|
-
for (const name of names) {
|
|
169
|
-
const spec = servers[name] || {};
|
|
170
|
-
if (!spec.command) continue;
|
|
171
|
-
const client = new McpStdioClient(name, spec);
|
|
172
|
-
try {
|
|
173
|
-
await client.start();
|
|
174
|
-
const mcpTools = await client.listTools();
|
|
175
|
-
clients.push(client);
|
|
176
|
-
for (const t of mcpTools) {
|
|
177
|
-
const realName = t.name;
|
|
178
|
-
const fnName = sanitize(`mcp__${name}__${realName}`);
|
|
179
|
-
const readOnly = t.annotations?.readOnlyHint === true;
|
|
180
|
-
tools.push({
|
|
181
|
-
readOnly,
|
|
182
|
-
spec: {
|
|
183
|
-
type: 'function',
|
|
184
|
-
function: {
|
|
185
|
-
name: fnName,
|
|
186
|
-
description: `[MCP:${name}] ${t.description || realName}`,
|
|
187
|
-
parameters: t.inputSchema && typeof t.inputSchema === 'object'
|
|
188
|
-
? t.inputSchema
|
|
189
|
-
: { type: 'object', properties: {} },
|
|
190
|
-
},
|
|
191
|
-
},
|
|
192
|
-
run: (args) => client.callTool(realName, args),
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
process.stdout.write(c.gray(` · MCP[${name}] 已连接,工具 ${mcpTools.length} 个`) + '\n');
|
|
196
|
-
} catch (e) {
|
|
197
|
-
client.close();
|
|
198
|
-
process.stdout.write(c.yellow(` · MCP[${name}] 加载失败,已跳过:${e.message}`) + '\n');
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
return { tools, clients };
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/** 关闭全部 MCP 子进程。 */
|
|
205
|
-
export function closeMcp(clients) {
|
|
206
|
-
for (const c2 of clients || []) c2.close();
|
|
207
|
-
}
|
|
1
|
+
import t from"node:fs";import e from"node:path";import{spawn as i}from"node:child_process";import{CONFIG_DIR as s}from"./config.js";import{c as n}from"./colors.js";export const MCP_CONFIG_PATH=e.join(s,"mcp.json");class o{constructor(t,e){this.name=t,this.spec=e,this.child=null,this.buffer="",this.nextId=1,this.pending=new Map,this.closed=!1}async start(){const t={...process.env,...this.spec.env||{}};this.child=i(this.spec.command,this.spec.args||[],{env:t,stdio:["pipe","pipe","pipe"]}),this.child.on("error",t=>this._failAll(new Error(`MCP[${this.name}] 进程错误:${t.message}`))),this.child.on("exit",()=>{this.closed=!0,this._failAll(new Error(`MCP[${this.name}] 进程已退出`))}),this.child.stdout.setEncoding("utf8"),this.child.stdout.on("data",t=>this._onData(t)),this.child.stderr.on("data",()=>{}),await this._request("initialize",{protocolVersion:"2024-11-05",capabilities:{},clientInfo:{name:"huicai-cli",version:"0.2.0"}},2e4),this._notify("notifications/initialized",{})}async listTools(){const t=await this._request("tools/list",{},2e4);return Array.isArray(t?.tools)?t.tools:[]}async callTool(t,e){const i=await this._request("tools/call",{name:t,arguments:e||{}},12e4),s=(Array.isArray(i?.content)?i.content:[]).map(t=>"text"===t?.type?t.text:t?.text||JSON.stringify(t)).filter(Boolean).join("\n");return i?.isError?`工具报错:${s||"(无详情)"}`:s||"(无输出)"}close(){this.closed=!0;try{this.child?.kill("SIGTERM")}catch{}}_onData(t){let e;for(this.buffer+=t;(e=this.buffer.indexOf("\n"))>=0;){const t=this.buffer.slice(0,e).trim();if(this.buffer=this.buffer.slice(e+1),!t)continue;let i;try{i=JSON.parse(t)}catch{continue}if(null!=i.id&&this.pending.has(i.id)){const{resolve:t,reject:e,timer:s}=this.pending.get(i.id);clearTimeout(s),this.pending.delete(i.id),i.error?e(new Error(i.error.message||"MCP 错误")):t(i.result)}}}_request(t,e,i){if(this.closed)return Promise.reject(new Error(`MCP[${this.name}] 已关闭`));const s=this.nextId++,n=JSON.stringify({jsonrpc:"2.0",id:s,method:t,params:e})+"\n";return new Promise((e,o)=>{const r=setTimeout(()=>{this.pending.delete(s),o(new Error(`MCP[${this.name}] ${t} 超时`))},i);this.pending.set(s,{resolve:e,reject:o,timer:r});try{this.child.stdin.write(n)}catch(t){clearTimeout(r),this.pending.delete(s),o(t)}})}_notify(t,e){try{this.child.stdin.write(JSON.stringify({jsonrpc:"2.0",method:t,params:e})+"\n")}catch{}}_failAll(t){for(const{reject:e,timer:i}of this.pending.values())clearTimeout(i),e(t);this.pending.clear()}}function r(t){const e=String(t).replace(/[^a-zA-Z0-9_-]/g,"_");return e.length>60?e.slice(0,60):e}export async function loadMcpTools(){const e=function(){try{const e=JSON.parse(t.readFileSync(MCP_CONFIG_PATH,"utf8"));return e?.mcpServers&&"object"==typeof e.mcpServers?e.mcpServers:{}}catch{return{}}}(),i=Object.keys(e),s=[],c=[];if(0===i.length)return{tools:s,clients:c};for(const t of i){const i=e[t]||{};if(!i.command)continue;const a=new o(t,i);try{await a.start();const e=await a.listTools();c.push(a);for(const i of e){const e=i.name,n=r(`mcp__${t}__${e}`),o=!0===i.annotations?.readOnlyHint;s.push({readOnly:o,spec:{type:"function",function:{name:n,description:`[MCP:${t}] ${i.description||e}`,parameters:i.inputSchema&&"object"==typeof i.inputSchema?i.inputSchema:{type:"object",properties:{}}}},run:t=>a.callTool(e,t)})}process.stdout.write(n.gray(` · MCP[${t}] 已连接,工具 ${e.length} 个`)+"\n")}catch(e){a.close(),process.stdout.write(n.yellow(` · MCP[${t}] 加载失败,已跳过:${e.message}`)+"\n")}}return{tools:s,clients:c}}export function closeMcp(t){for(const e of t||[])e.close()}
|
package/src/memory.js
CHANGED
|
@@ -1,161 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// 模型经内置 memory_write 工具主动记录「值得跨会话记住的事实」(用户偏好、项目结论、约定等),
|
|
3
|
-
// 每次新会话由 context.js 注入系统提示词——即「长期记忆」;「短期记忆」= 会话消息 + 非破坏式上下文压缩摘要。
|
|
4
|
-
// 开关:config.memoryEnabled(默认开,配置 memory:false 或环境变量 HUICAI_MEMORY=0 关闭)。
|
|
5
|
-
|
|
6
|
-
import fs from 'node:fs';
|
|
7
|
-
import path from 'node:path';
|
|
8
|
-
import crypto from 'node:crypto';
|
|
9
|
-
import { CONFIG_DIR } from './config.js';
|
|
10
|
-
import { tokenize } from './codeindex.js';
|
|
11
|
-
|
|
12
|
-
export const MEMORY_DIR = path.join(CONFIG_DIR, 'memory');
|
|
13
|
-
const MAX_MEMORY_CHARS = 6000; // 注入系统提示词的上限(超出保留最新条目)
|
|
14
|
-
const MAX_ENTRY_CHARS = 1000;
|
|
15
|
-
|
|
16
|
-
export function memoryPath(cwd) {
|
|
17
|
-
const h = crypto.createHash('sha1').update(String(cwd || '')).digest('hex').slice(0, 12);
|
|
18
|
-
return path.join(MEMORY_DIR, h + '.md');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** 读取该项目的长期记忆(超限保尾部=最新条目)。无则空串。 */
|
|
22
|
-
export function loadMemory(cwd) {
|
|
23
|
-
try {
|
|
24
|
-
const s = fs.readFileSync(memoryPath(cwd), 'utf8');
|
|
25
|
-
if (s.length <= MAX_MEMORY_CHARS) return s.trim();
|
|
26
|
-
const tail = s.slice(-MAX_MEMORY_CHARS);
|
|
27
|
-
const nl = tail.indexOf('\n');
|
|
28
|
-
return ('…(较早记忆已省略)\n' + (nl >= 0 ? tail.slice(nl + 1) : tail)).trim();
|
|
29
|
-
} catch {
|
|
30
|
-
return '';
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** 追加一条记忆(带日期,best-effort)。返回是否成功。 */
|
|
35
|
-
export function appendMemory(cwd, content) {
|
|
36
|
-
const t = String(content || '').trim();
|
|
37
|
-
if (!t) return false;
|
|
38
|
-
try {
|
|
39
|
-
fs.mkdirSync(MEMORY_DIR, { recursive: true });
|
|
40
|
-
const entry = `- [${new Date().toISOString().slice(0, 10)}] ${t.slice(0, MAX_ENTRY_CHARS)}\n`;
|
|
41
|
-
const p = memoryPath(cwd);
|
|
42
|
-
if (!fs.existsSync(p)) {
|
|
43
|
-
fs.writeFileSync(p, `# 长期记忆(${cwd})\n\n${entry}`, { mode: 0o600 });
|
|
44
|
-
} else {
|
|
45
|
-
fs.appendFileSync(p, entry);
|
|
46
|
-
}
|
|
47
|
-
return true;
|
|
48
|
-
} catch {
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** 整体重写记忆(模型做记忆整理时用)。 */
|
|
54
|
-
export function replaceMemory(cwd, content) {
|
|
55
|
-
try {
|
|
56
|
-
fs.mkdirSync(MEMORY_DIR, { recursive: true });
|
|
57
|
-
fs.writeFileSync(memoryPath(cwd), String(content || '').slice(0, MAX_MEMORY_CHARS * 2), { mode: 0o600 });
|
|
58
|
-
return true;
|
|
59
|
-
} catch {
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function clearMemory(cwd) {
|
|
65
|
-
try {
|
|
66
|
-
fs.unlinkSync(memoryPath(cwd));
|
|
67
|
-
return true;
|
|
68
|
-
} catch {
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** 读取完整长期记忆原文(不截断),供 recall_memory 按相关性检索。 */
|
|
74
|
-
export function readMemoryRaw(cwd) {
|
|
75
|
-
try {
|
|
76
|
-
return fs.readFileSync(memoryPath(cwd), 'utf8');
|
|
77
|
-
} catch {
|
|
78
|
-
return '';
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* 构造 recall_memory 工具(跨会话经验主动召回):在本项目长期记忆里按关键词检索相关条目,
|
|
84
|
-
* 连超出常驻注入预算(6k)的历史也能查到。CJK 感知分词(复用 codeindex.tokenize)。
|
|
85
|
-
*/
|
|
86
|
-
export function makeRecallTool({ cwd }) {
|
|
87
|
-
const spec = {
|
|
88
|
-
type: 'function',
|
|
89
|
-
function: {
|
|
90
|
-
name: 'recall_memory',
|
|
91
|
-
description:
|
|
92
|
-
'在本项目的长期记忆里按关键词检索相关的过往结论/踩坑/约定(连超出常驻注入预算的历史也能查到)。'
|
|
93
|
-
+ '开始一个任务前,若怀疑「以前处理过类似问题」,可先 recall 一下,少走弯路。',
|
|
94
|
-
parameters: {
|
|
95
|
-
type: 'object',
|
|
96
|
-
properties: { query: { type: 'string', description: '关键词/概念' } },
|
|
97
|
-
required: ['query'],
|
|
98
|
-
},
|
|
99
|
-
},
|
|
100
|
-
};
|
|
101
|
-
const run = async (args) => {
|
|
102
|
-
const q = String(args?.query || '').trim();
|
|
103
|
-
if (!q) return '缺少 query';
|
|
104
|
-
const entries = readMemoryRaw(cwd).split('\n').map((l) => l.trim()).filter((l) => l.startsWith('- '));
|
|
105
|
-
if (!entries.length) return '本项目暂无可检索的长期记忆。';
|
|
106
|
-
const qt = new Set(tokenize(q));
|
|
107
|
-
if (!qt.size) return '查询词无有效 token。';
|
|
108
|
-
const scored = entries
|
|
109
|
-
.map((e) => {
|
|
110
|
-
let s = 0;
|
|
111
|
-
for (const t of new Set(tokenize(e))) if (qt.has(t)) s++;
|
|
112
|
-
return { e, s };
|
|
113
|
-
})
|
|
114
|
-
.filter((x) => x.s > 0)
|
|
115
|
-
.sort((a, b) => b.s - a.s)
|
|
116
|
-
.slice(0, 6);
|
|
117
|
-
if (!scored.length) return `未在长期记忆中找到与「${q}」相关的条目。`;
|
|
118
|
-
return '相关的历史记忆:\n' + scored.map((x) => x.e).join('\n');
|
|
119
|
-
};
|
|
120
|
-
return { readOnly: true, spec, run };
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* 构造 memory_write 内置工具(顶层注入;记忆是机制本身,自动放行)。
|
|
125
|
-
* @param {{cwd:string, emit?:(text:string)=>void}} p
|
|
126
|
-
*/
|
|
127
|
-
export function makeMemoryTool({ cwd, emit }) {
|
|
128
|
-
const spec = {
|
|
129
|
-
type: 'function',
|
|
130
|
-
function: {
|
|
131
|
-
name: 'memory_write',
|
|
132
|
-
description:
|
|
133
|
-
'把值得跨会话记住的信息写入本项目的长期记忆(用户偏好、项目关键事实/约定、踩坑结论等)。'
|
|
134
|
-
+ '新会话会自动注入这些记忆。仅记「以后仍然有用」的稳定信息;不要记一次性细节或大段代码。'
|
|
135
|
-
+ 'mode=append 追加一条(默认);mode=replace 用整理后的全文替换(做记忆瘦身时用)。',
|
|
136
|
-
parameters: {
|
|
137
|
-
type: 'object',
|
|
138
|
-
properties: {
|
|
139
|
-
content: { type: 'string', description: '要记住的内容(简洁的一条事实/偏好/结论)' },
|
|
140
|
-
mode: { type: 'string', enum: ['append', 'replace'], description: '追加或整体替换(默认 append)' },
|
|
141
|
-
},
|
|
142
|
-
required: ['content'],
|
|
143
|
-
},
|
|
144
|
-
},
|
|
145
|
-
};
|
|
146
|
-
const run = async (args) => {
|
|
147
|
-
const content = String(args?.content || '').trim();
|
|
148
|
-
if (!content) return 'memory_write 需要非空 content';
|
|
149
|
-
const ok = args?.mode === 'replace' ? replaceMemory(cwd, content) : appendMemory(cwd, content);
|
|
150
|
-
if (ok) {
|
|
151
|
-
try {
|
|
152
|
-
emit?.(content);
|
|
153
|
-
} catch {
|
|
154
|
-
/* ignore */
|
|
155
|
-
}
|
|
156
|
-
return `已写入长期记忆(${args?.mode === 'replace' ? '整体替换' : '追加'})。`;
|
|
157
|
-
}
|
|
158
|
-
return '长期记忆写入失败(磁盘不可写?),可忽略并继续任务。';
|
|
159
|
-
};
|
|
160
|
-
return { readOnly: true, spec, run };
|
|
161
|
-
}
|
|
1
|
+
import e from"node:fs";import r from"node:path";import t from"node:crypto";import{CONFIG_DIR as n}from"./config.js";import{tokenize as o}from"./codeindex.js";export const MEMORY_DIR=r.join(n,"memory");const i=6e3;export function memoryPath(e){const n=t.createHash("sha1").update(String(e||"")).digest("hex").slice(0,12);return r.join(MEMORY_DIR,n+".md")}export function loadMemory(r){try{const t=e.readFileSync(memoryPath(r),"utf8");if(t.length<=i)return t.trim();const n=t.slice(-6e3),o=n.indexOf("\n");return("…(较早记忆已省略)\n"+(o>=0?n.slice(o+1):n)).trim()}catch{return""}}export function appendMemory(r,t){const n=String(t||"").trim();if(!n)return!1;try{e.mkdirSync(MEMORY_DIR,{recursive:!0});const t=`- [${(new Date).toISOString().slice(0,10)}] ${n.slice(0,1e3)}\n`,o=memoryPath(r);return e.existsSync(o)?e.appendFileSync(o,t):e.writeFileSync(o,`# 长期记忆(${r})\n\n${t}`,{mode:384}),!0}catch{return!1}}export function replaceMemory(r,t){try{return e.mkdirSync(MEMORY_DIR,{recursive:!0}),e.writeFileSync(memoryPath(r),String(t||"").slice(0,12e3),{mode:384}),!0}catch{return!1}}export function clearMemory(r){try{return e.unlinkSync(memoryPath(r)),!0}catch{return!1}}export function readMemoryRaw(r){try{return e.readFileSync(memoryPath(r),"utf8")}catch{return""}}export function makeRecallTool({cwd:e}){return{readOnly:!0,spec:{type:"function",function:{name:"recall_memory",description:"在本项目的长期记忆里按关键词检索相关的过往结论/踩坑/约定(连超出常驻注入预算的历史也能查到)。开始一个任务前,若怀疑「以前处理过类似问题」,可先 recall 一下,少走弯路。",parameters:{type:"object",properties:{query:{type:"string",description:"关键词/概念"}},required:["query"]}}},run:async r=>{const t=String(r?.query||"").trim();if(!t)return"缺少 query";const n=readMemoryRaw(e).split("\n").map(e=>e.trim()).filter(e=>e.startsWith("- "));if(!n.length)return"本项目暂无可检索的长期记忆。";const i=new Set(o(t));if(!i.size)return"查询词无有效 token。";const c=n.map(e=>{let r=0;for(const t of new Set(o(e)))i.has(t)&&r++;return{e:e,s:r}}).filter(e=>e.s>0).sort((e,r)=>r.s-e.s).slice(0,6);return c.length?"相关的历史记忆:\n"+c.map(e=>e.e).join("\n"):`未在长期记忆中找到与「${t}」相关的条目。`}}}export function makeMemoryTool({cwd:e,emit:r}){return{readOnly:!0,spec:{type:"function",function:{name:"memory_write",description:"把值得跨会话记住的信息写入本项目的长期记忆(用户偏好、项目关键事实/约定、踩坑结论等)。新会话会自动注入这些记忆。仅记「以后仍然有用」的稳定信息;不要记一次性细节或大段代码。mode=append 追加一条(默认);mode=replace 用整理后的全文替换(做记忆瘦身时用)。",parameters:{type:"object",properties:{content:{type:"string",description:"要记住的内容(简洁的一条事实/偏好/结论)"},mode:{type:"string",enum:["append","replace"],description:"追加或整体替换(默认 append)"}},required:["content"]}}},run:async t=>{const n=String(t?.content||"").trim();if(!n)return"memory_write 需要非空 content";if("replace"===t?.mode?replaceMemory(e,n):appendMemory(e,n)){try{r?.(n)}catch{}return`已写入长期记忆(${"replace"===t?.mode?"整体替换":"追加"})。`}return"长期记忆写入失败(磁盘不可写?),可忽略并继续任务。"}}}
|