mirror of
https://github.com/Wx-2025/ST-Amily2-Chat-Optimisation.git
synced 2026-06-10 13:35:50 +00:00
Compare commits
8 Commits
08e1dbde85
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a4a10d42d | ||
|
|
347016d5ac | ||
|
|
59c4adc1c0 | ||
|
|
e66544f774 | ||
|
|
d6b3b00c86 | ||
|
|
a8c3ad9027 | ||
|
|
0e11f85031 | ||
|
|
9bc2f694b0 |
@@ -46,3 +46,49 @@
|
|||||||
- 修复自动归档失效问题
|
- 修复自动归档失效问题
|
||||||
- 修复归档管理器在同一事件中被三次触发的回归问题
|
- 修复归档管理器在同一事件中被三次触发的回归问题
|
||||||
- 修复翰林院设置旧版迁移逻辑异常
|
- 修复翰林院设置旧版迁移逻辑异常
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v2.2.5
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
|
||||||
|
- **翰林院(RAG)API Key 污染**:
|
||||||
|
- 修复 `saveSettingsFromUI` 无差别遍历翰林院面板内全部 `[data-setting-key]` 输入(包含被 `profile-sync` 接管隐藏的字段),导致掩码占位符 `••••••••` 被当作真值写回 `settings.rerank.apiKey` / `settings.retrieval.apiKey`,URL / model 也被 Profile 值覆盖到 legacy 字段。修复后会跳过祖先带 `data-profile-hidden` 的输入
|
||||||
|
- `getRerankSettings` / `getEmbedRetrievalSettings` 同时加入防御性还原:识别历史污染留下的 `••••••••` 时归为空字符串,避免取消 Profile 分配后实际请求带占位符 token 被 401
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v2.2.4
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
|
||||||
|
- **Function Call 填表**:
|
||||||
|
- FC 首次请求时对 DeepSeek 系模型自动附加 `thinking: { type: "disabled" }`,避免思考模式与 tool_choice 冲突
|
||||||
|
- 操作列表为空时在日志面板输出原始响应 JSON,便于区分"AI 判断无需变更"、"格式校验全部不通过"和"JSON 解析失败"三种情况
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
|
||||||
|
- **剧情优化**:移除剧情优化页面遗留的 Jqyh 直连配置字段(URL / Key / Model),统一走 API 连接配置功能分配槽位
|
||||||
|
- **表格**:
|
||||||
|
- 补全 `batch-filling-threshold` 批处理阈值的持久化绑定(页面刷新后不再还原为默认值 30)
|
||||||
|
- 修复分步填表并发锁与 async/await 时序问题
|
||||||
|
- 修复外层多余 `try...finally` 导致的插件加载报错
|
||||||
|
- **Rerank**:
|
||||||
|
- 修复选择连接配置后报"API Key 未配置"的问题(`apiMode` 现从设置读取而非硬编码 `custom`)
|
||||||
|
- 补全 `hly-rerank-api-mode` 加载绑定及默认值
|
||||||
|
- **翰林院 RAG**:补全 `priorityRetrieval.sources` 各来源条目的缺失键,修复设置面板回填 TypeError
|
||||||
|
- **二次填表**:
|
||||||
|
- 修复 `secondary-filler.js` 把哈希/重试次数写入非持久化的 `msg.metadata` 字段(ST 标准位是 `msg.extra`),导致刷新后去重与重试计数失效
|
||||||
|
- 修复扫描深度重复计入 `bufferSize`(`contextLimit + buffer + batch + redundancy` → `contextLimit + batch + redundancy`),避免越过预期窗口
|
||||||
|
- SWIPED 事件改走扫描路径,不再用 `targetMessage` bypass 强填最末条,`保留缓冲区(bufferSize)` 设置在滑动场景下正确生效(手动"回退重填"按钮仍保留 bypass,意图明确)
|
||||||
|
- 修复 FC(Function Call)路径下成功填表与"AI 判断无需修改"两种结果均未写回 `amily2_process_hash` 与 `saveChat()` 的问题——之前导致 FC 模式去重完全失效,最旧的未处理楼层会被每次扫描重复发给 AI;现统一回写路径为 `markTargetsProcessed`
|
||||||
|
- FC 空操作时同步输出原始响应 JSON 到控制台(与批量回填日志面板保持一致),便于区分"无需变更"/"格式校验失败"/"JSON 解析失败"
|
||||||
|
- 修复 `fillWithSecondaryApi` 入口处过早设置 `secondaryFillerRunning = true`,导致防抖/总开关关闭/聊天过短/非分步模式/系统瘫痪五条早返路径均不解锁的死锁问题(特别是防抖路径——锁住后 setTimeout 回调撞上自己的锁,永久跳过后续触发)。锁的获取已挪到所有早返检查之后、`try` 块之前
|
||||||
|
- **填表设置面板**:新增"手动解除填表锁"按钮(位于触发延迟下方),用于兜底应急——若仍遇到"分步填表正在进行中,跳过本次触发"反复刷屏,可手动点击释放
|
||||||
|
- **API 调用层全面支持 AbortController**(`callAI` / `callAIForTools` / `callNccsAI` 及其全部下游 provider):
|
||||||
|
- 新增 `options.signal` 透传,OpenAI 兼容 / OpenAI(测试) / Google 直连 / ST 后端 / FC 等所有 `fetch` 调用均接受 `AbortSignal`
|
||||||
|
- `callSillyTavernBackend` 由 `$.ajax` 改写为 `fetch`,以原生支持 signal
|
||||||
|
- `callSillyTavernPreset` / `callNccsSillyTavernPreset` 通过 `raceAgainstSignal` 兜底,外部不可终止的 `ConnectionManagerRequestService.sendRequest` 也能在 signal 触发时即时返回 AbortError
|
||||||
|
- 全部 catch 块识别 `AbortError`,rethrow 而不弹错误 toast;FC 重试逻辑识别中断后跳过重试
|
||||||
|
- **填表设置面板**:在"手动解除填表锁"旁新增"强制中断当前填表"按钮——通过 AbortController 真正掐断 fetch 连接(fetch 立即抛错),结果会被丢弃,不会污染表格 / hash / `saveChat`
|
||||||
|
|||||||
@@ -257,6 +257,21 @@
|
|||||||
<input type="number" id="secondary-filler-delay" min="0" max="60000" step="100" value="0" class="text_pole" style="width: 80px; margin-top: 5px;">
|
<input type="number" id="secondary-filler-delay" min="0" max="60000" step="100" value="0" class="text_pole" style="width: 80px; margin-top: 5px;">
|
||||||
<small class="notes" style="margin-top: 5px; display: block;">收到新消息后延迟多少毫秒再触发分步填表 (0 = 立即触发);延迟期内若再次收到消息会重置计时,起到防抖作用。</small>
|
<small class="notes" style="margin-top: 5px; display: block;">收到新消息后延迟多少毫秒再触发分步填表 (0 = 立即触发);延迟期内若再次收到消息会重置计时,起到防抖作用。</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 中断与手动解锁(兜底) -->
|
||||||
|
<div class="control-block-with-switch" style="margin-bottom: 10px; flex-direction: column; align-items: flex-start;">
|
||||||
|
<label>填表运行控制</label>
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px; margin-top: 5px; flex-wrap: wrap;">
|
||||||
|
<button id="amily2-abort-secondary-filler" class="menu_button danger small_button interactable" type="button">
|
||||||
|
<i class="fas fa-stop-circle"></i> 强制中断当前填表
|
||||||
|
</button>
|
||||||
|
<button id="amily2-reset-secondary-filler-lock" class="menu_button warning small_button interactable" type="button">
|
||||||
|
<i class="fas fa-unlock"></i> 手动解除填表锁
|
||||||
|
</button>
|
||||||
|
<span id="amily2-secondary-filler-lock-status" class="notes" style="font-size: 12px;">状态:空闲</span>
|
||||||
|
</div>
|
||||||
|
<small class="notes" style="margin-top: 5px; display: block;"><b>强制中断</b>:通过 AbortController 真正掐断进行中的 API 请求并丢弃结果(写表/写 hash/saveChat 都不会执行)。<br><b>手动解除填表锁</b>:仅释放 UI 锁,用于"中断"也救不回来的极端死锁兜底——若遇到"分步填表正在进行中,跳过本次触发"反复出现且新消息无法触发,可手动点击释放。</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="control-block-with-switch" style="margin-bottom: 10px; display: flex; flex-direction: column; align-items: flex-start; gap: 8px;">
|
<div class="control-block-with-switch" style="margin-bottom: 10px; display: flex; flex-direction: column; align-items: flex-start; gap: 8px;">
|
||||||
|
|||||||
79
core/api.js
79
core/api.js
@@ -588,6 +588,7 @@ export async function callAI(messages, options = {}) {
|
|||||||
apiKey: apiSettings.apiKey,
|
apiKey: apiSettings.apiKey,
|
||||||
apiProvider: apiSettings.apiProvider,
|
apiProvider: apiSettings.apiProvider,
|
||||||
customParams: apiSettings.customParams ?? {},
|
customParams: apiSettings.customParams ?? {},
|
||||||
|
signal: options.signal,
|
||||||
...options,
|
...options,
|
||||||
// options 可显式覆盖 customParams,体现"代码内显式 > profile 配置"
|
// options 可显式覆盖 customParams,体现"代码内显式 > profile 配置"
|
||||||
customParams: { ...(apiSettings.customParams ?? {}), ...(options.customParams ?? {}) },
|
customParams: { ...(apiSettings.customParams ?? {}), ...(options.customParams ?? {}) },
|
||||||
@@ -648,6 +649,10 @@ export async function callAI(messages, options = {}) {
|
|||||||
return responseContent;
|
return responseContent;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error?.name === 'AbortError') {
|
||||||
|
console.warn('[Amily2-外交部] API 调用被用户中断。');
|
||||||
|
throw error; // 让上层(如 secondary-filler)识别并跳过结果处理
|
||||||
|
}
|
||||||
console.error(`[Amily2-外交部] API调用发生错误:`, error);
|
console.error(`[Amily2-外交部] API调用发生错误:`, error);
|
||||||
|
|
||||||
if (error.message.includes('400')) {
|
if (error.message.includes('400')) {
|
||||||
@@ -690,7 +695,8 @@ async function callOpenAICompatible(messages, options) {
|
|||||||
max_tokens: options.maxTokens,
|
max_tokens: options.maxTokens,
|
||||||
temperature: options.temperature,
|
temperature: options.temperature,
|
||||||
stream: false,
|
stream: false,
|
||||||
})
|
}),
|
||||||
|
signal: options.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -732,7 +738,8 @@ async function callOpenAITest(messages, options) {
|
|||||||
const response = await fetch('/api/backends/chat-completions/generate', {
|
const response = await fetch('/api/backends/chat-completions/generate', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { ...getRequestHeaders(), 'Content-Type': 'application/json' },
|
headers: { ...getRequestHeaders(), 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(body)
|
body: JSON.stringify(body),
|
||||||
|
signal: options.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -777,7 +784,8 @@ async function callGoogleDirect(messages, options) {
|
|||||||
const response = await fetch(finalApiUrl, {
|
const response = await fetch(finalApiUrl, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: headers,
|
headers: headers,
|
||||||
body: requestBody
|
body: requestBody,
|
||||||
|
signal: options.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -822,11 +830,10 @@ async function callGoogleDirect(messages, options) {
|
|||||||
async function callSillyTavernBackend(messages, options) {
|
async function callSillyTavernBackend(messages, options) {
|
||||||
console.log('[Amily2号-ST后端] 通过SillyTavern后端调用API');
|
console.log('[Amily2号-ST后端] 通过SillyTavern后端调用API');
|
||||||
|
|
||||||
const rawResponse = await $.ajax({
|
const response = await fetch('/api/backends/chat-completions/generate', {
|
||||||
url: '/api/backends/chat-completions/generate',
|
method: 'POST',
|
||||||
type: 'POST',
|
headers: { ...getRequestHeaders(), 'Content-Type': 'application/json' },
|
||||||
contentType: 'application/json',
|
body: JSON.stringify({
|
||||||
data: JSON.stringify({
|
|
||||||
// 用户 customParams(可被核心字段覆盖)
|
// 用户 customParams(可被核心字段覆盖)
|
||||||
...(options.customParams || {}),
|
...(options.customParams || {}),
|
||||||
// 表单托管字段总是 win
|
// 表单托管字段总是 win
|
||||||
@@ -838,9 +845,16 @@ async function callSillyTavernBackend(messages, options) {
|
|||||||
max_tokens: options.maxTokens,
|
max_tokens: options.maxTokens,
|
||||||
temperature: options.temperature,
|
temperature: options.temperature,
|
||||||
stream: false,
|
stream: false,
|
||||||
})
|
}),
|
||||||
|
signal: options.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(`SillyTavern后端API请求失败: ${response.status} - ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawResponse = await response.json();
|
||||||
const result = normalizeApiResponse(rawResponse);
|
const result = normalizeApiResponse(rawResponse);
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
throw new Error(result.error.message || 'SillyTavern后端API调用失败');
|
throw new Error(result.error.message || 'SillyTavern后端API调用失败');
|
||||||
@@ -850,6 +864,28 @@ async function callSillyTavernBackend(messages, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function raceAgainstSignal(promise, signal) {
|
||||||
|
if (!signal) return promise;
|
||||||
|
if (signal.aborted) {
|
||||||
|
const err = new Error('Aborted');
|
||||||
|
err.name = 'AbortError';
|
||||||
|
return Promise.reject(err);
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const onAbort = () => {
|
||||||
|
signal.removeEventListener('abort', onAbort);
|
||||||
|
const err = new Error('Aborted');
|
||||||
|
err.name = 'AbortError';
|
||||||
|
reject(err);
|
||||||
|
};
|
||||||
|
signal.addEventListener('abort', onAbort, { once: true });
|
||||||
|
promise.then(
|
||||||
|
(v) => { signal.removeEventListener('abort', onAbort); resolve(v); },
|
||||||
|
(e) => { signal.removeEventListener('abort', onAbort); reject(e); },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function callSillyTavernPreset(messages, options) {
|
async function callSillyTavernPreset(messages, options) {
|
||||||
console.log('[Amily2号-ST预设] 使用SillyTavern预设调用');
|
console.log('[Amily2号-ST预设] 使用SillyTavern预设调用');
|
||||||
|
|
||||||
@@ -909,7 +945,7 @@ async function callSillyTavernPreset(messages, options) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await responsePromise;
|
const result = await raceAgainstSignal(responsePromise, options.signal);
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
throw new Error('未收到API响应');
|
throw new Error('未收到API响应');
|
||||||
@@ -969,6 +1005,7 @@ export async function callAIForTools(messages, tool, options = {}) {
|
|||||||
apiKey: apiSettings.apiKey,
|
apiKey: apiSettings.apiKey,
|
||||||
apiProvider: apiSettings.apiProvider,
|
apiProvider: apiSettings.apiProvider,
|
||||||
customParams: { ...(apiSettings.customParams ?? {}), ...(options.customParams ?? {}) },
|
customParams: { ...(apiSettings.customParams ?? {}), ...(options.customParams ?? {}) },
|
||||||
|
signal: options.signal,
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -985,7 +1022,11 @@ export async function callAIForTools(messages, tool, options = {}) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildFCBody = (withToolChoice, overrideMessages) => ({
|
// deepseek.com 域名或模型名含 deepseek 时,第一次调用主动关闭思考模式,
|
||||||
|
// 让 tool_choice 强制走 Function Call(思考模式下 tool_choice 会报错/失败)
|
||||||
|
const isDeepSeek = /deepseek/i.test(finalOptions.apiUrl || '') || /deepseek/i.test(finalOptions.model || '');
|
||||||
|
|
||||||
|
const buildFCBody = (withToolChoice, overrideMessages, extraParams = {}) => ({
|
||||||
chat_completion_source: 'openai',
|
chat_completion_source: 'openai',
|
||||||
reverse_proxy: finalOptions.apiUrl,
|
reverse_proxy: finalOptions.apiUrl,
|
||||||
proxy_password: finalOptions.apiKey,
|
proxy_password: finalOptions.apiKey,
|
||||||
@@ -995,15 +1036,17 @@ export async function callAIForTools(messages, tool, options = {}) {
|
|||||||
temperature: finalOptions.temperature ?? 1,
|
temperature: finalOptions.temperature ?? 1,
|
||||||
stream: false,
|
stream: false,
|
||||||
...(finalOptions.customParams || {}),
|
...(finalOptions.customParams || {}),
|
||||||
|
...extraParams,
|
||||||
tools: [tool],
|
tools: [tool],
|
||||||
...(withToolChoice ? { tool_choice: { type: 'function', function: { name: tool.function.name } } } : {}),
|
...(withToolChoice ? { tool_choice: { type: 'function', function: { name: tool.function.name } } } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const doFCRequest = async (withToolChoice, overrideMessages) => {
|
const doFCRequest = async (withToolChoice, overrideMessages, extraParams) => {
|
||||||
const response = await fetch('/api/backends/chat-completions/generate', {
|
const response = await fetch('/api/backends/chat-completions/generate', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { ...getRequestHeaders(), 'Content-Type': 'application/json' },
|
headers: { ...getRequestHeaders(), 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(buildFCBody(withToolChoice, overrideMessages)),
|
body: JSON.stringify(buildFCBody(withToolChoice, overrideMessages, extraParams)),
|
||||||
|
signal: finalOptions.signal,
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorText = await response.text();
|
const errorText = await response.text();
|
||||||
@@ -1026,8 +1069,12 @@ export async function callAIForTools(messages, tool, options = {}) {
|
|||||||
let data;
|
let data;
|
||||||
try {
|
try {
|
||||||
// 走 ST 后端代理,避免浏览器 CSP 拦截直连外部 URL
|
// 走 ST 后端代理,避免浏览器 CSP 拦截直连外部 URL
|
||||||
data = await doFCRequest(true);
|
// DeepSeek 思考模式与 tool_choice 不兼容,第一次请求时主动关闭思考模式
|
||||||
|
const firstAttemptExtra = isDeepSeek ? { thinking: { type: 'disabled' } } : {};
|
||||||
|
if (isDeepSeek) console.log('[Amily2-外交部] 检测到 DeepSeek 端点,首次 FC 请求附加 thinking:disabled');
|
||||||
|
data = await doFCRequest(true, undefined, firstAttemptExtra);
|
||||||
} catch (firstError) {
|
} catch (firstError) {
|
||||||
|
if (firstError?.name === 'AbortError') throw firstError; // 用户中断,不要重试
|
||||||
// 首次失败(含 ST 代理吞掉错误码场景)无条件去掉 tool_choice 重试一次
|
// 首次失败(含 ST 代理吞掉错误码场景)无条件去掉 tool_choice 重试一次
|
||||||
// 思考模式模型支持 tools 但不支持强制 tool_choice,追加强制指令防止模型直接输出文本
|
// 思考模式模型支持 tools 但不支持强制 tool_choice,追加强制指令防止模型直接输出文本
|
||||||
console.warn('[Amily2-外交部] 首次 FC 请求失败,去掉 tool_choice 重试…', firstError.message);
|
console.warn('[Amily2-外交部] 首次 FC 请求失败,去掉 tool_choice 重试…', firstError.message);
|
||||||
@@ -1051,6 +1098,10 @@ export async function callAIForTools(messages, tool, options = {}) {
|
|||||||
return argsString ?? null;
|
return argsString ?? null;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error?.name === 'AbortError') {
|
||||||
|
console.warn('[Amily2-外交部] Function Call 调用被用户中断。');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
console.error('[Amily2-外交部] Function Call 调用失败:', error);
|
console.error('[Amily2-外交部] Function Call 调用失败:', error);
|
||||||
toastr.error(`Function Call 调用失败: ${error.message}`, 'Amily2-外交部');
|
toastr.error(`Function Call 调用失败: ${error.message}`, 'Amily2-外交部');
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ export async function callNccsAI(messages, options = {}) {
|
|||||||
const settings = await getNccsApiSettings();
|
const settings = await getNccsApiSettings();
|
||||||
const finalOptions = {
|
const finalOptions = {
|
||||||
...settings,
|
...settings,
|
||||||
|
signal: options.signal,
|
||||||
...options
|
...options
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -123,14 +124,40 @@ export async function callNccsAI(messages, options = {}) {
|
|||||||
}
|
}
|
||||||
return responseContent;
|
return responseContent;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error?.name === 'AbortError') {
|
||||||
|
console.warn('[Amily2-Nccs] API 调用被用户中断。');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
console.error(`[Amily2-Nccs] API 调用失败:`, error);
|
console.error(`[Amily2-Nccs] API 调用失败:`, error);
|
||||||
toastr.error(`调用失败: ${error.message}`, "Nccs API Error");
|
toastr.error(`调用失败: ${error.message}`, "Nccs API Error");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchFakeStream(url, opts) {
|
function raceAgainstSignal(promise, signal) {
|
||||||
const res = await fetch(url, opts);
|
if (!signal) return promise;
|
||||||
|
if (signal.aborted) {
|
||||||
|
const err = new Error('Aborted');
|
||||||
|
err.name = 'AbortError';
|
||||||
|
return Promise.reject(err);
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const onAbort = () => {
|
||||||
|
signal.removeEventListener('abort', onAbort);
|
||||||
|
const err = new Error('Aborted');
|
||||||
|
err.name = 'AbortError';
|
||||||
|
reject(err);
|
||||||
|
};
|
||||||
|
signal.addEventListener('abort', onAbort, { once: true });
|
||||||
|
promise.then(
|
||||||
|
(v) => { signal.removeEventListener('abort', onAbort); resolve(v); },
|
||||||
|
(e) => { signal.removeEventListener('abort', onAbort); reject(e); },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchFakeStream(url, opts, signal) {
|
||||||
|
const res = await fetch(url, { ...opts, signal });
|
||||||
if (!res.ok) throw new Error(`Stream HTTP ${res.status}: ${await res.text()}`);
|
if (!res.ok) throw new Error(`Stream HTTP ${res.status}: ${await res.text()}`);
|
||||||
|
|
||||||
const reader = res.body.getReader();
|
const reader = res.body.getReader();
|
||||||
@@ -217,10 +244,10 @@ async function callNccsOpenAITest(messages, options) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (options.stream) {
|
if (options.stream) {
|
||||||
return await fetchFakeStream('/api/backends/chat-completions/generate', fetchOpts);
|
return await fetchFakeStream('/api/backends/chat-completions/generate', fetchOpts, options.signal);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch('/api/backends/chat-completions/generate', fetchOpts);
|
const response = await fetch('/api/backends/chat-completions/generate', { ...fetchOpts, signal: options.signal });
|
||||||
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
||||||
return normalizeApiResponse(await response.json());
|
return normalizeApiResponse(await response.json());
|
||||||
}
|
}
|
||||||
@@ -244,13 +271,14 @@ async function callNccsSillyTavernPreset(messages, options) {
|
|||||||
|
|
||||||
if (!context.ConnectionManagerRequestService) throw new Error('ConnectionManagerRequestService unavailable');
|
if (!context.ConnectionManagerRequestService) throw new Error('ConnectionManagerRequestService unavailable');
|
||||||
|
|
||||||
const result = await context.ConnectionManagerRequestService.sendRequest(
|
const sendPromise = context.ConnectionManagerRequestService.sendRequest(
|
||||||
targetProfile.id,
|
targetProfile.id,
|
||||||
messages,
|
messages,
|
||||||
8192,
|
8192,
|
||||||
options.customParams || {}
|
options.customParams || {}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const result = await raceAgainstSignal(sendPromise, options.signal);
|
||||||
return normalizeApiResponse(result);
|
return normalizeApiResponse(result);
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
51
core/memory-blocks/builtin-blocks.js
Normal file
51
core/memory-blocks/builtin-blocks.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* core/memory-blocks/builtin-blocks.js
|
||||||
|
*
|
||||||
|
* 内置块注册。当前只把剧情优化原硬编码的 sulv1-4 迁过来,作为新流水线的首批
|
||||||
|
* 静态块——既验证 substitution 流程正常,又保留原行为字节级一致。
|
||||||
|
*
|
||||||
|
* 旧位置:core/summarizer.js 中 processPlotOptimization 的硬编码 replacements。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { register } from './registry.js';
|
||||||
|
|
||||||
|
let initialized = false;
|
||||||
|
|
||||||
|
export function registerBuiltinBlocks() {
|
||||||
|
if (initialized) return;
|
||||||
|
initialized = true;
|
||||||
|
|
||||||
|
// 剧情优化(processPlotOptimization)的四个速率占位符
|
||||||
|
register({
|
||||||
|
id: 'plotOpt.sulv1',
|
||||||
|
placeholder: 'sulv1',
|
||||||
|
context: 'plotOptimization',
|
||||||
|
generator: { type: 'static', valueKey: 'plotOpt_rateMain', defaultValue: 1.0 },
|
||||||
|
name: '主线剧情速率',
|
||||||
|
order: 1,
|
||||||
|
});
|
||||||
|
register({
|
||||||
|
id: 'plotOpt.sulv2',
|
||||||
|
placeholder: 'sulv2',
|
||||||
|
context: 'plotOptimization',
|
||||||
|
generator: { type: 'static', valueKey: 'plotOpt_ratePersonal', defaultValue: 1.0 },
|
||||||
|
name: '个人线速率',
|
||||||
|
order: 2,
|
||||||
|
});
|
||||||
|
register({
|
||||||
|
id: 'plotOpt.sulv3',
|
||||||
|
placeholder: 'sulv3',
|
||||||
|
context: 'plotOptimization',
|
||||||
|
generator: { type: 'static', valueKey: 'plotOpt_rateErotic', defaultValue: 1.0 },
|
||||||
|
name: '速率3(留空)',
|
||||||
|
order: 3,
|
||||||
|
});
|
||||||
|
register({
|
||||||
|
id: 'plotOpt.sulv4',
|
||||||
|
placeholder: 'sulv4',
|
||||||
|
context: 'plotOptimization',
|
||||||
|
generator: { type: 'static', valueKey: 'plotOpt_rateCuckold', defaultValue: 1.0 },
|
||||||
|
name: '速率4(留空)',
|
||||||
|
order: 4,
|
||||||
|
});
|
||||||
|
}
|
||||||
98
core/memory-blocks/executor.js
Normal file
98
core/memory-blocks/executor.js
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
* core/memory-blocks/executor.js
|
||||||
|
*
|
||||||
|
* 工作流执行器:拉 context 下的全部块 → Promise.all 并发执行 generator
|
||||||
|
* → 把每个块的结果按 placeholder 替换回模板。
|
||||||
|
*
|
||||||
|
* 核心 API:
|
||||||
|
* applyToTemplate(template, opts) 单模板进,字符串出
|
||||||
|
* applyToTemplates(templates, opts) 多模板进(数组或对象),结构同形出;
|
||||||
|
* 块只执行一次,对每个模板复用结果
|
||||||
|
* generateBlockMap(opts) 不替换,返回 { id → value } 给调用方自己玩
|
||||||
|
*
|
||||||
|
* 中断行为:opts.signal 由调用方控制,传给每个 handler;任一 handler 抛
|
||||||
|
* AbortError 时,executor 也抛 AbortError 向上传递(与现有 callAI 体系一致)。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getHandler } from './generator-handlers.js';
|
||||||
|
import { listByContext } from './registry.js';
|
||||||
|
|
||||||
|
function escapeForRegex(s) {
|
||||||
|
return String(s).replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runBlock(block, ctx) {
|
||||||
|
const handler = getHandler(block.generator?.type);
|
||||||
|
if (!handler) {
|
||||||
|
console.warn(`[MemoryBlocks] 未注册的 generator 类型 "${block.generator?.type}",块 ${block.id} 已跳过。`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const value = await handler(block, ctx);
|
||||||
|
if (value === null || value === undefined) return null;
|
||||||
|
return { block, value: String(value) };
|
||||||
|
} catch (error) {
|
||||||
|
if (error?.name === 'AbortError') throw error;
|
||||||
|
console.error(`[MemoryBlocks] 块 ${block.id} 生成失败:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function substituteOne(template, results) {
|
||||||
|
if (typeof template !== 'string' || !template) return template ?? '';
|
||||||
|
let out = template;
|
||||||
|
for (const r of results) {
|
||||||
|
if (!r) continue;
|
||||||
|
const re = new RegExp(escapeForRegex(r.block.placeholder), 'g');
|
||||||
|
out = out.replace(re, r.value);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行 context 下的所有块,返回 [ {block, value} | null, ... ]。
|
||||||
|
* 内部使用,applyToTemplate(s) 复用。
|
||||||
|
*/
|
||||||
|
async function executeBlocks({ context, settings, signal, extras } = {}) {
|
||||||
|
const blocks = listByContext(context);
|
||||||
|
if (blocks.length === 0) return [];
|
||||||
|
const ctx = { settings: settings ?? {}, signal, context, extras };
|
||||||
|
return await Promise.all(blocks.map(b => runBlock(b, ctx)));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function applyToTemplate(template, opts = {}) {
|
||||||
|
if (typeof template !== 'string' || !template) return template ?? '';
|
||||||
|
const results = await executeBlocks(opts);
|
||||||
|
return substituteOne(template, results);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 多模板批处理。templates 可以是:
|
||||||
|
* - 字符串数组 → 返回字符串数组
|
||||||
|
* - 对象 { key: template } → 返回对象 { key: replaced }
|
||||||
|
* - 字符串 → 退化为 applyToTemplate
|
||||||
|
*/
|
||||||
|
export async function applyToTemplates(templates, opts = {}) {
|
||||||
|
const results = await executeBlocks(opts);
|
||||||
|
|
||||||
|
if (typeof templates === 'string') return substituteOne(templates, results);
|
||||||
|
if (Array.isArray(templates)) return templates.map(t => substituteOne(t, results));
|
||||||
|
if (templates && typeof templates === 'object') {
|
||||||
|
const out = {};
|
||||||
|
for (const [k, v] of Object.entries(templates)) out[k] = substituteOne(v, results);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
return templates;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 不替换,只把块结果汇成 Map<id, value>,调用方拿去自由组合。
|
||||||
|
*/
|
||||||
|
export async function generateBlockMap(opts = {}) {
|
||||||
|
const results = await executeBlocks(opts);
|
||||||
|
const map = new Map();
|
||||||
|
for (const r of results) {
|
||||||
|
if (r) map.set(r.block.id, r.value);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
46
core/memory-blocks/generator-handlers.js
Normal file
46
core/memory-blocks/generator-handlers.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* core/memory-blocks/generator-handlers.js
|
||||||
|
*
|
||||||
|
* type → handler 函数 的注册表。BlockDefinition.generator.type 在这里查表后执行。
|
||||||
|
*
|
||||||
|
* Handler 签名:async (block, ctx) => string | null
|
||||||
|
* - block: BlockDefinition
|
||||||
|
* - ctx: ExecuteContext { settings, signal, context, extras }
|
||||||
|
* - 返回 string:替换值;返回 null/undefined:视为"无内容,保留占位符"
|
||||||
|
*
|
||||||
|
* 当前内置 'static';'ai_call'/'plugin' 在后续 Phase 注册(保留接口)。
|
||||||
|
*/
|
||||||
|
|
||||||
|
const handlers = new Map();
|
||||||
|
|
||||||
|
export function registerHandler(type, fn) {
|
||||||
|
if (!type || typeof fn !== 'function') {
|
||||||
|
throw new Error('[MemoryBlocks] registerHandler 需要 type 字符串 + 函数 fn。');
|
||||||
|
}
|
||||||
|
handlers.set(type, fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unregisterHandler(type) {
|
||||||
|
handlers.delete(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHandler(type) {
|
||||||
|
return handlers.get(type) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listHandlerTypes() {
|
||||||
|
return [...handlers.keys()];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 内置 handler:static ──────────────────────────────────────────────────────
|
||||||
|
registerHandler('static', async (block, ctx) => {
|
||||||
|
const gen = block.generator || {};
|
||||||
|
// 优先级:硬编码 value > settings[valueKey] > defaultValue > ''
|
||||||
|
if (gen.value !== undefined) return String(gen.value);
|
||||||
|
if (gen.valueKey != null) {
|
||||||
|
const v = ctx?.settings?.[gen.valueKey];
|
||||||
|
if (v !== undefined && v !== null && v !== '') return String(v);
|
||||||
|
}
|
||||||
|
if (gen.defaultValue !== undefined) return String(gen.defaultValue);
|
||||||
|
return '';
|
||||||
|
});
|
||||||
51
core/memory-blocks/index.js
Normal file
51
core/memory-blocks/index.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* core/memory-blocks/index.js
|
||||||
|
*
|
||||||
|
* 记忆块工作流系统对外入口。导入此模块即触发:
|
||||||
|
* 1. generator-handlers 加载 → 注册内置 'static' handler
|
||||||
|
* 2. registerBuiltinBlocks() → 注册首批内置块(sulv1-4)
|
||||||
|
*
|
||||||
|
* 公开 API:
|
||||||
|
* - register / unregister / getById / listByContext / listAll
|
||||||
|
* - registerHandler / getHandler / listHandlerTypes
|
||||||
|
* - applyToTemplate(template, opts)
|
||||||
|
* - applyToTemplates(templates, opts) ← 多模板批处理首选
|
||||||
|
* - generateBlockMap(opts)
|
||||||
|
*
|
||||||
|
* opts 字段:{ context, settings, signal?, extras? }
|
||||||
|
*
|
||||||
|
* 设计目标:
|
||||||
|
* - BlockDefinition 纯数据,可 JSON 序列化(Phase 3 用户自定义导入导出)
|
||||||
|
* - generator 通过 type 查表,handler 集中注册,便于扩展 ai_call / plugin
|
||||||
|
* - 同一 context 下的块 Promise.all 并发;任一块抛 AbortError 整体中断
|
||||||
|
*/
|
||||||
|
|
||||||
|
export {
|
||||||
|
register,
|
||||||
|
unregister,
|
||||||
|
getById,
|
||||||
|
listByContext,
|
||||||
|
listAll,
|
||||||
|
clear,
|
||||||
|
replaceContextBlocks,
|
||||||
|
} from './registry.js';
|
||||||
|
|
||||||
|
export {
|
||||||
|
registerHandler,
|
||||||
|
unregisterHandler,
|
||||||
|
getHandler,
|
||||||
|
listHandlerTypes,
|
||||||
|
} from './generator-handlers.js';
|
||||||
|
|
||||||
|
export {
|
||||||
|
applyToTemplate,
|
||||||
|
applyToTemplates,
|
||||||
|
generateBlockMap,
|
||||||
|
} from './executor.js';
|
||||||
|
|
||||||
|
import { registerBuiltinBlocks } from './builtin-blocks.js';
|
||||||
|
|
||||||
|
// 导入此模块即完成内置块注册(幂等)
|
||||||
|
registerBuiltinBlocks();
|
||||||
|
|
||||||
|
export { registerBuiltinBlocks };
|
||||||
63
core/memory-blocks/registry.js
Normal file
63
core/memory-blocks/registry.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* core/memory-blocks/registry.js
|
||||||
|
*
|
||||||
|
* BlockDefinition 的注册中心。所有块共享同一个全局 Map。
|
||||||
|
*
|
||||||
|
* 调用方:
|
||||||
|
* - 内置块:builtin-blocks.js 在 bootstrap 时注册
|
||||||
|
* - 用户块:未来 UI / JSON 导入注册
|
||||||
|
* - 插件块:战斗系统等外部模块注册
|
||||||
|
*
|
||||||
|
* 字段校验只做最小必填检查,避免后续扩展时频繁报错。
|
||||||
|
*/
|
||||||
|
|
||||||
|
const blocks = new Map();
|
||||||
|
|
||||||
|
function validate(def) {
|
||||||
|
if (!def || typeof def !== 'object') throw new Error('[MemoryBlocks] BlockDefinition 必须是对象。');
|
||||||
|
if (!def.id) throw new Error('[MemoryBlocks] BlockDefinition.id 必填。');
|
||||||
|
if (!def.placeholder) throw new Error(`[MemoryBlocks] BlockDefinition[${def.id}].placeholder 必填。`);
|
||||||
|
if (!def.context) throw new Error(`[MemoryBlocks] BlockDefinition[${def.id}].context 必填。`);
|
||||||
|
if (!def.generator?.type) throw new Error(`[MemoryBlocks] BlockDefinition[${def.id}].generator.type 必填。`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function register(def) {
|
||||||
|
validate(def);
|
||||||
|
blocks.set(def.id, { enabled: true, ...def });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unregister(id) {
|
||||||
|
return blocks.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getById(id) {
|
||||||
|
return blocks.get(id) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listByContext(context) {
|
||||||
|
const out = [];
|
||||||
|
for (const b of blocks.values()) {
|
||||||
|
if (b.context === context && b.enabled !== false) out.push(b);
|
||||||
|
}
|
||||||
|
out.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listAll() {
|
||||||
|
return [...blocks.values()];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clear() {
|
||||||
|
blocks.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量替换(用于 JSON 导入时整体覆盖某 context 下的块) */
|
||||||
|
export function replaceContextBlocks(context, defs) {
|
||||||
|
for (const [id, b] of blocks) {
|
||||||
|
if (b.context === context) blocks.delete(id);
|
||||||
|
}
|
||||||
|
for (const d of defs) {
|
||||||
|
if (d.context !== context) continue; // 防止越界注册
|
||||||
|
register(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
56
core/memory-blocks/types.js
Normal file
56
core/memory-blocks/types.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* core/memory-blocks/types.js — 类型契约(JSDoc 文档,无运行时代码)
|
||||||
|
*
|
||||||
|
* BlockDefinition 是工作流的最小单位,描述"如何为某个占位符产出内容"。
|
||||||
|
* 所有字段必须 JSON 可序列化,为后续支持 JSON 导入导出做准备。
|
||||||
|
*
|
||||||
|
* 生成器(generator)只承载"用哪个 handler、参数是什么"的元数据,
|
||||||
|
* 真正的执行逻辑由 generator-handlers.js 按 type 查表的 handler 函数承担,
|
||||||
|
* 因此 BlockDefinition 本身永远不持有函数引用、可直接 JSON.stringify。
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} StaticGenerator 直接读取 settings 或常量值
|
||||||
|
* @property {'static'} type
|
||||||
|
* @property {string} [valueKey] - 从 ctx.settings[valueKey] 读取
|
||||||
|
* @property {*} [defaultValue]- valueKey 不存在/为空时的兜底
|
||||||
|
* @property {*} [value] - 硬编码值,优先级高于 valueKey
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} AiCallGenerator (Phase 2 预留)
|
||||||
|
* @property {'ai_call'} type
|
||||||
|
* @property {string} apiSlot
|
||||||
|
* @property {string} promptTemplate
|
||||||
|
* @property {string} [extractTag]
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} PluginGenerator (Phase 3 预留:战斗模块走这条)
|
||||||
|
* @property {'plugin'} type
|
||||||
|
* @property {string} handlerKey - 在 handler 注册表里查 handler 函数
|
||||||
|
* @property {Object} [params]
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @typedef {StaticGenerator | AiCallGenerator | PluginGenerator} GeneratorSpec */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} BlockDefinition
|
||||||
|
* @property {string} id - 全局唯一
|
||||||
|
* @property {string} placeholder - 在模板中要被替换的占位符(按字面量匹配,正则元字符自动转义)
|
||||||
|
* @property {string} context - 所属流水线,如 'plotOptimization'
|
||||||
|
* @property {GeneratorSpec} generator
|
||||||
|
* @property {string} [name] - UI 显示名
|
||||||
|
* @property {boolean} [enabled=true]
|
||||||
|
* @property {number} [order] - 仅影响 listByContext 的返回顺序;执行并发,不阻塞
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} ExecuteContext
|
||||||
|
* @property {Object} settings - extension_settings[extensionName]
|
||||||
|
* @property {AbortSignal} [signal] - 来自调用方的中断信号
|
||||||
|
* @property {string} context
|
||||||
|
* @property {Object} [extras] - 额外上下文,供 handler 自取
|
||||||
|
*/
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -14,6 +14,14 @@ import { extensionName } from '../utils/settings.js';
|
|||||||
const MODULE_NAME = 'hanlinyuan-rag-core';
|
const MODULE_NAME = 'hanlinyuan-rag-core';
|
||||||
const GOOGLE_API_BASE_URL = 'https://generativelanguage.googleapis.com';
|
const GOOGLE_API_BASE_URL = 'https://generativelanguage.googleapis.com';
|
||||||
|
|
||||||
|
// profile-sync 在 UI 隐藏字段时填入的掩码占位符(const MASKED_KEY = '••••••••')。
|
||||||
|
// 历史上 saveSettingsFromUI 曾把这个占位符写回 settings.{rerank,retrieval}.apiKey,
|
||||||
|
// 导致取消 Profile 分配后实际请求带占位符 token 被 401。这里做防御性还原。
|
||||||
|
const PROFILE_MASKED_KEY = '••••••••';
|
||||||
|
function sanitizeMaskedKey(key) {
|
||||||
|
return key === PROFILE_MASKED_KEY ? '' : key;
|
||||||
|
}
|
||||||
|
|
||||||
function getSettings() {
|
function getSettings() {
|
||||||
const root = extension_settings[extensionName];
|
const root = extension_settings[extensionName];
|
||||||
const nested = root && root[MODULE_NAME];
|
const nested = root && root[MODULE_NAME];
|
||||||
@@ -35,12 +43,13 @@ export async function getEmbedRetrievalSettings() {
|
|||||||
return {
|
return {
|
||||||
apiEndpoint: profile.provider === 'google' ? 'google_direct' : 'custom',
|
apiEndpoint: profile.provider === 'google' ? 'google_direct' : 'custom',
|
||||||
customApiUrl: profile.apiUrl,
|
customApiUrl: profile.apiUrl,
|
||||||
apiKey: profile.apiKey ?? '',
|
apiKey: sanitizeMaskedKey(profile.apiKey ?? ''),
|
||||||
embeddingModel: profile.model,
|
embeddingModel: profile.model,
|
||||||
batchSize: getSettings().retrieval?.batchSize ?? 5,
|
batchSize: getSettings().retrieval?.batchSize ?? 5,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return getSettings().retrieval || {};
|
const fallback = getSettings().retrieval || {};
|
||||||
|
return { ...fallback, apiKey: sanitizeMaskedKey(fallback.apiKey ?? '') };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,15 +58,17 @@ export async function getEmbedRetrievalSettings() {
|
|||||||
export async function getRerankSettings() {
|
export async function getRerankSettings() {
|
||||||
const profile = await getSlotProfile('ragRerank');
|
const profile = await getSlotProfile('ragRerank');
|
||||||
if (profile) {
|
if (profile) {
|
||||||
|
const manualSettings = getSettings().rerank || {};
|
||||||
return {
|
return {
|
||||||
url: profile.apiUrl,
|
url: profile.apiUrl,
|
||||||
apiKey: profile.apiKey ?? '',
|
apiKey: sanitizeMaskedKey(profile.apiKey ?? ''),
|
||||||
model: profile.model,
|
model: profile.model,
|
||||||
top_n: getSettings().rerank?.top_n ?? 10,
|
top_n: manualSettings.top_n ?? 10,
|
||||||
apiMode: 'custom',
|
apiMode: manualSettings.apiMode ?? 'custom',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return getSettings().rerank || {};
|
const fallback = getSettings().rerank || {};
|
||||||
|
return { ...fallback, apiKey: sanitizeMaskedKey(fallback.apiKey ?? '') };
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeApiResponse(responseData) {
|
function normalizeApiResponse(responseData) {
|
||||||
|
|||||||
@@ -346,6 +346,20 @@ function getSettings() {
|
|||||||
if (s.rerank?.priorityRetrieval && !s.rerank.priorityRetrieval.sources) {
|
if (s.rerank?.priorityRetrieval && !s.rerank.priorityRetrieval.sources) {
|
||||||
s.rerank.priorityRetrieval.sources = structuredClone(ragDefaultSettings.rerank.priorityRetrieval.sources);
|
s.rerank.priorityRetrieval.sources = structuredClone(ragDefaultSettings.rerank.priorityRetrieval.sources);
|
||||||
}
|
}
|
||||||
|
// 确保 sources 中每个来源条目完整(新增来源 / 新增字段时旧用户不会缺失)
|
||||||
|
if (s.rerank?.priorityRetrieval?.sources) {
|
||||||
|
const defaultSources = ragDefaultSettings.rerank.priorityRetrieval.sources;
|
||||||
|
for (const sourceName in defaultSources) {
|
||||||
|
if (!s.rerank.priorityRetrieval.sources[sourceName]) {
|
||||||
|
s.rerank.priorityRetrieval.sources[sourceName] = structuredClone(defaultSources[sourceName]);
|
||||||
|
} else {
|
||||||
|
const existing = s.rerank.priorityRetrieval.sources[sourceName];
|
||||||
|
for (const key in defaultSources[sourceName]) {
|
||||||
|
if (existing[key] === undefined) existing[key] = defaultSources[sourceName][key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export const defaultSettings = {
|
|||||||
},
|
},
|
||||||
rerank: {
|
rerank: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
apiMode: 'custom',
|
||||||
url: 'https://api.siliconflow.cn/v1',
|
url: 'https://api.siliconflow.cn/v1',
|
||||||
apiKey: '',
|
apiKey: '',
|
||||||
model: 'Pro/BAAI/bge-reranker-v2-m3',
|
model: 'Pro/BAAI/bge-reranker-v2-m3',
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { resolveHistoriographyRuleConfig } from "../utils/config/RuleProfileMana
|
|||||||
import { getPresetPrompts, getMixedOrder } from '../PresetSettings/index.js';
|
import { getPresetPrompts, getMixedOrder } from '../PresetSettings/index.js';
|
||||||
import { callAI, generateRandomSeed } from './api.js';
|
import { callAI, generateRandomSeed } from './api.js';
|
||||||
import { callConcurrentAI } from './api/ConcurrentApi.js';
|
import { callConcurrentAI } from './api/ConcurrentApi.js';
|
||||||
|
import { applyToTemplates } from './memory-blocks/index.js';
|
||||||
|
|
||||||
export async function processOptimization(latestMessage, previousMessages) {
|
export async function processOptimization(latestMessage, previousMessages) {
|
||||||
if (window.AMILY2_SYSTEM_PARALYZED === true) {
|
if (window.AMILY2_SYSTEM_PARALYZED === true) {
|
||||||
@@ -276,22 +277,18 @@ export async function processPlotOptimization(currentUserMessage, contextMessage
|
|||||||
const userName = context.name1 || '用户';
|
const userName = context.name1 || '用户';
|
||||||
const charName = context.name2 || '角色';
|
const charName = context.name2 || '角色';
|
||||||
|
|
||||||
const replacements = {
|
// 【Phase 1 重构】sulv1-4 占位符替换迁入记忆块工作流。
|
||||||
'sulv1': settings.plotOpt_rateMain ?? 1.0,
|
// 块定义见 core/memory-blocks/builtin-blocks.js,行为与旧硬编码字节级一致:
|
||||||
'sulv2': settings.plotOpt_ratePersonal ?? 1.0,
|
// - 同一 context 内 Promise.all 并发执行 generator
|
||||||
'sulv3': settings.plotOpt_rateErotic ?? 1.0,
|
// - 模板批量替换,块只跑一次复用结果
|
||||||
'sulv4': settings.plotOpt_rateCuckold ?? 1.0,
|
// - 后续新增占位符(含战斗系统)走 register({...}),此处零改动
|
||||||
};
|
const { mainPrompt, systemPrompt } = await applyToTemplates(
|
||||||
|
{
|
||||||
let mainPrompt = settings.plotOpt_mainPrompt || '';
|
mainPrompt: settings.plotOpt_mainPrompt || '',
|
||||||
let systemPrompt = settings.plotOpt_systemPrompt || '';
|
systemPrompt: settings.plotOpt_systemPrompt || '',
|
||||||
|
},
|
||||||
for (const key in replacements) {
|
{ context: 'plotOptimization', settings },
|
||||||
const value = replacements[key];
|
);
|
||||||
const regex = new RegExp(key.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), 'g');
|
|
||||||
mainPrompt = mainPrompt.replace(regex, value);
|
|
||||||
systemPrompt = systemPrompt.replace(regex, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
onProgress(getRandomText(['正在进行情感光谱分析...', '正在解析情绪波动频率...', '正在捕捉微表情信号...']), false);
|
onProgress(getRandomText(['正在进行情感光谱分析...', '正在解析情绪波动频率...', '正在捕捉微表情信号...']), false);
|
||||||
onProgress(getRandomText(['正在进行情感光谱分析...', '正在解析情绪波动频率...', '正在捕捉微表情信号...']), true);
|
onProgress(getRandomText(['正在进行情感光谱分析...', '正在解析情绪波动频率...', '正在捕捉微表情信号...']), true);
|
||||||
|
|||||||
@@ -291,7 +291,17 @@ async function runBatchAttempt(batchNum, attemptNum) {
|
|||||||
if (!argsString) throw new Error('Function Call 返回为空。');
|
if (!argsString) throw new Error('Function Call 返回为空。');
|
||||||
const ops = parseToolCallArgs(argsString);
|
const ops = parseToolCallArgs(argsString);
|
||||||
if (ops.length === 0) {
|
if (ops.length === 0) {
|
||||||
log(`批次 ${batchNum} 的 Function Call 返回操作列表为空,AI 判断此批次无需变更。`, 'warn');
|
let parseHint = '';
|
||||||
|
try {
|
||||||
|
const rawParsed = JSON.parse(argsString);
|
||||||
|
const rawOpsLen = rawParsed?.operations?.length ?? 0;
|
||||||
|
if (rawOpsLen > 0) {
|
||||||
|
parseHint = `(响应含 ${rawOpsLen} 条操作,但全部未通过格式校验)`;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
parseHint = '(响应 JSON 解析失败)';
|
||||||
|
}
|
||||||
|
log(`批次 ${batchNum} FC 操作列表为空${parseHint},原始响应:\n${argsString}`, 'warn');
|
||||||
toastr.info('AI 判断此批次无需修改。', `批次 ${batchNum}`);
|
toastr.info('AI 判断此批次无需修改。', `批次 ${batchNum}`);
|
||||||
} else {
|
} else {
|
||||||
await updateTableFromOps(ops, { immediateDelete: true });
|
await updateTableFromOps(ops, { immediateDelete: true });
|
||||||
@@ -417,7 +427,9 @@ export function startBatchFilling() {
|
|||||||
manualStopRequested = false;
|
manualStopRequested = false;
|
||||||
const context = getContext();
|
const context = getContext();
|
||||||
chatHistoryLength = context.chat.length;
|
chatHistoryLength = context.chat.length;
|
||||||
threshold = parseInt(document.getElementById('batch-filling-threshold')?.value, 10) || 30;
|
threshold = extension_settings[extensionName]?.batch_filling_threshold
|
||||||
|
?? parseInt(/** @type {HTMLInputElement|null} */ (document.getElementById('batch-filling-threshold'))?.value, 10)
|
||||||
|
?? 30;
|
||||||
|
|
||||||
const ruleTemplate = getBatchFillerRuleTemplate();
|
const ruleTemplate = getBatchFillerRuleTemplate();
|
||||||
const flowTemplate = getBatchFillerFlowTemplate();
|
const flowTemplate = getBatchFillerFlowTemplate();
|
||||||
@@ -562,7 +574,17 @@ export async function startFloorRangeFilling(startFloor, endFloor) {
|
|||||||
if (!argsString) throw new Error('Function Call 返回为空。');
|
if (!argsString) throw new Error('Function Call 返回为空。');
|
||||||
const ops = parseToolCallArgs(argsString);
|
const ops = parseToolCallArgs(argsString);
|
||||||
if (ops.length === 0) {
|
if (ops.length === 0) {
|
||||||
log(`楼层 ${startFloor}-${endFloor} Function Call 返回操作列表为空,无需变更。`, 'warn');
|
let parseHint = '';
|
||||||
|
try {
|
||||||
|
const rawParsed = JSON.parse(argsString);
|
||||||
|
const rawOpsLen = rawParsed?.operations?.length ?? 0;
|
||||||
|
if (rawOpsLen > 0) {
|
||||||
|
parseHint = `(响应含 ${rawOpsLen} 条操作,但全部未通过格式校验)`;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
parseHint = '(响应 JSON 解析失败)';
|
||||||
|
}
|
||||||
|
log(`楼层 ${startFloor}-${endFloor} FC 操作列表为空${parseHint},原始响应:\n${argsString}`, 'warn');
|
||||||
toastr.info('AI 判断此楼层范围无需修改。', `楼层 ${startFloor}-${endFloor}`);
|
toastr.info('AI 判断此楼层范围无需修改。', `楼层 ${startFloor}-${endFloor}`);
|
||||||
} else {
|
} else {
|
||||||
await updateTableFromOps(ops, { immediateDelete: true });
|
await updateTableFromOps(ops, { immediateDelete: true });
|
||||||
|
|||||||
@@ -1021,7 +1021,7 @@ export async function rollbackAndRefill() {
|
|||||||
const lastMessage = context.chat[context.chat.length - 1];
|
const lastMessage = context.chat[context.chat.length - 1];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fillWithSecondaryApi(lastMessage, true);
|
await fillWithSecondaryApi(lastMessage, true, { targetMessage: lastMessage });
|
||||||
log('回退并重新填表操作完成。', 'success');
|
log('回退并重新填表操作完成。', 'success');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(`回退重填过程中发生错误: ${error.message}`, 'error');
|
log(`回退重填过程中发生错误: ${error.message}`, 'error');
|
||||||
|
|||||||
@@ -18,13 +18,15 @@ import { showTableFillReviewModal } from '../../ui/page-window.js';
|
|||||||
const CONTINUE_PROMPT_SECONDARY = '上一条回复不完整或缺少 <Amily2Edit> 指令块。请直接从中断处继续生成剩余内容,不要重复已输出的文本,也不要添加任何解释或寒暄,确保最终输出中包含完整的 <Amily2Edit>...</Amily2Edit> 指令块。';
|
const CONTINUE_PROMPT_SECONDARY = '上一条回复不完整或缺少 <Amily2Edit> 指令块。请直接从中断处继续生成剩余内容,不要重复已输出的文本,也不要添加任何解释或寒暄,确保最终输出中包含完整的 <Amily2Edit>...</Amily2Edit> 指令块。';
|
||||||
|
|
||||||
let secondaryFillerDebounceTimer = null;
|
let secondaryFillerDebounceTimer = null;
|
||||||
|
let secondaryFillerRunning = false;
|
||||||
|
let currentAbortController = null;
|
||||||
|
|
||||||
async function callSecondaryModel(messages) {
|
async function callSecondaryModel(messages, signal) {
|
||||||
const settings = extension_settings[extensionName] || {};
|
const settings = extension_settings[extensionName] || {};
|
||||||
if (settings.nccsEnabled) {
|
if (settings.nccsEnabled) {
|
||||||
return await callNccsAI(messages);
|
return await callNccsAI(messages, { signal });
|
||||||
}
|
}
|
||||||
return await callAI(messages);
|
return await callAI(messages, { signal });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requestSecondaryContinuation(baseMessages, partialResponse) {
|
async function requestSecondaryContinuation(baseMessages, partialResponse) {
|
||||||
@@ -38,23 +40,30 @@ async function requestSecondaryContinuation(baseMessages, partialResponse) {
|
|||||||
return `${partialResponse || ''}${continued}`;
|
return `${partialResponse || ''}${continued}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function commitSecondaryFillResult(rawContent, targetMessages) {
|
async function markTargetsProcessed(targetMessages, { skipTableSave = false } = {}) {
|
||||||
updateTableFromText(rawContent);
|
if (!targetMessages || targetMessages.length === 0) return;
|
||||||
|
|
||||||
const memoryState = getMemoryState();
|
|
||||||
const lastProcessedMsg = targetMessages[targetMessages.length - 1].msg;
|
const lastProcessedMsg = targetMessages[targetMessages.length - 1].msg;
|
||||||
|
|
||||||
for (const target of targetMessages) {
|
for (const target of targetMessages) {
|
||||||
if (!target.msg.metadata) target.msg.metadata = {};
|
if (!target.msg.extra) target.msg.extra = {};
|
||||||
target.msg.metadata.Amily2_Process_Hash = target.hash;
|
target.msg.extra.amily2_process_hash = target.hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!skipTableSave) {
|
||||||
|
const memoryState = getMemoryState();
|
||||||
if (saveStateToMessage(memoryState, lastProcessedMsg)) {
|
if (saveStateToMessage(memoryState, lastProcessedMsg)) {
|
||||||
renderTables();
|
renderTables();
|
||||||
updateOrInsertTableInChat();
|
updateOrInsertTableInChat();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
saveChat();
|
await saveChat();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function commitSecondaryFillResult(rawContent, targetMessages) {
|
||||||
|
await updateTableFromText(rawContent);
|
||||||
|
await markTargetsProcessed(targetMessages);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -110,11 +119,16 @@ async function getWorldBookContext() {
|
|||||||
return content.trim() ? `<世界书>\n${content.trim()}\n</世界书>` : '';
|
return content.trim() ? `<世界书>\n${content.trim()}\n</世界书>` : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
|
export async function fillWithSecondaryApi(latestMessage, forceRun = false, opts = {}) {
|
||||||
|
if (secondaryFillerRunning) {
|
||||||
|
log('分步填表正在进行中,跳过本次触发。', 'warn');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const settings = extension_settings[extensionName] || {};
|
const settings = extension_settings[extensionName] || {};
|
||||||
|
|
||||||
// 【V2.1.1】分步填表触发延迟 / 防抖:自动触发时若配置了延迟,则延后执行,
|
// 【V2.1.1】分步填表触发延迟 / 防抖:自动触发时若配置了延迟,则延后执行,
|
||||||
// 延迟期内再次到来的事件会重置计时器,避免消息连续到达时重复拉起填表。
|
// 延迟期内再次到来的事件会重置计时器,避免消息连续到达时重复拉起填表。
|
||||||
|
// 注意:防抖与早返路径都不持锁,避免 setTimeout 回调撞上自己的锁导致死锁。
|
||||||
const delay = Math.max(0, parseInt(settings.secondary_filler_delay || 0, 10));
|
const delay = Math.max(0, parseInt(settings.secondary_filler_delay || 0, 10));
|
||||||
if (!forceRun && delay > 0) {
|
if (!forceRun && delay > 0) {
|
||||||
if (secondaryFillerDebounceTimer) {
|
if (secondaryFillerDebounceTimer) {
|
||||||
@@ -122,7 +136,7 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
|
|||||||
}
|
}
|
||||||
secondaryFillerDebounceTimer = setTimeout(() => {
|
secondaryFillerDebounceTimer = setTimeout(() => {
|
||||||
secondaryFillerDebounceTimer = null;
|
secondaryFillerDebounceTimer = null;
|
||||||
fillWithSecondaryApi(latestMessage, forceRun);
|
fillWithSecondaryApi(latestMessage, forceRun, opts);
|
||||||
}, delay);
|
}, delay);
|
||||||
console.log(`[Amily2-副API] 分步填表已按防抖延迟 ${delay}ms 调度。`);
|
console.log(`[Amily2-副API] 分步填表已按防抖延迟 ${delay}ms 调度。`);
|
||||||
return;
|
return;
|
||||||
@@ -157,33 +171,25 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 所有早返检查通过后再获取锁,确保 finally 一定能解锁
|
||||||
|
secondaryFillerRunning = true;
|
||||||
|
currentAbortController = new AbortController();
|
||||||
|
const signal = currentAbortController.signal;
|
||||||
try {
|
try {
|
||||||
const bufferSize = parseInt(settings.secondary_filler_buffer || 0, 10);
|
const bufferSize = parseInt(settings.secondary_filler_buffer || 0, 10);
|
||||||
const batchSize = parseInt(settings.secondary_filler_batch || 0, 10);
|
const batchSize = parseInt(settings.secondary_filler_batch || 0, 10);
|
||||||
const contextLimit = parseInt(settings.secondary_filler_context || 2, 10);
|
const contextLimit = parseInt(settings.secondary_filler_context || 2, 10);
|
||||||
|
|
||||||
// 【V1.7.7 修复】限制最大回溯深度,防止更新后无限填补旧历史
|
// 【V1.7.7 修复】限制最大回溯深度,防止更新后无限填补旧历史
|
||||||
// 响应用户反馈:扫描深度 = 上下文 + 填表批次 + 保留楼层 + 冗余量(10)
|
// 扫描深度 = 上下文 + 填表批次 + 冗余量(10)
|
||||||
// redundancy (冗余量): 额外扫描 10 层作为安全缓冲,防止因消息索引计算偏差导致漏掉边缘消息
|
// bufferSize(保留楼层)仅用于限定尾部边界 validEndIndex,
|
||||||
|
// 不再回流到扫描起点,避免重复影响范围
|
||||||
const redundancy = 10;
|
const redundancy = 10;
|
||||||
const maxScanDepth = contextLimit + batchSize + bufferSize + redundancy;
|
const maxScanDepth = contextLimit + batchSize + redundancy;
|
||||||
|
|
||||||
const chat = context.chat;
|
const chat = context.chat;
|
||||||
const totalMessages = chat.length;
|
const totalMessages = chat.length;
|
||||||
|
|
||||||
const validEndIndex = totalMessages - 1 - bufferSize;
|
|
||||||
// 计算扫描的起始索引(不小于0)
|
|
||||||
const scanStartIndex = Math.max(0, validEndIndex - maxScanDepth);
|
|
||||||
|
|
||||||
if (validEndIndex < 0) {
|
|
||||||
console.log(`[Amily2-副API] 消息数量不足以超出保留区(${bufferSize}),跳过。`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let targetMessages = [];
|
|
||||||
let needsProcessing = false;
|
|
||||||
|
|
||||||
const getContentHash = (content) => {
|
const getContentHash = (content) => {
|
||||||
let hash = 0, i, chr;
|
let hash = 0, i, chr;
|
||||||
if (content.length === 0) return hash;
|
if (content.length === 0) return hash;
|
||||||
@@ -195,6 +201,35 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
|
|||||||
return hash;
|
return hash;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let targetMessages = [];
|
||||||
|
|
||||||
|
// 【SWIPED 旁路】swipe 后强制处理刚切出来的最新消息:
|
||||||
|
// 跳过扫描 / bufferSize / batchSize 累积逻辑,直接锁定目标
|
||||||
|
if (opts.targetMessage) {
|
||||||
|
const targetIndex = chat.indexOf(opts.targetMessage);
|
||||||
|
if (targetIndex < 0) {
|
||||||
|
console.log("[Amily2-副API] 旁路目标消息不在聊天列表中,跳过。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (opts.targetMessage.is_user) {
|
||||||
|
console.log("[Amily2-副API] 旁路目标是用户消息,跳过。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
targetMessages.push({
|
||||||
|
index: targetIndex,
|
||||||
|
msg: opts.targetMessage,
|
||||||
|
hash: getContentHash(opts.targetMessage.mes),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 常规扫描路径
|
||||||
|
const validEndIndex = totalMessages - 1 - bufferSize;
|
||||||
|
const scanStartIndex = Math.max(0, validEndIndex - maxScanDepth);
|
||||||
|
|
||||||
|
if (validEndIndex < 0) {
|
||||||
|
console.log(`[Amily2-副API] 消息数量不足以超出保留区(${bufferSize}),跳过。`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 【修复】改为正向扫描,优先处理最老的未处理消息,防止遗留消息被挤出扫描区
|
// 【修复】改为正向扫描,优先处理最老的未处理消息,防止遗留消息被挤出扫描区
|
||||||
for (let i = scanStartIndex; i <= validEndIndex; i++) {
|
for (let i = scanStartIndex; i <= validEndIndex; i++) {
|
||||||
const msg = chat[i];
|
const msg = chat[i];
|
||||||
@@ -202,7 +237,7 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
|
|||||||
if (msg.is_user) continue;
|
if (msg.is_user) continue;
|
||||||
|
|
||||||
const currentHash = getContentHash(msg.mes);
|
const currentHash = getContentHash(msg.mes);
|
||||||
const savedHash = msg.metadata?.Amily2_Process_Hash;
|
const savedHash = msg.extra?.amily2_process_hash;
|
||||||
|
|
||||||
const isUnprocessed = !savedHash;
|
const isUnprocessed = !savedHash;
|
||||||
const isChanged = savedHash && savedHash !== currentHash;
|
const isChanged = savedHash && savedHash !== currentHash;
|
||||||
@@ -211,7 +246,6 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
|
|||||||
targetMessages.push({ index: i, msg: msg, hash: currentHash });
|
targetMessages.push({ index: i, msg: msg, hash: currentHash });
|
||||||
|
|
||||||
if (batchSize > 0 && targetMessages.length >= batchSize) {
|
if (batchSize > 0 && targetMessages.length >= batchSize) {
|
||||||
needsProcessing = true;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -230,6 +264,7 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
|
|||||||
} else {
|
} else {
|
||||||
targetMessages = [targetMessages[targetMessages.length - 1]];
|
targetMessages = [targetMessages[targetMessages.length - 1]];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`[Amily2-副API] 触发填表: 处理 ${targetMessages.length} 条消息。索引范围: ${targetMessages[0].index} - ${targetMessages[targetMessages.length-1].index}`);
|
console.log(`[Amily2-副API] 触发填表: 处理 ${targetMessages.length} 条消息。索引范围: ${targetMessages[0].index} - ${targetMessages[targetMessages.length-1].index}`);
|
||||||
toastr.info(`分步填表正在执行,正在填写 ${targetMessages[0].index + 1} 楼至 ${targetMessages[targetMessages.length-1].index + 1} 楼的内容`, "Amily2-分步填表");
|
toastr.info(`分步填表正在执行,正在填写 ${targetMessages[0].index + 1} 楼至 ${targetMessages[targetMessages.length-1].index + 1} 楼的内容`, "Amily2-分步填表");
|
||||||
@@ -338,17 +373,27 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
|
|||||||
|
|
||||||
if (settings.tableFillFunctionCall) {
|
if (settings.tableFillFunctionCall) {
|
||||||
// Function Call 路径
|
// Function Call 路径
|
||||||
const argsString = await callAIForTools(messages, TABLE_FILL_TOOL, { slot: 'tableFilling' });
|
const argsString = await callAIForTools(messages, TABLE_FILL_TOOL, { slot: 'tableFilling', signal });
|
||||||
if (!argsString) {
|
if (!argsString) {
|
||||||
console.error('[Amily2-副API] Function Call 返回为空。');
|
console.error('[Amily2-副API] Function Call 返回为空。');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ops = parseToolCallArgs(argsString);
|
const ops = parseToolCallArgs(argsString);
|
||||||
if (ops.length === 0) {
|
if (ops.length === 0) {
|
||||||
console.warn('[Amily2-副API] Function Call 返回操作列表为空,无需变更。');
|
let parseHint = '';
|
||||||
|
try {
|
||||||
|
const rawParsed = JSON.parse(argsString);
|
||||||
|
const rawOpsLen = rawParsed?.operations?.length ?? 0;
|
||||||
|
if (rawOpsLen > 0) parseHint = `(响应含 ${rawOpsLen} 条操作,但全部未通过格式校验)`;
|
||||||
|
} catch {
|
||||||
|
parseHint = '(响应 JSON 解析失败)';
|
||||||
|
}
|
||||||
|
console.warn(`[Amily2-副API] Function Call 返回操作列表为空${parseHint},原始响应:\n${argsString}`);
|
||||||
toastr.info('AI 判断此范围无需修改。', 'Amily2-分步填表');
|
toastr.info('AI 判断此范围无需修改。', 'Amily2-分步填表');
|
||||||
|
await markTargetsProcessed(targetMessages, { skipTableSave: true });
|
||||||
} else {
|
} else {
|
||||||
await updateTableFromOps(ops);
|
await updateTableFromOps(ops);
|
||||||
|
await markTargetsProcessed(targetMessages);
|
||||||
toastr.success('分步填表(Function Call)执行完毕。', 'Amily2-分步填表');
|
toastr.success('分步填表(Function Call)执行完毕。', 'Amily2-分步填表');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -356,10 +401,10 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
|
|||||||
let rawContent;
|
let rawContent;
|
||||||
if (settings.nccsEnabled) {
|
if (settings.nccsEnabled) {
|
||||||
console.log('[Amily2-副API] 使用 Nccs API 进行分步填表...');
|
console.log('[Amily2-副API] 使用 Nccs API 进行分步填表...');
|
||||||
rawContent = await callNccsAI(messages);
|
rawContent = await callNccsAI(messages, { signal });
|
||||||
} else {
|
} else {
|
||||||
console.log('[Amily2-副API] 使用 tableFilling slot 进行分步填表...');
|
console.log('[Amily2-副API] 使用 tableFilling slot 进行分步填表...');
|
||||||
rawContent = await callAI(messages, { slot: 'tableFilling' });
|
rawContent = await callAI(messages, { slot: 'tableFilling', signal });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!rawContent) {
|
if (!rawContent) {
|
||||||
@@ -373,8 +418,8 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
|
|||||||
const rangeLabel = `${targetMessages[0].index + 1} - ${targetMessages[targetMessages.length - 1].index + 1}`;
|
const rangeLabel = `${targetMessages[0].index + 1} - ${targetMessages[targetMessages.length - 1].index + 1}`;
|
||||||
console.warn(`[Amily2-副API] 响应未包含 <Amily2Edit> 指令块(楼层 ${rangeLabel}),弹出检查窗口等待用户处理。`);
|
console.warn(`[Amily2-副API] 响应未包含 <Amily2Edit> 指令块(楼层 ${rangeLabel}),弹出检查窗口等待用户处理。`);
|
||||||
toastr.warning(`分步填表(楼层 ${rangeLabel})的响应缺少 <Amily2Edit> 指令块,请在弹窗中处理。`, 'Amily2-分步填表');
|
toastr.warning(`分步填表(楼层 ${rangeLabel})的响应缺少 <Amily2Edit> 指令块,请在弹窗中处理。`, 'Amily2-分步填表');
|
||||||
if (latestMessage && latestMessage.metadata) {
|
if (latestMessage && latestMessage.extra) {
|
||||||
delete latestMessage.metadata.Amily2_Retry_Count;
|
delete latestMessage.extra.amily2_retry_count;
|
||||||
}
|
}
|
||||||
showTableFillReviewModal(rawContent, {
|
showTableFillReviewModal(rawContent, {
|
||||||
title: `分步填表响应检查 - 楼层 ${rangeLabel}`,
|
title: `分步填表响应检查 - 楼层 ${rangeLabel}`,
|
||||||
@@ -389,12 +434,12 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
|
|||||||
}
|
}
|
||||||
return merged;
|
return merged;
|
||||||
},
|
},
|
||||||
onApply: (editedText) => {
|
onApply: async (editedText) => {
|
||||||
if (!editedText || !editedText.includes('<Amily2Edit>')) {
|
if (!editedText || !editedText.includes('<Amily2Edit>')) {
|
||||||
toastr.warning('应用的文本中未检测到 <Amily2Edit> 指令块,已按原文尝试写入。', '手动应用');
|
toastr.warning('应用的文本中未检测到 <Amily2Edit> 指令块,已按原文尝试写入。', '手动应用');
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
commitSecondaryFillResult(editedText, targetMessages);
|
await commitSecondaryFillResult(editedText, targetMessages);
|
||||||
toastr.success('分步填表已由用户手动处理完成。', 'Amily2-分步填表');
|
toastr.success('分步填表已由用户手动处理完成。', 'Amily2-分步填表');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[Amily2-副API] 手动应用失败:', err);
|
console.error('[Amily2-副API] 手动应用失败:', err);
|
||||||
@@ -402,11 +447,11 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRetry: () => {
|
onRetry: () => {
|
||||||
if (latestMessage && latestMessage.metadata) {
|
if (latestMessage && latestMessage.extra) {
|
||||||
delete latestMessage.metadata.Amily2_Retry_Count;
|
delete latestMessage.extra.amily2_retry_count;
|
||||||
}
|
}
|
||||||
toastr.info('将重新执行分步填表...', 'Amily2-分步填表');
|
toastr.info('将重新执行分步填表...', 'Amily2-分步填表');
|
||||||
setTimeout(() => fillWithSecondaryApi(latestMessage, forceRun), 300);
|
setTimeout(() => fillWithSecondaryApi(latestMessage, forceRun, opts), 300);
|
||||||
},
|
},
|
||||||
onCancel: () => {
|
onCancel: () => {
|
||||||
toastr.info('已取消本次分步填表。', 'Amily2-分步填表');
|
toastr.info('已取消本次分步填表。', 'Amily2-分步填表');
|
||||||
@@ -415,43 +460,83 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
commitSecondaryFillResult(rawContent, targetMessages);
|
await commitSecondaryFillResult(rawContent, targetMessages);
|
||||||
}
|
}
|
||||||
toastr.success("分步填表执行完毕。", "Amily2-分步填表");
|
toastr.success("分步填表执行完毕。", "Amily2-分步填表");
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error?.name === 'AbortError' || signal.aborted) {
|
||||||
|
console.warn('[Amily2-副API] 分步填表已被用户中断,跳过结果处理与重试。');
|
||||||
|
toastr.info('分步填表已中断。', 'Amily2-分步填表');
|
||||||
|
if (latestMessage && latestMessage.extra) {
|
||||||
|
delete latestMessage.extra.amily2_retry_count;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.error(`[Amily2-副API] 发生严重错误:`, error);
|
console.error(`[Amily2-副API] 发生严重错误:`, error);
|
||||||
|
|
||||||
// 【新增】自定义重试逻辑
|
// 【新增】自定义重试逻辑
|
||||||
const maxRetries = parseInt(settings.secondary_filler_max_retries || 0, 10);
|
const maxRetries = parseInt(settings.secondary_filler_max_retries || 0, 10);
|
||||||
const currentRetryCount = latestMessage?.metadata?.Amily2_Retry_Count || 0;
|
const currentRetryCount = latestMessage?.extra?.amily2_retry_count || 0;
|
||||||
|
|
||||||
if (currentRetryCount < maxRetries) {
|
if (currentRetryCount < maxRetries) {
|
||||||
const nextRetryCount = currentRetryCount + 1;
|
const nextRetryCount = currentRetryCount + 1;
|
||||||
console.log(`[Amily2-副API] 准备进行第 ${nextRetryCount}/${maxRetries} 次重试...`);
|
console.log(`[Amily2-副API] 准备进行第 ${nextRetryCount}/${maxRetries} 次重试...`);
|
||||||
toastr.warning(`副API填表失败: ${error.message}。将在3秒后进行第 ${nextRetryCount} 次重试...`, "自动重试");
|
toastr.warning(`副API填表失败: ${error.message}。将在3秒后进行第 ${nextRetryCount} 次重试...`, "自动重试");
|
||||||
|
|
||||||
// 记录重试次数到最新消息的 metadata 中,以便跨调用传递状态
|
// 记录重试次数到最新消息的 extra 中,以便跨调用传递状态(跟 amily2_tables_data 一起持久化)
|
||||||
if (latestMessage) {
|
if (latestMessage) {
|
||||||
if (!latestMessage.metadata) latestMessage.metadata = {};
|
if (!latestMessage.extra) latestMessage.extra = {};
|
||||||
latestMessage.metadata.Amily2_Retry_Count = nextRetryCount;
|
latestMessage.extra.amily2_retry_count = nextRetryCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
fillWithSecondaryApi(latestMessage, forceRun);
|
fillWithSecondaryApi(latestMessage, forceRun, opts);
|
||||||
}, 3000);
|
}, 3000);
|
||||||
} else {
|
} else {
|
||||||
console.log(`[Amily2-副API] 已达到最大重试次数 (${maxRetries}),放弃本次填表。`);
|
console.log(`[Amily2-副API] 已达到最大重试次数 (${maxRetries}),放弃本次填表。`);
|
||||||
toastr.error(`副API填表失败: ${error.message}。已达到最大重试次数,任务终止。`, "严重错误");
|
toastr.error(`副API填表失败: ${error.message}。已达到最大重试次数,任务终止。`, "严重错误");
|
||||||
|
|
||||||
// 清除重试计数器
|
// 清除重试计数器
|
||||||
if (latestMessage && latestMessage.metadata) {
|
if (latestMessage && latestMessage.extra) {
|
||||||
delete latestMessage.metadata.Amily2_Retry_Count;
|
delete latestMessage.extra.amily2_retry_count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
secondaryFillerRunning = false;
|
||||||
|
currentAbortController = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resetSecondaryFillerLock() {
|
||||||
|
const wasLocked = secondaryFillerRunning;
|
||||||
|
if (secondaryFillerDebounceTimer) {
|
||||||
|
clearTimeout(secondaryFillerDebounceTimer);
|
||||||
|
secondaryFillerDebounceTimer = null;
|
||||||
|
}
|
||||||
|
if (currentAbortController) {
|
||||||
|
try { currentAbortController.abort(); } catch {}
|
||||||
|
currentAbortController = null;
|
||||||
|
}
|
||||||
|
secondaryFillerRunning = false;
|
||||||
|
return wasLocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSecondaryFillerRunning() {
|
||||||
|
return secondaryFillerRunning;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function abortCurrentSecondaryFiller() {
|
||||||
|
if (!secondaryFillerRunning && !currentAbortController) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (currentAbortController) {
|
||||||
|
try { currentAbortController.abort(); } catch {}
|
||||||
|
}
|
||||||
|
// 锁的释放由 finally 完成;这里只发出中断信号
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
async function getHistoryContext(messagesToFetch, historyEndIndex, tagsToExtract, exclusionRules) {
|
async function getHistoryContext(messagesToFetch, historyEndIndex, tagsToExtract, exclusionRules) {
|
||||||
const context = getContext();
|
const context = getContext();
|
||||||
const chat = context.chat;
|
const chat = context.chat;
|
||||||
|
|||||||
@@ -161,4 +161,7 @@ export const tableSystemDefaultSettings = {
|
|||||||
|
|
||||||
// Function Call 填表
|
// Function Call 填表
|
||||||
tableFillFunctionCall: false,
|
tableFillFunctionCall: false,
|
||||||
|
|
||||||
|
// 批量填表每批楼层数
|
||||||
|
batch_filling_threshold: 30,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export { injectTableData, generateTableContent } from "./core/table-system/injec
|
|||||||
export { initialize as initializeRagProcessor } from "./core/rag-processor.js";
|
export { initialize as initializeRagProcessor } from "./core/rag-processor.js";
|
||||||
export { loadSettingsToUI as loadHanlinyuanSettingsToUI } from "./ui/hanlinyuan-bindings.js";
|
export { loadSettingsToUI as loadHanlinyuanSettingsToUI } from "./ui/hanlinyuan-bindings.js";
|
||||||
export { loadTables, clearHighlights, rollbackAndRefill, rollbackState, commitPendingDeletions, saveStateToMessage, getMemoryState, clearUpdatedTables } from './core/table-system/manager.js';
|
export { loadTables, clearHighlights, rollbackAndRefill, rollbackState, commitPendingDeletions, saveStateToMessage, getMemoryState, clearUpdatedTables } from './core/table-system/manager.js';
|
||||||
export { fillWithSecondaryApi } from './core/table-system/secondary-filler.js';
|
export { fillWithSecondaryApi, resetSecondaryFillerLock, isSecondaryFillerRunning, abortCurrentSecondaryFiller } from './core/table-system/secondary-filler.js';
|
||||||
export { renderTables } from './ui/table-bindings.js';
|
export { renderTables } from './ui/table-bindings.js';
|
||||||
export { log } from './core/table-system/logger.js';
|
export { log } from './core/table-system/logger.js';
|
||||||
export { checkForUpdates, fetchMessageBoardContent } from './core/api.js';
|
export { checkForUpdates, fetchMessageBoardContent } from './core/api.js';
|
||||||
|
|||||||
2
index.js
2
index.js
@@ -697,7 +697,7 @@ function registerEventListeners() {
|
|||||||
log(`【监察系统】主填表模式,回退后强制刷新消息ID: ${chat_id}。`, 'info');
|
log(`【监察系统】主填表模式,回退后强制刷新消息ID: ${chat_id}。`, 'info');
|
||||||
await handleTableUpdate(chat_id, true);
|
await handleTableUpdate(chat_id, true);
|
||||||
} else if (fillingMode === 'secondary-api' || fillingMode === 'optimized') {
|
} else if (fillingMode === 'secondary-api' || fillingMode === 'optimized') {
|
||||||
log('【监察系统】分步/优化模式,回退后强制二次填表最新消息。', 'info');
|
log('【监察系统】分步/优化模式,回退后触发二次填表扫描(受保留缓冲区限制)。', 'info');
|
||||||
await fillWithSecondaryApi(latestMessage, true);
|
await fillWithSecondaryApi(latestMessage, true);
|
||||||
} else {
|
} else {
|
||||||
log('【监察系统】未配置填表模式,跳过填表。', 'info');
|
log('【监察系统】未配置填表模式,跳过填表。', 'info');
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "Amily2号聊天优化助手",
|
"name": "Amily2号聊天优化助手",
|
||||||
"display_name": "Amily2号助手",
|
"display_name": "Amily2号助手",
|
||||||
"version": "2.2.3",
|
"version": "2.2.5",
|
||||||
"author": "Wx-2025",
|
"author": "Wx-2025",
|
||||||
"description": "一个拥有独立UI的智能引擎,正文优化、自动总结、记忆表格、rag向量、隐藏楼层、剧情推进等多功能整合。",
|
"description": "一个拥有独立UI的智能引擎,正文优化、自动总结、记忆表格、rag向量、隐藏楼层、剧情推进等多功能整合。",
|
||||||
"minSillyTavernVersion": "1.10.0",
|
"minSillyTavernVersion": "1.10.0",
|
||||||
|
|||||||
@@ -651,6 +651,7 @@ export function loadSettingsToUI() {
|
|||||||
|
|
||||||
// Rerank 设置
|
// Rerank 设置
|
||||||
document.getElementById('hly-rerank-enabled').checked = settings.rerank.enabled;
|
document.getElementById('hly-rerank-enabled').checked = settings.rerank.enabled;
|
||||||
|
/** @type {HTMLSelectElement} */ (document.getElementById('hly-rerank-api-mode')).value = settings.rerank.apiMode ?? 'custom';
|
||||||
document.getElementById('hly-rerank-url').value = settings.rerank.url;
|
document.getElementById('hly-rerank-url').value = settings.rerank.url;
|
||||||
document.getElementById('hly-rerank-api-key').value = settings.rerank.apiKey;
|
document.getElementById('hly-rerank-api-key').value = settings.rerank.apiKey;
|
||||||
const rerankModelSelect = document.getElementById('hly-rerank-model');
|
const rerankModelSelect = document.getElementById('hly-rerank-model');
|
||||||
@@ -705,6 +706,11 @@ function saveSettingsFromUI(isAutoSave = true) {
|
|||||||
const key = target.dataset.settingKey;
|
const key = target.dataset.settingKey;
|
||||||
if (!key) return;
|
if (!key) return;
|
||||||
|
|
||||||
|
// 被 profile-sync 接管的字段(祖先元素带 data-profile-hidden)会被填充
|
||||||
|
// MASKED_KEY 占位符并隐藏,若一并写回会污染 settings.{rerank,retrieval}.apiKey
|
||||||
|
// 等字段为 '••••••••',导致取消 Profile 分配后实际请求带占位符 token 被 401。
|
||||||
|
if (target.closest('[data-profile-hidden]')) return;
|
||||||
|
|
||||||
let value;
|
let value;
|
||||||
const type = target.dataset.type || 'string';
|
const type = target.dataset.type || 'string';
|
||||||
|
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ const SLOT_CONFIGS = {
|
|||||||
ragRerank: {
|
ragRerank: {
|
||||||
container: '#hly-rerank-tab .hly-settings-group',
|
container: '#hly-rerank-tab .hly-settings-group',
|
||||||
hideParentBlock: ['#hly-rerank-api-mode', '#hly-rerank-url', '#hly-rerank-api-key', '#hly-rerank-model'],
|
hideParentBlock: ['#hly-rerank-api-mode', '#hly-rerank-url', '#hly-rerank-api-key', '#hly-rerank-model'],
|
||||||
fields: { provider: '#hly-rerank-api-mode', apiUrl: '#hly-rerank-url', model: '#hly-rerank-model' },
|
fields: { apiUrl: '#hly-rerank-url', model: '#hly-rerank-model' },
|
||||||
keyField: '#hly-rerank-api-key',
|
keyField: '#hly-rerank-api-key',
|
||||||
testFn: async () => {
|
testFn: async () => {
|
||||||
await executeRagRerank('test', ['test'], null);
|
await executeRagRerank('test', ['test'], null);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { extensionName } from '../utils/settings.js';
|
|||||||
import { updateOrInsertTableInChat } from './message-table-renderer.js';
|
import { updateOrInsertTableInChat } from './message-table-renderer.js';
|
||||||
import { saveSettingsDebounced } from '/script.js';
|
import { saveSettingsDebounced } from '/script.js';
|
||||||
import { startBatchFilling } from '../core/table-system/batch-filler.js';
|
import { startBatchFilling } from '../core/table-system/batch-filler.js';
|
||||||
|
import { resetSecondaryFillerLock, isSecondaryFillerRunning, abortCurrentSecondaryFiller } from '../core/table-system/secondary-filler.js';
|
||||||
import { showHtmlModal } from './page-window.js';
|
import { showHtmlModal } from './page-window.js';
|
||||||
import { DEFAULT_AI_RULE_TEMPLATE, DEFAULT_AI_FLOW_TEMPLATE } from '../core/table-system/settings.js';
|
import { DEFAULT_AI_RULE_TEMPLATE, DEFAULT_AI_FLOW_TEMPLATE } from '../core/table-system/settings.js';
|
||||||
import { world_names, loadWorldInfo } from '/scripts/world-info.js';
|
import { world_names, loadWorldInfo } from '/scripts/world-info.js';
|
||||||
@@ -1370,8 +1371,9 @@ export function bindTableEvents(panelElement = null) {
|
|||||||
const contextSlider = document.getElementById('secondary-filler-context');
|
const contextSlider = document.getElementById('secondary-filler-context');
|
||||||
const batchSlider = document.getElementById('secondary-filler-batch');
|
const batchSlider = document.getElementById('secondary-filler-batch');
|
||||||
const bufferSlider = document.getElementById('secondary-filler-buffer');
|
const bufferSlider = document.getElementById('secondary-filler-buffer');
|
||||||
const maxRetriesSlider = document.getElementById('secondary-filler-max-retries'); // 【新增】
|
const maxRetriesSlider = document.getElementById('secondary-filler-max-retries');
|
||||||
const delaySlider = document.getElementById('secondary-filler-delay');
|
const delaySlider = document.getElementById('secondary-filler-delay');
|
||||||
|
const batchFillingThresholdInput = document.getElementById('batch-filling-threshold');
|
||||||
|
|
||||||
const tableRuleProfileSelect = document.getElementById('table-rule-profile-select');
|
const tableRuleProfileSelect = document.getElementById('table-rule-profile-select');
|
||||||
|
|
||||||
@@ -1458,6 +1460,58 @@ export function bindTableEvents(panelElement = null) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (batchFillingThresholdInput) {
|
||||||
|
const value = extension_settings[extensionName]?.batch_filling_threshold ?? 30;
|
||||||
|
batchFillingThresholdInput.value = value;
|
||||||
|
|
||||||
|
batchFillingThresholdInput.addEventListener('change', function() {
|
||||||
|
const parsed = Math.max(1, parseInt(this.value, 10) || 30);
|
||||||
|
this.value = parsed;
|
||||||
|
updateAndSaveTableSetting('batch_filling_threshold', parsed);
|
||||||
|
toastr.info(`批处理阈值已设置为 ${parsed}。`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const abortBtn = document.getElementById('amily2-abort-secondary-filler');
|
||||||
|
const resetLockBtn = document.getElementById('amily2-reset-secondary-filler-lock');
|
||||||
|
const lockStatusSpan = document.getElementById('amily2-secondary-filler-lock-status');
|
||||||
|
if ((abortBtn || resetLockBtn) && lockStatusSpan) {
|
||||||
|
const refreshLockStatus = () => {
|
||||||
|
const running = isSecondaryFillerRunning();
|
||||||
|
lockStatusSpan.textContent = running ? '状态:占用中' : '状态:空闲';
|
||||||
|
lockStatusSpan.style.color = running ? 'var(--SmartThemeQuoteColor, #d97706)' : '';
|
||||||
|
};
|
||||||
|
refreshLockStatus();
|
||||||
|
if (abortBtn) {
|
||||||
|
abortBtn.addEventListener('click', () => {
|
||||||
|
const signaled = abortCurrentSecondaryFiller();
|
||||||
|
if (signaled) {
|
||||||
|
toastr.warning('已发出中断信号,进行中的请求将立即终止,结果会被丢弃。', 'Amily2');
|
||||||
|
log('用户手动中断了当前分步填表(AbortController.abort)。', 'warn');
|
||||||
|
} else {
|
||||||
|
toastr.info('当前没有正在进行的分步填表。', 'Amily2');
|
||||||
|
}
|
||||||
|
setTimeout(refreshLockStatus, 300);
|
||||||
|
});
|
||||||
|
abortBtn.addEventListener('mouseenter', refreshLockStatus);
|
||||||
|
abortBtn.addEventListener('focus', refreshLockStatus);
|
||||||
|
}
|
||||||
|
if (resetLockBtn) {
|
||||||
|
resetLockBtn.addEventListener('click', () => {
|
||||||
|
const wasLocked = resetSecondaryFillerLock();
|
||||||
|
refreshLockStatus();
|
||||||
|
if (wasLocked) {
|
||||||
|
toastr.success('分步填表锁已手动释放。', 'Amily2');
|
||||||
|
log('用户手动释放了分步填表锁(之前处于占用状态)。', 'warn');
|
||||||
|
} else {
|
||||||
|
toastr.info('当前并无锁占用,无需释放。', 'Amily2');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
resetLockBtn.addEventListener('mouseenter', refreshLockStatus);
|
||||||
|
resetLockBtn.addEventListener('focus', refreshLockStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const fcToggle = document.getElementById('table-fill-function-call-enabled');
|
const fcToggle = document.getElementById('table-fill-function-call-enabled');
|
||||||
if (fcToggle) {
|
if (fcToggle) {
|
||||||
fcToggle.checked = extension_settings[extensionName]?.tableFillFunctionCall ?? false;
|
fcToggle.checked = extension_settings[extensionName]?.tableFillFunctionCall ?? false;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
const a0_0x31e962=a0_0x4c2c;(function(_0xb18088,_0x3dccea){const _0x2301d5=a0_0x4c2c,_0x1d8c5a=_0xb18088();while(!![]){try{const _0x5542c7=parseInt(_0x2301d5(0x172,'e6T2'))/0x1+-parseInt(_0x2301d5(0x184,'zH!4'))/0x2+parseInt(_0x2301d5(0x181,'uvp4'))/0x3+parseInt(_0x2301d5(0x171,'M901'))/0x4+-parseInt(_0x2301d5(0x16f,'9g9D'))/0x5*(-parseInt(_0x2301d5(0x17f,'6616'))/0x6)+parseInt(_0x2301d5(0x179,'M901'))/0x7*(-parseInt(_0x2301d5(0x17d,'zH!4'))/0x8)+-parseInt(_0x2301d5(0x174,'Xue!'))/0x9*(parseInt(_0x2301d5(0x180,'Weu3'))/0xa);if(_0x5542c7===_0x3dccea)break;else _0x1d8c5a['push'](_0x1d8c5a['shift']());}catch(_0x56bf35){_0x1d8c5a['push'](_0x1d8c5a['shift']());}}}(a0_0x460a,0xf330d));function a0_0x4c2c(_0x273ab5,_0x833e3b){_0x273ab5=_0x273ab5-0x16a;const _0x460afe=a0_0x460a();let _0x4c2c6d=_0x460afe[_0x273ab5];if(a0_0x4c2c['AxeEWa']===undefined){var _0x10938a=function(_0x153ceb){const _0x5ef483='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';let _0x514ff3='',_0x9c2e8='';for(let _0x473fa6=0x0,_0x3e62d4,_0x48496b,_0x2e760a=0x0;_0x48496b=_0x153ceb['charAt'](_0x2e760a++);~_0x48496b&&(_0x3e62d4=_0x473fa6%0x4?_0x3e62d4*0x40+_0x48496b:_0x48496b,_0x473fa6++%0x4)?_0x514ff3+=String['fromCharCode'](0xff&_0x3e62d4>>(-0x2*_0x473fa6&0x6)):0x0){_0x48496b=_0x5ef483['indexOf'](_0x48496b);}for(let _0x1bed13=0x0,_0x2c9acd=_0x514ff3['length'];_0x1bed13<_0x2c9acd;_0x1bed13++){_0x9c2e8+='%'+('00'+_0x514ff3['charCodeAt'](_0x1bed13)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0x9c2e8);};const _0x1032bf=function(_0x20ebd8,_0x1a8248){let _0x36160c=[],_0x24e72f=0x0,_0x4e540d,_0x3bdbc9='';_0x20ebd8=_0x10938a(_0x20ebd8);let _0x5a67e8;for(_0x5a67e8=0x0;_0x5a67e8<0x100;_0x5a67e8++){_0x36160c[_0x5a67e8]=_0x5a67e8;}for(_0x5a67e8=0x0;_0x5a67e8<0x100;_0x5a67e8++){_0x24e72f=(_0x24e72f+_0x36160c[_0x5a67e8]+_0x1a8248['charCodeAt'](_0x5a67e8%_0x1a8248['length']))%0x100,_0x4e540d=_0x36160c[_0x5a67e8],_0x36160c[_0x5a67e8]=_0x36160c[_0x24e72f],_0x36160c[_0x24e72f]=_0x4e540d;}_0x5a67e8=0x0,_0x24e72f=0x0;for(let _0x1db0ac=0x0;_0x1db0ac<_0x20ebd8['length'];_0x1db0ac++){_0x5a67e8=(_0x5a67e8+0x1)%0x100,_0x24e72f=(_0x24e72f+_0x36160c[_0x5a67e8])%0x100,_0x4e540d=_0x36160c[_0x5a67e8],_0x36160c[_0x5a67e8]=_0x36160c[_0x24e72f],_0x36160c[_0x24e72f]=_0x4e540d,_0x3bdbc9+=String['fromCharCode'](_0x20ebd8['charCodeAt'](_0x1db0ac)^_0x36160c[(_0x36160c[_0x5a67e8]+_0x36160c[_0x24e72f])%0x100]);}return _0x3bdbc9;};a0_0x4c2c['byhWut']=_0x1032bf,a0_0x4c2c['aIsNtK']={},a0_0x4c2c['AxeEWa']=!![];}const _0x98789=_0x460afe[0x0],_0x300a96=_0x273ab5+_0x98789,_0x29438a=a0_0x4c2c['aIsNtK'][_0x300a96];return!_0x29438a?(a0_0x4c2c['AlrPtr']===undefined&&(a0_0x4c2c['AlrPtr']=!![]),_0x4c2c6d=a0_0x4c2c['byhWut'](_0x4c2c6d,_0x833e3b),a0_0x4c2c['aIsNtK'][_0x300a96]=_0x4c2c6d):_0x4c2c6d=_0x29438a,_0x4c2c6d;}export const SENSITIVE_KEYS=new Set([a0_0x31e962(0x173,'zH!4'),a0_0x31e962(0x177,'9g9D'),a0_0x31e962(0x170,'9g9D'),a0_0x31e962(0x17a,'Weu3'),a0_0x31e962(0x16b,'Weu3'),a0_0x31e962(0x183,'QTt6'),a0_0x31e962(0x175,'wKYv'),a0_0x31e962(0x182,'SRqH')]);function a0_0x460a(){const _0x5b1173=['xqhdRaNdVmkaW69fW61x','oetcHrurpCkXkghcHa','xaVdRa3cL8oGWRnaW7negvmY','W7NdKmoqtSoslSkVW7v+yCoNjwy','WPD5j8oqvCoUW45wW5zwqeK','oMDTW5u+xCoPCq','Et5mW4S2ECosyCo8tmobWRRdVmoSWP3cJKfPW7aJcLdcKCoy','hW8oBIfWxmkHgCksrSoSkW','BfmrW77dU1WTWRLpwMLgW4e','gmoXzmokAd8','qCkjWQK2W7hcKgKDoqS','ucnZWOldM8kfWPrXphSm','jmkHa8k7WQS5WPSMhG','Et5mW4S2ECosyCo+u8ogWPldRmoN','wCoyCHpdL1/cP0S','haSopv8bcmkXma','oedcIXurpCkXkghcHa','mSo/W50yW6bVhcFdUNHR','WRldHCoFW5pcU8kBi8kUWR/cGvK','s8k2pSk0pXr1W77cKgKO','nu58eSovWRxcLCk9WPtdO8owpwa','W4xcLKldTuZdOwBdPmo1zmk3W4G','zbddLfbGoCkVef3cNWO','cCojeSkPWQn3W7tdMmkJAmk1W5jM','WRJcNCkhhmkNz8o0W4PJxq','WPtcLgpcLgpcPSkIW6jeWRW','sSk4pSk1p3mnW5NcTw4jWQ3cMq','nmkzWO8GrLnrxSkUyGWeW6e'];a0_0x460a=function(){return _0x5b1173;};return a0_0x460a();}
|
const a0_0xeed94e=a0_0x1d2d;(function(_0x1a15ef,_0x377e1d){const _0xd98876=a0_0x1d2d,_0x5a7541=_0x1a15ef();while(!![]){try{const _0xd9a1c2=-parseInt(_0xd98876(0x16d,')p$['))/0x1*(-parseInt(_0xd98876(0x17f,'$t18'))/0x2)+parseInt(_0xd98876(0x176,'uJ$1'))/0x3*(parseInt(_0xd98876(0x173,'0nh^'))/0x4)+parseInt(_0xd98876(0x185,'G]x0'))/0x5+parseInt(_0xd98876(0x175,'iFK8'))/0x6*(-parseInt(_0xd98876(0x172,'0nh^'))/0x7)+parseInt(_0xd98876(0x16e,'llbF'))/0x8*(-parseInt(_0xd98876(0x17d,'^@If'))/0x9)+parseInt(_0xd98876(0x170,'@9OG'))/0xa*(parseInt(_0xd98876(0x18a,'TuD$'))/0xb)+parseInt(_0xd98876(0x189,'*5*3'))/0xc;if(_0xd9a1c2===_0x377e1d)break;else _0x5a7541['push'](_0x5a7541['shift']());}catch(_0x48a0b9){_0x5a7541['push'](_0x5a7541['shift']());}}}(a0_0x3d0e,0x8e557));function a0_0x1d2d(_0x13ad22,_0x3291b5){_0x13ad22=_0x13ad22-0x16d;const _0x3d0e1=a0_0x3d0e();let _0x1d2d20=_0x3d0e1[_0x13ad22];if(a0_0x1d2d['HknRdg']===undefined){var _0x499a0f=function(_0xd00d1d){const _0x332f54='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';let _0x3117be='',_0x46d902='';for(let _0x3bd389=0x0,_0xa2ce1a,_0x20ac16,_0x4ed342=0x0;_0x20ac16=_0xd00d1d['charAt'](_0x4ed342++);~_0x20ac16&&(_0xa2ce1a=_0x3bd389%0x4?_0xa2ce1a*0x40+_0x20ac16:_0x20ac16,_0x3bd389++%0x4)?_0x3117be+=String['fromCharCode'](0xff&_0xa2ce1a>>(-0x2*_0x3bd389&0x6)):0x0){_0x20ac16=_0x332f54['indexOf'](_0x20ac16);}for(let _0x471831=0x0,_0x1e98a5=_0x3117be['length'];_0x471831<_0x1e98a5;_0x471831++){_0x46d902+='%'+('00'+_0x3117be['charCodeAt'](_0x471831)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0x46d902);};const _0xdaaf25=function(_0x392ffc,_0x55746a){let _0x56e0b4=[],_0x36ea9f=0x0,_0x3efb15,_0x507d29='';_0x392ffc=_0x499a0f(_0x392ffc);let _0x344843;for(_0x344843=0x0;_0x344843<0x100;_0x344843++){_0x56e0b4[_0x344843]=_0x344843;}for(_0x344843=0x0;_0x344843<0x100;_0x344843++){_0x36ea9f=(_0x36ea9f+_0x56e0b4[_0x344843]+_0x55746a['charCodeAt'](_0x344843%_0x55746a['length']))%0x100,_0x3efb15=_0x56e0b4[_0x344843],_0x56e0b4[_0x344843]=_0x56e0b4[_0x36ea9f],_0x56e0b4[_0x36ea9f]=_0x3efb15;}_0x344843=0x0,_0x36ea9f=0x0;for(let _0x3e0c7f=0x0;_0x3e0c7f<_0x392ffc['length'];_0x3e0c7f++){_0x344843=(_0x344843+0x1)%0x100,_0x36ea9f=(_0x36ea9f+_0x56e0b4[_0x344843])%0x100,_0x3efb15=_0x56e0b4[_0x344843],_0x56e0b4[_0x344843]=_0x56e0b4[_0x36ea9f],_0x56e0b4[_0x36ea9f]=_0x3efb15,_0x507d29+=String['fromCharCode'](_0x392ffc['charCodeAt'](_0x3e0c7f)^_0x56e0b4[(_0x56e0b4[_0x344843]+_0x56e0b4[_0x36ea9f])%0x100]);}return _0x507d29;};a0_0x1d2d['cMINFB']=_0xdaaf25,a0_0x1d2d['rMAOMA']={},a0_0x1d2d['HknRdg']=!![];}const _0x7f1111=_0x3d0e1[0x0],_0xed149e=_0x13ad22+_0x7f1111,_0x22e261=a0_0x1d2d['rMAOMA'][_0xed149e];return!_0x22e261?(a0_0x1d2d['Taqwym']===undefined&&(a0_0x1d2d['Taqwym']=!![]),_0x1d2d20=a0_0x1d2d['cMINFB'](_0x1d2d20,_0x3291b5),a0_0x1d2d['rMAOMA'][_0xed149e]=_0x1d2d20):_0x1d2d20=_0x22e261,_0x1d2d20;}export const SENSITIVE_KEYS=new Set([a0_0xeed94e(0x179,'sJZi'),a0_0xeed94e(0x17c,'0nh^'),a0_0xeed94e(0x171,'mm2P'),a0_0xeed94e(0x17e,'NZ6R'),a0_0xeed94e(0x183,'pO#l'),a0_0xeed94e(0x174,'9oir'),a0_0xeed94e(0x186,'uJ$1'),a0_0xeed94e(0x182,'iToR')]);function a0_0x3d0e(){const _0x12d67d=['pCkqdCkyWOpdQWxdNmoQW5Wpcmk+','W5tcQvtdHmkBW7pdJCoQ','W4xdUKv8jZJdJq','f0zxWQxdICoEWQhcIdpdTtS/','WRtdPg3dVInIbmkXw8kdECkE','W4bxWRhdJCoBW7LryM8MlSk2W4NcHvdcP2XRfIVdTqZcGCki','hIukWQlcP8oArSk2rmkKW74o','hMbYW4xdKmk0AG','W4hdPSkGzWRdMhXwW6jV','WRpcP3tdGtdcJSknta','FxiRsmkalWfGW5xdNtD4yG','sZC2w8oIW5vBcG','WRNcRcdcQH8rWR4LsvqAW4NdQq','qGypW53cMCkE','DmkozCkgqqzaDG','WOpdRbRdO8o9DCow','wNLuW6xdNCktr8kdFCkwW4mwWQFcNa','WQ3dVIfDWOeuW556sSoZl8kfWQu','WOHgW4hdSdBcGe7cRWe5','WO92vrhdQuVcGLBdH8oz','rmocWRpcTmotW64G','WR/dOXJcNKGCgSkX','DstcTCkCBSkSW5JcQfyM','WPyMtHxdPIxcOuVcRxm','pZrUAmo6ze41W6RdJSool24','ctLBW5BdLmkcW7jTesZdGexcLq','lZnWlSorz1frW5ddKZ4','W4fWhf7cLwxcKxdcSKKHtq','p8kxbCkzWOldRGtdPCo7W5G7oCk5','W5RdJwCIfSobW6SqwJuGWPxdLG','BCk2qCoDtN0HW5S'];a0_0x3d0e=function(){return _0x12d67d;};return a0_0x3d0e();}
|
||||||
Reference in New Issue
Block a user