技能展示样本运行器 + 参考文献来源过滤调整
- skills 路由新增 _run_skill_python_sample,按本地 Python 候选实际执行 技能脚本,作为展示适配器的真实样本来源 - skill-display 运行时与适配器调整,参考文献收集放宽起始消息约束 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
26a388e3bf
commit
d3d7db1f34
4 files changed
+303
-104
No files matched your search
@@ -65,25 +65,16 @@ const ChatBox: React.FC<{ children: React.ReactNode; threadId: string }> = ({
|
||||
() => findLatestHumanMessageIndex(thread.values.messages ?? []),
|
||||
[thread.values.messages],
|
||||
);
|
||||
const latestAssistant = useMemo(
|
||||
() => findLatestAssistantMessageAfter(thread.values.messages ?? [], latestHumanIndex),
|
||||
[latestHumanIndex, thread.values.messages],
|
||||
);
|
||||
const displaySources = useMemo(() => {
|
||||
if (!settings.display.ragModeEnabled) {
|
||||
return [];
|
||||
}
|
||||
const messages = thread.values.messages ?? [];
|
||||
if (!latestAssistant?.id) {
|
||||
return [];
|
||||
}
|
||||
return collectRuntimeDisplaySources(messages, skillDisplayMap, {
|
||||
afterMessageId: latestHumanIndex >= 0 ? messages[latestHumanIndex]?.id : undefined,
|
||||
beforeMessageId: latestAssistant.id,
|
||||
includeBeforeMessage: true,
|
||||
includeModes: ["citation"],
|
||||
});
|
||||
}, [settings.display.ragModeEnabled, skillDisplayMap, latestAssistant?.id, latestHumanIndex, thread.values.messages]);
|
||||
}, [settings.display.ragModeEnabled, skillDisplayMap, latestHumanIndex, thread.values.messages]);
|
||||
const referenceSources = useMemo(
|
||||
() => displaySources.filter((source) => source.mode === "citation"),
|
||||
[displaySources],
|
||||
@@ -93,7 +84,7 @@ const ChatBox: React.FC<{ children: React.ReactNode; threadId: string }> = ({
|
||||
if (hasCitationReferences) {
|
||||
setReferencesOpen(true);
|
||||
}
|
||||
}, [hasCitationReferences, latestAssistant?.id]);
|
||||
}, [hasCitationReferences]);
|
||||
useEffect(() => {
|
||||
const handleShowReference = () => {
|
||||
setReferencesOpen(true);
|
||||
@@ -333,17 +324,4 @@ function findLatestHumanMessageIndex(messages: Message[]) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
function findLatestAssistantMessageAfter(messages: Message[], afterIndex: number) {
|
||||
for (let index = messages.length - 1; index >= 0; index--) {
|
||||
if (afterIndex >= 0 && index <= afterIndex) {
|
||||
return null;
|
||||
}
|
||||
const message = messages[index];
|
||||
if (message?.type === "ai") {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export { ChatBox };
|
||||
@@ -34,62 +34,6 @@ export const emptyDisplayConfig = (): SkillDisplayConfig => ({
|
||||
tableColumns: [],
|
||||
});
|
||||
|
||||
export const mockKnowledgeSearchV2Result = {
|
||||
success: true,
|
||||
query: "参考文献模式测试 知识库固定测试",
|
||||
count: 4,
|
||||
source: "fixed-display-sample",
|
||||
results: [
|
||||
{
|
||||
index: 1,
|
||||
recUuid: "ksv2-fixed-001",
|
||||
relevance_score: 0.982,
|
||||
title: "参考文献模式测试:参考文献模式固定测试数据一",
|
||||
source_category: "知识库/测试资料",
|
||||
source: "knowledge-search-v2/fixed/source-001",
|
||||
publish_time: "2026-05-25",
|
||||
content_preview:
|
||||
"这是 knowledge-search-v2 返回的第一条固定测试资料,用于验证右侧参考文献列表、正文编号以及 recUuid 模板跳转是否正常。",
|
||||
content_length: 168,
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
recUuid: "ksv2-fixed-002",
|
||||
relevance_score: 0.936,
|
||||
title: "参考文献模式测试:结构化字段映射验证",
|
||||
source_category: "知识库/字段映射",
|
||||
source: "knowledge-search-v2/fixed/source-002",
|
||||
publish_time: "2026-05-24",
|
||||
content_preview:
|
||||
"本条用于验证 title、source_category、source、publish_time、content_preview、relevance_score、recUuid 等字段能被前端正确识别并渲染。",
|
||||
content_length: 154,
|
||||
},
|
||||
{
|
||||
index: 3,
|
||||
recUuid: "ksv2-fixed-003",
|
||||
relevance_score: 0.887,
|
||||
title: "参考文献模式测试:多引用排序验证",
|
||||
source_category: "知识库/排序验证",
|
||||
source: "knowledge-search-v2/fixed/source-003",
|
||||
publish_time: "2026-05-23",
|
||||
content_preview:
|
||||
"本条用于验证多条参考文献的编号顺序是否与 results 数组顺序一致,并验证点击正文编号时能定位到右侧对应参考文献。",
|
||||
content_length: 132,
|
||||
},
|
||||
{
|
||||
index: 4,
|
||||
recUuid: "",
|
||||
relevance_score: 0.801,
|
||||
title: "参考文献模式测试:无 recUuid 跳转隐藏验证",
|
||||
source_category: "知识库/边界场景",
|
||||
source: "knowledge-search-v2/fixed/source-004",
|
||||
publish_time: "2026-05-22",
|
||||
content_preview: "本条故意把 recUuid 置为空,用于验证右侧弹框仍展示详情,但不展示外部跳转按钮。",
|
||||
content_length: 98,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const candidateFields: Record<string, string[]> = {
|
||||
titleField: ["title", "m_title", "name", "subject", "headline"],
|
||||
summaryField: ["summary", "snippet", "content_preview", "description", "abstract"],
|
||||
@@ -99,18 +43,23 @@ const candidateFields: Record<string, string[]> = {
|
||||
timeField: ["publish_time", "m_publish", "updatedAt", "createdAt", "date", "time"],
|
||||
scoreField: ["relevance_score", "rerank_score", "score", "similarity"],
|
||||
urlField: ["url", "link", "href", "source_url"],
|
||||
idField: ["recUuid", "id", "uuid", "docId", "taskId", "recordId"],
|
||||
idField: ["recUuid", "rec_uuid", "uuid", "docId", "taskId", "recordId", "id"],
|
||||
};
|
||||
|
||||
function getByPath(value: unknown, path: string): unknown {
|
||||
if (!path) return value;
|
||||
return path.split(".").reduce<unknown>((acc, key) => {
|
||||
if (acc && typeof acc === "object") return (acc as Record<string, unknown>)[key];
|
||||
if (acc && typeof acc === "object") {
|
||||
return (acc as Record<string, unknown>)[key];
|
||||
}
|
||||
return undefined;
|
||||
}, value);
|
||||
}
|
||||
|
||||
function findFirstArray(value: unknown, path = ""): { path: string; items: Record<string, unknown>[] } | null {
|
||||
function findFirstArray(
|
||||
value: unknown,
|
||||
path = "",
|
||||
): { path: string; items: Record<string, unknown>[] } | null {
|
||||
if (Array.isArray(value) && value.every((item) => item && typeof item === "object")) {
|
||||
return { path, items: value as Record<string, unknown>[] };
|
||||
}
|
||||
@@ -157,12 +106,12 @@ export function inferDisplayConfig(samplePayload: unknown, skillName?: string):
|
||||
inferred.typeField = inferred.typeField || "source_category";
|
||||
inferred.timeField = inferred.timeField || "publish_time";
|
||||
inferred.scoreField = inferred.scoreField || "relevance_score";
|
||||
inferred.idField = inferred.idField || "recUuid";
|
||||
inferred.linkStrategy = "id";
|
||||
inferred.idField = pickField(sample, ["recUuid", "rec_uuid", "uuid", "id"]) || inferred.idField;
|
||||
inferred.linkStrategy = inferred.urlField ? "auto" : "id";
|
||||
inferred.linkTemplate = "https://xxx.xxx.xxx?recuuid={recUuid}";
|
||||
}
|
||||
inferred.tableColumns = buildDefaultTableColumns(sample, inferred);
|
||||
|
||||
inferred.tableColumns = buildDefaultTableColumns(sample, inferred);
|
||||
return inferred;
|
||||
}
|
||||
|
||||
@@ -192,14 +141,18 @@ function stringify(value: unknown, fallback = ""): string {
|
||||
}
|
||||
|
||||
function renderTemplate(template: string, item: Record<string, unknown>): string {
|
||||
return template.replace(/\{([^}]+)\}/g, (_match, key: string) => encodeURIComponent(stringify(item[key])));
|
||||
return template.replace(/\{([^}]+)\}/g, (_match, key: string) =>
|
||||
encodeURIComponent(stringify(item[key])),
|
||||
);
|
||||
}
|
||||
|
||||
function resolveLink(item: Record<string, unknown>, config: SkillDisplayConfig): string | undefined {
|
||||
const url = config.urlField ? stringify(item[config.urlField]) : "";
|
||||
const id = config.idField ? stringify(item[config.idField]) : "";
|
||||
if (config.linkStrategy === "url") return url || undefined;
|
||||
if (config.linkStrategy === "id") return id && config.linkTemplate ? renderTemplate(config.linkTemplate, item) : undefined;
|
||||
if (config.linkStrategy === "id") {
|
||||
return id && config.linkTemplate ? renderTemplate(config.linkTemplate, item) : undefined;
|
||||
}
|
||||
return url || (id && config.linkTemplate ? renderTemplate(config.linkTemplate, item) : undefined) || undefined;
|
||||
}
|
||||
|
||||
@@ -217,7 +170,10 @@ export function normalizeDisplaySources(payload: unknown, config: SkillDisplayCo
|
||||
content: config.contentField ? stringify(item[config.contentField]) : undefined,
|
||||
source: config.sourceField ? stringify(item[config.sourceField]) : undefined,
|
||||
time: config.timeField ? stringify(item[config.timeField]) : undefined,
|
||||
score: config.scoreField && typeof item[config.scoreField] === "number" ? item[config.scoreField] as number : undefined,
|
||||
score:
|
||||
config.scoreField && typeof item[config.scoreField] === "number"
|
||||
? (item[config.scoreField] as number)
|
||||
: undefined,
|
||||
url: resolveLink(item, config),
|
||||
raw: item,
|
||||
}));
|
||||
|
||||
@@ -17,6 +17,12 @@ export type RuntimeDisplaySource = DisplaySourceItem & {
|
||||
|
||||
export type SkillDisplayMap = Map<string, SkillDisplayConfig>;
|
||||
|
||||
type RuntimeToolCall = {
|
||||
id?: string;
|
||||
name: string;
|
||||
args?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export function buildSkillDisplayMap(skills: Skill[]): SkillDisplayMap {
|
||||
const map = new Map<string, SkillDisplayConfig>();
|
||||
for (const skill of skills) {
|
||||
@@ -163,13 +169,13 @@ export function collectRuntimeDisplaySources(
|
||||
if (!message || message.type !== "ai") {
|
||||
continue;
|
||||
}
|
||||
for (const toolCall of message.tool_calls ?? []) {
|
||||
for (const toolCall of extractRuntimeToolCalls(message)) {
|
||||
const toolMatch = findDisplayConfigForTool(
|
||||
toolCall.name,
|
||||
displays,
|
||||
toolCall.args as Record<string, unknown> | undefined,
|
||||
toolCall.args,
|
||||
);
|
||||
const result = findToolResultPayload(toolCall.id, messages);
|
||||
const result = findToolResultPayload(toolCall, messages, index);
|
||||
if (result === undefined) {
|
||||
continue;
|
||||
}
|
||||
@@ -192,7 +198,7 @@ export function collectRuntimeDisplaySources(
|
||||
: shouldUseDefaultCitationMode(
|
||||
toolCall.name,
|
||||
result,
|
||||
toolCall.args as Record<string, unknown> | undefined,
|
||||
toolCall.args,
|
||||
)
|
||||
? normalizeDefaultDisplaySources(result)
|
||||
: [];
|
||||
@@ -214,6 +220,80 @@ export function collectRuntimeDisplaySources(
|
||||
return sources;
|
||||
}
|
||||
|
||||
function extractRuntimeToolCalls(message: Message): RuntimeToolCall[] {
|
||||
const calls: RuntimeToolCall[] = [];
|
||||
const direct = (message as { tool_calls?: unknown }).tool_calls;
|
||||
if (Array.isArray(direct)) {
|
||||
for (const raw of direct) {
|
||||
const call = normalizeRuntimeToolCall(raw);
|
||||
if (call) {
|
||||
calls.push(call);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const kwargs = (message as { additional_kwargs?: Record<string, unknown> })
|
||||
.additional_kwargs;
|
||||
const extra = kwargs?.tool_calls;
|
||||
if (Array.isArray(extra)) {
|
||||
for (const raw of extra) {
|
||||
const call = normalizeRuntimeToolCall(raw);
|
||||
if (call && !calls.some((item) => item.id && item.id === call.id)) {
|
||||
calls.push(call);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const chunks = (message as { tool_call_chunks?: unknown }).tool_call_chunks;
|
||||
if (Array.isArray(chunks)) {
|
||||
for (const raw of chunks) {
|
||||
const call = normalizeRuntimeToolCall(raw);
|
||||
if (call && !calls.some((item) => item.id && item.id === call.id)) {
|
||||
calls.push(call);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return calls;
|
||||
}
|
||||
|
||||
function normalizeRuntimeToolCall(raw: unknown): RuntimeToolCall | null {
|
||||
if (!isRecord(raw)) {
|
||||
return null;
|
||||
}
|
||||
const fn = isRecord(raw.function) ? raw.function : null;
|
||||
const name =
|
||||
stringifyMaybe(raw.name) ??
|
||||
stringifyMaybe(fn?.name) ??
|
||||
stringifyMaybe(raw.tool_name);
|
||||
if (!name) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id:
|
||||
stringifyMaybe(raw.id) ??
|
||||
stringifyMaybe(raw.tool_call_id) ??
|
||||
stringifyMaybe(raw.call_id),
|
||||
name,
|
||||
args: normalizeToolArgs(raw.args ?? raw.arguments ?? fn?.arguments),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeToolArgs(value: unknown): Record<string, unknown> | undefined {
|
||||
if (isRecord(value)) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return isRecord(parsed) ? parsed : undefined;
|
||||
} catch {
|
||||
return { command: value };
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function findDisplayConfigForPayload(
|
||||
toolName: string,
|
||||
payload: unknown,
|
||||
@@ -445,16 +525,18 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return !!value && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function stringifyMaybe(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim() ? value : undefined;
|
||||
}
|
||||
|
||||
function findToolResultPayload(
|
||||
toolCallId: string | undefined,
|
||||
toolCall: RuntimeToolCall,
|
||||
messages: Message[],
|
||||
toolCallMessageIndex = -1,
|
||||
): unknown {
|
||||
if (!toolCallId) {
|
||||
return undefined;
|
||||
}
|
||||
const toolMessage = messages.find(
|
||||
(message) => message.type === "tool" && message.tool_call_id === toolCallId,
|
||||
);
|
||||
const toolMessage =
|
||||
findToolMessageById(toolCall.id, messages) ??
|
||||
findToolMessageByName(toolCall, messages, toolCallMessageIndex);
|
||||
if (!toolMessage) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -473,6 +555,73 @@ function findToolResultPayload(
|
||||
return content;
|
||||
}
|
||||
|
||||
function findToolMessageById(
|
||||
toolCallId: string | undefined,
|
||||
messages: Message[],
|
||||
): Message | undefined {
|
||||
if (!toolCallId) {
|
||||
return undefined;
|
||||
}
|
||||
return messages.find((message) => {
|
||||
if (message.type !== "tool") {
|
||||
return false;
|
||||
}
|
||||
const candidates = [
|
||||
(message as { tool_call_id?: unknown }).tool_call_id,
|
||||
(message as { id?: unknown }).id,
|
||||
(message as { additional_kwargs?: Record<string, unknown> }).additional_kwargs
|
||||
?.tool_call_id,
|
||||
(message as { additional_kwargs?: Record<string, unknown> }).additional_kwargs
|
||||
?.id,
|
||||
];
|
||||
return candidates.some((candidate) => candidate === toolCallId);
|
||||
});
|
||||
}
|
||||
|
||||
function findToolMessageByName(
|
||||
toolCall: RuntimeToolCall,
|
||||
messages: Message[],
|
||||
toolCallMessageIndex: number,
|
||||
): Message | undefined {
|
||||
const start = Math.max(toolCallMessageIndex + 1, 0);
|
||||
for (let index = start; index < messages.length; index++) {
|
||||
const message = messages[index];
|
||||
if (!message) {
|
||||
continue;
|
||||
}
|
||||
if (index > start && (message.type === "human" || message.type === "ai")) {
|
||||
break;
|
||||
}
|
||||
if (message.type !== "tool") {
|
||||
continue;
|
||||
}
|
||||
const name =
|
||||
stringifyMaybe((message as { name?: unknown }).name) ??
|
||||
stringifyMaybe(
|
||||
(message as { additional_kwargs?: Record<string, unknown> }).additional_kwargs
|
||||
?.name,
|
||||
);
|
||||
const messageToolCallId = stringifyMaybe(
|
||||
(message as { tool_call_id?: unknown }).tool_call_id,
|
||||
);
|
||||
if (messageToolCallId && toolCall.id && messageToolCallId !== toolCall.id) {
|
||||
continue;
|
||||
}
|
||||
if (name && normalizeName(name) !== normalizeName(toolCall.name)) {
|
||||
continue;
|
||||
}
|
||||
const payload = extractStructuredToolPayload(message.content);
|
||||
if (payload !== undefined) {
|
||||
return message;
|
||||
}
|
||||
const text = extractToolText(message.content);
|
||||
if (text && parseJSONLikeText(text) !== undefined) {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function extractStructuredToolPayload(content: unknown): unknown {
|
||||
if (isRecord(content)) {
|
||||
return unwrapToolPayloadObject(content);
|
||||
|
||||
Reference in new issue
Block a user