技能展示配置持久化并适配引用来源,新增技能文件预览组件

- 后端将技能展示适配配置存入数据库,引用批次按持久化映射进行适配
- 前端新增技能文件预览弹窗与文件树组件,完善技能来源展示
- 修正 .gitignore 中宽泛的 skills/ 规则误忽略前端组件目录的问题

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
吉鑫升andClaude Opus 4.7 authored and xuhd committed 2026-05-28 18:29:10 +08:00
1 parent ac98d2f585
commit dac5ff38db
10 files changed
+1152 -171

No files matched your search

+12 -89
View File
@@ -106,23 +106,6 @@ function canInferSkillFromToolArgs(toolName: string): boolean {
);
}
function canInspectPayloadForDisplay(toolName: string): boolean {
const normalized = normalizeName(toolName);
if (
TOOL_ARG_SKILL_MATCH_EXCLUDES.has(toolName) ||
TOOL_ARG_SKILL_MATCH_EXCLUDES.has(normalized)
) {
return false;
}
return (
canInferSkillFromToolArgs(toolName) ||
normalized.includes("search") ||
normalized.includes("retriev") ||
normalized.includes("knowledge") ||
normalized.includes("query")
);
}
function extractSkillNameFromArgs(args: Record<string, unknown>): string | undefined {
for (const value of Object.values(args)) {
if (typeof value !== "string") continue;
@@ -164,9 +147,7 @@ export function collectRuntimeDisplaySources(
: null;
const sources: RuntimeDisplaySource[] = [];
let citationIndex = 0;
const displayIndexes = new Map<string, number>();
const seenCitationKeys = new Map<string, RuntimeDisplaySource>();
const seenDisplayKeys = new Set<string>();
const consumedToolMessageIds = new Set<string>();
const consumedToolMessages = new Set<Message>();
@@ -224,9 +205,7 @@ export function collectRuntimeDisplaySources(
mode: effectiveMode,
tableColumns: match?.display.tableColumns,
})),
displayIndexes,
seenCitationKeys,
seenDisplayKeys,
() => ++citationIndex,
);
}
@@ -247,7 +226,7 @@ export function collectRuntimeDisplaySources(
continue;
}
const toolName = extractToolMessageName(message);
const match = null;
const match = findDisplayConfigForPayload(toolName, result, displays);
const effectiveMode = match?.display.mode ?? "citation";
if (allowedModes && !allowedModes.has(effectiveMode)) {
continue;
@@ -266,9 +245,7 @@ export function collectRuntimeDisplaySources(
mode: effectiveMode,
tableColumns: match?.display.tableColumns,
})),
displayIndexes,
seenCitationKeys,
seenDisplayKeys,
() => ++citationIndex,
);
}
@@ -278,9 +255,7 @@ export function collectRuntimeDisplaySources(
function appendDisplaySources(
target: RuntimeDisplaySource[],
drafts: RuntimeDisplaySourceDraft[],
displayIndexes: Map<string, number>,
seenCitationKeys: Map<string, RuntimeDisplaySource>,
seenDisplayKeys: Set<string>,
nextCitationIndex: () => number,
) {
for (const draft of drafts) {
@@ -297,17 +272,9 @@ function appendDisplaySources(
continue;
}
const stableKey = nonCitationSourceKey(draft);
if (seenDisplayKeys.has(stableKey)) {
continue;
}
seenDisplayKeys.add(stableKey);
const displayKey = `${draft.skillName}:${draft.mode}`;
const displayIndex = (displayIndexes.get(displayKey) ?? 0) + 1;
displayIndexes.set(displayKey, displayIndex);
target.push({
...draft,
index: displayIndex,
index: nextCitationIndex(),
});
}
}
@@ -323,9 +290,6 @@ export function reindexRuntimeDisplaySources(
globalSources.map((source) => [citationSourceKey(source), source]),
);
return sources.map((source) => {
if (source.mode !== "citation") {
return source;
}
const global = globalByKey.get(citationSourceKey(source));
return global
? {
@@ -361,10 +325,6 @@ function citationSourceKey(source: RuntimeDisplaySourceDraft): string {
return normalizeCitationKey(fallback) || normalizeCitationKey(JSON.stringify(raw).slice(0, 300));
}
function nonCitationSourceKey(source: RuntimeDisplaySourceDraft): string {
return `${source.skillName}:${source.mode}:${citationSourceKey(source)}`;
}
function extractRuntimeToolCalls(message: Message): RuntimeToolCall[] {
const calls: RuntimeToolCall[] = [];
const direct = (message as { tool_calls?: unknown }).tool_calls;
@@ -441,20 +401,10 @@ function normalizeToolArgs(value: unknown): Record<string, unknown> | undefined
export function findDisplayConfigForPayload(
toolName: string,
payload: unknown,
_payload: unknown,
displays: SkillDisplayMap,
): { skillName: string; display: SkillDisplayConfig } | null {
if (!canInspectPayloadForDisplay(toolName)) {
return null;
}
let best: { skillName: string; display: SkillDisplayConfig; score: number } | null = null;
for (const [skillName, display] of displays) {
const score = scoreDisplayPayloadMatch(payload, display);
if (score >= 3 && (!best || score > best.score)) {
best = { skillName, display, score };
}
}
return best ? { skillName: best.skillName, display: best.display } : null;
return findDisplayConfigForTool(toolName, displays);
}
export function isUnconfiguredSkillInvocation(
@@ -498,33 +448,6 @@ export function isDisplaySamplePayload(payload: unknown): boolean {
);
}
function scoreDisplayPayloadMatch(payload: unknown, display: SkillDisplayConfig): number {
const items = getArrayByPath(payload, display.resultPath);
const first = items?.find((item): item is Record<string, unknown> => isRecord(item));
if (!first) {
return 0;
}
const fields = [
display.titleField,
display.summaryField,
display.contentField,
display.sourceField,
display.typeField,
display.timeField,
display.scoreField,
display.urlField,
display.idField,
].filter((field): field is string => !!field);
return new Set(fields).size === 0
? 0
: [...new Set(fields)].filter(
(field) =>
first[field] !== undefined &&
first[field] !== null &&
first[field] !== "",
).length;
}
function getArrayByPath(payload: unknown, path: string): unknown[] | null {
const value = path.split(".").reduce<unknown>((acc, key) => {
if (acc && typeof acc === "object") {
@@ -589,14 +512,14 @@ export function applyInlineCitations(
if (!content || sources.length === 0) {
return content;
}
const citationSources = sources.filter((source) => source.mode === "citation");
if (citationSources.length === 0) {
const referenceSources = sources.filter((source) => source.mode !== "none");
if (referenceSources.length === 0) {
return content;
}
const byIndex = new Map(citationSources.map((source) => [source.index, source]));
const byIndex = new Map(referenceSources.map((source) => [source.index, source]));
const byUrl = new Map<string, RuntimeDisplaySource>();
const byTitle = new Map<string, RuntimeDisplaySource>();
for (const source of citationSources) {
for (const source of referenceSources) {
if (source.url) {
byUrl.set(source.url, source);
}
@@ -610,7 +533,7 @@ export function applyInlineCitations(
byIndex,
byUrl,
byTitle,
citationSources,
referenceSources,
);
const markdownLinkPattern = /!?\[(?:[^\[\]]|\[[^\]]*\])*\]\([^)]+\)/g;
@@ -622,7 +545,7 @@ export function applyInlineCitations(
const replacedSlice = replaceCitationTokens(
normalized.slice(cursor, start),
byIndex,
citationSources,
referenceSources,
);
if (replacedSlice !== normalized.slice(cursor, start)) {
replaced = true;
@@ -631,7 +554,7 @@ export function applyInlineCitations(
next += match[0];
cursor = start + match[0].length;
}
const tail = replaceCitationTokens(normalized.slice(cursor), byIndex, citationSources);
const tail = replaceCitationTokens(normalized.slice(cursor), byIndex, referenceSources);
if (tail !== normalized.slice(cursor)) {
replaced = true;
}
@@ -639,7 +562,7 @@ export function applyInlineCitations(
if (replaced || hasCitationMarkdown(next)) {
return next;
}
return appendCitationSummary(next, citationSources);
return appendCitationSummary(next, referenceSources);
}
function replaceCitationTokens(
+26 -15
View File
@@ -14,8 +14,8 @@ import { getStoredAuth } from "../auth";
import { getBackendBaseURL } from "../config";
import { useI18n } from "../i18n/hooks";
import type { FileInMessage } from "../messages/utils";
import type { LocalSettings } from "../settings";
import { isThreadRagModeLocked, lockThreadRagMode, useLocalSettings } from "../settings";
import type { LocalSettings } from "../settings";
import { isThreadRagModeLocked, lockThreadRagMode, useLocalSettings } from "../settings";
import { useUpdateSubtask } from "../tasks/context";
import type { UploadedFileInfo } from "../uploads";
import { promptInputFilePartToFile, uploadFiles } from "../uploads";
@@ -343,7 +343,7 @@ export function useThreadStream({
if (isThreadNotAccessibleError(message)) {
const userLabel = buildCurrentUserLabel();
toast.info(
`当前登录用户为 ${userLabel},原对话不存在或无权访问,已为你打开新对话。`,
`当前登录用户为 ${userLabel},原对话不存在或无权访问,正在为你刷新页面…`,
);
// Prefer staying inside an agent chat context when the user is on an
// agent thread URL — otherwise fall back to the global new chat.
@@ -352,7 +352,18 @@ export function useThreadStream({
const target = agentMatch
? `/page/workspace/agents/${agentMatch[1]}/chats/new`
: "/page/workspace/chats/new";
navigate(target, { replace: true });
// First navigate to a safe URL so the post-reload page lands on a
// valid route. Then full-reload so TanStack Query cache, context
// providers and any other state captured under the previous user's
// identity get rebuilt against the current login.
if (typeof window !== "undefined") {
window.location.hash = target;
window.setTimeout(() => {
window.location.reload();
}, 900);
} else {
navigate(target, { replace: true });
}
return;
}
toast.error(message);
@@ -555,13 +566,13 @@ export function useThreadStream({
}),
);
const ragModeEnabled =
localSettings.display.ragModeEnabled || isThreadRagModeLocked(threadId);
if (ragModeEnabled) {
lockThreadRagMode(threadId);
}
await thread.submit(
const ragModeEnabled =
localSettings.display.ragModeEnabled || isThreadRagModeLocked(threadId);
if (ragModeEnabled) {
lockThreadRagMode(threadId);
}
await thread.submit(
{
messages: [
{
@@ -604,10 +615,10 @@ export function useThreadStream({
? "low"
: undefined),
memory_injection_enabled: context.memory_injection_enabled ?? true,
rag_mode_enabled: ragModeEnabled,
thread_id: threadId,
},
},
rag_mode_enabled: ragModeEnabled,
thread_id: threadId,
},
},
);
void queryClient.invalidateQueries({ queryKey: ["threads", "search"] });
} catch (error) {