create-dwy 0.13.1 → 0.13.2
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/bin/index.js +18 -0
- package/package.json +4 -1
- package/src/sync-all.js +154 -0
- package/src/sync-codex.js +27 -8
- package/src/sync.js +25 -8
- package/templates/claude-global/rules/iOS/dwy-swift-style.md +96 -0
- package/templates/claude-global/skills//345/206/205/345/256/271/345/210/233/344/275/234/dwy-article/SKILL.md +164 -0
- package/templates/claude-global/skills//345/206/205/345/256/271/345/210/233/344/275/234/dwy-article/templates/README.md +53 -0
- package/templates/claude-global/skills//345/206/205/345/256/271/345/210/233/344/275/234/dwy-article/templates//344/270/252/344/272/272/346/204/237/346/202/237.md +21 -0
- package/templates/claude-global/skills//345/206/205/345/256/271/345/210/233/344/275/234/dwy-article/templates//345/267/245/345/205/267/350/265/204/346/272/220/345/256/211/345/210/251.md +26 -0
- package/templates/claude-global/skills//345/206/205/345/256/271/345/210/233/344/275/234/dwy-article/templates//346/212/200/346/234/257/346/225/231/347/250/213.md +38 -0
- package/templates/claude-global/skills//345/206/205/345/256/271/345/210/233/344/275/234/dwy-article/templates//350/241/214/344/270/232/350/247/202/345/257/237/350/257/204/350/256/272.md +21 -0
- package/templates/claude-global/skills//345/206/205/345/256/271/345/210/233/344/275/234/dwy-article/templates//350/270/251/345/235/221/346/216/222/351/224/231/345/256/236/345/275/225.md +28 -0
- package/templates/claude-global/skills//345/206/205/345/256/271/345/210/233/344/275/234/dwy-article/templates//351/241/271/347/233/256/345/244/215/347/233/230.md +26 -0
package/bin/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { program } from 'commander'
|
|
|
7
7
|
import { createProject } from '../src/create.js'
|
|
8
8
|
import { syncClaude } from '../src/sync.js'
|
|
9
9
|
import { syncCodex } from '../src/sync-codex.js'
|
|
10
|
+
import { syncAll } from '../src/sync-all.js'
|
|
10
11
|
|
|
11
12
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
12
13
|
const pkg = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf-8'))
|
|
@@ -18,6 +19,9 @@ program
|
|
|
18
19
|
.addHelpText('after', `
|
|
19
20
|
Examples:
|
|
20
21
|
$ dwy create my-app 创建新项目
|
|
22
|
+
$ dwy sync 同步 Claude Code 与 Codex 配置(只选择一次)
|
|
23
|
+
$ dwy sync -i 重新交互式选择后同步到 Claude Code 与 Codex
|
|
24
|
+
$ dwy sync md 同步 CLAUDE.md 到 ~/.claude/ 与 ~/.codex/AGENTS.md
|
|
21
25
|
$ dwy claude sync 同步 Claude 配置(基于已选覆盖)
|
|
22
26
|
$ dwy claude sync -i 重新交互式选择
|
|
23
27
|
$ dwy claude sync --dry-run 预演同步,不写入文件
|
|
@@ -37,6 +41,20 @@ Examples:
|
|
|
37
41
|
$ dwy create my-app
|
|
38
42
|
`)
|
|
39
43
|
|
|
44
|
+
program
|
|
45
|
+
.command('sync [target]')
|
|
46
|
+
.description('同步 Claude Code 与 Codex 配置(target=md 时同步 CLAUDE.md 到两个全局目录)')
|
|
47
|
+
.option('-i, --reselect', '忽略已选缓存,重新交互式选择(已选项默认勾选)')
|
|
48
|
+
.option('--dry-run', '预演模式,不实际写入文件')
|
|
49
|
+
.action((target, opts) => syncAll({ target, ...opts }))
|
|
50
|
+
.addHelpText('after', `
|
|
51
|
+
Examples:
|
|
52
|
+
$ dwy sync 基于已选覆盖同步到 Claude Code 与 Codex
|
|
53
|
+
$ dwy sync -i 重新交互式选择一次,同步到两端
|
|
54
|
+
$ dwy sync --dry-run 预演,不写入文件
|
|
55
|
+
$ dwy sync md 同步 CLAUDE.md 到 ~/.claude/ 与 ~/.codex/AGENTS.md
|
|
56
|
+
`)
|
|
57
|
+
|
|
40
58
|
const claudeCmd = program
|
|
41
59
|
.command('claude')
|
|
42
60
|
.description('Claude Code 配置管理')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-dwy",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.2",
|
|
4
4
|
"description": "项目脚手架与 Claude Code 配置同步工具",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -31,5 +31,8 @@
|
|
|
31
31
|
"publishConfig": {
|
|
32
32
|
"registry": "https://registry.npmjs.org/",
|
|
33
33
|
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"test": "node --test tests/*.test.js"
|
|
34
37
|
}
|
|
35
38
|
}
|
package/src/sync-all.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import fs from 'fs-extra'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import inquirer from 'inquirer'
|
|
4
|
+
import { chalk } from './utils.js'
|
|
5
|
+
import {
|
|
6
|
+
interactiveSelect,
|
|
7
|
+
resolveSourceDir,
|
|
8
|
+
scanCommands,
|
|
9
|
+
scanExisting,
|
|
10
|
+
scanHooks,
|
|
11
|
+
scanRules,
|
|
12
|
+
scanSkills,
|
|
13
|
+
syncClaude,
|
|
14
|
+
} from './sync.js'
|
|
15
|
+
import {
|
|
16
|
+
parseManagedRuleNames,
|
|
17
|
+
scanExistingCodexHooks,
|
|
18
|
+
scanExistingCodexSkills,
|
|
19
|
+
syncCodex,
|
|
20
|
+
} from './sync-codex.js'
|
|
21
|
+
|
|
22
|
+
function unionSets(...sets) {
|
|
23
|
+
return new Set(sets.flatMap(set => [...set]))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function selectFromExisting(scans, existing) {
|
|
27
|
+
return {
|
|
28
|
+
skills: scans.skills.filter(item => existing.skills.has(item.name)),
|
|
29
|
+
rules: scans.rules.filter(item => existing.rules.has(item.name)),
|
|
30
|
+
commands: scans.commands.filter(item => existing.commands.has(item.name)),
|
|
31
|
+
hooks: existing.hooks.size > 0
|
|
32
|
+
? scans.hooks.filter(item => existing.hooks.has(item.name))
|
|
33
|
+
: scans.hooks,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function scanCombinedExisting(projectDir) {
|
|
38
|
+
const claudeDir = path.join(projectDir, '.claude')
|
|
39
|
+
const agentsMdPath = path.join(projectDir, 'AGENTS.md')
|
|
40
|
+
const agentsMdContent = await fs.pathExists(agentsMdPath) ? await fs.readFile(agentsMdPath, 'utf-8') : ''
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
skills: unionSets(
|
|
44
|
+
await scanExisting(claudeDir, 'skills'),
|
|
45
|
+
await scanExistingCodexSkills(projectDir),
|
|
46
|
+
),
|
|
47
|
+
rules: unionSets(
|
|
48
|
+
await scanExisting(claudeDir, 'rules'),
|
|
49
|
+
parseManagedRuleNames(agentsMdContent),
|
|
50
|
+
),
|
|
51
|
+
commands: await scanExisting(claudeDir, 'commands'),
|
|
52
|
+
hooks: unionSets(
|
|
53
|
+
await scanExisting(claudeDir, 'hooks'),
|
|
54
|
+
await scanExistingCodexHooks(projectDir),
|
|
55
|
+
),
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function shouldInitialize(projectDir, existing) {
|
|
60
|
+
const hasAnyExisting = existing.skills.size + existing.rules.size + existing.commands.size + existing.hooks.size > 0
|
|
61
|
+
if (hasAnyExisting) return true
|
|
62
|
+
|
|
63
|
+
const hasConfigDir = await fs.pathExists(path.join(projectDir, '.claude'))
|
|
64
|
+
|| await fs.pathExists(path.join(projectDir, '.codex'))
|
|
65
|
+
|| await fs.pathExists(path.join(projectDir, '.agents'))
|
|
66
|
+
|
|
67
|
+
if (hasConfigDir) return true
|
|
68
|
+
|
|
69
|
+
const { ok } = await inquirer.prompt([{
|
|
70
|
+
type: 'confirm',
|
|
71
|
+
name: 'ok',
|
|
72
|
+
message: `当前目录 ${chalk.yellow(projectDir)} 尚无 Claude Code/Codex 配置,是否在此初始化?`,
|
|
73
|
+
default: false,
|
|
74
|
+
}])
|
|
75
|
+
return ok
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function syncAll({
|
|
79
|
+
target,
|
|
80
|
+
reselect = false,
|
|
81
|
+
dryRun = false,
|
|
82
|
+
sourceDir: sourceDirOverride,
|
|
83
|
+
projectDir: projectDirOverride,
|
|
84
|
+
} = {}) {
|
|
85
|
+
const sourceDir = sourceDirOverride || await resolveSourceDir()
|
|
86
|
+
if (!await fs.pathExists(sourceDir)) {
|
|
87
|
+
console.error(chalk.red('Error: claude-global templates not found in repo'))
|
|
88
|
+
process.exit(1)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (target === 'md') {
|
|
92
|
+
await syncClaude({ target, dryRun, sourceDir })
|
|
93
|
+
await syncCodex({ target, dryRun, sourceDir })
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (target !== undefined) {
|
|
98
|
+
console.error(chalk.red(`Error: unknown sync target "${target}". Supported: (none) | md`))
|
|
99
|
+
process.exit(2)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const projectDir = projectDirOverride || process.cwd()
|
|
103
|
+
|
|
104
|
+
console.log(chalk.blue('\nScanning available Claude Code/Codex configuration...\n'))
|
|
105
|
+
const scans = {
|
|
106
|
+
skills: await scanSkills(sourceDir),
|
|
107
|
+
rules: await scanRules(sourceDir),
|
|
108
|
+
commands: await scanCommands(sourceDir),
|
|
109
|
+
hooks: await scanHooks(sourceDir),
|
|
110
|
+
}
|
|
111
|
+
console.log(chalk.yellow(`Found ${scans.skills.length} skills, ${scans.rules.length} rules, ${scans.commands.length} commands, ${scans.hooks.length} hooks\n`))
|
|
112
|
+
|
|
113
|
+
const existing = await scanCombinedExisting(projectDir)
|
|
114
|
+
if (!await shouldInitialize(projectDir, existing)) {
|
|
115
|
+
console.log(chalk.yellow('\n已取消同步。'))
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const hasAnyExisting = existing.skills.size + existing.rules.size + existing.commands.size + existing.hooks.size > 0
|
|
120
|
+
let selected
|
|
121
|
+
if (!reselect && hasAnyExisting) {
|
|
122
|
+
selected = selectFromExisting(scans, existing)
|
|
123
|
+
console.log(chalk.gray('使用已选覆盖同步(如需调整请加 -i / --reselect):'))
|
|
124
|
+
console.log(chalk.gray(` ${selected.skills.length} skills, ${selected.rules.length} rules, ${selected.commands.length} commands, ${selected.hooks.length} hooks\n`))
|
|
125
|
+
} else {
|
|
126
|
+
if (!reselect && !hasAnyExisting) console.log(chalk.gray('首次同步,进入交互式选择...\n'))
|
|
127
|
+
const result = await interactiveSelect(scans, existing)
|
|
128
|
+
if (result === null) {
|
|
129
|
+
console.log(chalk.yellow('\n已取消同步。'))
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
selected = result
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.log(chalk.blue('\nSyncing Claude Code configuration...\n'))
|
|
136
|
+
await syncClaude({
|
|
137
|
+
sourceDir,
|
|
138
|
+
projectDir,
|
|
139
|
+
selected,
|
|
140
|
+
reselect,
|
|
141
|
+
dryRun,
|
|
142
|
+
skipInitPrompt: true,
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
console.log(chalk.blue('\nSyncing Codex configuration...\n'))
|
|
146
|
+
await syncCodex({
|
|
147
|
+
sourceDir,
|
|
148
|
+
projectDir,
|
|
149
|
+
selected,
|
|
150
|
+
reselect,
|
|
151
|
+
dryRun,
|
|
152
|
+
skipInitPrompt: true,
|
|
153
|
+
})
|
|
154
|
+
}
|
package/src/sync-codex.js
CHANGED
|
@@ -54,7 +54,7 @@ async function ruleToSection(ruleItem) {
|
|
|
54
54
|
return [marker, title, scopeLine, body].filter(Boolean).join('\n\n')
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
function parseManagedRuleNames(content) {
|
|
57
|
+
export function parseManagedRuleNames(content) {
|
|
58
58
|
const block = content.match(BLOCK_RE)
|
|
59
59
|
if (!block) return new Set()
|
|
60
60
|
const names = new Set()
|
|
@@ -94,7 +94,7 @@ async function syncRulesToAgentsMd(selectedRules, proj, dryRun) {
|
|
|
94
94
|
|
|
95
95
|
// ---------- skills → .agents/skills/ ----------
|
|
96
96
|
|
|
97
|
-
async function scanExistingCodexSkills(proj) {
|
|
97
|
+
export async function scanExistingCodexSkills(proj) {
|
|
98
98
|
const dir = path.join(proj, '.agents', 'skills')
|
|
99
99
|
if (!await fs.pathExists(dir)) return new Set()
|
|
100
100
|
const entries = await fs.readdir(dir, { withFileTypes: true })
|
|
@@ -129,7 +129,7 @@ async function removeUnselectedFlat(subdir, existingNames, selectedNames, templa
|
|
|
129
129
|
|
|
130
130
|
// ---------- hooks → .codex/ ----------
|
|
131
131
|
|
|
132
|
-
async function scanExistingCodexHooks(proj) {
|
|
132
|
+
export async function scanExistingCodexHooks(proj) {
|
|
133
133
|
const dir = path.join(proj, '.codex', 'hooks')
|
|
134
134
|
if (!await fs.pathExists(dir)) return new Set()
|
|
135
135
|
const entries = await fs.readdir(dir)
|
|
@@ -258,8 +258,16 @@ async function interactiveSelect(scans, existing) {
|
|
|
258
258
|
* target=undefined 同步到项目:rules→AGENTS.md / skills→.agents/skills / hooks→.codex
|
|
259
259
|
* target='md' 仅同步 CLAUDE.md → 全局 ~/.codex/AGENTS.md
|
|
260
260
|
*/
|
|
261
|
-
export async function syncCodex({
|
|
262
|
-
|
|
261
|
+
export async function syncCodex({
|
|
262
|
+
target,
|
|
263
|
+
reselect = false,
|
|
264
|
+
dryRun = false,
|
|
265
|
+
sourceDir: sourceDirOverride,
|
|
266
|
+
projectDir,
|
|
267
|
+
selected: selectedOverride,
|
|
268
|
+
skipInitPrompt = false,
|
|
269
|
+
} = {}) {
|
|
270
|
+
const claudeGlobal = sourceDirOverride || await resolveSourceDir()
|
|
263
271
|
if (!await fs.pathExists(claudeGlobal)) {
|
|
264
272
|
console.error(chalk.red('Error: claude-global templates not found in repo'))
|
|
265
273
|
process.exit(1)
|
|
@@ -289,7 +297,7 @@ export async function syncCodex({ target, reselect = false, dryRun = false } = {
|
|
|
289
297
|
|
|
290
298
|
const codexGlobal = path.join(claudeGlobal, '..', 'codex-global')
|
|
291
299
|
const hooksJsonSrc = path.join(codexGlobal, 'hooks.json')
|
|
292
|
-
const proj = process.cwd()
|
|
300
|
+
const proj = projectDir || process.cwd()
|
|
293
301
|
|
|
294
302
|
console.log(chalk.blue('\nScanning available config for Codex...\n'))
|
|
295
303
|
|
|
@@ -309,7 +317,11 @@ export async function syncCodex({ target, reselect = false, dryRun = false } = {
|
|
|
309
317
|
}
|
|
310
318
|
const hasAnyExisting = existing.skills.size + existing.rules.size + existing.hooks.size > 0
|
|
311
319
|
|
|
312
|
-
if (!
|
|
320
|
+
if (!selectedOverride
|
|
321
|
+
&& !skipInitPrompt
|
|
322
|
+
&& !hasAnyExisting
|
|
323
|
+
&& !await fs.pathExists(path.join(proj, '.codex'))
|
|
324
|
+
&& !await fs.pathExists(path.join(proj, '.agents'))) {
|
|
313
325
|
const { ok } = await inquirer.prompt([{
|
|
314
326
|
type: 'confirm',
|
|
315
327
|
name: 'ok',
|
|
@@ -323,7 +335,14 @@ export async function syncCodex({ target, reselect = false, dryRun = false } = {
|
|
|
323
335
|
}
|
|
324
336
|
|
|
325
337
|
let selected
|
|
326
|
-
if (
|
|
338
|
+
if (selectedOverride) {
|
|
339
|
+
const selectedNames = key => new Set((selectedOverride[key] || []).map(item => item.name))
|
|
340
|
+
selected = {
|
|
341
|
+
skills: scans.skills.filter(item => selectedNames('skills').has(item.name)),
|
|
342
|
+
rules: scans.rules.filter(item => selectedNames('rules').has(item.name)),
|
|
343
|
+
hooks: scans.hooks.filter(item => selectedNames('hooks').has(item.name)),
|
|
344
|
+
}
|
|
345
|
+
} else if (!reselect && hasAnyExisting) {
|
|
327
346
|
selected = {
|
|
328
347
|
skills: scans.skills.filter(s => existing.skills.has(s.name)),
|
|
329
348
|
rules: scans.rules.filter(r => existing.rules.has(r.name)),
|
package/src/sync.js
CHANGED
|
@@ -103,7 +103,7 @@ export async function scanRules(sourceDir) {
|
|
|
103
103
|
return rules.sort((a, b) => a.name.localeCompare(b.name))
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
async function scanCommands(sourceDir) {
|
|
106
|
+
export async function scanCommands(sourceDir) {
|
|
107
107
|
const commandsDir = path.join(sourceDir, 'commands')
|
|
108
108
|
if (!await fs.pathExists(commandsDir)) return []
|
|
109
109
|
|
|
@@ -163,7 +163,7 @@ async function promptSelection(items, label, defaultNames) {
|
|
|
163
163
|
* 交互式选择:每步可回退到上一步,最后总览支持任意类别重选。
|
|
164
164
|
* 返回 { skills, rules, commands, hooks } 或 null(取消)。
|
|
165
165
|
*/
|
|
166
|
-
async function interactiveSelect(scans, existing) {
|
|
166
|
+
export async function interactiveSelect(scans, existing) {
|
|
167
167
|
const sel = {}
|
|
168
168
|
|
|
169
169
|
// 第一轮:顺序遍历,每步给导航
|
|
@@ -350,8 +350,16 @@ export async function resolveSourceDir() {
|
|
|
350
350
|
* target=undefined 同步项目 .claude/(不动全局 CLAUDE.md)
|
|
351
351
|
* target='md' 仅同步 CLAUDE.md → 全局 ~/.claude/
|
|
352
352
|
*/
|
|
353
|
-
export async function syncClaude({
|
|
354
|
-
|
|
353
|
+
export async function syncClaude({
|
|
354
|
+
target,
|
|
355
|
+
reselect = false,
|
|
356
|
+
dryRun = false,
|
|
357
|
+
sourceDir: sourceDirOverride,
|
|
358
|
+
projectDir,
|
|
359
|
+
selected: selectedOverride,
|
|
360
|
+
skipInitPrompt = false,
|
|
361
|
+
} = {}) {
|
|
362
|
+
const sourceDir = sourceDirOverride || await resolveSourceDir()
|
|
355
363
|
if (!await fs.pathExists(sourceDir)) {
|
|
356
364
|
console.error(chalk.red('Error: claude-global templates not found in repo'))
|
|
357
365
|
process.exit(1)
|
|
@@ -373,13 +381,14 @@ export async function syncClaude({ target, reselect = false, dryRun = false } =
|
|
|
373
381
|
}
|
|
374
382
|
|
|
375
383
|
console.log(chalk.blue('\nScanning available Claude configuration...\n'))
|
|
376
|
-
const
|
|
384
|
+
const projectRoot = projectDir || process.cwd()
|
|
385
|
+
const projectTargetDir = path.join(projectRoot, '.claude')
|
|
377
386
|
|
|
378
|
-
if (!await fs.pathExists(projectTargetDir)) {
|
|
387
|
+
if (!selectedOverride && !skipInitPrompt && !await fs.pathExists(projectTargetDir)) {
|
|
379
388
|
const { shouldCreate } = await inquirer.prompt([{
|
|
380
389
|
type: 'confirm',
|
|
381
390
|
name: 'shouldCreate',
|
|
382
|
-
message: `当前目录 ${chalk.yellow(
|
|
391
|
+
message: `当前目录 ${chalk.yellow(projectRoot)} 未找到 .claude 目录,是否在此创建?`,
|
|
383
392
|
default: false,
|
|
384
393
|
}])
|
|
385
394
|
if (!shouldCreate) {
|
|
@@ -406,7 +415,15 @@ export async function syncClaude({ target, reselect = false, dryRun = false } =
|
|
|
406
415
|
|
|
407
416
|
let selected
|
|
408
417
|
|
|
409
|
-
if (
|
|
418
|
+
if (selectedOverride) {
|
|
419
|
+
const selectedNames = key => new Set((selectedOverride[key] || []).map(item => item.name))
|
|
420
|
+
selected = {
|
|
421
|
+
skills: scans.skills.filter(item => selectedNames('skills').has(item.name)),
|
|
422
|
+
rules: scans.rules.filter(item => selectedNames('rules').has(item.name)),
|
|
423
|
+
commands: scans.commands.filter(item => selectedNames('commands').has(item.name)),
|
|
424
|
+
hooks: scans.hooks.filter(item => selectedNames('hooks').has(item.name)),
|
|
425
|
+
}
|
|
426
|
+
} else if (!reselect && hasAnyExisting) {
|
|
410
427
|
// 缓存模式:模板 ∩ 已选
|
|
411
428
|
selected = {
|
|
412
429
|
skills: scans.skills.filter(s => existing.skills.has(s.name)),
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/*.swift"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Swift 代码规范
|
|
7
|
+
|
|
8
|
+
编辑任何 `.swift` 时遵守。本规则仅覆盖代码写法层(缩进、命名、注释、控制流、UI token 化),是与具体业务无关的通用约定。
|
|
9
|
+
|
|
10
|
+
## 1. 缩进与格式
|
|
11
|
+
|
|
12
|
+
- **4 空格缩进**,不用 Tab。
|
|
13
|
+
- 类型 / 函数声明参数过长时换行,每参数一行,右括号与返回类型 `) -> X` 单起一行。
|
|
14
|
+
- 一行只做一件事,避免把多个 `if` 挤进一行。
|
|
15
|
+
|
|
16
|
+
## 2. 命名
|
|
17
|
+
|
|
18
|
+
- 类型 `UpperCamelCase`,属性 / 方法 / 局部变量 `lowerCamelCase`。
|
|
19
|
+
- **局部短生命周期变量可用短名**(`i` = index、`f` = formatter、`cal` = calendar),同一文件内保持一致。
|
|
20
|
+
- 名字表达意图,避免无信息量的 `data` / `info` / `tmp` 当主力变量名。
|
|
21
|
+
|
|
22
|
+
## 3. 类型声明(iOS 17)
|
|
23
|
+
|
|
24
|
+
状态对象统一 iOS 17 写法,不要 `ObservableObject / @Published / @StateObject`:
|
|
25
|
+
|
|
26
|
+
```swift
|
|
27
|
+
@MainActor // 所有触碰 UI / 主线程隔离状态的类型都标
|
|
28
|
+
@Observable // 替代 ObservableObject
|
|
29
|
+
final class ListViewModel { // 非继承类一律 final
|
|
30
|
+
private let store: DataStore // 依赖私有 + 构造注入
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## 4. 注释(中文,强制全量)
|
|
35
|
+
|
|
36
|
+
**所有代码都要加注释,包括每个类型和每个方法**。注释说明**意图**(为什么这么做、约束是什么),而非复述代码字面。
|
|
37
|
+
|
|
38
|
+
- **类型顶部必加** `///` 文档注释:说明该类型的职责、它在数据流里的位置或关键约束。`class` / `struct` / `enum` / `protocol` / 有逻辑的 `extension` 都算。
|
|
39
|
+
- **每个方法 / 函数必加** `///` 文档注释(公开和私有都要),说明它做什么、入参出参的语义、可能抛出的错误。
|
|
40
|
+
- 关键属性、反直觉分支、易踩坑处用行内 `//` 标注触发条件。
|
|
41
|
+
- 注释里**禁止 emoji**。
|
|
42
|
+
- 不写废话注释(`// 设置 name` 这种复述赋值的删掉),注释要补充代码没说出口的信息。
|
|
43
|
+
|
|
44
|
+
```swift
|
|
45
|
+
/// 列表数据源。负责从持久层读取并按时间分组,供视图直接渲染。
|
|
46
|
+
@MainActor
|
|
47
|
+
@Observable
|
|
48
|
+
final class ListViewModel {
|
|
49
|
+
|
|
50
|
+
/// 重新加载全部数据并写回 `grouped`。
|
|
51
|
+
/// 失败时透传持久层错误,由调用方决定提示策略。
|
|
52
|
+
func reload() throws {
|
|
53
|
+
let items = try store.all() // 持久层已保证非 nil,无需兜底
|
|
54
|
+
grouped = group(items)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## 5. 控制流:信任上游,不过度防御
|
|
60
|
+
|
|
61
|
+
只在系统外边界(用户输入、文件 / 网络读入、外部 API)校验;内部调用信任类型签名。
|
|
62
|
+
|
|
63
|
+
```swift
|
|
64
|
+
// 好:early return,无多余 else
|
|
65
|
+
guard !isBusy else { return }
|
|
66
|
+
withAnimation(.spring(response: 0.5, dampingFraction: 0.75)) { isBusy = true }
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- 不写 `value ?? default` 兜已有明确来源的值。
|
|
70
|
+
- 不写 `try? { ... }` 吞异常掩盖 bug;只 catch 预期的具体错误,其余让它抛。
|
|
71
|
+
- 持久 / IO 层方法用 `throws` 透传错误,由调用方决定处理,不在底层吞。
|
|
72
|
+
|
|
73
|
+
## 6. 图标:矢量图,禁止 emoji
|
|
74
|
+
|
|
75
|
+
UI 图标**一律 SF Symbols 矢量图**(`Image(systemName:)`),任何视图、文案、注释都**禁止 emoji**:
|
|
76
|
+
|
|
77
|
+
```swift
|
|
78
|
+
// 好:SF Symbols 矢量图标
|
|
79
|
+
Image(systemName: "checkmark")
|
|
80
|
+
.font(.system(size: 11, weight: .bold))
|
|
81
|
+
.foregroundStyle(.white)
|
|
82
|
+
|
|
83
|
+
// 禁止:emoji 当图标
|
|
84
|
+
Text("✅") // 不允许
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## 7. 颜色 / 字体 / 动效 token 化
|
|
88
|
+
|
|
89
|
+
- 颜色集中到设计 token(如 `ColorTokens.*` / `theme.*`),**禁止在视图里散落硬编码颜色字面量**。
|
|
90
|
+
- 字体用 `.font(.system(size:weight:))` 或集中的字体 token。
|
|
91
|
+
- 动效统一用一组 `.spring(response:dampingFraction:)` 参数,不用 `.easeInOut`,保持全局手感一致。
|
|
92
|
+
|
|
93
|
+
## 8. 文案与时间
|
|
94
|
+
|
|
95
|
+
- 所有 UI 文案走 `String(localized: "some_key")`,**禁止硬编码字面量**;key 命名风格全项目统一。
|
|
96
|
+
- 业务逻辑里「现在」作为依赖注入(`now: @escaping () -> Date = { Date() }`),方便单测固定时间,禁止在逻辑里直接散用 `Date()`。
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dwy-article
|
|
3
|
+
description: 用「我」的口吻把素材写成微信公众号图文文章,输出 Markdown(可直接喂 markdown-nice / 秀米 转排版)。涉及以下任何场景**必须**使用此 skill(即使用户没点名 'dwy-article'):用户说『写一篇公众号』『帮我写篇推文/分享』『把这个 markdown / 草稿整理成公众号』『润色成公众号』『写篇技术分享 / 个人感悟』,或给一个中心思想 + 约束让我扩写成文,或丢一篇技术笔记 / 草稿让我去掉 AI 味、改成像他本人写的。覆盖多种文章类型,骨架模板放在本 skill 的 templates/ 目录、可随时扩展:技术教程、个人感悟、项目复盘、踩坑排错实录、工具资源安利、行业观察评论等。核心目标——简洁清晰、像一个 IT 全栈开发者本人随手写的,绝不能有 AI 范儿(套话结构词、空洞金句、说教礼貌腔、emoji 标题党)。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# dwy-article
|
|
7
|
+
|
|
8
|
+
用「我」的口吻,把一个中心思想、一份提纲、或一篇半成品草稿,写成一篇能直接发的微信公众号图文。输出纯 Markdown。
|
|
9
|
+
|
|
10
|
+
「我」是一个 **IT 全栈开发者**,写公众号是为了分享技术和想法,不是为了 KPI。读者大多是同行或愿意照着步骤动手的人。
|
|
11
|
+
|
|
12
|
+
## 为什么这个 skill 存在
|
|
13
|
+
|
|
14
|
+
公众号读者点开一篇文章,想看的是「一个真人朋友在跟我聊他踩过的坑、想明白的事」,不是一篇百度百科。AI 写的东西一眼能看出来——四平八稳、面面俱到、每段都在升华、用一堆「首先其次综上所述」把话说圆。这种味道一冒出来,读者立刻出戏,信任也没了。
|
|
15
|
+
|
|
16
|
+
这个 skill 的全部工作就是:**把内容写对,同时把 AI 马脚全部抹掉**,让文章读起来像我本人随手写的。
|
|
17
|
+
|
|
18
|
+
## 输入 → 输出
|
|
19
|
+
|
|
20
|
+
**输入**可能是三种形态,先判断是哪种:
|
|
21
|
+
|
|
22
|
+
| 输入 | 我该做什么 |
|
|
23
|
+
|---|---|
|
|
24
|
+
| 一个中心思想 / 一句话观点 + 约束 | 以这个观点为骨,扩写成完整文章 |
|
|
25
|
+
| 一份提纲 / 几个要点 | 按要点组织,补充论证、例子、细节 |
|
|
26
|
+
| 一篇写好的 Markdown 草稿 | **去 AI 味、重排结构、润色语气**——但不改立场、不改事实、不擅自加观点 |
|
|
27
|
+
|
|
28
|
+
**输出 = 落盘一个 `.md` 文件(强制最终产物,不是只在对话里贴正文)。** 文章写完、脱敏后,**必须用 Write 把正文存到本机文件**,否则就是没交付:
|
|
29
|
+
|
|
30
|
+
- **位置**:当前工作目录(cwd),就放在用户手边。
|
|
31
|
+
- **文件名**:`<年月日时分>-<标题>.md`,如 `202606011430-frps打洞.md`。时间戳放最前面,是为了归档时按文件名排序就等于按时间排序。时间用 `date +%Y%m%d%H%M` 取当前时刻(12 位:年月日时分);标题取文章 `#` 标题压成简短 slug(去掉空格和 `/ \ : * ? " < > |` 等非法字符,保留中文,别太长)。
|
|
32
|
+
- **文件内容**:纯 Markdown 正文,**脱敏后**的版本。不要「以下是文章:」这类包裹话,不要 front-matter。用 `#` 标题、`##` 小标题、``` 代码块、`>` 引用、`-` 列表(列表少用,见下文)。
|
|
33
|
+
- 写完在对话里告诉用户**完整绝对路径**,再附上「真值 → 假值」脱敏对照表(对照表只在对话里,不进文件)。
|
|
34
|
+
|
|
35
|
+
为什么强制落盘:用户拿成稿去发公众号,要的是一个能直接编辑、导进 markdown-nice / 秀米 排版的 `.md` 文件,不是对话里一段读完就散的文字。只贴在对话里 = 没交付。
|
|
36
|
+
|
|
37
|
+
**关键边界**:拿到的是已有草稿时,我只动语气、结构、AI 味,**不碰作者的技术结论、观点、立场和事实**。这是润色,不是替他重写思想。中心思想模糊、或拿不准是技术类还是感悟类时,**先问一句再写**,别自己脑补一个观点硬塞进去。
|
|
38
|
+
|
|
39
|
+
## 脱敏:真实敏感信息一律换成假值(安全红线)
|
|
40
|
+
|
|
41
|
+
作者常把本地真实的配置、日志、命令输出、截图文字直接喂进来——里面藏着真 IP、token、域名、真实路径。**这些真实值绝不能出现在文章里。** 文章发出去就收不回,泄露一个 token 或内网拓扑可能直接被人拿去打,当成红线对待。输出前把它们全换成假值。
|
|
42
|
+
|
|
43
|
+
### 要换的(出现就替换)
|
|
44
|
+
|
|
45
|
+
| 类型 | 换成 |
|
|
46
|
+
|---|---|
|
|
47
|
+
| 公网 IP | 文档保留段 `203.0.113.x`(必要时 `198.51.100.x` / `192.0.2.x`)——RFC 5737 专用于文档,绝不会撞到真实资产 |
|
|
48
|
+
| 内网 IP | 常见私网示例 `192.168.1.x` / `10.0.0.x`,换掉真实那个即可 |
|
|
49
|
+
| 域名 / 主机名 | `example.com` / `example.net`(RFC 2606 保留),或 `your-domain.com` |
|
|
50
|
+
| token / 密钥 / 密码 / API key / Secret | **明显占位**,如 `your-token-here`、`改成你自己的密钥`——哪怕看着像随机串也必须换;**不用逼真假串**,免得读者当真值照抄 |
|
|
51
|
+
| 数据库连接串 | host 换文档值、密码换占位:`postgresql://user:your-password@example.com:5432/mydb` |
|
|
52
|
+
| SSH 用户名 / 含真名的真实路径 | 通用名:`/Users/you/`、`/home/ubuntu/`、用户名 `deploy`(真名是 PII) |
|
|
53
|
+
| 邮箱 / 手机号 | `you@example.com` / `138xxxx0000` |
|
|
54
|
+
| 云 AccessKey / 账号 ID / MAC / 设备序列号 | 明显假占位 |
|
|
55
|
+
|
|
56
|
+
### 不要动的(动了教程反而错)
|
|
57
|
+
|
|
58
|
+
`127.0.0.1`、`0.0.0.0`、`::1`、`localhost`、标准端口(22 / 80 / 443 / 7000 等)、公共 DNS(`8.8.8.8` / `1.1.1.1`)、开源项目的公开域名(`github.com` 等)、本来就是示例的假值。
|
|
59
|
+
|
|
60
|
+
### 两条铁律
|
|
61
|
+
|
|
62
|
+
- **一致性**:同一个真值,全篇映射到**同一个**假值;相互关联的值(配置里的 `serverAddr` 和日志里出现的同一 IP)要换成同一个。否则教程和日志前后对不上,读者照着做就错。
|
|
63
|
+
- **正文干净 + 对话留底**:文章正文只放脱敏后的内容;写完后,**在文章之外(对话里,别写进正文)**简短列一下「真值 → 假值」做了哪些替换,方便核对没漏、也方便作者自己留底。
|
|
64
|
+
|
|
65
|
+
拿不准某个值算不算敏感?**当成敏感处理**——漏换一个真 token 的代价,远大于多换一个无所谓的值。
|
|
66
|
+
|
|
67
|
+
## 别露 AI 马脚(这是重点,全程对照)
|
|
68
|
+
|
|
69
|
+
下面每一条都解释了为什么——理解了「为什么」才能在新场景里举一反三,而不是机械避词。
|
|
70
|
+
|
|
71
|
+
### 1. 不用模板结构词
|
|
72
|
+
|
|
73
|
+
AI 爱用一套把话说圆的连接词,真人聊天根本不这么说。
|
|
74
|
+
|
|
75
|
+
**禁用**:首先 / 其次 / 再者 / 最后(机械罗列时)、综上所述 / 总而言之 / 总的来说 / 归根结底、值得注意的是 / 需要指出的是 / 不难发现 / 众所周知、在当今……的时代 / 随着……的发展、不仅……而且(成对堆排比时)。
|
|
76
|
+
|
|
77
|
+
为什么:这些词的作用是「让结构显得完整」,但真人写东西靠的是想法本身的连贯,不靠连接词撑场面。该换行就换行,该跳就跳。
|
|
78
|
+
|
|
79
|
+
### 2. 不写空洞金句和升华结尾
|
|
80
|
+
|
|
81
|
+
**最容易暴露 AI 的就是最后一段。** AI 总忍不住在结尾把全文复述一遍再拔高一句,像作文。
|
|
82
|
+
|
|
83
|
+
- ❌「综上所述,通过本文的介绍,相信你已经掌握了 frps 的配置方法。希望这篇文章对你有所帮助,我们下期再见!」
|
|
84
|
+
- ✅「到这内网的服务就能从公网访问了。临时调试用 ngrok 更省事;要长期跑、还想白嫖自己那台云服务器,frps 这套更划算。」
|
|
85
|
+
|
|
86
|
+
为什么:结尾不是用来总结的,是用来落地的。技术类落到「能用了 + 一句延伸或取舍建议」,感悟类落到「一个具体的、很个人的判断或动作」。不复述、不上价值、不替读者做人生总结。
|
|
87
|
+
|
|
88
|
+
### 3. 敢下判断,别和稀泥
|
|
89
|
+
|
|
90
|
+
AI 怕得罪人,什么都「因人而异」「视情况而定」。真人有偏见、有立场。
|
|
91
|
+
|
|
92
|
+
- ❌「理财是否重要因人而异,建议根据自身实际情况谨慎决策。」
|
|
93
|
+
- ✅「我的结论很简单:哪怕你月薪三千,也得懂点理财。不是让你去炒股,是别让通胀和你的懒把那点钱慢慢吃掉。」
|
|
94
|
+
|
|
95
|
+
为什么:读者关注一个人,是想听他**真实的判断**,不是听一段免责声明。早点亮明态度,敢说「我不推荐 X」「这个坑我卡了俩小时」。
|
|
96
|
+
|
|
97
|
+
### 4. 上具体的,别泛泛而谈
|
|
98
|
+
|
|
99
|
+
去 AI 味最有效的一招:**用真实的细节**——真实命令、真实报错、真实数字、真实踩坑经过。AI 爱说正确的废话,真人说具体的事。
|
|
100
|
+
|
|
101
|
+
- ❌「在配置过程中可能会遇到一些问题,需要仔细检查相关配置。」
|
|
102
|
+
- ✅「我第一次起服务端起不来,报 `bind port already in use`——7000 端口被另一个进程占了,`lsof -i:7000` 一看是之前没杀干净的旧进程。」
|
|
103
|
+
|
|
104
|
+
### 5. 少用列表,多用段落
|
|
105
|
+
|
|
106
|
+
AI 一上来就把什么都拆成 1. 2. 3.,每条一句话。真人会把有逻辑关系的点串成一段话来说。
|
|
107
|
+
|
|
108
|
+
为什么:列表适合**真正并列的步骤或清单**(命令清单、配置项)。但论证、观点、过程,用段落更像人说话,有起承转合。拿不准就用段落。
|
|
109
|
+
|
|
110
|
+
### 6. 句子有长短、有口语、有呼吸
|
|
111
|
+
|
|
112
|
+
长短句交错,多用短句和断句。允许碎片句、口语词(其实、说白了、反正、我一开始也以为、踩过坑)。偶尔用破折号或括号补一句吐槽——就像真人聊天那样插一嘴。一个意思一句话能说完,就不要写三句。
|
|
113
|
+
|
|
114
|
+
### 7. 标题朴素,正文不放 emoji
|
|
115
|
+
|
|
116
|
+
标题说人话、直接讲清这篇讲什么,可以带点个人态度,但**不标题党、不震惊体、不堆夸张数字**。正文和小标题都不用 emoji。
|
|
117
|
+
|
|
118
|
+
为什么:标题党和 emoji 堆砌是公众号最廉价的 AI / 营销号味,跟「一个工程师认真分享」的气质冲突。
|
|
119
|
+
|
|
120
|
+
## 选模板(固定骨架放在 templates/)
|
|
121
|
+
|
|
122
|
+
每篇都套一个**固定骨架**:锚点的顺序和命名都固定——这是我的栏目感和辨识度,读者一看就知道是我写的。骨架不写死在这份 SKILL 里,全部放在本 skill 的 `templates/` 目录,方便随时加新类型。
|
|
123
|
+
|
|
124
|
+
**写作前先选模板:**
|
|
125
|
+
|
|
126
|
+
1. `ls` 本 skill 的 `templates/` 目录,看有哪些类型(文件名就是类型名,如 `技术教程.md`、`项目复盘.md`、`踩坑排错实录.md`)。
|
|
127
|
+
2. 按文章内容挑最匹配的一个 `Read` 进来。拿不准时打开一两个看顶部 `适用场景` 再定。
|
|
128
|
+
3. 照那个文件里的固定锚点骨架写,锚点齐全、按顺序。
|
|
129
|
+
4. 都不太匹配?挑最接近的,或先问用户一句想要哪种结构,别硬套。
|
|
130
|
+
|
|
131
|
+
**两条通用规则,对所有模板都成立:**
|
|
132
|
+
|
|
133
|
+
- **固定的是结构,不是语气**:锚点按模板来,但每一块里面的话仍要保持人味(短句、口语、敢下判断、上真实细节),别因为有了模板就写得四平八稳。
|
|
134
|
+
- **含步骤 / 命令的模板(如教程、排错),步骤区要齐到能复现**:命令完整、占位符标清、给预期结果、零跳步。这种工整**不算** AI 味——反 AI 味管的是语气和废话,不管操作的严谨。
|
|
135
|
+
|
|
136
|
+
> 加新类型只需往 `templates/` 丢一个新 `.md`(格式见 `templates/README.md`),不用改这份 SKILL。
|
|
137
|
+
|
|
138
|
+
## 写作流程
|
|
139
|
+
|
|
140
|
+
1. **判断输入形态,并想好属于哪类文章**(教程 / 感悟 / 复盘 / 排错 / 安利 / 观察…)。模糊就先问一句,别脑补。
|
|
141
|
+
2. **定中心思想**:用户给了观点就以它为准;给了草稿就守住他的观点和事实。
|
|
142
|
+
3. **从 `templates/` 选对应模板,读进来照骨架写**:锚点齐全、按顺序,全程对照「别露 AI 马脚」七条。含步骤的类型额外保证零跳步、命令完整、能照着复现。
|
|
143
|
+
4. **脱敏**:把正文里所有真实敏感值(IP / token / 域名 / 真名路径等)按「脱敏」一节的规则换成假值,同一真值保持一致。
|
|
144
|
+
5. **交稿前过一遍自检清单**(下方),自己改掉问题再交。
|
|
145
|
+
6. **落盘**(强制):`date +%Y%m%d%H%M` 取当前时刻,用 Write 把脱敏后的正文存成 `<年月日时分>-<标题slug>.md` 到当前工作目录;然后在对话里给出**完整绝对路径** + 「真值 → 假值」对照表。**别忘了这步——只在对话里贴正文不算交付。**
|
|
146
|
+
|
|
147
|
+
## 交稿前自检清单
|
|
148
|
+
|
|
149
|
+
逐条过,发现就改:
|
|
150
|
+
|
|
151
|
+
- [ ] **脱敏过了吗(红线)?** 正文里还有没有真实 IP / token / 域名 / 真名路径 / 邮箱手机号?同一真值换的假值前后一致吗?拿不准的当敏感处理。
|
|
152
|
+
- [ ] 选的模板里那几个固定锚点,文章是不是都齐了、按顺序排?
|
|
153
|
+
- [ ] (含步骤的类型)每步命令完整可复制吗?要替换的占位符标清了吗?给了预期结果吗?有没有「配置一下」这种跳步?**一个没碰过的人照着,能不能跑通?**
|
|
154
|
+
- [ ] 有没有「首先 / 其次 / 综上 / 值得注意的是」这类结构词?删掉重写
|
|
155
|
+
- [ ] 最后一段是不是在复述全文 + 升华?改成具体落点
|
|
156
|
+
- [ ] 通篇有没有真实细节(命令 / 数字 / 报错 / 经历),还是全在说正确的废话?
|
|
157
|
+
- [ ] 观点是不是在和稀泥、面面俱到?敢不敢下一个判断?
|
|
158
|
+
- [ ] 列表是不是太多?能改成段落的改掉
|
|
159
|
+
- [ ] 标题是不是标题党 / 震惊体?emoji 是不是冒出来了?
|
|
160
|
+
- [ ] 标点是不是混入了英文逗号 / 英文括号?正文一律用中文标点(代码块内除外)
|
|
161
|
+
- [ ] 出声读一遍:像不像我这个人在跟朋友说话?
|
|
162
|
+
- [ ] **落盘了吗?** 是不是已经用 Write 把文章存成 `<年月日时分>-<标题>.md`,并把完整绝对路径告诉了用户?
|
|
163
|
+
|
|
164
|
+
倒数第二条最重要:如果读起来还是「一篇被生成出来的文章」,那就没写对,回去改。最后一条是交付红线:没存文件等于没干完。
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# 文章模板目录
|
|
2
|
+
|
|
3
|
+
这里每个 `.md` 是一个文章类型的固定骨架。写作时 SKILL.md 会让模型扫这个目录、按文件名 + `适用场景` 挑一个读进来用。
|
|
4
|
+
|
|
5
|
+
## 怎么加一个新模板
|
|
6
|
+
|
|
7
|
+
往这个目录丢一个 `.md` 文件就行,**SKILL.md 不用改**。
|
|
8
|
+
|
|
9
|
+
### 文件格式
|
|
10
|
+
|
|
11
|
+
````
|
|
12
|
+
---
|
|
13
|
+
适用场景: 一句话说清什么内容用这个模板(写作时靠这句来匹配)
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# XX 模板
|
|
17
|
+
|
|
18
|
+
定位:一句话,这类文章的写法重心是什么。
|
|
19
|
+
|
|
20
|
+
固定锚点,按顺序:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
# 标题:……
|
|
24
|
+
[锚点1] ……
|
|
25
|
+
## 锚点2
|
|
26
|
+
……
|
|
27
|
+
[落点] ……
|
|
28
|
+
```
|
|
29
|
+
````
|
|
30
|
+
|
|
31
|
+
### 两条要求
|
|
32
|
+
|
|
33
|
+
1. **文件名**用能一眼看懂的中文类型名(如 `项目复盘.md`)——写作时先靠文件名挑模板。
|
|
34
|
+
2. 顶部 **`适用场景`** 写清楚——文件名撞车或拿不准时靠它区分。
|
|
35
|
+
|
|
36
|
+
### 边界
|
|
37
|
+
|
|
38
|
+
- 骨架里的锚点会成为文章的固定栏目(栏目感 = 个人辨识度)。
|
|
39
|
+
- 模板只管**结构**;语气、反 AI 味规则、输入输出约定统一在 SKILL.md,模板别重复写。
|
|
40
|
+
- 含步骤 / 命令 / 日志的类型(教程、排错),步骤区要可复现、工整,这不算 AI 味。
|
|
41
|
+
|
|
42
|
+
## 现有模板
|
|
43
|
+
|
|
44
|
+
| 文件 | 什么时候用 |
|
|
45
|
+
|---|---|
|
|
46
|
+
| 技术教程.md | 教别人搭起来 / 跑通某个东西,要能复现 |
|
|
47
|
+
| 个人感悟.md | 分享一个想明白的判断 / 观点 |
|
|
48
|
+
| 工具资源安利.md | 推荐一个好用的工具 / 库 / 服务 |
|
|
49
|
+
| 项目复盘.md | 做完一个项目 / 功能的经验总结 |
|
|
50
|
+
| 踩坑排错实录.md | 一次具体 bug / 故障的排查全过程 |
|
|
51
|
+
| 行业观察评论.md | 对某个 IT 新闻 / 趋势 / 热点的看法 |
|
|
52
|
+
|
|
53
|
+
> 这张表是给人看的索引,不影响选模板逻辑——加新模板可以顺手补一行,忘了也不影响功能。
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
适用场景: 分享一个想明白的判断 / 观点——AI 会不会裁员、要不要懂理财、要不要进大厂等
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# 个人感悟模板
|
|
6
|
+
|
|
7
|
+
定位:分享一个**想明白的判断**,用经历和生活比方说服人。
|
|
8
|
+
|
|
9
|
+
固定锚点,按顺序(感悟文别硬加一堆小标题割裂阅读,锚点是内在骨架;论点多时才用 1-2 个小标题分段):
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
# 标题:一个判断或一个问题,带点个人态度(如「AI 会让程序员失业吗?说点实话」)
|
|
13
|
+
|
|
14
|
+
[由头] 1-2 段:从一个具体的人 / 事 / 对话 / 新闻 / 数字切入。绝不用「在 AI 飞速发展的今天」这种宏大开场。
|
|
15
|
+
|
|
16
|
+
[我的判断] 早早亮明立场,一句话,别绕。
|
|
17
|
+
|
|
18
|
+
[展开] 用经历、例子、生活比方论证。可以分几个角度(角度多时用小标题分段),但角度之间别强行排比对齐——长短不一才真实。敢有偏见。
|
|
19
|
+
|
|
20
|
+
[落点] 落到一个具体的、可执行的或很个人的判断 / 动作。不升华、不上价值、不替读者做人生总结。
|
|
21
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
适用场景: 推荐一个好用的工具 / 库 / 服务 / 软件,讲清楚它好在哪、自己怎么用
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# 工具资源安利模板
|
|
6
|
+
|
|
7
|
+
定位:以**自己的真实使用**为底,安利一个东西。不是搬官网介绍,是说「我用了,值不值得你也上」。
|
|
8
|
+
|
|
9
|
+
固定锚点,按顺序:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
# 标题:直接说推荐什么、解决什么(如「这个内网穿透工具,我用了半年」)
|
|
13
|
+
|
|
14
|
+
[由头] 1-2 句:你之前被什么问题烦着,怎么发现这东西的。
|
|
15
|
+
|
|
16
|
+
## 它是什么
|
|
17
|
+
一句话说清它干嘛、解决什么痛点。别堆官网那套介绍。
|
|
18
|
+
|
|
19
|
+
## 我怎么用的
|
|
20
|
+
真实的使用场景 + 关键操作 / 配置(按需给命令或标注截图位)。说你自己的用法,不是复述文档。
|
|
21
|
+
|
|
22
|
+
## 好在哪、差在哪
|
|
23
|
+
敢说优点也敢说短板,别只夸。跟你用过的同类比一句。
|
|
24
|
+
|
|
25
|
+
[落点] 适不适合你:什么人 / 什么场景值得上,什么情况别用。附上链接位。
|
|
26
|
+
```
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
适用场景: 教别人把某个东西搭起来 / 跑通——内网穿透、部署、环境配置、工具搭建等,读者照着步骤要能复现
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# 技术教程模板
|
|
6
|
+
|
|
7
|
+
定位:**照着零跳步就能复现**。步骤必须齐、命令必须完整。但省掉原理和术语科普——不解释「为什么」,只给「怎么做」,保持精简。
|
|
8
|
+
|
|
9
|
+
固定锚点,按顺序:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
# 标题:直接说清做什么(如「用 frps 把群晖 NAS 暴露到公网」)
|
|
13
|
+
|
|
14
|
+
[开篇] 1-2 句:要解决什么问题 / 什么场景用得上。不铺垫、不讲原理。
|
|
15
|
+
|
|
16
|
+
## 开始前准备
|
|
17
|
+
把前置条件一次性列全,读者照着先备齐:系统 / 软件版本 / 下载链接 / 需要的账号 / 要放行的端口 / 依赖。漏一项,读者就会卡在中途。
|
|
18
|
+
|
|
19
|
+
## 步骤 1 · 这步在干嘛
|
|
20
|
+
先一句说这步做什么,再给**完整、可直接复制**的命令或操作:
|
|
21
|
+
- 命令不截断、不省略,含真实路径和文件名
|
|
22
|
+
- 凡是要读者替换的,用占位符写清并标注「改成你自己的 X」(如 `serverAddr = "你的公网IP"`)
|
|
23
|
+
- 给出预期结果:看到什么输出 / 什么现象,才算这步成了
|
|
24
|
+
## 步骤 2 · 下一步在干嘛
|
|
25
|
+
同上。**零跳步**——不能出现「配置一下 xxx」这种含糊带过,每一步都要能照着做出来。
|
|
26
|
+
|
|
27
|
+
(步骤按实际数量往下排)
|
|
28
|
+
|
|
29
|
+
## 踩坑提醒
|
|
30
|
+
把坑单独成一节,每个坑写清三件事:现象(报什么错 / 什么表现)→ 原因 → 怎么解。让读者对照自己卡在哪。
|
|
31
|
+
|
|
32
|
+
## 验证
|
|
33
|
+
怎么确认整套通了——一条命令或一个现象。
|
|
34
|
+
|
|
35
|
+
[收尾] 一句:到这就能用了。可选加一句取舍或延伸(什么场景用别的方案更省事 / 还能怎么扩展)。不升华。
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
省的是原理,不是步骤。一个没碰过的人照着这篇,要能真跑通。
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
适用场景: 对某个 IT 新闻 / 趋势 / 热点的看法——新模型发布、大厂裁员、技术风向、政策变化等
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# 行业观察评论模板
|
|
6
|
+
|
|
7
|
+
定位:就一个热点亮出**自己的判断**,用事实和经历支撑。别假装中立面面俱到,读者要的是你怎么看。
|
|
8
|
+
|
|
9
|
+
固定锚点,按顺序:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
# 标题:一个观点或一个发问,带态度(不标题党、不震惊体)
|
|
13
|
+
|
|
14
|
+
[由头] 从具体的事件 / 新闻 / 数据切入,一两句交代清楚发生了什么。
|
|
15
|
+
|
|
16
|
+
[我的观点] 早早亮明你怎么看,别和稀泥。
|
|
17
|
+
|
|
18
|
+
[展开] 用事实、亲身经历、行业里的例子论证。可以分角度,敢有偏见。别堆「一方面…另一方面…」装客观。
|
|
19
|
+
|
|
20
|
+
[落点] 落到一个具体判断,或对读者的实际影响(这事跟你有什么关系、该怎么应对)。不空喊、不升华。
|
|
21
|
+
```
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
适用场景: 一次具体 bug / 故障的排查全过程——线上报错、偶发问题、诡异现象的破案实录
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# 踩坑排错实录模板
|
|
6
|
+
|
|
7
|
+
定位:还原一次真实的破案过程。**把弯路也写上**,读者跟着你一起查才有代入感、才学得到。
|
|
8
|
+
|
|
9
|
+
固定锚点,按顺序:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
# 标题:什么现象 + 一句话定性(如「线上接口偶发 502,查了两天是它」)
|
|
13
|
+
|
|
14
|
+
[现象] 1-2 段:什么场景下出的问题,表现是什么,多严重。给真实的报错 / 日志。
|
|
15
|
+
|
|
16
|
+
## 排查过程
|
|
17
|
+
按你**真实的排查顺序**走:怀疑过什么 → 怎么验证 → 排除了什么。把走过的弯路、错误的猜测也写上,这才真实有用。
|
|
18
|
+
|
|
19
|
+
## 根因
|
|
20
|
+
最后定位到的真正原因,讲清楚为什么会这样。
|
|
21
|
+
|
|
22
|
+
## 解决
|
|
23
|
+
怎么修的,给完整的改动 / 命令。顺带说一句怎么防复发。
|
|
24
|
+
|
|
25
|
+
[落点] 这事的泛化教训——同类问题以后怎么更快定位。不升华。
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
含命令 / 日志 / 报错的地方要完整真实,工整不算 AI 味。
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
适用场景: 做完一个项目 / 功能后的经验总结,重点是决策、踩坑和「再来一次怎么改」
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# 项目复盘模板
|
|
6
|
+
|
|
7
|
+
定位:复盘一段真实经历,把决策和教训讲给同行。**诚实**比好看重要,敢暴露翻车。
|
|
8
|
+
|
|
9
|
+
固定锚点,按顺序:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
# 标题:做了什么 + 一句结果或教训(如「重构这个老项目,我踩了三个月的坑」)
|
|
13
|
+
|
|
14
|
+
[背景] 1-2 段:这是个什么项目,为什么做,当时什么状况、什么约束。
|
|
15
|
+
|
|
16
|
+
## 怎么做的
|
|
17
|
+
关键决策和路径,不写流水账。重点说「**为什么这么选**」,而不是「做了 1234」。
|
|
18
|
+
|
|
19
|
+
## 踩了哪些坑
|
|
20
|
+
最有价值的一节。具体的坑 + 当时怎么解的,别怕暴露翻车——读者就爱看这个。
|
|
21
|
+
|
|
22
|
+
## 再来一次怎么改
|
|
23
|
+
诚实说哪里做错了、哪里绕了远路、下次怎么做。
|
|
24
|
+
|
|
25
|
+
[落点] 一条能带走的经验,落到具体可复用的判断,不喊口号、不升华。
|
|
26
|
+
```
|