Files
wehub-resource-sync e4dcfc49aa
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:00:43 +08:00

248 lines
7.1 KiB
TypeScript

import type { TFunction } from "i18next";
export interface KnowledgeUploadPolicy {
extensions: string[];
accept: string;
max_file_size_bytes: number;
}
export const DEFAULT_UPLOAD_POLICY: KnowledgeUploadPolicy = {
extensions: [],
accept: "",
max_file_size_bytes: 200 * 1024 * 1024,
};
export interface ProgressInfo {
task_id?: string;
stage?: string;
message?: string;
current?: number;
total?: number;
percent?: number;
progress_percent?: number;
indexed_count?: number;
index_changed?: boolean;
index_action?: string;
}
export interface IndexVersion {
signature?: string;
model?: string;
dimension?: number;
binding?: string;
created_at?: string;
ready?: boolean;
legacy?: boolean;
}
export interface KnowledgeBase {
id?: string;
name: string;
is_default?: boolean;
status?: string;
path?: string;
metadata?: {
created_at?: string;
last_updated?: string;
last_indexed_at?: string;
last_indexed_count?: number;
last_indexed_action?: string;
rag_provider?: string;
needs_reindex?: boolean;
embedding_model?: string;
embedding_dim?: number;
embedding_mismatch?: boolean;
/** Connected-source kind (e.g. "obsidian", "subagent"); absent for ordinary indexed KBs. */
type?: string;
/** Absolute path of a connected Obsidian vault (when type === "obsidian"). */
vault_path?: string;
/** Backend of a connected subagent (when type === "subagent"): "claude_code" | "codex" | "partner". */
agent_kind?: string;
/** Bound partner id when agent_kind === "partner". */
partner_id?: string;
};
progress?: ProgressInfo;
statistics?: {
raw_documents?: number;
images?: number;
content_lists?: number;
rag_provider?: string;
rag_initialized?: boolean;
needs_reindex?: boolean;
status?: string;
progress?: ProgressInfo;
index_versions?: IndexVersion[];
active_signature?: string | null;
active_match?: boolean;
};
source?: "admin" | "user";
assigned?: boolean;
read_only?: boolean;
provenance_label?: string;
available?: boolean;
}
export interface ValidatedSelectionFile {
id: string;
file: File;
extension: string;
sizeLabel: string;
valid: boolean;
error: string | null;
}
export interface ValidatedFileSelection {
items: ValidatedSelectionFile[];
validFiles: File[];
invalidFiles: ValidatedSelectionFile[];
totalBytes: number;
}
export const formatFileSize = (bytes: number): string => {
if (bytes >= 1024 * 1024 * 1024)
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${bytes} B`;
};
export const getFileExtension = (filename: string): string => {
const index = filename.lastIndexOf(".");
return index >= 0 ? filename.slice(index).toLowerCase() : "";
};
export const selectionFileId = (file: File): string =>
`${file.name}:${file.size}:${file.lastModified}`;
export const mergeSelectedFiles = (
existing: File[],
incoming: File[],
): File[] => {
const merged = new Map<string, File>();
[...existing, ...incoming].forEach((file) => {
merged.set(selectionFileId(file), file);
});
return Array.from(merged.values());
};
const parseKnowledgeTimestamp = (value?: string): Date | null => {
if (!value) return null;
const normalized = value.includes("T") ? value : value.replace(" ", "T");
const parsed = new Date(normalized);
return Number.isNaN(parsed.getTime()) ? null : parsed;
};
export const formatKnowledgeTimestamp = (value?: string): string | null => {
const parsed = parseKnowledgeTimestamp(value);
return parsed ? parsed.toLocaleString() : value || null;
};
/** The retrieval engine a KB is bound to. Connected vaults badge by source. */
export const kbProvider = (kb: KnowledgeBase): string => {
if (kb.metadata?.type === "obsidian") return "obsidian";
return (
(kb.statistics?.rag_provider as string | undefined) ||
(kb.metadata?.rag_provider as string | undefined) ||
"llamaindex"
);
};
/** Source-document count for a KB, or null when unknown. */
export const kbDocCount = (kb: KnowledgeBase): number | null => {
const raw = kb.statistics?.raw_documents;
if (typeof raw === "number") return raw;
const indexed = kb.metadata?.last_indexed_count;
return typeof indexed === "number" ? indexed : null;
};
export const resolveKbStatus = (kb: KnowledgeBase): string =>
kb.status ?? kb.statistics?.status ?? "unknown";
export const kbNeedsReindex = (kb: KnowledgeBase): boolean =>
Boolean(kb.statistics?.needs_reindex) ||
resolveKbStatus(kb) === "needs_reindex";
export const kbIsUploadable = (kb: KnowledgeBase): boolean =>
resolveKbStatus(kb) === "ready" && !kbNeedsReindex(kb);
export const kbCanReindex = (kb: KnowledgeBase): boolean => {
const status = resolveKbStatus(kb);
const hasSourceFiles =
typeof kb.statistics?.raw_documents === "number"
? kb.statistics.raw_documents > 0
: true;
if (!hasSourceFiles) return false;
if (status === "error") return true;
return (
Boolean(kb.statistics?.needs_reindex) ||
kb.statistics?.active_match === false
);
};
const LIVE_PROGRESS_STAGES = new Set([
"initializing",
"starting",
"processing_documents",
"processing_file",
]);
export const kbHasLiveProgress = (kb: KnowledgeBase): boolean => {
const status = resolveKbStatus(kb);
if (status === "ready" || status === "error" || status === "needs_reindex") {
return false;
}
const stage = kb.progress?.stage;
if (!stage) return false;
if (stage === "completed" || stage === "error") return false;
return LIVE_PROGRESS_STAGES.has(stage);
};
export const resolveProgressPercent = (progress?: ProgressInfo): number => {
const directPercent = progress?.progress_percent ?? progress?.percent;
if (typeof directPercent === "number") return directPercent;
const current = progress?.current ?? 0;
const total = progress?.total ?? 0;
if (!current || !total) return 0;
return Math.round((current / total) * 100);
};
export function validateFiles(
files: File[],
uploadPolicy: KnowledgeUploadPolicy,
t: TFunction,
): ValidatedFileSelection {
const allowedExtensions = new Set(
uploadPolicy.extensions.map((ext) => ext.toLowerCase()),
);
const items = files.map((file) => {
const extension = getFileExtension(file.name);
let error: string | null = null;
if (allowedExtensions.size > 0 && !allowedExtensions.has(extension)) {
error = t("Unsupported file type");
} else if (file.size > uploadPolicy.max_file_size_bytes) {
error = t("This file exceeds the maximum size of {{size}}.", {
size: formatFileSize(uploadPolicy.max_file_size_bytes),
});
}
return {
id: selectionFileId(file),
file,
extension: extension || t("No extension"),
sizeLabel: formatFileSize(file.size),
valid: !error,
error,
};
});
return {
items,
validFiles: items.filter((item) => item.valid).map((item) => item.file),
invalidFiles: items.filter((item) => !item.valid),
totalBytes: files.reduce((total, file) => total + file.size, 0),
};
}