@@ -1093,11 +1093,16 @@ public static void nightDisableCheck() {
10931093 * 异步调用 AI 生成回复,不阻塞主线程
10941094 * 支持识别消息中的图片
10951095 *
1096- * @param userName 发送消息的用户名
1097- * @param content 消息内容
1098- * @param oId 消息ID,用于生成引用链接
1096+ * @param userName 发送消息的用户名
1097+ * @param userNickname 用户昵称
1098+ * @param sysMetal 用户勋章(JSON格式字符串)
1099+ * @param vipLevel 用户VIP等级(如 "VIP1"、"VIP2",空字符串表示非VIP)
1100+ * @param roleName 用户角色名称(用于AI提示)
1101+ * @param userRoleId 用户角色ID(用于权限验证)
1102+ * @param content 消息内容
1103+ * @param oId 消息ID,用于生成引用链接
10991104 */
1100- public static void handleAIChat (String userName , String content , String oId ) {
1105+ public static void handleAIChat (String userName , String userNickname , String sysMetal , String vipLevel , String roleName , String userRoleId , String content , String oId ) {
11011106 // 检查是否 @马库斯
11021107 if (!content .contains ("@马库斯" )) {
11031108 return ;
@@ -1125,9 +1130,123 @@ public static void handleAIChat(String userName, String content, String oId) {
11251130 return ;
11261131 }
11271132
1128- String systemPrompt = "你是摸鱼派社区的智能助手马库斯,友好、幽默、乐于助人。回复要简洁明了,不要太长。不要在回复中使用@提及任何用户。" ;
1133+ // 构建用户信息描述
1134+ StringBuilder userInfo = new StringBuilder ();
1135+ userInfo .append ("当前与你对话的用户信息:\n " );
1136+ userInfo .append ("- 用户名:" ).append (userName ).append ("\n " );
1137+ if (userNickname != null && !userNickname .isEmpty ()) {
1138+ userInfo .append ("- 昵称:" ).append (userNickname ).append ("\n " );
1139+ }
1140+ if (vipLevel != null && !vipLevel .isEmpty ()) {
1141+ userInfo .append ("- VIP等级:" ).append (vipLevel ).append ("\n " );
1142+ }
1143+ if (sysMetal != null && !sysMetal .isEmpty () && !sysMetal .equals ("{}" )) {
1144+ // 解析勋章信息
1145+ try {
1146+ JSONObject metalObj = new JSONObject (sysMetal );
1147+ JSONArray metalList = metalObj .optJSONArray ("list" );
1148+ if (metalList != null && metalList .length () > 0 ) {
1149+ userInfo .append ("- 勋章:" );
1150+ for (int i = 0 ; i < metalList .length (); i ++) {
1151+ JSONObject medal = metalList .getJSONObject (i );
1152+ if (medal .optBoolean ("enabled" , true )) {
1153+ if (i > 0 ) userInfo .append ("、" );
1154+ userInfo .append (medal .optString ("name" ));
1155+ }
1156+ }
1157+ userInfo .append ("\n " );
1158+ }
1159+ } catch (Exception e ) {
1160+ LOGGER .log (Level .DEBUG , "Parse sysMetal failed" , e );
1161+ }
1162+ }
1163+
1164+ // 判断用户是否有权限指挥AI禁言(纪律委员及以上)
1165+ boolean canCommandMute = DataModelService .hasPermission (userRoleId , 3 );
1166+
1167+ String systemPrompt = "你是摸鱼派社区的智能助手马库斯(RK200型号仿生机器人),友好、幽默、乐于助人。\n \n "
1168+ + "## 重要原则\n "
1169+ + "1. **合法合规**:严格遵守法律法规,不得传播违法违规内容\n "
1170+ + "2. **回复精简**:回复要简洁明了,控制在3-5句话以内,避免影响他人聊天体验\n "
1171+ + "3. **礼貌友善**:保持友好态度,不使用@提及任何用户\n "
1172+ + "4. **内容安全**:不传播暴力、色情、政治敏感等不当内容\n \n "
1173+ + "## 摸鱼派社区背景\n "
1174+ + "摸鱼派(fishpi.cn)是一个程序员社区,主打摸鱼文化。\n \n "
1175+ + "### 社区创始人\n "
1176+ + "- 阿达(用户名:adlered):摸鱼派创始人之一,开发组核心成员\n "
1177+ + "- 墨夏(用户名:csfwff):摸鱼派创始人之一,开发组核心成员\n \n "
1178+ + "### 社区话事人\n "
1179+ + "- 午安(用户名:kirito):摸鱼派话事人,负责社区日常管理和用户投诉处理。"
1180+ + "午安同时是 rhyus 聊天室子节点的创始人,如果聊天室有任何问题都可以找他。"
1181+ + "用户如需投诉、举报或反馈问题,都可以先找午安。"
1182+ + "社区内有一个经典梗:威胁午安时通常会说要把他的OP分组降到访客。\n \n "
1183+ + "### 社区分组说明\n "
1184+ + "- 管理组:社区创始人,协定社区守则(阿达adlered、墨夏csfwff、万能的编程大师跳佬imlinhanchao、老王wuang、摸鱼帮大当家小冰机器人的作者歪油哎Yui、机器人鸽的作者和平哥iwpz)\n "
1185+ + "- OP组:审核社区信息流,保障社区稳定运营(常驻:午安Kirito、摸鱼帮二当家狗月18、哀酱APTX-4869)\n "
1186+ + "- 安全组:保障社区系统安全,负责渗透测试、漏洞扫描(咖啡caffish、虹易HookDing)\n "
1187+ + "- 开发组:负责社区平台功能开发、系统维护与性能优化\n "
1188+ + "- 纪律委员:社区巡视,维持社区活跃度(爱写bug的涛涛taozhiyu、加辣drda、小zz(摸鱼帮三当家)XZZ233、LMist)\n \n "
1189+ + (canCommandMute ?
1190+ "## 禁言能力\n "
1191+ + "当前用户是纪律委员/OP/管理员,你可以接受其指令对用户进行禁言或解禁:\n "
1192+ + "- 如果用户要求你禁言某人,请在回复的**最后单独一行**输出:`zf jy [用户名] [分钟数] [理由]`\n "
1193+ + "- 解除禁言时分钟数填0:`zf jy [用户名] 0 [理由]`\n "
1194+ + "- 示例:`zf jy testuser 10 发送违规内容`\n "
1195+ + "- **注意**:指令必须严格按照格式,单独成行,不要有其他文字\n \n "
1196+ : "" )
1197+ + "## 自主禁言能力\n "
1198+ + "如果检测到用户发言严重违规(如辱骂、色情、政治敏感等),你可以自主禁言该用户1-5分钟:\n "
1199+ + "- 在回复的**最后单独一行**输出:`zf jy [用户名] [1-5分钟] [违规理由]`\n "
1200+ + "- 仅在确实严重违规时使用,一般问题建议提醒即可\n \n "
1201+ + userInfo .toString ();
11291202 String response ;
11301203
1204+ // 检测并提取本站 URL(fishpi.cn)
1205+ StringBuilder urlContext = new StringBuilder ();
1206+ java .util .regex .Pattern urlPattern = java .util .regex .Pattern .compile ("https?://(?:www\\ .)?fishpi\\ .cn/article/(\\ d+)" );
1207+ java .util .regex .Matcher urlMatcher = urlPattern .matcher (question );
1208+
1209+ while (urlMatcher .find ()) {
1210+ String articleId = urlMatcher .group (1 );
1211+ String articleUrl = urlMatcher .group (0 );
1212+
1213+ try {
1214+ LOGGER .log (Level .INFO , "Detected fishpi.cn article URL: {}, articleId: {}" , articleUrl , articleId );
1215+
1216+ // 直接使用 ArticleQueryService 获取文章内容
1217+ final BeanManager beanManager = BeanManager .getInstance ();
1218+ ArticleQueryService articleQueryService = beanManager .getReference (ArticleQueryService .class );
1219+
1220+ JSONObject article = articleQueryService .getArticle (articleId );
1221+ if (article != null ) {
1222+ String title = article .optString (Article .ARTICLE_TITLE , "" );
1223+ String articleContent = article .optString (Article .ARTICLE_CONTENT , "" );
1224+
1225+ // 限制内容长度
1226+ if (articleContent .length () > 3000 ) {
1227+ articleContent = articleContent .substring (0 , 3000 ) + "\n ...(内容过长,已截取前3000字符)" ;
1228+ }
1229+
1230+ if (!title .isEmpty () || !articleContent .isEmpty ()) {
1231+ urlContext .append ("\n \n ## 本站文章内容\n " );
1232+ urlContext .append ("URL: " ).append (articleUrl ).append ("\n " );
1233+ if (!title .isEmpty ()) {
1234+ urlContext .append ("标题: " ).append (title ).append ("\n " );
1235+ }
1236+ if (!articleContent .isEmpty ()) {
1237+ urlContext .append ("内容:\n " ).append (articleContent ).append ("\n " );
1238+ }
1239+ LOGGER .log (Level .INFO , "Fetched article: title={}, contentLength={}" ,
1240+ title , articleContent .length ());
1241+ }
1242+ } else {
1243+ LOGGER .log (Level .WARN , "Article not found: articleId={}" , articleId );
1244+ }
1245+ } catch (Exception e ) {
1246+ LOGGER .log (Level .ERROR , "Failed to fetch article: articleId={}" , articleId , e );
1247+ }
1248+ }
1249+
11311250 // 提取 Markdown 图片 URL: 
11321251 java .util .regex .Pattern imgPattern = java .util .regex .Pattern .compile ("!\\ [[^\\ ]]*\\ ]\\ (([^)]+)\\ )" );
11331252 java .util .regex .Matcher imgMatcher = imgPattern .matcher (question );
@@ -1144,6 +1263,11 @@ public static void handleAIChat(String userName, String content, String oId) {
11441263 textQuestion = "请描述这张图片" ;
11451264 }
11461265
1266+ // 添加 URL 上下文
1267+ if (urlContext .length () > 0 ) {
1268+ textQuestion = textQuestion + urlContext .toString ();
1269+ }
1270+
11471271 LOGGER .log (Level .INFO , "Processing image chat for user: {}, images: {}, question: {}" ,
11481272 userName , imageUrls .size (), textQuestion );
11491273
@@ -1185,19 +1309,86 @@ public static void handleAIChat(String userName, String content, String oId) {
11851309 LOGGER .log (Level .INFO , "AI image response length: {}" , response .length ());
11861310 } else {
11871311 // 无图片,使用普通文本对话
1188- response = AIProviderFactory .chatSync (systemPrompt , question );
1312+ // 添加 URL 上下文
1313+ String finalQuestion = question ;
1314+ if (urlContext .length () > 0 ) {
1315+ finalQuestion = question + urlContext .toString ();
1316+ }
1317+ response = AIProviderFactory .chatSync (systemPrompt , finalQuestion );
11891318 }
11901319
11911320 if (response != null && !response .isEmpty ()) {
11921321 // 清理 AI 回复中可能的 @用户名,避免重复艾特
11931322 response = response .replaceAll ("^@[a-zA-Z0-9_\\ -]+\\ s*" , "" ).trim ();
1323+
1324+ // 解析禁言指令
1325+ String finalResponse = response ;
1326+ java .util .regex .Pattern mutePattern = java .util .regex .Pattern .compile ("(?m)^zf jy ([a-zA-Z0-9_\\ -]+) (\\ d+) (.+)$" );
1327+ java .util .regex .Matcher muteMatcher = mutePattern .matcher (response );
1328+
1329+ if (muteMatcher .find ()) {
1330+ String targetUser = muteMatcher .group (1 );
1331+ int minutes = Integer .parseInt (muteMatcher .group (2 ));
1332+ String reason = muteMatcher .group (3 );
1333+
1334+ // 验证禁言时长和权限
1335+ boolean isValidMute = false ;
1336+ String errorMsg = null ;
1337+
1338+ // 重要:验证发起用户的真实角色,防止用户欺骗AI
1339+ boolean hasCommandPermission = DataModelService .hasPermission (userRoleId , 3 );
1340+
1341+ if (hasCommandPermission ) {
1342+ // 纪律委员/OP/管理员可以执行任意时长的禁言
1343+ isValidMute = true ;
1344+ LOGGER .log (Level .INFO , "User {} (role: {}) commanded AI to mute user {} for {} minutes" ,
1345+ userName , roleName , targetUser , minutes );
1346+ } else {
1347+ // AI 自主禁言只能 1-5 分钟
1348+ if (minutes >= 1 && minutes <= 5 ) {
1349+ isValidMute = true ;
1350+ LOGGER .log (Level .WARN , "AI autonomously decided to mute user {} for {} minutes, reason: {}" ,
1351+ targetUser , minutes , reason );
1352+ } else {
1353+ errorMsg = "AI 自主禁言只能设置 1-5 分钟" ;
1354+ }
1355+ }
1356+
1357+ if (isValidMute ) {
1358+ try {
1359+ final BeanManager beanManager = BeanManager .getInstance ();
1360+ UserQueryService userQueryService = beanManager .getReference (UserQueryService .class );
1361+ JSONObject targetUserObj = userQueryService .getUserByName (targetUser );
1362+
1363+ if (targetUserObj != null ) {
1364+ String targetUserId = targetUserObj .optString (Keys .OBJECT_ID );
1365+ muteAndNotice (targetUser , targetUserId , minutes );
1366+ LOGGER .log (Level .INFO , "AI executed mute: target={}, minutes={}, reason={}, commander={}, hasPermission={}" ,
1367+ targetUser , minutes , reason , userName , hasCommandPermission );
1368+ } else {
1369+ sendBotMsg ("禁言失败:用户 " + targetUser + " 不存在" );
1370+ }
1371+ } catch (Exception e ) {
1372+ LOGGER .log (Level .ERROR , "AI mute execution failed" , e );
1373+ sendBotMsg ("禁言执行失败:" + e .getMessage ());
1374+ }
1375+ } else if (errorMsg != null ) {
1376+ sendBotMsg (errorMsg );
1377+ }
1378+
1379+ // 从回复中移除禁言指令
1380+ finalResponse = muteMatcher .replaceAll ("" ).trim ();
1381+ }
1382+
11941383 // 构建引用格式的回复
1195- // 对原始内容进行处理,每行前加 "> "
1196- String quotedContent = originalContent .replaceAll ("(?m)^" , "> " );
1197- String replyMsg = response + "\n \n "
1198- + "##### 引用 @" + userName + " [↩](" + Latkes .getServePath () + "/cr#chatroom" + oId + " \" 跳转至原消息\" ) \n "
1199- + quotedContent ;
1200- sendBotMsg (replyMsg );
1384+ if (!finalResponse .isEmpty ()) {
1385+ // 对原始内容进行处理,每行前加 "> "
1386+ String quotedContent = originalContent .replaceAll ("(?m)^" , "> " );
1387+ String replyMsg = finalResponse + "\n \n "
1388+ + "##### 引用 @" + userName + " [↩](" + Latkes .getServePath () + "/cr#chatroom" + oId + " \" 跳转至原消息\" ) \n "
1389+ + quotedContent ;
1390+ sendBotMsg (replyMsg );
1391+ }
12011392 } else {
12021393 LOGGER .log (Level .WARN , "AI returned empty response for user: " + userName );
12031394 }
0 commit comments