deeper-cli 1.2.5 → 1.3.1
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/dist/cli/index.js +229 -44
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
- package/src/memory/xmemory.ts +254 -48
- package/src/model/DeepSeekClient.ts +7 -1
- package/chinese_chess_ai/README.md +0 -62
- package/chinese_chess_ai/config.py +0 -63
- package/chinese_chess_ai/requirements.txt +0 -1
package/package.json
CHANGED
package/src/memory/xmemory.ts
CHANGED
|
@@ -7,14 +7,14 @@ export interface MemoryEntry {
|
|
|
7
7
|
type: 'working' | 'episodic' | 'semantic' | 'procedural';
|
|
8
8
|
content: string;
|
|
9
9
|
tags: string[];
|
|
10
|
-
importance: number;
|
|
11
|
-
accuracy: number;
|
|
10
|
+
importance: number;
|
|
11
|
+
accuracy: number;
|
|
12
12
|
createdAt: number;
|
|
13
13
|
accessedAt: number;
|
|
14
14
|
accessCount: number;
|
|
15
15
|
sessionId: string;
|
|
16
|
-
source: string;
|
|
17
|
-
references: string[];
|
|
16
|
+
source: string;
|
|
17
|
+
references: string[];
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
interface XMemStats {
|
|
@@ -22,13 +22,17 @@ interface XMemStats {
|
|
|
22
22
|
byType: Record<string, number>;
|
|
23
23
|
totalTokens: number;
|
|
24
24
|
lastCleanup: number;
|
|
25
|
+
lastConsolidate: number;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
const MEM_DIR = join(DEEPER_HOME, 'xmemory');
|
|
28
29
|
const STATS_FILE = join(MEM_DIR, 'stats.json');
|
|
29
|
-
const MAX_WORKING_MEM =
|
|
30
|
-
const MAX_TOTAL_MEM =
|
|
31
|
-
const CLEANUP_THRESHOLD =
|
|
30
|
+
const MAX_WORKING_MEM = 100;
|
|
31
|
+
const MAX_TOTAL_MEM = 3000;
|
|
32
|
+
const CLEANUP_THRESHOLD = 2000;
|
|
33
|
+
const DEDUP_SIMILARITY_THRESHOLD = 0.85;
|
|
34
|
+
const CONSOLIDATE_THRESHOLD = 5;
|
|
35
|
+
const SAVE_DEBOUNCE_MS = 3000;
|
|
32
36
|
|
|
33
37
|
let currentSessionId = '';
|
|
34
38
|
|
|
@@ -41,30 +45,87 @@ function uid(): string {
|
|
|
41
45
|
function ensureDir(): void {
|
|
42
46
|
if (!existsSync(MEM_DIR)) mkdirSync(MEM_DIR, { recursive: true });
|
|
43
47
|
if (!existsSync(STATS_FILE)) {
|
|
44
|
-
writeFileSync(STATS_FILE, JSON.stringify({ totalEntries: 0, byType: {}, totalTokens: 0, lastCleanup: Date.now() }), 'utf-8');
|
|
48
|
+
writeFileSync(STATS_FILE, JSON.stringify({ totalEntries: 0, byType: {}, totalTokens: 0, lastCleanup: Date.now(), lastConsolidate: Date.now() }, null, 2), 'utf-8');
|
|
45
49
|
}
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
function loadStats(): XMemStats {
|
|
49
53
|
try {
|
|
50
54
|
return JSON.parse(readFileSync(STATS_FILE, 'utf-8')) as XMemStats;
|
|
51
|
-
} catch { return { totalEntries: 0, byType: {}, totalTokens: 0, lastCleanup: 0 }; }
|
|
55
|
+
} catch { return { totalEntries: 0, byType: {}, totalTokens: 0, lastCleanup: 0, lastConsolidate: 0 }; }
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
function saveStats(s: XMemStats): void {
|
|
55
59
|
writeFileSync(STATS_FILE, JSON.stringify(s, null, 2), 'utf-8');
|
|
56
60
|
}
|
|
57
61
|
|
|
62
|
+
function tokenize(text: string): string[] {
|
|
63
|
+
const lower = text.toLowerCase();
|
|
64
|
+
const words = lower.split(/[\s\p{P}\p{S}]+/).filter(w => w.length >= 2);
|
|
65
|
+
const bigrams: string[] = [];
|
|
66
|
+
for (let i = 0; i < words.length - 1; i++) bigrams.push(words[i] + '_' + words[i + 1]);
|
|
67
|
+
return [...new Set([...words, ...bigrams])];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function similarity(a: string, b: string): number {
|
|
71
|
+
if (a === b) return 1;
|
|
72
|
+
if (!a || !b) return 0;
|
|
73
|
+
const ta = tokenize(a);
|
|
74
|
+
const tb = tokenize(b);
|
|
75
|
+
if (ta.length === 0 || tb.length === 0) return 0;
|
|
76
|
+
const sa = new Set(ta);
|
|
77
|
+
const sb = new Set(tb);
|
|
78
|
+
let intersection = 0;
|
|
79
|
+
for (const t of sa) if (sb.has(t)) intersection++;
|
|
80
|
+
const union = Math.max(sa.size, sb.size);
|
|
81
|
+
return union > 0 ? intersection / union : 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
class TFIDFIndexer {
|
|
85
|
+
private docFreq: Map<string, number> = new Map();
|
|
86
|
+
private numDocs = 0;
|
|
87
|
+
|
|
88
|
+
build(entries: Iterable<MemoryEntry>): void {
|
|
89
|
+
this.docFreq.clear();
|
|
90
|
+
this.numDocs = 0;
|
|
91
|
+
for (const entry of entries) {
|
|
92
|
+
this.numDocs++;
|
|
93
|
+
const tokens = new Set(tokenize(entry.content));
|
|
94
|
+
for (const t of tokens) this.docFreq.set(t, (this.docFreq.get(t) || 0) + 1);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
idf(term: string): number {
|
|
99
|
+
const df = this.docFreq.get(term) || 0;
|
|
100
|
+
if (df === 0 || df >= this.numDocs) return 0;
|
|
101
|
+
return Math.log((this.numDocs + 0.5) / (df + 0.5)) + 1;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
tfidfScore(content: string, queryTerms: string[]): number {
|
|
105
|
+
const tokens = tokenize(content);
|
|
106
|
+
const tfMap = new Map<string, number>();
|
|
107
|
+
for (const t of tokens) tfMap.set(t, (tfMap.get(t) || 0) + 1);
|
|
108
|
+
const maxTf = Math.max(...tfMap.values(), 1);
|
|
109
|
+
let score = 0;
|
|
110
|
+
for (const qt of queryTerms) {
|
|
111
|
+
const tf = ((tfMap.get(qt) || 0) / maxTf) * (1 + Math.log(tokens.length));
|
|
112
|
+
score += tf * this.idf(qt);
|
|
113
|
+
}
|
|
114
|
+
return score;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
58
118
|
export class XMemory {
|
|
59
119
|
private working: MemoryEntry[] = [];
|
|
60
120
|
private index: Map<string, MemoryEntry> = new Map();
|
|
61
121
|
private dirty = false;
|
|
122
|
+
private saveTimer: ReturnType<typeof setTimeout> | null = null;
|
|
123
|
+
private tfidf: TFIDFIndexer = new TFIDFIndexer();
|
|
62
124
|
|
|
63
125
|
constructor() {
|
|
64
126
|
ensureDir();
|
|
65
127
|
}
|
|
66
128
|
|
|
67
|
-
// ============ 写入 ============
|
|
68
129
|
store(
|
|
69
130
|
type: MemoryEntry['type'],
|
|
70
131
|
content: string,
|
|
@@ -74,9 +135,12 @@ export class XMemory {
|
|
|
74
135
|
source: MemoryEntry['source'] = 'agent',
|
|
75
136
|
references: string[] = [],
|
|
76
137
|
): string {
|
|
138
|
+
const deduped = this.deduplicate(type, content, importance);
|
|
139
|
+
if (deduped !== null) return deduped;
|
|
140
|
+
|
|
77
141
|
const id = uid();
|
|
78
142
|
const entry: MemoryEntry = {
|
|
79
|
-
id, type, content: content.slice(0,
|
|
143
|
+
id, type, content: content.slice(0, 4000), tags,
|
|
80
144
|
importance: Math.min(10, Math.max(0, importance)),
|
|
81
145
|
accuracy: Math.min(10, Math.max(0, accuracy)),
|
|
82
146
|
createdAt: Date.now(), accessedAt: Date.now(),
|
|
@@ -86,13 +150,12 @@ export class XMemory {
|
|
|
86
150
|
|
|
87
151
|
if (type === 'working') {
|
|
88
152
|
this.working.push(entry);
|
|
89
|
-
|
|
90
|
-
this.working.shift();
|
|
91
|
-
}
|
|
153
|
+
this.evictWorking();
|
|
92
154
|
}
|
|
93
155
|
|
|
94
156
|
this.index.set(id, entry);
|
|
95
157
|
this.dirty = true;
|
|
158
|
+
this.scheduleSave();
|
|
96
159
|
|
|
97
160
|
if (this.index.size > CLEANUP_THRESHOLD) {
|
|
98
161
|
this.autoCleanup();
|
|
@@ -117,38 +180,54 @@ export class XMemory {
|
|
|
117
180
|
return this.store('working', content, tags, 3, 5, 'agent');
|
|
118
181
|
}
|
|
119
182
|
|
|
120
|
-
// ============ 检索 ============
|
|
121
183
|
recall(query: string, limit = 5, minImportance = 0): MemoryEntry[] {
|
|
122
|
-
const keywords = query
|
|
184
|
+
const keywords = tokenize(query).filter(w => w.length >= 2);
|
|
123
185
|
if (keywords.length === 0) return [];
|
|
124
186
|
|
|
125
|
-
|
|
187
|
+
this.tfidf.build(this.index.values());
|
|
126
188
|
const now = Date.now();
|
|
189
|
+
const scored: Array<{ entry: MemoryEntry; score: number }> = [];
|
|
127
190
|
|
|
128
191
|
for (const entry of this.index.values()) {
|
|
129
192
|
if (entry.importance < minImportance) continue;
|
|
130
|
-
|
|
131
|
-
const c =
|
|
193
|
+
|
|
194
|
+
const c = entry.content.toLowerCase();
|
|
132
195
|
const t = entry.tags.join(' ').toLowerCase();
|
|
133
196
|
|
|
197
|
+
let keywordScore = 0;
|
|
198
|
+
let exactMatchBonus = 0;
|
|
134
199
|
for (const kw of keywords) {
|
|
135
|
-
if (c.includes(kw))
|
|
136
|
-
if (t.includes(kw))
|
|
137
|
-
if (entry.type === 'procedural' && c.includes(kw))
|
|
200
|
+
if (c.includes(kw)) { keywordScore += 3; exactMatchBonus += 1; }
|
|
201
|
+
if (t.includes(kw)) keywordScore += 2;
|
|
202
|
+
if (entry.type === 'procedural' && c.includes(kw)) keywordScore += 1;
|
|
203
|
+
if (entry.type === 'semantic' && c.includes(kw)) keywordScore += 1.5;
|
|
138
204
|
}
|
|
139
205
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
206
|
+
const tfidfScore = this.tfidf.tfidfScore(entry.content, keywords);
|
|
207
|
+
|
|
208
|
+
const recencyBoost = Math.max(0, 3 - (now - entry.accessedAt) / (1000 * 60 * 30));
|
|
209
|
+
const frequencyBoost = Math.min(entry.accessCount * 0.3, 4);
|
|
210
|
+
const importanceWeight = entry.importance * 0.6;
|
|
211
|
+
const ageHours = (now - entry.createdAt) / (1000 * 60 * 60);
|
|
212
|
+
const ageDecay = Math.min(ageHours / 48, 6);
|
|
144
213
|
|
|
145
|
-
|
|
214
|
+
const totalScore =
|
|
215
|
+
keywordScore * 1.0 +
|
|
216
|
+
tfidfScore * 2.0 +
|
|
217
|
+
importanceWeight +
|
|
218
|
+
recencyBoost +
|
|
219
|
+
frequencyBoost -
|
|
220
|
+
ageDecay +
|
|
221
|
+
(exactMatchBonus >= 3 ? 3 : 0);
|
|
222
|
+
|
|
223
|
+
if (totalScore > 0) scored.push({ entry, score: totalScore });
|
|
146
224
|
}
|
|
147
225
|
|
|
148
226
|
scored.sort((a, b) => b.score - a.score);
|
|
149
227
|
return scored.slice(0, limit).map(s => {
|
|
150
228
|
s.entry.accessedAt = now;
|
|
151
229
|
s.entry.accessCount++;
|
|
230
|
+
s.entry.importance = Math.min(10, s.entry.importance + 0.1);
|
|
152
231
|
return s.entry;
|
|
153
232
|
});
|
|
154
233
|
}
|
|
@@ -158,20 +237,23 @@ export class XMemory {
|
|
|
158
237
|
for (const entry of this.index.values()) {
|
|
159
238
|
if (entry.type === type) res.push(entry);
|
|
160
239
|
}
|
|
161
|
-
res.sort((a, b) => b.
|
|
240
|
+
res.sort((a, b) => (b.importance * 2 + b.accessCount) - (a.importance * 2 + a.accessCount));
|
|
162
241
|
return res.slice(0, limit);
|
|
163
242
|
}
|
|
164
243
|
|
|
165
244
|
getWorking(): MemoryEntry[] {
|
|
166
|
-
return this.working
|
|
245
|
+
return this.working.sort((a, b) =>
|
|
246
|
+
(b.importance * 2 + b.accessCount) - (a.importance * 2 + a.accessCount)
|
|
247
|
+
);
|
|
167
248
|
}
|
|
168
249
|
|
|
169
|
-
getWorkingContext(maxTokens =
|
|
250
|
+
getWorkingContext(maxTokens = 4000): string {
|
|
170
251
|
if (this.working.length === 0) return '';
|
|
171
|
-
const
|
|
252
|
+
const sorted = this.getWorking();
|
|
253
|
+
const lines = sorted.map(e => `[${e.source}] ${e.content.slice(0, 500)}`);
|
|
172
254
|
let result = '';
|
|
173
255
|
for (const line of lines) {
|
|
174
|
-
if ((result + line).length > maxTokens *
|
|
256
|
+
if ((result + line).length > maxTokens * 3) break;
|
|
175
257
|
result += line + '\n';
|
|
176
258
|
}
|
|
177
259
|
return result;
|
|
@@ -181,28 +263,51 @@ export class XMemory {
|
|
|
181
263
|
const recalled = this.recall(task, limit, 3);
|
|
182
264
|
const proc = recalled.filter(r => r.type === 'procedural' || r.type === 'semantic');
|
|
183
265
|
if (proc.length === 0) return '';
|
|
184
|
-
return '[记忆提示]\n' + proc.map(p => `- ${p.content.slice(0,
|
|
266
|
+
return '[记忆提示]\n' + proc.map(p => `- [${p.type === 'procedural' ? '技能' : '知识'}] ${p.content.slice(0, 350)}`).join('\n');
|
|
185
267
|
}
|
|
186
268
|
|
|
187
269
|
getSessionSummary(): string {
|
|
188
270
|
const entries = [...this.index.values()].filter(e => e.sessionId === currentSessionId);
|
|
189
271
|
if (entries.length === 0) return '';
|
|
190
|
-
|
|
191
|
-
|
|
272
|
+
|
|
273
|
+
const byType: Record<string, MemoryEntry[]> = { semantic: [], procedural: [], episodic: [], working: [] };
|
|
274
|
+
for (const e of entries) {
|
|
275
|
+
if (byType[e.type]) byType[e.type].push(e);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const parts: string[] = [];
|
|
279
|
+
|
|
280
|
+
const keySemantic = byType.semantic
|
|
281
|
+
.sort((a, b) => b.importance - a.importance)
|
|
282
|
+
.slice(0, 5);
|
|
283
|
+
if (keySemantic.length > 0) {
|
|
284
|
+
parts.push(`[知识记忆·${keySemantic.length}条]\n` +
|
|
285
|
+
keySemantic.map(e => `• ${e.content.slice(0, 250)}`).join('\n'));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const keyProcedural = byType.procedural
|
|
289
|
+
.sort((a, b) => b.importance - a.importance)
|
|
290
|
+
.slice(0, 5);
|
|
291
|
+
if (keyProcedural.length > 0) {
|
|
292
|
+
parts.push(`[技能记忆·${keyProcedural.length}条]\n` +
|
|
293
|
+
keyProcedural.map(e => `• ${e.content.slice(0, 250)}`).join('\n'));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const keyEpisodic = byType.episodic
|
|
192
297
|
.sort((a, b) => b.importance - a.importance)
|
|
193
|
-
.slice(0,
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
298
|
+
.slice(0, 3);
|
|
299
|
+
if (keyEpisodic.length > 0) {
|
|
300
|
+
parts.push(`[经历记忆·${keyEpisodic.length}条]\n` +
|
|
301
|
+
keyEpisodic.map(e => `→ ${e.content.slice(0, 150)}`).join('\n'));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return `[XMemory·本会话 ${entries.length}条]\n${parts.join('\n')}`;
|
|
198
305
|
}
|
|
199
306
|
|
|
200
|
-
// ============ 持久化 ============
|
|
201
307
|
async save(): Promise<void> {
|
|
202
308
|
if (!this.dirty) return;
|
|
203
309
|
ensureDir();
|
|
204
310
|
|
|
205
|
-
// 分批写入:每个 type 一个文件
|
|
206
311
|
const byType: Record<string, MemoryEntry[]> = {};
|
|
207
312
|
for (const entry of this.index.values()) {
|
|
208
313
|
if (!byType[entry.type]) byType[entry.type] = [];
|
|
@@ -211,7 +316,7 @@ export class XMemory {
|
|
|
211
316
|
|
|
212
317
|
for (const [type, entries] of Object.entries(byType)) {
|
|
213
318
|
const file = join(MEM_DIR, `${type}.json`);
|
|
214
|
-
writeFileSync(file, JSON.stringify(entries.slice(-
|
|
319
|
+
writeFileSync(file, JSON.stringify(entries.slice(-800)), 'utf-8');
|
|
215
320
|
}
|
|
216
321
|
|
|
217
322
|
const stats = loadStats();
|
|
@@ -241,24 +346,125 @@ export class XMemory {
|
|
|
241
346
|
if (entry.type === 'working') this.working.push(entry);
|
|
242
347
|
total++;
|
|
243
348
|
}
|
|
244
|
-
} catch {
|
|
349
|
+
} catch {}
|
|
245
350
|
}
|
|
246
351
|
|
|
247
352
|
if (this.working.length > MAX_WORKING_MEM) {
|
|
248
353
|
this.working = this.working.slice(-MAX_WORKING_MEM);
|
|
249
354
|
}
|
|
355
|
+
|
|
356
|
+
this.consolidateIfNeeded();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private scheduleSave(): void {
|
|
360
|
+
if (this.saveTimer) return;
|
|
361
|
+
this.saveTimer = setTimeout(() => {
|
|
362
|
+
this.saveTimer = null;
|
|
363
|
+
this.save().catch(() => {});
|
|
364
|
+
}, SAVE_DEBOUNCE_MS);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private deduplicate(type: MemoryEntry['type'], content: string, importance: number): string | null {
|
|
368
|
+
const threshold = type === 'working' ? 0.95 : type === 'episodic' ? 0.85 : DEDUP_SIMILARITY_THRESHOLD;
|
|
369
|
+
const shortContent = content.slice(0, 500);
|
|
370
|
+
|
|
371
|
+
for (const entry of this.index.values()) {
|
|
372
|
+
if (entry.type !== type) continue;
|
|
373
|
+
const sim = similarity(shortContent, entry.content.slice(0, 500));
|
|
374
|
+
if (sim >= threshold) {
|
|
375
|
+
entry.accessedAt = Date.now();
|
|
376
|
+
entry.accessCount++;
|
|
377
|
+
entry.importance = Math.min(10, Math.max(entry.importance, importance));
|
|
378
|
+
if (content.length > entry.content.length && content.length <= 4000) {
|
|
379
|
+
entry.content = content;
|
|
380
|
+
this.dirty = true;
|
|
381
|
+
}
|
|
382
|
+
return entry.id;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private evictWorking(): void {
|
|
389
|
+
while (this.working.length > MAX_WORKING_MEM) {
|
|
390
|
+
let worstIdx = 0;
|
|
391
|
+
let worstScore = Infinity;
|
|
392
|
+
for (let i = 0; i < this.working.length; i++) {
|
|
393
|
+
const e = this.working[i];
|
|
394
|
+
const score = e.importance + e.accessCount * 0.3 - (Date.now() - e.accessedAt) / (1000 * 60 * 15);
|
|
395
|
+
if (score < worstScore) { worstScore = score; worstIdx = i; }
|
|
396
|
+
}
|
|
397
|
+
this.working.splice(worstIdx, 1);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private consolidateIfNeeded(): void {
|
|
402
|
+
const stats = loadStats();
|
|
403
|
+
const now = Date.now();
|
|
404
|
+
if (now - stats.lastConsolidate < 24 * 60 * 60 * 1000) return;
|
|
405
|
+
|
|
406
|
+
const candidates: Map<string, MemoryEntry[]> = new Map();
|
|
407
|
+
for (const entry of this.index.values()) {
|
|
408
|
+
if (entry.type === 'episodic' && entry.accessCount >= CONSOLIDATE_THRESHOLD) {
|
|
409
|
+
const key = entry.tags.slice(0, 3).sort().join('|') || '_default_';
|
|
410
|
+
if (!candidates.has(key)) candidates.set(key, []);
|
|
411
|
+
candidates.get(key)!.push(entry);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
let consolidated = 0;
|
|
416
|
+
for (const [, group] of candidates) {
|
|
417
|
+
if (group.length < 2) continue;
|
|
418
|
+
group.sort((a, b) => b.accessCount - a.accessCount);
|
|
419
|
+
const best = group[0];
|
|
420
|
+
|
|
421
|
+
const allContent = group.map(g => g.content).join('\n');
|
|
422
|
+
const summary = `[ consolidated from ${group.length} memories ]\n${allContent.slice(0, 2000)}`;
|
|
423
|
+
|
|
424
|
+
const newType = best.accessCount >= 10 ? 'procedural' : 'semantic';
|
|
425
|
+
const existing = this.findSimilar(newType, best.content);
|
|
426
|
+
if (existing) {
|
|
427
|
+
existing.content = summary.slice(0, 4000);
|
|
428
|
+
existing.accessCount += Math.floor(group.reduce((s, g) => s + g.accessCount, 0) / group.length);
|
|
429
|
+
existing.importance = Math.min(10, existing.importance + 1);
|
|
430
|
+
} else {
|
|
431
|
+
this.store(newType, summary, best.tags, Math.min(10, best.importance + 2), 9, 'system');
|
|
432
|
+
consolidated++;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
for (const old of group) {
|
|
436
|
+
if (old.id !== best.id) this.index.delete(old.id);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (consolidated > 0 || candidates.size > 0) {
|
|
441
|
+
stats.lastConsolidate = now;
|
|
442
|
+
saveStats(stats);
|
|
443
|
+
this.dirty = true;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
private findSimilar(type: MemoryEntry['type'], content: string): MemoryEntry | null {
|
|
448
|
+
const threshold = 0.75;
|
|
449
|
+
let best: MemoryEntry | null = null;
|
|
450
|
+
let bestSim = 0;
|
|
451
|
+
for (const entry of this.index.values()) {
|
|
452
|
+
if (entry.type !== type) continue;
|
|
453
|
+
const sim = similarity(content.slice(0, 300), entry.content.slice(0, 300));
|
|
454
|
+
if (sim > bestSim) { bestSim = sim; best = entry; }
|
|
455
|
+
}
|
|
456
|
+
return bestSim >= threshold ? best : null;
|
|
250
457
|
}
|
|
251
458
|
|
|
252
|
-
// ============ 维护 ============
|
|
253
459
|
private autoCleanup(): void {
|
|
254
460
|
const entries = [...this.index.values()];
|
|
255
461
|
entries.sort((a, b) => {
|
|
256
|
-
const scoreA = a.importance * 2 + a.accessCount - (Date.now() - a.accessedAt) / (1000 * 60 * 60 * 24);
|
|
257
|
-
const scoreB = b.importance * 2 + b.accessCount - (Date.now() - b.accessedAt) / (1000 * 60 * 60 * 24);
|
|
462
|
+
const scoreA = a.importance * 2 + a.accessCount + (a.type === 'semantic' ? 3 : a.type === 'procedural' ? 2 : 0) - (Date.now() - a.accessedAt) / (1000 * 60 * 60 * 24);
|
|
463
|
+
const scoreB = b.importance * 2 + b.accessCount + (b.type === 'semantic' ? 3 : b.type === 'procedural' ? 2 : 0) - (Date.now() - b.accessedAt) / (1000 * 60 * 60 * 24);
|
|
258
464
|
return scoreA - scoreB;
|
|
259
465
|
});
|
|
260
466
|
|
|
261
|
-
const toKeep = entries.slice(-
|
|
467
|
+
const toKeep = entries.slice(-800);
|
|
262
468
|
this.index.clear();
|
|
263
469
|
this.working = [];
|
|
264
470
|
for (const entry of toKeep) {
|
|
@@ -240,7 +240,13 @@ export class DeepSeekClient {
|
|
|
240
240
|
body.tool_choice = 'auto';
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
return JSON.stringify(
|
|
243
|
+
return JSON.stringify({
|
|
244
|
+
...body,
|
|
245
|
+
messages: (body.messages as Array<Record<string, unknown>>).map(m => {
|
|
246
|
+
if (m.role === 'tool' && !m.name) m.name = 'tool';
|
|
247
|
+
return m;
|
|
248
|
+
}),
|
|
249
|
+
});
|
|
244
250
|
}
|
|
245
251
|
|
|
246
252
|
private async makeRequest(config: DeepSeekConfig, body: string): Promise<Response> {
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
# Chinese Chess AI - 中国象棋智能体
|
|
2
|
-
|
|
3
|
-
基于 Alpha-Beta 搜索 + 自博弈学习的轻量级中国象棋 AI 引擎
|
|
4
|
-
|
|
5
|
-
## 特性
|
|
6
|
-
- ✅ 完整的中国象棋规则实现
|
|
7
|
-
- ✅ Alpha-Beta 剪枝搜索算法
|
|
8
|
-
- ✅ 高级优化:置换表、迭代加深、历史启发、杀手步法
|
|
9
|
-
- ✅ 自博弈强化学习系统(CPU友好)
|
|
10
|
-
- ✅ 轻量级设计,无需GPU
|
|
11
|
-
- ✅ 智能评估函数(棋子价值+位置价值+机动性)
|
|
12
|
-
- ✅ 命令行交互界面
|
|
13
|
-
|
|
14
|
-
## 安装依赖
|
|
15
|
-
```bash
|
|
16
|
-
pip install -r requirements.txt
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## 使用方法
|
|
20
|
-
|
|
21
|
-
### 人机对战
|
|
22
|
-
```bash
|
|
23
|
-
python main.py --mode vs
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
### AI自训练
|
|
27
|
-
```bash
|
|
28
|
-
python main.py --mode train --epochs 100
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
### AI对弈(观看两AI对战)
|
|
32
|
-
```bash
|
|
33
|
-
python main.py --mode watch
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## 项目结构
|
|
37
|
-
```
|
|
38
|
-
chinese_chess_ai/
|
|
39
|
-
├── core/ # 核心引擎
|
|
40
|
-
│ ├── board.py # 棋盘状态管理
|
|
41
|
-
│ ├── pieces.py # 棋子定义与规则
|
|
42
|
-
│ ├── move_generator.py # 走法生成器
|
|
43
|
-
│ └── evaluator.py # 局面评估器
|
|
44
|
-
├── search/ # 搜索算法
|
|
45
|
-
│ ├── alphabeta.py # Alpha-Beta搜索
|
|
46
|
-
│ └── optimizations.py # 搜索优化技术
|
|
47
|
-
├── learning/ # 学习系统
|
|
48
|
-
│ ├── trainer.py # 自博弈训练器
|
|
49
|
-
│ └── experience.py # 经验回放缓冲区
|
|
50
|
-
├── ui/ # 用户界面
|
|
51
|
-
│ └── cli.py # 命令行界面
|
|
52
|
-
├── utils/ # 工具函数
|
|
53
|
-
│ └── helpers.py # 辅助函数
|
|
54
|
-
├── main.py # 主入口
|
|
55
|
-
└── config.py # 配置参数
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## 技术栈
|
|
59
|
-
- Python 3.8+
|
|
60
|
-
- 纯NumPy计算(无深度学习框架依赖)
|
|
61
|
-
- 经典游戏树搜索算法
|
|
62
|
-
- 轻量级强化学习
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""
|
|
3
|
-
中国象棋AI - 配置参数
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
# 搜索配置
|
|
7
|
-
SEARCH_CONFIG = {
|
|
8
|
-
'max_depth': 4, # 最大搜索深度(默认4层,平衡速度与智能)
|
|
9
|
-
'iterative_deepening': True, # 启用迭代加深
|
|
10
|
-
'use_transposition_table': True, # 使用置换表
|
|
11
|
-
'use_killer_moves': True, # 使用杀手步法
|
|
12
|
-
'use_history_heuristic': True, # 使用历史启发
|
|
13
|
-
'null_move_pruning': True, # 空步裁剪
|
|
14
|
-
'null_move_reduction': 2, # 空步减少深度
|
|
15
|
-
'aspiration_window': 50, # 渴望窗口大小
|
|
16
|
-
'time_limit': 3.0, # 时间限制(秒)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
# 评估函数配置
|
|
20
|
-
EVALUATOR_CONFIG = {
|
|
21
|
-
# 棋子基础价值(分)
|
|
22
|
-
'piece_values': {
|
|
23
|
-
'KING': 10000, # 将/帅
|
|
24
|
-
'ADVISOR': 200, # 士/仕
|
|
25
|
-
'ELEPHANT': 200, # 象/相
|
|
26
|
-
'HORSE': 400, # 马
|
|
27
|
-
'CHARIOT': 900, # 车
|
|
28
|
-
'CANNON': 450, # 炮
|
|
29
|
-
'PAWN': 100, # 兵/卒
|
|
30
|
-
},
|
|
31
|
-
|
|
32
|
-
# 位置价值表权重
|
|
33
|
-
'position_weight': 0.6,
|
|
34
|
-
|
|
35
|
-
# 机动性权重
|
|
36
|
-
'mobility_weight': 0.15,
|
|
37
|
-
|
|
38
|
-
# 保护与威胁权重
|
|
39
|
-
'protection_weight': 0.1,
|
|
40
|
-
|
|
41
|
-
# 中心控制权重
|
|
42
|
-
'center_control_weight': 0.15,
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
# 训练配置
|
|
46
|
-
TRAINING_CONFIG = {
|
|
47
|
-
'learning_rate': 0.01,
|
|
48
|
-
'epochs': 100,
|
|
49
|
-
'games_per_epoch': 10,
|
|
50
|
-
'experience_replay_size': 10000,
|
|
51
|
-
'batch_size': 32,
|
|
52
|
-
'exploration_rate': 0.3, # 初始探索率
|
|
53
|
-
'exploration_decay': 0.995, # 探索率衰减
|
|
54
|
-
'min_exploration_rate': 0.05, # 最小探索率
|
|
55
|
-
'discount_factor': 0.95, # 折扣因子
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
# 棋盘配置
|
|
59
|
-
BOARD_CONFIG = {
|
|
60
|
-
'rows': 10,
|
|
61
|
-
'cols': 9,
|
|
62
|
-
'red_side': 'bottom', # 红方在下方
|
|
63
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
numpy>=1.21.0
|