dna-api 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts ADDED
@@ -0,0 +1,935 @@
1
+ import * as forge from "node-forge"
2
+ //#region const
3
+ const MAIN_URL = "https://dnabbs-api.yingxiong.com"
4
+ const LOGIN_URL = `${MAIN_URL}/user/sdkLogin`
5
+ const GET_RSA_PUBLIC_KEY_URL = `${MAIN_URL}/config/getRsaPublicKey`
6
+ const LOGIN_LOG_URL = `${MAIN_URL}/user/login/log`
7
+ const ROLE_LIST_URL = `${MAIN_URL}/role/list`
8
+ const ROLE_FOR_TOOL_URL = `${MAIN_URL}/role/defaultRoleForTool`
9
+ const ROLE_DETAIL_URL = `${MAIN_URL}/role/getCharDetail`
10
+ const WEAPON_DETAIL_URL = `${MAIN_URL}/weapon/detail`
11
+ const SHORT_NOTE_URL = `${MAIN_URL}/role/getShortNoteInfo`
12
+ const SIGN_CALENDAR_URL = `${MAIN_URL}/encourage/signin/show`
13
+ const GAME_SIGN_URL = `${MAIN_URL}/encourage/signin/signin`
14
+ const BBS_SIGN_URL = `${MAIN_URL}/user/signIn`
15
+ const HAVE_SIGN_IN_URL = `${MAIN_URL}/user/haveSignInNew`
16
+ const GET_TASK_PROCESS_URL = `${MAIN_URL}/encourage/level/getTaskProcess`
17
+ const GET_POST_LIST_URL = `${MAIN_URL}/forum/list`
18
+ const GET_POST_DETAIL_URL = `${MAIN_URL}/forum/getPostDetail`
19
+ const LIKE_POST_URL = `${MAIN_URL}/forum/like`
20
+ const SHARE_POST_URL = `${MAIN_URL}/encourage/level/shareTask`
21
+ const REPLY_POST_URL = `${MAIN_URL}/forum/comment/createComment`
22
+ const ANN_LIST_URL = `${MAIN_URL}/user/mine`
23
+
24
+ enum RespCode {
25
+ ERROR = -999,
26
+ OK_ZERO = 0,
27
+ OK_HTTP = 200,
28
+ BAD_REQUEST = 400,
29
+ SERVER_ERROR = 500,
30
+ }
31
+
32
+ const DNA_GAME_ID = 268
33
+ //#endregion
34
+
35
+ /**
36
+ * DNA API类,用于与DNA游戏服务器交互
37
+ */
38
+ export class DNAAPI {
39
+ public fetchFn: typeof fetch
40
+ public is_h5 = false
41
+ public rsa_public_key =
42
+ "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDGpdbezK+eknQZQzPOjp8mr/dP+QHwk8CRkQh6C6qFnfLH3tiyl0pnt3dePuFDnM1PUXGhCkQ157ePJCQgkDU2+mimDmXh0oLFn9zuWSp+U8uLSLX3t3PpJ8TmNCROfUDWvzdbnShqg7JfDmnrOJz49qd234W84nrfTHbzdqeigQIDAQAB"
43
+
44
+ /**
45
+ * 构造函数
46
+ * @param dev_code 设备码
47
+ * @param uid 用户ID
48
+ * @param token 访问令牌
49
+ * @param options 选项
50
+ * @param options.fetchFn 自定义fetch函数
51
+ * @param options.is_h5 是否为H5端
52
+ * @param options.rsa_public_key RSA公钥(base64) 设为空字符串从服务器获取
53
+ */
54
+ constructor(
55
+ public dev_code: string,
56
+ public uid = "",
57
+ public token = "",
58
+ options: { fetchFn?: typeof fetch; is_h5?: boolean; rsa_public_key?: string } = {},
59
+ ) {
60
+ this.fetchFn = options.fetchFn || fetch.bind(window)
61
+ if (options.is_h5 !== undefined) this.is_h5 = options.is_h5
62
+ if (options.rsa_public_key !== undefined) this.rsa_public_key = options.rsa_public_key
63
+ }
64
+
65
+ /**
66
+ * 获取RSA公钥
67
+ * @returns RSA公钥(base64)
68
+ */
69
+ async getRsaPublicKey() {
70
+ if (this.rsa_public_key) {
71
+ return this.rsa_public_key
72
+ }
73
+ const res = await this._dna_request<{ key: string }>(GET_RSA_PUBLIC_KEY_URL)
74
+
75
+ if (res.is_success && res.data) {
76
+ const key = res.data.key
77
+ if (typeof key === "string") {
78
+ this.rsa_public_key = key
79
+ }
80
+ }
81
+ return this.rsa_public_key
82
+ }
83
+
84
+ /**
85
+ * 登录
86
+ */
87
+ async login(mobile: string, code: string, dev_code: string) {
88
+ const data = { mobile, code, gameList: DNA_GAME_ID }
89
+ const res = await this._dna_request<DNALoginRes>(LOGIN_URL, data, { sign: true, refer: true })
90
+ if (res.is_success && res.data) {
91
+ const data = res.data
92
+ if (typeof data.userId === "string") {
93
+ this.uid = data.userId
94
+ }
95
+ if (typeof data.token === "string") {
96
+ this.token = data.token
97
+ }
98
+ }
99
+ return res
100
+ }
101
+
102
+ /**
103
+ * 获取登录日志
104
+ */
105
+ async loginLog() {
106
+ return await this._dna_request<DNALoginRes>(LOGIN_LOG_URL)
107
+ }
108
+
109
+ /**
110
+ * 获取角色列表
111
+ */
112
+ async getRoleList() {
113
+ return await this._dna_request<DNARoleListRes>(ROLE_LIST_URL)
114
+ }
115
+
116
+ /**
117
+ * 获取默认角色
118
+ */
119
+ async getDefaultRoleForTool() {
120
+ const data = { type: 1 }
121
+ return await this._dna_request<DNARoleForToolRes>(ROLE_FOR_TOOL_URL, data, { sign: true })
122
+ }
123
+
124
+ /**
125
+ * 获取角色详情
126
+ */
127
+ async getRoleDetail(char_id: string, char_eid: string) {
128
+ const data = { charId: char_id, charEid: char_eid, type: 1 }
129
+ return await this._dna_request<DNARoleDetailRes>(ROLE_DETAIL_URL, data)
130
+ }
131
+
132
+ /**
133
+ * 获取武器详情
134
+ */
135
+ async getWeaponDetail(weapon_id: string) {
136
+ const data = { weaponId: weapon_id, type: 1 }
137
+ return await this._dna_request<DNARoleDetailRes>(WEAPON_DETAIL_URL, data)
138
+ }
139
+
140
+ /**
141
+ * 获取角色简讯
142
+ */
143
+ async getShortNoteInfo() {
144
+ return await this._dna_request<DNARoleShortNoteRes>(SHORT_NOTE_URL)
145
+ }
146
+
147
+ /**
148
+ * 检查是否签到
149
+ */
150
+ async haveSignIn() {
151
+ const data = { gameId: DNA_GAME_ID }
152
+ return await this._dna_request<DNAHaveSignInRes>(HAVE_SIGN_IN_URL, data)
153
+ }
154
+
155
+ /**
156
+ * 签到日历
157
+ */
158
+ async signCalendar() {
159
+ const data = { gameId: DNA_GAME_ID }
160
+ return await this._dna_request<DNACalendarSignRes>(SIGN_CALENDAR_URL, data)
161
+ }
162
+
163
+ /**
164
+ * 游戏签到
165
+ */
166
+ async gameSign(day_award_id: number, period: number) {
167
+ const data = {
168
+ dayAwardId: day_award_id,
169
+ periodId: period,
170
+ signinType: 1,
171
+ }
172
+ return await this._dna_request(GAME_SIGN_URL, data)
173
+ }
174
+
175
+ /**
176
+ * 皎皎角签到
177
+ */
178
+ async bbsSign() {
179
+ const data = { gameId: DNA_GAME_ID }
180
+ return await this._dna_request(BBS_SIGN_URL, data)
181
+ }
182
+
183
+ /**
184
+ * 获取任务进度
185
+ */
186
+ async getTaskProcess() {
187
+ const data = { gameId: DNA_GAME_ID }
188
+ return await this._dna_request<DNATaskProcessRes>(GET_TASK_PROCESS_URL, data)
189
+ }
190
+
191
+ /**
192
+ * 获取帖子列表
193
+ */
194
+ async getPostList() {
195
+ const data = {
196
+ forumId: 46, // 全部
197
+ gameId: DNA_GAME_ID,
198
+ pageIndex: 1,
199
+ pageSize: 20,
200
+ searchType: 1, // 1:最新 2:热门
201
+ timeType: 0,
202
+ }
203
+ return await this._dna_request<DNAPostListRes>(GET_POST_LIST_URL, data)
204
+ }
205
+
206
+ /**
207
+ * 获取帖子详情
208
+ */
209
+ async getPostDetail(post_id: string) {
210
+ const data = { postId: post_id }
211
+ try {
212
+ return await this._dna_request<DNAPostDetailRes>(GET_POST_DETAIL_URL, data)
213
+ } catch (e) {
214
+ console.error("get_post_detail", e as Error)
215
+ return DNAApiResponse.err("请求皎皎角服务失败")
216
+ }
217
+ }
218
+
219
+ /**
220
+ * 点赞帖子
221
+ */
222
+ async doLike(post: { gameForumId: string; postId: string; postType: string; userId: string }) {
223
+ const data = {
224
+ forumId: post.gameForumId,
225
+ gameId: DNA_GAME_ID,
226
+ likeType: "1",
227
+ operateType: "1",
228
+ postCommentId: "",
229
+ postCommentReplyId: "",
230
+ postId: post.postId,
231
+ postType: post.postType,
232
+ toUserId: post.userId,
233
+ }
234
+ try {
235
+ return await this._dna_request(LIKE_POST_URL, data)
236
+ } catch (e) {
237
+ console.error("do_like", e as Error)
238
+ return DNAApiResponse.err("请求皎皎角服务失败")
239
+ }
240
+ }
241
+
242
+ // 分享帖子
243
+ async doShare() {
244
+ const data = { gameId: DNA_GAME_ID }
245
+ try {
246
+ return await this._dna_request(SHARE_POST_URL, data)
247
+ } catch (e) {
248
+ console.error("do_share", e as Error)
249
+ return DNAApiResponse.err("请求皎皎角服务失败")
250
+ }
251
+ }
252
+
253
+ // 回复帖子
254
+ async doReply(post: Record<string, any>, content: string) {
255
+ const content_json = JSON.stringify([
256
+ {
257
+ content,
258
+ contentType: "1",
259
+ imgHeight: 0,
260
+ imgWidth: 0,
261
+ url: "",
262
+ },
263
+ ])
264
+ const data = {
265
+ postId: post.postId,
266
+ forumId: post.gameForumId || 47,
267
+ postType: "1",
268
+ content: content_json,
269
+ }
270
+
271
+ return await this._dna_request(REPLY_POST_URL, data, { sign: true, refer: true, params: { toUserId: post.userId } })
272
+ }
273
+
274
+ // 获取游戏公告列表
275
+ async getAnnList() {
276
+ const data = {
277
+ otherUserId: "709542994134436647",
278
+ searchType: 1,
279
+ type: 2,
280
+ }
281
+ return await this._dna_request<DNAPostListRes>(ANN_LIST_URL, data)
282
+ }
283
+
284
+ async getHeaders(options?: {
285
+ payload?: Record<string, any> | string
286
+ exparams?: Record<string, any>
287
+ dev_code?: string
288
+ refer?: boolean
289
+ token?: string
290
+ }) {
291
+ let { payload, exparams, dev_code = this.dev_code, refer, token = this.token } = options || {}
292
+
293
+ const CONTENT_TYPE = "application/x-www-form-urlencoded; charset=utf-8"
294
+ const iosBaseHeader = {
295
+ version: "1.1.3",
296
+ source: "ios",
297
+ "Content-Type": CONTENT_TYPE,
298
+ "User-Agent": "DoubleHelix/4 CFNetwork/3860.100.1 Darwin/25.0.0",
299
+ }
300
+ const h5BaseHeader = {
301
+ version: "3.11.0",
302
+ source: "h5",
303
+ "Content-Type": CONTENT_TYPE,
304
+ "User-Agent":
305
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
306
+ }
307
+ // 默认获取ios头
308
+ const headers = { ...(this.is_h5 ? h5BaseHeader : iosBaseHeader) } as Record<string, any>
309
+ if (dev_code) {
310
+ headers.devCode = dev_code
311
+ }
312
+ if (refer) {
313
+ headers.origin = "https://dnabbs.yingxiong.com"
314
+ headers.refer = "https://dnabbs.yingxiong.com/"
315
+ }
316
+ if (token) {
317
+ headers.token = token
318
+ }
319
+ if (typeof payload === "object") {
320
+ const si = build_signature(payload)
321
+ Object.assign(payload, { sign: si.s, timestamp: si.t })
322
+ if (exparams) {
323
+ Object.assign(payload, exparams)
324
+ }
325
+
326
+ const params = new URLSearchParams()
327
+ Object.entries(payload).forEach(([key, value]) => {
328
+ params.append(key, String(value))
329
+ })
330
+ payload = params.toString()
331
+
332
+ const rk = si.k
333
+ const pk = await this.getRsaPublicKey()
334
+ const ek = rsa_encrypt(rk, pk)
335
+ if (this.is_h5) {
336
+ headers.k = ek
337
+ } else {
338
+ headers.rk = rk
339
+ headers.key = ek
340
+ }
341
+ }
342
+ return { headers, payload }
343
+ }
344
+
345
+ private async _dna_request<T = any>(
346
+ url: string,
347
+ data?: any,
348
+ options?: {
349
+ method?: "GET" | "POST"
350
+ sign?: boolean
351
+ refer?: boolean
352
+ params?: Record<string, any>
353
+ max_retries?: number
354
+ retry_delay?: number
355
+ timeout?: number
356
+ },
357
+ ): Promise<DNAApiResponse<T>> {
358
+ let { method = "POST", sign, refer, params, max_retries = 3, retry_delay = 1, timeout = 10000 } = options || {}
359
+ let headers: Record<string, any>
360
+ if (sign) {
361
+ const { payload: p, headers: h } = await this.getHeaders({ payload: data, refer, exparams: params })
362
+ data = p
363
+ headers = h
364
+ } else {
365
+ const { headers: h } = await this.getHeaders()
366
+ headers = h
367
+ }
368
+
369
+ for (let attempt = 0; attempt < max_retries; attempt++) {
370
+ try {
371
+ const fetchOptions: RequestInit = {
372
+ method,
373
+ headers,
374
+ body: data,
375
+ }
376
+
377
+ // 实现超时控制
378
+ const controller = new AbortController()
379
+ const timeoutId = setTimeout(() => controller.abort(), timeout)
380
+
381
+ const response = await this.fetchFn(url, {
382
+ ...fetchOptions,
383
+ signal: controller.signal,
384
+ })
385
+ clearTimeout(timeoutId)
386
+
387
+ // 获取响应头的 content-type
388
+ const contentType = response.headers.get("content-type") || ""
389
+ let raw_res: any
390
+
391
+ // 根据 content-type 处理响应数据
392
+ if (contentType.includes("text/")) {
393
+ const textData = await response.text()
394
+ raw_res = {
395
+ code: RespCode.ERROR,
396
+ data: textData,
397
+ }
398
+ } else {
399
+ raw_res = await response.json()
400
+ }
401
+
402
+ if (typeof raw_res === "object" && raw_res !== null) {
403
+ try {
404
+ if (typeof raw_res.data === "string") {
405
+ raw_res.data = JSON.parse(raw_res.data)
406
+ }
407
+ } catch (e) {
408
+ // 忽略解析错误
409
+ }
410
+ }
411
+
412
+ console.debug(
413
+ `[DNA] url:[${url}] headers:[${JSON.stringify(headers)}] data:[${JSON.stringify(data)}] raw_res:${JSON.stringify(raw_res)}`,
414
+ )
415
+ return new DNAApiResponse<T>(raw_res)
416
+ } catch (e) {
417
+ console.error(`请求失败: ${(e as Error).message}`)
418
+ if (attempt < max_retries - 1) {
419
+ await new Promise((resolve) => setTimeout(resolve, retry_delay * Math.pow(2, attempt)))
420
+ }
421
+ }
422
+ }
423
+
424
+ return DNAApiResponse.err("请求服务器失败,已达最大重试次数")
425
+ }
426
+ }
427
+
428
+ //#region 接口定义
429
+
430
+ export interface UserGame {
431
+ gameId: number // gameId
432
+ gameName: string // gameName
433
+ }
434
+
435
+ export interface DNALoginRes {
436
+ applyCancel?: number // applyCancel
437
+ gender?: number // gender
438
+ signature?: string // signature
439
+ headUrl: string // headUrl
440
+ userName: string // userName
441
+ userId: string // userId
442
+ isOfficial: number // isOfficial
443
+ token: string // token
444
+ userGameList: UserGame[] // userGameList
445
+ isRegister: number // isRegister
446
+ status: number // status
447
+ /** 是否完成绑定 0: 未绑定, 1: 已绑定 */
448
+ isComplete: number
449
+ refreshToken: string // refreshToken
450
+ }
451
+
452
+ export interface DNARoleShowVo {
453
+ roleId: string // roleId
454
+ headUrl?: string // headUrl
455
+ level?: number // level
456
+ roleName?: string // roleName
457
+ isDefault?: number // isDefault
458
+ roleRegisterTime?: string // roleRegisterTime
459
+ boundType?: number // boundType
460
+ roleBoundId: string // roleBoundId
461
+ }
462
+
463
+ export interface DNARole {
464
+ gameName: string // gameName
465
+ showVoList: DNARoleShowVo[] // showVoList
466
+ gameId: number // gameId
467
+ }
468
+
469
+ export interface DNARoleListRes {
470
+ roles: DNARole[] // roles
471
+ }
472
+
473
+ /** 密函 */
474
+ export interface DNARoleForToolInstance {
475
+ id: number // id
476
+ name: string // name
477
+ /** 密函编码 */
478
+ code: string
479
+ on: number // 0
480
+ }
481
+
482
+ enum DNAInstanceMHType {
483
+ "角色" = "role",
484
+ "武器" = "weapon",
485
+ "魔之楔" = "mzx",
486
+ "role" = "角色",
487
+ "weapon" = "武器",
488
+ "mzx" = "魔之楔",
489
+ }
490
+
491
+ export function getDNAInstanceMHType(key: keyof typeof DNAInstanceMHType) {
492
+ return DNAInstanceMHType[key]
493
+ }
494
+
495
+ export interface DNARoleForToolInstanceInfo {
496
+ /** 密函列表 */
497
+ instances: DNARoleForToolInstance[] // instances
498
+ /** 密函类型 */
499
+ mh_type?: DNAInstanceMHType // 密函类型
500
+ }
501
+
502
+ export interface DraftDoingInfo {
503
+ draftCompleteNum: number // draftCompleteNum
504
+ draftDoingNum: number // draftDoingNum
505
+ /** 结束时间 */
506
+ endTime: string
507
+ /** 产品id */
508
+ productId?: number
509
+ /** 产品名称 */
510
+ productName: string
511
+ /** 开始时间 */
512
+ startTime: string
513
+ }
514
+
515
+ export interface DraftInfo {
516
+ /** 正在做的锻造 */
517
+ draftDoingInfo?: DraftDoingInfo[]
518
+ /** 正在做的锻造数量 */
519
+ draftDoingNum: number
520
+ /** 最大锻造数量 */
521
+ draftMaxNum: number
522
+ }
523
+
524
+ export interface DNARoleShortNoteRes {
525
+ /** 迷津进度 */
526
+ rougeLikeRewardCount: number
527
+ /** 迷津总数 */
528
+ rougeLikeRewardTotal: number
529
+ /** 备忘手记进度 */
530
+ currentTaskProgress: number
531
+ /** 备忘手记总数 */
532
+ maxDailyTaskProgress: number
533
+ /** 梦魇进度 */
534
+ hardBossRewardCount: number
535
+ /** 梦魇总数 */
536
+ hardBossRewardTotal: number
537
+ /** 锻造信息 */
538
+ draftInfo: DraftInfo
539
+ }
540
+
541
+ export interface WeaponInsForTool {
542
+ /** 武器类型图标 */
543
+ elementIcon: string
544
+ /** 武器图标 */
545
+ icon: string
546
+ /** 武器等级 */
547
+ level: number
548
+ /** 武器名称 */
549
+ name: string
550
+ /** 是否解锁 */
551
+ unLocked: boolean
552
+ weaponEid?: string
553
+ weaponId: number
554
+ }
555
+
556
+ export interface RoleInsForTool {
557
+ charEid?: string
558
+ charId: number
559
+ /** 元素图标 */
560
+ elementIcon: string
561
+ /** 命座等级 */
562
+ gradeLevel: number
563
+ /** 角色图标 */
564
+ icon: string
565
+ /** 角色等级 */
566
+ level: number
567
+ /** 角色名称 */
568
+ name: string
569
+ /** 是否解锁 */
570
+ unLocked: boolean
571
+ }
572
+
573
+ export interface RoleAchievement {
574
+ paramKey: string // paramKey
575
+ paramValue: string // paramValue
576
+ }
577
+
578
+ export interface RoleShowForTool {
579
+ /** 角色列表 */
580
+ roleChars: RoleInsForTool[]
581
+ /** 武器列表 */
582
+ langRangeWeapons: WeaponInsForTool[]
583
+ /** 武器列表 */
584
+ closeWeapons: WeaponInsForTool[]
585
+ /** 等级 */
586
+ level: number
587
+ /** 成就列表 */
588
+ params: RoleAchievement[]
589
+ /** 角色id */
590
+ roleId: string
591
+ /** 角色名称 */
592
+ roleName: string
593
+ }
594
+
595
+ export interface RoleInfoForTool {
596
+ /** 角色信息 */
597
+ roleShow: RoleShowForTool
598
+ }
599
+
600
+ export interface DNARoleForToolRes {
601
+ /** 角色信息 */
602
+ roleInfo: RoleInfoForTool
603
+ /** 密函 */
604
+ instanceInfo: DNARoleForToolInstanceInfo[]
605
+ }
606
+
607
+ export interface RoleAttribute {
608
+ /** 技能范围 */
609
+ skillRange: string
610
+ /** 强化值 */
611
+ strongValue: string
612
+ /** 技能威力 */
613
+ skillIntensity: string
614
+ /** 武器精通 */
615
+ weaponTags: string[]
616
+ /** 防御 */
617
+ defense: number
618
+ /** 仇恨值 */
619
+ enmityValue: string
620
+ /** 技能效益 */
621
+ skillEfficiency: string
622
+ /** 技能耐久 */
623
+ skillSustain: string
624
+ /** 最大生命值 */
625
+ maxHp: number
626
+ /** 攻击 */
627
+ atk: number
628
+ /** 最大护盾 */
629
+ maxES: number
630
+ /** 最大神志 */
631
+ maxSp: number
632
+ }
633
+
634
+ export interface RoleSkill {
635
+ /** 技能id */
636
+ skillId: number
637
+ /** 技能图标 */
638
+ icon: string
639
+ /** 技能等级 */
640
+ level: number
641
+ /** 技能名称 */
642
+ skillName: string
643
+ }
644
+
645
+ export interface RoleTrace {
646
+ /** 溯源图标 */
647
+ icon: string
648
+ /** 溯源描述 */
649
+ description: string
650
+ }
651
+
652
+ export interface Mode {
653
+ /** id 没佩戴为-1 */
654
+ id: number
655
+ /** 图标 */
656
+ icon?: string
657
+ /** 质量 */
658
+ quality?: number
659
+ /** 名称 */
660
+ name?: string
661
+ }
662
+
663
+ export interface RoleDetail {
664
+ /** 角色属性 */
665
+ attribute: RoleAttribute
666
+ /** 角色技能 */
667
+ skills: RoleSkill[]
668
+ /** 立绘 */
669
+ paint: string
670
+ /** 角色名称 */
671
+ charName: string
672
+ /** 元素图标 */
673
+ elementIcon: string
674
+ /** 溯源 */
675
+ traces: RoleTrace[]
676
+ /** 当前魔之楔 */
677
+ currentVolume: number
678
+ /** 最大魔之楔 */
679
+ sumVolume: number
680
+ /** 角色等级 */
681
+ level: number
682
+ /** 角色头像 */
683
+ icon: string
684
+ /** 溯源等级 0-6 */
685
+ gradeLevel: number
686
+ /** 元素名称 */
687
+ elementName: string
688
+ /** mode */
689
+ modes: Mode[]
690
+ }
691
+
692
+ export interface DNARoleDetailRes {
693
+ /** 角色详情 */
694
+ charDetail: RoleDetail
695
+ }
696
+
697
+ export interface DNADayAward {
698
+ gameId: number // gameId
699
+ periodId: number // periodId
700
+ iconUrl: string // iconUrl
701
+ id: number // id
702
+ dayInPeriod: number // dayInPeriod
703
+ updateTime: number // updateTime
704
+ awardNum: number // awardNum
705
+ thirdProductId: string // thirdProductId
706
+ createTime: number // createTime
707
+ awardName: string // awardName
708
+ }
709
+
710
+ export interface DNACaSignPeriod {
711
+ gameId: number // gameId
712
+ retryCos: number // retryCos
713
+ endDate: number // endDate
714
+ id: number // id
715
+ startDate: number // startDate
716
+ retryTimes: number // retryTimes
717
+ overDays: number // overDays
718
+ createTime: number // createTime
719
+ name: string // name
720
+ }
721
+
722
+ export interface DNACaSignRoleInfo {
723
+ headUrl: string // headUrl
724
+ roleId: string // roleId
725
+ roleName: string // roleName
726
+ level: number // level
727
+ roleBoundId: string // roleBoundId
728
+ }
729
+
730
+ export interface DNAHaveSignInRes {
731
+ /** 已签到天数 */
732
+ totalSignInDay: number
733
+ }
734
+
735
+ export interface DNACalendarSignRes {
736
+ todaySignin: boolean // todaySignin
737
+ userGoldNum: number // userGoldNum
738
+ dayAward: DNADayAward[] // dayAward
739
+ signinTime: number // signinTime
740
+ period: DNACaSignPeriod // period
741
+ roleInfo: DNACaSignRoleInfo // roleInfo
742
+ }
743
+
744
+ export interface DNABBSTask {
745
+ /** 备注 */
746
+ remark: string
747
+ /** 完成次数 */
748
+ completeTimes: number
749
+ /** 需要次数 */
750
+ times: number
751
+ /** skipType */
752
+ skipType: number
753
+ /** 获取经验 */
754
+ gainExp: number
755
+ /** 进度 */
756
+ process: number
757
+ /** 获取金币 */
758
+ gainGold: number
759
+ /** 任务标识名 */
760
+ markName?: string
761
+ }
762
+
763
+ export interface DNATaskProcessRes {
764
+ dailyTask: DNABBSTask[] // dailyTask
765
+ }
766
+
767
+ export interface DNAPostListRes {
768
+ postList: DNAPost[] // posts
769
+ }
770
+
771
+ export interface DNAPost {
772
+ postId: number // postId
773
+ title: string // title
774
+ }
775
+
776
+ export interface DNAPostDetailRes {
777
+ postDetail: DNAPostDetail // post
778
+ }
779
+
780
+ export interface DNAPostDetail {
781
+ postId: number // postId
782
+ postTime: number // postTime
783
+ postContent: DNAPostContent[] // postContent
784
+ title: string // title
785
+ }
786
+
787
+ export enum PostContentType {
788
+ TEXT = 1,
789
+ IMAGE = 2,
790
+ VIDEO = 5,
791
+ }
792
+
793
+ export interface DNAPostContent {
794
+ contentType: PostContentType // content
795
+ content: string // content
796
+ url?: string // url
797
+ contentVideo?: DNAPostContentVideo // contentVideo
798
+ }
799
+
800
+ export interface DNAPostContentVideo {
801
+ videoUrl: string // videoUrl
802
+ coverUrl?: string // coverUrl
803
+ }
804
+
805
+ // interface DNAWikiDetail {
806
+ // name: string // name
807
+ // }
808
+
809
+ // interface DNAWikiRes {
810
+ // wikis: DNAWikiDetail[] // wikis
811
+ // }
812
+
813
+ class DNAApiResponse<T = any> {
814
+ code: number = 0
815
+ msg: string = ""
816
+ success: boolean = false
817
+ data?: T
818
+
819
+ constructor(raw_data: any) {
820
+ this.code = raw_data.code || 0
821
+ this.msg = raw_data.msg || ""
822
+ this.success = raw_data.success || false
823
+ this.data = raw_data.data
824
+ }
825
+
826
+ // 判断是否成功
827
+ get is_success() {
828
+ return this.success && [RespCode.OK_ZERO, RespCode.OK_HTTP].includes(this.code)
829
+ }
830
+
831
+ // 错误响应静态方法
832
+ static err<T = undefined>(msg: string, code: number = RespCode.ERROR): DNAApiResponse<T> {
833
+ return new DNAApiResponse<T>({ code, msg, data: undefined, success: false })
834
+ }
835
+ }
836
+ //#endregion
837
+
838
+ //#region utils
839
+
840
+ // RSA加密函数
841
+ function rsa_encrypt(text: string, public_key_b64: string): string {
842
+ try {
843
+ // 将base64公钥转换为PEM格式
844
+ const lines: string[] = []
845
+ for (let i = 0; i < public_key_b64.length; i += 64) {
846
+ lines.push(public_key_b64.slice(i, i + 64))
847
+ }
848
+ const pem = `-----BEGIN PUBLIC KEY-----\n${lines.join("\n")}\n-----END PUBLIC KEY-----`
849
+
850
+ // 导入PEM格式的RSA公钥
851
+ const publicKey = forge.pki.publicKeyFromPem(pem)
852
+
853
+ // 执行PKCS1_v1_5加密
854
+ const textBytes = forge.util.encodeUtf8(text)
855
+ const encrypted = publicKey.encrypt(textBytes)
856
+
857
+ return forge.util.encode64(encrypted)
858
+ } catch (e) {
859
+ throw new Error(`[DNA] RSA 加密失败: ${(e as Error).message}`)
860
+ }
861
+ }
862
+
863
+ // 生成随机字符串
864
+ function rand_str(length: number = 16): string {
865
+ const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
866
+ let result = ""
867
+ for (let i = 0; i < length; i++) {
868
+ result += chars.charAt(Math.floor(Math.random() * chars.length))
869
+ }
870
+ return result
871
+ }
872
+
873
+ // MD5加密并转换为大写
874
+ function md5_upper(text: string): string {
875
+ const md = forge.md.md5.create()
876
+ md.update(text)
877
+ return md.digest().toHex().toUpperCase()
878
+ }
879
+
880
+ // 签名哈希函数
881
+ function signature_hash(text: string): string {
882
+ function swap_positions(text: string, positions: number[]): string {
883
+ const chars = text.split("")
884
+ for (let i = 1; i < positions.length; i += 2) {
885
+ const p1 = positions[i - 1]
886
+ const p2 = positions[i]
887
+ if (p1 >= 0 && p1 < chars.length && p2 >= 0 && p2 < chars.length) {
888
+ ;[chars[p1], chars[p2]] = [chars[p2], chars[p1]]
889
+ }
890
+ }
891
+ return chars.join("")
892
+ }
893
+
894
+ return swap_positions(md5_upper(text), [1, 13, 5, 17, 7, 23])
895
+ }
896
+
897
+ // 签名函数
898
+ function sign_fI(data: Record<string, any>, secret: string): string {
899
+ const pairs: string[] = []
900
+ const sortedKeys = Object.keys(data).sort()
901
+ for (const k of sortedKeys) {
902
+ const v = data[k]
903
+ if (v !== null && v !== undefined && v !== "") {
904
+ pairs.push(`${k}=${v}`)
905
+ }
906
+ }
907
+ const qs = pairs.join("&")
908
+ return signature_hash(`${qs}&${secret}`)
909
+ }
910
+
911
+ // XOR编码函数
912
+ function xor_encode(text: string, key: string): string {
913
+ const encoder = new TextEncoder()
914
+ const tb = encoder.encode(text)
915
+ const kb = encoder.encode(key)
916
+ const out: string[] = []
917
+ for (let i = 0; i < tb.length; i++) {
918
+ const b = tb[i]
919
+ const e = (b & 255) + (kb[i % kb.length] & 255)
920
+ out.push(`@${e}`)
921
+ }
922
+ return out.join("")
923
+ }
924
+
925
+ // 构建签名
926
+ function build_signature(data: Record<string, any>, token?: string): Record<string, any> {
927
+ const ts = Date.now()
928
+ const sign_data = { ...data, timestamp: ts, token }
929
+ const sec = rand_str(16)
930
+ const sig = sign_fI(sign_data, sec)
931
+ const enc = xor_encode(sig, sec)
932
+ return { s: enc, t: ts, k: sec }
933
+ }
934
+
935
+ //#endregion