@itradingai/aiwiki 0.2.10 → 0.2.11
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.
- package/README.md +150 -116
- package/dist/src/app.js +36 -3
- package/dist/src/context.js +123 -0
- package/dist/src/frontmatter.js +55 -0
- package/dist/src/ingest.js +17 -1
- package/dist/src/lint.js +188 -0
- package/dist/src/payload.js +143 -2
- package/dist/src/wiki-entry.js +151 -0
- package/dist/src/workspace.js +1 -0
- package/docs/AGENT_HANDOFF.md +41 -13
- package/docs/FAQ.md +38 -5
- package/docs/SHOWCASE.md +8 -3
- package/docs/USAGE.md +40 -10
- package/package.json +1 -1
- package/skill/LINT_PROTOCOL.md +42 -0
- package/skill/QUERY_PROTOCOL.md +38 -0
- package/skill/SKILL.md +100 -37
package/dist/src/ingest.js
CHANGED
|
@@ -4,6 +4,7 @@ import { randomBytes } from "node:crypto";
|
|
|
4
4
|
import { normalizePayload } from "./payload.js";
|
|
5
5
|
import { appendRunIdBeforeExt, relativePath, safeJoin, slugify } from "./paths.js";
|
|
6
6
|
import { initWorkspace } from "./workspace.js";
|
|
7
|
+
import { renderWikiEntry } from "./wiki-entry.js";
|
|
7
8
|
export async function ingestPayload(rootPath, rawPayload) {
|
|
8
9
|
await initWorkspace(rootPath);
|
|
9
10
|
const root = path.resolve(rootPath);
|
|
@@ -29,11 +30,14 @@ export async function ingestPayload(rootPath, rawPayload) {
|
|
|
29
30
|
const links = buildArtifactLinks(root, slug, runDirName, runStartedAt, longTermTargets);
|
|
30
31
|
await writeFile(path.join(runDir, "raw.md"), contentFile(payload, content, links), generatedFiles);
|
|
31
32
|
await writeFile(path.join(runDir, "source-card.md"), sourceCard(payload, runDirName, links), generatedFiles);
|
|
33
|
+
const wikiEntryResult = renderWikiEntry(payload, links);
|
|
34
|
+
await writeFile(path.join(runDir, "wiki-entry.md"), wikiEntryResult.markdown, generatedFiles);
|
|
32
35
|
await writeFile(path.join(runDir, "creative-assets.md"), creativeAssets(payload, links), generatedFiles);
|
|
33
36
|
await writeFile(path.join(runDir, "topics.md"), topics(payload, links), generatedFiles);
|
|
34
37
|
await writeFile(path.join(runDir, "draft-outline.md"), outline(payload, links), generatedFiles);
|
|
35
38
|
await writeFile(longTermTargets.raw, contentFile(payload, content, links), generatedFiles);
|
|
36
39
|
await writeFile(longTermTargets.sourceCard, sourceCard(payload, runDirName, links), generatedFiles);
|
|
40
|
+
await writeFile(longTermTargets.wikiEntry, wikiEntryResult.markdown, generatedFiles);
|
|
37
41
|
await writeFile(longTermTargets.claims, claims(payload, links), generatedFiles);
|
|
38
42
|
await writeFile(longTermTargets.assets, creativeAssets(payload, links), generatedFiles);
|
|
39
43
|
await writeFile(longTermTargets.topics, topics(payload, links), generatedFiles);
|
|
@@ -72,6 +76,7 @@ async function chooseLongTermTargets(root, slug, runId, warnings) {
|
|
|
72
76
|
return {
|
|
73
77
|
raw: await chooseLongTermTarget(root, "02-raw/articles", `${slug}.md`, runId, warnings),
|
|
74
78
|
sourceCard: await chooseLongTermTarget(root, "03-sources/article-cards", `${slug}.md`, runId, warnings),
|
|
79
|
+
wikiEntry: await chooseLongTermTarget(root, "05-wiki/source-knowledge", `${slug}.md`, runId, warnings),
|
|
75
80
|
claims: await chooseLongTermTarget(root, "04-claims/_suggestions", `${slug}-claims.md`, runId, warnings),
|
|
76
81
|
assets: await chooseLongTermTarget(root, "06-assets/_suggestions", `${slug}-assets.md`, runId, warnings),
|
|
77
82
|
topics: await chooseLongTermTarget(root, "07-topics/ready", `${slug}-topics.md`, runId, warnings),
|
|
@@ -168,6 +173,7 @@ function contentFile(payload, content, links) {
|
|
|
168
173
|
"",
|
|
169
174
|
"## AIWiki 链接",
|
|
170
175
|
"",
|
|
176
|
+
`- Wiki 条目:${obsidianLink(links.wikiEntry, "Wiki 条目")}`,
|
|
171
177
|
`- 资料卡:${obsidianLink(links.sourceCard, "资料卡")}`,
|
|
172
178
|
`- 处理记录:${obsidianLink(links.runSummary, "处理记录")}`,
|
|
173
179
|
"",
|
|
@@ -199,6 +205,7 @@ function sourceCard(payload, runId, links) {
|
|
|
199
205
|
"",
|
|
200
206
|
"## Obsidian 链接",
|
|
201
207
|
"",
|
|
208
|
+
`- Wiki 条目:${obsidianLink(links.wikiEntry, "Wiki 条目")}`,
|
|
202
209
|
`- 原文:${obsidianLink(links.raw, "原文")}`,
|
|
203
210
|
`- Claim 建议:${obsidianLink(links.claims, "Claim 建议")}`,
|
|
204
211
|
`- 素材建议:${obsidianLink(links.assets, "素材建议")}`,
|
|
@@ -231,6 +238,7 @@ function claims(payload, links) {
|
|
|
231
238
|
"",
|
|
232
239
|
"# Claim 建议",
|
|
233
240
|
"",
|
|
241
|
+
`- Wiki 条目:${obsidianLink(links.wikiEntry, "Wiki 条目")}`,
|
|
234
242
|
`- 资料卡:${obsidianLink(links.sourceCard, "资料卡")}`,
|
|
235
243
|
`- 原文:${obsidianLink(links.raw, "原文")}`,
|
|
236
244
|
`- 待人工审阅:${payload.source.title ?? "Untitled"}`,
|
|
@@ -256,6 +264,7 @@ function creativeAssets(payload, links) {
|
|
|
256
264
|
"",
|
|
257
265
|
"# 素材建议",
|
|
258
266
|
"",
|
|
267
|
+
`- Wiki 条目:${obsidianLink(links.wikiEntry, "Wiki 条目")}`,
|
|
259
268
|
`- 资料卡:${obsidianLink(links.sourceCard, "资料卡")}`,
|
|
260
269
|
`- 原文:${obsidianLink(links.raw, "原文")}`,
|
|
261
270
|
`- 可复用素材:${payload.source.title ?? "Untitled"}`,
|
|
@@ -281,6 +290,7 @@ function topics(payload, links) {
|
|
|
281
290
|
"",
|
|
282
291
|
"# 选题候选",
|
|
283
292
|
"",
|
|
293
|
+
`- Wiki 条目:${obsidianLink(links.wikiEntry, "Wiki 条目")}`,
|
|
284
294
|
`- 资料卡:${obsidianLink(links.sourceCard, "资料卡")}`,
|
|
285
295
|
`- 大纲:${obsidianLink(links.outline, "大纲")}`,
|
|
286
296
|
`- ${payload.source.title ?? "Untitled"}`,
|
|
@@ -306,6 +316,7 @@ function outline(payload, links) {
|
|
|
306
316
|
"",
|
|
307
317
|
"# 草稿大纲",
|
|
308
318
|
"",
|
|
319
|
+
`Wiki 条目:${obsidianLink(links.wikiEntry, "Wiki 条目")}`,
|
|
309
320
|
`资料卡:${obsidianLink(links.sourceCard, "资料卡")}`,
|
|
310
321
|
`原文:${obsidianLink(links.raw, "原文")}`,
|
|
311
322
|
"",
|
|
@@ -334,11 +345,14 @@ function buildAgentReport(root, runDir, payload, generatedFiles) {
|
|
|
334
345
|
summary: fetchFailed ? (payload.source.fetch_notes ?? "宿主 Agent 没有提供可读正文。") : summarizeContent(content),
|
|
335
346
|
keyFiles: {
|
|
336
347
|
processingSummary: relativePath(root, path.join(runDir, "processing-summary.md")),
|
|
348
|
+
wikiEntry: findGeneratedFileInDir(root, generatedFiles, "05-wiki/source-knowledge"),
|
|
337
349
|
sourceCard: findGeneratedFileInDir(root, generatedFiles, "03-sources/article-cards"),
|
|
338
350
|
draftOutline: findGeneratedFile(root, generatedFiles, "draft-outline.md"),
|
|
339
351
|
dashboard: "dashboards/AIWiki Home.md",
|
|
340
352
|
reviewQueue: "dashboards/Review Queue.md"
|
|
341
|
-
}
|
|
353
|
+
},
|
|
354
|
+
wikiEntryGenerationMode: fetchFailed ? undefined : (payload.wiki_entry || payload.analysis ? "agent_enriched" : "deterministic_fallback"),
|
|
355
|
+
wikiEntryQuality: fetchFailed ? undefined : (payload.wiki_entry || payload.analysis ? "enriched" : "scaffold")
|
|
342
356
|
};
|
|
343
357
|
}
|
|
344
358
|
function estimateFitScore(payload, content) {
|
|
@@ -394,6 +408,7 @@ function buildArtifactLinks(root, slug, runDirName, createdAt, longTermTargets)
|
|
|
394
408
|
createdAt,
|
|
395
409
|
raw: relativePath(root, longTermTargets.raw),
|
|
396
410
|
sourceCard: relativePath(root, longTermTargets.sourceCard),
|
|
411
|
+
wikiEntry: relativePath(root, longTermTargets.wikiEntry),
|
|
397
412
|
claims: relativePath(root, longTermTargets.claims),
|
|
398
413
|
assets: relativePath(root, longTermTargets.assets),
|
|
399
414
|
topics: relativePath(root, longTermTargets.topics),
|
|
@@ -403,6 +418,7 @@ function buildArtifactLinks(root, slug, runDirName, createdAt, longTermTargets)
|
|
|
403
418
|
}
|
|
404
419
|
function relationshipFrontmatter(links) {
|
|
405
420
|
return [
|
|
421
|
+
`wiki_entry: "${escapeYaml(obsidianLink(links.wikiEntry, "Wiki 条目"))}"`,
|
|
406
422
|
`source_card: "${escapeYaml(obsidianLink(links.sourceCard, "资料卡"))}"`,
|
|
407
423
|
`raw_note: "${escapeYaml(obsidianLink(links.raw, "原文"))}"`,
|
|
408
424
|
`claims_note: "${escapeYaml(obsidianLink(links.claims, "Claim 建议"))}"`,
|
package/dist/src/lint.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { 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, "represents_user_view") === true && frontmatterString(entry.frontmatter, "source_role") === "input") {
|
|
61
|
+
issues.push({ severity: "warning", path: entry.path, message: "外部 input 被标记为代表用户观点。", suggestion: "将 represents_user_view 改为 false,或在 P1 使用 source_role=output。" });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
issues.push(...duplicateIssues(sourceCards, "source_url", "重复 URL"));
|
|
65
|
+
issues.push(...duplicateTitles(allNotes));
|
|
66
|
+
issues.push(...brokenLinkIssues(root, allNotes));
|
|
67
|
+
return {
|
|
68
|
+
generated_at: now,
|
|
69
|
+
summary: {
|
|
70
|
+
wiki_entries: wikiEntries.length,
|
|
71
|
+
source_cards: sourceCards.length,
|
|
72
|
+
raw_files: rawFiles.length,
|
|
73
|
+
runs: runs.length
|
|
74
|
+
},
|
|
75
|
+
issues
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export async function writeLintReport(rootPath, report) {
|
|
79
|
+
const root = path.resolve(rootPath);
|
|
80
|
+
const target = safeJoin(root, "dashboards", "Lint Report.md");
|
|
81
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
82
|
+
await fs.writeFile(target, renderLintReport(report), "utf8");
|
|
83
|
+
return relativePath(root, target);
|
|
84
|
+
}
|
|
85
|
+
export function renderLintReport(report) {
|
|
86
|
+
return [
|
|
87
|
+
"# AIWiki Lint Report",
|
|
88
|
+
"",
|
|
89
|
+
`Generated at: ${report.generated_at}`,
|
|
90
|
+
"",
|
|
91
|
+
"## Summary",
|
|
92
|
+
"",
|
|
93
|
+
`- Wiki Entries: ${report.summary.wiki_entries}`,
|
|
94
|
+
`- Source Cards: ${report.summary.source_cards}`,
|
|
95
|
+
`- Raw Files: ${report.summary.raw_files}`,
|
|
96
|
+
`- Runs: ${report.summary.runs}`,
|
|
97
|
+
"",
|
|
98
|
+
"## Issues",
|
|
99
|
+
"",
|
|
100
|
+
...(report.issues.length ? report.issues.map((issue) => {
|
|
101
|
+
const suffix = issue.path ? ` (${issue.path})` : "";
|
|
102
|
+
const suggestion = issue.suggestion ? `\n - Suggested Fix: ${issue.suggestion}` : "";
|
|
103
|
+
return `- [${issue.severity}] ${issue.message}${suffix}${suggestion}`;
|
|
104
|
+
}) : ["- none"]),
|
|
105
|
+
""
|
|
106
|
+
].join("\n");
|
|
107
|
+
}
|
|
108
|
+
async function readNotes(root, dir) {
|
|
109
|
+
const absolute = path.join(root, dir);
|
|
110
|
+
if (!(await exists(absolute))) {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
const files = await listMarkdownFiles(absolute);
|
|
114
|
+
return Promise.all(files.map(async (file) => {
|
|
115
|
+
const text = await fs.readFile(file, "utf8");
|
|
116
|
+
const parsed = parseMarkdown(text);
|
|
117
|
+
return {
|
|
118
|
+
path: relativePath(root, file),
|
|
119
|
+
title: frontmatterString(parsed.frontmatter, "title") ?? path.basename(file, ".md"),
|
|
120
|
+
sourceUrl: frontmatterString(parsed.frontmatter, "source_url") ?? "",
|
|
121
|
+
body: parsed.body,
|
|
122
|
+
frontmatter: parsed.frontmatter
|
|
123
|
+
};
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
async function runDirs(root) {
|
|
127
|
+
const dir = path.join(root, "09-runs");
|
|
128
|
+
if (!(await exists(dir))) {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
return (await fs.readdir(dir, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
132
|
+
}
|
|
133
|
+
async function listMarkdownFiles(dir) {
|
|
134
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
135
|
+
const files = [];
|
|
136
|
+
for (const entry of entries) {
|
|
137
|
+
const target = path.join(dir, entry.name);
|
|
138
|
+
if (entry.isDirectory()) {
|
|
139
|
+
files.push(...await listMarkdownFiles(target));
|
|
140
|
+
}
|
|
141
|
+
else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
|
|
142
|
+
files.push(target);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return files;
|
|
146
|
+
}
|
|
147
|
+
function duplicateIssues(notes, field, label) {
|
|
148
|
+
const seen = new Map();
|
|
149
|
+
for (const note of notes) {
|
|
150
|
+
const value = field === "source_url" ? note.sourceUrl : "";
|
|
151
|
+
if (!value) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
seen.set(value, [...(seen.get(value) ?? []), note.path]);
|
|
155
|
+
}
|
|
156
|
+
return Array.from(seen.entries()).flatMap(([value, paths]) => paths.length > 1
|
|
157
|
+
? [{ severity: "warning", message: `${label}: ${value}`, suggestion: paths.join(", ") }]
|
|
158
|
+
: []);
|
|
159
|
+
}
|
|
160
|
+
function duplicateTitles(notes) {
|
|
161
|
+
const seen = new Map();
|
|
162
|
+
for (const note of notes) {
|
|
163
|
+
if (!note.title) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
seen.set(note.title, [...(seen.get(note.title) ?? []), note.path]);
|
|
167
|
+
}
|
|
168
|
+
return Array.from(seen.entries()).flatMap(([title, paths]) => paths.length > 1
|
|
169
|
+
? [{ severity: "info", message: `重复标题: ${title}`, suggestion: paths.join(", ") }]
|
|
170
|
+
: []);
|
|
171
|
+
}
|
|
172
|
+
function brokenLinkIssues(root, notes) {
|
|
173
|
+
const existing = new Set(notes.map((note) => note.path.replace(/\.md$/i, "")));
|
|
174
|
+
const issues = [];
|
|
175
|
+
for (const note of notes) {
|
|
176
|
+
for (const link of note.body.matchAll(/\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g)) {
|
|
177
|
+
const target = link[1].replace(/\\/g, "/").replace(/\.md$/i, "");
|
|
178
|
+
if (!existing.has(target) && !isRunLocalLink(target)) {
|
|
179
|
+
issues.push({ severity: "error", path: note.path, message: `内部链接断裂: ${target}`, suggestion: "检查目标文件是否存在或更新 wikilink。" });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return issues;
|
|
184
|
+
}
|
|
185
|
+
function isRunLocalLink(target) {
|
|
186
|
+
// Run-local notes are valid trace links but are not part of the long-term note set.
|
|
187
|
+
return target.startsWith("09-runs/");
|
|
188
|
+
}
|
package/dist/src/payload.js
CHANGED
|
@@ -39,8 +39,8 @@ export function normalizePayload(raw, runStartedAt) {
|
|
|
39
39
|
const requestedOutputs = Array.isArray(requestRaw.outputs)
|
|
40
40
|
? requestRaw.outputs.filter((item) => typeof item === "string")
|
|
41
41
|
: ["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
|
|
42
|
+
const outputs = ["source_card", "wiki_entry", "creative_assets", "topics", "draft_outline", "processing_summary"];
|
|
43
|
+
if (fetchStatus !== "failed" && requestedOutputs.length && hasCustomOutputRequest(requestedOutputs)) {
|
|
44
44
|
warnings.push("AIWiki 会为每条输入生成完整资料产物,request.outputs 已按全量输出处理。");
|
|
45
45
|
}
|
|
46
46
|
if (typeof raw.target_kb === "string" && raw.target_kb.trim()) {
|
|
@@ -55,6 +55,8 @@ export function normalizePayload(raw, runStartedAt) {
|
|
|
55
55
|
if (fetchNotesRepair.repaired) {
|
|
56
56
|
warnings.push("source.fetch_notes 检测到疑似 UTF-8 mojibake,已自动修复。");
|
|
57
57
|
}
|
|
58
|
+
const analysis = normalizeAnalysis(raw.analysis, warnings);
|
|
59
|
+
const wikiEntry = normalizeWikiEntry(raw.wiki_entry, warnings);
|
|
58
60
|
if (fetchStatus === "failed" && content?.trim()) {
|
|
59
61
|
throw new Error("source.content must be empty when source.fetch_status is failed");
|
|
60
62
|
}
|
|
@@ -80,6 +82,8 @@ export function normalizePayload(raw, runStartedAt) {
|
|
|
80
82
|
outputs,
|
|
81
83
|
language: stringValue(requestRaw.language) ?? stringValue(sourceRaw.language)
|
|
82
84
|
},
|
|
85
|
+
analysis,
|
|
86
|
+
wiki_entry: wikiEntry,
|
|
83
87
|
warnings
|
|
84
88
|
};
|
|
85
89
|
}
|
|
@@ -99,6 +103,9 @@ function rejectWriteControlFields(raw) {
|
|
|
99
103
|
if (typeof raw.output_file === "string") {
|
|
100
104
|
throw new Error("payload must not control output paths");
|
|
101
105
|
}
|
|
106
|
+
if (isRecord(raw.wiki_entry) && typeof raw.wiki_entry.path === "string") {
|
|
107
|
+
throw new Error("payload must not control output paths");
|
|
108
|
+
}
|
|
102
109
|
}
|
|
103
110
|
function stringValue(value) {
|
|
104
111
|
return typeof value === "string" ? value : undefined;
|
|
@@ -106,3 +113,137 @@ function stringValue(value) {
|
|
|
106
113
|
function isRecord(value) {
|
|
107
114
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
108
115
|
}
|
|
116
|
+
function normalizeAnalysis(value, warnings) {
|
|
117
|
+
if (value === undefined) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
if (!isRecord(value)) {
|
|
121
|
+
warnings.push("analysis 已忽略:必须是对象。");
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
const analysis = {
|
|
125
|
+
summary: stringValue(value.summary),
|
|
126
|
+
key_points: stringArray(value.key_points, "analysis.key_points", warnings),
|
|
127
|
+
reusable_knowledge: reusableKnowledgeArray(value.reusable_knowledge, warnings),
|
|
128
|
+
related_concepts: stringArray(value.related_concepts, "analysis.related_concepts", warnings),
|
|
129
|
+
use_cases: stringArray(value.use_cases, "analysis.use_cases", warnings),
|
|
130
|
+
topic_candidates: stringArray(value.topic_candidates, "analysis.topic_candidates", warnings),
|
|
131
|
+
claims: claimsArray(value.claims, warnings),
|
|
132
|
+
outline: outlineValue(value.outline, warnings)
|
|
133
|
+
};
|
|
134
|
+
return hasAnalysisContent(analysis) ? analysis : undefined;
|
|
135
|
+
}
|
|
136
|
+
function normalizeWikiEntry(value, warnings) {
|
|
137
|
+
if (value === undefined) {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
if (!isRecord(value)) {
|
|
141
|
+
warnings.push("wiki_entry 已忽略:必须是对象。");
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
const entry = {
|
|
145
|
+
title: stringValue(value.title),
|
|
146
|
+
summary: stringValue(value.summary),
|
|
147
|
+
sections: wikiSections(value.sections, warnings),
|
|
148
|
+
markdown: stringValue(value.markdown)
|
|
149
|
+
};
|
|
150
|
+
return entry.title || entry.summary || entry.sections.length || entry.markdown ? entry : undefined;
|
|
151
|
+
}
|
|
152
|
+
function stringArray(value, field, warnings) {
|
|
153
|
+
if (value === undefined) {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
if (!Array.isArray(value)) {
|
|
157
|
+
warnings.push(`${field} 已忽略:必须是字符串数组。`);
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
return value.filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
161
|
+
}
|
|
162
|
+
function reusableKnowledgeArray(value, warnings) {
|
|
163
|
+
if (value === undefined) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
if (!Array.isArray(value)) {
|
|
167
|
+
warnings.push("analysis.reusable_knowledge 已忽略:必须是数组。");
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
return value.flatMap((item) => {
|
|
171
|
+
if (typeof item === "string" && item.trim()) {
|
|
172
|
+
return [{ content: item.trim() }];
|
|
173
|
+
}
|
|
174
|
+
if (isRecord(item) && typeof item.content === "string" && item.content.trim()) {
|
|
175
|
+
return [{ title: stringValue(item.title), content: item.content.trim() }];
|
|
176
|
+
}
|
|
177
|
+
return [];
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
function claimsArray(value, warnings) {
|
|
181
|
+
if (value === undefined) {
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
if (!Array.isArray(value)) {
|
|
185
|
+
warnings.push("analysis.claims 已忽略:必须是数组。");
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
return value.flatMap((item) => {
|
|
189
|
+
if (typeof item === "string" && item.trim()) {
|
|
190
|
+
return [{ claim: item.trim() }];
|
|
191
|
+
}
|
|
192
|
+
if (isRecord(item) && typeof item.claim === "string" && item.claim.trim()) {
|
|
193
|
+
return [{
|
|
194
|
+
claim: item.claim.trim(),
|
|
195
|
+
confidence: stringValue(item.confidence),
|
|
196
|
+
source_quote: stringValue(item.source_quote)
|
|
197
|
+
}];
|
|
198
|
+
}
|
|
199
|
+
return [];
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
function outlineValue(value, warnings) {
|
|
203
|
+
if (value === undefined) {
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
if (!isRecord(value)) {
|
|
207
|
+
warnings.push("analysis.outline 已忽略:必须是对象。");
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
const sections = stringArray(value.sections, "analysis.outline.sections", warnings);
|
|
211
|
+
if (!sections.length && !stringValue(value.title)) {
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
return { title: stringValue(value.title), sections };
|
|
215
|
+
}
|
|
216
|
+
function wikiSections(value, warnings) {
|
|
217
|
+
if (value === undefined) {
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
if (!Array.isArray(value)) {
|
|
221
|
+
warnings.push("wiki_entry.sections 已忽略:必须是数组。");
|
|
222
|
+
return [];
|
|
223
|
+
}
|
|
224
|
+
return value.flatMap((item) => {
|
|
225
|
+
if (!isRecord(item) || typeof item.heading !== "string" || !item.heading.trim()) {
|
|
226
|
+
return [];
|
|
227
|
+
}
|
|
228
|
+
return [{
|
|
229
|
+
heading: item.heading.trim(),
|
|
230
|
+
items: stringArray(item.items, "wiki_entry.sections.items", warnings)
|
|
231
|
+
}];
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
function hasAnalysisContent(analysis) {
|
|
235
|
+
return Boolean(analysis.summary ||
|
|
236
|
+
analysis.key_points.length ||
|
|
237
|
+
analysis.reusable_knowledge.length ||
|
|
238
|
+
analysis.related_concepts.length ||
|
|
239
|
+
analysis.use_cases.length ||
|
|
240
|
+
analysis.topic_candidates.length ||
|
|
241
|
+
analysis.claims.length ||
|
|
242
|
+
analysis.outline);
|
|
243
|
+
}
|
|
244
|
+
function hasCustomOutputRequest(outputs) {
|
|
245
|
+
const legacyDefault = ["source_card", "creative_assets", "topics", "draft_outline", "processing_summary"];
|
|
246
|
+
const currentDefault = ["source_card", "wiki_entry", "creative_assets", "topics", "draft_outline", "processing_summary"];
|
|
247
|
+
const sameSet = (left, right) => left.length === right.length && left.every((item) => right.includes(item));
|
|
248
|
+
return !sameSet(outputs, legacyDefault) && !sameSet(outputs, currentDefault);
|
|
249
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
export function renderWikiEntry(payload, links) {
|
|
2
|
+
const enriched = Boolean(payload.wiki_entry || payload.analysis);
|
|
3
|
+
const mode = enriched ? "agent_enriched" : "deterministic_fallback";
|
|
4
|
+
const quality = enriched ? "enriched" : "scaffold";
|
|
5
|
+
const title = payload.wiki_entry?.title ?? payload.source.title ?? "Untitled";
|
|
6
|
+
const frontmatter = wikiFrontmatter(payload, links, title, mode, quality);
|
|
7
|
+
const body = enriched ? enrichedBody(payload, links, title) : fallbackBody(payload, links, title);
|
|
8
|
+
return {
|
|
9
|
+
mode,
|
|
10
|
+
quality,
|
|
11
|
+
markdown: `${frontmatter}\n${body}`
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function wikiFrontmatter(payload, links, title, mode, quality) {
|
|
15
|
+
return [
|
|
16
|
+
"---",
|
|
17
|
+
`aiwiki_id: "${escapeYaml(`${links.slug}:wiki-entry`)}"`,
|
|
18
|
+
`type: "wiki_entry"`,
|
|
19
|
+
`wiki_type: "source_knowledge"`,
|
|
20
|
+
`source_role: "input"`,
|
|
21
|
+
`represents_user_view: false`,
|
|
22
|
+
`status: "active"`,
|
|
23
|
+
`generation_mode: "${mode}"`,
|
|
24
|
+
`quality: "${quality}"`,
|
|
25
|
+
`generated_by: "${mode === "agent_enriched" ? "host_agent" : "aiwiki_cli"}"`,
|
|
26
|
+
`llm_enriched: ${mode === "agent_enriched" ? "true" : "false"}`,
|
|
27
|
+
`title: "${escapeYaml(title)}"`,
|
|
28
|
+
`slug: "${escapeYaml(links.slug)}"`,
|
|
29
|
+
`source_url: "${escapeYaml(payload.source.url ?? "")}"`,
|
|
30
|
+
`source_type: "${escapeYaml(payload.source.kind)}"`,
|
|
31
|
+
`source_card: "${escapeYaml(links.sourceCard)}"`,
|
|
32
|
+
`raw_file: "${escapeYaml(links.raw)}"`,
|
|
33
|
+
`claims_file: "${escapeYaml(links.claims)}"`,
|
|
34
|
+
`topics_file: "${escapeYaml(links.topics)}"`,
|
|
35
|
+
`outline_file: "${escapeYaml(links.outline)}"`,
|
|
36
|
+
`run_summary: "${escapeYaml(links.runSummary)}"`,
|
|
37
|
+
`run_id: "${escapeYaml(links.runId)}"`,
|
|
38
|
+
`created_at: "${escapeYaml(links.createdAt)}"`,
|
|
39
|
+
`updated_at: "${escapeYaml(links.createdAt)}"`,
|
|
40
|
+
...(mode === "agent_enriched" ? [`summary: "${escapeYaml(payload.wiki_entry?.summary ?? payload.analysis?.summary ?? "")}"`] : []),
|
|
41
|
+
`topics: ${yamlStringArray(payload.analysis?.related_concepts ?? [])}`,
|
|
42
|
+
`claims: ${yamlStringArray(payload.analysis?.claims.map((claim) => claim.claim) ?? [])}`,
|
|
43
|
+
`tags: ["aiwiki/wiki-entry"]`,
|
|
44
|
+
"---"
|
|
45
|
+
].join("\n");
|
|
46
|
+
}
|
|
47
|
+
function enrichedBody(payload, links, title) {
|
|
48
|
+
const sections = [`# ${title}`, ""];
|
|
49
|
+
if (payload.wiki_entry?.summary || payload.analysis?.summary) {
|
|
50
|
+
sections.push("## 一句话总结", "", payload.wiki_entry?.summary ?? payload.analysis?.summary ?? "", "");
|
|
51
|
+
}
|
|
52
|
+
if (payload.wiki_entry?.markdown?.trim()) {
|
|
53
|
+
sections.push(payload.wiki_entry.markdown.trim(), "");
|
|
54
|
+
if (payload.wiki_entry.sections.length) {
|
|
55
|
+
for (const section of payload.wiki_entry.sections) {
|
|
56
|
+
if (bodyHasHeading(payload.wiki_entry.markdown, section.heading)) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
sections.push(`## ${section.heading}`, "", ...listOrFallback(section.items, "待宿主 Agent 补充。"), "");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
sections.push(sourceSection(links));
|
|
63
|
+
return sections.join("\n");
|
|
64
|
+
}
|
|
65
|
+
if (payload.wiki_entry?.sections.length) {
|
|
66
|
+
for (const section of payload.wiki_entry.sections) {
|
|
67
|
+
sections.push(`## ${section.heading}`, "", ...listOrFallback(section.items, "待宿主 Agent 补充。"), "");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
sections.push("## 核心观点", "", ...listOrFallback(payload.analysis?.key_points ?? [], "待宿主 Agent 补充。"), "");
|
|
72
|
+
sections.push("## 可复用知识点", "", ...knowledgeList(payload), "");
|
|
73
|
+
sections.push("## 相关概念", "", ...listOrFallback(payload.analysis?.related_concepts ?? [], "待宿主 Agent 补充。"), "");
|
|
74
|
+
sections.push("## 适合用于什么场景", "", ...listOrFallback(payload.analysis?.use_cases ?? [], "待宿主 Agent 补充。"), "");
|
|
75
|
+
sections.push("## 可转化的选题", "", ...listOrFallback(payload.analysis?.topic_candidates ?? [], "待宿主 Agent 补充。"), "");
|
|
76
|
+
}
|
|
77
|
+
sections.push(sourceSection(links));
|
|
78
|
+
return sections.join("\n");
|
|
79
|
+
}
|
|
80
|
+
function bodyHasHeading(markdown, heading) {
|
|
81
|
+
const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
82
|
+
return new RegExp(`^#{1,6}\\s+${escaped}\\s*$`, "m").test(markdown);
|
|
83
|
+
}
|
|
84
|
+
function fallbackBody(payload, links, title) {
|
|
85
|
+
const preview = trimPreview(payload.source.content ?? payload.source.fetch_notes ?? "", 1000);
|
|
86
|
+
return [
|
|
87
|
+
`# ${title}`,
|
|
88
|
+
"",
|
|
89
|
+
"## 说明",
|
|
90
|
+
"",
|
|
91
|
+
"这是 AIWiki 根据原文和元数据生成的基础 Wiki 条目。当前条目未经过宿主 Agent 的深度分析,仅用于建立知识库索引、来源追踪和后续 Query。",
|
|
92
|
+
"",
|
|
93
|
+
"## 来源信息",
|
|
94
|
+
"",
|
|
95
|
+
`- 原文链接:${payload.source.url ?? "无"}`,
|
|
96
|
+
`- Source Card:${obsidianLink(links.sourceCard, "资料卡")}`,
|
|
97
|
+
`- Raw:${obsidianLink(links.raw, "原文")}`,
|
|
98
|
+
`- Run:${obsidianLink(links.runSummary, "处理记录")}`,
|
|
99
|
+
"",
|
|
100
|
+
"## 内容预览",
|
|
101
|
+
"",
|
|
102
|
+
preview ? blockquote(preview) : "暂无可用正文预览。",
|
|
103
|
+
"",
|
|
104
|
+
"## 待 Agent 补全",
|
|
105
|
+
"",
|
|
106
|
+
"以下内容需要宿主 Agent 基于原文进一步生成:",
|
|
107
|
+
"",
|
|
108
|
+
"- 一句话总结",
|
|
109
|
+
"- 核心观点",
|
|
110
|
+
"- 可复用知识点",
|
|
111
|
+
"- 相关概念",
|
|
112
|
+
"- 适用场景",
|
|
113
|
+
"- 可转化选题",
|
|
114
|
+
"",
|
|
115
|
+
sourceSection(links)
|
|
116
|
+
].join("\n");
|
|
117
|
+
}
|
|
118
|
+
function knowledgeList(payload) {
|
|
119
|
+
const items = payload.analysis?.reusable_knowledge ?? [];
|
|
120
|
+
if (!items.length) {
|
|
121
|
+
return ["待宿主 Agent 补充。"];
|
|
122
|
+
}
|
|
123
|
+
return items.flatMap((item) => item.title ? [`### ${item.title}`, "", item.content] : [`- ${item.content}`]);
|
|
124
|
+
}
|
|
125
|
+
function listOrFallback(values, fallback) {
|
|
126
|
+
return values.length ? values.map((value) => `- ${value}`) : [fallback];
|
|
127
|
+
}
|
|
128
|
+
function sourceSection(links) {
|
|
129
|
+
return [
|
|
130
|
+
"## 来源",
|
|
131
|
+
"",
|
|
132
|
+
`- Source Card: ${obsidianLink(links.sourceCard, "资料卡")}`,
|
|
133
|
+
`- Raw: ${obsidianLink(links.raw, "原文")}`,
|
|
134
|
+
`- Run: ${obsidianLink(links.runSummary, "处理记录")}`
|
|
135
|
+
].join("\n");
|
|
136
|
+
}
|
|
137
|
+
function obsidianLink(vaultPath, label) {
|
|
138
|
+
return `[[${vaultPath.replace(/\\/g, "/").replace(/\.md$/i, "")}|${label}]]`;
|
|
139
|
+
}
|
|
140
|
+
function blockquote(value) {
|
|
141
|
+
return value.split(/\r?\n/).map((line) => `> ${line}`).join("\n");
|
|
142
|
+
}
|
|
143
|
+
function trimPreview(value, length) {
|
|
144
|
+
return value.replace(/\s+/g, " ").trim().slice(0, length);
|
|
145
|
+
}
|
|
146
|
+
function yamlStringArray(values) {
|
|
147
|
+
return `[${values.map((value) => `"${escapeYaml(value)}"`).join(", ")}]`;
|
|
148
|
+
}
|
|
149
|
+
function escapeYaml(value) {
|
|
150
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
151
|
+
}
|