diff --git a/api/apps/restful_apis/bot_api.py b/api/apps/restful_apis/bot_api.py index 0081157be8..8c0e6e7467 100644 --- a/api/apps/restful_apis/bot_api.py +++ b/api/apps/restful_apis/bot_api.py @@ -14,6 +14,7 @@ # limitations under the License. # import copy +import hashlib import json import re @@ -250,6 +251,54 @@ async def begin_inputs(agent_id, tenant_id=None): "prologue": canvas.get_prologue(), "mode": canvas.get_mode()}) +@manager.route("/agentbots//logs/", methods=["GET"]) # noqa: F821 +async def agent_bot_logs(shared_id, message_id): + # Beta-token sibling of /agents//logs/. + # Used by the shared/embedded chat page's "Thinking" button (fixes #14985). + # The path segment is just the value the client passed in the + # URL (it equals the beta token in the share flow); authentication comes + # from the Authorization header and the real agent_id is read from the + # looked-up APIToken so we never trust client-supplied identifiers. + from rag.utils.redis_conn import REDIS_CONN + + token = _get_sdk_authorization_token() + if not token: + logger.warning( + "agent_bot_logs: missing Authorization header (shared_id=%s message_id=%s)", + shared_id, message_id, + ) + return get_error_data_result(message='Authorization is not valid!') + # Non-reversible fingerprint of the share token: lets operators correlate + # auth-failure log lines for the same token without leaking a guessable + # substring of the secret itself. + token_fp = hashlib.sha256(token.encode("utf-8")).hexdigest()[:16] + objs = await thread_pool_exec(APIToken.query, beta=token) + if not objs: + logger.warning( + "agent_bot_logs: invalid beta token (fingerprint=%s shared_id=%s)", + token_fp, shared_id, + ) + return get_error_data_result(message='Authentication error: API key is invalid!"') + + agent_id = objs[0].dialog_id + if not agent_id: + logger.warning( + "agent_bot_logs: APIToken has no dialog_id (tenant_id=%s fingerprint=%s)", + objs[0].tenant_id, token_fp, + ) + return get_error_data_result(message='API token is not bound to an agent.') + + try: + binary = await thread_pool_exec(REDIS_CONN.get, f"{agent_id}-{message_id}-logs") + if not binary: + return get_json_result(data={}) + payload = binary.decode("utf-8") if isinstance(binary, bytes) else binary + return get_json_result(data=json.loads(payload)) + except Exception as exc: + logging.exception(exc) + return server_error_response(exc) + + @manager.route("/searchbots/ask", methods=["POST"]) # noqa: F821 @login_required(auth_types=AUTH_BETA) @add_tenant_id_to_kwargs diff --git a/web/src/hooks/use-agent-request.ts b/web/src/hooks/use-agent-request.ts index 715613f17e..f0728a4630 100644 --- a/web/src/hooks/use-agent-request.ts +++ b/web/src/hooks/use-agent-request.ts @@ -26,6 +26,7 @@ import agentService, { fetchAgentLogsByCanvasId, fetchAgentLogsById, fetchPipeLineList, + fetchSharedTrace, fetchTrace, fetchWebhookTrace, updateAgent, @@ -529,8 +530,12 @@ export const useUploadAgentFileWithProgress = (identifier?: string | null) => { return { data, loading, uploadAgentFile: mutateAsync }; }; -export const useFetchMessageTrace = (canvasId?: string) => { +export const useFetchMessageTrace = (canvasId?: string, isShare?: boolean) => { const { id } = useParams(); + // In shared mode there's no :id route param and `canvasId` actually carries + // the share (beta) APIToken — route through fetchSharedTrace so the request + // hits the beta-token-aware endpoint instead of /agents//logs which + // requires a session login (fixes #14985). const queryId = id || canvasId; const [messageId, setMessageId] = useState(''); const [isStopFetchTrace, setISStopFetchTrace] = useState(false); @@ -540,7 +545,7 @@ export const useFetchMessageTrace = (canvasId?: string) => { isFetching: loading, refetch, } = useQuery({ - queryKey: [AgentApiAction.Trace, queryId, messageId], + queryKey: [AgentApiAction.Trace, queryId, messageId, !!isShare], refetchOnReconnect: false, refetchOnMount: false, refetchOnWindowFocus: false, @@ -548,10 +553,15 @@ export const useFetchMessageTrace = (canvasId?: string) => { enabled: !!queryId && !!messageId, refetchInterval: !isStopFetchTrace ? 3000 : false, queryFn: async () => { - const { data } = await fetchTrace({ - canvas_id: queryId as string, - message_id: messageId, - }); + const { data } = isShare + ? await fetchSharedTrace({ + shared_id: queryId as string, + message_id: messageId, + }) + : await fetchTrace({ + canvas_id: queryId as string, + message_id: messageId, + }); return Array.isArray(data?.data) ? data?.data : []; }, diff --git a/web/src/pages/agent/log-sheet/workflow-timeline.tsx b/web/src/pages/agent/log-sheet/workflow-timeline.tsx index 4b8b3eed15..5a78f90b6a 100644 --- a/web/src/pages/agent/log-sheet/workflow-timeline.tsx +++ b/web/src/pages/agent/log-sheet/workflow-timeline.tsx @@ -103,7 +103,7 @@ export const WorkFlowTimeline = ({ data: traceData, setMessageId, setISStopFetchTrace, - } = useFetchMessageTrace(canvasId); + } = useFetchMessageTrace(canvasId, isShare); useEffect(() => { setMessageId(currentMessageId); diff --git a/web/src/services/agent-service.ts b/web/src/services/agent-service.ts index 92dcfeaf65..fee6709eae 100644 --- a/web/src/services/agent-service.ts +++ b/web/src/services/agent-service.ts @@ -154,6 +154,15 @@ export const fetchTrace = (data: { canvas_id: string; message_id: string }) => { }), ); }; + +// Used by the shared/embedded chat page where the only credential available +// is the share (beta) APIToken (fixes #14985). +export const fetchSharedTrace = (data: { + shared_id: string; + message_id: string; +}) => { + return request.get(api.sharedTrace(data.shared_id, data.message_id)); +}; export const fetchAgentLogsByCanvasId = ( canvasId: string, params: IAgentLogsRequest, diff --git a/web/src/utils/api.ts b/web/src/utils/api.ts index 690baf252f..806090d277 100644 --- a/web/src/utils/api.ts +++ b/web/src/utils/api.ts @@ -256,6 +256,8 @@ export default { `${restAPIv1}/agents/${agentId}/components/${componentId}/debug`, trace: (agentId: string, messageId: string) => `${restAPIv1}/agents/${agentId}/logs/${messageId}`, + sharedTrace: (sharedId: string, messageId: string) => + `${restAPIv1}/agentbots/${sharedId}/logs/${messageId}`, cancelCanvas: (taskId: string) => `${restAPIv1}/tasks/${taskId}/cancel`, // agent inputForm: (agentId: string, componentId: string) =>