1.18更新
This commit is contained in:
@@ -7,13 +7,17 @@ import { xbLog } from "../core/debug-core.js";
|
||||
|
||||
const SOURCE_TAG = 'xiaobaix-host';
|
||||
|
||||
const POSITIONS = Object.freeze({ BEFORE_PROMPT: 'BEFORE_PROMPT', IN_PROMPT: 'IN_PROMPT', IN_CHAT: 'IN_CHAT', AFTER_COMPONENT: 'AFTER_COMPONENT' });
|
||||
const KNOWN_KEYS = Object.freeze(new Set([
|
||||
'main', 'chatHistory', 'worldInfo', 'worldInfoBefore', 'worldInfoAfter',
|
||||
'charDescription', 'charPersonality', 'scenario', 'personaDescription',
|
||||
'dialogueExamples', 'authorsNote', 'vectorsMemory', 'vectorsDataBank',
|
||||
'smartContext', 'jailbreak', 'nsfw', 'summary', 'bias', 'impersonate', 'quietPrompt',
|
||||
]));
|
||||
const POSITIONS = Object.freeze({ BEFORE_PROMPT: 'BEFORE_PROMPT', IN_PROMPT: 'IN_PROMPT', IN_CHAT: 'IN_CHAT', AFTER_COMPONENT: 'AFTER_COMPONENT' });
|
||||
const KNOWN_KEYS = Object.freeze(new Set([
|
||||
'main', 'chatHistory', 'worldInfo', 'worldInfoBefore', 'worldInfoAfter',
|
||||
'charDescription', 'charPersonality', 'scenario', 'personaDescription',
|
||||
'dialogueExamples', 'authorsNote', 'vectorsMemory', 'vectorsDataBank',
|
||||
'smartContext', 'jailbreak', 'nsfw', 'summary', 'bias', 'impersonate', 'quietPrompt',
|
||||
]));
|
||||
const resolveTargetOrigin = (origin) => {
|
||||
if (typeof origin === 'string' && origin) return origin;
|
||||
try { return window.location.origin; } catch { return '*'; }
|
||||
};
|
||||
|
||||
// @ts-nocheck
|
||||
class CallGenerateService {
|
||||
@@ -44,11 +48,11 @@ class CallGenerateService {
|
||||
}
|
||||
}
|
||||
|
||||
sendError(sourceWindow, requestId, streamingEnabled, err, fallbackCode = 'API_ERROR', details = null) {
|
||||
const e = this.normalizeError(err, fallbackCode, details);
|
||||
const type = streamingEnabled ? 'generateStreamError' : 'generateError';
|
||||
try { sourceWindow?.postMessage({ source: SOURCE_TAG, type, id: requestId, error: e }, '*'); } catch {}
|
||||
}
|
||||
sendError(sourceWindow, requestId, streamingEnabled, err, fallbackCode = 'API_ERROR', details = null, targetOrigin = null) {
|
||||
const e = this.normalizeError(err, fallbackCode, details);
|
||||
const type = streamingEnabled ? 'generateStreamError' : 'generateError';
|
||||
try { sourceWindow?.postMessage({ source: SOURCE_TAG, type, id: requestId, error: e }, resolveTargetOrigin(targetOrigin)); } catch {}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string|undefined} rawId
|
||||
@@ -253,11 +257,11 @@ class CallGenerateService {
|
||||
* @param {string} type
|
||||
* @param {object} body
|
||||
*/
|
||||
postToTarget(target, type, body) {
|
||||
try {
|
||||
target?.postMessage({ source: SOURCE_TAG, type, ...body }, '*');
|
||||
} catch (e) {}
|
||||
}
|
||||
postToTarget(target, type, body, targetOrigin = null) {
|
||||
try {
|
||||
target?.postMessage({ source: SOURCE_TAG, type, ...body }, resolveTargetOrigin(targetOrigin));
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// ===== ST Prompt 干跑捕获与组件切换 =====
|
||||
|
||||
@@ -759,7 +763,6 @@ class CallGenerateService {
|
||||
async _annotateIdentifiersIfMissing(messages, targetKeys) {
|
||||
const arr = Array.isArray(messages) ? messages.map(m => ({ ...m })) : [];
|
||||
if (!arr.length) return arr;
|
||||
const hasIdentifier = arr.some(m => typeof m?.identifier === 'string' && m.identifier);
|
||||
// 标注 chatHistory:依据 role + 来源判断
|
||||
const isFromChat = this._createIsFromChat();
|
||||
for (const m of arr) {
|
||||
@@ -1005,7 +1008,7 @@ class CallGenerateService {
|
||||
|
||||
_applyContentFilter(list, filterCfg) {
|
||||
if (!filterCfg) return list;
|
||||
const { contains, regex, fromUserNames, beforeTs, afterTs } = filterCfg;
|
||||
const { contains, regex, fromUserNames } = filterCfg;
|
||||
let out = list.slice();
|
||||
if (contains) {
|
||||
const needles = Array.isArray(contains) ? contains : [contains];
|
||||
@@ -1044,7 +1047,6 @@ class CallGenerateService {
|
||||
}
|
||||
|
||||
_applyIndicesRange(list, selector) {
|
||||
const idxBase = selector?.indexBase === 'all' ? 'all' : 'history';
|
||||
let result = list.slice();
|
||||
// indices 优先
|
||||
if (Array.isArray(selector?.indices?.values) && selector.indices.values.length) {
|
||||
@@ -1130,7 +1132,7 @@ class CallGenerateService {
|
||||
|
||||
// ===== 发送实现(构建后的统一发送) =====
|
||||
|
||||
async _sendMessages(messages, options, requestId, sourceWindow) {
|
||||
async _sendMessages(messages, options, requestId, sourceWindow, targetOrigin = null) {
|
||||
const sessionId = this.normalizeSessionId(options?.session?.id || 'xb1');
|
||||
const session = this.ensureSession(sessionId);
|
||||
const streamingEnabled = options?.streaming?.enabled !== false; // 默认开
|
||||
@@ -1141,11 +1143,11 @@ class CallGenerateService {
|
||||
const shouldExport = !!(options?.debug?.enabled || options?.debug?.exportPrompt);
|
||||
const already = options?.debug?._exported === true;
|
||||
if (shouldExport && !already) {
|
||||
this.postToTarget(sourceWindow, 'generatePromptPreview', { id: requestId, messages: (messages || []).map(m => ({ role: m.role, content: m.content })) });
|
||||
this.postToTarget(sourceWindow, 'generatePromptPreview', { id: requestId, messages: (messages || []).map(m => ({ role: m.role, content: m.content })) }, targetOrigin);
|
||||
}
|
||||
|
||||
if (streamingEnabled) {
|
||||
this.postToTarget(sourceWindow, 'generateStreamStart', { id: requestId, sessionId });
|
||||
this.postToTarget(sourceWindow, 'generateStreamStart', { id: requestId, sessionId }, targetOrigin);
|
||||
const streamFn = await ChatCompletionService.sendRequest(payload, false, session.abortController.signal);
|
||||
let last = '';
|
||||
const generator = typeof streamFn === 'function' ? streamFn() : null;
|
||||
@@ -1153,7 +1155,7 @@ class CallGenerateService {
|
||||
const chunk = text.slice(last.length);
|
||||
last = text;
|
||||
session.accumulated = text;
|
||||
this.postToTarget(sourceWindow, 'generateStreamChunk', { id: requestId, chunk, accumulated: text, metadata: {} });
|
||||
this.postToTarget(sourceWindow, 'generateStreamChunk', { id: requestId, chunk, accumulated: text, metadata: {} }, targetOrigin);
|
||||
}
|
||||
const result = {
|
||||
success: true,
|
||||
@@ -1161,7 +1163,7 @@ class CallGenerateService {
|
||||
sessionId,
|
||||
metadata: { duration: Date.now() - session.startedAt, model: apiCfg.model, finishReason: 'stop' },
|
||||
};
|
||||
this.postToTarget(sourceWindow, 'generateStreamComplete', { id: requestId, result });
|
||||
this.postToTarget(sourceWindow, 'generateStreamComplete', { id: requestId, result }, targetOrigin);
|
||||
return result;
|
||||
} else {
|
||||
const extracted = await ChatCompletionService.sendRequest(payload, true, session.abortController.signal);
|
||||
@@ -1171,17 +1173,17 @@ class CallGenerateService {
|
||||
sessionId,
|
||||
metadata: { duration: Date.now() - session.startedAt, model: apiCfg.model, finishReason: 'stop' },
|
||||
};
|
||||
this.postToTarget(sourceWindow, 'generateResult', { id: requestId, result });
|
||||
this.postToTarget(sourceWindow, 'generateResult', { id: requestId, result }, targetOrigin);
|
||||
return result;
|
||||
}
|
||||
} catch (err) {
|
||||
this.sendError(sourceWindow, requestId, streamingEnabled, err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
this.sendError(sourceWindow, requestId, streamingEnabled, err, 'API_ERROR', null, targetOrigin);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 主流程 =====
|
||||
async handleRequestInternal(options, requestId, sourceWindow) {
|
||||
async handleRequestInternal(options, requestId, sourceWindow, targetOrigin = null) {
|
||||
// 1) 校验
|
||||
this.validateOptions(options);
|
||||
|
||||
@@ -1275,10 +1277,10 @@ class CallGenerateService {
|
||||
working = this._appendUserInput(working, options?.userInput);
|
||||
|
||||
// 8) 调试导出
|
||||
this._exportDebugData({ sourceWindow, requestId, working, baseStrategy, orderedRefs, inlineMapped, listLevelOverrides, debug: options?.debug });
|
||||
this._exportDebugData({ sourceWindow, requestId, working, baseStrategy, orderedRefs, inlineMapped, listLevelOverrides, debug: options?.debug, targetOrigin });
|
||||
|
||||
// 9) 发送
|
||||
return await this._sendMessages(working, { ...options, debug: { ...(options?.debug || {}), _exported: true } }, requestId, sourceWindow);
|
||||
return await this._sendMessages(working, { ...options, debug: { ...(options?.debug || {}), _exported: true } }, requestId, sourceWindow, targetOrigin);
|
||||
}
|
||||
|
||||
_applyOrderingStrategy(messages, baseStrategy, orderedRefs, unorderedKeys) {
|
||||
@@ -1338,9 +1340,9 @@ class CallGenerateService {
|
||||
return out;
|
||||
}
|
||||
|
||||
_exportDebugData({ sourceWindow, requestId, working, baseStrategy, orderedRefs, inlineMapped, listLevelOverrides, debug }) {
|
||||
_exportDebugData({ sourceWindow, requestId, working, baseStrategy, orderedRefs, inlineMapped, listLevelOverrides, debug, targetOrigin }) {
|
||||
const exportPrompt = !!(debug?.enabled || debug?.exportPrompt);
|
||||
if (exportPrompt) this.postToTarget(sourceWindow, 'generatePromptPreview', { id: requestId, messages: working.map(m => ({ role: m.role, content: m.content })) });
|
||||
if (exportPrompt) this.postToTarget(sourceWindow, 'generatePromptPreview', { id: requestId, messages: working.map(m => ({ role: m.role, content: m.content })) }, targetOrigin);
|
||||
if (debug?.exportBlueprint) {
|
||||
try {
|
||||
const bp = {
|
||||
@@ -1349,7 +1351,7 @@ class CallGenerateService {
|
||||
injections: (debug?.injections || []).concat(inlineMapped || []),
|
||||
overrides: listLevelOverrides || null,
|
||||
};
|
||||
this.postToTarget(sourceWindow, 'blueprint', bp);
|
||||
this.postToTarget(sourceWindow, 'blueprint', bp, targetOrigin);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
@@ -1357,7 +1359,7 @@ class CallGenerateService {
|
||||
/**
|
||||
* 入口:处理 generateRequest(统一入口)
|
||||
*/
|
||||
async handleGenerateRequest(options, requestId, sourceWindow) {
|
||||
async handleGenerateRequest(options, requestId, sourceWindow, targetOrigin = null) {
|
||||
let streamingEnabled = false;
|
||||
try {
|
||||
streamingEnabled = options?.streaming?.enabled !== false;
|
||||
@@ -1369,10 +1371,10 @@ class CallGenerateService {
|
||||
xbLog.info('callGenerateBridge', `generateRequest id=${requestId} stream=${!!streamingEnabled} comps=${compsCount} userInputLen=${userInputLen}`);
|
||||
}
|
||||
} catch {}
|
||||
return await this.handleRequestInternal(options, requestId, sourceWindow);
|
||||
return await this.handleRequestInternal(options, requestId, sourceWindow, targetOrigin);
|
||||
} catch (err) {
|
||||
try { xbLog.error('callGenerateBridge', `generateRequest failed id=${requestId}`, err); } catch {}
|
||||
this.sendError(sourceWindow, requestId, streamingEnabled, err, 'BAD_REQUEST');
|
||||
this.sendError(sourceWindow, requestId, streamingEnabled, err, 'BAD_REQUEST', null, targetOrigin);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1392,9 +1394,9 @@ class CallGenerateService {
|
||||
|
||||
const callGenerateService = new CallGenerateService();
|
||||
|
||||
export async function handleGenerateRequest(options, requestId, sourceWindow) {
|
||||
return await callGenerateService.handleGenerateRequest(options, requestId, sourceWindow);
|
||||
}
|
||||
export async function handleGenerateRequest(options, requestId, sourceWindow, targetOrigin = null) {
|
||||
return await callGenerateService.handleGenerateRequest(options, requestId, sourceWindow, targetOrigin);
|
||||
}
|
||||
|
||||
// Host bridge for handling iframe generateRequest → respond via postMessage
|
||||
let __xb_generate_listener_attached = false;
|
||||
@@ -1410,11 +1412,12 @@ export function initCallGenerateHostBridge() {
|
||||
if (!data || data.type !== 'generateRequest') return;
|
||||
const id = data.id;
|
||||
const options = data.options || {};
|
||||
await handleGenerateRequest(options, id, event.source || window);
|
||||
await handleGenerateRequest(options, id, event.source || window, event.origin);
|
||||
} catch (e) {
|
||||
try { xbLog.error('callGenerateBridge', 'generateRequest listener error', e); } catch {}
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line no-restricted-syntax -- bridge listener; origin can be null for sandboxed iframes.
|
||||
try { window.addEventListener('message', __xb_generate_listener); } catch (e) {}
|
||||
__xb_generate_listener_attached = true;
|
||||
}
|
||||
@@ -1511,7 +1514,8 @@ if (typeof window !== 'undefined') {
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', listener);
|
||||
// eslint-disable-next-line no-restricted-syntax -- local listener for internal request flow.
|
||||
window.addEventListener('message', listener);
|
||||
|
||||
// 发送请求
|
||||
handleGenerateRequest(options, requestId, window).catch(err => {
|
||||
|
||||
@@ -22,7 +22,11 @@ import {
|
||||
} from "../../../../world-info.js";
|
||||
import { getCharaFilename, findChar } from "../../../../utils.js";
|
||||
|
||||
const SOURCE_TAG = "xiaobaix-host";
|
||||
const SOURCE_TAG = "xiaobaix-host";
|
||||
const resolveTargetOrigin = (origin) => {
|
||||
if (typeof origin === 'string' && origin) return origin;
|
||||
try { return window.location.origin; } catch { return '*'; }
|
||||
};
|
||||
|
||||
function isString(value) {
|
||||
return typeof value === 'string';
|
||||
@@ -91,18 +95,18 @@ class WorldbookBridgeService {
|
||||
}
|
||||
}
|
||||
|
||||
sendResult(target, requestId, result) {
|
||||
try { target?.postMessage({ source: SOURCE_TAG, type: 'worldbookResult', id: requestId, result }, '*'); } catch {}
|
||||
}
|
||||
|
||||
sendError(target, requestId, err, fallbackCode = 'API_ERROR', details = null) {
|
||||
const e = this.normalizeError(err, fallbackCode, details);
|
||||
try { target?.postMessage({ source: SOURCE_TAG, type: 'worldbookError', id: requestId, error: e }, '*'); } catch {}
|
||||
}
|
||||
|
||||
postEvent(event, payload) {
|
||||
try { window?.postMessage({ source: SOURCE_TAG, type: 'worldbookEvent', event, payload }, '*'); } catch {}
|
||||
}
|
||||
sendResult(target, requestId, result, targetOrigin = null) {
|
||||
try { target?.postMessage({ source: SOURCE_TAG, type: 'worldbookResult', id: requestId, result }, resolveTargetOrigin(targetOrigin)); } catch {}
|
||||
}
|
||||
|
||||
sendError(target, requestId, err, fallbackCode = 'API_ERROR', details = null, targetOrigin = null) {
|
||||
const e = this.normalizeError(err, fallbackCode, details);
|
||||
try { target?.postMessage({ source: SOURCE_TAG, type: 'worldbookError', id: requestId, error: e }, resolveTargetOrigin(targetOrigin)); } catch {}
|
||||
}
|
||||
|
||||
postEvent(event, payload) {
|
||||
try { window?.postMessage({ source: SOURCE_TAG, type: 'worldbookEvent', event, payload }, resolveTargetOrigin()); } catch {}
|
||||
}
|
||||
|
||||
async ensureWorldExists(name, autoCreate) {
|
||||
if (!isString(name) || !name.trim()) throw new Error('MISSING_PARAMS');
|
||||
@@ -217,8 +221,8 @@ class WorldbookBridgeService {
|
||||
if (!entry) return '';
|
||||
if (newWorldInfoEntryTemplate[field] === undefined) return '';
|
||||
|
||||
const ctx = getContext();
|
||||
const tags = ctx.tags || [];
|
||||
const ctx = getContext();
|
||||
const tags = ctx.tags || [];
|
||||
|
||||
let fieldValue;
|
||||
switch (field) {
|
||||
@@ -381,9 +385,7 @@ class WorldbookBridgeService {
|
||||
const entry = data.entries[uid];
|
||||
if (!entry) throw new Error('NOT_FOUND');
|
||||
|
||||
const ctx = getContext();
|
||||
const tags = ctx.tags || [];
|
||||
const result = {};
|
||||
const result = {};
|
||||
|
||||
// Get all template fields
|
||||
for (const field of Object.keys(newWorldInfoEntryTemplate)) {
|
||||
@@ -837,13 +839,14 @@ class WorldbookBridgeService {
|
||||
}
|
||||
} catch {}
|
||||
const result = await self.handleRequest(action, params);
|
||||
self.sendResult(event.source || window, id, result);
|
||||
self.sendResult(event.source || window, id, result, event.origin);
|
||||
} catch (err) {
|
||||
try { xbLog.error('worldbookBridge', `worldbookRequest failed id=${id} action=${String(action || '')}`, err); } catch {}
|
||||
self.sendError(event.source || window, id, err);
|
||||
self.sendError(event.source || window, id, err, 'API_ERROR', null, event.origin);
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
// eslint-disable-next-line no-restricted-syntax -- validated by isOriginAllowed before handling.
|
||||
try { window.addEventListener('message', this._listener); } catch {}
|
||||
this._attached = true;
|
||||
if (forwardEvents) this.attachEventsForwarding();
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
(function(){
|
||||
function defineCallGenerate(){
|
||||
(function(){
|
||||
function defineCallGenerate(){
|
||||
var parentOrigin;
|
||||
try{parentOrigin=new URL(document.referrer).origin}catch(_){parentOrigin='*'}
|
||||
function sanitizeOptions(options){
|
||||
try{
|
||||
return JSON.parse(JSON.stringify(options,function(k,v){return(typeof v==='function')?undefined:v}))
|
||||
@@ -29,12 +31,13 @@
|
||||
function CallGenerateImpl(options){
|
||||
return new Promise(function(resolve,reject){
|
||||
try{
|
||||
function post(m){try{parent.postMessage(m,'*')}catch(e){}}
|
||||
function post(m){try{parent.postMessage(m,parentOrigin)}catch(e){}}
|
||||
if(!options||typeof options!=='object'){reject(new Error('Invalid options'));return}
|
||||
var id=Date.now().toString(36)+Math.random().toString(36).slice(2);
|
||||
function onMessage(e){
|
||||
var d=e&&e.data||{};
|
||||
if(d.source!=='xiaobaix-host'||d.id!==id)return;
|
||||
function onMessage(e){
|
||||
if(parentOrigin!=='*'&&e&&e.origin!==parentOrigin)return;
|
||||
var d=e&&e.data||{};
|
||||
if(d.source!=='xiaobaix-host'||d.id!==id)return;
|
||||
if(d.type==='generateStreamStart'&&options.streaming&&options.streaming.onStart){try{options.streaming.onStart(d.sessionId)}catch(_){}}
|
||||
else if(d.type==='generateStreamChunk'&&options.streaming&&options.streaming.onChunk){try{options.streaming.onChunk(d.chunk,d.accumulated)}catch(_){}}
|
||||
else if(d.type==='generateStreamComplete'){try{window.removeEventListener('message',onMessage)}catch(_){}
|
||||
@@ -46,10 +49,14 @@
|
||||
else if(d.type==='generateError'){try{window.removeEventListener('message',onMessage)}catch(_){}
|
||||
reject(new Error(d.error||'Generation failed'))}
|
||||
}
|
||||
try{window.addEventListener('message',onMessage)}catch(_){}
|
||||
// eslint-disable-next-line no-restricted-syntax -- origin checked via parentOrigin.
|
||||
try{window.addEventListener('message',onMessage)}catch(_){}
|
||||
var sanitized=sanitizeOptions(options);
|
||||
post({type:'generateRequest',id:id,options:sanitized});
|
||||
setTimeout(function(){try{window.removeEventListener('message',onMessage)}catch(e){};reject(new Error('Generation timeout'))},300000);
|
||||
setTimeout(function(){
|
||||
try{window.removeEventListener('message',onMessage)}catch(e){}
|
||||
reject(new Error('Generation timeout'));
|
||||
},300000);
|
||||
}catch(e){reject(e)}
|
||||
})
|
||||
}
|
||||
@@ -57,10 +64,12 @@
|
||||
try{window.callGenerate=CallGenerateImpl}catch(e){}
|
||||
try{window.__xb_callGenerate_loaded=true}catch(e){}
|
||||
}
|
||||
try{defineCallGenerate()}catch(e){}
|
||||
})();
|
||||
|
||||
(function(){
|
||||
try{defineCallGenerate()}catch(e){}
|
||||
})();
|
||||
|
||||
(function(){
|
||||
var parentOrigin;
|
||||
try{parentOrigin=new URL(document.referrer).origin}catch(_){parentOrigin='*'}
|
||||
function applyAvatarCss(urls){
|
||||
try{
|
||||
const root=document.documentElement;
|
||||
@@ -83,18 +92,20 @@
|
||||
}
|
||||
}catch(_){}
|
||||
}
|
||||
function requestAvatars(){
|
||||
try{parent.postMessage({type:'getAvatars'},'*')}catch(_){}
|
||||
}
|
||||
function onMessage(e){
|
||||
const d=e&&e.data||{};
|
||||
if(d&&d.source==='xiaobaix-host'&&d.type==='avatars'){
|
||||
function requestAvatars(){
|
||||
try{parent.postMessage({type:'getAvatars'},parentOrigin)}catch(_){}
|
||||
}
|
||||
function onMessage(e){
|
||||
if(parentOrigin!=='*'&&e&&e.origin!==parentOrigin)return;
|
||||
const d=e&&e.data||{};
|
||||
if(d&&d.source==='xiaobaix-host'&&d.type==='avatars'){
|
||||
applyAvatarCss(d.urls);
|
||||
try{window.removeEventListener('message',onMessage)}catch(_){}
|
||||
}
|
||||
}
|
||||
try{
|
||||
window.addEventListener('message',onMessage);
|
||||
try{
|
||||
// eslint-disable-next-line no-restricted-syntax -- origin checked via parentOrigin.
|
||||
window.addEventListener('message',onMessage);
|
||||
if(document.readyState==='loading'){
|
||||
document.addEventListener('DOMContentLoaded',requestAvatars,{once:true});
|
||||
}else{
|
||||
@@ -102,4 +113,4 @@
|
||||
}
|
||||
window.addEventListener('load',requestAvatars,{once:true});
|
||||
}catch(_){}
|
||||
})();
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user