@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,495 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createInterface } from "node:readline/promises";
|
|
5
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
6
|
+
import { CliError } from "./output.js";
|
|
7
|
+
export const CONFIG_FILE = "aiwiki.yaml";
|
|
8
|
+
export const REQUIRED_DIRS = [
|
|
9
|
+
"02-raw/articles",
|
|
10
|
+
"03-sources/article-cards",
|
|
11
|
+
"04-claims/_suggestions",
|
|
12
|
+
"05-wiki",
|
|
13
|
+
"06-assets/_suggestions",
|
|
14
|
+
"07-topics/ready",
|
|
15
|
+
"08-outputs/outlines",
|
|
16
|
+
"09-runs",
|
|
17
|
+
"dashboards",
|
|
18
|
+
"_system/templates",
|
|
19
|
+
"_system/schemas",
|
|
20
|
+
"_system/logs"
|
|
21
|
+
];
|
|
22
|
+
const WORKSPACE_SEEDS = [
|
|
23
|
+
{
|
|
24
|
+
path: "dashboards/AIWiki Home.md",
|
|
25
|
+
content: `# AIWiki 首页
|
|
26
|
+
|
|
27
|
+
AIWiki 的 Obsidian 入口。Dataview 是可选增强;未安装时仍可使用下方普通链接、Properties、Backlinks 和 Graph View。
|
|
28
|
+
|
|
29
|
+
## 原生链接入口
|
|
30
|
+
|
|
31
|
+
- [[dashboards/Review Queue|待审队列]]
|
|
32
|
+
- [[dashboards/Recent Runs|最近处理]]
|
|
33
|
+
- [[dashboards/Topic Pipeline|选题管线]]
|
|
34
|
+
- [[_system/schemas/aiwiki-frontmatter|字段说明]]
|
|
35
|
+
|
|
36
|
+
## 最近收录
|
|
37
|
+
|
|
38
|
+
\`\`\`dataview
|
|
39
|
+
TABLE status, source_url, captured_at, run_summary
|
|
40
|
+
FROM "03-sources/article-cards"
|
|
41
|
+
WHERE type = "source_card"
|
|
42
|
+
SORT captured_at DESC
|
|
43
|
+
\`\`\`
|
|
44
|
+
|
|
45
|
+
## 待审队列
|
|
46
|
+
|
|
47
|
+
\`\`\`dataview
|
|
48
|
+
TABLE type, status, source_card, raw_note, run_summary
|
|
49
|
+
FROM "03-sources/article-cards" or "04-claims/_suggestions" or "06-assets/_suggestions" or "08-outputs/outlines"
|
|
50
|
+
WHERE status = "to-review"
|
|
51
|
+
SORT created_at DESC
|
|
52
|
+
\`\`\`
|
|
53
|
+
`
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
path: "dashboards/Review Queue.md",
|
|
57
|
+
content: `# 待审队列
|
|
58
|
+
|
|
59
|
+
未安装 Dataview 时,可直接打开 [[03-sources/article-cards]]、[[04-claims/_suggestions]]、[[06-assets/_suggestions]] 和 [[08-outputs/outlines]] 手工审阅。
|
|
60
|
+
|
|
61
|
+
## 待审内容
|
|
62
|
+
|
|
63
|
+
\`\`\`dataview
|
|
64
|
+
TABLE type, source_url, source_card, raw_note, claims_note, assets_note, outline_note
|
|
65
|
+
FROM "03-sources/article-cards" or "04-claims/_suggestions" or "06-assets/_suggestions" or "08-outputs/outlines"
|
|
66
|
+
WHERE status = "to-review"
|
|
67
|
+
SORT captured_at DESC
|
|
68
|
+
\`\`\`
|
|
69
|
+
`
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
path: "dashboards/Recent Runs.md",
|
|
73
|
+
content: `# 最近处理
|
|
74
|
+
|
|
75
|
+
处理记录用于追溯每次宿主 Agent 入库的 payload、产物和告警。
|
|
76
|
+
|
|
77
|
+
\`\`\`dataview
|
|
78
|
+
TABLE status, source_url, source_card, raw_note, created_at
|
|
79
|
+
FROM "09-runs"
|
|
80
|
+
WHERE type = "processing_summary"
|
|
81
|
+
SORT created_at DESC
|
|
82
|
+
\`\`\`
|
|
83
|
+
`
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
path: "dashboards/Topic Pipeline.md",
|
|
87
|
+
content: `# 选题管线
|
|
88
|
+
|
|
89
|
+
选题和大纲是从资料卡继续写作的入口。
|
|
90
|
+
|
|
91
|
+
\`\`\`dataview
|
|
92
|
+
TABLE status, source_card, outline_note, source_url, created_at
|
|
93
|
+
FROM "07-topics/ready" or "08-outputs/outlines"
|
|
94
|
+
WHERE type = "topic_candidates" or type = "draft_outline"
|
|
95
|
+
SORT created_at DESC
|
|
96
|
+
\`\`\`
|
|
97
|
+
`
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
path: "_system/schemas/aiwiki-frontmatter.md",
|
|
101
|
+
content: `# AIWiki Frontmatter Schema
|
|
102
|
+
|
|
103
|
+
AIWiki 使用 Obsidian 原生 Properties 作为基础数据库层,Dataview 只作为可选增强。
|
|
104
|
+
|
|
105
|
+
## Shared Fields
|
|
106
|
+
|
|
107
|
+
- \`aiwiki_id\`: AIWiki 内部稳定标识。
|
|
108
|
+
- \`type\`: \`source_card\`, \`raw_article\`, \`claim_suggestions\`, \`asset_suggestions\`, \`topic_candidates\`, \`draft_outline\`, \`processing_summary\`。
|
|
109
|
+
- \`status\`: \`to-review\`, \`ready\`, \`draft\`, \`reviewed\`, \`archived\`, \`fetch-failed\`。
|
|
110
|
+
- \`slug\`: 来源标题或 URL 生成的 slug。
|
|
111
|
+
- \`source_url\`: 原始 URL,若没有则为空。
|
|
112
|
+
- \`source_type\`: \`url\`, \`file\`, \`text\` 等来源类型。
|
|
113
|
+
- \`created_at\`: AIWiki 写入时间。
|
|
114
|
+
- \`captured_at\`: 宿主 Agent 读取来源的时间。
|
|
115
|
+
- \`run_id\`: 本次处理记录目录名。
|
|
116
|
+
- \`source_card\`, \`raw_note\`, \`claims_note\`, \`assets_note\`, \`topics_note\`, \`outline_note\`, \`run_summary\`: Obsidian 内部链接字符串。
|
|
117
|
+
- \`tags\`: AIWiki 类型标签。
|
|
118
|
+
|
|
119
|
+
## Rule
|
|
120
|
+
|
|
121
|
+
正文中的 wikilink 用于人工阅读;frontmatter 字段是 Dataview 查询和数据库筛选的来源。
|
|
122
|
+
`
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
path: "_system/templates/source-card.md",
|
|
126
|
+
content: `---
|
|
127
|
+
aiwiki_id: ""
|
|
128
|
+
type: "source_card"
|
|
129
|
+
status: "to-review"
|
|
130
|
+
slug: ""
|
|
131
|
+
title: ""
|
|
132
|
+
source_url: ""
|
|
133
|
+
source_type: ""
|
|
134
|
+
created_at: ""
|
|
135
|
+
captured_at: ""
|
|
136
|
+
run_id: ""
|
|
137
|
+
source_card: ""
|
|
138
|
+
raw_note: ""
|
|
139
|
+
claims_note: ""
|
|
140
|
+
assets_note: ""
|
|
141
|
+
topics_note: ""
|
|
142
|
+
outline_note: ""
|
|
143
|
+
run_summary: ""
|
|
144
|
+
tags: ["aiwiki/source-card"]
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
# 资料卡
|
|
148
|
+
|
|
149
|
+
## Obsidian 链接
|
|
150
|
+
|
|
151
|
+
- 原文:
|
|
152
|
+
- Claim 建议:
|
|
153
|
+
- 素材建议:
|
|
154
|
+
- 选题:
|
|
155
|
+
- 大纲:
|
|
156
|
+
- 处理记录:
|
|
157
|
+
|
|
158
|
+
## 摘要
|
|
159
|
+
|
|
160
|
+
`
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
path: "_system/templates/review-note.md",
|
|
164
|
+
content: `---
|
|
165
|
+
type: "review_note"
|
|
166
|
+
status: "draft"
|
|
167
|
+
source_card: ""
|
|
168
|
+
created_at: ""
|
|
169
|
+
tags: ["aiwiki/review"]
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
# 审阅记录
|
|
173
|
+
|
|
174
|
+
## 判断
|
|
175
|
+
|
|
176
|
+
## 可复用结论
|
|
177
|
+
|
|
178
|
+
## 后续动作
|
|
179
|
+
|
|
180
|
+
`
|
|
181
|
+
}
|
|
182
|
+
];
|
|
183
|
+
export function resolveRoot(rootPath) {
|
|
184
|
+
return path.resolve(rootPath);
|
|
185
|
+
}
|
|
186
|
+
export function defaultConfig(createdAt = new Date().toISOString()) {
|
|
187
|
+
return [
|
|
188
|
+
"product: aiwiki",
|
|
189
|
+
"schema_version: 1",
|
|
190
|
+
`created_at: "${createdAt}"`,
|
|
191
|
+
"",
|
|
192
|
+
"knowledge_base:",
|
|
193
|
+
" id: default",
|
|
194
|
+
" name: AIWiki",
|
|
195
|
+
" language: zh-CN",
|
|
196
|
+
"",
|
|
197
|
+
"agent:",
|
|
198
|
+
" url_first: true",
|
|
199
|
+
" fetch_owner: host_agent",
|
|
200
|
+
" cli_fetch_webpage: false",
|
|
201
|
+
"",
|
|
202
|
+
"review:",
|
|
203
|
+
" wiki_merge_policy: manual",
|
|
204
|
+
" claim_policy: suggest_only",
|
|
205
|
+
" asset_policy: suggest_only",
|
|
206
|
+
""
|
|
207
|
+
].join("\n");
|
|
208
|
+
}
|
|
209
|
+
export function defaultSetupPath() {
|
|
210
|
+
return path.join(os.homedir(), "AIWiki");
|
|
211
|
+
}
|
|
212
|
+
export function userConfigPath() {
|
|
213
|
+
const home = process.env.AIWIKI_HOME ? path.resolve(process.env.AIWIKI_HOME) : path.join(os.homedir(), ".aiwiki");
|
|
214
|
+
return path.join(home, "config.json");
|
|
215
|
+
}
|
|
216
|
+
export async function setDefaultWorkspace(rootPath) {
|
|
217
|
+
const root = resolveRoot(rootPath);
|
|
218
|
+
const configPath = userConfigPath();
|
|
219
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
220
|
+
const config = {
|
|
221
|
+
defaultPath: root,
|
|
222
|
+
updatedAt: new Date().toISOString()
|
|
223
|
+
};
|
|
224
|
+
await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
225
|
+
return { configPath, defaultPath: root };
|
|
226
|
+
}
|
|
227
|
+
export async function readUserConfig() {
|
|
228
|
+
const configPath = userConfigPath();
|
|
229
|
+
if (!(await exists(configPath))) {
|
|
230
|
+
return {};
|
|
231
|
+
}
|
|
232
|
+
return JSON.parse(await fs.readFile(configPath, "utf8"));
|
|
233
|
+
}
|
|
234
|
+
export async function initWorkspace(rootPath) {
|
|
235
|
+
const root = resolveRoot(rootPath);
|
|
236
|
+
await fs.mkdir(root, { recursive: true });
|
|
237
|
+
const createdDirs = [];
|
|
238
|
+
for (const dir of REQUIRED_DIRS) {
|
|
239
|
+
const absolute = path.join(root, dir);
|
|
240
|
+
const existed = await exists(absolute);
|
|
241
|
+
await fs.mkdir(absolute, { recursive: true });
|
|
242
|
+
if (!existed) {
|
|
243
|
+
createdDirs.push(dir);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const configPath = path.join(root, CONFIG_FILE);
|
|
247
|
+
const createdConfig = !(await exists(configPath));
|
|
248
|
+
if (createdConfig) {
|
|
249
|
+
await fs.writeFile(configPath, defaultConfig(), "utf8");
|
|
250
|
+
}
|
|
251
|
+
const seededFiles = await seedWorkspaceFiles(root);
|
|
252
|
+
return { root, createdConfig, createdDirs, seededFiles };
|
|
253
|
+
}
|
|
254
|
+
async function seedWorkspaceFiles(root) {
|
|
255
|
+
const files = [];
|
|
256
|
+
for (const seed of WORKSPACE_SEEDS) {
|
|
257
|
+
const target = path.join(root, seed.path);
|
|
258
|
+
const alreadyExists = await exists(target);
|
|
259
|
+
if (!alreadyExists) {
|
|
260
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
261
|
+
await fs.writeFile(target, seed.content, "utf8");
|
|
262
|
+
}
|
|
263
|
+
files.push({ path: seed.path, created: !alreadyExists });
|
|
264
|
+
}
|
|
265
|
+
return files;
|
|
266
|
+
}
|
|
267
|
+
export async function resolveWorkspace(optionalPath, startDir = process.cwd()) {
|
|
268
|
+
if (optionalPath) {
|
|
269
|
+
const root = resolveRoot(optionalPath);
|
|
270
|
+
if (!(await exists(path.join(root, CONFIG_FILE)))) {
|
|
271
|
+
throw new CliError(`未找到配置文件:${path.join(root, CONFIG_FILE)}。请先运行 aiwiki init --path "${root}" --yes。`);
|
|
272
|
+
}
|
|
273
|
+
return root;
|
|
274
|
+
}
|
|
275
|
+
const found = await findWorkspace(startDir);
|
|
276
|
+
if (found) {
|
|
277
|
+
return found;
|
|
278
|
+
}
|
|
279
|
+
const userConfig = await readUserConfig();
|
|
280
|
+
if (userConfig.defaultPath) {
|
|
281
|
+
const root = resolveRoot(userConfig.defaultPath);
|
|
282
|
+
if (await exists(path.join(root, CONFIG_FILE))) {
|
|
283
|
+
return root;
|
|
284
|
+
}
|
|
285
|
+
throw new CliError(`默认知识库不可用:${root}。请运行 aiwiki setup --path <知识库路径> --yes 重新设置。`);
|
|
286
|
+
}
|
|
287
|
+
throw new CliError("未找到 AIWiki 知识库。请先运行 aiwiki setup,或运行 aiwiki setup --path <知识库路径> --yes。");
|
|
288
|
+
}
|
|
289
|
+
export async function findWorkspace(startDir) {
|
|
290
|
+
let current = path.resolve(startDir);
|
|
291
|
+
while (true) {
|
|
292
|
+
if (await exists(path.join(current, CONFIG_FILE))) {
|
|
293
|
+
return current;
|
|
294
|
+
}
|
|
295
|
+
const parent = path.dirname(current);
|
|
296
|
+
if (parent === current) {
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
current = parent;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
export async function promptForInitPath() {
|
|
303
|
+
const rl = createInterface({ input, output });
|
|
304
|
+
try {
|
|
305
|
+
const answer = await rl.question("AIWiki 知识库路径: ");
|
|
306
|
+
if (!answer.trim()) {
|
|
307
|
+
throw new CliError("路径不能为空。");
|
|
308
|
+
}
|
|
309
|
+
return answer.trim();
|
|
310
|
+
}
|
|
311
|
+
finally {
|
|
312
|
+
rl.close();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
export async function promptForSetup(options) {
|
|
316
|
+
if (options.rootPath && options.yes) {
|
|
317
|
+
return { rootPath: options.rootPath, confirmed: true };
|
|
318
|
+
}
|
|
319
|
+
if (!input.isTTY) {
|
|
320
|
+
return promptForSetupFromPipe(options);
|
|
321
|
+
}
|
|
322
|
+
const rl = createInterface({ input, output });
|
|
323
|
+
try {
|
|
324
|
+
let rootPath = options.rootPath;
|
|
325
|
+
if (!rootPath) {
|
|
326
|
+
const defaultPath = defaultSetupPath();
|
|
327
|
+
const answer = await rl.question(`AIWiki 知识库路径(直接回车使用 ${defaultPath}): `);
|
|
328
|
+
rootPath = answer.trim() || defaultPath;
|
|
329
|
+
}
|
|
330
|
+
if (!options.yes) {
|
|
331
|
+
const root = resolveRoot(rootPath);
|
|
332
|
+
output.write(`将创建或补齐 AIWiki 目录: ${root}\n`);
|
|
333
|
+
for (const dir of REQUIRED_DIRS) {
|
|
334
|
+
output.write(` - ${dir}\n`);
|
|
335
|
+
}
|
|
336
|
+
const answer = await rl.question("确认创建?输入 y 继续: ");
|
|
337
|
+
if (answer.trim().toLowerCase() !== "y") {
|
|
338
|
+
return { rootPath, confirmed: false };
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return { rootPath, confirmed: true };
|
|
342
|
+
}
|
|
343
|
+
finally {
|
|
344
|
+
rl.close();
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
async function promptForSetupFromPipe(options) {
|
|
348
|
+
const lines = await readInputLines();
|
|
349
|
+
const hasExplicitInput = lines.some((line) => line.trim().length > 0);
|
|
350
|
+
if (!options.yes && !hasExplicitInput) {
|
|
351
|
+
throw new CliError("Interactive setup requires a terminal. For scripts, run aiwiki setup --path <path> --yes.");
|
|
352
|
+
}
|
|
353
|
+
let lineIndex = 0;
|
|
354
|
+
let rootPath = options.rootPath;
|
|
355
|
+
if (!rootPath) {
|
|
356
|
+
const defaultPath = defaultSetupPath();
|
|
357
|
+
output.write(`AIWiki 知识库路径(直接回车使用 ${defaultPath}): `);
|
|
358
|
+
rootPath = lines[lineIndex]?.trim() || defaultPath;
|
|
359
|
+
lineIndex += 1;
|
|
360
|
+
}
|
|
361
|
+
if (!options.yes) {
|
|
362
|
+
const root = resolveRoot(rootPath);
|
|
363
|
+
output.write(`将创建或补齐 AIWiki 目录: ${root}\n`);
|
|
364
|
+
for (const dir of REQUIRED_DIRS) {
|
|
365
|
+
output.write(` - ${dir}\n`);
|
|
366
|
+
}
|
|
367
|
+
output.write("确认创建?输入 y 继续: ");
|
|
368
|
+
return { rootPath, confirmed: lines[lineIndex]?.trim().toLowerCase() === "y" };
|
|
369
|
+
}
|
|
370
|
+
return { rootPath, confirmed: true };
|
|
371
|
+
}
|
|
372
|
+
async function readInputLines() {
|
|
373
|
+
const chunks = [];
|
|
374
|
+
for await (const chunk of input) {
|
|
375
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
376
|
+
}
|
|
377
|
+
return Buffer.concat(chunks).toString("utf8").split(/\r?\n/);
|
|
378
|
+
}
|
|
379
|
+
export async function confirmInit(rootPath) {
|
|
380
|
+
const root = resolveRoot(rootPath);
|
|
381
|
+
const rl = createInterface({ input, output });
|
|
382
|
+
try {
|
|
383
|
+
output.write(`将创建或补齐 AIWiki 目录: ${root}\n`);
|
|
384
|
+
for (const dir of REQUIRED_DIRS) {
|
|
385
|
+
output.write(` - ${dir}\n`);
|
|
386
|
+
}
|
|
387
|
+
const answer = await rl.question("确认创建?输入 y 继续: ");
|
|
388
|
+
return answer.trim().toLowerCase() === "y";
|
|
389
|
+
}
|
|
390
|
+
finally {
|
|
391
|
+
rl.close();
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
export async function readConfig(rootPath) {
|
|
395
|
+
const root = resolveRoot(rootPath);
|
|
396
|
+
const configPath = path.join(root, CONFIG_FILE);
|
|
397
|
+
if (!(await exists(configPath))) {
|
|
398
|
+
throw new CliError(`未找到配置文件:${configPath}`);
|
|
399
|
+
}
|
|
400
|
+
const text = await fs.readFile(configPath, "utf8");
|
|
401
|
+
return {
|
|
402
|
+
product: readScalar(text, "product") ?? "unknown",
|
|
403
|
+
schemaVersion: readScalar(text, "schema_version") ?? "unknown",
|
|
404
|
+
createdAt: unquote(readScalar(text, "created_at") ?? "unknown")
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
export async function directorySummary(rootPath) {
|
|
408
|
+
const root = resolveRoot(rootPath);
|
|
409
|
+
const missing = [];
|
|
410
|
+
for (const dir of REQUIRED_DIRS) {
|
|
411
|
+
if (!(await exists(path.join(root, dir)))) {
|
|
412
|
+
missing.push(dir);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return { present: REQUIRED_DIRS.length - missing.length, missing };
|
|
416
|
+
}
|
|
417
|
+
export async function doctor(rootPath) {
|
|
418
|
+
const root = resolveRoot(rootPath);
|
|
419
|
+
const checks = [];
|
|
420
|
+
const configPath = path.join(root, CONFIG_FILE);
|
|
421
|
+
checks.push({
|
|
422
|
+
name: CONFIG_FILE,
|
|
423
|
+
status: (await exists(configPath)) ? "ok" : "missing",
|
|
424
|
+
detail: configPath
|
|
425
|
+
});
|
|
426
|
+
for (const dir of REQUIRED_DIRS) {
|
|
427
|
+
const absolute = path.join(root, dir);
|
|
428
|
+
checks.push({
|
|
429
|
+
name: dir,
|
|
430
|
+
status: (await exists(absolute)) ? "ok" : "missing",
|
|
431
|
+
detail: absolute
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
const writeTarget = path.join(root, "_system", "logs", ".doctor-write-test");
|
|
435
|
+
try {
|
|
436
|
+
await fs.writeFile(writeTarget, "ok", "utf8");
|
|
437
|
+
await fs.unlink(writeTarget);
|
|
438
|
+
checks.push({ name: "write_permission", status: "ok", detail: root });
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
checks.push({ name: "write_permission", status: "permission error", detail: root });
|
|
442
|
+
}
|
|
443
|
+
return checks;
|
|
444
|
+
}
|
|
445
|
+
export async function statusSummary(rootPath) {
|
|
446
|
+
const root = resolveRoot(rootPath);
|
|
447
|
+
const runsRoot = path.join(root, "09-runs");
|
|
448
|
+
if (!(await exists(runsRoot))) {
|
|
449
|
+
return { root, runCount: 0, failedCount: 0 };
|
|
450
|
+
}
|
|
451
|
+
const entries = await fs.readdir(runsRoot, { withFileTypes: true });
|
|
452
|
+
const dirs = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
453
|
+
let failedCount = 0;
|
|
454
|
+
for (const dir of dirs) {
|
|
455
|
+
const payloadPath = path.join(runsRoot, dir, "payload.json");
|
|
456
|
+
try {
|
|
457
|
+
const payload = JSON.parse(await fs.readFile(payloadPath, "utf8"));
|
|
458
|
+
if (payload.source?.fetch_status === "failed") {
|
|
459
|
+
failedCount += 1;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
if (dir.endsWith("-fetch-failed")) {
|
|
464
|
+
failedCount += 1;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const stats = [];
|
|
469
|
+
for (const dir of dirs) {
|
|
470
|
+
stats.push({ dir, mtimeMs: (await fs.stat(path.join(runsRoot, dir))).mtimeMs });
|
|
471
|
+
}
|
|
472
|
+
stats.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
473
|
+
return {
|
|
474
|
+
root,
|
|
475
|
+
runCount: dirs.length,
|
|
476
|
+
failedCount,
|
|
477
|
+
lastRunId: stats[0]?.dir
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
export async function exists(target) {
|
|
481
|
+
try {
|
|
482
|
+
await fs.access(target);
|
|
483
|
+
return true;
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
function readScalar(text, key) {
|
|
490
|
+
const pattern = new RegExp(`^${key}:\\s*(.+)$`, "m");
|
|
491
|
+
return pattern.exec(text)?.[1]?.trim();
|
|
492
|
+
}
|
|
493
|
+
function unquote(value) {
|
|
494
|
+
return value.replace(/^["']|["']$/g, "");
|
|
495
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# AIWiki Agent 对接说明
|
|
2
|
+
|
|
3
|
+
这份文档写给任何可以读取网页、生成结构化内容并调用本机命令的宿主 Agent。
|
|
4
|
+
|
|
5
|
+
## 目标
|
|
6
|
+
|
|
7
|
+
当用户说:
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
入库 https://example.com/article
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Agent 应自动读取网页,生成 payload,通过 AIWiki CLI 写入本地知识库,并把结果摘要回复给用户。
|
|
14
|
+
|
|
15
|
+
## 职责边界
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
Agent 负责:读取网页、整理正文、生成 payload、调用 CLI、回复用户
|
|
19
|
+
AIWiki CLI 负责:校验 payload、写本地文件、输出入库结果
|
|
20
|
+
用户负责:提供链接或正文,最后审阅结果
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
AIWiki CLI 不做通用网页抓取。网页读取失败时,Agent 仍然要调用 CLI 记录失败原因。
|
|
24
|
+
|
|
25
|
+
## 标准流程
|
|
26
|
+
|
|
27
|
+
1. 读取用户给的 URL、正文、附件或消息。
|
|
28
|
+
2. 如果读取成功,生成 `fetch_status: "ok"` 的 payload。
|
|
29
|
+
3. 如果读取失败,生成 `fetch_status: "failed"` 的 payload,并写清 `fetch_notes`。
|
|
30
|
+
4. 不要让用户保存 payload。
|
|
31
|
+
5. 不要让用户手动运行命令。
|
|
32
|
+
6. 优先通过 stdin 调用:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
aiwiki ingest-agent --stdin
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
7. 读取 CLI 输出,向用户回复入库状态、契合度、摘要、资料卡、处理记录和 Obsidian 审阅入口。
|
|
39
|
+
|
|
40
|
+
## 成功 payload
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"schema_version": "aiwiki.agent_payload.v1",
|
|
45
|
+
"source": {
|
|
46
|
+
"kind": "url",
|
|
47
|
+
"url": "https://example.com/article",
|
|
48
|
+
"title": "文章标题",
|
|
49
|
+
"content_format": "markdown",
|
|
50
|
+
"content": "这里是宿主 Agent 读取到的正文内容。",
|
|
51
|
+
"fetcher": "host-agent",
|
|
52
|
+
"fetch_status": "ok",
|
|
53
|
+
"captured_at": "2026-05-07T10:00:00+08:00"
|
|
54
|
+
},
|
|
55
|
+
"request": {
|
|
56
|
+
"mode": "ingest",
|
|
57
|
+
"outputs": [
|
|
58
|
+
"source_card",
|
|
59
|
+
"creative_assets",
|
|
60
|
+
"topics",
|
|
61
|
+
"draft_outline",
|
|
62
|
+
"processing_summary"
|
|
63
|
+
],
|
|
64
|
+
"language": "zh-CN"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 失败 payload
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"schema_version": "aiwiki.agent_payload.v1",
|
|
74
|
+
"source": {
|
|
75
|
+
"kind": "url",
|
|
76
|
+
"url": "https://example.com/article",
|
|
77
|
+
"title": "无法读取的文章",
|
|
78
|
+
"fetcher": "host-agent",
|
|
79
|
+
"fetch_status": "failed",
|
|
80
|
+
"fetch_notes": "网页需要登录或宿主 Agent 无法访问正文。",
|
|
81
|
+
"captured_at": "2026-05-07T10:00:00+08:00"
|
|
82
|
+
},
|
|
83
|
+
"request": {
|
|
84
|
+
"mode": "record_fetch_failure",
|
|
85
|
+
"outputs": [
|
|
86
|
+
"processing_summary"
|
|
87
|
+
],
|
|
88
|
+
"language": "zh-CN"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## 禁止事项
|
|
94
|
+
|
|
95
|
+
- 不要在 payload 中包含输出路径。
|
|
96
|
+
- 不要让用户手动保存 payload。
|
|
97
|
+
- 不要让用户每次输入 `--path`。
|
|
98
|
+
- 不要声称网页抓取是 AIWiki CLI 的能力。
|
|
99
|
+
- 不要在 `fetch_status: "failed"` 时塞入正文内容。
|
|
100
|
+
- 不要替用户安装 Dataview。
|
|
101
|
+
- 不要修改 `.obsidian`、`community-plugins.json` 或 Obsidian 插件配置。
|
|
102
|
+
|
|
103
|
+
## Obsidian + Dataview 边界
|
|
104
|
+
|
|
105
|
+
AIWiki 生成的知识库不依赖 Dataview。用户不安装 Dataview 时,也可以用 Obsidian 原生 Properties、Backlinks、Search、Graph View 和普通 wikilink 审阅。
|
|
106
|
+
|
|
107
|
+
Dataview 只是可选增强。用户自行在 Obsidian Community plugins 中安装并启用 Dataview 后,`dashboards/AIWiki Home.md`、`dashboards/Review Queue.md`、`dashboards/Recent Runs.md` 和 `dashboards/Topic Pipeline.md` 会渲染成表格。
|
|
108
|
+
|
|
109
|
+
## Agent 回复模板
|
|
110
|
+
|
|
111
|
+
成功时:
|
|
112
|
+
|
|
113
|
+
```text
|
|
114
|
+
已加入 Obsidian 审阅队列。
|
|
115
|
+
契合度:<fit_score> / <fit_level>
|
|
116
|
+
摘要:<summary>
|
|
117
|
+
资料卡:<source_card>
|
|
118
|
+
处理记录:<processing_summary>
|
|
119
|
+
Obsidian 入口:<dashboard>
|
|
120
|
+
待审队列:<review_queue>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
失败但已记录时:
|
|
124
|
+
|
|
125
|
+
```text
|
|
126
|
+
未成功入库正文,但已记录失败原因。
|
|
127
|
+
原因:<summary>
|
|
128
|
+
记录目录:<run_dir>
|
|
129
|
+
处理摘要:<processing_summary>
|
|
130
|
+
Obsidian 入口:<dashboard>
|
|
131
|
+
待审队列:<review_queue>
|
|
132
|
+
```
|