#
tokens: 11242/50000 11/11 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .github
│   └── workflows
│       ├── ci.yml
│       └── release.yml
├── .gitignore
├── .npmignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── docker-compose.yml
├── Dockerfile
├── LICENSE
├── package.json
├── README.md
├── src
│   ├── index.ts
│   ├── logger.ts
│   ├── manager.ts
│   ├── types.ts
│   └── utils.ts
├── test
│   └── test-script.js
├── tsconfig.json
└── tsup.config.ts
```

# Files

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | FROM node:22-alpine
 2 | 
 3 | WORKDIR /app
 4 | 
 5 | COPY package.json ./
 6 | COPY tsconfig.json ./
 7 | COPY src ./src
 8 | 
 9 | RUN npm install
10 | RUN npm run build
11 | 
12 | CMD ["npm", "start"]
```

--------------------------------------------------------------------------------
/tsup.config.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { defineConfig } from 'tsup';
 2 | 
 3 | export default defineConfig({
 4 |   entry: ['src/index.ts'],
 5 |   format: ['esm'],
 6 |   dts: true,
 7 |   clean: true,
 8 |   outExtension: () => ({
 9 |     js: '.mjs',
10 |   }),
11 | });
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "NodeNext",
 5 |     "moduleResolution": "NodeNext",
 6 |     "esModuleInterop": true,
 7 |     "strict": true,
 8 |     "skipLibCheck": true,
 9 |     "forceConsistentCasingInFileNames": true,
10 |     "outDir": "dist",
11 |     "declaration": true,
12 |     "sourceMap": true
13 |   },
14 |   "include": ["src/**/*"],
15 |   "exclude": ["node_modules", "dist"]
16 | }
```

--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------

```yaml
 1 | version: '3'
 2 | 
 3 | services:
 4 |     neo4j:
 5 |     image: neo4j:5.18.0
 6 |     container_name: ai-info-neo4j
 7 |     restart: always
 8 |     ports:
 9 |       - "7474:7474"  # HTTP
10 |       - "7687:7687"  # Bolt
11 |     environment:
12 |       - NEO4J_AUTH=neo4j/password  # 用户名/密码
13 |       - NEO4J_apoc_export_file_enabled=true
14 |       - NEO4J_apoc_import_file_enabled=true
15 |       - NEO4J_apoc_import_file_use__neo4j__config=true
16 |       - NEO4J_PLUGINS=["apoc"]  # 启用APOC插件
17 |       - NEO4J_dbms_security_procedures_unrestricted=apoc.*
18 |       - NEO4J_dbms_security_procedures_allowlist=apoc.*
19 |     volumes:
20 |       - neo4j_data:/data
21 |       - neo4j_logs:/logs
22 |       - neo4j_import:/var/lib/neo4j/import
23 |       - neo4j_plugins:/plugins
24 |     healthcheck:
25 |       test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:7474" ]
26 |       interval: 10s
27 |       timeout: 10s
28 |       retries: 5
29 |       start_period: 40s
```

--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from "zod";
 2 | 
 3 | // 实体对象模式
 4 | export const EntityObject = z.object({
 5 |   name: z.string().describe("The name of the entity"),
 6 |   entityType: z.string().describe("The type of the entity"),
 7 |   observations: z.array(z.string()).describe("An array of observation contents associated with the entity")
 8 | });
 9 | 
10 | // 关系对象模式
11 | export const RelationObject = z.object({
12 |   from: z.string().describe("The name of the entity where the relation starts"),
13 |   to: z.string().describe("The name of the entity where the relation ends"),
14 |   relationType: z.string().describe("The type of the relation")
15 | });
16 | 
17 | // 观察对象模式
18 | export const ObservationObject = z.object({
19 |   entityName: z.string().describe("The name of the entity to add the observations to"),
20 |   contents: z.array(z.string()).describe("An array of observation contents to add")
21 | });
22 | 
23 | // 实体类型
24 | export type Entity = z.infer<typeof EntityObject>;
25 | 
26 | // 关系类型
27 | export type Relation = z.infer<typeof RelationObject>;
28 | 
29 | // 观察类型
30 | export type Observation = z.infer<typeof ObservationObject>;
31 | 
32 | // 知识图谱类型
33 | export interface KnowledgeGraph {
34 |   entities: Entity[];
35 |   relations: Relation[];
36 | }
37 | 
38 | // 删除观察类型
39 | export interface ObservationDeletion {
40 |   entityName: string;
41 |   contents: string[];
42 | }
```

--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: Release
 2 | 
 3 | on:
 4 |   push:
 5 |     branches:
 6 |       - main
 7 | 
 8 | concurrency: ${{ github.workflow }}-${{ github.ref }}
 9 | 
