Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,11 @@ PLATFORM_DB_PASSWORD=
API_BASE_URL=https://open.bigmodel.cn/api/paas/v4
API_KEY=
MODEL=GLM-4.7-Flash

# Optional per-source connection overrides. Replace PLATFORM_MYSQL with the
# uppercase data-source code, with non-alphanumeric characters changed to _.
DATASOURCE_PLATFORM_MYSQL_HOST=
DATASOURCE_PLATFORM_MYSQL_PORT=
DATASOURCE_PLATFORM_MYSQL_DATABASE=
DATASOURCE_PLATFORM_MYSQL_USER=
DATASOURCE_PLATFORM_MYSQL_PASSWORD=
37 changes: 37 additions & 0 deletions eval/query-context-cases.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
[
{
"id": "new-task-no-context-leak",
"kind": "new_question",
"question": "上个月的销售额是多少",
"turnType": "NEW_TASK",
"previousQuestion": "本月华东销售额按渠道分组",
"mustNotInherit": ["filters", "dimensions"]
},
{
"id": "follow-up-add-dimension",
"kind": "follow_up",
"question": "按省区分组呢",
"turnType": "CONTINUATION",
"previousQuestion": "查询本月含税销售金额,只看华东",
"mustInherit": ["time", "filters"],
"mustBeExplicit": ["dimensions"]
},
{
"id": "result-operation-keeps-artifact-reference",
"kind": "result_operation",
"question": "改成折线图",
"turnType": "RESULT_OPERATION",
"previousQuestion": "查询本月含税销售金额",
"referencesResult": true,
"expectedArtifactId": "artifact-query-42"
},
{
"id": "clarification-answer-fills-time-only",
"kind": "clarification",
"question": "2026-01-01",
"turnType": "CLARIFICATION_ANSWER",
"previousQuestion": "销售额,只看华东",
"mustBeExplicit": ["time"],
"mustInherit": ["filters"]
}
]
12 changes: 12 additions & 0 deletions public/js/components/chatStreamState.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export function createChatStreamState() {
answerText: '',
answerStarted: false,
answerDone: false,
error: null,
startedAt: Date.now(),
lastEventAt: Date.now(),
};
Expand Down Expand Up @@ -42,6 +43,17 @@ export function applyChatStreamEvent(stream, event) {
}
return next;
}
if (event.type === 'error' || event.type === 'stream_error') {
next.error = {
message: String(event.message ?? '本轮查询失败'),
code: event.code ?? null,
statusCode: event.statusCode ?? null,
providerStatus: event.providerStatus ?? null,
providerDetail: event.providerDetail ?? null,
recovery: event.recovery ?? null,
};
return next;
}
if (event.type === 'assistant_answer_start') {
next.answerStarted = true;
next.answerDone = false;
Expand Down
9 changes: 9 additions & 0 deletions public/js/components/liveExecution.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export function renderLiveExecution(stream, { renderExecutionDetails }) {
?? '正在启动 DataAgent 工作流';
const detail = compactDetail(activeProgress?.detail ?? activeStage?.summary ?? '');
const hasAnswer = stream?.answerStarted || Boolean(stream?.answerText);
const streamError = stream?.error;
return `
<div class="live-agent-stream">
<div class="live-agent-head">
Expand Down Expand Up @@ -107,6 +108,14 @@ export function renderLiveExecution(stream, { renderExecutionDetails }) {
<span>Agent 正在执行,结果会在这里实时输出</span>
</div>
`}
${streamError ? `
<div class="live-agent-error" role="alert">
<strong>${escapeHtml(streamError.message)}</strong>
${streamError.providerDetail && streamError.providerDetail !== streamError.message
? `<small>上游返回:${escapeHtml(streamError.providerDetail)}</small>` : ''}
${streamError.statusCode ? `<small>模型服务 HTTP ${escapeHtml(streamError.statusCode)}</small>` : ''}
</div>
` : ''}
${(stream?.warnings ?? []).map((warning) => `
<div class="live-warning">${escapeHtml(warning.message)}</div>
`).join('')}
Expand Down
9 changes: 8 additions & 1 deletion public/js/core/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,14 @@ async function streamApi(path, options = {}, onEvent) {
}
const event = JSON.parse(text);
if (event.type === 'stream_error') {
throw new Error(event.message ?? 'query failed');
const error = new Error(event.message ?? 'query failed');
error.code = event.code ?? null;
error.statusCode = event.statusCode ?? null;
error.providerStatus = event.providerStatus ?? null;
error.providerDetail = event.providerDetail ?? null;
error.recovery = event.recovery ?? null;
onEvent?.(event);
throw error;
}
if (event.type === 'final') {
finalAnswer = event.answer;
Expand Down
13 changes: 11 additions & 2 deletions public/js/pages/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -1545,8 +1545,17 @@ async function runQuestion({
state.chatMessages.push({
id: `error-${Date.now()}`,
role: 'assistant',
content: `本次查询失败:${error.message}`,
result: { error: error.message },
content: `本次查询失败:${error.message}${error.providerDetail && error.providerDetail !== error.message
&& !String(error.message).includes('上游返回')
? `\n\n上游返回:${error.providerDetail}` : ''}`,
result: {
error: error.message,
errorCode: error.code ?? null,
statusCode: error.statusCode ?? null,
providerStatus: error.providerStatus ?? null,
providerDetail: error.providerDetail ?? null,
recovery: error.recovery ?? null,
},
createdAt: new Date().toISOString(),
});
toast(error.message, 'error');
Expand Down
55 changes: 52 additions & 3 deletions public/js/pages/themes.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,11 @@ async function renderThemeEditorPage(root, theme) {
return;
}
root.innerHTML = '<div class="loading-state"><span class="spinner"></span>正在加载智能体配置</div>';
const models = await api('/api/models');
const [models, indicatorResult] = await Promise.all([
api('/api/models'),
isAdminUser() ? api('/api/local-indicators?limit=2000').catch(() => ({ items: [] })) : Promise.resolve({ items: [] }),
]);
const availableIndicators = indicatorResult.items ?? [];
const selectedSkillCodes = new Set(
theme?.skillCodes
?? state.skills.filter((skill) => skill.defaultEnabled).map((skill) => skill.code),
Expand Down Expand Up @@ -227,7 +231,7 @@ async function renderThemeEditorPage(root, theme) {
`).join('') || '<div class="muted">暂无模型配置,请先在模型管理中新增模型。</div>'}
</div>
</div>
<div class="form-field">
<div class="form-field span-2">
<label for="themeDefaultModel">默认模型</label>
<select class="select" id="themeDefaultModel">
${models.map((model) => `<option value="${model.id}"${Number(effectiveDefaultModelId) === Number(model.id) ? ' selected' : ''}>${escapeHtml(model.name)}</option>`).join('')}
Expand Down Expand Up @@ -261,6 +265,18 @@ async function renderThemeEditorPage(root, theme) {
<p>限定智能体可检索的指标语义数据集、业务数据库数据集和可用维度;点击数据集右侧「字段」可直接启用或禁用字段。</p>
</div>
</header>
<div class="form-grid">
<div class="form-field span-2">
<label>指标范围</label>
<div class="theme-indicator-config-control">
<button class="btn btn-primary" type="button" id="configureThemeIndicatorsBtn">
${icon('settings', '配置指标')}配置指标
</button>
<span class="theme-indicator-selection-summary" id="themeIndicatorSelectionSummary">${configuredIndicatorIds.size ? `已选择 ${configuredIndicatorIds.size} 个指标` : '未限定指标范围'}</span>
</div>
<small>配置后,问数时该智能体将只检索所选指标;用户指标权限仍会继续生效。未选择表示不按主题限定指标。</small>
</div>
</div>
<div class="form-grid">
<div class="form-field span-2">
<label>业务数据库数据集</label>
Expand Down Expand Up @@ -323,7 +339,7 @@ async function renderThemeEditorPage(root, theme) {
<header class="theme-config-section-head">
<span class="theme-config-index">06</span>
<div>
<h3>指标与 Skills</h3>
<h3>Skills</h3>
<p>控制智能体可访问的指标白名单,以及分析过程中可启用的能力。</p>
</div>
</header>
Expand Down Expand Up @@ -396,6 +412,38 @@ async function renderThemeEditorPage(root, theme) {
modal.querySelector('#themeForm').addEventListener('submit', (event) => {
event.preventDefault();
});
const selectedIndicatorIds = new Set(configuredIndicatorIds);
const indicatorButton = modal.querySelector('#configureThemeIndicatorsBtn');
indicatorButton?.addEventListener('click', () => {
const picker = openModal({
title: '配置智能体可用指标',
wide: true,
body: `
<p class="muted">勾选此智能体问数时可使用的指标。未选择任何指标时不启用主题级指标限制。</p>
<div class="check-grid theme-indicator-picker">
${availableIndicators.map((indicator) => `
<label class="check-item">
<input type="checkbox" name="themeIndicatorPicker" value="${escapeAttr(indicator.id)}"${selectedIndicatorIds.has(String(indicator.id)) ? ' checked' : ''} />
<span>
<strong>${escapeHtml(indicator.bizName || indicator.name)}</strong>
<small class="muted">${escapeHtml(indicator.name)}${indicator.typeName ? ` · ${escapeHtml(indicator.typeName)}` : ''}${indicator.businessCaliber ? ` · ${escapeHtml(indicator.businessCaliber)}` : ''}</small>
</span>
</label>
`).join('') || '<div class="muted">暂无本地指标,请先在指标管理中新增指标。</div>'}
</div>
`,
footer: '<button class="btn" type="button" data-close-modal>取消</button><button class="btn btn-primary" type="button" id="applyThemeIndicatorSelection">应用选择</button>',
});
picker.querySelector('#applyThemeIndicatorSelection').addEventListener('click', () => {
selectedIndicatorIds.clear();
picker.querySelectorAll('[name="themeIndicatorPicker"]:checked')
.forEach((input) => selectedIndicatorIds.add(String(input.value)));
modal.querySelector('#themeIndicatorSelectionSummary').textContent = selectedIndicatorIds.size
? `已选择 ${selectedIndicatorIds.size} 个指标`
: '未限定指标范围';
closeModal();
});
});
// 顶部状态开关是真实开关:点击即调用状态接口启用/停用,不影响表单里未保存的其它编辑。
mountThemeStatusBadge(modal);
const semanticValueFields = modal.querySelector('#themeSemanticValueFields');
Expand Down Expand Up @@ -624,6 +672,7 @@ async function renderThemeEditorPage(root, theme) {
.filter(Boolean),
businessDatasetIds: [...modal.querySelectorAll('[name="themeBusinessDataset"]:checked')]
.map((input) => Number(input.value)),
indicatorIds: [...selectedIndicatorIds],
primaryBusinessDatasetId: null,
modelIds: [...modal.querySelectorAll('[name="themeModel"]:checked')]
.map((input) => Number(input.value)),
Expand Down
18 changes: 18 additions & 0 deletions public/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -5982,6 +5982,24 @@ body.is-auth .main-content {
line-height: 1.5;
}

.live-agent-error {
display: grid;
gap: 4px;
margin-top: 10px;
padding: 10px 12px;
border: 1px solid #efcaca;
border-radius: 8px;
color: #9b2226;
background: #fff5f5;
font-size: 12px;
line-height: 1.5;
}

.live-agent-error small {
color: #7d4c4f;
overflow-wrap: anywhere;
}

.live-agent-typing {
display: inline-flex;
align-items: center;
Expand Down
24 changes: 24 additions & 0 deletions public/tokens.css
Original file line number Diff line number Diff line change
Expand Up @@ -913,6 +913,30 @@ textarea::placeholder {
gap: 7px 12px;
}

.theme-indicator-picker {
max-height: min(58vh, 560px);
overflow-y: auto;
padding: 2px 4px 2px 0;
}

.theme-indicator-config-control {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}

.theme-indicator-config-control .btn {
min-height: 38px;
padding-inline: 15px;
box-shadow: 0 2px 7px rgba(30, 45, 60, 0.12);
}

.theme-indicator-selection-summary {
color: var(--text-2);
font-size: 12px;
}

.check-item {
min-width: 0;
min-height: 40px;
Expand Down
Loading
Loading