fix(rc): address all P0/P1 review items for 2.0 RC

P0: tsconfig lib upgraded from ES6 to ES2018 (matches esbuild target,
    eliminates Object.entries type risk)

P1: sourceLinks format safety
  - Prompt explicitly forbids [[]] in sourceLinks
  - ArtifactRenderer.wikiLink() strips accidental double-wrapping
  - All sourceLinks rendering uses wikiLink() helper

P1: High-risk content disclaimer
  - ArtifactRenderer.needsSafetyDisclaimer() detects sensitive keywords
    (health/baby/medical/legal/investment) and high-risk items
  - Automatically prepends warning callout to template/run output

P1: Preview shows concrete file paths
  - ArtifactPreviewModal shows exact Knowledge/Artifacts/xxx.md and
    Knowledge/Artifact Runs/yyyy xxx.md paths instead of '1-2 files'

P1: README Architecture tree updated with core/artifact + features/artifact
P1: README Configuration table corrected to match actual settings page

P2: YAML frontmatter escaping
  - ArtifactRenderer.yamlStr() uses JSON.stringify for safe YAML scalars
  - title and target fields no longer break on quotes/special chars

P2: Prompt clarifies 'do not fabricate sourceLinks'
This commit is contained in:
Yan 2026-05-20 18:33:51 +08:00
parent 2bf2032b5d
commit 25eb0bc04c
5 changed files with 54 additions and 18 deletions

View file

@ -122,9 +122,9 @@ Settings are organized into three tabs:
| Tab | Key settings |
| --- | --- |
| **Knowledge** | Q&A path, Concepts path, Questions index path, Reading path, auto-classify, concept depth, vault QA source limit |
| **Execution** | Require preview, log path, scheduled tasks, safety policy |
| **Auxiliary** | API key, Base URL, model, output style, Baidu sync, UI preferences |
| **Knowledge** | Q&A path, Concepts path, Questions index path, knowledge index status + rebuild |
| **Execution** | Execution log path (read-only), scheduled tasks status (v2.1) |
| **Auxiliary** | API key, Base URL, model, output style, Baidu sync (App ID/Secret, remote path, auto-backup) |
---
@ -150,12 +150,14 @@ src/
core/
llm/ Unified LLM client + JSON extractor
knowledge/ KnowledgeIndexService (metadata index)
execution/ ExecutionPlan, PlanBuilder, PlanExecutor
scheduler/ ScheduledTask types + runner (WIP)
execution/ ExecutionPlan, PlanBuilder, PlanExecutor, PlanDraftStore
artifact/ ExecutionArtifact types, prompt, validation, rendering
scheduler/ ScheduledTask types + runner (disabled in v2.0)
schema.ts SCHEMA_VERSION + helpers
ai/ AI feature modules (assistant, classifier, planner, ...)
features/
assistant/ AI assistant modals
artifact/ Artifact builder, preview, feature controller
concept/ Concept completion + page manager
question/ Question classify + graph manager
reading/ Reading project manager

View file

