
Google Maps dành cho cơ sở mã: Dán URL GitHub, Hỏi bất cứ điều gì
Google Maps for Codebases: Paste a GitHub URL, Ask Anything
Lần đầu tiên điều hướng một cơ sở mã lớn thật khó khăn. Bạn sao chép kho lưu trữ, nhận ra có 300 tệp và không biết có thứ gì tồn tại ở đâu. Bạn có thể hỏi trợ lý AI, nhưng nó sẽ làm hỏng bối cảnh...
Việc điều hướng một cơ sở mã lớn lần đầu tiên thật khó khăn. Bạn sao chép kho lưu trữ và nhận ra có 300 tệp và không biết mọi thứ nằm ở đâu.
Bạn có thể hỏi trợ lý AI, nhưng nó sẽ đọc nhanh ngữ cảnh và bạn không bao giờ biết liệu nó có gây ảo giác cho một đường dẫn tệp thậm chí không tồn tại hay không.
Codebase Navigator giải quyết vấn đề này. Dán URL, hỏi bất kỳ điều gì bằng tiếng Anh đơn giản và xem biểu đồ phụ thuộc thực được tạo từ các câu lệnh nhập thực tế xuất hiện trong thời gian thực.
Nó được xây dựng bằng cách sử dụng CopilotKit, Zenflow, GitHub API và React Flow. Bạn có thể chạy ứng dụng này hoàn toàn miễn phí bằng cách sử dụng Ollama cục bộ.
Trong blog này, chúng ta sẽ tìm hiểu cấu trúc, các mẫu chính và cách mọi thứ hoạt động từ đầu đến cuối.
Chúng ta đang xây dựng cái gì?
Codebase Navigator cho phép bạn dán bất kỳ URL kho lưu trữ GitHub công khai nào và đặt câu hỏi về URL đó bằng tiếng Anh đơn giản. Thay vì lấy lại một bức tường văn bản, bạn sẽ có bốn bảng cập nhật cùng một lúc.
| Bảng điều khiển | Công dụng của nó |
|---|---|
| Canvas biểu đồ | Biểu đồ phụ thuộc trực tiếp được tạo từ câu lệnh nhập thực tế |
| Trình xem mã | Nội dung tệp được tìm nạp trực tiếp từ GitHub, các dòng có liên quan được đánh dấu |
| Repo Explorer | Cây tệp đầy đủ, nhấp vào bất kỳ tệp nào để mở tệp đó |
| Trò chuyện | Câu hỏi tiếp theo và giải thích đơn giản bằng tiếng Anh |
Biểu đồ biến đổi để hiển thị mọi tệp có liên quan được kết nối bằng các lần nhập thực, trình xem mã sẽ mở tệp và cuộc trò chuyện giải thích công dụng của từng phần. Không có chuyển đổi ngữ cảnh.
Bạn có thể chạy ứng dụng này hoàn toàn miễn phí bằng cách sử dụng Ollama cục bộ mà không cần bất kỳ khóa API nào.
Đây là toàn bộ yêu cầu → luồng phản hồi về những gì xảy ra khi bạn đặt câu hỏi:
Người dùng gõ "xác thực hoạt động như thế nào?"
↓
CopilotChat → POST /api/copilotkit
↓
LLM nhận được repo bối cảnh (đường dẫn tệp, lựa chọn hiện tại, quy tắc hệ thống)
↓
LLM gọi công cụ analyzeRepository
↓
Công cụ tìm nạp các tệp có liên quan qua /api/github/file
↓
Trích xuất các câu lệnh nhập/yêu cầu → giải quyết các đường dẫn → xây dựng biểu đồ phụ thuộc
↓
Cập nhật cửa hàng Zustand
↓
Tất cả bốn bảng đều hiển thị lại trực tiếp: biểu đồ, cây tệp, trình xem mã, phản hồi trò chuyện
Vào chế độ toàn màn hình Thoát toàn màn hình chế độ
Công cụ ngăn xếp công nghệ &
Ở cấp độ cao, dự án này được xây dựng bằng cách kết hợp:
Next.js 16 - giao diện người dùng và các tuyến API
CopilotKit - giao diện trò chuyện và đồng bộ hóa trạng thái giao diện người dùng-tác nhân. cung cấp các thành phần hook & tích hợp như
useAgentContext,useFrontendTool,CopilotChatZenflow - công cụ quy trình công việc lên kế hoạch, kiểm tra và điều phối quá trình xây dựng
Dòng phản ứng & dagre - phần phụ thuộc tương tác biểu đồ có bố cục tự động
Octokit - Proxy GitHub API để tìm nạp cây repo và nội dung tệp
Zustand - trạng thái được chia sẻ trên tất cả bốn bảng
Tailwind CSS - tạo kiểu
Ollama / OpenAI - LLM cục bộ hoặc đám mây chương trình phụ trợ có thể chuyển đổi từ giao diện người dùng
Thời gian chạy CopilotKit được tự lưu trữ bên trong tuyến API Next.js, cho phép bạn cắm vào bất kỳ chương trình phụ trợ nào tương thích với OpenAI, bao gồm cả Ollama, để suy luận cục bộ hoàn toàn miễn phí.
Nó kết nối giao diện người dùng, nhân viên hỗ trợ và công cụ của bạn thành một vòng tương tác duy nhất.
Zenflow là gì?
Zenflow là một công cụ phát triển AI coi việc xây dựng phần mềm là một quy trình kỹ thuật có cấu trúc thay vì chỉ tự động hoàn thành mã.
Dự án này đã được lên kế hoạch, được xây dựng, thử nghiệm, đánh giá và triển khai bằng Zenflow (công cụ xử lý công việc của Zencoder) trong một phiên duy nhất.
Nó chạy quy trình sáu giai đoạn từ đầu:
- Cấu trúc và thông số kỹ thuật
- Giàn giáo và móng
- Lớp dữ liệu (Tích hợp API GitHub)
- Lớp AI (hành động CopilotKit và bối cảnh)
- Lớp trực quan (Biểu đồ luồng phản ứng)
- Lắp ráp và nối dây lần cuối
Mỗi giai đoạn đều được xác minh bằng kiểm tra tìm lỗi mã nguồn, loại và kiểm tra trước khi chuyển sang giai đoạn tiếp theo. Sau khi xây dựng, nó đã thực hiện đánh giá mã, tìm thấy 14 vấn đề và sửa 11 vấn đề một cách có hệ thống:
- Khóa API hiển thị trong tiêu đề → được chuyển sang cookie httpOnly
- Biểu đồ hình ngôi sao giả → được thay thế bằng độ phân giải phụ thuộc dựa trên nhập thực tế
- Logic tìm nạp tệp trùng lặp → hợp nhất thành một tiện ích được lưu trong bộ nhớ đệm duy nhất
.zenflow/ thư mục trong kho lưu trữ chứa các tệp kế hoạch nhiệm vụ, thông số kỹ thuật và báo cáo mà nó đã tạo trong quá trình thực hiện.
Đó là cách chúng tôi xây dựng nó bằng Zenflow. Đây là kiến trúc được tạo ra từ nó.
Kiến trúc & Cấu trúc dự án
Ứng dụng được chia thành ba lớp: trình duyệt xử lý tất cả logic của công cụ AI và UI, các tuyến API Next.js hoạt động như một proxy an toàn cho các dịch vụ bên ngoài và API GitHub, cùng với phần phụ trợ LLM nằm ở phía dưới cùng. Không có gì trong trình duyệt nói chuyện trực tiếp với GitHub hoặc LLM.
Dưới đây là thông tin tổng quan cấp cao về tất cả các lớp và cách chúng được sắp xếp.
┌───────────────── ──────────────────── ────────────────────┐
│ TRÌNH DUYỆT │
│ │
│ ┌─────────┐ ┌──────────┐ ┌──────────┐ ┌────────┐ │
│ │ Trò chuyện │ │ Đồ thị │ │ Mã │ │ Tệp │ │
│ │ Bảng điều khiển │ │ Canvas │ │ Người xem │ │ Cây │ │
│ └────┬────┘ └────┬─────┘ └────┬─────┘ └───┬────┘ │
│ └────────────┴────── ───────┴────────────┘ │
│ │ │
│ ┌──────▼──────┐ │
│ │ Zustand │ ← trạng thái chia sẻ │
│ └──────┬──────┘ │
│ │ │
│ ┌───────────▼────────────┐ │
│ │ CopilotKit │ │
│ │ công cụ giao diện người dùng │ │
│ │ kho phân tích │ │
│ │ tìm nạp Nội dung tệp │ │
│ │ Mã đánh dấu │ │
│ └───────────┬────────────┘ │
└──────────────────────────┼─ ─────────────────────────────┘
│
┌──────────────────────────▼─ ─────────────────────────────┐
│ CÁC TUYẾN ĐƯỜNG API TIẾP THEO │
│ │
│ /api/copilotkit /api/github/* /api/settings │
│ (thời gian chạy LLM) (proxy) (httpOnly bánh quy)│
└──────────┬────────────────┬ ─────────────────────────────┘
│ │
┌─────▼─────┐ ┌──────▼──────┐
│ Ollama │ │ GitHub API │
│ / OpenAI │ │ (Octokit) │
└───────────┘ └─────────────┘
Vào chế độ toàn màn hình Thoát toàn màn hình chế độ
Dưới đây là chế độ xem đơn giản về cách cấu trúc dự án.
src/
├── ứng dụng/api/
│ ├── copilotkit/route.ts → Điểm cuối thời gian chạy CopilotKit
│ ├── github/
│ │ ├── cây/route.ts → tìm nạp cây repo
│ │ ├── tệp/route.ts → tìm nạp + tệp giải mã base64
│ │ └── tìm kiếm/route.ts → tìm kiếm mã
│ └── settings/route.ts → Cấu hình LLM (httpOnly cookie)
├── thành phần/
│ ├── bảng/ → 5 bảng giao diện người dùng (trò chuyện, biểu đồ, mã, phân tích, kho lưu trữ)
│ └── flow/ → các loại nút React Flow tùy chỉnh
├── hooks/ → Đăng ký công cụ AI, đồng bộ hóa bối cảnh tác nhân, tải repo
├── lib/ → trích xuất nhập, bố cục dagre, ứng dụng khách Octokit
├── store/ → Zustand (AppState + SettingState)
└── .zenflow/ → Kế hoạch nhiệm vụ, thông số kỹ thuật và báo cáo của Zenflow
Vào chế độ toàn màn hình Thoát chế độ toàn màn hình
Hãy tìm hiểu cách mọi thứ hoạt động ở hậu trường. Điều này sẽ giúp bạn hiểu các mẫu chính.
Cách mọi thứ hoạt động dưới mui xe
Bây giờ bạn đã có được bức tranh toàn cảnh, hãy cùng tìm hiểu chi tiết từng phần.
Có rất nhiều điều đang diễn ra bên trong, vì vậy chúng tôi sẽ chia thành 10 phần, bắt đầu từ khi bạn tải repo lần đầu tiên cho đến cách trạng thái diễn ra trên các bảng.
1. Đang tải một kho lưu trữ
Khi bạn dán URL GitHub và nhấp vào Khám phá, useRepository.loadRepository() sẽ kích hoạt (từ hook useRepository.ts).
Nó gọi /api/github/tree trên máy chủ sử dụng Octokit để:
- Phân tích URL kho lưu trữ thành
{ chủ sở hữu, kho lưu trữ - Tìm nạp nhánh mặc định
- Nhận toàn bộ cây tệp đệ quy từ API Cây GitHub
- Chuyển đổi mảng phẳng thành
TreeNodelồng nhau cấu trúc
Đây là tuyến đường src/api/github/tree/route.ts trông như thế nào:
export async function GET(yêu cầu: Yêu cầu tiếp theo) {
const { owner, repo = parseRepoUrl(repoUrl);
const resolvedBranch = nhánh || (đang chờ getDefaultBranch(owner, repo));
const cây = await getRepoTree(owner, repo, chi nhánh đã giải quyết);
trở lại Phản hồi tiếp theo.json({ chủ sở hữu, repo, branch: resolvedBranch, cây });
Vào chế độ toàn màn hình Thoát chế độ toàn màn hình
Cây lồng nhau đó hỗ trợ trình khám phá tệp thanh bên và trở thành điểm khởi đầu cho mọi thứ khác.
Tất cả lệnh gọi GitHub đều được ủy quyền thông qua các tuyến API Next.js. Trình duyệt không bao giờ giao tiếp trực tiếp với GitHub để giữ an toàn cho mã thông báo.
2. Từ khung nhìn kiến trúc đến biểu đồ phụ thuộc
Đây là khoảnh khắc trực quan khiến ứng dụng trở nên sống động. Biểu đồ bạn nhìn thấy khi tải (chế độ xem kiến trúc) và biểu đồ bạn nhìn thấy sau khi đặt câu hỏi (biểu đồ phụ thuộc) là hai thứ hoàn toàn khác nhau, được xây dựng theo hai cách khác nhau.
Cả hai đều tồn tại trong src/lib/analyzer.ts - buildOverviewGraph cho chế độ xem kiến trúc và buildDependencyNodes cho phần phụ thuộc biểu đồ.
Khi kho lưu trữ tải, useRepository.ts gọi buildOverviewGraph ngay sau khi phản hồi của cây quay trở lại và đẩy kết quả trực tiếp tới biểu đồ:
const tổng quan = buildOverviewGraph(data.cây);
setTrực quan hóa(tổng quan.nút, tổng quan.cạnh, "kiến trúc");
Vào chế độ toàn màn hình Thoát chế độ toàn màn hình
buildOverviewGraph không bao giờ tìm nạp bất kỳ tệp nào. Nó chỉ đi theo cấu trúc cây và xây dựng bản đồ thư mục: nút gốc ở trên cùng, các thư mục cấp cao nhất là con, một cấp các thư mục con bên dưới:
xuất chức năng buildOverviewGraph(tree: TreeNode) {
const rootId = `node-${nodeId[[TAG_ 558]]++`;
nút.push({ id: rootId, type: "mô-đun", nhãn: cây.đường dẫn || "gốc" });
const topDirs = (cây.trẻ em || []).filter((c) => c.loại === "thư mục");
cho (const dir of topDirs) {
const fileCount = flattenTree(dir).độ dài;
nút.push({ id, nhãn: `${dir.name (${fileCount)`, loại: categorizeDirType(dir.path) });
cạnh.push({ source: rootId, target: id, loại: "luồng" });
}
Vào chế độ toàn màn hình Thoát chế độ toàn màn hình
Các cạnh ở đây không có ý nghĩa gì ngoài "thư mục này là bên trong thư mục đó."
Sau khi bạn đặt câu hỏi, useCopilotActions.ts sẽ tìm nạp nội dung tệp thực tế và gọi buildDependencyNodes thay vào đó:
const graph = buildDependencyNodes(fileData);
setVisualization(graph.nodes, đồ thị.cạnh, "phụ thuộc");
Vào chế độ toàn màn hình Thoát chế độ toàn màn hình
buildDependencyNodes tạo một nút trên mỗi tệp và một cạnh trên mỗi câu lệnh nhập thực:
xuất hàm buildDependencyNodes(files: { đường dẫn: string; imports: chuỗi[] []) { const nút = tệp.map((tệp) => ({
id: tệp.đường dẫn, loại: "tệp",
nhãn: tệp.đường dẫn.split([[TAG_96 8]]"/").pop() || tệp.đường dẫn, siêu dữ liệu: { fullPath: tệp.đường dẫn },
}));
cho (const tệp của tệp) {
cho (const imp of tệp.imports) {
const đã giải quyết = resolveImportPath(imp, tệp.đường dẫn, filePathSet);
nếu (đã giải quyết) {
cạnh.đẩy({ nguồn: tệp.đường dẫn, mục tiêu: đã giải quyết, loại: "import" });
return { nút, cạnh };
Vào chế độ toàn màn hình Thoát chế độ toàn màn hình
Cùng lệnh gọi setVisualization, dữ liệu khác nhau. React Flow hiển thị lại và biểu đồ biến đổi từ bản đồ thư mục thành biểu đồ phụ thuộc thực sự tập trung vào chính xác các tệp có liên quan đến câu hỏi của bạn.
3. Thời gian chạy CopilotKit
Tuyến đường /api/copilotkit là nơi các yêu cầu LLM thực sự đến. Khóa API và cấu hình nhà cung cấp được lưu trữ trong cookie httpOnly, nghĩa là chúng tồn tại trên máy chủ và không bao giờ được gửi tới trình duyệt.
Tuyến đường đọc cấu hình đó, tạo ứng dụng khách tương thích với OpenAI và chuyển yêu cầu tới thời gian chạy CopilotKit:
export const POST = không đồng bộ (req: Yêu cầu tiếp theo) => {
const { baseURL, apiKey, model = await getLLMConfig();
const openai = mới OpenAI({ baseURL, apiKey }); const serviceAdapter = mới OpenAIAdapter({ openai, model });
const runtime = mới Thời gian phi công phụ();
const { handleRequest = copilotRuntimeNextJSAppRouterEndpoint({
thời gian chạy,
serviceAdapter, điểm cuối: "/api/copilotkit",
});
trở lại handleRequest(req);
};
Nhập toàn màn hình chế độ Thoát chế độ toàn màn hình
Các giá trị mặc định trỏ tới Ollama trên localhost:11434/v1 với qwen2.5.
Vì OpenAI SDK chấp nhận baseURL tùy chỉnh nên việc chuyển đổi sang OpenAI, Groq hoặc bất kỳ nhà cung cấp nào khác chỉ là thay đổi cấu hình mà không cần thay đổi mã.
4. Cung cấp bối cảnh LLM
Trước khi LLM có thể trả lời bất kỳ điều gì hữu ích, LLM cần biết kho lưu trữ nào được tải và tệp nào tồn tại. Đó là công việc của móc useCopilotContext.ts.
Nó gọi useAgentContext, một móc CopilotKit gắn dữ liệu có cấu trúc vào mọi thư bạn gửi. Bạn có thể gọi nó nhiều lần để đính kèm các phần ngữ cảnh khác nhau.
Ở đây, nó được gọi hai lần: một lần cho danh sách tệp và một lần cho hướng dẫn hệ thống cho LLM biết cách hoạt động:
useAgentContext({
mô tả: "Tệp đường dẫn trong kho lưu trữ (tối đa 500), một đường dẫn trên mỗi dòng",
value: fileList, // flattened from the TreeNode structure
});
useAgentContext({
mô tả: "Hướng dẫn hệ thống",
giá trị: `Bạn là trợ lý Điều hướng Codebase. Bạn PHẢI sử dụng các cuộc gọi công cụ để trả lời các câu hỏi.
QUY TẮC QUAN TRỌNG:
1. Đối với BẤT KỲ câu hỏi nào về kho lưu trữ, hãy gọi công cụ "analyzeRepository".
2. Để hiển thị một tệp, hãy gọi "fetchFileContent" với đường dẫn tệp chính xác.
3. KHÔNG BAO GIỜ trả lời chỉ bằng văn bản. LUÔN LUÔN gọi một công cụ trước.
4. CHỈ sử dụng đường dẫn tệp từ danh sách tệp ở trên.`
});
Vào chế độ toàn màn hình Thoát chế độ toàn màn hình
Danh sách tệp được giới hạn ở 500 đường dẫn để luôn nằm trong giới hạn mã thông báo.
Vì hook này chạy lại mỗi khi cửa hàng Zustand thay đổi nên LLM luôn nhìn thấy kho lưu trữ hiện tại, tệp đã chọn và trạng thái phân tích thay vì ảnh chụp nhanh cũ từ khi trang được tải.
Lưu ý: Nếu bạn đang chạy mô hình cục bộ có cửa sổ ngữ cảnh lớn, bạn có thể tăng giới hạn này trong useCopilotContext.ts.
5. Bốn công cụ giao diện người dùng
Đây là cốt lõi về cách hoạt động của ứng dụng. Thay vì chỉ trả lời bằng văn bản, LLM có thể gọi tools. Mỗi công cụ có một tên, một tập hợp các tham số mà nó chấp nhận và một trình xử lý, đây chỉ là một hàm chạy khi LLM gọi nó.
CopilotKit cho phép bạn đăng ký các công cụ này trực tiếp trong trình duyệt bằng cách sử dụng hook useFrontendTool, được xác định trong useCopilotActions.ts.
Khi trình xử lý của công cụ chạy, nó sẽ cập nhật trực tiếp trạng thái Zustand, đó là lý do tại sao cả bốn bảng, biểu đồ, trình xem mã, bảng phân tích và trò chuyện đều phản ứng cùng lúc mà không cần nối thêm bất kỳ dây nào.
analyzeRepository là công cụ chính mà LLM sử dụng cho hầu hết mọi câu hỏi. Đây là cách nó thực hiện từng bước:
sử dụngFrontendTool({
tên: "analyzeRepository",
thông số: z.đối tượng({
truy vấn: z.chuỗi(),
giải thích: z.chuỗi(), }),
trình xử lý: không đồng bộ ({ truy vấn, giải thích }) => {
// 1. Tìm các tệp có liên quan bằng cách sử dụng đối sánh từ khóa
const matchedPaths = findFilesByQuery(repo[[TA G_1549]].cây, truy vấn);
// 2. Giới hạn ở 15 tệp để duy trì tốc độ nhanh
const capped = matchedPaths.slice[[TA G_1573]](0, 15);
// 3. Tìm nạp từng nội dung tệp (được lưu trong bộ nhớ đệm)
const fileData = await Lời hứa.tất cả(
đã giới hạn.map(không đồng bộ (p) => ({
đường dẫn: p,
imports: extractImports(await tìm nạpTệp(chủ sở hữu, repo, p, nhánh)),
}))
);
// 4. Xây dựng biểu đồ phụ thuộc thực từ các câu lệnh nhập thực tế
const graph = buildDependencyNodes(fileData);
// 5. Phân loại từng nút theo loại tệp
đồ thị.nút. forEach(nút => {
nút.loại = categorizeFileType(nút.[[TAG_171 5]]siêu dữ liệu.fullPath);
});
// 6. Đẩy tới Zustand - cả bốn bảng đều phản ứng
setAnalysisResult({ explanation, có liên quanTệp, sơ đồ luồng: biểu đồ }); setTrực quan hóa(graph[[TA G_1753]].nút, đồ thị.cạnh, "phụ thuộc"); },
});
Vào chế độ toàn màn hình Thoát chế độ toàn màn hình
fetchFileContent mở một tệp cụ thể trong trình xem mã. LLM gọi điều này khi bạn yêu cầu nó hiển thị cho bạn một tệp, nó chỉ tìm nạp nội dung và gọi setCodeViewer:
sử dụngFrontendTool({
tên: "fetchFileContent", thông số: z.đối tượng({
đường dẫn tệp: z.chuỗi(),
}),
trình xử lý: không đồng bộ ({ filePath }) => {
const nội dung = await fetchFile(repo. repoInfo.owner, repo.repoInfo[[TAG_18 84]].repo, đường dẫn tệp, repo.repoInfo[[TAG_1900 ]].chi nhánh);
setCodeViewer(filePath, nội dung); },
});
Vào chế độ toàn màn hình Thoát chế độ toàn màn hình
generateFlowDiagram tương tự như analyzeRepository nhưng tập trung hơn. Thay vì tìm kiếm toàn bộ kho lưu trữ, bạn cung cấp cho nó một danh sách cụ thể các đường dẫn tệp và nó sẽ tạo biểu đồ cho các tệp đó. Hữu ích khi bạn muốn trực quan hóa một phần của cơ sở mã giống như lớp API:
sử dụngFrontendTool({
tên: "tạo Sơ đồ dòng chảy",
thông số: z.object({
tệp: z.mảng([[ TAG_1980]]z.chuỗi()),
loại sơ đồ: z.enum([[[ TAG_2000]]"phụ thuộc", "luồng", "kiến trúc"]), }),
trình xử lý: không đồng bộ ({ tệp, diagramType }) => {
const fileData = await Lời hứa.tất cả(tệp[[TAG_20 63]].lát(0, 20).map(không đồng bộ (f) => ({
đường dẫn: f,
imports: extractImports(await fetchFile(repo. repoInfo.owner, repo.repoInfo[[TAG_21 33]].repo, f, repo.repoInfo[[TAG_2149 ]].chi nhánh)),
})));
const graph = buildDependencyNodes(fileData); setTrực quan hóa(graph[[TA G_2177]].nút, đồ thị.cạnh, loại sơ đồ); },
});
Vào chế độ toàn màn hình Thoát chế độ toàn màn hình
highlightCode tìm nạp tệp và đánh dấu các số dòng cụ thể kèm theo lời giải thích. LLM sử dụng điều này khi muốn chỉ ra chính xác vị trí xảy ra điều gì đó trong mã:
sử dụngFrontendTool({
tên: "highlightCode",
tham số: z.object({
đường dẫn tệp: z.chuỗi(),
dòng: z.mảng([[ TAG_2267]]z.số()),
giải thích: z.chuỗi(),
}), trình xử lý: không đồng bộ ({ filePath, dòng, giải thích }) => {
const nội dung = đang chờ fetchFile(repo. repoInfo.owner, repo.repoInfo[[TAG_23 42]].repo, đường dẫn tệp, repo.repoInfo[[TAG_2358 ]].chi nhánh);
setCodeViewer(filePath, nội dung, dòng, giải thích);
},
});
Vào chế độ toàn màn hình Thoát chế độ toàn màn hình
Lưu ý rằng không có công cụ nào trong số này trả lại văn bản cho cuộc trò chuyện. Tất cả đều chỉ cập nhật trạng thái. LLM không mô tả những gì nó tìm thấy mà trực tiếp kiểm soát những gì bạn nhìn thấy trên màn hình.
Ví dụ: tôi đã hỏi về cấu trúc của gói v2.
6. Tìm các tập tin có liên quan
Khi analyzeRepository nhận được truy vấn như "xác thực hoạt động như thế nào?", nó cần tìm ra tệp nào cần xem xét thực sự. Việc đó được xử lý bởi findFilesByQuery trong src/lib/analyzer.ts.
Nó kiểm tra xem truy vấn có khớp với một trong sáu danh mục được xác định trước hay không, mỗi danh mục có một tập hợp từ khóa và mẫu đường dẫn tệp:
const ANALYSIS_CATEGORIES = [
{
tên: "xác thực",
từ khóa: ["auth", "đăng nhập", "đăng xuất", "mã thông báo", "jwt", "phiên", "oauth"],
mẫu tệp: [/auth/i, /đăng nhập/i, /phiên/i, /oauth/i, /jwt/i],
},
{
tên: "cơ sở dữ liệu", từ khóa: ["cơ sở dữ liệu", "db", "kiểu máy", "lược đồ", "prisma", "mongoose"],
Mẫu tệp: [/model/i, /schema/i, /migration/i, /prisma/i, /db/i],
},
// ... api, cấu hình, thử nghiệm, tạo kiểu
]
Vào chế độ toàn màn hình Thoát chế độ toàn màn hình
Nếu truy vấn khớp với một danh mục thì truy vấn sẽ lọc tất cả đường dẫn tệp bằng cách sử dụng mẫu biểu thức chính quy của danh mục đó.
Nếu không có gì khớp, nó sẽ quay lại việc tách truy vấn thành các cụm từ riêng lẻ và kiểm tra xem có đường dẫn tệp nào chứa chúng hay không. Vì vậy, ngay cả một truy vấn mơ hồ như "cấu hình ở đâu" vẫn sẽ tìm thấy các tệp có "config" trong tên.
Điều này có chủ ý đơn giản. Không nhúng, không tìm kiếm vectơ, chỉ có biểu thức chính quy trên tên tệp. Cách này hoạt động tốt vì tên tệp trong các dự án có cấu trúc tốt thường mang tính mô tả đủ để việc khớp với chúng sẽ giúp bạn có được tệp phù hợp.
7. Bộ nhớ đệm tệp
Mỗi khi một công cụ tìm nạp một tệp, nó sẽ đi qua src/lib/fetch-file.ts duy trì bộ nhớ đệm trong bộ nhớ với TTL 5 phút:
const cache = mới Bản đồ<chuỗi, { nội dung: chuỗi; dấu thời gian: số >();
const CACHE_TTL = 5 * 60 * 1000; // 5 phút
xuất không đồng bộ chức năng fetchFile(owner, repo, đường dẫn, tham chiếu) {
const khóa = `${chủ sở hữu[[TAG_2 764]]/${repo/[[T AG_2773]]${ref/ ${đường dẫn`[[TAG_279 0]];
const được lưu vào bộ nhớ đệm = bộ nhớ đệm.nhận[[TAG_2803 ]](chìa khóa);
if (được lưu vào bộ nhớ đệm && Ngày.bây giờ() - được lưu vào bộ nhớ đệm.dấu thời gian < CACHE_TTL) {
trở lại được lưu vào bộ nhớ đệm.nội dung; // phân phát từ bộ nhớ đệm
const res = await tìm nạp(`/api/github/file?r epo=...&path=...&ref=...`);
const data = await độ phân giải.json(); bộ nhớ đệm.đặt[[TAG_2893 ]](chìa khóa, { nội dung: dữ liệu.nội dung, dấu thời gian: Ngày.bây giờ() });
// Trục xuất LRU: nếu trên 200 mục, bỏ 50 mục cũ nhất
if (cache.size > 200) {
const cũ nhất = [...bộ nhớ đệm.mục nhập[[TAG_296 2]]()].sắp xếp((a, b) => a[1].dấu thời gian - b[1[[TAG _2996]]].dấu thời gian);
dành cho (cho phép i = 0; i < 50; i++) bộ nhớ đệm.xóa(cũ nhất[[TAG_ 3039]][i][0]);
trở lại dữ liệu.nội dung;
Vào chế độ toàn màn hình Thoát chế độ toàn màn hình
Điều này quan trọng vì analyzeRepository tìm nạp tối đa 15 tệp cùng một lúc. Một câu hỏi tiếp theo về cùng một tệp sẽ được lưu vào bộ đệm thay vì thực hiện thêm 15 yêu cầu API.
Nếu không có mã thông báo GitHub, bạn bị giới hạn ở 60 yêu cầu mỗi giờ, do đó, bộ nhớ đệm giúp các câu hỏi lặp lại nhanh chóng và miễn phí.
Bộ nhớ đệm cũng xóa các mục nhập cũ nhất sau khi vượt quá 200 mục, do đó, bộ nhớ đệm không bao giờ phát triển không giới hạn trong các phiên dài.
8. Phân tích cú pháp nhập và giải quyết đường dẫn
Sau khi analyzeRepository có đường dẫn tệp liên quan, nó sẽ tìm nạp từng tệp và phân tích cú pháp tệp đó để tìm câu lệnh nhập. Cả hai hàm xử lý vấn đề này đều có trong src/lib/analyzer.ts.
extractImports chạy trên nội dung tệp thô và rút ra mọi lần nhập, xử lý cả cú pháp ES6 và CommonJS:
xuất chức năng extractImports(content: chuỗi): chuỗi[ {
const imports: string[] = [];
// ES6: nhập X từ 'y' và nhập 'y'
const esImports = nội dung.matchAll( /(?:import\s[[TA G_3159]]+.*?\s+from\s[[TAG_31 67]]+['"](.+[[TA G_3176]]?)['"]|nhập[[TAG_31 84]]\s+['"]([[TAG_3194] .+?)['"])/g
);
cho (const match of esNhập khẩu) nhập.đẩy([[TAG_322 8]]trùng khớp[1] || match[2]);
// CommonJS: require('y')
const yêu cầu = nội dung.matchAll[[TAG_3 260]](/require\s[[TAG_32 66]]*\(\s*[ [TAG_3273]]['"]([[TAG_32 79]].+?)['"]\s[[ TAG_3287]]*\)/g);
cho (const match of yêu cầu) nhập.đẩy([[TAG_3316 ]]trùng khớp[1]);
trả lại nhập khẩu;
Vào chế độ toàn màn hình Thoát toàn màn hình chế độ
Nhưng các chuỗi nhập thô như "./utils" hoặc "@/lib/github" chưa phải là đường dẫn tệp.
buildDependencyNodes gọi resolveImportPath trên mỗi đường dẫn để biến chúng thành các đường dẫn thực tế tồn tại trong kho lưu trữ:
chức năng resolveImportPath(importPath, fromFile, filePathSet) {
// Bỏ qua các gói npm (không ./ hoặc @/)
nếu (!importPath.bắt đầuW thứ (".") && !importPath.bắt đầu với[[TA G_3410]]("@/")) trả về null;
// Giải quyết @/ bí danh thành src/
nếu (importPath.bắt đầuVới[[TAG_3437 ]]("@/")) {
đã giải quyết = "src/" + importPath.slice[[TAG_ 3467]](2);
// Hãy thử với các tiện ích mở rộng phổ biến: .ts, .tsx, .js, /index.ts, v.v.
const tiện ích mở rộng = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx"];
cho (const ext trong số tiện ích mở rộng) {
nếu (filePathSet.[[TAG _3561]]đã(đã giải quyết + ext)) return đã giải quyết + ext;
trả về null;
Vào chế độ toàn màn hình Thoát chế độ toàn màn hình
gói npm như react hoặc zod bị bỏ qua ngay lập tức vì chúng không bắt đầu bằng ./ hoặc @/. Đối với nội dung nhập cục bộ, nó thử các tiện ích mở rộng phổ biến như .ts, .tsx và /index.ts, do đó, nó xử lý cả nhập tệp trực tiếp và thư mục xuất thông qua tệp chỉ mục.
Nếu đường dẫn đã giải quyết không tồn tại trong kho lưu trữ thì đường dẫn đó sẽ trả về giá trị rỗng và không có cạnh nào được tạo. Đó là yếu tố giữ cho biểu đồ luôn trung thực.
9. Bố cục đồ thị với Dagre
Sau khi các nút và cạnh được tạo, chúng cần có vị trí thực tế trên màn hình trước khi React Flow có thể hiển thị chúng. Đó là những gì applyDagreLayout thực hiện trong src/lib/graph-layout.ts.
Nó sử dụng Dagre, một thư viện JavaScript tự động tính toán tọa độ x và y cho mọi nút trong biểu đồ có hướng để không có gì trùng lặp, tất cả đều ở phía máy khách mà không cần máy chủ:
export function áp dụngDagreLayout(nút, cạnh, options = {}) {
const g = mới dagre.graphlib[[TAG_367 6]].Biểu đồ();
g.setGraph({ rankdir: "TB", xếp hạng: 80, nodesep: 40 });
// Cho biết kích thước của mỗi nút
dành cho (const nút của nút) {
g.setNode([[TAG_3744] ]nút.id, { chiều rộng: 200, chiều cao: 60 });
cho (const edge of cạnh) { g.setEdge([ [TAG_3797]]cạnh.nguồn, cạnh.target);
dagre.bố cục[[TAG_38 20]](g); // dagre tính x,y cho mọi nút
trở lại nút.bản đồ[[TAG_3836] ((nút) => {
const pos = g.nút([ [TAG_3861]]nút.id);
trở lại { ...nút, vị trí: { x: pos.x - 100, y: pos.y - 30 };
});
Vào chế độ toàn màn hình Thoát chế độ toàn màn hình
Có thể chuyển đổi hướng từ trên xuống dưới (TB) và từ trái sang phải (LR) từ giao diện người dùng.
Mỗi loại nút ánh xạ tới thành phần Dòng phản ứng dựa trên loại tệp categorizeFileType được chỉ định trước đó:
-
mô-đun/dịch vụ→ModuleNode(màu chàm) -
chức năng→FunctionNode(màu xanh mòng két) -
tệp→FileNode(xám)
10. Quản lý nhà nước với Zustand
Tất cả bốn bảng vẫn đồng bộ hóa vì tất cả đều đọc từ cùng một cửa hàng Zustand trong src/store/index.ts. Cửa hàng có bốn phần, mỗi phần dành cho mỗi mối quan tâm chính:
giao diện AppState {
repo: {
repoInfo: RepoInfo | null; // chủ sở hữu, kho lưu trữ, chi nhánh
cây: TreeNode | null; // cây tệp đầy đủ
đã chọnTệp: chuỗi | null; // đường dẫn tệp đang hoạt động
đang tải: boolean;
lỗi: chuỗi | null;
}; phân tích: {
kết quả: AnalysisResult | null; // giải thích + tệp liên quan + biểu đồ
đang tải: boolean; lỗi: chuỗi | null;
};
hình ảnh hóa: {
nút: FlowNode[]; cạnh: FlowEdge[];
graphType: "dependency" | "luồng" | "kiến trúc" | null;
};
codeViewer: {
filePath: string | null;
nội dung: chuỗi | null;
dòng được đánh dấu: số[];
giải thích: chuỗi | null;
};
Vào chế độ toàn màn hình Thoát chế độ toàn màn hình
Mỗi bảng chỉ đăng ký phần mà nó cần bằng cách sử dụng bộ chọn như useAppStore((s) => s.visualization).
Vì vậy, khi một công cụ gọi setVisualization, chỉ bảng biểu đồ hiển thị lại. Khi setCodeViewer kích hoạt, chỉ trình xem mã mới cập nhật. Không có gì khác chuyển động.
Các cài đặt như nhà cung cấp LLM, khóa API và mô hình nằm trong một cửa hàng SettingsState riêng biệt. Nó đồng bộ hóa ở hai nơi:
-
localStorageđể trình duyệt ghi nhớ cấu hình của bạn trong các lần tải lại - một cookie httpOnly để tuyến
/api/copilotkitphía máy chủ có thể đọc chúng mà không cần khóa API xuất hiện trong tiêu đề yêu cầu
Đây là cách mọi thứ hoạt động ở hậu trường. Đã đến lúc chạy nó cục bộ.
Chạy nó cục bộ
Sao chép kho lưu trữ GitHub và cài đặt các phần phụ thuộc.
git clone <repo-url>
cd codebase-navigator
npm cài đặt
npm chạy nhà phát triển
Vào chế độ toàn màn hình Thoát chế độ toàn màn hình
Mở http://localhost:3000 để xem trực tiếp trên trình duyệt. Dưới đây là cách thiết lập từng nhà cung cấp.
Sử dụng OpenAI
Bạn sẽ cần Khóa API OpenAI. Sau đó, chỉ cần nhấp vào biểu tượng Cài đặt ở trên cùng bên phải, chuyển nhà cung cấp sang OpenAI, dán khóa API của bạn và chọn một mô hình như gpt-5.2. Thế là xong.
Sử dụng Ollama (miễn phí, chạy cục bộ)
Bạn có thể tải xuống từ ollama.com hoặc cài đặt nó thông qua các lệnh này dựa trên hệ điều hành bạn sử dụng.
brew cài đặt ollama #macOS
cuộn tròn -fsSL https://ollama.com/install.sh | sh #linux
Vào chế độ toàn màn hình Thoát chế độ toàn màn hình
Sau đó, kéo mô hình và khởi động máy chủ:
ollama giao bóng
ollama pull qwen2.5 # trong một thiết bị đầu cuối khác
Vào chế độ toàn màn hình Thoát chế độ toàn màn hình
Ứng dụng trỏ đến Ollama theo mặc định nên không cần thay đổi gì khác. Chỉ cần mở http://localhost:3000 và nó hoạt động.
Đối với GitHub, các kho lưu trữ công khai hoạt động mà không cần mã thông báo (điều hấp dẫn duy nhất là nó bị giới hạn ở 60 yêu cầu mỗi giờ). Thêm GITHUB_TOKEN vào tệp .env.local để có giới hạn cao hơn:
GITHUB_TOKEN=your_token_here
Vào chế độ toàn màn hình Thoát chế độ toàn màn hình
Tôi đã thử với CopilotKit GitHub repo và nó ngay lập tức vạch ra toàn bộ cấu trúc repo.
Tôi đã hỏi nó "cho tôi biết về kiến trúc" và nó trả lời cho tôi:
- biểu đồ của mọi tệp có liên quan được kết nối bằng các lần nhập thực tế
- bản phân tích đơn giản bằng tiếng Anh về chức năng của từng tệp
- mã thô được tìm nạp trực tiếp từ GitHub
- câu trả lời trò chuyện chi tiết mà tôi có thể tiếp tục theo dõi
Tôi đã thử những câu hỏi khó và mơ hồ và nó đã giải quyết được tất cả.
các bức tranh lớn hơn
Dự án này bắt đầu bằng việc kết hợp ba ý tưởng lại với nhau và xem điều gì đã xảy ra.
GitHub là nguồn dữ liệu, không chỉ là máy chủ lưu trữ. Mọi tệp tham chiếu AI đều được tìm nạp trực tiếp, phân tích cú pháp và xử lý ngay tại chỗ. Không ảo giác, không đoán theo trí nhớ.
CopilotKit thay đổi những gì AI thực sự có thể thực hiện trong trình duyệt. Thay vì trả lời bằng văn bản, nó gọi các công cụ cập nhật trạng thái trực tiếp. Biểu đồ thay đổi. Trình xem mã sẽ mở ra. AI không mô tả những gì nó tìm thấy mà đang hiển thị cho bạn.
Các cạnh phụ thuộc là có thật. Rất nhiều công cụ vẽ đồ thị trông ấn tượng nhưng chẳng có ý nghĩa gì. Mọi cạnh ở đây tồn tại vì một tệp có câu lệnh nhập thực sự trỏ đến một tệp khác. Vậy đó.
Kết hợp ba thứ đó lại với nhau và bạn sẽ có được thứ gì đó thực sự hiểu được cơ sở mã thay vì chỉ tìm kiếm qua nó.
Có rất nhiều thứ bạn có thể mở rộng từ đây: khám phá sự khác biệt PR, so sánh nhiều kho lưu trữ, chế độ kiểm tra bảo mật. Nhưng là điểm khởi đầu để hiểu bất kỳ cơ sở mã nào, nó đã thực hiện chính xác những gì nó cần.
Hãy cho tôi biết suy nghĩ của bạn trong phần nhận xét!
Bạn có thể kết nối với tôi trên GitHub, Twitter và LinkedIn.
Theo dõi CopilotKit trên Twitter và gửi lời chào, đồng thời nếu bạn muốn xây dựng thứ gì đó thú vị, hãy tham gia cộng đồng Discord.
Tác giả: Anmol Baranwal



















