907 lines
33 KiB
TypeScript
907 lines
33 KiB
TypeScript
import { defineStore } from 'pinia';
|
||
import { loadInitialDataset } from '../utils/yamlLoader';
|
||
import type { PromptDataset, PromptCategory, PromptGroup, PromptTag, LangCode, ExportBundle, CustomDiff, PromptPreset, ExtendedPreset, PresetFolder, PresetManagement, PresetType } from '../types';
|
||
|
||
const LS_KEY = 'ops.prompt.dataset.v1';
|
||
let saveTimer: number | null = null; // 非响应式计时器,避免递归更新
|
||
let baseline: PromptDataset | null = null; // 基线词库(从 public/sd 加载)
|
||
|
||
function deepClone<T>(obj: T): T {
|
||
return JSON.parse(JSON.stringify(obj));
|
||
}
|
||
|
||
export const usePromptStore = defineStore('promptStore', {
|
||
state: () => ({
|
||
dataset: null as PromptDataset | null,
|
||
selectedLang: 'zh_CN' as LangCode,
|
||
selectedCategoryIndex: 0,
|
||
selectedGroupIndex: 0,
|
||
searchQuery: '',
|
||
// 编辑器相关
|
||
promptText: '',
|
||
presets: [] as PromptPreset[],
|
||
// 扩展预设管理
|
||
extendedPresets: [] as ExtendedPreset[],
|
||
presetFolders: [] as PresetFolder[],
|
||
presetManagement: {
|
||
folders: [],
|
||
presets: [],
|
||
settings: {
|
||
autoBackup: true,
|
||
maxPresets: 1000
|
||
}
|
||
} as PresetManagement,
|
||
}),
|
||
getters: {
|
||
categories: (s) => s.dataset?.categories ?? [],
|
||
currentCategory: (s) => s.dataset?.categories[s.selectedCategoryIndex] ?? null,
|
||
currentGroup(): PromptGroup | null {
|
||
return this.currentCategory?.groups[this.selectedGroupIndex] ?? null;
|
||
},
|
||
languages: (s) => s.dataset?.languages ?? ['en'],
|
||
filteredTags(): PromptTag[] {
|
||
const grp = this.currentGroup;
|
||
if (!grp) return [] as PromptTag[];
|
||
const q = this.searchQuery.trim().toLowerCase();
|
||
const qNorm = q.replace(/_/g, ' ');
|
||
if (!q) return grp.tags;
|
||
return grp.tags.filter((t) => {
|
||
const trans = t.translation?.[this.selectedLang] ?? '';
|
||
const keyLower = t.key.toLowerCase();
|
||
const keyNorm = keyLower.replace(/_/g, ' ');
|
||
const transLower = trans.toLowerCase();
|
||
const transNorm = transLower.replace(/_/g, ' ');
|
||
return (
|
||
keyLower.includes(q) ||
|
||
keyNorm.includes(qNorm) ||
|
||
transLower.includes(q) ||
|
||
transNorm.includes(qNorm)
|
||
);
|
||
});
|
||
},
|
||
tokens(s): string[] {
|
||
return splitTokens(s.promptText);
|
||
},
|
||
},
|
||
actions: {
|
||
async initialize() {
|
||
// 先加载基线词库
|
||
baseline = await loadInitialDataset();
|
||
const raw = localStorage.getItem(LS_KEY);
|
||
if (raw) {
|
||
try {
|
||
const bundle = JSON.parse(raw) as ExportBundle;
|
||
if (bundle.dataset) {
|
||
this.dataset = bundle.dataset;
|
||
} else if (bundle.customDiff) {
|
||
this.dataset = this.applyDiff(deepClone(baseline!), bundle.customDiff);
|
||
} else {
|
||
this.dataset = deepClone(baseline!);
|
||
}
|
||
this.presets = bundle.presets || [];
|
||
// 恢复扩展预设数据
|
||
this.extendedPresets = bundle.extendedPresets || [];
|
||
this.presetFolders = bundle.presetFolders || [];
|
||
if (bundle.presetManagement) {
|
||
this.presetManagement = bundle.presetManagement;
|
||
}
|
||
// 恢复编辑器内容与语言
|
||
if (typeof bundle.promptText === 'string') {
|
||
this.promptText = bundle.promptText;
|
||
}
|
||
if (bundle.selectedLang) {
|
||
this.selectedLang = bundle.selectedLang as LangCode;
|
||
}
|
||
} catch {
|
||
this.dataset = deepClone(baseline!);
|
||
}
|
||
} else {
|
||
this.dataset = deepClone(baseline!);
|
||
}
|
||
// 若无恢复语言,则按数据集进行推断
|
||
if (!this.selectedLang) {
|
||
const guessLang: LangCode = (this.dataset.languages.includes('zh_CN') ? 'zh_CN' : 'en') as LangCode;
|
||
this.selectedLang = guessLang;
|
||
}
|
||
// 初始化扩展预设管理
|
||
this.initializeExtendedPresets();
|
||
// 自动迁移旧预设到新系统
|
||
this.migrateOldPresets();
|
||
this.autoPersist();
|
||
},
|
||
autoPersist() {
|
||
// 订阅状态变化并延迟保存(不写入 store,避免递归)
|
||
this.$subscribe(() => {
|
||
if (saveTimer) window.clearTimeout(saveTimer);
|
||
saveTimer = window.setTimeout(() => {
|
||
this.save();
|
||
}, 400);
|
||
}, { detached: true });
|
||
},
|
||
save() {
|
||
if (!this.dataset) return;
|
||
const bundle: ExportBundle = {
|
||
version: 1,
|
||
savedAt: new Date().toISOString(),
|
||
dataset: deepClone(this.dataset),
|
||
presets: deepClone(this.presets),
|
||
// 扩展预设数据
|
||
extendedPresets: deepClone(this.extendedPresets),
|
||
presetFolders: deepClone(this.presetFolders),
|
||
presetManagement: deepClone(this.presetManagement),
|
||
promptText: this.promptText,
|
||
selectedLang: this.selectedLang,
|
||
};
|
||
localStorage.setItem(LS_KEY, JSON.stringify(bundle));
|
||
},
|
||
resetToDefault() {
|
||
this.importFromBundle(null);
|
||
},
|
||
async importFromBundle(bundle: ExportBundle | null) {
|
||
if (!bundle) {
|
||
if (!baseline) baseline = await loadInitialDataset();
|
||
this.dataset = deepClone(baseline!);
|
||
this.presets = [];
|
||
this.extendedPresets = [];
|
||
this.presetFolders = [];
|
||
} else {
|
||
// 确保兼容性
|
||
bundle = this.ensureCompatibility(bundle);
|
||
|
||
if (bundle.dataset) {
|
||
this.dataset = bundle.dataset;
|
||
} else if (bundle.customDiff) {
|
||
if (!baseline) baseline = await loadInitialDataset();
|
||
this.dataset = this.applyDiff(deepClone(baseline!), bundle.customDiff);
|
||
}
|
||
this.presets = bundle.presets || [];
|
||
// 导入扩展预设数据
|
||
this.extendedPresets = bundle.extendedPresets || [];
|
||
this.presetFolders = bundle.presetFolders || [];
|
||
if (bundle.presetManagement) {
|
||
this.presetManagement = bundle.presetManagement;
|
||
}
|
||
}
|
||
this.selectedCategoryIndex = 0;
|
||
this.selectedGroupIndex = 0;
|
||
// 确保扩展预设管理已初始化
|
||
this.initializeExtendedPresets();
|
||
this.save();
|
||
},
|
||
exportToJson(): string {
|
||
// 导出仅包含自定义差异(不包含公共词库)
|
||
const diff = this.buildDiff(baseline!, this.dataset!);
|
||
const bundle: ExportBundle = {
|
||
version: 1,
|
||
savedAt: new Date().toISOString(),
|
||
customDiff: diff,
|
||
presets: deepClone(this.presets),
|
||
// 导出扩展预设数据
|
||
extendedPresets: deepClone(this.extendedPresets),
|
||
presetFolders: deepClone(this.presetFolders),
|
||
presetManagement: deepClone(this.presetManagement),
|
||
};
|
||
return JSON.stringify(bundle, null, 2);
|
||
},
|
||
setLanguage(lang: LangCode) {
|
||
this.selectedLang = lang;
|
||
},
|
||
selectCategory(idx: number) {
|
||
this.selectedCategoryIndex = idx;
|
||
this.selectedGroupIndex = 0;
|
||
},
|
||
selectGroup(idx: number) {
|
||
this.selectedGroupIndex = idx;
|
||
},
|
||
setSearch(q: string) {
|
||
this.searchQuery = q;
|
||
},
|
||
addTag(groupId: string, key = 'new_tag') {
|
||
const grp = this.findGroupById(groupId);
|
||
if (!grp) return;
|
||
grp.tags.push({ key, translation: { en: key, [this.selectedLang]: key } });
|
||
},
|
||
removeTag(groupId: string, key: string) {
|
||
const grp = this.findGroupById(groupId);
|
||
if (!grp) return;
|
||
grp.tags = grp.tags.filter((t) => t.key !== key);
|
||
},
|
||
updateTagKey(groupId: string, oldKey: string, newKey: string) {
|
||
const grp = this.findGroupById(groupId);
|
||
if (!grp) return;
|
||
const tag = grp.tags.find((t) => t.key === oldKey);
|
||
if (!tag) return;
|
||
tag.key = newKey;
|
||
if (!tag.translation) tag.translation = {};
|
||
tag.translation.en = newKey;
|
||
},
|
||
setTranslation(groupId: string, key: string, lang: LangCode, val: string) {
|
||
const grp = this.findGroupById(groupId);
|
||
if (!grp) return;
|
||
const tag = grp.tags.find((t) => t.key === key);
|
||
if (!tag) return;
|
||
if (!tag.translation) tag.translation = {};
|
||
tag.translation[lang] = val;
|
||
},
|
||
toggleHidden(groupId: string, key: string) {
|
||
const grp = this.findGroupById(groupId);
|
||
if (!grp) return;
|
||
const tag = grp.tags.find((t) => t.key === key);
|
||
if (!tag) return;
|
||
tag.hidden = !tag.hidden;
|
||
},
|
||
reorderTags(groupId: string, fromIndex: number, toIndex: number) {
|
||
const grp = this.findGroupById(groupId);
|
||
if (!grp) return;
|
||
const list = grp.tags;
|
||
if (fromIndex < 0 || toIndex < 0 || fromIndex >= list.length || toIndex >= list.length) return;
|
||
const [item] = list.splice(fromIndex, 1);
|
||
if (item) list.splice(toIndex, 0, item);
|
||
},
|
||
// —— 编辑器核心:基于左侧文本的 token 操作 ——
|
||
setPromptText(text: string) {
|
||
this.promptText = normalizePrompt(text);
|
||
},
|
||
setPromptTextRaw(text: string) {
|
||
// 原始赋值,不做格式化,保留光标与撤回体验
|
||
this.promptText = text;
|
||
},
|
||
replaceChineseComma() {
|
||
this.promptText = this.promptText.replace(/,/g, ',');
|
||
},
|
||
formatPrompt() {
|
||
this.promptText = normalizePrompt(this.promptText);
|
||
},
|
||
// 切换下划线和空格
|
||
toggleUnderscoreSpace() {
|
||
const tokens = splitTokens(this.promptText);
|
||
const newTokens = tokens.map(token => {
|
||
// 解析包裹层数和核心内容
|
||
const { core, wrappers } = this.parseTokenWrappers(token);
|
||
|
||
// 切换下划线和空格
|
||
let newCore;
|
||
if (core.includes('_')) {
|
||
newCore = core.replace(/_/g, ' ');
|
||
} else if (core.includes(' ')) {
|
||
newCore = core.replace(/ /g, '_');
|
||
} else {
|
||
newCore = core;
|
||
}
|
||
|
||
// 重新包装
|
||
return this.wrapToken(newCore, wrappers);
|
||
});
|
||
this.promptText = newTokens.join(', ');
|
||
},
|
||
// 为单个token添加包裹层
|
||
addWrapperToToken(index: number, wrapperType: string = '{}') {
|
||
const tokens = splitTokens(this.promptText);
|
||
if (index < 0 || index >= tokens.length) return;
|
||
|
||
const token = tokens[index];
|
||
if (!token) return;
|
||
const { core, wrappers } = this.parseTokenWrappers(token);
|
||
const newWrappers = [...wrappers, wrapperType];
|
||
tokens[index] = this.wrapToken(core, newWrappers);
|
||
|
||
this.promptText = tokens.join(', ');
|
||
},
|
||
// 为单个token移除包裹层
|
||
removeWrapperFromToken(index: number) {
|
||
const tokens = splitTokens(this.promptText);
|
||
if (index < 0 || index >= tokens.length) return;
|
||
|
||
const token = tokens[index];
|
||
if (!token) return;
|
||
const { core, wrappers } = this.parseTokenWrappers(token);
|
||
if (wrappers.length > 0) {
|
||
const newWrappers = wrappers.slice(0, -1);
|
||
tokens[index] = this.wrapToken(core, newWrappers);
|
||
this.promptText = tokens.join(', ');
|
||
}
|
||
},
|
||
// 获取token的包裹信息(用于显示)
|
||
getTokenWrapperInfo(token: string): { core: string; wrappers: string[]; wrapperCount: number } {
|
||
const { core, wrappers } = this.parseTokenWrappers(token);
|
||
return { core, wrappers, wrapperCount: wrappers.length };
|
||
},
|
||
// 解析token的包裹层和核心内容
|
||
parseTokenWrappers(token: string): { core: string; wrappers: string[] } {
|
||
const wrapperPairs = [
|
||
['{}', '{', '}'],
|
||
['()', '(', ')'],
|
||
['[]', '[', ']'],
|
||
['<>', '<', '>']
|
||
];
|
||
|
||
let current = token.trim();
|
||
const wrappers: string[] = [];
|
||
|
||
// 从外到内解析包裹层
|
||
while (current.length >= 2) {
|
||
let found = false;
|
||
for (const [type, start, end] of wrapperPairs) {
|
||
if (start && end && current.startsWith(start) && current.endsWith(end)) {
|
||
if (type) wrappers.push(type);
|
||
current = current.slice(start.length, -end.length);
|
||
found = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!found) break;
|
||
}
|
||
|
||
return { core: current, wrappers };
|
||
},
|
||
// 用包裹层包装token
|
||
wrapToken(core: string, wrappers: string[]): string {
|
||
let result = core;
|
||
|
||
// 从内到外添加包裹层
|
||
for (let i = wrappers.length - 1; i >= 0; i--) {
|
||
const wrapper = wrappers[i];
|
||
switch (wrapper) {
|
||
case '{}':
|
||
result = `{${result}}`;
|
||
break;
|
||
case '()':
|
||
result = `(${result})`;
|
||
break;
|
||
case '[]':
|
||
result = `[${result}]`;
|
||
break;
|
||
case '<>':
|
||
result = `<${result}>`;
|
||
break;
|
||
}
|
||
}
|
||
|
||
return result;
|
||
},
|
||
updateToken(index: number, newToken: string) {
|
||
const tokens = splitTokens(this.promptText);
|
||
if (index < 0 || index >= tokens.length) return;
|
||
tokens[index] = normalizeToken(newToken);
|
||
this.promptText = tokens.join(', ');
|
||
},
|
||
reorderTokens(fromIndex: number, toIndex: number) {
|
||
const tokens = splitTokens(this.promptText);
|
||
if (fromIndex < 0 || toIndex < 0 || fromIndex >= tokens.length || toIndex >= tokens.length) return;
|
||
const [item] = tokens.splice(fromIndex, 1);
|
||
tokens.splice(toIndex, 0, item!);
|
||
this.promptText = tokens.join(', ');
|
||
},
|
||
removeToken(index: number) {
|
||
const tokens = splitTokens(this.promptText);
|
||
if (index < 0 || index >= tokens.length) return;
|
||
tokens.splice(index, 1);
|
||
this.promptText = tokens.join(', ');
|
||
},
|
||
addTokenAfter(index: number, token: string) {
|
||
const tokens = splitTokens(this.promptText);
|
||
tokens.splice(index + 1, 0, normalizeToken(token));
|
||
this.promptText = tokens.join(', ');
|
||
},
|
||
getTagByKey(key: string): PromptTag | null {
|
||
const target = normalizeKeyForMatch(key);
|
||
for (const cat of this.dataset?.categories || []) {
|
||
for (const g of cat.groups) {
|
||
for (const t of g.tags) {
|
||
if (t.key === key) return t; // 精确匹配优先
|
||
if (normalizeKeyForMatch(t.key) === target) return t; // 下划线/空格归一化匹配
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
},
|
||
getTranslation(key: string, lang: LangCode): string | null {
|
||
// 兼容包裹层:如 {aaa}、(aaa) 等
|
||
const { core, wrappers } = this.parseTokenWrappers(key);
|
||
const tag = this.getTagByKey(core);
|
||
if (!tag) return null;
|
||
const translatedCore = tag.translation?.[lang] ?? tag.key;
|
||
// 保持原有包裹层结构,返回被翻译后的核心
|
||
return this.wrapToken(translatedCore, wrappers);
|
||
},
|
||
getSuggestions(prefix: string, limit = 8): string[] {
|
||
const list: string[] = [];
|
||
const seen = new Set<string>();
|
||
const p = prefix.trim().toLowerCase();
|
||
const pNorm = p.replace(/_/g, ' ');
|
||
if (!p) return [];
|
||
for (const cat of this.dataset?.categories || []) {
|
||
for (const g of cat.groups) {
|
||
for (const t of g.tags) {
|
||
const k = t.key;
|
||
if (seen.has(k)) continue;
|
||
const kLower = k.toLowerCase();
|
||
const kNorm = kLower.replace(/_/g, ' ');
|
||
if (kLower.includes(p) || kNorm.includes(pNorm)) {
|
||
list.push(k);
|
||
seen.add(k);
|
||
if (list.length >= limit) return list;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return list;
|
||
},
|
||
addMapping(key: string, lang: LangCode, val: string) {
|
||
// 若 key 已存在则更新翻译;否则在自定义分组增加映射
|
||
const exist = this.getTagByKey(key);
|
||
if (exist) {
|
||
if (!exist.translation) exist.translation = {};
|
||
exist.translation[lang] = val;
|
||
return;
|
||
}
|
||
const grp = this.ensureCustomGroup();
|
||
grp.tags.push({ key, translation: { en: key, [lang]: val } });
|
||
},
|
||
ensureCustomGroup(): PromptGroup {
|
||
const catName = 'Custom';
|
||
let cat = this.dataset?.categories.find((c) => c.name === catName);
|
||
if (!cat) {
|
||
cat = { id: `cat_${Math.random().toString(36).slice(2, 9)}`, name: catName, groups: [] };
|
||
this.dataset?.categories.push(cat);
|
||
}
|
||
let grp = cat.groups.find((g) => g.name === 'User Mapping');
|
||
if (!grp) {
|
||
grp = { id: `grp_${Math.random().toString(36).slice(2, 9)}`, name: 'User Mapping', color: '#6366f1', tags: [] };
|
||
cat.groups.push(grp);
|
||
}
|
||
return grp;
|
||
},
|
||
savePreset(name: string) {
|
||
const now = new Date().toISOString();
|
||
const exist = this.presets.find((p) => p.name === name);
|
||
if (exist) {
|
||
exist.text = this.promptText;
|
||
exist.updatedAt = now;
|
||
} else {
|
||
this.presets.push({ name, text: this.promptText, updatedAt: now });
|
||
}
|
||
this.save();
|
||
},
|
||
loadPreset(name: string) {
|
||
const p = this.presets.find((x) => x.name === name);
|
||
if (!p) return;
|
||
this.promptText = p.text;
|
||
},
|
||
deletePreset(name: string) {
|
||
this.presets = this.presets.filter((p) => p.name !== name);
|
||
this.save();
|
||
},
|
||
renamePreset(oldName: string, newName: string) {
|
||
const target = this.presets.find((p) => p.name === oldName);
|
||
if (!target) return;
|
||
const conflict = this.presets.find((p) => p.name === newName);
|
||
const now = new Date().toISOString();
|
||
if (conflict && conflict !== target) {
|
||
// 合并到冲突项:用新名覆盖旧名文本
|
||
conflict.text = target.text;
|
||
conflict.updatedAt = now;
|
||
this.deletePreset(oldName);
|
||
} else {
|
||
target.name = newName;
|
||
target.updatedAt = now;
|
||
}
|
||
this.save();
|
||
},
|
||
findGroupById(id: string): PromptGroup | null {
|
||
for (const cat of this.dataset?.categories || []) {
|
||
const grp = cat.groups.find((g) => g.id === id);
|
||
if (grp) return grp;
|
||
}
|
||
return null;
|
||
},
|
||
// 工具方法:构建差异与应用差异
|
||
buildDiff(base: PromptDataset, cur: PromptDataset): CustomDiff {
|
||
const categories: CustomDiff['categories'] = [];
|
||
const baseCatMap = new Map(base.categories.map((c) => [c.name, c]));
|
||
const curCatMap = new Map(cur.categories.map((c) => [c.name, c]));
|
||
|
||
for (const [name, curCat] of curCatMap.entries()) {
|
||
const baseCat = baseCatMap.get(name);
|
||
const catDiff: CustomDiff['categories'][number] = { name };
|
||
if (!baseCat) {
|
||
catDiff.addedGroups = curCat.groups.map((g) => deepClone(g));
|
||
} else {
|
||
const baseGrpMap = new Map(baseCat.groups.map((g) => [g.name, g]));
|
||
const curGrpMap = new Map(curCat.groups.map((g) => [g.name, g]));
|
||
const groupsDiff: NonNullable<typeof catDiff.groups> = [];
|
||
|
||
for (const [gname, curGrp] of curGrpMap.entries()) {
|
||
const baseGrp = baseGrpMap.get(gname);
|
||
if (!baseGrp) {
|
||
if (!catDiff.addedGroups) catDiff.addedGroups = [];
|
||
catDiff.addedGroups.push(deepClone(curGrp));
|
||
continue;
|
||
}
|
||
const updated: { name: string; color?: string; added?: PromptTag[]; removed?: string[]; updated?: Array<{ key: string; translation?: Partial<Record<LangCode, string>>; hidden?: boolean }>; order?: string[] } = { name: gname } as any;
|
||
if ((curGrp.color || '') !== (baseGrp.color || '')) updated.color = curGrp.color;
|
||
const baseTagMap = new Map(baseGrp.tags.map((t) => [t.key, t]));
|
||
const curTagMap = new Map(curGrp.tags.map((t) => [t.key, t]));
|
||
|
||
const addList: PromptTag[] = [];
|
||
const updList: Array<{ key: string; translation?: Partial<Record<LangCode, string>>; hidden?: boolean }> = [];
|
||
for (const [key, curTag] of curTagMap.entries()) {
|
||
const baseTag = baseTagMap.get(key);
|
||
if (!baseTag) {
|
||
addList.push(deepClone(curTag));
|
||
} else {
|
||
let changed = false;
|
||
const change: { key: string; translation?: Partial<Record<LangCode, string>>; hidden?: boolean } = { key };
|
||
for (const l of ['en', 'zh_CN', 'es_ES'] as LangCode[]) {
|
||
const a = baseTag.translation?.[l] ?? '';
|
||
const b = curTag.translation?.[l] ?? '';
|
||
if (a !== b) {
|
||
if (!change.translation) change.translation = {};
|
||
change.translation[l] = b;
|
||
changed = true;
|
||
}
|
||
}
|
||
const aHidden = !!baseTag.hidden;
|
||
const bHidden = !!curTag.hidden;
|
||
if (aHidden !== bHidden) { change.hidden = bHidden; changed = true; }
|
||
if (changed) updList.push(change);
|
||
}
|
||
}
|
||
const remList: string[] = [];
|
||
for (const [key] of baseTagMap.entries()) {
|
||
if (!curTagMap.has(key)) remList.push(key);
|
||
}
|
||
const baseOrder = baseGrp.tags.map((t) => t.key);
|
||
const curOrder = curGrp.tags.map((t) => t.key);
|
||
const orderChanged = baseOrder.length !== curOrder.length || baseOrder.some((k, i) => k !== curOrder[i]);
|
||
|
||
if (updated.color || addList.length || remList.length || updList.length || orderChanged) {
|
||
if (updated.color) updated.color = updated.color;
|
||
if (addList.length) updated.added = addList;
|
||
if (remList.length) updated.removed = remList;
|
||
if (updList.length) updated.updated = updList;
|
||
if (orderChanged) updated.order = curOrder;
|
||
groupsDiff.push(updated as any);
|
||
}
|
||
}
|
||
const removedGroups = baseCat.groups.filter((bg) => !curGrpMap.has(bg.name)).map((g) => g.name);
|
||
if (removedGroups.length) catDiff.removedGroups = removedGroups;
|
||
if (groupsDiff.length) catDiff.groups = groupsDiff;
|
||
}
|
||
if (catDiff.addedGroups?.length || catDiff.removedGroups?.length || catDiff.groups?.length) {
|
||
categories.push(catDiff);
|
||
}
|
||
}
|
||
for (const baseCat of base.categories) {
|
||
if (!curCatMap.has(baseCat.name)) {
|
||
categories.push({ name: baseCat.name, removedGroups: baseCat.groups.map((g) => g.name) });
|
||
}
|
||
}
|
||
return { categories };
|
||
},
|
||
applyDiff(target: PromptDataset, diff: CustomDiff): PromptDataset {
|
||
const catMap = new Map(target.categories.map((c) => [c.name, c]));
|
||
for (const c of diff.categories || []) {
|
||
let cat = catMap.get(c.name);
|
||
if (!cat) {
|
||
cat = { id: `cat_${Math.random().toString(36).slice(2, 9)}`, name: c.name, groups: [] };
|
||
target.categories.push(cat);
|
||
catMap.set(c.name, cat);
|
||
}
|
||
const grpMap = new Map(cat.groups.map((g) => [g.name, g]));
|
||
for (const g of c.addedGroups || []) {
|
||
const copy = deepClone(g);
|
||
// 保持现有结构:如果没有 id,生成一个
|
||
(copy as PromptGroup).id = `grp_${Math.random().toString(36).slice(2, 9)}`;
|
||
(copy as PromptGroup).id = `grp_${Math.random().toString(36).slice(2, 9)}`;
|
||
cat.groups.push(copy as PromptGroup);
|
||
grpMap.set(copy.name, copy as PromptGroup);
|
||
}
|
||
for (const gname of c.removedGroups || []) {
|
||
cat.groups = cat.groups.filter((g) => g.name !== gname);
|
||
grpMap.delete(gname);
|
||
}
|
||
for (const gdiff of c.groups || []) {
|
||
let grp = grpMap.get(gdiff.name);
|
||
if (!grp) {
|
||
grp = { id: `grp_${Math.random().toString(36).slice(2, 9)}`, name: gdiff.name, color: gdiff.color, tags: [] };
|
||
cat.groups.push(grp);
|
||
grpMap.set(grp.name, grp);
|
||
}
|
||
if (gdiff.color !== undefined) grp.color = gdiff.color;
|
||
const tagMap = new Map(grp.tags.map((t) => [t.key, t]));
|
||
for (const t of gdiff.added || []) {
|
||
const copy = deepClone(t);
|
||
grp.tags.push(copy);
|
||
tagMap.set(copy.key, copy);
|
||
}
|
||
for (const key of gdiff.removed || []) {
|
||
grp.tags = grp.tags.filter((t) => t.key !== key);
|
||
tagMap.delete(key);
|
||
}
|
||
for (const u of gdiff.updated || []) {
|
||
const tag = tagMap.get(u.key);
|
||
if (!tag) continue;
|
||
if (u.translation) tag.translation = { ...(tag.translation || {}), ...u.translation };
|
||
if (u.hidden !== undefined) tag.hidden = u.hidden;
|
||
}
|
||
if (gdiff.order && gdiff.order.length) {
|
||
const keyToTag = new Map(grp.tags.map((t) => [t.key, t]));
|
||
const reordered: PromptTag[] = [];
|
||
for (const key of gdiff.order) {
|
||
const tag = keyToTag.get(key);
|
||
if (tag) reordered.push(tag);
|
||
}
|
||
// 保留未列出的条目
|
||
for (const t of grp.tags) {
|
||
if (!gdiff.order.includes(t.key)) reordered.push(t);
|
||
}
|
||
grp.tags = reordered;
|
||
}
|
||
}
|
||
}
|
||
return target;
|
||
},
|
||
|
||
// 扩展预设管理方法
|
||
initializeExtendedPresets() {
|
||
// 如果没有扩展预设数据,初始化默认结构
|
||
if (!this.extendedPresets) {
|
||
this.extendedPresets = [];
|
||
}
|
||
if (!this.presetFolders) {
|
||
this.presetFolders = [];
|
||
}
|
||
if (!this.presetManagement) {
|
||
this.presetManagement = {
|
||
folders: [],
|
||
presets: [],
|
||
settings: {
|
||
autoBackup: true,
|
||
maxPresets: 1000
|
||
}
|
||
};
|
||
}
|
||
|
||
// 确保有默认文件夹
|
||
if (this.presetFolders.length === 0) {
|
||
const defaultFolder = this.createPresetFolder({
|
||
name: '默认文件夹',
|
||
description: '系统默认预设文件夹',
|
||
color: '#6366f1'
|
||
});
|
||
|
||
// 设置为默认文件夹
|
||
if (this.presetManagement.settings) {
|
||
this.presetManagement.settings.defaultFolder = defaultFolder.id;
|
||
}
|
||
}
|
||
},
|
||
|
||
createExtendedPreset(data: Omit<ExtendedPreset, 'id' | 'createdAt' | 'updatedAt'>) {
|
||
const now = new Date().toISOString();
|
||
const preset: ExtendedPreset = {
|
||
id: `preset_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
|
||
createdAt: now,
|
||
updatedAt: now,
|
||
...data
|
||
};
|
||
|
||
this.extendedPresets.push(preset);
|
||
this.save();
|
||
return preset;
|
||
},
|
||
|
||
updateExtendedPreset(id: string, data: Partial<Omit<ExtendedPreset, 'id' | 'createdAt'>>) {
|
||
const preset = this.extendedPresets.find(p => p.id === id);
|
||
if (!preset) return false;
|
||
|
||
Object.assign(preset, data, { updatedAt: new Date().toISOString() });
|
||
this.save();
|
||
return true;
|
||
},
|
||
|
||
deleteExtendedPreset(id: string) {
|
||
const index = this.extendedPresets.findIndex(p => p.id === id);
|
||
if (index === -1) return false;
|
||
|
||
this.extendedPresets.splice(index, 1);
|
||
this.save();
|
||
return true;
|
||
},
|
||
|
||
createPresetFolder(data: Omit<PresetFolder, 'id' | 'createdAt' | 'updatedAt'>) {
|
||
const now = new Date().toISOString();
|
||
const folder: PresetFolder = {
|
||
id: `folder_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
|
||
createdAt: now,
|
||
updatedAt: now,
|
||
...data
|
||
};
|
||
|
||
this.presetFolders.push(folder);
|
||
this.save();
|
||
return folder;
|
||
},
|
||
|
||
updatePresetFolder(id: string, data: Partial<Omit<PresetFolder, 'id' | 'createdAt'>>) {
|
||
const folder = this.presetFolders.find(f => f.id === id);
|
||
if (!folder) return false;
|
||
|
||
Object.assign(folder, data, { updatedAt: new Date().toISOString() });
|
||
this.save();
|
||
return true;
|
||
},
|
||
|
||
deletePresetFolder(id: string) {
|
||
const index = this.presetFolders.findIndex(f => f.id === id);
|
||
if (index === -1) return false;
|
||
|
||
// 将该文件夹下的预设移动到未分类
|
||
this.extendedPresets.forEach(preset => {
|
||
if (preset.folderId === id) {
|
||
preset.folderId = undefined;
|
||
preset.updatedAt = new Date().toISOString();
|
||
}
|
||
});
|
||
|
||
// 删除子文件夹或将其移动到父级
|
||
const folder = this.presetFolders[index];
|
||
if (folder) {
|
||
this.presetFolders.forEach(f => {
|
||
if (f.parentId === id) {
|
||
f.parentId = folder.parentId;
|
||
f.updatedAt = new Date().toISOString();
|
||
}
|
||
});
|
||
}
|
||
|
||
this.presetFolders.splice(index, 1);
|
||
this.save();
|
||
return true;
|
||
},
|
||
|
||
importExtendedPresets(data: { folders?: PresetFolder[]; presets?: ExtendedPreset[] }) {
|
||
if (data.folders) {
|
||
// 合并文件夹,避免ID冲突
|
||
data.folders.forEach(folder => {
|
||
const existingFolder = this.presetFolders.find(f => f.name === folder.name);
|
||
if (existingFolder) {
|
||
// 更新现有文件夹
|
||
Object.assign(existingFolder, folder, {
|
||
id: existingFolder.id,
|
||
updatedAt: new Date().toISOString()
|
||
});
|
||
} else {
|
||
// 创建新文件夹,重新生成ID
|
||
const newFolder = {
|
||
...folder,
|
||
id: `folder_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
|
||
createdAt: new Date().toISOString(),
|
||
updatedAt: new Date().toISOString()
|
||
};
|
||
this.presetFolders.push(newFolder);
|
||
}
|
||
});
|
||
}
|
||
|
||
if (data.presets) {
|
||
// 合并预设,避免ID冲突
|
||
data.presets.forEach(preset => {
|
||
const existingPreset = this.extendedPresets.find(p => p.name === preset.name && p.type === preset.type);
|
||
if (existingPreset) {
|
||
// 更新现有预设
|
||
Object.assign(existingPreset, preset, {
|
||
id: existingPreset.id,
|
||
updatedAt: new Date().toISOString()
|
||
});
|
||
} else {
|
||
// 创建新预设,重新生成ID
|
||
const newPreset = {
|
||
...preset,
|
||
id: `preset_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
|
||
createdAt: new Date().toISOString(),
|
||
updatedAt: new Date().toISOString()
|
||
};
|
||
this.extendedPresets.push(newPreset);
|
||
}
|
||
});
|
||
}
|
||
|
||
this.save();
|
||
},
|
||
|
||
// 兼容性:从旧预设迁移到新预设系统
|
||
migrateOldPresets() {
|
||
if (this.presets.length === 0) return; // 没有旧预设需要迁移
|
||
|
||
console.log(`开始迁移 ${this.presets.length} 个旧预设到新系统...`);
|
||
|
||
let migratedCount = 0;
|
||
const defaultFolder = this.presetManagement?.settings?.defaultFolder;
|
||
|
||
this.presets.forEach(oldPreset => {
|
||
const existingExtended = this.extendedPresets.find(p =>
|
||
p.name === oldPreset.name && p.type === 'positive'
|
||
);
|
||
|
||
if (!existingExtended) {
|
||
// 创建新的扩展预设,但不触发保存(避免递归)
|
||
const now = new Date().toISOString();
|
||
const preset: ExtendedPreset = {
|
||
id: `migrated_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
|
||
name: oldPreset.name,
|
||
type: 'positive',
|
||
content: oldPreset.text,
|
||
description: '从旧版快速预设自动迁移',
|
||
folderId: defaultFolder,
|
||
createdAt: oldPreset.updatedAt,
|
||
updatedAt: oldPreset.updatedAt
|
||
};
|
||
|
||
this.extendedPresets.push(preset);
|
||
migratedCount++;
|
||
}
|
||
});
|
||
|
||
if (migratedCount > 0) {
|
||
console.log(`成功迁移 ${migratedCount} 个预设到新系统`);
|
||
// 清空旧预设数组,完成迁移
|
||
this.presets = [];
|
||
console.log('旧预设已清空,迁移完成');
|
||
}
|
||
},
|
||
|
||
// 检查并处理版本兼容性
|
||
ensureCompatibility(bundle: ExportBundle) {
|
||
// 如果是旧版本的导出文件,确保新字段存在
|
||
if (!bundle.extendedPresets && bundle.presets && bundle.presets.length > 0) {
|
||
// 自动迁移旧预设到新系统
|
||
bundle.extendedPresets = bundle.presets.map(preset => ({
|
||
id: `migrated_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
|
||
name: preset.name,
|
||
type: 'positive' as PresetType,
|
||
content: preset.text,
|
||
description: '从旧版本自动迁移',
|
||
createdAt: preset.updatedAt,
|
||
updatedAt: preset.updatedAt
|
||
}));
|
||
}
|
||
|
||
if (!bundle.presetFolders) {
|
||
bundle.presetFolders = [];
|
||
}
|
||
|
||
if (!bundle.presetManagement) {
|
||
bundle.presetManagement = {
|
||
folders: [],
|
||
presets: [],
|
||
settings: {
|
||
autoBackup: true,
|
||
maxPresets: 1000
|
||
}
|
||
};
|
||
}
|
||
|
||
return bundle;
|
||
},
|
||
},
|
||
});
|
||
// —— 工具方法 ——
|
||
function splitTokens(text: string): string[] {
|
||
return text
|
||
.split(/[,,]/)
|
||
.map((s) => s.trim())
|
||
.filter((s) => s.length > 0);
|
||
}
|
||
function normalizeToken(t: string): string {
|
||
return t.trim();
|
||
}
|
||
function normalizePrompt(text: string): string {
|
||
return splitTokens(text).join(', ');
|
||
}
|
||
|
||
// 归一化用于匹配的 key:统一大小写与下划线/空格
|
||
function normalizeKeyForMatch(s: string): string {
|
||
return s.trim().toLowerCase().replace(/_/g, ' ');
|
||
} |