Add files via upload

This commit is contained in:
公明
2026-08-18 20:48:50 +08:00
committed by GitHub
parent 1f66b93866
commit 286511d0a8
7 changed files with 340 additions and 187 deletions
+10
View File
@@ -45920,6 +45920,16 @@ html[data-theme="dark"] .conversation-sidebar .recent-conversations-section {
font-family: inherit;
}
.chat-system-channel-option-label {
font-family: inherit;
}
.chat-system-model-section-divider {
height: 1px;
margin: 5px 10px;
background: var(--border-color, #e5e7eb);
}
.chat-system-model-current {
flex: 0 0 auto;
padding: 3px 7px;
+3 -2
View File
@@ -773,9 +773,9 @@
"reasoningPanelHint": "Only Eino single- and multi-agent requests use these; merged with defaults in Settings.",
"sessionSettingsTitle": "Session settings",
"sessionSettingsAria": "Open session settings",
"sessionSettingsHint": "AI channel, reasoning, and HITL settings only affect future messages.",
"sessionSettingsHint": "Human-in-the-loop settings only affect future messages.",
"sessionShortcutAuditAgent": "Agent review",
"modelSettingsAria": "Choose model and reasoning effort",
"modelSettingsAria": "Choose AI channel, model, and reasoning settings",
"systemModelPickerTitle": "Choose system model",
"systemModelField": "Model",
"systemModelLoading": "Fetching model list…",
@@ -783,6 +783,7 @@
"systemModelCurrent": "Current",
"systemModelSaving": "Saving…",
"systemModelSaved": "Saved automatically",
"reasoningSessionUpdated": "Session reasoning updated",
"systemModelLoadFailed": "Failed to fetch models",
"systemModelSaveFailed": "Failed to save model",
"systemModelApplyFailed": "Failed to apply model",
+3 -2
View File
@@ -761,9 +761,9 @@
"reasoningPanelHint": "仅 Eino 单代理与多代理请求会带上这些参数;与系统设置中的默认值合并。",
"sessionSettingsTitle": "会话设置",
"sessionSettingsAria": "打开会话设置",
"sessionSettingsHint": "AI 通道、推理设置与人机协同只影响后续消息。",
"sessionSettingsHint": "人机协同设置只影响后续消息。",
"sessionShortcutAuditAgent": "Agent 审查",
"modelSettingsAria": "选择模型与推理强度",
"modelSettingsAria": "选择 AI 通道、模型与推理设置",
"systemModelPickerTitle": "选择系统模型",
"systemModelField": "模型",
"systemModelLoading": "正在获取模型列表…",
@@ -771,6 +771,7 @@
"systemModelCurrent": "当前",
"systemModelSaving": "正在保存…",
"systemModelSaved": "已自动保存",
"reasoningSessionUpdated": "会话推理设置已更新",
"systemModelLoadFailed": "获取模型失败",
"systemModelSaveFailed": "保存模型失败",
"systemModelApplyFailed": "应用模型失败",
+2 -2
View File
@@ -398,7 +398,7 @@ test('刷新指定对话时立即恢复且加载完成前不闪出无项目状
assert.match(css, /\.chat-container\.is-conversation-restoring #chat-messages/);
assert.match(css, /\.chat-container\.is-conversation-restoring #chat-input-container/);
assert.match(html, /router\.js\?v=20260813-2/);
assert.match(html, /chat\.js\?v=20260815-2/);
assert.match(html, /chat\.js\?v=20260818-3/);
});
test('刷新运行中回复会复用已持久化 planning 并继续追加未来增量', () => {
@@ -462,5 +462,5 @@ test('暗色模式对话三点悬浮不会触发浅色父行背景', () => {
const css = fs.readFileSync('web/static/css/style.css', 'utf8');
assert.match(css, /html\[data-theme="dark"\] \.project-conversation-row:hover \.project-conversation-item/);
assert.match(css, /html\[data-theme="dark"\] \.project-folder-action:hover,[\s\S]*?background: rgba\(71, 85, 105, 0\.28\);[\s\S]*?box-shadow: none;/);
assert.match(html, /style\.css\?v=20260815-2/);
assert.match(html, /style\.css\?v=20260818-3/);
});
+261 -132
View File
@@ -111,6 +111,8 @@ let chatSystemModelCloseTimer = null;
let chatSystemModelOptions = [];
let chatSystemModelCurrent = '';
let chatSystemModelLoadError = '';
const CHAT_SYSTEM_MODEL_CACHE_TTL_MS = 5 * 60 * 1000;
const chatSystemModelCache = new Map();
// 人机协同(HITL)会话级配置
const HITL_STORAGE_PREFIX = 'cyberstrike-chat-hitl';
@@ -1061,27 +1063,31 @@ function currentHitlAuditModelLabel() {
return chatHitlAuditModelName || currentSystemModelLabel();
}
function currentSystemReasoningEffort() {
const ch = chatDefaultAIChannel ? chatAIChannels[chatDefaultAIChannel] : null;
const reasoning = ch && ch.reasoning && typeof ch.reasoning === 'object' ? ch.reasoning : {};
const effort = typeof reasoning.effort === 'string' ? reasoning.effort.trim() : '';
return ['', 'low', 'medium', 'high', 'xhigh', 'max'].includes(effort) ? effort : '';
function resolveChatPickerChannelId() {
return selectedChatAIChannelId() || chatDefaultAIChannel;
}
function chatSystemModelConfigState(cfg) {
function chatSystemModelConfigState(cfg, preferredChannelId) {
const source = cfg && typeof cfg === 'object' ? cfg : {};
const sourceAI = source.ai && typeof source.ai === 'object' ? source.ai : {};
const channels = sourceAI.channels && typeof sourceAI.channels === 'object'
? { ...sourceAI.channels }
: {};
let channelId = String(sourceAI.default_channel || '').trim();
const resolveFromChannels = function (value) {
const raw = String(value || '').trim();
if (raw && channels[raw]) return raw;
const normalized = normalizeChatAIChannelId(raw);
return normalized
? Object.keys(channels).find(function (id) {
return normalizeChatAIChannelId(id) === normalized;
}) || ''
: '';
};
let defaultChannelId = resolveFromChannels(sourceAI.default_channel);
let channelId = resolveFromChannels(preferredChannelId) || defaultChannelId;
if (!channels[channelId]) {
const normalized = normalizeChatAIChannelId(channelId);
channelId = Object.keys(channels).find(function (id) {
return normalizeChatAIChannelId(id) === normalized;
}) || '';
channelId = Object.keys(channels)[0] || 'default';
}
if (!channelId) channelId = Object.keys(channels)[0] || 'default';
if (!channels[channelId]) {
const legacy = source.openai && typeof source.openai === 'object' ? source.openai : {};
channels[channelId] = {
@@ -1092,8 +1098,9 @@ function chatSystemModelConfigState(cfg) {
model: legacy.model || ''
};
}
if (!defaultChannelId) defaultChannelId = channelId;
return {
ai: { ...sourceAI, default_channel: channelId, channels: channels },
ai: { ...sourceAI, default_channel: defaultChannelId, channels: channels },
channelId: channelId,
channel: channels[channelId]
};
@@ -1110,7 +1117,9 @@ function chatSystemModelElements() {
list: document.getElementById('chat-system-model-list'),
status: document.getElementById('chat-system-model-status'),
subviewStatus: document.getElementById('chat-system-model-subview-status'),
channelValue: document.getElementById('chat-system-model-channel-value'),
currentValue: document.getElementById('chat-system-model-current-value'),
modeValue: document.getElementById('chat-system-model-mode-value'),
effortValue: document.getElementById('chat-system-model-effort-value')
};
}
@@ -1140,11 +1149,28 @@ function currentChatReasoningEffort() {
return effort ? String(effort.value || '').trim() : '';
}
function currentChatReasoningMode() {
const mode = document.getElementById('chat-reasoning-mode');
const value = mode ? String(mode.value || 'default').trim() : 'default';
return ['default', 'off', 'on', 'auto'].includes(value) ? value : 'default';
}
function currentChatReasoningMenuLabel() {
const modeValue = currentChatReasoningMode();
if (modeValue === 'off') return chatTranslate('chat.reasoningModeOff', '关闭');
const effort = currentChatReasoningEffort();
return effort ? chatReasoningEffortLabel(effort) : reasoningSummaryModeLabel(modeValue);
}
function updateChatSystemModelPickerValues() {
const ui = chatSystemModelElements();
const model = currentSystemModelLabel();
const effort = chatReasoningEffortLabel(currentSystemReasoningEffort());
const channel = currentChatAIChannelLabel();
const model = currentChatModelLabel();
const mode = reasoningSummaryModeLabel(currentChatReasoningMode());
const effort = currentChatReasoningMenuLabel();
if (ui.channelValue) ui.channelValue.textContent = channel;
if (ui.currentValue) ui.currentValue.textContent = model;
if (ui.modeValue) ui.modeValue.textContent = mode;
if (ui.effortValue) ui.effortValue.textContent = effort;
const composerEffort = document.getElementById('chat-model-shortcut-effort');
if (composerEffort) composerEffort.textContent = effort;
@@ -1218,7 +1244,7 @@ function renderChatReasoningEffortOptions() {
const ui = chatSystemModelElements();
if (!ui.list) return;
ui.list.innerHTML = '';
const currentEffort = currentSystemReasoningEffort();
const currentEffort = currentChatReasoningEffort();
['', 'low', 'medium', 'high', 'xhigh', 'max'].forEach(function (effort) {
const option = document.createElement('button');
option.type = 'button';
@@ -1247,61 +1273,68 @@ function renderChatReasoningEffortOptions() {
});
}
async function selectChatReasoningEffort(effort) {
if (chatSystemModelSaving) return;
if (typeof requirePermission === 'function' && !requirePermission('config:write')) return;
const chosen = ['', 'low', 'medium', 'high', 'xhigh', 'max'].includes(String(effort || '').trim())
? String(effort || '').trim()
: '';
function renderChatReasoningModeOptions() {
const ui = chatSystemModelElements();
chatSystemModelSaving = true;
if (ui.list) {
ui.list.querySelectorAll('button').forEach(function (button) { button.disabled = true; });
}
setChatSystemModelStatus(chatTranslate('chat.systemModelSaving', '正在保存…'), 'loading');
try {
const latestResponse = await apiFetch('/api/config');
if (!latestResponse.ok) {
throw new Error(await readChatSystemModelError(latestResponse, chatTranslate('chat.systemModelSaveFailed', '保存失败')));
if (!ui.list) return;
ui.list.innerHTML = '';
const currentMode = currentChatReasoningMode();
['default', 'off', 'on', 'auto'].forEach(function (mode) {
const option = document.createElement('button');
option.type = 'button';
option.className = 'chat-system-model-option';
option.setAttribute('role', 'option');
option.setAttribute('aria-selected', mode === currentMode ? 'true' : 'false');
if (mode === currentMode) option.classList.add('is-selected');
const label = document.createElement('span');
label.className = 'chat-system-model-option-label chat-system-effort-option-label';
label.textContent = reasoningSummaryModeLabel(mode);
option.appendChild(label);
if (mode === currentMode) {
const current = document.createElement('span');
current.className = 'chat-system-model-current';
current.textContent = chatTranslate('chat.systemModelCurrent', '当前');
option.appendChild(current);
}
const latest = await latestResponse.json();
const state = chatSystemModelConfigState(latest);
state.ai.channels[state.channelId] = {
...state.channel,
reasoning: { ...(state.channel.reasoning || {}), effort: chosen }
};
const updateResponse = await apiFetch('/api/config', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ai: state.ai })
option.addEventListener('click', function (event) {
event.preventDefault();
event.stopPropagation();
selectChatReasoningMode(mode);
});
if (!updateResponse.ok) {
throw new Error(await readChatSystemModelError(updateResponse, chatTranslate('chat.systemModelSaveFailed', '保存失败')));
}
const applyResponse = await apiFetch('/api/config/apply', { method: 'POST' });
if (!applyResponse.ok) {
throw new Error(await readChatSystemModelError(applyResponse, chatTranslate('chat.systemModelApplyFailed', '应用模型失败')));
}
chatAIChannels = state.ai.channels;
chatDefaultAIChannel = state.channelId;
await initChatAgentModeFromConfig();
updateChatComposerSessionShortcuts();
renderChatReasoningEffortOptions();
setChatSystemModelStatus(chatTranslate('chat.systemModelSaved', '已自动保存'), 'success');
if (chatSystemModelCloseTimer) window.clearTimeout(chatSystemModelCloseTimer);
chatSystemModelCloseTimer = window.setTimeout(function () {
chatSystemModelSaving = false;
closeChatSystemModelPicker(true);
}, 650);
return;
} catch (error) {
console.error('selectChatReasoningEffort', error);
setChatSystemModelStatus(error.message || chatTranslate('chat.systemModelSaveFailed', '保存失败'), 'error');
}
chatSystemModelSaving = false;
if (ui.list) {
ui.list.querySelectorAll('button').forEach(function (button) { button.disabled = false; });
}
ui.list.appendChild(option);
});
}
function finishChatReasoningPickerUpdate() {
persistChatReasoningPrefs();
setChatSystemModelStatus(chatTranslate('chat.reasoningSessionUpdated', '会话推理设置已更新'), 'success');
if (chatSystemModelCloseTimer) window.clearTimeout(chatSystemModelCloseTimer);
chatSystemModelCloseTimer = window.setTimeout(function () {
chatSystemModelSaving = false;
closeChatSystemModelPicker(true);
}, 450);
}
function selectChatReasoningMode(mode) {
if (chatSystemModelSaving) return;
const chosen = ['default', 'off', 'on', 'auto'].includes(String(mode || '').trim())
? String(mode || '').trim()
: 'default';
const modeControl = document.getElementById('chat-reasoning-mode');
if (!modeControl) return;
chatSystemModelSaving = true;
modeControl.value = chosen;
finishChatReasoningPickerUpdate();
}
function selectChatReasoningEffort(effort) {
if (chatSystemModelSaving) return;
const raw = String(effort || '').trim();
const chosen = ['', 'low', 'medium', 'high', 'xhigh', 'max'].includes(raw) ? raw : '';
const effortControl = document.getElementById('chat-reasoning-effort');
if (!effortControl) return;
chatSystemModelSaving = true;
effortControl.value = chosen;
finishChatReasoningPickerUpdate();
}
function renderChatSystemModelRetry() {
@@ -1313,12 +1346,62 @@ function renderChatSystemModelRetry() {
retry.className = 'chat-system-model-retry';
retry.textContent = chatTranslate('chat.systemModelRetry', '重新获取');
retry.addEventListener('click', function (retryEvent) {
closeChatSystemModelPicker(true);
openChatSystemModelPicker(retryEvent);
retryEvent.preventDefault();
retryEvent.stopPropagation();
fetchChatSystemModelsForChannel(resolveChatPickerChannelId(), { force: true });
});
ui.list.appendChild(retry);
}
function renderChatAIChannelOptions() {
const ui = chatSystemModelElements();
if (!ui.list) return;
ui.list.innerHTML = '';
const selected = selectedChatAIChannelId();
const choices = [{ id: '', label: chatTranslate('chat.aiChannelDefault', '跟随默认通道') }]
.concat(Object.keys(chatAIChannels).sort().map(function (id) {
const channel = chatAIChannels[id] || {};
return { id: id, label: channel.name || id };
}));
choices.forEach(function (choice) {
const option = document.createElement('button');
option.type = 'button';
option.className = 'chat-system-model-option';
option.setAttribute('role', 'option');
option.setAttribute('aria-selected', choice.id === selected ? 'true' : 'false');
if (choice.id === selected) option.classList.add('is-selected');
const label = document.createElement('span');
label.className = 'chat-system-model-option-label chat-system-channel-option-label';
label.textContent = choice.label;
option.appendChild(label);
if (choice.id === selected) {
const current = document.createElement('span');
current.className = 'chat-system-model-current';
current.textContent = chatTranslate('chat.systemModelCurrent', '当前');
option.appendChild(current);
}
option.addEventListener('click', function (event) {
event.preventDefault();
event.stopPropagation();
selectChatAIChannel(choice.id);
});
ui.list.appendChild(option);
});
}
async function selectChatAIChannel(channelId) {
const select = document.getElementById('chat-ai-channel-select');
if (!select) return;
const resolved = resolveChatAIChannelId(channelId);
select.value = resolved || '';
persistChatAIChannelPref();
refreshSessionSettingsSelects();
updateChatSystemModelPickerValues();
openChatSystemModelView('main');
setChatSystemModelStatus(chatTranslate('chat.systemModelLoading', '正在获取模型列表…'), 'loading');
await fetchChatSystemModelsForChannel(resolveChatPickerChannelId(), { force: true });
}
function openChatSystemModelView(view, event) {
if (event) {
event.preventDefault();
@@ -1335,6 +1418,18 @@ function openChatSystemModelView(view, event) {
ui.main.hidden = true;
ui.subview.hidden = false;
ui.subview.dataset.view = view;
if (view === 'channel') {
if (ui.subviewTitle) ui.subviewTitle.textContent = chatTranslate('chat.aiChannelLabel', 'AI 通道');
setChatSystemModelStatus('', '');
renderChatAIChannelOptions();
return;
}
if (view === 'mode') {
if (ui.subviewTitle) ui.subviewTitle.textContent = chatTranslate('chat.reasoningModeLabel', '推理模式');
setChatSystemModelStatus('', '');
renderChatReasoningModeOptions();
return;
}
if (view === 'effort') {
if (ui.subviewTitle) ui.subviewTitle.textContent = chatTranslate('chat.reasoningEffortLabel', '推理强度');
setChatSystemModelStatus('', '');
@@ -1374,7 +1469,7 @@ async function selectChatSystemModel(model) {
throw new Error(await readChatSystemModelError(latestResponse, chatTranslate('chat.systemModelSaveFailed', '保存失败')));
}
const latest = await latestResponse.json();
const state = chatSystemModelConfigState(latest);
const state = chatSystemModelConfigState(latest, resolveChatPickerChannelId());
state.ai.channels[state.channelId] = { ...state.channel, model: chosen };
const updateResponse = await apiFetch('/api/config', {
method: 'PUT',
@@ -1389,7 +1484,7 @@ async function selectChatSystemModel(model) {
throw new Error(await readChatSystemModelError(applyResponse, chatTranslate('chat.systemModelApplyFailed', '应用模型失败')));
}
chatAIChannels = state.ai.channels;
chatDefaultAIChannel = state.channelId;
chatDefaultAIChannel = resolveChatAIChannelId(state.ai.default_channel) || state.channelId;
updateChatComposerSessionShortcuts();
await initChatAgentModeFromConfig();
chatSystemModelCurrent = chosen;
@@ -1413,6 +1508,89 @@ async function selectChatSystemModel(model) {
}
}
function chatSystemModelCacheKey(channelId, channel) {
return [
String(channelId || ''),
String(channel && channel.provider || 'openai'),
String(channel && channel.base_url || '').trim()
].join('|');
}
async function fetchChatSystemModelsForChannel(channelId, options) {
const opts = options || {};
const ui = chatSystemModelElements();
if (!ui.menu || !ui.list || ui.menu.hidden) return;
const resolvedChannelId = resolveChatAIChannelId(channelId) || chatDefaultAIChannel;
const channel = resolvedChannelId ? chatAIChannels[resolvedChannelId] || {} : {};
const cacheKey = chatSystemModelCacheKey(resolvedChannelId, channel);
const cached = chatSystemModelCache.get(cacheKey);
const requestId = ++chatSystemModelRequestSeq;
chatSystemModelCurrent = String(channel.model || '').trim();
chatSystemModelOptions = [];
chatSystemModelLoadError = '';
if (!opts.force && cached && Date.now() - cached.fetchedAt < CHAT_SYSTEM_MODEL_CACHE_TTL_MS) {
chatSystemModelOptions = cached.models.slice();
if (ui.subview && !ui.subview.hidden && ui.subview.dataset.view === 'model') {
renderChatSystemModelOptions(chatSystemModelOptions, chatSystemModelCurrent);
}
const cachedCount = [chatSystemModelCurrent].concat(chatSystemModelOptions)
.map(function (model) { return String(model || '').trim(); })
.filter(function (model, index, all) { return model && all.indexOf(model) === index; })
.length;
setChatSystemModelStatus(
chatTranslate('chat.systemModelLoaded', '已获取 {count} 个模型').replace('{count}', String(cachedCount)),
'success'
);
return;
}
if (ui.subview && !ui.subview.hidden && ui.subview.dataset.view === 'model') {
ui.list.innerHTML = '';
}
setChatSystemModelStatus(chatTranslate('chat.systemModelLoading', '正在获取模型列表…'), 'loading');
try {
if (!String(channel.api_key || '').trim()) {
throw new Error(chatTranslate('chat.systemModelNeedApiKey', '请先在系统设置中配置 API Key'));
}
const listResponse = await apiFetch('/api/config/list-models', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
provider: channel.provider || 'openai',
base_url: String(channel.base_url || '').trim(),
api_key: String(channel.api_key || '').trim()
})
});
const result = await listResponse.json().catch(function () { return {}; });
if (!listResponse.ok || !result.success) {
throw new Error(result.error || chatTranslate('chat.systemModelLoadFailed', '获取模型失败'));
}
if (requestId !== chatSystemModelRequestSeq || ui.menu.hidden) return;
chatSystemModelOptions = Array.isArray(result.models) ? result.models.slice() : [];
chatSystemModelCache.set(cacheKey, {
models: chatSystemModelOptions.slice(),
fetchedAt: Date.now()
});
const count = [chatSystemModelCurrent].concat(chatSystemModelOptions)
.map(function (model) { return String(model || '').trim(); })
.filter(function (model, index, all) { return model && all.indexOf(model) === index; })
.length;
if (ui.subview && !ui.subview.hidden && ui.subview.dataset.view === 'model') {
renderChatSystemModelOptions(chatSystemModelOptions, chatSystemModelCurrent);
}
setChatSystemModelStatus(
chatTranslate('chat.systemModelLoaded', '已获取 {count} 个模型').replace('{count}', String(count)),
'success'
);
} catch (error) {
if (requestId !== chatSystemModelRequestSeq || ui.menu.hidden) return;
chatSystemModelLoadError = error.message || chatTranslate('chat.systemModelLoadFailed', '获取模型失败');
if (ui.subview && !ui.subview.hidden && ui.subview.dataset.view === 'model') {
renderChatSystemModelRetry();
}
setChatSystemModelStatus(chatSystemModelLoadError, 'error');
}
}
async function openChatSystemModelPicker(event) {
if (event) {
event.preventDefault();
@@ -1435,58 +1613,8 @@ async function openChatSystemModelPicker(event) {
if (ui.main) ui.main.hidden = false;
if (ui.subview) ui.subview.hidden = true;
ui.list.innerHTML = '';
chatSystemModelOptions = [];
chatSystemModelCurrent = currentSystemModelLabel();
chatSystemModelLoadError = '';
updateChatSystemModelPickerValues();
setChatSystemModelStatus(chatTranslate('chat.systemModelLoading', '正在获取模型列表…'), 'loading');
const requestId = ++chatSystemModelRequestSeq;
try {
const configResponse = await apiFetch('/api/config');
if (!configResponse.ok) {
throw new Error(await readChatSystemModelError(configResponse, chatTranslate('chat.systemModelLoadFailed', '获取模型失败')));
}
const cfg = await configResponse.json();
const state = chatSystemModelConfigState(cfg);
const channel = state.channel || {};
if (!String(channel.api_key || '').trim()) {
throw new Error(chatTranslate('chat.systemModelNeedApiKey', '请先在系统设置中配置 API Key'));
}
const listResponse = await apiFetch('/api/config/list-models', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
provider: channel.provider || 'openai',
base_url: String(channel.base_url || '').trim(),
api_key: String(channel.api_key || '').trim()
})
});
const result = await listResponse.json().catch(function () { return {}; });
if (!listResponse.ok || !result.success) {
throw new Error(result.error || chatTranslate('chat.systemModelLoadFailed', '获取模型失败'));
}
if (requestId !== chatSystemModelRequestSeq || ui.menu.hidden) return;
chatSystemModelCurrent = String(channel.model || '').trim();
chatSystemModelOptions = Array.isArray(result.models) ? result.models.slice() : [];
const count = [chatSystemModelCurrent].concat(chatSystemModelOptions)
.map(function (model) { return String(model || '').trim(); })
.filter(function (model, index, all) { return model && all.indexOf(model) === index; })
.length;
if (ui.subview && !ui.subview.hidden && ui.subview.dataset.view === 'model') {
renderChatSystemModelOptions(chatSystemModelOptions, chatSystemModelCurrent);
}
setChatSystemModelStatus(
chatTranslate('chat.systemModelLoaded', '已获取 {count} 个模型').replace('{count}', String(count)),
'success'
);
} catch (error) {
if (requestId !== chatSystemModelRequestSeq || ui.menu.hidden) return;
chatSystemModelLoadError = error.message || chatTranslate('chat.systemModelLoadFailed', '获取模型失败');
if (ui.subview && !ui.subview.hidden && ui.subview.dataset.view === 'model') {
renderChatSystemModelRetry();
}
setChatSystemModelStatus(chatSystemModelLoadError, 'error');
}
await fetchChatSystemModelsForChannel(resolveChatPickerChannelId());
}
function truncateChatAIChannelSummaryLabel(label) {
@@ -1502,6 +1630,7 @@ function persistChatAIChannelPref() {
else localStorage.removeItem(AI_CHANNEL_STORAGE_KEY);
} catch (e) {}
updateChatReasoningSummary();
updateChatSystemModelPickerValues();
}
function reasoningSummaryModeLabel(mode) {
@@ -1533,9 +1662,8 @@ function updateChatReasoningSummary() {
}
const channelPart = currentChatAIChannelLabel();
const modelPart = currentChatModelLabel();
const parts = [truncateChatAIChannelSummaryLabel(channelPart), reasoningPart, hitlPart].filter(Boolean);
el.textContent = parts.join(' / ');
el.title = [channelPart, reasoningPart, hitlPart].filter(Boolean).join(' / ');
el.textContent = hitlPart;
el.title = hitlPart;
updateChatComposerSessionShortcuts({
channel: channelPart,
model: modelPart,
@@ -1549,16 +1677,17 @@ function updateChatComposerSessionShortcuts(summary) {
const modelEl = document.getElementById('chat-model-shortcut-text');
const hitlEl = document.getElementById('chat-hitl-shortcut-text');
if (modelEl) {
// 输入框右侧展示系统默认主模型;审批模型只出现在 HITL 入口。
const label = currentSystemModelLabel();
// 输入框右侧展示当前会话通道的模型;审批模型只出现在 HITL 入口。
const label = currentChatModelLabel();
modelEl.textContent = truncateChatAIChannelSummaryLabel(label);
modelEl.title = label;
const shortcut = document.getElementById('chat-model-shortcut');
if (shortcut) {
const effort = chatReasoningEffortLabel(currentSystemReasoningEffort());
const action = chatTranslate('chat.modelSettingsAria', '选择模型与推理强度');
shortcut.setAttribute('aria-label', action + '' + label + ' · ' + effort);
shortcut.title = action + '' + label + ' · ' + effort;
const channel = currentChatAIChannelLabel();
const effort = currentChatReasoningMenuLabel();
const action = chatTranslate('chat.modelSettingsAria', '选择 AI 通道、模型与推理设置');
shortcut.setAttribute('aria-label', action + '' + channel + ' · ' + label + ' · ' + effort);
shortcut.title = action + '' + channel + ' · ' + label + ' · ' + effort;
}
updateChatSystemModelPickerValues();
}
+20 -11
View File
@@ -22,29 +22,38 @@ test('输入区提供独立审批入口并暴露可配置等待时限', () => {
assert.match(chat, /body\.hitl = \{[\s\S]*?timeoutSeconds: normalizeHitlTimeoutForChat\(hitlCfg\.timeoutSeconds/);
});
test('输入框可直接保存系统模型和系统推理强度且审批模型只出现在审计 Agent 入口', () => {
test('输入框可按会话通道获取模型并双向同步会话推理且审批模型只出现在审计 Agent 入口', () => {
assert.match(chat, /function currentSystemModelLabel\(\)/);
assert.match(chat, /chatDefaultAIChannel \? chatAIChannels\[chatDefaultAIChannel\]/);
assert.match(chat, /function currentHitlAuditModelLabel\(\)/);
assert.match(chat, /const label = currentSystemModelLabel\(\)/);
assert.match(chat, /const label = currentChatModelLabel\(\)/);
assert.doesNotMatch(chat, /const label = data\.model \|\| currentChatModelLabel\(\)/);
assert.match(chat, /const approvalModel = auditAgent \? currentHitlAuditModelLabel\(\) : ''/);
assert.match(chat, /hitlAuditModel\.model\.trim\(\)/);
assert.match(template, /id="chat-model-shortcut"[^>]+onclick="openChatSystemModelPicker\(event\)"/);
assert.match(template, /id="chat-system-model-menu"[^>]+hidden/);
assert.doesNotMatch(template, /id="chat-reasoning-shortcut"/);
assert.match(template, /openChatSystemModelView\('model', event\)[\s\S]{0,1200}openChatSystemModelView\('effort', event\)/);
assert.doesNotMatch(template, /session-settings-group-ai/);
assert.match(template, /class="chat-ai-session-state" hidden[\s\S]{0,500}id="chat-ai-channel-select"/);
assert.match(template, /openChatSystemModelView\('channel', event\)[\s\S]{0,1200}openChatSystemModelView\('model', event\)[\s\S]{0,1200}openChatSystemModelView\('mode', event\)[\s\S]{0,1200}openChatSystemModelView\('effort', event\)/);
assert.match(chat, /function renderChatReasoningEffortOptions\(\)/);
assert.match(chat, /function currentSystemReasoningEffort\(\)[\s\S]{0,500}reasoning\.effort/);
assert.match(chat, /function renderChatReasoningModeOptions\(\)/);
assert.match(chat, /case 'low': return 'low'[\s\S]{0,300}case 'max': return 'max'/);
assert.match(chat, /chatTranslate\('chat\.reasoningEffortUnset', '不指定'\)/);
assert.match(chat, /function selectChatReasoningEffort\(effort\)[\s\S]{0,2400}reasoning: \{ \.\.\.\(state\.channel\.reasoning \|\| \{\}\), effort: chosen \}/);
assert.match(chat, /function selectChatReasoningEffort\(effort\)[\s\S]{0,4200}body: JSON\.stringify\(\{ ai: state\.ai \}\)[\s\S]{0,900}apiFetch\('\/api\/config\/apply'/);
assert.match(chat, /function openChatSystemModelPicker\(event\)[\s\S]{0,4200}apiFetch\('\/api\/config\/list-models'/);
assert.match(chat, /\['default', 'off', 'on', 'auto'\]/);
assert.match(chat, /\['', 'low', 'medium', 'high', 'xhigh', 'max'\]/);
assert.match(chat, /function selectChatReasoningMode\(mode\)[\s\S]{0,700}modeControl\.value = chosen[\s\S]{0,200}finishChatReasoningPickerUpdate\(\)/);
assert.match(chat, /function selectChatReasoningEffort\(effort\)[\s\S]{0,700}effortControl\.value = chosen[\s\S]{0,200}finishChatReasoningPickerUpdate\(\)/);
assert.match(chat, /function fetchChatSystemModelsForChannel\(channelId, options\)[\s\S]{0,4200}apiFetch\('\/api\/config\/list-models'/);
assert.match(chat, /function selectChatAIChannel\(channelId\)[\s\S]{0,900}fetchChatSystemModelsForChannel\(resolveChatPickerChannelId\(\), \{ force: true \}\)/);
assert.match(chat, /const chatSystemModelCache = new Map\(\)/);
assert.match(chat, /Date\.now\(\) - cached\.fetchedAt < CHAT_SYSTEM_MODEL_CACHE_TTL_MS/);
assert.match(chat, /function selectChatSystemModel\(model\)[\s\S]{0,2600}method: 'PUT'[\s\S]{0,900}apiFetch\('\/api\/config\/apply'/);
assert.match(chat, /body: JSON\.stringify\(\{ ai: state\.ai \}\)/);
assert.equal(zh.chat.modelSettingsAria, '选择模型与推理强度');
assert.equal(en.chat.modelSettingsAria, 'Choose model and reasoning effort');
assert.equal(zh.chat.modelSettingsAria, '选择 AI 通道、模型与推理设置');
assert.equal(en.chat.modelSettingsAria, 'Choose AI channel, model, and reasoning settings');
assert.equal(zh.chat.reasoningSessionUpdated, '会话推理设置已更新');
assert.equal(en.chat.reasoningSessionUpdated, 'Session reasoning updated');
});
test('审批请求按浏览器、命令、文件和通用工具动态描述', () => {
@@ -255,8 +264,8 @@ test('多对话并发时释放隐藏主流且旧请求不能覆盖新对话状
assert.match(chat, /signal: conversationLoadController\.signal/);
assert.match(template, /monitor\.js\?v=20260815-2/);
assert.match(template, /chat-scroll\.js\?v=20260815-1/);
assert.match(template, /chat\.js\?v=20260815-2/);
assert.match(template, /style\.css\?v=20260815-2/);
assert.match(template, /chat\.js\?v=20260818-3/);
assert.match(template, /style\.css\?v=20260818-3/);
});
test('输入区 Agent 审查文字保留足够行高且不会裁切字形', () => {
+41 -38
View File
@@ -31,7 +31,7 @@
}
})();
</script>
<link rel="stylesheet" href="/static/css/style.css?v=20260815-2">
<link rel="stylesheet" href="/static/css/style.css?v=20260818-3">
<link rel="stylesheet" href="/static/css/chat-plan-progress.css?v=20260813-4">
<link rel="stylesheet" href="/static/css/c2.css">
<link rel="stylesheet" href="/static/vendor/xterm.css">
@@ -1025,39 +1025,8 @@
</span>
</button>
<div id="conversation-reasoning-body" class="conversation-reasoning-body" role="region">
<p class="chat-reasoning-panel-hint" data-i18n="chat.sessionSettingsHint">AI 通道、推理设置与人机协同只影响后续消息。</p>
<p class="chat-reasoning-panel-hint" data-i18n="chat.sessionSettingsHint">人机协同设置只影响后续消息。</p>
<div class="chat-reasoning-fields">
<div class="session-settings-group session-settings-group-ai">
<div class="session-settings-group-title">AI</div>
<div class="chat-reasoning-field">
<label class="chat-reasoning-field-label" for="chat-ai-channel-select"><span data-i18n="chat.aiChannelLabel">AI 通道</span></label>
<select id="chat-ai-channel-select" class="chat-reasoning-select" onchange="persistChatAIChannelPref()">
<option value="" data-i18n="chat.aiChannelDefault">跟随默认通道</option>
</select>
</div>
<div class="session-settings-inline">
<div class="chat-reasoning-field">
<label class="chat-reasoning-field-label" for="chat-reasoning-mode"><span data-i18n="chat.reasoningModeLabel">模式</span></label>
<select id="chat-reasoning-mode" class="chat-reasoning-select" onchange="persistChatReasoningPrefs()">
<option value="default" data-i18n="chat.reasoningModeDefault">跟随系统</option>
<option value="off" data-i18n="chat.reasoningModeOff">关闭</option>
<option value="on" data-i18n="chat.reasoningModeOn">开启</option>
<option value="auto" data-i18n="chat.reasoningModeAuto">自动</option>
</select>
</div>
<div class="chat-reasoning-field">
<label class="chat-reasoning-field-label" for="chat-reasoning-effort"><span data-i18n="chat.reasoningEffortLabel">推理强度</span></label>
<select id="chat-reasoning-effort" class="chat-reasoning-select" onchange="persistChatReasoningPrefs()">
<option value="" data-i18n="chat.reasoningEffortUnset">不指定</option>
<option value="low">low</option>
<option value="medium">medium</option>
<option value="high">high</option>
<option value="xhigh">xhigh</option>
<option value="max">max</option>
</select>
</div>
</div>
</div>
<div class="chat-reasoning-field session-hitl-field session-settings-group">
<div class="session-settings-group-title" data-i18n="chat.hitlTitle">人机协同</div>
<div id="hitl-apply-feedback" class="hitl-apply-feedback" role="status" aria-live="polite"></div>
@@ -1332,16 +1301,42 @@
</button>
</div>
<div class="chat-composer-footer-trailing">
<div class="chat-ai-session-state" hidden>
<select id="chat-ai-channel-select" onchange="persistChatAIChannelPref()">
<option value="" data-i18n="chat.aiChannelDefault">跟随默认通道</option>
</select>
<select id="chat-reasoning-mode" onchange="persistChatReasoningPrefs()">
<option value="default" data-i18n="chat.reasoningModeDefault">跟随系统</option>
<option value="off" data-i18n="chat.reasoningModeOff">关闭</option>
<option value="on" data-i18n="chat.reasoningModeOn">开启</option>
<option value="auto" data-i18n="chat.reasoningModeAuto">自动</option>
</select>
<select id="chat-reasoning-effort" onchange="persistChatReasoningPrefs()">
<option value="" data-i18n="chat.reasoningEffortUnset">不指定</option>
<option value="low">low</option>
<option value="medium">medium</option>
<option value="high">high</option>
<option value="xhigh">xhigh</option>
<option value="max">max</option>
</select>
</div>
<div id="chat-model-shortcut-wrap" class="chat-model-shortcut-wrap">
<button type="button" id="chat-model-shortcut" class="chat-session-shortcut chat-session-meta" onclick="openChatSystemModelPicker(event)" data-i18n="chat.modelSettingsAria" data-i18n-attr="aria-label,title" data-i18n-skip-text="true" aria-label="选择模型与推理强度" title="选择模型与推理强度" aria-haspopup="dialog" aria-expanded="false" aria-controls="chat-system-model-menu">
<button type="button" id="chat-model-shortcut" class="chat-session-shortcut chat-session-meta" onclick="openChatSystemModelPicker(event)" data-i18n="chat.modelSettingsAria" data-i18n-attr="aria-label,title" data-i18n-skip-text="true" aria-label="选择 AI 通道、模型与推理设置" title="选择 AI 通道、模型与推理设置" aria-haspopup="dialog" aria-expanded="false" aria-controls="chat-system-model-menu">
<span id="chat-model-shortcut-text">默认通道</span>
<span id="chat-model-shortcut-effort" class="chat-model-shortcut-effort">不指定</span>
<svg class="chat-system-model-caret" width="12" height="12" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<div id="chat-system-model-menu" class="chat-system-model-menu" role="dialog" aria-label="模型与推理强度" hidden>
<div id="chat-system-model-menu" class="chat-system-model-menu" role="dialog" aria-label="AI 通道、模型与推理设置" hidden>
<div id="chat-system-model-main" class="chat-system-model-main">
<button type="button" class="chat-system-model-setting-row" onclick="openChatSystemModelView('channel', event)">
<span class="chat-system-model-setting-label" data-i18n="chat.aiChannelLabel">AI 通道</span>
<span class="chat-system-model-setting-value">
<span id="chat-system-model-channel-value">默认通道</span>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M9 6l6 6-6 6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
</button>
<button type="button" class="chat-system-model-setting-row" onclick="openChatSystemModelView('model', event)">
<span class="chat-system-model-setting-label" data-i18n="chat.systemModelField">模型</span>
<span class="chat-system-model-setting-value">
@@ -1349,6 +1344,14 @@
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M9 6l6 6-6 6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
</button>
<div class="chat-system-model-section-divider" role="separator"></div>
<button type="button" class="chat-system-model-setting-row" onclick="openChatSystemModelView('mode', event)">
<span class="chat-system-model-setting-label" data-i18n="chat.reasoningModeLabel">推理模式</span>
<span class="chat-system-model-setting-value">
<span id="chat-system-model-mode-value">跟随系统</span>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M9 6l6 6-6 6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
</button>
<button type="button" class="chat-system-model-setting-row" onclick="openChatSystemModelView('effort', event)">
<span class="chat-system-model-setting-label" data-i18n="chat.reasoningEffortLabel">推理强度</span>
<span class="chat-system-model-setting-value">
@@ -4017,7 +4020,7 @@
<div class="settings-form">
<div class="form-group">
<label for="zoomeye-base-url">Base URL</label>
<input type="text" id="zoomeye-base-url" placeholder="https://api.zoomeye.org/v2/search(可选)" />
<input type="text" id="zoomeye-base-url" placeholder="https://api.zoomeye.ai/v2/search(可选)" />
<small class="form-hint">留空则使用默认地址。</small>
</div>
<div class="form-group">
@@ -4034,7 +4037,7 @@
<div class="settings-form">
<div class="form-group">
<label for="quake-base-url">Base URL</label>
<input type="text" id="quake-base-url" placeholder="https://quake.360.cn/api/v3/search/quake_service(可选)" />
<input type="text" id="quake-base-url" placeholder="https://quake.360.net/api/v3/search/quake_service(可选)" />
<small class="form-hint">留空则使用默认地址。</small>
</div>
<div class="form-group">
@@ -6842,7 +6845,7 @@
<script src="/static/js/dashboard.js"></script>
<script src="/static/js/chat-scroll.js?v=20260815-1"></script>
<script src="/static/js/monitor.js?v=20260815-2"></script>
<script src="/static/js/chat.js?v=20260815-2"></script>
<script src="/static/js/chat.js?v=20260818-3"></script>
<script src="/static/js/chat-plan-progress.js?v=20260815-1"></script>
<script src="/static/js/hitl.js?v=20260811-4"></script>
<script src="/static/js/settings.js?v=20260717-1"></script>