feat: add API config system, FilePipe backend, and ConfigManager

- ConfigManager: route sensitive keys (API keys) to localStorage,
  migrate existing values out of extension_settings on startup
- ApiKeyStore: local/cloud storage modes with RSA+AES hybrid encryption
- ApiProfileManager: named connection profiles (chat/embedding/rerank)
  with per-slot type-validated assignments
- FilePipe: complete IndexedDB backend (read/write/delete/list/stat)
- Amily2Bus: inject FilePipe via forPlugin() capability token
- UI: api-config-panel with profile CRUD and slot assignment
- TableSystemService: initial service layer scaffold
- logger.js: XSS fix
This commit is contained in:
2026-03-10 22:07:15 +08:00
parent ed3f52a568
commit 0be6a86e94
17 changed files with 1970 additions and 110 deletions

View File

@@ -0,0 +1,304 @@
/**
* 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: '剧情优化', type: 'chat' },
plotOptConc: { label: '剧情优化(并发)', type: 'chat' },
ngms: { label: 'NGMS 历史记录', type: 'chat' },
nccs: { label: 'NCCS 并发', type: 'chat' },
jqyh: { label: 'JQYH', type: 'chat' },
cwb: { label: '角色卡编辑器', type: 'chat' },
superMemory: { 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);