10 | jobs:
11 |   release:
12 |     name: Release
13 |     runs-on: ubuntu-latest
14 |     steps:
15 |       - name: Checkout Repo
16 |         uses: actions/checkout@v3
17 |         with:
18 |           fetch-depth: 0
19 | 
20 |       - name: Setup Node.js
21 |         uses: actions/setup-node@v3
22 |         with:
23 |           node-version: '22'
24 |           registry-url: 'https://registry.npmjs.org'
25 | 
26 |       - name: Install pnpm
27 |         uses: pnpm/action-setup@v2
28 |         with:
29 |           version: 8
30 |           run_install: false
31 | 
32 |       - name: Get pnpm store directory
33 |         id: pnpm-cache
34 |         shell: bash
35 |         run: |
36 |           echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
37 | 
38 |       - uses: actions/cache@v3
39 |         name: Setup pnpm cache
40 |         with:
41 |           path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
42 |           key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
43 |           restore-keys: |
44 |             ${{ runner.os }}-pnpm-store-
45 | 
46 |       - name: Install dependencies
47 |         run: pnpm install
48 | 
49 |       - name: Build
50 |         run: pnpm build
51 | 
52 |       - name: Create Release Pull Request or Publish to npm
53 |         id: changesets
54 |         uses: changesets/action@v1
55 |         with:
56 |           publish: pnpm release
57 |         env:
58 |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
59 |           NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```

--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: CI
 2 | 
 3 | on:
 4 |   push:
 5 |     branches: [main]
 6 |   pull_request:
 7 |     branches: [main]
 8 | 
 9 | jobs:
10 |   test:
11 |     runs-on: ubuntu-latest
12 |     
13 |     services:
14 |       neo4j:
15 |         image: neo4j:5-community
16 |         env:
17 |           NEO4J_AUTH: neo4j/password
18 |         ports:
19 |           - 7474:7474
20 |           - 7687:7687
21 |         options: >-
22 |           --health-cmd "cypher-shell -u neo4j -p password 'RETURN 1;'"
23 |           --health-interval 10s
24 |           --health-timeout 5s
25 |           --health-retries 5
26 | 
27 |     steps:
28 |       - uses: actions/checkout@v3
29 |       
30 |       - name: Setup Node.js
31 |         uses: actions/setup-node@v3
32 |         with:
33 |           node-version: '22'
34 |           
35 |       - name: Install pnpm
36 |         uses: pnpm/action-setup@v2
37 |         with:
38 |           version: 8
39 |           run_install: false
40 |           
41 |       - name: Get pnpm store directory
42 |         id: pnpm-cache
43 |         shell: bash
44 |         run: |
45 |           echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
46 |           
47 |       - uses: actions/cache@v3
48 |         name: Setup pnpm cache
49 |         with:
50 |           path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
51 |           key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
52 |           restore-keys: |
53 |             ${{ runner.os }}-pnpm-store-
54 |             
55 |       - name: Install dependencies
56 |         run: pnpm install
57 |         
58 |       - name: Build
59 |         run: pnpm build
60 |         
61 |       - name: Run tests
62 |         run: pnpm test
63 |         env:
64 |           NEO4J_URI: bolt://localhost:7687
65 |           NEO4J_USER: neo4j
66 |           NEO4J_PASSWORD: password
67 |           NEO4J_DATABASE: neo4j
```

--------------------------------------------------------------------------------
/src/logger.ts:
--------------------------------------------------------------------------------

