技能展示样本运行器 + 参考文献来源过滤调整

- 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:
吉鑫升andClaude Opus 4.7 committed 2026-05-26 14:24:47 +08:00
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,
}));
+160 -11
View File
@@ -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);