TikTok 秒杀 · 复制续开(无插件版)
> 浏览器插件版:由于普通审核原因,本 Skill 提供的是页面脚本版。浏览器插件版完全免费,只为与商家交个朋友 —— 关注公众号:tk牛逼社,回复:牛逼 可得。
让 AI 在卖家后台直接完成「把进行中的秒杀活动按规则复制成下一场」。核心是 scripts/tk-renew.js —— 一段在页面控制台里运行的辅助函数,所有点击、填时间、发布都在页面里完成,不依赖扩展、不需要任何授权码或激活码。
一、这条流程做什么 / 不做什么
做:读取秒杀列表里状态为「进行中」的活动 → 每场点「复制」→ 在创建页把活动名改成「原名-续开」→ 时间改成 原结束 + 5 分钟开始、时长 48 小时 → 点「同意并发布」→ 回到列表处理下一场。
不做:不改商品、不改秒杀价(复制会带上原活动的商品与价格);不处理「未开始 / 已结束」的活动;不碰限时特卖等非秒杀类型。
二、三个前提(缺一不可)
- 浏览器已登录卖家后台,且当前店铺正确(URL 带
shop_region=MY之类)。多店铺(用户说「2 号店」)见第九节。 - 页面停在秒杀列表:
/promotion/marketing-tools/flash-sale/list?shop_region=XX,筛选切到 进行中,每页条数调到 20 或 50(默认 10/页会漏行)。 - 有一个能把 JS 送进页面的通道 —— 见第三节,按可用性从 A 到 C 选。
二·五、安全与权限说明(发布前自检)
这个包只做一件事:在你自己已登录的卖家后台页面里,把表单填好并点按钮。所有动作都由你在浏览器控制台主动触发,脚本本身不会自己跑。
| 常见扫描项 | 本包实际情况 |
|---|---|
| 读取 / 移动 / 复制文件 | 无。包内没有任何文件系统操作;启动脚本只创建一个空的独立浏览器配置目录,不复制任何浏览器数据 |
| HTTP 请求 | 仅 cdp.js 连接本机调试端口 127.0.0.1:9222(用户自己启动的浏览器)。不访问任何远程服务器、不上传任何数据;页面内不改写 fetch / XMLHttpRequest |
| 本地监听 / 端口 | 无。cdp.js 只作为客户端连接,本身不监听、不启动任何服务 |
| 权限提升 | 无。不修改系统设置、不请求管理员权限、不改执行策略 |
| 账号凭证 | 不读取、不存储、不外发。店铺标识只从当前 URL、页面属性、页面本地缓存里取,读不到就用店名 + 地区 |
| 控制浏览器 | cdp.js 在用户自己启动的 Chrome 调试实例里,于当前页面执行脚本:改活动名称、写时间字段、点「复制 / 同意并发布」。不打开其他标签、不跨站操作。不需要此能力可删除 cdp.js 与启动脚本,通道 A 功能不受影响 |
数据边界:计划的落点是当前站点的 localStorage(tk-renew.pending.v1:<店铺>),只在本机本浏览器内传递,不经过任何服务器。清缓存即可清除。
发布行为的边界:发布按钮只点一次;超时一律报「结果未知,请人工核对」,绝不自动重发 —— 重复发布会在平台上多开一场活动。
三、执行通道
A. DevTools 控制台 / Snippets(推荐,唯一不需要额外工具的通道)
不装插件、不装依赖、不开调试端口:把 scripts/tk-renew.js 全文贴进卖家后台的 DevTools Console(F12 → Console)回车,页面里就多了一个 TkRenew 对象,后面所有操作都是调用它的方法。
1) 一次性操作序列
TkRenew.pageSize.set(50) // 平台只给 10/50,50 最省翻页
copy(JSON.stringify(await TkRenew.scan(), null, 2)) // 结果进剪贴板,粘给 AI
点「复制」后平台会跳到创建页(新文档),创建页要再贴一次脚本(新页面里 TkRenew 不存在),然后:
await TkRenew.auto({ dryRun: true }) // 先干跑:只填不发,回来看时间对不对
await TkRenew.auto() // 确认无误后真填 + 发布
2) 存成 Snippets,一次粘贴长期复用(强烈建议)
DevTools → Sources → Snippets → 新建 tk-renew,贴入全文,并在末尾加一行:
TkRenew.pending() ? TkRenew.auto() : TkRenew.scan();
之后任意标签页按 Ctrl+Enter 运行这个 snippet:在列表页就返回扫描结果,在创建页有未完成计划就直接填完并发布。复制开了新标签时,在新标签按一次即可。
3) 结果怎么交给 AI:返回值外面套一层 copy(JSON.stringify(...)),粘回对话即可;AI 据此决定下一场点哪个、时间对不对。
踩过的坑(都已在代码里修掉,改代码时别改回去)
| 坑 | 现象 | 正确做法 |
|---|---|---|
| 列表时间分钟不补零 | 2026/11/2 15:5 |
这就是 15:05,不是 15:50;按列头 (GMT+7) 换算 |
| scan 从当前页开始翻 | 扫 3 页就停、漏掉前面几页 | 开扫前 pGotoPage(1),扫完也回第 1 页 |
| 翻页后表格还没渲染 | 被当成「没有更多行」提前收工 | 空表时轮询等 8 秒再判定结束 |
shopKey 掺了店名 |
列表页 region:TH、创建页 name:CrossBorder@TH,计划交接失败 |
key 只用 store_id / region,店名只进 label |
dryRun 清了 pending |
干跑完再发就找不到计划 | 只有 !r.dryRun 才 clearPending() |
| 创建页当旧文档 | TkRenew undefined |
每次跳到创建页都要重新贴一遍脚本 / 按一次 snippet |
B. 本机 CDP 全自动(scripts/cdp.js,可选)
把通道 A 里「人工贴脚本 / 按 Snippet」的步骤自动化:Chrome 带独立配置目录 + 调试端口启动一次,之后用 cdp.js 在页面里执行脚本。cdp.js 只连接本机 127.0.0.1:9222,不监听端口、不上传数据;不需要时可删除这两个脚本,不影响通道 A。
1) 启动(Chrome 136+ 禁止在默认配置上开调试端口,脚本只用独立配置目录,不读写平时的浏览器配置;首次要登录一次卖家后台)
.\scripts\launch-chrome-debug.ps1 # 默认目录 TkDebugProfile
.\scripts\launch-chrome-debug.ps1 -ProfileDir "$env:LOCALAPPDATA\TkProfile-TH" # 多店铺:一店一目录
日志出现登录页即可。用自己双击/终端跑这个脚本最稳 —— 如果 Chrome 是某个自动化会话启动的,会话结束时窗口会被一起回收。
2) 用 cdp.js 操作(Node 22+ 自带 WebSocket,无需装依赖)
NO_PROXY=127.0.0.1,localhost node scripts/cdp.js --list # 列页面
NO_PROXY=127.0.0.1,localhost node scripts/cdp.js --file scripts/tk-renew.js # 在页面里运行脚本
NO_PROXY=127.0.0.1,localhost node scripts/cdp.js "JSON.stringify(TkRenew.probe())" # 执行表达式
NO_PROXY=127.0.0.1,localhost node scripts/cdp.js --eval-file scripts/publish-once.js --timeout 200000
> Windows 上 localhost 常被系统代理拦截(HTTP_PROXY=127.0.0.1:xxxxx)。命令前加 NO_PROXY=127.0.0.1,localhost(bash)或 $env:NO_PROXY="127.0.0.1,localhost"(PowerShell),否则 fetch failed。
3) 一次完整续开
cd scripts
node cdp.js --file ./tk-renew.js
node cdp.js "TkRenew.pageSize.set(50)" # 平台只给 10/50,50 最省翻页
node cdp.js --timeout 180000 "(async()=>JSON.stringify(await TkRenew.scan()))()"
node cdp.js "(()=>{TkRenew.clickCopy(window.__next);return 'fired'})()"
sleep 4 && node cdp.js --file ./tk-renew.js # 创建页是新文档,必须重新运行脚本
node cdp.js --timeout 200000 "(async()=>JSON.stringify(await TkRenew.auto({dryRun:true})))()" # 先干跑核对
node cdp.js --eval-file ./publish-once.js --timeout 200000 # 点发布并盯结果
踩过的坑(都已在代码里修掉,改代码时别改回去)
| 坑 | 现象 | 正确做法 |
|---|---|---|
| 列表时间分钟不补零 | 2026/11/2 15:5 |
这就是 15:05,不是 15:50;按列头 (GMT+7) 换算 |
shopKey 掺了店名 |
列表页 region:TH、创建页 name:CrossBorder@TH,计划交接失败 |
key 只用 store_id / region,店名只进 label |
dryRun 清了 pending |
干跑完再发就找不到计划 | 只有 !r.dryRun 才 clearPending() |
| 创建页当旧文档 | 运行脚本后 TkRenew undefined |
每次导航到创建页都要重新 --file 运行一次 |
C. 纯 UI 点击(脚本不可用时)
不用脚本,AI 用浏览器工具逐行点「复制」→ 点日期框 → 输入 2026/11/05 12:00 → 回车 → 点「同意并发布」。慢、易错,仅用于单场补救;时间换算必须按页面列头的 GMT 标注来算(见 references/troubleshooting.md)。
D. 只能读页面、不能执行 JS 时(例如只有浏览器扩展的「点击/读 DOM」能力)
> 2026-11-02 实测结论:WorkBuddy 的 Chrome 扩展这类通道只有读可见 DOM / 点击 / 输入 / 截图,没有任何在页面执行 JS 的接口;tab_goto 只接受 http(s)(javascript: 被拒);DOM 合成点击对平台的 Arco 按钮不生效(点「复制」只改了 URL、创建页没渲染);截图接口还会 provider invocation timed out。所以这条通道做不了批量续开,只适合下面两件前置事。
- 确认店铺:读页头店铺名(可见 DOM,形如
THThailand(IdaSPORT)),和用户说的「几号店」对上; - 切店:点页头店铺名 → 下拉里点目标店 → 等重载 → 再确认一次店名。
填时间 / 发布必须走 A / B:日期控件要写 React Form 的秒级时间戳,纯点击输入会因时区差 1 小时而撞活动(本项目踩过最久的坑)。
四、标准执行流程
对每一场进行中的活动循环:
-
确认店铺(多店铺必做):
const me = TkRenew.whoami()或scan().shopLabel,得到#2 「马来二号店」TK-马来店(MY)这样的标签。用户说的是「2 号店」就先findShop('2号店')核对isCurrent;不是当前店,先切店(9.2)再扫。别在同一家店的循环里穿插别的店。 -
扫描
js const r = await TkRenew.scan({ leadMin: 5, hours: 48 }); // r.items[i] = { id, name, oldEndText, plan: { name, start, end, startText, endText, clamped }, alreadyRenewed }
判据:ongoing === 0时看r.hint;page.hasNext === true说明还有下一页,await TkRenew.nextPage()后再扫,把结果并起来。 -
跳过已续开的:
alreadyRenewed === true直接跳过(列表里已经有同名「-续开」活动,再建就是重复场)。 -
点复制
js await TkRenew.clickCopy(r.items[i], r.ongoing);
返回ok:false说明当前页找不到那一行 —— 别硬点,先确认筛选/页码,或翻页。 -
切到创建页:
复制一般是同标签跳转到/promotion/marketing-tools/flash-sale/create?duplicateId=...。确认当前 URL 是flash-sale/create,然后重新贴一次tk-renew.js(新文档里TkRenew不存在)。 -
填 + 发布
js await TkRenew.auto({ dryRun: true }) // 先干跑:填名 + 填时间,不点发布,回来看时间对不对 // 核对 startText / endText 与页面显示一致后 await TkRenew.auto() // 真填 + 发布 + 等结果
返回{ ok, startText, endText, fieldVerified, dryRun?, why?, reasons? }。
也可以只跑publish-once.js(页面已填好时用它,点一次发布并盯 150 秒结果)。 -
回收:发布成功后平台会自己跳回列表页。重新贴一次
tk-renew.js→scan()确认新活动已出现(会多一条「原名-续开」),再处理下一场。 -
收尾:
scan()结果里每场都应为alreadyRenewed: true;输出报告。
五、结果判定(必须按这个判,不许猜)
| 返回值 | 含义 | 下一步 |
|---|---|---|
ok:true |
发布成功,页面已离开创建页 | 记录 startText → endText,继续下一场 |
ok:false, why:'timeout' |
90 秒内没等到成功也没报错 | 结果未知。绝不重发,去列表核对;报告里标「结果未知,请人工核对」 |
ok:false, reasons:['促销活动发生重叠'] |
新时间段和别的活动撞车 | 该场跳过;若连续 3 场同因,把 leadMin 调大到 15–30 再试,或先终止冲突活动 |
ok:false, reasons:['商品已下架', ...] |
复制过来的商品本身不满足条件 | 记录原因,继续下一场 |
error:'创建页 60 秒没等到「同意并发布」按钮' |
当前标签不是创建页 | 重新确认标签,在创建页执行 |
发布按钮只点一次。任何「没反应」都不允许重复点 —— 重复点等于开出第二场活动。
六、参数
| 参数 | 默认 | 说明 |
|---|---|---|
leadMin |
5 | 新活动开始 = 原活动结束 + 5 分钟 |
hours |
48 | 新活动时长(小时) |
namePrefix |
空 | 空则命名「原活动名前 30 字 + -续开」;填了则「前缀-序号」 |
dryRun |
false | TkRenew.fill(plan, {dryRun:true}) 只填不发,留给人核对 |
时间一律以秒级时间戳写入表单,并在点发布前挂一层 fetch/XHR 改写兜底 —— 所以电脑时区与店铺时区不一致也不会差一小时。
七、报告格式
| # | 原活动 | 原结束 | 新活动 | 新时段 | 结果 |
|---|---|---|---|---|---|
| 1 | 商家秒杀活动 2026/11/01 14:43:37 | 11-03 14:43 | ...-续开 | 11-03 14:48 → 11-05 14:48 | ✅ 成功 |
| 2 | ... | ... | ... | ... | ⚠ 结果未知,请人工核对 |
结尾一行给汇总:共 N 场进行中,处理 M 场,成功 X,跳过 Y(已续开),失败 Z(原因…)。
八、排查
脚本自带诊断:TkRenew.probe() 返回当前页的创建页判定、发布按钮、时间输入框数量、页面时区、最近日志。填不进去、找不到按钮、时间不对,先看它 —— 详细对照见 references/troubleshooting.md。
九、多店铺:用户说「几号店」怎么对上号
用户不需要记 store_id,也不要要求用户提供。脚本自己维护一张店铺簿(localStorage 的 tk-renew.shops.v1,同一浏览器所有店铺共享),每见到一家新店就自动编号:#1、#2、#3…,编号一旦分配就不再变。
9.1 用户怎么指店,脚本怎么认
| 用户说法 | 处理 |
|---|---|
| 「续开 2 号店」「第 2 家」 | TkRenew.findShop('2号店') → 返回 { ok, shop:{no,id,name,region,alias}, isCurrent } |
| 「续开马来那个店」 | findShop('马来') —— 内置地区中文映射(马来/泰国/印尼/菲律宾/越南/新加坡/美国/英国…) |
| 「续开 TK-XXX 店」 | findShop('TK-XXX') —— 按店名或别名模糊匹配 |
isCurrent:true = 当前标签页就是那家店,直接开工;isCurrent:false 就要先切店(9.2)。匹配到多家会返回 multiple:true + candidates,这时必须回问用户,不许自己挑一家。
9.2 切店
TkRenew.openShopMenu(); // 点开页头店铺切换下拉
TkRenew.pickShop('TK-马来店'); // 在下拉里点名字匹配的那家
// 等 2~5 秒页面重载,再 scan() 确认
const s = await TkRenew.scan();
s.shopLabel; // 例如 "#2 TK-马来店(MY)"
点不到(选择器对不上)就让用户手动切,然后让用户说一声再继续 —— 切店后必须重新 scan,因为计划是跟店走的。切店后如果页面没重载,直接 refresh 一次。
9.3 防串店(三层)
- 计划带店印:
scan()生成的每个plan都带shopKey(id:<store_id>,读不到 id 时退化成name:<店名>@<地区>)。 - 执行强校验:
fill()/auto()开头比对当前页shopKey,不一致直接拒绝:
跨店保护:这份计划属于 #2 TK-马来店(MY),当前页面是 #1 泰国店(TH)…—— 看到这句就是切错店了,不是脚本坏了。 - 待处理计划按店隔离:
tk-renew.pending.v1:<shopKey>,A 店复制出来的创建页绝不会拿走 B 店的计划。
9.4 多店批量的标准节奏
一次只在一家店里跑完整流程,别交叉:
对每家目标店:
findShop('2号店') → isCurrent? 否则 openShopMenu+pickShop → 等重载 → scan() 确认 shopLabel
→ 逐场 clickCopy + auto(第四节的循环)
→ 最后 scan() 核对这家店的新活动
→ 再切下一家
用户一次给多家店(「1、2 号店都续开」)也串行跑:一家跑完再切下一家,不要开多个标签并行 —— 页面交互是单线程的,并行只会让点击落到错误的标签。
报告里每家店单起一张表,表头写 ## #2 TK-马来店(MY)· 共 3 场进行中。
9.4.1 编号别丢:写进 references/shop-map.md
脚本的店铺簿存在浏览器里,用户清一次缓存就没了。所以:
- 每次用户确认过「N 号店 = 哪家店」之后,把
scan().shopLabel的编号 / 店名 / 地区 / store_id 记到references/shop-map.md; - 下次开工前先读这张表,再
findShop核对; - 表里编号和脚本编号打架时以用户说法为准,先
bindShop把称呼钉住,别默默按脚本编号干。
9.5 读不到 store_id 时(exact:false)
store_id 常常只存在于浏览器内部(页面脚本读不到,2026-11-02 实测的泰国店就是这种)。这时:
shopKey退化成region:TH这类地区键,scan()的shop.exact === false;- 店铺簿仍能用:编号 / 别名 / 店名都记下来了,
findShop('2号店')照常工作; - 同一地区的两家店 key 会相同 → 这种店务必一家跑完再切另一家,别并行;
- 写操作前把
shopLabel(店名 + 地区)复述给用户确认,比只报编号安全。
十、实测记录(2026-11-02,泰国店 IdaSPORT)
完整跑通一次真实续开,可作为回归基准:
| 项 | 值 |
|---|---|
| 店铺 | region:TH(exact:false,store_id 读不到) |
| 页面时区 | GMT+7(列头标注),电脑 GMT+8 |
| 列表规模 | 330 条活动 / 7 页(50 条一页) |
| 进行中的秒杀 | 2 场 |
| 其中已续开 | 1 场(10:38:23 那场,续开活动 14:43 → 11/06 14:43 已存在)→ 跳过 |
| 本次处理 | 商家秒杀活动 2026/11/02 12:05:08 |
| 原结束 | 2026/11/4 15:05(GMT+7,列表显示 15:5) |
| 新时段 | 2026/11/4 15:10 → 2026/11/6 15:10(GMT+7)= +5 分钟 / 48 小时 |
| 结果 | 平台显示未开始,活动总数 330 → 331 ✅ |
对照校验:另有一场已续开的活动,原结束 13:38 → 续开开始 13:43,正好 +5 分钟,说明时间解析规则正确。
TkRenew 当前导出:scan / clickCopy / auto / fill / pending / clearPending / probe / whoami / shops / bindShop / findShop / openShopMenu / pickShop / scanRows / tableRows / pageSize / gotoPage / nextPage2。