@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.
- package/README.md +163 -115
- package/dist/src/app.js +200 -3
- package/dist/src/context.js +131 -0
- package/dist/src/frontmatter.js +55 -0
- package/dist/src/grounding.js +83 -0
- package/dist/src/ingest.js +72 -10
- package/dist/src/lint.js +197 -0
- package/dist/src/payload.js +160 -2
- package/dist/src/wiki-entry.js +180 -0
- package/dist/src/workspace.js +32 -0
- package/docs/AGENT_HANDOFF.md +80 -14
- package/docs/FAQ.md +38 -5
- package/docs/SHOWCASE.md +8 -3
- package/docs/USAGE.md +81 -10
- package/package.json +1 -1
- package/skill/LINT_PROTOCOL.md +42 -0
- package/skill/QUERY_PROTOCOL.md +38 -0
- package/skill/SKILL.md +122 -38
|
@@ -0,0 +1,131 @@
|
|
|
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 } from "./paths.js";
|
|
5
|
+
import { exists } from "./workspace.js";
|
|
6
|
+
const GROUPS = [
|
|
7
|
+
{ key: "wiki_entries", dir: "05-wiki", weight: 6 },
|
|
8
|
+
{ key: "topics", dir: "07-topics/ready", weight: 5 },
|
|
9
|
+
{ key: "source_cards", dir: "03-sources/article-cards", weight: 4 },
|
|
10
|
+
{ key: "claims", dir: "04-claims/_suggestions", weight: 3 },
|
|
11
|
+
{ key: "outlines", dir: "08-outputs/outlines", weight: 2 },
|
|
12
|
+
{ key: "raw_refs", dir: "02-raw/articles", weight: 1 }
|
|
13
|
+
];
|
|
14
|
+
export async function buildContext(rootPath, query, now = new Date().toISOString()) {
|
|
15
|
+
const root = path.resolve(rootPath);
|
|
16
|
+
const tokens = tokenize(query);
|
|
17
|
+
const result = {
|
|
18
|
+
schema_version: "aiwiki.context.v1",
|
|
19
|
+
query,
|
|
20
|
+
generated_at: now,
|
|
21
|
+
matches: {
|
|
22
|
+
wiki_entries: [],
|
|
23
|
+
source_cards: [],
|
|
24
|
+
claims: [],
|
|
25
|
+
topics: [],
|
|
26
|
+
outlines: [],
|
|
27
|
+
raw_refs: []
|
|
28
|
+
},
|
|
29
|
+
suggested_answer_structure: ["主题概览", "核心观点", "已有资料依据", "可复用判断", "下一步建议"],
|
|
30
|
+
warnings: []
|
|
31
|
+
};
|
|
32
|
+
if (!tokens.length) {
|
|
33
|
+
result.warnings.push("query is empty after tokenization");
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
for (const group of GROUPS.filter((item) => item.key !== "raw_refs")) {
|
|
37
|
+
const dir = path.join(root, group.dir);
|
|
38
|
+
if (!(await exists(dir))) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const matches = await searchDir(root, dir, tokens, group.weight);
|
|
42
|
+
result.matches[group.key].push(...matches.slice(0, 10));
|
|
43
|
+
}
|
|
44
|
+
if (!result.matches.wiki_entries.length) {
|
|
45
|
+
result.warnings.push("未命中 Wiki Entry,结果可能来自资料卡、选题或原文引用。");
|
|
46
|
+
const rawGroup = GROUPS.find((item) => item.key === "raw_refs");
|
|
47
|
+
if (rawGroup) {
|
|
48
|
+
const rawDir = path.join(root, rawGroup.dir);
|
|
49
|
+
if (await exists(rawDir)) {
|
|
50
|
+
result.matches.raw_refs.push(...(await searchDir(root, rawDir, tokens, rawGroup.weight)).slice(0, 10));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
async function searchDir(root, dir, tokens, weight) {
|
|
57
|
+
const files = await listMarkdownFiles(dir);
|
|
58
|
+
const matches = [];
|
|
59
|
+
for (const file of files) {
|
|
60
|
+
const text = await fs.readFile(file, "utf8");
|
|
61
|
+
const parsed = parseMarkdown(text);
|
|
62
|
+
const rel = relativePath(root, file);
|
|
63
|
+
const title = frontmatterString(parsed.frontmatter, "title") ?? path.basename(file, ".md");
|
|
64
|
+
const haystack = [
|
|
65
|
+
rel,
|
|
66
|
+
title,
|
|
67
|
+
frontmatterString(parsed.frontmatter, "source_url") ?? "",
|
|
68
|
+
frontmatterArray(parsed.frontmatter, "topics").join(" "),
|
|
69
|
+
frontmatterArray(parsed.frontmatter, "tags").join(" "),
|
|
70
|
+
parsed.body
|
|
71
|
+
].join("\n").toLowerCase();
|
|
72
|
+
const hits = tokens.filter((token) => haystack.includes(token.toLowerCase())).length;
|
|
73
|
+
if (!hits) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const generationMode = frontmatterString(parsed.frontmatter, "generation_mode");
|
|
77
|
+
const quality = frontmatterString(parsed.frontmatter, "quality");
|
|
78
|
+
const groundingMarkers = frontmatterArray(parsed.frontmatter, "grounding_markers");
|
|
79
|
+
const groundingNeedsReview = frontmatterBoolean(parsed.frontmatter, "grounding_needs_review");
|
|
80
|
+
const warnings = generationMode === "deterministic_fallback"
|
|
81
|
+
? ["该 Wiki Entry 是 deterministic fallback,仅包含来源、正文预览和待补全区。"]
|
|
82
|
+
: [];
|
|
83
|
+
if (groundingNeedsReview) {
|
|
84
|
+
warnings.push(`Grounding needs review${groundingMarkers.length ? `: ${groundingMarkers.join(", ")}` : ""}.`);
|
|
85
|
+
}
|
|
86
|
+
matches.push({
|
|
87
|
+
title,
|
|
88
|
+
path: rel,
|
|
89
|
+
summary: frontmatterString(parsed.frontmatter, "summary") ?? summarize(parsed.body, quality),
|
|
90
|
+
score: Number(((hits / tokens.length) * weight).toFixed(2)),
|
|
91
|
+
topics: frontmatterArray(parsed.frontmatter, "topics"),
|
|
92
|
+
source_url: frontmatterString(parsed.frontmatter, "source_url") ?? "",
|
|
93
|
+
generation_mode: generationMode,
|
|
94
|
+
quality,
|
|
95
|
+
grounding_evidence_available: frontmatterBoolean(parsed.frontmatter, "grounding_evidence_available"),
|
|
96
|
+
grounding_needs_review: groundingNeedsReview,
|
|
97
|
+
grounding_markers: groundingMarkers,
|
|
98
|
+
warnings
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return matches.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
|
|
102
|
+
}
|
|
103
|
+
async function listMarkdownFiles(dir) {
|
|
104
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
105
|
+
const files = [];
|
|
106
|
+
for (const entry of entries) {
|
|
107
|
+
const target = path.join(dir, entry.name);
|
|
108
|
+
if (entry.isDirectory()) {
|
|
109
|
+
files.push(...await listMarkdownFiles(target));
|
|
110
|
+
}
|
|
111
|
+
else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
|
|
112
|
+
files.push(target);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return files;
|
|
116
|
+
}
|
|
117
|
+
function tokenize(value) {
|
|
118
|
+
const compact = value.trim();
|
|
119
|
+
if (!compact) {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
const asciiTokens = compact.split(/[^\p{L}\p{N}]+/u).filter((token) => token.length >= 2);
|
|
123
|
+
return Array.from(new Set([compact, ...asciiTokens]));
|
|
124
|
+
}
|
|
125
|
+
function summarize(body, quality) {
|
|
126
|
+
const compact = body.replace(/\s+/g, " ").trim();
|
|
127
|
+
if (quality === "scaffold") {
|
|
128
|
+
return "仅有正文预览,未生成高质量摘要。";
|
|
129
|
+
}
|
|
130
|
+
return compact.length > 180 ? `${compact.slice(0, 180)}...` : compact;
|
|
131
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export function parseMarkdown(text) {
|
|
2
|
+
if (!text.startsWith("---")) {
|
|
3
|
+
return { frontmatter: {}, body: text };
|
|
4
|
+
}
|
|
5
|
+
const end = text.indexOf("\n---", 3);
|
|
6
|
+
if (end === -1) {
|
|
7
|
+
return { frontmatter: {}, body: text };
|
|
8
|
+
}
|
|
9
|
+
const rawFrontmatter = text.slice(3, end).trim();
|
|
10
|
+
const body = text.slice(end).replace(/^\n---\r?\n?/, "");
|
|
11
|
+
return { frontmatter: parseFrontmatter(rawFrontmatter), body };
|
|
12
|
+
}
|
|
13
|
+
export function parseFrontmatter(text) {
|
|
14
|
+
const result = {};
|
|
15
|
+
for (const line of text.split(/\r?\n/)) {
|
|
16
|
+
const match = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line.trim());
|
|
17
|
+
if (!match) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
result[match[1]] = parseScalar(match[2]);
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
export function frontmatterString(value, key) {
|
|
25
|
+
const item = value[key];
|
|
26
|
+
return typeof item === "string" ? item : undefined;
|
|
27
|
+
}
|
|
28
|
+
export function frontmatterBoolean(value, key) {
|
|
29
|
+
const item = value[key];
|
|
30
|
+
return typeof item === "boolean" ? item : undefined;
|
|
31
|
+
}
|
|
32
|
+
export function frontmatterArray(value, key) {
|
|
33
|
+
const item = value[key];
|
|
34
|
+
return Array.isArray(item) ? item : [];
|
|
35
|
+
}
|
|
36
|
+
function parseScalar(value) {
|
|
37
|
+
const trimmed = value.trim();
|
|
38
|
+
if (trimmed === "true") {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
if (trimmed === "false") {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
45
|
+
return trimmed
|
|
46
|
+
.slice(1, -1)
|
|
47
|
+
.split(",")
|
|
48
|
+
.map((item) => unquote(item.trim()))
|
|
49
|
+
.filter(Boolean);
|
|
50
|
+
}
|
|
51
|
+
return unquote(trimmed);
|
|
52
|
+
}
|
|
53
|
+
function unquote(value) {
|
|
54
|
+
return value.replace(/^["']|["']$/g, "");
|
|
55
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export function buildGroundingReport(payload) {
|
|
2
|
+
const claims = payload.analysis?.claims ?? [];
|
|
3
|
+
const reusableKnowledge = payload.analysis?.reusable_knowledge ?? [];
|
|
4
|
+
const content = payload.source.content ?? "";
|
|
5
|
+
let evidenceQuoteCount = 0;
|
|
6
|
+
let unsupportedClaimCount = 0;
|
|
7
|
+
const markers = new Set();
|
|
8
|
+
for (const claim of claims) {
|
|
9
|
+
const quote = claim.source_quote?.trim();
|
|
10
|
+
if (quote && content.includes(quote)) {
|
|
11
|
+
evidenceQuoteCount += 1;
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
unsupportedClaimCount += 1;
|
|
15
|
+
markers.add(quote ? "source_quote_not_found" : "unsupported_claims");
|
|
16
|
+
}
|
|
17
|
+
for (const item of reusableKnowledge) {
|
|
18
|
+
const quote = item.source_quote?.trim();
|
|
19
|
+
if (quote && content.includes(quote)) {
|
|
20
|
+
evidenceQuoteCount += 1;
|
|
21
|
+
}
|
|
22
|
+
else if (quote) {
|
|
23
|
+
markers.add("source_quote_not_found");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const coverageSuspectedIncomplete = hasSparseAnalysisForLongContent(payload);
|
|
27
|
+
if (coverageSuspectedIncomplete) {
|
|
28
|
+
markers.add("coverage_suspected_incomplete");
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
evidence_available: evidenceQuoteCount > 0,
|
|
32
|
+
evidence_channel: evidenceQuoteCount > 0 ? "host_supplied" : "none",
|
|
33
|
+
claim_count: claims.length,
|
|
34
|
+
claim_quote_count: claims.filter((claim) => claim.source_quote?.trim() && content.includes(claim.source_quote.trim())).length,
|
|
35
|
+
unsupported_claim_count: unsupportedClaimCount,
|
|
36
|
+
coverage_suspected_incomplete: coverageSuspectedIncomplete,
|
|
37
|
+
needs_review: markers.size > 0,
|
|
38
|
+
suspicion_markers: [...markers]
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function groundingWarnings(report) {
|
|
42
|
+
const warnings = [];
|
|
43
|
+
if (report.unsupported_claim_count > 0) {
|
|
44
|
+
warnings.push(`grounding: ${report.unsupported_claim_count} claim(s) lack a source_quote found in raw content.`);
|
|
45
|
+
}
|
|
46
|
+
if (report.coverage_suspected_incomplete) {
|
|
47
|
+
warnings.push("grounding: coverage_suspected_incomplete is heuristic and needs review.");
|
|
48
|
+
}
|
|
49
|
+
return warnings;
|
|
50
|
+
}
|
|
51
|
+
export function groundingFrontmatterLines(grounding) {
|
|
52
|
+
return [
|
|
53
|
+
`grounding_evidence_available: ${grounding.evidence_available ? "true" : "false"}`,
|
|
54
|
+
`grounding_evidence_channel: "${grounding.evidence_channel}"`,
|
|
55
|
+
`grounding_needs_review: ${grounding.needs_review ? "true" : "false"}`,
|
|
56
|
+
`grounding_markers: ${yamlStringArray(grounding.suspicion_markers)}`,
|
|
57
|
+
`grounding_claim_count: "${grounding.claim_count}"`,
|
|
58
|
+
`grounding_claim_quote_count: "${grounding.claim_quote_count}"`,
|
|
59
|
+
`grounding_unsupported_claim_count: "${grounding.unsupported_claim_count}"`,
|
|
60
|
+
`coverage_suspected_incomplete: ${grounding.coverage_suspected_incomplete ? "true" : "false"}`
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
function hasSparseAnalysisForLongContent(payload) {
|
|
64
|
+
if (!payload.analysis) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
const contentLength = (payload.source.content ?? "").replace(/\s+/g, "").length;
|
|
68
|
+
if (contentLength < 500) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
const extractedSignals = payload.analysis.key_points.length +
|
|
72
|
+
payload.analysis.reusable_knowledge.length +
|
|
73
|
+
payload.analysis.related_concepts.length +
|
|
74
|
+
payload.analysis.use_cases.length +
|
|
75
|
+
payload.analysis.topic_candidates.length;
|
|
76
|
+
return extractedSignals < 3;
|
|
77
|
+
}
|
|
78
|
+
function yamlStringArray(values) {
|
|
79
|
+
return `[${values.map((value) => `"${escapeYaml(value)}"`).join(", ")}]`;
|
|
80
|
+
}
|
|
81
|
+
function escapeYaml(value) {
|
|
82
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
83
|
+
}
|
package/dist/src/ingest.js
CHANGED
|
@@ -2,8 +2,10 @@ import { promises as fs } from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
4
|
import { normalizePayload } from "./payload.js";
|
|
5
|
+
import { buildGroundingReport, groundingFrontmatterLines, groundingWarnings } from "./grounding.js";
|
|
5
6
|
import { appendRunIdBeforeExt, relativePath, safeJoin, slugify } from "./paths.js";
|
|
6
7
|
import { initWorkspace } from "./workspace.js";
|
|
8
|
+
import { renderWikiEntry } from "./wiki-entry.js";
|
|
7
9
|
export async function ingestPayload(rootPath, rawPayload) {
|
|
8
10
|
await initWorkspace(rootPath);
|
|
9
11
|
const root = path.resolve(rootPath);
|
|
@@ -16,10 +18,11 @@ export async function ingestPayload(rootPath, rawPayload) {
|
|
|
16
18
|
const generatedFiles = [];
|
|
17
19
|
await writeFile(path.join(runDir, "payload.json"), `${JSON.stringify(payload, null, 2)}\n`, generatedFiles);
|
|
18
20
|
if (payload.source.fetch_status === "failed") {
|
|
21
|
+
const grounding = buildGroundingReport(payload);
|
|
19
22
|
await writeSummary(root, runDir, payload, generatedFiles, [
|
|
20
23
|
...payload.warnings,
|
|
21
24
|
"宿主 Agent 未能提供正文,AIWiki CLI 没有自行抓取网页。"
|
|
22
|
-
]);
|
|
25
|
+
], undefined, grounding);
|
|
23
26
|
return { runId: runDirName, runDir, generatedFiles, warnings: payload.warnings, agentReport: buildAgentReport(root, runDir, payload, generatedFiles) };
|
|
24
27
|
}
|
|
25
28
|
const slug = slugify(payload.source.title ?? payload.source.url);
|
|
@@ -27,19 +30,23 @@ export async function ingestPayload(rootPath, rawPayload) {
|
|
|
27
30
|
const collisionWarnings = [];
|
|
28
31
|
const longTermTargets = await chooseLongTermTargets(root, slug, runId, collisionWarnings);
|
|
29
32
|
const links = buildArtifactLinks(root, slug, runDirName, runStartedAt, longTermTargets);
|
|
33
|
+
const grounding = buildGroundingReport(payload);
|
|
30
34
|
await writeFile(path.join(runDir, "raw.md"), contentFile(payload, content, links), generatedFiles);
|
|
31
|
-
await writeFile(path.join(runDir, "source-card.md"), sourceCard(payload, runDirName, links), generatedFiles);
|
|
35
|
+
await writeFile(path.join(runDir, "source-card.md"), sourceCard(payload, runDirName, links, grounding), generatedFiles);
|
|
36
|
+
const wikiEntryResult = renderWikiEntry(payload, links);
|
|
37
|
+
await writeFile(path.join(runDir, "wiki-entry.md"), wikiEntryResult.markdown, generatedFiles);
|
|
32
38
|
await writeFile(path.join(runDir, "creative-assets.md"), creativeAssets(payload, links), generatedFiles);
|
|
33
39
|
await writeFile(path.join(runDir, "topics.md"), topics(payload, links), generatedFiles);
|
|
34
40
|
await writeFile(path.join(runDir, "draft-outline.md"), outline(payload, links), generatedFiles);
|
|
35
41
|
await writeFile(longTermTargets.raw, contentFile(payload, content, links), generatedFiles);
|
|
36
|
-
await writeFile(longTermTargets.sourceCard, sourceCard(payload, runDirName, links), generatedFiles);
|
|
37
|
-
await writeFile(longTermTargets.
|
|
42
|
+
await writeFile(longTermTargets.sourceCard, sourceCard(payload, runDirName, links, grounding), generatedFiles);
|
|
43
|
+
await writeFile(longTermTargets.wikiEntry, wikiEntryResult.markdown, generatedFiles);
|
|
44
|
+
await writeFile(longTermTargets.claims, claims(payload, links, grounding), generatedFiles);
|
|
38
45
|
await writeFile(longTermTargets.assets, creativeAssets(payload, links), generatedFiles);
|
|
39
46
|
await writeFile(longTermTargets.topics, topics(payload, links), generatedFiles);
|
|
40
47
|
await writeFile(longTermTargets.outline, outline(payload, links), generatedFiles);
|
|
41
|
-
const warnings = [...payload.warnings, ...collisionWarnings];
|
|
42
|
-
await writeSummary(root, runDir, payload, generatedFiles, warnings, links);
|
|
48
|
+
const warnings = [...payload.warnings, ...groundingWarnings(grounding), ...collisionWarnings];
|
|
49
|
+
await writeSummary(root, runDir, payload, generatedFiles, warnings, links, grounding);
|
|
43
50
|
return { runId, runDir, generatedFiles, warnings, agentReport: buildAgentReport(root, runDir, payload, generatedFiles) };
|
|
44
51
|
}
|
|
45
52
|
export async function ingestFile(rootPath, filePath) {
|
|
@@ -72,6 +79,7 @@ async function chooseLongTermTargets(root, slug, runId, warnings) {
|
|
|
72
79
|
return {
|
|
73
80
|
raw: await chooseLongTermTarget(root, "02-raw/articles", `${slug}.md`, runId, warnings),
|
|
74
81
|
sourceCard: await chooseLongTermTarget(root, "03-sources/article-cards", `${slug}.md`, runId, warnings),
|
|
82
|
+
wikiEntry: await chooseLongTermTarget(root, "05-wiki/source-knowledge", `${slug}.md`, runId, warnings),
|
|
75
83
|
claims: await chooseLongTermTarget(root, "04-claims/_suggestions", `${slug}-claims.md`, runId, warnings),
|
|
76
84
|
assets: await chooseLongTermTarget(root, "06-assets/_suggestions", `${slug}-assets.md`, runId, warnings),
|
|
77
85
|
topics: await chooseLongTermTarget(root, "07-topics/ready", `${slug}-topics.md`, runId, warnings),
|
|
@@ -106,7 +114,7 @@ async function writeFile(target, content, generatedFiles) {
|
|
|
106
114
|
}
|
|
107
115
|
generatedFiles.push(target);
|
|
108
116
|
}
|
|
109
|
-
async function writeSummary(root, runDir, payload, generatedFiles, warnings, links) {
|
|
117
|
+
async function writeSummary(root, runDir, payload, generatedFiles, warnings, links, grounding = buildGroundingReport(payload)) {
|
|
110
118
|
const summaryPath = path.join(runDir, "processing-summary.md");
|
|
111
119
|
const files = [...generatedFiles, summaryPath];
|
|
112
120
|
const runId = path.basename(runDir);
|
|
@@ -125,6 +133,7 @@ async function writeSummary(root, runDir, payload, generatedFiles, warnings, lin
|
|
|
125
133
|
`captured_at: "${escapeYaml(payload.source.captured_at)}"`,
|
|
126
134
|
`run_id: "${escapeYaml(runId)}"`,
|
|
127
135
|
...(links ? relationshipFrontmatter(links) : []),
|
|
136
|
+
...groundingFrontmatterLines(grounding),
|
|
128
137
|
`tags: ["aiwiki/run"]`,
|
|
129
138
|
"---",
|
|
130
139
|
"",
|
|
@@ -140,6 +149,13 @@ async function writeSummary(root, runDir, payload, generatedFiles, warnings, lin
|
|
|
140
149
|
"告警:",
|
|
141
150
|
...(warnings.length ? warnings.map((warning) => `- ${warning}`) : ["- none"]),
|
|
142
151
|
"",
|
|
152
|
+
"Grounding:",
|
|
153
|
+
`- evidence_available: ${grounding.evidence_available ? "yes" : "no"}`,
|
|
154
|
+
`- evidence_channel: ${grounding.evidence_channel}`,
|
|
155
|
+
`- needs_review: ${grounding.needs_review ? "yes" : "no"}`,
|
|
156
|
+
`- suspicion_markers: ${grounding.suspicion_markers.length ? grounding.suspicion_markers.join(", ") : "none"}`,
|
|
157
|
+
`- claims_with_quotes: ${grounding.claim_quote_count}/${grounding.claim_count}`,
|
|
158
|
+
"",
|
|
143
159
|
"下一步审阅:",
|
|
144
160
|
"- 请在 Obsidian 中人工审阅资料卡、Claim 建议、素材建议、选题和大纲。",
|
|
145
161
|
"- AIWiki CLI 不负责网页抓取稳定性。"
|
|
@@ -168,6 +184,7 @@ function contentFile(payload, content, links) {
|
|
|
168
184
|
"",
|
|
169
185
|
"## AIWiki 链接",
|
|
170
186
|
"",
|
|
187
|
+
`- Wiki 条目:${obsidianLink(links.wikiEntry, "Wiki 条目")}`,
|
|
171
188
|
`- 资料卡:${obsidianLink(links.sourceCard, "资料卡")}`,
|
|
172
189
|
`- 处理记录:${obsidianLink(links.runSummary, "处理记录")}`,
|
|
173
190
|
"",
|
|
@@ -175,7 +192,7 @@ function contentFile(payload, content, links) {
|
|
|
175
192
|
""
|
|
176
193
|
].join("\n");
|
|
177
194
|
}
|
|
178
|
-
function sourceCard(payload, runId, links) {
|
|
195
|
+
function sourceCard(payload, runId, links, grounding) {
|
|
179
196
|
return [
|
|
180
197
|
"---",
|
|
181
198
|
`aiwiki_id: "${escapeYaml(`${links.slug}:source-card`)}"`,
|
|
@@ -191,6 +208,7 @@ function sourceCard(payload, runId, links) {
|
|
|
191
208
|
`captured_at: "${escapeYaml(payload.source.captured_at)}"`,
|
|
192
209
|
`run_id: "${escapeYaml(runId)}"`,
|
|
193
210
|
...relationshipFrontmatter(links),
|
|
211
|
+
...groundingFrontmatterLines(grounding),
|
|
194
212
|
`aliases: ["${escapeYaml(payload.source.title ?? "Untitled")}"]`,
|
|
195
213
|
`tags: ["aiwiki/source-card"]`,
|
|
196
214
|
"---",
|
|
@@ -199,6 +217,7 @@ function sourceCard(payload, runId, links) {
|
|
|
199
217
|
"",
|
|
200
218
|
"## Obsidian 链接",
|
|
201
219
|
"",
|
|
220
|
+
`- Wiki 条目:${obsidianLink(links.wikiEntry, "Wiki 条目")}`,
|
|
202
221
|
`- 原文:${obsidianLink(links.raw, "原文")}`,
|
|
203
222
|
`- Claim 建议:${obsidianLink(links.claims, "Claim 建议")}`,
|
|
204
223
|
`- 素材建议:${obsidianLink(links.assets, "素材建议")}`,
|
|
@@ -209,10 +228,19 @@ function sourceCard(payload, runId, links) {
|
|
|
209
228
|
"## 摘要",
|
|
210
229
|
"",
|
|
211
230
|
trimPreview(payload.source.content ?? payload.source.fetch_notes ?? ""),
|
|
231
|
+
"",
|
|
232
|
+
"## Grounding 状态",
|
|
233
|
+
"",
|
|
234
|
+
`- 证据通道:${grounding.evidence_channel}`,
|
|
235
|
+
`- 需要复核:${grounding.needs_review ? "yes" : "no"}`,
|
|
236
|
+
`- 疑似标记:${grounding.suspicion_markers.length ? grounding.suspicion_markers.join(", ") : "none"}`,
|
|
212
237
|
""
|
|
213
238
|
].join("\n");
|
|
214
239
|
}
|
|
215
|
-
function claims(payload, links) {
|
|
240
|
+
function claims(payload, links, grounding) {
|
|
241
|
+
const claimLines = payload.analysis?.claims.length
|
|
242
|
+
? payload.analysis.claims.flatMap((item, index) => claimSuggestionLines(index + 1, item.claim, item.confidence, item.source_quote, payload.source.content ?? ""))
|
|
243
|
+
: ["- 暂无宿主 Agent 提供的 Claim。"];
|
|
216
244
|
return [
|
|
217
245
|
"---",
|
|
218
246
|
`aiwiki_id: "${escapeYaml(`${links.slug}:claims`)}"`,
|
|
@@ -226,14 +254,26 @@ function claims(payload, links) {
|
|
|
226
254
|
`captured_at: "${escapeYaml(payload.source.captured_at)}"`,
|
|
227
255
|
`run_id: "${escapeYaml(links.runId)}"`,
|
|
228
256
|
...relationshipFrontmatter(links),
|
|
257
|
+
...groundingFrontmatterLines(grounding),
|
|
229
258
|
`tags: ["aiwiki/claims"]`,
|
|
230
259
|
"---",
|
|
231
260
|
"",
|
|
232
261
|
"# Claim 建议",
|
|
233
262
|
"",
|
|
263
|
+
`- Wiki 条目:${obsidianLink(links.wikiEntry, "Wiki 条目")}`,
|
|
234
264
|
`- 资料卡:${obsidianLink(links.sourceCard, "资料卡")}`,
|
|
235
265
|
`- 原文:${obsidianLink(links.raw, "原文")}`,
|
|
236
266
|
`- 待人工审阅:${payload.source.title ?? "Untitled"}`,
|
|
267
|
+
"",
|
|
268
|
+
"## Grounding",
|
|
269
|
+
"",
|
|
270
|
+
`- evidence_channel: ${grounding.evidence_channel}`,
|
|
271
|
+
`- needs_review: ${grounding.needs_review ? "yes" : "no"}`,
|
|
272
|
+
`- suspicion_markers: ${grounding.suspicion_markers.length ? grounding.suspicion_markers.join(", ") : "none"}`,
|
|
273
|
+
"",
|
|
274
|
+
"## 建议",
|
|
275
|
+
"",
|
|
276
|
+
...claimLines,
|
|
237
277
|
""
|
|
238
278
|
].join("\n");
|
|
239
279
|
}
|
|
@@ -256,6 +296,7 @@ function creativeAssets(payload, links) {
|
|
|
256
296
|
"",
|
|
257
297
|
"# 素材建议",
|
|
258
298
|
"",
|
|
299
|
+
`- Wiki 条目:${obsidianLink(links.wikiEntry, "Wiki 条目")}`,
|
|
259
300
|
`- 资料卡:${obsidianLink(links.sourceCard, "资料卡")}`,
|
|
260
301
|
`- 原文:${obsidianLink(links.raw, "原文")}`,
|
|
261
302
|
`- 可复用素材:${payload.source.title ?? "Untitled"}`,
|
|
@@ -281,6 +322,7 @@ function topics(payload, links) {
|
|
|
281
322
|
"",
|
|
282
323
|
"# 选题候选",
|
|
283
324
|
"",
|
|
325
|
+
`- Wiki 条目:${obsidianLink(links.wikiEntry, "Wiki 条目")}`,
|
|
284
326
|
`- 资料卡:${obsidianLink(links.sourceCard, "资料卡")}`,
|
|
285
327
|
`- 大纲:${obsidianLink(links.outline, "大纲")}`,
|
|
286
328
|
`- ${payload.source.title ?? "Untitled"}`,
|
|
@@ -306,6 +348,7 @@ function outline(payload, links) {
|
|
|
306
348
|
"",
|
|
307
349
|
"# 草稿大纲",
|
|
308
350
|
"",
|
|
351
|
+
`Wiki 条目:${obsidianLink(links.wikiEntry, "Wiki 条目")}`,
|
|
309
352
|
`资料卡:${obsidianLink(links.sourceCard, "资料卡")}`,
|
|
310
353
|
`原文:${obsidianLink(links.raw, "原文")}`,
|
|
311
354
|
"",
|
|
@@ -316,6 +359,19 @@ function outline(payload, links) {
|
|
|
316
359
|
""
|
|
317
360
|
].join("\n");
|
|
318
361
|
}
|
|
362
|
+
function claimSuggestionLines(index, claim, confidence, sourceQuote, content) {
|
|
363
|
+
const quote = sourceQuote?.trim();
|
|
364
|
+
const supported = Boolean(quote && content.includes(quote));
|
|
365
|
+
return [
|
|
366
|
+
`### Claim ${index}`,
|
|
367
|
+
"",
|
|
368
|
+
`- claim: ${claim}`,
|
|
369
|
+
`- confidence: ${confidence ?? "unknown"}`,
|
|
370
|
+
`- evidence_status: ${supported ? "host_quote_found" : "needs_review"}`,
|
|
371
|
+
...(quote ? [`- source_quote: ${quote}`] : ["- source_quote: missing"]),
|
|
372
|
+
""
|
|
373
|
+
];
|
|
374
|
+
}
|
|
319
375
|
function trimPreview(value) {
|
|
320
376
|
return value.trim().slice(0, 500) || "待人工补充。";
|
|
321
377
|
}
|
|
@@ -334,11 +390,15 @@ function buildAgentReport(root, runDir, payload, generatedFiles) {
|
|
|
334
390
|
summary: fetchFailed ? (payload.source.fetch_notes ?? "宿主 Agent 没有提供可读正文。") : summarizeContent(content),
|
|
335
391
|
keyFiles: {
|
|
336
392
|
processingSummary: relativePath(root, path.join(runDir, "processing-summary.md")),
|
|
393
|
+
wikiEntry: findGeneratedFileInDir(root, generatedFiles, "05-wiki/source-knowledge"),
|
|
337
394
|
sourceCard: findGeneratedFileInDir(root, generatedFiles, "03-sources/article-cards"),
|
|
338
395
|
draftOutline: findGeneratedFile(root, generatedFiles, "draft-outline.md"),
|
|
339
396
|
dashboard: "dashboards/AIWiki Home.md",
|
|
340
397
|
reviewQueue: "dashboards/Review Queue.md"
|
|
341
|
-
}
|
|
398
|
+
},
|
|
399
|
+
wikiEntryGenerationMode: fetchFailed ? undefined : (payload.wiki_entry || payload.analysis ? "agent_enriched" : "deterministic_fallback"),
|
|
400
|
+
wikiEntryQuality: fetchFailed ? undefined : (payload.wiki_entry || payload.analysis ? "enriched" : "scaffold"),
|
|
401
|
+
grounding: buildGroundingReport(payload)
|
|
342
402
|
};
|
|
343
403
|
}
|
|
344
404
|
function estimateFitScore(payload, content) {
|
|
@@ -394,6 +454,7 @@ function buildArtifactLinks(root, slug, runDirName, createdAt, longTermTargets)
|
|
|
394
454
|
createdAt,
|
|
395
455
|
raw: relativePath(root, longTermTargets.raw),
|
|
396
456
|
sourceCard: relativePath(root, longTermTargets.sourceCard),
|
|
457
|
+
wikiEntry: relativePath(root, longTermTargets.wikiEntry),
|
|
397
458
|
claims: relativePath(root, longTermTargets.claims),
|
|
398
459
|
assets: relativePath(root, longTermTargets.assets),
|
|
399
460
|
topics: relativePath(root, longTermTargets.topics),
|
|
@@ -403,6 +464,7 @@ function buildArtifactLinks(root, slug, runDirName, createdAt, longTermTargets)
|
|
|
403
464
|
}
|
|
404
465
|
function relationshipFrontmatter(links) {
|
|
405
466
|
return [
|
|
467
|
+
`wiki_entry: "${escapeYaml(obsidianLink(links.wikiEntry, "Wiki 条目"))}"`,
|
|
406
468
|
`source_card: "${escapeYaml(obsidianLink(links.sourceCard, "资料卡"))}"`,
|
|
407
469
|
`raw_note: "${escapeYaml(obsidianLink(links.raw, "原文"))}"`,
|
|
408
470
|
`claims_note: "${escapeYaml(obsidianLink(links.claims, "Claim 建议"))}"`,
|