@chtholly-fan/chtholly-notes-style 0.0.0-stage → 1.0.0
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/README.md +204 -2
- package/cordis.patch.yml +14 -0
- package/index.js +142 -0
- package/package.json +37 -4
- package/scripts/install-dsh-skill.mjs +346 -0
- package/skills/chtholly-notes-style/SKILL.md +58 -0
- package/skills/chtholly-notes-style/agents/openai.yaml +4 -0
- package/skills/chtholly-notes-style/references/entry-patterns.md +81 -0
- package/skills/chtholly-notes-style/references/style-profile.md +151 -0
package/README.md
CHANGED
|
@@ -1,3 +1,205 @@
|
|
|
1
|
-
#
|
|
1
|
+
# chtholly-notes-style
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
将 C++ 开发中的优化、算法和 debug 记录整理成个人中文博客风格。
|
|
4
|
+
|
|
5
|
+
这个 skill 重点保留问题推进过程、个人判断和适度口语,避免把工作记录写成整齐的报告模板。适用于需要按既有大学博客风格重写 C++ 工作笔记的场景,不用于正式技术文档、代码审查或与博客写作无关的编码任务。
|
|
6
|
+
|
|
7
|
+
## 安装
|
|
8
|
+
|
|
9
|
+
四种方式装的是同一份 `skills/chtholly-notes-style`,按场景挑一个。
|
|
10
|
+
|
|
11
|
+
### 用 npx 安装(推荐)
|
|
12
|
+
|
|
13
|
+
不需要克隆仓库,也不需要 npm 账号——包直接从 GitHub 解析:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx --yes github:ChthollyFan/chtholly-notes-style
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
默认装到 dsh 的用户级 skill 根目录(`$DSH_HOME/skills`,未设置时为 `~/.dsh/skills`)。常用参数:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx --yes github:ChthollyFan/chtholly-notes-style --copy # 复制副本,而不是建链接
|
|
23
|
+
npx --yes github:ChthollyFan/chtholly-notes-style --target-dir <skill 根> # 换目标目录
|
|
24
|
+
npx --yes github:ChthollyFan/chtholly-notes-style --force # 已装过副本时覆盖,旧的改名备份
|
|
25
|
+
npx --yes github:ChthollyFan/chtholly-notes-style uninstall # 卸载
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
npx 会把包体放在 npm 的临时缓存里(`_npx/<hash>/node_modules`),缓存回收后链接就会悬挂,所以这种场景脚本自动改用复制。想要 `git pull` 之后立即生效的链接模式,用下面的仓库内安装。
|
|
29
|
+
|
|
30
|
+
包发布到 npm registry 之后(见「发布为 npm 包」)可以简写成:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx --yes @chtholly-fan/chtholly-notes-style
|
|
34
|
+
npm i -g @chtholly-fan/chtholly-notes-style && chtholly-notes-style
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
npm 装的是快照:更新要重跑命令并加 `--force`(旧副本改名成 `<名字>.bak-<时间戳>`,不会删除),链接模式则由 `git pull` 直接生效。
|
|
38
|
+
|
|
39
|
+
### 用 dsh 插件安装(dsh bundle 通道)
|
|
40
|
+
|
|
41
|
+
包在 `package.json` 里声明了 `dsh.bundle`,因此可以直接走 dsh 自己的插件安装命令(包名带 `@chtholly-fan/` scope,技能名本身仍是 `chtholly-notes-style`):
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
dsh plugin --profile web add @chtholly-fan/chtholly-notes-style
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
dsh 会把包装进该 profile 的 `node_modules`,并登记到 `dsh.profile.bundles`;profile 下次启动时,包内的 `index.js` 会把 `skills/chtholly-notes-style` 植入 `$DSH_HOME/skills`(所有 profile 共享的用户级 skill 根),所以 **web 与 desktop 装一次即可共用**。
|
|
48
|
+
|
|
49
|
+
- 来源不限于 registry,GitHub 与本地 tarball 同样可以:
|
|
50
|
+
`dsh plugin --profile web add ./chtholly-fan-chtholly-notes-style-1.0.0.tgz`
|
|
51
|
+
- desktop profile 由桌面应用独占管理:`dsh plugin --profile desktop add ...` 只在桌面版自带的 dsh 下放行,且应用运行时会撞上 profile 的文件锁,建议先完全退出应用,或改用应用内的插件市场安装。
|
|
52
|
+
- 生效时机:web 端要等下一次启动 profile;desktop 端的 `patchReload: live` 是否热加载尚未实测。
|
|
53
|
+
- 如果 `$DSH_HOME/skills/chtholly-notes-style` 已经存在、又不带本插件的版本戳(例如下面「仓库内安装」建立的目录链接),插件会跳过并在日志里说明,不会覆盖你现有的目录。
|
|
54
|
+
|
|
55
|
+
卸载分两步:dsh 侧移除依赖与 bundles 条目,植入的副本由脚本清理(只改名备份,不删除内容):
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
dsh plugin --profile web remove @chtholly-fan/chtholly-notes-style
|
|
59
|
+
npx --yes @chtholly-fan/chtholly-notes-style uninstall --force
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Codex 官方脚本
|
|
63
|
+
|
|
64
|
+
使用 Codex 自带的 skill 安装脚本:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
python install-skill-from-github.py \
|
|
68
|
+
--repo ChthollyFan/chtholly-notes-style \
|
|
69
|
+
--path skills/chtholly-notes-style
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
也可以直接传入 GitHub 目录地址:
|
|
73
|
+
|
|
74
|
+
```text
|
|
75
|
+
https://github.com/ChthollyFan/chtholly-notes-style/tree/main/skills/chtholly-notes-style
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
安装完成后,会在 `$CODEX_HOME/skills/chtholly-notes-style` 下生成对应目录,可以通过 `$chtholly-notes-style` 调用。
|
|
79
|
+
|
|
80
|
+
## 在 dsh 中使用
|
|
81
|
+
|
|
82
|
+
dsh(DeepSeek Harness)的本地文件系统提供方直接扫描 `<root>/<name>/SKILL.md`,一层深、不递归,格式与 Codex 的 Agent Skills 一致,因此同一个 bundle 不需要改任何内容就能两端共用。
|
|
83
|
+
|
|
84
|
+
### 发现路径
|
|
85
|
+
|
|
86
|
+
dsh 按下面的优先级发现本地 skill,本仓库的 `skills/chtholly-notes-style` 放在任意一个根下都能被发现:
|
|
87
|
+
|
|
88
|
+
| 优先级 | 来源 | 路径 |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| 100 | 项目 | `<项目根>/.dsh/skills` |
|
|
91
|
+
| 200 | 项目 | `<项目根>/.agents/skills` |
|
|
92
|
+
| 300 | 自定义 | `customSkillDirs` 配置项 |
|
|
93
|
+
| 400 | 用户 | `~/.dsh/skills` |
|
|
94
|
+
| 500 | 用户 | `~/.agents/skills` |
|
|
95
|
+
|
|
96
|
+
项目根是最近包含 `.git` 的祖先目录。推荐装到用户级 `~/.dsh/skills`,所有会话都能用。
|
|
97
|
+
|
|
98
|
+
### 安装
|
|
99
|
+
|
|
100
|
+
Windows PowerShell:
|
|
101
|
+
|
|
102
|
+
```powershell
|
|
103
|
+
node scripts/install-dsh-skill.mjs
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
脚本会在 `~/.dsh/skills/chtholly-notes-style` 建立指向本仓库的目录链接,`git pull` 之后 dsh 侧立即看到新内容。Windows 下用的是 junction(普通权限即可创建;Git Bash 的 `ln -s` 在没有开启开发者模式时会退化成复制)。想复制一份独立副本而不是链接:
|
|
107
|
+
|
|
108
|
+
```powershell
|
|
109
|
+
node scripts/install-dsh-skill.mjs --copy
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
卸载(只删除指向本仓库的链接,链向的内容不动):
|
|
113
|
+
|
|
114
|
+
```powershell
|
|
115
|
+
node scripts/install-dsh-skill.mjs uninstall
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
手动安装等价于:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
mkdir -p ~/.dsh/skills
|
|
122
|
+
ln -s "$(pwd)/skills/chtholly-notes-style" ~/.dsh/skills/chtholly-notes-style # macOS / Linux
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
```cmd
|
|
126
|
+
:: Windows:junction 不需要管理员权限
|
|
127
|
+
mklink /J "%USERPROFILE%\.dsh\skills\chtholly-notes-style" "<仓库路径>\skills\chtholly-notes-style"
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
脚本默认使用 `$DSH_HOME`,没有该变量时回退到 `~/.dsh`;需要写到别处时用 `--target-dir <skill 根目录>`。目标已存在且不是本仓库的链接时会拒绝安装,`--force` 也只是把原内容重命名为 `<名字>.bak-<时间戳>`,不会删除。
|
|
131
|
+
|
|
132
|
+
### 调用
|
|
133
|
+
|
|
134
|
+
- 在 dsh Desktop / Web 的输入框里打 `/`,选择 `chtholly-notes-style`,输入框会插入 `/chtholly-notes-style `。
|
|
135
|
+
- 也可以直接描述任务,由模型侧的持久 skill 目录(或 `skill_search` / `skill_load`)自动加载。
|
|
136
|
+
- 装完需要新开一个会话,dsh 在会话开始和目录变化时刷新 skill 目录。
|
|
137
|
+
|
|
138
|
+
### 注意事项
|
|
139
|
+
|
|
140
|
+
- dsh 要求 `name` 匹配 `^[a-z0-9]+(?:-[a-z0-9]+)*$`、`description` 非空,当前 frontmatter 已满足。
|
|
141
|
+
- 不要往 `SKILL.md` 里加 `whenToUse`、`user-invocable` 等 dsh 专有字段:Codex 只接受 `name`、`description`、`license`、`allowed-tools`、`metadata`,多出的键会让 Codex 直接拒绝整个 skill。
|
|
142
|
+
- dsh 的目录消息只渲染 `name` 和 `description`(描述截断到 500 字符),`references/` 通过资源目录提示可达,正文里保持相对路径引用即可。
|
|
143
|
+
- `agents/openai.yaml` 是 Codex 的界面元数据,dsh 不读取也不会因此报错,两份可以共存。
|
|
144
|
+
|
|
145
|
+
## 发布为 npm 包
|
|
146
|
+
|
|
147
|
+
`package.json` 已经带好 `bin`(指向 `scripts/install-dsh-skill.mjs`)、`dsh.bundle`(指向 `cordis.patch.yml`)和 `files` 白名单(打包 `scripts/`、`skills/`、`index.js` 与 `cordis.patch.yml`)。`docs/` 是本地写作草稿,既被 `.gitignore` 忽略,也不在 `files` 里,不会进包。
|
|
148
|
+
|
|
149
|
+
`private` 字段已移除(发布前提),直接发布即可:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
npm pack --dry-run # 确认真实进包的文件
|
|
153
|
+
npm publish --access public
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
发布后的调用方式见上面的「用 npx 安装」。包体是快照:改动 `SKILL.md` 或 `references/` 之后要重新发布并升版本号,用户才能拿到新内容。
|
|
157
|
+
|
|
158
|
+
## 目录结构
|
|
159
|
+
|
|
160
|
+
```text
|
|
161
|
+
package.json # npm 包元数据:bin 指向安装脚本,dsh.bundle 指向 patch
|
|
162
|
+
cordis.patch.yml # dsh bundle 的 patch 层,插入本包的 host 插件行
|
|
163
|
+
index.js # 该插件:把随包技能植入 $DSH_HOME/skills
|
|
164
|
+
skills/
|
|
165
|
+
└── chtholly-notes-style/
|
|
166
|
+
├── SKILL.md
|
|
167
|
+
├── agents/
|
|
168
|
+
│ └── openai.yaml
|
|
169
|
+
└── references/
|
|
170
|
+
├── entry-patterns.md
|
|
171
|
+
└── style-profile.md
|
|
172
|
+
scripts/
|
|
173
|
+
└── install-dsh-skill.mjs
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`SKILL.md` 提供主要工作流,`references/` 中的文件只在需要判断文章骨架或具体写作风格时读取。`scripts/install-dsh-skill.mjs` 负责 dsh 侧的安装、卸载与 frontmatter 自检,同时是 npm 包的 `bin` 入口。
|
|
177
|
+
|
|
178
|
+
## 使用范围
|
|
179
|
+
|
|
180
|
+
- 根据日志、源码、diff 和测试记录整理 C++ 调试过程。
|
|
181
|
+
- 改写性能优化、算法实现和排障笔记。
|
|
182
|
+
- 保持用户已有博客的标题、叙述、代码展示和收尾习惯。
|
|
183
|
+
- 对没有证据的根因、失败过程、性能数字和验证结果不进行补写。
|
|
184
|
+
|
|
185
|
+
## 写作约定
|
|
186
|
+
|
|
187
|
+
- 材料里出现优化,或修 bug 时引入了和常规做法不同的算法,成文前先用 `ask_user_question` 确认定位。默认按「介绍一种新的优化/算法方式」写;只有用户说明是针对项目中某个方法的优化记录时,才按「旧做法与瓶颈 → 新做法 → 边界 → 代价」写;分辨不出就先问。
|
|
188
|
+
- 算法和优化类文章用二级小标题分段,每个关键步骤写成「文字说明 + 对应代码」;短代码直接进正文,完整实现和长日志才用 `<details>` 折起。
|
|
189
|
+
- 有推导价值时补一段和常规做法的复杂度对比:总操作量、依赖深度、误差界和内存访问分开算,前提和适用规模写清楚。
|
|
190
|
+
- 算法和优化类文章在复杂度和误差之后、完整实现之前加一节适用场景,写清划算的规模、算子性质和不适用的边界。
|
|
191
|
+
- 单独成行的公式用 `$$ ... $$` 居中,正文里的数学量用 `$ ... $` 行内公式。
|
|
192
|
+
- 记录算法、数据结构或知识点这类科普性内容前,先用联网工具核对来源:优先论文原文、规范、官方文档和上游 changelog,整理性资料只用于交叉印证;复杂度与误差结论要自己独立推导一遍,找不到可靠来源或来源互相矛盾的内容按猜测语气写。
|
|
193
|
+
- 引用外部资料优先一手来源,正文用 `^[url]` 内联脚注,脚注只放 URL、闭合括号后留一个半角空格;不设末尾的参考文献节。
|
|
194
|
+
- 不写「这个实现后来单独放进了一个头文件,只做这一件事」这类只交代代码去向的过渡句。
|
|
195
|
+
- 要验证代码先探本机工具链,有编译器就在本机编,没有才用在线服务,内部代码不投;每次验证记四项(在哪编、编译器和版本、标准、结果),每个程序后面跟一行编译环境,没实跑过的代码不写成跑过。
|
|
196
|
+
|
|
197
|
+
## 校验
|
|
198
|
+
|
|
199
|
+
使用 skill-creator 提供的校验脚本检查 `SKILL.md`:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
python quick_validate.py skills/chtholly-notes-style
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
dsh 侧的校验由安装脚本自己完成,不需要额外命令:`node scripts/install-dsh-skill.mjs` 会检查 `SKILL.md` 是否存在、`name` 是否为 kebab-case、`description` 是否为空,并提示 frontmatter 里是否混入了 Codex 白名单之外的键。
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# dsh bundle patch 层:只插入本包的 host 插件行。
|
|
2
|
+
#
|
|
3
|
+
# 为什么不在这里直接把技能目录挂给 skill-filesystem:bundle 的 patch 是静态
|
|
4
|
+
# YAML,而包的真实安装路径取决于它被装在哪个 profile 的 node_modules 下,
|
|
5
|
+
# 静态文件算不出绝对路径。改由这个插件在 profile 启动时自己定位包内目录,
|
|
6
|
+
# 再把 skills/chtholly-notes-style 植入 $DSH_HOME/skills(dsh 默认扫描的
|
|
7
|
+
# 用户级 skill 根),因此 web 与 desktop 装一次即可共用同一份技能。
|
|
8
|
+
#
|
|
9
|
+
# name 必须是 npm 上安装后的包名:loader 从 profile 的 node_modules 解析它。
|
|
10
|
+
# 该包不能声明 @deepseek-ai/dsh* 的 peerDependencies,否则会被宿主的
|
|
11
|
+
# 兼容性检查拒载。
|
|
12
|
+
- insert:
|
|
13
|
+
- id: chtholly-notes-style
|
|
14
|
+
name: '@chtholly-fan/chtholly-notes-style'
|
package/index.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// chtholly-notes-style —— dsh bundle 的技能植入插件。
|
|
2
|
+
//
|
|
3
|
+
// dsh 按 `<root>/<name>/SKILL.md` 发现技能,其中用户级根 `$DSH_HOME/skills`
|
|
4
|
+
// 被所有 profile 共享。本插件作为 bundle 的一层,在 profile 启动时把包内
|
|
5
|
+
// `skills/chtholly-notes-style` 复制到该根下,于是 `dsh plugin add` 装一次,
|
|
6
|
+
// web、desktop 以及之后新建的 profile 都能看到这个技能。
|
|
7
|
+
//
|
|
8
|
+
// 用"植入 + 版本戳"而不是在 patch 里直接引用包内目录:patch 是静态 YAML,
|
|
9
|
+
// 而包的实际安装路径随 profile 变化,只有 apply() 运行时才能确定。
|
|
10
|
+
// "目标没有我们的版本戳就拒绝触碰"的约定来自 @ziduup/dsh-programming-mode,
|
|
11
|
+
// 用来保护用户手写的同名技能目录,以及仓库内安装脚本建立的目录链接。
|
|
12
|
+
|
|
13
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
14
|
+
import { homedir } from 'node:os'
|
|
15
|
+
import { dirname, join, resolve } from 'node:path'
|
|
16
|
+
import { fileURLToPath } from 'node:url'
|
|
17
|
+
|
|
18
|
+
/** 技能标识:必须与 SKILL.md 的 frontmatter name 以及目录名一致 */
|
|
19
|
+
const SKILL_NAME = 'chtholly-notes-style'
|
|
20
|
+
|
|
21
|
+
/** 版本戳文件名:标记该目录由本插件植入,据此决定是覆盖还是跳过 */
|
|
22
|
+
const STAMP_FILENAME = '.chtholly-notes-style.planted.json'
|
|
23
|
+
|
|
24
|
+
/** 本包根目录,用于定位随包分发的 skills/ */
|
|
25
|
+
const PACKAGE_DIR = dirname(fileURLToPath(import.meta.url))
|
|
26
|
+
|
|
27
|
+
/** 随包分发的技能源目录 */
|
|
28
|
+
const SOURCE_DIR = join(PACKAGE_DIR, 'skills', SKILL_NAME)
|
|
29
|
+
|
|
30
|
+
/** cordis 插件名,显示在 loader 日志里 */
|
|
31
|
+
export const name = 'chtholly-notes-style-plant'
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @brief 计算 dsh 用户级 skill 根目录
|
|
35
|
+
* @param explicit 显式指定的根目录,可为空
|
|
36
|
+
* @return skill 根目录的绝对路径
|
|
37
|
+
* @note 与 dsh 自身的回退规则保持一致:优先 $DSH_HOME,其次 ~/.dsh
|
|
38
|
+
*/
|
|
39
|
+
function resolveSkillRoot(explicit) {
|
|
40
|
+
if (typeof explicit === 'string' && explicit.trim() !== '') return resolve(explicit.trim())
|
|
41
|
+
const envHome = typeof process.env.DSH_HOME === 'string' && process.env.DSH_HOME.trim() !== ''
|
|
42
|
+
? process.env.DSH_HOME.trim()
|
|
43
|
+
: join(homedir(), '.dsh')
|
|
44
|
+
return join(resolve(envHome), 'skills')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @brief 读取本包版本,作为植入内容的版本标记
|
|
49
|
+
* @return 版本字符串,读取失败时回退为 0.0.0
|
|
50
|
+
*/
|
|
51
|
+
function readOwnVersion() {
|
|
52
|
+
try {
|
|
53
|
+
const manifest = JSON.parse(readFileSync(join(PACKAGE_DIR, 'package.json'), 'utf8'))
|
|
54
|
+
return String(manifest.version ?? '0.0.0')
|
|
55
|
+
} catch {
|
|
56
|
+
return '0.0.0'
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @brief 读取目标目录里的版本戳
|
|
62
|
+
* @param targetDir 已植入的技能目录
|
|
63
|
+
* @return 版本戳对象,缺失或格式不符时返回 null
|
|
64
|
+
* @note 返回 null 表示该目录不是本插件植入的,调用方必须跳过
|
|
65
|
+
*/
|
|
66
|
+
function readStamp(targetDir) {
|
|
67
|
+
try {
|
|
68
|
+
const stamp = JSON.parse(readFileSync(join(targetDir, STAMP_FILENAME), 'utf8'))
|
|
69
|
+
return typeof stamp?.version === 'string' ? stamp : null
|
|
70
|
+
} catch {
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @brief 写入版本戳,记录植入来源、版本与时间
|
|
77
|
+
* @param targetDir 已植入的技能目录
|
|
78
|
+
* @param version 本次植入的包版本
|
|
79
|
+
*/
|
|
80
|
+
function writeStamp(targetDir, version) {
|
|
81
|
+
const stamp = { version, source: 'chtholly-notes-style', plantedAt: new Date().toISOString() }
|
|
82
|
+
writeFileSync(join(targetDir, STAMP_FILENAME), JSON.stringify(stamp, null, 2) + '\n')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @brief 把随包技能植入用户级 skill 根
|
|
87
|
+
* @param options 可选覆盖项:sourceDir(技能源目录)、targetRoot(skill 根)、version(版本号)
|
|
88
|
+
* @return 结果对象,action 取值 planted / updated / unchanged / skipped / error
|
|
89
|
+
* @note 目标已存在但没有本插件的版本戳时一律跳过,不覆盖用户手写目录或仓库内
|
|
90
|
+
* 安装脚本建立的目录链接;升级时用 force 覆盖同名文件,但不会删除目标里
|
|
91
|
+
* 多出的旧文件(避免误删用户放进该目录的内容)
|
|
92
|
+
*/
|
|
93
|
+
export function plantSkill(options = {}) {
|
|
94
|
+
const sourceDir = resolve(options.sourceDir ?? SOURCE_DIR)
|
|
95
|
+
const version = String(options.version ?? readOwnVersion())
|
|
96
|
+
const targetRoot = resolveSkillRoot(options.targetRoot)
|
|
97
|
+
const targetDir = join(targetRoot, SKILL_NAME)
|
|
98
|
+
|
|
99
|
+
if (!existsSync(sourceDir)) {
|
|
100
|
+
return { action: 'error', reason: `随包技能目录不存在:${sourceDir}`, targetDir }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!existsSync(targetDir)) {
|
|
104
|
+
mkdirSync(targetRoot, { recursive: true })
|
|
105
|
+
cpSync(sourceDir, targetDir, { recursive: true })
|
|
106
|
+
writeStamp(targetDir, version)
|
|
107
|
+
return { action: 'planted', version, targetDir }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const stamped = readStamp(targetDir)
|
|
111
|
+
if (stamped === null) {
|
|
112
|
+
return {
|
|
113
|
+
action: 'skipped',
|
|
114
|
+
reason: `${targetDir} 已存在且没有本插件的版本戳,未做任何修改`,
|
|
115
|
+
targetDir,
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (stamped.version !== version) {
|
|
120
|
+
cpSync(sourceDir, targetDir, { recursive: true, force: true })
|
|
121
|
+
writeStamp(targetDir, version)
|
|
122
|
+
return { action: 'updated', from: stamped.version, to: version, version, targetDir }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { action: 'unchanged', version, targetDir }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @brief cordis 插件入口:profile 启动时植入技能
|
|
130
|
+
* @note 不需要 cordis 上下文:本插件只按 $DSH_HOME 约定写文件,不注册任何服务。
|
|
131
|
+
* 所有异常都在这里吞掉——插件不能让宿主的 profile 启动失败。
|
|
132
|
+
*/
|
|
133
|
+
export function apply() {
|
|
134
|
+
try {
|
|
135
|
+
const result = plantSkill()
|
|
136
|
+
const detail = result.reason
|
|
137
|
+
?? (result.action === 'updated' ? `${result.from} -> ${result.to}` : result.version ?? '')
|
|
138
|
+
console.log(`[chtholly-notes-style] ${result.action}${detail ? `:${detail}` : ''}`)
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.log(`[chtholly-notes-style] 植入技能失败:${error instanceof Error ? error.message : String(error)}`)
|
|
141
|
+
}
|
|
142
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chtholly-fan/chtholly-notes-style",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "把 C++ 开发中的优化、算法与调试记录改写成个人中文博客风格的 Agent Skill(Codex / dsh 双端可用)",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"agent-skill",
|
|
7
|
+
"codex",
|
|
8
|
+
"dsh",
|
|
9
|
+
"writing",
|
|
10
|
+
"blog",
|
|
11
|
+
"cpp"
|
|
12
|
+
],
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "index.js",
|
|
15
|
+
"bin": {
|
|
16
|
+
"chtholly-notes-style": "scripts/install-dsh-skill.mjs"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"scripts",
|
|
20
|
+
"skills",
|
|
21
|
+
"index.js",
|
|
22
|
+
"cordis.patch.yml"
|
|
23
|
+
],
|
|
24
|
+
"dsh": {
|
|
25
|
+
"bundle": {
|
|
26
|
+
"patch": "./cordis.patch.yml"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/ChthollyFan/chtholly-notes-style.git"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"install-skill": "node scripts/install-dsh-skill.mjs"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @brief 把 chtholly-notes-style skill 安装到 dsh 的用户级 skill 根目录
|
|
4
|
+
*
|
|
5
|
+
* dsh 的本地文件系统提供方按 `<root>/<name>/SKILL.md` 发现 skill,用户级根目录是
|
|
6
|
+
* `$DSH_HOME/skills`(默认 `~/.dsh/skills`),且只扫描一层,不递归子目录。
|
|
7
|
+
* 本脚本把目标根目录下的 `chtholly-notes-style` 建成指向本仓库
|
|
8
|
+
* `skills/chtholly-notes-style` 的目录链接,这样 `git pull` 之后 dsh 立即看到新内容。
|
|
9
|
+
*
|
|
10
|
+
* Windows 下使用 junction 而不是符号链接:junction 由普通权限即可创建,
|
|
11
|
+
* 而 Git Bash 的 `ln -s` 在没有开启开发者模式时会退化成复制。
|
|
12
|
+
*
|
|
13
|
+
* 用法:
|
|
14
|
+
* node scripts/install-dsh-skill.mjs # 在默认根下建链接
|
|
15
|
+
* node scripts/install-dsh-skill.mjs --copy # 复制副本而不是链接
|
|
16
|
+
* node scripts/install-dsh-skill.mjs --force # 已有同名条目时先重命名备份
|
|
17
|
+
* node scripts/install-dsh-skill.mjs --target-dir <目录> # 指定 skill 根目录
|
|
18
|
+
* node scripts/install-dsh-skill.mjs uninstall # 卸载,只删指向本仓库的链接
|
|
19
|
+
*
|
|
20
|
+
* 作为 npm 包时 package.json 的 bin 指向本文件,所以下面这些写法等价:
|
|
21
|
+
* npx --yes github:ChthollyFan/chtholly-notes-style
|
|
22
|
+
* npm i -g chtholly-notes-style && chtholly-notes-style
|
|
23
|
+
* npx 把包体放在临时缓存里,缓存回收后链接会悬挂,因此这种场景自动改成复制。
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
cpSync,
|
|
28
|
+
existsSync,
|
|
29
|
+
lstatSync,
|
|
30
|
+
mkdirSync,
|
|
31
|
+
readFileSync,
|
|
32
|
+
readlinkSync,
|
|
33
|
+
renameSync,
|
|
34
|
+
rmdirSync,
|
|
35
|
+
statSync,
|
|
36
|
+
symlinkSync,
|
|
37
|
+
unlinkSync,
|
|
38
|
+
} from 'node:fs'
|
|
39
|
+
import { homedir } from 'node:os'
|
|
40
|
+
import { dirname, join, relative, resolve } from 'node:path'
|
|
41
|
+
import { fileURLToPath } from 'node:url'
|
|
42
|
+
import process from 'node:process'
|
|
43
|
+
|
|
44
|
+
/** 目录名与 frontmatter 中的 name 必须一致,dsh 用它作为 skill 标识 */
|
|
45
|
+
const SKILL_NAME = 'chtholly-notes-style'
|
|
46
|
+
|
|
47
|
+
/** dsh 要求 kebab-case,对应 dsh-skill 中的 SKILL_NAME 正则 */
|
|
48
|
+
const SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
|
|
49
|
+
|
|
50
|
+
/** codex 只接受这些 frontmatter 键,出现其他键会让 codex 拒绝整个 skill */
|
|
51
|
+
const CODEX_ALLOWED_KEYS = new Set(['name', 'description', 'license', 'allowed-tools', 'metadata'])
|
|
52
|
+
|
|
53
|
+
/** 仓库根目录,即本脚本所在目录的上一级 */
|
|
54
|
+
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
55
|
+
|
|
56
|
+
/** 本仓库中 skill bundle 的源目录 */
|
|
57
|
+
const SOURCE_DIR = join(REPO_ROOT, 'skills', SKILL_NAME)
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @brief 解析命令行参数
|
|
61
|
+
* @param argv 去掉 node 与脚本路径后的参数列表
|
|
62
|
+
* @return 解析出的选项对象
|
|
63
|
+
* @throw Error 出现未知参数时抛出
|
|
64
|
+
*/
|
|
65
|
+
function parseArgs(argv) {
|
|
66
|
+
const options = { command: 'install', force: false, help: false, mode: 'auto', targetRoot: undefined }
|
|
67
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
68
|
+
const arg = argv[index]
|
|
69
|
+
if (arg === 'install' || arg === 'uninstall') {
|
|
70
|
+
options.command = arg
|
|
71
|
+
} else if (arg === '--copy') {
|
|
72
|
+
options.mode = 'copy'
|
|
73
|
+
} else if (arg === '--link') {
|
|
74
|
+
options.mode = 'link'
|
|
75
|
+
} else if (arg === '--force') {
|
|
76
|
+
options.force = true
|
|
77
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
78
|
+
options.help = true
|
|
79
|
+
} else if (arg === '--target-dir') {
|
|
80
|
+
// 下一个参数是 skill 根目录,例如 ~/.dsh/skills
|
|
81
|
+
options.targetRoot = argv[index + 1]
|
|
82
|
+
index += 1
|
|
83
|
+
} else {
|
|
84
|
+
throw new Error(`未知参数:${arg}`)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return options
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 打印用法说明 */
|
|
91
|
+
function printUsage() {
|
|
92
|
+
console.log(`用法:node scripts/install-dsh-skill.mjs [install|uninstall] [--copy|--link] [--force] [--target-dir <目录>]
|
|
93
|
+
|
|
94
|
+
install 安装 skill,默认动作
|
|
95
|
+
uninstall 卸载:只删除指向本仓库的链接
|
|
96
|
+
--copy 复制一份独立副本,而不是建立目录链接
|
|
97
|
+
--link 强制建立目录链接;在 npm 临时目录里运行时默认改成复制
|
|
98
|
+
--force 目标已存在且不是本仓库的链接时,先重命名备份再安装/卸载
|
|
99
|
+
--target-dir <目录> 指定 skill 根目录,默认 $DSH_HOME/skills 或 ~/.dsh/skills`)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @brief 计算 dsh 用户级 skill 根目录
|
|
104
|
+
* @param explicit 命令行显式指定的根目录,可为空
|
|
105
|
+
* @return skill 根目录的绝对路径
|
|
106
|
+
* @note 未显式指定时优先读 $DSH_HOME,其次回退到 ~/.dsh
|
|
107
|
+
*/
|
|
108
|
+
function resolveSkillRoot(explicit) {
|
|
109
|
+
if (explicit !== undefined && explicit !== '') return resolve(explicit)
|
|
110
|
+
const dshHome = process.env.DSH_HOME !== undefined && process.env.DSH_HOME !== ''
|
|
111
|
+
? process.env.DSH_HOME
|
|
112
|
+
: join(homedir(), '.dsh')
|
|
113
|
+
return join(resolve(dshHome), 'skills')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @brief 读取 SKILL.md 的 frontmatter,只取顶层键
|
|
118
|
+
* @param skillFile SKILL.md 的绝对路径
|
|
119
|
+
* @return 顶层键值组成的 Map
|
|
120
|
+
* @throw Error frontmatter 缺失或格式不完整时抛出
|
|
121
|
+
* @note 这里刻意只做轻量解析,不引入 YAML 依赖;缩进的嵌套键会被跳过
|
|
122
|
+
*/
|
|
123
|
+
function readFrontmatterFields(skillFile) {
|
|
124
|
+
const text = readFileSync(skillFile, 'utf8')
|
|
125
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text)
|
|
126
|
+
if (match === null) throw new Error(`${skillFile} 缺少 YAML frontmatter`)
|
|
127
|
+
|
|
128
|
+
const fields = new Map()
|
|
129
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
130
|
+
const entry = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/.exec(line)
|
|
131
|
+
if (entry !== null) fields.set(entry[1], entry[2].trim().replace(/^["']|["']$/g, ''))
|
|
132
|
+
}
|
|
133
|
+
return fields
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @brief 校验 skill 是否满足 dsh 与 codex 的双端约束
|
|
138
|
+
* @param skillDir skill bundle 目录
|
|
139
|
+
* @return 包含 problems(阻塞问题)与 foreignKeys(codex 不认识的键)的对象
|
|
140
|
+
* @throw Error SKILL.md 不存在时抛出
|
|
141
|
+
*/
|
|
142
|
+
function verifySkill(skillDir) {
|
|
143
|
+
const skillFile = join(skillDir, 'SKILL.md')
|
|
144
|
+
if (!existsSync(skillFile)) throw new Error(`${skillFile} 不存在,skill bundle 不完整`)
|
|
145
|
+
|
|
146
|
+
const fields = readFrontmatterFields(skillFile)
|
|
147
|
+
const name = fields.get('name')
|
|
148
|
+
const description = fields.get('description')
|
|
149
|
+
const problems = []
|
|
150
|
+
|
|
151
|
+
if (name !== SKILL_NAME) problems.push(`frontmatter 的 name 应为 "${SKILL_NAME}",实际为 "${name ?? ''}"`)
|
|
152
|
+
if (name === undefined || !SKILL_NAME_PATTERN.test(name)) problems.push('name 不符合 dsh 要求的 kebab-case')
|
|
153
|
+
if (description === undefined || description === '') problems.push('description 为空,dsh 会忽略该 skill')
|
|
154
|
+
|
|
155
|
+
const foreignKeys = [...fields.keys()].filter((key) => !CODEX_ALLOWED_KEYS.has(key))
|
|
156
|
+
return { problems, foreignKeys }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @brief 描述目标路径的现状
|
|
161
|
+
* @param target 目标路径
|
|
162
|
+
* @return 已存在时返回 { kind, dest },不存在或无法读取时返回 undefined
|
|
163
|
+
*/
|
|
164
|
+
function describeTarget(target) {
|
|
165
|
+
try {
|
|
166
|
+
const stats = lstatSync(target)
|
|
167
|
+
if (stats.isSymbolicLink()) return { kind: 'link', dest: readlinkSync(target) }
|
|
168
|
+
return { kind: stats.isDirectory() ? 'dir' : 'file' }
|
|
169
|
+
} catch {
|
|
170
|
+
return undefined
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* @brief 判断两个路径是否指向同一个目录
|
|
176
|
+
* @param left 第一个路径
|
|
177
|
+
* @param right 第二个路径
|
|
178
|
+
* @return 指向同一目录返回 true
|
|
179
|
+
* @note 用文件标识而不是路径文本比较:Windows 的 junction 既不被
|
|
180
|
+
* isSymbolicLink() 识别、也读不了 readlink,只能靠 statSync 跟随
|
|
181
|
+
* 之后的 ino/dev 判断。inode 不可用(返回 0)时一律判为不同,
|
|
182
|
+
* 宁可多问一次 --force,也不误删用户的真实目录
|
|
183
|
+
*/
|
|
184
|
+
function isSameDirectory(left, right) {
|
|
185
|
+
try {
|
|
186
|
+
const a = statSync(left, { bigint: true })
|
|
187
|
+
const b = statSync(right, { bigint: true })
|
|
188
|
+
return a.ino !== 0n && a.ino === b.ino && a.dev === b.dev
|
|
189
|
+
} catch {
|
|
190
|
+
return false
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* @brief 在目标位置建立指向源目录的链接
|
|
196
|
+
* @param target 链接路径
|
|
197
|
+
* @param source 源目录
|
|
198
|
+
* @note Windows 使用 junction,普通权限即可创建且不需要开发者模式
|
|
199
|
+
*/
|
|
200
|
+
function createLink(target, source) {
|
|
201
|
+
const linkType = process.platform === 'win32' ? 'junction' : 'dir'
|
|
202
|
+
symlinkSync(source, target, linkType)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* @brief 删除目录链接本身,不触碰链向的内容
|
|
207
|
+
* @param target 链接路径
|
|
208
|
+
* @note Windows 的 junction 只能用 rmdir 删(unlink 返回 EPERM),
|
|
209
|
+
* 其他平台的目录符号链接相反,所以两个都试一遍
|
|
210
|
+
*/
|
|
211
|
+
function removeLink(target) {
|
|
212
|
+
const attempts = process.platform === 'win32' ? [rmdirSync, unlinkSync] : [unlinkSync, rmdirSync]
|
|
213
|
+
let lastError
|
|
214
|
+
for (const attempt of attempts) {
|
|
215
|
+
try {
|
|
216
|
+
attempt(target)
|
|
217
|
+
return
|
|
218
|
+
} catch (error) {
|
|
219
|
+
lastError = error
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
throw lastError
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* @brief 生成备份路径,保留原名并追加时间戳
|
|
227
|
+
* @param target 待备份的路径
|
|
228
|
+
* @return 备份路径
|
|
229
|
+
*/
|
|
230
|
+
function backupPath(target) {
|
|
231
|
+
return `${target}.bak-${new Date().toISOString().replace(/[:.]/g, '-')}`
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* @brief 判断本脚本是否运行在 npm 的临时安装目录里
|
|
236
|
+
* @return 运行于 npx 缓存等临时位置时返回 true
|
|
237
|
+
* @note npx 把包体放在 cache/_npx/<hash>/node_modules 下,缓存随时会被回收,
|
|
238
|
+
* 此时建链接会留下指向空目录的悬挂链接,所以自动降级为复制
|
|
239
|
+
*/
|
|
240
|
+
function isEphemeralInstall() {
|
|
241
|
+
return /[\\/]_npx[\\/]/.test(REPO_ROOT) || /[\\/]_cacache[\\/]/.test(REPO_ROOT)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* @brief 卸载已安装的 skill
|
|
246
|
+
* @param target 已安装路径
|
|
247
|
+
* @param force 目标不是本仓库链接时,是否重命名备份而不是直接报错
|
|
248
|
+
* @return 卸载成功返回 true
|
|
249
|
+
* @note 只删除指向本仓库源码目录的链接,链接指向别处或真实目录一律不动
|
|
250
|
+
*/
|
|
251
|
+
function uninstall(target, force) {
|
|
252
|
+
const existing = describeTarget(target)
|
|
253
|
+
if (existing === undefined) {
|
|
254
|
+
console.log(`[跳过] ${target} 不存在,无需卸载`)
|
|
255
|
+
return true
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (isSameDirectory(target, SOURCE_DIR)) {
|
|
259
|
+
// 删除链接本身,不触碰链向的内容
|
|
260
|
+
removeLink(target)
|
|
261
|
+
console.log(`[删除] ${target}`)
|
|
262
|
+
return true
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (!force) {
|
|
266
|
+
console.error(`[错误] ${target} 不是指向本仓库的链接(类型:${existing.kind}),未做任何修改`)
|
|
267
|
+
console.error(' 确认要移除副本后加 --force;脚本只会重命名备份,不会删除内容')
|
|
268
|
+
return false
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const backup = backupPath(target)
|
|
272
|
+
renameSync(target, backup)
|
|
273
|
+
console.log(`[备份] 原内容已重命名为 ${backup}`)
|
|
274
|
+
return true
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** 主流程:校验源、处理已有目标、安装并打印结果 */
|
|
278
|
+
function main() {
|
|
279
|
+
const options = parseArgs(process.argv.slice(2))
|
|
280
|
+
if (options.help) {
|
|
281
|
+
printUsage()
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const { problems, foreignKeys } = verifySkill(SOURCE_DIR)
|
|
286
|
+
if (problems.length > 0) {
|
|
287
|
+
for (const problem of problems) console.error(`[错误] ${problem}`)
|
|
288
|
+
process.exitCode = 1
|
|
289
|
+
return
|
|
290
|
+
}
|
|
291
|
+
for (const key of foreignKeys) {
|
|
292
|
+
console.warn(`[警告] frontmatter 存在 codex 白名单外的键 "${key}",这会让 codex 拒绝该 skill`)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const skillRoot = resolveSkillRoot(options.targetRoot)
|
|
296
|
+
const target = join(skillRoot, SKILL_NAME)
|
|
297
|
+
|
|
298
|
+
if (options.command === 'uninstall') {
|
|
299
|
+
if (!uninstall(target, options.force)) process.exitCode = 1
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const existing = describeTarget(target)
|
|
304
|
+
|
|
305
|
+
if (existing !== undefined) {
|
|
306
|
+
// 已经指向本仓库时直接视为已安装,避免重复操作
|
|
307
|
+
if (isSameDirectory(target, SOURCE_DIR)) {
|
|
308
|
+
console.log(`[跳过] ${target} 已指向本仓库,无需重新安装`)
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
if (!options.force) {
|
|
312
|
+
console.error(`[错误] ${target} 已存在(类型:${existing.kind}),未做任何修改`)
|
|
313
|
+
console.error(' 确认可以覆盖后加 --force;脚本只会重命名备份,不会删除原内容')
|
|
314
|
+
process.exitCode = 1
|
|
315
|
+
return
|
|
316
|
+
}
|
|
317
|
+
// 按文件安全约定先重命名备份,不直接删除用户目录
|
|
318
|
+
const backup = backupPath(target)
|
|
319
|
+
renameSync(target, backup)
|
|
320
|
+
console.log(`[备份] 原内容已重命名为 ${backup}`)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
mkdirSync(skillRoot, { recursive: true })
|
|
324
|
+
const useCopy = options.mode === 'copy' || (options.mode === 'auto' && isEphemeralInstall())
|
|
325
|
+
if (useCopy) {
|
|
326
|
+
cpSync(SOURCE_DIR, target, { recursive: true })
|
|
327
|
+
const reason = options.mode === 'auto' ? '(npm 临时目录里的包体随时会被回收,改用复制)' : ''
|
|
328
|
+
console.log(`[复制] ${relative(REPO_ROOT, SOURCE_DIR)} -> ${target}${reason}`)
|
|
329
|
+
} else {
|
|
330
|
+
createLink(target, SOURCE_DIR)
|
|
331
|
+
console.log(`[链接] ${target} -> ${SOURCE_DIR}`)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
console.log(`
|
|
335
|
+
安装完成,skill 根目录:${skillRoot}
|
|
336
|
+
- dsh Desktop / Web:输入框中打 “/”,选择 ${SKILL_NAME}
|
|
337
|
+
- 模型侧:由持久 skill 目录或 skill_search / skill_load 自动加载
|
|
338
|
+
- 生效时机:新开一个会话(dsh 在会话开始和目录变化时刷新 skill 目录)`)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
try {
|
|
342
|
+
main()
|
|
343
|
+
} catch (error) {
|
|
344
|
+
console.error(`[错误] ${error instanceof Error ? error.message : String(error)}`)
|
|
345
|
+
process.exitCode = 1
|
|
346
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: chtholly-notes-style
|
|
3
|
+
description: 将 C++ 开发中的优化、算法、调试与性能排查记录改写成个人中文博客;当用户要求按其大学时期博客风格整理、重写或润色 C++ 工作笔记时使用。不要用于正式技术文档、代码审查或与博客写作无关的编码任务。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# C++ 工作笔记
|
|
7
|
+
|
|
8
|
+
把 C++ 工作中的优化、算法和 debug 记录写成用户大学时期博客的风格。目标不是套一个“程序员博客模板”,而是保留问题推进过程、个人判断和适度口语,让文章看起来确实由本人整理过。
|
|
9
|
+
|
|
10
|
+
写作前先读 [references/style-profile.md](references/style-profile.md)。需要判断文章骨架或开头时,再读 [references/entry-patterns.md](references/entry-patterns.md)。
|
|
11
|
+
|
|
12
|
+
## 工作流
|
|
13
|
+
|
|
14
|
+
1. 先核对原始材料:日志、源码、diff、测试结果和用户描述。没有证据的根因、失败过程、性能数字和验证结果不要补写。需要查外部资料时优先一手来源,例如语言与库的规范、官方文档、编译器或运行时的问题报告,二手教程只当线索。要验证代码时先探本机工具链,有编译器就在本机编,没有才转到在线服务。
|
|
15
|
+
2. 记录算法、数据结构或某个知识点这类科普性内容时,动笔前必须先用联网工具查一遍,确认要写的内容站得住:
|
|
16
|
+
- 查什么:这套做法的来源和原始定义、能写进正文的复杂度与误差结论、容易记错的边界和限制条件,以及它和相邻做法的区别。凭印象写下的复杂度或结论一律先去核对。
|
|
17
|
+
- 优先找一手来源:论文原文、标准或规范、官方文档、源代码和 changelog、编译器或运行时的问题报告。OI-Wiki、CP-Algorithms、厂商技术博客这类整理性资料用来交叉印证,不作为唯一依据。
|
|
18
|
+
- 动手前先自己算一遍,再拿来源比对:总操作量、关键路径、误差界各用自己的推导核对。对不上时先查自己的假设错在哪,不为了和引用一致而改结论。
|
|
19
|
+
- 结论只写来源实际说了什么。只找到低可信度资料、或者几份资料互相矛盾时,按「区分三类信息」那条当猜测写,并在交付说明里列出哪几条没有核实。
|
|
20
|
+
3. 动笔前先把定位定下来,它决定主线,不能等成文之后再补。材料里出现优化,或者修 bug 时换了一套和常规做法不同的算法,先用 `ask_user_question` 确认这篇按哪种写:
|
|
21
|
+
- 用户说明是针对项目中某个方法的优化记录时,按优化记录写:先写旧做法和可观察到的瓶颈,再写新做法、正确性边界以及性能或行为上的代价。
|
|
22
|
+
- 材料本身就是排查过程时,按 debug 记录写:保留“现象、怀疑、失败尝试、源码证据、结论”的推进顺序。
|
|
23
|
+
- 其余情况默认按“介绍一种优化/算法方式”写:正文围绕这套做法怎么成立、代价在哪里展开,不写旧实现和改造历程,也不按排查顺序推进。
|
|
24
|
+
分辨不出该用哪种时直接问,不要自己猜。只是改参数、调顺序而没有引入新算法的情况不用问。
|
|
25
|
+
4. 区分三类信息:确定现象、能由源码支持的机制、仍未闭环的猜测。后两类要明确语气,不能写成确定结论。
|
|
26
|
+
5. 找到文章唯一的主线。debug 写问题如何被逐步缩小;优化记录写旧做法为什么不够以及新做法的代价,介绍一种做法本身时写这套做法怎么成立、代价在哪里;算法写关键观察和实现细节。
|
|
27
|
+
6. 用连续叙述推进,先写现象或最初想法,再写尝试、发现、修改和验证。失败尝试和不成立的猜测通常可以保留。
|
|
28
|
+
7. 算法和优化类文章用小标题分段,每个标题直接写这一节在讲什么,例如 `整体求和序`、`块间递归`、`抛开对齐看复杂度`。不编号,也不写“背景介绍”“方案对比”“总结”这类只有形式的栏目名;一节只承载一个判断或一段实现,节数按内容定,不为了对称凑标题。短小的 debug 记录保持单 H1,用连续段落推进。
|
|
29
|
+
8. 每个关键步骤写成“文字说明 + 对应代码”的形式。先说这一步要解决什么、判断是什么,再贴支撑这个判断的那段代码,让读者能把推导和实现对齐到具体几行,不要只丢一大段代码让人自己找。一屏以内的短代码直接放在正文,不上折叠;长代码和大段日志用 `<details>` 折起,`<details>` 与 `<summary>点击查看代码</summary>` 分两行写。
|
|
30
|
+
9. 介绍算法或与常规做法不同的实现时,如果有推导价值,就把复杂度和优缺点讲清楚:总操作量、依赖深度(最长加法链或关键路径)、误差界、内存访问各算一遍,和常规做法放在一起比。成立的前提要写明,例如 $n$ 是否是块长的整数倍、pivot 是否接近真实中位数、有没有开 `-ffast-math`;渐近量级和常数分开说,不要把某个规模的实测值当成渐近结论。单独成行的推导和结论用 `$$ ... $$`。写完回头通读一遍,检查有没有把总操作量和关键路径混在一起、有没有把一步推导的假设套到不适用的数据上;没想通的地方直接写“这里还没推明白”,不补一个看起来完整的结论。
|
|
31
|
+
10. 算法和优化类文章在复杂度和误差之后、完整实现之前,加一节写适用场景:什么规模下这套做法划算,什么规模下分层的固定开销超过收益,算子或数据结构需要满足什么性质,不同硬件形态下的取舍差在哪。边界要写到能据此判断用不用,不写“视情况而定”。
|
|
32
|
+
11. 需要跑代码验证时先探本机工具链(`g++ --version`、`clang++ --version`、`wsl.exe -l -v`,Windows 上再看 `cl.exe`、`vswhere.exe` 以及 MSYS2、LLVM、VS Build Tools 的常见位置):本机有编译器就在本机编,记下编译器和版本;没有才用在线服务。投给在线服务的代码当成公开内容,内部代码、真实数据和内网符号一律不投。
|
|
33
|
+
在线编译用 Wandbox 的 `api/list.json` 和 `api/compile.json`:`api/list.json` 列出可用编译器和每个编译器接受的开关名,`api/compile.json` 提交 `compiler`、`code`、`options`、`stdin` 四个字段,`options` 是逗号连接的开关名列表。每换一个环境先编一个打印 `__cplusplus` 的小程序确认标准真的生效,因为 `options` 里不认识的名字会被静默忽略;一批不要超过三个,出现 `fetch failed` 是退避重试而不是编译失败;编译失败看 `status` 和 `compiler_error`,不是 HTTP 错误。每次验证至少记四项:在哪编、编译器与版本、标准、结果。没实跑过的代码不写成跑过,也不补“已通过”。
|
|
34
|
+
12. 优化、算法或 bug 记录,如果相关代码已经有比较完整的解决或实现方案,在正文写完后附一份完整实现。引导语只写清是什么代码,直接写“完整的 xxx 实现:”,不写“这个实现后来单独放进了一个头文件,只做这一件事”这类交代代码去向的过渡句,后面接 `<details>` 折叠块或代码块。关键函数或类写完整、能独立看懂即可,不必是整份工程文件;代码还在改或只剩片段就不要附,也不为了看起来完整而补代码。
|
|
35
|
+
13. 写完后检查是否出现报告式标题、空泛总结、整齐排比、过量粗体、emoji、勾叉和没有证据的“最佳实践”;同时检查脚注闭合的 `]` 后是否留了空格,以及程序和输出有没有跟所属的那条编译环境说明、有没有把没跑过的代码写成跑过、有没有把验证流程当成正文写出来。
|
|
36
|
+
|
|
37
|
+
## 输出约定
|
|
38
|
+
|
|
39
|
+
- 不写项目内部信息:内部文件路径和目录、模块名、命名空间、类名,公司/团队/产品/仓库名,客户与同事姓名、工号、内网域名和 IP,以及内部数据结构与业务字段。需要交代代码位置时只用泛化说法,而且只在这一点确实影响读者理解时提一句,不专门交代代码后来搬到了哪里;贴日志时内部符号要脱敏,完整清单见 [references/style-profile.md](references/style-profile.md) 的「不能出现的内容」。
|
|
40
|
+
- 正文使用中文,技术名词、函数名和错误信息保留原文。
|
|
41
|
+
- 正文里的数学量(变量、下标、索引、常量)和阿拉伯数字都写成 LaTeX 行内公式,与相邻的中文、字母、数字之间留一个空格,与标点和括号之间不加空格,例如 `这是 $a_{i}^{10}$ 正文`、`前 $10$ 个`、`退出码是 $139$`、`$k$,`;量纲词留在公式外,写成 `$6$ GB`、`$2473$ 万行`。公式内部的运算符也留空格,写 `$k = 10$`。版本号(`CentOS 7.9`、`GCC 12.2.0`)和十六进制字面量(`0x1420`)保持原样;库名、工具名、语言名和缩写保持普通文本或反引号,不进公式;代码标识符、代码块、标题、路径和文件名里的数字也保持原样。
|
|
42
|
+
- 算法叙述里的下标、区间、算式、条件和赋值都写成公式:`v[0]` 写 `$v_{0}$`,`v[1..len)` 写 `$v_{1} \sim v_{len-1}$`,`a, a+1, a+2` 写 `$a, a+1, a+2$`,除法用 `\frac{}{}`、乘法用 `\times`。
|
|
43
|
+
- 变量名、关键字和单独提到的运算符一律写成公式,正文里裸写的也要改:单字母写 `$i$`,多字母用 `\text{}` 保持直立(`$\text{pivot}$`、`$\text{if}$`、`$\text{sizeof}$`),`>`、`>=` 写 `$>$`、`$\ge$`。函数名、API 调用、类型名和文件名用行内代码,不进公式。
|
|
44
|
+
- 单独成行的公式用 `$$ ... $$`,前后各留一个空行,让它居中显示,不再用反引号或裸文本包推导;行内公式继续用 `$ ... $`。只有关键等式、递推式和复杂度结论才单独成行,正文里出现的量、下标和单个变量留在行内。
|
|
45
|
+
- H1 直接写问题、算法或优化主题,下一行给一个简短标签,不写“排查记”“深入理解”“一文搞懂”等标题包装。标题不写“xxxx:xxxxx”的冒号格式,简要说明即可:算法和知识点类文章直接用算法或技术名(`# SIMD`、`# PairWise`),有明确中文名时写「英文|中文」(`# PairWise|成对求和`);针对某种做法的优化用「基于 xxx 的 xxx 做法」,debug 记录写清在什么条件下出问题,例如 `# 在容器内跨 DSO 加载时的符号解析表现`。不写成“某某对不上排查”。
|
|
46
|
+
- 文件名为 `主题.md`。
|
|
47
|
+
- 涉及算法、数据结构或知识点的结论要先在正文里对得上来源:能在正文里对照的就用内联脚注,只用来说明出处、不改变叙述的信息放在交付说明里。结论和来源对不上、或者找不到来源时,按猜测语气写,不当成确定结论。
|
|
48
|
+
- 引用外部资料用内联脚注 `^[引用内容]`,脚注里只放 URL,闭合的 `]` 后留一个半角空格再接文字或标点;同一个序号不能复用,一条内容对应多个链接时拆成相邻的两个脚注。不设末尾的参考文献节,细节见 [references/style-profile.md](references/style-profile.md) 的「引用与外部资料」。
|
|
49
|
+
- 不生成目录,不写背景介绍和总结章节。
|
|
50
|
+
- 不写“根因如下”“修复方案”“验证结果”“不影响范围”等模板段落。
|
|
51
|
+
- 编译环境按文章分层标注,不逐段重复:整篇环境一致就只在开头标一次;一篇里有多种环境按节归并,写在节首(大节一致写在大节,小节一致写在小节);节里个别程序换了标准或编译器的,只给那一段单独标一句。每处标注写清在哪编、编译器和版本、标准、结果。
|
|
52
|
+
- 本机编的写“本地”,例如 `编译环境:本地 g++ 13.2.0,-std=gnu++20。`;在线编的写明站点,例如 `本节代码:Wandbox 在线编译,gcc-13.2.0,-std=gnu++2a。`。程序后面紧跟的输出或报错跟着它所属的那条环境说明,不再单独标。这个结果只代表该编译器版本加该标准,不当成本机或线上工具链的结论;诊断里的服务端临时文件名不能当成本地目录结构。
|
|
53
|
+
- 编译环境按事实写:直接写“文中验证环境:Wandbox 在线编译,gcc-13.2.0,-std=gnu++2a。”这种陈述,不解释为什么用在线服务、不写“本机没有编译器所以……”“先探本机工具链”这类交代验证流程的话。验证规矩本身不是文章内容;报错和输出是这些环境上的原文,也不必声明。
|
|
54
|
+
- 在线诊断引用前清掉 ANSI 颜色码,`compiler_message` 常和 `compiler_error` 重复,去重后再引用。
|
|
55
|
+
- 对已经更新的代码,明确使用“当时”“后来改成”等时间语气,不假装仍能复现旧实现。
|
|
56
|
+
- 不为了口语化编造错误、对话、心路历程或删除线内容。
|
|
57
|
+
|
|
58
|
+
如果用户只给了一段工作记录,内容不足以判断根因或效果,直接按现有证据成文并把未确定处写清楚,不要向用户索取可以在仓库中查到的信息。
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# 常见记录骨架
|
|
2
|
+
|
|
3
|
+
这些只是组织材料的方式,不要把标题直接写进文章。现有博客通常是一篇一个问题,正文用连续段落展开。
|
|
4
|
+
|
|
5
|
+
算法和优化类文章用小标题把下面的步骤切成几节,每个标题写清这一节在讲什么(`整体求和序`、`块间递归`、`抛开对齐看复杂度`),不编号、不用栏目名。debug 记录通常保持单 H1 连续叙述。
|
|
6
|
+
|
|
7
|
+
## Debug
|
|
8
|
+
|
|
9
|
+
适合复现、日志、崩溃、偶发错误、线程问题和环境差异。
|
|
10
|
+
|
|
11
|
+
推荐顺序:
|
|
12
|
+
|
|
13
|
+
1. 用一两句话写清现象,能贴原始报错就贴原始报错。
|
|
14
|
+
2. 写最初怀疑的机制和已经做过的修改。
|
|
15
|
+
3. 写删除修改、增加日志或换一种实现后的结果。
|
|
16
|
+
4. 回到源码或调用链,说明哪条证据推翻了之前的判断。
|
|
17
|
+
5. 写最终修改和能够确认的验证结果。
|
|
18
|
+
6. 如果还有没查清的部分,直接留下,不补一个完美结论。
|
|
19
|
+
|
|
20
|
+
如果排查过程中换了一套新算法,先按 `SKILL.md` 的定位确认步骤问清这篇到底按 debug 写还是按新算法写,再决定用这个骨架还是下面的算法骨架。
|
|
21
|
+
|
|
22
|
+
开场示例:
|
|
23
|
+
|
|
24
|
+
```markdown
|
|
25
|
+
# 在容器内跨 DSO 加载时的符号解析表现
|
|
26
|
+
|
|
27
|
+
fmtlog,DSO,符号可见性。
|
|
28
|
+
|
|
29
|
+
程序启动后会先输出两行工作线程绑核日志,接着直接 `free(): invalid pointer`。
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 优化
|
|
33
|
+
|
|
34
|
+
适合缓存、批处理、数据布局、编译选项和性能回退。标题用「基于 xxx 的 xxx 做法」,或者写清在什么条件下表现如何。
|
|
35
|
+
|
|
36
|
+
推荐顺序:
|
|
37
|
+
|
|
38
|
+
1. 写旧实现和可观察到的瓶颈。
|
|
39
|
+
2. 写为什么当前路径重复做了工作,或者为什么无法直接并行。
|
|
40
|
+
3. 写优化思路和实现,只贴改变行为的关键代码,每个关键步骤配一小段。
|
|
41
|
+
4. 写正确性边界,例如顺序、精度、内存所有权和线程安全。
|
|
42
|
+
5. 有推导价值时写和常规做法的复杂度对比,总量、深度、误差和内存访问分开算。
|
|
43
|
+
6. 写适用场景:什么规模下划算,什么规模下分层的固定开销超过收益,不同硬件形态的取舍差在哪。
|
|
44
|
+
7. 有真实数据时再写优化前后的数据。编译和运行按 `SKILL.md` 的编译验证步骤记四项:在哪编、编译器与版本、标准、结果;编译环境按文章分层标注,整篇一致只在开头标一次,一篇多环境按节写在节首,换标准或编译器的程序单独标一句。
|
|
45
|
+
|
|
46
|
+
开场通常可以直接写:
|
|
47
|
+
|
|
48
|
+
```markdown
|
|
49
|
+
原来的实现每次都会重新遍历一遍整张表,数据量大之后这里就成了主要耗时。
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## 算法
|
|
53
|
+
|
|
54
|
+
适合工作中遇到的匹配、调度、压缩、图算法和小规模组合问题。
|
|
55
|
+
|
|
56
|
+
推荐顺序:
|
|
57
|
+
|
|
58
|
+
1. 写问题转化后的核心观察。
|
|
59
|
+
2. 说明为什么贪心、二分、动态规划或数据结构能够成立。
|
|
60
|
+
3. 写转移、维护量或容易漏掉的边界。
|
|
61
|
+
4. 写实现细节,观察、推导和关键代码对齐到具体几行。
|
|
62
|
+
5. 写和常规做法的复杂度对比,把推导的前提和成立的规模范围写清楚。复杂度、误差和边界结论先用联网工具核对来源,并自己独立推一遍;引用按 `style-profile.md` 的「引用与外部资料」处理,找不到来源的按猜测语气写。
|
|
63
|
+
6. 写适用场景:这套做法适合什么规模、什么数据特征,什么时候不如直接写朴素版本。编译和运行按 `SKILL.md` 的编译验证步骤记四项:在哪编、编译器与版本、标准、结果;编译环境按文章分层标注,整篇一致只在开头标一次,一篇多环境按节写在节首,换标准或编译器的程序单独标一句。
|
|
64
|
+
|
|
65
|
+
开场通常可以直接写:
|
|
66
|
+
|
|
67
|
+
```markdown
|
|
68
|
+
考虑固定右端点,左端点满足条件时一定是一段连续区间。
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 多个方案
|
|
72
|
+
|
|
73
|
+
如果确实有两个值得保留的做法,可以按现有博客里的方式写:`解法一`、`解法二`,或者“也可以……”放在正文后面。不要为了显得完整而强行拼出多个方案。
|
|
74
|
+
|
|
75
|
+
## 完整实现的位置
|
|
76
|
+
|
|
77
|
+
优化、算法和 debug 记录,如果代码已经有比较完整的解决或实现方案,可以在正文之后补一份完整实现:引导语只写清是什么代码,直接写成 `完整的 top_k cpp 复现代码:`,后面接 `<details>` 折叠块或代码块,作为文章的最后一块结束,后面不再接总结。关键函数或类写完整、能独立看懂即可,不必是整份工程文件。
|
|
78
|
+
|
|
79
|
+
不要用“后来单独拆成了一个头文件”这类句子给完整实现做过渡,也不要在正文里专门交代代码搬去了哪里。只有代码位置本身影响理解时才用一句泛化说法,内部路径、模块名和类名按 `style-profile.md` 的「不能出现的内容」处理,一律不写进文章。
|
|
80
|
+
|
|
81
|
+
竞赛题解本来就在每道题后面接完整代码,最后一题的代码块直接收到文件结尾,这一类不额外加引导语。
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# 个人博客风格
|
|
2
|
+
|
|
3
|
+
## 标题与标签
|
|
4
|
+
|
|
5
|
+
- H1 直接把主题当标题,简要说明即可,例如 `# SIMD`、`# PairWise`、`# K. Maximum Rating`。
|
|
6
|
+
- H1 下一行只写标签式短语,例如 `数据结构。`、`贪心。`、`fmtlog,DSO,符号可见性。`。
|
|
7
|
+
- 标签通常由技术名、数据类型或问题类型组成,不使用完整句子。
|
|
8
|
+
- 文件名为 `主题.md`,主题里不堆错误信息、反引号和冒号。
|
|
9
|
+
- 标题不写“xxxx:xxxxx”这种冒号格式,读起来像 AI 生成的图鉴式小标题;用词组或短语说清主题就够了。
|
|
10
|
+
- 算法和知识点类文章的标题就是那个算法名或技术名,例如 `# SIMD`、`# PairWise`;有比较明确的中文名时写成「英文|中文」,例如 `# PairWise|成对求和`。
|
|
11
|
+
- 针对某种做法做优化时,标题用类似论文的「基于 xxx 的 xxx 做法」格式,例如 `# 基于分块求和的递归归约`;讲究在什么条件下表现如何的,也可以写成「在 xxx 下 xxx 的表现」。
|
|
12
|
+
- debug 和排错记录用同样的写法,标题写清在什么条件下出问题,例如 `# 在容器内跨 DSO 加载时的符号解析表现`。
|
|
13
|
+
- 写完 H1 前先确认文章定位:如果是“介绍一种新的优化/算法方式”,H1 就写算法或做法本身,不要让标题停留在“某某数据对不上”“某某崩溃”这种排查口吻;如果定位是 debug 记录,H1 才写条件、现象和表现。
|
|
14
|
+
|
|
15
|
+
## 小标题与分节
|
|
16
|
+
|
|
17
|
+
- 算法和优化类文章用小标题分段,H1 下面直接进内容,不再写一段“背景介绍”。
|
|
18
|
+
- 小标题是内容短语,写清这一节在讲什么,例如 `整体求和序`、`块间递归`、`块内归约`、`抛开对齐看复杂度`、`适用场景`、`完整实现`。
|
|
19
|
+
- 不编号,不用「一、」「(2)」这类前缀;不写「背景」「概览」「思路」「总结」这种只有形式的栏目名。`完整实现`、`适用场景` 这类直接指向内容的标题可以留,因为它们各自对应具体的代码块或判断。
|
|
20
|
+
- 一节只讲一个判断、一段实现或一层推导,节数按内容定,不为对称硬凑。数据结构和流程本身需要多层展开时,节可以多一点。
|
|
21
|
+
- 短小的 debug 记录保持单 H1,用连续段落推进,不为了看起来整齐而切段。
|
|
22
|
+
|
|
23
|
+
## 叙述方式
|
|
24
|
+
|
|
25
|
+
- 默认使用第一人称或省略主语,少用“本文将”“我们需要”“可以看到”。
|
|
26
|
+
- 用短句推进,一段通常只解决一个小问题,逻辑词偏向“考虑、发现、后来、于是、不过、其实、注意、也可以”。
|
|
27
|
+
- 先写现象或直觉,再写尝试和修正。允许出现“一开始觉得”“当时没想清楚”“后来重新看源码才发现”。
|
|
28
|
+
- 对不确定内容保留猜测语气,例如“可能是”“应该是”“暂时还没找到”“这里不太确定”。
|
|
29
|
+
- 可以轻松一点,但不刻意卖萌。删除线只用于真实的自嘲或补充,不为模仿风格而添加。
|
|
30
|
+
- 不追求每一段都有总结句,也不把所有过程整理成整齐的三段式或并列结构。
|
|
31
|
+
|
|
32
|
+
## 技术内容
|
|
33
|
+
|
|
34
|
+
- 算法题通常先点明核心观察,再说明为什么这样做,最后补复杂度或容易写错的位置。
|
|
35
|
+
- debug 记录通常从原始报错或异常现象开始,随后写最初判断、失败尝试、源码证据、修改和现在的状态。
|
|
36
|
+
- 优化记录(针对项目中某个方法做改造)通常先写旧做法和瓶颈,再写新做法、边界条件以及性能或行为上的代价。
|
|
37
|
+
- 定位成介绍一种优化或算法做法本身时,正文围绕这套做法怎么成立、代价在哪里展开,不写旧实现和改造历程,也不按排查顺序推进。分辨不出属于哪种定位时先问用户,默认按介绍做法写。
|
|
38
|
+
- 代码用于说明关键实现,不贴与结论无关的完整工程文件。
|
|
39
|
+
- 每个关键步骤写成“文字说明 + 对应代码”:先说这一步要解决什么、判断是什么,紧接着贴支撑判断的那几行。关键步骤包括入口的分支裁剪、循环不变量、选择/分区规则、递归或收缩条件、水平归约之类的收尾顺序、以及和外部实现对齐的接点。
|
|
40
|
+
- 一屏以内的短代码直接放正文,多个关键步骤就多贴几个小代码块,不要为了省篇幅把它们挤成一段或塞进折叠区。只有完整实现、大段日志和确实只是备查的长代码用 `<details>` 折起。
|
|
41
|
+
- 大段代码使用:
|
|
42
|
+
|
|
43
|
+
````html
|
|
44
|
+
<details>
|
|
45
|
+
<summary>点击查看代码</summary>
|
|
46
|
+
|
|
47
|
+
```cpp
|
|
48
|
+
// code
|
|
49
|
+
```
|
|
50
|
+
</details>
|
|
51
|
+
````
|
|
52
|
+
|
|
53
|
+
- 完整实现放在正文结束后单独附一份:引导语只写清是什么代码,直接写成 `完整的 top_k cpp 复现代码:`,后面接 `<details>` 折叠块或代码块,作为文章的最后一块收尾,后面不再接总结。不要写“这个实现后来单独放进了一个头文件,只做这一件事”这类交代代码去向的过渡句。
|
|
54
|
+
- 关键函数或类写完整、能独立看懂即可,不必是整份工程文件;代码的位置和后来的变化只用泛化说法交代,不写内部路径和模块名。
|
|
55
|
+
- 长日志可以直接贴出,但只保留定位问题需要的行;错误类型、行号和栈深这些定位信息不改写,内部符号按下面的「不能出现的内容」脱敏。
|
|
56
|
+
- 不编造数据。只有用户提供过 benchmark、运行日志或测试结果时,才写具体耗时、吞吐、文件大小和线程数量。
|
|
57
|
+
|
|
58
|
+
## 代码与编译环境
|
|
59
|
+
|
|
60
|
+
- 编译环境按文章分层标注,不逐段重复:整篇环境一致就只在开头标一次;一篇里有多种环境按节归并,写在节首(大节一致写在大节,小节一致写在小节);节里个别程序换了标准或编译器的,只给那一段单独标一句。
|
|
61
|
+
- 每一处标注都写清在哪编、编译器和版本、标准、结果。本机编的写“本地”,例如 `编译环境:本地 g++ 13.2.0,-std=gnu++20。`;在线编的写明站点,例如 `本节代码:Wandbox 在线编译,gcc-13.2.0,-std=gnu++2a。`。
|
|
62
|
+
- 程序后面紧跟的输出或报错跟着它所属的那条环境说明,不再单独标。
|
|
63
|
+
- 没实跑过的代码不写成跑过,不补“已通过”;报错原文照抄,只删定位不需要的行,不改写错误类型和行号。
|
|
64
|
+
- 本机没有编译器才用在线服务,提交的代码按公开内容对待,内部代码、真实数据和内网符号一律不投;在线结果只代表那个编译器版本加那个标准,不当成本机或线上工具链的结论。
|
|
65
|
+
|
|
66
|
+
## 复杂度与推导
|
|
67
|
+
|
|
68
|
+
- 算法和优化写不写复杂度,看有没有推导价值。有就写,并和常规做法放在一起比;没有比单纯复述教科书更容易出错的地方,就不硬凑一节。
|
|
69
|
+
- 对比的维度分开算,不混成一句“更快”:总操作量(加了多少次、比较了多少次)、依赖深度(最长链、关键路径)、误差界、内存访问(顺序还是跨步、working set 多大)。
|
|
70
|
+
- 计数要具体。能写出精确表达式就写成 $\frac{9}{8}n$ 这种形式,再看渐近;不能精确计数的量说明是上界还是平均情形,在哪一步引入的假设。
|
|
71
|
+
- 前提和边界写清楚:$n$ 是否是块长或步长的整数倍,余数分支怎么处理,pivot 是否接近真实中位数,有没有开 `-ffast-math` 之类的重结合选项,数据规模是否落在某个阈值以上。
|
|
72
|
+
- 区分“总量”和“深度”。并行度和 ILP 靠深度,单核墙钟时间还受总量和内存带宽约束,别把 $\log n$ 的深度直接说成 $\log n$ 的耗时,也别把单块代价乘上递归层数当成总量。
|
|
73
|
+
- 和另一种算法对比时点明代价关系,例如“总量多出八分之一,换来关键路径从 $O(n)$ 降到 $O(\log n)$”“没有补偿项,误差界从接近 $O(\varepsilon)$ 退到 $O(\varepsilon \log n)$”,不要只贴一张都有哪些优点的清单。
|
|
74
|
+
- 推导写完回头通读一遍,检查每一步的假设是否还在成立、有没有把结论用在不适用的规模上。没推完的部分直接写“这一步我还没推明白”,不补完美结论。
|
|
75
|
+
- 算法和优化类文章在复杂度与误差之后补一节适用场景,把划算的规模范围、算子性质要求和硬件形态差异写成能据此判断的边界,不写“视情况而定”。
|
|
76
|
+
|
|
77
|
+
## 引用与外部资料
|
|
78
|
+
|
|
79
|
+
- 引用外部资料用内联脚注 `^[引用内容]`,渲染时自动排号。同一个序号不能复用,所以一条内容对应多个链接时要拆成相邻的两个脚注,各自带上说明。
|
|
80
|
+
- 脚注里只放 URL,不写标题、作者和日期。
|
|
81
|
+
- 脚注闭合的 `]` 后面留一个半角空格再接文字或标点,写成 `……压下来的^[https://example.com] ,每一步……`,避免后面的文字被解析进链接。
|
|
82
|
+
- 不设末尾的「参考文献」节。脚注已经带上来源,末尾再列一遍是重复;因为不设这一节,附了完整实现时代码块仍然是文章最后一块。
|
|
83
|
+
- 写算法、数据结构、复杂度或某个知识点的结论前先用联网工具查一遍,不靠记忆成文。查的内容包括来源与原始定义、复杂度与误差结论、边界和限制条件、以及它和相邻做法的区别;凭印象写下的复杂度或结论先去核对。
|
|
84
|
+
- 资料可信度分级,优先用高一级的,低一级的只用来说明明细:
|
|
85
|
+
1. 论文原文、标准或规范、语言与库的官方文档、上游源码和 changelog、编译器或运行时官方的问题报告。
|
|
86
|
+
2. 教科书、权威教材和经过评审的讲义。
|
|
87
|
+
3. OI-Wiki、CP-Algorithms、厂商技术博客这类整理性资料,用来交叉印证,不作为唯一依据。
|
|
88
|
+
4. 社区问答、论坛帖和个人博客,只当线索,不单独支撑结论。
|
|
89
|
+
- 自己先推一遍再和来源比:总操作量、关键路径、误差界各按自己的推导核对,对不上先检查假设,不为了和引用一致而改结论,也不必贴出与正文无关的完整证明。
|
|
90
|
+
- 引用只对应来源实际说了什么,不把不同来源的结论拼成一句更强的话;同一结论有多份来源时引用权威的那一份,其余的放在交付说明里提一句。
|
|
91
|
+
- 只找到低可信度资料,或者几份资料互相矛盾时,正文按猜测语气写,并在交付说明里列出哪几条没有核实。
|
|
92
|
+
- 拿不到原文时(例如只有 PDF 而读不到正文),只按书目信息引用,并让正文的结论不依赖它的具体数字;同时跟用户说明哪几条是核对过正文的。
|
|
93
|
+
|
|
94
|
+
## 不能出现的内容
|
|
95
|
+
|
|
96
|
+
下面这些一律不写进文章,正文、行内代码、代码注释和贴出的日志都算:
|
|
97
|
+
|
|
98
|
+
- 内部文件路径、目录和文件名,例如带真实模块层级的 `模块目录/子模块/实现文件.h`。
|
|
99
|
+
- 带项目语义的模块名、命名空间和类名,例如 `内部前缀Foo`、`内部命名空间::子模块`。
|
|
100
|
+
- 公司、团队、产品、仓库名,客户与同事姓名,工号,内网域名和 IP。
|
|
101
|
+
- 内部数据结构、表名、行情字段名、因子编号和其他业务口径。
|
|
102
|
+
|
|
103
|
+
需要交代代码位置时,只在这一点确实影响读者理解时用一句泛化说法,而且不交代代码后来搬去了哪个文件、也不解释“这个文件只做这一件事”。实现归属的来龙去脉不属于正文内容。
|
|
104
|
+
|
|
105
|
+
贴原始日志或崩溃栈时保留错误类型、行号、栈深这些定位信息,内部符号换成中性的占位名称,例如把 `内部类::方法名` 写成 `FactorWriter::write`;拿不准某个名字算不算内部时,一律用泛化写法。涉及的内部模块层级太多、脱敏之后不成形时,不贴原始片段,直接用文字描述这段日志或调用栈说了什么。
|
|
106
|
+
|
|
107
|
+
## 排版
|
|
108
|
+
|
|
109
|
+
- H1 按上面的「标题与标签」写主题本身,之后按「小标题与分节」组织算法和优化类文章,debug 记录可以只有 H1。
|
|
110
|
+
- 连续叙述使用普通段落,不把每一句话拆成项目符号。
|
|
111
|
+
- 技术名词和代码使用行内代码;少用粗体。
|
|
112
|
+
- 不使用 ✅、❌、🚀 等 emoji,不做勾叉清单。
|
|
113
|
+
- 避免 ASCII 架构图、复杂表格和只有形式感的流程框。
|
|
114
|
+
- 正文里的数学量(变量、下标、索引、常量)和阿拉伯数字都写成 LaTeX 行内公式 `$...$`,与相邻的中文、字母、数字之间留一个空格(例如 `这是 $a_{i}^{10}$ 正文`、`取 $k = 10$`、`前 $10$ 个`、`退出码是 $139$`),与标点和括号之间不加空格(`$k$,`、`($k$)`)。量纲词留在公式外:`$6$ GB`、`$2473$ 万行`。
|
|
115
|
+
- 需要单独成行的等式、递推式和复杂度结论用 `$$ ... $$`,前后各留一个空行,居中显示;正文里的量、下标和单个变量仍走行内公式,不因为出现过一次就全部搬到块级。
|
|
116
|
+
- 版本号(`CentOS 7.9`、`GCC 12.2.0`、`Arrow/Parquet 21.0.0`)和十六进制字面量(`0x1420`)保持普通文本,不套 `$...$`。
|
|
117
|
+
- 公式内部的运算符按 LaTeX 习惯留空格,写 `$k = 10$` 而不是 `$k=10$`。
|
|
118
|
+
- 库名、工具名、语言名和缩写保持普通文本或反引号(polars、py、C++、gdb、ASAN、DSO),不进公式;函数名、类型名、文件名和错误信息这些代码标识符继续用反引号。
|
|
119
|
+
- 算法叙述里的数组引用写成下标形式:`v[0]` 写成 `$v_{0}$`,`v[i]` 写成 `$v_{i}$`;半开区间写成 `$v_{1} \sim v_{len-1}$`,`v[0..10)` 写成 `$v_{0} \sim v_{9}$`;连续几项写成 `$a, a+1, a+2$`。
|
|
120
|
+
- 公式里不写 `/` 和 `*`:除法用 `$\frac{\text{len}}{12}$`,乘法用 `$\times$`,例如 `$v_{\frac{\text{len}}{8} \times 4}$`。路径、命令行参数和 API 调用里的 `/` 不是除法,仍按原样放在行内代码里。
|
|
121
|
+
- 代码里的条件与赋值也可以写进公式:`index == len - 1` 写成 `$\text{index} = \text{len} - 1$`,`len <= 16` 写成 `$\text{len} \le 16$`,`frac = 83` 写成 `$\text{frac} = 83$`。
|
|
122
|
+
- 变量名一律写进公式,出现在正文里但没加任何标记的也要改:单字母直接写(`$i$`、`$a$`),多字母用 `\text{}` 保持直立(`$\text{frac}$`、`$\text{pivot}$`、`$\text{numLt}$`),避免渲染成斜体字母串。类型名、库名和数据字段名(double、polars、tick、qty)保持普通文本。
|
|
123
|
+
- 关键字和单独提到的运算符也写进公式:`if` 写成 `$\text{if}$`,`return` 写成 `$\text{return}$`,`try/catch` 写成 `$\text{try}/\text{catch}$`,比较关系从 `>` 换成 `>=` 写成从 `$>$` 换成 `$\ge$`。
|
|
124
|
+
- 函数名、API 调用、命名空间名和文件名用行内代码,不进公式;类型名和字面量(`const char*`、`void*`、`true`、`nullptr`)也保持在行内代码里。
|
|
125
|
+
- 反引号里的代码、代码块、标题、路径和文件名里的数字保持原样,不套 `$...$`。
|
|
126
|
+
- 块级推导只在确有必要时展开,不为了显得严谨而堆公式。
|
|
127
|
+
|
|
128
|
+
## 明确避开 AI 味
|
|
129
|
+
|
|
130
|
+
- 不写“xxxx:xxxxx”这种冒号式标题,也不把标题包装成解说腔。
|
|
131
|
+
- 不写“根因如下”“修复方案”“验证结果”“不影响范围”“核心收获”“经验教训”等固定栏目。
|
|
132
|
+
- 不连续使用“首先、其次、最后”“一方面、另一方面”“不仅、而且”这类模板连接词。
|
|
133
|
+
- 不写“值得注意的是”“需要强调的是”“综上所述”“由此可见”等空转句。
|
|
134
|
+
- 不把约定、规矩和验证流程当正文写。作者视角只交代事实:写“文中验证环境:Wandbox 在线编译,gcc-13.2.0,-std=gnu++2a。”,不写“本机没有编译器所以在线上编”“先看本机有没有工具链”“有就用本地、没有才用在线”这类解释。
|
|
135
|
+
- 不写只交代代码去向的过渡句,例如“这个实现后来单独放进了一个头文件,只做这一件事”“后来这段逻辑被抽走了”,除非这次搬动本身就是文章要讲的问题。
|
|
136
|
+
- 不把内容强行排成三个并列短句,也不反复使用“不是……而是……”制造金句感。
|
|
137
|
+
- 不写“综上所述,这个方案兼顾了……”“希望这篇文章能帮助到你”之类结尾。
|
|
138
|
+
- 不在每段后补一句解释上一句的废话。
|
|
139
|
+
|
|
140
|
+
## 收尾方式
|
|
141
|
+
|
|
142
|
+
文章可以在验证结果、剩下的疑点或一个补充做法处自然结束。常见收尾是:
|
|
143
|
+
|
|
144
|
+
- 简短说明现在的实现状态。
|
|
145
|
+
- 留下尚未完全解释清楚的问题。
|
|
146
|
+
- 给出另一个可行做法。
|
|
147
|
+
- 用删除线补一句真实感想。
|
|
148
|
+
|
|
149
|
+
如果附了完整实现,代码块就是文章最后一块,后面不再接总结。
|
|
150
|
+
|
|
151
|
+
不要为了完整而补一个概括全文的总结。
|