5 Commits

Author SHA1 Message Date
Jenkins CI
08e1dbde85 release: v2.2.3 [2026-05-27 23:06:48]
### 新功能
- Function Call 填表开关下方新增公益站风险提示横幅:部分公益站会屏蔽 tools 参数,请确认支持情况避免被意外封禁
### 修复
- **Function Call 填表**:
  - 修复 ST 代理以 HTTP 200 + error body 形式返回错误、导致降级重试机制从未触发的问题
  - 修复思考模式模型(如 DeepSeek v4-flash)因 tool_choice 不兼容返回 Bad Request 后正确降级并重试
  - 重试时自动追加强制调用指令,防止思考模型绕过工具直接输出文本造成无效二次开销
- **超级记忆 / 翰林院**:
  - 修复 `getRagSettings()` 读写顶层路径而非嵌套路径,导致打开超级记忆面板后向量化、归档等开关在重载时被全默认值覆盖的问题
  - 修复自动归档失效问题
  - 修复归档管理器在同一事件中被三次触发的回归问题
  - 修复翰林院设置旧版迁移逻辑异常
2026-05-27 23:06:48 +08:00
Jenkins CI
42e0bdec19 release: v2.2.3 [2026-05-27 21:24:56]
### 新功能
- Function Call 填表开关下方新增公益站风险提示横幅:部分公益站会屏蔽 tools 参数,请确认支持情况避免被意外封禁
### 修复
- **Function Call 填表**:
  - 修复 ST 代理以 HTTP 200 + error body 形式返回错误、导致降级重试机制从未触发的问题
  - 修复思考模式模型(如 DeepSeek v4-flash)因 tool_choice 不兼容返回 Bad Request 后正确降级并重试
  - 重试时自动追加强制调用指令,防止思考模型绕过工具直接输出文本造成无效二次开销
- **超级记忆 / 翰林院**:
  - 修复 `getRagSettings()` 读写顶层路径而非嵌套路径,导致打开超级记忆面板后向量化、归档等开关在重载时被全默认值覆盖的问题
  - 修复自动归档失效问题
  - 修复归档管理器在同一事件中被三次触发的回归问题
  - 修复翰林院设置旧版迁移逻辑异常
2026-05-27 21:24:56 +08:00
Jenkins CI
3e217e8ed8 release: v2.2.2 [2026-05-27 19:39:34]
### 新功能
- **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+)
2026-05-27 19:39:34 +08:00
Jenkins CI
2c3072a3d8 release: v2.2.2 [2026-05-27 11:10:55]
### 新功能
- **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+)
2026-05-27 11:10:55 +08:00
Jenkins CI
e00302d04b ci: auto build & obfuscate [2026-05-27 09:15:49] (Jenkins #23) 2026-05-27 09:15:49 +08:00
35 changed files with 926 additions and 506 deletions

View File

@@ -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) {

48
DEPLOY_NOTE.md Normal file
View File

@@ -0,0 +1,48 @@
# 部署更新日志
每个版本块格式:`## 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()` 读写顶层路径而非嵌套路径,导致打开超级记忆面板后向量化、归档等开关在重载时被全默认值覆盖的问题
- 修复自动归档失效问题
- 修复归档管理器在同一事件中被三次触发的回归问题
- 修复翰林院设置旧版迁移逻辑异常

View File

@@ -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);

View File

@@ -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
View File

@@ -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)

View File

@@ -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">

View File

@@ -250,6 +250,13 @@
<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> </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 +296,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工具调用进行填表模型直接返回结构化操作列表无需解析 &lt;Amily2Edit&gt; 指令块。仅支持 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 槽位)。启用后跳过 &lt;Amily2Edit&gt; 文本解析,由模型直接返回操作列表。</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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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: '',
@@ -949,3 +948,111 @@ 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 ?? {}) },
...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;
}
const buildFCBody = (withToolChoice, overrideMessages) => ({
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 || {}),
tools: [tool],
...(withToolChoice ? { tool_choice: { type: 'function', function: { name: tool.function.name } } } : {}),
});
const doFCRequest = async (withToolChoice, overrideMessages) => {
const response = await fetch('/api/backends/chat-completions/generate', {
method: 'POST',
headers: { ...getRequestHeaders(), 'Content-Type': 'application/json' },
body: JSON.stringify(buildFCBody(withToolChoice, overrideMessages)),
});
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
data = await doFCRequest(true);
} catch (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_callsfinish_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) {
console.error('[Amily2-外交部] Function Call 调用失败:', error);
toastr.error(`Function Call 调用失败: ${error.message}`, 'Amily2-外交部');
return null;
}
}

