@itradingai/aiwiki 0.2.18 → 0.2.20

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.
Files changed (67) hide show
  1. package/README.md +16 -12
  2. package/dist/src/app.js +9 -17
  3. package/dist/src/ingest.js +52 -27
  4. package/dist/src/lint.js +84 -1
  5. package/dist/src/payload.js +25 -10
  6. package/dist/src/wiki-entry.js +3 -3
  7. package/dist/src/workspace.js +18 -9
  8. package/docs/20260607-aiwiki-feature-pruning-plan.md +468 -0
  9. package/docs/20260607-aiwiki-long-term-operating-roadmap.md +409 -0
  10. package/docs/AGENT_HANDOFF.md +6 -7
  11. package/docs/FAQ.md +8 -2
  12. package/docs/README.md +8 -4
  13. package/docs/RELEASE.md +9 -14
  14. package/docs/ROADMAP.md +5 -0
  15. package/docs/SHOWCASE.md +19 -9
  16. package/docs/USAGE.md +23 -19
  17. package/docs/development-log.md +227 -0
  18. package/examples/demo-run/README.md +28 -0
  19. package/examples/demo-run/context-output.json +341 -0
  20. package/examples/demo-run/ingest-agent-output.txt +25 -0
  21. package/examples/demo-run/ingest-file-output.txt +23 -0
  22. package/examples/demo-run/input/agent-enriched-payload.json +88 -0
  23. package/examples/demo-run/input/local-article.md +5 -0
  24. package/examples/demo-run/lint-output.json +46 -0
  25. package/examples/demo-run/query-output.txt +54 -0
  26. package/examples/demo-run/setup-output.txt +9 -0
  27. package/examples/demo-run/status-output.txt +20 -0
  28. package/examples/obsidian-vault-sample/02-raw/articles/llm-wiki-notes.md +32 -0
  29. package/examples/obsidian-vault-sample/02-raw/articles/local-article.md +33 -0
  30. package/examples/obsidian-vault-sample/03-sources/article-cards/llm-wiki-notes.md +62 -0
  31. package/examples/obsidian-vault-sample/03-sources/article-cards/local-article.md +58 -0
  32. package/examples/obsidian-vault-sample/04-claims/_suggestions/llm-wiki-notes-claims.md +58 -0
  33. package/examples/obsidian-vault-sample/05-wiki/source-knowledge/llm-wiki-notes.md +103 -0
  34. package/examples/obsidian-vault-sample/05-wiki/source-knowledge/local-article.md +72 -0
  35. package/examples/obsidian-vault-sample/06-assets/_suggestions/llm-wiki-notes-assets.md +29 -0
  36. package/examples/obsidian-vault-sample/07-topics/ready/llm-wiki-notes-topics.md +29 -0
  37. package/examples/obsidian-vault-sample/08-outputs/outlines/llm-wiki-notes-outline.md +43 -0
  38. package/examples/obsidian-vault-sample/09-runs/20260608-160603905-085f05/payload.json +24 -0
  39. package/examples/obsidian-vault-sample/09-runs/20260608-160603905-085f05/processing-summary.md +56 -0
  40. package/examples/obsidian-vault-sample/09-runs/20260608-160603905-085f05/raw.md +33 -0
  41. package/examples/obsidian-vault-sample/09-runs/20260608-160603905-085f05/source-card.md +58 -0
  42. package/examples/obsidian-vault-sample/09-runs/20260608-160603905-085f05/wiki-entry.md +72 -0
  43. package/examples/obsidian-vault-sample/09-runs/20260608-160603980-89f570/creative-assets.md +29 -0
  44. package/examples/obsidian-vault-sample/09-runs/20260608-160603980-89f570/draft-outline.md +43 -0
  45. package/examples/obsidian-vault-sample/09-runs/20260608-160603980-89f570/payload.json +91 -0
  46. package/examples/obsidian-vault-sample/09-runs/20260608-160603980-89f570/processing-summary.md +67 -0
  47. package/examples/obsidian-vault-sample/09-runs/20260608-160603980-89f570/raw.md +32 -0
  48. package/examples/obsidian-vault-sample/09-runs/20260608-160603980-89f570/source-card.md +62 -0
  49. package/examples/obsidian-vault-sample/09-runs/20260608-160603980-89f570/topics.md +29 -0
  50. package/examples/obsidian-vault-sample/09-runs/20260608-160603980-89f570/wiki-entry.md +103 -0
  51. package/examples/obsidian-vault-sample/_system/index.md +37 -0
  52. package/examples/obsidian-vault-sample/_system/log.md +8 -0
  53. package/examples/obsidian-vault-sample/_system/purpose.md +31 -0
  54. package/examples/obsidian-vault-sample/_system/schemas/aiwiki-frontmatter.md +21 -0
  55. package/examples/obsidian-vault-sample/_system/templates/review-note.md +16 -0
  56. package/examples/obsidian-vault-sample/_system/templates/source-card.md +34 -0
  57. package/examples/obsidian-vault-sample/aiwiki.yaml +18 -0
  58. package/examples/obsidian-vault-sample/dashboards/AIWiki Home.md +31 -0
  59. package/examples/obsidian-vault-sample/dashboards/Recent Runs.md +10 -0
  60. package/examples/obsidian-vault-sample/dashboards/Review Queue.md +12 -0
  61. package/examples/obsidian-vault-sample/dashboards/Source Cards.md +10 -0
  62. package/examples/obsidian-vault-sample/dashboards/Topic Pipeline.md +10 -0
  63. package/examples/obsidian-vault-sample/dashboards/Wiki Entries.md +10 -0
  64. package/package.json +13 -2
  65. package/skill/LINT_PROTOCOL.md +6 -4
  66. package/skill/SKILL.md +12 -5
  67. package/docs/POSITIONING_CONTEXT_COMPILER_PLAN.md +0 -347
