@zhin.js/adapter-icqq 6.0.2 → 7.0.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.
Files changed (125) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +48 -435
  3. package/adapters/icqq.ts +92 -0
  4. package/agent/tools/announce.ts +2 -2
  5. package/agent/tools/essence.ts +2 -2
  6. package/agent/tools/friend_list.ts +2 -2
  7. package/agent/tools/group_files.ts +2 -2
  8. package/agent/tools/group_invite.ts +2 -2
  9. package/agent/tools/group_list.ts +2 -2
  10. package/agent/tools/group_sign.ts +2 -2
  11. package/agent/tools/list_muted.ts +2 -2
  12. package/agent/tools/poke.ts +2 -2
  13. package/agent/tools/send_user_like.ts +2 -2
  14. package/agent/tools/set_anonymous.ts +2 -2
  15. package/agent/tools/set_title.ts +2 -2
  16. package/lib/agent-prompt.d.ts +2 -0
  17. package/lib/{src/agent-prompt.js → agent-prompt.js} +0 -1
  18. package/lib/cq-message.d.ts +9 -0
  19. package/lib/{src/cq-message.js → cq-message.js} +0 -1
  20. package/lib/endpoint.d.ts +101 -0
  21. package/lib/endpoint.js +715 -0
  22. package/lib/forward-msg.d.ts +26 -0
  23. package/lib/{src/forward-msg.js → forward-msg.js} +0 -1
  24. package/lib/icqq-agent-deps.d.ts +25 -0
  25. package/lib/icqq-agent-deps.js +32 -0
  26. package/lib/icqq-guild.d.ts +53 -0
  27. package/lib/{src/icqq-guild.js → icqq-guild.js} +4 -2
  28. package/lib/icqq-inbound.d.ts +139 -0
  29. package/lib/{src/icqq-inbound.js → icqq-inbound.js} +27 -3
  30. package/lib/icqq-inbox.d.ts +33 -0
  31. package/lib/icqq-inbox.js +129 -0
  32. package/lib/index.d.ts +6 -0
  33. package/lib/index.js +6 -0
  34. package/lib/ipc-client.d.ts +73 -0
  35. package/lib/{src/ipc-client.js → ipc-client.js} +0 -1
  36. package/lib/outbound-media.d.ts +13 -0
  37. package/lib/{src/outbound-media.js → outbound-media.js} +0 -1
  38. package/lib/protocol.d.ts +269 -0
  39. package/lib/{src/protocol.js → protocol.js} +124 -2
  40. package/lib/types.d.ts +87 -0
  41. package/lib/{src/types.js → types.js} +0 -1
  42. package/package.json +49 -45
  43. package/plugin.ts +32 -0
  44. package/schema.json +104 -0
  45. package/src/endpoint.ts +704 -795
  46. package/src/icqq-agent-deps.ts +48 -9
  47. package/src/icqq-guild.ts +10 -4
  48. package/src/icqq-inbound.ts +31 -2
  49. package/src/icqq-inbox.ts +155 -0
  50. package/src/index.ts +36 -69
  51. package/src/protocol.ts +191 -1
  52. package/src/types.ts +16 -0
  53. package/client/ICQQManagement.tsx +0 -147
  54. package/client/LoginAssistPanel.tsx +0 -300
  55. package/client/index.tsx +0 -11
  56. package/client/tsconfig.json +0 -7
  57. package/client/utils/api.ts +0 -30
  58. package/dist/index.js +0 -34
  59. package/lib/agent/tools/announce.js +0 -21
  60. package/lib/agent/tools/announce.js.map +0 -1
  61. package/lib/agent/tools/essence.js +0 -22
  62. package/lib/agent/tools/essence.js.map +0 -1
  63. package/lib/agent/tools/friend_list.js +0 -19
  64. package/lib/agent/tools/friend_list.js.map +0 -1
  65. package/lib/agent/tools/group_files.js +0 -29
  66. package/lib/agent/tools/group_files.js.map +0 -1
  67. package/lib/agent/tools/group_invite.js +0 -20
  68. package/lib/agent/tools/group_invite.js.map +0 -1
  69. package/lib/agent/tools/group_list.js +0 -20
  70. package/lib/agent/tools/group_list.js.map +0 -1
  71. package/lib/agent/tools/group_sign.js +0 -19
  72. package/lib/agent/tools/group_sign.js.map +0 -1
  73. package/lib/agent/tools/list_muted.js +0 -23
  74. package/lib/agent/tools/list_muted.js.map +0 -1
  75. package/lib/agent/tools/poke.js +0 -20
  76. package/lib/agent/tools/poke.js.map +0 -1
  77. package/lib/agent/tools/send_user_like.js +0 -22
  78. package/lib/agent/tools/send_user_like.js.map +0 -1
  79. package/lib/agent/tools/set_anonymous.js +0 -22
  80. package/lib/agent/tools/set_anonymous.js.map +0 -1
  81. package/lib/agent/tools/set_title.js +0 -25
  82. package/lib/agent/tools/set_title.js.map +0 -1
  83. package/lib/src/adapter.js +0 -128
  84. package/lib/src/adapter.js.map +0 -1
  85. package/lib/src/agent-prompt.js.map +0 -1
  86. package/lib/src/cq-message.js.map +0 -1
  87. package/lib/src/endpoint.js +0 -755
  88. package/lib/src/endpoint.js.map +0 -1
  89. package/lib/src/forward-msg.js.map +0 -1
  90. package/lib/src/get-msg.js +0 -46
  91. package/lib/src/get-msg.js.map +0 -1
  92. package/lib/src/icqq-agent-deps.js +0 -10
  93. package/lib/src/icqq-agent-deps.js.map +0 -1
  94. package/lib/src/icqq-guild.js.map +0 -1
  95. package/lib/src/icqq-inbound.js.map +0 -1
  96. package/lib/src/icqq-login-assist-bridge.js +0 -148
  97. package/lib/src/icqq-login-assist-bridge.js.map +0 -1
  98. package/lib/src/icqq-side-events.js +0 -130
  99. package/lib/src/icqq-side-events.js.map +0 -1
  100. package/lib/src/index.js +0 -50
  101. package/lib/src/index.js.map +0 -1
  102. package/lib/src/ipc-client.js.map +0 -1
  103. package/lib/src/login-assist-routes.js +0 -29
  104. package/lib/src/login-assist-routes.js.map +0 -1
  105. package/lib/src/login-ipc-contract.js +0 -35
  106. package/lib/src/login-ipc-contract.js.map +0 -1
  107. package/lib/src/outbound-media.js.map +0 -1
  108. package/lib/src/protocol.js.map +0 -1
  109. package/lib/src/routes.js +0 -67
  110. package/lib/src/routes.js.map +0 -1
  111. package/lib/src/segment-mapper.js +0 -2
  112. package/lib/src/segment-mapper.js.map +0 -1
  113. package/lib/src/tools/index.js +0 -23
  114. package/lib/src/tools/index.js.map +0 -1
  115. package/lib/src/types.js.map +0 -1
  116. package/plugin.yml +0 -3
  117. package/src/adapter.ts +0 -151
  118. package/src/get-msg.ts +0 -56
  119. package/src/icqq-login-assist-bridge.ts +0 -196
  120. package/src/icqq-side-events.ts +0 -143
  121. package/src/login-assist-routes.ts +0 -39
  122. package/src/login-ipc-contract.ts +0 -52
  123. package/src/routes.ts +0 -83
  124. package/src/segment-mapper.ts +0 -1
  125. package/src/tools/index.ts +0 -31