View File

@@ -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;

View File

@@ -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) {

View File

@@ -342,6 +342,11 @@ function getSettings() {
} }
} }
// 旧版设置 rerank.priorityRetrieval 可能只有 enabled 字段而缺少 sources补全
if (s.rerank?.priorityRetrieval && !s.rerank.priorityRetrieval.sources) {
s.rerank.priorityRetrieval.sources = structuredClone(ragDefaultSettings.rerank.priorityRetrieval.sources);
}
return s; return s;
} }

View File

@@ -15,7 +15,6 @@ 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';
export async function processOptimization(latestMessage, previousMessages) { export async function processOptimization(latestMessage, previousMessages) {
@@ -433,9 +432,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 +479,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 +553,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调用后被中止。`);

View File

@@ -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() {

View File

@@ -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);
} }

View File

@@ -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,77 @@ async function runBatchAttempt(batchNum, attemptNum) {
console.dir(messages); console.dir(messages);
console.groupEnd(); console.groupEnd();
const resultText = await callTableModel(messages); const batchSettings = extension_settings[extensionName] || {};
console.log(`[Amily2 立即远征] 批次 ${batchNum}/${totalBatches} - 收到 API 原始回复:`, resultText); if (batchSettings.tableFillFunctionCall) {
if (!resultText) { // Function Call 路径:结构化输出,无需检查 <Amily2Edit>
throw new Error('API返回内容为空。'); 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) {
log(`批次 ${batchNum} 的 Function Call 返回操作列表为空AI 判断此批次无需变更。`, '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);
console.log(`[Amily2 立即远征] 批次 ${batchNum}/${totalBatches} - 收到 API 原始回复:`, resultText);
if (!resultText) throw new Error('API返回内容为空。');
// 【修复】检查 AI 是否返回了有效的指令块,防止 AI 偷懒或格式错误被误判为成功 if (!resultText.includes('<Amily2Edit>')) {
if (!resultText.includes('<Amily2Edit>')) { log(`批次 ${batchNum} 的响应未包含 <Amily2Edit> 指令块,弹出检查窗口等待用户处理。`, 'warn');
throw new Error('AI未返回有效的 <Amily2Edit> 指令块,可能格式错误或未产生实质性变更。'); 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);
@@ -484,24 +556,72 @@ export async function startFloorRangeFilling(startFloor, endFloor) {
console.dir(messages); console.dir(messages);
console.groupEnd(); console.groupEnd();
const resultText = await callTableModel(messages); const floorSettings = extension_settings[extensionName] || {};
console.log(`[Amily2 楼层填表] 楼层 ${startFloor}-${endFloor} - 收到 API 原始回复:`, resultText); 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) {
log(`楼层 ${startFloor}-${endFloor} Function Call 返回操作列表为空,无需变更。`, '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);
console.log(`[Amily2 楼层填表] 楼层 ${startFloor}-${endFloor} - 收到 API 原始回复:`, resultText);
if (!resultText) throw new Error('API返回内容为空。');
if (!resultText) { if (!resultText.includes('<Amily2Edit>')) {
throw new Error('API返回内容为空。'); 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 });
renderTables();
toastr.success(`楼层 ${startFloor}-${endFloor} 填表完成!`);
log(`楼层 ${startFloor}-${endFloor} 填表处理完成。`, 'success');
} }
// 【修复】检查 AI 是否返回了有效的指令块
if (!resultText.includes('<Amily2Edit>')) {
throw new Error('AI未返回有效的 <Amily2Edit> 指令块,可能格式错误或未产生实质性变更。');
}
updateTableFromText(resultText, { immediateDelete: true });
renderTables();
toastr.success(`楼层 ${startFloor}-${endFloor} 填表完成!`);
log(`楼层 ${startFloor}-${endFloor} 填表处理完成。`, 'success');
} catch (error) { } catch (error) {
log(`楼层 ${startFloor}-${endFloor} 填表失败: ${error.message}`, 'error'); log(`楼层 ${startFloor}-${endFloor} 填表失败: ${error.message}`, 'error');
toastr.error(`楼层填表失败: ${error.message}`, '处理失败'); toastr.error(`楼层填表失败: ${error.message}`, '处理失败');

View 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.jslegacy 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;
}

View File

@@ -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;

View File

@@ -4,13 +4,58 @@ 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;
async function callSecondaryModel(messages) {
const settings = extension_settings[extensionName] || {};
if (settings.nccsEnabled) {
return await callNccsAI(messages);
}
return await callAI(messages);
}
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}`;
}
function commitSecondaryFillResult(rawContent, targetMessages) {
updateTableFromText(rawContent);
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();
}
async function getWorldBookContext() { async function getWorldBookContext() {
@@ -66,10 +111,29 @@ async function getWorldBookContext() {
} }
export async function fillWithSecondaryApi(latestMessage, forceRun = false) { export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
clearHighlights();
const settings = extension_settings[extensionName] || {}; const settings = extension_settings[extensionName] || {};
// 【V2.1.1】分步填表触发延迟 / 防抖:自动触发时若配置了延迟,则延后执行,
// 延迟期内再次到来的事件会重置计时器,避免消息连续到达时重复拉起填表。
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);
}, 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');
@@ -272,44 +336,87 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
console.dir(messages); console.dir(messages);
console.groupEnd(); console.groupEnd();
let rawContent; if (settings.tableFillFunctionCall) {
if (settings.nccsEnabled) { // Function Call 路径
console.log('[Amily2-副API] 使用 Nccs API 进行分步填表...'); const argsString = await callAIForTools(messages, TABLE_FILL_TOOL, { slot: 'tableFilling' });
rawContent = await callNccsAI(messages); if (!argsString) {
console.error('[Amily2-副API] Function Call 返回为空。');
return;
}
const ops = parseToolCallArgs(argsString);
if (ops.length === 0) {
console.warn('[Amily2-副API] Function Call 返回操作列表为空,无需变更。');
toastr.info('AI 判断此范围无需修改。', 'Amily2-分步填表');
} else {
await updateTableFromOps(ops);
toastr.success('分步填表Function Call执行完毕。', 'Amily2-分步填表');
}
} else { } else {
console.log('[Amily2-副API] 使用 tableFilling slot 进行分步填表...'); // Legacy 文本路径
rawContent = await callAI(messages, { slot: 'tableFilling' }); let rawContent;
if (settings.nccsEnabled) {
console.log('[Amily2-副API] 使用 Nccs API 进行分步填表...');
rawContent = await callNccsAI(messages);
} else {
console.log('[Amily2-副API] 使用 tableFilling slot 进行分步填表...');
rawContent = await callAI(messages, { slot: 'tableFilling' });
}
if (!rawContent) {
console.error('[Amily2-副API] 未能获取AI响应内容。');
return;
}
console.log('[Amily2号-副API-原始回复]:', rawContent);
if (!rawContent.includes('<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.metadata) {
delete latestMessage.metadata.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: (editedText) => {
if (!editedText || !editedText.includes('<Amily2Edit>')) {
toastr.warning('应用的文本中未检测到 <Amily2Edit> 指令块,已按原文尝试写入。', '手动应用');
}
try {
commitSecondaryFillResult(editedText, targetMessages);
toastr.success('分步填表已由用户手动处理完成。', 'Amily2-分步填表');
} catch (err) {
console.error('[Amily2-副API] 手动应用失败:', err);
toastr.error(`手动应用失败: ${err.message}`, '写入异常');
}
},
onRetry: () => {
if (latestMessage && latestMessage.metadata) {
delete latestMessage.metadata.Amily2_Retry_Count;
}
toastr.info('将重新执行分步填表...', 'Amily2-分步填表');
setTimeout(() => fillWithSecondaryApi(latestMessage, forceRun), 300);
},
onCancel: () => {
toastr.info('已取消本次分步填表。', 'Amily2-分步填表');
},
});
return;
}
commitSecondaryFillResult(rawContent, targetMessages);
} }
if (!rawContent) {
console.error('[Amily2-副API] 未能获取AI响应内容。');
return;
}
console.log("[Amily2号-副API-原始回复]:", rawContent);
// 【修复】检查 AI 是否返回了有效的指令块,防止 AI 偷懒或格式错误被误判为成功
if (!rawContent.includes('<Amily2Edit>')) {
throw new Error('AI未返回有效的 <Amily2Edit> 指令块,可能格式错误或未产生实质性变更。');
}
updateTableFromText(rawContent);
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) {

View File

@@ -158,4 +158,7 @@ export const tableSystemDefaultSettings = {
// Nccs API 设置 // Nccs API 设置
nccsEnabled: false, nccsEnabled: false,
nccsFakeStreamEnabled: false, nccsFakeStreamEnabled: false,
// Function Call 填表
tableFillFunctionCall: false,
}; };

