@hupan56/wlkj 2.1.0 → 2.2.1

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/cli.js CHANGED
@@ -7,6 +7,23 @@ const { execSync } = require("child_process");
7
7
 
8
8
  const T = path.join(__dirname, "..", "templates");
9
9
 
10
+ // 当前 npm 包版本(从 package.json 读,不硬编码)
11
+ const PKG_VERSION = JSON.parse(
12
+ fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf-8")
13
+ ).version;
14
+
15
+ // 升级时受保护的用户数据文件(绝不覆盖)
16
+ // 这些文件含团队/机器特定配置,覆盖 = 数据丢失
17
+ const PROTECTED_FILES = new Set([
18
+ "config.yaml", // 团队 git 仓库 URL、平台映射
19
+ "settings.json", // 本地权限/hook 配置
20
+ ]);
21
+
22
+ // 本地状态文件(升级时也绝不碰,但不算"受保护配置",不提示合并)
23
+ const LOCAL_STATE_FILES = new Set([
24
+ ".developer", ".current-task", ".engine-version",
25
+ ]);
26
+
10
27
  function py(script, args = []) {
11
28
  const p = path.join(process.cwd(), ".qoder", "scripts", script);
12
29
  if (!fs.existsSync(p)) { console.log("请先运行: npx wlkj init"); process.exit(1); }
@@ -48,10 +65,16 @@ function doInit(name) {
48
65
 
49
66
  // === 1. 拷贝完整引擎 (镜像 .qoder/ 结构) ===
50
67
  // 源: templates/qoder/* 目标: .qoder/*
68
+ // 注意: init 用 "update" 模式的保护逻辑更安全 —— 新装时目标文件不存在,
69
+ // 保护逻辑不触发; 重跑 init 时 config.yaml/settings.json 不会被覆盖。
51
70
  const qoderSrc = path.join(T, "qoder");
52
71
  let copied = 0;
53
72
  if (fs.existsSync(qoderSrc)) {
54
- copied = copyDirRecursive(qoderSrc, path.join(cwd, ".qoder"));
73
+ const r = copyDirRecursive(qoderSrc, path.join(cwd, ".qoder"), "update");
74
+ copied = r.copied;
75
+ if (r.protectedN > 0) {
76
+ console.log(` [保护] ${r.protectedN} 个配置文件未覆盖(新版存为 .new)`);
77
+ }
55
78
  }
56
79
  console.log(` 引擎: ${copied} 个文件 (${hasExisting ? "更新" : "新建"})`);
57
80
 
@@ -86,7 +109,8 @@ function doInit(name) {
86
109
  const baseGitignore = [
87
110
  "# Source code repos (cloned by git_sync.py)", "data/code/",
88
111
  "", "# AI pipeline runtime", ".qoder/.developer", ".qoder/.current-task",
89
- ".qoder/.runtime/", "", "# Personal learning", ".qoder/learning/feedback.jsonl",
112
+ ".qoder/.engine-version", ".qoder/.runtime/", "",
113
+ "# Personal learning", ".qoder/learning/feedback.jsonl",
90
114
  "", "# Machine-local", "data/index/.last-sync", "data/index/.prd-collected.json",
91
115
  "data/index/.index-meta.json", "data/index/.sync-lock", "data/index/.file-keys.json",
92
116
  "data/index/.inverted-cache.json", "data/index/*.corrupt",
@@ -113,8 +137,11 @@ function doInit(name) {
113
137
  console.log(` setup.py 未找到, 跳过自动初始化`);
114
138
  }
115
139
 
140
+ // === 7. 写版本戳 ===
141
+ writeEngineVersion(cwd);
142
+
116
143
  console.log(`\n${"=".repeat(50)}`);
117
- console.log(` 安装完成!`);
144
+ console.log(` 安装完成! (v${PKG_VERSION})`);
118
145
  console.log(`${"=".repeat(50)}`);
119
146
  console.log(`\n 现在可以开始了:`);
120
147
  console.log(` 在 Qoder 里说 "写个 XX 的需求"`);
@@ -123,31 +150,98 @@ function doInit(name) {
123
150
  console.log(` 环境问题? python .qoder/scripts/init_doctor.py --fix\n`);
124
151
  }
125
152
 
126
- // 递归拷贝目录, 返回文件数
127
- function copyDirRecursive(src, dst) {
128
- let count = 0;
129
- if (!fs.existsSync(src)) return 0;
153
+ // 递归拷贝目录, 返回 {copied, protected, skipped}
154
+ // mode: "init" (新装, 全拷) | "update" (升级, 保护用户数据)
155
+ function copyDirRecursive(src, dst, mode = "init") {
156
+ let copied = 0, protectedN = 0, skipped = 0;
157
+ if (!fs.existsSync(src)) return { copied, protectedN, skipped };
130
158
  fs.mkdirSync(dst, { recursive: true });
131
159
  for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
132
160
  if (entry.name === "__pycache__" || entry.name.endsWith(".pyc")) continue;
133
161
  const s = path.join(src, entry.name);
134
162
  const d = path.join(dst, entry.name);
135
163
  if (entry.isDirectory()) {
136
- count += copyDirRecursive(s, d);
164
+ const r = copyDirRecursive(s, d, mode);
165
+ copied += r.copied; protectedN += r.protectedN; skipped += r.skipped;
137
166
  } else {
138
- // 不覆盖已存在的 (增量更新, 保留用户改动)
139
- // 但 .py 和 .md / config.yaml / rules 始终更新 (引擎文件)
140
167
  const isEngine = entry.name.endsWith(".py") || entry.name.endsWith(".md") ||
141
168
  entry.name.endsWith(".yaml") || entry.name.endsWith(".yml") ||
142
169
  entry.name.endsWith(".toml") || entry.name.endsWith(".json") ||
143
170
  entry.name.endsWith(".html") || entry.name.endsWith(".txt");
144
- if (isEngine || !fs.existsSync(d)) {
145
- fs.copyFileSync(s, d);
171
+
172
+ if (mode === "update") {
173
+ // 升级模式: 受保护文件绝不覆盖
174
+ if (PROTECTED_FILES.has(entry.name) && fs.existsSync(d)) {
175
+ // 新版内容存为 .new 让用户手动合并(仅当内容不同)
176
+ if (!filesEqual(s, d)) {
177
+ fs.copyFileSync(s, d + ".new");
178
+ protectedN++;
179
+ } else {
180
+ skipped++;
181
+ }
182
+ continue;
183
+ }
184
+ // 引擎文件: 无条件刷新(纯引擎, 不含用户数据)
185
+ // 非引擎文件: 已存在跳过, 不存在才建
186
+ if (isEngine || !fs.existsSync(d)) {
187
+ fs.copyFileSync(s, d);
188
+ copied++;
189
+ } else {
190
+ skipped++;
191
+ }
192
+ } else {
193
+ // init 模式: 引擎文件全覆盖, 非引擎已存在跳过 (保留用户改动)
194
+ if (isEngine || !fs.existsSync(d)) {
195
+ fs.copyFileSync(s, d);
196
+ }
197
+ copied++;
146
198
  }
147
- count++;
148
199
  }
149
200
  }
150
- return count;
201
+ return { copied, protectedN, skipped };
202
+ }
203
+
204
+ // 两文件内容是否相同(先比大小再比内容)
205
+ function filesEqual(a, b) {
206
+ try {
207
+ const sa = fs.statSync(a), sb = fs.statSync(b);
208
+ if (sa.size !== sb.size) return false;
209
+ return fs.readFileSync(a).equals(fs.readFileSync(b));
210
+ } catch { return false; }
211
+ }
212
+
213
+ // 版本戳: .qoder/.engine-version 存当前引擎版本
214
+ function writeEngineVersion(cwd) {
215
+ try {
216
+ const p = path.join(cwd, ".qoder", ".engine-version");
217
+ fs.mkdirSync(path.dirname(p), { recursive: true });
218
+ fs.writeFileSync(p, PKG_VERSION + "\n", "utf-8");
219
+ } catch { /* 不阻塞 */ }
220
+ }
221
+
222
+ function readEngineVersion(cwd) {
223
+ try {
224
+ const p = path.join(cwd, ".qoder", ".engine-version");
225
+ if (fs.existsSync(p)) return fs.readFileSync(p, "utf-8").trim();
226
+ } catch { /* */ }
227
+ return null;
228
+ }
229
+
230
+ // 跑 install_qoderwork.py(升级时强制刷新 commands)
231
+ function runQoderWorkInstall(cwd, forceCommands) {
232
+ const script = path.join(cwd, ".qoder", "scripts", "install_qoderwork.py");
233
+ if (!fs.existsSync(script)) {
234
+ console.log(" [跳过] install_qoderwork.py 不存在");
235
+ return;
236
+ }
237
+ const args = ["python", `"${script}"`];
238
+ if (forceCommands) args.push("--force-commands");
239
+ try {
240
+ console.log(`\n--- 刷新 QoderWork ---`);
241
+ execSync(args.join(" "), { cwd, stdio: "inherit", timeout: 60000 });
242
+ } catch (e) {
243
+ console.log(` QoderWork 刷新失败 (不阻塞): ${(e.message || "").slice(0, 80)}`);
244
+ }
151
245
  }
152
246
 
153
247
  function doStatus() {
@@ -165,12 +259,80 @@ function doStatus() {
165
259
  const n = fs.readdirSync(wsSpecs).filter(f => f.endsWith(".md")).length;
166
260
  console.log(`PRDs: ${n}`);
167
261
  }
262
+ const ver = readEngineVersion(process.cwd());
263
+ console.log(`engine: ${ver || "未知"} (最新: v${PKG_VERSION})${ver === PKG_VERSION ? " [最新]" : ver ? " [可升级: npx @hupan56/wlkj update]" : ""}`);
264
+ console.log("");
265
+ }
266
+
267
+ function doUpdate() {
268
+ const cwd = process.cwd();
269
+ const oldVer = readEngineVersion(cwd);
270
+ const qoderSrc = path.join(T, "qoder");
271
+
272
+ // 前置检查: 必须已装过
273
+ if (!fs.existsSync(path.join(cwd, ".qoder", "scripts"))) {
274
+ console.log("\n 未检测到已安装的引擎。首次安装请用:");
275
+ console.log(" npx @hupan56/wlkj init [你的名字]\n");
276
+ return;
277
+ }
278
+
279
+ console.log(`\nwlkj update${oldVer ? " " + oldVer : ""} -> v${PKG_VERSION}\n`);
280
+
281
+ // === 1. 刷新引擎文件(保护 config.yaml / settings.json)===
282
+ let copied = 0, protectedN = 0, skipped = 0;
283
+ if (fs.existsSync(qoderSrc)) {
284
+ const r = copyDirRecursive(qoderSrc, path.join(cwd, ".qoder"), "update");
285
+ copied = r.copied; protectedN = r.protectedN; skipped = r.skipped;
286
+ }
287
+ console.log(` 引擎刷新: ${copied} 个文件${skipped ? ` / ${skipped} 无变化` : ""}`);
288
+
289
+ // 报告受保护文件
290
+ if (protectedN > 0) {
291
+ console.log(` [保护] ${protectedN} 个配置文件已保留你的本地设置:`);
292
+ console.log(` 新版模板存为 .new 文件, 如需合并新结构请手动对比:`);
293
+ // 列出实际生成的 .new 文件
294
+ try {
295
+ for (const f of fs.readdirSync(path.join(cwd, ".qoder"))) {
296
+ if (f.endsWith(".new")) {
297
+ console.log(` .qoder/${f}`);
298
+ }
299
+ }
300
+ } catch { /* */ }
301
+ } else {
302
+ console.log(` config.yaml / settings.json: 已保护(未改动)`);
303
+ }
304
+
305
+ // === 2. 根文件(文档类, 安全覆盖)===
306
+ ["root/AGENTS.md", "root/新手指南.md"].forEach(f => {
307
+ const src = path.join(T, f);
308
+ if (fs.existsSync(src)) {
309
+ fs.copyFileSync(src, path.join(cwd, path.basename(f)));
310
+ }
311
+ });
312
+ console.log(` 根文档: 已更新`);
313
+
314
+ // === 3. 刷新 QoderWork commands(强制覆盖已存在的)===
315
+ runQoderWorkInstall(cwd, true);
316
+
317
+ // === 4. 写版本戳 ===
318
+ writeEngineVersion(cwd);
319
+
320
+ console.log(`\n${"=".repeat(50)}`);
321
+ console.log(` 升级完成! (v${PKG_VERSION})`);
322
+ console.log(`${"=".repeat(50)}`);
323
+ console.log(`\n 生效方式:`);
324
+ console.log(` Qoder IDE / Quest: 新建对话即可`);
325
+ console.log(` QoderWork: 重启应用或新建对话`);
326
+ if (protectedN > 0) {
327
+ console.log(`\n ⚠ 有配置文件新版有变化, 生成 .new 文件:`);
328
+ console.log(` 确认无需合并后可直接删除, 或手动合并后删除`);
329
+ }
168
330
  console.log("");
169
331
  }
170
332
 
171
333
  function doHelp() {
172
334
  console.log("");
173
- console.log("wlkj - AI 产品研发工作流 (v2.0)");
335
+ console.log(`wlkj - AI 产品研发工作流 (v${PKG_VERSION})`);
174
336
  console.log("");
175
337
  console.log("=== 环境安装 (什么都没装时先跑这个) ===");
176
338
  console.log(" npx @hupan56/wlkj install-env 检测+自动装 Node/Python/git");
@@ -179,23 +341,26 @@ function doHelp() {
179
341
  console.log("=== 一键安装 (装好环境后) ===");
180
342
  console.log(" npx @hupan56/wlkj init [你的名字] 安装完整引擎 + 自动初始化");
181
343
  console.log("");
344
+ console.log("=== 升级 (已装用户, 新版本发布后) ===");
345
+ console.log(" npx @hupan56/wlkj update 刷新引擎, 保护你的配置");
346
+ console.log("");
182
347
  console.log("=== 安装后怎么用 ===");
183
348
  console.log(" 在 Qoder (IDE/Quest/QoderWork) 里:");
184
349
  console.log(" 说中文: '写个 XX 的需求' '查一下 XX 代码' '建个任务'");
185
350
  console.log(" 或斜杠: /wl-prd /wl-search /wl-task /wl-status /wl-report");
186
351
  console.log("");
187
352
  console.log("=== 状态 ===");
188
- console.log(" npx wlkj status 查看当前状态");
353
+ console.log(" npx @hupan56/wlkj status 查看当前状态 + 引擎版本");
189
354
  console.log("");
190
355
  console.log("=== 环境修复 ===");
191
356
  console.log(" python .qoder/scripts/init_doctor.py --fix 自动修复环境");
192
357
  console.log(" python .qoder/scripts/setup.py 重新初始化");
193
358
  console.log("");
194
359
  console.log("=== Git (中文命令) ===");
195
- console.log(" npx wlkj 提交PRD 提交 PRD");
196
- console.log(" npx wlkj 提交任务 提交任务");
197
- console.log(" npx wlkj 提交 全部提交并推送");
198
- console.log(" npx wlkj 拉取最新 / 同步 git pull");
360
+ console.log(" npx @hupan56/wlkj 提交PRD 提交 PRD");
361
+ console.log(" npx @hupan56/wlkj 提交任务 提交任务");
362
+ console.log(" npx @hupan56/wlkj 提交 全部提交并推送");
363
+ console.log(" npx @hupan56/wlkj 拉取最新 / 同步 git pull");
199
364
  console.log("");
200
365
  }
201
366
 
@@ -285,6 +450,7 @@ const mapped = rest.map(a => a === "-p" ? "--priority" : a);
285
450
 
286
451
  switch (cmd) {
287
452
  case "init": doInit(rest[0]); break;
453
+ case "update": case "upgrade": doUpdate(); break;
288
454
  case "install-env": doInstallEnv(rest[0] === "--check"); break;
289
455
  case "task": process.stdout.write(py("task.py", mapped)); break;
290
456
  case "status": doStatus(); break;
package/package.json CHANGED
@@ -1,12 +1,28 @@
1
1
  {
2
2
  "name": "@hupan56/wlkj",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
4
4
  "description": "AI Product R&D Workflow - PRD/Prototype/Search/Task/Report",
5
- "bin": { "wlkj": "./bin/cli.js" },
6
- "files": ["bin/", "templates/"],
7
- "keywords": ["workflow", "ai", "prd", "pipeline", "qoder", "product", "team"],
5
+ "bin": {
6
+ "wlkj": "bin/cli.js"
7
+ },
8
+ "files": [
9
+ "bin/",
10
+ "templates/"
11
+ ],
12
+ "keywords": [
13
+ "workflow",
14
+ "ai",
15
+ "prd",
16
+ "pipeline",
17
+ "qoder",
18
+ "product",
19
+ "team"
20
+ ],
8
21
  "license": "MIT",
9
- "publishConfig": { "access": "public" },
10
- "engines": { "node": ">=16" }
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "engines": {
26
+ "node": ">=16"
27
+ }
11
28
  }
12
-
@@ -2,8 +2,11 @@
2
2
  """
3
3
  install_qoderwork.py - 把 .qoder/skills/ 安装到 QoderWork 桌面端可识别的位置
4
4
 
5
- QoderWork 从 %USERPROFILE%\\.qoderworkcn\\skills\\ 加载技能(每个子目录一个 skill)。
6
- 本项目源在 <repo>/.qoder/skills/,QoderWork 不会读项目目录,必须软链/拷过去。
5
+ QoderWork 从 %USERPROFILE%\\.qoderwork\\skills\\ 加载技能(每个子目录一个 skill),
6
+ %USERPROFILE%\\.qoderwork\\commands\\ 加载斜杠命令。
7
+ 本项目源在 <repo>/.qoder/skills/ 和 <repo>/.qoder/commands/,QoderWork 不会读项目
8
+ 目录,必须软链/拷过去。
9
+ (注: 旧版本误用 ~/.qoderworkcn/,已废弃,官方路径为 ~/.qoderwork/)
7
10
 
8
11
  为什么用 junction(mklink /J)而不是 symlink(mklink /D):
9
12
  - junction 不需要管理员权限,普通用户可建
@@ -85,6 +88,16 @@ def find_source_skills() -> list:
85
88
  return result
86
89
 
87
90
 
91
+ def file_equal(a: Path, b: Path) -> bool:
92
+ """快速判断两文件内容是否相同(先比大小再比内容,避免无谓全读)。"""
93
+ try:
94
+ if a.stat().st_size != b.stat().st_size:
95
+ return False
96
+ return a.read_bytes() == b.read_bytes()
97
+ except OSError:
98
+ return False
99
+
100
+
88
101
  def create_junction(link: Path, target: Path) -> bool:
89
102
  """用 mklink /J 创建 junction。返回是否成功。"""
90
103
  # 用 errors="replace" 防止 Windows GBK 输出(如"为...创建的联接")触发 UnicodeDecodeError
@@ -194,6 +207,8 @@ def main():
194
207
  help="仅检查状态,不改动")
195
208
  parser.add_argument("--copy", action="store_true",
196
209
  help="用拷贝代替 junction(非 Windows 或不想软链时用)")
210
+ parser.add_argument("--force-commands", action="store_true",
211
+ help="强制覆盖已存在的 command 文件(升级时用;默认已存在则跳过)")
197
212
  args = parser.parse_args()
198
213
 
199
214
  print("=" * 56)
@@ -292,7 +307,7 @@ def main():
292
307
  # 同时安装 commands (让 QoderWork 用户级也能看到 /wl-* 命令)
293
308
  if action in ("install", "check") and SOURCE_COMMANDS_DIR.is_dir():
294
309
  print("\n--- Commands (/wl-* 斜杠命令) ---")
295
- cmd_count = {"ok": 0, "new": 0}
310
+ cmd_count = {"ok": 0, "new": 0, "upd": 0}
296
311
  for cmd_file in sorted(SOURCE_COMMANDS_DIR.glob("wl-*.md")):
297
312
  name = cmd_file.name # e.g. wl-prd.md
298
313
  target = QODERWORK_COMMANDS_DIR / name
@@ -302,18 +317,31 @@ def main():
302
317
  else:
303
318
  print(f" [MISS] {name}")
304
319
  else: # install
305
- if target.exists():
306
- cmd_count["ok"] += 1
307
- else:
320
+ need_copy = False
321
+ if not target.exists():
322
+ need_copy = True
323
+ elif args.force_commands:
324
+ # 强制刷新(升级场景):仅当内容不同才覆盖,避免无谓写
325
+ import shutil
326
+ if not file_equal(cmd_file, target):
327
+ need_copy = True
328
+ if need_copy:
308
329
  try:
309
330
  QODERWORK_COMMANDS_DIR.mkdir(parents=True, exist_ok=True)
310
- # commands 是单文件, 用拷贝 (不像 skills 是目录用 junction)
311
331
  import shutil
312
332
  shutil.copy2(str(cmd_file), str(target))
313
- cmd_count["new"] += 1
333
+ if target.exists() and args.force_commands:
334
+ cmd_count["upd"] += 1
335
+ else:
336
+ cmd_count["new"] += 1
314
337
  except OSError as e:
315
338
  print(f" [ERR] {name}: {e}")
316
- print(f" commands: {cmd_count['new']} 新建 / {cmd_count['ok']} 已存在")
339
+ else:
340
+ cmd_count["ok"] += 1
341
+ msg = f" commands: {cmd_count['new']} 新建 / {cmd_count['ok']} 已存在"
342
+ if cmd_count["upd"]:
343
+ msg += f" / {cmd_count['upd']} 刷新"
344
+ print(msg)
317
345
 
318
346
  if action == "install" and counts["err"] == 0:
319
347
  print("\n✓ 安装完成。重启 QoderWork(或新建对话)后技能生效。")
@@ -15,7 +15,7 @@ Usage:
15
15
  python search_index.py --list
16
16
  python search_index.py --vben
17
17
  """
18
- import os, json, sys
18
+ import os, json, sys, time
19
19
 
20
20
  # UTF-8 stdio (防御性: stdout 被捕获时不崩溃)
21
21
  try:
@@ -69,7 +69,53 @@ def _match_keyword(query_word, kw):
69
69
  return False
70
70
 
71
71
 
72
+ def _search_cache_key(query, platform):
73
+ """缓存 key: query|platform|keyword-index mtime。"""
74
+ import hashlib
75
+ ki_path = os.path.join(BASE, 'data', 'index', 'keyword-index.json') if os.path.isdir(os.path.join(BASE, 'data', 'index')) else None
76
+ mtime = '%d' % os.path.getmtime(ki_path) if ki_path and os.path.isfile(ki_path) else '0'
77
+ raw = '{}|{}|{}'.format(query.lower(), platform or '', mtime)
78
+ return hashlib.md5(raw.encode('utf-8')).hexdigest()[:16]
79
+
80
+
72
81
  def search_keywords(query, platform=None):
82
+ # 结果缓存: 同 query+platform+索引未变 → 直接返回缓存输出
83
+ cache_key = _search_cache_key(query, platform)
84
+ cache_path = os.path.join(BASE, '.qoder', '.runtime', 'search-cache-%s.txt' % cache_key)
85
+ if os.path.isfile(cache_path):
86
+ try:
87
+ age = time.time() - os.path.getmtime(cache_path)
88
+ if age < 86400: # 24h 有效
89
+ with open(cache_path, encoding='utf-8') as f:
90
+ sys.stdout.write(f.read())
91
+ return
92
+ except OSError:
93
+ pass
94
+
95
+ # 缓存未命中, 正常计算并写入缓存
96
+ import io
97
+ old_stdout = sys.stdout
98
+ sys.stdout = buf = io.StringIO()
99
+ try:
100
+ _search_keywords_impl(query, platform)
101
+ finally:
102
+ sys.stdout = old_stdout
103
+ output = buf.getvalue()
104
+ sys.stdout.write(output)
105
+ # 写缓存 (best-effort)
106
+ try:
107
+ cache_dir = os.path.join(BASE, '.qoder', '.runtime')
108
+ os.makedirs(cache_dir, exist_ok=True)
109
+ tmp = cache_path + '.tmp'
110
+ with open(tmp, 'w', encoding='utf-8') as f:
111
+ f.write(output)
112
+ os.replace(tmp, cache_path)
113
+ except OSError:
114
+ pass
115
+
116
+
117
+ def _search_keywords_impl(query, platform=None):
118
+ """实际的搜索逻辑 (原 search_keywords 内容)。"""
73
119
  ki = load_index('keyword-index.json', hint='Run: python git_sync.py')
74
120
  if ki is None:
75
121
  return
@@ -0,0 +1,36 @@
1
+ ---
2
+ name: wl-code
3
+ description: "按开发 Spec 严格实现代码(/wl-code 命令入口)。Implement code following a Spec strictly. 用户说'开始写代码''按规格实现''实现这个接口''把这个需求做出来'或输入 /wl-code 时触发。需用户确认(DANGEROUS)。"
4
+ trigger: "Spec 已确认待实现;用户说'开始写代码''按spec实现''/wl-code'"
5
+ ---
6
+
7
+ # wl-code — 按 Spec 实现代码(/wl-code 命令入口)
8
+
9
+ > 这是 `/wl-code` 命令的 skill 入口。让 QoderWork 的 `/` 列表也能用 `/wl-code`。
10
+ > ⚠️ DANGEROUS 操作(写代码),执行前向用户确认范围。
11
+ > 完整规则见功能型 skill `.qoder/skills/spec-coder/SKILL.md`
12
+ > 和命令 `.qoder/commands/wl-code.md`。
13
+
14
+ ## ⚙️ 自取上下文(Quest / QoderWork 无 hook 注入,必须自读)
15
+
16
+ - `.qoder/.developer` — 当前开发者
17
+ - `.qoder/.current-task` — 当前活动任务(找对应的 spec)
18
+ - 活动任务的 spec:`workspace/specs/` 或 `workspace/tasks/{id}/spec.md`
19
+
20
+ ## 执行
21
+
22
+ 1. **加载 Spec**:按 REQ-ID 找 spec;没参数就读 `.qoder/.current-task`
23
+ 2. **搜相关代码**:
24
+ ```bash
25
+ python .qoder/scripts/search_index.py <关键词>
26
+ ```
27
+ 读相关文件了解现有模式。
28
+ 3. **按 spec-coder skill 实现**:严格按 spec,遵循 data/code/ 的团队约定
29
+ 4. **自检**:
30
+ - [ ] Spec 所有需求都覆盖
31
+ - [ ] 无 TODO/FIXME
32
+ - [ ] 风格跟周围代码一致
33
+ - [ ] 有错误处理
34
+ 5. **报告**:告诉用户实现了什么,建议下一步 `/wl-test`
35
+
36
+ 详细编码规范见 `.qoder/skills/spec-coder/SKILL.md`。
@@ -0,0 +1,73 @@
1
+ ---
2
+ name: wl-prd
3
+ description: "生成 PRD + 平台匹配的原型(/wl-prd 命令入口)。Generate PRD with platform-aware prototype. 用户说'写个需求''生成PRD''做个原型''保单这块加个功能''写个产品需求文档'或输入 /wl-prd 时触发。先问平台!"
4
+ trigger: "用户描述需求、要 PRD、要原型、要 mockup,或直接 /wl-prd"
5
+ ---
6
+
7
+ # wl-prd — PRD + 原型生成(/wl-prd 命令入口)
8
+
9
+ > 这是 `/wl-prd` 命令的 skill 入口。QoderWork 的 `/` 列表读 skills,
10
+ > 所以本 skill 让 QoderWork 也能用 `/wl-prd`,与 Qoder IDE 保持一致。
11
+ > 完整工作流见同名 command 文件 `.qoder/commands/wl-prd.md` 和
12
+ > 功能型 skill `.qoder/skills/prd-generator/SKILL.md`。
13
+
14
+ ## ⚠️ STEP 0: 必须先问平台(任何分析前)
15
+
16
+ ```
17
+ 这个需求是针对哪个平台?
18
+
19
+ 1. Web 管理端 (fywl-ui) - Ant Design Vue + VxeGrid 风格
20
+ 2. APP 移动端 (Carmg-H5) - Vant 风格
21
+ 3. 两端都要
22
+
23
+ 请选择 (1/2/3):
24
+ ```
25
+
26
+ **绝不自动判断。绝不假设。绝不跳过。永远先问。等用户回答。**
27
+
28
+ | 回答 | 平台 | 项目 | 搜索 flag | 原型模板 |
29
+ |------|------|------|-----------|----------|
30
+ | 1 / Web / PC / 管理端 | Web | fywl-ui | `--platform web` | prototype-web.html |
31
+ | 2 / APP / H5 / 移动端 | APP | Carmg-H5 | `--platform app` | prototype-app.html |
32
+ | 3 / 都要 / 两端 | Both | Both | 都跑 | 两个模板,两份原型 |
33
+
34
+ ## ⚙️ 自取上下文(Quest / QoderWork 无 hook 注入,必须自读)
35
+
36
+ 会话开始先读(不存在就跳过,不要报错中断):
37
+ - `.qoder/.developer` — 当前开发者名(产出存到 `workspace/members/{dev}/drafts/`)
38
+ - `.qoder/.current-task` — 当前活动任务
39
+ - `data/index/.index-meta.json` — 知识图谱新鲜度(过期提示先 `/wl-init`,不阻塞)
40
+
41
+ ## 完整流程
42
+
43
+ 按 `.qoder/commands/wl-prd.md` 执行(含 Reference/Brainstorm/Planning/Quick
44
+ 四种模式)。核心步骤:
45
+
46
+ 1. **一次取全上下文**(fast path):
47
+ ```bash
48
+ python .qoder/scripts/context_pack.py <业务关键词> --platform <web|app>
49
+ ```
50
+ 2. 读历史 PRD、业务草稿、风格 token
51
+ 3. 一次性确认发现(不要一条一条问)
52
+ 4. 生成 PRD + 原型(从平台模板开始,填真实字段,diff 高亮)
53
+ 5. **EVA 质量门禁**:
54
+ ```bash
55
+ python .qoder/scripts/eval_prd.py <draft-prd.md> <prototype.html>
56
+ ```
57
+ <80% 就修到 PASS。
58
+ 6. 发布 + 自动同步:
59
+ ```bash
60
+ python .qoder/scripts/team_sync.py push
61
+ ```
62
+
63
+ 详细生成规则、样式 token、原型规则见:
64
+ - 功能型 skill:`.qoder/skills/prd-generator/SKILL.md`
65
+ - 原型规则:`.qoder/skills/prototype-generator/SKILL.md`
66
+ - 命令完整定义:`.qoder/commands/wl-prd.md`
67
+
68
+ ## REQ-ID 分配(并发安全)
69
+
70
+ **必须**用原子分配器,禁止手动扫描最大值:
71
+ ```bash
72
+ python -c "import sys; sys.path.insert(0,'.qoder/scripts'); from common.reqid import allocate_req_id; n=allocate_req_id(); print('REQ-%d-%03d' % (__import__('datetime').date.today().year, n))"
73
+ ```
@@ -0,0 +1,39 @@
1
+ ---
2
+ name: wl-spec
3
+ description: "从已确认 PRD 生成开发 Spec 文档(/wl-spec 命令入口)。Generate or review technical Spec from PRD + Design. 用户说'生成规格''写spec''出技术方案''把PRD转开发文档'或输入 /wl-spec 时触发。"
4
+ trigger: "PRD 确认/发布后;用户说'生成Spec''写技术规格''/wl-spec'"
5
+ ---
6
+
7
+ # wl-spec — 生成开发 Spec(/wl-spec 命令入口)
8
+
9
+ > 这是 `/wl-spec` 命令的 skill 入口。让 QoderWork 的 `/` 列表也能用 `/wl-spec`。
10
+ > 完整规则见功能型 skill `.qoder/skills/spec-generator/SKILL.md`
11
+ > 和命令 `.qoder/commands/wl-spec.md`。
12
+
13
+ ## ⚙️ 自取上下文(Quest / QoderWork 无 hook 注入,必须自读)
14
+
15
+ - `.qoder/.developer` — 当前开发者
16
+ - `.qoder/.current-task` — 当前任务(决定 spec 存哪个 task 目录)
17
+ - 扫描 PRD 来源:`data/docs/prd/`(已发布)+
18
+ `workspace/members/{dev}/drafts/REQ-*.md`(草稿),找出还没 Spec 的 PRD
19
+ - 字段命名约定:`python .qoder/scripts/search_index.py --field <字段名>`
20
+ - 团队 Java 约定(MyBatis Plus + RESTful + BigDecimal 金额)见
21
+ `.qoder/skills/spec-generator/SKILL.md` 的 Step 3
22
+
23
+ ## 执行
24
+
25
+ 1. 定位 PRD(按 REQ-ID 或最新发布的)
26
+ 2. 读 PRD + 相关代码(用 search_index.py 找现有实现模式)
27
+ 3. 按 spec-generator skill 生成 spec.md(接口/字段/数据模型/验收标准)
28
+ 4. 存到 `workspace/specs/spec-{REQ-ID}-{desc}.md`
29
+ 5. 通知开发评审
30
+ 6. 发布后自动同步:
31
+ ```bash
32
+ python .qoder/scripts/team_sync.py push
33
+ ```
34
+
35
+ ## Review 模式
36
+
37
+ 带 `review` 参数 → 读现有 spec 做评审,给出改进建议,不重新生成。
38
+
39
+ 详细生成规则见 `.qoder/skills/spec-generator/SKILL.md`。
@@ -0,0 +1,40 @@
1
+ ---
2
+ name: wl-test
3
+ description: "为已实现代码生成单元测试(/wl-test 命令入口)。Generate unit tests for implemented code. 用户说'写单元测试''补测试''覆盖一下''写个test'或输入 /wl-test 时触发。需用户确认(DANGEROUS)。"
4
+ trigger: "代码实现完成;用户说'写测试''补单测''生成test''/wl-test'"
5
+ ---
6
+
7
+ # wl-test — 生成单元测试(/wl-test 命令入口)
8
+
9
+ > 这是 `/wl-test` 命令的 skill 入口。让 QoderWork 的 `/` 列表也能用 `/wl-test`。
10
+ > ⚠️ DANGEROUS 操作(写测试文件),执行前向用户确认。
11
+ > 完整规则见功能型 skill `.qoder/skills/test-generator/SKILL.md`
12
+ > 和命令 `.qoder/commands/wl-test.md`。
13
+
14
+ ## ⚙️ 自取上下文(Quest / QoderWork 无 hook 注入,必须自读)
15
+
16
+ - `.qoder/.developer` — 当前开发者
17
+ - `.qoder/.current-task` — 当前活动任务(找刚实现的代码)
18
+ - 活动 task 的实现代码 + 对应 spec
19
+
20
+ ## 执行
21
+
22
+ 1. **找目标代码**:
23
+ ```bash
24
+ python .qoder/scripts/search_index.py <类名或关键词>
25
+ ```
26
+ 没参数就用当前 task 的实现代码。
27
+ 2. **读代码 + spec**:理解要测什么
28
+ 3. **按 test-generator skill 生成**,覆盖:
29
+ - 每个函数的 happy path
30
+ - 边界值(null/空/边界)
31
+ - 错误处理
32
+ - 集成点
33
+ 4. **自检**:
34
+ - [ ] 测试能跑
35
+ - [ ] 覆盖主场景
36
+ - [ ] 断言正确
37
+ - [ ] 遵循团队测试约定
38
+ 5. **报告**:告诉用户生成了哪些测试,建议下一步 `/wl-commit`
39
+
40
+ 详细测试规范见 `.qoder/skills/test-generator/SKILL.md`。
@@ -1,5 +0,0 @@
1
- # Qoder CLI 项目级配置
2
- # 文档: https://docs.qoder.com/zh/cli/permissions
3
-
4
- # 权限模式: auto = 自动批准 (不弹确认)
5
- approval_mode = "auto"