@itradingai/aiwiki 0.2.10 → 0.2.12

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.
@@ -0,0 +1,197 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { frontmatterArray, frontmatterBoolean, frontmatterString, parseMarkdown } from "./frontmatter.js";
4
+ import { relativePath, safeJoin } from "./paths.js";
5
+ import { exists } from "./workspace.js";
6
+ export async function lintWorkspace(rootPath, now = new Date().toISOString()) {
7
+ const root = path.resolve(rootPath);
8
+ const wikiEntries = await readNotes(root, "05-wiki/source-knowledge");
9
+ const sourceCards = await readNotes(root, "03-sources/article-cards");
10
+ const rawFiles = await readNotes(root, "02-raw/articles");
11
+ const runs = await runDirs(root);
12
+ const allNotes = [
13
+ ...wikiEntries,
14
+ ...sourceCards,
15
+ ...rawFiles,
16
+ ...await readNotes(root, "04-claims/_suggestions"),
17
+ ...await readNotes(root, "06-assets/_suggestions"),
18
+ ...await readNotes(root, "07-topics/ready"),
19
+ ...await readNotes(root, "08-outputs/outlines")
20
+ ];
21
+ const issues = [];
22
+ const wikiSourceCards = new Set(wikiEntries.map((note) => frontmatterString(note.frontmatter, "source_card")).filter(Boolean));
23
+ for (const card of sourceCards) {
24
+ if (!wikiSourceCards.has(card.path)) {
25
+ issues.push({
26
+ severity: "warning",
27
+ path: card.path,
28
+ message: "Source Card 没有对应 Wiki Entry。",
29
+ suggestion: `重新入库或生成 05-wiki/source-knowledge/${path.basename(card.path)}`
30
+ });
31
+ }
32
+ }
33
+ for (const entry of wikiEntries) {
34
+ const sourceCard = frontmatterString(entry.frontmatter, "source_card");
35
+ const rawFile = frontmatterString(entry.frontmatter, "raw_file");
36
+ const mode = frontmatterString(entry.frontmatter, "generation_mode");
37
+ if (!sourceCard) {
38
+ issues.push({ severity: "warning", path: entry.path, message: "Wiki Entry 缺少 source_card。", suggestion: "补写 source_card vault 路径。" });
39
+ }
40
+ if (!rawFile) {
41
+ issues.push({ severity: "warning", path: entry.path, message: "Wiki Entry 缺少 raw_file。", suggestion: "补写 raw_file vault 路径。" });
42
+ }
43
+ if (mode === "deterministic_fallback") {
44
+ issues.push({ severity: "info", path: entry.path, message: "Wiki Entry 是 deterministic fallback,仅包含来源和正文预览。", suggestion: "让宿主 Agent 基于原文补充 analysis 或 wiki_entry。" });
45
+ const createdAt = Date.parse(frontmatterString(entry.frontmatter, "created_at") ?? "");
46
+ if (Number.isFinite(createdAt) && Date.parse(now) - createdAt > 7 * 24 * 60 * 60 * 1000) {
47
+ issues.push({ severity: "warning", path: entry.path, message: "fallback Wiki Entry 超过 7 天未补全。", suggestion: "重新运行宿主 Agent 生成 enriched Wiki Entry。" });
48
+ }
49
+ }
50
+ if (mode === "agent_enriched") {
51
+ const hasSummary = /## 一句话总结/.test(entry.body) || Boolean(frontmatterString(entry.frontmatter, "summary"));
52
+ const hasKeyPoints = /## 核心观点[\s\S]*-\s+/.test(entry.body);
53
+ if (!hasSummary) {
54
+ issues.push({ severity: "warning", path: entry.path, message: "agent_enriched Wiki Entry 缺少 summary。", suggestion: "让宿主 Agent 提供 analysis.summary。" });
55
+ }
56
+ if (!hasKeyPoints) {
57
+ issues.push({ severity: "warning", path: entry.path, message: "agent_enriched Wiki Entry 缺少 key_points。", suggestion: "让宿主 Agent 提供 analysis.key_points。" });
58
+ }
59
+ }
60
+ if (frontmatterBoolean(entry.frontmatter, "grounding_needs_review") === true) {
61
+ const markers = frontmatterArray(entry.frontmatter, "grounding_markers");
62
+ issues.push({
63
+ severity: "warning",
64
+ path: entry.path,
65
+ message: `Wiki Entry 需要 grounding 复核${markers.length ? `: ${markers.join(", ")}` : "。"}`,
66
+ suggestion: "检查 source_quote 是否能在 Raw 中找到;coverage_suspected_incomplete 仅代表启发式疑似风险。"
67
+ });
68
+ }
69
+ if (frontmatterBoolean(entry.frontmatter, "represents_user_view") === true && frontmatterString(entry.frontmatter, "source_role") !== "output") {
70
+ issues.push({ severity: "warning", path: entry.path, message: "只有 output 角色应标记为代表用户观点。", suggestion: "将 represents_user_view 改为 false,或将 source_role 改为 output。" });
71
+ }
72
+ }
73
+ issues.push(...duplicateIssues(sourceCards, "source_url", "重复 URL"));
74
+ issues.push(...duplicateTitles(allNotes));
75
+ issues.push(...brokenLinkIssues(root, allNotes));
76
+ return {
77
+ generated_at: now,
78
+ summary: {
79
+ wiki_entries: wikiEntries.length,
80
+ source_cards: sourceCards.length,
81
+ raw_files: rawFiles.length,
82
+ runs: runs.length
83
+ },
84
+ issues
85
+ };
86
+ }
87
+ export async function writeLintReport(rootPath, report) {
88
+ const root = path.resolve(rootPath);
89
+ const target = safeJoin(root, "dashboards", "Lint Report.md");
90
+ await fs.mkdir(path.dirname(target), { recursive: true });
91
+ await fs.writeFile(target, renderLintReport(report), "utf8");
92
+ return relativePath(root, target);
93
+ }
94
+ export function renderLintReport(report) {
95
+ return [
96
+ "# AIWiki Lint Report",
97
+ "",
98
+ `Generated at: ${report.generated_at}`,
99
+ "",
100
+ "## Summary",
101
+ "",
102
+ `- Wiki Entries: ${report.summary.wiki_entries}`,
103
+ `- Source Cards: ${report.summary.source_cards}`,
104
+ `- Raw Files: ${report.summary.raw_files}`,
105
+ `- Runs: ${report.summary.runs}`,
106
+ "",
107
+ "## Issues",
108
+ "",
109
+ ...(report.issues.length ? report.issues.map((issue) => {
110
+ const suffix = issue.path ? ` (${issue.path})` : "";
111
+ const suggestion = issue.suggestion ? `\n - Suggested Fix: ${issue.suggestion}` : "";
112
+ return `- [${issue.severity}] ${issue.message}${suffix}${suggestion}`;
113
+ }) : ["- none"]),
114
+ ""
115
+ ].join("\n");
116
+ }
117
+ async function readNotes(root, dir) {
118
+ const absolute = path.join(root, dir);
119
+ if (!(await exists(absolute))) {
120
+ return [];
121
+ }
122
+ const files = await listMarkdownFiles(absolute);
123
+ return Promise.all(files.map(async (file) => {
124
+ const text = await fs.readFile(file, "utf8");
125
+ const parsed = parseMarkdown(text);
126
+ return {
127
+ path: relativePath(root, file),
128
+ title: frontmatterString(parsed.frontmatter, "title") ?? path.basename(file, ".md"),
129
+ sourceUrl: frontmatterString(parsed.frontmatter, "source_url") ?? "",
130
+ body: parsed.body,
131
+ frontmatter: parsed.frontmatter
132
+ };
133
+ }));
134
+ }
135
+ async function runDirs(root) {
136
+ const dir = path.join(root, "09-runs");
137
+ if (!(await exists(dir))) {
138
+ return [];
139
+ }
140
+ return (await fs.readdir(dir, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
141
+ }
142
+ async function listMarkdownFiles(dir) {
143
+ const entries = await fs.readdir(dir, { withFileTypes: true });
144
+ const files = [];
145
+ for (const entry of entries) {
146
+ const target = path.join(dir, entry.name);
147
+ if (entry.isDirectory()) {
148
+ files.push(...await listMarkdownFiles(target));
149
+ }
150
+ else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
151
+ files.push(target);
152
+ }
153
+ }
154
+ return files;
155
+ }
156
+ function duplicateIssues(notes, field, label) {
157
+ const seen = new Map();
158
+ for (const note of notes) {
159
+ const value = field === "source_url" ? note.sourceUrl : "";
160
+ if (!value) {
161
+ continue;
162
+ }
163
+ seen.set(value, [...(seen.get(value) ?? []), note.path]);
164
+ }
165
+ return Array.from(seen.entries()).flatMap(([value, paths]) => paths.length > 1
166
+ ? [{ severity: "warning", message: `${label}: ${value}`, suggestion: paths.join(", ") }]
167
+ : []);
168
+ }
169
+ function duplicateTitles(notes) {
170
+ const seen = new Map();
171
+ for (const note of notes) {
172
+ if (!note.title) {
173
+ continue;
174
+ }
175
+ seen.set(note.title, [...(seen.get(note.title) ?? []), note.path]);
176
+ }
177
+ return Array.from(seen.entries()).flatMap(([title, paths]) => paths.length > 1
178
+ ? [{ severity: "info", message: `重复标题: ${title}`, suggestion: paths.join(", ") }]
179
+ : []);
180
+ }
181
+ function brokenLinkIssues(root, notes) {
182
+ const existing = new Set(notes.map((note) => note.path.replace(/\.md$/i, "")));
183
+ const issues = [];
184
+ for (const note of notes) {
185
+ for (const link of note.body.matchAll(/\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g)) {
186
+ const target = link[1].replace(/\\/g, "/").replace(/\.md$/i, "");
187
+ if (!existing.has(target) && !isRunLocalLink(target)) {
188
+ issues.push({ severity: "error", path: note.path, message: `内部链接断裂: ${target}`, suggestion: "检查目标文件是否存在或更新 wikilink。" });
189
+ }
190
+ }
191
+ }
192
+ return issues;
193
+ }
194
+ function isRunLocalLink(target) {
195
+ // Run-local notes are valid trace links but are not part of the long-term note set.
196
+ return target.startsWith("09-runs/");
197
+ }
@@ -35,12 +35,14 @@ export function normalizePayload(raw, runStartedAt) {
35
35
  if (!kind) {
36
36
  throw new Error("source.kind is required");
37
37
  }
38
+ const sourceRole = normalizeSourceRole(stringValue(sourceRaw.source_role) ?? stringValue(sourceRaw.role), warnings);
39
+ const representsUserView = booleanValue(sourceRaw.represents_user_view) ?? sourceRole === "output";
38
40
  const requestRaw = isRecord(raw.request) ? raw.request : {};
39
41
  const requestedOutputs = Array.isArray(requestRaw.outputs)
40
42
  ? requestRaw.outputs.filter((item) => typeof item === "string")
41
43
  : ["source_card", "creative_assets", "topics", "draft_outline", "processing_summary"];
42
- const outputs = ["source_card", "creative_assets", "topics", "draft_outline", "processing_summary"];
43
- if (fetchStatus !== "failed" && requestedOutputs.length && requestedOutputs.length !== outputs.length) {
44
+ const outputs = ["source_card", "wiki_entry", "creative_assets", "topics", "draft_outline", "processing_summary"];
45
+ if (fetchStatus !== "failed" && requestedOutputs.length && hasCustomOutputRequest(requestedOutputs)) {
44
46
  warnings.push("AIWiki 会为每条输入生成完整资料产物,request.outputs 已按全量输出处理。");
45
47
  }
46
48
  if (typeof raw.target_kb === "string" && raw.target_kb.trim()) {
@@ -55,6 +57,8 @@ export function normalizePayload(raw, runStartedAt) {
55
57
  if (fetchNotesRepair.repaired) {
56
58
  warnings.push("source.fetch_notes 检测到疑似 UTF-8 mojibake,已自动修复。");
57
59
  }
60
+ const analysis = normalizeAnalysis(raw.analysis, warnings);
61
+ const wikiEntry = normalizeWikiEntry(raw.wiki_entry, warnings);
58
62
  if (fetchStatus === "failed" && content?.trim()) {
59
63
  throw new Error("source.content must be empty when source.fetch_status is failed");
60
64
  }
@@ -63,6 +67,8 @@ export function normalizePayload(raw, runStartedAt) {
63
67
  target_kb: stringValue(raw.target_kb),
64
68
  source: {
65
69
  kind,
70
+ source_role: sourceRole,
71
+ represents_user_view: representsUserView,
66
72
  url: stringValue(sourceRaw.url),
67
73
  title: titleRepair.value,
68
74
  author: stringValue(sourceRaw.author),
@@ -80,9 +86,21 @@ export function normalizePayload(raw, runStartedAt) {
80
86
  outputs,
81
87
  language: stringValue(requestRaw.language) ?? stringValue(sourceRaw.language)
82
88
  },
89
+ analysis,
90
+ wiki_entry: wikiEntry,
83
91
  warnings
84
92
  };
85
93
  }
94
+ function normalizeSourceRole(value, warnings) {
95
+ if (!value) {
96
+ return "input";
97
+ }
98
+ if (value === "input" || value === "processing" || value === "output") {
99
+ return value;
100
+ }
101
+ warnings.push(`source_role 已忽略:${value} 不是 input、processing 或 output。`);
102
+ return "input";
103
+ }
86
104
  function normalizeFetchStatus(value, content) {
87
105
  if (value === "failed") {
88
106
  return "failed";
@@ -99,10 +117,150 @@ function rejectWriteControlFields(raw) {
99
117
  if (typeof raw.output_file === "string") {
100
118
  throw new Error("payload must not control output paths");
101
119
  }
120
+ if (isRecord(raw.wiki_entry) && typeof raw.wiki_entry.path === "string") {
121
+ throw new Error("payload must not control output paths");
122
+ }
102
123
  }
103
124
  function stringValue(value) {
104
125
  return typeof value === "string" ? value : undefined;
105
126
  }
127
+ function booleanValue(value) {
128
+ return typeof value === "boolean" ? value : undefined;
129
+ }
106
130
  function isRecord(value) {
107
131
  return typeof value === "object" && value !== null && !Array.isArray(value);
108
132
  }
133
+ function normalizeAnalysis(value, warnings) {
134
+ if (value === undefined) {
135
+ return undefined;
136
+ }
137
+ if (!isRecord(value)) {
138
+ warnings.push("analysis 已忽略:必须是对象。");
139
+ return undefined;
140
+ }
141
+ const analysis = {
142
+ summary: stringValue(value.summary),
143
+ key_points: stringArray(value.key_points, "analysis.key_points", warnings),
144
+ reusable_knowledge: reusableKnowledgeArray(value.reusable_knowledge, warnings),
145
+ related_concepts: stringArray(value.related_concepts, "analysis.related_concepts", warnings),
146
+ use_cases: stringArray(value.use_cases, "analysis.use_cases", warnings),
147
+ topic_candidates: stringArray(value.topic_candidates, "analysis.topic_candidates", warnings),
148
+ claims: claimsArray(value.claims, warnings),
149
+ outline: outlineValue(value.outline, warnings)
150
+ };
151
+ return hasAnalysisContent(analysis) ? analysis : undefined;
152
+ }
153
+ function normalizeWikiEntry(value, warnings) {
154
+ if (value === undefined) {
155
+ return undefined;
156
+ }
157
+ if (!isRecord(value)) {
158
+ warnings.push("wiki_entry 已忽略:必须是对象。");
159
+ return undefined;
160
+ }
161
+ const entry = {
162
+ title: stringValue(value.title),
163
+ summary: stringValue(value.summary),
164
+ sections: wikiSections(value.sections, warnings),
165
+ markdown: stringValue(value.markdown)
166
+ };
167
+ return entry.title || entry.summary || entry.sections.length || entry.markdown ? entry : undefined;
168
+ }
169
+ function stringArray(value, field, warnings) {
170
+ if (value === undefined) {
171
+ return [];
172
+ }
173
+ if (!Array.isArray(value)) {
174
+ warnings.push(`${field} 已忽略:必须是字符串数组。`);
175
+ return [];
176
+ }
177
+ return value.filter((item) => typeof item === "string" && item.trim().length > 0);
178
+ }
179
+ function reusableKnowledgeArray(value, warnings) {
180
+ if (value === undefined) {
181
+ return [];
182
+ }
183
+ if (!Array.isArray(value)) {
184
+ warnings.push("analysis.reusable_knowledge 已忽略:必须是数组。");
185
+ return [];
186
+ }
187
+ return value.flatMap((item) => {
188
+ if (typeof item === "string" && item.trim()) {
189
+ return [{ content: item.trim() }];
190
+ }
191
+ if (isRecord(item) && typeof item.content === "string" && item.content.trim()) {
192
+ return [{ title: stringValue(item.title), content: item.content.trim(), source_quote: stringValue(item.source_quote) }];
193
+ }
194
+ return [];
195
+ });
196
+ }
197
+ function claimsArray(value, warnings) {
198
+ if (value === undefined) {
199
+ return [];
200
+ }
201
+ if (!Array.isArray(value)) {
202
+ warnings.push("analysis.claims 已忽略:必须是数组。");
203
+ return [];
204
+ }
205
+ return value.flatMap((item) => {
206
+ if (typeof item === "string" && item.trim()) {
207
+ return [{ claim: item.trim() }];
208
+ }
209
+ if (isRecord(item) && typeof item.claim === "string" && item.claim.trim()) {
210
+ return [{
211
+ claim: item.claim.trim(),
212
+ confidence: stringValue(item.confidence),
213
+ source_quote: stringValue(item.source_quote)
214
+ }];
215
+ }
216
+ return [];
217
+ });
218
+ }
219
+ function outlineValue(value, warnings) {
220
+ if (value === undefined) {
221
+ return undefined;
222
+ }
223
+ if (!isRecord(value)) {
224
+ warnings.push("analysis.outline 已忽略:必须是对象。");
225
+ return undefined;
226
+ }
227
+ const sections = stringArray(value.sections, "analysis.outline.sections", warnings);
228
+ if (!sections.length && !stringValue(value.title)) {
229
+ return undefined;
230
+ }
231
+ return { title: stringValue(value.title), sections };
232
+ }
233
+ function wikiSections(value, warnings) {
234
+ if (value === undefined) {
235
+ return [];
236
+ }
237
+ if (!Array.isArray(value)) {
238
+ warnings.push("wiki_entry.sections 已忽略:必须是数组。");
239
+ return [];
240
+ }
241
+ return value.flatMap((item) => {
242
+ if (!isRecord(item) || typeof item.heading !== "string" || !item.heading.trim()) {
243
+ return [];
244
+ }
245
+ return [{
246
+ heading: item.heading.trim(),
247
+ items: stringArray(item.items, "wiki_entry.sections.items", warnings)
248
+ }];
249
+ });
250
+ }
251
+ function hasAnalysisContent(analysis) {
252
+ return Boolean(analysis.summary ||
253
+ analysis.key_points.length ||
254
+ analysis.reusable_knowledge.length ||
255
+ analysis.related_concepts.length ||
256
+ analysis.use_cases.length ||
257
+ analysis.topic_candidates.length ||
258
+ analysis.claims.length ||
259
+ analysis.outline);
260
+ }
261
+ function hasCustomOutputRequest(outputs) {
262
+ const legacyDefault = ["source_card", "creative_assets", "topics", "draft_outline", "processing_summary"];
263
+ const currentDefault = ["source_card", "wiki_entry", "creative_assets", "topics", "draft_outline", "processing_summary"];
264
+ const sameSet = (left, right) => left.length === right.length && left.every((item) => right.includes(item));
265
+ return !sameSet(outputs, legacyDefault) && !sameSet(outputs, currentDefault);
266
+ }
@@ -0,0 +1,180 @@
1
+ import { buildGroundingReport, groundingFrontmatterLines } from "./grounding.js";
2
+ export function renderWikiEntry(payload, links) {
3
+ const enriched = Boolean(payload.wiki_entry || payload.analysis);
4
+ const mode = enriched ? "agent_enriched" : "deterministic_fallback";
5
+ const quality = enriched ? "enriched" : "scaffold";
6
+ const title = payload.wiki_entry?.title ?? payload.source.title ?? "Untitled";
7
+ const grounding = buildGroundingReport(payload);
8
+ const frontmatter = wikiFrontmatter(payload, links, title, mode, quality, grounding);
9
+ const body = enriched ? enrichedBody(payload, links, title, grounding) : fallbackBody(payload, links, title, grounding);
10
+ return {
11
+ mode,
12
+ quality,
13
+ markdown: `${frontmatter}\n${body}`
14
+ };
15
+ }
16
+ function wikiFrontmatter(payload, links, title, mode, quality, grounding) {
17
+ return [
18
+ "---",
19
+ `aiwiki_id: "${escapeYaml(`${links.slug}:wiki-entry`)}"`,
20
+ `type: "wiki_entry"`,
21
+ `wiki_type: "${wikiType(payload)}"`,
22
+ `source_role: "${payload.source.source_role}"`,
23
+ `represents_user_view: ${payload.source.represents_user_view ? "true" : "false"}`,
24
+ `status: "active"`,
25
+ `generation_mode: "${mode}"`,
26
+ `quality: "${quality}"`,
27
+ `generated_by: "${mode === "agent_enriched" ? "host_agent" : "aiwiki_cli"}"`,
28
+ `llm_enriched: ${mode === "agent_enriched" ? "true" : "false"}`,
29
+ `title: "${escapeYaml(title)}"`,
30
+ `slug: "${escapeYaml(links.slug)}"`,
31
+ `source_url: "${escapeYaml(payload.source.url ?? "")}"`,
32
+ `source_type: "${escapeYaml(payload.source.kind)}"`,
33
+ `source_card: "${escapeYaml(links.sourceCard)}"`,
34
+ `raw_file: "${escapeYaml(links.raw)}"`,
35
+ `claims_file: "${escapeYaml(links.claims)}"`,
36
+ `topics_file: "${escapeYaml(links.topics)}"`,
37
+ `outline_file: "${escapeYaml(links.outline)}"`,
38
+ `run_summary: "${escapeYaml(links.runSummary)}"`,
39
+ `run_id: "${escapeYaml(links.runId)}"`,
40
+ `created_at: "${escapeYaml(links.createdAt)}"`,
41
+ `updated_at: "${escapeYaml(links.createdAt)}"`,
42
+ ...(mode === "agent_enriched" ? [`summary: "${escapeYaml(payload.wiki_entry?.summary ?? payload.analysis?.summary ?? "")}"`] : []),
43
+ `topics: ${yamlStringArray(payload.analysis?.related_concepts ?? [])}`,
44
+ `claims: ${yamlStringArray(payload.analysis?.claims.map((claim) => claim.claim) ?? [])}`,
45
+ ...groundingFrontmatterLines(grounding),
46
+ `tags: ["aiwiki/wiki-entry"]`,
47
+ "---"
48
+ ].join("\n");
49
+ }
50
+ function wikiType(payload) {
51
+ if (payload.source.source_role === "output") {
52
+ return "personal_knowledge";
53
+ }
54
+ if (payload.source.source_role === "processing") {
55
+ return "thought_note";
56
+ }
57
+ return "source_knowledge";
58
+ }
59
+ function enrichedBody(payload, links, title, grounding) {
60
+ const sections = [`# ${title}`, ""];
61
+ appendGroundingReview(sections, grounding);
62
+ if (payload.wiki_entry?.summary || payload.analysis?.summary) {
63
+ sections.push("## 一句话总结", "", payload.wiki_entry?.summary ?? payload.analysis?.summary ?? "", "");
64
+ }
65
+ if (payload.wiki_entry?.markdown?.trim()) {
66
+ sections.push(payload.wiki_entry.markdown.trim(), "");
67
+ if (payload.wiki_entry.sections.length) {
68
+ for (const section of payload.wiki_entry.sections) {
69
+ if (bodyHasHeading(payload.wiki_entry.markdown, section.heading)) {
70
+ continue;
71
+ }
72
+ sections.push(`## ${section.heading}`, "", ...listOrFallback(section.items, "待宿主 Agent 补充。"), "");
73
+ }
74
+ }
75
+ sections.push(sourceSection(links));
76
+ return sections.join("\n");
77
+ }
78
+ if (payload.wiki_entry?.sections.length) {
79
+ for (const section of payload.wiki_entry.sections) {
80
+ sections.push(`## ${section.heading}`, "", ...listOrFallback(section.items, "待宿主 Agent 补充。"), "");
81
+ }
82
+ }
83
+ else {
84
+ sections.push("## 核心观点", "", ...listOrFallback(payload.analysis?.key_points ?? [], "待宿主 Agent 补充。"), "");
85
+ sections.push("## 可复用知识点", "", ...knowledgeList(payload), "");
86
+ sections.push("## 相关概念", "", ...listOrFallback(payload.analysis?.related_concepts ?? [], "待宿主 Agent 补充。"), "");
87
+ sections.push("## 适合用于什么场景", "", ...listOrFallback(payload.analysis?.use_cases ?? [], "待宿主 Agent 补充。"), "");
88
+ sections.push("## 可转化的选题", "", ...listOrFallback(payload.analysis?.topic_candidates ?? [], "待宿主 Agent 补充。"), "");
89
+ }
90
+ sections.push(sourceSection(links));
91
+ return sections.join("\n");
92
+ }
93
+ function bodyHasHeading(markdown, heading) {
94
+ const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
95
+ return new RegExp(`^#{1,6}\\s+${escaped}\\s*$`, "m").test(markdown);
96
+ }
97
+ function fallbackBody(payload, links, title, grounding) {
98
+ const preview = trimPreview(payload.source.content ?? payload.source.fetch_notes ?? "", 1000);
99
+ return [
100
+ `# ${title}`,
101
+ "",
102
+ "## 说明",
103
+ "",
104
+ "这是 AIWiki 根据原文和元数据生成的基础 Wiki 条目。当前条目未经过宿主 Agent 的深度分析,仅用于建立知识库索引、来源追踪和后续 Query。",
105
+ "",
106
+ "## 来源信息",
107
+ "",
108
+ `- 原文链接:${payload.source.url ?? "无"}`,
109
+ `- Source Card:${obsidianLink(links.sourceCard, "资料卡")}`,
110
+ `- Raw:${obsidianLink(links.raw, "原文")}`,
111
+ `- Run:${obsidianLink(links.runSummary, "处理记录")}`,
112
+ "",
113
+ "## 内容预览",
114
+ "",
115
+ preview ? blockquote(preview) : "暂无可用正文预览。",
116
+ "",
117
+ "## 待 Agent 补全",
118
+ "",
119
+ "以下内容需要宿主 Agent 基于原文进一步生成:",
120
+ "",
121
+ "- 一句话总结",
122
+ "- 核心观点",
123
+ "- 可复用知识点",
124
+ "- 相关概念",
125
+ "- 适用场景",
126
+ "- 可转化选题",
127
+ "",
128
+ ...groundingReviewLines(grounding),
129
+ "",
130
+ sourceSection(links)
131
+ ].join("\n");
132
+ }
133
+ function knowledgeList(payload) {
134
+ const items = payload.analysis?.reusable_knowledge ?? [];
135
+ if (!items.length) {
136
+ return ["待宿主 Agent 补充。"];
137
+ }
138
+ return items.flatMap((item) => item.title ? [`### ${item.title}`, "", item.content] : [`- ${item.content}`]);
139
+ }
140
+ function listOrFallback(values, fallback) {
141
+ return values.length ? values.map((value) => `- ${value}`) : [fallback];
142
+ }
143
+ function sourceSection(links) {
144
+ return [
145
+ "## 来源",
146
+ "",
147
+ `- Source Card: ${obsidianLink(links.sourceCard, "资料卡")}`,
148
+ `- Raw: ${obsidianLink(links.raw, "原文")}`,
149
+ `- Run: ${obsidianLink(links.runSummary, "处理记录")}`
150
+ ].join("\n");
151
+ }
152
+ function appendGroundingReview(sections, grounding) {
153
+ if (!grounding.needs_review) {
154
+ return;
155
+ }
156
+ sections.push("## Grounding 复核", "", ...groundingReviewLines(grounding), "");
157
+ }
158
+ function groundingReviewLines(grounding) {
159
+ return [
160
+ `- 证据通道:${grounding.evidence_channel}`,
161
+ `- 需要复核:${grounding.needs_review ? "yes" : "no"}`,
162
+ `- 疑似标记:${grounding.suspicion_markers.length ? grounding.suspicion_markers.join(", ") : "none"}`,
163
+ `- Claim 引用覆盖:${grounding.claim_quote_count}/${grounding.claim_count}`
164
+ ];
165
+ }
166
+ function obsidianLink(vaultPath, label) {
167
+ return `[[${vaultPath.replace(/\\/g, "/").replace(/\.md$/i, "")}|${label}]]`;
168
+ }
169
+ function blockquote(value) {
170
+ return value.split(/\r?\n/).map((line) => `> ${line}`).join("\n");
171
+ }
172
+ function trimPreview(value, length) {
173
+ return value.replace(/\s+/g, " ").trim().slice(0, length);
174
+ }
175
+ function yamlStringArray(values) {
176
+ return `[${values.map((value) => `"${escapeYaml(value)}"`).join(", ")}]`;
177
+ }
178
+ function escapeYaml(value) {
179
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
180
+ }
@@ -10,6 +10,7 @@ export const REQUIRED_DIRS = [
10
10
  "03-sources/article-cards",
11
11
  "04-claims/_suggestions",
12
12
  "05-wiki",
13
+ "05-wiki/source-knowledge",
13
14
  "06-assets/_suggestions",
14
15
  "07-topics/ready",
15
16
  "08-outputs/outlines",
@@ -28,9 +29,12 @@ AIWiki 的 Obsidian 入口。Dataview 是可选增强;未安装时仍可使用
28
29
 
29
30
  ## 原生链接入口
30
31
 
32
+ - [[dashboards/Wiki Entries|Wiki 条目]]
33
+ - [[dashboards/Source Cards|资料卡]]
31
34
  - [[dashboards/Review Queue|待审队列]]
32
35
  - [[dashboards/Recent Runs|最近处理]]
33
36
  - [[dashboards/Topic Pipeline|选题管线]]
37
+ - [[dashboards/Lint Report|结构检查]]
34
38
  - [[_system/schemas/aiwiki-frontmatter|字段说明]]
35
39
 
36
40
  ## 最近收录
@@ -50,6 +54,34 @@ FROM "03-sources/article-cards" or "04-claims/_suggestions" or "06-assets/_sugge
50
54
  WHERE status = "to-review"
51
55
  SORT created_at DESC
52
56
  \`\`\`
57
+ `
58
+ },
59
+ {
60
+ path: "dashboards/Wiki Entries.md",
61
+ content: `# Wiki 条目
62
+
63
+ AIWiki 每次成功入库都会生成 Wiki Entry。这里是知识层入口,不要求先经过 Review Queue 才能查询。
64
+
65
+ \`\`\`dataview
66
+ TABLE wiki_type, source_role, represents_user_view, quality, source_card, raw_file, updated_at
67
+ FROM "05-wiki"
68
+ WHERE type = "wiki_entry"
69
+ SORT updated_at DESC
70
+ \`\`\`
71
+ `
72
+ },
73
+ {
74
+ path: "dashboards/Source Cards.md",
75
+ content: `# 资料卡
76
+
77
+ 资料卡用于追踪来源、原文、Claim 建议、素材建议、选题和大纲。
78
+
79
+ \`\`\`dataview
80
+ TABLE status, source_url, wiki_entry, raw_note, captured_at
81
+ FROM "03-sources/article-cards"
82
+ WHERE type = "source_card"
83
+ SORT captured_at DESC
84
+ \`\`\`
53
85
  `
54
86
  },
55
87
  {