browser-workspace 0.4.1 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -2
- package/SKILL.md +60 -37
- package/bin/__pycache__/browser-workspacecpython-311.pyc +0 -0
- package/bin/browser-workspace +174 -13
- package/package.json +1 -1
- package/platform_runner.py +23 -6
- package/platforms/chatgpt/actions/_temporary_submit.py +22 -2
- package/platforms/chatgpt/tests/test_contract.py +7 -0
- package/platforms/wechat-channels/actions/_common.py +1 -1
- package/platforms/wechat-channels/actions/_post.py +37 -38
- package/platforms/wechat-channels/manifest.yaml +19 -13
- package/platforms/xhs/actions/_post.py +109 -20
- package/platforms/xhs/manifest.yaml +14 -5
- package/platforms/xhs/tests/test_contract.py +17 -0
- package/platforms/youtube/actions/_post.py +7 -4
- package/pyproject.toml +6 -2
- package/session_client.py +21 -11
- package/session_daemon.py +115 -9
- package/src/browser_workspace.egg-info/PKG-INFO +2 -1
- package/src/browser_workspace.egg-info/SOURCES.txt +84 -15
- package/src/browser_workspace.egg-info/entry_points.txt +2 -0
- package/src/browser_workspace.egg-info/requires.txt +1 -0
- package/src/browser_workspace.egg-info/top_level.txt +1 -1
- package/src/browser_workspace_app/__init__.py +1 -0
- package/src/browser_workspace_app/agent-workspace/agent_helpers.py +718 -0
- package/src/browser_workspace_app/cli.py +236 -0
- package/src/browser_workspace_app/extension/manifest.json +26 -0
- package/src/browser_workspace_app/extension/service-worker.mjs +126 -0
- package/src/browser_workspace_app/extension/workspace-manager.mjs +680 -0
- package/src/browser_workspace_app/extension/workspace-page.mjs +7 -0
- package/src/browser_workspace_app/extension/workspace.html +8 -0
- package/src/browser_workspace_app/interaction-skills/connection.md +52 -0
- package/src/browser_workspace_app/interaction-skills/cookies.md +3 -0
- package/src/browser_workspace_app/interaction-skills/cross-origin-iframes.md +3 -0
- package/src/browser_workspace_app/interaction-skills/dialogs.md +64 -0
- package/src/browser_workspace_app/interaction-skills/downloads.md +3 -0
- package/src/browser_workspace_app/interaction-skills/drag-and-drop.md +3 -0
- package/src/browser_workspace_app/interaction-skills/dropdowns.md +3 -0
- package/src/browser_workspace_app/interaction-skills/iframes.md +3 -0
- package/src/browser_workspace_app/interaction-skills/network-requests.md +3 -0
- package/src/browser_workspace_app/interaction-skills/print-as-pdf.md +3 -0
- package/src/browser_workspace_app/interaction-skills/profile-sync.md +90 -0
- package/src/browser_workspace_app/interaction-skills/screenshots.md +17 -0
- package/src/browser_workspace_app/interaction-skills/scrolling.md +22 -0
- package/src/browser_workspace_app/interaction-skills/shadow-dom.md +3 -0
- package/src/browser_workspace_app/interaction-skills/tabs.md +73 -0
- package/src/browser_workspace_app/interaction-skills/uploads.md +1 -0
- package/src/browser_workspace_app/interaction-skills/viewport.md +16 -0
- package/src/browser_workspace_app/licenses/browser-harness-MIT.txt +21 -0
- package/src/browser_workspace_app/managed_chrome.py +244 -0
- package/src/browser_workspace_app/node/admin.mjs +44 -0
- package/src/browser_workspace_app/package.json +36 -0
- package/src/browser_workspace_app/platform_runner.py +204 -0
- package/src/browser_workspace_app/platforms/chatgpt/README.md +60 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_bootstrap_start.py +58 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_lifecycle_common.py +119 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_mcp_app.py +80 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_new_thread.py +85 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_new_turn.py +33 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_project_setup.py +353 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_readiness.py +52 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_submit_thread.py +129 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_temporary_submit.py +591 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_thread_turn.py +362 -0
- package/src/browser_workspace_app/platforms/chatgpt/manifest.yaml +34 -0
- package/src/browser_workspace_app/platforms/chatgpt/tests/test_contract.py +183 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/README.md +49 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/actions/_chrome_web_store.py +425 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/check-status.md +7 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/open-item.md +10 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/submit-review.md +10 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/update-listing.md +18 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/upload-package.md +11 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/verify-published.md +9 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/manifest.yaml +33 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/tests/test_contract.py +83 -0
- package/src/browser_workspace_app/platforms/microsoft-teams/README.md +32 -0
- package/src/browser_workspace_app/platforms/microsoft-teams/actions/_meeting.py +210 -0
- package/src/browser_workspace_app/platforms/microsoft-teams/flows/instant-meeting.md +21 -0
- package/src/browser_workspace_app/platforms/microsoft-teams/manifest.yaml +25 -0
- package/src/browser_workspace_app/platforms/microsoft-teams/tests/test_contract.py +24 -0
- package/src/browser_workspace_app/platforms/tiktok/README.md +10 -0
- package/src/browser_workspace_app/platforms/tiktok/actions/_post.py +180 -0
- package/src/browser_workspace_app/platforms/tiktok/manifest.yaml +99 -0
- package/src/browser_workspace_app/platforms/wechat-channels/README.md +10 -0
- package/src/browser_workspace_app/platforms/wechat-channels/actions/_common.py +163 -0
- package/src/browser_workspace_app/platforms/wechat-channels/actions/_manage.py +130 -0
- package/src/browser_workspace_app/platforms/wechat-channels/actions/_post.py +231 -0
- package/src/browser_workspace_app/platforms/wechat-channels/manifest.yaml +124 -0
- package/src/browser_workspace_app/platforms/wechat-channels/tests/test_contract.py +36 -0
- package/src/browser_workspace_app/platforms/xhs/README.md +10 -0
- package/src/browser_workspace_app/platforms/xhs/actions/_manage.py +355 -0
- package/src/browser_workspace_app/platforms/xhs/actions/_post.py +281 -0
- package/src/browser_workspace_app/platforms/xhs/manifest.yaml +107 -0
- package/src/browser_workspace_app/platforms/xhs/tests/test_contract.py +34 -0
- package/src/browser_workspace_app/platforms/youtube/README.md +10 -0
- package/src/browser_workspace_app/platforms/youtube/actions/_post.py +103 -0
- package/src/browser_workspace_app/platforms/youtube/manifest.yaml +94 -0
- package/src/browser_workspace_app/pyproject.toml +33 -0
- package/src/browser_workspace_app/session_client.py +107 -0
- package/src/browser_workspace_app/session_daemon.py +431 -0
- package/src/browser_harness/__init__.py +0 -2
- package/src/browser_harness/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/browser_harness/_ipc.py +0 -201
- package/src/browser_harness/admin.py +0 -1588
- package/src/browser_harness/auth.py +0 -546
- package/src/browser_harness/daemon.py +0 -894
- package/src/browser_harness/helpers.py +0 -668
- package/src/browser_harness/macos.py +0 -131
- package/src/browser_harness/mcp_cli.py +0 -16
- package/src/browser_harness/paths.py +0 -49
- package/src/browser_harness/recorder.py +0 -343
- package/src/browser_harness/run.py +0 -413
- package/src/browser_harness/telemetry.py +0 -308
- package/src/browser_harness/video-template.html +0 -970
- package/src/browser_harness/video.py +0 -749
- package/src/browser_harness/video_render.py +0 -524
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import json, time, re
|
|
2
|
+
|
|
3
|
+
CFG = json.load(open("__CFG_PATH__"))
|
|
4
|
+
OP = CFG["operation"]
|
|
5
|
+
TARGET_NOTE_ID = CFG.get("note_id")
|
|
6
|
+
TARGET_TITLE = CFG.get("title")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def open_or_reuse_xhs(url):
|
|
11
|
+
current=current_tab()
|
|
12
|
+
current_url=current.get("url","")
|
|
13
|
+
if "xiaohongshu.com" in current_url:
|
|
14
|
+
target=current
|
|
15
|
+
else:
|
|
16
|
+
tabs=[t for t in list_tabs() if "xiaohongshu.com" in (t.get("url") or "")]
|
|
17
|
+
target=tabs[0] if tabs else None
|
|
18
|
+
if target:
|
|
19
|
+
switch_tab(target)
|
|
20
|
+
if (current_tab().get("url") or "") != url:
|
|
21
|
+
goto_url(url)
|
|
22
|
+
else:
|
|
23
|
+
new_tab(url)
|
|
24
|
+
return current_tab()
|
|
25
|
+
|
|
26
|
+
def out(payload):
|
|
27
|
+
print(json.dumps(payload, ensure_ascii=False, separators=(",", ":")))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def resolve_manager_card():
|
|
31
|
+
open_or_reuse_xhs("https://creator.xiaohongshu.com/new/note-manager")
|
|
32
|
+
try:
|
|
33
|
+
wait_for_load()
|
|
34
|
+
except Exception:
|
|
35
|
+
pass
|
|
36
|
+
raw=""
|
|
37
|
+
for _ in range(20):
|
|
38
|
+
time.sleep(.5)
|
|
39
|
+
try:
|
|
40
|
+
raw = js('Array.from(document.querySelectorAll(".note-card")).map(function(c){var t=c.querySelector(".note-card__title"); return JSON.stringify({title:t?(t.innerText||t.textContent||"").trim():"", impression:c.dataset.impression||"", text:(c.innerText||"").trim()})}).join("\\n")') or ""
|
|
41
|
+
except Exception:
|
|
42
|
+
raw=""
|
|
43
|
+
if raw:
|
|
44
|
+
break
|
|
45
|
+
rows=[]
|
|
46
|
+
for line in (raw or "").splitlines():
|
|
47
|
+
try:
|
|
48
|
+
row=json.loads(line)
|
|
49
|
+
imp=json.loads(row.get("impression") or "{}")
|
|
50
|
+
row["note_id"]=(((imp.get("noteTarget") or {}).get("value") or {}).get("noteId"))
|
|
51
|
+
rows.append(row)
|
|
52
|
+
except Exception:
|
|
53
|
+
pass
|
|
54
|
+
for row in rows:
|
|
55
|
+
if TARGET_NOTE_ID and row.get("note_id")==TARGET_NOTE_ID:
|
|
56
|
+
return row
|
|
57
|
+
if TARGET_TITLE and row.get("title")==TARGET_TITLE:
|
|
58
|
+
return row
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def card_exists_in_current_tab(note_id, title):
|
|
63
|
+
return js('Array.from(document.querySelectorAll(".note-card")).some(function(c){var t=c.querySelector(".note-card__title"); var ttl=t?(t.innerText||t.textContent||"").trim():""; var imp=c.dataset.impression||""; return '+json.dumps(bool(note_id))+' ? imp.indexOf('+json.dumps(note_id or "")+')>=0 : ttl==='+json.dumps(title or "")+'}) ? "yes" : "no"') == "yes"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def resolve_status(row):
|
|
67
|
+
text=row.get("text") or ""
|
|
68
|
+
if "审核中" in text:
|
|
69
|
+
return "reviewing","审核中"
|
|
70
|
+
if "未通过" in text:
|
|
71
|
+
return "rejected","未通过"
|
|
72
|
+
for label,status in [("审核中","reviewing"),("未通过","rejected"),("已发布","published")]:
|
|
73
|
+
clicked=js('var es=Array.from(document.querySelectorAll("*")).filter(function(e){return e.children.length===0 && e.offsetParent && (e.innerText||e.textContent||"").trim()==='+json.dumps(label)+'}); if(es.length){es[0].click();"yes"}else{"no"}')
|
|
74
|
+
if clicked=="yes":
|
|
75
|
+
time.sleep(1.2)
|
|
76
|
+
if card_exists_in_current_tab(row.get("note_id"), row.get("title")):
|
|
77
|
+
return status,label
|
|
78
|
+
return "unknown","unknown"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def open_update_editor(row):
|
|
82
|
+
note_id=row.get("note_id") or ""
|
|
83
|
+
title=row.get("title") or ""
|
|
84
|
+
clicked=js('var cards=Array.from(document.querySelectorAll(".note-card")); var c=cards.find(function(c){var t=c.querySelector(".note-card__title"); var ttl=t?(t.innerText||t.textContent||"").trim():""; var imp=c.dataset.impression||""; return '+json.dumps(bool(note_id))+' ? imp.indexOf('+json.dumps(note_id)+')>=0 : ttl==='+json.dumps(title)+'}); if(!c){"missing-card"}else{var a=c.querySelectorAll(".note-card__action-btn")[3]; if(a){a.click();"clicked"}else{"missing-edit"}}')
|
|
85
|
+
if clicked!="clicked":
|
|
86
|
+
return None, clicked
|
|
87
|
+
for _ in range(30):
|
|
88
|
+
time.sleep(.5)
|
|
89
|
+
url=current_tab().get("url") or ""
|
|
90
|
+
if "/publish/update" in url:
|
|
91
|
+
tab=current_tab()
|
|
92
|
+
return {"update_url":url,"target_id":tab.get("targetId") or tab.get("target_id")}, None
|
|
93
|
+
return None,"update_editor_timeout"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def editor_media_kind():
|
|
97
|
+
accepts=""
|
|
98
|
+
for _ in range(30):
|
|
99
|
+
try:
|
|
100
|
+
accepts=js('Array.from(document.querySelectorAll("input[type=file]")).map(function(e){return e.accept||""}).join("|")') or ""
|
|
101
|
+
except Exception:
|
|
102
|
+
accepts=""
|
|
103
|
+
if accepts:
|
|
104
|
+
break
|
|
105
|
+
time.sleep(.5)
|
|
106
|
+
if any(ext in accepts for ext in (".mp4",".mov",".mkv",".mpeg")):
|
|
107
|
+
return "video"
|
|
108
|
+
if any(ext in accepts for ext in (".jpg",".jpeg",".png",".webp")):
|
|
109
|
+
return "image"
|
|
110
|
+
return "unknown"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def set_update_fields(editor):
|
|
114
|
+
target_id=editor.get("target_id")
|
|
115
|
+
if target_id:
|
|
116
|
+
switch_tab(target_id)
|
|
117
|
+
if CFG.get("video"):
|
|
118
|
+
if editor_media_kind()!="video":
|
|
119
|
+
return False,"media_type_mismatch_video"
|
|
120
|
+
upload_file('input[type=file][accept*=".mp4"],input.upload-input', CFG["video"])
|
|
121
|
+
if target_id:
|
|
122
|
+
switch_tab(target_id)
|
|
123
|
+
for _ in range(120):
|
|
124
|
+
time.sleep(1)
|
|
125
|
+
text=js('(document.body.innerText||"").slice(0,1800)') or ""
|
|
126
|
+
# Once the editor is usable again and no obvious upload-progress copy is visible,
|
|
127
|
+
# proceed; final publish button enabled state is checked below.
|
|
128
|
+
if "上传中" not in text and "处理中" not in text:
|
|
129
|
+
break
|
|
130
|
+
if CFG.get("images"):
|
|
131
|
+
if editor_media_kind()!="image":
|
|
132
|
+
return False,"media_type_mismatch_image"
|
|
133
|
+
selector='input[type=file][accept*=".jpg"],input[type=file][accept*=".jpeg"],input[type=file][accept*=".png"]'
|
|
134
|
+
# upload_file accepts one file per call; replacing one image is
|
|
135
|
+
# enough for current Vlog use. Multiple-image update remains explicit and bounded.
|
|
136
|
+
first=True
|
|
137
|
+
for image in CFG["images"]:
|
|
138
|
+
upload_file(selector, image)
|
|
139
|
+
if target_id:
|
|
140
|
+
switch_tab(target_id)
|
|
141
|
+
time.sleep(1.5)
|
|
142
|
+
first=False
|
|
143
|
+
if target_id:
|
|
144
|
+
switch_tab(target_id)
|
|
145
|
+
if CFG.get("new_title") is not None:
|
|
146
|
+
fill_input('input[placeholder="填写标题会有更多赞哦"]', CFG["new_title"], clear_first=True, timeout=20)
|
|
147
|
+
if CFG.get("body") is not None:
|
|
148
|
+
fill_input('.tiptap.ProseMirror,[contenteditable=true]', CFG["body"], clear_first=True, timeout=20)
|
|
149
|
+
return True,None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def submit_update():
|
|
153
|
+
# Upload processing can keep the publish control disabled for a while. Wait for
|
|
154
|
+
# the real control to become enabled rather than failing immediately.
|
|
155
|
+
clicked=False
|
|
156
|
+
for _ in range(180):
|
|
157
|
+
try:
|
|
158
|
+
state=js('var b=Array.from(document.querySelectorAll("button,[role=button]")).find(function(e){return e.offsetParent && (e.innerText||e.textContent||"").trim()==="发布"}); if(!b){"missing"}else if(b.disabled || (b.getAttribute("aria-disabled")||"false")==="true"){"disabled"}else{b.click();"clicked"}')
|
|
159
|
+
if state=="clicked":
|
|
160
|
+
clicked=True
|
|
161
|
+
break
|
|
162
|
+
if state=="missing":
|
|
163
|
+
nodes=cdp("Accessibility.getFullAXTree")["nodes"]
|
|
164
|
+
target=next((n for n in nodes if (n.get("role") or {}).get("value")=="button" and (n.get("name") or {}).get("value")=="发布" and n.get("backendDOMNodeId")),None)
|
|
165
|
+
if target:
|
|
166
|
+
obj=cdp("DOM.resolveNode", backendNodeId=target["backendDOMNodeId"])["object"]["objectId"]
|
|
167
|
+
value=cdp("Runtime.callFunctionOn", objectId=obj, functionDeclaration='function(){if(this.disabled||this.getAttribute("aria-disabled")==="true")return "disabled";this.click();return "clicked"}', returnByValue=True)["result"].get("value")
|
|
168
|
+
if value=="clicked":
|
|
169
|
+
clicked=True
|
|
170
|
+
break
|
|
171
|
+
except Exception:
|
|
172
|
+
pass
|
|
173
|
+
time.sleep(1)
|
|
174
|
+
if not clicked:
|
|
175
|
+
return False,"publish_button_missing_or_disabled"
|
|
176
|
+
for _ in range(30):
|
|
177
|
+
time.sleep(1)
|
|
178
|
+
try:
|
|
179
|
+
url=js('location.href') or ""
|
|
180
|
+
if "/publish/success" in url or "published=true" in url:
|
|
181
|
+
return True,None
|
|
182
|
+
except Exception:
|
|
183
|
+
pass
|
|
184
|
+
return False,"update_submit_unverified"
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def own_profile_url():
|
|
188
|
+
open_or_reuse_xhs("https://www.xiaohongshu.com")
|
|
189
|
+
time.sleep(3)
|
|
190
|
+
href=js('var a=Array.from(document.querySelectorAll("a[href*=\\"/user/profile/\\"]")).find(function(a){return (a.innerText||a.textContent||"").trim()==="我"}); a?a.href:""')
|
|
191
|
+
return href or None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def open_post_detail(note_id):
|
|
195
|
+
profile=own_profile_url()
|
|
196
|
+
if not profile:
|
|
197
|
+
return None, "own_profile_not_found"
|
|
198
|
+
goto_url(profile); time.sleep(3)
|
|
199
|
+
selector='section.note-item[data-note-id="'+note_id+'"]'
|
|
200
|
+
href=""
|
|
201
|
+
for _ in range(30):
|
|
202
|
+
href=js('var e=document.querySelector('+json.dumps(selector)+' ); var a=e&&e.querySelector("a.cover,a.title"); a?a.href:""')
|
|
203
|
+
if href:
|
|
204
|
+
break
|
|
205
|
+
js('window.scrollBy(0, Math.max(window.innerHeight*0.85, 600)); "scrolled"')
|
|
206
|
+
time.sleep(.6)
|
|
207
|
+
if not href:
|
|
208
|
+
return None, "post_not_visible_in_my_posts"
|
|
209
|
+
# The My Posts card carries the live xsec token. Following that route opens the
|
|
210
|
+
# post detail overlay/page; raw /explore/<id> URLs can 404 without this token.
|
|
211
|
+
goto_url(href); time.sleep(3)
|
|
212
|
+
ready=js('document.querySelector(".note-container") ? "yes" : "no"')
|
|
213
|
+
if ready!="yes":
|
|
214
|
+
return None, "post_detail_not_opened"
|
|
215
|
+
return {"profile_url":profile,"detail_url":current_tab().get("url") or ""}, None
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def expand_replies():
|
|
219
|
+
for _ in range(8):
|
|
220
|
+
n=js('var es=Array.from(document.querySelectorAll(".comments-el *")).filter(function(e){var t=(e.innerText||e.textContent||"").trim(); return e.offsetParent && e.children.length===0 && (/^展开\\s*\\d+\\s*条回复$/.test(t) || t==="展开更多回复" || t==="展开更多")}); es.slice(0,20).forEach(function(e){e.click()}); es.length')
|
|
221
|
+
try:
|
|
222
|
+
count=int(n or 0)
|
|
223
|
+
except Exception:
|
|
224
|
+
count=0
|
|
225
|
+
if count<=0:
|
|
226
|
+
break
|
|
227
|
+
time.sleep(.8)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def detail_comments(limit):
|
|
231
|
+
expand_replies()
|
|
232
|
+
raw=js('Array.from(document.querySelectorAll(".comment-item")).map(function(c,i){var author=c.querySelector(".author .name"); var content=c.querySelector(".content .note-text"); var date=c.querySelector(".date > span:first-child"); var loc=c.querySelector(".date .location"); var reply=c.querySelector(".reply .count"); return JSON.stringify({dom_index:i,comment_id:(c.id||"").replace(/^comment-/,""),author:author?(author.innerText||author.textContent||"").trim():"",text:content?(content.innerText||content.textContent||"").trim():"",date:date?(date.innerText||date.textContent||"").trim():"",location:loc?(loc.innerText||loc.textContent||"").trim():"",is_reply:c.classList.contains("comment-item-sub"),reply_label:reply?(reply.innerText||reply.textContent||"").trim():"",replyable:!!c.querySelector(".reply")})}).join("\\n")')
|
|
233
|
+
rows=[]
|
|
234
|
+
for line in (raw or "").splitlines():
|
|
235
|
+
try:
|
|
236
|
+
row=json.loads(line)
|
|
237
|
+
row["comment_index"]=len(rows)
|
|
238
|
+
rows.append(row)
|
|
239
|
+
if len(rows)>=limit:
|
|
240
|
+
break
|
|
241
|
+
except Exception:
|
|
242
|
+
pass
|
|
243
|
+
return rows
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def activate_composer_for_comment():
|
|
247
|
+
# Top-level composer is present but inert until the visible "评论" affordance is clicked.
|
|
248
|
+
active=js('var e=document.querySelector("p.content-input[contenteditable=true]"); e&&e.offsetParent ? "yes" : "no"')
|
|
249
|
+
if active=="yes":
|
|
250
|
+
return True
|
|
251
|
+
clicked=js('var e=document.querySelector(".content-edit .inner-when-not-active, .content-edit .inner"); if(e){e.click();"yes"}else{"no"}')
|
|
252
|
+
if clicked!="yes":
|
|
253
|
+
return False
|
|
254
|
+
time.sleep(.4)
|
|
255
|
+
return js('var e=document.querySelector("p.content-input[contenteditable=true]"); e&&e.offsetParent ? "yes" : "no"')=="yes"
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def type_and_send(text):
|
|
259
|
+
focused=js('var e=document.querySelector("p.content-input[contenteditable=true]"); if(e&&e.offsetParent){e.focus();e.click();"yes"}else{"no"}')
|
|
260
|
+
if focused!="yes":
|
|
261
|
+
return False,"comment_editor_missing"
|
|
262
|
+
type_text(text)
|
|
263
|
+
time.sleep(.2)
|
|
264
|
+
sent=js('var b=document.querySelector("button.btn.submit,button.submit"); if(b && b.offsetParent && !b.disabled){b.click();"yes"}else{"no"}')
|
|
265
|
+
if sent!="yes":
|
|
266
|
+
return False,"comment_submit_missing_or_disabled"
|
|
267
|
+
time.sleep(1.5)
|
|
268
|
+
return True,None
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
row=resolve_manager_card()
|
|
272
|
+
if not row:
|
|
273
|
+
out({"ok":False,"operation":OP,"error":"post_not_found","note_id":TARGET_NOTE_ID,"title":TARGET_TITLE})
|
|
274
|
+
raise SystemExit(2)
|
|
275
|
+
|
|
276
|
+
if OP=="status":
|
|
277
|
+
status,label=resolve_status(row)
|
|
278
|
+
out({"ok":True,"operation":"status","note_id":row.get("note_id"),"title":row.get("title"),"status":status,"status_label":label,"card_text":row.get("text")})
|
|
279
|
+
raise SystemExit(0)
|
|
280
|
+
|
|
281
|
+
if OP=="update":
|
|
282
|
+
editor,err=open_update_editor(row)
|
|
283
|
+
if err:
|
|
284
|
+
out({"ok":False,"operation":"update","error":err,"note_id":row.get("note_id"),"title":row.get("title")}); raise SystemExit(5)
|
|
285
|
+
ok,err=set_update_fields(editor)
|
|
286
|
+
if not ok:
|
|
287
|
+
out({"ok":False,"operation":"update","error":err,"note_id":row.get("note_id"),"title":row.get("title"),"media_kind":editor_media_kind()}); raise SystemExit(4)
|
|
288
|
+
submit_ok,submit_err=submit_update()
|
|
289
|
+
# XHS edits sometimes apply without a stable success redirect. Platform-side
|
|
290
|
+
# note-manager verification is authoritative and prevents unsafe blind retries.
|
|
291
|
+
open_or_reuse_xhs("https://creator.xiaohongshu.com/new/note-manager")
|
|
292
|
+
try:
|
|
293
|
+
wait_for_load()
|
|
294
|
+
except Exception:
|
|
295
|
+
pass
|
|
296
|
+
time.sleep(2)
|
|
297
|
+
new_title=CFG.get("new_title") or row.get("title")
|
|
298
|
+
found=card_exists_in_current_tab(row.get("note_id"), new_title)
|
|
299
|
+
if found:
|
|
300
|
+
out({"ok":True,"operation":"update","note_id":row.get("note_id"),"old_title":row.get("title"),"title":new_title,"media_replaced":bool(CFG.get("video") or CFG.get("images")),"verified":True,"submit_redirect_verified":submit_ok})
|
|
301
|
+
raise SystemExit(0)
|
|
302
|
+
out({"ok":False,"operation":"update","error":submit_err or "platform_verification_failed","note_id":row.get("note_id"),"old_title":row.get("title"),"title":new_title,"verified":False})
|
|
303
|
+
raise SystemExit(8)
|
|
304
|
+
|
|
305
|
+
route,err=open_post_detail(row.get("note_id"))
|
|
306
|
+
if err:
|
|
307
|
+
out({"ok":False,"operation":OP,"error":err,"note_id":row.get("note_id"),"title":row.get("title")})
|
|
308
|
+
raise SystemExit(6)
|
|
309
|
+
|
|
310
|
+
if OP=="comments":
|
|
311
|
+
rows=detail_comments(int(CFG.get("limit",50)))
|
|
312
|
+
out({"ok":True,"operation":"comments","note_id":row.get("note_id"),"title":row.get("title"),"source":"my_posts_detail","detail_url":route["detail_url"],"count":len(rows),"comments":rows})
|
|
313
|
+
raise SystemExit(0)
|
|
314
|
+
|
|
315
|
+
if OP=="comment":
|
|
316
|
+
if not activate_composer_for_comment():
|
|
317
|
+
out({"ok":False,"operation":"comment","error":"comment_editor_missing"}); raise SystemExit(5)
|
|
318
|
+
ok,send_err=type_and_send(CFG["text"])
|
|
319
|
+
if not ok:
|
|
320
|
+
out({"ok":False,"operation":"comment","error":send_err}); raise SystemExit(5)
|
|
321
|
+
verified=js('Array.from(document.querySelectorAll(".comment-item .content .note-text")).some(function(e){return (e.innerText||e.textContent||"").trim()==='+json.dumps(CFG["text"])+ '}) ? "yes" : "no"')=="yes"
|
|
322
|
+
out({"ok":verified,"operation":"comment","note_id":row.get("note_id"),"title":row.get("title"),"text":CFG["text"],"verified":verified})
|
|
323
|
+
raise SystemExit(0 if verified else 7)
|
|
324
|
+
|
|
325
|
+
if OP=="reply":
|
|
326
|
+
rows=detail_comments(200)
|
|
327
|
+
target=None
|
|
328
|
+
if CFG.get("comment_id"):
|
|
329
|
+
target=next((r for r in rows if r.get("comment_id")==CFG["comment_id"]),None)
|
|
330
|
+
elif CFG.get("comment_index") is not None:
|
|
331
|
+
idx=int(CFG["comment_index"]); target=next((r for r in rows if r["comment_index"]==idx),None)
|
|
332
|
+
else:
|
|
333
|
+
needle=CFG.get("contains") or ""
|
|
334
|
+
matches=[r for r in rows if needle in (r.get("text") or "")]
|
|
335
|
+
if len(matches)==1: target=matches[0]
|
|
336
|
+
elif len(matches)>1:
|
|
337
|
+
out({"ok":False,"operation":"reply","error":"ambiguous_comment_match","matches":len(matches)}); raise SystemExit(3)
|
|
338
|
+
if not target:
|
|
339
|
+
out({"ok":False,"operation":"reply","error":"comment_not_found"}); raise SystemExit(2)
|
|
340
|
+
if not target.get("replyable"):
|
|
341
|
+
out({"ok":False,"operation":"reply","error":"comment_not_replyable","comment":target}); raise SystemExit(4)
|
|
342
|
+
cid=target.get("comment_id")
|
|
343
|
+
opened=js('var c=document.getElementById('+json.dumps("comment-"+(cid or ""))+'); var r=c&&c.querySelector(".reply"); if(r){r.click();"yes"}else{"no"}')
|
|
344
|
+
if opened!="yes":
|
|
345
|
+
out({"ok":False,"operation":"reply","error":"reply_ui_missing"}); raise SystemExit(5)
|
|
346
|
+
time.sleep(.4)
|
|
347
|
+
ok,send_err=type_and_send(CFG["text"])
|
|
348
|
+
if not ok:
|
|
349
|
+
out({"ok":False,"operation":"reply","error":send_err}); raise SystemExit(5)
|
|
350
|
+
verified=js('Array.from(document.querySelectorAll(".comment-item .content .note-text")).some(function(e){return (e.innerText||e.textContent||"").trim()==='+json.dumps(CFG["text"])+ '}) ? "yes" : "no"')=="yes"
|
|
351
|
+
out({"ok":verified,"operation":"reply","note_id":row.get("note_id"),"title":row.get("title"),"replied_to":target,"text":CFG["text"],"verified":verified})
|
|
352
|
+
raise SystemExit(0 if verified else 7)
|
|
353
|
+
|
|
354
|
+
out({"ok":False,"error":"unsupported_operation","operation":OP})
|
|
355
|
+
raise SystemExit(2)
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import time, json, os
|
|
2
|
+
|
|
3
|
+
CFG = json.load(open("__CFG_PATH__"))
|
|
4
|
+
|
|
5
|
+
# NOTE: XHS overrides JSON.stringify (anti-bot). This script avoids it entirely.
|
|
6
|
+
# Use js() for scalar returns only; use .click() instead of click_at_xy with rects.
|
|
7
|
+
|
|
8
|
+
mode = CFG.get("mode", "image")
|
|
9
|
+
|
|
10
|
+
def open_or_reuse_xhs(url):
|
|
11
|
+
current=current_tab()
|
|
12
|
+
current_url=current.get("url","")
|
|
13
|
+
if "xiaohongshu.com" in current_url:
|
|
14
|
+
target=current
|
|
15
|
+
else:
|
|
16
|
+
tabs=[t for t in list_tabs() if "xiaohongshu.com" in (t.get("url") or "")]
|
|
17
|
+
target=tabs[0] if tabs else None
|
|
18
|
+
if target:
|
|
19
|
+
switch_tab(target)
|
|
20
|
+
if (current_tab().get("url") or "") != url:
|
|
21
|
+
goto_url(url)
|
|
22
|
+
else:
|
|
23
|
+
new_tab(url)
|
|
24
|
+
return current_tab()
|
|
25
|
+
|
|
26
|
+
images = CFG.get("images", [CFG.get("image", "")])
|
|
27
|
+
video = CFG.get("video", "")
|
|
28
|
+
tags = CFG.get("tags", [])
|
|
29
|
+
collection = (CFG.get("collection") or "").strip()
|
|
30
|
+
collection_description = (CFG.get("collection_description") or "").strip()
|
|
31
|
+
|
|
32
|
+
# Calculate steps dynamically
|
|
33
|
+
base_steps = 6 # navigate, tab, upload, title, body, action
|
|
34
|
+
extra = 0
|
|
35
|
+
if mode == "image" and len(images) > 1:
|
|
36
|
+
extra += 1
|
|
37
|
+
if mode == "video":
|
|
38
|
+
extra += 1 # wait for upload processing
|
|
39
|
+
if tags:
|
|
40
|
+
extra += 1
|
|
41
|
+
total_steps = base_steps + extra
|
|
42
|
+
step = [0]
|
|
43
|
+
def next_step(label):
|
|
44
|
+
step[0] += 1
|
|
45
|
+
print(f"[{step[0]}/{total_steps}] {label}")
|
|
46
|
+
|
|
47
|
+
next_step("Navigating to XHS creator...")
|
|
48
|
+
open_or_reuse_xhs("https://creator.xiaohongshu.com/publish/publish")
|
|
49
|
+
wait_for_load()
|
|
50
|
+
time.sleep(3)
|
|
51
|
+
|
|
52
|
+
url = current_tab().get("url") or ""
|
|
53
|
+
if "login" in url:
|
|
54
|
+
print("ERROR: Not logged in. Log in at creator.xiaohongshu.com first.")
|
|
55
|
+
capture_screenshot()
|
|
56
|
+
raise SystemExit(1)
|
|
57
|
+
|
|
58
|
+
if mode == "video":
|
|
59
|
+
# Video tab is the default (first) tab — no need to click
|
|
60
|
+
next_step("Using video upload tab (default)...")
|
|
61
|
+
time.sleep(1)
|
|
62
|
+
|
|
63
|
+
next_step("Uploading video: " + video)
|
|
64
|
+
# Wait for the upload input; XHS can render the tab shell before mounting the file input.
|
|
65
|
+
for _ in range(20):
|
|
66
|
+
ready = js('document.querySelector(".upload-input,input[type=file]") ? "ready" : "wait"')
|
|
67
|
+
if ready == "ready":
|
|
68
|
+
break
|
|
69
|
+
time.sleep(0.5)
|
|
70
|
+
# Try upload_file first, fall back to DOM.performSearch for hidden inputs
|
|
71
|
+
try:
|
|
72
|
+
upload_file(".upload-input,input[type=file][accept*=video],input[type=file]", video)
|
|
73
|
+
except Exception:
|
|
74
|
+
doc = cdp("DOM.getDocument", depth=-1, pierce=True)
|
|
75
|
+
sr = cdp("DOM.performSearch", query='input[type="file"]')
|
|
76
|
+
if sr.get("resultCount", 0) > 0:
|
|
77
|
+
nodes = cdp("DOM.getSearchResults", searchId=sr["searchId"], fromIndex=0, toIndex=1)
|
|
78
|
+
nid = nodes["nodeIds"][0]
|
|
79
|
+
cdp("DOM.setFileInputFiles", files=[video], nodeId=nid)
|
|
80
|
+
cdp("DOM.discardSearchResults", searchId=sr["searchId"])
|
|
81
|
+
else:
|
|
82
|
+
print("ERROR: Cannot find file input for video upload.")
|
|
83
|
+
capture_screenshot()
|
|
84
|
+
raise SystemExit(1)
|
|
85
|
+
|
|
86
|
+
next_step("Waiting for video upload...")
|
|
87
|
+
for attempt in range(180):
|
|
88
|
+
time.sleep(1)
|
|
89
|
+
# A preview/video element appears immediately, even while XHS still says
|
|
90
|
+
# "上传中 0%". Treat the editor's completed state as authoritative.
|
|
91
|
+
upload_state = js('(function(){var t=document.body.innerText||""; if(/上传中\\s*\\d*%?/.test(t)||t.indexOf("取消上传")>=0)return "uploading"; if(t.indexOf("重新上传")>=0||t.indexOf("检测为高清视频")>=0)return "ready"; return "waiting"})()')
|
|
92
|
+
if upload_state == "ready":
|
|
93
|
+
print(" -> Upload processed and editor-ready!")
|
|
94
|
+
break
|
|
95
|
+
if attempt % 15 == 14:
|
|
96
|
+
pct = js('var el=document.querySelector("[class*=progress],.percent,[class*=uploadProgress]"); el ? el.textContent : "..."')
|
|
97
|
+
print(f" -> Upload state {upload_state}; {pct} ({attempt+1}s)")
|
|
98
|
+
else:
|
|
99
|
+
print("ERROR: video upload never reached the editor-ready state")
|
|
100
|
+
capture_screenshot()
|
|
101
|
+
raise SystemExit(1)
|
|
102
|
+
|
|
103
|
+
else:
|
|
104
|
+
# Image mode — click the 2nd tab (上传图文)
|
|
105
|
+
next_step("Switching to image+text tab...")
|
|
106
|
+
js('var tabs=document.querySelectorAll(".creator-tab-item,.tab-item,[class*=publishTypeTabs] span,[class*=publish-type] span");if(tabs.length>=2){tabs[1].click()}else{var all=document.querySelectorAll("span");for(var i=0;i<all.length;i++){if(all[i].textContent.indexOf("\\u56fe\\u6587")>=0){all[i].click();break}}};"done"')
|
|
107
|
+
time.sleep(2)
|
|
108
|
+
|
|
109
|
+
check = js('document.querySelector(".upload-input") ? "found" : "notfound"')
|
|
110
|
+
if check != "found":
|
|
111
|
+
js('var spans=document.querySelectorAll("span,div,a");for(var i=0;i<spans.length;i++){var t=spans[i].textContent;if(t&&t.indexOf("\\u56fe\\u6587")>=0&&spans[i].offsetWidth>0){spans[i].click();break}};"retry"')
|
|
112
|
+
time.sleep(2)
|
|
113
|
+
|
|
114
|
+
next_step("Uploading image: " + images[0])
|
|
115
|
+
upload_file(".upload-input", images[0])
|
|
116
|
+
time.sleep(4)
|
|
117
|
+
|
|
118
|
+
# Upload additional images if any
|
|
119
|
+
if len(images) > 1:
|
|
120
|
+
next_step(f"Uploading {len(images)-1} more image(s)...")
|
|
121
|
+
for i, img in enumerate(images[1:], 2):
|
|
122
|
+
add_btn = js('document.querySelector(".add-upload,.upload-more,.add-btn,[class*=addImage]") ? "found" : "notfound"')
|
|
123
|
+
if add_btn == "found":
|
|
124
|
+
upload_file(".add-upload,.upload-more,.add-btn,[class*=addImage] input[type=file],.upload-input", img)
|
|
125
|
+
else:
|
|
126
|
+
upload_file(".upload-input", img)
|
|
127
|
+
print(f" -> Image {i}/{len(images)}: {os.path.basename(img)}")
|
|
128
|
+
time.sleep(3)
|
|
129
|
+
|
|
130
|
+
next_step("Setting title: " + CFG["title"])
|
|
131
|
+
title_escaped = json.dumps(CFG["title"])
|
|
132
|
+
title_set = js("var inp=document.querySelector('input[placeholder=\"填写标题会有更多赞哦\"],.c-input_inner input,input[placeholder]');if(inp){var ns=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set;ns.call(inp," + title_escaped + " );inp.dispatchEvent(new Event('input',{bubbles:true}));inp.dispatchEvent(new Event('change',{bubbles:true}));'done'}else{'missing'}")
|
|
133
|
+
if title_set != "done":
|
|
134
|
+
print("ERROR: title input not found")
|
|
135
|
+
capture_screenshot()
|
|
136
|
+
raise SystemExit(1)
|
|
137
|
+
time.sleep(0.5)
|
|
138
|
+
|
|
139
|
+
next_step("Setting body text...")
|
|
140
|
+
# Click into the editor area then type
|
|
141
|
+
editor_set = js('var ed=document.querySelector(".tiptap.ProseMirror,#post-textarea,.ql-editor,[contenteditable=true]");if(ed){ed.focus();ed.click();"done"}else{"missing"}')
|
|
142
|
+
if editor_set != "done":
|
|
143
|
+
print("ERROR: body editor not found")
|
|
144
|
+
capture_screenshot()
|
|
145
|
+
raise SystemExit(1)
|
|
146
|
+
time.sleep(0.3)
|
|
147
|
+
|
|
148
|
+
# Build body with tags appended as hashtags
|
|
149
|
+
body = CFG["body"]
|
|
150
|
+
if tags:
|
|
151
|
+
tag_str = " " + " ".join(f"#{t}" for t in tags)
|
|
152
|
+
body = body + tag_str
|
|
153
|
+
type_text(body)
|
|
154
|
+
time.sleep(0.5)
|
|
155
|
+
|
|
156
|
+
if tags:
|
|
157
|
+
next_step(f"Tags appended: {', '.join(tags)}")
|
|
158
|
+
|
|
159
|
+
def choose_collection(name, description):
|
|
160
|
+
if not name:
|
|
161
|
+
return None
|
|
162
|
+
opened = js('var es=Array.from(document.querySelectorAll("*")).filter(function(e){var t=(e.innerText||e.textContent||"").trim(); return e.offsetParent && e.children.length===0 && t==="选择合集"}); if(es.length){es[0].click();"yes"}else{"no"}')
|
|
163
|
+
if opened != "yes":
|
|
164
|
+
raise RuntimeError("collection chooser not found")
|
|
165
|
+
time.sleep(.5)
|
|
166
|
+
selected = js('var es=Array.from(document.querySelectorAll("*")).filter(function(e){return e.offsetParent && e.children.length===0 && (e.innerText||e.textContent||"").trim()===' + json.dumps(name) + '}); if(es.length){es[0].click();"yes"}else{"no"}')
|
|
167
|
+
if selected == "yes":
|
|
168
|
+
time.sleep(.5)
|
|
169
|
+
return "selected"
|
|
170
|
+
create = js('var es=Array.from(document.querySelectorAll("button,*")).filter(function(e){return e.offsetParent && e.children.length===0 && (e.innerText||e.textContent||"").trim()==="创建合集"}); if(es.length){es[0].click();"yes"}else{"no"}')
|
|
171
|
+
if create != "yes":
|
|
172
|
+
raise RuntimeError("collection not found and create control missing")
|
|
173
|
+
time.sleep(.5)
|
|
174
|
+
fill_input('input[placeholder="好的合集名称能吸引更多用户"]', name, clear_first=True, timeout=20)
|
|
175
|
+
if description:
|
|
176
|
+
fill_input('input[placeholder="简单介绍你的合集"],textarea[placeholder="简单介绍你的合集"]', description, clear_first=True, timeout=20)
|
|
177
|
+
created = js('var b=Array.from(document.querySelectorAll("button")).find(function(e){return e.offsetParent && (e.innerText||e.textContent||"").trim()==="创建并加入"}); if(b&&!b.disabled){b.click();"yes"}else{"no"}')
|
|
178
|
+
if created != "yes":
|
|
179
|
+
raise RuntimeError("create-and-join collection control unavailable")
|
|
180
|
+
for _ in range(20):
|
|
181
|
+
time.sleep(.5)
|
|
182
|
+
visible = js('(document.body.innerText||"").indexOf(' + json.dumps(name) + ')>=0 && (document.body.innerText||"").indexOf("创建并加入")<0 ? "yes" : "no"')
|
|
183
|
+
if visible == "yes":
|
|
184
|
+
return "created"
|
|
185
|
+
raise RuntimeError("collection creation was not verified")
|
|
186
|
+
|
|
187
|
+
if collection:
|
|
188
|
+
next_step("Joining collection: " + collection)
|
|
189
|
+
print(" -> Collection " + choose_collection(collection, collection_description))
|
|
190
|
+
|
|
191
|
+
def creator_manager_receipt(title):
|
|
192
|
+
goto_url("https://creator.xiaohongshu.com/new/note-manager")
|
|
193
|
+
try:
|
|
194
|
+
wait_for_load()
|
|
195
|
+
except Exception:
|
|
196
|
+
pass
|
|
197
|
+
rows = []
|
|
198
|
+
for _ in range(30):
|
|
199
|
+
time.sleep(.5)
|
|
200
|
+
raw = js('Array.from(document.querySelectorAll(".note-card")).map(function(c){var t=c.querySelector(".note-card__title"); return JSON.stringify({title:t?(t.innerText||t.textContent||"").trim():"", text:(c.innerText||"").trim(), impression:c.dataset.impression||""})}).join("\\n")') or ""
|
|
201
|
+
rows = []
|
|
202
|
+
for line in raw.splitlines():
|
|
203
|
+
try:
|
|
204
|
+
row = json.loads(line)
|
|
205
|
+
imp = json.loads(row.get("impression") or "{}")
|
|
206
|
+
row["note_id"] = (((imp.get("noteTarget") or {}).get("value") or {}).get("noteId"))
|
|
207
|
+
if row.get("title") == title:
|
|
208
|
+
rows.append(row)
|
|
209
|
+
except Exception:
|
|
210
|
+
pass
|
|
211
|
+
if rows:
|
|
212
|
+
break
|
|
213
|
+
if not rows:
|
|
214
|
+
return None
|
|
215
|
+
newest = rows[0]
|
|
216
|
+
text = newest.get("text") or ""
|
|
217
|
+
status = "reviewing" if "审核中" in text else "rejected" if "未通过" in text else "published"
|
|
218
|
+
return {"note_id": newest.get("note_id"), "title": title, "status": status, "duplicate_count": len(rows), "card_text": text}
|
|
219
|
+
|
|
220
|
+
if CFG["action"] == "publish":
|
|
221
|
+
next_step("Publishing...")
|
|
222
|
+
# XHS can render the publish button before video processing is complete. A
|
|
223
|
+
# programmatic click on a disabled button is a no-op, so wait for the real
|
|
224
|
+
# control to become enabled before clicking it.
|
|
225
|
+
publish_result = "missing"
|
|
226
|
+
for attempt in range(180):
|
|
227
|
+
publish_result = js('var btn=[...document.querySelectorAll("button,[role=button]")].find(function(b){return b.offsetParent && (b.innerText||b.textContent||"").trim()==="发布"}) || document.querySelector(".publishBtn,.publish-btn,button.css-k01wfk,[class*=submit]");if(!btn){"missing"}else if(btn.disabled || btn.getAttribute("aria-disabled")==="true" || btn.classList.contains("disabled")){"disabled"}else{btn.scrollIntoView({block:"center"});btn.click();"clicked"}')
|
|
228
|
+
if publish_result == "clicked":
|
|
229
|
+
break
|
|
230
|
+
if attempt % 15 == 14:
|
|
231
|
+
print(f" -> Publish control {publish_result}; waiting for processing... ({attempt+1}s)")
|
|
232
|
+
time.sleep(1)
|
|
233
|
+
if publish_result != "clicked":
|
|
234
|
+
# XHS may expose the publish control only through the accessibility tree.
|
|
235
|
+
nodes = cdp("Accessibility.getFullAXTree")["nodes"]
|
|
236
|
+
target = next((n for n in nodes if (n.get("role") or {}).get("value") == "button" and (n.get("name") or {}).get("value") == "发布" and n.get("backendDOMNodeId")), None)
|
|
237
|
+
if target:
|
|
238
|
+
resolved = cdp("DOM.resolveNode", backendNodeId=target["backendDOMNodeId"])["object"]
|
|
239
|
+
object_id = resolved.get("objectId")
|
|
240
|
+
if object_id:
|
|
241
|
+
cdp(
|
|
242
|
+
"Runtime.callFunctionOn",
|
|
243
|
+
objectId=object_id,
|
|
244
|
+
functionDeclaration="function(){ this.click(); return true; }",
|
|
245
|
+
returnByValue=True,
|
|
246
|
+
)
|
|
247
|
+
publish_result = "clicked"
|
|
248
|
+
if publish_result != "clicked":
|
|
249
|
+
print("ERROR: publish button not found")
|
|
250
|
+
capture_screenshot()
|
|
251
|
+
raise SystemExit(1)
|
|
252
|
+
redirect_verified = False
|
|
253
|
+
for _ in range(20):
|
|
254
|
+
time.sleep(1)
|
|
255
|
+
state = js('location.href.indexOf("publish/success")>=0 || /发布成功|成功发布/.test(document.body.innerText||"") ? "success" : "wait"')
|
|
256
|
+
if state == "success":
|
|
257
|
+
redirect_verified = True
|
|
258
|
+
break
|
|
259
|
+
receipt = creator_manager_receipt(CFG["title"])
|
|
260
|
+
if not receipt or not receipt.get("note_id"):
|
|
261
|
+
print("ERROR: publish was not verified in creator manager")
|
|
262
|
+
capture_screenshot()
|
|
263
|
+
raise SystemExit(2)
|
|
264
|
+
receipt["redirect_verified"] = redirect_verified
|
|
265
|
+
receipt["collection"] = collection or None
|
|
266
|
+
print("XHS_RECEIPT " + json.dumps(receipt, ensure_ascii=False, separators=(",", ":")))
|
|
267
|
+
verified = receipt["status"] in {"published", "reviewing"}
|
|
268
|
+
if not verified:
|
|
269
|
+
print("ERROR: creator manager reports " + receipt["status"])
|
|
270
|
+
raise SystemExit(2)
|
|
271
|
+
else:
|
|
272
|
+
next_step("Saving as draft...")
|
|
273
|
+
# Find 暂存 button by unicode
|
|
274
|
+
js('var btns=document.querySelectorAll("button");for(var i=0;i<btns.length;i++){if(btns[i].textContent.indexOf("\\u6682\\u5b58")>=0){btns[i].click();break}};"done"')
|
|
275
|
+
|
|
276
|
+
time.sleep(3)
|
|
277
|
+
capture_screenshot()
|
|
278
|
+
if CFG["action"] == "publish":
|
|
279
|
+
print("Done! Post published." if verified else "Done! Post submitted; verification pending.")
|
|
280
|
+
else:
|
|
281
|
+
print("Done! Post saved as draft.")
|