koishi-plugin-nitter 0.0.4 → 0.0.6

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;
@@ -17,3 +19,4 @@ export interface Config {
17
19
  }
18
20
  export declare const Config: Schema<Config, Dict>;
19
21
  export declare function apply(ctx: Context, config: Config): void;
22
+ export declare function downloadVideosToBase64(videoUrls: any): Promise<any[]>;
package/lib/index.js CHANGED
@@ -32,6 +32,7 @@ var src_exports = {};
32
32
  __export(src_exports, {
33
33
  Config: () => Config,
34
34
  apply: () => apply,
35
+ downloadVideosToBase64: () => downloadVideosToBase64,
35
36
  inject: () => inject,
36
37
  name: () => name
37
38
  });
@@ -73,14 +74,13 @@ function setGoogleTranslate(key, proxy) {
73
74
  }, "translate");
74
75
  }
75
76
  __name(setGoogleTranslate, "setGoogleTranslate");
76
- function setSiliconTranslate(key, model, prompt, timeout = 6e4) {
77
+ function setOpenAiTranslate(url, key, model, prompt, temperature, timeout = 6e4) {
77
78
  translate = /* @__PURE__ */ __name(async (texts) => {
78
- const url = "https://api.siliconflow.cn/v1/chat/completions";
79
79
  let data;
80
80
  await retry(3, async () => {
81
81
  const response = await (0, import_axios.default)({
82
82
  method: "POST",
83
- url,
83
+ url: `${url}/chat/completions`,
84
84
  headers: {
85
85
  "Authorization": `Bearer ${key}`,
86
86
  "Content-Type": "application/json"
@@ -94,14 +94,15 @@ function setSiliconTranslate(key, model, prompt, timeout = 6e4) {
94
94
  },
95
95
  {
96
96
  role: "user",
97
- content: `请翻译以下${texts.length}条HTML内容,返回JSON数组:
97
+ content: `请以JSON数组格式翻译以下${texts.length}条HTML内容:
98
98
 
99
- ${JSON.stringify(texts)}`
99
+ ${JSON.stringify(texts)}
100
+
101
+ json输出格式:["翻译结果1", "翻译结果2", ...]`
100
102
  }
101
103
  ],
102
104
  stream: false,
103
- temperature: 0.3,
104
- // 较低的温度值使翻译更稳定
105
+ temperature,
105
106
  response_format: { type: "json_object" }
106
107
  },
107
108
  timeout
@@ -124,7 +125,7 @@ ${JSON.stringify(texts)}`
124
125
  return data;
125
126
  }, "translate");
126
127
  }
127
- __name(setSiliconTranslate, "setSiliconTranslate");
128
+ __name(setOpenAiTranslate, "setOpenAiTranslate");
128
129
  async function addTranslate(page, className) {
129
130
  const elementsHTML = await page.evaluate((className2) => {
130
131
  const elements = document.querySelectorAll(className2);
@@ -153,8 +154,12 @@ __name(addTranslate, "addTranslate");
153
154
  // src/index.tsx
154
155
  var import_rettiwt_api = require("rettiwt-api");
155
156
  var import_node_cron = require("node-cron");
157
+ var import_fluent_ffmpeg = __toESM(require("fluent-ffmpeg"));
158
+ var import_path = __toESM(require("path"));
159
+ var import_promises = __toESM(require("fs/promises"));
156
160
  var import_jsx_runtime = require("@satorijs/element/jsx-runtime");
157
161
  var name = "nitter";
162
+ var logger = new import_koishi.Logger(name);
158
163
  var inject = ["puppeteer", "subscription"];
159
164
  var Config = import_koishi.Schema.intersect([
160
165
  import_koishi.Schema.object({
@@ -165,7 +170,7 @@ var Config = import_koishi.Schema.intersect([
165
170
  import_koishi.Schema.object({
166
171
  enableTranslate: import_koishi.Schema.union([
167
172
  import_koishi.Schema.const("google").description("google cloud translation"),
168
- import_koishi.Schema.const("silicon").description("硅基流动"),
173
+ import_koishi.Schema.const("openai").description("openai"),
169
174
  import_koishi.Schema.const("disable").description("关闭")
170
175
  ]).default("disable").role("radio")
171
176
  }).description("翻译设置"),
@@ -175,10 +180,12 @@ var Config = import_koishi.Schema.intersect([
175
180
  googleApiKey: import_koishi.Schema.string().required().description("访问https://cloud.google.com/获取,使用v2")
176
181
  }),
177
182
  import_koishi.Schema.object({
178
- enableTranslate: import_koishi.Schema.const("silicon").required(),
179
- siliconApiKey: import_koishi.Schema.string().required().description("访问https://www.siliconflow.cn/获取"),
183
+ enableTranslate: import_koishi.Schema.const("openai").required(),
184
+ baseurl: import_koishi.Schema.string().required().description('不要在后面添加"/"'),
185
+ openaiApiKey: import_koishi.Schema.string().required().role("secret"),
180
186
  model: import_koishi.Schema.string().required().description("模型名称"),
181
187
  prompt: import_koishi.Schema.string().required().default("你是一个专业的HTML翻译助手。请将一个HTML数组翻译为中文,严格遵循以下规则:\n1. 翻译文本内容,包括链接标签(如<a>)内的显示文本\n2.保持所有HTML标签、属性、class、id,以及结构和格式,URL链接完全不变\n3. 返回一个严格的由html文本组成的JSON数组,包含与输入数量完全相同的翻译结果,不要添加任何额外说明").role("textarea").description("提示词"),
188
+ temperature: import_koishi.Schema.number().default(1.3).required(),
182
189
  timeout: import_koishi.Schema.number().default(6e4).description("等待翻译时长")
183
190
  }),
184
191
  import_koishi.Schema.object({})
@@ -202,8 +209,8 @@ function apply(ctx, config) {
202
209
  (async () => {
203
210
  if (config.enableTranslate == "google") {
204
211
  setGoogleTranslate(config.googleApiKey, config.proxy);
205
- } else if (config.enableTranslate == "silicon") {
206
- setSiliconTranslate(config.siliconApiKey, config.model, config.prompt, config.timeout);
212
+ } else if (config.enableTranslate == "openai") {
213
+ setOpenAiTranslate(config.baseurl, config.openaiApiKey, config.model, config.prompt, config.temperature, config.timeout);
207
214
  }
208
215
  const tweetList = await getFollowedFeed();
209
216
  for (const data of tweetList) {
@@ -237,12 +244,17 @@ function apply(ctx, config) {
237
244
  return "请输入推文ID";
238
245
  }
239
246
  try {
240
- const [screenshot, imageUrls] = await renderTweetScreenshot(ctx, tweetId, config);
247
+ const [screenshot, imageUrls, hlsUrls] = await renderTweetScreenshot(ctx, tweetId, config);
241
248
  let msg = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: "data:image/png;base64," + screenshot.toString("base64") });
249
+ let forwardMsg = [];
250
+ const videoUrls = await downloadVideosToBase64(hlsUrls);
242
251
  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 }) })) });
252
+ forwardMsg.push(...imageUrls.map((url) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: url }) })));
253
+ }
254
+ if (videoUrls.length > 0) {
255
+ forwardMsg.push(...videoUrls.map((url) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("video", { src: url }) })));
244
256
  }
245
- return msg;
257
+ await session.send(msg + /* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { forward: true, children: forwardMsg }));
246
258
  } catch (error) {
247
259
  ctx.logger("nitter").error("获取推文失败:", error);
248
260
  return "获取推文失败";
@@ -256,12 +268,19 @@ function apply(ctx, config) {
256
268
  });
257
269
  async function broadcast(account, tweetId) {
258
270
  try {
259
- const [screenshot, imageUrls] = await renderTweetScreenshot(ctx, tweetId, config);
271
+ const [screenshot, imageUrls, hlsUrls] = await renderTweetScreenshot(ctx, tweetId, config);
260
272
  const screenshotMsg = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: "data:image/png;base64," + screenshot.toString("base64") });
261
273
  ctx.subscription.broadcast(config.app, account, screenshotMsg);
274
+ let forwardMsg = [];
275
+ const videoUrls = await downloadVideosToBase64(hlsUrls);
262
276
  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);
277
+ forwardMsg.push(...imageUrls.map((url) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: url }) })));
278
+ }
279
+ if (videoUrls.length > 0) {
280
+ forwardMsg.push(...videoUrls.map((url) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("video", { src: url }) })));
281
+ }
282
+ if (forwardMsg.length > 0) {
283
+ await ctx.subscription.broadcastForward(config.app, account, forwardMsg);
265
284
  }
266
285
  } catch (error) {
267
286
  ctx.logger("nitter").error("获取推文失败:", error);
@@ -293,6 +312,48 @@ function apply(ctx, config) {
293
312
  __name(checkForUpdates, "checkForUpdates");
294
313
  }
295
314
  __name(apply, "apply");
315
+ var tempDir = import_path.default.join(process.cwd(), "tmp");
316
+ async function downloadVideosToBase64(videoUrls) {
317
+ await import_promises.default.mkdir(tempDir, { recursive: true });
318
+ const results = [];
319
+ for (const url of videoUrls) {
320
+ let outputPath = null;
321
+ try {
322
+ outputPath = import_path.default.join(tempDir, `video_${Date.now()}_${Math.random().toString(36).substr(2, 9)}.mp4`);
323
+ await new Promise((resolve, reject) => {
324
+ (0, import_fluent_ffmpeg.default)(url).inputOptions([
325
+ "-protocol_whitelist",
326
+ "file,http,https,tcp,tls,crypto"
327
+ ]).outputOptions([
328
+ "-c",
329
+ "copy"
330
+ ]).output(outputPath).on("end", () => resolve(outputPath)).on("error", reject).run();
331
+ });
332
+ const stats = await import_promises.default.stat(outputPath);
333
+ const fileSizeInMB = stats.size / (1024 * 1024);
334
+ if (fileSizeInMB > 10) {
335
+ logger.info(`视频大小 ${fileSizeInMB.toFixed(2)}MB 超过10MB限制,跳过转换`);
336
+ } else {
337
+ const fileBuffer = await import_promises.default.readFile(outputPath);
338
+ const base64String = fileBuffer.toString("base64");
339
+ const dataUrl = `data:video/mp4;base64,${base64String}`;
340
+ results.push(dataUrl);
341
+ }
342
+ } catch (error) {
343
+ console.error(`处理失败 (${url}):`, error.message);
344
+ results.push(null);
345
+ } finally {
346
+ if (outputPath) {
347
+ try {
348
+ await import_promises.default.unlink(outputPath);
349
+ } catch (deleteError) {
350
+ }
351
+ }
352
+ }
353
+ }
354
+ return results;
355
+ }
356
+ __name(downloadVideosToBase64, "downloadVideosToBase64");
296
357
  async function renderTweetScreenshot(ctx, tweetId, config) {
297
358
  const puppeteer = ctx.puppeteer;
298
359
  if (!puppeteer) {
@@ -327,20 +388,17 @@ async function renderTweetScreenshot(ctx, tweetId, config) {
327
388
  omitBackground: false
328
389
  });
329
390
  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;
391
+ const originalImages = await page.$$eval(
392
+ ".main-tweet a.still-image",
393
+ (links, baseUrl) => links.map((link) => link.getAttribute("href")).filter((href) => href).map((href) => `${baseUrl}${href}`),
394
+ config.nitterUrl
395
+ );
396
+ const hlsUrls = await page.$$eval(".main-tweet video", (videos, baseUrl) => {
397
+ return videos.map((video) => video.getAttribute("data-url")).filter((dataUrl) => dataUrl).map((dataUrl) => `${baseUrl}${dataUrl}`);
340
398
  }, config.nitterUrl);
341
- return [buffer, originalImages];
399
+ return [buffer, originalImages, hlsUrls];
342
400
  }
343
- return [buffer, []];
401
+ return [buffer, [], []];
344
402
  } catch (e) {
345
403
  throw e;
346
404
  } finally {
@@ -352,6 +410,7 @@ __name(renderTweetScreenshot, "renderTweetScreenshot");
352
410
  0 && (module.exports = {
353
411
  Config,
354
412
  apply,
413
+ downloadVideosToBase64,
355
414
  inject,
356
415
  name
357
416
  });
@@ -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.6",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
@@ -19,8 +19,11 @@
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
+ "fluent-ffmpeg": "^2.1.3"
25
28
  }
26
29
  }