@dsh-plugin/dsh-synapse 0.1.0-dev.41044228975 → 0.1.0

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.
Files changed (74) hide show
  1. package/LICENSE +661 -661
  2. package/README.md +0 -14
  3. package/package.json +1 -1
  4. package/vendor/gugle-chat-bot/LICENSE +661 -661
  5. package/vendor/gugle-chat-bot/package.json +57 -57
  6. package/vendor/gugle-chat-bot/src/allow-rule.yml +9345 -9345
  7. package/vendor/gugle-chat-bot/src/api.json +4284 -4284
  8. package/vendor/gugle-chat-bot/src/command/index.ts +141 -141
  9. package/vendor/gugle-chat-bot/src/config/features.ts +547 -547
  10. package/vendor/gugle-chat-bot/src/config/index.ts +184 -184
  11. package/vendor/gugle-chat-bot/src/config/manager.ts +356 -356
  12. package/vendor/gugle-chat-bot/src/constants/index.ts +13 -13
  13. package/vendor/gugle-chat-bot/src/custom.ts +548 -548
  14. package/vendor/gugle-chat-bot/src/event/index.ts +118 -118
  15. package/vendor/gugle-chat-bot/src/features/admin/index.ts +414 -414
  16. package/vendor/gugle-chat-bot/src/features/bili/image/impl/video.ts +39 -39
  17. package/vendor/gugle-chat-bot/src/features/bili/image/index.ts +37 -37
  18. package/vendor/gugle-chat-bot/src/features/bili/index.ts +65 -65
  19. package/vendor/gugle-chat-bot/src/features/bili/template/style/video.css +97 -97
  20. package/vendor/gugle-chat-bot/src/features/bili/template/video.html +56 -56
  21. package/vendor/gugle-chat-bot/src/features/discord-bridge/index.ts +1128 -1128
  22. package/vendor/gugle-chat-bot/src/features/discord-bridge/markdown.ts +105 -105
  23. package/vendor/gugle-chat-bot/src/features/discord-bridge/slash.ts +355 -355
  24. package/vendor/gugle-chat-bot/src/features/discord-bridge/source.ts +96 -96
  25. package/vendor/gugle-chat-bot/src/features/discord-bridge/template/message.html +14 -14
  26. package/vendor/gugle-chat-bot/src/features/discord-bridge/template/style/message.css +144 -144
  27. package/vendor/gugle-chat-bot/src/features/github/api.ts +93 -93
  28. package/vendor/gugle-chat-bot/src/features/github/binding.ts +161 -161
  29. package/vendor/gugle-chat-bot/src/features/github/image/impl/issue.ts +132 -132
  30. package/vendor/gugle-chat-bot/src/features/github/image/impl/pr.ts +144 -144
  31. package/vendor/gugle-chat-bot/src/features/github/image/impl/release.ts +160 -160
  32. package/vendor/gugle-chat-bot/src/features/github/image/index.ts +310 -310
  33. package/vendor/gugle-chat-bot/src/features/github/index.ts +302 -302
  34. package/vendor/gugle-chat-bot/src/features/github/template/issue.html +118 -118
  35. package/vendor/gugle-chat-bot/src/features/github/template/pull_request.html +125 -125
  36. package/vendor/gugle-chat-bot/src/features/github/template/release.html +53 -53
  37. package/vendor/gugle-chat-bot/src/features/github/template/style/issue.css +468 -468
  38. package/vendor/gugle-chat-bot/src/features/github/template/style/pull_request.css +468 -468
  39. package/vendor/gugle-chat-bot/src/features/github/template/style/release.css +412 -412
  40. package/vendor/gugle-chat-bot/src/features/hermes/client.ts +184 -184
  41. package/vendor/gugle-chat-bot/src/features/hermes/config.ts +210 -210
  42. package/vendor/gugle-chat-bot/src/features/hermes/context.ts +67 -67
  43. package/vendor/gugle-chat-bot/src/features/hermes/history.ts +86 -86
  44. package/vendor/gugle-chat-bot/src/features/hermes/index.ts +1533 -1533
  45. package/vendor/gugle-chat-bot/src/features/hermes/message.ts +319 -319
  46. package/vendor/gugle-chat-bot/src/features/hermes/reaction.ts +181 -181
  47. package/vendor/gugle-chat-bot/src/features/hermes/render.ts +118 -118
  48. package/vendor/gugle-chat-bot/src/features/hermes/reply-units.ts +331 -331
  49. package/vendor/gugle-chat-bot/src/features/hermes/skills.ts +406 -406
  50. package/vendor/gugle-chat-bot/src/features/hermes/template/approval.html +30 -30
  51. package/vendor/gugle-chat-bot/src/features/hermes/template/progress.html +21 -21
  52. package/vendor/gugle-chat-bot/src/features/hermes/template/style/approval.css +81 -81
  53. package/vendor/gugle-chat-bot/src/features/hermes/template/style/progress.css +120 -120
  54. package/vendor/gugle-chat-bot/src/features/hermes/trigger.ts +86 -86
  55. package/vendor/gugle-chat-bot/src/features/hermes/types.ts +327 -327
  56. package/vendor/gugle-chat-bot/src/features/management/index.ts +156 -156
  57. package/vendor/gugle-chat-bot/src/features/minecraft/index.ts +127 -127
  58. package/vendor/gugle-chat-bot/src/features/modrinth/index.ts +71 -71
  59. package/vendor/gugle-chat-bot/src/features/parentheses/index.ts +117 -117
  60. package/vendor/gugle-chat-bot/src/features/peak-valley-timer/index.ts +451 -451
  61. package/vendor/gugle-chat-bot/src/features/poke/index.ts +24 -24
  62. package/vendor/gugle-chat-bot/src/features/version-tracker/index.ts +77 -77
  63. package/vendor/gugle-chat-bot/src/features/welcome/index.ts +57 -57
  64. package/vendor/gugle-chat-bot/src/github-ip.json +5814 -5814
  65. package/vendor/gugle-chat-bot/src/image/index.ts +278 -278
  66. package/vendor/gugle-chat-bot/src/index.ts +695 -695
  67. package/vendor/gugle-chat-bot/src/logger/index.ts +54 -54
  68. package/vendor/gugle-chat-bot/src/result.json +359 -359
  69. package/vendor/gugle-chat-bot/src/type/bili.d.ts +825 -825
  70. package/vendor/gugle-chat-bot/src/type/event.d.ts +146 -146
  71. package/vendor/gugle-chat-bot/src/type/github.d.ts +446 -446
  72. package/vendor/gugle-chat-bot/src/type/index.d.ts +254 -254
  73. package/vendor/gugle-chat-bot/tsconfig.json +46 -46
  74. package/scripts/dump-npm-latest.mjs +0 -30
