feat: 階段 5 wiki 知識庫(讀寫、10 分鐘快取、動態註冊指令)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 }
|
||||
}
|
||||
Reference in New Issue
Block a user