mirror of
https://github.com/Wx-2025/ST-Amily2-Chat-Optimisation.git
synced 2026-06-11 07:05:50 +00:00
Compare commits
13 Commits
dabc8992f1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a4a10d42d | ||
|
|
347016d5ac | ||
|
|
59c4adc1c0 | ||
|
|
e66544f774 | ||
|
|
d6b3b00c86 | ||
|
|
a8c3ad9027 | ||
|
|
0e11f85031 | ||
|
|
9bc2f694b0 | ||
|
|
08e1dbde85 | ||
|
|
42e0bdec19 | ||
|
|
3e217e8ed8 | ||
|
|
2c3072a3d8 | ||
|
|
e00302d04b |
@@ -449,6 +449,30 @@ export function bindSettingsEvents($settingsPanel) {
|
|||||||
|
|
||||||
$(document).trigger('cwb:master-switch-changed', { isEnabled: isChecked });
|
$(document).trigger('cwb:master-switch-changed', { isEnabled: isChecked });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 处理来自 API 配置面板总开关同步的 change 事件(该面板通过 dispatchEvent 设置 checkbox 状态)
|
||||||
|
// jQuery 的 .prop('checked') 不触发 change,故与上方 click 处理器不会双重触发
|
||||||
|
$panel.on('change', '#cwb_master_enabled-checkbox', function () {
|
||||||
|
const isChecked = $(this).prop('checked');
|
||||||
|
|
||||||
|
getSettings().cwb_master_enabled = isChecked;
|
||||||
|
|
||||||
|
const overrides = JSON.parse(localStorage.getItem(CWB_BOOLEAN_SETTINGS_OVERRIDE_KEY) || '{}');
|
||||||
|
overrides.cwb_master_enabled = isChecked;
|
||||||
|
localStorage.setItem(CWB_BOOLEAN_SETTINGS_OVERRIDE_KEY, JSON.stringify(overrides));
|
||||||
|
|
||||||
|
state.masterEnabled = isChecked;
|
||||||
|
saveSettingsDebounced();
|
||||||
|
updateControlsLockState();
|
||||||
|
|
||||||
|
const $viewerButton = $(`#${CHAR_CARD_VIEWER_BUTTON_ID}`);
|
||||||
|
if ($viewerButton.length > 0) {
|
||||||
|
$viewerButton.toggle(isChecked && state.viewerEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
showToastr('info', `CharacterWorldBook 已 ${isChecked ? '启用' : '禁用'}`);
|
||||||
|
$(document).trigger('cwb:master-switch-changed', { isEnabled: isChecked });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateApiModeUI(mode) {
|
function updateApiModeUI(mode) {
|
||||||
|
|||||||
94
DEPLOY_NOTE.md
Normal file
94
DEPLOY_NOTE.md
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
# 部署更新日志
|
||||||
|
|
||||||
|
每个版本块格式:`## v{version}`,Jenkins 构建时自动提取对应块作为 GitHub 提交说明。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v2.2.2
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
|
||||||
|
- **Function Call 填表模式**:在填表设置中新增独立开关,启用后支持通过 OpenAI 兼容接口(DeepSeek / OpenRouter / 各类中转等)直接返回结构化操作列表,绕过 `<Amily2Edit>` 文本解析路径,填表更稳定
|
||||||
|
- 遇到不支持 `tool_choice` 的接口时自动降级重试
|
||||||
|
- 对思考模型注入强制调用指令,防止绕过工具直接输出文本
|
||||||
|
- 全部走 ST 后端代理,修复 CSP 拦截直连外部 URL 的问题
|
||||||
|
- **主界面新增提示词链编辑器入口**,同时调换了记忆管理与角色世界书的按钮位置
|
||||||
|
- **规则中心**新增"自动排除用户楼层"选项
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
|
||||||
|
- 提示词链按钮点击无响应(改为事件委托方式绑定)
|
||||||
|
- 拖拽组件微抖误触发(加 5px 移动阈值过滤)
|
||||||
|
- 填表检查窗若干问题修复;翰林院(批量回填)修复;防抖逻辑落地
|
||||||
|
- 角色世界书入口添加使用警告弹窗(强制 10 秒倒计时),提示该功能长期未维护
|
||||||
|
- ApiProfile `fakeStream` 字段保存丢失问题
|
||||||
|
- 正文优化默认改为关闭状态
|
||||||
|
- NGMS / NCCS API 配置槽位标签修正(NGMS→总结,NCCS→填表)
|
||||||
|
- API Profile 面板选择逻辑统一重构,修复多处旧字段覆盖新配置的问题
|
||||||
|
- 世界书控制参数兼容性修复(排除递归、插入位置、扫描深度等,适配 ST 1.17.0+)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v2.2.3
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
|
||||||
|
- Function Call 填表开关下方新增公益站风险提示横幅:部分公益站会屏蔽 tools 参数,请确认支持情况避免被意外封禁
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
|
||||||
|
- **Function Call 填表**:
|
||||||
|
- 修复 ST 代理以 HTTP 200 + error body 形式返回错误、导致降级重试机制从未触发的问题
|
||||||
|
- 修复思考模式模型(如 DeepSeek v4-flash)因 tool_choice 不兼容返回 Bad Request 后正确降级并重试
|
||||||
|
- 重试时自动追加强制调用指令,防止思考模型绕过工具直接输出文本造成无效二次开销
|
||||||
|
- **超级记忆 / 翰林院**:
|
||||||
|
- 修复 `getRagSettings()` 读写顶层路径而非嵌套路径,导致打开超级记忆面板后向量化、归档等开关在重载时被全默认值覆盖的问题
|
||||||
|
- 修复自动归档失效问题
|
||||||
|
- 修复归档管理器在同一事件中被三次触发的回归问题
|
||||||
|
- 修复翰林院设置旧版迁移逻辑异常
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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`
|
||||||
@@ -60,9 +60,16 @@ export function makeDraggable($element, onClick, storageKey) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DRAG_THRESHOLD = 5;
|
||||||
|
|
||||||
const dragMove = (e) => {
|
const dragMove = (e) => {
|
||||||
if (!isDragging) return;
|
if (!isDragging) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (!hasDragged) {
|
||||||
|
const coords = getEventCoords(e.originalEvent || e);
|
||||||
|
const dist = Math.abs(coords.x - startPos.x) + Math.abs(coords.y - startPos.y);
|
||||||
|
if (dist < DRAG_THRESHOLD) return;
|
||||||
|
}
|
||||||
hasDragged = true;
|
hasDragged = true;
|
||||||
|
|
||||||
const coords = getEventCoords(e.originalEvent || e);
|
const coords = getEventCoords(e.originalEvent || e);
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ export function toggleSettingsOrb() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showPresetSettings() {
|
export async function showPresetSettings() {
|
||||||
const template = $(await renderExtensionTemplateAsync(presetSettingsPath, 'prese-settings'));
|
const template = $(await renderExtensionTemplateAsync(presetSettingsPath, 'prese-settings'));
|
||||||
|
|
||||||
renderPresetManager(template);
|
renderPresetManager(template);
|
||||||
|
|||||||
14
TODO.md
14
TODO.md
@@ -46,7 +46,7 @@
|
|||||||
|
|
||||||
- 添加记忆管理并发调用
|
- 添加记忆管理并发调用
|
||||||
|
|
||||||
### 最新更新 (待发布)
|
### 2.1.1 (2026/04/23)
|
||||||
|
|
||||||
以下为修复内容:
|
以下为修复内容:
|
||||||
- **自动写卡系统 Diff 视图修复**:
|
- **自动写卡系统 Diff 视图修复**:
|
||||||
@@ -81,6 +81,18 @@
|
|||||||
- **Ngms API 强制参数**:在 `core/api/Ngms_api.js` 中,移除了旧版 UI 中的温度和最大 Token 设置,强制将默认温度设为 `1.0`,最大 Token 设为 `30000`,以确保总结任务的稳定性和完整性。
|
- **Ngms API 强制参数**:在 `core/api/Ngms_api.js` 中,移除了旧版 UI 中的温度和最大 Token 设置,强制将默认温度设为 `1.0`,最大 Token 设为 `30000`,以确保总结任务的稳定性和完整性。
|
||||||
- **总结失败自动重试**:在 `core/historiographer.js` 中为“微言录”和“宏史卷”的生成过程添加了自定义重试逻辑。用户可在 UI 中设置重试次数,当 AI 返回空内容时,系统会自动等待并重试,降低了因 API 波动导致的总结失败率。
|
- **总结失败自动重试**:在 `core/historiographer.js` 中为“微言录”和“宏史卷”的生成过程添加了自定义重试逻辑。用户可在 UI 中设置重试次数,当 AI 返回空内容时,系统会自动等待并重试,降低了因 API 波动导致的总结失败率。
|
||||||
- **时间跨度标识优化**:修改了 `utils/settings.js` 中的”微言录”和”宏史卷”提示词,强制要求 AI 在提取时间时加入相对时间跨度标识 `(Xd)`(如 `2023-09-15(2d)-星期五-15:00`),以解决长篇剧情中因缺乏具体日期导致的时间线混乱问题。
|
- **时间跨度标识优化**:修改了 `utils/settings.js` 中的”微言录”和”宏史卷”提示词,强制要求 AI 在提取时间时加入相对时间跨度标识 `(Xd)`(如 `2023-09-15(2d)-星期五-15:00`),以解决长篇剧情中因缺乏具体日期导致的时间线混乱问题。
|
||||||
|
- **翰林院设置回填中断修复(Rerank 等开关无法回显的根因)**:修复了 `ui/hanlinyuan-bindings.js` 的 `loadSettingsToUI` 在处理“标签提取”相关 DOM(`hly-tag-extraction-toggle` / `hly-tag-input` / `hly-tag-input-container`,已在 2.1.0 重构中删除)时对 `null` 赋值抛出 TypeError 的问题。由于该异常发生在 Rerank 设置回填之前,导致 Rerank 等开关虽已正确保存至 `extension_settings['hanlinyuan-rag-core']`,但刷新后 UI 不再回显,表现为“开关无法持久化”。清理相关 DOM 回填与 `bindInternalUIEvents` 中同名元素的事件绑定后,Rerank 等翰林院面板设置可正常持久化显示。
|
||||||
|
- **翰林院孤儿引用清理**:移除 `ui/hanlinyuan-bindings.js` → `updateAndSaveSetting` 中对已删除函数 `syncHanlinLinkedRuleProfile` 的四处调用,修复了修改浓缩/查询预处理的标签提取或标签字段时抛出 ReferenceError 的问题(2.1.0 重构遗留)。
|
||||||
|
- **超级记忆 RAG 设置路径修复**:修复了 `core/super-memory/bindings.js` 中 `getRagSettings` 使用错误路径 `extension_settings[extensionName]['hanlinyuan-rag-core']` 读写的问题。翰林院核心 (`core/rag-processor.js`) 使用的是顶层 `extension_settings['hanlinyuan-rag-core']`,改为一致路径后,归档开关 / 关联图谱开关 / 归档阈值等设置可正确持久化并与翰林院面板同步。
|
||||||
|
- **分步填表防抖延迟参数落地**:之前 `utils/settings.js` 与 `core/table-system/settings.js` 均声明了 `secondary_filler_delay` 默认值,但既没有 UI 入口也没有在代码中被读取。现已:
|
||||||
|
- 在「分步填表高级控制」面板新增「触发延迟 (毫秒)」数值输入(`assets/amily-data-table/Memorisation-forms.html`);
|
||||||
|
- 在 `ui/table-bindings.js` 中为该输入框补齐值回填与 `updateAndSaveTableSetting('secondary_filler_delay', ...)` 的 change 绑定;
|
||||||
|
- 在 `core/table-system/secondary-filler.js` 的 `fillWithSecondaryApi` 入口处实现真正的防抖:自动触发(`forceRun=false`)且延迟 > 0 时,会用模块级定时器调度本次调用,延迟期内再次到来的触发会重置计时器;`forceRun=true` 的手动触发及重新填表仍会立即执行,并清掉待触发的防抖任务。
|
||||||
|
- **填表响应检查窗(Amily2Edit 指令块缺失处理)**:
|
||||||
|
- 新增 `ui/page-window.js` → `showTableFillReviewModal`,参照总结模块 `showSummaryModal` 的交互模式,提供原始响应查看/编辑、继续补全、重新填表、手动应用、取消五种操作。
|
||||||
|
- **批量填表 / 楼层填表**:修改 `core/table-system/batch-filler.js` 的 `runBatchAttempt` 与 `startFloorRangeFilling`,当 AI 响应缺少 `<Amily2Edit>` 指令块时不再直接抛错进入自动重试,而是弹出检查窗让用户查看原始报文;批次模式下会先将按钮置为“继续填表”暂停状态,操作结束后自动恢复流程;网络/空响应等其它异常仍走原有的 `MAX_RETRIES` 自动重试。
|
||||||
|
- **分步填表**:修改 `core/table-system/secondary-filler.js` 的 `fillWithSecondaryApi`,在缺少指令块时弹出同款检查窗,并将原先分散的“写表 → 存 hash → saveChat”流程抽取为 `commitSecondaryFillResult` 公共函数,供正常路径与手动应用路径复用;顺带补齐该文件缺失的 `log` 导入。
|
||||||
|
- **继续补全实现**:新增 `requestContinuation` / `requestSecondaryContinuation` 工具函数,将用户当前编辑的文本作为 `assistant` 消息追加到原始请求之后,并附加专用的“接续”用户提示词再次调用表格模型,将返回文本拼接到原文末尾回填到检查窗文本框中。
|
||||||
|
|
||||||
### 2.1.0 (2026/04/18)
|
### 2.1.0 (2026/04/18)
|
||||||
|
|
||||||
|
|||||||
@@ -36,47 +36,12 @@
|
|||||||
<!-- API Settings Tab -->
|
<!-- API Settings Tab -->
|
||||||
<div id="sinan-api-settings-tab" class="sinan-tab-pane active">
|
<div id="sinan-api-settings-tab" class="sinan-tab-pane active">
|
||||||
<fieldset class="settings-group">
|
<fieldset class="settings-group">
|
||||||
<legend>Jqyh API</legend>
|
<legend>剧情优化 API</legend>
|
||||||
<div class="control-block-with-switch">
|
<p class="notes" style="margin: 0;">
|
||||||
<label for="amily2_jqyh_enabled"><strong>启用 Jqyh API</strong></label>
|
剧情优化所用的连接配置统一在
|
||||||
<label class="toggle-switch">
|
<strong>API 连接配置 → 功能分配 → 剧情优化 / JQYH</strong>
|
||||||
<input id="amily2_jqyh_enabled" type="checkbox" />
|
中指定,无需在此单独填写。
|
||||||
<span class="slider"></span>
|
</p>
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div id="amily2_jqyh_content" style="display: none;" class="inline-settings-grid">
|
|
||||||
<label for="amily2_jqyh_api_mode">API 模式</label>
|
|
||||||
<select id="amily2_jqyh_api_mode" class="text_pole">
|
|
||||||
<option value="openai_test">全兼容模式</option>
|
|
||||||
<option value="sillytavern_preset">SillyTavern 预设</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<div id="amily2_jqyh_compatible_config" class="inline-settings-grid" style="grid-column: 1 / -1;">
|
|
||||||
<label for="amily2_jqyh_api_url">API URL</label>
|
|
||||||
<input type="text" id="amily2_jqyh_api_url" class="text_pole" placeholder="例如: https://api.openai.com/v1">
|
|
||||||
<label for="amily2_jqyh_api_key">API Key</label>
|
|
||||||
<input type="password" id="amily2_jqyh_api_key" class="text_pole" placeholder="请输入您的 API Key">
|
|
||||||
<label for="amily2_jqyh_model">模型</label>
|
|
||||||
<div class="amily2_opt_preset_selector_wrapper">
|
|
||||||
<input type="text" id="amily2_jqyh_model" class="text_pole" placeholder="请先获取模型列表或手动输入">
|
|
||||||
<select id="amily2_jqyh_model_select" class="text_pole" style="display: none;"></select>
|
|
||||||
</div>
|
|
||||||
<div class="jqyh-button-row" style="grid-column: 1 / -1;">
|
|
||||||
<button id="amily2_jqyh_fetch_models" class="menu_button secondary" title="获取模型列表"><i class="fas fa-sync-alt"></i> 获取模型</button>
|
|
||||||
<button id="amily2_jqyh_test_connection" class="menu_button primary"><i class="fas fa-plug"></i> 测试连接</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="amily2_jqyh_preset_config" class="inline-settings-grid" style="display: none; grid-column: 1 / -1;">
|
|
||||||
<label for="amily2_jqyh_tavern_profile">选择酒馆预设</label>
|
|
||||||
<select id="amily2_jqyh_tavern_profile" class="text_pole"></select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label for="amily2_jqyh_max_tokens">最大 Tokens: <span id="amily2_jqyh_max_tokens_value">4000</span></label>
|
|
||||||
<input type="number" class="text_pole" id="amily2_jqyh_max_tokens" min="100" max="100000" value="4000">
|
|
||||||
<label for="amily2_jqyh_temperature">温度: <span id="amily2_jqyh_temperature_value">0.7</span></label>
|
|
||||||
<input type="number" class="text_pole" id="amily2_jqyh_temperature" min="0" max="2" value="0.7">
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset class="settings-group">
|
<fieldset class="settings-group">
|
||||||
|
|||||||
@@ -250,6 +250,28 @@
|
|||||||
<input type="number" id="secondary-filler-max-retries" min="0" max="10" step="1" value="2" class="text_pole" style="width: 80px; margin-top: 5px;">
|
<input type="number" id="secondary-filler-max-retries" min="0" max="10" step="1" value="2" 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 for="secondary-filler-delay">触发延迟 (毫秒)</label>
|
||||||
|
<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>
|
||||||
|
</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;">
|
||||||
@@ -289,6 +311,21 @@
|
|||||||
|
|
||||||
<hr class="section-divider" style="margin: 10px 0;">
|
<hr class="section-divider" style="margin: 10px 0;">
|
||||||
|
|
||||||
|
<!-- Function Call 填表 -->
|
||||||
|
<div class="control-block-with-switch" style="margin-bottom: 6px;">
|
||||||
|
<label for="table-fill-function-call-enabled" title="使用 OpenAI Function Call(工具调用)进行填表,模型直接返回结构化操作列表,无需解析 <Amily2Edit> 指令块。仅支持 openai 直连模式。">使用 Function Call 填表</label>
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" id="table-fill-function-call-enabled">
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p class="notes" style="margin-bottom: 6px;">仅支持 openai 直连接口(tableFilling 槽位)。启用后跳过 <Amily2Edit> 文本解析,由模型直接返回操作列表。</p>
|
||||||
|
<div style="background: rgba(255, 160, 0, 0.12); border-left: 3px solid #ffa000; border-radius: 3px; padding: 6px 10px; margin-bottom: 10px; font-size: 0.85em; color: #ffcc80;">
|
||||||
|
⚠️ 部分公益站因禁止用于跑代码会屏蔽 tools 参数,请确认公益站是否支持 tools 调用,避免被意外封禁。
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="section-divider" style="margin: 10px 0;">
|
||||||
|
|
||||||
<!-- Nccs API 控制区域 -->
|
<!-- Nccs API 控制区域 -->
|
||||||
<fieldset class="settings-group" style="border-style: dashed; padding: 8px; margin-bottom: 10px;">
|
<fieldset class="settings-group" style="border-style: dashed; padding: 8px; margin-bottom: 10px;">
|
||||||
<legend><i class="fas fa-brain"></i> Nccs API 系统</legend>
|
<legend><i class="fas fa-brain"></i> Nccs API 系统</legend>
|
||||||
|
|||||||
@@ -212,14 +212,14 @@
|
|||||||
<button id="amily2_open_additional_features" class="menu_button wide_button"><i class="fas fa-landmark-dome"></i> 总结模块</button>
|
<button id="amily2_open_additional_features" class="menu_button wide_button"><i class="fas fa-landmark-dome"></i> 总结模块</button>
|
||||||
<button id="amily2_open_rag_palace" class="menu_button wide_button"><i class="fas fa-brain"></i> 向量模块</button>
|
<button id="amily2_open_rag_palace" class="menu_button wide_button"><i class="fas fa-brain"></i> 向量模块</button>
|
||||||
<button id="amily2_open_memorisation_forms" class="menu_button wide_button"><i class="fas fa-table"></i> 表格模块</button>
|
<button id="amily2_open_memorisation_forms" class="menu_button wide_button"><i class="fas fa-table"></i> 表格模块</button>
|
||||||
<button id="amily2_open_character_world_book" class="menu_button wide_button"><i class="fa-solid fa-book-atlas"></i> 角色世界</button>
|
<button id="amily2_open_plot_optimization" class="menu_button wide_button"><i class="fas fa-feather-alt"></i> 记忆管理</button>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset class="settings-group">
|
<fieldset class="settings-group">
|
||||||
<legend><i class="fas fa-puzzle-piece"></i> 附加功能</legend>
|
<legend><i class="fas fa-puzzle-piece"></i> 附加功能</legend>
|
||||||
<div class="button-group" style="display: flex; justify-content: space-between; gap: 8px;">
|
<div class="button-group" style="display: flex; justify-content: space-between; gap: 8px;">
|
||||||
<button id="amily2_open_plot_optimization" class="menu_button wide_button"><i class="fas fa-feather-alt"></i> 记忆管理</button>
|
<button id="amily2_open_character_world_book" class="menu_button wide_button"><i class="fa-solid fa-book-atlas"></i> 角色世界</button>
|
||||||
<button id="amily2_open_text_optimization" class="menu_button wide_button"><i class="fas fa-cogs"></i> 正文优化</button>
|
<button id="amily2_open_text_optimization" class="menu_button wide_button"><i class="fas fa-cogs"></i> 正文优化</button>
|
||||||
<button id="amily2_open_world_editor" class="menu_button wide_button"><i class="fas fa-globe"></i> 世界编辑</button>
|
<button id="amily2_open_world_editor" class="menu_button wide_button"><i class="fas fa-globe"></i> 世界编辑</button>
|
||||||
<button id="amily2_open_glossary" class="menu_button wide_button"><i class="fas fa-book"></i> 术语表单</button>
|
<button id="amily2_open_glossary" class="menu_button wide_button"><i class="fas fa-book"></i> 术语表单</button>
|
||||||
@@ -227,6 +227,7 @@
|
|||||||
<div class="button-group" style="display: flex; justify-content: space-between; gap: 8px; margin-top: 8px;">
|
<div class="button-group" style="display: flex; justify-content: space-between; gap: 8px; margin-top: 8px;">
|
||||||
<button id="amily2_open_renderer" class="menu_button wide_button"><i class="fas fa-paint-brush"></i> 前端渲染</button>
|
<button id="amily2_open_renderer" class="menu_button wide_button"><i class="fas fa-paint-brush"></i> 前端渲染</button>
|
||||||
<button id="amily2_open_sfigen" class="menu_button wide_button"><i class="fas fa-image"></i> 硅基生图</button>
|
<button id="amily2_open_sfigen" class="menu_button wide_button"><i class="fas fa-image"></i> 硅基生图</button>
|
||||||
|
<button id="amily2_open_preset_editor" class="menu_button wide_button"><i class="fa-solid fa-scroll"></i> 提示词链</button>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,10 @@
|
|||||||
<div class="amily2_settings_block" style="margin-top:10px;">
|
<div class="amily2_settings_block" style="margin-top:10px;">
|
||||||
<label><input id="amily2_rule_profile_tag_toggle" type="checkbox"> 启用标签提取</label>
|
<label><input id="amily2_rule_profile_tag_toggle" type="checkbox"> 启用标签提取</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="amily2_settings_block" style="margin-top:10px;">
|
||||||
|
<label><input id="amily2_rule_profile_exclude_user" type="checkbox"> 自动排除用户楼层</label>
|
||||||
|
<small class="notes" style="display:block; margin-top:4px;">勾选后,使用此规则时将自动跳过用户发送的消息楼层,不纳入总结/提取内容。</small>
|
||||||
|
</div>
|
||||||
<div id="amily2_rule_profile_tags_wrap" class="amily2_settings_block" style="display:none; margin-top:10px;">
|
<div id="amily2_rule_profile_tags_wrap" class="amily2_settings_block" style="display:none; margin-top:10px;">
|
||||||
<label for="amily2_rule_profile_tags">标签列表</label>
|
<label for="amily2_rule_profile_tags">标签列表</label>
|
||||||
<textarea id="amily2_rule_profile_tags" class="text_pole" rows="3" placeholder="例如:content,details,summary"></textarea>
|
<textarea id="amily2_rule_profile_tags" class="text_pole" rows="3" placeholder="例如:content,details,summary"></textarea>
|
||||||
|
|||||||
182
core/api.js
182
core/api.js
@@ -485,8 +485,7 @@ export async function getApiSettings(slot = 'main') {
|
|||||||
apiProvider: apiMode,
|
apiProvider: apiMode,
|
||||||
apiUrl: settings.plotOpt_apiUrl?.trim() || '',
|
apiUrl: settings.plotOpt_apiUrl?.trim() || '',
|
||||||
apiKey: configManager.get('plotOpt_apiKey') || '',
|
apiKey: configManager.get('plotOpt_apiKey') || '',
|
||||||
model: document.getElementById('amily2_opt_model')?.value?.trim()
|
model: settings.plotOpt_model || '',
|
||||||
|| settings.plotOpt_model || '',
|
|
||||||
maxTokens: settings.plotOpt_max_tokens ?? 65500,
|
maxTokens: settings.plotOpt_max_tokens ?? 65500,
|
||||||
temperature: settings.plotOpt_temperature ?? 1.0,
|
temperature: settings.plotOpt_temperature ?? 1.0,
|
||||||
tavernProfile: '',
|
tavernProfile: '',
|
||||||
@@ -589,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 ?? {}) },
|
||||||
@@ -649,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')) {
|
||||||
@@ -691,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) {
|
||||||
@@ -733,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) {
|
||||||
@@ -778,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) {
|
||||||
@@ -823,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
|
||||||
@@ -839,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调用失败');
|
||||||
@@ -851,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预设调用');
|
||||||
|
|
||||||
@@ -910,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响应');
|
||||||
@@ -949,3 +984,126 @@ export async function checkAndFixWithAPI(latestMessage, previousMessages) {
|
|||||||
const { processOptimization } = await import('./summarizer.js');
|
const { processOptimization } = await import('./summarizer.js');
|
||||||
return await processOptimization(latestMessage, previousMessages);
|
return await processOptimization(latestMessage, previousMessages);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 OpenAI Function Call 调用 AI,返回 tool_calls[0].function.arguments 字符串。
|
||||||
|
* 仅支持 openai / openai_test 接口(Google / ST preset / backend 不在标准 tool_calls 格式下工作)。
|
||||||
|
*
|
||||||
|
* @param {Array} messages
|
||||||
|
* @param {Object} tool - OpenAI tools 定义对象(单个,含 type/function 字段)
|
||||||
|
* @param {Object} options - 同 callAI 的 options,支持 slot / customParams 等
|
||||||
|
* @returns {Promise<string|null>} arguments JSON 字符串,失败返回 null
|
||||||
|
*/
|
||||||
|
export async function callAIForTools(messages, tool, options = {}) {
|
||||||
|
const apiSettings = await getApiSettings(options.slot || 'main');
|
||||||
|
|
||||||
|
const finalOptions = {
|
||||||
|
maxTokens: apiSettings.maxTokens,
|
||||||
|
temperature: apiSettings.temperature,
|
||||||
|
model: apiSettings.model,
|
||||||
|
apiUrl: apiSettings.apiUrl,
|
||||||
|
apiKey: apiSettings.apiKey,
|
||||||
|
apiProvider: apiSettings.apiProvider,
|
||||||
|
customParams: { ...(apiSettings.customParams ?? {}), ...(options.customParams ?? {}) },
|
||||||
|
signal: options.signal,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
|
||||||
|
const FC_SUPPORTED_PROVIDERS = new Set(['openai', 'openai_test', 'custom_oai', 'openrouter', 'deepseek', 'xai']);
|
||||||
|
if (!FC_SUPPORTED_PROVIDERS.has(finalOptions.apiProvider)) {
|
||||||
|
console.warn(`[Amily2-外交部] Function Call 不支持当前接口类型: ${finalOptions.apiProvider}`);
|
||||||
|
toastr.warning(`当前 API 接口类型(${finalOptions.apiProvider})不支持 Function Call。`, 'Function Call');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!finalOptions.apiUrl || !finalOptions.model) {
|
||||||
|
console.warn('[Amily2-外交部] API URL 或模型未配置,无法调用 Function Call AI');
|
||||||
|
toastr.error('API URL 或模型未配置。', 'Amily2-外交部');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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',
|
||||||
|
reverse_proxy: finalOptions.apiUrl,
|
||||||
|
proxy_password: finalOptions.apiKey,
|
||||||
|
model: finalOptions.model,
|
||||||
|
messages: overrideMessages ?? messages,
|
||||||
|
max_tokens: finalOptions.maxTokens || 30000,
|
||||||
|
temperature: finalOptions.temperature ?? 1,
|
||||||
|
stream: false,
|
||||||
|
...(finalOptions.customParams || {}),
|
||||||
|
...extraParams,
|
||||||
|
tools: [tool],
|
||||||
|
...(withToolChoice ? { tool_choice: { type: 'function', function: { name: tool.function.name } } } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const doFCRequest = async (withToolChoice, overrideMessages, extraParams) => {
|
||||||
|
const response = await fetch('/api/backends/chat-completions/generate', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { ...getRequestHeaders(), 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(buildFCBody(withToolChoice, overrideMessages, extraParams)),
|
||||||
|
signal: finalOptions.signal,
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(`Function Call 请求失败: ${response.status} - ${errorText}`);
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
// ST 代理在上游报错时仍返回 HTTP 200,错误信息在 body 里
|
||||||
|
if (data?.error) {
|
||||||
|
throw new Error(`Function Call 请求失败: ${JSON.stringify(data.error)}`);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.groupCollapsed(`[Amily2号-Function Call] ${new Date().toLocaleTimeString()}`);
|
||||||
|
console.log('【工具】:', tool.function?.name, '【模型】:', finalOptions.model);
|
||||||
|
console.log('【消息】:', messages);
|
||||||
|
console.groupEnd();
|
||||||
|
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
// 走 ST 后端代理,避免浏览器 CSP 拦截直连外部 URL
|
||||||
|
// 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) {
|
||||||
|
if (firstError?.name === 'AbortError') throw firstError; // 用户中断,不要重试
|
||||||
|
// 首次失败(含 ST 代理吞掉错误码场景)无条件去掉 tool_choice 重试一次
|
||||||
|
// 思考模式模型支持 tools 但不支持强制 tool_choice,追加强制指令防止模型直接输出文本
|
||||||
|
console.warn('[Amily2-外交部] 首次 FC 请求失败,去掉 tool_choice 重试…', firstError.message);
|
||||||
|
const retryMessages = [
|
||||||
|
...messages,
|
||||||
|
{ role: 'user', content: `你必须通过调用 \`${tool.function.name}\` 函数来返回结果,禁止直接输出文本内容。` },
|
||||||
|
];
|
||||||
|
data = await doFCRequest(false, retryMessages);
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolCalls = data?.choices?.[0]?.message?.tool_calls;
|
||||||
|
if (!Array.isArray(toolCalls) || toolCalls.length === 0) {
|
||||||
|
console.warn('[Amily2-外交部] Function Call 响应中无 tool_calls,finish_reason:', data?.choices?.[0]?.finish_reason);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const argsString = toolCalls[0]?.function?.arguments;
|
||||||
|
console.groupCollapsed('[Amily2号-Function Call 响应]');
|
||||||
|
console.log(argsString);
|
||||||
|
console.groupEnd();
|
||||||
|
return argsString ?? null;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (error?.name === 'AbortError') {
|
||||||
|
console.warn('[Amily2-外交部] Function Call 调用被用户中断。');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
console.error('[Amily2-外交部] Function Call 调用失败:', error);
|
||||||
|
toastr.error(`Function Call 调用失败: ${error.message}`, 'Amily2-外交部');
|
||||||
|
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 {
|
||||||
|
|||||||
@@ -10,8 +10,16 @@ export function initializeArchiveManager() {
|
|||||||
console.log('[归档管理器] 已启动,正在监控表格状态...');
|
console.log('[归档管理器] 已启动,正在监控表格状态...');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Bus 直调路径:由 super-memory/manager.js 的 pushUpdate 调用,接受纯 payload 对象。 */
|
||||||
|
export function handleArchiveUpdate(payload) {
|
||||||
|
return handleArchivePayload(payload);
|
||||||
|
}
|
||||||
|
|
||||||
async function handleTableUpdate(event) {
|
async function handleTableUpdate(event) {
|
||||||
const { tableName, data, role } = event.detail;
|
return handleArchivePayload(event.detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleArchivePayload({ tableName, data, role }) {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
if (!settings.archive || !settings.archive.enabled) return;
|
if (!settings.archive || !settings.archive.enabled) return;
|
||||||
@@ -24,6 +32,7 @@ async function handleTableUpdate(event) {
|
|||||||
if (isArchiving) return;
|
if (isArchiving) return;
|
||||||
|
|
||||||
let hasNotice = false;
|
let hasNotice = false;
|
||||||
|
let realRows = data;
|
||||||
|
|
||||||
if (data.length > 0 && data[0][2] && data[0][2].includes('已自动归档')) {
|
if (data.length > 0 && data[0][2] && data[0][2].includes('已自动归档')) {
|
||||||
hasNotice = true;
|
hasNotice = true;
|
||||||
|
|||||||
@@ -307,8 +307,11 @@ function getRawMessagesForSummary(startFloor, endFloor) {
|
|||||||
const useTagExtraction = historiographyRuleConfig.tagExtractionEnabled ?? false;
|
const useTagExtraction = historiographyRuleConfig.tagExtractionEnabled ?? false;
|
||||||
const tagsToExtract = useTagExtraction ? (historiographyRuleConfig.tags || '').split(',').map(t => t.trim()).filter(Boolean) : [];
|
const tagsToExtract = useTagExtraction ? (historiographyRuleConfig.tags || '').split(',').map(t => t.trim()).filter(Boolean) : [];
|
||||||
const exclusionRules = historiographyRuleConfig.exclusionRules || [];
|
const exclusionRules = historiographyRuleConfig.exclusionRules || [];
|
||||||
|
const excludeUserMessages = historiographyRuleConfig.excludeUserMessages ?? false;
|
||||||
|
|
||||||
const messages = historySlice.map((msg, index) => {
|
const messages = historySlice.map((msg, index) => {
|
||||||
|
if (excludeUserMessages && msg.is_user) return null;
|
||||||
|
|
||||||
let content = msg.mes;
|
let content = msg.mes;
|
||||||
|
|
||||||
if (useTagExtraction && tagsToExtract.length > 0) {
|
if (useTagExtraction && tagsToExtract.length > 0) {
|
||||||
|
|||||||
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) {
|
||||||
|
|||||||
@@ -342,6 +342,25 @@ function getSettings() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 旧版设置 rerank.priorityRetrieval 可能只有 enabled 字段而缺少 sources,补全
|
||||||
|
if (s.rerank?.priorityRetrieval && !s.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',
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ 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 { callJqyhAI } from './api/JqyhApi.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) {
|
||||||
@@ -277,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);
|
||||||
@@ -433,9 +429,11 @@ export async function processPlotOptimization(currentUserMessage, contextMessage
|
|||||||
const useTagExtraction = historiographyRuleConfig.tagExtractionEnabled ?? false;
|
const useTagExtraction = historiographyRuleConfig.tagExtractionEnabled ?? false;
|
||||||
const tagsToExtract = useTagExtraction ? (historiographyRuleConfig.tags || '').split(',').map(t => t.trim()).filter(Boolean) : [];
|
const tagsToExtract = useTagExtraction ? (historiographyRuleConfig.tags || '').split(',').map(t => t.trim()).filter(Boolean) : [];
|
||||||
const exclusionRules = historiographyRuleConfig.exclusionRules || [];
|
const exclusionRules = historiographyRuleConfig.exclusionRules || [];
|
||||||
|
const excludeUserMessages = historiographyRuleConfig.excludeUserMessages ?? false;
|
||||||
|
|
||||||
history = historyMessages
|
history = historyMessages
|
||||||
.map(msg => {
|
.map(msg => {
|
||||||
|
if (excludeUserMessages && msg.is_user) return null;
|
||||||
if (msg.mes && msg.mes.trim()) {
|
if (msg.mes && msg.mes.trim()) {
|
||||||
let content = msg.mes.trim();
|
let content = msg.mes.trim();
|
||||||
|
|
||||||
@@ -478,7 +476,7 @@ export async function processPlotOptimization(currentUserMessage, contextMessage
|
|||||||
onProgress(getRandomText(['正在检索辅助记忆 (LLM-B)...', '正在扫描平行世界线 (LLM-B)...']), false);
|
onProgress(getRandomText(['正在检索辅助记忆 (LLM-B)...', '正在扫描平行世界线 (LLM-B)...']), false);
|
||||||
|
|
||||||
onProgress(getRandomText(['正在与核心意识同步 (LLM-A)...', '正在等待灵魂共鸣 (LLM-A)...']), false);
|
onProgress(getRandomText(['正在与核心意识同步 (LLM-A)...', '正在等待灵魂共鸣 (LLM-A)...']), false);
|
||||||
const promise1 = (settings.jqyhEnabled ? callJqyhAI(mainMessages) : callAI(mainMessages, { slot: 'plotOpt' })).then(res => {
|
const promise1 = callAI(mainMessages, { slot: 'plotOpt' }).then(res => {
|
||||||
onProgress(getRandomText(['正在与核心意识同步 (LLM-A)...', '正在等待灵魂共鸣 (LLM-A)...']), true);
|
onProgress(getRandomText(['正在与核心意识同步 (LLM-A)...', '正在等待灵魂共鸣 (LLM-A)...']), true);
|
||||||
return res;
|
return res;
|
||||||
});
|
});
|
||||||
@@ -552,7 +550,7 @@ export async function processPlotOptimization(currentUserMessage, contextMessage
|
|||||||
attempt++;
|
attempt++;
|
||||||
console.log(`[${extensionName}] 剧情优化第 ${attempt} 次尝试...`);
|
console.log(`[${extensionName}] 剧情优化第 ${attempt} 次尝试...`);
|
||||||
|
|
||||||
const rawResponse = settings.jqyhEnabled ? await callJqyhAI(mainMessages) : await callAI(mainMessages, { slot: 'plotOpt' });
|
const rawResponse = await callAI(mainMessages, { slot: 'plotOpt' });
|
||||||
|
|
||||||
if (cancellationState.isCancelled) {
|
if (cancellationState.isCancelled) {
|
||||||
console.log(`[${extensionName}] 优化任务在API调用后被中止。`);
|
console.log(`[${extensionName}] 优化任务在API调用后被中止。`);
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ const RAG_MODULE_NAME = 'hanlinyuan-rag-core';
|
|||||||
|
|
||||||
function getRagSettings() {
|
function getRagSettings() {
|
||||||
if (!extension_settings[extensionName]) extension_settings[extensionName] = {};
|
if (!extension_settings[extensionName]) extension_settings[extensionName] = {};
|
||||||
if (!extension_settings[extensionName][RAG_MODULE_NAME]) {
|
const root = extension_settings[extensionName];
|
||||||
extension_settings[extensionName][RAG_MODULE_NAME] = structuredClone(ragDefaultSettings);
|
if (!root[RAG_MODULE_NAME]) {
|
||||||
|
root[RAG_MODULE_NAME] = structuredClone(ragDefaultSettings);
|
||||||
}
|
}
|
||||||
return extension_settings[extensionName][RAG_MODULE_NAME];
|
return root[RAG_MODULE_NAME];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function bindSuperMemoryEvents() {
|
export function bindSuperMemoryEvents() {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { syncToLorebook, ensureMemoryBook, updateTransientHint, getMemoryBookNam
|
|||||||
import { getMemoryState, loadMemoryState, saveMemoryState } from "../table-system/manager.js";
|
import { getMemoryState, loadMemoryState, saveMemoryState } from "../table-system/manager.js";
|
||||||
import { TABLE_UPDATED_EVENT } from "../table-system/events-schema.js";
|
import { TABLE_UPDATED_EVENT } from "../table-system/events-schema.js";
|
||||||
import { eventSource, event_types } from "/script.js";
|
import { eventSource, event_types } from "/script.js";
|
||||||
|
import { handleArchiveUpdate } from "../archive-manager.js";
|
||||||
|
|
||||||
/* ── [AMILY2-MODIFIED] ── pipeline integration: awaitSync() export ── */
|
/* ── [AMILY2-MODIFIED] ── pipeline integration: awaitSync() export ── */
|
||||||
let isInitialized = false;
|
let isInitialized = false;
|
||||||
@@ -110,10 +111,15 @@ export function pushUpdate(payload) {
|
|||||||
|
|
||||||
updateQueue.push({ tableName, data, role, headers, rowStatuses });
|
updateQueue.push({ tableName, data, role, headers, rowStatuses });
|
||||||
_syncPromise = processQueue();
|
_syncPromise = processQueue();
|
||||||
|
|
||||||
|
// Bus 路径下 document event 不再分发,需直接通知归档管理器
|
||||||
|
handleArchiveUpdate(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** CustomEvent 降级路径(Bus 未就绪时的兜底监听器) */
|
/** CustomEvent 降级路径(Bus 未就绪时的兜底监听器) */
|
||||||
function handleTableUpdate(event) {
|
function handleTableUpdate(event) {
|
||||||
|
// Bus 已就绪时 pushUpdate 已由 dispatchTableUpdate 直调,跳过避免重复处理
|
||||||
|
if (window.Amily2Bus?.query('SuperMemory')?.pushUpdate) return;
|
||||||
pushUpdate(event.detail);
|
pushUpdate(event.detail);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,29 @@ import { updateTableFromText } from './manager.js';
|
|||||||
import { extensionName } from '../../utils/settings.js';
|
import { extensionName } from '../../utils/settings.js';
|
||||||
import { renderTables } from '../../ui/table-bindings.js';
|
import { renderTables } from '../../ui/table-bindings.js';
|
||||||
import { getPresetPrompts, getMixedOrder } from '../../PresetSettings/index.js';
|
import { getPresetPrompts, getMixedOrder } from '../../PresetSettings/index.js';
|
||||||
import { callAI, generateRandomSeed } from '../api.js';
|
import { callAI, callAIForTools, generateRandomSeed } from '../api.js';
|
||||||
import { callNccsAI } from '../api/NccsApi.js';
|
import { callNccsAI } from '../api/NccsApi.js';
|
||||||
|
import { TABLE_FILL_TOOL, parseToolCallArgs } from './formatters/tool-call.js';
|
||||||
|
import { updateTableFromOps } from './manager.js';
|
||||||
import { extractBlocksByTags, applyExclusionRules } from '../utils/rag-tag-extractor.js';
|
import { extractBlocksByTags, applyExclusionRules } from '../utils/rag-tag-extractor.js';
|
||||||
import { resolveTableRuleConfig } from '../../utils/config/RuleProfileManager.js';
|
import { resolveTableRuleConfig } from '../../utils/config/RuleProfileManager.js';
|
||||||
|
import { showTableFillReviewModal } from '../../ui/page-window.js';
|
||||||
|
|
||||||
import { getBatchFillerRuleTemplate, getBatchFillerFlowTemplate, convertTablesToCsvString } from './manager.js';
|
import { getBatchFillerRuleTemplate, getBatchFillerFlowTemplate, convertTablesToCsvString } from './manager.js';
|
||||||
|
|
||||||
|
const CONTINUE_PROMPT = '上一条回复不完整或缺少 <Amily2Edit> 指令块。请直接从中断处继续生成剩余内容,不要重复已输出的文本,也不要添加任何解释或寒暄,确保最终输出中包含完整的 <Amily2Edit>...</Amily2Edit> 指令块。';
|
||||||
|
|
||||||
|
async function requestContinuation(baseMessages, partialResponse) {
|
||||||
|
const continueMessages = [
|
||||||
|
...baseMessages,
|
||||||
|
{ role: 'assistant', content: partialResponse || '' },
|
||||||
|
{ role: 'user', content: CONTINUE_PROMPT },
|
||||||
|
];
|
||||||
|
const continued = await callTableModel(continueMessages);
|
||||||
|
if (!continued) return null;
|
||||||
|
return `${partialResponse || ''}${continued}`;
|
||||||
|
}
|
||||||
|
|
||||||
let isFilling = false;
|
let isFilling = false;
|
||||||
let manualStopRequested = false;
|
let manualStopRequested = false;
|
||||||
let currentBatch = 0;
|
let currentBatch = 0;
|
||||||
@@ -268,21 +284,87 @@ async function runBatchAttempt(batchNum, attemptNum) {
|
|||||||
console.dir(messages);
|
console.dir(messages);
|
||||||
console.groupEnd();
|
console.groupEnd();
|
||||||
|
|
||||||
|
const batchSettings = extension_settings[extensionName] || {};
|
||||||
|
if (batchSettings.tableFillFunctionCall) {
|
||||||
|
// Function Call 路径:结构化输出,无需检查 <Amily2Edit>
|
||||||
|
const argsString = await callAIForTools(messages, TABLE_FILL_TOOL, { slot: 'tableFilling' });
|
||||||
|
if (!argsString) throw new Error('Function Call 返回为空。');
|
||||||
|
const ops = parseToolCallArgs(argsString);
|
||||||
|
if (ops.length === 0) {
|
||||||
|
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}`);
|
||||||
|
} else {
|
||||||
|
await updateTableFromOps(ops, { immediateDelete: true });
|
||||||
|
renderTables();
|
||||||
|
log(`批次 ${batchNum} Function Call 处理成功(${ops.length} 条操作)。`, 'success');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Legacy 文本路径
|
||||||
const resultText = await callTableModel(messages);
|
const resultText = await callTableModel(messages);
|
||||||
console.log(`[Amily2 立即远征] 批次 ${batchNum}/${totalBatches} - 收到 API 原始回复:`, resultText);
|
console.log(`[Amily2 立即远征] 批次 ${batchNum}/${totalBatches} - 收到 API 原始回复:`, resultText);
|
||||||
if (!resultText) {
|
if (!resultText) throw new Error('API返回内容为空。');
|
||||||
throw new Error('API返回内容为空。');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 【修复】检查 AI 是否返回了有效的指令块,防止 AI 偷懒或格式错误被误判为成功
|
|
||||||
if (!resultText.includes('<Amily2Edit>')) {
|
if (!resultText.includes('<Amily2Edit>')) {
|
||||||
throw new Error('AI未返回有效的 <Amily2Edit> 指令块,可能格式错误或未产生实质性变更。');
|
log(`批次 ${batchNum} 的响应未包含 <Amily2Edit> 指令块,弹出检查窗口等待用户处理。`, 'warn');
|
||||||
|
updateButtonState('paused');
|
||||||
|
showTableFillReviewModal(resultText, {
|
||||||
|
title: `填表响应检查 - 批次 ${batchNum}/${totalBatches}`,
|
||||||
|
subtitle: `批次 ${batchNum}/${totalBatches}(楼层 ${startFloor}-${endFloor})的 AI 响应未包含有效的 <Amily2Edit> 指令块。请检查原始响应并选择处理方式。`,
|
||||||
|
onContinue: async (currentText) => {
|
||||||
|
const merged = await requestContinuation(messages, currentText);
|
||||||
|
if (!merged) { toastr.error('补全请求失败或返回为空。', '继续补全'); return null; }
|
||||||
|
if (!merged.includes('<Amily2Edit>')) {
|
||||||
|
toastr.warning('补全后仍未包含 <Amily2Edit> 指令块,可继续补全、手动应用或重新填表。', '继续补全');
|
||||||
|
} else {
|
||||||
|
toastr.success('已获得包含指令块的补全内容,可点击”手动应用”写入。', '继续补全');
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
},
|
||||||
|
onApply: (editedText) => {
|
||||||
|
if (!editedText || !editedText.includes('<Amily2Edit>')) {
|
||||||
|
toastr.warning('应用的文本中未检测到 <Amily2Edit> 指令块,已按原文尝试写入。', '手动应用');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
updateTableFromText(editedText, { immediateDelete: true });
|
||||||
|
renderTables();
|
||||||
|
log(`批次 ${batchNum} 已由用户手动处理完成。`, 'success');
|
||||||
|
} catch (err) {
|
||||||
|
log(`批次 ${batchNum} 手动应用失败: ${err.message}`, 'error');
|
||||||
|
toastr.error(`手动应用失败: ${err.message}`, '写入异常');
|
||||||
|
currentBatch = batchNum - 1;
|
||||||
|
updateButtonState('error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentBatch = batchNum;
|
||||||
|
setTimeout(processNextBatch, 500);
|
||||||
|
},
|
||||||
|
onRetry: () => {
|
||||||
|
log(`用户选择重新填表,批次 ${batchNum} 将重新执行。`, 'warn');
|
||||||
|
setTimeout(() => runBatchAttempt(batchNum, 0), 300);
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
log(`用户取消了批次 ${batchNum} 的处理,任务已暂停。`, 'warn');
|
||||||
|
currentBatch = batchNum - 1;
|
||||||
|
updateButtonState('error');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 【V155.0】批量填表时,启用立即删除模式,避免红色待删除行残留
|
|
||||||
updateTableFromText(resultText, { immediateDelete: true });
|
updateTableFromText(resultText, { immediateDelete: true });
|
||||||
renderTables();
|
renderTables();
|
||||||
log(`批次 ${batchNum} 处理成功。`, 'success');
|
log(`批次 ${batchNum} 处理成功。`, 'success');
|
||||||
|
}
|
||||||
|
|
||||||
currentBatch = batchNum;
|
currentBatch = batchNum;
|
||||||
setTimeout(processNextBatch, 1000);
|
setTimeout(processNextBatch, 1000);
|
||||||
@@ -345,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();
|
||||||
@@ -484,23 +568,81 @@ export async function startFloorRangeFilling(startFloor, endFloor) {
|
|||||||
console.dir(messages);
|
console.dir(messages);
|
||||||
console.groupEnd();
|
console.groupEnd();
|
||||||
|
|
||||||
|
const floorSettings = extension_settings[extensionName] || {};
|
||||||
|
if (floorSettings.tableFillFunctionCall) {
|
||||||
|
const argsString = await callAIForTools(messages, TABLE_FILL_TOOL, { slot: 'tableFilling' });
|
||||||
|
if (!argsString) throw new Error('Function Call 返回为空。');
|
||||||
|
const ops = parseToolCallArgs(argsString);
|
||||||
|
if (ops.length === 0) {
|
||||||
|
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}`);
|
||||||
|
} else {
|
||||||
|
await updateTableFromOps(ops, { immediateDelete: true });
|
||||||
|
renderTables();
|
||||||
|
toastr.success(`楼层 ${startFloor}-${endFloor} 填表完成!`);
|
||||||
|
log(`楼层 ${startFloor}-${endFloor} Function Call 处理成功(${ops.length} 条操作)。`, 'success');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
const resultText = await callTableModel(messages);
|
const resultText = await callTableModel(messages);
|
||||||
console.log(`[Amily2 楼层填表] 楼层 ${startFloor}-${endFloor} - 收到 API 原始回复:`, resultText);
|
console.log(`[Amily2 楼层填表] 楼层 ${startFloor}-${endFloor} - 收到 API 原始回复:`, resultText);
|
||||||
|
if (!resultText) throw new Error('API返回内容为空。');
|
||||||
|
|
||||||
if (!resultText) {
|
|
||||||
throw new Error('API返回内容为空。');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 【修复】检查 AI 是否返回了有效的指令块
|
|
||||||
if (!resultText.includes('<Amily2Edit>')) {
|
if (!resultText.includes('<Amily2Edit>')) {
|
||||||
throw new Error('AI未返回有效的 <Amily2Edit> 指令块,可能格式错误或未产生实质性变更。');
|
log(`楼层 ${startFloor}-${endFloor} 的响应未包含 <Amily2Edit> 指令块,弹出检查窗口等待用户处理。`, 'warn');
|
||||||
|
showTableFillReviewModal(resultText, {
|
||||||
|
title: `填表响应检查 - 楼层 ${startFloor}-${endFloor}`,
|
||||||
|
subtitle: `楼层 ${startFloor}-${endFloor} 的 AI 响应未包含有效的 <Amily2Edit> 指令块。请检查原始响应并选择处理方式。`,
|
||||||
|
onContinue: async (currentText) => {
|
||||||
|
const merged = await requestContinuation(messages, currentText);
|
||||||
|
if (!merged) { toastr.error('补全请求失败或返回为空。', '继续补全'); return null; }
|
||||||
|
if (!merged.includes('<Amily2Edit>')) {
|
||||||
|
toastr.warning('补全后仍未包含 <Amily2Edit> 指令块,可继续补全、手动应用或重新填表。', '继续补全');
|
||||||
|
} else {
|
||||||
|
toastr.success('已获得包含指令块的补全内容,可点击”手动应用”写入。', '继续补全');
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
},
|
||||||
|
onApply: (editedText) => {
|
||||||
|
if (!editedText || !editedText.includes('<Amily2Edit>')) {
|
||||||
|
toastr.warning('应用的文本中未检测到 <Amily2Edit> 指令块,已按原文尝试写入。', '手动应用');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
updateTableFromText(editedText, { immediateDelete: true });
|
||||||
|
renderTables();
|
||||||
|
toastr.success(`楼层 ${startFloor}-${endFloor} 填表完成!`);
|
||||||
|
log(`楼层 ${startFloor}-${endFloor} 填表由用户手动处理完成。`, 'success');
|
||||||
|
} catch (err) {
|
||||||
|
log(`楼层 ${startFloor}-${endFloor} 手动应用失败: ${err.message}`, 'error');
|
||||||
|
toastr.error(`手动应用失败: ${err.message}`, '写入异常');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRetry: () => {
|
||||||
|
log(`用户请求重新填写楼层 ${startFloor}-${endFloor}。`, 'warn');
|
||||||
|
setTimeout(() => startFloorRangeFilling(startFloor, endFloor), 300);
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
log(`用户取消了楼层 ${startFloor}-${endFloor} 的填表。`, 'warn');
|
||||||
|
toastr.info(`已取消楼层 ${startFloor}-${endFloor} 的填表。`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTableFromText(resultText, { immediateDelete: true });
|
updateTableFromText(resultText, { immediateDelete: true });
|
||||||
renderTables();
|
renderTables();
|
||||||
|
|
||||||
toastr.success(`楼层 ${startFloor}-${endFloor} 填表完成!`);
|
toastr.success(`楼层 ${startFloor}-${endFloor} 填表完成!`);
|
||||||
log(`楼层 ${startFloor}-${endFloor} 填表处理完成。`, 'success');
|
log(`楼层 ${startFloor}-${endFloor} 填表处理完成。`, 'success');
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(`楼层 ${startFloor}-${endFloor} 填表失败: ${error.message}`, 'error');
|
log(`楼层 ${startFloor}-${endFloor} 填表失败: ${error.message}`, 'error');
|
||||||
|
|||||||
91
core/table-system/formatters/tool-call.js
Normal file
91
core/table-system/formatters/tool-call.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* @file formatters/tool-call.js — Function Call 填表格式器
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* - 导出 TABLE_FILL_TOOL:发给模型的 tools 定义(单工具 + operations 数组)
|
||||||
|
* - 导出 parseToolCallArgs:把 tool_calls[0].function.arguments 解析为 Operation[]
|
||||||
|
*
|
||||||
|
* 与 executor.js(legacy formatter)并列;下游 applyOperations 不感知来源。
|
||||||
|
*
|
||||||
|
* @typedef {import('../dto/Operation.js').Operation} Operation
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 填表工具 schema。使用 operations 数组而非多工具并发,兼容所有支持 function calling 的提供商。
|
||||||
|
*
|
||||||
|
* data 的 key 为列索引字符串("0"、"1"...),与 executor.js legacy 格式保持一致,
|
||||||
|
* 提示词中会给出列索引与列名的对应关系。
|
||||||
|
*/
|
||||||
|
export const TABLE_FILL_TOOL = {
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'apply_table_edits',
|
||||||
|
description: '将一批表格编辑操作应用到记忆表格中。',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
operations: {
|
||||||
|
type: 'array',
|
||||||
|
description: '按顺序执行的操作列表。',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
op: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['insertRow', 'updateRow', 'deleteRow'],
|
||||||
|
description: 'insertRow=新增行,updateRow=更新已有行,deleteRow=删除行'
|
||||||
|
},
|
||||||
|
tableIndex: {
|
||||||
|
type: 'integer',
|
||||||
|
description: '目标表格的 0-based 索引'
|
||||||
|
},
|
||||||
|
rowIndex: {
|
||||||
|
type: 'integer',
|
||||||
|
description: 'updateRow / deleteRow 时必填,目标行的 0-based 索引'
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: 'object',
|
||||||
|
description: 'insertRow / updateRow 时必填,key 为列索引字符串("0"/"1"...),value 为单元格内容',
|
||||||
|
additionalProperties: { type: 'string' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['op', 'tableIndex']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['operations']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 tool_calls[0].function.arguments 字符串为 Operation[]。
|
||||||
|
* 结构校验失败的单条操作会被静默跳过,不中断整体解析。
|
||||||
|
*
|
||||||
|
* @param {string} argsString - JSON 字符串
|
||||||
|
* @returns {Operation[]}
|
||||||
|
*/
|
||||||
|
export function parseToolCallArgs(argsString) {
|
||||||
|
let parsed;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(argsString);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawOps = parsed?.operations;
|
||||||
|
if (!Array.isArray(rawOps)) return [];
|
||||||
|
|
||||||
|
/** @type {Operation[]} */
|
||||||
|
const ops = [];
|
||||||
|
for (const raw of rawOps) {
|
||||||
|
if (raw.op === 'insertRow' && Number.isInteger(raw.tableIndex) && raw.data && typeof raw.data === 'object') {
|
||||||
|
ops.push({ op: 'insertRow', tableIndex: raw.tableIndex, data: raw.data });
|
||||||
|
} else if (raw.op === 'updateRow' && Number.isInteger(raw.tableIndex) && Number.isInteger(raw.rowIndex) && raw.data && typeof raw.data === 'object') {
|
||||||
|
ops.push({ op: 'updateRow', tableIndex: raw.tableIndex, rowIndex: raw.rowIndex, data: raw.data });
|
||||||
|
} else if (raw.op === 'deleteRow' && Number.isInteger(raw.tableIndex) && Number.isInteger(raw.rowIndex)) {
|
||||||
|
ops.push({ op: 'deleteRow', tableIndex: raw.tableIndex, rowIndex: raw.rowIndex });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ops;
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ import { extensionName } from '../../utils/settings.js';
|
|||||||
|
|
||||||
import { log } from './logger.js';
|
import { log } from './logger.js';
|
||||||
import { executeCommands } from './executor.js';
|
import { executeCommands } from './executor.js';
|
||||||
|
import { applyOperations } from './actions/applyOperations.js';
|
||||||
import { fillWithSecondaryApi } from './secondary-filler.js';
|
import { fillWithSecondaryApi } from './secondary-filler.js';
|
||||||
import { renderTables } from '../../ui/table-bindings.js';
|
import { renderTables } from '../../ui/table-bindings.js';
|
||||||
import { updateOrInsertTableInChat } from '../../ui/message-table-renderer.js';
|
import { updateOrInsertTableInChat } from '../../ui/message-table-renderer.js';
|
||||||
@@ -874,6 +875,65 @@ export async function updateTableFromText(textContent, options = {}) {
|
|||||||
document.dispatchEvent(new CustomEvent('amily2-force-ui-reload'));
|
document.dispatchEvent(new CustomEvent('amily2-force-ui-reload'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 直接从 Operation[] 应用变更(Function Call 路径),跳过文本解析。
|
||||||
|
* 后续流程与 updateTableFromText 完全一致。
|
||||||
|
*
|
||||||
|
* @param {import('./dto/Operation.js').Operation[]} ops
|
||||||
|
* @param {Object} options - 同 updateTableFromText 的 options
|
||||||
|
*/
|
||||||
|
export async function updateTableFromOps(ops, options = {}) {
|
||||||
|
const settings = extension_settings[extensionName] || {};
|
||||||
|
if (settings.table_system_enabled === false) return;
|
||||||
|
|
||||||
|
if (!Array.isArray(ops) || ops.length === 0) {
|
||||||
|
log('Function Call 返回操作列表为空,无需更新表格。', 'info');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { state, changes } = applyOperations(getState(), ops);
|
||||||
|
|
||||||
|
if (changes.length === 0) {
|
||||||
|
log('Function Call 操作未产生任何实质性变更。', 'info');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(state);
|
||||||
|
|
||||||
|
if (options.immediateDelete) {
|
||||||
|
commitPendingDeletions();
|
||||||
|
}
|
||||||
|
|
||||||
|
changes.forEach(change => {
|
||||||
|
markTableUpdated(change.tableIndex);
|
||||||
|
if (change.type === 'update' || change.type === 'insert') {
|
||||||
|
if (change.rowIndex !== undefined && change.colIndex !== undefined) {
|
||||||
|
addHighlight(change.tableIndex, change.rowIndex, change.colIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
log(`Function Call 成功执行了 ${changes.length} 处变更。`, 'success');
|
||||||
|
|
||||||
|
const affectedTables = [...new Set(changes.map(c => c.tableIndex))];
|
||||||
|
affectedTables.forEach(tableIndex => dispatchTableUpdate(tableIndex));
|
||||||
|
|
||||||
|
const context = getContext();
|
||||||
|
if (context.chat && context.chat.length > 0) {
|
||||||
|
const lastMessage = context.chat[context.chat.length - 1];
|
||||||
|
if (_persistSaveStateToMessage(getState(), lastMessage)) {
|
||||||
|
await saveChat();
|
||||||
|
toastr.success('已根据AI的指示成功更新表格!', '填表完成');
|
||||||
|
document.dispatchEvent(new CustomEvent('amily2-force-ui-reload'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveChatDebounced();
|
||||||
|
toastr.success('已根据AI的指示成功更新表格!', '填表完成');
|
||||||
|
document.dispatchEvent(new CustomEvent('amily2-force-ui-reload'));
|
||||||
|
}
|
||||||
|
|
||||||
// ── 预设(re-export 或 wrapper) ─────────────────────────────────────────
|
// ── 预设(re-export 或 wrapper) ─────────────────────────────────────────
|
||||||
|
|
||||||
export const exportPreset = _presetExportPreset;
|
export const exportPreset = _presetExportPreset;
|
||||||
@@ -961,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');
|
||||||
|
|||||||
@@ -4,13 +4,67 @@ import { saveChat } from "/script.js";
|
|||||||
import { renderTables } from '../../ui/table-bindings.js';
|
import { renderTables } from '../../ui/table-bindings.js';
|
||||||
import { updateOrInsertTableInChat } from '../../ui/message-table-renderer.js';
|
import { updateOrInsertTableInChat } from '../../ui/message-table-renderer.js';
|
||||||
import { extensionName } from "../../utils/settings.js";
|
import { extensionName } from "../../utils/settings.js";
|
||||||
import { updateTableFromText, getBatchFillerRuleTemplate, getBatchFillerFlowTemplate, convertTablesToCsvString, saveStateToMessage, getMemoryState, clearHighlights } from './manager.js';
|
import { updateTableFromText, updateTableFromOps, getBatchFillerRuleTemplate, getBatchFillerFlowTemplate, convertTablesToCsvString, saveStateToMessage, getMemoryState, clearHighlights } from './manager.js';
|
||||||
import { getPresetPrompts, getMixedOrder } from '../../PresetSettings/index.js';
|
import { getPresetPrompts, getMixedOrder } from '../../PresetSettings/index.js';
|
||||||
import { callAI, generateRandomSeed } from '../api.js';
|
import { callAI, callAIForTools, generateRandomSeed } from '../api.js';
|
||||||
|
import { TABLE_FILL_TOOL, parseToolCallArgs } from './formatters/tool-call.js';
|
||||||
import { callNccsAI } from '../api/NccsApi.js';
|
import { callNccsAI } from '../api/NccsApi.js';
|
||||||
import { extractBlocksByTags, applyExclusionRules } from '../utils/rag-tag-extractor.js';
|
import { extractBlocksByTags, applyExclusionRules } from '../utils/rag-tag-extractor.js';
|
||||||
import { resolveTableRuleConfig } from '../../utils/config/RuleProfileManager.js';
|
import { resolveTableRuleConfig } from '../../utils/config/RuleProfileManager.js';
|
||||||
import { safeLorebookEntries } from '../tavernhelper-compatibility.js';
|
import { safeLorebookEntries } from '../tavernhelper-compatibility.js';
|
||||||
|
import { log } from './logger.js';
|
||||||
|
import { showTableFillReviewModal } from '../../ui/page-window.js';
|
||||||
|
|
||||||
|
const CONTINUE_PROMPT_SECONDARY = '上一条回复不完整或缺少 <Amily2Edit> 指令块。请直接从中断处继续生成剩余内容,不要重复已输出的文本,也不要添加任何解释或寒暄,确保最终输出中包含完整的 <Amily2Edit>...</Amily2Edit> 指令块。';
|
||||||
|
|
||||||
|
let secondaryFillerDebounceTimer = null;
|
||||||
|
let secondaryFillerRunning = false;
|
||||||
|
let currentAbortController = null;
|
||||||
|
|
||||||
|
async function callSecondaryModel(messages, signal) {
|
||||||
|
const settings = extension_settings[extensionName] || {};
|
||||||
|
if (settings.nccsEnabled) {
|
||||||
|
return await callNccsAI(messages, { signal });
|
||||||
|
}
|
||||||
|
return await callAI(messages, { signal });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestSecondaryContinuation(baseMessages, partialResponse) {
|
||||||
|
const continueMessages = [
|
||||||
|
...baseMessages,
|
||||||
|
{ role: 'assistant', content: partialResponse || '' },
|
||||||
|
{ role: 'user', content: CONTINUE_PROMPT_SECONDARY },
|
||||||
|
];
|
||||||
|
const continued = await callSecondaryModel(continueMessages);
|
||||||
|
if (!continued) return null;
|
||||||
|
return `${partialResponse || ''}${continued}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function markTargetsProcessed(targetMessages, { skipTableSave = false } = {}) {
|
||||||
|
if (!targetMessages || targetMessages.length === 0) return;
|
||||||
|
|
||||||
|
const lastProcessedMsg = targetMessages[targetMessages.length - 1].msg;
|
||||||
|
|
||||||
|
for (const target of targetMessages) {
|
||||||
|
if (!target.msg.extra) target.msg.extra = {};
|
||||||
|
target.msg.extra.amily2_process_hash = target.hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!skipTableSave) {
|
||||||
|
const memoryState = getMemoryState();
|
||||||
|
if (saveStateToMessage(memoryState, lastProcessedMsg)) {
|
||||||
|
renderTables();
|
||||||
|
updateOrInsertTableInChat();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveChat();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function commitSecondaryFillResult(rawContent, targetMessages) {
|
||||||
|
await updateTableFromText(rawContent);
|
||||||
|
await markTargetsProcessed(targetMessages);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async function getWorldBookContext() {
|
async function getWorldBookContext() {
|
||||||
@@ -65,11 +119,35 @@ 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 = {}) {
|
||||||
clearHighlights();
|
if (secondaryFillerRunning) {
|
||||||
|
log('分步填表正在进行中,跳过本次触发。', 'warn');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const settings = extension_settings[extensionName] || {};
|
const settings = extension_settings[extensionName] || {};
|
||||||
|
|
||||||
|
// 【V2.1.1】分步填表触发延迟 / 防抖:自动触发时若配置了延迟,则延后执行,
|
||||||
|
// 延迟期内再次到来的事件会重置计时器,避免消息连续到达时重复拉起填表。
|
||||||
|
// 注意:防抖与早返路径都不持锁,避免 setTimeout 回调撞上自己的锁导致死锁。
|
||||||
|
const delay = Math.max(0, parseInt(settings.secondary_filler_delay || 0, 10));
|
||||||
|
if (!forceRun && delay > 0) {
|
||||||
|
if (secondaryFillerDebounceTimer) {
|
||||||
|
clearTimeout(secondaryFillerDebounceTimer);
|
||||||
|
}
|
||||||
|
secondaryFillerDebounceTimer = setTimeout(() => {
|
||||||
|
secondaryFillerDebounceTimer = null;
|
||||||
|
fillWithSecondaryApi(latestMessage, forceRun, opts);
|
||||||
|
}, delay);
|
||||||
|
console.log(`[Amily2-副API] 分步填表已按防抖延迟 ${delay}ms 调度。`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (secondaryFillerDebounceTimer) {
|
||||||
|
clearTimeout(secondaryFillerDebounceTimer);
|
||||||
|
secondaryFillerDebounceTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearHighlights();
|
||||||
|
|
||||||
// 总开关关闭时,分步填表同样禁用
|
// 总开关关闭时,分步填表同样禁用
|
||||||
if (settings.table_system_enabled === false) {
|
if (settings.table_system_enabled === false) {
|
||||||
log('【分步填表】表格系统总开关已关闭,跳过。', 'info');
|
log('【分步填表】表格系统总开关已关闭,跳过。', 'info');
|
||||||
@@ -93,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;
|
||||||
@@ -131,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];
|
||||||
@@ -138,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;
|
||||||
@@ -147,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,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-分步填表");
|
||||||
@@ -272,13 +371,40 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
|
|||||||
console.dir(messages);
|
console.dir(messages);
|
||||||
console.groupEnd();
|
console.groupEnd();
|
||||||
|
|
||||||
|
if (settings.tableFillFunctionCall) {
|
||||||
|
// Function Call 路径
|
||||||
|
const argsString = await callAIForTools(messages, TABLE_FILL_TOOL, { slot: 'tableFilling', signal });
|
||||||
|
if (!argsString) {
|
||||||
|
console.error('[Amily2-副API] Function Call 返回为空。');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ops = parseToolCallArgs(argsString);
|
||||||
|
if (ops.length === 0) {
|
||||||
|
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-分步填表');
|
||||||
|
await markTargetsProcessed(targetMessages, { skipTableSave: true });
|
||||||
|
} else {
|
||||||
|
await updateTableFromOps(ops);
|
||||||
|
await markTargetsProcessed(targetMessages);
|
||||||
|
toastr.success('分步填表(Function Call)执行完毕。', 'Amily2-分步填表');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Legacy 文本路径
|
||||||
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) {
|
||||||
@@ -286,65 +412,131 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[Amily2号-副API-原始回复]:", rawContent);
|
console.log('[Amily2号-副API-原始回复]:', rawContent);
|
||||||
|
|
||||||
// 【修复】检查 AI 是否返回了有效的指令块,防止 AI 偷懒或格式错误被误判为成功
|
|
||||||
if (!rawContent.includes('<Amily2Edit>')) {
|
if (!rawContent.includes('<Amily2Edit>')) {
|
||||||
throw new Error('AI未返回有效的 <Amily2Edit> 指令块,可能格式错误或未产生实质性变更。');
|
const rangeLabel = `${targetMessages[0].index + 1} - ${targetMessages[targetMessages.length - 1].index + 1}`;
|
||||||
|
console.warn(`[Amily2-副API] 响应未包含 <Amily2Edit> 指令块(楼层 ${rangeLabel}),弹出检查窗口等待用户处理。`);
|
||||||
|
toastr.warning(`分步填表(楼层 ${rangeLabel})的响应缺少 <Amily2Edit> 指令块,请在弹窗中处理。`, 'Amily2-分步填表');
|
||||||
|
if (latestMessage && latestMessage.extra) {
|
||||||
|
delete latestMessage.extra.amily2_retry_count;
|
||||||
|
}
|
||||||
|
showTableFillReviewModal(rawContent, {
|
||||||
|
title: `分步填表响应检查 - 楼层 ${rangeLabel}`,
|
||||||
|
subtitle: `分步填表(楼层 ${rangeLabel})的 AI 响应未包含有效的 <Amily2Edit> 指令块。请检查原始响应并选择处理方式。`,
|
||||||
|
onContinue: async (currentText) => {
|
||||||
|
const merged = await requestSecondaryContinuation(messages, currentText);
|
||||||
|
if (!merged) { toastr.error('补全请求失败或返回为空。', '继续补全'); return null; }
|
||||||
|
if (!merged.includes('<Amily2Edit>')) {
|
||||||
|
toastr.warning('补全后仍未包含 <Amily2Edit> 指令块,可继续补全、手动应用或重新填表。', '继续补全');
|
||||||
|
} else {
|
||||||
|
toastr.success('已获得包含指令块的补全内容,可点击”手动应用”写入。', '继续补全');
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
},
|
||||||
|
onApply: async (editedText) => {
|
||||||
|
if (!editedText || !editedText.includes('<Amily2Edit>')) {
|
||||||
|
toastr.warning('应用的文本中未检测到 <Amily2Edit> 指令块,已按原文尝试写入。', '手动应用');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await commitSecondaryFillResult(editedText, targetMessages);
|
||||||
|
toastr.success('分步填表已由用户手动处理完成。', 'Amily2-分步填表');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Amily2-副API] 手动应用失败:', err);
|
||||||
|
toastr.error(`手动应用失败: ${err.message}`, '写入异常');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRetry: () => {
|
||||||
|
if (latestMessage && latestMessage.extra) {
|
||||||
|
delete latestMessage.extra.amily2_retry_count;
|
||||||
|
}
|
||||||
|
toastr.info('将重新执行分步填表...', 'Amily2-分步填表');
|
||||||
|
setTimeout(() => fillWithSecondaryApi(latestMessage, forceRun, opts), 300);
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
toastr.info('已取消本次分步填表。', 'Amily2-分步填表');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTableFromText(rawContent);
|
await commitSecondaryFillResult(rawContent, targetMessages);
|
||||||
|
|
||||||
const memoryState = getMemoryState();
|
|
||||||
|
|
||||||
const lastProcessedMsg = targetMessages[targetMessages.length - 1].msg;
|
|
||||||
|
|
||||||
for (const target of targetMessages) {
|
|
||||||
if (!target.msg.metadata) target.msg.metadata = {};
|
|
||||||
target.msg.metadata.Amily2_Process_Hash = target.hash;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (saveStateToMessage(memoryState, lastProcessedMsg)) {
|
|
||||||
renderTables();
|
|
||||||
updateOrInsertTableInChat();
|
|
||||||
}
|
|
||||||
|
|
||||||
saveChat();
|
|
||||||
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;
|
||||||
|
|||||||
@@ -158,4 +158,10 @@ export const tableSystemDefaultSettings = {
|
|||||||
// Nccs API 设置
|
// Nccs API 设置
|
||||||
nccsEnabled: false,
|
nccsEnabled: false,
|
||||||
nccsFakeStreamEnabled: false,
|
nccsFakeStreamEnabled: false,
|
||||||
|
|
||||||
|
// Function Call 填表
|
||||||
|
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.1",
|
"version": "2.2.5",
|
||||||
"author": "Wx-2025",
|
"author": "Wx-2025",
|
||||||
"description": "一个拥有独立UI的智能引擎,正文优化、自动总结、记忆表格、rag向量、隐藏楼层、剧情推进等多功能整合。",
|
"description": "一个拥有独立UI的智能引擎,正文优化、自动总结、记忆表格、rag向量、隐藏楼层、剧情推进等多功能整合。",
|
||||||
"minSillyTavernVersion": "1.10.0",
|
"minSillyTavernVersion": "1.10.0",
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ import { setAvailableModels, populateModelDropdown, getLatestUpdateInfo } from "
|
|||||||
import { fixCommand, testReplyChecker } from "../core/commands.js";
|
import { fixCommand, testReplyChecker } from "../core/commands.js";
|
||||||
import { messageFormatting } from '/script.js';
|
import { messageFormatting } from '/script.js';
|
||||||
import { executeManualCommand } from '../core/autoHideManager.js';
|
import { executeManualCommand } from '../core/autoHideManager.js';
|
||||||
import { showContentModal, showHtmlModal } from './page-window.js';
|
import { showContentModal, showHtmlModal, showCwbWarningModal } from './page-window.js';
|
||||||
import { openAutoCharCardWindow } from '../core/auto-char-card/ui-bindings.js';
|
import { openAutoCharCardWindow } from '../core/auto-char-card/ui-bindings.js';
|
||||||
|
import { showPresetSettings } from '../PresetSettings/prese_ui.js';
|
||||||
|
|
||||||
function displayDailyAuthCode() {
|
function displayDailyAuthCode() {
|
||||||
const displayEl = document.getElementById('amily2_daily_code_display');
|
const displayEl = document.getElementById('amily2_daily_code_display');
|
||||||
@@ -806,7 +807,7 @@ export function bindModalEvents() {
|
|||||||
container
|
container
|
||||||
.off("click.amily2.chamber_nav")
|
.off("click.amily2.chamber_nav")
|
||||||
.on("click.amily2.chamber_nav",
|
.on("click.amily2.chamber_nav",
|
||||||
"#amily2_open_text_optimization, #amily2_open_plot_optimization, #amily2_open_additional_features, #amily2_open_rag_palace, #amily2_open_memorisation_forms, #amily2_open_character_world_book, #amily2_open_world_editor, #amily2_open_glossary, #amily2_open_renderer, #amily2_open_super_memory, #amily2_open_auto_char_card, #amily2_open_api_config, #amily2_open_rule_config, #amily2_open_sfigen, #amily2_back_to_main_settings, #amily2_back_to_main_from_hanlinyuan, #amily2_back_to_main_from_forms, #amily2_back_to_main_from_optimization, #amily2_back_to_main_from_text_optimization, #amily2_back_to_main_from_cwb, #amily2_back_to_main_from_world_editor, #amily2_back_to_main_from_glossary, #amily2_renderer_back_button, #amily2_back_to_main_from_super_memory, #amily2_back_to_main_from_api_config, #amily2_back_to_main_from_rule_config, #amily2_sfigen_back_to_main", function () {
|
"#amily2_open_text_optimization, #amily2_open_plot_optimization, #amily2_open_additional_features, #amily2_open_rag_palace, #amily2_open_memorisation_forms, #amily2_open_character_world_book, #amily2_open_world_editor, #amily2_open_glossary, #amily2_open_renderer, #amily2_open_super_memory, #amily2_open_auto_char_card, #amily2_open_api_config, #amily2_open_rule_config, #amily2_open_sfigen, #amily2_open_preset_editor, #amily2_back_to_main_settings, #amily2_back_to_main_from_hanlinyuan, #amily2_back_to_main_from_forms, #amily2_back_to_main_from_optimization, #amily2_back_to_main_from_text_optimization, #amily2_back_to_main_from_cwb, #amily2_back_to_main_from_world_editor, #amily2_back_to_main_from_glossary, #amily2_renderer_back_button, #amily2_back_to_main_from_super_memory, #amily2_back_to_main_from_api_config, #amily2_back_to_main_from_rule_config, #amily2_sfigen_back_to_main", function () {
|
||||||
if (!pluginAuthStatus.authorized) return;
|
if (!pluginAuthStatus.authorized) return;
|
||||||
|
|
||||||
const mainPanel = container.find('.plugin-features');
|
const mainPanel = container.find('.plugin-features');
|
||||||
@@ -874,7 +875,10 @@ export function bindModalEvents() {
|
|||||||
memorisationFormsPanel.show();
|
memorisationFormsPanel.show();
|
||||||
break;
|
break;
|
||||||
case 'amily2_open_character_world_book':
|
case 'amily2_open_character_world_book':
|
||||||
characterWorldBookPanel.show();
|
showCwbWarningModal(
|
||||||
|
() => characterWorldBookPanel.show(),
|
||||||
|
() => mainPanel.show()
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case 'amily2_open_world_editor':
|
case 'amily2_open_world_editor':
|
||||||
worldEditorPanel.show();
|
worldEditorPanel.show();
|
||||||
@@ -891,6 +895,10 @@ export function bindModalEvents() {
|
|||||||
case 'amily2_open_sfigen':
|
case 'amily2_open_sfigen':
|
||||||
sfigenPanel.show();
|
sfigenPanel.show();
|
||||||
break;
|
break;
|
||||||
|
case 'amily2_open_preset_editor':
|
||||||
|
showPresetSettings();
|
||||||
|
mainPanel.show();
|
||||||
|
return;
|
||||||
case 'amily2_back_to_main_settings':
|
case 'amily2_back_to_main_settings':
|
||||||
case 'amily2_back_to_main_from_hanlinyuan':
|
case 'amily2_back_to_main_from_hanlinyuan':
|
||||||
case 'amily2_back_to_main_from_forms':
|
case 'amily2_back_to_main_from_forms':
|
||||||
|
|||||||
@@ -77,16 +77,6 @@ function updateAndSaveSetting(key, value) {
|
|||||||
|
|
||||||
HanlinyuanCore.saveSettings();
|
HanlinyuanCore.saveSettings();
|
||||||
|
|
||||||
if (key === 'condensation.tagExtractionEnabled') {
|
|
||||||
syncHanlinLinkedRuleProfile('condensation', { tagExtractionEnabled: value });
|
|
||||||
} else if (key === 'condensation.tags') {
|
|
||||||
syncHanlinLinkedRuleProfile('condensation', { tags: value });
|
|
||||||
} else if (key === 'queryPreprocessing.tagExtractionEnabled') {
|
|
||||||
syncHanlinLinkedRuleProfile('queryPreprocessing', { tagExtractionEnabled: value });
|
|
||||||
} else if (key === 'queryPreprocessing.tags') {
|
|
||||||
syncHanlinLinkedRuleProfile('queryPreprocessing', { tags: value });
|
|
||||||
}
|
|
||||||
|
|
||||||
log(`[自动保存] 设置项 '${key}' 已更新为: ${JSON.stringify(value)}`, 'success');
|
log(`[自动保存] 设置项 '${key}' 已更新为: ${JSON.stringify(value)}`, 'success');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,15 +380,7 @@ function bindInternalUIEvents() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 注入设置的UI逻辑已由 initializeUnifiedInjectionEditor 函数统一处理。
|
// 注入设置的UI逻辑已由 initializeUnifiedInjectionEditor 函数统一处理。
|
||||||
|
// 标签提取开关/输入框已在 2.1.0 重构中移除,改为规则配置下拉选单管理。
|
||||||
// 【新增】为“标签提取”复选框绑定事件
|
|
||||||
const tagExtractionToggle = document.getElementById('hly-tag-extraction-toggle');
|
|
||||||
const tagInputContainer = document.getElementById('hly-tag-input-container');
|
|
||||||
if (tagExtractionToggle && tagInputContainer) {
|
|
||||||
tagExtractionToggle.addEventListener('change', () => {
|
|
||||||
tagInputContainer.style.display = tagExtractionToggle.checked ? 'block' : 'none';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 为“书库选择”下拉框绑定联动事件
|
// 为“书库选择”下拉框绑定联动事件
|
||||||
const librarySelect = document.getElementById('hly-hist-select-library');
|
const librarySelect = document.getElementById('hly-hist-select-library');
|
||||||
@@ -664,20 +646,12 @@ export function loadSettingsToUI() {
|
|||||||
histMaxRetriesEl.value = settings.historiographyMaxRetries ?? 2;
|
histMaxRetriesEl.value = settings.historiographyMaxRetries ?? 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注:hly-tag-extraction-toggle / hly-tag-input / hly-tag-input-container 已从 HTML 移除,
|
// 标签提取开关/输入框已在 2.1.0 重构中移除(改为规则配置下拉选单),
|
||||||
// 标签提取规则改由 RuleProfileManager 管理。此处保留兼容性 null 检查,避免抛错吞掉后续段落加载。
|
// 这里不再回填对应 DOM,避免因元素已不存在导致 loadSettingsToUI 中断。
|
||||||
const tagExtractionToggle = document.getElementById('hly-tag-extraction-toggle');
|
|
||||||
const tagInput = document.getElementById('hly-tag-input');
|
|
||||||
const tagInputContainer = document.getElementById('hly-tag-input-container');
|
|
||||||
|
|
||||||
if (tagExtractionToggle) tagExtractionToggle.checked = settings.condensation.tagExtractionEnabled;
|
|
||||||
if (tagInput) tagInput.value = settings.condensation.tags;
|
|
||||||
if (tagInputContainer && tagExtractionToggle) {
|
|
||||||
tagInputContainer.style.display = tagExtractionToggle.checked ? 'block' : 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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');
|
||||||
@@ -701,7 +675,7 @@ export function loadSettingsToUI() {
|
|||||||
|
|
||||||
const sources = ['novel', 'chat_history', 'lorebook', 'manual'];
|
const sources = ['novel', 'chat_history', 'lorebook', 'manual'];
|
||||||
sources.forEach(source => {
|
sources.forEach(source => {
|
||||||
const sourceSettings = prioritySettings.sources[source];
|
const sourceSettings = prioritySettings.sources?.[source];
|
||||||
if (sourceSettings) {
|
if (sourceSettings) {
|
||||||
const enabledCheckbox = document.querySelector(`[data-setting-key="rerank.priorityRetrieval.sources.${source}.enabled"]`);
|
const enabledCheckbox = document.querySelector(`[data-setting-key="rerank.priorityRetrieval.sources.${source}.enabled"]`);
|
||||||
const countInput = document.querySelector(`[data-setting-key="rerank.priorityRetrieval.sources.${source}.count"]`);
|
const countInput = document.querySelector(`[data-setting-key="rerank.priorityRetrieval.sources.${source}.count"]`);
|
||||||
@@ -732,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';
|
||||||
|
|
||||||
|
|||||||
@@ -167,3 +167,145 @@ export function showSummaryModal(summaryText, callbacks) {
|
|||||||
|
|
||||||
dialogElement.find('.popup-controls').prepend(regenerateButton);
|
dialogElement.find('.popup-controls').prepend(regenerateButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function showTableFillReviewModal(rawResponse, callbacks = {}) {
|
||||||
|
const {
|
||||||
|
title = '填表响应检查',
|
||||||
|
subtitle = 'AI未返回有效的 <Amily2Edit> 指令块。您可以在下方查看/编辑原始响应,并选择后续处理方式。',
|
||||||
|
onApply,
|
||||||
|
onContinue,
|
||||||
|
onRetry,
|
||||||
|
onCancel,
|
||||||
|
} = callbacks;
|
||||||
|
|
||||||
|
const modalHtml = `
|
||||||
|
<div class="amily2-fill-review-modal">
|
||||||
|
<div class="notes" style="margin-bottom: 10px; color: #ffb74d; line-height: 1.6;">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i> ${escapeHtml(subtitle)}
|
||||||
|
</div>
|
||||||
|
<textarea class="text_pole amily2-fill-review-text"
|
||||||
|
style="width: 100%; height: 45vh; resize: vertical; font-family: var(--monoFontFamily, monospace); font-size: 12px; white-space: pre; overflow-wrap: normal; overflow-x: auto;"
|
||||||
|
>${escapeHtml(rawResponse || '')}</textarea>
|
||||||
|
<div class="notes" style="margin-top: 8px; font-size: 0.85em; opacity: 0.8; line-height: 1.6;">
|
||||||
|
<div><b>继续补全</b>:让 AI 基于当前文本继续生成剩余内容,结果会追加到文本框后。</div>
|
||||||
|
<div><b>重新填表</b>:舍弃当前响应并重新向 AI 请求同一批次的填表。</div>
|
||||||
|
<div><b>手动应用</b>:将文本框中的当前内容直接作为最终结果写入表格(跳过格式校验)。</div>
|
||||||
|
<div><b>取消</b>:放弃本次填表,任务暂停。</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const dialogElement = showHtmlModal(title, modalHtml, {
|
||||||
|
okText: '手动应用',
|
||||||
|
cancelText: '取消',
|
||||||
|
showCancel: true,
|
||||||
|
onOk: (dialog) => {
|
||||||
|
const editedText = dialog.find('.amily2-fill-review-text').val();
|
||||||
|
if (onApply) {
|
||||||
|
onApply(editedText);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
if (onCancel) {
|
||||||
|
onCancel();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const textarea = dialogElement.find('.amily2-fill-review-text');
|
||||||
|
|
||||||
|
if (typeof onContinue === 'function') {
|
||||||
|
const continueButton = $('<button class="menu_button interactable" style="margin-right: auto;"><i class="fas fa-forward"></i> 继续补全</button>');
|
||||||
|
continueButton.on('click', async () => {
|
||||||
|
const currentText = textarea.val();
|
||||||
|
textarea.prop('disabled', true);
|
||||||
|
continueButton.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> 正在请求补全...');
|
||||||
|
try {
|
||||||
|
const continued = await onContinue(currentText);
|
||||||
|
if (typeof continued === 'string' && continued.length > 0) {
|
||||||
|
textarea.val(continued);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Amily2 填表检查] 补全请求失败:', err);
|
||||||
|
if (window.toastr) toastr.error(`补全失败: ${err.message || err}`, '继续补全');
|
||||||
|
} finally {
|
||||||
|
textarea.prop('disabled', false);
|
||||||
|
continueButton.prop('disabled', false).html('<i class="fas fa-forward"></i> 继续补全');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dialogElement.find('.popup-controls').prepend(continueButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof onRetry === 'function') {
|
||||||
|
const retryButton = $('<button class="menu_button secondary interactable"><i class="fas fa-redo"></i> 重新填表</button>');
|
||||||
|
retryButton.on('click', () => {
|
||||||
|
dialogElement[0].close();
|
||||||
|
dialogElement.remove();
|
||||||
|
onRetry();
|
||||||
|
});
|
||||||
|
const okBtn = dialogElement.find('.popup-button-ok');
|
||||||
|
if (okBtn.length) {
|
||||||
|
retryButton.insertBefore(okBtn);
|
||||||
|
} else {
|
||||||
|
dialogElement.find('.popup-controls').append(retryButton);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dialogElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CWB_WARNING_COUNTDOWN = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色世界书入口警告弹窗,强制倒计时后才可继续。
|
||||||
|
* @param {Function} onProceed - 用户点击"继续使用"时的回调
|
||||||
|
* @param {Function} onClose - 用户点击"关闭退出"时的回调(含弹窗关闭前直接离开)
|
||||||
|
*/
|
||||||
|
export function showCwbWarningModal(onProceed, onClose) {
|
||||||
|
const dialogHtml = `
|
||||||
|
<dialog class="popup wide_dialogue_popup">
|
||||||
|
<div class="popup-body">
|
||||||
|
<h3 style="margin-top:0; color:#e8a838; border-bottom:1px solid rgba(255,255,255,0.2); padding-bottom:10px;">
|
||||||
|
<i class="fas fa-exclamation-triangle" style="color:#e8a838;"></i> 注意 — 角色世界书功能维护状态
|
||||||
|
</h3>
|
||||||
|
<div style="line-height:1.8; padding:12px 4px; color:var(--SmartThemeBodyColor);">
|
||||||
|
该功能长期未进行维护且其实现可被表格及其他功能替代,若非必须一般不建议使用,如确认希望使用,请明确该功能无法获得有效技术支持。
|
||||||
|
</div>
|
||||||
|
<div class="popup-controls" style="gap:8px;">
|
||||||
|
<button class="cwb-warning-close menu_button secondary interactable">关闭退出</button>
|
||||||
|
<button class="cwb-warning-proceed menu_button menu_button_primary interactable" disabled>
|
||||||
|
继续使用(<span class="cwb-countdown">${CWB_WARNING_COUNTDOWN}</span>)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>`;
|
||||||
|
|
||||||
|
const $dialog = $(dialogHtml).appendTo('body');
|
||||||
|
|
||||||
|
const close = (cb) => {
|
||||||
|
clearInterval(timer);
|
||||||
|
$dialog[0].close();
|
||||||
|
$dialog.remove();
|
||||||
|
cb?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
$dialog.find('.cwb-warning-close').on('click', () => close(onClose));
|
||||||
|
|
||||||
|
$dialog.find('.cwb-warning-proceed').on('click', function () {
|
||||||
|
if (!this.disabled) close(onProceed);
|
||||||
|
});
|
||||||
|
|
||||||
|
let remaining = CWB_WARNING_COUNTDOWN;
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
remaining -= 1;
|
||||||
|
$dialog.find('.cwb-countdown').text(remaining);
|
||||||
|
if (remaining <= 0) {
|
||||||
|
clearInterval(timer);
|
||||||
|
const $btn = $dialog.find('.cwb-warning-proceed');
|
||||||
|
$btn.prop('disabled', false).html('继续使用');
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
$dialog[0].showModal();
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
import { extension_settings, getContext } from "/scripts/extensions.js";
|
import { extension_settings, getContext } from "/scripts/extensions.js";
|
||||||
import { characters, this_chid, getRequestHeaders, saveSettingsDebounced, eventSource, event_types } from "/script.js";
|
import { characters, this_chid, getRequestHeaders, saveSettingsDebounced, eventSource, event_types } from "/script.js";
|
||||||
import { defaultSettings, extensionName } from "../utils/settings.js";
|
import { defaultSettings, extensionName } from "../utils/settings.js";
|
||||||
import { testJqyhApiConnection, fetchJqyhModels } from '../core/api/JqyhApi.js';
|
|
||||||
import { testConcurrentApiConnection, fetchConcurrentModels } from '../core/api/ConcurrentApi.js';
|
import { testConcurrentApiConnection, fetchConcurrentModels } from '../core/api/ConcurrentApi.js';
|
||||||
import { safeLorebooks, safeCharLorebooks, safeLorebookEntries } from "../core/tavernhelper-compatibility.js";
|
import { safeLorebooks, safeCharLorebooks, safeLorebookEntries } from "../core/tavernhelper-compatibility.js";
|
||||||
import { createDrawer } from '../ui/drawer.js';
|
import { createDrawer } from '../ui/drawer.js';
|
||||||
@@ -45,30 +44,6 @@ function opt_toCamelCase(str) {
|
|||||||
return str.replace(/[-_]([a-z])/g, (g) => g[1].toUpperCase());
|
return str.replace(/[-_]([a-z])/g, (g) => g[1].toUpperCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
function opt_updateApiUrlVisibility(panel, apiMode) {
|
|
||||||
const customApiSettings = panel.find('#amily2_opt_custom_api_settings_block');
|
|
||||||
const tavernProfileSettings = panel.find('#amily2_opt_tavern_api_profile_block');
|
|
||||||
const apiUrlInput = panel.find('#amily2_opt_api_url');
|
|
||||||
|
|
||||||
customApiSettings.hide();
|
|
||||||
tavernProfileSettings.hide();
|
|
||||||
|
|
||||||
if (apiMode === 'tavern') {
|
|
||||||
tavernProfileSettings.show();
|
|
||||||
} else {
|
|
||||||
customApiSettings.show();
|
|
||||||
if (apiMode === 'google') {
|
|
||||||
panel.find('#amily2_opt_api_url_block').hide();
|
|
||||||
const googleUrl = 'https://generativelanguage.googleapis.com';
|
|
||||||
if (apiUrlInput.val() !== googleUrl) {
|
|
||||||
apiUrlInput.val(googleUrl).attr('type', 'text').trigger('change');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
panel.find('#amily2_opt_api_url_block').show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function opt_updateWorldbookSourceVisibility(panel, source) {
|
function opt_updateWorldbookSourceVisibility(panel, source) {
|
||||||
const manualSelectionWrapper = panel.find('#amily2_opt_worldbook_select_wrapper');
|
const manualSelectionWrapper = panel.find('#amily2_opt_worldbook_select_wrapper');
|
||||||
if (source === 'manual') {
|
if (source === 'manual') {
|
||||||
@@ -85,49 +60,6 @@ function opt_updateWorldbookSourceVisibility(panel, source) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function opt_loadTavernApiProfiles(panel) {
|
|
||||||
const select = panel.find('#amily2_opt_tavern_api_profile_select');
|
|
||||||
const apiSettings = opt_getMergedSettings();
|
|
||||||
const currentProfileId = apiSettings.plotOpt_tavernProfile;
|
|
||||||
|
|
||||||
const currentValue = select.val();
|
|
||||||
select.empty().append(new Option('-- 请选择一个酒馆预设 --', ''));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const tavernProfiles = getContext().extensionSettings?.connectionManager?.profiles || [];
|
|
||||||
if (!tavernProfiles || tavernProfiles.length === 0) {
|
|
||||||
select.append($('<option>', { value: '', text: '未找到酒馆预设', disabled: true }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let foundCurrentProfile = false;
|
|
||||||
tavernProfiles.forEach(profile => {
|
|
||||||
if (profile.api && profile.preset) {
|
|
||||||
const option = $('<option>', {
|
|
||||||
value: profile.id,
|
|
||||||
text: profile.name || profile.id,
|
|
||||||
selected: profile.id === currentProfileId
|
|
||||||
});
|
|
||||||
select.append(option);
|
|
||||||
if (profile.id === currentProfileId) {
|
|
||||||
foundCurrentProfile = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (currentProfileId && !foundCurrentProfile) {
|
|
||||||
toastr.warning(`之前选择的酒馆预设 "${currentProfileId}" 已不存在,请重新选择。`);
|
|
||||||
opt_saveSetting('tavernProfile', '');
|
|
||||||
} else if (foundCurrentProfile) {
|
|
||||||
select.val(currentProfileId);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[${extensionName}] 加载酒馆API预设失败:`, error);
|
|
||||||
toastr.error('无法加载酒馆API预设列表,请查看控制台。');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const opt_characterSpecificSettings = [
|
const opt_characterSpecificSettings = [
|
||||||
'plotOpt_worldbookSource',
|
'plotOpt_worldbookSource',
|
||||||
@@ -640,27 +572,9 @@ function opt_loadSettings(panel) {
|
|||||||
panel.find('#amily2_opt_table_enabled').val(tableEnabledValue);
|
panel.find('#amily2_opt_table_enabled').val(tableEnabledValue);
|
||||||
|
|
||||||
panel.find('#amily2_opt_ejs_enabled').prop('checked', settings.plotOpt_ejsEnabled);
|
panel.find('#amily2_opt_ejs_enabled').prop('checked', settings.plotOpt_ejsEnabled);
|
||||||
panel.find(`input[name="amily2_opt_api_mode"][value="${settings.plotOpt_apiMode}"]`).prop('checked', true);
|
|
||||||
panel.find('#amily2_opt_tavern_api_profile_select').val(settings.plotOpt_tavernProfile);
|
|
||||||
panel.find(`input[name="amily2_opt_worldbook_source"][value="${settings.plotOpt_worldbookSource || 'character'}"]`).prop('checked', true);
|
panel.find(`input[name="amily2_opt_worldbook_source"][value="${settings.plotOpt_worldbookSource || 'character'}"]`).prop('checked', true);
|
||||||
panel.find('#amily2_opt_worldbook_enabled').prop('checked', settings.plotOpt_worldbookEnabled);
|
panel.find('#amily2_opt_worldbook_enabled').prop('checked', settings.plotOpt_worldbookEnabled);
|
||||||
panel.find('#amily2_opt_new_memory_logic_enabled').prop('checked', settings.plotOpt_newMemoryLogicEnabled);
|
panel.find('#amily2_opt_new_memory_logic_enabled').prop('checked', settings.plotOpt_newMemoryLogicEnabled);
|
||||||
panel.find('#amily2_opt_api_url').val(settings.plotOpt_apiUrl);
|
|
||||||
// plotOpt_apiKey 是敏感字段,从 configManager(localStorage)读取
|
|
||||||
panel.find('#amily2_opt_api_key').val(configManager.get('plotOpt_apiKey') || '');
|
|
||||||
|
|
||||||
const modelInput = panel.find('#amily2_opt_model');
|
|
||||||
const modelSelect = panel.find('#amily2_opt_model_select');
|
|
||||||
|
|
||||||
modelInput.val(settings.plotOpt_model);
|
|
||||||
modelSelect.empty();
|
|
||||||
if (settings.plotOpt_model) {
|
|
||||||
modelSelect.append(new Option(settings.plotOpt_model, settings.plotOpt_model, true, true));
|
|
||||||
} else {
|
|
||||||
modelSelect.append(new Option('<-请先获取模型', '', true, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
syncModelMirror(modelInput.get(0), modelSelect.get(0));
|
|
||||||
panel.find('#amily2_opt_top_p').val(settings.plotOpt_top_p);
|
panel.find('#amily2_opt_top_p').val(settings.plotOpt_top_p);
|
||||||
panel.find('#amily2_opt_presence_penalty').val(settings.plotOpt_presence_penalty);
|
panel.find('#amily2_opt_presence_penalty').val(settings.plotOpt_presence_penalty);
|
||||||
panel.find('#amily2_opt_frequency_penalty').val(settings.plotOpt_frequency_penalty);
|
panel.find('#amily2_opt_frequency_penalty').val(settings.plotOpt_frequency_penalty);
|
||||||
@@ -690,7 +604,6 @@ function opt_loadSettings(panel) {
|
|||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
opt_updateApiUrlVisibility(panel, settings.plotOpt_apiMode);
|
|
||||||
opt_updateWorldbookSourceVisibility(panel, settings.plotOpt_worldbookSource || 'character');
|
opt_updateWorldbookSourceVisibility(panel, settings.plotOpt_worldbookSource || 'character');
|
||||||
|
|
||||||
opt_bindSlider(panel, '#amily2_opt_top_p', '#amily2_opt_top_p_value');
|
opt_bindSlider(panel, '#amily2_opt_top_p', '#amily2_opt_top_p_value');
|
||||||
@@ -703,7 +616,6 @@ function opt_loadSettings(panel) {
|
|||||||
opt_loadWorldbookEntries(panel);
|
opt_loadWorldbookEntries(panel);
|
||||||
});
|
});
|
||||||
|
|
||||||
opt_loadTavernApiProfiles(panel);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1219,17 +1131,13 @@ export function initializePlotOptimizationBindings() {
|
|||||||
opt_saveSetting(key, value);
|
opt_saveSetting(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === 'plotOpt_api_mode') {
|
|
||||||
opt_updateApiUrlVisibility(panel, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element.name === 'amily2_opt_worldbook_source') {
|
if (element.name === 'amily2_opt_worldbook_source') {
|
||||||
opt_updateWorldbookSourceVisibility(panel, value);
|
opt_updateWorldbookSourceVisibility(panel, value);
|
||||||
opt_loadWorldbookEntries(panel);
|
opt_loadWorldbookEntries(panel);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const allInputSelectors = [
|
const allInputSelectors = [
|
||||||
'input[type="checkbox"]', 'input[type="radio"]', 'select:not(#amily2_opt_model_select)',
|
'input[type="checkbox"]', 'input[type="radio"]', 'select',
|
||||||
'input[type="text"]', 'input[type="password"]', 'textarea',
|
'input[type="text"]', 'input[type="password"]', 'textarea',
|
||||||
'input[type="range"]', 'input[type="number"]'
|
'input[type="range"]', 'input[type="number"]'
|
||||||
].join(', ');
|
].join(', ');
|
||||||
@@ -1238,30 +1146,6 @@ export function initializePlotOptimizationBindings() {
|
|||||||
handleSettingChange(this);
|
handleSettingChange(this);
|
||||||
});
|
});
|
||||||
|
|
||||||
panel.on('input.amily2_opt change.amily2_opt', '#amily2_opt_model', function() {
|
|
||||||
syncModelMirror(
|
|
||||||
panel.find('#amily2_opt_model').get(0),
|
|
||||||
panel.find('#amily2_opt_model_select').get(0)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
panel.on('change.amily2_opt', '#amily2_opt_model_select', function() {
|
|
||||||
const selectedModel = $(this).val();
|
|
||||||
if (selectedModel) {
|
|
||||||
panel.find('#amily2_opt_model').val(selectedModel).trigger('change');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
panel.on('click.amily2_opt', '#amily2_opt_refresh_tavern_api_profiles', () => {
|
|
||||||
opt_loadTavernApiProfiles(panel);
|
|
||||||
});
|
|
||||||
|
|
||||||
panel.on('change.amily2_opt', '#amily2_opt_tavern_api_profile_select', function() {
|
|
||||||
const value = $(this).val();
|
|
||||||
opt_saveSetting('tavernProfile', value);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
panel.find('#amily2_opt_import_prompt_presets').on('click', () => panel.find('#amily2_opt_preset_file_input').click());
|
panel.find('#amily2_opt_import_prompt_presets').on('click', () => panel.find('#amily2_opt_preset_file_input').click());
|
||||||
panel.find('#amily2_opt_export_prompt_presets').on('click', () => opt_exportPromptPresets());
|
panel.find('#amily2_opt_export_prompt_presets').on('click', () => opt_exportPromptPresets());
|
||||||
@@ -1391,220 +1275,9 @@ export function initializePlotOptimizationBindings() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Jqyh API 事件绑定函数 ==========
|
// ========== Jqyh API 事件绑定函数(已迁移至 plotOpt 槽位,此处仅保留空壳) ==========
|
||||||
function bindJqyhApiEvents() {
|
function bindJqyhApiEvents() {
|
||||||
console.log("[Amily2号-Jqyh工部] 正在绑定Jqyh API事件...");
|
// Jqyh 直连配置已移除,剧情优化统一走 ApiProfile plotOpt 槽位
|
||||||
|
|
||||||
const updateAndSaveSetting = (key, value) => {
|
|
||||||
console.log(`[Amily2-Jqyh令] 收到指令: 将 [${key}] 设置为 ->`, value);
|
|
||||||
if (!extension_settings[extensionName]) {
|
|
||||||
extension_settings[extensionName] = {};
|
|
||||||
}
|
|
||||||
extension_settings[extensionName][key] = value;
|
|
||||||
saveSettingsDebounced();
|
|
||||||
console.log(`[Amily2-Jqyh录] [${key}] 的新状态已保存。`);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Jqyh API 开关控制
|
|
||||||
const jqyhToggle = document.getElementById('amily2_jqyh_enabled');
|
|
||||||
const jqyhContent = document.getElementById('amily2_jqyh_content');
|
|
||||||
|
|
||||||
if (jqyhToggle && jqyhContent) {
|
|
||||||
jqyhToggle.checked = extension_settings[extensionName].jqyhEnabled ?? false;
|
|
||||||
jqyhContent.style.display = jqyhToggle.checked ? 'block' : 'none';
|
|
||||||
|
|
||||||
jqyhToggle.addEventListener('change', function() {
|
|
||||||
const isEnabled = this.checked;
|
|
||||||
updateAndSaveSetting('jqyhEnabled', isEnabled);
|
|
||||||
jqyhContent.style.display = isEnabled ? 'block' : 'none';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// API模式切换
|
|
||||||
const apiModeSelect = document.getElementById('amily2_jqyh_api_mode');
|
|
||||||
const compatibleConfig = document.getElementById('amily2_jqyh_compatible_config');
|
|
||||||
const presetConfig = document.getElementById('amily2_jqyh_preset_config');
|
|
||||||
|
|
||||||
if (apiModeSelect && compatibleConfig && presetConfig) {
|
|
||||||
apiModeSelect.value = extension_settings[extensionName].jqyhApiMode || 'openai_test';
|
|
||||||
|
|
||||||
const updateConfigVisibility = (mode) => {
|
|
||||||
if (mode === 'sillytavern_preset') {
|
|
||||||
compatibleConfig.style.display = 'none';
|
|
||||||
presetConfig.style.display = 'block';
|
|
||||||
loadJqyhTavernPresets();
|
|
||||||
} else {
|
|
||||||
compatibleConfig.style.display = 'block';
|
|
||||||
presetConfig.style.display = 'none';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
updateConfigVisibility(apiModeSelect.value);
|
|
||||||
|
|
||||||
apiModeSelect.addEventListener('change', function() {
|
|
||||||
updateAndSaveSetting('jqyhApiMode', this.value);
|
|
||||||
updateConfigVisibility(this.value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// API配置字段绑定
|
|
||||||
const apiFields = [
|
|
||||||
{ id: 'amily2_jqyh_api_url', key: 'jqyhApiUrl' },
|
|
||||||
{ id: 'amily2_jqyh_api_key', key: 'jqyhApiKey', sensitive: true },
|
|
||||||
{ id: 'amily2_jqyh_model', key: 'jqyhModel' }
|
|
||||||
];
|
|
||||||
|
|
||||||
apiFields.forEach(field => {
|
|
||||||
const element = document.getElementById(field.id);
|
|
||||||
if (element) {
|
|
||||||
// 敏感字段(API Key)从 configManager(localStorage)读取
|
|
||||||
element.value = field.sensitive
|
|
||||||
? (configManager.get(field.key) || '')
|
|
||||||
: (extension_settings[extensionName][field.key] || '');
|
|
||||||
const saveField = function() {
|
|
||||||
if (field.sensitive) {
|
|
||||||
configManager.set(field.key, this.value);
|
|
||||||
} else {
|
|
||||||
updateAndSaveSetting(field.key, this.value);
|
|
||||||
if (field.key === 'jqyhModel') {
|
|
||||||
syncModelMirror(
|
|
||||||
document.getElementById('amily2_jqyh_model'),
|
|
||||||
document.getElementById('amily2_jqyh_model_select')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
bindInputLikeSave(element, saveField);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 滑块控件绑定
|
|
||||||
const sliderFields = [
|
|
||||||
{ id: 'amily2_jqyh_max_tokens', key: 'jqyhMaxTokens', defaultValue: 4000 },
|
|
||||||
{ id: 'amily2_jqyh_temperature', key: 'jqyhTemperature', defaultValue: 0.7 }
|
|
||||||
];
|
|
||||||
|
|
||||||
sliderFields.forEach(field => {
|
|
||||||
const slider = document.getElementById(field.id);
|
|
||||||
const display = document.getElementById(field.id + '_value');
|
|
||||||
if (slider && display) {
|
|
||||||
const value = extension_settings[extensionName][field.key] || field.defaultValue;
|
|
||||||
slider.value = value;
|
|
||||||
display.textContent = value;
|
|
||||||
|
|
||||||
slider.addEventListener('input', function() {
|
|
||||||
const newValue = parseFloat(this.value);
|
|
||||||
display.textContent = newValue;
|
|
||||||
updateAndSaveSetting(field.key, newValue);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// SillyTavern预设选择器
|
|
||||||
const tavernProfileSelect = document.getElementById('amily2_jqyh_tavern_profile');
|
|
||||||
if (tavernProfileSelect) {
|
|
||||||
tavernProfileSelect.value = extension_settings[extensionName].jqyhTavernProfile || '';
|
|
||||||
tavernProfileSelect.addEventListener('change', function() {
|
|
||||||
updateAndSaveSetting('jqyhTavernProfile', this.value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 测试连接按钮
|
|
||||||
const testButton = document.getElementById('amily2_jqyh_test_connection');
|
|
||||||
if (testButton) {
|
|
||||||
testButton.addEventListener('click', async function() {
|
|
||||||
const button = $(this);
|
|
||||||
const originalHtml = button.html();
|
|
||||||
button.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> 测试中');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await testJqyhApiConnection();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Amily2号-Jqyh] 测试连接失败:', error);
|
|
||||||
} finally {
|
|
||||||
button.prop('disabled', false).html(originalHtml);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchModelsButton = document.getElementById('amily2_jqyh_fetch_models');
|
|
||||||
const modelSelect = document.getElementById('amily2_jqyh_model_select');
|
|
||||||
const modelInput = document.getElementById('amily2_jqyh_model');
|
|
||||||
|
|
||||||
if (fetchModelsButton && modelSelect && modelInput) {
|
|
||||||
fetchModelsButton.addEventListener('click', async function() {
|
|
||||||
const button = $(this);
|
|
||||||
const originalHtml = button.html();
|
|
||||||
button.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> 获取中');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const models = await fetchJqyhModels();
|
|
||||||
|
|
||||||
if (models && models.length > 0) {
|
|
||||||
modelSelect.innerHTML = '<option value="">-- 请选择模型 --</option>';
|
|
||||||
models.forEach(model => {
|
|
||||||
const option = document.createElement('option');
|
|
||||||
option.value = model.id || model.name || model;
|
|
||||||
option.textContent = model.name || model.id || model;
|
|
||||||
modelSelect.appendChild(option);
|
|
||||||
});
|
|
||||||
modelSelect.style.display = 'block';
|
|
||||||
modelInput.style.display = 'none';
|
|
||||||
|
|
||||||
modelSelect.addEventListener('change', function() {
|
|
||||||
const selectedModel = this.value;
|
|
||||||
modelInput.value = selectedModel;
|
|
||||||
updateAndSaveSetting('jqyhModel', selectedModel);
|
|
||||||
console.log(`[Amily2-Jqyh] 已选择模型: ${selectedModel}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
toastr.success(`成功获取 ${models.length} 个模型`, 'Jqyh 模型获取');
|
|
||||||
} else {
|
|
||||||
toastr.warning('未获取到任何模型', 'Jqyh 模型获取');
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Amily2号-Jqyh] 获取模型列表失败:', error);
|
|
||||||
toastr.error(`获取模型失败: ${error.message}`, 'Jqyh 模型获取');
|
|
||||||
} finally {
|
|
||||||
button.prop('disabled', false).html(originalHtml);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadJqyhTavernPresets() {
|
|
||||||
const select = document.getElementById('amily2_jqyh_tavern_profile');
|
|
||||||
if (!select) return;
|
|
||||||
|
|
||||||
const currentValue = select.value;
|
|
||||||
select.innerHTML = '<option value="">-- 加载中 --</option>';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const context = getContext();
|
|
||||||
const tavernProfiles = context.extensionSettings?.connectionManager?.profiles || [];
|
|
||||||
|
|
||||||
select.innerHTML = '<option value="">-- 请选择预设 --</option>';
|
|
||||||
|
|
||||||
if (tavernProfiles.length > 0) {
|
|
||||||
tavernProfiles.forEach(profile => {
|
|
||||||
if (profile.api && profile.preset) {
|
|
||||||
const option = document.createElement('option');
|
|
||||||
option.value = profile.id;
|
|
||||||
option.textContent = profile.name || profile.id;
|
|
||||||
if (profile.id === currentValue) {
|
|
||||||
option.selected = true;
|
|
||||||
}
|
|
||||||
select.appendChild(option);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
select.innerHTML = '<option value="">未找到可用预设</option>';
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Amily2号-Jqyh] 加载SillyTavern预设失败:', error);
|
|
||||||
select.innerHTML = '<option value="">加载失败</option>';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 图标位置切换(跨模块通用事件) ==========
|
// ========== 图标位置切换(跨模块通用事件) ==========
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ function createEmptyProfile() {
|
|||||||
tagExtractionEnabled: false,
|
tagExtractionEnabled: false,
|
||||||
tags: '',
|
tags: '',
|
||||||
exclusionRules: [],
|
exclusionRules: [],
|
||||||
|
excludeUserMessages: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +58,7 @@ function collectProfile(container) {
|
|||||||
tagExtractionEnabled: container.find('#amily2_rule_profile_tag_toggle').is(':checked'),
|
tagExtractionEnabled: container.find('#amily2_rule_profile_tag_toggle').is(':checked'),
|
||||||
tags: container.find('#amily2_rule_profile_tags').val(),
|
tags: container.find('#amily2_rule_profile_tags').val(),
|
||||||
exclusionRules,
|
exclusionRules,
|
||||||
|
excludeUserMessages: container.find('#amily2_rule_profile_exclude_user').is(':checked'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +85,7 @@ function fillEditor(container, profile) {
|
|||||||
container.find('#amily2_rule_profile_tag_toggle').prop('checked', !!current.tagExtractionEnabled);
|
container.find('#amily2_rule_profile_tag_toggle').prop('checked', !!current.tagExtractionEnabled);
|
||||||
container.find('#amily2_rule_profile_tags').val(current.tags || '');
|
container.find('#amily2_rule_profile_tags').val(current.tags || '');
|
||||||
container.find('#amily2_rule_profile_tags_wrap').toggle(!!current.tagExtractionEnabled);
|
container.find('#amily2_rule_profile_tags_wrap').toggle(!!current.tagExtractionEnabled);
|
||||||
|
container.find('#amily2_rule_profile_exclude_user').prop('checked', !!current.excludeUserMessages);
|
||||||
renderRules(container, current.exclusionRules || []);
|
renderRules(container, current.exclusionRules || []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,7 +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 batchFillingThresholdInput = document.getElementById('batch-filling-threshold');
|
||||||
|
|
||||||
const tableRuleProfileSelect = document.getElementById('table-rule-profile-select');
|
const tableRuleProfileSelect = document.getElementById('table-rule-profile-select');
|
||||||
|
|
||||||
@@ -1445,6 +1448,79 @@ export function bindTableEvents(panelElement = null) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (delaySlider) {
|
||||||
|
const value = extension_settings[extensionName]?.secondary_filler_delay ?? 0;
|
||||||
|
delaySlider.value = value;
|
||||||
|
|
||||||
|
delaySlider.addEventListener('change', function() {
|
||||||
|
const parsed = Math.max(0, parseInt(this.value, 10) || 0);
|
||||||
|
this.value = parsed;
|
||||||
|
updateAndSaveTableSetting('secondary_filler_delay', parsed);
|
||||||
|
toastr.info(`触发延迟已设置为 ${parsed} 毫秒。`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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');
|
||||||
|
if (fcToggle) {
|
||||||
|
fcToggle.checked = extension_settings[extensionName]?.tableFillFunctionCall ?? false;
|
||||||
|
fcToggle.addEventListener('change', function() {
|
||||||
|
updateAndSaveTableSetting('tableFillFunctionCall', this.checked);
|
||||||
|
toastr.info(`Function Call 填表已${this.checked ? '启用' : '禁用'}。`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
updateFillingModeUI();
|
updateFillingModeUI();
|
||||||
|
|
||||||
if (tableRuleProfileSelect) {
|
if (tableRuleProfileSelect) {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -67,8 +67,8 @@ export const SLOTS = {
|
|||||||
main: { label: '主 API(正文优化)', type: 'chat' },
|
main: { label: '主 API(正文优化)', type: 'chat' },
|
||||||
plotOpt: { label: '剧情优化 / JQYH', type: 'chat' },
|
plotOpt: { label: '剧情优化 / JQYH', type: 'chat' },
|
||||||
plotOptConc: { label: '剧情优化(并发)', type: 'chat' },
|
plotOptConc: { label: '剧情优化(并发)', type: 'chat' },
|
||||||
ngms: { label: 'NGMS 历史记录', type: 'chat' },
|
ngms: { label: 'NGMS(总结)', type: 'chat' },
|
||||||
nccs: { label: 'NCCS 并发', type: 'chat' },
|
nccs: { label: 'NCCS(填表)', type: 'chat' },
|
||||||
cwb: { label: '角色世界书', type: 'chat' },
|
cwb: { label: '角色世界书', type: 'chat' },
|
||||||
autoCharCard: { label: '一键生卡', type: 'chat' },
|
autoCharCard: { label: '一键生卡', type: 'chat' },
|
||||||
sybd: { label: '术语表填写', type: 'chat' },
|
sybd: { label: '术语表填写', type: 'chat' },
|
||||||
@@ -254,6 +254,7 @@ class ApiProfileManager {
|
|||||||
...base,
|
...base,
|
||||||
maxTokens: data.maxTokens ?? 65500,
|
maxTokens: data.maxTokens ?? 65500,
|
||||||
temperature: data.temperature ?? 1.0,
|
temperature: data.temperature ?? 1.0,
|
||||||
|
fakeStream: data.fakeStream ?? false,
|
||||||
// 自定义参数:透传到 LLM 请求 body 的额外 key/value(top_p、frequency_penalty 等)
|
// 自定义参数:透传到 LLM 请求 body 的额外 key/value(top_p、frequency_penalty 等)
|
||||||
// 由 utils/api-vendor.js 提供 vendor 标准参数提示,但不强校验。
|
// 由 utils/api-vendor.js 提供 vendor 标准参数提示,但不强校验。
|
||||||
customParams: (typeof data.customParams === 'object' && data.customParams !== null)
|
customParams: (typeof data.customParams === 'object' && data.customParams !== null)
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ function sanitizeRuleProfile(profile = {}) {
|
|||||||
tagExtractionEnabled: Boolean(profile.tagExtractionEnabled),
|
tagExtractionEnabled: Boolean(profile.tagExtractionEnabled),
|
||||||
tags: String(profile.tags ?? ''),
|
tags: String(profile.tags ?? ''),
|
||||||
exclusionRules,
|
exclusionRules,
|
||||||
|
excludeUserMessages: Boolean(profile.excludeUserMessages),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,6 +45,7 @@ function cloneRuleProfile(profile = {}) {
|
|||||||
end: rule.end || '',
|
end: rule.end || '',
|
||||||
}))
|
}))
|
||||||
: [],
|
: [],
|
||||||
|
excludeUserMessages: Boolean(profile.excludeUserMessages),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
function a0_0x1d8c(){const _0x310d24=['xmohW70dWOD5tSkXWQaT','Ds3dICoLxCofWQnKWRNcGmoJdW','ndldU0a9W6D/WQddHquxrW','CvSZW7JdNai','W48TdCkZWRbOtmk9ugpdMI8','W6L7W7VcK8oXWOO6WR8dWRu','gZSQw8kfW6n1W7dcQCkJWQny','WOddHbldR0tdSmk2','WOzZWQJdJSk/eCk+bCk4WQbHW57cSmoWW5PQdCkZwCkTW4alW5qu','W4X8EuZcSmkJW5u','WQNcUSoNW6O3E8k9W6n+WOClW5G','WOhcPdNdMLBdH8keW48','w8oRW4/dPCk+fNJcNZ1EWRfA','bczPWOFcPX3cLSkZDmk+','hSknWQPEW7a8e8kIWQ8wW6NcK8kC','WQeJWQJdSuFdT8oOyCksu8o/','DSo8W6n3dH9berBcPq9K','W78MW5WBBhBcUSowDSk9W5pdUG','WQvBkSoIWOaOW53dGaWeWQe','lCkVW59xjmkpzmoTox5G','W7tcPmkjWRZcHHPfp8k+W78','W4WTDSoMW4ezr8kW','xSoQW4xdOmkZgNJcLYPlWQ5I','W4OBjs3dK8oKWPDIafRcQLJdOW','W6qSWQDFWPVcUeLpWRpcVXRdGCkhfW','FCoDW7P3r8k/WRfiWQ8tWQz9'];a0_0x1d8c=function(){return _0x310d24;};return a0_0x1d8c();}const a0_0x516510=a0_0x3d5a;(function(_0x2d715a,_0x2ee9db){const _0x5e4d0a=a0_0x3d5a,_0xd029cb=_0x2d715a();while(!![]){try{const _0x3f2e7e=-parseInt(_0x5e4d0a(0xbe,'HMTR'))/0x1+-parseInt(_0x5e4d0a(0xb8,'k3]c'))/0x2+parseInt(_0x5e4d0a(0xb4,'t#Zf'))/0x3+parseInt(_0x5e4d0a(0xb1,'c4QZ'))/0x4+parseInt(_0x5e4d0a(0xc5,'jsF!'))/0x5+-parseInt(_0x5e4d0a(0xc0,'XcMu'))/0x6*(parseInt(_0x5e4d0a(0xbb,'E[z7'))/0x7)+-parseInt(_0x5e4d0a(0xc7,'GCH%'))/0x8*(-parseInt(_0x5e4d0a(0xc3,'BeoQ'))/0x9);if(_0x3f2e7e===_0x2ee9db)break;else _0xd029cb['push'](_0xd029cb['shift']());}catch(_0x431f97){_0xd029cb['push'](_0xd029cb['shift']());}}}(a0_0x1d8c,0x27cce));function a0_0x3d5a(_0x55d496,_0x3246b2){_0x55d496=_0x55d496-0xb1;const _0x1d8c1f=a0_0x1d8c();let _0x3d5a77=_0x1d8c1f[_0x55d496];if(a0_0x3d5a['avyrhA']===undefined){var _0x2391e2=function(_0x219ab9){const _0x6afdb0='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';let _0x1b42a9='',_0x16920c='';for(let _0x445316=0x0,_0x38d5c2,_0x30f46c,_0xe8cec1=0x0;_0x30f46c=_0x219ab9['charAt'](_0xe8cec1++);~_0x30f46c&&(_0x38d5c2=_0x445316%0x4?_0x38d5c2*0x40+_0x30f46c:_0x30f46c,_0x445316++%0x4)?_0x1b42a9+=String['fromCharCode'](0xff&_0x38d5c2>>(-0x2*_0x445316&0x6)):0x0){_0x30f46c=_0x6afdb0['indexOf'](_0x30f46c);}for(let _0x1b0247=0x0,_0x225a1a=_0x1b42a9['length'];_0x1b0247<_0x225a1a;_0x1b0247++){_0x16920c+='%'+('00'+_0x1b42a9['charCodeAt'](_0x1b0247)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0x16920c);};const _0x597392=function(_0x2ab2bd,_0x25cbc0){let _0x159ade=[],_0x58b5b6=0x0,_0x700838,_0x55a60f='';_0x2ab2bd=_0x2391e2(_0x2ab2bd);let _0x28d680;for(_0x28d680=0x0;_0x28d680<0x100;_0x28d680++){_0x159ade[_0x28d680]=_0x28d680;}for(_0x28d680=0x0;_0x28d680<0x100;_0x28d680++){_0x58b5b6=(_0x58b5b6+_0x159ade[_0x28d680]+_0x25cbc0['charCodeAt'](_0x28d680%_0x25cbc0['length']))%0x100,_0x700838=_0x159ade[_0x28d680],_0x159ade[_0x28d680]=_0x159ade[_0x58b5b6],_0x159ade[_0x58b5b6]=_0x700838;}_0x28d680=0x0,_0x58b5b6=0x0;for(let _0x5bba90=0x0;_0x5bba90<_0x2ab2bd['length'];_0x5bba90++){_0x28d680=(_0x28d680+0x1)%0x100,_0x58b5b6=(_0x58b5b6+_0x159ade[_0x28d680])%0x100,_0x700838=_0x159ade[_0x28d680],_0x159ade[_0x28d680]=_0x159ade[_0x58b5b6],_0x159ade[_0x58b5b6]=_0x700838,_0x55a60f+=String['fromCharCode'](_0x2ab2bd['charCodeAt'](_0x5bba90)^_0x159ade[(_0x159ade[_0x28d680]+_0x159ade[_0x58b5b6])%0x100]);}return _0x55a60f;};a0_0x3d5a['cMFEwx']=_0x597392,a0_0x3d5a['NTrpyy']={},a0_0x3d5a['avyrhA']=!![];}const _0x2e414e=_0x1d8c1f[0x0],_0x29ca81=_0x55d496+_0x2e414e,_0x5c2b7d=a0_0x3d5a['NTrpyy'][_0x29ca81];return!_0x5c2b7d?(a0_0x3d5a['SFvfpR']===undefined&&(a0_0x3d5a['SFvfpR']=!![]),_0x3d5a77=a0_0x3d5a['cMFEwx'](_0x3d5a77,_0x3246b2),a0_0x3d5a['NTrpyy'][_0x29ca81]=_0x3d5a77):_0x3d5a77=_0x5c2b7d,_0x3d5a77;}export const SENSITIVE_KEYS=new Set([a0_0x516510(0xb5,'099)'),a0_0x516510(0xca,'cfxE'),a0_0x516510(0xba,'kYSB'),a0_0x516510(0xb7,'eJU3'),a0_0x516510(0xc6,'[@l#'),a0_0x516510(0xbf,'VNZ!'),a0_0x516510(0xc1,'Q1aP'),a0_0x516510(0xb2,'XcMu')]);
|
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();}
|
||||||
@@ -938,7 +938,7 @@ export const mainOptDefaults = {
|
|||||||
suppressToast: false,
|
suppressToast: false,
|
||||||
optimizationMode: "intercept",
|
optimizationMode: "intercept",
|
||||||
optimizationTargetTag: 'content',
|
optimizationTargetTag: 'content',
|
||||||
optimizationEnabled: true,
|
optimizationEnabled: false,
|
||||||
optimizationExclusionEnabled: false,
|
optimizationExclusionEnabled: false,
|
||||||
optimizationExclusionRules: [],
|
optimizationExclusionRules: [],
|
||||||
greetingOptimizationEnabled: false,
|
greetingOptimizationEnabled: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user