czon 0.5.2 → 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/cli.js +34 -0
- 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
|
+
- "生成我们内容的幽默和严肃版本"
|
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,
|
|
@@ -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
|