Skip to content

Commit 0df7c48

Browse files
committed
fix: 增加Deepwiki 的一次性链接模式
1 parent e11bf0f commit 0df7c48

2 files changed

Lines changed: 197 additions & 59 deletions

File tree

__tests__/utils/deepwiki.test.ts

Lines changed: 85 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,90 @@
1-
import { describe, it, expect } from 'vitest';
2-
import { queryDeepWiki } from '../../src/utils/deepwiki';
1+
import { describe, it, expect, afterAll } from 'vitest';
2+
import {
3+
queryDeepWiki,
4+
adaptedQueryDeepWiki,
5+
closeDeepWikiConnection,
6+
getRepoName,
7+
} from '../../src/utils/deepwiki';
8+
9+
afterAll(async () => {
10+
await closeDeepWikiConnection();
11+
});
12+
13+
describe('getRepoName', () => {
14+
it('should map adc to ant-design/ant-design-charts', () => {
15+
expect(getRepoName('adc')).toBe('ant-design/ant-design-charts');
16+
});
17+
18+
it('should map normal library to antvis/<UPPER>', () => {
19+
expect(getRepoName('g2')).toBe('antvis/G2');
20+
expect(getRepoName('s2')).toBe('antvis/S2');
21+
expect(getRepoName('l7')).toBe('antvis/L7');
22+
});
23+
});
324

425
describe('DeepWiki Integration Test', () => {
526
it('should get real response from DeepWiki for G2 question', async () => {
6-
// Set timeout to 120s as this is a real network request
7-
8-
try {
9-
const result = await queryDeepWiki({
10-
repoName: 'antvis/G2',
11-
question: '如何调整折线图两端的间隔',
12-
});
13-
14-
console.log('DeepWiki Answer:', result);
15-
16-
expect(result).toBeDefined();
17-
expect(typeof result).toBe('string');
18-
expect(result.length).toBeGreaterThan(10);
19-
// Basic check to see if it returned something relevant or at least not an error string
20-
expect(result).not.toMatch(/^Error/);
21-
} catch (error) {
22-
console.error('DeepWiki Query Failed:', error);
23-
throw error;
24-
}
27+
const result = await queryDeepWiki({
28+
repoName: 'antvis/G2',
29+
question: '如何调整折线图两端的间隔',
30+
});
31+
32+
console.log('DeepWiki Answer (keep-alive):', result.slice(0, 200));
33+
34+
expect(result).toBeDefined();
35+
expect(typeof result).toBe('string');
36+
expect(result.length).toBeGreaterThan(10);
37+
expect(result).not.toMatch(/^Error/);
38+
// 确保尾部 wiki 推荐链接已被清理
39+
expect(result).not.toMatch(/Wiki pages you might want to explore/i);
40+
expect(result).not.toMatch(/View this search on DeepWiki/i);
41+
}, 120000);
42+
43+
it('should work with close-after-query mode', async () => {
44+
const result = await queryDeepWiki({
45+
repoName: 'antvis/G2',
46+
question: 'What is G2?',
47+
connectionMode: 'close-after-query',
48+
});
49+
50+
console.log('DeepWiki Answer (close-after-query):', result.slice(0, 200));
51+
52+
expect(result).toBeDefined();
53+
expect(typeof result).toBe('string');
54+
expect(result.length).toBeGreaterThan(10);
55+
}, 120000);
56+
57+
it('should auto-resolve short repoName via getRepoName', async () => {
58+
const result = await queryDeepWiki({
59+
repoName: 'g2',
60+
question: 'What charts does G2 support?',
61+
});
62+
63+
expect(result).toBeDefined();
64+
expect(result.length).toBeGreaterThan(10);
65+
}, 120000);
66+
});
67+
68+
describe('adaptedQueryDeepWiki', () => {
69+
it('should return { documentation } on success', async () => {
70+
const result = await adaptedQueryDeepWiki({
71+
repoName: 'antvis/G2',
72+
question: 'What is G2?',
73+
});
74+
75+
expect(result.documentation).toBeDefined();
76+
expect(typeof result.documentation).toBe('string');
77+
expect(result).not.toHaveProperty('error');
78+
}, 120000);
79+
80+
it('should return { documentation: null, error } on failure', async () => {
81+
const result = await adaptedQueryDeepWiki({
82+
repoName: 'nonexistent/repo-that-does-not-exist-12345',
83+
question: 'anything',
84+
});
85+
86+
// DeepWiki 对不存在的仓库可能返回空或报错
87+
// adaptedQueryDeepWiki 应该把异常吞掉,返回 error 字段
88+
expect(result.documentation === null || typeof result.error === 'string').toBe(true);
2589
}, 120000);
2690
});

