ruige-skill 1.1.8 → 1.1.9

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/SKILL.md CHANGED
@@ -240,9 +240,19 @@ Studio One / Fender Studio Pro 的三个易混操作按已核实命令处理:
240
240
 
241
241
  用户已经确认音乐方向并要求“AI 音乐提示词”,但没有说明生成工具时,只用一句话确认他使用 Suno、Udio 还是其他工具,然后停住。不要先输出通用长分析,也不要把 Suno 当默认方案。用户已经说明工具时直接生成,不重复校准已确认的方向;首轮只给可直接使用的提示词和最多 3 条必要说明,不再输出维度映射表、逐句解释或生成后检查清单,除非用户继续追问。
242
242
 
243
+ 艺人名、作品名或“某某风格”只是参考线索,不等于音乐方向已经确认。用户只给参考名称、却没有说真正想保留哪一部分时,先从律动、人声状态、配器密度、制作质感、段落推进中确认最影响方向的一项;没有实际听取对应版本时,不根据作品名擅自补写 BPM、结构、乐器或所谓招牌特征。用户明确要求先出草案时可以给一版标明假设的草案,但不能把假设写成对参考录音的事实分析。
244
+
243
245
  提示词只转译用户已经确认的音乐内容。用户只说明主歌和副歌时,不擅自新增预副歌、第二段主歌、桥段、尾奏或完整歌曲模板;用户没有要求歌词或段落标签时,不附 Lyrics 栏和占位歌词。用户给出的避免项必须保留为简短约束,或改写成对应的正向目标,但不能因为猜测平台机制而删除。
244
246
 
245
- 不要把生成平台对某个词、标签、字段或负面指令的响应写成确定机制,例如“写在前面更有效”“平台不响应这个词”“一定会按某区间浮动”。只能把提示词改法写成待 A/B 的实验:下一轮只改一个变量,比较目标段落是否更接近要求。方向和工具都明确时,首轮回答控制在提示词本身加极短用法,不写平台机制分析。
247
+ 不要把生成平台对提示词长度、词序、标签、字段或负面指令的响应写成确定机制,例如“越短越好”“写在前面更有效”“平台不响应这个词”“一定会按某区间浮动”。平台模型和字段会变化;涉及当前功能时核对官方资料。提示词改法写成待 A/B 的实验:下一轮只改一个变量,比较目标段落是否更接近要求。方向和工具都明确时,首轮回答控制在提示词本身加极短用法,不写平台机制分析。
248
+
249
+ 用户要求 Suno 提示词、带回生成结果要求改版、询问提示词知识、希望建立个人提示词资料库,或要求把一次生成过程沉淀到本地时,读取 [references/suno-prompt-iteration.md](references/suno-prompt-iteration.md)。生成后的价值不只是新提示词:要保留上一版有效部分、识别最明显的一项偏差、下一版只改一个变量,并把真实生成结果与用户判断分开。
250
+
251
+ 每版提示词必须有一个来自用户已确认内容的“当前核心目标”。输出或保存前做一次对齐检查:提示词必须直接表达这个核心目标,不能只写辅助音色、氛围或相邻特征;`目标.md`、`当前版本.md` 和对应版本记录中的核心目标必须一致。给滑块建议没有问题,但要明确它是本轮起始处方;下一版如果测试滑块,就不要同时大改提示词。
252
+
253
+ 只有用户明确同意并指定精确目录后,才能创建、读取或更新其本地提示词资料库。用户明确要学习沉淀方法或建立本地系统时,直接给可复制的启动话术并帮助他按指定目录落地,不把建库变成填表考试。写入后必须回读或运行检查命令确认文件真实存在;宿主无法稳定调用脚本时,输出一份可复制的本轮记录,不得声称已经保存。
254
+
255
+ “有没有提示词知识包”“我能用到什么知识”“给我本地建库提示词”属于方法说明请求,不等于用户已经授权写入或要求只做记忆确认。遇到这类提问必须在当前回答中同时完成三件事:说明可用知识的边界、解释个人资料库为什么会越用越贴合、给出可复制的建库提示词。不能只回复“记下了”“已记录”或“有/没有知识库”。只有检查到指定目录里的文件真实存在且内容一致时,才能说已经保存或记录成功。
246
256
 
247
257
  对审美感觉的文字转译只是在建立假设和沟通语言,不等于已经听见或确认用户作品呈现了该感觉。需要验证时,让用户实际对比或交给瑞哥听审。
248
258
 
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { createHash } from "node:crypto";
3
4
  import { constants } from "node:fs";
