koishi-plugin-rocom 1.0.12 → 1.0.17
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/lib/atlas-service.d.ts +33 -0
- package/lib/atlas-service.js +439 -0
- package/lib/client.d.ts +36 -1
- package/lib/client.js +221 -21
- package/lib/commands/account.js +1 -0
- package/lib/commands/merchant.js +351 -91
- package/lib/commands/query.js +162 -14
- package/lib/commands/tools.js +104 -0
- package/lib/commands/wiki.js +167 -6
- package/lib/egg-service.d.ts +2 -0
- package/lib/egg-service.js +50 -15
- package/lib/index.d.ts +9 -0
- package/lib/index.js +3626 -303
- package/lib/pet-data-service.d.ts +62 -0
- package/lib/pet-data-service.js +468 -0
- package/lib/render-templates/home/img/rocomuid/a_title.png +0 -0
- package/lib/render-templates/home/img/rocomuid/banner_bg.png +0 -0
- package/lib/render-templates/home/img/rocomuid/bg.jpg +0 -0
- package/lib/render-templates/home/img/rocomuid/img_head.png +0 -0
- package/lib/render-templates/home/img/rocomuid/jindu_bar.png +0 -0
- package/lib/render-templates/home/img/rocomuid/jindu_tc.png +0 -0
- package/lib/render-templates/home/img/rocomuid/level_icon.png +0 -0
- package/lib/render-templates/home/img/rocomuid/pet_bg.png +0 -0
- package/lib/render-templates/home/img/rocomuid/plant_bg.png +0 -0
- package/lib/render-templates/home/img/rocomuid/star_1.png +0 -0
- package/lib/render-templates/home/img/rocomuid/star_8.png +0 -0
- package/lib/render-templates/home/img/rocomuid/star_9.png +0 -0
- package/lib/render-templates/home/img/rocomuid/title_fg.png +0 -0
- package/lib/render-templates/home/img/rocomuid/top_bg.png +0 -0
- package/lib/render-templates/home/img/rocomuid/touxiang_mask.png +0 -0
- package/lib/render-templates/home/index.html +81 -97
- package/lib/render-templates/home/style.css +316 -375
- package/lib/render-templates/pet-data/index.html +172 -0
- package/lib/render-templates/pet-data/style.css +553 -0
- package/lib/render-templates/wiki/detail/index.html +84 -0
- package/lib/render-templates/wiki/list/index.html +84 -0
- package/lib/render-templates/wiki/menu/index.html +65 -0
- package/lib/render-templates/wiki/pet/index.html +158 -0
- package/lib/render-templates/wiki/pet/style.css +393 -0
- package/lib/render-templates/wiki/shared/style.css +431 -0
- package/lib/render-templates/wiki/skill/index.html +79 -0
- package/lib/render-templates/wiki/skill/style.css +233 -0
- package/lib/render-templates/wiki/suggestions/index.html +84 -0
- package/lib/render-templates/yuanxing-shangren/CHANGELOG.md +138 -0
- package/lib/render-templates/yuanxing-shangren/README.md +86 -0
- package/lib/render-templates/yuanxing-shangren/img/coin.png +0 -0
- package/lib/render-templates/yuanxing-shangren/img//345/272/225/351/203/250/344/277/241/346/201/257/346/241/206.png +0 -0
- package/lib/render-templates/yuanxing-shangren/img//346/240/207/351/242/230.png +0 -0
- package/lib/render-templates/yuanxing-shangren/img//347/203/255/351/224/200.png +0 -0
- package/lib/render-templates/yuanxing-shangren/img//347/274/226/345/217/267/350/203/214/346/231/257/346/241/206.png +0 -0
- package/lib/render-templates/yuanxing-shangren/index.html +198 -170
- package/lib/render-templates/yuanxing-shangren/index.html.backup +356 -0
- package/lib/render-templates/yuanxing-shangren/merchant.html +461 -0
- package/lib/render-templates/yuanxing-shangren/style.css +201 -256
- package/lib/render-templates/yuanxing-shangren/today.html +461 -0
- package/lib/render-templates/yuanxing-shangren/today.style.css +366 -0
- package/lib/render.js +48 -12
- package/lib/subscription-send.js +4 -5
- package/lib/ttf/rocom_skill_origin.ttf +0 -0
- package/lib/ttf//351/200/240/345/255/227/345/267/245/346/210/277/345/220/257/351/273/221/344/275/223.ttf +0 -0
- package/lib/types.d.ts +13 -1
- package/lib/user.d.ts +23 -1
- package/lib/user.js +32 -1
- package/lib/wiki-catalog.d.ts +19 -0
- package/lib/wiki-catalog.js +124 -0
- package/lib/wiki-service.d.ts +220 -0
- package/lib/wiki-service.js +1463 -0
- package/package.json +1 -1
- package/readme.md +4 -5
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Context } from 'koishi';
|
|
2
|
+
export type AtlasProgressCallback = (percent: number, stage: string) => void | Promise<void>;
|
|
3
|
+
export declare class AtlasService {
|
|
4
|
+
private dataDir;
|
|
5
|
+
private zipUrls;
|
|
6
|
+
private gitUrl;
|
|
7
|
+
constructor(dataDir: string, options?: {
|
|
8
|
+
zipUrls?: string[];
|
|
9
|
+
gitUrl?: string;
|
|
10
|
+
});
|
|
11
|
+
get atlasDir(): string;
|
|
12
|
+
private indexPath;
|
|
13
|
+
private petsDir;
|
|
14
|
+
private aliasPath;
|
|
15
|
+
isReady(): boolean;
|
|
16
|
+
loadIndex(): Record<string, string>;
|
|
17
|
+
private stripYamlValue;
|
|
18
|
+
loadPetAliases(): Record<string, string[]>;
|
|
19
|
+
private localImagePath;
|
|
20
|
+
private existingImagePath;
|
|
21
|
+
findMatch(query: string): {
|
|
22
|
+
name: string;
|
|
23
|
+
imagePath: string;
|
|
24
|
+
candidates: string[];
|
|
25
|
+
};
|
|
26
|
+
private prepareAtlasDir;
|
|
27
|
+
private replaceAtlasDir;
|
|
28
|
+
private downloadZipWithProgress;
|
|
29
|
+
download(ctx: Context, progressCb?: AtlasProgressCallback): Promise<{
|
|
30
|
+
imageCount: number;
|
|
31
|
+
totalBytes: number;
|
|
32
|
+
}>;
|
|
33
|
+
}
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.AtlasService = void 0;
|
|
7
|
+
const koishi_1 = require("koishi");
|
|
8
|
+
const node_child_process_1 = require("node:child_process");
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const node_zlib_1 = __importDefault(require("node:zlib"));
|
|
12
|
+
const wiki_service_1 = require("./wiki-service");
|
|
13
|
+
const logger = new koishi_1.Logger('rocom-atlas');
|
|
14
|
+
const DEFAULT_ATLAS_ZIP_URLS = [
|
|
15
|
+
'https://codeload.github.com/Entropy-Increase-Team/Rocom-Atlas/zip/refs/heads/main',
|
|
16
|
+
'https://github.com/Entropy-Increase-Team/Rocom-Atlas/archive/refs/heads/main.zip',
|
|
17
|
+
];
|
|
18
|
+
const DEFAULT_ATLAS_GIT_URL = 'https://github.com/Entropy-Increase-Team/Rocom-Atlas.git';
|
|
19
|
+
// ===== 最小 ZIP 解包(stored + deflate),避免引入额外依赖 =====
|
|
20
|
+
function extractZip(buffer, destDir) {
|
|
21
|
+
// 定位 End of Central Directory(自尾部反向搜索签名 0x06054b50)
|
|
22
|
+
let eocd = -1;
|
|
23
|
+
const minEocd = Math.max(0, buffer.length - 65557);
|
|
24
|
+
for (let i = buffer.length - 22; i >= minEocd; i--) {
|
|
25
|
+
if (buffer.readUInt32LE(i) === 0x06054b50) {
|
|
26
|
+
eocd = i;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (eocd < 0)
|
|
31
|
+
throw new Error('ZIP 结构异常:找不到中央目录结尾');
|
|
32
|
+
const entryCount = buffer.readUInt16LE(eocd + 10);
|
|
33
|
+
let offset = buffer.readUInt32LE(eocd + 16);
|
|
34
|
+
for (let i = 0; i < entryCount; i++) {
|
|
35
|
+
if (buffer.readUInt32LE(offset) !== 0x02014b50)
|
|
36
|
+
throw new Error('ZIP 结构异常:中央目录签名错误');
|
|
37
|
+
const compressionMethod = buffer.readUInt16LE(offset + 10);
|
|
38
|
+
const compressedSize = buffer.readUInt32LE(offset + 20);
|
|
39
|
+
const nameLength = buffer.readUInt16LE(offset + 28);
|
|
40
|
+
const extraLength = buffer.readUInt16LE(offset + 30);
|
|
41
|
+
const commentLength = buffer.readUInt16LE(offset + 32);
|
|
42
|
+
const localHeaderOffset = buffer.readUInt32LE(offset + 42);
|
|
43
|
+
const name = buffer.subarray(offset + 46, offset + 46 + nameLength).toString('utf8');
|
|
44
|
+
offset += 46 + nameLength + extraLength + commentLength;
|
|
45
|
+
// 拒绝路径穿越
|
|
46
|
+
const normalized = name.replace(/\\/g, '/');
|
|
47
|
+
if (normalized.includes('..'))
|
|
48
|
+
continue;
|
|
49
|
+
const target = node_path_1.default.join(destDir, ...normalized.split('/'));
|
|
50
|
+
if (!node_path_1.default.resolve(target).startsWith(node_path_1.default.resolve(destDir)))
|
|
51
|
+
continue;
|
|
52
|
+
if (normalized.endsWith('/')) {
|
|
53
|
+
node_fs_1.default.mkdirSync(target, { recursive: true });
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
// 读本地文件头拿数据真实偏移(本地头的 name/extra 长度可能与中央目录不同)
|
|
57
|
+
if (buffer.readUInt32LE(localHeaderOffset) !== 0x04034b50)
|
|
58
|
+
throw new Error('ZIP 结构异常:本地文件头签名错误');
|
|
59
|
+
const localNameLength = buffer.readUInt16LE(localHeaderOffset + 26);
|
|
60
|
+
const localExtraLength = buffer.readUInt16LE(localHeaderOffset + 28);
|
|
61
|
+
const dataStart = localHeaderOffset + 30 + localNameLength + localExtraLength;
|
|
62
|
+
const compressed = buffer.subarray(dataStart, dataStart + compressedSize);
|
|
63
|
+
let content;
|
|
64
|
+
if (compressionMethod === 0)
|
|
65
|
+
content = Buffer.from(compressed);
|
|
66
|
+
else if (compressionMethod === 8)
|
|
67
|
+
content = node_zlib_1.default.inflateRawSync(compressed);
|
|
68
|
+
else
|
|
69
|
+
throw new Error(`ZIP 不支持的压缩方式: ${compressionMethod}`);
|
|
70
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(target), { recursive: true });
|
|
71
|
+
node_fs_1.default.writeFileSync(target, content);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function gitClone(dst, gitUrl) {
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
// --progress 让 git 在非 TTY 下也输出克隆进度(写到 stderr),逐行转发到日志。
|
|
77
|
+
const child = (0, node_child_process_1.spawn)('git', ['clone', '--depth', '1', '--single-branch', '--progress', gitUrl, dst], { env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } });
|
|
78
|
+
const stderrLines = [];
|
|
79
|
+
let stderrTail = '';
|
|
80
|
+
child.stderr.on('data', (chunk) => {
|
|
81
|
+
stderrTail += chunk.toString('utf8');
|
|
82
|
+
// git 进度行用 \r 刷新,按 \r 和 \n 一起切
|
|
83
|
+
const lines = stderrTail.split(/[\r\n]+/);
|
|
84
|
+
stderrTail = lines.pop() || '';
|
|
85
|
+
for (const line of lines) {
|
|
86
|
+
const text = line.trim();
|
|
87
|
+
if (!text)
|
|
88
|
+
continue;
|
|
89
|
+
stderrLines.push(text);
|
|
90
|
+
logger.info(`git clone: ${text}`);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
const timer = setTimeout(() => {
|
|
94
|
+
child.kill();
|
|
95
|
+
reject(new Error('git clone Rocom-Atlas 超时(180 秒)'));
|
|
96
|
+
}, 180000);
|
|
97
|
+
child.on('error', (error) => {
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
reject(new Error(`git clone Rocom-Atlas 失败:${error.message}`));
|
|
100
|
+
});
|
|
101
|
+
child.on('close', (code) => {
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
if (code === 0)
|
|
104
|
+
resolve();
|
|
105
|
+
else
|
|
106
|
+
reject(new Error(`git clone Rocom-Atlas 失败(exit ${code}):${stderrLines.slice(-3).join(' | ')}`));
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
class AtlasService {
|
|
111
|
+
dataDir;
|
|
112
|
+
zipUrls;
|
|
113
|
+
gitUrl;
|
|
114
|
+
constructor(dataDir, options = {}) {
|
|
115
|
+
this.dataDir = dataDir;
|
|
116
|
+
this.zipUrls = options.zipUrls?.length ? options.zipUrls : DEFAULT_ATLAS_ZIP_URLS;
|
|
117
|
+
this.gitUrl = options.gitUrl || DEFAULT_ATLAS_GIT_URL;
|
|
118
|
+
}
|
|
119
|
+
get atlasDir() {
|
|
120
|
+
return node_path_1.default.join(this.dataDir, 'rocom_atlas');
|
|
121
|
+
}
|
|
122
|
+
indexPath() {
|
|
123
|
+
return node_path_1.default.join(this.atlasDir, 'path.json');
|
|
124
|
+
}
|
|
125
|
+
petsDir() {
|
|
126
|
+
return node_path_1.default.join(this.atlasDir, 'pets');
|
|
127
|
+
}
|
|
128
|
+
aliasPath() {
|
|
129
|
+
return node_path_1.default.join(this.atlasDir, 'othername', 'pets.yaml');
|
|
130
|
+
}
|
|
131
|
+
isReady() {
|
|
132
|
+
return node_fs_1.default.existsSync(this.indexPath()) && node_fs_1.default.existsSync(this.petsDir());
|
|
133
|
+
}
|
|
134
|
+
loadIndex() {
|
|
135
|
+
try {
|
|
136
|
+
const payload = JSON.parse(node_fs_1.default.readFileSync(this.indexPath(), 'utf-8'));
|
|
137
|
+
const pets = payload?.pets;
|
|
138
|
+
return pets && typeof pets === 'object' ? pets : {};
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
logger.warn(`读取本地图鉴索引失败: ${e}`);
|
|
142
|
+
return {};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
stripYamlValue(value) {
|
|
146
|
+
const text = String(value || '').trim();
|
|
147
|
+
if (text.length >= 2 && text[0] === text[text.length - 1] && ["'", '"'].includes(text[0])) {
|
|
148
|
+
return text.slice(1, -1).trim();
|
|
149
|
+
}
|
|
150
|
+
return text;
|
|
151
|
+
}
|
|
152
|
+
// 解析上游简单结构的 pets.yaml:`正式名:` + 缩进 `- 别名` 列表。
|
|
153
|
+
loadPetAliases() {
|
|
154
|
+
const filePath = this.aliasPath();
|
|
155
|
+
if (!node_fs_1.default.existsSync(filePath))
|
|
156
|
+
return {};
|
|
157
|
+
const aliases = {};
|
|
158
|
+
let currentName = '';
|
|
159
|
+
try {
|
|
160
|
+
const lines = node_fs_1.default.readFileSync(filePath, 'utf-8').split(/\r?\n/);
|
|
161
|
+
for (const line of lines) {
|
|
162
|
+
const stripped = line.trim();
|
|
163
|
+
if (!stripped || stripped.startsWith('#'))
|
|
164
|
+
continue;
|
|
165
|
+
if (!/^\s/.test(line) && stripped.endsWith(':')) {
|
|
166
|
+
currentName = this.stripYamlValue(stripped.slice(0, -1));
|
|
167
|
+
if (currentName) {
|
|
168
|
+
const key = (0, wiki_service_1.normalizeQueryText)(currentName);
|
|
169
|
+
aliases[key] = aliases[key] || [];
|
|
170
|
+
if (!aliases[key].includes(currentName))
|
|
171
|
+
aliases[key].push(currentName);
|
|
172
|
+
}
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (currentName && stripped.startsWith('-')) {
|
|
176
|
+
const alias = this.stripYamlValue(stripped.slice(1));
|
|
177
|
+
if (!alias)
|
|
178
|
+
continue;
|
|
179
|
+
const key = (0, wiki_service_1.normalizeQueryText)(alias);
|
|
180
|
+
aliases[key] = aliases[key] || [];
|
|
181
|
+
if (!aliases[key].includes(currentName))
|
|
182
|
+
aliases[key].push(currentName);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch (e) {
|
|
187
|
+
logger.warn(`读取本地图鉴别名失败: ${e}`);
|
|
188
|
+
return {};
|
|
189
|
+
}
|
|
190
|
+
return aliases;
|
|
191
|
+
}
|
|
192
|
+
localImagePath(atlasRelPath) {
|
|
193
|
+
const rel = String(atlasRelPath || '').replace(/\\/g, '/').replace(/^\/+/, '');
|
|
194
|
+
if (!rel)
|
|
195
|
+
return '';
|
|
196
|
+
const candidate = node_path_1.default.resolve(this.atlasDir, ...rel.split('/'));
|
|
197
|
+
const root = node_path_1.default.resolve(this.atlasDir);
|
|
198
|
+
if (!candidate.startsWith(root))
|
|
199
|
+
return '';
|
|
200
|
+
return candidate;
|
|
201
|
+
}
|
|
202
|
+
existingImagePath(index, name) {
|
|
203
|
+
const filePath = this.localImagePath(index[name] || '');
|
|
204
|
+
return filePath && node_fs_1.default.existsSync(filePath) ? filePath : '';
|
|
205
|
+
}
|
|
206
|
+
findMatch(query) {
|
|
207
|
+
const trimmed = String(query || '').trim();
|
|
208
|
+
if (!trimmed)
|
|
209
|
+
return { name: '', imagePath: '', candidates: [] };
|
|
210
|
+
if (/^\d+$/.test(trimmed)) {
|
|
211
|
+
const filePath = node_path_1.default.join(this.petsDir(), `${trimmed}.png`);
|
|
212
|
+
if (node_fs_1.default.existsSync(filePath))
|
|
213
|
+
return { name: `#${trimmed}`, imagePath: filePath, candidates: [] };
|
|
214
|
+
}
|
|
215
|
+
const index = this.loadIndex();
|
|
216
|
+
if (!Object.keys(index).length)
|
|
217
|
+
return { name: '', imagePath: '', candidates: [] };
|
|
218
|
+
const normalizedQuery = (0, wiki_service_1.normalizeQueryText)(trimmed);
|
|
219
|
+
let exactKey = '';
|
|
220
|
+
for (const name of Object.keys(index)) {
|
|
221
|
+
if ((0, wiki_service_1.normalizeQueryText)(name) === normalizedQuery) {
|
|
222
|
+
exactKey = name;
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (exactKey) {
|
|
227
|
+
const filePath = this.existingImagePath(index, exactKey);
|
|
228
|
+
if (filePath)
|
|
229
|
+
return { name: exactKey, imagePath: filePath, candidates: [] };
|
|
230
|
+
}
|
|
231
|
+
const aliasMap = this.loadPetAliases();
|
|
232
|
+
const exactAliasCandidates = (aliasMap[normalizedQuery] || []).filter(name => this.existingImagePath(index, name));
|
|
233
|
+
if (exactAliasCandidates.length) {
|
|
234
|
+
const only = exactAliasCandidates[0];
|
|
235
|
+
return { name: only, imagePath: this.existingImagePath(index, only), candidates: [] };
|
|
236
|
+
}
|
|
237
|
+
const candidates = [];
|
|
238
|
+
const addCandidate = (name) => {
|
|
239
|
+
if (!candidates.includes(name) && this.existingImagePath(index, name))
|
|
240
|
+
candidates.push(name);
|
|
241
|
+
};
|
|
242
|
+
for (const name of Object.keys(index)) {
|
|
243
|
+
const normalizedName = (0, wiki_service_1.normalizeQueryText)(name);
|
|
244
|
+
if (normalizedQuery && (normalizedName.includes(normalizedQuery) || normalizedQuery.includes(normalizedName))) {
|
|
245
|
+
addCandidate(name);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
for (const [alias, names] of Object.entries(aliasMap)) {
|
|
249
|
+
if (normalizedQuery && (alias.includes(normalizedQuery) || normalizedQuery.includes(alias))) {
|
|
250
|
+
for (const name of names)
|
|
251
|
+
addCandidate(name);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (candidates.length === 1) {
|
|
255
|
+
const only = candidates[0];
|
|
256
|
+
return { name: only, imagePath: this.existingImagePath(index, only), candidates: [] };
|
|
257
|
+
}
|
|
258
|
+
return { name: '', imagePath: '', candidates: candidates.slice(0, 10) };
|
|
259
|
+
}
|
|
260
|
+
prepareAtlasDir(sourceRoot, preparedDir) {
|
|
261
|
+
node_fs_1.default.mkdirSync(preparedDir, { recursive: true });
|
|
262
|
+
for (const name of ['path.json', 'index', 'othername', 'pets']) {
|
|
263
|
+
const src = node_path_1.default.join(sourceRoot, name);
|
|
264
|
+
const dst = node_path_1.default.join(preparedDir, name);
|
|
265
|
+
if (node_fs_1.default.existsSync(src)) {
|
|
266
|
+
node_fs_1.default.cpSync(src, dst, { recursive: true });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (!node_fs_1.default.existsSync(node_path_1.default.join(preparedDir, 'path.json')))
|
|
270
|
+
throw new Error('Atlas 缺少 path.json');
|
|
271
|
+
if (!node_fs_1.default.existsSync(node_path_1.default.join(preparedDir, 'othername', 'pets.yaml')))
|
|
272
|
+
throw new Error('Atlas 缺少 othername/pets.yaml');
|
|
273
|
+
const petsDir = node_path_1.default.join(preparedDir, 'pets');
|
|
274
|
+
if (!node_fs_1.default.existsSync(petsDir))
|
|
275
|
+
throw new Error('Atlas 缺少 pets 图片目录');
|
|
276
|
+
const imageCount = node_fs_1.default.readdirSync(petsDir).filter(name => name.toLowerCase().endsWith('.png')).length;
|
|
277
|
+
let totalBytes = 0;
|
|
278
|
+
const walk = (dir) => {
|
|
279
|
+
for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
280
|
+
const entryPath = node_path_1.default.join(dir, entry.name);
|
|
281
|
+
if (entry.isDirectory())
|
|
282
|
+
walk(entryPath);
|
|
283
|
+
else
|
|
284
|
+
totalBytes += node_fs_1.default.statSync(entryPath).size;
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
walk(preparedDir);
|
|
288
|
+
return { imageCount, totalBytes };
|
|
289
|
+
}
|
|
290
|
+
replaceAtlasDir(preparedDir) {
|
|
291
|
+
const target = node_path_1.default.resolve(this.atlasDir);
|
|
292
|
+
const dataRoot = node_path_1.default.resolve(this.dataDir);
|
|
293
|
+
if (!target.startsWith(dataRoot))
|
|
294
|
+
throw new Error('图鉴目标目录不在插件数据目录内');
|
|
295
|
+
if (node_fs_1.default.existsSync(target))
|
|
296
|
+
node_fs_1.default.rmSync(target, { recursive: true, force: true });
|
|
297
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(target), { recursive: true });
|
|
298
|
+
try {
|
|
299
|
+
node_fs_1.default.renameSync(preparedDir, target);
|
|
300
|
+
}
|
|
301
|
+
catch (e) {
|
|
302
|
+
// Windows 跨盘符(如 C: 临时目录 → D: 数据目录)rename 会抛 EXDEV/EPERM,回退为复制。
|
|
303
|
+
if (['EXDEV', 'EPERM'].includes(e?.code)) {
|
|
304
|
+
logger.info(`rename 失败(${e.code}),回退为复制方式迁移图鉴目录`);
|
|
305
|
+
node_fs_1.default.cpSync(preparedDir, target, { recursive: true });
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
throw e;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// 流式下载压缩包:每 2 秒输出一次已下载字节数/速度,避免大文件下载期间毫无动静。
|
|
313
|
+
async downloadZipWithProgress(ctx, url) {
|
|
314
|
+
const started = Date.now();
|
|
315
|
+
const res = await ctx.http(url, { responseType: 'stream', timeout: 180000 });
|
|
316
|
+
const totalHeader = Number(res.headers?.get?.('content-length'));
|
|
317
|
+
const totalBytes = Number.isFinite(totalHeader) && totalHeader > 0 ? totalHeader : 0;
|
|
318
|
+
const totalText = totalBytes ? `${(totalBytes / 1024 / 1024).toFixed(1)} MB` : '未知大小';
|
|
319
|
+
logger.info(`开始下载图鉴压缩包:${url}(${totalText})`);
|
|
320
|
+
const chunks = [];
|
|
321
|
+
let received = 0;
|
|
322
|
+
let lastLogAt = Date.now();
|
|
323
|
+
const reader = res.data.getReader();
|
|
324
|
+
while (true) {
|
|
325
|
+
const { done, value } = await reader.read();
|
|
326
|
+
if (done)
|
|
327
|
+
break;
|
|
328
|
+
if (value) {
|
|
329
|
+
chunks.push(Buffer.from(value));
|
|
330
|
+
received += value.byteLength;
|
|
331
|
+
}
|
|
332
|
+
const now = Date.now();
|
|
333
|
+
if (now - lastLogAt >= 2000) {
|
|
334
|
+
lastLogAt = now;
|
|
335
|
+
const mb = (received / 1024 / 1024).toFixed(1);
|
|
336
|
+
const speed = (received / 1024 / (now - started) * 1000 / 1024).toFixed(2);
|
|
337
|
+
const percentText = totalBytes ? `${Math.min(99, Math.round(received / totalBytes * 100))}%,` : '';
|
|
338
|
+
logger.info(`图鉴压缩包下载中:${percentText}已收到 ${mb} MB,平均 ${speed} MB/s`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const elapsed = ((Date.now() - started) / 1000).toFixed(1);
|
|
342
|
+
logger.info(`图鉴压缩包下载完成:共 ${(received / 1024 / 1024).toFixed(1)} MB,耗时 ${elapsed} 秒`);
|
|
343
|
+
return Buffer.concat(chunks);
|
|
344
|
+
}
|
|
345
|
+
async download(ctx, progressCb) {
|
|
346
|
+
const emit = async (percent, stage) => {
|
|
347
|
+
if (progressCb)
|
|
348
|
+
await progressCb(Math.max(0, Math.min(100, Math.round(percent))), stage);
|
|
349
|
+
};
|
|
350
|
+
// 清理上次崩溃或中断留下的临时目录。
|
|
351
|
+
node_fs_1.default.mkdirSync(this.dataDir, { recursive: true });
|
|
352
|
+
for (const entry of node_fs_1.default.readdirSync(this.dataDir, { withFileTypes: true })) {
|
|
353
|
+
if (entry.isDirectory() && entry.name.startsWith('.atlas_tmp_')) {
|
|
354
|
+
cleanupTempDir(node_path_1.default.join(this.dataDir, entry.name));
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// 临时目录放在数据目录同盘,确保 renameSync 原子操作不跨文件系统(避免 EXDEV)。
|
|
358
|
+
const tmpRoot = node_fs_1.default.mkdtempSync(node_path_1.default.join(this.dataDir, '.atlas_tmp_'));
|
|
359
|
+
const extractDir = node_path_1.default.join(tmpRoot, 'extract');
|
|
360
|
+
let preparedDir = node_path_1.default.join(tmpRoot, 'rocom_atlas');
|
|
361
|
+
const cloneDir = node_path_1.default.join(tmpRoot, 'clone');
|
|
362
|
+
try {
|
|
363
|
+
let zipBuffer = null;
|
|
364
|
+
const zipErrors = [];
|
|
365
|
+
for (const url of this.zipUrls) {
|
|
366
|
+
try {
|
|
367
|
+
await emit(10, '正在下载图鉴压缩包');
|
|
368
|
+
zipBuffer = await this.downloadZipWithProgress(ctx, url);
|
|
369
|
+
await emit(80, '图鉴压缩包下载完成');
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
catch (e) {
|
|
373
|
+
logger.warn(`图鉴压缩包下载失败:${url}: ${e}`);
|
|
374
|
+
zipErrors.push(`${url}: ${e}`);
|
|
375
|
+
zipBuffer = null;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
let stats;
|
|
379
|
+
if (zipBuffer) {
|
|
380
|
+
await emit(80, '正在解压图鉴文件');
|
|
381
|
+
logger.info('正在解压图鉴压缩包…');
|
|
382
|
+
node_fs_1.default.mkdirSync(extractDir, { recursive: true });
|
|
383
|
+
extractZip(zipBuffer, extractDir);
|
|
384
|
+
logger.info('图鉴压缩包解压完成');
|
|
385
|
+
const roots = node_fs_1.default.readdirSync(extractDir, { withFileTypes: true })
|
|
386
|
+
.filter(entry => entry.isDirectory())
|
|
387
|
+
.map(entry => node_path_1.default.join(extractDir, entry.name));
|
|
388
|
+
if (!roots.length)
|
|
389
|
+
throw new Error('Atlas 压缩包结构异常');
|
|
390
|
+
stats = this.prepareAtlasDir(roots[0], preparedDir);
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
logger.warn(`GitHub zip 下载失败,尝试 git clone:${zipErrors.join(' | ')}`);
|
|
394
|
+
await emit(20, '正在 git clone 图鉴仓库');
|
|
395
|
+
await gitClone(cloneDir, this.gitUrl);
|
|
396
|
+
await emit(80, '正在整理图鉴文件');
|
|
397
|
+
stats = this.prepareAtlasDir(cloneDir, preparedDir);
|
|
398
|
+
}
|
|
399
|
+
this.replaceAtlasDir(preparedDir);
|
|
400
|
+
preparedDir = '';
|
|
401
|
+
logger.info(`本地图鉴缓存已更新:${stats.imageCount} 张图片,${(stats.totalBytes / 1024 / 1024).toFixed(1)} MB`);
|
|
402
|
+
await emit(100, '本地图鉴缓存已更新');
|
|
403
|
+
return stats;
|
|
404
|
+
}
|
|
405
|
+
finally {
|
|
406
|
+
cleanupTempDir(tmpRoot);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
exports.AtlasService = AtlasService;
|
|
411
|
+
// Windows 下 git clone 产生的 .git pack 文件是只读的,直接 rmSync 会抛 EPERM;
|
|
412
|
+
// 先带重试删,失败则清掉只读属性再删。清理失败只告警,不影响下载结果。
|
|
413
|
+
function cleanupTempDir(tmpRoot) {
|
|
414
|
+
try {
|
|
415
|
+
node_fs_1.default.rmSync(tmpRoot, { recursive: true, force: true, maxRetries: 3, retryDelay: 200 });
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
try {
|
|
419
|
+
const clearReadonly = (dir) => {
|
|
420
|
+
for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
421
|
+
const entryPath = node_path_1.default.join(dir, entry.name);
|
|
422
|
+
try {
|
|
423
|
+
node_fs_1.default.chmodSync(entryPath, 0o666);
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
// ignore
|
|
427
|
+
}
|
|
428
|
+
if (entry.isDirectory())
|
|
429
|
+
clearReadonly(entryPath);
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
clearReadonly(tmpRoot);
|
|
433
|
+
node_fs_1.default.rmSync(tmpRoot, { recursive: true, force: true, maxRetries: 3, retryDelay: 200 });
|
|
434
|
+
}
|
|
435
|
+
catch (e) {
|
|
436
|
+
logger.warn(`清理图鉴临时目录失败(不影响下载结果):${tmpRoot}: ${e}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
package/lib/client.d.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import { Context } from 'koishi';
|
|
2
|
+
export interface IngameTaskPollOptions {
|
|
3
|
+
/** 服务端同步等待毫秒(long-poll),这段时间内服务端会尽量直接返回结果而不入队 */
|
|
4
|
+
waitMs?: number;
|
|
5
|
+
/** 进入排队后轮询任务状态的间隔毫秒 */
|
|
6
|
+
intervalMs?: number;
|
|
7
|
+
/** 进入排队后等待任务完成的总超时毫秒 */
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
/** 任务进入排队(拿到 task_id)时回调一次,可用于向用户发送“排队中”提示 */
|
|
10
|
+
onQueued?: (taskId: string) => void | Promise<void>;
|
|
11
|
+
}
|
|
2
12
|
export declare class RocomClient {
|
|
3
13
|
private baseUrl;
|
|
4
14
|
private apiKey;
|
|
@@ -25,6 +35,12 @@ export declare class RocomClient {
|
|
|
25
35
|
private requestWithStatus;
|
|
26
36
|
private requestIngameWithFallback;
|
|
27
37
|
private getIngameTask;
|
|
38
|
+
private static normalizeTaskStatus;
|
|
39
|
+
private static extractTaskId;
|
|
40
|
+
private static isCompletedGatewayPayload;
|
|
41
|
+
private static extractCompletedIngamePayload;
|
|
42
|
+
private static taskErrorMessage;
|
|
43
|
+
private pollIngameTask;
|
|
28
44
|
qqQrLogin(ctx: Context, userIdentifier: string): Promise<any>;
|
|
29
45
|
qqQrStatus(ctx: Context, fwToken: string, userIdentifier: string): Promise<any>;
|
|
30
46
|
wechatQrLogin(ctx: Context, userIdentifier: string): Promise<any>;
|
|
@@ -77,8 +93,12 @@ export declare class RocomClient {
|
|
|
77
93
|
getEggExchangeSubscriptions(ctx: Context, userIdentifier?: string): Promise<any>;
|
|
78
94
|
deleteEggExchangeSubscription(ctx: Context, subscriptionId: string | number, userIdentifier?: string): Promise<any>;
|
|
79
95
|
getEggExchangeEvents(ctx: Context, subscriptionId: string | number, afterEventId?: string, limit?: number, userIdentifier?: string): Promise<any>;
|
|
80
|
-
ingameHomeInfo(ctx: Context, uid: string,
|
|
96
|
+
ingameHomeInfo(ctx: Context, uid: string, options?: IngameTaskPollOptions): Promise<any>;
|
|
81
97
|
ingameMerchantInfo(ctx: Context, shopId: string | number): Promise<any>;
|
|
98
|
+
ingamePetData(ctx: Context, uid: string, extras?: {
|
|
99
|
+
petGid?: string | number;
|
|
100
|
+
npcId?: string | number;
|
|
101
|
+
}, options?: IngameTaskPollOptions): Promise<any>;
|
|
82
102
|
getFriendship(ctx: Context, fwToken: string, userIds: string, userIdentifier?: string): Promise<any>;
|
|
83
103
|
getStudentState(ctx: Context, fwToken: string, accountType?: number, userIdentifier?: string): Promise<any>;
|
|
84
104
|
getStudentPerks(ctx: Context, fwToken: string, area?: number, accountType?: number, userIdentifier?: string): Promise<any>;
|
|
@@ -88,4 +108,19 @@ export declare class RocomClient {
|
|
|
88
108
|
id?: number;
|
|
89
109
|
name?: string;
|
|
90
110
|
}): Promise<any>;
|
|
111
|
+
private wikiPagedParams;
|
|
112
|
+
listWikiPets(ctx: Context, q?: string, pageNo?: number, pageSize?: number, filters?: Record<string, any>): Promise<any>;
|
|
113
|
+
getWikiPet(ctx: Context, petId: string | number): Promise<any>;
|
|
114
|
+
getWikiPetProfile(ctx: Context, petId: string | number): Promise<any>;
|
|
115
|
+
getWikiPetSkills(ctx: Context, petId: string | number): Promise<any>;
|
|
116
|
+
getWikiPetFamily(ctx: Context, petId: string | number): Promise<any>;
|
|
117
|
+
getWikiPetHandbook(ctx: Context, petId: string | number): Promise<any>;
|
|
118
|
+
listWikiSkills(ctx: Context, q?: string, pageNo?: number, pageSize?: number, filters?: Record<string, any>): Promise<any>;
|
|
119
|
+
getWikiSkill(ctx: Context, skillId: string | number): Promise<any>;
|
|
120
|
+
getWikiSkillPets(ctx: Context, skillId: string | number): Promise<any>;
|
|
121
|
+
getWikiCatalogs(ctx: Context): Promise<any>;
|
|
122
|
+
getWikiOptions(ctx: Context): Promise<any>;
|
|
123
|
+
getWikiPath(ctx: Context, path: string, params?: Record<string, any>): Promise<any>;
|
|
124
|
+
listWikiCatalogItems(ctx: Context, path: string, q?: string, pageNo?: number, pageSize?: number, search?: boolean): Promise<any>;
|
|
125
|
+
get wikiAssetBaseUrl(): string;
|
|
91
126
|
}
|