koishi-plugin-xlon 1.0.2 → 1.1.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/constants.d.ts +2 -0
- package/lib/features/translator.d.ts +1 -7
- package/lib/index.d.ts +1 -2
- package/lib/index.js +202 -145
- package/lib/service/message-builder.d.ts +1 -1
- package/package.json +1 -1
|
@@ -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
|
@@ -23,17 +23,16 @@ __export(src_exports, {
|
|
|
23
23
|
Config: () => Config,
|
|
24
24
|
apply: () => apply,
|
|
25
25
|
inject: () => inject,
|
|
26
|
-
logger: () => logger7,
|
|
27
26
|
name: () => name
|
|
28
27
|
});
|
|
29
28
|
module.exports = __toCommonJS(src_exports);
|
|
30
|
-
var
|
|
29
|
+
var import_koishi9 = require("koishi");
|
|
31
30
|
|
|
32
31
|
// 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} 失败(可能网络/用户名问题)。" } };
|
|
32
|
+
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
33
|
|
|
35
34
|
// 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)." } };
|
|
35
|
+
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
36
|
|
|
38
37
|
// src/db/model.ts
|
|
39
38
|
var import_koishi = require("koishi");
|
|
@@ -51,7 +50,6 @@ function extendDatabase(ctx) {
|
|
|
51
50
|
autoInc: false
|
|
52
51
|
}
|
|
53
52
|
);
|
|
54
|
-
logger.info("数据库初始化成功");
|
|
55
53
|
}
|
|
56
54
|
__name(extendDatabase, "extendDatabase");
|
|
57
55
|
|
|
@@ -77,52 +75,61 @@ var XlonRepo = class {
|
|
|
77
75
|
};
|
|
78
76
|
|
|
79
77
|
// src/features/commands/index.ts
|
|
78
|
+
var import_koishi2 = require("koishi");
|
|
79
|
+
var logger2 = new import_koishi2.Logger("xlon/commands");
|
|
80
80
|
function registerCommands(ctx, config, deps) {
|
|
81
81
|
ctx.command("x.check").alias("check").action(async ({ session }) => {
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
try {
|
|
83
|
+
await session.send(session.text("messages.checking"));
|
|
84
|
+
await deps.checkOnce(session);
|
|
85
|
+
} catch (e) {
|
|
86
|
+
logger2.error("x.check 命令执行失败", e);
|
|
87
|
+
await session.send(session.text("messages.error"));
|
|
88
|
+
}
|
|
84
89
|
});
|
|
85
90
|
ctx.command("x.tweet <input:text>").alias("tweet").action(async ({ session }, input) => {
|
|
86
|
-
|
|
91
|
+
try {
|
|
92
|
+
await deps.handleTweetInput(session, input || "");
|
|
93
|
+
} catch (e) {
|
|
94
|
+
logger2.error("x.tweet 命令执行失败", e);
|
|
95
|
+
await session.send(session.text("messages.error"));
|
|
96
|
+
}
|
|
87
97
|
});
|
|
88
98
|
}
|
|
89
99
|
__name(registerCommands, "registerCommands");
|
|
90
100
|
|
|
91
101
|
// src/features/link-detector.ts
|
|
92
|
-
var
|
|
93
|
-
var
|
|
94
|
-
var urlRe = /
|
|
102
|
+
var import_koishi3 = require("koishi");
|
|
103
|
+
var logger3 = new import_koishi3.Logger("xlon/link-detector");
|
|
104
|
+
var urlRe = /https?:\/\/[^\s'")\]]+/g;
|
|
95
105
|
function extractUrls(text) {
|
|
96
106
|
const matches = text.match(urlRe) || [];
|
|
97
|
-
return matches.map((m) => m.replace(/[
|
|
107
|
+
return matches.map((m) => m.replace(/[。,?!.,!?,。?!、]+$/g, ""));
|
|
98
108
|
}
|
|
99
109
|
__name(extractUrls, "extractUrls");
|
|
100
110
|
function isXDomain(urlStr) {
|
|
101
111
|
try {
|
|
102
|
-
const u = new URL(urlStr
|
|
112
|
+
const u = new URL(urlStr);
|
|
103
113
|
const hn = (u.hostname || "").toLowerCase();
|
|
104
114
|
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
|
|
115
|
+
} catch {
|
|
106
116
|
return false;
|
|
107
117
|
}
|
|
108
118
|
}
|
|
109
119
|
__name(isXDomain, "isXDomain");
|
|
110
|
-
async function expandShortLink(
|
|
120
|
+
async function expandShortLink(url) {
|
|
111
121
|
try {
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
122
|
+
const controller = new AbortController();
|
|
123
|
+
const timer = setTimeout(() => controller.abort(), 5e3);
|
|
124
|
+
const res = await fetch(url, {
|
|
125
|
+
method: "HEAD",
|
|
126
|
+
redirect: "follow",
|
|
127
|
+
signal: controller.signal
|
|
116
128
|
});
|
|
117
|
-
|
|
118
|
-
return
|
|
119
|
-
} catch
|
|
120
|
-
|
|
121
|
-
const location = err?.response?.headers?.location;
|
|
122
|
-
return location || url;
|
|
123
|
-
} catch (_) {
|
|
124
|
-
return url;
|
|
125
|
-
}
|
|
129
|
+
clearTimeout(timer);
|
|
130
|
+
return res.url || url;
|
|
131
|
+
} catch {
|
|
132
|
+
return url;
|
|
126
133
|
}
|
|
127
134
|
}
|
|
128
135
|
__name(expandShortLink, "expandShortLink");
|
|
@@ -136,27 +143,26 @@ function registerLinkDetector(ctx, config, handler) {
|
|
|
136
143
|
if (!candidates.length) return next();
|
|
137
144
|
const found = [];
|
|
138
145
|
for (const c of candidates) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const exp = await expandShortLink(ctx, normalized);
|
|
146
|
+
if (/^https?:\/\/t\.co\//i.test(c)) {
|
|
147
|
+
const exp = await expandShortLink(c);
|
|
142
148
|
if (isXDomain(exp)) found.push(exp);
|
|
143
|
-
} else if (isXDomain(
|
|
144
|
-
found.push(
|
|
149
|
+
} else if (isXDomain(c)) {
|
|
150
|
+
found.push(c);
|
|
145
151
|
}
|
|
146
152
|
if (found.length >= 3) break;
|
|
147
153
|
}
|
|
148
154
|
if (found.length && config.outputLogs) {
|
|
149
|
-
|
|
155
|
+
logger3.info("检测到 X/Twitter 链接:", found);
|
|
150
156
|
}
|
|
151
157
|
for (const link of found) {
|
|
152
158
|
try {
|
|
153
159
|
await handler(session, link);
|
|
154
160
|
} catch (e) {
|
|
155
|
-
|
|
161
|
+
logger3.error("处理检测到的 X/Twitter 链接时出错", e);
|
|
156
162
|
}
|
|
157
163
|
}
|
|
158
164
|
} catch (err) {
|
|
159
|
-
|
|
165
|
+
logger3.error("X/Twitter 链接检测失败", err);
|
|
160
166
|
}
|
|
161
167
|
return next();
|
|
162
168
|
});
|
|
@@ -164,8 +170,8 @@ function registerLinkDetector(ctx, config, handler) {
|
|
|
164
170
|
__name(registerLinkDetector, "registerLinkDetector");
|
|
165
171
|
|
|
166
172
|
// src/features/translator.ts
|
|
167
|
-
var
|
|
168
|
-
var
|
|
173
|
+
var import_koishi4 = require("koishi");
|
|
174
|
+
var logger4 = new import_koishi4.Logger("xlon/translator");
|
|
169
175
|
function buildGlossaryBlock(term) {
|
|
170
176
|
const entries = Object.entries(term || {}).filter(([k]) => !!k);
|
|
171
177
|
if (!entries.length) return "";
|
|
@@ -208,24 +214,20 @@ function validateTranslation(original, translated) {
|
|
|
208
214
|
if (original.length > 30 && translated.length < original.length * 0.15) {
|
|
209
215
|
return { ok: false, reason: `翻译结果疑似截断(原文 ${original.length} 字 → 译文 ${translated.length} 字)` };
|
|
210
216
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
217
|
+
const sample = translated.length > 1500 ? translated.slice(0, 1500) : translated;
|
|
218
|
+
for (let winLen = 8; winLen <= Math.min(30, Math.floor(sample.length / 3)); winLen++) {
|
|
219
|
+
for (let i = 0; i + winLen * 3 <= sample.length; i++) {
|
|
220
|
+
if (sample.slice(i + winLen, i + winLen * 2) !== sample.slice(i, i + winLen)) continue;
|
|
221
|
+
if (sample.slice(i + winLen * 2, i + winLen * 3) !== sample.slice(i, i + winLen)) continue;
|
|
222
|
+
const seg = sample.slice(i, i + winLen);
|
|
214
223
|
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} 次` };
|
|
224
|
+
let count = 3;
|
|
225
|
+
let j = i + winLen * 3;
|
|
226
|
+
while (j + winLen <= sample.length && sample.slice(j, j + winLen) === seg) {
|
|
227
|
+
count++;
|
|
228
|
+
j += winLen;
|
|
228
229
|
}
|
|
230
|
+
return { ok: false, reason: `翻译结果包含连续重复片段:"${seg.slice(0, 20)}…" 重复 ${count} 次` };
|
|
229
231
|
}
|
|
230
232
|
}
|
|
231
233
|
return { ok: true };
|
|
@@ -245,13 +247,16 @@ async function callTranslateAPI(ctx, config, opts) {
|
|
|
245
247
|
],
|
|
246
248
|
stream: false
|
|
247
249
|
};
|
|
248
|
-
const response = await ctx.http.post(url, data, { headers });
|
|
250
|
+
const response = await ctx.http.post(url, data, { headers, timeout: 3e4 });
|
|
249
251
|
return String(response?.choices?.[0]?.message?.content ?? "").trim();
|
|
250
252
|
}
|
|
251
253
|
__name(callTranslateAPI, "callTranslateAPI");
|
|
252
254
|
async function translateIfEnabled(ctx, config, text) {
|
|
253
255
|
if (!config.translateEnabled) return text;
|
|
254
|
-
if (!config.apiKey)
|
|
256
|
+
if (!config.apiKey) {
|
|
257
|
+
logger4.warn("translateEnabled 已开启,但 apiKey 未配置,跳过翻译");
|
|
258
|
+
return text;
|
|
259
|
+
}
|
|
255
260
|
if (!text?.trim()) return text;
|
|
256
261
|
const glossaryBlock = buildGlossaryBlock(config.term);
|
|
257
262
|
const systemContent = injectGlossary(config.systemPrompt || "", glossaryBlock);
|
|
@@ -264,10 +269,10 @@ async function translateIfEnabled(ctx, config, text) {
|
|
|
264
269
|
});
|
|
265
270
|
const v1 = validateTranslation(text, translated);
|
|
266
271
|
if (v1.ok) {
|
|
267
|
-
if (config.outputLogs)
|
|
272
|
+
if (config.outputLogs) logger4.info("翻译通过退化检测");
|
|
268
273
|
return applyTermMap(translated, config.term);
|
|
269
274
|
}
|
|
270
|
-
|
|
275
|
+
logger4.warn(`翻译退化(${v1.reason}),使用简化 prompt 重试`);
|
|
271
276
|
const retrySystem = "你是翻译助手。直接输出简体中文翻译,不要添加任何额外内容。";
|
|
272
277
|
const retryUser = text;
|
|
273
278
|
try {
|
|
@@ -278,33 +283,37 @@ async function translateIfEnabled(ctx, config, text) {
|
|
|
278
283
|
});
|
|
279
284
|
const v2 = validateTranslation(text, retried);
|
|
280
285
|
if (v2.ok) {
|
|
281
|
-
if (config.outputLogs)
|
|
286
|
+
if (config.outputLogs) logger4.info("重试翻译通过退化检测");
|
|
282
287
|
return applyTermMap(retried, config.term);
|
|
283
288
|
}
|
|
284
|
-
|
|
289
|
+
logger4.warn(`重试翻译仍退化(${v2.reason}),回退原文`);
|
|
285
290
|
return text;
|
|
286
291
|
} catch (retryErr) {
|
|
287
|
-
|
|
292
|
+
logger4.error("重试翻译 API 调用失败", retryErr);
|
|
288
293
|
return text;
|
|
289
294
|
}
|
|
290
295
|
} catch (err) {
|
|
291
|
-
|
|
296
|
+
logger4.error("翻译 API 调用失败", err);
|
|
292
297
|
return text;
|
|
293
298
|
}
|
|
294
299
|
}
|
|
295
300
|
__name(translateIfEnabled, "translateIfEnabled");
|
|
296
301
|
|
|
297
302
|
// src/service/message-builder.ts
|
|
298
|
-
var
|
|
299
|
-
|
|
303
|
+
var import_koishi5 = require("koishi");
|
|
304
|
+
|
|
305
|
+
// src/constants.ts
|
|
306
|
+
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";
|
|
307
|
+
|
|
308
|
+
// src/service/message-builder.ts
|
|
309
|
+
var logger5 = new import_koishi5.Logger("xlon/message-builder");
|
|
300
310
|
async function fetchBinary(ctx, url, maxRetries = 3) {
|
|
301
311
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
302
312
|
try {
|
|
303
313
|
return await ctx.http.get(url, {
|
|
304
314
|
responseType: "arraybuffer",
|
|
305
|
-
headers: {
|
|
306
|
-
|
|
307
|
-
}
|
|
315
|
+
headers: { "User-Agent": UA },
|
|
316
|
+
timeout: 3e4
|
|
308
317
|
});
|
|
309
318
|
} catch (e) {
|
|
310
319
|
if (attempt >= maxRetries) throw e;
|
|
@@ -318,13 +327,15 @@ async function buildTweetMessages(ctx, config, detail, translated, opts) {
|
|
|
318
327
|
const altOriginalText = detail.altTexts && detail.altTexts.length ? "\n" + detail.altTexts.map(
|
|
319
328
|
(alt, i) => `[图片${detail.altTexts.length > 1 ? i + 1 : ""}描述原文: ${alt}]`
|
|
320
329
|
).join("\n") : "";
|
|
321
|
-
const
|
|
330
|
+
const hasMedia = (detail.mediaUrls?.length ?? 0) > 0;
|
|
331
|
+
const hasVideo = detail.mediaUrls?.some((u) => u.endsWith(".mp4")) ?? false;
|
|
332
|
+
const mediaLabel = !hasMedia ? "文字" : hasVideo ? "视频" : "图片";
|
|
322
333
|
const prefixUser = opts?.username ? `【${opts.username}】 ` : "";
|
|
323
334
|
const retweetNote = opts?.isRetweet ? "[转发推文]\n" : "";
|
|
324
|
-
let textMsg = `${prefixUser}${config.messagePrefix}一条${
|
|
335
|
+
let textMsg = `${prefixUser}${config.messagePrefix}一条${mediaLabel}推文:
|
|
325
336
|
${translated}${altOriginalText}
|
|
326
337
|
${retweetNote}`;
|
|
327
|
-
if (detail.screenshotBuffer) textMsg += `${
|
|
338
|
+
if (detail.screenshotBuffer) textMsg += `${import_koishi5.h.image(detail.screenshotBuffer, "image/webp")}`;
|
|
328
339
|
segments.push({ content: textMsg });
|
|
329
340
|
const imageUrls = (detail.mediaUrls || []).filter((u) => !u.endsWith(".mp4"));
|
|
330
341
|
if (imageUrls.length) {
|
|
@@ -332,20 +343,20 @@ ${retweetNote}`;
|
|
|
332
343
|
for (const imgUrl of imageUrls) {
|
|
333
344
|
try {
|
|
334
345
|
const bin = await fetchBinary(ctx, imgUrl);
|
|
335
|
-
images.push(
|
|
346
|
+
images.push(String(import_koishi5.h.image(bin, "image/jpeg")));
|
|
336
347
|
} catch (e) {
|
|
337
|
-
|
|
348
|
+
logger5.error(`下载图片失败:${imgUrl}`, e);
|
|
338
349
|
}
|
|
339
350
|
}
|
|
340
|
-
if (images.length) segments.push({ content: images.join("
|
|
351
|
+
if (images.length) segments.push({ content: images.join("") });
|
|
341
352
|
}
|
|
342
353
|
const videoUrl = (detail.mediaUrls || []).find((u) => u.endsWith(".mp4"));
|
|
343
354
|
if (videoUrl) {
|
|
344
355
|
try {
|
|
345
356
|
const bin = await fetchBinary(ctx, videoUrl);
|
|
346
|
-
segments.push({ content:
|
|
357
|
+
segments.push({ content: import_koishi5.h.video(bin, "video/mp4") });
|
|
347
358
|
} catch (e) {
|
|
348
|
-
|
|
359
|
+
logger5.error(`下载视频失败:${videoUrl}`, e);
|
|
349
360
|
}
|
|
350
361
|
}
|
|
351
362
|
return segments;
|
|
@@ -353,28 +364,32 @@ ${retweetNote}`;
|
|
|
353
364
|
__name(buildTweetMessages, "buildTweetMessages");
|
|
354
365
|
|
|
355
366
|
// src/scheduler/poller.ts
|
|
356
|
-
var
|
|
357
|
-
var
|
|
367
|
+
var import_koishi6 = require("koishi");
|
|
368
|
+
var logger6 = new import_koishi6.Logger("xlon/poller");
|
|
358
369
|
function createPoller(ctx, config, repo, fetcher, deps) {
|
|
359
370
|
let checking = false;
|
|
360
371
|
async function init() {
|
|
361
372
|
const existingIds = new Set(await repo.listIds());
|
|
362
373
|
const newSubs = config.subscriptions.filter((s) => !existingIds.has(s.id));
|
|
363
374
|
if (config.outputLogs) {
|
|
364
|
-
|
|
365
|
-
|
|
375
|
+
logger6.info(`[初始化] 已存在订阅:${Array.from(existingIds).join(", ")}`);
|
|
376
|
+
logger6.info(`[初始化] 需要初始化:${newSubs.map((s) => s.id).join(", ")}`);
|
|
366
377
|
}
|
|
367
|
-
for (
|
|
378
|
+
for (let i = 0; i < newSubs.length; i++) {
|
|
379
|
+
const sub = newSubs[i];
|
|
368
380
|
try {
|
|
369
381
|
const latest = await fetcher.getLatestTweet(sub.id);
|
|
370
382
|
const link = latest.tweets[0]?.link;
|
|
371
383
|
if (!link) continue;
|
|
372
384
|
await repo.upsert({ id: sub.id, link, content: latest.wordContent || "" });
|
|
373
385
|
} catch (e) {
|
|
374
|
-
|
|
386
|
+
logger6.error(`[初始化] 获取 ${sub.id} 最新推文失败(请检查用户名/网络)`, e);
|
|
387
|
+
}
|
|
388
|
+
if (i < newSubs.length - 1) {
|
|
389
|
+
await ctx.sleep(1e3 + Math.random() * 2e3);
|
|
375
390
|
}
|
|
376
391
|
}
|
|
377
|
-
|
|
392
|
+
logger6.info("初始化完成");
|
|
378
393
|
}
|
|
379
394
|
__name(init, "init");
|
|
380
395
|
async function checkOnce(session) {
|
|
@@ -384,7 +399,8 @@ function createPoller(ctx, config, repo, fetcher, deps) {
|
|
|
384
399
|
}
|
|
385
400
|
checking = true;
|
|
386
401
|
try {
|
|
387
|
-
for (
|
|
402
|
+
for (let i = 0; i < config.subscriptions.length; i++) {
|
|
403
|
+
const sub = config.subscriptions[i];
|
|
388
404
|
const username = sub.id;
|
|
389
405
|
try {
|
|
390
406
|
const latest = await fetcher.getLatestTweet(username);
|
|
@@ -392,24 +408,29 @@ function createPoller(ctx, config, repo, fetcher, deps) {
|
|
|
392
408
|
if (!latestLink) continue;
|
|
393
409
|
const stored = await repo.get(username);
|
|
394
410
|
const storedLink = stored?.link || "";
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
411
|
+
await repo.upsert({
|
|
412
|
+
id: username,
|
|
413
|
+
link: latestLink,
|
|
414
|
+
content: latest.wordContent || ""
|
|
415
|
+
});
|
|
416
|
+
if (!storedLink) {
|
|
417
|
+
if (config.outputLogs) logger6.info(`[轮询] 首次记录基准:${username}`);
|
|
418
|
+
} else if (storedLink !== latestLink) {
|
|
401
419
|
await deps.pushTweet({
|
|
402
420
|
username,
|
|
403
421
|
url: latestLink,
|
|
404
422
|
isRetweet: latest.tweets[0]?.isRetweet || false
|
|
405
423
|
});
|
|
406
424
|
} else if (config.outputLogs) {
|
|
407
|
-
|
|
425
|
+
logger6.info(`已是最新:${username}`);
|
|
408
426
|
}
|
|
409
427
|
} catch (e) {
|
|
410
|
-
|
|
428
|
+
logger6.error(`轮询 ${username} 失败`, e);
|
|
411
429
|
if (session) await session.send(session.text("messages.poll-fail", [username]));
|
|
412
430
|
}
|
|
431
|
+
if (i < config.subscriptions.length - 1) {
|
|
432
|
+
await ctx.sleep(1e3 + Math.random() * 2e3);
|
|
433
|
+
}
|
|
413
434
|
}
|
|
414
435
|
} finally {
|
|
415
436
|
checking = false;
|
|
@@ -418,7 +439,7 @@ function createPoller(ctx, config, repo, fetcher, deps) {
|
|
|
418
439
|
__name(checkOnce, "checkOnce");
|
|
419
440
|
function start() {
|
|
420
441
|
const ms = Math.max(1, config.updateInterval) * 60 * 1e3;
|
|
421
|
-
ctx.setInterval(() => checkOnce().catch((e) =>
|
|
442
|
+
ctx.setInterval(() => checkOnce().catch((e) => logger6.error("定时任务异常", e)), ms);
|
|
422
443
|
}
|
|
423
444
|
__name(start, "start");
|
|
424
445
|
return { init, checkOnce, start };
|
|
@@ -426,9 +447,8 @@ function createPoller(ctx, config, repo, fetcher, deps) {
|
|
|
426
447
|
__name(createPoller, "createPoller");
|
|
427
448
|
|
|
428
449
|
// src/service/fetcher.ts
|
|
429
|
-
var
|
|
430
|
-
var
|
|
431
|
-
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";
|
|
450
|
+
var import_koishi7 = require("koishi");
|
|
451
|
+
var logger7 = new import_koishi7.Logger("xlon/fetcher");
|
|
432
452
|
function toAbsoluteUrl(input) {
|
|
433
453
|
const raw = (input || "").trim();
|
|
434
454
|
if (!raw) return raw;
|
|
@@ -439,6 +459,43 @@ function buildVxApiUrl(tweetUrl) {
|
|
|
439
459
|
return tweetUrl.replace(/(twitter\.com|x\.com)/i, "api.vxtwitter.com");
|
|
440
460
|
}
|
|
441
461
|
__name(buildVxApiUrl, "buildVxApiUrl");
|
|
462
|
+
async function diagnosePage(page) {
|
|
463
|
+
return await page.evaluate(() => {
|
|
464
|
+
const url = location.href;
|
|
465
|
+
const title = document.title || "";
|
|
466
|
+
const bodyText = (document.body && document.body.innerText || "").slice(0, 600);
|
|
467
|
+
let reason = "页面未渲染出推文(可能网络缓慢或 X 页面结构变化)";
|
|
468
|
+
const isLoginWall = /\/i\/flow\/login|\/login|\/i\/flow\/single-sign-on/i.test(url) || !!document.querySelector('input[autocomplete="username"]') || /^(log in|登录|登入|sign in)/i.test(title.trim());
|
|
469
|
+
if (isLoginWall) {
|
|
470
|
+
reason = "Cookie(auth_token) 可能已失效,被重定向到登录页,请更新 cookies 配置";
|
|
471
|
+
} else if (/rate limit|too many requests|请求过于频繁|尝试次数过多/i.test(bodyText)) {
|
|
472
|
+
reason = "触发限流(Rate limit),请调大 updateInterval 或更换账号";
|
|
473
|
+
} else if (/account doesn.?t exist|账号不存在|這個帳號不存在|user not found/i.test(bodyText)) {
|
|
474
|
+
reason = "账号不存在或用户名拼写有误";
|
|
475
|
+
} else if (/suspended|账号已被冻结|帳號已遭凍結/i.test(bodyText)) {
|
|
476
|
+
reason = "账号已被冻结(suspended)";
|
|
477
|
+
} else if (/something went wrong|出错了|发生了一些错误|重试/i.test(bodyText)) {
|
|
478
|
+
reason = "X 返回错误页(Something went wrong),通常为临时故障或限流";
|
|
479
|
+
}
|
|
480
|
+
return { url, reason };
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
__name(diagnosePage, "diagnosePage");
|
|
484
|
+
async function waitForArticle(page, timeout = 3e4) {
|
|
485
|
+
const outcome = await Promise.race([
|
|
486
|
+
page.waitForSelector("article", { timeout }).then(() => "article").catch(() => null),
|
|
487
|
+
page.waitForFunction(
|
|
488
|
+
() => /\/i\/flow\/login|\/login|\/i\/flow\/single-sign-on/i.test(location.href) || !!document.querySelector('input[autocomplete="username"]'),
|
|
489
|
+
{ timeout }
|
|
490
|
+
).then(() => "login").catch(() => null)
|
|
491
|
+
]);
|
|
492
|
+
if (outcome === "article") return;
|
|
493
|
+
const diag = await diagnosePage(page).catch(() => null);
|
|
494
|
+
throw new Error(
|
|
495
|
+
diag ? `未加载到推文(${diag.reason})| URL: ${diag.url}` : "未加载到推文(页面状态未知)"
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
__name(waitForArticle, "waitForArticle");
|
|
442
499
|
async function screenshotTweetArticle(page, selector = 'article[data-testid="tweet"]') {
|
|
443
500
|
const element = await page.waitForSelector(selector, { timeout: 15e3 });
|
|
444
501
|
if (!element) throw new Error("未能找到推文容器");
|
|
@@ -453,14 +510,13 @@ async function screenshotTweetArticle(page, selector = 'article[data-testid="twe
|
|
|
453
510
|
return imgsLoaded && videosLoaded;
|
|
454
511
|
},
|
|
455
512
|
{ timeout: 2e4 },
|
|
456
|
-
// 增加到 20 秒
|
|
457
513
|
selector
|
|
458
514
|
).catch((err) => {
|
|
459
|
-
|
|
515
|
+
logger7.warn(`媒体加载超时: ${err.message}`);
|
|
460
516
|
return null;
|
|
461
517
|
});
|
|
462
518
|
if (!mediaLoadResult) {
|
|
463
|
-
|
|
519
|
+
logger7.warn("部分媒体可能未完全加载,额外等待 2 秒");
|
|
464
520
|
await page.evaluate(() => new Promise((resolve) => setTimeout(resolve, 2e3)));
|
|
465
521
|
}
|
|
466
522
|
await page.evaluate(() => {
|
|
@@ -519,7 +575,6 @@ async function preparePage(page, cookies) {
|
|
|
519
575
|
secure: true
|
|
520
576
|
});
|
|
521
577
|
await page.setUserAgent(UA);
|
|
522
|
-
await page.setDefaultNavigationTimeout(6e4);
|
|
523
578
|
await page.setDefaultTimeout(6e4);
|
|
524
579
|
}
|
|
525
580
|
__name(preparePage, "preparePage");
|
|
@@ -541,7 +596,7 @@ var XFetcher = class {
|
|
|
541
596
|
*/
|
|
542
597
|
async getLatestTweet(username, maxRetries = 3) {
|
|
543
598
|
if (!this.hasPuppeteer) {
|
|
544
|
-
|
|
599
|
+
logger7.warn("puppeteer 不可用,无法获取最新推文");
|
|
545
600
|
return { tweets: [], wordContent: "" };
|
|
546
601
|
}
|
|
547
602
|
const url = `https://x.com/${username}`;
|
|
@@ -557,7 +612,7 @@ var XFetcher = class {
|
|
|
557
612
|
});
|
|
558
613
|
await preparePage(page, this.config.cookies);
|
|
559
614
|
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 6e4 });
|
|
560
|
-
await page
|
|
615
|
+
await waitForArticle(page, 3e4);
|
|
561
616
|
const result = await page.evaluate(() => {
|
|
562
617
|
const articles = Array.from(document.querySelectorAll("article"));
|
|
563
618
|
const collected = [];
|
|
@@ -580,7 +635,7 @@ var XFetcher = class {
|
|
|
580
635
|
const textEl = article.querySelector(
|
|
581
636
|
'div[data-testid="tweetText"], div[lang]'
|
|
582
637
|
);
|
|
583
|
-
const
|
|
638
|
+
const wordContent = (textEl && textEl.textContent ? textEl.textContent : "").trim();
|
|
584
639
|
const timeEl = article.querySelector('a[href*="/status/"] time');
|
|
585
640
|
const linkEl = timeEl && timeEl.parentElement || article.querySelector('a[href*="/status/"]');
|
|
586
641
|
const href = linkEl && linkEl.getAttribute("href") || "";
|
|
@@ -597,7 +652,7 @@ var XFetcher = class {
|
|
|
597
652
|
let absolute = href;
|
|
598
653
|
if (absolute.startsWith("/")) absolute = "https://x.com" + absolute;
|
|
599
654
|
if (!absolute.startsWith("http")) absolute = "https://x.com/" + absolute;
|
|
600
|
-
collected.push({ link: absolute, isRetweet, isVideo,
|
|
655
|
+
collected.push({ link: absolute, isRetweet, isVideo, wordContent });
|
|
601
656
|
}
|
|
602
657
|
const latest = collected.slice(0, 1);
|
|
603
658
|
return {
|
|
@@ -606,17 +661,18 @@ var XFetcher = class {
|
|
|
606
661
|
isRetweet: t.isRetweet,
|
|
607
662
|
isVideo: t.isVideo
|
|
608
663
|
})),
|
|
609
|
-
wordContent: latest.length ? latest[0].
|
|
664
|
+
wordContent: latest.length ? latest[0].wordContent : ""
|
|
610
665
|
};
|
|
611
666
|
});
|
|
612
667
|
return result;
|
|
613
668
|
} catch (error) {
|
|
614
|
-
|
|
669
|
+
logger7.error(`抓取最新推文失败(第 ${attempt} 次): ${username}`, error);
|
|
615
670
|
if (attempt >= maxRetries) return { tweets: [], wordContent: "" };
|
|
616
671
|
await this.ctx.sleep(2e3 * attempt);
|
|
617
672
|
} finally {
|
|
618
673
|
if (page) await page.close().catch(() => {
|
|
619
674
|
});
|
|
675
|
+
page = void 0;
|
|
620
676
|
}
|
|
621
677
|
}
|
|
622
678
|
return { tweets: [], wordContent: "" };
|
|
@@ -640,8 +696,8 @@ var XFetcher = class {
|
|
|
640
696
|
else req.continue();
|
|
641
697
|
});
|
|
642
698
|
await preparePage(page, this.config.cookies);
|
|
643
|
-
await page.goto(tweetUrl, { waitUntil: "
|
|
644
|
-
await page
|
|
699
|
+
await page.goto(tweetUrl, { waitUntil: "domcontentloaded", timeout: 6e4 });
|
|
700
|
+
await waitForArticle(page, 3e4);
|
|
645
701
|
protectedAccount = await page.evaluate(() => {
|
|
646
702
|
return !!(document.querySelector('[data-testid="icon-lock"]') || document.querySelector('[aria-label*="rotect"]') || document.querySelector('[aria-label*="保护"]') || document.querySelector('[aria-label*="保護"]'));
|
|
647
703
|
});
|
|
@@ -664,7 +720,8 @@ var XFetcher = class {
|
|
|
664
720
|
}
|
|
665
721
|
const apiUrl = buildVxApiUrl(tweetUrl);
|
|
666
722
|
const apiResponse = await this.ctx.http.get(apiUrl, {
|
|
667
|
-
headers: { "User-Agent": UA }
|
|
723
|
+
headers: { "User-Agent": UA },
|
|
724
|
+
timeout: 15e3
|
|
668
725
|
});
|
|
669
726
|
const mediaExt = apiResponse && apiResponse.media_extended || [];
|
|
670
727
|
const altTexts = mediaExt.filter((m) => m && m.altText && String(m.altText).trim()).map((m) => String(m.altText).trim());
|
|
@@ -684,7 +741,7 @@ var XFetcher = class {
|
|
|
684
741
|
protectedAccount
|
|
685
742
|
};
|
|
686
743
|
} catch (error) {
|
|
687
|
-
|
|
744
|
+
logger7.error(`抓取推文详情失败(第 ${attempt} 次): ${tweetUrlInput}`, error);
|
|
688
745
|
if (attempt >= maxRetries) {
|
|
689
746
|
return {
|
|
690
747
|
url: tweetUrlInput,
|
|
@@ -698,6 +755,7 @@ var XFetcher = class {
|
|
|
698
755
|
} finally {
|
|
699
756
|
if (page) await page.close().catch(() => {
|
|
700
757
|
});
|
|
758
|
+
page = void 0;
|
|
701
759
|
}
|
|
702
760
|
}
|
|
703
761
|
return {
|
|
@@ -711,43 +769,43 @@ var XFetcher = class {
|
|
|
711
769
|
};
|
|
712
770
|
|
|
713
771
|
// src/config.ts
|
|
714
|
-
var
|
|
715
|
-
var Config =
|
|
716
|
-
|
|
717
|
-
cookies:
|
|
718
|
-
updateInterval:
|
|
719
|
-
messagePrefix:
|
|
720
|
-
screenshot:
|
|
772
|
+
var import_koishi8 = require("koishi");
|
|
773
|
+
var Config = import_koishi8.Schema.intersect([
|
|
774
|
+
import_koishi8.Schema.object({
|
|
775
|
+
cookies: import_koishi8.Schema.string().required(),
|
|
776
|
+
updateInterval: import_koishi8.Schema.number().min(1).default(5),
|
|
777
|
+
messagePrefix: import_koishi8.Schema.string().default("获取了"),
|
|
778
|
+
screenshot: import_koishi8.Schema.boolean().default(true)
|
|
721
779
|
}).description("基础设置"),
|
|
722
|
-
|
|
723
|
-
translateEnabled:
|
|
780
|
+
import_koishi8.Schema.object({
|
|
781
|
+
translateEnabled: import_koishi8.Schema.boolean().default(false)
|
|
724
782
|
}).description("翻译设置"),
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
translateEnabled:
|
|
728
|
-
apiKey:
|
|
729
|
-
apiUrl:
|
|
730
|
-
model:
|
|
731
|
-
systemPrompt:
|
|
783
|
+
import_koishi8.Schema.union([
|
|
784
|
+
import_koishi8.Schema.object({
|
|
785
|
+
translateEnabled: import_koishi8.Schema.const(true).required(),
|
|
786
|
+
apiKey: import_koishi8.Schema.string().required(),
|
|
787
|
+
apiUrl: import_koishi8.Schema.string().default("https://api.deepseek.com"),
|
|
788
|
+
model: import_koishi8.Schema.string().default("deepseek-chat"),
|
|
789
|
+
systemPrompt: import_koishi8.Schema.string().role("textarea").default(
|
|
732
790
|
"你是一名推文翻译专员,负责将推文翻译为简体中文。规则:\n1. 直接输出翻译结果,不添加任何前缀、解释或注释\n2. 保留原文中的 @用户名、#话题标签、URL 不翻译\n3. 保留原文的 emoji\n4. 不要修改标点符号的使用习惯\n{glossary}"
|
|
733
791
|
),
|
|
734
|
-
prompt:
|
|
735
|
-
term:
|
|
792
|
+
prompt: import_koishi8.Schema.string().role("textarea").default("{text}"),
|
|
793
|
+
term: import_koishi8.Schema.dict(String).role("table").default({})
|
|
736
794
|
}),
|
|
737
|
-
|
|
795
|
+
import_koishi8.Schema.object({})
|
|
738
796
|
]),
|
|
739
|
-
|
|
740
|
-
subscriptions:
|
|
741
|
-
|
|
742
|
-
id:
|
|
743
|
-
targets:
|
|
744
|
-
blacklist:
|
|
797
|
+
import_koishi8.Schema.object({
|
|
798
|
+
subscriptions: import_koishi8.Schema.array(
|
|
799
|
+
import_koishi8.Schema.object({
|
|
800
|
+
id: import_koishi8.Schema.string().required(),
|
|
801
|
+
targets: import_koishi8.Schema.array(String).role("table").default([]),
|
|
802
|
+
blacklist: import_koishi8.Schema.array(import_koishi8.Schema.string()).default([])
|
|
745
803
|
})
|
|
746
804
|
).default([])
|
|
747
805
|
}).description("订阅列表"),
|
|
748
|
-
|
|
749
|
-
outputLogs:
|
|
750
|
-
detectXLinks:
|
|
806
|
+
import_koishi8.Schema.object({
|
|
807
|
+
outputLogs: import_koishi8.Schema.boolean().default(true),
|
|
808
|
+
detectXLinks: import_koishi8.Schema.boolean().default(true)
|
|
751
809
|
}).description("调试设置")
|
|
752
810
|
]).i18n({
|
|
753
811
|
"en-US": en_US_default._config,
|
|
@@ -756,7 +814,7 @@ var Config = import_koishi7.Schema.intersect([
|
|
|
756
814
|
|
|
757
815
|
// src/index.ts
|
|
758
816
|
var name = "xlon";
|
|
759
|
-
var
|
|
817
|
+
var logger8 = new import_koishi9.Logger("xlon");
|
|
760
818
|
var inject = {
|
|
761
819
|
required: ["database"],
|
|
762
820
|
optional: ["puppeteer"]
|
|
@@ -764,14 +822,14 @@ var inject = {
|
|
|
764
822
|
async function sendToTarget(ctx, target, content) {
|
|
765
823
|
const idx = target.indexOf(":");
|
|
766
824
|
if (idx < 0) {
|
|
767
|
-
|
|
825
|
+
logger8.warn(`无效 target 格式(应为 platform:channelId):${target}`);
|
|
768
826
|
return;
|
|
769
827
|
}
|
|
770
828
|
const platform = target.slice(0, idx);
|
|
771
829
|
const channelId = target.slice(idx + 1);
|
|
772
830
|
const bot = ctx.bots.find((b) => b.platform === platform);
|
|
773
831
|
if (!bot) {
|
|
774
|
-
|
|
832
|
+
logger8.warn(`未找到平台 ${platform} 上的 bot,跳过 target: ${target}`);
|
|
775
833
|
return;
|
|
776
834
|
}
|
|
777
835
|
await bot.sendMessage(channelId, content);
|
|
@@ -804,7 +862,7 @@ function apply(ctx, config) {
|
|
|
804
862
|
});
|
|
805
863
|
if (hit.length) {
|
|
806
864
|
if (config.outputLogs)
|
|
807
|
-
|
|
865
|
+
logger8.info(`命中违禁词,跳过推送:${opts.username} -> ${hit.join(", ")}`);
|
|
808
866
|
return;
|
|
809
867
|
}
|
|
810
868
|
}
|
|
@@ -821,7 +879,7 @@ function apply(ctx, config) {
|
|
|
821
879
|
await session.send(session.text("messages.empty-url"));
|
|
822
880
|
return;
|
|
823
881
|
}
|
|
824
|
-
if (config.outputLogs)
|
|
882
|
+
if (config.outputLogs) logger8.info("处理推文链接:", u);
|
|
825
883
|
await session.send(session.text("messages.fetching"));
|
|
826
884
|
await processAndSend((content) => session.send(content), u);
|
|
827
885
|
}
|
|
@@ -841,7 +899,7 @@ function apply(ctx, config) {
|
|
|
841
899
|
await session.send(session.text("messages.empty-url"));
|
|
842
900
|
return;
|
|
843
901
|
}
|
|
844
|
-
if (config.outputLogs)
|
|
902
|
+
if (config.outputLogs) logger8.info("按用户名处理最新推文:", username);
|
|
845
903
|
await session.send(session.text("messages.fetching"));
|
|
846
904
|
const latest = await fetcher.getLatestTweet(username);
|
|
847
905
|
const first = latest.tweets[0];
|
|
@@ -864,7 +922,7 @@ function apply(ctx, config) {
|
|
|
864
922
|
try {
|
|
865
923
|
await sendToTarget(ctx, target, content);
|
|
866
924
|
} catch (e) {
|
|
867
|
-
|
|
925
|
+
logger8.error(`推送到 ${target} 失败`, e);
|
|
868
926
|
}
|
|
869
927
|
}
|
|
870
928
|
}, "send");
|
|
@@ -888,6 +946,5 @@ __name(apply, "apply");
|
|
|
888
946
|
Config,
|
|
889
947
|
apply,
|
|
890
948
|
inject,
|
|
891
|
-
logger,
|
|
892
949
|
name
|
|
893
950
|
});
|
|
@@ -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;
|