@zhin.js/adapter-icqq 8.0.13 → 8.0.14

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,52 @@
1
1
  # @zhin.js/adapter-icqq
2
2
 
3
+ ## 8.0.14
4
+
5
+ ### Patch Changes
6
+
7
+ - 5969c5b: Remove legacy compound-string message targets and Endpoint control probing. Endpoint control and outbound Host operations now carry structured `MessageRef` identities. Endpoint send has one exact result contract (a platform message id), which IM Runtime projects into `DeliveryReceipt.message`; arbitrary result guessing is removed.
8
+ - 5969c5b: Drop ICQQ unified_inbox dual-write for request/notice; Console request.list prefers EndpointManagement.listRequests, and approval stays on $approve / management.
9
+ - 5969c5b: Wire ICQQ login challenges (QR / slider / SMS / auth) through LoginAssist so pending tasks survive Console refresh; add login.list/submit/cancel RPC and TTY stdin consumer.
10
+ - 7cd8b34: Send canonical outbound segments as native ICQQ `Sendable` elements instead of literal CQ-like text. Resolve inbound voice and video URLs while the endpoint client is active, preserve file metadata and opaque platform references, and reject unsupported mixed standalone media messages instead of silently dropping segments.
11
+ - 5969c5b: Add SideEventGateway so adapters forward notice/request/system into HandlerIndex. HandlerContext now exposes only generation-safe capabilities and prompt ports; live Endpoint escape hatches are removed.
12
+ - 2f786bd: Replace text-only IM context and metadata-based quote handling with canonical
13
+ conversation events, scoped reference resolution, explicit Endpoint content
14
+ ports, and typed multimedia outcomes. Conversation notices are projected as
15
+ untrusted context data rather than model-authority system instructions.
16
+ The process-global passive-group buffer is removed: prior inbound conversation messages
17
+ are now consumed from the same event store and cursor, with the current Turn
18
+ message excluded from its own context projection.
19
+ The unused `ConversationMemory` topic-memory runtime, its timers, legacy tables,
20
+ and topic-window configuration are removed; `ContextRepository` is the only
21
+ Agent conversation-history authority.
22
+ - Updated dependencies [5969c5b]
23
+ - Updated dependencies [d336a3f]
24
+ - Updated dependencies [0c82a7e]
25
+ - Updated dependencies [b9217e4]
26
+ - Updated dependencies [5969c5b]
27
+ - Updated dependencies [5969c5b]
28
+ - Updated dependencies [974772e]
29
+ - Updated dependencies [5969c5b]
30
+ - Updated dependencies [5969c5b]
31
+ - Updated dependencies [2f786bd]
32
+ - Updated dependencies [63d89f9]
33
+ - Updated dependencies [71c7cdd]
34
+ - Updated dependencies [3cca0ea]
35
+ - Updated dependencies [1312ca0]
36
+ - Updated dependencies [985fa22]
37
+ - Updated dependencies [04b861d]
38
+ - Updated dependencies [a23d544]
39
+ - Updated dependencies [8cddabf]
40
+ - Updated dependencies [dbe5081]
41
+ - @zhin.js/im-contract@1.0.4
42
+ - @zhin.js/core@1.5.12
43
+ - @zhin.js/adapter@1.1.11
44
+ - @zhin.js/agent@1.1.14
45
+ - @zhin.js/command@1.0.15
46
+ - zhin.js@6.0.12
47
+ - @zhin.js/permission@1.0.3
48
+ - @zhin.js/tool@1.0.12
49
+
3
50
  ## 8.0.13
4
51
 
5
52
  ### Patch Changes
