@zhin.js/adapter-icqq 1.1.29 → 1.2.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,70 @@
1
1
  # @zhin.js/adapter-icqq
2
2
 
3
+ ## 1.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8bf8dfa: #1250 第 10 批(试用反馈、关键修复与 AI 账号)的公开 API 新增按 minor 发布:Console 协议的功能未启用状态、功能矩阵分类与传输状态文字;Host 端口校验与 owner 判定导出;网关项目标识与插件候选;Agent 运行时的连接测试与 Host 环境变量引用授权;模型请求首响应时限;`zhin ai` 命令与 `--allow-unverified-host`;日志单行转义导出;Host 根配置新增 `firstResponseTimeoutMs`,icqq 新增 `logLevel` 配置(按 commandSuggestion 先例为 minor)。
8
+
9
+ ### Patch Changes
10
+
11
+ - da9dc8a: 聊天角色引导文案(ADR 0060 P0):需要 zhin 角色的命令被拒绝时,回复说明去哪里获得角色(联系主人 / Console「聊天角色」页),不点名缺哪个角色或谁持有;发言者是群主 / 群管理员时另说明平台身份不等于 zhin 角色。`功能` / `角色` 的拒绝回复同样指向 Console,尚无全局 owner 时提示认领码;Agent 信任档位的限制说明改为「联系主人授予 trusted 角色」。Sandbox 页把模拟的「Master」改称「群主(平台身份)」,并显示服务端解析的 zhin 角色与 Agent 信任档位(入站回显帧新增 `senderChatRole` / `senderTrustLevel`)。ICQQ README 去掉已不读取的 `master`「必填」写法。
12
+ - 35e5d89: 日志里的聊天内容不再能伪造日志行、群聊全文默认不落盘(#1250):
13
+
14
+ - `@zhin.js/logger`:新增 `escapeLogText()`(换行写成 `⏎`,ANSI 等控制字符转义为 `\uXXXX`,保留制表符)与 `LOG_CONTENT_MAX_LENGTH`(300)。记录消息内容用的 `truncatePreview()` 现在总是转义成单行,未指定长度时截断到 300 字(原先不截断)。
15
+ - ICQQ 适配器:SDK 自己的 log4js 输出改由适配器转写到 Zhin 日志(同一格式,输出不是终端时没有 ANSI 颜色),继续脱敏验证链接和手机号。新增配置项 `logLevel`(`trace`…`mark` / `off`,默认 `warn`,原先写死 `info`),SDK 逐条打印的收发消息原文(`recv from: …`、`succeed to send: …`)一律按 `debug` 处理,默认不写入日志;打开时转义成单行并截断。
16
+
17
+ - Updated dependencies [8bf8dfa]
18
+ - Updated dependencies [da9dc8a]
19
+ - Updated dependencies [bc31cd3]
20
+ - Updated dependencies [760e9f5]
21
+ - Updated dependencies [5d90159]
22
+ - Updated dependencies [73475bd]
23
+ - Updated dependencies [35e5d89]
24
+ - Updated dependencies [809c6ee]
25
+ - @zhin.js/logger@1.2.0
26
+ - @zhin.js/core@1.9.0
27
+ - @zhin.js/adapter@1.3.1
28
+ - @zhin.js/adapter-onebot-events@0.0.10
29
+
30
+ ## 1.1.30
31
+
32
+ ### Patch Changes
33
+
34
+ - e6d7cc1: 配置表单与插件显示名的多语言约定(#1250):Schema 节点沿用已有的 `x-i18n`(`Schema#i18n()`),按 `zh-CN` / `en` 分别声明字段的 `title`、`description` 与 `enumLabels`;插件 `metadata`(`definePlugin` 或 `package.json#zhin.metadata`)新增可选的 `displayNameI18n: { 'zh-CN'?, en? }`,与 `displayName` 一样只能在一处声明,清单只接受这两个键且值必须是非空字符串。Console 插件列表(`/api/plugins`、插件详情)和 `feature.list` / `feature.matrix` 原样带出 `displayNameI18n`,由 Console 按界面语言选择(当前语言 → `en` → `displayName` → 实例 key)。没有这些字段时行为不变。
35
+
36
+ 官方插件补齐中英文配置字段标题与说明(group-suite、repeater、roulette、answers、calendar、ledger、reminder、rss、earthquake、bilibili、content-moderation、lottery、nettools、60s、process-monitor、activity-feedback)以及全部官方插件的中英文显示名。
37
+
38
+ - 7cd299e: ICQQ `system.*` 事件按语义分级记录(#1250):上线(`system.online`)与等待人工处理的登录验证(二维码、设备锁、滑块、身份验证)记为 INFO,只有登录失败(`system.login.error`)与掉线(`system.offline*`)记为 WARN,正常登录不再进入「历史警告」。
39
+ - 70ddaf4: 插件市场元数据(#1250 marketplace-metadata):`package.json#zhin.metadata` 新增可选的 `descriptionI18n: { 'zh-CN'?, en? }`(本地化描述)与 `category`(`adapter`、`ai`、`admin`、`community`、`entertainment`、`game`、`information`、`moderation`、`service`、`utility` 之一),校验方式与 `displayNameI18n` 相同:只接受这两个语言键、值为非空字符串、分类必须在枚举内。这两个字段只能写在清单里,插件市场才能从发布的 `package.json` 读到。`@zhin.js/plugin-runtime` 导出 `PLUGIN_CATEGORIES`、`PLUGIN_CATEGORY_LABELS`、`informativeKeywords`、`packageAuthorName`、`readPluginMarketplaceMetadata` 等读取工具。
40
+
41
+ 市场读取侧:`/pub/marketplace/search` 带出 `displayNameI18n` / `descriptionI18n`,分类读声明的 `category`(旧索引的 `util` 等按别名映射),可选 `locale=zh-CN|en` 按语言解析显示名与描述,并按两种语言的名称与描述搜索;`/pub/marketplace/detail/*` 带出发布清单的显示名、描述翻译与分类。作者一律取 `author`(没有时取 `maintainers`),不再显示发布账号——此前 `plugins.json` 生成脚本优先读 `_npmUser`,CI 可信发布的包因此都显示为「GitHub Actions」。keywords 去掉 `zhin`、`zhin.js`、`bot` 等通用词后展示与参与搜索。`zhin search` / `zhin info` 同样显示中文描述、分类和维护者,`zhin info` 不再读不存在的 `zhin.displayName` / `zhin.category`。
42
+
43
+ 官方插件与适配器:显示名移到 `package.json#zhin.metadata`,补齐中英文描述、分类、3–6 个有信息量的 keywords,author 统一为 `{ name: "lc-cn", url: "https://github.com/zhinjs/zhin" }`(不再发布邮箱);npm `description` 与英文描述一致。
44
+
45
+ - Updated dependencies [7cd299e]
46
+ - Updated dependencies [7cd299e]
47
+ - Updated dependencies [9026329]
48
+ - Updated dependencies [ccab2c1]
49
+ - Updated dependencies [c6b5563]
50
+ - Updated dependencies [f1a9745]
51
+ - Updated dependencies [e0b4e51]
52
+ - Updated dependencies [cca4427]
53
+ - Updated dependencies [b08cff2]
54
+ - Updated dependencies [704c7a4]
55
+ - Updated dependencies [07bd3dc]
56
+ - @zhin.js/core@1.8.0
57
+ - @zhin.js/adapter@1.3.0
58
+ - @zhin.js/im-contract@1.2.0
59
+ - @zhin.js/console-contract@1.2.0
60
+ - @zhin.js/adapter-onebot-events@0.0.9
61
+ - @zhin.js/page@1.1.20
62
+ - @zhin.js/agent-feature@1.1.19
63
+ - @zhin.js/feature-kit@1.1.18
64
+ - @zhin.js/permission@1.2.2
65
+ - @zhin.js/skill@1.1.19
66
+ - @zhin.js/tool@1.1.21
67
+
3
68
  ## 1.1.29
4
69
 
5
70
  ### Patch Changes
package/README.md CHANGED
@@ -45,13 +45,16 @@ configVersion: 2
45
45
  plugins:
46
46
  icqq: "@zhin.js/adapter-icqq"
47
47
  icqq:
48
- master: "1659488338" # 必填,顶层共享(/approve 与 master 角色)
49
48
  autoReconnect: true
50
49
  endpoints:
51
50
  - id: "${ICQQ_ACCOUNT}" # QQ 号
52
51
  # password: "${ICQQ_PASSWORD}" # 可选;不填则扫码登录
53
52
  ```
54
53
 
54
+ ::: tip 主人不在适配器配置里
55
+ `master` 不再是适配器字段(也不是必填)。「谁是主人」由聊天角色决定(ADR 0056 / ADR 0060):在 Console「聊天角色」中授予全局 owner,或在新部署中按 `zhin status` 给出的认领码私聊机器人发送 `认领 <码>`。旧配置中的 `master` 会在首次启动时迁移为账号层 owner;之后再写入不会自动授予,而是进入 Console「需要你处理」等待确认(见[升级到聊天角色](https://zhin.js.org/migrations/chat-roles))。
56
+ :::
57
+
55
58
  多账号:一个插件实例挂多个 endpoint(`endpoints` 数组逐项覆盖顶层字段,`id` 必填):
56
59
 
57
60
  ```yaml
@@ -59,7 +62,6 @@ configVersion: 2
59
62
  plugins:
60
63
  icqq: "@zhin.js/adapter-icqq"
61
64
  icqq:
62
- master: "1659488338"
63
65
  endpoints:
64
66
  - id: "${ICQQ_ACCOUNT}"
65
67
  - id: "${ICQQ_ACCOUNT_2}"
@@ -205,7 +207,8 @@ ICQQ 首次登录的二维码、滑块、设备锁会出现在「登录待办」
205
207
  - **滑块提交格式**:NT 登录(SDK 的 `sig.nt_captcha` 指向这个滑块)提交 `ticket,randstr`,缺 randstr 时会提示并留在滑块;旧版 wtlogin 只提交 `ticket`。
206
208
  - **验证链接**带会话标识(sid / sig),密保手机号也算敏感信息:
207
209
  - Zhin 自己的日志和 `system.receive` 副事件只有挑战类型和链接的主机名(`{ kind, host }`),不含链接、手机号、设备信息和二维码图片;`login.error` 只带错误码和清洗后的说明。
208
- - icqq SDK 自己的日志(log4js,`mark` 级别会打印滑块、设备锁、身份验证链接和手机号)经适配器包装:链接只保留到路径(查询串变成 `?…`),手机号变成 `***`。
210
+ - icqq SDK 自己的日志(原本经 log4js 输出,`mark` 级别会打印滑块、设备锁、身份验证链接和手机号)改由适配器转写到 Zhin 日志(同一格式;输出不是终端时没有颜色):链接只保留到路径(查询串变成 `?…`),手机号变成 `***`。
211
+ - SDK 日志级别用 `logLevel` 配置(`trace` / `debug` / `info` / `warn` / `error` / `fatal` / `mark` / `off`,默认 `warn`,可在 `endpoints[]` 里单独设置)。SDK 逐条打印的收发消息原文(`recv from: …`、`succeed to send: …`)一律按 `debug` 处理,默认不会写进日志;开到 `debug` 时,消息中的换行写成 `⏎`、控制字符转义,并截断到 300 字,多行消息不会再拆成多条没有时间戳的日志行。
209
212
  - **未覆盖**:SDK 绕过日志直接写到控制台的两处——终端里画出的登录二维码,以及旧版登录 237 分支的 `console.log(proto.toJSON())`(可能包含验证页信息)。这两处不改 SDK 无法拦截;如果控制台输出会被收集,请注意。
210
213
  - `endpoint.login.flow` 事件和历史里不含验证链接(`linkOmitted`),Console 用 `login.flow.get` 读取当前链接。
211
214
  - **设备 JSON** 只存在于当前挑战上:挑战结束即清空;不写 Zhin 的日志,不进事件和 `system.receive`。辅助脚本是固定文本,只在 `accounts.qq.com/login/attack` 上运行,只设置 `window.__INITIAL_STATE__.deviceInfo`,从不自动提交,也不含任何设备数据。
@@ -239,19 +242,18 @@ console:
239
242
 
240
243
  ## 掉线通知与重新登录
241
244
 
242
- 配置 `notify` 后,ICQQ 被踢下线、登录失败或启动失败时,会通过另一个适配器实例给 master 发通知。例如用 QQ 官方机器人实例通知:
245
+ 配置 `notify` 后,ICQQ 被踢下线、登录失败或启动失败时,会通过另一个适配器实例给 `notify` 指向的会话(通常是部署者自己)发通知。例如用 QQ 官方机器人实例通知:
243
246
 
244
247
  ```yaml
245
248
  qq:
246
249
  # ... 已配置好的 QQ 官方机器人
247
250
  icqq:
248
- master: 123456
249
251
  autoReconnect: true # 默认 true:被踢下线后自动重新登录
250
252
  notify:
251
253
  adapter: qq
252
254
  endpoint: bot-1
253
255
  kind: private
254
- id: "${QQ_MASTER_OPENID}" # master 在 QQ 官方机器人里的 openid
256
+ id: "${QQ_OWNER_OPENID}" # 接收通知的人在 QQ 官方机器人里的 openid
255
257
  ```
256
258
 
257
259
  `autoReconnect: true` 时,被踢下线会自动重试登录;出现二维码、滑块、设备锁或身份验证时,通知会提示下一步。收到通知后可以直接在 QQ 里完成:
@@ -266,7 +268,7 @@ icqq:
266
268
 
267
269
  **设备锁**:`icqq login status` 会给出验证链接。用手机 QQ 完成验证后回复 `icqq login confirm`;无法用链接验证时回复 `icqq login code sms`,收到短信后回复 `icqq login code <验证码>`。不回复 `sms` 不会发短信。身份验证需要复制设备信息,请在 Console 完成。等待验证期间不会自动重新登录(否则链接会失效);5 分钟没有处理,挑战过期,等你重新登录。
268
270
 
269
- 命令只接受 master(顶层或 `endpoints[i].master`)或 `notify.id` 对应账号;两者都没配置时请用 Console 的 ICQQ 页面。没有配置 `notify` 时,掉线只写日志并在 Console 显示登录待办。
271
+ 命令只接受全局 owner(聊天角色,ADR 0056)或 `notify.id` 对应账号;两者都没有时请用 Console 的 ICQQ 页面。没有配置 `notify` 时,掉线只写日志并在 Console 显示登录待办。
270
272
 
271
273
  ## 故障排查
272
274
 
package/lib/adapter.js CHANGED
@@ -8,7 +8,7 @@ import { resolveIcqqConfig } from './protocol.js';
8
8
  export class IcqqAdapter extends Adapter {
9
9
  get configurationIdentityFields() { return ['id']; }
10
10
  get configurationUpdateFields() {
11
- return ['password', 'platform', 'ver', 'dataDir', 'signApiAddr', 'autoReconnect', 'autoServer', 'qqnt', 'ntLogin'];
11
+ return ['password', 'platform', 'ver', 'dataDir', 'signApiAddr', 'autoReconnect', 'autoServer', 'qqnt', 'ntLogin', 'logLevel'];
12
12
  }
13
13
  #context;
14
14
  constructor(context) { super(); this.#context = context; }
package/lib/endpoint.d.ts CHANGED
@@ -14,6 +14,12 @@ export interface IcqqEndpointOptions {
14
14
  /** Out-of-band outage notifier; absent in tests/hosts without outbound. */
15
15
  readonly notifier?: AdapterOutageNotifier;
16
16
  }
17
+ /**
18
+ * Log level of an ICQQ `system.*` event by meaning: coming online and login challenges waiting for
19
+ * the person (QR code, device lock, slider, identity check) are normal progress (`info`); only
20
+ * failures (login errors, going offline) are `warn`, so a routine login stays out of the warnings.
21
+ */
22
+ export declare function icqqSystemEventLogLevel(type: string): 'info' | 'warn';
17
23
  export declare class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
18
24
  #private;
19
25
  readonly [endpointSpec]: EndpointSpecification;
package/lib/endpoint.js CHANGED
@@ -8,7 +8,7 @@ import { EndpointDeliveryError } from '@zhin.js/im-contract';
8
8
  import { formatCompact, getAdapterLogger, truncatePreview } from '@zhin.js/logger';
9
9
  import { IcqqLoginDriver, icqqLinkHost } from './icqq-login-assist.js';
10
10
  import { messages as t } from './i18n.js';
11
- import { redactingIcqqLogger } from './sdk-logger.js';
11
+ import { bridgeIcqqSdkLogger } from './sdk-logger.js';
12
12
  import { sanitizeIcqqLoginMessage } from './icqq-login-errors.js';
13
13
  import { IcqqContentResolver } from './content-resolver.js';
14
14
  import { InboundMessageDeduper, findIcqqNestedMessageSource, isIcqqBotMentioned, isIcqqMessagePostType, normalizeIcqqInboundMessage, messageLookupFromIcqqSource, resolveIcqqQuoteIdFromEvent, resolveIcqqInboundMedia, resolveIcqqSelfUserId, shouldSkipSelfInboundMessage, } from './icqq-inbound.js';
@@ -39,6 +39,14 @@ const BOUND_EVENTS = [
39
39
  'system.login.error',
40
40
  'system.login.auth',
41
41
  ];
42
+ /**
43
+ * Log level of an ICQQ `system.*` event by meaning: coming online and login challenges waiting for
44
+ * the person (QR code, device lock, slider, identity check) are normal progress (`info`); only
45
+ * failures (login errors, going offline) are `warn`, so a routine login stays out of the warnings.
46
+ */
47
+ export function icqqSystemEventLogLevel(type) {
48
+ return type === 'online' || type.startsWith('login.') && type !== 'login.error' ? 'info' : 'warn';
49
+ }
42
50
  export class IcqqEndpoint extends ManagedIcqqClient {
43
51
  [endpointSpec];
44
52
  #logger;
@@ -190,12 +198,14 @@ export class IcqqEndpoint extends ManagedIcqqClient {
190
198
  },
191
199
  },
192
200
  });
193
- // The SDK logs verification links (sid / sig) and the phone at `mark` level: redact them.
201
+ this.#logger = getAdapterLogger('icqq', options.config.id);
202
+ // The SDK's own log4js output is replaced by a bridge into this Zhin logger (#1250): one format,
203
+ // no ANSI off a terminal, `logLevel` (default warn), chat text only at debug and escaped onto one
204
+ // line, verification links (sid / sig) and phone numbers redacted.
194
205
  if (this.logger)
195
- this.logger = redactingIcqqLogger(this.logger);
206
+ this.logger = bridgeIcqqSdkLogger(this.#logger, options.config.logLevel);
196
207
  this.#contentResolver = new IcqqContentResolver(this);
197
208
  this.#content = this.#contentResolver.port;
198
- this.#logger = getAdapterLogger('icqq', options.config.id);
199
209
  this.#options = options;
200
210
  this.endpointName = options.config.id;
201
211
  this.#login = new IcqqLoginDriver({
@@ -969,7 +979,7 @@ export class IcqqEndpoint extends ManagedIcqqClient {
969
979
  #logSystemEvent(type, event, safe) {
970
980
  const payload = safe === undefined ? serializeIcqqEvent(event) : safe;
971
981
  const line = payload;
972
- this.#logger.warn(formatCompact({
982
+ this.#logger[icqqSystemEventLogLevel(type)](formatCompact({
973
983
  op: `system.${type}`,
974
984
  endpoint: this.endpointName,
975
985
  ...(line ? { event: JSON.stringify(line) } : {}),
@@ -1370,7 +1380,7 @@ function resolveIcqqGroupReactionTarget(message) {
1370
1380
  }
1371
1381
  function resolveNativeClientConfig(config) {
1372
1382
  return {
1373
- log_level: 'info',
1383
+ log_level: config.logLevel,
1374
1384
  platform: config.platform,
1375
1385
  ...(config.ver ? { ver: config.ver } : {}),
1376
1386
  ...(config.dataDir ? { data_dir: config.dataDir } : {}),
package/lib/protocol.d.ts CHANGED
@@ -3,6 +3,7 @@ import type { Sendable } from '@icqqjs/icqq';
3
3
  import { type ConversationRef, type PlatformStanding } from '@zhin.js/im-contract';
4
4
  import { type AdapterNotifyTarget, type ResolvedAdapterNotifyTarget } from 'zhin.js/adapter';
5
5
  import type { MessageSegment, SendContent } from "zhin.js";
6
+ import { type IcqqSdkLogLevel } from "./sdk-logger.js";
6
7
  export type ActionResult = {
7
8
  id: string;
8
9
  ok: boolean;
@@ -179,6 +180,8 @@ export interface IcqqEndpointConfig extends EndpointProfileConfiguration {
179
180
  readonly autoServer?: boolean;
180
181
  readonly qqnt?: boolean;
181
182
  readonly ntLogin?: boolean;
183
+ /** Level of the ICQQ SDK's own log lines (default `warn`; its per-message lines are `debug`). */
184
+ readonly logLevel?: IcqqSdkLogLevel;
182
185
  /** Optional out-of-band outage notification target (usually a QQ adapter instance). */
183
186
  readonly notify?: AdapterNotifyTarget;
184
187
  }
@@ -198,6 +201,7 @@ export interface ResolvedIcqqConfig {
198
201
  readonly autoServer?: boolean;
199
202
  readonly qqnt?: boolean;
200
203
  readonly ntLogin?: boolean;
204
+ readonly logLevel: IcqqSdkLogLevel;
201
205
  readonly notify?: ResolvedAdapterNotifyTarget;
202
206
  }
203
207
  export interface IcqqWireSegment {
package/lib/protocol.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { isMediaRef } from '@zhin.js/im-contract';
2
2
  import { resolveAdapterNotifyTarget } from 'zhin.js/adapter';
3
3
  import { resolveIcqqMediaFile } from "./cq-message.js";
4
+ import { DEFAULT_ICQQ_SDK_LOG_LEVEL, ICQQ_SDK_LOG_LEVELS, isIcqqSdkLogLevel } from "./sdk-logger.js";
4
5
  export { resolveIcqqInboundMessageId } from "./icqq-inbound.js";
5
6
  /**
6
7
  * 所有操作常量(@icqqjs/icqq)
@@ -192,10 +193,15 @@ export function resolveIcqqConfig(config) {
192
193
  const qqnt = config.qqnt;
193
194
  const ntLogin = config.ntLogin;
194
195
  const notify = resolveAdapterNotifyTarget(config.notify);
196
+ if (config.logLevel !== undefined && !isIcqqSdkLogLevel(config.logLevel)) {
197
+ throw new TypeError(`ICQQ logLevel must be one of ${ICQQ_SDK_LOG_LEVELS.join(', ')}`);
198
+ }
199
+ const logLevel = config.logLevel ?? DEFAULT_ICQQ_SDK_LOG_LEVEL;
195
200
  return {
196
201
  context: 'icqq',
197
202
  id,
198
203
  autoReconnect,
204
+ logLevel,
199
205
  ...(outboundMedia ? { outboundMedia } : {}),
200
206
  ...(password ? { password } : {}),
201
207
  ...(platform != null ? { platform: Number(platform) } : {}),
@@ -12,3 +12,27 @@ export interface IcqqSdkLogger {
12
12
  export declare function redactIcqqLogText(text: string): string;
13
13
  /** Wrap the SDK logger; `level` reads and writes through to the original. */
14
14
  export declare function redactingIcqqLogger<T extends IcqqSdkLogger>(original: T): T;
15
+ /** ICQQ SDK log levels from most to least verbose (`off` silences it). */
16
+ export declare const ICQQ_SDK_LOG_LEVELS: readonly ["trace", "debug", "info", "warn", "error", "fatal", "mark", "off"];
17
+ export type IcqqSdkLogLevel = typeof ICQQ_SDK_LOG_LEVELS[number];
18
+ /** Default `logLevel`: the SDK's warnings and errors, not its per-message `info` lines (#1250). */
19
+ export declare const DEFAULT_ICQQ_SDK_LOG_LEVEL: IcqqSdkLogLevel;
20
+ export declare function isIcqqSdkLogLevel(value: unknown): value is IcqqSdkLogLevel;
21
+ /** The part of a Zhin logger the SDK bridge writes to. */
22
+ export interface IcqqSdkLogTarget {
23
+ debug(message: string): unknown;
24
+ info(message: string): unknown;
25
+ warn(message: string): unknown;
26
+ error(message: string): unknown;
27
+ }
28
+ /**
29
+ * Replaces the SDK's log4js logger with one that writes through the Zhin logger (#1250): one log
30
+ * format, the Zhin logger's colour handling (no ANSI when the output is not a terminal), and
31
+ * - `level` (the SDK's `log_level`, default {@link DEFAULT_ICQQ_SDK_LOG_LEVEL}) filters first;
32
+ * - message dumps go to `debug`, so chat text is not written to disk at the default level;
33
+ * - links and phone numbers are redacted ({@link redactIcqqLogText}), line breaks and control
34
+ * characters escaped and the line capped ({@link truncatePreview}), so a multi-line message
35
+ * cannot forge log lines.
36
+ */
37
+ export declare function bridgeIcqqSdkLogger(target: IcqqSdkLogTarget, level?: IcqqSdkLogLevel): IcqqSdkLogger;
38
+ export declare function isBridgedIcqqSdkLogger(value: unknown): boolean;
package/lib/sdk-logger.js CHANGED
@@ -9,6 +9,8 @@
9
9
  * Not covered (SDK code that bypasses its logger): the terminal QR code drawing and the
10
10
  * `console.log(proto.toJSON())` of the legacy 237 branch; see the README "登录" section.
11
11
  */
12
+ import { inspect } from 'node:util';
13
+ import { LOG_CONTENT_MAX_LENGTH, truncatePreview } from '@zhin.js/logger';
12
14
  const LEVELS = ['trace', 'debug', 'info', 'warn', 'error', 'fatal', 'mark'];
13
15
  const REDACTED = Symbol('icqq.redacted-logger');
14
16
  /** Links keep scheme, host and path; query and fragment become `…`. Phone numbers become `***`. */
@@ -42,3 +44,74 @@ export function redactingIcqqLogger(original) {
42
44
  Object.defineProperty(wrapper, REDACTED, { value: true });
43
45
  return wrapper;
44
46
  }
47
+ /** ICQQ SDK log levels from most to least verbose (`off` silences it). */
48
+ export const ICQQ_SDK_LOG_LEVELS = ['trace', 'debug', 'info', 'warn', 'error', 'fatal', 'mark', 'off'];
49
+ /** Default `logLevel`: the SDK's warnings and errors, not its per-message `info` lines (#1250). */
50
+ export const DEFAULT_ICQQ_SDK_LOG_LEVEL = 'warn';
51
+ export function isIcqqSdkLogLevel(value) {
52
+ return typeof value === 'string' && ICQQ_SDK_LOG_LEVELS.includes(value);
53
+ }
54
+ /**
55
+ * The SDK's per-message lines (`recv from: [Group: …] <full text>`, `succeed to send: […] …`) carry
56
+ * chat content: they are logged at `debug`, whatever level the SDK used.
57
+ */
58
+ const MESSAGE_DUMP = /^\s*(?:recv from|succeed to send|send to)\b/iu;
59
+ /** Longest SDK diagnostic kept on one line (message dumps use the shorter content cap). */
60
+ const SDK_LINE_MAX_LENGTH = 2000;
61
+ function sdkArgText(value) {
62
+ if (typeof value === 'string')
63
+ return value;
64
+ if (value instanceof Error)
65
+ return value.stack ?? value.message;
66
+ try {
67
+ return inspect(value, { depth: 3, breakLength: Infinity });
68
+ }
69
+ catch {
70
+ return String(value);
71
+ }
72
+ }
73
+ const BRIDGED = Symbol('icqq.bridged-logger');
74
+ /**
75
+ * Replaces the SDK's log4js logger with one that writes through the Zhin logger (#1250): one log
76
+ * format, the Zhin logger's colour handling (no ANSI when the output is not a terminal), and
77
+ * - `level` (the SDK's `log_level`, default {@link DEFAULT_ICQQ_SDK_LOG_LEVEL}) filters first;
78
+ * - message dumps go to `debug`, so chat text is not written to disk at the default level;
79
+ * - links and phone numbers are redacted ({@link redactIcqqLogText}), line breaks and control
80
+ * characters escaped and the line capped ({@link truncatePreview}), so a multi-line message
81
+ * cannot forge log lines.
82
+ */
83
+ export function bridgeIcqqSdkLogger(target, level = DEFAULT_ICQQ_SDK_LOG_LEVEL) {
84
+ let current = level;
85
+ const enabled = (at) => current !== 'off' && at >= ICQQ_SDK_LOG_LEVELS.indexOf(current);
86
+ const bridge = Object.create(null);
87
+ for (const sdkLevel of LEVELS) {
88
+ bridge[sdkLevel] = (...args) => {
89
+ const text = redactIcqqLogText(args.map(sdkArgText).join(' '));
90
+ const dump = MESSAGE_DUMP.test(text);
91
+ if (!enabled(dump ? ICQQ_SDK_LOG_LEVELS.indexOf('debug') : ICQQ_SDK_LOG_LEVELS.indexOf(sdkLevel)))
92
+ return;
93
+ const line = truncatePreview(text, dump ? LOG_CONTENT_MAX_LENGTH : SDK_LINE_MAX_LENGTH);
94
+ if (dump || sdkLevel === 'trace' || sdkLevel === 'debug')
95
+ target.debug(line);
96
+ else if (sdkLevel === 'info' || sdkLevel === 'mark')
97
+ target.info(line);
98
+ else if (sdkLevel === 'warn')
99
+ target.warn(line);
100
+ else
101
+ target.error(line);
102
+ };
103
+ }
104
+ Object.defineProperty(bridge, 'level', {
105
+ get: () => current,
106
+ // `client.log_level = …` writes here; an unknown value keeps the current level.
107
+ set: (value) => { if (isIcqqSdkLogLevel(value))
108
+ current = value; },
109
+ enumerable: true,
110
+ });
111
+ Object.defineProperty(bridge, BRIDGED, { value: true });
112
+ Object.defineProperty(bridge, REDACTED, { value: true });
113
+ return bridge;
114
+ }
115
+ export function isBridgedIcqqSdkLogger(value) {
116
+ return Boolean(value && typeof value === 'object' && value[BRIDGED]);
117
+ }
package/package.json CHANGED
@@ -1,7 +1,17 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-icqq",
3
- "version": "1.1.29",
4
- "description": "Zhin.js ICQQ adapter — direct connection to @icqqjs/icqq",
3
+ "version": "1.2.0",
4
+ "description": "ICQQ adapter: in-process @icqqjs/icqq client for QQ groups, private chats, temporary sessions and guild channels.",
5
+ "keywords": [
6
+ "zhin.js",
7
+ "icqq",
8
+ "qq",
9
+ "tencent"
10
+ ],
11
+ "author": {
12
+ "name": "lc-cn",
13
+ "url": "https://github.com/zhinjs/zhin"
14
+ },
5
15
  "type": "module",
6
16
  "main": "./lib/index.js",
7
17
  "types": "./lib/index.d.ts",
@@ -11,21 +21,6 @@
11
21
  "import": "./lib/index.js"
12
22
  }
13
23
  },
14
- "keywords": [
15
- "zhin",
16
- "zhin.js",
17
- "bot",
18
- "adapter",
19
- "icqq",
20
- "qq",
21
- "tencent",
22
- "chatbot"
23
- ],
24
- "author": {
25
- "name": "lc-cn",
26
- "email": "admin@liucl.cn",
27
- "url": "https://github.com/lc-cn"
28
- },
29
24
  "license": "MIT",
30
25
  "repository": {
31
26
  "url": "git+https://github.com/zhinjs/zhin.git",
@@ -34,27 +29,27 @@
34
29
  },
35
30
  "dependencies": {
36
31
  "zod": "^4.6.7",
37
- "@zhin.js/adapter": "1.1.34",
38
- "@zhin.js/agent-feature": "1.1.18",
39
- "@zhin.js/adapter-onebot-events": "0.0.8",
40
- "@zhin.js/core": "1.7.0",
41
- "@zhin.js/console-contract": "1.1.6",
42
- "@zhin.js/im-contract": "1.1.14",
43
- "@zhin.js/feature-kit": "1.1.17",
44
- "@zhin.js/logger": "1.1.3",
45
- "@zhin.js/page": "1.1.19",
46
- "@zhin.js/tool": "1.1.20",
47
- "@zhin.js/skill": "1.1.18",
48
- "@zhin.js/permission": "1.2.1"
32
+ "@zhin.js/adapter": "1.3.1",
33
+ "@zhin.js/adapter-onebot-events": "0.0.10",
34
+ "@zhin.js/agent-feature": "1.1.19",
35
+ "@zhin.js/console-contract": "1.2.0",
36
+ "@zhin.js/core": "1.9.0",
37
+ "@zhin.js/feature-kit": "1.1.18",
38
+ "@zhin.js/im-contract": "1.2.0",
39
+ "@zhin.js/logger": "1.2.0",
40
+ "@zhin.js/page": "1.1.20",
41
+ "@zhin.js/permission": "1.2.2",
42
+ "@zhin.js/skill": "1.1.19",
43
+ "@zhin.js/tool": "1.1.21"
49
44
  },
50
45
  "peerDependencies": {
51
46
  "@icqqjs/icqq": "^1.12.2",
52
- "@zhin.js/adapter": "^1.1.34",
53
- "@zhin.js/agent": "^1.3.1",
54
- "@zhin.js/command": "^1.2.2",
55
- "@zhin.js/core": "^1.7.0",
56
- "zhin.js": "^1.2.0",
57
- "@zhin.js/prompt-section": "^1.1.17"
47
+ "@zhin.js/adapter": "^1.3.1",
48
+ "@zhin.js/agent": "^1.5.0",
49
+ "@zhin.js/command": "^1.3.0",
50
+ "@zhin.js/core": "^1.9.0",
51
+ "@zhin.js/prompt-section": "^1.1.18",
52
+ "zhin.js": "^1.4.0"
58
53
  },
59
54
  "peerDependenciesMeta": {
60
55
  "@icqqjs/icqq": {
@@ -78,9 +73,9 @@
78
73
  "@types/node": "^26.1.2",
79
74
  "typescript": "^6.0.3",
80
75
  "vitest": "^4.1.10",
81
- "@zhin.js/prompt-section": "1.1.17",
82
- "zhin.js": "1.2.0",
83
- "@zhin.js/agent": "1.3.1"
76
+ "@zhin.js/agent": "1.5.0",
77
+ "@zhin.js/prompt-section": "1.1.18",
78
+ "zhin.js": "1.4.0"
84
79
  },
85
80
  "files": [
86
81
  "adapters",
@@ -140,7 +135,19 @@
140
135
  "optional": true
141
136
  }
142
137
  ],
143
- "plugins": []
138
+ "plugins": [],
139
+ "metadata": {
140
+ "displayName": "ICQQ Adapter",
141
+ "displayNameI18n": {
142
+ "zh-CN": "ICQQ 适配器",
143
+ "en": "ICQQ Adapter"
144
+ },
145
+ "descriptionI18n": {
146
+ "zh-CN": "ICQQ 适配器:进程内直连 @icqqjs/icqq,支持 QQ 群聊、私聊、临时会话与频道消息。",
147
+ "en": "ICQQ adapter: in-process @icqqjs/icqq client for QQ groups, private chats, temporary sessions and guild channels."
148
+ },
149
+ "category": "adapter"
150
+ }
144
151
  },
145
152
  "scripts": {
146
153
  "build": "tsc",
package/plugin.js CHANGED
@@ -6,9 +6,6 @@ import { icqqSensitiveExecRule } from "./lib/sensitive-commands.js";
6
6
  export default definePlugin({
7
7
  name: 'icqq',
8
8
  configUpdate: { featurePaths: [['nickname'], ['profile'], ['endpoints']] },
9
- metadata: {
10
- displayName: 'ICQQ Adapter',
11
- },
12
9
  setup(context) {
13
10
  const disposeExecRule = registerSensitiveExecCommand(icqqSensitiveExecRule);
14
11
  return {
package/schema.json CHANGED
@@ -141,6 +141,26 @@
141
141
  }
142
142
  }
143
143
  },
144
+ "logLevel": {
145
+ "type": "string",
146
+ "enum": [
147
+ "trace",
148
+ "debug",
149
+ "info",
150
+ "warn",
151
+ "error",
152
+ "fatal",
153
+ "mark",
154
+ "off"
155
+ ],
156
+ "default": "warn",
157
+ "description": "ICQQ SDK 自身日志的级别(默认 warn);它逐条打印的收发消息原文(recv from / succeed to send)只在 debug 及以下出现,且转义为单行",
158
+ "x-i18n": {
159
+ "en": {
160
+ "description": "Level of the ICQQ SDK's own log lines (default warn). Its per-message lines with the message text (recv from / succeed to send) only appear at debug or below, escaped onto one line."
161
+ }
162
+ }
163
+ },
144
164
  "endpoints": {
145
165
  "type": "array",
146
166
  "minItems": 1,
@@ -284,6 +304,25 @@
284
304
  }
285
305
  }
286
306
  },
307
+ "logLevel": {
308
+ "type": "string",
309
+ "enum": [
310
+ "trace",
311
+ "debug",
312
+ "info",
313
+ "warn",
314
+ "error",
315
+ "fatal",
316
+ "mark",
317
+ "off"
318
+ ],
319
+ "description": "ICQQ SDK 自身日志的级别",
320
+ "x-i18n": {
321
+ "en": {
322
+ "description": "Level of the ICQQ SDK's own log lines."
323
+ }
324
+ }
325
+ },
287
326
  "commandPrefix": {
288
327
  "type": "string",
289
328
  "description": "本 endpoint 的命令前缀",
package/src/adapter.ts CHANGED
@@ -10,7 +10,7 @@ import type { IcqqClientEventMap } from './client.js';
10
10
  export class IcqqAdapter extends Adapter<IcqqEndpoint, IcqqEndpointConfig, IcqqClientEventMap> {
11
11
  override get configurationIdentityFields(): readonly string[] { return ['id']; }
12
12
  override get configurationUpdateFields(): readonly string[] {
13
- return ['password', 'platform', 'ver', 'dataDir', 'signApiAddr', 'autoReconnect', 'autoServer', 'qqnt', 'ntLogin'];
13
+ return ['password', 'platform', 'ver', 'dataDir', 'signApiAddr', 'autoReconnect', 'autoServer', 'qqnt', 'ntLogin', 'logLevel'];
14
14
  }
15
15
  readonly #context: AdapterContext<IcqqEndpointConfig>;
16
16
  constructor(context: AdapterContext<IcqqEndpointConfig>) { super(); this.#context = context; }
package/src/endpoint.ts CHANGED
@@ -41,7 +41,7 @@ import type { CapabilityId } from 'zhin.js';
41
41
  import type { IcqqLoginControl } from './icqq-login-control.js';
42
42
  import { IcqqLoginDriver, icqqLinkHost, type IcqqChallengeKind } from './icqq-login-assist.js';
43
43
  import { messages as t } from './i18n.js';
44
- import { redactingIcqqLogger, type IcqqSdkLogger } from './sdk-logger.js';
44
+ import { bridgeIcqqSdkLogger } from './sdk-logger.js';
45
45
  import { sanitizeIcqqLoginMessage } from './icqq-login-errors.js';
46
46
  import { IcqqContentResolver } from './content-resolver.js';
47
47
  import {
@@ -116,6 +116,15 @@ const BOUND_EVENTS = [
116
116
  'system.login.auth',
117
117
  ] as const;
118
118
 
119
+ /**
120
+ * Log level of an ICQQ `system.*` event by meaning: coming online and login challenges waiting for
121
+ * the person (QR code, device lock, slider, identity check) are normal progress (`info`); only
122
+ * failures (login errors, going offline) are `warn`, so a routine login stays out of the warnings.
123
+ */
124
+ export function icqqSystemEventLogLevel(type: string): 'info' | 'warn' {
125
+ return type === 'online' || type.startsWith('login.') && type !== 'login.error' ? 'info' : 'warn';
126
+ }
127
+
119
128
  export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
120
129
  readonly [endpointSpec]: EndpointSpecification;
121
130
  readonly #logger: ReturnType<typeof getAdapterLogger>;
@@ -266,11 +275,13 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
266
275
  },
267
276
  },
268
277
  });
269
- // The SDK logs verification links (sid / sig) and the phone at `mark` level: redact them.
270
- if (this.logger) this.logger = redactingIcqqLogger(this.logger as IcqqSdkLogger) as typeof this.logger;
278
+ this.#logger = getAdapterLogger('icqq', options.config.id);
279
+ // The SDK's own log4js output is replaced by a bridge into this Zhin logger (#1250): one format,
280
+ // no ANSI off a terminal, `logLevel` (default warn), chat text only at debug and escaped onto one
281
+ // line, verification links (sid / sig) and phone numbers redacted.
282
+ if (this.logger) this.logger = bridgeIcqqSdkLogger(this.#logger, options.config.logLevel) as unknown as typeof this.logger;
271
283
  this.#contentResolver = new IcqqContentResolver(this);
272
284
  this.#content = this.#contentResolver.port;
273
- this.#logger = getAdapterLogger('icqq', options.config.id);
274
285
  this.#options = options;
275
286
  this.endpointName = options.config.id;
276
287
  this.#login = new IcqqLoginDriver({
@@ -1032,7 +1043,7 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
1032
1043
  #logSystemEvent(type: string, event: unknown, safe?: Record<string, unknown> | null): void {
1033
1044
  const payload = safe === undefined ? serializeIcqqEvent(event) : safe;
1034
1045
  const line = payload;
1035
- this.#logger.warn(formatCompact({
1046
+ this.#logger[icqqSystemEventLogLevel(type)](formatCompact({
1036
1047
  op: `system.${type}`,
1037
1048
  endpoint: this.endpointName,
1038
1049
  ...(line ? { event: JSON.stringify(line) } : {}),
@@ -1434,7 +1445,7 @@ function resolveIcqqGroupReactionTarget(
1434
1445
 
1435
1446
  function resolveNativeClientConfig(config: ResolvedIcqqConfig): NativeIcqqClientConfig {
1436
1447
  return {
1437
- log_level: 'info',
1448
+ log_level: config.logLevel,
1438
1449
  platform: config.platform,
1439
1450
  ...(config.ver ? { ver: config.ver } : {}),
1440
1451
  ...(config.dataDir ? { data_dir: config.dataDir } : {}),
package/src/protocol.ts CHANGED
@@ -4,6 +4,7 @@ import { isMediaRef, type ConversationRef, type PlatformStanding } from '@zhin.j
4
4
  import { resolveAdapterNotifyTarget, type AdapterNotifyTarget, type ResolvedAdapterNotifyTarget } from 'zhin.js/adapter';
5
5
  import type { MessageSegment, SendContent } from "zhin.js";
6
6
  import { resolveIcqqMediaFile } from "./cq-message.js";
7
+ import { DEFAULT_ICQQ_SDK_LOG_LEVEL, ICQQ_SDK_LOG_LEVELS, isIcqqSdkLogLevel, type IcqqSdkLogLevel } from "./sdk-logger.js";
7
8
 
8
9
  export type ActionResult = {
9
10
  id: string;
@@ -248,6 +249,8 @@ export interface IcqqEndpointConfig extends EndpointProfileConfiguration {
248
249
  readonly autoServer?: boolean;
249
250
  readonly qqnt?: boolean;
250
251
  readonly ntLogin?: boolean;
252
+ /** Level of the ICQQ SDK's own log lines (default `warn`; its per-message lines are `debug`). */
253
+ readonly logLevel?: IcqqSdkLogLevel;
251
254
  /** Optional out-of-band outage notification target (usually a QQ adapter instance). */
252
255
  readonly notify?: AdapterNotifyTarget;
253
256
  }
@@ -268,6 +271,7 @@ export interface ResolvedIcqqConfig {
268
271
  readonly autoServer?: boolean;
269
272
  readonly qqnt?: boolean;
270
273
  readonly ntLogin?: boolean;
274
+ readonly logLevel: IcqqSdkLogLevel;
271
275
  readonly notify?: ResolvedAdapterNotifyTarget;
272
276
  }
273
277
 
@@ -321,10 +325,15 @@ export function resolveIcqqConfig(config: IcqqEndpointConfig): ResolvedIcqqConfi
321
325
  const qqnt = config.qqnt;
322
326
  const ntLogin = config.ntLogin;
323
327
  const notify = resolveAdapterNotifyTarget(config.notify);
328
+ if (config.logLevel !== undefined && !isIcqqSdkLogLevel(config.logLevel)) {
329
+ throw new TypeError(`ICQQ logLevel must be one of ${ICQQ_SDK_LOG_LEVELS.join(', ')}`);
330
+ }
331
+ const logLevel = config.logLevel ?? DEFAULT_ICQQ_SDK_LOG_LEVEL;
324
332
  return {
325
333
  context: 'icqq',
326
334
  id,
327
335
  autoReconnect,
336
+ logLevel,
328
337
  ...(outboundMedia ? { outboundMedia } : {}),
329
338
  ...(password ? { password } : {}),
330
339
  ...(platform != null ? { platform: Number(platform) } : {}),
package/src/sdk-logger.ts CHANGED
@@ -9,6 +9,9 @@
9
9
  * Not covered (SDK code that bypasses its logger): the terminal QR code drawing and the
10
10
  * `console.log(proto.toJSON())` of the legacy 237 branch; see the README "登录" section.
11
11
  */
12
+ import { inspect } from 'node:util';
13
+ import { LOG_CONTENT_MAX_LENGTH, truncatePreview } from '@zhin.js/logger';
14
+
12
15
  const LEVELS = ['trace', 'debug', 'info', 'warn', 'error', 'fatal', 'mark'] as const;
13
16
  type Level = typeof LEVELS[number];
14
17
  const REDACTED = Symbol('icqq.redacted-logger');
@@ -55,3 +58,81 @@ export function redactingIcqqLogger<T extends IcqqSdkLogger>(original: T): T {
55
58
  return wrapper as unknown as T;
56
59
  }
57
60
 
61
+
62
+ /** ICQQ SDK log levels from most to least verbose (`off` silences it). */
63
+ export const ICQQ_SDK_LOG_LEVELS = ['trace', 'debug', 'info', 'warn', 'error', 'fatal', 'mark', 'off'] as const;
64
+ export type IcqqSdkLogLevel = typeof ICQQ_SDK_LOG_LEVELS[number];
65
+ /** Default `logLevel`: the SDK's warnings and errors, not its per-message `info` lines (#1250). */
66
+ export const DEFAULT_ICQQ_SDK_LOG_LEVEL: IcqqSdkLogLevel = 'warn';
67
+
68
+ export function isIcqqSdkLogLevel(value: unknown): value is IcqqSdkLogLevel {
69
+ return typeof value === 'string' && (ICQQ_SDK_LOG_LEVELS as readonly string[]).includes(value);
70
+ }
71
+
72
+ /** The part of a Zhin logger the SDK bridge writes to. */
73
+ export interface IcqqSdkLogTarget {
74
+ debug(message: string): unknown;
75
+ info(message: string): unknown;
76
+ warn(message: string): unknown;
77
+ error(message: string): unknown;
78
+ }
79
+
80
+ /**
81
+ * The SDK's per-message lines (`recv from: [Group: …] <full text>`, `succeed to send: […] …`) carry
82
+ * chat content: they are logged at `debug`, whatever level the SDK used.
83
+ */
84
+ const MESSAGE_DUMP = /^\s*(?:recv from|succeed to send|send to)\b/iu;
85
+ /** Longest SDK diagnostic kept on one line (message dumps use the shorter content cap). */
86
+ const SDK_LINE_MAX_LENGTH = 2000;
87
+
88
+ function sdkArgText(value: unknown): string {
89
+ if (typeof value === 'string') return value;
90
+ if (value instanceof Error) return value.stack ?? value.message;
91
+ try {
92
+ return inspect(value, { depth: 3, breakLength: Infinity });
93
+ } catch {
94
+ return String(value);
95
+ }
96
+ }
97
+
98
+ const BRIDGED = Symbol('icqq.bridged-logger');
99
+
100
+ /**
101
+ * Replaces the SDK's log4js logger with one that writes through the Zhin logger (#1250): one log
102
+ * format, the Zhin logger's colour handling (no ANSI when the output is not a terminal), and
103
+ * - `level` (the SDK's `log_level`, default {@link DEFAULT_ICQQ_SDK_LOG_LEVEL}) filters first;
104
+ * - message dumps go to `debug`, so chat text is not written to disk at the default level;
105
+ * - links and phone numbers are redacted ({@link redactIcqqLogText}), line breaks and control
106
+ * characters escaped and the line capped ({@link truncatePreview}), so a multi-line message
107
+ * cannot forge log lines.
108
+ */
109
+ export function bridgeIcqqSdkLogger(target: IcqqSdkLogTarget, level: IcqqSdkLogLevel = DEFAULT_ICQQ_SDK_LOG_LEVEL): IcqqSdkLogger {
110
+ let current: IcqqSdkLogLevel = level;
111
+ const enabled = (at: number): boolean => current !== 'off' && at >= ICQQ_SDK_LOG_LEVELS.indexOf(current);
112
+ const bridge = Object.create(null) as Record<Level, (...args: unknown[]) => void>;
113
+ for (const sdkLevel of LEVELS) {
114
+ bridge[sdkLevel] = (...args: unknown[]) => {
115
+ const text = redactIcqqLogText(args.map(sdkArgText).join(' '));
116
+ const dump = MESSAGE_DUMP.test(text);
117
+ if (!enabled(dump ? ICQQ_SDK_LOG_LEVELS.indexOf('debug') : ICQQ_SDK_LOG_LEVELS.indexOf(sdkLevel))) return;
118
+ const line = truncatePreview(text, dump ? LOG_CONTENT_MAX_LENGTH : SDK_LINE_MAX_LENGTH);
119
+ if (dump || sdkLevel === 'trace' || sdkLevel === 'debug') target.debug(line);
120
+ else if (sdkLevel === 'info' || sdkLevel === 'mark') target.info(line);
121
+ else if (sdkLevel === 'warn') target.warn(line);
122
+ else target.error(line);
123
+ };
124
+ }
125
+ Object.defineProperty(bridge, 'level', {
126
+ get: () => current,
127
+ // `client.log_level = …` writes here; an unknown value keeps the current level.
128
+ set: (value: unknown) => { if (isIcqqSdkLogLevel(value)) current = value; },
129
+ enumerable: true,
130
+ });
131
+ Object.defineProperty(bridge, BRIDGED, { value: true });
132
+ Object.defineProperty(bridge, REDACTED, { value: true });
133
+ return bridge as unknown as IcqqSdkLogger;
134
+ }
135
+
136
+ export function isBridgedIcqqSdkLogger(value: unknown): boolean {
137
+ return Boolean(value && typeof value === 'object' && (value as { [BRIDGED]?: true })[BRIDGED]);
138
+ }