@itradingai/aiwiki 0.2.11 → 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 CHANGED
@@ -30,9 +30,10 @@ AIWiki 是一个开源的 Agent-first 本地 LLM-wiki CLI。
30
30
 
31
31
  ```bash
32
32
  aiwiki context "AI Agent 出海机会"
33
+ aiwiki query "AI Agent 出海机会"
33
34
  ```
34
35
 
35
- `context` 返回 JSON,主要给宿主 Agent 用。第一版是本地关键词检索,不是向量检索。
36
+ `context` 返回 JSON,主要给宿主 Agent 用;`query` 使用同一套检索结果,输出给人看的摘要。第一版是本地关键词检索,不是向量检索。
36
37
 
37
38
  ### Lint:检查知识库结构
38
39
 
@@ -71,6 +72,7 @@ aiwiki agent install
71
72
  ```bash
72
73
  aiwiki agent list
73
74
  aiwiki agent install
75
+ aiwiki agent check
74
76
  ```
75
77
 
76
78
  也可以直接输出通用协议:
@@ -103,6 +105,12 @@ aiwiki prompt agent
103
105
  aiwiki context "xxx"
104
106
  ```
105
107
 
108
+ 人直接查询时可以运行:
109
+
110
+ ```bash
111
+ aiwiki query "xxx"
112
+ ```
113
+
106
114
  ## AIWiki 会生成什么
107
115
 
108
116
  成功入库会生成:
@@ -131,6 +139,8 @@ generation_mode: "agent_enriched"
131
139
  quality: "enriched"
132
140
  generated_by: "host_agent"
133
141
  llm_enriched: true
142
+ source_role: "input"
143
+ represents_user_view: false
134
144
  ```
135
145
 
136
146
  ### Deterministic Fallback Wiki Entry
@@ -144,6 +154,8 @@ generation_mode: "deterministic_fallback"
144
154
  quality: "scaffold"
145
155
  generated_by: "aiwiki_cli"
146
156
  llm_enriched: false
157
+ source_role: "input"
158
+ represents_user_view: false
147
159
  ```
148
160
 
149
161
  AIWiki CLI 本身不调用 LLM,所以不会在没有 Agent 分析字段时承诺高质量提炼。
@@ -156,7 +168,7 @@ AIWiki 做:
156
168
  - 写入本地 Markdown。
157
169
  - 生成 frontmatter、wikilink、处理记录。
158
170
  - 生成 Wiki Entry 容器。
159
- - 支持 `context` 和 `lint`。
171
+ - 支持 `context`、`query`、`next` 和 `lint`。
160
172
 
161
173
  AIWiki 不做:
162
174
 
@@ -195,7 +207,7 @@ Review Queue 可以保留为回看入口,但不是 AIWiki 的主流程。
195
207
 
196
208
  ### 什么内容才代表我的观点?
197
209
 
198
- 后续 `source_role=output` 会用于标记用户已发布文章、演讲稿、公众号文章等个人输出。本阶段外部资料默认是 `source_role: input`、`represents_user_view: false`。
210
+ `source_role=output` 用于标记用户已发布文章、演讲稿、公众号文章等个人输出,并可配合 `represents_user_view: true`。外部资料默认是 `source_role: input`、`represents_user_view: false`。
199
211
 
200
212
  ### Dataview 必须安装吗?
201
213
 
@@ -227,6 +239,8 @@ aiwiki setup --path "F:\knowledge_data\aiwiki-test" --yes
227
239
  aiwiki doctor
228
240
  aiwiki ingest-agent --payload tests/fixtures/agent_payload.url.valid.json --path "F:\knowledge_data\aiwiki-test"
229
241
  aiwiki context "AI Agent" --path "F:\knowledge_data\aiwiki-test"
242
+ aiwiki query "AI Agent" --path "F:\knowledge_data\aiwiki-test"
243
+ aiwiki next --path "F:\knowledge_data\aiwiki-test"
230
244
  aiwiki lint --path "F:\knowledge_data\aiwiki-test"
231
245
  ```
232
246
 
package/dist/src/app.js CHANGED
@@ -54,6 +54,10 @@ export async function runCli(argv, streams = { stdout: process.stdout, stderr: p
54
54
  }
55
55
  return 0;
56
56
  }
