fix: 新增代理
This commit is contained in:
@@ -544,32 +544,36 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="proxy">代理 <span>*</span></label>
|
||||
<label for="proxy">代理(可选,不填则由后端自动读取)</label>
|
||||
<input
|
||||
id="proxy"
|
||||
placeholder="socks5://ip:port 或 socks5://user:pass@ip:port(必填,香港服务器不上代理必封号)"
|
||||
placeholder="socks5://ip:port 或 socks5://user:pass@ip:port"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<div class="small-label" style="margin-top: 4px;">socks5 代理,必填。服务器在香港,不上代理必封号。</div>
|
||||
<button type="button" class="secondary" id="btn-check-proxy" style="margin-top: 6px; padding: 4px 10px; font-size: 12px;">检测代理是否正常</button>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>登录方式</label>
|
||||
<div class="small-label" style="padding: 8px 0;">取码用 iPad;仅当「无数字」需滑块时用一次「重新取码(Mac)」</div>
|
||||
<label for="device">登录设备类型</label>
|
||||
<select id="device">
|
||||
<option value="">自动</option>
|
||||
<option value="ipad">iPad</option>
|
||||
<option value="mac">Mac</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="primary" id="btn-qrcode">
|
||||
1. 获取登录二维码
|
||||
获取登录二维码
|
||||
</button>
|
||||
<button class="secondary" id="btn-qrcode-mac" title="仅当需滑块且手机无数字验证码时使用,取码后手机显示 Mac,再去滑块">
|
||||
<button class="secondary" id="btn-qrcode-mac" title="使用 Mac 取码(与上方选择 Mac 后点获取二维码等效)">
|
||||
重新取码(Mac)
|
||||
</button>
|
||||
<button class="secondary" id="btn-wake">
|
||||
唤醒
|
||||
</button>
|
||||
<button class="secondary" id="btn-check-scan" title="state=2 为登录成功;若手机显示 Mac 登录,多点击几次会自动切 iPad">
|
||||
2. 检测扫码状态
|
||||
<button class="secondary" id="btn-check-scan">
|
||||
检测扫码状态
|
||||
</button>
|
||||
<button class="secondary" id="btn-online">
|
||||
获取在线状态
|
||||
@@ -623,7 +627,6 @@
|
||||
<div id="slider-area" style="display: none;">
|
||||
<div class="card" style="max-width: 480px;">
|
||||
<div class="card-title">滑块验证(无数字时:先点「重新取码(Mac)」,手机停确认页再滑)</div>
|
||||
<div class="small-label" style="margin-bottom: 8px;">Key 为 7765 服务方 QQ(408449830),与账号 key 无关。Data62=取码返回,Ticket=检测扫码状态返回(无乱码)。</div>
|
||||
<div id="slider-app" data-v-app="">
|
||||
<div class="params-section" style="margin-bottom: 12px;">
|
||||
<label class="form-label" for="keyInput">Key (7765 服务方):</label>
|
||||
@@ -659,6 +662,7 @@
|
||||
sliderParams: null,
|
||||
sliderScriptLoaded: false,
|
||||
sliderListenersBound: false,
|
||||
qrcodeFetchedAt: 0,
|
||||
};
|
||||
|
||||
function parseSliderUrlParams(sliderUrl) {
|
||||
@@ -758,21 +762,18 @@
|
||||
$('btn-logout').disabled = loading;
|
||||
}
|
||||
|
||||
function getCommonPayload(requireProxy) {
|
||||
function getCommonPayload() {
|
||||
const key = $('key').value.trim();
|
||||
const proxy = $('proxy').value.trim();
|
||||
const device = ($('device') && $('device').value) || '';
|
||||
if (!key) {
|
||||
alert('请先填写账号唯一标识(key)');
|
||||
return null;
|
||||
}
|
||||
if (requireProxy && !proxy) {
|
||||
alert('请先填写 socks5 代理。服务器在香港,不上代理必封号。');
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
key,
|
||||
proxy: proxy || undefined,
|
||||
ipadOrMac: 'ipad',
|
||||
ipadOrMac: device || 'ipad',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1025,15 +1026,37 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function onCheckProxy() {
|
||||
var proxyInput = ($('proxy') && $('proxy').value || '').trim();
|
||||
var url = '/api/check-proxy';
|
||||
if (proxyInput) url += '?proxy=' + encodeURIComponent(proxyInput);
|
||||
log('正在检测代理…' + (proxyInput ? '(使用当前填写的代理)' : '(使用环境变量或 KDL)'));
|
||||
var btn = $('btn-check-proxy');
|
||||
if (btn) btn.disabled = true;
|
||||
try {
|
||||
var data = await callApi(url);
|
||||
if (data && data.ok) {
|
||||
log('代理正常。来源: ' + (data.source || '') + (data.proxy_preview ? ',代理: ' + data.proxy_preview : ''), 'ok');
|
||||
} else {
|
||||
log('代理不可用: ' + (data && data.error ? data.error : JSON.stringify(data)), 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
log('检测代理失败: ' + (e.message || e), 'error');
|
||||
} finally {
|
||||
if (btn) btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function onGetQrCode(forceMac) {
|
||||
const payload = getCommonPayload(true);
|
||||
const payload = getCommonPayload();
|
||||
if (!payload) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
log(forceMac ? '重新取码(Mac),用于无数字时走滑块…' : '请求登录二维码(iPad)...');
|
||||
var deviceLabel = forceMac ? 'Mac' : (payload.ipadOrMac || 'ipad');
|
||||
log(forceMac ? '重新取码(Mac)…' : '请求登录二维码(' + deviceLabel + ')...');
|
||||
const body = {
|
||||
Proxy: payload.proxy || '',
|
||||
IpadOrmac: forceMac ? 'mac' : 'ipad',
|
||||
IpadOrmac: forceMac ? 'mac' : (payload.ipadOrMac || 'ipad'),
|
||||
Check: false,
|
||||
force_mac: !!forceMac,
|
||||
};
|
||||
@@ -1055,16 +1078,19 @@
|
||||
log(line2);
|
||||
if (line3) log(line3);
|
||||
})();
|
||||
state.qrcodeFetchedAt = Date.now();
|
||||
renderQrFromResponse(data);
|
||||
updateLoginState(forceMac ? '已用 Mac 重新取码,请手机停在确认页并完成下方滑块' : '等待扫码 / 确认中', 'pending', forceMac ? '滑块 Key=408449830,Data62 已更新' : '请在 60 秒内使用微信扫码。');
|
||||
updateLoginState(forceMac ? '已用 Mac 重新取码,请手机停在确认页并完成下方滑块' : '等待扫码 / 确认中', 'pending', forceMac ? 'Data62 已更新,需滑块时请点「打开滑块验证」' : '请在 60 秒内使用微信扫码。');
|
||||
if (forceMac && data) {
|
||||
var newData62 = (data.Data && data.Data.Data62) || data.Data62 || data.data62 || '';
|
||||
if (newData62) {
|
||||
if (!state.sliderParams) state.sliderParams = {};
|
||||
state.sliderParams.data62 = newData62;
|
||||
state.sliderParams.key = state.sliderParams.key || '408449830';
|
||||
showSliderAreaAndFill(state.sliderParams);
|
||||
log('已用新 Data62 更新滑块区域,请完成滑块后到手机点确认。', 'warn');
|
||||
state.sliderOpened = true;
|
||||
var reopenWrap = $('slider-reopen-wrap');
|
||||
if (reopenWrap) reopenWrap.style.display = 'block';
|
||||
log('Mac 取码成功,Data62 已更新。上方显示二维码/返回内容;需滑块时请点击「打开滑块验证」。', 'warn');
|
||||
}
|
||||
}
|
||||
if (state.pollingScan) clearInterval(state.pollingScan);
|
||||
@@ -1095,14 +1121,17 @@
|
||||
const d = obj.Data && typeof obj.Data === 'object' ? obj.Data : obj;
|
||||
const stateVal = d.state ?? d.State;
|
||||
|
||||
// 后端返回滑块 path,在当前页右侧替换为滑块区域并自动填充参数
|
||||
// 后端返回滑块 path,在当前页右侧替换为滑块区域并自动填充参数(取码后 15 秒内不自动切,避免未出码就进验证)
|
||||
const sliderUrl = data.slider_url;
|
||||
if (sliderUrl && typeof sliderUrl === 'string') {
|
||||
var allowSliderSwitch = !state.qrcodeFetchedAt || (Date.now() - state.qrcodeFetchedAt) > 15000;
|
||||
if (sliderUrl && typeof sliderUrl === 'string' && allowSliderSwitch) {
|
||||
state.sliderOpened = true;
|
||||
log('需完成滑块验证,已切换到滑块验证区域。', 'warn');
|
||||
const params = parseSliderUrlParams(sliderUrl);
|
||||
if (params) state.sliderParams = params;
|
||||
showSliderAreaAndFill(params);
|
||||
} else if (sliderUrl && !allowSliderSwitch) {
|
||||
log('检测到需验证,但刚取码不久,暂不自动切到滑块,请先扫码或稍后再检测。', 'warn');
|
||||
}
|
||||
|
||||
// 检测状态是否成功登录:state==2 或 Success+已登录 等,成功后自动跳转管理页
|
||||
@@ -1134,11 +1163,14 @@
|
||||
} else {
|
||||
updateLoginState(needVerify || sliderUrl ? '请完成滑块验证' : '正在确认登录状态…', 'pending',
|
||||
needVerify || sliderUrl ? '扫码完成,请完成验证后登录' : '');
|
||||
// 判定到需验证时直接切换验证模块,不等用户点「检测扫码状态」;有 slider_url 已在上方填参,无则先切过去,后续轮询会带参
|
||||
// 判定到需验证时切换验证模块;取码后 15 秒内不自动切,避免未出码就进验证
|
||||
if (needVerify || sliderUrl) {
|
||||
state.sliderOpened = true;
|
||||
if (!sliderUrl) log('需完成滑块验证,已切换到滑块验证区域,等待参数…', 'warn');
|
||||
showSliderAreaAndFill(state.sliderParams || {});
|
||||
var allowSwitch = !state.qrcodeFetchedAt || (Date.now() - state.qrcodeFetchedAt) > 15000;
|
||||
if (allowSwitch) {
|
||||
state.sliderOpened = true;
|
||||
if (!sliderUrl) log('需完成滑块验证,已切换到滑块验证区域,等待参数…', 'warn');
|
||||
showSliderAreaAndFill(state.sliderParams || {});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1214,6 +1246,10 @@
|
||||
e.preventDefault();
|
||||
onWake();
|
||||
});
|
||||
$('btn-check-proxy') && $('btn-check-proxy').addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
onCheckProxy();
|
||||
});
|
||||
$('btn-show-slider') && $('btn-show-slider').addEventListener('click', function() {
|
||||
if (state.sliderParams) showSliderAreaAndFill(state.sliderParams);
|
||||
});
|
||||
|
||||
@@ -146,11 +146,11 @@
|
||||
<div class="field full">
|
||||
<label>客户标签(从客户档案中选)</label>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:8px;align-items:center;">
|
||||
<select id="g-tag-select" style="max-width:160px"><option value="">请先登录后加载</option></select>
|
||||
<select id="g-tag-select" style="max-width:200px"><option value="">选择标签…</option></select>
|
||||
<button type="button" class="secondary" id="g-tag-add" style="padding:4px 10px;font-size:12px">添加标签</button>
|
||||
<span id="g-tags-chips" class="tags-chips"></span>
|
||||
</div>
|
||||
<p class="small-label" style="margin-top:4px">在下方「客户档案」中维护的标签会出现在下拉列表;可多选,发送时仅推送给带这些标签的客户。</p>
|
||||
<p class="small-label" style="margin-top:4px">在下方「客户档案」中维护的标签会出现在下拉列表;可多选,发送时仅推送给带这些标签的客户。支持获取联系人列表。</p>
|
||||
</div>
|
||||
<div class="field full"><label>问候语模板(可用 {{name}})</label><textarea id="g-template" rows="2" placeholder="早安,{{name}}!今日上新…"></textarea></div>
|
||||
<div class="field"><label> </label><label><input type="checkbox" id="g-use-qwen" /> 使用千问生成个性化问候</label></div>
|
||||
@@ -165,9 +165,10 @@
|
||||
<label>选择接收人(从好友/客户列表)</label>
|
||||
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
|
||||
<button type="button" class="secondary" id="btn-load-friends" style="padding:4px 10px;font-size:12px">加载联系人</button>
|
||||
<button type="button" class="secondary" id="btn-load-customers-mass" style="padding:4px 10px;font-size:12px" title="从客户档案加载,与联系人合并去重">加载客户</button>
|
||||
<span id="mass-selected-count" class="small-label">已选 0 人</span>
|
||||
</div>
|
||||
<div id="mass-friend-list" style="max-height:140px;overflow-y:auto;border:1px solid var(--border);border-radius:8px;padding:8px;margin-top:6px;background:rgba(15,23,42,0.6)"></div>
|
||||
<div id="mass-friend-list" style="max-height:200px;overflow-y:auto;border:1px solid var(--border);border-radius:8px;padding:8px;margin-top:6px;background:rgba(15,23,42,0.6)"></div>
|
||||
</div>
|
||||
<div class="field full"><label>群发文案</label><textarea id="mass-content" rows="2" placeholder="输入要群发的文字…"></textarea></div>
|
||||
<button type="button" class="primary" id="btn-mass-send">一键群发</button>
|
||||
@@ -630,6 +631,28 @@
|
||||
}
|
||||
|
||||
let massSelectedWxids = [];
|
||||
/** 快速群发可选列表(好友+客户合并去重):{ wxid, name, source?: 'friend'|'customer' } */
|
||||
let massContactList = [];
|
||||
|
||||
function renderMassContactList() {
|
||||
const el = $('mass-friend-list');
|
||||
if (!el) return;
|
||||
if (!massContactList.length) {
|
||||
el.innerHTML = '<span class="small-label">请点击「加载联系人」或「加载客户」获取列表。</span>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = massContactList.map(f => {
|
||||
const wxid = (f.wxid || '').toString();
|
||||
const name = (f.name || wxid).toString();
|
||||
const checked = massSelectedWxids.includes(wxid) ? ' checked' : '';
|
||||
const badge = f.source === 'customer' ? ' <span class="small-label" style="color:var(--muted)">[客户]</span>' : '';
|
||||
return '<label style="display:block;margin:4px 0"><input type="checkbox" class="mass-friend-cb" data-wxid="' + escapeHtml(wxid) + '"' + checked + ' /> ' + escapeHtml(name) + badge + ' <span class="small-label">(' + escapeHtml(wxid.slice(0, 16)) + (wxid.length > 16 ? '…' : '') + ')</span></label>';
|
||||
}).join('');
|
||||
el.querySelectorAll('.mass-friend-cb').forEach(cb => {
|
||||
cb.addEventListener('change', updateMassSelected);
|
||||
});
|
||||
updateMassSelected();
|
||||
}
|
||||
|
||||
async function loadFriendsForMass() {
|
||||
const key = $('key').value.trim();
|
||||
@@ -637,27 +660,55 @@
|
||||
const el = $('mass-friend-list');
|
||||
el.innerHTML = '<span class="small-label">加载中…</span>';
|
||||
try {
|
||||
const data = await callApi('/api/friends?key=' + encodeURIComponent(key), { cache: 'no-store' });
|
||||
const data = await callApi('/api/contact-list?key=' + encodeURIComponent(key) + '&refresh=1', { cache: 'no-store' });
|
||||
const list = data.items || [];
|
||||
if (!list.length) {
|
||||
el.innerHTML = '<span class="small-label">暂无联系人,请先在「客户档案」添加客户。</span>';
|
||||
massContactList = list.map(f => {
|
||||
const wxid = (f.wxid || f.Wxid || f.UserName || '').toString();
|
||||
const name = (f.remark_name || f.RemarkName || f.nick_name || f.NickName || wxid).toString();
|
||||
return { wxid, name, source: 'friend' };
|
||||
}).filter(f => f.wxid);
|
||||
if (!massContactList.length) {
|
||||
el.innerHTML = '<span class="small-label">暂无联系人。可点击「加载客户」从客户档案选择。</span>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = list.map(f => {
|
||||
const wxid = (f.wxid || f.Wxid || f.UserName || '').toString();
|
||||
const name = (f.remark_name || f.RemarkName || f.NickName || wxid).toString();
|
||||
return '<label style="display:block;margin:4px 0"><input type="checkbox" class="mass-friend-cb" data-wxid="' + escapeHtml(wxid) + '" /> ' + escapeHtml(name) + ' <span class="small-label">(' + escapeHtml(wxid.slice(0, 16)) + '…)</span></label>';
|
||||
}).join('');
|
||||
el.querySelectorAll('.mass-friend-cb').forEach(cb => {
|
||||
cb.addEventListener('change', updateMassSelected);
|
||||
});
|
||||
massSelectedWxids = [];
|
||||
updateMassSelected();
|
||||
renderMassContactList();
|
||||
} catch (e) {
|
||||
el.innerHTML = '<span class="small-label">加载失败: ' + escapeHtml(e.message) + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCustomersForMass() {
|
||||
const key = $('key').value.trim();
|
||||
if (!key) { alert('请先登录'); return; }
|
||||
const el = $('mass-friend-list');
|
||||
const wasEmpty = !massContactList.length;
|
||||
if (wasEmpty) el.innerHTML = '<span class="small-label">加载中…</span>';
|
||||
try {
|
||||
const data = await callApi('/api/customers?key=' + encodeURIComponent(key));
|
||||
const list = data.items || [];
|
||||
const existingWxids = new Set(massContactList.map(f => f.wxid));
|
||||
list.forEach(c => {
|
||||
const wxid = (c.wxid || '').toString();
|
||||
if (!wxid) return;
|
||||
if (existingWxids.has(wxid)) return;
|
||||
existingWxids.add(wxid);
|
||||
massContactList.push({
|
||||
wxid,
|
||||
name: (c.remark_name || c.wxid || wxid).toString(),
|
||||
source: 'customer'
|
||||
});
|
||||
});
|
||||
if (!massContactList.length) {
|
||||
el.innerHTML = '<span class="small-label">暂无客户。请先在「客户档案」添加客户,或点击「加载联系人」获取好友列表。</span>';
|
||||
return;
|
||||
}
|
||||
renderMassContactList();
|
||||
} catch (e) {
|
||||
if (wasEmpty) el.innerHTML = '<span class="small-label">加载失败: ' + escapeHtml(e.message) + '</span>';
|
||||
else alert('加载客户失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function updateMassSelected() {
|
||||
const list = $('mass-friend-list');
|
||||
if (!list) return;
|
||||
@@ -832,6 +883,7 @@
|
||||
if ($('g-time')) { $('g-time').addEventListener('change', onGreetingTimeChange); $('g-time').addEventListener('input', onGreetingTimeChange); }
|
||||
})();
|
||||
$('btn-load-friends').addEventListener('click', loadFriendsForMass);
|
||||
$('btn-load-customers-mass') && $('btn-load-customers-mass').addEventListener('click', loadCustomersForMass);
|
||||
$('btn-mass-send').addEventListener('click', doMassSend);
|
||||
$('btn-send-image').addEventListener('click', doSendImage);
|
||||
if ($('img-file') && $('img-file-name')) {
|
||||
|
||||
Reference in New Issue
Block a user