package/README.md CHANGED
@@ -5,8 +5,8 @@ ICQQ Plugin Runtime 适配器 — 进程内直接使用 [@icqqjs/icqq](https://g
5
5
  ## 功能特性
6
6
 
7
7
  - 群聊 / 私聊 / 群临时会话 / QQ 频道消息
8
- - 入站:`messageGatewayToken`,归一为 `gateway.receive({ conversation, message, content, ... })`
9
- - 出站:`send({ conversation, payload })` → `sendGroupMsg` / `sendPrivateMsg` / …(conversation kind/id/parent 路由)
8
+ - 入站:`messageGatewayToken`,文本与图片 / 语音 / 视频 / 文件统一归一为 canonical `Segment` + `MediaRef`
9
+ - 出站:canonical Segment 直接投影为 ICQQ 原生 `Sendable`,再由 `sendGroupMsg` / `sendPrivateMsg` / …发送
10
10
  - 群聊 reaction:`control.addReaction` / `removeReaction`(协议 ACK 失败不阻塞后续发送)
11
11
  - Agent 工具:包根 `tools/`(`@zhin.js/tool` Feature;模型侧名为 `icqq__send_user_like` 等)
12
12
  - Console Endpoint 管理:`src/endpoint.ts` 显式实现 `EndpointManagement`(好友/群/群成员列表、请求审批、删好友、踢人、禁言、设管理)
@@ -76,9 +76,13 @@ plugins:
76
76
 
77
77
  - 不再经过 `@icqqjs/cli` IPC 守护进程;登录态与协议栈都在本进程。
78
78
  - `autoReconnect`:Client 断线后按配置自动重连(`stop()` 为主动断开,不触发重连)。
79
- - `outboundMedia: file | base64`:`file` 把 segment base64 落盘后发本地路径;`base64` 发 CQ `base64://` 内联。
79
+ - `outboundMedia: file | base64`:`file` 在发送期间把 segment base64 物化为临时文件并于发送结束清理;`base64` 使用 ICQQ 原生支持的 `base64://` file 参数。
80
+ - ICQQ 的语音、视频、文件是独立消息元素;它们与其他段混发时适配器会明确拒绝,避免协议栈静默丢段。
81
+ - 入站语音 / 视频在 Endpoint 持有原生 Client 时解析可下载 URL;解析失败则保留真实平台引用,不伪造或丢弃媒体。
80
82
  - **Console 社交/群管 RPC 已接线**:endpoint 把好友/群/群成员列表、请求审批和群管操作归一化为冻结的 `EndpointManagement`。Host 只消费该语义端口。
81
- - 好友/入群请求与通知写入 unified inbox(有 DatabaseHost 时),不再走已删除的 `notice.receive` / `request.receive` 事件。
83
+ - 好友/入群请求与通知:经 `sideEventGatewayToken` 分发到 `handlers`(`notice.receive` / `request.receive`);审批走 `Request.$approve` / `EndpointManagement.approveRequest`。Console `request.list` 优先读 `management.listRequests()`(`getSystemMsg`),不再写入 `unified_inbox_request/notice`。
84
+ - `system.*`(登录扫码等)分发到 `system.receive`。
85
+ - **登录辅助**:`system.login.qrcode|slider|device|auth` 经 `loginAssistToken`(`LoginAssist`)挂起待办;刷新后可用 Console `login.list` / `login.submit` 或终端 stdin 继续(对齐 icqq 官方 stdin 流程)。`system.online` / `login.error` 会清理该 endpoint 待办。
82
86
 
83
87
  ## License
84
88
 
package/adapters/icqq.js CHANGED
@@ -3,14 +3,11 @@
3
3
  * Convention entry: discover `adapters/icqq.ts` → defineAdapter.
4
4
  */
5
5
  import { defineAdapter } from 'zhin.js/adapter';
6
- import { messageGatewayToken } from '@zhin.js/core/runtime';
7
- import { formatCompact, getLogger } from '@zhin.js/logger';
8
- import { databaseHostToken, INBOX_TABLE_NOTICE, INBOX_TABLE_REQUEST, runtimeEventPublisherToken, } from 'zhin.js';
6
+ import { loginAssistToken, messageGatewayToken, sideEventGatewayToken, } from '@zhin.js/core/runtime';
9
7
  import { IcqqEndpoint } from "../lib/endpoint.js";
10
8
  import { icqqRuntimeStateToken } from "../lib/icqq-runtime-state.js";
11
9
  import { resolveIcqqConfig, } from "../lib/protocol.js";
12
10
  export { IcqqEndpoint } from "../lib/endpoint.js";
13
- const logger = getLogger('icqq');
14
11
  export default defineAdapter({
15
12
  capabilities: ['inbound', 'outbound'],
16
13
  operations: ['recall', 'reaction'],
@@ -25,61 +22,12 @@ export default defineAdapter({
25
22
  id: config.id,
26
23
  mode: 'direct',
27
24
  });
28
- const inbox = resolveInboxHooks(context);
29
25
  return new IcqqEndpoint({
30
26
  id: context.id,
31
27
  gateway: context.use(messageGatewayToken),
28
+ sideEvents: context.use(sideEventGatewayToken),
29
+ loginAssist: context.use(loginAssistToken),
32
30
  config,
33
- ...(inbox ? { inbox } : {}),
34
31
  });
35
32
  },
36
33
  });
