skyloom 1.12.0 → 1.13.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/.github/workflows/ci.yml +36 -36
- package/README.md +142 -46
- package/config/default.yaml +43 -47
- package/config/models.yaml +155 -155
- package/config/providers.yaml +39 -39
- package/config/skills/api_integrator/SKILL.md +15 -15
- package/config/skills/arch_designer/SKILL.md +13 -13
- package/config/skills/ci_cd_manager/SKILL.md +14 -14
- package/config/skills/code_analysis/SKILL.md +13 -13
- package/config/skills/code_generator/SKILL.md +12 -12
- package/config/skills/code_reviewer/SKILL.md +13 -13
- package/config/skills/content_writer/SKILL.md +14 -14
- package/config/skills/data_transformer/SKILL.md +15 -15
- package/config/skills/document_analysis/SKILL.md +13 -13
- package/config/skills/emotional_companion/SKILL.md +15 -15
- package/config/skills/performance_checker/SKILL.md +14 -14
- package/config/skills/security_auditor/SKILL.md +14 -14
- package/config/skills/self_evolve/SKILL.md +13 -13
- package/config/skills/sys_operator/SKILL.md +15 -15
- package/config/skills/task_planner/SKILL.md +14 -14
- package/config/skills/web_research/SKILL.md +14 -14
- package/config/skills/workflow_designer/SKILL.md +13 -13
- package/dist/agents/dew.js +52 -52
- package/dist/agents/fair.js +84 -84
- package/dist/agents/fog.js +30 -30
- package/dist/agents/frost.js +32 -32
- package/dist/agents/rain.js +32 -32
- package/dist/agents/snow.js +68 -68
- package/dist/cli/main.js +103 -51
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/tui.d.ts.map +1 -1
- package/dist/cli/tui.js +8 -1
- package/dist/cli/tui.js.map +1 -1
- package/dist/core/agent/task.d.ts +58 -0
- package/dist/core/agent/task.d.ts.map +1 -0
- package/dist/core/agent/task.js +83 -0
- package/dist/core/agent/task.js.map +1 -0
- package/dist/core/agent.d.ts +2 -45
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js +61 -145
- package/dist/core/agent.js.map +1 -1
- package/dist/core/agent_helpers.d.ts +10 -0
- package/dist/core/agent_helpers.d.ts.map +1 -1
- package/dist/core/agent_helpers.js +39 -0
- package/dist/core/agent_helpers.js.map +1 -1
- package/dist/core/catalog.d.ts +71 -0
- package/dist/core/catalog.d.ts.map +1 -0
- package/dist/core/catalog.js +176 -0
- package/dist/core/catalog.js.map +1 -0
- package/dist/core/config.d.ts +8 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +12 -4
- package/dist/core/config.js.map +1 -1
- package/dist/core/factory.js +16 -16
- package/dist/core/llm.d.ts +7 -0
- package/dist/core/llm.d.ts.map +1 -1
- package/dist/core/llm.js +139 -7
- package/dist/core/llm.js.map +1 -1
- package/dist/core/longdoc.js +5 -5
- package/dist/core/memory.d.ts.map +1 -1
- package/dist/core/memory.js +69 -62
- package/dist/core/memory.js.map +1 -1
- package/dist/core/theme.d.ts +46 -0
- package/dist/core/theme.d.ts.map +1 -0
- package/dist/core/theme.js +42 -0
- package/dist/core/theme.js.map +1 -0
- package/dist/web/server.js +542 -519
- package/dist/web/server.js.map +1 -1
- package/docs/AESTHETIC_DESIGN.md +144 -0
- package/docs/OPTIMIZATION_PLAN.md +178 -0
- package/package.json +60 -60
- package/scripts/install.js +48 -48
- package/scripts/link.js +10 -10
- package/setup.bat +79 -79
- package/skill-test-ty2fOA/test.md +10 -10
- package/src/agents/dew.ts +70 -70
- package/src/agents/fair.ts +102 -102
- package/src/agents/fog.ts +48 -48
- package/src/agents/frost.ts +50 -50
- package/src/agents/rain.ts +50 -50
- package/src/agents/snow.ts +239 -239
- package/src/cli/main.ts +425 -372
- package/src/cli/mode.ts +58 -58
- package/src/cli/tui.ts +272 -269
- package/src/core/agent/task.ts +100 -0
- package/src/core/agent.ts +1446 -1549
- package/src/core/agent_helpers.ts +496 -461
- package/src/core/arbitrate.ts +162 -162
- package/src/core/catalog.ts +178 -0
- package/src/core/checkpoint.ts +94 -94
- package/src/core/config.ts +20 -4
- package/src/core/estimate.ts +104 -104
- package/src/core/evolve.ts +191 -191
- package/src/core/factory.ts +627 -627
- package/src/core/filter.ts +103 -103
- package/src/core/graph.ts +156 -156
- package/src/core/icons.ts +53 -53
- package/src/core/index.ts +37 -37
- package/src/core/learn.ts +146 -146
- package/src/core/llm.ts +108 -5
- package/src/core/longdoc.ts +155 -155
- package/src/core/mcp_server.ts +176 -176
- package/src/core/memory.ts +1178 -1171
- package/src/core/profile.ts +255 -255
- package/src/core/router.ts +124 -124
- package/src/core/sandbox.ts +142 -142
- package/src/core/security.ts +243 -243
- package/src/core/skill.ts +342 -342
- package/src/core/theme.ts +65 -0
- package/src/core/tool_router.ts +193 -193
- package/src/core/vector.ts +152 -152
- package/src/core/workspace.ts +150 -150
- package/src/plugins/loader.ts +66 -66
- package/src/skills/loader.ts +46 -46
- package/src/sql.js.d.ts +29 -29
- package/src/tools/builtin.ts +380 -380
- package/src/tools/computer.ts +269 -269
- package/src/tools/delegate.ts +49 -49
- package/src/web/server.ts +660 -634
- package/src/web/tts.ts +93 -93
- package/tests/agent_helpers.test.ts +48 -0
- package/tests/bus.test.ts +121 -121
- package/tests/catalog.test.ts +86 -0
- package/tests/config.test.ts +41 -0
- package/tests/icons.test.ts +45 -45
- package/tests/memory.test.ts +147 -0
- package/tests/router.test.ts +86 -86
- package/tests/schemas.test.ts +51 -51
- package/tests/semantic.test.ts +83 -83
- package/tests/setup.ts +10 -10
- package/tests/skill.test.ts +172 -172
- package/tests/task.test.ts +60 -0
- package/tests/tool.test.ts +108 -108
- package/tests/tool_router.test.ts +71 -71
- package/vitest.config.ts +17 -17
package/src/core/profile.ts
CHANGED
|
@@ -1,255 +1,255 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Local user profile (用户画像) + per-agent custom personas.
|
|
3
|
-
*
|
|
4
|
-
* Everything here lives under ~/.skyloom/ and never leaves the machine:
|
|
5
|
-
*
|
|
6
|
-
* - profile.json — a free-form dict of facts about the user.
|
|
7
|
-
* - memories.json — running narrative of moods, life events, things worth following up on.
|
|
8
|
-
* - personas/<agent>.md — optional custom role for a specific agent.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import * as fs from 'fs';
|
|
12
|
-
import * as path from 'path';
|
|
13
|
-
import { USER_CONFIG_DIR, AGENT_NAMES } from './config';
|
|
14
|
-
|
|
15
|
-
const VALID_AGENTS = new Set<string>(AGENT_NAMES);
|
|
16
|
-
|
|
17
|
-
// Keep only the most recent N memories in the prompt + on disk.
|
|
18
|
-
const MEMORY_CAP = 40;
|
|
19
|
-
// When over cap, fold this many of the oldest notes into ONE digest entry.
|
|
20
|
-
const FOLD_BATCH = 8;
|
|
21
|
-
|
|
22
|
-
// Pluggable summarizer for memory folding.
|
|
23
|
-
let _summarizer: ((notes: string[]) => string) | null = null;
|
|
24
|
-
|
|
25
|
-
export function setMemorySummarizer(fn: (notes: string[]) => string): void {
|
|
26
|
-
_summarizer = fn;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function profilePath(): string {
|
|
30
|
-
return path.join(USER_CONFIG_DIR, 'profile.json');
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function memoriesPath(): string {
|
|
34
|
-
return path.join(USER_CONFIG_DIR, 'memories.json');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function personaPath(agent: string): string {
|
|
38
|
-
return path.join(USER_CONFIG_DIR, 'personas', `${agent}.md`);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// ── User profile ──
|
|
42
|
-
|
|
43
|
-
export function loadProfile(): Record<string, string> {
|
|
44
|
-
const p = profilePath();
|
|
45
|
-
if (!fs.existsSync(p)) return {};
|
|
46
|
-
try {
|
|
47
|
-
const data = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
48
|
-
return typeof data === 'object' && data !== null ? data : {};
|
|
49
|
-
} catch {
|
|
50
|
-
return {};
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function saveProfile(data: Record<string, string>): void {
|
|
55
|
-
const p = profilePath();
|
|
56
|
-
const dir = path.dirname(p);
|
|
57
|
-
if (!fs.existsSync(dir)) {
|
|
58
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
59
|
-
}
|
|
60
|
-
const tmp = p + '.tmp';
|
|
61
|
-
fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf-8');
|
|
62
|
-
fs.renameSync(tmp, p);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function setProfileField(key: string, value: string): void {
|
|
66
|
-
key = (key || '').trim();
|
|
67
|
-
if (!key) return;
|
|
68
|
-
const data = loadProfile();
|
|
69
|
-
data[key] = value;
|
|
70
|
-
saveProfile(data);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function clearProfileField(key?: string | null): void {
|
|
74
|
-
if (key == null) {
|
|
75
|
-
try {
|
|
76
|
-
const p = profilePath();
|
|
77
|
-
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
78
|
-
} catch { /* ignore */ }
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
const data = loadProfile();
|
|
82
|
-
delete data[key];
|
|
83
|
-
saveProfile(data);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function formatProfileForPrompt(lang: string = 'zh'): string {
|
|
87
|
-
const data = loadProfile();
|
|
88
|
-
const entries = Object.entries(data);
|
|
89
|
-
if (entries.length === 0) return '';
|
|
90
|
-
|
|
91
|
-
const lines = entries.map(([k, v]) => {
|
|
92
|
-
return lang === 'en' ? `- ${k}: ${v}` : `- ${k}:${v}`;
|
|
93
|
-
});
|
|
94
|
-
const body = lines.join('\n');
|
|
95
|
-
|
|
96
|
-
if (lang === 'en') {
|
|
97
|
-
return '\n\n## About the user (remember this and use it naturally)\n' + body;
|
|
98
|
-
}
|
|
99
|
-
return '\n\n## 关于用户(记住,并在对话中自然运用,不要生硬复述)\n' + body;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// ── Emotional / narrative memory ──
|
|
103
|
-
|
|
104
|
-
export function loadMemories(): Record<string, any>[] {
|
|
105
|
-
const p = memoriesPath();
|
|
106
|
-
if (!fs.existsSync(p)) return [];
|
|
107
|
-
try {
|
|
108
|
-
const data = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
109
|
-
return Array.isArray(data) ? data : [];
|
|
110
|
-
} catch {
|
|
111
|
-
return [];
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function norm(text: string): string {
|
|
116
|
-
return text.toLowerCase().split('').filter(c => !c.match(/\s/) && !',。,.、!!??~…「」"\',。、;:?!'.includes(c)).join('');
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function writeMemories(items: Record<string, any>[]): void {
|
|
120
|
-
const p = memoriesPath();
|
|
121
|
-
const dir = path.dirname(p);
|
|
122
|
-
if (!fs.existsSync(dir)) {
|
|
123
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
124
|
-
}
|
|
125
|
-
const tmp = p + '.tmp';
|
|
126
|
-
fs.writeFileSync(tmp, JSON.stringify(items, null, 2), 'utf-8');
|
|
127
|
-
fs.renameSync(tmp, p);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function digest(notes: string[]): string {
|
|
131
|
-
const seen: string[] = [];
|
|
132
|
-
for (let n of notes) {
|
|
133
|
-
n = n.trim();
|
|
134
|
-
if (n.startsWith('早些时候:')) {
|
|
135
|
-
n = n.slice('早些时候:'.length);
|
|
136
|
-
}
|
|
137
|
-
for (const part of n.split(';')) {
|
|
138
|
-
const p = part.trim();
|
|
139
|
-
if (p && !seen.includes(p)) {
|
|
140
|
-
seen.push(p);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
let joined = seen.join(';');
|
|
145
|
-
if (joined.length > 180) {
|
|
146
|
-
joined = joined.slice(0, 179) + '…';
|
|
147
|
-
}
|
|
148
|
-
return '早些时候:' + joined;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function summarizeNotes(notes: string[]): string {
|
|
152
|
-
if (_summarizer) {
|
|
153
|
-
try {
|
|
154
|
-
const out = _summarizer(notes);
|
|
155
|
-
if (out && out.trim()) return out.trim();
|
|
156
|
-
} catch { /* ignore */ }
|
|
157
|
-
}
|
|
158
|
-
return digest(notes);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function foldOldest(items: Record<string, any>[]): Record<string, any>[] {
|
|
162
|
-
let foldN = (items.length - MEMORY_CAP) + FOLD_BATCH;
|
|
163
|
-
foldN = Math.min(foldN, items.length - 1);
|
|
164
|
-
if (foldN <= 0) return items.slice(-MEMORY_CAP);
|
|
165
|
-
|
|
166
|
-
const old = items.slice(0, foldN);
|
|
167
|
-
const rest = items.slice(foldN);
|
|
168
|
-
const digestEntry: Record<string, any> = {
|
|
169
|
-
ts: old[old.length - 1]?.ts || new Date().toISOString().slice(0, 10),
|
|
170
|
-
note: summarizeNotes(old.map(m => String(m.note || ''))),
|
|
171
|
-
summary: true,
|
|
172
|
-
};
|
|
173
|
-
return [digestEntry, ...rest];
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
export function appendMemory(note: string): boolean {
|
|
177
|
-
note = (note || '').trim();
|
|
178
|
-
if (!note) return false;
|
|
179
|
-
|
|
180
|
-
const items = loadMemories();
|
|
181
|
-
const key = norm(note);
|
|
182
|
-
|
|
183
|
-
if (key) {
|
|
184
|
-
for (const m of items) {
|
|
185
|
-
if (norm(String(m.note || '')) === key) {
|
|
186
|
-
m.ts = new Date().toISOString().slice(0, 10); // refresh recency
|
|
187
|
-
if (note.length > String(m.note || '').length) {
|
|
188
|
-
m.note = note; // keep the richer phrasing
|
|
189
|
-
}
|
|
190
|
-
writeMemories(items);
|
|
191
|
-
return true;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
items.push({ ts: new Date().toISOString().slice(0, 10), note });
|
|
197
|
-
if (items.length > MEMORY_CAP) {
|
|
198
|
-
writeMemories(foldOldest(items));
|
|
199
|
-
} else {
|
|
200
|
-
writeMemories(items);
|
|
201
|
-
}
|
|
202
|
-
return true;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
export function clearMemories(): void {
|
|
206
|
-
try {
|
|
207
|
-
const p = memoriesPath();
|
|
208
|
-
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
209
|
-
} catch { /* ignore */ }
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
export function formatMemoriesForPrompt(lang: string = 'zh', limit: number = 12): string {
|
|
213
|
-
const items = loadMemories();
|
|
214
|
-
if (items.length === 0) return '';
|
|
215
|
-
|
|
216
|
-
const recent = items.slice(-limit);
|
|
217
|
-
const lines = recent.map((m: any) => `- [${m.ts || ''}] ${m.note || ''}`);
|
|
218
|
-
const body = lines.join('\n');
|
|
219
|
-
|
|
220
|
-
if (lang === 'en') {
|
|
221
|
-
return '\n\n## What you remember about them (recent context — weave in naturally, never recite)\n' + body;
|
|
222
|
-
}
|
|
223
|
-
return '\n\n## 你记得关于 ta 的事(近期,自然带出,别生硬复述)\n' + body;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// ── Per-agent custom persona ──
|
|
227
|
-
|
|
228
|
-
export function loadPersona(agent: string): string | null {
|
|
229
|
-
const p = personaPath(agent);
|
|
230
|
-
if (!fs.existsSync(p)) return null;
|
|
231
|
-
try {
|
|
232
|
-
const text = fs.readFileSync(p, 'utf-8').trim();
|
|
233
|
-
return text || null;
|
|
234
|
-
} catch {
|
|
235
|
-
return null;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
export function savePersona(agent: string, text: string): boolean {
|
|
240
|
-
if (!VALID_AGENTS.has(agent)) return false;
|
|
241
|
-
const p = personaPath(agent);
|
|
242
|
-
const dir = path.dirname(p);
|
|
243
|
-
if (!fs.existsSync(dir)) {
|
|
244
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
245
|
-
}
|
|
246
|
-
fs.writeFileSync(p, text.trim() + '\n', 'utf-8');
|
|
247
|
-
return true;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
export function clearPersona(agent: string): void {
|
|
251
|
-
try {
|
|
252
|
-
const p = personaPath(agent);
|
|
253
|
-
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
254
|
-
} catch { /* ignore */ }
|
|
255
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Local user profile (用户画像) + per-agent custom personas.
|
|
3
|
+
*
|
|
4
|
+
* Everything here lives under ~/.skyloom/ and never leaves the machine:
|
|
5
|
+
*
|
|
6
|
+
* - profile.json — a free-form dict of facts about the user.
|
|
7
|
+
* - memories.json — running narrative of moods, life events, things worth following up on.
|
|
8
|
+
* - personas/<agent>.md — optional custom role for a specific agent.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import { USER_CONFIG_DIR, AGENT_NAMES } from './config';
|
|
14
|
+
|
|
15
|
+
const VALID_AGENTS = new Set<string>(AGENT_NAMES);
|
|
16
|
+
|
|
17
|
+
// Keep only the most recent N memories in the prompt + on disk.
|
|
18
|
+
const MEMORY_CAP = 40;
|
|
19
|
+
// When over cap, fold this many of the oldest notes into ONE digest entry.
|
|
20
|
+
const FOLD_BATCH = 8;
|
|
21
|
+
|
|
22
|
+
// Pluggable summarizer for memory folding.
|
|
23
|
+
let _summarizer: ((notes: string[]) => string) | null = null;
|
|
24
|
+
|
|
25
|
+
export function setMemorySummarizer(fn: (notes: string[]) => string): void {
|
|
26
|
+
_summarizer = fn;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function profilePath(): string {
|
|
30
|
+
return path.join(USER_CONFIG_DIR, 'profile.json');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function memoriesPath(): string {
|
|
34
|
+
return path.join(USER_CONFIG_DIR, 'memories.json');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function personaPath(agent: string): string {
|
|
38
|
+
return path.join(USER_CONFIG_DIR, 'personas', `${agent}.md`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── User profile ──
|
|
42
|
+
|
|
43
|
+
export function loadProfile(): Record<string, string> {
|
|
44
|
+
const p = profilePath();
|
|
45
|
+
if (!fs.existsSync(p)) return {};
|
|
46
|
+
try {
|
|
47
|
+
const data = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
48
|
+
return typeof data === 'object' && data !== null ? data : {};
|
|
49
|
+
} catch {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function saveProfile(data: Record<string, string>): void {
|
|
55
|
+
const p = profilePath();
|
|
56
|
+
const dir = path.dirname(p);
|
|
57
|
+
if (!fs.existsSync(dir)) {
|
|
58
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
const tmp = p + '.tmp';
|
|
61
|
+
fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf-8');
|
|
62
|
+
fs.renameSync(tmp, p);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function setProfileField(key: string, value: string): void {
|
|
66
|
+
key = (key || '').trim();
|
|
67
|
+
if (!key) return;
|
|
68
|
+
const data = loadProfile();
|
|
69
|
+
data[key] = value;
|
|
70
|
+
saveProfile(data);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function clearProfileField(key?: string | null): void {
|
|
74
|
+
if (key == null) {
|
|
75
|
+
try {
|
|
76
|
+
const p = profilePath();
|
|
77
|
+
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
78
|
+
} catch { /* ignore */ }
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const data = loadProfile();
|
|
82
|
+
delete data[key];
|
|
83
|
+
saveProfile(data);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function formatProfileForPrompt(lang: string = 'zh'): string {
|
|
87
|
+
const data = loadProfile();
|
|
88
|
+
const entries = Object.entries(data);
|
|
89
|
+
if (entries.length === 0) return '';
|
|
90
|
+
|
|
91
|
+
const lines = entries.map(([k, v]) => {
|
|
92
|
+
return lang === 'en' ? `- ${k}: ${v}` : `- ${k}:${v}`;
|
|
93
|
+
});
|
|
94
|
+
const body = lines.join('\n');
|
|
95
|
+
|
|
96
|
+
if (lang === 'en') {
|
|
97
|
+
return '\n\n## About the user (remember this and use it naturally)\n' + body;
|
|
98
|
+
}
|
|
99
|
+
return '\n\n## 关于用户(记住,并在对话中自然运用,不要生硬复述)\n' + body;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── Emotional / narrative memory ──
|
|
103
|
+
|
|
104
|
+
export function loadMemories(): Record<string, any>[] {
|
|
105
|
+
const p = memoriesPath();
|
|
106
|
+
if (!fs.existsSync(p)) return [];
|
|
107
|
+
try {
|
|
108
|
+
const data = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
109
|
+
return Array.isArray(data) ? data : [];
|
|
110
|
+
} catch {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function norm(text: string): string {
|
|
116
|
+
return text.toLowerCase().split('').filter(c => !c.match(/\s/) && !',。,.、!!??~…「」"\',。、;:?!'.includes(c)).join('');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function writeMemories(items: Record<string, any>[]): void {
|
|
120
|
+
const p = memoriesPath();
|
|
121
|
+
const dir = path.dirname(p);
|
|
122
|
+
if (!fs.existsSync(dir)) {
|
|
123
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
124
|
+
}
|
|
125
|
+
const tmp = p + '.tmp';
|
|
126
|
+
fs.writeFileSync(tmp, JSON.stringify(items, null, 2), 'utf-8');
|
|
127
|
+
fs.renameSync(tmp, p);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function digest(notes: string[]): string {
|
|
131
|
+
const seen: string[] = [];
|
|
132
|
+
for (let n of notes) {
|
|
133
|
+
n = n.trim();
|
|
134
|
+
if (n.startsWith('早些时候:')) {
|
|
135
|
+
n = n.slice('早些时候:'.length);
|
|
136
|
+
}
|
|
137
|
+
for (const part of n.split(';')) {
|
|
138
|
+
const p = part.trim();
|
|
139
|
+
if (p && !seen.includes(p)) {
|
|
140
|
+
seen.push(p);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
let joined = seen.join(';');
|
|
145
|
+
if (joined.length > 180) {
|
|
146
|
+
joined = joined.slice(0, 179) + '…';
|
|
147
|
+
}
|
|
148
|
+
return '早些时候:' + joined;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function summarizeNotes(notes: string[]): string {
|
|
152
|
+
if (_summarizer) {
|
|
153
|
+
try {
|
|
154
|
+
const out = _summarizer(notes);
|
|
155
|
+
if (out && out.trim()) return out.trim();
|
|
156
|
+
} catch { /* ignore */ }
|
|
157
|
+
}
|
|
158
|
+
return digest(notes);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function foldOldest(items: Record<string, any>[]): Record<string, any>[] {
|
|
162
|
+
let foldN = (items.length - MEMORY_CAP) + FOLD_BATCH;
|
|
163
|
+
foldN = Math.min(foldN, items.length - 1);
|
|
164
|
+
if (foldN <= 0) return items.slice(-MEMORY_CAP);
|
|
165
|
+
|
|
166
|
+
const old = items.slice(0, foldN);
|
|
167
|
+
const rest = items.slice(foldN);
|
|
168
|
+
const digestEntry: Record<string, any> = {
|
|
169
|
+
ts: old[old.length - 1]?.ts || new Date().toISOString().slice(0, 10),
|
|
170
|
+
note: summarizeNotes(old.map(m => String(m.note || ''))),
|
|
171
|
+
summary: true,
|
|
172
|
+
};
|
|
173
|
+
return [digestEntry, ...rest];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function appendMemory(note: string): boolean {
|
|
177
|
+
note = (note || '').trim();
|
|
178
|
+
if (!note) return false;
|
|
179
|
+
|
|
180
|
+
const items = loadMemories();
|
|
181
|
+
const key = norm(note);
|
|
182
|
+
|
|
183
|
+
if (key) {
|
|
184
|
+
for (const m of items) {
|
|
185
|
+
if (norm(String(m.note || '')) === key) {
|
|
186
|
+
m.ts = new Date().toISOString().slice(0, 10); // refresh recency
|
|
187
|
+
if (note.length > String(m.note || '').length) {
|
|
188
|
+
m.note = note; // keep the richer phrasing
|
|
189
|
+
}
|
|
190
|
+
writeMemories(items);
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
items.push({ ts: new Date().toISOString().slice(0, 10), note });
|
|
197
|
+
if (items.length > MEMORY_CAP) {
|
|
198
|
+
writeMemories(foldOldest(items));
|
|
199
|
+
} else {
|
|
200
|
+
writeMemories(items);
|
|
201
|
+
}
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function clearMemories(): void {
|
|
206
|
+
try {
|
|
207
|
+
const p = memoriesPath();
|
|
208
|
+
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
209
|
+
} catch { /* ignore */ }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function formatMemoriesForPrompt(lang: string = 'zh', limit: number = 12): string {
|
|
213
|
+
const items = loadMemories();
|
|
214
|
+
if (items.length === 0) return '';
|
|
215
|
+
|
|
216
|
+
const recent = items.slice(-limit);
|
|
217
|
+
const lines = recent.map((m: any) => `- [${m.ts || ''}] ${m.note || ''}`);
|
|
218
|
+
const body = lines.join('\n');
|
|
219
|
+
|
|
220
|
+
if (lang === 'en') {
|
|
221
|
+
return '\n\n## What you remember about them (recent context — weave in naturally, never recite)\n' + body;
|
|
222
|
+
}
|
|
223
|
+
return '\n\n## 你记得关于 ta 的事(近期,自然带出,别生硬复述)\n' + body;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ── Per-agent custom persona ──
|
|
227
|
+
|
|
228
|
+
export function loadPersona(agent: string): string | null {
|
|
229
|
+
const p = personaPath(agent);
|
|
230
|
+
if (!fs.existsSync(p)) return null;
|
|
231
|
+
try {
|
|
232
|
+
const text = fs.readFileSync(p, 'utf-8').trim();
|
|
233
|
+
return text || null;
|
|
234
|
+
} catch {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function savePersona(agent: string, text: string): boolean {
|
|
240
|
+
if (!VALID_AGENTS.has(agent)) return false;
|
|
241
|
+
const p = personaPath(agent);
|
|
242
|
+
const dir = path.dirname(p);
|
|
243
|
+
if (!fs.existsSync(dir)) {
|
|
244
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
245
|
+
}
|
|
246
|
+
fs.writeFileSync(p, text.trim() + '\n', 'utf-8');
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function clearPersona(agent: string): void {
|
|
251
|
+
try {
|
|
252
|
+
const p = personaPath(agent);
|
|
253
|
+
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
254
|
+
} catch { /* ignore */ }
|
|
255
|
+
}
|