package/README.md CHANGED
@@ -23,7 +23,7 @@ AIWiki 是一个开源的 Agent-first 本地 LLM-wiki CLI。
23
23
  用户给 URL / 正文 / 文件
24
24
  -> 宿主 Agent 读取内容并尽量生成 analysis / wiki_entry
25
25
  -> aiwiki ingest-agent --stdin
26
- -> AIWiki 写入 Raw / Source Card / Wiki Entry / Claim / Topic / Outline / Run Log
26
+ -> AIWiki 写入 Raw / Source Card / Wiki Entry / Run Summary;有明确内容或请求时再写 Claim / Topic / Outline / Asset
27
27
  ```
28
28
 
29
29
  ### Query:从 Wiki 调度知识
@@ -43,6 +43,15 @@ aiwiki lint
43
43
 
44
44
  `lint` 会检查缺失链接、重复来源、fallback Wiki 条目、enriched 条目缺字段等问题,并写入 `dashboards/Lint Report.md`。
45
45
 
46
+ ## 示例
47
+
48
+ 仓库内置了一个由当前 CLI 重新生成的样例:
49
+
50
+ - `examples/demo-run/`:输入材料、执行命令和关键 CLI 输出。
51
+ - `examples/obsidian-vault-sample/`:可直接查看的样例知识库。
52
+
53
+ 样例展示了核心产物优先的约定:Raw、Source Card、Wiki Entry、Run Summary、Processing Summary 总是最先检查;Claim、Asset、Topic、Outline 只在 payload 有对应内容或明确请求时出现。
54
+
46
55
  ## 快速开始
47
56
 
48
57
  ### 第一步:安装 AIWiki CLI
@@ -55,7 +64,7 @@ aiwiki lint
55
64
  我的知识库路径:F:\knowledges
56
65
 
57
66
  请检查 Node.js >=20,执行 aiwiki setup --path "我的知识库路径" --yes,
58
- 然后运行 aiwiki agent list / aiwiki agent install 完成宿主 Agent 对接。
67
+ 然后运行 aiwiki agent sync --yes aiwiki agent check --json 完成宿主 Agent 对接。
59
68
  最后执行 aiwiki doctor 和 aiwiki status,告诉我实际执行了哪些命令和还差什么手动步骤。
60
69
  ```
61
70
 
@@ -63,16 +72,15 @@ aiwiki lint
63
72
 
64
73
  ```bash
65
74
  npx @itradingai/aiwiki@latest setup
66
- aiwiki agent list
67
- aiwiki agent install
75
+ aiwiki agent sync --yes
76
+ aiwiki agent check --json
68
77
  ```
69
78
 
70
79
  ### 第二步:接入宿主 Agent
71
80
 
72
81
  ```bash
73
- aiwiki agent list
74
- aiwiki agent install
75
- aiwiki agent check
82
+ aiwiki agent sync --yes
83
+ aiwiki agent check --json
76
84
  ```
77
85
 
78
86
  也可以直接输出通用协议:
@@ -118,15 +126,11 @@ aiwiki query "xxx"
118
126
  ```text
119
127
  02-raw/articles/
120
128
  03-sources/article-cards/
121
- 04-claims/_suggestions/
122
129
  05-wiki/source-knowledge/
123
- 06-assets/_suggestions/
124
- 07-topics/ready/
125
- 08-outputs/outlines/
126
130
  09-runs/<run-id>/
127
131
  ```
128
132
 
129
- 其中 `05-wiki/source-knowledge/<slug>.md` 是默认 Wiki Entry。
133
+ 其中 `02-raw/articles/`、`03-sources/article-cards/`、`05-wiki/source-knowledge/` 和 `09-runs/<run-id>/` 是核心产物。`04-claims/_suggestions/`、`06-assets/_suggestions/`、`07-topics/ready/`、`08-outputs/outlines/` 只在 payload 有对应内容或 `request.outputs` 明确请求时生成。
130
134
 
131
135
  ### Agent-Enriched Wiki Entry
132
136
 
package/dist/src/app.js CHANGED
@@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url";
6
6
  import { flagBool, flagString, parseArgs } from "./args.js";
7
7
  import { buildContext } from "./context.js";
8
8
  import { deriveFileTitle, ingestFile, ingestPayload } from "./ingest.js";
9
- import { filterLintReport, lintWorkspace, renderLintReport, renderLintSummary, writeLintReport } from "./lint.js";
9
+ import { attachAppliedSafeFixes, filterLintReport, lintWorkspace, removeEmptyOptionalDirs, renderLintReport, renderLintSummary, writeLintReport } from "./lint.js";
10
10
  import { CliError, writeLine } from "./output.js";
