koishi-plugin-xlon 1.0.3 → 1.1.1
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/config.d.ts +2 -0
- package/lib/constants.d.ts +2 -0
- package/lib/features/translator.d.ts +1 -7
- package/lib/index.d.ts +1 -2
- package/lib/index.js +212 -143
- package/lib/service/message-builder.d.ts +1 -1
- package/package.json +1 -1
package/lib/config.d.ts
CHANGED
|
@@ -22,5 +22,7 @@ export interface Config {
|
|
|
22
22
|
subscriptions: SubscriptionItem[];
|
|
23
23
|
outputLogs: boolean;
|
|
24
24
|
detectXLinks: boolean;
|
|
25
|
+
/** 抓取失败时是否保存网页截图(保存至 {baseDir}/data/xlon/debug) */
|
|
26
|
+
debugScreenshot: boolean;
|
|
25
27
|
}
|
|
26
28
|
export declare const Config: Schema<Schemastery.ObjectS<{}>, {} & import("cosmokit").Dict>;
|
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
import { Context } from 'koishi';
|
|
2
2
|
import { Config } from '../config';
|
|
3
|
-
/**
|
|
4
|
-
* 翻译入口。完整流程:
|
|
5
|
-
* 1. 构建 system prompt(注入术语表)
|
|
6
|
-
* 2. 调用 LLM → 退化检测
|
|
7
|
-
* 3. 如退化 → 简化 prompt 重试一次
|
|
8
|
-
* 4. 事后兜底:术语表强制替换
|
|
9
|
-
*/
|
|
3
|
+
/** 翻译入口:注入术语表 → 调用 LLM → 退化检测 → 失败重试 → 术语表兜底。 */
|
|
10
4
|
export declare function translateIfEnabled(ctx: Context, config: Config, text: string): Promise<string>;
|
package/lib/index.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { Context
|
|
1
|
+
import { Context } from 'koishi';
|
|
2
2
|
import { Config } from './config';
|
|
3
3
|
export declare const name = "xlon";
|
|
4
|
-
export declare const logger: Logger;
|
|
5
4
|
export declare const inject: {
|
|
6
5
|
required: string[];
|
|
7
6
|
optional: string[];
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
1
2
|
var __defProp = Object.defineProperty;
|
|
2
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
4
6
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
7
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
6
8
|
var __export = (target, all) => {
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
@@ -23,17 +33,16 @@ __export(src_exports, {
|
|
|
23
33
|
Config: () => Config,
|
|
24
34
|
apply: () => apply,
|
|
25
35
|
inject: () => inject,
|
|
26
|
-
logger: () => logger7,
|
|
27
36
|
name: () => name
|
|
28
37
|
});
|
|
29
38
|
module.exports = __toCommonJS(src_exports);
|
|
30
|
-
var
|
|
39
|
+
var import_koishi9 = require("koishi");
|
|
31
40
|
|
|
32
41
|
// src/locales/zh-CN.yml
|
|
33
|
-
var zh_CN_default = { _config: { cookies: "X 的 auth_token Cookie", updateInterval: "检查更新间隔(分钟)", messagePrefix: "推送消息前缀", screenshot: "是否启用推文截图(需要 puppeteer)", translateEnabled: "是否启用翻译", apiKey: "API Key(OpenAI 兼容)", apiUrl: "API 地址", model: "模型名称", systemPrompt: "系统提示词({glossary} 会被术语表自动替换)", prompt: "用户消息模板({text} 代表待翻译文本)", term: "术语表(注入 prompt + 翻译后兜底替换)", subscriptions: { id: "博主用户名(@ 后面的部分,不要加 @)", targets: "推送目标(格式 platform:channelId,如 onebot:123456)", blacklist: "违禁词(命中则跳过推送)" }, outputLogs: "日志调试模式", detectXLinks: "是否启用 X/Twitter 链接检测" }, commands: { "xlon.check": { description: "检查一次订阅更新" }, "xlon.tweet": { description: "根据 URL 或用户名获取推文内容" } }, messages: { fetching: "正在获取推文内容...", "empty-url": "请输入推文链接。", "no-latest-tweet": "未找到用户 {0} 的最新推文,请检查用户名是否正确。", checking: "正在检查更新...", busy: "上一轮检查尚未完成,请稍候。", "poll-fail": "轮询 {0} 失败(可能网络/用户名问题)。" } };
|
|
42
|
+
var zh_CN_default = { _config: { cookies: "X 的 auth_token Cookie", updateInterval: "检查更新间隔(分钟)", messagePrefix: "推送消息前缀", screenshot: "是否启用推文截图(需要 puppeteer)", translateEnabled: "是否启用翻译", apiKey: "API Key(OpenAI 兼容)", apiUrl: "API 地址", model: "模型名称", systemPrompt: "系统提示词({glossary} 会被术语表自动替换)", prompt: "用户消息模板({text} 代表待翻译文本)", term: "术语表(注入 prompt + 翻译后兜底替换)", subscriptions: { id: "博主用户名(@ 后面的部分,不要加 @)", targets: "推送目标(格式 platform:channelId,如 onebot:123456)", blacklist: "违禁词(命中则跳过推送)" }, outputLogs: "日志调试模式", detectXLinks: "是否启用 X/Twitter 链接检测" }, commands: { "xlon.check": { description: "检查一次订阅更新" }, "xlon.tweet": { description: "根据 URL 或用户名获取推文内容" } }, messages: { fetching: "正在获取推文内容...", "empty-url": "请输入推文链接。", "no-latest-tweet": "未找到用户 {0} 的最新推文,请检查用户名是否正确。", checking: "正在检查更新...", busy: "上一轮检查尚未完成,请稍候。", "poll-fail": "轮询 {0} 失败(可能网络/用户名问题)。", error: "处理请求时发生错误,请稍后重试。" } };
|
|
34
43
|
|
|
35
44
|
// src/locales/en-US.yml
|
|
36
|
-
var en_US_default = { _config: { cookies: "X auth_token cookie", updateInterval: "Poll interval (minutes)", messagePrefix: "Message prefix", screenshot: "Enable tweet screenshot (requires puppeteer)", translateEnabled: "Enable translation", apiKey: "API key (OpenAI-compatible)", apiUrl: "API base URL", model: "Model name", systemPrompt: "System prompt ({glossary} is replaced by glossary)", prompt: "User message template ({text} is replaced by source text)", term: "Glossary (injected into prompt + post-process fallback)", subscriptions: { id: "Username (without @)", targets: "Targets (format platform:channelId, e.g. onebot:123456)", blacklist: "Block words (skip when hit)" }, outputLogs: "Verbose logs", detectXLinks: "Detect X/Twitter links in messages" }, commands: { "xlon.check": { description: "Check subscriptions for updates" }, "xlon.tweet": { description: "Fetch tweet content by URL or username" } }, messages: { fetching: "Fetching tweet content...", "empty-url": "Please provide a tweet URL.", "no-latest-tweet": "No latest tweet found for user {0}. Please check the username.", checking: "Checking for updates...", busy: "Previous check is still running, please wait.", "poll-fail": "Failed to poll {0} (possible network/username issue)." } };
|
|
45
|
+
var en_US_default = { _config: { cookies: "X auth_token cookie", updateInterval: "Poll interval (minutes)", messagePrefix: "Message prefix", screenshot: "Enable tweet screenshot (requires puppeteer)", translateEnabled: "Enable translation", apiKey: "API key (OpenAI-compatible)", apiUrl: "API base URL", model: "Model name", systemPrompt: "System prompt ({glossary} is replaced by glossary)", prompt: "User message template ({text} is replaced by source text)", term: "Glossary (injected into prompt + post-process fallback)", subscriptions: { id: "Username (without @)", targets: "Targets (format platform:channelId, e.g. onebot:123456)", blacklist: "Block words (skip when hit)" }, outputLogs: "Verbose logs", detectXLinks: "Detect X/Twitter links in messages" }, commands: { "xlon.check": { description: "Check subscriptions for updates" }, "xlon.tweet": { description: "Fetch tweet content by URL or username" } }, messages: { fetching: "Fetching tweet content...", "empty-url": "Please provide a tweet URL.", "no-latest-tweet": "No latest tweet found for user {0}. Please check the username.", checking: "Checking for updates...", busy: "Previous check is still running, please wait.", "poll-fail": "Failed to poll {0} (possible network/username issue).", error: "An error occurred while processing the request. Please try again later." } };
|
|
37
46
|
|
|
38
47
|
// src/db/model.ts
|
|
39
48
|
var import_koishi = require("koishi");
|
|
@@ -51,7 +60,6 @@ function extendDatabase(ctx) {
|
|
|
51
60
|
autoInc: false
|
|
52
61
|
}
|
|
53
62
|
);
|
|
54
|
-
logger.info("数据库初始化成功");
|
|
55
63
|
}
|
|
56
64
|
__name(extendDatabase, "extendDatabase");
|
|
57
65
|
|
|
@@ -77,52 +85,61 @@ var XlonRepo = class {
|
|
|
77
85
|
};
|
|
78
86
|
|
|
79
87
|
// src/features/commands/index.ts
|
|
88
|
+
var import_koishi2 = require("koishi");
|
|
89
|
+
var logger2 = new import_koishi2.Logger("xlon/commands");
|
|
80
90
|
function registerCommands(ctx, config, deps) {
|
|
81
91
|
ctx.command("x.check").alias("check").action(async ({ session }) => {
|
|
82
|
-
|
|
83
|
-
|
|
92
|
+
try {
|
|
93
|
+
await session.send(session.text("messages.checking"));
|
|
94
|
+
await deps.checkOnce(session);
|
|
95
|
+
} catch (e) {
|
|
96
|
+
logger2.error("x.check 命令执行失败", e);
|
|
97
|
+
await session.send(session.text("messages.error"));
|
|
98
|
+
}
|
|
84
99
|
});
|
|
85
100
|
ctx.command("x.tweet <input:text>").alias("tweet").action(async ({ session }, input) => {
|
|
86
|
-
|
|
101
|
+
try {
|
|
102
|
+
await deps.handleTweetInput(session, input || "");
|
|
103
|
+
} catch (e) {
|
|
104
|
+
logger2.error("x.tweet 命令执行失败", e);
|
|
105
|
+
await session.send(session.text("messages.error"));
|
|
106
|
+
}
|
|
87
107
|
});
|
|
88
108
|
}
|
|
89
109
|
__name(registerCommands, "registerCommands");
|
|
90
110
|
|
|
91
111
|
// src/features/link-detector.ts
|
|
92
|
-
var
|
|
93
|
-
var
|
|
94
|
-
var urlRe = /
|
|
112
|
+
var import_koishi3 = require("koishi");
|
|
113
|
+
var logger3 = new import_koishi3.Logger("xlon/link-detector");
|
|
114
|
+
var urlRe = /https?:\/\/[^\s'")\]]+/g;
|
|
95
115
|
function extractUrls(text) {
|
|
96
116
|
const matches = text.match(urlRe) || [];
|
|
97
|
-
return matches.map((m) => m.replace(/[
|
|
117
|
+
return matches.map((m) => m.replace(/[。,?!.,!?,。?!、]+$/g, ""));
|
|
98
118
|
}
|
|
99
119
|
__name(extractUrls, "extractUrls");
|
|
100
120
|
function isXDomain(urlStr) {
|
|
101
121
|
try {
|
|
102
|
-
const u = new URL(urlStr
|
|
122
|
+
const u = new URL(urlStr);
|
|
103
123
|
const hn = (u.hostname || "").toLowerCase();
|
|
104
124
|
return hn === "t.co" || hn === "x.com" || hn.endsWith(".x.com") || hn === "twitter.com" || hn.endsWith(".twitter.com") || hn === "m.twitter.com" || hn === "mobile.twitter.com";
|
|
105
|
-
} catch
|
|
125
|
+
} catch {
|
|
106
126
|
return false;
|
|
107
127
|
}
|
|
108
128
|
}
|
|
109
129
|
__name(isXDomain, "isXDomain");
|
|
110
|
-
async function expandShortLink(
|
|
130
|
+
async function expandShortLink(url) {
|
|
111
131
|
try {
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
132
|
+
const controller = new AbortController();
|
|
133
|
+
const timer = setTimeout(() => controller.abort(), 5e3);
|
|
134
|
+
const res = await fetch(url, {
|
|
135
|
+
method: "HEAD",
|
|
136
|
+
redirect: "follow",
|
|
137
|
+
signal: controller.signal
|
|
116
138
|
});
|
|
117
|
-
|
|
118
|
-
return
|
|
119
|
-
} catch
|
|
120
|
-
|
|
121
|
-
const location2 = err?.response?.headers?.location;
|
|
122
|
-
return location2 || url;
|
|
123
|
-
} catch (_) {
|
|
124
|
-
return url;
|
|
125
|
-
}
|
|
139
|
+
clearTimeout(timer);
|
|
140
|
+
return res.url || url;
|
|
141
|
+
} catch {
|
|
142
|
+
return url;
|
|
126
143
|
}
|
|
127
144
|
}
|
|
128
145
|
__name(expandShortLink, "expandShortLink");
|
|
@@ -136,27 +153,26 @@ function registerLinkDetector(ctx, config, handler) {
|
|
|
136
153
|
if (!candidates.length) return next();
|
|
137
154
|
const found = [];
|
|
138
155
|
for (const c of candidates) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const exp = await expandShortLink(ctx, normalized);
|
|
156
|
+
if (/^https?:\/\/t\.co\//i.test(c)) {
|
|
157
|
+
const exp = await expandShortLink(c);
|
|
142
158
|
if (isXDomain(exp)) found.push(exp);
|
|
143
|
-
} else if (isXDomain(
|
|
144
|
-
found.push(
|
|
159
|
+
} else if (isXDomain(c)) {
|
|
160
|
+
found.push(c);
|
|
145
161
|
}
|
|
146
162
|
if (found.length >= 3) break;
|
|
147
163
|
}
|
|
148
164
|
if (found.length && config.outputLogs) {
|
|
149
|
-
|
|
165
|
+
logger3.info("检测到 X/Twitter 链接:", found);
|
|
150
166
|
}
|
|
151
167
|
for (const link of found) {
|
|
152
168
|
try {
|
|
153
169
|
await handler(session, link);
|
|
154
170
|
} catch (e) {
|
|
155
|
-
|
|
171
|
+
logger3.error("处理检测到的 X/Twitter 链接时出错", e);
|
|
156
172
|
}
|
|
157
173
|
}
|
|
158
174
|
} catch (err) {
|
|
159
|
-
|
|
175
|
+
logger3.error("X/Twitter 链接检测失败", err);
|
|
160
176
|
}
|
|
161
177
|
return next();
|
|
162
178
|
});
|
|
@@ -164,8 +180,8 @@ function registerLinkDetector(ctx, config, handler) {
|
|
|
164
180
|
__name(registerLinkDetector, "registerLinkDetector");
|
|
165
181
|
|
|
166
182
|
// src/features/translator.ts
|
|
167
|
-
var
|
|
168
|
-
var
|
|
183
|
+
var import_koishi4 = require("koishi");
|
|
184
|
+
var logger4 = new import_koishi4.Logger("xlon/translator");
|
|
169
185
|
function buildGlossaryBlock(term) {
|
|
170
186
|
const entries = Object.entries(term || {}).filter(([k]) => !!k);
|
|
171
187
|
if (!entries.length) return "";
|
|
@@ -208,24 +224,20 @@ function validateTranslation(original, translated) {
|
|
|
208
224
|
if (original.length > 30 && translated.length < original.length * 0.15) {
|
|
209
225
|
return { ok: false, reason: `翻译结果疑似截断(原文 ${original.length} 字 → 译文 ${translated.length} 字)` };
|
|
210
226
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
227
|
+
const sample = translated.length > 1500 ? translated.slice(0, 1500) : translated;
|
|
228
|
+
for (let winLen = 8; winLen <= Math.min(30, Math.floor(sample.length / 3)); winLen++) {
|
|
229
|
+
for (let i = 0; i + winLen * 3 <= sample.length; i++) {
|
|
230
|
+
if (sample.slice(i + winLen, i + winLen * 2) !== sample.slice(i, i + winLen)) continue;
|
|
231
|
+
if (sample.slice(i + winLen * 2, i + winLen * 3) !== sample.slice(i, i + winLen)) continue;
|
|
232
|
+
const seg = sample.slice(i, i + winLen);
|
|
214
233
|
if (/^[\s\p{P}]+$/u.test(seg)) continue;
|
|
215
|
-
|
|
216
|
-
let
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
count++;
|
|
221
|
-
pos += winLen;
|
|
222
|
-
} else {
|
|
223
|
-
break;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
if (count >= 3) {
|
|
227
|
-
return { ok: false, reason: `翻译结果包含连续重复片段:"${seg.slice(0, 20)}…" 重复 ${count} 次` };
|
|
234
|
+
let count = 3;
|
|
235
|
+
let j = i + winLen * 3;
|
|
236
|
+
while (j + winLen <= sample.length && sample.slice(j, j + winLen) === seg) {
|
|
237
|
+
count++;
|
|
238
|
+
j += winLen;
|
|
228
239
|
}
|
|
240
|
+
return { ok: false, reason: `翻译结果包含连续重复片段:"${seg.slice(0, 20)}…" 重复 ${count} 次` };
|
|
229
241
|
}
|
|
230
242
|
}
|
|
231
243
|
return { ok: true };
|
|
@@ -245,13 +257,16 @@ async function callTranslateAPI(ctx, config, opts) {
|
|
|
245
257
|
],
|
|
246
258
|
stream: false
|
|
247
259
|
};
|
|
248
|
-
const response = await ctx.http.post(url, data, { headers });
|
|
260
|
+
const response = await ctx.http.post(url, data, { headers, timeout: 3e4 });
|
|
249
261
|
return String(response?.choices?.[0]?.message?.content ?? "").trim();
|
|
250
262
|
}
|
|
251
263
|
__name(callTranslateAPI, "callTranslateAPI");
|
|
252
264
|
async function translateIfEnabled(ctx, config, text) {
|
|
253
265
|
if (!config.translateEnabled) return text;
|
|
254
|
-
if (!config.apiKey)
|
|
266
|
+
if (!config.apiKey) {
|
|
267
|
+
logger4.warn("translateEnabled 已开启,但 apiKey 未配置,跳过翻译");
|
|
268
|
+
return text;
|
|
269
|
+
}
|
|
255
270
|
if (!text?.trim()) return text;
|
|
256
271
|
const glossaryBlock = buildGlossaryBlock(config.term);
|
|
257
272
|
const systemContent = injectGlossary(config.systemPrompt || "", glossaryBlock);
|
|
@@ -264,10 +279,10 @@ async function translateIfEnabled(ctx, config, text) {
|
|
|
264
279
|
});
|
|
265
280
|
const v1 = validateTranslation(text, translated);
|
|
266
281
|
if (v1.ok) {
|
|
267
|
-
if (config.outputLogs)
|
|
282
|
+
if (config.outputLogs) logger4.info("翻译通过退化检测");
|
|
268
283
|
return applyTermMap(translated, config.term);
|
|
269
284
|
}
|
|
270
|
-
|
|
285
|
+
logger4.warn(`翻译退化(${v1.reason}),使用简化 prompt 重试`);
|
|
271
286
|
const retrySystem = "你是翻译助手。直接输出简体中文翻译,不要添加任何额外内容。";
|
|
272
287
|
const retryUser = text;
|
|
273
288
|
try {
|
|
@@ -278,33 +293,37 @@ async function translateIfEnabled(ctx, config, text) {
|
|
|
278
293
|
});
|
|
279
294
|
const v2 = validateTranslation(text, retried);
|
|
280
295
|
if (v2.ok) {
|
|
281
|
-
if (config.outputLogs)
|
|
296
|
+
if (config.outputLogs) logger4.info("重试翻译通过退化检测");
|
|
282
297
|
return applyTermMap(retried, config.term);
|
|
283
298
|
}
|
|
284
|
-
|
|
299
|
+
logger4.warn(`重试翻译仍退化(${v2.reason}),回退原文`);
|
|
285
300
|
return text;
|
|
286
301
|
} catch (retryErr) {
|
|
287
|
-
|
|
302
|
+
logger4.error("重试翻译 API 调用失败", retryErr);
|
|
288
303
|
return text;
|
|
289
304
|
}
|
|
290
305
|
} catch (err) {
|
|
291
|
-
|
|
306
|
+
logger4.error("翻译 API 调用失败", err);
|
|
292
307
|
return text;
|
|
293
308
|
}
|
|
294
309
|
}
|
|
295
310
|
__name(translateIfEnabled, "translateIfEnabled");
|
|
296
311
|
|
|
297
312
|
// src/service/message-builder.ts
|
|
298
|
-
var
|
|
299
|
-
|
|
313
|
+
var import_koishi5 = require("koishi");
|
|
314
|
+
|
|
315
|
+
// src/constants.ts
|
|
316
|
+
var UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36";
|
|
317
|
+
|
|
318
|
+
// src/service/message-builder.ts
|
|
319
|
+
var logger5 = new import_koishi5.Logger("xlon/message-builder");
|
|
300
320
|
async function fetchBinary(ctx, url, maxRetries = 3) {
|
|
301
321
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
302
322
|
try {
|
|
303
323
|
return await ctx.http.get(url, {
|
|
304
324
|
responseType: "arraybuffer",
|
|
305
|
-
headers: {
|
|
306
|
-
|
|
307
|
-
}
|
|
325
|
+
headers: { "User-Agent": UA },
|
|
326
|
+
timeout: 3e4
|
|
308
327
|
});
|
|
309
328
|
} catch (e) {
|
|
310
329
|
if (attempt >= maxRetries) throw e;
|
|
@@ -318,13 +337,15 @@ async function buildTweetMessages(ctx, config, detail, translated, opts) {
|
|
|
318
337
|
const altOriginalText = detail.altTexts && detail.altTexts.length ? "\n" + detail.altTexts.map(
|
|
319
338
|
(alt, i) => `[图片${detail.altTexts.length > 1 ? i + 1 : ""}描述原文: ${alt}]`
|
|
320
339
|
).join("\n") : "";
|
|
321
|
-
const
|
|
340
|
+
const hasMedia = (detail.mediaUrls?.length ?? 0) > 0;
|
|
341
|
+
const hasVideo = detail.mediaUrls?.some((u) => u.endsWith(".mp4")) ?? false;
|
|
342
|
+
const mediaLabel = !hasMedia ? "文字" : hasVideo ? "视频" : "图片";
|
|
322
343
|
const prefixUser = opts?.username ? `【${opts.username}】 ` : "";
|
|
323
344
|
const retweetNote = opts?.isRetweet ? "[转发推文]\n" : "";
|
|
324
|
-
let textMsg = `${prefixUser}${config.messagePrefix}一条${
|
|
345
|
+
let textMsg = `${prefixUser}${config.messagePrefix}一条${mediaLabel}推文:
|
|
325
346
|
${translated}${altOriginalText}
|
|
326
347
|
${retweetNote}`;
|
|
327
|
-
if (detail.screenshotBuffer) textMsg += `${
|
|
348
|
+
if (detail.screenshotBuffer) textMsg += `${import_koishi5.h.image(detail.screenshotBuffer, "image/webp")}`;
|
|
328
349
|
segments.push({ content: textMsg });
|
|
329
350
|
const imageUrls = (detail.mediaUrls || []).filter((u) => !u.endsWith(".mp4"));
|
|
330
351
|
if (imageUrls.length) {
|
|
@@ -332,20 +353,20 @@ ${retweetNote}`;
|
|
|
332
353
|
for (const imgUrl of imageUrls) {
|
|
333
354
|
try {
|
|
334
355
|
const bin = await fetchBinary(ctx, imgUrl);
|
|
335
|
-
images.push(
|
|
356
|
+
images.push(String(import_koishi5.h.image(bin, "image/jpeg")));
|
|
336
357
|
} catch (e) {
|
|
337
|
-
|
|
358
|
+
logger5.error(`下载图片失败:${imgUrl}`, e);
|
|
338
359
|
}
|
|
339
360
|
}
|
|
340
|
-
if (images.length) segments.push({ content: images.join("
|
|
361
|
+
if (images.length) segments.push({ content: images.join("") });
|
|
341
362
|
}
|
|
342
363
|
const videoUrl = (detail.mediaUrls || []).find((u) => u.endsWith(".mp4"));
|
|
343
364
|
if (videoUrl) {
|
|
344
365
|
try {
|
|
345
366
|
const bin = await fetchBinary(ctx, videoUrl);
|
|
346
|
-
segments.push({ content:
|
|
367
|
+
segments.push({ content: import_koishi5.h.video(bin, "video/mp4") });
|
|
347
368
|
} catch (e) {
|
|
348
|
-
|
|
369
|
+
logger5.error(`下载视频失败:${videoUrl}`, e);
|
|
349
370
|
}
|
|
350
371
|
}
|
|
351
372
|
return segments;
|
|
@@ -353,28 +374,32 @@ ${retweetNote}`;
|
|
|
353
374
|
__name(buildTweetMessages, "buildTweetMessages");
|
|
354
375
|
|
|
355
376
|
// src/scheduler/poller.ts
|
|
356
|
-
var
|
|
357
|
-
var
|
|
377
|
+
var import_koishi6 = require("koishi");
|
|
378
|
+
var logger6 = new import_koishi6.Logger("xlon/poller");
|
|
358
379
|
function createPoller(ctx, config, repo, fetcher, deps) {
|
|
359
380
|
let checking = false;
|
|
360
381
|
async function init() {
|
|
361
382
|
const existingIds = new Set(await repo.listIds());
|
|
362
383
|
const newSubs = config.subscriptions.filter((s) => !existingIds.has(s.id));
|
|
363
384
|
if (config.outputLogs) {
|
|
364
|
-
|
|
365
|
-
|
|
385
|
+
logger6.info(`[初始化] 已存在订阅:${Array.from(existingIds).join(", ")}`);
|
|
386
|
+
logger6.info(`[初始化] 需要初始化:${newSubs.map((s) => s.id).join(", ")}`);
|
|
366
387
|
}
|
|
367
|
-
for (
|
|
388
|
+
for (let i = 0; i < newSubs.length; i++) {
|
|
389
|
+
const sub = newSubs[i];
|
|
368
390
|
try {
|
|
369
391
|
const latest = await fetcher.getLatestTweet(sub.id);
|
|
370
392
|
const link = latest.tweets[0]?.link;
|
|
371
393
|
if (!link) continue;
|
|
372
394
|
await repo.upsert({ id: sub.id, link, content: latest.wordContent || "" });
|
|
373
395
|
} catch (e) {
|
|
374
|
-
|
|
396
|
+
logger6.error(`[初始化] 获取 ${sub.id} 最新推文失败(请检查用户名/网络)`, e);
|
|
397
|
+
}
|
|
398
|
+
if (i < newSubs.length - 1) {
|
|
399
|
+
await ctx.sleep(1e3 + Math.random() * 2e3);
|
|
375
400
|
}
|
|
376
401
|
}
|
|
377
|
-
|
|
402
|
+
logger6.info("初始化完成");
|
|
378
403
|
}
|
|
379
404
|
__name(init, "init");
|
|
380
405
|
async function checkOnce(session) {
|
|
@@ -384,7 +409,8 @@ function createPoller(ctx, config, repo, fetcher, deps) {
|
|
|
384
409
|
}
|
|
385
410
|
checking = true;
|
|
386
411
|
try {
|
|
387
|
-
for (
|
|
412
|
+
for (let i = 0; i < config.subscriptions.length; i++) {
|
|
413
|
+
const sub = config.subscriptions[i];
|
|
388
414
|
const username = sub.id;
|
|
389
415
|
try {
|
|
390
416
|
const latest = await fetcher.getLatestTweet(username);
|
|
@@ -392,24 +418,29 @@ function createPoller(ctx, config, repo, fetcher, deps) {
|
|
|
392
418
|
if (!latestLink) continue;
|
|
393
419
|
const stored = await repo.get(username);
|
|
394
420
|
const storedLink = stored?.link || "";
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
421
|
+
await repo.upsert({
|
|
422
|
+
id: username,
|
|
423
|
+
link: latestLink,
|
|
424
|
+
content: latest.wordContent || ""
|
|
425
|
+
});
|
|
426
|
+
if (!storedLink) {
|
|
427
|
+
if (config.outputLogs) logger6.info(`[轮询] 首次记录基准:${username}`);
|
|
428
|
+
} else if (storedLink !== latestLink) {
|
|
401
429
|
await deps.pushTweet({
|
|
402
430
|
username,
|
|
403
431
|
url: latestLink,
|
|
404
432
|
isRetweet: latest.tweets[0]?.isRetweet || false
|
|
405
433
|
});
|
|
406
434
|
} else if (config.outputLogs) {
|
|
407
|
-
|
|
435
|
+
logger6.info(`已是最新:${username}`);
|
|
408
436
|
}
|
|
409
437
|
} catch (e) {
|
|
410
|
-
|
|
438
|
+
logger6.error(`轮询 ${username} 失败`, e);
|
|
411
439
|
if (session) await session.send(session.text("messages.poll-fail", [username]));
|
|
412
440
|
}
|
|
441
|
+
if (i < config.subscriptions.length - 1) {
|
|
442
|
+
await ctx.sleep(1e3 + Math.random() * 2e3);
|
|
443
|
+
}
|
|
413
444
|
}
|
|
414
445
|
} finally {
|
|
415
446
|
checking = false;
|
|
@@ -418,7 +449,7 @@ function createPoller(ctx, config, repo, fetcher, deps) {
|
|
|
418
449
|
__name(checkOnce, "checkOnce");
|
|
419
450
|
function start() {
|
|
420
451
|
const ms = Math.max(1, config.updateInterval) * 60 * 1e3;
|
|
421
|
-
ctx.setInterval(() => checkOnce().catch((e) =>
|
|
452
|
+
ctx.setInterval(() => checkOnce().catch((e) => logger6.error("定时任务异常", e)), ms);
|
|
422
453
|
}
|
|
423
454
|
__name(start, "start");
|
|
424
455
|
return { init, checkOnce, start };
|
|
@@ -426,9 +457,42 @@ function createPoller(ctx, config, repo, fetcher, deps) {
|
|
|
426
457
|
__name(createPoller, "createPoller");
|
|
427
458
|
|
|
428
459
|
// src/service/fetcher.ts
|
|
429
|
-
var
|
|
430
|
-
var
|
|
431
|
-
var
|
|
460
|
+
var fs = __toESM(require("fs"));
|
|
461
|
+
var path = __toESM(require("path"));
|
|
462
|
+
var import_koishi7 = require("koishi");
|
|
463
|
+
var logger7 = new import_koishi7.Logger("xlon/fetcher");
|
|
464
|
+
var DEBUG_SCREENSHOT_MAX = 3;
|
|
465
|
+
async function saveFailureScreenshot(page, username, attempt, dir) {
|
|
466
|
+
if (!dir || !page) return;
|
|
467
|
+
try {
|
|
468
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
469
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
470
|
+
const filename = `xlon-fail-${username}-attempt${attempt}-${ts}.png`;
|
|
471
|
+
const filepath = path.join(dir, filename);
|
|
472
|
+
const buf = await page.screenshot({ type: "png", fullPage: true });
|
|
473
|
+
await fs.promises.writeFile(filepath, buf);
|
|
474
|
+
logger7.info(`[debug] 已保存失败截图: ${filepath}`);
|
|
475
|
+
const entries = await fs.promises.readdir(dir);
|
|
476
|
+
const debugFiles = entries.filter(
|
|
477
|
+
(f) => f.startsWith("xlon-fail-") && f.endsWith(".png")
|
|
478
|
+
);
|
|
479
|
+
if (debugFiles.length > DEBUG_SCREENSHOT_MAX) {
|
|
480
|
+
const withStat = await Promise.all(
|
|
481
|
+
debugFiles.map(async (f) => ({
|
|
482
|
+
name: f,
|
|
483
|
+
mtime: (await fs.promises.stat(path.join(dir, f))).mtimeMs
|
|
484
|
+
}))
|
|
485
|
+
);
|
|
486
|
+
withStat.sort((a, b) => a.mtime - b.mtime).slice(0, debugFiles.length - DEBUG_SCREENSHOT_MAX).forEach(
|
|
487
|
+
({ name: name2 }) => fs.promises.unlink(path.join(dir, name2)).catch(() => {
|
|
488
|
+
})
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
} catch (err) {
|
|
492
|
+
logger7.warn(`[debug] 保存失败截图时出错: ${err}`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
__name(saveFailureScreenshot, "saveFailureScreenshot");
|
|
432
496
|
function toAbsoluteUrl(input) {
|
|
433
497
|
const raw = (input || "").trim();
|
|
434
498
|
if (!raw) return raw;
|
|
@@ -490,14 +554,13 @@ async function screenshotTweetArticle(page, selector = 'article[data-testid="twe
|
|
|
490
554
|
return imgsLoaded && videosLoaded;
|
|
491
555
|
},
|
|
492
556
|
{ timeout: 2e4 },
|
|
493
|
-
// 增加到 20 秒
|
|
494
557
|
selector
|
|
495
558
|
).catch((err) => {
|
|
496
|
-
|
|
559
|
+
logger7.warn(`媒体加载超时: ${err.message}`);
|
|
497
560
|
return null;
|
|
498
561
|
});
|
|
499
562
|
if (!mediaLoadResult) {
|
|
500
|
-
|
|
563
|
+
logger7.warn("部分媒体可能未完全加载,额外等待 2 秒");
|
|
501
564
|
await page.evaluate(() => new Promise((resolve) => setTimeout(resolve, 2e3)));
|
|
502
565
|
}
|
|
503
566
|
await page.evaluate(() => {
|
|
@@ -556,7 +619,6 @@ async function preparePage(page, cookies) {
|
|
|
556
619
|
secure: true
|
|
557
620
|
});
|
|
558
621
|
await page.setUserAgent(UA);
|
|
559
|
-
await page.setDefaultNavigationTimeout(6e4);
|
|
560
622
|
await page.setDefaultTimeout(6e4);
|
|
561
623
|
}
|
|
562
624
|
__name(preparePage, "preparePage");
|
|
@@ -578,7 +640,7 @@ var XFetcher = class {
|
|
|
578
640
|
*/
|
|
579
641
|
async getLatestTweet(username, maxRetries = 3) {
|
|
580
642
|
if (!this.hasPuppeteer) {
|
|
581
|
-
|
|
643
|
+
logger7.warn("puppeteer 不可用,无法获取最新推文");
|
|
582
644
|
return { tweets: [], wordContent: "" };
|
|
583
645
|
}
|
|
584
646
|
const url = `https://x.com/${username}`;
|
|
@@ -617,7 +679,7 @@ var XFetcher = class {
|
|
|
617
679
|
const textEl = article.querySelector(
|
|
618
680
|
'div[data-testid="tweetText"], div[lang]'
|
|
619
681
|
);
|
|
620
|
-
const
|
|
682
|
+
const wordContent = (textEl && textEl.textContent ? textEl.textContent : "").trim();
|
|
621
683
|
const timeEl = article.querySelector('a[href*="/status/"] time');
|
|
622
684
|
const linkEl = timeEl && timeEl.parentElement || article.querySelector('a[href*="/status/"]');
|
|
623
685
|
const href = linkEl && linkEl.getAttribute("href") || "";
|
|
@@ -634,7 +696,7 @@ var XFetcher = class {
|
|
|
634
696
|
let absolute = href;
|
|
635
697
|
if (absolute.startsWith("/")) absolute = "https://x.com" + absolute;
|
|
636
698
|
if (!absolute.startsWith("http")) absolute = "https://x.com/" + absolute;
|
|
637
|
-
collected.push({ link: absolute, isRetweet, isVideo,
|
|
699
|
+
collected.push({ link: absolute, isRetweet, isVideo, wordContent });
|
|
638
700
|
}
|
|
639
701
|
const latest = collected.slice(0, 1);
|
|
640
702
|
return {
|
|
@@ -643,17 +705,22 @@ var XFetcher = class {
|
|
|
643
705
|
isRetweet: t.isRetweet,
|
|
644
706
|
isVideo: t.isVideo
|
|
645
707
|
})),
|
|
646
|
-
wordContent: latest.length ? latest[0].
|
|
708
|
+
wordContent: latest.length ? latest[0].wordContent : ""
|
|
647
709
|
};
|
|
648
710
|
});
|
|
649
711
|
return result;
|
|
650
712
|
} catch (error) {
|
|
651
|
-
|
|
713
|
+
logger7.error(`抓取最新推文失败(第 ${attempt} 次): ${username}`, error);
|
|
714
|
+
if (this.config.debugScreenshot && page) {
|
|
715
|
+
const debugDir = path.join(this.ctx.baseDir, "data", "xlon", "debug");
|
|
716
|
+
await saveFailureScreenshot(page, username, attempt, debugDir);
|
|
717
|
+
}
|
|
652
718
|
if (attempt >= maxRetries) return { tweets: [], wordContent: "" };
|
|
653
719
|
await this.ctx.sleep(2e3 * attempt);
|
|
654
720
|
} finally {
|
|
655
721
|
if (page) await page.close().catch(() => {
|
|
656
722
|
});
|
|
723
|
+
page = void 0;
|
|
657
724
|
}
|
|
658
725
|
}
|
|
659
726
|
return { tweets: [], wordContent: "" };
|
|
@@ -677,7 +744,7 @@ var XFetcher = class {
|
|
|
677
744
|
else req.continue();
|
|
678
745
|
});
|
|
679
746
|
await preparePage(page, this.config.cookies);
|
|
680
|
-
await page.goto(tweetUrl, { waitUntil: "
|
|
747
|
+
await page.goto(tweetUrl, { waitUntil: "domcontentloaded", timeout: 6e4 });
|
|
681
748
|
await waitForArticle(page, 3e4);
|
|
682
749
|
protectedAccount = await page.evaluate(() => {
|
|
683
750
|
return !!(document.querySelector('[data-testid="icon-lock"]') || document.querySelector('[aria-label*="rotect"]') || document.querySelector('[aria-label*="保护"]') || document.querySelector('[aria-label*="保護"]'));
|
|
@@ -701,7 +768,8 @@ var XFetcher = class {
|
|
|
701
768
|
}
|
|
702
769
|
const apiUrl = buildVxApiUrl(tweetUrl);
|
|
703
770
|
const apiResponse = await this.ctx.http.get(apiUrl, {
|
|
704
|
-
headers: { "User-Agent": UA }
|
|
771
|
+
headers: { "User-Agent": UA },
|
|
772
|
+
timeout: 15e3
|
|
705
773
|
});
|
|
706
774
|
const mediaExt = apiResponse && apiResponse.media_extended || [];
|
|
707
775
|
const altTexts = mediaExt.filter((m) => m && m.altText && String(m.altText).trim()).map((m) => String(m.altText).trim());
|
|
@@ -721,7 +789,7 @@ var XFetcher = class {
|
|
|
721
789
|
protectedAccount
|
|
722
790
|
};
|
|
723
791
|
} catch (error) {
|
|
724
|
-
|
|
792
|
+
logger7.error(`抓取推文详情失败(第 ${attempt} 次): ${tweetUrlInput}`, error);
|
|
725
793
|
if (attempt >= maxRetries) {
|
|
726
794
|
return {
|
|
727
795
|
url: tweetUrlInput,
|
|
@@ -735,6 +803,7 @@ var XFetcher = class {
|
|
|
735
803
|
} finally {
|
|
736
804
|
if (page) await page.close().catch(() => {
|
|
737
805
|
});
|
|
806
|
+
page = void 0;
|
|
738
807
|
}
|
|
739
808
|
}
|
|
740
809
|
return {
|
|
@@ -748,43 +817,44 @@ var XFetcher = class {
|
|
|
748
817
|
};
|
|
749
818
|
|
|
750
819
|
// src/config.ts
|
|
751
|
-
var
|
|
752
|
-
var Config =
|
|
753
|
-
|
|
754
|
-
cookies:
|
|
755
|
-
updateInterval:
|
|
756
|
-
messagePrefix:
|
|
757
|
-
screenshot:
|
|
820
|
+
var import_koishi8 = require("koishi");
|
|
821
|
+
var Config = import_koishi8.Schema.intersect([
|
|
822
|
+
import_koishi8.Schema.object({
|
|
823
|
+
cookies: import_koishi8.Schema.string().required(),
|
|
824
|
+
updateInterval: import_koishi8.Schema.number().min(1).default(5),
|
|
825
|
+
messagePrefix: import_koishi8.Schema.string().default("获取了"),
|
|
826
|
+
screenshot: import_koishi8.Schema.boolean().default(true)
|
|
758
827
|
}).description("基础设置"),
|
|
759
|
-
|
|
760
|
-
translateEnabled:
|
|
828
|
+
import_koishi8.Schema.object({
|
|
829
|
+
translateEnabled: import_koishi8.Schema.boolean().default(false)
|
|
761
830
|
}).description("翻译设置"),
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
translateEnabled:
|
|
765
|
-
apiKey:
|
|
766
|
-
apiUrl:
|
|
767
|
-
model:
|
|
768
|
-
systemPrompt:
|
|
831
|
+
import_koishi8.Schema.union([
|
|
832
|
+
import_koishi8.Schema.object({
|
|
833
|
+
translateEnabled: import_koishi8.Schema.const(true).required(),
|
|
834
|
+
apiKey: import_koishi8.Schema.string().required(),
|
|
835
|
+
apiUrl: import_koishi8.Schema.string().default("https://api.deepseek.com"),
|
|
836
|
+
model: import_koishi8.Schema.string().default("deepseek-chat"),
|
|
837
|
+
systemPrompt: import_koishi8.Schema.string().role("textarea").default(
|
|
769
838
|
"你是一名推文翻译专员,负责将推文翻译为简体中文。规则:\n1. 直接输出翻译结果,不添加任何前缀、解释或注释\n2. 保留原文中的 @用户名、#话题标签、URL 不翻译\n3. 保留原文的 emoji\n4. 不要修改标点符号的使用习惯\n{glossary}"
|
|
770
839
|
),
|
|
771
|
-
prompt:
|
|
772
|
-
term:
|
|
840
|
+
prompt: import_koishi8.Schema.string().role("textarea").default("{text}"),
|
|
841
|
+
term: import_koishi8.Schema.dict(String).role("table").default({})
|
|
773
842
|
}),
|
|
774
|
-
|
|
843
|
+
import_koishi8.Schema.object({})
|
|
775
844
|
]),
|
|
776
|
-
|
|
777
|
-
subscriptions:
|
|
778
|
-
|
|
779
|
-
id:
|
|
780
|
-
targets:
|
|
781
|
-
blacklist:
|
|
845
|
+
import_koishi8.Schema.object({
|
|
846
|
+
subscriptions: import_koishi8.Schema.array(
|
|
847
|
+
import_koishi8.Schema.object({
|
|
848
|
+
id: import_koishi8.Schema.string().required(),
|
|
849
|
+
targets: import_koishi8.Schema.array(String).role("table").default([]),
|
|
850
|
+
blacklist: import_koishi8.Schema.array(import_koishi8.Schema.string()).default([])
|
|
782
851
|
})
|
|
783
852
|
).default([])
|
|
784
853
|
}).description("订阅列表"),
|
|
785
|
-
|
|
786
|
-
outputLogs:
|
|
787
|
-
detectXLinks:
|
|
854
|
+
import_koishi8.Schema.object({
|
|
855
|
+
outputLogs: import_koishi8.Schema.boolean().default(true),
|
|
856
|
+
detectXLinks: import_koishi8.Schema.boolean().default(true),
|
|
857
|
+
debugScreenshot: import_koishi8.Schema.boolean().default(false).description("抓取失败时保存网页截图(保存至 {baseDir}/data/xlon/debug,保留最近 3 张)")
|
|
788
858
|
}).description("调试设置")
|
|
789
859
|
]).i18n({
|
|
790
860
|
"en-US": en_US_default._config,
|
|
@@ -793,7 +863,7 @@ var Config = import_koishi7.Schema.intersect([
|
|
|
793
863
|
|
|
794
864
|
// src/index.ts
|
|
795
865
|
var name = "xlon";
|
|
796
|
-
var
|
|
866
|
+
var logger8 = new import_koishi9.Logger("xlon");
|
|
797
867
|
var inject = {
|
|
798
868
|
required: ["database"],
|
|
799
869
|
optional: ["puppeteer"]
|
|
@@ -801,14 +871,14 @@ var inject = {
|
|
|
801
871
|
async function sendToTarget(ctx, target, content) {
|
|
802
872
|
const idx = target.indexOf(":");
|
|
803
873
|
if (idx < 0) {
|
|
804
|
-
|
|
874
|
+
logger8.warn(`无效 target 格式(应为 platform:channelId):${target}`);
|
|
805
875
|
return;
|
|
806
876
|
}
|
|
807
877
|
const platform = target.slice(0, idx);
|
|
808
878
|
const channelId = target.slice(idx + 1);
|
|
809
879
|
const bot = ctx.bots.find((b) => b.platform === platform);
|
|
810
880
|
if (!bot) {
|
|
811
|
-
|
|
881
|
+
logger8.warn(`未找到平台 ${platform} 上的 bot,跳过 target: ${target}`);
|
|
812
882
|
return;
|
|
813
883
|
}
|
|
814
884
|
await bot.sendMessage(channelId, content);
|
|
@@ -841,7 +911,7 @@ function apply(ctx, config) {
|
|
|
841
911
|
});
|
|
842
912
|
if (hit.length) {
|
|
843
913
|
if (config.outputLogs)
|
|
844
|
-
|
|
914
|
+
logger8.info(`命中违禁词,跳过推送:${opts.username} -> ${hit.join(", ")}`);
|
|
845
915
|
return;
|
|
846
916
|
}
|
|
847
917
|
}
|
|
@@ -858,7 +928,7 @@ function apply(ctx, config) {
|
|
|
858
928
|
await session.send(session.text("messages.empty-url"));
|
|
859
929
|
return;
|
|
860
930
|
}
|
|
861
|
-
if (config.outputLogs)
|
|
931
|
+
if (config.outputLogs) logger8.info("处理推文链接:", u);
|
|
862
932
|
await session.send(session.text("messages.fetching"));
|
|
863
933
|
await processAndSend((content) => session.send(content), u);
|
|
864
934
|
}
|
|
@@ -878,7 +948,7 @@ function apply(ctx, config) {
|
|
|
878
948
|
await session.send(session.text("messages.empty-url"));
|
|
879
949
|
return;
|
|
880
950
|
}
|
|
881
|
-
if (config.outputLogs)
|
|
951
|
+
if (config.outputLogs) logger8.info("按用户名处理最新推文:", username);
|
|
882
952
|
await session.send(session.text("messages.fetching"));
|
|
883
953
|
const latest = await fetcher.getLatestTweet(username);
|
|
884
954
|
const first = latest.tweets[0];
|
|
@@ -901,7 +971,7 @@ function apply(ctx, config) {
|
|
|
901
971
|
try {
|
|
902
972
|
await sendToTarget(ctx, target, content);
|
|
903
973
|
} catch (e) {
|
|
904
|
-
|
|
974
|
+
logger8.error(`推送到 ${target} 失败`, e);
|
|
905
975
|
}
|
|
906
976
|
}
|
|
907
977
|
}, "send");
|
|
@@ -925,6 +995,5 @@ __name(apply, "apply");
|
|
|
925
995
|
Config,
|
|
926
996
|
apply,
|
|
927
997
|
inject,
|
|
928
|
-
logger,
|
|
929
998
|
name
|
|
930
999
|
});
|
|
@@ -2,7 +2,7 @@ import { Context, h } from 'koishi';
|
|
|
2
2
|
import { Config } from '../config';
|
|
3
3
|
import { TweetDetail } from '../types';
|
|
4
4
|
export interface MessageSegment {
|
|
5
|
-
content: string | ReturnType<typeof h.
|
|
5
|
+
content: string | ReturnType<typeof h.video>;
|
|
6
6
|
}
|
|
7
7
|
export interface BuildOptions {
|
|
8
8
|
username?: string;
|