@@ -1,18 +1,57 @@
1
- import type { IcqqAdapter } from './adapter.js';
2
- import type { IcqqEndpoint } from './endpoint.js';
1
+ /**
2
+ * Agent tool deps for icqq.
3
+ * Endpoints register themselves on start; tools look up by config name / QQ uin.
4
+ */
5
+
6
+ import type { IpcClient } from './ipc-client.js';
7
+ import type { IpcFriendInfo, IpcGroupInfo } from './types.js';
8
+ import type { IpcResponse } from './protocol.js';
9
+
10
+ export interface IcqqAgentEndpoint {
11
+ readonly name: string;
12
+ readonly ipc: {
13
+ request(action: string, params?: Record<string, unknown>): Promise<IpcResponse>;
14
+ };
15
+ readonly friends: Map<number, IpcFriendInfo>;
16
+ readonly groups: Map<number, IpcGroupInfo>;
17
+ request(action: string, params?: Record<string, unknown>): Promise<IpcResponse>;
18
+ }
3
19
 
4
20
  export interface IcqqAgentDeps {
5
- getEndpoint: (endpointId: string) => IcqqEndpoint;
6
- getAdapter: () => IcqqAdapter;
21
+ getEndpoint: (endpointId: string) => IcqqAgentEndpoint;
22
+ getAdapter?: () => { getEndpoint: (id: string) => IcqqAgentEndpoint };
7
23
  }
8
24
 
