@napuketto/kernel 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1186,7 +1186,20 @@ function toCanonicalElement(el) {
1186
1186
  function toCanonicalElements(msg) {
1187
1187
  return msg.elements.map(toCanonicalElement);
1188
1188
  }
1189
- /** canonical 元素 → NT 发送元素(text/at/face/image/voice/reply 核心五类)。 */
1189
+ /**
1190
+ * canonical json → ARK bytesData 字符串:字符串原样透传(OB11 json 段 /
1191
+ * satori 发送链给的已是卡片 JSON 文本),其余值 JSON 序列化。
1192
+ * 与接收方向 arkToCanonical 对称(bytesData 字段名 2026-10-25 真实消息实测)。
1193
+ */
1194
+ function jsonToArkBytes(raw) {
1195
+ if (typeof raw === "string") return raw;
1196
+ try {
1197
+ return JSON.stringify(raw) ?? "";
1198
+ } catch (err) {
1199
+ throw kernelError(`json 元素序列化失败: ${String(err)}`, "INVALID_PARAM");
1200
+ }
1201
+ }
1202
+ /** canonical 元素 → NT 发送元素(text/at/face/image/voice/reply 核心 + json ARK 透传)。 */
1190
1203
  function toSendElements(elements) {
1191
1204
  const out = [];
1192
1205
  for (const el of elements) switch (el.type) {
@@ -1242,9 +1255,14 @@ function toSendElements(elements) {
1242
1255
  });
1243
1256
  break;
1244
1257
  case "video": throw kernelError("视频发送需经 MsgApi 预处理(prepareVideoElement)", "INVALID_PARAM");
1258
+ case "json":
1259
+ out.push({
1260
+ elementType: ElementType.ARK,
1261
+ arkElement: { bytesData: jsonToArkBytes(el.raw) }
1262
+ });
1263
+ break;
1245
1264
  case "file":
1246
1265
  case "forward":
1247
- case "json":
1248
1266
  case "xml":
1249
1267
  case "unknown": throw kernelError(`发送元素 ${el.type} 暂不支持(P2-2 探测后补)`, "INVALID_PARAM");
1250
1268
  }
@@ -4538,6 +4556,33 @@ function tryCall(obj, name) {
4538
4556
  };
4539
4557
  }
4540
4558
  }
4559
+ /** 服务 getter 判定(get*Service 命名约定)。 */
4560
+ function isServiceGetter(name) {
4561
+ return name.startsWith("get") && name.endsWith("Service");
4562
+ }
4563
+ /** 非空服务实例计数(逐个调用 get*Service 数非空返回;候选可用度评分)。
4564
+ * 自建宿主下 QQ 主 session 未被 init,get*Service 恒 null(2026-10-31 实测
4565
+ * napuketto-probe-late.json),kernel 自建 session 服务可用——主目标按此评分选取。 */
4566
+ function countNonNullServices(session) {
4567
+ if (session === null || session === void 0) return 0;
4568
+ let count = 0;
4569
+ for (const name of listMethods(session)) {
4570
+ if (!isServiceGetter(name)) continue;
4571
+ const call = tryCall(session, name);
4572
+ if (call.ok && call.value !== null && call.value !== void 0) count += 1;
4573
+ }
4574
+ return count;
4575
+ }
4576
+ /** 主探测目标选取:服务实例最多者优先,评分并列保持候选顺序(主 session 在前)。 */
4577
+ function pickPrimaryCandidate(candidates) {
4578
+ let best = candidates[0];
4579
+ for (const candidate of candidates.slice(1)) if (best !== void 0 && candidate.serviceNonNull > best.serviceNonNull) best = candidate;
4580
+ return best ?? {
4581
+ source: "own-create",
4582
+ session: null,
4583
+ serviceNonNull: 0
4584
+ };
4585
+ }
4541
4586
  //#endregion
4542
4587
  //#region src/wrapper/probe/probe-services.ts
4543
4588
  /** 探测关键 export 构造器方法(get/create/getNTWrapperSession 等)。 */
