提示词编辑强化,支持归一化符号,支持嵌套组处理,鲁棒性增强

This commit is contained in:
2025-11-30 11:01:33 +08:00
parent 15af40f92f
commit 8e601ddfef
2 changed files with 381 additions and 60 deletions
+172 -40
View File
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, onUnmounted, ref, computed, nextTick, watch } from 'vue'; import { onMounted, onUnmounted, ref, computed, nextTick, watch } from 'vue';
import { usePromptStore } from '../stores/promptStore'; import { usePromptStore, splitTokens, normalizeSymbols, parseDetailedToken, constructToken } from '../stores/promptStore';
import type { LangCode, PresetFolder } from '../types'; import type { LangCode, PresetFolder } from '../types';
import NotificationToast from './NotificationToast.vue'; import NotificationToast from './NotificationToast.vue';
import PresetDropdown from './PresetDropdown.vue'; import PresetDropdown from './PresetDropdown.vue';
@@ -135,10 +135,12 @@ function currentEditEl(): HTMLInputElement | null {
const priorityStyle = ref<'{}' | '()' | '[]' | '<>' | 'suffix'>('{}'); const priorityStyle = ref<'{}' | '()' | '[]' | '<>' | 'suffix'>('{}');
const priorityStep = ref(1); const priorityStep = ref(1);
function splitTokensLocal(txt: string): string[] { function splitTokensLocal(txt: string): string[] {
return txt.split(/[,]/).map(s => s.trim()).filter(s => s.length > 0); return splitTokens(txt);
}
function normalizeToken(t: string): string { return t.trim().replace(/\s+/g, ' '); }
function normalizePromptLocal(txt: string): string {
return splitTokens(txt).map(t => t.replace(/\s+/g, ' ')).join(', ');
} }
function normalizeToken(t: string): string { return t.trim(); }
function normalizePromptLocal(txt: string): string { return splitTokensLocal(txt).join(', '); }
function applyFullPrompt(newText: string) { function applyFullPrompt(newText: string) {
const el = inputEl.value; const el = inputEl.value;
if (!el) { text.value = newText; return; } if (!el) { text.value = newText; return; }
@@ -207,7 +209,10 @@ function updateSuggestions() {
const rightCandidates = [rightCommaEn, rightCommaCn].filter(i => i !== -1); const rightCandidates = [rightCommaEn, rightCommaCn].filter(i => i !== -1);
const right = rightCandidates.length ? Math.min(...rightCandidates) : txt.length; const right = rightCandidates.length ? Math.min(...rightCandidates) : txt.length;
const segment = txt.slice(left < 0 ? 0 : left + 1, right).trim(); const segment = txt.slice(left < 0 ? 0 : left + 1, right).trim();
suggestions.value = store.getSuggestions(segment, 8); const { core } = parseDetailedToken(segment);
// 去除 core 前后可能残留的符号(针对未闭合情况,如 "(aa" -> "aa"
const cleanCore = core.replace(/^[\(\[\{<]+/, '').replace(/[\)\]\}>]+$/, '');
suggestions.value = store.getSuggestions(cleanCore, 8);
} }
function updateEditSuggestions() { function updateEditSuggestions() {
@@ -221,7 +226,9 @@ function updateEditSuggestions() {
const before = val.slice(0, pos); const before = val.slice(0, pos);
const match = before.match(/[^,]*$/); const match = before.match(/[^,]*$/);
const prefix = (match ? match[0] : before).trim(); const prefix = (match ? match[0] : before).trim();
editSuggestions.value = store.getSuggestions(prefix, 8); const { core } = parseDetailedToken(prefix);
const cleanCore = core.replace(/^[\(\[\{<]+/, '').replace(/[\)\]\}>]+$/, '');
editSuggestions.value = store.getSuggestions(cleanCore, 8);
} }
// 计算左侧输入(textarea)基于光标位置的片段替换范围(修剪前后空格) // 计算左侧输入(textarea)基于光标位置的片段替换范围(修剪前后空格)
@@ -275,21 +282,37 @@ async function onKeyDown(e: KeyboardEvent) {
const el = inputEl.value; const el = inputEl.value;
if (!el) return; if (!el) return;
const pos = el.selectionStart ?? store.promptText.length; const pos = el.selectionStart ?? store.promptText.length;
const before = store.promptText.slice(0, pos);
const match = before.match(/[^,]*$/);
const prefix = (match ? match[0] : '').trim();
const { start, end } = getTextSegmentBounds(store.promptText, pos); const { start, end } = getTextSegmentBounds(store.promptText, pos);
const list = store.getSuggestions(prefix, 8); const segment = store.promptText.slice(start, end);
const { core } = parseDetailedToken(segment);
const cleanCore = core.replace(/^[\(\[\{<]+/, '').replace(/[\)\]\}>]+$/, '');
const list = store.getSuggestions(cleanCore, 8);
if (list.length > 0) { if (list.length > 0) {
e.preventDefault(); e.preventDefault();
const s = list[0]; const s = list[0];
if (!s) return; if (!s) return;
// 使用原生插入或回退方案,确保撤回可用
applyTextReplacement(el, start, end, s); // 智能替换:保留包裹层和权重
const { weight, wrappers } = parseDetailedToken(segment);
// 即使 parseDetailedToken 没解析出 wrapper (如未闭合情况),我们也尝试保留非 core 部分?
// 目前策略:如果 parseDetailedToken 能解析出结构,则完美重构。
// 如果是未闭合如 "(aa"wrappers 为空,core 为 "(aa"cleanCore 为 "aa"。
// 此时如果直接用 constructToken("aaa", undefined, []) -> "aaa",会丢失 "("。
// 针对未闭合情况的特殊处理:
let newToken = '';
if (wrappers.length === 0 && weight === undefined && segment !== cleanCore) {
// 简单替换核心部分
newToken = segment.replace(cleanCore, s);
} else {
newToken = constructToken(s, weight, wrappers);
}
applyTextReplacement(el, start, end, newToken);
await nextTick(); await nextTick();
updateSuggestions(); updateSuggestions();
} }
} }
} }
async function copyLeft() { async function copyLeft() {
@@ -300,26 +323,82 @@ async function copyLeft() {
showNotification('复制失败,请手动复制', 'error'); showNotification('复制失败,请手动复制', 'error');
} }
} }
function replaceCnComma() { applyFullPrompt(text.value.replace(//g, ',')); } function replaceCnComma() { applyFullPrompt(normalizeSymbols(text.value)); }
function formatPrompt() { applyFullPrompt(normalizePromptLocal(text.value)); } function formatPrompt() { applyFullPrompt(normalizePromptLocal(text.value)); }
function unifyPriorityStyle() {
const tokens = splitTokens(text.value);
const processed = tokens.map(token => {
const { core, weight, wrappers } = parseDetailedToken(token);
let result = core;
let currentWrappers = [...wrappers];
if (weight !== undefined && weight !== 1) {
const lastWrapper = currentWrappers[currentWrappers.length - 1];
if (lastWrapper === '()') {
currentWrappers.pop();
}
const wStr = Number.isInteger(weight) ? weight.toString() : weight.toFixed(2).replace(/\.?0+$/, '');
result = `(${result}:${wStr})`;
}
return store.wrapToken(result, currentWrappers);
});
applyFullPrompt(processed.join(', '));
showNotification('已统一优先级样式', 'success');
}
// 新增功能方法 // 新增功能方法
function toggleUnderscoreSpace() { function toggleUnderscoreSpace() {
const tokens = splitTokensLocal(text.value); const tokens = splitTokens(text.value);
const newTokens = tokens.map(token => {
const { core, wrappers } = store.parseTokenWrappers(token); // 1. 统计全局倾向
let newCore; let spaceCount = 0;
if (core.includes('_')) { let underscoreCount = 0;
newCore = core.replace(/_/g, ' ');
} else if (core.includes(' ')) { // 预解析所有 Token
newCore = core.replace(/ /g, '_'); const parsedList = tokens.map(t => parseDetailedToken(t));
} else {
newCore = core; parsedList.forEach(({ core }) => {
for (const char of core) {
if (char === ' ') spaceCount++;
if (char === '_') underscoreCount++;
} }
return store.wrapToken(newCore, wrappers);
}); });
// 2. 确定目标格式
// 逻辑:统一成“非优势”的一方。
// 如果下划线更多(或相等),则统一变成空格(通常是为了可读性)。
// 如果空格更多,则统一变成下划线(通常是为了作为 Tag 使用)。
const targetIsUnderscore = spaceCount > underscoreCount;
const newTokens = parsedList.map(({ core, weight, wrappers }) => {
let newCore = core;
if (targetIsUnderscore) {
newCore = newCore.replace(/ /g, '_');
} else {
newCore = newCore.replace(/_/g, ' ');
}
// 重构 Token (保持权重和包裹层)
let result = newCore;
let currentWrappers = [...wrappers];
if (weight !== undefined && weight !== 1) {
const lastWrapper = currentWrappers[currentWrappers.length - 1];
if (lastWrapper === '()') {
currentWrappers.pop();
}
const wStr = Number.isInteger(weight) ? weight.toString() : weight.toFixed(2).replace(/\.?0+$/, '');
result = `(${result}:${wStr})`;
}
return store.wrapToken(result, currentWrappers);
});
applyFullPrompt(newTokens.join(', ')); applyFullPrompt(newTokens.join(', '));
showNotification('已切换下划线/空格格式', 'success'); showNotification(targetIsUnderscore ? '已统一为下划线格式' : '已统一为空格格式', 'success');
} }
function addWrapperToToken(index: number) { function addWrapperToToken(index: number) {
@@ -623,8 +702,21 @@ async function applySuggestion(s: string) {
el.focus(); el.focus();
const pos = el.selectionStart ?? store.promptText.length; const pos = el.selectionStart ?? store.promptText.length;
const { start, end } = getTextSegmentBounds(store.promptText, pos); const { start, end } = getTextSegmentBounds(store.promptText, pos);
// 使用原生插入或回退方式替换片段,确保撤回可用
applyTextReplacement(el, start, end, s); // 智能替换逻辑
const segment = store.promptText.slice(start, end);
const { core, weight, wrappers } = parseDetailedToken(segment);
const cleanCore = core.replace(/^[\(\[\{<]+/, '').replace(/[\)\]\}>]+$/, '');
let newToken = '';
if (wrappers.length === 0 && weight === undefined && segment !== cleanCore) {
// 简单替换核心部分 (针对未闭合情况)
newToken = segment.replace(cleanCore, s);
} else {
newToken = constructToken(s, weight, wrappers);
}
applyTextReplacement(el, start, end, newToken);
await nextTick(); await nextTick();
updateSuggestions(); updateSuggestions();
} }
@@ -653,9 +745,31 @@ function applyEditSuggestion(s: string) {
// 保持焦点在编辑输入上 // 保持焦点在编辑输入上
el.focus(); el.focus();
const val = editingValue.value || ''; const val = editingValue.value || '';
// 直接替换整个输入为建议,保证撤回可用 const pos = el.selectionStart ?? val.length;
applyTextReplacement(el, 0, val.length, s);
// 智能替换逻辑 (Compact Mode)
const { core, weight, wrappers } = parseDetailedToken(val);
const cleanCore = core.replace(/^[\(\[\{<]+/, '').replace(/[\)\]\}>]+$/, '');
let newVal = '';
if (wrappers.length === 0 && weight === undefined && val !== cleanCore) {
// 简单替换核心部分 (针对未闭合情况)
newVal = val.replace(cleanCore, s);
} else {
newVal = constructToken(s, weight, wrappers);
}
editingValue.value = newVal;
nextTick(() => {
// 光标移动到插入词后
const newCoreIndex = newVal.indexOf(s);
if (newCoreIndex !== -1) {
el.setSelectionRange(newCoreIndex + s.length, newCoreIndex + s.length);
} else {
el.setSelectionRange(newVal.length, newVal.length);
}
updateEditSuggestions(); updateEditSuggestions();
});
} }
const unmappedTokens = computed(() => { const unmappedTokens = computed(() => {
@@ -698,13 +812,23 @@ async function autoTranslateSingle() {
} }
function displayTrans(key: string): string { function displayTrans(key: string): string {
const { core, wrappers } = store.parseTokenWrappers(key); const { core, weight, wrappers } = parseDetailedToken(key);
const m = core.match(/:(\d+(?:\.\d+)?)$/); const tag = store.getTagByKey(core);
const base = m ? core.slice(0, core.lastIndexOf(':')) : core; const translatedCore = tag?.translation?.[selectedLang.value] ?? tag?.key ?? core;
const suffix = m ? ':' + m[1]! : '';
const tag = store.getTagByKey(base); let result = translatedCore;
const translatedCore = tag?.translation?.[selectedLang.value] ?? tag?.key ?? base; let currentWrappers = [...wrappers];
return store.wrapToken(translatedCore + suffix, wrappers);
if (weight !== undefined && weight !== 1) {
const lastWrapper = currentWrappers[currentWrappers.length - 1];
if (lastWrapper === '()') {
currentWrappers.pop();
}
const wStr = Number.isInteger(weight) ? weight.toString() : weight.toFixed(2).replace(/\.?0+$/, '');
result = `(${result}:${wStr})`;
}
return store.wrapToken(result, currentWrappers);
} }
function isRemoveDisabled(token: string): boolean { function isRemoveDisabled(token: string): boolean {
@@ -796,14 +920,14 @@ function isRemoveDisabled(token: string): boolean {
placeholder="例如:1girl, aaa, bbb, ccc" placeholder="例如:1girl, aaa, bbb, ccc"
></textarea> ></textarea>
<div class="pe-input-actions"> <div class="pe-input-actions">
<button @click="replaceCnComma" title="将中文逗号替换为英文号"> <button @click="replaceCnComma" title="将中文逗号、括号等替换为英文号">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" stroke="currentColor" stroke-width="2"/> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" stroke="currentColor" stroke-width="2"/>
<polyline points="14,2 14,8 20,8" stroke="currentColor" stroke-width="2"/> <polyline points="14,2 14,8 20,8" stroke="currentColor" stroke-width="2"/>
<line x1="16" y1="13" x2="8" y2="13" stroke="currentColor" stroke-width="2"/> <line x1="16" y1="13" x2="8" y2="13" stroke="currentColor" stroke-width="2"/>
<line x1="16" y1="17" x2="8" y2="17" stroke="currentColor" stroke-width="2"/> <line x1="16" y1="17" x2="8" y2="17" stroke="currentColor" stroke-width="2"/>
</svg> </svg>
替换中文逗 归一化符
</button> </button>
<button @click="formatPrompt" title="格式化提示词为标准格式"> <button @click="formatPrompt" title="格式化提示词为标准格式">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -811,7 +935,15 @@ function isRemoveDisabled(token: string): boolean {
<line x1="9" y1="20" x2="15" y2="20" stroke="currentColor" stroke-width="2"/> <line x1="9" y1="20" x2="15" y2="20" stroke="currentColor" stroke-width="2"/>
<line x1="12" y1="4" x2="12" y2="20" stroke="currentColor" stroke-width="2"/> <line x1="12" y1="4" x2="12" y2="20" stroke="currentColor" stroke-width="2"/>
</svg> </svg>
格式化提示词 格式化
</button>
<button @click="unifyPriorityStyle" title="统一优先级样式 (core:weight)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
<path d="M8 12h8" stroke="currentColor" stroke-width="2"/>
<path d="M12 8v8" stroke="currentColor" stroke-width="2"/>
</svg>
统一优先级
</button> </button>
<button @click="toggleUnderscoreSpace" title="切换下划线和空格格式"> <button @click="toggleUnderscoreSpace" title="切换下划线和空格格式">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+201 -12
View File
@@ -327,11 +327,55 @@ export const usePromptStore = defineStore('promptStore', {
this.promptText = text; this.promptText = text;
}, },
replaceChineseComma() { replaceChineseComma() {
this.promptText = this.promptText.replace(//g, ','); this.promptText = normalizeSymbols(this.promptText);
}, },
formatPrompt() { formatPrompt() {
this.promptText = normalizePrompt(this.promptText); this.promptText = normalizePrompt(this.promptText);
}, },
// 统一优先级样式:去除无效括号,转换格式,处理乱码
unifyPriorityStyle() {
const tokens = splitTokens(this.promptText);
const processed = tokens.map(token => {
const { core, weight, wrappers } = parseDetailedToken(token);
// 重构 Token
let result = core;
// 如果有权重,应用标准权重格式 (core:weight)
// 注意:如果原来的 wrappers 里包含用于权重的括号,需要处理
let currentWrappers = [...wrappers];
if (weight !== undefined && weight !== 1) {
// 检查是否需要消耗一个外层括号 (如果是 () 类型)
// 通常 (aaa:1.2) 解析出 wrappers=['()'], core='aaa:1.2' (旧逻辑)
// 新逻辑 parseDetailedToken 会解析出 wrappers=['()'], core='aaa', weight=1.2
// 我们需要保留非权重定义的 wrapper。
// 假设最内层的 () 是权重定义的一部分,我们将其消耗掉,用 (core:weight) 替代
// 简单的策略:先生成 (core:weight),然后应用剩余的 wrappers
// 但我们需要知道原来的 wrappers 是否真的包含权重的括号。
// parseDetailedToken 逻辑:如果解析出了 weight,且 strippedWrappers 包含 (),则认为消耗了一个。
// 这里简单化:如果 weight 存在,我们生成 (core:weight)。
// 如果 original wrappers 包含 (),我们认为其中一个就是这个权重括号。
// 除非是 ((aaa:1.2)) -> wrappers=['()', '()'].
const lastWrapper = currentWrappers[currentWrappers.length - 1];
if (lastWrapper === '()') {
currentWrappers.pop();
}
// 格式化权重,最多保留2位小数
const wStr = Number.isInteger(weight) ? weight.toString() : weight.toFixed(2).replace(/\.?0+$/, '');
result = `(${result}:${wStr})`;
}
// 应用剩余的 wrappers
return this.wrapToken(result, currentWrappers);
});
this.promptText = processed.join(', ');
},
// 切换下划线和空格 // 切换下划线和空格
toggleUnderscoreSpace() { toggleUnderscoreSpace() {
const tokens = splitTokens(this.promptText); const tokens = splitTokens(this.promptText);
@@ -473,13 +517,26 @@ export const usePromptStore = defineStore('promptStore', {
return tagNormIndex.get(target) || null; return tagNormIndex.get(target) || null;
}, },
getTranslation(key: string, lang: LangCode): string | null { getTranslation(key: string, lang: LangCode): string | null {
// 兼容包裹层:如 {aaa}、(aaa) 等 // 兼容包裹层:如 {aaa}、(aaa) 等,以及复杂权重 (aaa:1.2)
const { core, wrappers } = this.parseTokenWrappers(key); const { core, wrappers, weight } = parseDetailedToken(key);
const tag = this.getTagByKey(core); const tag = this.getTagByKey(core);
if (!tag) return null; if (!tag) return null;
const translatedCore = tag.translation?.[lang] ?? tag.key; const translatedCore = tag.translation?.[lang] ?? tag.key;
// 保持原有包裹层结构,返回被翻译后的核心 // 保持原有包裹层结构,返回被翻译后的核心
return this.wrapToken(translatedCore, wrappers); let result = translatedCore;
let currentWrappers = [...wrappers];
if (weight !== undefined && weight !== 1) {
const lastWrapper = currentWrappers[currentWrappers.length - 1];
if (lastWrapper === '()') {
currentWrappers.pop();
}
const wStr = Number.isInteger(weight) ? weight.toString() : weight.toFixed(2).replace(/\.?0+$/, '');
result = `(${result}:${wStr})`;
}
return this.wrapToken(result, currentWrappers);
}, },
getSuggestions(prefix: string, limit = 8): string[] { getSuggestions(prefix: string, limit = 8): string[] {
const list: string[] = []; const list: string[] = [];
@@ -1161,20 +1218,152 @@ export const usePromptStore = defineStore('promptStore', {
}, },
}); });
// —— 工具方法 —— // —— 工具方法 ——
function splitTokens(text: string): string[] {
export function normalizeSymbols(text: string): string {
return text return text
.split(/[,]/) .replace(//g, ',')
.map((s) => s.trim()) .replace(/。/g, '.')
.filter((s) => s.length > 0); .replace(//g, ':')
.replace(//g, ';')
.replace(//g, '(')
.replace(//g, ')')
.replace(/【/g, '[')
.replace(/】/g, ']')
.replace(/《/g, '<')
.replace(/》/g, '>')
.replace(/“/g, '"')
.replace(/”/g, '"')
.replace(/\u3000/g, ' ');
} }
function normalizeToken(t: string): string {
return t.trim(); export function splitTokens(text: string): string[] {
// 先归一化符号,确保括号匹配正确
const normalized = normalizeSymbols(text);
const result: string[] = [];
let current = '';
let depth = 0;
for (let i = 0; i < normalized.length; i++) {
const char = normalized[i];
if (char && ['(', '[', '{', '<'].includes(char)) {
depth++;
} else if (char && [')', ']', '}', '>'].includes(char)) {
depth = Math.max(0, depth - 1);
}
// 只有在顶层(depth===0)才分割逗号
if ((char === ',' || char === '\n') && depth === 0) {
if (current.trim()) {
result.push(current.trim());
}
current = '';
} else {
// 换行符转空格
if (char === '\n') {
current += ' ';
} else {
current += char;
}
}
}
if (current.trim()) {
result.push(current.trim());
}
return result;
} }
function normalizePrompt(text: string): string {
return splitTokens(text).join(', '); export function normalizeToken(t: string): string {
return t.trim().replace(/\s+/g, ' ');
}
export function normalizePrompt(text: string): string {
return splitTokens(text).map(normalizeToken).join(', ');
} }
// 归一化用于匹配的 key:统一大小写与下划线/空格 // 归一化用于匹配的 key:统一大小写与下划线/空格
function normalizeKeyForMatch(s: string): string { function normalizeKeyForMatch(s: string): string {
return s.trim().toLowerCase().replace(/_/g, ' '); return s.trim().toLowerCase().replace(/_/g, ' ');
} }
// 解析详细 Token 信息(核心、权重、包裹层)
export function parseDetailedToken(token: string): { core: string; weight?: number; wrappers: string[] } {
// 1. 归一化符号
let current = normalizeSymbols(token).trim();
const wrappers: string[] = [];
const wrapperPairs = [
['{}', '{', '}'],
['()', '(', ')'],
['[]', '[', ']'],
['<>', '<', '>']
];
const hasWrapper = (s: string) => {
for(const [type, start, end] of wrapperPairs) {
if(s && s.startsWith(start!) && s.endsWith(end!)) return { type, start: start!, end: end! };
}
return null;
};
// 2. 剥离外层包裹
while(true) {
const w = hasWrapper(current);
if(w) {
if (w.type) wrappers.push(w.type);
current = current.slice(w.start.length, -w.end.length).trim();
} else {
break;
}
}
let weight: number | undefined;
// 3. 解析核心内容里的权重
// 标准格式 (core:weight) -> 剥离后变成 core:weight
const colonMatch = /^(.+):([0-9.]+)$/.exec(current);
if(colonMatch) {
current = colonMatch[1]?.trim() ?? '';
const w = parseFloat(colonMatch[2]!);
if (!isNaN(w)) weight = w;
} else {
// 自定义格式 (weight :: core) -> 剥离后变成 weight :: core
const customMatch = /^([0-9.]+)\s*::\s*(.+)$/.exec(current);
if(customMatch) {
const w = parseFloat(customMatch[1]!);
if (!isNaN(w)) weight = w;
current = customMatch[2]?.trim() ?? current;
}
}
return { core: current, weight, wrappers };
}
// 重构 Token(将核心内容与权重、包裹层组合)
export function constructToken(core: string, weight: number | undefined, wrappers: string[]): string {
let result = core;
let currentWrappers = [...wrappers];
if (weight !== undefined && weight !== 1) {
// 检查并消耗一个外层 () 作为权重包裹(如果存在)
// 注意:这里采用简化策略,假设最内层的 () 是为了包裹权重而存在的
// 实际上如果原 Token 是 (aaa:1.2),解析得到 wrappers=['()']
// 重构时如果不消耗,会变成 ((aaa:1.2))
const lastWrapper = currentWrappers[currentWrappers.length - 1];
if (lastWrapper === '()') {
currentWrappers.pop();
}
const wStr = Number.isInteger(weight) ? weight.toString() : weight.toFixed(2).replace(/\.?0+$/, '');
result = `(${result}:${wStr})`;
}
// 应用包裹层
for (let i = currentWrappers.length - 1; i >= 0; i--) {
const w = currentWrappers[i];
if (!w) continue;
const start = w.slice(0, w.length / 2);
const end = w.slice(w.length / 2);
result = start + result + end;
}
return result;
}