技能展示配置持久化并适配引用来源,新增技能文件预览组件
- 后端将技能展示适配配置存入数据库,引用批次按持久化映射进行适配 - 前端新增技能文件预览弹窗与文件树组件,完善技能来源展示 - 修正 .gitignore 中宽泛的 skills/ 规则误忽略前端组件目录的问题 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
ac98d2f585
commit
dac5ff38db
10 files changed
+1152
-171
No files matched your search
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user