llmapi-v2 2.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 (162) hide show
  1. package/.env.example +40 -0
  2. package/Dockerfile +17 -0
  3. package/dist/config.d.ts +48 -0
  4. package/dist/config.js +98 -0
  5. package/dist/config.js.map +1 -0
  6. package/dist/converter/request.d.ts +6 -0
  7. package/dist/converter/request.js +184 -0
  8. package/dist/converter/request.js.map +1 -0
  9. package/dist/converter/response.d.ts +6 -0
  10. package/dist/converter/response.js +76 -0
  11. package/dist/converter/response.js.map +1 -0
  12. package/dist/converter/stream.d.ts +54 -0
  13. package/dist/converter/stream.js +318 -0
  14. package/dist/converter/stream.js.map +1 -0
  15. package/dist/converter/types.d.ts +239 -0
  16. package/dist/converter/types.js +6 -0
  17. package/dist/converter/types.js.map +1 -0
  18. package/dist/data/posts.d.ts +19 -0
  19. package/dist/data/posts.js +462 -0
  20. package/dist/data/posts.js.map +1 -0
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.js +233 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/middleware/api-key-auth.d.ts +6 -0
  25. package/dist/middleware/api-key-auth.js +76 -0
  26. package/dist/middleware/api-key-auth.js.map +1 -0
  27. package/dist/middleware/quota-guard.d.ts +10 -0
  28. package/dist/middleware/quota-guard.js +27 -0
  29. package/dist/middleware/quota-guard.js.map +1 -0
  30. package/dist/middleware/rate-limiter.d.ts +5 -0
  31. package/dist/middleware/rate-limiter.js +50 -0
  32. package/dist/middleware/rate-limiter.js.map +1 -0
  33. package/dist/middleware/request-logger.d.ts +6 -0
  34. package/dist/middleware/request-logger.js +37 -0
  35. package/dist/middleware/request-logger.js.map +1 -0
  36. package/dist/middleware/session-auth.d.ts +19 -0
  37. package/dist/middleware/session-auth.js +99 -0
  38. package/dist/middleware/session-auth.js.map +1 -0
  39. package/dist/providers/aliyun.d.ts +13 -0
  40. package/dist/providers/aliyun.js +20 -0
  41. package/dist/providers/aliyun.js.map +1 -0
  42. package/dist/providers/base-provider.d.ts +36 -0
  43. package/dist/providers/base-provider.js +133 -0
  44. package/dist/providers/base-provider.js.map +1 -0
  45. package/dist/providers/deepseek.d.ts +11 -0
  46. package/dist/providers/deepseek.js +18 -0
  47. package/dist/providers/deepseek.js.map +1 -0
  48. package/dist/providers/registry.d.ts +18 -0
  49. package/dist/providers/registry.js +98 -0
  50. package/dist/providers/registry.js.map +1 -0
  51. package/dist/providers/types.d.ts +17 -0
  52. package/dist/providers/types.js +3 -0
  53. package/dist/providers/types.js.map +1 -0
  54. package/dist/routes/admin.d.ts +1 -0
  55. package/dist/routes/admin.js +153 -0
  56. package/dist/routes/admin.js.map +1 -0
  57. package/dist/routes/auth.d.ts +2 -0
  58. package/dist/routes/auth.js +318 -0
  59. package/dist/routes/auth.js.map +1 -0
  60. package/dist/routes/blog.d.ts +1 -0
  61. package/dist/routes/blog.js +29 -0
  62. package/dist/routes/blog.js.map +1 -0
  63. package/dist/routes/dashboard.d.ts +1 -0
  64. package/dist/routes/dashboard.js +184 -0
  65. package/dist/routes/dashboard.js.map +1 -0
  66. package/dist/routes/messages.d.ts +1 -0
  67. package/dist/routes/messages.js +309 -0
  68. package/dist/routes/messages.js.map +1 -0
  69. package/dist/routes/models.d.ts +1 -0
  70. package/dist/routes/models.js +39 -0
  71. package/dist/routes/models.js.map +1 -0
  72. package/dist/routes/payment.d.ts +1 -0
  73. package/dist/routes/payment.js +150 -0
  74. package/dist/routes/payment.js.map +1 -0
  75. package/dist/routes/sitemap.d.ts +1 -0
  76. package/dist/routes/sitemap.js +38 -0
  77. package/dist/routes/sitemap.js.map +1 -0
  78. package/dist/services/alipay.d.ts +27 -0
  79. package/dist/services/alipay.js +106 -0
  80. package/dist/services/alipay.js.map +1 -0
  81. package/dist/services/database.d.ts +4 -0
  82. package/dist/services/database.js +170 -0
  83. package/dist/services/database.js.map +1 -0
  84. package/dist/services/health-checker.d.ts +13 -0
  85. package/dist/services/health-checker.js +95 -0
  86. package/dist/services/health-checker.js.map +1 -0
  87. package/dist/services/mailer.d.ts +3 -0
  88. package/dist/services/mailer.js +91 -0
  89. package/dist/services/mailer.js.map +1 -0
  90. package/dist/services/metrics.d.ts +56 -0
  91. package/dist/services/metrics.js +94 -0
  92. package/dist/services/metrics.js.map +1 -0
  93. package/dist/services/remote-control.d.ts +20 -0
  94. package/dist/services/remote-control.js +209 -0
  95. package/dist/services/remote-control.js.map +1 -0
  96. package/dist/services/remote-ws.d.ts +5 -0
  97. package/dist/services/remote-ws.js +143 -0
  98. package/dist/services/remote-ws.js.map +1 -0
  99. package/dist/services/usage.d.ts +13 -0
  100. package/dist/services/usage.js +39 -0
  101. package/dist/services/usage.js.map +1 -0
  102. package/dist/utils/errors.d.ts +27 -0
  103. package/dist/utils/errors.js +48 -0
  104. package/dist/utils/errors.js.map +1 -0
  105. package/dist/utils/logger.d.ts +2 -0
  106. package/dist/utils/logger.js +14 -0
  107. package/dist/utils/logger.js.map +1 -0
  108. package/docker-compose.yml +19 -0
  109. package/package.json +39 -0
  110. package/public/robots.txt +8 -0
  111. package/src/config.ts +140 -0
  112. package/src/converter/request.ts +207 -0
  113. package/src/converter/response.ts +85 -0
  114. package/src/converter/stream.ts +373 -0
  115. package/src/converter/types.ts +257 -0
  116. package/src/data/posts.ts +474 -0
  117. package/src/index.ts +219 -0
  118. package/src/middleware/api-key-auth.ts +82 -0
  119. package/src/middleware/quota-guard.ts +28 -0
  120. package/src/middleware/rate-limiter.ts +61 -0
  121. package/src/middleware/request-logger.ts +36 -0
  122. package/src/middleware/session-auth.ts +91 -0
  123. package/src/providers/aliyun.ts +16 -0
  124. package/src/providers/base-provider.ts +148 -0
  125. package/src/providers/deepseek.ts +14 -0
  126. package/src/providers/registry.ts +111 -0
  127. package/src/providers/types.ts +26 -0
  128. package/src/routes/admin.ts +169 -0
  129. package/src/routes/auth.ts +369 -0
  130. package/src/routes/blog.ts +28 -0
  131. package/src/routes/dashboard.ts +208 -0
  132. package/src/routes/messages.ts +346 -0
  133. package/src/routes/models.ts +37 -0
  134. package/src/routes/payment.ts +189 -0
  135. package/src/routes/sitemap.ts +40 -0
  136. package/src/services/alipay.ts +116 -0
  137. package/src/services/database.ts +187 -0
  138. package/src/services/health-checker.ts +115 -0
  139. package/src/services/mailer.ts +90 -0
  140. package/src/services/metrics.ts +104 -0
  141. package/src/services/remote-control.ts +226 -0
  142. package/src/services/remote-ws.ts +145 -0
  143. package/src/services/usage.ts +57 -0
  144. package/src/types/express.d.ts +46 -0
  145. package/src/utils/errors.ts +44 -0
  146. package/src/utils/logger.ts +8 -0
  147. package/tsconfig.json +17 -0
  148. package/views/pages/404.ejs +14 -0
  149. package/views/pages/admin.ejs +307 -0
  150. package/views/pages/blog-post.ejs +378 -0
  151. package/views/pages/blog.ejs +148 -0
  152. package/views/pages/dashboard.ejs +441 -0
  153. package/views/pages/docs.ejs +807 -0
  154. package/views/pages/index.ejs +416 -0
  155. package/views/pages/login.ejs +170 -0
  156. package/views/pages/orders.ejs +111 -0
  157. package/views/pages/pricing.ejs +379 -0
  158. package/views/pages/register.ejs +397 -0
  159. package/views/pages/remote.ejs +334 -0
  160. package/views/pages/settings.ejs +373 -0
  161. package/views/partials/header.ejs +70 -0
  162. package/views/partials/nav.ejs +140 -0