@@ -4688,24 +4733,37 @@ function probeStartupChain(created) {
4688
4733
  /** 从 getSessionIdList 结果提取 sessionIds(Map 值或键优先)。 */
4689
4734
  function extractSessionIds(ids) {
4690
4735
  if (!(ids.ok && ids.value instanceof Map)) return [];
4691
- const out = [];
4692
- for (const [k, v] of ids.value) out.push(typeof v === "string" ? v : String(k));
4693
- return out;
4736
+ return [...ids.value].map(([k, v]) => sessionIdOfEntry(k, v));
4694
4737
  }
4695
- /** 用真实 sessionId 取主 session(getNTWrapperSession(nt_x))并记录方法面。 */
4696
- function probeSessionById(ctx, id) {
4738
+ /** 单条 sessionIds 项(Map 值是字符串直接用,否则退化键名)。 */
4739
+ function sessionIdOfEntry(k, v) {
4740
+ return typeof v === "string" ? v : String(k);
4741
+ }
4742
+ /** session 实例方法面(空值记 null)。 */
4743
+ function sessionFace(session) {
4744
+ return session !== null && session !== void 0 ? {
4745
+ methods: listMethods(session),
4746
+ ownKeys: Object.getOwnPropertyNames(session)
4747
+ } : null;
4748
+ }
4749
+ /** 未知异常 → 错误消息字符串。 */
4750
+ function errMsgString(e) {
4751
+ return e instanceof Error ? e.message : String(e);
4752
+ }
4753
+ /** try getNTWrapperSession(id) → session 方法面(fn 缺失记 null,失败记 error)。 */
4754
+ function trySessionFace(ctx, id) {
4697
4755
  const fn = getNTWrapperSessionFn(ctx);
4698
4756
  if (fn === null) return null;
4699
4757
  try {
4700
- const value = fn.call(ctx.exports.NodeIQQNTWrapperSession, id);
4701
- return value ? {
4702
- methods: listMethods(value),
4703
- ownKeys: Object.getOwnPropertyNames(value)
4704
- } : null;
4758
+ return sessionFace(fn.call(ctx.exports.NodeIQQNTWrapperSession, id));
4705
4759
  } catch (e) {
4706
- return { error: e instanceof Error ? e.message : String(e) };
4760
+ return { error: errMsgString(e) };
4707
4761
  }
4708
4762
  }
4763
+ /** 用真实 sessionId 取主 session(getNTWrapperSession(nt_x))并记录方法面。 */
4764
+ function probeSessionById(ctx, id) {
4765
+ return trySessionFace(ctx, id);
4766
+ }
4709
4767
  /** 兜底:候选名字 getNTWrapperSession(无 Map 数据时)。 */
4710
4768
  function probeByNameFallback(ctx) {
4711
4769
  const candidates = [
@@ -4721,31 +4779,39 @@ function probeByNameFallback(ctx) {
4721
4779
  }
4722
4780
  /** 探测单个候选名(getNTWrapperSession(name),记录方法面或错误)。 */
4723
4781
  function probeSessionByName(ctx, name) {
4724
- try {
4725
- const fn = getNTWrapperSessionFn(ctx);
4726
- if (fn === null) return null;
4727
- const value = fn.call(ctx.exports.NodeIQQNTWrapperSession, name);
4728
- return value ? {
4729
- methods: listMethods(value),
4730
- ownKeys: Object.getOwnPropertyNames(value)
4731
- } : null;
4732
- } catch (e) {
4733
- return { error: e instanceof Error ? e.message : String(e) };
4734
- }
4782
+ return trySessionFace(ctx, name);
4735
4783
  }
4736
- /** 探测 startup session 链路:create() → start() → getSessionIdList → getNTWrapperSession。 */
4737
- function probeStartup(ctx) {
4784
+ /** StartupSessionWrapper 可用性判定(缺 create 方法视为不可用)。 */
4785
+ function startupWrapper(ctx) {
4738
4786
  const startup = ctx.exports.NodeIQQNTStartupSessionWrapper;
4739
- if (startup === null || typeof startup.create !== "function") return null;
4740
- const out = { staticMethods: listMethods(startup) };
4741
- const created = tryCall(startup, "create");
4742
- out["create"] = created.ok ? {
4787
+ return startup === null || typeof startup.create !== "function" ? null : startup;
4788
+ }
4789
+ /** create() 调用结果面(成功记实例方法面 + 序列化值,失败记 error)。 */
4790
+ function createFace(created) {
4791
+ return created.ok ? {
4743
4792
  methods: created.value ? listMethods(created.value) : [],
4744
4793
  value: serialize(created.value)
4745
4794
  } : { error: created.error };
4795
+ }
4796
+ /** create 成功时并入实例链探测结果。 */
4797
+ function assignCreatedChain(out, ctx, created) {
4746
4798
  if (created.ok && created.value) Object.assign(out, probeCreatedChain(ctx, created.value));
4799
+ }
4800
+ /** 无 Map 数据时并入候选名兜底结果。 */
4801
+ function assignNameFallback(out, ctx) {
4802
+ if (out["sessionIds"] !== void 0) return;
4803
+ out["getNTWrapperSession"] = probeByNameFallback(ctx);
4804
+ }
4805
+ /** 探测 startup session 链路:create() → start() → getSessionIdList → getNTWrapperSession。 */
4806
+ function probeStartup(ctx) {
4807
+ const startup = startupWrapper(ctx);
4808
+ if (startup === null) return null;
4809
+ const out = { staticMethods: listMethods(startup) };
4810
+ const created = tryCall(startup, "create");
4811
+ out["create"] = createFace(created);
4812
+ assignCreatedChain(out, ctx, created);
4747
4813
  out["enumSessionIds"] = enumerateSessionIds(ctx);
4748
- if (out["sessionIds"] === void 0) out["getNTWrapperSession"] = probeByNameFallback(ctx);
4814
+ assignNameFallback(out, ctx);
4749
4815
  return out;
4750
4816
  }
4751
4817
  /** 探测 create 后的实例链:start / getSessionIdList / 主 session。 */
@@ -4767,31 +4833,60 @@ function probeCreatedChain(ctx, created) {
4767
4833
  function findMainProbeId(sessionIds) {
4768
4834
  return sessionIds.find((id) => id.startsWith("nt_")) ?? sessionIds[0];
4769
4835
  }
4770
- /** 探测 session 与 service 方法(优先复用 QQ 主 session)。 */
4836
+ /** 探测 session 与 service 方法(候选中选服务实例最多者为主目标)。
4837
+ * 自建宿主下 QQ 主 session 未被 init(get*Service 恒 null,2026-10-31 实测
4838
+ * napuketto-probe-late.json 全服务空),kernel 自建 session 已带 adapter init
4839
+ * 服务可用——主目标按 countNonNullServices 评分选取,并列保持主 session 优先。 */
4771
4840
  function probeSession(ctx) {
4772
4841
  const out = {};
4773
- const session = pickProbeSession(ctx, out);
4774
- out["session"] = session ? {
4775
- methods: listMethods(session),
4776
- ownKeys: Object.getOwnPropertyNames(session)
4777
- } : null;
4778
- if (session !== null && session !== void 0) out["services"] = collectServiceGetters(session);
4842
+ const candidates = collectProbeCandidates(ctx).map(scoreCandidate);
4843
+ const primary = pickPrimaryCandidate(candidates);
4844
+ out["sessionSource"] = primary.source;
4845
+ out["session"] = sessionFace(primary.session);
4846
+ if (primary.session !== null && primary.session !== void 0) out["services"] = collectServiceGetters(primary.session);
4847
+ if (candidates.length > 1) out["sessionAlternates"] = alternateFaces(candidates, primary);
4779
4848
  return out;
4780
4849
  }
4781
- /** 选探测目标 session:优先 QQ 主 session(getMainSession),回退 ctx.session。 */
4782
- function pickProbeSession(ctx, out) {
4850
+ /** 候选评分(非空服务实例数)。 */
4851
+ function scoreCandidate(candidate) {
4852
+ return {
4853
+ ...candidate,
4854
+ serviceNonNull: countNonNullServices(candidate.session)
4855
+ };
4856
+ }
4857
+ /** 落选候选的概要面(方法数 + 评分,诊断用)。 */
4858
+ function alternateFaces(candidates, primary) {
4859
+ return candidates.filter((candidate) => candidate !== primary).map((candidate) => ({
4860
+ source: candidate.source,
4861
+ methodCount: listMethods(candidate.session).length,
4862
+ serviceNonNull: candidate.serviceNonNull
4863
+ }));
4864
+ }
4865
+ /** 收集候选 session:QQ 主 session(若可得)在前 + kernel 自建 session 去重补充。 */
4866
+ function collectProbeCandidates(ctx) {
4867
+ const candidates = [];
4868
+ pushMainCandidate(ctx, candidates);
4869
+ pushOwnCandidate(ctx, candidates);
4870
+ return candidates;
4871
+ }
4872
+ /** 并入 QQ 主 session 候选(枚举失败静默跳过)。 */
4873
+ function pushMainCandidate(ctx, out) {
4783
4874
  try {
4784
4875
  const main = getMainSession(ctx);
4785
- if (main !== null) {
4786
- out["sessionSource"] = "mainSession(getNTWrapperSession)";
4787
- return main;
4788
- }
4876
+ if (main !== null) out.push({
4877
+ source: "mainSession(getNTWrapperSession)",
4878
+ session: main
4879
+ });
4789
4880
  } catch {}
4790
- if (ctx.session !== null) {
4791
- out["sessionSource"] = "own-create";
4792
- return ctx.session;
4793
- }
4794
- return null;
4881
+ }
4882
+ /** 并入 kernel 自建 session 候选(与已有候选同一实例时去重)。 */
4883
+ function pushOwnCandidate(ctx, out) {
4884
+ if (ctx.session === null) return;
4885
+ if (out.some((candidate) => candidate.session === ctx.session)) return;
4886
+ out.push({
4887
+ source: "own-create",
4888
+ session: ctx.session
4889
+ });
4795
4890
  }
4796
4891
  /** 收集 session 的 get*Service getter 方法面与返回形状。 */
4797
4892
  function collectServiceGetters(session) {