ci: auto build & obfuscate [2026-04-06 00:50:28] (Jenkins #7)

This commit is contained in:
Jenkins CI
2026-04-06 00:50:28 +08:00
parent ed3f52a568
commit 49c1fa6f60
142 changed files with 38769 additions and 29661 deletions

View File

@@ -0,0 +1,303 @@
/**
* ApiProfileManager — API 连接配置组管理
*
* Profile 是一组完整的 API 连接参数,按模型类型分为三类:
* chat — 对话/补全模型(主 API、剧情优化、各子系统等
* embedding — 向量嵌入模型RAG 向量化)
* rerank — 重排序模型RAG 精排)
*
* 存储分离:
* Profile 元数据name、type、provider、url、model、params→ extension_settings.amily2_profiles
* API Key → ApiKeyStorelocal 或 cloud 加密)
*
* 功能分配assignments
* 记录每个系统功能当前使用哪个 Profile ID存于 extension_settings.amily2_profile_assignments
* 选单会按功能对应的 Profile 类型进行过滤,防止类型错配。
*
* Bus 注册名:'ApiProfiles'
*
* 公开接口:
* getProfiles(type?) — 获取全部或指定类型的 Profile 列表
* getProfile(id) — 获取单个 Profile 元数据
* createProfile(data) — 新建 Profile返回新 ID
* updateProfile(id, data) — 更新 Profile 元数据
* deleteProfile(id) — 删除 Profile含清理 Key
* getKey(id) — 读取 Profile 的 API Key异步自动解密
* setKey(id, value) — 写入 Profile 的 API Key异步自动加密
* getAssignment(slot) — 获取功能槽当前分配的 Profile ID
* setAssignment(slot, id) — 设置功能槽的 Profile
* getAssignedProfile(slot) — 获取功能槽完整 Profile含解密 Key
* SLOTS — 可用功能槽清单(静态)
* PROFILE_TYPES — Profile 类型定义(静态)
*/
import { extension_settings } from "/scripts/extensions.js";
import { saveSettingsDebounced } from "/script.js";
import { extensionName } from "../settings.js";
import { apiKeyStore } from "./api-key-store/ApiKeyStore.js";
// ── 类型与功能槽定义 ──────────────────────────────────────────────────────────
/** Profile 类型定义 */
export const PROFILE_TYPES = {
chat: {
label: '对话模型',
icon: 'fa-comments',
description: '用于文本生成、对话补全的模型Chat / Completion',
params: ['maxTokens', 'temperature'],
},
embedding: {
label: '向量嵌入',
icon: 'fa-project-diagram',
description: '将文本转换为向量的模型,用于 RAG 语义检索',
params: ['dimensions', 'encodingFormat'],
},
rerank: {
label: '重排序',
icon: 'fa-sort-amount-down',
description: '对检索结果重新打分排序的模型,用于 RAG 精排',
params: ['topN', 'returnDocuments'],
},
};
/** 功能槽:每个系统功能需要的 Profile 类型 */
export const SLOTS = {
// Chat 槽
main: { label: '主 API正文优化', type: 'chat' },
plotOpt: { label: '剧情优化 / JQYH', type: 'chat' },
plotOptConc: { label: '剧情优化(并发)', type: 'chat' },
ngms: { label: 'NGMS 历史记录', type: 'chat' },
nccs: { label: 'NCCS 并发', type: 'chat' },
cwb: { label: '角色世界书', type: 'chat' },
autoCharCard: { label: '一键生卡', type: 'chat' },
// Embedding 槽
ragEmbed: { label: 'RAG 向量化', type: 'embedding' },
// Rerank 槽
ragRerank: { label: 'RAG 重排序', type: 'rerank' },
};
// extension_settings 存储 key
const EXT_PROFILES = 'amily2_profiles';
const EXT_ASSIGNMENTS = 'amily2_profile_assignments';
// ── ApiProfileManager ─────────────────────────────────────────────────────────
class ApiProfileManager {
// ── 内部工具 ────────────────────────────────────────────────────────────
_settings() {
if (!extension_settings[extensionName]) extension_settings[extensionName] = {};
return extension_settings[extensionName];
}
_profiles() {
const s = this._settings();
if (!Array.isArray(s[EXT_PROFILES])) s[EXT_PROFILES] = [];
return s[EXT_PROFILES];
}
_assignments() {
const s = this._settings();
if (!s[EXT_ASSIGNMENTS] || typeof s[EXT_ASSIGNMENTS] !== 'object') {
s[EXT_ASSIGNMENTS] = {};
}
return s[EXT_ASSIGNMENTS];
}
_save() {
saveSettingsDebounced();
}
_newId() {
return `p_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
}
// ── Profile CRUD ────────────────────────────────────────────────────────
/**
* 获取 Profile 列表。
* @param {'chat'|'embedding'|'rerank'} [type] 不传则返回全部
* @returns {Array}
*/
getProfiles(type) {
const all = this._profiles();
return type ? all.filter(p => p.type === type) : [...all];
}
/**
* 获取单个 Profile 元数据(不含 Key
*/
getProfile(id) {
return this._profiles().find(p => p.id === id) ?? null;
}
/**
* 新建 Profile。
* @param {Object} data Profile 数据(不含 id、apiKey
* @returns {string} 新 Profile 的 id
*/
createProfile(data) {
const id = this._newId();
const profile = this._buildProfile(id, data);
this._profiles().push(profile);
this._save();
return id;
}
/**
* 更新 Profile 元数据(不更新 KeyKey 用 setKey())。
*/
updateProfile(id, data) {
const list = this._profiles();
const idx = list.findIndex(p => p.id === id);
if (idx === -1) return false;
list[idx] = this._buildProfile(id, { ...list[idx], ...data });
this._save();
return true;
}
/**
* 删除 Profile同时清理存储的 Key 和功能槽引用)。
*/
deleteProfile(id) {
const s = this._settings();
s[EXT_PROFILES] = this._profiles().filter(p => p.id !== id);
// 清理功能槽引用
const asgn = this._assignments();
for (const slot in asgn) {
if (asgn[slot] === id) delete asgn[slot];
}
// 清理 Key
apiKeyStore.deleteById(id);
this._save();
}
// ── Key 操作 ────────────────────────────────────────────────────────────
/** 读取 Profile 的 API Key异步自动解密 */
async getKey(id) {
return apiKeyStore.retrieveById(id);
}
/** 写入 Profile 的 API Key异步自动加密 */
async setKey(id, value) {
return apiKeyStore.storeById(id, value);
}
// ── 功能槽分配 ──────────────────────────────────────────────────────────
/** 获取功能槽当前分配的 Profile IDnull = 未分配) */
getAssignment(slot) {
return this._assignments()[slot] ?? null;
}
/**
* 设置功能槽的 Profile。
* 会校验 Profile 类型是否与槽类型匹配。
*/
setAssignment(slot, profileId) {
if (!SLOTS[slot]) {
console.warn(`[ApiProfiles] 未知功能槽 "${slot}"。`);
return false;
}
if (profileId !== null) {
const profile = this.getProfile(profileId);
if (!profile) {
console.warn(`[ApiProfiles] Profile "${profileId}" 不存在。`);
return false;
}
if (profile.type !== SLOTS[slot].type) {
console.warn(`[ApiProfiles] 类型不匹配:槽 "${slot}" 需要 ${SLOTS[slot].type}Profile 类型为 ${profile.type}`);
return false;
}
}
this._assignments()[slot] = profileId;
this._save();
return true;
}
/**
* 获取功能槽完整 Profile包含解密后的 API Key。
* @returns {Promise<Object|null>}
*/
async getAssignedProfile(slot) {
const id = this.getAssignment(slot);
if (!id) return null;
const profile = this.getProfile(id);
if (!profile) return null;
const apiKey = await this.getKey(id);
return { ...profile, apiKey };
}
// ── 内部Profile 对象构造 ──────────────────────────────────────────────
_buildProfile(id, data) {
const type = data.type || 'chat';
const base = {
id,
name: data.name || '未命名配置',
type,
provider: data.provider || 'openai',
apiUrl: data.apiUrl || '',
model: data.model || '',
};
if (type === 'chat') {
return {
...base,
maxTokens: data.maxTokens ?? 65500,
temperature: data.temperature ?? 1.0,
};
}
if (type === 'embedding') {
return {
...base,
dimensions: data.dimensions ?? null,
encodingFormat: data.encodingFormat ?? 'float',
};
}
if (type === 'rerank') {
return {
...base,
topN: data.topN ?? 5,
returnDocuments: data.returnDocuments ?? false,
};
}
return base;
}
}
// ── 单例导出 ─────────────────────────────────────────────────────────────────
export const apiProfileManager = new ApiProfileManager();
// ── Bus 注册 ──────────────────────────────────────────────────────────────────
setTimeout(() => {
try {
const _ctx = window.Amily2Bus?.register('ApiProfiles');
if (!_ctx) {
console.warn('[ApiProfiles] Amily2Bus 尚未就绪,注册跳过。');
return;
}
_ctx.expose({
getProfiles: (type) => apiProfileManager.getProfiles(type),
getProfile: (id) => apiProfileManager.getProfile(id),
createProfile: (data) => apiProfileManager.createProfile(data),
updateProfile: (id, data) => apiProfileManager.updateProfile(id, data),
deleteProfile: (id) => apiProfileManager.deleteProfile(id),
getKey: (id) => apiProfileManager.getKey(id),
setKey: (id, val) => apiProfileManager.setKey(id, val),
getAssignment: (slot) => apiProfileManager.getAssignment(slot),
setAssignment: (slot, id) => apiProfileManager.setAssignment(slot, id),
getAssignedProfile: (slot) => apiProfileManager.getAssignedProfile(slot),
SLOTS: SLOTS,
PROFILE_TYPES: PROFILE_TYPES,
});
_ctx.log('ApiProfiles', 'info', 'ApiProfiles 服务已注册到 Bus。');
} catch (e) {
console.error('[ApiProfiles] Bus 注册失败:', e);
}
}, 0);

View File

@@ -0,0 +1,155 @@
/**
* ConfigManager — 独立配置持久化管理模块
*
* 解决的安全问题:
* SillyTavern 的 extension_settings 会通过 saveSettingsDebounced() 上传到 ST
* 服务端 settings.json。使用三方云服务商时服务商可读取该文件导致所有
* API 密钥泄露。
*
* 解决方案:
* 敏感字段API Key / URL→ localStorage浏览器本地绝不上传
* 非敏感字段 → extension_settings维持原有行为
*
* Bus 注册名:'Config'
*
* 公开接口query('Config')
* get(key) — 读取配置项(自动路由)
* set(key, value) — 写入配置项(自动路由 + 触发保存)
* getSettings() — 返回完整配置对象(敏感字段从 localStorage 注入)
* migrate() — 将 extension_settings 中残留的敏感字段迁移到 localStorage
*/
import { extension_settings } from "/scripts/extensions.js";
import { saveSettingsDebounced } from "/script.js";
import { extensionName } from "../settings.js";
import { SENSITIVE_KEYS } from "./sensitive-keys.js";
// localStorage key 前缀,避免与其他插件冲突
const LS_PREFIX = 'amily2_secure_';
// ── ConfigManager ────────────────────────────────────────────────────────────
class ConfigManager {
/**
* 读取配置项。
* 敏感字段从 localStorage 读取,其余从 extension_settings 读取。
* @param {string} key
* @returns {*}
*/
get(key) {
if (SENSITIVE_KEYS.has(key)) {
return localStorage.getItem(LS_PREFIX + key) ?? '';
}
return extension_settings[extensionName]?.[key];
}
/**
* 写入配置项并持久化。
* 敏感字段写入 localStorage同时从 extension_settings 清除残留)。
* 非敏感字段写入 extension_settings 并触发 saveSettingsDebounced。
* @param {string} key
* @param {*} value
*/
set(key, value) {
if (SENSITIVE_KEYS.has(key)) {
if (value !== null && value !== undefined && value !== '') {
localStorage.setItem(LS_PREFIX + key, value);
} else {
localStorage.removeItem(LS_PREFIX + key);
}
// 确保 extension_settings 中不保留该敏感字段
const settings = extension_settings[extensionName];
if (settings && Object.prototype.hasOwnProperty.call(settings, key)) {
delete settings[key];
saveSettingsDebounced();
}
} else {
if (!extension_settings[extensionName]) {
extension_settings[extensionName] = {};
}
extension_settings[extensionName][key] = value;
saveSettingsDebounced();
}
}
/**
* 返回完整配置对象(合并视图)。
* 以 extension_settings 为基础,将 localStorage 中的敏感字段注入覆盖。
*
* 用途:替换现有 `const settings = extension_settings[extensionName]` 的读取点,
* 使 API 调用模块能透明地获取到敏感字段,无需感知存储层差异。
*
* @returns {Object}
*/
getSettings() {
const base = extension_settings[extensionName] ?? {};
const result = { ...base };
for (const key of SENSITIVE_KEYS) {
const val = localStorage.getItem(LS_PREFIX + key);
// null 表示 localStorage 中不存在,保留 base 中原值(如有)
if (val !== null) {
result[key] = val;
}
}
return result;
}
/**
* 迁移:将 extension_settings 中已存在的敏感字段移到 localStorage。
*
* 应在插件初始化阶段调用一次。
* 逻辑:
* - 若 extension_settings 有值 → 迁移到 localStorage若 localStorage 已有值则跳过,保留用户上次输入)
* - 从 extension_settings 删除该字段
* - 最终触发一次 saveSettingsDebounced 清洗服务端
*/
migrate() {
const settings = extension_settings[extensionName];
if (!settings) return;
let needsSave = false;
for (const key of SENSITIVE_KEYS) {
const settingsVal = settings[key];
if (settingsVal !== undefined && settingsVal !== '') {
// localStorage 中已有值时不覆盖(优先保留用户最新输入)
if (!localStorage.getItem(LS_PREFIX + key)) {
localStorage.setItem(LS_PREFIX + key, settingsVal);
console.info(`[Amily2-Config] 已迁移敏感字段 "${key}" 到本地安全存储。`);
}
delete settings[key];
needsSave = true;
}
}
if (needsSave) {
saveSettingsDebounced();
console.info('[Amily2-Config] 敏感配置迁移完成,已从云同步配置中清除密钥。');
}
}
}
// ── 单例导出 ─────────────────────────────────────────────────────────────────
export const configManager = new ConfigManager();
// ── Bus 注册 ──────────────────────────────────────────────────────────────────
// setTimeout 确保 window.Amily2Bus 在 Amily2Bus.js 模块体执行后已挂载
setTimeout(() => {
try {
const _ctx = window.Amily2Bus?.register('Config');
if (!_ctx) {
console.warn('[Config] Amily2Bus 尚未就绪Config 服务注册跳过。');
return;
}
_ctx.expose({
get: (key) => configManager.get(key),
set: (key, value) => configManager.set(key, value),
getSettings: () => configManager.getSettings(),
migrate: () => configManager.migrate(),
});
_ctx.log('ConfigManager', 'info', 'Config 服务已注册到 Bus。');
} catch (e) {
console.error('[Config] Bus 注册失败:', e);
}
}, 0);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
function a0_0x3989(_0x1df389,_0x310d89){_0x1df389=_0x1df389-0x1c6;const _0x5ab531=a0_0x5ab5();let _0x398999=_0x5ab531[_0x1df389];if(a0_0x3989['Foddzv']===undefined){var _0xe81465=function(_0x411c31){const _0x3a91f9='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';let _0x38439d='',_0x1a6e14='';for(let _0x57af3d=0x0,_0x377e1b,_0x3132f0,_0x2de46a=0x0;_0x3132f0=_0x411c31['charAt'](_0x2de46a++);~_0x3132f0&&(_0x377e1b=_0x57af3d%0x4?_0x377e1b*0x40+_0x3132f0:_0x3132f0,_0x57af3d++%0x4)?_0x38439d+=String['fromCharCode'](0xff&_0x377e1b>>(-0x2*_0x57af3d&0x6)):0x0){_0x3132f0=_0x3a91f9['indexOf'](_0x3132f0);}for(let _0x1ce259=0x0,_0x4bbf56=_0x38439d['length'];_0x1ce259<_0x4bbf56;_0x1ce259++){_0x1a6e14+='%'+('00'+_0x38439d['charCodeAt'](_0x1ce259)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0x1a6e14);};const _0x472d1d=function(_0x32e8e6,_0x173be5){let _0x55a167=[],_0x3959cd=0x0,_0x1d7066,_0x2e940d='';_0x32e8e6=_0xe81465(_0x32e8e6);let _0x486e5f;for(_0x486e5f=0x0;_0x486e5f<0x100;_0x486e5f++){_0x55a167[_0x486e5f]=_0x486e5f;}for(_0x486e5f=0x0;_0x486e5f<0x100;_0x486e5f++){_0x3959cd=(_0x3959cd+_0x55a167[_0x486e5f]+_0x173be5['charCodeAt'](_0x486e5f%_0x173be5['length']))%0x100,_0x1d7066=_0x55a167[_0x486e5f],_0x55a167[_0x486e5f]=_0x55a167[_0x3959cd],_0x55a167[_0x3959cd]=_0x1d7066;}_0x486e5f=0x0,_0x3959cd=0x0;for(let _0x432f1f=0x0;_0x432f1f<_0x32e8e6['length'];_0x432f1f++){_0x486e5f=(_0x486e5f+0x1)%0x100,_0x3959cd=(_0x3959cd+_0x55a167[_0x486e5f])%0x100,_0x1d7066=_0x55a167[_0x486e5f],_0x55a167[_0x486e5f]=_0x55a167[_0x3959cd],_0x55a167[_0x3959cd]=_0x1d7066,_0x2e940d+=String['fromCharCode'](_0x32e8e6['charCodeAt'](_0x432f1f)^_0x55a167[(_0x55a167[_0x486e5f]+_0x55a167[_0x3959cd])%0x100]);}return _0x2e940d;};a0_0x3989['Kjmdif']=_0x472d1d,a0_0x3989['QzqDJl']={},a0_0x3989['Foddzv']=!![];}const _0x1734a8=_0x5ab531[0x0],_0xb89448=_0x1df389+_0x1734a8,_0x815408=a0_0x3989['QzqDJl'][_0xb89448];return!_0x815408?(a0_0x3989['nyCSiw']===undefined&&(a0_0x3989['nyCSiw']=!![]),_0x398999=a0_0x3989['Kjmdif'](_0x398999,_0x310d89),a0_0x3989['QzqDJl'][_0xb89448]=_0x398999):_0x398999=_0x815408,_0x398999;}const a0_0x34aa0e=a0_0x3989;(function(_0x442981,_0x44c8a7){const _0x56aab8=a0_0x3989,_0x5ed526=_0x442981();while(!![]){try{const _0x301501=parseInt(_0x56aab8(0x1c7,'vIZB'))/0x1+parseInt(_0x56aab8(0x1d6,'pNw*'))/0x2*(-parseInt(_0x56aab8(0x1dd,'J*IK'))/0x3)+parseInt(_0x56aab8(0x1d3,'zVhL'))/0x4+parseInt(_0x56aab8(0x1d4,'8wEC'))/0x5*(parseInt(_0x56aab8(0x1db,'4pxo'))/0x6)+parseInt(_0x56aab8(0x1cc,'IJnB'))/0x7+-parseInt(_0x56aab8(0x1de,'se7B'))/0x8+-parseInt(_0x56aab8(0x1d5,'1yDA'))/0x9*(-parseInt(_0x56aab8(0x1df,'SJGV'))/0xa);if(_0x301501===_0x44c8a7)break;else _0x5ed526['push'](_0x5ed526['shift']());}catch(_0x3951ea){_0x5ed526['push'](_0x5ed526['shift']());}}}(a0_0x5ab5,0x9f59f));function a0_0x5ab5(){const _0x3ca214=['e8kbFHhdUmofdcniu8ovwq','lSoMWRLta1jiya','aSkTWRn0o8oM','DfhcJ8kiAcrypKddQmoYW7u','W6dcVGSfjfBcPCoUntyqha','WQ4ODuFcKGq9FmouWP1WsW','f17dGbJcMSotmqruu8knESoY','kmoLW6qawq4KwxzZvq1L','W5PtvcvqW7JcISovWRmpWRddMa','sCknW6m6WQhcMmokWPlcImkN','rCkJW60UWQBcUSovgaGFaCk3','W649W7D1WOldHmkFhG','W5lcO8krBHJdPCkYBW','rCkGWRX6W7FdP8oggW','WORcGmonW6fuWRpdPL11ohTU','W7G8WReFBNddT8oGW7HqqK5K','W5hdKsCfWOrmpN4','W5vdorZcT8orWRDWpHldTmokW78','CSkSdIhdKSoksSkG','W6LZxGv3W4BcT8koWRldLW','kmoKW6WbxWTvAhfYsWG','lxhdQKlcLmonBCorimoOWQ4lp2FdPCo+W4zaWPXXWQmgwqa','lCo3fdddU8opwCkObWO','W5yvBYZcJmkiW4BdL8okCwrT','b1GnW6pcPqehkmkulHq','WOBdNtSgW6dcN8ozh8kyW57dSmo/'];a0_0x5ab5=function(){return _0x3ca214;};return a0_0x5ab5();}export const SENSITIVE_KEYS=new Set([a0_0x34aa0e(0x1c6,'B#@t'),a0_0x34aa0e(0x1d9,'NseI'),a0_0x34aa0e(0x1cd,'50TD'),a0_0x34aa0e(0x1da,'pNw*'),a0_0x34aa0e(0x1d7,'^iY*'),a0_0x34aa0e(0x1dc,'6^hr')]);