czon 0.5.1 → 0.5.3
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/.opencode/skills/article-summarizer/SKILL.md +123 -0
- package/dist/ai/extractMetadataFromMarkdown.js +71 -82
- package/dist/cli.js +34 -0
- package/dist/process/processTranslations.js +5 -2
- package/dist/services/opencode.js +1 -1
- package/dist/ssg/ContentPage.js +2 -1
- package/dist/ssg/IndexPage.js +14 -3
- package/dist/ssg/resourceMap.js +14 -1
- package/package.json +1 -1
- package/dist/services/opencode.test.js +0 -34
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: article-summarizer
|
|
3
|
+
description: 'Guide for generating multi-style article summaries from Markdown files in a repository. Use when user requests article summarization, multi-perspective analysis, or content summarization with different writing styles. Triggers on keywords: article summary, markdown summary, multi-style summary, SUMMARY directory, git ls-files, 7 styles.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 文章总结器
|
|
7
|
+
|
|
8
|
+
## 概述
|
|
9
|
+
|
|
10
|
+
本skill指导代理从仓库中的Markdown文章生成全面的多风格摘要。它提供一个结构化工作流程:清空SUMMARY目录、通过git命令识别目标Markdown文件、读取所有.md文件(排除.czon)、并以7种不同写作风格生成摘要,附带正确的引用规则。
|
|
11
|
+
|
|
12
|
+
## 工作流程
|
|
13
|
+
|
|
14
|
+
### 1. 准备SUMMARY目录
|
|
15
|
+
|
|
16
|
+
首先,确保SUMMARY目录存在并清空之前的内容:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
mkdir -p SUMMARY
|
|
20
|
+
rm -rf SUMMARY/*
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 2. 识别目标Markdown文件
|
|
24
|
+
|
|
25
|
+
使用以下git命令列出仓库中的所有Markdown文件(已跟踪和未跟踪),排除.czon目录中的文件:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
git ls-files --others --cached --exclude-standard -z -x ".czon" | tr '\0' '\n' | grep -v ".czon" | grep ".md$"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
将输出捕获为文件路径列表。验证每个文件存在且可读。
|
|
32
|
+
|
|
33
|
+
### 3. 读取和处理Markdown内容
|
|
34
|
+
|
|
35
|
+
对于每个Markdown文件:
|
|
36
|
+
|
|
37
|
+
- 使用`Read`工具读取完整内容
|
|
38
|
+
- 提取元数据:标题(第一个H1)、修改日期(git log)、字数统计
|
|
39
|
+
- 解析结构:标题、链接、代码块
|
|
40
|
+
- 注意指向其他Markdown文件的内部链接
|
|
41
|
+
|
|
42
|
+
### 4. 生成多风格摘要
|
|
43
|
+
|
|
44
|
+
在`SUMMARY/`目录中为7种风格创建单独的摘要文件。每个摘要必须严格基于Markdown文件的事实内容,不得虚构。
|
|
45
|
+
|
|
46
|
+
**风格指南:**
|
|
47
|
+
|
|
48
|
+
1. **客观中立风格**(基于事实,保持简洁,就像名片和履历一样)
|
|
49
|
+
- 呈现关键事实、日期、角色、成就
|
|
50
|
+
- 避免主观语言;坚持可验证的信息
|
|
51
|
+
- 格式:项目符号或简短段落
|
|
52
|
+
|
|
53
|
+
2. **客观批判风格**(基于事实的批判,反思视角)
|
|
54
|
+
- 基于证据识别优势和劣势
|
|
55
|
+
- 提出改进领域或替代方法
|
|
56
|
+
- 保持专业语气;避免人身攻击
|
|
57
|
+
|
|
58
|
+
3. **赞扬鼓励风格**(基于事实的积极强化)
|
|
59
|
+
- 突出成功、创新元素、有价值的贡献
|
|
60
|
+
- 以建设性的方式构建为"哪些做得好以及为什么"
|
|
61
|
+
- 为未来工作提供激励视角
|
|
62
|
+
|
|
63
|
+
4. **幽默调侃风格**(轻松愉快,面向普通受众)
|
|
64
|
+
- 用通俗语言解释复杂概念("说人话")
|
|
65
|
+
- 使用温和的幽默、类比、相关例子
|
|
66
|
+
- 跳过过于技术性或敏感、可能被误解的内容
|
|
67
|
+
- 目标:引发会心一笑,而非嘲笑
|
|
68
|
+
|
|
69
|
+
5. **文艺感性风格**(叙事性,引发共鸣,故事驱动)
|
|
70
|
+
- 创造生动的意象,情感共鸣
|
|
71
|
+
- 将事实编织成引人入胜的叙事弧线
|
|
72
|
+
- 旨在激发共情和连接
|
|
73
|
+
|
|
74
|
+
6. **心理分析风格**(例如,基于证据的MBTI分析)
|
|
75
|
+
- 分配心理特质(如INTJ、ENFP),并提供明确推理
|
|
76
|
+
- 为每个特质维度提供内容中的具体例子
|
|
77
|
+
- 确保分析基于可观察的模式
|
|
78
|
+
|
|
79
|
+
7. **历史时间跨度风格**(按时间发展视角)
|
|
80
|
+
- 沿时间线组织内容
|
|
81
|
+
- 展示演变、转折点、随时间趋势
|
|
82
|
+
- 在更广泛的历史或项目叙事中情境化
|
|
83
|
+
|
|
84
|
+
### 5. 输出格式规则
|
|
85
|
+
|
|
86
|
+
- **文件命名**: `SUMMARY/neutral.md`, `SUMMARY/critical.md`, `SUMMARY/praise.md`, `SUMMARY/humorous.md`, `SUMMARY/literary.md`, `SUMMARY/psychological.md`, `SUMMARY/historical.md`
|
|
87
|
+
- **头部**: 每个文件必须以以下内容开头:
|
|
88
|
+
```
|
|
89
|
+
# [风格名称] 摘要
|
|
90
|
+
*由AI生成于YYYY-MM-DD HH:MM UTC*
|
|
91
|
+
*内容为AI生成,仅供参考*
|
|
92
|
+
```
|
|
93
|
+
- **引用链接**: 当引用源Markdown文件时:
|
|
94
|
+
- 使用相对路径链接到具体文件:`../path/to/file.md`
|
|
95
|
+
- 锚文本应该是文章的标题(第一个H1),而不是文件名
|
|
96
|
+
- 确保链接有效(如有需要,使用`Read`工具测试)
|
|
97
|
+
- 永远不要链接到目录——始终链接到具体的.md文件
|
|
98
|
+
- **权重分配**: 给予最近修改的文章更高的权重(使用git时间戳)
|
|
99
|
+
- **事实核查**: 跨文件交叉引用以确保一致性
|
|
100
|
+
|
|
101
|
+
### 6. 质量保证
|
|
102
|
+
|
|
103
|
+
生成所有摘要后:
|
|
104
|
+
|
|
105
|
+
- 验证没有摘要与事实源材料相矛盾
|
|
106
|
+
- 检查所有内部链接是否正确解析
|
|
107
|
+
- 确保每种风格保持其独特的语调
|
|
108
|
+
- 确认SUMMARY目录恰好包含7个文件
|
|
109
|
+
|
|
110
|
+
## 常见陷阱
|
|
111
|
+
|
|
112
|
+
- **虚构**: 绝不添加源Markdown文件中不存在的信息
|
|
113
|
+
- **链接错误**: 从SUMMARY目录链接时始终使用`../`前缀
|
|
114
|
+
- **风格混杂**: 保持每个摘要严格在其指定的语调范围内
|
|
115
|
+
- **新近度偏差**: 虽然近期文章获得更高权重,但不要忽略较旧的有价值内容
|
|
116
|
+
- **过度解读**: 在心理分析中,仅基于明确的证据得出结论
|
|
117
|
+
|
|
118
|
+
## 示例用户请求
|
|
119
|
+
|
|
120
|
+
- "以7种不同风格总结所有Markdown文章"
|
|
121
|
+
- "生成我们文档的多视角分析"
|
|
122
|
+
- "为我们的知识库创建文章摘要"
|
|
123
|
+
- "生成我们内容的幽默和严肃版本"
|
|
@@ -1,13 +1,59 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.extractMetadataFromMarkdown = extractMetadataFromMarkdown;
|
|
4
|
+
const metadata_1 = require("../metadata");
|
|
4
5
|
const openai_1 = require("../services/openai");
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
7
|
+
* AI Metadata 提取模块
|
|
8
|
+
*
|
|
9
|
+
* 优化策略说明:
|
|
10
|
+
* 1. 从 MetaData 全局状态读取已有 slug,不作为参数传递
|
|
11
|
+
* 2. 如果已有 slug,条件化 prompt - 完全不提及 slug 相关指令
|
|
12
|
+
* 3. trade-off: 优先提升 AI 质量(减少无关任务干扰),可能降低 context 缓存命中率
|
|
8
13
|
*/
|
|
9
14
|
async function extractMetadataFromMarkdown(filePath, content) {
|
|
10
|
-
const
|
|
15
|
+
const existingSlug = metadata_1.MetaData.files.find(f => f.path === filePath)?.metadata?.slug;
|
|
16
|
+
const hasExistingSlug = !!existingSlug;
|
|
17
|
+
const fields = [
|
|
18
|
+
'title: 文档的标题(简洁明了,不超过 30 个字)',
|
|
19
|
+
'tags: 关键词列表(3-8 个关键词,使用中文或英文)',
|
|
20
|
+
'description: 文档的简短描述,微摘要(用一句话概括本文核心价值,不超过 100 字符),用于 SEO meta description,社交卡片短描述',
|
|
21
|
+
'summary: 文档中型摘要(用一段话总结文章,包含关键论点和结论,控制在 300 字以内),用于 邮件推送内容,newsletter 介绍',
|
|
22
|
+
'inferred_date: 文档中隐含的创建日期(如果有的话,格式:YYYY-MM-DD,没有就留空字符串)',
|
|
23
|
+
'inferred_lang: 文档使用的语言代码(例如:zh-Hans 表示简体中文,en-US 表示美式英语)',
|
|
24
|
+
'key_points: 文章的关键要点列表(5-10 个要点,简洁明了)',
|
|
25
|
+
'audience: 目标读者描述(简短描述,50 字以内)',
|
|
26
|
+
'short_summary: 文档的超短摘要(用 2-3 句话概括文章主要内容,突出核心观点),用于文章列表页摘要,RSS feed 描述',
|
|
27
|
+
...(hasExistingSlug
|
|
28
|
+
? []
|
|
29
|
+
: ['slug: URL 友好别名(使用小写字母、数字和连字符,仅包含英文和数字)']),
|
|
30
|
+
];
|
|
31
|
+
const jsonFields = [
|
|
32
|
+
'{',
|
|
33
|
+
' "title": "文档标题",',
|
|
34
|
+
' "description": "用一句话概括本文核心价值,不超过 100 字符",',
|
|
35
|
+
' "summary": "中型摘要,用一段话总结文章,包含关键论点和结论",',
|
|
36
|
+
' "short_summary": "超短摘要,用 2-3 句话概括文章主要内容,突出核心观点",',
|
|
37
|
+
' "tags": ["关键词1", "关键词2", "关键词3"],',
|
|
38
|
+
' "inferred_date": "2023-01-01",',
|
|
39
|
+
' "inferred_lang": "zh-Hans",',
|
|
40
|
+
' "key_points": ["要点1", "要点2", "要点3"],',
|
|
41
|
+
...(hasExistingSlug ? [] : [' "slug": "URL 友好别名",']),
|
|
42
|
+
' "audience": "目标读者描述"',
|
|
43
|
+
'}',
|
|
44
|
+
];
|
|
45
|
+
const prompt = `请分析以下文档内容,提取以下信息并返回 JSON 格式:
|
|
46
|
+
|
|
47
|
+
文档内容:
|
|
48
|
+
"""
|
|
49
|
+
${content}
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
请提取:
|
|
53
|
+
${fields.join('\n')}
|
|
54
|
+
|
|
55
|
+
请严格按照以下 JSON 格式返回,不要包含任何其他文本:
|
|
56
|
+
${jsonFields.join('\n')}`;
|
|
11
57
|
const messages = [
|
|
12
58
|
{
|
|
13
59
|
role: 'system',
|
|
@@ -22,85 +68,28 @@ async function extractMetadataFromMarkdown(filePath, content) {
|
|
|
22
68
|
response_format: { type: 'json_object' },
|
|
23
69
|
task_id: `extract-metadata:${filePath}`,
|
|
24
70
|
});
|
|
25
|
-
const metadata =
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
71
|
+
const metadata = JSON.parse(response.choices[0].message.content);
|
|
72
|
+
const result = {
|
|
73
|
+
title: metadata.title?.trim() || '',
|
|
74
|
+
description: metadata.description?.trim() || '',
|
|
75
|
+
short_summary: metadata.short_summary?.trim() || '',
|
|
76
|
+
audience: metadata.audience?.trim() || '',
|
|
77
|
+
key_points: Array.isArray(metadata.key_points)
|
|
78
|
+
? metadata.key_points.map((point) => point.trim()).filter(Boolean)
|
|
79
|
+
: [],
|
|
80
|
+
summary: metadata.summary?.trim() || '',
|
|
81
|
+
slug: metadata.slug?.trim() || existingSlug || '',
|
|
82
|
+
tags: Array.isArray(metadata.tags)
|
|
83
|
+
? metadata.tags.map((tag) => tag.trim()).filter(Boolean)
|
|
84
|
+
: [],
|
|
85
|
+
inferred_date: metadata.inferred_date?.trim() || undefined,
|
|
86
|
+
inferred_lang: metadata.inferred_lang?.trim() || undefined,
|
|
87
|
+
tokens_used: {
|
|
88
|
+
prompt: response.usage.prompt_tokens,
|
|
89
|
+
completion: response.usage.completion_tokens,
|
|
90
|
+
total: response.usage.total_tokens,
|
|
91
|
+
},
|
|
31
92
|
};
|
|
32
|
-
return
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* 构建提取 metadata 的 prompt
|
|
36
|
-
*/
|
|
37
|
-
function buildMetadataPrompt(content) {
|
|
38
|
-
// 限制内容长度以避免 token 超限
|
|
39
|
-
const maxContentLength = Infinity; // 可根据需要调整长度限制
|
|
40
|
-
const truncatedContent = content.length > maxContentLength
|
|
41
|
-
? content.substring(0, maxContentLength) + '... [内容已截断]'
|
|
42
|
-
: content;
|
|
43
|
-
return `请分析以下文档内容,提取以下信息并返回 JSON 格式:
|
|
44
|
-
|
|
45
|
-
文档内容:
|
|
46
|
-
"""
|
|
47
|
-
${truncatedContent}
|
|
48
|
-
"""
|
|
49
|
-
|
|
50
|
-
请提取:
|
|
51
|
-
1. title: 文档的标题(简洁明了,不超过 30 个字)
|
|
52
|
-
2. slug: URL 友好别名(使用小写字母、数字和连字符,仅包含英文和数字)
|
|
53
|
-
3. tags: 关键词列表(3-8 个关键词,使用中文或英文)
|
|
54
|
-
4. description: 文档的简短描述,微摘要(用一句话概括本文核心价值,不超过 100 字符),用于 SEO meta description,社交卡片短描述
|
|
55
|
-
5. summary: 文档中型摘要(用一段话总结文章,包含关键论点和结论,控制在 300 字以内),用于 邮件推送内容,newsletter 介绍
|
|
56
|
-
6. inferred_date: 文档中隐含的创建日期(如果有的话,格式:YYYY-MM-DD,没有就留空字符串)
|
|
57
|
-
7. inferred_lang: 文档使用的语言代码(例如:zh-Hans 表示简体中文,en-US 表示美式英语)
|
|
58
|
-
8. key_points: 文章的关键要点列表(5-10 个要点,简洁明了)
|
|
59
|
-
9. audience: 目标读者描述(简短描述,50 字以内)
|
|
60
|
-
10. short_summary: 文档的超短摘要(用 2-3 句话概括文章主要内容,突出核心观点),用于文章列表页摘要,RSS feed 描述
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
请严格按照以下 JSON 格式返回,不要包含任何其他文本:
|
|
64
|
-
{
|
|
65
|
-
"title": "文档标题",
|
|
66
|
-
"description": "用一句话概括本文核心价值,不超过 100 字符",
|
|
67
|
-
"summary": "中型摘要,用一段话总结文章,包含关键论点和结论",
|
|
68
|
-
"short_summary": "超短摘要,用 2-3 句话概括文章主要内容,突出核心观点",
|
|
69
|
-
"slug": "URL 友好别名",
|
|
70
|
-
"tags": ["关键词1", "关键词2", "关键词3"],
|
|
71
|
-
"inferred_date": "2023-01-01",
|
|
72
|
-
"inferred_lang": "zh-Hans",
|
|
73
|
-
"key_points": ["要点1", "要点2", "要点3"],
|
|
74
|
-
"audience": "目标读者描述"
|
|
75
|
-
}`;
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* 解析 AI 返回的 metadata
|
|
79
|
-
*/
|
|
80
|
-
function parseMetadataResponse(responseContent) {
|
|
81
|
-
try {
|
|
82
|
-
const metadata = JSON.parse(responseContent);
|
|
83
|
-
// 验证和清理数据
|
|
84
|
-
return {
|
|
85
|
-
title: metadata.title?.trim() || '未命名文档',
|
|
86
|
-
description: metadata.description?.trim() || '',
|
|
87
|
-
short_summary: metadata.short_summary?.trim() || '',
|
|
88
|
-
audience: metadata.audience?.trim() || '',
|
|
89
|
-
key_points: Array.isArray(metadata.key_points)
|
|
90
|
-
? metadata.key_points.map((point) => point.trim()).filter(Boolean)
|
|
91
|
-
: [],
|
|
92
|
-
summary: metadata.summary?.trim() || '',
|
|
93
|
-
slug: metadata.slug?.trim() || '',
|
|
94
|
-
tags: Array.isArray(metadata.tags)
|
|
95
|
-
? metadata.tags.map((tag) => tag.trim()).filter(Boolean)
|
|
96
|
-
: [],
|
|
97
|
-
inferred_date: metadata.inferred_date?.trim() || undefined,
|
|
98
|
-
inferred_lang: metadata.inferred_lang?.trim() || 'zh-Hans',
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
catch (error) {
|
|
102
|
-
console.error('❌ Failed to parse AI response:', error, 'Response:', responseContent);
|
|
103
|
-
throw error;
|
|
104
|
-
}
|
|
93
|
+
return result;
|
|
105
94
|
}
|
|
106
95
|
//# sourceMappingURL=extractMetadataFromMarkdown.js.map
|
package/dist/cli.js
CHANGED
|
@@ -5,8 +5,41 @@ const clipanion_1 = require("clipanion");
|
|
|
5
5
|
const dotenv_1 = require("dotenv");
|
|
6
6
|
const pipeline_1 = require("./build/pipeline");
|
|
7
7
|
const version_1 = require("./version");
|
|
8
|
+
const findEntries_1 = require("./findEntries");
|
|
8
9
|
// 加载 .env 文件中的环境变量
|
|
9
10
|
(0, dotenv_1.config)();
|
|
11
|
+
// LsFiles 命令
|
|
12
|
+
class LsFilesCommand extends clipanion_1.Command {
|
|
13
|
+
async execute() {
|
|
14
|
+
try {
|
|
15
|
+
const files = await (0, findEntries_1.findMarkdownEntries)(process.cwd());
|
|
16
|
+
if (files.length === 0) {
|
|
17
|
+
this.context.stdout.write('No markdown files found.\n');
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
files.forEach(file => {
|
|
21
|
+
this.context.stdout.write(`${file}\n`);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
this.context.stderr.write(`❌ Failed to list files: ${error}\n`);
|
|
28
|
+
return 1;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
LsFilesCommand.paths = [['ls-files']];
|
|
33
|
+
LsFilesCommand.usage = clipanion_1.Command.Usage({
|
|
34
|
+
description: 'List all markdown files in the current directory',
|
|
35
|
+
details: `
|
|
36
|
+
This command lists all markdown files in the current directory using git.
|
|
37
|
+
It uses the same logic as the internal findMarkdownEntries function.
|
|
38
|
+
|
|
39
|
+
Examples:
|
|
40
|
+
$ czon ls-files
|
|
41
|
+
`,
|
|
42
|
+
});
|
|
10
43
|
// Build 命令
|
|
11
44
|
class BuildCommand extends clipanion_1.Command {
|
|
12
45
|
constructor() {
|
|
@@ -55,6 +88,7 @@ const cli = new clipanion_1.Cli({
|
|
|
55
88
|
});
|
|
56
89
|
// 注册命令
|
|
57
90
|
cli.register(BuildCommand);
|
|
91
|
+
cli.register(LsFilesCommand);
|
|
58
92
|
// 运行 CLI
|
|
59
93
|
cli.runExit(process.argv.slice(2), {
|
|
60
94
|
...clipanion_1.Cli.defaultContext,
|
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.processTranslations = processTranslations;
|
|
37
37
|
const promises_1 = require("fs/promises");
|
|
38
38
|
const path_1 = __importStar(require("path"));
|
|
39
|
+
const translateMarkdown_1 = require("../ai/translateMarkdown");
|
|
39
40
|
const languages_1 = require("../languages");
|
|
40
41
|
const metadata_1 = require("../metadata");
|
|
41
42
|
const paths_1 = require("../paths");
|
|
@@ -94,9 +95,11 @@ async function processTranslations() {
|
|
|
94
95
|
console.info(`ℹ️ Content unchanged for ${file.path}, skipping translation.`);
|
|
95
96
|
return;
|
|
96
97
|
}
|
|
97
|
-
const
|
|
98
|
+
const translatedResponse = await (0, translateMarkdown_1.translateMarkdown)(sourcePath, content, lang);
|
|
99
|
+
const translatedContent = translatedResponse.choices?.[0].message.content?.trim() || '';
|
|
98
100
|
const translationMeta = ((_a = (file.translations ?? (file.translations = {})))[lang] ?? (_a[lang] = {}));
|
|
99
|
-
translationMeta.content_length = translatedContent.length;
|
|
101
|
+
translationMeta.content_length = translatedContent.length; // 记录翻译后内容长度
|
|
102
|
+
translationMeta.token_used = translatedResponse.usage; // 记录 token 使用情况
|
|
100
103
|
await (0, writeFile_1.writeFile)(targetPath, translatedContent);
|
|
101
104
|
// 存储已增强内容的哈希值
|
|
102
105
|
file.nativeMarkdownHash = hash;
|
|
@@ -32,7 +32,7 @@ const writeFile_1 = require("../utils/writeFile");
|
|
|
32
32
|
* ]);
|
|
33
33
|
*/
|
|
34
34
|
const runOpenCode = (prompt, options) => {
|
|
35
|
-
const model = options?.model ?? 'opencode/
|
|
35
|
+
const model = options?.model ?? 'opencode/gpt-5-nano';
|
|
36
36
|
const signal = options?.signal;
|
|
37
37
|
const cwd = options?.cwd;
|
|
38
38
|
const verbose = metadata_1.MetaData.options.verbose;
|
package/dist/ssg/ContentPage.js
CHANGED
|
@@ -12,6 +12,7 @@ const CZONHeader_1 = require("./components/CZONHeader");
|
|
|
12
12
|
const LanguageSwitcher_1 = require("./components/LanguageSwitcher");
|
|
13
13
|
const Navigator_1 = require("./components/Navigator");
|
|
14
14
|
const PageLayout_1 = require("./layouts/PageLayout");
|
|
15
|
+
const resourceMap_1 = require("./resourceMap");
|
|
15
16
|
const style_1 = require("./style");
|
|
16
17
|
const ContentPage = props => {
|
|
17
18
|
const frontmatter = props.content.frontmatter || {};
|
|
@@ -30,7 +31,7 @@ const ContentPage = props => {
|
|
|
30
31
|
react_1.default.createElement("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
|
|
31
32
|
react_1.default.createElement("title", null, title),
|
|
32
33
|
react_1.default.createElement("meta", { name: "description", content: `tags: ${tags.join(', ')}` }),
|
|
33
|
-
react_1.default.createElement("script", { src: (0,
|
|
34
|
+
react_1.default.createElement("script", { src: (0, resourceMap_1.getResourceUrlFrom)(props.ctx.path, 'tailwindcss.js'), defer: true }),
|
|
34
35
|
react_1.default.createElement("style", null, style_1.style),
|
|
35
36
|
react_1.default.createElement("script", { dangerouslySetInnerHTML: {
|
|
36
37
|
__html: `
|
package/dist/ssg/IndexPage.js
CHANGED
|
@@ -4,7 +4,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.IndexPage = void 0;
|
|
7
|
-
const node_path_1 = require("node:path");
|
|
8
7
|
const react_1 = __importDefault(require("react"));
|
|
9
8
|
const sortBy_1 = require("../utils/sortBy");
|
|
10
9
|
const ContentMeta_1 = require("./components/ContentMeta");
|
|
@@ -12,6 +11,7 @@ const CZONFooter_1 = require("./components/CZONFooter");
|
|
|
12
11
|
const CZONHeader_1 = require("./components/CZONHeader");
|
|
13
12
|
const LanguageSwitcher_1 = require("./components/LanguageSwitcher");
|
|
14
13
|
const PageLayout_1 = require("./layouts/PageLayout");
|
|
14
|
+
const resourceMap_1 = require("./resourceMap");
|
|
15
15
|
const style_1 = require("./style");
|
|
16
16
|
const IndexPage = props => {
|
|
17
17
|
const contents = (0, sortBy_1.toSortedBy)(props.ctx.site.files.filter(f => f.metadata && (!props.category || f.category === props.category)), [
|
|
@@ -50,7 +50,18 @@ const IndexPage = props => {
|
|
|
50
50
|
const title = category || 'All';
|
|
51
51
|
const link = category ? `categories_${category}.html` : 'index.html';
|
|
52
52
|
const isActive = category === props.category;
|
|
53
|
-
|
|
53
|
+
const articlesCount = category
|
|
54
|
+
? props.ctx.site.files.filter(f => f.category === category).length
|
|
55
|
+
: props.ctx.site.files.length;
|
|
56
|
+
return (react_1.default.createElement("span", { key: title }, isActive ? (react_1.default.createElement("span", { className: "font-bold" },
|
|
57
|
+
title,
|
|
58
|
+
" (",
|
|
59
|
+
articlesCount,
|
|
60
|
+
")")) : (react_1.default.createElement("a", { href: link },
|
|
61
|
+
title,
|
|
62
|
+
" (",
|
|
63
|
+
articlesCount,
|
|
64
|
+
")"))));
|
|
54
65
|
}))),
|
|
55
66
|
react_1.default.createElement("div", null, contents.map(file => {
|
|
56
67
|
const metadata = file.metadata;
|
|
@@ -60,7 +71,7 @@ const IndexPage = props => {
|
|
|
60
71
|
react_1.default.createElement("footer", null,
|
|
61
72
|
react_1.default.createElement(LanguageSwitcher_1.LanguageSwitcher, { ctx: props.ctx, lang: props.lang }),
|
|
62
73
|
react_1.default.createElement(CZONFooter_1.CZONFooter, null))), footer: null }),
|
|
63
|
-
react_1.default.createElement("script", { src: (0,
|
|
74
|
+
react_1.default.createElement("script", { src: (0, resourceMap_1.getResourceUrlFrom)(props.ctx.path, 'tailwindcss.js'), defer: true }))));
|
|
64
75
|
// TODO: 渲染多语言首页列表
|
|
65
76
|
// return (
|
|
66
77
|
// <div>
|
package/dist/ssg/resourceMap.js
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.EXTERNAL_RESOURCES = void 0;
|
|
3
|
+
exports.getResourceUrlFrom = exports.EXTERNAL_RESOURCES = void 0;
|
|
4
|
+
const path_1 = require("path");
|
|
4
5
|
exports.EXTERNAL_RESOURCES = [
|
|
5
6
|
{
|
|
6
7
|
name: 'tailwindcss.js',
|
|
7
8
|
url: 'https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4',
|
|
8
9
|
},
|
|
9
10
|
];
|
|
11
|
+
/**
|
|
12
|
+
* 获取资源的相对引用 URL
|
|
13
|
+
* @param path - 当前文件路径 (e.g. `/en-US/index.html`)
|
|
14
|
+
* @param name - 资源名称 (e.g. `tailwindcss.js`)
|
|
15
|
+
*/
|
|
16
|
+
const getResourceUrlFrom = (path, name) => {
|
|
17
|
+
const resource = exports.EXTERNAL_RESOURCES.find(r => r.name === name);
|
|
18
|
+
if (!resource)
|
|
19
|
+
throw new Error(`Resource ${name} not found`);
|
|
20
|
+
return (0, path_1.relative)((0, path_1.dirname)(path), `/assets/${resource.name}`);
|
|
21
|
+
};
|
|
22
|
+
exports.getResourceUrlFrom = getResourceUrlFrom;
|
|
10
23
|
//# sourceMappingURL=resourceMap.js.map
|
package/package.json
CHANGED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
const node_test_1 = require("node:test");
|
|
7
|
-
const node_assert_1 = __importDefault(require("node:assert"));
|
|
8
|
-
const opencode_1 = require("./opencode");
|
|
9
|
-
(0, node_test_1.describe)('runOpenCode', () => {
|
|
10
|
-
(0, node_test_1.it)('should return response from OpenCode', async () => {
|
|
11
|
-
const result = await (0, opencode_1.runOpenCode)('hello');
|
|
12
|
-
node_assert_1.default.ok(result.length > 0, 'Should return non-empty response');
|
|
13
|
-
});
|
|
14
|
-
(0, node_test_1.it)('should use default model when not specified', async () => {
|
|
15
|
-
const result = await (0, opencode_1.runOpenCode)('test prompt');
|
|
16
|
-
node_assert_1.default.ok(typeof result === 'string');
|
|
17
|
-
});
|
|
18
|
-
(0, node_test_1.it)('should accept custom model in options', async () => {
|
|
19
|
-
const options = { model: 'opencode/glm-4.7-free' };
|
|
20
|
-
const result = await (0, opencode_1.runOpenCode)('test', options);
|
|
21
|
-
node_assert_1.default.ok(typeof result === 'string');
|
|
22
|
-
});
|
|
23
|
-
(0, node_test_1.it)('should handle abort signal', async () => {
|
|
24
|
-
const controller = new AbortController();
|
|
25
|
-
setTimeout(() => controller.abort(), 10);
|
|
26
|
-
await node_assert_1.default.rejects(async () => await (0, opencode_1.runOpenCode)('long running prompt', { signal: controller.signal }), /OpenCode execution was aborted/);
|
|
27
|
-
});
|
|
28
|
-
(0, node_test_1.it)('should accept cwd option', async () => {
|
|
29
|
-
const options = { cwd: '/tmp' };
|
|
30
|
-
const result = await (0, opencode_1.runOpenCode)('test', options);
|
|
31
|
-
node_assert_1.default.ok(typeof result === 'string');
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
//# sourceMappingURL=opencode.test.js.map
|