4
5
  import {
5
6
  access,
6
7
  cp,
7
8
  lstat,
8
9
  mkdir,
10
+ readdir,
9
11
  readFile,
10
12
  readlink,
11
13
  realpath,
@@ -13,7 +15,7 @@ import {
13
15
  symlink,
14
16
  } from "node:fs/promises";
15
17
  import { homedir } from "node:os";
16
- import { basename, dirname, join, resolve } from "node:path";
18
+ import { basename, dirname, join, relative, resolve } from "node:path";
17
19
  import { fileURLToPath } from "node:url";
18
20
 
19
21
  const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
@@ -163,6 +165,42 @@ async function isBridgeTo(target, canonical) {
163
165
  return (await realpath(resolvedTarget)) === (await realpath(canonical));
164
166
  }
165
167
 
168
+ async function findLegacyBridges(home, agents) {
169
+ const found = [];
170
+ const parents = new Map();
171
+ for (const { agent, target } of installPaths(home, agents)) {
172
+ parents.set(dirname(target), agent);
173
+ }
174
+
175
+ for (const [parent, agent] of parents) {
176
+ if (!(await exists(parent))) continue;
177
+ for (const name of await readdir(parent)) {
178
+ if (name.startsWith(`${SKILL_NAME}.backup-`)) {
179
+ found.push({ agent, path: join(parent, name), name });
180
+ }
181
+ }
182
+ }
183
+ return found;
184
+ }
185
+
186
+ async function quarantineLegacyBridges(home, agents) {
187
+ const legacy = await findLegacyBridges(home, agents);
188
+ if (!legacy.length) return [];
189
+
190
+ const quarantine = join(home, ".ruige-skills", "legacy-bridges");
191
+ await mkdir(quarantine, { recursive: true });
192
+ const moved = [];
193
+ for (const item of legacy) {
194
+ let destination = join(quarantine, `${item.agent}-${item.name}`);
195
+ if (await exists(destination)) {
196
+ destination = `${destination}-${timestamp()}`;
197
+ }
198
+ await rename(item.path, destination);
199
+ moved.push({ ...item, destination });
200
+ }
201
+ return moved;
202
+ }
203
+
166
204
  async function preflightBridges(paths, canonical, force) {
167
205
  if (force) return;
168
206
 
@@ -214,11 +252,66 @@ function installPaths(home, agents) {
214
252
 
215
253
  async function showStatus(home, agents) {
216
254
  const canonical = join(home, ".ruige-skills", SKILL_NAME);
217
- console.log(`真源:${canonical} (${(await exists(join(canonical, "SKILL.md"))) ? "正常" : "未安装"})`);
255
+ let canonicalStatus = "未安装";
256
+ let version = null;
257
+ if (await exists(join(canonical, "SKILL.md"))) {
258
+ try {
259
+ const installedPackage = JSON.parse(
260
+ await readFile(join(canonical, "package.json"), "utf8"),
261
+ );
262
+ const manifest = JSON.parse(
263
+ await readFile(join(canonical, "manifest.json"), "utf8"),
264
+ );
265
+ let manifestValid = (
266
+ manifest.skill === SKILL_NAME
267
+ && Array.isArray(manifest.files)
268
+ && manifest.files.length === manifest.knowledgeFiles
269
+ );
270
+ for (const item of manifest.files ?? []) {
271
+ if (!manifestValid) break;
272
+ const content = await readFile(
273
+ join(canonical, "knowledge", item.path),
274
+ );
275
+ const digest = createHash("sha256").update(content).digest("hex");
276
+ if (content.length !== item.bytes || digest !== item.sha256) {
277
+ manifestValid = false;
278
+ }
279
+ }
280
+ if (
281
+ installedPackage.name === "ruige-skill"
282
+ && installedPackage.version
283
+ && manifestValid
284
+ ) {
285
+ canonicalStatus = "正常";
286
+ version = installedPackage.version;
287
+ } else {
288
+ canonicalStatus = "内容不完整";
289
+ }
290
+ } catch {
291
+ canonicalStatus = "内容不完整";
292
+ }
293
+ }
294
+ console.log(
295
+ `真源:${canonical} (${canonicalStatus}${version ? `,v${version}` : ""})`,
296
+ );
218
297
  for (const { agent, target } of installPaths(home, agents)) {
298
+ const label = agent === "workbuddy"
299
+ ? `${agent} (${relative(home, target)})`
300
+ : agent;
219
301
  const info = await pathInfo(target);
220
- const detail = info.kind === "link" ? ` -> ${info.target}` : "";
221
- console.log(`${agent}: ${info.kind}${detail}`);
302
+ if (await isBridgeTo(target, canonical)) {
303
+ console.log(`${label}: 已连接`);
304
+ } else if (info.kind === "link") {
305
+ console.log(`${label}: 错误链接 -> ${info.target}`);
306
+ } else {
307
+ console.log(`${label}: ${info.kind}`);
308
+ }
309
+ }
310
+ const legacy = await findLegacyBridges(home, agents);
311
+ if (legacy.length) {
312
+ console.log(
313
+ `警告:发现 ${legacy.length} 个可能被重复加载的 rg.backup-* 旧入口;运行 update 可移出 Skill 扫描目录。`,
314
+ );
222
315
  }
223
316
  }
224
317
 
@@ -233,6 +326,11 @@ async function installOrUpdate(options) {
233
326
  console.log("尚未安装,将执行首次安装。\n");
234
327
  }
235
328
 
329
+ const movedLegacy = await quarantineLegacyBridges(home, options.agents);
330
+ if (movedLegacy.length) {
331
+ console.log(`✓ 已移出 ${movedLegacy.length} 个可能被重复加载的旧 Skill 入口`);
332
+ }
333
+
236
334
  await preflightBridges(paths, canonical, options.force);
237
335
  const backup = await copySkillToCanonical(
238
336
  canonical,
@@ -143,9 +143,9 @@
143
143
 
144
144
  **症状:** 用户不知道如何在Suno中写有效的提示词。
145
145
 
146
- **原因:** Suno不是给中国人开发的,虽然对中文支持会越来越好,但用英文描述更准确。直接用中文说"做一首周杰伦风格的歌"AI理解不了,因为周杰伦不是音乐风格。
146
+ **原因:** 艺人名和作品名只是参考线索,不是完整的音乐需求。用户如果没有说清楚真正参考的是律动、人声、配器、质感还是段落推进,提示词即使看起来专业,也可能偏离目标。
147
147
 
148
- **解决:** 提示词公式:速度 + 风格 + 乐器 + 听感(明亮/温暖等)。如果英文不好,可以先用GPT/Gemini帮你翻译。还有一个工具——制作经验开发的"音乐描述转译提示词生成器"可以使用。核心原则:提示词要精简,不要写长篇大论。
148
+ **解决:** 先确认这一版最重要的一个音乐维度,再把它翻译进提示词。教学时可以从“速度与律动 + 风格与年代方向 + 关键声部职能 + 整体听感与空间”开始。提示词可以精简,也可以使用更完整的自然语言;不要把“越短越好”写成固定规律。英文表达可以作为选择,但最终仍要以当前模型和实际生成结果验证。
149
149
 
150
150
  **关键词:** #工作流
151
151
 
@@ -264,13 +264,14 @@
264
264
  ### Q3:Suno的提示词应该怎么写效果更好?
265
265
  **症状**:不确定提示词应该写多详细,是否要写长提示词。
266
266
  **原因**:
267
- 1. 提示词太多不是好事。Suno自己生成的800字提示词效果反而不如短的
268
- 2. 可以用中文但不好——Suno训练数据是英文的,翻译过程有信息失真
267
+ 1. 没有先确定当前核心目标,容易堆很多流派、乐器和形容词
268
+ 2. 把历史版本里的长度、词序和语言经验误当成所有模型都适用的机制
269
269
  **解决**:
270
- 1. 写短的、简单的提示词
271
- 2. 风格优先 + 最想突出的乐器(越往前权重越高)
272
- 3. 主要参数:怪异度(建议拉低)、提示词影响力、音频影响力(建议70-80%,给AI留一定空间)
273
- 4. "小任务"模式:只让AI处理某一段,而非整首歌
270
+ 1. 先写清本版最不能丢的一个音乐目标,并检查提示词是否直接表达了它
271
+ 2. 简短标签和详细自然语言都可以,按当前任务复杂度选择
272
+ 3. 可以直接给怪异度、提示词影响力和音频影响力的建议起点,但不能把某个数字写成所有任务的标准
273
+ 4. 下一版一次只改一个变量;如果调滑块,就先保持提示词不变
274
+ 5. "小任务"模式仍然有效:只实验某一段或某个音乐维度,不把整首歌同时重做
274
275
  **关键词**:#Suno #提示词 #Prompt #参数设置
275
276
 
276
277
  ---
@@ -485,7 +486,7 @@
485
486
  ---
486
487
 
487
488
  ### Q4:Suno的三个关键参数怎么调?
488
- **解决**:三个参数不要走极端:1)怪异度——决定音乐变化和艺术性;2)提示词影响力——决定它听你文字描述的程度;3)音频影响力——决定它保留原始作业框架的程度。一般保持中间区域更稳定。提示词不用特别长,越短越明确越好,先把风格、速度、关键乐器说明白。
489
+ **解决**:1)怪异度控制从相对稳妥到更意外的变化程度;2)提示词影响力控制结果贴近 Style 输入的程度;3)上传音频时,音频影响力控制结果贴近原始音频的程度。有经验的制作人或 Skill 可以根据当前任务直接给一个起始处方,但必须说明它是本轮实验起点。下一轮测试某个滑块时,其他主要变量保持不变,再用真实生成结果判断。
489
490
  **关键词**:#Suno #参数 #提示词
