mirror of
https://github.com/Wx-2025/ST-Amily2-Chat-Optimisation.git
synced 2026-06-06 08:05:49 +00:00
### 新功能 - **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+)
307 lines
11 KiB
JavaScript
307 lines
11 KiB
JavaScript
import { extension_settings } from "/scripts/extensions.js";
|
||
import { saveSettingsDebounced } from "/script.js";
|
||
import { extensionName } from "../settings.js";
|
||
|
||
const RULE_PROFILE_KEY = 'ruleProfiles';
|
||
const RULE_ASSIGNMENTS_KEY = 'ruleProfileAssignments';
|
||
|
||
// ── 功能槽定义 ──────────────────────────────────────────────────────────────────
|
||
export const RULE_SLOTS = {
|
||
table: { label: '表格提取规则' },
|
||
historiography: { label: '史官/总结提取规则' },
|
||
condensation: { label: '翰林院·浓缩规则' },
|
||
queryPreprocessing:{ label: '翰林院·查询预处理规则' },
|
||
};
|
||
|
||
function sanitizeRuleProfile(profile = {}) {
|
||
const exclusionRules = Array.isArray(profile.exclusionRules)
|
||
? profile.exclusionRules
|
||
.map(rule => ({
|
||
start: String(rule?.start ?? '').trim(),
|
||
end: String(rule?.end ?? '').trim(),
|
||
}))
|
||
.filter(rule => rule.start)
|
||
: [];
|
||
|
||
return {
|
||
id: String(profile.id ?? '').trim(),
|
||
name: String(profile.name ?? '').trim(),
|
||
tagExtractionEnabled: Boolean(profile.tagExtractionEnabled),
|
||
tags: String(profile.tags ?? ''),
|
||
exclusionRules,
|
||
excludeUserMessages: Boolean(profile.excludeUserMessages),
|
||
};
|
||
}
|
||
|
||
function cloneRuleProfile(profile = {}) {
|
||
return {
|
||
id: profile.id || '',
|
||
name: profile.name || '',
|
||
tagExtractionEnabled: Boolean(profile.tagExtractionEnabled),
|
||
tags: profile.tags || '',
|
||
exclusionRules: Array.isArray(profile.exclusionRules)
|
||
? profile.exclusionRules.map(rule => ({
|
||
start: rule.start || '',
|
||
end: rule.end || '',
|
||
}))
|
||
: [],
|
||
excludeUserMessages: Boolean(profile.excludeUserMessages),
|
||
};
|
||
}
|
||
|
||
function createRuleProfileId(name = 'rule-profile') {
|
||
const base = String(name || 'rule-profile')
|
||
.toLowerCase()
|
||
.replace(/[^a-z0-9]+/g, '-')
|
||
.replace(/^-+|-+$/g, '') || 'rule-profile';
|
||
return `${base}-${Date.now().toString(36)}`;
|
||
}
|
||
|
||
function ensureSettingsRoot() {
|
||
if (!extension_settings[extensionName]) {
|
||
extension_settings[extensionName] = {};
|
||
}
|
||
return extension_settings[extensionName];
|
||
}
|
||
|
||
function ensureProfileMap() {
|
||
const settings = ensureSettingsRoot();
|
||
if (!settings[RULE_PROFILE_KEY] || typeof settings[RULE_PROFILE_KEY] !== 'object' || Array.isArray(settings[RULE_PROFILE_KEY])) {
|
||
settings[RULE_PROFILE_KEY] = {};
|
||
}
|
||
return settings[RULE_PROFILE_KEY];
|
||
}
|
||
|
||
function ensureAssignments() {
|
||
const settings = ensureSettingsRoot();
|
||
if (!settings[RULE_ASSIGNMENTS_KEY] || typeof settings[RULE_ASSIGNMENTS_KEY] !== 'object' || Array.isArray(settings[RULE_ASSIGNMENTS_KEY])) {
|
||
settings[RULE_ASSIGNMENTS_KEY] = {};
|
||
}
|
||
return settings[RULE_ASSIGNMENTS_KEY];
|
||
}
|
||
|
||
function mergeRuleConfig(profile, fallback = {}) {
|
||
const safeFallback = sanitizeRuleProfile({
|
||
id: fallback.id,
|
||
name: fallback.name,
|
||
tagExtractionEnabled: fallback.tagExtractionEnabled,
|
||
tags: fallback.tags,
|
||
exclusionRules: fallback.exclusionRules,
|
||
});
|
||
|
||
if (!profile) {
|
||
return safeFallback;
|
||
}
|
||
|
||
return {
|
||
id: profile.id,
|
||
name: profile.name,
|
||
tagExtractionEnabled: profile.tagExtractionEnabled,
|
||
tags: profile.tags,
|
||
exclusionRules: cloneRuleProfile(profile).exclusionRules.length > 0
|
||
? cloneRuleProfile(profile).exclusionRules
|
||
: safeFallback.exclusionRules,
|
||
};
|
||
}
|
||
|
||
function _dispatchChange() {
|
||
const profiles = Object.values(ensureProfileMap())
|
||
.map(p => cloneRuleProfile(p))
|
||
.sort((a, b) => (a.name || a.id).localeCompare(b.name || b.id, 'zh-Hans-CN'));
|
||
const assignments = { ...ensureAssignments() };
|
||
document.dispatchEvent(new CustomEvent('amily2:ruleProfilesChanged', {
|
||
detail: { profiles, assignments },
|
||
}));
|
||
}
|
||
|
||
export class RuleProfileManager {
|
||
listProfiles() {
|
||
const profiles = Object.values(ensureProfileMap())
|
||
.map(profile => cloneRuleProfile(profile))
|
||
.sort((left, right) => {
|
||
const leftName = left.name || left.id;
|
||
const rightName = right.name || right.id;
|
||
return leftName.localeCompare(rightName, 'zh-Hans-CN');
|
||
});
|
||
return profiles;
|
||
}
|
||
|
||
getProfile(id) {
|
||
if (!id) return null;
|
||
const profile = ensureProfileMap()[id];
|
||
return profile ? cloneRuleProfile(profile) : null;
|
||
}
|
||
|
||
saveProfile(profile) {
|
||
const normalized = sanitizeRuleProfile(profile);
|
||
const profileId = normalized.id || createRuleProfileId(normalized.name);
|
||
const nextProfile = {
|
||
...normalized,
|
||
id: profileId,
|
||
name: normalized.name || profileId,
|
||
};
|
||
|
||
ensureProfileMap()[profileId] = nextProfile;
|
||
saveSettingsDebounced();
|
||
_dispatchChange();
|
||
return cloneRuleProfile(nextProfile);
|
||
}
|
||
|
||
deleteProfile(id) {
|
||
if (!id) return false;
|
||
const profiles = ensureProfileMap();
|
||
if (!profiles[id]) return false;
|
||
delete profiles[id];
|
||
saveSettingsDebounced();
|
||
_dispatchChange();
|
||
return true;
|
||
}
|
||
|
||
resolveProfile(id, fallback = {}) {
|
||
return mergeRuleConfig(this.getProfile(id), fallback);
|
||
}
|
||
|
||
// ── 功能槽分配 ──────────────────────────────────────────────────────────────
|
||
|
||
getAssignment(slot) {
|
||
if (!RULE_SLOTS[slot]) return null;
|
||
return ensureAssignments()[slot] || null;
|
||
}
|
||
|
||
setAssignment(slot, profileId) {
|
||
if (!RULE_SLOTS[slot]) return false;
|
||
const assignments = ensureAssignments();
|
||
if (profileId) {
|
||
assignments[slot] = profileId;
|
||
} else {
|
||
delete assignments[slot];
|
||
}
|
||
saveSettingsDebounced();
|
||
_dispatchChange();
|
||
return true;
|
||
}
|
||
|
||
getAssignedProfile(slot) {
|
||
const id = this.getAssignment(slot);
|
||
if (!id) return null;
|
||
const profile = ensureProfileMap()[id];
|
||
return profile ? cloneRuleProfile(profile) : null;
|
||
}
|
||
}
|
||
|
||
export const ruleProfileManager = new RuleProfileManager();
|
||
|
||
export function resolveRuleConfig(ruleProfileId, fallback = {}) {
|
||
return ruleProfileManager.resolveProfile(ruleProfileId, fallback);
|
||
}
|
||
|
||
/**
|
||
* 通过功能槽名解析规则配置(推荐方式)
|
||
* 先查 assignments,再回退到旧字段
|
||
*/
|
||
export function resolveSlotRuleConfig(slot, legacyFallback = {}) {
|
||
const assignedId = ruleProfileManager.getAssignment(slot);
|
||
if (assignedId) {
|
||
const profile = ruleProfileManager.getProfile(assignedId);
|
||
if (profile) return profile;
|
||
}
|
||
// 回退到旧的 resolve 路径
|
||
return sanitizeRuleProfile(legacyFallback);
|
||
}
|
||
|
||
export function resolveCondensationRuleConfig(settings = {}) {
|
||
const condensation = settings.condensation || {};
|
||
return resolveSlotRuleConfig('condensation', {
|
||
...condensation,
|
||
ruleProfileId: condensation.ruleProfileId,
|
||
});
|
||
}
|
||
|
||
export function resolveQueryPreprocessingRuleConfig(settings = {}) {
|
||
const queryPreprocessing = settings.queryPreprocessing || {};
|
||
return resolveSlotRuleConfig('queryPreprocessing', {
|
||
...queryPreprocessing,
|
||
ruleProfileId: queryPreprocessing.ruleProfileId,
|
||
});
|
||
}
|
||
|
||
export function resolveTableRuleConfig(settings = {}) {
|
||
return resolveSlotRuleConfig('table', {
|
||
id: settings.table_rule_profile_id,
|
||
tagExtractionEnabled: Boolean(settings.table_tags_to_extract),
|
||
tags: settings.table_tags_to_extract || '',
|
||
exclusionRules: settings.table_exclusion_rules || [],
|
||
});
|
||
}
|
||
|
||
export function resolveHistoriographyRuleConfig(settings = {}) {
|
||
return resolveSlotRuleConfig('historiography', {
|
||
id: settings.historiographyRuleProfileId,
|
||
tagExtractionEnabled: settings.historiographyTagExtractionEnabled ?? false,
|
||
tags: settings.historiographyTags || '',
|
||
exclusionRules: settings.historiographyExclusionRules || [],
|
||
});
|
||
}
|
||
|
||
// ── 一次性迁移:旧分散 profileId 字段 → 统一 assignments ─────────────────────
|
||
;(() => {
|
||
const settings = ensureSettingsRoot();
|
||
const assignments = ensureAssignments();
|
||
let changed = false;
|
||
|
||
// table: table_rule_profile_id → assignments.table
|
||
if (settings.table_rule_profile_id && !assignments.table) {
|
||
assignments.table = settings.table_rule_profile_id;
|
||
changed = true;
|
||
}
|
||
|
||
// historiography: historiographyRuleProfileId → assignments.historiography
|
||
if (settings.historiographyRuleProfileId && !assignments.historiography) {
|
||
assignments.historiography = settings.historiographyRuleProfileId;
|
||
changed = true;
|
||
}
|
||
|
||
// condensation: condensation.ruleProfileId → assignments.condensation
|
||
const condensation = settings.condensation || {};
|
||
if (condensation.ruleProfileId && !assignments.condensation) {
|
||
assignments.condensation = condensation.ruleProfileId;
|
||
changed = true;
|
||
}
|
||
|
||
// queryPreprocessing: queryPreprocessing.ruleProfileId → assignments.queryPreprocessing
|
||
const queryPreprocessing = settings.queryPreprocessing || {};
|
||
if (queryPreprocessing.ruleProfileId && !assignments.queryPreprocessing) {
|
||
assignments.queryPreprocessing = queryPreprocessing.ruleProfileId;
|
||
changed = true;
|
||
}
|
||
|
||
if (changed) {
|
||
saveSettingsDebounced();
|
||
console.log('[RuleProfiles] 已迁移旧规则配置分配到统一 assignments。', assignments);
|
||
}
|
||
})();
|
||
|
||
setTimeout(() => {
|
||
try {
|
||
const ctx = window.Amily2Bus?.register('RuleProfiles');
|
||
if (!ctx) {
|
||
console.warn('[RuleProfiles] Amily2Bus 尚未就绪,注册跳过。');
|
||
return;
|
||
}
|
||
ctx.expose({
|
||
listProfiles: () => ruleProfileManager.listProfiles(),
|
||
getProfile: (id) => ruleProfileManager.getProfile(id),
|
||
saveProfile: (profile) => ruleProfileManager.saveProfile(profile),
|
||
deleteProfile: (id) => ruleProfileManager.deleteProfile(id),
|
||
resolveProfile: (id, fallback) => ruleProfileManager.resolveProfile(id, fallback),
|
||
getAssignment: (slot) => ruleProfileManager.getAssignment(slot),
|
||
setAssignment: (slot, id) => ruleProfileManager.setAssignment(slot, id),
|
||
getAssignedProfile: (slot) => ruleProfileManager.getAssignedProfile(slot),
|
||
RULE_SLOTS,
|
||
});
|
||
ctx.log('RuleProfiles', 'info', 'RuleProfiles 服务已注册到 Bus。');
|
||
} catch (error) {
|
||
console.error('[RuleProfiles] Bus 注册失败:', error);
|
||
}
|
||
}, 0);
|