mirror of
https://github.com/Gloridust/WechatOnCloud.git
synced 2026-06-16 19:53:53 +08:00
fix(P0): unique persistent machine-id per instance + manual reset
All instances shared the image-baked machine-id (a67bf09f...), so Tencent saw every WechatOnCloud account worldwide as one "device" — a textbook device-farm signal triggering risk control and the forced-logout loop reported across old and new versions. - docker/woc-identity.sh: new /custom-cont-init.d/00-woc-identity hook — generates a unique machine-id on first start, persists it in the data volume (survives restart/upgrade/recreate), writes /etc/machine-id + /var/lib/dbus/machine-id, removes /.dockerenv. Existing instances get a fresh unique id on first upgraded start (volume lacks the file). - regenInstanceMachineId + POST /api/admin/instances/:id/regen-machine-id: roll a brand-new device id and restart, for accounts re-flagged by risk control. Gated on the hook being present (old image → instructs upgrade). - Admin 实例卡片「安全」弹窗新增「重置设备 ID 并重启」。 Verified: two fresh containers get distinct machine-ids; id persists across restart; regen (rm persisted file + restart) yields a new persistent id. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -165,6 +165,26 @@ export async function upgradeInstance(inst: Instance): Promise<void> {
|
||||
await runInstance(inst);
|
||||
}
|
||||
|
||||
// 重置实例的设备 machine-id:删掉持久化的 .woc-machine-id 后重启,由 00-woc-identity 钩子重新生成
|
||||
// 一个全新的唯一值(相当于"换一台新设备")。用于某账号被腾讯风控标记后手动滚新设备身份。
|
||||
// 仅对含身份钩子的新镜像有效;旧镜像(升级前)无钩子,先 throw 提示升级,避免做无用功。
|
||||
export async function regenInstanceMachineId(inst: Instance): Promise<void> {
|
||||
const hasHook = (
|
||||
await execCapture(inst, [
|
||||
'sh',
|
||||
'-c',
|
||||
'test -f /custom-cont-init.d/00-woc-identity && echo yes || echo no',
|
||||
])
|
||||
).trim();
|
||||
if (hasHook !== 'yes') {
|
||||
throw new Error('该实例运行的是旧镜像(无设备身份模块),请先「升级实例」后再重置设备 ID');
|
||||
}
|
||||
// 删除持久化文件;重启时钩子检测到缺失 → 生成新的唯一 machine-id 并写回卷
|
||||
await execCapture(inst, ['sh', '-c', 'rm -f /config/.woc-machine-id']);
|
||||
await stopInstance(inst);
|
||||
await runInstance(inst);
|
||||
}
|
||||
|
||||
// 停止实例容器(保留容器与数据卷,可再启动)。
|
||||
export async function stopInstance(inst: Instance): Promise<void> {
|
||||
try {
|
||||
|
||||
@@ -53,6 +53,7 @@ import {
|
||||
listOrphanContainers,
|
||||
removeContainerById,
|
||||
instanceMemoryMB,
|
||||
regenInstanceMachineId,
|
||||
} from './docker.js';
|
||||
import { createSession, getSession, destroySession, destroyUserSessions } from './sessions.js';
|
||||
import { parseHost, parseAllowedHosts, isAllowedHost } from './host-guard.js';
|
||||
@@ -395,6 +396,21 @@ app.put('/api/admin/instances/:id/mem-limits', async (req, reply) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 重置实例的设备 machine-id(仅管理员):滚一个全新的唯一设备身份并重启实例。
|
||||
// 用于某微信账号被腾讯按"设备风险"标记、登录即被踢时,像"换台新设备"一样恢复。会触发重新扫码登录。
|
||||
app.post('/api/admin/instances/:id/regen-machine-id', async (req, reply) => {
|
||||
if (!requireAdmin(req, reply)) return;
|
||||
const id = (req.params as any).id;
|
||||
const inst = findInstance(id);
|
||||
if (!inst) return reply.code(404).send({ error: '实例不存在' });
|
||||
try {
|
||||
await regenInstanceMachineId(inst);
|
||||
return { ok: true };
|
||||
} catch (e: any) {
|
||||
return reply.code(400).send({ error: e?.message || '重置设备 ID 失败' });
|
||||
}
|
||||
});
|
||||
|
||||
// 删除实例(仅管理员):默认保留数据卷,?purge=1 才永久删聊天记录
|
||||
app.delete('/api/admin/instances/:id', async (req, reply) => {
|
||||
if (!requireAdmin(req, reply)) return;
|
||||
|
||||
@@ -91,6 +91,8 @@ export const api = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name, allowedUserIds, reuseVolume: reuseVolume || undefined }),
|
||||
}),
|
||||
regenMachineId: (id: string) =>
|
||||
req(`/api/admin/instances/${id}/regen-machine-id`, { method: 'POST' }),
|
||||
getInstanceMemLimits: (id: string) =>
|
||||
req<MemLimits>(`/api/admin/instances/${id}/mem-limits`),
|
||||
setInstanceMemLimits: (id: string, soft: number | null | undefined, hard: number | null | undefined) =>
|
||||
|
||||
@@ -534,6 +534,7 @@ function ResetPassword({ user, onClose, onDone }: { user: PanelUser; onClose: ()
|
||||
// hard:超过即强制重启(无视会话,防止 OOM)
|
||||
// 留空 = 使用面板全局默认(来自 env)。
|
||||
function InstanceSecurity({ inst, onClose, onDone }: { inst: InstanceWithStatus; onClose: () => void; onDone: () => void }) {
|
||||
const { toast, confirm } = useUI();
|
||||
const [data, setData] = useState<import('../api').MemLimits | null>(null);
|
||||
// 输入字段:空串 = "使用默认"(→ 提交时映射为 null)
|
||||
const [softStr, setSoftStr] = useState('');
|
||||
@@ -541,6 +542,28 @@ function InstanceSecurity({ inst, onClose, onDone }: { inst: InstanceWithStatus;
|
||||
const [err, setErr] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [regenBusy, setRegenBusy] = useState(false);
|
||||
|
||||
const regenMachineId = async () => {
|
||||
const ok = await confirm({
|
||||
title: '重置该实例的设备 ID?',
|
||||
body: '会生成一个全新的设备标识(machine-id)并重启实例,相当于"换一台新设备"。微信需要重新扫码登录。适用于该账号被微信判定设备风险、登录即被强制退出的情况。',
|
||||
danger: true,
|
||||
confirmText: '重置并重启',
|
||||
});
|
||||
if (!ok) return;
|
||||
setRegenBusy(true);
|
||||
try {
|
||||
await api.regenMachineId(inst.id);
|
||||
toast('已重置设备 ID,实例正在重启,请稍后重新扫码登录', 'ok');
|
||||
onClose();
|
||||
onDone();
|
||||
} catch (e: any) {
|
||||
toast(e.message || '重置失败', 'error');
|
||||
} finally {
|
||||
setRegenBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 首次加载 + 每 5s 刷新 currentMB(运行实例的实时内存)
|
||||
useEffect(() => {
|
||||
@@ -664,6 +687,22 @@ function InstanceSecurity({ inst, onClose, onDone }: { inst: InstanceWithStatus;
|
||||
<div className="muted small" style={{ marginTop: 6 }}>
|
||||
提示:日常活跃内存约 1500 MiB;soft 建议略高于此(如 2000),hard 建议远低于宿主可用内存(如 3000~4000)。
|
||||
</div>
|
||||
|
||||
<div className="field-label" style={{ marginTop: 16 }}>设备身份(machine-id)</div>
|
||||
<div className="muted small" style={{ lineHeight: 1.6 }}>
|
||||
微信会用设备标识做风控。若该账号被判定<b>设备风险</b>、登录后被强制退出且反复循环,
|
||||
可重置为一个全新的唯一设备 ID(相当于换台新设备),再重新扫码登录。会重启该实例。
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn"
|
||||
style={{ marginTop: 8, alignSelf: 'flex-start' }}
|
||||
onClick={regenMachineId}
|
||||
disabled={regenBusy || busy}
|
||||
>
|
||||
{regenBusy ? '重置中…' : '↻ 重置设备 ID 并重启'}
|
||||
</button>
|
||||
|
||||
{err && <div className="error">{err}</div>}
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user