完善细节

This commit is contained in:
BBIT-Kai
2026-07-17 09:53:02 +08:00
parent a84173066e
commit 89f6944e65
4 changed files with 2835 additions and 86 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ def get_cube_report_settings() -> CubeReportSettings:
"DIFY_DATABASE_ASSISTANT_API_BASE", "https://chat.bbitcn.net/v1"
).rstrip("/"),
dify_api_key=os.getenv(
"DIFY_DATABASE_ASSISTANT_API_KEY", "app-uibWo8ZEpqHCsWXREPTCBDH6"
"DIFY_DATABASE_ASSISTANT_API_KEY", "app-Ojd3zT2wBsO2Bop575ZiHQOD"
),
cube_api_base=os.getenv(
"CUBE_API_BASE_URL", "http://10.10.12.101:4001/cubejs-api/v1"
+31 -10
View File
@@ -138,6 +138,32 @@ function getFetchConfig() {
};
}
async function authorizedFetch(path: string, init: RequestInit) {
const request = () => {
const { authorization, baseURL } = getFetchConfig();
const headers = new Headers(init.headers);
if (authorization) {
headers.set('Authorization', authorization);
} else {
headers.delete('Authorization');
}
return fetch(`${baseURL}${path}`, { ...init, headers });
};
let response = await request();
if (response.status !== 401) return response;
try {
// 标准请求客户端会使用项目现有的 refresh token 机制更新令牌。
await getCubeReportScope();
} catch {
return response;
}
await response.body?.cancel();
response = await request();
return response;
}
async function responseError(response: Response) {
const text = await response.text();
try {
@@ -272,12 +298,10 @@ export async function stopCubeReportTask(taskId: string) {
}
export async function uploadCubeReportFile(file: File) {
const { authorization, baseURL } = getFetchConfig();
const form = new FormData();
form.append('file', file);
const response = await fetch(`${baseURL}/llm/cube-report/files/upload`, {
const response = await authorizedFetch('/llm/cube-report/files/upload', {
method: 'POST',
headers: { Authorization: authorization },
body: form,
});
if (!response.ok) throw new Error(await responseError(response));
@@ -291,14 +315,13 @@ export async function downloadCubeReportFile(
conversationId: string,
file: CubeReportFile,
) {
const { authorization, baseURL } = getFetchConfig();
const params = new URLSearchParams({
conversationId,
asAttachment: 'true',
});
const response = await fetch(
`${baseURL}/llm/cube-report/files/${file.id}/preview?${params}`,
{ headers: { Authorization: authorization } },
const response = await authorizedFetch(
`/llm/cube-report/files/${file.id}/preview?${params}`,
{},
);
if (!response.ok) throw new Error(await responseError(response));
const blob = await response.blob();
@@ -314,12 +337,10 @@ export async function streamCubeReportMessage(
payload: SendCubeReportMessage,
onEvent: (event: CubeReportStreamEvent) => void,
) {
const { authorization, baseURL } = getFetchConfig();
const response = await fetch(`${baseURL}/llm/cube-report/messages/stream`, {
const response = await authorizedFetch('/llm/cube-report/messages/stream', {
method: 'POST',
headers: {
Accept: 'text/event-stream',
Authorization: authorization,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
@@ -381,49 +381,64 @@ async function sendMessage() {
let completedConversationId = currentSessionId.value;
let completedWithData = false;
try {
await api.streamCubeReportMessage(
{
content,
conversationId: currentSessionId.value,
files: sentFiles,
tenantId: currentSession.value?.tenantId || selectedCompany.value?.id,
tenantName:
currentSession.value?.tenantName || selectedCompany.value?.name,
},
(event: CubeReportStreamEvent) => {
switch (event.type) {
case 'complete': {
completedConversationId =
event.conversationId || completedConversationId;
completedWithData = event.hasData;
assistantMessage.status = 'completed';
if (event.hasData) activeView.value = 'split';
break;
try {
await api.streamCubeReportMessage(
{
content,
conversationId: currentSessionId.value,
files: sentFiles,
tenantId: currentSession.value?.tenantId || selectedCompany.value?.id,
tenantName:
currentSession.value?.tenantName || selectedCompany.value?.name,
},
(event: CubeReportStreamEvent) => {
switch (event.type) {
case 'complete': {
completedConversationId =
event.conversationId || completedConversationId;
completedWithData = event.hasData;
assistantMessage.status = 'completed';
if (event.hasData) activeView.value = 'split';
break;
}
case 'message_delta': {
assistantMessage.content += event.content;
void scrollToBottom();
break;
}
case 'message_replace': {
assistantMessage.content = event.content;
break;
}
case 'status': {
streamStatus.value = event.text;
break;
}
case 'task': {
currentTaskId.value = event.taskId;
break;
}
case 'error': {
throw new Error(event.message);
}
// No default
}
case 'message_delta': {
assistantMessage.content += event.content;
void scrollToBottom();
break;
}
case 'message_replace': {
assistantMessage.content = event.content;
break;
}
case 'status': {
streamStatus.value = event.text;
break;
}
case 'task': {
currentTaskId.value = event.taskId;
break;
}
case 'error': {
throw new Error(event.message);
}
// No default
}
},
);
},
);
} catch (error) {
const text =
error instanceof Error ? error.message : '请求失败,请稍后重试';
assistantMessage.status = stopRequested.value ? 'completed' : 'failed';
assistantMessage.content = stopRequested.value
? assistantMessage.content || '响应已停止。'
: `请求失败:${text}`;
if (!stopRequested.value) {
inputMessage.value = content;
pendingFiles.value = sentFiles;
message.error(text);
}
return;
}
if (!assistantMessage.content) {
assistantMessage.content = stopRequested.value
@@ -431,30 +446,24 @@ async function sendMessage() {
: '查询已完成。';
}
if (completedConversationId) {
currentSessionId.value = completedConversationId;
const detail = await api.getCubeReportSession(completedConversationId);
currentSession.value = detail.session;
messages.value = detail.messages;
if (currentReportId.value) {
currentReport.value = await api.getCubeReport(currentReportId.value);
try {
currentSessionId.value = completedConversationId;
const detail = await api.getCubeReportSession(completedConversationId);
currentSession.value = detail.session;
messages.value = detail.messages;
if (currentReportId.value) {
currentReport.value = await api.getCubeReport(currentReportId.value);
}
await reloadNavigation();
if (completedWithData || detail.session.hasData) {
activeView.value = 'split';
await loadData(1);
}
} catch (error) {
const text =
error instanceof Error ? error.message : '会话和数据刷新失败';
message.warning(`回复已完成,但数据刷新失败:${text}`);
}
await reloadNavigation();
if (completedWithData || detail.session.hasData) {
activeView.value = 'split';
await loadData(1);
}
}
} catch (error) {
const text =
error instanceof Error ? error.message : '请求失败,请稍后重试';
assistantMessage.status = stopRequested.value ? 'completed' : 'failed';
assistantMessage.content = stopRequested.value
? assistantMessage.content || '响应已停止。'
: `请求失败:${text}`;
if (!stopRequested.value) {
inputMessage.value = content;
pendingFiles.value = sentFiles;
message.error(text);
}
} finally {
sending.value = false;
@@ -465,6 +474,12 @@ async function sendMessage() {
}
}
function sendSuggestedQuestion(question: string) {
if (sending.value) return;
inputMessage.value = question;
void sendMessage();
}
async function stopResponse() {
if (!currentTaskId.value || stopping.value) return;
stopping.value = true;
@@ -874,11 +889,11 @@ onBeforeUnmount(() => {
v-for="session in sessions"
:key="session.id"
type="button"
class="session-item"
:class="{
'session-item-active':
!currentReportId && currentSessionId === session.id,
}"
class="session-item"
:class="{
'session-item-active':
!currentReportId && currentSessionId === session.id,
}"
@click="loadConversation(session.id)"
>
<div class="flex items-start gap-3">
@@ -1183,17 +1198,14 @@ onBeforeUnmount(() => {
v-for="(question, index) in suggestedQuestions"
:key="question"
type="button"
@click="inputMessage = question"
:disabled="sending"
@click="sendSuggestedQuestion(question)"
>
<IconifyIcon
:icon="suggestionIcons[index % suggestionIcons.length]!"
/>
<span>{{ question }}</span>
</button>
<button type="button" @click="fileInput?.click()">
<IconifyIcon icon="lucide:file-up" />
<span>上传 Excel 辅助分析</span>
</button>
</div>
</div>
</Spin>
File diff suppressed because it is too large Load Diff