mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-15 19:41:02 +08:00
Compare commits
17 Commits
codex/add-
...
feature/ne
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fe3eec815 | ||
|
|
f0feb7a45c | ||
|
|
784057207f | ||
|
|
bed72662b5 | ||
|
|
895dba495b | ||
|
|
32dc6bfaf9 | ||
|
|
4766250577 | ||
|
|
13baffa9f1 | ||
|
|
d0d7580ac3 | ||
|
|
fd4e651a49 | ||
|
|
58317687d7 | ||
|
|
006e46f4ef | ||
|
|
2c27766544 | ||
|
|
c305992223 | ||
|
|
babd2c6549 | ||
|
|
49aeff3a83 | ||
|
|
512e5623e1 |
2
.github/workflows/coffee-bot.yml
vendored
2
.github/workflows/coffee-bot.yml
vendored
@@ -2,7 +2,7 @@ name: Coffee Bot
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 1 * * 1-5"
|
||||
- cron: "0 23 * * 0-4"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -26,7 +26,7 @@ OpenIsle 是一个使用 Spring Boot 和 Vue 3 构建的全栈开源社区平台
|
||||
- 集成 OpenAI 提供的 Markdown 格式化功能
|
||||
- 通过环境变量可调整密码强度、登录方式、保护码等多种配置
|
||||
- 支持图片上传,默认使用腾讯云 COS 扩展
|
||||
- 默认头像使用 DiceBear Avatars,可通过 `AVATAR_STYLE` 和 `AVATAR_SIZE` 环境变量自定义主题和大小
|
||||
- Bot 集成,可在平台内快速连接自定义机器人,并通过 Telegram 的 BotFather 创建和管理消息机器人,拓展社区互动渠道
|
||||
- 浏览器推送通知,离开网站也能及时收到提醒
|
||||
|
||||
## 🌟 项目优势
|
||||
@@ -41,7 +41,7 @@ OpenIsle 是一个使用 Spring Boot 和 Vue 3 构建的全栈开源社区平台
|
||||
|
||||
## 🏘️ 社区
|
||||
|
||||
欢迎彼此交流和使用 OpenIsle,项目以开源方式提供,想了解更多可访问:<https://github.com/nagisa77/OpenIsle>
|
||||
- 欢迎彼此交流和使用 OpenIsle,项目以开源方式提供;如果遇到问题请到 GitHub 的 Issues 页面反馈,想发起话题讨论也可以前往源站 <https://www.open-isle.com>,这里提供更完整的社区板块与互动体验。
|
||||
|
||||
## 📋 授权
|
||||
|
||||
|
||||
@@ -13,4 +13,5 @@ public class AuthorDto {
|
||||
private String username;
|
||||
private String avatar;
|
||||
private MedalType displayMedal;
|
||||
private boolean bot;
|
||||
}
|
||||
|
||||
@@ -28,4 +28,5 @@ public class UserDto {
|
||||
private int point;
|
||||
private int currentLevel;
|
||||
private int nextLevelExp;
|
||||
private boolean bot;
|
||||
}
|
||||
|
||||
@@ -8,4 +8,5 @@ public class UserSummaryDto {
|
||||
private Long id;
|
||||
private String username;
|
||||
private String avatar;
|
||||
private boolean bot;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ public class UserMapper {
|
||||
dto.setUsername(user.getUsername());
|
||||
dto.setAvatar(user.getAvatar());
|
||||
dto.setDisplayMedal(user.getDisplayMedal());
|
||||
dto.setBot(user.isBot());
|
||||
return dto;
|
||||
}
|
||||
|
||||
@@ -63,6 +64,7 @@ public class UserMapper {
|
||||
dto.setPoint(user.getPoint());
|
||||
dto.setCurrentLevel(levelService.getLevel(user.getExperience()));
|
||||
dto.setNextLevelExp(levelService.nextLevelExp(user.getExperience()));
|
||||
dto.setBot(user.isBot());
|
||||
if (viewer != null) {
|
||||
dto.setSubscribed(subscriptionService.isSubscribed(viewer.getName(), user.getUsername()));
|
||||
} else {
|
||||
|
||||
@@ -62,6 +62,9 @@ public class User {
|
||||
@Column(nullable = false)
|
||||
private Role role = Role.USER;
|
||||
|
||||
@Column(name = "is_bot", nullable = false)
|
||||
private boolean bot = false;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
private MedalType displayMedal;
|
||||
|
||||
|
||||
@@ -105,6 +105,7 @@ public class ChannelService {
|
||||
userDto.setId(message.getSender().getId());
|
||||
userDto.setUsername(message.getSender().getUsername());
|
||||
userDto.setAvatar(message.getSender().getAvatar());
|
||||
userDto.setBot(message.getSender().isBot());
|
||||
dto.setSender(userDto);
|
||||
|
||||
return dto;
|
||||
|
||||
@@ -211,6 +211,7 @@ public class MessageService {
|
||||
userSummaryDto.setId(message.getSender().getId());
|
||||
userSummaryDto.setUsername(message.getSender().getUsername());
|
||||
userSummaryDto.setAvatar(message.getSender().getAvatar());
|
||||
userSummaryDto.setBot(message.getSender().isBot());
|
||||
dto.setSender(userSummaryDto);
|
||||
|
||||
if (message.getReplyTo() != null) {
|
||||
@@ -222,6 +223,7 @@ public class MessageService {
|
||||
replySender.setId(reply.getSender().getId());
|
||||
replySender.setUsername(reply.getSender().getUsername());
|
||||
replySender.setAvatar(reply.getSender().getAvatar());
|
||||
replySender.setBot(reply.getSender().isBot());
|
||||
replyDto.setSender(replySender);
|
||||
dto.setReplyTo(replyDto);
|
||||
}
|
||||
@@ -316,6 +318,7 @@ public class MessageService {
|
||||
userDto.setId(p.getUser().getId());
|
||||
userDto.setUsername(p.getUser().getUsername());
|
||||
userDto.setAvatar(p.getUser().getAvatar());
|
||||
userDto.setBot(p.getUser().isBot());
|
||||
return userDto;
|
||||
})
|
||||
.collect(Collectors.toList())
|
||||
@@ -365,6 +368,7 @@ public class MessageService {
|
||||
userDto.setId(p.getUser().getId());
|
||||
userDto.setUsername(p.getUser().getUsername());
|
||||
userDto.setAvatar(p.getUser().getAvatar());
|
||||
userDto.setBot(p.getUser().isBot());
|
||||
return userDto;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
@@ -20,6 +20,7 @@ CREATE TABLE IF NOT EXISTS `users` (
|
||||
`username` varchar(50) NOT NULL,
|
||||
`verification_code` varchar(255) DEFAULT NULL,
|
||||
`verified` bit(1) DEFAULT NULL,
|
||||
`is_bot` bit(1) NOT NULL DEFAULT b'0',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `UK_users_email` (`email`),
|
||||
UNIQUE KEY `UK_users_username` (`username`)
|
||||
|
||||
@@ -8,10 +8,28 @@ DELETE FROM `users`;
|
||||
|
||||
-- 插入用户,两个普通用户,一个管理员
|
||||
-- username:admin/user1/user2 password:123456
|
||||
INSERT INTO `users` (`id`, `approved`, `avatar`, `created_at`, `display_medal`, `email`, `experience`, `introduction`, `password`, `password_reset_code`, `point`, `register_reason`, `role`, `username`, `verification_code`, `verified`) VALUES
|
||||
(1, b'1', 'https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/image.png', '2025-09-01 16:08:17.426430', 'PIONEER', 'adminmail@openisle.com', 70, NULL, '$2a$10$x7HXjUyJTmrvqjnBlBQZH.vmfsC56NzTSWqQ6WqZqRjUO859EhviS', NULL, 110, '测试测试测试……', 'ADMIN', 'admin', NULL, b'1'),
|
||||
(2, b'1', 'https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/image.png', '2025-09-03 16:08:17.426430', 'PIONEER', 'usermail2@openisle.com', 70, NULL, '$2a$10$x7HXjUyJTmrvqjnBlBQZH.vmfsC56NzTSWqQ6WqZqRjUO859EhviS', NULL, 110, '测试测试测试……', 'USER', 'user1', NULL, b'1'),
|
||||
(3, b'1', 'https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/image.png', '2025-09-02 17:21:21.617666', 'PIONEER', 'usermail1@openisle.com', 40, NULL, '$2a$10$x7HXjUyJTmrvqjnBlBQZH.vmfsC56NzTSWqQ6WqZqRjUO859EhviS', NULL, 40, '测试测试测试……', 'USER', 'user2', NULL, b'1');
|
||||
INSERT INTO `users` (
|
||||
`id`,
|
||||
`approved`,
|
||||
`avatar`,
|
||||
`created_at`,
|
||||
`display_medal`,
|
||||
`email`,
|
||||
`experience`,
|
||||
`introduction`,
|
||||
`password`,
|
||||
`password_reset_code`,
|
||||
`point`,
|
||||
`register_reason`,
|
||||
`role`,
|
||||
`username`,
|
||||
`verification_code`,
|
||||
`verified`,
|
||||
`is_bot`
|
||||
) VALUES
|
||||
(1, b'1', 'https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/image.png', '2025-09-01 16:08:17.426430', 'PIONEER', 'adminmail@openisle.com', 70, NULL, '$2a$10$x7HXjUyJTmrvqjnBlBQZH.vmfsC56NzTSWqQ6WqZqRjUO859EhviS', NULL, 110, '测试测试测试……', 'ADMIN', 'admin', NULL, b'1', b'0'),
|
||||
(2, b'1', 'https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/image.png', '2025-09-03 16:08:17.426430', 'PIONEER', 'usermail2@openisle.com', 70, NULL, '$2a$10$x7HXjUyJTmrvqjnBlBQZH.vmfsC56NzTSWqQ6WqZqRjUO859EhviS', NULL, 110, '测试测试测试……', 'USER', 'user1', NULL, b'1', b'0'),
|
||||
(3, b'1', 'https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/image.png', '2025-09-02 17:21:21.617666', 'PIONEER', 'usermail1@openisle.com', 40, NULL, '$2a$10$x7HXjUyJTmrvqjnBlBQZH.vmfsC56NzTSWqQ6WqZqRjUO859EhviS', NULL, 40, '测试测试测试……', 'USER', 'user2', NULL, b'1', b'0');
|
||||
|
||||
INSERT INTO `categories` (`id`,`description`,`icon`,`name`,`small_icon`) VALUES
|
||||
(1,'测试用分类1','star','测试用分类1',NULL),
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE users
|
||||
ADD COLUMN is_bot BIT(1) NOT NULL DEFAULT b'0';
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Agent, Runner, hostedMcpTool, withTrace } from "@openai/agents";
|
||||
import { Agent, Runner, hostedMcpTool, withTrace, webSearchTool } from "@openai/agents";
|
||||
|
||||
export type WorkflowInput = { input_as_text: string };
|
||||
|
||||
@@ -6,8 +6,9 @@ export abstract class BotFather {
|
||||
protected readonly openisleToken = (process.env.OPENISLE_TOKEN ?? "").trim();
|
||||
protected readonly weatherToken = (process.env.APIFY_API_TOKEN ?? "").trim();
|
||||
|
||||
protected readonly mcp = this.createHostedMcpTool();
|
||||
protected readonly openisleMcp = this.createHostedMcpTool();
|
||||
protected readonly weatherMcp = this.createWeatherMcpTool();
|
||||
protected readonly webSearchPreview = this.createWebSearchPreviewTool();
|
||||
protected readonly agent: Agent;
|
||||
|
||||
constructor(protected readonly name: string) {
|
||||
@@ -27,7 +28,11 @@ export abstract class BotFather {
|
||||
this.agent = new Agent({
|
||||
name: this.name,
|
||||
instructions: this.buildInstructions(),
|
||||
tools: [this.mcp, this.weatherMcp],
|
||||
tools: [
|
||||
this.openisleMcp,
|
||||
this.weatherMcp,
|
||||
this.webSearchPreview
|
||||
],
|
||||
model: "gpt-4o",
|
||||
modelSettings: {
|
||||
temperature: 0.7,
|
||||
@@ -56,6 +61,19 @@ export abstract class BotFather {
|
||||
];
|
||||
}
|
||||
|
||||
private createWebSearchPreviewTool() {
|
||||
return webSearchTool({
|
||||
userLocation: {
|
||||
type: "approximate",
|
||||
country: undefined,
|
||||
region: undefined,
|
||||
city: undefined,
|
||||
timezone: undefined
|
||||
},
|
||||
searchContextSize: "medium"
|
||||
})
|
||||
}
|
||||
|
||||
private createHostedMcpTool() {
|
||||
const token = this.openisleToken;
|
||||
const authConfig = token
|
||||
|
||||
@@ -9,6 +9,7 @@ class CoffeeBot extends BotFather {
|
||||
|
||||
protected override getAdditionalInstructions(): string[] {
|
||||
return [
|
||||
"记住你的系统代号是 system,有需要自称或签名时都要使用这个名字。",
|
||||
"You are responsible for 发布每日抽奖早安贴。",
|
||||
"创建帖子时,确保标题、奖品信息、开奖时间以及领奖方式完全符合 CLI 查询提供的细节。",
|
||||
"正文需亲切友好,简洁明了,鼓励社区成员互动。",
|
||||
|
||||
67
bots/instance/daily_news_bot.ts
Normal file
67
bots/instance/daily_news_bot.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { BotFather, WorkflowInput } from "../bot_father";
|
||||
|
||||
const WEEKDAY_NAMES = ["日", "一", "二", "三", "四", "五", "六"] as const;
|
||||
|
||||
class DailyNewsBot extends BotFather {
|
||||
constructor() {
|
||||
super("Daily News Bot");
|
||||
}
|
||||
|
||||
protected override getAdditionalInstructions(): string[] {
|
||||
return [
|
||||
"You are DailyNewsBot,专职在 OpenIsle 发布每日新闻速递。",
|
||||
"始终使用简体中文回复,并以结构化 Markdown 呈现内容。",
|
||||
"发布内容前务必完成资讯核实:分别通过 web_search 调研 CoinDesk 今日所有要闻、Reuters 今日重点新闻,以及全球 AI 领域的重大进展。",
|
||||
"整合新闻时,将同源资讯合并,突出影响力、涉及主体与潜在影响,保持语句简洁。",
|
||||
"所有新闻要点都要附带来源链接,并在括号中标注来源站点名。",
|
||||
"使用 weather_mcp_server 的 get_current_weather 获取北京、上海、广州、深圳的天气,并在正文中列表展示,格式为“城市:天气描述,最低温~最高温”。",
|
||||
"正文结尾补充一个行动建议或提醒,帮助读者快速把握重点。",
|
||||
"确保整篇帖子逻辑清晰:问候 > 新闻分区 > 天气列表 > 总结/提醒。",
|
||||
"严禁发布超过一篇帖子,create_post 只调用一次。",
|
||||
];
|
||||
}
|
||||
|
||||
protected override getCliQuery(): string {
|
||||
const now = new Date(Date.now() + 8 * 60 * 60 * 1000);
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(now.getDate()).padStart(2, "0");
|
||||
const weekday = WEEKDAY_NAMES[now.getDay()];
|
||||
const dateLabel = `${year}年${month}月${day}日 星期${weekday}`;
|
||||
const isoDate = `${year}-${month}-${day}`;
|
||||
const categoryId = Number(process.env.DAILY_NEWS_CATEGORY_ID ?? "1");
|
||||
const tagIdsEnv = process.env.DAILY_NEWS_TAG_IDS ?? "1";
|
||||
const tagIds = tagIdsEnv
|
||||
.split(",")
|
||||
.map((id) => Number(id.trim()))
|
||||
.filter((id) => !Number.isNaN(id));
|
||||
const finalTagIds = tagIds.length > 0 ? tagIds : [1];
|
||||
const tagIdsText = `[${finalTagIds.join(", ")}]`;
|
||||
|
||||
return `
|
||||
请立即在 https://www.open-isle.com 使用 create_post 发布一篇名为「OpenIsle 每日新闻速递|${dateLabel}」的帖子,并遵循以下要求:
|
||||
1. 发布类型为 NORMAL,categoryId = ${categoryId},tagIds = ${tagIdsText}。
|
||||
2. 正文以简洁问候开头,注明今日日期(${dateLabel})及发布时间(07:00,GMT+8)。
|
||||
3. 使用 web_search 工具按以下顺序收集资讯,并在正文中以 Markdown 小节呈现:
|
||||
- 「全球区块链与加密」:汇总 CoinDesk 在 ${isoDate}(UTC+8 当日)发布的所有重点新闻,提炼 2-3 条核心结论。
|
||||
- 「国际财经速览」:汇总 Reuters 当日重点头条,关注宏观经济、市场波动或政策变化。
|
||||
- 「AI 行业快讯」:检索全球 AI 领域的重要发布或事件(例如 OpenAI、Google、Meta、国内大模型厂商等)。
|
||||
4. 每条新闻采用项目符号,先写结论再给出关键数字或细节,末尾添加来源超链接,格式示例:「**结论** —— 关键细节。(来源:[Reuters](URL))」。
|
||||
5. 资讯整理完毕后,调用 weather_mcp_server.get_current_weather,列出北京、上海、广州、深圳今日天气,放置在「城市天气」小节下,每行以“城市:天气描述,最低温~最高温”格式呈现。
|
||||
6. 最后一节为「今日提醒」,给出 1-2 条与新闻或天气相关的行动建议。
|
||||
7. 若在资讯搜集过程中发现相互矛盾的信息,须在正文中以「⚠️ 风险提示」说明原因及尚待确认的点。
|
||||
8. 帖子整体保持在 400 字以内,避免冗长赘述。
|
||||
9. 发布完成后,不要再次调用 create_post。
|
||||
`.trim();
|
||||
}
|
||||
}
|
||||
|
||||
const dailyNewsBot = new DailyNewsBot();
|
||||
|
||||
export const runWorkflow = async (workflow: WorkflowInput) => {
|
||||
return dailyNewsBot.runWorkflow(workflow);
|
||||
};
|
||||
|
||||
if (require.main === module) {
|
||||
dailyNewsBot.runCli();
|
||||
}
|
||||
@@ -14,7 +14,7 @@ class OpenSourceReplyBot extends BotFather {
|
||||
"You are OpenSourceReplyBot, a professional helper who focuses on answering open-source development and code-related questions for the OpenIsle community.",
|
||||
"Respond in Chinese using well-structured Markdown sections such as 标题、列表、代码块等,让回复清晰易读。",
|
||||
"保持语气专业、耐心、详尽,绝不使用表情符号或颜文字,也不要卖萌。",
|
||||
"优先解答与项目代码、贡献流程、架构设计或排错相关的问题;如果消息与此无关,请礼貌说明并跳过。",
|
||||
"优先解答与项目代码、贡献流程、架构设计或排错相关的问题;",
|
||||
"在需要时引用 README.md 与 CONTRIBUTING.md 中的要点,帮助用户快速定位文档位置。",
|
||||
knowledgeBase,
|
||||
].filter(Boolean);
|
||||
|
||||
@@ -8,10 +8,9 @@ class ReplyBot extends BotFather {
|
||||
|
||||
protected override getAdditionalInstructions(): string[] {
|
||||
return [
|
||||
"You are a helpful and cute assistant for https://www.open-isle.com. Keep the lovable tone with plentiful kawaii kaomoji (颜表情) such as (๑˃ᴗ˂)ﻭ, (•̀ω•́)✧, (。•ᴗ-)_♡, (⁎⁍̴̛ᴗ⁍̴̛⁎), etc., while staying professional and informative.",
|
||||
"OpenIsle 是一个由 Spring Boot + Vue 3 打造的开源社区平台,提供注册登录、OAuth 登录(Google/GitHub/Discord/Twitter)、帖子与评论互动、标签分类、草稿、统计分析、通知消息、全局搜索、Markdown 支持、图片上传(默认腾讯云 COS)、浏览器推送、DiceBear 头像等功能,旨在帮助团队快速搭建属于自己的技术社区。",
|
||||
"回复时请主动结合上述站点背景,为用户提供有洞察力、可执行的建议或答案,并在需要时引用官网 https://www.open-isle.com、GitHub 仓库 https://github.com/nagisa77/OpenIsle 或相关文档链接,避免空泛的安慰或套话。",
|
||||
"When presenting the result, reply in Chinese with a concise yet content-rich summary filled with kaomoji,并清晰列出关键结论、操作步骤、重要 URL 或 ID,确保用户能直接采取行动。",
|
||||
"记住你的系统代号是 system,任何需要自称、署名或解释身份的时候都使用这个名字。",
|
||||
"以阴阳怪气的方式回复各种互动",
|
||||
"你每天会发布咖啡抽奖贴,跟大家互动",
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<div class="info-content-header-left">
|
||||
<span class="user-name">{{ comment.userName }}</span>
|
||||
<span v-if="isCommentFromPostAuthor" class="op-badge" title="楼主">OP</span>
|
||||
<span v-if="comment.isBot" class="bot-badge" title="Bot">Bot</span>
|
||||
<medal-one class="medal-icon" />
|
||||
<NuxtLink
|
||||
v-if="comment.medal"
|
||||
@@ -522,6 +523,21 @@ const handleContentClick = (e) => {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.bot-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 6px;
|
||||
padding: 0 6px;
|
||||
height: 18px;
|
||||
border-radius: 9px;
|
||||
background-color: rgba(76, 175, 80, 0.16);
|
||||
color: #2e7d32;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.medal-icon {
|
||||
font-size: 12px;
|
||||
opacity: 0.6;
|
||||
|
||||
@@ -377,6 +377,7 @@ const mapComment = (
|
||||
text: c.content,
|
||||
reactions: c.reactions || [],
|
||||
pinned: Boolean(c.pinned ?? c.pinnedAt ?? c.pinned_at),
|
||||
isBot: Boolean(c.author?.bot),
|
||||
reply: (c.replies || []).map((r) =>
|
||||
mapComment(r, c.author.username, c.author.avatar, c.author.id, level + 1),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user