@itradingai/aiwiki 0.2.16 → 0.2.19

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.
@@ -11,13 +11,28 @@ const GROUPS = [
11
11
  { key: "outlines", dir: "08-outputs/outlines", weight: 2 },
12
12
  { key: "raw_refs", dir: "02-raw/articles", weight: 1 }
13
13
  ];
14
- export async function buildContext(rootPath, query, now = new Date().toISOString()) {
14
+ const DEFAULT_LIMIT = 10;
15
+ export async function buildContext(rootPath, query, options = {}, now = new Date().toISOString()) {
15
16
  const root = path.resolve(rootPath);
16
17
  const tokens = tokenize(query);
18
+ const limit = normalizeLimit(options.limit);
19
+ const filters = normalizeFilters(options.filters);
17
20
  const result = {
18
21
  schema_version: "aiwiki.context.v1",
19
22
  query,
20
23
  generated_at: now,
24
+ query_scope: {
25
+ filters,
26
+ limit,
27
+ searched_groups: []
28
+ },
29
+ result_quality: {
30
+ total_matches: 0,
31
+ best_score: 0,
32
+ has_wiki_entry: false,
33
+ warnings: []
34
+ },
35
+ recommended_next_action: "broaden_query",
21
36
  matches: {
22
37
  wiki_entries: [],
23
38
  source_cards: [],
@@ -26,34 +41,41 @@ export async function buildContext(rootPath, query, now = new Date().toISOString
26
41
  outlines: [],
27
42
  raw_refs: []
28
43
  },
29
- suggested_answer_structure: ["主题概览", "核心观点", "已有资料依据", "可复用判断", "下一步建议"],
44
+ suggested_answer_structure: ["topic overview", "core claims", "available evidence", "reuse judgment", "next action"],
30
45
  warnings: []
31
46
  };
32
47
  if (!tokens.length) {
33
48
  result.warnings.push("query is empty after tokenization");
49
+ finalizeQuality(result);
34
50
  return result;
35
51
  }
36
52
  for (const group of GROUPS.filter((item) => item.key !== "raw_refs")) {
53
+ if (!groupAllowed(group.key, filters.type)) {
54
+ continue;
55
+ }
37
56
  const dir = path.join(root, group.dir);
38
57
  if (!(await exists(dir))) {
39
58
  continue;
40
59
  }
41
- const matches = await searchDir(root, dir, tokens, group.weight);
42
- result.matches[group.key].push(...matches.slice(0, 10));
60
+ result.query_scope.searched_groups.push(group.key);
61
+ const matches = await searchDir(root, dir, tokens, group.weight, group.key, filters);
62
+ result.matches[group.key].push(...matches.slice(0, limit));
43
63
  }
44
- if (!result.matches.wiki_entries.length) {
45
- result.warnings.push("未命中 Wiki Entry,结果可能来自资料卡、选题或原文引用。");
64
+ if (!result.matches.wiki_entries.length && groupAllowed("raw_refs", filters.type)) {
65
+ result.warnings.push("No Wiki Entry matched; results may come from source cards, topics, outlines, or raw references.");
46
66
  const rawGroup = GROUPS.find((item) => item.key === "raw_refs");
47
67
  if (rawGroup) {
48
68
  const rawDir = path.join(root, rawGroup.dir);
49
69
  if (await exists(rawDir)) {
50
- result.matches.raw_refs.push(...(await searchDir(root, rawDir, tokens, rawGroup.weight)).slice(0, 10));
70
+ result.query_scope.searched_groups.push(rawGroup.key);
71
+ result.matches.raw_refs.push(...(await searchDir(root, rawDir, tokens, rawGroup.weight, rawGroup.key, filters)).slice(0, limit));
51
72
  }
52
73
  }
53
74
  }
75
+ finalizeQuality(result);
54
76
  return result;
55
77
  }
56
- async function searchDir(root, dir, tokens, weight) {
78
+ async function searchDir(root, dir, tokens, weight, groupKey, filters) {
57
79
  const files = await listMarkdownFiles(dir);
58
80
  const matches = [];
59
81
  for (const file of files) {
@@ -61,12 +83,24 @@ async function searchDir(root, dir, tokens, weight) {
61
83
  const parsed = parseMarkdown(text);
62
84
  const rel = relativePath(root, file);
63
85
  const title = frontmatterString(parsed.frontmatter, "title") ?? path.basename(file, ".md");
86
+ const type = frontmatterString(parsed.frontmatter, "type") ?? groupKey;
87
+ const status = frontmatterString(parsed.frontmatter, "status");
88
+ const sourceRole = frontmatterString(parsed.frontmatter, "source_role");
89
+ const wikiType = frontmatterString(parsed.frontmatter, "wiki_type");
90
+ if (!passesFilters({ type, groupKey, status, source_role: sourceRole, wiki_type: wikiType }, filters)) {
91
+ continue;
92
+ }
93
+ const topics = frontmatterArray(parsed.frontmatter, "topics");
94
+ const tags = frontmatterArray(parsed.frontmatter, "tags");
95
+ const relatedRefs = relatedReferences(parsed.frontmatter, parsed.body);
96
+ const sourceUrl = frontmatterString(parsed.frontmatter, "source_url") ?? "";
64
97
  const haystack = [
65
98
  rel,
66
99
  title,
67
- frontmatterString(parsed.frontmatter, "source_url") ?? "",
68
- frontmatterArray(parsed.frontmatter, "topics").join(" "),
69
- frontmatterArray(parsed.frontmatter, "tags").join(" "),
100
+ sourceUrl,
101
+ topics.join(" "),
102
+ tags.join(" "),
103
+ relatedRefs.join(" "),
70
104
  parsed.body
71
105
  ].join("\n").toLowerCase();
72
106
  const hits = tokens.filter((token) => haystack.includes(token.toLowerCase())).length;
@@ -77,8 +111,9 @@ async function searchDir(root, dir, tokens, weight) {
77
111
  const quality = frontmatterString(parsed.frontmatter, "quality");
78
112
  const groundingMarkers = frontmatterArray(parsed.frontmatter, "grounding_markers");
79
113
  const groundingNeedsReview = frontmatterBoolean(parsed.frontmatter, "grounding_needs_review");
114
+ const groundingEvidenceAvailable = frontmatterBoolean(parsed.frontmatter, "grounding_evidence_available");
80
115
  const warnings = generationMode === "deterministic_fallback"
81
- ? [" Wiki Entry deterministic fallback,仅包含来源、正文预览和待补全区。"]
116
+ ? ["This Wiki Entry is a deterministic fallback; it may need host-agent enrichment."]
82
117
  : [];
83
118
  if (groundingNeedsReview) {
84
119
  warnings.push(`Grounding needs review${groundingMarkers.length ? `: ${groundingMarkers.join(", ")}` : ""}.`);
@@ -88,11 +123,19 @@ async function searchDir(root, dir, tokens, weight) {
88
123
  path: rel,
89
124
  summary: frontmatterString(parsed.frontmatter, "summary") ?? summarize(parsed.body, quality),
90
125
  score: Number(((hits / tokens.length) * weight).toFixed(2)),
91
- topics: frontmatterArray(parsed.frontmatter, "topics"),
92
- source_url: frontmatterString(parsed.frontmatter, "source_url") ?? "",
126
+ type,
127
+ topics,
128
+ tags,
129
+ source_url: sourceUrl,
130
+ status,
131
+ source_role: sourceRole,
132
+ wiki_type: wikiType,
133
+ match_reasons: matchReasons(tokens, { rel, title, body: parsed.body, topics, tags, relatedRefs, sourceUrl }),
134
+ quality_signals: qualitySignals({ quality, generationMode, groundingEvidenceAvailable, groundingNeedsReview, status, relatedRefs }),
135
+ related_refs: relatedRefs,
93
136
  generation_mode: generationMode,
94
137
  quality,
95
- grounding_evidence_available: frontmatterBoolean(parsed.frontmatter, "grounding_evidence_available"),
138
+ grounding_evidence_available: groundingEvidenceAvailable,
96
139
  grounding_needs_review: groundingNeedsReview,
97
140
  grounding_markers: groundingMarkers,
98
141
  warnings
@@ -125,7 +168,121 @@ function tokenize(value) {
125
168
  function summarize(body, quality) {
126
169
  const compact = body.replace(/\s+/g, " ").trim();
127
170
  if (quality === "scaffold") {
128
- return "仅有正文预览,未生成高质量摘要。";
171
+ return "Only a scaffold preview is available; enrich before relying on it as a final answer.";
129
172
  }
130
173
  return compact.length > 180 ? `${compact.slice(0, 180)}...` : compact;
131
174
  }
175
+ function normalizeLimit(value) {
176
+ if (typeof value !== "number" || !Number.isFinite(value)) {
177
+ return DEFAULT_LIMIT;
178
+ }
179
+ return Math.max(1, Math.min(50, Math.floor(value)));
180
+ }
181
+ function normalizeFilters(filters) {
182
+ return Object.fromEntries(Object.entries(filters ?? {})
183
+ .filter(([, value]) => typeof value === "string" && value.trim())
184
+ .map(([key, value]) => [key, String(value).trim()]));
185
+ }
186
+ function groupAllowed(groupKey, typeFilter) {
187
+ if (!typeFilter) {
188
+ return true;
189
+ }
190
+ return normalizeType(typeFilter) === normalizeType(groupKey);
191
+ }
192
+ function passesFilters(item, filters) {
193
+ if (filters.type && normalizeType(filters.type) !== normalizeType(item.type) && normalizeType(filters.type) !== normalizeType(item.groupKey)) {
194
+ return false;
195
+ }
196
+ if (filters.status && filters.status !== item.status) {
197
+ return false;
198
+ }
199
+ if (filters.source_role && filters.source_role !== item.source_role) {
200
+ return false;
201
+ }
202
+ if (filters.wiki_type && filters.wiki_type !== item.wiki_type) {
203
+ return false;
204
+ }
205
+ return true;
206
+ }
207
+ function normalizeType(value) {
208
+ return value.replace(/-/g, "_").toLowerCase();
209
+ }
210
+ function relatedReferences(frontmatter, body) {
211
+ const refs = [
212
+ frontmatterString(frontmatter, "source_card"),
213
+ frontmatterString(frontmatter, "raw_file"),
214
+ frontmatterString(frontmatter, "claims_note"),
215
+ frontmatterString(frontmatter, "assets_note"),
216
+ frontmatterString(frontmatter, "topics_note"),
217
+ frontmatterString(frontmatter, "outline_note"),
218
+ ...extractWikilinks(body)
219
+ ].filter((value) => Boolean(value));
220
+ return Array.from(new Set(refs));
221
+ }
222
+ function extractWikilinks(body) {
223
+ const refs = [];
224
+ const pattern = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g;
225
+ for (const match of body.matchAll(pattern)) {
226
+ refs.push(match[1].trim());
227
+ }
228
+ return refs;
229
+ }
230
+ function matchReasons(tokens, fields) {
231
+ const reasons = new Set();
232
+ for (const token of tokens.map((item) => item.toLowerCase())) {
233
+ if (fields.title.toLowerCase().includes(token))
234
+ reasons.add("title");
235
+ if (fields.rel.toLowerCase().includes(token))
236
+ reasons.add("path");
237
+ if (fields.sourceUrl.toLowerCase().includes(token))
238
+ reasons.add("source_url");
239
+ if (fields.topics.join(" ").toLowerCase().includes(token))
240
+ reasons.add("topics");
241
+ if (fields.tags.join(" ").toLowerCase().includes(token))
242
+ reasons.add("tags");
243
+ if (fields.relatedRefs.join(" ").toLowerCase().includes(token))
244
+ reasons.add("relationships");
245
+ if (fields.body.toLowerCase().includes(token))
246
+ reasons.add("body");
247
+ }
248
+ return Array.from(reasons);
249
+ }
250
+ function qualitySignals(item) {
251
+ const signals = [];
252
+ if (item.quality)
253
+ signals.push(`quality:${item.quality}`);
254
+ if (item.status)
255
+ signals.push(`status:${item.status}`);
256
+ if (item.generationMode)
257
+ signals.push(`generation_mode:${item.generationMode}`);
258
+ if (item.groundingEvidenceAvailable === true)
259
+ signals.push("grounding:evidence_available");
260
+ if (item.groundingEvidenceAvailable === false)
261
+ signals.push("grounding:no_evidence_flag");
262
+ if (item.groundingNeedsReview)
263
+ signals.push("grounding:needs_review");
264
+ if (item.relatedRefs.length)
265
+ signals.push("relationships:present");
266
+ return signals;
267
+ }
268
+ function finalizeQuality(result) {
269
+ const all = Object.values(result.matches).flat();
270
+ result.result_quality = {
271
+ total_matches: all.length,
272
+ best_score: all.reduce((best, item) => Math.max(best, item.score), 0),
273
+ has_wiki_entry: result.matches.wiki_entries.length > 0,
274
+ warnings: result.warnings
275
+ };
276
+ if (!all.length) {
277
+ result.recommended_next_action = "broaden_query_or_ingest_source";
278
+ }
279
+ else if (!result.matches.wiki_entries.length) {
280
+ result.recommended_next_action = "review_source_cards_then_create_wiki_entry";
281
+ }
282
+ else if (all.some((item) => item.grounding_needs_review || item.quality === "scaffold")) {
283
+ result.recommended_next_action = "review_grounding_or_enrich_entry";
284
+ }
285
+ else {
286
+ result.recommended_next_action = "use_matches_for_answer";
287
+ }
288
+ }
@@ -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)}"`,