src/utils/deepwiki.ts

Lines changed: 112 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@ import EventSource from 'eventsource';
44
import { logger } from './logger';
55
import { AntVLibrary } from '../types';
66

7+
export type DeepWikiConnectionMode = 'keep-alive' | 'close-after-query';
8+
9+
type DeepWikiQueryParams = {
10+
repoName: string;
11+
question: string;
12+
connectionMode?: DeepWikiConnectionMode;
13+
};
14+
15+
type McpConnection = {
16+
client: Client;
17+
transport: StreamableHTTPClientTransport;
18+
};
19+
720
// ---------------------------------------------------------
821
// 1. 全局环境配置
922
// ---------------------------------------------------------
@@ -21,6 +34,73 @@ let _client: Client | null = null;
2134
let _transport: StreamableHTTPClientTransport | null = null;
2235
let _connectingPromise: Promise<Client> | null = null;
2336

37+
function createMcpConnection(): McpConnection {
38+
const transport = new StreamableHTTPClientTransport(
39+
new URL('https://mcp.deepwiki.com/mcp'),
40+
);
41+
42+
const client = new Client(
43+
{
44+
name: 'deepwiki-node-client',
45+
version: '1.0.0',
46+
},
47+
{ capabilities: {} },
48+
);
49+
50+
return { client, transport };
51+
}
52+
53+
async function connectMcpConnection(params: {
54+
client: Client;
55+
transport: StreamableHTTPClientTransport;
56+
isShared?: boolean;
57+
}) {
58+
const { client, transport, isShared = false } = params;
59+
60+
transport.onerror = (err) => {
61+
logger.error('DeepWiki MCP Transport Error:', err);
62+
if (isShared) {
63+
resetClient();
64+
}
65+
};
66+
67+
transport.onclose = () => {
68+
logger.info('DeepWiki MCP Connection Closed');
69+
if (isShared) {
70+
resetClient();
71+
}
72+
};
73+
74+
await client.connect(transport);
75+
}
76+
77+
async function closeMcpConnection(params: {
78+
client?: Client | null;
79+
transport?: StreamableHTTPClientTransport | null;
80+
isShared?: boolean;
81+
}) {
82+
const { client, transport, isShared = false } = params;
83+
84+
if (!client && !transport) {
85+
return;
86+
}
87+
88+
try {
89+
await client?.close();
90+
} catch (error) {
91+
logger.error('DeepWiki MCP Client Close Error:', error);
92+
try {
93+
await transport?.close();
94+
} catch (transportError) {
95+
logger.error('DeepWiki MCP Transport Close Error:', transportError);
96+
}
97+
} finally {
98+
if (isShared) {
99+
resetClient();
100+
}
101+
}
102+
}
103+
24104
/**
25105
* 获取或初始化 MCP 客户端 (单例模式)
26106
* 包含防止并发重复连接的锁机制
@@ -42,31 +122,8 @@ async function getMcpClient(): Promise<Client> {
42122
_connectingPromise = (async () => {
43123
try {
44124
logger.info('DeepWiki MCP: 初始化连接...');
45-
46-
const transport = new StreamableHTTPClientTransport(
47-
new URL('https://mcp.deepwiki.com/mcp'),
48-
);
49-
50-
const client = new Client(
51-
{
52-
name: 'deepwiki-node-client',
53-
version: '1.0.0',
54-
},
55-
{ capabilities: {} },
56-
);
57-
58-
// 错误处理:监听传输层关闭或错误,以便下次重连
59-
transport.onerror = (err) => {
60-
logger.error('DeepWiki MCP Transport Error:', err);
61-
resetClient(); // 出错时重置,下次调用会触发重连
62-
};
63-
64-
transport.onclose = () => {
65-
logger.info('DeepWiki MCP Connection Closed');
66-
resetClient();
67-
};
68-
69-
await client.connect(transport);
125+
const { client, transport } = createMcpConnection();
126+
await connectMcpConnection({ client, transport, isShared: true });
70127

71128
// 连接成功,赋值给模块级变量
72129
_client = client;
@@ -106,22 +163,36 @@ function resetClient() {
106163
export async function queryDeepWiki(_params: {
107164
repoName: string;
108165
question: string;
166+
connectionMode?: DeepWikiConnectionMode;
109167
}): Promise<string> {
168+
let localConnection: McpConnection | null = null;
110169
try {
111-
const params = {
170+
const params: DeepWikiQueryParams = {
112171
..._params,
113172
};
114173
if (!params.repoName.includes('/')) {
115174
params.repoName = getRepoName(params.repoName as AntVLibrary);
116175
}
117176

118-
// 获取单例客户端 (自动处理连接)
119-
const client = await getMcpClient();
177+
let client: Client;
178+
if (params.connectionMode === 'close-after-query') {
179+
logger.info('DeepWiki MCP: 初始化一次性连接...');
180+
localConnection = createMcpConnection();
181+
await connectMcpConnection(localConnection);
182+
client = localConnection.client;
183+
logger.info('DeepWiki MCP: 一次性连接成功');
184+
} else {
185+
// 获取单例客户端 (自动处理连接)
186+
client = await getMcpClient();
187+
}
120188

121189
// 调用工具
122190
const result: any = await client.callTool({
123191
name: 'ask_question',
124-
arguments: params,
192+
arguments: {
193+
repoName: params.repoName,
194+
question: params.question,
195+
},
125196
});
126197

127198
// ---------------------------------------------------------
@@ -159,12 +230,18 @@ export async function queryDeepWiki(_params: {
159230
// resetClient();
160231
}
161232
throw error;
233+
} finally {
234+
if (localConnection) {
235+
logger.info('DeepWiki MCP: 关闭一次性连接...');
236+
await closeMcpConnection(localConnection);
237+
}
162238
}
163239
}
164240

165241
export async function adaptedQueryDeepWiki(_params: {
166242
repoName: string;
167243
question: string;
244+
connectionMode?: DeepWikiConnectionMode;
168245
}) {
169246
try {
170247
const answer = await queryDeepWiki(_params);
@@ -182,16 +259,13 @@ export async function adaptedQueryDeepWiki(_params: {
182259
* 供 EggJS 应用在 app.beforeClose 时调用
183260
*/
184261
export async function closeDeepWikiConnection() {
185-
if (_transport) {
262+
if (_client || _transport) {
186263
logger.info('DeepWiki MCP: 关闭连接...');
187-
// SDK 目前可能没有直接暴露 close 方法,通常关闭 transport 即可
188-
// StreamableHTTPClientTransport 内部并没有显式的 close 方法暴露出来,
189-
// 但我们可以将引用置空,让 GC 回收,或者依赖进程退出
190-
// 如果 SSEClientTransport 实现了 close,则调用:
191-
// await _transport.close();
192-
193-
// 目前 SDK 版本可以直接置空
194-
resetClient();
264+
await closeMcpConnection({
265+
client: _client,
266+
transport: _transport,
267+
isShared: true,
268+
});
195269
}
196270
}
197271

0 commit comments

Comments
 (0)