feat: 階段 5 wiki 知識庫(讀寫、10 分鐘快取、動態註冊指令)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 06:15:03 +00:00
co-authored by Claude Sonnet 5.5
parent c890059a50
commit 0c525d07b2
5 changed files with 379 additions and 3 deletions
+92
View File
@@ -0,0 +1,92 @@
// wiki 知識庫的純邏輯:目錄解析與渲染、頁名、驗證。讀寫與 git 在 register.tsx。
export type Entry = { domain: string; topic: string }
export const CACHE_MS = 10 * 60 * 1000
// 指令名稱只能用英數與底線,領域與主題也沿用。
const NAME_RE = /^[A-Za-z0-9_]+$/
export const validName = (s: string): boolean => NAME_RE.test(s)
export const commandName = (e: Entry): string => `${e.domain}-${e.topic}`
export const pageName = (e: Entry): string => `${e.domain}-${e.topic}`
// Home 的格式:「## 領域」章、「- [主題](頁名)」節。
export const parseCatalog = (home: string): Entry[] => {
const out: Entry[] = []
let domain = ''
for (const line of home.split('\n')) {
const h = /^##\s+(.+?)\s*$/.exec(line)
if (h) {
domain = h[1]!
continue
}
const m = /^\s*[-*]\s+\[(.+?)\]\((.+?)\)\s*$/.exec(line)
if (m && domain) out.push({ domain, topic: m[1]! })
}
return out
}
const group = (entries: Entry[]): Map<string, string[]> => {
const g = new Map<string, string[]>()
for (const e of entries) g.set(e.domain, [...(g.get(e.domain) ?? []), e.topic])
return g
}
export const addEntry = (entries: Entry[], e: Entry): Entry[] =>
entries.some(x => x.domain === e.domain && x.topic === e.topic) ? entries : [...entries, e]
export const renderHome = (entries: Entry[]): string => {
const lines = ['# 知識庫', '']
for (const [domain, topics] of group(entries)) {
lines.push(`## ${domain}`, '')
for (const t of topics) lines.push(`- [${t}](${pageName({ domain, topic: t })})`)
lines.push('')
}
return lines.join('\n')
}
export const renderSidebar = (entries: Entry[]): string => {
const lines = ['**[知識庫](Home)**', '']
for (const [domain, topics] of group(entries)) {
lines.push(`**${domain}**`, '')
for (const t of topics) lines.push(`- [${t}](${pageName({ domain, topic: t })})`)
lines.push('')
}
return lines.join('\n')
}
export const renderPage = (e: Entry, body: string, source: string): string =>
`# ${e.topic}\n\n> 領域:${e.domain}\n\n${body.trim()}\n\n---\n\n出處:${source}\n`
export const commitMessage = (e: Entry, existed: boolean): string =>
`feat: ${existed ? '更新' : '新增'}知識「${e.domain}/${e.topic}」`
// tea 設定檔(YAML)中,找出網址主機相符的登入的 token。
export const teaToken = (yaml: string, host: string): string | undefined => {
for (const block of yaml.split(/^\s*-\s+name:/m).slice(1)) {
const url = /^\s*url:\s*(\S+)/m.exec(block)?.[1]
const token = /^\s*token:\s*(\S+)/m.exec(block)?.[1]
if (!url || !token) continue
try {
if (new URL(url.replace(/^['"]|['"]$/g, '')).host === host) return token.replace(/^['"]|['"]$/g, '')
} catch {}
}
return undefined
}
export const wikiUrlFromRemote = (remote: string): string | undefined => {
const base = remote.trim().replace(/\.git$/, '')
return base ? `${base}.wiki.git` : undefined
}
// 指令參數:第一行「領域 主題 [出處]」,其後為內容。
export const parseWriteArgs = (args: string): { entry: Entry; source: string; body: string } | undefined => {
const [first = '', ...rest] = args.trim().split('\n')
const [domain, topic, ...src] = first.trim().split(/\s+/)
const body = rest.join('\n').trim()
if (!domain || !topic || !body || !validName(domain) || !validName(topic)) return undefined
return { entry: { domain, topic }, source: src.join(' ') || '使用者提供', body }
}