@napuketto/kernel 0.0.1 → 0.0.3
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/README.md +17 -0
- package/dist/index.d.mts +1032 -988
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1360 -1170
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { get } from "node:https";
|
|
2
1
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { readFile, rename, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, extname, join, resolve } from "node:path";
|
|
4
|
+
import { parse, stringify } from "smol-toml";
|
|
4
5
|
import process from "node:process";
|
|
5
6
|
import pino from "pino";
|
|
6
7
|
import pinoPretty from "pino-pretty";
|
|
7
8
|
import { homedir, hostname } from "node:os";
|
|
8
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { get } from "node:https";
|
|
9
11
|
import { EventEmitter } from "node:events";
|
|
10
|
-
import { readFile, rename, writeFile } from "node:fs/promises";
|
|
11
|
-
import { parse, stringify } from "smol-toml";
|
|
12
12
|
//#region src/infra/errors.ts
|
|
13
13
|
/**
|
|
14
14
|
* 全部错误码集合。协议层映射表可据此推导:
|
|
@@ -42,489 +42,549 @@ function isKernelError(err) {
|
|
|
42
42
|
return err instanceof KernelError;
|
|
43
43
|
}
|
|
44
44
|
//#endregion
|
|
45
|
-
//#region src/
|
|
45
|
+
//#region src/infra/config.ts
|
|
46
46
|
/**
|
|
47
|
-
*
|
|
47
|
+
* 配置基类(ADR-012)
|
|
48
48
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
49
|
+
* 读文件 → 校验 → 内存对象 → 变更写回。支持 **JSON / TOML** 两种文本格式
|
|
50
|
+
* (按扩展名推断:.toml → TOML(smol-toml),其余 → JSON)。
|
|
51
|
+
*
|
|
52
|
+
* kernel 不依赖 zod:只依赖校验器的最小 `parse` 形状(`ConfigSchema`),
|
|
53
|
+
* 协议包的 zod schema 天然满足;kernel 主配置用手写校验器包装。
|
|
54
|
+
* 协议配置 schema 在各自协议包,见各包 docs/design.md。
|
|
53
55
|
*/
|
|
54
|
-
/**
|
|
55
|
-
function
|
|
56
|
-
if (
|
|
57
|
-
|
|
56
|
+
/** 按扩展名推断格式(.toml → toml,其余 → json)。 */
|
|
57
|
+
function inferFormat(path) {
|
|
58
|
+
if (extname(path).toLowerCase() === ".toml") return "toml";
|
|
59
|
+
return "json";
|
|
58
60
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
for (const uid of uids) out.push({
|
|
84
|
-
uid,
|
|
85
|
-
uin: uinMap.get(uid) ?? uid,
|
|
86
|
-
nickname: this.service.getBuddyNick(uid),
|
|
87
|
-
remark: this.service.getBuddyRemark(uid)
|
|
88
|
-
});
|
|
89
|
-
return out;
|
|
61
|
+
function isMissingFileError(err) {
|
|
62
|
+
if (err instanceof Error) return err.code === "ENOENT";
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 配置基类。每次 `load()` / `save()` 都经过 schema 校验,
|
|
67
|
+
* 校验失败抛 `KernelError('INVALID_PARAM')`,不静默吞掉。
|
|
68
|
+
* - `load()`:seed 存在 → 直接用 seed;否则读文件(缺失落默认值)。
|
|
69
|
+
* - `save()`:按格式序列化(TOML/JSON)原子写。
|
|
70
|
+
*/
|
|
71
|
+
var ConfigBase = class {
|
|
72
|
+
path;
|
|
73
|
+
defaults;
|
|
74
|
+
schema;
|
|
75
|
+
format;
|
|
76
|
+
seed;
|
|
77
|
+
value;
|
|
78
|
+
constructor(opts) {
|
|
79
|
+
this.path = opts.path;
|
|
80
|
+
this.defaults = opts.defaults;
|
|
81
|
+
this.schema = opts.schema;
|
|
82
|
+
this.format = opts.format ?? inferFormat(opts.path);
|
|
83
|
+
this.seed = opts.seed;
|
|
84
|
+
this.value = opts.seed ?? opts.defaults;
|
|
90
85
|
}
|
|
91
|
-
/**
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const friendMap = /* @__PURE__ */ new Map();
|
|
95
|
-
for (const friend of await this.getFriendList()) friendMap.set(friend.uid, friend);
|
|
96
|
-
return categories.map((cat) => {
|
|
97
|
-
const buddies = [];
|
|
98
|
-
for (const uid of cat.buddyUids) {
|
|
99
|
-
const friend = friendMap.get(uid);
|
|
100
|
-
if (friend !== void 0) buddies.push(friend);
|
|
101
|
-
}
|
|
102
|
-
return {
|
|
103
|
-
categoryId: cat.categoryId,
|
|
104
|
-
categorySortId: cat.categorySortId,
|
|
105
|
-
categoryName: cat.categroyName,
|
|
106
|
-
categoryMbCount: cat.categroyMbCount,
|
|
107
|
-
onlineCount: cat.onlineCount,
|
|
108
|
-
buddyList: buddies
|
|
109
|
-
};
|
|
110
|
-
});
|
|
86
|
+
/** 当前内存值(只读消费;外部不得直接修改返回对象)。 */
|
|
87
|
+
get() {
|
|
88
|
+
return this.value;
|
|
111
89
|
}
|
|
112
|
-
/**
|
|
113
|
-
|
|
90
|
+
/**
|
|
91
|
+
* 加载配置:
|
|
92
|
+
* - seed 存在 → 直接用 seed(已校验,不读文件;全局 TOML 分段装配方式);
|
|
93
|
+
* - 文件缺失 → 写入默认值并落盘(首次运行自动生成),返回默认值;
|
|
94
|
+
* - 文件存在 → 读取 + 校验,更新内存,返回。
|
|
95
|
+
*/
|
|
96
|
+
async load() {
|
|
97
|
+
if (this.seed !== void 0) {
|
|
98
|
+
this.value = this.seed;
|
|
99
|
+
return this.value;
|
|
100
|
+
}
|
|
101
|
+
let raw;
|
|
114
102
|
try {
|
|
115
|
-
|
|
116
|
-
} catch {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
103
|
+
raw = await readFile(this.path, "utf8");
|
|
104
|
+
} catch (err) {
|
|
105
|
+
if (isMissingFileError(err)) {
|
|
106
|
+
await this.save(this.defaults);
|
|
107
|
+
return this.value;
|
|
108
|
+
}
|
|
109
|
+
throw new KernelError(`读取配置文件失败: ${this.path}`, "UNKNOWN", { cause: err });
|
|
120
110
|
}
|
|
111
|
+
this.value = this.parse(raw);
|
|
112
|
+
return this.value;
|
|
121
113
|
}
|
|
122
|
-
/**
|
|
123
|
-
|
|
124
|
-
return this.
|
|
125
|
-
}
|
|
126
|
-
/** 好友申请列表(set_friend_add_request 应答前查找匹配项)。 */
|
|
127
|
-
async getBuddyReqList() {
|
|
128
|
-
const raw = await this.service.getBuddyReq();
|
|
129
|
-
unwrap$3("getBuddyReq", raw.result, raw.errMsg);
|
|
130
|
-
return raw.buddyReqs ?? [];
|
|
131
|
-
}
|
|
132
|
-
/** 应答好友申请(accept=true 同意;remark 由调用方另行 setFriendRemark)。 */
|
|
133
|
-
async handleFriendRequest(notify, accept) {
|
|
134
|
-
await this.service.approvalFriendRequest({
|
|
135
|
-
friendUid: notify.friendUid,
|
|
136
|
-
reqTime: notify.reqTime,
|
|
137
|
-
accept
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
/** 设置好友备注(void 语义乐观处理)。 */
|
|
141
|
-
setFriendRemark(uid, remark) {
|
|
142
|
-
this.service.setBuddyRemark({
|
|
143
|
-
uid,
|
|
144
|
-
remark
|
|
145
|
-
});
|
|
114
|
+
/** 重新从磁盘加载(外部变更后的热更新入口,P2 配置热更新使用)。 */
|
|
115
|
+
reload() {
|
|
116
|
+
return this.load();
|
|
146
117
|
}
|
|
147
|
-
/**
|
|
148
|
-
async
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
118
|
+
/** 校验并落盘(原子写:临时文件 + rename),随后更新内存。 */
|
|
119
|
+
async save(next) {
|
|
120
|
+
const parsed = this.parse(this.serialize(next));
|
|
121
|
+
mkdirSync(dirname(this.path), { recursive: true });
|
|
122
|
+
const tmpPath = `${this.path}.tmp`;
|
|
123
|
+
await writeFile(tmpPath, `${this.serialize(parsed)}\n`, "utf8");
|
|
124
|
+
await rename(tmpPath, this.path);
|
|
125
|
+
this.value = parsed;
|
|
155
126
|
}
|
|
156
|
-
/**
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
unwrap$3("getDoubtBuddyReq", raw.result, raw.errMsg);
|
|
161
|
-
const list = raw.doubtList ?? [];
|
|
162
|
-
let uinMap = /* @__PURE__ */ new Map();
|
|
163
|
-
if (this.uidToUin !== void 0 && list.length > 0) uinMap = await this.uidToUin(list.map((item) => item.uid));
|
|
164
|
-
const out = [];
|
|
165
|
-
for (const item of list) out.push(this.toDoubtFriendRequestInfo(item, uinMap));
|
|
166
|
-
return out;
|
|
127
|
+
/** 对象 → 文本(TOML/JSON)。 */
|
|
128
|
+
serialize(value) {
|
|
129
|
+
if (this.format === "toml") return stringify(value);
|
|
130
|
+
return JSON.stringify(value, null, 4);
|
|
167
131
|
}
|
|
168
|
-
/**
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
uin: Number(uinMap.get(item.uid) ?? item.uid),
|
|
173
|
-
type: "doubt"
|
|
174
|
-
};
|
|
175
|
-
if (item.nick !== void 0) info.nick = item.nick;
|
|
176
|
-
if (item.source !== void 0) info.source = item.source;
|
|
177
|
-
if (item.reason !== void 0) info.reason = item.reason;
|
|
178
|
-
if (item.msg !== void 0) info.msg = item.msg;
|
|
179
|
-
if (item.groupCode !== void 0) info.group_code = item.groupCode;
|
|
180
|
-
if (item.reqTime !== void 0) info.time = item.reqTime;
|
|
181
|
-
return info;
|
|
132
|
+
/** 文本 → 对象(TOML/JSON)。 */
|
|
133
|
+
deserialize(raw) {
|
|
134
|
+
if (this.format === "toml") return parse(raw);
|
|
135
|
+
return JSON.parse(raw);
|
|
182
136
|
}
|
|
183
|
-
/**
|
|
184
|
-
|
|
185
|
-
|
|
137
|
+
/** 反序列化 + schema 校验,错误统一包装为 KernelError。 */
|
|
138
|
+
parse(raw) {
|
|
139
|
+
let input;
|
|
140
|
+
try {
|
|
141
|
+
input = this.deserialize(raw);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
throw new KernelError(`配置文件 ${this.format.toUpperCase()} 解析失败: ${this.path}`, "INVALID_PARAM", { cause: err });
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
return this.schema.parse(input);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
throw new KernelError(`配置文件校验失败: ${this.path}`, "INVALID_PARAM", { cause: err });
|
|
149
|
+
}
|
|
186
150
|
}
|
|
187
151
|
};
|
|
152
|
+
/** 解析 TOML 文本 → 对象(boot.cjs / 探测脚本等无 ConfigBase 场景复用)。 */
|
|
153
|
+
function parseToml(text) {
|
|
154
|
+
return parse(text);
|
|
155
|
+
}
|
|
156
|
+
/** 对象 → TOML 文本(config init 生成全局配置文件等场景)。 */
|
|
157
|
+
function stringifyToml(value) {
|
|
158
|
+
return stringify(value);
|
|
159
|
+
}
|
|
188
160
|
//#endregion
|
|
189
|
-
//#region src/
|
|
161
|
+
//#region src/infra/logger.ts
|
|
190
162
|
/**
|
|
191
|
-
*
|
|
163
|
+
* pino 日志封装(ADR-007)
|
|
192
164
|
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
165
|
+
* - console(pino-pretty 可读输出)+ 可选文件双目的地
|
|
166
|
+
* - 统一 level 控制
|
|
167
|
+
* - 内置 redact(token / 票据等敏感字段不打日志),可追加
|
|
168
|
+
* - 无全局单例(ADR-015 推论):每进程实例化一份,由装配层持有
|
|
195
169
|
*/
|
|
196
|
-
/**
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS: 7,
|
|
248
|
-
SET_ADMIN: 8,
|
|
249
|
-
KICK_MEMBER_NOTIFY_ADMIN: 9,
|
|
250
|
-
KICK_MEMBER_NOTIFY_KICKED: 10,
|
|
251
|
-
MEMBER_LEAVE_NOTIFY_ADMIN: 11,
|
|
252
|
-
CANCEL_ADMIN_NOTIFY_CANCELED: 12,
|
|
253
|
-
CANCEL_ADMIN_NOTIFY_ADMIN: 13,
|
|
254
|
-
TRANSFER_GROUP_NOTIFY_OLDOWNER: 14,
|
|
255
|
-
TRANSFER_GROUP_NOTIFY_ADMIN: 15
|
|
256
|
-
};
|
|
257
|
-
/** 群通知处理状态(GroupNotifyMsgStatus,说明书参考)。 */
|
|
258
|
-
const GroupNotifyMsgStatus = {
|
|
259
|
-
KINIT: 0,
|
|
260
|
-
KUNHANDLE: 1,
|
|
261
|
-
KAGREED: 2,
|
|
262
|
-
KREFUSED: 3,
|
|
263
|
-
KIGNORED: 4
|
|
264
|
-
};
|
|
265
|
-
/** 群请求操作类型(NTGroupRequestOperateTypes,说明书参考)。 */
|
|
266
|
-
const NTGroupRequestOperateTypes = {
|
|
267
|
-
KUNSPECIFIED: 0,
|
|
268
|
-
KAGREE: 1,
|
|
269
|
-
KREFUSE: 2,
|
|
270
|
-
KIGNORE: 3,
|
|
271
|
-
KDELETE: 4
|
|
272
|
-
};
|
|
170
|
+
/** 默认 redact 路径:顶层 token/票据 + 任意深度的密码/密钥。 */
|
|
171
|
+
const DEFAULT_REDACT_PATHS = [
|
|
172
|
+
"token",
|
|
173
|
+
"ticket",
|
|
174
|
+
"cookie",
|
|
175
|
+
"password",
|
|
176
|
+
"secret"
|
|
177
|
+
];
|
|
178
|
+
function toSingle(streams) {
|
|
179
|
+
const [only] = streams;
|
|
180
|
+
if (only === void 0) throw new KernelError("logger 内部错误:目的地列表为空", "UNKNOWN");
|
|
181
|
+
return only.stream;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* 创建 pino logger:console(pretty)+ 可选文件,统一 level / redact。
|
|
185
|
+
*
|
|
186
|
+
* 文件目的地用同步写入(`sync: true`):日志量级下性能可接受,且进程退出不丢日志,
|
|
187
|
+
* 无需额外的 flush 步骤(对比异步 SonicBoom 的 flushSync 时序陷阱)。
|
|
188
|
+
*/
|
|
189
|
+
function createLogger(opts = {}) {
|
|
190
|
+
const level = opts.level ?? "info";
|
|
191
|
+
const consoleEnabled = opts.console ?? true;
|
|
192
|
+
const { file, base } = opts;
|
|
193
|
+
const streams = [];
|
|
194
|
+
if (consoleEnabled) streams.push({ stream: pinoPretty({
|
|
195
|
+
colorize: true,
|
|
196
|
+
translateTime: "SYS:HH:MM:ss.l",
|
|
197
|
+
singleLine: false,
|
|
198
|
+
destination: process.stdout
|
|
199
|
+
}) });
|
|
200
|
+
if (file) {
|
|
201
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
202
|
+
streams.push({ stream: pino.destination({
|
|
203
|
+
dest: file,
|
|
204
|
+
sync: true
|
|
205
|
+
}) });
|
|
206
|
+
}
|
|
207
|
+
if (streams.length === 0) throw new KernelError("logger 至少需要一个输出目标:console 或 file", "INVALID_PARAM");
|
|
208
|
+
const pinoOptions = {
|
|
209
|
+
level,
|
|
210
|
+
redact: [...DEFAULT_REDACT_PATHS, ...opts.redact ?? []]
|
|
211
|
+
};
|
|
212
|
+
if (base) pinoOptions.base = {
|
|
213
|
+
pid: process.pid,
|
|
214
|
+
...base
|
|
215
|
+
};
|
|
216
|
+
let stream;
|
|
217
|
+
if (streams.length === 1) stream = toSingle(streams);
|
|
218
|
+
else stream = pino.multistream(streams);
|
|
219
|
+
return pino(pinoOptions, stream);
|
|
220
|
+
}
|
|
273
221
|
//#endregion
|
|
274
|
-
//#region src/
|
|
222
|
+
//#region src/infra/paths.ts
|
|
275
223
|
/**
|
|
276
|
-
*
|
|
224
|
+
* 数据目录布局(ADR-016,2026-08-08 修订:数据根默认放项目根)
|
|
277
225
|
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
226
|
+
* ```
|
|
227
|
+
* <数据根>/<qq号>/ # 每账号独立目录(ADR-015 多账号前提)
|
|
228
|
+
* ├── config/ # 各账号独立配置(历史产物,协议段已走 seed)
|
|
229
|
+
* ├── logs/ # pino 文件日志
|
|
230
|
+
* └── cache/ # 临时文件、媒体缓存
|
|
231
|
+
*
|
|
232
|
+
* <项目根>/napuketto.toml # 全局配置文件(放项目根目录,不入库)
|
|
233
|
+
* <项目根>/.napuketto/ # 数据根(默认值,2026-08-08 用户拍板:直接放项目主目录)
|
|
234
|
+
* ```
|
|
235
|
+
*
|
|
236
|
+
* 数据根优先级:cli `--data-dir`(显式参数)> `NAPKETTO_DATA` 环境变量 >
|
|
237
|
+
* `<项目根>/.napuketto`(默认,部署原因不放用户目录)。
|
|
238
|
+
* 配置文件路径见 `resolveConfigPath`(项目根探测,与数据根解耦)。
|
|
281
239
|
*/
|
|
282
|
-
/**
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
240
|
+
/** 默认数据根目录名(项目根下,2026-08-08 用户拍板:不放用户目录)。 */
|
|
241
|
+
const DEFAULT_DATA_ROOT_NAME = ".napuketto";
|
|
242
|
+
/** 全局配置文件名。 */
|
|
243
|
+
const MAIN_CONFIG_FILE = "napuketto.toml";
|
|
244
|
+
/** `~` 前缀展开为用户主目录(跨平台配置文件写法 `dataDir = "~/.napuketto"`)。 */
|
|
245
|
+
function expandTilde(p) {
|
|
246
|
+
if (p === "~") return homedir();
|
|
247
|
+
if (p.startsWith("~/") || p.startsWith("~\\")) return join(homedir(), p.slice(2));
|
|
248
|
+
return p;
|
|
286
249
|
}
|
|
287
|
-
/**
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
if (typeof result === "number" && result !== 0) unwrap$2("getGroupDetailInfo", result, errMsg);
|
|
317
|
-
if (result !== null && typeof result === "object") return result;
|
|
318
|
-
const detail = raw;
|
|
319
|
-
const candidate = detail.groupInfo ?? detail.detailInfo ?? detail.info;
|
|
320
|
-
if (candidate !== void 0 && candidate !== null && typeof candidate === "object") return candidate;
|
|
321
|
-
throw kernelError(`getGroupDetailInfo 返回形状异常(groupCode=${groupCode}): ${errMsg}`, "UNKNOWN");
|
|
322
|
-
}
|
|
323
|
-
/** 群成员列表(forceFetch=true 强制拉取,默认缓存优先)。 */
|
|
324
|
-
async getGroupMemberList(groupCode, forceFetch = false) {
|
|
325
|
-
const raw = await this.service.getAllMemberList(groupCode, forceFetch);
|
|
326
|
-
if (raw.errCode !== 0) throw kernelError(`getAllMemberList 失败: ${raw.errMsg}`, "UNKNOWN");
|
|
327
|
-
return [...raw.result.infos.values()];
|
|
328
|
-
}
|
|
329
|
-
/** 群成员详情(uids 为空返回空数组)。 */
|
|
330
|
-
async getGroupMemberInfo(groupCode, uids) {
|
|
331
|
-
if (uids.length === 0) return [];
|
|
332
|
-
const raw = await this.service.getMemberInfo(groupCode, uids, true);
|
|
333
|
-
unwrap$2("getMemberInfo", raw.result, raw.errMsg);
|
|
334
|
-
const { infos } = raw;
|
|
335
|
-
if (infos instanceof Map) return [...infos.values()];
|
|
336
|
-
return [];
|
|
337
|
-
}
|
|
338
|
-
/** uin → uid(私聊发送等需要)。 */
|
|
339
|
-
async uinToUid(uins) {
|
|
340
|
-
if (uins.length === 0) return /* @__PURE__ */ new Map();
|
|
341
|
-
const raw = await this.service.getUidByUins(uins);
|
|
342
|
-
if (raw.errCode !== 0) throw kernelError(`getUidByUins 失败: ${raw.errMsg}`, "UNKNOWN");
|
|
343
|
-
return raw.uids;
|
|
344
|
-
}
|
|
345
|
-
/** uid → uin。 */
|
|
346
|
-
async uidToUin(uids) {
|
|
347
|
-
if (uids.length === 0) return /* @__PURE__ */ new Map();
|
|
348
|
-
const raw = await this.service.getUinByUids(uids);
|
|
349
|
-
if (raw.errCode !== 0) throw kernelError(`getUinByUids 失败: ${raw.errMsg}`, "UNKNOWN");
|
|
350
|
-
return raw.uins;
|
|
351
|
-
}
|
|
352
|
-
/** 群成员角色 → OB11 role 字符串。 */
|
|
353
|
-
static roleToString(role) {
|
|
354
|
-
if (role === NTGroupMemberRole.OWNER) return "owner";
|
|
355
|
-
if (role === NTGroupMemberRole.ADMIN) return "admin";
|
|
356
|
-
return "member";
|
|
357
|
-
}
|
|
358
|
-
/**
|
|
359
|
-
* 踢人(set_group_kick)。memberUids 为成员 uid;refuseForever=true 拉黑拒绝再次入群。
|
|
360
|
-
* kickMemberV2 的 kickFlag/kickList 字段值待探测校准(TODO P3 联调)。
|
|
361
|
-
*/
|
|
362
|
-
async kickMember(groupCode, memberUids, refuseForever, reason = "") {
|
|
363
|
-
if (memberUids.length === 0) throw kernelError("kickMember 需要至少一个成员 uid", "INVALID_PARAM");
|
|
364
|
-
let optFlag = 1;
|
|
365
|
-
if (refuseForever) optFlag = 0;
|
|
366
|
-
const req = {
|
|
367
|
-
groupCode,
|
|
368
|
-
kickFlag: 0,
|
|
369
|
-
kickList: memberUids.map((uid) => ({
|
|
370
|
-
optFlag,
|
|
371
|
-
optOperate: 0,
|
|
372
|
-
optMemberUid: uid,
|
|
373
|
-
optBytesMsg: ""
|
|
374
|
-
})),
|
|
375
|
-
kickListUids: memberUids,
|
|
376
|
-
kickMsg: reason
|
|
377
|
-
};
|
|
378
|
-
const raw = await this.service.kickMemberV2(req);
|
|
379
|
-
unwrap$2("kickMemberV2", raw.result, raw.errMsg);
|
|
380
|
-
}
|
|
381
|
-
/** 成员禁言(duration 秒,0 解除禁言)。 */
|
|
382
|
-
async setMemberShutUp(groupCode, members) {
|
|
383
|
-
if (members.length === 0) throw kernelError("setMemberShutUp 需要至少一个成员", "INVALID_PARAM");
|
|
384
|
-
const raw = await this.service.setMemberShutUp(groupCode, members.map((m) => ({
|
|
385
|
-
uid: m.uid,
|
|
386
|
-
timeStamp: m.duration
|
|
387
|
-
})));
|
|
388
|
-
unwrap$2("setMemberShutUp", raw.result, raw.errMsg);
|
|
389
|
-
}
|
|
390
|
-
/** 全员禁言(enable=true 开启全群禁言)。 */
|
|
391
|
-
async setGroupShutUp(groupCode, enable) {
|
|
392
|
-
const raw = await this.service.setGroupShutUp(groupCode, enable);
|
|
393
|
-
unwrap$2("setGroupShutUp", raw.result, raw.errMsg);
|
|
394
|
-
}
|
|
395
|
-
/** 设置管理员(role=ADMIN 设管理,MEMBER 取消;void 语义乐观处理)。 */
|
|
396
|
-
setMemberRole(groupCode, uid, role) {
|
|
397
|
-
this.service.modifyMemberRole(groupCode, uid, role);
|
|
398
|
-
}
|
|
399
|
-
/** 设置群名片(void 语义乐观处理)。 */
|
|
400
|
-
setMemberCardName(groupCode, uid, cardName) {
|
|
401
|
-
this.service.modifyMemberCardName(groupCode, uid, cardName);
|
|
402
|
-
}
|
|
403
|
-
/** 修改群名。 */
|
|
404
|
-
async modifyGroupName(groupCode, groupName) {
|
|
405
|
-
const raw = await this.service.modifyGroupName(groupCode, groupName, false);
|
|
406
|
-
unwrap$2("modifyGroupName", raw.result, raw.errMsg);
|
|
407
|
-
}
|
|
408
|
-
/** 退群(needDeleteLocalMsg=is_dismiss:是否解散群,仅群主)。 */
|
|
409
|
-
async quitGroup(groupCode, needDeleteLocalMsg = false) {
|
|
410
|
-
const raw = await this.service.quitGroupV2({
|
|
411
|
-
groupCode,
|
|
412
|
-
needDeleteLocalMsg
|
|
413
|
-
});
|
|
414
|
-
unwrap$2("quitGroupV2", raw.result, raw.errMsg);
|
|
415
|
-
}
|
|
416
|
-
/** 设置精华消息(msgId 经 msgService 取 msgSeq/msgRandom)。 */
|
|
417
|
-
async addGroupEssence(groupCode, msgId) {
|
|
418
|
-
await this.setEssence(groupCode, msgId, true);
|
|
250
|
+
/**
|
|
251
|
+
* 项目根查找:入口模块向上找 `pnpm-workspace.yaml`(monorepo 开发场景,即仓库根)→
|
|
252
|
+
* cwd 向上找 `pnpm-workspace.yaml` / `package.json`(发布后用户项目)→ undefined(兜底)。
|
|
253
|
+
*/
|
|
254
|
+
function resolveProjectRoot() {
|
|
255
|
+
const moduleRoot = findProjectRoot(dirname(fileURLToPath(import.meta.url)), "pnpm-workspace.yaml");
|
|
256
|
+
if (moduleRoot !== void 0) return moduleRoot;
|
|
257
|
+
return findProjectRoot(process.cwd(), "pnpm-workspace.yaml") ?? findProjectRoot(process.cwd(), "package.json");
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* 解析数据根:显式参数 > 环境变量 > `<项目根>/.napuketto`(默认)> 用户目录兜底(裸脚本)。
|
|
261
|
+
* 支持 `~` 前缀(展开为用户主目录)与相对路径(相对 cwd 解析)。
|
|
262
|
+
* 返回绝对路径。独立导出便于 cli / 探测脚本复用。
|
|
263
|
+
*/
|
|
264
|
+
function resolveDataRoot(dataRoot) {
|
|
265
|
+
const explicit = dataRoot ?? process.env["NAPKETTO_DATA"];
|
|
266
|
+
if (explicit !== void 0 && explicit !== "") return resolve(expandTilde(explicit));
|
|
267
|
+
const projectRoot = resolveProjectRoot();
|
|
268
|
+
if (projectRoot !== void 0) return join(projectRoot, DEFAULT_DATA_ROOT_NAME);
|
|
269
|
+
return join(homedir(), DEFAULT_DATA_ROOT_NAME);
|
|
270
|
+
}
|
|
271
|
+
/** 向上查找含标志文件的目录(项目根探测:monorepo 根 / 用户项目根)。 */
|
|
272
|
+
function findProjectRoot(startDir, marker) {
|
|
273
|
+
let dir = startDir;
|
|
274
|
+
for (;;) {
|
|
275
|
+
if (existsSync(join(dir, marker))) return dir;
|
|
276
|
+
const parent = dirname(dir);
|
|
277
|
+
if (parent === dir) return;
|
|
278
|
+
dir = parent;
|
|
419
279
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* 全局配置文件路径解析(2026-08-07 用户拍板:**配置文件放项目根目录,
|
|
283
|
+
* 数据仍按数据根组织**——数据根只承载账号目录/日志/缓存/QQ 数据)。
|
|
284
|
+
*
|
|
285
|
+
* 优先级:
|
|
286
|
+
* 1. `NAPKETTO_CONFIG` 环境变量(显式指定完整路径,任意场景可用)
|
|
287
|
+
* 2. 项目根探测(入口模块向上找 pnpm-workspace.yaml → cwd 向上找,见 resolveProjectRoot)
|
|
288
|
+
* 3. 数据根兜底 `<dataRoot>/napuketto.toml`(旧行为兼容)
|
|
289
|
+
*
|
|
290
|
+
* 返回绝对路径。独立导出便于 cli / loader 装配链复用。
|
|
291
|
+
*/
|
|
292
|
+
function resolveConfigPath(opts = {}) {
|
|
293
|
+
const explicit = process.env["NAPKETTO_CONFIG"];
|
|
294
|
+
if (explicit !== void 0 && explicit !== "") return resolve(explicit);
|
|
295
|
+
const projectRoot = resolveProjectRoot();
|
|
296
|
+
if (projectRoot !== void 0) return join(projectRoot, MAIN_CONFIG_FILE);
|
|
297
|
+
return join(opts.dataRoot ?? resolveDataRoot(), MAIN_CONFIG_FILE);
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* 路径装配器:持有账号数据目录下的全部子目录。
|
|
301
|
+
* 无全局单例(ADR-015 推论)——每账号每进程实例化一份。
|
|
302
|
+
*/
|
|
303
|
+
var PathWrapper = class {
|
|
304
|
+
/** 数据根(绝对路径)。 */
|
|
305
|
+
root;
|
|
306
|
+
/** 账号数据目录(绝对路径),缺省 account 时等于 root。 */
|
|
307
|
+
accountDir;
|
|
308
|
+
/** config 目录(napuketto.json + 各协议配置)。 */
|
|
309
|
+
configDir;
|
|
310
|
+
/** logs 目录(pino 文件日志)。 */
|
|
311
|
+
logsDir;
|
|
312
|
+
/** cache 目录(临时文件、媒体缓存)。 */
|
|
313
|
+
cacheDir;
|
|
314
|
+
constructor(opts = {}) {
|
|
315
|
+
const root = resolveDataRoot(opts.dataRoot);
|
|
316
|
+
this.root = root;
|
|
317
|
+
if (opts.account) this.accountDir = join(root, opts.account);
|
|
318
|
+
else this.accountDir = root;
|
|
319
|
+
this.configDir = join(this.accountDir, "config");
|
|
320
|
+
this.logsDir = join(this.accountDir, "logs");
|
|
321
|
+
this.cacheDir = join(this.accountDir, "cache");
|
|
423
322
|
}
|
|
424
|
-
/**
|
|
425
|
-
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
unwrap$2("getMsgsByMsgId", raw.result, raw.errMsg);
|
|
432
|
-
const msg = raw.msgList?.[0];
|
|
433
|
-
if (msg === void 0) throw kernelError(`消息 ${msgId} 不存在`, "NOT_FOUND");
|
|
434
|
-
const param = {
|
|
435
|
-
groupCode,
|
|
436
|
-
msgRandom: Number(msg.msgRandom ?? 0),
|
|
437
|
-
msgSeq: Number(msg.msgSeq)
|
|
438
|
-
};
|
|
439
|
-
let label = "removeGroupEssence";
|
|
440
|
-
let res;
|
|
441
|
-
if (add) {
|
|
442
|
-
label = "addGroupEssence";
|
|
443
|
-
res = await this.service.addGroupEssence(param);
|
|
444
|
-
} else res = await this.service.removeGroupEssence(param);
|
|
445
|
-
if (res !== void 0 && res !== null && typeof res.result === "number" && res.result !== 0) throw kernelError(`${label} 失败: ${String(res.errMsg ?? "")}`, "UNKNOWN");
|
|
323
|
+
/** 递归创建全部目录,启动装配时调用一次。 */
|
|
324
|
+
ensure() {
|
|
325
|
+
for (const dir of [
|
|
326
|
+
this.configDir,
|
|
327
|
+
this.logsDir,
|
|
328
|
+
this.cacheDir
|
|
329
|
+
]) mkdirSync(dir, { recursive: true });
|
|
446
330
|
}
|
|
447
|
-
/**
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
331
|
+
/** 账号目录内的文件路径(如 `file('config', 'napuketto.json')`)。 */
|
|
332
|
+
file(...segments) {
|
|
333
|
+
return join(this.accountDir, ...segments);
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* 清空缓存目录(clean_cache 动作经注入回调消费)。
|
|
337
|
+
* 只删除 cacheDir 下的条目,保留目录本身。
|
|
338
|
+
*/
|
|
339
|
+
clearCache() {
|
|
340
|
+
let entries;
|
|
341
|
+
try {
|
|
342
|
+
entries = readdirSync(this.cacheDir);
|
|
343
|
+
} catch {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
for (const entry of entries) rmSync(join(this.cacheDir, entry), {
|
|
347
|
+
recursive: true,
|
|
348
|
+
force: true
|
|
349
|
+
});
|
|
456
350
|
}
|
|
457
351
|
};
|
|
458
352
|
//#endregion
|
|
459
|
-
//#region src/apis/
|
|
353
|
+
//#region src/apis/result.ts
|
|
460
354
|
/**
|
|
461
|
-
*
|
|
355
|
+
* 原生调用结果解包(ADR-009 统一错误语义)
|
|
462
356
|
*
|
|
463
|
-
*
|
|
464
|
-
*
|
|
465
|
-
*
|
|
357
|
+
* wrapper.node 的 service 方法大多返回 `{ result, errMsg }`:
|
|
358
|
+
* - unwrap:result 为 number 且非 0 → 抛 KernelError
|
|
359
|
+
* - checkLooseResult:宽松形状(result 可能缺失/非数字)→ 仅当 result 为
|
|
360
|
+
* 数字且非 0 时抛错(探测期兼容:某些 API 成功时 result 缺省或为对象)
|
|
361
|
+
* - unwrapResult:result !== 0 → 按 errMsg 内容映射语义错误码(msg 系专用)
|
|
466
362
|
*/
|
|
363
|
+
/** errMsg 关键词 → 错误码规则(顺序即优先级,命中任一组关键词即映射)。 */
|
|
364
|
+
const RESULT_CODE_RULES = [
|
|
365
|
+
{
|
|
366
|
+
code: "NOT_LOGIN",
|
|
367
|
+
keywords: ["未登录", "login"]
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
code: "PERMISSION_DENIED",
|
|
371
|
+
keywords: ["无权限", "permission"]
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
code: "NOT_FOUND",
|
|
375
|
+
keywords: ["不存在", "not found"]
|
|
376
|
+
}
|
|
377
|
+
];
|
|
378
|
+
/** 按 errMsg 关键词映射错误码(未命中 → UNKNOWN)。 */
|
|
379
|
+
function mapResultCode(msg) {
|
|
380
|
+
for (const rule of RESULT_CODE_RULES) if (rule.keywords.some((k) => msg.includes(k))) return rule.code;
|
|
381
|
+
return "UNKNOWN";
|
|
382
|
+
}
|
|
467
383
|
/** 原生 result 解包(result 字段非 0 抛 KernelError)。 */
|
|
468
|
-
function unwrap
|
|
384
|
+
function unwrap(label, result, errMsg) {
|
|
469
385
|
if (result === 0) return;
|
|
470
386
|
throw kernelError(`${label} 失败: ${errMsg ?? "无错误详情"}`, "UNKNOWN");
|
|
471
387
|
}
|
|
472
|
-
/**
|
|
473
|
-
|
|
388
|
+
/** 宽松 result 校验(result 为数字非 0 视为失败;缺失/非数字视为成功)。 */
|
|
389
|
+
function checkLooseResult(label, res) {
|
|
390
|
+
if (res === void 0 || res === null || typeof res.result !== "number" || res.result === 0) return;
|
|
391
|
+
throw kernelError(`${label} 失败: ${String(res.errMsg ?? "")}`, "UNKNOWN");
|
|
392
|
+
}
|
|
393
|
+
/** 原生结果解包:result !== 0 抛 KernelError(errMsg 语义映射错误码)。 */
|
|
394
|
+
function unwrapResult(label, raw) {
|
|
395
|
+
if (raw.result === 0) return;
|
|
396
|
+
const msg = raw.errMsg || "无错误详情";
|
|
397
|
+
let code = mapResultCode(msg);
|
|
398
|
+
if (code === "UNKNOWN" && label === "sendMsg") code = "SEND_FAILED";
|
|
399
|
+
throw kernelError(`${label} 失败: ${msg}`, code);
|
|
400
|
+
}
|
|
401
|
+
//#endregion
|
|
402
|
+
//#region src/apis/friend.ts
|
|
403
|
+
/**
|
|
404
|
+
* FriendApi:好友语义化 API(ADR-009 统一错误语义)
|
|
405
|
+
*
|
|
406
|
+
* 内部解包原生返回:成功返回纯业务值,失败抛 KernelError。
|
|
407
|
+
* 方法面(P2-4 + P2-11):好友列表(uid + 昵称/备注)+ 好友请求
|
|
408
|
+
* (getBuddyReq / approvalFriendRequest / setBuddyRemark / delBuddy /
|
|
409
|
+
* getDoubtBuddyReq / approvalDoubtBuddyReq)。
|
|
410
|
+
*/
|
|
411
|
+
/** 好友 API:从 session 拿 buddy service,包装成语义化方法。 */
|
|
412
|
+
var FriendApi = class {
|
|
474
413
|
service;
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
414
|
+
uidToUin;
|
|
415
|
+
constructor(session, opts = {}) {
|
|
416
|
+
const service = session.getBuddyService();
|
|
417
|
+
if (service === null || service === void 0) throw kernelError("getBuddyService() 返回空(session 未 init)", "INVALID_STATE");
|
|
478
418
|
this.service = service;
|
|
419
|
+
this.uidToUin = opts.uidToUin;
|
|
479
420
|
}
|
|
480
|
-
/**
|
|
481
|
-
async
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
421
|
+
/** 好友列表(分类拍平为 Friend[];昵称/备注从 service 缓存读取)。 */
|
|
422
|
+
async getFriendList() {
|
|
423
|
+
let categories;
|
|
424
|
+
try {
|
|
425
|
+
categories = await this.service.getBuddyListFromCache(0);
|
|
426
|
+
} catch {
|
|
427
|
+
const raw = await this.service.getBuddyListV2("cli", true, 0);
|
|
428
|
+
if (raw.result !== 0) throw kernelError(`getBuddyListV2 失败: ${raw.errMsg}`, "UNKNOWN");
|
|
429
|
+
categories = raw.data;
|
|
430
|
+
}
|
|
431
|
+
const uids = categories.flatMap((cat) => cat.buddyUids);
|
|
432
|
+
let uinMap = /* @__PURE__ */ new Map();
|
|
433
|
+
if (this.uidToUin !== void 0) uinMap = await this.uidToUin(uids);
|
|
434
|
+
const out = [];
|
|
435
|
+
for (const uid of uids) out.push({
|
|
436
|
+
uid,
|
|
437
|
+
uin: uinMap.get(uid) ?? uid,
|
|
438
|
+
nickname: this.service.getBuddyNick(uid),
|
|
439
|
+
remark: this.service.getBuddyRemark(uid)
|
|
440
|
+
});
|
|
441
|
+
return out;
|
|
489
442
|
}
|
|
490
|
-
/**
|
|
491
|
-
async
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
seq,
|
|
501
|
-
type,
|
|
502
|
-
groupCode,
|
|
503
|
-
postscript
|
|
443
|
+
/** 带分类的好友列表(get_friends_with_category,分类保留)。 */
|
|
444
|
+
async getFriendCategories() {
|
|
445
|
+
const categories = await this.getBuddyCategories();
|
|
446
|
+
const friendMap = /* @__PURE__ */ new Map();
|
|
447
|
+
for (const friend of await this.getFriendList()) friendMap.set(friend.uid, friend);
|
|
448
|
+
return categories.map((cat) => {
|
|
449
|
+
const buddies = [];
|
|
450
|
+
for (const uid of cat.buddyUids) {
|
|
451
|
+
const friend = friendMap.get(uid);
|
|
452
|
+
if (friend !== void 0) buddies.push(friend);
|
|
504
453
|
}
|
|
454
|
+
return {
|
|
455
|
+
categoryId: cat.categoryId,
|
|
456
|
+
categorySortId: cat.categorySortId,
|
|
457
|
+
categoryName: cat.categroyName,
|
|
458
|
+
categoryMbCount: cat.categroyMbCount,
|
|
459
|
+
onlineCount: cat.onlineCount,
|
|
460
|
+
buddyList: buddies
|
|
461
|
+
};
|
|
505
462
|
});
|
|
506
463
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
464
|
+
/** 原始分类(getBuddyListV2 / 缓存)。 */
|
|
465
|
+
async getBuddyCategories() {
|
|
466
|
+
try {
|
|
467
|
+
return await this.service.getBuddyListFromCache(0);
|
|
468
|
+
} catch {
|
|
469
|
+
const raw = await this.service.getBuddyListV2("cli", true, 0);
|
|
470
|
+
if (raw.result !== 0) throw kernelError(`getBuddyListV2 失败: ${raw.errMsg}`, "UNKNOWN");
|
|
471
|
+
return raw.data;
|
|
472
|
+
}
|
|
515
473
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
function extractMemberList(result) {
|
|
520
|
-
if (Array.isArray(result)) return result;
|
|
521
|
-
if (result !== null && typeof result === "object") {
|
|
522
|
-
const obj = result;
|
|
523
|
-
if (Array.isArray(obj.memberList)) return obj.memberList;
|
|
524
|
-
if (Array.isArray(obj.list)) return obj.list;
|
|
474
|
+
/** 判断是否好友。 */
|
|
475
|
+
isBuddy(uid) {
|
|
476
|
+
return this.service.isBuddy(uid);
|
|
525
477
|
}
|
|
526
|
-
|
|
527
|
-
|
|
478
|
+
/** 好友申请列表(set_friend_add_request 应答前查找匹配项)。 */
|
|
479
|
+
async getBuddyReqList() {
|
|
480
|
+
const raw = await this.service.getBuddyReq();
|
|
481
|
+
unwrap("getBuddyReq", raw.result, raw.errMsg);
|
|
482
|
+
return raw.buddyReqs ?? [];
|
|
483
|
+
}
|
|
484
|
+
/** 应答好友申请(accept=true 同意;remark 由调用方另行 setFriendRemark)。 */
|
|
485
|
+
async handleFriendRequest(notify, accept) {
|
|
486
|
+
await this.service.approvalFriendRequest({
|
|
487
|
+
friendUid: notify.friendUid,
|
|
488
|
+
reqTime: notify.reqTime,
|
|
489
|
+
accept
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
/** 设置好友备注(void 语义乐观处理)。 */
|
|
493
|
+
setFriendRemark(uid, remark) {
|
|
494
|
+
this.service.setBuddyRemark({
|
|
495
|
+
uid,
|
|
496
|
+
remark
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
/** 删除好友(tempBlock=拉黑,tempBothDel=双向删除)。 */
|
|
500
|
+
async deleteFriend(uid, tempBlock = false, tempBothDel = false) {
|
|
501
|
+
checkLooseResult("delBuddy", await this.service.delBuddy({
|
|
502
|
+
friendUid: uid,
|
|
503
|
+
tempBlock,
|
|
504
|
+
tempBothDel
|
|
505
|
+
}));
|
|
506
|
+
}
|
|
507
|
+
/** 可疑好友申请列表(get_doubt_friends_add_request;uin 经 uidToUin 转换)。 */
|
|
508
|
+
async getDoubtFriendRequest(count) {
|
|
509
|
+
const reqId = String(Date.now());
|
|
510
|
+
const raw = await this.service.getDoubtBuddyReq(reqId, count, "");
|
|
511
|
+
unwrap("getDoubtBuddyReq", raw.result, raw.errMsg);
|
|
512
|
+
const list = raw.doubtList ?? [];
|
|
513
|
+
let uinMap = /* @__PURE__ */ new Map();
|
|
514
|
+
if (this.uidToUin !== void 0 && list.length > 0) uinMap = await this.uidToUin(list.map((item) => item.uid));
|
|
515
|
+
const out = [];
|
|
516
|
+
for (const item of list) out.push(this.toDoubtFriendRequestInfo(item, uinMap));
|
|
517
|
+
return out;
|
|
518
|
+
}
|
|
519
|
+
/** 单项转换:DoubtBuddyReq → OB11 结构(可选字段条件赋值,exactOptionalPropertyTypes)。 */
|
|
520
|
+
toDoubtFriendRequestInfo(item, uinMap) {
|
|
521
|
+
const info = {
|
|
522
|
+
flag: item.uid,
|
|
523
|
+
uin: Number(uinMap.get(item.uid) ?? item.uid),
|
|
524
|
+
type: "doubt"
|
|
525
|
+
};
|
|
526
|
+
if (item.nick !== void 0) info.nick = item.nick;
|
|
527
|
+
if (item.source !== void 0) info.source = item.source;
|
|
528
|
+
if (item.reason !== void 0) info.reason = item.reason;
|
|
529
|
+
if (item.msg !== void 0) info.msg = item.msg;
|
|
530
|
+
if (item.groupCode !== void 0) info.group_code = item.groupCode;
|
|
531
|
+
if (item.reqTime !== void 0) info.time = item.reqTime;
|
|
532
|
+
return info;
|
|
533
|
+
}
|
|
534
|
+
/** 处理可疑好友申请(set_doubt_friends_add_request;uid=flag)。 */
|
|
535
|
+
handleDoubtFriendRequest(uid) {
|
|
536
|
+
this.service.approvalDoubtBuddyReq(uid, "", "");
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
//#endregion
|
|
540
|
+
//#region src/types/entities.ts
|
|
541
|
+
/**
|
|
542
|
+
* 实体类型层(运行时探测产物 + 公开资料作说明书,ADR-006)
|
|
543
|
+
*
|
|
544
|
+
* ⚠️ 部分字段按公开资料(wrapper 外部契约)自研描述,待下一次进程内
|
|
545
|
+
* 探测(probe.ts)校准——接口签名是外部系统事实,自研描述其形状。
|
|
546
|
+
*/
|
|
547
|
+
/** 会话类型(QQ wrapper 外部契约)。 */
|
|
548
|
+
const ChatType = {
|
|
549
|
+
C2C: 1,
|
|
550
|
+
GROUP: 2,
|
|
551
|
+
DISC: 3,
|
|
552
|
+
GUILD: 4,
|
|
553
|
+
C2C_TEMP: 100
|
|
554
|
+
};
|
|
555
|
+
/** 灰色提示子类型(系统事件类型,QQ wrapper 契约)。 */
|
|
556
|
+
const GrayTipSubType = {
|
|
557
|
+
REVOKE: 1,
|
|
558
|
+
PROCLAMATION: 2,
|
|
559
|
+
GROUP: 4,
|
|
560
|
+
BUDDY: 5,
|
|
561
|
+
FEED: 6,
|
|
562
|
+
ESSENCE: 7,
|
|
563
|
+
GROUP_NOTIFY: 8,
|
|
564
|
+
BUDDY_NOTIFY: 9,
|
|
565
|
+
FILE: 10,
|
|
566
|
+
JSON: 17
|
|
567
|
+
};
|
|
568
|
+
/** 群成员变动类型(TipGroupElement.type)。 */
|
|
569
|
+
const TipGroupElementType = {
|
|
570
|
+
MEMBER_ADD: 1,
|
|
571
|
+
DISBANDED: 2,
|
|
572
|
+
QUIT: 3,
|
|
573
|
+
CREATED: 4,
|
|
574
|
+
GROUP_NAME_MODIFIED: 5,
|
|
575
|
+
BLOCK: 6,
|
|
576
|
+
UNBLOCK: 7,
|
|
577
|
+
SHUT_UP: 8
|
|
578
|
+
};
|
|
579
|
+
//#endregion
|
|
580
|
+
//#region src/types/listeners/group.ts
|
|
581
|
+
/** 群列表更新类型(说明书参考,值待探测校准)。 */
|
|
582
|
+
const GroupListUpdateType = {
|
|
583
|
+
/** 全量更新(groupList 为完整列表)。 */
|
|
584
|
+
ALL: 0,
|
|
585
|
+
/** 增量更新(合并到现有缓存)。 */
|
|
586
|
+
PARTIAL: 1
|
|
587
|
+
};
|
|
528
588
|
//#endregion
|
|
529
589
|
//#region src/types/services/msg-service.ts
|
|
530
590
|
/** 消息元素类型枚举(QQ wrapper 外部契约,自研描述)。 */
|
|
@@ -762,7 +822,332 @@ function toSendElements(elements) {
|
|
|
762
822
|
case "xml":
|
|
763
823
|
case "unknown": throw kernelError(`发送元素 ${el.type} 暂不支持(P2-2 探测后补)`, "INVALID_PARAM");
|
|
764
824
|
}
|
|
765
|
-
return out;
|
|
825
|
+
return out;
|
|
826
|
+
}
|
|
827
|
+
//#endregion
|
|
828
|
+
//#region src/types/services/group-service.ts
|
|
829
|
+
/** 群成员角色(QQ wrapper 契约)。 */
|
|
830
|
+
const NTGroupMemberRole = {
|
|
831
|
+
UNSPECIFIED: 0,
|
|
832
|
+
STRANGER: 1,
|
|
833
|
+
MEMBER: 2,
|
|
834
|
+
ADMIN: 3,
|
|
835
|
+
OWNER: 4
|
|
836
|
+
};
|
|
837
|
+
/** 群通知类型(GroupNotifyMsgType,说明书参考)。 */
|
|
838
|
+
const GroupNotifyMsgType = {
|
|
839
|
+
UN_SPECIFIED: 0,
|
|
840
|
+
INVITED_BY_MEMBER: 1,
|
|
841
|
+
REFUSE_INVITED: 2,
|
|
842
|
+
REFUSED_BY_ADMINI_STRATOR: 3,
|
|
843
|
+
AGREED_TOJOIN_DIRECT: 4,
|
|
844
|
+
INVITED_NEED_ADMINI_STRATOR_PASS: 5,
|
|
845
|
+
AGREED_TO_JOIN_BY_ADMINI_STRATOR: 6,
|
|
846
|
+
REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS: 7,
|
|
847
|
+
SET_ADMIN: 8,
|
|
848
|
+
KICK_MEMBER_NOTIFY_ADMIN: 9,
|
|
849
|
+
KICK_MEMBER_NOTIFY_KICKED: 10,
|
|
850
|
+
MEMBER_LEAVE_NOTIFY_ADMIN: 11,
|
|
851
|
+
CANCEL_ADMIN_NOTIFY_CANCELED: 12,
|
|
852
|
+
CANCEL_ADMIN_NOTIFY_ADMIN: 13,
|
|
853
|
+
TRANSFER_GROUP_NOTIFY_OLDOWNER: 14,
|
|
854
|
+
TRANSFER_GROUP_NOTIFY_ADMIN: 15
|
|
855
|
+
};
|
|
856
|
+
/** 群通知处理状态(GroupNotifyMsgStatus,说明书参考)。 */
|
|
857
|
+
const GroupNotifyMsgStatus = {
|
|
858
|
+
KINIT: 0,
|
|
859
|
+
KUNHANDLE: 1,
|
|
860
|
+
KAGREED: 2,
|
|
861
|
+
KREFUSED: 3,
|
|
862
|
+
KIGNORED: 4
|
|
863
|
+
};
|
|
864
|
+
/** 群请求操作类型(NTGroupRequestOperateTypes,说明书参考)。 */
|
|
865
|
+
const NTGroupRequestOperateTypes = {
|
|
866
|
+
KUNSPECIFIED: 0,
|
|
867
|
+
KAGREE: 1,
|
|
868
|
+
KREFUSE: 2,
|
|
869
|
+
KIGNORE: 3,
|
|
870
|
+
KDELETE: 4
|
|
871
|
+
};
|
|
872
|
+
//#endregion
|
|
873
|
+
//#region src/types/wrapper.ts
|
|
874
|
+
/**
|
|
875
|
+
* wrapper.node 的 NAPI 接口面(自研描述,非移植)
|
|
876
|
+
*
|
|
877
|
+
* 来源:wrapper.node 在 QQ 定制版 Electron 里通过 napi_define_class / napi_set_named_property
|
|
878
|
+
* 注册的 JS 类(2026-08-05 Ghidra 反编译 FUN_180c6274a / FUN_180c80224 确认类名)。
|
|
879
|
+
* 这些接口是 wrapper.node 对外暴露的外部系统契约——我们自研描述其形状,未复制任何第三方代码。
|
|
880
|
+
*
|
|
881
|
+
* 注意:这里所有对象由 QQ 运行时的 NAPI 层自动构建/转换,业务层禁止触碰 C++ ABI。
|
|
882
|
+
*/
|
|
883
|
+
/** 平台类型(与 wrapper.node 的 platform 字段对应)。 */
|
|
884
|
+
const PlatformType = {
|
|
885
|
+
KUNKNOWN: 0,
|
|
886
|
+
KANDROID: 1,
|
|
887
|
+
KIOS: 2,
|
|
888
|
+
KWINDOWS: 3,
|
|
889
|
+
KMAC: 4,
|
|
890
|
+
KLINUX: 5
|
|
891
|
+
};
|
|
892
|
+
/** 厂商类型(deviceInfo.vendorType)。 */
|
|
893
|
+
const VendorType = {
|
|
894
|
+
KNOSETONIOS: 0,
|
|
895
|
+
KUNSUPPORTANDROIDPUSH: 1,
|
|
896
|
+
KSUPPORTTPNS: 2,
|
|
897
|
+
KSUPPORTHMS: 3,
|
|
898
|
+
KSUPPORTOPPOPUSH: 4,
|
|
899
|
+
KSUPPORTVIVOPUSH: 5,
|
|
900
|
+
KSUPPORTGOOGLEPUSH: 99
|
|
901
|
+
};
|
|
902
|
+
//#endregion
|
|
903
|
+
//#region src/apis/group.ts
|
|
904
|
+
/**
|
|
905
|
+
* GroupApi:群语义化 API(ADR-009 统一错误语义)
|
|
906
|
+
*
|
|
907
|
+
* 内部解包原生返回:成功返回纯业务值,失败抛 KernelError。
|
|
908
|
+
* 方法面(P2-4 + P2-10):群列表 / 群详情 / 成员列表 / 成员详情 / uin↔uid +
|
|
909
|
+
* 群管操作(踢人 / 禁言 / 管理员 / 名片 / 群名 / 退群 / 精华消息 / @all 剩余)。
|
|
910
|
+
*/
|
|
911
|
+
/** 群 API:从 session 拿 group service,包装成语义化方法。 */
|
|
912
|
+
var GroupApi = class {
|
|
913
|
+
service;
|
|
914
|
+
/** 精华消息需要 msg service(按 msgId 拉消息取 msgSeq/msgRandom)。 */
|
|
915
|
+
msgService;
|
|
916
|
+
constructor(session) {
|
|
917
|
+
const service = session.getGroupService();
|
|
918
|
+
if (service === null || service === void 0) throw kernelError("getGroupService() 返回空(session 未 init)", "INVALID_STATE");
|
|
919
|
+
this.service = service;
|
|
920
|
+
const msgService = session.getMsgService();
|
|
921
|
+
if (msgService === null || msgService === void 0) throw kernelError("getMsgService() 返回空(session 未 init)", "INVALID_STATE");
|
|
922
|
+
this.msgService = msgService;
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* 群列表(force=true 触发原生刷新)。
|
|
926
|
+
*
|
|
927
|
+
* ⚠️ 实测校准(2026-08-08 端到端联调):原生 getGroupList 返回仅
|
|
928
|
+
* `{ result, errMsg }`——**列表数据经 onGroupListUpdate 事件推送**,
|
|
929
|
+
* 不走返回值。消费群列表一律从 `GroupCache.listGroups()` 读(缓存由
|
|
930
|
+
* 事件主动维护);本方法仅作「触发刷新」用,返回值恒为 []。
|
|
931
|
+
*/
|
|
932
|
+
async getGroupList(force = false) {
|
|
933
|
+
const raw = await this.service.getGroupList(force);
|
|
934
|
+
if (raw.result !== 0) unwrap("getGroupList", raw.result, raw.errMsg);
|
|
935
|
+
return [];
|
|
936
|
+
}
|
|
937
|
+
/** 群详情(groupCode 为群号字符串)。
|
|
938
|
+
* getGroupDetailInfo 返回形状:失败 result=错误码;成功详情可能直接在 result
|
|
939
|
+
* 对象里,也可能在 groupInfo/detailInfo 字段——三种形状都兼容(探测校准)。
|
|
940
|
+
* 2026-08-07 修复:成功(result=0)时不再把 {result:0,errMsg} 原样当详情返回,
|
|
941
|
+
* 而是提取详情字段;形状异常抛 UNKNOWN 而非静默返回垃圾数据。 */
|
|
942
|
+
async getGroupInfo(groupCode) {
|
|
943
|
+
const raw = await this.service.getGroupDetailInfo(groupCode, 0);
|
|
944
|
+
const { result, errMsg } = raw;
|
|
945
|
+
if (typeof result === "number" && result !== 0) unwrap("getGroupDetailInfo", result, errMsg);
|
|
946
|
+
if (result !== null && typeof result === "object") return result;
|
|
947
|
+
const detail = raw;
|
|
948
|
+
const candidate = detail.groupInfo ?? detail.detailInfo ?? detail.info;
|
|
949
|
+
if (candidate !== void 0 && candidate !== null && typeof candidate === "object") return candidate;
|
|
950
|
+
throw kernelError(`getGroupDetailInfo 返回形状异常(groupCode=${groupCode}): ${errMsg}`, "UNKNOWN");
|
|
951
|
+
}
|
|
952
|
+
/** 群成员列表(forceFetch=true 强制拉取,默认缓存优先)。 */
|
|
953
|
+
async getGroupMemberList(groupCode, forceFetch = false) {
|
|
954
|
+
const raw = await this.service.getAllMemberList(groupCode, forceFetch);
|
|
955
|
+
if (raw.errCode !== 0) throw kernelError(`getAllMemberList 失败: ${raw.errMsg}`, "UNKNOWN");
|
|
956
|
+
return [...raw.result.infos.values()];
|
|
957
|
+
}
|
|
958
|
+
/** 群成员详情(uids 为空返回空数组)。 */
|
|
959
|
+
async getGroupMemberInfo(groupCode, uids) {
|
|
960
|
+
if (uids.length === 0) return [];
|
|
961
|
+
const raw = await this.service.getMemberInfo(groupCode, uids, true);
|
|
962
|
+
unwrap("getMemberInfo", raw.result, raw.errMsg);
|
|
963
|
+
const { infos } = raw;
|
|
964
|
+
if (infos instanceof Map) return [...infos.values()];
|
|
965
|
+
return [];
|
|
966
|
+
}
|
|
967
|
+
/** uin → uid(私聊发送等需要)。 */
|
|
968
|
+
async uinToUid(uins) {
|
|
969
|
+
if (uins.length === 0) return /* @__PURE__ */ new Map();
|
|
970
|
+
const raw = await this.service.getUidByUins(uins);
|
|
971
|
+
if (raw.errCode !== 0) throw kernelError(`getUidByUins 失败: ${raw.errMsg}`, "UNKNOWN");
|
|
972
|
+
return raw.uids;
|
|
973
|
+
}
|
|
974
|
+
/** uid → uin。 */
|
|
975
|
+
async uidToUin(uids) {
|
|
976
|
+
if (uids.length === 0) return /* @__PURE__ */ new Map();
|
|
977
|
+
const raw = await this.service.getUinByUids(uids);
|
|
978
|
+
if (raw.errCode !== 0) throw kernelError(`getUinByUids 失败: ${raw.errMsg}`, "UNKNOWN");
|
|
979
|
+
return raw.uins;
|
|
980
|
+
}
|
|
981
|
+
/** 群成员角色 → OB11 role 字符串。 */
|
|
982
|
+
static roleToString(role) {
|
|
983
|
+
if (role === NTGroupMemberRole.OWNER) return "owner";
|
|
984
|
+
if (role === NTGroupMemberRole.ADMIN) return "admin";
|
|
985
|
+
return "member";
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* 踢人(set_group_kick)。memberUids 为成员 uid;refuseForever=true 拉黑拒绝再次入群。
|
|
989
|
+
* kickMemberV2 的 kickFlag/kickList 字段值待探测校准(TODO P3 联调)。
|
|
990
|
+
*/
|
|
991
|
+
async kickMember(groupCode, memberUids, refuseForever, reason = "") {
|
|
992
|
+
if (memberUids.length === 0) throw kernelError("kickMember 需要至少一个成员 uid", "INVALID_PARAM");
|
|
993
|
+
let optFlag = 1;
|
|
994
|
+
if (refuseForever) optFlag = 0;
|
|
995
|
+
const req = {
|
|
996
|
+
groupCode,
|
|
997
|
+
kickFlag: 0,
|
|
998
|
+
kickList: memberUids.map((uid) => ({
|
|
999
|
+
optFlag,
|
|
1000
|
+
optOperate: 0,
|
|
1001
|
+
optMemberUid: uid,
|
|
1002
|
+
optBytesMsg: ""
|
|
1003
|
+
})),
|
|
1004
|
+
kickListUids: memberUids,
|
|
1005
|
+
kickMsg: reason
|
|
1006
|
+
};
|
|
1007
|
+
const raw = await this.service.kickMemberV2(req);
|
|
1008
|
+
unwrap("kickMemberV2", raw.result, raw.errMsg);
|
|
1009
|
+
}
|
|
1010
|
+
/** 成员禁言(duration 秒,0 解除禁言)。 */
|
|
1011
|
+
async setMemberShutUp(groupCode, members) {
|
|
1012
|
+
if (members.length === 0) throw kernelError("setMemberShutUp 需要至少一个成员", "INVALID_PARAM");
|
|
1013
|
+
const raw = await this.service.setMemberShutUp(groupCode, members.map((m) => ({
|
|
1014
|
+
uid: m.uid,
|
|
1015
|
+
timeStamp: m.duration
|
|
1016
|
+
})));
|
|
1017
|
+
unwrap("setMemberShutUp", raw.result, raw.errMsg);
|
|
1018
|
+
}
|
|
1019
|
+
/** 全员禁言(enable=true 开启全群禁言)。 */
|
|
1020
|
+
async setGroupShutUp(groupCode, enable) {
|
|
1021
|
+
const raw = await this.service.setGroupShutUp(groupCode, enable);
|
|
1022
|
+
unwrap("setGroupShutUp", raw.result, raw.errMsg);
|
|
1023
|
+
}
|
|
1024
|
+
/** 设置管理员(role=ADMIN 设管理,MEMBER 取消;void 语义乐观处理)。 */
|
|
1025
|
+
setMemberRole(groupCode, uid, role) {
|
|
1026
|
+
this.service.modifyMemberRole(groupCode, uid, role);
|
|
1027
|
+
}
|
|
1028
|
+
/** 设置群名片(void 语义乐观处理)。 */
|
|
1029
|
+
setMemberCardName(groupCode, uid, cardName) {
|
|
1030
|
+
this.service.modifyMemberCardName(groupCode, uid, cardName);
|
|
1031
|
+
}
|
|
1032
|
+
/** 修改群名。 */
|
|
1033
|
+
async modifyGroupName(groupCode, groupName) {
|
|
1034
|
+
const raw = await this.service.modifyGroupName(groupCode, groupName, false);
|
|
1035
|
+
unwrap("modifyGroupName", raw.result, raw.errMsg);
|
|
1036
|
+
}
|
|
1037
|
+
/** 退群(needDeleteLocalMsg=is_dismiss:是否解散群,仅群主)。 */
|
|
1038
|
+
async quitGroup(groupCode, needDeleteLocalMsg = false) {
|
|
1039
|
+
const raw = await this.service.quitGroupV2({
|
|
1040
|
+
groupCode,
|
|
1041
|
+
needDeleteLocalMsg
|
|
1042
|
+
});
|
|
1043
|
+
unwrap("quitGroupV2", raw.result, raw.errMsg);
|
|
1044
|
+
}
|
|
1045
|
+
/** 设置精华消息(msgId 经 msgService 取 msgSeq/msgRandom)。 */
|
|
1046
|
+
async addGroupEssence(groupCode, msgId) {
|
|
1047
|
+
await this.setEssence(groupCode, msgId, true);
|
|
1048
|
+
}
|
|
1049
|
+
/** 取消精华消息。 */
|
|
1050
|
+
async removeGroupEssence(groupCode, msgId) {
|
|
1051
|
+
await this.setEssence(groupCode, msgId, false);
|
|
1052
|
+
}
|
|
1053
|
+
/** 精华消息公共流程:按 msgId 拉消息 → add/removeGroupEssence。 */
|
|
1054
|
+
async setEssence(groupCode, msgId, add) {
|
|
1055
|
+
const peer = {
|
|
1056
|
+
chatType: ChatType.GROUP,
|
|
1057
|
+
peerUid: groupCode
|
|
1058
|
+
};
|
|
1059
|
+
const raw = await this.msgService.getMsgsByMsgId(peer, [msgId]);
|
|
1060
|
+
unwrap("getMsgsByMsgId", raw.result, raw.errMsg);
|
|
1061
|
+
const msg = raw.msgList?.[0];
|
|
1062
|
+
if (msg === void 0) throw kernelError(`消息 ${msgId} 不存在`, "NOT_FOUND");
|
|
1063
|
+
const param = {
|
|
1064
|
+
groupCode,
|
|
1065
|
+
msgRandom: Number(msg.msgRandom ?? 0),
|
|
1066
|
+
msgSeq: Number(msg.msgSeq)
|
|
1067
|
+
};
|
|
1068
|
+
let label = "removeGroupEssence";
|
|
1069
|
+
let res;
|
|
1070
|
+
if (add) {
|
|
1071
|
+
label = "addGroupEssence";
|
|
1072
|
+
res = await this.service.addGroupEssence(param);
|
|
1073
|
+
} else res = await this.service.removeGroupEssence(param);
|
|
1074
|
+
checkLooseResult(label, res);
|
|
1075
|
+
}
|
|
1076
|
+
/** @all 剩余次数(get_group_at_all_remain)。 */
|
|
1077
|
+
async getGroupRemainAtTimes(groupCode) {
|
|
1078
|
+
const raw = await this.service.getGroupRemainAtTimes(groupCode);
|
|
1079
|
+
if (raw.errCode !== 0) throw kernelError(`getGroupRemainAtTimes 失败: ${raw.errMsg}`, "UNKNOWN");
|
|
1080
|
+
return {
|
|
1081
|
+
canAtAll: raw.atInfo.canAtAll,
|
|
1082
|
+
remainAtAllCountForUin: raw.atInfo.remainAtAllCountForUin,
|
|
1083
|
+
remainAtAllCountForGroup: raw.atInfo.remainAtAllCountForGroup
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
};
|
|
1087
|
+
//#endregion
|
|
1088
|
+
//#region src/apis/group-notify.ts
|
|
1089
|
+
/**
|
|
1090
|
+
* GroupNotifyApi:群通知/禁言列表语义化 API(ADR-009 统一错误语义,P2-13)
|
|
1091
|
+
*
|
|
1092
|
+
* get_group_system_msg / set_group_add_request / get_group_shut_list 共用。
|
|
1093
|
+
* getSingleScreenNotifies / getGroupShutUpMemberList 的列表可能走 listener 回调,
|
|
1094
|
+
* 直接调用返回形状待探测校准:兼容 result 数组 / 字段 / 直接数组。
|
|
1095
|
+
*/
|
|
1096
|
+
/** 群通知 API:从 session 拿 group service,包装成语义化方法。 */
|
|
1097
|
+
var GroupNotifyApi = class {
|
|
1098
|
+
service;
|
|
1099
|
+
constructor(session) {
|
|
1100
|
+
const service = session.getGroupService();
|
|
1101
|
+
if (service === null || service === void 0) throw kernelError("getGroupService() 返回空(session 未 init)", "INVALID_STATE");
|
|
1102
|
+
this.service = service;
|
|
1103
|
+
}
|
|
1104
|
+
/** 单屏通知列表(get_group_system_msg / set_group_add_request 共用)。 */
|
|
1105
|
+
async getSingleScreenNotifies(doubt, count) {
|
|
1106
|
+
return extractNotifyList((await this.service.getSingleScreenNotifies(doubt, "", count)).result);
|
|
1107
|
+
}
|
|
1108
|
+
/** 禁言成员列表(get_group_shut_list)。 */
|
|
1109
|
+
async getGroupShutUpMemberList(groupCode) {
|
|
1110
|
+
const raw = await this.service.getGroupShutUpMemberList(groupCode);
|
|
1111
|
+
if (raw.result !== 0) unwrap("getGroupShutUpMemberList", raw.result, raw.errMsg);
|
|
1112
|
+
return extractMemberList(raw.result);
|
|
1113
|
+
}
|
|
1114
|
+
/** 处理群请求(set_group_add_request 应答;approve=true 同意 / false 拒绝)。 */
|
|
1115
|
+
async handleGroupRequest(doubt, notify, operateType, reason) {
|
|
1116
|
+
const { group, seq, type } = notify;
|
|
1117
|
+
let groupCode = "";
|
|
1118
|
+
if (group !== void 0) ({groupCode} = group);
|
|
1119
|
+
let postscript = reason;
|
|
1120
|
+
if (postscript === "") postscript = " ";
|
|
1121
|
+
await this.service.operateSysNotify(doubt, {
|
|
1122
|
+
operateType,
|
|
1123
|
+
targetMsg: {
|
|
1124
|
+
seq,
|
|
1125
|
+
type,
|
|
1126
|
+
groupCode,
|
|
1127
|
+
postscript
|
|
1128
|
+
}
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
};
|
|
1132
|
+
/** 从 getSingleScreenNotifies 返回提取通知数组(兼容 result 数组 / notifies 字段)。 */
|
|
1133
|
+
function extractNotifyList(result) {
|
|
1134
|
+
if (Array.isArray(result)) return result;
|
|
1135
|
+
if (result !== null && typeof result === "object") {
|
|
1136
|
+
const obj = result;
|
|
1137
|
+
if (Array.isArray(obj.notifies)) return obj.notifies;
|
|
1138
|
+
if (Array.isArray(obj.groupNotifies)) return obj.groupNotifies;
|
|
1139
|
+
}
|
|
1140
|
+
return [];
|
|
1141
|
+
}
|
|
1142
|
+
/** 从 getGroupShutUpMemberList 返回提取成员数组(兼容 result 数组 / memberList 字段)。 */
|
|
1143
|
+
function extractMemberList(result) {
|
|
1144
|
+
if (Array.isArray(result)) return result;
|
|
1145
|
+
if (result !== null && typeof result === "object") {
|
|
1146
|
+
const obj = result;
|
|
1147
|
+
if (Array.isArray(obj.memberList)) return obj.memberList;
|
|
1148
|
+
if (Array.isArray(obj.list)) return obj.list;
|
|
1149
|
+
}
|
|
1150
|
+
return [];
|
|
766
1151
|
}
|
|
767
1152
|
//#endregion
|
|
768
1153
|
//#region src/apis/msg.ts
|
|
@@ -774,17 +1159,6 @@ function toSendElements(elements) {
|
|
|
774
1159
|
*
|
|
775
1160
|
* 方法面(P2-1):发送 / 撤回 / 拉历史 / 标记已读。group/friend 等后续 apis 同构。
|
|
776
1161
|
*/
|
|
777
|
-
/** 原生结果解包:result !== 0 抛 KernelError(errMsg 语义映射错误码)。 */
|
|
778
|
-
function unwrapResult(label, raw) {
|
|
779
|
-
if (raw.result === 0) return;
|
|
780
|
-
const msg = raw.errMsg || "无错误详情";
|
|
781
|
-
let code = "UNKNOWN";
|
|
782
|
-
if (msg.includes("未登录") || msg.includes("login")) code = "NOT_LOGIN";
|
|
783
|
-
else if (msg.includes("无权限") || msg.includes("permission")) code = "PERMISSION_DENIED";
|
|
784
|
-
else if (msg.includes("不存在") || msg.includes("not found")) code = "NOT_FOUND";
|
|
785
|
-
else if (label === "sendMsg") code = "SEND_FAILED";
|
|
786
|
-
throw kernelError(`${label} 失败: ${msg}`, code);
|
|
787
|
-
}
|
|
788
1162
|
/** 消息 API:从 session 拿 msg service,包装成语义化方法。 */
|
|
789
1163
|
var MsgApi = class {
|
|
790
1164
|
service;
|
|
@@ -902,77 +1276,49 @@ var MsgApi = class {
|
|
|
902
1276
|
}
|
|
903
1277
|
};
|
|
904
1278
|
//#endregion
|
|
905
|
-
//#region src/apis/
|
|
906
|
-
/**
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
this.service = service;
|
|
922
|
-
}
|
|
923
|
-
/** 获取 clientKey(get_clientkey)。 */
|
|
924
|
-
async getClientKey() {
|
|
925
|
-
const raw = await this.service.forceFetchClientKey("");
|
|
926
|
-
if (raw.result !== 0 || raw.clientKey === "") throw kernelError(`forceFetchClientKey 失败: ${raw.errMsg}`, "UNKNOWN");
|
|
927
|
-
return raw;
|
|
928
|
-
}
|
|
929
|
-
/** 获取指定域名的 cookies(get_cookies;uin 为机器人 QQ 号)。 */
|
|
930
|
-
async getCookies(domain, uin) {
|
|
931
|
-
const { clientKey } = await this.getClientKey();
|
|
932
|
-
const u1 = `https://${domain}/${uin}/infocenter`;
|
|
933
|
-
return fetchJumpCookies(`https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=${encodeURIComponent(uin)}&clientkey=${encodeURIComponent(clientKey)}&u1=${encodeURIComponent(u1)}&keyindex=19`);
|
|
934
|
-
}
|
|
935
|
-
/** 计算 bkn(get_csrf_token 与 web 接口共用;skey → bkn 哈希)。 */
|
|
936
|
-
static getBkn(skey) {
|
|
937
|
-
let hash = 5381;
|
|
938
|
-
for (let i = 0; i < skey.length; i += 1) {
|
|
939
|
-
const code = skey.charCodeAt(i);
|
|
940
|
-
hash = hash + (hash << 5) + code;
|
|
941
|
-
}
|
|
942
|
-
return (hash & 2147483647).toString();
|
|
943
|
-
}
|
|
944
|
-
};
|
|
945
|
-
/** 请求 jump 地址并解析 set-cookie → dict(fetch 吞 Set-Cookie,故用 node:https)。 */
|
|
946
|
-
function fetchJumpCookies(url) {
|
|
947
|
-
return new Promise((resolve, reject) => {
|
|
948
|
-
const req = get(url, { headers: { "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" } }, (res) => {
|
|
949
|
-
const cookies = parseSetCookieHeaders(res.headers["set-cookie"]);
|
|
950
|
-
res.resume();
|
|
951
|
-
res.on("end", () => resolve(cookies));
|
|
952
|
-
});
|
|
953
|
-
req.setTimeout(JUMP_TIMEOUT_MS, () => {
|
|
954
|
-
req.destroy();
|
|
955
|
-
reject(/* @__PURE__ */ new Error("获取 cookies 超时"));
|
|
956
|
-
});
|
|
957
|
-
req.on("error", (err) => reject(err));
|
|
958
|
-
});
|
|
1279
|
+
//#region src/apis/stranger-info.ts
|
|
1280
|
+
/** 宽松取值:undefined 时给默认值(null 也归默认)。 */
|
|
1281
|
+
function orDefault(value, fallback) {
|
|
1282
|
+
return value ?? fallback;
|
|
1283
|
+
}
|
|
1284
|
+
/** 合并两份详情:优先 info(uid 完整详情),字段缺失回退 byUin(uin 初查详情)。 */
|
|
1285
|
+
function mergeDetail(info, byUin) {
|
|
1286
|
+
const infoSimple = info.detail?.simpleInfo;
|
|
1287
|
+
const byUinSimple = byUin.detail?.simpleInfo;
|
|
1288
|
+
return {
|
|
1289
|
+
coreInfo: infoSimple?.coreInfo ?? byUinSimple?.coreInfo,
|
|
1290
|
+
baseInfo: infoSimple?.baseInfo ?? byUinSimple?.baseInfo,
|
|
1291
|
+
vasInfo: infoSimple?.vasInfo ?? byUinSimple?.vasInfo,
|
|
1292
|
+
commonExt: info.detail?.commonExt ?? byUin.detail?.commonExt,
|
|
1293
|
+
status: infoSimple?.status?.status ?? byUinSimple?.status?.status
|
|
1294
|
+
};
|
|
959
1295
|
}
|
|
960
|
-
/**
|
|
961
|
-
function
|
|
962
|
-
const
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1296
|
+
/** 扁平化两份详情 → StrangerInfo(宽松取值,字段缺失给默认)。 */
|
|
1297
|
+
function flattenStrangerInfo(uin, uid, byUin, info) {
|
|
1298
|
+
const d = mergeDetail(info, byUin);
|
|
1299
|
+
return {
|
|
1300
|
+
user_id: Number(uin),
|
|
1301
|
+
uid,
|
|
1302
|
+
nickname: orDefault(d.coreInfo?.nick, ""),
|
|
1303
|
+
age: orDefault(d.baseInfo?.age, 0),
|
|
1304
|
+
qid: orDefault(d.baseInfo?.qid, ""),
|
|
1305
|
+
qq_level: orDefault(d.commonExt?.qqLevel, 0),
|
|
1306
|
+
sex: mapSex(d.baseInfo?.sex),
|
|
1307
|
+
long_nick: orDefault(d.baseInfo?.longNick, ""),
|
|
1308
|
+
reg_time: orDefault(d.commonExt?.regTime, 0),
|
|
1309
|
+
is_vip: orDefault(d.vasInfo?.svipFlag, false),
|
|
1310
|
+
is_years_vip: orDefault(d.vasInfo?.yearVipFlag, false),
|
|
1311
|
+
vip_level: orDefault(d.vasInfo?.vipLevel, 0),
|
|
1312
|
+
remark: orDefault(d.coreInfo?.remark, ""),
|
|
1313
|
+
status: orDefault(d.status, 0),
|
|
1314
|
+
login_days: 0
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
/** 性别数值 → OB11 字符串(1=男 2=女 其余 unknown)。 */
|
|
1318
|
+
function mapSex(raw) {
|
|
1319
|
+
if (raw === 1) return "male";
|
|
1320
|
+
if (raw === 2) return "female";
|
|
1321
|
+
return "unknown";
|
|
976
1322
|
}
|
|
977
1323
|
//#endregion
|
|
978
1324
|
//#region src/apis/profile.ts
|
|
@@ -994,13 +1340,11 @@ var ProfileApi = class {
|
|
|
994
1340
|
}
|
|
995
1341
|
/** 设置个性签名(set_self_longnick)。 */
|
|
996
1342
|
async setLongNick(longNick) {
|
|
997
|
-
|
|
998
|
-
if (res !== void 0 && res !== null && typeof res.result === "number" && res.result !== 0) throw kernelError(`setLongNick 失败: ${String(res.errMsg ?? "")}`, "UNKNOWN");
|
|
1343
|
+
checkLooseResult("setLongNick", await this.service.setLongNick(longNick));
|
|
999
1344
|
}
|
|
1000
1345
|
/** 设置昵称(set_qq_profile)。 */
|
|
1001
1346
|
async setNickName(nickName) {
|
|
1002
|
-
|
|
1003
|
-
if (res !== void 0 && res !== null && typeof res.result === "number" && res.result !== 0) throw kernelError(`setNickName 失败: ${String(res.errMsg ?? "")}`, "UNKNOWN");
|
|
1347
|
+
checkLooseResult("setNickName", await this.service.setNickName(nickName));
|
|
1004
1348
|
}
|
|
1005
1349
|
/** 设置头像(set_qq_avatar;filePath 为本地路径)。 */
|
|
1006
1350
|
async setHeader(filePath) {
|
|
@@ -1017,37 +1361,6 @@ var ProfileApi = class {
|
|
|
1017
1361
|
return flattenStrangerInfo(uin, uid, byUin, info);
|
|
1018
1362
|
}
|
|
1019
1363
|
};
|
|
1020
|
-
/** 扁平化两份详情 → StrangerInfo(宽松取值,字段缺失给默认)。 */
|
|
1021
|
-
function flattenStrangerInfo(uin, uid, byUin, info) {
|
|
1022
|
-
const coreInfo = info.detail?.simpleInfo?.coreInfo ?? byUin.detail?.simpleInfo?.coreInfo;
|
|
1023
|
-
const baseInfo = info.detail?.simpleInfo?.baseInfo ?? byUin.detail?.simpleInfo?.baseInfo;
|
|
1024
|
-
const vasInfo = info.detail?.simpleInfo?.vasInfo ?? byUin.detail?.simpleInfo?.vasInfo;
|
|
1025
|
-
const commonExt = info.detail?.commonExt ?? byUin.detail?.commonExt;
|
|
1026
|
-
const status = info.detail?.simpleInfo?.status?.status ?? 0;
|
|
1027
|
-
return {
|
|
1028
|
-
user_id: Number(uin),
|
|
1029
|
-
uid,
|
|
1030
|
-
nickname: coreInfo?.nick ?? "",
|
|
1031
|
-
age: baseInfo?.age ?? 0,
|
|
1032
|
-
qid: baseInfo?.qid ?? "",
|
|
1033
|
-
qq_level: commonExt?.qqLevel ?? 0,
|
|
1034
|
-
sex: mapSex(baseInfo?.sex),
|
|
1035
|
-
long_nick: baseInfo?.longNick ?? "",
|
|
1036
|
-
reg_time: commonExt?.regTime ?? 0,
|
|
1037
|
-
is_vip: vasInfo?.svipFlag ?? false,
|
|
1038
|
-
is_years_vip: vasInfo?.yearVipFlag ?? false,
|
|
1039
|
-
vip_level: vasInfo?.vipLevel ?? 0,
|
|
1040
|
-
remark: coreInfo?.remark ?? "",
|
|
1041
|
-
status,
|
|
1042
|
-
login_days: 0
|
|
1043
|
-
};
|
|
1044
|
-
}
|
|
1045
|
-
/** 性别数值 → OB11 字符串(1=男 2=女 其余 unknown)。 */
|
|
1046
|
-
function mapSex(raw) {
|
|
1047
|
-
if (raw === 1) return "male";
|
|
1048
|
-
if (raw === 2) return "female";
|
|
1049
|
-
return "unknown";
|
|
1050
|
-
}
|
|
1051
1364
|
//#endregion
|
|
1052
1365
|
//#region src/apis/profile-like.ts
|
|
1053
1366
|
/**
|
|
@@ -1096,11 +1409,6 @@ const HIDE_ONLINEDOC_FOLDER = 0;
|
|
|
1096
1409
|
const DEFAULT_FILE_COUNT = 50;
|
|
1097
1410
|
/** 空间查询文件数量(单条即可)。 */
|
|
1098
1411
|
const SPACE_QUERY_FILE_COUNT = 1;
|
|
1099
|
-
/** 原生 result 解包(result 字段非 0 抛 KernelError)。 */
|
|
1100
|
-
function unwrap(label, result, errMsg) {
|
|
1101
|
-
if (result === 0) return;
|
|
1102
|
-
throw kernelError(`${label} 失败: ${errMsg ?? "无错误详情"}`, "UNKNOWN");
|
|
1103
|
-
}
|
|
1104
1412
|
/** 富媒体 API:从 session 拿 rich media service,包装成语义化方法。 */
|
|
1105
1413
|
var RichMediaApi = class {
|
|
1106
1414
|
service;
|
|
@@ -1200,6 +1508,79 @@ function countGroupFiles(result) {
|
|
|
1200
1508
|
return 0;
|
|
1201
1509
|
}
|
|
1202
1510
|
//#endregion
|
|
1511
|
+
//#region src/apis/ticket.ts
|
|
1512
|
+
/**
|
|
1513
|
+
* TicketApi:票据语义化 API(ADR-009 统一错误语义,P2-13)
|
|
1514
|
+
*
|
|
1515
|
+
* - getClientKey:TicketService.forceFetchClientKey 解包(get_clientkey)
|
|
1516
|
+
* - getCookies:clientKey → ssl.ptlogin2.qq.com/jump 跳转 → 解析 set-cookie 头
|
|
1517
|
+
* (fetch 会吞掉 Set-Cookie 头,故用 node:https 手动请求)
|
|
1518
|
+
*/
|
|
1519
|
+
/** 跳转请求超时(毫秒)。 */
|
|
1520
|
+
const JUMP_TIMEOUT_MS = 1e4;
|
|
1521
|
+
/** 票据 API:从 session 拿 ticket service,包装成语义化方法。 */
|
|
1522
|
+
var TicketApi = class {
|
|
1523
|
+
service;
|
|
1524
|
+
constructor(session) {
|
|
1525
|
+
const service = session.getTicketService();
|
|
1526
|
+
if (service === null || service === void 0) throw kernelError("getTicketService() 返回空(session 未 init)", "INVALID_STATE");
|
|
1527
|
+
this.service = service;
|
|
1528
|
+
}
|
|
1529
|
+
/** 获取 clientKey(get_clientkey)。 */
|
|
1530
|
+
async getClientKey() {
|
|
1531
|
+
const raw = await this.service.forceFetchClientKey("");
|
|
1532
|
+
if (raw.result !== 0 || raw.clientKey === "") throw kernelError(`forceFetchClientKey 失败: ${raw.errMsg}`, "UNKNOWN");
|
|
1533
|
+
return raw;
|
|
1534
|
+
}
|
|
1535
|
+
/** 获取指定域名的 cookies(get_cookies;uin 为机器人 QQ 号)。 */
|
|
1536
|
+
async getCookies(domain, uin) {
|
|
1537
|
+
const { clientKey } = await this.getClientKey();
|
|
1538
|
+
const u1 = `https://${domain}/${uin}/infocenter`;
|
|
1539
|
+
return fetchJumpCookies(`https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=${encodeURIComponent(uin)}&clientkey=${encodeURIComponent(clientKey)}&u1=${encodeURIComponent(u1)}&keyindex=19`);
|
|
1540
|
+
}
|
|
1541
|
+
/** 计算 bkn(get_csrf_token 与 web 接口共用;skey → bkn 哈希)。 */
|
|
1542
|
+
static getBkn(skey) {
|
|
1543
|
+
let hash = 5381;
|
|
1544
|
+
for (let i = 0; i < skey.length; i += 1) {
|
|
1545
|
+
const code = skey.charCodeAt(i);
|
|
1546
|
+
hash = hash + (hash << 5) + code;
|
|
1547
|
+
}
|
|
1548
|
+
return (hash & 2147483647).toString();
|
|
1549
|
+
}
|
|
1550
|
+
};
|
|
1551
|
+
/** 请求 jump 地址并解析 set-cookie → dict(fetch 吞 Set-Cookie,故用 node:https)。 */
|
|
1552
|
+
function fetchJumpCookies(url) {
|
|
1553
|
+
return new Promise((resolve, reject) => {
|
|
1554
|
+
const req = get(url, { headers: { "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" } }, (res) => {
|
|
1555
|
+
const cookies = parseSetCookieHeaders(res.headers["set-cookie"]);
|
|
1556
|
+
res.resume();
|
|
1557
|
+
res.on("end", () => resolve(cookies));
|
|
1558
|
+
});
|
|
1559
|
+
req.setTimeout(JUMP_TIMEOUT_MS, () => {
|
|
1560
|
+
req.destroy();
|
|
1561
|
+
reject(/* @__PURE__ */ new Error("获取 cookies 超时"));
|
|
1562
|
+
});
|
|
1563
|
+
req.on("error", (err) => reject(err));
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
/** 解析 set-cookie 头数组 → { name: value } dict。 */
|
|
1567
|
+
function parseSetCookieHeaders(raw) {
|
|
1568
|
+
const cookies = {};
|
|
1569
|
+
if (raw === void 0) return cookies;
|
|
1570
|
+
for (const line of raw) {
|
|
1571
|
+
const [pair] = line.split(";");
|
|
1572
|
+
if (pair !== void 0) {
|
|
1573
|
+
const eq = pair.indexOf("=");
|
|
1574
|
+
if (eq > 0) {
|
|
1575
|
+
const name = pair.slice(0, eq).trim();
|
|
1576
|
+
const value = pair.slice(eq + 1);
|
|
1577
|
+
if (name !== "") cookies[name] = value;
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
return cookies;
|
|
1582
|
+
}
|
|
1583
|
+
//#endregion
|
|
1203
1584
|
//#region src/apis/webapi.ts
|
|
1204
1585
|
/**
|
|
1205
1586
|
* WebApi:群空间 web 接口(P2-15)
|
|
@@ -1280,29 +1661,33 @@ var WebApi = class {
|
|
|
1280
1661
|
type: String(type)
|
|
1281
1662
|
}).toString(), cookie);
|
|
1282
1663
|
if (text === null) return [];
|
|
1283
|
-
|
|
1284
|
-
if (match === null || match[1] === void 0) return [];
|
|
1285
|
-
let state;
|
|
1286
|
-
try {
|
|
1287
|
-
state = JSON.parse(match[1]);
|
|
1288
|
-
} catch {
|
|
1289
|
-
return [];
|
|
1290
|
-
}
|
|
1291
|
-
const raw = type === WebHonorType.TALKATIVE ? state.talkativeList : state.actorList;
|
|
1292
|
-
if (!Array.isArray(raw)) return [];
|
|
1293
|
-
const out = [];
|
|
1294
|
-
for (const item of raw) if (item !== null && typeof item === "object") {
|
|
1295
|
-
const obj = item;
|
|
1296
|
-
out.push({
|
|
1297
|
-
uin: String(obj.uin ?? ""),
|
|
1298
|
-
name: String(obj.name ?? ""),
|
|
1299
|
-
avatar: String(obj.avatar ?? ""),
|
|
1300
|
-
desc: String(obj.desc ?? "")
|
|
1301
|
-
});
|
|
1302
|
-
}
|
|
1303
|
-
return out;
|
|
1664
|
+
return parseHonorList(text, type);
|
|
1304
1665
|
}
|
|
1305
1666
|
};
|
|
1667
|
+
/** 从 honorlist 页面提取 __INITIAL_STATE__ → 荣誉列表(解析失败返回空)。 */
|
|
1668
|
+
function parseHonorList(text, type) {
|
|
1669
|
+
const match = /window\.__INITIAL_STATE__=(.*?);/.exec(text);
|
|
1670
|
+
if (match === null || match[1] === void 0) return [];
|
|
1671
|
+
let state;
|
|
1672
|
+
try {
|
|
1673
|
+
state = JSON.parse(match[1]);
|
|
1674
|
+
} catch {
|
|
1675
|
+
return [];
|
|
1676
|
+
}
|
|
1677
|
+
const raw = type === WebHonorType.TALKATIVE ? state.talkativeList : state.actorList;
|
|
1678
|
+
if (!Array.isArray(raw)) return [];
|
|
1679
|
+
const out = [];
|
|
1680
|
+
for (const item of raw) if (item !== null && typeof item === "object") {
|
|
1681
|
+
const obj = item;
|
|
1682
|
+
out.push({
|
|
1683
|
+
uin: String(obj.uin ?? ""),
|
|
1684
|
+
name: String(obj.name ?? ""),
|
|
1685
|
+
avatar: String(obj.avatar ?? ""),
|
|
1686
|
+
desc: String(obj.desc ?? "")
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
return out;
|
|
1690
|
+
}
|
|
1306
1691
|
/** 请求 qun.qq.com JSON 接口(带 Cookie;失败返回 null)。 */
|
|
1307
1692
|
async function fetchQunJson(url, cookie) {
|
|
1308
1693
|
try {
|
|
@@ -1405,14 +1790,34 @@ var MsgBridge = class {
|
|
|
1405
1790
|
}
|
|
1406
1791
|
};
|
|
1407
1792
|
//#endregion
|
|
1408
|
-
//#region src/
|
|
1409
|
-
/**
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1793
|
+
//#region src/login/wait.ts
|
|
1794
|
+
/**
|
|
1795
|
+
* 等待/延迟工具(login 模块共享,2026-08-08 克隆合并)
|
|
1796
|
+
*/
|
|
1797
|
+
/** 等待条件满足(轮询,带超时)。 */
|
|
1798
|
+
function waitFor(predicate, timeoutMs, intervalMs = 500) {
|
|
1799
|
+
return new Promise((resolve) => {
|
|
1800
|
+
const started = Date.now();
|
|
1801
|
+
const tick = () => {
|
|
1802
|
+
if (predicate()) {
|
|
1803
|
+
resolve(true);
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
if (Date.now() - started > timeoutMs) {
|
|
1807
|
+
resolve(false);
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
setTimeout(tick, intervalMs);
|
|
1811
|
+
};
|
|
1812
|
+
tick();
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
/** 短延迟。 */
|
|
1816
|
+
function sleep(ms) {
|
|
1817
|
+
return new Promise((resolve) => {
|
|
1818
|
+
setTimeout(resolve, ms);
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1416
1821
|
//#endregion
|
|
1417
1822
|
//#region src/cache/group-cache.ts
|
|
1418
1823
|
/**
|
|
@@ -1482,6 +1887,28 @@ var GroupCache = class {
|
|
|
1482
1887
|
return this.groupList.has(groupCode) || this.groupDetails.has(groupCode);
|
|
1483
1888
|
}
|
|
1484
1889
|
/**
|
|
1890
|
+
* 群列表快照(只读,缓存由 onGroupListUpdate 事件主动维护)。
|
|
1891
|
+
* 群列表数据不走原生 getGroupList 返回值(实测仅 { result, errMsg },
|
|
1892
|
+
* 列表经事件推送)——协议/IPC 层消费列表一律经此方法。
|
|
1893
|
+
*/
|
|
1894
|
+
listGroups() {
|
|
1895
|
+
return [...this.groupList.values()];
|
|
1896
|
+
}
|
|
1897
|
+
/**
|
|
1898
|
+
* 群列表快照(缓存为空时主动触发原生刷新,等待事件回填后返回)。
|
|
1899
|
+
*
|
|
1900
|
+
* 实测(2026-08-08 端到端联调):onGroupListUpdate 仅在登录后首轮同步
|
|
1901
|
+
* 推送(MMKV 已有缓存后不再重复推送)——进程内缓存为空时列表读不到。
|
|
1902
|
+
* 触发 getGroupList(true) 会重新拉取并推送事件回填缓存。
|
|
1903
|
+
* 返回空数组 = 刷新后仍无数据(超时 5s)。
|
|
1904
|
+
*/
|
|
1905
|
+
async listGroupsRefreshed() {
|
|
1906
|
+
if (this.groupList.size > 0) return [...this.groupList.values()];
|
|
1907
|
+
await this.groupApi.getGroupList(true);
|
|
1908
|
+
await waitFor(() => this.groupList.size > 0, 5e3, 200);
|
|
1909
|
+
return [...this.groupList.values()];
|
|
1910
|
+
}
|
|
1911
|
+
/**
|
|
1485
1912
|
* 群列表项(只读;缺失惰性回填 getGroupList)。
|
|
1486
1913
|
* 返回 undefined 表示群不存在。
|
|
1487
1914
|
*/
|
|
@@ -1566,181 +1993,6 @@ function createCoreContext(opts) {
|
|
|
1566
1993
|
};
|
|
1567
1994
|
}
|
|
1568
1995
|
//#endregion
|
|
1569
|
-
//#region src/infra/logger.ts
|
|
1570
|
-
/**
|
|
1571
|
-
* pino 日志封装(ADR-007)
|
|
1572
|
-
*
|
|
1573
|
-
* - console(pino-pretty 可读输出)+ 可选文件双目的地
|
|
1574
|
-
* - 统一 level 控制
|
|
1575
|
-
* - 内置 redact(token / 票据等敏感字段不打日志),可追加
|
|
1576
|
-
* - 无全局单例(ADR-015 推论):每进程实例化一份,由装配层持有
|
|
1577
|
-
*/
|
|
1578
|
-
/** 默认 redact 路径:顶层 token/票据 + 任意深度的密码/密钥。 */
|
|
1579
|
-
const DEFAULT_REDACT_PATHS = [
|
|
1580
|
-
"token",
|
|
1581
|
-
"ticket",
|
|
1582
|
-
"cookie",
|
|
1583
|
-
"password",
|
|
1584
|
-
"secret"
|
|
1585
|
-
];
|
|
1586
|
-
function toSingle(streams) {
|
|
1587
|
-
const [only] = streams;
|
|
1588
|
-
if (only === void 0) throw new KernelError("logger 内部错误:目的地列表为空", "UNKNOWN");
|
|
1589
|
-
return only.stream;
|
|
1590
|
-
}
|
|
1591
|
-
/**
|
|
1592
|
-
* 创建 pino logger:console(pretty)+ 可选文件,统一 level / redact。
|
|
1593
|
-
*
|
|
1594
|
-
* 文件目的地用同步写入(`sync: true`):日志量级下性能可接受,且进程退出不丢日志,
|
|
1595
|
-
* 无需额外的 flush 步骤(对比异步 SonicBoom 的 flushSync 时序陷阱)。
|
|
1596
|
-
*/
|
|
1597
|
-
function createLogger(opts = {}) {
|
|
1598
|
-
const level = opts.level ?? "info";
|
|
1599
|
-
const consoleEnabled = opts.console ?? true;
|
|
1600
|
-
const { file, base } = opts;
|
|
1601
|
-
const streams = [];
|
|
1602
|
-
if (consoleEnabled) streams.push({ stream: pinoPretty({
|
|
1603
|
-
colorize: true,
|
|
1604
|
-
translateTime: "SYS:HH:MM:ss.l",
|
|
1605
|
-
singleLine: false,
|
|
1606
|
-
destination: process.stdout
|
|
1607
|
-
}) });
|
|
1608
|
-
if (file) {
|
|
1609
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
1610
|
-
streams.push({ stream: pino.destination({
|
|
1611
|
-
dest: file,
|
|
1612
|
-
sync: true
|
|
1613
|
-
}) });
|
|
1614
|
-
}
|
|
1615
|
-
if (streams.length === 0) throw new KernelError("logger 至少需要一个输出目标:console 或 file", "INVALID_PARAM");
|
|
1616
|
-
const pinoOptions = {
|
|
1617
|
-
level,
|
|
1618
|
-
redact: [...DEFAULT_REDACT_PATHS, ...opts.redact ?? []]
|
|
1619
|
-
};
|
|
1620
|
-
if (base) pinoOptions.base = {
|
|
1621
|
-
pid: process.pid,
|
|
1622
|
-
...base
|
|
1623
|
-
};
|
|
1624
|
-
let stream;
|
|
1625
|
-
if (streams.length === 1) stream = toSingle(streams);
|
|
1626
|
-
else stream = pino.multistream(streams);
|
|
1627
|
-
return pino(pinoOptions, stream);
|
|
1628
|
-
}
|
|
1629
|
-
//#endregion
|
|
1630
|
-
//#region src/infra/paths.ts
|
|
1631
|
-
/**
|
|
1632
|
-
* 数据目录布局(ADR-016,2026-08-07 修订:配置文件独立于数据根)
|
|
1633
|
-
*
|
|
1634
|
-
* ```
|
|
1635
|
-
* <数据根>/<qq号>/ # 每账号独立目录(ADR-015 多账号前提)
|
|
1636
|
-
* ├── config/ # 各账号独立配置(历史产物,协议段已走 seed)
|
|
1637
|
-
* ├── logs/ # pino 文件日志
|
|
1638
|
-
* └── cache/ # 临时文件、媒体缓存
|
|
1639
|
-
*
|
|
1640
|
-
* <项目根>/napuketto.toml # 全局配置文件(2026-08-07 用户拍板:放项目根目录)
|
|
1641
|
-
* ```
|
|
1642
|
-
*
|
|
1643
|
-
* 数据根优先级:cli `--data-dir`(显式参数)> `NAPKETTO_DATA` 环境变量 > `~/.napuketto`(默认)。
|
|
1644
|
-
* 配置文件路径见 `resolveConfigPath`(项目根探测,与数据根解耦)。
|
|
1645
|
-
*/
|
|
1646
|
-
/** 默认数据根目录名(用户目录下,程序目录可能只读,见 ADR-016)。 */
|
|
1647
|
-
const DEFAULT_DATA_ROOT_NAME = ".napuketto";
|
|
1648
|
-
/** 全局配置文件名。 */
|
|
1649
|
-
const MAIN_CONFIG_FILE = "napuketto.toml";
|
|
1650
|
-
/**
|
|
1651
|
-
* 解析数据根:显式参数 > 环境变量 > 用户目录默认。
|
|
1652
|
-
* 返回绝对路径。独立导出便于 cli / 探测脚本复用。
|
|
1653
|
-
*/
|
|
1654
|
-
function resolveDataRoot(dataRoot) {
|
|
1655
|
-
const explicit = dataRoot ?? process.env["NAPKETTO_DATA"];
|
|
1656
|
-
if (explicit) return resolve(explicit);
|
|
1657
|
-
return join(homedir(), DEFAULT_DATA_ROOT_NAME);
|
|
1658
|
-
}
|
|
1659
|
-
/** 向上查找含标志文件的目录(项目根探测:monorepo 根 / 用户项目根)。 */
|
|
1660
|
-
function findProjectRoot(startDir, marker) {
|
|
1661
|
-
let dir = startDir;
|
|
1662
|
-
for (;;) {
|
|
1663
|
-
if (existsSync(join(dir, marker))) return dir;
|
|
1664
|
-
const parent = dirname(dir);
|
|
1665
|
-
if (parent === dir) return;
|
|
1666
|
-
dir = parent;
|
|
1667
|
-
}
|
|
1668
|
-
}
|
|
1669
|
-
/**
|
|
1670
|
-
* 全局配置文件路径解析(2026-08-07 用户拍板:**配置文件放项目根目录,
|
|
1671
|
-
* 数据仍按数据根组织**——数据根只承载账号目录/日志/缓存/QQ 数据)。
|
|
1672
|
-
*
|
|
1673
|
-
* 优先级:
|
|
1674
|
-
* 1. `NAPKETTO_CONFIG` 环境变量(显式指定完整路径,任意场景可用)
|
|
1675
|
-
* 2. 入口模块向上找 `pnpm-workspace.yaml`(monorepo 开发场景,即项目根)
|
|
1676
|
-
* 3. 运行目录(cwd)向上找 `pnpm-workspace.yaml` / `package.json`(发布后用户项目)
|
|
1677
|
-
* 4. 数据根兜底 `<dataRoot>/napuketto.toml`(旧行为兼容)
|
|
1678
|
-
*
|
|
1679
|
-
* 返回绝对路径。独立导出便于 cli / loader 装配链复用。
|
|
1680
|
-
*/
|
|
1681
|
-
function resolveConfigPath(opts = {}) {
|
|
1682
|
-
const explicit = process.env["NAPKETTO_CONFIG"];
|
|
1683
|
-
if (explicit !== void 0 && explicit !== "") return resolve(explicit);
|
|
1684
|
-
const moduleRoot = findProjectRoot(dirname(fileURLToPath(import.meta.url)), "pnpm-workspace.yaml");
|
|
1685
|
-
if (moduleRoot !== void 0) return join(moduleRoot, MAIN_CONFIG_FILE);
|
|
1686
|
-
const cwdRoot = findProjectRoot(process.cwd(), "pnpm-workspace.yaml") ?? findProjectRoot(process.cwd(), "package.json");
|
|
1687
|
-
if (cwdRoot !== void 0) return join(cwdRoot, MAIN_CONFIG_FILE);
|
|
1688
|
-
return join(opts.dataRoot ?? resolveDataRoot(), MAIN_CONFIG_FILE);
|
|
1689
|
-
}
|
|
1690
|
-
/**
|
|
1691
|
-
* 路径装配器:持有账号数据目录下的全部子目录。
|
|
1692
|
-
* 无全局单例(ADR-015 推论)——每账号每进程实例化一份。
|
|
1693
|
-
*/
|
|
1694
|
-
var PathWrapper = class {
|
|
1695
|
-
/** 数据根(绝对路径)。 */
|
|
1696
|
-
root;
|
|
1697
|
-
/** 账号数据目录(绝对路径),缺省 account 时等于 root。 */
|
|
1698
|
-
accountDir;
|
|
1699
|
-
/** config 目录(napuketto.json + 各协议配置)。 */
|
|
1700
|
-
configDir;
|
|
1701
|
-
/** logs 目录(pino 文件日志)。 */
|
|
1702
|
-
logsDir;
|
|
1703
|
-
/** cache 目录(临时文件、媒体缓存)。 */
|
|
1704
|
-
cacheDir;
|
|
1705
|
-
constructor(opts = {}) {
|
|
1706
|
-
const root = resolveDataRoot(opts.dataRoot);
|
|
1707
|
-
this.root = root;
|
|
1708
|
-
if (opts.account) this.accountDir = join(root, opts.account);
|
|
1709
|
-
else this.accountDir = root;
|
|
1710
|
-
this.configDir = join(this.accountDir, "config");
|
|
1711
|
-
this.logsDir = join(this.accountDir, "logs");
|
|
1712
|
-
this.cacheDir = join(this.accountDir, "cache");
|
|
1713
|
-
}
|
|
1714
|
-
/** 递归创建全部目录,启动装配时调用一次。 */
|
|
1715
|
-
ensure() {
|
|
1716
|
-
for (const dir of [
|
|
1717
|
-
this.configDir,
|
|
1718
|
-
this.logsDir,
|
|
1719
|
-
this.cacheDir
|
|
1720
|
-
]) mkdirSync(dir, { recursive: true });
|
|
1721
|
-
}
|
|
1722
|
-
/** 账号目录内的文件路径(如 `file('config', 'napuketto.json')`)。 */
|
|
1723
|
-
file(...segments) {
|
|
1724
|
-
return join(this.accountDir, ...segments);
|
|
1725
|
-
}
|
|
1726
|
-
/**
|
|
1727
|
-
* 清空缓存目录(clean_cache 动作经注入回调消费)。
|
|
1728
|
-
* 只删除 cacheDir 下的条目,保留目录本身。
|
|
1729
|
-
*/
|
|
1730
|
-
clearCache() {
|
|
1731
|
-
let entries;
|
|
1732
|
-
try {
|
|
1733
|
-
entries = readdirSync(this.cacheDir);
|
|
1734
|
-
} catch {
|
|
1735
|
-
return;
|
|
1736
|
-
}
|
|
1737
|
-
for (const entry of entries) rmSync(join(this.cacheDir, entry), {
|
|
1738
|
-
recursive: true,
|
|
1739
|
-
force: true
|
|
1740
|
-
});
|
|
1741
|
-
}
|
|
1742
|
-
};
|
|
1743
|
-
//#endregion
|
|
1744
1996
|
//#region src/wrapper/wrapper-adapters.ts
|
|
1745
1997
|
/** NAPI 回调占位(方法存在即可被反射调用,无副作用)。 */
|
|
1746
1998
|
const noop = () => void 0;
|
|
@@ -1784,78 +2036,21 @@ function createSessionListener(opts = {}) {
|
|
|
1784
2036
|
onOpentelemetryInit: (info) => emit("onOpentelemetryInit", info),
|
|
1785
2037
|
onUserOnlineResult: (result) => emit("onUserOnlineResult", result),
|
|
1786
2038
|
onGetSelfTinyId: (result) => emit("onGetSelfTinyId", result)
|
|
1787
|
-
};
|
|
1788
|
-
}
|
|
1789
|
-
/** 创建登录监听器(占位,登录器接入时填充)。 */
|
|
1790
|
-
function createLoginListener() {
|
|
1791
|
-
return {};
|
|
1792
|
-
}
|
|
1793
|
-
//#endregion
|
|
1794
|
-
//#region src/login/lifecycle.ts
|
|
1795
|
-
/**
|
|
1796
|
-
* 完整启动生命周期(wrapper.node 契约流程,2026-08-05 实测确认,自研实现)
|
|
1797
|
-
*
|
|
1798
|
-
* 启动顺序由 wrapper.node 外部契约决定(engine→login→session→startNT),
|
|
1799
|
-
* 运行时实测确认,自研实现:
|
|
1800
|
-
* 1. engine.initWithDeskTopConfig(appid/qua/版本)
|
|
1801
|
-
* 2. loginService.initConfig + addKernelLoginListener
|
|
1802
|
-
* 3. getLoginList() → quickLoginWithUin(uin)(或 QR 登录)
|
|
1803
|
-
* 4. 登录成功 → genSessionConfig → session.init(config, 3 adapter, listener) → startNT(0)
|
|
1804
|
-
* 5. 等 init 完成信号(onOpentelemetryInit(is_init) 为主,onSessionInitComplete 为辅)
|
|
1805
|
-
*
|
|
1806
|
-
* 关键(2026-08-05 实测修正):
|
|
1807
|
-
* - session 用 `new wrapper.NodeIQQNTWrapperSession()` 创建;exports 不含 NodeI*Adapter/Listener
|
|
1808
|
-
* 构造器(89 键实测无)——adapter 与 listener 一律传普通 JS 对象(NAPI 反射读取方法回调)。
|
|
1809
|
-
*
|
|
1810
|
-
* 模块边界(2026-08-05 解耦):
|
|
1811
|
-
* - 配置装配(buildEngineConfig / buildLoginConfig / buildSessionConfig)→ wrapper-config.ts
|
|
1812
|
-
* - NAPI 回调适配器(GlobalAdapter / DependsAdapter / DispatcherAdapter / listener 工厂)→ wrapper-adapters.ts
|
|
1813
|
-
* - 本文件只保留流程编排:快速登录 + session 初始化 + init 完成信号等待。
|
|
1814
|
-
*/
|
|
1815
|
-
/** 等待条件满足(轮询,带超时)。 */
|
|
1816
|
-
function waitFor(predicate, timeoutMs, intervalMs = 500) {
|
|
1817
|
-
return new Promise((resolve) => {
|
|
1818
|
-
const started = Date.now();
|
|
1819
|
-
const tick = () => {
|
|
1820
|
-
if (predicate()) {
|
|
1821
|
-
resolve(true);
|
|
1822
|
-
return;
|
|
1823
|
-
}
|
|
1824
|
-
if (Date.now() - started > timeoutMs) {
|
|
1825
|
-
resolve(false);
|
|
1826
|
-
return;
|
|
1827
|
-
}
|
|
1828
|
-
setTimeout(tick, intervalMs);
|
|
1829
|
-
};
|
|
1830
|
-
tick();
|
|
1831
|
-
});
|
|
2039
|
+
};
|
|
1832
2040
|
}
|
|
1833
|
-
/**
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
2041
|
+
/** 创建登录监听器(占位,登录器接入时填充)。 */
|
|
2042
|
+
function createLoginListener() {
|
|
2043
|
+
return {};
|
|
2044
|
+
}
|
|
2045
|
+
//#endregion
|
|
2046
|
+
//#region src/login/login-connect.ts
|
|
1839
2047
|
/**
|
|
1840
|
-
*
|
|
2048
|
+
* 登录连接与快速登录(从 lifecycle.ts 拆分,2026-08-08 FTA 优化)
|
|
1841
2049
|
*
|
|
1842
|
-
*
|
|
1843
|
-
*
|
|
1844
|
-
*
|
|
1845
|
-
* 正确做法:等 QQ 的 session 就绪(getMsgService 非 null = 核心 service 已挂载)。
|
|
2050
|
+
* - waitForNetworkConnection:等 MSF 网络连接就绪(getMsfStatus() === 3)
|
|
2051
|
+
* - ensureLoginConnected:connect() → 等 onLoginConnected(自建宿主必需)
|
|
2052
|
+
* - quickLogin:历史账号快速登录(网络异常 1006511 等就绪后重试)
|
|
1846
2053
|
*/
|
|
1847
|
-
async function waitSessionReady(ctx, opts) {
|
|
1848
|
-
const { session } = ctx;
|
|
1849
|
-
if (session === null || session === void 0) throw kernelError("session 未创建", "INVALID_STATE");
|
|
1850
|
-
if (!await waitFor(() => {
|
|
1851
|
-
try {
|
|
1852
|
-
const svc = session.getMsgService();
|
|
1853
|
-
return svc !== null && svc !== void 0;
|
|
1854
|
-
} catch {
|
|
1855
|
-
return false;
|
|
1856
|
-
}
|
|
1857
|
-
}, opts.timeoutMs ?? SESSION_READY_TIMEOUT_MS, SESSION_READY_POLL_MS)) throw kernelError("等待 QQ session 就绪超时(QQ 未完成 init)", "TIMEOUT");
|
|
1858
|
-
}
|
|
1859
2054
|
/**
|
|
1860
2055
|
* 网络状态(MSF)常量。
|
|
1861
2056
|
* 3 = 已连接(NapCat waitForNetworkConnection 语义,自研描述)。
|
|
@@ -1871,6 +2066,8 @@ const NETWORK_RETRY_MAX = 3;
|
|
|
1871
2066
|
const NETWORK_READY_TIMEOUT_MS = 15e3;
|
|
1872
2067
|
/** 网络就绪轮询间隔(毫秒)。 */
|
|
1873
2068
|
const NETWORK_READY_POLL_MS = 1e3;
|
|
2069
|
+
/** 连接稳定缓冲(毫秒,p0-kernel-flow 实证值)。 */
|
|
2070
|
+
const CONNECTION_SETTLE_MS = 3e3;
|
|
1874
2071
|
/**
|
|
1875
2072
|
* 等待网络连接就绪(loginService.getMsfStatus() === 3)。
|
|
1876
2073
|
* 参考 NapCat waitForNetworkConnection 思路(自研实现):快速登录在 QQ 刚拉起、
|
|
@@ -1931,41 +2128,21 @@ async function ensureLoginConnected(ctx, opts) {
|
|
|
1931
2128
|
});
|
|
1932
2129
|
await sleep(CONNECTION_SETTLE_MS);
|
|
1933
2130
|
}
|
|
1934
|
-
/** 连接稳定缓冲(毫秒,p0-kernel-flow 实证值)。 */
|
|
1935
|
-
const CONNECTION_SETTLE_MS = 3e3;
|
|
1936
|
-
/** 短延迟。 */
|
|
1937
|
-
function sleep(ms) {
|
|
1938
|
-
return new Promise((resolve) => {
|
|
1939
|
-
setTimeout(resolve, ms);
|
|
1940
|
-
});
|
|
1941
|
-
}
|
|
1942
2131
|
/**
|
|
1943
|
-
*
|
|
1944
|
-
*
|
|
2132
|
+
* 目标账号选择(2026-08-07 修复:显式 uin 不在列表时**报错**而非静默 fallback——
|
|
2133
|
+
* 之前会退到第一个可快速登录账号,导致 `-q <uin>` 实际登成别的号)。
|
|
1945
2134
|
*/
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
const available = items.map((i) => i.uin).join("、");
|
|
1958
|
-
throw kernelError(`账号 ${opts.uin} 不在登录列表(可用:${available})`, "NOT_FOUND");
|
|
1959
|
-
}
|
|
1960
|
-
} else {
|
|
1961
|
-
target = items.find((i) => i.isQuickLogin);
|
|
1962
|
-
if (target === void 0) {
|
|
1963
|
-
const [first] = items;
|
|
1964
|
-
target = first;
|
|
1965
|
-
}
|
|
1966
|
-
}
|
|
1967
|
-
if (target === void 0) throw kernelError("无可用登录账号", "NOT_LOGIN");
|
|
1968
|
-
return loginWithNetworkRetry(ctx, loginService, target, opts);
|
|
2135
|
+
function pickLoginTarget(items, uin) {
|
|
2136
|
+
if (uin !== void 0) {
|
|
2137
|
+
const target = items.find((i) => i.uin === uin);
|
|
2138
|
+
if (target === void 0) throw kernelError(`账号 ${uin} 不在登录列表(可用:${items.map((i) => i.uin).join("、")})`, "NOT_FOUND");
|
|
2139
|
+
return target;
|
|
2140
|
+
}
|
|
2141
|
+
const quick = items.find((i) => i.isQuickLogin);
|
|
2142
|
+
if (quick !== void 0) return quick;
|
|
2143
|
+
const [first] = items;
|
|
2144
|
+
if (first === void 0) throw kernelError("无可用登录账号", "NOT_LOGIN");
|
|
2145
|
+
return first;
|
|
1969
2146
|
}
|
|
1970
2147
|
/**
|
|
1971
2148
|
* 带网络重试的快速登录(P2-1)。
|
|
@@ -1986,6 +2163,69 @@ async function loginWithNetworkRetry(ctx, loginService, target, opts) {
|
|
|
1986
2163
|
}
|
|
1987
2164
|
throw kernelError(`快速登录失败: ${lastErrMsg}`, "NOT_LOGIN");
|
|
1988
2165
|
}
|
|
2166
|
+
/**
|
|
2167
|
+
* 快速登录:遍历历史登录列表尝试。
|
|
2168
|
+
* P2-1:失败且错误为网络异常(1006511)时,等网络就绪后重试(最多 NETWORK_RETRY_MAX 次)。
|
|
2169
|
+
*/
|
|
2170
|
+
async function quickLogin(ctx, opts) {
|
|
2171
|
+
const raw = ctx.loginService;
|
|
2172
|
+
if (raw === null) throw kernelError("loginService 无效(缺 getLoginList)", "INVALID_STATE");
|
|
2173
|
+
const loginService = raw;
|
|
2174
|
+
await ensureLoginConnected(ctx, { ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {} });
|
|
2175
|
+
const items = (await loginService.getLoginList()).LocalLoginInfoList;
|
|
2176
|
+
if (items.length === 0) throw kernelError("无历史登录账号", "NOT_LOGIN");
|
|
2177
|
+
return loginWithNetworkRetry(ctx, loginService, pickLoginTarget(items, opts.uin), opts);
|
|
2178
|
+
}
|
|
2179
|
+
//#endregion
|
|
2180
|
+
//#region src/login/lifecycle.ts
|
|
2181
|
+
/**
|
|
2182
|
+
* 完整启动生命周期(wrapper.node 契约流程,2026-08-05 实测确认,自研实现)
|
|
2183
|
+
*
|
|
2184
|
+
* 启动顺序由 wrapper.node 外部契约决定(engine→login→session→startNT),
|
|
2185
|
+
* 运行时实测确认,自研实现:
|
|
2186
|
+
* 1. engine.initWithDeskTopConfig(appid/qua/版本)
|
|
2187
|
+
* 2. loginService.initConfig + addKernelLoginListener
|
|
2188
|
+
* 3. getLoginList() → quickLoginWithUin(uin)(或 QR 登录)
|
|
2189
|
+
* 4. 登录成功 → genSessionConfig → session.init(config, 3 adapter, listener) → startNT(0)
|
|
2190
|
+
* 5. 等 init 完成信号(onOpentelemetryInit(is_init) 为主,onSessionInitComplete 为辅)
|
|
2191
|
+
*
|
|
2192
|
+
* 关键(2026-08-05 实测修正):
|
|
2193
|
+
* - session 用 `new wrapper.NodeIQQNTWrapperSession()` 创建;exports 不含 NodeI*Adapter/Listener
|
|
2194
|
+
* 构造器(89 键实测无)——adapter 与 listener 一律传普通 JS 对象(NAPI 反射读取方法回调)。
|
|
2195
|
+
*
|
|
2196
|
+
* 模块边界(2026-08-05 解耦;2026-08-08 FTA 优化再拆):
|
|
2197
|
+
* - 配置装配(buildEngineConfig / buildLoginConfig / buildSessionConfig)→ wrapper-config.ts
|
|
2198
|
+
* - NAPI 回调适配器(GlobalAdapter / DependsAdapter / DispatcherAdapter / listener 工厂)→ wrapper-adapters.ts
|
|
2199
|
+
* - 登录连接 + 快速登录(waitForNetworkConnection / quickLogin 等)→ login-connect.ts
|
|
2200
|
+
* - 本文件只保留 session 流程编排:就绪等待(waitSessionReady)+ session 初始化(initAndStartSession)。
|
|
2201
|
+
*/
|
|
2202
|
+
/** session init 默认超时(毫秒)。 */
|
|
2203
|
+
const DEFAULT_INIT_TIMEOUT_MS = 15e3;
|
|
2204
|
+
/** session 就绪等待默认超时(毫秒)。 */
|
|
2205
|
+
const SESSION_READY_TIMEOUT_MS = 3e4;
|
|
2206
|
+
/** session 就绪轮询间隔(毫秒)。 */
|
|
2207
|
+
const SESSION_READY_POLL_MS = 1e3;
|
|
2208
|
+
/**
|
|
2209
|
+
* 等待 QQ 自己完成 session 初始化(framework 复用模式,2026-08-05 修正)。
|
|
2210
|
+
*
|
|
2211
|
+
* 背景:QQ 9.9.31 实测——session 由 QQ 主进程自己创建并 init(登录后)。
|
|
2212
|
+
* 我们不自己 init(重复 init 断言 "implementation of IQQNTWrapperSession is not valid"),
|
|
2213
|
+
* 也不自己 create(登录前 create() 干扰 QQ,实测 QQ 退出 code=0)。
|
|
2214
|
+
* 正确做法:等 QQ 的 session 就绪(getMsgService 非 null = 核心 service 已挂载)。
|
|
2215
|
+
*/
|
|
2216
|
+
async function waitSessionReady(ctx, opts) {
|
|
2217
|
+
const { session } = ctx;
|
|
2218
|
+
if (session === null || session === void 0) throw kernelError("session 未创建", "INVALID_STATE");
|
|
2219
|
+
if (!await waitFor(() => {
|
|
2220
|
+
try {
|
|
2221
|
+
const svc = session.getMsgService();
|
|
2222
|
+
return svc !== null && svc !== void 0;
|
|
2223
|
+
} catch {
|
|
2224
|
+
return false;
|
|
2225
|
+
}
|
|
2226
|
+
}, opts.timeoutMs ?? SESSION_READY_TIMEOUT_MS, SESSION_READY_POLL_MS)) throw kernelError("等待 QQ session 就绪超时(QQ 未完成 init)", "TIMEOUT");
|
|
2227
|
+
}
|
|
2228
|
+
/** 登录连接 + 快速登录(quickLogin 等)已拆到 login-connect.ts(见文件头 re-export)。 */
|
|
1989
2229
|
/** session 初始化(4 参全为普通 JS 对象,等 init 完成信号)。 */
|
|
1990
2230
|
async function initAndStartSession(ctx, config, listener, opts) {
|
|
1991
2231
|
const { session } = ctx;
|
|
@@ -2200,36 +2440,6 @@ function findInObject(value) {
|
|
|
2200
2440
|
return null;
|
|
2201
2441
|
}
|
|
2202
2442
|
//#endregion
|
|
2203
|
-
//#region src/types/wrapper.ts
|
|
2204
|
-
/**
|
|
2205
|
-
* wrapper.node 的 NAPI 接口面(自研描述,非移植)
|
|
2206
|
-
*
|
|
2207
|
-
* 来源:wrapper.node 在 QQ 定制版 Electron 里通过 napi_define_class / napi_set_named_property
|
|
2208
|
-
* 注册的 JS 类(2026-08-05 Ghidra 反编译 FUN_180c6274a / FUN_180c80224 确认类名)。
|
|
2209
|
-
* 这些接口是 wrapper.node 对外暴露的外部系统契约——我们自研描述其形状,未复制任何第三方代码。
|
|
2210
|
-
*
|
|
2211
|
-
* 注意:这里所有对象由 QQ 运行时的 NAPI 层自动构建/转换,业务层禁止触碰 C++ ABI。
|
|
2212
|
-
*/
|
|
2213
|
-
/** 平台类型(与 wrapper.node 的 platform 字段对应)。 */
|
|
2214
|
-
const PlatformType = {
|
|
2215
|
-
KUNKNOWN: 0,
|
|
2216
|
-
KANDROID: 1,
|
|
2217
|
-
KIOS: 2,
|
|
2218
|
-
KWINDOWS: 3,
|
|
2219
|
-
KMAC: 4,
|
|
2220
|
-
KLINUX: 5
|
|
2221
|
-
};
|
|
2222
|
-
/** 厂商类型(deviceInfo.vendorType)。 */
|
|
2223
|
-
const VendorType = {
|
|
2224
|
-
KNOSETONIOS: 0,
|
|
2225
|
-
KUNSUPPORTANDROIDPUSH: 1,
|
|
2226
|
-
KSUPPORTTPNS: 2,
|
|
2227
|
-
KSUPPORTHMS: 3,
|
|
2228
|
-
KSUPPORTOPPOPUSH: 4,
|
|
2229
|
-
KSUPPORTVIVOPUSH: 5,
|
|
2230
|
-
KSUPPORTGOOGLEPUSH: 99
|
|
2231
|
-
};
|
|
2232
|
-
//#endregion
|
|
2233
2443
|
//#region src/wrapper/wrapper-config.ts
|
|
2234
2444
|
/**
|
|
2235
2445
|
* wrapper 交互配置装配(纯函数,无状态)
|
|
@@ -2542,20 +2752,8 @@ function startNapuketto(options) {
|
|
|
2542
2752
|
const isSelfHost = electronProcessType() === void 0;
|
|
2543
2753
|
if (qqSession !== void 0 && qqSession !== null) ctx.session = qqSession;
|
|
2544
2754
|
else if (isSelfHost) createSession(ctx);
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
const root = resolveQqUserDataRoot(wrapperExports);
|
|
2548
|
-
if (root !== null) qqGlobalPath = resolveQqGlobalPath(root);
|
|
2549
|
-
}
|
|
2550
|
-
initEngine(ctx, engineConfig ?? defaultEngineConfig(env ?? {}, qqGlobalPath ?? void 0));
|
|
2551
|
-
if (qqLoginService !== void 0 && qqLoginService !== null) ctx.loginService = qqLoginService;
|
|
2552
|
-
else try {
|
|
2553
|
-
const loginSvcCtor = ctx.exports.NodeIKernelLoginService;
|
|
2554
|
-
if (loginSvcCtor !== null && typeof loginSvcCtor.get === "function") ctx.loginService = loginSvcCtor.get();
|
|
2555
|
-
else ctx.loginService = new ctx.exports.NodeIKernelLoginService();
|
|
2556
|
-
} catch {
|
|
2557
|
-
ctx.loginService = null;
|
|
2558
|
-
}
|
|
2755
|
+
initEngine(ctx, engineConfig ?? defaultEngineConfig(env ?? {}, resolveBootQqGlobalPath(ctx)));
|
|
2756
|
+
resolveLoginService(ctx, qqLoginService);
|
|
2559
2757
|
if (qqSession !== void 0 && qqSession !== null) ctx.session = qqSession;
|
|
2560
2758
|
else if (!isSelfHost) createSession(ctx);
|
|
2561
2759
|
if (sessionConfig !== void 0) {
|
|
@@ -2564,6 +2762,37 @@ function startNapuketto(options) {
|
|
|
2564
2762
|
}
|
|
2565
2763
|
return ctx;
|
|
2566
2764
|
}
|
|
2765
|
+
/**
|
|
2766
|
+
* 解析 QQ 真实数据目录的 global 路径(desktopGlobalPath 三要素之三)。
|
|
2767
|
+
* worker(utilityProcess)模式 + 自建宿主(标准 node)下必须指向 QQ 真实
|
|
2768
|
+
* 数据目录(数据根/nt_qq/global,HANDOVER-V6),否则登录数据读写错位
|
|
2769
|
+
* (P2-1 实测 2026-08-06;getLoginList 空)。QQ 主进程(V1)QQ 自己已配置
|
|
2770
|
+
* engine,无需解析。
|
|
2771
|
+
*/
|
|
2772
|
+
function resolveBootQqGlobalPath(ctx) {
|
|
2773
|
+
if (electronProcessType() === "browser") return;
|
|
2774
|
+
const root = resolveQqUserDataRoot(ctx.exports);
|
|
2775
|
+
if (root === null) return;
|
|
2776
|
+
return resolveQqGlobalPath(root);
|
|
2777
|
+
}
|
|
2778
|
+
/**
|
|
2779
|
+
* 解析 loginService:优先捕获实例,其次单例 get()(自建宿主/worker 下 QQ 环境
|
|
2780
|
+
* 创建的实例才完整——`new` 自建实例读不到登录列表(getLoginList 空),
|
|
2781
|
+
* HANDOVER-V6 p0-login3 实证),最后 new 回退。
|
|
2782
|
+
*/
|
|
2783
|
+
function resolveLoginService(ctx, qqLoginService) {
|
|
2784
|
+
if (qqLoginService !== void 0 && qqLoginService !== null) {
|
|
2785
|
+
ctx.loginService = qqLoginService;
|
|
2786
|
+
return;
|
|
2787
|
+
}
|
|
2788
|
+
try {
|
|
2789
|
+
const loginSvcCtor = ctx.exports.NodeIKernelLoginService;
|
|
2790
|
+
if (loginSvcCtor !== null && typeof loginSvcCtor.get === "function") ctx.loginService = loginSvcCtor.get();
|
|
2791
|
+
else ctx.loginService = new ctx.exports.NodeIKernelLoginService();
|
|
2792
|
+
} catch {
|
|
2793
|
+
ctx.loginService = null;
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2567
2796
|
//#endregion
|
|
2568
2797
|
//#region src/core.ts
|
|
2569
2798
|
/**
|
|
@@ -2686,9 +2915,14 @@ var NapukettoCore = class NapukettoCore {
|
|
|
2686
2915
|
session.onQrCode((qr) => {
|
|
2687
2916
|
this.ctx.logger.warn(`请扫描二维码登录(保存: ${qrPath} | URL: ${qr.qrcodeUrl})`);
|
|
2688
2917
|
if (qr.pngBase64 !== "") writeQrCodePng(qrPath, qr.pngBase64);
|
|
2918
|
+
opts.onLoginProgress?.({
|
|
2919
|
+
state: "waiting_scan",
|
|
2920
|
+
qr
|
|
2921
|
+
});
|
|
2689
2922
|
});
|
|
2690
2923
|
session.onStateChange((state) => {
|
|
2691
2924
|
this.ctx.logger.info({ state }, "QR 登录状态");
|
|
2925
|
+
opts.onLoginProgress?.({ state });
|
|
2692
2926
|
});
|
|
2693
2927
|
const startOpts = {};
|
|
2694
2928
|
if (opts.quickUin !== void 0) startOpts.quickUin = opts.quickUin;
|
|
@@ -2754,6 +2988,7 @@ var NTEventChannel = class {
|
|
|
2754
2988
|
serviceName;
|
|
2755
2989
|
emitter = new EventEmitter();
|
|
2756
2990
|
errorHandlers = /* @__PURE__ */ new Set();
|
|
2991
|
+
anyHandlers = /* @__PURE__ */ new Set();
|
|
2757
2992
|
constructor(serviceName) {
|
|
2758
2993
|
this.serviceName = serviceName;
|
|
2759
2994
|
this.emitter.setMaxListeners(0);
|
|
@@ -2765,170 +3000,69 @@ var NTEventChannel = class {
|
|
|
2765
3000
|
on(event, handler) {
|
|
2766
3001
|
this.emitter.addListener(event, handler);
|
|
2767
3002
|
return () => {
|
|
2768
|
-
this.emitter.removeListener(event, handler);
|
|
2769
|
-
};
|
|
2770
|
-
}
|
|
2771
|
-
/**
|
|
2772
|
-
* 请求-响应桥:注册临时监听 → filter 命中 → resolve;超时自动清理并 reject。
|
|
2773
|
-
* 替代魔法字符串事件名方案。
|
|
2774
|
-
*/
|
|
2775
|
-
waitFor(event, opts = {}) {
|
|
2776
|
-
const timeoutMs = opts.timeout ?? DEFAULT_WAIT_TIMEOUT_MS;
|
|
2777
|
-
return new Promise((resolve, reject) => {
|
|
2778
|
-
const handler = (...args) => {
|
|
2779
|
-
if (opts.filter && !opts.filter(...args)) return;
|
|
2780
|
-
cleanup();
|
|
2781
|
-
resolve(args);
|
|
2782
|
-
};
|
|
2783
|
-
const timer = setTimeout(() => {
|
|
2784
|
-
cleanup();
|
|
2785
|
-
reject(new KernelError(`等待事件超时: ${event}`, "TIMEOUT"));
|
|
2786
|
-
}, timeoutMs);
|
|
2787
|
-
const cleanup = () => {
|
|
2788
|
-
clearTimeout(timer);
|
|
2789
|
-
this.emitter.removeListener(event, handler);
|
|
2790
|
-
};
|
|
2791
|
-
this.emitter.addListener(event, handler);
|
|
2792
|
-
});
|
|
2793
|
-
}
|
|
2794
|
-
/**
|
|
2795
|
-
* 类型化派发:wrapper-loader 的原生回调经此推入通道。
|
|
2796
|
-
* 2026-08-07 修复:原生 EventEmitter.emit 中一个 listener 抛异常会中断后续
|
|
2797
|
-
* listener 派发(Node 语义)——改为快照数组逐个 try/catch,保证「单个订阅者
|
|
2798
|
-
* 异常不打断派发」(协议翻译层异常不饿死缓存维护等其他订阅者)。
|
|
2799
|
-
*/
|
|
2800
|
-
emit(event, ...args) {
|
|
2801
|
-
const listeners = this.emitter.listeners(event);
|
|
2802
|
-
for (const listener of listeners) try {
|
|
2803
|
-
listener(...args);
|
|
2804
|
-
} catch (err) {
|
|
2805
|
-
for (const handler of this.errorHandlers) handler(err);
|
|
2806
|
-
}
|
|
2807
|
-
}
|
|
2808
|
-
/** 订阅错误通知(监听器抛出的异常 / 其他内部错误)。 */
|
|
2809
|
-
onError(handler) {
|
|
2810
|
-
this.errorHandlers.add(handler);
|
|
2811
|
-
return () => {
|
|
2812
|
-
this.errorHandlers.delete(handler);
|
|
2813
|
-
};
|
|
2814
|
-
}
|
|
2815
|
-
};
|
|
2816
|
-
//#endregion
|
|
2817
|
-
//#region src/infra/config.ts
|
|
2818
|
-
/**
|
|
2819
|
-
* 配置基类(ADR-012)
|
|
2820
|
-
*
|
|
2821
|
-
* 读文件 → 校验 → 内存对象 → 变更写回。支持 **JSON / TOML** 两种文本格式
|
|
2822
|
-
* (按扩展名推断:.toml → TOML(smol-toml),其余 → JSON)。
|
|
2823
|
-
*
|
|
2824
|
-
* kernel 不依赖 zod:只依赖校验器的最小 `parse` 形状(`ConfigSchema`),
|
|
2825
|
-
* 协议包的 zod schema 天然满足;kernel 主配置用手写校验器包装。
|
|
2826
|
-
* 协议配置 schema 在各自协议包,见各包 docs/design.md。
|
|
2827
|
-
*/
|
|
2828
|
-
/** 按扩展名推断格式(.toml → toml,其余 → json)。 */
|
|
2829
|
-
function inferFormat(path) {
|
|
2830
|
-
if (extname(path).toLowerCase() === ".toml") return "toml";
|
|
2831
|
-
return "json";
|
|
2832
|
-
}
|
|
2833
|
-
function isMissingFileError(err) {
|
|
2834
|
-
if (err instanceof Error) return err.code === "ENOENT";
|
|
2835
|
-
return false;
|
|
2836
|
-
}
|
|
2837
|
-
/**
|
|
2838
|
-
* 配置基类。每次 `load()` / `save()` 都经过 schema 校验,
|
|
2839
|
-
* 校验失败抛 `KernelError('INVALID_PARAM')`,不静默吞掉。
|
|
2840
|
-
* - `load()`:seed 存在 → 直接用 seed;否则读文件(缺失落默认值)。
|
|
2841
|
-
* - `save()`:按格式序列化(TOML/JSON)原子写。
|
|
2842
|
-
*/
|
|
2843
|
-
var ConfigBase = class {
|
|
2844
|
-
path;
|
|
2845
|
-
defaults;
|
|
2846
|
-
schema;
|
|
2847
|
-
format;
|
|
2848
|
-
seed;
|
|
2849
|
-
value;
|
|
2850
|
-
constructor(opts) {
|
|
2851
|
-
this.path = opts.path;
|
|
2852
|
-
this.defaults = opts.defaults;
|
|
2853
|
-
this.schema = opts.schema;
|
|
2854
|
-
this.format = opts.format ?? inferFormat(opts.path);
|
|
2855
|
-
this.seed = opts.seed;
|
|
2856
|
-
this.value = opts.seed ?? opts.defaults;
|
|
2857
|
-
}
|
|
2858
|
-
/** 当前内存值(只读消费;外部不得直接修改返回对象)。 */
|
|
2859
|
-
get() {
|
|
2860
|
-
return this.value;
|
|
3003
|
+
this.emitter.removeListener(event, handler);
|
|
3004
|
+
};
|
|
2861
3005
|
}
|
|
2862
3006
|
/**
|
|
2863
|
-
*
|
|
2864
|
-
*
|
|
2865
|
-
* - 文件缺失 → 写入默认值并落盘(首次运行自动生成),返回默认值;
|
|
2866
|
-
* - 文件存在 → 读取 + 校验,更新内存,返回。
|
|
3007
|
+
* 请求-响应桥:注册临时监听 → filter 命中 → resolve;超时自动清理并 reject。
|
|
3008
|
+
* 替代魔法字符串事件名方案。
|
|
2867
3009
|
*/
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
/** 重新从磁盘加载(外部变更后的热更新入口,P2 配置热更新使用)。 */
|
|
2887
|
-
reload() {
|
|
2888
|
-
return this.load();
|
|
2889
|
-
}
|
|
2890
|
-
/** 校验并落盘(原子写:临时文件 + rename),随后更新内存。 */
|
|
2891
|
-
async save(next) {
|
|
2892
|
-
const parsed = this.parse(this.serialize(next));
|
|
2893
|
-
mkdirSync(dirname(this.path), { recursive: true });
|
|
2894
|
-
const tmpPath = `${this.path}.tmp`;
|
|
2895
|
-
await writeFile(tmpPath, `${this.serialize(parsed)}\n`, "utf8");
|
|
2896
|
-
await rename(tmpPath, this.path);
|
|
2897
|
-
this.value = parsed;
|
|
2898
|
-
}
|
|
2899
|
-
/** 对象 → 文本(TOML/JSON)。 */
|
|
2900
|
-
serialize(value) {
|
|
2901
|
-
if (this.format === "toml") return stringify(value);
|
|
2902
|
-
return JSON.stringify(value, null, 4);
|
|
3010
|
+
waitFor(event, opts = {}) {
|
|
3011
|
+
const timeoutMs = opts.timeout ?? DEFAULT_WAIT_TIMEOUT_MS;
|
|
3012
|
+
return new Promise((resolve, reject) => {
|
|
3013
|
+
const handler = (...args) => {
|
|
3014
|
+
if (opts.filter && !opts.filter(...args)) return;
|
|
3015
|
+
cleanup();
|
|
3016
|
+
resolve(args);
|
|
3017
|
+
};
|
|
3018
|
+
const timer = setTimeout(() => {
|
|
3019
|
+
cleanup();
|
|
3020
|
+
reject(new KernelError(`等待事件超时: ${event}`, "TIMEOUT"));
|
|
3021
|
+
}, timeoutMs);
|
|
3022
|
+
const cleanup = () => {
|
|
3023
|
+
clearTimeout(timer);
|
|
3024
|
+
this.emitter.removeListener(event, handler);
|
|
3025
|
+
};
|
|
3026
|
+
this.emitter.addListener(event, handler);
|
|
3027
|
+
});
|
|
2903
3028
|
}
|
|
2904
|
-
/**
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
3029
|
+
/**
|
|
3030
|
+
* 类型化派发:wrapper-loader 的原生回调经此推入通道。
|
|
3031
|
+
* 2026-08-07 修复:原生 EventEmitter.emit 中一个 listener 抛异常会中断后续
|
|
3032
|
+
* listener 派发(Node 语义)——改为快照数组逐个 try/catch,保证「单个订阅者
|
|
3033
|
+
* 异常不打断派发」(协议翻译层异常不饿死缓存维护等其他订阅者)。
|
|
3034
|
+
*/
|
|
3035
|
+
/**
|
|
3036
|
+
* 订阅所有事件(IPC 子进程模式整通道转发用:事件名 `"${service}/${method}"`
|
|
3037
|
+
* 经 handler 首参透出,args 原样透传)。返回退订函数。
|
|
3038
|
+
*/
|
|
3039
|
+
onAny(handler) {
|
|
3040
|
+
this.anyHandlers.add(handler);
|
|
3041
|
+
return () => {
|
|
3042
|
+
this.anyHandlers.delete(handler);
|
|
3043
|
+
};
|
|
2908
3044
|
}
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
try {
|
|
2913
|
-
input = this.deserialize(raw);
|
|
3045
|
+
emit(event, ...args) {
|
|
3046
|
+
for (const anyHandler of this.anyHandlers) try {
|
|
3047
|
+
anyHandler(event, ...args);
|
|
2914
3048
|
} catch (err) {
|
|
2915
|
-
|
|
3049
|
+
for (const errorHandler of this.errorHandlers) errorHandler(err);
|
|
2916
3050
|
}
|
|
2917
|
-
|
|
2918
|
-
|
|
3051
|
+
const listeners = this.emitter.listeners(event);
|
|
3052
|
+
for (const listener of listeners) try {
|
|
3053
|
+
listener(...args);
|
|
2919
3054
|
} catch (err) {
|
|
2920
|
-
|
|
3055
|
+
for (const handler of this.errorHandlers) handler(err);
|
|
2921
3056
|
}
|
|
2922
3057
|
}
|
|
3058
|
+
/** 订阅错误通知(监听器抛出的异常 / 其他内部错误)。 */
|
|
3059
|
+
onError(handler) {
|
|
3060
|
+
this.errorHandlers.add(handler);
|
|
3061
|
+
return () => {
|
|
3062
|
+
this.errorHandlers.delete(handler);
|
|
3063
|
+
};
|
|
3064
|
+
}
|
|
2923
3065
|
};
|
|
2924
|
-
/** 解析 TOML 文本 → 对象(boot.cjs / 探测脚本等无 ConfigBase 场景复用)。 */
|
|
2925
|
-
function parseToml(text) {
|
|
2926
|
-
return parse(text);
|
|
2927
|
-
}
|
|
2928
|
-
/** 对象 → TOML 文本(config init 生成全局配置文件等场景)。 */
|
|
2929
|
-
function stringifyToml(value) {
|
|
2930
|
-
return stringify(value);
|
|
2931
|
-
}
|
|
2932
3066
|
//#endregion
|
|
2933
3067
|
//#region src/wrapper/session-resolver.ts
|
|
2934
3068
|
/** 从 getSessionIdList 的 Map 提取主 sessionId(nt_ 前缀优先)。 */
|
|
@@ -2990,50 +3124,14 @@ function getMainSession(ctx) {
|
|
|
2990
3124
|
return null;
|
|
2991
3125
|
}
|
|
2992
3126
|
//#endregion
|
|
2993
|
-
//#region src/wrapper/probe.ts
|
|
3127
|
+
//#region src/wrapper/probe/probe-serialize.ts
|
|
2994
3128
|
/**
|
|
2995
|
-
*
|
|
2996
|
-
*
|
|
2997
|
-
* 注入链路打通后(2026-08-05),wrapper exports 是 QQ 已注册的单例。
|
|
2998
|
-
* 本模块在 **QQ 主进程内**反射枚举 session / service 的真实方法名与返回形状,
|
|
2999
|
-
* 产出 JSON 到 NAPUTO_CFG_DIR/napuketto-probe.json,作为 types/services/ 的权威来源。
|
|
3129
|
+
* 运行时反射探测的序列化工具(从 probe.ts 拆分,2026-08-08 FTA 优化)
|
|
3000
3130
|
*
|
|
3001
|
-
*
|
|
3002
|
-
*
|
|
3131
|
+
* - serialize:深度受限序列化(BigInt 转字符串,Map/Set/Promise 展开,循环引用防护)
|
|
3132
|
+
* - tryShape:解析 service getter 返回形状
|
|
3133
|
+
* - shapeKeyGetters:探测 service 关键数据 getter 返回形状
|
|
3003
3134
|
*/
|
|
3004
|
-
/** 反射枚举一个对象原型链上的方法名(去重、去构造器)。 */
|
|
3005
|
-
function listMethods(obj) {
|
|
3006
|
-
if (obj === null || obj === void 0) return [];
|
|
3007
|
-
const names = /* @__PURE__ */ new Set();
|
|
3008
|
-
let proto = Object.getPrototypeOf(obj);
|
|
3009
|
-
while (proto && proto !== Object.prototype && proto !== Function.prototype) {
|
|
3010
|
-
for (const name of Object.getOwnPropertyNames(proto)) if (name !== "constructor") names.add(name);
|
|
3011
|
-
proto = Object.getPrototypeOf(proto);
|
|
3012
|
-
}
|
|
3013
|
-
for (const name of Object.getOwnPropertyNames(obj)) if (name !== "constructor") names.add(name);
|
|
3014
|
-
return [...names].sort();
|
|
3015
|
-
}
|
|
3016
|
-
/** 安全调用:尝试调用方法拿返回值,失败记录错误(不中断探测)。 */
|
|
3017
|
-
function tryCall(obj, name) {
|
|
3018
|
-
try {
|
|
3019
|
-
const fn = obj[name];
|
|
3020
|
-
if (typeof fn !== "function") return {
|
|
3021
|
-
ok: false,
|
|
3022
|
-
error: `not a function (typeof=${typeof fn})`
|
|
3023
|
-
};
|
|
3024
|
-
return {
|
|
3025
|
-
ok: true,
|
|
3026
|
-
value: fn.call(obj)
|
|
3027
|
-
};
|
|
3028
|
-
} catch (e) {
|
|
3029
|
-
let message = String(e);
|
|
3030
|
-
if (e instanceof Error) message = e.message;
|
|
3031
|
-
return {
|
|
3032
|
-
ok: false,
|
|
3033
|
-
error: message
|
|
3034
|
-
};
|
|
3035
|
-
}
|
|
3036
|
-
}
|
|
3037
3135
|
/** 序列化深度上限(循环引用/深层对象防护)。 */
|
|
3038
3136
|
const MAX_SERIALIZE_DEPTH = 4;
|
|
3039
3137
|
/** 数组序列化条数上限。 */
|
|
@@ -3048,34 +3146,52 @@ function serialize(value, depth = 0) {
|
|
|
3048
3146
|
if (typeof value !== "object") return value;
|
|
3049
3147
|
if (depth > MAX_SERIALIZE_DEPTH) return "[depth-limit]";
|
|
3050
3148
|
try {
|
|
3051
|
-
|
|
3052
|
-
if (value instanceof Map) {
|
|
3053
|
-
const out = {};
|
|
3054
|
-
let i = 0;
|
|
3055
|
-
for (const [k, v] of value) {
|
|
3056
|
-
if (i >= MAX_OBJECT_KEYS) break;
|
|
3057
|
-
out[String(k)] = serialize(v, depth + 1);
|
|
3058
|
-
i += 1;
|
|
3059
|
-
}
|
|
3060
|
-
return {
|
|
3061
|
-
kind: "Map",
|
|
3062
|
-
size: value.size,
|
|
3063
|
-
entries: out
|
|
3064
|
-
};
|
|
3065
|
-
}
|
|
3066
|
-
if (value instanceof Set) return {
|
|
3067
|
-
kind: "Set",
|
|
3068
|
-
size: value.size,
|
|
3069
|
-
values: [...value].slice(0, MAX_ARRAY_ITEMS).map((v) => serialize(v, depth + 1))
|
|
3070
|
-
};
|
|
3071
|
-
if (typeof value.then === "function") return "[Promise]";
|
|
3072
|
-
const out = {};
|
|
3073
|
-
for (const key of Object.keys(value).slice(0, MAX_OBJECT_KEYS)) out[key] = serialize(value[key], depth + 1);
|
|
3074
|
-
return out;
|
|
3149
|
+
return serializeContainer(value, depth);
|
|
3075
3150
|
} catch {
|
|
3076
3151
|
return "[unserializable]";
|
|
3077
3152
|
}
|
|
3078
3153
|
}
|
|
3154
|
+
/** 序列化容器对象(数组 / Map / Set / Promise / 普通对象)。 */
|
|
3155
|
+
function serializeContainer(value, depth) {
|
|
3156
|
+
if (Array.isArray(value)) return serializeArray(value, depth);
|
|
3157
|
+
if (value instanceof Map) return serializeMap(value, depth);
|
|
3158
|
+
if (value instanceof Set) return serializeSet(value, depth);
|
|
3159
|
+
if (typeof value.then === "function") return "[Promise]";
|
|
3160
|
+
return serializeObject(value, depth);
|
|
3161
|
+
}
|
|
3162
|
+
/** 数组序列化(条数截断)。 */
|
|
3163
|
+
function serializeArray(value, depth) {
|
|
3164
|
+
return value.slice(0, MAX_ARRAY_ITEMS).map((v) => serialize(v, depth + 1));
|
|
3165
|
+
}
|
|
3166
|
+
/** Map 序列化(键转字符串,条目截断)。 */
|
|
3167
|
+
function serializeMap(value, depth) {
|
|
3168
|
+
const out = {};
|
|
3169
|
+
let i = 0;
|
|
3170
|
+
for (const [k, v] of value) {
|
|
3171
|
+
if (i >= MAX_OBJECT_KEYS) break;
|
|
3172
|
+
out[String(k)] = serialize(v, depth + 1);
|
|
3173
|
+
i += 1;
|
|
3174
|
+
}
|
|
3175
|
+
return {
|
|
3176
|
+
kind: "Map",
|
|
3177
|
+
size: value.size,
|
|
3178
|
+
entries: out
|
|
3179
|
+
};
|
|
3180
|
+
}
|
|
3181
|
+
/** Set 序列化(值截断)。 */
|
|
3182
|
+
function serializeSet(value, depth) {
|
|
3183
|
+
return {
|
|
3184
|
+
kind: "Set",
|
|
3185
|
+
size: value.size,
|
|
3186
|
+
values: [...value].slice(0, MAX_ARRAY_ITEMS).map((v) => serialize(v, depth + 1))
|
|
3187
|
+
};
|
|
3188
|
+
}
|
|
3189
|
+
/** 普通对象序列化(键截断)。 */
|
|
3190
|
+
function serializeObject(value, depth) {
|
|
3191
|
+
const out = {};
|
|
3192
|
+
for (const key of Object.keys(value).slice(0, MAX_OBJECT_KEYS)) out[key] = serialize(value[key], depth + 1);
|
|
3193
|
+
return out;
|
|
3194
|
+
}
|
|
3079
3195
|
/** 尝试解析一个 service getter 的返回值形状(深度受限)。 */
|
|
3080
3196
|
function tryShape(session, getter) {
|
|
3081
3197
|
try {
|
|
@@ -3097,6 +3213,49 @@ function shapeKeyGetters(session, serviceMethods) {
|
|
|
3097
3213
|
for (const m of candidates.slice(0, 8)) out[m] = tryShape(session, m);
|
|
3098
3214
|
return out;
|
|
3099
3215
|
}
|
|
3216
|
+
//#endregion
|
|
3217
|
+
//#region src/wrapper/probe/probe-utils.ts
|
|
3218
|
+
/**
|
|
3219
|
+
* 运行时反射探测基础工具(从 probe.ts 拆分,2026-08-08 FTA 优化)
|
|
3220
|
+
*
|
|
3221
|
+
* - listMethods:反射枚举对象原型链上的方法名(去重、去构造器)
|
|
3222
|
+
* - tryCall:安全调用方法拿返回值(失败记录错误,不中断探测)
|
|
3223
|
+
*/
|
|
3224
|
+
/** 反射枚举一个对象原型链上的方法名(去重、去构造器)。 */
|
|
3225
|
+
function listMethods(obj) {
|
|
3226
|
+
if (obj === null || obj === void 0) return [];
|
|
3227
|
+
const names = /* @__PURE__ */ new Set();
|
|
3228
|
+
let proto = Object.getPrototypeOf(obj);
|
|
3229
|
+
while (proto && proto !== Object.prototype && proto !== Function.prototype) {
|
|
3230
|
+
for (const name of Object.getOwnPropertyNames(proto)) if (name !== "constructor") names.add(name);
|
|
3231
|
+
proto = Object.getPrototypeOf(proto);
|
|
3232
|
+
}
|
|
3233
|
+
for (const name of Object.getOwnPropertyNames(obj)) if (name !== "constructor") names.add(name);
|
|
3234
|
+
return [...names].sort();
|
|
3235
|
+
}
|
|
3236
|
+
/** 安全调用:尝试调用方法拿返回值,失败记录错误(不中断探测)。 */
|
|
3237
|
+
function tryCall(obj, name) {
|
|
3238
|
+
try {
|
|
3239
|
+
const fn = obj[name];
|
|
3240
|
+
if (typeof fn !== "function") return {
|
|
3241
|
+
ok: false,
|
|
3242
|
+
error: `not a function (typeof=${typeof fn})`
|
|
3243
|
+
};
|
|
3244
|
+
return {
|
|
3245
|
+
ok: true,
|
|
3246
|
+
value: fn.call(obj)
|
|
3247
|
+
};
|
|
3248
|
+
} catch (e) {
|
|
3249
|
+
let message = String(e);
|
|
3250
|
+
if (e instanceof Error) message = e.message;
|
|
3251
|
+
return {
|
|
3252
|
+
ok: false,
|
|
3253
|
+
error: message
|
|
3254
|
+
};
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
//#endregion
|
|
3258
|
+
//#region src/wrapper/probe/probe-services.ts
|
|
3100
3259
|
/** 探测关键 export 构造器方法(get/create/getNTWrapperSession 等)。 */
|
|
3101
3260
|
function probeExportCtors(ctx) {
|
|
3102
3261
|
const keys = [
|
|
@@ -3154,6 +3313,22 @@ function probeLoginService(ctx) {
|
|
|
3154
3313
|
]) if (typeof inst.value[m] === "function") out[m] = tryShape(inst.value, m);
|
|
3155
3314
|
return out;
|
|
3156
3315
|
}
|
|
3316
|
+
//#endregion
|
|
3317
|
+
//#region src/wrapper/probe/probe.ts
|
|
3318
|
+
/**
|
|
3319
|
+
* 运行时反射探测(ADR-006 落地,P1-4)
|
|
3320
|
+
*
|
|
3321
|
+
* 注入链路打通后(2026-08-05),wrapper exports 是 QQ 已注册的单例。
|
|
3322
|
+
* 本模块在 **QQ 主进程内**反射枚举 session / service 的真实方法名与返回形状,
|
|
3323
|
+
* 产出 JSON 到 NAPUTO_CFG_DIR/napuketto-probe.json,作为 types/services/ 的权威来源。
|
|
3324
|
+
*
|
|
3325
|
+
* 用法:boot.cjs 在 NAPUTO_PROBE=1 时于 startNapuketto 后调用 probeRuntime(ctx)。
|
|
3326
|
+
* 产物是研究工具数据,类型层据此补全(自研描述,非移植)。
|
|
3327
|
+
*
|
|
3328
|
+
* 工具拆分(2026-08-08 FTA 优化):方法枚举/安全调用 → probe-utils.ts;
|
|
3329
|
+
* 序列化/形状探测 → probe-serialize.ts;exports/service 探测 → probe-services.ts;
|
|
3330
|
+
* 本文件只留 session 探测编排(probeStartup/probeSession/probeRuntime)。
|
|
3331
|
+
*/
|
|
3157
3332
|
/** 枚举候选 sessionId(nt_0..nt_9 / gpro_0..gpro_9)找 QQ 已 init 的 session。 */
|
|
3158
3333
|
function enumerateSessionIds(ctx) {
|
|
3159
3334
|
const S = ctx.exports.NodeIQQNTWrapperSession;
|
|
@@ -3184,47 +3359,38 @@ function enumerateSessionIds(ctx) {
|
|
|
3184
3359
|
} catch {}
|
|
3185
3360
|
return out;
|
|
3186
3361
|
}
|
|
3187
|
-
/** 探测 startup
|
|
3188
|
-
function
|
|
3189
|
-
const
|
|
3190
|
-
|
|
3191
|
-
const
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
value: serialize(created.value)
|
|
3196
|
-
} : { error: created.error };
|
|
3197
|
-
if (created.ok && created.value) {
|
|
3198
|
-
const started = tryCall(created.value, "start");
|
|
3199
|
-
out["start"] = started.ok ? { ok: true } : { error: started.error };
|
|
3200
|
-
const ids = tryCall(created.value, "getSessionIdList");
|
|
3201
|
-
const idsValue = ids.ok ? serialize(ids.value) : null;
|
|
3202
|
-
out["createdGetSessionIdList"] = ids.ok ? { value: idsValue } : { error: ids.error };
|
|
3203
|
-
const foundIds = [];
|
|
3204
|
-
if (ids.ok && ids.value instanceof Map) for (const [k, v] of ids.value) {
|
|
3205
|
-
const id = typeof v === "string" ? v : String(k);
|
|
3206
|
-
foundIds.push(id);
|
|
3207
|
-
}
|
|
3208
|
-
out["sessionIds"] = foundIds;
|
|
3209
|
-
if (foundIds.length > 0) {
|
|
3210
|
-
const mainId = foundIds.find((id) => id.startsWith("nt_")) ?? foundIds[0];
|
|
3211
|
-
out["mainSessionId"] = mainId;
|
|
3212
|
-
const S = ctx.exports.NodeIQQNTWrapperSession;
|
|
3213
|
-
const fn = S["getNTWrapperSession"];
|
|
3214
|
-
if (typeof fn === "function" && mainId !== void 0) try {
|
|
3215
|
-
const value = fn.call(S, mainId);
|
|
3216
|
-
out["mainSession"] = value ? {
|
|
3217
|
-
methods: listMethods(value),
|
|
3218
|
-
ownKeys: Object.getOwnPropertyNames(value)
|
|
3219
|
-
} : null;
|
|
3220
|
-
} catch (e) {
|
|
3221
|
-
out["mainSession"] = { error: e instanceof Error ? e.message : String(e) };
|
|
3222
|
-
}
|
|
3223
|
-
out["enumSessionIds"] = enumerateSessionIds(ctx);
|
|
3224
|
-
return out;
|
|
3225
|
-
}
|
|
3362
|
+
/** 探测 startup 实例:start() + getSessionIdList(),提取 sessionIds(Map 值或键)。 */
|
|
3363
|
+
function probeStartupChain(created) {
|
|
3364
|
+
const started = tryCall(created, "start");
|
|
3365
|
+
const ids = tryCall(created, "getSessionIdList");
|
|
3366
|
+
const sessionIds = [];
|
|
3367
|
+
if (ids.ok && ids.value instanceof Map) for (const [k, v] of ids.value) {
|
|
3368
|
+
const id = typeof v === "string" ? v : String(k);
|
|
3369
|
+
sessionIds.push(id);
|
|
3226
3370
|
}
|
|
3227
|
-
|
|
3371
|
+
return {
|
|
3372
|
+
start: started.ok ? { ok: true } : { error: started.error },
|
|
3373
|
+
createdGetSessionIdList: ids.ok ? { value: serialize(ids.value) } : { error: ids.error },
|
|
3374
|
+
sessionIds
|
|
3375
|
+
};
|
|
3376
|
+
}
|
|
3377
|
+
/** 用真实 sessionId 取主 session(getNTWrapperSession(nt_x))并记录方法面。 */
|
|
3378
|
+
function probeSessionById(ctx, id) {
|
|
3379
|
+
const S = ctx.exports.NodeIQQNTWrapperSession;
|
|
3380
|
+
const fn = S["getNTWrapperSession"];
|
|
3381
|
+
if (typeof fn !== "function") return null;
|
|
3382
|
+
try {
|
|
3383
|
+
const value = fn.call(S, id);
|
|
3384
|
+
return value ? {
|
|
3385
|
+
methods: listMethods(value),
|
|
3386
|
+
ownKeys: Object.getOwnPropertyNames(value)
|
|
3387
|
+
} : null;
|
|
3388
|
+
} catch (e) {
|
|
3389
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3392
|
+
/** 兜底:候选名字 getNTWrapperSession(无 Map 数据时)。 */
|
|
3393
|
+
function probeByNameFallback(ctx) {
|
|
3228
3394
|
const S = ctx.exports.NodeIQQNTWrapperSession;
|
|
3229
3395
|
const candidates = [
|
|
3230
3396
|
"main",
|
|
@@ -3244,7 +3410,31 @@ function probeStartup(ctx) {
|
|
|
3244
3410
|
} catch (e) {
|
|
3245
3411
|
getResults[name === "" ? "<empty>" : name] = { error: e instanceof Error ? e.message : String(e) };
|
|
3246
3412
|
}
|
|
3247
|
-
|
|
3413
|
+
return getResults;
|
|
3414
|
+
}
|
|
3415
|
+
/** 探测 startup session 链路:create() → start() → getSessionIdList → getNTWrapperSession。 */
|
|
3416
|
+
function probeStartup(ctx) {
|
|
3417
|
+
const startup = ctx.exports.NodeIQQNTStartupSessionWrapper;
|
|
3418
|
+
if (startup === null || typeof startup.create !== "function") return null;
|
|
3419
|
+
const out = { staticMethods: listMethods(startup) };
|
|
3420
|
+
const created = tryCall(startup, "create");
|
|
3421
|
+
out["create"] = created.ok ? {
|
|
3422
|
+
methods: created.value ? listMethods(created.value) : [],
|
|
3423
|
+
value: serialize(created.value)
|
|
3424
|
+
} : { error: created.error };
|
|
3425
|
+
if (created.ok && created.value) {
|
|
3426
|
+
const chain = probeStartupChain(created.value);
|
|
3427
|
+
out["start"] = chain.start;
|
|
3428
|
+
out["createdGetSessionIdList"] = chain.createdGetSessionIdList;
|
|
3429
|
+
out["sessionIds"] = chain.sessionIds;
|
|
3430
|
+
const mainId = chain.sessionIds.find((id) => id.startsWith("nt_")) ?? chain.sessionIds[0];
|
|
3431
|
+
if (mainId !== void 0) {
|
|
3432
|
+
out["mainSessionId"] = mainId;
|
|
3433
|
+
out["mainSession"] = probeSessionById(ctx, mainId);
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
out["enumSessionIds"] = enumerateSessionIds(ctx);
|
|
3437
|
+
if (out["sessionIds"] === void 0) out["getNTWrapperSession"] = probeByNameFallback(ctx);
|
|
3248
3438
|
return out;
|
|
3249
3439
|
}
|
|
3250
3440
|
/** 探测 session 与 service 方法(优先复用 QQ 主 session)。 */
|