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 +62 -18
- package/package.json +1 -1
- package/scripts/build-report.mjs +10 -1
- package/scripts/download-report-covers.mjs +2 -2
- package/scripts/summarize-videos.mjs +26 -3
package/README.md
CHANGED
|
@@ -1,18 +1,43 @@
|
|
|
1
|
-
|
|
1
|
+

|
|
2
|
+

|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
2
6
|
|
|
3
|
-
|
|
7
|
+
# AI 周报
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
一个本地优先的命令行工具:从 GitHub 本周 Trending 选出项目,在 Bilibili 查找相关视频,读取站内已有字幕,再通过本机登录的 Codex CLI 生成结构化摘要和离线周报网页。
|
|
6
10
|
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
231
|
+
npm install
|
|
232
|
+
npm test
|
|
233
|
+
|
|
234
|
+
# 在源码目录运行完整流程
|
|
235
|
+
npm run weekly -- --date 2026-08-30
|
|
192
236
|
```
|
|
193
237
|
|
|
194
|
-
|
|
238
|
+
可分别运行 `npm run discover`、`subtitles`、`summarize`、`synthesize`、`review-creators`、`covers`、`report` 或 `weekly`。项目采用 [MIT License](LICENSE)。
|
package/package.json
CHANGED
package/scripts/build-report.mjs
CHANGED
|
@@ -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
|
|
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,
|
|
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}`);
|