mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 20:08:18 +08:00
feat(wiki): unify raw materials & source watcher into a Sources tab with per-KB auto-sync (#316)
* feat(wiki): unify raw materials & source watcher into a Sources tab with per-KB auto-sync The raw-material directory scan and the Advanced "source watcher" sub-tab were the same engine (same kb.sourceDirectory, same WikiDirectoryScanService) split across two surfaces with two editable directory inputs. Merge them into one "Sources" tab (upload / paste / directory manual scan + auto-sync toggle + the raw-material list) and drop the watcher sub-tab from Advanced. Auto-sync is now per-KB opt-in: a new watcher_enabled column (V146) gates the periodic scan per knowledge base. The server-global mate.wiki.watcher-enabled stays as an ops master switch — a KB is auto-scanned only when both are on (AND). Manual scans are unaffected. Scan interval stays global for now (tracked separately). Closes matevip/mateclaw#314 * docs(wiki): document source-watcher global switch env vars Expose MATE_WIKI_WATCHER_ENABLED / MATE_WIKI_WATCHER_INTERVAL_MS as explicit placeholders in application-mysql.yml, .env.example and docker-compose.yml, mirroring MATE_WIKI_ALLOWED_SOURCE_ROOTS. Notes the AND semantics (global ops gate + per-KB toggle) so operators know the global switch alone is not sufficient.
This commit is contained in:
parent
92d401bc1f
commit
18daad79b2
@ -87,6 +87,14 @@ MATECLAW_OAUTH_OPENAI_CALLBACK_BIND_HOST=
|
|||||||
# - /your/host/path:/data/wiki
|
# - /your/host/path:/data/wiki
|
||||||
MATE_WIKI_ALLOWED_SOURCE_ROOTS=
|
MATE_WIKI_ALLOWED_SOURCE_ROOTS=
|
||||||
|
|
||||||
|
# ── Wiki 知识源自动同步(变更监测)总开关 ────────────────────────
|
||||||
|
# 定时扫描各知识库的源目录、自动消化新文件。默认关闭,运维主动开启。
|
||||||
|
# AND 语义:全局这个开关开 *且* 某知识库自己的「自动同步」开关也开,
|
||||||
|
# 该库才会被定时扫描;手动「立即扫描」不受此开关影响。
|
||||||
|
# 间隔单位毫秒,默认 5 分钟(目前为全局,暂不支持按库配置)。
|
||||||
|
MATE_WIKI_WATCHER_ENABLED=false
|
||||||
|
MATE_WIKI_WATCHER_INTERVAL_MS=300000
|
||||||
|
|
||||||
# ── Skill 工作区目录 ─────────────────────────────────────────────
|
# ── Skill 工作区目录 ─────────────────────────────────────────────
|
||||||
# 已安装的 skill、运行时积累的 LESSONS.md、skill 运行产物都落在这个目录。
|
# 已安装的 skill、运行时积累的 LESSONS.md、skill 运行产物都落在这个目录。
|
||||||
# 默认(容器内)已指向 /app/data/skills,由 docker-compose 的 server_data 卷
|
# 默认(容器内)已指向 /app/data/skills,由 docker-compose 的 server_data 卷
|
||||||
|
|||||||
@ -100,6 +100,11 @@ services:
|
|||||||
# 示例:MATE_WIKI_ALLOWED_SOURCE_ROOTS=/data/wiki,/opt/docs
|
# 示例:MATE_WIKI_ALLOWED_SOURCE_ROOTS=/data/wiki,/opt/docs
|
||||||
# 记得同步在 volumes 里把宿主机路径挂进容器。
|
# 记得同步在 volumes 里把宿主机路径挂进容器。
|
||||||
MATE_WIKI_ALLOWED_SOURCE_ROOTS: ${MATE_WIKI_ALLOWED_SOURCE_ROOTS:-}
|
MATE_WIKI_ALLOWED_SOURCE_ROOTS: ${MATE_WIKI_ALLOWED_SOURCE_ROOTS:-}
|
||||||
|
# Wiki 知识源自动同步总开关(运维总闸,默认关)。AND 语义:全局开关与
|
||||||
|
# 每个知识库自己的「自动同步」开关都开,该库才会被定时扫描。
|
||||||
|
# 间隔单位毫秒,默认 5 分钟。
|
||||||
|
MATE_WIKI_WATCHER_ENABLED: ${MATE_WIKI_WATCHER_ENABLED:-false}
|
||||||
|
MATE_WIKI_WATCHER_INTERVAL_MS: ${MATE_WIKI_WATCHER_INTERVAL_MS:-300000}
|
||||||
# Skill 工作区根目录。放在 /app/data 下,让现有的 server_data 卷一并持久化
|
# Skill 工作区根目录。放在 /app/data 下,让现有的 server_data 卷一并持久化
|
||||||
# 已安装的 skill、运行时积累的 LESSONS.md 以及 skill 运行产物,容器重启不丢。
|
# 已安装的 skill、运行时积累的 LESSONS.md 以及 skill 运行产物,容器重启不丢。
|
||||||
# 内置 skill 仍由 JAR classpath 每次启动现场释放,空卷不会丢内置文件。
|
# 内置 skill 仍由 JAR classpath 每次启动现场释放,空卷不会丢内置文件。
|
||||||
|
|||||||
@ -345,7 +345,10 @@ public class WikiController {
|
|||||||
if (kb == null) return R.fail(404, "Knowledge base not found");
|
if (kb == null) return R.fail(404, "Knowledge base not found");
|
||||||
vip.mate.wiki.source.WikiIngestSourceProvider provider = sourceWatcherService.providerFor(kb);
|
vip.mate.wiki.source.WikiIngestSourceProvider provider = sourceWatcherService.providerFor(kb);
|
||||||
Map<String, Object> out = new LinkedHashMap<>();
|
Map<String, Object> out = new LinkedHashMap<>();
|
||||||
|
// Global master switch (ops): gates the scheduler at all.
|
||||||
out.put("watcherEnabled", properties.isWatcherEnabled());
|
out.put("watcherEnabled", properties.isWatcherEnabled());
|
||||||
|
// Per-KB opt-in: auto-sync runs only when both are true (AND semantics).
|
||||||
|
out.put("kbWatcherEnabled", kb.getWatcherEnabled() != null && kb.getWatcherEnabled() == 1);
|
||||||
out.put("intervalMs", properties.getWatcherIntervalMs());
|
out.put("intervalMs", properties.getWatcherIntervalMs());
|
||||||
out.put("sourceDirectory", kb.getSourceDirectory());
|
out.put("sourceDirectory", kb.getSourceDirectory());
|
||||||
out.put("sourceType", provider != null ? provider.sourceType() : null);
|
out.put("sourceType", provider != null ? provider.sourceType() : null);
|
||||||
@ -354,6 +357,20 @@ public class WikiController {
|
|||||||
return R.ok(out);
|
return R.ok(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequireWorkspaceRole("member")
|
||||||
|
@Operation(summary = "开关知识库的自动同步(每库)")
|
||||||
|
@PutMapping("/knowledge-bases/{id}/source-watcher/enabled")
|
||||||
|
public R<Void> setWatcherEnabled(@PathVariable Long id, @RequestBody Map<String, Object> body,
|
||||||
|
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) {
|
||||||
|
verifyKBWorkspace(id, workspaceId);
|
||||||
|
WikiKnowledgeBaseEntity kb = kbService.getById(id);
|
||||||
|
if (kb == null) return R.fail(404, "Knowledge base not found");
|
||||||
|
Object v = body.get("enabled");
|
||||||
|
boolean enabled = (v instanceof Boolean b) ? b : Boolean.parseBoolean(String.valueOf(v));
|
||||||
|
kbService.updateWatcherEnabled(id, enabled);
|
||||||
|
return R.ok();
|
||||||
|
}
|
||||||
|
|
||||||
@RequireWorkspaceRole("member")
|
@RequireWorkspaceRole("member")
|
||||||
@Operation(summary = "手动触发一次源监听扫描")
|
@Operation(summary = "手动触发一次源监听扫描")
|
||||||
@PostMapping("/knowledge-bases/{id}/source-watcher/scan")
|
@PostMapping("/knowledge-bases/{id}/source-watcher/scan")
|
||||||
|
|||||||
@ -33,6 +33,13 @@ public class WikiKnowledgeBaseEntity {
|
|||||||
/** 关联的本地目录路径(可选,用于批量扫描导入) */
|
/** 关联的本地目录路径(可选,用于批量扫描导入) */
|
||||||
private String sourceDirectory;
|
private String sourceDirectory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否对该知识库启用自动同步(周期扫描 sourceDirectory)。1=开,0=关。
|
||||||
|
* 自动扫描需"全局总闸 mate.wiki.watcher-enabled 开 且 本字段为 1"(AND 语义);
|
||||||
|
* 手动扫描不受此字段影响。
|
||||||
|
*/
|
||||||
|
private Integer watcherEnabled;
|
||||||
|
|
||||||
/** 状态:active / processing / error */
|
/** 状态:active / processing / error */
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
|
|||||||
@ -383,6 +383,17 @@ public class WikiKnowledgeBaseService {
|
|||||||
kbMapper.updateById(entity);
|
kbMapper.updateById(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Toggle per-KB auto-sync (the periodic source-watcher scan). */
|
||||||
|
@Transactional
|
||||||
|
public void updateWatcherEnabled(Long id, boolean enabled) {
|
||||||
|
WikiKnowledgeBaseEntity entity = kbMapper.selectById(id);
|
||||||
|
if (entity == null) {
|
||||||
|
throw new IllegalArgumentException("Knowledge base not found: " + id);
|
||||||
|
}
|
||||||
|
entity.setWatcherEnabled(enabled ? 1 : 0);
|
||||||
|
kbMapper.updateById(entity);
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void decrementRawCount(Long kbId) {
|
public void decrementRawCount(Long kbId) {
|
||||||
WikiKnowledgeBaseEntity entity = kbMapper.selectById(kbId);
|
WikiKnowledgeBaseEntity entity = kbMapper.selectById(kbId);
|
||||||
|
|||||||
@ -63,6 +63,12 @@ public class WikiSourceWatcherService {
|
|||||||
public int runScanCycle() {
|
public int runScanCycle() {
|
||||||
int totalAdded = 0;
|
int totalAdded = 0;
|
||||||
for (WikiKnowledgeBaseEntity kb : kbService.listAll()) {
|
for (WikiKnowledgeBaseEntity kb : kbService.listAll()) {
|
||||||
|
// Per-KB opt-in: the global master switch (checked in scheduledScan)
|
||||||
|
// gates the scheduler at all; this flag gates each KB. AND semantics —
|
||||||
|
// a KB is auto-scanned only when both are on. Manual scans bypass this.
|
||||||
|
if (kb.getWatcherEnabled() == null || kb.getWatcherEnabled() != 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
vip.mate.wiki.source.WikiIngestSourceProvider provider = providerFor(kb);
|
vip.mate.wiki.source.WikiIngestSourceProvider provider = providerFor(kb);
|
||||||
if (provider == null) {
|
if (provider == null) {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@ -33,7 +33,14 @@ spring:
|
|||||||
# Set MATE_WIKI_ALLOWED_SOURCE_ROOTS in .env (comma-separated paths):
|
# Set MATE_WIKI_ALLOWED_SOURCE_ROOTS in .env (comma-separated paths):
|
||||||
# MATE_WIKI_ALLOWED_SOURCE_ROOTS=/data/wiki,/opt/docs
|
# MATE_WIKI_ALLOWED_SOURCE_ROOTS=/data/wiki,/opt/docs
|
||||||
# The default profile (H2 / desktop / single-tenant) leaves this off.
|
# The default profile (H2 / desktop / single-tenant) leaves this off.
|
||||||
|
# Source watcher master switch (ops gate). Off by default; operators opt in.
|
||||||
|
# AND semantics: a KB is auto-scanned only when this global switch AND that
|
||||||
|
# KB's own auto-sync toggle are both on. Manual scans are unaffected.
|
||||||
|
# MATE_WIKI_WATCHER_ENABLED=true
|
||||||
|
# MATE_WIKI_WATCHER_INTERVAL_MS=300000 # scan interval, default 5 min
|
||||||
mate:
|
mate:
|
||||||
wiki:
|
wiki:
|
||||||
require-allowed-roots: true
|
require-allowed-roots: true
|
||||||
allowed-source-roots: ${MATE_WIKI_ALLOWED_SOURCE_ROOTS:}
|
allowed-source-roots: ${MATE_WIKI_ALLOWED_SOURCE_ROOTS:}
|
||||||
|
watcher-enabled: ${MATE_WIKI_WATCHER_ENABLED:false}
|
||||||
|
watcher-interval-ms: ${MATE_WIKI_WATCHER_INTERVAL_MS:300000}
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
-- Per-KB source-watcher toggle. Auto-sync (the periodic directory scan) was
|
||||||
|
-- previously gated only by the server-global `mate.wiki.watcher-enabled`.
|
||||||
|
-- This column makes the auto-sync opt-in per knowledge base: a KB is scanned
|
||||||
|
-- automatically only when the global master switch is on AND this flag is set
|
||||||
|
-- (AND semantics). Manual "scan now" is unaffected by this flag. Defaults to 0
|
||||||
|
-- so existing KBs are not auto-scanned until explicitly enabled.
|
||||||
|
|
||||||
|
ALTER TABLE mate_wiki_knowledge_base
|
||||||
|
ADD COLUMN IF NOT EXISTS watcher_enabled TINYINT(1) NOT NULL DEFAULT 0;
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
-- Per-KB source-watcher toggle. See the h2 sibling for the prose explanation.
|
||||||
|
-- MySQL lacks `ADD COLUMN IF NOT EXISTS`, so the column is guarded by an
|
||||||
|
-- INFORMATION_SCHEMA check + prepared statement (idempotent).
|
||||||
|
|
||||||
|
SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'mate_wiki_knowledge_base'
|
||||||
|
AND COLUMN_NAME = 'watcher_enabled');
|
||||||
|
SET @s := IF(@c = 0,
|
||||||
|
'ALTER TABLE mate_wiki_knowledge_base ADD COLUMN watcher_enabled TINYINT(1) NOT NULL DEFAULT 0',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
@ -35,6 +35,8 @@ class WikiSourceWatcherServiceE2ETest {
|
|||||||
private WikiKnowledgeBaseService kbService;
|
private WikiKnowledgeBaseService kbService;
|
||||||
@Autowired
|
@Autowired
|
||||||
private WikiDirectoryScanService scanService;
|
private WikiDirectoryScanService scanService;
|
||||||
|
@Autowired
|
||||||
|
private WikiRawMaterialService rawMaterialService;
|
||||||
|
|
||||||
private static final java.util.concurrent.atomic.AtomicLong SEQ =
|
private static final java.util.concurrent.atomic.AtomicLong SEQ =
|
||||||
new java.util.concurrent.atomic.AtomicLong(System.nanoTime());
|
new java.util.concurrent.atomic.AtomicLong(System.nanoTime());
|
||||||
@ -47,6 +49,8 @@ class WikiSourceWatcherServiceE2ETest {
|
|||||||
WikiKnowledgeBaseEntity kb = kbService.create(
|
WikiKnowledgeBaseEntity kb = kbService.create(
|
||||||
"watcher-" + SEQ.incrementAndGet(), "test", null);
|
"watcher-" + SEQ.incrementAndGet(), "test", null);
|
||||||
kbService.updateSourceDirectory(kb.getId(), sourceDir.toString());
|
kbService.updateSourceDirectory(kb.getId(), sourceDir.toString());
|
||||||
|
// Auto-sync is per-KB opt-in; enable it so the cycle scans this KB.
|
||||||
|
kbService.updateWatcherEnabled(kb.getId(), true);
|
||||||
|
|
||||||
// First cycle ingests both new files.
|
// First cycle ingests both new files.
|
||||||
int firstAdded = watcherService.runScanCycle();
|
int firstAdded = watcherService.runScanCycle();
|
||||||
@ -109,4 +113,23 @@ class WikiSourceWatcherServiceE2ETest {
|
|||||||
// Should complete without throwing (count is non-negative).
|
// Should complete without throwing (count is non-negative).
|
||||||
assertTrue(watcherService.runScanCycle() >= 0);
|
assertTrue(watcherService.runScanCycle() >= 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void disabledKb_isNotAutoScanned_untilEnabled(@TempDir Path sourceDir) throws IOException {
|
||||||
|
Files.writeString(sourceDir.resolve("note.md"), "# Note\n\ncontent");
|
||||||
|
WikiKnowledgeBaseEntity kb = kbService.create(
|
||||||
|
"watcher-off-" + SEQ.incrementAndGet(), "test", null);
|
||||||
|
kbService.updateSourceDirectory(kb.getId(), sourceDir.toString());
|
||||||
|
// watcher_enabled defaults to 0 → the auto cycle must skip this KB.
|
||||||
|
|
||||||
|
watcherService.runScanCycle();
|
||||||
|
assertEquals(0, rawMaterialService.listByKbId(kb.getId()).size(),
|
||||||
|
"a KB with auto-sync disabled must not be auto-scanned");
|
||||||
|
|
||||||
|
// Once enabled, the same cycle ingests its file.
|
||||||
|
kbService.updateWatcherEnabled(kb.getId(), true);
|
||||||
|
watcherService.runScanCycle();
|
||||||
|
assertTrue(rawMaterialService.listByKbId(kb.getId()).size() >= 1,
|
||||||
|
"after enabling auto-sync the KB's file should be ingested");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -946,6 +946,8 @@ export const wikiApi = {
|
|||||||
http.get(`/wiki/knowledge-bases/${kbId}/source-watcher`),
|
http.get(`/wiki/knowledge-bases/${kbId}/source-watcher`),
|
||||||
triggerSourceWatcher: (kbId: string | number) =>
|
triggerSourceWatcher: (kbId: string | number) =>
|
||||||
http.post(`/wiki/knowledge-bases/${kbId}/source-watcher/scan`),
|
http.post(`/wiki/knowledge-bases/${kbId}/source-watcher/scan`),
|
||||||
|
setWatcherEnabled: (kbId: string | number, enabled: boolean) =>
|
||||||
|
http.put(`/wiki/knowledge-bases/${kbId}/source-watcher/enabled`, { enabled }),
|
||||||
|
|
||||||
// ---- Pipelines (REQ-5) ----
|
// ---- Pipelines (REQ-5) ----
|
||||||
listPipelines: (kbId: string | number) =>
|
listPipelines: (kbId: string | number) =>
|
||||||
|
|||||||
@ -2091,6 +2091,13 @@ export default {
|
|||||||
kbDescription: 'Description',
|
kbDescription: 'Description',
|
||||||
kbDescPlaceholder: 'Briefly describe the knowledge base purpose',
|
kbDescPlaceholder: 'Briefly describe the knowledge base purpose',
|
||||||
rawMaterials: 'Raw Materials',
|
rawMaterials: 'Raw Materials',
|
||||||
|
sources: {
|
||||||
|
tab: 'Sources',
|
||||||
|
autoSync: 'Auto-sync',
|
||||||
|
autoSyncInterval: 'every {sec}s',
|
||||||
|
autoSyncGlobalOffHint: 'Auto-sync needs the global switch enabled by ops',
|
||||||
|
toggleFailed: 'Failed to toggle auto-sync',
|
||||||
|
},
|
||||||
pages: 'Wiki Pages',
|
pages: 'Wiki Pages',
|
||||||
config: 'Config',
|
config: 'Config',
|
||||||
transformations: {
|
transformations: {
|
||||||
|
|||||||
@ -2103,6 +2103,13 @@ export default {
|
|||||||
kbDescription: '描述',
|
kbDescription: '描述',
|
||||||
kbDescPlaceholder: '简要描述知识库用途',
|
kbDescPlaceholder: '简要描述知识库用途',
|
||||||
rawMaterials: '原始材料',
|
rawMaterials: '原始材料',
|
||||||
|
sources: {
|
||||||
|
tab: '来源',
|
||||||
|
autoSync: '自动同步',
|
||||||
|
autoSyncInterval: '每 {sec} 秒',
|
||||||
|
autoSyncGlobalOffHint: '自动同步需运维开启全局开关',
|
||||||
|
toggleFailed: '切换自动同步失败',
|
||||||
|
},
|
||||||
pages: 'Wiki 页面',
|
pages: 'Wiki 页面',
|
||||||
config: '处理配置',
|
config: '处理配置',
|
||||||
transformations: {
|
transformations: {
|
||||||
|
|||||||
@ -72,6 +72,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Auto-sync (per-KB source watcher): periodically scans the directory above -->
|
||||||
|
<div v-if="canManageWiki" class="auto-sync-row">
|
||||||
|
<label class="auto-sync-toggle" :class="{ disabled: !watcher.globalEnabled || watcher.busy }">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="watcher.kbEnabled"
|
||||||
|
:disabled="!watcher.globalEnabled || watcher.busy"
|
||||||
|
@change="toggleWatcher(($event.target as HTMLInputElement).checked)"
|
||||||
|
/>
|
||||||
|
<span>{{ t('wiki.sources.autoSync') }}</span>
|
||||||
|
</label>
|
||||||
|
<span v-if="watcher.globalEnabled && watcher.kbEnabled" class="auto-sync-meta">
|
||||||
|
{{ t('wiki.sources.autoSyncInterval', { sec: Math.round(watcher.intervalMs / 1000) }) }}
|
||||||
|
<template v-if="watcher.sourceType"> · {{ watcher.sourceType }}</template>
|
||||||
|
</span>
|
||||||
|
<span v-else-if="!watcher.globalEnabled" class="auto-sync-hint">
|
||||||
|
{{ t('wiki.sources.autoSyncGlobalOffHint') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Raw materials list -->
|
<!-- Raw materials list -->
|
||||||
<div class="raw-list">
|
<div class="raw-list">
|
||||||
<h4 class="raw-list-title">
|
<h4 class="raw-list-title">
|
||||||
@ -502,6 +522,48 @@ const dirPath = ref(store.currentKB?.sourceDirectory || '')
|
|||||||
const scanning = ref(false)
|
const scanning = ref(false)
|
||||||
const scanResult = ref<{ scanned: number; added: number; skipped: number; errors?: string[] } | null>(null)
|
const scanResult = ref<{ scanned: number; added: number; skipped: number; errors?: string[] } | null>(null)
|
||||||
|
|
||||||
|
// ─── Per-KB auto-sync (source watcher) ────────────────────────────────────────
|
||||||
|
// Auto-sync periodically scans the directory above. It runs only when the
|
||||||
|
// server-global master switch (watcher.globalEnabled, ops-controlled) AND this
|
||||||
|
// KB's toggle (watcher.kbEnabled) are both on. When the global switch is off the
|
||||||
|
// toggle is disabled with a hint — there's nothing a non-ops user can do here.
|
||||||
|
const watcher = reactive({
|
||||||
|
globalEnabled: false,
|
||||||
|
kbEnabled: false,
|
||||||
|
intervalMs: 0,
|
||||||
|
sourceType: null as string | null,
|
||||||
|
busy: false,
|
||||||
|
})
|
||||||
|
async function loadWatcher(kbId: number) {
|
||||||
|
try {
|
||||||
|
const res: any = await wikiApi.getSourceWatcher(kbId)
|
||||||
|
const d = res?.data ?? res
|
||||||
|
watcher.globalEnabled = !!d.watcherEnabled
|
||||||
|
watcher.kbEnabled = !!d.kbWatcherEnabled
|
||||||
|
watcher.intervalMs = d.intervalMs || 0
|
||||||
|
watcher.sourceType = d.sourceType || null
|
||||||
|
} catch { /* leave defaults; auto-sync UI just shows disabled */ }
|
||||||
|
}
|
||||||
|
async function toggleWatcher(next: boolean) {
|
||||||
|
if (!store.currentKB) return
|
||||||
|
watcher.busy = true
|
||||||
|
try {
|
||||||
|
await wikiApi.setWatcherEnabled(store.currentKB.id, next)
|
||||||
|
watcher.kbEnabled = next
|
||||||
|
mcToast.success(t('common.saved'))
|
||||||
|
} catch (e: any) {
|
||||||
|
mcToast.error(e?.response?.data?.message || t('wiki.sources.toggleFailed'))
|
||||||
|
} finally {
|
||||||
|
watcher.busy = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
watch(() => store.currentKB?.id, (id) => {
|
||||||
|
if (id) {
|
||||||
|
dirPath.value = store.currentKB?.sourceDirectory || ''
|
||||||
|
void loadWatcher(id as number)
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
// ─── Drag-over state ──────────────────────────────────────────────────────────
|
// ─── Drag-over state ──────────────────────────────────────────────────────────
|
||||||
const { isDragging, onDragEnter, onDragLeave, onDrop: handleDrop } = useFileDrop(uploadDroppedFiles)
|
const { isDragging, onDragEnter, onDragLeave, onDrop: handleDrop } = useFileDrop(uploadDroppedFiles)
|
||||||
|
|
||||||
@ -707,6 +769,14 @@ async function handleScanDir() {
|
|||||||
|
|
||||||
/* Directory scan */
|
/* Directory scan */
|
||||||
.dir-scan-row { display: flex; gap: 10px; align-items: center; }
|
.dir-scan-row { display: flex; gap: 10px; align-items: center; }
|
||||||
|
|
||||||
|
/* Auto-sync (per-KB source watcher) */
|
||||||
|
.auto-sync-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: -4px; }
|
||||||
|
.auto-sync-toggle { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; color: var(--mc-text-secondary); cursor: pointer; }
|
||||||
|
.auto-sync-toggle.disabled { opacity: 0.55; cursor: not-allowed; }
|
||||||
|
.auto-sync-toggle input { cursor: inherit; }
|
||||||
|
.auto-sync-meta { font-size: 12px; color: var(--mc-text-tertiary); font-variant-numeric: tabular-nums; }
|
||||||
|
.auto-sync-hint { font-size: 12px; color: var(--mc-text-tertiary); }
|
||||||
.dir-input-wrap { flex: 1; display: flex; align-items: center; gap: 8px; padding: 8px 12px; border: 1px solid var(--mc-border); border-radius: 12px; background: var(--mc-bg-elevated); color: var(--mc-text-tertiary); }
|
.dir-input-wrap { flex: 1; display: flex; align-items: center; gap: 8px; padding: 8px 12px; border: 1px solid var(--mc-border); border-radius: 12px; background: var(--mc-bg-elevated); color: var(--mc-text-tertiary); }
|
||||||
.dir-input-wrap:focus-within { border-color: var(--mc-primary); box-shadow: 0 0 0 2px rgba(217,119,87,0.1); }
|
.dir-input-wrap:focus-within { border-color: var(--mc-primary); box-shadow: 0 0 0 2px rgba(217,119,87,0.1); }
|
||||||
.dir-input { flex: 1; border: none; background: transparent; font-size: 13px; color: var(--mc-text-primary); outline: none; }
|
.dir-input { flex: 1; border: none; background: transparent; font-size: 13px; color: var(--mc-text-primary); outline: none; }
|
||||||
|
|||||||
@ -107,41 +107,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ===================== REQ-4: Source Watcher ===================== -->
|
<!-- Source-watcher moved to the unified Sources tab (RawMaterialPanel). -->
|
||||||
<section v-else-if="section === 'watcher'" class="adv-section">
|
|
||||||
<header class="adv-head">
|
|
||||||
<div>
|
|
||||||
<h3>{{ t('wiki.adv.watcher.title') }}</h3>
|
|
||||||
<p class="adv-desc">{{ t('wiki.adv.watcher.desc') }}</p>
|
|
||||||
</div>
|
|
||||||
<button class="btn-ghost" @click="loadWatcher" :disabled="watcher.busy">{{ t('common.refresh') }}</button>
|
|
||||||
</header>
|
|
||||||
<div class="kv-grid">
|
|
||||||
<div class="kv"><span>{{ t('wiki.adv.watcher.enabled') }}</span><b>{{ watcher.data.watcherEnabled ? t('common.yes') : t('common.no') }}</b></div>
|
|
||||||
<div class="kv"><span>{{ t('wiki.adv.watcher.active') }}</span><b>{{ watcher.data.active ? t('common.yes') : t('common.no') }}</b></div>
|
|
||||||
<div class="kv"><span>{{ t('wiki.adv.watcher.interval') }}</span><b>{{ watcher.data.intervalMs ? (watcher.data.intervalMs / 1000) + 's' : '—' }}</b></div>
|
|
||||||
<div class="kv"><span>{{ t('wiki.adv.watcher.sourceType') }}</span><b>{{ watcher.data.sourceType || '—' }}</b></div>
|
|
||||||
</div>
|
|
||||||
<label class="field-label">{{ t('wiki.adv.watcher.directory') }}</label>
|
|
||||||
<textarea
|
|
||||||
v-model="watcher.dir"
|
|
||||||
class="code-editor dir-editor"
|
|
||||||
spellcheck="false"
|
|
||||||
:placeholder="t('wiki.adv.watcher.dirHint')"
|
|
||||||
></textarea>
|
|
||||||
<div class="adv-actions">
|
|
||||||
<button class="btn-ghost" @click="saveDirectory" :disabled="watcher.busy">{{ t('common.save') }}</button>
|
|
||||||
</div>
|
|
||||||
<p v-if="watcher.data.availableSourceTypes?.length" class="adv-desc">
|
|
||||||
{{ t('wiki.adv.watcher.availableTypes') }}: {{ watcher.data.availableSourceTypes.join(', ') }}
|
|
||||||
</p>
|
|
||||||
<div class="adv-actions">
|
|
||||||
<button class="btn-primary" @click="triggerScan" :disabled="watcher.busy || !watcher.data.active">{{ t('wiki.adv.watcher.scanNow') }}</button>
|
|
||||||
</div>
|
|
||||||
<div v-if="watcher.lastScan" class="issue-box ok">
|
|
||||||
{{ t('wiki.adv.watcher.scanResult', { scanned: watcher.lastScan.scanned, added: watcher.lastScan.added, skipped: watcher.lastScan.skipped, errors: watcher.lastScan.errors }) }}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ===================== REQ-5: Pipeline ===================== -->
|
<!-- ===================== REQ-5: Pipeline ===================== -->
|
||||||
<section v-else-if="section === 'pipeline'" class="adv-section">
|
<section v-else-if="section === 'pipeline'" class="adv-section">
|
||||||
@ -227,12 +193,11 @@ const agentStore = useAgentStore()
|
|||||||
const kbId = computed(() => (store.currentKB ? String(store.currentKB.id) : ''))
|
const kbId = computed(() => (store.currentKB ? String(store.currentKB.id) : ''))
|
||||||
const agents = computed(() => agentStore.agents)
|
const agents = computed(() => agentStore.agents)
|
||||||
|
|
||||||
const section = ref<'profile' | 'layers' | 'permissions' | 'watcher' | 'pipeline'>('profile')
|
const section = ref<'profile' | 'layers' | 'permissions' | 'pipeline'>('profile')
|
||||||
const sections = computed(() => [
|
const sections = computed(() => [
|
||||||
{ key: 'profile' as const, label: t('wiki.adv.profile.tab') },
|
{ key: 'profile' as const, label: t('wiki.adv.profile.tab') },
|
||||||
{ key: 'layers' as const, label: t('wiki.adv.layers.tab') },
|
{ key: 'layers' as const, label: t('wiki.adv.layers.tab') },
|
||||||
{ key: 'permissions' as const, label: t('wiki.adv.perm.tab') },
|
{ key: 'permissions' as const, label: t('wiki.adv.perm.tab') },
|
||||||
{ key: 'watcher' as const, label: t('wiki.adv.watcher.tab') },
|
|
||||||
{ key: 'pipeline' as const, label: t('wiki.adv.pipeline.tab') },
|
{ key: 'pipeline' as const, label: t('wiki.adv.pipeline.tab') },
|
||||||
])
|
])
|
||||||
|
|
||||||
@ -243,7 +208,6 @@ function switchSection(key: typeof section.value) {
|
|||||||
loaded[key] = true
|
loaded[key] = true
|
||||||
if (key === 'layers') loadLayers()
|
if (key === 'layers') loadLayers()
|
||||||
else if (key === 'permissions') { if (agents.value.length === 0) agentStore.fetchAgents() }
|
else if (key === 'permissions') { if (agents.value.length === 0) agentStore.fetchAgents() }
|
||||||
else if (key === 'watcher') loadWatcher()
|
|
||||||
else if (key === 'pipeline') loadPipelines()
|
else if (key === 'pipeline') loadPipelines()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -315,31 +279,7 @@ async function deletePermission(row: any) {
|
|||||||
function flag(v: any) { return v ? '✓' : '·' }
|
function flag(v: any) { return v ? '✓' : '·' }
|
||||||
function policyClass(p?: string) { return p === 'allow' ? 'badge-ok' : p === 'deny' ? 'badge-warn' : 'badge-muted' }
|
function policyClass(p?: string) { return p === 'allow' ? 'badge-ok' : p === 'deny' ? 'badge-warn' : 'badge-muted' }
|
||||||
|
|
||||||
// ---- REQ-4 Watcher ----
|
// REQ-4 Source-watcher moved to the unified Sources tab (RawMaterialPanel).
|
||||||
const watcher = reactive({ data: {} as any, dir: '', lastScan: null as any, busy: false })
|
|
||||||
async function loadWatcher() {
|
|
||||||
if (!kbId.value) return
|
|
||||||
watcher.busy = true
|
|
||||||
try {
|
|
||||||
watcher.data = unwrap(await wikiApi.getSourceWatcher(kbId.value)) || {}
|
|
||||||
watcher.dir = watcher.data.sourceDirectory || ''
|
|
||||||
} catch (e: any) { mcToast.error(errMsg(e, 'Load watcher failed')) } finally { watcher.busy = false }
|
|
||||||
}
|
|
||||||
async function saveDirectory() {
|
|
||||||
watcher.busy = true
|
|
||||||
try {
|
|
||||||
await wikiApi.setSourceDirectory(kbId.value, watcher.dir)
|
|
||||||
mcToast.success(t('common.saved'))
|
|
||||||
await loadWatcher()
|
|
||||||
} catch (e: any) { mcToast.error(errMsg(e, 'Save failed')) } finally { watcher.busy = false }
|
|
||||||
}
|
|
||||||
async function triggerScan() {
|
|
||||||
watcher.busy = true
|
|
||||||
try {
|
|
||||||
watcher.lastScan = unwrap(await wikiApi.triggerSourceWatcher(kbId.value))
|
|
||||||
mcToast.success(t('wiki.adv.watcher.scanDone'))
|
|
||||||
} catch (e: any) { mcToast.error(errMsg(e, 'Scan failed')) } finally { watcher.busy = false }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- REQ-5 Pipeline ----
|
// ---- REQ-5 Pipeline ----
|
||||||
const pipeline = reactive({ defs: [] as any[], config: '', issues: [] as string[], runsFor: null as any, runs: [] as any[], busy: false })
|
const pipeline = reactive({ defs: [] as any[], config: '', issues: [] as string[], runsFor: null as any, runs: [] as any[], busy: false })
|
||||||
|
|||||||
@ -147,7 +147,7 @@ watch(() => store.currentPage, (page) => {
|
|||||||
const tabs = computed<{ key: WikiTab; label: string }[]>(() => {
|
const tabs = computed<{ key: WikiTab; label: string }[]>(() => {
|
||||||
if (!canManageWiki.value) return []
|
if (!canManageWiki.value) return []
|
||||||
return [
|
return [
|
||||||
{ key: 'raw', label: t('wiki.rawMaterials') },
|
{ key: 'raw', label: t('wiki.sources.tab') },
|
||||||
{ key: 'config', label: t('wiki.config') },
|
{ key: 'config', label: t('wiki.config') },
|
||||||
{ key: 'transformations', label: t('wiki.transformations.tab') },
|
{ key: 'transformations', label: t('wiki.transformations.tab') },
|
||||||
{ key: 'advanced', label: t('wiki.adv.tab') },
|
{ key: 'advanced', label: t('wiki.adv.tab') },
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user