@iflyrpa/actions 0.0.1

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/dist/index.cjs ADDED
@@ -0,0 +1,228 @@
1
+ 'use strict';
2
+
3
+ const path = require('node:path');
4
+ const fs = require('node:fs');
5
+ const https = require('node:https');
6
+
7
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
8
+
9
+ const path__default = /*#__PURE__*/_interopDefaultCompat(path);
10
+ const fs__default = /*#__PURE__*/_interopDefaultCompat(fs);
11
+ const https__default = /*#__PURE__*/_interopDefaultCompat(https);
12
+
13
+ function ensureFile(filePath) {
14
+ return new Promise((resolve, reject) => {
15
+ const dirPath = path__default.dirname(filePath);
16
+ fs__default.stat(dirPath, (err, stats) => {
17
+ if (err) {
18
+ if (err.code === "ENOENT") {
19
+ fs__default.mkdir(dirPath, { recursive: true }, (err2) => {
20
+ if (err2) {
21
+ return reject(err2);
22
+ }
23
+ createFile();
24
+ });
25
+ } else {
26
+ return reject(err);
27
+ }
28
+ } else if (stats.isDirectory()) {
29
+ checkFile();
30
+ } else {
31
+ reject(new Error(`${dirPath} is not a directory`));
32
+ }
33
+ });
34
+ function checkFile() {
35
+ fs__default.stat(filePath, (err, stats) => {
36
+ if (err) {
37
+ if (err.code === "ENOENT") {
38
+ createFile();
39
+ } else {
40
+ reject(err);
41
+ }
42
+ } else if (stats.isFile()) {
43
+ resolve();
44
+ } else {
45
+ reject(new Error(`${filePath} is not a file`));
46
+ }
47
+ });
48
+ }
49
+ function createFile() {
50
+ fs__default.writeFile(filePath, "", (err) => {
51
+ if (err) {
52
+ reject(err);
53
+ } else {
54
+ resolve();
55
+ }
56
+ });
57
+ }
58
+ });
59
+ }
60
+ async function downloadImage(url, savePath) {
61
+ await ensureFile(savePath);
62
+ return new Promise((resolve, reject) => {
63
+ https__default.get(url, (response) => {
64
+ if (response.statusCode === 200) {
65
+ const fileStream = fs__default.createWriteStream(savePath);
66
+ response.pipe(fileStream);
67
+ fileStream.on("finish", () => {
68
+ fileStream.close();
69
+ console.log("\u4E0B\u8F7D\u5B8C\u6210\uFF0C\u6587\u4EF6\u5DF2\u4FDD\u5B58\u81F3:", savePath);
70
+ resolve(savePath);
71
+ });
72
+ } else {
73
+ console.log("\u56FE\u7247\u4E0B\u8F7D\u5931\u8D25:", response.statusCode);
74
+ response.resume();
75
+ reject();
76
+ }
77
+ }).on("error", (error) => {
78
+ console.error("\u8BF7\u6C42\u56FE\u7247\u65F6\u53D1\u751F\u9519\u8BEF:", error.message);
79
+ reject();
80
+ });
81
+ });
82
+ }
83
+ function getFilenameFromUrl(imageUrl) {
84
+ const parsedUrl = new URL(imageUrl);
85
+ return path__default.basename(parsedUrl.pathname);
86
+ }
87
+
88
+ const visibleRangeTexts = {
89
+ public: "\u516C\u5F00",
90
+ private: "\u79C1\u5BC6"
91
+ };
92
+ const xiaohongshuPublish = async (task, params) => {
93
+ task.logger.info("\u5F00\u59CB\u5C0F\u7EA2\u4E66\u53D1\u5E03");
94
+ const commonCookies = {
95
+ path: "/",
96
+ sameSite: "lax",
97
+ secure: false,
98
+ domain: "xiaohongshu.com",
99
+ url: "https://creator.xiaohongshu.com",
100
+ httpOnly: true
101
+ };
102
+ const page = await task.createPage({
103
+ show: task.debug,
104
+ url: params.url || "https://creator.xiaohongshu.com/publish/publish",
105
+ cookies: params.cookies?.map((it) => Object.assign(commonCookies, it)) || []
106
+ });
107
+ const tmpCachePath = task.getTmpPath();
108
+ const selectAddress = async (selector, address) => {
109
+ const instance = typeof selector === "string" ? page.locator(selector) : selector;
110
+ await instance.click();
111
+ await instance.locator("input").fill(address);
112
+ const poperInstance = page.locator(
113
+ '.d-popover:not([style*="display: none"]) .d-options .d-grid-item'
114
+ );
115
+ await poperInstance.first().waitFor();
116
+ await poperInstance.first().click();
117
+ };
118
+ const selectDate = async (selector, date) => {
119
+ const instance = typeof selector === "string" ? page.locator(selector) : selector;
120
+ await instance.click();
121
+ await instance.fill(date);
122
+ await instance.blur();
123
+ };
124
+ await page.waitForSelector("#CreatorPlatform", { state: "visible" }).catch(() => {
125
+ throw new Error("\u767B\u5F55\u5931\u8D25");
126
+ });
127
+ await page.locator("#content-area .menu-container .publish-video a").click().catch(() => {
128
+ throw new Error("\u672A\u627E\u5230\u53D1\u5E03\u7B14\u8BB0\u6309\u94AE");
129
+ });
130
+ await page.locator(".creator-container .header .title").filter({ hasText: /^上传图文$/ }).click().catch(() => {
131
+ throw new Error("\u672A\u627E\u5230\u4E0A\u4F20\u56FE\u6587\u6309\u94AE");
132
+ });
133
+ const images = await Promise.all(
134
+ params.banners.map((url) => {
135
+ const fileName = getFilenameFromUrl(url);
136
+ return downloadImage(url, path__default.join(tmpCachePath, fileName));
137
+ })
138
+ );
139
+ const fileChooserPromise = page.waitForEvent("filechooser");
140
+ await page.getByRole("textbox").click();
141
+ const fileChooser = await fileChooserPromise;
142
+ await fileChooser.setFiles(images);
143
+ const titleInstance = page.locator(".input.titleInput input");
144
+ await titleInstance.click();
145
+ await titleInstance.fill(params.title);
146
+ const descInstance = page.locator("#post-textarea");
147
+ await descInstance.click();
148
+ await descInstance.fill(params.content);
149
+ const container = page.locator(".creator-container .content .scroll-content");
150
+ await container.focus();
151
+ await page.mouse.wheel(0, 500);
152
+ if (params.address) {
153
+ await selectAddress(
154
+ page.locator(".media-extension .address-input").filter({ hasText: "\u6DFB\u52A0\u5730\u70B9" }),
155
+ params.address
156
+ );
157
+ }
158
+ if (params.selfDeclaration) {
159
+ await page.locator(".declaration-wrapper").click();
160
+ const selfDeclarationInstance = page.locator(
161
+ ".el-popper[aria-hidden=false] ul li[role=menuitem]"
162
+ );
163
+ if (params.selfDeclaration.type === "fictional-rendition") {
164
+ await selfDeclarationInstance.filter({ hasText: "\u865A\u6784\u6F14\u7ECE\uFF0C\u4EC5\u4F9B\u5A31\u4E50" }).click();
165
+ } else if (params.selfDeclaration.type === "ai-generated") {
166
+ await selfDeclarationInstance.filter({ hasText: "\u7B14\u8BB0\u542BAI\u5408\u6210\u5185\u5BB9" }).click();
167
+ } else if (params.selfDeclaration.type === "source-statement") {
168
+ await selfDeclarationInstance.filter({ hasText: "\u5185\u5BB9\u6765\u6E90\u58F0\u660E" }).click();
169
+ const selfDeclarationSecondaryMenuInstance = page.locator(".el-popper[aria-hidden=false] .el-cascader-menu").nth(1).locator("ul li[role=menuitem]");
170
+ await selfDeclarationSecondaryMenuInstance.first().waitFor();
171
+ if (params.selfDeclaration.childType === "self-labeling") {
172
+ await selfDeclarationSecondaryMenuInstance.filter({ hasText: "\u5DF2\u81EA\u4E3B\u6807\u6CE8" }).click();
173
+ } else if (params.selfDeclaration.childType === "self-shooting") {
174
+ const { shootingDate, shootingLocation } = params.selfDeclaration;
175
+ await selfDeclarationSecondaryMenuInstance.filter({ hasText: "\u81EA\u4E3B\u62CD\u6444" }).click();
176
+ const selfShootingPopup = page.locator(".el-overlay-dialog[aria-modal=true][role=dialog]").filter({ hasText: "\u81EA\u4E3B\u62CD\u6444" });
177
+ await selfShootingPopup.waitFor();
178
+ const hasCustomContent = shootingDate || shootingLocation;
179
+ if (shootingLocation) {
180
+ await selectAddress(
181
+ selfShootingPopup.locator(".address-input"),
182
+ shootingLocation
183
+ );
184
+ }
185
+ if (shootingDate) {
186
+ await selectDate(
187
+ selfShootingPopup.locator(".date-picker input"),
188
+ shootingDate
189
+ );
190
+ }
191
+ await selfShootingPopup.locator("footer button").filter({ hasText: hasCustomContent ? "\u786E\u8BA4" : "\u53D6\u6D88" }).click();
192
+ } else if (params.selfDeclaration.childType === "transshipment") {
193
+ await selfDeclarationSecondaryMenuInstance.filter({ hasText: "\u6765\u6E90\u8F6C\u8F7D" }).click();
194
+ const selfShootingPopup = page.locator(".el-overlay-dialog[aria-modal=true][role=dialog]").filter({ hasText: "\u6765\u6E90\u5A92\u4F53" });
195
+ await selfShootingPopup.waitFor();
196
+ const sourceMedia = params.selfDeclaration.sourceMedia;
197
+ if (sourceMedia) {
198
+ await selfShootingPopup.locator(".el-input input").fill(sourceMedia);
199
+ }
200
+ await selfShootingPopup.locator("footer button").filter({ hasText: sourceMedia ? "\u786E\u8BA4" : "\u53D6\u6D88" }).click();
201
+ }
202
+ }
203
+ }
204
+ const publicLabelInstance = page.locator("label").filter({ hasText: visibleRangeTexts[params.visibleRange] });
205
+ await publicLabelInstance.click();
206
+ const releaseTimeInstance = page.locator("label").filter({ hasText: params.isImmediatelyPublish ? "\u7ACB\u5373\u53D1\u5E03" : "\u5B9A\u65F6\u53D1\u5E03" });
207
+ await releaseTimeInstance.click();
208
+ if (params.scheduledPublish) {
209
+ await selectDate(".date-picker input", params.scheduledPublish);
210
+ }
211
+ const response = await new Promise((resolve) => {
212
+ const handleResponse = async (response2) => {
213
+ if (response2.url().includes("/web_api/sns/v2/note")) {
214
+ const jsonResponse = await response2.json();
215
+ page.off("response", handleResponse);
216
+ resolve(jsonResponse?.data?.id);
217
+ }
218
+ };
219
+ page.on("response", handleResponse);
220
+ page.locator(".submit .publishBtn").click();
221
+ });
222
+ await page.close();
223
+ return response;
224
+ };
225
+
226
+ const actions = { xiaohongshuPublish };
227
+
228
+ exports.actions = actions;
package/dist/index.mjs ADDED
@@ -0,0 +1,220 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+ import https from 'node:https';
4
+
5
+ function ensureFile(filePath) {
6
+ return new Promise((resolve, reject) => {
7
+ const dirPath = path.dirname(filePath);
8
+ fs.stat(dirPath, (err, stats) => {
9
+ if (err) {
10
+ if (err.code === "ENOENT") {
11
+ fs.mkdir(dirPath, { recursive: true }, (err2) => {
12
+ if (err2) {
13
+ return reject(err2);
14
+ }
15
+ createFile();
16
+ });
17
+ } else {
18
+ return reject(err);
19
+ }
20
+ } else if (stats.isDirectory()) {
21
+ checkFile();
22
+ } else {
23
+ reject(new Error(`${dirPath} is not a directory`));
24
+ }
25
+ });
26
+ function checkFile() {
27
+ fs.stat(filePath, (err, stats) => {
28
+ if (err) {
29
+ if (err.code === "ENOENT") {
30
+ createFile();
31
+ } else {
32
+ reject(err);
33
+ }
34
+ } else if (stats.isFile()) {
35
+ resolve();
36
+ } else {
37
+ reject(new Error(`${filePath} is not a file`));
38
+ }
39
+ });
40
+ }
41
+ function createFile() {
42
+ fs.writeFile(filePath, "", (err) => {
43
+ if (err) {
44
+ reject(err);
45
+ } else {
46
+ resolve();
47
+ }
48
+ });
49
+ }
50
+ });
51
+ }
52
+ async function downloadImage(url, savePath) {
53
+ await ensureFile(savePath);
54
+ return new Promise((resolve, reject) => {
55
+ https.get(url, (response) => {
56
+ if (response.statusCode === 200) {
57
+ const fileStream = fs.createWriteStream(savePath);
58
+ response.pipe(fileStream);
59
+ fileStream.on("finish", () => {
60
+ fileStream.close();
61
+ console.log("\u4E0B\u8F7D\u5B8C\u6210\uFF0C\u6587\u4EF6\u5DF2\u4FDD\u5B58\u81F3:", savePath);
62
+ resolve(savePath);
63
+ });
64
+ } else {
65
+ console.log("\u56FE\u7247\u4E0B\u8F7D\u5931\u8D25:", response.statusCode);
66
+ response.resume();
67
+ reject();
68
+ }
69
+ }).on("error", (error) => {
70
+ console.error("\u8BF7\u6C42\u56FE\u7247\u65F6\u53D1\u751F\u9519\u8BEF:", error.message);
71
+ reject();
72
+ });
73
+ });
74
+ }
75
+ function getFilenameFromUrl(imageUrl) {
76
+ const parsedUrl = new URL(imageUrl);
77
+ return path.basename(parsedUrl.pathname);
78
+ }
79
+
80
+ const visibleRangeTexts = {
81
+ public: "\u516C\u5F00",
82
+ private: "\u79C1\u5BC6"
83
+ };
84
+ const xiaohongshuPublish = async (task, params) => {
85
+ task.logger.info("\u5F00\u59CB\u5C0F\u7EA2\u4E66\u53D1\u5E03");
86
+ const commonCookies = {
87
+ path: "/",
88
+ sameSite: "lax",
89
+ secure: false,
90
+ domain: "xiaohongshu.com",
91
+ url: "https://creator.xiaohongshu.com",
92
+ httpOnly: true
93
+ };
94
+ const page = await task.createPage({
95
+ show: task.debug,
96
+ url: params.url || "https://creator.xiaohongshu.com/publish/publish",
97
+ cookies: params.cookies?.map((it) => Object.assign(commonCookies, it)) || []
98
+ });
99
+ const tmpCachePath = task.getTmpPath();
100
+ const selectAddress = async (selector, address) => {
101
+ const instance = typeof selector === "string" ? page.locator(selector) : selector;
102
+ await instance.click();
103
+ await instance.locator("input").fill(address);
104
+ const poperInstance = page.locator(
105
+ '.d-popover:not([style*="display: none"]) .d-options .d-grid-item'
106
+ );
107
+ await poperInstance.first().waitFor();
108
+ await poperInstance.first().click();
109
+ };
110
+ const selectDate = async (selector, date) => {
111
+ const instance = typeof selector === "string" ? page.locator(selector) : selector;
112
+ await instance.click();
113
+ await instance.fill(date);
114
+ await instance.blur();
115
+ };
116
+ await page.waitForSelector("#CreatorPlatform", { state: "visible" }).catch(() => {
117
+ throw new Error("\u767B\u5F55\u5931\u8D25");
118
+ });
119
+ await page.locator("#content-area .menu-container .publish-video a").click().catch(() => {
120
+ throw new Error("\u672A\u627E\u5230\u53D1\u5E03\u7B14\u8BB0\u6309\u94AE");
121
+ });
122
+ await page.locator(".creator-container .header .title").filter({ hasText: /^上传图文$/ }).click().catch(() => {
123
+ throw new Error("\u672A\u627E\u5230\u4E0A\u4F20\u56FE\u6587\u6309\u94AE");
124
+ });
125
+ const images = await Promise.all(
126
+ params.banners.map((url) => {
127
+ const fileName = getFilenameFromUrl(url);
128
+ return downloadImage(url, path.join(tmpCachePath, fileName));
129
+ })
130
+ );
131
+ const fileChooserPromise = page.waitForEvent("filechooser");
132
+ await page.getByRole("textbox").click();
133
+ const fileChooser = await fileChooserPromise;
134
+ await fileChooser.setFiles(images);
135
+ const titleInstance = page.locator(".input.titleInput input");
136
+ await titleInstance.click();
137
+ await titleInstance.fill(params.title);
138
+ const descInstance = page.locator("#post-textarea");
139
+ await descInstance.click();
140
+ await descInstance.fill(params.content);
141
+ const container = page.locator(".creator-container .content .scroll-content");
142
+ await container.focus();
143
+ await page.mouse.wheel(0, 500);
144
+ if (params.address) {
145
+ await selectAddress(
146
+ page.locator(".media-extension .address-input").filter({ hasText: "\u6DFB\u52A0\u5730\u70B9" }),
147
+ params.address
148
+ );
149
+ }
150
+ if (params.selfDeclaration) {
151
+ await page.locator(".declaration-wrapper").click();
152
+ const selfDeclarationInstance = page.locator(
153
+ ".el-popper[aria-hidden=false] ul li[role=menuitem]"
154
+ );
155
+ if (params.selfDeclaration.type === "fictional-rendition") {
156
+ await selfDeclarationInstance.filter({ hasText: "\u865A\u6784\u6F14\u7ECE\uFF0C\u4EC5\u4F9B\u5A31\u4E50" }).click();
157
+ } else if (params.selfDeclaration.type === "ai-generated") {
158
+ await selfDeclarationInstance.filter({ hasText: "\u7B14\u8BB0\u542BAI\u5408\u6210\u5185\u5BB9" }).click();
159
+ } else if (params.selfDeclaration.type === "source-statement") {
160
+ await selfDeclarationInstance.filter({ hasText: "\u5185\u5BB9\u6765\u6E90\u58F0\u660E" }).click();
161
+ const selfDeclarationSecondaryMenuInstance = page.locator(".el-popper[aria-hidden=false] .el-cascader-menu").nth(1).locator("ul li[role=menuitem]");
162
+ await selfDeclarationSecondaryMenuInstance.first().waitFor();
163
+ if (params.selfDeclaration.childType === "self-labeling") {
164
+ await selfDeclarationSecondaryMenuInstance.filter({ hasText: "\u5DF2\u81EA\u4E3B\u6807\u6CE8" }).click();
165
+ } else if (params.selfDeclaration.childType === "self-shooting") {
166
+ const { shootingDate, shootingLocation } = params.selfDeclaration;
167
+ await selfDeclarationSecondaryMenuInstance.filter({ hasText: "\u81EA\u4E3B\u62CD\u6444" }).click();
168
+ const selfShootingPopup = page.locator(".el-overlay-dialog[aria-modal=true][role=dialog]").filter({ hasText: "\u81EA\u4E3B\u62CD\u6444" });
169
+ await selfShootingPopup.waitFor();
170
+ const hasCustomContent = shootingDate || shootingLocation;
171
+ if (shootingLocation) {
172
+ await selectAddress(
173
+ selfShootingPopup.locator(".address-input"),
174
+ shootingLocation
175
+ );
176
+ }
177
+ if (shootingDate) {
178
+ await selectDate(
179
+ selfShootingPopup.locator(".date-picker input"),
180
+ shootingDate
181
+ );
182
+ }
183
+ await selfShootingPopup.locator("footer button").filter({ hasText: hasCustomContent ? "\u786E\u8BA4" : "\u53D6\u6D88" }).click();
184
+ } else if (params.selfDeclaration.childType === "transshipment") {
185
+ await selfDeclarationSecondaryMenuInstance.filter({ hasText: "\u6765\u6E90\u8F6C\u8F7D" }).click();
186
+ const selfShootingPopup = page.locator(".el-overlay-dialog[aria-modal=true][role=dialog]").filter({ hasText: "\u6765\u6E90\u5A92\u4F53" });
187
+ await selfShootingPopup.waitFor();
188
+ const sourceMedia = params.selfDeclaration.sourceMedia;
189
+ if (sourceMedia) {
190
+ await selfShootingPopup.locator(".el-input input").fill(sourceMedia);
191
+ }
192
+ await selfShootingPopup.locator("footer button").filter({ hasText: sourceMedia ? "\u786E\u8BA4" : "\u53D6\u6D88" }).click();
193
+ }
194
+ }
195
+ }
196
+ const publicLabelInstance = page.locator("label").filter({ hasText: visibleRangeTexts[params.visibleRange] });
197
+ await publicLabelInstance.click();
198
+ const releaseTimeInstance = page.locator("label").filter({ hasText: params.isImmediatelyPublish ? "\u7ACB\u5373\u53D1\u5E03" : "\u5B9A\u65F6\u53D1\u5E03" });
199
+ await releaseTimeInstance.click();
200
+ if (params.scheduledPublish) {
201
+ await selectDate(".date-picker input", params.scheduledPublish);
202
+ }
203
+ const response = await new Promise((resolve) => {
204
+ const handleResponse = async (response2) => {
205
+ if (response2.url().includes("/web_api/sns/v2/note")) {
206
+ const jsonResponse = await response2.json();
207
+ page.off("response", handleResponse);
208
+ resolve(jsonResponse?.data?.id);
209
+ }
210
+ };
211
+ page.on("response", handleResponse);
212
+ page.locator(".submit .publishBtn").click();
213
+ });
214
+ await page.close();
215
+ return response;
216
+ };
217
+
218
+ const actions = { xiaohongshuPublish };
219
+
220
+ export { actions };
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@iflyrpa/actions",
3
+ "version": "0.0.1",
4
+ "description": "",
5
+ "main": "./dist/index.cjs",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./src/index.ts",
8
+ "files": [
9
+ "dist",
10
+ "src"
11
+ ],
12
+ "author": "bijinfeng",
13
+ "license": "ISC",
14
+ "devDependencies": {
15
+ "unbuild": "^2.0.0",
16
+ "@iflyrpa/share": "0.0.1"
17
+ },
18
+ "scripts": {
19
+ "build": "unbuild",
20
+ "dev": "unbuild --stub"
21
+ }
22
+ }
@@ -0,0 +1,286 @@
1
+ import path from "node:path";
2
+ import type {
3
+ AutomateTask,
4
+ CommonAction,
5
+ CookieMap,
6
+ CookiesSetDetails,
7
+ Locator,
8
+ Response,
9
+ } from "@iflyrpa/share";
10
+ import { downloadImage, getFilenameFromUrl } from "@iflyrpa/share";
11
+
12
+ interface FictionalRendition {
13
+ type: "fictional-rendition"; // 虚拟演绎,仅供娱乐
14
+ }
15
+
16
+ interface AIGenerated {
17
+ type: "ai-generated"; // 笔记含AI合成内容
18
+ }
19
+
20
+ interface SourceStatement {
21
+ type: "source-statement"; // 内容来源声明
22
+ childType: "self-labeling" | "self-shooting" | "transshipment"; // 已自主标注 / 自助拍摄 / 转载
23
+ shootingLocation?: string; // 拍摄地点
24
+ shootingDate?: string; // 拍摄日期
25
+ sourceMedia?: string; // 来源媒体
26
+ }
27
+
28
+ type SelfDeclaration = FictionalRendition | AIGenerated | SourceStatement;
29
+
30
+ export interface ActiomCommonParams {
31
+ url?: string;
32
+ cookies?: CookieMap;
33
+ }
34
+
35
+ export interface XiaohongshuPublishParams
36
+ extends Omit<ActiomCommonParams, "cookies"> {
37
+ cookies: Partial<CookiesSetDetails>[];
38
+ banners: string[]; // 图片
39
+ title: string; // 标题
40
+ content: string; // 正文
41
+ address?: string; // 地点
42
+ selfDeclaration?: SelfDeclaration; // 自主声明
43
+ visibleRange: "public" | "private"; // 可见范围
44
+ isImmediatelyPublish?: boolean; // 是否立即发布
45
+ scheduledPublish?: string; // 定时发布时间
46
+ }
47
+
48
+ const visibleRangeTexts: Record<
49
+ XiaohongshuPublishParams["visibleRange"],
50
+ string
51
+ > = {
52
+ public: "公开",
53
+ private: "私密",
54
+ };
55
+
56
+ export const xiaohongshuPublish: CommonAction<
57
+ XiaohongshuPublishParams,
58
+ string
59
+ > = async (task, params) => {
60
+ task.logger.info("开始小红书发布");
61
+
62
+ const commonCookies: CookieMap[number] = {
63
+ path: "/",
64
+ sameSite: "lax",
65
+ secure: false,
66
+ domain: "xiaohongshu.com",
67
+ url: "https://creator.xiaohongshu.com",
68
+ httpOnly: true,
69
+ };
70
+
71
+ const page = await task.createPage({
72
+ show: task.debug,
73
+ url: params.url || "https://creator.xiaohongshu.com/publish/publish",
74
+ cookies:
75
+ params.cookies?.map((it) => Object.assign(commonCookies, it)) || [],
76
+ });
77
+
78
+ const tmpCachePath = task.getTmpPath();
79
+
80
+ // 通用的选择地点
81
+ const selectAddress = async (selector: Locator, address: string) => {
82
+ const instance =
83
+ typeof selector === "string" ? page.locator(selector) : selector;
84
+ await instance.click();
85
+ await instance.locator("input").fill(address);
86
+ const poperInstance = page.locator(
87
+ '.d-popover:not([style*="display: none"]) .d-options .d-grid-item',
88
+ );
89
+ await poperInstance.first().waitFor();
90
+ await poperInstance.first().click();
91
+ };
92
+ // 通用的选择日期
93
+ const selectDate = async (selector: string | Locator, date: string) => {
94
+ const instance =
95
+ typeof selector === "string" ? page.locator(selector) : selector;
96
+ await instance.click();
97
+ await instance.fill(date);
98
+ await instance.blur();
99
+ };
100
+
101
+ // 自动化脚本
102
+ // ----------------------------------------------------------------------------------
103
+
104
+ // 等待登录成功
105
+ await page
106
+ .waitForSelector("#CreatorPlatform", { state: "visible" })
107
+ .catch(() => {
108
+ throw new Error("登录失败");
109
+ });
110
+
111
+ // 跳转到发布页面
112
+ await page
113
+ .locator("#content-area .menu-container .publish-video a")
114
+ .click()
115
+ .catch(() => {
116
+ throw new Error("未找到发布笔记按钮");
117
+ });
118
+
119
+ // 点击上传图文
120
+ await page
121
+ .locator(".creator-container .header .title")
122
+ .filter({ hasText: /^上传图文$/ })
123
+ .click()
124
+ .catch(() => {
125
+ throw new Error("未找到上传图文按钮");
126
+ });
127
+
128
+ // 获取待上传图片的本地路径
129
+ const images = await Promise.all(
130
+ params.banners.map((url) => {
131
+ const fileName = getFilenameFromUrl(url);
132
+ return downloadImage(url, path.join(tmpCachePath, fileName));
133
+ }),
134
+ );
135
+
136
+ // 选择文件
137
+ const fileChooserPromise = page.waitForEvent("filechooser");
138
+ await page.getByRole("textbox").click();
139
+ const fileChooser = await fileChooserPromise;
140
+ await fileChooser.setFiles(images);
141
+
142
+ // 填写标题
143
+ const titleInstance = page.locator(".input.titleInput input");
144
+ await titleInstance.click();
145
+ await titleInstance.fill(params.title);
146
+
147
+ // 填写正文
148
+ const descInstance = page.locator("#post-textarea");
149
+ await descInstance.click();
150
+ await descInstance.fill(params.content);
151
+
152
+ // 滚动到底部
153
+ const container = page.locator(".creator-container .content .scroll-content");
154
+ await container.focus();
155
+ await page.mouse.wheel(0, 500); // 向下滚动 500 像素
156
+
157
+ if (params.address) {
158
+ // 填写地点
159
+ await selectAddress(
160
+ page
161
+ .locator(".media-extension .address-input")
162
+ .filter({ hasText: "添加地点" }),
163
+ params.address,
164
+ );
165
+ }
166
+
167
+ if (params.selfDeclaration) {
168
+ // 自主声明
169
+ await page.locator(".declaration-wrapper").click();
170
+ const selfDeclarationInstance = page.locator(
171
+ ".el-popper[aria-hidden=false] ul li[role=menuitem]",
172
+ );
173
+ if (params.selfDeclaration.type === "fictional-rendition") {
174
+ await selfDeclarationInstance
175
+ .filter({ hasText: "虚构演绎,仅供娱乐" })
176
+ .click();
177
+ } else if (params.selfDeclaration.type === "ai-generated") {
178
+ await selfDeclarationInstance
179
+ .filter({ hasText: "笔记含AI合成内容" })
180
+ .click();
181
+ } else if (params.selfDeclaration.type === "source-statement") {
182
+ await selfDeclarationInstance.filter({ hasText: "内容来源声明" }).click();
183
+ // 内容来源声明二级菜单
184
+ const selfDeclarationSecondaryMenuInstance = page
185
+ .locator(".el-popper[aria-hidden=false] .el-cascader-menu")
186
+ .nth(1)
187
+ .locator("ul li[role=menuitem]");
188
+ // 等待元素出现
189
+ await selfDeclarationSecondaryMenuInstance.first().waitFor();
190
+ if (params.selfDeclaration.childType === "self-labeling") {
191
+ await selfDeclarationSecondaryMenuInstance
192
+ .filter({ hasText: "已自主标注" })
193
+ .click();
194
+ } else if (params.selfDeclaration.childType === "self-shooting") {
195
+ const { shootingDate, shootingLocation } = params.selfDeclaration;
196
+ await selfDeclarationSecondaryMenuInstance
197
+ .filter({ hasText: "自主拍摄" })
198
+ .click();
199
+
200
+ // 自主拍摄弹窗
201
+ const selfShootingPopup = page
202
+ .locator(".el-overlay-dialog[aria-modal=true][role=dialog]")
203
+ .filter({ hasText: "自主拍摄" });
204
+ await selfShootingPopup.waitFor();
205
+
206
+ const hasCustomContent = shootingDate || shootingLocation;
207
+
208
+ // 选择拍摄地点
209
+ if (shootingLocation) {
210
+ await selectAddress(
211
+ selfShootingPopup.locator(".address-input"),
212
+ shootingLocation,
213
+ );
214
+ }
215
+ // 选择拍摄日期
216
+ if (shootingDate) {
217
+ await selectDate(
218
+ selfShootingPopup.locator(".date-picker input"),
219
+ shootingDate,
220
+ );
221
+ }
222
+
223
+ await selfShootingPopup
224
+ .locator("footer button")
225
+ .filter({ hasText: hasCustomContent ? "确认" : "取消" })
226
+ .click();
227
+ } else if (params.selfDeclaration.childType === "transshipment") {
228
+ await selfDeclarationSecondaryMenuInstance
229
+ .filter({ hasText: "来源转载" })
230
+ .click();
231
+ // 来源媒体弹窗
232
+ const selfShootingPopup = page
233
+ .locator(".el-overlay-dialog[aria-modal=true][role=dialog]")
234
+ .filter({ hasText: "来源媒体" });
235
+ await selfShootingPopup.waitFor();
236
+
237
+ const sourceMedia = params.selfDeclaration.sourceMedia;
238
+ if (sourceMedia) {
239
+ await selfShootingPopup.locator(".el-input input").fill(sourceMedia);
240
+ }
241
+
242
+ await selfShootingPopup
243
+ .locator("footer button")
244
+ .filter({ hasText: sourceMedia ? "确认" : "取消" })
245
+ .click();
246
+ }
247
+ }
248
+ }
249
+
250
+ // 可见范围
251
+ const publicLabelInstance = page
252
+ .locator("label")
253
+ .filter({ hasText: visibleRangeTexts[params.visibleRange] });
254
+ await publicLabelInstance.click();
255
+
256
+ const releaseTimeInstance = page
257
+ .locator("label")
258
+ .filter({ hasText: params.isImmediatelyPublish ? "立即发布" : "定时发布" });
259
+ await releaseTimeInstance.click();
260
+
261
+ if (params.scheduledPublish) {
262
+ // 自定义发布时间
263
+ await selectDate(".date-picker input", params.scheduledPublish);
264
+ }
265
+
266
+ const response = await new Promise<string>((resolve) => {
267
+ // 定义响应处理函数
268
+ const handleResponse = async (response: Response) => {
269
+ if (response.url().includes("/web_api/sns/v2/note")) {
270
+ const jsonResponse = await response.json();
271
+ page.off("response", handleResponse);
272
+ resolve(jsonResponse?.data?.id);
273
+ }
274
+ };
275
+
276
+ // 开始监听响应事件
277
+ page.on("response", handleResponse);
278
+
279
+ page.locator(".submit .publishBtn").click();
280
+ });
281
+
282
+ // 关闭页面
283
+ await page.close();
284
+
285
+ return response;
286
+ };
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { xiaohongshuPublish } from "./actions/xiaohongshuPublish";
2
+
3
+ export const actions = { xiaohongshuPublish };
4
+
5
+ export type { XiaohongshuPublishParams } from "./actions/xiaohongshuPublish";