koishi-plugin-nitter 0.0.4 → 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 CHANGED
@@ -5,11 +5,13 @@ export interface Config {
5
5
  apiKey: string;
6
6
  nitterUrl: string;
7
7
  proxy: string;
8
- enableTranslate: "google" | "silicon" | "disable";
8
+ enableTranslate: "google" | "openai" | "disable";
9
9
  googleApiKey?: string;
10
- siliconApiKey?: string;
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 setSiliconTranslate(key, model, prompt, timeout = 6e4) {
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: `请翻译以下${texts.length}条HTML内容,返回JSON数组:
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: 0.3,
104
- // 较低的温度值使翻译更稳定
104
+ temperature,
105
105
  response_format: { type: "json_object" }
106
106
  },
107
107
  timeout
@@ -124,7 +124,7 @@ ${JSON.stringify(texts)}`
124
124
  return data;
125
125
  }, "translate");
126
126
  }
127
- __name(setSiliconTranslate, "setSiliconTranslate");
127
+ __name(setOpenAiTranslate, "setOpenAiTranslate");
128
128
  async function addTranslate(page, className) {
129
129
  const elementsHTML = await page.evaluate((className2) => {
130
130
  const elements = document.querySelectorAll(className2);
@@ -153,7 +153,13 @@ __name(addTranslate, "addTranslate");
153
153
  // src/index.tsx
154
154
  var import_rettiwt_api = require("rettiwt-api");
155
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");
156
161
  var import_jsx_runtime = require("@satorijs/element/jsx-runtime");
162
+ import_fluent_ffmpeg.default.setFfmpegPath(import_ffmpeg_static.default);
157
163
  var name = "nitter";
158
164
  var inject = ["puppeteer", "subscription"];
159
165
  var Config = import_koishi.Schema.intersect([
@@ -165,7 +171,7 @@ var Config = import_koishi.Schema.intersect([
165
171
  import_koishi.Schema.object({
166
172
  enableTranslate: import_koishi.Schema.union([
167
173
  import_koishi.Schema.const("google").description("google cloud translation"),
168
- import_koishi.Schema.const("silicon").description("硅基流动"),
174
+ import_koishi.Schema.const("openai").description("openai"),
169
175
  import_koishi.Schema.const("disable").description("关闭")
170
176
  ]).default("disable").role("radio")
171
177
  }).description("翻译设置"),
@@ -175,10 +181,12 @@ var Config = import_koishi.Schema.intersect([
175
181
  googleApiKey: import_koishi.Schema.string().required().description("访问https://cloud.google.com/获取,使用v2")
176
182
  }),
177
183
  import_koishi.Schema.object({
178
- enableTranslate: import_koishi.Schema.const("silicon").required(),
179
- siliconApiKey: import_koishi.Schema.string().required().description("访问https://www.siliconflow.cn/获取"),
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"),
180
187
  model: import_koishi.Schema.string().required().description("模型名称"),
181
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(),
182
190
  timeout: import_koishi.Schema.number().default(6e4).description("等待翻译时长")
183
191
  }),
184
192
  import_koishi.Schema.object({})
@@ -202,8 +210,8 @@ function apply(ctx, config) {
202
210
  (async () => {
203
211
  if (config.enableTranslate == "google") {
204
212
  setGoogleTranslate(config.googleApiKey, config.proxy);
205
- } else if (config.enableTranslate == "silicon") {
206
- setSiliconTranslate(config.siliconApiKey, config.model, config.prompt, config.timeout);
213
+ } else if (config.enableTranslate == "openai") {
214
+ setOpenAiTranslate(config.baseurl, config.openaiApiKey, config.model, config.prompt, config.temperature, config.timeout);
207
215
  }
208
216
  const tweetList = await getFollowedFeed();
209
217
  for (const data of tweetList) {
@@ -237,12 +245,18 @@ function apply(ctx, config) {
237
245
  return "请输入推文ID";
238
246
  }
239
247
  try {
240
- const [screenshot, imageUrls] = await renderTweetScreenshot(ctx, tweetId, config);
248
+ const [screenshot, imageUrls, hlsUrls] = await renderTweetScreenshot(ctx, tweetId, config);
241
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);
242
252
  if (imageUrls.length > 0) {
243
- msg += /* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { forward: true, children: imageUrls.map((url) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: url }) })) });
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 }) })));
244
257
  }
245
- return msg;
258
+ await session.send(msg + /* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { forward: true, children: forwardMsg }));
259
+ await deleteFiles(videoUrls);
246
260
  } catch (error) {
247
261
  ctx.logger("nitter").error("获取推文失败:", error);
248
262
  return "获取推文失败";
@@ -256,13 +270,21 @@ function apply(ctx, config) {
256
270
  });
257
271
  async function broadcast(account, tweetId) {
258
272
  try {
259
- const [screenshot, imageUrls] = await renderTweetScreenshot(ctx, tweetId, config);
273
+ const [screenshot, imageUrls, hlsUrls] = await renderTweetScreenshot(ctx, tweetId, config);
260
274
  const screenshotMsg = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: "data:image/png;base64," + screenshot.toString("base64") });
261
275
  ctx.subscription.broadcast(config.app, account, screenshotMsg);
276
+ let forwardMsg = ``;
277
+ const videoUrls = await downloadAndConvertVideos(hlsUrls);
262
278
  if (imageUrls.length > 0) {
263
- const picsForward = imageUrls.map((url) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: url }) }));
264
- 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 }) }));
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 }) }));
265
283
  }
284
+ if (forwardMsg.length > 0) {
285
+ await ctx.subscription.broadcastForward(config.app, account, forwardMsg);
286
+ }
287
+ await deleteFiles(videoUrls);
266
288
  } catch (error) {
267
289
  ctx.logger("nitter").error("获取推文失败:", error);
268
290
  }
@@ -293,6 +315,34 @@ function apply(ctx, config) {
293
315
  __name(checkForUpdates, "checkForUpdates");
294
316
  }
295
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");
296
346
  async function renderTweetScreenshot(ctx, tweetId, config) {
297
347
  const puppeteer = ctx.puppeteer;
298
348
  if (!puppeteer) {
@@ -327,20 +377,17 @@ async function renderTweetScreenshot(ctx, tweetId, config) {
327
377
  omitBackground: false
328
378
  });
329
379
  if (config.sendPic) {
330
- const originalImages = await page.evaluate((url) => {
331
- const stillImageLinks = document.querySelectorAll(".main-tweet a.still-image");
332
- const imageUrls = [];
333
- stillImageLinks.forEach((link) => {
334
- const href = link.getAttribute("href");
335
- if (href) {
336
- imageUrls.push(`${url}${href}`);
337
- }
338
- });
339
- 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}`);
340
387
  }, config.nitterUrl);
341
- return [buffer, originalImages];
388
+ return [buffer, originalImages, hlsUrls];
342
389
  }
343
- return [buffer, []];
390
+ return [buffer, [], []];
344
391
  } catch (e) {
345
392
  throw e;
346
393
  } finally {
@@ -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",
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.5",
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
  }