```typescript
 1 | // 日志级别枚举
 2 | export enum LogLevel {
 3 |   DEBUG = "debug",
 4 |   INFO = "info",
 5 |   WARN = "warning",
 6 |   ERROR = "error",
 7 | }
 8 | 
 9 | // 日志记录器接口
10 | export interface Logger {
11 |   debug(message: string, payload?: any): void;
12 |   info(message: string, payload?: any): void;
13 |   warn(message: string, payload?: any): void;
14 |   error(message: string, payload?: any): void;
15 |   setLevel(level: LogLevel): void;
16 | }
17 | 
18 | // 空日志记录器实现
19 | export class NullLogger implements Logger {
20 |   debug(message: string, payload?: any): void {}
21 |   info(message: string, payload?: any): void {}
22 |   warn(message: string, payload?: any): void {}
23 |   error(message: string, payload?: any): void {}
24 |   setLevel(level: LogLevel): void {}
25 | }
26 | 
27 | // 控制台日志记录器实现
28 | export class ConsoleLogger implements Logger {
29 |   private level: LogLevel = LogLevel.INFO;
30 | 
31 |   setLevel(level: LogLevel): void {
32 |     this.level = level;
33 |   }
34 | 
35 |   debug(message: string, payload?: any): void {
36 |     if (this.shouldLog(LogLevel.DEBUG)) {
37 |       console.debug(message, payload);
38 |     }
39 |   }
40 | 
41 |   info(message: string, payload?: any): void {
42 |     if (this.shouldLog(LogLevel.INFO)) {
43 |       console.info(message, payload);
44 |     }
45 |   }
46 | 
47 |   warn(message: string, payload?: any): void {
48 |     if (this.shouldLog(LogLevel.WARN)) {
49 |       console.warn(message, payload);
50 |     }
51 |   }
52 | 
53 |   error(message: string, payload?: any): void {
54 |     if (this.shouldLog(LogLevel.ERROR)) {
55 |       console.error(message, payload);
56 |     }
57 |   }
58 | 
59 |   private shouldLog(messageLevel: LogLevel): boolean {
60 |     const levels = [
61 |       LogLevel.DEBUG,
62 |       LogLevel.INFO,
63 |       LogLevel.WARN,
64 |       LogLevel.ERROR,
65 |     ];
66 |     return levels.indexOf(messageLevel) >= levels.indexOf(this.level);
67 |   }
68 | }
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "@jovanhsu/mcp-neo4j-memory-server",
 3 |   "version": "1.0.0",
 4 |   "private": false,
 5 |   "description": "MCP Memory Server with Neo4j backend for AI knowledge graph storage",
 6 |   "homepage": "https://github.com/JovanHsu/mcp-neo4j-memory-server",
 7 |   "repository": {
 8 |     "type": "git",
 9 |     "url": "git+https://github.com/JovanHsu/mcp-neo4j-memory-server.git"
10 |   },
11 |   "type": "module",
12 |   "main": "dist/index.mjs",
13 |   "types": "dist/index.d.ts",
14 |   "bin": {
15 |     "mcp-neo4j-memory-server": "dist/index.mjs"
16 |   },
17 |   "files": [
18 |     "dist",
19 |     "README.md",
20 |     "LICENSE"
21 |   ],
22 |   "keywords": [
23 |     "mcp",
24 |     "memory",
25 |     "knowledge",
26 |     "graph",
27 |     "neo4j",
28 |     "ai",
29 |     "claude",
30 |     "anthropic",
31 |     "knowledge-graph",
32 |     "memory-server",
33 |     "model-context-protocol"
34 |   ],
35 |   "author": "JovanHsu",
36 |   "license": "MIT",
37 |   "bugs": {
38 |     "url": "https://github.com/JovanHsu/mcp-neo4j-memory-server/issues"
39 |   },
40 |   "dependencies": {
41 |     "@modelcontextprotocol/sdk": "^1.6.0",
42 |     "neo4j-driver": "^5.18.0",
43 |     "fuse.js": "^7.1.0",
44 |     "zod": "^3.24.2"
45 |   },
46 |   "devDependencies": {
47 |     "@changesets/cli": "^2.28.1",
48 |     "@types/node": "^22.13.5",
49 |     "prettier": "^3.5.2",
50 |     "shx": "^0.3.4",
51 |     "tsup": "^8.4.0",
52 |     "typescript": "^5.7.3",
53 |     "vitest": "^3.0.7"
54 |   },
55 |   "engines": {
56 |     "node": ">=22.0.0"
57 |   },
58 |   "scripts": {
59 |     "dev": "pnpm build && npx @modelcontextprotocol/inspector pnpm start",
60 |     "build": "tsup src/index.ts && shx chmod +x dist/index.mjs",
61 |     "start": "node dist/index.mjs 2>/dev/null",
62 |     "test": "vitest run",
63 |     "lint": "prettier --write .",
64 |     "prepublishOnly": "pnpm build",
65 |     "release": "pnpm build && changeset publish"
66 |   },
67 |   "publishConfig": {
68 |     "access": "public"
69 |   }
70 | }
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
  2 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
  3 | import { z } from 'zod';
  4 | import { ConsoleLogger, LogLevel } from './logger.js';
  5 | import { Neo4jKnowledgeGraphManager } from './manager.js';
  6 | import { EntityObject, ObservationObject, RelationObject } from './types.js';
  7 | import { extractError } from './utils.js';
  8 | 
  9 | // 创建MCP服务器
 10 | const server = new McpServer({
 11 |   name: 'neo4j-memory-server',
 12 |   version: '1.0.0',
 13 | });
 14 | 
 15 | // 创建日志记录器,并设置为仅输出错误信息
 16 | const logger = new ConsoleLogger();
 17 | logger.setLevel(LogLevel.ERROR);
 18 | 
 19 | // 创建知识图谱管理器
 20 | const knowledgeGraphManager = new Neo4jKnowledgeGraphManager(
 21 |   /**
 22 |    * 根据环境变量获取Neo4j配置
 23 |    * @returns Neo4j配置
 24 |    */
 25 |   () => {
 26 |     return {
 27 |       uri: process.env.NEO4J_URI || 'bolt://localhost:7687',
 28 |       user: process.env.NEO4J_USER || 'neo4j',
 29 |       password: process.env.NEO4J_PASSWORD || 'password',
 30 |       database: process.env.NEO4J_DATABASE || 'neo4j',
 31 |     };
 32 |   },
 33 |   logger
 34 | );
 35 | 
 36 | // 注册创建实体工具
 37 | server.tool(
 38 |   'create_entities',
 39 |   'Create multiple new entities in the knowledge graph',
 40 |   {
 41 |     entities: z.array(EntityObject),
 42 |   },
 43 |   async ({ entities }) => ({
 44 |     content: [
 45 |       {
 46 |         type: 'text',
 47 |         text: JSON.stringify(
 48 |           await knowledgeGraphManager.createEntities(entities),
 49 |           null,
 50 |           2
 51 |         ),
 52 |       },
 53 |     ],
 54 |   })
 55 | );
 56 | 
 57 | // 注册创建关系工具
 58 | server.tool(
 59 |   'create_relations',
 60 |   'Create multiple new relations between entities in the knowledge graph. Relations should be in active voice',
 61 |   {
 62 |     relations: z.array(RelationObject),
 63 |   },
 64 |   async ({ relations }) => ({
 65 |     content: [
 66 |       {
 67 |         type: 'text',
 68 |         text: JSON.stringify(
 69 |           await knowledgeGraphManager.createRelations(relations),
 70 |           null,
 71 |           2
 72 |         ),
 73 |       },
 74 |     ],
 75 |   })
 76 | );
 77 | 
 78 | // 注册添加观察工具
 79 | server.tool(
 80 |   'add_observations',
 81 |   'Add new observations to existing entities in the knowledge graph',
 82 |   {
 83 |     observations: z.array(ObservationObject),
 84 |   },
 85 |   async ({ observations }) => ({
 86 |     content: [
 87 |       {
 88 |         type: 'text',
 89 |         text: JSON.stringify(
 90 |           await knowledgeGraphManager.addObservations(observations),
 91 |           null,
 92 |           2
 93 |         ),
 94 |       },
 95 |     ],
 96 |   })
 97 | );
 98 | 
 99 | // 注册删除实体工具
100 | server.tool(
101 |   'delete_entities',
102 |   'Delete multiple entities and their associated relations from the knowledge graph',
103 |   {
104 |     entityNames: z
105 |       .array(z.string())
106 |       .describe('An array of entity names to delete'),
107 |   },
108 |   async ({ entityNames }) => {
109 |     await knowledgeGraphManager.deleteEntities(entityNames);
110 |     return {
111 |       content: [{ type: 'text', text: 'Entities deleted successfully' }],
112 |     };
113 |   }
114 | );
115 | 
116 | // 注册删除观察工具
117 | server.tool(
118 |   'delete_observations',
119 |   'Delete specific observations from entities in the knowledge graph',
120 |   {
121 |     deletions: z.array(
122 |       z.object({
123 |         entityName: z
124 |           .string()
125 |           .describe('The name of the entity containing the observations'),
126 |         contents: z
127 |           .array(z.string())
128 |           .describe('An array of observations to delete'),
129 |       })
130 |     ),
131 |   },
132 |   async ({ deletions }) => {
133 |     await knowledgeGraphManager.deleteObservations(deletions);
134 |     return {
135 |       content: [{ type: 'text', text: 'Observations deleted successfully' }],
136 |     };
137 |   }
138 | );
139 | 
140 | // 注册删除关系工具
141 | server.tool(
142 |   'delete_relations',
143 |   'Delete multiple relations from the knowledge graph',
144 |   {
145 |     relations: z
146 |       .array(
147 |         z.object({
148 |           from: z
149 |             .string()
150 |             .describe('The name of the entity where the relation starts'),
151 |           to: z
152 |             .string()
153 |             .describe('The name of the entity where the relation ends'),
154 |           relationType: z.string().describe('The type of the relation'),
155 |         })
156 |       )
157 |       .describe('An array of relations to delete'),
158 |   },
159 |   async ({ relations }) => {
160 |     await knowledgeGraphManager.deleteRelations(relations);
161 |     return {
162 |       content: [{ type: 'text', text: 'Relations deleted successfully' }],
163 |     };
164 |   }
165 | );
166 | 
167 | // 注册搜索节点工具
168 | server.tool(
169 |   'search_nodes',
170 |   'Search for nodes in the knowledge graph based on a query',
171 |   {
172 |     query: z
173 |       .string()
174 |       .describe(
175 |         'The search query to match against entity names, types, and observation content'
176 |       ),
177 |   },
178 |   async ({ query }) => ({
179 |     content: [
180 |       {
181 |         type: 'text',
182 |         text: JSON.stringify(
183 |           await knowledgeGraphManager.searchNodes(query),
184 |           null,
185 |           2
186 |         ),
187 |       },
188 |     ],
189 |   })
190 | );
191 | 
192 | // 注册打开节点工具
193 | server.tool(
194 |   'open_nodes',
195 |   'Open specific nodes in the knowledge graph by their names',
196 |   {
197 |     names: z.array(z.string()).describe('An array of entity names to retrieve'),
198 |   },
199 |   async ({ names }) => ({
200 |     content: [
201 |       {
202 |         type: 'text',
203 |         text: JSON.stringify(
204 |           await knowledgeGraphManager.openNodes(names),
205 |           null,
206 |           2
207 |         ),
208 |       },
209 |     ],
210 |   })
211 | );
212 | 
213 | // 主函数
214 | const main = async () => {
215 |   try {
216 |     // 初始化知识图谱管理器
217 |     await knowledgeGraphManager.initialize();
218 |     
219 |     // 创建传输层
220 |     const transport = new StdioServerTransport();
221 |     
222 |     // 连接服务器
223 |     await server.connect(transport);
224 |     
225 |     // 使用logger代替console.info
226 |     logger.info('Neo4j Knowledge Graph MCP Server running on stdio');
227 |   } catch (error) {
228 |     // 使用logger代替console.error
229 |     logger.error('Failed to start server:', extractError(error));
230 |     process.exit(1);
231 |   }
232 | };
233 | 
234 | // 启动服务器
235 | main().catch((error) => {
236 |   // 使用logger代替console.error
237 |   logger.error('Error during server startup:', extractError(error));
238 |   process.exit(1);
239 | });
```

