Skip to content

Commit 477e8c8

Browse files
authored
fix: 增加Deepwiki 的一次性链接模式 (#28)
* fix: 增加Deepwiki 的一次性链接模式 * fix: 修复 CR 意见 * fix: 修复 CR 意见
1 parent e11bf0f commit 477e8c8

2 files changed

Lines changed: 215 additions & 62 deletions

File tree

__tests__/utils/deepwiki.test.ts

Lines changed: 86 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,91 @@
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).toBeNull();
89+
expect(typeof result.error).toBe('string');
2590
}, 120000);
2691
});

src/utils/deepwiki.ts

Lines changed: 129 additions & 41 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,77 @@ 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+
if (client) {
90+
await client.close();
91+
} else {
92+
await transport?.close();
93+
}
94+
} catch (error) {
95+
logger.error('DeepWiki MCP Client Close Error:', error);
96+
try {
97+
await transport?.close();
98+
} catch (transportError) {
99+
logger.error('DeepWiki MCP Transport Close Error:', transportError);
100+
}
101+
} finally {
102+
if (isShared) {
103+
resetClient();
104+
}
105+
}
106+
}
107+
24108
/**
25109
* 获取或初始化 MCP 客户端 (单例模式)
26110
* 包含防止并发重复连接的锁机制
@@ -40,42 +124,21 @@ async function getMcpClient(): Promise<Client> {
40124

41125
// 开始初始化连接
42126
_connectingPromise = (async () => {
127+
let connection: McpConnection | null = null;
43128
try {
44129
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);
130+
connection = createMcpConnection();
131+
await connectMcpConnection({ ...connection, isShared: true });
70132

71133
// 连接成功,赋值给模块级变量
72-
_client = client;
73-
_transport = transport;
134+
_client = connection.client;
135+
_transport = connection.transport;
74136
logger.info('DeepWiki MCP: 连接成功');
75137

76-
return client;
138+
return connection.client;
77139
} catch (error) {
78140
logger.error('DeepWiki MCP: 连接失败', error);
141+
await closeMcpConnection(connection ?? {});
79142
resetClient();
80143
throw error;
81144
} finally {
@@ -106,22 +169,36 @@ function resetClient() {
106169
export async function queryDeepWiki(_params: {
107170
repoName: string;
108171
question: string;
172+
connectionMode?: DeepWikiConnectionMode;
109173
}): Promise<string> {
174+
let localConnection: McpConnection | null = null;
110175
try {
111-
const params = {
176+
const params: DeepWikiQueryParams = {
112177
..._params,
113178
};
114179
if (!params.repoName.includes('/')) {
115180
params.repoName = getRepoName(params.repoName as AntVLibrary);
116181
}
117182

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

121195
// 调用工具
122196
const result: any = await client.callTool({
123197
name: 'ask_question',
124-
arguments: params,
198+
arguments: {
199+
repoName: params.repoName,
200+
question: params.question,
201+
},
125202
});
126203

127204
// ---------------------------------------------------------
@@ -134,6 +211,10 @@ export async function queryDeepWiki(_params: {
134211
.map((item: any) => item.text)
135212
.join('\n'); // 如果有多段文本,用换行符拼接
136213

214+
if (result.isError) {
215+
throw new Error('DeepWiki Tool Error, Answer = ' + answer);
216+
}
217+
137218
const regex =
138219
/Wiki pages you might want to explore:|View this search on DeepWiki:/i;
139220
const splitIndex = answer.search(regex);
@@ -159,12 +240,18 @@ export async function queryDeepWiki(_params: {
159240
// resetClient();
160241
}
161242
throw error;
243+
} finally {
244+
if (localConnection) {
245+
logger.info('DeepWiki MCP: 关闭一次性连接...');
246+
await closeMcpConnection(localConnection);
247+
}
162248
}
163249
}
164250

165251
export async function adaptedQueryDeepWiki(_params: {
166252
repoName: string;
167253
question: string;
254+
connectionMode?: DeepWikiConnectionMode;
168255
}) {
169256
try {
170257
const answer = await queryDeepWiki(_params);
@@ -182,16 +269,17 @@ export async function adaptedQueryDeepWiki(_params: {
182269
* 供 EggJS 应用在 app.beforeClose 时调用
183270
*/
184271
export async function closeDeepWikiConnection() {
185-
if (_transport) {
272+
if (_connectingPromise) {
273+
await _connectingPromise.catch(() => {});
274+
}
275+
276+
if (_client || _transport) {
186277
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();
278+
await closeMcpConnection({
279+
client: _client,
280+
transport: _transport,
281+
isShared: true,
282+
});
195283
}
196284
}
197285

0 commit comments

Comments
 (0)