docs: 初始化 cmshopee 文档、设计与项目骨架
- docs/ 完整 harness coding 文档集(愿景/需求/技术栈/架构/编码规则/任务/api/routes/current-state) - 5 Tab 流水线设计 + UI 效果图 SVG(docs/ui/) - cdp.py CDP 底座;prototypes/ 已验证原型脚本(待 editor.py 移植后清理) - AGENTS.md/CLAUDE.md 入口、progress.md 执行流水、.gitignore(排除凭证/DB/图片) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+25
@@ -0,0 +1,25 @@
|
||||
# ── 凭证 / 业务数据 / 本地产物(绝不提交)──
|
||||
# AI 模型清单 ai_models.json(含密钥)
|
||||
config/
|
||||
# 应用配置(路径/模型选择/参数)
|
||||
config.json
|
||||
# SQLite(账号/任务/结果,含密码)
|
||||
cmshopee.db
|
||||
# 各账号 Chrome 配置(含登录态)
|
||||
chrome_user_data_dir/
|
||||
# 采集的旧封面 / AI 生成的新封面
|
||||
images/
|
||||
|
||||
# ── Python ──
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
|
||||
# ── 编辑器 / 系统 ──
|
||||
.vscode/
|
||||
.idea/
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
@@ -0,0 +1,52 @@
|
||||
# AGENTS.md
|
||||
|
||||
> Codex / 通用 AI coding agent 的仓库级入口。进入本仓库后,先读本文,再进入 [`docs/00-ai-start-here.md`](docs/00-ai-start-here.md)。
|
||||
|
||||
## 项目定位
|
||||
|
||||
cmshopee 是一个 **Windows 本地桌面自动化工具**:管理多个 Shopee 卖家账号,用 CDP 驱动 Chrome 自动改商品标题、换商品封面。当前单账号流程已验证,正在扩展多账号管理与 GUI。
|
||||
|
||||
## 文档位置
|
||||
|
||||
- 项目文档集合在 [`docs/`](docs/),导航见 [`docs/README.md`](docs/README.md)。
|
||||
- 编程入口是 [`docs/00-ai-start-here.md`](docs/00-ai-start-here.md),它继续导航到愿景、需求、技术栈、架构、编码规则、任务看板。
|
||||
- 执行历史流水在根目录 [`progress.md`](progress.md);当前快照在 [`docs/current-state.md`](docs/current-state.md)。
|
||||
|
||||
## 必读顺序
|
||||
|
||||
每次开始工作前,按顺序读取:
|
||||
|
||||
1. 本文 `AGENTS.md`:仓库定位与规则。
|
||||
2. [`docs/00-ai-start-here.md`](docs/00-ai-start-here.md):编程入口与流程。
|
||||
3. [`docs/04-architecture.md`](docs/04-architecture.md):尤其**第四节 CDP 已验证事实**。
|
||||
4. [`docs/05-coding-rules.md`](docs/05-coding-rules.md):硬性编码规则。
|
||||
5. [`docs/06-tasks.md`](docs/06-tasks.md) + [`docs/current-state.md`](docs/current-state.md):领取任务、了解现状。
|
||||
6. 与当前任务相关的具体文档。
|
||||
|
||||
## 工作规则
|
||||
|
||||
- 复用已有 `cdp.py`,遵守架构第四节的选择器、就绪判断、上传/拖拽方式,不另起一套 CDP 交互。
|
||||
- 只做当前任务范围内的事;V2(批量、多账号并行)只记录不实现。
|
||||
- CDP 选择器 / 流程 / 配置 schema 变化,必须同步更新 `docs/04-architecture.md` 与相关任务。
|
||||
- 改完后更新任务状态、追加 `progress.md`、覆盖 `docs/current-state.md`。
|
||||
|
||||
## 安全红线
|
||||
|
||||
- 绝不把真实账号、密码、Cookie、token 写进代码、文档或日志;密码加密存、不自动登录。
|
||||
- `config.json`、`cmshopee.db`、`chrome_user_data_dir/`(含密码/登录态)绝不提交版本库。
|
||||
- 「更新」提交线上、删除满 9 张封面等不可逆动作,必须显式确认/开关,并先在测试商品验证。
|
||||
- 不绕过 Shopee 的验证码、风控、限流或权限校验。
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
python -m py_compile *.py # 语法检查
|
||||
python prototypes/demo.py # 单账号闭环(不提交)
|
||||
```
|
||||
|
||||
涉及 CDP 改动,在测试商品(ITEM_ID 51100639510)上实跑确认;如命令不可运行,在回复里如实说明。
|
||||
|
||||
## 风格
|
||||
|
||||
- 文档与 UI 文案用中文,标识符用英文。
|
||||
- 内容面向 agent 执行:能落到“读什么、改什么、验证什么”。
|
||||
@@ -0,0 +1,13 @@
|
||||
# CLAUDE.md
|
||||
|
||||
> Claude Code 的仓库级薄入口。进入本仓库后,先读本文,再读 [`AGENTS.md`](AGENTS.md)。
|
||||
|
||||
本仓库的权威 agent 规则、文档入口、工作边界和验证方式统一维护在 [`AGENTS.md`](AGENTS.md)。
|
||||
|
||||
Claude Code 处理本仓库任务时:
|
||||
|
||||
1. 先读取 [`AGENTS.md`](AGENTS.md)。
|
||||
2. 再按 `AGENTS.md` 进入 [`docs/00-ai-start-here.md`](docs/00-ai-start-here.md) 和相关文档(重点 `docs/04-architecture.md` 第四节 CDP 已验证事实)。
|
||||
3. 不在本文重复维护任务流程、编码规则或文档清单,避免和 `AGENTS.md` 漂移。
|
||||
|
||||
安全红线(详见 `AGENTS.md`):不写真实凭证、密码加密存不自动登录;不提交 `config.json`、`cmshopee.db`、`chrome_user_data_dir/`;不可逆操作需显式确认;不绕过 Shopee 风控。
|
||||
@@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env python3
|
||||
# cdp.py —— 共享 CDP 底座:连接 Chrome、定位/打开商品 tab、执行 JS、模拟拖拽。
|
||||
# 被 demo.py 及各能力脚本复用。
|
||||
#
|
||||
# 依赖:pip install websocket-client requests
|
||||
|
||||
import os
|
||||
|
||||
# CDP 局域网直连,绝不走代理(WSL 里 *_proxy 指向 :1080 会导致超时)。
|
||||
for _k in ("ALL_PROXY", "all_proxy", "HTTP_PROXY", "http_proxy", "HTTPS_PROXY", "https_proxy"):
|
||||
os.environ.pop(_k, None)
|
||||
|
||||
import json
|
||||
import time
|
||||
import threading
|
||||
|
||||
import requests
|
||||
from websocket import create_connection
|
||||
|
||||
CDP_HOST = os.environ.get("CDP_HOST", "192.168.0.224:9333")
|
||||
BASE = f"http://{CDP_HOST}"
|
||||
|
||||
|
||||
def http_get(path):
|
||||
s = requests.Session()
|
||||
s.trust_env = False # 忽略环境代理
|
||||
return s.get(f"{BASE}{path}", timeout=10).json()
|
||||
|
||||
|
||||
class CDP:
|
||||
"""单个 target 的 CDP 客户端:命令同步、事件回调异步。"""
|
||||
|
||||
def __init__(self, ws_url):
|
||||
# suppress_origin=True:不发 Origin 头,避免 Chrome 未开 --remote-allow-origins 时 403。
|
||||
self.ws = create_connection(ws_url, max_size=None, suppress_origin=True)
|
||||
self._id = 0
|
||||
self._res = {}
|
||||
self._cond = threading.Condition()
|
||||
self._handlers = []
|
||||
self._stop = False
|
||||
threading.Thread(target=self._loop, daemon=True).start()
|
||||
|
||||
def _loop(self):
|
||||
while not self._stop:
|
||||
try:
|
||||
raw = self.ws.recv()
|
||||
except Exception:
|
||||
break
|
||||
if not raw:
|
||||
continue
|
||||
m = json.loads(raw)
|
||||
if "id" in m:
|
||||
with self._cond:
|
||||
self._res[m["id"]] = m
|
||||
self._cond.notify_all()
|
||||
elif "method" in m:
|
||||
for h in self._handlers:
|
||||
try:
|
||||
h(m)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def on(self, fn):
|
||||
self._handlers.append(fn)
|
||||
|
||||
def send(self, method, params=None, timeout=30):
|
||||
self._id += 1
|
||||
mid = self._id
|
||||
self.ws.send(json.dumps({"id": mid, "method": method, "params": params or {}}))
|
||||
end = time.time() + timeout
|
||||
with self._cond:
|
||||
while mid not in self._res:
|
||||
left = end - time.time()
|
||||
if left <= 0:
|
||||
raise TimeoutError(f"CDP {method} 超时")
|
||||
self._cond.wait(left)
|
||||
m = self._res.pop(mid)
|
||||
if "error" in m:
|
||||
raise RuntimeError(f"{method}: {m['error'].get('message')}")
|
||||
return m.get("result", {})
|
||||
|
||||
def ev(self, expr, by_value=True):
|
||||
r = self.send("Runtime.evaluate", {"expression": expr, "returnByValue": by_value})
|
||||
if "exceptionDetails" in r:
|
||||
raise RuntimeError("JS 异常: " + r["exceptionDetails"].get("text", ""))
|
||||
return r.get("result", {})
|
||||
|
||||
def val(self, expr):
|
||||
return self.ev(expr).get("value")
|
||||
|
||||
def object_id(self, expr):
|
||||
return self.ev(expr, by_value=False).get("objectId")
|
||||
|
||||
def drag(self, x0, y0, x1, y1, steps=34):
|
||||
"""按住源点 → 多步移动到目标点 → 释放,驱动自定义拖拽排序库。"""
|
||||
me = "Input.dispatchMouseEvent"
|
||||
self.send(me, {"type": "mouseMoved", "x": x0, "y": y0})
|
||||
self.send(me, {"type": "mousePressed", "x": x0, "y": y0, "button": "left", "buttons": 1, "clickCount": 1})
|
||||
for dx in (3, 6, 10): # 小幅抖动,越过拖拽启动阈值
|
||||
self.send(me, {"type": "mouseMoved", "x": x0 + dx, "y": y0, "button": "left", "buttons": 1})
|
||||
time.sleep(0.03)
|
||||
for i in range(1, steps + 1):
|
||||
x = x0 + (x1 - x0) * i / steps
|
||||
y = y0 + (y1 - y0) * i / steps
|
||||
self.send(me, {"type": "mouseMoved", "x": x, "y": y, "button": "left", "buttons": 1})
|
||||
time.sleep(0.025)
|
||||
for _ in range(10): # 在目标点停留,让排序库吸附
|
||||
self.send(me, {"type": "mouseMoved", "x": x1, "y": y1, "button": "left", "buttons": 1})
|
||||
time.sleep(0.05)
|
||||
self.send(me, {"type": "mouseReleased", "x": x1, "y": y1, "button": "left", "buttons": 0, "clickCount": 1})
|
||||
|
||||
def close(self):
|
||||
self._stop = True
|
||||
try:
|
||||
self.ws.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def find_product_tab(item_id):
|
||||
"""在已打开的 tab 里找 URL 同时含 item_id 和 shopee.tw 的页面。"""
|
||||
for t in http_get("/json"):
|
||||
if t.get("type") != "page":
|
||||
continue
|
||||
url = t.get("url") or ""
|
||||
if item_id in url and "shopee.tw" in url:
|
||||
return t
|
||||
return None
|
||||
|
||||
|
||||
def create_tab(url):
|
||||
"""用 browser 级 Target.createTarget 新建 tab,返回其 page websocket。"""
|
||||
ver = http_get("/json/version")
|
||||
b = CDP(ver["webSocketDebuggerUrl"])
|
||||
try:
|
||||
tid = b.send("Target.createTarget", {"url": url})["targetId"]
|
||||
finally:
|
||||
b.close()
|
||||
end = time.time() + 15
|
||||
while time.time() < end:
|
||||
for t in http_get("/json"):
|
||||
if t.get("id") == tid and t.get("webSocketDebuggerUrl"):
|
||||
return t["webSocketDebuggerUrl"]
|
||||
time.sleep(0.5)
|
||||
raise TimeoutError("等待新 tab websocket 超时")
|
||||
@@ -0,0 +1,6 @@
|
||||
@echo off
|
||||
rem 单账号原型演示:脚本已移到 prototypes/,设 PYTHONPATH 让根目录 cdp.py 可导入
|
||||
cd /d %~dp0
|
||||
set PYTHONPATH=%~dp0
|
||||
python prototypes\demo.py
|
||||
pause
|
||||
@@ -0,0 +1,116 @@
|
||||
# AI 开发入口
|
||||
|
||||
> 给 AI coding agent 的项目入口。这里负责导航和流程,硬性编码规则见 [`05-coding-rules.md`](05-coding-rules.md)。
|
||||
|
||||
## 一句话定位
|
||||
|
||||
cmshopee 是一个 Windows 本地桌面自动化工具(Tkinter,5 Tab),让运营管理多个 Shopee 账号,并用 CDP 驱动 Chrome + AI 批量改商品标题、换商品封面。
|
||||
|
||||
5 Tab 流水线(工作流优先顺序):
|
||||
**① 导入采集 → ② AI生成 → ③ 更新shopee → ④ 账号管理 → ⑤ 设置**
|
||||
|
||||
目标闭环:④ 配账号并登录 → ① 导入 Excel(按“别名”列关联账号)、采集旧标题/旧封面并回写 → ② 用提示词 AI 生成新标题/新封面(无人工确认)→ ③ 对已生成任务改标题+换封面并直接点「更新」提交(无开关)→ 结果实时存 SQLite、批量回写原 Excel。
|
||||
|
||||
存储:应用设置 `config.json`(含 AI Key)+ 业务数据 SQLite `cmshopee.db` + Excel 用 openpyxl + 图片存本地 `images/`。AI 服务商待定。
|
||||
|
||||
## 必读顺序
|
||||
|
||||
每次开始写代码前,按这个顺序建立上下文:
|
||||
|
||||
1. [`01-vision.md`](01-vision.md):为什么做、为谁做、什么不做。
|
||||
2. [`02-requirements.md`](02-requirements.md):MVP 要什么、怎么算达成。
|
||||
3. [`03-tech-stack.md`](03-tech-stack.md):既定技术选型(Python + 自研 CDP + Tkinter 待确认)。
|
||||
4. [`04-architecture.md`](04-architecture.md):模块职责、账号数据模型、**第四节 CDP 已验证事实(重点)**。
|
||||
5. [`05-coding-rules.md`](05-coding-rules.md):写代码前必须遵守的规则。
|
||||
6. [`06-tasks.md`](06-tasks.md):领取本轮唯一任务。
|
||||
7. [`../progress.md`](../progress.md):历史执行记录、验证结果、阻塞点和关键决策。
|
||||
8. [`current-state.md`](current-state.md):当前代码现实、可运行命令、下一步任务。
|
||||
|
||||
如果仓库根目录有 `AGENTS.md`、`CLAUDE.md`,也必须先读,仓库级规则优先。
|
||||
|
||||
## 当前阶段
|
||||
|
||||
当前项目处于:**单账号流程已验证,正扩展多账号管理 + GUI**。
|
||||
|
||||
优先路径:
|
||||
|
||||
1. Phase 0:把已验证流程模块化(`editor.py`),定义账号配置。
|
||||
2. Phase 1:账号绑定 user-data-dir、Chrome 启动、登录保活、加载商品页。
|
||||
3. Phase 2:GUI 跑通单账号改标题 + 换封面闭环。
|
||||
4. Phase 3:批量与多账号并行(V2)。
|
||||
|
||||
## 领取任务规则
|
||||
|
||||
从 [`06-tasks.md`](06-tasks.md) 领取任务时:
|
||||
|
||||
- 只领取第一个状态为 `TODO` 且依赖均为 `DONE` 的任务。
|
||||
- 开始前把该任务状态改为 `DOING`。
|
||||
- 本轮只完成这一个任务。
|
||||
- 验收通过后把状态改为 `DONE`。
|
||||
- 完成后把执行记录追加到 [`../progress.md`](../progress.md),覆盖更新 [`current-state.md`](current-state.md)。
|
||||
- 做完即停,汇报验证结果,等待下一步指令。
|
||||
|
||||
如果代码实际状态和任务看板冲突,先说明冲突,不要擅自跳步。
|
||||
|
||||
## MVP 边界
|
||||
|
||||
MVP 只做:
|
||||
|
||||
- 账号配置(增删改 1..N 个 Shopee 账号)。
|
||||
- 每账号绑定 `chrome_user_data_dir/<slug>` 独立配置目录。
|
||||
- 一键启动 Chrome 供首次人工登录、登录态持久化。
|
||||
- 用某账号打开指定商品详情页,改标题、换封面。
|
||||
- 显式确认后点击「更新」提交(默认不提交)。
|
||||
|
||||
MVP 不做:
|
||||
|
||||
- 自动登录 / 自动填账号密码。
|
||||
- 绕过验证码、风控、限流。
|
||||
- 批量、多账号并行、爬取、数据库(属 V2 及之后)。
|
||||
|
||||
## 事实来源
|
||||
|
||||
项目事实只信:
|
||||
|
||||
- [`04-architecture.md`](04-architecture.md) 第四节:CDP 交互已验证结论(选择器、就绪判断、上传/拖拽方式)。
|
||||
- [`04-architecture.md`](04-architecture.md) 4.1/4.2/4.3:`config.json`、SQLite schema、Excel 模板。
|
||||
- 真实页面探查结果(用 `prototypes/inspect_images.py` / `prototypes/cookies.py` 实地确认)。
|
||||
- 已验证脚本 `cdp.py`、`prototypes/demo.py`、`prototypes/set_title.py`、`prototypes/set_cover.py` 中跑通的逻辑。
|
||||
|
||||
不要把以下当事实来源:
|
||||
|
||||
- 旧脚本里可能已失效的选择器(Shopee 页面会变)。
|
||||
- 未经实测的猜测。
|
||||
- 临时探查脚本(`/tmp` 下的一次性脚本)。
|
||||
|
||||
## 常见任务该看哪里
|
||||
|
||||
做 GUI:
|
||||
|
||||
- 先看 `02-requirements.md` 的对应验收标准。
|
||||
- 再看 `routes.md` 的窗口职责与操作流程。
|
||||
- 最后看 `04-architecture.md` 的模块边界(GUI 不写业务逻辑)。
|
||||
|
||||
做 CDP / 浏览器操作:
|
||||
|
||||
- 先看 `04-architecture.md` 第四节已验证事实。
|
||||
- 再看 `api.md` 的 `cdp` / `editor` 模块合约。
|
||||
- 复用 `cdp.py`,不重写一套。
|
||||
|
||||
做账号配置 / Chrome 启动:
|
||||
|
||||
- 先看 `04-architecture.md` 3.1 数据模型与 `api.md` 的 `config` / `chrome` 合约。
|
||||
- Chrome 启动参数严格按第四节“Chrome 启动参数”一条。
|
||||
|
||||
## 验证命令
|
||||
|
||||
```bash
|
||||
python -m py_compile *.py # 语法检查
|
||||
python prototypes/demo.py # 单账号闭环验证(分步,不提交)
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- 改 CDP / editor 逻辑后:在测试商品(ITEM_ID 51100639510)上跑 `prototypes/demo.py` 实测。
|
||||
- 改账号配置后:验证 SQLite 账号读写与 user-data-dir 创建;改应用设置后验证 `config.json` 读写。
|
||||
- 如果命令当前不可运行(如 GUI 未建),在回复里如实说明。
|
||||
@@ -0,0 +1,44 @@
|
||||
# 项目愿景
|
||||
|
||||
## 一、核心目标
|
||||
|
||||
cmshopee 要解决:**运营要在多个 Shopee 卖家账号下,逐个商品手动改标题、换封面,效率低、易出错、来回切换登录痛苦**。
|
||||
|
||||
> 让**电商运营**能够在一个工具里集中管理多个 Shopee 账号,并自动完成商品标题修改和封面图替换,无需反复登录、手动逐项操作。
|
||||
|
||||
它不是一个通用爬虫,也不是 Shopee 官方 API 客户端,而是一个**驱动已登录的真实 Chrome 浏览器、在卖家中心页面上代为操作**的本地自动化工具。
|
||||
|
||||
## 二、目标用户
|
||||
|
||||
- **电商运营 / 店铺管理员**:管理 1 个或多个 Shopee 卖家账号,需要批量调整商品标题和主图。
|
||||
- **小型卖家团队负责人**:希望把重复的商品维护操作标准化、半自动化。
|
||||
- **本项目维护者(含 AI agent)**:需要在已验证的单账号流程上,扩展出多账号管理与 GUI。
|
||||
|
||||
## 三、产品原则
|
||||
|
||||
遇到取舍时,以这些原则为准:
|
||||
|
||||
- **核心流程优先**:先把「单账号改标题 + 换封面」做稳,再扩展多账号和批量。
|
||||
- **真实环境优先**:操作真实的已登录 Chrome,不伪造登录、不绕过风控。
|
||||
- **小步交付**:每一步都能运行、能验证、能回退;默认不提交线上,显式开关才落库。
|
||||
- **少即是稳**:MVP 不追求全自动,只追求最小可靠闭环;首次登录由人工完成。
|
||||
- **凭证安全**:登录态保存在各账号独立的 Chrome user-data-dir 里,不导出、不外传、不写进代码或文档。
|
||||
|
||||
## 四、核心价值主张
|
||||
|
||||
| 价值点 | 说明 |
|
||||
| --- | --- |
|
||||
| 多账号集中管理 | 一处配置多个 Shopee 账号,各自绑定独立浏览器配置目录,互不串号 |
|
||||
| 免反复登录 | 每个账号的登录态持久化在自己的 user-data-dir,启动即带登录 Cookie |
|
||||
| 自动改标题/换封面 | CDP 驱动浏览器自动改标题、上传新图并拖拽设为封面 |
|
||||
| 可控可回退 | 默认只改不提交,显式确认/开关才点「更新」,演示与试错零风险 |
|
||||
|
||||
## 五、不做什么(非目标)
|
||||
|
||||
- 不做 Shopee 账号的**自动登录 / 自动填账号密码**(首次登录人工完成)。
|
||||
- 不**绕过验证码、风控、限流或平台权限校验**。
|
||||
- 不做商品数据的**批量爬取 / 导出**。
|
||||
- 不做云端服务、数据库、多人协作后台(MVP 为本地单机工具)。
|
||||
- 不接管标题/封面以外的商品字段编辑。
|
||||
|
||||
> MVP 的具体功能范围与验收标准,见 [需求](02-requirements.md)。
|
||||
@@ -0,0 +1,112 @@
|
||||
# 需求
|
||||
|
||||
> 本文只描述**要什么**与**怎么算达成**,用产品 / 用户语言表达,**不涉及技术实现**。
|
||||
> 技术方案、数据结构、CDP 细节见 [架构设计](04-architecture.md)。
|
||||
|
||||
## 一、业务现状
|
||||
|
||||
| 项 | 状态 |
|
||||
| --- | --- |
|
||||
| 用户 | 电商运营,手动在多个 Shopee 卖家账号下逐个改标题、换封面,效率低 |
|
||||
| 数据 | 商品数据在 Shopee 卖家中心;本工具不落库,只在浏览器页面上操作 |
|
||||
| 现有系统 | 已用 CDP 验证单账号流程(改标题、上传图、拖拽换封面、可选更新),见 `current-state.md` |
|
||||
| 约束 | 依赖真实 Chrome 与已登录态;受 Shopee 页面结构、限流、风控影响;首次登录需人工 |
|
||||
|
||||
## 二、用户角色
|
||||
|
||||
- **运营操作员**:配置账号、为账号登录、对选定商品执行改标题/换封面。
|
||||
- **未登录账号**:已在配置中但其 user-data-dir 尚未登录 Shopee;执行任务前必须先人工登录。
|
||||
|
||||
## 三、功能清单
|
||||
|
||||
### 第一版 MVP(最小闭环)
|
||||
|
||||
| 功能 | 用户能做什么 | 优先级 |
|
||||
| --- | --- | --- |
|
||||
| 账号配置 | 在 GUI 里新增/编辑/删除 1 个或多个 Shopee 账号(名称、地区、备注) | P0 |
|
||||
| 绑定配置目录 | 为每个账号在项目 `chrome_user_data_dir/<账号标识>` 下创建并绑定独立 user-data-dir | P0 |
|
||||
| 启动并登录 | 一键用某账号的 user-data-dir 启动 Chrome(带调试参数),首次人工登录 Shopee 后登录态持久化 | P0 |
|
||||
| 生成快捷方式 | 可选:为每账号生成桌面快捷方式,目标带该账号 user-data-dir,供手动打开对应账号 Chrome | P1 |
|
||||
| 加载商品页 | 选定账号,用其已登录 Chrome 打开指定商品详情页 | P0 |
|
||||
| 改标题 | 对该商品按规则修改标题(如去掉结尾若干字 / 指定新标题) | P0 |
|
||||
| 换封面 | 上传一张本地图片,自动设为商品封面(满 9 张时先删原封面再传) | P0 |
|
||||
| 提交更新 | 在显式确认后点击「更新」,把改动提交到线上(默认不提交) | P0 |
|
||||
|
||||
### 第二版(多账号 + Excel + AI,5 Tab 流水线)
|
||||
|
||||
5 个 Tab,顺序:① 导入采集 → ② AI生成 → ③ 更新shopee → ④ 账号管理 → ⑤ 设置。
|
||||
|
||||
| 功能 | 用户能做什么 | 优先级 |
|
||||
| --- | --- | --- |
|
||||
| 账号管理(④) | 增删改账号:账号名/别名/地区/数据目录/端口/密码(加密仅参考)/备注;登录状态;启动登录、检测登录 | P0 |
|
||||
| 导入(①) | 导入多个 Excel,按模板解析输入列(账号名/别名/商品id)为任务列表 | P0 |
|
||||
| 导入校验汇总(①) | 导入后展示文件数、解析行数(原始数据量)、有效/无效行、匹配账号行数(按账号细分)、未匹配行数;跑采集前先纠错 | P0 |
|
||||
| 采集旧数据(①) | 程序只读打开商品页,抓取旧标题、下载旧封面到本地,回写 Excel 旧字段 | P0 |
|
||||
| AI 生成(②) | 输入标题/封面提示词,AI 据提示词+旧标题生成新标题、据提示词+旧封面生成新封面 | P0 |
|
||||
| 提示词管理(②) | 标题提示词「保存」到 `title_prompt.txt` 并启动回显;封面提示词多模板(下拉 + 新建/保存/另存为/重命名/删除)+ 插入 `{新标题}` + 预览(变量替换) | P0 |
|
||||
| 查看对照(②) | 双击任务弹窗查看新旧封面(纯查看,无人工确认);可选对单行重生成 | P0 |
|
||||
| 更新 shopee(③) | 按批次/店铺/状态筛选;对筛选出的已生成任务打开编辑页换标题+封面,并直接点「更新」提交线上(无开关);可按状态=失败重试 | P0 |
|
||||
| 结果存储与回写 | 各阶段结果实时存 SQLite;该文件全部完成后把旧/新数据+状态批量回写原 Excel | P0 |
|
||||
| 设置(⑤) | AI 模型管理(下拉+新增/删除/详情/测试连接,至少各一个文本+图像模型);标题/图片大模型角色选择;分辨率(512/1k/2k/4k,返回超时随分辨率自动);并发/重试/jpg质量;图片目录/Chrome 路径/端口 | P0 |
|
||||
| 首次引导保护 | 未配账号/未登录时,① ③ 执行按钮禁用并提示去④ | P0 |
|
||||
|
||||
### 后续迭代
|
||||
|
||||
| 功能 | 描述 | 阶段 |
|
||||
| --- | --- | --- |
|
||||
| 多账号并行 | 多个账号各自端口同时运行 | V2 |
|
||||
| 运行日志 / dry-run | 操作留痕、可先空跑预览 | V2 |
|
||||
| 标题规则模板 | 预设多种改标题规则(前缀、替换、截断等) | V3 |
|
||||
|
||||
## 四、核心用户故事(MVP)
|
||||
|
||||
1. 作为运营,我打开工具后能看到已配置的账号列表,并能新增一个账号。
|
||||
2. 我为某账号点击「启动并登录」,工具用它专属的配置目录打开 Chrome,我手动登录一次 Shopee 卖家中心。
|
||||
3. 之后我选中该账号、填入商品 ID,工具自动打开该商品详情页(带登录态)。
|
||||
4. 工具自动把标题改成目标值,并上传我指定的图片、把它拖到第一位设为封面。
|
||||
5. 我核对无误后点击「更新」(或让工具在确认后点击),改动提交线上;不确认则刷新即还原。
|
||||
6. 当账号尚未登录、商品页加载失败或封面已满 9 张时,工具给出明确提示并安全处理。
|
||||
|
||||
## 五、验收标准(MVP)
|
||||
|
||||
- **账号配置**:新增/编辑/删除账号后,配置持久化到本地文件,重启工具仍在。
|
||||
- **绑定配置目录**:每个账号对应唯一的 `chrome_user_data_dir/<账号标识>` 目录;不同账号互不共用、不串号。
|
||||
- **启动并登录**:点击后 Chrome 用对应 user-data-dir 启动并开启调试端口;人工登录一次后,再次启动无需重新登录。
|
||||
- **加载商品页**:选定账号执行时,能用该账号登录态打开目标商品详情页,标题框与图片管理器渲染就绪。
|
||||
- **改标题**:写入后标题框 `value` 与 `modelvalue` 均等于目标值(确认页面模型已更新)。
|
||||
- **换封面**:上传成功后图片张数 +1(满 9 张时先删第一张再传);目标图被拖到第一位成为封面。
|
||||
- **提交更新**:默认不点「更新」;开启提交时,按钮可点才点击,禁用态则不点并提示原因。
|
||||
- **安全**:任意失败路径不崩溃、不误删、不在无确认下提交线上。
|
||||
|
||||
## 六、范围边界与决策
|
||||
|
||||
| 问题 | 决策 |
|
||||
| --- | --- |
|
||||
| 第一版平台 | Windows 桌面(Chrome 与各 user-data-dir 同机) |
|
||||
| 多账号隔离方式 | 每账号独立 user-data-dir(非 Chrome profile);理由见 [架构 3.0](04-architecture.md) |
|
||||
| 是否需要账号 | 是;管理多个 Shopee 卖家账号,但登录由人工首次完成 |
|
||||
| 存储 | 应用设置用 `config.json`;账号/任务/结果用 SQLite;Excel 读写用 openpyxl |
|
||||
| 账号↔任务绑定 | 以 Excel“别名”列为权威(非文件名);匹配不到的略过并最后弹窗汇总 |
|
||||
| 是否提交更新 | ③ 执行时总是点「更新」提交线上(无人工确认、无开关) |
|
||||
| 执行方式 | 多账号串行、逐任务执行,单条失败继续 |
|
||||
| 旧标题/旧封面 | 程序在「采集」阶段改前抓取的快照(输出列),运营不填 |
|
||||
| 新标题/新封面 | AI 生成(输出列),直接用于更新(无人工确认);本地留档+回写 Excel 供追溯 |
|
||||
| AI 服务 | 文本+图像生成,服务商/模型待定;Key 加密存于配置;见 [技术栈](03-tech-stack.md) |
|
||||
| 本地图片 | 旧封面下载、新封面生成存本地图片目录,路径记 DB |
|
||||
| 结果落库时机 | 各阶段处理完立即写 SQLite;该 Excel 全部完成后批量回写原文件 |
|
||||
| 原文件被占用 | 回写时若原 Excel 被锁定,提示关闭重试或另存副本(SQLite 为事实来源) |
|
||||
| 暂不支持 | 自动登录、多账号并行(V2)、爬取 |
|
||||
|
||||
## 七、待确认 / 风险点
|
||||
|
||||
- **账号 / 凭证风险**:登录态存在各账号 user-data-dir,等同账号凭证;目录不得提交版本库、不得外传。
|
||||
- **密码存储风险**:Tab2 的密码加密存于 SQLite,仅供人工参考,**绝不自动登录/自动填**;`cmshopee.db`、`config.json` 必须 gitignore;加密密钥不得与密文同存(见 [任务 T-403](06-tasks.md))。
|
||||
- **别名匹配风险**:别名以 Excel 列为准;不匹配的行略过并最后弹窗,执行起始在日志逐条打印「文件→匹配账号」留痕,防误改。
|
||||
- **AI 主图风险(高,已知并接受)**:本设计**无人工确认、无提交开关**,AI 生成的标题/封面经 ③ 执行即直接上线;主图若失真/夸大/侵权可能违反 Shopee 规则甚至下架。唯一缓解:新图本地留档 + 回写 Excel 供事后追溯;强烈建议先在测试商品验证再批量。
|
||||
- **AI 成本与依赖**:接入文本+图像 AI = 新外部依赖 + API 费用 + Key 管理;服务商/模型/合规待 [技术栈](03-tech-stack.md) 确认。
|
||||
- **AI Key 安全**:Key 加密存于 `config.json`,不写日志、不提交版本库。
|
||||
- **第三方平台风险**:Shopee 页面结构、class 名、接口随时可能变;限流、风控、封号风险存在,禁止高频批量。
|
||||
- **自动化边界风险**:③ 执行会自动改标题、上传图片、拖拽并**直接点更新提交线上**(无确认、无开关);点「开始更新」前需自行确保任务来源与 AI 产出可接受。
|
||||
- **合规风险**:仅在自有/授权账号上操作;遵守 Shopee 卖家条款;不绕过任何平台限制。
|
||||
- **GUI 选型待确认**:默认 Tkinter(零依赖),是否改用 PySide/Web 由维护者确认,见 [技术栈](03-tech-stack.md)。
|
||||
- **多账号并行待确认**:MVP 单账号单端口;并行的端口分配与资源占用在 V2 评估。
|
||||
@@ -0,0 +1,68 @@
|
||||
# 技术栈(Tech Stack)
|
||||
|
||||
> “用什么”的统一速查表。选型与理由在此集中维护;“怎么把它们搭起来”见 [架构设计](04-architecture.md)。
|
||||
> 未定项必须标为待定,不要让 agent 在代码里自行决定。
|
||||
|
||||
## 一、技术栈一览
|
||||
|
||||
| 维度 | 选型 | 状态 | 理由 / 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| 语言 | Python 3.10+ | 已定 | 已有脚本均为 Python;标准库够用 |
|
||||
| 运行平台 | Windows(生产);WSL 可用于开发 | 已定 | Chrome 与各 user-data-dir 在 Windows;GUI 与 Chrome 同机,CDP 走 `localhost` |
|
||||
| 浏览器自动化 | 自研 CDP 客户端 `cdp.py` | 已定 | 基于 websocket-client + requests 手写;不引入 playwright/selenium,规避代理与 Origin 坑 |
|
||||
| WebSocket | `websocket-client`(import `websocket`) | 已定 | 讲 CDP 协议;用 `suppress_origin=True` 绕过 403 |
|
||||
| HTTP | `requests` | 已定 | 读 `/json` 拿 tab 列表;`trust_env=False` 忽略代理 |
|
||||
| 浏览器 | Google Chrome(已安装) | 已定 | 带 `--remote-debugging-port` 启动 |
|
||||
| GUI 框架 | Tkinter(标准库,`ttk.Notebook` 5 Tab) | 待定(推荐) | 零依赖、单机够用;如需更现代 UI 再评估 PySide6 / Web |
|
||||
| 应用配置 | `config.json`(JSON,stdlib) | 已定 | 少量应用级设置:Chrome 路径、目录根、端口、DB 路径等 |
|
||||
| 业务数据 | SQLite(stdlib `sqlite3`,`cmshopee.db`) | 已定 | 账号、任务、结果:成行增长、要查询/统计/导出 |
|
||||
| Excel 读写 | `openpyxl` | 已定 | 导入任务、回写结果;stdlib 读不了 .xlsx |
|
||||
| AI 模型注册 | `config/ai_models.json` 多模型清单(HTTP 调用) | 已定(结构) | 每模型 name/category(text/image)/url/model/key/api_type/连接超时;⑤ 设置可增删改+测试连接 |
|
||||
| AI 文本生成 | 选 `default_text_model`(category=text) | 选型在配置 | 提示词+旧标题→新标题 |
|
||||
| AI 图像生成 | 选 `default_image_model`(category=image,image-to-image) | 选型在配置 | 提示词+旧封面→新封面;分辨率 512/1k/2k/4k,返回超时随分辨率 |
|
||||
| 并发 | 标准库 `concurrent.futures.ThreadPoolExecutor` | 已定 | 标题/图片分别按并发数并行;可停止、可重试 |
|
||||
| 图片处理 | `requests`(下载)+ `Pillow`(按分辨率/jpg质量存盘) | 部分待定 | 下载旧封面;新封面按 resolution 生成、jpg_quality 存盘 |
|
||||
| 测试 | `python -m py_compile` + 手动 CDP 验证 | 待完善 | 纯 UI/浏览器交互难以单测;关键逻辑(配置读写、参数拼装)可加 `pytest` |
|
||||
|
||||
## 二、决策记录与演进
|
||||
|
||||
- **CDP 自研而非 playwright**:当前手写 `cdp.py`,因为它零重依赖、完全可控,并已在开发环境绕开了代理(`*_proxy` 指向本地 :1080)和 Chrome 的 Origin 403 两个坑。未来若交互复杂度大幅上升,再评估 playwright。
|
||||
- **GUI 选 Tkinter**:MVP 是单机配置 + 触发操作的轻量界面,Tkinter 零依赖即可。若后续需要表格、拖拽、复杂布局,再评估 PySide6。**该选型在动代码前需维护者确认。**
|
||||
- **存储拆两层**:应用设置进 `config.json`,账号/任务/结果进 SQLite。判据:少量人改无需查询 → 配置文件;成行增长要查询/导出 → DB。同一事实只存一处,不重复。取代早期的 `accounts.json` 方案。
|
||||
- **Excel 用 openpyxl**:运营用真实 .xlsx;stdlib 无法读写 xlsx,引入一个轻依赖比改用 CSV 更贴合用户习惯。
|
||||
- **多账号隔离用独立 user-data-dir,不用 Chrome profile**:profile 共享同一 user-data-dir/进程/调试端口,无法每账号独立 CDP 与并行;独立 user-data-dir 才契合自动化。详见 [架构 3.0](04-architecture.md)。
|
||||
- **快捷方式生成用 PowerShell(无额外依赖)**:用 `WScript.Shell.CreateShortcut` 生成 `.lnk`,不引入 `pywin32` 等依赖。
|
||||
- **AI 服务商待定**:需选支持文本生成 + 图像 image-to-image 的服务;选型要权衡能力、合规(电商主图)、计费、Key 管理。**未确认前不在代码里写死某家 SDK**,先在 `ai.py` 留稳定接口(`gen_title`/`gen_cover`)。
|
||||
- **AI 产出无人工确认**:生成的新标题/新封面经 ③ 执行即直接上线(无确认、无提交开关);本地留档 + 回写 Excel 供追溯。
|
||||
- **不引入数据库(指外部 DB)**:用 stdlib SQLite 足够;不引入 Postgres/MySQL 等。
|
||||
- **生产在 Windows 直跑**:开发期我们用过 WSL→Windows 的 `netsh portproxy`(9333→9222)连 CDP;但 GUI 与 Chrome 都在 Windows 时,直接连 `127.0.0.1:9222`,无需 portproxy。
|
||||
|
||||
## 三、构建与运行命令
|
||||
|
||||
| 用途 | 命令 |
|
||||
| --- | --- |
|
||||
| 安装依赖 | `pip install websocket-client requests openpyxl` |
|
||||
| 语法检查 | `python -m py_compile *.py` |
|
||||
| 跑单账号演示 | `python prototypes/demo.py`(分步)/ `set AUTO=1 && python prototypes/demo.py`(自动) |
|
||||
| 提交更新(真改线上) | `set UPDATE=1 && python prototypes/demo.py` |
|
||||
| 读取某账号 Cookie(调试) | `python prototypes/cookies.py` |
|
||||
|
||||
Windows PowerShell 下设置环境变量与 cmd 不同:
|
||||
|
||||
```powershell
|
||||
# PowerShell
|
||||
$env:AUTO=1; python prototypes/demo.py
|
||||
$env:UPDATE=1; python prototypes/demo.py
|
||||
```
|
||||
|
||||
```cmd
|
||||
:: cmd
|
||||
set AUTO=1 && python prototypes/demo.py
|
||||
```
|
||||
|
||||
## 四、依赖纪律
|
||||
|
||||
- 新增第三方依赖前,先在本文说明用途、替代方案和维护成本。
|
||||
- GUI 框架一旦确定(Tkinter 或其他),固定下来,不允许两套 UI 框架并存。
|
||||
- 不引入第二套浏览器自动化方案(不要 cdp.py 之外再混入 selenium/playwright)。
|
||||
- 不确定的技术选型先更新本文,再进入代码。
|
||||
@@ -0,0 +1,321 @@
|
||||
# 架构设计
|
||||
|
||||
> 本文讲“怎么把技术栈搭起来”:模块职责、存储模型、流水线、CDP 已验证事实、开发顺序。
|
||||
> 具体用了哪些库 / 平台,见 [技术栈](03-tech-stack.md)。
|
||||
|
||||
## 一、系统结构
|
||||
|
||||
Windows 本地桌面自动化工具,无后端服务,5 Tab GUI 驱动一条流水线。
|
||||
|
||||
```text
|
||||
运营(人)
|
||||
|
|
||||
v
|
||||
GUI(Tkinter ttk.Notebook,5 Tab)
|
||||
① 导入采集 ② AI生成 ③ 更新shopee ④ 账号管理 ⑤ 设置
|
||||
|
|
||||
v
|
||||
核心模块(Python)
|
||||
├── appconfig 读应用配置 config.json(Chrome 路径、目录根、AI 配置、端口范围…)
|
||||
├── db SQLite 读写:账号、任务、各阶段结果(cmshopee.db)
|
||||
├── excel openpyxl 导入输入列 / 回写输出列到原 Excel
|
||||
├── config 账号 ↔ user-data-dir 绑定、slug、目录创建
|
||||
├── chrome 按账号拼启动参数、启动/探测 Chrome、生成快捷方式
|
||||
├── cdp CDP 客户端(连接、找/开 tab、执行 JS、拖拽)
|
||||
├── editor 登录检测 / 采集旧标题旧封面 / 改标题 / 换封面 / 点更新
|
||||
└── ai 文本生成(提示词+旧标题→新标题)/ 图像生成(提示词+旧封面→新封面)
|
||||
|
|
||||
v
|
||||
Google Chrome(每账号独立 --user-data-dir + --remote-debugging-port) + AI 服务(外部)
|
||||
|
|
||||
v
|
||||
Shopee 卖家中心页面 / 本地图片目录
|
||||
```
|
||||
|
||||
真实组件:
|
||||
|
||||
- GUI 入口:`gui.py`(待建,Tkinter + `ttk.Notebook`,5 Tab)。
|
||||
- 核心模块:`appconfig.py`、`db.py`、`excel.py`、`config.py`、`chrome.py`、`editor.py`、`ai.py`(待建);CDP 底座 `cdp.py`(已有)。
|
||||
- 已验证脚本(重构进模块):`prototypes/demo.py`、`prototypes/set_title.py`、`prototypes/set_cover.py`、`prototypes/get_title.py`、`prototypes/cookies.py`、`prototypes/inspect_images.py`、`prototypes/grab.py`。
|
||||
- 外部依赖:本机 Google Chrome;Shopee;AI 服务(文本+图像,服务商待定);`openpyxl`。
|
||||
|
||||
## 二、流水线(核心)
|
||||
|
||||
每个任务(Excel 行)依次走过 5 个阶段,状态字段 `stage` 贯穿全程:
|
||||
|
||||
```text
|
||||
imported → collected → generated → applied
|
||||
(导入 Excel) (采集旧数据) (AI 生成新数据) (改 Shopee 并提交)
|
||||
① Tab ① Tab ② Tab ③ Tab
|
||||
```
|
||||
|
||||
- **imported**:openpyxl 解析输入列入库。
|
||||
- **collected**:只读打开商品页,读旧标题、下载旧封面到本地,写 `old_title/old_cover_path`,回写 Excel 旧字段。
|
||||
- **generated**:AI 用提示词+旧标题生成新标题;用提示词+旧封面生成新封面(image-to-image),新图存本地。**无人工确认环节**,生成完即可被 ③ 执行(新图留档本地 + 回写 Excel 供事后追溯)。
|
||||
- **applied**:打开编辑页换标题+封面,**总是点「更新」提交线上**(无开关),回写结果。
|
||||
|
||||
任意阶段失败 → `stage` 不前进、记 `error`、`result/skipped`,不影响其他任务。
|
||||
|
||||
## 三、职责划分
|
||||
|
||||
**GUI(5 Tab)**:见 [routes.md](routes.md)。只做交互与预览,不写业务逻辑;耗时操作走后台线程。**首次未配账号/未登录时,① ③ 执行按钮禁用并提示去 ④。**
|
||||
|
||||
**核心模块**
|
||||
|
||||
- `appconfig`:读写 `config.json`(Chrome 路径、`chrome_user_data_dir` 根、图片目录、AI 配置、端口、DB 路径、默认是否提交)。
|
||||
- `db`:SQLite 读写账号、任务、各阶段结果;建表/迁移。
|
||||
- `excel`:openpyxl 读输入列、把输出列回写原 Excel(处理文件锁)。
|
||||
- `config`:账号 ↔ user-data-dir 绑定;slug;目录创建。
|
||||
- `chrome`:拼接启动命令、启动、探测端口、(可选)生成快捷方式。
|
||||
- `cdp`:连接调试端口、找/开 tab、执行 JS、拖拽、注入文件。
|
||||
- `editor`:登录检测、**采集**(读旧标题、下载旧封面)、改标题、换封面、点更新。
|
||||
- `ai`:`gen_title(prompt, old_title)`、`gen_cover(prompt, old_cover_path)`(外部 AI;服务商待定)。
|
||||
|
||||
**存储(同一事实只存一处)**
|
||||
|
||||
- 应用配置(模型选择、生成参数、目录、Chrome 路径)→ `config.json`。
|
||||
- AI 模型清单(url/模型/密钥/类型/连接超时)→ `config/ai_models.json`(含密钥,必须 gitignore)。
|
||||
- 业务数据(账号、任务、各阶段结果)→ SQLite `cmshopee.db`。
|
||||
- 图片(采集的旧封面、AI 生成的新封面)→ 本地图片目录(路径记在 DB)。
|
||||
- 提示词 → 标题提示词存单文件 `title_prompt.txt`;封面提示词存多模板 `prompts/cover/<名称>.txt`。
|
||||
- 登录态 → 各账号 `chrome_user_data_dir/<slug>/`。
|
||||
|
||||
## 四、多账号隔离方案(决策)
|
||||
|
||||
采用**每账号独立 user-data-dir**(非 Chrome profile)。`--remote-debugging-port` 绑定在 user-data-dir/进程上,profile 方案无法每账号独立 CDP、串号风险高。启动主路径用程序 `subprocess` 直启(`--remote-debugging-port` + `--remote-allow-origins=*` + `--user-data-dir`);可选生成 `.lnk` 快捷方式(PowerShell `WScript.Shell`,参数写在「目标」字段)。
|
||||
|
||||
## 五、数据模型
|
||||
|
||||
### 5.1 应用配置 `config.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"chrome_path": "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
||||
"user_data_root": "chrome_user_data_dir",
|
||||
"image_dir": "images",
|
||||
"db_path": "cmshopee.db",
|
||||
"default_debug_port": 9222,
|
||||
"debug_port_range": [9222, 9260],
|
||||
"cdp_ready_timeout": 60,
|
||||
"ai": {
|
||||
"default_text_model": "GPT-5.5 文本",
|
||||
"default_image_model": "Nano Banana 2",
|
||||
"title_concurrency": 4,
|
||||
"image_concurrency": 4,
|
||||
"retry": 2,
|
||||
"jpg_quality": 90,
|
||||
"resolution": "1k",
|
||||
"resolution_timeouts": { "512": 180, "1k": 240, "2k": 360, "4k": 600 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`ai` 段只放**选择 + 全局生成参数**:
|
||||
|
||||
- `default_text_model` / `default_image_model`:引用 `ai_models.json` 里的模型名(标题用文本模型、封面用图像模型)。
|
||||
- `resolution`:当前分辨率,下拉 `512 / 1k / 2k / 4k`。
|
||||
- `resolution_timeouts`:分辨率 → **等待大模型返回超时(秒)** 的映射;用户选分辨率即自动套用,不单独填。
|
||||
- 模型本身的定义(url/key/类型/连接超时…)在 `config/ai_models.json`,见 5.1b。
|
||||
- 密钥不在 `config.json`:每个模型的 `api_key` 存于 `config/ai_models.json`,加密、打码、gitignore。
|
||||
|
||||
### 5.1b AI 模型清单 `config/ai_models.json`
|
||||
|
||||
模型定义清单("有哪些模型"),与 `config.json` 的 `ai` 段("选了哪个 + 全局参数")职责分开。
|
||||
|
||||
```json
|
||||
{
|
||||
"models": [
|
||||
{
|
||||
"name": "Nano Banana 2", // 服务商/模型名,唯一,作下拉显示与引用键
|
||||
"category": "image", // text | image —— 决定它出现在“标题/图片大模型”哪个下拉
|
||||
"enabled": true,
|
||||
"url": "https://api.vectorengine.ai/v1/chat/completions",
|
||||
"model": "gemini-3.1-flash-image-preview", // 模型 ID
|
||||
"api_key": "***", // 密钥:加密存、UI 打码、不入日志、不进版本库
|
||||
"api_type": "auto", // chat | images_edits | auto —— 决定请求构造方式
|
||||
"connect_timeout_seconds": 30, // 连接该服务超时(每模型,默认 30)
|
||||
"timeout_seconds": 0, // 返回超时:0/留空 = 运行时按 resolution_timeouts 取值
|
||||
"extra_body": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
关键事实:
|
||||
|
||||
- **`category`(文本/图像)是必需的**:角色下拉据此过滤(标题下拉只列 text、图片下拉只列 image),防止错配。
|
||||
- 约束:**至少各有一个 text 与一个 image 模型**;下拉默认最少一项、删到剩一项时禁用「删除」。
|
||||
- `connect_timeout_seconds`(连接超时)属于**模型**;返回超时由分辨率映射决定(不在模型上单设)。
|
||||
- `api_type` 反映不同 API 形状(`chat`/`images_edits`/`auto`),请求构造按它分支。
|
||||
- `name` 唯一;`api_key` 加密存、打码显示。
|
||||
|
||||
### 5.2 SQLite `cmshopee.db`
|
||||
|
||||
```sql
|
||||
-- 账号(④ 账号管理)
|
||||
CREATE TABLE accounts (
|
||||
id INTEGER PRIMARY KEY,
|
||||
account_name TEXT NOT NULL, -- Shopee 登录账号名(展示/参考)
|
||||
alias TEXT UNIQUE NOT NULL, -- 别名,Excel 用它匹配
|
||||
region_host TEXT NOT NULL,
|
||||
slug TEXT UNIQUE NOT NULL, -- user-data-dir 子目录名 [a-z0-9_]
|
||||
user_data_dir TEXT NOT NULL,
|
||||
debug_port INTEGER NOT NULL,
|
||||
password_enc TEXT, -- 加密存,仅参考,不自动登录
|
||||
note TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
last_login_at TEXT
|
||||
);
|
||||
|
||||
-- 任务 + 各阶段结果(贯穿流水线)
|
||||
CREATE TABLE tasks (
|
||||
id INTEGER PRIMARY KEY,
|
||||
batch_id TEXT NOT NULL,
|
||||
source_file TEXT,
|
||||
-- 输入列(Excel)
|
||||
account_name TEXT,
|
||||
alias TEXT NOT NULL,
|
||||
item_id TEXT NOT NULL,
|
||||
-- 采集输出(程序写,改前快照)
|
||||
old_title TEXT,
|
||||
old_cover_path TEXT, -- 旧封面本地图片路径
|
||||
-- AI 输出
|
||||
new_title TEXT,
|
||||
new_cover_path TEXT, -- 新封面本地图片路径
|
||||
-- 应用
|
||||
committed INTEGER DEFAULT 0, -- 是否成功点「更新」提交
|
||||
stage TEXT DEFAULT 'imported', -- imported/collected/generated/applied/failed/skipped
|
||||
error TEXT,
|
||||
updated_at TEXT
|
||||
);
|
||||
```
|
||||
|
||||
关键事实:
|
||||
|
||||
- `alias` 是账号↔任务**唯一关联键**;找不到账号 → stage=skipped,error=别名未匹配,最后弹窗汇总。
|
||||
- `account_name` 仅展示/参考;匹配以 `alias` 为准。
|
||||
- `old_title/old_cover_path`:程序**采集阶段抓取**的快照(输出)。
|
||||
- `new_title/new_cover_path`:**AI 生成**结果(输出),图片落本地,**无人工确认**,生成即可应用。
|
||||
- `committed`:③ 执行时**总是点「更新」提交**;该字段记录提交是否成功。
|
||||
- 各字段**每阶段处理完立即写回 SQLite**(实时落库);阶段结束后批量回写 Excel。
|
||||
|
||||
### 5.3 Excel 模板
|
||||
|
||||
| 列 | 含义 | 输入/输出 |
|
||||
| --- | --- | --- |
|
||||
| 账号名 | Shopee 账号名(展示) | 输入 |
|
||||
| 别名 | 关联 accounts.alias(匹配键) | 输入 |
|
||||
| 商品id | item_id | 输入 |
|
||||
| 旧标题 | 采集到的旧标题 | 输出,回写 |
|
||||
| 旧封面图片路径 | 采集到的旧封面本地路径 | 输出,回写 |
|
||||
| 新标题 | AI 生成的新标题 | 输出,回写 |
|
||||
| 新封面图片路径 | AI 生成的新封面本地路径 | 输出,回写 |
|
||||
| 更新状态 | 处理结果(成功/失败/跳过+原因) | 输出,回写 |
|
||||
|
||||
- 输入列(账号名/别名/商品id)运营填;其余为程序各阶段输出,**回写到原 Excel**(被锁→提示重试/另存副本)。
|
||||
- 别名以此列为匹配权威,不解析文件名。
|
||||
|
||||
### 5.4 本地图片目录
|
||||
|
||||
```text
|
||||
images/<slug>/<item_id>_old.<ext> # 采集下载的旧封面
|
||||
images/<slug>/<item_id>_new.<ext> # AI 生成的新封面
|
||||
```
|
||||
|
||||
路径记入 DB;上传新封面用本地 `<item_id>_new` 文件(Windows 绝对路径传 setFileInputFiles)。
|
||||
|
||||
## 六、关键流程细节
|
||||
|
||||
### 6.1 采集(① Tab,只读)
|
||||
|
||||
- 用账号 Chrome 打开商品页,等就绪,读旧标题(标题输入框 value)。
|
||||
- 旧封面:取第一张 itembox 的 `img.src`(CDN 链接),下载到 `images/<slug>/<item_id>_old`。
|
||||
- 写 `old_title/old_cover_path`、stage=collected;批量回写 Excel 旧字段。
|
||||
|
||||
### 6.2 AI 生成(② Tab)
|
||||
|
||||
单个「开始生成」按钮,**两段式、各自并发**(标题快、图片慢,分开并发更高效):
|
||||
|
||||
1. **并发生成标题**:线程池大小 = `title_concurrency`,用 `default_text_model` 调 `gen_title(标题提示词, old_title)` → new_title。
|
||||
2. **接着并发生成图片**:线程池大小 = `image_concurrency`,用 `default_image_model` 调 `gen_cover(封面提示词, old_cover_path, resolution, jpg_quality)` → 新图存 `images/<slug>/<item_id>_new.jpg`。
|
||||
- 连接超时取该模型 `connect_timeout_seconds`;**返回超时取 `resolution_timeouts[resolution]`**(512→180/1k→240/2k→360/4k→600)。
|
||||
|
||||
- 失败重试:每次调用失败按 `retry` 次重试,仍失败则记 error(不阻塞其余)。
|
||||
- 每条/每张完成**立即 `set_generated` 写库**(实时落库,停止或崩溃不丢已生成的)。
|
||||
- **「停止」**:取消未开始的任务,正在跑的少量完成或中断;停止后可再次「开始生成」对剩余继续。
|
||||
- 进度:`标题 x/n · 封面 x/n · 失败 n`。
|
||||
- 生成后 stage=generated;**无人工确认**。双击任务弹窗查看新旧封面(纯查看),可选对某行 `重生成`;新标题直接用 AI 输出(不可编辑)。
|
||||
- 并发数、重试、分辨率、jpg 质量、模型/Key 均来自 ⑤ 设置(`config.json` 的 `ai` 段)。
|
||||
|
||||
提示词管理:
|
||||
|
||||
- **标题提示词**:单个文本,「保存」写入 `title_prompt.txt`;软件启动时加载该文件回显到输入框(缺失则空)。
|
||||
- **封面提示词**:多模板。下拉选模板(读 `prompts/cover/*.txt`),图标工具栏 新建/保存/另存为/重命名/删除;重名校验、删除二次确认、删空给默认。
|
||||
- **变量**:封面提示词支持占位符 `{旧标题}`、`{新标题}`、`{商品id}`、`{店铺}`,生成前用该任务真实值替换(`render_prompt`)。「插入标题」= 在光标处插入 `{新标题}`;「预览」= 用某条任务的值替换变量后展示,确认实际发送给 AI 的内容。
|
||||
|
||||
### 6.3 应用更新(③ Tab)
|
||||
|
||||
- 对**已生成(generated)任务**:`open_product` → `change_title(new_title)`(如有)→ `replace_cover(new_cover_path)`(如有)→ **总是 `click_update` 提交**。
|
||||
- 串行、单条失败继续;每条立即写 SQLite;全部完成回写 Excel(新字段+状态)+ 弹窗汇总。
|
||||
|
||||
### 6.4 登录检测
|
||||
|
||||
- 打开卖家中心,重定向到登录页或缺会话 Cookie(`SPC_ST`/`SPC_U`)→ 未登录;不自动登录,提示人工登录。
|
||||
|
||||
## 七、CDP 已验证事实(务必遵守)
|
||||
|
||||
| 难点 | 已验证结论 |
|
||||
| --- | --- |
|
||||
| Chrome 启动参数 | 全关后带 `--remote-debugging-port=<port> --remote-allow-origins=* --user-data-dir=<dir>`;缺 allow-origins 则 WebSocket 403 |
|
||||
| 代理干扰 | 清除 `*_proxy`(requests `trust_env=False`),否则连本地 CDP 超时 |
|
||||
| WebSocket Origin | `websocket-client` `suppress_origin=True` |
|
||||
| SPA 就绪 | 不用 load 事件;轮询“标题输入框 + 图片 itembox + 上传输入框”三者都在 |
|
||||
| 标题输入框 | XPath `//input[@class='eds-input__input' and string-length(@modelvalue)>24]` |
|
||||
| 写标题 | 原生 setter + 派发 `input`/`change`;`value`==`modelvalue`==新值 |
|
||||
| 读旧封面 | 第一张 itembox 的 `img.src`(`susercontent` CDN),下载到本地 |
|
||||
| 上传输入框 | `.shopee-image-manager__upload input[type=file]`;`DOM.setFileInputFiles` 传 Windows 路径 |
|
||||
| 上传成功 | 张数 +1 且新图 src 为 `susercontent` |
|
||||
| 封面=第一位 | `Input.dispatchMouseEvent` 拖到第一位,落点 `第一张.left - 0.30*宽` |
|
||||
| 满 9 张 | 上限 9;换封面先删第一张(`.shopee-image-manager__icon--delete`,确认框待实测)|
|
||||
| 更新按钮 | `button.eds-button` 中 `<span>更新</span>`;③ 执行即点提交;禁用态(校验未过)记为失败 |
|
||||
| 登录检测 | 重定向到登录页或缺 `SPC_ST` → 未登录 |
|
||||
|
||||
高风险动作(删满 9 张封面、点更新提交、AI 图直接上线)先在测试商品验证。注意:本设计**无人工确认、无提交开关**,③ 一旦执行即把 AI 标题/封面提交线上,新图本地留档+回写 Excel 是唯一事后追溯手段。
|
||||
|
||||
## 八、推荐开发顺序
|
||||
|
||||
1. **地基**:`editor.py`(含采集)、`appconfig.py`+`config.json`、`db.py`、`.gitignore`。
|
||||
2. **账号与启动**(④):`config` 建目录、`chrome` 启动器/快捷方式、登录保活与检测、账号 CRUD。
|
||||
3. **导入采集**(①):`excel` 导入、采集旧标题/旧封面、回写。
|
||||
4. **AI 生成**(②):`ai` 模块、提示词、对照预览与确认。
|
||||
5. **更新 shopee**(③):对已生成任务换标题+封面、直接提交、回写。
|
||||
6. **设置**(⑤)+ 首次未配账号引导保护。
|
||||
|
||||
## 九、项目结构建议
|
||||
|
||||
```text
|
||||
cmshopee/
|
||||
├── docs/
|
||||
├── config.json # 应用配置(模型选择/生成参数/路径,gitignore)
|
||||
├── config/ai_models.json # AI 模型清单(含密钥,必须 gitignore)
|
||||
├── cmshopee.db # SQLite(账号/任务/结果,gitignore)
|
||||
├── chrome_user_data_dir/ # 各账号 Chrome 配置(含登录态,gitignore)
|
||||
├── images/ # 旧封面/新封面本地图片(建议 gitignore)
|
||||
├── title_prompt.txt # 标题提示词(单文件,启动回显)
|
||||
├── prompts/cover/<名称>.txt # 封面提示词模板(多个)
|
||||
├── appconfig.py / db.py / excel.py / config.py / chrome.py / editor.py / ai.py / prompts.py / gui.py # 待建
|
||||
├── cdp.py # CDP 底座(正式模块,已有)
|
||||
└── prototypes/ # 已验证原型/探查脚本(demo/set_*/get_title/cookies/inspect_images/grab/1.py)
|
||||
# 逻辑待并入 editor.py 后清理;见 prototypes/README.md
|
||||
```
|
||||
|
||||
> `config.json`、`config/ai_models.json`、`cmshopee.db`、`chrome_user_data_dir/`、`images/` 含密钥/凭证/业务数据,必须 gitignore。
|
||||
|
||||
## 十、架构纪律
|
||||
|
||||
- CDP 交互事实变化同步第七节。
|
||||
- 存储边界:应用设置→config.json,账号/任务/结果→SQLite,图片→本地目录并记路径于 DB,登录态→user-data-dir;同一事实只存一处。
|
||||
- 别名是账号↔任务唯一关联键。
|
||||
- 密码与 AI Key 加密存、不外传、不写日志;不自动登录。
|
||||
- AI 生成内容直接用于 ③ 更新(无人工确认);新图本地留档 + 回写 Excel 以备追溯。
|
||||
- 高风险模块先单独验证,再接入流水线。
|
||||
@@ -0,0 +1,84 @@
|
||||
# 编码规则(Coding Rules)
|
||||
|
||||
> 每次写代码前先读完本文件。这是让 AI 不跑偏、代码质量稳定的硬约束。
|
||||
> 与技术细节冲突时,以 [技术栈](03-tech-stack.md) / [架构设计](04-architecture.md) 的事实为准;与“该不该做”冲突时,以 [需求](02-requirements.md) 为准。
|
||||
|
||||
## 0. 黄金法则
|
||||
|
||||
1. **不臆造**:选择器、字段、文件、接口不确定就查证或先用 `prototypes/inspect_images.py` 探查页面,不要猜 class 名。
|
||||
2. **守范围**:只做当前任务要求的事,不顺手加批量/并行等后续功能。
|
||||
3. **照架构**:复用 `cdp.py`,遵守 `04-architecture.md` 第四节“已验证结论”,不另起一套 CDP 交互。
|
||||
4. **小步改**:一次只解决一个问题,不夹带无关重构。
|
||||
5. **可验证**:改完必须能 `py_compile`,关键路径能在测试商品上跑通,对得上验收标准。
|
||||
|
||||
## 1. 动手前
|
||||
|
||||
- 按链路确认:`vision` -> `requirements` -> `tech-stack` -> `architecture` -> `tasks`。
|
||||
- 找到本任务对应的验收标准,写之前就知道“怎么算做对”。
|
||||
- 复用优先:已有 `cdp.py` 的 `CDP`、`find_product_tab`、`create_tab`、`drag`;已有 `prototypes/demo.py`/`prototypes/set_*.py` 中验证过的 JS 片段。
|
||||
- 需求含糊或改动会偏离已验证事实时,先问。
|
||||
|
||||
## 2. 事实来源纪律
|
||||
|
||||
- CDP 交互只信 `04-architecture.md` 第四节的“已验证结论”和真实页面探查结果。
|
||||
- 不从旧脚本注释或记忆里推断仍然有效的选择器;Shopee 页面可能已变,必要时重新探查。
|
||||
- 不虚构 Shopee 接口、字段、按钮文案。
|
||||
- 选择器 / 流程变化必须同步更新 `04-architecture.md` 和相关任务。
|
||||
|
||||
## 3. 范围纪律
|
||||
|
||||
- MVP 只做 `02-requirements.md` 中列为 P0 的功能(账号配置、绑定目录、启动登录、加载页、改标题、换封面、可选更新)。
|
||||
- V2 / V3 功能(批量、多账号并行、日志、规则模板)只记录,不实现。
|
||||
- 需求明确排除的非目标(自动登录、绕风控、爬取、数据库)不得实现。
|
||||
|
||||
## 4. 架构纪律
|
||||
|
||||
- 技术栈以 `03-tech-stack.md` 为准;GUI 框架未确认前不要写大量 UI 代码。
|
||||
- 新增依赖前先说明理由;优先标准库(Tkinter、json、sqlite3、subprocess),Excel 用 openpyxl。
|
||||
- 存储边界:应用设置进 `config.json`,账号/任务/结果进 SQLite,登录态只在 user-data-dir,同一事实只存一处。
|
||||
- 模块职责以 `04-architecture.md` 为准:GUI 不写业务逻辑,业务在 `appconfig/db/excel/config/chrome/cdp/editor`。
|
||||
- 模块/CLI 合约以 `api.md` 为准。
|
||||
|
||||
## 5. 代码规范
|
||||
|
||||
- 标识符使用英文;UI 文案、注释、文档保持中文,与现有脚本一致。
|
||||
- 错误必须处理:CDP 超时、tab 找不到、上传失败、按钮禁用都要给明确提示,不吞错。
|
||||
- 注释解释“为什么”(尤其 CDP 的坑:代理、Origin、就绪判断、拖拽落点),不复述“做了什么”。
|
||||
- 复用现有脚本里已验证的 JS 字符串,不重写出不一致的版本。
|
||||
|
||||
## 6. 测试与验证
|
||||
|
||||
完成前至少检查:
|
||||
|
||||
- [ ] `python -m py_compile` 通过。
|
||||
- [ ] 涉及 CDP 的改动,在测试商品(ITEM_ID 51100639510)上实跑验证。
|
||||
- [ ] 对得上需求验收标准(如标题 `value`+`modelvalue` 双等于、封面在第一位)。
|
||||
- [ ] 没有夹带无关改动。
|
||||
- [ ] 涉及选择器/流程/配置 schema 变化时,文档已同步。
|
||||
- [ ] 回复里如实说明跑了什么命令、结果如何。
|
||||
|
||||
```bash
|
||||
python -m py_compile *.py
|
||||
python prototypes/demo.py # 单账号闭环验证(不提交)
|
||||
```
|
||||
|
||||
## 7. 绝不
|
||||
|
||||
- 绝不把真实账号、密码、Cookie、token 写进代码、文档或日志。
|
||||
- 绝不明文存密码;DB 里密码必须加密,且加密密钥不与密文同存。
|
||||
- 绝不把 `config.json`、`cmshopee.db`、`chrome_user_data_dir/` 提交版本库。
|
||||
- 绝不自动登录 / 自动填账号密码;登录由人工完成,程序只检测登录态。
|
||||
- 绝不在没有显式确认/开关的情况下点击「更新」提交线上。
|
||||
- 绝不擅自删除用户文件或重置 user-data-dir。
|
||||
- 绝不为通过验证而降低验收标准(如不验证 `modelvalue` 就当改成功)。
|
||||
|
||||
## 8. 安全与合规
|
||||
|
||||
- 登录凭证只存在于各账号 user-data-dir;不导出、不外传、不写入配置或日志。
|
||||
- 涉及 Shopee 时,遵守 `04-architecture.md` 写明的页面规则与限流边界;不高频批量、不绕风控/验证码。
|
||||
- 高风险动作(删满 9 张的封面、点击更新)必须有显式确认或开关,并先在测试商品验证。
|
||||
- 自动化默认支持“只改不提交”(dry-run 思路);提交、删除等不可逆动作要可控、可回退(刷新还原)。
|
||||
|
||||
## 9. 拿不准就问
|
||||
|
||||
问题要具体:说明卡在哪、有哪些选项、倾向哪个及原因。尤其 GUI 框架、端口分配、删图确认框结构这类影响后续的决策,先确认再写。
|
||||
@@ -0,0 +1,91 @@
|
||||
# 任务看板(Tasks)
|
||||
|
||||
> 把目标拆成小步、可独立交付的任务,让 AI 一步一步开发,避免一次生成整个项目。
|
||||
|
||||
## 使用规则
|
||||
|
||||
1. **一次只做一个任务**:每轮只领取一个 `TODO` 且依赖均 `DONE` 的任务,取最靠前的。
|
||||
2. **做完即停**:完成、自测通过、改 `DONE` 后汇报。
|
||||
3. **不跳步**:依赖未完成不开工。
|
||||
4. **完成定义**:以 [编码规则](05-coding-rules.md) 验证清单为准。
|
||||
5. **动手前**读 `00-ai-start-here.md`、`05-coding-rules.md`、`current-state.md`、`04-architecture.md`(尤其第七节已验证事实、第二节流水线)。
|
||||
6. **完成后**同步本文状态,追加 [`../progress.md`](../progress.md),覆盖 [`current-state.md`](current-state.md)。
|
||||
|
||||
## 状态图例
|
||||
|
||||
`TODO` · `DOING`(同一时间最多 1 个)· `DONE` · `BLOCKED`
|
||||
|
||||
---
|
||||
|
||||
## Phase 0 · 地基(模块化 + 存储)
|
||||
|
||||
| ID | 任务 | 依赖 | 验收要点 | 状态 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| T-001 | `editor.py`:改标题/换封面/点更新/登录检测/**采集(读旧标题+旧封面下载)**/apply_task,复用 `cdp.py` | - | 函数可调用,在测试商品跑通;与 `prototypes/demo.py` 行为一致 | TODO |
|
||||
| T-002 | `appconfig.py` + `config.json`(含 image_dir、ai 段、端口等默认值) | - | 读写正常;不存在则写默认;AI Key 加密占位 | TODO |
|
||||
| T-003 | `db.py` + SQLite 建表(accounts/tasks,含各阶段字段) | - | `init_db` 幂等;账号/任务/各 set_* 可用;schema 同架构 5.2 | TODO |
|
||||
| T-004 | `.gitignore`:排除 `config.json`、`cmshopee.db`、`chrome_user_data_dir/`、`images/` | T-002, T-003 | 凭证/业务/图片不被提交 | TODO |
|
||||
|
||||
## Phase 1 · 账号管理(④)
|
||||
|
||||
| ID | 任务 | 依赖 | 验收要点 | 状态 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| T-101 | `config` 生成 slug + 创建 `chrome_user_data_dir/<slug>` | T-003 | 别名→唯一 slug;目录按需建;路径绝对化 | TODO |
|
||||
| T-102 | `chrome.py` 启动器:拼参数并启动、探测端口 | T-101, T-002 | 含三参数;端口就绪可探测 | TODO |
|
||||
| T-103 | 首次登录保活 + 登录检测 `is_logged_in` | T-102, T-001 | 关闭再启动免重登;登录/未登录判断准确 | TODO |
|
||||
| T-104 | 五 Tab 主窗口骨架(`ttk.Notebook`,5 Tab 空壳) | T-002 | 五个 Tab 按顺序可切换 | TODO |
|
||||
| T-105 | Tab④ 账号增删改(账号名/别名/端口/密码加密)+ 启动登录 + 检测登录 | T-104, T-101, T-103 | 增删改入库、建目录;密码加密默认打码;状态列刷新 | TODO |
|
||||
| T-106 | 可选:为账号生成桌面快捷方式 | T-102 | `.lnk` 目标含该账号参数;双击进对应账号 | TODO |
|
||||
|
||||
## Phase 2 · 导入采集(①)
|
||||
|
||||
| ID | 任务 | 依赖 | 验收要点 | 状态 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| T-201 | `excel.py` 导入:解析多文件输入列入库 | T-003 | 按模板解析账号名/别名/商品id;脏数据容错;写 tasks(batch_id) | TODO |
|
||||
| T-202 | Tab① 任务列表 + 导入按钮 + 别名匹配标记 | T-201, T-105 | Treeview 显示账号/别名/商品id/阶段;未匹配标“略过” | TODO |
|
||||
| T-202b | Tab① 导入汇总栏 | T-202 | 导入后显示 文件数/解析行数/有效/无效/匹配(按账号)/未匹配;未匹配可点击筛出 | TODO |
|
||||
| T-203 | 采集旧标题+旧封面(只读),下载图片,立即写库 | T-202, T-001 | 逐条 set_collected;旧封面下载到 `images/<slug>/`;未登录/未匹配略过记原因 | TODO |
|
||||
| T-204 | 回写旧字段到原 Excel(含文件锁处理) | T-203, T-201 | 旧标题/旧封面回写原文件;被锁提示重试或 export_copy | TODO |
|
||||
| T-205 | 首次未配账号/未登录的引导保护 | T-105, T-203 | 无账号/未登录时 ① 执行按钮禁用并提示去④ | TODO |
|
||||
|
||||
## Phase 3 · AI 生成(②)
|
||||
|
||||
| ID | 任务 | 依赖 | 验收要点 | 状态 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| T-301 | 确定 AI 服务商/模型并接入 `ai.py`(`gen_title`/`gen_cover`,带重试/分辨率/jpg质量) | T-002 | 可调;Key 加密读取;`gen_cover` 支持 resolution+jpg_quality;失败按 retry 重试;错误明确 | TODO |
|
||||
| T-302 | Tab② 左右布局:左提示词(标题/封面),右按批次/店铺/状态筛选 + 任务列表 | T-301, T-203 | 左 ~1/4 提示词多行;右筛选+列表(店铺/商品id/旧标题/新标题/状态) | TODO |
|
||||
| T-302p | `prompts.py` + Tab② 提示词管理 | T-302 | 标题保存/启动回显 title_prompt.txt;封面多模板(下拉+新建/保存/另存为/重命名/删除,存 prompts/cover/);插入 `{新标题}`;预览变量替换;render_prompt 接入生成 | TODO |
|
||||
| T-303 | Tab② 开始生成(单按钮)+ 停止 + 进度:**先并发标题再并发图片** | T-302 | `generate_batch` 先 title_concurrency 并发标题、再 image_concurrency 并发图片;每条 set_generated 立即写库;停止可取消未开始项;进度 标题/封面/失败 计数;双击弹窗看新旧封面 | TODO |
|
||||
|
||||
## Phase 4 · 更新 shopee(③)
|
||||
|
||||
| ID | 任务 | 依赖 | 验收要点 | 状态 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| T-401 | Tab③ 列出已生成任务 + 按批次/店铺/状态筛选 | T-303 | 顶部批次/店铺/状态筛选;「开始更新」仅作用于当前筛选结果;状态=失败可重试;无提交开关;警示“执行即提交线上” | TODO |
|
||||
| T-402 | 串行执行 apply:换标题+换封面+**总是点更新提交**,单条失败继续,立即写库 | T-401, T-001 | 逐条 set_applied;恒提交;失败继续;未登录引导保护 | TODO |
|
||||
| T-403 | 回写结果到原 Excel(新标题/新封面/更新状态)+ 结束弹窗汇总 | T-402, T-204 | 回写原文件(锁处理);弹窗 成功/失败/略过 | TODO |
|
||||
|
||||
## Phase 5 · 设置与收尾
|
||||
|
||||
| ID | 任务 | 依赖 | 验收要点 | 状态 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| T-501 | Tab⑤ 设置 · AI 模型管理(master-detail):模型下拉 + 新增/删除 + 详情(服务商名/类别/api_type/模型ID/网址/密钥打码/连接超时) + 测试连接 | T-002 | 读写 `config/ai_models.json`;name 唯一;类别 text/image;至少各一个;密钥打码加密;测试连接可用 | TODO |
|
||||
| T-501b | Tab⑤ 设置 · 角色与生成参数:标题大模型/图片大模型下拉、分辨率(512/1k/2k/4k)、返回超时随分辨率、并发/重试/jpg质量、路径/端口 | T-501 | 角色下拉按类别过滤;分辨率选后返回超时按 resolution_timeouts 自动;持久化 config.json | TODO |
|
||||
| T-502 | 满 9 张封面:删第一张再上传 | T-001 | 实测删除确认框并处理;满 9 张流程正确 | TODO |
|
||||
| T-503 | 密码 / AI Key 加密密钥方案落地 | T-105, T-501 | 密钥不与密文同存;说明威胁模型 | TODO |
|
||||
| T-504 | 多账号并行 / dry-run / 运行日志(V2) | T-402 | 端口不冲突;dry-run 只预览;操作留痕 | TODO |
|
||||
|
||||
## 里程碑
|
||||
|
||||
- M1:editor(含采集)模块化、config.json + SQLite 就绪(Phase 0)。
|
||||
- M2:账号管理可用、能登录与检测(Phase 1)。
|
||||
- M3:导入 + 采集 + 回写旧数据(Phase 2)。
|
||||
- M4:AI 生成新标题/新封面(Phase 3)。
|
||||
- M5:对已生成任务更新 Shopee(直接提交)+ 回写结果(Phase 4)。
|
||||
|
||||
## 待办池(Backlog)
|
||||
|
||||
- 标题规则/提示词模板库。
|
||||
- 上传弹裁剪框商品的自动裁剪。
|
||||
- 账号登录态健康巡检。
|
||||
- AI 生成图的合规/质量自检。
|
||||
@@ -0,0 +1,36 @@
|
||||
# 项目文档导航
|
||||
|
||||
> cmshopee 的 harness coding 文档集合。agent 开始编程前从 [`00-ai-start-here.md`](00-ai-start-here.md) 进入。
|
||||
|
||||
## 一句话定位
|
||||
|
||||
cmshopee 是一个给**电商运营**使用的 Windows 桌面自动化工具,用于**管理多个 Shopee 卖家账号、并用 CDP 驱动 Chrome 批量修改商品标题、替换商品封面图**。第一版先跑通「配置账号 → 绑定独立 Chrome 配置目录(保留登录态)→ 用某账号打开指定商品详情页 → 改标题 + 换封面(可选提交更新)」闭环。
|
||||
|
||||
## 文档导航
|
||||
|
||||
- [`../AGENTS.md`](../AGENTS.md):Codex / 通用 AI coding agent 的仓库级入口。
|
||||
- [`../CLAUDE.md`](../CLAUDE.md):Claude Code 的薄入口,具体规则以 `AGENTS.md` 为准。
|
||||
- [`../progress.md`](../progress.md):执行历史流水,只追加记录任务执行、验证、阻塞和决策。
|
||||
- [AI 开发入口](00-ai-start-here.md):agent 每次开始工作的入口、阅读顺序和任务领取规则。
|
||||
- [项目愿景](01-vision.md):为什么做、为谁做、产品原则、非目标。
|
||||
- [需求](02-requirements.md):要什么、用户故事、验收标准。
|
||||
- [技术栈](03-tech-stack.md):语言、运行平台、CDP 方案、GUI 选型和运行命令。
|
||||
- [架构设计](04-architecture.md):模块职责、账号与配置目录数据模型、CDP 已验证事实和开发顺序。
|
||||
- [编码规则](05-coding-rules.md):写代码前必须遵守的硬约束。
|
||||
- [任务看板](06-tasks.md):按依赖拆分的小任务,agent 每轮只做一个。
|
||||
- [模块 / CLI 合约](api.md):本地模块接口、Chrome 启动参数、账号配置 schema。
|
||||
- [界面与流程结构](routes.md):GUI 窗口、操作流程、按钮职责(无前端路由,用 GUI 流程替代)。
|
||||
- [当前实现状态](current-state.md):当前代码现实、可运行命令、下一步可做任务。
|
||||
|
||||
## 任务 / 进度 / 当前状态
|
||||
|
||||
- `06-tasks.md` 维护任务看板:任务 ID、依赖、验收要点和状态。
|
||||
- `../progress.md` 维护执行进度:每轮实际做了什么、跑了什么验证、遇到什么阻塞、做了什么决策。
|
||||
- `current-state.md` 维护当前快照:当前目录、可运行命令、已完成摘要和下一个可领取任务。
|
||||
|
||||
## 维护原则
|
||||
|
||||
- 需求变化先改文档,再改代码。
|
||||
- CDP 交互的关键事实(选择器、就绪判断、上传/拖拽方式)一旦定稿,写入 `04-architecture.md`,代码不得另起一套。
|
||||
- 涉及账号、登录凭证、Cookie 时,文档与代码只用占位符或路径引用,绝不写入真实值。
|
||||
- agent 开始新任务前,必须从 `00-ai-start-here.md` 进入。
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
# 模块 / CLI 合约
|
||||
|
||||
> 本项目无后端 API,全部为**本地 Python 模块合约**。实现前可细化,但不要另起一套不兼容接口。
|
||||
|
||||
## 通用约定
|
||||
|
||||
- 形态:本地函数 + 子进程(Chrome)+ CDP(`127.0.0.1:<port>`)+ SQLite + openpyxl + AI 服务调用。
|
||||
- 编码:UTF-8;传 Chrome / `setFileInputFiles` 的路径为 **Windows 绝对路径**。
|
||||
- 凭证:登录态在 user-data-dir;密码、AI Key 加密存于 config/DB,不出现在日志/导出明文。
|
||||
- 失败处理:抛带中文说明的异常或返回状态字段;GUI 负责提示,不静默吞错。
|
||||
|
||||
## appconfig 模块(`appconfig.py`,待建)
|
||||
|
||||
读写 `config.json`(schema 见 [架构 5.1](04-architecture.md))。
|
||||
|
||||
```python
|
||||
load_config(path="config.json") -> dict # 不存在则写默认
|
||||
chrome_path() -> str
|
||||
user_data_root() -> str
|
||||
image_dir() -> str
|
||||
db_path() -> str
|
||||
ai_config() -> dict # default_text_model/default_image_model/
|
||||
# title_concurrency/image_concurrency/retry/jpg_quality/
|
||||
# resolution/resolution_timeouts
|
||||
response_timeout() -> int # = resolution_timeouts[resolution](返回超时,随分辨率)
|
||||
|
||||
# AI 模型清单 config/ai_models.json(含密钥;CRUD 由 ⑤ 设置)
|
||||
list_ai_models(category=None) -> list[dict] # category=text/image 过滤;含 connect_timeout_seconds 等
|
||||
add_ai_model(model) -> None # name 唯一校验
|
||||
update_ai_model(name, **fields) -> None
|
||||
delete_ai_model(name) -> None # 至少各留一个 text+image;删到剩一禁用
|
||||
test_ai_model(name) -> dict # 「测试连接」:用 key/url/model 发最小请求 -> {ok, error}
|
||||
get_model(name) -> dict # 解密 api_key 供调用
|
||||
```
|
||||
|
||||
## db 模块(`db.py`,待建)
|
||||
|
||||
SQLite 读写,表见 [架构 5.2](04-architecture.md)。
|
||||
|
||||
```python
|
||||
init_db(path)
|
||||
# 账号
|
||||
list_accounts() -> list[Account]
|
||||
get_account_by_alias(alias) -> Account|None
|
||||
add_account(account_name, alias, region_host, debug_port, password_enc=None, note=None) -> Account
|
||||
update_account(alias, **fields) -> None
|
||||
delete_account(alias) -> None
|
||||
# 任务 / 各阶段结果
|
||||
insert_tasks(batch_id, rows) -> int # 写输入列
|
||||
list_tasks(batch_id=None, stage=None) -> list[Task]
|
||||
set_collected(task_id, old_title, old_cover_path) -> None # 采集结果,立即写
|
||||
set_generated(task_id, new_title, new_cover_path) -> None # AI 结果,立即写
|
||||
set_applied(task_id, committed, error=None) -> None # 更新结果,立即写
|
||||
```
|
||||
|
||||
`stage` 随各 set_* 推进(imported→collected→generated→applied);任意步失败写 `error` 且 stage 标 failed/skipped。无 confirmed 阶段。
|
||||
|
||||
## excel 模块(`excel.py`,待建,依赖 openpyxl)
|
||||
|
||||
```python
|
||||
import_tasks(file_paths: list[str]) -> dict
|
||||
# 只解析【输入列】:账号名、别名、商品id(+ source_file);输出列运行时回写
|
||||
# -> {"rows": [...], "stats": {"files": int, "total": int, "valid": int, "invalid": int}}
|
||||
# invalid = 缺别名/商品id 或脏数据的行
|
||||
|
||||
match_summary(rows: list[dict], accounts: list) -> dict
|
||||
# 用 accounts 的别名对 rows 做匹配统计(导入汇总栏用)
|
||||
# -> {"matched": int, "unmatched": int, "by_account": {别名: 行数}, "unmatched_aliases": [..]}
|
||||
|
||||
write_back(batch_id, excel_path) -> str
|
||||
# 把【旧标题/旧封面/新标题/新封面/更新状态】批量回写到【原 Excel】
|
||||
# 原文件被占用(锁) → 抛错,调用方提示“请关闭后重试”,或改用 export_copy
|
||||
export_copy(batch_id, out_path) -> str # 退路:另存新结果文件,不动原文件
|
||||
```
|
||||
|
||||
列模板见 [架构 5.3](04-architecture.md);别名以“别名”列为权威。
|
||||
|
||||
## config 模块(`config.py`,待建)
|
||||
|
||||
```python
|
||||
make_slug(alias) -> str # 别名→唯一 slug [a-z0-9_]
|
||||
ensure_user_data_dir(slug) -> str # chrome_user_data_dir/<slug> 绝对路径,按需创建
|
||||
```
|
||||
|
||||
## chrome 模块(`chrome.py`,待建)
|
||||
|
||||
```python
|
||||
build_launch_args(account) -> list[str] # chrome + --remote-debugging-port + --remote-allow-origins=* + --user-data-dir
|
||||
launch_chrome(account) -> subprocess.Popen
|
||||
wait_debug_ready(port, timeout=60) -> bool
|
||||
is_running(port) -> bool
|
||||
create_shortcut(account, dest_dir=None) -> str # 可选 .lnk,PowerShell WScript.Shell
|
||||
```
|
||||
|
||||
## cdp 模块(`cdp.py`,已实现)
|
||||
|
||||
```python
|
||||
CDP_HOST: str
|
||||
http_get(path); find_product_tab(item_id); create_tab(url)
|
||||
class CDP: send/ev/val/object_id/drag/close # suppress_origin、trust_env=False
|
||||
```
|
||||
|
||||
## editor 模块(`editor.py`,待建,重构自现有脚本)
|
||||
|
||||
```python
|
||||
is_logged_in(account) -> bool # 重定向登录页或缺 SPC_ST → False
|
||||
open_product(account, item_id) -> CDP # 连端口、导航商品页、等就绪
|
||||
|
||||
# 采集(只读)
|
||||
read_title(cdp) -> str
|
||||
read_cover_src(cdp) -> str # 第一张 itembox 的 img.src
|
||||
download_cover(src, out_path) -> str # 下载旧封面到本地
|
||||
collect(account, task) -> dict # -> {old_title, old_cover_path}
|
||||
|
||||
# 应用
|
||||
change_title(cdp, new_title) -> dict # {ok, value, modelvalue},要求三者相等
|
||||
replace_cover(cdp, image_win_path) -> dict # 上传→等CDN→拖第一位;满9张先删第一张
|
||||
click_update(cdp) -> dict # {clicked, reason};禁用则记失败
|
||||
apply_task(account, task) -> dict # 对已生成任务:换标题+换封面+点「更新」提交(恒提交)
|
||||
# -> {committed, error}
|
||||
```
|
||||
|
||||
## ai 模块(`ai.py`,待建,外部 AI,服务商待定)
|
||||
|
||||
```python
|
||||
gen_title(title_prompt, old_title, retry=2) -> str
|
||||
# 文本生成:提示词 + 旧标题 → 新标题
|
||||
|
||||
gen_cover(cover_prompt, old_cover_path, out_path, resolution, jpg_quality, retry=2) -> str
|
||||
# 图像生成(image-to-image):提示词 + 旧封面 → 新封面,按 resolution 生成、jpg_quality 存盘,返回路径
|
||||
|
||||
generate_batch(tasks, prompts, ai_cfg, on_progress, should_stop) -> None
|
||||
# 编排:先以 title_concurrency 线程池并发跑 gen_title,再以 image_concurrency 并发跑 gen_cover
|
||||
# 每条完成即 db.set_generated(实时落库);should_stop() 为真则取消未开始项
|
||||
# on_progress(标题完成数, 封面完成数, 失败数) 回调刷新进度
|
||||
```
|
||||
|
||||
要点:
|
||||
|
||||
- 标题用 `default_text_model`、封面用 `default_image_model`(`appconfig.get_model` 取定义,含 url/key/api_type)。
|
||||
- 连接超时 = 模型 `connect_timeout_seconds`;**返回超时 = `appconfig.response_timeout()`(随分辨率:512/1k/2k/4k → 180/240/360/600)**。
|
||||
- 并发数/重试/分辨率/jpg 质量来自 `appconfig.ai_config()`;Key 加密存、不入日志。
|
||||
- 标题快、图片慢:分两段、各用各自并发数;失败按 `retry` 重试,仍失败记 error 不阻塞其余。
|
||||
- 调用有成本与失败可能:超时、限流、内容安全拒绝都要返回明确错误。
|
||||
- 生成结果**直接用于 ③ 更新**(无人工确认);本地留档 + 回写 Excel 供追溯。
|
||||
|
||||
## prompts 模块(`prompts.py`,待建)
|
||||
|
||||
```python
|
||||
# 标题提示词:单文件
|
||||
load_title_prompt(path="title_prompt.txt") -> str # 启动回显;缺失返回 ""
|
||||
save_title_prompt(text, path="title_prompt.txt") -> None # 「保存」按钮
|
||||
|
||||
# 封面提示词:多模板(prompts/cover/<名称>.txt)
|
||||
list_cover_templates() -> list[str] # 模板名列表(下拉用)
|
||||
load_cover_template(name) -> str
|
||||
save_cover_template(name, text) -> None # 保存 / 另存为
|
||||
rename_cover_template(old, new) -> None # 重名校验,重复则报错
|
||||
delete_cover_template(name) -> None # 删除(二次确认由 GUI 负责)
|
||||
|
||||
# 变量替换
|
||||
render_prompt(template_text, task) -> str
|
||||
# 占位符 {旧标题}/{新标题}/{商品id}/{店铺} → 该任务真实值;预览与生成时调用
|
||||
```
|
||||
|
||||
要点:
|
||||
|
||||
- 「插入标题」在封面提示词光标处插入 `{新标题}`;「预览」对选中任务调用 `render_prompt` 后展示。
|
||||
- 生成封面时 `gen_cover` 的 prompt = `render_prompt(当前封面模板, task)`。
|
||||
- 模板与 `title_prompt.txt` 均为可手改的纯文本文件。
|
||||
|
||||
## CLI / 触发合约(现有脚本,过渡期保留)
|
||||
|
||||
```bash
|
||||
python prototypes/demo.py # 单账号闭环:改标题+换封面,不提交
|
||||
set AUTO=1 && python prototypes/demo.py
|
||||
set UPDATE=1 && python prototypes/demo.py # 走完点击「更新」提交
|
||||
```
|
||||
|
||||
环境变量:`CDP_HOST`、`ITEM_ID`、`IMG_WIN`、`NEW_TITLE`、`AUTO`、`UPDATE`。
|
||||
|
||||
## 待实现时确认
|
||||
|
||||
- AI 服务商/模型/计费;图像 image-to-image 能力与合规。
|
||||
- 密码、AI Key 加密的密钥来源(机器派生 / 主口令)。
|
||||
- 满 9 张删除封面的确认框选择器(需实测)。
|
||||
- Excel 缺列/脏数据容错(整文件拒绝 vs 逐行跳过)。
|
||||
- 旧封面下载的图片格式/扩展名处理。
|
||||
@@ -0,0 +1,85 @@
|
||||
# 当前实现状态
|
||||
|
||||
> 本文是可覆盖的当前快照,记录代码与任务看板的现实状态。
|
||||
> 历史执行流水追加到 [`../progress.md`](../progress.md),不在本文重复完整日志。
|
||||
|
||||
## 当前快照
|
||||
|
||||
- 日期:2026-06-25
|
||||
- 阶段:单账号 CDP 流程已验证;多账号管理 + 5 Tab GUI + Excel/AI 流水线为既定设计,尚未开始编码。
|
||||
- 技术栈:Python 3.10+,自研 CDP(websocket-client + requests),SQLite(sqlite3)+ `config.json` + openpyxl + AI(服务商待定),GUI Tkinter 5 Tab(待最终确认)。
|
||||
- 生产代码:尚无 `appconfig/db/excel/config/chrome/editor/gui`;现有为验证脚本 + `cdp.py`。
|
||||
- 测试:以 `py_compile` + 在测试商品上手动跑 `prototypes/demo.py` 为主,无自动化测试。
|
||||
- 数据:无 `config.json`、`cmshopee.db`、`chrome_user_data_dir/`(待 Phase 0/1 建立)。
|
||||
|
||||
## 既定设计要点(文档已定)
|
||||
|
||||
- GUI:**5 Tab 流水线**,顺序 ① 导入采集 → ② AI生成 → ③ 更新shopee → ④ 账号管理 → ⑤ 设置。
|
||||
- 流水线阶段:imported → collected(采集旧标题/旧封面+下载+回写)→ generated(AI 提示词生成新标题/新封面)→ applied(改 Shopee 并直接提交)。**无 confirmed、无提交开关。**
|
||||
- 存储:`config.json`(应用设置含 AI Key)+ SQLite `cmshopee.db`(账号/任务/各阶段结果)+ openpyxl(Excel)+ 本地 `images/`(旧/新封面)。
|
||||
- 多账号隔离:每账号独立 user-data-dir(非 profile)。
|
||||
- 账号↔任务绑定:以 Excel“别名”列为权威;未匹配略过,结束弹窗汇总。
|
||||
- 执行:多账号串行、单条失败继续;③ 执行即点「更新」提交线上(无确认、无开关)。
|
||||
- AI:服务商待定;生成内容直接用于更新,本地留档+回写 Excel 供追溯。
|
||||
- 登录:人工登录 + 程序检测,不自动登录;首次未配账号/未登录时 ① ③ 禁用并引导去④。
|
||||
|
||||
## 当前目录要点
|
||||
|
||||
| 路径 | 状态 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `docs/` | 已有 | 本 harness coding 文档集合 |
|
||||
| `cdp.py` | 已有 | CDP 底座:连接/找 tab/开 tab/执行 JS/拖拽(正式模块,留根目录) |
|
||||
| `prototypes/` | 已有 | 已验证原型/探查脚本(demo/set_title/set_cover/get_title/cookies/inspect_images/grab/1.py),逻辑待并入 `editor.py` 后清理;见 `prototypes/README.md` |
|
||||
| `chrome-remote-debug-lan.md` | 已有 | WSL→Windows CDP 转发排查记录 |
|
||||
| `appconfig.py` / `db.py` / `excel.py` / `config.py` / `chrome.py` / `editor.py` / `gui.py` | 待建 | Phase 0-3 产出 |
|
||||
| `config.json` / `cmshopee.db` / `chrome_user_data_dir/` | 待建 | 含配置/业务/凭证,须 gitignore |
|
||||
|
||||
## 已验证能力(单账号)
|
||||
|
||||
- CDP 连接 Chrome、遍历 tab、读取 shopee.tw Cookie。
|
||||
- 找到/新建商品详情页 tab,等编辑器就绪。
|
||||
- 改标题:原生 setter + 派发事件,`value` 与 `modelvalue` 双等于新值。
|
||||
- 换封面:`setFileInputFiles` 上传(`.shopee-image-manager__upload input[type=file]`)→ 等 CDN 链接 → `Input.dispatchMouseEvent` 拖到第一位(落点 `first.left - 0.30*w`)。
|
||||
- 「更新」按钮:可点才点,禁用态识别;默认不提交。
|
||||
|
||||
## 任务看板状态
|
||||
|
||||
任务状态以 [`06-tasks.md`](06-tasks.md) 为准,历史记录见 [`../progress.md`](../progress.md)。
|
||||
|
||||
- 已完成:无(任务均为 TODO;单账号能力以脚本形式存在,待 T-001 模块化)。
|
||||
- 正在进行:无。
|
||||
- 下一个可领取任务:Phase 0 的 **T-001(`editor.py` 含采集)/ T-002(`appconfig.py`+`config.json`)/ T-003(`db.py`+SQLite)**——三者无相互依赖,可任选其一起步。
|
||||
|
||||
## 当前可运行内容
|
||||
|
||||
```bash
|
||||
# 语法检查
|
||||
python -m py_compile *.py
|
||||
|
||||
# 单账号闭环(不提交线上)
|
||||
python prototypes/demo.py # 分步(需根目录 cdp.py 可导入)
|
||||
set AUTO=1 && python prototypes/demo.py # 自动(cmd)
|
||||
|
||||
# 真实提交(谨慎)
|
||||
set UPDATE=1 && python prototypes/demo.py
|
||||
```
|
||||
|
||||
前置条件:
|
||||
|
||||
- Chrome 已用某 user-data-dir 带 `--remote-debugging-port` + `--remote-allow-origins=*` 启动并登录 Shopee。
|
||||
- 已 `pip install websocket-client requests`。
|
||||
- 默认连 `127.0.0.1:9222`(开发期可用 `CDP_HOST` 指向 WSL 转发的 `192.168.0.224:9333`)。
|
||||
|
||||
## 开始编码前检查
|
||||
|
||||
1. 读仓库级 `AGENTS.md` / `CLAUDE.md`(如有)。
|
||||
2. 读 `docs/00-ai-start-here.md`。
|
||||
3. 读 `docs/05-coding-rules.md` 与 `docs/04-architecture.md` 第四节。
|
||||
4. 在 `docs/06-tasks.md` 取第一个 `TODO` 且依赖 `DONE` 的任务。
|
||||
5. 将该任务状态改为 `DOING`。
|
||||
|
||||
## 维护规则
|
||||
|
||||
- 新建 `config/chrome/editor/gui` 或改 CDP 选择器后,更新本文与 `04-architecture.md`。
|
||||
- 任务状态变化同步 [`06-tasks.md`](06-tasks.md);执行记录追加 [`../progress.md`](../progress.md)。
|
||||
- 本文只保留当前快照,不保留完整历史。
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
# 界面与流程结构
|
||||
|
||||
> 桌面工具,无前端路由。用 **5 Tab GUI(Tkinter `ttk.Notebook`)+ 流水线** 约定界面职责与导航。
|
||||
|
||||
## Tab 顺序与职责(工作流优先)
|
||||
|
||||
```
|
||||
① 导入采集 │ ② AI生成 │ ③ 更新shopee │ ④ 账号管理 │ ⑤ 设置
|
||||
```
|
||||
|
||||
| Tab | 职责 | 风险 |
|
||||
| --- | --- | --- |
|
||||
| ① 导入采集 | 导入多个 Excel;任务列表;**采集**商品当前的旧标题/旧封面(只读),封面图下载本地;回写 Excel 旧字段 | 只读,低 |
|
||||
| ② AI生成 | 左侧标题/封面**提示词**;右侧按批次/店铺筛选任务列表;AI 生成新标题/新封面;双击看新旧封面 | 不触线上,中 |
|
||||
| ③ 更新shopee | 对**已生成**任务打开编辑页换标题+封面并**直接点「更新」提交**;结果回写 Excel | **写线上,高** |
|
||||
| ④ 账号管理 | Shopee 账号(账号名/别名/数据目录/端口/密码加密/登录状态);启动登录、检测登录、生成快捷方式 | 中 |
|
||||
| ⑤ 设置 | AI 模型/API Key、本地图片目录、Chrome 路径、默认端口等 | — |
|
||||
|
||||
任务的**阶段状态**贯穿各 Tab:`imported → collected → generated → applied`(或 `failed/skipped`)。**无人工确认、无提交开关**。各 Tab 聚焦各自阶段的列与按钮,但操作同一批任务(同一 batch)。
|
||||
|
||||
## 首次使用引导保护
|
||||
|
||||
- ① 导入采集 与 ③ 更新shopee 都依赖**账号已配置且已登录**(在 ④ 账号管理)。
|
||||
- 当无账号 / 账号未登录时:相关执行按钮**禁用**,并提示「请先到『账号管理』配置账号并登录」。
|
||||
- 老用户账号已就绪则无感。
|
||||
|
||||
## ① 导入采集
|
||||
|
||||
```
|
||||
┌ 导入采集 ─────────────────────────────────────────────────────┐
|
||||
│ [导入 Excel…] [移除] [清空] │
|
||||
│ ▸ 3 文件 · 128 行 · 有效125/无效3 · 匹配123 · 未匹配5⚠ │ ← 导入汇总栏
|
||||
│ 匹配明细:女装店60 · my主店40 · 饰品店23 │
|
||||
│ ┌───────────────────────────────────────────────────────────┐ │
|
||||
│ │ 账号名 别名 商品ID 阶段 旧标题 旧封面 │ │
|
||||
│ │ 主店A 女装店 51100639510 待采集 — — │ │
|
||||
│ └───────────────────────────────────────────────────────────┘ │
|
||||
│ [▶ 采集旧标题/旧封面] [■停止] [回写旧数据到 Excel] │
|
||||
│ 日志:逐条 文件→匹配账号、采集结果 │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- 导入:openpyxl 解析**输入列**(账号名/别名/商品id)入 SQLite。
|
||||
- **导入汇总栏**(导入后即时刷新,跑采集前的校验关口):显示 文件数、解析行数(原始数据量)、有效/无效行、匹配账号行数(按账号细分)、未匹配行数。未匹配/无效数字标红可点,点击在列表筛出便于定位纠错。
|
||||
- 采集:用该账号 Chrome 只读打开商品页,读旧标题、下载旧封面到本地图片目录,写 `old_title/old_cover_path`,stage=collected。
|
||||
- 回写:采集完把旧标题/旧封面路径批量回写原 Excel(原文件被锁→提示重试/另存)。
|
||||
- 别名未匹配账号 / 账号未登录 → 该行 skipped 并记原因。
|
||||
|
||||
## ② AI生成
|
||||
|
||||
左右布局:左侧约 1/4 放提示词,右侧放筛选 + 任务列表。
|
||||
|
||||
```
|
||||
┌ AI生成 ───────────────────────────────────────────────────────┐
|
||||
│ ┌─左 ~1/4─┐ ┌──────────────── 右 3/4 ──────────────────────┐ │
|
||||
│ │标题提示词│ │ 批次[本次▼] 店铺[全部▼] 状态[全部▼] [筛选] │ │
|
||||
│ │[ ]│ │ ┌──────────────────────────────────────────┐ │ │
|
||||
│ │[ ]│ │ │ 店铺名 商品id 旧标题 新标题 状态 │ │ │
|
||||
│ │ │ │ │ 女装店 511..639 …T恤 …百搭 已生成 │ │ │
|
||||
│ │封面提示词│ │ │ 女装店 511..640 … — 待生成 │ │ │
|
||||
│ │[ ]│ │ └──────────────────────────────────────────┘ │ │
|
||||
│ │[ ]│ │ (双击某行 → 弹窗看 旧封面 | 新封面) │ │
|
||||
│ └──────────┘ └───────────────────────────────────────────────┘ │
|
||||
│ 进度:标题30/30 · 封面12/30 · 失败1 [▶ 开始生成] [■停止] │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- 左侧(提示词管理,上下两块):
|
||||
- **标题提示词**:多行输入 + 「保存」(写 `title_prompt.txt`);启动时自动加载回显。
|
||||
- **封面提示词**:模板下拉(读 `prompts/cover/*.txt`)+ 图标工具栏(新建/保存/另存为/重命名/删除)+ 多行输入 + 「插入标题」(插 `{新标题}`)/「预览」(变量替换后查看)。
|
||||
- 变量:`{旧标题}`/`{新标题}`/`{商品id}`/`{店铺}`,生成前按任务替换。
|
||||
- 右上:按导入批次 / 店铺 / 状态筛选任务。
|
||||
- 右下:任务列表(店铺名、商品id、旧标题、新标题、状态);**双击某条 → 弹窗展示旧封面 | 新封面**(纯查看)。
|
||||
- 底部**单个「开始生成」+「停止」**:开始生成 = **先按 `title_concurrency` 并发生成标题,接着按 `image_concurrency` 并发生成封面**;进度实时显示 标题/封面/失败 计数。
|
||||
- 生成参数(标题/图片并发数、失败重试、分辨率、jpg 质量、模型/Key)在 **⑤ 设置**,不在本 Tab 重复。
|
||||
- 每条/每张完成即写库;「停止」取消未开始项,可再次「开始生成」对剩余继续。
|
||||
- **无人工确认环节**;新标题直接用 AI 输出(不可编辑);可选对单行 `重生成`。生成完即可进入 ③。
|
||||
|
||||
## ③ 更新shopee
|
||||
|
||||
```
|
||||
┌ 更新shopee ───────────────────────────────────────────────────┐
|
||||
│ 批次[本次▼] 店铺[全部▼] 状态[已生成▼] [筛选] │
|
||||
│ ⚠ 「开始更新」对【当前筛选结果】执行,即改标题+换封面并直接提交 │
|
||||
│ ┌───────────────────────────────────────────────────────────┐ │
|
||||
│ │ 店铺 商品ID 新标题 新封面 阶段 结果 │ │
|
||||
│ └───────────────────────────────────────────────────────────┘ │
|
||||
│ [▶ 开始更新] [■停止] [回写结果到 Excel] │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- 顶部**按批次 / 店铺 / 状态筛选**(与 ①②一致);「开始更新」作用于**当前筛选结果**,是一道范围控制。
|
||||
- 店铺筛选:建议**逐店铺更新**(每店铺需先启动其 Chrome 并登录)。
|
||||
- 状态筛选:`已生成` 只跑未更新的;`失败` 用于**失败重试**;`已更新成功/略过` 仅查看。
|
||||
- 对筛选出的**已生成(generated)任务**执行:打开编辑页换标题+换封面 → **总是点「更新」提交**(无开关)。
|
||||
- 串行、单条失败继续;每条立即写回 SQLite(committed/状态/error)。
|
||||
- 全部完成 → 把新标题/新封面/更新状态批量回写原 Excel;弹窗汇总。
|
||||
|
||||
## ④ 账号管理
|
||||
|
||||
```
|
||||
┌ 账号管理 ─────────────────────────────────────────────────────┐
|
||||
│ 账号名 别名 地区 端口 登录状态 备注 │
|
||||
│ 主店A 女装店 seller.shopee.tw 9222 ●已登录 │
|
||||
│ [+新增][✎编辑][🗑删除] [▶启动并登录][🔄检测登录][⧉快捷方式] │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
账号弹窗字段:账号名、别名(唯一,Excel 用它匹配)、地区域名、调试端口、密码(加密仅参考)、备注;配置目录按别名生成 slug 只读显示。
|
||||
|
||||
## ⑤ 设置
|
||||
|
||||
- AI:服务商 / 文本模型 / 图像模型 / API Key(加密存)。
|
||||
- AI 生成参数:**标题并发数、图片并发数、失败重试次数、分辨率、jpg 质量**。
|
||||
- 本地图片目录(旧封面下载、新封面生成的存放根目录)。
|
||||
- Chrome 路径、默认调试端口 / 端口范围、超时、DB 路径。
|
||||
|
||||
## 流程导航
|
||||
|
||||
```text
|
||||
④ 账号管理:配账号 + 启动登录(首次必做)
|
||||
│
|
||||
① 导入采集:导入 Excel → 采集旧标题/旧封面 → 回写旧字段
|
||||
│
|
||||
② AI生成:提示词 → 生成新标题/新封面(无确认)
|
||||
│
|
||||
③ 更新shopee:对已生成任务换标题+封面 → 直接点「更新」提交 → 回写结果
|
||||
```
|
||||
|
||||
- 未配账号/未登录:① ③ 的执行按钮禁用并提示去 ④。
|
||||
- 已生成的任务即可进 ③;③ 执行即提交线上(无确认、无开关)。
|
||||
- 任意步骤失败:记入该任务、日志标明,不影响其他任务。
|
||||
|
||||
## 组件建议(Tkinter)
|
||||
|
||||
| 组件 | 归属 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `MainNotebook` | 根窗口 | 5 个 Tab |
|
||||
| `CollectTab` | ① | 导入、任务表、采集、回写 |
|
||||
| `GenerateTab` | ② | 左提示词 + 右筛选/任务列表、双击看新旧封面、开始生成 |
|
||||
| `ApplyTab` | ③ | 已生成任务、换标题+封面+直接提交、回写 |
|
||||
| `AccountsTab` | ④ | 账号增删改、启动登录 |
|
||||
| `SettingsTab` | ⑤ | AI/目录/Chrome 配置 |
|
||||
|
||||
> 采集、生成、更新都是耗时操作,放后台线程,避免界面卡死。
|
||||
@@ -0,0 +1,14 @@
|
||||
# UI 效果图(SVG 线框)
|
||||
|
||||
cmshopee 5 Tab 桌面应用的界面效果图。SVG 可用浏览器直接打开查看。
|
||||
|
||||
| 文件 | 内容 |
|
||||
| --- | --- |
|
||||
| [overview-pipeline.svg](overview-pipeline.svg) | 5 Tab 流水线总览(stage:imported→collected→generated→confirmed→applied) |
|
||||
| [tab1-import-collect.svg](tab1-import-collect.svg) | ① 导入采集:导入 Excel、任务列表、采集旧标题/旧封面、回写、日志 |
|
||||
| [tab2-ai-generate.svg](tab2-ai-generate.svg) | ② AI生成:提示词、生成、新旧对照预览、确认/重生成 |
|
||||
| [tab3-update-shopee.svg](tab3-update-shopee.svg) | ③ 更新shopee:已确认任务、批次提交开关、执行结果、汇总 |
|
||||
| [tab4-accounts.svg](tab4-accounts.svg) | ④ 账号管理:账号列表、启动登录、检测登录、账号编辑弹窗 |
|
||||
| [tab5-settings.svg](tab5-settings.svg) | ⑤ 设置:AI 模型/Key、路径、端口 |
|
||||
|
||||
> 仅为线框效果图,最终样式以实现为准。界面职责与流程见 [../routes.md](../routes.md)。
|
||||
@@ -0,0 +1,91 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="940" height="420" viewBox="0 0 940 420" font-family="'Segoe UI','Microsoft YaHei',sans-serif">
|
||||
<defs>
|
||||
<marker id="arr" markerWidth="10" markerHeight="10" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#9aa3b2"/></marker>
|
||||
<style>
|
||||
.bg{fill:#f4f6f9;stroke:#d3d9e2}
|
||||
.h1{fill:#1f2733;font-size:18px;font-weight:700}
|
||||
.sub{fill:#8a92a0;font-size:12px}
|
||||
.card{fill:#fff;stroke:#cdd6e2}
|
||||
.tt{fill:#2f6fed;font-size:13px;font-weight:700}
|
||||
.st{fill:#1f2733;font-size:12px;font-weight:600}
|
||||
.ds{fill:#5c6573;font-size:11px}
|
||||
.stage{fill:#eaf3ff;stroke:#cfe2fb}
|
||||
.stageT{fill:#1f4e91;font-size:11px;font-weight:600}
|
||||
.gate{fill:#fff7e6;stroke:#f0d089}
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="bg" x="0.5" y="0.5" width="939" height="419" rx="10"/>
|
||||
<text class="h1" x="24" y="40">cmshopee · 5 Tab 流水线总览</text>
|
||||
<text class="sub" x="24" y="60">工作流优先顺序 · 对象是 Excel 里的每一行任务 · 状态字段 stage 贯穿全程</text>
|
||||
|
||||
<!-- stage chips row (无 confirmed、无提交开关) -->
|
||||
<g transform="translate(24,84)">
|
||||
<rect class="stage" x="0" y="0" width="200" height="26" rx="13"/><text class="stageT" x="66" y="17">imported</text>
|
||||
<rect class="stage" x="230" y="0" width="200" height="26" rx="13"/><text class="stageT" x="292" y="17">collected</text>
|
||||
<rect class="stage" x="460" y="0" width="200" height="26" rx="13"/><text class="stageT" x="520" y="17">generated</text>
|
||||
<rect class="stage" x="690" y="0" width="200" height="26" rx="13"/><text class="stageT" x="742" y="17">applied(直接提交)</text>
|
||||
</g>
|
||||
|
||||
<!-- 5 tab cards -->
|
||||
<g transform="translate(24,140)">
|
||||
<!-- card template -->
|
||||
<g>
|
||||
<rect class="card" x="0" y="0" width="168" height="210" rx="8"/>
|
||||
<text class="tt" x="14" y="28">① 导入采集</text>
|
||||
<text class="st" x="14" y="52">读 Excel 输入列</text>
|
||||
<text class="ds" x="14" y="72">账号名/别名/商品id</text>
|
||||
<text class="st" x="14" y="98">只读采集</text>
|
||||
<text class="ds" x="14" y="118">抓旧标题</text>
|
||||
<text class="ds" x="14" y="136">下载旧封面到本地</text>
|
||||
<text class="ds" x="14" y="154">回写 Excel 旧字段</text>
|
||||
<text class="ds" x="14" y="190" fill="#20a464">→ collected</text>
|
||||
</g>
|
||||
<g transform="translate(193,0)">
|
||||
<rect class="card" x="0" y="0" width="168" height="210" rx="8"/>
|
||||
<text class="tt" x="14" y="28">② AI生成</text>
|
||||
<text class="st" x="14" y="52">提示词输入</text>
|
||||
<text class="ds" x="14" y="72">标题/封面分开</text>
|
||||
<text class="st" x="14" y="98">AI 生成</text>
|
||||
<text class="ds" x="14" y="118">旧标题→新标题</text>
|
||||
<text class="ds" x="14" y="136">旧封面→新封面</text>
|
||||
<text class="ds" x="14" y="156">双击看新旧封面</text>
|
||||
<text class="ds" x="14" y="190" fill="#20a464">→ generated(无确认)</text>
|
||||
</g>
|
||||
<g transform="translate(386,0)">
|
||||
<rect class="card" x="0" y="0" width="168" height="210" rx="8"/>
|
||||
<text class="tt" x="14" y="28">③ 更新shopee</text>
|
||||
<text class="st" x="14" y="52">已生成任务</text>
|
||||
<text class="ds" x="14" y="72">打开编辑页</text>
|
||||
<text class="ds" x="14" y="90">换标题 + 换封面</text>
|
||||
<text class="ds" x="14" y="108">直接点更新提交</text>
|
||||
<text class="st" x="14" y="134">串行 · 失败继续</text>
|
||||
<text class="ds" x="14" y="154">实时写库 + 回写Excel</text>
|
||||
<text class="ds" x="14" y="190" fill="#20a464">→ applied</text>
|
||||
</g>
|
||||
<g transform="translate(579,0)">
|
||||
<rect class="card" x="0" y="0" width="168" height="210" rx="8"/>
|
||||
<text class="tt" x="14" y="28">④ 账号管理</text>
|
||||
<text class="ds" x="14" y="52">账号名/别名/端口</text>
|
||||
<text class="ds" x="14" y="72">密码加密·仅参考</text>
|
||||
<text class="ds" x="14" y="92">独立 user-data-dir</text>
|
||||
<text class="ds" x="14" y="112">启动并登录(人工)</text>
|
||||
<text class="ds" x="14" y="132">检测登录状态</text>
|
||||
<text class="ds" x="14" y="184" fill="#8a92a0">配一次,少改</text>
|
||||
</g>
|
||||
<g transform="translate(772,0)">
|
||||
<rect class="card" x="0" y="0" width="144" height="210" rx="8"/>
|
||||
<text class="tt" x="14" y="28">⑤ 设置</text>
|
||||
<text class="ds" x="14" y="52">AI 模型/Key</text>
|
||||
<text class="ds" x="14" y="72">本地图片目录</text>
|
||||
<text class="ds" x="14" y="92">Chrome 路径</text>
|
||||
<text class="ds" x="14" y="112">端口范围</text>
|
||||
<text class="ds" x="14" y="184" fill="#8a92a0">全局配置</text>
|
||||
</g>
|
||||
|
||||
<!-- arrows between 1-2-3 -->
|
||||
<line x1="168" y1="105" x2="191" y2="105" stroke="#9aa3b2" stroke-width="2" marker-end="url(#arr)"/>
|
||||
<line x1="361" y1="105" x2="384" y2="105" stroke="#9aa3b2" stroke-width="2" marker-end="url(#arr)"/>
|
||||
</g>
|
||||
|
||||
<text class="sub" x="24" y="394">① ③ 依赖账号已配置且已登录(④);未配/未登录则执行按钮禁用并提示去④。存储:config.json + SQLite + 本地 images/。</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.1 KiB |
@@ -0,0 +1,117 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="940" height="620" viewBox="0 0 940 620" font-family="'Segoe UI','Microsoft YaHei',sans-serif">
|
||||
<defs>
|
||||
<style>
|
||||
.win{fill:#f4f6f9;stroke:#d3d9e2}
|
||||
.hd{fill:#2b3a55}
|
||||
.title{fill:#fff;font-size:14px;font-weight:600}
|
||||
.tab{fill:#e8ebf0}
|
||||
.tabA{fill:#f4f6f9}
|
||||
.tabT{fill:#5c6573;font-size:13px}
|
||||
.tabTA{fill:#2f6fed;font-size:13px;font-weight:600}
|
||||
.sec{fill:#1f2733;font-size:13px;font-weight:600}
|
||||
.mut{fill:#8a92a0;font-size:11px}
|
||||
.cell{fill:#2a2f3a;font-size:12px}
|
||||
.th{fill:#5c6573;font-size:12px;font-weight:600}
|
||||
.btnP{fill:#2f6fed}
|
||||
.btnPT{fill:#fff;font-size:12px;font-weight:600}
|
||||
.btn{fill:#fff;stroke:#c5ccd6}
|
||||
.btnT{fill:#3a4150;font-size:12px}
|
||||
.panel{fill:#fff;stroke:#e2e6ec}
|
||||
.rowL{fill:#fafbfc}
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<!-- window -->
|
||||
<rect class="win" x="0.5" y="0.5" width="939" height="619" rx="8"/>
|
||||
<rect class="hd" x="0.5" y="0.5" width="939" height="40" rx="8"/>
|
||||
<rect class="hd" x="0.5" y="20" width="939" height="21"/>
|
||||
<text class="title" x="18" y="26">cmshopee · Shopee 商品批量改标题换封面</text>
|
||||
<circle cx="888" cy="20" r="6" fill="#f5bf4f"/><circle cx="910" cy="20" r="6" fill="#5ac26a"/><circle cx="866" cy="20" r="6" fill="#e0e4ea"/>
|
||||
|
||||
<!-- tab strip -->
|
||||
<g>
|
||||
<rect class="tab" x="0.5" y="41" width="939" height="38"/>
|
||||
<rect class="tabA" x="8" y="45" width="150" height="38"/>
|
||||
<rect x="8" y="45" width="150" height="3" fill="#2f6fed"/>
|
||||
<text class="tabTA" x="34" y="68">① 导入采集</text>
|
||||
<text class="tabT" x="186" y="68">② AI生成</text>
|
||||
<text class="tabT" x="330" y="68">③ 更新shopee</text>
|
||||
<text class="tabT" x="492" y="68">④ 账号管理</text>
|
||||
<text class="tabT" x="636" y="68">⑤ 设置</text>
|
||||
</g>
|
||||
|
||||
<!-- toolbar -->
|
||||
<g transform="translate(0,94)">
|
||||
<rect class="btn" x="18" y="0" width="110" height="30" rx="6"/><text class="btnT" x="34" y="20">导入 Excel…</text>
|
||||
<rect class="btn" x="136" y="0" width="64" height="30" rx="6"/><text class="btnT" x="152" y="20">移除</text>
|
||||
<rect class="btn" x="208" y="0" width="64" height="30" rx="6"/><text class="btnT" x="224" y="20">清空</text>
|
||||
<text class="mut" x="560" y="20">别名以 Excel“别名”列匹配账号 · 未匹配→略过</text>
|
||||
</g>
|
||||
|
||||
<!-- import summary bar -->
|
||||
<g transform="translate(18,130)">
|
||||
<rect x="0" y="0" width="904" height="40" rx="6" fill="#eef4ff" stroke="#cfe0fb"/>
|
||||
<text x="14" y="18" font-size="12.5" fill="#1f4e91" font-weight="600">▸ 导入汇总</text>
|
||||
<text x="86" y="18" font-size="12.5" fill="#2a2f3a">3 文件 · 128 行 · 有效 125 ·</text>
|
||||
<text x="300" y="18" font-size="12.5" fill="#c2473d">无效 3</text>
|
||||
<text x="350" y="18" font-size="12.5" fill="#2a2f3a">· 匹配 123 ·</text>
|
||||
<text x="448" y="18" font-size="12.5" fill="#c2473d">未匹配 5 ⚠</text>
|
||||
<text x="14" y="33" font-size="11" fill="#5c6573">匹配明细:女装店 60 · my主店 40 · 饰品店 23 (点“无效/未匹配”可在列表筛出定位)</text>
|
||||
</g>
|
||||
|
||||
<!-- task table -->
|
||||
<g transform="translate(18,180)">
|
||||
<rect class="panel" x="0" y="0" width="904" height="190" rx="6"/>
|
||||
<rect x="1" y="1" width="902" height="28" fill="#eef1f6"/>
|
||||
<text class="th" x="14" y="19">账号名</text>
|
||||
<text class="th" x="110" y="19">别名</text>
|
||||
<text class="th" x="210" y="19">商品ID</text>
|
||||
<text class="th" x="340" y="19">阶段</text>
|
||||
<text class="th" x="470" y="19">旧标题</text>
|
||||
<text class="th" x="770" y="19">旧封面</text>
|
||||
<!-- rows -->
|
||||
<g font-size="12">
|
||||
<rect class="rowL" x="1" y="29" width="902" height="32"/>
|
||||
<text class="cell" x="14" y="49">主店A</text><text class="cell" x="110" y="49">女装店</text>
|
||||
<text class="cell" x="210" y="49">51100639510</text>
|
||||
<circle cx="346" cy="45" r="4" fill="#20a464"/><text class="cell" x="356" y="49">已采集</text>
|
||||
<text class="cell" x="470" y="49">…顯瘦百搭T恤</text><text class="cell" x="770" y="49">old_…01.jpg</text>
|
||||
|
||||
<text class="cell" x="14" y="81">主店A</text><text class="cell" x="110" y="81">女装店</text>
|
||||
<text class="cell" x="210" y="81">51100640000</text>
|
||||
<circle cx="346" cy="77" r="4" fill="#b6bdc7"/><text class="cell" x="356" y="81">待采集</text>
|
||||
<text class="mut" x="470" y="81">—</text><text class="mut" x="770" y="81">—</text>
|
||||
|
||||
<rect class="rowL" x="1" y="93" width="902" height="32"/>
|
||||
<text class="cell" x="14" y="113">马来店</text><text class="cell" x="110" y="113">my主店</text>
|
||||
<text class="cell" x="210" y="113">52000000001</text>
|
||||
<circle cx="346" cy="109" r="4" fill="#20a464"/><text class="cell" x="356" y="113">已采集</text>
|
||||
<text class="cell" x="470" y="113">Korean Loose Tee</text><text class="cell" x="770" y="113">old_…07.jpg</text>
|
||||
|
||||
<text class="cell" x="14" y="145" fill="#c2473d">未知店</text><text class="cell" x="110" y="145" fill="#c2473d">x别名</text>
|
||||
<text class="cell" x="210" y="145">52999999</text>
|
||||
<circle cx="346" cy="141" r="4" fill="#e7a008"/><text class="cell" x="356" y="145" fill="#c2473d">略过·别名未匹配</text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- action buttons -->
|
||||
<g transform="translate(18,382)">
|
||||
<rect class="btnP" x="0" y="0" width="178" height="34" rx="6"/><text class="btnPT" x="22" y="22">▶ 采集旧标题/旧封面</text>
|
||||
<rect class="btn" x="190" y="0" width="64" height="34" rx="6"/><text class="btnT" x="208" y="22">■ 停止</text>
|
||||
<rect class="btn" x="700" y="0" width="204" height="34" rx="6"/><text class="btnT" x="724" y="22">回写旧数据到 Excel</text>
|
||||
</g>
|
||||
|
||||
<!-- log -->
|
||||
<g transform="translate(18,432)">
|
||||
<text class="sec" x="0" y="12">运行日志</text>
|
||||
<rect class="panel" x="0" y="20" width="904" height="160" rx="6"/>
|
||||
<g font-family="Consolas,monospace" font-size="11.5" fill="#586073">
|
||||
<text x="14" y="42">[10:21:03] 文件 tw01.xlsx → 匹配账号「女装店」(9222)</text>
|
||||
<text x="14" y="62">[10:21:05] 51100639510 采集旧标题:…顯瘦百搭T恤</text>
|
||||
<text x="14" y="82">[10:21:07] 下载旧封面 → images/nuzhuang_01/51100639510_old.jpg</text>
|
||||
<text x="14" y="102">[10:21:08] set_collected ✓(已写 SQLite)</text>
|
||||
<text x="14" y="122">[10:21:14] 52999999 别名「x别名」未匹配账号 → 略过</text>
|
||||
<text x="14" y="142">[10:21:20] 采集完成 3/4,已回写 Excel 旧字段</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.4 KiB |
@@ -0,0 +1,105 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="940" height="620" viewBox="0 0 940 620" font-family="'Segoe UI','Microsoft YaHei',sans-serif">
|
||||
<defs>
|
||||
<style>
|
||||
.win{fill:#f4f6f9;stroke:#d3d9e2}.hd{fill:#2b3a55}.title{fill:#fff;font-size:14px;font-weight:600}
|
||||
.tab{fill:#e8ebf0}.tabA{fill:#f4f6f9}.tabT{fill:#5c6573;font-size:13px}.tabTA{fill:#2f6fed;font-size:13px;font-weight:600}
|
||||
.sec{fill:#1f2733;font-size:13px;font-weight:600}.mut{fill:#8a92a0;font-size:11px}
|
||||
.cell{fill:#2a2f3a;font-size:12px}.th{fill:#5c6573;font-size:12px;font-weight:600}
|
||||
.btnP{fill:#2f6fed}.btnPT{fill:#fff;font-size:13px;font-weight:600}
|
||||
.btn{fill:#fff;stroke:#c5ccd6}.btnT{fill:#3a4150;font-size:12px}
|
||||
.panel{fill:#fff;stroke:#e2e6ec}.inp{fill:#fff;stroke:#c5ccd6}.ta{fill:#fbfcfe;stroke:#c5ccd6}
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="win" x="0.5" y="0.5" width="939" height="619" rx="8"/>
|
||||
<rect class="hd" x="0.5" y="0.5" width="939" height="40" rx="8"/><rect class="hd" x="0.5" y="20" width="939" height="21"/>
|
||||
<text class="title" x="18" y="26">cmshopee · Shopee 商品批量改标题换封面</text>
|
||||
<circle cx="888" cy="20" r="6" fill="#f5bf4f"/><circle cx="910" cy="20" r="6" fill="#5ac26a"/><circle cx="866" cy="20" r="6" fill="#e0e4ea"/>
|
||||
|
||||
<rect class="tab" x="0.5" y="41" width="939" height="38"/>
|
||||
<rect class="tabA" x="166" y="45" width="138" height="38"/><rect x="166" y="45" width="138" height="3" fill="#2f6fed"/>
|
||||
<text class="tabT" x="34" y="68">① 导入采集</text>
|
||||
<text class="tabTA" x="196" y="68">② AI生成</text>
|
||||
<text class="tabT" x="330" y="68">③ 更新shopee</text>
|
||||
<text class="tabT" x="492" y="68">④ 账号管理</text>
|
||||
<text class="tabT" x="636" y="68">⑤ 设置</text>
|
||||
|
||||
<!-- LEFT ~1/4: prompts management -->
|
||||
<g transform="translate(18,96)">
|
||||
<rect class="panel" x="0" y="0" width="218" height="446" rx="6"/>
|
||||
|
||||
<!-- title prompt: single file + save -->
|
||||
<text class="sec" x="14" y="24">标题提示词</text>
|
||||
<rect class="btn" x="150" y="8" width="54" height="22" rx="5"/><text class="btnT" x="164" y="23">保存</text>
|
||||
<rect class="ta" x="14" y="36" width="190" height="78" rx="6"/>
|
||||
<text class="cell" x="22" y="56" fill="#586073" font-size="11">精简标题、去「T恤」、</text>
|
||||
<text class="cell" x="22" y="72" fill="#586073" font-size="11">突出韩版加绒保暖 ≤60字</text>
|
||||
<text class="mut" x="14" y="128">启动自动加载 title_prompt.txt 回显</text>
|
||||
|
||||
<!-- cover prompt: multi-template -->
|
||||
<text class="sec" x="14" y="160">封面提示词(模板)</text>
|
||||
<rect class="inp" x="14" y="170" width="190" height="24" rx="5"/><text class="cell" x="22" y="187" font-size="11">模板:韩版女装 ▼</text>
|
||||
<!-- icon toolbar -->
|
||||
<g font-size="11">
|
||||
<rect class="btn" x="14" y="200" width="34" height="24" rx="5"/><text class="btnT" x="22" y="216">+</text>
|
||||
<rect class="btn" x="52" y="200" width="34" height="24" rx="5"/><text class="btnT" x="60" y="216">💾</text>
|
||||
<rect class="btn" x="90" y="200" width="34" height="24" rx="5"/><text class="btnT" x="96" y="216">另存</text>
|
||||
<rect class="btn" x="128" y="200" width="34" height="24" rx="5"/><text class="btnT" x="136" y="216">改名</text>
|
||||
<rect class="btn" x="166" y="200" width="38" height="24" rx="5"/><text class="btnT" x="176" y="216">删除</text>
|
||||
</g>
|
||||
<rect class="ta" x="14" y="232" width="190" height="118" rx="6"/>
|
||||
<text class="cell" x="22" y="252" fill="#586073" font-size="11">纯白背景、保留主体、</text>
|
||||
<text class="cell" x="22" y="268" fill="#586073" font-size="11">呼应标题:</text>
|
||||
<text x="22" y="284" fill="#2f6fed" font-size="11">{新标题}</text>
|
||||
<!-- insert / preview -->
|
||||
<rect class="btn" x="14" y="360" width="92" height="26" rx="6"/><text class="btnT" x="26" y="377">插入标题</text>
|
||||
<rect class="btn" x="112" y="360" width="92" height="26" rx="6"/><text class="btnT" x="138" y="377">预览</text>
|
||||
<text class="mut" x="14" y="406">变量 {旧标题}{新标题}{商品id}{店铺}</text>
|
||||
<text class="mut" x="14" y="422">模型/并发/重试/分辨率/质量 → ⑤设置</text>
|
||||
</g>
|
||||
|
||||
<!-- RIGHT: filters + list -->
|
||||
<g transform="translate(248,96)">
|
||||
<!-- filter bar -->
|
||||
<rect class="inp" x="0" y="0" width="120" height="28" rx="6"/><text class="cell" x="12" y="18">批次:本次 ▼</text>
|
||||
<rect class="inp" x="130" y="0" width="120" height="28" rx="6"/><text class="cell" x="142" y="18">店铺:全部 ▼</text>
|
||||
<rect class="inp" x="260" y="0" width="120" height="28" rx="6"/><text class="cell" x="272" y="18">状态:全部 ▼</text>
|
||||
<rect class="btn" x="390" y="0" width="64" height="28" rx="6"/><text class="btnT" x="408" y="18">筛选</text>
|
||||
<text class="mut" x="470" y="18">双击某行 → 弹窗看 旧封面 | 新封面</text>
|
||||
|
||||
<!-- table -->
|
||||
<rect class="panel" x="0" y="40" width="656" height="362" rx="6"/>
|
||||
<rect x="1" y="41" width="654" height="28" fill="#eef1f6"/>
|
||||
<text class="th" x="14" y="59">店铺名</text><text class="th" x="96" y="59">商品ID</text>
|
||||
<text class="th" x="220" y="59">旧标题</text><text class="th" x="420" y="59">新标题</text><text class="th" x="600" y="59">状态</text>
|
||||
<g font-size="12">
|
||||
<rect x="1" y="69" width="654" height="30" fill="#fafbfc"/>
|
||||
<text class="cell" x="14" y="89">女装店</text><text class="cell" x="96" y="89">51100639510</text>
|
||||
<text class="cell" x="220" y="89">…顯瘦百搭T恤</text><text class="cell" x="420" y="89" fill="#1f6f43">…顯瘦百搭</text>
|
||||
<circle cx="606" cy="85" r="4" fill="#20a464"/><text class="cell" x="616" y="89">已生成</text>
|
||||
|
||||
<text class="cell" x="14" y="119">女装店</text><text class="cell" x="96" y="119">51100640000</text>
|
||||
<text class="cell" x="220" y="119">…加絨打底衫T恤</text><text class="mut" x="420" y="119">—</text>
|
||||
<circle cx="606" cy="115" r="4" fill="#b6bdc7"/><text class="cell" x="616" y="119">待生成</text>
|
||||
|
||||
<rect x="1" y="129" width="654" height="30" fill="#fafbfc"/>
|
||||
<text class="cell" x="14" y="149">my主店</text><text class="cell" x="96" y="149">52000000001</text>
|
||||
<text class="cell" x="220" y="149">Korean Loose Tee</text><text class="cell" x="420" y="149" fill="#1f6f43">Korean Oversize Tee</text>
|
||||
<circle cx="606" cy="145" r="4" fill="#20a464"/><text class="cell" x="616" y="149">已生成</text>
|
||||
|
||||
<text class="cell" x="14" y="179">饰品店</text><text class="cell" x="96" y="179">53000000009</text>
|
||||
<text class="cell" x="220" y="179">…百搭耳環飾品</text><text class="cell" x="420" y="179" fill="#2f6fed">生成中…</text>
|
||||
<circle cx="606" cy="175" r="4" fill="#2f6fed"/><text class="cell" x="616" y="179">处理中</text>
|
||||
</g>
|
||||
<text class="mut" x="14" y="392">新标题直接用 AI 输出(不可编辑)· 可对单行右键「重生成」· 无人工确认</text>
|
||||
</g>
|
||||
|
||||
<!-- bottom action -->
|
||||
<g transform="translate(0,556)">
|
||||
<text class="sec" x="248" y="24" font-size="12">进度:</text>
|
||||
<text class="cell" x="290" y="24">标题 30/30 · 封面 12/30 ·</text>
|
||||
<text x="470" y="24" font-size="12" fill="#c2473d">失败 1</text>
|
||||
<text class="mut" x="248" y="42">先并发生成标题 → 接着并发生成封面 · 每条完成即写库 · 停止可取消未开始项</text>
|
||||
<rect class="btnP" x="690" y="8" width="130" height="38" rx="6"/><text class="btnPT" x="724" y="32">▶ 开始生成</text>
|
||||
<rect class="btn" x="828" y="8" width="92" height="38" rx="6"/><text class="btnT" x="852" y="32">■ 停止</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.5 KiB |
@@ -0,0 +1,81 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="940" height="620" viewBox="0 0 940 620" font-family="'Segoe UI','Microsoft YaHei',sans-serif">
|
||||
<defs><style>
|
||||
.win{fill:#f4f6f9;stroke:#d3d9e2}.hd{fill:#2b3a55}.title{fill:#fff;font-size:14px;font-weight:600}
|
||||
.tab{fill:#e8ebf0}.tabA{fill:#f4f6f9}.tabT{fill:#5c6573;font-size:13px}.tabTA{fill:#2f6fed;font-size:13px;font-weight:600}
|
||||
.sec{fill:#1f2733;font-size:13px;font-weight:600}.mut{fill:#8a92a0;font-size:11px}
|
||||
.cell{fill:#2a2f3a;font-size:12px}.th{fill:#5c6573;font-size:12px;font-weight:600}
|
||||
.btnP{fill:#2f6fed}.btnPT{fill:#fff;font-size:12px;font-weight:600}.btn{fill:#fff;stroke:#c5ccd6}.btnT{fill:#3a4150;font-size:12px}
|
||||
.panel{fill:#fff;stroke:#e2e6ec}.warn{fill:#fff7e6;stroke:#f0d089}
|
||||
</style></defs>
|
||||
<rect class="win" x="0.5" y="0.5" width="939" height="619" rx="8"/>
|
||||
<rect class="hd" x="0.5" y="0.5" width="939" height="40" rx="8"/><rect class="hd" x="0.5" y="20" width="939" height="21"/>
|
||||
<text class="title" x="18" y="26">cmshopee · Shopee 商品批量改标题换封面</text>
|
||||
<circle cx="888" cy="20" r="6" fill="#f5bf4f"/><circle cx="910" cy="20" r="6" fill="#5ac26a"/><circle cx="866" cy="20" r="6" fill="#e0e4ea"/>
|
||||
|
||||
<rect class="tab" x="0.5" y="41" width="939" height="38"/>
|
||||
<rect class="tabA" x="304" y="45" width="162" height="38"/><rect x="304" y="45" width="162" height="3" fill="#2f6fed"/>
|
||||
<text class="tabT" x="34" y="68">① 导入采集</text><text class="tabT" x="186" y="68">② AI生成</text>
|
||||
<text class="tabTA" x="334" y="68">③ 更新shopee</text>
|
||||
<text class="tabT" x="492" y="68">④ 账号管理</text><text class="tabT" x="636" y="68">⑤ 设置</text>
|
||||
|
||||
<!-- filter bar -->
|
||||
<g transform="translate(18,94)">
|
||||
<rect class="btn" x="0" y="0" width="120" height="28" rx="6"/><text class="btnT" x="12" y="18">批次:本次 ▼</text>
|
||||
<rect class="btn" x="130" y="0" width="120" height="28" rx="6"/><text class="btnT" x="142" y="18">店铺:全部 ▼</text>
|
||||
<rect class="btn" x="260" y="0" width="130" height="28" rx="6"/><text class="btnT" x="272" y="18">状态:已生成 ▼</text>
|
||||
<rect class="btnP" x="400" y="0" width="64" height="28" rx="6"/><text class="btnPT" x="418" y="18">筛选</text>
|
||||
</g>
|
||||
<!-- warn bar -->
|
||||
<g transform="translate(18,128)">
|
||||
<rect x="0" y="0" width="904" height="24" rx="6" fill="#fdeceb" stroke="#f3c2bd"/>
|
||||
<text x="14" y="16" font-size="12" fill="#b23b30">⚠ 「开始更新」对【当前筛选结果】执行:改标题+换封面并直接提交线上(无确认、无开关)</text>
|
||||
</g>
|
||||
|
||||
<!-- table -->
|
||||
<g transform="translate(18,162)">
|
||||
<rect class="panel" x="0" y="0" width="904" height="250" rx="6"/>
|
||||
<rect x="1" y="1" width="902" height="28" fill="#eef1f6"/>
|
||||
<text class="th" x="14" y="19">别名</text><text class="th" x="120" y="19">商品ID</text>
|
||||
<text class="th" x="250" y="19">新标题</text><text class="th" x="560" y="19">新封面</text>
|
||||
<text class="th" x="690" y="19">阶段</text><text class="th" x="800" y="19">结果</text>
|
||||
<g font-size="12">
|
||||
<rect x="1" y="29" width="902" height="34" fill="#fafbfc"/>
|
||||
<text class="cell" x="14" y="51">女装店</text><text class="cell" x="120" y="51">51100639510</text>
|
||||
<text class="cell" x="250" y="51">…顯瘦百搭</text><text class="cell" x="560" y="51">new_…01.jpg</text>
|
||||
<circle cx="696" cy="47" r="4" fill="#20a464"/><text class="cell" x="706" y="51">已应用</text>
|
||||
<text class="cell" x="800" y="51" fill="#1f6f43">成功·已提交</text>
|
||||
|
||||
<text class="cell" x="14" y="85">女装店</text><text class="cell" x="120" y="85">51100640200</text>
|
||||
<text class="cell" x="250" y="85">…加絨上衣</text><text class="cell" x="560" y="85">new_…02.jpg</text>
|
||||
<circle cx="696" cy="81" r="4" fill="#2f6fed"/><text class="cell" x="706" y="85">处理中…</text>
|
||||
<text class="mut" x="800" y="85">—</text>
|
||||
|
||||
<rect x="1" y="97" width="902" height="34" fill="#fafbfc"/>
|
||||
<text class="cell" x="14" y="119">my主店</text><text class="cell" x="120" y="119">52000000001</text>
|
||||
<text class="cell" x="250" y="119">Korean Loose Tee</text><text class="cell" x="560" y="119">new_…07.jpg</text>
|
||||
<circle cx="696" cy="115" r="4" fill="#b6bdc7"/><text class="cell" x="706" y="119">待应用</text><text class="mut" x="800" y="119">—</text>
|
||||
|
||||
<text class="cell" x="14" y="153">饰品店</text><text class="cell" x="120" y="153">53000000009</text>
|
||||
<text class="cell" x="250" y="153">…百搭耳環</text><text class="cell" x="560" y="153">new_…03.jpg</text>
|
||||
<circle cx="696" cy="149" r="4" fill="#c2473d"/><text class="cell" x="706" y="153">失败</text>
|
||||
<text class="cell" x="800" y="153" fill="#c2473d">更新按钮禁用</text>
|
||||
</g>
|
||||
<text class="mut" x="14" y="234">串行执行 · 单条失败继续 · 每条立即写回 SQLite</text>
|
||||
</g>
|
||||
|
||||
<!-- actions -->
|
||||
<g transform="translate(18,420)">
|
||||
<rect class="btnP" x="0" y="0" width="120" height="34" rx="6"/><text class="btnPT" x="26" y="22">▶ 开始更新</text>
|
||||
<rect class="btn" x="132" y="0" width="64" height="34" rx="6"/><text class="btnT" x="150" y="22">■ 停止</text>
|
||||
<rect class="btn" x="700" y="0" width="204" height="34" rx="6"/><text class="btnT" x="724" y="22">回写结果到 Excel</text>
|
||||
</g>
|
||||
|
||||
<!-- summary box -->
|
||||
<g transform="translate(18,470)">
|
||||
<rect class="warn" x="0" y="0" width="904" height="138" rx="6"/>
|
||||
<text class="sec" x="16" y="26">结束汇总</text>
|
||||
<text class="cell" x="16" y="54">成功:2 失败:1(更新按钮禁用 / 校验未过) 略过:0</text>
|
||||
<text class="cell" x="16" y="80">结果已写回原 Excel(旧/新标题、旧/新封面、更新状态)。</text>
|
||||
<text class="mut" x="16" y="104">⚠ 原 Excel 被打开锁定时回写失败 → 提示关闭后重试,或另存副本(SQLite 为事实来源)。</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.8 KiB |
@@ -0,0 +1,72 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="940" height="620" viewBox="0 0 940 620" font-family="'Segoe UI','Microsoft YaHei',sans-serif">
|
||||
<defs><style>
|
||||
.win{fill:#f4f6f9;stroke:#d3d9e2}.hd{fill:#2b3a55}.title{fill:#fff;font-size:14px;font-weight:600}
|
||||
.tab{fill:#e8ebf0}.tabA{fill:#f4f6f9}.tabT{fill:#5c6573;font-size:13px}.tabTA{fill:#2f6fed;font-size:13px;font-weight:600}
|
||||
.sec{fill:#1f2733;font-size:13px;font-weight:600}.mut{fill:#8a92a0;font-size:11px}
|
||||
.cell{fill:#2a2f3a;font-size:12px}.th{fill:#5c6573;font-size:12px;font-weight:600}
|
||||
.btnP{fill:#2f6fed}.btnPT{fill:#fff;font-size:12px;font-weight:600}.btn{fill:#fff;stroke:#c5ccd6}.btnT{fill:#3a4150;font-size:12px}
|
||||
.panel{fill:#fff;stroke:#e2e6ec}.inp{fill:#fff;stroke:#c5ccd6}.dlg{fill:#fff;stroke:#cdd4de}
|
||||
</style></defs>
|
||||
<rect class="win" x="0.5" y="0.5" width="939" height="619" rx="8"/>
|
||||
<rect class="hd" x="0.5" y="0.5" width="939" height="40" rx="8"/><rect class="hd" x="0.5" y="20" width="939" height="21"/>
|
||||
<text class="title" x="18" y="26">cmshopee · Shopee 商品批量改标题换封面</text>
|
||||
<circle cx="888" cy="20" r="6" fill="#f5bf4f"/><circle cx="910" cy="20" r="6" fill="#5ac26a"/><circle cx="866" cy="20" r="6" fill="#e0e4ea"/>
|
||||
|
||||
<rect class="tab" x="0.5" y="41" width="939" height="38"/>
|
||||
<rect class="tabA" x="466" y="45" width="150" height="38"/><rect x="466" y="45" width="150" height="3" fill="#2f6fed"/>
|
||||
<text class="tabT" x="34" y="68">① 导入采集</text><text class="tabT" x="186" y="68">② AI生成</text>
|
||||
<text class="tabT" x="330" y="68">③ 更新shopee</text>
|
||||
<text class="tabTA" x="496" y="68">④ 账号管理</text>
|
||||
<text class="tabT" x="636" y="68">⑤ 设置</text>
|
||||
|
||||
<!-- table -->
|
||||
<g transform="translate(18,100)">
|
||||
<rect class="panel" x="0" y="0" width="904" height="220" rx="6"/>
|
||||
<rect x="1" y="1" width="902" height="28" fill="#eef1f6"/>
|
||||
<text class="th" x="14" y="19">账号名</text><text class="th" x="140" y="19">别名</text>
|
||||
<text class="th" x="270" y="19">地区</text><text class="th" x="470" y="19">端口</text>
|
||||
<text class="th" x="560" y="19">登录状态</text><text class="th" x="720" y="19">备注</text>
|
||||
<g font-size="12">
|
||||
<rect x="1" y="29" width="902" height="32" fill="#fafbfc"/>
|
||||
<text class="cell" x="14" y="49">shop_tw_a</text><text class="cell" x="140" y="49">女装店</text>
|
||||
<text class="cell" x="270" y="49">seller.shopee.tw</text><text class="cell" x="470" y="49">9222</text>
|
||||
<circle cx="566" cy="45" r="4" fill="#20a464"/><text class="cell" x="576" y="49">已登录</text><text class="cell" x="720" y="49">主力店</text>
|
||||
|
||||
<text class="cell" x="14" y="81">shop_my</text><text class="cell" x="140" y="81">my主店</text>
|
||||
<text class="cell" x="270" y="81">seller.shopee.my</text><text class="cell" x="470" y="81">9223</text>
|
||||
<circle cx="566" cy="77" r="4" fill="#b6bdc7"/><text class="cell" x="576" y="81">未登录</text>
|
||||
|
||||
<rect x="1" y="93" width="902" height="32" fill="#fafbfc"/>
|
||||
<text class="cell" x="14" y="113">shop_tw_b</text><text class="cell" x="140" y="113">饰品店</text>
|
||||
<text class="cell" x="270" y="113">seller.shopee.tw</text><text class="cell" x="470" y="113">9224</text>
|
||||
<circle cx="566" cy="109" r="4" fill="#e7a008"/><text class="cell" x="576" y="113">未启动</text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- buttons -->
|
||||
<g transform="translate(18,332)">
|
||||
<rect class="btn" x="0" y="0" width="64" height="30" rx="6"/><text class="btnT" x="14" y="20">+新增</text>
|
||||
<rect class="btn" x="72" y="0" width="64" height="30" rx="6"/><text class="btnT" x="88" y="20">✎编辑</text>
|
||||
<rect class="btn" x="144" y="0" width="64" height="30" rx="6"/><text class="btnT" x="160" y="20">🗑删除</text>
|
||||
<rect class="btnP" x="430" y="0" width="120" height="30" rx="6"/><text class="btnPT" x="450" y="20">▶ 启动并登录</text>
|
||||
<rect class="btn" x="558" y="0" width="100" height="30" rx="6"/><text class="btnT" x="574" y="20">🔄 检测登录</text>
|
||||
<rect class="btn" x="666" y="0" width="120" height="30" rx="6"/><text class="btnT" x="682" y="20">⧉ 生成快捷方式</text>
|
||||
</g>
|
||||
|
||||
<!-- edit dialog overlay -->
|
||||
<g transform="translate(300,380)">
|
||||
<rect class="dlg" x="0" y="0" width="340" height="218" rx="8"/>
|
||||
<rect x="0" y="0" width="340" height="34" rx="8" fill="#eef1f6"/><rect x="0" y="20" width="340" height="14" fill="#eef1f6"/>
|
||||
<text class="sec" x="16" y="22">新增 / 编辑账号</text>
|
||||
<g font-size="12" fill="#2a2f3a">
|
||||
<text x="20" y="62">账号名*</text><rect class="inp" x="96" y="48" width="224" height="22" rx="4"/>
|
||||
<text x="20" y="92">别名*</text><rect class="inp" x="96" y="78" width="224" height="22" rx="4"/>
|
||||
<text x="20" y="122">地区域名</text><rect class="inp" x="96" y="108" width="224" height="22" rx="4"/>
|
||||
<text x="20" y="152">端口/密码</text><rect class="inp" x="96" y="138" width="108" height="22" rx="4"/>
|
||||
<rect class="inp" x="212" y="138" width="108" height="22" rx="4"/><text x="222" y="153" fill="#8a92a0">••••(加密)</text>
|
||||
</g>
|
||||
<text class="mut" x="20" y="182">配置目录(只读):chrome_user_data_dir\nuzhuang</text>
|
||||
<rect class="btn" x="180" y="186" width="64" height="24" rx="5"/><text class="btnT" x="198" y="202">取消</text>
|
||||
<rect class="btnP" x="252" y="186" width="68" height="24" rx="5"/><text class="btnPT" x="276" y="202">保存</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.4 KiB |
@@ -0,0 +1,81 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="940" height="620" viewBox="0 0 940 620" font-family="'Segoe UI','Microsoft YaHei',sans-serif">
|
||||
<defs><style>
|
||||
.win{fill:#f4f6f9;stroke:#d3d9e2}.hd{fill:#2b3a55}.title{fill:#fff;font-size:14px;font-weight:600}
|
||||
.tab{fill:#e8ebf0}.tabA{fill:#f4f6f9}.tabT{fill:#5c6573;font-size:13px}.tabTA{fill:#2f6fed;font-size:13px;font-weight:600}
|
||||
.sec{fill:#1f2733;font-size:13px;font-weight:600}.mut{fill:#8a92a0;font-size:11px}
|
||||
.cell{fill:#2a2f3a;font-size:12px}.lbl{fill:#3a4150;font-size:12px}
|
||||
.btnP{fill:#2f6fed}.btnPT{fill:#fff;font-size:12px;font-weight:600}.btn{fill:#fff;stroke:#c5ccd6}.btnT{fill:#3a4150;font-size:12px}
|
||||
.panel{fill:#fff;stroke:#e2e6ec}.inp{fill:#fff;stroke:#c5ccd6}.sub{fill:#fafbfc;stroke:#eef1f6}
|
||||
</style></defs>
|
||||
<rect class="win" x="0.5" y="0.5" width="939" height="619" rx="8"/>
|
||||
<rect class="hd" x="0.5" y="0.5" width="939" height="40" rx="8"/><rect class="hd" x="0.5" y="20" width="939" height="21"/>
|
||||
<text class="title" x="18" y="26">cmshopee · Shopee 商品批量改标题换封面</text>
|
||||
<circle cx="888" cy="20" r="6" fill="#f5bf4f"/><circle cx="910" cy="20" r="6" fill="#5ac26a"/><circle cx="866" cy="20" r="6" fill="#e0e4ea"/>
|
||||
|
||||
<rect class="tab" x="0.5" y="41" width="939" height="38"/>
|
||||
<rect class="tabA" x="616" y="45" width="110" height="38"/><rect x="616" y="45" width="110" height="3" fill="#2f6fed"/>
|
||||
<text class="tabT" x="34" y="68">① 导入采集</text><text class="tabT" x="186" y="68">② AI生成</text>
|
||||
<text class="tabT" x="330" y="68">③ 更新shopee</text><text class="tabT" x="492" y="68">④ 账号管理</text>
|
||||
<text class="tabTA" x="640" y="68">⑤ 设置</text>
|
||||
|
||||
<!-- A. AI 模型管理 (master-detail) -->
|
||||
<g transform="translate(18,94)">
|
||||
<rect class="panel" x="0" y="0" width="904" height="170" rx="6"/>
|
||||
<text class="sec" x="14" y="24">AI 模型管理</text>
|
||||
|
||||
<!-- left: model selector -->
|
||||
<rect class="sub" x="14" y="36" width="250" height="120" rx="6"/>
|
||||
<text class="lbl" x="26" y="58">模型</text>
|
||||
<rect class="inp" x="26" y="64" width="226" height="26" rx="5"/><text class="cell" x="36" y="81">Nano Banana 2 ▼</text>
|
||||
<rect class="btn" x="26" y="98" width="70" height="26" rx="5"/><text class="btnT" x="44" y="115">新增</text>
|
||||
<rect class="btn" x="102" y="98" width="70" height="26" rx="5"/><text class="btnT" x="120" y="115">删除</text>
|
||||
<rect class="btnP" x="178" y="98" width="74" height="26" rx="5"/><text class="btnPT" x="190" y="115">测试连接</text>
|
||||
<text class="mut" x="26" y="142">至少各 1 个 文本/图像模型</text>
|
||||
|
||||
<!-- right: detail form -->
|
||||
<text class="lbl" x="286" y="56">服务商名称</text><rect class="inp" x="372" y="42" width="180" height="24" rx="4"/><text class="cell" x="382" y="59">Nano Banana 2</text>
|
||||
<text class="lbl" x="572" y="56">网址</text><rect class="inp" x="612" y="42" width="278" height="24" rx="4"/><text class="cell" x="620" y="59" font-size="11" fill="#586073">https://api.vectorengine.ai/v1/chat/completions</text>
|
||||
|
||||
<text class="lbl" x="286" y="92">类别</text><rect class="inp" x="372" y="78" width="92" height="24" rx="4"/><text class="cell" x="382" y="95">图像 ▼</text>
|
||||
<text class="lbl" x="478" y="92">api_type</text><rect class="inp" x="538" y="78" width="100" height="24" rx="4"/><text class="cell" x="548" y="95">auto ▼</text>
|
||||
<text class="lbl" x="656" y="92">连接超时</text><rect class="inp" x="724" y="78" width="56" height="24" rx="4"/><text class="cell" x="736" y="95">30</text><text class="mut" x="786" y="95">秒</text>
|
||||
|
||||
<text class="lbl" x="286" y="128">模型ID</text><rect class="inp" x="372" y="114" width="266" height="24" rx="4"/><text class="cell" x="382" y="131" font-size="11" fill="#586073">gemini-3.1-flash-image-preview</text>
|
||||
<text class="lbl" x="656" y="128">密钥</text><rect class="inp" x="724" y="114" width="140" height="24" rx="4"/><text class="cell" x="734" y="131" fill="#8a92a0">••••••••</text>
|
||||
<rect class="btn" x="868" y="114" width="22" height="24" rx="4"/><text class="btnT" x="874" y="131">👁</text>
|
||||
</g>
|
||||
|
||||
<!-- B. 角色 + 生成参数 -->
|
||||
<g transform="translate(18,272)">
|
||||
<rect class="panel" x="0" y="0" width="904" height="120" rx="6"/>
|
||||
<text class="sec" x="14" y="24">角色分配 + 生成参数</text>
|
||||
<text class="lbl" x="14" y="54">标题大模型</text><rect class="inp" x="100" y="40" width="190" height="24" rx="4"/><text class="cell" x="110" y="57">GPT-5.5 文本 ▼</text>
|
||||
<text class="lbl" x="320" y="54">图片大模型</text><rect class="inp" x="406" y="40" width="190" height="24" rx="4"/><text class="cell" x="416" y="57">Nano Banana 2 ▼</text>
|
||||
<text class="lbl" x="630" y="54">分辨率</text><rect class="inp" x="690" y="40" width="90" height="24" rx="4"/><text class="cell" x="700" y="57">1k ▼</text>
|
||||
<text class="mut" x="790" y="57">→ 返回超时 240s</text>
|
||||
|
||||
<text class="lbl" x="14" y="90">标题并发</text><rect class="inp" x="80" y="76" width="50" height="24" rx="4"/><text class="cell" x="98" y="93">4</text>
|
||||
<text class="lbl" x="150" y="90">图片并发</text><rect class="inp" x="216" y="76" width="50" height="24" rx="4"/><text class="cell" x="234" y="93">4</text>
|
||||
<text class="lbl" x="286" y="90">失败重试</text><rect class="inp" x="352" y="76" width="50" height="24" rx="4"/><text class="cell" x="370" y="93">2</text>
|
||||
<text class="lbl" x="422" y="90">jpg质量</text><rect class="inp" x="478" y="76" width="50" height="24" rx="4"/><text class="cell" x="496" y="93">90</text>
|
||||
<text class="mut" x="560" y="93">▸ 高级 分辨率→返回超时:512:180 1k:240 2k:360 4k:600(可改)</text>
|
||||
</g>
|
||||
|
||||
<!-- C. 路径与运行 -->
|
||||
<g transform="translate(18,400)">
|
||||
<rect class="panel" x="0" y="0" width="904" height="104" rx="6"/>
|
||||
<text class="sec" x="14" y="24">路径与运行</text>
|
||||
<text class="lbl" x="14" y="52">Chrome 路径</text><rect class="inp" x="100" y="38" width="520" height="24" rx="4"/>
|
||||
<text class="cell" x="110" y="55" font-size="11" fill="#586073">C:\Program Files\Google\Chrome\Application\chrome.exe</text>
|
||||
<rect class="btn" x="630" y="38" width="60" height="24" rx="4"/><text class="btnT" x="644" y="55">浏览…</text>
|
||||
<text class="lbl" x="14" y="88">图片目录</text><rect class="inp" x="100" y="74" width="180" height="24" rx="4"/><text class="cell" x="110" y="91" fill="#586073">images</text>
|
||||
<text class="lbl" x="300" y="88">DB</text><rect class="inp" x="334" y="74" width="160" height="24" rx="4"/><text class="cell" x="344" y="91" fill="#586073">cmshopee.db</text>
|
||||
<text class="lbl" x="520" y="88">端口范围</text><rect class="inp" x="586" y="74" width="70" height="24" rx="4"/><text class="cell" x="598" y="91">9222</text>
|
||||
<text class="lbl" x="662" y="88">–</text><rect class="inp" x="676" y="74" width="70" height="24" rx="4"/><text class="cell" x="688" y="91">9260</text>
|
||||
</g>
|
||||
|
||||
<g transform="translate(0,512)">
|
||||
<rect class="btnP" x="822" y="0" width="100" height="34" rx="6"/><text class="btnPT" x="850" y="22">保存设置</text>
|
||||
<text class="mut" x="18" y="22">模型清单存 config/ai_models.json(密钥加密);选择与参数存 config.json。均 gitignore。</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.2 KiB |
+161
@@ -0,0 +1,161 @@
|
||||
# 执行进度记录
|
||||
|
||||
> 本文件是只追加的历史流水,记录任务执行过程、验证命令、阻塞点和关键决策。
|
||||
> 当前目录、当前命令、下一个可领取任务等可覆盖快照,写入 [`docs/current-state.md`](docs/current-state.md)。
|
||||
|
||||
## 职责边界
|
||||
|
||||
- `docs/06-tasks.md`:任务看板,维护任务状态、依赖和验收要点。
|
||||
- `progress.md`:历史流水,只追加记录每轮执行发生了什么。
|
||||
- `docs/current-state.md`:当前快照,可覆盖更新仓库现实、可运行命令和下一步。
|
||||
|
||||
## 记录格式
|
||||
|
||||
```markdown
|
||||
## 【YYYY-MM-DD】T-【编号】 【任务名】
|
||||
|
||||
- 状态:【DONE / BLOCKED / PARTIAL】
|
||||
- 变更:【修改了哪些文件或模块】
|
||||
- 验证:【运行的真实命令和结果】
|
||||
- 阻塞:【如有,写明原因和需要谁决策】
|
||||
- 决策:【如有,记录本轮确定的关键取舍】
|
||||
- 下一步:【建议下一个任务 ID 或待确认事项】
|
||||
```
|
||||
|
||||
## 执行记录
|
||||
|
||||
## 【2026-06-25】Phase -1 单账号流程验证(建文档前)
|
||||
|
||||
- 状态:DONE
|
||||
- 变更:新增 `cdp.py`、`demo.py`、`set_title.py`、`set_cover.py`、`get_title.py`、`cookies.py`、`inspect_images.py`、`grab.py`;本轮建立 `docs/` 全套 harness coding 文档与 `progress.md`、`AGENTS.md`、`CLAUDE.md`。
|
||||
- 验证:
|
||||
- CDP 连接 Chrome(开发期经 WSL→Windows portproxy `192.168.0.224:9333`→`127.0.0.1:9222`)成功。
|
||||
- `cookies.py`:读到 shopee.tw 标签页 50 个 Cookie(含 csrftoken)。
|
||||
- `get_title.py`:读到测试商品(ITEM_ID 51100639510)标题。
|
||||
- `set_title.py`:改标题后 `value`==`modelvalue`==新值。
|
||||
- `set_cover.py`:`setFileInputFiles` 上传成功(张数 5→6),拖拽落点 `first.left-0.30*w` 后新图到第一位。
|
||||
- `demo.py`:在 Windows 端跑通改标题 + 换封面(未提交)。
|
||||
- 文档:`python -m py_compile *.py` 通过;`demo.py` 增加 `UPDATE=1` 提交开关并语法校验通过。
|
||||
- 决策:
|
||||
- 浏览器自动化用自研 `cdp.py`(websocket-client + requests),不引入 playwright/selenium。
|
||||
- 必须清代理环境变量 + `suppress_origin=True` + `--remote-allow-origins=*`,否则连不上 CDP。
|
||||
- SPA 就绪用“标题框+图片 itembox+上传框”三元素存在判断,不用 load 事件。
|
||||
- 封面由位置决定,用模拟鼠标拖拽到第一位;「更新」默认不点,`UPDATE=1` 才提交。
|
||||
- 下一阶段做多账号管理 + GUI,GUI 暂定 Tkinter(待维护者确认)。
|
||||
- 下一步:从 `docs/06-tasks.md` 领取 T-001(抽 `editor.py`)或 T-002(`accounts.json` + `config.py`)。
|
||||
|
||||
## 【2026-06-25】决策 · 多账号隔离方案
|
||||
|
||||
- 状态:DONE(仅文档决策,未涉及代码)
|
||||
- 变更:更新 `docs/04-architecture.md`(新增 3.0 多账号隔离方案决策表)、`docs/02-requirements.md`(功能加“生成快捷方式 P1”、决策表加“多账号隔离方式”)、`docs/03-tech-stack.md`(决策记录)、`docs/api.md`(chrome 模块加 `create_shortcut`)、`docs/06-tasks.md`(新增 T-105 生成快捷方式)。
|
||||
- 决策:
|
||||
- 多账号隔离采用**方案 B:每账号独立 user-data-dir**,否决方案 A(Chrome profile)。理由:`--remote-debugging-port` 绑在 user-data-dir/进程上而非 profile,方案 A 无法每账号独立 CDP 端点、无法并行、串号风险高。
|
||||
- 启动主路径用程序 `subprocess` 直接带参数启动;快捷方式为可选便利,用 PowerShell `WScript.Shell.CreateShortcut` 生成,参数须写在快捷方式「目标」字段而非「起始位置」。
|
||||
- 下一步:实现 `chrome.py`(T-102 启动器 + T-105 快捷方式),或先做 T-001/T-002 地基。
|
||||
|
||||
## 【2026-06-25】决策 · 双 Tab + 配置文件 + SQLite + Excel 批量 + 登录检测 + 导出
|
||||
|
||||
- 状态:DONE(仅文档决策,未涉及代码)
|
||||
- 变更:系统性更新 `docs/02-requirements.md`(第二版功能/决策表/风险)、`docs/03-tech-stack.md`(config.json+SQLite+openpyxl)、`docs/04-architecture.md`(双 Tab 结构、4.1 config.json、4.2 SQLite schema、4.3 Excel 模板、5.x 批量与登录检测流程、第六节加登录检测事实)、`docs/api.md`(拆 appconfig/db/excel 模块、editor 加登录检测与 run_task)、`docs/06-tasks.md`(重排 Phase 0-4)、`docs/routes.md`(重写为双 Tab 线框)、`docs/00-ai-start-here.md`、`docs/current-state.md`。
|
||||
- 决策:
|
||||
- 存储拆两层:应用设置 `config.json` + 业务数据 SQLite `cmshopee.db`,取代早期 `accounts.json`。
|
||||
- GUI 双 Tab:Tab1 Excel 批量(导入/列表/执行/导出),Tab2 账号管理(密码加密仅参考、登录检测)。
|
||||
- 账号↔任务绑定以 Excel“别名”列为权威(弃文件名);未匹配略过,结束弹窗汇总。
|
||||
- Excel 模板列:别名/商品id/新标题/新封面图片路径/旧标题(留档)/旧封面图片路径(留档)/更新状态(输出结果)。
|
||||
- 是否提交更新由 Tab1 批次级开关控制,默认关;多账号串行、单条失败继续。
|
||||
- 密码不自动登录、仅参考、加密存;人工登录 + 程序检测登录态。
|
||||
- 引入 openpyxl;DB/config/用户数据目录均 gitignore。
|
||||
- 下一步:从 Phase 0 起步(T-001 editor.py / T-002 appconfig / T-003 db.py,任选其一)。
|
||||
|
||||
## 【2026-06-25】修正 · 旧数据为程序输出 + 回写时机 + 文件锁
|
||||
|
||||
- 状态:DONE(仅文档修正)
|
||||
- 变更:`docs/04-architecture.md`(4.2 schema 注释、4.3 列性质、3.x 关键事实、5.1 流程)、`docs/api.md`(`run_task` 抓旧值、`update_task_result` 加旧数据参数、`export_results`→`write_back` 回写原文件 + `export_copy` 退路)、`docs/06-tasks.md`(T-303/304/305)、`docs/02-requirements.md`(功能行 + 决策表)。
|
||||
- 决策修正:
|
||||
- 「旧标题/旧封面」由“运营输入/留档”改为 **程序改动前抓取的快照(输出列)**,用于审计/回滚。
|
||||
- SQLite **逐条实时写回**(每条任务处理完立即写状态+旧数据),不是批末统一写。
|
||||
- Excel **回写到原文件**:该 Excel 全部任务完成后,批量把旧数据+更新状态写回原 .xlsx;原文件被锁则提示关闭重试或 `export_copy` 另存副本。SQLite 为事实来源。
|
||||
- 下一步:Phase 0 起步(T-001/T-002/T-003)。
|
||||
|
||||
## 【2026-06-25】扩展 · 5 Tab 流水线 + AI 生成 + 采集回写 + 首次引导
|
||||
|
||||
- 状态:DONE(仅文档)
|
||||
- 变更:重写 `docs/routes.md`(5 Tab + AI生成界面)、`docs/04-architecture.md`(流水线/ai 模块/采集/本地图片目录/扩展 tasks schema)、`docs/api.md`(新增 `ai` 模块、editor 加采集 read_cover/download_cover/collect、db 拆 set_collected/generated/confirmed/applied)、`docs/06-tasks.md`(Phase 0-5 按 5 Tab 重排);增量更新 `docs/02-requirements.md`、`docs/03-tech-stack.md`(AI 依赖待定)、`docs/00-ai-start-here.md`、`docs/current-state.md`。
|
||||
- 决策:
|
||||
- GUI 定为 **5 Tab,工作流优先顺序**:① 导入采集 ② AI生成 ③ 更新shopee ④ 账号管理 ⑤ 设置。
|
||||
- 流水线五阶段:imported→collected→generated→confirmed→applied,stage 字段贯穿。
|
||||
- 「采集」只读抓旧标题+下载旧封面到本地并回写 Excel;「旧数据」为程序输出(已确认)。
|
||||
- 新增 AI:提示词+旧标题→新标题、提示词+旧封面→新封面(image-to-image);**服务商/模型待定**;产出必须人工确认才进更新。
|
||||
- Excel 增 `账号名` 输入列;输出列扩为 旧标题/旧封面/新标题/新封面/更新状态。
|
||||
- 本地 `images/<slug>/<item_id>_old|_new` 存图,路径记 DB;`images/` 加 gitignore。
|
||||
- 首次未配账号/未登录:① ③ 执行按钮禁用并提示去④。
|
||||
- 待确认:AI 服务商/模型/计费/合规;密码与 AI Key 加密密钥来源。
|
||||
- 下一步:Phase 0 起步(T-001/T-002/T-003)。
|
||||
|
||||
## 【2026-06-25】增补 · Tab① 导入汇总栏
|
||||
|
||||
- 状态:DONE(文档 + 效果图)
|
||||
- 变更:`docs/routes.md`(Tab① 加导入汇总栏 + 说明)、`docs/02-requirements.md`(功能加“导入校验汇总”)、`docs/api.md`(`import_tasks` 返回 stats、新增 `match_summary`)、`docs/06-tasks.md`(新增 T-202b)、`docs/ui/tab1-import-collect.svg`(加汇总栏)。
|
||||
- 决策:导入多个 Excel 后即时展示 文件数/解析行数/有效/无效/匹配(按账号细分)/未匹配,作为跑采集前的校验关口;未匹配/无效可点击在列表筛出。与执行后的“结束弹窗汇总”互补。
|
||||
- 下一步:Phase 0 起步(T-001/T-002/T-003)。
|
||||
|
||||
## 【2026-06-25】简化 · 去掉人工确认 + 去掉提交开关 + Tab② 左右布局
|
||||
|
||||
- 状态:DONE(文档;SVG 同步)
|
||||
- 变更:`docs/04-architecture.md`(流水线去 confirmed、schema 删 confirmed 列、config 删 commit_update_default、5.x/6.x 流程、第七/十节)、`docs/api.md`(删 set_confirmed、apply_task 去 commit 参数恒提交、ai 说明)、`docs/routes.md`(Tab② 左右布局无确认、Tab③ 去开关/改“已生成”、流程/组件)、`docs/06-tasks.md`(T-302/303/401/402、里程碑)、`docs/02-requirements.md`、`docs/03-tech-stack.md`、`docs/00-ai-start-here.md`、`docs/current-state.md`;SVG `tab2/tab3/overview`。
|
||||
- 决策(用户拍板):
|
||||
- **去掉人工确认**:流水线 imported→collected→generated→applied(删 confirmed 阶段)。
|
||||
- **去掉提交开关**:③ 执行即点「更新」总是提交线上。
|
||||
- Tab② 改左右布局:左 ~1/4 提示词(标题/封面),右 筛选(批次/店铺)+任务列表(店铺/商品id/旧标题/新标题),双击弹窗看新旧封面(纯查看);底部「开始生成」。
|
||||
- 新标题不可编辑,直接用 AI 输出。
|
||||
- 已知并接受风险:AI 标题/封面经 ③ 直接上线无核对,本地留档+回写 Excel 为唯一追溯。
|
||||
- 下一步:Phase 0 起步(T-001/T-002/T-003)。
|
||||
|
||||
## 【2026-06-25】AI 生成 · 并发参数 + 单按钮两段式
|
||||
|
||||
- 状态:DONE(文档 + SVG)
|
||||
- 变更:`docs/04-architecture.md`(config.json ai 段加 title_concurrency/image_concurrency/retry/resolution/jpg_quality;6.2 两段式并发流程)、`docs/api.md`(`gen_title`/`gen_cover` 加重试/分辨率/质量、新增 `generate_batch` 并发编排、`ai_config` 字段)、`docs/routes.md`(Tab② 单按钮+停止+进度、参数移⑤、⑤ 设置补参数)、`docs/06-tasks.md`(T-301/303/501)、`docs/03-tech-stack.md`(concurrent.futures 并发、Pillow 存盘);SVG `tab2`(单按钮+停止+进度、参数移⑤)、`tab5`(AI 段加 5 参数)。
|
||||
- 决策(用户):
|
||||
- 配置(⑤)新增:标题并发数、图片并发数、失败重试次数、分辨率、jpg 质量。
|
||||
- Tab② 单个「开始生成」+「停止」;点击后**先按 title_concurrency 并发生成标题,接着按 image_concurrency 并发生成图片**;进度显示 标题/封面/失败。
|
||||
- 每条/每张完成即写库;停止取消未开始项,可继续。并发用 `concurrent.futures.ThreadPoolExecutor`。
|
||||
- 下一步:Phase 0 起步(T-001/T-002/T-003)。
|
||||
|
||||
## 【2026-06-25】AI 生成 · 提示词管理
|
||||
|
||||
- 状态:DONE(文档 + SVG)
|
||||
- 变更:`docs/04-architecture.md`(存储加 title_prompt.txt + prompts/cover/、6.2 提示词管理与变量、目录结构)、`docs/api.md`(新增 `prompts` 模块:标题存取、封面模板 CRUD、`render_prompt`)、`docs/routes.md`(Tab② 左侧提示词管理说明)、`docs/02-requirements.md`(提示词管理功能行)、`docs/06-tasks.md`(T-302p)、SVG `tab2`(标题加保存、封面加模板下拉+图标工具栏+插入标题/预览)。
|
||||
- 决策(用户):
|
||||
- 标题提示词:单文件 `title_prompt.txt` +「保存」;启动回显。
|
||||
- 封面提示词:多模板,存 `prompts/cover/<名>.txt`;下拉 + 图标工具栏(新建/保存/另存为/重命名/删除)+「插入标题」(=插 `{新标题}`)+「预览」。
|
||||
- 变量 `{旧标题}/{新标题}/{商品id}/{店铺}`,生成前 `render_prompt` 替换;预览=替换后查看。
|
||||
- 下一步:Phase 0 起步(T-001/T-002/T-003)。
|
||||
|
||||
## 【2026-06-25】更新shopee · 加批次/店铺/状态筛选
|
||||
|
||||
- 状态:DONE(文档 + SVG)
|
||||
- 变更:`docs/routes.md`(Tab③ 加筛选栏 + 「开始更新」作用于当前筛选 + 状态=失败重试说明)、`docs/02-requirements.md`(③ 功能行)、`docs/06-tasks.md`(T-401)、SVG `tab3`(顶部加批次/店铺/状态筛选行,表与按钮下移避让)。
|
||||
- 决策(用户):③ 与 ①② 一致加筛选;店铺筛选利于逐店铺更新,状态筛选用于失败重试;「开始更新」仅作用于当前筛选结果(兼作范围控制/安全)。
|
||||
- 下一步:Phase 0 起步(T-001/T-002/T-003)。
|
||||
|
||||
## 【2026-06-26】AI 模型配置 · 模型清单 + 角色分配 + 分辨率/超时
|
||||
|
||||
- 状态:DONE(文档 + SVG)
|
||||
- 变更:`docs/04-architecture.md`(config.json ai 段改为 default_text/image_model + resolution + resolution_timeouts;新增 5.1b `config/ai_models.json` schema 含 category/api_type/connect_timeout;存储/目录/gitignore;6.2 模型选择与超时派生)、`docs/api.md`(appconfig 加 response_timeout、模型 CRUD/测试连接、get_model;ai 模块超时来源)、`docs/02-requirements.md`(⑤ 功能)、`docs/03-tech-stack.md`(AI 多模型行)、`docs/06-tasks.md`(T-501 模型管理 + T-501b 角色与参数);SVG `tab5` 重画为模型管理(master-detail)+角色下拉+分辨率/超时+路径。
|
||||
- 决策(用户 + 分析):
|
||||
- 模型清单 `config/ai_models.json`:每模型加 `category`(text/image)、唯一 name、api_type、`connect_timeout_seconds`(默认30,每模型)。
|
||||
- ⑤ 设置:模型下拉 + 新增/删除/测试连接 + 详情(服务商名/类别/api_type/模型ID/网址/密钥打码);至少各一个 text+image。
|
||||
- 角色:标题大模型/图片大模型下拉(按 category 过滤)→ config.json default_text/image_model。
|
||||
- 分辨率下拉 512/1k/2k/4k;**返回超时由 resolution_timeouts 映射自动**(180/240/360/600),不单独填。
|
||||
- 安全:密钥加密、打码、不入日志;`config/` 必须 gitignore;提醒轮换已暴露的 Key。
|
||||
- 下一步:Phase 0 起步(T-001/T-002/T-003)。
|
||||
|
||||
## 【2026-06-26】整理 · 测试脚本迁入 prototypes/
|
||||
|
||||
- 状态:DONE
|
||||
- 变更:`git mv` 把 `demo.py`/`set_title.py`/`set_cover.py`/`get_title.py`/`cookies.py`/`inspect_images.py`/`grab.py`/`1.py` 移入 `prototypes/`;`cdp.py` 留根目录(正式模块);新增 `prototypes/README.md`;批量把文档里的脚本引用改为 `prototypes/` 路径(含 04/00/03/05/06/api/AGENTS、current-state);修掉 `config.json` 里残留的 `api_key_enc` 描述(密钥已移至 ai_models.json)。
|
||||
- 决策:这些是已验证原型/探查脚本,逻辑待 `editor.py`(T-001) 移植并实测后清理;现保留为唯一已验证参照。`1.py` 作为 `ai.py` 调用方式参考。
|
||||
- 注意:`prototypes/demo.py` 依赖根目录 `cdp.py`,需从项目根运行或设 PYTHONPATH。
|
||||
- 下一步:Phase 0 起步(T-001/T-002/T-003)。
|
||||
|
||||
<!-- 新一轮从这里向下追加记录。 -->
|
||||
@@ -0,0 +1,25 @@
|
||||
# prototypes —— 已验证原型 / 探查脚本
|
||||
|
||||
这些是单账号阶段写的**已验证脚本**,逻辑与关键事实将被正式模块(尤其 `editor.py`,见任务 T-001)移植。
|
||||
**待 `editor.py` 完成并实测通过后,本目录可清理删除。** 在此之前保留它们作为"唯一已验证参照"。
|
||||
|
||||
## 文件
|
||||
|
||||
| 文件 | 用途 | 移植去向 |
|
||||
| --- | --- | --- |
|
||||
| `demo.py` | 单账号端到端:改标题 + 换封面(UPDATE=1 提交) | `editor.py` 编排 |
|
||||
| `set_title.py` | 改标题(原生 setter + 事件,value/modelvalue 验证) | `editor.change_title` |
|
||||
| `set_cover.py` | 上传图片 + 拖到第一位设封面(含选择器、拖拽落点) | `editor.replace_cover` |
|
||||
| `get_title.py` | 找/开商品 tab 并读取标题 | `editor.open_product` / `read_title` |
|
||||
| `cookies.py` | 读 shopee.tw 标签页 Cookie/会话 | `editor.is_logged_in` 的依据 |
|
||||
| `inspect_images.py` | 只读探查图片管理器 DOM(**Shopee 改版时重新探查可复用**) | 工具,保留参考 |
|
||||
| `grab.py` | 抓商品页 HTML + 列表接口 JSON(早期探查) | 参考 |
|
||||
| `1.py` | OpenAI SDK 最小调用测试 | `ai.py` 调用方式参考 |
|
||||
|
||||
## 运行注意
|
||||
|
||||
- **`cdp.py` 在项目根目录**(正式模块),不在本目录。
|
||||
- 仅 `demo.py` `import cdp`;其余脚本各自内置了 CDP 类,可独立运行。
|
||||
- 跑 `demo.py` 需让根目录的 `cdp.py` 可被导入(从项目根运行,或设 `PYTHONPATH=项目根`)。
|
||||
- 这些脚本默认连开发期的 WSL 转发地址 `192.168.0.224:9333`;正式模块用 `127.0.0.1:9222`(可用 `CDP_HOST` 覆盖)。
|
||||
- 已验证的关键事实(选择器、SPA 就绪判断、上传/拖拽方式)权威记录见 [`../docs/04-architecture.md`](../docs/04-architecture.md) 第七节。
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
# cookies.py —— 连 CDP,遍历所有 tab,找到域名含 shopee.tw 的 tab,取 cookie 并打印。
|
||||
#
|
||||
# 依赖:pip install websocket-client requests
|
||||
# 运行:python3 cookies.py
|
||||
# 环境变量:CDP_HOST(默认 192.168.0.224:9333)、DOMAIN(默认 shopee.tw)
|
||||
|
||||
import os
|
||||
|
||||
# CDP 是局域网直连,绝不能走代理。WSL 里设了 *_proxy(指向 :1080),
|
||||
# requests / websocket-client 都会读它导致超时。启动即清掉,最干净。
|
||||
for _k in ("ALL_PROXY", "all_proxy", "HTTP_PROXY", "http_proxy", "HTTPS_PROXY", "https_proxy"):
|
||||
os.environ.pop(_k, None)
|
||||
|
||||
import json
|
||||
|
||||
import requests
|
||||
from websocket import create_connection
|
||||
|
||||
CDP_HOST = os.environ.get("CDP_HOST", "192.168.0.224:9333")
|
||||
BASE = f"http://{CDP_HOST}"
|
||||
DOMAIN = os.environ.get("DOMAIN", "shopee.tw")
|
||||
|
||||
|
||||
def cdp_call(ws_url, method, params=None):
|
||||
# suppress_origin=True:不发 Origin 头,避免 Chrome 在未开 --remote-allow-origins 时返回 403。
|
||||
ws = create_connection(ws_url, max_size=None, suppress_origin=True)
|
||||
try:
|
||||
ws.send(json.dumps({"id": 1, "method": method, "params": params or {}}))
|
||||
while True:
|
||||
msg = json.loads(ws.recv())
|
||||
if msg.get("id") == 1:
|
||||
if "error" in msg:
|
||||
raise RuntimeError(msg["error"].get("message"))
|
||||
return msg.get("result", {})
|
||||
finally:
|
||||
ws.close()
|
||||
|
||||
|
||||
def main():
|
||||
print(f"[*] 读取目标列表: {BASE}/json")
|
||||
sess = requests.Session()
|
||||
sess.trust_env = False # 双保险:忽略环境代理
|
||||
targets = sess.get(f"{BASE}/json", timeout=10).json()
|
||||
pages = [t for t in targets if t.get("type") == "page"]
|
||||
|
||||
print(f"[*] 共 {len(pages)} 个 page tab:")
|
||||
for i, p in enumerate(pages):
|
||||
print(f" [{i}] {p.get('url')}")
|
||||
|
||||
matches = [p for p in pages if DOMAIN in (p.get("url") or "")]
|
||||
if not matches:
|
||||
print(f"[!] 没有找到 URL 含 '{DOMAIN}' 的 tab。")
|
||||
return
|
||||
|
||||
for p in matches:
|
||||
print(f"\n[*] 命中 tab: {p['url']}")
|
||||
result = cdp_call(p["webSocketDebuggerUrl"], "Network.getCookies")
|
||||
cookies = result.get("cookies", [])
|
||||
print(f"[*] 共 {len(cookies)} 个 cookie:\n")
|
||||
for c in cookies:
|
||||
print(f" {c['name']} = {c['value']}")
|
||||
print(f" domain={c.get('domain')} path={c.get('path')} "
|
||||
f"httpOnly={c.get('httpOnly')} secure={c.get('secure')}")
|
||||
|
||||
# 同时给一份可直接用于请求头的 Cookie 串
|
||||
header = "; ".join(f"{c['name']}={c['value']}" for c in cookies)
|
||||
print(f"\n[*] Cookie 请求头格式:\n{header}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,289 @@
|
||||
#!/usr/bin/env python3
|
||||
# demo.py —— 演示编排:打开商品页 → 改标题 → 上传新封面并拖到首位。分步停顿、不保存。
|
||||
#
|
||||
# 给运营同事演示用。每次运行都先把页面导航到干净状态(未保存过,重载即回到原始标题/图片),
|
||||
# 因此可反复演示。全程不点保存,绝不改动线上商品。
|
||||
#
|
||||
# 依赖:pip install websocket-client requests
|
||||
# 运行:python3 demo.py (分步,按 Enter 推进——适合讲解)
|
||||
# AUTO=1 python3 demo.py (一气呵成,自动推进)
|
||||
# 环境变量:CDP_HOST、ITEM_ID(默认 51100639510)、IMG_WIN(封面图 Windows 路径)、NEW_TITLE
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
|
||||
# 确保能 import 同目录的 cdp.py(某些 Windows Python 启动方式不会自动把脚本目录加进 sys.path)
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from cdp import CDP, http_get, find_product_tab, create_tab
|
||||
|
||||
ITEM_ID = os.environ.get("ITEM_ID", "51100639510")
|
||||
IMG_WIN = os.environ.get("IMG_WIN", r"D:\chengma\cmshopee\1_TY030.jpg")
|
||||
PRODUCT_URL = f"https://seller.shopee.tw/portal/product/{ITEM_ID}?pageEntry=product_list&ignore-html-cache=1"
|
||||
AUTO = os.environ.get("AUTO") == "1"
|
||||
DO_UPDATE = os.environ.get("UPDATE") == "1" # 默认不点「更新」;UPDATE=1 才真实提交到线上
|
||||
|
||||
TITLE_XPATH = "//input[@class='eds-input__input' and string-length(@modelvalue)>24]"
|
||||
ITEMBOX_XPATH = "//div[@class='container']/div[@class='can-drag shopee-image-manager__itembox' and @data-draggable='true']"
|
||||
|
||||
|
||||
# ---------- 小工具 ----------
|
||||
def banner(text):
|
||||
print("\n" + "=" * 60 + f"\n {text}\n" + "=" * 60)
|
||||
|
||||
|
||||
def pause(msg):
|
||||
if AUTO:
|
||||
print(f"\n>>> {msg}")
|
||||
time.sleep(1.5)
|
||||
else:
|
||||
try:
|
||||
input(f"\n>>> {msg} (按 Enter 继续)")
|
||||
except EOFError:
|
||||
time.sleep(1.5)
|
||||
|
||||
|
||||
# ---------- 页面 JS ----------
|
||||
def js_read_title():
|
||||
return (
|
||||
"(function(){"
|
||||
f"var r=document.evaluate({json.dumps(TITLE_XPATH)},document,null,"
|
||||
"XPathResult.FIRST_ORDERED_NODE_TYPE,null);var el=r.singleNodeValue;"
|
||||
"return el?el.value:null;})()"
|
||||
)
|
||||
|
||||
|
||||
def js_read_modelvalue():
|
||||
return (
|
||||
"(function(){"
|
||||
f"var r=document.evaluate({json.dumps(TITLE_XPATH)},document,null,"
|
||||
"XPathResult.FIRST_ORDERED_NODE_TYPE,null);var el=r.singleNodeValue;"
|
||||
"return el?el.getAttribute('modelvalue'):null;})()"
|
||||
)
|
||||
|
||||
|
||||
def js_write_title(t):
|
||||
return (
|
||||
"(function(){"
|
||||
f"var r=document.evaluate({json.dumps(TITLE_XPATH)},document,null,"
|
||||
"XPathResult.FIRST_ORDERED_NODE_TYPE,null);var el=r.singleNodeValue;"
|
||||
"if(!el)return 'NO_INPUT';"
|
||||
"var s=Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value').set;"
|
||||
f"el.focus();s.call(el,{json.dumps(t)});"
|
||||
"el.dispatchEvent(new Event('input',{bubbles:true}));"
|
||||
"el.dispatchEvent(new Event('change',{bubbles:true}));el.blur();return el.value;})()"
|
||||
)
|
||||
|
||||
|
||||
JS_RECTS = (
|
||||
"(function(){"
|
||||
f"var s=document.evaluate({json.dumps(ITEMBOX_XPATH)},document,null,XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,null);"
|
||||
"var a=[];for(var i=0;i<s.snapshotLength;i++){var el=s.snapshotItem(i);var r=el.getBoundingClientRect();"
|
||||
"var im=el.querySelector('img');a.push({i:i,x:r.left+r.width/2,y:r.top+r.height/2,"
|
||||
"left:r.left,top:r.top,w:r.width,h:r.height,src:im?im.src:null});}return JSON.stringify(a);})()"
|
||||
)
|
||||
|
||||
JS_READY = (
|
||||
"(function(){"
|
||||
f"var s=document.evaluate({json.dumps(ITEMBOX_XPATH)},document,null,XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,null);"
|
||||
f"var r=document.evaluate({json.dumps(TITLE_XPATH)},document,null,XPathResult.FIRST_ORDERED_NODE_TYPE,null);"
|
||||
"var up=document.querySelector('.shopee-image-manager__upload input[type=file]');"
|
||||
"return (s.snapshotLength>0 && !!r.singleNodeValue && !!up);})()"
|
||||
)
|
||||
|
||||
|
||||
# 定位「更新」按钮:button.eds-button 中 span 文本为“更新”的可见按钮
|
||||
JS_FIND_UPDATE = (
|
||||
"(function(){var bs=[].slice.call(document.querySelectorAll('button.eds-button'));"
|
||||
"var b=bs.find(function(b){var sp=b.querySelector('span');"
|
||||
"return (sp?sp.innerText:(b.innerText||'')).trim()==='更新' && b.offsetParent!==null;});"
|
||||
"if(!b)return JSON.stringify({found:false});"
|
||||
"var dis=b.disabled||/disabled/i.test(b.className);"
|
||||
"return JSON.stringify({found:true,disabled:dis});})()"
|
||||
)
|
||||
|
||||
JS_CLICK_UPDATE = (
|
||||
"(function(){var bs=[].slice.call(document.querySelectorAll('button.eds-button'));"
|
||||
"var b=bs.find(function(b){var sp=b.querySelector('span');"
|
||||
"return (sp?sp.innerText:(b.innerText||'')).trim()==='更新' && b.offsetParent!==null;});"
|
||||
"if(!b)return 'NO_BTN';if(b.disabled||/disabled/i.test(b.className))return 'DISABLED';"
|
||||
"b.click();return 'CLICKED';})()"
|
||||
)
|
||||
|
||||
# 提交后抓页面提示(toast/message),用于判断成功或失败
|
||||
JS_TOASTS = (
|
||||
"(function(){var es=[].slice.call(document.querySelectorAll("
|
||||
"'[class*=toast],[class*=Toast],[class*=message],[class*=Message],[class*=notice]'));"
|
||||
"var t=es.filter(function(e){return e.offsetParent!==null;})"
|
||||
".map(function(e){return (e.innerText||'').trim();}).filter(Boolean);"
|
||||
"return JSON.stringify(t.slice(0,5));})()"
|
||||
)
|
||||
|
||||
|
||||
def rects(cdp):
|
||||
return json.loads(cdp.val(JS_RECTS))
|
||||
|
||||
|
||||
def short(src):
|
||||
return src.rsplit("/", 1)[-1] if src else src
|
||||
|
||||
|
||||
# ---------- 流程 ----------
|
||||
def connect_clean_page():
|
||||
"""定位或新建商品 tab,导航到干净状态并等编辑器就绪。"""
|
||||
tab = find_product_tab(ITEM_ID)
|
||||
if tab:
|
||||
print(f"[*] 复用已打开的商品 tab")
|
||||
ws = tab["webSocketDebuggerUrl"]
|
||||
else:
|
||||
print(f"[*] 未找到商品 tab,新建并打开商品页")
|
||||
ws = create_tab(PRODUCT_URL)
|
||||
cdp = CDP(ws)
|
||||
cdp.send("Page.enable")
|
||||
cdp.send("Runtime.enable")
|
||||
cdp.send("DOM.enable")
|
||||
# 把该 tab 切到前台:演示时同事看得见,也避免后台标签被 Chrome 限流导致迟迟不渲染
|
||||
try:
|
||||
cdp.send("Page.bringToFront")
|
||||
except Exception:
|
||||
pass
|
||||
print(f"[*] 导航到干净状态:{PRODUCT_URL}")
|
||||
cdp.send("Page.navigate", {"url": PRODUCT_URL})
|
||||
|
||||
print("[*] 等待编辑器渲染就绪 ...", end="", flush=True)
|
||||
end = time.time() + 60
|
||||
while time.time() < end:
|
||||
time.sleep(1)
|
||||
try:
|
||||
if cdp.val(JS_READY):
|
||||
print(" 就绪 ✅")
|
||||
cdp.val("(function(){var m=document.querySelector('.shopee-image-manager');"
|
||||
"if(m)m.scrollIntoView({block:'center'});return 1;})()")
|
||||
time.sleep(0.5)
|
||||
return cdp
|
||||
except Exception:
|
||||
pass
|
||||
print(".", end="", flush=True)
|
||||
raise TimeoutError("等待编辑器就绪超时")
|
||||
|
||||
|
||||
def step_show_title(cdp):
|
||||
banner("步骤 1 / 读取当前商品标题")
|
||||
original = cdp.val(js_read_title())
|
||||
print(f"当前标题:\n {original}")
|
||||
return original
|
||||
|
||||
|
||||
def step_change_title(cdp, original):
|
||||
new_title = os.environ.get("NEW_TITLE") or original[:-2] # 默认去掉结尾两个字
|
||||
banner("步骤 2 / 修改标题")
|
||||
print(f"新标题(去掉结尾两字):\n {new_title}")
|
||||
pause("开始写入新标题")
|
||||
cdp.val(js_write_title(new_title))
|
||||
time.sleep(0.4)
|
||||
value = cdp.val(js_read_title())
|
||||
model = cdp.val(js_read_modelvalue())
|
||||
print(f" 写入后 value = {value}")
|
||||
print(f" 写入后 modelvalue = {model}")
|
||||
ok = value == new_title and model == new_title
|
||||
print(" 结果:" + ("✅ 标题已更新(value 与 modelvalue 一致,Vue 模型已绑定)" if ok else "❌ 写入异常"))
|
||||
return new_title
|
||||
|
||||
|
||||
def step_upload_and_cover(cdp):
|
||||
banner("步骤 3 / 上传新封面图并拖到首位")
|
||||
before = rects(cdp)
|
||||
before_srcs = {r["src"] for r in before}
|
||||
print(f"上传前共 {len(before)} 张图:")
|
||||
for r in before:
|
||||
print(f" [{r['i']}] {short(r['src'])}")
|
||||
|
||||
pause(f"开始上传封面图:{IMG_WIN}")
|
||||
oid = cdp.object_id("document.querySelector('.shopee-image-manager__upload input[type=file]')")
|
||||
if not oid:
|
||||
print("[!] 没找到上传输入框,终止。"); return
|
||||
cdp.send("DOM.setFileInputFiles", {"objectId": oid, "files": [IMG_WIN]})
|
||||
|
||||
print("[*] 等待上传完成(新图出现且为 CDN 链接)...")
|
||||
new_src = None
|
||||
end = time.time() + 90
|
||||
while time.time() < end:
|
||||
time.sleep(1.5)
|
||||
cur = rects(cdp)
|
||||
ready = [r for r in cur if r["src"] not in before_srcs
|
||||
and r["src"] and "susercontent" in r["src"] and "blob:" not in r["src"]]
|
||||
if len(cur) > len(before) and ready:
|
||||
new_src = ready[-1]["src"]
|
||||
print(f"[+] 上传成功:{short(new_src)}(当前 {len(cur)} 张)")
|
||||
break
|
||||
if not new_src:
|
||||
print("[!] 上传未确认成功,请看浏览器。"); return
|
||||
|
||||
pause("开始把新图拖到第一位(封面)")
|
||||
cur = rects(cdp)
|
||||
new = next(r for r in cur if r["src"] == new_src)
|
||||
first = cur[0]
|
||||
tx = first["left"] - first["w"] * 0.30 # 落点在第一张左缘外侧,才会插到最前
|
||||
ty = first["y"]
|
||||
print(f"[*] 拖拽:新图[{new['i']}] -> 首位前")
|
||||
cdp.drag(new["x"], new["y"], tx, ty)
|
||||
time.sleep(1.2)
|
||||
|
||||
after = rects(cdp)
|
||||
print(f"\n拖拽后顺序({len(after)} 张):")
|
||||
for r in after:
|
||||
print(f" [{r['i']}] {short(r['src'])}" + (" <== 新封面" if r["src"] == new_src else ""))
|
||||
print("\n结果:" + ("✅ 新图已在第一位(封面)" if after and after[0]["src"] == new_src
|
||||
else "❌ 新图未到第一位"))
|
||||
|
||||
|
||||
def step_update(cdp):
|
||||
banner("步骤 4 / 点击「更新」提交(⚠ 持久化到线上商品)")
|
||||
info = json.loads(cdp.val(JS_FIND_UPDATE))
|
||||
if not info.get("found"):
|
||||
print("[!] 没找到「更新」按钮,跳过。")
|
||||
return
|
||||
if info.get("disabled"):
|
||||
print("[!] 「更新」按钮当前为【禁用】状态——通常意味着有未通过的必填/校验项,不点击。")
|
||||
print(" 请到浏览器查看是否有红字校验提示。")
|
||||
return
|
||||
print("[*] 找到可点击的「更新」按钮。")
|
||||
pause("⚠ 即将点击「更新」,这会把标题+封面真实提交到线上商品")
|
||||
r = cdp.val(JS_CLICK_UPDATE)
|
||||
print(f"[*] 点击结果:{r}")
|
||||
if r != "CLICKED":
|
||||
return
|
||||
time.sleep(3)
|
||||
toasts = json.loads(cdp.val(JS_TOASTS))
|
||||
if toasts:
|
||||
print("[*] 页面提示:" + " | ".join(toasts))
|
||||
print("[*] 已提交。请到浏览器确认是否出现「更新成功」之类提示。")
|
||||
|
||||
|
||||
def main():
|
||||
banner("Shopee 商品改标题 + 换封面 自动化演示" + ("(含更新提交)" if DO_UPDATE else "(不保存)"))
|
||||
cdp = connect_clean_page()
|
||||
try:
|
||||
original = step_show_title(cdp)
|
||||
pause("进入步骤 2:修改标题")
|
||||
step_change_title(cdp, original)
|
||||
pause("进入步骤 3:上传并替换封面")
|
||||
step_upload_and_cover(cdp)
|
||||
|
||||
if DO_UPDATE:
|
||||
pause("进入步骤 4:点击「更新」提交")
|
||||
step_update(cdp)
|
||||
banner("演示结束")
|
||||
print("已点击「更新」,改动应已提交线上。请到浏览器核对提示。")
|
||||
else:
|
||||
banner("演示结束")
|
||||
print("以上改动全部停留在页面内存,未保存、未提交线上。")
|
||||
print("如需真正生效:UPDATE=1 python3 demo.py(脚本会自动点「更新」),")
|
||||
print("或直接到浏览器点「更新」。刷新页面则一切还原。")
|
||||
finally:
|
||||
cdp.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env python3
|
||||
# get_title.py —— 找到(或新建)商品编辑页 tab,读取标题输入框的当前内容。
|
||||
#
|
||||
# 逻辑:
|
||||
# 1. 遍历所有 page tab,找 URL 同时含 ITEM_ID 和 shopee.tw 的编辑页;
|
||||
# 2. 没有就用 Target.createTarget 新建 tab 打开 PRODUCT_URL;
|
||||
# 3. 等页面渲染,轮询 XPath 取标题输入框,打印其内容。
|
||||
#
|
||||
# 依赖:pip install websocket-client requests
|
||||
# 运行:python3 get_title.py
|
||||
# 环境变量:CDP_HOST(默认 192.168.0.224:9333)、ITEM_ID(默认 51100639510)
|
||||
|
||||
import os
|
||||
|
||||
# CDP 局域网直连,绝不走代理(WSL 里设了 *_proxy 指向 :1080,会导致超时)。
|
||||
for _k in ("ALL_PROXY", "all_proxy", "HTTP_PROXY", "http_proxy", "HTTPS_PROXY", "https_proxy"):
|
||||
os.environ.pop(_k, None)
|
||||
|
||||
import json
|
||||
import time
|
||||
import threading
|
||||
|
||||
import requests
|
||||
from websocket import create_connection
|
||||
|
||||
CDP_HOST = os.environ.get("CDP_HOST", "192.168.0.224:9333")
|
||||
BASE = f"http://{CDP_HOST}"
|
||||
ITEM_ID = os.environ.get("ITEM_ID", "51100639510")
|
||||
PRODUCT_URL = f"https://seller.shopee.tw/portal/product/{ITEM_ID}?pageEntry=product_list&ignore-html-cache=1"
|
||||
|
||||
# 标题输入框:eds 输入框且 modelvalue 长度 > 24(排除短输入框)
|
||||
TITLE_XPATH = "//input[@class='eds-input__input' and string-length(@modelvalue)>24]"
|
||||
|
||||
|
||||
def http_get(path):
|
||||
s = requests.Session()
|
||||
s.trust_env = False
|
||||
return s.get(f"{BASE}{path}", timeout=10).json()
|
||||
|
||||
|
||||
class CDP:
|
||||
"""极简 CDP 客户端:命令同步、事件回调异步。"""
|
||||
|
||||
def __init__(self, ws_url):
|
||||
# suppress_origin=True:不发 Origin 头,避免 Chrome 未开 --remote-allow-origins 时 403。
|
||||
self.ws = create_connection(ws_url, max_size=None, suppress_origin=True)
|
||||
self._id = 0
|
||||
self._results = {}
|
||||
self._cond = threading.Condition()
|
||||
self._handlers = []
|
||||
self._stop = False
|
||||
threading.Thread(target=self._recv_loop, daemon=True).start()
|
||||
|
||||
def _recv_loop(self):
|
||||
while not self._stop:
|
||||
try:
|
||||
raw = self.ws.recv()
|
||||
except Exception:
|
||||
break
|
||||
if not raw:
|
||||
continue
|
||||
msg = json.loads(raw)
|
||||
if "id" in msg:
|
||||
with self._cond:
|
||||
self._results[msg["id"]] = msg
|
||||
self._cond.notify_all()
|
||||
elif "method" in msg:
|
||||
for h in self._handlers:
|
||||
try:
|
||||
h(msg)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def on(self, fn):
|
||||
self._handlers.append(fn)
|
||||
|
||||
def send(self, method, params=None, timeout=30):
|
||||
self._id += 1
|
||||
mid = self._id
|
||||
self.ws.send(json.dumps({"id": mid, "method": method, "params": params or {}}))
|
||||
deadline = time.time() + timeout
|
||||
with self._cond:
|
||||
while mid not in self._results:
|
||||
left = deadline - time.time()
|
||||
if left <= 0:
|
||||
raise TimeoutError(f"CDP {method} 超时")
|
||||
self._cond.wait(left)
|
||||
msg = self._results.pop(mid)
|
||||
if "error" in msg:
|
||||
raise RuntimeError(f"{method} 失败: {msg['error'].get('message')}")
|
||||
return msg.get("result", {})
|
||||
|
||||
def close(self):
|
||||
self._stop = True
|
||||
try:
|
||||
self.ws.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def find_edit_tab():
|
||||
pages = [t for t in http_get("/json") if t.get("type") == "page"]
|
||||
print(f"[*] 当前 {len(pages)} 个 page tab:")
|
||||
for i, p in enumerate(pages):
|
||||
print(f" [{i}] {p.get('url')}")
|
||||
for p in pages:
|
||||
url = p.get("url") or ""
|
||||
if ITEM_ID in url and "shopee.tw" in url:
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
def open_edit_tab():
|
||||
"""用 browser 级 Target.createTarget 新建 tab,返回其 targetId。"""
|
||||
ver = http_get("/json/version")
|
||||
browser_ws = ver["webSocketDebuggerUrl"]
|
||||
bcdp = CDP(browser_ws)
|
||||
try:
|
||||
res = bcdp.send("Target.createTarget", {"url": PRODUCT_URL})
|
||||
return res["targetId"]
|
||||
finally:
|
||||
bcdp.close()
|
||||
|
||||
|
||||
def wait_page_ws(target_id, timeout=15):
|
||||
"""轮询 /json 等到新 tab 的 page websocket 出现。"""
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
for t in http_get("/json"):
|
||||
if t.get("id") == target_id and t.get("webSocketDebuggerUrl"):
|
||||
return t["webSocketDebuggerUrl"]
|
||||
time.sleep(0.5)
|
||||
raise TimeoutError("等待新 tab 的 websocket 超时")
|
||||
|
||||
|
||||
def read_title(cdp, timeout=40):
|
||||
"""轮询 XPath,取标题输入框内容(value 优先,回退 modelvalue 属性)。"""
|
||||
expr = (
|
||||
"(function(){"
|
||||
f"var r=document.evaluate({json.dumps(TITLE_XPATH)},document,null,"
|
||||
"XPathResult.FIRST_ORDERED_NODE_TYPE,null);"
|
||||
"var el=r.singleNodeValue;"
|
||||
"if(!el)return null;"
|
||||
"return JSON.stringify({value:el.value,modelvalue:el.getAttribute('modelvalue')});"
|
||||
"})()"
|
||||
)
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
r = cdp.send("Runtime.evaluate", {"expression": expr, "returnByValue": True})
|
||||
val = r.get("result", {}).get("value")
|
||||
if val:
|
||||
return json.loads(val)
|
||||
time.sleep(1)
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
tab = find_edit_tab()
|
||||
if tab:
|
||||
print(f"\n[*] 已存在编辑页 tab: {tab['url']}")
|
||||
page_ws = tab["webSocketDebuggerUrl"]
|
||||
else:
|
||||
print(f"\n[*] 未找到编辑页 tab,新建并打开:\n {PRODUCT_URL}")
|
||||
tid = open_edit_tab()
|
||||
page_ws = wait_page_ws(tid)
|
||||
print(f"[*] 新 tab 已就绪 (targetId={tid})")
|
||||
|
||||
cdp = CDP(page_ws)
|
||||
try:
|
||||
cdp.send("Page.enable")
|
||||
cdp.send("Runtime.enable")
|
||||
print("[*] 等待标题输入框渲染...")
|
||||
title = read_title(cdp)
|
||||
finally:
|
||||
cdp.close()
|
||||
|
||||
if not title:
|
||||
print("[!] 未匹配到标题输入框。页面可能没加载完,或 XPath 需要调整。")
|
||||
return
|
||||
content = title.get("value") or title.get("modelvalue") or ""
|
||||
print("\n========== 商品标题 ==========")
|
||||
print(content)
|
||||
print("==============================")
|
||||
print(f"\n(value={title.get('value')!r} modelvalue={title.get('modelvalue')!r})")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,217 @@
|
||||
#!/usr/bin/env python3
|
||||
# grab.py —— 通过 CDP 连接已登录的 Chrome,保存“我的商品”第一页渲染后的 HTML,
|
||||
# 并拦截列表接口的 JSON 响应。
|
||||
#
|
||||
# 前置:Windows 上 Chrome 已带 --remote-debugging-port 启动,且 portproxy 把
|
||||
# 0.0.0.0:9333 -> 127.0.0.1:9222(见 chrome-remote-debug-lan.md)。
|
||||
# 依赖:pip install websocket-client requests
|
||||
# 运行:python3 grab.py
|
||||
#
|
||||
# 环境变量(可选):
|
||||
# CDP_HOST CDP 地址,默认 192.168.0.224:9333
|
||||
# OUT_DIR 输出目录,默认 ./out
|
||||
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import time
|
||||
import base64
|
||||
import pathlib
|
||||
import datetime
|
||||
import threading
|
||||
|
||||
import requests
|
||||
from websocket import create_connection
|
||||
|
||||
CDP_HOST = os.environ.get("CDP_HOST", "192.168.0.224:9333")
|
||||
BASE = f"http://{CDP_HOST}"
|
||||
OUT_DIR = pathlib.Path(os.environ.get("OUT_DIR", pathlib.Path(__file__).parent / "out"))
|
||||
|
||||
# 命中这些关键词的 JSON 响应,视为“商品列表接口”重点保存。
|
||||
# 不同站点/版本端点名不一样,先广撒网,跑完看 _all_xhr.log 里的真实端点再收窄。
|
||||
LIST_HINTS = [
|
||||
"get_product_list",
|
||||
"search_product",
|
||||
"product/list",
|
||||
"mpsku/list",
|
||||
"search_item",
|
||||
"product_list",
|
||||
"get_item_list",
|
||||
]
|
||||
|
||||
# CDP 是局域网直连,绝不能走代理。requests 默认会读 *_proxy 环境变量,这里强制不用代理。
|
||||
NO_PROXY = {"http": None, "https": None}
|
||||
|
||||
|
||||
def ts():
|
||||
return datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||
|
||||
|
||||
def safe(s):
|
||||
return re.sub(r"[^a-z0-9._-]+", "_", s, flags=re.I)[:120]
|
||||
|
||||
|
||||
class CDP:
|
||||
"""极简 CDP 客户端:单个 page target 的 WebSocket,命令同步、事件回调异步。"""
|
||||
|
||||
def __init__(self, ws_url):
|
||||
self.ws = create_connection(ws_url, max_size=None)
|
||||
self._id = 0
|
||||
self._results = {}
|
||||
self._cond = threading.Condition()
|
||||
self._handlers = []
|
||||
self._stop = False
|
||||
self._t = threading.Thread(target=self._recv_loop, daemon=True)
|
||||
self._t.start()
|
||||
|
||||
def _recv_loop(self):
|
||||
while not self._stop:
|
||||
try:
|
||||
raw = self.ws.recv()
|
||||
except Exception:
|
||||
break
|
||||
if not raw:
|
||||
continue
|
||||
msg = json.loads(raw)
|
||||
if "id" in msg:
|
||||
with self._cond:
|
||||
self._results[msg["id"]] = msg
|
||||
self._cond.notify_all()
|
||||
elif "method" in msg:
|
||||
for h in self._handlers:
|
||||
try:
|
||||
h(msg)
|
||||
except Exception as e:
|
||||
print(f"[!] handler 出错: {e}")
|
||||
|
||||
def on(self, fn):
|
||||
self._handlers.append(fn)
|
||||
|
||||
def send(self, method, params=None, timeout=30):
|
||||
self._id += 1
|
||||
mid = self._id
|
||||
self.ws.send(json.dumps({"id": mid, "method": method, "params": params or {}}))
|
||||
deadline = time.time() + timeout
|
||||
with self._cond:
|
||||
while mid not in self._results:
|
||||
remaining = deadline - time.time()
|
||||
if remaining <= 0:
|
||||
raise TimeoutError(f"CDP {method} 超时")
|
||||
self._cond.wait(remaining)
|
||||
msg = self._results.pop(mid)
|
||||
if "error" in msg:
|
||||
raise RuntimeError(f"{method} 失败: {msg['error'].get('message')}")
|
||||
return msg.get("result", {})
|
||||
|
||||
def close(self):
|
||||
self._stop = True
|
||||
try:
|
||||
self.ws.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def pick_target(pages):
|
||||
# 选中卖家中心“我的商品”那一页
|
||||
for p in pages:
|
||||
if re.search(r"seller\..*shopee", p["url"], re.I) and re.search(r"product|portal", p["url"], re.I):
|
||||
return p
|
||||
for p in pages:
|
||||
if re.search(r"seller\..*shopee", p["url"], re.I):
|
||||
return p
|
||||
return pages[0] if pages else None
|
||||
|
||||
|
||||
def main():
|
||||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
print(f"[*] 读取目标列表: {BASE}/json")
|
||||
targets = requests.get(f"{BASE}/json", proxies=NO_PROXY, timeout=10).json()
|
||||
pages = [t for t in targets if t.get("type") == "page"]
|
||||
print(f"[*] 共 {len(pages)} 个 page 标签页:")
|
||||
for i, p in enumerate(pages):
|
||||
print(f" [{i}] {p['url']}")
|
||||
|
||||
target = pick_target(pages)
|
||||
if not target:
|
||||
raise SystemExit("没找到任何 page 标签页,Chrome 是否带调试参数启动?")
|
||||
print(f"[*] 使用标签页: {target['url']}")
|
||||
|
||||
cdp = CDP(target["webSocketDebuggerUrl"])
|
||||
cdp.send("Page.enable")
|
||||
cdp.send("Network.enable")
|
||||
|
||||
xhr_lines = []
|
||||
tracked = {} # requestId -> url(疑似列表接口,待取 body)
|
||||
state = {"loaded": False, "hits": 0}
|
||||
|
||||
def on_event(msg):
|
||||
method = msg["method"]
|
||||
if method == "Page.loadEventFired":
|
||||
state["loaded"] = True
|
||||
return
|
||||
if method == "Network.responseReceived":
|
||||
p = msg["params"]
|
||||
resp, rtype = p["response"], p.get("type")
|
||||
if rtype not in ("XHR", "Fetch"):
|
||||
return
|
||||
mime = (resp.get("mimeType") or "").lower()
|
||||
if "json" not in mime:
|
||||
return
|
||||
url = resp["url"]
|
||||
xhr_lines.append(f"{resp.get('status')} {url}")
|
||||
if any(h in url.lower() for h in LIST_HINTS):
|
||||
tracked[p["requestId"]] = url
|
||||
return
|
||||
if method == "Network.loadingFinished":
|
||||
rid = msg["params"]["requestId"]
|
||||
url = tracked.pop(rid, None)
|
||||
if url is None:
|
||||
return
|
||||
try:
|
||||
r = cdp.send("Network.getResponseBody", {"requestId": rid})
|
||||
body = base64.b64decode(r["body"]).decode("utf-8", "replace") if r.get("base64Encoded") else r["body"]
|
||||
state["hits"] += 1
|
||||
fname = f"list_{state['hits']}_{safe(url.split('?')[0].rsplit('/', 1)[-1] or 'list')}.json"
|
||||
(OUT_DIR / fname).write_text(body, encoding="utf-8")
|
||||
print(f"[+] 命中列表接口 -> out/{fname}")
|
||||
except Exception as e:
|
||||
print(f"[!] 取 body 失败 ({url}): {e}")
|
||||
|
||||
cdp.on(on_event)
|
||||
|
||||
print("[*] 重新加载页面以触发接口请求...")
|
||||
cdp.send("Page.reload", {"ignoreCache": False})
|
||||
|
||||
# 等 load 事件 + 额外等晚到的 XHR
|
||||
for _ in range(30):
|
||||
if state["loaded"]:
|
||||
break
|
||||
time.sleep(0.5)
|
||||
time.sleep(4)
|
||||
|
||||
# 保存渲染后的整页 HTML
|
||||
r = cdp.send("Runtime.evaluate", {
|
||||
"expression": "document.documentElement.outerHTML",
|
||||
"returnByValue": True,
|
||||
})
|
||||
html = "<!DOCTYPE html>\n" + (r.get("result", {}).get("value") or "")
|
||||
html_path = OUT_DIR / f"my-products-page1_{ts()}.html"
|
||||
html_path.write_text(html, encoding="utf-8")
|
||||
print(f"[+] 已保存 HTML -> {html_path}")
|
||||
|
||||
# 落盘全部 XHR 端点清单
|
||||
log_path = OUT_DIR / "_all_xhr.log"
|
||||
log_path.write_text("\n".join(xhr_lines) + "\n", encoding="utf-8")
|
||||
print(f"[*] 全部 JSON XHR 端点 -> {log_path}")
|
||||
if state["hits"] == 0:
|
||||
print("[!] 未命中列表接口。打开 out/_all_xhr.log 找真实端点,把关键词加进 grab.py 的 LIST_HINTS。")
|
||||
else:
|
||||
print(f"[*] 共抓到 {state['hits']} 个列表接口响应。")
|
||||
|
||||
cdp.close() # 仅断开 CDP,不会关你的 Chrome
|
||||
print("[*] 完成。")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
# inspect_images.py —— 只读探查商品图片管理器的 DOM 结构,为实现换封面提供依据。
|
||||
|
||||
import os
|
||||
for _k in ("ALL_PROXY", "all_proxy", "HTTP_PROXY", "http_proxy", "HTTPS_PROXY", "https_proxy"):
|
||||
os.environ.pop(_k, None)
|
||||
|
||||
import json
|
||||
import time
|
||||
import threading
|
||||
import requests
|
||||
from websocket import create_connection
|
||||
|
||||
CDP_HOST = os.environ.get("CDP_HOST", "192.168.0.224:9333")
|
||||
BASE = f"http://{CDP_HOST}"
|
||||
ITEM_ID = os.environ.get("ITEM_ID", "51100639510")
|
||||
|
||||
|
||||
def http_get(path):
|
||||
s = requests.Session(); s.trust_env = False
|
||||
return s.get(f"{BASE}{path}", timeout=10).json()
|
||||
|
||||
|
||||
class CDP:
|
||||
def __init__(self, ws):
|
||||
self.ws = create_connection(ws, max_size=None, suppress_origin=True)
|
||||
self._id = 0; self._res = {}; self._c = threading.Condition(); self._stop = False
|
||||
threading.Thread(target=self._loop, daemon=True).start()
|
||||
def _loop(self):
|
||||
while not self._stop:
|
||||
try: raw = self.ws.recv()
|
||||
except Exception: break
|
||||
if not raw: continue
|
||||
m = json.loads(raw)
|
||||
if "id" in m:
|
||||
with self._c: self._res[m["id"]] = m; self._c.notify_all()
|
||||
def send(self, method, params=None, timeout=30):
|
||||
self._id += 1; mid = self._id
|
||||
self.ws.send(json.dumps({"id": mid, "method": method, "params": params or {}}))
|
||||
end = time.time() + timeout
|
||||
with self._c:
|
||||
while mid not in self._res:
|
||||
left = end - time.time()
|
||||
if left <= 0: raise TimeoutError(method)
|
||||
self._c.wait(left)
|
||||
m = self._res.pop(mid)
|
||||
if "error" in m: raise RuntimeError(m["error"].get("message"))
|
||||
return m.get("result", {})
|
||||
def eval(self, expr):
|
||||
r = self.send("Runtime.evaluate", {"expression": expr, "returnByValue": True})
|
||||
return r.get("result", {}).get("value")
|
||||
def close(self):
|
||||
self._stop = True
|
||||
try: self.ws.close()
|
||||
except Exception: pass
|
||||
|
||||
|
||||
def main():
|
||||
pages = [t for t in http_get("/json") if t.get("type") == "page"]
|
||||
tab = next((p for p in pages if ITEM_ID in (p.get("url") or "") and "shopee.tw" in (p.get("url") or "")), None)
|
||||
if not tab:
|
||||
print("[!] 没找到编辑页 tab"); return
|
||||
cdp = CDP(tab["webSocketDebuggerUrl"])
|
||||
cdp.send("Runtime.enable")
|
||||
|
||||
expr = r"""
|
||||
(function(){
|
||||
var xp="//div[@class='container']/div[@class='can-drag shopee-image-manager__itembox' and @data-draggable='true']";
|
||||
var snap=document.evaluate(xp,document,null,XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,null);
|
||||
var n=snap.snapshotLength;
|
||||
var first=n>0?snap.snapshotItem(0):null;
|
||||
|
||||
var fileInputs=[].slice.call(document.querySelectorAll('input[type=file]')).map(function(f){
|
||||
return {accept:f.accept,multiple:f.multiple,name:f.name,cls:f.className,hidden:(f.offsetParent===null)};
|
||||
});
|
||||
|
||||
// 在图片管理器范围内找“设为封面/删除”等小按钮
|
||||
var mgr=document.querySelector('.shopee-image-manager')||document.body;
|
||||
var btns=[].slice.call(mgr.querySelectorAll('*')).filter(function(e){
|
||||
var t=(e.innerText||'').trim();
|
||||
return t && t.length<12 && /(封面|設為|设为|cover|刪除|删除|delete|主圖|主图)/i.test(t);
|
||||
}).slice(0,12).map(function(e){return {tag:e.tagName,cls:String(e.className).slice(0,80),text:(e.innerText||'').trim()};});
|
||||
|
||||
// 第一个 itembox 的结构(截断),看 hover 操作层/删除按钮
|
||||
var firstHTML=first?first.outerHTML.replace(/\s+/g,' ').slice(0,2500):null;
|
||||
|
||||
// 拖拽机制线索:draggable 属性 / 事件库特征
|
||||
var dragInfo=null;
|
||||
if(first){
|
||||
dragInfo={
|
||||
htmlDraggable:first.getAttribute('draggable'),
|
||||
dataDraggable:first.getAttribute('data-draggable'),
|
||||
cls:first.className
|
||||
};
|
||||
}
|
||||
// 上传按钮(“新增/上傳/+”)
|
||||
var addBtn=[].slice.call(mgr.querySelectorAll('*')).filter(function(e){
|
||||
var t=(e.innerText||'').trim();
|
||||
return t && t.length<12 && /(新增|上傳|上传|添加|\+)/.test(t);
|
||||
}).slice(0,6).map(function(e){return {tag:e.tagName,cls:String(e.className).slice(0,80),text:(e.innerText||'').trim()};});
|
||||
|
||||
return JSON.stringify({count:n,fileInputs:fileInputs,actionBtns:btns,addBtns:addBtn,dragInfo:dragInfo,firstHTML:firstHTML},null,2);
|
||||
})()
|
||||
"""
|
||||
out = cdp.eval(expr)
|
||||
print(out)
|
||||
cdp.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,184 @@
|
||||
#!/usr/bin/env python3
|
||||
# set_cover.py —— 上传一张图片到商品图片管理器,等上传成功后拖到第一位(封面)。不保存。
|
||||
#
|
||||
# 依赖:pip install websocket-client requests
|
||||
# 运行:python3 set_cover.py
|
||||
# 环境变量:CDP_HOST(默认 192.168.0.224:9333)、ITEM_ID(默认 51100639510)、
|
||||
# IMG_WIN(Chrome 所在 Windows 上的图片路径,默认 D:\chengma\cmshopee\1_TY030.jpg)
|
||||
|
||||
import os
|
||||
for _k in ("ALL_PROXY", "all_proxy", "HTTP_PROXY", "http_proxy", "HTTPS_PROXY", "https_proxy"):
|
||||
os.environ.pop(_k, None)
|
||||
|
||||
import json
|
||||
import time
|
||||
import threading
|
||||
import requests
|
||||
from websocket import create_connection
|
||||
|
||||
CDP_HOST = os.environ.get("CDP_HOST", "192.168.0.224:9333")
|
||||
BASE = f"http://{CDP_HOST}"
|
||||
ITEM_ID = os.environ.get("ITEM_ID", "51100639510")
|
||||
IMG_WIN = os.environ.get("IMG_WIN", r"D:\chengma\cmshopee\1_TY030.jpg")
|
||||
|
||||
XPATH = "//div[@class='container']/div[@class='can-drag shopee-image-manager__itembox' and @data-draggable='true']"
|
||||
|
||||
|
||||
def http_get(path):
|
||||
s = requests.Session(); s.trust_env = False
|
||||
return s.get(f"{BASE}{path}", timeout=10).json()
|
||||
|
||||
|
||||
class CDP:
|
||||
def __init__(self, ws):
|
||||
self.ws = create_connection(ws, max_size=None, suppress_origin=True)
|
||||
self._id = 0; self._res = {}; self._c = threading.Condition(); self._stop = False
|
||||
threading.Thread(target=self._loop, daemon=True).start()
|
||||
def _loop(self):
|
||||
while not self._stop:
|
||||
try: raw = self.ws.recv()
|
||||
except Exception: break
|
||||
if not raw: continue
|
||||
m = json.loads(raw)
|
||||
if "id" in m:
|
||||
with self._c: self._res[m["id"]] = m; self._c.notify_all()
|
||||
def send(self, method, params=None, timeout=30):
|
||||
self._id += 1; mid = self._id
|
||||
self.ws.send(json.dumps({"id": mid, "method": method, "params": params or {}}))
|
||||
end = time.time() + timeout
|
||||
with self._c:
|
||||
while mid not in self._res:
|
||||
left = end - time.time()
|
||||
if left <= 0: raise TimeoutError(method)
|
||||
self._c.wait(left)
|
||||
m = self._res.pop(mid)
|
||||
if "error" in m: raise RuntimeError(f"{method}: {m['error'].get('message')}")
|
||||
return m.get("result", {})
|
||||
def ev(self, expr, by_value=True):
|
||||
r = self.send("Runtime.evaluate", {"expression": expr, "returnByValue": by_value})
|
||||
if "exceptionDetails" in r:
|
||||
raise RuntimeError("JS 异常: " + r["exceptionDetails"].get("text", ""))
|
||||
return r.get("result", {})
|
||||
def val(self, expr):
|
||||
return self.ev(expr).get("value")
|
||||
def close(self):
|
||||
self._stop = True
|
||||
try: self.ws.close()
|
||||
except Exception: pass
|
||||
|
||||
|
||||
# 返回各 itembox 的 [{i,x,y,left,top,w,h,src}]
|
||||
JS_RECTS = (
|
||||
"(function(){"
|
||||
f"var s=document.evaluate({json.dumps(XPATH)},document,null,XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,null);"
|
||||
"var a=[];for(var i=0;i<s.snapshotLength;i++){var el=s.snapshotItem(i);var r=el.getBoundingClientRect();"
|
||||
"var im=el.querySelector('img');a.push({i:i,x:r.left+r.width/2,y:r.top+r.height/2,"
|
||||
"left:r.left,top:r.top,w:r.width,h:r.height,src:im?im.src:null});}"
|
||||
"return JSON.stringify(a);})()"
|
||||
)
|
||||
|
||||
|
||||
def rects(cdp):
|
||||
return json.loads(cdp.val(JS_RECTS))
|
||||
|
||||
|
||||
def drag(cdp, x0, y0, x1, y1, steps=34):
|
||||
me = "Input.dispatchMouseEvent"
|
||||
cdp.send(me, {"type": "mouseMoved", "x": x0, "y": y0})
|
||||
cdp.send(me, {"type": "mousePressed", "x": x0, "y": y0, "button": "left", "buttons": 1, "clickCount": 1})
|
||||
# 先小幅抖动,越过拖拽启动阈值
|
||||
for dx in (3, 6, 10):
|
||||
cdp.send(me, {"type": "mouseMoved", "x": x0 + dx, "y": y0, "button": "left", "buttons": 1})
|
||||
time.sleep(0.03)
|
||||
for i in range(1, steps + 1):
|
||||
x = x0 + (x1 - x0) * i / steps
|
||||
y = y0 + (y1 - y0) * i / steps
|
||||
cdp.send(me, {"type": "mouseMoved", "x": x, "y": y, "button": "left", "buttons": 1})
|
||||
time.sleep(0.025)
|
||||
for _ in range(10): # 在目标位置停留,让排序库吸附
|
||||
cdp.send(me, {"type": "mouseMoved", "x": x1, "y": y1, "button": "left", "buttons": 1})
|
||||
time.sleep(0.05)
|
||||
cdp.send(me, {"type": "mouseReleased", "x": x1, "y": y1, "button": "left", "buttons": 0, "clickCount": 1})
|
||||
|
||||
|
||||
def main():
|
||||
pages = [t for t in http_get("/json") if t.get("type") == "page"]
|
||||
tab = next((p for p in pages if ITEM_ID in (p.get("url") or "") and "shopee.tw" in (p.get("url") or "")), None)
|
||||
if not tab:
|
||||
print("[!] 没找到编辑页 tab"); return
|
||||
cdp = CDP(tab["webSocketDebuggerUrl"])
|
||||
cdp.send("DOM.enable"); cdp.send("Runtime.enable")
|
||||
|
||||
# 滚动到图片管理器,确保坐标在可视区
|
||||
cdp.val("(function(){var m=document.querySelector('.shopee-image-manager');"
|
||||
"if(m)m.scrollIntoView({block:'center'});return !!m;})()")
|
||||
time.sleep(0.5)
|
||||
|
||||
before = rects(cdp)
|
||||
before_srcs = [r["src"] for r in before]
|
||||
print(f"[*] 上传前 {len(before)} 张图:")
|
||||
for r in before:
|
||||
print(f" [{r['i']}] {r['src']}")
|
||||
|
||||
# ---- 注入文件到商品图 file input ----
|
||||
print(f"\n[*] 上传文件: {IMG_WIN}")
|
||||
# 正确的上传输入框:在“新增圖片”按钮区 .shopee-image-manager__upload 内
|
||||
obj = cdp.ev(
|
||||
"document.querySelector('.shopee-image-manager__upload input[type=file]')",
|
||||
by_value=False,
|
||||
)
|
||||
oid = obj.get("objectId")
|
||||
if not oid:
|
||||
print("[!] 没找到商品图 file input,停止。"); cdp.close(); return
|
||||
cdp.send("DOM.setFileInputFiles", {"objectId": oid, "files": [IMG_WIN]})
|
||||
|
||||
# ---- 等上传成功:张数增加,且新图 src 是 susercontent CDN 链接 ----
|
||||
print("[*] 等待上传完成(轮询新图出现且为 CDN 链接)...")
|
||||
new_src = None
|
||||
deadline = time.time() + 90
|
||||
while time.time() < deadline:
|
||||
time.sleep(1.5)
|
||||
cur = rects(cdp)
|
||||
if len(cur) > len(before):
|
||||
# 找出新增的、且 src 已是正式 CDN 链接的那张
|
||||
news = [r for r in cur if r["src"] not in before_srcs]
|
||||
ready = [r for r in news if r["src"] and "susercontent" in r["src"] and "blob:" not in r["src"]]
|
||||
if ready:
|
||||
new_src = ready[-1]["src"]
|
||||
print(f"[+] 上传成功,新图:{new_src} 当前 {len(cur)} 张")
|
||||
break
|
||||
else:
|
||||
print(f" ...已出现新图但还在处理中({len(cur)} 张)")
|
||||
if not new_src:
|
||||
print("[!] 等待上传超时/未确认成功。可能弹了裁剪框或需要确认,请看浏览器。")
|
||||
cdp.close(); return
|
||||
|
||||
# ---- 拖拽:把新图(最后一张)拖到第一位 ----
|
||||
time.sleep(1)
|
||||
cur = rects(cdp)
|
||||
last = cur[-1]
|
||||
first = cur[0]
|
||||
# 目标落点:第一张的左缘【外侧】,越过其左半区,排序库才会插到最前
|
||||
tx = first["left"] - first["w"] * 0.30
|
||||
ty = first["y"]
|
||||
print(f"\n[*] 拖拽:从 last[{last['i']}]({last['x']:.0f},{last['y']:.0f}) -> 第一位前({tx:.0f},{ty:.0f})")
|
||||
drag(cdp, last["x"], last["y"], tx, ty)
|
||||
time.sleep(1.2)
|
||||
|
||||
# ---- 核对:第一张是不是刚上传的新图 ----
|
||||
after = rects(cdp)
|
||||
print(f"\n[*] 拖拽后顺序({len(after)} 张):")
|
||||
for r in after:
|
||||
flag = " <== 新图" if r["src"] == new_src else ""
|
||||
print(f" [{r['i']}] {r['src']}{flag}")
|
||||
if after and after[0]["src"] == new_src:
|
||||
print("\n[*] ✅ 新图已在第一位(封面)。未保存——请到浏览器核对,确认后再走保存。")
|
||||
else:
|
||||
idx = next((r["i"] for r in after if r["src"] == new_src), None)
|
||||
print(f"\n[!] 新图当前在第 {idx} 位,未到第一位。拖拽可能需要微调(步数/落点/事件类型)。未保存。")
|
||||
|
||||
cdp.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,245 @@
|
||||
#!/usr/bin/env python3
|
||||
# set_title.py —— 在商品编辑页写入新标题(让 Vue/eds 组件感知),可选点击保存。
|
||||
#
|
||||
# 三种模式:
|
||||
# 1) 默认(自检):临时改成 原标题+(測試)→ 验证写入生效 → 还原原标题。不点保存,零持久化。
|
||||
# 2) 设值不保存: NEW_TITLE="新标题" python3 set_title.py —— 写入但不保存,你手动检查/保存。
|
||||
# 3) 设值并保存: NEW_TITLE="新标题" SAVE=1 python3 set_title.py —— 写入并点击保存按钮(真改!)。
|
||||
#
|
||||
# 依赖:pip install websocket-client requests
|
||||
# 环境变量:CDP_HOST(默认 192.168.0.224:9333)、ITEM_ID(默认 51100639510)、NEW_TITLE、SAVE
|
||||
|
||||
import os
|
||||
|
||||
for _k in ("ALL_PROXY", "all_proxy", "HTTP_PROXY", "http_proxy", "HTTPS_PROXY", "https_proxy"):
|
||||
os.environ.pop(_k, None)
|
||||
|
||||
import json
|
||||
import time
|
||||
import threading
|
||||
|
||||
import requests
|
||||
from websocket import create_connection
|
||||
|
||||
CDP_HOST = os.environ.get("CDP_HOST", "192.168.0.224:9333")
|
||||
BASE = f"http://{CDP_HOST}"
|
||||
ITEM_ID = os.environ.get("ITEM_ID", "51100639510")
|
||||
PRODUCT_URL = f"https://seller.shopee.tw/portal/product/{ITEM_ID}?pageEntry=product_list&ignore-html-cache=1"
|
||||
NEW_TITLE = os.environ.get("NEW_TITLE")
|
||||
DO_SAVE = os.environ.get("SAVE") == "1"
|
||||
|
||||
TITLE_XPATH = "//input[@class='eds-input__input' and string-length(@modelvalue)>24]"
|
||||
|
||||
|
||||
def http_get(path):
|
||||
s = requests.Session()
|
||||
s.trust_env = False
|
||||
return s.get(f"{BASE}{path}", timeout=10).json()
|
||||
|
||||
|
||||
class CDP:
|
||||
def __init__(self, ws_url):
|
||||
self.ws = create_connection(ws_url, max_size=None, suppress_origin=True)
|
||||
self._id = 0
|
||||
self._results = {}
|
||||
self._cond = threading.Condition()
|
||||
self._stop = False
|
||||
threading.Thread(target=self._recv_loop, daemon=True).start()
|
||||
|
||||
def _recv_loop(self):
|
||||
while not self._stop:
|
||||
try:
|
||||
raw = self.ws.recv()
|
||||
except Exception:
|
||||
break
|
||||
if not raw:
|
||||
continue
|
||||
msg = json.loads(raw)
|
||||
if "id" in msg:
|
||||
with self._cond:
|
||||
self._results[msg["id"]] = msg
|
||||
self._cond.notify_all()
|
||||
|
||||
def send(self, method, params=None, timeout=30):
|
||||
self._id += 1
|
||||
mid = self._id
|
||||
self.ws.send(json.dumps({"id": mid, "method": method, "params": params or {}}))
|
||||
deadline = time.time() + timeout
|
||||
with self._cond:
|
||||
while mid not in self._results:
|
||||
left = deadline - time.time()
|
||||
if left <= 0:
|
||||
raise TimeoutError(f"CDP {method} 超时")
|
||||
self._cond.wait(left)
|
||||
msg = self._results.pop(mid)
|
||||
if "error" in msg:
|
||||
raise RuntimeError(f"{method} 失败: {msg['error'].get('message')}")
|
||||
return msg.get("result", {})
|
||||
|
||||
def eval(self, expr):
|
||||
r = self.send("Runtime.evaluate", {"expression": expr, "returnByValue": True})
|
||||
if "exceptionDetails" in r:
|
||||
raise RuntimeError(f"页面 JS 异常: {r['exceptionDetails'].get('text')}")
|
||||
return r.get("result", {}).get("value")
|
||||
|
||||
def close(self):
|
||||
self._stop = True
|
||||
try:
|
||||
self.ws.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def find_edit_tab():
|
||||
pages = [t for t in http_get("/json") if t.get("type") == "page"]
|
||||
for p in pages:
|
||||
url = p.get("url") or ""
|
||||
if ITEM_ID in url and "shopee.tw" in url:
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
def open_edit_tab():
|
||||
ver = http_get("/json/version")
|
||||
bcdp = CDP(ver["webSocketDebuggerUrl"])
|
||||
try:
|
||||
tid = bcdp.send("Target.createTarget", {"url": PRODUCT_URL})["targetId"]
|
||||
finally:
|
||||
bcdp.close()
|
||||
deadline = time.time() + 15
|
||||
while time.time() < deadline:
|
||||
for t in http_get("/json"):
|
||||
if t.get("id") == tid and t.get("webSocketDebuggerUrl"):
|
||||
return t["webSocketDebuggerUrl"]
|
||||
time.sleep(0.5)
|
||||
raise TimeoutError("等待新 tab websocket 超时")
|
||||
|
||||
|
||||
# ---- 读取当前标题 ----
|
||||
def js_read():
|
||||
return (
|
||||
"(function(){"
|
||||
f"var r=document.evaluate({json.dumps(TITLE_XPATH)},document,null,"
|
||||
"XPathResult.FIRST_ORDERED_NODE_TYPE,null);"
|
||||
"var el=r.singleNodeValue;"
|
||||
"return el?el.value:null;})()"
|
||||
)
|
||||
|
||||
|
||||
# ---- 写入新标题:原生 setter + 派发事件,让 Vue v-model 感知 ----
|
||||
def js_write(new_title):
|
||||
return (
|
||||
"(function(){"
|
||||
f"var r=document.evaluate({json.dumps(TITLE_XPATH)},document,null,"
|
||||
"XPathResult.FIRST_ORDERED_NODE_TYPE,null);"
|
||||
"var el=r.singleNodeValue;"
|
||||
"if(!el)return 'NO_INPUT';"
|
||||
"var setter=Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value').set;"
|
||||
"el.focus();"
|
||||
f"setter.call(el,{json.dumps(new_title)});"
|
||||
"el.dispatchEvent(new Event('input',{bubbles:true}));"
|
||||
"el.dispatchEvent(new Event('change',{bubbles:true}));"
|
||||
"el.blur();"
|
||||
"return el.value;})()"
|
||||
)
|
||||
|
||||
|
||||
def wait_title(cdp, timeout=40):
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
v = cdp.eval(js_read())
|
||||
if v:
|
||||
return v
|
||||
time.sleep(1)
|
||||
return None
|
||||
|
||||
|
||||
def click_save(cdp):
|
||||
# 按钮文字常见为 儲存/保存/Save;找可见且可点击的那个。
|
||||
expr = (
|
||||
"(function(){"
|
||||
"var btns=[].slice.call(document.querySelectorAll('button'));"
|
||||
"var t=btns.filter(function(b){var s=(b.innerText||'').trim();"
|
||||
"return /儲存|保存|Save/i.test(s) && b.offsetParent!==null && !b.disabled;});"
|
||||
"if(!t.length)return 'NO_SAVE_BTN';"
|
||||
"t[0].click();return 'CLICKED:'+(t[0].innerText||'').trim();})()"
|
||||
)
|
||||
return cdp.eval(expr)
|
||||
|
||||
|
||||
def main():
|
||||
tab = find_edit_tab()
|
||||
if tab:
|
||||
print(f"[*] 复用编辑页 tab: {tab['url']}")
|
||||
page_ws = tab["webSocketDebuggerUrl"]
|
||||
else:
|
||||
print(f"[*] 新建 tab 打开: {PRODUCT_URL}")
|
||||
page_ws = open_edit_tab()
|
||||
|
||||
cdp = CDP(page_ws)
|
||||
try:
|
||||
cdp.send("Runtime.enable")
|
||||
print("[*] 等待标题输入框渲染...")
|
||||
original = wait_title(cdp)
|
||||
if not original:
|
||||
print("[!] 没读到标题输入框,退出。")
|
||||
return
|
||||
print(f"[*] 当前标题: {original}")
|
||||
|
||||
if NEW_TITLE is None:
|
||||
# ---- 自检模式:临时改 → 验证 → 还原,全程不保存 ----
|
||||
test = (original + "(測試)")[:255]
|
||||
print(f"\n[自检] 临时写入: {test}")
|
||||
cdp.eval(js_write(test))
|
||||
time.sleep(0.5)
|
||||
after = cdp.eval(js_read())
|
||||
print(f"[自检] 读回: {after}")
|
||||
ok = after == test
|
||||
print(f"[自检] 写入生效: {'✅ 是' if ok else '❌ 否'}")
|
||||
print(f"\n[自检] 还原原标题: {original}")
|
||||
cdp.eval(js_write(original))
|
||||
time.sleep(0.5)
|
||||
restored = cdp.eval(js_read())
|
||||
print(f"[自检] 读回: {restored}")
|
||||
print(f"[自检] 已还原: {'✅ 是' if restored == original else '❌ 否(请手动检查)'}")
|
||||
print("\n[自检] 全程未点保存,页面未持久化任何改动。")
|
||||
print(" 真改请用: NEW_TITLE=\"新标题\" SAVE=1 python3 set_title.py")
|
||||
return
|
||||
|
||||
# ---- 真实写入 ----
|
||||
print(f"\n[*] 写入新标题: {NEW_TITLE}")
|
||||
cdp.eval(js_write(NEW_TITLE))
|
||||
time.sleep(0.5)
|
||||
after = cdp.eval(js_read())
|
||||
# modelvalue 是 Vue 模型反向绑定到 DOM 的属性,等于新值才说明 v-model 真的更新了
|
||||
model = cdp.eval(
|
||||
"(function(){"
|
||||
f"var r=document.evaluate({json.dumps(TITLE_XPATH)},document,null,"
|
||||
"XPathResult.FIRST_ORDERED_NODE_TYPE,null);"
|
||||
"var el=r.singleNodeValue;return el?el.getAttribute('modelvalue'):null;})()"
|
||||
)
|
||||
print(f"[*] 读回 value: {after}")
|
||||
print(f"[*] 读回 modelvalue: {model}")
|
||||
if after != NEW_TITLE:
|
||||
print("[!] value 与新标题不一致,停止(不保存)。")
|
||||
return
|
||||
if model != NEW_TITLE:
|
||||
print("[!] modelvalue 未跟随更新,Vue 模型可能没绑定,保存会提交旧值,停止(不保存)。")
|
||||
return
|
||||
print("[*] ✅ value 与 modelvalue 均=新标题,Vue 模型已更新。")
|
||||
|
||||
if not DO_SAVE:
|
||||
print("\n[*] 已写入但未保存(SAVE!=1)。请到浏览器里核对后手动保存,或加 SAVE=1 重跑。")
|
||||
return
|
||||
|
||||
print("[*] 点击保存按钮...")
|
||||
r = click_save(cdp)
|
||||
print(f"[*] 保存结果: {r}")
|
||||
if r.startswith("CLICKED"):
|
||||
print("[*] 已触发保存。请到浏览器确认是否有校验弹窗/成功提示。")
|
||||
finally:
|
||||
cdp.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user