@harness.farm/social-cli 0.1.0

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.
@@ -0,0 +1,271 @@
1
+ platform: bilibili
2
+ login_url: https://www.bilibili.com
3
+ login_check:
4
+ cookie: SESSDATA
5
+
6
+ commands:
7
+
8
+ # ── 搜索 ──────────────────────────────────────────────────────────────────────
9
+ search:
10
+ args: [keyword]
11
+ steps:
12
+ - open: "https://search.bilibili.com/all?keyword={{keyword}}&search_source=1"
13
+ - wait: 4000
14
+ - extract:
15
+ selector: ".bili-video-card"
16
+ fields:
17
+ link:
18
+ selector: "a[href*='/video/']"
19
+ attr: href
20
+ title: ".bili-video-card__info--tit,h3"
21
+ up: ".bili-video-card__info--author,.up-name"
22
+
23
+ # ── 点赞视频 ──────────────────────────────────────────────────────────────────
24
+ like:
25
+ args: [url]
26
+ steps:
27
+ - open: "{{url}}"
28
+ - wait: 4000
29
+ - capture:
30
+ name: before
31
+ eval: "String(document.querySelectorAll('.video-like.on').length > 0)"
32
+ - eval: >-
33
+ (function(){
34
+ var btn = document.querySelector(".video-like");
35
+ if(btn) { btn.click(); return true; }
36
+ return false;
37
+ })()
38
+ - wait: 1000
39
+ - capture:
40
+ name: after
41
+ eval: "String(document.querySelectorAll('.video-like.on').length > 0)"
42
+ - return:
43
+ - field: 操作
44
+ value: "{{before === 'true' ? '取消点赞' : '点赞成功'}}"
45
+ - field: 当前状态
46
+ value: "{{after === 'true' ? '已点赞' : '未点赞'}}"
47
+
48
+ # ── 关注 UP 主 ────────────────────────────────────────────────────────────────
49
+ follow:
50
+ args: [url]
51
+ steps:
52
+ - open: "{{url}}"
53
+ - wait: 4000
54
+ - capture:
55
+ name: is_not_follow
56
+ eval: "String(document.querySelectorAll('.follow-btn.not-follow').length > 0)"
57
+ # agent-browser click 发送真实鼠标事件(Vue 事件监听器需要)
58
+ - click: ".follow-btn"
59
+ - wait: 1500
60
+ - capture:
61
+ name: after_class
62
+ eval: "document.querySelector('.follow-btn') ? document.querySelector('.follow-btn').className : ''"
63
+ - return:
64
+ - field: 操作
65
+ value: "{{is_not_follow === 'true' ? '关注' : '取消关注'}}"
66
+ - field: 状态
67
+ value: "{{after_class.includes('not-follow') ? '❌ 未关注' : '✅ 已关注'}}"
68
+
69
+ # ── 发表评论 ──────────────────────────────────────────────────────────────────
70
+ # B站评论区是多层嵌套 shadow DOM:
71
+ # bili-comments → bili-comments-header-renderer → bili-comment-box
72
+ # → bili-comment-rich-textarea → div.brt-editor[contenteditable]
73
+ comment:
74
+ args: [url, text]
75
+ steps:
76
+ - open: "{{url}}"
77
+ - wait: 4000
78
+ # 滚动触发评论区懒加载(bili-comments 默认 lazy-load)
79
+ - eval: "window.scrollTo(0, 600)"
80
+ - wait: 2000
81
+ # 深度 focus 到 shadow DOM 内 .brt-editor(需要穿透多层 shadow root)
82
+ - eval: >-
83
+ (function(){
84
+ var host = document.querySelector("bili-comments");
85
+ if(!host) return false;
86
+ function deepFocus(root){
87
+ var sr = root.shadowRoot;
88
+ if(!sr) return null;
89
+ var editor = sr.querySelector(".brt-editor");
90
+ if(editor) { editor.click(); editor.focus(); return true; }
91
+ var els = sr.querySelectorAll("*");
92
+ for(var i=0; i<els.length; i++){
93
+ var r = deepFocus(els[i]);
94
+ if(r) return r;
95
+ }
96
+ return null;
97
+ }
98
+ return deepFocus(host) || false;
99
+ })()
100
+ - wait: 500
101
+ # CDP Input.insertText 直接插入文本,可穿透 shadow DOM
102
+ - insert_text: "{{text}}"
103
+ - wait: 500
104
+ - capture:
105
+ name: input_check
106
+ eval: "document.querySelector('bili-comments').shadowRoot.querySelector('bili-comments-header-renderer').shadowRoot.querySelector('bili-comment-box').shadowRoot.querySelector('bili-comment-rich-textarea').shadowRoot.querySelector('.brt-editor').textContent.trim()"
107
+ # 点击发布按钮
108
+ - eval: >-
109
+ (function(){
110
+ var btn = document.querySelector("bili-comments")
111
+ .shadowRoot.querySelector("bili-comments-header-renderer")
112
+ .shadowRoot.querySelector("bili-comment-box")
113
+ .shadowRoot.querySelector("#pub button");
114
+ if(btn) { btn.click(); return true; }
115
+ return false;
116
+ })()
117
+ - wait: 2000
118
+ - return:
119
+ - field: 状态
120
+ value: "✅ 评论成功"
121
+ - field: 评论内容
122
+ value: "{{text}}"
123
+ - field: 输入确认
124
+ value: "{{input_check}}"
125
+
126
+ # ── 回复评论 ──────────────────────────────────────────────────────────────────
127
+ # 点击第一条评论的"回复"按钮,然后输入文字
128
+ reply:
129
+ args: [url, text]
130
+ steps:
131
+ - open: "{{url}}"
132
+ - wait: 4000
133
+ # 滚动触发评论区懒加载
134
+ - eval: "window.scrollTo(0, 600)"
135
+ - wait: 2000
136
+ # 点击第一条评论的回复按钮
137
+ - eval: >-
138
+ (function(){
139
+ var cr = document.querySelector("bili-comments").shadowRoot;
140
+ var thread = cr.querySelector("#feed bili-comment-thread-renderer");
141
+ if(!thread) return false;
142
+ var tsr = thread.shadowRoot;
143
+ var renderer = tsr.querySelector("bili-comment-renderer");
144
+ if(!renderer) return false;
145
+ var rsr = renderer.shadowRoot;
146
+ var actions = rsr.querySelector("bili-comment-action-buttons-renderer");
147
+ if(!actions) return false;
148
+ var absr = actions.shadowRoot;
149
+ var replyBtn = absr.querySelector("#reply button");
150
+ if(replyBtn) { replyBtn.click(); return true; }
151
+ return false;
152
+ })()
153
+ - wait: 1000
154
+ # 找到回复输入框并 focus(在 reply-container 下展开的 bili-comment-box)
155
+ - eval: >-
156
+ (function(){
157
+ var cr = document.querySelector("bili-comments").shadowRoot;
158
+ var thread = cr.querySelector("#feed bili-comment-thread-renderer");
159
+ var tsr = thread.shadowRoot;
160
+ var replyContainer = tsr.querySelector("#reply-container");
161
+ if(!replyContainer) return false;
162
+ var box = replyContainer.querySelector("bili-comment-box");
163
+ if(!box) return false;
164
+ var bsr = box.shadowRoot;
165
+ var rta = bsr.querySelector("bili-comment-rich-textarea");
166
+ if(!rta) return false;
167
+ var editor = rta.shadowRoot.querySelector(".brt-editor");
168
+ if(!editor) return false;
169
+ editor.click();
170
+ editor.focus();
171
+ return true;
172
+ })()
173
+ - wait: 300
174
+ # CDP Input.insertText 直接插入(穿透 shadow DOM)
175
+ - insert_text: "{{text}}"
176
+ - wait: 500
177
+ # 点击发布
178
+ - eval: >-
179
+ (function(){
180
+ var cr = document.querySelector("bili-comments").shadowRoot;
181
+ var thread = cr.querySelector("#feed bili-comment-thread-renderer");
182
+ var tsr = thread.shadowRoot;
183
+ var replyContainer = tsr.querySelector("#reply-container");
184
+ if(!replyContainer) return false;
185
+ var box = replyContainer.querySelector("bili-comment-box");
186
+ if(!box) return false;
187
+ var btn = box.shadowRoot.querySelector("#pub button");
188
+ if(btn) { btn.click(); return true; }
189
+ return false;
190
+ })()
191
+ - wait: 2000
192
+ - return:
193
+ - field: 状态
194
+ value: "✅ 回复成功"
195
+ - field: 回复内容
196
+ value: "{{text}}"
197
+
198
+ # ── 投稿(发布视频)──────────────────────────────────────────────────────────
199
+ post:
200
+ args: [video, title, desc] # video: 本地视频路径(必填)
201
+ steps:
202
+ - open: "https://member.bilibili.com/platform/upload/video/frame"
203
+ - wait: 4000
204
+ # 关掉"开启通知"弹窗(知道了)
205
+ - eval: >-
206
+ (function(){
207
+ var btn = [...document.querySelectorAll("button")].find(function(b){ return b.textContent.trim() === "知道了"; });
208
+ if(btn) btn.click();
209
+ return !!btn;
210
+ })()
211
+ - wait: 500
212
+ # 上传视频
213
+ - upload:
214
+ selector: "input[accept*='.mp4']"
215
+ file: "{{video}}"
216
+ # 等待上传完成(标题输入框出现)
217
+ - wait:
218
+ selector: "input[placeholder='请输入稿件标题']"
219
+ - wait: 2000
220
+ # 关掉所有弹窗(二创计划 / 定时发布 / 通知)
221
+ - eval: >-
222
+ (function(){
223
+ ["暂不考虑","知道了"].forEach(function(text){
224
+ var btn = [...document.querySelectorAll("button")].find(function(b){ return b.textContent.trim() === text; });
225
+ if(btn) btn.click();
226
+ });
227
+ document.querySelectorAll("button").forEach(function(b){
228
+ if(b.textContent.trim() === "取消") b.click();
229
+ });
230
+ })()
231
+ - wait: 800
232
+ # 填写标题(覆盖自动填充)
233
+ - fill:
234
+ selector: "input[placeholder='请输入稿件标题']"
235
+ value: "{{title}}"
236
+ - wait: 300
237
+ # 填写简介(Quill 富文本编辑器)
238
+ - eval: >-
239
+ (function(){
240
+ var el = document.querySelector(".ql-editor");
241
+ if(!el) return false;
242
+ el.focus();
243
+ document.execCommand("selectAll", false, null);
244
+ document.execCommand("delete", false, null);
245
+ document.execCommand("insertText", false, "{{desc}}");
246
+ return el.textContent.trim().slice(0,30);
247
+ })()
248
+ - wait: 500
249
+ # 点击"立即投稿"
250
+ - eval: >-
251
+ (function(){
252
+ var btn = document.querySelector("span.submit-add");
253
+ if(btn) { btn.click(); return true; }
254
+ return false;
255
+ })()
256
+ - wait: 5000
257
+ - capture:
258
+ name: result_url
259
+ eval: "location.href"
260
+ - capture:
261
+ name: success
262
+ eval: "document.body.innerText.includes('投递成功') || document.body.innerText.includes('投稿成功')"
263
+ - return:
264
+ - field: 状态
265
+ value: "{{success === 'true' ? '✅ 投稿成功' : '⚠️ 请检查页面'}}"
266
+ - field: 标题
267
+ value: "{{title}}"
268
+ - field: 描述
269
+ value: "{{desc}}"
270
+ - field: 页面URL
271
+ value: "{{result_url}}"
@@ -0,0 +1,176 @@
1
+ platform: douyin
2
+ login_url: https://www.douyin.com
3
+ login_check:
4
+ cookie: passport_csrf_token
5
+
6
+ commands:
7
+
8
+ search:
9
+ args: [keyword]
10
+ steps:
11
+ - open: "https://www.douyin.com/search/{{keyword}}?type=video"
12
+ - wait: 4000
13
+ - extract:
14
+ selector: "a[href*='/video/']"
15
+ fields:
16
+ link:
17
+ selector: "a[href*='/video/']"
18
+ attr: href
19
+ text: ".kgnD1hJB, [class*=title], [class*=desc]"
20
+
21
+ like:
22
+ args: [url]
23
+ steps:
24
+ - open: "{{url}}"
25
+ - wait: 4000
26
+ - capture:
27
+ name: state_before
28
+ eval: "document.querySelector('[data-e2e=\"video-player-digg\"]')?.getAttribute('data-e2e-state') || ''"
29
+ - key: "z"
30
+ - wait: 1000
31
+ - capture:
32
+ name: state_after
33
+ eval: "document.querySelector('[data-e2e=\"video-player-digg\"]')?.getAttribute('data-e2e-state') || ''"
34
+ - return:
35
+ - field: 操作
36
+ value: "{{state_before === 'video-player-no-digged' ? '点赞成功' : '取消点赞'}}"
37
+ - field: 状态
38
+ value: "{{state_after}}"
39
+
40
+ comment:
41
+ args: [url, text]
42
+ steps:
43
+ - open: "{{url}}"
44
+ - wait: 4000
45
+ # X 键打开评论区
46
+ - key: "x"
47
+ - wait: 1200
48
+ # PointerEvent 激活 Draft.js 输入框(触发 React 事件)
49
+ - eval: >-
50
+ (function(){
51
+ var el = document.querySelector('.lFk180Rt') || document.querySelector('.GXmFLge7');
52
+ if(!el) return false;
53
+ ['pointerdown','mousedown','pointerup','mouseup','click'].forEach(function(evt){
54
+ el.dispatchEvent(new (evt.startsWith('pointer') ? PointerEvent : MouseEvent)(evt, {bubbles:true,cancelable:true,composed:true,view:window}));
55
+ });
56
+ return true;
57
+ })()
58
+ - wait: 1200
59
+ # focus Draft.js 编辑器
60
+ - eval: "document.querySelector('.notranslate.public-DraftEditor-content')?.focus()"
61
+ - wait: 300
62
+ # 插入文字(CDP char 事件,正确触发 Draft.js EditorState 更新)
63
+ - keyboard_insert: "{{text}}"
64
+ - wait: 600
65
+ - capture:
66
+ name: input_text
67
+ eval: "document.querySelector('.notranslate.public-DraftEditor-content')?.textContent?.trim() || ''"
68
+ # Enter 发送
69
+ - key: "Enter"
70
+ - wait: 2000
71
+ - return:
72
+ - field: 状态
73
+ value: "✅ 评论成功"
74
+ - field: 评论内容
75
+ value: "{{text}}"
76
+ - field: 输入确认
77
+ value: "{{input_text}}"
78
+
79
+ follow:
80
+ args: [url]
81
+ steps:
82
+ - open: "{{url}}"
83
+ - wait: 4000
84
+ # JbfEzak6 = the + follow button on the avatar in the right sidebar
85
+ # present = not yet following; absent = already following
86
+ - capture:
87
+ name: has_follow_btn
88
+ eval: "!!document.querySelector('.JbfEzak6')"
89
+ - eval: >-
90
+ (function(){
91
+ var el = document.querySelector('.JbfEzak6');
92
+ if(el) el.click();
93
+ return !!el;
94
+ })()
95
+ - wait: 1000
96
+ - capture:
97
+ name: btn_after
98
+ eval: "!!document.querySelector('.JbfEzak6')"
99
+ - return:
100
+ - field: 操作
101
+ value: "{{has_follow_btn === 'true' ? '关注成功' : '已在关注中(无操作)'}}"
102
+ - field: 关注按钮消失
103
+ value: "{{btn_after === 'false' ? '✅ 是' : '⚠️ 否'}}"
104
+
105
+ post:
106
+ args: [video, title, desc] # video: 本地视频路径(必填)
107
+ steps:
108
+ - open: "https://creator.douyin.com/creator-micro/content/upload"
109
+ - wait: 4000
110
+ # 上传视频
111
+ - upload:
112
+ selector: "input[type=file]"
113
+ file: "{{video}}"
114
+ # 等待编辑器渲染(标题输入框出现)
115
+ - wait:
116
+ selector: "input[placeholder='填写作品标题,为作品获得更多流量']"
117
+ - wait: 1000
118
+ # 填写标题
119
+ - fill:
120
+ selector: "input[placeholder='填写作品标题,为作品获得更多流量']"
121
+ value: "{{title}}"
122
+ - wait: 500
123
+ # 填写描述(contenteditable)
124
+ - type_rich:
125
+ selector: ".zone-container.editor-kit-container"
126
+ value: "{{desc}}"
127
+ - wait: 800
128
+ # 选择封面:点击"选择封面" → 封面编辑器弹出 → 点"完成"
129
+ - eval: >-
130
+ (function(){
131
+ var btn = document.querySelector('.cover-Jg3T4p');
132
+ if (btn) { btn.click(); return true; }
133
+ return false;
134
+ })()
135
+ - wait: 2000
136
+ - eval: >-
137
+ (function(){
138
+ var btn = [...document.querySelectorAll('button')].find(function(b){
139
+ return b.textContent.trim() === '完成';
140
+ });
141
+ if (btn) { btn.click(); return true; }
142
+ return false;
143
+ })()
144
+ - wait: 1500
145
+ # 如果弹出"暂不设置竖封面"提示,跳过
146
+ - eval: >-
147
+ (function(){
148
+ var btn = [...document.querySelectorAll('button')].find(function(b){
149
+ return b.textContent.trim() === '暂不设置';
150
+ });
151
+ if (btn) { btn.click(); return true; }
152
+ return false;
153
+ })()
154
+ - wait: 1000
155
+ # 点击发布
156
+ - eval: >-
157
+ (function(){
158
+ var btn = [...document.querySelectorAll('button')].find(function(b){
159
+ return b.textContent.trim() === '发布' && !b.disabled;
160
+ });
161
+ if (btn) { btn.click(); return true; }
162
+ return false;
163
+ })()
164
+ - wait: 5000
165
+ - capture:
166
+ name: result_url
167
+ eval: "location.href"
168
+ - return:
169
+ - field: 状态
170
+ value: "✅ 发布成功"
171
+ - field: 标题
172
+ value: "{{title}}"
173
+ - field: 描述
174
+ value: "{{desc}}"
175
+ - field: 跳转URL
176
+ value: "{{result_url}}"
@@ -0,0 +1,116 @@
1
+ platform: x
2
+ login_url: https://x.com
3
+ login_check:
4
+ cookie: auth_token
5
+
6
+ commands:
7
+
8
+ search:
9
+ args: [keyword]
10
+ steps:
11
+ - open: "https://x.com/search?q={{keyword}}&src=typed_query&f=top"
12
+ - wait: 4000
13
+ - extract:
14
+ selector: "[data-testid='tweet']"
15
+ fields:
16
+ text: "[data-testid='tweetText']"
17
+ user: "[data-testid='User-Name']"
18
+ link:
19
+ selector: "a[href*='/status/']"
20
+ attr: href
21
+ time:
22
+ selector: time
23
+ attr: datetime
24
+
25
+ like:
26
+ args: [url]
27
+ steps:
28
+ - open: "{{url}}"
29
+ - wait: 3000
30
+ - capture:
31
+ name: already_liked
32
+ eval: "!!document.querySelector('[data-testid=\"unlike\"]')"
33
+ - eval: >-
34
+ (function(){
35
+ var btn = document.querySelector('[data-testid="like"]') || document.querySelector('[data-testid="unlike"]');
36
+ if (btn) { btn.click(); return true; }
37
+ return false;
38
+ })()
39
+ - wait: 1000
40
+ - capture:
41
+ name: is_liked
42
+ eval: "!!document.querySelector('[data-testid=\"unlike\"]')"
43
+ - return:
44
+ - field: 操作
45
+ value: "{{already_liked == 'true' && '取消点赞' || '点赞成功'}}"
46
+ - field: 状态
47
+ value: "{{is_liked}}"
48
+
49
+ reply:
50
+ args: [url, text]
51
+ steps:
52
+ - open: "{{url}}"
53
+ - wait: 3000
54
+ - click:
55
+ selector: "[data-testid='reply']"
56
+ - wait: 1000
57
+ - type_rich:
58
+ selector: "[data-testid='tweetTextarea_0']"
59
+ value: "{{text}}"
60
+ - wait: 500
61
+ - capture:
62
+ name: input_text
63
+ eval: "document.querySelector('[data-testid=\"tweetTextarea_0\"]')?.textContent?.trim() || ''"
64
+ - click:
65
+ selector: "[data-testid='tweetButtonInline']"
66
+ - wait: 2000
67
+ - return:
68
+ - field: 状态
69
+ value: "✅ 回复成功"
70
+ - field: 回复内容
71
+ value: "{{text}}"
72
+ - field: 输入确认
73
+ value: "{{input_text}}"
74
+
75
+ post:
76
+ args: [text]
77
+ steps:
78
+ - open: "https://x.com/home"
79
+ - wait: 3000
80
+ - type_rich:
81
+ selector: "[data-testid='tweetTextarea_0']"
82
+ value: "{{text}}"
83
+ - wait: 500
84
+ - capture:
85
+ name: input_text
86
+ eval: "document.querySelector('[data-testid=\"tweetTextarea_0\"]')?.textContent?.trim() || ''"
87
+ - click:
88
+ selector: "[data-testid='tweetButton']"
89
+ - wait: 2500
90
+ - capture:
91
+ name: result_url
92
+ eval: "location.href"
93
+ - return:
94
+ - field: 状态
95
+ value: "✅ 发推成功"
96
+ - field: 内容
97
+ value: "{{text}}"
98
+ - field: 输入确认
99
+ value: "{{input_text}}"
100
+
101
+ retweet:
102
+ args: [url]
103
+ steps:
104
+ - open: "{{url}}"
105
+ - wait: 3000
106
+ - click:
107
+ selector: "[data-testid='retweet']"
108
+ - wait: 800
109
+ - click:
110
+ selector: "[data-testid='retweetConfirm']"
111
+ - wait: 1500
112
+ - return:
113
+ - field: 状态
114
+ value: "✅ 转推成功"
115
+ - field: URL
116
+ value: "{{url}}"
@@ -0,0 +1,178 @@
1
+ platform: xiaohongshu
2
+ login_url: https://www.xiaohongshu.com
3
+ login_check:
4
+ cookie: web_session
5
+
6
+ commands:
7
+
8
+ search:
9
+ args: [keyword]
10
+ steps:
11
+ - open: "https://www.xiaohongshu.com/search_result?keyword={{keyword}}&source=web_explore_feed"
12
+ - wait: 4000
13
+ - extract:
14
+ selector: .note-item
15
+ fields:
16
+ title: ".title, [class*=title]"
17
+ author: ".author .name, .nickname"
18
+ link:
19
+ selector: a
20
+ attr: href
21
+
22
+ hot:
23
+ args: []
24
+ steps:
25
+ - open: "https://www.xiaohongshu.com/explore"
26
+ - wait: 4000
27
+ - extract:
28
+ selector: .note-item
29
+ fields:
30
+ title: ".title, [class*=title]"
31
+ author: ".author .name, .nickname"
32
+ link:
33
+ selector: a
34
+ attr: href
35
+
36
+ like:
37
+ args: [url]
38
+ steps:
39
+ - open: "{{url}}"
40
+ - wait: 3000
41
+ - capture:
42
+ name: was_liked
43
+ eval: "document.querySelector('.like-wrapper') ? document.querySelector('.like-wrapper').classList.contains('like-active') : false"
44
+ - click:
45
+ selector: ".like-wrapper"
46
+ - wait: 500
47
+ - capture:
48
+ name: is_liked
49
+ eval: "document.querySelector('.like-wrapper') ? document.querySelector('.like-wrapper').classList.contains('like-active') : false"
50
+ - capture:
51
+ name: count
52
+ eval: "document.querySelector('.like-wrapper .count') ? document.querySelector('.like-wrapper .count').textContent.trim() : '?'"
53
+ - return:
54
+ - field: 操作
55
+ value: "{{was_liked == 'true' && '取消点赞' || '点赞成功'}}"
56
+ - field: 状态
57
+ value: "{{is_liked}}"
58
+ - field: 点赞数
59
+ value: "{{count}}"
60
+
61
+ comment:
62
+ args: [url, text]
63
+ steps:
64
+ - open: "{{url}}"
65
+ - wait: 3000
66
+ - click:
67
+ text: "说点什么..."
68
+ - wait: 800
69
+ - type_rich:
70
+ selector: ".content-input"
71
+ value: "{{text}}"
72
+ - wait: 500
73
+ - capture:
74
+ name: input_text
75
+ eval: "document.querySelector('.content-input') ? document.querySelector('.content-input').textContent.trim() : ''"
76
+ - click:
77
+ text: "发送"
78
+ - wait: 1500
79
+ - return:
80
+ - field: 状态
81
+ value: "✅ 评论成功"
82
+ - field: 评论内容
83
+ value: "{{text}}"
84
+ - field: 输入确认
85
+ value: "{{input_text}}"
86
+
87
+ post_video:
88
+ args: [video, title, desc] # video: 本地视频路径(必填)
89
+ steps:
90
+ - open: "https://creator.xiaohongshu.com/publish/publish?source=official"
91
+ - wait: 3000
92
+ # 默认就在「上传视频」tab,直接上传
93
+ - upload:
94
+ selector: "input.upload-input"
95
+ file: "{{video}}"
96
+ # 等待上传完成(标题输入框出现)
97
+ - wait:
98
+ selector: "input.d-text"
99
+ - wait: 1000
100
+ # 填写标题
101
+ - fill:
102
+ selector: "input.d-text"
103
+ value: "{{title}}"
104
+ - wait: 500
105
+ # 填写描述(ProseMirror 富文本)
106
+ - type_rich:
107
+ selector: ".tiptap.ProseMirror"
108
+ value: "{{desc}}"
109
+ - wait: 800
110
+ # 点击发布
111
+ - eval: >-
112
+ (function(){
113
+ var btns = Array.from(document.querySelectorAll("button"));
114
+ var btn = btns.filter(function(b){ return b.textContent.trim() === "发布"; }).pop();
115
+ if(btn && !btn.disabled) { btn.click(); return true; }
116
+ return false;
117
+ })()
118
+ - wait: 3000
119
+ - capture:
120
+ name: result_url
121
+ eval: "location.href"
122
+ - return:
123
+ - field: 状态
124
+ value: "✅ 发布成功"
125
+ - field: 标题
126
+ value: "{{title}}"
127
+ - field: 描述
128
+ value: "{{desc}}"
129
+ - field: 跳转URL
130
+ value: "{{result_url}}"
131
+
132
+ post:
133
+ args: [title, content, image] # image: 本地图片路径(必填,小红书图文必须有图)
134
+ steps:
135
+ - open: "https://creator.xiaohongshu.com/publish/publish?source=official"
136
+ - wait: 3000
137
+ # 切到图文 tab
138
+ - click:
139
+ text: "上传图文"
140
+ - wait: 1500
141
+ # 上传图片
142
+ - upload:
143
+ selector: "input[type=file][accept*=jpg]"
144
+ file: "{{image}}"
145
+ # 等待图片上传 + 编辑器渲染
146
+ - wait:
147
+ selector: "input.d-text"
148
+ # 填标题
149
+ - fill:
150
+ selector: "input.d-text"
151
+ value: "{{title}}"
152
+ - wait: 500
153
+ # 填正文
154
+ - type_rich:
155
+ selector: ".tiptap.ProseMirror"
156
+ value: "{{content}}"
157
+ - wait: 800
158
+ # 点发布(selector 更稳定,避免 text 匹配问题)
159
+ - eval: >-
160
+ (function(){
161
+ var btns = [...document.querySelectorAll('button')];
162
+ var btn = btns.reverse().find(function(b){ return b.textContent.trim() === '发布'; });
163
+ if (btn && !btn.disabled) { btn.click(); return true; }
164
+ return false;
165
+ })()
166
+ - wait: 2500
167
+ - capture:
168
+ name: result_url
169
+ eval: "location.href"
170
+ - return:
171
+ - field: 状态
172
+ value: "✅ 发布成功"
173
+ - field: 标题
174
+ value: "{{title}}"
175
+ - field: 正文
176
+ value: "{{content}}"
177
+ - field: 跳转URL
178
+ value: "{{result_url}}"
@@ -0,0 +1,120 @@
1
+ platform: xiaohongshu
2
+ login_url: https://www.xiaohongshu.com
3
+ login_check:
4
+ cookie: web_session
5
+
6
+ commands:
7
+
8
+ search:
9
+ args: [keyword]
10
+ steps:
11
+ - open: "https://www.xiaohongshu.com/search_result?keyword={{keyword}}&source=web_explore_feed"
12
+ - wait: 4000
13
+ - extract:
14
+ selector: .note-item
15
+ fields:
16
+ title: ".title, [class*=title]"
17
+ author: ".author .name, .nickname"
18
+ link:
19
+ selector: a
20
+ attr: href
21
+
22
+ hot:
23
+ args: []
24
+ steps:
25
+ - open: "https://www.xiaohongshu.com/explore"
26
+ - wait: 4000
27
+ - extract:
28
+ selector: .note-item
29
+ fields:
30
+ title: ".title, [class*=title]"
31
+ author: ".author .name, .nickname"
32
+ link:
33
+ selector: a
34
+ attr: href
35
+
36
+ like:
37
+ args: [url]
38
+ steps:
39
+ - open: "{{url}}"
40
+ - wait: 3000
41
+ - capture:
42
+ name: was_liked
43
+ eval: "document.querySelector('.like-wrapper') ? document.querySelector('.like-wrapper').classList.contains('like-active') : false"
44
+ - click:
45
+ selector: ".like-wrapper"
46
+ - wait: 500
47
+ - capture:
48
+ name: is_liked
49
+ eval: "document.querySelector('.like-wrapper') ? document.querySelector('.like-wrapper').classList.contains('like-active') : false"
50
+ - capture:
51
+ name: count
52
+ eval: "document.querySelector('.like-wrapper .count') ? document.querySelector('.like-wrapper .count').textContent.trim() : '?'"
53
+ - return:
54
+ - field: 操作
55
+ value: "{{was_liked == 'true' && '取消点赞' || '点赞成功'}}"
56
+ - field: 状态
57
+ value: "{{is_liked}}"
58
+ - field: 点赞数
59
+ value: "{{count}}"
60
+
61
+ comment:
62
+ args: [url, text]
63
+ steps:
64
+ - open: "{{url}}"
65
+ - wait: 3000
66
+ - click:
67
+ text: "说点什么..."
68
+ - wait: 800
69
+ - type_rich:
70
+ selector: ".content-input"
71
+ value: "{{text}}"
72
+ - wait: 500
73
+ - capture:
74
+ name: input_text
75
+ eval: "document.querySelector('.content-input') ? document.querySelector('.content-input').textContent.trim() : ''"
76
+ - click:
77
+ text: "发送"
78
+ - wait: 1500
79
+ - return:
80
+ - field: 状态
81
+ value: "✅ 评论成功"
82
+ - field: 评论内容
83
+ value: "{{text}}"
84
+ - field: 输入确认
85
+ value: "{{input_text}}"
86
+
87
+ post:
88
+ args: [title, content]
89
+ steps:
90
+ - open: "https://creator.xiaohongshu.com/publish/publish?source=official"
91
+ - wait: 3000
92
+ - click:
93
+ text: "上传图文"
94
+ - wait: 1500
95
+ - fill:
96
+ selector: "input.d-text[placeholder*='标题']"
97
+ value: "{{title}}"
98
+ - wait: 500
99
+ - type_rich:
100
+ selector: ".tiptap.ProseMirror"
101
+ value: "{{content}}"
102
+ - wait: 500
103
+ - capture:
104
+ name: current_url
105
+ eval: "location.href"
106
+ - click:
107
+ text: "发布"
108
+ - wait: 2000
109
+ - capture:
110
+ name: result_url
111
+ eval: "location.href"
112
+ - return:
113
+ - field: 状态
114
+ value: "✅ 发布成功"
115
+ - field: 标题
116
+ value: "{{title}}"
117
+ - field: 正文
118
+ value: "{{content}}"
119
+ - field: 跳转URL
120
+ value: "{{result_url}}"
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@harness.farm/social-cli",
3
+ "version": "0.1.0",
4
+ "description": "CDP-based social media automation CLI — X, 小红书, 抖音, B站, Temu",
5
+ "type": "module",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "bin": {
10
+ "social-cli": "./dist/cli.js"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "adapters"
15
+ ],
16
+ "scripts": {
17
+ "dev": "tsx src/cli.ts",
18
+ "build": "tsc",
19
+ "x": "tsx src/cli.ts x",
20
+ "xhs": "tsx src/cli.ts xhs",
21
+ "douyin": "tsx src/cli.ts douyin",
22
+ "bilibili": "tsx src/cli.ts bilibili",
23
+ "temu": "tsx src/cli.ts temu"
24
+ },
25
+ "keywords": [
26
+ "social-media",
27
+ "automation",
28
+ "cdp",
29
+ "xiaohongshu",
30
+ "douyin",
31
+ "bilibili",
32
+ "temu",
33
+ "cli"
34
+ ],
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/harness-farm/social-cli.git"
39
+ },
40
+ "dependencies": {
41
+ "ws": "^8.18.0",
42
+ "yaml": "^2.8.3"
43
+ },
44
+ "devDependencies": {
45
+ "@types/ws": "^8.5.12",
46
+ "tsx": "^4.19.2",
47
+ "typescript": "^5.8.2"
48
+ }
49
+ }