Move text filters to summary settings and apply to summary generation
This commit is contained in:
@@ -1,56 +1,62 @@
|
|||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
// Story Summary - Config (v2 简化版)
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
import { extension_settings } from "../../../../../../extensions.js";
|
import { extension_settings } from "../../../../../../extensions.js";
|
||||||
import { EXT_ID } from "../../../core/constants.js";
|
import { EXT_ID } from "../../../core/constants.js";
|
||||||
import { xbLog } from "../../../core/debug-core.js";
|
import { xbLog } from "../../../core/debug-core.js";
|
||||||
import { CommonSettingStorage } from "../../../core/server-storage.js";
|
import { CommonSettingStorage } from "../../../core/server-storage.js";
|
||||||
|
|
||||||
const MODULE_ID = 'summaryConfig';
|
const MODULE_ID = "summaryConfig";
|
||||||
const SUMMARY_CONFIG_KEY = 'storySummaryPanelConfig';
|
const SUMMARY_CONFIG_KEY = "storySummaryPanelConfig";
|
||||||
|
|
||||||
|
const DEFAULT_FILTER_RULES = [
|
||||||
|
{ start: "<think>", end: "</think>" },
|
||||||
|
{ start: "<thinking>", end: "</thinking>" },
|
||||||
|
{ start: "```", end: "```" },
|
||||||
|
];
|
||||||
|
|
||||||
export function getSettings() {
|
export function getSettings() {
|
||||||
const ext = extension_settings[EXT_ID] ||= {};
|
const ext = (extension_settings[EXT_ID] ||= {});
|
||||||
ext.storySummary ||= { enabled: true };
|
ext.storySummary ||= { enabled: true };
|
||||||
return ext;
|
return ext;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_FILTER_RULES = [
|
|
||||||
{ start: '<think>', end: '</think>' },
|
|
||||||
{ start: '<thinking>', end: '</thinking>' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export function getSummaryPanelConfig() {
|
export function getSummaryPanelConfig() {
|
||||||
const defaults = {
|
const defaults = {
|
||||||
api: { provider: 'st', url: '', key: '', model: '', modelCache: [] },
|
api: { provider: "st", url: "", key: "", model: "", modelCache: [] },
|
||||||
gen: { temperature: null, top_p: null, top_k: null, presence_penalty: null, frequency_penalty: null },
|
gen: { temperature: null, top_p: null, top_k: null, presence_penalty: null, frequency_penalty: null },
|
||||||
trigger: {
|
trigger: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
interval: 20,
|
interval: 20,
|
||||||
timing: 'before_user',
|
timing: "before_user",
|
||||||
role: 'system',
|
role: "system",
|
||||||
useStream: true,
|
useStream: true,
|
||||||
maxPerRun: 100,
|
maxPerRun: 100,
|
||||||
wrapperHead: '',
|
wrapperHead: "",
|
||||||
wrapperTail: '',
|
wrapperTail: "",
|
||||||
forceInsertAtEnd: false,
|
forceInsertAtEnd: false,
|
||||||
},
|
},
|
||||||
|
textFilterRules: [...DEFAULT_FILTER_RULES],
|
||||||
vector: null,
|
vector: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem('summary_panel_config');
|
const raw = localStorage.getItem("summary_panel_config");
|
||||||
if (!raw) return defaults;
|
if (!raw) return defaults;
|
||||||
const parsed = JSON.parse(raw);
|
const parsed = JSON.parse(raw);
|
||||||
|
|
||||||
|
const textFilterRules = Array.isArray(parsed.textFilterRules)
|
||||||
|
? parsed.textFilterRules
|
||||||
|
: (Array.isArray(parsed.vector?.textFilterRules)
|
||||||
|
? parsed.vector.textFilterRules
|
||||||
|
: defaults.textFilterRules);
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
api: { ...defaults.api, ...(parsed.api || {}) },
|
api: { ...defaults.api, ...(parsed.api || {}) },
|
||||||
gen: { ...defaults.gen, ...(parsed.gen || {}) },
|
gen: { ...defaults.gen, ...(parsed.gen || {}) },
|
||||||
trigger: { ...defaults.trigger, ...(parsed.trigger || {}) },
|
trigger: { ...defaults.trigger, ...(parsed.trigger || {}) },
|
||||||
|
textFilterRules,
|
||||||
|
vector: parsed.vector || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (result.trigger.timing === 'manual') result.trigger.enabled = false;
|
if (result.trigger.timing === "manual") result.trigger.enabled = false;
|
||||||
if (result.trigger.useStream === undefined) result.trigger.useStream = true;
|
if (result.trigger.useStream === undefined) result.trigger.useStream = true;
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -61,35 +67,27 @@ export function getSummaryPanelConfig() {
|
|||||||
|
|
||||||
export function saveSummaryPanelConfig(config) {
|
export function saveSummaryPanelConfig(config) {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem('summary_panel_config', JSON.stringify(config));
|
localStorage.setItem("summary_panel_config", JSON.stringify(config));
|
||||||
CommonSettingStorage.set(SUMMARY_CONFIG_KEY, config);
|
CommonSettingStorage.set(SUMMARY_CONFIG_KEY, config);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
xbLog.error(MODULE_ID, '保存面板配置失败', e);
|
xbLog.error(MODULE_ID, "保存面板配置失败", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
// 向量配置(简化版 - 只需要 key)
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
export function getVectorConfig() {
|
export function getVectorConfig() {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem('summary_panel_config');
|
const raw = localStorage.getItem("summary_panel_config");
|
||||||
if (!raw) return null;
|
if (!raw) return null;
|
||||||
|
|
||||||
const parsed = JSON.parse(raw);
|
const parsed = JSON.parse(raw);
|
||||||
const cfg = parsed.vector || null;
|
const cfg = parsed.vector || null;
|
||||||
|
if (!cfg) return null;
|
||||||
|
|
||||||
if (cfg && !cfg.textFilterRules) {
|
// Keep vector side normalized to online + siliconflow.
|
||||||
cfg.textFilterRules = [...DEFAULT_FILTER_RULES];
|
cfg.engine = "online";
|
||||||
}
|
|
||||||
|
|
||||||
// 简化:统一使用硅基
|
|
||||||
if (cfg) {
|
|
||||||
cfg.engine = 'online';
|
|
||||||
cfg.online = cfg.online || {};
|
cfg.online = cfg.online || {};
|
||||||
cfg.online.provider = 'siliconflow';
|
cfg.online.provider = "siliconflow";
|
||||||
cfg.online.model = 'BAAI/bge-m3';
|
cfg.online.model = "BAAI/bge-m3";
|
||||||
}
|
|
||||||
|
|
||||||
return cfg;
|
return cfg;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -98,31 +96,31 @@ export function getVectorConfig() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getTextFilterRules() {
|
export function getTextFilterRules() {
|
||||||
const cfg = getVectorConfig();
|
const cfg = getSummaryPanelConfig();
|
||||||
return cfg?.textFilterRules || DEFAULT_FILTER_RULES;
|
return Array.isArray(cfg?.textFilterRules)
|
||||||
|
? cfg.textFilterRules
|
||||||
|
: DEFAULT_FILTER_RULES;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveVectorConfig(vectorCfg) {
|
export function saveVectorConfig(vectorCfg) {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem('summary_panel_config') || '{}';
|
const raw = localStorage.getItem("summary_panel_config") || "{}";
|
||||||
const parsed = JSON.parse(raw);
|
const parsed = JSON.parse(raw);
|
||||||
|
|
||||||
// 简化配置
|
|
||||||
parsed.vector = {
|
parsed.vector = {
|
||||||
enabled: vectorCfg?.enabled || false,
|
enabled: !!vectorCfg?.enabled,
|
||||||
engine: 'online',
|
engine: "online",
|
||||||
online: {
|
online: {
|
||||||
provider: 'siliconflow',
|
provider: "siliconflow",
|
||||||
key: vectorCfg?.online?.key || '',
|
key: vectorCfg?.online?.key || "",
|
||||||
model: 'BAAI/bge-m3',
|
model: "BAAI/bge-m3",
|
||||||
},
|
},
|
||||||
textFilterRules: vectorCfg?.textFilterRules || DEFAULT_FILTER_RULES,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
localStorage.setItem('summary_panel_config', JSON.stringify(parsed));
|
localStorage.setItem("summary_panel_config", JSON.stringify(parsed));
|
||||||
CommonSettingStorage.set(SUMMARY_CONFIG_KEY, parsed);
|
CommonSettingStorage.set(SUMMARY_CONFIG_KEY, parsed);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
xbLog.error(MODULE_ID, '保存向量配置失败', e);
|
xbLog.error(MODULE_ID, "保存向量配置失败", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,12 +128,12 @@ export async function loadConfigFromServer() {
|
|||||||
try {
|
try {
|
||||||
const savedConfig = await CommonSettingStorage.get(SUMMARY_CONFIG_KEY, null);
|
const savedConfig = await CommonSettingStorage.get(SUMMARY_CONFIG_KEY, null);
|
||||||
if (savedConfig) {
|
if (savedConfig) {
|
||||||
localStorage.setItem('summary_panel_config', JSON.stringify(savedConfig));
|
localStorage.setItem("summary_panel_config", JSON.stringify(savedConfig));
|
||||||
xbLog.info(MODULE_ID, '已从服务器加载面板配置');
|
xbLog.info(MODULE_ID, "已从服务端加载面板配置");
|
||||||
return savedConfig;
|
return savedConfig;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
xbLog.warn(MODULE_ID, '加载面板配置失败', e);
|
xbLog.warn(MODULE_ID, "加载面板配置失败", e);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { getContext } from "../../../../../../extensions.js";
|
|||||||
import { xbLog } from "../../../core/debug-core.js";
|
import { xbLog } from "../../../core/debug-core.js";
|
||||||
import { getSummaryStore, saveSummaryStore, addSummarySnapshot, mergeNewData, getFacts } from "../data/store.js";
|
import { getSummaryStore, saveSummaryStore, addSummarySnapshot, mergeNewData, getFacts } from "../data/store.js";
|
||||||
import { generateSummary, parseSummaryJson } from "./llm.js";
|
import { generateSummary, parseSummaryJson } from "./llm.js";
|
||||||
|
import { filterText } from "../vector/utils/text-filter.js";
|
||||||
|
|
||||||
const MODULE_ID = 'summaryGenerator';
|
const MODULE_ID = 'summaryGenerator';
|
||||||
const SUMMARY_SESSION_ID = 'xb9';
|
const SUMMARY_SESSION_ID = 'xb9';
|
||||||
@@ -168,7 +169,8 @@ export function buildIncrementalSlice(targetMesId, lastSummarizedMesId, maxPerRu
|
|||||||
|
|
||||||
const text = slice.map((m, i) => {
|
const text = slice.map((m, i) => {
|
||||||
const speaker = m.name || (m.is_user ? userLabel : charLabel);
|
const speaker = m.name || (m.is_user ? userLabel : charLabel);
|
||||||
return `#${start + i + 1} 【${speaker}】\n${m.mes}`;
|
const filteredMessage = filterText(m.mes || "");
|
||||||
|
return `#${start + i + 1} 【${speaker}】\n${filteredMessage}`;
|
||||||
}).join('\n\n');
|
}).join('\n\n');
|
||||||
|
|
||||||
return { text, count: slice.length, range: `${start + 1}-${end + 1}楼`, endMesId: end };
|
return { text, count: slice.length, range: `${start + 1}-${end + 1}楼`, endMesId: end };
|
||||||
|
|||||||
@@ -87,6 +87,7 @@
|
|||||||
api: { provider: 'st', url: '', key: '', model: '', modelCache: [] },
|
api: { provider: 'st', url: '', key: '', model: '', modelCache: [] },
|
||||||
gen: { temperature: null, top_p: null, top_k: null, presence_penalty: null, frequency_penalty: null },
|
gen: { temperature: null, top_p: null, top_k: null, presence_penalty: null, frequency_penalty: null },
|
||||||
trigger: { enabled: false, interval: 20, timing: 'before_user', role: 'system', useStream: true, maxPerRun: 100, wrapperHead: '', wrapperTail: '', forceInsertAtEnd: false },
|
trigger: { enabled: false, interval: 20, timing: 'before_user', role: 'system', useStream: true, maxPerRun: 100, wrapperHead: '', wrapperTail: '', forceInsertAtEnd: false },
|
||||||
|
textFilterRules: [...DEFAULT_FILTER_RULES],
|
||||||
vector: { enabled: false, engine: 'online', local: { modelId: 'bge-small-zh' }, online: { provider: 'siliconflow', url: '', key: '', model: '' } }
|
vector: { enabled: false, engine: 'online', local: { modelId: 'bge-small-zh' }, online: { provider: 'siliconflow', url: '', key: '', model: '' } }
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -123,6 +124,9 @@
|
|||||||
Object.assign(config.api, p.api || {});
|
Object.assign(config.api, p.api || {});
|
||||||
Object.assign(config.gen, p.gen || {});
|
Object.assign(config.gen, p.gen || {});
|
||||||
Object.assign(config.trigger, p.trigger || {});
|
Object.assign(config.trigger, p.trigger || {});
|
||||||
|
config.textFilterRules = Array.isArray(p.textFilterRules)
|
||||||
|
? p.textFilterRules
|
||||||
|
: (Array.isArray(p.vector?.textFilterRules) ? p.vector.textFilterRules : [...DEFAULT_FILTER_RULES]);
|
||||||
if (p.vector) config.vector = p.vector;
|
if (p.vector) config.vector = p.vector;
|
||||||
if (config.trigger.timing === 'manual' && config.trigger.enabled) {
|
if (config.trigger.timing === 'manual' && config.trigger.enabled) {
|
||||||
config.trigger.enabled = false;
|
config.trigger.enabled = false;
|
||||||
@@ -137,6 +141,11 @@
|
|||||||
Object.assign(config.api, cfg.api || {});
|
Object.assign(config.api, cfg.api || {});
|
||||||
Object.assign(config.gen, cfg.gen || {});
|
Object.assign(config.gen, cfg.gen || {});
|
||||||
Object.assign(config.trigger, cfg.trigger || {});
|
Object.assign(config.trigger, cfg.trigger || {});
|
||||||
|
config.textFilterRules = Array.isArray(cfg.textFilterRules)
|
||||||
|
? cfg.textFilterRules
|
||||||
|
: (Array.isArray(cfg.vector?.textFilterRules)
|
||||||
|
? cfg.vector.textFilterRules
|
||||||
|
: (Array.isArray(config.textFilterRules) ? config.textFilterRules : [...DEFAULT_FILTER_RULES]));
|
||||||
if (cfg.vector) config.vector = cfg.vector;
|
if (cfg.vector) config.vector = cfg.vector;
|
||||||
if (config.trigger.timing === 'manual') config.trigger.enabled = false;
|
if (config.trigger.timing === 'manual') config.trigger.enabled = false;
|
||||||
localStorage.setItem('summary_panel_config', JSON.stringify(config));
|
localStorage.setItem('summary_panel_config', JSON.stringify(config));
|
||||||
@@ -145,7 +154,10 @@
|
|||||||
function saveConfig() {
|
function saveConfig() {
|
||||||
try {
|
try {
|
||||||
const settingsOpen = $('settings-modal')?.classList.contains('active');
|
const settingsOpen = $('settings-modal')?.classList.contains('active');
|
||||||
if (settingsOpen) config.vector = getVectorConfig();
|
if (settingsOpen) {
|
||||||
|
config.vector = getVectorConfig();
|
||||||
|
config.textFilterRules = collectFilterRules();
|
||||||
|
}
|
||||||
if (!config.vector) {
|
if (!config.vector) {
|
||||||
config.vector = { enabled: false, engine: 'online', online: { provider: 'siliconflow', key: '', model: 'BAAI/bge-m3' } };
|
config.vector = { enabled: false, engine: 'online', online: { provider: 'siliconflow', key: '', model: 'BAAI/bge-m3' } };
|
||||||
}
|
}
|
||||||
@@ -169,7 +181,6 @@
|
|||||||
key: $('vector-api-key')?.value?.trim() || '',
|
key: $('vector-api-key')?.value?.trim() || '',
|
||||||
model: 'BAAI/bge-m3',
|
model: 'BAAI/bge-m3',
|
||||||
},
|
},
|
||||||
textFilterRules: collectFilterRules(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,7 +193,6 @@
|
|||||||
$('vector-api-key').value = cfg.online.key;
|
$('vector-api-key').value = cfg.online.key;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderFilterRules(cfg?.textFilterRules || DEFAULT_FILTER_RULES);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
@@ -471,6 +481,7 @@
|
|||||||
|
|
||||||
updateProviderUI(config.api.provider);
|
updateProviderUI(config.api.provider);
|
||||||
if (config.vector) loadVectorConfig(config.vector);
|
if (config.vector) loadVectorConfig(config.vector);
|
||||||
|
renderFilterRules(Array.isArray(config.textFilterRules) ? config.textFilterRules : DEFAULT_FILTER_RULES);
|
||||||
|
|
||||||
// Initialize sub-options visibility
|
// Initialize sub-options visibility
|
||||||
const autoSummaryOptions = $('auto-summary-options');
|
const autoSummaryOptions = $('auto-summary-options');
|
||||||
@@ -520,6 +531,7 @@
|
|||||||
config.trigger.wrapperHead = $('trigger-wrapper-head').value;
|
config.trigger.wrapperHead = $('trigger-wrapper-head').value;
|
||||||
config.trigger.wrapperTail = $('trigger-wrapper-tail').value;
|
config.trigger.wrapperTail = $('trigger-wrapper-tail').value;
|
||||||
config.trigger.forceInsertAtEnd = $('trigger-insert-at-end').checked;
|
config.trigger.forceInsertAtEnd = $('trigger-insert-at-end').checked;
|
||||||
|
config.textFilterRules = collectFilterRules();
|
||||||
|
|
||||||
config.vector = getVectorConfig();
|
config.vector = getVectorConfig();
|
||||||
saveConfig();
|
saveConfig();
|
||||||
|
|||||||
@@ -334,6 +334,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter Rules -->
|
||||||
|
<div class="settings-collapse" id="filter-rules-collapse"
|
||||||
|
style="margin-top:0; margin-bottom: 16px;">
|
||||||
|
<div class="settings-collapse-header" id="filter-rules-toggle">
|
||||||
|
<span>文本过滤规则 · <strong id="filter-rules-count">0</strong> 条</span>
|
||||||
|
<svg class="collapse-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||||
|
stroke-width="2">
|
||||||
|
<polyline points="6 9 12 15 18 9"></polyline>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="settings-collapse-content hidden" id="filter-rules-content"
|
||||||
|
style="border-left: 1px solid var(--bdr); border-right: 1px solid var(--bdr); border-bottom: 1px solid var(--bdr); border-radius: 0 0 6px 6px; margin-top: -2px;">
|
||||||
|
<div class="filter-rules-header">
|
||||||
|
<p class="settings-hint" style="margin:0">过滤干扰内容(如思考标签)</p>
|
||||||
|
<button class="btn btn-sm btn-add" id="btn-add-filter-rule">
|
||||||
|
<svg viewBox="0 0 24 24" width="14" height="14" fill="none"
|
||||||
|
stroke="currentColor" stroke-width="2">
|
||||||
|
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||||
|
</svg> 添加
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="filter-rules-list" class="filter-rules-list"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Force Insert with wrapper options -->
|
<!-- Force Insert with wrapper options -->
|
||||||
<div class="settings-checkbox-group">
|
<div class="settings-checkbox-group">
|
||||||
<label class="settings-checkbox">
|
<label class="settings-checkbox">
|
||||||
@@ -525,33 +551,6 @@
|
|||||||
<span>设置与工具</span>
|
<span>设置与工具</span>
|
||||||
<span style="opacity:0.5">///</span>
|
<span style="opacity:0.5">///</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filter Rules -->
|
|
||||||
<div class="settings-collapse" id="filter-rules-collapse"
|
|
||||||
style="margin-top:0; margin-bottom: 16px;">
|
|
||||||
<div class="settings-collapse-header" id="filter-rules-toggle">
|
|
||||||
<span>文本过滤规则 · <strong id="filter-rules-count">0</strong> 条</span>
|
|
||||||
<svg class="collapse-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
||||||
stroke-width="2">
|
|
||||||
<polyline points="6 9 12 15 18 9"></polyline>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="settings-collapse-content hidden" id="filter-rules-content"
|
|
||||||
style="border-left: 1px solid var(--bdr); border-right: 1px solid var(--bdr); border-bottom: 1px solid var(--bdr); border-radius: 0 0 6px 6px; margin-top: -2px;">
|
|
||||||
<div class="filter-rules-header">
|
|
||||||
<p class="settings-hint" style="margin:0">过滤干扰内容(如思考标签)</p>
|
|
||||||
<button class="btn btn-sm btn-add" id="btn-add-filter-rule">
|
|
||||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none"
|
|
||||||
stroke="currentColor" stroke-width="2">
|
|
||||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
||||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
||||||
</svg> 添加
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div id="filter-rules-list" class="filter-rules-list"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Import/Export -->
|
<!-- Import/Export -->
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<div class="settings-field full">
|
<div class="settings-field full">
|
||||||
|
|||||||
Reference in New Issue
Block a user