Files
ST-Amily2-Chat-Optimisation/utils/config/RuleProfileManager.js
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

307 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);