啊
This commit is contained in:
7
core/constants.js
Normal file
7
core/constants.js
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* LittleWhiteBox 共享常量
|
||||
*/
|
||||
|
||||
export const EXT_ID = "LittleWhiteBox";
|
||||
export const EXT_NAME = "小白X";
|
||||
export const extensionFolderPath = `scripts/extensions/third-party/${EXT_ID}`;
|
||||
322
core/debug-core.js
Normal file
322
core/debug-core.js
Normal file
@@ -0,0 +1,322 @@
|
||||
import { EventCenter } from "./event-manager.js";
|
||||
|
||||
const DEFAULT_MAX_LOGS = 200;
|
||||
|
||||
function now() {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
function safeStringify(value) {
|
||||
try {
|
||||
if (typeof value === "string") return value;
|
||||
return JSON.stringify(value);
|
||||
} catch {
|
||||
try {
|
||||
return String(value);
|
||||
} catch {
|
||||
return "[unstringifiable]";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function errorToStack(err) {
|
||||
try {
|
||||
if (!err) return null;
|
||||
if (typeof err === "string") return err;
|
||||
if (err && typeof err.stack === "string") return err.stack;
|
||||
return safeStringify(err);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class LoggerCore {
|
||||
constructor() {
|
||||
this._enabled = false;
|
||||
this._buffer = [];
|
||||
this._maxSize = DEFAULT_MAX_LOGS;
|
||||
this._seq = 0;
|
||||
this._originalConsole = null;
|
||||
this._originalOnError = null;
|
||||
this._originalOnUnhandledRejection = null;
|
||||
this._mounted = false;
|
||||
}
|
||||
|
||||
setMaxSize(n) {
|
||||
const v = Number.parseInt(n, 10);
|
||||
if (Number.isFinite(v) && v > 0) this._maxSize = v;
|
||||
if (this._buffer.length > this._maxSize) {
|
||||
this._buffer.splice(0, this._buffer.length - this._maxSize);
|
||||
}
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return !!this._enabled;
|
||||
}
|
||||
|
||||
enable() {
|
||||
if (this._enabled) return;
|
||||
this._enabled = true;
|
||||
this._mountGlobalHooks();
|
||||
}
|
||||
|
||||
disable() {
|
||||
this._enabled = false;
|
||||
this.clear();
|
||||
this._unmountGlobalHooks();
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._buffer.length = 0;
|
||||
}
|
||||
|
||||
getAll() {
|
||||
return this._buffer.slice();
|
||||
}
|
||||
|
||||
export() {
|
||||
return JSON.stringify(
|
||||
{
|
||||
version: 1,
|
||||
exportedAt: now(),
|
||||
maxSize: this._maxSize,
|
||||
logs: this.getAll(),
|
||||
},
|
||||
null,
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
_push(entry) {
|
||||
if (!this._enabled) return;
|
||||
this._buffer.push(entry);
|
||||
if (this._buffer.length > this._maxSize) {
|
||||
this._buffer.splice(0, this._buffer.length - this._maxSize);
|
||||
}
|
||||
}
|
||||
|
||||
_log(level, moduleId, message, err) {
|
||||
if (!this._enabled) return;
|
||||
const id = ++this._seq;
|
||||
const timestamp = now();
|
||||
const stack = err ? errorToStack(err) : null;
|
||||
this._push({
|
||||
id,
|
||||
timestamp,
|
||||
level,
|
||||
module: moduleId || "unknown",
|
||||
message: typeof message === "string" ? message : safeStringify(message),
|
||||
stack,
|
||||
});
|
||||
}
|
||||
|
||||
info(moduleId, message) {
|
||||
this._log("info", moduleId, message, null);
|
||||
}
|
||||
|
||||
warn(moduleId, message) {
|
||||
this._log("warn", moduleId, message, null);
|
||||
}
|
||||
|
||||
error(moduleId, message, err) {
|
||||
this._log("error", moduleId, message, err || null);
|
||||
}
|
||||
|
||||
_mountGlobalHooks() {
|
||||
if (this._mounted) return;
|
||||
this._mounted = true;
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
try {
|
||||
this._originalOnError = window.onerror;
|
||||
} catch {}
|
||||
try {
|
||||
this._originalOnUnhandledRejection = window.onunhandledrejection;
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
window.onerror = (message, source, lineno, colno, error) => {
|
||||
try {
|
||||
const loc = source ? `${source}:${lineno || 0}:${colno || 0}` : "";
|
||||
this.error("window", `${String(message || "error")} ${loc}`.trim(), error || null);
|
||||
} catch {}
|
||||
try {
|
||||
if (typeof this._originalOnError === "function") {
|
||||
return this._originalOnError(message, source, lineno, colno, error);
|
||||
}
|
||||
} catch {}
|
||||
return false;
|
||||
};
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
window.onunhandledrejection = (event) => {
|
||||
try {
|
||||
const reason = event?.reason;
|
||||
this.error("promise", "Unhandled promise rejection", reason || null);
|
||||
} catch {}
|
||||
try {
|
||||
if (typeof this._originalOnUnhandledRejection === "function") {
|
||||
return this._originalOnUnhandledRejection(event);
|
||||
}
|
||||
} catch {}
|
||||
return undefined;
|
||||
};
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (typeof console !== "undefined" && console) {
|
||||
this._originalConsole = this._originalConsole || {
|
||||
warn: console.warn?.bind(console),
|
||||
error: console.error?.bind(console),
|
||||
};
|
||||
|
||||
try {
|
||||
if (typeof this._originalConsole.warn === "function") {
|
||||
console.warn = (...args) => {
|
||||
try {
|
||||
const msg = args.map(a => (typeof a === "string" ? a : safeStringify(a))).join(" ");
|
||||
this.warn("console", msg);
|
||||
} catch {}
|
||||
return this._originalConsole.warn(...args);
|
||||
};
|
||||
}
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
if (typeof this._originalConsole.error === "function") {
|
||||
console.error = (...args) => {
|
||||
try {
|
||||
const msg = args.map(a => (typeof a === "string" ? a : safeStringify(a))).join(" ");
|
||||
this.error("console", msg, null);
|
||||
} catch {}
|
||||
return this._originalConsole.error(...args);
|
||||
};
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
_unmountGlobalHooks() {
|
||||
if (!this._mounted) return;
|
||||
this._mounted = false;
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
try {
|
||||
if (this._originalOnError !== null && this._originalOnError !== undefined) {
|
||||
window.onerror = this._originalOnError;
|
||||
} else {
|
||||
window.onerror = null;
|
||||
}
|
||||
} catch {}
|
||||
try {
|
||||
if (this._originalOnUnhandledRejection !== null && this._originalOnUnhandledRejection !== undefined) {
|
||||
window.onunhandledrejection = this._originalOnUnhandledRejection;
|
||||
} else {
|
||||
window.onunhandledrejection = null;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (typeof console !== "undefined" && console && this._originalConsole) {
|
||||
try {
|
||||
if (this._originalConsole.warn) console.warn = this._originalConsole.warn;
|
||||
} catch {}
|
||||
try {
|
||||
if (this._originalConsole.error) console.error = this._originalConsole.error;
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const logger = new LoggerCore();
|
||||
|
||||
export const xbLog = {
|
||||
enable: () => logger.enable(),
|
||||
disable: () => logger.disable(),
|
||||
isEnabled: () => logger.isEnabled(),
|
||||
setMaxSize: (n) => logger.setMaxSize(n),
|
||||
info: (moduleId, message) => logger.info(moduleId, message),
|
||||
warn: (moduleId, message) => logger.warn(moduleId, message),
|
||||
error: (moduleId, message, err) => logger.error(moduleId, message, err),
|
||||
getAll: () => logger.getAll(),
|
||||
clear: () => logger.clear(),
|
||||
export: () => logger.export(),
|
||||
};
|
||||
|
||||
export const CacheRegistry = (() => {
|
||||
const _registry = new Map();
|
||||
|
||||
function register(moduleId, cacheInfo) {
|
||||
if (!moduleId || !cacheInfo || typeof cacheInfo !== "object") return;
|
||||
_registry.set(String(moduleId), cacheInfo);
|
||||
}
|
||||
|
||||
function unregister(moduleId) {
|
||||
if (!moduleId) return;
|
||||
_registry.delete(String(moduleId));
|
||||
}
|
||||
|
||||
function getStats() {
|
||||
const out = [];
|
||||
for (const [moduleId, info] of _registry.entries()) {
|
||||
let size = null;
|
||||
let bytes = null;
|
||||
let name = null;
|
||||
let hasDetail = false;
|
||||
try { name = info?.name || moduleId; } catch { name = moduleId; }
|
||||
try { size = typeof info?.getSize === "function" ? info.getSize() : null; } catch { size = null; }
|
||||
try { bytes = typeof info?.getBytes === "function" ? info.getBytes() : null; } catch { bytes = null; }
|
||||
try { hasDetail = typeof info?.getDetail === "function"; } catch { hasDetail = false; }
|
||||
out.push({ moduleId, name, size, bytes, hasDetail });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function getDetail(moduleId) {
|
||||
const info = _registry.get(String(moduleId));
|
||||
if (!info || typeof info.getDetail !== "function") return null;
|
||||
try {
|
||||
return info.getDetail();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function clear(moduleId) {
|
||||
const info = _registry.get(String(moduleId));
|
||||
if (!info || typeof info.clear !== "function") return false;
|
||||
try {
|
||||
info.clear();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
const results = {};
|
||||
for (const moduleId of _registry.keys()) {
|
||||
results[moduleId] = clear(moduleId);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
return { register, unregister, getStats, getDetail, clear, clearAll };
|
||||
})();
|
||||
|
||||
export function enableDebugMode() {
|
||||
xbLog.enable();
|
||||
try { EventCenter.enableDebug?.(); } catch {}
|
||||
}
|
||||
|
||||
export function disableDebugMode() {
|
||||
xbLog.disable();
|
||||
try { EventCenter.disableDebug?.(); } catch {}
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.xbLog = xbLog;
|
||||
window.xbCacheRegistry = CacheRegistry;
|
||||
}
|
||||
|
||||
241
core/event-manager.js
Normal file
241
core/event-manager.js
Normal file
@@ -0,0 +1,241 @@
|
||||
import { eventSource, event_types } from "../../../../../script.js";
|
||||
|
||||
const registry = new Map();
|
||||
const customEvents = new Map();
|
||||
const handlerWrapperMap = new WeakMap();
|
||||
|
||||
export const EventCenter = {
|
||||
_debugEnabled: false,
|
||||
_eventHistory: [],
|
||||
_maxHistory: 100,
|
||||
_historySeq: 0,
|
||||
|
||||
enableDebug() {
|
||||
this._debugEnabled = true;
|
||||
},
|
||||
|
||||
disableDebug() {
|
||||
this._debugEnabled = false;
|
||||
this.clearHistory();
|
||||
},
|
||||
|
||||
getEventHistory() {
|
||||
return this._eventHistory.slice();
|
||||
},
|
||||
|
||||
clearHistory() {
|
||||
this._eventHistory.length = 0;
|
||||
},
|
||||
|
||||
_pushHistory(type, eventName, triggerModule, data) {
|
||||
if (!this._debugEnabled) return;
|
||||
try {
|
||||
const now = Date.now();
|
||||
const last = this._eventHistory[this._eventHistory.length - 1];
|
||||
if (
|
||||
last &&
|
||||
last.type === type &&
|
||||
last.eventName === eventName &&
|
||||
now - last.timestamp < 100
|
||||
) {
|
||||
last.repeatCount = (last.repeatCount || 1) + 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const id = ++this._historySeq;
|
||||
let dataSummary = null;
|
||||
try {
|
||||
if (data === undefined) {
|
||||
dataSummary = "undefined";
|
||||
} else if (data === null) {
|
||||
dataSummary = "null";
|
||||
} else if (typeof data === "string") {
|
||||
dataSummary = data.length > 120 ? data.slice(0, 120) + "…" : data;
|
||||
} else if (typeof data === "number" || typeof data === "boolean") {
|
||||
dataSummary = String(data);
|
||||
} else if (typeof data === "object") {
|
||||
const keys = Object.keys(data).slice(0, 6);
|
||||
dataSummary = `{ ${keys.join(", ")}${keys.length < Object.keys(data).length ? ", …" : ""} }`;
|
||||
} else {
|
||||
dataSummary = String(data).slice(0, 80);
|
||||
}
|
||||
} catch {
|
||||
dataSummary = "[unstringifiable]";
|
||||
}
|
||||
this._eventHistory.push({
|
||||
id,
|
||||
timestamp: now,
|
||||
type,
|
||||
eventName,
|
||||
triggerModule,
|
||||
dataSummary,
|
||||
repeatCount: 1,
|
||||
});
|
||||
if (this._eventHistory.length > this._maxHistory) {
|
||||
this._eventHistory.splice(0, this._eventHistory.length - this._maxHistory);
|
||||
}
|
||||
} catch {}
|
||||
},
|
||||
|
||||
on(moduleId, eventType, handler) {
|
||||
if (!moduleId || !eventType || typeof handler !== "function") return;
|
||||
if (!registry.has(moduleId)) {
|
||||
registry.set(moduleId, []);
|
||||
}
|
||||
const self = this;
|
||||
const wrappedHandler = function (...args) {
|
||||
if (self._debugEnabled) {
|
||||
self._pushHistory("ST_EVENT", eventType, moduleId, args[0]);
|
||||
}
|
||||
return handler.apply(this, args);
|
||||
};
|
||||
handlerWrapperMap.set(handler, wrappedHandler);
|
||||
try {
|
||||
eventSource.on(eventType, wrappedHandler);
|
||||
registry.get(moduleId).push({ eventType, handler, wrappedHandler });
|
||||
} catch (e) {
|
||||
console.error(`[EventCenter] Failed to register ${eventType} for ${moduleId}:`, e);
|
||||
}
|
||||
},
|
||||
|
||||
onMany(moduleId, eventTypes, handler) {
|
||||
if (!Array.isArray(eventTypes)) return;
|
||||
eventTypes.filter(Boolean).forEach((type) => this.on(moduleId, type, handler));
|
||||
},
|
||||
|
||||
off(moduleId, eventType, handler) {
|
||||
try {
|
||||
const listeners = registry.get(moduleId);
|
||||
if (!listeners) return;
|
||||
const idx = listeners.findIndex((l) => l.eventType === eventType && l.handler === handler);
|
||||
if (idx === -1) return;
|
||||
const entry = listeners[idx];
|
||||
eventSource.removeListener(eventType, entry.wrappedHandler);
|
||||
listeners.splice(idx, 1);
|
||||
handlerWrapperMap.delete(handler);
|
||||
} catch {}
|
||||
},
|
||||
|
||||
cleanup(moduleId) {
|
||||
const listeners = registry.get(moduleId);
|
||||
if (!listeners) return;
|
||||
listeners.forEach(({ eventType, handler, wrappedHandler }) => {
|
||||
try {
|
||||
eventSource.removeListener(eventType, wrappedHandler);
|
||||
handlerWrapperMap.delete(handler);
|
||||
} catch {}
|
||||
});
|
||||
registry.delete(moduleId);
|
||||
},
|
||||
|
||||
cleanupAll() {
|
||||
for (const moduleId of registry.keys()) {
|
||||
this.cleanup(moduleId);
|
||||
}
|
||||
customEvents.clear();
|
||||
},
|
||||
|
||||
count(moduleId) {
|
||||
return registry.get(moduleId)?.length || 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取统计:每个模块注册了多少监听器
|
||||
*/
|
||||
stats() {
|
||||
const stats = {};
|
||||
for (const [moduleId, listeners] of registry) {
|
||||
stats[moduleId] = listeners.length;
|
||||
}
|
||||
return stats;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取详细信息:每个模块监听了哪些具体事件
|
||||
*/
|
||||
statsDetail() {
|
||||
const detail = {};
|
||||
for (const [moduleId, listeners] of registry) {
|
||||
const eventCounts = {};
|
||||
for (const l of listeners) {
|
||||
const t = l.eventType || "unknown";
|
||||
eventCounts[t] = (eventCounts[t] || 0) + 1;
|
||||
}
|
||||
detail[moduleId] = {
|
||||
total: listeners.length,
|
||||
events: eventCounts,
|
||||
};
|
||||
}
|
||||
return detail;
|
||||
},
|
||||
|
||||
emit(eventName, data) {
|
||||
this._pushHistory("CUSTOM", eventName, null, data);
|
||||
const handlers = customEvents.get(eventName);
|
||||
if (!handlers) return;
|
||||
handlers.forEach(({ handler }) => {
|
||||
try {
|
||||
handler(data);
|
||||
} catch {}
|
||||
});
|
||||
},
|
||||
|
||||
subscribe(moduleId, eventName, handler) {
|
||||
if (!customEvents.has(eventName)) {
|
||||
customEvents.set(eventName, []);
|
||||
}
|
||||
customEvents.get(eventName).push({ moduleId, handler });
|
||||
},
|
||||
|
||||
unsubscribe(moduleId, eventName) {
|
||||
const handlers = customEvents.get(eventName);
|
||||
if (handlers) {
|
||||
const filtered = handlers.filter((h) => h.moduleId !== moduleId);
|
||||
if (filtered.length) {
|
||||
customEvents.set(eventName, filtered);
|
||||
} else {
|
||||
customEvents.delete(eventName);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export function createModuleEvents(moduleId) {
|
||||
return {
|
||||
on: (eventType, handler) => EventCenter.on(moduleId, eventType, handler),
|
||||
onMany: (eventTypes, handler) => EventCenter.onMany(moduleId, eventTypes, handler),
|
||||
off: (eventType, handler) => EventCenter.off(moduleId, eventType, handler),
|
||||
cleanup: () => EventCenter.cleanup(moduleId),
|
||||
count: () => EventCenter.count(moduleId),
|
||||
emit: (eventName, data) => EventCenter.emit(eventName, data),
|
||||
subscribe: (eventName, handler) => EventCenter.subscribe(moduleId, eventName, handler),
|
||||
unsubscribe: (eventName) => EventCenter.unsubscribe(moduleId, eventName),
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.xbEventCenter = {
|
||||
stats: () => EventCenter.stats(),
|
||||
statsDetail: () => EventCenter.statsDetail(),
|
||||
modules: () => Array.from(registry.keys()),
|
||||
history: () => EventCenter.getEventHistory(),
|
||||
clearHistory: () => EventCenter.clearHistory(),
|
||||
detail: (moduleId) => {
|
||||
const listeners = registry.get(moduleId);
|
||||
if (!listeners) return `模块 "${moduleId}" 未注册`;
|
||||
return listeners.map((l) => l.eventType).join(", ");
|
||||
},
|
||||
help: () =>
|
||||
console.log(`
|
||||
📊 小白X 事件管理器调试命令:
|
||||
xbEventCenter.stats() - 查看所有模块的事件数量
|
||||
xbEventCenter.statsDetail() - 查看所有模块监听的具体事件
|
||||
xbEventCenter.modules() - 列出所有已注册模块
|
||||
xbEventCenter.history() - 查看事件触发历史
|
||||
xbEventCenter.clearHistory() - 清空事件历史
|
||||
xbEventCenter.detail('模块名') - 查看模块监听的事件类型
|
||||
`),
|
||||
};
|
||||
}
|
||||
|
||||
export { event_types };
|
||||
30
core/slash-command.js
Normal file
30
core/slash-command.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import { getContext } from "../../../../extensions.js";
|
||||
|
||||
/**
|
||||
* 执行 SillyTavern 斜杠命令
|
||||
* @param {string} command - 要执行的命令
|
||||
* @returns {Promise<any>} 命令执行结果
|
||||
*/
|
||||
export async function executeSlashCommand(command) {
|
||||
try {
|
||||
if (!command) return { error: "命令为空" };
|
||||
if (!command.startsWith('/')) command = '/' + command;
|
||||
const { executeSlashCommands, substituteParams } = getContext();
|
||||
if (typeof executeSlashCommands !== 'function') throw new Error("executeSlashCommands 函数不可用");
|
||||
command = substituteParams(command);
|
||||
const result = await executeSlashCommands(command, true);
|
||||
if (result && typeof result === 'object' && result.pipe !== undefined) {
|
||||
const pipeValue = result.pipe;
|
||||
if (typeof pipeValue === 'string') {
|
||||
try { return JSON.parse(pipeValue); } catch { return pipeValue; }
|
||||
}
|
||||
return pipeValue;
|
||||
}
|
||||
if (typeof result === 'string' && result.trim()) {
|
||||
try { return JSON.parse(result); } catch { return result; }
|
||||
}
|
||||
return result === undefined ? "" : result;
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
384
core/variable-path.js
Normal file
384
core/variable-path.js
Normal file
@@ -0,0 +1,384 @@
|
||||
/**
|
||||
* @file core/variable-path.js
|
||||
* @description 变量路径解析与深层操作工具
|
||||
* @description 零依赖的纯函数模块,供多个变量相关模块使用
|
||||
*/
|
||||
|
||||
/* ============= 路径解析 ============= */
|
||||
|
||||
/**
|
||||
* 解析带中括号的路径
|
||||
* @param {string} path - 路径字符串,如 "a.b[0].c" 或 "a['key'].b"
|
||||
* @returns {Array<string|number>} 路径段数组,如 ["a", "b", 0, "c"]
|
||||
* @example
|
||||
* lwbSplitPathWithBrackets("a.b[0].c") // ["a", "b", 0, "c"]
|
||||
* lwbSplitPathWithBrackets("a['key'].b") // ["a", "key", "b"]
|
||||
* lwbSplitPathWithBrackets("a[\"0\"].b") // ["a", "0", "b"] (字符串"0")
|
||||
*/
|
||||
export function lwbSplitPathWithBrackets(path) {
|
||||
const s = String(path || '');
|
||||
const segs = [];
|
||||
let i = 0;
|
||||
let buf = '';
|
||||
|
||||
const flushBuf = () => {
|
||||
if (buf.length) {
|
||||
const pushed = /^\d+$/.test(buf) ? Number(buf) : buf;
|
||||
segs.push(pushed);
|
||||
buf = '';
|
||||
}
|
||||
};
|
||||
|
||||
while (i < s.length) {
|
||||
const ch = s[i];
|
||||
|
||||
if (ch === '.') {
|
||||
flushBuf();
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === '[') {
|
||||
flushBuf();
|
||||
i++;
|
||||
// 跳过空白
|
||||
while (i < s.length && /\s/.test(s[i])) i++;
|
||||
|
||||
let val;
|
||||
if (s[i] === '"' || s[i] === "'") {
|
||||
// 引号包裹的字符串键
|
||||
const quote = s[i++];
|
||||
let str = '';
|
||||
let esc = false;
|
||||
while (i < s.length) {
|
||||
const c = s[i++];
|
||||
if (esc) {
|
||||
str += c;
|
||||
esc = false;
|
||||
continue;
|
||||
}
|
||||
if (c === '\\') {
|
||||
esc = true;
|
||||
continue;
|
||||
}
|
||||
if (c === quote) break;
|
||||
str += c;
|
||||
}
|
||||
val = str;
|
||||
while (i < s.length && /\s/.test(s[i])) i++;
|
||||
if (s[i] === ']') i++;
|
||||
} else {
|
||||
// 无引号,可能是数字索引或普通键
|
||||
let raw = '';
|
||||
while (i < s.length && s[i] !== ']') raw += s[i++];
|
||||
if (s[i] === ']') i++;
|
||||
const trimmed = String(raw).trim();
|
||||
val = /^-?\d+$/.test(trimmed) ? Number(trimmed) : trimmed;
|
||||
}
|
||||
segs.push(val);
|
||||
continue;
|
||||
}
|
||||
|
||||
buf += ch;
|
||||
i++;
|
||||
}
|
||||
|
||||
flushBuf();
|
||||
return segs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分离路径和值(用于命令解析)
|
||||
* @param {string} raw - 原始字符串,如 "a.b[0] some value"
|
||||
* @returns {{path: string, value: string}} 路径和值
|
||||
* @example
|
||||
* lwbSplitPathAndValue("a.b[0] hello") // { path: "a.b[0]", value: "hello" }
|
||||
* lwbSplitPathAndValue("a.b") // { path: "a.b", value: "" }
|
||||
*/
|
||||
export function lwbSplitPathAndValue(raw) {
|
||||
const s = String(raw || '');
|
||||
let i = 0;
|
||||
let depth = 0; // 中括号深度
|
||||
let inQ = false; // 是否在引号内
|
||||
let qch = ''; // 当前引号字符
|
||||
|
||||
for (; i < s.length; i++) {
|
||||
const ch = s[i];
|
||||
|
||||
if (inQ) {
|
||||
if (ch === '\\') {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (ch === qch) {
|
||||
inQ = false;
|
||||
qch = '';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === '"' || ch === "'") {
|
||||
inQ = true;
|
||||
qch = ch;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === '[') {
|
||||
depth++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === ']') {
|
||||
depth = Math.max(0, depth - 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 在顶层遇到空白,分割
|
||||
if (depth === 0 && /\s/.test(ch)) {
|
||||
const path = s.slice(0, i).trim();
|
||||
const value = s.slice(i + 1).trim();
|
||||
return { path, value };
|
||||
}
|
||||
}
|
||||
|
||||
return { path: s.trim(), value: '' };
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单分割路径段(仅支持点号分隔)
|
||||
* @param {string} path - 路径字符串
|
||||
* @returns {Array<string|number>} 路径段数组
|
||||
*/
|
||||
export function splitPathSegments(path) {
|
||||
return String(path || '')
|
||||
.split('.')
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean)
|
||||
.map(seg => /^\d+$/.test(seg) ? Number(seg) : seg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化路径(统一为点号分隔格式)
|
||||
* @param {string} path - 路径字符串
|
||||
* @returns {string} 规范化后的路径
|
||||
* @example
|
||||
* normalizePath("a[0].b['c']") // "a.0.b.c"
|
||||
*/
|
||||
export function normalizePath(path) {
|
||||
try {
|
||||
const segs = lwbSplitPathWithBrackets(path);
|
||||
return segs.map(s => String(s)).join('.');
|
||||
} catch {
|
||||
return String(path || '').trim();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取根变量名和子路径
|
||||
* @param {string} name - 完整路径
|
||||
* @returns {{root: string, subPath: string}}
|
||||
* @example
|
||||
* getRootAndPath("a.b.c") // { root: "a", subPath: "b.c" }
|
||||
* getRootAndPath("a") // { root: "a", subPath: "" }
|
||||
*/
|
||||
export function getRootAndPath(name) {
|
||||
const segs = String(name || '').split('.').map(s => s.trim()).filter(Boolean);
|
||||
if (segs.length <= 1) {
|
||||
return { root: String(name || '').trim(), subPath: '' };
|
||||
}
|
||||
return { root: segs[0], subPath: segs.slice(1).join('.') };
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接路径
|
||||
* @param {string} base - 基础路径
|
||||
* @param {string} more - 追加路径
|
||||
* @returns {string} 拼接后的路径
|
||||
*/
|
||||
export function joinPath(base, more) {
|
||||
return base ? (more ? base + '.' + more : base) : more;
|
||||
}
|
||||
|
||||
/* ============= 深层对象操作 ============= */
|
||||
|
||||
/**
|
||||
* 确保深层容器存在
|
||||
* @param {Object|Array} root - 根对象
|
||||
* @param {Array<string|number>} segs - 路径段数组
|
||||
* @returns {{parent: Object|Array, lastKey: string|number}} 父容器和最后一个键
|
||||
*/
|
||||
export function ensureDeepContainer(root, segs) {
|
||||
let cur = root;
|
||||
|
||||
for (let i = 0; i < segs.length - 1; i++) {
|
||||
const key = segs[i];
|
||||
const nextKey = segs[i + 1];
|
||||
const shouldBeArray = typeof nextKey === 'number';
|
||||
|
||||
let val = cur?.[key];
|
||||
if (val === undefined || val === null || typeof val !== 'object') {
|
||||
cur[key] = shouldBeArray ? [] : {};
|
||||
}
|
||||
cur = cur[key];
|
||||
}
|
||||
|
||||
return {
|
||||
parent: cur,
|
||||
lastKey: segs[segs.length - 1]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置深层值
|
||||
* @param {Object} root - 根对象
|
||||
* @param {string} path - 路径(点号分隔)
|
||||
* @param {*} value - 要设置的值
|
||||
* @returns {boolean} 是否有变化
|
||||
*/
|
||||
export function setDeepValue(root, path, value) {
|
||||
const segs = splitPathSegments(path);
|
||||
if (segs.length === 0) return false;
|
||||
|
||||
const { parent, lastKey } = ensureDeepContainer(root, segs);
|
||||
const prev = parent[lastKey];
|
||||
|
||||
if (prev !== value) {
|
||||
parent[lastKey] = value;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 向深层数组推入值(去重)
|
||||
* @param {Object} root - 根对象
|
||||
* @param {string} path - 路径
|
||||
* @param {*|Array} values - 要推入的值
|
||||
* @returns {boolean} 是否有变化
|
||||
*/
|
||||
export function pushDeepValue(root, path, values) {
|
||||
const segs = splitPathSegments(path);
|
||||
if (segs.length === 0) return false;
|
||||
|
||||
const { parent, lastKey } = ensureDeepContainer(root, segs);
|
||||
|
||||
let arr = parent[lastKey];
|
||||
let changed = false;
|
||||
|
||||
// 确保是数组
|
||||
if (!Array.isArray(arr)) {
|
||||
arr = arr === undefined ? [] : [arr];
|
||||
}
|
||||
|
||||
const incoming = Array.isArray(values) ? values : [values];
|
||||
for (const v of incoming) {
|
||||
if (!arr.includes(v)) {
|
||||
arr.push(v);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
parent[lastKey] = arr;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除深层键
|
||||
* @param {Object} root - 根对象
|
||||
* @param {string} path - 路径
|
||||
* @returns {boolean} 是否成功删除
|
||||
*/
|
||||
export function deleteDeepKey(root, path) {
|
||||
const segs = splitPathSegments(path);
|
||||
if (segs.length === 0) return false;
|
||||
|
||||
const { parent, lastKey } = ensureDeepContainer(root, segs);
|
||||
|
||||
// 父级是数组
|
||||
if (Array.isArray(parent)) {
|
||||
// 数字索引:直接删除
|
||||
if (typeof lastKey === 'number' && lastKey >= 0 && lastKey < parent.length) {
|
||||
parent.splice(lastKey, 1);
|
||||
return true;
|
||||
}
|
||||
// 值匹配:删除所有匹配项
|
||||
const equal = (a, b) => a === b || a == b || String(a) === String(b);
|
||||
let changed = false;
|
||||
for (let i = parent.length - 1; i >= 0; i--) {
|
||||
if (equal(parent[i], lastKey)) {
|
||||
parent.splice(i, 1);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
// 父级是对象
|
||||
if (Object.prototype.hasOwnProperty.call(parent, lastKey)) {
|
||||
delete parent[lastKey];
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ============= 值处理工具 ============= */
|
||||
|
||||
/**
|
||||
* 安全的 JSON 序列化
|
||||
* @param {*} v - 要序列化的值
|
||||
* @returns {string} JSON 字符串,失败返回空字符串
|
||||
*/
|
||||
export function safeJSONStringify(v) {
|
||||
try {
|
||||
return JSON.stringify(v);
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试将原始值解析为对象
|
||||
* @param {*} rootRaw - 原始值(可能是字符串或对象)
|
||||
* @returns {Object|Array|null} 解析后的对象,失败返回 null
|
||||
*/
|
||||
export function maybeParseObject(rootRaw) {
|
||||
if (typeof rootRaw === 'string') {
|
||||
try {
|
||||
const s = rootRaw.trim();
|
||||
return (s && (s[0] === '{' || s[0] === '[')) ? JSON.parse(s) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return (rootRaw && typeof rootRaw === 'object') ? rootRaw : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将值转换为输出字符串
|
||||
* @param {*} v - 任意值
|
||||
* @returns {string} 字符串表示
|
||||
*/
|
||||
export function valueToString(v) {
|
||||
if (v == null) return '';
|
||||
if (typeof v === 'object') return safeJSONStringify(v) || '';
|
||||
return String(v);
|
||||
}
|
||||
|
||||
/**
|
||||
* 深度克隆对象(使用 structuredClone 或 JSON)
|
||||
* @param {*} obj - 要克隆的对象
|
||||
* @returns {*} 克隆后的对象
|
||||
*/
|
||||
export function deepClone(obj) {
|
||||
if (obj === null || typeof obj !== 'object') return obj;
|
||||
try {
|
||||
return typeof structuredClone === 'function'
|
||||
? structuredClone(obj)
|
||||
: JSON.parse(JSON.stringify(obj));
|
||||
} catch {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user