fix(server): 组帧漏写帧级 nullmap 位,opt 字段改值会产出错位帧(M10 回显的真因)
space.js 的 BuildInitFrames 自己逐条调 Container.Write / Semantic.EncodeRecord 出负载,再把模板的 NullmapHex 原样带上。但帧级 nullmap 是所有记录共用的一串位(每条记录 1 个形态位 + 每个 opt 字段 1 个空/非空位,按编码顺序排),只有 semantic.EncodeFrameBody 持位游标(容器与字段两条路径都会推进 Ctx)。于是「把 opt<T> 由空改成有值」时负载多出 T 的字节数而 nullmap 位仍是 1(=空),解码侧按位读到空、把那多出来的字节当成下一条记录头 —— 实测 devicesLoaded 的 mountedDevice 由 null 改成设备 id 时帧 255848B→255856B 且 Bad(29 条记录只解出 7 条)。M7 覆盖 mountedItems 时没暴露,是因为 list<T> 改长度不占 nullmap 位(长度编在负载里)。改法:BuildInitFrames 把记录按模板形态原样交给 Semantic.EncodeFrameBody(容器 → Layout+Value、字段 → Fields、解不动 → Hex、旧模板的现成 Payload 仍透传),由它按编码顺序重建 nullmap 位;没传 semantic 实例时保留旧路径。验证:字节中性 —— names/ 下全部模板(init+session,61 帧)改动前后逐字节相同 61/61;修复有效 —— 把 s2c_0004 槽位 920009631046 的 mountedDevice 由 null 改为设备 id 后,帧 +8B、解码 29 条记录 Bad=false、nullmap 长度不变且恰好 1 个字节变化、其余 27 个槽位 0 处变化;回归 —— semantic.js --demo 全部断言通过(含「space.js 组帧 == 参考字节」那条)、selftest 逐字节一致 1108/不一致 9/解码中断 90、CompareContainerDecode 5 帧/26950/0。
This commit is contained in:
1 parent
356647937e
commit
2299b0996b
4 files changed
+173
-23
No files matched your search
@@ -56,11 +56,14 @@ const LOG_DIR = path.resolve(Arg('log', path.join(PROJECT, 'server-logs', LABEL)
|
||||
// 默认关 —— 查库应答照发,回放器一字不改(现状已知能跑通,不拿它冒险)。
|
||||
const STORE_PREEMPT = Argv.indexOf('--store-preempt') >= 0;
|
||||
// 查库接管范围:默认**只接管 ba.lcr**(理由见 responder.js 头注:模板/回放里已经播发过的东西不能重复发,
|
||||
// 否则客户端 nap() 会抛 "Model already subscribed!")。--store-all 才把 ls.xbl / G.q1wd 一起打开,仅供实验。
|
||||
// 否则客户端 nap() 会抛 "Model already subscribed!")。
|
||||
// M10 追加 DeviceInsert(dh.hcu,把设备装到挂载位):它是一条**写**上行,必须由查库路径接管(回放里没有它),
|
||||
// 且它的应答 devicesLoaded 与 ba.lcr 同源(客户端走 srg 覆盖写,重复消费幂等),所以默认开。
|
||||
// --store-all 才把 ls.xbl / G.q1wd 一起打开,仅供实验。
|
||||
const STORE_ALL = Argv.indexOf('--store-all') >= 0;
|
||||
const STORE_SCOPE = STORE_ALL
|
||||
? { BaLcr: true, LsXbl: true, GQ1wd: true }
|
||||
: { BaLcr: true, LsXbl: false, GQ1wd: false };
|
||||
? { BaLcr: true, DeviceInsert: true, LsXbl: true, GQ1wd: true }
|
||||
: { BaLcr: true, DeviceInsert: true, LsXbl: false, GQ1wd: false };
|
||||
|
||||
// ws 模块:优先本项目解析,回落 sockets/(唯一装过 ws 的地方)
|
||||
function RequireWs() {
|
||||
@@ -176,14 +179,24 @@ function HandleEnterSpace(State, Client, Conn, Body, At) {
|
||||
if (Session) {
|
||||
// M7 回显:进空间时顺手刷一次「已挂载物品」快照,保证下发的是库里此刻的真实状态
|
||||
Store.LoadMountedFlags().catch(() => {});
|
||||
// 字段级覆盖钩子:只改 mountedItems 这一条记录的内容(库里 Items.Mounted=1 的集合),
|
||||
// 其余记录/字段一字不动;集合无差异时返回原数组 ⇒ 帧字节仍与抓包逐字节一致。
|
||||
// 字段级覆盖钩子:只改 mountedItems(库 Items.Mounted=1 集合)与 devicesLoaded 的 mountedDevice
|
||||
// (库 Mounted.DeviceId)这两处记录的内容,其余记录/字段一字不动;无差异时返回原数组
|
||||
// ⇒ 帧字节仍与抓包逐字节一致。M10 的 devicesLoaded 覆盖与 M7 同一套机制。
|
||||
const RecordOverride = (F, Records) => {
|
||||
const R = Store.OverrideMountedRecord(Records);
|
||||
if (!R || !R.Changed) return Records;
|
||||
State.Log(' → 回显覆盖:mountedItems 由模板 ' + R.Before.length + ' 项改为库中 ' + R.After.length +
|
||||
' 项(' + F.File + ';库值 [' + R.After.join(', ') + '])');
|
||||
return R.Records;
|
||||
let Out = Records;
|
||||
const R = Store.OverrideMountedRecord(Out);
|
||||
if (R && R.Changed) {
|
||||
State.Log(' → 回显覆盖:mountedItems 由模板 ' + R.Before.length + ' 项改为库中 ' + R.After.length +
|
||||
' 项(' + F.File + ';库值 [' + R.After.join(', ') + '])');
|
||||
Out = R.Records;
|
||||
}
|
||||
const D = Store.OverrideDevicesLoadedRecords(Out);
|
||||
if (D && D.Changed) {
|
||||
State.Log(' → 回显覆盖:devicesLoaded 的 mountedDevice 由模板 [' + D.Before.join(', ') +
|
||||
'] 改为库中 [' + D.After.join(', ') + '](' + F.File + ')');
|
||||
Out = D.Records;
|
||||
}
|
||||
return Out;
|
||||
};
|
||||
const Replay = SessionReplay.MakeReplay(Session.Frames, {
|
||||
Container, Semantic, Log: (M) => State.Log(M), RecordOverride,
|
||||
|
||||
@@ -7,9 +7,15 @@
|
||||
// 客户端 nap() 里 `s2c_1.has(t)` 命中 → module_10849.js:440 抛 "Model already subscribed!"。
|
||||
// (`nap` 是**模型订阅**,同一模型订阅两次直接抛;这跟 devicesLoaded 那种「覆盖写设备行」不一样。)
|
||||
//
|
||||
// 接管范围由 Scope 决定,默认**只接管 ba.lcr**:
|
||||
// 接管范围由 Scope 决定,默认**只接管 ba.lcr**(+ M10 的 DeviceInsert):
|
||||
// BaLcr(默认开) ba.lcr 7806260029063678398 靶 = 槽位对象 id → 一条 devicesLoaded
|
||||
// 安全原因:客户端消费它走 srg(按设备 id 覆盖写 + 置 mounted 位),重复下发幂等,不会抛异常。
|
||||
// DeviceInsert(默认开) dh.hcu 3804107380183105968 靶 = 槽位对象 id,负载 i64 = 设备对象 id
|
||||
// → 先落库(Mounted.DeviceId 覆盖写,见 store.js 的 SetDeviceMounted),再回一条该槽位的 devicesLoaded。
|
||||
// 这是 M10 查清的「把某个设备装到某个挂载位」的上行;逐行源码链见 store.js 的 SetDeviceMounted 头注
|
||||
// 与客户端 Uplink.kt 的 DH_HCU 头注。**真实抓包里没有这条上行的样本**(会话里没做过这个动作),
|
||||
// 所以这里没有「逐字节对照」这一层保证,只有源码链 + uplink_table.json 的字段表。
|
||||
// 默认开的原因与 ba.lcr 同源:devicesLoaded 走 srg 覆盖写,客户端重复消费幂等。
|
||||
// LsXbl(默认关) ls.xbl 2528403217084400809 靶 = 要卸载的对象 id → 一条 _unloadObjects([靶])
|
||||
// 关的原因:模板空间在进空间时已经把「对 ls.xbl 的应答」一起播发了(模板是照抄真实会话的帧、
|
||||
// 不区分触发者),再答一遍属于重复卸载,没验证过客户端会不会有副作用。
|
||||
@@ -36,11 +42,13 @@ const Store = require('./store');
|
||||
|
||||
const METHOD = {
|
||||
BaLcr: '7806260029063678398', // ba.lcr
|
||||
DeviceInsert: '3804107380183105968', // dh.hcu(把设备装到挂载位;负载 i64 = 设备 id)
|
||||
LsXbl: '2528403217084400809', // ls.xbl
|
||||
GQ1wd: '1816792453857564692', // G.q1wd(客户端请求对象数据,负载 i32)
|
||||
};
|
||||
const TAG = {};
|
||||
TAG[METHOD.BaLcr] = 'ba.lcr';
|
||||
TAG[METHOD.DeviceInsert] = 'dh.hcu';
|
||||
TAG[METHOD.LsXbl] = 'ls.xbl';
|
||||
TAG[METHOD.GQ1wd] = 'G.q1wd';
|
||||
|
||||
@@ -52,8 +60,8 @@ function MakeResponder(Deps) {
|
||||
// 默认关(false):查库应答照发,回放器行为与今天一模一样 —— 现状已知能跑通,不拿它冒险;
|
||||
// index.js 的 --store-preempt 打开严格模式。
|
||||
const Preempt = !!D.Preempt;
|
||||
// 接管范围:默认只开 ba.lcr(理由见文件头注)
|
||||
const Scope = Object.assign({ BaLcr: true, LsXbl: false, GQ1wd: false }, D.Scope || {});
|
||||
// 接管范围:默认只开 ba.lcr 与 M10 的 DeviceInsert(理由见文件头注)
|
||||
const Scope = Object.assign({ BaLcr: true, DeviceInsert: true, LsXbl: false, GQ1wd: false }, D.Scope || {});
|
||||
const XTable = new Map();
|
||||
const Count = { Hit: 0, Miss: 0, Disabled: 0, Unavailable: 0, Fallback: 0, FallbackFrames: 0, Suppressed: 0, ByMethod: {} };
|
||||
|
||||
@@ -84,6 +92,12 @@ function MakeResponder(Deps) {
|
||||
return Payload.readInt32BE(0);
|
||||
}
|
||||
|
||||
// 上行的 i64 负载(M10 的 dh.hcu:8 字节大端,设备对象 id)。不足 8 字节 → null(不猜)
|
||||
function ReadI64(Payload) {
|
||||
if (!Buffer.isBuffer(Payload) || Payload.length < 8) return null;
|
||||
return Payload.readBigInt64BE(0).toString();
|
||||
}
|
||||
|
||||
// 同步判定:返回 thunk(查库路径**已接管**这条上行)或 null(让位给回放 / 未实现清单)
|
||||
// Ctx = { Sender, Replay, ReplaySender }:Sender(Buf)→bool 是发字节的出口;Replay 是会话回放器(可为 null)
|
||||
function Prepare(R, TargetId, Ctx) {
|
||||
@@ -96,12 +110,39 @@ function MakeResponder(Deps) {
|
||||
return null;
|
||||
}
|
||||
// 接管范围之外的上行直接让位(默认的 ls.xbl / G.q1wd 走这里,见文件头注)
|
||||
const ScopeKey = Method === METHOD.BaLcr ? 'BaLcr' : Method === METHOD.LsXbl ? 'LsXbl' : 'GQ1wd';
|
||||
const ScopeKey =
|
||||
Method === METHOD.BaLcr ? 'BaLcr' :
|
||||
Method === METHOD.DeviceInsert ? 'DeviceInsert' :
|
||||
Method === METHOD.LsXbl ? 'LsXbl' : 'GQ1wd';
|
||||
if (!Scope[ScopeKey]) {
|
||||
Bump('Disabled', Tag);
|
||||
return null;
|
||||
}
|
||||
|
||||
// M10:把设备装到挂载位(dh.hcu)。这是一条**写**上行:先落库 Mounted.DeviceId,再用库里的值组一条
|
||||
// devicesLoaded 回显(客户端 srg 覆盖写该槽位的设备行 + mountedDevice,重复下发幂等)。
|
||||
if (Method === METHOD.DeviceInsert) {
|
||||
const Device = ReadI64(R.Payload);
|
||||
if (Device === null) {
|
||||
Bump('Miss', Tag);
|
||||
Log('→ 查库未命中:dh.hcu 负载不足 8 字节(解不出设备 id)→ 回落回放');
|
||||
return null;
|
||||
}
|
||||
return async () => {
|
||||
try {
|
||||
await Store.SetDeviceMounted(String(TargetId), Device);
|
||||
const Rec = Store.DevicesLoaded(SpaceId, String(TargetId));
|
||||
if (!Rec) throw new Error('库里的 DeviceSlots 没有挂载位 ' + TargetId);
|
||||
Log('→ 查库应答:devicesLoaded(槽位 ' + TargetId + ') ' + Rec.Fields.devices.length +
|
||||
' 个设备 / mountedDevice=' + (Rec.Fields.mountedDevice === null ? 'null' : Rec.Fields.mountedDevice));
|
||||
return Emit([Rec], Tag, R, TargetId, C);
|
||||
} catch (Err) {
|
||||
Log(' ⚠ 设备落库路径未成(' + Err.message + ')→ 回落会话回放');
|
||||
return Fallback(R, TargetId, C, Tag);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (Method === METHOD.BaLcr) {
|
||||
const Rec = Store.DevicesLoaded(SpaceId, String(TargetId));
|
||||
if (!Rec) {
|
||||
|
||||
@@ -44,20 +44,49 @@ function LoadInitTemplate(NamesDir, SpaceId) {
|
||||
|
||||
// 模板 → [{ File, DelayMs, Frame }];负载由 Container / Semantic 重新编码
|
||||
// 记录的三种形态:① { Layout, Value } 容器(container.js)② { Fields } 普通字段(semantic.js)③ { Hex } 未解原样透传
|
||||
//
|
||||
// 【为什么把记录原样交给 Semantic.EncodeFrameBody,而不是在这里逐条编成字节】
|
||||
// 帧级 nullmap 是**所有记录共用的一串位**:每条记录先占 1 个「形态位」,随后它负载里每个
|
||||
// opt<T> 字段再占 1 个「空/非空位」,按编码顺序排下去(见 semantic.js 头注的 gap A)。
|
||||
// 所以「改一个 opt 字段的值」= 写出负载 **且** 把那位由 1 改 0 —— 两件事必须一起做。
|
||||
// 只有 semantic.EncodeFrameBody 持有那个位游标(它对容器与字段两条路径都会推进 Ctx;
|
||||
// container.js 的 Write/Read 都接受 Ctx)。旧写法(本函数自己 EncodeRecord 不带游标 +
|
||||
// nullmap 原样带上)只写负载、不改位,产出的是**错位帧**:负载多出 T 的字节数,而解码侧按
|
||||
// 位读到「空」,于是把那多出来的字节当成下一条记录的头(实测 devicesLoaded 的 mountedDevice
|
||||
// 由空改成有值时,帧从 255848B 变 255856B 且 Bad)。
|
||||
// 注意 list<T> 字段改长度不碰 nullmap 位(长度在负载里编),所以 M7 覆盖 mountedItems 时没暴露。
|
||||
function BuildInitFrames(Template, Container, Semantic) {
|
||||
const Out = [];
|
||||
for (const F of Template.Frames || []) {
|
||||
// 没有语义层实例时退回旧路径(逐条编字节 + nullmap 原样带上):它只适用于「记录类型全是
|
||||
// 容器/Hex,或改动的只是 list 这类不占位的字段」。空模板就别在这里抛错,交给语义层报。
|
||||
if (!Semantic) {
|
||||
const Old = F.Records.map((R) => {
|
||||
let Payload;
|
||||
if (R.Layout !== undefined) Payload = Container.Write(R.Layout, R.Value);
|
||||
else Payload = Buffer.from(R.Hex || '', 'hex');
|
||||
return { ObjectId: R.ObjectId, MethodId: R.MethodId, Payload };
|
||||
});
|
||||
Out.push({ File: F.File, DelayMs: F.DelayMs || 0, Frame: BuildSpaceFrame(F.SpaceId, Old, F.NullmapHex) });
|
||||
continue;
|
||||
}
|
||||
const Records = F.Records.map((R) => {
|
||||
let Payload;
|
||||
if (R.Layout !== undefined) Payload = Container.Write(R.Layout, R.Value);
|
||||
else if (R.Fields !== undefined) {
|
||||
// 字段 JSON 必须由语义层现编:它才知道该 methodId 的有序字段与类型
|
||||
if (!Semantic) throw new Error('模板记录是 Fields 形态(methodId=' + R.MethodId + '),但没传 semantic.js 实例');
|
||||
Payload = Semantic.EncodeRecord(R.MethodId, R);
|
||||
} else Payload = Buffer.from(R.Hex || '', 'hex');
|
||||
return { ObjectId: R.ObjectId, MethodId: R.MethodId, Payload };
|
||||
// objectId 缺省 → 回退帧级 SpaceId(与 BuildSpaceFrame 的兼容路径同规则)
|
||||
const ObjectId = R.ObjectId === undefined || R.ObjectId === null ? F.SpaceId : R.ObjectId;
|
||||
if (R.Hex !== undefined) return { ObjectId, MethodId: R.MethodId, Hex: R.Hex };
|
||||
if (R.Layout !== undefined) return { ObjectId, MethodId: R.MethodId, Layout: R.Layout, Value: R.Value };
|
||||
if (R.Fields !== undefined) return { ObjectId, MethodId: R.MethodId, Fields: R.Fields };
|
||||
// 旧模板可能直接存了编好的负载:原样透传(语义层把它当不可重建记录,之后的位保持原值)
|
||||
return { ObjectId, MethodId: R.MethodId, Payload: R.Payload };
|
||||
});
|
||||
// EncodeFrameBody 返回的就是完整帧体 [nullmap][记录…];再用 EncodeFrame 补长度头
|
||||
const Body = Semantic.EncodeFrameBody({ Nullmap: F.NullmapHex || '00', Records });
|
||||
const NmSize = Link.NullmapSize(Body, 0);
|
||||
Out.push({
|
||||
File: F.File,
|
||||
DelayMs: F.DelayMs || 0,
|
||||
Frame: Link.EncodeFrame(Body.slice(NmSize), Body.slice(0, NmSize)),
|
||||
});
|
||||
Out.push({ File: F.File, DelayMs: F.DelayMs || 0, Frame: BuildSpaceFrame(F.SpaceId, Records, F.NullmapHex) });
|
||||
}
|
||||
return Out;
|
||||
}
|
||||
|
||||
@@ -246,6 +246,71 @@ function OverrideMountedRecord(Records) {
|
||||
return { Records: New, Changed: true, Before, After };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 挂载位当前设备(M10:设备落库 + devicesLoaded 回显覆盖)
|
||||
|
||||
// 设备落库:把「某挂载位当前装备」写进 Mounted.DeviceId(M10 查清的口径,依据见 schema.sql 与 responder.js)。
|
||||
// ★ 为什么是 Mounted.DeviceId:devicesLoaded 的 mountedDevice 就是 store.js 的 DevicesLoaded() 从
|
||||
// Cache.Devices.Mounted 取的,而那份缓存整表来自 `SELECT SlotObjectId, DeviceId FROM Mounted`。
|
||||
// SlotDevices 只是「该槽位可用设备清单」,跟"当前装着谁"无关。
|
||||
// ★ 幂等/覆盖:同一槽位换设备 = 同一行 UPDATE 覆盖;值没变时 UPDATE 不匹配任何行(不产生副作用)。
|
||||
// ★ 两道校验(不猜):槽位必须在 DeviceSlots 里、设备必须在 DeviceSlots→SlotDevices 的清单里;
|
||||
// 否则只打日志、不写库 —— 上行负载是不可信输入,写错槽位比不写更难查。
|
||||
async function SetDeviceMounted(SlotObjectId, DeviceId) {
|
||||
Require();
|
||||
const Slot = String(SlotObjectId);
|
||||
const Dev = String(DeviceId);
|
||||
if (!Cache.Devices || !Cache.Devices.Slots.has(Slot)) {
|
||||
Log(' ⚠ 落库失败:槽位 ' + Slot + ' 不在 DeviceSlots 里 → 不写(不猜槽位)');
|
||||
return false;
|
||||
}
|
||||
const Avail = Cache.Devices.BySlot.get(Slot) || [];
|
||||
if (Avail.indexOf(Dev) < 0) {
|
||||
Log(' ⚠ 落库失败:设备 ' + Dev + ' 不在槽位 ' + Slot + ' 的可用设备清单里(清单 ' + Avail.length + ' 个)→ 不写(不猜设备)');
|
||||
return false;
|
||||
}
|
||||
const Before = Cache.Devices.Mounted.has(Slot) ? Cache.Devices.Mounted.get(Slot) : null;
|
||||
if (Before === Dev) {
|
||||
Log('→ 落库:槽位 ' + Slot + ' 的装备改为 设备 ' + Dev + '(原 ' + (Before === null ? '无' : Before) + ')(值未变,幂等跳过)');
|
||||
return false;
|
||||
}
|
||||
const [Res] = await Pool.query(
|
||||
'UPDATE Mounted SET DeviceId = ? WHERE SlotObjectId = ? AND (DeviceId IS NULL OR DeviceId <> ?)',
|
||||
[Dev, Slot, Dev],
|
||||
);
|
||||
Cache.Devices.Mounted.set(Slot, Dev); // 内存快照同步(DevicesLoaded 是同步路径,必须立刻可见)
|
||||
Log('→ 落库:槽位 ' + Slot + ' 的装备改为 设备 ' + Dev + '(原 ' + (Before === null ? '无' : Before) + ')' +
|
||||
(Res.affectedRows ? '(UPDATE 1 行)' : '(库里没有这一行:仅内存生效,DB 未变)'));
|
||||
return Res.affectedRows > 0;
|
||||
}
|
||||
|
||||
// 字段级覆盖:把帧里所有 devicesLoaded(methodId 7615142300796401815/…1818)记录的 mountedDevice
|
||||
// 换成库里 Mounted.DeviceId 的值,其余记录与字段一字不动。
|
||||
// 与 M7 的 OverrideMountedRecord 同一套口径:**只在确实有差异时**返回新数组(集合/值相同 ⇒ 原样返回 ⇒
|
||||
// 帧字节仍与抓包逐字节一致)。返回 { Records, Changed, Before, After };帧里没有 devicesLoaded → null。
|
||||
// 覆盖范围:只覆盖**库里 Mounted 表里有这一行**的槽位(没有这一行 ⇒ 不动,绝不用 null 冒充"库里没装")。
|
||||
function OverrideDevicesLoadedRecords(Records) {
|
||||
if (!Array.isArray(Records) || !Cache.Devices) return null;
|
||||
const Before = [];
|
||||
const After = [];
|
||||
let Changed = false;
|
||||
const New = Records.map((R) => {
|
||||
if (String(R.MethodId) !== METHOD.DevicesLoaded || !R.Fields) return R;
|
||||
const Slot = String(R.ObjectId);
|
||||
if (!Cache.Devices.Mounted.has(Slot)) return R;
|
||||
const Db = Cache.Devices.Mounted.get(Slot);
|
||||
const Cur = R.Fields.mountedDevice === undefined || R.Fields.mountedDevice === null
|
||||
? null
|
||||
: String(R.Fields.mountedDevice);
|
||||
Before.push(Cur);
|
||||
After.push(Db);
|
||||
if (Cur === Db) return R;
|
||||
Changed = true;
|
||||
return Object.assign({}, R, { Fields: Object.assign({}, R.Fields, { mountedDevice: Db }) });
|
||||
});
|
||||
if (!Before.length) return null;
|
||||
return { Records: Changed ? New : Records, Changed, Before, After };
|
||||
}
|
||||
|
||||
function Require() {
|
||||
if (!Pool) throw new Error('store 还没连库');
|
||||
}
|
||||
@@ -807,6 +872,8 @@ module.exports = {
|
||||
SpaceObjectIds, BuildFrame, MakeNullmap, CountBits, Cache,
|
||||
// M7:装备落库 + mountedItems 回显覆盖
|
||||
SetMounted, LoadMountedFlags, MountedFlagsSnapshot, OverrideMountedRecord,
|
||||
// M10:挂载位设备落库 + devicesLoaded 回显覆盖
|
||||
SetDeviceMounted, OverrideDevicesLoadedRecords,
|
||||
};
|
||||
|
||||
if (process.argv.indexOf('--selfcheck') >= 0 || process.argv.indexOf('--verify') >= 0) {
|
||||
|
||||
Reference in new issue
Block a user