@napuketto/adapter 0.0.2 → 0.0.3

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.
@@ -6,6 +6,31 @@ import { join } from "node:path";
6
6
  import { HttpServer, WsServer } from "@napuketto/network";
7
7
  import { mkdir, open, writeFile } from "node:fs/promises";
8
8
  import { encodePcmToSilk } from "@napuketto/media";
9
+ //#region src/satori/helper/config.ts
10
+ /**
11
+ * Satori 协议配置(zod schema,归属本包,ADR-012)
12
+ * 协议配置 schema 在各自协议包,kernel 只提供 ConfigBase 基类。
13
+ */
14
+ /** HTTP RPC 服务器默认端口(satori.chat 惯例 5500 区间,取 5500)。 */
15
+ const DEFAULT_HTTP_PORT = 5500;
16
+ /** WS 事件服务默认端口(satori.chat 惯例 5500 区间,取 5501)。 */
17
+ const DEFAULT_WS_PORT = 5501;
18
+ /** HTTP RPC 服务器实例(POST /v1/{resource}.{method})。 */
19
+ const httpServerSchema = createHttpServerSchema(DEFAULT_HTTP_PORT);
20
+ /** WS 事件服务实例(/v1/events 信令,第三方 SDK 连入)。 */
21
+ const wsServerSchema = createWsServerSchema(DEFAULT_WS_PORT);
22
+ /** Satori 配置 schema。 */
23
+ const satoriConfigSchema = z.object({
24
+ /** 鉴权 token(全局默认,实例未指定时继承)。 */
25
+ token: z.string().optional(),
26
+ /** 媒体资源(img/audio/video/file)下载缓存目录。 */
27
+ cacheDir: z.string().optional(),
28
+ /** HTTP RPC 服务器列表。 */
29
+ httpServers: z.array(httpServerSchema).default([]),
30
+ /** WS 事件服务列表。 */
31
+ wsServers: z.array(wsServerSchema).default([])
32
+ });
33
+ //#endregion
9
34
  //#region src/satori/helper/error.ts
10
35
  /** KernelErrorCode → HTTP 状态码映射表。 */
