提示词编辑强化,支持归一化符号,支持嵌套组处理,鲁棒性增强
This commit is contained in:
+172
-40
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
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 NotificationToast from './NotificationToast.vue';
|
||||
import PresetDropdown from './PresetDropdown.vue';
|
||||
@@ -135,10 +135,12 @@ function currentEditEl(): HTMLInputElement | null {
|
||||
const priorityStyle = ref<'{}' | '()' | '[]' | '<>' | 'suffix'>('{}');
|
||||
const priorityStep = ref(1);
|
||||
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) {
|
||||
const el = inputEl.value;
|
||||
if (!el) { text.value = newText; return; }
|
||||
@@ -207,7 +209,10 @@ function updateSuggestions() {
|
||||
const rightCandidates = [rightCommaEn, rightCommaCn].filter(i => i !== -1);
|
||||
const right = rightCandidates.length ? Math.min(...rightCandidates) : txt.length;
|
||||
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() {
|
||||
@@ -221,7 +226,9 @@ function updateEditSuggestions() {
|
||||
const before = val.slice(0, pos);
|
||||
const match = before.match(/[^,,]*$/);
|
||||
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)基于光标位置的片段替换范围(修剪前后空格)
|
||||
@@ -275,21 +282,37 @@ async function onKeyDown(e: KeyboardEvent) {
|
||||
const el = inputEl.value;
|
||||
if (!el) return;
|
||||
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 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) {
|
||||
e.preventDefault();
|
||||
const s = list[0];
|
||||
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();
|
||||
updateSuggestions();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function copyLeft() {
|
||||
@@ -300,26 +323,82 @@ async function copyLeft() {
|
||||
showNotification('复制失败,请手动复制', 'error');
|
||||
}
|
||||
}
|
||||
function replaceCnComma() { applyFullPrompt(text.value.replace(/,/g, ',')); }
|
||||
function replaceCnComma() { applyFullPrompt(normalizeSymbols(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() {
|
||||
const tokens = splitTokensLocal(text.value);
|
||||
const newTokens = tokens.map(token => {
|
||||
const { core, wrappers } = store.parseTokenWrappers(token);
|
||||
let newCore;
|
||||
if (core.includes('_')) {
|
||||
newCore = core.replace(/_/g, ' ');
|
||||
} else if (core.includes(' ')) {
|
||||
newCore = core.replace(/ /g, '_');
|
||||
} else {
|
||||
newCore = core;
|
||||
const tokens = splitTokens(text.value);
|
||||
|
||||
// 1. 统计全局倾向
|
||||
let spaceCount = 0;
|
||||
let underscoreCount = 0;
|
||||
|
||||
// 预解析所有 Token
|
||||
const parsedList = tokens.map(t => parseDetailedToken(t));
|
||||
|
||||
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(', '));
|
||||
showNotification('已切换下划线/空格格式', 'success');
|
||||
showNotification(targetIsUnderscore ? '已统一为下划线格式' : '已统一为空格格式', 'success');
|
||||
}
|
||||
|
||||
function addWrapperToToken(index: number) {
|
||||
@@ -623,8 +702,21 @@ async function applySuggestion(s: string) {
|
||||
el.focus();
|
||||
const pos = el.selectionStart ?? store.promptText.length;
|
||||
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();
|
||||
updateSuggestions();
|
||||
}
|
||||
@@ -653,9 +745,31 @@ function applyEditSuggestion(s: string) {
|
||||
// 保持焦点在编辑输入上
|
||||
el.focus();
|
||||
const val = editingValue.value || '';
|
||||
// 直接替换整个输入为建议,保证撤回可用
|
||||
applyTextReplacement(el, 0, val.length, s);
|
||||
const pos = el.selectionStart ?? val.length;
|
||||
|
||||
// 智能替换逻辑 (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();
|
||||
});
|
||||
}
|
||||
|
||||
const unmappedTokens = computed(() => {
|
||||
@@ -698,13 +812,23 @@ async function autoTranslateSingle() {
|
||||
}
|
||||
|
||||
function displayTrans(key: string): string {
|
||||
const { core, wrappers } = store.parseTokenWrappers(key);
|
||||
const m = core.match(/:(\d+(?:\.\d+)?)$/);
|
||||
const base = m ? core.slice(0, core.lastIndexOf(':')) : core;
|
||||
const suffix = m ? ':' + m[1]! : '';
|
||||
const tag = store.getTagByKey(base);
|
||||
const translatedCore = tag?.translation?.[selectedLang.value] ?? tag?.key ?? base;
|
||||
return store.wrapToken(translatedCore + suffix, wrappers);
|
||||
const { core, weight, wrappers } = parseDetailedToken(key);
|
||||
const tag = store.getTagByKey(core);
|
||||
const translatedCore = tag?.translation?.[selectedLang.value] ?? tag?.key ?? core;
|
||||
|
||||
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 store.wrapToken(result, currentWrappers);
|
||||
}
|
||||
|
||||
function isRemoveDisabled(token: string): boolean {
|
||||
@@ -796,14 +920,14 @@ function isRemoveDisabled(token: string): boolean {
|
||||
placeholder="例如:1girl, aaa, bbb, ccc"
|
||||
></textarea>
|
||||
<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">
|
||||
<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"/>
|
||||
<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"/>
|
||||
</svg>
|
||||
替换中文逗号
|
||||
归一化符号
|
||||
</button>
|
||||
<button @click="formatPrompt" title="格式化提示词为标准格式">
|
||||
<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="12" y1="4" x2="12" y2="20" stroke="currentColor" stroke-width="2"/>
|
||||
</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 @click="toggleUnderscoreSpace" title="切换下划线和空格格式">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
+201
-12
@@ -327,11 +327,55 @@ export const usePromptStore = defineStore('promptStore', {
|
||||
this.promptText = text;
|
||||
},
|
||||
replaceChineseComma() {
|
||||
this.promptText = this.promptText.replace(/,/g, ',');
|
||||
this.promptText = normalizeSymbols(this.promptText);
|
||||
},
|
||||
formatPrompt() {
|
||||
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() {
|
||||
const tokens = splitTokens(this.promptText);
|
||||
@@ -473,13 +517,26 @@ export const usePromptStore = defineStore('promptStore', {
|
||||
return tagNormIndex.get(target) || null;
|
||||
},
|
||||
getTranslation(key: string, lang: LangCode): string | null {
|
||||
// 兼容包裹层:如 {aaa}、(aaa) 等
|
||||
const { core, wrappers } = this.parseTokenWrappers(key);
|
||||
// 兼容包裹层:如 {aaa}、(aaa) 等,以及复杂权重 (aaa:1.2)
|
||||
const { core, wrappers, weight } = parseDetailedToken(key);
|
||||
const tag = this.getTagByKey(core);
|
||||
if (!tag) return null;
|
||||
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[] {
|
||||
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
|
||||
.split(/[,,]/)
|
||||
.map((s) => s.trim())
|
||||
.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(/“/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:统一大小写与下划线/空格
|
||||
function normalizeKeyForMatch(s: string): string {
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user