Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/OpenDeepWiki/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,7 @@
app.MapChatAppEndpoints();
app.MapEmbedEndpoints();

app.MapGet("/health", () => Results.Ok(new { status = "healthy", timestamp = DateTime.UtcNow }));
app.MapSystemEndpoints();
app.MapIncrementalUpdateEndpoints();
app.MapMcpProviderEndpoints();
Expand Down
11 changes: 7 additions & 4 deletions src/OpenDeepWiki/Services/Admin/AdminRepositoryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,8 @@ public async Task<bool> DeleteRepositoryAsync(string id)

if (repo == null) return false;

await ClearRepositoryReferencesAsync([repo.Id]);
_context.Repositories.Remove(repo);
await DeleteRepositoryDataAsync([repo.Id]);
repo.MarkAsDeleted();
await _context.SaveChangesAsync();
return true;
}
Expand Down Expand Up @@ -297,8 +297,11 @@ public async Task<BatchDeleteResult> BatchDeleteRepositoriesAsync(string[] ids)

if (repos.Count > 0)
{
await ClearRepositoryReferencesAsync(repos.Select(r => r.Id).ToArray());
_context.Repositories.RemoveRange(repos);
await DeleteRepositoryDataAsync(repos.Select(r => r.Id).ToArray());
foreach (var repo in repos)
{
repo.MarkAsDeleted();
}
result.SuccessCount = repos.Count;
}

Expand Down
8 changes: 8 additions & 0 deletions src/OpenDeepWiki/Services/Admin/SystemSettingDefaults.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ public static async Task InitializeDefaultsAsync(IConfiguration configuration, I
existing.Description = description;
hasChanges = true;
}

// Sync existing setting value with current environment variable
var envValue = GetEnvironmentOrConfigurationValue(configuration, key);
if (!string.IsNullOrWhiteSpace(envValue) && existing.Value != envValue)
{
existing.Value = envValue;
hasChanges = true;
}
}
else
{
Expand Down
24 changes: 9 additions & 15 deletions src/OpenDeepWiki/Services/Repositories/RepositoryDocsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,20 +131,7 @@ public async Task<RepositoryTreeResponse> GetTreeAsync(string owner, string repo
};
}

// 仓库处理失败
if (repository.Status == RepositoryStatus.Failed)
{
return new RepositoryTreeResponse
{
Owner = repository.OrgName,
Repo = repository.RepoName,
Exists = true,
Status = repository.Status,
Nodes = []
};
}

// 仓库处理完成,获取文档目录
// 仓库处理完成或失败,获取文档目录
var branchEntity = await GetBranchAsync(repository.Id, branch);
var language = await GetLanguageAsync(branchEntity.Id, lang);

Expand Down Expand Up @@ -364,7 +351,7 @@ private async Task<BranchLanguage> GetDefaultLanguageAsync(string branchId)

private static string NormalizePath(string path)
{
return path.Trim().Trim('/');
return System.Net.WebUtility.UrlDecode(path.Trim().Trim('/'));
}

