5 Commits

Author SHA1 Message Date
anian d59689f8a4 大幅优化性能;优化显示逻辑、效果 2026-07-22 19:17:57 +08:00
anian edd12b6488 屏蔽空内容;接入搜索引擎;新增详情页面 2026-07-18 19:41:46 +08:00
anian 239bbf84f3 补充rsbuild依赖 2026-05-12 20:21:19 +08:00
anian 611db0d977 新增视频封面的记录与下载 2026-05-02 19:50:07 +08:00
anian 656ee0bcb4 新增控制台配置项;修改图片列表字段 2026-04-21 15:56:25 +08:00
21 changed files with 769 additions and 106 deletions
+15
View File
@@ -2,6 +2,21 @@
> 基于原作者v1.4.3代码修改 > 基于原作者v1.4.3代码修改
#### v1.4.3-6
1. 大幅优化分类查询性能
2. URL作者参数改为tag
3. 作者分类依据改为“链接”插件ID(新旧作者姓名将归为一类)
#### v1.4.3-5
1. 补充ui/package.json内"@rsbuild/core": "^1.4.3"依赖
2. 新增单条朋友圈页面(前端模板friend-post.html;访问地址/friends/friend-post-xxxxx
3. 朋友圈内容可搜索并返回单条朋友圈页面链接
4. 内容(description)为空的项目将不再存入数据库
#### v1.4.3-4
1. 控制台新增并行处理数量、图片下载开关、图片下载位置、图片下载代理等配置
2. 修改图片列表字段为imageFiles(不再覆盖title
#### v1.4.3-3 #### v1.4.3-3
1. title字段改为微博图片列表 1. title字段改为微博图片列表
2. 图片列表内文件将自动下载至/upload/image-host 2. 图片列表内文件将自动下载至/upload/image-host
+1 -1
View File
@@ -1 +1 @@
version=1.4.3-3 version=1.4.3-6
+3
View File
@@ -126,6 +126,9 @@ importers:
'@iconify/json': '@iconify/json':
specifier: ^2.2.224 specifier: ^2.2.224
version: 2.2.446 version: 2.2.446
'@rsbuild/core':
specifier: ^1.4.3
version: 1.7.3
'@rspack/cli': '@rspack/cli':
specifier: 1.4.0-beta.0 specifier: 1.4.0-beta.0
version: 1.4.0-beta.0(@rspack/core@1.4.0-beta.0(@swc/helpers@0.5.19))(@types/express@4.17.25)(tslib@2.8.1)(webpack@5.105.4) version: 1.4.0-beta.0(@rspack/core@1.4.0-beta.0(@swc/helpers@0.5.19))(@types/express@4.17.25)(tslib@2.8.1)(webpack@5.105.4)
@@ -9,12 +9,24 @@ import java.util.List;
public class RssFeedSyncEvent extends ApplicationEvent { public class RssFeedSyncEvent extends ApplicationEvent {
private final Link link; private final Link link;
private final int sum; private final int sum;
private final int parallelCount;
private final boolean enableWeiboImageRecordAndDownload;
private final List<String> disableSyncList; private final List<String> disableSyncList;
private final String imageSavePath;
private final String imageBaseUrl;
private final String imageReferer;
public RssFeedSyncEvent(Object source, Link link, int sum, List<String> disableSyncList) { public RssFeedSyncEvent(Object source, Link link, int sum, int parallelCount,
boolean enableWeiboImageRecordAndDownload, List<String> disableSyncList,
String imageSavePath, String imageBaseUrl, String imageReferer) {
super(source); super(source);
this.link = link; this.link = link;
this.sum = sum; this.sum = sum;
this.parallelCount = parallelCount;
this.enableWeiboImageRecordAndDownload = enableWeiboImageRecordAndDownload;
this.disableSyncList = disableSyncList; this.disableSyncList = disableSyncList;
this.imageSavePath = imageSavePath;
this.imageBaseUrl = imageBaseUrl;
this.imageReferer = imageReferer;
} }
} }
@@ -33,6 +33,11 @@ public class CronFriendPost extends AbstractExtension {
private String cron; private String cron;
private String timezone; private String timezone;
private boolean suspend; private boolean suspend;
private boolean enableWeiboImageRecordAndDownload;
private int parallelCount;
private String imageSavePath;
private String imageBaseUrl;
private String imageReferer;
@Schema( @Schema(
minimum = "0" minimum = "0"
@@ -38,6 +38,9 @@ public class FriendPost extends AbstractExtension {
private Instant pubDate; private Instant pubDate;
private String linkName; private String linkName;
// Comma-separated image file names extracted from content.
private String imageFiles;
} }
} }
@@ -3,6 +3,7 @@ package la.moony.friends.finders;
import la.moony.friends.finders.impl.FriendFinderImpl; import la.moony.friends.finders.impl.FriendFinderImpl;
import la.moony.friends.vo.FriendPostVo; import la.moony.friends.vo.FriendPostVo;
import la.moony.friends.vo.LinkGroupVo; import la.moony.friends.vo.LinkGroupVo;
import la.moony.friends.vo.LinkVo;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import run.halo.app.extension.ListResult; import run.halo.app.extension.ListResult;
@@ -27,4 +28,6 @@ public interface FriendFinder {
Flux<LinkGroupVo> linkGroupBy(); Flux<LinkGroupVo> linkGroupBy();
Flux<String> listAllAuthors(); Flux<String> listAllAuthors();
Flux<LinkVo> listAuthorLinks();
} }
@@ -112,6 +112,16 @@ public class FriendFinderImpl implements FriendFinder {
.distinct(); .distinct();
} }
@Override
public Flux<LinkVo> listAuthorLinks() {
var listOptions = new ListOptions();
listOptions.setFieldSelector(FieldSelector.of(isNull("metadata.deletionTimestamp")));
return client.listAll(Link.class, listOptions, defaultLinkSort())
.filter(link -> link.getSpec() != null
&& StringUtils.isNotBlank(link.getSpec().getDisplayName()))
.map(LinkVo::from);
}
private Mono<ListResult<FriendPostVo>> pageFriendPost(ListOptions queryOptions, PageRequest page){ private Mono<ListResult<FriendPostVo>> pageFriendPost(ListOptions queryOptions, PageRequest page){
var listOptions = new ListOptions(); var listOptions = new ListOptions();
var query = all(); var query = all();
@@ -7,7 +7,6 @@ import com.rometools.rome.feed.synd.SyndFeed;
import com.rometools.rome.io.SyndFeedInput; import com.rometools.rome.io.SyndFeedInput;
import com.rometools.rome.io.XmlReader; import com.rometools.rome.io.XmlReader;
import la.moony.friends.RssFeedSyncEvent; import la.moony.friends.RssFeedSyncEvent;
import la.moony.friends.extension.CronFriendPost;
import la.moony.friends.extension.FriendPost; import la.moony.friends.extension.FriendPost;
import la.moony.friends.extension.Link; import la.moony.friends.extension.Link;
import la.moony.friends.extension.RssFeedSyncLog; import la.moony.friends.extension.RssFeedSyncLog;
@@ -36,6 +35,7 @@ import run.halo.app.extension.controller.RequestQueue;
import run.halo.app.extension.router.selector.FieldSelector; import run.halo.app.extension.router.selector.FieldSelector;
import java.io.InputStream; import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@@ -49,6 +49,7 @@ import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.Semaphore;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -69,13 +70,19 @@ public class RssSyncReconciler implements Reconciler<RssSyncReconciler.Request>,
Pattern.CASE_INSENSITIVE Pattern.CASE_INSENSITIVE
); );
private static final Pattern VIDEO_POSTER_PATTERN = Pattern.compile(
"<video[^>]*?poster\\s*=\\s*(['\"])(.*?)\\1[^>]*?>",
Pattern.CASE_INSENSITIVE
);
private static final Pattern MW2000_IMAGE_FILE_PATTERN = Pattern.compile( private static final Pattern MW2000_IMAGE_FILE_PATTERN = Pattern.compile(
".*/mw2000/([^/?#]+)(?:[?#].*)?$", ".*/mw2000/([^/?#]+)(?:[?#].*)?$",
Pattern.CASE_INSENSITIVE Pattern.CASE_INSENSITIVE
); );
private static final String IMAGE_CDN_BASE_URL = "https://cdn.img2ipfs.com/weibo/middle/"; private static final String DEFAULT_IMAGE_BASE_URL = "https://wx3.sinaimg.cn/middle/";
private static final Path IMAGE_HOST_DIR = Path.of("/root/.halo2/attachments/upload/image-host/"); private static final String DEFAULT_IMAGE_REFERER = "https://weibo.com/";
private static final String DEFAULT_IMAGE_HOST_PATH = "/root/.halo2/attachments/upload/image-host/";
private volatile boolean running = false; private volatile boolean running = false;
@@ -83,6 +90,8 @@ public class RssSyncReconciler implements Reconciler<RssSyncReconciler.Request>,
private final RequestQueue<Request> queue; private final RequestQueue<Request> queue;
private final Controller controller; private final Controller controller;
private final ObjectMapper objectMapper = Unstructured.OBJECT_MAPPER; private final ObjectMapper objectMapper = Unstructured.OBJECT_MAPPER;
private volatile int configuredParallelCount = 1;
private volatile Semaphore syncSemaphore = new Semaphore(1, true);
public RssSyncReconciler(ExtensionClient client) { public RssSyncReconciler(ExtensionClient client) {
this.client = client; this.client = client;
@@ -92,9 +101,22 @@ public class RssSyncReconciler implements Reconciler<RssSyncReconciler.Request>,
@Override @Override
public Result reconcile(Request request) { public Result reconcile(Request request) {
int parallelCount = normalizeParallelCount(request.parallelCount());
refreshSyncSemaphore(parallelCount);
Semaphore semaphore = this.syncSemaphore;
boolean acquired = false;
try {
semaphore.acquire();
acquired = true;
Link link = request.link(); Link link = request.link();
int sum = request.sum(); int sum = request.sum();
List<String> disableSyncList = request.disableSyncList(); List<String> disableSyncList = request.disableSyncList();
boolean enableWeiboImageRecordAndDownload =
request.enableWeiboImageRecordAndDownload();
String imageSavePath = request.imageSavePath();
String imageBaseUrl = request.imageBaseUrl();
String imageReferer = request.imageReferer();
var linkName = link.getMetadata().getName(); var linkName = link.getMetadata().getName();
String rssUrl = getRss(link); String rssUrl = getRss(link);
boolean isContains = false; boolean isContains = false;
@@ -108,7 +130,8 @@ public class RssSyncReconciler implements Reconciler<RssSyncReconciler.Request>,
newRssFeedSyncLog.setMetadata(metadata); newRssFeedSyncLog.setMetadata(metadata);
newRssFeedSyncLog.setLinkName(linkName); newRssFeedSyncLog.setLinkName(linkName);
if (StringUtils.isNotEmpty(rssUrl) && !isContains) { if (StringUtils.isNotEmpty(rssUrl) && !isContains) {
tryToSynchronizeFriendPost(newRssFeedSyncLog, link, sum); tryToSynchronizeFriendPost(newRssFeedSyncLog, link, sum, imageSavePath,
imageBaseUrl, imageReferer, enableWeiboImageRecordAndDownload);
} else if (StringUtils.isEmpty(rssUrl)) { } else if (StringUtils.isEmpty(rssUrl)) {
newRssFeedSyncLog.setSyncTime(Instant.now()); newRssFeedSyncLog.setSyncTime(Instant.now());
newRssFeedSyncLog.setState(RssFeedSyncLog.RssFeedSyncLogState.nolink); newRssFeedSyncLog.setState(RssFeedSyncLog.RssFeedSyncLogState.nolink);
@@ -132,6 +155,27 @@ public class RssSyncReconciler implements Reconciler<RssSyncReconciler.Request>,
client.create(newRssFeedSyncLog); client.create(newRssFeedSyncLog);
} }
return Result.doNotRetry(); return Result.doNotRetry();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("Interrupted while waiting for sync permit.", e);
return Result.doNotRetry();
} finally {
if (acquired) {
semaphore.release();
}
}
}
private synchronized void refreshSyncSemaphore(int parallelCount) {
if (parallelCount == configuredParallelCount) {
return;
}
configuredParallelCount = parallelCount;
syncSemaphore = new Semaphore(parallelCount, true);
}
private int normalizeParallelCount(int parallelCount) {
return parallelCount <= 0 ? 1 : parallelCount;
} }
public String getRss(Link link) { public String getRss(Link link) {
@@ -166,15 +210,12 @@ public class RssSyncReconciler implements Reconciler<RssSyncReconciler.Request>,
client.update(extension); client.update(extension);
} }
private void tryToSynchronizeFriendPost(RssFeedSyncLog syncLog, Link link, int sum) { private void tryToSynchronizeFriendPost(RssFeedSyncLog syncLog, Link link, int sum,
String imageSavePath, String imageBaseUrl, String imageReferer,
boolean enableWeiboImageRecordAndDownload) {
var linkName = link.getMetadata().getName(); var linkName = link.getMetadata().getName();
var annotations = nullSafeAnnotations(link); var annotations = nullSafeAnnotations(link);
var rssUrl = annotations.get("rss_url"); var rssUrl = annotations.get("rss_url");
CronFriendPost cron = new CronFriendPost();
var spec = new CronFriendPost.CronSpec();
spec.setSuccessfulRetainLimit(5);
cron.setSpec(spec);
syncLog.setSyncTime(Instant.now()); syncLog.setSyncTime(Instant.now());
List<FriendPost> friendPostList = fetchFriendPost(rssUrl, sum, syncLog); List<FriendPost> friendPostList = fetchFriendPost(rssUrl, sum, syncLog);
@@ -201,34 +242,40 @@ public class RssSyncReconciler implements Reconciler<RssSyncReconciler.Request>,
friendPostSpec.setAuthorUrl(link.getSpec().getUrl()); friendPostSpec.setAuthorUrl(link.getSpec().getUrl());
friendPostSpec.setLinkName(linkName); friendPostSpec.setLinkName(linkName);
String description = friendPost.getSpec().getDescription(); String description = friendPost.getSpec().getDescription();
String pureText = normalizeDescription(description);
// 先提取图片文件名覆盖标题,再将 HTML 描述转换为纯文本 if (StringUtils.isBlank(pureText)) {
if (StringUtils.isNotEmpty(description)) { continue;
List<String> imageFileNames = new ArrayList<>();
Matcher matcher = IMG_SRC_PATTERN.matcher(description);
while (matcher.find()) {
String imgSrc = matcher.group(2);
Matcher mw2000Matcher = MW2000_IMAGE_FILE_PATTERN.matcher(imgSrc);
if (mw2000Matcher.matches()) {
imageFileNames.add(mw2000Matcher.group(1));
}
}
if (!imageFileNames.isEmpty()) {
String imageTitle = "img:::" + String.join(",", imageFileNames);
friendPostSpec.setTitle(imageTitle);
// 通知图片处理服务
notifyImageService(imageTitle);
}
String pureText = CommonUtils.parseAndTruncateHtml2Text(description, 100000);
String regexp = "[ *|\\s*]*";
pureText = pureText.replaceFirst(regexp, "").trim();
String weiboVideoRegex = "\\s[^\\s]*的微博视频 视频无法显示,请前往微博视频观看。";
pureText = pureText.replaceAll(weiboVideoRegex, "");
friendPostSpec.setDescription(pureText);
} }
// 提取图片和视频 poster 文件名,并保存过滤后的纯文本描述
List<String> imageFileNames = new ArrayList<>();
Matcher matcher = IMG_SRC_PATTERN.matcher(description);
while (matcher.find()) {
String imgSrc = matcher.group(2);
Matcher mw2000Matcher = MW2000_IMAGE_FILE_PATTERN.matcher(imgSrc);
if (mw2000Matcher.matches()) {
imageFileNames.add(mw2000Matcher.group(1));
}
}
Matcher posterMatcher = VIDEO_POSTER_PATTERN.matcher(description);
while (posterMatcher.find()) {
String posterSrc = posterMatcher.group(2);
String posterFileName = extractFileNameFromUrl(posterSrc);
if (StringUtils.isNotBlank(posterFileName)) {
imageFileNames.add(posterFileName);
}
}
if (enableWeiboImageRecordAndDownload && !imageFileNames.isEmpty()) {
String imageFiles = String.join(",", imageFileNames);
friendPostSpec.setImageFiles(imageFiles);
// 通知图片处理服务
notifyImageService(imageFiles, imageSavePath, imageBaseUrl,
imageReferer);
}
friendPostSpec.setDescription(pureText);
client.create(friendPost); client.create(friendPost);
} }
} }
@@ -236,6 +283,17 @@ public class RssSyncReconciler implements Reconciler<RssSyncReconciler.Request>,
} }
} }
private String normalizeDescription(String description) {
if (StringUtils.isEmpty(description)) {
return "";
}
String pureText = CommonUtils.parseAndTruncateHtml2Text(description, 100000);
String regexp = "[ *|\\s*]*";
pureText = pureText.replaceFirst(regexp, "").trim();
String weiboVideoRegex = "\\s[^\\s]*的微博视频 视频无法显示,请前往微博视频观看。";
return pureText.replaceAll(weiboVideoRegex, "").trim();
}
public List<FriendPost> fetchFriendPost(String rssAddress, int postsLimit, RssFeedSyncLog sync) { public List<FriendPost> fetchFriendPost(String rssAddress, int postsLimit, RssFeedSyncLog sync) {
List<FriendPost> friendPostList = new ArrayList<>(); List<FriendPost> friendPostList = new ArrayList<>();
try { try {
@@ -300,22 +358,28 @@ public class RssSyncReconciler implements Reconciler<RssSyncReconciler.Request>,
/** /**
* 通知图片处理服务有新的图片需要处理 * 通知图片处理服务有新的图片需要处理
* @param imageTitle 图片标题,格式为 "img:::file1,file2,..." * @param imageFiles 图片文件名,英文逗号分隔,如 "file1,file2,..."
*/ */
private void notifyImageService(String imageTitle) { private void notifyImageService(String imageFiles, String imageSavePath, String imageBaseUrl,
if (StringUtils.isEmpty(imageTitle)) { String imageReferer) {
if (StringUtils.isBlank(imageFiles)) {
return; return;
} }
new Thread(() -> downloadImagesByTitle(imageTitle)).start(); new Thread(() -> downloadImagesByFiles(imageFiles, imageSavePath, imageBaseUrl,
imageReferer)).start();
} }
private void downloadImagesByTitle(String imageTitle) { private void downloadImagesByFiles(String imageFiles, String imageSavePath,
if (!imageTitle.startsWith("img:::")) { String imageBaseUrl, String imageReferer) {
if (StringUtils.isBlank(imageFiles)) {
return; return;
} }
try { try {
Files.createDirectories(IMAGE_HOST_DIR); Path imageHostDir = resolveImageHostDir(imageSavePath);
String[] fileNames = imageTitle.substring("img:::".length()).split(","); String resolvedBaseUrl = resolveImageBaseUrl(imageBaseUrl);
String resolvedReferer = resolveImageReferer(imageBaseUrl, imageReferer);
Files.createDirectories(imageHostDir);
String[] fileNames = imageFiles.split(",");
for (String fileName : fileNames) { for (String fileName : fileNames) {
String trimmedName = fileName.trim(); String trimmedName = fileName.trim();
if (StringUtils.isEmpty(trimmedName)) { if (StringUtils.isEmpty(trimmedName)) {
@@ -324,18 +388,72 @@ public class RssSyncReconciler implements Reconciler<RssSyncReconciler.Request>,
String encodedFileName = URLEncoder.encode(trimmedName, StandardCharsets.UTF_8) String encodedFileName = URLEncoder.encode(trimmedName, StandardCharsets.UTF_8)
.replace("+", "%20"); .replace("+", "%20");
String downloadUrl = IMAGE_CDN_BASE_URL + encodedFileName; String downloadUrl = resolvedBaseUrl + encodedFileName;
Path targetPath = IMAGE_HOST_DIR.resolve(trimmedName); Path targetPath = imageHostDir.resolve(trimmedName);
try (InputStream inputStream = new URL(downloadUrl).openStream()) { HttpURLConnection connection = (HttpURLConnection) new URL(downloadUrl)
.openConnection();
if (StringUtils.isNotBlank(resolvedReferer)) {
connection.setRequestProperty("Referer", resolvedReferer);
}
try (InputStream inputStream = connection.getInputStream()) {
Files.copy(inputStream, targetPath, StandardCopyOption.REPLACE_EXISTING); Files.copy(inputStream, targetPath, StandardCopyOption.REPLACE_EXISTING);
} finally {
connection.disconnect();
} }
} }
} catch (Exception e) { } catch (Exception e) {
log.warn("Failed to download images for title: {}", imageTitle, e); log.warn("Failed to download images for files: {}", imageFiles, e);
} }
} }
private Path resolveImageHostDir(String imageSavePath) {
if (StringUtils.isBlank(imageSavePath)) {
return Path.of(DEFAULT_IMAGE_HOST_PATH);
}
return Path.of(imageSavePath.trim());
}
private String resolveImageBaseUrl(String imageBaseUrl) {
String resolvedBaseUrl = StringUtils.isBlank(imageBaseUrl)
? DEFAULT_IMAGE_BASE_URL
: imageBaseUrl.trim();
if (!resolvedBaseUrl.endsWith("/")) {
return resolvedBaseUrl + "/";
}
return resolvedBaseUrl;
}
private String resolveImageReferer(String imageBaseUrl, String imageReferer) {
if (StringUtils.isBlank(imageBaseUrl)) {
return DEFAULT_IMAGE_REFERER;
}
if (StringUtils.isBlank(imageReferer)) {
return null;
}
return imageReferer.trim();
}
private String extractFileNameFromUrl(String url) {
if (StringUtils.isBlank(url)) {
return null;
}
String sanitizedUrl = url.trim();
int queryIndex = sanitizedUrl.indexOf('?');
if (queryIndex >= 0) {
sanitizedUrl = sanitizedUrl.substring(0, queryIndex);
}
int fragmentIndex = sanitizedUrl.indexOf('#');
if (fragmentIndex >= 0) {
sanitizedUrl = sanitizedUrl.substring(0, fragmentIndex);
}
int lastSlashIndex = sanitizedUrl.lastIndexOf('/');
if (lastSlashIndex < 0 || lastSlashIndex == sanitizedUrl.length() - 1) {
return null;
}
return sanitizedUrl.substring(lastSlashIndex + 1);
}
static int pageNullSafe(Integer page) { static int pageNullSafe(Integer page) {
return ObjectUtils.defaultIfNull(page, 1); return ObjectUtils.defaultIfNull(page, 1);
} }
@@ -375,10 +493,16 @@ public class RssSyncReconciler implements Reconciler<RssSyncReconciler.Request>,
@EventListener(RssFeedSyncEvent.class) @EventListener(RssFeedSyncEvent.class)
public void onReplyEvent(RssFeedSyncEvent event) { public void onReplyEvent(RssFeedSyncEvent event) {
var request = new Request(event.getLink(), event.getSum(), event.getDisableSyncList()); var request = new Request(event.getLink(), event.getSum(), event.getDisableSyncList(),
event.isEnableWeiboImageRecordAndDownload(),
event.getImageSavePath(), event.getImageBaseUrl(), event.getImageReferer(),
event.getParallelCount());
queue.addImmediately(request); queue.addImmediately(request);
} }
public record Request(Link link, int sum, List<String> disableSyncList) { public record Request(Link link, int sum, List<String> disableSyncList,
boolean enableWeiboImageRecordAndDownload,
String imageSavePath, String imageBaseUrl,
String imageReferer, int parallelCount) {
} }
} }
@@ -2,6 +2,7 @@ package la.moony.friends.rest;
import la.moony.friends.finders.FriendFinder; import la.moony.friends.finders.FriendFinder;
import la.moony.friends.vo.FriendPostVo; import la.moony.friends.vo.FriendPostVo;
import la.moony.friends.vo.LinkVo;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@@ -13,17 +14,18 @@ import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import run.halo.app.extension.ListResult;
import run.halo.app.plugin.ReactiveSettingFetcher; import run.halo.app.plugin.ReactiveSettingFetcher;
import run.halo.app.theme.TemplateNameResolver; import run.halo.app.theme.TemplateNameResolver;
import run.halo.app.theme.router.ModelConst; import run.halo.app.theme.router.ModelConst;
import run.halo.app.theme.router.PageUrlUtils;
import run.halo.app.theme.router.UrlContextListResult; import run.halo.app.theme.router.UrlContextListResult;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import static run.halo.app.theme.router.PageUrlUtils.totalPage;
@Configuration @Configuration
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j @Slf4j
@@ -40,17 +42,31 @@ public class FriendRouter {
RouterFunction<ServerResponse> friendTemplateRoute() { RouterFunction<ServerResponse> friendTemplateRoute() {
return RouterFunctions.route().GET("/friends",this::handlerFunction) return RouterFunctions.route().GET("/friends",this::handlerFunction)
.GET("/friends/page/{page:\\d+}",this::handlerFunction) .GET("/friends/page/{page:\\d+}",this::handlerFunction)
.GET("/friends/{friendPostName:\\S+}", this::friendPostDetail)
.build(); .build();
} }
private Mono<ServerResponse> handlerFunction(ServerRequest request) { private Mono<ServerResponse> handlerFunction(ServerRequest request) {
return templateNameResolver.resolveTemplateNameOrDefault(request.exchange(), "friends") return templateNameResolver.resolveTemplateNameOrDefault(request.exchange(), "friends")
.flatMap( templateName -> ServerResponse.ok().render(templateName, .flatMap(templateName -> {
java.util.Map.of(ModelConst.TEMPLATE_ID, templateName, Mono<List<LinkVo>> authors = friendFinder.listAuthorLinks().collectList().cache();
"friends", friendPostList(request), return ServerResponse.ok().render(templateName,
"authors", friendFinder.listAllAuthors().collectList(), Map.of(ModelConst.TEMPLATE_ID, templateName,
"title",getFriendTitle()) "friends", friendPostList(request, authors),
"authors", authors,
"title", getFriendTitle())
);
});
}
private Mono<ServerResponse> friendPostDetail(ServerRequest request) {
String friendPostName = request.pathVariable("friendPostName");
return templateNameResolver.resolveTemplateNameOrDefault(request.exchange(), "friend-post")
.flatMap(templateName -> ServerResponse.ok().render(templateName,
Map.of(ModelConst.TEMPLATE_ID, templateName,
"friend", friendFinder.getByName(friendPostName),
"title", getFriendTitle())
)); ));
} }
@@ -60,50 +76,38 @@ public class FriendRouter {
"友链朋友圈"); "友链朋友圈");
} }
private Mono<UrlContextListResult<FriendPostVo>> friendPostList(ServerRequest request) { private Mono<UrlContextListResult<FriendPostVo>> friendPostList(ServerRequest request,
String path = request.path(); Mono<List<LinkVo>> authorLinks) {
int pageNum = pageNumInPathVariable(request); int pageNum = pageNumInPathVariable(request);
String linkName = request.queryParam("linkName") String tag = request.queryParam("tag")
.filter(StringUtils::isNotBlank)
.orElse(null);
// --- 新增:获取 author 参数 ---
String author = request.queryParam("author")
.filter(StringUtils::isNotBlank) .filter(StringUtils::isNotBlank)
.orElse(null); .orElse(null);
// --- 新增:构建查询参数字符串 --- return authorLinks.flatMap(links -> {
StringBuilder queryString = new StringBuilder(); String linkName = linkNameByDisplayName(links, tag);
if (StringUtils.isNotBlank(linkName)) { String finalQueryString = buildQueryString(tag);
queryString.append("linkName=").append(linkName);
}
if (StringUtils.isNotBlank(author)) {
if (queryString.length() > 0) {
queryString.append("&");
}
queryString.append("author=").append(author);
}
final String finalQueryString = queryString.toString();
return this.settingFetcher.get("base") return this.settingFetcher.get("base")
.map(item -> item.get("pageSize").asInt(10)) .map(item -> item.get("pageSize").asInt(10))
.defaultIfEmpty(10) .defaultIfEmpty(10)
.flatMap(pageSize -> { .flatMap(pageSize -> {
Map<String, Object> params = new HashMap<>(); if (StringUtils.isNotBlank(tag) && StringUtils.isBlank(linkName)) {
params.put("page",pageNum); return Mono.just(emptyFriendPostList(pageNum, pageSize, finalQueryString));
params.put("size",pageSize); }
if(StringUtils.isNotBlank(linkName)){
params.put("linkName",linkName);
}
// --- 新增:把 author 也放进 params ---
if(StringUtils.isNotBlank(author)){
params.put("author",author);
}
return friendFinder.list(params) Map<String, Object> params = new HashMap<>();
params.put("page", pageNum);
params.put("size", pageSize);
if (StringUtils.isNotBlank(linkName)) {
params.put("linkName", linkName);
}
return friendFinder.list(params)
.map(list -> { .map(list -> {
// --- 修改:生成链接时带上查询参数 --- String baseNextUrl = "/friends?page=" + (list.getPage() + 1);
String baseNextUrl = PageUrlUtils.nextPageUrl(path, totalPage(list)); String basePrevUrl = list.getPage() > 2
String basePrevUrl = PageUrlUtils.prevPageUrl(path); ? "/friends?page=" + (list.getPage() - 1)
: "/friends";
String nextUrl = appendQueryString(baseNextUrl, finalQueryString); String nextUrl = appendQueryString(baseNextUrl, finalQueryString);
String prevUrl = appendQueryString(basePrevUrl, finalQueryString); String prevUrl = appendQueryString(basePrevUrl, finalQueryString);
@@ -114,7 +118,44 @@ public class FriendRouter {
.prevUrl(prevUrl) .prevUrl(prevUrl)
.build(); .build();
}); });
}); });
});
}
private UrlContextListResult<FriendPostVo> emptyFriendPostList(int page, int size,
String queryString) {
String nextUrl = appendQueryString("/friends?page=" + (page + 1), queryString);
String prevUrl = appendQueryString(page > 2 ? "/friends?page=" + (page - 1) : "/friends",
queryString);
return new UrlContextListResult.Builder<FriendPostVo>()
.listResult(new ListResult<>(page, size, 0, List.of()))
.nextUrl(nextUrl)
.prevUrl(prevUrl)
.build();
}
private String buildQueryString(String tag) {
if (StringUtils.isNotBlank(tag)) {
return "tag=" + encodeQueryValue(tag);
}
return "";
}
private String linkNameByDisplayName(List<LinkVo> authorLinks, String displayName) {
if (StringUtils.isBlank(displayName)) {
return null;
}
return authorLinks.stream()
.filter(link -> link.getMetadata() != null && link.getSpec() != null)
.filter(link -> StringUtils.equals(displayName, link.getSpec().getDisplayName()))
.map(link -> link.getMetadata().getName())
.filter(StringUtils::isNotBlank)
.findFirst()
.orElse(null);
}
private String encodeQueryValue(String value) {
return URLEncoder.encode(value, StandardCharsets.UTF_8);
} }
// --- 新增:辅助方法,用于拼接查询参数 --- // --- 新增:辅助方法,用于拼接查询参数 ---
@@ -131,6 +172,9 @@ public class FriendRouter {
private int pageNumInPathVariable(ServerRequest request) { private int pageNumInPathVariable(ServerRequest request) {
String page = request.pathVariables().get("page"); String page = request.pathVariables().get("page");
if (StringUtils.isBlank(page)) {
page = request.queryParam("page").orElse("1");
}
return NumberUtils.toInt(page, 1); return NumberUtils.toInt(page, 1);
} }
@@ -0,0 +1,101 @@
package la.moony.friends.search;
import static la.moony.friends.search.FriendPostHaloDocumentsProvider.FRIEND_POST_DOCUMENT_TYPE;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import la.moony.friends.extension.FriendPost;
import la.moony.friends.util.CommonUtils;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import run.halo.app.infra.ExternalUrlSupplier;
import run.halo.app.search.HaloDocument;
@Component
@RequiredArgsConstructor
public class FriendPostDocumentConverter implements Converter<FriendPost, Mono<HaloDocument>> {
private static final String EMPTY_CONTENT = "无内容";
private final ExternalUrlSupplier externalUrlSupplier;
@Override
@NonNull
public Mono<HaloDocument> convert(FriendPost friendPost) {
var spec = friendPost.getSpec();
var author = authorName(friendPost);
var title = defaultIfBlank(spec == null ? null : spec.getTitle(), EMPTY_CONTENT);
var description = defaultIfBlank(plainDescription(spec), EMPTY_CONTENT);
var haloDoc = new HaloDocument();
haloDoc.setMetadataName(friendPost.getMetadata().getName());
haloDoc.setType(FRIEND_POST_DOCUMENT_TYPE);
haloDoc.setId(haloDocId(friendPost));
haloDoc.setTitle(title);
haloDoc.setDescription(description);
haloDoc.setContent(content(title, author, description));
haloDoc.setExposed(true);
haloDoc.setTags(tags(author));
haloDoc.setOwnerName(author);
haloDoc.setUpdateTimestamp(updateTimestamp(friendPost));
haloDoc.setCreationTimestamp(creationTimestamp(friendPost));
haloDoc.setPermalink(permalink(friendPost));
haloDoc.setPublished(true);
return Mono.just(haloDoc);
}
String haloDocId(FriendPost friendPost) {
return FRIEND_POST_DOCUMENT_TYPE + '-' + friendPost.getMetadata().getName();
}
private String authorName(FriendPost friendPost) {
var author = Optional.ofNullable(friendPost.getSpec())
.map(FriendPost.FriendPostSpec::getAuthor)
.orElse(null);
return StringUtils.defaultIfBlank(author, EMPTY_CONTENT);
}
private String plainDescription(FriendPost.FriendPostSpec spec) {
if (spec == null) {
return null;
}
return CommonUtils.parseAndTruncateHtml2Text(defaultString(spec.getDescription()), 300);
}
private String content(String title, String author, String description) {
return String.join(" ", title, author, description).trim();
}
private List<String> tags(String author) {
return StringUtils.isBlank(author) ? List.of() : List.of(author);
}
private Instant updateTimestamp(FriendPost friendPost) {
var spec = friendPost.getSpec();
return Optional.ofNullable(spec)
.map(FriendPost.FriendPostSpec::getPubDate)
.orElse(creationTimestamp(friendPost));
}
private Instant creationTimestamp(FriendPost friendPost) {
return Optional.ofNullable(friendPost.getMetadata().getCreationTimestamp())
.orElse(Instant.EPOCH);
}
private String permalink(FriendPost friendPost) {
var externalUrl = externalUrlSupplier.get();
return externalUrl.resolve("friends/" + friendPost.getMetadata().getName()).toString();
}
private static String defaultString(String value) {
return StringUtils.defaultString(value);
}
private static String defaultIfBlank(String value, String defaultValue) {
return StringUtils.defaultIfBlank(value, defaultValue);
}
}
@@ -0,0 +1,59 @@
package la.moony.friends.search;
import la.moony.friends.extension.FriendPost;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.PageRequest;
import run.halo.app.extension.PageRequestImpl;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.router.selector.FieldSelector;
import run.halo.app.search.HaloDocument;
import run.halo.app.search.HaloDocumentsProvider;
import static run.halo.app.extension.index.query.QueryFactory.isNull;
@Component
@RequiredArgsConstructor
public class FriendPostHaloDocumentsProvider implements HaloDocumentsProvider {
public static final String FRIEND_POST_DOCUMENT_TYPE = "friendpost.friend.moony.la";
private static final int PAGE_SIZE = 200;
private final ReactiveExtensionClient client;
private final FriendPostDocumentConverter converter;
@Override
public Flux<HaloDocument> fetchAll() {
var options = new ListOptions();
var notDeleted = isNull("metadata.deletionTimestamp");
options.setFieldSelector(FieldSelector.of(notDeleted));
var pageRequest = createPageRequest();
return client.listBy(FriendPost.class, options, pageRequest)
.expand(result -> result.hasNext()
? client.listBy(FriendPost.class, options, nextPage(result, pageRequest.getSort()))
: Mono.empty())
.flatMap(result -> Flux.fromStream(result.get()))
.flatMap(converter::convert);
}
@Override
public String getType() {
return FRIEND_POST_DOCUMENT_TYPE;
}
private PageRequest createPageRequest() {
return PageRequestImpl.of(1, PAGE_SIZE,
Sort.by("metadata.creationTimestamp", "metadata.name"));
}
private static PageRequest nextPage(ListResult<FriendPost> result, Sort sort) {
return PageRequestImpl.of(result.getPage() + 1, result.getSize(), sort);
}
}
@@ -0,0 +1,61 @@
package la.moony.friends.search;
import java.util.List;
import java.util.Set;
import la.moony.friends.extension.FriendPost;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.ExtensionUtil;
import run.halo.app.extension.controller.Controller;
import run.halo.app.extension.controller.ControllerBuilder;
import run.halo.app.extension.controller.Reconciler;
import run.halo.app.search.event.HaloDocumentAddRequestEvent;
import run.halo.app.search.event.HaloDocumentDeleteRequestEvent;
@Component
@RequiredArgsConstructor
public class FriendPostSearchReconciler implements Reconciler<Reconciler.Request> {
private static final String FINALIZER = "friend-post-search-protection";
private final ApplicationEventPublisher eventPublisher;
private final ExtensionClient client;
private final FriendPostDocumentConverter converter;
@Override
public Result reconcile(Request request) {
client.fetch(FriendPost.class, request.name()).ifPresent(friendPost -> {
if (ExtensionUtil.isDeleted(friendPost)) {
if (ExtensionUtil.removeFinalizers(friendPost.getMetadata(), Set.of(FINALIZER))) {
eventPublisher.publishEvent(
new HaloDocumentDeleteRequestEvent(this,
List.of(converter.haloDocId(friendPost)))
);
client.update(friendPost);
}
return;
}
ExtensionUtil.addFinalizers(friendPost.getMetadata(), Set.of(FINALIZER));
var haloDoc = converter.convert(friendPost)
.blockOptional().orElseThrow();
eventPublisher.publishEvent(
new HaloDocumentAddRequestEvent(this, List.of(haloDoc)));
client.update(friendPost);
});
return Result.doNotRetry();
}
@Override
public Controller setupWith(ControllerBuilder builder) {
return builder
.extension(new FriendPost())
.workerCount(1)
.build();
}
}
@@ -74,14 +74,25 @@ public class RssFeedSyncLogServiceImpl implements RssFeedSyncLogService {
CronFriendPost cron = new CronFriendPost(); CronFriendPost cron = new CronFriendPost();
CronFriendPost.CronSpec cronSpec = new CronFriendPost.CronSpec(); CronFriendPost.CronSpec cronSpec = new CronFriendPost.CronSpec();
cronSpec.setSuccessfulRetainLimit(5); cronSpec.setSuccessfulRetainLimit(5);
cronSpec.setParallelCount(1);
cronSpec.setEnableWeiboImageRecordAndDownload(false);
cronSpec.setDisableSyncList(new ArrayList<>()); cronSpec.setDisableSyncList(new ArrayList<>());
cronSpec.setImageSavePath("");
cronSpec.setImageBaseUrl("");
cronSpec.setImageReferer("");
cron.setSpec(cronSpec); cron.setSpec(cronSpec);
return client.fetch(CronFriendPost.class, "cron-friends-default") return client.fetch(CronFriendPost.class, "cron-friends-default")
.defaultIfEmpty(cron) .defaultIfEmpty(cron)
.flatMap(cronFriendPost -> { .flatMap(cronFriendPost -> {
var spec = cronFriendPost.getSpec(); var spec = cronFriendPost.getSpec();
int successfulRetainLimit = spec.getSuccessfulRetainLimit(); int successfulRetainLimit = spec.getSuccessfulRetainLimit();
int parallelCount = spec.getParallelCount();
boolean enableWeiboImageRecordAndDownload =
spec.isEnableWeiboImageRecordAndDownload();
List<String> disableSyncList = spec.getDisableSyncList(); List<String> disableSyncList = spec.getDisableSyncList();
String imageSavePath = spec.getImageSavePath();
String imageBaseUrl = spec.getImageBaseUrl();
String imageReferer = spec.getImageReferer();
int sum = successfulRetainLimit == 0 ? 5 : successfulRetainLimit; int sum = successfulRetainLimit == 0 ? 5 : successfulRetainLimit;
var listOptions = new ListOptions(); var listOptions = new ListOptions();
FieldSelector fieldSelector = FieldSelector.of(isNull("metadata.deletionTimestamp")); FieldSelector fieldSelector = FieldSelector.of(isNull("metadata.deletionTimestamp"));
@@ -90,7 +101,11 @@ public class RssFeedSyncLogServiceImpl implements RssFeedSyncLogService {
} }
listOptions.setFieldSelector(fieldSelector); listOptions.setFieldSelector(fieldSelector);
return client.listAll(Link.class, listOptions, Sort.by("metadata.creationTimestamp")) return client.listAll(Link.class, listOptions, Sort.by("metadata.creationTimestamp"))
.doOnNext(link -> eventPublisher.publishEvent(new RssFeedSyncEvent(this, link,sum,disableSyncList))) .doOnNext(link -> eventPublisher.publishEvent(
new RssFeedSyncEvent(this, link, sum, parallelCount,
enableWeiboImageRecordAndDownload, disableSyncList, imageSavePath,
imageBaseUrl, imageReferer)
))
.then(); .then();
}); });
} }
@@ -0,0 +1,133 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" th:content="${friend.spec.description ?: '无内容'}">
<title th:text="${site.title + '-' + (friend.spec.title ?: friend.spec.author)}"></title>
<link data-n-head="true" rel="icon" type="image/x-icon" th:href="${site.favicon}">
<style>
body {
margin: 0;
color: #242424;
background: #f7f7f7;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
Arial, "Noto Sans", sans-serif;
line-height: 1.8;
}
a {
color: #666;
text-decoration: none;
}
a:hover {
color: #111;
}
.wrapper {
max-width: 820px;
margin: 60px auto 32px;
padding: 56px;
background: #fff;
box-shadow: 0 10px 20px 0 hsla(0, 0%, 93%, .86);
}
.back {
display: inline-block;
margin-bottom: 28px;
color: #888;
font-size: 14px;
}
h1 {
margin: 0 0 18px;
color: #1f1f1f;
font-size: 28px;
line-height: 1.35;
}
.meta {
display: flex;
flex-wrap: wrap;
gap: 12px 18px;
margin-bottom: 36px;
color: #888;
font-size: 14px;
}
.author {
display: inline-flex;
align-items: center;
gap: 8px;
}
.author img {
width: 24px;
height: 24px;
border-radius: 50%;
object-fit: cover;
}
.content {
color: #333;
font-size: 16px;
white-space: pre-wrap;
word-break: break-word;
}
.actions {
margin-top: 40px;
padding-top: 24px;
border-top: 1px solid #eee;
}
footer {
margin: 0 auto 40px;
color: #999;
font-size: 13px;
text-align: center;
}
@media (max-width: 760px) {
.wrapper {
margin: 0;
padding: 32px 22px;
min-height: 100vh;
box-shadow: none;
}
h1 {
font-size: 24px;
}
}
</style>
</head>
<body>
<article class="wrapper">
<a class="back" href="/friends">返回朋友圈</a>
<h1 th:text="${friend.spec.title ?: '无内容'}"></h1>
<div class="meta">
<span class="author">
<img th:if="${friend.spec.logo != null and not #strings.isEmpty(friend.spec.logo)}"
th:src="${friend.spec.logo}" alt="">
<a th:if="${friend.spec.authorUrl != null and not #strings.isEmpty(friend.spec.authorUrl)}"
th:href="${friend.spec.authorUrl}" target="_blank" rel="noopener noreferrer"
th:text="${friend.spec.author}"></a>
<span th:unless="${friend.spec.authorUrl != null and not #strings.isEmpty(friend.spec.authorUrl)}"
th:text="${friend.spec.author}"></span>
</span>
<time th:if="${friend.spec.pubDate != null}"
th:datetime="${#dates.format(friend.spec.pubDate,'yyyy-MM-dd')}"
th:text="${#dates.format(friend.spec.pubDate,'yyyy.MM.dd')}"></time>
</div>
<div class="content" th:text="${friend.spec.description ?: '无内容'}"></div>
<div class="actions" th:if="${friend.spec.postLink != null and not #strings.isEmpty(friend.spec.postLink)}">
<a th:href="${friend.spec.postLink}" target="_blank" rel="noopener noreferrer">查看原文</a>
</div>
</article>
<footer>
<span>©2024 [[${site.title}]]</span>
</footer>
</body>
</html>
+3 -2
View File
@@ -1511,9 +1511,10 @@
<main> <main>
<section class="article-list"> <section class="article-list">
<article th:each="friend : ${friends.items}"> <article th:each="friend : ${friends.items}">
<h2><a th:href="${friend.spec.postLink}" target="_blank" th:text="${friend.spec.title}"></a></h2> <h2><a th:href="@{/friends/{name}(name=${friend.metadata.name})}"
th:text="${friend.spec.title ?: '无内容'}"></a></h2>
<div class="excerpt"> <div class="excerpt">
<p th:text="${friend.spec.description}"></p> <p th:text="${friend.spec.description ?: '无内容'}"></p>
</div> </div>
<div class="meta"> <div class="meta">
<span class="item"> <span class="item">
+1
View File
@@ -39,6 +39,7 @@
"@kunkunyu/fridends-rss": "workspace:*" "@kunkunyu/fridends-rss": "workspace:*"
}, },
"devDependencies": { "devDependencies": {
"@rsbuild/core": "^1.4.3",
"@halo-dev/ui-plugin-bundler-kit": "^2.21.2", "@halo-dev/ui-plugin-bundler-kit": "^2.21.2",
"@iconify/json": "^2.2.224", "@iconify/json": "^2.2.224",
"@rspack/cli": "1.4.0-beta.0", "@rspack/cli": "1.4.0-beta.0",
+30
View File
@@ -32,6 +32,36 @@ export interface CronSpec {
* @memberof CronSpec * @memberof CronSpec
*/ */
'disableSyncList'?: Array<string>; 'disableSyncList'?: Array<string>;
/**
*
* @type {number}
* @memberof CronSpec
*/
'parallelCount'?: number;
/**
*
* @type {boolean}
* @memberof CronSpec
*/
'enableWeiboImageRecordAndDownload'?: boolean;
/**
*
* @type {string}
* @memberof CronSpec
*/
'imageSavePath'?: string;
/**
*
* @type {string}
* @memberof CronSpec
*/
'imageBaseUrl'?: string;
/**
*
* @type {string}
* @memberof CronSpec
*/
'imageReferer'?: string;
/** /**
* *
* @type {number} * @type {number}
@@ -38,6 +38,12 @@ export interface FriendPostSpec {
* @memberof FriendPostSpec * @memberof FriendPostSpec
*/ */
'description'?: string; 'description'?: string;
/**
* Comma-separated image file names.
* @type {string}
* @memberof FriendPostSpec
*/
'imageFiles'?: string;
/** /**
* *
* @type {string} * @type {string}
+37
View File
@@ -18,7 +18,12 @@ const initialFormState: CronFriendPost = {
cron: "@daily", cron: "@daily",
timezone:"Asia/Shanghai", timezone:"Asia/Shanghai",
suspend: false, suspend: false,
enableWeiboImageRecordAndDownload: false,
parallelCount: 1,
successfulRetainLimit: 0, successfulRetainLimit: 0,
imageSavePath: "",
imageBaseUrl: "",
imageReferer: "",
disableSyncList: undefined, disableSyncList: undefined,
}, },
kind: "CronFriendPost", kind: "CronFriendPost",
@@ -130,6 +135,20 @@ const { mutate:save, isLoading:saveIsLoading } = useMutation({
}, },
]" ]"
/> />
<FormKit
type="number"
name="parallelCount"
label="并行请求数量"
number="integer"
validation="required|number|min:1"
help="默认 1(串行),大于 1 为并行"
/>
<FormKit
type="checkbox"
name="enableWeiboImageRecordAndDownload"
label="记录并下载微博图片"
help="默认关闭,开启后才会记录 imageFiles 字段并下载微博图片"
/>
<FormKit <FormKit
type="number" type="number"
name="successfulRetainLimit" name="successfulRetainLimit"
@@ -138,6 +157,24 @@ const { mutate:save, isLoading:saveIsLoading } = useMutation({
validation="required|number|min:0" validation="required|number|min:0"
help="设置之后会保留的数据条数,设置为 0 即为5条" help="设置之后会保留的数据条数,设置为 0 即为5条"
/> />
<FormKit
type="text"
name="imageSavePath"
label="图片保存路径"
help="为空时使用默认路径 /root/.halo2/attachments/upload/image-host/"
/>
<FormKit
type="text"
name="imageBaseUrl"
label="图片下载 Base URL"
help="为空时默认 https://wx3.sinaimg.cn/middle/"
/>
<FormKit
type="text"
name="imageReferer"
label="图片下载 Referer"
help="当 Base URL 为空时默认 https://weibo.com/;当 Base URL 不为空时可留空"
/>
<LinkFormKit <LinkFormKit
name="disableSyncList" name="disableSyncList"
label="禁止同步名单" label="禁止同步名单"