czon 0.8.8 → 0.8.10

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.
@@ -71,16 +71,15 @@ async function buildPipeline(options) {
71
71
  await (0, opencode_1.installAgentsToGlobal)();
72
72
  // 清理输出目录
73
73
  await fs.rm(paths_1.CZON_DIST_DIR, { recursive: true, force: true });
74
+ // 扫描源文件
75
+ await (0, scanSourceFiles_1.scanSourceFiles)();
74
76
  // 确保 .czon/.gitignore 文件
75
77
  await (0, writeFile_1.writeFile)(path.join(paths_1.CZON_DIR, '.gitignore'), [
76
78
  'dist',
77
79
  'tmp',
78
80
  // 忽略所有非 md 文件: 先忽略所有文件,再排除 Markdown 文件不忽略
79
- 'src/**/*.*',
80
- '!src/**/*.md',
81
+ ...Array.from(new Set(metadata_1.MetaData.files.filter(f => !f.path.endsWith('.md')).map(f => path.extname(f.path)))).map(ext => `src/**/*${ext}`),
81
82
  ].join('\n'));
82
- // 扫描源文件
83
- await (0, scanSourceFiles_1.scanSourceFiles)();
84
83
  // 链接资源文件 (非翻译文件)
85
84
  for (const file of metadata_1.MetaData.files) {
86
85
  if (file.path.endsWith('.md'))
@@ -4,9 +4,15 @@ exports.LsFilesCommand = void 0;
4
4
  const clipanion_1 = require("clipanion");
5
5
  const findEntries_1 = require("../findEntries");
6
6
  class LsFilesCommand extends clipanion_1.Command {
7
+ constructor() {
8
+ super(...arguments);
9
+ this.aigc = clipanion_1.Option.Boolean('--aigc', false, {
10
+ description: 'Include files under .czon/AIGC directory',
11
+ });
12
+ }
7
13
  async execute() {
8
14
  try {
9
- const files = await (0, findEntries_1.findMarkdownEntries)(process.cwd());
15
+ const files = await (0, findEntries_1.findMarkdownEntries)(process.cwd(), { aigc: this.aigc });
10
16
  if (files.length === 0) {
11
17
  this.context.stdout.write('No markdown files found.\n');
12
18
  }
@@ -12,9 +12,12 @@ const execAsync = (0, util_1.promisify)(child_process_1.exec);
12
12
  * 然后过滤掉.czon目录和只保留.md文件
13
13
  *
14
14
  * @param dirPath 要扫描的目录路径
15
+ * @param options 可选参数
16
+ * @param options.aigc 是否包含 .czon/AIGC 目录下的文件
15
17
  * @returns Promise<string[]> 返回Markdown文件的相对路径数组
16
18
  */
17
- const findMarkdownEntries = async (dirPath) => {
19
+ const findMarkdownEntries = async (dirPath, options) => {
20
+ const aigc = options?.aigc ?? false;
18
21
  // 获取git仓库的根目录
19
22
  const gitRoot = (await execAsync('git rev-parse --show-toplevel', { cwd: dirPath })).stdout.trim();
20
23
  // 使用git命令获取所有文件(包括已跟踪和未跟踪的文件)
@@ -27,7 +30,7 @@ const findMarkdownEntries = async (dirPath) => {
27
30
  const files = stdout
28
31
  .split('\0') // 按空字符分割文件名
29
32
  .filter(line => line.trim() !== '') // 移除空行
30
- .filter(file => !file.startsWith('.')) // 过滤掉隐藏目录下的文件
33
+ .filter(file => !file.startsWith('.') || (aigc && file.startsWith('.czon/AIGC/'))) // 过滤掉隐藏目录下的文件(aigc 模式下保留 .czon/AIGC/)
31
34
  .filter(file => file.endsWith('.md')); // 只保留.md文件
32
35
  // 排除文件系统中不存在的文件
33
36
  const existingFiles = [];
@@ -25,7 +25,7 @@ async function scanSourceFiles() {
25
25
  console.log(`🔍 Scanning source directory...`);
26
26
  const queue = [];
27
27
  const isVisited = new Set();
28
- const markdownFiles = await (0, findEntries_1.findMarkdownEntries)(paths_1.INPUT_DIR);
28
+ const markdownFiles = await (0, findEntries_1.findMarkdownEntries)(paths_1.INPUT_DIR, { aigc: true });
29
29
  for (const filePath of markdownFiles) {
30
30
  queue.push(filePath);
31
31
  }
@@ -2,21 +2,25 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.processSummary = void 0;
4
4
  const promises_1 = require("fs/promises");
5
- const fs_1 = require("fs");
6
5
  const path_1 = require("path");
6
+ const paths_1 = require("../paths");
7
7
  const opencode_1 = require("../services/opencode");
8
8
  // Prompt 模板目录路径(在项目根目录的 prompts/ 文件夹中)
9
9
  const PROMPTS_DIR = (0, path_1.join)(__dirname, '../../prompts');
10
+ // 输出目录:.czon/AIGC/SUMMARY/
11
+ const SUMMARY_DIR = (0, path_1.join)(paths_1.CZON_DIR, 'AIGC', 'SUMMARY');
12
+ // 最大重试次数
13
+ const MAX_RETRIES = 3;
10
14
  // 风格配置
11
15
  const SUMMARY_STYLES = [
12
- { skill: 'summary-objective', output: '1-objective.md', name: '客观中立' },
13
- { skill: 'summary-critical', output: '2-critical.md', name: '客观批判' },
14
- { skill: 'summary-positive', output: '3-positive.md', name: '赞扬鼓励' },
15
- { skill: 'summary-popular', output: '4-popular.md', name: '科普介绍' },
16
- { skill: 'summary-artistic', output: '5-artistic.md', name: '文艺感性' },
17
- { skill: 'summary-philosophical', output: '6-philosophical.md', name: '哲学思辨' },
18
- { skill: 'summary-psychological', output: '7-psychological.md', name: '心理分析' },
19
- { skill: 'summary-historical', output: '8-history.md', name: '历史时间跨度' },
16
+ { skill: 'summary-objective', name: '客观中立' },
17
+ { skill: 'summary-critical', name: '客观批判' },
18
+ { skill: 'summary-positive', name: '赞扬鼓励' },
19
+ { skill: 'summary-popular', name: '科普介绍' },
20
+ { skill: 'summary-artistic', name: '文艺感性' },
21
+ { skill: 'summary-philosophical', name: '哲学思辨' },
22
+ { skill: 'summary-psychological', name: '心理分析' },
23
+ { skill: 'summary-historical', name: '历史时间跨度' },
20
24
  ];
21
25
  /**
22
26
  * 读取 Prompt 模板文件内容
@@ -26,16 +30,24 @@ const loadPromptTemplate = async (templateName) => {
26
30
  return (0, promises_1.readFile)(templatePath, 'utf-8');
27
31
  };
28
32
  /**
29
- * 验证文件是否存在
33
+ * 获取文件的 mtime(毫秒),文件不存在则返回 null
30
34
  */
31
- const verifyFileExists = (filePath) => {
32
- return (0, fs_1.existsSync)(filePath);
35
+ const getFileMtimeMs = async (filePath) => {
36
+ try {
37
+ const s = await (0, promises_1.stat)(filePath);
38
+ return s.mtimeMs;
39
+ }
40
+ catch {
41
+ return null;
42
+ }
33
43
  };
34
44
  /**
35
- * 生成单个风格的报告
45
+ * 生成单个风格的报告(带重试循环)
36
46
  */
37
47
  const generateStyleReport = async (model, baseContent, styleConfig, cwd) => {
38
48
  try {
49
+ const relativeOutput = `.czon/AIGC/SUMMARY/${styleConfig.skill}.md`;
50
+ const outputPath = (0, path_1.join)(paths_1.INPUT_DIR, relativeOutput);
39
51
  const styleContent = await loadPromptTemplate(styleConfig.skill);
40
52
  const prompt = `
41
53
  ${baseContent}
@@ -50,23 +62,45 @@ ${styleContent}
50
62
 
51
63
  请严格按照上述指南,生成「${styleConfig.name}」风格的分析报告。
52
64
 
53
- 输出文件:SUMMARY/${styleConfig.output}
65
+ 输出文件:${relativeOutput}
54
66
 
55
67
  注意:
56
68
  1. 必须生成完整的报告文件
57
- 2. 文件必须保存到 SUMMARY/${styleConfig.output}
69
+ 2. 文件必须保存到 ${relativeOutput}
58
70
  3. 确保所有链接使用 ../ 开头的相对路径
59
71
  `.trim();
60
- await (0, opencode_1.runOpenCode)(prompt, { model, cwd });
61
- // 验证文件是否生成
62
- const outputPath = (0, path_1.join)(cwd, 'SUMMARY', styleConfig.output);
63
- if (!verifyFileExists(outputPath)) {
64
- return {
65
- success: false,
66
- error: `文件未生成: ${outputPath}`,
67
- };
72
+ // 记录发送前的 mtime
73
+ const mtimeBefore = await getFileMtimeMs(outputPath);
74
+ // 发送初始 prompt,获取句柄
75
+ const handle = await (0, opencode_1.runOpenCode)(prompt, { model, cwd });
76
+ // 验证 + 重试循环
77
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
78
+ const mtimeAfter = await getFileMtimeMs(outputPath);
79
+ const fileExists = mtimeAfter !== null;
80
+ const fileModified = mtimeBefore !== mtimeAfter;
81
+ if (fileExists && fileModified) {
82
+ return { success: true };
83
+ }
84
+ // 构造错误反馈
85
+ let errorMsg;
86
+ if (!fileExists) {
87
+ errorMsg = `错误:文件 ${relativeOutput} 未生成。请立即创建并写入完整的报告内容到 ${relativeOutput}。`;
88
+ }
89
+ else {
90
+ errorMsg = `错误:文件 ${relativeOutput} 未被修改。请重新生成完整内容并覆盖写入 ${relativeOutput}。`;
91
+ }
92
+ console.warn(` ⚠️ 重试 ${attempt}/${MAX_RETRIES}: ${errorMsg}`);
93
+ await handle.prompt(errorMsg);
94
+ }
95
+ // 最终检查
96
+ const mtimeFinal = await getFileMtimeMs(outputPath);
97
+ if (mtimeFinal !== null && mtimeBefore !== mtimeFinal) {
98
+ return { success: true };
68
99
  }
69
- return { success: true };
100
+ return {
101
+ success: false,
102
+ error: `${MAX_RETRIES} 次重试后仍未成功生成文件: ${relativeOutput}`,
103
+ };
70
104
  }
71
105
  catch (error) {
72
106
  return {
@@ -83,10 +117,6 @@ const processSummary = async (model) => {
83
117
  // 加载基础规则
84
118
  console.info('📖 加载基础规则...');
85
119
  const baseContent = await loadPromptTemplate('summary-base');
86
- // Phase 0: 清空 SUMMARY 目录
87
- console.info('🗑️ 清空 SUMMARY 目录...');
88
- await (0, promises_1.rm)((0, path_1.join)(cwd, 'SUMMARY'), { recursive: true, force: true });
89
- await (0, promises_1.mkdir)((0, path_1.join)(cwd, 'SUMMARY'), { recursive: true });
90
120
  // Phase 1: 串行生成 8 种风格报告
91
121
  console.info(`\n📊 开始生成 ${SUMMARY_STYLES.length} 种风格的分析报告...\n`);
92
122
  const results = [];
@@ -96,7 +126,6 @@ const processSummary = async (model) => {
96
126
  const result = await generateStyleReport(model, baseContent, style, cwd);
97
127
  results.push({
98
128
  name: style.name,
99
- output: style.output,
100
129
  ...result,
101
130
  });
102
131
  if (result.success) {
@@ -114,7 +143,7 @@ const processSummary = async (model) => {
114
143
  if (failedResults.length > 0) {
115
144
  console.info('❌ 失败的报告:');
116
145
  for (const failed of failedResults) {
117
- console.info(` - ${failed.name} (${failed.output}): ${failed.error}`);
146
+ console.info(` - ${failed.name}: ${failed.error}`);
118
147
  }
119
148
  console.info('');
120
149
  }
@@ -122,10 +151,10 @@ const processSummary = async (model) => {
122
151
  console.info('📁 验证文件存在性:');
123
152
  let allFilesExist = true;
124
153
  for (const style of SUMMARY_STYLES) {
125
- const filePath = (0, path_1.join)(cwd, 'SUMMARY', style.output);
126
- const exists = verifyFileExists(filePath);
154
+ const filePath = (0, path_1.join)(SUMMARY_DIR, style.skill + '.md');
155
+ const exists = (await getFileMtimeMs(filePath)) !== null;
127
156
  const status = exists ? '✅' : '❌';
128
- console.info(` ${status} SUMMARY/${style.output}`);
157
+ console.info(` ${status} .czon/AIGC/SUMMARY/${style.skill}.md`);
129
158
  if (!exists) {
130
159
  allFilesExist = false;
131
160
  }
@@ -5,6 +5,18 @@ const promises_1 = require("fs/promises");
5
5
  const path_1 = require("path");
6
6
  const paths_1 = require("../paths");
7
7
  const writeFile_1 = require("../utils/writeFile");
8
+ // ---------------------------------------------------------------------------
9
+ // Constants
10
+ // ---------------------------------------------------------------------------
11
+ /** Retry delays for transient network errors (exponential back-off). */
12
+ const RETRY_DELAYS_MS = [1000, 2000, 4000];
13
+ /** Interval between status polls while waiting for a session to become idle. */
14
+ const POLL_INTERVAL_MS = 2000;
15
+ /** Maximum time to wait for a session to become idle (10 minutes). */
16
+ const POLL_MAX_DURATION_MS = 600000;
17
+ // ---------------------------------------------------------------------------
18
+ // Helpers
19
+ // ---------------------------------------------------------------------------
8
20
  function parseModelString(model) {
9
21
  const parts = model.split('/');
10
22
  if (parts.length === 2) {
@@ -13,111 +25,143 @@ function parseModelString(model) {
13
25
  // Default provider if no slash
14
26
  return { providerID: 'opencode', modelID: model };
15
27
  }
28
+ function sleep(ms) {
29
+ return new Promise(resolve => setTimeout(resolve, ms));
30
+ }
31
+ /**
32
+ * Retry `fn` on transient network errors (TypeError / "fetch failed").
33
+ * Non-network errors are re-thrown immediately.
34
+ */
35
+ async function withRetry(fn) {
36
+ for (let attempt = 0;; attempt++) {
37
+ try {
38
+ return await fn();
39
+ }
40
+ catch (err) {
41
+ if (attempt >= RETRY_DELAYS_MS.length)
42
+ throw err;
43
+ const isNetworkError = err instanceof TypeError || (err instanceof Error && err.message.includes('fetch failed'));
44
+ if (!isNetworkError)
45
+ throw err;
46
+ const delay = RETRY_DELAYS_MS[attempt];
47
+ console.warn(`⚠️ Network error, retrying in ${delay}ms... (${attempt + 1}/${RETRY_DELAYS_MS.length})`);
48
+ await sleep(delay);
49
+ }
50
+ }
51
+ }
52
+ /**
53
+ * Poll `session.status()` until the given session becomes idle.
54
+ * Individual poll failures are silently retried (the server is still running,
55
+ * just a transient network hiccup).
56
+ */
57
+ async function pollUntilIdle(client, sessionId, cwd, signal) {
58
+ const deadline = Date.now() + POLL_MAX_DURATION_MS;
59
+ while (Date.now() < deadline) {
60
+ if (signal?.aborted) {
61
+ throw new Error('OpenCode execution was aborted');
62
+ }
63
+ await sleep(POLL_INTERVAL_MS);
64
+ try {
65
+ const statusResp = await client.session.status({
66
+ query: { directory: cwd },
67
+ });
68
+ const statuses = statusResp.data ?? {};
69
+ const sessionStatus = statuses[sessionId];
70
+ if (!sessionStatus || sessionStatus.type === 'idle') {
71
+ return; // done
72
+ }
73
+ // busy / retry → keep polling
74
+ }
75
+ catch {
76
+ // Network error → ignore, keep polling
77
+ console.warn('⚠️ Status poll failed, will retry...');
78
+ }
79
+ }
80
+ throw new Error(`OpenCode session timed out after ${POLL_MAX_DURATION_MS / 1000}s`);
81
+ }
16
82
  /**
17
83
  * Run OpenCode to generate AI response for a given prompt.
18
84
  *
19
85
  * This function uses the OpenCode SDK to connect to a running OpenCode server.
20
86
  * Assumes an OpenCode server is already running externally.
21
87
  *
88
+ * Creates a new session, sends the initial prompt, and returns a handle that
89
+ * can be used to continue the conversation by calling `handle.prompt()`.
90
+ *
91
+ * Internally uses `promptAsync()` + status polling instead of the blocking
92
+ * `prompt()` call, so that long-running AI tasks survive transient network
93
+ * interruptions.
94
+ *
22
95
  * @param prompt - The prompt to send to OpenCode
23
96
  * @param options - Optional configuration
24
- * @returns Promise that resolves when the operation completes
25
- *
26
- * @important
27
- * The AI response is handled internally by OpenCode. This function does not
28
- * return the response content. Any output files or results are managed by
29
- * the OpenCode agent or session directly.
97
+ * @returns A session handle for continued interaction
30
98
  */
31
- const runOpenCode = (prompt, options) => {
99
+ const runOpenCode = async (prompt, options) => {
32
100
  const model = options?.model ?? 'opencode/gpt-5-nano';
33
101
  const signal = options?.signal;
34
102
  const cwd = options?.cwd || process.cwd();
35
103
  const agent = options?.agent;
36
- console.info(`🛠️ Running OpenCode with model: ${model}, agent: ${agent || 'none'}`);
37
- return new Promise(async (resolve, reject) => {
38
- const agentInfo = agent ? ` with agent ${agent}` : '';
39
- console.info(`🚀 Running OpenCode with model ${model}${agentInfo}`);
40
- let cancelled = false;
41
- const cleanup = () => {
42
- if (signal) {
43
- signal.removeEventListener('abort', onAbort);
44
- }
45
- };
46
- const onAbort = () => {
47
- cancelled = true;
48
- cleanup();
49
- reject(new Error('OpenCode execution was aborted'));
50
- };
51
- if (signal) {
52
- signal.addEventListener('abort', onAbort);
53
- if (signal.aborted) {
54
- onAbort();
55
- return;
56
- }
57
- }
58
- try {
59
- const { createOpencodeClient } = await import('@opencode-ai/sdk');
60
- const baseUrl = 'http://localhost:4096';
61
- const client = createOpencodeClient({
62
- baseUrl: baseUrl,
63
- directory: cwd,
64
- });
65
- const modelObj = parseModelString(model);
66
- const session = await client.session.create();
67
- if (!session.data?.id)
68
- throw new Error('Failed to create OpenCode session', { cause: session.error });
69
- options?.signal?.addEventListener('abort', () => {
70
- console.info(`🛑 Aborting OpenCode session ${session.data.id}...`);
71
- client.session.abort({
72
- path: {
73
- id: session.data.id,
104
+ const agentInfo = agent ? ` with agent ${agent}` : '';
105
+ console.info(`🚀 Running OpenCode with model ${model}${agentInfo}`);
106
+ const { createOpencodeClient } = await import('@opencode-ai/sdk');
107
+ const baseUrl = 'http://localhost:4096';
108
+ const client = createOpencodeClient({
109
+ baseUrl: baseUrl,
110
+ directory: cwd,
111
+ });
112
+ const modelObj = parseModelString(model);
113
+ const session = await client.session.create();
114
+ if (!session.data?.id)
115
+ throw new Error('Failed to create OpenCode session', { cause: session.error });
116
+ const sessionId = session.data.id;
117
+ const directoryBase64 = Buffer.from(session.data.directory).toString('base64');
118
+ const url = `${baseUrl}/${directoryBase64}/session/${sessionId}`;
119
+ console.info('OpenCode Session Created', url);
120
+ const handle = {
121
+ sessionId,
122
+ url,
123
+ async prompt(text, promptOptions) {
124
+ const promptSignal = promptOptions?.signal;
125
+ // Step 1: Send the message asynchronously (short request, returns 204)
126
+ await withRetry(async () => {
127
+ const resp = await client.session.promptAsync({
128
+ path: { id: sessionId },
129
+ body: {
130
+ model: modelObj,
131
+ agent,
132
+ parts: [{ type: 'text', text }],
74
133
  },
134
+ query: { directory: cwd },
135
+ signal: promptSignal,
75
136
  });
137
+ if (resp.error) {
138
+ throw new Error(`OpenCode API error: ${JSON.stringify(resp.error)}`);
139
+ }
76
140
  });
77
- const directoryBase64 = Buffer.from(session.data.directory).toString('base64');
78
- const url = `${baseUrl}/${directoryBase64}/session/${session.data.id}`;
79
- console.info('OpenCode Session Created', url);
80
- const response = await client.session.prompt({
81
- path: {
82
- id: session.data.id,
83
- },
84
- body: {
85
- model: modelObj,
86
- agent,
87
- parts: [
88
- {
89
- type: 'text',
90
- text: prompt,
91
- },
92
- ],
93
- },
94
- query: {
95
- directory: cwd,
96
- },
97
- signal,
98
- });
99
- if (cancelled) {
100
- throw new Error('Cancelled');
101
- }
102
- if (response.error) {
103
- throw new Error(`OpenCode API error: ${JSON.stringify(response.error)}`);
104
- }
105
- // await client.session.delete({
106
- // path: {
107
- // id: session.data.id,
108
- // },
109
- // });
110
- cleanup();
111
- resolve();
112
- }
113
- catch (err) {
114
- if (cancelled) {
115
- return;
141
+ // Step 2: Poll until the session becomes idle
142
+ await pollUntilIdle(client, sessionId, cwd, promptSignal);
143
+ // Step 3: Fetch the latest assistant message
144
+ const msgs = await withRetry(() => client.session.messages({
145
+ path: { id: sessionId },
146
+ query: { directory: cwd },
147
+ }));
148
+ const allMessages = msgs.data ?? [];
149
+ const lastAssistant = [...allMessages].reverse().find(m => m.info.role === 'assistant');
150
+ if (!lastAssistant) {
151
+ throw new Error('No assistant response found after prompt');
116
152
  }
117
- cleanup();
118
- reject(new Error(`OpenCode SDK error: ${err instanceof Error ? err.message : String(err)}. Make sure an OpenCode server is running.`));
119
- }
120
- });
153
+ return lastAssistant;
154
+ },
155
+ async abort() {
156
+ await client.session.abort({ path: { id: sessionId } });
157
+ },
158
+ async delete() {
159
+ await client.session.delete({ path: { id: sessionId } });
160
+ },
161
+ };
162
+ // Send the initial prompt
163
+ await handle.prompt(prompt, { signal });
164
+ return handle;
121
165
  };
122
166
  exports.runOpenCode = runOpenCode;
123
167
  const installAgentsToGlobal = async () => {
@@ -48,7 +48,8 @@ const convertMarkdownToHtml = (article, path, lang, mdContent) => {
48
48
  const file = metadata_1.MetaData.files.find(f => f.path === resolvedPath);
49
49
  if (!file) {
50
50
  console.warn(`⚠️ Link target not found for path ${resolvedPath} in file ${path}`);
51
- return originalLinkRenderer.call(this, link);
51
+ // 链接目标不存在,使用斜体表示损坏的链接,不渲染为 <a> 标签
52
+ return `<em title="Link target not found: ${escapeHtml(link.href)}">${link.text}</em>`;
52
53
  }
53
54
  if (link.href.endsWith('.md')) {
54
55
  if (!file.metadata?.slug) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "czon",
3
- "version": "0.8.8",
3
+ "version": "0.8.10",
4
4
  "description": "CZON - AI enhanced Markdown content engine",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -118,10 +118,6 @@
118
118
  - 使用节奏:长短句交替,创造韵律感
119
119
  - 引用原文:选择有感染力的句子
120
120
 
121
- ## 输出文件
122
-
123
- `SUMMARY/5-artistic.md`
124
-
125
121
  ## 质量检查清单
126
122
 
127
123
  - [ ] 语言富有画面感
@@ -41,10 +41,58 @@
41
41
 
42
42
  **如果发现遗漏**:返回阶段 2 处理遗漏的文件。
43
43
 
44
- ### 阶段 5:生成报告
44
+ ### 阶段 5:生成报告(分段写入)
45
45
 
46
46
  只有在完成以上所有阶段后,才能开始生成报告。
47
47
 
48
+ 由于报告内容可能很长,**必须分段写入**,禁止一次性写入整个报告。
49
+
50
+ #### 步骤 1:写入骨架文件
51
+
52
+ 首先,使用 Write 工具创建报告文件,写入以下内容:
53
+
54
+ - 头部格式(标题、AI 分析时间、文件数量、注释、分隔线)
55
+ - 所有章节的标题(仅标题,不含正文)
56
+ - 每个章节标题下方放置占位标记:`<!-- SECTION: [章节名] -->`
57
+
58
+ **示例骨架**:
59
+
60
+ ```markdown
61
+ # 报告标题
62
+
63
+ **AI 分析时间**:2025年01月01日
64
+ **基于 42 个 Markdown 文件生成**
65
+ **注**:本报告由 AI 生成,内容仅供参考。
66
+
67
+ ---
68
+
69
+ ## 概述
70
+
71
+ <!-- SECTION: 概述 -->
72
+
73
+ ## 第二章标题
74
+
75
+ <!-- SECTION: 第二章标题 -->
76
+
77
+ ...
78
+ ```
79
+
80
+ #### 步骤 2:逐章节填充内容
81
+
82
+ 按章节顺序,使用 Edit 工具逐一替换占位标记为实际内容:
83
+
84
+ 1. 每次只填充**一个章节**的内容
85
+ 2. 将 `<!-- SECTION: [章节名] -->` 替换为该章节的完整正文
86
+ 3. 单次写入内容控制在 **2000 字以内**。如果某个章节超过 2000 字,将其拆分为多次写入(先写入前半部分并在末尾保留一个临时占位标记,再继续写入后半部分)
87
+
88
+ #### 步骤 3:完整性检查
89
+
90
+ 所有章节填充完毕后:
91
+
92
+ 1. 读取完整文件,确认无遗漏的 `<!-- SECTION:` 占位标记
93
+ 2. 确认所有链接格式正确(相对路径以 `../` 开头,链接文本为文章标题)
94
+ 3. 确认头部的文件数量 N 正确
95
+
48
96
  ### 禁止行为
49
97
 
50
98
  - ❌ 不得在阅读完所有文件前开始生成报告
@@ -103,8 +151,3 @@
103
151
 
104
152
  - 考虑时间跨度,给予最近的文章更高的权重
105
153
  - 但不要忽略较早的重要内容
106
-
107
- ### 5. 输出位置
108
-
109
- - 所有报告生成到 `SUMMARY/` 目录中
110
- - 文件名按指定格式命名
@@ -136,10 +136,6 @@
136
136
  2. 建议 2:具体可操作的改进方案
137
137
  ```
138
138
 
139
- ## 输出文件
140
-
141
- `SUMMARY/2-critical.md`
142
-
143
139
  ## 质量检查清单
144
140
 
145
141
  - [ ] 每个批评点都有事实依据
@@ -154,10 +154,6 @@
154
154
  - 关键洞察
155
155
  - 历史意义
156
156
 
157
- ## 输出文件
158
-
159
- `SUMMARY/8-history.md`
160
-
161
157
  ## 质量检查清单
162
158
 
163
159
  - [ ] 时间线准确
@@ -129,10 +129,6 @@
129
129
  - **数学表达**:如有公式,列出
130
130
  ```
131
131
 
132
- ## 输出文件
133
-
134
- `SUMMARY/1-objective.md`
135
-
136
132
  ## 质量检查清单
137
133
 
138
134
  - [ ] 无主观评价词汇
@@ -132,10 +132,6 @@
132
132
  - 批判理论:权力、解放、批判
133
133
  - 东方哲学:道、无为、中庸
134
134
 
135
- ## 输出文件
136
-
137
- `SUMMARY/6-philosophical.md`
138
-
139
135
  ## 质量检查清单
140
136
 
141
137
  - [ ] 命题清晰明确
@@ -117,10 +117,6 @@
117
117
 
118
118
  推荐阅读顺序,从最易懂的开始
119
119
 
120
- ## 输出文件
121
-
122
- `SUMMARY/4-popular.md`
123
-
124
120
  ## 质量检查清单
125
121
 
126
122
  - [ ] 无未解释的专业术语
@@ -115,10 +115,6 @@
115
115
  - 未来潜力展望
116
116
  - 鼓励性结语
117
117
 
118
- ## 输出文件
119
-
120
- `SUMMARY/3-positive.md`
121
-
122
118
  ## 质量检查清单
123
119
 
124
120
  - [ ] 所有赞扬都有事实依据
@@ -161,10 +161,6 @@
161
161
  - 发展建议
162
162
  - 心理健康风险评估
163
163
 
164
- ## 输出文件
165
-
166
- `SUMMARY/7-psychological.md`
167
-
168
164
  ## 质量检查清单
169
165
 
170
166
  - [ ] MBTI 四个维度都有分析