3 Commits

Author SHA1 Message Date
anian 3e9d0b4441 新增微博图片下载与显示 2026-04-13 17:23:27 +08:00
anian 5a13068143 新增作者相关参数及接口 2026-03-15 23:41:07 +08:00
anian b1f30b374f 正文增加字数上限、去除微博视频提示 2026-03-15 23:39:04 +08:00
10 changed files with 10224 additions and 53 deletions
+20
View File
@@ -1,3 +1,23 @@
# anian-plugin-friends
> 基于原作者v1.4.3代码修改
#### v1.4.3-3
1. title字段改为微博图片列表
2. 图片列表内文件将自动下载至/upload/image-host
#### v1.4.3-2
1. 新增URL参数author用于查询作者
2. 模板参数新增${authors}返回所有作者
#### v1.4.3-1
1. 大幅增加description字段的字符数上限
2. 去除description字段内的微博视频提示
# plugin-friends # plugin-friends
> 1.3.4版本重构朋友圈插件 > 1.3.4版本重构朋友圈插件
> 如果使用了之前版本,请清掉之前的所有数据,不然会有问题 > 如果使用了之前版本,请清掉之前的所有数据,不然会有问题
+1 -1
View File
@@ -16,7 +16,7 @@ repositories {
} }
dependencies { dependencies {
implementation platform('run.halo.tools.platform:plugin:2.20.0-SNAPSHOT') implementation platform('run.halo.tools.platform:plugin:2.20.11')
compileOnly 'run.halo.app:api' compileOnly 'run.halo.app:api'
testImplementation 'run.halo.app:api' testImplementation 'run.halo.app:api'
+1 -1
View File
@@ -1 +1 @@
version=1.3.7 version=1.4.3-3
+9982
View File
File diff suppressed because it is too large Load Diff
@@ -25,4 +25,6 @@ public interface FriendFinder {
Mono<FriendPostVo> getByName(String friendPostName); Mono<FriendPostVo> getByName(String friendPostName);
Flux<LinkGroupVo> linkGroupBy(); Flux<LinkGroupVo> linkGroupBy();
Flux<String> listAllAuthors();
} }
@@ -96,6 +96,22 @@ public class FriendFinderImpl implements FriendFinder {
)); ));
} }
@Override
public Flux<String> listAllAuthors() {
var listOptions = new ListOptions();
// 只查有数据的,排除已删除的
var query = isNull("metadata.deletionTimestamp");
listOptions.setFieldSelector(FieldSelector.of(query));
return client.listAll(FriendPost.class, listOptions, defaultSort())
// 提取作者名
.map(friendPost -> friendPost.getSpec().getAuthor())
// 过滤掉空值
.filter(StringUtils::isNotBlank)
// 去重
.distinct();
}
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();
@@ -220,6 +236,8 @@ public class FriendFinderImpl implements FriendFinder {
private Integer page; private Integer page;
private Integer size; private Integer size;
private String linkName; private String linkName;
// 作者查询
private String author;
private List<String> sort; private List<String> sort;
public ListOptions toListOptions() { public ListOptions toListOptions() {
@@ -227,6 +245,10 @@ public class FriendFinderImpl implements FriendFinder {
if (StringUtils.isNotBlank(linkName)) { if (StringUtils.isNotBlank(linkName)) {
builder.andQuery(equal("spec.linkName", linkName)); builder.andQuery(equal("spec.linkName", linkName));
} }
// 作者查询
if (StringUtils.isNotBlank(author)) {
builder.andQuery(equal("spec.author", author));
}
return builder.build(); return builder.build();
} }
@@ -39,6 +39,12 @@ public class FriendPostQuery extends SortableRequest {
return queryParams.getFirst("linkName"); return queryParams.getFirst("linkName");
} }
// 作者查询
@Nullable
public String getAuthor() {
return StringUtils.defaultIfBlank(queryParams.getFirst("author"), null);
}
public ListOptions toListOptions() { public ListOptions toListOptions() {
var builder = ListOptions.builder(super.toListOptions()); var builder = ListOptions.builder(super.toListOptions());
@@ -55,6 +61,11 @@ public class FriendPostQuery extends SortableRequest {
.filter(StringUtils::isNotBlank) .filter(StringUtils::isNotBlank)
.ifPresent(linkName -> builder.andQuery(equal("spec.linkName", linkName))); .ifPresent(linkName -> builder.andQuery(equal("spec.linkName", linkName)));
// 作者查询
Optional.ofNullable(getAuthor())
.filter(StringUtils::isNotBlank)
.ifPresent(author -> builder.andQuery(equal("spec.author", author)));
return builder.build(); return builder.build();
} }
@@ -102,6 +113,13 @@ public class FriendPostQuery extends SortableRequest {
.name("keyword") .name("keyword")
.description("CronFriendPost filtered by keyword.") .description("CronFriendPost filtered by keyword.")
.implementation(String.class) .implementation(String.class)
.required(false))
// 作者查询
.parameter(parameterBuilder()
.in(ParameterIn.QUERY)
.name("author")
.description("FriendPost filtered by exact author name.")
.implementation(String.class)
.required(false)); .required(false));
} }
} }
@@ -27,16 +27,30 @@ import run.halo.app.extension.ListResult;
import run.halo.app.extension.Metadata; import run.halo.app.extension.Metadata;
import run.halo.app.extension.PageRequestImpl; import run.halo.app.extension.PageRequestImpl;
import run.halo.app.extension.Unstructured; import run.halo.app.extension.Unstructured;
import run.halo.app.extension.controller.*; import run.halo.app.extension.controller.Controller;
import run.halo.app.extension.controller.ControllerBuilder;
import run.halo.app.extension.controller.DefaultController;
import run.halo.app.extension.controller.DefaultQueue;
import run.halo.app.extension.controller.Reconciler;
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.net.URL; import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; 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.regex.Matcher;
import java.util.regex.Pattern;
import static org.springframework.data.domain.Sort.Order.asc; import static org.springframework.data.domain.Sort.Order.asc;
import static run.halo.app.extension.MetadataUtil.nullSafeAnnotations; import static run.halo.app.extension.MetadataUtil.nullSafeAnnotations;
@@ -49,6 +63,20 @@ public class RssSyncReconciler implements Reconciler<RssSyncReconciler.Request>,
private static final Logger log = LoggerFactory.getLogger(RssSyncReconciler.class); private static final Logger log = LoggerFactory.getLogger(RssSyncReconciler.class);
// 新增:正则匹配所有img标签的src属性
private static final Pattern IMG_SRC_PATTERN = Pattern.compile(
"<img[^>]*?src\\s*=\\s*(['\"])(.*?)\\1[^>]*?>",
Pattern.CASE_INSENSITIVE
);
private static final Pattern MW2000_IMAGE_FILE_PATTERN = Pattern.compile(
".*/mw2000/([^/?#]+)(?:[?#].*)?$",
Pattern.CASE_INSENSITIVE
);
private static final String IMAGE_CDN_BASE_URL = "https://cdn.img2ipfs.com/weibo/middle/";
private static final Path IMAGE_HOST_DIR = Path.of("/root/.halo2/attachments/upload/image-host/");
private volatile boolean running = false; private volatile boolean running = false;
private final ExtensionClient client; private final ExtensionClient client;
@@ -56,7 +84,6 @@ public class RssSyncReconciler implements Reconciler<RssSyncReconciler.Request>,
private final Controller controller; private final Controller controller;
private final ObjectMapper objectMapper = Unstructured.OBJECT_MAPPER; private final ObjectMapper objectMapper = Unstructured.OBJECT_MAPPER;
public RssSyncReconciler(ExtensionClient client) { public RssSyncReconciler(ExtensionClient client) {
this.client = client; this.client = client;
queue = new DefaultQueue<>(Instant::now); queue = new DefaultQueue<>(Instant::now);
@@ -133,7 +160,6 @@ public class RssSyncReconciler implements Reconciler<RssSyncReconciler.Request>,
return rssUrl; return rssUrl;
} }
public void updateLink(Link link) { public void updateLink(Link link) {
Map extensionMap = objectMapper.convertValue(link, Map.class); Map extensionMap = objectMapper.convertValue(link, Map.class);
var extension = new Unstructured(extensionMap); var extension = new Unstructured(extensionMap);
@@ -175,13 +201,34 @@ 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();
//解析html内容转换成文本
// 先提取图片文件名覆盖标题,再将 HTML 描述转换为纯文本
if (StringUtils.isNotEmpty(description)) { if (StringUtils.isNotEmpty(description)) {
description = CommonUtils.parseAndTruncateHtml2Text(description, 200); List<String> imageFileNames = new ArrayList<>();
String regexp = "[ *|\\s*]*"; Matcher matcher = IMG_SRC_PATTERN.matcher(description);
description = description.replaceFirst(regexp, "").trim(); while (matcher.find()) {
friendPostSpec.setDescription(description); 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);
}
client.create(friendPost); client.create(friendPost);
} }
} }
@@ -251,6 +298,44 @@ public class RssSyncReconciler implements Reconciler<RssSyncReconciler.Request>,
} }
} }
/**
* 通知图片处理服务有新的图片需要处理
* @param imageTitle 图片标题,格式为 "img:::file1,file2,..."
*/
private void notifyImageService(String imageTitle) {
if (StringUtils.isEmpty(imageTitle)) {
return;
}
new Thread(() -> downloadImagesByTitle(imageTitle)).start();
}
private void downloadImagesByTitle(String imageTitle) {
if (!imageTitle.startsWith("img:::")) {
return;
}
try {
Files.createDirectories(IMAGE_HOST_DIR);
String[] fileNames = imageTitle.substring("img:::".length()).split(",");
for (String fileName : fileNames) {
String trimmedName = fileName.trim();
if (StringUtils.isEmpty(trimmedName)) {
continue;
}
String encodedFileName = URLEncoder.encode(trimmedName, StandardCharsets.UTF_8)
.replace("+", "%20");
String downloadUrl = IMAGE_CDN_BASE_URL + encodedFileName;
Path targetPath = IMAGE_HOST_DIR.resolve(trimmedName);
try (InputStream inputStream = new URL(downloadUrl).openStream()) {
Files.copy(inputStream, targetPath, StandardCopyOption.REPLACE_EXISTING);
}
}
} catch (Exception e) {
log.warn("Failed to download images for title: {}", imageTitle, e);
}
}
static int pageNullSafe(Integer page) { static int pageNullSafe(Integer page) {
return ObjectUtils.defaultIfNull(page, 1); return ObjectUtils.defaultIfNull(page, 1);
} }
@@ -259,7 +344,6 @@ public class RssSyncReconciler implements Reconciler<RssSyncReconciler.Request>,
return ObjectUtils.defaultIfNull(size, 10); return ObjectUtils.defaultIfNull(size, 10);
} }
@Override @Override
public Controller setupWith(ControllerBuilder builder) { public Controller setupWith(ControllerBuilder builder) {
return new DefaultController<>( return new DefaultController<>(
@@ -49,6 +49,7 @@ public class FriendRouter {
.flatMap( templateName -> ServerResponse.ok().render(templateName, .flatMap( templateName -> ServerResponse.ok().render(templateName,
java.util.Map.of(ModelConst.TEMPLATE_ID, templateName, java.util.Map.of(ModelConst.TEMPLATE_ID, templateName,
"friends", friendPostList(request), "friends", friendPostList(request),
"authors", friendFinder.listAllAuthors().collectList(),
"title",getFriendTitle()) "title",getFriendTitle())
)); ));
} }
@@ -65,6 +66,24 @@ public class FriendRouter {
String linkName = request.queryParam("linkName") String linkName = request.queryParam("linkName")
.filter(StringUtils::isNotBlank) .filter(StringUtils::isNotBlank)
.orElse(null); .orElse(null);
// --- 新增:获取 author 参数 ---
String author = request.queryParam("author")
.filter(StringUtils::isNotBlank)
.orElse(null);
// --- 新增:构建查询参数字符串 ---
StringBuilder queryString = new StringBuilder();
if (StringUtils.isNotBlank(linkName)) {
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)
@@ -75,16 +94,40 @@ public class FriendRouter {
if(StringUtils.isNotBlank(linkName)){ if(StringUtils.isNotBlank(linkName)){
params.put("linkName",linkName); params.put("linkName",linkName);
} }
// --- 新增:把 author 也放进 params ---
if(StringUtils.isNotBlank(author)){
params.put("author",author);
}
return friendFinder.list(params) return friendFinder.list(params)
.map(list -> new UrlContextListResult.Builder<FriendPostVo>() .map(list -> {
// --- 修改:生成链接时带上查询参数 ---
String baseNextUrl = PageUrlUtils.nextPageUrl(path, totalPage(list));
String basePrevUrl = PageUrlUtils.prevPageUrl(path);
String nextUrl = appendQueryString(baseNextUrl, finalQueryString);
String prevUrl = appendQueryString(basePrevUrl, finalQueryString);
return new UrlContextListResult.Builder<FriendPostVo>()
.listResult(list) .listResult(list)
.nextUrl(PageUrlUtils.nextPageUrl(path, totalPage(list))) .nextUrl(nextUrl)
.prevUrl(PageUrlUtils.prevPageUrl(path)) .prevUrl(prevUrl)
.build() .build();
); });
}); });
} }
// --- 新增:辅助方法,用于拼接查询参数 ---
private String appendQueryString(String url, String queryString) {
if (StringUtils.isBlank(queryString)) {
return url;
}
if (url.contains("?")) {
return url + "&" + queryString;
} else {
return url + "?" + queryString;
}
}
private int pageNumInPathVariable(ServerRequest request) { private int pageNumInPathVariable(ServerRequest request) {
String page = request.pathVariables().get("page"); String page = request.pathVariables().get("page");
File diff suppressed because one or more lines are too long