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
This commit is contained in:
倪程伟 2026-06-07 16:18:49 +08:00 committed by GitHub
parent 808047d723
commit 29fcfb5572
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 40 additions and 9 deletions

View File

@ -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 的顺序。

View File

@ -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.

View File

@ -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

View File

@ -46,18 +46,20 @@ public class WikiSourcePathValidator {
}
Path resolved = canonicalize(Paths.get(rawPath));
List<String> 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<String> 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;

View File

@ -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:}

View File

@ -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',

View File

@ -2276,6 +2276,7 @@ export default {
scan: '扫描',
scanning: '扫描中...',
scanResult: '已扫描 {scanned} 个文件,新增 {added} 个,跳过 {skipped} 个',
scanFailed: '扫描失败',
saving: '保存中...',
status: {
active: '启用',

View File

@ -67,6 +67,9 @@
</div>
<div v-if="scanResult" class="scan-result">
{{ t('wiki.scanResult', { scanned: scanResult.scanned, added: scanResult.added, skipped: scanResult.skipped }) }}
<div v-if="scanResult.errors?.length" class="scan-errors">
<span v-for="err in scanResult.errors" :key="err" class="scan-error-item">{{ err }}</span>
</div>
</div>
<!-- Raw materials list -->
@ -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; }