@zhin.js/adapter-icqq 1.1.30 → 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,32 @@
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
+
3
30
  ## 1.1.30
4
31
 
5
32
  ### 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.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';
@@ -198,12 +198,14 @@ export class IcqqEndpoint extends ManagedIcqqClient {
198
198
  },
199
199
  },
200
200
  });
201
- // 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.
202
205
  if (this.logger)
203
- this.logger = redactingIcqqLogger(this.logger);
206
+ this.logger = bridgeIcqqSdkLogger(this.#logger, options.config.logLevel);
204
207
  this.#contentResolver = new IcqqContentResolver(this);
205
208
  this.#content = this.#contentResolver.port;
206
- this.#logger = getAdapterLogger('icqq', options.config.id);
207
209
  this.#options = options;
208
210
  this.endpointName = options.config.id;
209
211
  this.#login = new IcqqLoginDriver({
@@ -1378,7 +1380,7 @@ function resolveIcqqGroupReactionTarget(message) {
1378
1380
  }
1379
1381
  function resolveNativeClientConfig(config) {
1380
1382
  return {
1381
- log_level: 'info',
1383
+ log_level: config.logLevel,
1382
1384
  platform: config.platform,
1383
1385
  ...(config.ver ? { ver: config.ver } : {}),
1384
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-icqq",
3
- "version": "1.1.30",
3
+ "version": "1.2.0",
4
4
  "description": "ICQQ adapter: in-process @icqqjs/icqq client for QQ groups, private chats, temporary sessions and guild channels.",
5
5
  "keywords": [
6
6
  "zhin.js",
@@ -29,14 +29,14 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "zod": "^4.6.7",
32
- "@zhin.js/adapter": "1.3.0",
33
- "@zhin.js/adapter-onebot-events": "0.0.9",
32
+ "@zhin.js/adapter": "1.3.1",
33
+ "@zhin.js/adapter-onebot-events": "0.0.10",
34
34
  "@zhin.js/agent-feature": "1.1.19",
35
35
  "@zhin.js/console-contract": "1.2.0",
36
- "@zhin.js/core": "1.8.0",
36
+ "@zhin.js/core": "1.9.0",
37
37
  "@zhin.js/feature-kit": "1.1.18",
38
38
  "@zhin.js/im-contract": "1.2.0",
39
- "@zhin.js/logger": "1.1.3",
39
+ "@zhin.js/logger": "1.2.0",
40
40
  "@zhin.js/page": "1.1.20",
41
41
  "@zhin.js/permission": "1.2.2",
42
42
  "@zhin.js/skill": "1.1.19",
@@ -44,12 +44,12 @@
44
44
  },
45
45
  "peerDependencies": {
46
46
  "@icqqjs/icqq": "^1.12.2",
47
- "@zhin.js/adapter": "^1.3.0",
48
- "@zhin.js/agent": "^1.4.0",
47
+ "@zhin.js/adapter": "^1.3.1",
48
+ "@zhin.js/agent": "^1.5.0",
49
49
  "@zhin.js/command": "^1.3.0",
50
- "@zhin.js/core": "^1.8.0",
50
+ "@zhin.js/core": "^1.9.0",
51
51
  "@zhin.js/prompt-section": "^1.1.18",
52
- "zhin.js": "^1.3.0"
52
+ "zhin.js": "^1.4.0"
53
53
  },
54
54
  "peerDependenciesMeta": {
55
55
  "@icqqjs/icqq": {
@@ -73,9 +73,9 @@
73
73
  "@types/node": "^26.1.2",
74
74
  "typescript": "^6.0.3",
75
75
  "vitest": "^4.1.10",
76
- "@zhin.js/agent": "1.4.0",
76
+ "@zhin.js/agent": "1.5.0",
77
77
  "@zhin.js/prompt-section": "1.1.18",
78
- "zhin.js": "1.3.0"
78
+ "zhin.js": "1.4.0"
79
79
  },
80
80
  "files": [
81
81
  "adapters",
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 {
@@ -275,11 +275,13 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
275
275
  },
276
276
  },
277
277
  });
278
- // The SDK logs verification links (sid / sig) and the phone at `mark` level: redact them.
279
- 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;
280
283
  this.#contentResolver = new IcqqContentResolver(this);
281
284
  this.#content = this.#contentResolver.port;
282
- this.#logger = getAdapterLogger('icqq', options.config.id);
283
285
  this.#options = options;
284
286
  this.endpointName = options.config.id;
285
287
  this.#login = new IcqqLoginDriver({
@@ -1443,7 +1445,7 @@ function resolveIcqqGroupReactionTarget(
1443
1445
 
1444
1446
  function resolveNativeClientConfig(config: ResolvedIcqqConfig): NativeIcqqClientConfig {
1445
1447
  return {
1446
- log_level: 'info',
1448
+ log_level: config.logLevel,
1447
1449
  platform: config.platform,
1448
1450
  ...(config.ver ? { ver: config.ver } : {}),
1449
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
+ }