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 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
@@ -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)) throw new Error("模型未返回数组");
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(setSiliconTranslate, "setSiliconTranslate");
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("silicon").description("硅基流动"),
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("silicon").required(),
172
- 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"),
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 == "silicon") {
199
- 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);
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
- 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 }) })));
237
257
  }
238
- return msg;
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
- const picsForward = imageUrls.map((url) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: url }) }));
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.evaluate((url) => {
324
- const stillImageLinks = document.querySelectorAll(".main-tweet a.still-image");
325
- const imageUrls = [];
326
- stillImageLinks.forEach((link) => {
327
- const href = link.getAttribute("href");
328
- if (href) {
329
- imageUrls.push(`${url}${href}`);
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 {
@@ -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.3",
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.1",
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
  }