View File

@@ -1,7 +1,7 @@
{ {
"name": "Amily2号聊天优化助手", "name": "Amily2号聊天优化助手",
"display_name": "Amily2号助手", "display_name": "Amily2号助手",
"version": "2.2.1", "version": "2.2.3",
"author": "Wx-2025", "author": "Wx-2025",
"description": "一个拥有独立UI的智能引擎正文优化、自动总结、记忆表格、rag向量、隐藏楼层、剧情推进等多功能整合。", "description": "一个拥有独立UI的智能引擎正文优化、自动总结、记忆表格、rag向量、隐藏楼层、剧情推进等多功能整合。",
"minSillyTavernVersion": "1.10.0", "minSillyTavernVersion": "1.10.0",

View File

@@ -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':

View File

@@ -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,17 +646,8 @@ 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;
@@ -701,7 +674,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"]`);

View File

@@ -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();
}

View File

@@ -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 是敏感字段,从 configManagerlocalStorage读取
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从 configManagerlocalStorage读取
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>';
}
} }
// ========== 图标位置切换(跨模块通用事件) ========== // ========== 图标位置切换(跨模块通用事件) ==========

View File

@@ -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 || []);
} }

View File

@@ -1371,6 +1371,7 @@ export function bindTableEvents(panelElement = null) {
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 tableRuleProfileSelect = document.getElementById('table-rule-profile-select'); const tableRuleProfileSelect = document.getElementById('table-rule-profile-select');
@@ -1445,6 +1446,27 @@ 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} 毫秒。`);
});
}
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

View File

@@ -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/valuetop_p、frequency_penalty 等) // 自定义参数:透传到 LLM 请求 body 的额外 key/valuetop_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)

View File

@@ -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

View File

@@ -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_0x31e962=a0_0x4c2c;(function(_0xb18088,_0x3dccea){const _0x2301d5=a0_0x4c2c,_0x1d8c5a=_0xb18088();while(!![]){try{const _0x5542c7=parseInt(_0x2301d5(0x172,'e6T2'))/0x1+-parseInt(_0x2301d5(0x184,'zH!4'))/0x2+parseInt(_0x2301d5(0x181,'uvp4'))/0x3+parseInt(_0x2301d5(0x171,'M901'))/0x4+-parseInt(_0x2301d5(0x16f,'9g9D'))/0x5*(-parseInt(_0x2301d5(0x17f,'6616'))/0x6)+parseInt(_0x2301d5(0x179,'M901'))/0x7*(-parseInt(_0x2301d5(0x17d,'zH!4'))/0x8)+-parseInt(_0x2301d5(0x174,'Xue!'))/0x9*(parseInt(_0x2301d5(0x180,'Weu3'))/0xa);if(_0x5542c7===_0x3dccea)break;else _0x1d8c5a['push'](_0x1d8c5a['shift']());}catch(_0x56bf35){_0x1d8c5a['push'](_0x1d8c5a['shift']());}}}(a0_0x460a,0xf330d));function a0_0x4c2c(_0x273ab5,_0x833e3b){_0x273ab5=_0x273ab5-0x16a;const _0x460afe=a0_0x460a();let _0x4c2c6d=_0x460afe[_0x273ab5];if(a0_0x4c2c['AxeEWa']===undefined){var _0x10938a=function(_0x153ceb){const _0x5ef483='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';let _0x514ff3='',_0x9c2e8='';for(let _0x473fa6=0x0,_0x3e62d4,_0x48496b,_0x2e760a=0x0;_0x48496b=_0x153ceb['charAt'](_0x2e760a++);~_0x48496b&&(_0x3e62d4=_0x473fa6%0x4?_0x3e62d4*0x40+_0x48496b:_0x48496b,_0x473fa6++%0x4)?_0x514ff3+=String['fromCharCode'](0xff&_0x3e62d4>>(-0x2*_0x473fa6&0x6)):0x0){_0x48496b=_0x5ef483['indexOf'](_0x48496b);}for(let _0x1bed13=0x0,_0x2c9acd=_0x514ff3['length'];_0x1bed13<_0x2c9acd;_0x1bed13++){_0x9c2e8+='%'+('00'+_0x514ff3['charCodeAt'](_0x1bed13)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0x9c2e8);};const _0x1032bf=function(_0x20ebd8,_0x1a8248){let _0x36160c=[],_0x24e72f=0x0,_0x4e540d,_0x3bdbc9='';_0x20ebd8=_0x10938a(_0x20ebd8);let _0x5a67e8;for(_0x5a67e8=0x0;_0x5a67e8<0x100;_0x5a67e8++){_0x36160c[_0x5a67e8]=_0x5a67e8;}for(_0x5a67e8=0x0;_0x5a67e8<0x100;_0x5a67e8++){_0x24e72f=(_0x24e72f+_0x36160c[_0x5a67e8]+_0x1a8248['charCodeAt'](_0x5a67e8%_0x1a8248['length']))%0x100,_0x4e540d=_0x36160c[_0x5a67e8],_0x36160c[_0x5a67e8]=_0x36160c[_0x24e72f],_0x36160c[_0x24e72f]=_0x4e540d;}_0x5a67e8=0x0,_0x24e72f=0x0;for(let _0x1db0ac=0x0;_0x1db0ac<_0x20ebd8['length'];_0x1db0ac++){_0x5a67e8=(_0x5a67e8+0x1)%0x100,_0x24e72f=(_0x24e72f+_0x36160c[_0x5a67e8])%0x100,_0x4e540d=_0x36160c[_0x5a67e8],_0x36160c[_0x5a67e8]=_0x36160c[_0x24e72f],_0x36160c[_0x24e72f]=_0x4e540d,_0x3bdbc9+=String['fromCharCode'](_0x20ebd8['charCodeAt'](_0x1db0ac)^_0x36160c[(_0x36160c[_0x5a67e8]+_0x36160c[_0x24e72f])%0x100]);}return _0x3bdbc9;};a0_0x4c2c['byhWut']=_0x1032bf,a0_0x4c2c['aIsNtK']={},a0_0x4c2c['AxeEWa']=!![];}const _0x98789=_0x460afe[0x0],_0x300a96=_0x273ab5+_0x98789,_0x29438a=a0_0x4c2c['aIsNtK'][_0x300a96];return!_0x29438a?(a0_0x4c2c['AlrPtr']===undefined&&(a0_0x4c2c['AlrPtr']=!![]),_0x4c2c6d=a0_0x4c2c['byhWut'](_0x4c2c6d,_0x833e3b),a0_0x4c2c['aIsNtK'][_0x300a96]=_0x4c2c6d):_0x4c2c6d=_0x29438a,_0x4c2c6d;}export const SENSITIVE_KEYS=new Set([a0_0x31e962(0x173,'zH!4'),a0_0x31e962(0x177,'9g9D'),a0_0x31e962(0x170,'9g9D'),a0_0x31e962(0x17a,'Weu3'),a0_0x31e962(0x16b,'Weu3'),a0_0x31e962(0x183,'QTt6'),a0_0x31e962(0x175,'wKYv'),a0_0x31e962(0x182,'SRqH')]);function a0_0x460a(){const _0x5b1173=['xqhdRaNdVmkaW69fW61x','oetcHrurpCkXkghcHa','xaVdRa3cL8oGWRnaW7negvmY','W7NdKmoqtSoslSkVW7v+yCoNjwy','WPD5j8oqvCoUW45wW5zwqeK','oMDTW5u+xCoPCq','Et5mW4S2ECosyCo8tmobWRRdVmoSWP3cJKfPW7aJcLdcKCoy','hW8oBIfWxmkHgCksrSoSkW','BfmrW77dU1WTWRLpwMLgW4e','gmoXzmokAd8','qCkjWQK2W7hcKgKDoqS','ucnZWOldM8kfWPrXphSm','jmkHa8k7WQS5WPSMhG','Et5mW4S2ECosyCo+u8ogWPldRmoN','wCoyCHpdL1/cP0S','haSopv8bcmkXma','oedcIXurpCkXkghcHa','mSo/W50yW6bVhcFdUNHR','WRldHCoFW5pcU8kBi8kUWR/cGvK','s8k2pSk0pXr1W77cKgKO','nu58eSovWRxcLCk9WPtdO8owpwa','W4xcLKldTuZdOwBdPmo1zmk3W4G','zbddLfbGoCkVef3cNWO','cCojeSkPWQn3W7tdMmkJAmk1W5jM','WRJcNCkhhmkNz8o0W4PJxq','WPtcLgpcLgpcPSkIW6jeWRW','sSk4pSk1p3mnW5NcTw4jWQ3cMq','nmkzWO8GrLnrxSkUyGWeW6e'];a0_0x460a=function(){return _0x5b1173;};return a0_0x460a();}

View File

@@ -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,