--------------------------------------------------------------------------------
/src/manager.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import Fuse from 'fuse.js';
  2 | import neo4j, { Driver, Session } from 'neo4j-driver';
  3 | import { ConsoleLogger, Logger } from './logger.js';
  4 | import { Entity, KnowledgeGraph, Observation, ObservationDeletion, Relation } from './types.js';
  5 | import { extractError } from './utils.js';
  6 | 
  7 | /**
  8 |  * Neo4j知识图谱管理器
  9 |  */
 10 | export class Neo4jKnowledgeGraphManager {
 11 |   private driver: Driver | null = null;
 12 |   private fuse: Fuse<Entity>;
 13 |   private initialized = false;
 14 |   private logger: Logger;
 15 |   private uri: string;
 16 |   private user: string;
 17 |   private password: string;
 18 |   private database: string;
 19 | 
 20 |   /**
 21 |    * 构造函数
 22 |    * @param configResolver 配置解析器函数
 23 |    * @param logger 可选的日志记录器
 24 |    */
 25 |   constructor(
 26 |     configResolver: () => { 
 27 |       uri: string; 
 28 |       user: string; 
 29 |       password: string; 
 30 |       database: string;
 31 |     },
 32 |     logger?: Logger
 33 |   ) {
 34 |     const config = configResolver();
 35 |     this.uri = config.uri;
 36 |     this.user = config.user;
 37 |     this.password = config.password;
 38 |     this.database = config.database;
 39 |     this.logger = logger || new ConsoleLogger();
 40 | 
 41 |     this.fuse = new Fuse([], {
 42 |       keys: ['name', 'entityType', 'observations'],
 43 |       includeScore: true,
 44 |       threshold: 0.4, // 搜索严格度(越接近0越严格)
 45 |     });
 46 |   }
 47 | 
 48 |   /**
 49 |    * 获取会话
 50 |    * @returns Neo4j会话
 51 |    */
 52 |   private async getSession(): Promise<Session> {
 53 |     if (!this.driver) {
 54 |       await this.initialize();
 55 |     }
 56 |     return this.driver!.session({ database: this.database });
 57 |   }
 58 | 
 59 |   /**
 60 |    * 初始化数据库
 61 |    */
 62 |   public async initialize(): Promise<void> {
 63 |     if (this.initialized) return;
 64 | 
 65 |     try {
 66 |       if (!this.driver) {
 67 |         this.driver = neo4j.driver(
 68 |           this.uri,
 69 |           neo4j.auth.basic(this.user, this.password),
 70 |           { maxConnectionLifetime: 3 * 60 * 60 * 1000 } // 3小时
 71 |         );
 72 |       }
 73 | 
 74 |       const session = await this.getSession();
 75 |       try {
 76 |         // 创建约束和索引
 77 |         await session.run(`
 78 |           CREATE CONSTRAINT entity_name_unique IF NOT EXISTS
 79 |           FOR (e:Entity) REQUIRE e.name IS UNIQUE
 80 |         `);
 81 | 
 82 |         await session.run(`
 83 |           CREATE INDEX entity_type_index IF NOT EXISTS
 84 |           FOR (e:Entity) ON (e.entityType)
 85 |         `);
 86 | 
 87 |         // 使用新的语法创建全文索引
 88 |         await session.run(`
 89 |           CREATE FULLTEXT INDEX entity_fulltext IF NOT EXISTS
 90 |           FOR (e:Entity)
 91 |           ON EACH [e.name, e.entityType]
 92 |         `);
 93 | 
 94 |         // 加载所有实体到Fuse.js
 95 |         const entities = await this.getAllEntities();
 96 |         this.fuse.setCollection(entities);
 97 |         
 98 |         this.initialized = true;
 99 |       } finally {
100 |         await session.close();
101 |       }
102 |     } catch (error) {
103 |       this.logger.error('Failed to initialize database', extractError(error));
104 |       throw error;
105 |     }
106 |   }
107 | 
108 |   /**
109 |    * 获取所有实体
110 |    * @returns 所有实体数组
111 |    */
112 |   private async getAllEntities(): Promise<Entity[]> {
113 |     const session = await this.getSession();
114 |     try {
115 |       const result = await session.run(`
116 |         MATCH (e:Entity)
117 |         OPTIONAL MATCH (e)-[r:HAS_OBSERVATION]->(o)
118 |         RETURN e.name AS name, e.entityType AS entityType, collect(o.content) AS observations
119 |       `);
120 | 
121 |       const entities: Entity[] = result.records.map(record => {
122 |         return {
123 |           name: record.get('name'),
124 |           entityType: record.get('entityType'),
125 |           observations: record.get('observations').filter(Boolean)
126 |         };
127 |       });
128 | 
129 |       return entities;
130 |     } catch (error) {
131 |       this.logger.error('Error getting all entities', extractError(error));
132 |       return [];
133 |     } finally {
134 |       await session.close();
135 |     }
136 |   }
137 | 
138 |   /**
139 |    * 创建实体
140 |    * @param entities 要创建的实体数组
141 |    * @returns 创建的实体数组
142 |    */
143 |   public async createEntities(entities: Entity[]): Promise<Entity[]> {
144 |     if (entities.length === 0) return [];
145 | 
146 |     const session = await this.getSession();
147 |     try {
148 |       const createdEntities: Entity[] = [];
149 |       const tx = session.beginTransaction();
150 | 
151 |       try {
152 |         // 获取现有实体名称
153 |         const existingEntitiesResult = await tx.run(
154 |           'MATCH (e:Entity) RETURN e.name AS name'
155 |         );
156 |         const existingNames = new Set(
157 |           existingEntitiesResult.records.map(record => record.get('name'))
158 |         );
159 | 
160 |         // 过滤出新实体
161 |         const newEntities = entities.filter(
162 |           entity => !existingNames.has(entity.name)
163 |         );
164 | 
165 |         // 创建新实体
166 |         for (const entity of newEntities) {
167 |           await tx.run(
168 |             `
169 |             CREATE (e:Entity {name: $name, entityType: $entityType})
170 |             WITH e
171 |             UNWIND $observations AS observation
172 |             CREATE (o:Observation {content: observation})
173 |             CREATE (e)-[:HAS_OBSERVATION]->(o)
174 |             RETURN e
175 |             `,
176 |             {
177 |               name: entity.name,
178 |               entityType: entity.entityType,
179 |               observations: entity.observations
180 |             }
181 |           );
182 |           createdEntities.push(entity);
183 |         }
184 | 
185 |         await tx.commit();
186 |         
187 |         // 更新Fuse.js集合
188 |         const allEntities = await this.getAllEntities();
189 |         this.fuse.setCollection(allEntities);
190 |         
191 |         return createdEntities;
192 |       } catch (error) {
193 |         await tx.rollback();
194 |         this.logger.error('Error creating entities', extractError(error));
195 |         throw error;
196 |       }
197 |     } finally {
198 |       await session.close();
199 |     }
200 |   }
201 | 
202 |   /**
203 |    * 创建关系
204 |    * @param relations 要创建的关系数组
205 |    * @returns 创建的关系数组
206 |    */
207 |   public async createRelations(relations: Relation[]): Promise<Relation[]> {
208 |     if (relations.length === 0) return [];
209 | 
210 |     const session = await this.getSession();
211 |     try {
212 |       const tx = session.beginTransaction();
213 |       try {
214 |         // 获取所有实体名称
215 |         const entityNamesResult = await tx.run(
216 |           'MATCH (e:Entity) RETURN e.name AS name'
217 |         );
218 |         const entityNames = new Set(
219 |           entityNamesResult.records.map(record => record.get('name'))
220 |         );
221 | 
222 |         // 过滤出有效关系(源实体和目标实体都存在)
223 |         const validRelations = relations.filter(
224 |           relation => entityNames.has(relation.from) && entityNames.has(relation.to)
225 |         );
226 | 
227 |         // 获取现有关系
228 |         const existingRelationsResult = await tx.run(`
229 |           MATCH (from:Entity)-[r]->(to:Entity)
230 |           RETURN from.name AS fromName, to.name AS toName, type(r) AS relationType
231 |         `);
232 |         
233 |         const existingRelations = existingRelationsResult.records.map(record => {
234 |           return {
235 |             from: record.get('fromName'),
236 |             to: record.get('toName'),
237 |             relationType: record.get('relationType')
238 |           };
239 |         });
240 | 
241 |         // 过滤出新关系
242 |         const newRelations = validRelations.filter(
243 |           newRel => !existingRelations.some(
244 |             existingRel => 
245 |               existingRel.from === newRel.from && 
246 |               existingRel.to === newRel.to && 
247 |               existingRel.relationType === newRel.relationType
248 |           )
249 |         );
250 | 
251 |         // 创建新关系
252 |         for (const relation of newRelations) {
253 |           await tx.run(
254 |             `
255 |             MATCH (from:Entity {name: $fromName})
256 |             MATCH (to:Entity {name: $toName})
257 |             CREATE (from)-[r:${relation.relationType}]->(to)
258 |             RETURN r
259 |             `,
260 |             {
261 |               fromName: relation.from,
262 |               toName: relation.to
263 |             }
264 |           );
265 |         }
266 | 
267 |         await tx.commit();
268 |         return newRelations;
269 |       } catch (error) {
270 |         await tx.rollback();
271 |         this.logger.error('Error creating relations', extractError(error));
272 |         throw error;
273 |       }
274 |     } finally {
275 |       await session.close();
276 |     }
277 |   }
278 | 
279 |   /**
280 |    * 添加观察
281 |    * @param observations 要添加的观察数组
282 |    * @returns 添加的观察数组
283 |    */
284 |   public async addObservations(observations: Observation[]): Promise<Observation[]> {
285 |     if (observations.length === 0) return [];
286 | 
287 |     const session = await this.getSession();
288 |     try {
289 |       const addedObservations: Observation[] = [];
290 |       const tx = session.beginTransaction();
291 | 
292 |       try {
293 |         for (const observation of observations) {
294 |           // 检查实体是否存在
295 |           const entityResult = await tx.run(
296 |             'MATCH (e:Entity {name: $name}) RETURN e',
297 |             { name: observation.entityName }
298 |           );
299 | 
300 |           if (entityResult.records.length > 0) {
301 |             // 获取现有观察
302 |             const existingObservationsResult = await tx.run(
303 |               `
304 |               MATCH (e:Entity {name: $name})-[:HAS_OBSERVATION]->(o:Observation)
305 |               RETURN o.content AS content
306 |               `,
307 |               { name: observation.entityName }
308 |             );
309 | 
310 |             const existingObservations = new Set(
311 |               existingObservationsResult.records.map(record => record.get('content'))
312 |             );
313 | 
314 |             // 过滤出新观察
315 |             const newContents = observation.contents.filter(
316 |               content => !existingObservations.has(content)
317 |             );
318 | 
319 |             if (newContents.length > 0) {
320 |               // 添加新观察
321 |               await tx.run(
322 |                 `
323 |                 MATCH (e:Entity {name: $name})
324 |                 UNWIND $contents AS content
325 |                 CREATE (o:Observation {content: content})
326 |                 CREATE (e)-[:HAS_OBSERVATION]->(o)
327 |                 `,
328 |                 {
329 |                   name: observation.entityName,
330 |                   contents: newContents
331 |                 }
332 |               );
333 | 
334 |               addedObservations.push({
335 |                 entityName: observation.entityName,
336 |                 contents: newContents
337 |               });
338 |             }
339 |           }
340 |         }
341 | 
342 |         await tx.commit();
343 |         
344 |         // 更新Fuse.js集合
345 |         const allEntities = await this.getAllEntities();
346 |         this.fuse.setCollection(allEntities);
347 |         
348 |         return addedObservations;
349 |       } catch (error) {
350 |         await tx.rollback();
351 |         this.logger.error('Error adding observations', extractError(error));
352 |         throw error;
353 |       }
354 |     } finally {
355 |       await session.close();
356 |     }
357 |   }
358 | 
359 |   /**
360 |    * 删除实体
361 |    * @param entityNames 要删除的实体名称数组
362 |    */
363 |   public async deleteEntities(entityNames: string[]): Promise<void> {
364 |     if (entityNames.length === 0) return;
365 | 
366 |     const session = await this.getSession();
367 |     try {
368 |       const tx = session.beginTransaction();
369 |       try {
370 |         // 删除实体及其关联的观察和关系
371 |         await tx.run(
372 |           `
373 |           UNWIND $names AS name
374 |           MATCH (e:Entity {name: name})
375 |           OPTIONAL MATCH (e)-[:HAS_OBSERVATION]->(o:Observation)
376 |           DETACH DELETE e, o
377 |           `,
378 |           { names: entityNames }
379 |         );
380 | 
381 |         await tx.commit();
382 |         
383 |         // 更新Fuse.js集合
384 |         const allEntities = await this.getAllEntities();
385 |         this.fuse.setCollection(allEntities);
386 |       } catch (error) {
387 |         await tx.rollback();
388 |         this.logger.error('Error deleting entities', extractError(error));
389 |         throw error;
390 |       }
391 |     } finally {
392 |       await session.close();
393 |     }
394 |   }
395 | 
396 |   /**
397 |    * 删除观察
398 |    * @param deletions 要删除的观察数组
399 |    */
400 |   public async deleteObservations(deletions: ObservationDeletion[]): Promise<void> {
401 |     if (deletions.length === 0) return;
402 | 
403 |     const session = await this.getSession();
404 |     try {
405 |       const tx = session.beginTransaction();
406 |       try {
407 |         for (const deletion of deletions) {
408 |           if (deletion.contents.length > 0) {
409 |             await tx.run(
410 |               `
411 |               MATCH (e:Entity {name: $name})-[:HAS_OBSERVATION]->(o:Observation)
412 |               WHERE o.content IN $contents
413 |               DETACH DELETE o
414 |               `,
415 |               {
416 |                 name: deletion.entityName,
417 |                 contents: deletion.contents
418 |               }
419 |             );
420 |           }
421 |         }
422 | 
423 |         await tx.commit();
424 |         
425 |         // 更新Fuse.js集合
426 |         const allEntities = await this.getAllEntities();
427 |         this.fuse.setCollection(allEntities);
428 |       } catch (error) {
429 |         await tx.rollback();
430 |         this.logger.error('Error deleting observations', extractError(error));
431 |         throw error;
432 |       }
433 |     } finally {
434 |       await session.close();
435 |     }
436 |   }
437 | 
438 |   /**
439 |    * 删除关系
440 |    * @param relations 要删除的关系数组
441 |    */
442 |   public async deleteRelations(relations: Relation[]): Promise<void> {
443 |     if (relations.length === 0) return;
444 | 
445 |     const session = await this.getSession();
446 |     try {
447 |       const tx = session.beginTransaction();
448 |       try {
449 |         for (const relation of relations) {
450 |           await tx.run(
451 |             `
452 |             MATCH (from:Entity {name: $fromName})-[r:${relation.relationType}]->(to:Entity {name: $toName})
453 |             DELETE r
454 |             `,
455 |             {
456 |               fromName: relation.from,
457 |               toName: relation.to
458 |             }
459 |           );
460 |         }
461 | 
462 |         await tx.commit();
463 |       } catch (error) {
464 |         await tx.rollback();
465 |         this.logger.error('Error deleting relations', extractError(error));
466 |         throw error;
467 |       }
468 |     } finally {
469 |       await session.close();
470 |     }
471 |   }
472 | 
473 |   /**
474 |    * 搜索节点
475 |    * @param query 搜索查询
476 |    * @returns 包含匹配实体和关系的知识图谱
477 |    */
478 |   public async searchNodes(query: string): Promise<KnowledgeGraph> {
479 |     if (!query || query.trim() === '') {
480 |       return { entities: [], relations: [] };
481 |     }
482 | 
483 |     const session = await this.getSession();
484 |     try {
485 |       // 使用Neo4j全文搜索
486 |       const searchResult = await session.run(
487 |         `
488 |         CALL db.index.fulltext.queryNodes("entity_fulltext", $query)
489 |         YIELD node, score
490 |         RETURN node
491 |         `,
492 |         { query }
493 |       );
494 | 
495 |       // 获取所有实体用于Fuse.js搜索
496 |       const allEntities = await this.getAllEntities();
497 |       this.fuse.setCollection(allEntities);
498 |       
499 |       // 使用Fuse.js进行模糊搜索
500 |       const fuseResults = this.fuse.search(query);
501 |       
502 |       // 合并搜索结果
503 |       const uniqueEntities = new Map<string, Entity>();
504 |       
505 |       // 添加Neo4j搜索结果
506 |       for (const record of searchResult.records) {
507 |         const node = record.get('node');
508 |         const name = node.properties.name;
509 |         
510 |         if (!uniqueEntities.has(name)) {
511 |           const entity = allEntities.find(e => e.name === name);
512 |           if (entity) {
513 |             uniqueEntities.set(name, entity);
514 |           }
515 |         }
516 |       }
517 |       
518 |       // 添加Fuse.js搜索结果
519 |       for (const result of fuseResults) {
520 |         if (!uniqueEntities.has(result.item.name)) {
521 |           uniqueEntities.set(result.item.name, result.item);
522 |         }
523 |       }
524 |       
525 |       const entities = Array.from(uniqueEntities.values());
526 |       const entityNames = entities.map(entity => entity.name);
527 |       
528 |       if (entityNames.length === 0) {
529 |         return { entities: [], relations: [] };
530 |       }
531 |       
532 |       // 获取关系
533 |       const relationsResult = await session.run(
534 |         `
535 |         MATCH (from:Entity)-[r]->(to:Entity)
536 |         WHERE from.name IN $names OR to.name IN $names
537 |         RETURN from.name AS fromName, to.name AS toName, type(r) AS relationType
538 |         `,
539 |         { names: entityNames }
540 |       );
541 |       
542 |       const relations: Relation[] = relationsResult.records.map(record => {
543 |         return {
544 |           from: record.get('fromName'),
545 |           to: record.get('toName'),
546 |           relationType: record.get('relationType')
547 |         };
548 |       });
549 |       
550 |       return {
551 |         entities,
552 |         relations
553 |       };
554 |     } catch (error) {
555 |       this.logger.error('Error searching nodes', extractError(error));
556 |       return { entities: [], relations: [] };
557 |     } finally {
558 |       await session.close();
559 |     }
560 |   }
561 | 
562 |   /**
563 |    * 打开节点
564 |    * @param names 要打开的节点名称数组
565 |    * @returns 包含匹配实体和关系的知识图谱
566 |    */
567 |   public async openNodes(names: string[]): Promise<KnowledgeGraph> {
568 |     if (names.length === 0) {
569 |       return { entities: [], relations: [] };
570 |     }
571 | 
572 |     const session = await this.getSession();
573 |     try {
574 |       // 获取实体及其观察
575 |       const entitiesResult = await session.run(
576 |         `
577 |         MATCH (e:Entity)
578 |         WHERE e.name IN $names
579 |         OPTIONAL MATCH (e)-[:HAS_OBSERVATION]->(o:Observation)
580 |         RETURN e.name AS name, e.entityType AS entityType, collect(o.content) AS observations
581 |         `,
582 |         { names }
583 |       );
584 |       
585 |       const entities: Entity[] = entitiesResult.records.map(record => {
586 |         return {
587 |           name: record.get('name'),
588 |           entityType: record.get('entityType'),
589 |           observations: record.get('observations').filter(Boolean)
590 |         };
591 |       });
592 |       
593 |       const entityNames = entities.map(entity => entity.name);
594 |       
595 |       if (entityNames.length > 0) {
596 |         // 获取关系
597 |         const relationsResult = await session.run(
598 |           `
599 |           MATCH (from:Entity)-[r]->(to:Entity)
600 |           WHERE from.name IN $names OR to.name IN $names
601 |           RETURN from.name AS fromName, to.name AS toName, type(r) AS relationType
602 |           `,
603 |           { names: entityNames }
604 |         );
605 |         
606 |         const relations: Relation[] = relationsResult.records.map(record => {
607 |           return {
608 |             from: record.get('fromName'),
609 |             to: record.get('toName'),
610 |             relationType: record.get('relationType')
611 |           };
612 |         });
613 |         
614 |         return {
615 |           entities,
616 |           relations
617 |         };
618 |       } else {
619 |         return {
620 |           entities,
621 |           relations: []
622 |         };
623 |       }
624 |     } catch (error) {
625 |       this.logger.error('Error opening nodes', extractError(error));
626 |       return { entities: [], relations: [] };
627 |     } finally {
628 |       await session.close();
629 |     }
630 |   }
631 | 
632 |   /**
633 |    * 关闭连接
634 |    */
635 |   public async close(): Promise<void> {
636 |     if (this.driver) {
637 |       await this.driver.close();
638 |       this.driver = null;
639 |     }
640 |   }
641 | }
```