1
0

Compare commits

...

6 Commits

17 changed files with 551 additions and 46 deletions

16
.idea/csv-editor.xml generated
View File

@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CsvFileAttributes">
<option name="attributeMap">
<map>
<entry key="/dumpDataPreview">
<value>
<Attribute>
<option name="separator" value="," />
</Attribute>
</value>
</entry>
</map>
</option>
</component>
</project>

View File

@@ -2,12 +2,12 @@ import {createRoot} from 'react-dom/client'
import {createHashRouter, Navigate, type RouteObject, RouterProvider} from 'react-router'
import './index.scss'
import './components/amis/Registry.ts'
import Overview from './pages/Overview.tsx'
import Root from './pages/Root.tsx'
import Test from './pages/Test.tsx'
import Bookshelf from './pages/book/Bookshelf.tsx'
import Book from './pages/book/Book.tsx'
import Chapter from './pages/book/Chapter.tsx'
import Creator from './pages/Creator.tsx'
const routes: RouteObject[] = [
{
@@ -16,11 +16,7 @@ const routes: RouteObject[] = [
children: [
{
index: true,
element: <Navigate to="/overview" replace/>,
},
{
path: 'overview',
Component: Overview,
element: <Navigate to="/bookshelf" replace/>,
},
{
path: 'bookshelf',
@@ -39,6 +35,10 @@ const routes: RouteObject[] = [
},
],
},
{
path: 'creator',
Component: Creator,
},
{
path: 'test',
Component: Test,

View File

@@ -0,0 +1,204 @@
import React from 'react'
import {amisRender, commonInfo, horizontalFormOptions} from '../util/amis.tsx'
import {type Schema, uuid} from 'amis'
const aiToolbar = (component: Schema): Schema[] => {
const id = uuid()
return [
{
...component,
componentId: id,
},
{
className: 'text-right',
type: 'button-toolbar',
buttons: [
{
type: 'action',
label: 'AI 生成',
icon: 'fa fa-brain',
},
{
type: 'action',
label: 'AI 润色',
icon: 'fa fa-edit',
},
],
},
]
}
function Creator() {
return (
<div className="creator">
{amisRender(
{
type: 'page',
title: 'AI创作',
body: [
{
debug: true,
type: 'form',
...horizontalFormOptions(),
wrapWithPanel: false,
canAccessSuperData: false,
body: [
...aiToolbar({
type: 'textarea',
name: 'outline',
label: '故事概述',
clearable: true,
required: true,
trimContents: true,
showCounter: true,
}),
...aiToolbar({
type: 'textarea',
name: 'world',
label: '世界观',
clearable: true,
required: true,
trimContents: true,
showCounter: true,
}),
{
type: 'input-tag',
name: 'tags',
label: '标签',
placeholder: '',
clearable: true,
source: `${commonInfo.baseUrl}/book/tags`,
max: 10,
joinValues: false,
extractValue: true,
},
{
type: 'combo',
name: 'characters',
label: '故事人物',
multiLine: true,
addable: true,
removable: true,
multiple: true,
subFormMode: 'horizontal',
subFormHorizontal: {
leftFixed: 'sm',
},
items: [
...aiToolbar({
type: 'input-text',
name: 'name',
label: '名称',
clearable: true,
required: true,
trimContents: true,
}),
{
type: 'select',
name: 'sex',
label: '性别',
required: true,
options: [
{
label: '男',
value: 'male',
},
{
label: '女',
value: 'female',
},
],
},
{
type: 'input-number',
name: 'age',
label: '年龄',
min: 10,
step: 1,
precision: 0,
required: true,
},
...aiToolbar({
type: 'textarea',
name: 'appearance',
label: '外形',
clearable: true,
required: true,
trimContents: true,
showCounter: true,
}),
...aiToolbar({
componentId: 'd8eecb59-153d-4f5e-97b7-a52f4dc2dc58',
type: 'textarea',
name: 'disposition',
label: '性格',
clearable: true,
required: true,
trimContents: true,
showCounter: true,
}),
...aiToolbar({
type: 'textarea',
name: 'clothes',
label: '衣着',
clearable: true,
trimContents: true,
showCounter: true,
}),
...aiToolbar({
type: 'textarea',
name: 'experience',
label: '经历',
clearable: true,
trimContents: true,
showCounter: true,
}),
...aiToolbar({
type: 'textarea',
name: 'family',
label: '家庭',
clearable: true,
trimContents: true,
showCounter: true,
}),
{
type: 'input-kvs',
name: 'extra2',
label: '更多信息',
draggable: false,
keyItem: {
type: 'input-text',
label: '属性名称',
clearable: true,
trimContents: true,
mode: 'horizontal',
horizontal: {
leftFixed: 'sm',
},
},
valueItems: aiToolbar({
type: 'textarea',
name: 'value',
label: '属性内容',
clearable: true,
required: true,
trimContents: true,
showCounter: true,
mode: 'horizontal',
horizontal: {
leftFixed: 'sm',
},
}),
},
],
},
],
},
],
},
)}
</div>
)
}
export default React.memo(Creator)

View File

@@ -1,9 +0,0 @@
import React from 'react'
function Overview() {
return (
<div className="overview"></div>
)
}
export default React.memo(Overview)

View File

@@ -1,4 +1,4 @@
import {BookOutlined, DeploymentUnitOutlined, InfoCircleOutlined} from '@ant-design/icons'
import {BookOutlined, DeploymentUnitOutlined, EditOutlined, InfoCircleOutlined} from '@ant-design/icons'
import {type AppItemProps, ProLayout} from '@ant-design/pro-components'
import {ConfigProvider} from 'antd'
import React, {useMemo} from 'react'
@@ -25,16 +25,16 @@ const menus = {
name: '概览',
icon: <InfoCircleOutlined/>,
routes: [
{
path: '/overview',
name: '概览',
icon: <InfoCircleOutlined/>,
},
{
path: '/bookshelf',
name: '书架',
icon: <BookOutlined/>,
},
{
path: '/creator',
name: '创作',
icon: <EditOutlined/>,
},
{
path: '/test',
name: '测试',

View File

@@ -203,7 +203,6 @@ function Book() {
{
name: 'description',
label: '描述',
width: 250,
},
{
label: '创建时间',
@@ -223,6 +222,7 @@ function Book() {
type: 'action',
label: '详情',
level: 'link',
size: 'sm',
onEvent: {
click: {
actions: [
@@ -250,6 +250,7 @@ function Book() {
type: 'action',
label: '删除',
level: 'link',
size: 'sm',
actionType: 'ajax',
api: `get:${commonInfo.baseUrl}/chapter/remove/\${id}`,
confirmText: '确认删除章节<span class="text-lg font-bold mx-2">${name}</span>',

View File

@@ -96,7 +96,7 @@ function Bookshelf() {
{
name: 'name',
label: '书名',
width: 120,
width: 150,
fixed: 'left',
},
{
@@ -107,7 +107,7 @@ function Bookshelf() {
{
name: 'description',
label: '描述',
width: 250,
width: 500,
},
{
name: 'source',
@@ -130,13 +130,14 @@ function Bookshelf() {
{
type: 'operation',
label: '操作',
width: 150,
width: 180,
fixed: 'right',
buttons: [
{
type: 'action',
label: '详情',
level: 'link',
size: 'sm',
onEvent: {
click: {
actions: [
@@ -172,6 +173,7 @@ function Bookshelf() {
type: 'action',
label: '删除',
level: 'link',
size: 'sm',
actionType: 'ajax',
api: `get:${commonInfo.baseUrl}/book/remove/\${id}`,
confirmText: '确认删除书籍<span class="text-lg font-bold mx-2">${name}</span>',

View File

@@ -65,7 +65,7 @@ function Chapter() {
{
type: 'action',
label: '',
icon: 'fa fa-book-open-reader',
icon: 'fa fa-rotate-right',
actionType: 'ajax',
tooltip: '序号重排',
tooltipPlacement: 'top',
@@ -76,7 +76,7 @@ function Chapter() {
{
type: 'action',
label: '',
icon: 'fa fa-glasses',
icon: 'fa fa-book-open-reader',
actionType: 'dialog',
tooltip: '全文阅读',
tooltipPlacement: 'top',
@@ -159,6 +159,7 @@ function Chapter() {
type: 'action',
label: '删除',
level: 'link',
size: 'sm',
actionType: 'ajax',
api: `get:${commonInfo.baseUrl}/line/remove/\${id}`,
confirmText: '确认删除行?',

32
pom.xml
View File

@@ -16,11 +16,13 @@
<spring-boot.version>3.5.0</spring-boot.version>
<spring-cloud.version>2025.0.0</spring-cloud.version>
<spring-ai.version>1.1.0</spring-ai.version>
<hibernate.version>6.6.15.Final</hibernate.version>
<querydsl.version>7.0</querydsl.version>
<hutool.version>5.8.39</hutool.version>
<liteflow.version>2.15.0</liteflow.version>
<selenium.version>4.38.1</selenium.version>
</properties>
<dependencies>
@@ -37,6 +39,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
@@ -44,15 +50,26 @@
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-ai</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-spring-boot-starter</artifactId>
<version>${liteflow.version}</version>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
@@ -82,6 +99,19 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-dependencies-bom</artifactId>
<version>${selenium.version}</version>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>

View File

@@ -41,6 +41,7 @@ public class Book extends SimpleEntity {
@Column(nullable = false)
private String name;
private String author;
@Column(columnDefinition = "text")
private String description;
private String source;
@ElementCollection(fetch = FetchType.EAGER)

View File

@@ -40,7 +40,6 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener;
public class Chapter extends SimpleEntity {
@Column(nullable = false)
private Double sequence;
@Column(nullable = false)
private String name;
private String description;

View File

@@ -0,0 +1,54 @@
package com.lanyuanxiaoyao.bookstore.helper;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
/**
* 优化工具
*
* @author lanyuanxiaoyao
* @version 20250929
*/
@Slf4j
public class OptimiseHelper {
public static String optimize(String text) {
// 移除空行
text = StrUtil.trimToNull(text);
if (ObjectUtil.isNull(text)) {
return null;
}
// 英文全角字符转换为半角字符
text = halfWidth(text);
return text;
}
private static String halfWidth(String text) {
var builder = new StringBuilder();
for (var c : text.toCharArray()) {
// 检查是否为全角数字 (U+FF10 到 U+FF19)
if (c >= 0xFF10 && c <= 0xFF19) {
// 对应的半角数字是 (c - 0xFEE0)
builder.append((char) (c - 0xFEE0));
}
// 检查是否为全角大写字母 (U+FF21 到 U+FF3A)
else if (c >= 0xFF21 && c <= 0xFF3A) {
// 对应的半角字母是 (c - 0xFEE0)
builder.append((char) (c - 0xFEE0));
}
// 检查是否为全角小写字母 (U+FF41 到 U+FF5A)
else if (c >= 0xFF41 && c <= 0xFF5A) {
// 对应的半角字母是 (c - 0xFEE0)
builder.append((char) (c - 0xFEE0));
}
// 非全角字母和数字字符直接添加
else {
builder.append(c);
}
}
return builder.toString();
}
}

View File

@@ -8,7 +8,7 @@ import org.springframework.stereotype.Repository;
@Repository
public interface ChapterRepository extends SimpleRepository<Chapter> {
@Query("select max(chapter.sequence) from Chapter chapter")
@Query("select max(chapter.sequence) from Chapter chapter where chapter.book.id = ?1")
Optional<Double> findMaxSequence(Long bookId);
}

View File

@@ -0,0 +1,13 @@
package com.lanyuanxiaoyao.bookstore.service;
import org.springframework.stereotype.Service;
/**
* 书籍下载
*
* @author lanyuanxiaoyao
* @version 20251031
*/
@Service
public class CrawlerService {
}

View File

@@ -19,6 +19,13 @@ spring:
ddl-auto: update
main:
banner-mode: off
ai:
openai:
base-url: https://openrouter.ai/api/v1
api-key: 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
chat:
options:
model: "x-ai/grok-4.1-fast:free"
fenix:
print-banner: false
liteflow:

View File

@@ -0,0 +1,99 @@
package com.lanyuanxiaoyao.bookstore;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.StrUtil;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import lombok.extern.slf4j.Slf4j;
import org.openqa.selenium.By;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeDriverService;
import org.openqa.selenium.chrome.ChromeOptions;
/**
* @author lanyuanxiaoyao
* @version 20251031
*/
@Slf4j
public class BookCrawlerTest {
public static void main(String[] args) throws IOException {
ChromeDriver driver = null;
try {
driver = new ChromeDriver(
new ChromeDriverService.Builder()
.usingDriverExecutable(new File("/Users/lanyuanxiaoyao/SynologyDrive/tools/chromium/134/macOS-1345775/chromedriver"))
.build(),
new ChromeOptions()
.setBinary(new File("/Users/lanyuanxiaoyao/SynologyDrive/tools/chromium/134/macOS-1345775/Chromium.app/Contents/MacOS/Chromium"))
.addArguments(
// 允许不安全的域名
"--allow-insecure-localhost",
// 禁用GPU渲染headLess 模式用不上
"--disable-gpu",
// 禁用音频输出
"--disable-audio-output",
// 禁用错误页自动重新刷新
"--disable-auto-reload",
// 禁用默认应用加载
"--disable-default-apps",
// 禁用浏览器扩展
"--disable-extensions",
// 禁用日志
"--disable-logging",
// 禁用通知
"--disable-notifications",
// 禁用远程字体
"--disable-remote-fonts",
// 禁用弹出窗口
"--disable-popup-blocking",
// 禁用同步
"--disable-sync",
// 禁用沙盒
"--no-sandbox",
// 禁用声音
"--mute-audio",
// 禁止图片显示
"blink-settings=imagesEnabled=false"
)
);
var articleUrl = "https://www.alicesw.com/novel/32007.html";
driver.get(articleUrl);
var contextUrl = driver.findElement(By.xpath("//div[@class='book_newchap']//a[contains(text(),'查看所有章节')]")).getDomProperty("href");
if (StrUtil.isBlank(contextUrl)) {
throw new RuntimeException("获取目录页链接失败");
}
driver.get(contextUrl);
var chapterItems = driver.findElements(By.cssSelector("ul.mulu_list > li > a"))
.stream()
.map(element -> element.getDomProperty("href"))
.toList();
for (var index = 0; index < chapterItems.size(); index++) {
var chapterUrl = chapterItems.get(index);
if (StrUtil.isBlank(chapterUrl)) {
throw new RuntimeException("获取章节链接失败: " + chapterUrl);
}
driver.get(chapterUrl);
var text = driver.findElement(By.cssSelector(".read-content")).getText();
log.info(text);
var title = driver.getTitle();
if (StrUtil.isBlank(title)) {
title = String.valueOf(index);
}
var filename = StrUtil.format("{}.txt", title.replaceAll("\\s", "_"));
var targetFile = Path.of("out2", filename);
Files.deleteIfExists(targetFile);
Files.createFile(targetFile);
Files.writeString(targetFile, text);
ThreadUtil.safeSleep(2000);
}
} finally {
if (driver != null) {
driver.close();
}
}
}
}

View File

@@ -0,0 +1,119 @@
package com.lanyuanxiaoyao.bookstore;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.db.dialect.impl.MysqlDialect;
import com.lanyuanxiaoyao.service.template.helper.SnowflakeIdGenerator;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import lombok.extern.slf4j.Slf4j;
/**
* 更新章节
*
* @author lanyuanxiaoyao
* @version 20251031
*/
@Slf4j
public class UpdateIntoDatabase {
public static void main(String[] args) throws IOException, SQLException, ClassNotFoundException {
var chapters = new ArrayList<Chapter>();
Files.list(Path.of("out"))
.sorted(Comparator.comparing(path -> FileUtil.lastModifiedTime(path.toFile())))
.forEach(path -> {
try {
chapters.add(new Chapter(
path.getFileName().toString(),
Files.readString(path)
));
} catch (IOException e) {
throw new RuntimeException(e);
}
});
Class.forName("com.mysql.cj.jdbc.Driver");
try (
var connection = DriverManager.getConnection(
"jdbc:mysql://mysql.lanyuanxiaoyao.com:43780/bookstore?useUnicode=true&characterEncoding=utf8&useSSL=false",
"bookstore",
"EzSn+RZ*x2&fHFh9kC+H"
)
/*var ds = new SimpleDataSource(
"jdbc:mysql://mysql.lanyuanxiaoyao.com:43780/bookstore?useUnicode=true&characterEncoding=utf8&useSSL=false",
"bookstore",
"EzSn+RZ*x2&fHFh9kC+H",
"com.mysql.cj.jdbc.Driver"
)*/
) {
var dialect = new MysqlDialect();
for (int index = 0; index < chapters.size(); index++) {
var chapter = chapters.get(index);
log.info("Chapter: {}", chapter.title());
var now = LocalDateTime.now();
var chapterId = SnowflakeIdGenerator.Snowflake.next();
/*Db.use(ds, dialect).insert(
Entity.create("bookstore_chapter")
.set("id", chapterId)
.set("created_time", now)
.set("modified_time", now)
.set("sequence", index)
.set("book_id", 3608359126886400L)
);*/
try (var statement = connection.prepareStatement("insert into bookstore_chapter(id,created_time,modified_time,sequence,book_id) values(?,current_timestamp(),current_timestamp(),?,?)")) {
statement.setLong(1, chapterId);
statement.setDouble(2, index);
statement.setLong(3, 3608359126886400L);
statement.execute();
}
var lines = StrUtil.split(chapter.content, "\n")
.stream()
.map(StrUtil::trimToNull)
.filter(ObjectUtil::isNotNull)
.toList();
// var entities = new ArrayList<Entity>();
for (int lineIndex = 0; lineIndex < lines.size(); lineIndex++) {
/*entities.add(
Entity.create("bookstore_line")
.set("id", SnowflakeIdGenerator.Snowflake.next())
.set("created_time", now)
.set("modified_time", now)
.set("sequence", lineIndex)
.set("text", lines.get(lineIndex))
.set("chapter_id", chapterId)
);*/
try (var statement = connection.prepareStatement("insert into bookstore_line(id,created_time,modified_time,sequence,text,chapter_id) values(?,current_timestamp(),current_timestamp(),?,?,?)")) {
statement.setLong(1, SnowflakeIdGenerator.Snowflake.next());
statement.setDouble(2, lineIndex);
statement.setString(3, lines.get(lineIndex));
statement.setLong(4, chapterId);
statement.execute();
}
}
// Db.use(ds, dialect).insert(entities);
}
}
/*var response = AIUtil.chat(
new AIConfigBuilder(ModelName.OPENAI.getValue())
.setApiUrl("http://127.0.0.1:30000")
.setApiKey("*XMySqV%>hR&v>>g*NwCs3tpQ5FVMFEF2VHVTj<MYQd$&@$sY7CgqNyea4giJi4")
.setModel("")
.setTimout(1000 * 60 * 5)
.build(),
"""
—————
上述是小说一个章节的内容需要你为章节拟2句金庸武侠风格的七言绝句作为章节标题标题在精确概括章节内容外也需要充满文学性、浪漫性直接输出标题文字除了标题外严禁输出任何无关的文字禁止输出任何markdown格式
"""
);
log.info("response: {}", response);*/
}
public record Chapter(String title, String content) {
}
}