11
36
  const satoriHttpStatusMap = {
@@ -28,47 +53,65 @@ const HTTP_STATUS = {
28
53
  notImplemented: 501,
29
54
  serverError: 500
30
55
  };
56
+ /** 适配器名(固定 napuketto)。 */
57
+ const ADAPTER = "napuketto";
58
+ /** 平台特性(API 可用性判断;群聊频道与群组重合 → guild.plain)。 */
59
+ const FEATURES = ["guild.plain", "message.list.from"];
60
+ /** 构造用户(uin)。 */
61
+ function toUser(id, name, nick) {
62
+ const user = { id };
63
+ if (name !== void 0 && name !== "") user.name = name;
64
+ if (nick !== void 0 && nick !== "") user.nick = nick;
65
+ return user;
66
+ }
67
+ /** 构造群聊频道(id = 群号)。 */
68
+ function toGroupChannel(groupCode, name) {
69
+ const channel = {
70
+ id: String(groupCode),
71
+ type: 0
72
+ };
73
+ if (name !== void 0 && name !== "") channel.name = name;
74
+ return channel;
75
+ }
76
+ /** 构造私聊频道(id = 对端 uin)。 */
77
+ function toDirectChannel(uin, name) {
78
+ const channel = {
79
+ id: String(uin),
80
+ type: 1
81
+ };
82
+ if (name !== void 0 && name !== "") channel.name = name;
83
+ return channel;
84
+ }
85
+ /** 构造群组(id = 群号)。 */
86
+ function toGuild(groupCode, name, avatar) {
87
+ const guild = { id: String(groupCode) };
88
+ if (name !== void 0 && name !== "") guild.name = name;
89
+ if (avatar !== void 0 && avatar !== "") guild.avatar = avatar;
90
+ return guild;
91
+ }
92
+ /** 构造登录信息(login.get / READY logins)。 */
93
+ function toLogin(self, sn, online) {
94
+ return {
95
+ sn,
96
+ platform: "qq",
97
+ user: toUser(self.uin, self.nickname),
98
+ status: online ? 1 : 0,
99
+ adapter: ADAPTER,
100
+ features: [...FEATURES]
101
+ };
102
+ }
103
+ /** 构造非登录事件的最小 login(只带 sn/user/platform,规范)。 */
104
+ function toMinimalLogin(sn, selfUin) {
105
+ return {
106
+ sn,
107
+ platform: "qq",
108
+ user: toUser(selfUin),
109
+ status: 1,
110
+ adapter: ADAPTER
111
+ };
112
+ }
31
113
  //#endregion
32
- //#region src/satori/action/base-action.ts
33
- /**
34
- * Satori 动作基类(响应形态与 OB11 不同:直接返回资源对象,错误用 HTTP 状态码)
35
- *
36
- * - `run(payload)`:zod 校验失败 → SatoriActionError(400);_handle 抛的
37
- * KernelError → satoriHttpStatusMap 映射 HTTP 状态码(ADR-017)
38
- * - `SatoriActionError`:带 HTTP 状态码的动作错误(transport 层直接透传状态码)
39
- */
40
- /** 动作错误(带 HTTP 状态码;由 transport 层透传)。 */
41
- var SatoriActionError = class extends Error {
42
- status;
43
- constructor(status, message) {
44
- super(message);
45
- this.status = status;
46
- }
47
- };
48
- /** 校验失败默认状态码。 */
49
- const INVALID_PARAM_STATUS = 400;
50
- /** 兜底错误状态码。 */
51
- const UNKNOWN_STATUS = 500;
52
- /**
53
- * Satori 动作基类:校验 + 执行 + 错误映射。
54
- * 成功返回资源对象(或数组/分页列表),失败抛 SatoriActionError。
55
- */
56
- var BaseSatoriAction = class {
57
- /** 校验并执行(HTTP/WS 共用)。 */
58
- async run(payload) {
59
- const parsed = this.schema.safeParse(payload);
60
- if (!parsed.success) throw new SatoriActionError(INVALID_PARAM_STATUS, "参数校验失败");
61
- try {
62
- return await this._handle(parsed.data);
63
- } catch (err) {
64
- if (err instanceof SatoriActionError) throw err;
65
- if (isKernelError(err)) throw new SatoriActionError(satoriHttpStatusMap[err.code] ?? UNKNOWN_STATUS, err.message);
66
- throw new SatoriActionError(UNKNOWN_STATUS, err instanceof Error ? err.message : "未知错误");
67
- }
68
- }
69
- };
70
- //#endregion
71
- //#region src/satori/helper/canonical.ts
114
+ //#region src/satori/helper/element/canonical.ts
72
115
  /** 收集 at uid → 批量 uidToUin(失败不阻塞,at 原样 uid)。 */
73
116
  async function resolveAtUids(elements, uidToUin) {
74
117
  const atUids = [];
@@ -163,7 +206,7 @@ async function canonicalToSatoriElements(elements, deps = {}) {
163
206
  return out;
164
207
  }
165
208
  //#endregion
166
- //#region src/satori/helper/parse-tag.ts
209
+ //#region src/satori/helper/element/parse-tag.ts
167
210
  /**
168
211
  * Satori 标签读取(元素名 / 属性解析,从 element.ts 拆分,2026-08-08 FTA 优化)
169
212
  *
@@ -294,7 +337,7 @@ function readTag(source, pos) {
294
337
  };
295
338
  }
296
339
  //#endregion
297
- //#region src/satori/helper/element.ts
340
+ //#region src/satori/helper/element/element.ts
298
341
  /**
299
342
  * Satori 消息元素解析/渲染(协议 v1,自研实现,规范参考 satori.chat/zh-CN/protocol/message.html)
300
343
  *
@@ -471,62 +514,222 @@ function renderElement(el) {
471
514
  const inner = renderElements(children);
472
515
  return `<${el.type}${attrs}>${inner}</${el.type}>`;
473
516
  }
474
- /** 适配器名(固定 napuketto)。 */
475
- const ADAPTER = "napuketto";
476
- /** 平台特性(API 可用性判断;群聊频道与群组重合 → guild.plain)。 */
477
- const FEATURES = ["guild.plain", "message.list.from"];
478
- /** 构造用户(uin)。 */
479
- function toUser(id, name, nick) {
480
- const user = { id };
481
- if (name !== void 0 && name !== "") user.name = name;
482
- if (nick !== void 0 && nick !== "") user.nick = nick;
483
- return user;
517
+ //#endregion
518
+ //#region src/satori/helper/element/asset.ts
519
+ /**
520
+ * Satori 发方向资源处理(从 element-convert.ts 拆分,2026-08-08 FTA 优化)
521
+ *
522
+ * - resolveAsset:internal: 路径 / http(s) 下载 / 本地路径原样
523
+ * - ensureSilk:非 silk 语音(wav/pcm)转 silk(QQ 语音格式,失败原样返回)
524
+ */
525
+ /** internal:{platform}/{user.id}/{path} → 本地路径(取 _tmp 之后的路径,回落 cacheDir)。 */
526
+ function resolveInternalPath(src) {
527
+ const pathPart = src.slice(9);
528
+ const slash1 = pathPart.indexOf("/");
529
+ const slash2 = slash1 === -1 ? -1 : pathPart.indexOf("/", slash1 + 1);
530
+ if (slash2 === -1) return pathPart;
531
+ const rest = pathPart.slice(slash2 + 1);
532
+ const tmpIdx = rest.indexOf("_tmp/");
533
+ return tmpIdx === -1 ? rest : rest.slice(tmpIdx + 5);
484
534
  }
485
- /** 构造群聊频道(id = 群号)。 */
486
- function toGroupChannel(groupCode, name) {
487
- const channel = {
488
- id: String(groupCode),
489
- type: 0
490
- };
491
- if (name !== void 0 && name !== "") channel.name = name;
492
- return channel;
535
+ /**
536
+ * 解析资源 src:
537
+ * - internal:{platform}/{user.id}/{path} → 本地路径(第一版仅支持 _tmp 保留路径)
538
+ * - http(s):// → 下载到 cacheDir
539
+ * - 其他 → 视为本地路径原样返回
540
+ */
541
+ async function resolveAsset(src, cacheDir) {
542
+ if (src.startsWith("internal:")) return resolveInternalPath(src);
543
+ if (/^https?:\/\//i.test(src)) return downloadAsset(src, cacheDir);
544
+ return src;
493
545
  }
494
- /** 构造私聊频道(id = 对端 uin)。 */
495
- function toDirectChannel(uin, name) {
496
- const channel = {
497
- id: String(uin),
498
- type: 1
546
+ /** 下载 URL 资源到缓存目录(返回本地路径)。 */
547
+ async function downloadAsset(url, cacheDir) {
548
+ await mkdir(cacheDir, { recursive: true });
549
+ const res = await fetch(url);
550
+ if (!res.ok) throw kernelError(`下载资源失败: ${url}`, "SEND_FAILED");
551
+ const buf = Buffer.from(await res.arrayBuffer());
552
+ const safeName = (url.split("/").pop() ?? `asset-${Date.now()}`).replace(/[^a-zA-Z0-9._-]/g, "_") || `asset-${Date.now()}`;
553
+ const filePath = join(cacheDir, safeName);
554
+ await writeFile(filePath, buf);
555
+ return filePath;
556
+ }
557
+ /** 语音转码:非 silk 输入(wav/pcm)转 silk(QQ 语音格式)。 */
558
+ async function ensureSilk(path) {
559
+ try {
560
+ if ((await readFileHead(path)).startsWith("#!SILK")) return path;
561
+ return await encodePcmToSilk(path);
562
+ } catch {
563
+ return path;
564
+ }
565
+ }
566
+ /** 读文件头 8 字节(判断格式)。 */
567
+ async function readFileHead(path) {
568
+ const handle = await open(path, "r");
569
+ try {
570
+ const buf = Buffer.alloc(8);
571
+ await handle.read(buf, 0, 8, 0);
572
+ return buf.toString("utf8");
573
+ } finally {
574
+ await handle.close();
575
+ }
576
+ }
577
+ //#endregion
578
+ //#region src/satori/helper/element/media-convert.ts
579
+ /** 解析元素媒体 src(空 → null 忽略),返回本地路径。 */
580
+ async function resolveMediaSrc(el, deps) {
581
+ const src = el.attrs?.["src"] ?? "";
582
+ if (src === "") return null;
583
+ return resolveAsset(src, deps.cacheDir);
584
+ }
585
+ /** Satori img → canonical image。 */
586
+ async function imgToCanonical(el, deps) {
587
+ const path = await resolveMediaSrc(el, deps);
588
+ return path === null ? [] : [{
589
+ type: "image",
590
+ path
591
+ }];
592
+ }
593
+ /** Satori audio → canonical voice(非 silk 转码)。 */
594
+ async function audioToCanonical(el, deps) {
595
+ const path = await resolveMediaSrc(el, deps);
596
+ return path === null ? [] : [{
597
+ type: "voice",
598
+ path: await ensureSilk(path)
599
+ }];
600
+ }
601
+ /** Satori video → canonical video。 */
602
+ async function videoToCanonical(el, deps) {
603
+ const path = await resolveMediaSrc(el, deps);
604
+ return path === null ? [] : [{
605
+ type: "video",
606
+ path
607
+ }];
608
+ }
609
+ /** Satori file → canonical file(title → name)。 */
610
+ async function fileToCanonical(el, deps) {
611
+ const path = await resolveMediaSrc(el, deps);
612
+ if (path === null) return [];
613
+ const title = el.attrs?.["title"];
614
+ const base = {
615
+ type: "file",
616
+ path
499
617
  };
500
- if (name !== void 0 && name !== "") channel.name = name;
501
- return channel;
618
+ if (title !== void 0 && title !== "") return [{
619
+ ...base,
620
+ name: title
621
+ }];
622
+ return [base];
502
623
  }
503
- /** 构造群组(id = 群号)。 */
504
- function toGuild(groupCode, name, avatar) {
505
- const guild = { id: String(groupCode) };
506
- if (name !== void 0 && name !== "") guild.name = name;
507
- if (avatar !== void 0 && avatar !== "") guild.avatar = avatar;
508
- return guild;
624
+ /** 媒体元素转换器子表(并入 element-convert 主表)。 */
625
+ const MEDIA_CONVERTERS = {
626
+ img: imgToCanonical,
627
+ audio: audioToCanonical,
628
+ video: videoToCanonical,
629
+ file: fileToCanonical
630
+ };
631
+ //#endregion
632
+ //#region src/satori/helper/element/element-convert.ts
633
+ /** 发方向:Satori 元素树 → kernel canonical(at uin→uid + 资源下载/转码)。 */
634
+ async function satoriToCanonicalElements(elements, deps) {
635
+ const atUins = [];
636
+ const collectAt = (list) => {
637
+ for (const el of list) {
638
+ const id = el.attrs?.["id"];
639
+ if (el.type === "at" && id !== void 0 && id !== "all") atUins.push(id);
640
+ if (el.children !== void 0) collectAt(el.children);
641
+ }
642
+ };
643
+ collectAt(elements);
644
+ let uinToUidMap = /* @__PURE__ */ new Map();
645
+ if (atUins.length > 0) try {
646
+ uinToUidMap = await deps.uinToUid([...new Set(atUins)]);
647
+ } catch {}
648
+ const out = [];
649
+ for (const el of elements) out.push(...await elementToCanonical(el, deps, uinToUidMap));
650
+ return out;
509
651
  }
510
- /** 构造登录信息(login.get / READY logins)。 */
511
- function toLogin(self, sn, online) {
512
- return {
513
- sn,
514
- platform: "qq",
515
- user: toUser(self.uin, self.nickname),
516
- status: online ? 1 : 0,
517
- adapter: ADAPTER,
518
- features: [...FEATURES]
652
+ /** Satori at → canonical at(uin → uid,name → display)。 */
653
+ function atToCanonical(el, uinToUidMap) {
654
+ const id = el.attrs?.["id"] ?? "";
655
+ if (id === "all") return [{
656
+ type: "at",
657
+ target: "all"
658
+ }];
659
+ if (id === "") return [];
660
+ const target = uinToUidMap.get(id) ?? id;
661
+ const display = el.attrs?.["name"];
662
+ const base = {
663
+ type: "at",
664
+ target
519
665
  };
666
+ if (display !== void 0 && display !== "") return [{
667
+ ...base,
668
+ display
669
+ }];
670
+ return [base];
671
+ }
672
+ /** Satori message:forward 属性 → canonical forward;否则展开子元素。 */
673
+ async function messageToCanonical(el, deps, uinToUidMap) {
674
+ if (el.attrs?.["forward"] !== void 0) {
675
+ const id = el.attrs?.["id"] ?? "";
676
+ return id === "" ? [] : [{
677
+ type: "forward",
678
+ messageIds: [id]
679
+ }];
680
+ }
681
+ return flattenChildren(el.children, deps, uinToUidMap);
682
+ }
683
+ /** 元素 type → 转换器(未知/修饰/排版类型走 fallback,不在此表)。 */
684
+ const ELEMENT_CONVERTERS = {
685
+ text: async (el) => {
686
+ const text = el.text ?? "";
687
+ return text === "" ? [] : [{
688
+ type: "text",
689
+ text
690
+ }];
691
+ },
692
+ at: async (el, _deps, uinToUidMap) => atToCanonical(el, uinToUidMap),
693
+ emoji: async (el) => {
694
+ const id = el.attrs?.["id"] ?? "";
695
+ return id === "" ? [] : [{
696
+ type: "face",
697
+ id
698
+ }];
699
+ },
700
+ quote: async (el) => {
701
+ const id = el.attrs?.["id"] ?? "";
702
+ return id === "" ? [] : [{
703
+ type: "reply",
704
+ messageId: id
705
+ }];
706
+ },
707
+ message: async (el, deps, uinToUidMap) => messageToCanonical(el, deps, uinToUidMap),
708
+ br: async () => [{
709
+ type: "text",
710
+ text: "\n"
711
+ }],
712
+ author: async () => [],
713
+ sharp: async () => [],
714
+ a: async () => [],
715
+ button: async () => [],
716
+ ...MEDIA_CONVERTERS
717
+ };
718
+ /** 单个元素 → canonical(修饰/排版元素展开子元素)。 */
719
+ async function elementToCanonical(el, deps, uinToUidMap) {
720
+ const converter = ELEMENT_CONVERTERS[el.type];
721
+ if (converter !== void 0) return converter(el, deps, uinToUidMap);
722
+ return flattenChildren(el.children, deps, uinToUidMap);
520
723
  }
521
- /** 构造非登录事件的最小 login(只带 sn/user/platform,规范)。 */
522
- function toMinimalLogin(sn, selfUin) {
523
- return {
524
- sn,
525
- platform: "qq",
526
- user: toUser(selfUin),
527
- status: 1,
528
- adapter: ADAPTER
529
- };
724
+ /** 展开子元素为 canonical(递归)。 */
725
+ async function flattenChildren(children, deps, uinToUidMap) {
726
+ const out = [];
727
+ for (const child of children ?? []) out.push(...await elementToCanonical(child, deps, uinToUidMap));
728
+ return out;
729
+ }
730
+ /** 解析消息 content 字符串 → canonical(message.create 用)。 */
731
+ async function parseContentToCanonical(content, deps) {
732
+ return satoriToCanonicalElements(parseElements(content), deps);
530
733
  }
531
734
  //#endregion
532
735
  //#region src/satori/helper/translate.ts
@@ -569,6 +772,45 @@ function toChannelById(channelId, isGroup, name) {
569
772
  return toDirectChannel(channelId, name);
570
773
  }
571
774
  //#endregion
775
+ //#region src/satori/action/base-action.ts
776
+ /**
777
+ * Satori 动作基类(响应形态与 OB11 不同:直接返回资源对象,错误用 HTTP 状态码)
778
+ *
779
+ * - `run(payload)`:zod 校验失败 → SatoriActionError(400);_handle 抛的
780
+ * KernelError → satoriHttpStatusMap 映射 HTTP 状态码(ADR-017)
781
+ * - `SatoriActionError`:带 HTTP 状态码的动作错误(transport 层直接透传状态码)
782
+ */
783
+ /** 动作错误(带 HTTP 状态码;由 transport 层透传)。 */
784
+ var SatoriActionError = class extends Error {
785
+ status;
786
+ constructor(status, message) {
787
+ super(message);
788
+ this.status = status;
789
+ }
790
+ };
791
+ /** 校验失败默认状态码。 */
792
+ const INVALID_PARAM_STATUS = 400;
793
+ /** 兜底错误状态码。 */
794
+ const UNKNOWN_STATUS = 500;
795
+ /**
796
+ * Satori 动作基类:校验 + 执行 + 错误映射。
797
+ * 成功返回资源对象(或数组/分页列表),失败抛 SatoriActionError。
798
+ */
799
+ var BaseSatoriAction = class {
800
+ /** 校验并执行(HTTP/WS 共用)。 */
801
+ async run(payload) {
802
+ const parsed = this.schema.safeParse(payload);
803
+ if (!parsed.success) throw new SatoriActionError(INVALID_PARAM_STATUS, "参数校验失败");
804
+ try {
805
+ return await this._handle(parsed.data);
806
+ } catch (err) {
807
+ if (err instanceof SatoriActionError) throw err;
808
+ if (isKernelError(err)) throw new SatoriActionError(satoriHttpStatusMap[err.code] ?? UNKNOWN_STATUS, err.message);
809
+ throw new SatoriActionError(UNKNOWN_STATUS, err instanceof Error ? err.message : "未知错误");
810
+ }
811
+ }
812
+ };
813
+ //#endregion
572
814
  //#region src/satori/action/channel.ts
573
815
  /**
574
816
  * Satori 频道动作:channel.get / channel.list
@@ -862,214 +1104,6 @@ var LoginGetAction = class extends BaseSatoriAction {
862
1104
  }
863
1105
  };
864
1106
  //#endregion
865
- //#region src/satori/helper/asset.ts
866
- /**
867
- * Satori 发方向资源处理(从 element-convert.ts 拆分,2026-08-08 FTA 优化)
868
- *
869
- * - resolveAsset:internal: 路径 / http(s) 下载 / 本地路径原样
870
- * - ensureSilk:非 silk 语音(wav/pcm)转 silk(QQ 语音格式,失败原样返回)
871
- */
872
- /** internal:{platform}/{user.id}/{path} → 本地路径(取 _tmp 之后的路径,回落 cacheDir)。 */
873
- function resolveInternalPath(src) {
874
- const pathPart = src.slice(9);
875
- const slash1 = pathPart.indexOf("/");
876
- const slash2 = slash1 === -1 ? -1 : pathPart.indexOf("/", slash1 + 1);
877
- if (slash2 === -1) return pathPart;
878
- const rest = pathPart.slice(slash2 + 1);
879
- const tmpIdx = rest.indexOf("_tmp/");
880
- return tmpIdx === -1 ? rest : rest.slice(tmpIdx + 5);
881
- }
882
- /**
883
- * 解析资源 src:
884
- * - internal:{platform}/{user.id}/{path} → 本地路径(第一版仅支持 _tmp 保留路径)
885
- * - http(s):// → 下载到 cacheDir
886
- * - 其他 → 视为本地路径原样返回
887
- */
888
- async function resolveAsset(src, cacheDir) {
889
- if (src.startsWith("internal:")) return resolveInternalPath(src);
890
- if (/^https?:\/\//i.test(src)) return downloadAsset(src, cacheDir);
891
- return src;
892
- }
893
- /** 下载 URL 资源到缓存目录(返回本地路径)。 */
894
- async function downloadAsset(url, cacheDir) {
895
- await mkdir(cacheDir, { recursive: true });
896
- const res = await fetch(url);
897
- if (!res.ok) throw kernelError(`下载资源失败: ${url}`, "SEND_FAILED");
898
- const buf = Buffer.from(await res.arrayBuffer());
899
- const safeName = (url.split("/").pop() ?? `asset-${Date.now()}`).replace(/[^a-zA-Z0-9._-]/g, "_") || `asset-${Date.now()}`;
900
- const filePath = join(cacheDir, safeName);
901
- await writeFile(filePath, buf);
902
- return filePath;
903
- }
904
- /** 语音转码:非 silk 输入(wav/pcm)转 silk(QQ 语音格式)。 */
905
- async function ensureSilk(path) {
906
- try {
907
- if ((await readFileHead(path)).startsWith("#!SILK")) return path;
908
- return await encodePcmToSilk(path);
909
- } catch {
910
- return path;
911
- }
912
- }
913
- /** 读文件头 8 字节(判断格式)。 */
914
- async function readFileHead(path) {
915
- const handle = await open(path, "r");
916
- try {
917
- const buf = Buffer.alloc(8);
918
- await handle.read(buf, 0, 8, 0);
919
- return buf.toString("utf8");
920
- } finally {
921
- await handle.close();
922
- }
923
- }
924
- //#endregion
925
- //#region src/satori/helper/element-convert.ts
926
- /** 发方向:Satori 元素树 → kernel canonical(at uin→uid + 资源下载/转码)。 */
927
- async function satoriToCanonicalElements(elements, deps) {
928
- const atUins = [];
929
- const collectAt = (list) => {
930
- for (const el of list) {
931
- const id = el.attrs?.["id"];
932
- if (el.type === "at" && id !== void 0 && id !== "all") atUins.push(id);
933
- if (el.children !== void 0) collectAt(el.children);
934
- }
935
- };
936
- collectAt(elements);
937
- let uinToUidMap = /* @__PURE__ */ new Map();
938
- if (atUins.length > 0) try {
939
- uinToUidMap = await deps.uinToUid([...new Set(atUins)]);
940
- } catch {}
941
- const out = [];
942
- for (const el of elements) out.push(...await elementToCanonical(el, deps, uinToUidMap));
943
- return out;
944
- }
945
- /** 解析元素媒体 src(空 → null 忽略),返回本地路径。 */
946
- async function resolveMediaSrc(el, deps) {
947
- const src = el.attrs?.["src"] ?? "";
948
- if (src === "") return null;
949
- return resolveAsset(src, deps.cacheDir);
950
- }
951
- /** Satori at → canonical at(uin → uid,name → display)。 */
952
- function atToCanonical(el, uinToUidMap) {
953
- const id = el.attrs?.["id"] ?? "";
954
- if (id === "all") return [{
955
- type: "at",
956
- target: "all"
957
- }];
958
- if (id === "") return [];
959
- const target = uinToUidMap.get(id) ?? id;
960
- const display = el.attrs?.["name"];
961
- const base = {
962
- type: "at",
963
- target
964
- };
965
- if (display !== void 0 && display !== "") return [{
966
- ...base,
967
- display
968
- }];
969
- return [base];
970
- }
971
- /** Satori img → canonical image。 */
972
- async function imgToCanonical(el, deps) {
973
- const path = await resolveMediaSrc(el, deps);
974
- return path === null ? [] : [{
975
- type: "image",
976
- path
977
- }];
978
- }
979
- /** Satori audio → canonical voice(非 silk 转码)。 */
980
- async function audioToCanonical(el, deps) {
981
- const path = await resolveMediaSrc(el, deps);
982
- return path === null ? [] : [{
983
- type: "voice",
984
- path: await ensureSilk(path)
985
- }];
986
- }
987
- /** Satori video → canonical video。 */
988
- async function videoToCanonical(el, deps) {
989
- const path = await resolveMediaSrc(el, deps);
990
- return path === null ? [] : [{
991
- type: "video",
992
- path
993
- }];
994
- }
995
- /** Satori file → canonical file(title → name)。 */
996
- async function fileToCanonical(el, deps) {
997
- const path = await resolveMediaSrc(el, deps);
998
- if (path === null) return [];
999
- const title = el.attrs?.["title"];
1000
- const base = {
1001
- type: "file",
1002
- path
1003
- };
1004
- if (title !== void 0 && title !== "") return [{
1005
- ...base,
1006
- name: title
1007
- }];
1008
- return [base];
1009
- }
1010
- /** Satori message:forward 属性 → canonical forward;否则展开子元素。 */
1011
- async function messageToCanonical(el, deps, uinToUidMap) {
1012
- if (el.attrs?.["forward"] !== void 0) {
1013
- const id = el.attrs?.["id"] ?? "";
1014
- return id === "" ? [] : [{
1015
- type: "forward",
1016
- messageIds: [id]
1017
- }];
1018
- }
1019
- return flattenChildren(el.children, deps, uinToUidMap);
1020
- }
1021
- /** 单个元素 → canonical(递归;修饰/排版元素展开子元素)。 */
1022
- async function elementToCanonical(el, deps, uinToUidMap) {
1023
- switch (el.type) {
1024
- case "text": {
1025
- const text = el.text ?? "";
1026
- return text === "" ? [] : [{
1027
- type: "text",
1028
- text
1029
- }];
1030
- }
1031
- case "at": return atToCanonical(el, uinToUidMap);
1032
- case "img": return imgToCanonical(el, deps);
1033
- case "audio": return audioToCanonical(el, deps);
1034
- case "video": return videoToCanonical(el, deps);
1035
- case "file": return fileToCanonical(el, deps);
1036
- case "emoji": {
1037
- const id = el.attrs?.["id"] ?? "";
1038
- return id === "" ? [] : [{
1039
- type: "face",
1040
- id
1041
- }];
1042
- }
1043
- case "quote": {
1044
- const id = el.attrs?.["id"] ?? "";
1045
- return id === "" ? [] : [{
1046
- type: "reply",
1047
- messageId: id
1048
- }];
1049
- }
1050
- case "message": return messageToCanonical(el, deps, uinToUidMap);
1051
- case "br": return [{
1052
- type: "text",
1053
- text: "\n"
1054
- }];
1055
- case "author":
1056
- case "sharp":
1057
- case "a":
1058
- case "button": return [];
1059
- default: return flattenChildren(el.children, deps, uinToUidMap);
1060
- }
1061
- }
1062
- /** 展开子元素为 canonical(递归)。 */
1063
- async function flattenChildren(children, deps, uinToUidMap) {
1064
- const out = [];
1065
- for (const child of children ?? []) out.push(...await elementToCanonical(child, deps, uinToUidMap));
1066
- return out;
1067
- }
1068
- /** 解析消息 content 字符串 → canonical(message.create 用)。 */
1069
- async function parseContentToCanonical(content, deps) {
1070
- return satoriToCanonicalElements(parseElements(content), deps);
1071
- }
1072
- //#endregion
1073
1107
  //#region src/satori/action/message.ts
1074
1108
  /**
1075
1109
  * Satori 消息动作:message.create / get / list / delete
@@ -1590,31 +1624,6 @@ function toMember(uid, nick, remark, uidToUinMap) {
1590
1624
  return member;
1591
1625
  }
1592
1626
  //#endregion
1593
- //#region src/satori/helper/config.ts
1594
- /**
1595
- * Satori 协议配置(zod schema,归属本包,ADR-012)
1596
- * 协议配置 schema 在各自协议包,kernel 只提供 ConfigBase 基类。
1597
- */
1598
- /** HTTP RPC 服务器默认端口(satori.chat 惯例 5500 区间,取 5500)。 */
1599
- const DEFAULT_HTTP_PORT = 5500;
1600
- /** WS 事件服务默认端口(satori.chat 惯例 5500 区间,取 5501)。 */
1601
- const DEFAULT_WS_PORT = 5501;
1602
- /** HTTP RPC 服务器实例(POST /v1/{resource}.{method})。 */
1603
- const httpServerSchema = createHttpServerSchema(DEFAULT_HTTP_PORT);
1604
- /** WS 事件服务实例(/v1/events 信令,第三方 SDK 连入)。 */
1605
- const wsServerSchema = createWsServerSchema(DEFAULT_WS_PORT);
1606
- /** Satori 配置 schema。 */
1607
- const satoriConfigSchema = z.object({
1608
- /** 鉴权 token(全局默认,实例未指定时继承)。 */
1609
- token: z.string().optional(),
1610
- /** 媒体资源(img/audio/video/file)下载缓存目录。 */
1611
- cacheDir: z.string().optional(),
1612
- /** HTTP RPC 服务器列表。 */
1613
- httpServers: z.array(httpServerSchema).default([]),
1614
- /** WS 事件服务列表。 */
1615
- wsServers: z.array(wsServerSchema).default([])
1616
- });
1617
- //#endregion
1618
1627
  //#region src/satori/types/event.ts
1619
1628
  /** 信令类型。 */
1620
1629
  const Opcode = {