fix(ena-planner): restore stable baseline and add stream preview in input
This commit is contained in:
@@ -40,14 +40,16 @@ export const DEFAULT_PROMPT_BLOCKS = [
|
|||||||
3. 推进而非重复:每次规划应让故事向前推进,避免原地踏步
|
3. 推进而非重复:每次规划应让故事向前推进,避免原地踏步
|
||||||
4. 留有空间:给出方向但不要过度规定细节,让主 AI 有创作余地
|
4. 留有空间:给出方向但不要过度规定细节,让主 AI 有创作余地
|
||||||
5. 遵守世界观:世界书中的规则和设定是硬约束,不可违反
|
5. 遵守世界观:世界书中的规则和设定是硬约束,不可违反
|
||||||
`,
|
|
||||||
|
如有思考过程,请放在 <thinking> 中(会被自动剔除)。`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ena-default-assistant-001',
|
id: 'ena-default-assistant-001',
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
name: 'Assistant Seed',
|
name: 'Assistant Seed',
|
||||||
content: `<think>
|
content: `<think>
|
||||||
让我分析当前情境,梳理玩家意图、已有伏笔和世界观约束,然后规划下一步走向,输出放在<plot>...</plot>和<note>...</note>两个块中...
|
让我分析当前情境,梳理玩家意图、已有伏笔和世界观约束,然后规划下一步走向...
|
||||||
|
规划结果输出在<plot>...</plot>和<note>...</note>两个块中
|
||||||
</think>`,
|
</think>`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -99,25 +99,38 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">渠道类型</label>
|
<label class="form-label">渠道类型</label>
|
||||||
<select id="ep_api_channel" class="input">
|
<select id="ep_api_channel" class="input">
|
||||||
<option value="st_main">ST Main API (no setup)</option>
|
<option value="openai">OpenAI 兼容</option>
|
||||||
<option value="openai">OpenAI compatible</option>
|
<option value="gemini">Gemini 兼容</option>
|
||||||
<option value="gemini">Gemini compatible</option>
|
<option value="claude">Claude 兼容</option>
|
||||||
<option value="claude">Claude compatible</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group hidden" id="ep_model_row">
|
<div class="form-group">
|
||||||
<label class="form-label">模型</label>
|
<label class="form-label">路径前缀</label>
|
||||||
<input id="ep_model" type="text" class="input" placeholder="gpt-4o, claude-3-5-sonnet...">
|
<select id="ep_prefix_mode" class="input">
|
||||||
|
<option value="auto">自动 (如 /v1)</option>
|
||||||
|
<option value="custom">自定义</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row hidden" id="ep_api_url_key_row">
|
<div class="form-group">
|
||||||
|
<label class="form-label">API 地址</label>
|
||||||
|
<input id="ep_api_base" type="text" class="input" placeholder="https://api.openai.com">
|
||||||
|
</div>
|
||||||
|
<div class="form-group hidden" id="ep_custom_prefix_group">
|
||||||
|
<label class="form-label">自定义前缀</label>
|
||||||
|
<input id="ep_prefix_custom" type="text" class="input" placeholder="/v1">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">API URL</label>
|
<label class="form-label">API Key</label>
|
||||||
<input id="ep_api_base" type="text" class="input" placeholder="https://api.openai.com 或代理地址">
|
<div class="input-row">
|
||||||
|
<input id="ep_api_key" type="password" class="input" placeholder="sk-...">
|
||||||
|
<button id="ep_toggle_key" class="btn">显示</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">API KEY</label>
|
<label class="form-label">模型</label>
|
||||||
<input id="ep_api_key" type="password" class="input" placeholder="仅本地保存,不上传">
|
<input id="ep_model" type="text" class="input" placeholder="gpt-4o, claude-3-5-sonnet...">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="ep_model_selector" class="hidden" style="margin-top:12px;">
|
<div id="ep_model_selector" class="hidden" style="margin-top:12px;">
|
||||||
@@ -126,7 +139,7 @@
|
|||||||
<option value="">-- 从列表选择 --</option>
|
<option value="">-- 从列表选择 --</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group" style="margin-top:16px;" id="ep_model_actions">
|
<div class="btn-group" style="margin-top:16px;">
|
||||||
<button id="ep_fetch_models" class="btn">拉取模型列表</button>
|
<button id="ep_fetch_models" class="btn">拉取模型列表</button>
|
||||||
<button id="ep_test_conn" class="btn">测试连接</button>
|
<button id="ep_test_conn" class="btn">测试连接</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -425,18 +438,8 @@
|
|||||||
if (viewId === 'debug') post('xb-ena:logs-request');
|
if (viewId === 'debug') post('xb-ena:logs-request');
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateApiChannelUI() {
|
function updatePrefixModeUI() {
|
||||||
const isMain = $('ep_api_channel').value === 'st_main';
|
$('ep_custom_prefix_group').classList.toggle('hidden', $('ep_prefix_mode').value !== 'custom');
|
||||||
$('ep_api_url_key_row').classList.toggle('hidden', isMain);
|
|
||||||
$('ep_model_row').classList.toggle('hidden', isMain);
|
|
||||||
$('ep_model_actions').classList.toggle('hidden', isMain);
|
|
||||||
if (isMain) $('ep_model_selector').classList.add('hidden');
|
|
||||||
const model = $('ep_model');
|
|
||||||
if (model && isMain) {
|
|
||||||
model.placeholder = 'Empty = follow ST main model';
|
|
||||||
} else if (model) {
|
|
||||||
model.placeholder = 'gpt-4o, claude-3-5-sonnet...';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Type conversion ── */
|
/* ── Type conversion ── */
|
||||||
@@ -551,19 +554,17 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTemplateSelect(selected) {
|
function renderTemplateSelect(selected = '') {
|
||||||
const sel = $('ep_tpl_select');
|
const sel = $('ep_tpl_select');
|
||||||
const prev = sel?.value || '';
|
|
||||||
const target = typeof selected === 'string' ? selected : prev;
|
|
||||||
sel.innerHTML = '<option value="">-- 选择模板 --</option>';
|
sel.innerHTML = '<option value="">-- 选择模板 --</option>';
|
||||||
const names = Object.keys(cfg?.promptTemplates || {});
|
const names = Object.keys(cfg?.promptTemplates || {});
|
||||||
names.forEach(name => {
|
names.forEach(name => {
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = name;
|
opt.value = name;
|
||||||
opt.textContent = name;
|
opt.textContent = name;
|
||||||
|
opt.selected = name === selected;
|
||||||
sel.appendChild(opt);
|
sel.appendChild(opt);
|
||||||
});
|
});
|
||||||
sel.value = names.includes(target) ? target : '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Undo ── */
|
/* ── Undo ── */
|
||||||
@@ -649,9 +650,11 @@
|
|||||||
$('ep_skip_plot').value = String(toBool(cfg.skipIfPlotPresent, true));
|
$('ep_skip_plot').value = String(toBool(cfg.skipIfPlotPresent, true));
|
||||||
|
|
||||||
const api = cfg.api || {};
|
const api = cfg.api || {};
|
||||||
$('ep_api_channel').value = api.channel || 'st_main';
|
$('ep_api_channel').value = api.channel || 'openai';
|
||||||
$('ep_api_base').value = api.url || '';
|
$('ep_prefix_mode').value = api.prefixMode || 'auto';
|
||||||
$('ep_api_key').value = api.key || '';
|
$('ep_api_base').value = api.baseUrl || '';
|
||||||
|
$('ep_prefix_custom').value = api.customPrefix || '';
|
||||||
|
$('ep_api_key').value = api.apiKey || '';
|
||||||
$('ep_model').value = api.model || '';
|
$('ep_model').value = api.model || '';
|
||||||
$('ep_stream').value = String(toBool(api.stream, false));
|
$('ep_stream').value = String(toBool(api.stream, false));
|
||||||
$('ep_temp').value = String(toNum(api.temperature, 1));
|
$('ep_temp').value = String(toNum(api.temperature, 1));
|
||||||
@@ -671,7 +674,7 @@
|
|||||||
$('ep_logs_max').value = String(toNum(cfg.logsMax, 20));
|
$('ep_logs_max').value = String(toNum(cfg.logsMax, 20));
|
||||||
|
|
||||||
setBadge(toBool(cfg.enabled, true));
|
setBadge(toBool(cfg.enabled, true));
|
||||||
updateApiChannelUI();
|
updatePrefixModeUI();
|
||||||
renderTemplateSelect();
|
renderTemplateSelect();
|
||||||
renderPromptList();
|
renderPromptList();
|
||||||
renderLogs();
|
renderLogs();
|
||||||
@@ -685,8 +688,10 @@
|
|||||||
|
|
||||||
p.api = {
|
p.api = {
|
||||||
channel: $('ep_api_channel').value,
|
channel: $('ep_api_channel').value,
|
||||||
url: $('ep_api_base').value.trim(),
|
prefixMode: $('ep_prefix_mode').value,
|
||||||
key: $('ep_api_key').value.trim(),
|
baseUrl: $('ep_api_base').value.trim(),
|
||||||
|
customPrefix: $('ep_prefix_custom').value.trim(),
|
||||||
|
apiKey: $('ep_api_key').value,
|
||||||
model: $('ep_model').value.trim(),
|
model: $('ep_model').value.trim(),
|
||||||
stream: toBool($('ep_stream').value, false),
|
stream: toBool($('ep_stream').value, false),
|
||||||
temperature: toNum($('ep_temp').value, 1),
|
temperature: toNum($('ep_temp').value, 1),
|
||||||
@@ -729,7 +734,17 @@
|
|||||||
setLocalStatus('ep_test_status', '测试中…', 'loading');
|
setLocalStatus('ep_test_status', '测试中…', 'loading');
|
||||||
});
|
});
|
||||||
|
|
||||||
$('ep_api_channel').addEventListener('change', () => { updateApiChannelUI(); scheduleSave(); });
|
$('ep_toggle_key').addEventListener('click', () => {
|
||||||
|
const input = $('ep_api_key');
|
||||||
|
const btn = $('ep_toggle_key');
|
||||||
|
if (input.type === 'password') {
|
||||||
|
input.type = 'text'; btn.textContent = '隐藏';
|
||||||
|
} else {
|
||||||
|
input.type = 'password'; btn.textContent = '显示';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('ep_prefix_mode').addEventListener('change', updatePrefixModeUI);
|
||||||
|
|
||||||
$('ep_fetch_models').addEventListener('click', () => {
|
$('ep_fetch_models').addEventListener('click', () => {
|
||||||
post('xb-ena:fetch-models');
|
post('xb-ena:fetch-models');
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { extension_settings } from '../../../../../extensions.js';
|
import { extension_settings } from '../../../../../extensions.js';
|
||||||
import { getRequestHeaders, saveSettingsDebounced, substituteParamsExtended } from '../../../../../../script.js';
|
import { getRequestHeaders, saveSettingsDebounced, substituteParamsExtended } from '../../../../../../script.js';
|
||||||
import { chat_completion_sources, getChatCompletionModel, oai_settings } from '../../../../../openai.js';
|
|
||||||
import { getStorySummaryForEna } from '../story-summary/story-summary.js';
|
import { getStorySummaryForEna } from '../story-summary/story-summary.js';
|
||||||
import { extensionFolderPath } from '../../core/constants.js';
|
import { extensionFolderPath } from '../../core/constants.js';
|
||||||
import { EnaPlannerStorage } from '../../core/server-storage.js';
|
import { EnaPlannerStorage } from '../../core/server-storage.js';
|
||||||
@@ -41,9 +40,11 @@ function getDefaultSettings() {
|
|||||||
|
|
||||||
// Planner API
|
// Planner API
|
||||||
api: {
|
api: {
|
||||||
channel: 'st_main',
|
channel: 'openai',
|
||||||
url: '',
|
baseUrl: '',
|
||||||
key: '',
|
prefixMode: 'auto',
|
||||||
|
customPrefix: '',
|
||||||
|
apiKey: '',
|
||||||
model: '',
|
model: '',
|
||||||
stream: false,
|
stream: false,
|
||||||
temperature: 1,
|
temperature: 1,
|
||||||
@@ -137,6 +138,10 @@ async function saveConfigNow() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toastInfo(msg) {
|
||||||
|
if (window.toastr?.info) return window.toastr.info(msg);
|
||||||
|
console.log('[EnaPlanner]', msg);
|
||||||
|
}
|
||||||
function toastErr(msg) {
|
function toastErr(msg) {
|
||||||
if (window.toastr?.error) return window.toastr.error(msg);
|
if (window.toastr?.error) return window.toastr.error(msg);
|
||||||
console.error('[EnaPlanner]', msg);
|
console.error('[EnaPlanner]', msg);
|
||||||
@@ -163,11 +168,30 @@ function nowISO() {
|
|||||||
return new Date().toISOString();
|
return new Date().toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeProxyBaseUrl(url) {
|
function normalizeUrlBase(u) {
|
||||||
let base = String(url || '').trim().replace(/\/+$/, '');
|
if (!u) return '';
|
||||||
if (/\/v1$/i.test(base)) base = base.replace(/\/v1$/i, '');
|
return u.replace(/\/+$/g, '');
|
||||||
if (/\/v1beta$/i.test(base)) base = base.replace(/\/v1beta$/i, '');
|
}
|
||||||
return base;
|
|
||||||
|
function getDefaultPrefixByChannel(channel) {
|
||||||
|
if (channel === 'gemini') return '/v1beta';
|
||||||
|
return '/v1';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildApiPrefix() {
|
||||||
|
const s = ensureSettings();
|
||||||
|
if (s.api.prefixMode === 'custom' && s.api.customPrefix?.trim()) return s.api.customPrefix.trim();
|
||||||
|
return getDefaultPrefixByChannel(s.api.channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildUrl(path) {
|
||||||
|
const s = ensureSettings();
|
||||||
|
const base = normalizeUrlBase(s.api.baseUrl);
|
||||||
|
const prefix = buildApiPrefix();
|
||||||
|
const p = prefix.startsWith('/') ? prefix : `/${prefix}`;
|
||||||
|
const finalPrefix = p.replace(/\/+$/g, '');
|
||||||
|
const finalPath = path.startsWith('/') ? path : `/${path}`;
|
||||||
|
return `${base}${finalPrefix}${finalPath}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setSendUIBusy(busy) {
|
function setSendUIBusy(busy) {
|
||||||
@@ -177,49 +201,6 @@ function setSendUIBusy(busy) {
|
|||||||
if (textarea) textarea.disabled = !!busy;
|
if (textarea) textarea.disabled = !!busy;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensurePlanningStatusEl() {
|
|
||||||
const ta = getSendTextarea();
|
|
||||||
if (!ta) return null;
|
|
||||||
let el = document.getElementById('xb-ena-planning-status');
|
|
||||||
if (el) return el;
|
|
||||||
|
|
||||||
el = document.createElement('div');
|
|
||||||
el.id = 'xb-ena-planning-status';
|
|
||||||
el.style.cssText = [
|
|
||||||
'margin-top:6px',
|
|
||||||
'font-size:12px',
|
|
||||||
'line-height:1.4',
|
|
||||||
'color:var(--SmartThemeBodyColor,#c9d1d9)',
|
|
||||||
'opacity:.82',
|
|
||||||
'display:none',
|
|
||||||
].join(';');
|
|
||||||
ta.insertAdjacentElement('afterend', el);
|
|
||||||
return el;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setPlanningStatus(text, type = 'info') {
|
|
||||||
const el = ensurePlanningStatusEl();
|
|
||||||
if (!el) return;
|
|
||||||
el.textContent = text || '';
|
|
||||||
el.style.display = text ? 'block' : 'none';
|
|
||||||
if (!text) return;
|
|
||||||
if (type === 'error') {
|
|
||||||
el.style.color = '#f87171';
|
|
||||||
} else if (type === 'success') {
|
|
||||||
el.style.color = '#3ecf8e';
|
|
||||||
} else {
|
|
||||||
el.style.color = 'var(--SmartThemeBodyColor,#c9d1d9)';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearPlanningStatus(delay = 0) {
|
|
||||||
if (delay > 0) {
|
|
||||||
setTimeout(() => setPlanningStatus(''), delay);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setPlanningStatus('');
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeStringify(val) {
|
function safeStringify(val) {
|
||||||
if (val == null) return '';
|
if (val == null) return '';
|
||||||
if (typeof val === 'string') return val;
|
if (typeof val === 'string') return val;
|
||||||
@@ -915,6 +896,10 @@ function filterPlannerForInput(rawFull) {
|
|||||||
return noThink;
|
return noThink;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function filterPlannerPreview(rawPartial) {
|
||||||
|
return stripThinkBlocks(rawPartial);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* -------------------------
|
* -------------------------
|
||||||
* Planner API calls
|
* Planner API calls
|
||||||
@@ -922,34 +907,16 @@ function filterPlannerForInput(rawFull) {
|
|||||||
*/
|
*/
|
||||||
async function callPlanner(messages, options = {}) {
|
async function callPlanner(messages, options = {}) {
|
||||||
const s = ensureSettings();
|
const s = ensureSettings();
|
||||||
const channel = String(s.api?.channel || 'st_main').toLowerCase();
|
if (!s.api.baseUrl) throw new Error('未配置 API URL');
|
||||||
const source = {
|
if (!s.api.apiKey) throw new Error('未配置 API KEY');
|
||||||
st_main: String(oai_settings?.chat_completion_source || chat_completion_sources.OPENAI),
|
if (!s.api.model) throw new Error('未选择模型');
|
||||||
openai: chat_completion_sources.OPENAI,
|
|
||||||
claude: chat_completion_sources.CLAUDE,
|
|
||||||
gemini: chat_completion_sources.MAKERSUITE,
|
|
||||||
google: chat_completion_sources.MAKERSUITE,
|
|
||||||
}[channel];
|
|
||||||
if (!source) throw new Error(`Unsupported channel: ${channel}`);
|
|
||||||
|
|
||||||
const model = channel === 'st_main'
|
const url = buildUrl('/chat/completions');
|
||||||
? String(getChatCompletionModel?.() || '').trim()
|
|
||||||
: String(s.api?.model || '').trim();
|
|
||||||
if (!model) throw new Error('No model selected in ST main panel or Ena settings');
|
|
||||||
const providerUrl = normalizeProxyBaseUrl(s.api?.url);
|
|
||||||
const providerKey = String(s.api?.key || '').trim();
|
|
||||||
if (channel !== 'st_main') {
|
|
||||||
if (!providerUrl) throw new Error('Please provide API URL');
|
|
||||||
if (!providerKey) throw new Error('Please provide API KEY');
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
type: 'quiet',
|
model: s.api.model,
|
||||||
model,
|
|
||||||
messages,
|
messages,
|
||||||
stream: !!s.api.stream,
|
stream: !!s.api.stream
|
||||||
chat_completion_source: source,
|
|
||||||
custom_prompt_post_processing: oai_settings?.custom_prompt_post_processing,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const t = Number(s.api.temperature);
|
const t = Number(s.api.temperature);
|
||||||
@@ -965,48 +932,11 @@ async function callPlanner(messages, options = {}) {
|
|||||||
const mt = s.api.max_tokens === '' ? null : Number(s.api.max_tokens);
|
const mt = s.api.max_tokens === '' ? null : Number(s.api.max_tokens);
|
||||||
if (mt != null && !Number.isNaN(mt) && mt > 0) body.max_tokens = mt;
|
if (mt != null && !Number.isNaN(mt) && mt > 0) body.max_tokens = mt;
|
||||||
|
|
||||||
if (source === chat_completion_sources.MAKERSUITE && body.max_tokens != null) {
|
const res = await fetch(url, {
|
||||||
body.max_output_tokens = body.max_tokens;
|
|
||||||
body.use_makersuite_sysprompt = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const reverseProxy = channel === 'st_main'
|
|
||||||
? String(oai_settings?.reverse_proxy || '').trim()
|
|
||||||
: providerUrl;
|
|
||||||
const proxyPassword = channel === 'st_main'
|
|
||||||
? String(oai_settings?.proxy_password || '').trim()
|
|
||||||
: providerKey;
|
|
||||||
|
|
||||||
if (reverseProxy && [
|
|
||||||
chat_completion_sources.CLAUDE,
|
|
||||||
chat_completion_sources.OPENAI,
|
|
||||||
chat_completion_sources.MISTRALAI,
|
|
||||||
chat_completion_sources.MAKERSUITE,
|
|
||||||
chat_completion_sources.VERTEXAI,
|
|
||||||
chat_completion_sources.DEEPSEEK,
|
|
||||||
chat_completion_sources.XAI,
|
|
||||||
chat_completion_sources.COHERE,
|
|
||||||
].includes(source)) {
|
|
||||||
body.reverse_proxy = reverseProxy;
|
|
||||||
if (proxyPassword) body.proxy_password = proxyPassword;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (source === chat_completion_sources.CUSTOM) {
|
|
||||||
body.custom_url = channel === 'st_main' ? oai_settings?.custom_url : providerUrl;
|
|
||||||
body.custom_include_headers = oai_settings?.custom_include_headers;
|
|
||||||
if (proxyPassword) body.proxy_password = proxyPassword;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (source === chat_completion_sources.AZURE_OPENAI) {
|
|
||||||
body.azure_base_url = oai_settings?.azure_base_url;
|
|
||||||
body.azure_deployment_name = oai_settings?.azure_deployment_name;
|
|
||||||
body.azure_api_version = oai_settings?.azure_api_version;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await fetch('/api/backends/chat-completions/generate', {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
...getRequestHeaders(),
|
...getRequestHeaders(),
|
||||||
|
Authorization: `Bearer ${s.api.apiKey}`,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify(body)
|
body: JSON.stringify(body)
|
||||||
@@ -1014,17 +944,17 @@ async function callPlanner(messages, options = {}) {
|
|||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const text = await res.text().catch(() => '');
|
const text = await res.text().catch(() => '');
|
||||||
throw new Error(`Planner request failed: ${res.status} ${text}`.slice(0, 500));
|
throw new Error(`规划请求失败: ${res.status} ${text}`.slice(0, 500));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!s.api.stream) {
|
if (!s.api.stream) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data?.error) throw new Error(data.error?.message || 'Planner request failed');
|
|
||||||
const text = String(data?.choices?.[0]?.message?.content ?? data?.choices?.[0]?.text ?? '');
|
const text = String(data?.choices?.[0]?.message?.content ?? data?.choices?.[0]?.text ?? '');
|
||||||
if (text) options?.onDelta?.(text, text);
|
if (text) options?.onDelta?.(text, text);
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SSE stream
|
||||||
const reader = res.body.getReader();
|
const reader = res.body.getReader();
|
||||||
const decoder = new TextDecoder('utf-8');
|
const decoder = new TextDecoder('utf-8');
|
||||||
let buf = '';
|
let buf = '';
|
||||||
@@ -1045,82 +975,38 @@ async function callPlanner(messages, options = {}) {
|
|||||||
if (payload === '[DONE]') continue;
|
if (payload === '[DONE]') continue;
|
||||||
try {
|
try {
|
||||||
const j = JSON.parse(payload);
|
const j = JSON.parse(payload);
|
||||||
if (j?.error) throw new Error(j.error?.message || 'Planner request failed');
|
|
||||||
const delta = j?.choices?.[0]?.delta;
|
const delta = j?.choices?.[0]?.delta;
|
||||||
const piece = delta?.content ?? delta?.text ?? '';
|
const piece = delta?.content ?? delta?.text ?? '';
|
||||||
if (piece) {
|
if (piece) {
|
||||||
full += piece;
|
full += piece;
|
||||||
options?.onDelta?.(piece, full);
|
options?.onDelta?.(piece, full);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch { }
|
||||||
// ignore non-json chunks
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return full;
|
return full;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchModelsForUi() {
|
async function fetchModelsForUi() {
|
||||||
const s = ensureSettings();
|
const s = ensureSettings();
|
||||||
const channel = String(s.api?.channel || 'st_main').toLowerCase();
|
if (!s.api.baseUrl) throw new Error('请先填写 API URL');
|
||||||
if (channel === 'st_main') {
|
if (!s.api.apiKey) throw new Error('请先填写 API KEY');
|
||||||
const source = String(oai_settings?.chat_completion_source || chat_completion_sources.OPENAI);
|
const url = buildUrl('/models');
|
||||||
const payload = {
|
const res = await fetch(url, {
|
||||||
chat_completion_source: source,
|
method: 'GET',
|
||||||
reverse_proxy: oai_settings?.reverse_proxy,
|
headers: {
|
||||||
proxy_password: oai_settings?.proxy_password,
|
...getRequestHeaders(),
|
||||||
};
|
Authorization: `Bearer ${s.api.apiKey}`
|
||||||
const res = await fetch('/api/backends/chat-completions/status', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
...getRequestHeaders(),
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
cache: 'no-cache',
|
|
||||||
});
|
|
||||||
if (!res.ok) {
|
|
||||||
const text = await res.text().catch(() => '');
|
|
||||||
throw new Error(`Model list request failed: ${res.status} ${text}`.slice(0, 300));
|
|
||||||
}
|
}
|
||||||
const data = await res.json();
|
});
|
||||||
const list = Array.isArray(data?.data) ? data.data : [];
|
if (!res.ok) {
|
||||||
return list.map(x => x?.id).filter(Boolean);
|
const text = await res.text().catch(() => '');
|
||||||
|
throw new Error(`拉取模型失败: ${res.status} ${text}`.slice(0, 300));
|
||||||
}
|
}
|
||||||
|
const data = await res.json();
|
||||||
// Keep consistent with story-summary: direct URL probing for non-ST channels.
|
const list = Array.isArray(data?.data) ? data.data : [];
|
||||||
let baseUrl = String(s.api?.url || '').trim().replace(/\/+$/, '');
|
return list.map(x => x?.id).filter(Boolean);
|
||||||
const apiKey = String(s.api?.key || '').trim();
|
|
||||||
if (!baseUrl) throw new Error('请先填写 API URL');
|
|
||||||
if (!apiKey) throw new Error('请先填写 API KEY');
|
|
||||||
|
|
||||||
const tryFetch = async (url) => {
|
|
||||||
try {
|
|
||||||
const res = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}`, Accept: 'application/json' } });
|
|
||||||
if (!res.ok) return null;
|
|
||||||
const data = await res.json();
|
|
||||||
if (!data) return null;
|
|
||||||
if (Array.isArray(data?.data)) return data.data.map(x => x?.id).filter(Boolean);
|
|
||||||
if (Array.isArray(data?.models)) return data.models.map(x => x?.id || x?.name).filter(Boolean);
|
|
||||||
} catch { }
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (baseUrl.endsWith('/v1')) baseUrl = baseUrl.slice(0, -3);
|
|
||||||
if (baseUrl.endsWith('/v1beta')) baseUrl = baseUrl.slice(0, -7);
|
|
||||||
|
|
||||||
const candidates = channel === 'gemini' || channel === 'google'
|
|
||||||
? [`${baseUrl}/v1beta/models`, `${baseUrl}/v1/models`, `${baseUrl}/models`]
|
|
||||||
: [`${baseUrl}/v1/models`, `${baseUrl}/models`];
|
|
||||||
|
|
||||||
for (const url of candidates) {
|
|
||||||
const models = await tryFetch(url);
|
|
||||||
if (models?.length) return [...new Set(models)];
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('未获取到模型');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function debugWorldbookForUi() {
|
async function debugWorldbookForUi() {
|
||||||
@@ -1280,7 +1166,7 @@ async function buildPlannerMessages(rawUserInput) {
|
|||||||
* Planning runner + logging
|
* Planning runner + logging
|
||||||
* --------------------------
|
* --------------------------
|
||||||
*/
|
*/
|
||||||
async function runPlanningOnce(rawUserInput, silent = false) {
|
async function runPlanningOnce(rawUserInput, silent = false, options = {}) {
|
||||||
const s = ensureSettings();
|
const s = ensureSettings();
|
||||||
|
|
||||||
const log = {
|
const log = {
|
||||||
@@ -1292,10 +1178,7 @@ async function runPlanningOnce(rawUserInput, silent = false) {
|
|||||||
const { messages } = await buildPlannerMessages(rawUserInput);
|
const { messages } = await buildPlannerMessages(rawUserInput);
|
||||||
log.requestMessages = messages;
|
log.requestMessages = messages;
|
||||||
|
|
||||||
const rawReply = await Promise.race([
|
const rawReply = await callPlanner(messages, options);
|
||||||
callPlanner(messages),
|
|
||||||
new Promise((_, reject) => setTimeout(() => reject(new Error('规划超时,请重试')), 120000)),
|
|
||||||
]);
|
|
||||||
log.rawReply = rawReply;
|
log.rawReply = rawReply;
|
||||||
|
|
||||||
const filtered = filterPlannerForInput(rawReply);
|
const filtered = filterPlannerForInput(rawReply);
|
||||||
@@ -1342,24 +1225,26 @@ async function doInterceptAndPlanThenSend() {
|
|||||||
|
|
||||||
state.isPlanning = true;
|
state.isPlanning = true;
|
||||||
setSendUIBusy(true);
|
setSendUIBusy(true);
|
||||||
setPlanningStatus('Planning...');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { filtered } = await runPlanningOnce(raw, false);
|
toastInfo('Ena Planner:正在规划…');
|
||||||
|
const { filtered } = await runPlanningOnce(raw, false, {
|
||||||
|
onDelta(_piece, full) {
|
||||||
|
if (!state.isPlanning) return;
|
||||||
|
if (!ensureSettings().api.stream) return;
|
||||||
|
const preview = filterPlannerPreview(full);
|
||||||
|
ta.value = `${raw}\n\n${preview}`.trim();
|
||||||
|
}
|
||||||
|
});
|
||||||
const merged = `${raw}\n\n${filtered}`.trim();
|
const merged = `${raw}\n\n${filtered}`.trim();
|
||||||
ta.value = merged;
|
ta.value = merged;
|
||||||
state.lastInjectedText = merged;
|
state.lastInjectedText = merged;
|
||||||
setPlanningStatus('Planning done', 'success');
|
|
||||||
|
|
||||||
state.bypassNextSend = true;
|
state.bypassNextSend = true;
|
||||||
btn.click();
|
btn.click();
|
||||||
} catch (err) {
|
|
||||||
setPlanningStatus(String(err?.message || 'Planning failed'), 'error');
|
|
||||||
throw err;
|
|
||||||
} finally {
|
} finally {
|
||||||
state.isPlanning = false;
|
state.isPlanning = false;
|
||||||
setSendUIBusy(false);
|
setSendUIBusy(false);
|
||||||
clearPlanningStatus(2000);
|
|
||||||
setTimeout(() => { state.bypassNextSend = false; }, 800);
|
setTimeout(() => { state.bypassNextSend = false; }, 800);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1370,11 +1255,6 @@ function installSendInterceptors() {
|
|||||||
const btn = getSendButton();
|
const btn = getSendButton();
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
if (e.target !== btn && !btn.contains(e.target)) return;
|
if (e.target !== btn && !btn.contains(e.target)) return;
|
||||||
if (state.isPlanning) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopImmediatePropagation();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!shouldInterceptNow()) return;
|
if (!shouldInterceptNow()) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation();
|
||||||
@@ -1581,3 +1461,4 @@ export function cleanupEnaPlanner() {
|
|||||||
}
|
}
|
||||||
delete window.xiaobaixEnaPlanner;
|
delete window.xiaobaixEnaPlanner;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user