Skip to content

Commit e993972

Browse files
committed
feat(#33): 批量代理+账号导入 + update.sh 自动更新 LS + 请求级日志 ID
#33 批量导入: - POST /dashboard/api/batch-import 接受每行 "[proxy] email password" - 自动登录 + 绑定代理到对应账号 + 启动 LS 实例 - Dashboard 批量导入已更新 placeholder 和提示 update.sh: - 新增 [2/5] 自动检查 LS binary 大小变化并从 GitHub Release 下载 - 用户 update.sh 就能自动获取最新 LS 不用手动 调试日志: - 每个请求分配 6 位 reqId (Chat[abc123]) - reuse HIT/MISS 带 reqId 方便用户贴日志定位问题
1 parent 45934b4 commit e993972

4 files changed

Lines changed: 96 additions & 24 deletions

File tree

‎src/dashboard/api.js‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,59 @@ export async function handleDashboardApi(method, subpath, body, req, res) {
544544
}
545545
}
546546

547+
// ─── Batch proxy + account import ─────────────────────
548+
// POST /batch-import — each line: "proxy email password" or "email password"
549+
if (subpath === '/batch-import' && method === 'POST') {
550+
try {
551+
const { text, autoAdd = true } = body || {};
552+
if (!text || typeof text !== 'string') return json(res, 400, { error: '缺少 text 字段' });
553+
const lines = text.split('\n').map(l => l.trim()).filter(Boolean);
554+
if (!lines.length) return json(res, 400, { error: '没有有效行' });
555+
556+
const results = [];
557+
for (const line of lines) {
558+
const parts = line.split(/\s+/);
559+
let proxy = null, email, password;
560+
if (parts.length >= 3 && (parts[0].includes('://') || parts[0].includes(':'))) {
561+
proxy = parts[0];
562+
email = parts[1];
563+
password = parts[2];
564+
} else if (parts.length >= 2) {
565+
email = parts[0];
566+
password = parts[1];
567+
} else {
568+
results.push({ success: false, email: line.slice(0, 30), error: '格式错误 需要 [proxy] email password' });
569+
continue;
570+
}
571+
try {
572+
const loginProxy = proxy ? { host: proxy } : getProxyConfig().global;
573+
const result = await processWindsurfLogin({ email, password, loginProxy, autoAdd });
574+
if (result.success && proxy && result.accountId) {
575+
const proxyParts = proxy.match(/^(?:(\w+):\/\/)?(?:([^:]+):([^@]+)@)?([^:]+):(\d+)$/);
576+
if (proxyParts) {
577+
setAccountProxy(result.accountId, {
578+
type: proxyParts[1] || 'http',
579+
host: proxyParts[4],
580+
port: parseInt(proxyParts[5]),
581+
username: proxyParts[2] || '',
582+
password: proxyParts[3] || '',
583+
});
584+
result.proxy = proxy;
585+
ensureLsForAccount(result.accountId).catch(() => {});
586+
}
587+
}
588+
results.push(result);
589+
} catch (err) {
590+
results.push({ success: false, email, error: err.message });
591+
}
592+
}
593+
const successCount = results.filter(r => r.success).length;
594+
return json(res, 200, { success: true, total: results.length, successCount, failCount: results.length - successCount, results });
595+
} catch (err) {
596+
return json(res, 400, { error: err.message });
597+
}
598+
}
599+
547600
// ─── OAuth login (Google / GitHub via Firebase) ────────
548601
// POST /oauth-login — accepts Firebase idToken from client-side OAuth
549602
if (subpath === '/oauth-login' && method === 'POST') {

‎src/dashboard/index.html‎

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1079,9 +1079,9 @@ <h1 class="page-title" data-i18n="page.windsurf-login">登录取号</h1>
10791079
id="wl-batch-input"
10801080
class="textarea"
10811081
rows="6"
1082-
placeholder="buulzqkz0iee@2116666.xyz zy$&X5f5CT8k&#10;ejjdthoqi34l@2116666.xyz zy$&X5f5CT8k&#10;lij3th36mtki@2116666.xyz zy$&X5f5CT8k"
1082+
placeholder="user1@mail.com pass123&#10;http://proxy:8080 user2@mail.com pass456&#10;socks5://user:pass@1.2.3.4:1080 user3@mail.com pass789"
10831083
></textarea>
1084-
<div class="field-hint">每行一个账号,格式:邮箱 + 空格/Tab + 密码。会自动忽略空行。</div>
1084+
<div class="field-hint">每行一个账号。格式:<code>[代理] 邮箱 密码</code>。代理可选,支持 http/socks5。会自动绑定代理到对应账号。</div>
10851085
<div class="field-row-actions">
10861086
<button class="btn btn-outline" onclick="App.pasteWindsurfBatch()">
10871087
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><rect x="8" y="2" width="8" height="4" rx="1" ry="1"/></svg>
@@ -2214,25 +2214,23 @@ <h3>控制台登录</h3>
22142214

22152215
async windsurfLoginBatch() {
22162216
const input = document.getElementById('wl-batch-input').value;
2217-
const { entries, errors } = this.parseWindsurfBatchInput(input);
2218-
if (!entries.length) return this.toast('请先粘贴批量账号', 'error');
2219-
if (errors.length) return this.toast(errors.slice(0, 3).join(';'), 'error');
2217+
if (!input.trim()) return this.toast('请先粘贴批量账号', 'error');
22202218

2221-
const proxy = this.getWindsurfLoginProxy();
22222219
const autoAdd = document.getElementById('wl-auto-add').checked;
22232220
const btn = document.getElementById('wl-batch-btn');
2221+
const lineCount = input.trim().split('\n').filter(l => l.trim()).length;
22242222
btn.disabled = true;
22252223
btn.innerHTML = '<span class="spinner"></span> 导入中...';
22262224
this.showWindsurfLoginResult(`
22272225
<div class="section-header">
22282226
<div>
22292227
<div class="section-title">批量导入中...</div>
2230-
<div class="section-desc">共 ${entries.length} 个账号,请稍候</div>
2228+
<div class="section-desc">共 ${lineCount} 行,请稍候</div>
22312229
</div>
22322230
</div>`);
22332231

22342232
try {
2235-
const r = await this.api('POST', '/windsurf-login/batch', { accounts: entries, proxy, autoAdd });
2233+
const r = await this.api('POST', '/batch-import', { text: input, autoAdd });
22362234
if (!r.success || !Array.isArray(r.results)) {
22372235
this.showWindsurfLoginResult(`
22382236
<div class="section-header">

‎src/handlers/chat.js‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ async function waitForAccount(tried, signal, maxWaitMs = QUEUE_MAX_WAIT_MS, mode
136136
}
137137

138138
export async function handleChatCompletions(body) {
139+
const reqId = Math.random().toString(36).slice(2, 8);
139140
const {
140141
model: reqModel,
141142
stream = false,
@@ -260,7 +261,7 @@ export async function handleChatCompletions(body) {
260261
const reuseEnabled = useCascade && isExperimentalEnabled('cascadeConversationReuse');
261262
const fpBefore = reuseEnabled ? fingerprintBefore(messages, modelKey) : null;
262263
let reuseEntry = reuseEnabled ? poolCheckout(fpBefore) : null;
263-
if (reuseEntry) log.info(`Chat: cascade reuse HIT cascadeId=${reuseEntry.cascadeId.slice(0, 8)}… model=${displayModel}`);
264+
if (reuseEntry) log.info(`Chat[${reqId}]: reuse HIT cascade=${reuseEntry.cascadeId.slice(0, 8)} model=${displayModel}`);
264265

265266
// Non-stream: retry with a different account on model-not-available errors
266267
const tried = [];
@@ -284,7 +285,7 @@ export async function handleChatCompletions(body) {
284285
acct = acquireAccountByKey(reuseEntry.apiKey, modelKey);
285286
}
286287
if (!acct) {
287-
log.info('Chat: cascade reuse skipped — owning account not available after 5s wait');
288+
log.info(`Chat[${reqId}]: reuse MISS — owning account not available after 5s wait`);
288289
reuseEntry = null;
289290
}
290291
}
@@ -318,14 +319,14 @@ export async function handleChatCompletions(body) {
318319
// Cascade pins cascade_id to a specific LS port too; if the LS it was
319320
// born on has been replaced, the cascade_id is dead.
320321
if (reuseEntry && reuseEntry.lsPort !== ls.port) {
321-
log.info('Chat: cascade reuse skipped — LS port changed');
322+
log.info(`Chat[${reqId}]: reuse MISS — LS port changed`);
322323
reuseEntry = null;
323324
}
324325
const _msgChars = (messages || []).reduce((n, m) => {
325326
const c = m?.content;
326327
return n + (typeof c === 'string' ? c.length : Array.isArray(c) ? c.reduce((k, p) => k + (typeof p?.text === 'string' ? p.text.length : 0), 0) : 0);
327328
}, 0);
328-
log.info(`Chat: model=${displayModel} flow=${useCascade ? 'cascade' : 'legacy'} attempt=${attempt + 1} account=${acct.email} ls=${ls.port} turns=${(messages||[]).length} chars=${_msgChars}${reuseEntry ? ' reuse=1' : ''}${emulateTools ? ' tools=emu' : ''}`);
329+
log.info(`Chat[${reqId}]: model=${displayModel} flow=${useCascade ? 'cascade' : 'legacy'} attempt=${attempt + 1} account=${acct.email} ls=${ls.port} turns=${(messages||[]).length} chars=${_msgChars}${reuseEntry ? ' reuse=1' : ''}${emulateTools ? ' tools=emu' : ''}`);
329330
const client = new WindsurfClient(acct.apiKey, ls.port, ls.csrfToken);
330331
const result = await nonStreamResponse(
331332
client, chatId, created, displayModel, modelKey, messages, cascadeMessages, modelEnum, modelUid,
@@ -700,7 +701,7 @@ function streamResponse(id, created, model, modelKey, messages, cascadeMessages,
700701
acct = acquireAccountByKey(reuseEntry.apiKey, modelKey);
701702
}
702703
if (!acct) {
703-
log.info('Chat: cascade reuse skipped — owning account not available after 5s wait');
704+
log.info(`Chat[${reqId}]: reuse MISS — owning account not available after 5s wait`);
704705
reuseEntry = null;
705706
}
706707
}
@@ -731,7 +732,7 @@ function streamResponse(id, created, model, modelKey, messages, cascadeMessages,
731732
const ls = getLsFor(acct.proxy);
732733
if (!ls) { lastErr = new Error('No LS instance available'); break; }
733734
if (reuseEntry && reuseEntry.lsPort !== ls.port) {
734-
log.info('Chat: cascade reuse skipped — LS port changed');
735+
log.info(`Chat[${reqId}]: reuse MISS — LS port changed`);
735736
reuseEntry = null;
736737
}
737738
const _msgCharsStream = (messages || []).reduce((n, m) => {

‎update.sh‎

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
#!/usr/bin/env bash
2-
# update.sh — one-click update: pull latest + restart PM2
2+
# update.sh — one-click update: pull latest + update LS binary + restart PM2
33
set -e
44

55
cd "$(dirname "$0")"
66

77
PORT="${PORT:-3003}"
88
NAME="${PM2_NAME:-windsurf-api}"
99

10-
echo "=== [1/4] Pull latest ==="
10+
echo "=== [1/5] Pull latest ==="
1111
git fetch --quiet origin
1212
BEFORE=$(git rev-parse HEAD)
1313
REMOTE=$(git rev-parse origin/master)
1414

15-
# Prefer fast-forward; fall back to hard reset if history was rewritten.
16-
# .env / accounts.json / logs / runtime-config.json are all gitignored,
17-
# so reset --hard only discards tracked-file local edits (which the update
18-
# workflow assumes there are none of).
1915
if ! git pull --ff-only --quiet 2>/dev/null; then
2016
echo " ! remote history rewritten — hard-resetting to origin/master"
2117
git reset --hard "$REMOTE"
@@ -30,25 +26,49 @@ else
3026
fi
3127

3228
echo ""
33-
echo "=== [2/4] Stop service ==="
29+
echo "=== [2/5] Update LS binary ==="
30+
LS_PATH="${LS_BINARY_PATH:-/opt/windsurf/language_server_linux_x64}"
31+
if [ -f .env ]; then
32+
_lp="$(grep -oP '(?<=LS_BINARY_PATH=).+' .env 2>/dev/null | head -1)"
33+
[ -n "$_lp" ] && LS_PATH="$_lp"
34+
fi
35+
RELEASE_URL="https://github.com/dwgx/WindsurfAPI/releases/latest/download/language_server_linux_x64"
36+
if [ -f "$LS_PATH" ]; then
37+
LOCAL_SIZE=$(stat --format=%s "$LS_PATH" 2>/dev/null || stat -f%z "$LS_PATH" 2>/dev/null || echo 0)
38+
REMOTE_SIZE=$(curl -sI -L "$RELEASE_URL" 2>/dev/null | grep -i content-length | tail -1 | tr -dc '0-9')
39+
if [ -n "$REMOTE_SIZE" ] && [ "$REMOTE_SIZE" -gt 0 ] && [ "$LOCAL_SIZE" != "$REMOTE_SIZE" ]; then
40+
echo " LS binary size changed ($LOCAL_SIZE → $REMOTE_SIZE), downloading..."
41+
curl -fL --progress-bar -o "$LS_PATH.tmp" "$RELEASE_URL" && mv -f "$LS_PATH.tmp" "$LS_PATH" && chmod +x "$LS_PATH"
42+
echo " LS binary updated"
43+
else
44+
echo " LS binary up to date (${LOCAL_SIZE} bytes)"
45+
fi
46+
else
47+
echo " LS binary not found, downloading..."
48+
mkdir -p "$(dirname "$LS_PATH")"
49+
curl -fL --progress-bar -o "$LS_PATH" "$RELEASE_URL" && chmod +x "$LS_PATH"
50+
echo " LS binary installed"
51+
fi
52+
53+
echo ""
54+
echo "=== [3/5] Stop service ==="
3455
pm2 stop "$NAME" >/dev/null 2>&1 || true
3556
pm2 delete "$NAME" >/dev/null 2>&1 || true
3657
fuser -k "$PORT"/tcp >/dev/null 2>&1 || true
3758
pkill -f "node.*WindsurfAPI/src/index.js" >/dev/null 2>&1 || true
3859

39-
# Wait for port to actually free up (max 30s)
4060
for i in $(seq 1 30); do
4161
if ! ss -ltn 2>/dev/null | grep -q ":$PORT "; then break; fi
4262
sleep 1
4363
done
4464

4565
echo ""
46-
echo "=== [3/4] Start service ==="
66+
echo "=== [4/5] Start service ==="
4767
pm2 start src/index.js --name "$NAME" --cwd "$(pwd)"
4868
pm2 save >/dev/null 2>&1 || true
4969

5070
echo ""
51-
echo "=== [4/4] Health check ==="
71+
echo "=== [5/5] Health check ==="
5272
sleep 3
5373
if curl -sf "http://localhost:$PORT/health" | head -200; then
5474
echo ""

0 commit comments

Comments
 (0)