9
- let _deps: IcqqAgentDeps | null = null;
25
+ const endpoints = new Map<string, IcqqAgentEndpoint>();
26
+ let override: IcqqAgentDeps | null = null;
27
+
28
+ export function registerIcqqAgentEndpoint(
29
+ endpointId: string,
30
+ endpoint: IcqqAgentEndpoint,
31
+ ): () => void {
32
+ endpoints.set(endpointId, endpoint);
33
+ return () => {
34
+ if (endpoints.get(endpointId) === endpoint) {
35
+ endpoints.delete(endpointId);
36
+ }
37
+ };
38
+ }
39
+
40
+ /** Optional override used by tests / transitional callers. Pass `null` to clear. */
41
+ export function setIcqqAgentDeps(deps: IcqqAgentDeps | null): void {
42
+ override = deps;
43
+ }
10
44
 
11
- export function setIcqqAgentDeps(deps: IcqqAgentDeps): void {
12
- _deps = deps;
45
+ function lookup(endpointId: string): IcqqAgentEndpoint {
46
+ const registered = endpoints.get(endpointId);
47
+ if (!registered) throw new Error(`Endpoint ${endpointId} 不存在`);
48
+ return registered;
13
49
  }
14
50
 
15
51
  export function getIcqqAgentDeps(): IcqqAgentDeps {
16
- if (!_deps) throw new Error('icqq agent deps not initialized');
17
- return _deps;
52
+ if (override) return override;
53
+ return {
54
+ getEndpoint: lookup,
55
+ getAdapter: () => ({ getEndpoint: lookup }),
56
+ };
18
57
  }
package/src/icqq-guild.ts CHANGED
@@ -1,11 +1,17 @@
1
1
  /**
2
2
  * ICQQ guild (QQ 频道) catalog sync, inbound normalize, Console channel list.
3
3
  */
4
- import type { MessageSegment } from "zhin.js";
4
+ import { toCanonicalSegments, type MessageSegment } from "zhin.js";
5
5
  import { Actions, type IpcGuildMessageEventData } from "./protocol.js";
6
- import type { IpcClient } from "./ipc-client.js";
7
6
  import { parseCqMessage } from "./cq-message.js";
8
- import { toCanonicalSegments } from "./segment-mapper.js";
7
+
8
+ /** Narrow request port shared by the real IPC client and endpoint test transports. */
9
+ export interface IcqqGuildRpc {
10
+ request(
11
+ action: string,
12
+ params?: Record<string, unknown>,
13
+ ): Promise<{ ok: boolean; data?: unknown }>;
14
+ }
9
15
 
10
16
  export interface IcqqGuildChannelEntry {
11
17
  channel_id: string;
@@ -37,7 +43,7 @@ export class IcqqGuildCatalog {
37
43
  this.guilds.clear();
38
44
  }
39
45
 
40
- async syncAll(ipc: IpcClient): Promise<void> {
46
+ async syncAll(ipc: IcqqGuildRpc): Promise<void> {
41
47
  const listResp = await ipc.request(Actions.GUILD_LIST);
42
48
  if (!listResp.ok) return;
43
49
 
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * icqq 守护进程 IPC 入站事件归一化(post_type / message_type / message 段)
3
3
  */
4
- import { Message, type MessageSegment, type QuotedMessagePayload } from "zhin.js";
4
+ import { Message, toCanonicalSegments, type MessageSegment, type QuotedMessagePayload } from "zhin.js";
5
5
  import { parseCqMessage } from "./cq-message.js";
6
- import { toCanonicalSegments } from "./segment-mapper.js";
7
6
  import { extractForwardResidFromJsonElement } from "./forward-msg.js";
8
7
  import type { GroupRole } from "./types.js";
9
8
 
@@ -110,6 +109,36 @@ export function parseIcqqSenderGroupRole(
110
109
  return ICQQ_GROUP_ROLES.has(role) ? role : undefined;
111
110
  }
112
111
 
112
+ /**
113
+ * 判断入站消息是否 @ 了本机账号(uin = endpoint 配置 name,即 QQ 号)。
114
+ * 新 Plugin Runtime 的 Message.content 为纯文本,at 信息只能经 metadata 传递,
115
+ * endpoint 在 admit 时用本函数标注 `metadata.mentioned`(供 Agent Host 的 @ 触发)。
116
+ */
117
+ export function isIcqqBotMentioned(input: {
118
+ readonly uin: string;
119
+ readonly content?: readonly MessageSegment[] | null;
120
+ readonly rawMessage?: string;
121
+ }): boolean {
122
+ const uin = String(input.uin ?? "").trim();
123
+ if (!uin) return false;
124
+ for (const seg of input.content ?? []) {
125
+ if (!seg || (seg.type !== "at" && seg.type !== "mention")) continue;
126
+ const data = (seg.data ?? {}) as Record<string, unknown>;
127
+ const target = String(
128
+ data.qq ?? data.user_id ?? data.id ?? data.target ?? "",
129
+ ).trim();
130
+ if (target && target !== "all" && Number(target) === Number(uin)) {
131
+ return true;
132
+ }
133
+ }
134
+ const raw = input.rawMessage ?? "";
135
+ if (!raw) return false;
136
+ const escaped = uin.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
137
+ // OneBot CQ 码 `[CQ:at,qq=uin]`;QQ 频道 markdown `<@!uin>`
138
+ return new RegExp(`\\[CQ:at,qq=${escaped}\\]`, "u").test(raw)
139
+ || new RegExp(`<@!?${escaped}>`, "u").test(raw);
140
+ }
141
+
113
142
  const MESSAGE_POST_TYPES = new Set(["message"]);
114
143
 
115
144
  function isMessagePostType(postType: unknown): boolean {
@@ -0,0 +1,155 @@
1
+ /**
2
+ * icqq IPC request/notice 事件 → 统一收件箱行(unified_inbox_request / unified_inbox_notice)。
3
+ *
4
+ * daemon 经 IpcEvent 推送 icqq client.em 分发的全部事件(见 protocol.ts),
5
+ * 好友/群请求(request.friend.add、request.group.add、request.group.invite)
6
+ * 与通知(notice.friend.*、notice.group.*)随事件流实时到达,无需轮询;
7
+ * endpoint 启动时补一次 GET_SYSTEM_MSG 拉取离线期间积存的待处理请求。
8
+ */
9
+ import type { IpcSystemMessage } from './types.js';
10
+
11
+ /** 行级公共前缀:adapter 槽 localName + endpoint live 名(uin)。 */
12
+ export interface IcqqInboxBase {
13
+ readonly adapter: string;
14
+ readonly endpointId: string;
15
+ }
16
+
17
+ /** post_type=request / request.* 判定(OneBot 事件壳)。 */
18
+ export function isIcqqRequestPayload(payload: unknown): boolean {
19
+ const postType = (payload as { post_type?: unknown } | null)?.post_type;
20
+ return typeof postType === 'string'
21
+ && (postType === 'request' || postType.startsWith('request.'));
22
+ }
23
+
24
+ /** post_type=notice / notice.* 判定。 */
25
+ export function isIcqqNoticePayload(payload: unknown): boolean {
26
+ const postType = (payload as { post_type?: unknown } | null)?.post_type;
27
+ return typeof postType === 'string'
28
+ && (postType === 'notice' || postType.startsWith('notice.'));
29
+ }
30
+
31
+ /**
32
+ * request 事件载荷 → unified_inbox_request 行。
33
+ * platform_request_id 取 flag(审批回执句柄),缺省回退 seq;两者皆无则无法审批,丢弃。
34
+ */
35
+ export function buildIcqqInboxRequestRow(
36
+ payload: Record<string, unknown>,
37
+ base: IcqqInboxBase,
38
+ ): Record<string, unknown> | null {
39
+ const userId = payload.user_id ?? payload.actor_id;
40
+ if (userId == null) return null;
41
+ const flag = asNonEmptyString(payload.flag);
42
+ const platformRequestId = flag ?? (payload.seq != null ? String(payload.seq) : '');
43
+ if (!platformRequestId) return null;
44
+ const requestType = asNonEmptyString(payload.request_type) ?? asNonEmptyString(payload.type);
45
+ const groupId = payload.group_id;
46
+ const isGroup = requestType === 'group' || groupId != null;
47
+ return {
48
+ adapter: base.adapter,
49
+ endpoint_id: base.endpointId,
50
+ platform_request_id: platformRequestId,
51
+ type: isGroup ? 'group' : (requestType ?? 'friend'),
52
+ scene_type: isGroup ? 'group' : null,
53
+ scene_id: String(groupId ?? userId),
54
+ sub_type: asNonEmptyString(payload.sub_type),
55
+ actor_id: String(userId),
56
+ actor_name: asNonEmptyString(payload.nickname),
57
+ comment: asNonEmptyString(payload.comment),
58
+ created_at: toMillis(payload.time),
59
+ resolved: 0,
60
+ resolved_at: null,
61
+ consumed: 0,
62
+ consumed_at: null,
63
+ };
64
+ }
65
+
66
+ /**
67
+ * notice 事件载荷 → unified_inbox_notice 行。
68
+ * 事件本身无稳定 id,platform_notice_id 由 time + actor + type 合成(配合去重)。
69
+ */
70
+ export function buildIcqqInboxNoticeRow(
71
+ payload: Record<string, unknown>,
72
+ base: IcqqInboxBase,
73
+ ): Record<string, unknown> | null {
74
+ const noticeType = asNonEmptyString(payload.notice_type) ?? asNonEmptyString(payload.type);
75
+ if (!noticeType) return null;
76
+ const subType = asNonEmptyString(payload.sub_type);
77
+ const userId = payload.user_id;
78
+ const operatorId = payload.operator_id;
79
+ const groupId = payload.group_id;
80
+ const createdAt = toMillis(payload.time);
81
+ const actorId = operatorId ?? userId;
82
+ const platformNoticeId = asNonEmptyString(payload.notice_id)
83
+ ?? (payload.seq != null ? String(payload.seq) : undefined)
84
+ ?? `${createdAt}_${String(actorId ?? '')}_${noticeType}${subType ? `.${subType}` : ''}`;
85
+ const targetId = userId != null && String(userId) !== String(actorId ?? '') ? userId : undefined;
86
+ return {
87
+ adapter: base.adapter,
88
+ endpoint_id: base.endpointId,
89
+ platform_notice_id: platformNoticeId,
90
+ type: noticeType,
91
+ scene_type: groupId != null ? 'group' : null,
92
+ scene_id: String(groupId ?? userId ?? operatorId ?? ''),
93
+ sub_type: subType,
94
+ actor_id: actorId != null ? String(actorId) : null,
95
+ actor_name: asNonEmptyString(payload.nickname),
96
+ target_id: targetId != null ? String(targetId) : null,
97
+ target_name: null,
98
+ payload: safeJson(payload),
99
+ created_at: createdAt,
100
+ consumed: 0,
101
+ consumed_at: null,
102
+ };
103
+ }
104
+
105
+ /**
106
+ * GET_SYSTEM_MSG 返回项 → unified_inbox_request 行(启动时首次拉取)。
107
+ * friend 列表项 type 语义即好友请求;group 列表项 type 为 add/invite(记 sub_type)。
108
+ */
109
+ export function buildIcqqSystemRequestRow(
110
+ message: IpcSystemMessage,
111
+ kind: 'friend' | 'group',
112
+ base: IcqqInboxBase,
113
+ ): Record<string, unknown> | null {
114
+ if (message.user_id == null) return null;
115
+ const platformRequestId = asNonEmptyString(message.flag)
116
+ ?? (message.seq != null ? String(message.seq) : '');
117
+ if (!platformRequestId) return null;
118
+ return {
119
+ adapter: base.adapter,
120
+ endpoint_id: base.endpointId,
121
+ platform_request_id: platformRequestId,
122
+ type: kind,
123
+ scene_type: kind === 'group' ? 'group' : null,
124
+ scene_id: String(kind === 'group' ? (message.group_id ?? message.user_id) : message.user_id),
125
+ sub_type: kind === 'group' ? (asNonEmptyString(message.type) ?? null) : null,
126
+ actor_id: String(message.user_id),
127
+ actor_name: asNonEmptyString(message.nickname),
128
+ comment: asNonEmptyString(message.comment),
129
+ created_at: toMillis(message.time),
130
+ resolved: 0,
131
+ resolved_at: null,
132
+ consumed: 0,
133
+ consumed_at: null,
134
+ };
135
+ }
136
+
137
+ function asNonEmptyString(value: unknown): string | null {
138
+ if (typeof value !== 'string') return null;
139
+ const trimmed = value.trim();
140
+ return trimmed ? trimmed : null;
141
+ }
142
+
143
+ /** oicq 事件 time 为秒;缺失时回退当前时间。 */
144
+ function toMillis(time: unknown): number {
145
+ const seconds = Number(time);
146
+ return Number.isFinite(seconds) && seconds > 0 ? seconds * 1000 : Date.now();
147
+ }
148
+
149
+ function safeJson(value: unknown): string {
150
+ try {
151
+ return JSON.stringify(value ?? {});
152
+ } catch {
153
+ return '{}';
154
+ }
155
+ }
package/src/index.ts CHANGED
@@ -1,73 +1,40 @@
1
- /**
2
- * ICQQ 适配器入口:类型扩展、导出、注册
3
- */
4
- import path from "path";
5
- import {
6
- usePlugin,
7
- type Plugin,
8
- type ToolFeature,
9
- } from "zhin.js";
10
- import { registerAgentPromptContributor, unregisterAgentPromptContributor } from "zhin.js/agent";
11
- import { createIcqqAgentPromptContributor } from "./agent-prompt.js";
12
- import type { Router } from "@zhin.js/host-router";
13
- import { PageManager } from "@zhin.js/host-api";
14
- import { IcqqAdapter } from "./adapter.js";
15
- import { registerTools } from "./tools/index.js";
16
- import { registerRoutes } from "./routes.js";
17
- import { registerLoginAssistRoutes } from "./login-assist-routes.js";
1
+ export {
2
+ Actions,
3
+ formatInboundContent,
4
+ formatInboundTarget,
5
+ formatOutboundBody,
6
+ parseSendTarget,
7
+ resolveIcqqConfig,
8
+ type IcqqAdapterConfig,
9
+ type IcqqInboundMessage,
10
+ type IcqqWireSegment,
11
+ type IpcEvent,
12
+ type IpcMessage,
13
+ type IpcRequest,
14
+ type IpcResponse,
15
+ type ParsedIcqqSendTarget,
16
+ type ResolvedIcqqConfig,
17
+ } from './protocol.js';
18
18
 
19
- declare module "zhin.js" {
20
- namespace Plugin {
21
- interface Contexts {
22
- web: PageManager;
23
- router: Router;
24
- }
25
- }
26
- interface Adapters {
27
- icqq: IcqqAdapter;
28
- }
29
- }
19
+ export {
20
+ IcqqIpcEndpoint,
21
+ type CreateIcqqIpc,
22
+ type IcqqEndpointOptions,
23
+ type IcqqInboxHooks,
24
+ type IcqqIpcTransport,
25
+ } from './endpoint.js';
30
26
 
31
- export * from "./types.js";
32
- export { IcqqEndpoint } from "./endpoint.js";
33
- export { IcqqAdapter } from "./adapter.js";
27
+ export * from './types.js';
34
28
 
35
- const plugin = usePlugin();
36
- const { provide, useContext, addCommand, root } = plugin;
29
+ export {
30
+ getIcqqAgentDeps,
31
+ registerIcqqAgentEndpoint,
32
+ setIcqqAgentDeps,
33
+ type IcqqAgentDeps,
34
+ type IcqqAgentEndpoint,
35
+ } from './icqq-agent-deps.js';
37
36
 
38
- // ── 适配器注册 ─────────────────────────────────────────────────────
39
- provide({
40
- name: "icqq",
41
- description: "ICQQ Adapter",
42
- mounted: async (p: Plugin) => {
43
- registerAgentPromptContributor(createIcqqAgentPromptContributor());
44
- const adapter = new IcqqAdapter(p);
45
- await adapter.start();
46
- return adapter;
47
- },
48
- dispose: async (adapter: IcqqAdapter) => {
49
- unregisterAgentPromptContributor("icqq");
50
- await adapter.stop();
51
- },
52
- } as any);
53
-
54
- // ── AI 工具注册 ────────────────────────────────────────────────────
55
- useContext("tool", "icqq", (toolService: ToolFeature, icqq: IcqqAdapter) => {
56
- return registerTools(toolService, icqq, plugin.name);
57
- });
58
-
59
- // ── Web 控制台入口 ─────────────────────────────────────────────────
60
- useContext("web", (pageManager) => {
61
- pageManager.addEntry({
62
- id: "icqq",
63
- development: path.resolve(import.meta.dirname, "../client/index.tsx"),
64
- production: path.resolve(import.meta.dirname, "../dist/index.js"),
65
- meta: { name: "ICQQ" },
66
- });
67
- });
68
-
69
- // ── HTTP 路由 ──────────────────────────────────────────────────────
70
- useContext("router", "icqq", async (router: Router, icqq: IcqqAdapter) => {
71
- registerRoutes(router, icqq, root);
72
- registerLoginAssistRoutes(router, root);
73
- });
37
+ export { IpcClient } from './ipc-client.js';
38
+ export {
39
+ resolveIcqqInboundMessageId,
40
+ } from './icqq-inbound.js';
package/src/protocol.ts CHANGED
@@ -9,9 +9,11 @@
9
9
  * 2. 认证通过后发送 IpcRequest → 等待 IpcResponse
10
10
  * 3. 认证通过后自动接收 icqq 事件推送(IpcEvent),断开连接自动停止
11
11
  *
12
- * 登录相关:**推送**走 IpcEvent(`system.login.*`),续传 invoke 见 `login-ipc-contract.ts`。
12
+ * 登录相关:**推送**走 IpcEvent(`system.login.*`)。
13
13
  */
14
14
 
15
+ import { pickCredential } from '@zhin.js/adapter';
16
+
15
17
  /** CLI → Daemon 请求 */
16
18
  export type IpcRequest = {
17
19
  id: string;
@@ -265,3 +267,191 @@ export const Actions = {
265
267
  SET_NOTIFY: "set_notify",
266
268
  GET_NOTIFY: "get_notify",
267
269
  } as const;
270
+
271
+ /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
272
+ export interface IcqqAdapterConfig {
273
+ readonly name?: string;
274
+ readonly autoReconnect?: boolean;
275
+ readonly outboundMedia?: 'file' | 'base64';
276
+ readonly rpc?: {
277
+ readonly host?: string;
278
+ readonly port?: number;
279
+ readonly token?: string;
280
+ };
281
+ /** Transitional: legacy root `endpoints[]` with `context: icqq`. */
282
+ readonly endpoints?: ReadonlyArray<{
283
+ readonly context?: string;
284
+ readonly name?: string;
285
+ readonly autoReconnect?: boolean;
286
+ readonly outboundMedia?: 'file' | 'base64';
287
+ readonly rpc?: {
288
+ readonly host?: string;
289
+ readonly port?: number;
290
+ readonly token?: string;
291
+ };
292
+ }>;
293
+ }
294
+
295
+ export interface ResolvedIcqqConfig {
296
+ readonly context: 'icqq';
297
+ readonly name: string;
298
+ readonly autoReconnect: boolean;
299
+ readonly outboundMedia?: 'file' | 'base64';
300
+ readonly rpc?: {
301
+ readonly host: string;
302
+ readonly port: number;
303
+ readonly token: string;
304
+ };
305
+ }
306
+
307
+ export interface IcqqWireSegment {
308
+ readonly type: string;
309
+ readonly data?: Record<string, unknown>;
310
+ }
311
+
312
+ export interface IcqqInboundMessage {
313
+ readonly id: string;
314
+ readonly target: string;
315
+ readonly content: string;
316
+ readonly sender: string;
317
+ readonly channelType: 'private' | 'group' | 'channel';
318
+ readonly metadata?: Record<string, unknown>;
319
+ }
320
+
321
+ export type ParsedIcqqSendTarget =
322
+ | { readonly kind: 'private'; readonly userId: number }
323
+ | { readonly kind: 'group'; readonly groupId: number }
324
+ | { readonly kind: 'temp'; readonly groupId: number; readonly userId: number }
325
+ | { readonly kind: 'channel'; readonly guildId: string; readonly channelId: string };
326
+
327
+ export function resolveIcqqConfig(config: IcqqAdapterConfig = {}): ResolvedIcqqConfig {
328
+ const entry = config.endpoints?.find((item) => item.context === 'icqq');
329
+ const name = pickCredential(config.name, entry?.name, process.env.ICQQ_ACCOUNT);
330
+ if (!/^\d+$/.test(name)) {
331
+ throw new TypeError(
332
+ 'ICQQ adapter requires numeric name (QQ uin) via plugins.<key>.name or ICQQ_ACCOUNT',
333
+ );
334
+ }
335
+ const autoReconnect = config.autoReconnect ?? entry?.autoReconnect ?? true;
336
+ const outboundMedia = config.outboundMedia ?? entry?.outboundMedia;
337
+ const rpcRaw = config.rpc ?? entry?.rpc;
338
+ const rpc = rpcRaw?.host && rpcRaw.port != null && rpcRaw.token
339
+ ? { host: rpcRaw.host, port: Number(rpcRaw.port), token: rpcRaw.token }
340
+ : undefined;
341
+ return {
342
+ context: 'icqq',
343
+ name,
344
+ autoReconnect,
345
+ ...(outboundMedia ? { outboundMedia } : {}),
346
+ ...(rpc ? { rpc } : {}),
347
+ };
348
+ }
349
+
350
+ /** Gateway reply target: `private:uid` / `group:gid` / `temp:gid:uid` / `channel:guild:channel`. */
351
+ export function formatInboundTarget(input: {
352
+ readonly channelType: 'private' | 'group' | 'channel';
353
+ readonly channelId: string;
354
+ readonly channelParentGroupId?: string;
355
+ readonly guildId?: string;
356
+ }): string {
357
+ if (input.channelType === 'private' && input.channelParentGroupId) {
358
+ return `temp:${input.channelParentGroupId}:${input.channelId}`;
359
+ }
360
+ if (input.channelType === 'channel' && input.guildId) {
361
+ return `channel:${input.guildId}:${input.channelId}`;
362
+ }
363
+ return `${input.channelType}:${input.channelId}`;
364
+ }
365
+
366
+ export function parseSendTarget(target: string): ParsedIcqqSendTarget {
367
+ const trimmed = target.trim();
368
+ if (trimmed.startsWith('temp:')) {
369
+ const rest = trimmed.slice(5);
370
+ const [groupId, userId] = rest.split(':');
371
+ if (!groupId || !userId) throw new TypeError(`Invalid icqq temp target: ${target}`);
372
+ return { kind: 'temp', groupId: Number(groupId), userId: Number(userId) };
373
+ }
374
+ if (trimmed.startsWith('channel:')) {
375
+ const rest = trimmed.slice(8);
376
+ const idx = rest.indexOf(':');
377
+ if (idx <= 0) throw new TypeError(`Invalid icqq channel target: ${target}`);
378
+ return {
379
+ kind: 'channel',
380
+ guildId: rest.slice(0, idx),
381
+ channelId: rest.slice(idx + 1),
382
+ };
383
+ }
384
+ if (trimmed.startsWith('private:')) {
385
+ return { kind: 'private', userId: Number(trimmed.slice(8)) };
386
+ }
387
+ if (trimmed.startsWith('group:')) {
388
+ return { kind: 'group', groupId: Number(trimmed.slice(6)) };
389
+ }
390
+ // Bare numeric id → private (common reply path)
391
+ if (/^\d+$/.test(trimmed)) {
392
+ return { kind: 'private', userId: Number(trimmed) };
393
+ }
394
+ throw new TypeError(`Invalid icqq send target: ${target}`);
395
+ }
396
+
397
+ export function formatOutboundBody(payload: unknown): string {
398
+ if (typeof payload === 'string') {
399
+ const text = payload.trim();
400
+ return text || '\u200b';
401
+ }
402
+ if (payload == null) return '\u200b';
403
+ if (!Array.isArray(payload)) {
404
+ // 单个段对象(非数组)按单元素数组处理,杜绝 String(object) → '[object Object]'
405
+ if (isWireSegment(payload)) {
406
+ return formatOutboundBody([payload]);
407
+ }
408
+ if (typeof payload === 'object' && payload !== null && 'text' in payload) {
409
+ const text = String((payload as { text?: unknown }).text ?? '').trim();
410
+ return text || '\u200b';
411
+ }
412
+ const text = String(payload).trim();
413
+ return text || '\u200b';
414
+ }
415
+ const joined = payload.map((seg) => {
416
+ if (typeof seg === 'string') return seg;
417
+ const item = seg as IcqqWireSegment;
418
+ switch (item.type) {
419
+ case 'text':
420
+ return String(item.data?.text ?? '');
421
+ case 'at':
422
+ case 'mention':
423
+ return `[at:${item.data?.qq ?? item.data?.id ?? item.data?.target ?? ''}]`;
424
+ case 'image':
425
+ return `[image:${item.data?.file || item.data?.url || base64Media(item.data) || ''}]`;
426
+ case 'face':
427
+ return `[face:${item.data?.id ?? ''}]`;
428
+ case 'reply':
429
+ return `[reply:${item.data?.message_id ?? item.data?.id ?? ''}]`;
430
+ case 'record':
431
+ case 'audio':
432
+ return `[record:${item.data?.file || item.data?.url || base64Media(item.data) || ''}]`;
433
+ case 'video':
434
+ return `[video:${item.data?.file || item.data?.url || base64Media(item.data) || ''}]`;
435
+ default:
436
+ return String(item.data?.text ?? '');
437
+ }
438
+ }).join('').trim();
439
+ return joined || '\u200b';
440
+ }
441
+
442
+ export function formatInboundContent(rawMessage: string): string {
443
+ return rawMessage;
444
+ }
445
+
446
+ /** base64 媒体回退:CQ `base64://` 由守护进程解码(异机 RPC 场景)。 */
447
+ function base64Media(data: Record<string, unknown> | undefined): string | undefined {
448
+ const b64 = data?.base64;
449
+ return typeof b64 === 'string' && b64 ? `base64://${b64}` : undefined;
450
+ }
451
+
452
+ /** 段对象判定:`{ type: string, data? }`(非数组 payload 的防御入口)。 */
453
+ function isWireSegment(value: unknown): value is IcqqWireSegment {
454
+ return typeof value === 'object'
455
+ && value !== null
456
+ && typeof (value as { type?: unknown }).type === 'string';
457
+ }
package/src/types.ts CHANGED
@@ -76,3 +76,19 @@ export interface IpcMemberInfo {
76
76
  last_sent_time?: number;
77
77
  shutup_time?: number;
78
78
  }
79
+
80
+ /**
81
+ * GET_SYSTEM_MSG 归一后的待处理请求(@icqqjs/cli NormalizedSystemMessage)。
82
+ * friendRequests 与 groupRequests 均为该形状,按 flag 处理。
83
+ */
84
+ export interface IpcSystemMessage {
85
+ type: string;
86
+ user_id?: number;
87
+ nickname?: string;
88
+ group_id?: number;
89
+ group_name?: string;
90
+ comment?: string;
91
+ flag?: string;
92
+ seq?: number;
93
+ time?: number;
94
+ }