/// <summary>
Expand Down Expand Up @@ -587,6 +574,13 @@ private static async Task AddFilesToArchive(
var children = catalogs.Where(c => c.ParentId == catalog.Id).ToList();
if (children.Count > 0)
{
// 如果没有文档文件但有子项,创建 ZIP 目录条目
if (catalog.DocFile == null)
{
var dirPath = CombineZipPath(parentZipPath, itemName);
archive.CreateEntry(dirPath.EndsWith('/') ? dirPath : dirPath + '/');
}

var nextParentZipPath = CombineZipPath(parentZipPath, itemName);
await AddFilesToArchive(archive, catalogs, catalog.Id, nextParentZipPath);
}
Expand Down
12 changes: 6 additions & 6 deletions web/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# 依赖安装阶段
FROM node:20-alpine AS deps
FROM node:22-bookworm AS deps
WORKDIR /app

# 安装依赖
COPY package.json package-lock.json* ./
RUN if [ -f package-lock.json ]; then npm ci; else npm install; fi
RUN if [ -f package-lock.json ]; then npm install --prefer-offline; else npm install; fi

# 构建阶段
FROM node:20-alpine AS builder
FROM node:22-bookworm AS builder
WORKDIR /app

COPY --from=deps /app/node_modules ./node_modules
Expand All @@ -17,14 +17,14 @@ COPY . .
RUN npm run build

# 生产运行阶段
FROM node:20-alpine AS runner
FROM node:22-bookworm AS runner
WORKDIR /app

ENV NODE_ENV=production

# 创建非 root 用户
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
RUN groupadd --system --gid 1001 nodejs && \
useradd --system --uid 1001 -g nodejs nextjs

# 复制构建产物
COPY --from=builder /app/public ./public
Expand Down
10 changes: 8 additions & 2 deletions web/app/admin/repositories/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -827,8 +827,14 @@ export default function AdminRepositoryManagementPage() {
</button>
<button
type="button"
className="flex-1 truncate text-left text-sm"
onClick={() => handleSelectDoc(node.slug)}
className={`flex-1 truncate text-left text-sm ${hasChildren ? "cursor-pointer" : ""}`}
onClick={() => {
if (hasChildren) {
toggleDocExpanded(node.slug);
} else {
handleSelectDoc(node.slug);
}
}}
title={node.slug}
>
{node.title}
Expand Down
116 changes: 101 additions & 15 deletions web/components/repo/repo-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Link from "next/link";
import type { RepoTreeNode, RepoBranchesResponse } from "@/types/repository";
import { BranchLanguageSelector } from "./branch-language-selector";
import { fetchRepoTree, fetchRepoBranches } from "@/lib/repository-api";
import { Network, Download } from "lucide-react";
import { ChevronDown, ChevronRight, Network, Download } from "lucide-react";
import { ChatAssistant, buildCatalogMenu } from "@/components/chat";
import { buildRepoBasePath, buildRepoDocPath, buildRepoMindMapPath } from "@/lib/repo-route";

Expand Down Expand Up @@ -51,28 +51,92 @@ function SidebarTree({
currentPath: string;
depth?: number;
}) {
// 追踪每个目录节点的展开/折叠状态
const [expandedSlugs, setExpandedSlugs] = useState<Set<string>>(() => {
const expanded = new Set<string>();
const expandParents = (items: RepoTreeNode[], targetPath: string): boolean => {
for (const item of items) {
if (item.slug === targetPath) return true;
if (item.children && item.children.length > 0 && expandParents(item.children, targetPath)) {
expanded.add(item.slug);
return true;
}
}
return false;
};
if (currentPath) expandParents(nodes, currentPath);
return expanded;
});

const toggleExpand = (slug: string) => {
setExpandedSlugs((prev) => {
const next = new Set(prev);
if (next.has(slug)) {
next.delete(slug);
} else {
next.add(slug);
}
return next;
});
};

return (
<ul className={depth === 0 ? "space-y-1" : "mt-1 space-y-1 border-l border-border/60 pl-3"}>
{nodes.map((node) => {
const hasChildren = node.children && node.children.length > 0;
const isDirectory = hasChildren;
const isExpanded = expandedSlugs.has(node.slug);
const isActive = currentPath === node.slug;
const href = queryString
? `${buildRepoDocPath(owner, repo, node.slug)}?${queryString}`
: buildRepoDocPath(owner, repo, node.slug);
const isActive = currentPath === node.slug;

return (
<li key={node.slug}>
<Link
href={href}
className={[
"block rounded-md px-3 py-2 text-sm transition-colors",
isActive
? "bg-primary text-primary-foreground"
: "text-foreground/80 hover:bg-muted hover:text-foreground",
].join(" ")}
>
{node.title}
</Link>
{node.children && node.children.length > 0 && (
<div className="flex items-center">
{/* 目录节点显示展开/折叠箭头 */}
{isDirectory && (
<button
type="button"
onClick={() => toggleExpand(node.slug)}
className="flex h-6 w-4 shrink-0 items-center justify-center hover:text-foreground"
>
{isExpanded ? (
<ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
) : (
<ChevronRight className="h-3.5 w-3.5 text-muted-foreground" />
)}
</button>
)}
{/* 节点标题 */}
{isDirectory ? (
<button
type="button"
onClick={() => toggleExpand(node.slug)}
className={[
"flex-1 rounded-md px-3 py-2 text-left text-sm transition-colors",
isActive
? "bg-primary text-primary-foreground font-medium"
: "text-foreground/80 hover:bg-muted hover:text-foreground",
].join(" ")}
>
{node.title}
</button>
) : (
<Link
href={href}
className={[
"block flex-1 rounded-md px-3 py-2 text-sm transition-colors",
isActive
? "bg-primary text-primary-foreground"
: "text-foreground/80 hover:bg-muted hover:text-foreground",
].join(" ")}
>
{node.title}
</Link>
)}
</div>
{isDirectory && isExpanded && (
<SidebarTree
nodes={node.children}
owner={owner}
Expand Down Expand Up @@ -204,8 +268,30 @@ export function RepoShell({
}
}

// 获取原始字节数据
const rawBytes = await response.arrayBuffer();

// MiniApi 框架会将 FileContentResult 序列化为 JSON(包含 base64 编码的 fileContents 字段)
// 需要检测并解码,否则直接使用原始数据
let blob: Blob;
const textDecoder = new TextDecoder();
const textPreview = textDecoder.decode(rawBytes.slice(0, 50));

if (textPreview.startsWith('{"') && textPreview.includes('fileContents')) {
const jsonString = textDecoder.decode(rawBytes);
const json = JSON.parse(jsonString);
const base64Content = json.fileContents as string;
const binaryString = atob(base64Content);
const byteArray = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
byteArray[i] = binaryString.charCodeAt(i);
}
blob = new Blob([byteArray], { type: "application/zip" });
} else {
blob = new Blob([rawBytes], { type: "application/zip" });
}

// 下载文件
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
Expand Down
81 changes: 0 additions & 81 deletions web/components/repo/repo-sidebar.tsx

This file was deleted.