ai-weekly 0.1.0 → 0.1.1

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/README.md CHANGED
@@ -1,18 +1,43 @@
1
- # AI 周报采集原型
1
+ ![](https://img.shields.io/badge/node-24.18.1-red)
2
+ ![](https://img.shields.io/badge/npm-12.0.2-green)
3
+ ![](https://img.shields.io/badge/codex-0.150.1-blue)
4
+ ![](https://img.shields.io/badge/omc-0.20.5-orange)
5
+ ![](https://img.shields.io/badge/Playwright-1.62.1-black)
2
6
 
3
- 抓取 GitHub 本周 Trending、按本周新增 Star 排序,随后在 Bilibili 检索前 N 个项目,筛选不超过 20 分钟的视频,并输出每个项目的前 5 个候选。
7
+ # AI 周报
4
8
 
5
- ## 作为命令行工具安装
9
+ 一个本地优先的命令行工具:从 GitHub 本周 Trending 选出项目,在 Bilibili 查找相关视频,读取站内已有字幕,再通过本机登录的 Codex CLI 生成结构化摘要和离线周报网页。
6
10
 
7
- 需要 Node.js 20 或更高版本。安装后在你希望保存数据的工作目录中运行;所有 `data/runs/` 和浏览器登录 profile 都会创建在当前目录,不会写入 npm 的安装目录。
11
+ 它不下载音频、不做转写;没有字幕或处理失败的视频会保留在运行记录中,而不会被编造成摘要。所有数据、浏览器登录 profile 和最终报告都创建在你运行命令时的当前目录,不会写入 npm 的安装目录。
12
+
13
+ ## 你会得到什么
14
+
15
+ - 按本周新增 Star 排序、自动排除历史已出现仓库的 GitHub 项目快照。
16
+ - 每个项目的 Bilibili 候选视频、已提供字幕、结构化视频摘要和来源链接。
17
+ - 可人工标记优质博主或营销号的本地复核页;营销号会在后续发现阶段被排除并自动补位。
18
+ - 按项目组织、支持项目和博主筛选的离线周报网页,含本地视频封面。
19
+
20
+ ## 快速开始
21
+
22
+ 需要 Node.js 20+、可访问 GitHub 与 Bilibili 的网络;摘要步骤还需要已安装并登录的 [Codex CLI](https://github.com/openai/codex)。
23
+
24
+ 首次使用先安装 Playwright 的 Chromium:
8
25
 
9
26
  ```bash
10
27
  npx --package=ai-weekly playwright install chromium
11
- npx ai-weekly --help
12
28
  npx ai-weekly
13
29
  ```
14
30
 
15
- 首次运行需要在打开的浏览器中登录 Bilibili;视频摘要步骤还需要本机已登录的 Codex CLI。详见下方“首次使用”。
31
+ 首次 Bilibili 登录或出现验证时,程序会打开独立浏览器窗口;完成后按 Enter 继续。完整流程会在博主复核页保存后再次等待 Enter,然后生成报告。指定日期可复用同一运行目录:
32
+
33
+ ```bash
34
+ npx ai-weekly --date 2026-08-30
35
+ npx ai-weekly --help
36
+ ```
37
+
38
+ > `npx ai-weekly [--date YYYY-MM-DD]` 是安装包用户可直接复制的入口;本仓库开发时可使用 `npm run weekly -- --date YYYY-MM-DD`。
39
+
40
+ ## 安装与网络镜像
16
41
 
17
42
  ### 国内网络使用镜像
18
43
 
@@ -39,7 +64,14 @@ npx ai-weekly --help
39
64
  npx ai-weekly
40
65
  ```
41
66
 
42
- 该命令在同一日期目录中依次执行项目发现、字幕获取、Luna 摘要、项目汇总、博主人工复核和离线报告。发现或字幕阶段检测到 Bilibili 登录受限时,才会打开浏览器并等待你完成登录或验证码后按 Enter;博主复核页保存后按 Enter,流程会继续生成最终报告。
67
+ 该命令在同一日期目录中依次执行:
68
+
69
+ ```text
70
+ 发现项目 → 获取字幕 → Codex 摘要 → 汇总项目 → 人工复核博主
71
+ → 再次汇总 → 下载封面 → 生成离线报告
72
+ ```
73
+
74
+ 发现或字幕阶段检测到 Bilibili 登录受限时,才会打开浏览器并等待你完成登录或验证码后按 Enter;博主复核页保存后按 Enter,流程会继续生成最终报告。完整流程的摘要阶段使用 `--concurrency 2`,独立执行 `summarize` 时默认并发为 1。
43
75
 
44
76
  ## 首次使用
45
77
 
@@ -62,10 +94,16 @@ npx ai-weekly discover
62
94
 
63
95
  每次执行写入 `data/runs/<日期>/`:
64
96
 
65
- - `github-trending.html`:GitHub 原始页面快照。
66
- - `github-trending.json`:排序后的项目快照。运行时会读取其他日期已生成的该文件,排除已出现的仓库,并从本次周榜后续项目补足至 `--project-limit`(默认 10)条。
67
- - `bilibili-raw/<项目名>.html`:带登录态获取的原始搜索响应,不应提交到公开仓库。
68
- - `discovery.json`:标准化结果,保留原始视频卡片、20 分钟过滤结果、前 5 个候选及评分拆分。
97
+ | 路径 | 内容 |
98
+ | --- | --- |
99
+ | `github-trending.json` | 项目快照;会排除历史运行中已出现的仓库,再补足到默认 10 个项目。 |
100
+ | `discovery.json` | 原始视频卡片、20 分钟过滤结果、每项目默认前 5 个候选及评分。 |
101
+ | `subtitles.json`、`subtitles/` | 已提供字幕和获取状态。 |
102
+ | `summaries.json`、`summaries/` | 视频结构化摘要和处理状态。 |
103
+ | `synthesis/` | 项目汇总、博主档案及用于报告的 `report-content.json`。 |
104
+ | `report/index.html` | 可离线打开的周报网页;封面位于 `report/covers/`。 |
105
+
106
+ `github-trending.html` 和 `bilibili-raw/` 是便于追溯的原始页面快照;后者含登录态下的搜索响应,已被 `.gitignore` 排除,不应提交到公开仓库。
69
107
 
70
108
  ## 常用参数
71
109
 
@@ -114,7 +152,7 @@ npx ai-weekly summarize --date 2026-08-29
114
152
 
115
153
  该命令只读取 `subtitles.json` 中标记为已保存的字幕工件,不打开浏览器、不下载音频、也不转写。它会写入:
116
154
 
117
- - `data/runs/<日期>/summaries/<BV号>-<字幕 ID>-<语言>.json`:由本机已登录的 Codex CLI 根据字幕纯文本生成的事实、视频观点、演示或使用结论、限制条件摘要;同一次调用还会生成单项目判断和阅读页的三段短文,并保留原视频与字幕来源。
155
+ - `data/runs/<日期>/summaries/<BV号>-<字幕 ID>-<语言>.json`:由本机已登录的 Codex CLI 根据字幕纯文本生成的事实、视频观点、演示或使用结论、限制条件摘要;同一次调用还会生成阅读页所需的“项目事实”和“视频观点”,并保留原视频与字幕来源。
118
156
  - `data/runs/<日期>/summaries.json`:每个视频的摘要状态;无字幕、需要重新登录和字幕获取失败会保留为待处理状态。
119
157
 
120
158
  命令会把已保存字幕转换为纯文本并通过标准输入交给本机已登录的 `codex exec`,生成固定 JSON 结构;只保存临时 Schema 和模型输出,并在命令结束后删除。摘要子进程会忽略个人 Codex 配置,避免加载无关插件和技能,但仍使用当前 Codex 登录。默认模型为 `gpt-5.6-luna`,可用 `--model <模型名>` 覆盖。字幕内容会发送给 OpenAI 的 Codex 服务,请只处理你有权提交给该服务的字幕。
@@ -177,18 +215,24 @@ npx ai-weekly review-creators
177
215
 
178
216
  ## 生成离线周报网页
179
217
 
180
- `npx ai-weekly summarize` 会在每条视频的一次 Codex 调用中同时生成结构化摘要和阅读页的三段短文。若视频同时介绍多个项目,AI 只提取当前目标项目的内容,跳过其他项目的事实、评价与使用方法;视频本身仍会保留。随后 `npx ai-weekly synthesize` 直接写出 `synthesis/report-content.json`,不再需要额外调用 Codex。
218
+ `npx ai-weekly summarize` 会在每条视频的一次 Codex 调用中同时生成结构化摘要和阅读页所需的两段短文(“项目事实”“视频观点”)。若视频同时介绍多个项目,AI 只提取当前目标项目的内容,跳过其他项目的事实、评价与使用方法;视频本身仍会保留。若本机已安装 `humanizer-zh` skill,摘要提示词会在成稿前调用它去除套话和机械表达,但不允许改写或补充字幕事实。随后 `npx ai-weekly synthesize` 直接写出 `synthesis/report-content.json`,不再需要额外调用 Codex。
181
219
 
182
- 此前按“多项目视频直接排除”生成的摘要不兼容这一规则,需要先重新摘要:
220
+ 生成无需实时第三方服务的单文件阅读页:
183
221
 
184
222
  ```bash
185
- npx ai-weekly summarize --date 2026-08-30 --all --force
223
+ npx ai-weekly report --date 2026-08-29
186
224
  ```
187
225
 
188
- 生成无需实时第三方服务的单文件阅读页:
226
+ 默认输出到 `data/runs/2026-08-29/report/index.html`;在 macOS 上,生成完成后会自动用默认浏览器打开该文件。页面按项目组织,每条视频展示“项目事实”和“视频观点”,并提供项目与博主筛选及仓库链接。页面只从 `report-content.json` 的白名单生成内容,不写入字幕、音频、视频文件、Cookie 或凭据。
227
+
228
+ ## 本地开发
189
229
 
190
230
  ```bash
191
- npx ai-weekly report --date 2026-08-29
231
+ npm install
232
+ npm test
233
+
234
+ # 在源码目录运行完整流程
235
+ npm run weekly -- --date 2026-08-30
192
236
  ```
193
237
 
194
- 默认输出到 `data/runs/2026-08-29/report/index.html`,可直接用浏览器打开。页面按项目组织,每条视频独立展示三个 AI 摘要板块,并保留项目与博主筛选及仓库链接。页面只从 `report-content.json` 的白名单生成内容,不写入字幕、音频、视频文件、Cookie 或凭据。
238
+ 可分别运行 `npm run discover`、`subtitles`、`summarize`、`synthesize`、`review-creators`、`covers`、`report` `weekly`。项目采用 [MIT License](LICENSE)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-weekly",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "从 GitHub Trending 与 Bilibili 候选视频生成本地 AI 周报的命令行工具。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,4 +1,5 @@
1
1
  import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { spawn } from "node:child_process";
2
3
  import { join, resolve } from "node:path";
3
4
  import { pathToFileURL } from "node:url";
4
5
 
@@ -27,7 +28,7 @@ function safeUrl(value) {
27
28
 
28
29
  function safeCoverFile(value, bvid) {
29
30
  const path = text(value);
30
- return /^BV[0-9A-Za-z]+$/.test(bvid) && new RegExp(`^covers/${bvid}\\.(?:jpg|png|webp)$`).test(path) ? path : "";
31
+ return /^BV[0-9A-Za-z]+$/.test(bvid) && new RegExp(`^covers/${bvid}\\.(?:jpg|png|webp|avif)$`).test(path) ? path : "";
31
32
  }
32
33
 
33
34
  function safeVideo(video, covers) {
@@ -88,11 +89,19 @@ export async function buildReport(options) {
88
89
  return { outputPath, data };
89
90
  }
90
91
 
92
+ export function openReport(outputPath, { platform = process.platform, spawnFn = spawn } = {}) {
93
+ if (platform !== "darwin") return false;
94
+ const opener = spawnFn("open", [outputPath], { stdio: "ignore" });
95
+ opener.once("error", (error) => console.warn(`无法自动打开周报网页:${error.message}`));
96
+ return true;
97
+ }
98
+
91
99
  async function main() {
92
100
  const options = parseArgs(process.argv.slice(2));
93
101
  if (options.help) return console.log("用法:npx ai-weekly report [--date YYYY-MM-DD] [--input PATH] [--output-dir PATH]");
94
102
  const result = await buildReport(options);
95
103
  console.log(`离线周报已生成:${result.outputPath}`);
104
+ if (openReport(result.outputPath)) console.log("已在默认浏览器中打开周报网页。");
96
105
  }
97
106
 
98
107
  if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) main().catch((error) => { console.error(`运行失败:${error.message}`); process.exitCode = 1; });
@@ -58,7 +58,7 @@ function isBilibiliCoverUrl(value) {
58
58
 
59
59
  function extensionFor(contentType) {
60
60
  const type = text(contentType).toLowerCase().split(";", 1)[0];
61
- return new Map([["image/jpeg", "jpg"], ["image/png", "png"], ["image/webp", "webp"]]).get(type) || "";
61
+ return new Map([["image/jpeg", "jpg"], ["image/png", "png"], ["image/webp", "webp"], ["image/avif", "avif"]]).get(type) || "";
62
62
  }
63
63
 
64
64
  async function exists(path) {
@@ -104,7 +104,7 @@ export async function downloadReportCovers(options, { fetch: fetchImpl = fetch }
104
104
  for (const bvid of selectedBvids(report)) {
105
105
  // The filesystem check is intentionally sequential so that an existing local image never triggers a network request.
106
106
  let existingExtension = "";
107
- for (const extension of ["jpg", "png", "webp"]) if (await exists(join(coversDir, `${bvid}.${extension}`))) { existingExtension = extension; break; }
107
+ for (const extension of ["jpg", "png", "webp", "avif"]) if (await exists(join(coversDir, `${bvid}.${extension}`))) { existingExtension = extension; break; }
108
108
  if (existingExtension && !options.force) {
109
109
  covers[bvid] = { file: `covers/${bvid}.${existingExtension}`, status: "already_downloaded" };
110
110
  summary.skipped += 1;
@@ -1,6 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { existsSync } from "node:fs";
2
3
  import { mkdtemp, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
+ import { homedir, tmpdir } from "node:os";
4
5
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
6
  import { pathToFileURL } from "node:url";
6
7
 
@@ -146,16 +147,38 @@ function usageFromCodex(stderr, configuredModel) {
146
147
  return { model: modelMatch?.[1] || configuredModel || "default", tokens: tokenMatch ? Number(tokenMatch[1].replaceAll(",", "")) : null };
147
148
  }
148
149
 
150
+ function localHumanizerSkillPaths() {
151
+ const codexHome = process.env.CODEX_HOME || join(homedir(), ".codex");
152
+ return [
153
+ join(codexHome, "skills", "humanizer-zh", "SKILL.md"),
154
+ join(homedir(), ".agents", "skills", "humanizer-zh", "SKILL.md")
155
+ ];
156
+ }
157
+
158
+ export function hasLocalHumanizerSkill(paths = localHumanizerSkillPaths()) {
159
+ return paths.some((skillPath) => existsSync(skillPath));
160
+ }
161
+
162
+ export function buildSummaryPrompt({ subtitleText: text, repository, videoTitle, useHumanizer = hasLocalHumanizerSkill() }) {
163
+ const humanizerInstruction = useHumanizer
164
+ ? "\n\n本机已安装 $humanizer-zh。完成内容后调用该 skill 做最后一轮去 AI 味润色:删去套话、宣传语和机械排比,保持句式自然。不得改变或补充字幕事实,不得加入个人感受、第一人称或无来源判断;结构化字段仍须符合下述要求。"
165
+ : "";
166
+ return `仅依据下列字幕纯文本,为目标项目 ${repository || "未命名项目"} 的视频《${videoTitle || "未命名视频"}》生成中文结构化摘要。区分视频陈述的事实、观点、演示或使用结论、限制条件;不确定的内容不要编造。\n\n同时填写 report。视频可能一次性介绍多个项目:只提取与目标项目直接相关的信息,跳过其他项目的名称、事实、评价和使用方法;无论是否涉及多个项目,都必须生成 project_facts 和 video_viewpoints 两项。两项各写成信息充分、自然连贯的中文短段,目标为 120 至 180 字;字幕信息不足时可更短,但不得为了凑字数补充材料外的信息。project_facts 只写可确认的项目能力、流程、演示结果和使用方式;video_viewpoints 概括视频给出的评价、适用判断与取舍,不要出现“视频作者”“作者认为”。语气自然克制,不用宣传词。${humanizerInstruction}\n\n最终回答必须只符合提供的 JSON Schema,不要解释。\n\n字幕:\n${text}\n`;
167
+ }
168
+
149
169
  export async function summarizeWithCodex({ subtitleText: text, repository, videoTitle, model, timeoutMs }) {
150
170
  const directory = await mkdtemp(join(tmpdir(), "ai-weekly-codex-summary-"));
151
171
  const schemaPath = join(directory, "summary-schema.json");
152
172
  const outputPath = join(directory, "summary.json");
153
173
  try {
154
174
  await writeFile(schemaPath, `${JSON.stringify(summarySchema)}\n`);
155
- const args = ["exec", "--skip-git-repo-check", "--ephemeral", "--ignore-user-config", "--sandbox", "read-only", "--output-schema", schemaPath, "--output-last-message", outputPath, "--cd", directory];
175
+ const useHumanizer = hasLocalHumanizerSkill();
176
+ const args = ["exec", "--skip-git-repo-check", "--ephemeral"];
177
+ if (!useHumanizer) args.push("--ignore-user-config");
178
+ args.push("--sandbox", "read-only", "--output-schema", schemaPath, "--output-last-message", outputPath, "--cd", directory);
156
179
  if (model) args.push("--model", model);
157
180
  args.push("-");
158
- const stderr = await runCodex(args, `仅依据下列字幕纯文本,为目标项目 ${repository || "未命名项目"} 的视频《${videoTitle || "未命名视频"}》生成中文结构化摘要。区分视频陈述的事实、观点、演示或使用结论、限制条件;不确定的内容不要编造。\n\n同时填写 report。视频可能一次性介绍多个项目:只提取与目标项目直接相关的信息,跳过其他项目的名称、事实、评价和使用方法;无论是否涉及多个项目,都必须生成 project_facts 和 video_viewpoints 两项。两项各写成信息充分、自然连贯的中文短段,目标为 120 至 180 字;字幕信息不足时可更短,但不得为了凑字数补充材料外的信息。project_facts 只写可确认的项目能力、流程、演示结果和使用方式;video_viewpoints 概括视频给出的评价、适用判断与取舍,不要出现“视频作者”“作者认为”。语气自然克制,不用宣传词。\n\n最终回答必须只符合提供的 JSON Schema,不要解释。\n\n字幕:\n${text}\n`, timeoutMs);
181
+ const stderr = await runCodex(args, buildSummaryPrompt({ subtitleText: text, repository, videoTitle, useHumanizer }), timeoutMs);
159
182
  return { summary: validateSummary(JSON.parse(await readFile(outputPath, "utf8"))), usage: usageFromCodex(stderr, model) };
160
183
  } catch (error) {
161
184
  throw new Error(`Codex 摘要失败:${error.message}`);