37
- /** 资源可选(轻量 Root 无 DatabaseHost / console hub):缺失时降级为不写收件箱。 */
38
- function tryUse(context, token) {
39
- try {
40
- return context.use(token);
41
- }
42
- catch {
43
- return undefined;
44
- }
45
- }
46
- /**
47
- * 收件箱钩子:写 unified_inbox_request/notice(按 platform_*_id 去重,
48
- * 推送事件与 GET_SYSTEM_MSG 首拉可能重复同一请求),并经 console hub 广播。
49
- */
50
- function resolveInboxHooks(context) {
51
- const host = tryUse(context, databaseHostToken);
52
- const publisher = tryUse(context, runtimeEventPublisherToken);
53
- if (!host && !publisher)
54
- return undefined;
55
- const insertDeduped = async (table, idField, row) => {
56
- const model = host?.models.get(table);
57
- if (!model)
58
- return;
59
- try {
60
- const existing = await model.select().where({
61
- adapter: row.adapter,
62
- endpoint_id: row.endpoint_id,
63
- [idField]: row[idField],
64
- });
65
- if (Array.isArray(existing) && existing.length > 0)
66
- return;
67
- await model.insert(row);
68
- }
69
- catch (error) {
70
- logger.warn(formatCompact({
71
- op: 'inbox_insert',
72
- table,
73
- error: error instanceof Error ? error.message : String(error),
74
- }));
75
- }
76
- };
77
- const publish = publisher;
78
- return {
79
- recordRequest: (row) => insertDeduped(INBOX_TABLE_REQUEST, 'platform_request_id', row),
80
- recordNotice: (row) => insertDeduped(INBOX_TABLE_NOTICE, 'platform_notice_id', row),
81
- ...(publish
82
- ? { publish: (type, data) => publish.publish(type, data) }
83
- : {}),
84
- };
85
- }
package/adapters/icqq.ts CHANGED
@@ -2,16 +2,12 @@
2
2
  * Convention entry: discover `adapters/icqq.ts` → defineAdapter.
3
3
  */
4
4
  import { defineAdapter, type AdapterContext } from 'zhin.js/adapter';
5
- import { messageGatewayToken } from '@zhin.js/core/runtime';
6
- import { formatCompact, getLogger } from '@zhin.js/logger';
7
5
  import {
8
- databaseHostToken,
9
- INBOX_TABLE_NOTICE,
10
- INBOX_TABLE_REQUEST,
11
- runtimeEventPublisherToken,
12
- type Token,
13
- } from 'zhin.js';
14
- import { IcqqEndpoint, type IcqqInboxHooks } from '../src/endpoint.js';
6
+ loginAssistToken,
7
+ messageGatewayToken,
8
+ sideEventGatewayToken,
9
+ } from '@zhin.js/core/runtime';
10
+ import { IcqqEndpoint } from '../src/endpoint.js';
15
11
  import { icqqRuntimeStateToken } from '../src/icqq-runtime-state.js';
