@fudrouter/fsrouter 0.6.80 → 0.6.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/open-sse/providers/registry/nous-research.js +1 -0
- package/open-sse/providers/registry/nous-research.js +1 -0
- package/package.json +1 -1
- package/src/automation/cf_token_via_session.py +158 -0
- package/src/automation/cloudflare_signup.py +3689 -0
- package/src/automation/codebuddy_signup.py +636 -0
- package/src/automation/flashloop_signup.py +338 -0
- package/src/automation/kimi_signup.py +401 -0
- package/src/automation/leonardo_signup.py +902 -0
- package/src/automation/openvecta_signup.py +652 -0
- package/src/automation/qoder_signup.py +323 -0
- package/src/automation/test_proxy.py +86 -0
- package/src/automation/weavy_signup.py +1964 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Standalone script to automate Kimi Coding auto-login via Google/GSuite OAuth.
|
|
3
|
+
Outputs step logs and final results to stdout as JSON lines.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
import json
|
|
8
|
+
import argparse
|
|
9
|
+
import time
|
|
10
|
+
import logging
|
|
11
|
+
import re
|
|
12
|
+
import urllib.request
|
|
13
|
+
import urllib.parse
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Dict, Optional
|
|
16
|
+
|
|
17
|
+
# Setup logger
|
|
18
|
+
logging.basicConfig(level=logging.WARNING)
|
|
19
|
+
logger = logging.getLogger("kimi_signup")
|
|
20
|
+
|
|
21
|
+
class KimiAutomationError(RuntimeError):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
def log_step(msg, *args):
|
|
25
|
+
txt = msg % args if args else msg
|
|
26
|
+
sys.stdout.write(json.dumps({"step": txt}, ensure_ascii=False) + "\n")
|
|
27
|
+
sys.stdout.flush()
|
|
28
|
+
|
|
29
|
+
def safe_email_to_dirname(email: str) -> str:
|
|
30
|
+
cleaned = (email or "").strip().lower()
|
|
31
|
+
cleaned = cleaned.replace("@", "_at_")
|
|
32
|
+
cleaned = re.sub(r"[^a-z0-9._-]+", "_", cleaned)
|
|
33
|
+
cleaned = cleaned.strip("._-")
|
|
34
|
+
return cleaned or "account"
|
|
35
|
+
|
|
36
|
+
def request_device_code() -> dict:
|
|
37
|
+
url = "https://auth.kimi.com/api/oauth/device_authorization"
|
|
38
|
+
headers = {
|
|
39
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
40
|
+
"Accept": "application/json"
|
|
41
|
+
}
|
|
42
|
+
data = urllib.parse.urlencode({"client_id": "17e5f671-d194-4dfb-9706-5516cb48c098"}).encode("utf-8")
|
|
43
|
+
req = urllib.request.Request(url, data=data, headers=headers, method="POST")
|
|
44
|
+
try:
|
|
45
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
46
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
47
|
+
except Exception as e:
|
|
48
|
+
raise KimiAutomationError(f"Gagal mengambil device code Kimi: {e}")
|
|
49
|
+
|
|
50
|
+
def click_first(page, selectors, timeout_ms: int = 8000) -> bool:
|
|
51
|
+
deadline = time.time() + (timeout_ms / 1000.0)
|
|
52
|
+
while time.time() < deadline:
|
|
53
|
+
for sel in selectors:
|
|
54
|
+
try:
|
|
55
|
+
loc = page.locator(sel).first
|
|
56
|
+
if loc.count() > 0 and loc.is_visible(timeout=300):
|
|
57
|
+
try:
|
|
58
|
+
loc.scroll_into_view_if_needed(timeout=600)
|
|
59
|
+
loc.click(timeout=2000, force=True)
|
|
60
|
+
return True
|
|
61
|
+
except Exception:
|
|
62
|
+
handle = loc.element_handle(timeout=300)
|
|
63
|
+
if handle:
|
|
64
|
+
page.evaluate("(el) => el.click()", handle)
|
|
65
|
+
return True
|
|
66
|
+
except Exception:
|
|
67
|
+
continue
|
|
68
|
+
time.sleep(0.3)
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
def fill_first(page, selectors, value: str, timeout_ms: int = 8000) -> bool:
|
|
72
|
+
deadline = time.time() + (timeout_ms / 1000.0)
|
|
73
|
+
while time.time() < deadline:
|
|
74
|
+
for sel in selectors:
|
|
75
|
+
try:
|
|
76
|
+
loc = page.locator(sel).first
|
|
77
|
+
if loc.count() > 0 and loc.is_visible(timeout=300):
|
|
78
|
+
try:
|
|
79
|
+
loc.fill("", timeout=500)
|
|
80
|
+
except Exception:
|
|
81
|
+
pass
|
|
82
|
+
loc.fill(value, timeout=2000)
|
|
83
|
+
return True
|
|
84
|
+
except Exception:
|
|
85
|
+
continue
|
|
86
|
+
time.sleep(0.3)
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
def poll_token(device_code: str, timeout: int = 120, interval: int = 5) -> dict:
|
|
90
|
+
url = "https://auth.kimi.com/api/oauth/token"
|
|
91
|
+
headers = {
|
|
92
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
93
|
+
"Accept": "application/json"
|
|
94
|
+
}
|
|
95
|
+
payload = {
|
|
96
|
+
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
|
|
97
|
+
"client_id": "17e5f671-d194-4dfb-9706-5516cb48c098",
|
|
98
|
+
"device_code": device_code
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
deadline = time.time() + timeout
|
|
102
|
+
while time.time() < deadline:
|
|
103
|
+
data = urllib.parse.urlencode(payload).encode("utf-8")
|
|
104
|
+
req = urllib.request.Request(url, data=data, headers=headers, method="POST")
|
|
105
|
+
try:
|
|
106
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
107
|
+
res_data = json.loads(resp.read().decode("utf-8"))
|
|
108
|
+
if "access_token" in res_data:
|
|
109
|
+
return res_data
|
|
110
|
+
except urllib.error.HTTPError as he:
|
|
111
|
+
try:
|
|
112
|
+
err_data = json.loads(he.read().decode("utf-8"))
|
|
113
|
+
err = err_data.get("error")
|
|
114
|
+
if err == "authorization_pending":
|
|
115
|
+
pass
|
|
116
|
+
else:
|
|
117
|
+
log_step(f"Polling error Kimi: {err_data.get('error_description') or err}")
|
|
118
|
+
if err in ("expired_token", "access_denied"):
|
|
119
|
+
break
|
|
120
|
+
except Exception:
|
|
121
|
+
pass
|
|
122
|
+
except Exception as e:
|
|
123
|
+
log_step(f"Polling network error Kimi: {e}")
|
|
124
|
+
|
|
125
|
+
time.sleep(interval)
|
|
126
|
+
return {}
|
|
127
|
+
|
|
128
|
+
def main():
|
|
129
|
+
parser = argparse.ArgumentParser(description="Kimi Coding auto-login tool")
|
|
130
|
+
parser.add_argument("--email", required=True)
|
|
131
|
+
parser.add_argument("--password", required=True)
|
|
132
|
+
parser.add_argument("--proxy-server")
|
|
133
|
+
parser.add_argument("--proxy-user")
|
|
134
|
+
parser.add_argument("--proxy-pass")
|
|
135
|
+
parser.add_argument("--profiles-dir", required=True)
|
|
136
|
+
parser.add_argument("--headless", action="store_true", default=False)
|
|
137
|
+
args = parser.parse_args()
|
|
138
|
+
|
|
139
|
+
# Step 1: Request Device Authorization Code
|
|
140
|
+
log_step("Mengambil device code dari Kimi...")
|
|
141
|
+
try:
|
|
142
|
+
device_data = request_device_code()
|
|
143
|
+
except Exception as e:
|
|
144
|
+
sys.stdout.write(json.dumps({"status": "error", "message": str(e)}) + "\n")
|
|
145
|
+
sys.exit(1)
|
|
146
|
+
|
|
147
|
+
device_code = device_data.get("device_code")
|
|
148
|
+
user_code = device_data.get("user_code")
|
|
149
|
+
verification_uri = device_data.get("verification_uri") or "https://www.kimi.com/code/authorize_device"
|
|
150
|
+
complete_url = f"{verification_uri}?user_code={user_code}"
|
|
151
|
+
|
|
152
|
+
log_step(f"Device code: {user_code}. Menghubungkan URL verifikasi...")
|
|
153
|
+
|
|
154
|
+
# Step 2: Set up profile dir and launch Camoufox
|
|
155
|
+
profiles_root = Path(args.profiles_dir)
|
|
156
|
+
profiles_root.mkdir(parents=True, exist_ok=True)
|
|
157
|
+
profile_dir = profiles_root / safe_email_to_dirname(args.email)
|
|
158
|
+
profile_dir.mkdir(parents=True, exist_ok=True)
|
|
159
|
+
|
|
160
|
+
proxy_dict = None
|
|
161
|
+
if args.proxy_server:
|
|
162
|
+
proxy_dict = {"server": args.proxy_server}
|
|
163
|
+
if args.proxy_user:
|
|
164
|
+
proxy_dict["username"] = args.proxy_user
|
|
165
|
+
if args.proxy_pass:
|
|
166
|
+
proxy_dict["password"] = args.proxy_pass
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
from camoufox.sync_api import Camoufox
|
|
170
|
+
except ImportError:
|
|
171
|
+
sys.stdout.write(json.dumps({"status": "error", "message": "Camoufox tidak terinstall di environment python."}) + "\n")
|
|
172
|
+
sys.exit(1)
|
|
173
|
+
|
|
174
|
+
kwargs = dict(
|
|
175
|
+
headless=args.headless,
|
|
176
|
+
persistent_context=True, no_viewport=True,
|
|
177
|
+
user_data_dir=str(profile_dir),
|
|
178
|
+
humanize=True,
|
|
179
|
+
geoip=True,
|
|
180
|
+
locale="en-US",
|
|
181
|
+
os=("windows", "macos", "linux"),
|
|
182
|
+
window=(1280, 800),
|
|
183
|
+
firefox_user_prefs={
|
|
184
|
+
"network.trr.mode": 5,
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
if proxy_dict:
|
|
188
|
+
kwargs["proxy"] = {k: v for k, v in proxy_dict.items() if v}
|
|
189
|
+
|
|
190
|
+
log_step("Meluncurkan browser...")
|
|
191
|
+
try:
|
|
192
|
+
with Camoufox(**kwargs) as browser:
|
|
193
|
+
context = getattr(browser, "context", None) or browser
|
|
194
|
+
page = context.new_page()
|
|
195
|
+
|
|
196
|
+
log_step("Membuka halaman verifikasi Kimi...")
|
|
197
|
+
page.goto(complete_url, wait_until="domcontentloaded", timeout=45000)
|
|
198
|
+
time.sleep(3.0)
|
|
199
|
+
|
|
200
|
+
# Wait for either the login button or the authorize button to appear
|
|
201
|
+
log_step("Menunggu halaman verifikasi termuat...")
|
|
202
|
+
try:
|
|
203
|
+
page.wait_for_selector(".google-login-btn, button:has-text('Confirm'), button:has-text('Authorize'), button:has-text('Allow'), button:has-text('确认授权'), button:has-text('确认'), button:has-text('授权'), button.kimi-button", timeout=15000)
|
|
204
|
+
except Exception:
|
|
205
|
+
pass
|
|
206
|
+
|
|
207
|
+
google_btn = page.locator(".google-login-btn").first
|
|
208
|
+
is_login_required = False
|
|
209
|
+
if google_btn.count() > 0 and google_btn.is_visible(timeout=500):
|
|
210
|
+
is_login_required = True
|
|
211
|
+
|
|
212
|
+
if is_login_required:
|
|
213
|
+
log_step("Menemukan tombol login Google. Mengklik tombol...")
|
|
214
|
+
|
|
215
|
+
with page.expect_popup() as popup_info:
|
|
216
|
+
google_btn.click(timeout=5000)
|
|
217
|
+
popup = popup_info.value
|
|
218
|
+
popup.wait_for_load_state()
|
|
219
|
+
time.sleep(2.0)
|
|
220
|
+
|
|
221
|
+
log_step("Menangani Google OAuth popup...")
|
|
222
|
+
google_deadline = time.time() + 60.0
|
|
223
|
+
filled_email = False
|
|
224
|
+
filled_pass = False
|
|
225
|
+
clicked_account = False
|
|
226
|
+
|
|
227
|
+
while time.time() < google_deadline:
|
|
228
|
+
if popup.is_closed():
|
|
229
|
+
log_step("Popup Google ditutup.")
|
|
230
|
+
break
|
|
231
|
+
try:
|
|
232
|
+
popup_url = popup.url or ""
|
|
233
|
+
popup_title = popup.title() or ""
|
|
234
|
+
log_step(f"Popup URL: {popup_url}, Title: {popup_title}")
|
|
235
|
+
|
|
236
|
+
# Check if Google account is disabled / speedbumped
|
|
237
|
+
if "speedbump/disabled" in popup_url or "disabled" in popup_url or "disabled" in popup_title.lower():
|
|
238
|
+
raise KimiAutomationError(f"Akun Google ({args.email}) dinonaktifkan/disabled oleh Google.")
|
|
239
|
+
|
|
240
|
+
# Account chooser
|
|
241
|
+
if not clicked_account and (
|
|
242
|
+
"accountchooser" in popup_url or
|
|
243
|
+
"Choose an account" in (popup.title() or "")
|
|
244
|
+
):
|
|
245
|
+
clicked = popup.evaluate(f"""
|
|
246
|
+
() => {{
|
|
247
|
+
const email = '{args.email}';
|
|
248
|
+
const lis = document.querySelectorAll('li[data-identifier], li.JDAKTe');
|
|
249
|
+
for (const li of lis) {{
|
|
250
|
+
const id = li.getAttribute('data-identifier') || '';
|
|
251
|
+
if (id === email || li.textContent.includes(email)) {{
|
|
252
|
+
li.click(); return true;
|
|
253
|
+
}}
|
|
254
|
+
}}
|
|
255
|
+
const all = document.querySelectorAll('[data-email], [data-identifier]');
|
|
256
|
+
for (const el of all) {{
|
|
257
|
+
const v = el.getAttribute('data-email') || el.getAttribute('data-identifier') || '';
|
|
258
|
+
if (v === args.email) {{ el.click(); return true; }}
|
|
259
|
+
}}
|
|
260
|
+
const rows = document.querySelectorAll('ul li');
|
|
261
|
+
if (rows.length > 0) {{ rows[0].click(); return true; }}
|
|
262
|
+
return false;
|
|
263
|
+
}}
|
|
264
|
+
""")
|
|
265
|
+
if clicked:
|
|
266
|
+
log_step(f"Memilih akun '{args.email}' di Google chooser...")
|
|
267
|
+
clicked_account = True
|
|
268
|
+
time.sleep(3.0)
|
|
269
|
+
continue
|
|
270
|
+
|
|
271
|
+
# Email input
|
|
272
|
+
if not filled_email:
|
|
273
|
+
email_in = popup.locator("input[type='email'], input[name='identifier']").first
|
|
274
|
+
if email_in.count() > 0 and email_in.is_visible(timeout=500):
|
|
275
|
+
log_step("Mengisi email GSuite di popup Google...")
|
|
276
|
+
email_in.fill(args.email)
|
|
277
|
+
popup.keyboard.press("Enter")
|
|
278
|
+
filled_email = True
|
|
279
|
+
time.sleep(3.0)
|
|
280
|
+
continue
|
|
281
|
+
|
|
282
|
+
# Password input
|
|
283
|
+
if filled_email and not filled_pass:
|
|
284
|
+
pass_in = popup.locator("input[type='password'], input[name='Passwd']").first
|
|
285
|
+
if pass_in.count() > 0 and pass_in.is_visible(timeout=500):
|
|
286
|
+
log_step("Mengisi password GSuite di popup Google...")
|
|
287
|
+
pass_in.fill(args.password)
|
|
288
|
+
popup.keyboard.press("Enter")
|
|
289
|
+
filled_pass = True
|
|
290
|
+
time.sleep(3.0)
|
|
291
|
+
continue
|
|
292
|
+
|
|
293
|
+
# Workspace terms confirmation
|
|
294
|
+
understand_btn = popup.locator(
|
|
295
|
+
"button:has-text('I understand'), "
|
|
296
|
+
"button:has-text('I Understand')"
|
|
297
|
+
).first
|
|
298
|
+
if understand_btn.count() > 0 and understand_btn.is_visible(timeout=500):
|
|
299
|
+
log_step("Klik 'I understand' di Google terms...")
|
|
300
|
+
understand_btn.click(timeout=3000)
|
|
301
|
+
time.sleep(2.0)
|
|
302
|
+
continue
|
|
303
|
+
|
|
304
|
+
# General continue button in Google Popup
|
|
305
|
+
for btn_text in ["Continue", "Allow", "Next", "Accept", "Confirm"]:
|
|
306
|
+
try:
|
|
307
|
+
b = popup.locator(f"button:has-text('{btn_text}')").first
|
|
308
|
+
if b.count() > 0 and b.is_visible(timeout=300):
|
|
309
|
+
log_step(f"Klik '{btn_text}' di popup Google...")
|
|
310
|
+
b.click(timeout=3000)
|
|
311
|
+
time.sleep(2.0)
|
|
312
|
+
break
|
|
313
|
+
except Exception:
|
|
314
|
+
pass
|
|
315
|
+
|
|
316
|
+
except KimiAutomationError:
|
|
317
|
+
raise
|
|
318
|
+
except Exception as e_popup:
|
|
319
|
+
logger.debug(f"Google popup handler error: {e_popup}")
|
|
320
|
+
time.sleep(1.0)
|
|
321
|
+
|
|
322
|
+
# Google popup final state screenshot
|
|
323
|
+
try:
|
|
324
|
+
popup_screenshot = str(profile_dir / "google_popup_final.png")
|
|
325
|
+
popup.screenshot(path=popup_screenshot)
|
|
326
|
+
log_step(f"Google popup final screenshot saved: {popup_screenshot}")
|
|
327
|
+
except Exception:
|
|
328
|
+
pass
|
|
329
|
+
|
|
330
|
+
# Wait for main page to reload/render after login
|
|
331
|
+
time.sleep(5.0)
|
|
332
|
+
|
|
333
|
+
# Accept terms check box if present on Kimi
|
|
334
|
+
try:
|
|
335
|
+
page.evaluate("""() => {
|
|
336
|
+
const checkboxes = document.querySelectorAll("input[type='checkbox']");
|
|
337
|
+
for (const cb of checkboxes) {
|
|
338
|
+
cb.checked = true;
|
|
339
|
+
cb.dispatchEvent(new Event('change', { bubbles: true }));
|
|
340
|
+
cb.dispatchEvent(new Event('click', { bubbles: true }));
|
|
341
|
+
}
|
|
342
|
+
}""")
|
|
343
|
+
except Exception:
|
|
344
|
+
pass
|
|
345
|
+
|
|
346
|
+
# Click Authorize/Confirm button on Kimi page
|
|
347
|
+
log_step("Mengklik tombol konfirmasi/otorisasi Kimi...")
|
|
348
|
+
auth_selectors = [
|
|
349
|
+
"button:has-text('Confirm')",
|
|
350
|
+
"button:has-text('Authorize')",
|
|
351
|
+
"button:has-text('Allow')",
|
|
352
|
+
"button:has-text('确认授权')",
|
|
353
|
+
"button:has-text('确认')",
|
|
354
|
+
"button:has-text('授权')",
|
|
355
|
+
"button.kimi-button"
|
|
356
|
+
]
|
|
357
|
+
clicked_auth = click_first(page, auth_selectors, timeout_ms=10000)
|
|
358
|
+
if clicked_auth:
|
|
359
|
+
log_step("Tombol otorisasi diklik, menunggu proses...")
|
|
360
|
+
else:
|
|
361
|
+
log_step("Tombol otorisasi tidak ditemukan, kemungkinan sudah terotorisasi otomatis.")
|
|
362
|
+
|
|
363
|
+
# Kimi main page final state screenshot
|
|
364
|
+
try:
|
|
365
|
+
screenshot_path = str(profile_dir / "final_state.png")
|
|
366
|
+
page.screenshot(path=screenshot_path)
|
|
367
|
+
log_step(f"Screenshot disimpan ke: {screenshot_path}")
|
|
368
|
+
except Exception:
|
|
369
|
+
pass
|
|
370
|
+
|
|
371
|
+
time.sleep(5.0)
|
|
372
|
+
|
|
373
|
+
except Exception as e:
|
|
374
|
+
try:
|
|
375
|
+
err_screenshot = str(profile_dir / "error_state.png")
|
|
376
|
+
page.screenshot(path=err_screenshot)
|
|
377
|
+
log_step(f"Error screenshot disimpan ke: {err_screenshot}")
|
|
378
|
+
except Exception:
|
|
379
|
+
pass
|
|
380
|
+
sys.stdout.write(json.dumps({"status": "error", "message": f"Browser Error: {e}"}) + "\n")
|
|
381
|
+
sys.exit(1)
|
|
382
|
+
|
|
383
|
+
# Step 3: Poll for token
|
|
384
|
+
log_step("Menunggu Kimi menerbitkan token...")
|
|
385
|
+
token_data = poll_token(device_code, timeout=60, interval=5)
|
|
386
|
+
|
|
387
|
+
if not token_data or "access_token" not in token_data:
|
|
388
|
+
sys.stdout.write(json.dumps({"status": "error", "message": "Timeout menunggu otorisasi token Kimi."}) + "\n")
|
|
389
|
+
sys.exit(1)
|
|
390
|
+
|
|
391
|
+
# Successful output
|
|
392
|
+
sys.stdout.write(json.dumps({
|
|
393
|
+
"status": "success",
|
|
394
|
+
"api_key": token_data.get("access_token"),
|
|
395
|
+
"refresh_token": token_data.get("refresh_token"),
|
|
396
|
+
"expires_in": token_data.get("expires_in") or 86400
|
|
397
|
+
}) + "\n")
|
|
398
|
+
sys.stdout.flush()
|
|
399
|
+
|
|
400
|
+
if __name__ == "__main__":
|
|
401
|
+
main()
|