✨ feat(vscode): 代码阅读加「问 AI」,块下面问 Claude Code,问答存进 .spx 挂在块下
This commit is contained in:
1 parent
badc722640
commit
fb53bb727c
34 files changed
+2726
-43
No files matched your search
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* 代码问答的存法:每个工作区文件夹一份 `<workspace>/.spx/code-qa.json`,读写的规矩同文件注释(见 pathMap)。
|
||||
* 形状是 `{ "相对路径": [{ anchor, entries: [{ id, question, answer, createdAt, sessionId }] }] }`:
|
||||
* 一份文件里挂着问答的每一块一项(anchor 见 BlockAnchors),块下面问过的按时间排。
|
||||
*/
|
||||
|
||||
import type { PathMap, PathMapFile } from '../pathMap'
|
||||
|
||||
/** 问过的一次:问题、回答(Markdown)、什么时候问的(ISO 时间)、Claude Code 的会话 id。 */
|
||||
export interface QaEntry {
|
||||
id: string
|
||||
question: string
|
||||
answer: string
|
||||
createdAt: string
|
||||
sessionId: string
|
||||
}
|
||||
|
||||
/** 挂在一块上的问答。 */
|
||||
export interface QaThread {
|
||||
anchor: string
|
||||
entries: QaEntry[]
|
||||
}
|
||||
|
||||
export type QaMap = PathMap<QaThread[]>
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function text(value: unknown): string | undefined {
|
||||
return typeof value === 'string' && value.trim() !== '' ? value : undefined
|
||||
}
|
||||
|
||||
function parseEntry(value: unknown): QaEntry | undefined {
|
||||
if (!isRecord(value))
|
||||
return undefined
|
||||
const [id, question, answer, createdAt] = [text(value.id), text(value.question), text(value.answer), text(value.createdAt)]
|
||||
if (!id || !question || !answer || !createdAt || Number.isNaN(Date.parse(createdAt)))
|
||||
return undefined
|
||||
return { id, question, answer, createdAt, sessionId: typeof value.sessionId === 'string' ? value.sessionId : '' }
|
||||
}
|
||||
|
||||
/** 同一块的并成一项;块按记号排,问答按时间排:写出来的顺序稳定,git diff 干净。 */
|
||||
function tidy(threads: QaThread[]): QaThread[] {
|
||||
const byAnchor = new Map<string, QaEntry[]>()
|
||||
for (const thread of threads)
|
||||
byAnchor.set(thread.anchor, [...byAnchor.get(thread.anchor) ?? [], ...thread.entries])
|
||||
return [...byAnchor]
|
||||
.filter(([, entries]) => entries.length > 0)
|
||||
.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)
|
||||
.map(([anchor, entries]) => ({
|
||||
anchor,
|
||||
entries: [...entries].sort((a, b) => Date.parse(a.createdAt) - Date.parse(b.createdAt) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)),
|
||||
}))
|
||||
}
|
||||
|
||||
/** 一份文件的问答:不是数组、一项都认不出的当没有;认不出的块、问答跳过。 */
|
||||
function parseThreads(value: unknown): QaThread[] | undefined {
|
||||
if (!Array.isArray(value))
|
||||
return undefined
|
||||
const threads = value.flatMap((item): QaThread[] => {
|
||||
const anchor = isRecord(item) ? text(item.anchor) : undefined
|
||||
if (!anchor || !isRecord(item) || !Array.isArray(item.entries))
|
||||
return []
|
||||
return [{ anchor, entries: item.entries.flatMap(entry => parseEntry(entry) ?? []) }]
|
||||
})
|
||||
const tidied = tidy(threads)
|
||||
return tidied.length > 0 ? tidied : undefined
|
||||
}
|
||||
|
||||
export const QA_FILE: PathMapFile<QaThread[]> = {
|
||||
path: '.spx/code-qa.json',
|
||||
label: '代码问答',
|
||||
parse: parseThreads,
|
||||
}
|
||||
|
||||
/** 在 key 这份文件的 anchor 那块下面记一次问答。 */
|
||||
export function withEntry(map: QaMap, key: string, anchor: string, entry: QaEntry): QaMap {
|
||||
return { ...map, [key]: tidy([...map[key] ?? [], { anchor, entries: [entry] }]) }
|
||||
}
|
||||
|
||||
/** 删掉一次问答;块下面删光了这块就不挂了,文件里删光了这份文件就不记了。 */
|
||||
export function withoutEntry(map: QaMap, key: string, anchor: string, entryId: string): QaMap {
|
||||
const threads = tidy((map[key] ?? []).map(thread => thread.anchor === anchor
|
||||
? { anchor, entries: thread.entries.filter(entry => entry.id !== entryId) }
|
||||
: thread))
|
||||
const next = { ...map }
|
||||
delete next[key]
|
||||
return threads.length > 0 ? { ...next, [key]: threads } : next
|
||||
}
|
||||
Reference in new issue
Block a user