16
12
  import {
17
13
  resolveIcqqConfig,
@@ -21,7 +17,11 @@ import {
21
17
  export { IcqqEndpoint } from '../src/endpoint.js';
22
18
  export type { IcqqEndpointOptions } from '../src/endpoint.js';
23
19
 
24
- const logger = getLogger('icqq');
20
+ declare module '@zhin.js/core' {
21
+ interface AdapterEndpoints {
22
+ icqq: import('../src/endpoint.js').IcqqEndpoint;
23
+ }
24
+ }
25
25
 
26
26
  export default defineAdapter<IcqqAdapterConfig>({
27
27
  capabilities: ['inbound', 'outbound'],
@@ -37,62 +37,12 @@ export default defineAdapter<IcqqAdapterConfig>({
37
37
  id: config.id,
38
38
  mode: 'direct',
39
39
  });
40
- const inbox = resolveInboxHooks(context);
41
40
  return new IcqqEndpoint({
42
41
  id: context.id,
43
42
  gateway: context.use(messageGatewayToken),
43
+ sideEvents: context.use(sideEventGatewayToken),
44
+ loginAssist: context.use(loginAssistToken),
44
45
  config,
45
- ...(inbox ? { inbox } : {}),
46
46
  });
47
47
  },
48
48
  });
49
-
50
- /** 资源可选(轻量 Root 无 DatabaseHost / console hub):缺失时降级为不写收件箱。 */
51
- function tryUse<T>(context: AdapterContext<IcqqAdapterConfig>, token: Token<T>): T | undefined {
52
- try {
53
- return context.use(token);
54
- } catch {
55
- return undefined;
56
- }
57
- }
58
-
59
- /**
60
- * 收件箱钩子:写 unified_inbox_request/notice(按 platform_*_id 去重,
61
- * 推送事件与 GET_SYSTEM_MSG 首拉可能重复同一请求),并经 console hub 广播。
62
- */
63
- function resolveInboxHooks(context: AdapterContext<IcqqAdapterConfig>): IcqqInboxHooks | undefined {
64
- const host = tryUse(context, databaseHostToken);
65
- const publisher = tryUse(context, runtimeEventPublisherToken);
66
- if (!host && !publisher) return undefined;
67
- const insertDeduped = async (
68
- table: string,
69
- idField: 'platform_request_id' | 'platform_notice_id',
70
- row: Record<string, unknown>,
71
- ): Promise<void> => {
72
- const model = host?.models.get(table);
73
- if (!model) return;
74
- try {
75
- const existing = await model.select().where({
76
- adapter: row.adapter,
77
- endpoint_id: row.endpoint_id,
78
- [idField]: row[idField],
79
- });
80
- if (Array.isArray(existing) && existing.length > 0) return;
81
- await model.insert(row);
82
- } catch (error) {
83
- logger.warn(formatCompact({
84
- op: 'inbox_insert',
85
- table,
86
- error: error instanceof Error ? error.message : String(error),
87
- }));
88
- }
89
- };
90
- const publish = publisher;
91
- return {
92
- recordRequest: (row) => insertDeduped(INBOX_TABLE_REQUEST, 'platform_request_id', row),
93
- recordNotice: (row) => insertDeduped(INBOX_TABLE_NOTICE, 'platform_notice_id', row),
94
- ...(publish
95
- ? { publish: (type: string, data: unknown): void => publish.publish(type, data) }
96
- : {}),
97
- };
98
- }
@@ -1,22 +1,14 @@
1
- /**
2
- * CQ 码与 MessageSegment 互转。
3
- * 出站:@icqqjs/cli 要求 message 为非空字符串;引用段编码为 [reply:id](需 cli parse-message 支持 reply,见 scripts/patch-icqq-cli-reply.mjs)。
4
- * 媒体段双向均为 canonical MediaRef(`data.media`),无 legacy url/file/base64 字段。
5
- */
1
+ /** ICQQ 历史文本视图 → canonical MessageSegment。出站只使用原生 Sendable。 */
6
2
  import { type MediaRef } from "@zhin.js/core";
7
- import { MessageSegment, SendContent } from "zhin.js";
3
+ import type { MessageSegment } from "zhin.js";
8
4
  /**
9
5
  * CQ 媒体参数 / 入站元素媒体值 → canonical MediaRef:
10
6
  * `base64://` → base64;http(s) → url;本机路径 → path;其余视为平台不透明引用(file)。
11
7
  */
12
8
  export declare function icqqMediaRefFromString(value: string): MediaRef;
13
9
  /**
14
- * canonical MediaRef(`data.media`,唯一媒体来源)→ CQ 媒体参数:
15
- * base64 → `base64://` 前缀(守护进程解码);url/path/file 原样直出。
16
- * 无 canonical 媒体引用时 warn 并返回 undefined(调用方丢弃该段)。
10
+ * canonical MediaRef(`data.media`,唯一媒体来源)→ ICQQ native
11
+ * element 的 file 参数。base64 使用 ICQQ 原生支持的 `base64://` 前缀。
17
12
  */
18
- export declare function resolveCqMediaArg(type: string, data: Record<string, unknown> | undefined): string | undefined;
13
+ export declare function resolveIcqqMediaFile(data: Record<string, unknown> | undefined): string | undefined;
19
14
  export declare function parseCqMessage(raw: string): MessageSegment[];
20
- /** 构建 icqq send_*_msg 的 message 字符串(非空) */
21
- export declare function buildIcqqMessage(content: SendContent): string;
22
- export declare function toCqString(content: SendContent): string;
package/lib/cq-message.js CHANGED
@@ -1,12 +1,5 @@
1
- /**
2
- * CQ 码与 MessageSegment 互转。
3
- * 出站:@icqqjs/cli 要求 message 为非空字符串;引用段编码为 [reply:id](需 cli parse-message 支持 reply,见 scripts/patch-icqq-cli-reply.mjs)。
4
- * 媒体段双向均为 canonical MediaRef(`data.media`),无 legacy url/file/base64 字段。
5
- */
1
+ /** ICQQ 历史文本视图 → canonical MessageSegment。出站只使用原生 Sendable。 */
6
2
  import { isMediaRef } from "@zhin.js/core";
7
- import { formatCompact, getLogger } from "@zhin.js/logger";
8
- import { segment } from "zhin.js";
9
- const logger = getLogger("icqq");
10
3
  const MAX_CQ_PARSE_LEN = 256_000;
11
4
  /**
12
5
  * CQ 媒体参数 / 入站元素媒体值 → canonical MediaRef:
@@ -25,20 +18,13 @@ export function icqqMediaRefFromString(value) {
25
18
  return { kind: "file", value };
26
19
  }
27
20
  /**
28
- * canonical MediaRef(`data.media`,唯一媒体来源)→ CQ 媒体参数:
29
- * base64 → `base64://` 前缀(守护进程解码);url/path/file 原样直出。
30
- * 无 canonical 媒体引用时 warn 并返回 undefined(调用方丢弃该段)。
21
+ * canonical MediaRef(`data.media`,唯一媒体来源)→ ICQQ native
22
+ * element 的 file 参数。base64 使用 ICQQ 原生支持的 `base64://` 前缀。
31
23
  */
32
- export function resolveCqMediaArg(type, data) {
24
+ export function resolveIcqqMediaFile(data) {
33
25
  const media = data?.media;
34
- if (!isMediaRef(media)) {
35
- logger.warn(formatCompact({
36
- op: "icqq_outbound_media_dropped",
37
- type,
38
- reason: "missing_media_ref",
39
- }));
26
+ if (!isMediaRef(media))
40
27
  return undefined;
41
- }
42
28
  if (media.kind === "base64") {
43
29
  return media.value.startsWith("base64://") ? media.value : `base64://${media.value}`;
44
30
  }
@@ -68,7 +54,7 @@ function pushCqSegment(segments, type, arg) {
68
54
  break;
69
55
  case "record":
70
56
  case "audio":
71
- segments.push({ type: "record", data: { media: icqqMediaRefFromString(arg) } });
57
+ segments.push({ type: "audio", data: { media: icqqMediaRefFromString(arg) } });
72
58
  break;
73
59
  case "video":
74
60
  segments.push({ type: "video", data: { media: icqqMediaRefFromString(arg) } });
@@ -118,55 +104,3 @@ export function parseCqMessage(raw) {
118
104
  }
119
105
  return segments.length ? segments : [{ type: "text", data: { text: raw } }];
120
106
  }
121
- /** 构建 icqq send_*_msg 的 message 字符串(非空) */
122
- export function buildIcqqMessage(content) {
123
- let message = toCqString(content).trim();
124
- if (!message)
125
- message = "\u200b";
126
- return message;
127
- }
128
- export function toCqString(content) {
129
- if (!Array.isArray(content))
130
- content = [content];
131
- return content
132
- .map((seg) => {
133
- if (typeof seg === "string")
134
- return seg;
135
- const { type, data } = seg;
136
- switch (type) {
137
- case "text":
138
- return data.text ?? "";
139
- case "face":
140
- return `[face:${data.id}]`;
141
- case "image": {
142
- const arg = resolveCqMediaArg("image", data);
143
- return arg ? `[image:${arg}]` : "";
144
- }
145
- case "at":
146
- case "mention": {
147
- const target = data.target ?? data.qq ?? data.id ?? data.user_id;
148
- return `[at:${target}]`;
149
- }
150
- case "dice":
151
- return "[dice]";
152
- case "rps":
153
- return "[rps]";
154
- case "record":
155
- case "audio": {
156
- const arg = resolveCqMediaArg("record", data);
157
- return arg ? `[record:${arg}]` : "";
158
- }
159
- case "video": {
160
- const arg = resolveCqMediaArg("video", data);
161
- return arg ? `[video:${arg}]` : "";
162
- }
163
- case "reply":
164
- return `[reply:${data.message_id ?? data.id}]`;
165
- case "json":
166
- return `[json:${data.text ?? ''}]`;
167
- default:
168
- return segment.toString(seg);
169
- }
170
- })
171
- .join("");
172
- }
package/lib/endpoint.d.ts CHANGED
@@ -1,29 +1,28 @@
1
1
  import { Client } from '@icqqjs/icqq';
2
2
  import type { EndpointControl, EndpointInstance, EndpointManagement, EndpointSendRequest } from 'zhin.js/adapter';
3
- import type { MessageGateway } from '@zhin.js/core/runtime';
3
+ import type { EndpointContentPort } from '@zhin.js/adapter';
4
+ import type { MessageGateway, SideEventGateway } from '@zhin.js/core/runtime';
5
+ import { type LoginAssist } from '@zhin.js/core';
4
6
  import type { CapabilityId } from 'zhin.js';
5
7
  import { type IcqqInboundMessage, type ResolvedIcqqConfig } from './protocol.js';
6
- export interface IcqqInboxHooks {
7
- recordRequest(row: Record<string, unknown>): void | Promise<void>;
8
- recordNotice(row: Record<string, unknown>): void | Promise<void>;
9
- publish?(type: string, data: unknown): void;
10
- }
11
8
  export interface IcqqEndpointOptions {
12
9
  readonly id: CapabilityId;
13
10
  readonly gateway: MessageGateway;
14
11
  readonly config: ResolvedIcqqConfig;
15
- readonly inbox?: IcqqInboxHooks;
12
+ readonly sideEvents: SideEventGateway;
13
+ readonly loginAssist: LoginAssist;
16
14
  }
17
15
  export declare class IcqqEndpoint extends Client implements EndpointInstance {
18
16
  #private;
19
17
  readonly endpointName: string;
18
+ readonly content: EndpointContentPort;
20
19
  readonly management: EndpointManagement;
21
20
  readonly control: EndpointControl;
22
21
  constructor(options: IcqqEndpointOptions);
23
- start(): Promise<void>;
22
+ start(signal: AbortSignal): Promise<void>;
24
23
  open(): void;
25
- close(): void;
24
+ close(): Promise<void>;
26
25
  stop(): Promise<void>;
27
26
  send({ conversation, payload }: EndpointSendRequest): Promise<string>;
28
- admit(msg: IcqqInboundMessage): void;
27
+ admit(msg: IcqqInboundMessage, admittedBeforeRetire?: boolean): void;
29
28
  }