deeper-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +254 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +12067 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +415 -0
- package/dist/index.js +1599 -0
- package/dist/index.js.map +1 -0
- package/docs/superpowers/plans/2026-05-14-deepercode-implementation.md +24 -0
- package/docs/superpowers/plans/2026-05-14-deepercode-plan.md +1248 -0
- package/docs/superpowers/specs/2026-05-14-deepercode-design.md +560 -0
- package/package.json +60 -0
- package/src/cli/bootstrap.ts +69 -0
- package/src/cli/chat-repl.ts +932 -0
- package/src/cli/commands/chat.ts +39 -0
- package/src/cli/commands/chat.tsx +39 -0
- package/src/cli/commands/config.ts +133 -0
- package/src/cli/commands/mcp.ts +172 -0
- package/src/cli/commands/run.ts +147 -0
- package/src/cli/commands/skill.ts +152 -0
- package/src/cli/index.ts +184 -0
- package/src/core/bugscan.ts +145 -0
- package/src/core/config.ts +285 -0
- package/src/core/constants.ts +49 -0
- package/src/core/eventbus.ts +202 -0
- package/src/core/logger.ts +109 -0
- package/src/core/storage.ts +96 -0
- package/src/index.ts +26 -0
- package/src/mcp/ConfigLoader.ts +74 -0
- package/src/mcp/MCPClient.ts +326 -0
- package/src/mcp/ResourceAdapter.ts +58 -0
- package/src/mcp/SSETransport.ts +133 -0
- package/src/mcp/StdioTransport.ts +116 -0
- package/src/mcp/ToolAdapter.ts +71 -0
- package/src/mcp/types.ts +58 -0
- package/src/memory/xmemory.ts +275 -0
- package/src/model/DeepSeekClient.ts +292 -0
- package/src/model/MessageBuilder.ts +155 -0
- package/src/model/RetryManager.ts +82 -0
- package/src/model/StreamHandler.ts +158 -0
- package/src/model/types.ts +86 -0
- package/src/skills/SkillCreator.ts +153 -0
- package/src/skills/SkillEngine.ts +158 -0
- package/src/skills/SkillExecutor.ts +107 -0
- package/src/skills/SkillLoader.ts +182 -0
- package/src/skills/SkillRegistry.ts +73 -0
- package/src/skills/SkillTrigger.ts +82 -0
- package/src/skills/types.ts +28 -0
- package/src/tools/ToolExecutor.ts +103 -0
- package/src/tools/ToolRegistry.ts +71 -0
- package/src/tools/ToolValidator.ts +103 -0
- package/src/tools/builtin/ai/context_summarize.ts +76 -0
- package/src/tools/builtin/ai/memory_store.ts +86 -0
- package/src/tools/builtin/ai/prompt_template.ts +71 -0
- package/src/tools/builtin/ai/skill_create.ts +53 -0
- package/src/tools/builtin/ai/subagent.ts +39 -0
- package/src/tools/builtin/ai/todo_manager.ts +157 -0
- package/src/tools/builtin/ai/token_count.ts +196 -0
- package/src/tools/builtin/ai/tool_create.ts +52 -0
- package/src/tools/builtin/code/analyze_deps.ts +72 -0
- package/src/tools/builtin/code/bug_scan.ts +80 -0
- package/src/tools/builtin/code/code_metrics.ts +111 -0
- package/src/tools/builtin/code/extract_function.ts +86 -0
- package/src/tools/builtin/code/format_code.ts +57 -0
- package/src/tools/builtin/code/generate_code.ts +75 -0
- package/src/tools/builtin/code/import_organizer.ts +82 -0
- package/src/tools/builtin/code/lint_code.ts +48 -0
- package/src/tools/builtin/code/parse_ast.ts +86 -0
- package/src/tools/builtin/code/refactor_code.ts +63 -0
- package/src/tools/builtin/code/type_check.ts +48 -0
- package/src/tools/builtin/data/chart_generate.ts +62 -0
- package/src/tools/builtin/data/csv_parse.ts +56 -0
- package/src/tools/builtin/data/data_diff.ts +79 -0
- package/src/tools/builtin/data/data_transform.ts +74 -0
- package/src/tools/builtin/data/data_validate.ts +75 -0
- package/src/tools/builtin/data/json_parse.ts +71 -0
- package/src/tools/builtin/data/template_render.ts +58 -0
- package/src/tools/builtin/data/toml_parse.ts +42 -0
- package/src/tools/builtin/data/xml_parse.ts +79 -0
- package/src/tools/builtin/data/yaml_parse.ts +42 -0
- package/src/tools/builtin/database/db_backup.ts +53 -0
- package/src/tools/builtin/database/db_restore.ts +51 -0
- package/src/tools/builtin/database/db_schema.ts +66 -0
- package/src/tools/builtin/database/nosql_query.ts +50 -0
- package/src/tools/builtin/database/orm_generate.ts +66 -0
- package/src/tools/builtin/database/redis_command.ts +46 -0
- package/src/tools/builtin/database/sql_migrate.ts +55 -0
- package/src/tools/builtin/database/sql_query.ts +60 -0
- package/src/tools/builtin/filesystem/batch_read.ts +56 -0
- package/src/tools/builtin/filesystem/batch_write.ts +67 -0
- package/src/tools/builtin/filesystem/copy_file.ts +36 -0
- package/src/tools/builtin/filesystem/create_dir.ts +30 -0
- package/src/tools/builtin/filesystem/delete_file.ts +30 -0
- package/src/tools/builtin/filesystem/diff_files.ts +47 -0
- package/src/tools/builtin/filesystem/edit_file.ts +47 -0
- package/src/tools/builtin/filesystem/file_info.ts +52 -0
- package/src/tools/builtin/filesystem/glob_find.ts +44 -0
- package/src/tools/builtin/filesystem/list_dir.ts +51 -0
- package/src/tools/builtin/filesystem/merge_files.ts +44 -0
- package/src/tools/builtin/filesystem/move_file.ts +37 -0
- package/src/tools/builtin/filesystem/read_file.ts +55 -0
- package/src/tools/builtin/filesystem/watch_file.ts +33 -0
- package/src/tools/builtin/filesystem/write_file.ts +45 -0
- package/src/tools/builtin/index.ts +244 -0
- package/src/tools/builtin/network/api_call.ts +79 -0
- package/src/tools/builtin/network/browser_action.ts +54 -0
- package/src/tools/builtin/network/check_url.ts +59 -0
- package/src/tools/builtin/network/download_file.ts +64 -0
- package/src/tools/builtin/network/graphql_query.ts +46 -0
- package/src/tools/builtin/network/http_request.ts +61 -0
- package/src/tools/builtin/network/parse_html.ts +101 -0
- package/src/tools/builtin/network/proxy_request.ts +53 -0
- package/src/tools/builtin/network/screenshot_page.ts +58 -0
- package/src/tools/builtin/network/web_fetch.ts +70 -0
- package/src/tools/builtin/network/web_search.ts +128 -0
- package/src/tools/builtin/network/websocket_connect.ts +70 -0
- package/src/tools/builtin/project/build_project.ts +68 -0
- package/src/tools/builtin/project/config_manage.ts +99 -0
- package/src/tools/builtin/project/coverage_report.ts +59 -0
- package/src/tools/builtin/project/docker_manage.ts +90 -0
- package/src/tools/builtin/project/env_manage.ts +88 -0
- package/src/tools/builtin/project/npm_manage.ts +71 -0
- package/src/tools/builtin/project/project_init.ts +59 -0
- package/src/tools/builtin/project/run_test.ts +74 -0
- package/src/tools/builtin/search/codebase_search.ts +76 -0
- package/src/tools/builtin/search/find_definition.ts +84 -0
- package/src/tools/builtin/search/find_references.ts +75 -0
- package/src/tools/builtin/search/fuzzy_find.ts +75 -0
- package/src/tools/builtin/search/grep_search.ts +90 -0
- package/src/tools/builtin/search/regex_find.ts +91 -0
- package/src/tools/builtin/search/search_docs.ts +51 -0
- package/src/tools/builtin/search/search_package.ts +50 -0
- package/src/tools/builtin/search/symbol_search.ts +82 -0
- package/src/tools/builtin/search/text_search.ts +63 -0
- package/src/tools/builtin/security/decrypt_file.ts +54 -0
- package/src/tools/builtin/security/encrypt_file.ts +52 -0
- package/src/tools/builtin/security/hash_generate.ts +48 -0
- package/src/tools/builtin/security/jwt_decode.ts +53 -0
- package/src/tools/builtin/security/secret_scan.ts +82 -0
- package/src/tools/builtin/security/vulnerability_check.ts +71 -0
- package/src/tools/builtin/shell/background_terminal.ts +38 -0
- package/src/tools/builtin/shell/check_status.ts +48 -0
- package/src/tools/builtin/shell/interactive_terminal.ts +31 -0
- package/src/tools/builtin/shell/kill_terminal.ts +29 -0
- package/src/tools/builtin/shell/list_terminals.ts +61 -0
- package/src/tools/builtin/shell/pipe_commands.ts +55 -0
- package/src/tools/builtin/shell/process-pool.ts +150 -0
- package/src/tools/builtin/shell/run_async.ts +73 -0
- package/src/tools/builtin/shell/run_command.ts +60 -0
- package/src/tools/builtin/shell/send_ctrl_keys.ts +43 -0
- package/src/tools/builtin/shell/send_keys.ts +36 -0
- package/src/tools/builtin/shell/send_text.ts +35 -0
- package/src/tools/builtin/shell/shell_script.ts +65 -0
- package/src/tools/builtin/shell/stop_command.ts +40 -0
- package/src/tools/builtin/shell/terminal_resize.ts +31 -0
- package/src/tools/builtin/shell/terminal_screenshot.ts +28 -0
- package/src/tools/builtin/system/log_viewer.ts +89 -0
- package/src/tools/builtin/system/notify_user.ts +55 -0
- package/src/tools/builtin/system/process_list.ts +66 -0
- package/src/tools/builtin/system/resource_monitor.ts +66 -0
- package/src/tools/builtin/system/system_info.ts +41 -0
- package/src/tools/tool-types.ts +97 -0
- package/src/ui/AgentTree.tsx +98 -0
- package/src/ui/App.tsx +46 -0
- package/src/ui/ChatView.tsx +278 -0
- package/src/ui/ConfirmDialog.tsx +68 -0
- package/src/ui/DiffView.tsx +64 -0
- package/src/ui/FilePreview.tsx +59 -0
- package/src/ui/InputBox.tsx +267 -0
- package/src/ui/MessageBubble.tsx +30 -0
- package/src/ui/Spinner.tsx +35 -0
- package/src/ui/StatusBar.tsx +41 -0
- package/src/ui/ToolCallCard.tsx +73 -0
- package/src/ui/ansi.ts +50 -0
- package/src/ui/markdown.ts +238 -0
- package/src/ui/themes/dark.ts +4 -0
- package/src/ui/themes/default.ts +25 -0
- package/src/ui/themes/light.ts +14 -0
- package/tests/unit/BuiltinTools.test.ts +129 -0
- package/tests/unit/BuiltinToolsIntegration.test.ts +111 -0
- package/tests/unit/FilesystemTools.test.ts +211 -0
- package/tests/unit/SkillLoader.test.ts +141 -0
- package/tests/unit/SkillRegistry.test.ts +113 -0
- package/tests/unit/ToolExecutor.test.ts +160 -0
- package/tests/unit/ToolRegistry.test.ts +103 -0
- package/tests/unit/ToolValidator.test.ts +137 -0
- package/tsconfig.json +28 -0
- package/tsup.config.ts +17 -0
- package/vitest.config.ts +20 -0
package/src/mcp/types.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { JSONSchema } from '../tools/tool-types.js';
|
|
2
|
+
|
|
3
|
+
export interface MCPServerConfig {
|
|
4
|
+
name: string;
|
|
5
|
+
type: 'stdio' | 'sse';
|
|
6
|
+
command?: string;
|
|
7
|
+
args?: string[];
|
|
8
|
+
url?: string;
|
|
9
|
+
headers?: Record<string, string>;
|
|
10
|
+
env?: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface MCPTool {
|
|
14
|
+
name: string;
|
|
15
|
+
description: string;
|
|
16
|
+
inputSchema: JSONSchema;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface MCPResource {
|
|
20
|
+
uri: string;
|
|
21
|
+
name: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
mimeType?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface JSONRPCRequest {
|
|
27
|
+
jsonrpc: '2.0';
|
|
28
|
+
id: number | string;
|
|
29
|
+
method: string;
|
|
30
|
+
params?: Record<string, unknown>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface JSONRPCResponse {
|
|
34
|
+
jsonrpc: '2.0';
|
|
35
|
+
id: number | string;
|
|
36
|
+
result?: unknown;
|
|
37
|
+
error?: {
|
|
38
|
+
code: number;
|
|
39
|
+
message: string;
|
|
40
|
+
data?: unknown;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface JSONRPCNotification {
|
|
45
|
+
jsonrpc: '2.0';
|
|
46
|
+
method: string;
|
|
47
|
+
params?: Record<string, unknown>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type JSONRPCMessage = JSONRPCRequest | JSONRPCResponse | JSONRPCNotification;
|
|
51
|
+
|
|
52
|
+
export interface MCPTransport {
|
|
53
|
+
connect(config: MCPServerConfig): Promise<void>;
|
|
54
|
+
disconnect(): void;
|
|
55
|
+
send(message: JSONRPCMessage): Promise<void>;
|
|
56
|
+
onMessage(handler: (message: JSONRPCMessage) => void): () => void;
|
|
57
|
+
isConnected(): boolean;
|
|
58
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { DEEPER_HOME } from '../core/constants.js';
|
|
4
|
+
|
|
5
|
+
export interface MemoryEntry {
|
|
6
|
+
id: string;
|
|
7
|
+
type: 'working' | 'episodic' | 'semantic' | 'procedural';
|
|
8
|
+
content: string;
|
|
9
|
+
tags: string[];
|
|
10
|
+
importance: number; // 0-10
|
|
11
|
+
accuracy: number; // 0-10, 自评置信度
|
|
12
|
+
createdAt: number;
|
|
13
|
+
accessedAt: number;
|
|
14
|
+
accessCount: number;
|
|
15
|
+
sessionId: string;
|
|
16
|
+
source: string; // 'user' | 'agent' | 'tool' | 'system'
|
|
17
|
+
references: string[]; // 关联条目 ID
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface XMemStats {
|
|
21
|
+
totalEntries: number;
|
|
22
|
+
byType: Record<string, number>;
|
|
23
|
+
totalTokens: number;
|
|
24
|
+
lastCleanup: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const MEM_DIR = join(DEEPER_HOME, 'xmemory');
|
|
28
|
+
const STATS_FILE = join(MEM_DIR, 'stats.json');
|
|
29
|
+
const MAX_WORKING_MEM = 50;
|
|
30
|
+
const MAX_TOTAL_MEM = 2000;
|
|
31
|
+
const CLEANUP_THRESHOLD = 1500;
|
|
32
|
+
|
|
33
|
+
let currentSessionId = '';
|
|
34
|
+
|
|
35
|
+
export function setSessionId(id: string) { currentSessionId = id; }
|
|
36
|
+
|
|
37
|
+
function uid(): string {
|
|
38
|
+
return `mem_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function ensureDir(): void {
|
|
42
|
+
if (!existsSync(MEM_DIR)) mkdirSync(MEM_DIR, { recursive: true });
|
|
43
|
+
if (!existsSync(STATS_FILE)) {
|
|
44
|
+
writeFileSync(STATS_FILE, JSON.stringify({ totalEntries: 0, byType: {}, totalTokens: 0, lastCleanup: Date.now() }), 'utf-8');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function loadStats(): XMemStats {
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(readFileSync(STATS_FILE, 'utf-8')) as XMemStats;
|
|
51
|
+
} catch { return { totalEntries: 0, byType: {}, totalTokens: 0, lastCleanup: 0 }; }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function saveStats(s: XMemStats): void {
|
|
55
|
+
writeFileSync(STATS_FILE, JSON.stringify(s, null, 2), 'utf-8');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class XMemory {
|
|
59
|
+
private working: MemoryEntry[] = [];
|
|
60
|
+
private index: Map<string, MemoryEntry> = new Map();
|
|
61
|
+
private dirty = false;
|
|
62
|
+
|
|
63
|
+
constructor() {
|
|
64
|
+
ensureDir();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ============ 写入 ============
|
|
68
|
+
store(
|
|
69
|
+
type: MemoryEntry['type'],
|
|
70
|
+
content: string,
|
|
71
|
+
tags: string[] = [],
|
|
72
|
+
importance = 5,
|
|
73
|
+
accuracy = 7,
|
|
74
|
+
source: MemoryEntry['source'] = 'agent',
|
|
75
|
+
references: string[] = [],
|
|
76
|
+
): string {
|
|
77
|
+
const id = uid();
|
|
78
|
+
const entry: MemoryEntry = {
|
|
79
|
+
id, type, content: content.slice(0, 2000), tags,
|
|
80
|
+
importance: Math.min(10, Math.max(0, importance)),
|
|
81
|
+
accuracy: Math.min(10, Math.max(0, accuracy)),
|
|
82
|
+
createdAt: Date.now(), accessedAt: Date.now(),
|
|
83
|
+
accessCount: 1, sessionId: currentSessionId,
|
|
84
|
+
source, references,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
if (type === 'working') {
|
|
88
|
+
this.working.push(entry);
|
|
89
|
+
if (this.working.length > MAX_WORKING_MEM) {
|
|
90
|
+
this.working.shift();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this.index.set(id, entry);
|
|
95
|
+
this.dirty = true;
|
|
96
|
+
|
|
97
|
+
if (this.index.size > CLEANUP_THRESHOLD) {
|
|
98
|
+
this.autoCleanup();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return id;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
storeEpisodic(content: string, tags: string[] = [], importance = 5): string {
|
|
105
|
+
return this.store('episodic', content, tags, importance, 7, 'agent');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
storeSemantic(content: string, tags: string[] = [], importance = 7): string {
|
|
109
|
+
return this.store('semantic', content, tags, importance, 9, 'agent');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
storeProcedural(content: string, tags: string[] = [], importance = 8): string {
|
|
113
|
+
return this.store('procedural', content, tags, importance, 9, 'agent');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
storeWorking(content: string, tags: string[] = []): string {
|
|
117
|
+
return this.store('working', content, tags, 3, 5, 'agent');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ============ 检索 ============
|
|
121
|
+
recall(query: string, limit = 5, minImportance = 0): MemoryEntry[] {
|
|
122
|
+
const keywords = query.toLowerCase().split(/[\s,,。]+/).filter(w => w.length > 1);
|
|
123
|
+
if (keywords.length === 0) return [];
|
|
124
|
+
|
|
125
|
+
const scored: Array<{ entry: MemoryEntry; score: number }> = [];
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
|
|
128
|
+
for (const entry of this.index.values()) {
|
|
129
|
+
if (entry.importance < minImportance) continue;
|
|
130
|
+
let score = 0;
|
|
131
|
+
const c = (entry.content || '').toLowerCase();
|
|
132
|
+
const t = entry.tags.join(' ').toLowerCase();
|
|
133
|
+
|
|
134
|
+
for (const kw of keywords) {
|
|
135
|
+
if (c.includes(kw)) score += 3;
|
|
136
|
+
if (t.includes(kw)) score += 2;
|
|
137
|
+
if (entry.type === 'procedural' && c.includes(kw)) score += 1;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 衰减: 重要度高 + 最近访问 权重高
|
|
141
|
+
const age = (now - entry.accessedAt) / (1000 * 60 * 60);
|
|
142
|
+
score += entry.importance * 0.5;
|
|
143
|
+
score -= Math.min(age / 24, 5); // 每 24 小时衰减 max 5
|
|
144
|
+
|
|
145
|
+
if (score > 0) scored.push({ entry, score });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
scored.sort((a, b) => b.score - a.score);
|
|
149
|
+
return scored.slice(0, limit).map(s => {
|
|
150
|
+
s.entry.accessedAt = now;
|
|
151
|
+
s.entry.accessCount++;
|
|
152
|
+
return s.entry;
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
getByType(type: MemoryEntry['type'], limit = 20): MemoryEntry[] {
|
|
157
|
+
const res: MemoryEntry[] = [];
|
|
158
|
+
for (const entry of this.index.values()) {
|
|
159
|
+
if (entry.type === type) res.push(entry);
|
|
160
|
+
}
|
|
161
|
+
res.sort((a, b) => b.createdAt - a.createdAt);
|
|
162
|
+
return res.slice(0, limit);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
getWorking(): MemoryEntry[] {
|
|
166
|
+
return this.working;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
getWorkingContext(maxTokens = 2000): string {
|
|
170
|
+
if (this.working.length === 0) return '';
|
|
171
|
+
const lines = this.working.map(e => `[工作记忆] ${e.content.slice(0, 300)}`);
|
|
172
|
+
let result = '';
|
|
173
|
+
for (const line of lines) {
|
|
174
|
+
if ((result + line).length > maxTokens * 4) break;
|
|
175
|
+
result += line + '\n';
|
|
176
|
+
}
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
getProceduralHints(task: string, limit = 5): string {
|
|
181
|
+
const recalled = this.recall(task, limit, 3);
|
|
182
|
+
const proc = recalled.filter(r => r.type === 'procedural' || r.type === 'semantic');
|
|
183
|
+
if (proc.length === 0) return '';
|
|
184
|
+
return '[记忆提示]\n' + proc.map(p => `- ${p.content.slice(0, 250)}`).join('\n');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
getSessionSummary(): string {
|
|
188
|
+
const entries = [...this.index.values()].filter(e => e.sessionId === currentSessionId);
|
|
189
|
+
if (entries.length === 0) return '';
|
|
190
|
+
const key = entries
|
|
191
|
+
.filter(e => e.importance >= 5)
|
|
192
|
+
.sort((a, b) => b.importance - a.importance)
|
|
193
|
+
.slice(0, 8);
|
|
194
|
+
return `[XMemory·本会话]\n` + key.map(e => {
|
|
195
|
+
const t = e.type === 'semantic' ? '知识' : e.type === 'procedural' ? '技能' : e.type === 'episodic' ? '经历' : '工作';
|
|
196
|
+
return `[${t}] ${e.content.slice(0, 200)}`;
|
|
197
|
+
}).join('\n');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ============ 持久化 ============
|
|
201
|
+
async save(): Promise<void> {
|
|
202
|
+
if (!this.dirty) return;
|
|
203
|
+
ensureDir();
|
|
204
|
+
|
|
205
|
+
// 分批写入:每个 type 一个文件
|
|
206
|
+
const byType: Record<string, MemoryEntry[]> = {};
|
|
207
|
+
for (const entry of this.index.values()) {
|
|
208
|
+
if (!byType[entry.type]) byType[entry.type] = [];
|
|
209
|
+
byType[entry.type].push(entry);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
for (const [type, entries] of Object.entries(byType)) {
|
|
213
|
+
const file = join(MEM_DIR, `${type}.json`);
|
|
214
|
+
writeFileSync(file, JSON.stringify(entries.slice(-500)), 'utf-8'); // 每个类型最多 500 条
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const stats = loadStats();
|
|
218
|
+
stats.totalEntries = this.index.size;
|
|
219
|
+
stats.byType = {};
|
|
220
|
+
for (const entry of this.index.values()) {
|
|
221
|
+
stats.byType[entry.type] = (stats.byType[entry.type] || 0) + 1;
|
|
222
|
+
}
|
|
223
|
+
stats.totalTokens = [...this.index.values()].reduce((s, e) => s + e.content.length, 0);
|
|
224
|
+
saveStats(stats);
|
|
225
|
+
|
|
226
|
+
this.dirty = false;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async load(): Promise<void> {
|
|
230
|
+
ensureDir();
|
|
231
|
+
let total = 0;
|
|
232
|
+
|
|
233
|
+
for (const type of ['working', 'episodic', 'semantic', 'procedural']) {
|
|
234
|
+
const file = join(MEM_DIR, `${type}.json`);
|
|
235
|
+
if (!existsSync(file)) continue;
|
|
236
|
+
try {
|
|
237
|
+
const entries = JSON.parse(readFileSync(file, 'utf-8')) as MemoryEntry[];
|
|
238
|
+
for (const entry of entries) {
|
|
239
|
+
if (total > MAX_TOTAL_MEM) break;
|
|
240
|
+
this.index.set(entry.id, entry);
|
|
241
|
+
if (entry.type === 'working') this.working.push(entry);
|
|
242
|
+
total++;
|
|
243
|
+
}
|
|
244
|
+
} catch { /* skip corrupt files */ }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (this.working.length > MAX_WORKING_MEM) {
|
|
248
|
+
this.working = this.working.slice(-MAX_WORKING_MEM);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ============ 维护 ============
|
|
253
|
+
private autoCleanup(): void {
|
|
254
|
+
const entries = [...this.index.values()];
|
|
255
|
+
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);
|
|
258
|
+
return scoreA - scoreB;
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const toKeep = entries.slice(-500);
|
|
262
|
+
this.index.clear();
|
|
263
|
+
this.working = [];
|
|
264
|
+
for (const entry of toKeep) {
|
|
265
|
+
this.index.set(entry.id, entry);
|
|
266
|
+
if (entry.type === 'working') this.working.push(entry);
|
|
267
|
+
}
|
|
268
|
+
this.dirty = true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
get totalEntries(): number { return this.index.size; }
|
|
272
|
+
get dirtyState(): boolean { return this.dirty; }
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export const xmemory = new XMemory();
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import type { ChatMessage, DeepSeekConfig, StreamChunk } from './types.js';
|
|
2
|
+
import type { ToolDefinition, ToolCall } from '../tools/tool-types.js';
|
|
3
|
+
import { RetryManager } from './RetryManager.js';
|
|
4
|
+
import { StreamHandler } from './StreamHandler.js';
|
|
5
|
+
import { logger } from '../core/logger.js';
|
|
6
|
+
|
|
7
|
+
interface ChatCompletionRequest {
|
|
8
|
+
model: string;
|
|
9
|
+
messages: Array<Record<string, unknown>>;
|
|
10
|
+
temperature?: number;
|
|
11
|
+
max_tokens?: number;
|
|
12
|
+
stream?: boolean;
|
|
13
|
+
tools?: Array<Record<string, unknown>>;
|
|
14
|
+
tool_choice?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ChatCompletionResponse {
|
|
18
|
+
id: string;
|
|
19
|
+
object: string;
|
|
20
|
+
created: number;
|
|
21
|
+
model: string;
|
|
22
|
+
choices: Array<{
|
|
23
|
+
index: number;
|
|
24
|
+
message: {
|
|
25
|
+
role: string;
|
|
26
|
+
content: string | null;
|
|
27
|
+
tool_calls?: Array<{
|
|
28
|
+
id: string;
|
|
29
|
+
type: string;
|
|
30
|
+
function: {
|
|
31
|
+
name: string;
|
|
32
|
+
arguments: string;
|
|
33
|
+
};
|
|
34
|
+
}>;
|
|
35
|
+
reasoning_content?: string;
|
|
36
|
+
};
|
|
37
|
+
finish_reason: string;
|
|
38
|
+
}>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const DEFAULT_TIMEOUT_MS = 120000;
|
|
42
|
+
const MAX_RETRIES = 3;
|
|
43
|
+
|
|
44
|
+
export class DeepSeekClient {
|
|
45
|
+
private config: DeepSeekConfig;
|
|
46
|
+
private retryManager: RetryManager;
|
|
47
|
+
|
|
48
|
+
constructor(config: DeepSeekConfig) {
|
|
49
|
+
this.config = config;
|
|
50
|
+
this.retryManager = new RetryManager(config.maxTokens > 0 ? MAX_RETRIES : MAX_RETRIES);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async chat(
|
|
54
|
+
messages: ChatMessage[],
|
|
55
|
+
tools?: ToolDefinition[],
|
|
56
|
+
overrides?: Partial<DeepSeekConfig>,
|
|
57
|
+
): Promise<ChatMessage> {
|
|
58
|
+
const cfg = this.mergeConfig(overrides);
|
|
59
|
+
const body = this.buildRequestBody(messages, tools, cfg, false);
|
|
60
|
+
|
|
61
|
+
const response = await this.retryManager.execute(async () => {
|
|
62
|
+
const result = await this.retryManager.withTimeout(
|
|
63
|
+
() => this.makeRequest(cfg, body),
|
|
64
|
+
cfg.maxTokens > 0 ? DEFAULT_TIMEOUT_MS : DEFAULT_TIMEOUT_MS,
|
|
65
|
+
);
|
|
66
|
+
return result;
|
|
67
|
+
}, this.shouldRetry);
|
|
68
|
+
|
|
69
|
+
const data = (await response.json()) as ChatCompletionResponse;
|
|
70
|
+
|
|
71
|
+
if (!data.choices || data.choices.length === 0) {
|
|
72
|
+
throw new Error('No choices returned from API');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const choice = data.choices[0];
|
|
76
|
+
const message = choice.message;
|
|
77
|
+
|
|
78
|
+
const result: ChatMessage = {
|
|
79
|
+
role: 'assistant',
|
|
80
|
+
content: message.content,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if (message.tool_calls) {
|
|
84
|
+
result.tool_calls = message.tool_calls.map((tc) => ({
|
|
85
|
+
id: tc.id,
|
|
86
|
+
name: tc.function.name,
|
|
87
|
+
arguments: this.parseArguments(tc.function.arguments),
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (message.reasoning_content) {
|
|
92
|
+
result.thinking = message.reasoning_content;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async chatStream(
|
|
99
|
+
messages: ChatMessage[],
|
|
100
|
+
tools?: ToolDefinition[],
|
|
101
|
+
overrides?: Partial<DeepSeekConfig>,
|
|
102
|
+
): Promise<AsyncIterable<StreamChunk>> {
|
|
103
|
+
const cfg = this.mergeConfig(overrides);
|
|
104
|
+
const body = this.buildRequestBody(messages, tools, cfg, true);
|
|
105
|
+
|
|
106
|
+
const response = await this.retryManager.execute(async () => {
|
|
107
|
+
const result = await this.retryManager.withTimeout(
|
|
108
|
+
() => this.makeRequest(cfg, body),
|
|
109
|
+
DEFAULT_TIMEOUT_MS,
|
|
110
|
+
);
|
|
111
|
+
return result;
|
|
112
|
+
}, this.shouldRetry);
|
|
113
|
+
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
const errorBody = await response.text().catch(() => '');
|
|
116
|
+
throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorBody}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!response.body) {
|
|
120
|
+
throw new Error('Response body is empty');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return this.createStreamIterable(response.body);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private async *createStreamIterable(body: unknown): AsyncIterable<StreamChunk> {
|
|
127
|
+
const handler = new StreamHandler();
|
|
128
|
+
let buffer = '';
|
|
129
|
+
const stream = body as AsyncIterable<Uint8Array>;
|
|
130
|
+
|
|
131
|
+
for await (const chunk of stream) {
|
|
132
|
+
const text = typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk);
|
|
133
|
+
buffer += text;
|
|
134
|
+
|
|
135
|
+
const lines = buffer.split('\n');
|
|
136
|
+
buffer = lines.pop() ?? '';
|
|
137
|
+
|
|
138
|
+
for (const line of lines) {
|
|
139
|
+
const trimmed = line.trim();
|
|
140
|
+
if (!trimmed || trimmed.startsWith(':')) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (trimmed.startsWith('data: ')) {
|
|
145
|
+
const data = trimmed.slice(6);
|
|
146
|
+
const result = handler.handleEvent('message', data);
|
|
147
|
+
if (result) {
|
|
148
|
+
yield result;
|
|
149
|
+
}
|
|
150
|
+
} else if (trimmed === 'data: [DONE]') {
|
|
151
|
+
yield { type: 'done' };
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (buffer.trim()) {
|
|
158
|
+
const trimmed = buffer.trim();
|
|
159
|
+
if (trimmed.startsWith('data: ')) {
|
|
160
|
+
const data = trimmed.slice(6);
|
|
161
|
+
const result = handler.handleEvent('message', data);
|
|
162
|
+
if (result) {
|
|
163
|
+
yield result;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!handler.isFinished()) {
|
|
169
|
+
yield { type: 'done' };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private buildRequestBody(
|
|
174
|
+
messages: ChatMessage[],
|
|
175
|
+
tools: ToolDefinition[] | undefined,
|
|
176
|
+
config: DeepSeekConfig,
|
|
177
|
+
stream: boolean,
|
|
178
|
+
): string {
|
|
179
|
+
const body: ChatCompletionRequest = {
|
|
180
|
+
model: config.model,
|
|
181
|
+
messages: messages.map((m) => {
|
|
182
|
+
const msg: Record<string, unknown> = {
|
|
183
|
+
role: m.role,
|
|
184
|
+
content: m.content,
|
|
185
|
+
};
|
|
186
|
+
if (m.tool_calls) {
|
|
187
|
+
msg.tool_calls = m.tool_calls.map((tc) => ({
|
|
188
|
+
id: tc.id,
|
|
189
|
+
type: 'function',
|
|
190
|
+
function: {
|
|
191
|
+
name: tc.name,
|
|
192
|
+
arguments: JSON.stringify(tc.arguments),
|
|
193
|
+
},
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
if (m.tool_call_id) {
|
|
197
|
+
msg.tool_call_id = m.tool_call_id;
|
|
198
|
+
}
|
|
199
|
+
if (m.name) {
|
|
200
|
+
msg.name = m.name;
|
|
201
|
+
}
|
|
202
|
+
return msg;
|
|
203
|
+
}),
|
|
204
|
+
temperature: config.temperature,
|
|
205
|
+
max_tokens: config.maxTokens,
|
|
206
|
+
stream,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
if (tools && tools.length > 0) {
|
|
210
|
+
body.tools = tools.map((tool) => ({
|
|
211
|
+
type: 'function',
|
|
212
|
+
function: {
|
|
213
|
+
name: tool.name,
|
|
214
|
+
description: tool.description,
|
|
215
|
+
parameters: tool.parameters,
|
|
216
|
+
},
|
|
217
|
+
}));
|
|
218
|
+
body.tool_choice = 'auto';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return JSON.stringify(body);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private async makeRequest(config: DeepSeekConfig, body: string): Promise<Response> {
|
|
225
|
+
const url = `${config.baseUrl}/v1/chat/completions`;
|
|
226
|
+
|
|
227
|
+
logger.debug('DeepSeek API request', { url, model: config.model });
|
|
228
|
+
|
|
229
|
+
const response = await fetch(url, {
|
|
230
|
+
method: 'POST',
|
|
231
|
+
headers: {
|
|
232
|
+
'Content-Type': 'application/json',
|
|
233
|
+
'Authorization': `Bearer ${config.apiKey}`,
|
|
234
|
+
'Accept': 'application/json',
|
|
235
|
+
},
|
|
236
|
+
body,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
if (!response.ok) {
|
|
240
|
+
const errorBody = await response.text().catch(() => '');
|
|
241
|
+
logger.error('DeepSeek API error', {
|
|
242
|
+
status: response.status,
|
|
243
|
+
statusText: response.statusText,
|
|
244
|
+
body: errorBody.slice(0, 500),
|
|
245
|
+
});
|
|
246
|
+
throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorBody.slice(0, 200)}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return response;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private shouldRetry(error: Error, attempt: number): boolean {
|
|
253
|
+
const message = error.message.toLowerCase();
|
|
254
|
+
|
|
255
|
+
if (message.includes('429') || message.includes('rate limit')) {
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
if (message.includes('5') && (message.includes('500') || message.includes('502') || message.includes('503') || message.includes('504'))) {
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
if (message.includes('timeout') || message.includes('abort')) {
|
|
262
|
+
return attempt < 2;
|
|
263
|
+
}
|
|
264
|
+
if (message.includes('econnreset') || message.includes('econnrefused')) {
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private parseArguments(argsStr: string): Record<string, unknown> {
|
|
272
|
+
try {
|
|
273
|
+
return JSON.parse(argsStr) as Record<string, unknown>;
|
|
274
|
+
} catch {
|
|
275
|
+
return {};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private mergeConfig(overrides?: Partial<DeepSeekConfig>): DeepSeekConfig {
|
|
280
|
+
if (!overrides) {
|
|
281
|
+
return this.config;
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
...this.config,
|
|
285
|
+
...overrides,
|
|
286
|
+
think: {
|
|
287
|
+
...this.config.think,
|
|
288
|
+
...(overrides.think ?? {}),
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
}
|