koishi-plugin-nitter 0.0.3 → 0.0.5
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.d.ts +4 -2
- package/lib/index.js +86 -32
- package/lib/translate.d.ts +1 -0
- package/package.json +7 -3
package/lib/index.d.ts
CHANGED
|
@@ -5,11 +5,13 @@ export interface Config {
|
|
|
5
5
|
apiKey: string;
|
|
6
6
|
nitterUrl: string;
|
|
7
7
|
proxy: string;
|
|
8
|
-
enableTranslate: "google" | "
|
|
8
|
+
enableTranslate: "google" | "openai" | "disable";
|
|
9
9
|
googleApiKey?: string;
|
|
10
|
-
|
|
10
|
+
baseurl?: string;
|
|
11
|
+
openaiApiKey?: string;
|
|
11
12
|
model?: string;
|
|
12
13
|
prompt?: string;
|
|
14
|
+
temperature: number;
|
|
13
15
|
timeout?: number;
|
|
14
16
|
app: string;
|
|
15
17
|
sendPic: boolean;
|
package/lib/index.js
CHANGED
|
@@ -73,14 +73,13 @@ function setGoogleTranslate(key, proxy) {
|
|
|
73
73
|
}, "translate");
|
|
74
74
|
}
|
|
75
75
|
__name(setGoogleTranslate, "setGoogleTranslate");
|
|
76
|
-
function
|
|
76
|
+
function setOpenAiTranslate(url, key, model, prompt, temperature, timeout = 6e4) {
|
|
77
77
|
translate = /* @__PURE__ */ __name(async (texts) => {
|
|
78
|
-
const url = "https://api.siliconflow.cn/v1/chat/completions";
|
|
79
78
|
let data;
|
|
80
79
|
await retry(3, async () => {
|
|
81
80
|
const response = await (0, import_axios.default)({
|
|
82
81
|
method: "POST",
|
|
83
|
-
url
|
|
82
|
+
url: `${url}/chat/completions`,
|
|
84
83
|
headers: {
|
|
85
84
|
"Authorization": `Bearer ${key}`,
|
|
86
85
|
"Content-Type": "application/json"
|
|
@@ -94,14 +93,15 @@ function setSiliconTranslate(key, model, prompt, timeout = 6e4) {
|
|
|
94
93
|
},
|
|
95
94
|
{
|
|
96
95
|
role: "user",
|
|
97
|
-
content:
|
|
96
|
+
content: `请以JSON数组格式翻译以下${texts.length}条HTML内容:
|
|
98
97
|
|
|
99
|
-
${JSON.stringify(texts)}
|
|
98
|
+
${JSON.stringify(texts)}
|
|
99
|
+
|
|
100
|
+
json输出格式:["翻译结果1", "翻译结果2", ...]`
|
|
100
101
|
}
|
|
101
102
|
],
|
|
102
103
|
stream: false,
|
|
103
|
-
temperature
|
|
104
|
-
// 较低的温度值使翻译更稳定
|
|
104
|
+
temperature,
|
|
105
105
|
response_format: { type: "json_object" }
|
|
106
106
|
},
|
|
107
107
|
timeout
|
|
@@ -109,7 +109,14 @@ ${JSON.stringify(texts)}`
|
|
|
109
109
|
if (response.data && response.data.choices && response.data.choices[0]) {
|
|
110
110
|
const content = response.data.choices[0].message.content;
|
|
111
111
|
data = JSON.parse(content);
|
|
112
|
-
if (!Array.isArray(data))
|
|
112
|
+
if (!Array.isArray(data)) {
|
|
113
|
+
if (typeof data === "object" && Object.keys(data).length === 1) {
|
|
114
|
+
const keys = Object.keys(data);
|
|
115
|
+
if (!Array.isArray(data[keys[0]]))
|
|
116
|
+
throw new Error("API返回数据格式异常");
|
|
117
|
+
data = data[keys[0]];
|
|
118
|
+
} else throw new Error("API返回数据格式异常");
|
|
119
|
+
}
|
|
113
120
|
} else {
|
|
114
121
|
throw new Error("API返回数据格式异常");
|
|
115
122
|
}
|
|
@@ -117,7 +124,7 @@ ${JSON.stringify(texts)}`
|
|
|
117
124
|
return data;
|
|
118
125
|
}, "translate");
|
|
119
126
|
}
|
|
120
|
-
__name(
|
|
127
|
+
__name(setOpenAiTranslate, "setOpenAiTranslate");
|
|
121
128
|
async function addTranslate(page, className) {
|
|
122
129
|
const elementsHTML = await page.evaluate((className2) => {
|
|
123
130
|
const elements = document.querySelectorAll(className2);
|
|
@@ -146,7 +153,13 @@ __name(addTranslate, "addTranslate");
|
|
|
146
153
|
// src/index.tsx
|
|
147
154
|
var import_rettiwt_api = require("rettiwt-api");
|
|
148
155
|
var import_node_cron = require("node-cron");
|
|
156
|
+
var import_fluent_ffmpeg = __toESM(require("fluent-ffmpeg"));
|
|
157
|
+
var import_ffmpeg_static = __toESM(require("ffmpeg-static"));
|
|
158
|
+
var import_path = __toESM(require("path"));
|
|
159
|
+
var import_promises = __toESM(require("fs/promises"));
|
|
160
|
+
var import_url = require("url");
|
|
149
161
|
var import_jsx_runtime = require("@satorijs/element/jsx-runtime");
|
|
162
|
+
import_fluent_ffmpeg.default.setFfmpegPath(import_ffmpeg_static.default);
|
|
150
163
|
var name = "nitter";
|
|
151
164
|
var inject = ["puppeteer", "subscription"];
|
|
152
165
|
var Config = import_koishi.Schema.intersect([
|
|
@@ -158,7 +171,7 @@ var Config = import_koishi.Schema.intersect([
|
|
|
158
171
|
import_koishi.Schema.object({
|
|
159
172
|
enableTranslate: import_koishi.Schema.union([
|
|
160
173
|
import_koishi.Schema.const("google").description("google cloud translation"),
|
|
161
|
-
import_koishi.Schema.const("
|
|
174
|
+
import_koishi.Schema.const("openai").description("openai"),
|
|
162
175
|
import_koishi.Schema.const("disable").description("关闭")
|
|
163
176
|
]).default("disable").role("radio")
|
|
164
177
|
}).description("翻译设置"),
|
|
@@ -168,10 +181,12 @@ var Config = import_koishi.Schema.intersect([
|
|
|
168
181
|
googleApiKey: import_koishi.Schema.string().required().description("访问https://cloud.google.com/获取,使用v2")
|
|
169
182
|
}),
|
|
170
183
|
import_koishi.Schema.object({
|
|
171
|
-
enableTranslate: import_koishi.Schema.const("
|
|
172
|
-
|
|
184
|
+
enableTranslate: import_koishi.Schema.const("openai").required(),
|
|
185
|
+
baseurl: import_koishi.Schema.string().required().description('不要在后面添加"/"'),
|
|
186
|
+
openaiApiKey: import_koishi.Schema.string().required().role("secret"),
|
|
173
187
|
model: import_koishi.Schema.string().required().description("模型名称"),
|
|
174
188
|
prompt: import_koishi.Schema.string().required().default("你是一个专业的HTML翻译助手。请将一个HTML数组翻译为中文,严格遵循以下规则:\n1. 翻译文本内容,包括链接标签(如<a>)内的显示文本\n2.保持所有HTML标签、属性、class、id,以及结构和格式,URL链接完全不变\n3. 返回一个严格的由html文本组成的JSON数组,包含与输入数量完全相同的翻译结果,不要添加任何额外说明").role("textarea").description("提示词"),
|
|
189
|
+
temperature: import_koishi.Schema.number().default(1.3).required(),
|
|
175
190
|
timeout: import_koishi.Schema.number().default(6e4).description("等待翻译时长")
|
|
176
191
|
}),
|
|
177
192
|
import_koishi.Schema.object({})
|
|
@@ -195,8 +210,8 @@ function apply(ctx, config) {
|
|
|
195
210
|
(async () => {
|
|
196
211
|
if (config.enableTranslate == "google") {
|
|
197
212
|
setGoogleTranslate(config.googleApiKey, config.proxy);
|
|
198
|
-
} else if (config.enableTranslate == "
|
|
199
|
-
|
|
213
|
+
} else if (config.enableTranslate == "openai") {
|
|
214
|
+
setOpenAiTranslate(config.baseurl, config.openaiApiKey, config.model, config.prompt, config.temperature, config.timeout);
|
|
200
215
|
}
|
|
201
216
|
const tweetList = await getFollowedFeed();
|
|
202
217
|
for (const data of tweetList) {
|
|
@@ -230,12 +245,18 @@ function apply(ctx, config) {
|
|
|
230
245
|
return "请输入推文ID";
|
|
231
246
|
}
|
|
232
247
|
try {
|
|
233
|
-
const [screenshot, imageUrls] = await renderTweetScreenshot(ctx, tweetId, config);
|
|
248
|
+
const [screenshot, imageUrls, hlsUrls] = await renderTweetScreenshot(ctx, tweetId, config);
|
|
234
249
|
let msg = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: "data:image/png;base64," + screenshot.toString("base64") });
|
|
250
|
+
let forwardMsg = [];
|
|
251
|
+
const videoUrls = await downloadAndConvertVideos(hlsUrls);
|
|
235
252
|
if (imageUrls.length > 0) {
|
|
236
|
-
|
|
253
|
+
forwardMsg.push(...imageUrls.map((url) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: url }) })));
|
|
254
|
+
}
|
|
255
|
+
if (videoUrls.length > 0) {
|
|
256
|
+
forwardMsg.push(...videoUrls.map((url) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("video", { src: (0, import_url.pathToFileURL)(url).href }) })));
|
|
237
257
|
}
|
|
238
|
-
|
|
258
|
+
await session.send(msg + /* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { forward: true, children: forwardMsg }));
|
|
259
|
+
await deleteFiles(videoUrls);
|
|
239
260
|
} catch (error) {
|
|
240
261
|
ctx.logger("nitter").error("获取推文失败:", error);
|
|
241
262
|
return "获取推文失败";
|
|
@@ -249,13 +270,21 @@ function apply(ctx, config) {
|
|
|
249
270
|
});
|
|
250
271
|
async function broadcast(account, tweetId) {
|
|
251
272
|
try {
|
|
252
|
-
const [screenshot, imageUrls] = await renderTweetScreenshot(ctx, tweetId, config);
|
|
273
|
+
const [screenshot, imageUrls, hlsUrls] = await renderTweetScreenshot(ctx, tweetId, config);
|
|
253
274
|
const screenshotMsg = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: "data:image/png;base64," + screenshot.toString("base64") });
|
|
254
275
|
ctx.subscription.broadcast(config.app, account, screenshotMsg);
|
|
276
|
+
let forwardMsg = ``;
|
|
277
|
+
const videoUrls = await downloadAndConvertVideos(hlsUrls);
|
|
255
278
|
if (imageUrls.length > 0) {
|
|
256
|
-
|
|
257
|
-
ctx.subscription.broadcastForward(config.app, account, picsForward);
|
|
279
|
+
forwardMsg += imageUrls.map((url) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: url }) }));
|
|
258
280
|
}
|
|
281
|
+
if (videoUrls.length > 0) {
|
|
282
|
+
forwardMsg += videoUrls.map((url) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("video", { src: (0, import_url.pathToFileURL)(url).href }) }));
|
|
283
|
+
}
|
|
284
|
+
if (forwardMsg.length > 0) {
|
|
285
|
+
await ctx.subscription.broadcastForward(config.app, account, forwardMsg);
|
|
286
|
+
}
|
|
287
|
+
await deleteFiles(videoUrls);
|
|
259
288
|
} catch (error) {
|
|
260
289
|
ctx.logger("nitter").error("获取推文失败:", error);
|
|
261
290
|
}
|
|
@@ -286,6 +315,34 @@ function apply(ctx, config) {
|
|
|
286
315
|
__name(checkForUpdates, "checkForUpdates");
|
|
287
316
|
}
|
|
288
317
|
__name(apply, "apply");
|
|
318
|
+
var tempDir = import_path.default.join(process.cwd(), "tmp");
|
|
319
|
+
async function downloadAndConvertVideos(videoUrls) {
|
|
320
|
+
await import_promises.default.mkdir(tempDir, { recursive: true });
|
|
321
|
+
const promises = videoUrls.map(async (url, index) => {
|
|
322
|
+
try {
|
|
323
|
+
const outputPath = import_path.default.join(tempDir, `video_${index}_${Date.now()}.mp4`);
|
|
324
|
+
await new Promise((resolve, reject) => {
|
|
325
|
+
(0, import_fluent_ffmpeg.default)(url).output(outputPath).on("end", () => {
|
|
326
|
+
resolve(outputPath);
|
|
327
|
+
}).on("error", reject).run();
|
|
328
|
+
});
|
|
329
|
+
return outputPath;
|
|
330
|
+
} catch (error) {
|
|
331
|
+
console.error(`处理失败 (${url}):`, error.message);
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
const results = await Promise.all(promises);
|
|
336
|
+
return results.filter((path2) => path2 !== null);
|
|
337
|
+
}
|
|
338
|
+
__name(downloadAndConvertVideos, "downloadAndConvertVideos");
|
|
339
|
+
async function deleteFiles(filePaths) {
|
|
340
|
+
await Promise.allSettled(
|
|
341
|
+
filePaths.map((filePath) => import_promises.default.unlink(filePath).catch(() => {
|
|
342
|
+
}))
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
__name(deleteFiles, "deleteFiles");
|
|
289
346
|
async function renderTweetScreenshot(ctx, tweetId, config) {
|
|
290
347
|
const puppeteer = ctx.puppeteer;
|
|
291
348
|
if (!puppeteer) {
|
|
@@ -320,20 +377,17 @@ async function renderTweetScreenshot(ctx, tweetId, config) {
|
|
|
320
377
|
omitBackground: false
|
|
321
378
|
});
|
|
322
379
|
if (config.sendPic) {
|
|
323
|
-
const originalImages = await page
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}
|
|
331
|
-
});
|
|
332
|
-
return imageUrls;
|
|
380
|
+
const originalImages = await page.$$eval(
|
|
381
|
+
".main-tweet a.still-image",
|
|
382
|
+
(links, baseUrl) => links.map((link) => link.getAttribute("href")).filter((href) => href).map((href) => `${baseUrl}${href}`),
|
|
383
|
+
config.nitterUrl
|
|
384
|
+
);
|
|
385
|
+
const hlsUrls = await page.$$eval(".main-tweet video", (videos, baseUrl) => {
|
|
386
|
+
return videos.map((video) => video.getAttribute("data-url")).filter((dataUrl) => dataUrl).map((dataUrl) => `${baseUrl}${dataUrl}`);
|
|
333
387
|
}, config.nitterUrl);
|
|
334
|
-
return [buffer, originalImages];
|
|
388
|
+
return [buffer, originalImages, hlsUrls];
|
|
335
389
|
}
|
|
336
|
-
return [buffer, []];
|
|
390
|
+
return [buffer, [], []];
|
|
337
391
|
} catch (e) {
|
|
338
392
|
throw e;
|
|
339
393
|
} finally {
|
package/lib/translate.d.ts
CHANGED
|
@@ -2,4 +2,5 @@ import { Page } from 'puppeteer-core';
|
|
|
2
2
|
export declare function retry(retries: number, fn: () => any, delay?: number): any;
|
|
3
3
|
export declare function setGoogleTranslate(key: string, proxy: string): void;
|
|
4
4
|
export declare function setSiliconTranslate(key: string, model: string, prompt: string, timeout?: number): void;
|
|
5
|
+
export declare function setOpenAiTranslate(url: string, key: string, model: string, prompt: string, temperature: number, timeout?: number): void;
|
|
5
6
|
export declare function addTranslate(page: Page, className: string): Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-nitter",
|
|
3
3
|
"description": "使用Rettiwt-API订阅推文,并使用nitter渲染",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.5",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"files": [
|
|
@@ -19,8 +19,12 @@
|
|
|
19
19
|
"axios": "^1.12.2",
|
|
20
20
|
"https-proxy-agent": "^7.0.6",
|
|
21
21
|
"koishi": "^4.18.9",
|
|
22
|
-
"koishi-plugin-subscription": "^0.0.
|
|
22
|
+
"koishi-plugin-subscription": "^0.0.5"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
23
25
|
"node-cron": "^4.2.1",
|
|
24
|
-
"rettiwt-api": "^6.0.6"
|
|
26
|
+
"rettiwt-api": "^6.0.6",
|
|
27
|
+
"ffmpeg-static": "^5.2.0",
|
|
28
|
+
"fluent-ffmpeg": "^2.1.3"
|
|
25
29
|
}
|
|
26
30
|
}
|