57
+ if (command === "agent" && subcommand === "check") {
58
+ await printAgentCheck(streams.stdout, await discoverAgentTargets());
59
+ return 0;
60
+ }
57
61
  if (command === "agent" && (subcommand === "list" || !subcommand)) {
58
62
  printAgentList(streams.stdout, await discoverAgentTargets());
59
63
  return 0;
@@ -124,6 +128,7 @@ export async function runCli(argv, streams = { stdout: process.stdout, stderr: p
124
128
  writeLine(streams.stdout, `处理次数: ${summary.runCount}`);
125
129
  writeLine(streams.stdout, `失败次数: ${summary.failedCount}`);
126
130
  writeLine(streams.stdout, `最近处理: ${summary.lastRunId ?? "无"}`);
131
+ await printStatusDetails(streams.stdout, root, summary.runCount);
127
132
  return 0;
128
133
  }
129
134
  if (command === "context") {
@@ -135,6 +140,23 @@ export async function runCli(argv, streams = { stdout: process.stdout, stderr: p
135
140
  writeLine(streams.stdout, JSON.stringify(await buildContext(root, query), null, 2));
136
141
  return 0;
137
142
  }
143
+ if (command === "query") {
144
+ const root = await resolveWorkspace(flagString(args, "path"));
145
+ const query = args.positional.slice(1).join(" ").trim();
146
+ if (!query) {
147
+ throw new CliError("请提供查询主题。");
148
+ }
149
+ writeLine(streams.stdout, renderQuery(await buildContext(root, query)));
150
+ return 0;
151
+ }
152
+ if (command === "next") {
153
+ const root = await resolveWorkspace(flagString(args, "path"));
154
+ const summary = await statusSummary(root);
155
+ const checks = await doctor(root);
156
+ const report = summary.runCount > 0 ? await lintWorkspace(root) : undefined;
157
+ await printNext(streams.stdout, root, summary.runCount, checks, await discoverAgentTargets(), report);
158
+ return 0;
159
+ }
138
160
  if (command === "lint") {
139
161
  const root = await resolveWorkspace(flagString(args, "path"));
140
162
  const report = await lintWorkspace(root);
@@ -223,6 +245,8 @@ function printHelp(stream) {
223
245
  writeLine(stream, " aiwiki doctor");
224
246
  writeLine(stream, " aiwiki status");
225
247
  writeLine(stream, " aiwiki context <query>");
248
+ writeLine(stream, " aiwiki query <query>");
249
+ writeLine(stream, " aiwiki next");
226
250
  writeLine(stream, " aiwiki lint");
227
251
  writeLine(stream, " aiwiki ingest-agent --stdin");
228
252
  writeLine(stream, " aiwiki ingest-file --file <file>");
@@ -230,6 +254,7 @@ function printHelp(stream) {
230
254
  writeLine(stream, " aiwiki config show");
231
255
  writeLine(stream, " aiwiki ingest-agent --payload <file>");
232
256
  writeLine(stream, " aiwiki ingest-url <url> --content-file <file>");
257
+ writeLine(stream, " aiwiki agent check");
233
258
  }
234
259
  async function discoverAgentTargets() {
235
260
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
@@ -309,6 +334,19 @@ function printAgentList(stream, targets) {
309
334
  }
310
335
  }
311
336
  }
337
+ async function printAgentCheck(stream, targets) {
338
+ writeLine(stream, "AIWiki Agent 接入检查");
339
+ for (const target of targets) {
340
+ const installed = target.target ? await exists(target.target) : false;
341
+ writeLine(stream, `${target.id}: ${target.name} | detected=${target.detected ? "yes" : "no"} | installed=${installed ? "yes" : "no"} | installable=${target.installable ? "yes" : "no"}`);
342
+ if (target.detected && target.installable && !installed) {
343
+ writeLine(stream, ` 建议: aiwiki agent install --agent ${target.id} --yes`);
344
+ }
345
+ else if (target.detected && !target.installable) {
346
+ writeLine(stream, " 建议: aiwiki prompt agent");
347
+ }
348
+ }
349
+ }
312
350
  async function installAgentSkill(options) {
313
351
  const targets = await discoverAgentTargets();
314
352
  const installable = targets.filter((target) => target.detected && target.installable);
@@ -389,6 +427,127 @@ function printAgentPrompt(stream) {
389
427
  writeLine(stream, "");
390
428
  writeLine(stream, "禁止:让用户保存 payload;让用户每次输入 --path;声称 AIWiki CLI 负责网页抓取;声称 AIWiki CLI 会在没有 Agent 分析字段时自动高质量总结。");
391
429
  }
430
+ async function printStatusDetails(stream, root, runCount) {
431
+ const counts = await contentCounts(root);
432
+ const lintPath = path.join(root, "dashboards", "Lint Report.md");
433
+ writeLine(stream, "");
434
+ writeLine(stream, "内容统计:");
435
+ writeLine(stream, `Wiki 条目: ${counts.wikiEntries}`);
436
+ writeLine(stream, `资料卡: ${counts.sourceCards}`);
437
+ writeLine(stream, `原文: ${counts.rawFiles}`);
438
+ writeLine(stream, `选题: ${counts.topics}`);
439
+ writeLine(stream, `大纲: ${counts.outlines}`);
440
+ writeLine(stream, `最近 lint: ${await exists(lintPath) ? await relativeMtime(root, lintPath) : "无"}`);
441
+ writeLine(stream, "");
442
+ writeLine(stream, "下一步建议:");
443
+ writeLine(stream, runCount === 0 ? "运行 `aiwiki agent install` 接入宿主 Agent,然后发送 `入库 <url>`。" : "运行 `aiwiki query <主题>` 查询知识,或运行 `aiwiki lint` 检查结构。");
444
+ }
445
+ async function printNext(stream, root, runCount, checks, targets, report) {
446
+ const missing = checks.filter((check) => check.status !== "ok");
447
+ const installableMissing = [];
448
+ for (const target of targets) {
449
+ if (target.detected && target.installable && target.target && !(await exists(target.target))) {
450
+ installableMissing.push(target);
451
+ }
452
+ }
453
+ writeLine(stream, "AIWiki 下一步建议");
454
+ writeLine(stream, `知识库路径: ${root}`);
455
+ if (missing.length) {
456
+ writeLine(stream, "");
457
+ writeLine(stream, "先修复知识库结构:");
458
+ writeLine(stream, `- aiwiki setup --path "${root}" --yes`);
459
+ return;
460
+ }
461
+ if (runCount === 0) {
462
+ writeLine(stream, "");
463
+ writeLine(stream, "还没有入库记录。");
464
+ writeLine(stream, "- aiwiki agent install");
465
+ writeLine(stream, "- 然后向宿主 Agent 发送 `入库 <url>`");
466
+ writeLine(stream, "- CLI 不抓网页;网页正文由宿主 Agent 提供。");
467
+ return;
468
+ }
469
+ const actionableIssues = report?.issues.filter((issue) => issue.severity !== "info") ?? [];
470
+ if (actionableIssues.length) {
471
+ writeLine(stream, "");
472
+ writeLine(stream, `结构检查发现 ${actionableIssues.length} 个需要处理的问题。`);
473
+ writeLine(stream, "- aiwiki lint");
474
+ writeLine(stream, `- 查看报告: dashboards/Lint Report.md`);
475
+ writeLine(stream, "- 先处理 error / warning,再继续扩展查询或入库。");
476
+ return;
477
+ }
478
+ writeLine(stream, "");
479
+ writeLine(stream, "已有入库记录,可以继续:");
480
+ writeLine(stream, "- aiwiki query <主题>");
481
+ writeLine(stream, "- aiwiki lint");
482
+ if (installableMissing.length) {
483
+ writeLine(stream, "");
484
+ writeLine(stream, "可补充宿主 Agent 接入:");
485
+ for (const target of installableMissing) {
486
+ writeLine(stream, `- aiwiki agent install --agent ${target.id} --yes`);
487
+ }
488
+ }
489
+ }
490
+ function renderQuery(context) {
491
+ const lines = [`AIWiki 查询: ${context.query}`, ""];
492
+ appendQueryGroup(lines, "Wiki 条目", context.matches.wiki_entries);
493
+ appendQueryGroup(lines, "资料卡", context.matches.source_cards);
494
+ appendQueryGroup(lines, "选题", context.matches.topics);
495
+ appendQueryGroup(lines, "Claim 建议", context.matches.claims);
496
+ appendQueryGroup(lines, "大纲", context.matches.outlines);
497
+ appendQueryGroup(lines, "原文引用", context.matches.raw_refs);
498
+ if (context.warnings.length) {
499
+ lines.push("提示:", ...context.warnings.map((warning) => `- ${warning}`), "");
500
+ }
501
+ lines.push("Agent JSON:", `- aiwiki context "${context.query}"`);
502
+ return `${lines.join("\n")}\n`;
503
+ }
504
+ function appendQueryGroup(lines, label, items) {
505
+ lines.push(`${label}:`);
506
+ if (!items.length) {
507
+ lines.push("- 无", "");
508
+ return;
509
+ }
510
+ for (const item of items.slice(0, 5)) {
511
+ lines.push(`- ${item.title} (${item.path})`);
512
+ if (item.summary) {
513
+ lines.push(` ${item.summary}`);
514
+ }
515
+ if (item.warnings.length) {
516
+ lines.push(` 提示: ${item.warnings.join(";")}`);
517
+ }
518
+ }
519
+ lines.push("");
520
+ }
521
+ async function contentCounts(root) {
522
+ return {
523
+ wikiEntries: await countMarkdownFiles(path.join(root, "05-wiki")),
524
+ sourceCards: await countMarkdownFiles(path.join(root, "03-sources", "article-cards")),
525
+ rawFiles: await countMarkdownFiles(path.join(root, "02-raw", "articles")),
526
+ topics: await countMarkdownFiles(path.join(root, "07-topics", "ready")),
527
+ outlines: await countMarkdownFiles(path.join(root, "08-outputs", "outlines"))
528
+ };
529
+ }
530
+ async function countMarkdownFiles(dir) {
531
+ if (!(await exists(dir))) {
532
+ return 0;
533
+ }
534
+ const entries = await fs.readdir(dir, { withFileTypes: true });
535
+ let count = 0;
536
+ for (const entry of entries) {
537
+ const target = path.join(dir, entry.name);
538
+ if (entry.isDirectory()) {
539
+ count += await countMarkdownFiles(target);
540
+ }
541
+ else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
542
+ count += 1;
543
+ }
544
+ }
545
+ return count;
546
+ }
547
+ async function relativeMtime(root, target) {
548
+ const stats = await fs.stat(target);
549
+ return `${path.relative(root, target).replace(/\\/g, "/")} (${stats.mtime.toISOString()})`;
550
+ }
392
551
  function doctorStatusText(status) {
393
552
  if (status === "ok") {
394
553
  return "正常";
@@ -422,6 +581,11 @@ function printIngestResult(stream, result) {
422
581
  if (result.agentReport.wikiEntryQuality) {
423
582
  writeLine(stream, `wiki_entry_quality: ${result.agentReport.wikiEntryQuality}`);
424
583
  }
584
+ writeLine(stream, `grounding_evidence_available: ${result.agentReport.grounding.evidence_available ? "yes" : "no"}`);
585
+ writeLine(stream, `grounding_evidence_channel: ${result.agentReport.grounding.evidence_channel}`);
586
+ writeLine(stream, `grounding_needs_review: ${result.agentReport.grounding.needs_review ? "yes" : "no"}`);
587
+ writeLine(stream, `grounding_markers: ${result.agentReport.grounding.suspicion_markers.length ? result.agentReport.grounding.suspicion_markers.join(",") : "none"}`);
588
+ writeLine(stream, `grounding_claims_with_quotes: ${result.agentReport.grounding.claim_quote_count}/${result.agentReport.grounding.claim_count}`);
425
589
  if (result.agentReport.keyFiles.sourceCard) {
426
590
  writeLine(stream, `source_card: ${result.agentReport.keyFiles.sourceCard}`);
427
591
  }
@@ -1,6 +1,6 @@
1
1
  import { promises as fs } from "node:fs";
2
2
  import path from "node:path";
3
- import { frontmatterArray, frontmatterString, parseMarkdown } from "./frontmatter.js";
3
+ import { frontmatterArray, frontmatterBoolean, frontmatterString, parseMarkdown } from "./frontmatter.js";
4
4
  import { relativePath } from "./paths.js";
5
5
  import { exists } from "./workspace.js";
6
6
  const GROUPS = [
@@ -75,9 +75,14 @@ async function searchDir(root, dir, tokens, weight) {
75
75
  }
76
76
  const generationMode = frontmatterString(parsed.frontmatter, "generation_mode");
77
77
  const quality = frontmatterString(parsed.frontmatter, "quality");
78
+ const groundingMarkers = frontmatterArray(parsed.frontmatter, "grounding_markers");
79
+ const groundingNeedsReview = frontmatterBoolean(parsed.frontmatter, "grounding_needs_review");
78
80
  const warnings = generationMode === "deterministic_fallback"
79
81
  ? ["该 Wiki Entry 是 deterministic fallback,仅包含来源、正文预览和待补全区。"]
80
82
  : [];
83
+ if (groundingNeedsReview) {
84
+ warnings.push(`Grounding needs review${groundingMarkers.length ? `: ${groundingMarkers.join(", ")}` : ""}.`);
85
+ }
81
86
  matches.push({
82
87
  title,
83
88
  path: rel,
@@ -87,6 +92,9 @@ async function searchDir(root, dir, tokens, weight) {
87
92
  source_url: frontmatterString(parsed.frontmatter, "source_url") ?? "",
88
93
  generation_mode: generationMode,
89
94
  quality,
95
+ grounding_evidence_available: frontmatterBoolean(parsed.frontmatter, "grounding_evidence_available"),
96
+ grounding_needs_review: groundingNeedsReview,
97
+ grounding_markers: groundingMarkers,
90
98
  warnings
91
99
  });
92
100
  }
@@ -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
+ }
@@ -2,6 +2,7 @@ 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";
7
8
  import { renderWikiEntry } from "./wiki-entry.js";
@@ -17,10 +18,11 @@ export async function ingestPayload(rootPath, rawPayload) {
17
18
  const generatedFiles = [];
18
19
  await writeFile(path.join(runDir, "payload.json"), `${JSON.stringify(payload, null, 2)}\n`, generatedFiles);
19
20
  if (payload.source.fetch_status === "failed") {
21
+ const grounding = buildGroundingReport(payload);
20
22
  await writeSummary(root, runDir, payload, generatedFiles, [
21
23
  ...payload.warnings,
22
24
  "宿主 Agent 未能提供正文,AIWiki CLI 没有自行抓取网页。"
23
- ]);
25
+ ], undefined, grounding);
24
26
  return { runId: runDirName, runDir, generatedFiles, warnings: payload.warnings, agentReport: buildAgentReport(root, runDir, payload, generatedFiles) };
25
27
  }
26
28
  const slug = slugify(payload.source.title ?? payload.source.url);
@@ -28,22 +30,23 @@ export async function ingestPayload(rootPath, rawPayload) {
28
30
  const collisionWarnings = [];
29
31
  const longTermTargets = await chooseLongTermTargets(root, slug, runId, collisionWarnings);
30
32
  const links = buildArtifactLinks(root, slug, runDirName, runStartedAt, longTermTargets);
33
+ const grounding = buildGroundingReport(payload);
31
34
  await writeFile(path.join(runDir, "raw.md"), contentFile(payload, content, links), generatedFiles);
32
- 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);
33
36
  const wikiEntryResult = renderWikiEntry(payload, links);
34
37
  await writeFile(path.join(runDir, "wiki-entry.md"), wikiEntryResult.markdown, generatedFiles);
35
38
  await writeFile(path.join(runDir, "creative-assets.md"), creativeAssets(payload, links), generatedFiles);
36
39
  await writeFile(path.join(runDir, "topics.md"), topics(payload, links), generatedFiles);
37
40
  await writeFile(path.join(runDir, "draft-outline.md"), outline(payload, links), generatedFiles);
38
41
  await writeFile(longTermTargets.raw, contentFile(payload, content, links), generatedFiles);
39
- await writeFile(longTermTargets.sourceCard, sourceCard(payload, runDirName, links), generatedFiles);
42
+ await writeFile(longTermTargets.sourceCard, sourceCard(payload, runDirName, links, grounding), generatedFiles);
40
43
  await writeFile(longTermTargets.wikiEntry, wikiEntryResult.markdown, generatedFiles);
41
- await writeFile(longTermTargets.claims, claims(payload, links), generatedFiles);
44
+ await writeFile(longTermTargets.claims, claims(payload, links, grounding), generatedFiles);
42
45
  await writeFile(longTermTargets.assets, creativeAssets(payload, links), generatedFiles);
43
46
  await writeFile(longTermTargets.topics, topics(payload, links), generatedFiles);
44
47
  await writeFile(longTermTargets.outline, outline(payload, links), generatedFiles);
45
- const warnings = [...payload.warnings, ...collisionWarnings];
46
- 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);
47
50
  return { runId, runDir, generatedFiles, warnings, agentReport: buildAgentReport(root, runDir, payload, generatedFiles) };
48
51
  }
49
52
  export async function ingestFile(rootPath, filePath) {
@@ -111,7 +114,7 @@ async function writeFile(target, content, generatedFiles) {
111
114
  }
112
115
  generatedFiles.push(target);
113
116
  }
114
- async function writeSummary(root, runDir, payload, generatedFiles, warnings, links) {
117
+ async function writeSummary(root, runDir, payload, generatedFiles, warnings, links, grounding = buildGroundingReport(payload)) {
115
118
  const summaryPath = path.join(runDir, "processing-summary.md");
116
119
  const files = [...generatedFiles, summaryPath];
117
120
  const runId = path.basename(runDir);
@@ -130,6 +133,7 @@ async function writeSummary(root, runDir, payload, generatedFiles, warnings, lin
130
133
  `captured_at: "${escapeYaml(payload.source.captured_at)}"`,
131
134
  `run_id: "${escapeYaml(runId)}"`,
132
135
  ...(links ? relationshipFrontmatter(links) : []),
136
+ ...groundingFrontmatterLines(grounding),
133
137
  `tags: ["aiwiki/run"]`,
134
138
  "---",
135
139
  "",
@@ -145,6 +149,13 @@ async function writeSummary(root, runDir, payload, generatedFiles, warnings, lin
145
149
  "告警:",
146
150
  ...(warnings.length ? warnings.map((warning) => `- ${warning}`) : ["- none"]),
147
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
+ "",
148
159
  "下一步审阅:",
149
160
  "- 请在 Obsidian 中人工审阅资料卡、Claim 建议、素材建议、选题和大纲。",
150
161
  "- AIWiki CLI 不负责网页抓取稳定性。"
@@ -181,7 +192,7 @@ function contentFile(payload, content, links) {
181
192
  ""
182
193
  ].join("\n");
183
194
  }
184
- function sourceCard(payload, runId, links) {
195
+ function sourceCard(payload, runId, links, grounding) {
185
196
  return [
186
197
  "---",
187
198
  `aiwiki_id: "${escapeYaml(`${links.slug}:source-card`)}"`,
@@ -197,6 +208,7 @@ function sourceCard(payload, runId, links) {
197
208
  `captured_at: "${escapeYaml(payload.source.captured_at)}"`,
198
209
  `run_id: "${escapeYaml(runId)}"`,
199
210
  ...relationshipFrontmatter(links),
211
+ ...groundingFrontmatterLines(grounding),
200
212
  `aliases: ["${escapeYaml(payload.source.title ?? "Untitled")}"]`,
201
213
  `tags: ["aiwiki/source-card"]`,
202
214
  "---",
@@ -216,10 +228,19 @@ function sourceCard(payload, runId, links) {
216
228
  "## 摘要",
217
229
  "",
218
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"}`,
219
237
  ""
220
238
  ].join("\n");
221
239
  }
222
- 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。"];
223
244
  return [
224
245
  "---",
225
246
  `aiwiki_id: "${escapeYaml(`${links.slug}:claims`)}"`,
@@ -233,6 +254,7 @@ function claims(payload, links) {
233
254
  `captured_at: "${escapeYaml(payload.source.captured_at)}"`,
234
255
  `run_id: "${escapeYaml(links.runId)}"`,
235
256
  ...relationshipFrontmatter(links),
257
+ ...groundingFrontmatterLines(grounding),
236
258
  `tags: ["aiwiki/claims"]`,
237
259
  "---",
238
260
  "",
@@ -242,6 +264,16 @@ function claims(payload, links) {
242
264
  `- 资料卡:${obsidianLink(links.sourceCard, "资料卡")}`,
243
265
  `- 原文:${obsidianLink(links.raw, "原文")}`,
244
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,
245
277
  ""
246
278
  ].join("\n");
247
279
  }
@@ -327,6 +359,19 @@ function outline(payload, links) {
327
359
  ""
328
360
  ].join("\n");
329
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
+ }
330
375
  function trimPreview(value) {
331
376
  return value.trim().slice(0, 500) || "待人工补充。";
332
377
  }
@@ -352,7 +397,8 @@ function buildAgentReport(root, runDir, payload, generatedFiles) {
352
397
  reviewQueue: "dashboards/Review Queue.md"
353
398
  },
354
399
  wikiEntryGenerationMode: fetchFailed ? undefined : (payload.wiki_entry || payload.analysis ? "agent_enriched" : "deterministic_fallback"),
355
- wikiEntryQuality: fetchFailed ? undefined : (payload.wiki_entry || payload.analysis ? "enriched" : "scaffold")
400
+ wikiEntryQuality: fetchFailed ? undefined : (payload.wiki_entry || payload.analysis ? "enriched" : "scaffold"),
401
+ grounding: buildGroundingReport(payload)
356
402
  };
357
403
  }
358
404
  function estimateFitScore(payload, content) {
package/dist/src/lint.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { promises as fs } from "node:fs";
2
2
  import path from "node:path";
3
- import { frontmatterBoolean, frontmatterString, parseMarkdown } from "./frontmatter.js";
3
+ import { frontmatterArray, frontmatterBoolean, frontmatterString, parseMarkdown } from "./frontmatter.js";
4
4
  import { relativePath, safeJoin } from "./paths.js";
5
5
  import { exists } from "./workspace.js";
6
6
  export async function lintWorkspace(rootPath, now = new Date().toISOString()) {
@@ -57,8 +57,17 @@ export async function lintWorkspace(rootPath, now = new Date().toISOString()) {
57
57
  issues.push({ severity: "warning", path: entry.path, message: "agent_enriched Wiki Entry 缺少 key_points。", suggestion: "让宿主 Agent 提供 analysis.key_points。" });
58
58
  }
59
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。" });
60
+ if (frontmatterBoolean(entry.frontmatter, "grounding_needs_review") === true) {
61
+ const markers = frontmatterArray(entry.frontmatter, "grounding_markers");
62
+ issues.push({
63
+ severity: "warning",
64
+ path: entry.path,
65
+ message: `Wiki Entry 需要 grounding 复核${markers.length ? `: ${markers.join(", ")}` : "。"}`,
66
+ suggestion: "检查 source_quote 是否能在 Raw 中找到;coverage_suspected_incomplete 仅代表启发式疑似风险。"
67
+ });
68
+ }
69
+ if (frontmatterBoolean(entry.frontmatter, "represents_user_view") === true && frontmatterString(entry.frontmatter, "source_role") !== "output") {
70
+ issues.push({ severity: "warning", path: entry.path, message: "只有 output 角色应标记为代表用户观点。", suggestion: "将 represents_user_view 改为 false,或将 source_role 改为 output。" });
62
71
  }
63
72
  }
64
73
  issues.push(...duplicateIssues(sourceCards, "source_url", "重复 URL"));
@@ -35,6 +35,8 @@ export function normalizePayload(raw, runStartedAt) {
35
35
  if (!kind) {
36
36
  throw new Error("source.kind is required");
37
37
  }
38
+ const sourceRole = normalizeSourceRole(stringValue(sourceRaw.source_role) ?? stringValue(sourceRaw.role), warnings);
39
+ const representsUserView = booleanValue(sourceRaw.represents_user_view) ?? sourceRole === "output";
38
40
  const requestRaw = isRecord(raw.request) ? raw.request : {};
39
41
  const requestedOutputs = Array.isArray(requestRaw.outputs)
40
42
  ? requestRaw.outputs.filter((item) => typeof item === "string")
@@ -65,6 +67,8 @@ export function normalizePayload(raw, runStartedAt) {
65
67
  target_kb: stringValue(raw.target_kb),
66
68
  source: {
67
69
  kind,
70
+ source_role: sourceRole,
71
+ represents_user_view: representsUserView,
68
72
  url: stringValue(sourceRaw.url),
69
73
  title: titleRepair.value,
70
74
  author: stringValue(sourceRaw.author),
@@ -87,6 +91,16 @@ export function normalizePayload(raw, runStartedAt) {
87
91
  warnings
88
92
  };
89
93
  }
94
+ function normalizeSourceRole(value, warnings) {
95
+ if (!value) {
96
+ return "input";
97
+ }
98
+ if (value === "input" || value === "processing" || value === "output") {
99
+ return value;
100
+ }
101
+ warnings.push(`source_role 已忽略:${value} 不是 input、processing 或 output。`);
102
+ return "input";
103
+ }
90
104
  function normalizeFetchStatus(value, content) {
91
105
  if (value === "failed") {
92
106
  return "failed";
@@ -110,6 +124,9 @@ function rejectWriteControlFields(raw) {
110
124
  function stringValue(value) {
111
125
  return typeof value === "string" ? value : undefined;
112
126
  }
127
+ function booleanValue(value) {
128
+ return typeof value === "boolean" ? value : undefined;
129
+ }
113
130
  function isRecord(value) {
114
131
  return typeof value === "object" && value !== null && !Array.isArray(value);
115
132
  }
@@ -172,7 +189,7 @@ function reusableKnowledgeArray(value, warnings) {
172
189
  return [{ content: item.trim() }];
173
190
  }
174
191
  if (isRecord(item) && typeof item.content === "string" && item.content.trim()) {
175
- return [{ title: stringValue(item.title), content: item.content.trim() }];
192
+ return [{ title: stringValue(item.title), content: item.content.trim(), source_quote: stringValue(item.source_quote) }];
176
193
  }
177
194
  return [];
178
195
  });
@@ -1,24 +1,26 @@
1
+ import { buildGroundingReport, groundingFrontmatterLines } from "./grounding.js";
1
2
  export function renderWikiEntry(payload, links) {
2
3
  const enriched = Boolean(payload.wiki_entry || payload.analysis);
3
4
  const mode = enriched ? "agent_enriched" : "deterministic_fallback";
4
5
  const quality = enriched ? "enriched" : "scaffold";
5
6
  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);
7
+ const grounding = buildGroundingReport(payload);
8
+ const frontmatter = wikiFrontmatter(payload, links, title, mode, quality, grounding);
9
+ const body = enriched ? enrichedBody(payload, links, title, grounding) : fallbackBody(payload, links, title, grounding);
8
10
  return {
9
11
  mode,
10
12
  quality,
11
13
  markdown: `${frontmatter}\n${body}`
12
14
  };
13
15
  }
14
- function wikiFrontmatter(payload, links, title, mode, quality) {
16
+ function wikiFrontmatter(payload, links, title, mode, quality, grounding) {
15
17
  return [
16
18
  "---",
17
19
  `aiwiki_id: "${escapeYaml(`${links.slug}:wiki-entry`)}"`,
18
20
  `type: "wiki_entry"`,
19
- `wiki_type: "source_knowledge"`,
20
- `source_role: "input"`,
21
- `represents_user_view: false`,
21
+ `wiki_type: "${wikiType(payload)}"`,
22
+ `source_role: "${payload.source.source_role}"`,
23
+ `represents_user_view: ${payload.source.represents_user_view ? "true" : "false"}`,
22
24
  `status: "active"`,
23
25
  `generation_mode: "${mode}"`,
24
26
  `quality: "${quality}"`,
@@ -40,12 +42,23 @@ function wikiFrontmatter(payload, links, title, mode, quality) {
40
42
  ...(mode === "agent_enriched" ? [`summary: "${escapeYaml(payload.wiki_entry?.summary ?? payload.analysis?.summary ?? "")}"`] : []),
41
43
  `topics: ${yamlStringArray(payload.analysis?.related_concepts ?? [])}`,
42
44
  `claims: ${yamlStringArray(payload.analysis?.claims.map((claim) => claim.claim) ?? [])}`,
45
+ ...groundingFrontmatterLines(grounding),
43
46
  `tags: ["aiwiki/wiki-entry"]`,
44
47
  "---"
45
48
  ].join("\n");
46
49
  }
47
- function enrichedBody(payload, links, title) {
50
+ function wikiType(payload) {
51
+ if (payload.source.source_role === "output") {
52
+ return "personal_knowledge";
53
+ }
54
+ if (payload.source.source_role === "processing") {
55
+ return "thought_note";
56
+ }
57
+ return "source_knowledge";
58
+ }
59
+ function enrichedBody(payload, links, title, grounding) {
48
60
  const sections = [`# ${title}`, ""];
61
+ appendGroundingReview(sections, grounding);
49
62
  if (payload.wiki_entry?.summary || payload.analysis?.summary) {
50
63
  sections.push("## 一句话总结", "", payload.wiki_entry?.summary ?? payload.analysis?.summary ?? "", "");
51
64
  }
@@ -81,7 +94,7 @@ function bodyHasHeading(markdown, heading) {
81
94
  const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
82
95
  return new RegExp(`^#{1,6}\\s+${escaped}\\s*$`, "m").test(markdown);
83
96
  }
84
- function fallbackBody(payload, links, title) {
97
+ function fallbackBody(payload, links, title, grounding) {
85
98
  const preview = trimPreview(payload.source.content ?? payload.source.fetch_notes ?? "", 1000);
86
99
  return [
87
100
  `# ${title}`,
@@ -112,6 +125,8 @@ function fallbackBody(payload, links, title) {
112
125
  "- 适用场景",
113
126
  "- 可转化选题",
114
127
  "",
128
+ ...groundingReviewLines(grounding),
129
+ "",
115
130
  sourceSection(links)
116
131
  ].join("\n");
117
132
  }
@@ -134,6 +149,20 @@ function sourceSection(links) {
134
149
  `- Run: ${obsidianLink(links.runSummary, "处理记录")}`
135
150
  ].join("\n");
136
151
  }
152
+ function appendGroundingReview(sections, grounding) {
153
+ if (!grounding.needs_review) {
154
+ return;
155
+ }
156
+ sections.push("## Grounding 复核", "", ...groundingReviewLines(grounding), "");
157
+ }
158
+ function groundingReviewLines(grounding) {
159
+ return [
160
+ `- 证据通道:${grounding.evidence_channel}`,
161
+ `- 需要复核:${grounding.needs_review ? "yes" : "no"}`,
162
+ `- 疑似标记:${grounding.suspicion_markers.length ? grounding.suspicion_markers.join(", ") : "none"}`,
163
+ `- Claim 引用覆盖:${grounding.claim_quote_count}/${grounding.claim_count}`
164
+ ];
165
+ }
137
166
  function obsidianLink(vaultPath, label) {
138
167
  return `[[${vaultPath.replace(/\\/g, "/").replace(/\.md$/i, "")}|${label}]]`;
139
168
  }
@@ -29,9 +29,12 @@ AIWiki 的 Obsidian 入口。Dataview 是可选增强;未安装时仍可使用
29
29
 
30
30
  ## 原生链接入口
31
31
 
32
+ - [[dashboards/Wiki Entries|Wiki 条目]]
33
+ - [[dashboards/Source Cards|资料卡]]
32
34
  - [[dashboards/Review Queue|待审队列]]
33
35
  - [[dashboards/Recent Runs|最近处理]]
34
36
  - [[dashboards/Topic Pipeline|选题管线]]
37
+ - [[dashboards/Lint Report|结构检查]]
35
38
  - [[_system/schemas/aiwiki-frontmatter|字段说明]]
36
39
 
37
40
  ## 最近收录
@@ -51,6 +54,34 @@ FROM "03-sources/article-cards" or "04-claims/_suggestions" or "06-assets/_sugge
51
54
  WHERE status = "to-review"
52
55
  SORT created_at DESC
53
56
  \`\`\`
57
+ `
58
+ },
59
+ {
60
+ path: "dashboards/Wiki Entries.md",
61
+ content: `# Wiki 条目
62
+
63
+ AIWiki 每次成功入库都会生成 Wiki Entry。这里是知识层入口,不要求先经过 Review Queue 才能查询。
64
+
65
+ \`\`\`dataview
66
+ TABLE wiki_type, source_role, represents_user_view, quality, source_card, raw_file, updated_at
67
+ FROM "05-wiki"
68
+ WHERE type = "wiki_entry"
69
+ SORT updated_at DESC
70
+ \`\`\`
71
+ `
72
+ },
73
+ {
74
+ path: "dashboards/Source Cards.md",
75
+ content: `# 资料卡
76
+
77
+ 资料卡用于追踪来源、原文、Claim 建议、素材建议、选题和大纲。
78
+
79
+ \`\`\`dataview
80
+ TABLE status, source_url, wiki_entry, raw_note, captured_at
81
+ FROM "03-sources/article-cards"
82
+ WHERE type = "source_card"
83
+ SORT captured_at DESC
84
+ \`\`\`
54
85
  `
55
86
  },
56
87
  {
@@ -22,6 +22,8 @@ AIWiki CLI 负责:校验 payload、写本地文件、生成 Wiki Entry、输
22
22
 
23
23
  AIWiki CLI 不做通用网页抓取,也不调用 LLM。网页读取失败时,Agent 仍然要调用 CLI 记录失败原因;高质量 Wiki 内容由宿主 Agent 提供。
24
24
 
25
+ 证据和疑似风险要分开:`source_quote` 等能回到原文的字段是宿主提供的 evidence/provenance;`coverage_suspected_incomplete`、`unsupported_claims`、`needs_review` 是 AIWiki 的复核提示,不代表已经证明遗漏或错误。
26
+
25
27
  ## 标准流程
26
28
 
27
29
  1. 读取用户给的 URL、正文、附件或消息。
@@ -63,7 +65,9 @@ AIWiki 会修复常见 UTF-8 mojibake,但这只是兜底;宿主 Agent 仍应
63
65
  "content": "这里是宿主 Agent 读取到的正文内容。",
64
66
  "fetcher": "host-agent",
65
67
  "fetch_status": "ok",
66
- "captured_at": "2026-05-07T10:00:00+08:00"
68
+ "captured_at": "2026-05-07T10:00:00+08:00",
69
+ "source_role": "input",
70
+ "represents_user_view": false
67
71
  },
68
72
  "analysis": {
69
73
  "summary": "一句话总结。",
@@ -73,6 +77,13 @@ AIWiki 会修复常见 UTF-8 mojibake,但这只是兜底;宿主 Agent 仍应
73
77
  ],
74
78
  "related_concepts": [
75
79
  "概念 A"
80
+ ],
81
+ "claims": [
82
+ {
83
+ "claim": "强主张应绑定原文证据",
84
+ "confidence": "high",
85
+ "source_quote": "这里是宿主 Agent 读取到的正文内容。"
86
+ }
76
87
  ]
77
88
  },
78
89
  "request": {
@@ -141,6 +152,7 @@ AIWiki 已完成入库,并生成 Wiki 条目。
141
152
  摘要:<summary>
142
153
  Wiki 条目:<wiki_entry>
143
154
  质量模式:<wiki_entry_quality> / <wiki_entry_generation_mode>
155
+ Grounding:<grounding_evidence_channel> / review=<grounding_needs_review> / markers=<grounding_markers>
144
156
  资料卡:<source_card>
145
157
  处理记录:<processing_summary>
146
158
  ```
@@ -163,6 +175,12 @@ Obsidian 入口:<dashboard>
163
175
  aiwiki context "<主题>"
164
176
  ```
165
177
 
178
+ `context` 是给宿主 Agent 使用的 JSON。如果用户是在终端里直接想看结果,可以运行:
179
+
180
+ ```bash
181
+ aiwiki query "<主题>"
182
+ ```
183
+
166
184
  当用户说“整理 / 检查知识库”时,调用:
167
185
 
168
186
  ```bash
@@ -170,3 +188,23 @@ aiwiki lint
170
188
  ```
171
189
 
172
190
  `context` 返回 JSON,注意其中的 `generation_mode`、`quality` 和 `warnings`。如果结果是 `deterministic_fallback` / `scaffold`,回复时要说明它只是可追溯脚手架,不是高质量知识提炼。
191
+
192
+ `context` 也可能返回 grounding 字段。回复用户时可以把 `grounding_needs_review: true` 解释为“这条资料需要复核证据或覆盖度”,不要说成“AIWiki 已确认漏掉重点”。
193
+
194
+ ## Source Role
195
+
196
+ 默认外部资料使用:
197
+
198
+ ```json
199
+ "source_role": "input",
200
+ "represents_user_view": false
201
+ ```
202
+
203
+ 只有当用户明确导入自己的已发布文章、演讲稿、公众号文章等个人输出时,才使用:
204
+
205
+ ```json
206
+ "source_role": "output",
207
+ "represents_user_view": true
208
+ ```
209
+
210
+ 不要把外部资料标成代表用户观点。
package/docs/USAGE.md CHANGED
@@ -10,6 +10,8 @@ AIWiki CLI 不负责网页抓取。Qclaw、Codex、Claude Code、Cursor、Gemini
10
10
 
11
11
  AIWiki CLI 也不调用 LLM。高质量 Wiki Entry 来自宿主 Agent 提供的 `analysis` 或 `wiki_entry`;如果没有这些字段,AIWiki 会生成可追溯的 deterministic fallback 条目,只包含来源、反链、正文预览和待补全区。
12
12
 
13
+ AIWiki 会把证据通道和疑似风险分开记录。`source_quote` 等宿主 Agent 提供的原文引用属于证据通道;`coverage_suspected_incomplete`、`unsupported_claims`、`needs_review` 等属于 AIWiki 生成的启发式复核信号,不等于已经证明内容遗漏。
14
+
13
15
  ## 1. 一次性设置
14
16
 
15
17
  发布后直接运行交互式 setup:
@@ -56,6 +58,7 @@ aiwiki status
56
58
 
57
59
  ```bash
58
60
  aiwiki agent list
61
+ aiwiki agent check
59
62
  ```
60
63
 
61
64
  再启动安装向导:
@@ -88,6 +91,8 @@ aiwiki prompt agent
88
91
 
89
92
  把输出内容安装成宿主 Agent 的 skill,或粘贴到宿主 Agent 的项目/会话说明里。不同 Agent 的安装入口不同,所以 AIWiki 提供自动安装向导和通用协议两条路径。
90
93
 
94
+ `aiwiki agent check` 用来确认本机检测到哪些宿主 Agent、哪些已经安装 AIWiki 对接文件、哪些还需要运行 `aiwiki agent install --agent <id> --yes`。
95
+
91
96
  ## 3. 日常使用
92
97
 
93
98
  宿主 Agent 已经加载 AIWiki 协议后,把下面的话发给它,并替换链接:
@@ -125,6 +130,11 @@ processing_summary: 09-runs/20260507-153012-abc123/processing-summary.md
125
130
  wiki_entry: 05-wiki/source-knowledge/article-slug.md
126
131
  wiki_entry_generation_mode: agent_enriched
127
132
  wiki_entry_quality: enriched
133
+ grounding_evidence_available: yes
134
+ grounding_evidence_channel: host_supplied
135
+ grounding_needs_review: no
136
+ grounding_markers: none
137
+ grounding_claims_with_quotes: 1/1
128
138
  source_card: 03-sources/article-cards/article-slug.md
129
139
  draft_outline: 09-runs/20260507-153012-abc123/draft-outline.md
130
140
  dashboard: dashboards/AIWiki Home.md
@@ -205,6 +215,26 @@ Wiki Entry 有两种质量模式:
205
215
  - `agent_enriched` / `enriched`:宿主 Agent 提供了 `analysis` 或 `wiki_entry`。
206
216
  - `deterministic_fallback` / `scaffold`:AIWiki 只生成来源、反链、正文预览和待补全区。
207
217
 
218
+ Artifact 角色保持固定:
219
+
220
+ - `03-sources/article-cards` 是 trace-first 的资料卡:保留来源、反链、原文预览和 grounding 状态,不承担完整知识正文。
221
+ - `05-wiki/source-knowledge` 是 enriched knowledge surface:宿主 Agent 的 `analysis` / `wiki_entry` 会进入这里。
222
+ - `02-raw/articles` 是原始证据层,后续摘要、Claim 和复核都应能回查这里。
223
+
224
+ Grounding 字段都是 additive,可被旧工具忽略:
225
+
226
+ - `grounding_evidence_available`:是否存在可回查的宿主证据。
227
+ - `grounding_evidence_channel`:`host_supplied` 或 `none`。
228
+ - `grounding_needs_review`:是否需要复核。
229
+ - `grounding_markers`:例如 `unsupported_claims`、`source_quote_not_found`、`coverage_suspected_incomplete`。
230
+ - `coverage_suspected_incomplete`:长文提取过少时的启发式疑似标记,不是确定遗漏结论。
231
+
232
+ Wiki Entry 还会记录来源角色:
233
+
234
+ - `source_role: input`:默认值,外部文章、网页、书籍、视频等资料;`represents_user_view: false`。
235
+ - `source_role: output`:用户已发布文章、演讲稿、公众号文章等个人输出;通常可配合 `represents_user_view: true`。
236
+ - `source_role: processing`:用户自己的草稿、笔记、思考过程;默认不直接代表最终观点。
237
+
208
238
  ### Obsidian 链接规则
209
239
 
210
240
  AIWiki 生成的 Markdown 按 Obsidian vault 内路径组织,文件正文会使用 wikilink:
@@ -229,6 +259,8 @@ AIWiki 生成的 Markdown 按 Obsidian vault 内路径组织,文件正文会
229
259
 
230
260
  ```text
231
261
  dashboards/AIWiki Home.md
262
+ dashboards/Wiki Entries.md
263
+ dashboards/Source Cards.md
232
264
  dashboards/Review Queue.md
233
265
  dashboards/Recent Runs.md
234
266
  dashboards/Topic Pipeline.md
@@ -271,15 +303,24 @@ docs/AGENT_HANDOFF.md
271
303
 
272
304
  ```bash
273
305
  aiwiki context "AI Agent"
306
+ aiwiki query "AI Agent"
274
307
  ```
275
308
 
309
+ `context` 返回 JSON 给宿主 Agent 使用;`query` 使用同一套检索结果,输出给人看的分组摘要。
310
+
276
311
  检查知识库结构:
277
312
 
278
313
  ```bash
279
314
  aiwiki lint
280
315
  ```
281
316
 
282
- `context` 返回 JSON 给宿主 Agent 使用。`lint` 输出报告并写入 `dashboards/Lint Report.md`。
317
+ 查看下一步建议:
318
+
319
+ ```bash
320
+ aiwiki next
321
+ ```
322
+
323
+ `lint` 输出报告并写入 `dashboards/Lint Report.md`。
283
324
 
284
325
  ## 8. 高级调试
285
326
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itradingai/aiwiki",
3
- "version": "0.2.11",
3
+ "version": "0.2.12",
4
4
  "type": "module",
5
5
  "description": "Agent-first AI knowledge base CLI for turning articles, links and notes into Obsidian-ready source cards, topics, outlines and reusable knowledge assets.",
6
6
  "license": "MIT",
package/skill/SKILL.md CHANGED
@@ -74,7 +74,9 @@ Because AIWiki CLI does not call an LLM, high-quality summary and knowledge extr
74
74
  "content": "Article body read by the host Agent.",
75
75
  "fetcher": "host-agent",
76
76
  "fetch_status": "ok",
77
- "captured_at": "2026-05-07T10:00:00+08:00"
77
+ "captured_at": "2026-05-07T10:00:00+08:00",
78
+ "source_role": "input",
79
+ "represents_user_view": false
78
80
  },
79
81
  "analysis": {
80
82
  "summary": "One-sentence summary.",
@@ -130,6 +132,14 @@ aiwiki context "<topic>"
130
132
 
131
133
  Use the returned JSON to answer. Prefer Wiki Entries first. Do not scan `02-raw` by default unless the Wiki result is insufficient, the user asks to verify the original text, or sources conflict.
132
134
 
135
+ For direct human terminal output, use:
136
+
137
+ ```bash
138
+ aiwiki query "<topic>"
139
+ ```
140
+
141
+ `query` renders the same retrieval basis as `context`; do not treat it as a separate search system.
142
+
133
143
  ## Lint Protocol
134
144
 
135
145
  When the user asks to整理 / 检查 / lint the knowledge base, call:
@@ -140,6 +150,17 @@ aiwiki lint
140
150
 
141
151
  Explain warnings and errors as structure-health feedback. Do not frame lint as a requirement that the user manually review every item.
142
152
 
153
+ ## Source Role
154
+
155
+ Use the default source role for external material:
156
+
157
+ ```json
158
+ "source_role": "input",
159
+ "represents_user_view": false
160
+ ```
161
+
162
+ Only use `source_role: "output"` with `represents_user_view: true` when the user is importing their own published writing, talk transcript, newsletter, or similar authored output. Do not mark external material as the user's view.
163
+
143
164
  ## Obsidian + Dataview Boundary
144
165
 
145
166
  - AIWiki can be used with plain Markdown.