@@ -0,0 +1,373 @@
1
+ <%- include('../partials/header') %>
2
+ <%- include('../partials/nav') %>
3
+
4
+ <main class="min-h-[calc(100vh-4rem)] py-10 px-4">
5
+ <div class="max-w-3xl mx-auto space-y-8">
6
+ <!-- Page Header -->
7
+ <div>
8
+ <h1 class="text-2xl font-bold text-claude-dark">账户设置</h1>
9
+ <p class="mt-1 text-sm text-claude-dark/60">管理您的个人信息、密码和订阅</p>
10
+ </div>
11
+
12
+ <!-- Global Messages -->
13
+ <div id="globalSuccess" class="hidden p-4 rounded-lg bg-green-50 border border-green-200 text-sm text-green-700"></div>
14
+ <div id="globalError" class="hidden p-4 rounded-lg bg-red-50 border border-red-200 text-sm text-red-700"></div>
15
+
16
+ <!-- Profile Section -->
17
+ <section class="bg-white rounded-2xl shadow-sm border border-claude-dark/5 p-6">
18
+ <h2 class="text-lg font-semibold text-claude-dark mb-4">个人信息</h2>
19
+ <form id="profileForm" onsubmit="handleUpdateProfile(event)" class="space-y-4">
20
+ <div>
21
+ <label for="profileName" class="block text-sm font-medium text-claude-dark/80 mb-1.5">用户名</label>
22
+ <input
23
+ type="text"
24
+ id="profileName"
25
+ value="<%= viewUser.name %>"
26
+ required
27
+ minlength="2"
28
+ class="w-full px-4 py-2.5 rounded-lg border border-claude-dark/15 bg-claude-cream/30 text-claude-dark focus:outline-none focus:ring-2 focus:ring-claude-orange/30 focus:border-claude-orange transition-colors"
29
+ >
30
+ </div>
31
+ <div>
32
+ <label for="profileEmail" class="block text-sm font-medium text-claude-dark/80 mb-1.5">邮箱地址</label>
33
+ <input
34
+ type="email"
35
+ id="profileEmail"
36
+ value="<%= viewUser.email %>"
37
+ disabled
38
+ class="w-full px-4 py-2.5 rounded-lg border border-claude-dark/10 bg-claude-dark/5 text-claude-dark/50 cursor-not-allowed"
39
+ >
40
+ <p class="mt-1 text-xs text-claude-dark/40">邮箱地址不可更改</p>
41
+ </div>
42
+ <div class="flex justify-end">
43
+ <button
44
+ type="submit"
45
+ id="profileBtn"
46
+ class="px-6 py-2.5 bg-claude-orange text-white font-medium rounded-lg hover:bg-claude-orange/90 transition-colors focus:outline-none focus:ring-2 focus:ring-claude-orange/30 disabled:opacity-50 disabled:cursor-not-allowed text-sm"
47
+ >
48
+ 保存修改
49
+ </button>
50
+ </div>
51
+ </form>
52
+ </section>
53
+
54
+ <!-- Change Password Section -->
55
+ <section class="bg-white rounded-2xl shadow-sm border border-claude-dark/5 p-6">
56
+ <h2 class="text-lg font-semibold text-claude-dark mb-4">修改密码</h2>
57
+ <div id="pwdError" class="hidden mb-4 p-3 rounded-lg bg-red-50 border border-red-200 text-sm text-red-700"></div>
58
+ <div id="pwdSuccess" class="hidden mb-4 p-3 rounded-lg bg-green-50 border border-green-200 text-sm text-green-700"></div>
59
+ <form id="passwordForm" onsubmit="handleChangePassword(event)" class="space-y-4">
60
+ <div>
61
+ <label for="currentPassword" class="block text-sm font-medium text-claude-dark/80 mb-1.5">当前密码</label>
62
+ <input
63
+ type="password"
64
+ id="currentPassword"
65
+ required
66
+ placeholder="输入当前密码"
67
+ class="w-full px-4 py-2.5 rounded-lg border border-claude-dark/15 bg-claude-cream/30 text-claude-dark placeholder-claude-dark/30 focus:outline-none focus:ring-2 focus:ring-claude-orange/30 focus:border-claude-orange transition-colors"
68
+ >
69
+ </div>
70
+ <div>
71
+ <label for="newPassword" class="block text-sm font-medium text-claude-dark/80 mb-1.5">新密码</label>
72
+ <input
73
+ type="password"
74
+ id="newPassword"
75
+ required
76
+ minlength="8"
77
+ placeholder="至少 8 个字符"
78
+ class="w-full px-4 py-2.5 rounded-lg border border-claude-dark/15 bg-claude-cream/30 text-claude-dark placeholder-claude-dark/30 focus:outline-none focus:ring-2 focus:ring-claude-orange/30 focus:border-claude-orange transition-colors"
79
+ >
80
+ </div>
81
+ <div>
82
+ <label for="confirmNewPassword" class="block text-sm font-medium text-claude-dark/80 mb-1.5">确认新密码</label>
83
+ <input
84
+ type="password"
85
+ id="confirmNewPassword"
86
+ required
87
+ minlength="8"
88
+ placeholder="再次输入新密码"
89
+ class="w-full px-4 py-2.5 rounded-lg border border-claude-dark/15 bg-claude-cream/30 text-claude-dark placeholder-claude-dark/30 focus:outline-none focus:ring-2 focus:ring-claude-orange/30 focus:border-claude-orange transition-colors"
90
+ >
91
+ </div>
92
+ <div class="flex justify-end">
93
+ <button
94
+ type="submit"
95
+ id="pwdBtn"
96
+ class="px-6 py-2.5 bg-claude-orange text-white font-medium rounded-lg hover:bg-claude-orange/90 transition-colors focus:outline-none focus:ring-2 focus:ring-claude-orange/30 disabled:opacity-50 disabled:cursor-not-allowed text-sm"
97
+ >
98
+ 修改密码
99
+ </button>
100
+ </div>
101
+ </form>
102
+ </section>
103
+
104
+ <!-- Current Plan Section -->
105
+ <section class="bg-white rounded-2xl shadow-sm border border-claude-dark/5 p-6">
106
+ <div class="flex items-center justify-between">
107
+ <div>
108
+ <h2 class="text-lg font-semibold text-claude-dark">当前套餐</h2>
109
+ <p class="mt-1 text-sm text-claude-dark/60" id="planInfo">加载中...</p>
110
+ </div>
111
+ <a
112
+ href="/pricing"
113
+ class="px-5 py-2.5 bg-claude-cream text-claude-orange font-medium rounded-lg hover:bg-claude-orange hover:text-white transition-colors text-sm border border-claude-orange/20"
114
+ >
115
+ 升级套餐
116
+ </a>
117
+ </div>
118
+ </section>
119
+
120
+ <!-- Danger Zone -->
121
+ <section class="bg-white rounded-2xl shadow-sm border border-red-200 p-6">
122
+ <h2 class="text-lg font-semibold text-red-600 mb-2">危险操作</h2>
123
+ <p class="text-sm text-claude-dark/60 mb-4">删除账户后,所有数据将被永久清除且无法恢复。此操作不可撤销。</p>
124
+ <button
125
+ onclick="showDeleteModal()"
126
+ class="px-5 py-2.5 bg-red-50 text-red-600 font-medium rounded-lg hover:bg-red-100 transition-colors text-sm border border-red-200"
127
+ >
128
+ 删除账户
129
+ </button>
130
+ </section>
131
+ </div>
132
+ </main>
133
+
134
+ <!-- Delete Account Modal -->
135
+ <div id="deleteModal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
136
+ <div class="bg-white rounded-2xl shadow-xl max-w-md w-full mx-4 p-6">
137
+ <div class="text-center mb-6">
138
+ <div class="w-12 h-12 rounded-full bg-red-100 flex items-center justify-center mx-auto mb-4">
139
+ <svg class="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
140
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"/>
141
+ </svg>
142
+ </div>
143
+ <h3 class="text-lg font-semibold text-claude-dark">确认删除账户</h3>
144
+ <p class="mt-2 text-sm text-claude-dark/60">此操作不可撤销。请输入您的邮箱地址 <span class="font-medium text-claude-dark"><%= viewUser.email %></span> 以确认删除。</p>
145
+ </div>
146
+
147
+ <div id="deleteError" class="hidden mb-4 p-3 rounded-lg bg-red-50 border border-red-200 text-sm text-red-700"></div>
148
+
149
+ <div class="mb-4">
150
+ <input
151
+ type="email"
152
+ id="deleteEmailConfirm"
153
+ placeholder="输入您的邮箱地址"
154
+ class="w-full px-4 py-2.5 rounded-lg border border-claude-dark/15 bg-claude-cream/30 text-claude-dark placeholder-claude-dark/30 focus:outline-none focus:ring-2 focus:ring-red-300 focus:border-red-400 transition-colors"
155
+ >
156
+ </div>
157
+
158
+ <div class="flex space-x-3">
159
+ <button
160
+ onclick="hideDeleteModal()"
161
+ class="flex-1 py-2.5 px-4 bg-claude-dark/5 text-claude-dark font-medium rounded-lg hover:bg-claude-dark/10 transition-colors text-sm"
162
+ >
163
+ 取消
164
+ </button>
165
+ <button
166
+ onclick="handleDeleteAccount()"
167
+ id="deleteBtn"
168
+ class="flex-1 py-2.5 px-4 bg-red-600 text-white font-medium rounded-lg hover:bg-red-700 transition-colors text-sm disabled:opacity-50 disabled:cursor-not-allowed"
169
+ >
170
+ 确认删除
171
+ </button>
172
+ </div>
173
+ </div>
174
+ </div>
175
+
176
+ <script>
177
+ // Load current plan info
178
+ (async function() {
179
+ try {
180
+ var res = await fetch('/api/auth/me', { credentials: 'same-origin' });
181
+ var data = await res.json();
182
+ if (data.success && data.subscription) {
183
+ var sub = data.subscription;
184
+ document.getElementById('planInfo').textContent =
185
+ (sub.display_name || sub.plan_name) + ' - ' +
186
+ (sub.price_monthly > 0 ? sub.price_monthly + ' 元/月' : '免费') +
187
+ ' | 额度: ' + formatTokens(sub.token_limit_monthly) + ' tokens/月';
188
+ } else {
189
+ document.getElementById('planInfo').textContent = 'Free - 免费套餐';
190
+ }
191
+ } catch (e) {
192
+ document.getElementById('planInfo').textContent = '无法加载套餐信息';
193
+ }
194
+ })();
195
+
196
+ function formatTokens(n) {
197
+ if (!n) return '0';
198
+ if (n >= 1000000000) return (n / 1000000000).toFixed(0) + 'B';
199
+ if (n >= 1000000) return (n / 1000000).toFixed(0) + 'M';
200
+ if (n >= 1000) return (n / 1000).toFixed(0) + 'K';
201
+ return String(n);
202
+ }
203
+
204
+ function showGlobalSuccess(msg) {
205
+ var el = document.getElementById('globalSuccess');
206
+ el.textContent = msg;
207
+ el.classList.remove('hidden');
208
+ document.getElementById('globalError').classList.add('hidden');
209
+ setTimeout(function() { el.classList.add('hidden'); }, 3000);
210
+ }
211
+
212
+ function showGlobalError(msg) {
213
+ var el = document.getElementById('globalError');
214
+ el.textContent = msg;
215
+ el.classList.remove('hidden');
216
+ document.getElementById('globalSuccess').classList.add('hidden');
217
+ }
218
+
219
+ // Update profile
220
+ async function handleUpdateProfile(e) {
221
+ e.preventDefault();
222
+ var btn = document.getElementById('profileBtn');
223
+ var name = document.getElementById('profileName').value.trim();
224
+
225
+ if (!name || name.length < 2) {
226
+ showGlobalError('用户名至少需要 2 个字符');
227
+ return;
228
+ }
229
+
230
+ btn.disabled = true;
231
+ btn.textContent = '保存中...';
232
+
233
+ try {
234
+ var res = await fetch('/api/auth/update-profile', {
235
+ method: 'POST',
236
+ headers: { 'Content-Type': 'application/json' },
237
+ credentials: 'same-origin',
238
+ body: JSON.stringify({ name: name }),
239
+ });
240
+
241
+ var data = await res.json();
242
+
243
+ if (data.success) {
244
+ showGlobalSuccess('个人信息已更新');
245
+ } else {
246
+ showGlobalError(data.error || '更新失败,请重试');
247
+ }
248
+ } catch (err) {
249
+ showGlobalError('网络错误,请重试');
250
+ } finally {
251
+ btn.disabled = false;
252
+ btn.textContent = '保存修改';
253
+ }
254
+ }
255
+
256
+ // Change password
257
+ async function handleChangePassword(e) {
258
+ e.preventDefault();
259
+ var pwdError = document.getElementById('pwdError');
260
+ var pwdSuccess = document.getElementById('pwdSuccess');
261
+ pwdError.classList.add('hidden');
262
+ pwdSuccess.classList.add('hidden');
263
+
264
+ var currentPassword = document.getElementById('currentPassword').value;
265
+ var newPassword = document.getElementById('newPassword').value;
266
+ var confirmNewPassword = document.getElementById('confirmNewPassword').value;
267
+
268
+ if (!currentPassword) {
269
+ pwdError.textContent = '请输入当前密码';
270
+ pwdError.classList.remove('hidden');
271
+ return;
272
+ }
273
+ if (newPassword.length < 8) {
274
+ pwdError.textContent = '新密码至少需要 8 个字符';
275
+ pwdError.classList.remove('hidden');
276
+ return;
277
+ }
278
+ if (newPassword !== confirmNewPassword) {
279
+ pwdError.textContent = '两次输入的新密码不一致';
280
+ pwdError.classList.remove('hidden');
281
+ return;
282
+ }
283
+
284
+ var btn = document.getElementById('pwdBtn');
285
+ btn.disabled = true;
286
+ btn.textContent = '修改中...';
287
+
288
+ try {
289
+ var res = await fetch('/api/auth/change-password', {
290
+ method: 'POST',
291
+ headers: { 'Content-Type': 'application/json' },
292
+ credentials: 'same-origin',
293
+ body: JSON.stringify({ currentPassword: currentPassword, newPassword: newPassword }),
294
+ });
295
+
296
+ var data = await res.json();
297
+
298
+ if (data.success) {
299
+ pwdSuccess.textContent = '密码已成功修改';
300
+ pwdSuccess.classList.remove('hidden');
301
+ document.getElementById('passwordForm').reset();
302
+ } else {
303
+ pwdError.textContent = data.error || '修改失败,请重试';
304
+ pwdError.classList.remove('hidden');
305
+ }
306
+ } catch (err) {
307
+ pwdError.textContent = '网络错误,请重试';
308
+ pwdError.classList.remove('hidden');
309
+ } finally {
310
+ btn.disabled = false;
311
+ btn.textContent = '修改密码';
312
+ }
313
+ }
314
+
315
+ // Delete account modal
316
+ function showDeleteModal() {
317
+ document.getElementById('deleteModal').classList.remove('hidden');
318
+ document.getElementById('deleteEmailConfirm').value = '';
319
+ document.getElementById('deleteError').classList.add('hidden');
320
+ }
321
+
322
+ function hideDeleteModal() {
323
+ document.getElementById('deleteModal').classList.add('hidden');
324
+ }
325
+
326
+ // Close modal on backdrop click
327
+ document.getElementById('deleteModal').addEventListener('click', function(e) {
328
+ if (e.target === this) hideDeleteModal();
329
+ });
330
+
331
+ async function handleDeleteAccount() {
332
+ var email = document.getElementById('deleteEmailConfirm').value.trim();
333
+ var deleteError = document.getElementById('deleteError');
334
+ deleteError.classList.add('hidden');
335
+
336
+ if (email !== '<%= viewUser.email %>') {
337
+ deleteError.textContent = '邮箱地址不匹配,请重新输入';
338
+ deleteError.classList.remove('hidden');
339
+ return;
340
+ }
341
+
342
+ var btn = document.getElementById('deleteBtn');
343
+ btn.disabled = true;
344
+ btn.textContent = '删除中...';
345
+
346
+ try {
347
+ var res = await fetch('/api/auth/delete-account', {
348
+ method: 'POST',
349
+ headers: { 'Content-Type': 'application/json' },
350
+ credentials: 'same-origin',
351
+ body: JSON.stringify({ email: email }),
352
+ });
353
+
354
+ var data = await res.json();
355
+
356
+ if (data.success) {
357
+ window.location.href = '/?deleted=1';
358
+ } else {
359
+ deleteError.textContent = data.error || '删除失败,请重试';
360
+ deleteError.classList.remove('hidden');
361
+ }
362
+ } catch (err) {
363
+ deleteError.textContent = '网络错误,请重试';
364
+ deleteError.classList.remove('hidden');
365
+ } finally {
366
+ btn.disabled = false;
367
+ btn.textContent = '确认删除';
368
+ }
369
+ }
370
+ </script>
371
+
372
+ </body>
373
+ </html>
@@ -0,0 +1,70 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title><%= typeof pageTitle !== 'undefined' ? pageTitle + ' - LLM API' : 'LLM API - Claude Code Compatible API | 90% Cost Savings' %></title>
7
+ <meta name="description" content="LLM API — a Claude Code compatible API with 90% cost savings. Two-variable setup, multi-provider high availability, stable and fast. Full tool support.">
8
+ <meta name="keywords" content="Claude Code API, cheap Claude Code, AI coding API, LLM API, API relay, Anthropic API, AI developer tools, code assistant API, Claude Code compatible">
9
+ <link rel="canonical" href="https://llmapi.pro<%= typeof canonicalPath !== 'undefined' ? canonicalPath : '' %>">
10
+
11
+ <!-- Open Graph -->
12
+ <meta property="og:type" content="website">
13
+ <meta property="og:url" content="https://llmapi.pro<%= typeof canonicalPath !== 'undefined' ? canonicalPath : '' %>">
14
+ <meta property="og:title" content="<%= typeof pageTitle !== 'undefined' ? pageTitle + ' - LLM API' : 'LLM API - Claude Code Compatible API | 90% Cost Savings' %>">
15
+ <meta property="og:description" content="Claude Code compatible API relay with 90% cost savings. One-line config, multi-provider high availability.">
16
+ <meta property="og:site_name" content="LLM API">
17
+ <meta property="og:locale" content="en_US">
18
+
19
+ <!-- Twitter Card -->
20
+ <meta name="twitter:card" content="summary_large_image">
21
+ <meta name="twitter:title" content="<%= typeof pageTitle !== 'undefined' ? pageTitle + ' - LLM API' : 'LLM API - Claude Code Compatible API | 90% Cost Savings' %>">
22
+ <meta name="twitter:description" content="Claude Code compatible API relay with 90% cost savings. One-line config, multi-provider high availability.">
23
+ <meta name="twitter:site" content="@llmapi">
24
+
25
+ <!-- JSON-LD Organization Schema -->
26
+ <script type="application/ld+json">
27
+ {
28
+ "@context": "https://schema.org",
29
+ "@type": "Organization",
30
+ "name": "LLM API",
31
+ "url": "https://llmapi.pro",
32
+ "description": "Claude Code compatible API relay service with 90% cost savings and multi-provider high availability.",
33
+ "sameAs": []
34
+ }
35
+ </script>
36
+
37
+ <!-- Tailwind CSS -->
38
+ <script src="https://cdn.tailwindcss.com"></script>
39
+ <script>
40
+ tailwind.config = {
41
+ theme: {
42
+ extend: {
43
+ colors: {
44
+ 'claude-orange': '#D97757',
45
+ 'claude-orange-hover': '#C4684A',
46
+ 'claude-cream': '#FAF6F1',
47
+ 'claude-dark': '#1A1915',
48
+ },
49
+ fontFamily: {
50
+ sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'],
51
+ },
52
+ },
53
+ },
54
+ }
55
+ </script>
56
+
57
+ <!-- Inter Font -->
58
+ <link rel="preconnect" href="https://fonts.googleapis.com">
59
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
60
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
61
+
62
+ <style>
63
+ html { scroll-behavior: smooth; scrollbar-width: thin; scrollbar-color: #D9775740 transparent; }
64
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
65
+ ::-webkit-scrollbar-track { background: transparent; }
66
+ ::-webkit-scrollbar-thumb { background: #D9775740; border-radius: 3px; }
67
+ ::-webkit-scrollbar-thumb:hover { background: #D97757; }
68
+ </style>
69
+ </head>
70
+ <body class="bg-claude-cream text-claude-dark font-sans antialiased">
@@ -0,0 +1,140 @@
1
+ <!-- Navigation -->
2
+ <nav class="sticky top-0 z-50 bg-white/70 backdrop-blur-md border-b border-gray-200/50 shadow-sm">
3
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
4
+ <div class="flex items-center justify-between h-16">
5
+
6
+ <!-- Logo -->
7
+ <a href="/" class="flex items-center space-x-2 group">
8
+ <svg class="w-7 h-7 text-claude-orange" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
9
+ <rect width="28" height="28" rx="6" fill="currentColor" fill-opacity="0.1"/>
10
+ <path d="M9 8l5 6-5 6" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
11
+ <path d="M16 18h4" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/>
12
+ </svg>
13
+ <span class="text-xl font-bold text-claude-dark tracking-tight">LLM <span class="text-claude-orange">API</span></span>
14
+ </a>
15
+
16
+ <!-- Desktop Nav Links -->
17
+ <div class="hidden md:flex items-center space-x-8">
18
+ <a href="/pricing" class="text-sm font-medium text-gray-600 hover:text-claude-orange transition-colors">Pricing</a>
19
+ <a href="/docs" class="text-sm font-medium text-gray-600 hover:text-claude-orange transition-colors">Docs</a>
20
+ <a href="/blog" class="text-sm font-medium text-gray-600 hover:text-claude-orange transition-colors">Blog</a>
21
+ <% if (typeof viewUser !== 'undefined' && viewUser) { %>
22
+ <a href="/dashboard" class="text-sm font-medium text-gray-600 hover:text-claude-orange transition-colors">Dashboard</a>
23
+ <% } %>
24
+ </div>
25
+
26
+ <!-- Desktop Right Section -->
27
+ <div class="hidden md:flex items-center space-x-4">
28
+ <% if (typeof viewUser !== 'undefined' && viewUser) { %>
29
+ <!-- User Dropdown -->
30
+ <div class="relative" id="userDropdown">
31
+ <button
32
+ onclick="toggleDropdown()"
33
+ class="flex items-center space-x-2 px-3 py-2 rounded-lg hover:bg-gray-100 transition-colors"
34
+ >
35
+ <div class="w-8 h-8 rounded-full bg-claude-orange text-white flex items-center justify-center text-sm font-semibold">
36
+ <%= viewUser.name ? viewUser.name.charAt(0).toUpperCase() : 'U' %>
37
+ </div>
38
+ <span class="text-sm font-medium text-claude-dark"><%= viewUser.name || 'User' %></span>
39
+ <svg class="w-4 h-4 text-gray-400 transition-transform" id="dropdownChevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
40
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
41
+ </svg>
42
+ </button>
43
+ <div
44
+ id="dropdownMenu"
45
+ class="hidden absolute right-0 mt-2 w-48 bg-white rounded-xl shadow-lg border border-gray-100 py-1 z-50"
46
+ >
47
+ <a href="/dashboard" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 transition-colors">Dashboard</a>
48
+ <a href="/dashboard/orders" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 transition-colors">Orders</a>
49
+ <a href="/dashboard/settings" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 transition-colors">Settings</a>
50
+ <div class="border-t border-gray-100 my-1"></div>
51
+ <button
52
+ onclick="handleLogout()"
53
+ class="block w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50 transition-colors"
54
+ >Log Out</button>
55
+ </div>
56
+ </div>
57
+ <% } else { %>
58
+ <a href="/login" class="text-sm font-medium text-gray-600 hover:text-claude-orange transition-colors">Log In</a>
59
+ <a href="/register" class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-claude-orange rounded-lg hover:bg-claude-orange-hover transition-all shadow-sm hover:shadow">Get API Key</a>
60
+ <% } %>
61
+ </div>
62
+
63
+ <!-- Mobile Hamburger -->
64
+ <button
65
+ onclick="toggleMobileMenu()"
66
+ class="md:hidden p-2 rounded-lg hover:bg-gray-100 transition-colors"
67
+ aria-label="Toggle menu"
68
+ id="mobileMenuBtn"
69
+ >
70
+ <svg class="w-6 h-6 text-claude-dark" fill="none" stroke="currentColor" viewBox="0 0 24 24">
71
+ <path id="menuIconOpen" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
72
+ <path id="menuIconClose" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
73
+ </svg>
74
+ </button>
75
+ </div>
76
+ </div>
77
+
78
+ <!-- Mobile Menu -->
79
+ <div id="mobileMenu" class="hidden md:hidden border-t border-gray-200/50 bg-white/95 backdrop-blur-md">
80
+ <div class="px-4 py-4 space-y-1">
81
+ <a href="/pricing" class="block px-3 py-2.5 text-sm font-medium text-gray-700 rounded-lg hover:bg-gray-50 transition-colors">Pricing</a>
82
+ <a href="/docs" class="block px-3 py-2.5 text-sm font-medium text-gray-700 rounded-lg hover:bg-gray-50 transition-colors">Docs</a>
83
+ <a href="/blog" class="block px-3 py-2.5 text-sm font-medium text-gray-700 rounded-lg hover:bg-gray-50 transition-colors">Blog</a>
84
+ <% if (typeof viewUser !== 'undefined' && viewUser) { %>
85
+ <div class="border-t border-gray-100 my-2"></div>
86
+ <a href="/dashboard" class="block px-3 py-2.5 text-sm font-medium text-gray-700 rounded-lg hover:bg-gray-50 transition-colors">Dashboard</a>
87
+ <a href="/dashboard/orders" class="block px-3 py-2.5 text-sm font-medium text-gray-700 rounded-lg hover:bg-gray-50 transition-colors">Orders</a>
88
+ <a href="/dashboard/settings" class="block px-3 py-2.5 text-sm font-medium text-gray-700 rounded-lg hover:bg-gray-50 transition-colors">Settings</a>
89
+ <div class="border-t border-gray-100 my-2"></div>
90
+ <button onclick="handleLogout()" class="block w-full text-left px-3 py-2.5 text-sm font-medium text-red-600 rounded-lg hover:bg-red-50 transition-colors">Log Out</button>
91
+ <% } else { %>
92
+ <div class="border-t border-gray-100 my-2"></div>
93
+ <a href="/login" class="block px-3 py-2.5 text-sm font-medium text-gray-700 rounded-lg hover:bg-gray-50 transition-colors">Log In</a>
94
+ <a href="/register" class="block px-3 py-2.5 text-sm font-medium text-white bg-claude-orange rounded-lg text-center hover:bg-claude-orange-hover transition-colors">Get API Key</a>
95
+ <% } %>
96
+ </div>
97
+ </div>
98
+ </nav>
99
+
100
+ <script>
101
+ function toggleDropdown() {
102
+ const menu = document.getElementById('dropdownMenu');
103
+ const chevron = document.getElementById('dropdownChevron');
104
+ menu.classList.toggle('hidden');
105
+ chevron.classList.toggle('rotate-180');
106
+ }
107
+
108
+ function toggleMobileMenu() {
109
+ const menu = document.getElementById('mobileMenu');
110
+ const iconOpen = document.getElementById('menuIconOpen');
111
+ const iconClose = document.getElementById('menuIconClose');
112
+ menu.classList.toggle('hidden');
113
+ iconOpen.classList.toggle('hidden');
114
+ iconClose.classList.toggle('hidden');
115
+ }
116
+
117
+ // Close dropdown on outside click
118
+ document.addEventListener('click', function(e) {
119
+ const dropdown = document.getElementById('userDropdown');
120
+ const menu = document.getElementById('dropdownMenu');
121
+ const chevron = document.getElementById('dropdownChevron');
122
+ if (dropdown && menu && !dropdown.contains(e.target)) {
123
+ menu.classList.add('hidden');
124
+ if (chevron) chevron.classList.remove('rotate-180');
125
+ }
126
+ });
127
+
128
+ async function handleLogout() {
129
+ try {
130
+ const res = await fetch('/api/auth/logout', { method: 'POST', credentials: 'same-origin' });
131
+ if (res.ok) {
132
+ window.location.href = '/';
133
+ } else {
134
+ alert('Logout failed. Please try again.');
135
+ }
136
+ } catch (err) {
137
+ alert('Network error. Please try again.');
138
+ }
139
+ }
140
+ </script>