@jeik/dingtalk-connector 0.8.21-fix1

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 (154) hide show
  1. package/CHANGELOG.md +686 -0
  2. package/LICENSE +21 -0
  3. package/README.en.md +181 -0
  4. package/README.md +221 -0
  5. package/bin/dingtalk-connector.js +858 -0
  6. package/bin/wizard-config.mjs +110 -0
  7. package/dist/accounts-BAzdqkAV.mjs +268 -0
  8. package/dist/accounts-BQptOmgB.mjs +2 -0
  9. package/dist/chunk-upload-BBQgGtcZ.mjs +193 -0
  10. package/dist/chunk-upload-DaLXXZH3.mjs +2 -0
  11. package/dist/common-C8pYKU_y.mjs +2 -0
  12. package/dist/common-Dt9n6fQN.mjs +101 -0
  13. package/dist/connection-DHHFFNQJ.mjs +423 -0
  14. package/dist/entry-bundled.d.mts +16 -0
  15. package/dist/entry-bundled.mjs +31 -0
  16. package/dist/game-xiyou-CqHt-6Q1.mjs +4271 -0
  17. package/dist/gateway-methods-C4tcgI7P.mjs +771 -0
  18. package/dist/gateway-methods-Ci31A3vg.mjs +2 -0
  19. package/dist/http-client-CpnJHB89.mjs +2 -0
  20. package/dist/http-client-DFWZgO1n.mjs +33 -0
  21. package/dist/index.d.mts +193 -0
  22. package/dist/index.mjs +45 -0
  23. package/dist/logger-BmJkQkm1.mjs +2 -0
  24. package/dist/logger-mZ9OSbmD.mjs +58 -0
  25. package/dist/media-C_SVin7s.mjs +2 -0
  26. package/dist/media-cz72EVS3.mjs +509 -0
  27. package/dist/message-handler-DESzFFDc.mjs +1971 -0
  28. package/dist/messaging-B6l1sRvX.mjs +1044 -0
  29. package/dist/runtime-DUgpo5zC.mjs +1422 -0
  30. package/dist/session-DJ4jYqPv.mjs +114 -0
  31. package/dist/utils-Bjh4r_qS.mjs +4 -0
  32. package/dist/utils-CIfI_3Jh.mjs +63 -0
  33. package/dist/utils-legacy-CALCPP1t.mjs +230 -0
  34. package/dist/utils-legacy-CFYDBM4r.mjs +3 -0
  35. package/docs/DEAP_AGENT_GUIDE.en.md +115 -0
  36. package/docs/DEAP_AGENT_GUIDE.md +115 -0
  37. package/docs/DINGTALK_MANUAL_SETUP.md +50 -0
  38. package/docs/MULTI_AGENT_SETUP.md +306 -0
  39. package/docs/RELEASE_NOTES_V0.7.10.md +40 -0
  40. package/docs/RELEASE_NOTES_V0.7.2.md +143 -0
  41. package/docs/RELEASE_NOTES_V0.7.3.md +149 -0
  42. package/docs/RELEASE_NOTES_V0.7.4.md +206 -0
  43. package/docs/RELEASE_NOTES_V0.7.5.md +267 -0
  44. package/docs/RELEASE_NOTES_V0.7.6.md +219 -0
  45. package/docs/RELEASE_NOTES_V0.7.7.md +122 -0
  46. package/docs/RELEASE_NOTES_V0.7.8.md +101 -0
  47. package/docs/RELEASE_NOTES_V0.7.9.md +65 -0
  48. package/docs/RELEASE_NOTES_V0.8.0.md +53 -0
  49. package/docs/RELEASE_NOTES_V0.8.1.md +47 -0
  50. package/docs/RELEASE_NOTES_V0.8.10.md +49 -0
  51. package/docs/RELEASE_NOTES_V0.8.11.md +51 -0
  52. package/docs/RELEASE_NOTES_V0.8.12.md +63 -0
  53. package/docs/RELEASE_NOTES_V0.8.13-beta.0.md +69 -0
  54. package/docs/RELEASE_NOTES_V0.8.13.md +62 -0
  55. package/docs/RELEASE_NOTES_V0.8.14.md +86 -0
  56. package/docs/RELEASE_NOTES_V0.8.16.md +40 -0
  57. package/docs/RELEASE_NOTES_V0.8.17.md +87 -0
  58. package/docs/RELEASE_NOTES_V0.8.18.md +64 -0
  59. package/docs/RELEASE_NOTES_V0.8.19.md +62 -0
  60. package/docs/RELEASE_NOTES_V0.8.2.md +55 -0
  61. package/docs/RELEASE_NOTES_V0.8.20.md +49 -0
  62. package/docs/RELEASE_NOTES_V0.8.3.md +63 -0
  63. package/docs/RELEASE_NOTES_V0.8.4.md +45 -0
  64. package/docs/RELEASE_NOTES_V0.8.7.md +49 -0
  65. package/docs/RELEASE_NOTES_V0.8.8.md +63 -0
  66. package/docs/RELEASE_NOTES_V0.8.9.md +81 -0
  67. package/docs/RELEASE_NOTES_v0.7.0.md +142 -0
  68. package/docs/RELEASE_NOTES_v0.7.1.md +74 -0
  69. package/docs/TROUBLESHOOTING.md +122 -0
  70. package/index.ts +77 -0
  71. package/openclaw.plugin.json +551 -0
  72. package/package.json +147 -0
  73. package/skills/dingtalk-channel-rules/SKILL.md +91 -0
  74. package/skills/dingtalk-troubleshoot/SKILL.md +93 -0
  75. package/skills/dws-cli/SKILL.md +129 -0
  76. package/skills/dws-cli/references/error-codes.md +95 -0
  77. package/skills/dws-cli/references/field-rules.md +105 -0
  78. package/skills/dws-cli/references/global-reference.md +104 -0
  79. package/skills/dws-cli/references/intent-guide.md +114 -0
  80. package/skills/dws-cli/references/products/aitable.md +452 -0
  81. package/skills/dws-cli/references/products/attendance.md +93 -0
  82. package/skills/dws-cli/references/products/calendar.md +217 -0
  83. package/skills/dws-cli/references/products/chat.md +292 -0
  84. package/skills/dws-cli/references/products/contact.md +108 -0
  85. package/skills/dws-cli/references/products/ding.md +57 -0
  86. package/skills/dws-cli/references/products/report.md +162 -0
  87. package/skills/dws-cli/references/products/simple.md +128 -0
  88. package/skills/dws-cli/references/products/todo.md +138 -0
  89. package/skills/dws-cli/references/products/workbench.md +39 -0
  90. package/skills/dws-cli/references/recovery-guide.md +94 -0
  91. package/src/channel.ts +588 -0
  92. package/src/config/accounts.ts +242 -0
  93. package/src/config/schema.ts +180 -0
  94. package/src/core/connection.ts +741 -0
  95. package/src/core/message-handler.ts +1788 -0
  96. package/src/core/provider.ts +111 -0
  97. package/src/core/state.ts +54 -0
  98. package/src/device-auth-config.ts +14 -0
  99. package/src/device-auth.ts +197 -0
  100. package/src/directory.ts +95 -0
  101. package/src/docs.ts +293 -0
  102. package/src/game-xiyou/achievement-engine.ts +252 -0
  103. package/src/game-xiyou/bounty-system.ts +315 -0
  104. package/src/game-xiyou/commands.ts +223 -0
  105. package/src/game-xiyou/drop-engine.ts +241 -0
  106. package/src/game-xiyou/encounter-system.ts +135 -0
  107. package/src/game-xiyou/escape-engine.ts +164 -0
  108. package/src/game-xiyou/exp-calculator.ts +139 -0
  109. package/src/game-xiyou/index.ts +479 -0
  110. package/src/game-xiyou/level-system.ts +91 -0
  111. package/src/game-xiyou/monster-pool.ts +180 -0
  112. package/src/game-xiyou/pity-counter.ts +114 -0
  113. package/src/game-xiyou/random-event-engine.ts +648 -0
  114. package/src/game-xiyou/renderer.ts +679 -0
  115. package/src/game-xiyou/storage.ts +218 -0
  116. package/src/game-xiyou/treasure-system.ts +105 -0
  117. package/src/game-xiyou/types.ts +582 -0
  118. package/src/game-xiyou/uid-resolver.ts +49 -0
  119. package/src/gateway-methods.ts +740 -0
  120. package/src/onboarding.ts +553 -0
  121. package/src/policy.ts +32 -0
  122. package/src/probe.ts +210 -0
  123. package/src/reply-dispatcher.ts +874 -0
  124. package/src/runtime.ts +32 -0
  125. package/src/sdk/helpers.ts +322 -0
  126. package/src/sdk/types.ts +519 -0
  127. package/src/secret-input.ts +19 -0
  128. package/src/services/media/audio.ts +54 -0
  129. package/src/services/media/chunk-upload.ts +296 -0
  130. package/src/services/media/common.ts +155 -0
  131. package/src/services/media/file.ts +75 -0
  132. package/src/services/media/image.ts +81 -0
  133. package/src/services/media/index.ts +10 -0
  134. package/src/services/media/video.ts +162 -0
  135. package/src/services/media.ts +1143 -0
  136. package/src/services/messaging/card.ts +604 -0
  137. package/src/services/messaging/index.ts +18 -0
  138. package/src/services/messaging/mentions.ts +267 -0
  139. package/src/services/messaging/send.ts +141 -0
  140. package/src/services/messaging.ts +1191 -0
  141. package/src/services/reply-markers.ts +55 -0
  142. package/src/targets.ts +45 -0
  143. package/src/types/index.ts +59 -0
  144. package/src/types/pdf-parse.d.ts +3 -0
  145. package/src/utils/agent.ts +63 -0
  146. package/src/utils/async.ts +51 -0
  147. package/src/utils/constants.ts +27 -0
  148. package/src/utils/http-client.ts +38 -0
  149. package/src/utils/index.ts +8 -0
  150. package/src/utils/logger.ts +78 -0
  151. package/src/utils/session.ts +147 -0
  152. package/src/utils/token.ts +93 -0
  153. package/src/utils/utils-legacy.ts +454 -0
  154. package/tsconfig.json +20 -0