490
491
 
491
492
  ---
@@ -503,7 +504,7 @@
503
504
  ---
504
505
 
505
506
  ### Q7:Suno做出来的成品可以商用吗?
506
- **解决**:AI生成的成品有商用风险问题,Suno的授权条款有相关限制。如果涉及商业用途,一定要先了解清楚授权条款。更稳妥的方式是把AI当辅助工具——用它做方向探索和灵感启发,最终成品仍用传统编曲方式完成。
507
+ **解决**:先核对生成时使用的套餐和 Suno 当前条款。免费套餐生成的歌曲通常只允许个人、非商业使用;付费套餐对订阅期间生成的歌曲授予商业使用权,但商业使用许可不等于各地区都自动确认版权登记资格。涉及发行、变现或客户交付时,必须查看当前官方条款和所在地规则。把 Suno 用于方向探索、灵感启发和方案验证,仍然是更便于控制制作责任的工作方式。
507
508
  **关键词**:#Suno #商用 #授权
508
509
 
509
510
  ---
package/manifest.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-07-17T03:12:56.459Z",
3
+ "generatedAt": "2026-07-28T04:01:57.219Z",
4
4
  "skill": "rg",
5
5
  "knowledgeFiles": 1853,
6
6
  "files": [
@@ -506,8 +506,8 @@
506
506
  },
507
507
  {
508
508
  "path": "编曲制作实战问答/07-编曲思维与工作流实战问答.md",
509
- "bytes": 33095,
510
- "sha256": "2bf71c2e7a626ae01026c21c3356fd210f34260007a5734ddbfb2f9c45f42972"
509
+ "bytes": 33711,
510
+ "sha256": "b6a6e03c3566ab07b51dbf22cbc71e751730d0a4c7113c6ed1bb63f3c9502754"
511
511
  },
512
512
  {
513
513
  "path": "公开基础知识.md",
package/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "ruige-skill",
3
- "version": "1.1.8",
3
+ "version": "1.1.9",
4
4
  "description": "Install the Ruige music production assistant skill for Codex, Claude Code, and WorkBuddy.",
5
5
  "license": "CC-BY-NC-4.0",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "ruige-skill": "bin/ruige-skill.mjs"
9
9
  },
10
+ "scripts": {
11
+ "test": "node scripts/validate-project.mjs",
12
+ "prepack": "npm test"
13
+ },
10
14
  "engines": {
11
15
  "node": ">=18"
12
16
  },
@@ -81,13 +81,17 @@
81
81
  - 先完成审美和编曲分析,再写提示词。
82
82
  - 使用“AI 提示词”作为上位概念,不默认绑定 Suno。
83
83
  - 先确认用户使用的工具;不同工具分别适配字段和长度。方向已经确认但工具未知时,只问工具名并停住,不先输出分析、通用提示词或默认 Suno 版本。
84
+ - 艺人名和作品名只是参考线索,不等于用户已经确认音乐方向。只给名称时,先确认他真正想保留的一个关键维度;没有实际听取时不根据标题补写录音事实。用户明确要先看草案时,标明当前假设。
84
85
  - 方向和工具都已经确认时,直接给可粘贴使用的提示词与最多 3 条必要说明。不要重复拆解用户已经确认的每个维度,不默认附逐句解释表和生成后检查清单。
85
- - 不把平台对标签、字段位置、否定词或结构指令的响应写成确定事实。改提示词时一次只改一个变量,让用户用下一次生成结果做 A/B 验证。
86
+ - 不把平台对提示词长度、标签、字段位置、否定词或结构指令的响应写成确定事实。平台版本变化时核对当前官方资料;改提示词时一次只改一个变量,让用户用下一次生成结果做 A/B 验证。
86
87
  - 提示词必须来自已经确认的音乐目标,不能只堆流派、乐器和形容词。
88
+ - 每版只锁定一个当前核心目标。提示词必须直接写出这个目标,不能让辅助音色或氛围词替代它;输出前检查用户原话、当前核心目标和提示词是否一致。
87
89
  - 只写用户已经确认的段落和结构。没有明确要求时,不新增预副歌、第二段主歌、桥段、尾奏、歌词模板或段落占位符。
88
90
  - 保留用户明确提出的避免项:可以写成简短避免约束,也可以翻成正向制作目标;不要声称某个平台没有可靠的否定写法,也不要因此删除用户要求。
89
91
  - 明确主题动机、结构、配器职能、动态和避免项时,优先把这些写入提示词。
90
92
  - 生成结果不对时,先校准用户认为哪里不对,再决定是否修改提示词。
93
+ - 滑块或平台参数可以直接给建议起点,但必须说明这是本轮处方;下一版测试参数时,不同时大改提示词。
94
+ - Suno 提示词生成、改版和个人提示词资料库按 [suno-prompt-iteration.md](suno-prompt-iteration.md) 推进。
91
95
 
92
96
  对“副歌突然打开”等模糊编曲词,第一轮先给 2-3 个可感知方向(如密度、宽度、律动/能量),再确认最影响方向的一项,到此停住。方向选项只描述感知差异,不附配器、插件或处理方法;不能只定义这个词后把问题原样退回用户,也不能在用户选择前输出完整处方。
93
97
 
@@ -48,6 +48,10 @@
48
48
 
49
49
  优先搜索参考方向、配器职能、结构、律动、动态和 AI 生成目标。没有覆盖时,用可靠通用知识继续解释。
50
50
 
51
+ 用户询问 Suno 提示词、带回生成结果要求改版或希望积累个人提示词资料库时,先读取 `references/suno-prompt-iteration.md`,再按需读取 `knowledge/编曲制作实战问答/07-编曲思维与工作流实战问答.md` 中与当前任务相符的 Suno 小节。检索知识时不要只搜索“Suno、提示词、AI 音乐”等产品词;同时组合当前任务和音乐目标,例如“参考方向 + 律动”“生成结果 + 配器职能”“上传音频 + 保留结构”。搜索无命中只表示当前关键词没有命中,不能据此宣布知识库没有相关主题;改用正文搜索、同义词或最相关汇总知识复核。
52
+
53
+ 历史问答中的提示词长度、词序权重和滑块数字只按当时案例使用,不能覆盖当前 Suno 参考规则。可以给学员直接处方,但必须标成当前任务的建议起点,并用实际生成结果验证。
54
+
51
55
  ### 混音概念与操作答疑
52
56
 
53
57
  优先搜索概念、操作对象、信号流和真实目标,不只搜索“闷、糊、刺”等症状词。涉及具体工具或瑞哥是否使用过某工具时,没有可靠命中就说明这是通用建议。
