koishi-plugin-share-links-analysis 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.js +146 -7
- package/lib/types.d.ts +1 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -30,6 +30,7 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
30
30
|
koishi_1.Schema.const(false).description('不返回文字提示'),
|
|
31
31
|
koishi_1.Schema.string().description('返回文字提示'),
|
|
32
32
|
]).description("是否返回等待提示。开启后,会发送`等待提示语`").default(false),
|
|
33
|
+
useForward: koishi_1.Schema.boolean().default(false).description("使用合并转发(强依赖Napcat,其他环境未测试)"),
|
|
33
34
|
}).description("基础设置"),
|
|
34
35
|
koishi_1.Schema.object({
|
|
35
36
|
format: koishi_1.Schema.string().role('textarea').default(`{title}
|
|
@@ -114,28 +115,45 @@ function apply(ctx, config) {
|
|
|
114
115
|
}
|
|
115
116
|
});
|
|
116
117
|
}
|
|
118
|
+
function escapeHtml(str) {
|
|
119
|
+
if (!str)
|
|
120
|
+
return '';
|
|
121
|
+
return str.replace(/&/g, '&')
|
|
122
|
+
.replace(/</g, '<')
|
|
123
|
+
.replace(/>/g, '>')
|
|
124
|
+
.replace(/"/g, '"')
|
|
125
|
+
.replace(/'/g, ''');
|
|
126
|
+
}
|
|
117
127
|
async function sendResult(session, config, result, logger) {
|
|
128
|
+
if (config.useForward) {
|
|
129
|
+
return sendResult_forward(session, config, result, logger);
|
|
130
|
+
}
|
|
131
|
+
else
|
|
132
|
+
return sendResult_plain(session, config, result, logger);
|
|
133
|
+
}
|
|
134
|
+
async function sendResult_plain(session, config, result, logger) {
|
|
118
135
|
let message = config.format;
|
|
119
|
-
|
|
120
|
-
message = message.replace(/{
|
|
121
|
-
message = message.replace(/{
|
|
122
|
-
message = message.replace(/{
|
|
136
|
+
// 对所有文本内容进行 HTML 转义
|
|
137
|
+
message = message.replace(/{title}/g, escapeHtml(result.title || ''));
|
|
138
|
+
message = message.replace(/{authorName}/g, escapeHtml(result.authorName || ''));
|
|
139
|
+
message = message.replace(/{description}/g, escapeHtml(result.description ? result.description : ''));
|
|
140
|
+
message = message.replace(/{sourceUrl}/g, escapeHtml(result.sourceUrl || ''));
|
|
123
141
|
message = message.replace(/{cover}/g, result.coverUrl ? koishi_1.h.image(result.coverUrl).toString() : '');
|
|
124
142
|
const imagesText = result.images ? result.images.map(img => koishi_1.h.image(img).toString()).join('\n') : '';
|
|
125
143
|
message = message.replace(/{images}/g, imagesText);
|
|
126
|
-
message = message.replace(/{stats}/g, result.stats || '');
|
|
144
|
+
message = message.replace(/{stats}/g, escapeHtml(result.stats || ''));
|
|
127
145
|
// 【修复】只要 videoUrl 存在就处理,仅当 duration 明确超长时才替换为提示
|
|
128
146
|
if (result.videoUrl) {
|
|
129
147
|
// 仅当 duration 是有效数字且超长时,才显示提示
|
|
130
148
|
if (typeof result.duration === 'number' && result.duration > config.Maximumduration * 60) {
|
|
131
|
-
const tip = config.Maximumduration_tip || '';
|
|
149
|
+
const tip = escapeHtml(config.Maximumduration_tip || '');
|
|
132
150
|
message = message.replace(/{video}/g, tip);
|
|
133
151
|
message = message.replace(/{videoUrl}/g, '');
|
|
134
152
|
}
|
|
135
153
|
else {
|
|
136
154
|
// 正常发送视频和链接
|
|
137
155
|
message = message.replace(/{video}/g, koishi_1.h.video(result.videoUrl).toString());
|
|
138
|
-
message = message.replace(/{videoUrl}/g, result.videoUrl);
|
|
156
|
+
message = message.replace(/{videoUrl}/g, escapeHtml(result.videoUrl));
|
|
139
157
|
if (config.logLevel === 'link_only' || config.logLevel === 'full') {
|
|
140
158
|
logger.info(`视频直链 (${result.platform}): ${result.videoUrl}`);
|
|
141
159
|
}
|
|
@@ -146,6 +164,7 @@ async function sendResult(session, config, result, logger) {
|
|
|
146
164
|
message = message.replace(/{video}/g, '');
|
|
147
165
|
message = message.replace(/{videoUrl}/g, '');
|
|
148
166
|
}
|
|
167
|
+
// 过滤空行,保留含有 < 的行(如图片、视频标签)
|
|
149
168
|
const cleanMessage = message.split('\n').filter(line => line.trim() !== '' || line.includes('<')).join('\n');
|
|
150
169
|
if (cleanMessage) {
|
|
151
170
|
await session.send(koishi_1.h.quote(session.messageId) + cleanMessage);
|
|
@@ -154,3 +173,123 @@ async function sendResult(session, config, result, logger) {
|
|
|
154
173
|
logger.info(`解析结果: \n ${JSON.stringify(result, null, 2)}`);
|
|
155
174
|
}
|
|
156
175
|
}
|
|
176
|
+
async function sendResult_forward(session, config, result, logger) {
|
|
177
|
+
let message = config.format;
|
|
178
|
+
// Step 1: 替换纯文本字段
|
|
179
|
+
message = message.replace(/{title}/g, escapeHtml(result.title || ''));
|
|
180
|
+
message = message.replace(/{authorName}/g, escapeHtml(result.authorName || ''));
|
|
181
|
+
message = message.replace(/{description}/g, escapeHtml(result.description || ''));
|
|
182
|
+
message = message.replace(/{sourceUrl}/g, escapeHtml(result.sourceUrl || ''));
|
|
183
|
+
message = message.replace(/{stats}/g, escapeHtml(result.stats || ''));
|
|
184
|
+
// Step 2: 检查是否包含视频占位符
|
|
185
|
+
const hasVideoInTemplate = message.includes('{video}');
|
|
186
|
+
// Step 3: 构建富媒体映射
|
|
187
|
+
const mediaMap = {};
|
|
188
|
+
// 处理封面
|
|
189
|
+
if (result.coverUrl) {
|
|
190
|
+
mediaMap['{cover}'] = [{ type: 'image', data: { file: result.coverUrl } }];
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
mediaMap['{cover}'] = [];
|
|
194
|
+
}
|
|
195
|
+
// 处理图片列表
|
|
196
|
+
if (result.images && result.images.length > 0) {
|
|
197
|
+
mediaMap['{images}'] = result.images.map(img => ({ type: 'image', data: { file: img } }));
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
mediaMap['{images}'] = [];
|
|
201
|
+
}
|
|
202
|
+
// Step 4: 按行处理,仅过滤纯空行,并精确控制换行
|
|
203
|
+
const lines = message.split('\n').filter(line => line.trim() !== '');
|
|
204
|
+
const nonVideoSegments = [];
|
|
205
|
+
for (let i = 0; i < lines.length; i++) {
|
|
206
|
+
const line = lines[i];
|
|
207
|
+
const isLastLine = i === lines.length - 1;
|
|
208
|
+
// 按富媒体占位符分割
|
|
209
|
+
const tokens = line.split(/(\{cover\}|\{images\}|\{video\})/g);
|
|
210
|
+
// 用于存储当前行的消息段
|
|
211
|
+
const currentLineSegments = [];
|
|
212
|
+
let hasTextContent = false; // 新增标志:当前行是否包含纯文本
|
|
213
|
+
for (const token of tokens) {
|
|
214
|
+
if (token === '{cover}' || token === '{images}') {
|
|
215
|
+
// 插入对应的消息段
|
|
216
|
+
currentLineSegments.push(...mediaMap[token]);
|
|
217
|
+
}
|
|
218
|
+
else if (token === '{video}') {
|
|
219
|
+
// 视频不放入 nonVideoSegments,跳过
|
|
220
|
+
}
|
|
221
|
+
else if (token.trim() !== '') {
|
|
222
|
+
// 普通文本
|
|
223
|
+
currentLineSegments.push({ type: 'text', data: { text: token } });
|
|
224
|
+
hasTextContent = true; // 标记当前行有文本
|
|
225
|
+
}
|
|
226
|
+
// 注意:token 为空字符串时(如占位符在行首/尾),不添加任何内容
|
|
227
|
+
}
|
|
228
|
+
// 只有当 currentLineSegments 不为空时,才将其加入总列表
|
|
229
|
+
if (currentLineSegments.length > 0) {
|
|
230
|
+
nonVideoSegments.push(...currentLineSegments);
|
|
231
|
+
}
|
|
232
|
+
// 如果不是最后一行,且当前行非空,则添加一个换行符
|
|
233
|
+
if (!isLastLine && hasTextContent) {
|
|
234
|
+
nonVideoSegments.push({ type: 'text', data: { text: '\n' } });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// Step 5: 构建转发节点
|
|
238
|
+
const forwardNodes = [];
|
|
239
|
+
// 非视频内容节点
|
|
240
|
+
if (nonVideoSegments.length > 0) {
|
|
241
|
+
forwardNodes.push({
|
|
242
|
+
type: 'node',
|
|
243
|
+
data: {
|
|
244
|
+
user_id: session.selfId,
|
|
245
|
+
nickname: '分享助手',
|
|
246
|
+
content: nonVideoSegments
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
// 视频节点(仅当模板中有 {video} 且有有效视频时)
|
|
251
|
+
if (hasVideoInTemplate && result.videoUrl) {
|
|
252
|
+
if (typeof result.duration === 'number' && result.duration > config.Maximumduration * 60) {
|
|
253
|
+
// 超时提示已作为普通文本处理(在模板中替换为文字)
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
forwardNodes.push({
|
|
257
|
+
type: 'node',
|
|
258
|
+
data: {
|
|
259
|
+
user_id: session.selfId,
|
|
260
|
+
nickname: '分享助手',
|
|
261
|
+
content: [
|
|
262
|
+
{ type: 'video', data: { file: result.videoUrl } },
|
|
263
|
+
{ type: 'text', data: { text: `\n视频直链: ${result.videoUrl}` } }
|
|
264
|
+
]
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
if (config.logLevel === 'link_only' || config.logLevel === 'full') {
|
|
268
|
+
logger.info(`视频直链 (${result.platform}): ${result.videoUrl}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (forwardNodes.length === 0)
|
|
273
|
+
return;
|
|
274
|
+
// Step 6: 发送合并转发
|
|
275
|
+
try {
|
|
276
|
+
if (!(session.onebot && session.onebot._request))
|
|
277
|
+
throw new Error("onebot is not defined");
|
|
278
|
+
await session.onebot._request('send_group_forward_msg', {
|
|
279
|
+
group_id: session.guildId,
|
|
280
|
+
messages: forwardNodes,
|
|
281
|
+
news: [{ text: result.description || '' }],
|
|
282
|
+
prompt: result.title || '',
|
|
283
|
+
summary: 'Powered by furryaxw',
|
|
284
|
+
source: result.title || ''
|
|
285
|
+
});
|
|
286
|
+
if (config.logLevel === 'full') {
|
|
287
|
+
logger.info(`解析结果: \n ${JSON.stringify(result, null, 2)}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
logger.warn('合并转发发送失败:', err);
|
|
292
|
+
// 失败时回退到普通消息
|
|
293
|
+
await sendResult_plain(session, config, result, logger);
|
|
294
|
+
}
|
|
295
|
+
}
|
package/lib/types.d.ts
CHANGED