copilot-router 1.0.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 (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +241 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +32 -0
  5. package/dist/lib/api-config.d.ts +15 -0
  6. package/dist/lib/api-config.js +30 -0
  7. package/dist/lib/database.d.ts +60 -0
  8. package/dist/lib/database.js +228 -0
  9. package/dist/lib/error.d.ts +11 -0
  10. package/dist/lib/error.js +34 -0
  11. package/dist/lib/state.d.ts +9 -0
  12. package/dist/lib/state.js +3 -0
  13. package/dist/lib/token-manager.d.ts +95 -0
  14. package/dist/lib/token-manager.js +241 -0
  15. package/dist/lib/utils.d.ts +8 -0
  16. package/dist/lib/utils.js +10 -0
  17. package/dist/main.d.ts +1 -0
  18. package/dist/main.js +97 -0
  19. package/dist/routes/anthropic/routes.d.ts +2 -0
  20. package/dist/routes/anthropic/routes.js +155 -0
  21. package/dist/routes/anthropic/stream-translation.d.ts +3 -0
  22. package/dist/routes/anthropic/stream-translation.js +136 -0
  23. package/dist/routes/anthropic/translation.d.ts +4 -0
  24. package/dist/routes/anthropic/translation.js +241 -0
  25. package/dist/routes/anthropic/types.d.ts +165 -0
  26. package/dist/routes/anthropic/types.js +2 -0
  27. package/dist/routes/anthropic/utils.d.ts +2 -0
  28. package/dist/routes/anthropic/utils.js +12 -0
  29. package/dist/routes/auth/routes.d.ts +2 -0
  30. package/dist/routes/auth/routes.js +158 -0
  31. package/dist/routes/gemini/routes.d.ts +2 -0
  32. package/dist/routes/gemini/routes.js +163 -0
  33. package/dist/routes/gemini/translation.d.ts +5 -0
  34. package/dist/routes/gemini/translation.js +215 -0
  35. package/dist/routes/gemini/types.d.ts +63 -0
  36. package/dist/routes/gemini/types.js +2 -0
  37. package/dist/routes/openai/routes.d.ts +2 -0
  38. package/dist/routes/openai/routes.js +215 -0
  39. package/dist/routes/utility/routes.d.ts +2 -0
  40. package/dist/routes/utility/routes.js +28 -0
  41. package/dist/services/copilot/create-chat-completions.d.ts +130 -0
  42. package/dist/services/copilot/create-chat-completions.js +32 -0
  43. package/dist/services/copilot/create-embeddings.d.ts +20 -0
  44. package/dist/services/copilot/create-embeddings.js +19 -0
  45. package/dist/services/copilot/get-models.d.ts +51 -0
  46. package/dist/services/copilot/get-models.js +45 -0
  47. package/dist/services/github/get-device-code.d.ts +11 -0
  48. package/dist/services/github/get-device-code.js +21 -0
  49. package/dist/services/github/get-user.d.ts +11 -0
  50. package/dist/services/github/get-user.js +17 -0
  51. package/dist/services/github/poll-access-token.d.ts +13 -0
  52. package/dist/services/github/poll-access-token.js +56 -0
  53. package/package.json +56 -0
  54. package/public/index.html +419 -0
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "copilot-router",
3
+ "version": "1.0.0",
4
+ "description": "GitHub Copilot API with OpenAI, Anthropic, and Gemini compatibility",
5
+ "type": "module",
6
+ "main": "./dist/main.js",
7
+ "bin": {
8
+ "copilot-router": "./dist/cli.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "public",
13
+ "package.json",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "dev": "tsx watch src/main.ts",
18
+ "start": "tsx src/main.ts",
19
+ "build": "tsc || true; tsc-alias --resolve-full-paths && npm run build:postprocess",
20
+ "build:postprocess": "node -e \"const fs = require('fs'); let cli = fs.readFileSync('./dist/cli.js', 'utf8'); if (!cli.startsWith('#!')) { cli = '#!/usr/bin/env node\\n' + cli; fs.writeFileSync('./dist/cli.js', cli); }\" && chmod +x dist/cli.js",
21
+ "prepublishOnly": "npm run build"
22
+ },
23
+ "keywords": [
24
+ "github",
25
+ "copilot",
26
+ "openai",
27
+ "anthropic",
28
+ "gemini",
29
+ "api",
30
+ "proxy",
31
+ "router"
32
+ ],
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/ShengM97/copilot-router.git"
36
+ },
37
+ "license": "MIT",
38
+ "dependencies": {
39
+ "@hono/node-server": "^1.13.7",
40
+ "@hono/zod-openapi": "^0.18.4",
41
+ "commander": "^12.1.0",
42
+ "consola": "^3.4.0",
43
+ "dotenv": "^17.2.3",
44
+ "fetch-event-stream": "^0.1.5",
45
+ "hono": "^4.6.16",
46
+ "mssql": "^11.0.1",
47
+ "yaml": "^2.7.0"
48
+ },
49
+ "devDependencies": {
50
+ "@types/mssql": "^9.1.8",
51
+ "@types/node": "^22.10.5",
52
+ "tsc-alias": "^1.8.10",
53
+ "tsx": "^4.19.2",
54
+ "typescript": "^5.7.2"
55
+ }
56
+ }
@@ -0,0 +1,419 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Copilot Router - 登录</title>
7
+ <style>
8
+ * { box-sizing: border-box; margin: 0; padding: 0; }
9
+ body {
10
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
11
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
12
+ min-height: 100vh;
13
+ display: flex;
14
+ align-items: center;
15
+ justify-content: center;
16
+ padding: 20px;
17
+ }
18
+ .container {
19
+ background: white;
20
+ border-radius: 16px;
21
+ padding: 40px;
22
+ max-width: 500px;
23
+ width: 100%;
24
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
25
+ }
26
+ h1 {
27
+ color: #333;
28
+ margin-bottom: 10px;
29
+ font-size: 28px;
30
+ }
31
+ .subtitle {
32
+ color: #666;
33
+ margin-bottom: 30px;
34
+ }
35
+ .step {
36
+ display: none;
37
+ animation: fadeIn 0.3s ease;
38
+ }
39
+ .step.active { display: block; }
40
+ @keyframes fadeIn {
41
+ from { opacity: 0; transform: translateY(10px); }
42
+ to { opacity: 1; transform: translateY(0); }
43
+ }
44
+ button {
45
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
46
+ color: white;
47
+ border: none;
48
+ padding: 14px 28px;
49
+ border-radius: 8px;
50
+ font-size: 16px;
51
+ cursor: pointer;
52
+ width: 100%;
53
+ transition: transform 0.2s, box-shadow 0.2s;
54
+ }
55
+ button:hover {
56
+ transform: translateY(-2px);
57
+ box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
58
+ }
59
+ button:disabled {
60
+ background: #ccc;
61
+ cursor: not-allowed;
62
+ transform: none;
63
+ box-shadow: none;
64
+ }
65
+ .code-box {
66
+ background: #f5f5f5;
67
+ border: 2px dashed #667eea;
68
+ border-radius: 12px;
69
+ padding: 30px;
70
+ text-align: center;
71
+ margin: 20px 0;
72
+ }
73
+ .user-code {
74
+ font-size: 36px;
75
+ font-weight: bold;
76
+ color: #667eea;
77
+ letter-spacing: 4px;
78
+ font-family: monospace;
79
+ }
80
+ .verify-link {
81
+ display: inline-block;
82
+ margin-top: 15px;
83
+ color: #764ba2;
84
+ text-decoration: none;
85
+ font-weight: 500;
86
+ }
87
+ .verify-link:hover { text-decoration: underline; }
88
+ .status {
89
+ padding: 15px;
90
+ border-radius: 8px;
91
+ margin: 20px 0;
92
+ text-align: center;
93
+ }
94
+ .status.waiting {
95
+ background: #fff3cd;
96
+ color: #856404;
97
+ }
98
+ .status.success {
99
+ background: #d4edda;
100
+ color: #155724;
101
+ }
102
+ .status.error {
103
+ background: #f8d7da;
104
+ color: #721c24;
105
+ }
106
+ .spinner {
107
+ display: inline-block;
108
+ width: 20px;
109
+ height: 20px;
110
+ border: 3px solid #f3f3f3;
111
+ border-top: 3px solid #667eea;
112
+ border-radius: 50%;
113
+ animation: spin 1s linear infinite;
114
+ margin-right: 10px;
115
+ vertical-align: middle;
116
+ }
117
+ @keyframes spin {
118
+ 0% { transform: rotate(0deg); }
119
+ 100% { transform: rotate(360deg); }
120
+ }
121
+ .token-list {
122
+ margin-top: 20px;
123
+ }
124
+ .token-item {
125
+ background: #f8f9fa;
126
+ padding: 15px;
127
+ border-radius: 8px;
128
+ margin-bottom: 10px;
129
+ display: flex;
130
+ justify-content: space-between;
131
+ align-items: center;
132
+ }
133
+ .token-info strong {
134
+ color: #333;
135
+ }
136
+ .token-info small {
137
+ color: #666;
138
+ }
139
+ .btn-delete {
140
+ background: #dc3545;
141
+ padding: 8px 16px;
142
+ font-size: 14px;
143
+ width: auto;
144
+ }
145
+ .tabs {
146
+ display: flex;
147
+ margin-bottom: 20px;
148
+ border-bottom: 2px solid #eee;
149
+ }
150
+ .tab {
151
+ padding: 10px 20px;
152
+ cursor: pointer;
153
+ border-bottom: 2px solid transparent;
154
+ margin-bottom: -2px;
155
+ color: #666;
156
+ }
157
+ .tab.active {
158
+ border-bottom-color: #667eea;
159
+ color: #667eea;
160
+ font-weight: 500;
161
+ }
162
+ input[type="text"] {
163
+ width: 100%;
164
+ padding: 12px;
165
+ border: 2px solid #ddd;
166
+ border-radius: 8px;
167
+ font-size: 14px;
168
+ margin-bottom: 15px;
169
+ }
170
+ input[type="text"]:focus {
171
+ border-color: #667eea;
172
+ outline: none;
173
+ }
174
+ </style>
175
+ </head>
176
+ <body>
177
+ <div class="container">
178
+ <h1>🚀 Copilot Router</h1>
179
+ <p class="subtitle">GitHub Copilot API 代理服务</p>
180
+
181
+ <div class="tabs">
182
+ <div class="tab active" onclick="showTab('login')">设备码登录</div>
183
+ <div class="tab" onclick="showTab('direct')">直接添加 Token</div>
184
+ <div class="tab" onclick="showTab('tokens')">管理 Tokens</div>
185
+ </div>
186
+
187
+ <!-- 设备码登录 -->
188
+ <div id="tab-login">
189
+ <div id="step1" class="step active">
190
+ <p style="margin-bottom: 20px;">使用 GitHub 设备码流程登录,支持添加多个账号实现负载均衡。</p>
191
+ <button onclick="startLogin()">开始登录</button>
192
+ </div>
193
+
194
+ <div id="step2" class="step">
195
+ <div class="code-box">
196
+ <p>请在 GitHub 输入此验证码:</p>
197
+ <div class="user-code" id="userCode">----</div>
198
+ <a href="#" id="verifyLink" class="verify-link" target="_blank">
199
+ 点击这里打开 GitHub 验证页面 →
200
+ </a>
201
+ </div>
202
+ <div class="status waiting">
203
+ <span class="spinner"></span>
204
+ 等待授权中... 请在 GitHub 完成验证
205
+ </div>
206
+ </div>
207
+
208
+ <div id="step3" class="step">
209
+ <div class="status success">
210
+ ✅ 登录成功!
211
+ </div>
212
+ <p id="successMessage" style="text-align: center; margin: 20px 0;"></p>
213
+ <button onclick="resetLogin()">添加另一个账号</button>
214
+ </div>
215
+
216
+ <div id="stepError" class="step">
217
+ <div class="status error">
218
+ ❌ <span id="errorMessage">登录失败</span>
219
+ </div>
220
+ <button onclick="resetLogin()" style="margin-top: 15px;">重试</button>
221
+ </div>
222
+ </div>
223
+
224
+ <!-- 直接添加 Token -->
225
+ <div id="tab-direct" style="display: none;">
226
+ <p style="margin-bottom: 20px;">如果你已经有 GitHub Token,可以直接添加:</p>
227
+ <input type="text" id="directToken" placeholder="输入 GitHub Token (ghu_xxx 或 gho_xxx)">
228
+ <button onclick="addTokenDirect()">添加 Token</button>
229
+ <div id="directResult"></div>
230
+ </div>
231
+
232
+ <!-- 管理 Tokens -->
233
+ <div id="tab-tokens" style="display: none;">
234
+ <button onclick="loadTokens()">刷新列表</button>
235
+ <div id="tokenList" class="token-list">
236
+ <p style="color: #666; text-align: center; padding: 20px;">点击刷新加载 Token 列表</p>
237
+ </div>
238
+ </div>
239
+ </div>
240
+
241
+ <script>
242
+ let deviceCode = null;
243
+ let pollInterval = null;
244
+
245
+ function showTab(tab) {
246
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
247
+ document.querySelectorAll('.tabs .tab').forEach((t, i) => {
248
+ if ((tab === 'login' && i === 0) || (tab === 'direct' && i === 1) || (tab === 'tokens' && i === 2)) {
249
+ t.classList.add('active');
250
+ }
251
+ });
252
+ document.getElementById('tab-login').style.display = tab === 'login' ? 'block' : 'none';
253
+ document.getElementById('tab-direct').style.display = tab === 'direct' ? 'block' : 'none';
254
+ document.getElementById('tab-tokens').style.display = tab === 'tokens' ? 'block' : 'none';
255
+
256
+ if (tab === 'tokens') loadTokens();
257
+ }
258
+
259
+ async function startLogin() {
260
+ try {
261
+ const res = await fetch('/auth/login', { method: 'POST' });
262
+ const data = await res.json();
263
+
264
+ if (data.error) {
265
+ showError(data.error.message);
266
+ return;
267
+ }
268
+
269
+ deviceCode = data;
270
+ document.getElementById('userCode').textContent = data.user_code;
271
+ document.getElementById('verifyLink').href = data.verification_uri;
272
+
273
+ showStep('step2');
274
+ startPolling();
275
+ } catch (e) {
276
+ showError('无法连接服务器');
277
+ }
278
+ }
279
+
280
+ function startPolling() {
281
+ pollInterval = setInterval(async () => {
282
+ try {
283
+ const res = await fetch('/auth/complete', {
284
+ method: 'POST',
285
+ headers: { 'Content-Type': 'application/json' },
286
+ body: JSON.stringify({
287
+ device_code: deviceCode.device_code,
288
+ interval: deviceCode.interval,
289
+ expires_in: deviceCode.expires_in
290
+ })
291
+ });
292
+ const data = await res.json();
293
+
294
+ // 处理新的响应格式
295
+ if (data.status === 'pending' || data.status === 'processing' || data.status === 'slow_down') {
296
+ // 还在等待授权或处理中,继续轮询
297
+ return;
298
+ }
299
+
300
+ if (data.status === 'success') {
301
+ clearInterval(pollInterval);
302
+ pollInterval = null;
303
+ document.getElementById('successMessage').textContent =
304
+ `已添加账号: ${data.username || 'Unknown'}`;
305
+ showStep('step3');
306
+ return;
307
+ }
308
+
309
+ if (data.error) {
310
+ clearInterval(pollInterval);
311
+ pollInterval = null;
312
+ showError(data.error.message || '授权失败');
313
+ return;
314
+ }
315
+ } catch (e) {
316
+ // 网络错误,继续轮询
317
+ console.error('Polling error:', e);
318
+ }
319
+ }, (deviceCode.interval + 1) * 1000);
320
+
321
+ // 超时处理
322
+ setTimeout(() => {
323
+ if (pollInterval) {
324
+ clearInterval(pollInterval);
325
+ showError('验证超时,请重试');
326
+ }
327
+ }, deviceCode.expires_in * 1000);
328
+ }
329
+
330
+ async function addTokenDirect() {
331
+ const token = document.getElementById('directToken').value.trim();
332
+ if (!token) {
333
+ document.getElementById('directResult').innerHTML =
334
+ '<div class="status error" style="margin-top:15px">请输入 Token</div>';
335
+ return;
336
+ }
337
+
338
+ try {
339
+ const res = await fetch('/auth/tokens', {
340
+ method: 'POST',
341
+ headers: { 'Content-Type': 'application/json' },
342
+ body: JSON.stringify({ github_token: token })
343
+ });
344
+ const data = await res.json();
345
+
346
+ if (data.error) {
347
+ document.getElementById('directResult').innerHTML =
348
+ `<div class="status error" style="margin-top:15px">${data.error.message}</div>`;
349
+ } else {
350
+ document.getElementById('directResult').innerHTML =
351
+ `<div class="status success" style="margin-top:15px">✅ 添加成功: ${data.username}</div>`;
352
+ document.getElementById('directToken').value = '';
353
+ }
354
+ } catch (e) {
355
+ document.getElementById('directResult').innerHTML =
356
+ '<div class="status error" style="margin-top:15px">添加失败</div>';
357
+ }
358
+ }
359
+
360
+ async function loadTokens() {
361
+ try {
362
+ const res = await fetch('/auth/tokens');
363
+ const data = await res.json();
364
+
365
+ if (!data.tokens || data.tokens.length === 0) {
366
+ document.getElementById('tokenList').innerHTML =
367
+ '<p style="color: #666; text-align: center; padding: 20px;">暂无 Token,请先登录添加</p>';
368
+ return;
369
+ }
370
+
371
+ let html = `<p style="margin-bottom:15px">共 ${data.total} 个 Token,${data.active} 个活跃</p>`;
372
+ for (const t of data.tokens) {
373
+ html += `
374
+ <div class="token-item">
375
+ <div class="token-info">
376
+ <strong>${t.username || 'Unknown'}</strong><br>
377
+ <small>ID: ${t.id} | ${t.is_active ? '✅ 活跃' : '❌ 停用'} | 请求: ${t.request_count}</small>
378
+ </div>
379
+ <button class="btn-delete" onclick="deleteToken(${t.id})">删除</button>
380
+ </div>
381
+ `;
382
+ }
383
+ document.getElementById('tokenList').innerHTML = html;
384
+ } catch (e) {
385
+ document.getElementById('tokenList').innerHTML =
386
+ '<p style="color: red; text-align: center; padding: 20px;">加载失败</p>';
387
+ }
388
+ }
389
+
390
+ async function deleteToken(id) {
391
+ if (!confirm('确定要删除这个 Token 吗?')) return;
392
+
393
+ try {
394
+ await fetch(`/auth/tokens/${id}`, { method: 'DELETE' });
395
+ loadTokens();
396
+ } catch (e) {
397
+ alert('删除失败');
398
+ }
399
+ }
400
+
401
+ function showStep(stepId) {
402
+ document.querySelectorAll('#tab-login .step').forEach(s => s.classList.remove('active'));
403
+ document.getElementById(stepId).classList.add('active');
404
+ }
405
+
406
+ function showError(message) {
407
+ if (pollInterval) clearInterval(pollInterval);
408
+ document.getElementById('errorMessage').textContent = message;
409
+ showStep('stepError');
410
+ }
411
+
412
+ function resetLogin() {
413
+ if (pollInterval) clearInterval(pollInterval);
414
+ deviceCode = null;
415
+ showStep('step1');
416
+ }
417
+ </script>
418
+ </body>
419
+ </html>