@zhin.js/adapter-icqq 1.0.71 → 1.1.2
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 +1216 -52
- package/PERMITS.md +24 -0
- package/README.md +123 -429
- package/adapters/icqq/index.js +31 -0
- package/adapters/icqq/index.ts +42 -0
- package/agents/icqq/agent.json +20 -0
- package/agents/icqq/boundaries.md +3 -0
- package/agents/icqq/conventions.md +3 -0
- package/agents/icqq/skills/icqq-directory/SKILL.md +20 -0
- package/agents/icqq/skills/icqq-directory/tools/friend_list/index.js +19 -0
- package/agents/icqq/skills/icqq-directory/tools/friend_list/index.ts +19 -0
- package/agents/icqq/skills/icqq-directory/tools/group_list/index.js +20 -0
- package/agents/icqq/skills/icqq-directory/tools/group_list/index.ts +20 -0
- package/agents/icqq/skills/icqq-directory/tools/group_member_info/index.js +26 -0
- package/agents/icqq/skills/icqq-directory/tools/group_member_info/index.ts +28 -0
- package/agents/icqq/skills/icqq-directory/tools/user_info/index.js +15 -0
- package/agents/icqq/skills/icqq-directory/tools/user_info/index.ts +14 -0
- package/agents/icqq/skills/icqq-group-admin/SKILL.md +25 -0
- package/agents/icqq/skills/icqq-group-admin/tools/announce/index.js +18 -0
- package/agents/icqq/skills/icqq-group-admin/tools/announce/index.ts +17 -0
- package/agents/icqq/skills/icqq-group-admin/tools/essence/index.js +24 -0
- package/agents/icqq/skills/icqq-group-admin/tools/essence/index.ts +22 -0
- package/agents/icqq/skills/icqq-group-admin/tools/group_invite/index.js +23 -0
- package/agents/icqq/skills/icqq-group-admin/tools/group_invite/index.ts +20 -0
- package/agents/icqq/skills/icqq-group-admin/tools/group_kick/index.js +27 -0
- package/agents/icqq/skills/icqq-group-admin/tools/group_kick/index.ts +23 -0
- package/agents/icqq/skills/icqq-group-admin/tools/set_anonymous/index.js +23 -0
- package/agents/icqq/skills/icqq-group-admin/tools/set_anonymous/index.ts +21 -0
- package/agents/icqq/skills/icqq-group-admin/tools/set_mute/index.js +25 -0
- package/agents/icqq/skills/icqq-group-admin/tools/set_mute/index.ts +22 -0
- package/agents/icqq/skills/icqq-group-admin/tools/set_title/index.js +26 -0
- package/agents/icqq/skills/icqq-group-admin/tools/set_title/index.ts +23 -0
- package/agents/icqq/skills/icqq-group-state/SKILL.md +17 -0
- package/agents/icqq/skills/icqq-group-state/tools/group_files/index.js +24 -0
- package/agents/icqq/skills/icqq-group-state/tools/group_files/index.ts +22 -0
- package/agents/icqq/skills/icqq-group-state/tools/list_muted/index.js +18 -0
- package/agents/icqq/skills/icqq-group-state/tools/list_muted/index.ts +17 -0
- package/agents/icqq/skills/icqq-interaction/SKILL.md +19 -0
- package/agents/icqq/skills/icqq-interaction/tools/group_sign/index.js +20 -0
- package/agents/icqq/skills/icqq-interaction/tools/group_sign/index.ts +18 -0
- package/agents/icqq/skills/icqq-interaction/tools/poke/index.js +17 -0
- package/agents/icqq/skills/icqq-interaction/tools/poke/index.ts +16 -0
- package/agents/icqq/skills/icqq-interaction/tools/send_user_like/index.js +26 -0
- package/agents/icqq/skills/icqq-interaction/tools/send_user_like/index.ts +22 -0
- package/agents/icqq/system.md +3 -0
- package/commands/icqq/endpoint/add/[[id]]/index.js +3 -0
- package/commands/icqq/endpoint/add/[[id]]/index.ts +3 -0
- package/commands/icqq/endpoint/definition.js +31 -0
- package/commands/icqq/endpoint/definition.ts +32 -0
- package/commands/icqq/endpoint/list/index.js +3 -0
- package/commands/icqq/endpoint/list/index.ts +3 -0
- package/commands/icqq/endpoint/remove/[id]/index.js +3 -0
- package/commands/icqq/endpoint/remove/[id]/index.ts +3 -0
- package/lib/client.d.ts +19 -0
- package/lib/client.js +3 -0
- package/lib/content-resolver.d.ts +10 -0
- package/lib/content-resolver.js +98 -0
- package/lib/cq-message.d.ts +14 -0
- package/lib/cq-message.js +106 -0
- package/lib/endpoint.d.ts +30 -0
- package/lib/endpoint.js +1003 -0
- package/lib/forward-msg.d.ts +6 -0
- package/lib/forward-msg.js +107 -0
- package/lib/icqq-guild.d.ts +57 -0
- package/lib/icqq-guild.js +109 -0
- package/lib/icqq-inbound.d.ts +165 -0
- package/lib/icqq-inbound.js +651 -0
- package/lib/icqq-inbox.d.ts +25 -0
- package/lib/icqq-inbox.js +129 -0
- package/lib/icqq-login-assist.d.ts +27 -0
- package/lib/icqq-login-assist.js +109 -0
- package/lib/icqq-runtime-state.d.ts +1 -0
- package/lib/icqq-runtime-state.js +6 -0
- package/lib/index.d.ts +5 -18
- package/lib/index.js +5 -124
- package/lib/outbound-media.d.ts +14 -0
- package/lib/outbound-media.js +76 -0
- package/lib/protocol.d.ts +258 -0
- package/lib/protocol.js +422 -0
- package/lib/types.d.ts +33 -13
- package/lib/types.js +0 -1
- package/package.json +86 -32
- package/plugin.js +19 -0
- package/prompt-sections/platform/index.js +16 -0
- package/prompt-sections/platform/index.ts +16 -0
- package/schema.json +168 -0
- package/src/client.ts +29 -0
- package/src/content-resolver.ts +131 -0
- package/src/cq-message.ts +113 -0
- package/src/endpoint.ts +1084 -0
- package/src/forward-msg.ts +112 -0
- package/src/icqq-guild.ts +154 -0
- package/src/icqq-inbound.ts +814 -0
- package/src/icqq-inbox.ts +147 -0
- package/src/icqq-login-assist.ts +132 -0
- package/src/icqq-runtime-state.ts +7 -0
- package/src/index.ts +30 -149
- package/src/outbound-media.ts +103 -0
- package/src/protocol.ts +551 -0
- package/src/types.ts +35 -13
- package/LICENSE +0 -21
- package/client/ICQQManagement.tsx +0 -139
- package/client/LoginAssistPanel.tsx +0 -293
- package/client/index.tsx +0 -11
- package/client/tsconfig.json +0 -7
- package/client/utils/api.ts +0 -20
- package/dist/index.js +0 -1
- package/lib/adapter.d.ts +0 -34
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js +0 -396
- package/lib/adapter.js.map +0 -1
- package/lib/bot.d.ts +0 -46
- package/lib/bot.d.ts.map +0 -1
- package/lib/bot.js +0 -457
- package/lib/bot.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js.map +0 -1
- package/skills/icqq/SKILL.md +0 -22
- package/src/adapter.ts +0 -448
- package/src/bot.ts +0 -521
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { SystemMessage } from './types.js';
|
|
2
|
+
|
|
3
|
+
/** 行级公共前缀:adapter 槽 localName + endpoint live 名(uin)。 */
|
|
4
|
+
export interface IcqqInboxBase {
|
|
5
|
+
readonly adapter: string;
|
|
6
|
+
readonly endpointKey: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** post_type=request / request.* 判定(OneBot 事件壳)。 */
|
|
10
|
+
export function isIcqqRequestPayload(payload: unknown): boolean {
|
|
11
|
+
const postType = (payload as { post_type?: unknown } | null)?.post_type;
|
|
12
|
+
return typeof postType === 'string'
|
|
13
|
+
&& (postType === 'request' || postType.startsWith('request.'));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** post_type=notice / notice.* 判定。 */
|
|
17
|
+
export function isIcqqNoticePayload(payload: unknown): boolean {
|
|
18
|
+
const postType = (payload as { post_type?: unknown } | null)?.post_type;
|
|
19
|
+
return typeof postType === 'string'
|
|
20
|
+
&& (postType === 'notice' || postType.startsWith('notice.'));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* request 事件载荷 → unified_inbox_request 行。
|
|
25
|
+
* platform_request_id 取 flag(审批回执句柄),缺省回退 seq;两者皆无则无法审批,丢弃。
|
|
26
|
+
*/
|
|
27
|
+
export function buildIcqqInboxRequestRow(
|
|
28
|
+
payload: Record<string, unknown>,
|
|
29
|
+
base: IcqqInboxBase,
|
|
30
|
+
): Record<string, unknown> | null {
|
|
31
|
+
const userId = payload.user_id ?? payload.actor_id;
|
|
32
|
+
if (userId == null) return null;
|
|
33
|
+
const flag = asNonEmptyString(payload.flag);
|
|
34
|
+
const platformRequestId = flag ?? (payload.seq != null ? String(payload.seq) : '');
|
|
35
|
+
if (!platformRequestId) return null;
|
|
36
|
+
const requestType = asNonEmptyString(payload.request_type) ?? asNonEmptyString(payload.type);
|
|
37
|
+
const groupId = payload.group_id;
|
|
38
|
+
const isGroup = requestType === 'group' || groupId != null;
|
|
39
|
+
return {
|
|
40
|
+
adapter: base.adapter,
|
|
41
|
+
endpoint_id: base.endpointKey,
|
|
42
|
+
platform_request_id: platformRequestId,
|
|
43
|
+
type: isGroup ? 'group' : (requestType ?? 'friend'),
|
|
44
|
+
scene_type: isGroup ? 'group' : null,
|
|
45
|
+
scene_id: String(groupId ?? userId),
|
|
46
|
+
sub_type: asNonEmptyString(payload.sub_type),
|
|
47
|
+
actor_id: String(userId),
|
|
48
|
+
actor_name: asNonEmptyString(payload.nickname),
|
|
49
|
+
comment: asNonEmptyString(payload.comment),
|
|
50
|
+
created_at: toMillis(payload.time),
|
|
51
|
+
resolved: 0,
|
|
52
|
+
resolved_at: null,
|
|
53
|
+
consumed: 0,
|
|
54
|
+
consumed_at: null,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* notice 事件载荷 → unified_inbox_notice 行。
|
|
60
|
+
* 事件本身无稳定 id,platform_notice_id 由 time + actor + type 合成(配合去重)。
|
|
61
|
+
*/
|
|
62
|
+
export function buildIcqqInboxNoticeRow(
|
|
63
|
+
payload: Record<string, unknown>,
|
|
64
|
+
base: IcqqInboxBase,
|
|
65
|
+
): Record<string, unknown> | null {
|
|
66
|
+
const noticeType = asNonEmptyString(payload.notice_type) ?? asNonEmptyString(payload.type);
|
|
67
|
+
if (!noticeType) return null;
|
|
68
|
+
const subType = asNonEmptyString(payload.sub_type);
|
|
69
|
+
const userId = payload.user_id;
|
|
70
|
+
const operatorId = payload.operator_id;
|
|
71
|
+
const groupId = payload.group_id;
|
|
72
|
+
const createdAt = toMillis(payload.time);
|
|
73
|
+
const actorId = operatorId ?? userId;
|
|
74
|
+
const platformNoticeId = asNonEmptyString(payload.notice_id)
|
|
75
|
+
?? (payload.seq != null ? String(payload.seq) : undefined)
|
|
76
|
+
?? `${createdAt}_${String(actorId ?? '')}_${noticeType}${subType ? `.${subType}` : ''}`;
|
|
77
|
+
const targetId = userId != null && String(userId) !== String(actorId ?? '') ? userId : undefined;
|
|
78
|
+
return {
|
|
79
|
+
adapter: base.adapter,
|
|
80
|
+
endpoint_id: base.endpointKey,
|
|
81
|
+
platform_notice_id: platformNoticeId,
|
|
82
|
+
type: noticeType,
|
|
83
|
+
scene_type: groupId != null ? 'group' : null,
|
|
84
|
+
scene_id: String(groupId ?? userId ?? operatorId ?? ''),
|
|
85
|
+
sub_type: subType,
|
|
86
|
+
actor_id: actorId != null ? String(actorId) : null,
|
|
87
|
+
actor_name: asNonEmptyString(payload.nickname),
|
|
88
|
+
target_id: targetId != null ? String(targetId) : null,
|
|
89
|
+
target_name: null,
|
|
90
|
+
payload: safeJson(payload),
|
|
91
|
+
created_at: createdAt,
|
|
92
|
+
consumed: 0,
|
|
93
|
+
consumed_at: null,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* GET_SYSTEM_MSG 返回项 → unified_inbox_request 行(启动时首次拉取)。
|
|
99
|
+
* friend 列表项 type 语义即好友请求;group 列表项 type 为 add/invite(记 sub_type)。
|
|
100
|
+
*/
|
|
101
|
+
export function buildIcqqSystemRequestRow(
|
|
102
|
+
message: SystemMessage,
|
|
103
|
+
kind: 'friend' | 'group',
|
|
104
|
+
base: IcqqInboxBase,
|
|
105
|
+
): Record<string, unknown> | null {
|
|
106
|
+
if (message.user_id == null) return null;
|
|
107
|
+
const platformRequestId = asNonEmptyString(message.flag)
|
|
108
|
+
?? (message.seq != null ? String(message.seq) : '');
|
|
109
|
+
if (!platformRequestId) return null;
|
|
110
|
+
return {
|
|
111
|
+
adapter: base.adapter,
|
|
112
|
+
endpoint_id: base.endpointKey,
|
|
113
|
+
platform_request_id: platformRequestId,
|
|
114
|
+
type: kind,
|
|
115
|
+
scene_type: kind === 'group' ? 'group' : null,
|
|
116
|
+
scene_id: String(kind === 'group' ? (message.group_id ?? message.user_id) : message.user_id),
|
|
117
|
+
sub_type: kind === 'group' ? (asNonEmptyString(message.type) ?? null) : null,
|
|
118
|
+
actor_id: String(message.user_id),
|
|
119
|
+
actor_name: asNonEmptyString(message.nickname),
|
|
120
|
+
comment: asNonEmptyString(message.comment),
|
|
121
|
+
created_at: toMillis(message.time),
|
|
122
|
+
resolved: 0,
|
|
123
|
+
resolved_at: null,
|
|
124
|
+
consumed: 0,
|
|
125
|
+
consumed_at: null,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function asNonEmptyString(value: unknown): string | null {
|
|
130
|
+
if (typeof value !== 'string') return null;
|
|
131
|
+
const trimmed = value.trim();
|
|
132
|
+
return trimmed ? trimmed : null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** oicq 事件 time 为秒;缺失时回退当前时间。 */
|
|
136
|
+
function toMillis(time: unknown): number {
|
|
137
|
+
const seconds = Number(time);
|
|
138
|
+
return Number.isFinite(seconds) && seconds > 0 ? seconds * 1000 : Date.now();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function safeJson(value: unknown): string {
|
|
142
|
+
try {
|
|
143
|
+
return JSON.stringify(value ?? {});
|
|
144
|
+
} catch {
|
|
145
|
+
return '{}';
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { LoginAssist, LoginAssistType, PendingLoginTaskPayload } from '@zhin.js/core';
|
|
2
|
+
import { formatCompact } from '@zhin.js/logger';
|
|
3
|
+
|
|
4
|
+
type IcqqLoginClient = {
|
|
5
|
+
login(password?: string): Promise<unknown>;
|
|
6
|
+
submitSlider(ticket: string): Promise<unknown>;
|
|
7
|
+
sendSmsCode(): Promise<unknown>;
|
|
8
|
+
submitSmsCode(code: string): Promise<unknown>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type Logger = {
|
|
12
|
+
info(message: string): void;
|
|
13
|
+
warn(message: string): void;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Drive ICQQ login challenges through LoginAssist (refresh-safe pending tasks).
|
|
18
|
+
* Mirrors the classic stdin flow: qrcode/auth → login(); slider → submitSlider;
|
|
19
|
+
* device → sendSmsCode then submitSmsCode.
|
|
20
|
+
*/
|
|
21
|
+
export async function runIcqqLoginAssistStep(input: {
|
|
22
|
+
assist: LoginAssist;
|
|
23
|
+
client: IcqqLoginClient;
|
|
24
|
+
adapter: string;
|
|
25
|
+
endpointKey: string;
|
|
26
|
+
owner: object;
|
|
27
|
+
logger: Logger;
|
|
28
|
+
step: LoginAssistType;
|
|
29
|
+
event: unknown;
|
|
30
|
+
}): Promise<void> {
|
|
31
|
+
const { assist, client, adapter, endpointKey, owner, logger, step, event } = input;
|
|
32
|
+
assist.cancelOwned(owner, `replaced_by_${step}`);
|
|
33
|
+
|
|
34
|
+
const payload = buildPayload(step, event);
|
|
35
|
+
logger.info(formatCompact({
|
|
36
|
+
op: 'login_assist_pending',
|
|
37
|
+
endpoint: endpointKey,
|
|
38
|
+
type: step,
|
|
39
|
+
message: payload.message,
|
|
40
|
+
...(typeof payload.url === 'string' ? { url: payload.url } : {}),
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
if (step === 'device') {
|
|
45
|
+
try {
|
|
46
|
+
await client.sendSmsCode();
|
|
47
|
+
} catch (error) {
|
|
48
|
+
logger.warn(formatCompact({
|
|
49
|
+
op: 'login_assist_send_sms_failed',
|
|
50
|
+
endpoint: endpointKey,
|
|
51
|
+
error: error instanceof Error ? error.message : String(error),
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const value = await assist.waitForInput(adapter, endpointKey, step, payload, { owner });
|
|
57
|
+
const text = normalizeSubmitValue(value);
|
|
58
|
+
|
|
59
|
+
switch (step) {
|
|
60
|
+
case 'qrcode':
|
|
61
|
+
case 'auth':
|
|
62
|
+
await client.login();
|
|
63
|
+
break;
|
|
64
|
+
case 'slider':
|
|
65
|
+
await client.submitSlider(text);
|
|
66
|
+
break;
|
|
67
|
+
case 'device':
|
|
68
|
+
case 'sms':
|
|
69
|
+
await client.submitSmsCode(text);
|
|
70
|
+
break;
|
|
71
|
+
default:
|
|
72
|
+
await client.login();
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
logger.warn(formatCompact({
|
|
77
|
+
op: 'login_assist_failed',
|
|
78
|
+
endpoint: endpointKey,
|
|
79
|
+
type: step,
|
|
80
|
+
error: error instanceof Error ? error.message : String(error),
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function buildPayload(step: LoginAssistType, event: unknown): PendingLoginTaskPayload {
|
|
86
|
+
const raw = event && typeof event === 'object' ? event as Record<string, unknown> : {};
|
|
87
|
+
switch (step) {
|
|
88
|
+
case 'qrcode':
|
|
89
|
+
return {
|
|
90
|
+
message: '扫码登录:请在手机 QQ 扫描二维码,完成后在 Console / 终端确认继续',
|
|
91
|
+
image: bufferToDataUrl(raw.image),
|
|
92
|
+
};
|
|
93
|
+
case 'slider':
|
|
94
|
+
return {
|
|
95
|
+
message: '滑动验证:打开链接完成滑块,将返回的 ticket(可含 randstr)提交继续',
|
|
96
|
+
url: typeof raw.url === 'string' ? raw.url : undefined,
|
|
97
|
+
};
|
|
98
|
+
case 'device':
|
|
99
|
+
return {
|
|
100
|
+
message: '设备锁:已请求短信验证码,请提交短信验证码继续',
|
|
101
|
+
url: typeof raw.url === 'string' ? raw.url : undefined,
|
|
102
|
+
phone: raw.phone,
|
|
103
|
+
};
|
|
104
|
+
case 'auth':
|
|
105
|
+
return {
|
|
106
|
+
message: '身份验证:完成后在 Console / 终端确认继续',
|
|
107
|
+
url: typeof raw.url === 'string' ? raw.url : undefined,
|
|
108
|
+
device: raw.device,
|
|
109
|
+
};
|
|
110
|
+
default:
|
|
111
|
+
return { message: `登录辅助(${step})` };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function normalizeSubmitValue(value: string | Record<string, unknown>): string {
|
|
116
|
+
if (typeof value === 'string') return value.trim();
|
|
117
|
+
if (typeof value.ticket === 'string') return value.ticket.trim();
|
|
118
|
+
if (typeof value.code === 'string') return value.code.trim();
|
|
119
|
+
if (typeof value.value === 'string') return value.value.trim();
|
|
120
|
+
return JSON.stringify(value);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function bufferToDataUrl(image: unknown): string | undefined {
|
|
124
|
+
if (Buffer.isBuffer(image)) {
|
|
125
|
+
return `data:image/png;base64,${image.toString('base64')}`;
|
|
126
|
+
}
|
|
127
|
+
if (image instanceof Uint8Array) {
|
|
128
|
+
return `data:image/png;base64,${Buffer.from(image).toString('base64')}`;
|
|
129
|
+
}
|
|
130
|
+
if (typeof image === 'string' && image.trim()) return image.trim();
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ICQQ 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
|
|
3
|
+
* 由 plugin.ts setup() provide,adapter create 与 `icqq endpoint` 命令共享(同一 owner generation)。
|
|
4
|
+
*/
|
|
5
|
+
import { defineEndpointRuntimeStateToken } from 'zhin.js/adapter';
|
|
6
|
+
|
|
7
|
+
export const icqqRuntimeStateToken = defineEndpointRuntimeStateToken('icqq');
|
package/src/index.ts
CHANGED
|
@@ -1,149 +1,30 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
name: "icqq",
|
|
32
|
-
description: "ICQQ Adapter",
|
|
33
|
-
mounted: async (p: Plugin) => {
|
|
34
|
-
const adapter = new IcqqAdapter(p);
|
|
35
|
-
await adapter.start();
|
|
36
|
-
return adapter;
|
|
37
|
-
},
|
|
38
|
-
dispose: async (adapter: IcqqAdapter) => {
|
|
39
|
-
await adapter.stop();
|
|
40
|
-
},
|
|
41
|
-
} as any);
|
|
42
|
-
|
|
43
|
-
useContext("icqq", (icqq: IcqqAdapter) => {
|
|
44
|
-
addCommand(
|
|
45
|
-
new MessageCommand<"icqq">("赞我 <times:number>")
|
|
46
|
-
.permit("adapter(icqq)")
|
|
47
|
-
.action(async (message, result) => {
|
|
48
|
-
const bot = icqq.bots.get(message.$bot);
|
|
49
|
-
const send1 = bot?.sendLike(Number(message.$sender.id), 20);
|
|
50
|
-
const send2 = bot?.sendLike(Number(message.$sender.id), 20);
|
|
51
|
-
const send3 = bot?.sendLike(Number(message.$sender.id), 10);
|
|
52
|
-
const [send1Result, send2Result, send3Result] = await Promise.all([send1, send2, send3]);
|
|
53
|
-
let times = 0;
|
|
54
|
-
if (send1Result) times += 20;
|
|
55
|
-
if (send2Result) times += 20;
|
|
56
|
-
if (send3Result) times += 10;
|
|
57
|
-
return `给你赞好啦,你已经获得了${times}个赞`;
|
|
58
|
-
}),
|
|
59
|
-
);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
useContext("web", (web: WebServer) => {
|
|
63
|
-
const dispose = web.addEntry(
|
|
64
|
-
path.resolve(import.meta.dirname, "../client/index.tsx"),
|
|
65
|
-
);
|
|
66
|
-
return dispose;
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
useContext("router", "icqq", async (router: Router, icqq: IcqqAdapter) => {
|
|
70
|
-
const loginAssist = root.inject("loginAssist" as any) as
|
|
71
|
-
| {
|
|
72
|
-
listPending: () => unknown[];
|
|
73
|
-
submit: (id: string, value: string | Record<string, unknown>) => boolean;
|
|
74
|
-
cancel: (id: string, reason?: string) => boolean;
|
|
75
|
-
}
|
|
76
|
-
| undefined;
|
|
77
|
-
if (loginAssist) {
|
|
78
|
-
router.get("/api/login-assist/pending", (ctx: any) => {
|
|
79
|
-
ctx.body = loginAssist.listPending();
|
|
80
|
-
});
|
|
81
|
-
router.post("/api/login-assist/submit", async (ctx: any) => {
|
|
82
|
-
const body = ctx.request?.body as { id: string; value?: string | Record<string, unknown> };
|
|
83
|
-
if (!body?.id) {
|
|
84
|
-
ctx.status = 400;
|
|
85
|
-
ctx.body = { error: "missing id" };
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
const ok = loginAssist.submit(body.id, body.value ?? "");
|
|
89
|
-
ctx.body = { ok };
|
|
90
|
-
});
|
|
91
|
-
router.post("/api/login-assist/cancel", async (ctx: any) => {
|
|
92
|
-
const body = ctx.request?.body as { id: string; reason?: string };
|
|
93
|
-
if (!body?.id) {
|
|
94
|
-
ctx.status = 400;
|
|
95
|
-
ctx.body = { error: "missing id" };
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
const ok = loginAssist.cancel(body.id, body.reason);
|
|
99
|
-
ctx.body = { ok };
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
router.get("/api/icqq/bots", async (ctx) => {
|
|
104
|
-
try {
|
|
105
|
-
const bots = Array.from(icqq.bots.values());
|
|
106
|
-
if (bots.length === 0) {
|
|
107
|
-
ctx.body = { success: true, data: [], message: "暂无ICQQ机器人实例" };
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
const result = bots.map((bot) => {
|
|
111
|
-
try {
|
|
112
|
-
return {
|
|
113
|
-
name: bot.$config.name,
|
|
114
|
-
connected: bot.$connected || false,
|
|
115
|
-
groupCount: bot.gl?.size || 0,
|
|
116
|
-
friendCount: bot.fl?.size || 0,
|
|
117
|
-
receiveCount: bot.stat?.recv_msg_cnt || 0,
|
|
118
|
-
sendCount: bot.stat?.sent_msg_cnt || 0,
|
|
119
|
-
loginMode: bot.$config.password ? "password" : "qrcode",
|
|
120
|
-
status: bot.$connected ? "online" : "offline",
|
|
121
|
-
lastActivity: new Date().toISOString(),
|
|
122
|
-
};
|
|
123
|
-
} catch {
|
|
124
|
-
return {
|
|
125
|
-
name: bot.$config.name,
|
|
126
|
-
connected: false,
|
|
127
|
-
groupCount: 0,
|
|
128
|
-
friendCount: 0,
|
|
129
|
-
receiveCount: 0,
|
|
130
|
-
sendCount: 0,
|
|
131
|
-
loginMode: "unknown",
|
|
132
|
-
status: "error",
|
|
133
|
-
error: "数据获取失败",
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
ctx.body = { success: true, data: result, timestamp: new Date().toISOString() };
|
|
138
|
-
} catch (error) {
|
|
139
|
-
ctx.status = 500;
|
|
140
|
-
ctx.body = {
|
|
141
|
-
success: false,
|
|
142
|
-
error: "ICQQ_API_ERROR",
|
|
143
|
-
message: "获取机器人数据失败",
|
|
144
|
-
details: process.env.NODE_ENV === "development" ? (error as Error).message : undefined,
|
|
145
|
-
timestamp: new Date().toISOString(),
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
});
|
|
1
|
+
export {
|
|
2
|
+
Actions,
|
|
3
|
+
formatInboundContent,
|
|
4
|
+
formatOutboundBody,
|
|
5
|
+
icqqInboundConversation,
|
|
6
|
+
icqqOutboundTarget,
|
|
7
|
+
type ActionResult,
|
|
8
|
+
type IcqqEndpointConfig,
|
|
9
|
+
type IcqqInboundMessage,
|
|
10
|
+
type IcqqWireSegment,
|
|
11
|
+
type ParsedIcqqSendTarget,
|
|
12
|
+
type ResolvedIcqqConfig,
|
|
13
|
+
} from './protocol.js';
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
IcqqEndpoint,
|
|
17
|
+
type IcqqEndpointOptions,
|
|
18
|
+
} from './endpoint.js';
|
|
19
|
+
|
|
20
|
+
export * from './types.js';
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
icqqClient,
|
|
24
|
+
type IcqqClient,
|
|
25
|
+
type IcqqClientEventMap,
|
|
26
|
+
} from './client.js';
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
resolveIcqqInboundMessageId,
|
|
30
|
+
} from './icqq-inbound.js';
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import * as crypto from "node:crypto";
|
|
5
|
+
import { isMediaRef } from '@zhin.js/im-contract';
|
|
6
|
+
import type { MessageSegment, SendContent } from "zhin.js";
|
|
7
|
+
import type { IcqqEndpointConfig } from "./protocol.js";
|
|
8
|
+
|
|
9
|
+
export type IcqqOutboundMediaMode = "file" | "base64";
|
|
10
|
+
|
|
11
|
+
function extForMime(mime: string, fallback: string): string {
|
|
12
|
+
const map: Record<string, string> = {
|
|
13
|
+
"image/jpeg": ".jpg",
|
|
14
|
+
"image/png": ".png",
|
|
15
|
+
"image/gif": ".gif",
|
|
16
|
+
"image/webp": ".webp",
|
|
17
|
+
"audio/mpeg": ".mp3",
|
|
18
|
+
"audio/wav": ".wav",
|
|
19
|
+
"video/mp4": ".mp4",
|
|
20
|
+
};
|
|
21
|
+
return map[mime] || fallback;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function spoolBase64ToFile(
|
|
25
|
+
base64: string,
|
|
26
|
+
mime: string,
|
|
27
|
+
kind: string,
|
|
28
|
+
): string {
|
|
29
|
+
const dir = path.join(os.tmpdir(), "zhin-icqq-outbound");
|
|
30
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
31
|
+
const ext = extForMime(mime, kind === "audio" ? ".mp3" : kind === "video" ? ".mp4" : ".bin");
|
|
32
|
+
const filePath = path.join(dir, `${kind}-${crypto.randomBytes(6).toString("hex")}${ext}`);
|
|
33
|
+
fs.writeFileSync(filePath, Buffer.from(base64, "base64"));
|
|
34
|
+
return filePath;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function resolveIcqqOutboundMediaMode(
|
|
38
|
+
config: Pick<IcqqEndpointConfig, "outboundMedia">,
|
|
39
|
+
): IcqqOutboundMediaMode {
|
|
40
|
+
if (config.outboundMedia === "base64" || config.outboundMedia === "file") {
|
|
41
|
+
return config.outboundMedia;
|
|
42
|
+
}
|
|
43
|
+
return "file";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* file 模式:将 canonical MediaRef(`data.media`,kind=base64)物化为本机路径,
|
|
48
|
+
* 并把 media 改写为 path 引用(icqq 客户端直接读盘)。
|
|
49
|
+
* base64 模式:不改动(由 native Sendable projection 生成 base64:// file)。
|
|
50
|
+
*/
|
|
51
|
+
export interface PreparedIcqqOutboundMedia {
|
|
52
|
+
readonly content: SendContent;
|
|
53
|
+
dispose(): void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function prepareIcqqOutboundMedia(
|
|
57
|
+
content: SendContent,
|
|
58
|
+
mode: IcqqOutboundMediaMode = "file",
|
|
59
|
+
): PreparedIcqqOutboundMedia {
|
|
60
|
+
if (mode === "base64") return { content, dispose() {} };
|
|
61
|
+
|
|
62
|
+
const createdFiles: string[] = [];
|
|
63
|
+
const inputWasArray = Array.isArray(content);
|
|
64
|
+
const segments = Array.isArray(content) ? content : [content];
|
|
65
|
+
const materialized = segments.map((seg) => {
|
|
66
|
+
if (typeof seg === "string") return seg;
|
|
67
|
+
const { type, data } = seg as MessageSegment;
|
|
68
|
+
if (!data || typeof data !== "object") return seg;
|
|
69
|
+
const d = data as Record<string, unknown>;
|
|
70
|
+
const media = d.media;
|
|
71
|
+
if (!isMediaRef(media) || media.kind !== "base64") return seg;
|
|
72
|
+
const b64 = media.value.replace(/^base64:\/\//, "");
|
|
73
|
+
if (!b64) return seg;
|
|
74
|
+
if (type !== "image" && type !== "record" && type !== "audio" && type !== "video") {
|
|
75
|
+
return seg;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const mime = media.mime_type ?? "";
|
|
79
|
+
const filePath = spoolBase64ToFile(
|
|
80
|
+
b64,
|
|
81
|
+
mime || (type === "image" ? "image/jpeg" : type === "video" ? "video/mp4" : "audio/mpeg"),
|
|
82
|
+
type === "record" || type === "audio" ? "audio" : type,
|
|
83
|
+
);
|
|
84
|
+
createdFiles.push(filePath);
|
|
85
|
+
return {
|
|
86
|
+
type,
|
|
87
|
+
data: {
|
|
88
|
+
...d,
|
|
89
|
+
media: {
|
|
90
|
+
kind: "path",
|
|
91
|
+
value: filePath,
|
|
92
|
+
...(mime ? { mime_type: mime } : {}),
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
} as MessageSegment;
|
|
96
|
+
});
|
|
97
|
+
return {
|
|
98
|
+
content: inputWasArray ? materialized : materialized[0]!,
|
|
99
|
+
dispose() {
|
|
100
|
+
for (const filePath of createdFiles) fs.rmSync(filePath, { force: true });
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|