@itradingai/aiwiki 0.2.5
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/LICENSE +21 -0
- package/README.md +175 -0
- package/dist/src/app.js +417 -0
- package/dist/src/args.js +32 -0
- package/dist/src/cli.js +4 -0
- package/dist/src/ingest.js +428 -0
- package/dist/src/output.js +10 -0
- package/dist/src/paths.js +32 -0
- package/dist/src/payload.js +95 -0
- package/dist/src/workspace.js +495 -0
- package/docs/AGENT_HANDOFF.md +132 -0
- package/docs/OBSIDIAN_DATAVIEW_PLAN.md +194 -0
- package/docs/README.md +28 -0
- package/docs/USAGE.md +329 -0
- package/docs/architecture.svg +103 -0
- package/docs/assets/join-group.png +0 -0
- package/docs/assets/wechat-official-account.png +0 -0
- package/package.json +44 -0
- package/skill/SKILL.md +87 -0
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { normalizePayload } from "./payload.js";
|
|
5
|
+
import { appendRunIdBeforeExt, relativePath, safeJoin, slugify } from "./paths.js";
|
|
6
|
+
import { initWorkspace } from "./workspace.js";
|
|
7
|
+
export async function ingestPayload(rootPath, rawPayload) {
|
|
8
|
+
await initWorkspace(rootPath);
|
|
9
|
+
const root = path.resolve(rootPath);
|
|
10
|
+
const runStartedAt = new Date().toISOString();
|
|
11
|
+
const runId = createRunId(runStartedAt);
|
|
12
|
+
const payload = normalizePayload(rawPayload, runStartedAt);
|
|
13
|
+
const runDirName = payload.source.fetch_status === "failed" ? `${runId}-fetch-failed` : runId;
|
|
14
|
+
const runDir = safeJoin(root, "09-runs", runDirName);
|
|
15
|
+
await fs.mkdir(runDir, { recursive: false });
|
|
16
|
+
const generatedFiles = [];
|
|
17
|
+
await writeFile(path.join(runDir, "payload.json"), `${JSON.stringify(payload, null, 2)}\n`, generatedFiles);
|
|
18
|
+
if (payload.source.fetch_status === "failed") {
|
|
19
|
+
await writeSummary(root, runDir, payload, generatedFiles, [
|
|
20
|
+
...payload.warnings,
|
|
21
|
+
"宿主 Agent 未能提供正文,AIWiki CLI 没有自行抓取网页。"
|
|
22
|
+
]);
|
|
23
|
+
return { runId: runDirName, runDir, generatedFiles, warnings: payload.warnings, agentReport: buildAgentReport(root, runDir, payload, generatedFiles) };
|
|
24
|
+
}
|
|
25
|
+
const slug = slugify(payload.source.title ?? payload.source.url);
|
|
26
|
+
const content = payload.source.content ?? "";
|
|
27
|
+
const collisionWarnings = [];
|
|
28
|
+
const longTermTargets = await chooseLongTermTargets(root, slug, runId, collisionWarnings);
|
|
29
|
+
const links = buildArtifactLinks(root, slug, runDirName, runStartedAt, longTermTargets);
|
|
30
|
+
await writeFile(path.join(runDir, "raw.md"), contentFile(payload, content, links), generatedFiles);
|
|
31
|
+
await writeFile(path.join(runDir, "source-card.md"), sourceCard(payload, runDirName, links), generatedFiles);
|
|
32
|
+
await writeFile(path.join(runDir, "creative-assets.md"), creativeAssets(payload, links), generatedFiles);
|
|
33
|
+
await writeFile(path.join(runDir, "topics.md"), topics(payload, links), generatedFiles);
|
|
34
|
+
await writeFile(path.join(runDir, "draft-outline.md"), outline(payload, links), generatedFiles);
|
|
35
|
+
await writeFile(longTermTargets.raw, contentFile(payload, content, links), generatedFiles);
|
|
36
|
+
await writeFile(longTermTargets.sourceCard, sourceCard(payload, runDirName, links), generatedFiles);
|
|
37
|
+
await writeFile(longTermTargets.claims, claims(payload, links), generatedFiles);
|
|
38
|
+
await writeFile(longTermTargets.assets, creativeAssets(payload, links), generatedFiles);
|
|
39
|
+
await writeFile(longTermTargets.topics, topics(payload, links), generatedFiles);
|
|
40
|
+
await writeFile(longTermTargets.outline, outline(payload, links), generatedFiles);
|
|
41
|
+
const warnings = [...payload.warnings, ...collisionWarnings];
|
|
42
|
+
await writeSummary(root, runDir, payload, generatedFiles, warnings, links);
|
|
43
|
+
return { runId, runDir, generatedFiles, warnings, agentReport: buildAgentReport(root, runDir, payload, generatedFiles) };
|
|
44
|
+
}
|
|
45
|
+
export async function ingestFile(rootPath, filePath) {
|
|
46
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
47
|
+
if (!content.trim()) {
|
|
48
|
+
throw new Error("input file is empty");
|
|
49
|
+
}
|
|
50
|
+
return ingestPayload(rootPath, {
|
|
51
|
+
schema_version: "aiwiki.agent_payload.v1",
|
|
52
|
+
source: {
|
|
53
|
+
kind: "file",
|
|
54
|
+
title: path.basename(filePath),
|
|
55
|
+
content_format: "markdown",
|
|
56
|
+
content,
|
|
57
|
+
fetcher: "local-file",
|
|
58
|
+
fetch_status: "ok",
|
|
59
|
+
captured_at: new Date().toISOString()
|
|
60
|
+
},
|
|
61
|
+
request: {
|
|
62
|
+
mode: "ingest",
|
|
63
|
+
outputs: ["source_card", "creative_assets", "topics", "draft_outline", "processing_summary"],
|
|
64
|
+
language: "zh-CN"
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
async function chooseLongTermTargets(root, slug, runId, warnings) {
|
|
69
|
+
return {
|
|
70
|
+
raw: await chooseLongTermTarget(root, "02-raw/articles", `${slug}.md`, runId, warnings),
|
|
71
|
+
sourceCard: await chooseLongTermTarget(root, "03-sources/article-cards", `${slug}.md`, runId, warnings),
|
|
72
|
+
claims: await chooseLongTermTarget(root, "04-claims/_suggestions", `${slug}-claims.md`, runId, warnings),
|
|
73
|
+
assets: await chooseLongTermTarget(root, "06-assets/_suggestions", `${slug}-assets.md`, runId, warnings),
|
|
74
|
+
topics: await chooseLongTermTarget(root, "07-topics/ready", `${slug}-topics.md`, runId, warnings),
|
|
75
|
+
outline: await chooseLongTermTarget(root, "08-outputs/outlines", `${slug}-outline.md`, runId, warnings)
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
async function chooseLongTermTarget(root, dir, fileName, runId, warnings) {
|
|
79
|
+
const target = safeJoin(root, dir, fileName);
|
|
80
|
+
try {
|
|
81
|
+
await fs.access(target);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (error.code === "ENOENT") {
|
|
85
|
+
return target;
|
|
86
|
+
}
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
const renamed = appendRunIdBeforeExt(fileName, runId);
|
|
90
|
+
const renamedTarget = safeJoin(root, dir, renamed);
|
|
91
|
+
warnings.push(`collision renamed: ${relativePath(root, target)} -> ${relativePath(root, renamedTarget)}`);
|
|
92
|
+
return renamedTarget;
|
|
93
|
+
}
|
|
94
|
+
async function writeFile(target, content, generatedFiles) {
|
|
95
|
+
try {
|
|
96
|
+
await fs.writeFile(target, content, { encoding: "utf8", flag: "wx" });
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
if (error.code === "EEXIST") {
|
|
100
|
+
throw new Error(`target file already exists: ${target}`);
|
|
101
|
+
}
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
generatedFiles.push(target);
|
|
105
|
+
}
|
|
106
|
+
async function writeSummary(root, runDir, payload, generatedFiles, warnings, links) {
|
|
107
|
+
const summaryPath = path.join(runDir, "processing-summary.md");
|
|
108
|
+
const files = [...generatedFiles, summaryPath];
|
|
109
|
+
const runId = path.basename(runDir);
|
|
110
|
+
const slug = links?.slug ?? slugify(payload.source.title ?? payload.source.url);
|
|
111
|
+
const createdAt = links?.createdAt ?? new Date().toISOString();
|
|
112
|
+
const lines = [
|
|
113
|
+
"---",
|
|
114
|
+
`aiwiki_id: "${escapeYaml(`${slug}:run:${runId}`)}"`,
|
|
115
|
+
`type: "processing_summary"`,
|
|
116
|
+
`status: "${payload.source.fetch_status === "failed" ? "fetch-failed" : "to-review"}"`,
|
|
117
|
+
`slug: "${escapeYaml(slug)}"`,
|
|
118
|
+
`title: "${escapeYaml(payload.source.title ?? "Untitled")}"`,
|
|
119
|
+
`source_url: "${escapeYaml(payload.source.url ?? "")}"`,
|
|
120
|
+
`source_type: "${escapeYaml(payload.source.kind)}"`,
|
|
121
|
+
`created_at: "${escapeYaml(createdAt)}"`,
|
|
122
|
+
`captured_at: "${escapeYaml(payload.source.captured_at)}"`,
|
|
123
|
+
`run_id: "${escapeYaml(runId)}"`,
|
|
124
|
+
...(links ? relationshipFrontmatter(links) : []),
|
|
125
|
+
`tags: ["aiwiki/run"]`,
|
|
126
|
+
"---",
|
|
127
|
+
"",
|
|
128
|
+
"# 处理记录",
|
|
129
|
+
"",
|
|
130
|
+
`来源类型:${payload.source.kind}`,
|
|
131
|
+
`读取状态:${payload.source.fetch_status}`,
|
|
132
|
+
`宿主读取器:${payload.source.fetcher ?? "unknown"}`,
|
|
133
|
+
"",
|
|
134
|
+
"生成文件:",
|
|
135
|
+
...files.map((file) => `- ${obsidianFileReference(root, file)}`),
|
|
136
|
+
"",
|
|
137
|
+
"告警:",
|
|
138
|
+
...(warnings.length ? warnings.map((warning) => `- ${warning}`) : ["- none"]),
|
|
139
|
+
"",
|
|
140
|
+
"下一步审阅:",
|
|
141
|
+
"- 请在 Obsidian 中人工审阅资料卡、Claim 建议、素材建议、选题和大纲。",
|
|
142
|
+
"- AIWiki CLI 不负责网页抓取稳定性。"
|
|
143
|
+
];
|
|
144
|
+
await fs.writeFile(summaryPath, `${lines.join("\n")}\n`, { encoding: "utf8", flag: "wx" });
|
|
145
|
+
generatedFiles.push(summaryPath);
|
|
146
|
+
}
|
|
147
|
+
function contentFile(payload, content, links) {
|
|
148
|
+
return [
|
|
149
|
+
"---",
|
|
150
|
+
`aiwiki_id: "${escapeYaml(`${links.slug}:raw`)}"`,
|
|
151
|
+
`title: "${escapeYaml(payload.source.title ?? "Untitled")}"`,
|
|
152
|
+
`type: "raw_article"`,
|
|
153
|
+
`status: "to-review"`,
|
|
154
|
+
`slug: "${escapeYaml(links.slug)}"`,
|
|
155
|
+
`source_type: "${escapeYaml(payload.source.kind)}"`,
|
|
156
|
+
`source_url: "${escapeYaml(payload.source.url ?? "")}"`,
|
|
157
|
+
`created_at: "${escapeYaml(links.createdAt)}"`,
|
|
158
|
+
`captured_at: "${escapeYaml(payload.source.captured_at)}"`,
|
|
159
|
+
`run_id: "${escapeYaml(links.runId)}"`,
|
|
160
|
+
...relationshipFrontmatter(links),
|
|
161
|
+
`tags: ["aiwiki/raw"]`,
|
|
162
|
+
"---",
|
|
163
|
+
"",
|
|
164
|
+
`# ${payload.source.title ?? "Untitled"}`,
|
|
165
|
+
"",
|
|
166
|
+
"## AIWiki 链接",
|
|
167
|
+
"",
|
|
168
|
+
`- 资料卡:${obsidianLink(links.sourceCard, "资料卡")}`,
|
|
169
|
+
`- 处理记录:${obsidianLink(links.runSummary, "处理记录")}`,
|
|
170
|
+
"",
|
|
171
|
+
content,
|
|
172
|
+
""
|
|
173
|
+
].join("\n");
|
|
174
|
+
}
|
|
175
|
+
function sourceCard(payload, runId, links) {
|
|
176
|
+
return [
|
|
177
|
+
"---",
|
|
178
|
+
`aiwiki_id: "${escapeYaml(`${links.slug}:source-card`)}"`,
|
|
179
|
+
`title: "${escapeYaml(payload.source.title ?? "Untitled")}"`,
|
|
180
|
+
`type: "source_card"`,
|
|
181
|
+
`status: "to-review"`,
|
|
182
|
+
`slug: "${escapeYaml(links.slug)}"`,
|
|
183
|
+
`source_type: "${escapeYaml(payload.source.kind)}"`,
|
|
184
|
+
`source_url: "${escapeYaml(payload.source.url ?? "")}"`,
|
|
185
|
+
`url: "${escapeYaml(payload.source.url ?? "")}"`,
|
|
186
|
+
`fetcher: "${escapeYaml(payload.source.fetcher ?? "unknown")}"`,
|
|
187
|
+
`created_at: "${escapeYaml(links.createdAt)}"`,
|
|
188
|
+
`captured_at: "${escapeYaml(payload.source.captured_at)}"`,
|
|
189
|
+
`run_id: "${escapeYaml(runId)}"`,
|
|
190
|
+
...relationshipFrontmatter(links),
|
|
191
|
+
`aliases: ["${escapeYaml(payload.source.title ?? "Untitled")}"]`,
|
|
192
|
+
`tags: ["aiwiki/source-card"]`,
|
|
193
|
+
"---",
|
|
194
|
+
"",
|
|
195
|
+
`# ${payload.source.title ?? "Untitled"}`,
|
|
196
|
+
"",
|
|
197
|
+
"## Obsidian 链接",
|
|
198
|
+
"",
|
|
199
|
+
`- 原文:${obsidianLink(links.raw, "原文")}`,
|
|
200
|
+
`- Claim 建议:${obsidianLink(links.claims, "Claim 建议")}`,
|
|
201
|
+
`- 素材建议:${obsidianLink(links.assets, "素材建议")}`,
|
|
202
|
+
`- 选题:${obsidianLink(links.topics, "选题")}`,
|
|
203
|
+
`- 大纲:${obsidianLink(links.outline, "大纲")}`,
|
|
204
|
+
`- 处理记录:${obsidianLink(links.runSummary, "处理记录")}`,
|
|
205
|
+
"",
|
|
206
|
+
"## 摘要",
|
|
207
|
+
"",
|
|
208
|
+
trimPreview(payload.source.content ?? payload.source.fetch_notes ?? ""),
|
|
209
|
+
""
|
|
210
|
+
].join("\n");
|
|
211
|
+
}
|
|
212
|
+
function claims(payload, links) {
|
|
213
|
+
return [
|
|
214
|
+
"---",
|
|
215
|
+
`aiwiki_id: "${escapeYaml(`${links.slug}:claims`)}"`,
|
|
216
|
+
`title: "${escapeYaml(payload.source.title ?? "Untitled")} Claims"`,
|
|
217
|
+
`type: "claim_suggestions"`,
|
|
218
|
+
`status: "to-review"`,
|
|
219
|
+
`slug: "${escapeYaml(links.slug)}"`,
|
|
220
|
+
`source_url: "${escapeYaml(payload.source.url ?? "")}"`,
|
|
221
|
+
`source_type: "${escapeYaml(payload.source.kind)}"`,
|
|
222
|
+
`created_at: "${escapeYaml(links.createdAt)}"`,
|
|
223
|
+
`captured_at: "${escapeYaml(payload.source.captured_at)}"`,
|
|
224
|
+
`run_id: "${escapeYaml(links.runId)}"`,
|
|
225
|
+
...relationshipFrontmatter(links),
|
|
226
|
+
`tags: ["aiwiki/claims"]`,
|
|
227
|
+
"---",
|
|
228
|
+
"",
|
|
229
|
+
"# Claim 建议",
|
|
230
|
+
"",
|
|
231
|
+
`- 资料卡:${obsidianLink(links.sourceCard, "资料卡")}`,
|
|
232
|
+
`- 原文:${obsidianLink(links.raw, "原文")}`,
|
|
233
|
+
`- 待人工审阅:${payload.source.title ?? "Untitled"}`,
|
|
234
|
+
""
|
|
235
|
+
].join("\n");
|
|
236
|
+
}
|
|
237
|
+
function creativeAssets(payload, links) {
|
|
238
|
+
return [
|
|
239
|
+
"---",
|
|
240
|
+
`aiwiki_id: "${escapeYaml(`${links.slug}:assets`)}"`,
|
|
241
|
+
`title: "${escapeYaml(payload.source.title ?? "Untitled")} Assets"`,
|
|
242
|
+
`type: "asset_suggestions"`,
|
|
243
|
+
`status: "to-review"`,
|
|
244
|
+
`slug: "${escapeYaml(links.slug)}"`,
|
|
245
|
+
`source_url: "${escapeYaml(payload.source.url ?? "")}"`,
|
|
246
|
+
`source_type: "${escapeYaml(payload.source.kind)}"`,
|
|
247
|
+
`created_at: "${escapeYaml(links.createdAt)}"`,
|
|
248
|
+
`captured_at: "${escapeYaml(payload.source.captured_at)}"`,
|
|
249
|
+
`run_id: "${escapeYaml(links.runId)}"`,
|
|
250
|
+
...relationshipFrontmatter(links),
|
|
251
|
+
`tags: ["aiwiki/assets"]`,
|
|
252
|
+
"---",
|
|
253
|
+
"",
|
|
254
|
+
"# 素材建议",
|
|
255
|
+
"",
|
|
256
|
+
`- 资料卡:${obsidianLink(links.sourceCard, "资料卡")}`,
|
|
257
|
+
`- 原文:${obsidianLink(links.raw, "原文")}`,
|
|
258
|
+
`- 可复用素材:${payload.source.title ?? "Untitled"}`,
|
|
259
|
+
""
|
|
260
|
+
].join("\n");
|
|
261
|
+
}
|
|
262
|
+
function topics(payload, links) {
|
|
263
|
+
return [
|
|
264
|
+
"---",
|
|
265
|
+
`aiwiki_id: "${escapeYaml(`${links.slug}:topics`)}"`,
|
|
266
|
+
`title: "${escapeYaml(payload.source.title ?? "Untitled")} Topics"`,
|
|
267
|
+
`type: "topic_candidates"`,
|
|
268
|
+
`status: "ready"`,
|
|
269
|
+
`slug: "${escapeYaml(links.slug)}"`,
|
|
270
|
+
`source_url: "${escapeYaml(payload.source.url ?? "")}"`,
|
|
271
|
+
`source_type: "${escapeYaml(payload.source.kind)}"`,
|
|
272
|
+
`created_at: "${escapeYaml(links.createdAt)}"`,
|
|
273
|
+
`captured_at: "${escapeYaml(payload.source.captured_at)}"`,
|
|
274
|
+
`run_id: "${escapeYaml(links.runId)}"`,
|
|
275
|
+
...relationshipFrontmatter(links),
|
|
276
|
+
`tags: ["aiwiki/topics"]`,
|
|
277
|
+
"---",
|
|
278
|
+
"",
|
|
279
|
+
"# 选题候选",
|
|
280
|
+
"",
|
|
281
|
+
`- 资料卡:${obsidianLink(links.sourceCard, "资料卡")}`,
|
|
282
|
+
`- 大纲:${obsidianLink(links.outline, "大纲")}`,
|
|
283
|
+
`- ${payload.source.title ?? "Untitled"}`,
|
|
284
|
+
""
|
|
285
|
+
].join("\n");
|
|
286
|
+
}
|
|
287
|
+
function outline(payload, links) {
|
|
288
|
+
return [
|
|
289
|
+
"---",
|
|
290
|
+
`aiwiki_id: "${escapeYaml(`${links.slug}:outline`)}"`,
|
|
291
|
+
`title: "${escapeYaml(payload.source.title ?? "Untitled")} Outline"`,
|
|
292
|
+
`type: "draft_outline"`,
|
|
293
|
+
`status: "draft"`,
|
|
294
|
+
`slug: "${escapeYaml(links.slug)}"`,
|
|
295
|
+
`source_url: "${escapeYaml(payload.source.url ?? "")}"`,
|
|
296
|
+
`source_type: "${escapeYaml(payload.source.kind)}"`,
|
|
297
|
+
`created_at: "${escapeYaml(links.createdAt)}"`,
|
|
298
|
+
`captured_at: "${escapeYaml(payload.source.captured_at)}"`,
|
|
299
|
+
`run_id: "${escapeYaml(links.runId)}"`,
|
|
300
|
+
...relationshipFrontmatter(links),
|
|
301
|
+
`tags: ["aiwiki/outline"]`,
|
|
302
|
+
"---",
|
|
303
|
+
"",
|
|
304
|
+
"# 草稿大纲",
|
|
305
|
+
"",
|
|
306
|
+
`资料卡:${obsidianLink(links.sourceCard, "资料卡")}`,
|
|
307
|
+
`原文:${obsidianLink(links.raw, "原文")}`,
|
|
308
|
+
"",
|
|
309
|
+
"1. 背景",
|
|
310
|
+
"2. 关键观点",
|
|
311
|
+
"3. 可复用方法",
|
|
312
|
+
`4. 来源:${payload.source.title ?? "Untitled"}`,
|
|
313
|
+
""
|
|
314
|
+
].join("\n");
|
|
315
|
+
}
|
|
316
|
+
function trimPreview(value) {
|
|
317
|
+
return value.trim().slice(0, 500) || "待人工补充。";
|
|
318
|
+
}
|
|
319
|
+
function buildAgentReport(root, runDir, payload, generatedFiles) {
|
|
320
|
+
const content = payload.source.content ?? "";
|
|
321
|
+
const fetchFailed = payload.source.fetch_status === "failed";
|
|
322
|
+
const fitScore = fetchFailed ? 0 : estimateFitScore(payload, content);
|
|
323
|
+
return {
|
|
324
|
+
ingested: !fetchFailed,
|
|
325
|
+
recorded: true,
|
|
326
|
+
fetchStatus: payload.source.fetch_status,
|
|
327
|
+
sourceTitle: payload.source.title ?? "Untitled",
|
|
328
|
+
sourceUrl: payload.source.url,
|
|
329
|
+
fitScore,
|
|
330
|
+
fitLevel: fitLevel(fitScore, fetchFailed),
|
|
331
|
+
summary: fetchFailed ? (payload.source.fetch_notes ?? "宿主 Agent 没有提供可读正文。") : summarizeContent(content),
|
|
332
|
+
keyFiles: {
|
|
333
|
+
processingSummary: relativePath(root, path.join(runDir, "processing-summary.md")),
|
|
334
|
+
sourceCard: findGeneratedFileInDir(root, generatedFiles, "03-sources/article-cards"),
|
|
335
|
+
draftOutline: findGeneratedFile(root, generatedFiles, "draft-outline.md"),
|
|
336
|
+
dashboard: "dashboards/AIWiki Home.md",
|
|
337
|
+
reviewQueue: "dashboards/Review Queue.md"
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
function estimateFitScore(payload, content) {
|
|
342
|
+
let score = 45;
|
|
343
|
+
if (payload.source.kind === "url" || payload.source.kind === "file") {
|
|
344
|
+
score += 10;
|
|
345
|
+
}
|
|
346
|
+
if ((payload.source.title ?? "").trim()) {
|
|
347
|
+
score += 10;
|
|
348
|
+
}
|
|
349
|
+
if (content.trim().length >= 500) {
|
|
350
|
+
score += 20;
|
|
351
|
+
}
|
|
352
|
+
else if (content.trim().length >= 150) {
|
|
353
|
+
score += 10;
|
|
354
|
+
}
|
|
355
|
+
if (payload.source.url) {
|
|
356
|
+
score += 5;
|
|
357
|
+
}
|
|
358
|
+
return Math.min(95, score);
|
|
359
|
+
}
|
|
360
|
+
function fitLevel(score, fetchFailed) {
|
|
361
|
+
if (fetchFailed) {
|
|
362
|
+
return "fetch_failed";
|
|
363
|
+
}
|
|
364
|
+
if (score >= 80) {
|
|
365
|
+
return "high";
|
|
366
|
+
}
|
|
367
|
+
if (score >= 60) {
|
|
368
|
+
return "medium";
|
|
369
|
+
}
|
|
370
|
+
return "low";
|
|
371
|
+
}
|
|
372
|
+
function summarizeContent(content) {
|
|
373
|
+
const compact = content.replace(/\s+/g, " ").trim();
|
|
374
|
+
if (!compact) {
|
|
375
|
+
return "没有提供可读正文。";
|
|
376
|
+
}
|
|
377
|
+
return compact.length > 180 ? `${compact.slice(0, 180)}...` : compact;
|
|
378
|
+
}
|
|
379
|
+
function findGeneratedFile(root, files, basename) {
|
|
380
|
+
const match = files.find((file) => path.basename(file) === basename);
|
|
381
|
+
return match ? relativePath(root, match) : undefined;
|
|
382
|
+
}
|
|
383
|
+
function findGeneratedFileInDir(root, files, dir) {
|
|
384
|
+
const match = files.find((file) => relativePath(root, file).startsWith(`${dir}/`));
|
|
385
|
+
return match ? relativePath(root, match) : undefined;
|
|
386
|
+
}
|
|
387
|
+
function buildArtifactLinks(root, slug, runDirName, createdAt, longTermTargets) {
|
|
388
|
+
return {
|
|
389
|
+
slug,
|
|
390
|
+
runId: runDirName,
|
|
391
|
+
createdAt,
|
|
392
|
+
raw: relativePath(root, longTermTargets.raw),
|
|
393
|
+
sourceCard: relativePath(root, longTermTargets.sourceCard),
|
|
394
|
+
claims: relativePath(root, longTermTargets.claims),
|
|
395
|
+
assets: relativePath(root, longTermTargets.assets),
|
|
396
|
+
topics: relativePath(root, longTermTargets.topics),
|
|
397
|
+
outline: relativePath(root, longTermTargets.outline),
|
|
398
|
+
runSummary: `09-runs/${runDirName}/processing-summary.md`
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
function relationshipFrontmatter(links) {
|
|
402
|
+
return [
|
|
403
|
+
`source_card: "${escapeYaml(obsidianLink(links.sourceCard, "资料卡"))}"`,
|
|
404
|
+
`raw_note: "${escapeYaml(obsidianLink(links.raw, "原文"))}"`,
|
|
405
|
+
`claims_note: "${escapeYaml(obsidianLink(links.claims, "Claim 建议"))}"`,
|
|
406
|
+
`assets_note: "${escapeYaml(obsidianLink(links.assets, "素材建议"))}"`,
|
|
407
|
+
`topics_note: "${escapeYaml(obsidianLink(links.topics, "选题"))}"`,
|
|
408
|
+
`outline_note: "${escapeYaml(obsidianLink(links.outline, "大纲"))}"`,
|
|
409
|
+
`run_summary: "${escapeYaml(obsidianLink(links.runSummary, "处理记录"))}"`
|
|
410
|
+
];
|
|
411
|
+
}
|
|
412
|
+
function obsidianFileReference(root, file) {
|
|
413
|
+
const vaultPath = relativePath(root, file);
|
|
414
|
+
if (!vaultPath.toLowerCase().endsWith(".md")) {
|
|
415
|
+
return vaultPath;
|
|
416
|
+
}
|
|
417
|
+
return `${obsidianLink(vaultPath, path.basename(vaultPath, ".md"))} (${vaultPath})`;
|
|
418
|
+
}
|
|
419
|
+
function obsidianLink(vaultPath, label) {
|
|
420
|
+
return `[[${vaultPath.replace(/\\/g, "/").replace(/\.md$/i, "")}|${label}]]`;
|
|
421
|
+
}
|
|
422
|
+
function escapeYaml(value) {
|
|
423
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
424
|
+
}
|
|
425
|
+
function createRunId(now) {
|
|
426
|
+
const stamp = now.replace(/[-:.]/g, "").replace("T", "-").replace("Z", "");
|
|
427
|
+
return `${stamp}-${randomBytes(3).toString("hex")}`;
|
|
428
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
export function safeJoin(root, ...parts) {
|
|
3
|
+
const resolvedRoot = path.resolve(root);
|
|
4
|
+
const target = path.resolve(resolvedRoot, ...parts);
|
|
5
|
+
const relative = path.relative(resolvedRoot, target);
|
|
6
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
7
|
+
throw new Error(`Refusing to write outside workspace: ${target}`);
|
|
8
|
+
}
|
|
9
|
+
return target;
|
|
10
|
+
}
|
|
11
|
+
export function toPosixPath(filePath) {
|
|
12
|
+
return filePath.split(path.sep).join("/");
|
|
13
|
+
}
|
|
14
|
+
export function relativePath(root, target) {
|
|
15
|
+
return toPosixPath(path.relative(root, target));
|
|
16
|
+
}
|
|
17
|
+
export function slugify(value) {
|
|
18
|
+
const source = value?.trim() || "item";
|
|
19
|
+
const ascii = source
|
|
20
|
+
.normalize("NFKD")
|
|
21
|
+
.replace(/[^\w\s-]/g, "")
|
|
22
|
+
.trim()
|
|
23
|
+
.toLowerCase()
|
|
24
|
+
.replace(/[-\s_]+/g, "-")
|
|
25
|
+
.replace(/^-+|-+$/g, "");
|
|
26
|
+
return ascii || "item";
|
|
27
|
+
}
|
|
28
|
+
export function appendRunIdBeforeExt(fileName, runId) {
|
|
29
|
+
const ext = path.extname(fileName);
|
|
30
|
+
const base = fileName.slice(0, fileName.length - ext.length);
|
|
31
|
+
return `${base}-${runId}${ext}`;
|
|
32
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export function normalizePayload(raw, runStartedAt) {
|
|
2
|
+
if (!isRecord(raw)) {
|
|
3
|
+
throw new Error("payload must be a JSON object");
|
|
4
|
+
}
|
|
5
|
+
if (raw.schema_version !== "aiwiki.agent_payload.v1") {
|
|
6
|
+
throw new Error("schema_version must be aiwiki.agent_payload.v1");
|
|
7
|
+
}
|
|
8
|
+
rejectWriteControlFields(raw);
|
|
9
|
+
const sourceRaw = raw.source;
|
|
10
|
+
if (!isRecord(sourceRaw)) {
|
|
11
|
+
throw new Error("source is required");
|
|
12
|
+
}
|
|
13
|
+
const legacyContent = isRecord(raw.content) ? stringValue(raw.content.text) : undefined;
|
|
14
|
+
const legacyMetadata = isRecord(raw.metadata) ? raw.metadata : undefined;
|
|
15
|
+
const warnings = [];
|
|
16
|
+
const content = stringValue(sourceRaw.content) ?? legacyContent;
|
|
17
|
+
const fetcher = stringValue(sourceRaw.fetcher) ?? (legacyMetadata ? stringValue(legacyMetadata.fetcher) : undefined);
|
|
18
|
+
const legacyCapturedAt = legacyMetadata ? stringValue(legacyMetadata.captured_at) : undefined;
|
|
19
|
+
const capturedAt = stringValue(sourceRaw.captured_at) ?? legacyCapturedAt ?? runStartedAt;
|
|
20
|
+
if (!stringValue(sourceRaw.captured_at) && legacyCapturedAt) {
|
|
21
|
+
warnings.push("metadata.captured_at 已规范化为 source.captured_at。");
|
|
22
|
+
}
|
|
23
|
+
if (!stringValue(sourceRaw.captured_at) && !legacyCapturedAt) {
|
|
24
|
+
warnings.push("缺少 captured_at,已使用 run_started_at 补齐。");
|
|
25
|
+
}
|
|
26
|
+
const fetchStatus = normalizeFetchStatus(stringValue(sourceRaw.fetch_status), content);
|
|
27
|
+
if (fetchStatus !== "failed" && !content?.trim()) {
|
|
28
|
+
throw new Error("source.content is required when source.fetch_status is ok");
|
|
29
|
+
}
|
|
30
|
+
const kind = stringValue(sourceRaw.kind);
|
|
31
|
+
if (!kind) {
|
|
32
|
+
throw new Error("source.kind is required");
|
|
33
|
+
}
|
|
34
|
+
const requestRaw = isRecord(raw.request) ? raw.request : {};
|
|
35
|
+
const requestedOutputs = Array.isArray(requestRaw.outputs)
|
|
36
|
+
? requestRaw.outputs.filter((item) => typeof item === "string")
|
|
37
|
+
: ["source_card", "creative_assets", "topics", "draft_outline", "processing_summary"];
|
|
38
|
+
const outputs = ["source_card", "creative_assets", "topics", "draft_outline", "processing_summary"];
|
|
39
|
+
if (fetchStatus !== "failed" && requestedOutputs.length && requestedOutputs.length !== outputs.length) {
|
|
40
|
+
warnings.push("基础版会生成完整单条资料产物,request.outputs 已按全量输出处理。");
|
|
41
|
+
}
|
|
42
|
+
if (typeof raw.target_kb === "string" && raw.target_kb.trim()) {
|
|
43
|
+
warnings.push(`target_kb=${raw.target_kb} 已被单知识库流程忽略。`);
|
|
44
|
+
}
|
|
45
|
+
if (fetchStatus === "failed" && content?.trim()) {
|
|
46
|
+
throw new Error("source.content must be empty when source.fetch_status is failed");
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
schema_version: "aiwiki.agent_payload.v1",
|
|
50
|
+
target_kb: stringValue(raw.target_kb),
|
|
51
|
+
source: {
|
|
52
|
+
kind,
|
|
53
|
+
url: stringValue(sourceRaw.url),
|
|
54
|
+
title: stringValue(sourceRaw.title),
|
|
55
|
+
author: stringValue(sourceRaw.author),
|
|
56
|
+
platform: stringValue(sourceRaw.platform),
|
|
57
|
+
content_format: stringValue(sourceRaw.content_format),
|
|
58
|
+
content,
|
|
59
|
+
fetcher,
|
|
60
|
+
fetch_status: fetchStatus,
|
|
61
|
+
fetch_notes: stringValue(sourceRaw.fetch_notes),
|
|
62
|
+
captured_at: capturedAt,
|
|
63
|
+
language: stringValue(sourceRaw.language) ?? (legacyMetadata ? stringValue(legacyMetadata.language) : undefined)
|
|
64
|
+
},
|
|
65
|
+
request: {
|
|
66
|
+
mode: stringValue(requestRaw.mode) ?? (fetchStatus === "failed" ? "record_fetch_failure" : "ingest"),
|
|
67
|
+
outputs,
|
|
68
|
+
language: stringValue(requestRaw.language) ?? stringValue(sourceRaw.language)
|
|
69
|
+
},
|
|
70
|
+
warnings
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function normalizeFetchStatus(value, content) {
|
|
74
|
+
if (value === "failed") {
|
|
75
|
+
return "failed";
|
|
76
|
+
}
|
|
77
|
+
if (value === "ok") {
|
|
78
|
+
return "ok";
|
|
79
|
+
}
|
|
80
|
+
return content ? "ok" : "failed";
|
|
81
|
+
}
|
|
82
|
+
function rejectWriteControlFields(raw) {
|
|
83
|
+
if (isRecord(raw.output) && (typeof raw.output.path === "string" || typeof raw.output.dir === "string")) {
|
|
84
|
+
throw new Error("payload must not control output paths");
|
|
85
|
+
}
|
|
86
|
+
if (typeof raw.output_file === "string") {
|
|
87
|
+
throw new Error("payload must not control output paths");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function stringValue(value) {
|
|
91
|
+
return typeof value === "string" ? value : undefined;
|
|
92
|
+
}
|
|
93
|
+
function isRecord(value) {
|
|
94
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
95
|
+
}
|