@ -20,8 +20,11 @@ const SYSTEM_PROMPT = `你是一个知识到执行资产的结构化转换助手
3. inferred: true
4. riskLevel: "watch" "high"
5.
6. id 使 "item-1", "item-2"
7. JSON
6. sourceLinkssourceLinks "来源内容" path|alias [[ ]]
"Reading/Book/Chapter1.md|第1章"
"[[Reading/Book/Chapter1.md|第1章]]"
7. id 使 "item-1", "item-2"
8. JSON
{

View file

@ -26,12 +26,12 @@ export class ArtifactRenderer {
`type: execution-artifact-${docType}`,
`schema_version: ${SCHEMA_VERSION}`,
`artifact_type: ${artifact.artifactType}`,
`title: "${artifact.title}"`,
`title: ${this.yamlStr(artifact.title)}`,
`usage_mode: ${artifact.usageMode}`,
`source_scope: ${artifact.sourceScope}`,
`evidence_policy: ${artifact.evidencePolicy}`,
];
if (artifact.target) lines.push(`target: "${artifact.target}"`);
if (artifact.target) lines.push(`target: ${this.yamlStr(artifact.target)}`);
if (artifact.frequency) lines.push(`frequency: ${artifact.frequency}`);
lines.push(`status: draft`);
lines.push(`created_at: ${todayIso()}`);
@ -45,7 +45,7 @@ export class ArtifactRenderer {
`type: execution-artifact-run`,
`schema_version: ${SCHEMA_VERSION}`,
`artifact_type: ${artifact.artifactType}`,
`template: "${artifact.title}"`,
`template: ${this.yamlStr(artifact.title)}`,
`date: ${date}`,
`status: open`,
"---",
@ -65,10 +65,15 @@ export class ArtifactRenderer {
artifact.target ? `> 对象:${artifact.target}` : null,
artifact.frequency ? `> 频率:${artifact.frequency}` : null,
artifact.sourceLinks.length > 0
? `> 来源:${artifact.sourceLinks.map((l) => `[[${l}]]`).join("、")}`
? `> 来源:${artifact.sourceLinks.map((l) => this.wikiLink(l)).join("、")}`
: null,
].filter(Boolean).join("\n");
// Safety disclaimer for sensitive domains
const disclaimer = this.needsSafetyDisclaimer(artifact)
? `\n> [!warning] 使用边界\n> 该执行资产用于记录、观察和复盘,不构成医学、法律或投资建议。出现异常情况请咨询专业人士。\n`
: "";
// Group items by category
const grouped = this.groupByCategory(artifact.items);
const sections: string[] = [];
@ -87,7 +92,7 @@ export class ArtifactRenderer {
inferredSection = `\n## 未验证项\n\n> [!warning]\n> 以下 ${inferredItems.length} 个条目由 AI 推断生成,未找到明确来源,建议人工确认。\n\n${inferredItems.map((i) => `- ${i.title}`).join("\n")}\n`;
}
return `${title}\n\n${meta}\n${sections.join("\n")}${inferredSection}`;
return `${title}\n\n${meta}${disclaimer}\n${sections.join("\n")}${inferredSection}`;
}
private renderItem(item: ArtifactItem, isRun: boolean): string {
@ -98,7 +103,7 @@ export class ArtifactRenderer {
lines.push(`${checkbox} ${item.title}${riskMark}`);
if (item.sourceLinks.length > 0) {
lines.push(` - 依据:${item.sourceLinks.map((l) => `[[${l}]]`).join("、")}`);
lines.push(` - 依据:${item.sourceLinks.map((l) => this.wikiLink(l)).join("、")}`);
} else if (item.inferred) {
lines.push(` - 依据:*AI 推断,建议确认*`);
}
@ -120,6 +125,23 @@ export class ArtifactRenderer {
return `\n## 今日问题\n\n- \n\n## 明日调整\n\n- \n`;
}
/** Safely encode a string for YAML (uses JSON quoting). */
private yamlStr(value: string): string {
return JSON.stringify(value);
}
private needsSafetyDisclaimer(artifact: ExecutionArtifact): boolean {
const sensitiveKeywords = [
"婴儿", "母亲", "健康", "医疗", "诊断", "症状", "用药", "治疗",
"法律", "合同", "诉讼", "投资", "理财", "基金", "股票",
"baby", "infant", "health", "medical", "diagnosis", "legal", "investment",
];
const text = `${artifact.title} ${artifact.target ?? ""}`.toLowerCase();
const hasHighRisk = artifact.items.some((i) => i.riskLevel === "high" || i.riskLevel === "watch");
const hasSensitiveKeyword = sensitiveKeywords.some((k) => text.includes(k));
return hasHighRisk || hasSensitiveKeyword;
}
private groupByCategory(items: ArtifactItem[]): Map<string, ArtifactItem[]> {
const map = new Map<string, ArtifactItem[]>();
for (const item of items) {
@ -129,4 +151,13 @@ export class ArtifactRenderer {
}
return map;
}
/** Ensure a link string becomes a proper [[wikilink]] without double-wrapping. */
private wikiLink(link: string): string {
const trimmed = link.trim();
if (trimmed.startsWith("[[") && trimmed.endsWith("]]")) return trimmed;
// Strip accidental [[ ]] if partially wrapped
const cleaned = trimmed.replace(/^\[\[/, "").replace(/\]\]$/, "");
return `[[${cleaned}]]`;
}
}

View file

@ -35,10 +35,10 @@ export class ArtifactPreviewModal extends Modal {
if (stats.highRisk > 0) statsEl.createSpan({ text: `高关注:${stats.highRisk}`, attr: { style: "color: var(--text-error);" } });
// File impact notice
contentEl.createEl("p", {
text: `保存后将创建 1-2 个文件(模板 + 可选的今日记录)`,
attr: { style: "font-size: 12px; color: var(--text-muted); margin-bottom: 8px;" },
});
const safeName = this.artifact.title.replace(/[\\/:*?"<>|#[\]]/g, "-").slice(0, 50);
contentEl.createEl("div", {
attr: { style: "font-size: 12px; color: var(--text-muted); margin-bottom: 8px; padding: 6px 8px; background: var(--background-secondary); border-radius: 4px;" },
}).innerHTML = `将创建:<br>• <code>Knowledge/Artifacts/${safeName}.md</code><br>• <code>Knowledge/Artifact Runs/${new Date().toISOString().slice(0, 10)} ${safeName}.md</code>(仅"保存并生成今日记录"时)`;
// Warnings
if (this.validation.warnings.length > 0) {

View file

@ -17,7 +17,7 @@
"strictFunctionTypes": true,
"strictNullChecks": true,
"noFallthroughCasesInSwitch": true,
"lib": ["ES6", "DOM"]
"lib": ["ES2018", "DOM"]
},
"include": ["src/**/*.ts"]
}