From 29fcfb5572d45daa367083f1c4cc08cf852faa87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=80=AA=E7=A8=8B=E4=BC=9F?= Date: Sun, 7 Jun 2026 16:18:49 +0800 Subject: [PATCH] fix(wiki): surface scan errors and fix Chinese path handling in Docker (#260) Surface directory-scan failures to the user via toast and render ScanResult.errors[]; expose MATE_WIKI_ALLOWED_SOURCE_ROOTS as a Docker env entry with blank-entry filtering in the path validator; set C.UTF-8 locale in the runtime image so non-ASCII file names decode correctly during scans. Fixes #259 --- .env.example | 13 +++++++++++++ docker-compose.yml | 4 ++++ mateclaw-server/Dockerfile | 4 +++- .../mate/wiki/service/WikiSourcePathValidator.java | 14 ++++++++------ .../src/main/resources/application-mysql.yml | 4 +++- mateclaw-ui/src/i18n/locales/en-US.ts | 1 + mateclaw-ui/src/i18n/locales/zh-CN.ts | 1 + .../src/views/Wiki/components/RawMaterialPanel.vue | 8 +++++++- 8 files changed, 40 insertions(+), 9 deletions(-) diff --git a/.env.example b/.env.example index 31fc3dfe..de591168 100644 --- a/.env.example +++ b/.env.example @@ -74,6 +74,19 @@ MATECLAW_BROWSER_CHANNEL= MATECLAW_OAUTH_OPENAI_DEPLOYMENT_MODE= MATECLAW_OAUTH_OPENAI_CALLBACK_BIND_HOST= +# ==================== Wiki 知识库目录白名单(Docker 模式,可选)==================== +# +# Docker 生产部署开启了路径安全校验(fail-closed)。 +# 知识库使用「目录扫描」功能时,扫描路径必须在此白名单内,否则返回 400 错误。 +# 多个路径用英文逗号分隔;留空则禁止所有目录扫描。 +# +# 示例:MATE_WIKI_ALLOWED_SOURCE_ROOTS=/data/wiki,/opt/docs +# +# 同时在 docker-compose.yml 的 volumes 里把宿主机目录挂进容器,例如: +# volumes: +# - /your/host/path:/data/wiki +MATE_WIKI_ALLOWED_SOURCE_ROOTS= + # ── Maven 镜像(国内加速)───────────────────────────────────────── # 在中国大陆构建时取消注释,将 Aliyun 仓库优先级提前,大幅提速 mvn 拉包。 # 空值(默认)使用 US Maven Central → Google CDN → Aliyun 的顺序。 diff --git a/docker-compose.yml b/docker-compose.yml index 69408c32..52546b8e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -96,6 +96,10 @@ services: # 本机 Docker 若要强制使用 localhost:1455 回调,可在 .env 显式设为 local。 MATECLAW_OAUTH_OPENAI_DEPLOYMENT_MODE: ${MATECLAW_OAUTH_OPENAI_DEPLOYMENT_MODE:-} MATECLAW_OAUTH_OPENAI_CALLBACK_BIND_HOST: ${MATECLAW_OAUTH_OPENAI_CALLBACK_BIND_HOST:-0.0.0.0} + # Wiki 知识库目录扫描白名单(逗号分隔,留空则禁止所有目录扫描)。 + # 示例:MATE_WIKI_ALLOWED_SOURCE_ROOTS=/data/wiki,/opt/docs + # 记得同步在 volumes 里把宿主机路径挂进容器。 + MATE_WIKI_ALLOWED_SOURCE_ROOTS: ${MATE_WIKI_ALLOWED_SOURCE_ROOTS:-} # Chromium needs a real /dev/shm. Docker defaults to 64MB which causes # SIGBUS / "Target page closed" errors under load. 2GB is the usual # recommendation for Playwright / headless chrome. diff --git a/mateclaw-server/Dockerfile b/mateclaw-server/Dockerfile index 05963794..529319d5 100644 --- a/mateclaw-server/Dockerfile +++ b/mateclaw-server/Dockerfile @@ -105,7 +105,9 @@ RUN apt-get update \ # BrowserLauncher's BUNDLED strategy will then succeed without extra config. ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright \ TZ=Asia/Shanghai \ - JAVA_TOOL_OPTIONS="-Duser.timezone=Asia/Shanghai" + LANG=C.UTF-8 \ + LC_ALL=C.UTF-8 \ + JAVA_TOOL_OPTIONS="-Duser.timezone=Asia/Shanghai -Dsun.jnu.encoding=UTF-8" COPY --from=builder /build/mateclaw-server/target/*.jar app.jar EXPOSE 18088 diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiSourcePathValidator.java b/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiSourcePathValidator.java index 2400a88b..593a9f31 100644 --- a/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiSourcePathValidator.java +++ b/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiSourcePathValidator.java @@ -46,18 +46,20 @@ public class WikiSourcePathValidator { } Path resolved = canonicalize(Paths.get(rawPath)); List roots = properties.getAllowedSourceRoots(); - if (roots == null || roots.isEmpty()) { + // Filter blank entries so MATE_WIKI_ALLOWED_SOURCE_ROOTS="" (unset env var) + // behaves identically to an empty list rather than a list with one blank entry. + List nonBlankRoots = (roots == null) ? List.of() + : roots.stream().filter(r -> r != null && !r.isBlank()).toList(); + if (nonBlankRoots.isEmpty()) { if (properties.isRequireAllowedRoots()) { throw new IllegalArgumentException( "No allowed source roots are configured; refusing the path (fail-closed). " - + "Set mate.wiki.allowed-source-roots to permit directories."); + + "Set MATE_WIKI_ALLOWED_SOURCE_ROOTS (env var) or " + + "mate.wiki.allowed-source-roots to permit directories."); } return resolved; } - for (String root : roots) { - if (root == null || root.isBlank()) { - continue; - } + for (String root : nonBlankRoots) { Path rootPath = canonicalize(Paths.get(root)); if (resolved.startsWith(rootPath)) { return resolved; diff --git a/mateclaw-server/src/main/resources/application-mysql.yml b/mateclaw-server/src/main/resources/application-mysql.yml index d7e0cafd..aaad80cc 100644 --- a/mateclaw-server/src/main/resources/application-mysql.yml +++ b/mateclaw-server/src/main/resources/application-mysql.yml @@ -30,8 +30,10 @@ spring: # validation. With no allowed-source-roots configured, every KB source # directory is rejected rather than allowing full-filesystem reads — so a # missing allow-list cannot silently re-open arbitrary directory scanning. -# Operators set mate.wiki.allowed-source-roots to permit specific roots. +# Set MATE_WIKI_ALLOWED_SOURCE_ROOTS in .env (comma-separated paths): +# MATE_WIKI_ALLOWED_SOURCE_ROOTS=/data/wiki,/opt/docs # The default profile (H2 / desktop / single-tenant) leaves this off. mate: wiki: require-allowed-roots: true + allowed-source-roots: ${MATE_WIKI_ALLOWED_SOURCE_ROOTS:} diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts index ca0cd40c..b3eb4223 100644 --- a/mateclaw-ui/src/i18n/locales/en-US.ts +++ b/mateclaw-ui/src/i18n/locales/en-US.ts @@ -2264,6 +2264,7 @@ export default { scan: 'Scan', scanning: 'Scanning...', scanResult: 'Scanned {scanned} files, added {added}, skipped {skipped}', + scanFailed: 'Scan failed', saving: 'Saving...', status: { active: 'ACTIVE', diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts index a3a94ab6..31b6bd1e 100644 --- a/mateclaw-ui/src/i18n/locales/zh-CN.ts +++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts @@ -2276,6 +2276,7 @@ export default { scan: '扫描', scanning: '扫描中...', scanResult: '已扫描 {scanned} 个文件,新增 {added} 个,跳过 {skipped} 个', + scanFailed: '扫描失败', saving: '保存中...', status: { active: '启用', diff --git a/mateclaw-ui/src/views/Wiki/components/RawMaterialPanel.vue b/mateclaw-ui/src/views/Wiki/components/RawMaterialPanel.vue index 31c238a4..bea0cf91 100644 --- a/mateclaw-ui/src/views/Wiki/components/RawMaterialPanel.vue +++ b/mateclaw-ui/src/views/Wiki/components/RawMaterialPanel.vue @@ -67,6 +67,9 @@
{{ t('wiki.scanResult', { scanned: scanResult.scanned, added: scanResult.added, skipped: scanResult.skipped }) }} +
+ {{ err }} +
@@ -680,7 +683,8 @@ async function handleScanDir() { const result = await store.scanDirectory(store.currentKB.id) scanResult.value = result } catch (e: any) { - console.error('Scan failed', e) + const msg = e?.response?.data?.message || e?.message || t('wiki.scanFailed') + mcToast.error(msg) } finally { scanning.value = false } @@ -708,6 +712,8 @@ async function handleScanDir() { .dir-input { flex: 1; border: none; background: transparent; font-size: 13px; color: var(--mc-text-primary); outline: none; } .dir-input::placeholder { color: var(--mc-text-tertiary); } .scan-result { font-size: 12px; color: var(--mc-text-secondary); padding: 8px 10px; background: rgba(90,138,90,0.1); border-radius: 10px; } +.scan-errors { margin-top: 6px; display: flex; flex-direction: column; gap: 2px; } +.scan-error-item { color: var(--mc-danger); font-size: 11px; } /* Upload row: zone + add text side by side */ .upload-row { display: flex; gap: 12px; align-items: stretch; }