Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into gzy_iframedf
This commit is contained in:
commit
a5b65f4087
91 files changed
+3731
-2141
No files matched your search
@@ -17,24 +17,70 @@ export async function listAllUsers(): Promise<UserListItem[]> {
|
||||
return res.json() as Promise<UserListItem[]>;
|
||||
}
|
||||
|
||||
// Normalize a FastAPI error body into a human-readable string. `detail` may be
|
||||
// a plain string, an object like AuthErrorResponse ({ code, message }), or an
|
||||
// array of validation errors — anything but a string would otherwise render as
|
||||
// the dreaded "[object Object]".
|
||||
function extractErrorMessage(body: unknown, fallback = "Login failed."): string {
|
||||
const detail = (body as { detail?: unknown } | null)?.detail ?? body;
|
||||
if (typeof detail === "string") return detail;
|
||||
if (Array.isArray(detail)) {
|
||||
const msg = detail
|
||||
.map((d) => (d && typeof d === "object" ? (d as { msg?: string }).msg : String(d)))
|
||||
.filter(Boolean)
|
||||
.join("; ");
|
||||
return msg || fallback;
|
||||
}
|
||||
if (detail && typeof detail === "object") {
|
||||
const obj = detail as { message?: string; msg?: string };
|
||||
return obj.message ?? obj.msg ?? fallback;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export async function loginByUsername(
|
||||
username: string,
|
||||
password?: string | null,
|
||||
): Promise<LoginByUsernameResponse> {
|
||||
// When the username-login password gate is enabled server-side, the shared
|
||||
// secret must be passed as ?password=. Omitted when no password is provided
|
||||
// so the passwordless flow keeps working unchanged.
|
||||
const base = `${getBackendBaseURL()}/api/v1/auth/login/username`;
|
||||
const url =
|
||||
password != null && password !== ""
|
||||
? `${base}?password=${encodeURIComponent(password)}`
|
||||
: base;
|
||||
const response = await apiFetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ username }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => null);
|
||||
throw new Error(extractErrorMessage(error));
|
||||
}
|
||||
return response.json() as Promise<LoginByUsernameResponse>;
|
||||
}
|
||||
|
||||
export async function loginByToken(
|
||||
token: string,
|
||||
): Promise<LoginByUsernameResponse> {
|
||||
// Token-exchange login: the gateway resolves the upstream token to a username
|
||||
// and signs the matching local account in.
|
||||
const response = await apiFetch(
|
||||
`${getBackendBaseURL()}/api/v1/auth/login/username`,
|
||||
`${getBackendBaseURL()}/api/v1/auth/login/token?token=${encodeURIComponent(token)}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ username }),
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
const error = await response
|
||||
.json()
|
||||
.catch(() => ({ detail: "Login failed." }));
|
||||
throw new Error(error.detail ?? "Login failed.");
|
||||
const error = await response.json().catch(() => null);
|
||||
throw new Error(extractErrorMessage(error));
|
||||
}
|
||||
return response.json() as Promise<LoginByUsernameResponse>;
|
||||
}
|
||||
|
||||
@@ -51,7 +51,14 @@ export function readExternalLoginParams(): Required<ExternalLoginParams> {
|
||||
}
|
||||
}
|
||||
|
||||
/** 把 ``theme`` query 值映射成 ThemeContext 的 ``'dark' | 'light'`` */
|
||||
/**
|
||||
* 把外部(URL query / 无界 props)的 ``theme`` 值映射成 ThemeContext 的
|
||||
* ``'dark' | 'light'``。已知取值:
|
||||
* - ``dark-blue`` → 深色
|
||||
* - ``red`` → 浅色(当前默认红白主题)
|
||||
* - ``blue`` → 暂无该主题,先用浅色代替(命中 else 分支)
|
||||
* 其余/缺省同样回退浅色。将来若新增 blue 主题,在此扩展映射即可。
|
||||
*/
|
||||
export function resolveThemeFromQuery(themeParam: string | null | undefined): "dark" | "light" {
|
||||
return themeParam === "dark-blue" ? "dark" : "light";
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* 无界(wujie)子应用运行时 props 读取。
|
||||
*
|
||||
* 本应用作为子应用被内网门户用无界嵌套时,父级通过
|
||||
* <WujieVue name="subApp" :url="..." :props="{ ticket, userId, theme, env }" />
|
||||
* 注入参数,子应用在 `window.$wujie.props` 上拿到**同一个对象**(无界在子应用
|
||||
* bootstrap 之前就已注入,因此首帧即可读)。
|
||||
*
|
||||
* 本期范围:只「读出来 + 存起来 + 打印」,**不接入对方鉴权**。读出的
|
||||
* ticket/userId/env 复用 external-login 的 localStorage 存储(与 URL 跳转登录
|
||||
* 同一套 key),theme 交给调用方(LoginPage)映射到 ThemeContext。
|
||||
*/
|
||||
import { useEffect, useMemo } from "react";
|
||||
|
||||
import { persistExternalLoginParams } from "@/core/auth/external-login";
|
||||
|
||||
/** 无界父级注入的 props 形状。除已知字段外允许透传其余未知字段便于排查。 */
|
||||
export interface WujieProps {
|
||||
/** 第三方鉴权 ticket(本期仅存储待用,不消费)。 */
|
||||
ticket?: string;
|
||||
/** 外部用户 id。 */
|
||||
userId?: string;
|
||||
/** 环境标识。 */
|
||||
env?: string;
|
||||
/** 主题色标识(映射到深/浅色)。 */
|
||||
theme?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface WujieRuntime {
|
||||
props?: WujieProps;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
/** 无界注入的子应用运行时;非无界环境下为 undefined。 */
|
||||
$wujie?: WujieRuntime;
|
||||
/** 无界注入的环境标志(子应用内为 true)。 */
|
||||
__POWERED_BY_WUJIE__?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
/** 当前是否运行在无界子应用环境中。 */
|
||||
export function isWujieEnv(): boolean {
|
||||
return typeof window !== "undefined" && !!window.__POWERED_BY_WUJIE__;
|
||||
}
|
||||
|
||||
/** 读取无界父级注入的 props;不在无界环境 / 未注入时返回空对象。 */
|
||||
export function readWujieProps(): WujieProps {
|
||||
if (typeof window === "undefined") return {};
|
||||
return window.$wujie?.props ?? {};
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局读取无界父级注入的 props 的 hook。
|
||||
*
|
||||
* - 首帧从 `window.$wujie.props` 读出(无界已在 bootstrap 前注入,引用稳定);
|
||||
* - 读到后把 ticket/userId/env 持久化到 localStorage(复用 external-login 存储),
|
||||
* 供后续任意页面/请求读取 —— 即「存到全局」;
|
||||
* - 返回 props 供调用方消费(如 LoginPage 取 theme)。
|
||||
*
|
||||
* 非无界环境(直接访问)下返回空对象且不做任何持久化,可安全无条件调用。
|
||||
*/
|
||||
export function useWujieProps(): WujieProps {
|
||||
const props = useMemo(() => readWujieProps(), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.ticket && !props.userId && !props.env) return;
|
||||
persistExternalLoginParams({
|
||||
ticket: props.ticket,
|
||||
userId: props.userId,
|
||||
env: props.env,
|
||||
});
|
||||
}, [props.ticket, props.userId, props.env]);
|
||||
|
||||
return props;
|
||||
}
|
||||
@@ -462,10 +462,10 @@ export function parseUploadedFiles(content: string): FileInMessage[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if the backend reported no new files were uploaded in this message
|
||||
if (uploadedFilesContent?.includes("(empty)")) {
|
||||
return [];
|
||||
}
|
||||
// NOTE: do NOT bail on "(empty)" here. The backend writes "(empty)" only for
|
||||
// the "uploaded in this message" section; files uploaded in previous messages
|
||||
// are still listed below it. Bailing dropped those valid entries. The file
|
||||
// regex below naturally skips the "(empty)" line, so parsing stays correct.
|
||||
|
||||
// Parse file list
|
||||
// Format: - filename (size)\n Path: /path/to/file
|
||||
|
||||
@@ -4,11 +4,12 @@ export const DEFAULT_LOCAL_SETTINGS: LocalSettings = {
|
||||
notification: {
|
||||
enabled: true,
|
||||
},
|
||||
display: {
|
||||
streamSpeedMeterEnabled: true,
|
||||
ragModeEnabled: false,
|
||||
citationCleanupWords: [],
|
||||
},
|
||||
display: {
|
||||
streamSpeedMeterEnabled: true,
|
||||
ragModeEnabled: false,
|
||||
citationCleanupWords: [],
|
||||
fontScale: 1,
|
||||
},
|
||||
context: {
|
||||
model_name: undefined,
|
||||
mode: undefined,
|
||||
@@ -17,9 +18,9 @@ export const DEFAULT_LOCAL_SETTINGS: LocalSettings = {
|
||||
},
|
||||
};
|
||||
|
||||
export const LOCAL_SETTINGS_KEY = "deerflow.local-settings";
|
||||
export const THREAD_MODEL_KEY_PREFIX = "deerflow.thread-model.";
|
||||
export const THREAD_RAG_MODE_KEY_PREFIX = "deerflow.thread-rag-mode.";
|
||||
export const LOCAL_SETTINGS_KEY = "deerflow.local-settings";
|
||||
export const THREAD_MODEL_KEY_PREFIX = "deerflow.thread-model.";
|
||||
export const THREAD_RAG_MODE_KEY_PREFIX = "deerflow.thread-rag-mode.";
|
||||
|
||||
function isBrowser(): boolean {
|
||||
return typeof window !== "undefined";
|
||||
@@ -40,13 +41,20 @@ export interface LocalSettings {
|
||||
* Global switch for skill result display adapters. When disabled, chat
|
||||
* messages render tool results with the legacy generic search UI only.
|
||||
*/
|
||||
ragModeEnabled: boolean;
|
||||
/**
|
||||
* Admin-maintained words or short phrases removed from reference snippets
|
||||
* and dialogs, one entry per line in the settings UI.
|
||||
*/
|
||||
citationCleanupWords: string[];
|
||||
};
|
||||
ragModeEnabled: boolean;
|
||||
/**
|
||||
* Admin-maintained words or short phrases removed from reference snippets
|
||||
* and dialogs, one entry per line in the settings UI.
|
||||
*/
|
||||
citationCleanupWords: string[];
|
||||
/**
|
||||
* Global UI font scale multiplier applied to the root <html> font-size.
|
||||
* Base is 16px (deep-blue UI spec §2.1.2 body text), so `1` === 16px.
|
||||
* The user picks a size from the appearance settings page; because the
|
||||
* app sizes text/spacing in rem, the whole UI scales proportionally.
|
||||
*/
|
||||
fontScale: number;
|
||||
};
|
||||
context: Omit<
|
||||
AgentThreadContext,
|
||||
| "thread_id"
|
||||
@@ -91,10 +99,10 @@ export function getThreadModelName(threadId: string): string | undefined {
|
||||
return localStorage.getItem(getThreadModelStorageKey(threadId)) ?? undefined;
|
||||
}
|
||||
|
||||
export function saveThreadModelName(
|
||||
threadId: string,
|
||||
modelName: string | undefined,
|
||||
) {
|
||||
export function saveThreadModelName(
|
||||
threadId: string,
|
||||
modelName: string | undefined,
|
||||
) {
|
||||
if (!isBrowser()) {
|
||||
return;
|
||||
}
|
||||
@@ -103,26 +111,26 @@ export function saveThreadModelName(
|
||||
localStorage.removeItem(key);
|
||||
return;
|
||||
}
|
||||
localStorage.setItem(key, modelName);
|
||||
}
|
||||
|
||||
function getThreadRagModeStorageKey(threadId: string): string {
|
||||
return `${THREAD_RAG_MODE_KEY_PREFIX}${threadId}`;
|
||||
}
|
||||
|
||||
export function isThreadRagModeLocked(threadId: string): boolean {
|
||||
if (!isBrowser() || !threadId) {
|
||||
return false;
|
||||
}
|
||||
return localStorage.getItem(getThreadRagModeStorageKey(threadId)) === "true";
|
||||
}
|
||||
|
||||
export function lockThreadRagMode(threadId: string) {
|
||||
if (!isBrowser() || !threadId) {
|
||||
return;
|
||||
}
|
||||
localStorage.setItem(getThreadRagModeStorageKey(threadId), "true");
|
||||
}
|
||||
localStorage.setItem(key, modelName);
|
||||
}
|
||||
|
||||
function getThreadRagModeStorageKey(threadId: string): string {
|
||||
return `${THREAD_RAG_MODE_KEY_PREFIX}${threadId}`;
|
||||
}
|
||||
|
||||
export function isThreadRagModeLocked(threadId: string): boolean {
|
||||
if (!isBrowser() || !threadId) {
|
||||
return false;
|
||||
}
|
||||
return localStorage.getItem(getThreadRagModeStorageKey(threadId)) === "true";
|
||||
}
|
||||
|
||||
export function lockThreadRagMode(threadId: string) {
|
||||
if (!isBrowser() || !threadId) {
|
||||
return;
|
||||
}
|
||||
localStorage.setItem(getThreadRagModeStorageKey(threadId), "true");
|
||||
}
|
||||
|
||||
export function applyThreadModelOverride(
|
||||
settings: LocalSettings,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { SkillDisplayConfig } from "@/core/skills/type";
|
||||
import type {
|
||||
SkillDisplayConfig,
|
||||
SkillDisplayResultSetConfig,
|
||||
} from "@/core/skills/type";
|
||||
|
||||
const DEFAULT_CITATION_MAX_ITEMS = 100;
|
||||
const DEFAULT_LIST_MAX_ITEMS = 20;
|
||||
@@ -15,6 +18,10 @@ export interface DisplaySourceItem {
|
||||
url?: string;
|
||||
content?: string;
|
||||
score?: number;
|
||||
displayMode?: SkillDisplayConfig["mode"];
|
||||
resultSetKey?: string;
|
||||
resultSetLabel?: string | null;
|
||||
tableColumns?: SkillDisplayConfig["tableColumns"];
|
||||
raw: Record<string, unknown>;
|
||||
}
|
||||
|
||||
@@ -38,6 +45,7 @@ export const emptyDisplayConfig = (): SkillDisplayConfig => ({
|
||||
maxItems: DEFAULT_CITATION_MAX_ITEMS,
|
||||
customInstruction: "",
|
||||
tableColumns: [],
|
||||
resultSets: [],
|
||||
});
|
||||
|
||||
const candidateFields: Record<string, string[]> = {
|
||||
@@ -53,7 +61,7 @@ const candidateFields: Record<string, string[]> = {
|
||||
idField: ["recUuid", "rec_uuid", "uuid", "docId", "taskId", "recordId", "id"],
|
||||
};
|
||||
|
||||
function getByPath(value: unknown, path: string): unknown {
|
||||
function getByPath(value: unknown, path: string | null | undefined): unknown {
|
||||
if (!path) return value;
|
||||
return path.split(".").reduce<unknown>((acc, key) => {
|
||||
if (acc && typeof acc === "object") {
|
||||
@@ -63,19 +71,55 @@ function getByPath(value: unknown, path: string): unknown {
|
||||
}, value);
|
||||
}
|
||||
|
||||
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>[] };
|
||||
export type ResultArrayCandidate = {
|
||||
key: string;
|
||||
label: string;
|
||||
path: string;
|
||||
items: Record<string, unknown>[];
|
||||
};
|
||||
|
||||
export function findResultArrayCandidates(samplePayload: unknown): ResultArrayCandidate[] {
|
||||
const candidates: ResultArrayCandidate[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
function visit(value: unknown, path = "", depth = 0) {
|
||||
if (depth > 8 || candidates.length >= 20) return;
|
||||
if (Array.isArray(value)) {
|
||||
const objectItems = value.filter(
|
||||
(item): item is Record<string, unknown> =>
|
||||
!!item && typeof item === "object" && !Array.isArray(item),
|
||||
);
|
||||
if (
|
||||
(value.length === 0 || objectItems.length > 0) &&
|
||||
objectItems.length === value.length &&
|
||||
!seen.has(path)
|
||||
) {
|
||||
seen.add(path);
|
||||
candidates.push({
|
||||
key: buildResultSetKey(path || "root"),
|
||||
label: buildResultSetLabel(path || "根数组", objectItems.length),
|
||||
path,
|
||||
items: objectItems,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
||||
for (const [key, child] of Object.entries(value)) {
|
||||
visit(child, path ? `${path}.${key}` : key, depth + 1);
|
||||
}
|
||||
}
|
||||
if (!value || typeof value !== "object") return null;
|
||||
for (const [key, child] of Object.entries(value)) {
|
||||
const found = findFirstArray(child, path ? `${path}.${key}` : key);
|
||||
if (found) return found;
|
||||
}
|
||||
return null;
|
||||
|
||||
visit(samplePayload);
|
||||
return candidates;
|
||||
}
|
||||
|
||||
function buildResultSetKey(path: string): string {
|
||||
return path.replace(/[^a-zA-Z0-9_]+/g, "_").replace(/^_+|_+$/g, "") || "root";
|
||||
}
|
||||
|
||||
function buildResultSetLabel(path: string, count: number): string {
|
||||
return `${path}(${count} 条)`;
|
||||
}
|
||||
|
||||
function pickField(sample: Record<string, unknown>, candidates: string[]): string | null {
|
||||
@@ -93,13 +137,31 @@ function pickField(sample: Record<string, unknown>, candidates: string[]): strin
|
||||
}
|
||||
|
||||
export function inferDisplayConfig(samplePayload: unknown, skillName?: string): SkillDisplayConfig {
|
||||
const found = findFirstArray(samplePayload) ?? { path: "results", items: [] };
|
||||
const sample = found.items[0] ?? {};
|
||||
const candidates = findResultArrayCandidates(samplePayload);
|
||||
const found = candidates[0] ?? {
|
||||
key: "results",
|
||||
label: "results(0 条)",
|
||||
path: "results",
|
||||
items: [],
|
||||
};
|
||||
const inferred = buildConfigFromCandidate(found, skillName);
|
||||
inferred.resultSets =
|
||||
candidates.length > 1
|
||||
? candidates.map((candidate) => buildResultSetConfigFromCandidate(candidate, skillName))
|
||||
: [];
|
||||
return inferred;
|
||||
}
|
||||
|
||||
function buildConfigFromCandidate(
|
||||
candidate: ResultArrayCandidate,
|
||||
skillName?: string,
|
||||
): SkillDisplayConfig {
|
||||
const sample = candidate.items[0] ?? {};
|
||||
const inferred = emptyDisplayConfig();
|
||||
inferred.enabled = true;
|
||||
inferred.mode = skillName === "knowledge-search-v2" ? "citation" : "list";
|
||||
inferred.resultPath = found.path || "results";
|
||||
inferred.maxItems = inferMaxItems(found.items.length, inferred.mode);
|
||||
inferred.resultPath = candidate.path || "results";
|
||||
inferred.maxItems = inferMaxItems(candidate.items.length, inferred.mode);
|
||||
|
||||
for (const [target, candidates] of Object.entries(candidateFields)) {
|
||||
(inferred as unknown as Record<string, string | null>)[target] = pickField(sample, candidates);
|
||||
@@ -123,6 +185,65 @@ export function inferDisplayConfig(samplePayload: unknown, skillName?: string):
|
||||
return inferred;
|
||||
}
|
||||
|
||||
function buildResultSetConfigFromCandidate(
|
||||
candidate: ResultArrayCandidate,
|
||||
skillName?: string,
|
||||
): SkillDisplayResultSetConfig {
|
||||
const config = buildConfigFromCandidate(candidate, skillName);
|
||||
return {
|
||||
key: candidate.key,
|
||||
label: candidate.label,
|
||||
enabled: true,
|
||||
mode: config.mode,
|
||||
resultPath: config.resultPath,
|
||||
titleField: config.titleField,
|
||||
summaryField: config.summaryField,
|
||||
contentField: config.contentField,
|
||||
sourceField: config.sourceField,
|
||||
authorField: config.authorField,
|
||||
typeField: config.typeField,
|
||||
timeField: config.timeField,
|
||||
scoreField: config.scoreField,
|
||||
urlField: config.urlField,
|
||||
idField: config.idField,
|
||||
linkStrategy: config.linkStrategy,
|
||||
linkTemplate: config.linkTemplate,
|
||||
citationToken: config.citationToken,
|
||||
maxItems: config.maxItems,
|
||||
customInstruction: config.customInstruction,
|
||||
tableColumns: config.tableColumns,
|
||||
};
|
||||
}
|
||||
|
||||
export function displayConfigFromResultSet(
|
||||
base: SkillDisplayConfig,
|
||||
resultSet: SkillDisplayResultSetConfig,
|
||||
): SkillDisplayConfig {
|
||||
return {
|
||||
...base,
|
||||
enabled: resultSet.enabled,
|
||||
mode: resultSet.mode,
|
||||
resultPath: resultSet.resultPath,
|
||||
titleField: resultSet.titleField,
|
||||
summaryField: resultSet.summaryField,
|
||||
contentField: resultSet.contentField,
|
||||
sourceField: resultSet.sourceField,
|
||||
authorField: resultSet.authorField,
|
||||
typeField: resultSet.typeField,
|
||||
timeField: resultSet.timeField,
|
||||
scoreField: resultSet.scoreField,
|
||||
urlField: resultSet.urlField,
|
||||
idField: resultSet.idField,
|
||||
linkStrategy: resultSet.linkStrategy,
|
||||
linkTemplate: resultSet.linkTemplate,
|
||||
citationToken: resultSet.citationToken,
|
||||
maxItems: resultSet.maxItems,
|
||||
customInstruction: resultSet.customInstruction,
|
||||
tableColumns: resultSet.tableColumns,
|
||||
resultSets: [],
|
||||
};
|
||||
}
|
||||
|
||||
function buildDefaultTableColumns(
|
||||
sample: Record<string, unknown>,
|
||||
config: SkillDisplayConfig,
|
||||
@@ -161,13 +282,13 @@ 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])),
|
||||
encodeURIComponent(stringify(getByPath(item, key.trim()))),
|
||||
);
|
||||
}
|
||||
|
||||
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]) : "";
|
||||
const url = config.urlField ? stringify(getByPath(item, config.urlField)) : "";
|
||||
const id = config.idField ? stringify(getByPath(item, config.idField)) : "";
|
||||
if (config.linkStrategy === "url") return url || undefined;
|
||||
if (config.linkStrategy === "id") {
|
||||
return id && config.linkTemplate ? renderTemplate(config.linkTemplate, item) : undefined;
|
||||
@@ -176,6 +297,30 @@ function resolveLink(item: Record<string, unknown>, config: SkillDisplayConfig):
|
||||
}
|
||||
|
||||
export function normalizeDisplaySources(payload: unknown, config: SkillDisplayConfig): DisplaySourceItem[] {
|
||||
const configuredResultSets = config.resultSets ?? [];
|
||||
const resultSets = configuredResultSets.filter(
|
||||
(resultSet) => resultSet.enabled && resultSet.mode !== "none",
|
||||
);
|
||||
if (configuredResultSets.length > 0) {
|
||||
return resultSets.flatMap((resultSet) =>
|
||||
normalizeDisplaySourcesForConfig(
|
||||
payload,
|
||||
displayConfigFromResultSet(config, resultSet),
|
||||
resultSet,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (!config.enabled || config.mode === "none") {
|
||||
return [];
|
||||
}
|
||||
return normalizeDisplaySourcesForConfig(payload, config);
|
||||
}
|
||||
|
||||
function normalizeDisplaySourcesForConfig(
|
||||
payload: unknown,
|
||||
config: SkillDisplayConfig,
|
||||
resultSet?: SkillDisplayResultSetConfig,
|
||||
): DisplaySourceItem[] {
|
||||
const rawItems = getByPath(payload, config.resultPath);
|
||||
if (!Array.isArray(rawItems)) return [];
|
||||
const maxItems = resolveMaxItems(config, rawItems.length);
|
||||
@@ -183,19 +328,23 @@ export function normalizeDisplaySources(payload: unknown, config: SkillDisplayCo
|
||||
.filter((item): item is Record<string, unknown> => !!item && typeof item === "object")
|
||||
.slice(0, maxItems)
|
||||
.map((item, index) => ({
|
||||
id: config.idField ? stringify(item[config.idField]) : undefined,
|
||||
title: config.titleField ? stringify(item[config.titleField], `结果 ${index + 1}`) : `结果 ${index + 1}`,
|
||||
type: config.typeField ? stringify(item[config.typeField], "result") : "result",
|
||||
snippet: config.summaryField ? stringify(item[config.summaryField]) : "",
|
||||
content: config.contentField ? stringify(item[config.contentField]) : undefined,
|
||||
source: config.sourceField ? stringify(item[config.sourceField]) : undefined,
|
||||
author: config.authorField ? stringify(item[config.authorField]) : undefined,
|
||||
time: config.timeField ? stringify(item[config.timeField]) : undefined,
|
||||
id: config.idField ? stringify(getByPath(item, config.idField)) : undefined,
|
||||
title: config.titleField ? stringify(getByPath(item, config.titleField), `结果 ${index + 1}`) : `结果 ${index + 1}`,
|
||||
type: config.typeField ? stringify(getByPath(item, config.typeField), "result") : "result",
|
||||
snippet: config.summaryField ? stringify(getByPath(item, config.summaryField)) : "",
|
||||
content: config.contentField ? stringify(getByPath(item, config.contentField)) : undefined,
|
||||
source: config.sourceField ? stringify(getByPath(item, config.sourceField)) : undefined,
|
||||
author: config.authorField ? stringify(getByPath(item, config.authorField)) : undefined,
|
||||
time: config.timeField ? stringify(getByPath(item, config.timeField)) : undefined,
|
||||
score:
|
||||
config.scoreField && typeof item[config.scoreField] === "number"
|
||||
? (item[config.scoreField] as number)
|
||||
config.scoreField && typeof getByPath(item, config.scoreField) === "number"
|
||||
? (getByPath(item, config.scoreField) as number)
|
||||
: undefined,
|
||||
url: resolveLink(item, config),
|
||||
displayMode: config.mode,
|
||||
resultSetKey: resultSet?.key,
|
||||
resultSetLabel: resultSet?.label,
|
||||
tableColumns: config.tableColumns,
|
||||
raw: item,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -202,8 +202,8 @@ export function collectRuntimeDisplaySources(
|
||||
normalized.map((item) => ({
|
||||
...item,
|
||||
skillName: match?.skillName ?? toolCall.name,
|
||||
mode: effectiveMode,
|
||||
tableColumns: match?.display.tableColumns,
|
||||
mode: item.displayMode ?? effectiveMode,
|
||||
tableColumns: item.tableColumns ?? match?.display.tableColumns,
|
||||
})),
|
||||
seenCitationKeys,
|
||||
() => ++citationIndex,
|
||||
@@ -242,8 +242,8 @@ export function collectRuntimeDisplaySources(
|
||||
normalized.map((item) => ({
|
||||
...item,
|
||||
skillName: match?.skillName ?? toolName,
|
||||
mode: effectiveMode,
|
||||
tableColumns: match?.display.tableColumns,
|
||||
mode: item.displayMode ?? effectiveMode,
|
||||
tableColumns: item.tableColumns ?? match?.display.tableColumns,
|
||||
})),
|
||||
seenCitationKeys,
|
||||
() => ++citationIndex,
|
||||
|
||||
@@ -12,7 +12,9 @@ export interface SkillDisplayTableColumn {
|
||||
width?: string | null;
|
||||
}
|
||||
|
||||
export interface SkillDisplayConfig {
|
||||
export interface SkillDisplayResultSetConfig {
|
||||
key: string;
|
||||
label?: string | null;
|
||||
enabled: boolean;
|
||||
mode: SkillDisplayMode;
|
||||
resultPath: string;
|
||||
@@ -34,6 +36,10 @@ export interface SkillDisplayConfig {
|
||||
tableColumns?: SkillDisplayTableColumn[];
|
||||
}
|
||||
|
||||
export interface SkillDisplayConfig extends Omit<SkillDisplayResultSetConfig, "key" | "label"> {
|
||||
resultSets?: SkillDisplayResultSetConfig[];
|
||||
}
|
||||
|
||||
export interface Skill {
|
||||
name: string;
|
||||
description: string;
|
||||
|
||||
Reference in new issue
Block a user