liangzimixin 0.3.70 → 0.3.71

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.cjs CHANGED
@@ -18532,7 +18532,7 @@ async function resolveAndUploadMedia(params) {
18532
18532
  content: JSON.stringify(contentPayload),
18533
18533
  skipEncrypt: params.skipEncrypt,
18534
18534
  encryptionMeta
18535
- // 文件加密的 keyId/iv → sendMessage 用于构建 extra
18535
+ // 文件加密的 keyId/iv → sendMessage 用于构建 extraContent
18536
18536
  });
18537
18537
  log5.info("media:sent", {
18538
18538
  chatId,
@@ -18772,6 +18772,10 @@ var quantumImOutbound = {
18772
18772
  /** 发送文本消息 — 通过 messagePipe 发送 */
18773
18773
  sendText: async ({ cfg, to, text, accountId }) => {
18774
18774
  log7.info("sendText called", { to, textLength: text.length });
18775
+ if (!text?.trim()) {
18776
+ log7.warn("outbound:sendText skipped \u2014 empty text", { to });
18777
+ return { channel: CHANNEL_ID, messageId: "", chatId: to };
18778
+ }
18775
18779
  if (!messagePipeGetter) {
18776
18780
  log7.error("outbound:sendText failed, messagePipe not initialized");
18777
18781
  return { channel: CHANNEL_ID, messageId: "", chatId: to };
@@ -20954,7 +20958,7 @@ var CallbackDataSchema = external_exports.object({
20954
20958
  msgUid: external_exports.string(),
20955
20959
  type: external_exports.string(),
20956
20960
  content: external_exports.record(external_exports.string(), external_exports.unknown()),
20957
- extra: external_exports.string().optional()
20961
+ extraContent: external_exports.string().optional()
20958
20962
  });
20959
20963
  var MessagePipe = class _MessagePipe {
20960
20964
  wsClient;
@@ -21071,8 +21075,8 @@ var MessagePipe = class _MessagePipe {
21071
21075
  /** 文件类消息类型集合 — 这些类型的加密消息保留原始 content(文件元数据) */
21072
21076
  static FILE_MSG_TYPES = /* @__PURE__ */ new Set(["image", "file", "voice", "video"]);
21073
21077
  /**
21074
- * 构建文件类加密消息的 extra — 完整模板格式。
21075
- * 文件消息不加密 content(需要保留 fileId 等元数据),只在 extra 中携带会话信息。
21078
+ * 构建文件类加密消息的 extraContent — 完整模板格式。
21079
+ * 文件消息不加密 content(需要保留 fileId 等元数据),只在 extraContent 中携带会话信息。
21076
21080
  */
21077
21081
  static buildFileEncryptExtra(iv, keyId) {
21078
21082
  return JSON.stringify({
@@ -21104,27 +21108,27 @@ var MessagePipe = class _MessagePipe {
21104
21108
  *
21105
21109
  * 文件类消息 (image/file/voice/video):
21106
21110
  * - content 保留原始文件元数据(fileId、fileName、size 等)
21107
- * - extra 使用完整模板格式,encryptMsg 为空
21111
+ * - extraContent 使用完整模板格式,encryptMsg 为空
21108
21112
  *
21109
21113
  * 文本类消息 (text/markdown):
21110
- * - content 加密后置空,密文放入 extra.encryptMsg
21114
+ * - content 加密后置空,密文放入 extraContent.encryptMsg
21111
21115
  */
21112
21116
  async sendMessage(msg) {
21113
21117
  let content = msg.content;
21114
- let extra = "";
21118
+ let extraContent = "";
21115
21119
  const isFileMessage = _MessagePipe.FILE_MSG_TYPES.has(msg.msgType);
21116
21120
  if (!msg.skipEncrypt && this.quantumAccount) {
21117
21121
  if (this.encryptionMode === "quantum_only") {
21118
21122
  if (isFileMessage && msg.encryptionMeta) {
21119
- extra = _MessagePipe.buildFileEncryptExtra(msg.encryptionMeta.iv, msg.encryptionMeta.keyId);
21123
+ extraContent = _MessagePipe.buildFileEncryptExtra(msg.encryptionMeta.iv, msg.encryptionMeta.keyId);
21120
21124
  log25.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_only)", { sessionId: msg.encryptionMeta.keyId, msgType: msg.msgType });
21121
21125
  } else if (isFileMessage) {
21122
21126
  const { keyId, iv } = await this.crypto.encrypt(msg.content);
21123
- extra = _MessagePipe.buildFileEncryptExtra(iv, keyId);
21127
+ extraContent = _MessagePipe.buildFileEncryptExtra(iv, keyId);
21124
21128
  log25.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_only, fallback)", { sessionId: keyId, msgType: msg.msgType });
21125
21129
  } else {
21126
21130
  const { ciphertext, keyId, iv } = await this.crypto.encrypt(msg.content);
21127
- extra = JSON.stringify({
21131
+ extraContent = JSON.stringify({
21128
21132
  cryptoIv: iv,
21129
21133
  encryptMsg: ciphertext,
21130
21134
  sessionId: keyId
@@ -21135,15 +21139,15 @@ var MessagePipe = class _MessagePipe {
21135
21139
  } else {
21136
21140
  try {
21137
21141
  if (isFileMessage && msg.encryptionMeta) {
21138
- extra = _MessagePipe.buildFileEncryptExtra(msg.encryptionMeta.iv, msg.encryptionMeta.keyId);
21142
+ extraContent = _MessagePipe.buildFileEncryptExtra(msg.encryptionMeta.iv, msg.encryptionMeta.keyId);
21139
21143
  log25.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_and_plain)", { sessionId: msg.encryptionMeta.keyId, msgType: msg.msgType });
21140
21144
  } else if (isFileMessage) {
21141
21145
  const { keyId, iv } = await this.crypto.encrypt(msg.content);
21142
- extra = _MessagePipe.buildFileEncryptExtra(iv, keyId);
21146
+ extraContent = _MessagePipe.buildFileEncryptExtra(iv, keyId);
21143
21147
  log25.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_and_plain, fallback)", { sessionId: keyId, msgType: msg.msgType });
21144
21148
  } else {
21145
21149
  const { ciphertext, keyId, iv } = await this.crypto.encrypt(msg.content);
21146
- extra = JSON.stringify({
21150
+ extraContent = JSON.stringify({
21147
21151
  cryptoIv: iv,
21148
21152
  encryptMsg: ciphertext,
21149
21153
  sessionId: keyId
@@ -21172,8 +21176,8 @@ var MessagePipe = class _MessagePipe {
21172
21176
  msg_type: msg.msgType,
21173
21177
  content
21174
21178
  };
21175
- if (extra) {
21176
- body.extra = extra;
21179
+ if (extraContent) {
21180
+ body.extraContent = extraContent;
21177
21181
  }
21178
21182
  if (msg.replyToMessageId) {
21179
21183
  body.reply_msg_id = msg.replyToMessageId;
@@ -21184,7 +21188,7 @@ var MessagePipe = class _MessagePipe {
21184
21188
  }
21185
21189
  log25.info("\u{1F4E4} outbound:sent \u2192 IM \u670D\u52A1\u5668", {
21186
21190
  chatId: msg.chatId,
21187
- encrypted: Boolean(extra),
21191
+ encrypted: Boolean(extraContent),
21188
21192
  requestId: result.request_id,
21189
21193
  body
21190
21194
  });
@@ -21374,16 +21378,16 @@ var MessagePipe = class _MessagePipe {
21374
21378
  groupId: callbackData.groupId,
21375
21379
  type: callbackData.type,
21376
21380
  content: callbackData.content,
21377
- extra: callbackData.extra
21381
+ extraContent: callbackData.extraContent
21378
21382
  });
21379
- const extra = callbackData.extra;
21383
+ const extraContent = callbackData.extraContent;
21380
21384
  let isEncrypted = false;
21381
- if (typeof extra === "string" && extra.length > 0) {
21385
+ if (typeof extraContent === "string" && extraContent.length > 0) {
21382
21386
  try {
21383
- const parsed = JSON.parse(extra);
21387
+ const parsed = JSON.parse(extraContent);
21384
21388
  isEncrypted = Boolean(parsed.encryptMsg);
21385
21389
  } catch {
21386
- log25.debug("\u2139\uFE0F extra \u4E0D\u662F\u6709\u6548 JSON\uFF0C\u89C6\u4E3A\u660E\u6587", { msgUid: callbackData.msgUid });
21390
+ log25.debug("\u2139\uFE0F extraContent \u4E0D\u662F\u6709\u6548 JSON\uFF0C\u89C6\u4E3A\u660E\u6587", { msgUid: callbackData.msgUid });
21387
21391
  }
21388
21392
  }
21389
21393
  let contentObj;
@@ -21498,15 +21502,15 @@ var MessagePipe = class _MessagePipe {
21498
21502
  }
21499
21503
  }
21500
21504
  /**
21501
- * 解密 extra 中的加密内容。
21505
+ * 解密 extraContent 中的加密内容。
21502
21506
  * @throws 解密失败时抛出异常
21503
21507
  */
21504
21508
  async decryptExtra(callbackData) {
21505
- const extraData = JSON.parse(callbackData.extra);
21509
+ const extraData = JSON.parse(callbackData.extraContent);
21506
21510
  const encryptMsg = extraData.encryptMsg ?? "";
21507
21511
  const sessionId = extraData.sessionId ?? "";
21508
21512
  if (!encryptMsg) {
21509
- log25.warn("\u26A0\uFE0F extra \u4E2D encryptMsg \u4E3A\u7A7A\uFF0C\u4F7F\u7528\u539F\u59CB content", { msgUid: callbackData.msgUid });
21513
+ log25.warn("\u26A0\uFE0F extraContent \u4E2D encryptMsg \u4E3A\u7A7A\uFF0C\u4F7F\u7528\u539F\u59CB content", { msgUid: callbackData.msgUid });
21510
21514
  return callbackData.content;
21511
21515
  }
21512
21516
  const cryptoIv = extraData.cryptoIv ?? "";
package/dist/index.d.cts CHANGED
@@ -214,7 +214,7 @@ interface OutboundMessage {
214
214
  replyToMessageId?: string;
215
215
  /** 跳过出站加密 — 用于"思考中"等无需加密的系统提示消息 */
216
216
  skipEncrypt?: boolean;
217
- /** 文件加密元数据 — 文件已在上传前加密时传入,sendMessage 直接使用此 keyId/iv 构建 extra */
217
+ /** 文件加密元数据 — 文件已在上传前加密时传入,sendMessage 直接使用此 keyId/iv 构建 extraContent */
218
218
  encryptionMeta?: {
219
219
  keyId: string;
220
220
  iv: string;
@@ -925,8 +925,8 @@ declare class MessagePipe {
925
925
  /** 文件类消息类型集合 — 这些类型的加密消息保留原始 content(文件元数据) */
926
926
  private static readonly FILE_MSG_TYPES;
927
927
  /**
928
- * 构建文件类加密消息的 extra — 完整模板格式。
929
- * 文件消息不加密 content(需要保留 fileId 等元数据),只在 extra 中携带会话信息。
928
+ * 构建文件类加密消息的 extraContent — 完整模板格式。
929
+ * 文件消息不加密 content(需要保留 fileId 等元数据),只在 extraContent 中携带会话信息。
930
930
  */
931
931
  private static buildFileEncryptExtra;
932
932
  /**
@@ -940,10 +940,10 @@ declare class MessagePipe {
940
940
  *
941
941
  * 文件类消息 (image/file/voice/video):
942
942
  * - content 保留原始文件元数据(fileId、fileName、size 等)
943
- * - extra 使用完整模板格式,encryptMsg 为空
943
+ * - extraContent 使用完整模板格式,encryptMsg 为空
944
944
  *
945
945
  * 文本类消息 (text/markdown):
946
- * - content 加密后置空,密文放入 extra.encryptMsg
946
+ * - content 加密后置空,密文放入 extraContent.encryptMsg
947
947
  */
948
948
  sendMessage(msg: OutboundMessage): Promise<void>;
949
949
  /**
@@ -990,7 +990,7 @@ declare class MessagePipe {
990
990
  */
991
991
  private sendHintMessage;
992
992
  /**
993
- * 解密 extra 中的加密内容。
993
+ * 解密 extraContent 中的加密内容。
994
994
  * @throws 解密失败时抛出异常
995
995
  */
996
996
  private decryptExtra;
@@ -4603,7 +4603,7 @@ async function resolveAndUploadMedia(params) {
4603
4603
  content: JSON.stringify(contentPayload),
4604
4604
  skipEncrypt: params.skipEncrypt,
4605
4605
  encryptionMeta
4606
- // 文件加密的 keyId/iv → sendMessage 用于构建 extra
4606
+ // 文件加密的 keyId/iv → sendMessage 用于构建 extraContent
4607
4607
  });
4608
4608
  log5.info("media:sent", {
4609
4609
  chatId,
@@ -4843,6 +4843,10 @@ var quantumImOutbound = {
4843
4843
  /** 发送文本消息 — 通过 messagePipe 发送 */
4844
4844
  sendText: async ({ cfg, to, text, accountId }) => {
4845
4845
  log7.info("sendText called", { to, textLength: text.length });
4846
+ if (!text?.trim()) {
4847
+ log7.warn("outbound:sendText skipped \u2014 empty text", { to });
4848
+ return { channel: CHANNEL_ID, messageId: "", chatId: to };
4849
+ }
4846
4850
  if (!messagePipeGetter) {
4847
4851
  log7.error("outbound:sendText failed, messagePipe not initialized");
4848
4852
  return { channel: CHANNEL_ID, messageId: "", chatId: to };
@@ -19852,7 +19856,7 @@ var CallbackDataSchema = external_exports.object({
19852
19856
  msgUid: external_exports.string(),
19853
19857
  type: external_exports.string(),
19854
19858
  content: external_exports.record(external_exports.string(), external_exports.unknown()),
19855
- extra: external_exports.string().optional()
19859
+ extraContent: external_exports.string().optional()
19856
19860
  });
19857
19861
  var MessagePipe = class _MessagePipe {
19858
19862
  wsClient;
@@ -19969,8 +19973,8 @@ var MessagePipe = class _MessagePipe {
19969
19973
  /** 文件类消息类型集合 — 这些类型的加密消息保留原始 content(文件元数据) */
19970
19974
  static FILE_MSG_TYPES = /* @__PURE__ */ new Set(["image", "file", "voice", "video"]);
19971
19975
  /**
19972
- * 构建文件类加密消息的 extra — 完整模板格式。
19973
- * 文件消息不加密 content(需要保留 fileId 等元数据),只在 extra 中携带会话信息。
19976
+ * 构建文件类加密消息的 extraContent — 完整模板格式。
19977
+ * 文件消息不加密 content(需要保留 fileId 等元数据),只在 extraContent 中携带会话信息。
19974
19978
  */
19975
19979
  static buildFileEncryptExtra(iv, keyId) {
19976
19980
  return JSON.stringify({
@@ -20002,27 +20006,27 @@ var MessagePipe = class _MessagePipe {
20002
20006
  *
20003
20007
  * 文件类消息 (image/file/voice/video):
20004
20008
  * - content 保留原始文件元数据(fileId、fileName、size 等)
20005
- * - extra 使用完整模板格式,encryptMsg 为空
20009
+ * - extraContent 使用完整模板格式,encryptMsg 为空
20006
20010
  *
20007
20011
  * 文本类消息 (text/markdown):
20008
- * - content 加密后置空,密文放入 extra.encryptMsg
20012
+ * - content 加密后置空,密文放入 extraContent.encryptMsg
20009
20013
  */
20010
20014
  async sendMessage(msg) {
20011
20015
  let content = msg.content;
20012
- let extra = "";
20016
+ let extraContent = "";
20013
20017
  const isFileMessage = _MessagePipe.FILE_MSG_TYPES.has(msg.msgType);
20014
20018
  if (!msg.skipEncrypt && this.quantumAccount) {
20015
20019
  if (this.encryptionMode === "quantum_only") {
20016
20020
  if (isFileMessage && msg.encryptionMeta) {
20017
- extra = _MessagePipe.buildFileEncryptExtra(msg.encryptionMeta.iv, msg.encryptionMeta.keyId);
20021
+ extraContent = _MessagePipe.buildFileEncryptExtra(msg.encryptionMeta.iv, msg.encryptionMeta.keyId);
20018
20022
  log16.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_only)", { sessionId: msg.encryptionMeta.keyId, msgType: msg.msgType });
20019
20023
  } else if (isFileMessage) {
20020
20024
  const { keyId, iv } = await this.crypto.encrypt(msg.content);
20021
- extra = _MessagePipe.buildFileEncryptExtra(iv, keyId);
20025
+ extraContent = _MessagePipe.buildFileEncryptExtra(iv, keyId);
20022
20026
  log16.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_only, fallback)", { sessionId: keyId, msgType: msg.msgType });
20023
20027
  } else {
20024
20028
  const { ciphertext, keyId, iv } = await this.crypto.encrypt(msg.content);
20025
- extra = JSON.stringify({
20029
+ extraContent = JSON.stringify({
20026
20030
  cryptoIv: iv,
20027
20031
  encryptMsg: ciphertext,
20028
20032
  sessionId: keyId
@@ -20033,15 +20037,15 @@ var MessagePipe = class _MessagePipe {
20033
20037
  } else {
20034
20038
  try {
20035
20039
  if (isFileMessage && msg.encryptionMeta) {
20036
- extra = _MessagePipe.buildFileEncryptExtra(msg.encryptionMeta.iv, msg.encryptionMeta.keyId);
20040
+ extraContent = _MessagePipe.buildFileEncryptExtra(msg.encryptionMeta.iv, msg.encryptionMeta.keyId);
20037
20041
  log16.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_and_plain)", { sessionId: msg.encryptionMeta.keyId, msgType: msg.msgType });
20038
20042
  } else if (isFileMessage) {
20039
20043
  const { keyId, iv } = await this.crypto.encrypt(msg.content);
20040
- extra = _MessagePipe.buildFileEncryptExtra(iv, keyId);
20044
+ extraContent = _MessagePipe.buildFileEncryptExtra(iv, keyId);
20041
20045
  log16.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_and_plain, fallback)", { sessionId: keyId, msgType: msg.msgType });
20042
20046
  } else {
20043
20047
  const { ciphertext, keyId, iv } = await this.crypto.encrypt(msg.content);
20044
- extra = JSON.stringify({
20048
+ extraContent = JSON.stringify({
20045
20049
  cryptoIv: iv,
20046
20050
  encryptMsg: ciphertext,
20047
20051
  sessionId: keyId
@@ -20070,8 +20074,8 @@ var MessagePipe = class _MessagePipe {
20070
20074
  msg_type: msg.msgType,
20071
20075
  content
20072
20076
  };
20073
- if (extra) {
20074
- body.extra = extra;
20077
+ if (extraContent) {
20078
+ body.extraContent = extraContent;
20075
20079
  }
20076
20080
  if (msg.replyToMessageId) {
20077
20081
  body.reply_msg_id = msg.replyToMessageId;
@@ -20082,7 +20086,7 @@ var MessagePipe = class _MessagePipe {
20082
20086
  }
20083
20087
  log16.info("\u{1F4E4} outbound:sent \u2192 IM \u670D\u52A1\u5668", {
20084
20088
  chatId: msg.chatId,
20085
- encrypted: Boolean(extra),
20089
+ encrypted: Boolean(extraContent),
20086
20090
  requestId: result.request_id,
20087
20091
  body
20088
20092
  });
@@ -20272,16 +20276,16 @@ var MessagePipe = class _MessagePipe {
20272
20276
  groupId: callbackData.groupId,
20273
20277
  type: callbackData.type,
20274
20278
  content: callbackData.content,
20275
- extra: callbackData.extra
20279
+ extraContent: callbackData.extraContent
20276
20280
  });
20277
- const extra = callbackData.extra;
20281
+ const extraContent = callbackData.extraContent;
20278
20282
  let isEncrypted = false;
20279
- if (typeof extra === "string" && extra.length > 0) {
20283
+ if (typeof extraContent === "string" && extraContent.length > 0) {
20280
20284
  try {
20281
- const parsed = JSON.parse(extra);
20285
+ const parsed = JSON.parse(extraContent);
20282
20286
  isEncrypted = Boolean(parsed.encryptMsg);
20283
20287
  } catch {
20284
- log16.debug("\u2139\uFE0F extra \u4E0D\u662F\u6709\u6548 JSON\uFF0C\u89C6\u4E3A\u660E\u6587", { msgUid: callbackData.msgUid });
20288
+ log16.debug("\u2139\uFE0F extraContent \u4E0D\u662F\u6709\u6548 JSON\uFF0C\u89C6\u4E3A\u660E\u6587", { msgUid: callbackData.msgUid });
20285
20289
  }
20286
20290
  }
20287
20291
  let contentObj;
@@ -20396,15 +20400,15 @@ var MessagePipe = class _MessagePipe {
20396
20400
  }
20397
20401
  }
20398
20402
  /**
20399
- * 解密 extra 中的加密内容。
20403
+ * 解密 extraContent 中的加密内容。
20400
20404
  * @throws 解密失败时抛出异常
20401
20405
  */
20402
20406
  async decryptExtra(callbackData) {
20403
- const extraData = JSON.parse(callbackData.extra);
20407
+ const extraData = JSON.parse(callbackData.extraContent);
20404
20408
  const encryptMsg = extraData.encryptMsg ?? "";
20405
20409
  const sessionId = extraData.sessionId ?? "";
20406
20410
  if (!encryptMsg) {
20407
- log16.warn("\u26A0\uFE0F extra \u4E2D encryptMsg \u4E3A\u7A7A\uFF0C\u4F7F\u7528\u539F\u59CB content", { msgUid: callbackData.msgUid });
20411
+ log16.warn("\u26A0\uFE0F extraContent \u4E2D encryptMsg \u4E3A\u7A7A\uFF0C\u4F7F\u7528\u539F\u59CB content", { msgUid: callbackData.msgUid });
20408
20412
  return callbackData.content;
20409
20413
  }
20410
20414
  const cryptoIv = extraData.cryptoIv ?? "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "liangzimixin",
3
- "version": "0.3.70",
3
+ "version": "0.3.71",
4
4
  "description": "Quantum-encrypted IM channel plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -7,7 +7,7 @@ REM liangzimixin install script (Windows)
7
7
  REM Usage: liangzimixin_install.bat <appId> <appSecret> [quantumAccount]
8
8
  REM ============================================================
9
9
 
10
- set "SCRIPT_VERSION=0.3.70"
10
+ set "SCRIPT_VERSION=0.3.71"
11
11
  set "NPM_PACKAGE=liangzimixin"
12
12
 
13
13
  set "SKIP_SELF_UPDATE=0"
@@ -6,7 +6,7 @@ set -euo pipefail
6
6
  # 用法: ./liangzimixin_install.sh <appId> <appSecret> [quantumAccount]
7
7
  # ============================================================
8
8
 
9
- SCRIPT_VERSION="0.3.70"
9
+ SCRIPT_VERSION="0.3.71"
10
10
  NPM_PACKAGE="liangzimixin"
11
11
 
12
12
  # ── 颜色 ──────────────────────────────────────────────────────