@itradingai/aiwiki 0.2.9 → 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 +149 -114
- package/dist/src/app.js +38 -5
- package/dist/src/context.js +123 -0
- package/dist/src/frontmatter.js +55 -0
- package/dist/src/ingest.js +21 -2
- package/dist/src/lint.js +188 -0
- package/dist/src/paths.js +5 -3
- 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 +45 -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);
|
|
@@ -51,7 +55,7 @@ export async function ingestFile(rootPath, filePath) {
|
|
|
51
55
|
schema_version: "aiwiki.agent_payload.v1",
|
|
52
56
|
source: {
|
|
53
57
|
kind: "file",
|
|
54
|
-
title:
|
|
58
|
+
title: deriveFileTitle(filePath),
|
|
55
59
|
content_format: "markdown",
|
|
56
60
|
content,
|
|
57
61
|
fetcher: "local-file",
|
|
@@ -65,10 +69,14 @@ export async function ingestFile(rootPath, filePath) {
|
|
|
65
69
|
}
|
|
66
70
|
});
|
|
67
71
|
}
|
|
72
|
+
export function deriveFileTitle(filePath) {
|
|
73
|
+
return path.basename(filePath, path.extname(filePath));
|
|
74
|
+
}
|
|
68
75
|
async function chooseLongTermTargets(root, slug, runId, warnings) {
|
|
69
76
|
return {
|
|
70
77
|
raw: await chooseLongTermTarget(root, "02-raw/articles", `${slug}.md`, runId, warnings),
|
|
71
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),
|
|
72
80
|
claims: await chooseLongTermTarget(root, "04-claims/_suggestions", `${slug}-claims.md`, runId, warnings),
|
|
73
81
|
assets: await chooseLongTermTarget(root, "06-assets/_suggestions", `${slug}-assets.md`, runId, warnings),
|
|
74
82
|
topics: await chooseLongTermTarget(root, "07-topics/ready", `${slug}-topics.md`, runId, warnings),
|
|
@@ -165,6 +173,7 @@ function contentFile(payload, content, links) {
|
|
|
165
173
|
"",
|
|
166
174
|
"## AIWiki 链接",
|
|
167
175
|
"",
|
|
176
|
+
`- Wiki 条目:${obsidianLink(links.wikiEntry, "Wiki 条目")}`,
|
|
168
177
|
`- 资料卡:${obsidianLink(links.sourceCard, "资料卡")}`,
|
|
169
178
|
`- 处理记录:${obsidianLink(links.runSummary, "处理记录")}`,
|
|
170
179
|
"",
|
|
@@ -196,6 +205,7 @@ function sourceCard(payload, runId, links) {
|
|
|
196
205
|
"",
|
|
197
206
|
"## Obsidian 链接",
|
|
198
207
|
"",
|
|
208
|
+
`- Wiki 条目:${obsidianLink(links.wikiEntry, "Wiki 条目")}`,
|
|
199
209
|
`- 原文:${obsidianLink(links.raw, "原文")}`,
|
|
200
210
|
`- Claim 建议:${obsidianLink(links.claims, "Claim 建议")}`,
|
|
201
211
|
`- 素材建议:${obsidianLink(links.assets, "素材建议")}`,
|
|
@@ -228,6 +238,7 @@ function claims(payload, links) {
|
|
|
228
238
|
"",
|
|
229
239
|
"# Claim 建议",
|
|
230
240
|
"",
|
|
241
|
+
`- Wiki 条目:${obsidianLink(links.wikiEntry, "Wiki 条目")}`,
|
|
231
242
|
`- 资料卡:${obsidianLink(links.sourceCard, "资料卡")}`,
|
|
232
243
|
`- 原文:${obsidianLink(links.raw, "原文")}`,
|
|
233
244
|
`- 待人工审阅:${payload.source.title ?? "Untitled"}`,
|
|
@@ -253,6 +264,7 @@ function creativeAssets(payload, links) {
|
|
|
253
264
|
"",
|
|
254
265
|
"# 素材建议",
|
|
255
266
|
"",
|
|
267
|
+
`- Wiki 条目:${obsidianLink(links.wikiEntry, "Wiki 条目")}`,
|
|
256
268
|
`- 资料卡:${obsidianLink(links.sourceCard, "资料卡")}`,
|
|
257
269
|
`- 原文:${obsidianLink(links.raw, "原文")}`,
|
|
258
270
|
`- 可复用素材:${payload.source.title ?? "Untitled"}`,
|
|
@@ -278,6 +290,7 @@ function topics(payload, links) {
|
|
|
278
290
|
"",
|
|
279
291
|
"# 选题候选",
|
|
280
292
|
"",
|
|
293
|
+
`- Wiki 条目:${obsidianLink(links.wikiEntry, "Wiki 条目")}`,
|
|
281
294
|
`- 资料卡:${obsidianLink(links.sourceCard, "资料卡")}`,
|
|
282
295
|
`- 大纲:${obsidianLink(links.outline, "大纲")}`,
|
|
283
296
|
`- ${payload.source.title ?? "Untitled"}`,
|
|
@@ -303,6 +316,7 @@ function outline(payload, links) {
|
|
|
303
316
|
"",
|
|
304
317
|
"# 草稿大纲",
|
|
305
318
|
"",
|
|
319
|
+
`Wiki 条目:${obsidianLink(links.wikiEntry, "Wiki 条目")}`,
|
|
306
320
|
`资料卡:${obsidianLink(links.sourceCard, "资料卡")}`,
|
|
307
321
|
`原文:${obsidianLink(links.raw, "原文")}`,
|
|
308
322
|
"",
|
|
@@ -331,11 +345,14 @@ function buildAgentReport(root, runDir, payload, generatedFiles) {
|
|
|
331
345
|
summary: fetchFailed ? (payload.source.fetch_notes ?? "宿主 Agent 没有提供可读正文。") : summarizeContent(content),
|
|
332
346
|
keyFiles: {
|
|
333
347
|
processingSummary: relativePath(root, path.join(runDir, "processing-summary.md")),
|
|
348
|
+
wikiEntry: findGeneratedFileInDir(root, generatedFiles, "05-wiki/source-knowledge"),
|
|
334
349
|
sourceCard: findGeneratedFileInDir(root, generatedFiles, "03-sources/article-cards"),
|
|
335
350
|
draftOutline: findGeneratedFile(root, generatedFiles, "draft-outline.md"),
|
|
336
351
|
dashboard: "dashboards/AIWiki Home.md",
|
|
337
352
|
reviewQueue: "dashboards/Review Queue.md"
|
|
338
|
-
}
|
|
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")
|
|
339
356
|
};
|
|
340
357
|
}
|
|
341
358
|
function estimateFitScore(payload, content) {
|
|
@@ -391,6 +408,7 @@ function buildArtifactLinks(root, slug, runDirName, createdAt, longTermTargets)
|
|
|
391
408
|
createdAt,
|
|
392
409
|
raw: relativePath(root, longTermTargets.raw),
|
|
393
410
|
sourceCard: relativePath(root, longTermTargets.sourceCard),
|
|
411
|
+
wikiEntry: relativePath(root, longTermTargets.wikiEntry),
|
|
394
412
|
claims: relativePath(root, longTermTargets.claims),
|
|
395
413
|
assets: relativePath(root, longTermTargets.assets),
|
|
396
414
|
topics: relativePath(root, longTermTargets.topics),
|
|
@@ -400,6 +418,7 @@ function buildArtifactLinks(root, slug, runDirName, createdAt, longTermTargets)
|
|
|
400
418
|
}
|
|
401
419
|
function relationshipFrontmatter(links) {
|
|
402
420
|
return [
|
|
421
|
+
`wiki_entry: "${escapeYaml(obsidianLink(links.wikiEntry, "Wiki 条目"))}"`,
|
|
403
422
|
`source_card: "${escapeYaml(obsidianLink(links.sourceCard, "资料卡"))}"`,
|
|
404
423
|
`raw_note: "${escapeYaml(obsidianLink(links.raw, "原文"))}"`,
|
|
405
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/paths.js
CHANGED
|
@@ -16,14 +16,16 @@ export function relativePath(root, target) {
|
|
|
16
16
|
}
|
|
17
17
|
export function slugify(value) {
|
|
18
18
|
const source = value?.trim() || "item";
|
|
19
|
-
const
|
|
19
|
+
const normalized = source
|
|
20
20
|
.normalize("NFKD")
|
|
21
|
-
.replace(/
|
|
21
|
+
.replace(/\p{M}/gu, "");
|
|
22
|
+
const slug = normalized
|
|
23
|
+
.replace(/[^\p{L}\p{N}\s-]/gu, "")
|
|
22
24
|
.trim()
|
|
23
25
|
.toLowerCase()
|
|
24
26
|
.replace(/[-\s_]+/g, "-")
|
|
25
27
|
.replace(/^-+|-+$/g, "");
|
|
26
|
-
return
|
|
28
|
+
return slug || "item";
|
|
27
29
|
}
|
|
28
30
|
export function appendRunIdBeforeExt(fileName, runId) {
|
|
29
31
|
const ext = path.extname(fileName);
|
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
|
+
}
|