@greatlhd/ailo-desktop 1.0.0 → 1.0.1
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/dist/cli.js +31 -8
- package/dist/config_server.js +14 -162
- package/dist/index.js +207 -173
- package/dist/static/app.css +252 -13
- package/dist/static/app.html +71 -84
- package/dist/static/app.js +326 -108
- package/package.json +3 -9
- package/src/cli.ts +35 -8
- package/src/config_server.ts +22 -171
- package/src/index.ts +221 -177
- package/src/static/app.css +252 -13
- package/src/static/app.html +71 -84
- package/src/static/app.js +326 -108
- package/src/dingtalk-types.ts +0 -26
- package/src/qq-types.ts +0 -49
- package/src/qq-ws.ts +0 -223
package/src/qq-types.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
export interface QQConfig {
|
|
2
|
-
appId: string;
|
|
3
|
-
appSecret: string;
|
|
4
|
-
apiBase?: string;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export const OP_DISPATCH = 0;
|
|
8
|
-
export const OP_HEARTBEAT = 1;
|
|
9
|
-
export const OP_IDENTIFY = 2;
|
|
10
|
-
export const OP_RESUME = 6;
|
|
11
|
-
export const OP_RECONNECT = 7;
|
|
12
|
-
export const OP_INVALID_SESSION = 9;
|
|
13
|
-
export const OP_HELLO = 10;
|
|
14
|
-
export const OP_HEARTBEAT_ACK = 11;
|
|
15
|
-
|
|
16
|
-
export const INTENT_PUBLIC_GUILD_MESSAGES = 1 << 30;
|
|
17
|
-
export const INTENT_DIRECT_MESSAGE = 1 << 12;
|
|
18
|
-
export const INTENT_GROUP_AND_C2C = 1 << 25;
|
|
19
|
-
|
|
20
|
-
export const DEFAULT_API_BASE = "https://api.sgroup.qq.com";
|
|
21
|
-
export const TOKEN_URL = "https://bots.qq.com/app/getAppAccessToken";
|
|
22
|
-
|
|
23
|
-
export const RECONNECT_DELAYS = [1, 2, 5, 10, 30, 60];
|
|
24
|
-
export const MAX_RECONNECT_ATTEMPTS = 50;
|
|
25
|
-
|
|
26
|
-
export interface QQGatewayPayload {
|
|
27
|
-
op: number;
|
|
28
|
-
d?: unknown;
|
|
29
|
-
s?: number;
|
|
30
|
-
t?: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface QQMessageEvent {
|
|
34
|
-
id: string;
|
|
35
|
-
content: string;
|
|
36
|
-
timestamp: string;
|
|
37
|
-
author: { id: string; username?: string; bot?: boolean };
|
|
38
|
-
channel_id?: string;
|
|
39
|
-
guild_id?: string;
|
|
40
|
-
group_openid?: string;
|
|
41
|
-
group_id?: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface QQC2CMessageEvent {
|
|
45
|
-
id: string;
|
|
46
|
-
content: string;
|
|
47
|
-
timestamp: string;
|
|
48
|
-
author: { id: string; user_openid?: string; username?: string };
|
|
49
|
-
}
|
package/src/qq-ws.ts
DELETED
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
import WebSocket from "ws";
|
|
2
|
-
import {
|
|
3
|
-
type QQConfig,
|
|
4
|
-
type QQGatewayPayload,
|
|
5
|
-
OP_DISPATCH,
|
|
6
|
-
OP_HEARTBEAT,
|
|
7
|
-
OP_IDENTIFY,
|
|
8
|
-
OP_RESUME,
|
|
9
|
-
OP_RECONNECT,
|
|
10
|
-
OP_INVALID_SESSION,
|
|
11
|
-
OP_HELLO,
|
|
12
|
-
OP_HEARTBEAT_ACK,
|
|
13
|
-
INTENT_PUBLIC_GUILD_MESSAGES,
|
|
14
|
-
INTENT_DIRECT_MESSAGE,
|
|
15
|
-
INTENT_GROUP_AND_C2C,
|
|
16
|
-
DEFAULT_API_BASE,
|
|
17
|
-
TOKEN_URL,
|
|
18
|
-
RECONNECT_DELAYS,
|
|
19
|
-
MAX_RECONNECT_ATTEMPTS,
|
|
20
|
-
} from "./qq-types.js";
|
|
21
|
-
|
|
22
|
-
type DispatchHandler = (event: string, data: any) => void;
|
|
23
|
-
|
|
24
|
-
export class QQGatewayClient {
|
|
25
|
-
private ws: WebSocket | null = null;
|
|
26
|
-
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
27
|
-
private lastSeq: number | null = null;
|
|
28
|
-
private sessionId: string | null = null;
|
|
29
|
-
private reconnectAttempts = 0;
|
|
30
|
-
private closed = false;
|
|
31
|
-
|
|
32
|
-
private accessToken: string = "";
|
|
33
|
-
private tokenExpiresAt = 0;
|
|
34
|
-
|
|
35
|
-
private onDispatch: DispatchHandler;
|
|
36
|
-
private log: (level: string, msg: string, data?: Record<string, unknown>) => void;
|
|
37
|
-
|
|
38
|
-
constructor(
|
|
39
|
-
private config: QQConfig,
|
|
40
|
-
onDispatch: DispatchHandler,
|
|
41
|
-
log?: (level: string, msg: string, data?: Record<string, unknown>) => void,
|
|
42
|
-
) {
|
|
43
|
-
this.onDispatch = onDispatch;
|
|
44
|
-
this.log = log ?? ((level, msg, data) => console.log(`[qq-ws] [${level}] ${msg}`, data ?? ""));
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
private get apiBase(): string {
|
|
48
|
-
return (this.config.apiBase ?? DEFAULT_API_BASE).replace(/\/$/, "");
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async refreshToken(): Promise<string> {
|
|
52
|
-
if (this.accessToken && Date.now() < this.tokenExpiresAt - 30_000) {
|
|
53
|
-
return this.accessToken;
|
|
54
|
-
}
|
|
55
|
-
const res = await fetch(TOKEN_URL, {
|
|
56
|
-
method: "POST",
|
|
57
|
-
headers: { "Content-Type": "application/json" },
|
|
58
|
-
body: JSON.stringify({ appId: this.config.appId, clientSecret: this.config.appSecret }),
|
|
59
|
-
});
|
|
60
|
-
if (!res.ok) throw new Error(`QQ token refresh failed: HTTP ${res.status}`);
|
|
61
|
-
const body = (await res.json()) as { access_token: string; expires_in: number };
|
|
62
|
-
this.accessToken = body.access_token;
|
|
63
|
-
this.tokenExpiresAt = Date.now() + body.expires_in * 1000;
|
|
64
|
-
this.log("info", "access token refreshed", { expires_in: body.expires_in });
|
|
65
|
-
return this.accessToken;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
getAccessToken(): string {
|
|
69
|
-
return this.accessToken;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async connect(): Promise<void> {
|
|
73
|
-
this.closed = false;
|
|
74
|
-
await this.refreshToken();
|
|
75
|
-
|
|
76
|
-
const gatewayUrl = await this.fetchGatewayUrl();
|
|
77
|
-
this.log("info", `connecting to gateway: ${gatewayUrl}`);
|
|
78
|
-
this.createConnection(gatewayUrl);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
private async fetchGatewayUrl(): Promise<string> {
|
|
82
|
-
const token = await this.refreshToken();
|
|
83
|
-
const res = await fetch(`${this.apiBase}/gateway`, {
|
|
84
|
-
headers: { Authorization: `QQBot ${token}` },
|
|
85
|
-
});
|
|
86
|
-
if (!res.ok) throw new Error(`QQ gateway fetch failed: HTTP ${res.status}`);
|
|
87
|
-
const body = (await res.json()) as { url: string };
|
|
88
|
-
return body.url;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
private createConnection(url: string): void {
|
|
92
|
-
const ws = new WebSocket(url);
|
|
93
|
-
this.ws = ws;
|
|
94
|
-
|
|
95
|
-
ws.on("open", () => {
|
|
96
|
-
this.log("info", "WebSocket connected");
|
|
97
|
-
this.reconnectAttempts = 0;
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
ws.on("message", (raw: WebSocket.Data) => {
|
|
101
|
-
try {
|
|
102
|
-
const payload = JSON.parse(raw.toString("utf-8")) as QQGatewayPayload;
|
|
103
|
-
this.handlePayload(payload);
|
|
104
|
-
} catch (err) {
|
|
105
|
-
this.log("error", "failed to parse WS message", { err: String(err) });
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
ws.on("close", (code: number, reason: Buffer) => {
|
|
110
|
-
this.log("warn", `WebSocket closed: ${code} ${reason.toString("utf-8")}`);
|
|
111
|
-
this.stopHeartbeat();
|
|
112
|
-
if (!this.closed) this.scheduleReconnect();
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
ws.on("error", (err: Error) => {
|
|
116
|
-
this.log("error", "WebSocket error", { err: err.message });
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
private handlePayload(payload: QQGatewayPayload): void {
|
|
121
|
-
if (payload.s != null) this.lastSeq = payload.s;
|
|
122
|
-
|
|
123
|
-
switch (payload.op) {
|
|
124
|
-
case OP_HELLO:
|
|
125
|
-
this.startHeartbeat((payload.d as Record<string, unknown>)?.heartbeat_interval as number ?? 41250);
|
|
126
|
-
if (this.sessionId) {
|
|
127
|
-
this.sendResume();
|
|
128
|
-
} else {
|
|
129
|
-
this.sendIdentify();
|
|
130
|
-
}
|
|
131
|
-
break;
|
|
132
|
-
|
|
133
|
-
case OP_DISPATCH:
|
|
134
|
-
if (payload.t === "READY") {
|
|
135
|
-
this.sessionId = (payload.d as Record<string, unknown>)?.session_id as string ?? null;
|
|
136
|
-
this.log("info", "READY", { session_id: this.sessionId });
|
|
137
|
-
}
|
|
138
|
-
if (payload.t) {
|
|
139
|
-
this.onDispatch(payload.t, payload.d);
|
|
140
|
-
}
|
|
141
|
-
break;
|
|
142
|
-
|
|
143
|
-
case OP_HEARTBEAT_ACK:
|
|
144
|
-
break;
|
|
145
|
-
|
|
146
|
-
case OP_RECONNECT:
|
|
147
|
-
this.log("info", "server requested reconnect");
|
|
148
|
-
this.ws?.close(4000, "reconnect");
|
|
149
|
-
break;
|
|
150
|
-
|
|
151
|
-
case OP_INVALID_SESSION:
|
|
152
|
-
this.log("warn", "invalid session, re-identifying");
|
|
153
|
-
this.sessionId = null;
|
|
154
|
-
this.lastSeq = null;
|
|
155
|
-
setTimeout(() => this.sendIdentify(), 2000);
|
|
156
|
-
break;
|
|
157
|
-
|
|
158
|
-
default:
|
|
159
|
-
this.log("debug", `unhandled op: ${payload.op}`, { d: payload.d });
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
private sendIdentify(): void {
|
|
164
|
-
const intents = INTENT_PUBLIC_GUILD_MESSAGES | INTENT_DIRECT_MESSAGE | INTENT_GROUP_AND_C2C;
|
|
165
|
-
this.send({
|
|
166
|
-
op: OP_IDENTIFY,
|
|
167
|
-
d: { token: `QQBot ${this.accessToken}`, intents, shard: [0, 1] },
|
|
168
|
-
});
|
|
169
|
-
this.log("debug", "sent IDENTIFY");
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
private sendResume(): void {
|
|
173
|
-
this.send({
|
|
174
|
-
op: OP_RESUME,
|
|
175
|
-
d: { token: `QQBot ${this.accessToken}`, session_id: this.sessionId, seq: this.lastSeq },
|
|
176
|
-
});
|
|
177
|
-
this.log("debug", "sent RESUME");
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
private startHeartbeat(intervalMs: number): void {
|
|
181
|
-
this.stopHeartbeat();
|
|
182
|
-
this.heartbeatTimer = setInterval(() => {
|
|
183
|
-
this.send({ op: OP_HEARTBEAT, d: this.lastSeq });
|
|
184
|
-
}, intervalMs);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
private stopHeartbeat(): void {
|
|
188
|
-
if (this.heartbeatTimer) {
|
|
189
|
-
clearInterval(this.heartbeatTimer);
|
|
190
|
-
this.heartbeatTimer = null;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
private send(payload: QQGatewayPayload): void {
|
|
195
|
-
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
196
|
-
this.ws.send(JSON.stringify(payload));
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
private scheduleReconnect(): void {
|
|
201
|
-
if (this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
|
202
|
-
this.log("error", "max reconnect attempts reached, giving up");
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
const delay = RECONNECT_DELAYS[Math.min(this.reconnectAttempts, RECONNECT_DELAYS.length - 1)] * 1000;
|
|
206
|
-
this.reconnectAttempts++;
|
|
207
|
-
this.log("info", `reconnecting in ${delay / 1000}s (attempt ${this.reconnectAttempts})`);
|
|
208
|
-
setTimeout(() => {
|
|
209
|
-
if (!this.closed) {
|
|
210
|
-
this.connect().catch((err) => this.log("error", "reconnect failed", { err: String(err) }));
|
|
211
|
-
}
|
|
212
|
-
}, delay);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
close(): void {
|
|
216
|
-
this.closed = true;
|
|
217
|
-
this.stopHeartbeat();
|
|
218
|
-
if (this.ws) {
|
|
219
|
-
this.ws.close(1000, "shutdown");
|
|
220
|
-
this.ws = null;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|