Skip to content

Commit 3ee1df1

Browse files
authored
Merge pull request #181 from InvolutionHell/feat/longsizhuo/giscus2docId
feat:为讨论加上title
2 parents 2d6d03f + ff3f8fb commit 3ee1df1

File tree

3 files changed

+286
-5
lines changed

3 files changed

+286
-5
lines changed

app/components/GiscusComments.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,21 @@ import Giscus from "@giscus/react";
55
interface GiscusCommentsProps {
66
className?: string;
77
docId?: string | null;
8+
title?: string | null;
89
}
910

10-
export function GiscusComments({ className, docId }: GiscusCommentsProps) {
11-
const useDocId = typeof docId === "string" && docId.trim().length > 0;
11+
export function GiscusComments({
12+
className,
13+
docId,
14+
title,
15+
}: GiscusCommentsProps) {
16+
const normalizedDocId = typeof docId === "string" ? docId.trim() : "";
17+
const normalizedTitle = typeof title === "string" ? title.trim() : "";
18+
19+
const useSpecificMapping = normalizedDocId.length > 0;
20+
const termValue = useSpecificMapping
21+
? `${normalizedTitle || "Untitled"} | ${normalizedDocId}`
22+
: undefined;
1223

1324
return (
1425
<div className={className}>
@@ -17,8 +28,8 @@ export function GiscusComments({ className, docId }: GiscusCommentsProps) {
1728
repoId="R_kgDOPuD_8A"
1829
category="Comments"
1930
categoryId="DIC_kwDOPuD_8M4Cvip8"
20-
mapping={useDocId ? "specific" : "pathname"}
21-
term={useDocId ? docId : undefined}
31+
mapping={useSpecificMapping ? "specific" : "pathname"}
32+
term={termValue}
2233
strict="0"
2334
reactionsEnabled="1"
2435
emitMetadata="0"

app/docs/[...slug]/page.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export default async function DocPage({ params }: Param) {
5757
const contributorsEntry =
5858
getDocContributorsByPath(page.file.path) ||
5959
getDocContributorsByDocId(docIdFromPage);
60+
const discussionTitle = page.data.title ?? docIdFromPage ?? page.path;
6061
const Mdx = page.data.body;
6162

6263
// Prepare page content for AI assistant
@@ -86,7 +87,10 @@ export default async function DocPage({ params }: Param) {
8687
<Mdx components={getMDXComponents()} />
8788
<Contributors entry={contributorsEntry} />
8889
<section className="mt-16">
89-
<GiscusComments docId={docIdFromPage ?? null} />
90+
<GiscusComments
91+
docId={docIdFromPage ?? null}
92+
title={discussionTitle}
93+
/>
9094
</section>
9195
</DocsBody>
9296
</DocsPage>

scripts/test.mjs

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
#!/usr/bin/env node
2+
/**
3+
* 将 GitHub Discussions 标题补上 [docId: <id>],用于从 pathname->docId 的 Giscus 迁移。
4+
*
5+
* 两种输入来源:
6+
* A) DB 模式(推荐):读取 Postgres(docs/path_current + doc_paths)获得每个 docId 的所有历史路径
7+
* B) 映射文件模式:传入 JSON 文件,手动提供 docId 与候选“旧 term”(通常是旧路径)
8+
*
9+
* 需要:
10+
* - GH_TOKEN(或者 GITHUB_TOKEN):具备 Discussions: read/write(fine-grained)或 repo 权限
11+
* - GITHUB_OWNER, GITHUB_REPO
12+
* - (可选)DATABASE_URL(启用 DB 模式)
13+
*
14+
* 用法示例:
15+
* # 仅预览(dry run,默认)
16+
* node scripts/migrate-giscus-add-docid.mjs --owner=InvolutionHell --repo=involutionhell.github.io
17+
*
18+
* # 真正执行(写入)
19+
* node scripts/migrate-giscus-add-docid.mjs --owner=InvolutionHell --repo=involutionhell.github.io --apply=true
20+
*
21+
* # 用映射文件(不连 DB)
22+
* node scripts/migrate-giscus-add-docid.mjs --map=tmp/discussion-map.json --apply=true
23+
*
24+
* 映射文件格式(示例):
25+
* {
26+
* "i0xmpsk...xls": ["app/docs/foo/bar.mdx", "/docs/foo/bar"],
27+
* "abcd123...": ["app/docs/baz.md"]
28+
* }
29+
*/
30+
31+
import "dotenv/config";
32+
import fs from "node:fs/promises";
33+
import path from "node:path";
34+
import process from "node:process";
35+
36+
// 可选:DB(Prisma)
37+
let prisma = null;
38+
try {
39+
const { PrismaClient } = await import("../generated/prisma/index.js");
40+
if (process.env.DATABASE_URL) {
41+
prisma = new PrismaClient();
42+
}
43+
} catch {
44+
// 没有 prisma 也可运行(映射文件模式)
45+
}
46+
47+
// Node18+ 自带 fetch
48+
const GH_TOKEN = process.env.GH_TOKEN || process.env.GITHUB_TOKEN || "";
49+
const OWNER = getArg("owner") || process.env.GITHUB_OWNER || "InvolutionHell";
50+
const REPO =
51+
getArg("repo") || process.env.GITHUB_REPO || "involutionhell.github.io";
52+
const MAP = getArg("map") || process.env.GISCUS_DISCUSSION_MAP || ""; // JSON 文件(映射文件模式)
53+
const APPLY = (getArg("apply") || "false").toLowerCase() === "true"; // 是否真的更新标题
54+
55+
if (!GH_TOKEN) {
56+
console.error("[migrate-giscus] Missing GH_TOKEN/GITHUB_TOKEN.");
57+
process.exit(1);
58+
}
59+
60+
function getArg(k) {
61+
const arg = process.argv.slice(2).find((s) => s.startsWith(`--${k}=`));
62+
return arg ? arg.split("=")[1] : null;
63+
}
64+
65+
const GQL = "https://api.github.com/graphql";
66+
const ghHeaders = {
67+
"Content-Type": "application/json",
68+
Authorization: `Bearer ${GH_TOKEN}`,
69+
"User-Agent": "giscus-docid-migrator",
70+
};
71+
72+
// 简单日志
73+
const log = (...a) => console.log("[migrate-giscus]", ...a);
74+
75+
// GraphQL helpers
76+
async function ghQuery(query, variables) {
77+
const res = await fetch(GQL, {
78+
method: "POST",
79+
headers: ghHeaders,
80+
body: JSON.stringify({ query, variables }),
81+
});
82+
if (!res.ok) {
83+
const text = await res.text();
84+
throw new Error(
85+
`GitHub GraphQL failed: ${res.status} ${res.statusText} -> ${text}`,
86+
);
87+
}
88+
const json = await res.json();
89+
if (json.errors) {
90+
throw new Error(`GraphQL errors: ${JSON.stringify(json.errors)}`);
91+
}
92+
return json.data;
93+
}
94+
95+
const Q_SEARCH_DISCUSSIONS = `
96+
query SearchDiscussions($q: String!) {
97+
search(query: $q, type: DISCUSSION, first: 20) {
98+
nodes {
99+
... on Discussion {
100+
id
101+
number
102+
title
103+
url
104+
category { id name }
105+
repository { nameWithOwner }
106+
}
107+
}
108+
}
109+
}
110+
`;
111+
112+
const M_UPDATE_DISCUSSION = `
113+
mutation UpdateDiscussion($id: ID!, $title: String!) {
114+
updateDiscussion(input: { discussionId: $id, title: $title }) {
115+
discussion { id number title url }
116+
}
117+
}
118+
`;
119+
120+
// 读取输入来源:DB 或 映射文件
121+
async function loadDocIdTerms() {
122+
// 优先 DB
123+
if (prisma) {
124+
log("Loading doc paths from DB…");
125+
const docs = await prisma.docs.findMany({
126+
select: {
127+
id: true,
128+
path_current: true,
129+
doc_paths: { select: { path: true } },
130+
},
131+
});
132+
const map = new Map(); // docId -> Set<term>
133+
for (const d of docs) {
134+
const set = map.get(d.id) ?? new Set();
135+
if (d.path_current) set.add(d.path_current);
136+
for (const p of d.doc_paths) if (p?.path) set.add(p.path);
137+
// 兼容站点实际的 pathname(可选添加去掉扩展名、加前缀)
138+
for (const p of Array.from(set)) {
139+
const noExt = p.replace(/\.(md|mdx|markdown)$/i, "");
140+
set.add(noExt);
141+
set.add(`/${noExt}`); // 常见 pathname 形态
142+
}
143+
map.set(d.id, set);
144+
}
145+
return map;
146+
}
147+
148+
// 退化:映射文件模式
149+
if (MAP) {
150+
const abs = path.resolve(process.cwd(), MAP);
151+
const raw = await fs.readFile(abs, "utf8");
152+
const obj = JSON.parse(raw);
153+
const map = new Map();
154+
for (const [docId, arr] of Object.entries(obj)) {
155+
const set = new Set();
156+
(arr || []).forEach((t) => {
157+
if (typeof t === "string" && t.trim()) {
158+
set.add(t.trim());
159+
const noExt = t.replace(/\.(md|mdx|markdown)$/i, "");
160+
set.add(noExt);
161+
set.add(`/${noExt}`);
162+
}
163+
});
164+
map.set(docId, set);
165+
}
166+
return map;
167+
}
168+
169+
throw new Error("No DATABASE_URL (DB 模式) and no --map JSON provided.");
170+
}
171+
172+
// 搜索一个 term 对应的讨论(尽量限定到你的仓库)
173+
async function searchDiscussionByTerm(term) {
174+
// GitHub 搜索语法:repo:OWNER/REPO in:title <term>
175+
const q = `${term} repo:${OWNER}/${REPO} in:title`;
176+
const data = await ghQuery(Q_SEARCH_DISCUSSIONS, { q });
177+
const nodes = data?.search?.nodes || [];
178+
// 过滤到目标仓库的讨论(双重保险)
179+
return nodes.filter(
180+
(n) =>
181+
n?.repository?.nameWithOwner?.toLowerCase() ===
182+
`${OWNER}/${REPO}`.toLowerCase(),
183+
);
184+
}
185+
186+
// 如果标题中已经包含 [docId: xxx],就跳过
187+
function alreadyHasDocIdTag(title, docId) {
188+
const tag = `[docId:${docId}]`;
189+
return title.includes(tag);
190+
}
191+
192+
// 生成新标题(在末尾追加,如已含则不变)
193+
function appendDocIdTag(title, docId) {
194+
const tag = `[docId:${docId}]`;
195+
if (title.includes(tag)) return title;
196+
// 避免标题太挤,加个空格
197+
return `${title.trim()} ${tag}`;
198+
}
199+
200+
async function main() {
201+
log(
202+
`Target repo: ${OWNER}/${REPO} | Mode: ${prisma ? "DB" : MAP ? "MAP" : "UNKNOWN"}`,
203+
);
204+
const docIdToTerms = await loadDocIdTerms();
205+
206+
let updated = 0,
207+
skipped = 0,
208+
notFound = 0,
209+
examined = 0;
210+
211+
for (const [docId, termsSet] of docIdToTerms) {
212+
const terms = Array.from(termsSet);
213+
let matched = null;
214+
215+
// 尝试每个 term,直到命中一个讨论
216+
for (const term of terms) {
217+
const hits = await searchDiscussionByTerm(term);
218+
// 多命中:优先那些标题更“像”旧路径的;简单按包含度/长度排序
219+
const scored = hits
220+
.map((d) => ({ d, score: d.title.includes(term) ? term.length : 0 }))
221+
.sort((a, b) => b.score - a.score);
222+
223+
if (scored.length > 0) {
224+
matched = scored[0].d;
225+
break;
226+
}
227+
}
228+
229+
if (!matched) {
230+
notFound += 1;
231+
log(`⚠️ docId=${docId} 未找到旧讨论(terms=${terms.join(", ")})`);
232+
continue;
233+
}
234+
235+
examined += 1;
236+
237+
const oldTitle = matched.title;
238+
if (alreadyHasDocIdTag(oldTitle, docId)) {
239+
skipped += 1;
240+
log(`⏭ #${matched.number} 已包含 docId:${matched.url}`);
241+
continue;
242+
}
243+
244+
const newTitle = appendDocIdTag(oldTitle, docId);
245+
log(
246+
`${APPLY ? "✏️ 更新" : "👀 预览"} #${matched.number} "${oldTitle}" → "${newTitle}"`,
247+
);
248+
249+
if (APPLY) {
250+
await ghQuery(M_UPDATE_DISCUSSION, { id: matched.id, title: newTitle });
251+
updated += 1;
252+
}
253+
}
254+
255+
log(
256+
`Done. examined=${examined}, updated=${updated}, skipped=${skipped}, notFound=${notFound}`,
257+
);
258+
259+
if (prisma) await prisma.$disconnect();
260+
}
261+
262+
main().catch(async (e) => {
263+
console.error(e);
264+
if (prisma) await prisma.$disconnect();
265+
process.exit(1);
266+
});

0 commit comments

Comments
 (0)