11
11
  import { confirmInit, directorySummary, doctor, exists, initWorkspace, promptForSetup, promptForInitPath, readConfig, resolveWorkspace, setDefaultWorkspace, statusSummary } from "./workspace.js";
12
12
  export async function runCli(argv, streams = { stdout: process.stdout, stderr: process.stderr }) {
@@ -44,8 +44,8 @@ export async function runCli(argv, streams = { stdout: process.stdout, stderr: p
44
44
  writeLine(streams.stdout, `默认知识库: ${defaultConfig.defaultPath}`);
45
45
  writeLine(streams.stdout, `用户配置: ${defaultConfig.configPath}`);
46
46
  writeLine(streams.stdout, "Obsidian 入口: dashboards/AIWiki Home.md");
47
- writeLine(streams.stdout, "下一步: 运行 `aiwiki agent install`,把 AIWiki 安装到宿主 Agent");
48
- writeLine(streams.stdout, "Agent 设置完成后: Agent 发送 `入库 <url>`");
47
+ writeLine(streams.stdout, "下一步: 运行 `aiwiki agent sync --yes`,同步 AIWiki 宿主 Agent 对接文件。");
48
+ writeLine(streams.stdout, "Agent 设置完成后: 让宿主 Agent 提供正文并调用 `aiwiki ingest-agent --stdin`,或运行 `aiwiki ingest-file --file <file>`。");
49
49
  return 0;
50
50
  }
51
51
  if (command === "agent" && subcommand === "install") {
@@ -184,7 +184,8 @@ export async function runCli(argv, streams = { stdout: process.stdout, stderr: p
184
184
  if (command === "lint") {
185
185
  const root = await resolveWorkspace(flagString(args, "path"));
186
186
  const severity = parseLintSeverity(flagString(args, "severity"));
187
- const report = filterLintReport(await lintWorkspace(root), severity);
187
+ const appliedSafeFixes = flagBool(args, "fix-empty-dirs") ? await removeEmptyOptionalDirs(root) : [];
188
+ const report = filterLintReport(attachAppliedSafeFixes(await lintWorkspace(root), appliedSafeFixes), severity);
188
189
  if (flagBool(args, "json")) {
189
190
  writeLine(streams.stdout, JSON.stringify(report, null, 2));
190
191
  return 0;
@@ -268,25 +269,16 @@ function printHelp(stream) {
268
269
  writeLine(stream, "用法:");
269
270
  writeLine(stream, " aiwiki setup");
270
271
  writeLine(stream, " aiwiki setup --path <path> --yes");
271
- writeLine(stream, " aiwiki agent list");
272
- writeLine(stream, " aiwiki agent install");
273
- writeLine(stream, " aiwiki agent install --agent codex --yes");
274
272
  writeLine(stream, " aiwiki agent sync --yes");
275
273
  writeLine(stream, " aiwiki agent check --json");
276
- writeLine(stream, " aiwiki prompt agent");
274
+ writeLine(stream, " aiwiki ingest-agent --stdin");
275
+ writeLine(stream, " aiwiki ingest-file --file <file>");
277
276
  writeLine(stream, " aiwiki doctor");
278
277
  writeLine(stream, " aiwiki status");
279
278
  writeLine(stream, " aiwiki context <query>");
280
279
  writeLine(stream, " aiwiki query <query>");
281
- writeLine(stream, " aiwiki next");
282
280
  writeLine(stream, " aiwiki lint");
283
- writeLine(stream, " aiwiki ingest-agent --stdin");
284
- writeLine(stream, " aiwiki ingest-file --file <file>");
285
- writeLine(stream, " aiwiki init --path <path> --yes --set-default");
286
- writeLine(stream, " aiwiki config show");
287
- writeLine(stream, " aiwiki ingest-agent --payload <file>");
288
- writeLine(stream, " aiwiki ingest-url <url> --content-file <file>");
289
- writeLine(stream, " aiwiki agent check");
281
+ writeLine(stream, " aiwiki lint --fix-empty-dirs --json");
290
282
  }
291
283
  function printAgentHelp(stream) {
292
284
  writeLine(stream, "AIWiki Agent commands");
@@ -633,7 +625,7 @@ function printAgentPrompt(stream) {
633
625
  writeLine(stream, "回复措辞:成功时说“AIWiki 已完成入库,并生成 Wiki 条目。” 如果 wiki_entry_quality=scaffold,说明该条目只是可追溯脚手架,仍需宿主 Agent 后续补全。Dataview 是可选增强,不要替用户安装插件或修改 .obsidian。");
634
626
  writeLine(stream, "");
635
627
  writeLine(stream, "查询:当用户要求从 AIWiki 里了解某个主题时,调用 `aiwiki context <主题>`。");
636
- writeLine(stream, "整理:当用户要求检查或整理知识库时,调用 `aiwiki lint`。");
628
+ writeLine(stream, "整理:当用户要求检查或整理知识库时,先调用 `aiwiki lint --json`;若只有 safe fix 且用户允许整理,再调用 `aiwiki lint --fix-empty-dirs --json`,随后重跑 `aiwiki lint --json`。");
637
629
  writeLine(stream, "");
638
630
  writeLine(stream, "禁止:让用户保存 payload;让用户每次输入 --path;声称 AIWiki CLI 负责网页抓取;声称 AIWiki CLI 会在没有 Agent 分析字段时自动高质量总结。");
639
631
  }
@@ -30,23 +30,38 @@ export async function ingestPayload(rootPath, rawPayload) {
30
30
  const contentFingerprint = createContentFingerprint(content);
31
31
  const collisionWarnings = [];
32
32
  await detectDuplicateContent(root, payload, contentFingerprint, collisionWarnings);
33
- const longTermTargets = await chooseLongTermTargets(root, slug, runId, collisionWarnings);
33
+ const optionalOutputs = optionalOutputPlan(payload);
34
+ const longTermTargets = await chooseLongTermTargets(root, slug, runId, collisionWarnings, optionalOutputs);
34
35
  const links = buildArtifactLinks(root, slug, runDirName, runStartedAt, contentFingerprint, longTermTargets);
35
36
  const grounding = buildGroundingReport(payload);
36
37
  await writeFile(path.join(runDir, "raw.md"), contentFile(payload, content, links), generatedFiles);
37
38
  await writeFile(path.join(runDir, "source-card.md"), sourceCard(payload, runDirName, links, grounding), generatedFiles);
38
39
  const wikiEntryResult = renderWikiEntry(payload, links);
39
40
  await writeFile(path.join(runDir, "wiki-entry.md"), wikiEntryResult.markdown, generatedFiles);
40
- await writeFile(path.join(runDir, "creative-assets.md"), creativeAssets(payload, links), generatedFiles);
41
- await writeFile(path.join(runDir, "topics.md"), topics(payload, links), generatedFiles);
42
- await writeFile(path.join(runDir, "draft-outline.md"), outline(payload, links), generatedFiles);
41
+ if (optionalOutputs.assets && links.assets) {
42
+ await writeFile(path.join(runDir, "creative-assets.md"), creativeAssets(payload, links), generatedFiles);
43
+ }
44
+ if (optionalOutputs.topics && links.topics) {
45
+ await writeFile(path.join(runDir, "topics.md"), topics(payload, links), generatedFiles);
46
+ }
47
+ if (optionalOutputs.outline && links.outline) {
48
+ await writeFile(path.join(runDir, "draft-outline.md"), outline(payload, links), generatedFiles);
49
+ }
43
50
  await writeFile(longTermTargets.raw, contentFile(payload, content, links), generatedFiles);
44
51
  await writeFile(longTermTargets.sourceCard, sourceCard(payload, runDirName, links, grounding), generatedFiles);
45
52
  await writeFile(longTermTargets.wikiEntry, wikiEntryResult.markdown, generatedFiles);
46
- await writeFile(longTermTargets.claims, claims(payload, links, grounding), generatedFiles);
47
- await writeFile(longTermTargets.assets, creativeAssets(payload, links), generatedFiles);
48
- await writeFile(longTermTargets.topics, topics(payload, links), generatedFiles);
49
- await writeFile(longTermTargets.outline, outline(payload, links), generatedFiles);
53
+ if (optionalOutputs.claims && longTermTargets.claims) {
54
+ await writeFile(longTermTargets.claims, claims(payload, links, grounding), generatedFiles);
55
+ }
56
+ if (optionalOutputs.assets && longTermTargets.assets) {
57
+ await writeFile(longTermTargets.assets, creativeAssets(payload, links), generatedFiles);
58
+ }
59
+ if (optionalOutputs.topics && longTermTargets.topics) {
60
+ await writeFile(longTermTargets.topics, topics(payload, links), generatedFiles);
61
+ }
62
+ if (optionalOutputs.outline && longTermTargets.outline) {
63
+ await writeFile(longTermTargets.outline, outline(payload, links), generatedFiles);
64
+ }
50
65
  const warnings = [...payload.warnings, ...groundingWarnings(grounding), ...collisionWarnings];
51
66
  await writeSummary(root, runDir, payload, generatedFiles, warnings, links, grounding);
52
67
  return { runId, runDir, generatedFiles, warnings, agentReport: buildAgentReport(root, runDir, payload, generatedFiles) };
@@ -69,7 +84,7 @@ export async function ingestFile(rootPath, filePath) {
69
84
  },
70
85
  request: {
71
86
  mode: "ingest",
72
- outputs: ["source_card", "creative_assets", "topics", "draft_outline", "processing_summary"],
87
+ outputs: ["source_card", "wiki_entry", "processing_summary"],
73
88
  language: "zh-CN"
74
89
  }
75
90
  });
@@ -77,19 +92,29 @@ export async function ingestFile(rootPath, filePath) {
77
92
  export function deriveFileTitle(filePath) {
78
93
  return path.basename(filePath, path.extname(filePath));
79
94
  }
80
- async function chooseLongTermTargets(root, slug, runId, warnings) {
95
+ async function chooseLongTermTargets(root, slug, runId, warnings, plan) {
81
96
  return {
82
97
  raw: await chooseLongTermTarget(root, "02-raw/articles", `${slug}.md`, runId, warnings),
83
98
  sourceCard: await chooseLongTermTarget(root, "03-sources/article-cards", `${slug}.md`, runId, warnings),
84
99
  wikiEntry: await chooseLongTermTarget(root, "05-wiki/source-knowledge", `${slug}.md`, runId, warnings),
85
- claims: await chooseLongTermTarget(root, "04-claims/_suggestions", `${slug}-claims.md`, runId, warnings),
86
- assets: await chooseLongTermTarget(root, "06-assets/_suggestions", `${slug}-assets.md`, runId, warnings),
87
- topics: await chooseLongTermTarget(root, "07-topics/ready", `${slug}-topics.md`, runId, warnings),
88
- outline: await chooseLongTermTarget(root, "08-outputs/outlines", `${slug}-outline.md`, runId, warnings)
100
+ claims: plan.claims ? await chooseLongTermTarget(root, "04-claims/_suggestions", `${slug}-claims.md`, runId, warnings) : undefined,
101
+ assets: plan.assets ? await chooseLongTermTarget(root, "06-assets/_suggestions", `${slug}-assets.md`, runId, warnings) : undefined,
102
+ topics: plan.topics ? await chooseLongTermTarget(root, "07-topics/ready", `${slug}-topics.md`, runId, warnings) : undefined,
103
+ outline: plan.outline ? await chooseLongTermTarget(root, "08-outputs/outlines", `${slug}-outline.md`, runId, warnings) : undefined
104
+ };
105
+ }
106
+ function optionalOutputPlan(payload) {
107
+ const outputs = new Set(payload.request.outputs);
108
+ return {
109
+ claims: outputs.has("claims") || outputs.has("claim_suggestions") || Boolean(payload.analysis?.claims.length),
110
+ assets: outputs.has("creative_assets"),
111
+ topics: outputs.has("topics") || Boolean(payload.analysis?.topic_candidates.length),
112
+ outline: outputs.has("draft_outline") || Boolean(payload.analysis?.outline || payload.analysis?.suggested_links.length || payload.analysis?.reusable_judgments.length)
89
113
  };
90
114
  }
91
115
  async function chooseLongTermTarget(root, dir, fileName, runId, warnings) {
92
116
  const target = safeJoin(root, dir, fileName);
117
+ await fs.mkdir(path.dirname(target), { recursive: true });
93
118
  try {
94
119
  await fs.access(target);
95
120
  }
@@ -260,10 +285,10 @@ function sourceCard(payload, runId, links, grounding) {
260
285
  "",
261
286
  `- Wiki 条目:${obsidianLink(links.wikiEntry, "Wiki 条目")}`,
262
287
  `- 原文:${obsidianLink(links.raw, "原文")}`,
263
- `- Claim 建议:${obsidianLink(links.claims, "Claim 建议")}`,
264
- `- 素材建议:${obsidianLink(links.assets, "素材建议")}`,
265
- `- 选题:${obsidianLink(links.topics, "选题")}`,
266
- `- 大纲:${obsidianLink(links.outline, "大纲")}`,
288
+ ...(links.claims ? [`- Claim 建议:${obsidianLink(links.claims, "Claim 建议")}`] : []),
289
+ ...(links.assets ? [`- 素材建议:${obsidianLink(links.assets, "素材建议")}`] : []),
290
+ ...(links.topics ? [`- 选题:${obsidianLink(links.topics, "选题")}`] : []),
291
+ ...(links.outline ? [`- 大纲:${obsidianLink(links.outline, "大纲")}`] : []),
267
292
  `- 处理记录:${obsidianLink(links.runSummary, "处理记录")}`,
268
293
  "",
269
294
  "## 摘要",
@@ -380,7 +405,7 @@ function topics(payload, links) {
380
405
  "",
381
406
  `- Wiki 条目:${obsidianLink(links.wikiEntry, "Wiki 条目")}`,
382
407
  `- 资料卡:${obsidianLink(links.sourceCard, "资料卡")}`,
383
- `- 大纲:${obsidianLink(links.outline, "大纲")}`,
408
+ ...(links.outline ? [`- 大纲:${obsidianLink(links.outline, "大纲")}`] : []),
384
409
  `- ${payload.source.title ?? "Untitled"}`,
385
410
  ""
386
411
  ].join("\n");
@@ -530,10 +555,10 @@ function buildArtifactLinks(root, slug, runDirName, createdAt, contentFingerprin
530
555
  raw: relativePath(root, longTermTargets.raw),
531
556
  sourceCard: relativePath(root, longTermTargets.sourceCard),
532
557
  wikiEntry: relativePath(root, longTermTargets.wikiEntry),
533
- claims: relativePath(root, longTermTargets.claims),
534
- assets: relativePath(root, longTermTargets.assets),
535
- topics: relativePath(root, longTermTargets.topics),
536
- outline: relativePath(root, longTermTargets.outline),
558
+ claims: longTermTargets.claims ? relativePath(root, longTermTargets.claims) : undefined,
559
+ assets: longTermTargets.assets ? relativePath(root, longTermTargets.assets) : undefined,
560
+ topics: longTermTargets.topics ? relativePath(root, longTermTargets.topics) : undefined,
561
+ outline: longTermTargets.outline ? relativePath(root, longTermTargets.outline) : undefined,
537
562
  runSummary: `09-runs/${runDirName}/processing-summary.md`
538
563
  };
539
564
  }
@@ -545,10 +570,10 @@ function relationshipFrontmatter(links) {
545
570
  `wiki_entry: "${escapeYaml(obsidianLink(links.wikiEntry, "Wiki 条目"))}"`,
546
571
  `source_card: "${escapeYaml(obsidianLink(links.sourceCard, "资料卡"))}"`,
547
572
  `raw_note: "${escapeYaml(obsidianLink(links.raw, "原文"))}"`,
548
- `claims_note: "${escapeYaml(obsidianLink(links.claims, "Claim 建议"))}"`,
549
- `assets_note: "${escapeYaml(obsidianLink(links.assets, "素材建议"))}"`,
550
- `topics_note: "${escapeYaml(obsidianLink(links.topics, "选题"))}"`,
551
- `outline_note: "${escapeYaml(obsidianLink(links.outline, "大纲"))}"`,
573
+ ...(links.claims ? [`claims_note: "${escapeYaml(obsidianLink(links.claims, "Claim 建议"))}"`] : []),
574
+ ...(links.assets ? [`assets_note: "${escapeYaml(obsidianLink(links.assets, "素材建议"))}"`] : []),
575
+ ...(links.topics ? [`topics_note: "${escapeYaml(obsidianLink(links.topics, "选题"))}"`] : []),
576
+ ...(links.outline ? [`outline_note: "${escapeYaml(obsidianLink(links.outline, "大纲"))}"`] : []),
552
577
  `run_summary: "${escapeYaml(obsidianLink(links.runSummary, "处理记录"))}"`
553
578
  ];
554
579
  }
package/dist/src/lint.js CHANGED
@@ -2,7 +2,7 @@ import { promises as fs } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { frontmatterArray, frontmatterBoolean, frontmatterString, parseMarkdown } from "./frontmatter.js";
4
4
  import { relativePath, safeJoin } from "./paths.js";
5
- import { exists } from "./workspace.js";
5
+ import { exists, OPTIONAL_DIRS, OPTIONAL_PARENT_DIRS } from "./workspace.js";
6
6
  export async function lintWorkspace(rootPath, now = new Date().toISOString()) {
7
7
  const root = path.resolve(rootPath);
8
8
  const wikiEntries = await readNotes(root, "05-wiki/source-knowledge");
@@ -20,6 +20,7 @@ export async function lintWorkspace(rootPath, now = new Date().toISOString()) {
20
20
  ];
21
21
  const issues = [];
22
22
  issues.push(...await systemFileIssues(root));
23
+ issues.push(...await emptyOptionalDirectoryIssues(root));
23
24
  const wikiSourceCards = new Set(wikiEntries.map((note) => frontmatterString(note.frontmatter, "source_card")).filter(Boolean));
24
25
  for (const card of sourceCards) {
25
26
  if (!wikiSourceCards.has(card.path)) {
@@ -135,6 +136,7 @@ export async function lintWorkspace(rootPath, now = new Date().toISOString()) {
135
136
  raw_files: rawFiles.length,
136
137
  runs: runs.length
137
138
  },
139
+ safe_fixes: safeFixSummary(issues),
138
140
  issues
139
141
  };
140
142
  }
@@ -144,9 +146,31 @@ export function filterLintReport(report, severity) {
144
146
  }
145
147
  return {
146
148
  ...report,
149
+ safe_fixes: safeFixSummary(report.issues.filter((issue) => issue.severity === severity), report.safe_fixes.applied),
147
150
  issues: report.issues.filter((issue) => issue.severity === severity)
148
151
  };
149
152
  }
153
+ export async function removeEmptyOptionalDirs(rootPath) {
154
+ const root = path.resolve(rootPath);
155
+ const applied = [];
156
+ for (const dir of [...OPTIONAL_DIRS].sort((left, right) => right.length - left.length)) {
157
+ if (await removeKnownEmptyDir(root, dir)) {
158
+ applied.push(safeFixFor(dir));
159
+ }
160
+ }
161
+ for (const dir of [...OPTIONAL_PARENT_DIRS].sort((left, right) => right.length - left.length)) {
162
+ if (await removeKnownEmptyDir(root, dir)) {
163
+ applied.push(safeFixFor(dir));
164
+ }
165
+ }
166
+ return applied;
167
+ }
168
+ export function attachAppliedSafeFixes(report, applied) {
169
+ return {
170
+ ...report,
171
+ safe_fixes: safeFixSummary(report.issues, applied)
172
+ };
173
+ }
150
174
  export async function writeLintReport(rootPath, report) {
151
175
  const root = path.resolve(rootPath);
152
176
  const target = safeJoin(root, "dashboards", "Lint Report.md");
@@ -171,6 +195,8 @@ export function renderLintReport(report) {
171
195
  `- Errors: ${counts.error}`,
172
196
  `- Warnings: ${counts.warning}`,
173
197
  `- Info: ${counts.info}`,
198
+ `- Safe Fixes Available: ${report.safe_fixes.available}`,
199
+ `- Only Safe Fixes: ${report.safe_fixes.only_safe_fixes ? "yes" : "no"}`,
174
200
  `- Top Issue: ${topIssue ? formatIssueLine(topIssue) : "none"}`,
175
201
  "",
176
202
  "## Suggested Handling Order",
@@ -190,6 +216,7 @@ export function renderLintSummary(report, reportPath) {
190
216
  const topIssue = report.issues[0];
191
217
  return [
192
218
  `lint_summary: errors=${counts.error} warnings=${counts.warning} info=${counts.info}`,
219
+ `safe_fixes: available=${report.safe_fixes.available} applied=${report.safe_fixes.applied.length} only_safe_fixes=${report.safe_fixes.only_safe_fixes ? "yes" : "no"}`,
193
220
  `top_issue: ${topIssue ? formatIssueLine(topIssue) : "none"}`,
194
221
  ...(reportPath ? [`report: ${reportPath}`] : [])
195
222
  ].join("\n");
@@ -211,6 +238,62 @@ async function systemFileIssues(root) {
211
238
  }
212
239
  return issues;
213
240
  }
241
+ async function emptyOptionalDirectoryIssues(root) {
242
+ const issues = [];
243
+ for (const dir of [...OPTIONAL_DIRS, ...OPTIONAL_PARENT_DIRS]) {
244
+ if (!(await isExistingEmptyDirectory(path.join(root, dir)))) {
245
+ continue;
246
+ }
247
+ issues.push({
248
+ severity: "info",
249
+ path: dir,
250
+ category: "empty_optional_directory",
251
+ action: "remove_empty_optional_dir",
252
+ message: `Optional directory is empty and can be safely removed: ${dir}`,
253
+ suggestion: "Run aiwiki lint --fix-empty-dirs --json to remove known empty optional directories.",
254
+ safe_fix: safeFixFor(dir)
255
+ });
256
+ }
257
+ return issues;
258
+ }
259
+ async function removeKnownEmptyDir(root, dir) {
260
+ const target = path.join(root, dir);
261
+ if (!(await isExistingEmptyDirectory(target))) {
262
+ return false;
263
+ }
264
+ await fs.rmdir(target);
265
+ return true;
266
+ }
267
+ async function isExistingEmptyDirectory(target) {
268
+ try {
269
+ const stats = await fs.stat(target);
270
+ if (!stats.isDirectory()) {
271
+ return false;
272
+ }
273
+ return (await fs.readdir(target)).length === 0;
274
+ }
275
+ catch (error) {
276
+ if (error.code === "ENOENT") {
277
+ return false;
278
+ }
279
+ throw error;
280
+ }
281
+ }
282
+ function safeFixFor(dir) {
283
+ return {
284
+ action: "remove_empty_optional_dir",
285
+ path: dir,
286
+ command: "aiwiki lint --fix-empty-dirs --json"
287
+ };
288
+ }
289
+ function safeFixSummary(issues, applied = []) {
290
+ const available = issues.filter((issue) => issue.safe_fix).length;
291
+ return {
292
+ available,
293
+ applied,
294
+ only_safe_fixes: issues.length > 0 && issues.every((issue) => Boolean(issue.safe_fix))
295
+ };
296
+ }
214
297
  async function readNotes(root, dir) {
215
298
  const absolute = path.join(root, dir);
216
299
  if (!(await exists(absolute))) {
@@ -40,11 +40,8 @@ export function normalizePayload(raw, runStartedAt) {
40
40
  const requestRaw = isRecord(raw.request) ? raw.request : {};
41
41
  const requestedOutputs = Array.isArray(requestRaw.outputs)
42
42
  ? requestRaw.outputs.filter((item) => typeof item === "string")
43
- : ["source_card", "creative_assets", "topics", "draft_outline", "processing_summary"];
44
- const outputs = ["source_card", "wiki_entry", "creative_assets", "topics", "draft_outline", "processing_summary"];
45
- if (fetchStatus !== "failed" && requestedOutputs.length && hasCustomOutputRequest(requestedOutputs)) {
46
- warnings.push("AIWiki 会为每条输入生成完整资料产物,request.outputs 已按全量输出处理。");
47
- }
43
+ : undefined;
44
+ const outputs = normalizeOutputs(requestedOutputs, fetchStatus, warnings);
48
45
  if (typeof raw.target_kb === "string" && raw.target_kb.trim()) {
49
46
  warnings.push(`target_kb=${raw.target_kb} 已被当前知识库流程忽略。`);
50
47
  }
@@ -313,9 +310,27 @@ function hasAnalysisContent(analysis) {
313
310
  analysis.suggested_links.length ||
314
311
  analysis.outline);
315
312
  }
316
- function hasCustomOutputRequest(outputs) {
317
- const legacyDefault = ["source_card", "creative_assets", "topics", "draft_outline", "processing_summary"];
318
- const currentDefault = ["source_card", "wiki_entry", "creative_assets", "topics", "draft_outline", "processing_summary"];
319
- const sameSet = (left, right) => left.length === right.length && left.every((item) => right.includes(item));
320
- return !sameSet(outputs, legacyDefault) && !sameSet(outputs, currentDefault);
313
+ function normalizeOutputs(outputs, fetchStatus, warnings) {
314
+ if (fetchStatus === "failed") {
315
+ return ["processing_summary"];
316
+ }
317
+ const core = ["source_card", "wiki_entry", "processing_summary"];
318
+ const allowed = new Set([
319
+ ...core,
320
+ "claims",
321
+ "claim_suggestions",
322
+ "creative_assets",
323
+ "topics",
324
+ "draft_outline"
325
+ ]);
326
+ const requested = outputs ?? core;
327
+ const normalized = new Set(core);
328
+ for (const output of requested) {
329
+ if (!allowed.has(output)) {
330
+ warnings.push(`request.outputs ignored unknown output: ${output}`);
331
+ continue;
332
+ }
333
+ normalized.add(output);
334
+ }
335
+ return [...normalized];
321
336
  }
@@ -32,9 +32,9 @@ function wikiFrontmatter(payload, links, title, mode, quality, grounding) {
32
32
  `source_type: "${escapeYaml(payload.source.kind)}"`,
33
33
  `source_card: "${escapeYaml(links.sourceCard)}"`,
34
34
  `raw_file: "${escapeYaml(links.raw)}"`,
35
- `claims_file: "${escapeYaml(links.claims)}"`,
36
- `topics_file: "${escapeYaml(links.topics)}"`,
37
- `outline_file: "${escapeYaml(links.outline)}"`,
35
+ ...(links.claims ? [`claims_file: "${escapeYaml(links.claims)}"`] : []),
36
+ ...(links.topics ? [`topics_file: "${escapeYaml(links.topics)}"`] : []),
37
+ ...(links.outline ? [`outline_file: "${escapeYaml(links.outline)}"`] : []),
38
38
  `run_summary: "${escapeYaml(links.runSummary)}"`,
39
39
  `run_id: "${escapeYaml(links.runId)}"`,
40
40
  `content_fingerprint: "${escapeYaml(links.contentFingerprint)}"`,
@@ -7,21 +7,30 @@ import { frontmatterBoolean, frontmatterString, parseMarkdown } from "./frontmat
7
7
  import { CliError } from "./output.js";
8
8
  import { relativePath } from "./paths.js";
9
9
  export const CONFIG_FILE = "aiwiki.yaml";
10
- export const REQUIRED_DIRS = [
10
+ export const CORE_DIRS = [
11
11
  "02-raw/articles",
12
12
  "03-sources/article-cards",
13
- "04-claims/_suggestions",
14
13
  "05-wiki",
15
14
  "05-wiki/source-knowledge",
16
- "06-assets/_suggestions",
17
- "07-topics/ready",
18
- "08-outputs/outlines",
19
15
  "09-runs",
20
16
  "dashboards",
21
17
  "_system/templates",
22
18
  "_system/schemas",
23
19
  "_system/logs"
24
20
  ];
21
+ export const OPTIONAL_DIRS = [
22
+ "04-claims/_suggestions",
23
+ "06-assets/_suggestions",
24
+ "07-topics/ready",
25
+ "08-outputs/outlines"
26
+ ];
27
+ export const OPTIONAL_PARENT_DIRS = [
28
+ "04-claims",
29
+ "06-assets",
30
+ "07-topics",
31
+ "08-outputs"
32
+ ];
33
+ export const REQUIRED_DIRS = CORE_DIRS;
25
34
  const WORKSPACE_SEEDS = [
26
35
  {
27
36
  path: "_system/purpose.md",
@@ -357,7 +366,7 @@ export async function initWorkspace(rootPath) {
357
366
  const root = resolveRoot(rootPath);
358
367
  await fs.mkdir(root, { recursive: true });
359
368
  const createdDirs = [];
360
- for (const dir of REQUIRED_DIRS) {
369
+ for (const dir of CORE_DIRS) {
361
370
  const absolute = path.join(root, dir);
362
371
  const existed = await exists(absolute);
363
372
  await fs.mkdir(absolute, { recursive: true });
@@ -452,7 +461,7 @@ export async function promptForSetup(options) {
452
461
  if (!options.yes) {
453
462
  const root = resolveRoot(rootPath);
454
463
  output.write(`将创建或补齐 AIWiki 目录: ${root}\n`);
455
- for (const dir of REQUIRED_DIRS) {
464
+ for (const dir of CORE_DIRS) {
456
465
  output.write(` - ${dir}\n`);
457
466
  }
458
467
  const answer = await rl.question("确认创建?输入 y 继续: ");
@@ -483,7 +492,7 @@ async function promptForSetupFromPipe(options) {
483
492
  if (!options.yes) {
484
493
  const root = resolveRoot(rootPath);
485
494
  output.write(`将创建或补齐 AIWiki 目录: ${root}\n`);
486
- for (const dir of REQUIRED_DIRS) {
495
+ for (const dir of CORE_DIRS) {
487
496
  output.write(` - ${dir}\n`);
488
497
  }
489
498
  output.write("确认创建?输入 y 继续: ");
@@ -503,7 +512,7 @@ export async function confirmInit(rootPath) {
503
512
  const rl = createInterface({ input, output });
504
513
  try {
505
514
  output.write(`将创建或补齐 AIWiki 目录: ${root}\n`);
506
- for (const dir of REQUIRED_DIRS) {
515
+ for (const dir of CORE_DIRS) {
507
516
  output.write(` - ${dir}\n`);
508
517
  }
509
518
  const answer = await rl.question("确认创建?输入 y 继续: ");