package/src/docs.ts ADDED
@@ -0,0 +1,293 @@
1
+ /**
2
+ * 钉钉文档 API 客户端
3
+ * 支持读写钉钉在线文档(文档、表格等)
4
+ */
5
+
6
+ import type { DingtalkConfig } from './types/index.ts';
7
+ import { getAccessToken, DINGTALK_API } from './utils/index.ts';
8
+ import { dingtalkHttp } from './utils/http-client.ts';
9
+
10
+ // ============ 类型定义 ============
11
+
12
+ /** 文档信息接口 */
13
+ export interface DocInfo {
14
+ docId: string;
15
+ title: string;
16
+ docType: string;
17
+ creatorId?: string;
18
+ updatedAt?: string;
19
+ }
20
+
21
+ /** 文档内容块 */
22
+ interface DocBlock {
23
+ blockId: string;
24
+ blockType: string;
25
+ text?: string;
26
+ children?: DocBlock[];
27
+ }
28
+
29
+ // ============ 钉钉文档客户端类 ============
30
+
31
+ export class DingtalkDocsClient {
32
+ private config: DingtalkConfig;
33
+ private log?: any;
34
+
35
+ constructor(config: DingtalkConfig, log?: any) {
36
+ this.config = config;
37
+ this.log = log;
38
+ }
39
+
40
+ /** 获取带鉴权的请求头 */
41
+ private async getHeaders(): Promise<Record<string, string>> {
42
+ const token = await getAccessToken(this.config);
43
+ return {
44
+ 'x-acs-dingtalk-access-token': token,
45
+ 'Content-Type': 'application/json',
46
+ };
47
+ }
48
+
49
+ /**
50
+ * 获取文档元信息
51
+ */
52
+ async getDocInfo(spaceId: string, docId: string): Promise<DocInfo | null> {
53
+ try {
54
+ const headers = await this.getHeaders();
55
+ this.log?.info?.(`[DingTalk][Docs] 获取文档信息: spaceId=${spaceId}, docId=${docId}`);
56
+
57
+ const resp = await dingtalkHttp.get(
58
+ `${DINGTALK_API}/v1.0/doc/spaces/${spaceId}/docs/${docId}`,
59
+ { headers, timeout: 10_000 },
60
+ );
61
+
62
+ const data = resp.data;
63
+ this.log?.info?.(`[DingTalk][Docs] 文档信息获取成功: title=${data?.title}`);
64
+
65
+ return {
66
+ docId: data.docId || docId,
67
+ title: data.title || '',
68
+ docType: data.docType || 'unknown',
69
+ creatorId: data.creatorId,
70
+ updatedAt: data.updatedAt,
71
+ };
72
+ } catch (err: any) {
73
+ this.log?.error?.(`[DingTalk][Docs] 获取文档信息失败: ${err.message}`);
74
+ return null;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * 读取文档内容(通过 v2.0/wiki 节点 API)
80
+ */
81
+ async readDoc(nodeId: string, operatorId?: string): Promise<string | null> {
82
+ try {
83
+ const headers = await this.getHeaders();
84
+ this.log?.info?.(`[DingTalk][Docs] 读取知识库节点: nodeId=${nodeId}, operatorId=${operatorId}`);
85
+
86
+ if (!operatorId) {
87
+ this.log?.error?.('[DingTalk][Docs] readDoc 需要 operatorId(unionId)');
88
+ return null;
89
+ }
90
+
91
+ const resp = await dingtalkHttp.get(
92
+ `${DINGTALK_API}/v2.0/wiki/nodes/${nodeId}/content`,
93
+ { headers, params: { operatorId }, timeout: 30_000 },
94
+ );
95
+
96
+ const node = resp.data?.node || resp.data;
97
+ const name = node.name || '未知文档';
98
+ const category = node.category || 'unknown';
99
+ const url = node.url || '';
100
+ const workspaceId = node.workspaceId || '';
101
+
102
+ const content = [
103
+ `文档名: ${name}`,
104
+ `类型: ${category}`,
105
+ `URL: ${url}`,
106
+ `工作区: ${workspaceId}`,
107
+ ].join('\n');
108
+
109
+ this.log?.info?.(`[DingTalk][Docs] 节点信息获取成功: name=${name}, category=${category}`);
110
+ return content;
111
+ } catch (err: any) {
112
+ this.log?.error?.(`[DingTalk][Docs] 读取节点失败: ${err.message}`);
113
+ if (err.response) {
114
+ this.log?.error?.(`[DingTalk][Docs] 错误详情: status=${err.response.status} data=${JSON.stringify(err.response.data)}`);
115
+ }
116
+ return null;
117
+ }
118
+ }
119
+
120
+ /**
121
+ * 从 block 树中递归提取纯文本内容
122
+ */
123
+ private extractTextFromBlocks(blocks: DocBlock[]): string[] {
124
+ const result: string[] = [];
125
+ for (const block of blocks) {
126
+ if (block.text) {
127
+ result.push(block.text);
128
+ }
129
+ if (block.children && block.children.length > 0) {
130
+ result.push(...this.extractTextFromBlocks(block.children));
131
+ }
132
+ }
133
+ return result;
134
+ }
135
+
136
+ /**
137
+ * 向文档追加内容
138
+ */
139
+ async appendToDoc(
140
+ docId: string,
141
+ content: string,
142
+ index: number = -1,
143
+ ): Promise<boolean> {
144
+ try {
145
+ const headers = await this.getHeaders();
146
+ this.log?.info?.(`[DingTalk][Docs] 向文档追加内容: docId=${docId}, contentLen=${content.length}`);
147
+
148
+ const body = {
149
+ blockType: 'PARAGRAPH',
150
+ body: {
151
+ text: content,
152
+ },
153
+ index,
154
+ };
155
+
156
+ await dingtalkHttp.post(
157
+ `${DINGTALK_API}/v1.0/doc/documents/${docId}/blocks/root/children`,
158
+ body,
159
+ { headers, timeout: 10_000 },
160
+ );
161
+
162
+ this.log?.info?.(`[DingTalk][Docs] 内容追加成功`);
163
+ return true;
164
+ } catch (err: any) {
165
+ this.log?.error?.(`[DingTalk][Docs] 追加内容失败: ${err.message}`);
166
+ if (err.response) {
167
+ this.log?.error?.(`[DingTalk][Docs] 错误详情: status=${err.response.status} data=${JSON.stringify(err.response.data)}`);
168
+ }
169
+ return false;
170
+ }
171
+ }
172
+
173
+ /**
174
+ * 创建新文档
175
+ */
176
+ async createDoc(
177
+ spaceId: string,
178
+ title: string,
179
+ content?: string,
180
+ ): Promise<DocInfo | null> {
181
+ try {
182
+ const headers = await this.getHeaders();
183
+ this.log?.info?.(`[DingTalk][Docs] 创建文档: spaceId=${spaceId}, title=${title}`);
184
+
185
+ const body: any = {
186
+ spaceId,
187
+ parentDentryId: '',
188
+ name: title,
189
+ docType: 'alidoc',
190
+ };
191
+
192
+ const resp = await dingtalkHttp.post(
193
+ `${DINGTALK_API}/v1.0/doc/spaces/${spaceId}/docs`,
194
+ body,
195
+ { headers, timeout: 10_000 },
196
+ );
197
+
198
+ const data = resp.data;
199
+ this.log?.info?.(`[DingTalk][Docs] 文档创建成功: docId=${data?.docId}`);
200
+
201
+ const docInfo: DocInfo = {
202
+ docId: data.docId || data.dentryUuid || '',
203
+ title: title,
204
+ docType: data.docType || 'alidoc',
205
+ };
206
+
207
+ if (content && docInfo.docId) {
208
+ await this.appendToDoc(docInfo.docId, content);
209
+ }
210
+
211
+ return docInfo;
212
+ } catch (err: any) {
213
+ this.log?.error?.(`[DingTalk][Docs] 创建文档失败: ${err.message}`);
214
+ if (err.response) {
215
+ this.log?.error?.(`[DingTalk][Docs] 错误详情: status=${err.response.status} data=${JSON.stringify(err.response.data)}`);
216
+ }
217
+ return null;
218
+ }
219
+ }
220
+
221
+ /**
222
+ * 搜索文档
223
+ */
224
+ async searchDocs(
225
+ keyword: string,
226
+ spaceId?: string,
227
+ ): Promise<DocInfo[]> {
228
+ try {
229
+ const headers = await this.getHeaders();
230
+ this.log?.info?.(`[DingTalk][Docs] 搜索文档: keyword=${keyword}, spaceId=${spaceId || '全部'}`);
231
+
232
+ const body: any = { keyword, maxResults: 20 };
233
+ if (spaceId) body.spaceId = spaceId;
234
+
235
+ const resp = await dingtalkHttp.post(
236
+ `${DINGTALK_API}/v1.0/doc/docs/search`,
237
+ body,
238
+ { headers, timeout: 10_000 },
239
+ );
240
+
241
+ const items = resp.data?.items || [];
242
+ const docs: DocInfo[] = items.map((item: any) => ({
243
+ docId: item.docId || item.dentryUuid || '',
244
+ title: item.name || item.title || '',
245
+ docType: item.docType || 'unknown',
246
+ creatorId: item.creatorId,
247
+ updatedAt: item.updatedAt,
248
+ }));
249
+
250
+ this.log?.info?.(`[DingTalk][Docs] 搜索到 ${docs.length} 个文档`);
251
+ return docs;
252
+ } catch (err: any) {
253
+ this.log?.error?.(`[DingTalk][Docs] 搜索文档失败: ${err.message}`);
254
+ return [];
255
+ }
256
+ }
257
+
258
+ /**
259
+ * 列出空间下的文档
260
+ */
261
+ async listDocs(
262
+ spaceId: string,
263
+ parentId?: string,
264
+ ): Promise<DocInfo[]> {
265
+ try {
266
+ const headers = await this.getHeaders();
267
+ this.log?.info?.(`[DingTalk][Docs] 列出文档: spaceId=${spaceId}, parentId=${parentId || '根目录'}`);
268
+
269
+ const params: any = { maxResults: 50 };
270
+ if (parentId) params.parentDentryId = parentId;
271
+
272
+ const resp = await dingtalkHttp.get(
273
+ `${DINGTALK_API}/v1.0/doc/spaces/${spaceId}/dentries`,
274
+ { headers, params, timeout: 10_000 },
275
+ );
276
+
277
+ const items = resp.data?.items || [];
278
+ const docs: DocInfo[] = items.map((item: any) => ({
279
+ docId: item.dentryUuid || item.docId || '',
280
+ title: item.name || '',
281
+ docType: item.docType || item.dentryType || 'unknown',
282
+ creatorId: item.creatorId,
283
+ updatedAt: item.updatedAt,
284
+ }));
285
+
286
+ this.log?.info?.(`[DingTalk][Docs] 列出 ${docs.length} 个文档/目录`);
287
+ return docs;
288
+ } catch (err: any) {
289
+ this.log?.error?.(`[DingTalk][Docs] 列出文档失败: ${err.message}`);
290
+ return [];
291
+ }
292
+ }
293
+ }
@@ -0,0 +1,252 @@
1
+ /**
2
+ * 成就判定引擎
3
+ *
4
+ * 每次操作后检查是否解锁新成就。
5
+ * 成就分为:修行成就、收集成就、产品成就、隐藏成就。
6
+ */
7
+
8
+ import type { Achievement, AchievementCondition, UserProfile, UserCollection, HistoryRecord } from './types.ts';
9
+ import { PRODUCT_BASE_EXP } from './types.ts';
10
+
11
+ /**
12
+ * 成就数据(内联)
13
+ */
14
+ const allAchievements: Achievement[] = [
15
+ // 修行成就
16
+ { id: "A001", name: "初出茅庐", emoji: "🐒", description: "首次成功调用 dws CLI", category: "cultivation", condition: { type: "totalOperations", count: 1 }, expReward: 10 },
17
+ { id: "A002", name: "三天打鱼", emoji: "🔥", description: "连续 3 天签到", category: "cultivation", condition: { type: "consecutiveSignIn", days: 3 }, expReward: 15 },
18
+ { id: "A003", name: "七七四十九", emoji: "📅", description: "连续 49 天签到", category: "cultivation", condition: { type: "consecutiveSignIn", days: 49 }, expReward: 200 },
19
+ { id: "A004", name: "十连斩", emoji: "⚡", description: "单次连击达到 10 次", category: "cultivation", condition: { type: "maxCombo", count: 10 }, expReward: 50 },
20
+ { id: "A005", name: "五行山下", emoji: "🏔️", description: "累计 500 次成功调用", category: "cultivation", condition: { type: "totalOperations", count: 500 }, expReward: 100 },
21
+ { id: "A006", name: "八十一难", emoji: "🌋", description: "累计 81 次 recovery 成功", category: "cultivation", condition: { type: "totalRecoveries", count: 81 }, expReward: 300 },
22
+ // 收集成就
23
+ { id: "A101", name: "妖怪猎人", emoji: "📖", description: "收服 10 种不同妖怪", category: "collection", condition: { type: "uniqueMonsters", count: 10 }, expReward: 30 },
24
+ { id: "A102", name: "半部西游", emoji: "📚", description: "收服 24 种不同妖怪", category: "collection", condition: { type: "uniqueMonsters", count: 24 }, expReward: 100 },
25
+ { id: "A103", name: "妖魔全书", emoji: "📜", description: "收服全部 48 种妖怪", category: "collection", condition: { type: "uniqueMonsters", count: 48 }, expReward: 500, titleReward: "妖魔克星" },
26
+ { id: "A104", name: "闪光猎人", emoji: "✨", description: "收服 1 只闪光妖怪", category: "collection", condition: { type: "shinyMonsters", count: 1 }, expReward: 200 },
27
+ { id: "A105", name: "闪光大师", emoji: "🌈", description: "收服 5 只闪光妖怪", category: "collection", condition: { type: "shinyMonsters", count: 5 }, expReward: 500, titleReward: "欧皇" },
28
+ { id: "A106", name: "全闪通关", emoji: "👑", description: "收服 10 只闪光妖怪", category: "collection", condition: { type: "shinyMonsters", count: 10 }, expReward: 1000, titleReward: "天选之人" },
29
+ // 产品成就
30
+ { id: "A201", name: "表格大师", emoji: "📊", description: "aitable 相关命令成功 50 次", category: "product", condition: { type: "productUsage", product: "aitable", count: 50 }, expReward: 30 },
31
+ { id: "A202", name: "时间管理者", emoji: "📅", description: "calendar 相关命令成功 50 次", category: "product", condition: { type: "productUsage", product: "calendar", count: 50 }, expReward: 30 },
32
+ { id: "A203", name: "群聊达人", emoji: "💬", description: "chat 相关命令成功 50 次", category: "product", condition: { type: "productUsage", product: "chat", count: 50 }, expReward: 30 },
33
+ { id: "A204", name: "待办终结者", emoji: "✅", description: "todo 相关命令成功 50 次", category: "product", condition: { type: "productUsage", product: "todo", count: 50 }, expReward: 30 },
34
+ { id: "A205", name: "日报之王", emoji: "📝", description: "report 连续 30 天提交", category: "product", condition: { type: "consecutiveReport", days: 30 }, expReward: 100 },
35
+ { id: "A206", name: "全能战士", emoji: "🎯", description: "使用过所有 11 个产品", category: "product", condition: { type: "allProducts" }, expReward: 200 },
36
+ // 隐藏成就
37
+ { id: "A301", name: "夜猫子", emoji: "🌙", description: "凌晨 2:00-5:00 成功调用", category: "hidden", condition: { type: "nightOwl" }, expReward: 20 },
38
+ { id: "A302", name: "非酋翻身", emoji: "🎰", description: "触发天命保底(150 次未出传说)", category: "hidden", condition: { type: "pityTriggered" }, expReward: 100, titleReward: "大器晚成" },
39
+ { id: "A303", name: "屡败屡战", emoji: "💀", description: "连续 10 次失败后第 11 次成功", category: "hidden", condition: { type: "consecutiveFailThenSuccess", failCount: 10 }, expReward: 50 },
40
+ { id: "A304", name: "屠龙勇士", emoji: "🐉", description: "单日收服 3 只稀有及以上妖怪", category: "hidden", condition: { type: "dailyRareOrAbove", count: 3 }, expReward: 100 },
41
+ { id: "A305", name: "生日快乐", emoji: "🎂", description: "在账号注册日当天使用", category: "hidden", condition: { type: "birthday" }, expReward: 50 },
42
+ // v2: 逃跑相关成就
43
+ { id: "A401", name: "逃跑大师", emoji: "💨", description: "累计被妖怪逃跑 50 次", category: "hidden", condition: { type: "totalEscapes", count: 50 }, expReward: 30 },
44
+ { id: "A402", name: "一网打尽", emoji: "🪤", description: "连续 10 次掉落无妖怪逃跑", category: "hidden", condition: { type: "consecutiveNoEscape", count: 10 }, expReward: 50 },
45
+ // v2: 悬赏令相关成就
46
+ { id: "A403", name: "赏金猎人", emoji: "📜", description: "累计完成 30 张悬赏令", category: "hidden", condition: { type: "totalBountiesCompleted", count: 30 }, expReward: 100, titleReward: "赏金猎人" },
47
+ { id: "A404", name: "金牌猎人", emoji: "🥇", description: "累计完成 10 张金令", category: "hidden", condition: { type: "goldBountiesCompleted", count: 10 }, expReward: 150 },
48
+ { id: "A405", name: "全勤猎人", emoji: "📅", description: "连续 7 天每日完成全部 3 张悬赏令", category: "hidden", condition: { type: "consecutiveFullClear", days: 7 }, expReward: 200 },
49
+ // v2: 随机事件相关成就
50
+ { id: "A406", name: "见多识广", emoji: "🎪", description: "累计触发 10 次随机事件", category: "hidden", condition: { type: "totalEventsTriggered", count: 10 }, expReward: 30 },
51
+ { id: "A407", name: "百战百胜", emoji: "⚔️", description: "累计完成 10 次挑战事件", category: "hidden", condition: { type: "challengesCompleted", count: 10 }, expReward: 100, titleReward: "战神" },
52
+ { id: "A408", name: "劫后余生", emoji: "😈", description: "触发「走火入魔」后存活(修行值未归零)", category: "hidden", condition: { type: "survivedMadness" }, expReward: 50, titleReward: "大难不死" },
53
+ { id: "A409", name: "蟠桃常客", emoji: "🍑", description: "累计触发 3 次「蟠桃大会」", category: "hidden", condition: { type: "specificEventCount", eventId: "EV001", count: 3 }, expReward: 80 },
54
+ { id: "A410", name: "火焰山主", emoji: "🔥", description: "累计完成 3 次「火焰山」挑战", category: "hidden", condition: { type: "specificEventCount", eventId: "EV102", count: 3 }, expReward: 60 },
55
+ { id: "A411", name: "真假悟空", emoji: "🐒", description: "在「真假美猴王」事件中选对", category: "hidden", condition: { type: "specificChallengeSuccess", eventId: "EV104" }, expReward: 100, titleReward: "火眼金睛" },
56
+ { id: "A412", name: "否极泰来", emoji: "🌈", description: "灾厄事件结束后立即触发增益事件", category: "hidden", condition: { type: "disasterThenBlessing" }, expReward: 200 },
57
+ { id: "A413", name: "化险为夷", emoji: "🛡️", description: "累计 5 次通过化解方式提前解除灾厄", category: "hidden", condition: { type: "disastersResolved", count: 5 }, expReward: 80 },
58
+ ];
59
+
60
+ /**
61
+ * 获取所有成就
62
+ */
63
+ export function getAllAchievements(): Achievement[] {
64
+ return allAchievements;
65
+ }
66
+
67
+ /**
68
+ * 根据 ID 查找成就
69
+ */
70
+ export function getAchievementById(achievementId: string): Achievement | undefined {
71
+ return allAchievements.find(a => a.id === achievementId);
72
+ }
73
+
74
+ /**
75
+ * 检查单个成就条件是否满足
76
+ */
77
+ function isConditionMet(
78
+ condition: AchievementCondition,
79
+ profile: UserProfile,
80
+ collection: UserCollection,
81
+ todayRecords: HistoryRecord[]
82
+ ): boolean {
83
+ switch (condition.type) {
84
+ case 'totalOperations':
85
+ return profile.totalOperations >= condition.count;
86
+
87
+ case 'consecutiveSignIn':
88
+ return profile.consecutiveSignInDays >= condition.days;
89
+
90
+ case 'maxCombo':
91
+ return profile.maxCombo >= condition.count;
92
+
93
+ case 'totalRecoveries':
94
+ return profile.totalRecoveries >= condition.count;
95
+
96
+ case 'uniqueMonsters': {
97
+ const uniqueNonShiny = collection.entries.filter(e => !e.isShiny).length;
98
+ return uniqueNonShiny >= condition.count;
99
+ }
100
+
101
+ case 'shinyMonsters': {
102
+ const shinyCount = collection.entries.filter(e => e.isShiny).length;
103
+ return shinyCount >= condition.count;
104
+ }
105
+
106
+ case 'productUsage': {
107
+ const usage = profile.productUsage[condition.product] ?? 0;
108
+ return usage >= condition.count;
109
+ }
110
+
111
+ case 'allProducts': {
112
+ const allProducts = Object.keys(PRODUCT_BASE_EXP);
113
+ return allProducts.every(p => (profile.productUsage[p] ?? 0) > 0);
114
+ }
115
+
116
+ case 'dailyOperations': {
117
+ return todayRecords.filter(r => r.success).length >= condition.count;
118
+ }
119
+
120
+ case 'nightOwl': {
121
+ const hour = new Date().getHours();
122
+ return hour >= 2 && hour < 5;
123
+ }
124
+
125
+ case 'pityTriggered':
126
+ // 由掉落引擎在触发天命保底时标记
127
+ return false;
128
+
129
+ case 'consecutiveFailThenSuccess':
130
+ return profile.consecutiveFailures >= condition.failCount;
131
+
132
+ case 'dailyRareOrAbove': {
133
+ const rareOrAboveToday = todayRecords.filter(r => {
134
+ if (!r.monsterId) return false;
135
+ // 简化判断:monsterId 以 S/E/L 开头的是稀有及以上
136
+ return r.monsterId.startsWith('S') || r.monsterId.startsWith('E') || r.monsterId.startsWith('L');
137
+ });
138
+ return rareOrAboveToday.length >= condition.count;
139
+ }
140
+
141
+ case 'birthday': {
142
+ const createdDate = new Date(profile.createdAt);
143
+ const now = new Date();
144
+ return (
145
+ createdDate.getMonth() === now.getMonth() &&
146
+ createdDate.getDate() === now.getDate() &&
147
+ now.getFullYear() > createdDate.getFullYear()
148
+ );
149
+ }
150
+
151
+ case 'consecutiveReport': {
152
+ const reportUsage = profile.productUsage['report'] ?? 0;
153
+ return reportUsage >= condition.days;
154
+ }
155
+
156
+ // v2: 逃跑相关
157
+ case 'totalEscapes':
158
+ return profile.totalEscapes >= condition.count;
159
+
160
+ case 'consecutiveNoEscape':
161
+ // 由主引擎在连续无逃跑时通过 triggerSpecialAchievement 触发
162
+ return false;
163
+
164
+ // v2: 悬赏令相关
165
+ case 'totalBountiesCompleted':
166
+ return profile.bountyHistory.totalCompleted >= condition.count;
167
+
168
+ case 'goldBountiesCompleted':
169
+ return profile.bountyHistory.goldCompleted >= condition.count;
170
+
171
+ case 'consecutiveFullClear':
172
+ return profile.bountyHistory.consecutiveFullClear >= condition.days;
173
+
174
+ // v2: 随机事件相关
175
+ case 'totalEventsTriggered':
176
+ return profile.eventStats.totalTriggered >= condition.count;
177
+
178
+ case 'challengesCompleted':
179
+ return profile.eventStats.challengesCompleted >= condition.count;
180
+
181
+ case 'survivedMadness': {
182
+ // 触发过"走火入魔"且修行值 > 0
183
+ const hadMadness = profile.eventHistory.some(e => e.eventId === 'EV206');
184
+ return hadMadness && profile.totalExp > 0;
185
+ }
186
+
187
+ case 'specificEventCount': {
188
+ const eventCount = profile.eventHistory.filter(e => e.eventId === condition.eventId).length;
189
+ return eventCount >= condition.count;
190
+ }
191
+
192
+ case 'specificChallengeSuccess': {
193
+ return profile.eventHistory.some(
194
+ e => e.eventId === condition.eventId && e.outcome === 'success'
195
+ );
196
+ }
197
+
198
+ case 'disasterThenBlessing':
199
+ // 由主引擎在灾厄结束后立即触发增益时通过 triggerSpecialAchievement 触发
200
+ return false;
201
+
202
+ case 'disastersResolved':
203
+ return profile.eventStats.disastersResolved >= condition.count;
204
+
205
+ default:
206
+ return false;
207
+ }
208
+ }
209
+
210
+ /**
211
+ * 检查所有未解锁的成就,返回新解锁的成就列表
212
+ */
213
+ export function checkAchievements(
214
+ profile: UserProfile,
215
+ collection: UserCollection,
216
+ todayRecords: HistoryRecord[]
217
+ ): Achievement[] {
218
+ const newlyUnlocked: Achievement[] = [];
219
+
220
+ for (const achievement of allAchievements) {
221
+ // 跳过已解锁的
222
+ if (profile.unlockedAchievements.includes(achievement.id)) {
223
+ continue;
224
+ }
225
+
226
+ // 检查条件
227
+ if (isConditionMet(achievement.condition, profile, collection, todayRecords)) {
228
+ newlyUnlocked.push(achievement);
229
+ profile.unlockedAchievements.push(achievement.id);
230
+ }
231
+ }
232
+
233
+ return newlyUnlocked;
234
+ }
235
+
236
+ /**
237
+ * 手动触发特殊成就(如保底触发)
238
+ */
239
+ export function triggerSpecialAchievement(
240
+ profile: UserProfile,
241
+ achievementId: string
242
+ ): Achievement | null {
243
+ if (profile.unlockedAchievements.includes(achievementId)) {
244
+ return null;
245
+ }
246
+
247
+ const achievement = getAchievementById(achievementId);
248
+ if (!achievement) return null;
249
+
250
+ profile.unlockedAchievements.push(achievementId);
251
+ return achievement;
252
+ }