技能展示配置持久化并适配引用来源,新增技能文件预览组件
- 后端将技能展示适配配置存入数据库,引用批次按持久化映射进行适配 - 前端新增技能文件预览弹窗与文件树组件,完善技能来源展示 - 修正 .gitignore 中宽泛的 skills/ 规则误忽略前端组件目录的问题 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
f200972c00
commit
4410acaee8
11 files changed
+1153
-172
No files matched your search
@@ -16,6 +16,7 @@ import { apiFetch } from "@/core/api";
|
||||
import { getBackendBaseURL } from "@/core/config";
|
||||
import { isThreadRagModeLocked, useLocalSettings } from "@/core/settings";
|
||||
import { useSkills } from "@/core/skills/hooks";
|
||||
import { normalizeDisplaySources } from "@/core/skill-display/display-adapter";
|
||||
import {
|
||||
buildSkillDisplayMap,
|
||||
collectRuntimeDisplaySources,
|
||||
@@ -147,16 +148,30 @@ const ChatBox: React.FC<{ children: React.ReactNode; threadId: string }> = ({
|
||||
});
|
||||
return () => controller.abort();
|
||||
}, [ragModeEnabled, thread.isLoading, threadId, thread.values.messages]);
|
||||
const referenceBatches =
|
||||
backendReferenceBatches.length > 0
|
||||
? backendReferenceBatches
|
||||
: fallbackReferenceBatches;
|
||||
const adaptedBackendReferenceBatches = useMemo(
|
||||
() =>
|
||||
adaptBackendReferenceBatches(
|
||||
backendReferenceBatches,
|
||||
fallbackReferenceBatches,
|
||||
skillDisplayMap,
|
||||
),
|
||||
[backendReferenceBatches, fallbackReferenceBatches, skillDisplayMap],
|
||||
);
|
||||
const referenceBatches = useMemo(
|
||||
() =>
|
||||
mergeReferenceBatches(
|
||||
adaptedBackendReferenceBatches,
|
||||
fallbackReferenceBatches,
|
||||
),
|
||||
[adaptedBackendReferenceBatches, fallbackReferenceBatches],
|
||||
);
|
||||
useEffect(() => {
|
||||
if (!ragModeEnabled) {
|
||||
return;
|
||||
}
|
||||
const signature = JSON.stringify({
|
||||
backend: backendReferenceBatches.map((batch) => batch.sources?.length ?? 0),
|
||||
adaptedBackend: adaptedBackendReferenceBatches.map((batch) => batch.sources?.length ?? 0),
|
||||
fallback: fallbackReferenceBatches.map((batch) => batch.sources?.length ?? 0),
|
||||
final: referenceBatches.map((batch) => batch.sources?.length ?? 0),
|
||||
});
|
||||
@@ -168,12 +183,13 @@ const ChatBox: React.FC<{ children: React.ReactNode; threadId: string }> = ({
|
||||
using: backendReferenceBatches.length > 0 ? "后端接口" : "前端兜底",
|
||||
backendBatchCount: backendReferenceBatches.length,
|
||||
backendSourceCounts: backendReferenceBatches.map((batch) => batch.sources?.length ?? 0),
|
||||
adaptedBackendSourceCounts: adaptedBackendReferenceBatches.map((batch) => batch.sources?.length ?? 0),
|
||||
fallbackBatchCount: fallbackReferenceBatches.length,
|
||||
fallbackSourceCounts: fallbackReferenceBatches.map((batch) => batch.sources?.length ?? 0),
|
||||
finalBatchCount: referenceBatches.length,
|
||||
finalSourceCounts: referenceBatches.map((batch) => batch.sources?.length ?? 0),
|
||||
});
|
||||
}, [backendReferenceBatches, fallbackReferenceBatches, ragModeEnabled, referenceBatches, threadId]);
|
||||
}, [adaptedBackendReferenceBatches, backendReferenceBatches, fallbackReferenceBatches, ragModeEnabled, referenceBatches, threadId]);
|
||||
const latestReferenceBatch =
|
||||
referenceBatches.length > 0
|
||||
? referenceBatches[referenceBatches.length - 1]!
|
||||
@@ -487,6 +503,191 @@ function ReferencePanel({
|
||||
);
|
||||
}
|
||||
|
||||
function adaptBackendReferenceBatches(
|
||||
backendBatches: BackendReferenceBatch[],
|
||||
configuredBatches: BackendReferenceBatch[],
|
||||
skillDisplayMap: ReturnType<typeof buildSkillDisplayMap>,
|
||||
): BackendReferenceBatch[] {
|
||||
if (backendBatches.length === 0) {
|
||||
return backendBatches;
|
||||
}
|
||||
return backendBatches.map((backendBatch, batchIndex) => {
|
||||
const configuredBatch =
|
||||
configuredBatches.find(
|
||||
(batch) =>
|
||||
batch.id === backendBatch.id ||
|
||||
batch.assistant_message_id === backendBatch.assistant_message_id,
|
||||
) ?? configuredBatches[batchIndex];
|
||||
const configuredSources = configuredBatch?.sources ?? [];
|
||||
const configuredByIndex = new Map(
|
||||
configuredSources.map((source) => [source.index, source]),
|
||||
);
|
||||
const configuredByKey = new Map(
|
||||
configuredSources.map((source) => [referenceSourceKey(source), source]),
|
||||
);
|
||||
return {
|
||||
...backendBatch,
|
||||
sources: backendBatch.sources.map((backendSource, sourceIndex) => {
|
||||
const configuredSource =
|
||||
configuredByIndex.get(backendSource.index) ??
|
||||
configuredByKey.get(referenceSourceKey(backendSource)) ??
|
||||
configuredSources[sourceIndex] ??
|
||||
adaptBackendSourceWithSkillDisplay(backendSource, skillDisplayMap);
|
||||
if (!configuredSource) {
|
||||
return backendSource;
|
||||
}
|
||||
return {
|
||||
...backendSource,
|
||||
...configuredSource,
|
||||
index: backendSource.index,
|
||||
mode:
|
||||
backendSource.mode !== "citation"
|
||||
? backendSource.mode
|
||||
: configuredSource.mode || backendSource.mode,
|
||||
raw: backendSource.raw ?? configuredSource.raw,
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function mergeReferenceBatches(
|
||||
backendBatches: BackendReferenceBatch[],
|
||||
fallbackBatches: BackendReferenceBatch[],
|
||||
): BackendReferenceBatch[] {
|
||||
if (backendBatches.length === 0) {
|
||||
return fallbackBatches;
|
||||
}
|
||||
if (fallbackBatches.length === 0) {
|
||||
return backendBatches;
|
||||
}
|
||||
const merged = [...backendBatches];
|
||||
const knownIds = new Set(
|
||||
backendBatches.flatMap((batch) =>
|
||||
[batch.id, batch.assistant_message_id].filter(Boolean),
|
||||
),
|
||||
);
|
||||
for (const fallbackBatch of fallbackBatches) {
|
||||
if (
|
||||
knownIds.has(fallbackBatch.id) ||
|
||||
(fallbackBatch.assistant_message_id &&
|
||||
knownIds.has(fallbackBatch.assistant_message_id))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
merged.push(fallbackBatch);
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
function adaptBackendSourceWithSkillDisplay(
|
||||
source: BackendReferenceSource,
|
||||
skillDisplayMap: ReturnType<typeof buildSkillDisplayMap>,
|
||||
): BackendReferenceSource | null {
|
||||
const raw = source.raw;
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
||||
return null;
|
||||
}
|
||||
const rawItem = raw as Record<string, unknown>;
|
||||
const match = findBackendSourceDisplay(source, skillDisplayMap);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const normalized = normalizeDisplaySources(
|
||||
buildPayloadAtPath(match.display.resultPath || "results", [rawItem]),
|
||||
match.display,
|
||||
)[0];
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...source,
|
||||
...normalized,
|
||||
skillName: match.skillName,
|
||||
mode: match.display.mode,
|
||||
tableColumns: match.display.tableColumns,
|
||||
index: source.index,
|
||||
raw: source.raw,
|
||||
};
|
||||
}
|
||||
|
||||
function findBackendSourceDisplay(
|
||||
source: BackendReferenceSource,
|
||||
skillDisplayMap: ReturnType<typeof buildSkillDisplayMap>,
|
||||
) {
|
||||
const direct = skillDisplayMap.get(source.skillName);
|
||||
if (direct) {
|
||||
return { skillName: source.skillName, display: direct };
|
||||
}
|
||||
const normalizedSource = normalizeSkillName(source.skillName);
|
||||
for (const [skillName, display] of skillDisplayMap) {
|
||||
const normalizedSkill = normalizeSkillName(skillName);
|
||||
if (
|
||||
normalizedSource === normalizedSkill ||
|
||||
normalizedSource.endsWith(normalizedSkill) ||
|
||||
normalizedSource.includes(normalizedSkill)
|
||||
) {
|
||||
return { skillName, display };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeSkillName(value: string): string {
|
||||
return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
||||
}
|
||||
|
||||
function buildPayloadAtPath(path: string, items: Record<string, unknown>[]) {
|
||||
const parts = path.split(".").map((part) => part.trim()).filter(Boolean);
|
||||
if (parts.length === 0) {
|
||||
return items;
|
||||
}
|
||||
const root: Record<string, unknown> = {};
|
||||
let cursor = root;
|
||||
for (let index = 0; index < parts.length; index++) {
|
||||
const part = parts[index]!;
|
||||
if (index === parts.length - 1) {
|
||||
cursor[part] = items;
|
||||
} else {
|
||||
const next: Record<string, unknown> = {};
|
||||
cursor[part] = next;
|
||||
cursor = next;
|
||||
}
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
function referenceSourceKey(source: BackendReferenceSource): string {
|
||||
const raw = source.raw ?? {};
|
||||
const rawRecord =
|
||||
raw && typeof raw === "object" && !Array.isArray(raw)
|
||||
? (raw as Record<string, unknown>)
|
||||
: {};
|
||||
const id =
|
||||
source.id ||
|
||||
source.url ||
|
||||
stringifyKeyValue(rawRecord.recUuid) ||
|
||||
stringifyKeyValue(rawRecord.uuid) ||
|
||||
stringifyKeyValue(rawRecord.docId) ||
|
||||
stringifyKeyValue(rawRecord.id);
|
||||
if (id) {
|
||||
return id.trim().toLowerCase();
|
||||
}
|
||||
return [source.title, source.source, source.time, source.content, source.snippet]
|
||||
.filter(Boolean)
|
||||
.join("|")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
function stringifyKeyValue(value: unknown): string | undefined {
|
||||
if (typeof value === "string" || typeof value === "number") {
|
||||
const text = String(value).trim();
|
||||
return text || undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function findPreviousHumanMessageId(
|
||||
messages: Message[],
|
||||
beforeMessageId?: string,
|
||||
@@ -530,8 +731,7 @@ function buildTurnReferenceBatches(
|
||||
beforeMessageId:
|
||||
nextHumanIndex === -1 ? undefined : messages[nextHumanIndex]?.id,
|
||||
includeBeforeMessage: false,
|
||||
includeModes: ["citation"],
|
||||
}).filter((source) => source.mode === "citation");
|
||||
}).filter((source) => source.mode !== "none");
|
||||
if (sources.length > 0) {
|
||||
batches.push({
|
||||
id: assistant.id,
|
||||
@@ -562,8 +762,7 @@ function findLastAssistantAnswer(
|
||||
if (
|
||||
message?.type === "ai" &&
|
||||
message.id &&
|
||||
hasAssistantAnswerContent(message) &&
|
||||
!hasAssistantToolCalls(message)
|
||||
hasAssistantAnswerContent(message)
|
||||
) {
|
||||
return message;
|
||||
}
|
||||
@@ -588,18 +787,4 @@ function hasAssistantAnswerContent(message: Message): boolean {
|
||||
});
|
||||
}
|
||||
|
||||
function hasAssistantToolCalls(message: Message): boolean {
|
||||
const direct = (message as { tool_calls?: unknown }).tool_calls;
|
||||
if (Array.isArray(direct) && direct.length > 0) {
|
||||
return true;
|
||||
}
|
||||
const chunks = (message as { tool_call_chunks?: unknown }).tool_call_chunks;
|
||||
if (Array.isArray(chunks) && chunks.length > 0) {
|
||||
return true;
|
||||
}
|
||||
const kwargs = (message as { additional_kwargs?: Record<string, unknown> }).additional_kwargs;
|
||||
const extra = kwargs?.tool_calls;
|
||||
return Array.isArray(extra) && extra.length > 0;
|
||||
}
|
||||
|
||||
export { ChatBox };
|
||||
Reference in new issue
Block a user