@@ -1,93 +1,93 @@
1
- import axios, { AxiosInstance } from 'axios';
2
- import { Logger } from 'winston';
3
- import { botConfig } from '@/config';
4
- import { Issue, IssueStateReason, State } from '@/type/github';
5
-
6
- /**
7
- * GitHub API 反代地址:URL 结构为 ghapi.anvilcraft.dev + apiPath。
8
- * 不再经其他第三方代理。
9
- */
10
- const GITHUB_API_BASE = 'https://ghapi.anvilcraft.dev';
11
-
12
- const axiosInstance: AxiosInstance = axios.create({
13
- timeout: 10000,
14
- headers: {
15
- 'Content-Type': 'application/json',
16
- 'User-Agent': botConfig.userAgent
17
- }
18
- });
19
-
20
- /**
21
- * 请求 GitHub API(经 ghapi.anvilcraft.dev 反代)。
22
- * @param apiPath GitHub API 路径(不含域名),如 /repos/owner/repo/issues/123
23
- * @param logger 可选日志器
24
- */
25
- export function fetchGithubApi<T>(apiPath: string, logger?: Logger): Promise<T> {
26
- const url = `${GITHUB_API_BASE}${apiPath}`;
27
- logger?.debug(`GitHub API: ${url}`);
28
- return axiosInstance.get(url).then(response => response.data as T);
29
- }
30
-
31
- /** issue/PR 状态细分(用于 pill 徽章渲染) */
32
- export type LinkedIssueStatus =
33
- | 'open' // issue 进行中(绿色圆点)
34
- | 'completed' // issue 已完成关闭(紫色对勾)
35
- | 'not_planned' // issue 未计划等关闭(灰色斜杠)
36
- | 'pr_open' // PR 进行中(绿色 PR 图标)
37
- | 'pr_merged' // PR 已合并(紫色合并图标)
38
- | 'pr_closed' // PR 关闭未合并(红色 PR 关闭图标)
39
- | 'unknown'; // 状态未知(查询失败回退,灰色问号)
40
-
41
- export interface LinkedIssueInfo {
42
- state: State;
43
- state_reason?: IssueStateReason;
44
- /** issue / PR 标题 */
45
- title?: string | number;
46
- /** issue API 对 PR 会带此字段;merged_at 非空表示已合并 */
47
- pull_request?: { merged_at?: string | null };
48
- }
49
-
50
- /** 把 API 返回的 state/state_reason/pull_request 归一化为 pill 状态 */
51
- function toStatus(info: LinkedIssueInfo): LinkedIssueStatus {
52
- // PR 优先:issue API 对 PR 会带 pull_request 字段,merged_at 区分合并/关闭未合并
53
- if (info.pull_request) {
54
- if (info.state === 'open') return 'pr_open';
55
- return info.pull_request.merged_at ? 'pr_merged' : 'pr_closed';
56
- }
57
- if (info.state === 'open') return 'open';
58
- if (info.state === 'closed') {
59
- return info.state_reason === 'completed' ? 'completed' : 'not_planned';
60
- }
61
- return 'unknown';
62
- }
63
-
64
- /** 查询结果:状态 + 标题(失败时标题缺失,调用方回退为编号显示) */
65
- export interface LinkedIssueDetail {
66
- status: LinkedIssueStatus;
67
- title?: string;
68
- }
69
-
70
- /**
71
- * 查询 issue/PR 状态与标题。失败回退:任何错误都返回 { status: 'unknown' },
72
- * 不抛异常、不阻塞调用方。
73
- * @param repository owner/repo
74
- * @param number issue 编号
75
- * @param logger 可选日志器
76
- */
77
- export async function fetchIssueDetail(
78
- repository: string,
79
- number: number,
80
- logger?: Logger
81
- ): Promise<LinkedIssueDetail> {
82
- try {
83
- const info = await fetchGithubApi<LinkedIssueInfo>(`/repos/${repository}/issues/${number}`, logger);
84
- const status = toStatus(info);
85
- if (info.title == null) {
86
- return { status };
87
- }
88
- return { status, title: String(info.title) };
89
- } catch (e) {
90
- logger?.warn(`查询 issue 状态失败 ${repository}#${number}:${(e as Error)?.message ?? e}`);
91
- return { status: 'unknown' };
92
- }
93
- }
1
+ import axios, { AxiosInstance } from 'axios';
2
+ import { Logger } from 'winston';
3
+ import { botConfig } from '@/config';
4
+ import { Issue, IssueStateReason, State } from '@/type/github';
5
+
6
+ /**
7
+ * GitHub API 反代地址:URL 结构为 ghapi.anvilcraft.dev + apiPath。
8
+ * 不再经其他第三方代理。
9
+ */
10
+ const GITHUB_API_BASE = 'https://ghapi.anvilcraft.dev';
11
+
12
+ const axiosInstance: AxiosInstance = axios.create({
13
+ timeout: 10000,
14
+ headers: {
15
+ 'Content-Type': 'application/json',
16
+ 'User-Agent': botConfig.userAgent
17
+ }
18
+ });
19
+
20
+ /**
21
+ * 请求 GitHub API(经 ghapi.anvilcraft.dev 反代)。
22
+ * @param apiPath GitHub API 路径(不含域名),如 /repos/owner/repo/issues/123
23
+ * @param logger 可选日志器
24
+ */
25
+ export function fetchGithubApi<T>(apiPath: string, logger?: Logger): Promise<T> {
26
+ const url = `${GITHUB_API_BASE}${apiPath}`;
27
+ logger?.debug(`GitHub API: ${url}`);
28
+ return axiosInstance.get(url).then(response => response.data as T);
29
+ }
30
+
31
+ /** issue/PR 状态细分(用于 pill 徽章渲染) */
32
+ export type LinkedIssueStatus =
33
+ | 'open' // issue 进行中(绿色圆点)
34
+ | 'completed' // issue 已完成关闭(紫色对勾)
35
+ | 'not_planned' // issue 未计划等关闭(灰色斜杠)
36
+ | 'pr_open' // PR 进行中(绿色 PR 图标)
37
+ | 'pr_merged' // PR 已合并(紫色合并图标)
38
+ | 'pr_closed' // PR 关闭未合并(红色 PR 关闭图标)
39
+ | 'unknown'; // 状态未知(查询失败回退,灰色问号)
40
+
41
+ export interface LinkedIssueInfo {
42
+ state: State;
43
+ state_reason?: IssueStateReason;
44
+ /** issue / PR 标题 */
45
+ title?: string | number;
46
+ /** issue API 对 PR 会带此字段;merged_at 非空表示已合并 */
47
+ pull_request?: { merged_at?: string | null };
48
+ }
49
+
50
+ /** 把 API 返回的 state/state_reason/pull_request 归一化为 pill 状态 */
51
+ function toStatus(info: LinkedIssueInfo): LinkedIssueStatus {
52
+ // PR 优先:issue API 对 PR 会带 pull_request 字段,merged_at 区分合并/关闭未合并
53
+ if (info.pull_request) {
54
+ if (info.state === 'open') return 'pr_open';
55
+ return info.pull_request.merged_at ? 'pr_merged' : 'pr_closed';
56
+ }
57
+ if (info.state === 'open') return 'open';
58
+ if (info.state === 'closed') {
59
+ return info.state_reason === 'completed' ? 'completed' : 'not_planned';
60
+ }
61
+ return 'unknown';
62
+ }
63
+
64
+ /** 查询结果:状态 + 标题(失败时标题缺失,调用方回退为编号显示) */
65
+ export interface LinkedIssueDetail {
66
+ status: LinkedIssueStatus;
67
+ title?: string;
68
+ }
69
+
70
+ /**
71
+ * 查询 issue/PR 状态与标题。失败回退:任何错误都返回 { status: 'unknown' },
72
+ * 不抛异常、不阻塞调用方。
73
+ * @param repository owner/repo
74
+ * @param number issue 编号
75
+ * @param logger 可选日志器
76
+ */
77
+ export async function fetchIssueDetail(
78
+ repository: string,
79
+ number: number,
80
+ logger?: Logger
81
+ ): Promise<LinkedIssueDetail> {
82
+ try {
83
+ const info = await fetchGithubApi<LinkedIssueInfo>(`/repos/${repository}/issues/${number}`, logger);
84
+ const status = toStatus(info);
85
+ if (info.title == null) {
86
+ return { status };
87
+ }
88
+ return { status, title: String(info.title) };
89
+ } catch (e) {
90
+ logger?.warn(`查询 issue 状态失败 ${repository}#${number}:${(e as Error)?.message ?? e}`);
91
+ return { status: 'unknown' };
92
+ }
93
+ }
@@ -1,161 +1,161 @@
1
- import { randomInt } from 'node:crypto';
2
- import path from 'node:path';
3
- import axios from 'axios';
4
- import { readOrCreate, writeConfigFile } from '@/config/manager';
5
-
6
- interface PendingBinding {
7
- code: string;
8
- username: string;
9
- }
10
-
11
- interface GitHubBindings {
12
- bindings: Record<string, string[]>;
13
- githubToQq: Record<string, string>;
14
- pending: Record<string, PendingBinding>;
15
- }
16
-
17
- interface GitHubUserProfile {
18
- login: string;
19
- bio: string | null;
20
- }
21
-
22
- export class GitHubBindingManager {
23
- private static readonly FILE = path.resolve(process.cwd(), 'configs', 'github-bind.json');
24
- private static readonly CODE_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
25
- private static bindings: GitHubBindings | undefined;
26
-
27
- /**
28
- * 绑定 GitHub 用户名。
29
- * @param qqUserId 平台内用户标识:QQ 号为 number;Discord 用户为 `dc:<用户ID>` 字符串
30
- * (`dc:` 前缀保证与纯数字 QQ 号不冲突,共用同一套绑定存储)
31
- */
32
- public static async bind(qqUserId: number | string, username: string): Promise<{ state: 'pending' | 'bound'; code?: string }> {
33
- const normalizedUsername = GitHubBindingManager.normalizeUsername(username);
34
- const bindings = GitHubBindingManager.load();
35
- const qqUserIdText = String(qqUserId);
36
-
37
- const boundQqUserId = bindings.githubToQq[normalizedUsername.toLowerCase()];
38
- if (boundQqUserId && boundQqUserId !== qqUserIdText) {
39
- throw new Error(`GitHub 用户 ${normalizedUsername} 已被其他用户绑定`);
40
- }
41
- if (GitHubBindingManager.hasBinding(qqUserIdText, normalizedUsername)) {
42
- return { state: 'bound' };
43
- }
44
-
45
- const pendingKey = `${qqUserIdText}:${normalizedUsername.toLowerCase()}`;
46
- const pending = bindings.pending[pendingKey];
47
- if (!pending) {
48
- const code = GitHubBindingManager.generateCode();
49
- bindings.pending[pendingKey] = { code, username: normalizedUsername };
50
- GitHubBindingManager.save();
51
- return { state: 'pending', code };
52
- }
53
-
54
- const profile = await GitHubBindingManager.getUserProfile(pending.username);
55
- if (!profile.bio?.includes(pending.code)) {
56
- return { state: 'pending', code: pending.code };
57
- }
58
-
59
- const boundUsernames = bindings.bindings[qqUserIdText] || [];
60
- const normalizedProfileUsername = profile.login.toLowerCase();
61
- const profileBoundQqUserId = bindings.githubToQq[normalizedProfileUsername];
62
- if (profileBoundQqUserId && profileBoundQqUserId !== qqUserIdText) {
63
- throw new Error(`GitHub 用户 ${profile.login} 已被其他用户绑定`);
64
- }
65
- if (!boundUsernames.some(boundUsername => boundUsername.toLowerCase() === normalizedProfileUsername)) {
66
- boundUsernames.push(profile.login);
67
- bindings.bindings[qqUserIdText] = boundUsernames;
68
- }
69
- bindings.githubToQq[normalizedProfileUsername] = qqUserIdText;
70
- delete bindings.pending[pendingKey];
71
- GitHubBindingManager.save();
72
- return { state: 'bound' };
73
- }
74
-
75
- public static isBoundUsername(username: string): boolean {
76
- const normalizedUsername = username.toLowerCase();
77
- return Boolean(GitHubBindingManager.load().githubToQq[normalizedUsername]);
78
- }
79
-
80
- private static normalizeUsername(username: string): string {
81
- if (!/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/.test(username)) {
82
- throw new Error('GitHub 用户名格式无效');
83
- }
84
- return username;
85
- }
86
-
87
- private static generateCode(): string {
88
- let code = '';
89
- for (let index = 0; index < 12; index++) {
90
- code += GitHubBindingManager.CODE_ALPHABET[randomInt(GitHubBindingManager.CODE_ALPHABET.length)];
91
- }
92
- return code;
93
- }
94
-
95
- private static async getUserProfile(username: string): Promise<GitHubUserProfile> {
96
- try {
97
- const response = await axios.get<GitHubUserProfile>(`https://api.github.com/users/${username}`, {
98
- timeout: 10000,
99
- headers: { Accept: 'application/vnd.github+json' }
100
- });
101
- return response.data;
102
- } catch (error) {
103
- if (axios.isAxiosError(error) && error.response?.status === 404) {
104
- throw new Error(`未找到 GitHub 用户 ${username}`);
105
- }
106
- throw new Error(`无法查询 GitHub 用户 ${username}`);
107
- }
108
- }
109
-
110
- private static hasBinding(qqUserId: string, username: string): boolean {
111
- return (GitHubBindingManager.load().bindings[qqUserId] || []).some(
112
- boundUsername => boundUsername.toLowerCase() === username.toLowerCase()
113
- );
114
- }
115
-
116
- /** 重新从磁盘加载绑定数据(/reload 用;清空内存缓存后懒加载) */
117
- public static reload(): void {
118
- GitHubBindingManager.bindings = undefined;
119
- }
120
-
121
- private static load(): GitHubBindings {
122
- if (GitHubBindingManager.bindings) return GitHubBindingManager.bindings;
123
-
124
- const { data } = readOrCreate<GitHubBindings>({
125
- path: GitHubBindingManager.FILE,
126
- factory: () => ({ bindings: {}, githubToQq: {}, pending: {} }),
127
- normalize: raw => {
128
- if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
129
- const parsed = raw as Partial<GitHubBindings>;
130
- const bindings: Record<string, string[]> =
131
- parsed.bindings && typeof parsed.bindings === 'object' ? (parsed.bindings as Record<string, string[]>) : {};
132
- const githubToQq: Record<string, string> =
133
- parsed.githubToQq && typeof parsed.githubToQq === 'object' ? (parsed.githubToQq as Record<string, string>) : {};
134
-
135
- for (const [qqUserId, usernames] of Object.entries(bindings)) {
136
- if (!Array.isArray(usernames)) continue;
137
- for (const username of usernames) {
138
- if (typeof username !== 'string') continue;
139
- const normalizedUsername = username.toLowerCase();
140
- if (!githubToQq[normalizedUsername]) {
141
- githubToQq[normalizedUsername] = qqUserId;
142
- }
143
- }
144
- }
145
-
146
- return {
147
- bindings,
148
- githubToQq,
149
- pending: parsed.pending && typeof parsed.pending === 'object' ? (parsed.pending as Record<string, PendingBinding>) : {}
150
- };
151
- }
152
- });
153
- GitHubBindingManager.bindings = data;
154
- return GitHubBindingManager.bindings;
155
- }
156
-
157
- private static save(): void {
158
- if (!GitHubBindingManager.bindings) return;
159
- writeConfigFile(GitHubBindingManager.FILE, GitHubBindingManager.bindings);
160
- }
161
- }
1
+ import { randomInt } from 'node:crypto';
2
+ import path from 'node:path';
3
+ import axios from 'axios';
4
+ import { readOrCreate, writeConfigFile } from '@/config/manager';
5
+
6
+ interface PendingBinding {
7
+ code: string;
8
+ username: string;
9
+ }
10
+
11
+ interface GitHubBindings {
12
+ bindings: Record<string, string[]>;
13
+ githubToQq: Record<string, string>;
14
+ pending: Record<string, PendingBinding>;
15
+ }
16
+
17
+ interface GitHubUserProfile {
18
+ login: string;
19
+ bio: string | null;
20
+ }
21
+
22
+ export class GitHubBindingManager {
23
+ private static readonly FILE = path.resolve(process.cwd(), 'configs', 'github-bind.json');
24
+ private static readonly CODE_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
25
+ private static bindings: GitHubBindings | undefined;
26
+
27
+ /**
28
+ * 绑定 GitHub 用户名。
29
+ * @param qqUserId 平台内用户标识:QQ 号为 number;Discord 用户为 `dc:<用户ID>` 字符串
30
+ * (`dc:` 前缀保证与纯数字 QQ 号不冲突,共用同一套绑定存储)
31
+ */
32
+ public static async bind(qqUserId: number | string, username: string): Promise<{ state: 'pending' | 'bound'; code?: string }> {
33
+ const normalizedUsername = GitHubBindingManager.normalizeUsername(username);
34
+ const bindings = GitHubBindingManager.load();
35
+ const qqUserIdText = String(qqUserId);
36
+
37
+ const boundQqUserId = bindings.githubToQq[normalizedUsername.toLowerCase()];
38
+ if (boundQqUserId && boundQqUserId !== qqUserIdText) {
39
+ throw new Error(`GitHub 用户 ${normalizedUsername} 已被其他用户绑定`);
40
+ }
41
+ if (GitHubBindingManager.hasBinding(qqUserIdText, normalizedUsername)) {
42
+ return { state: 'bound' };
43
+ }
44
+
45
+ const pendingKey = `${qqUserIdText}:${normalizedUsername.toLowerCase()}`;
46
+ const pending = bindings.pending[pendingKey];
47
+ if (!pending) {
48
+ const code = GitHubBindingManager.generateCode();
49
+ bindings.pending[pendingKey] = { code, username: normalizedUsername };
50
+ GitHubBindingManager.save();
51
+ return { state: 'pending', code };
52
+ }
53
+
54
+ const profile = await GitHubBindingManager.getUserProfile(pending.username);
55
+ if (!profile.bio?.includes(pending.code)) {
56
+ return { state: 'pending', code: pending.code };
57
+ }
58
+
59
+ const boundUsernames = bindings.bindings[qqUserIdText] || [];
60
+ const normalizedProfileUsername = profile.login.toLowerCase();
61
+ const profileBoundQqUserId = bindings.githubToQq[normalizedProfileUsername];
62
+ if (profileBoundQqUserId && profileBoundQqUserId !== qqUserIdText) {
63
+ throw new Error(`GitHub 用户 ${profile.login} 已被其他用户绑定`);
64
+ }
65
+ if (!boundUsernames.some(boundUsername => boundUsername.toLowerCase() === normalizedProfileUsername)) {
66
+ boundUsernames.push(profile.login);
67
+ bindings.bindings[qqUserIdText] = boundUsernames;
68
+ }
69
+ bindings.githubToQq[normalizedProfileUsername] = qqUserIdText;
70
+ delete bindings.pending[pendingKey];
71
+ GitHubBindingManager.save();
72
+ return { state: 'bound' };
73
+ }
74
+
75
+ public static isBoundUsername(username: string): boolean {
76
+ const normalizedUsername = username.toLowerCase();
77
+ return Boolean(GitHubBindingManager.load().githubToQq[normalizedUsername]);
78
+ }
79
+
80
+ private static normalizeUsername(username: string): string {
81
+ if (!/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/.test(username)) {
82
+ throw new Error('GitHub 用户名格式无效');
83
+ }
84
+ return username;
85
+ }
86
+
87
+ private static generateCode(): string {
88
+ let code = '';
89
+ for (let index = 0; index < 12; index++) {
90
+ code += GitHubBindingManager.CODE_ALPHABET[randomInt(GitHubBindingManager.CODE_ALPHABET.length)];
91
+ }
92
+ return code;
93
+ }
94
+
95
+ private static async getUserProfile(username: string): Promise<GitHubUserProfile> {
96
+ try {
97
+ const response = await axios.get<GitHubUserProfile>(`https://api.github.com/users/${username}`, {
98
+ timeout: 10000,
99
+ headers: { Accept: 'application/vnd.github+json' }
100
+ });
101
+ return response.data;
102
+ } catch (error) {
103
+ if (axios.isAxiosError(error) && error.response?.status === 404) {
104
+ throw new Error(`未找到 GitHub 用户 ${username}`);
105
+ }
106
+ throw new Error(`无法查询 GitHub 用户 ${username}`);
107
+ }
108
+ }
109
+
110
+ private static hasBinding(qqUserId: string, username: string): boolean {
111
+ return (GitHubBindingManager.load().bindings[qqUserId] || []).some(
112
+ boundUsername => boundUsername.toLowerCase() === username.toLowerCase()
113
+ );
114
+ }
115
+
116
+ /** 重新从磁盘加载绑定数据(/reload 用;清空内存缓存后懒加载) */
117
+ public static reload(): void {
118
+ GitHubBindingManager.bindings = undefined;
119
+ }
120
+
121
+ private static load(): GitHubBindings {
122
+ if (GitHubBindingManager.bindings) return GitHubBindingManager.bindings;
123
+
124
+ const { data } = readOrCreate<GitHubBindings>({
125
+ path: GitHubBindingManager.FILE,
126
+ factory: () => ({ bindings: {}, githubToQq: {}, pending: {} }),
127
+ normalize: raw => {
128
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
129
+ const parsed = raw as Partial<GitHubBindings>;
130
+ const bindings: Record<string, string[]> =
131
+ parsed.bindings && typeof parsed.bindings === 'object' ? (parsed.bindings as Record<string, string[]>) : {};
132
+ const githubToQq: Record<string, string> =
133
+ parsed.githubToQq && typeof parsed.githubToQq === 'object' ? (parsed.githubToQq as Record<string, string>) : {};
134
+
135
+ for (const [qqUserId, usernames] of Object.entries(bindings)) {
136
+ if (!Array.isArray(usernames)) continue;
137
+ for (const username of usernames) {
138
+ if (typeof username !== 'string') continue;
139
+ const normalizedUsername = username.toLowerCase();
140
+ if (!githubToQq[normalizedUsername]) {
141
+ githubToQq[normalizedUsername] = qqUserId;
142
+ }
143
+ }
144
+ }
145
+
146
+ return {
147
+ bindings,
148
+ githubToQq,
149
+ pending: parsed.pending && typeof parsed.pending === 'object' ? (parsed.pending as Record<string, PendingBinding>) : {}
150
+ };
151
+ }
152
+ });
153
+ GitHubBindingManager.bindings = data;
154
+ return GitHubBindingManager.bindings;
155
+ }
156
+
157
+ private static save(): void {
158
+ if (!GitHubBindingManager.bindings) return;
159
+ writeConfigFile(GitHubBindingManager.FILE, GitHubBindingManager.bindings);
160
+ }
161
+ }