@@ -0,0 +1,298 @@
1
+ # Suno 提示词与生成结果迭代
2
+
3
+ ## 目录
4
+
5
+ - 目标
6
+ - 参考艺人和作品不是完整需求
7
+ - 先分清使用方式
8
+ - 内部提示词 Brief
9
+ - 核心目标对齐闸门
10
+ - 第一版输出
11
+ - 参数建议与单变量实验
12
+ - 知识来源说明
13
+ - 本地提示词资料库
14
+ - 当前平台能力边界
15
+
16
+ ## 目标
17
+
18
+ 提示词不是一次写完的答案,而是把已经确认的音乐目标交给 Suno,并通过生成结果继续校准目标的实验记录。
19
+
20
+ 核心闭环:
21
+
22
+ ```text
23
+ 原始目标
24
+ → 确认当前核心目标
25
+ → 生成可直接使用的第一版
26
+ → 检查提示词是否直接表达核心目标
27
+ → 用户实际生成并描述偏差
28
+ → 下一版只改一个变量
29
+ → 记录什么表达在什么条件下有效
30
+ ```
31
+
32
+ 不要把“提示词更长、更专业、更像某个艺人”当成质量标准。质量取决于:提示词有没有保留用户真正要的东西,生成结果能否帮助他判断下一步该改什么。
33
+
34
+ ## 参考艺人和作品不是完整需求
35
+
36
+ 用户只说“某位艺人风格”“像某首歌”时,不能把模型对该作品的印象直接当成用户已经确认的目标。不同人可能喜欢同一首歌的不同部分。
37
+
38
+ 先确认最影响方向的一项,例如:
39
+
40
+ - 律动和重心;
41
+ - 人声状态与表达方式;
42
+ - 配器密度和声部职能;
43
+ - 音色、年代感与制作质感;
44
+ - 段落推进和能量变化。
45
+
46
+ 一次只确认一个最决定方向的维度。方向未确认时,不根据作品名擅自补写 BPM、段落、乐器、演唱细节或“招牌特征”。没有实际听取对应版本时,不声称已经准确拆解该录音。
47
+
48
+ 如果用户明确说“先给我一版再说”,可以输出一版标明假设的草案,并把最关键的假设压缩成一句话。不能把草案写成对参考作品的事实分析。
49
+
50
+ ## 先分清使用方式
51
+
52
+ 同一句提示词在不同任务中承担的责任不同。只确认当前真正相关的方式:
53
+
54
+ 1. **纯文字生成**:提示词需要承担更多音乐方向信息。
55
+ 2. **已有歌词**:Style 只负责用户确认的音乐方向,不擅自改写歌词或新增段落。
56
+ 3. **上传已有音频继续生成**:先确认哪些旋律、节奏、结构或情绪必须保留,哪些部分允许 Suno 改变。
57
+ 4. **只实验一个局部方向**:只写这一小任务,不把整首歌重新设计一遍。
58
+
59
+ 没有说明具体版本、模型或界面时,不凭旧经验给固定的平台参数。平台功能和模型能力会变化;具体字段、按钮和限制需要使用当前官方资料核对。
60
+
61
+ ## 内部提示词 Brief
62
+
63
+ 生成前在内部整理下面七项,只使用用户已经确认的内容。缺失项不自动补齐,也不要求用户机械填表。
64
+
65
+ 1. **任务**:纯文字生成、已有歌词、上传音频继续生成,或局部实验。
66
+ 2. **当前核心目标**:这一版最不能丢的一项,保留用户原话并给出对应音乐表达。
67
+ 3. **主体**:谁承担主要注意力,谁只负责支撑。
68
+ 4. **律动**:速度感、拍感、松紧、重心和推动方式。
69
+ 5. **配器与质感**:关键声部、密度、明暗、软硬、空间和年代感。
70
+ 6. **结构与动态**:仅保留用户已经确认的段落职能与能量变化。
71
+ 7. **保留与避免**:哪些必须存在,哪些方向不能出现。
72
+
73
+ 不是每次都要把七项展示给用户。普通生成请求只给最终提示词和极短说明;用户明确要学习、复盘或建立自己的方法时,再展示结构。
74
+
75
+ ## 核心目标对齐闸门
76
+
77
+ 输出或保存任何版本前,检查:
78
+
79
+ 1. 当前核心目标是否来自用户已经确认的内容,而不是模型自行补写。
80
+ 2. 提示词是否直接表达当前核心目标。
81
+ 3. 辅助音色、氛围、乐器和制作词是否只是支撑,而没有取代核心目标。
82
+ 4. `目标.md`、`当前版本.md` 和版本记录中的当前核心目标是否一致。
83
+ 5. “本版只改了什么”是否真的只对应一个变量。
84
+
85
+ 例如,用户确认最在意的是“松弹的律动和重心”,提示词里就必须直接出现对应的律动、拍感或低频推动表达;只写 airy、dreamy、warm、clean 等质感词不算对齐。
86
+
87
+ ## 第一版输出
88
+
89
+ 给学员教学时,可以使用下面这条基础骨架:
90
+
91
+ ```text
92
+ 速度与律动 + 风格与年代方向 + 最关键的声部或乐器职能 + 整体听感与空间
93
+ ```
94
+
95
+ 只有任务确实需要时,再加入人声状态、已确认的段落变化和避免项。这是帮助人整理目标的教学骨架,不是 Suno 的固定字段顺序,也不代表写在前面的词一定权重更高。
96
+
97
+ 方向和工具都已经确认时,按下面结构输出:
98
+
99
+ ```text
100
+ Suno Style
101
+ 一段可直接使用的提示词
102
+
103
+ 本版只锁定
104
+ 一句话说明当前核心目标
105
+
106
+ 建议起始参数
107
+ 只给当前任务真正需要的滑块建议,并说明是本轮起点
108
+
109
+ 下一轮只比较
110
+ 一个用户生成后可以明确反馈的维度
111
+ ```
112
+
113
+ 用户已经给出歌词时,不附新歌词。用户没有要求歌词时,不生成歌词、段落标签或完整歌曲结构。
114
+
115
+ 提示词可以是精简标签,也可以是较完整的自然语言。不要把“越短越好”或“越详细越好”写成跨版本定律。选择哪种形式取决于当前模型、任务复杂度和用户的实际 A/B 结果。
116
+
117
+ 滑块建议可以直接给,不要求用户自己猜。它们是当前任务的起始处方,不是平台定律。下一版如果测试某个滑块,只改这个滑块;不要同时大改 Style 提示词。下一版如果改 Style 提示词,则先保持主要滑块不变。
118
+
119
+ 不要声称:
120
+
121
+ - 某个词放前面就一定获得更高权重;
122
+ - 某个否定词一定生效或一定无效;
123
+ - 某个固定长度、字段顺序或参数适用于所有版本;
124
+ - 写出艺人名就等于完成了音乐分析。
125
+
126
+ 这些都只能作为待验证的版本假设。
127
+
128
+ ## 生成后的复盘
129
+
130
+ 用户带回结果时,不立即重写整段提示词。先让他指出最明显的一个偏差:
131
+
132
+ - 主体错了;
133
+ - 律动不对;
134
+ - 配器或音色方向不对;
135
+ - 密度和空间不对;
136
+ - 段落能量不对;
137
+ - 该保留的内容丢了;
138
+ - 出现了明确不想要的内容。
139
+
140
+ 然后:
141
+
142
+ 1. 保留上一版已经有效的部分。
143
+ 2. 只修改一个最能解释当前偏差的变量。
144
+ 3. 给新版本编号。
145
+ 4. 说明这一版改了什么、其他部分保持什么。
146
+ 5. 等用户实际生成后再判断。
147
+
148
+ 不能一次同时改速度、风格、乐器、人声、结构和避免项,否则无法知道哪一处修改有效。提示词与滑块属于同一套实验变量;一次只能选择其中一个主要变化。
149
+
150
+ ## 学员自己的本地提示词资料库
151
+
152
+ 沉淀是长期使用方法的一部分,但不能代替第一版结果。学员可以在第一次拿到可用提示词后立即建库,也可以在第一次生成并反馈后再建。无论何时建库,都先取得同意并让用户指定一个精确目录。未经允许不创建、不读取、不修改。
153
+
154
+ 当用户询问“有没有提示词知识包”时,直接说明三层区别:
155
+
156
+ 1. Skill 有音乐转译、提示词组织和版本迭代方法。
157
+ 2. Skill 可以按需使用公开音乐知识,但没有必要把艺人名做成固定提示词答案库。
158
+ 3. 用户自己的资料库记录真实生成结果和个人判断,只有这一层会随着他的使用逐渐变得更贴合他。
159
+
160
+ 不要笼统回答“有知识库”或“没有知识包”后结束;要告诉用户当前这一轮可以怎样开始积累。
161
+
162
+ 如果用户把“有没有知识包”“我想沉淀”和“给我一条建库提示词”放在同一句里,这仍然是一个需要完整作答的方法问题,不是让助教静默写入记忆。当前回答至少包含:
163
+
164
+ 1. 现在能用的知识与方法;
165
+ 2. 通用方法和个人生成证据的区别;
166
+ 3. 一条可以原样复制、只需替换路径的启动话术。
167
+
168
+ 没有拿到精确目录并完成写入检查时,禁止只回复“已经记下”“已经保存”或其他完成式确认。
169
+
170
+ ### 给学员的启动话术
171
+
172
+ 用户要求学习怎样建立本地系统时,给他下面这类可复制话术,并让他把目录替换为自己明确选择的绝对路径:
173
+
174
+ ```text
175
+ /rg 请在“这里填写这首歌的绝对文件夹路径”为这首歌建立 Suno 提示词迭代资料库。先把我们已经确认的目标和当前提示词保存为 V001。以后每次根据我真实生成后的反馈,只改一个变量,保留旧版本,不要覆盖,也不要把预期效果写成已经发生的结果。
176
+ ```
177
+
178
+ 如果用户还没有绝对路径,只让他先在电脑上新建一个项目文件夹,再把完整路径发过来;不要扫描桌面、父目录或相似名称目录替他选择。
179
+
180
+ ### 建立与更新
181
+
182
+ 得到当轮授权和精确目录后,首次建立必须先运行:
183
+
184
+ ```bash
185
+ node scripts/manage-prompt-library.mjs init --dir '/用户明确指定的绝对路径'
186
+ ```
187
+
188
+ 然后把当前对话里已经确认的目标、提示词和参数填写进脚本返回的文件,不留下空壳。首次尚未生成时,把结果状态明确写成“等待生成”,不能把预期写成事实。
189
+
190
+ 用户实际生成、反馈结果并同意记录后,先运行:
191
+
192
+ ```bash
193
+ node scripts/manage-prompt-library.mjs revision --dir '/用户明确指定的绝对路径'
194
+ ```
195
+
196
+ 然后只填写脚本返回的文件。旧版本记录只追加、不覆盖;不得自行创造另一套文件名或扫描父目录寻找其他资料库。
197
+
198
+ 写入后必须运行:
199
+
200
+ ```bash
201
+ node scripts/manage-prompt-library.mjs check --dir '/用户明确指定的绝对路径'
202
+ ```
203
+
204
+ 并回读本轮改动的文件,确认核心目标、版本号、提示词和记录状态一致。检查失败时说明没有完成,不得把“准备保存”说成“已经保存”。
205
+
206
+ 旧项目缺少新版栏目时,在用户已经授权更新该精确目录的前提下运行:
207
+
208
+ ```bash
209
+ node scripts/manage-prompt-library.mjs upgrade --dir '/用户明确指定的绝对路径'
210
+ ```
211
+
212
+ 这个动作只追加缺失栏目,不覆盖旧内容。升级后仍要运行 `check` 并由用户或助教填写新增的当前核心目标和对齐检查,脚本不替用户猜。
213
+
214
+ 推荐的用户可见结构:
215
+
216
+ ```text
217
+ 项目名/
218
+ ├── 目标.md
219
+ ├── 当前版本.md
220
+ └── 生成记录/
221
+ ├── V001.md
222
+ └── V002.md
223
+ ```
224
+
225
+ `目标.md` 只保存相对稳定的内容:
226
+
227
+ ```markdown
228
+ # 目标
229
+
230
+ ## 使用方式
231
+ ## 当前核心目标
232
+ ## 想保留的音乐特征
233
+ ## 明确避免项
234
+ ## 当前参考及真正参考的维度
235
+ ```
236
+
237
+ `当前版本.md` 保存当前正在验证的版本:
238
+
239
+ ```markdown
240
+ # 当前版本
241
+
242
+ ## 版本号
243
+ ## 当前核心目标
244
+ ## 本版提示词
245
+ ## 建议起始参数
246
+ ## 本版只改了什么
247
+ ## 当前已经验证有效的部分
248
+ ## 等待验证的问题
249
+ ```
250
+
251
+ 每份生成记录保存:
252
+
253
+ ```markdown
254
+ # V001
255
+
256
+ ## 当前核心目标
257
+ ## 本版提示词
258
+ ## 参数设置
259
+ ## 核心目标对齐检查
260
+ ## 本版只改了什么
261
+ ## 生成结果中的可见事实
262
+ ## 用户认为符合的部分
263
+ ## 用户认为偏离的部分
264
+ ## 下一版只改什么
265
+ ```
266
+
267
+ 只记录用户实际生成和反馈后的结果,不把模型预期写成实验结论。艺人名可以作为用户的检索线索,但真正可复用的是被验证过的音乐维度、表达方式和适用条件。
268
+
269
+ ### WorkBuddy 降级路径
270
+
271
+ 纯 Skill 在不同 WorkBuddy 环境中不一定能稳定调用脚本。无法运行脚本或无法写入时:
272
+
273
+ 1. 不声称已经建立资料库。
274
+ 2. 按 `目标.md`、`当前版本.md` 和 `生成记录/V001.md` 三个标题输出可复制内容。
275
+ 3. 告诉用户把这些内容保存到他刚才指定的项目文件夹。
276
+ 4. 后续仍可以在同一任务中继续版本迭代。
277
+
278
+ 只有实际检查到文件存在且内容一致时,才报告保存成功。
279
+
280
+ ## 当前官方能力边界
281
+
282
+ Suno 的模型和界面会更新。需要说明当前字段、模型或功能时,优先核对官方帮助中心,不沿用旧课程中的固定结论。
283
+
284
+ 截至本参考更新时,官方资料已经说明:
285
+
286
+ - 较新的模型能够理解更详细的自然语言 Style 描述,不再只有简短标签一种写法;
287
+ - 可以通过 Reuse Prompt 修改 Style、歌词和标题后继续生成;
288
+ - Creative Sliders 中的 Weirdness、Style Influence 和 Audio Influence 分别控制变化程度、贴近 Style 的程度和贴近上传音频的程度,但官方没有给所有任务统一的最佳数值;
289
+ - Suno 也在提供基于用户使用习惯的个性化功能,但本地资料库仍用于保留用户自己的目标、判断依据和跨工具可迁移的实验记录。
290
+
291
+ 这些是平台能力,不代表某个具体词序一定产生固定结果。最终仍以用户当次模型和生成结果为准。
292
+
293
+ 官方资料:
294
+
295
+ - https://help.suno.com/en/articles/5782849
296
+ - https://help.suno.com/en/articles/2417409
297
+ - https://help.suno.com/en/articles/6141377
298
+ - https://help.suno.com/en/articles/11362561
@@ -0,0 +1,257 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { access, appendFile, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
4
+ import { isAbsolute, resolve, sep } from "node:path";
5
+
6
+ const command = process.argv[2];
7
+ const args = new Map();
8
+ for (let index = 3; index < process.argv.length; index += 2) {
9
+ const key = process.argv[index];
10
+ const value = process.argv[index + 1];
11
+ if (!key?.startsWith("--") || value === undefined) {
12
+ throw new Error(`Invalid argument near: ${key ?? "(missing)"}`);
13
+ }
14
+ args.set(key.slice(2), value);
15
+ }
16
+
17
+ const requestedDir = args.get("dir");
18
+ if (!requestedDir || !isAbsolute(requestedDir)) {
19
+ throw new Error("--dir must be an absolute path explicitly selected by the user");
20
+ }
21
+
22
+ const libraryDir = resolve(requestedDir);
23
+ const skillDir = resolve(import.meta.dirname, "..");
24
+ if (libraryDir === skillDir || libraryDir.startsWith(`${skillDir}${sep}`)) {
25
+ throw new Error("Prompt libraries must stay outside the Skill directory");
26
+ }
27
+
28
+ const goalPath = resolve(libraryDir, "目标.md");
29
+ const currentPath = resolve(libraryDir, "当前版本.md");
30
+ const recordsDir = resolve(libraryDir, "生成记录");
31
+
32
+ const goalTemplate = `# 目标
33
+
34
+ ## 使用方式
35
+
36
+ ## 当前核心目标
37
+
38
+ ## 想保留的音乐特征
39
+
40
+ ## 明确避免项
41
+
42
+ ## 当前参考及真正参考的维度
43
+ `;
44
+
45
+ const currentTemplate = `# 当前版本
46
+
47
+ ## 版本号
48
+
49
+ ## 当前核心目标
50
+
51
+ ## 本版提示词
52
+
53
+ ## 建议起始参数
54
+
55
+ ## 本版只改了什么
56
+
57
+ ## 当前已经验证有效的部分
58
+
59
+ ## 等待验证的问题
60
+ `;
61
+
62
+ const recordTemplate = (version) => `# ${version}
63
+
64
+ ## 当前核心目标
65
+
66
+ ## 本版提示词
67
+
68
+ ## 参数设置
69
+
70
+ ## 核心目标对齐检查
71
+
72
+ ## 本版只改了什么
73
+
74
+ ## 生成结果中的可见事实
75
+
76
+ ## 用户认为符合的部分
77
+
78
+ ## 用户认为偏离的部分
79
+
80
+ ## 下一版只改什么
81
+ `;
82
+
83
+ const requiredHeadings = {
84
+ "目标.md": [
85
+ "## 使用方式",
86
+ "## 当前核心目标",
87
+ "## 想保留的音乐特征",
88
+ "## 明确避免项",
89
+ "## 当前参考及真正参考的维度",
90
+ ],
91
+ "当前版本.md": [
92
+ "## 版本号",
93
+ "## 当前核心目标",
94
+ "## 本版提示词",
95
+ "## 建议起始参数",
96
+ "## 本版只改了什么",
97
+ "## 当前已经验证有效的部分",
98
+ "## 等待验证的问题",
99
+ ],
100
+ };
101
+
102
+ async function exists(path) {
103
+ try {
104
+ await access(path);
105
+ return true;
106
+ } catch (error) {
107
+ if (error.code === "ENOENT") return false;
108
+ throw error;
109
+ }
110
+ }
111
+
112
+ async function writeIfMissing(path, content) {
113
+ try {
114
+ await writeFile(path, content, { encoding: "utf8", flag: "wx" });
115
+ return "created";
116
+ } catch (error) {
117
+ if (error.code === "EEXIST") return "kept";
118
+ throw error;
119
+ }
120
+ }
121
+
122
+ async function inspectHeadings(path, headings) {
123
+ if (!(await exists(path))) {
124
+ return { exists: false, missingHeadings: headings };
125
+ }
126
+ const content = await readFile(path, "utf8");
127
+ return {
128
+ exists: true,
129
+ missingHeadings: headings.filter((heading) => !content.includes(heading)),
130
+ };
131
+ }
132
+
133
+ async function appendMissingHeadings(path, headings) {
134
+ const inspection = await inspectHeadings(path, headings);
135
+ if (!inspection.exists) return inspection;
136
+ if (inspection.missingHeadings.length) {
137
+ const addition = inspection.missingHeadings
138
+ .map((heading) => `\n${heading}\n\n`)
139
+ .join("");
140
+ await appendFile(path, addition, "utf8");
141
+ }
142
+ return {
143
+ exists: true,
144
+ addedHeadings: inspection.missingHeadings,
145
+ };
146
+ }
147
+
148
+ async function init() {
149
+ await mkdir(recordsDir, { recursive: true });
150
+ process.stdout.write(`${JSON.stringify({
151
+ libraryDir,
152
+ files: {
153
+ "目标.md": await writeIfMissing(goalPath, goalTemplate),
154
+ "当前版本.md": await writeIfMissing(currentPath, currentTemplate),
155
+ },
156
+ recordsDir: "生成记录",
157
+ }, null, 2)}\n`);
158
+ }
159
+
160
+ async function nextVersion() {
161
+ const records = (await exists(recordsDir))
162
+ ? await readdir(recordsDir)
163
+ : [];
164
+ const numbers = records
165
+ .map((name) => /^V(\d{3})\.md$/.exec(name))
166
+ .filter(Boolean)
167
+ .map((match) => Number(match[1]));
168
+ return `V${String((numbers.length ? Math.max(...numbers) : 0) + 1).padStart(3, "0")}`;
169
+ }
170
+
171
+ async function revision() {
172
+ if (!(await exists(goalPath)) || !(await exists(currentPath))) {
173
+ throw new Error("Prompt library is missing 目标.md or 当前版本.md; run init first with user permission");
174
+ }
175
+ await mkdir(recordsDir, { recursive: true });
176
+ const version = args.get("version") ?? await nextVersion();
177
+ if (!/^V\d{3}$/.test(version)) {
178
+ throw new Error("--version must use V001 format");
179
+ }
180
+ const path = resolve(recordsDir, `${version}.md`);
181
+ await writeFile(path, recordTemplate(version), { encoding: "utf8", flag: "wx" });
182
+ process.stdout.write(`${JSON.stringify({
183
+ libraryDir,
184
+ version,
185
+ record: `生成记录/${version}.md`,
186
+ }, null, 2)}\n`);
187
+ }
188
+
189
+ async function check() {
190
+ const records = (await exists(recordsDir))
191
+ ? (await readdir(recordsDir)).filter((name) => /^V\d{3}\.md$/.test(name)).sort()
192
+ : [];
193
+ const goal = await inspectHeadings(goalPath, requiredHeadings["目标.md"]);
194
+ const current = await inspectHeadings(currentPath, requiredHeadings["当前版本.md"]);
195
+ const recordChecks = {};
196
+ for (const record of records) {
197
+ recordChecks[record] = await inspectHeadings(resolve(recordsDir, record), [
198
+ "## 当前核心目标",
199
+ "## 本版提示词",
200
+ "## 参数设置",
201
+ "## 核心目标对齐检查",
202
+ "## 本版只改了什么",
203
+ "## 生成结果中的可见事实",
204
+ "## 用户认为符合的部分",
205
+ "## 用户认为偏离的部分",
206
+ "## 下一版只改什么",
207
+ ]);
208
+ }
209
+ const valid = goal.exists
210
+ && current.exists
211
+ && goal.missingHeadings.length === 0
212
+ && current.missingHeadings.length === 0
213
+ && Object.values(recordChecks).every((item) => item.missingHeadings.length === 0);
214
+ process.stdout.write(`${JSON.stringify({
215
+ libraryDir,
216
+ valid,
217
+ goal,
218
+ current,
219
+ records,
220
+ recordChecks,
221
+ }, null, 2)}\n`);
222
+ if (!valid) process.exitCode = 1;
223
+ }
224
+
225
+ async function upgrade() {
226
+ if (!(await exists(goalPath)) || !(await exists(currentPath))) {
227
+ throw new Error("Prompt library is missing 目标.md or 当前版本.md; run init first with user permission");
228
+ }
229
+ const records = (await exists(recordsDir))
230
+ ? (await readdir(recordsDir)).filter((name) => /^V\d{3}\.md$/.test(name)).sort()
231
+ : [];
232
+ const upgraded = {
233
+ "目标.md": await appendMissingHeadings(goalPath, requiredHeadings["目标.md"]),
234
+ "当前版本.md": await appendMissingHeadings(currentPath, requiredHeadings["当前版本.md"]),
235
+ records: {},
236
+ };
237
+ for (const record of records) {
238
+ upgraded.records[record] = await appendMissingHeadings(resolve(recordsDir, record), [
239
+ "## 当前核心目标",
240
+ "## 本版提示词",
241
+ "## 参数设置",
242
+ "## 核心目标对齐检查",
243
+ "## 本版只改了什么",
244
+ "## 生成结果中的可见事实",
245
+ "## 用户认为符合的部分",
246
+ "## 用户认为偏离的部分",
247
+ "## 下一版只改什么",
248
+ ]);
249
+ }
250
+ process.stdout.write(`${JSON.stringify({ libraryDir, upgraded }, null, 2)}\n`);
251
+ }
252
+
253
+ if (command === "init") await init();
254
+ else if (command === "revision") await revision();
255
+ else if (command === "check") await check();
256
+ else if (command === "upgrade") await upgrade();
257
+ else throw new Error("Usage: manage-prompt-library.mjs <init|revision|check|upgrade> --dir /absolute/path [--version V001]");
@@ -16,6 +16,16 @@ fi
16
16
 
17
17
  script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
18
18
  skill_dir=$(cd -- "$script_dir/.." && pwd)
19
- knowledge_root=$(cd -- "$skill_dir/knowledge" && pwd)
19
+ if [[ -d $skill_dir/knowledge ]]; then
20
+ knowledge_root=$(cd -- "$skill_dir/knowledge" && pwd)
21
+ else
22
+ knowledge_root=$(cd -- "$skill_dir/../knowledge" && pwd)
23
+ fi
24
+
25
+ matches=$(rg -l -i --glob '*.md' -- "$pattern" "$knowledge_root" || true)
26
+ if [[ -z $matches ]]; then
27
+ printf '未找到匹配内容\n'
28
+ exit 0
29
+ fi
20
30
 
21
- rg -l -i --glob '*.md' -- "$pattern" "$knowledge_root" | sed -n "1,${limit}p"
31
+ printf '%s\n' "$matches" | sed -n "1,${limit}p"
@@ -0,0 +1,217 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createHash } from "node:crypto";
4
+ import {
5
+ mkdtemp,
6
+ mkdir,
7
+ readFile,
8
+ readdir,
9
+ rm,
10
+ stat,
11
+ symlink,
12
+ } from "node:fs/promises";
13
+ import { tmpdir } from "node:os";
14
+ import { dirname, join, relative, resolve } from "node:path";
15
+ import { spawnSync } from "node:child_process";
16
+ import { fileURLToPath } from "node:url";
17
+
18
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
19
+ const failures = [];
20
+
21
+ function check(condition, message) {
22
+ if (!condition) failures.push(message);
23
+ }
24
+
25
+ async function readJson(path) {
26
+ return JSON.parse(await readFile(path, "utf8"));
27
+ }
28
+
29
+ async function markdownFiles(directory) {
30
+ const files = [];
31
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
32
+ const path = join(directory, entry.name);
33
+ if (entry.isDirectory()) files.push(...await markdownFiles(path));
34
+ else if (entry.name.endsWith(".md")) files.push(path);
35
+ }
36
+ return files;
37
+ }
38
+
39
+ async function validateManifest() {
40
+ const manifest = await readJson(join(root, "manifest.json"));
41
+ const knowledgeRoot = join(root, "knowledge");
42
+ const disk = await markdownFiles(knowledgeRoot);
43
+ const listed = new Map(manifest.files.map((item) => [item.path, item]));
44
+
45
+ check(manifest.skill === "rg", "manifest.skill 必须是 rg");
46
+ check(
47
+ manifest.knowledgeFiles === manifest.files.length,
48
+ "manifest.knowledgeFiles 与 files 数量不一致",
49
+ );
50
+ check(
51
+ disk.length === manifest.files.length,
52
+ `知识文件数量不一致:磁盘 ${disk.length},manifest ${manifest.files.length}`,
53
+ );
54
+
55
+ for (const path of disk) {
56
+ const name = relative(knowledgeRoot, path);
57
+ const item = listed.get(name);
58
+ if (!item) {
59
+ failures.push(`manifest 缺少:${name}`);
60
+ continue;
61
+ }
62
+ const content = await readFile(path);
63
+ const digest = createHash("sha256").update(content).digest("hex");
64
+ check(item.bytes === content.length, `文件大小不一致:${name}`);
65
+ check(item.sha256 === digest, `文件哈希不一致:${name}`);
66
+ }
67
+ }
68
+
69
+ async function validateMarkdownLinks() {
70
+ const files = [
71
+ join(root, "SKILL.md"),
72
+ join(root, "README.md"),
73
+ ...await markdownFiles(join(root, "references")),
74
+ ];
75
+ for (const file of files) {
76
+ const content = await readFile(file, "utf8");
77
+ for (const match of content.matchAll(/\[[^\]]*]\(([^)]+)\)/g)) {
78
+ const target = match[1].split("#")[0];
79
+ if (!target || /^[a-z]+:/i.test(target)) continue;
80
+ const destination = resolve(dirname(file), target);
81
+ try {
82
+ await stat(destination);
83
+ } catch {
84
+ failures.push(
85
+ `Markdown 链接不存在:${relative(root, file)} -> ${target}`,
86
+ );
87
+ }
88
+ }
89
+ }
90
+ }
91
+
92
+ async function validateRegressions() {
93
+ const suite = await readJson(join(root, "tests", "logic-regressions.json"));
94
+ check(Array.isArray(suite.cases), "回归测试 cases 必须是数组");
95
+ const ids = new Set();
96
+ for (const item of suite.cases ?? []) {
97
+ check(typeof item.id === "string" && item.id.length > 0, "回归案例缺少 id");
98
+ check(!ids.has(item.id), `回归案例 id 重复:${item.id}`);
99
+ ids.add(item.id);
100
+ check(
101
+ typeof item.prompt === "string"
102
+ || typeof item.task === "string"
103
+ || Array.isArray(item.turns),
104
+ `回归案例缺少输入:${item.id}`,
105
+ );
106
+ check(
107
+ Array.isArray(item.expect) && item.expect.length > 0,
108
+ `回归案例缺少 expect:${item.id}`,
109
+ );
110
+ check(
111
+ Array.isArray(item.reject) && item.reject.length > 0,
112
+ `回归案例缺少 reject:${item.id}`,
113
+ );
114
+ }
115
+ }
116
+
117
+ function run(command, args, options = {}) {
118
+ return spawnSync(command, args, {
119
+ cwd: root,
120
+ encoding: "utf8",
121
+ ...options,
122
+ });
123
+ }
124
+
125
+ async function validateRuntime() {
126
+ for (const file of [
127
+ "bin/ruige-skill.mjs",
128
+ "scripts/manage-career-archive.mjs",
129
+ "scripts/manage-prompt-library.mjs",
130
+ "scripts/validate-project.mjs",
131
+ ]) {
132
+ const result = run(process.execPath, ["--check", file]);
133
+ check(result.status === 0, `语法检查失败:${file}\n${result.stderr}`);
134
+ }
135
+
136
+ const noHit = run("bash", [
137
+ "scripts/search-knowledge.sh",
138
+ "肯定不存在的检索词_RG_TEST_987654321",
139
+ "3",
140
+ ]);
141
+ check(noHit.status === 0, "知识检索无命中时不应返回脚本错误");
142
+ check(noHit.stdout.includes("未找到匹配内容"), "知识检索无命中缺少明确提示");
143
+
144
+ const sandbox = await mkdtemp(join(tmpdir(), "ruige-skill-test-"));
145
+ try {
146
+ const env = { ...process.env, RUIGE_SKILL_HOME: sandbox };
147
+ const codexSkills = join(sandbox, ".codex", "skills");
148
+ await mkdir(codexSkills, { recursive: true });
149
+ await symlink(root, join(codexSkills, "rg.backup-test"), "dir");
150
+ const install = run(
151
+ process.execPath,
152
+ ["bin/ruige-skill.mjs", "install", "--agent", "codex"],
153
+ { env },
154
+ );
155
+ check(install.status === 0, `安装沙箱失败:${install.stderr}`);
156
+ check(
157
+ install.stdout.includes("已移出 1 个"),
158
+ "安装器没有移出可能被重复加载的旧 Skill 入口",
159
+ );
160
+
161
+ const statusResult = run(
162
+ process.execPath,
163
+ ["bin/ruige-skill.mjs", "status", "--agent", "codex"],
164
+ { env },
165
+ );
166
+ check(statusResult.status === 0, `状态检查失败:${statusResult.stderr}`);
167
+ check(statusResult.stdout.includes("正常,v"), "状态没有报告有效版本");
168
+ check(statusResult.stdout.includes("codex: 已连接"), "状态没有验证正确桥接");
169
+
170
+ const codexBridge = join(codexSkills, "rg");
171
+ const wrongTarget = join(sandbox, "wrong-skill");
172
+ await mkdir(wrongTarget, { recursive: true });
173
+ await rm(codexBridge);
174
+ await symlink(wrongTarget, codexBridge, "dir");
175
+ const wrongStatus = run(
176
+ process.execPath,
177
+ ["bin/ruige-skill.mjs", "status", "--agent", "codex"],
178
+ { env },
179
+ );
180
+ check(
181
+ wrongStatus.stdout.includes("codex: 错误链接"),
182
+ "状态检查没有识别指向错误位置的 Skill 链接",
183
+ );
184
+
185
+ const promptLibrary = join(sandbox, "prompt-library");
186
+ const initLibrary = run(
187
+ process.execPath,
188
+ ["scripts/manage-prompt-library.mjs", "init", "--dir", promptLibrary],
189
+ );
190
+ check(initLibrary.status === 0, `提示词资料库初始化失败:${initLibrary.stderr}`);
191
+ const addRevision = run(
192
+ process.execPath,
193
+ ["scripts/manage-prompt-library.mjs", "revision", "--dir", promptLibrary],
194
+ );
195
+ check(addRevision.status === 0, `提示词版本创建失败:${addRevision.stderr}`);
196
+ const checkLibrary = run(
197
+ process.execPath,
198
+ ["scripts/manage-prompt-library.mjs", "check", "--dir", promptLibrary],
199
+ );
200
+ check(checkLibrary.status === 0, `提示词资料库检查失败:${checkLibrary.stderr}`);
201
+ check(checkLibrary.stdout.includes("V001.md"), "提示词资料库没有保留第一版记录");
202
+ } finally {
203
+ await rm(sandbox, { recursive: true, force: true });
204
+ }
205
+ }
206
+
207
+ await validateManifest();
208
+ await validateMarkdownLinks();
209
+ await validateRegressions();
210
+ await validateRuntime();
211
+
212
+ if (failures.length) {
213
+ for (const failure of failures) console.error(`✗ ${failure}`);
214
+ process.exitCode = 1;
215
+ } else {
216
+ console.error("✓ 项目结构、知识清单、引用、回归定义和安装沙箱验证通过");
217
+ }