feat: ClassIsland / ClassHelper 灵动岛提示词套件

- 重构版主提示词(14 parts,面向 ClassHelper 灵动岛重构)
- ClassIsland 全量规格提示词(20 parts,含分卷与逐章版本)
- 投喂指南、分卷、逐节文件
- 调研记录、一致性基线、生成与质检脚本
This commit is contained in:
ClassIsland Prompt Kit committed 2026-11-12 21:59:58 +08:00
commit 22c1784bab
117 files changed
+47759

No files matched your search

+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env node
/** 汇总 parts-r/00..13 为《ClassHelper灵动岛重构-给ChatGPT的完整提示词.md》 */
import { readdirSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
const partsDir = join(root, 'parts-r');
const outFile = join(root, 'ClassHelper灵动岛重构-给ChatGPT的完整提示词.md');
const files = readdirSync(partsDir)
.filter((f) => /^\d{2}-.+\.md$/.test(f))
.sort((a, b) => a.localeCompare(b, 'en'));
if (files.length !== 14) console.warn(`警告:parts-r 文件数 ${files.length},预期 14`);
if (!files.some((f) => f.startsWith('00-'))) throw new Error('缺少 parts-r/00-preamble.md');
const sections = [];
const toc = [];
const stats = [];
let preamble = '';
for (const file of files) {
const content = readFileSync(join(partsDir, file), 'utf8').replace(/\r\n/g, '\n').trimEnd();
if (file.startsWith('00-')) {
preamble = content;
for (const line of content.split('\n')) if (/^## /.test(line)) toc.push(`- ${line.replace(/^##\s+/, '').trim()}`);
continue;
}
for (const line of content.split('\n')) {
if (/^## /.test(line)) toc.push(`- ${line.replace(/^##\s+/, '').trim()}`);
else if (/^### /.test(line)) toc.push(` - ${line.replace(/^###\s+/, '').trim()}`);
}
const chars = [...content.replace(/\s/g, '')].length;
const cjk = (content.match(/[\u4e00-\u9fff]/g) || []).length;
stats.push({ file, chars, cjk, lines: content.split('\n').length });
sections.push(content);
}
if (!preamble.includes('<!-- TOC -->')) throw new Error('preamble 缺少 TOC 占位符');
const tocBlock = ['## 目录', '', ...toc, ''].join('\n');
const body = [preamble.replace('<!-- TOC -->', tocBlock), ...sections].join('\n\n---\n\n');
writeFileSync(outFile, body + '\n', 'utf8');
const totalChars = [...body.replace(/\s/g, '')].length;
const totalCjk = (body.match(/[\u4e00-\u9fff]/g) || []).length;
console.log(`输出:${outFile}`);
console.log(`去空白字符:${totalChars};汉字:${totalCjk};行数:${body.split('\n').length}`);
console.log(`粗略 token:${Math.round(totalCjk * 0.9 + (totalChars - totalCjk) * 0.3)}`);
for (const s of stats) console.log(` ${s.file.padEnd(26)} ${String(s.chars).padStart(7)} 字符 ${String(s.cjk).padStart(6)} 汉字`);
console.log(` 00-preamble.md ${String([...preamble.replace(/\s/g, '')].length).padStart(7)} 字符 ${String((preamble.match(/[\u4e00-\u9fff]/g) || []).length).padStart(6)} 汉字`);
if (process.argv.includes('--split')) {
const outDir = join(root, 'out');
const sectionsDir = join(outDir, 'refactor-sections');
mkdirSync(sectionsDir, { recursive: true });
const volumes = [
{ name: 'vol1-任务声明与现状基线(00-02)', nums: [0, 1, 2] },
{ name: 'vol2-基准语义与文件数据改造(03-05)', nums: [3, 4, 5] },
{ name: 'vol3-tokens与组件规格(06-07)', nums: [6, 7] },
{ name: 'vol4-消息课程与编辑器(08-09)', nums: [8, 9] },
{ name: 'vol5-动画兼容与验收里程碑(10-13)', nums: [10, 11, 12, 13] },
];
for (const v of volumes) {
const chunks = [];
if (v.nums.includes(0)) chunks.push(preamble.replace('<!-- TOC -->', tocBlock));
for (const n of v.nums) {
if (n === 0) continue;
const f = files.find((x) => Number(x.slice(0, 2)) === n);
if (f) chunks.push(readFileSync(join(partsDir, f), 'utf8').replace(/\r\n/g, '\n').trimEnd());
else console.warn(`警告:缺少 part ${n}`);
}
const p = join(outDir, `${v.name}.md`);
writeFileSync(p, chunks.join('\n\n---\n\n') + '\n', 'utf8');
const c = readFileSync(p, 'utf8');
console.log(`分卷:${p}(${[...c.replace(/\s/g, '')].length} 字符)`);
}
for (const f of files) copyFileSync(join(partsDir, f), join(sectionsDir, f));
console.log(`逐节文件:${sectionsDir}(${files.length} 个)`);
}
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env node
/**
* 汇总 parts/00-preamble.md + parts/NN-*.md 为最终提示词文件。
* 用法:node scripts/assemble.mjs [--split]
* 输出:ClassIsland-给ChatGPT的完整提示词.md(工作区根目录);--split 额外输出 out/ 下的分卷。
*/
import { readdirSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
const partsDir = join(root, 'parts');
const outFile = join(root, 'ClassIsland-给ChatGPT的完整提示词.md');
const files = readdirSync(partsDir)
.filter((f) => /^\d{2}-.+\.md$/.test(f))
.sort((a, b) => a.localeCompare(b, 'en'));
if (!files.some((f) => f.startsWith('00-'))) throw new Error('缺少 parts/00-preamble.md');
if (files.length !== 20) console.warn(`警告:part 数量为 ${files.length},预期 20`);
const sections = [];
const toc = [];
const stats = [];
let preamble = '';
for (const file of files) {
const content = readFileSync(join(partsDir, file), 'utf8').replace(/\r\n/g, '\n').trimEnd();
if (file.startsWith('00-')) {
preamble = content;
for (const line of content.split('\n')) {
if (/^## /.test(line)) toc.push(`- ${line.replace(/^##\s+/, '').trim()}`);
}
continue;
}
for (const line of content.split('\n')) {
if (/^## /.test(line)) toc.push(`- ${line.replace(/^##\s+/, '').trim()}`);
else if (/^### /.test(line)) toc.push(` - ${line.replace(/^###\s+/, '').trim()}`);
}
const chars = [...content.replace(/\s/g, '')].length;
const cjk = (content.match(/[\u4e00-\u9fff]/g) || []).length;
stats.push({ file, chars, cjk, lines: content.split('\n').length });
sections.push(content);
}
if (!preamble.includes('<!-- TOC -->')) throw new Error('preamble 缺少 <!-- TOC --> 占位符');
const tocBlock = ['## 目录', '', ...toc, ''].join('\n');
const body = [preamble.replace('<!-- TOC -->', tocBlock), ...sections].join('\n\n---\n\n');
writeFileSync(outFile, body + '\n', 'utf8');
const totalChars = [...body.replace(/\s/g, '')].length;
const totalCjk = (body.match(/[\u4e00-\u9fff]/g) || []).length;
const lines = body.split('\n').length;
console.log(`输出:${outFile}`);
console.log(`总字符(去空白):${totalChars};汉字:${totalCjk};行数:${lines}`);
console.log(`粗略 token 估算:${Math.round(totalCjk * 0.9 + (totalChars - totalCjk) * 0.3)}(仅参考)`);
console.log('各 part:');
for (const s of stats) console.log(` ${s.file.padEnd(28)} ${String(s.chars).padStart(7)} 字符 ${String(s.cjk).padStart(6)} 汉字`);
console.log(` 00-preamble.md ${String([...preamble.replace(/\s/g, '')].length).padStart(7)} 字符 ${String((preamble.match(/[\u4e00-\u9fff]/g) || []).length).padStart(6)} 汉字`);
if (process.argv.includes('--split')) {
const outDir = join(root, 'out');
const sectionsDir = join(outDir, 'sections');
mkdirSync(sectionsDir, { recursive: true });
const volumes = [
{ name: 'vol1-总纲与技术栈(00-02)', nums: [0, 1, 2] },
{ name: 'vol2-设计系统与主界面组件(03-05)', nums: [3, 4, 5] },
{ name: 'vol3-课表提醒自动化(06-08)', nums: [6, 7, 8] },
{ name: 'vol4-主题设置窗口系统与数据(09-13)', nums: [9, 10, 11, 12, 13] },
{ name: 'vol5-动画资源验收与结语(14-19)', nums: [14, 15, 16, 17, 18, 19] },
];
for (const v of volumes) {
const chunks = [];
if (v.nums.includes(0)) chunks.push(preamble.replace('<!-- TOC -->', tocBlock));
for (const n of v.nums) {
if (n === 0) continue;
const f = files.find((x) => Number(x.slice(0, 2)) === n);
if (f) chunks.push(readFileSync(join(partsDir, f), 'utf8').replace(/\r\n/g, '\n').trimEnd());
else console.warn(`警告:缺少 part ${n}`);
}
const p = join(outDir, `${v.name}.md`);
writeFileSync(p, chunks.join('\n\n---\n\n') + '\n', 'utf8');
const c = readFileSync(p, 'utf8');
console.log(`分卷:${p}(${[...c.replace(/\s/g, '')].length} 字符,${(c.match(/[\u4e00-\u9fff]/g) || []).length} 汉字)`);
}
// 逐节文件(适合按章节投喂)
let copied = 0;
for (const f of files) {
if (f.startsWith('00-')) continue;
copyFileSync(join(partsDir, f), join(sectionsDir, f));
copied += 1;
}
copyFileSync(join(partsDir, '00-preamble.md'), join(sectionsDir, '00-preamble.md'));
console.log(`逐节文件:${sectionsDir}(${copied + 1} 个)`);
}
+18
View File
@@ -0,0 +1,18 @@
import { readFileSync } from 'node:fs';
const t = readFileSync('ClassHelper灵动岛重构-给ChatGPT的完整提示词.md', 'utf8');
const checks = [
['fixed窗口宽 min(工作区,960)', /宽 = min\(目标显示器工作区宽, 960\)/],
['fixed窗口高 40+40=80', /40 \+ 2 × 20 = 80/],
['family只影响fixed', /family` 只影响 `fixed`/],
['custom 始终 ClassIsland', /custom` 始终使用 ClassIsland 规格|custom.*始终.*ClassIsland/],
['气泡行高恒40', /行高恒为 40|行高恒 40/],
['配置版本 v2', /islandLayout\.version/],
['无头验证', /verify :desktop|verify:desktop/],
['强提醒', /Topmost|强提醒/],
['迁移回滚', /回滚/],
];
for (const [n, re] of checks) console.log((re.test(t) ? 'OK ' : 'MISS ') + n);
const stale = ['宽 = 行内容自然宽 + 2 × 12', '8 条裁决', 'NotificationCenter', 'schemaVersion: 2 落盘'];
for (const s of stale) console.log(`stale [${s}]: ${t.split(s).length - 1}`);
console.log('sections:', (t.match(/^## \d+\./gm) || []).length, 'lines:', t.split('\n').length, 'chars:', t.length);
console.log('has 07 fixed section:', t.includes('A. `family=`') || t.includes("A. `family='classisland'`"));
+29
View File
@@ -0,0 +1,29 @@
import { readFileSync } from 'node:fs';
const t = readFileSync('ClassIsland-给ChatGPT的完整提示词.md', 'utf8');
const checks = [
['行高 40 / IslandContainerHeight', /IslandContainerHeight/],
['圆角 8', /RadiusX|半径 8|圆角 8/],
['不透明度 0.5', /0\.5.*不透明|Opacity.*0\.5|opacity 0\.5/],
['阴影 0 4 8 2', /0 4 8 2/],
['Fluent 300ms 曲线', /0\.65, 0, 0\.35/],
['OOBE 8 页', /8 页/],
['无通知中心窗口', /没有.{0,6}通知中心|无独立.{0,6}通知中心/],
['无设置搜索框', /无.{0,6}搜索框|没有.{0,6}搜索/],
['Excel P1', /Excel[^\n]{0,40}P1|P1[^\n]{0,40}Excel/],
['壁纸取色停用/P2', /壁纸取色[^\n]{0,40}(停用|P2)/],
['行动术语', /行动/],
];
for (const [n, re] of checks) {
const m = t.match(re);
console.log((m ? 'OK ' : 'MISS ') + n + (m ? ' -> ' + JSON.stringify(m[0]).slice(0, 70) : ''));
}
const stale = ['9 页', 'NotificationCenter', '通知中心与测试提醒', 'OEE', '顶部搜索 + 面包屑'];
for (const s of stale) {
const c = t.split(s).length - 1;
console.log(`stale [${s}]: ${c}`);
}
// 出现“通知中心”的上下文
let i = -1, arr = [];
while ((i = t.indexOf('通知中心', i + 1)) >= 0) arr.push(t.slice(Math.max(0, i - 25), i + 25).replace(/\n/g, ' '));
console.log('通知中心 上下文:');
for (const a of arr.slice(0, 8)) console.log(' ...' + a + '...');
+9
View File
@@ -0,0 +1,9 @@
import { readFileSync, writeFileSync } from 'node:fs';
const p = 'parts-r/07-island-components.md';
let t = readFileSync(p, 'utf8');
const before = 'classisland` fixed 的窗口高恒为 40 + 2×`ISLAND_SHADOW_PAD`';
const after = 'classisland` fixed 的窗口高恒为 40 + 2×20 = 80 DIP';
if (!t.includes(before)) throw new Error('未找到验收第 5 条');
t = t.replace(before, after);
writeFileSync(p, t, 'utf8');
console.log('patched');
+20
View File
@@ -0,0 +1,20 @@
import { readFileSync, writeFileSync } from 'node:fs';
const p = 'parts-r/07-island-components.md';
let t = readFileSync(p, 'utf8');
const oldS = `- 窗口几何(固定包围盒,开合时不动):
- 宽 = 行内容自然宽 + 2 × 12(行内边距)+ 2 × \`ISLAND_SHADOW_PAD\`(=10,\`packages/shared/src/constants.ts:351\`);
- **高恒为 40 + 2 × \`ISLAND_SHADOW_PAD\`**(无消息 / 有消息都一样;气泡在行内,不改变窗口高度);
- 只在"行宽跨档"、显示器/DPI 变化或"窗口不可见"时 \`setBounds\`(沿用 \`applyWindowLayout\` 的比较逻辑,\`main/island.ts:1598-1612\`)。
- 行宽/内容宽必须由渲染层测量后上报:\`reportSize({ width, height })\`(\`CustomIslandRuntime.vue:255\`)+ \`setHitRect\`(\`IslandApp.vue:421-454\`),主进程**不得**自行猜内容尺寸。
- 气泡宽度上限 **720**【推断:按 1080p 下 ClassIsland 岛常用宽度取整】;超宽时课程表按 \`settings.count\` 截断(7.4.5),气泡正文最多 2 行(7.2.8)。`;
const newS = `- 窗口几何(**预置固定包围盒**,开合/消息变化时都不改窗口尺寸):
- **宽 = min(目标显示器工作区宽, 960) DIP**(960 为本实现的岛内容上限;行内容在窗口内按停靠位对齐、超出截断);**不得**按"行内容自然宽"逐次扩缩窗口;
- **高 = 40 + 2 × 20 = 80 DIP**(20 = ClassIsland Fluent 安全区/阴影余量;行高 40 恒不变,气泡、正文 2 行、进度条都在 40 行内完成);
- **只在显示器/DPI 变化、停靠/偏移设置变化、窗口不可见时**重算 \`setBounds\`;行内容变化(课程名、秒数、消息出现/收起)**禁止**改窗口尺寸(\`main/island.ts:1598-1612\` 的比较逻辑仅用于上述设置变化);
- 可见行背景宽度 = 行内容自然宽(≤ 窗口宽),水平对齐按停靠位:左/中/右;行背景之外是透明余量,用户不可见。
- 行宽/内容宽仍必须由渲染层测量后上报(\`reportSize\`/\`setHitRect\`,\`CustomIslandRuntime.vue:255\`、\`IslandApp.vue:421-454\`),用于**行背景宽度、截断与命中矩形**,不用于改窗口尺寸。
- 气泡宽度上限 **720**【推断:按 1080p 下 ClassIsland 岛常用宽度取整】;超宽时课程表按 \`settings.count\` 截断(7.4.5),气泡正文最多 2 行(7.2.8)。`;
if (!t.includes(oldS)) throw new Error('未找到 7.1.2 fixed 窗口几何段');
t = t.replace(oldS, newS);
writeFileSync(p, t, 'utf8');
console.log('07 fixed geometry patched');
+18
View File
@@ -0,0 +1,18 @@
import { readFileSync, writeFileSync } from 'node:fs';
function patch(file, pairs) {
let t = readFileSync('parts-r/' + file, 'utf8');
for (const [a, b] of pairs) {
if (!t.includes(a)) { console.log('MISS ' + file + ' :: ' + a.slice(0, 50)); continue; }
t = t.split(a).join(b);
}
writeFileSync('parts-r/' + file, t, 'utf8');
console.log('patched ' + file);
}
patch('11-compat-tests.md', [
['| 课程 | 课表解析结果(`ScheduleDto`/`IslandTimePointDto`)、准备/上课/课间/下课前/课中进度提醒、下课前秒数徽标、当前/下一节/今日时间点显示 | 时间状态机单测 + 假时钟无头场景 |',
'| 课程 | 课表解析结果(`ScheduleDto`/`IslandTimePointDto`)、准备/上课/课间/下课前/课中进度提醒、下课前秒数徽标、当前/下一节/今日时间点显示;**行为变更**:custom 不再临时接管岛显示课程提醒(仅保留 ending 徽标,进度由课程组件显示),fixed 按 ClassIsland 接管 | 时间状态机单测 + 假时钟无头场景 |'],
]);
patch('12-acceptance.md', [
['- [ ] 课程状态机跨天、跨周、临时课表、预定课表、调课场景正确(与 ClassHelper 现有解析结果一致)。',
'- [ ] 课程状态机跨天、跨周、临时课表、预定课表、调课场景正确(与 ClassHelper 现有解析结果一致)。\n- [ ] 课程提醒接管:fixed(classisland)按 ClassIsland 行内容接管显示 prepare/on-class/break;custom 不再临时接管整岛(移除 `resident-lesson-page`),进度由课程组件显示,ending 只保留课程名右侧 30×30 蓝色圆形秒数徽标。'],
]);
+17
View File
@@ -0,0 +1,17 @@
import { readFileSync, writeFileSync } from 'node:fs';
function patch(file, pairs) {
const p = 'parts-r/' + file;
let t = readFileSync(p, 'utf8');
for (const [a, b] of pairs) {
if (!t.includes(a)) { console.log('MISS ' + file + ' :: ' + a.slice(0, 60)); continue; }
t = t.split(a).join(b);
}
writeFileSync(p, t, 'utf8');
console.log('patched ' + file);
}
patch('00-preamble.md', [
['1. **投喂方式**:全文约 20 万 tokens,建议按 `out/投喂指南-重构版.md` 的五卷/逐章方式投喂;一次性投喂需 ≥ 200k 上下文模型。投喂前请把 ClassHelper 仓库(或至少 `packages/desktop-client`、`packages/shared`、`packages/ui`、`docs/`)提供给 ChatGPT(作为代码上下文或附件)。',
'1. **投喂方式**:全文约 12 万 tokens,建议按 `out/投喂指南-重构版.md` 的五卷/逐章方式投喂;一次性投喂需 ≥ 128k 上下文模型。投喂前请把 ClassHelper 仓库(或至少 `packages/desktop-client`、`packages/shared`、`packages/ui`、`docs/`)提供给 ChatGPT(作为代码上下文或附件)。'],
['8. 既有硬约束必须保留:经典岛连续圆角、投影常驻、触摸输入、失焦宽限、固定窗口几何护栏、消息按 id 去重、通知/叫人双播不重复、课件传输优先、默认验证不创建可见岛或托盘、冒烟假执行不真关机。',
'8. 既有硬约束必须保留:`winisland` 兼容形态下的连续圆角与投影常驻、触摸输入、失焦宽限、固定窗口几何护栏;所有形态下消息按 id 去重、通知/叫人双播不重复、课件传输优先、默认验证不创建可见岛或托盘、冒烟假执行不真关机。'],
]);
+35
View File
@@ -0,0 +1,35 @@
import { readFileSync, writeFileSync } from 'node:fs';
function patch(file, pairs) {
let t = readFileSync('parts-r/' + file, 'utf8');
for (const [a, b] of pairs) {
if (!t.includes(a)) { console.log('MISS ' + file + ' :: ' + a.slice(0, 50)); continue; }
t = t.split(a).join(b);
}
writeFileSync('parts-r/' + file, t, 'utf8');
console.log('patched ' + file);
}
patch('01-objective.md', [
['5. **winisland 兼容风格**:`appearance.family = \'winisland\'` 时保留现有经典观感(连续圆角、胶囊 268×44、纯黑/毛玻璃/主题色渐变、弹簧形变);默认 `classisland`。设置页可一键切换并即时预览。',
'5. **winisland 兼容风格(只作用于 fixed)**:`appearance.family = \'winisland\'` 时保留现有经典 fixed 观感(连续圆角、胶囊 268×44、纯黑/毛玻璃/主题色渐变、弹簧形变);`custom` 不随 family 变化,始终使用 ClassIsland 规格(其编辑器/行/组件结构保持不变,只按 07 细化像素)。默认新装 `classisland`,老用户升级迁移为 `winisland`。'],
['1. **风格双轨、默认 ClassIsland**:`appearance.family: \'classisland\' | \'winisland\'`,默认 `classisland`;旧用户升级时**保持** `winisland`(避免观感突变),新安装默认 `classisland`;设置页提供切换并即时预览。',
'1. **固定形态风格双轨、默认 ClassIsland**:`appearance.family: \'classisland\' | \'winisland\'` 只决定 **fixed** 的形态(信息行 vs 旧胶囊);`custom` 始终 ClassIsland 规格。新装默认 `classisland`;旧用户升级保持 `winisland`(避免 fixed 观感突变);设置页在 fixed 下提供切换并即时预览。'],
]);
patch('11-compat-tests.md', [
['| 风格 | `family=\'winisland\'` 保持旧观感;`family=\'classisland\'` 为默认新观感;切换即时生效、可回退 | 截图(仅 `--visual`)+ 视觉回归 |',
'| 风格 | `family` 只影响 fixed:`winisland` 保持旧胶囊观感,`classisland` 为默认信息行;`custom` 始终 ClassIsland 规格;切换即时生效、可回退 | 截图(仅 `--visual`)+ 视觉回归 |'],
]);
patch('12-acceptance.md', [
['- [ ] 深浅色/主题切换无残留样式;`prefers-reduced-motion` 下只保留必要透明度变化。',
'- [ ] 深浅色/主题切换无残留样式;岛动画由 `appearance.animations` 开关控制(系统 `prefers-reduced-motion` 不强制关闭岛动画,遵循 ClassHelper 既有文档约定)。'],
['- [ ] `animations=false` / `prefers-reduced-motion` 生效;失焦/隐藏页暂停;恢复后不补播。',
'- [ ] `animations=false` 生效(唯一岛动画开关);`speed` 统一缩放时长;失焦/隐藏页暂停;恢复后不补播。'],
['**custom(classisland 风格)**', '**custom(始终 ClassIsland 规格,family 不影响 custom)**'],
]);
patch('13-milestones.md', [
['- `family=\'classisland\'` 与 `family=\'winisland\'` 双轨渲染;默认按用户升级策略选择;设置页可切换。',
'- `family` 双轨渲染仅作用于 fixed(classisland 信息行 / winisland 旧胶囊);custom 统一 ClassIsland 规格;默认按用户升级策略选择;设置页可切换。'],
]);
+31
View File
@@ -0,0 +1,31 @@
import { readFileSync, writeFileSync } from 'node:fs';
function patch(file, pairs) {
let t = readFileSync('parts-r/' + file, 'utf8');
for (const [a, b] of pairs) {
if (!t.includes(a)) { console.log('MISS ' + file + ' :: ' + a.slice(0, 60)); continue; }
t = t.split(a).join(b);
}
writeFileSync('parts-r/' + file, t, 'utf8');
console.log('patched ' + file);
}
patch('06-tokens.md', [
['| 固定包围盒 | 不适用(classisland fixed 行宽按内容 + 窗口按行宽自适应;仍需包围盒) |',
'| 固定包围盒(fixed) | `宽 = min(工作区宽, 960)`、`高 = 40 + 2×pad`(pad 默认 20,覆盖阴影);行内容在窗口内按停靠对齐并在超出时截断/省略;消息到达不改变窗口尺寸 |'],
]);
patch('07-island-components.md', [
['| 窗口几何 | 宽按内容、高恒 40+2×pad |',
'| 窗口几何 | 宽 = min(工作区宽, 960),高恒 40+2×pad(pad 默认 20) |'],
]);
patch('10-animations.md', [
['| t=0 | 主进程把消息压入队列并把 `mode` 切为 `expanded`;窗口在「不可见时」先扩到气泡所需包围盒 |',
'| t=0 | 主进程把消息压入队列并把 `mode` 切为 `expanded`;窗口使用预设包围盒(宽 = min(工作区宽,960)、高 = 40+2×pad),**不因气泡扩窗** |'],
]);
patch('04-file-plan.md', [
['- **风险**:行内气泡会改变行宽测量;必须由 `geometry.ts` 统一计算"内容自然宽/截断/换行",并让主进程窗口包围盒同步(见 05 的 metrics);「还有 N 条」跳转必须带未读上下文,不得丢失已读回执。',
'- **风险**:行内气泡只改变行内内容测量与截断(正文最多 2 行、超出省略),**不改变窗口包围盒**(fixed 窗口尺寸固定为 min(工作区宽,960) × (40+2×pad));「还有 N 条」跳转必须带未读上下文,不得丢失已读回执。'],
]);
patch('12-acceptance.md', [
['**fixed(classisland 风格)**',
'**fixed(classisland 风格)**\n- [ ] fixed 窗口包围盒固定:宽 = min(工作区宽, 960)、高 = 40 + 2×pad(pad 默认 20);消息到达/展开/多条列表均不改变窗口尺寸。'],
]);
+21
View File
@@ -0,0 +1,21 @@
import { readFileSync, writeFileSync } from 'node:fs';
const p = 'parts/02-tech-stack.md';
let t = readFileSync(p, 'utf8');
const oldBullets = `- \`width/height\` 使用"固定包围盒":取所有形态(胶囊/展开/紧急/叫人)中最大者 + 阴影留白 \`ISLAND_SHADOW_PAD = 10\`,**开合过程中窗口尺寸不变**,形变全部发生在渲染层画布内。依据:\`repos/ClassHelper-master/packages/shared/src/constants.ts:344-351\`、\`src/main/island.ts:72-112\`。
- 窗口标志:\`frame:false\`、\`transparent:true\`、\`resizable:false\`、\`movable:false\`、\`minimizable:false\`、\`maximizable:false\`、\`fullscreenable:false\`、\`skipTaskbar:true\`、\`hasShadow:false\`、\`focusable:true\`、\`alwaysOnTop:true\`、\`type:'toolbar'\`。依据:\`src/main/island.ts:480-504\`。
- \`webPreferences\`:\`preload\` 指向 \`dist/preload/island.js\`、\`contextIsolation:true\`、\`nodeIntegration:false\`、\`sandbox:true\`、\`backgroundThrottling:false\`。依据:\`:505-511\`。
- 创建后必须:\`setAlwaysOnTop(appearance.alwaysOnTop,'screen-saver')\`、\`setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true})\`、\`setWindowOpenHandler(()=>({action:'deny'}))\`、\`will-navigate\` 白名单校验、默认 \`setIgnoreMouseEvents(true,{forward:true})\`、**禁止** \`setBackgroundMaterial()\`。依据:\`:514-527\`。
- 定位:默认顶部居中,6 个停靠位(\`top-center/top-left/top-right/bottom-center/bottom-left/bottom-right\`),左右边距仅左/右停靠生效(\`marginX\` 默认 8、\`marginY\` 默认 8)。依据:\`constants.ts:285-321\`。`;
const newBullets = `- \`width/height\` 使用"固定包围盒 + 窗内形变",**以第 4 部分 4.3 为唯一实现口径**:\`winW = 目标屏幕宽(DIP)\`,\`winH = contentMaxHeight + 2 × safePadding\`(\`safePadding ≥ 20\`,其中包含阴影/光晕余量,参考 ClassHelper 的 \`ISLAND_SHADOW_PAD = 10\` 双侧口径);窗口在开合、行增删与提醒展开过程中尺寸不变,形变全部发生在渲染层画布内,只有显示器/DPI 变化、行数超出上限、编辑模式进出、偏移/缩放/停靠设置变化时才 \`setBounds\` 且节流 ≥100ms。依据:\`repos/ClassHelper-master/packages/desktop-client/src/main/island.ts:72-112,480-512\`(固定包围盒思想)与第 4 部分 4.3(ClassIsland 全屏宽窗口适配)。
- 窗口标志:\`frame:false\`、\`transparent:true\`、\`resizable:false\`、\`movable:false\`、\`minimizable:false\`、\`maximizable:false\`、\`fullscreenable:false\`、\`skipTaskbar:true\`、\`hasShadow:false\`、\`focusable:false\`、\`alwaysOnTop:true\`、\`type:'toolbar'\`。依据:\`src/main/island.ts:480-504\`;其中 \`focusable\` 必须为 \`false\`(ClassIsland 主窗口不抢焦点,见 04 §4.2)。
- \`webPreferences\`:\`preload\` 指向 \`dist/preload/island.js\`、\`contextIsolation:true\`、\`nodeIntegration:false\`、\`sandbox:true\`、\`backgroundThrottling:false\`。依据:\`:505-511\`。
- 创建后必须:\`setAlwaysOnTop(appearance.alwaysOnTop,'screen-saver')\`、\`setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true})\`、\`setWindowOpenHandler(()=>({action:'deny'}))\`、\`will-navigate\` 白名单校验、默认 \`setIgnoreMouseEvents(true,{forward:true})\`、**禁止** \`setBackgroundMaterial()\`。依据:\`:514-527\`。
- 定位:按 ClassIsland 的 6 个停靠位(\`WindowDockingLocation\`:0 左上 / 1 中上 / 2 右上 / 3 左下 / 4 中下 / 5 右下)+ \`WindowDockingOffsetX/Y\`(默认 0)+ \`WindowDockingMonitorIndex\`(默认 0);ClassHelper 的 \`top-center\` 等 6 个枚举与 \`marginX/marginY=8\` 只作参考,**不得**替代 ClassIsland 的停靠与偏移语义。依据:\`repos/ClassIsland/ClassIsland/Models/Settings.cs:1810-1833\`。`;
if (!t.includes(oldBullets)) throw new Error('未找到 2.7 岛窗口段落');
t = t.replace(oldBullets, newBullets);
const oldSettings = `**设置窗口**必须采用 ClassHelper 主窗口参数作为基线(\`width 1242\`、\`height 582\`、\`minWidth 1000\`、\`minHeight 540\`、\`center:true\`、\`autoHideMenuBar:true\`、\`backgroundColor\` 随主题 \`#161719\`/\`#f3f3f3\`),并补上 \`preload: dist/preload/index.js\`、\`contextIsolation:true\`、\`nodeIntegration:false\`、\`sandbox:true\`、\`backgroundThrottling:false\`。依据:\`src/main/index.ts:63-88\`。`;
const newSettings = `**设置窗口**必须采用 **ClassIsland 的尺寸(第 10 部分为唯一口径)**:\`width 1240\`、\`height 550\`、\`minWidth 560\`、\`frame:false\`(Windows 11 21H2+ 可用 Mica,否则退回纯色背景)、\`center:true\`、\`minHeight 550\`,并补上 \`preload: dist/preload/index.js\`、\`contextIsolation:true\`、\`nodeIntegration:false\`、\`sandbox:true\`、\`backgroundThrottling:false\`。依据:\`repos/ClassIsland/ClassIsland/Views/SettingsWindowNew.axaml\`(第 10 部分 10.1)与 \`src/main/index.ts:63-88\`(窗口骨架参考)。`;
if (!t.includes(oldSettings)) throw new Error('未找到设置窗口段落');
t = t.replace(oldSettings, newSettings);
writeFileSync(p, t, 'utf8');
console.log('02 §2.7 patched');
+29
View File
@@ -0,0 +1,29 @@
import { readFileSync, writeFileSync } from 'node:fs';
const p = 'parts/02-tech-stack.md';
let t = readFileSync(p, 'utf8');
const repl = [
[
'4. 弹簧动画必须用 `SpringValue`(帧积分):默认 `stiffness=0.10`、`damping=0.68`、`momentumRetention=0.35`、`maxDistanceRatio=0.2`、`maxDt=6`、`snapThreshold=0.05px`、帧单位 `1000/60`、零过冲钳制。依据:`repos/ClassHelper-master/packages/desktop-client/src/island/spring.ts:26-30,41-47,85-114`。',
'4. 动画时长与曲线**以第 14 部分为准**(ClassIsland 的 Storyboard/Transition 规格);弹簧动画(`SpringValue`:`stiffness=0.10`、`damping=0.68`、`momentumRetention=0.35`、`maxDistanceRatio=0.2`、`maxDt=6`、`snapThreshold=0.05px`、帧单位 `1000/60`、零过冲钳制)只用于第 14 部分明确标注为「spring/跟手」的交互(如拖拽预览、内容宽度跟随),**不得**用它替代第 14 部分给出确定时长的规格动画。依据:`repos/ClassHelper-master/packages/desktop-client/src/island/spring.ts:26-30,41-47,85-114` 与第 14 部分。',
],
[
'5. 圆角必须用连续圆角(squircle):超椭圆指数 `4.2`、每角 `14` 段、半径 `min(r, w/2, h/2)`、展开卡半径 `max(radius,24)` 再夹紧。依据:`src/island/squircle.ts:21-24,44-81`。',
'5. 圆角实现:**默认必须使用 CSS `border-radius`(等同 Avalonia `CornerRadius`,半径取 `Settings.RadiusX`,默认 8)**,以保证与 ClassIsland 的直角圆弧逐像素一致;ClassHelper 的连续圆角(squircle:超椭圆指数 `4.2`、每角 `14` 段、半径 `min(r,w/2,h/2)`)只能由主题包显式启用(P2),不得作为默认,否则 8px 角的曲率会与 ClassIsland 原版不同。依据:`repos/ClassIsland/ClassIsland/MainWindow.axaml:282` 与 `XamlThemes/FluentTheme/Styles.axaml:48`;对照实现见 `src/island/squircle.ts:21-24,44-81`。',
],
[
'8. 经典岛外观默认值:`width 268`、`height 44`、`radius 20`、`fontSize 13`、`opacity 1`、`accent #0a84ff`、`style \'black\'`、`position \'top-center\'`、`marginX/marginY 8`、`idleSliver false`、`followCursorDisplay false`。范围:高度 36–72、宽 220–420、圆角 8–32、透明度 0.4–1、字号 11–20、速度 0.5–2、marginX 0–200、marginY 0–160。依据:`constants.ts:265-302`。',
'8. **参考值(不是本应用默认值)**:ClassHelper 既有「经典岛」外观默认值 `width 268`、`height 44`、`radius 20`、`fontSize 13`、`opacity 1`、`accent #0a84ff`、`style \'black\'`、`position \'top-center\'`、`marginX/marginY 8`、`idleSliver false`、`followCursorDisplay false`(范围:高度 36–72、宽 220–420、圆角 8–32、透明度 0.4–1、字号 11–20、速度 0.5–2、marginX 0–200、marginY 0–160,依据 `constants.ts:265-302`)。本应用(ClassIsland 复刻)的默认值**必须**按第 3、4、10 部分:行高 40、圆角 8、字号 14/16/18/20、透明度 0.5、停靠位 `WindowDockingLocation=1`、偏移 0,**不得**套用 ClassHelper 经典岛的 268×44 胶囊默认值。',
],
[
'| Avalonia `Window` + `TransparencyLevelHint=AcrylicBlur` | `BrowserWindow{transparent:true, frame:false}` + CSS 半透明背景 | Electron 下**禁止** `setBackgroundMaterial()`;毛玻璃用 CSS `backdrop-filter` 或半透明纯色(ClassHelper 的 glass 就是半透明色,不采样桌面模糊,见 `docs/dev/winisland-design-tokens.md:23`) |',
'| Avalonia `Window` + `TransparencyLevelHint=AcrylicBlur` | `BrowserWindow{transparent:true, frame:false}` + CSS 半透明背景 | Electron 下**禁止** `setBackgroundMaterial()`,也**禁止** `backdrop-filter`(透明窗口下 Chromium 采样不可靠,会闪/发灰);毛玻璃效果用**预混合半透明纯色**实现(ClassHelper 的 glass 同样是半透明色、不采样桌面模糊,见 `docs/dev/winisland-design-tokens.md:23`);编辑模式按第 4 部分用全屏半透明纯色遮罩 |',
],
];
for (const [oldS, newS] of repl) {
if (!t.includes(oldS)) throw new Error('未找到待替换段落:' + oldS.slice(0, 40));
t = t.replace(oldS, newS);
}
writeFileSync(p, t, 'utf8');
console.log('02 §2.10 / 2.12 patched');
+8
View File
@@ -0,0 +1,8 @@
import { readFileSync, writeFileSync } from 'node:fs';
const p = 'parts/03-design-tokens.md';
let t = readFileSync(p, 'utf8');
const oldS = '- 若引入 ClassHelper 的连续圆角(squircle),必须保持 `SUPERELLIPSE_EXPONENT = 4.2`、每角 `CORNER_SEGMENTS = 14`、展开卡半径 `min(48*scale, w/2, h/2)` 不变(`ClassHelper/packages/desktop-client/src/island/squircle.ts:21,24,79-81`)。**默认实现用 CSS `border-radius`**,连续圆角仅用于岛外形。';
const newS = '- 若引入 ClassHelper 的连续圆角(squircle),必须保持 `SUPERELLIPSE_EXPONENT = 4.2`、每角 `CORNER_SEGMENTS = 14`、展开卡半径 `min(48*scale, w/2, h/2)` 不变(`ClassHelper/packages/desktop-client/src/island/squircle.ts:21,24,79-81`)。**默认实现必须用 CSS `border-radius`**(与 Avalonia `CornerRadius` 的直角圆弧一致,保证与 ClassIsland 逐像素相同);连续圆角只能由主题包显式启用(P2),不得作为默认岛外形。';
if (!t.includes(oldS)) throw new Error('未找到 03 圆角约束段');
writeFileSync(p, t.replace(oldS, newS), 'utf8');
console.log('03 patched');
+15
View File
@@ -0,0 +1,15 @@
import { readFileSync, writeFileSync } from 'node:fs';
function patch(p, fn) {
const f = 'parts/' + p;
const t = readFileSync(f, 'utf8');
const n = fn(t);
if (n !== t) { writeFileSync(f, n, 'utf8'); console.log('patched ' + p); } else console.log('nochange ' + p);
}
patch('01-overview.md', (t) => t.replace('OOBE 完成后写 `settings.json` 的', 'OOBE 完成后写 `config.json`(并兼容导出 ClassIsland 的 `Settings.json`)的'));
patch('07-notifications.md', (t) => t.replace('持久化在 `Settings.json`(字段', '持久化在 `config.json`(ClassIsland 原生写入 `Settings.json`,字段'));
patch('10-settings.md', (t) =>
t
.replace('必须**立刻**把整个 `Settings.json` 写盘', '必须**立刻**把整个配置(`config.json`,ClassIsland 对应 `Settings.json`)写盘')
.replace('且 `Settings.json` 中 `Opacity` 为', '且 `config.json` 中 `opacity` 为')
.replace('可用 `Settings.json` 全文比对', '可用 `config.json` 全文比对'),
);
+8
View File
@@ -0,0 +1,8 @@
import { readFileSync, writeFileSync } from 'node:fs';
const p = 'analysis/50-refactor-brief.md';
let t = readFileSync(p, 'utf8');
const oldS = '3. **两种模式的定位**:\n - `fixed`(经典):保留"胶囊 + 详情卡 + 紧急展开"的消息交互与暂存规则(这是 ClassHelper 已有语义),但视觉、行/气泡布局、动画、强提醒按 ClassIsland 规格重做;增加 ClassIsland 风格的课程行/倒计时/进度条显示。\n - `custom`(常驻):保留现有 schemes/rows/nodes/expandedRows/编辑器的数据模型,按 ClassIsland 的行/组件/编辑模式/隐藏规则/动画规格重构渲染与交互;行高 40、组件间距、行设置抽屉、组件库拖拽、主行/允许提醒/淡出规则与 ClassIsland 对齐。';
const newS = '3. **两种模式的定位**(最终以 parts-r/01 为准):\n - `fixed`(默认风格 `classisland`):**重定义为 ClassIsland 风格的信息行**——默认 1 行 = 日期 + 课程表,行高 40、圆角 8、背景不透明度 0.5、底部 3px 进度条;消息到来时在同一行内展开 ClassIsland 风格气泡,保留 ClassHelper 的暂存/紧急/叫人/已读规则。原「胶囊 + 详情卡」形态**只在 `winisland` 兼容风格下保留**(`appearance.family=\'winisland\'`)。\n - `custom`(常驻):保留现有 schemes/rows/nodes/expandedRows/编辑器的数据模型,按 ClassIsland 的行/组件/编辑模式/隐藏规则/动画规格重构渲染与交互;行高 40、组件间距、行设置抽屉、组件库拖拽、主行/允许提醒/淡出规则与 ClassIsland 对齐。';
if (!t.includes(oldS)) throw new Error('未找到 brief §2.3 段落');
writeFileSync(p, t.replace(oldS, newS), 'utf8');
console.log('brief §2.3 updated');
+10
View File
@@ -0,0 +1,10 @@
import { readFileSync, writeFileSync } from 'node:fs';
const p = 'parts/00-preamble.md';
let t = readFileSync(p, 'utf8');
t = t.replace(
'1. **整体投喂**:若所用 ChatGPT 模型上下文足够(≥ 64k tokens),将本文件从「## 0.」到结尾一次性粘贴,尾附一句:「请严格按此提示词实现,先给出实施计划与文件树,再按里程碑推进。」',
'1. **整体投喂**:全文约 200k tokens,需使用上下文 ≥ 200k 的模型(如 GPT-5 级长上下文 / Gemini 2.5 Pro)才能一次粘贴;将本文件从「## 0.」到结尾整体粘贴,尾附一句:「请严格按此提示词实现,先给出实施计划与文件树,再按里程碑推进。」',
);
if (!t.includes('约 200k tokens')) throw new Error('preamble 替换失败');
writeFileSync(p, t, 'utf8');
console.log('preamble updated');
+117
View File
@@ -0,0 +1,117 @@
import { readdirSync, readFileSync, writeFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
const partsDir = join(root, 'parts');
const files = readdirSync(partsDir).filter((f) => /^\d{2}-.+\.md$/.test(f)).sort();
const titles = {
'00': '使用说明 + 任务声明(总入口)',
'01': '项目总纲(定位 / 体验原则 / 架构 / 功能全景)',
'02': '技术栈与工程结构(pnpm/Electron/Vue/Vite、进程模型、构建)',
'03': '设计系统 Token(颜色/字体/圆角/间距/阴影/层级/DPI)',
'04': '主界面(岛)像素级规格(窗口/行/背景/进度/遮罩/编辑/隐藏)',
'05': '组件系统(11 个内置组件逐个规格)',
'06': '课程表/时间表/档案/科目/周次/调课/导入导出',
'07': '提醒系统(队列/提供者/气泡/强提醒/音效/TTS/天气服务)',
'08': '自动化(触发器/行动/规则集)',
'09': '主题系统(Fluent/Classic、主题包、壁纸取色 P2)',
'10': '设置中心(19 个设置页逐项)',
'11': '其它窗口与向导(OOBE/闪屏/崩溃/恢复/更新/换课/Excel/教程…)',
'12': '插件系统、IPC、单实例与自动更新',
'13': '数据模型、存储、备份与迁移(JSON schema)',
'14': '动画与动效总表(99 条)',
'15': '资源清单(图标/字体/音效/天气图标/教程/本地化)',
'16': '质量门禁与验收清单',
'17': '开发里程碑(M0–M8)',
'18': '禁止事项与实现边界(红线)',
'19': '结语',
};
const rows = [];
for (const f of files) {
const t = readFileSync(join(partsDir, f), 'utf8');
const chars = [...t.replace(/\s/g, '')].length;
const cjk = (t.match(/[\u4e00-\u9fff]/g) || []).length;
const tokens = Math.round(cjk * 0.9 + (chars - cjk) * 0.3);
const num = f.slice(0, 2);
rows.push({ num, file: f, title: titles[num] || '', chars, cjk, tokens });
}
const table = rows
.map((r) => `| ${r.num} | ${r.title} | \`${r.file}\` | ${r.chars} | ${r.tokens} |`)
.join('\n');
const totalChars = rows.reduce((a, r) => a + r.chars, 0);
const totalTokens = rows.reduce((a, r) => a + r.tokens, 0);
const md = `# 投喂指南:把这份 ClassIsland 提示词交给 ChatGPT
> 本目录是最终交付物。主提示词共 20 个 part(00–19),合计约 **${totalChars}** 字符(去空白)、**约 ${totalTokens} tokens**。请不要一次性把全部内容塞进小上下文模型;按下面三套方案挑一套。
## 一、交付物
| 文件 | 内容 | 用途 |
|---|---|---|
| \`../ClassIsland-给ChatGPT的完整提示词.md\` | 全量主提示词(20 个 part,含目录) | 唯一权威版本;模型上下文足够时一次投喂 |
| \`vol1-总纲与技术栈(00-02).md\` ~ \`vol5-动画资源验收与结语(14-19).md\` | 5 个分卷 | 适合 128k 上下文模型分批投喂 |
| \`sections/\` | 20 个独立 part 文件 | 适合 32k–64k 上下文模型逐章投喂;也方便只补某一章 |
| \`../analysis/\`、\`../parts/\` | 调研记录与原始 part 源文件 | 供人工核对证据(不投喂给模型) |
## 二、各 part 体量与 token 估算
| # | 内容 | 文件 | 字符(去空白) | 估算 tokens |
|---|---|---|---|---|
${table}
> token 估算按「汉字 0.9、其他字符 0.3」粗算,仅用于选择投喂方式;实际以所用模型 tokenizer 为准。
## 三、三套投喂方案
### 方案 A:一次性投喂(上下文 ≥ 200k tokens)
- 适用:GPT-5 级别 / Claude 长上下文 / Gemini 2.5 Pro 等。
- 做法:粘贴 \`ClassIsland-给ChatGPT的完整提示词.md\` 全文,末尾追加:
> 请严格按此提示词实现「classisland-ts」。先通读全文,输出:①实施计划与完整文件树;②你识别出的歧义点(一次性列出,等我确认);③M0 的验收脚本设计。确认后再开始写代码。
- 注意:长上下文也会「中间遗忘」,务必要求模型在每个里程碑复述本阶段关键数值。
### 方案 B:五卷投喂(上下文 ≥ 128k tokens)
1. 第 1 批:\`vol1\` + 追加话术:「这是项目的总纲、技术栈与工程结构;先只确认理解,不要写代码。请复述 10 条体验原则、技术栈版本、进程模型。」
2. 第 2 批:\`vol2\` +「这是设计 tokens、主界面与组件规格;请输出你将创建的 Vue 组件树与 CSS token 文件草案。」
3. 第 3 批:\`vol3\` +「这是课表、提醒、自动化;请输出数据流与状态机设计。」
4. 第 4 批:\`vol4\` +「这是主题、设置、窗口、插件、数据;请输出 IPC 通道表与数据 schema 清单。」
5. 第 5 批:\`vol5\` +「这是动画、资源、验收、里程碑、红线;请输出最终实施顺序与验收计划,然后从 M0 开始实现。」
- 每批结束时要求模型把「已确认的关键参数」写进一份 \`SPEC-LOCK.md\`,下一批先复述再继续。
### 方案 C:逐章投喂(上下文 32k–64k)
- 适用:GPT-4o 等 32k/128k 模型或需要精细控制时。
- 做法:按 00 → 19 顺序,每次投喂 \`sections/\` 下 1–3 个 part(建议:00+01+02;03+04;05;06;07;08;09+10;11;12+13;14+15;16+17+18+19)。
- 每次投喂加一句:「只实现/修改与本章相关的文件;先复述本章关键数值与验收点,再动手;不要重写其他模块。」
## 四、建议的追问与验收话术
- 开工前:「请先输出 20 个你认为最可能歧义的地方,并给出你的默认决策。」
- 架构评审:「请给出 packages/desktop-client/src 的完整文件树,每个文件一句话职责;标注哪些文件对应提示词的哪个章节。」
- 像素校验:「请给出岛在 1920×1080、缩放 100%、深色 Fluent 主题下的基准截图校验清单(元素/坐标/颜色/字号)。」
- 动画校验:「请列出第 14 部分要求的 99 条动画中,你已实现/未实现的清单,并给出每条的验证方法。」
- 无头验证:「所有自动验证必须无头运行,不得显示可见窗口或播放声音;请提供 headless 测试命令与输出。」
- 数据安全:「请演示一次配置迁移:备份 → 迁移 → 校验 → 失败回滚。」
- 完成前:「请对照第 16 部分逐条勾选,并列出未通过项与原因;对照第 18 部分确认红线。」
## 五、必须向模型强调的三件事
1. **证据路径不是依赖**:文中 \`repos/ClassIsland/...\`、\`repos/ClassHelper-master/...\` 是规格来源,实现时应转写为 TypeScript/CSS,不得在产物中引用这两个仓库。
2. **技术栈不可替换**:pnpm 11 + Electron 44 + Vue 3.5 + Element Plus 2.14 + Pinia 4 + Vite 8 + TS 5.9;更新器自研(禁 electron-updater);岛不得引入 Element Plus 全局样式。
3. **不打扰纪律**:任何自动验证必须无头/离屏,不得弹窗、发声、占用真实桌面(第 18 部分)。
## 六、已知的「与 ClassIsland 原版差异」(模型需在交付说明中列出)
- Avalonia 窗口 → Electron 透明 BrowserWindow(固定包围盒 + 窗内形变,见 04 §4.3)。
- XAML 资源字典/主题 → CSS 变量 + 受控主题包(09)。
- 插件 DLL/.cipx → npm 包 + utilityProcess 宿主(12)。
- 命名管道 IPC → Electron IPC + 可选本地只读 HTTP(12)。
- Excel 导入导出:原版 2.1 已停用,本实现用 xlsx 重建为 P1(06/11)。
- 壁纸取色:原版 2.1 已停用,本实现仅 P2 实验性、默认关闭(09)。
- 置顶轮询:原版 1ms 轮询 → 事件驱动 + ≥1000ms 兜底(04/18)。
`;
writeFileSync(join(root, 'out', '投喂指南.md'), md, 'utf8');
console.log('written out/投喂指南.md');
console.log(md.split('\n').slice(0, 5).join('\n'));
+64
View File
@@ -0,0 +1,64 @@
# 将本仓库创建/推送到 Gitea(默认目标 http://10.126.232.10:3000)
# 用法:
# $env:GITEA_TOKEN = '<你的 Gitea 访问令牌>'
# $env:GITEA_URL = 'http://10.126.232.10:3000' # 可选
# $env:GITEA_REPO = 'classhelper-island-refactor-prompt' # 可选
# pwsh -File scripts/push-to-gitea.ps1
# 说明:令牌只从环境变量读取,不写入仓库文件;推送完成后会把 origin 还原为不含凭据的 URL。
param(
[string]$RepoName = $(if ($env:GITEA_REPO) { $env:GITEA_REPO } else { 'classhelper-island-refactor-prompt' }),
[string]$BaseUrl = $(if ($env:GITEA_URL) { $env:GITEA_URL } else { 'http://10.126.232.10:3000' }),
[switch]$Public
)
$ErrorActionPreference = 'Stop'
if (-not $env:GITEA_TOKEN) { throw '缺少环境变量 GITEA_TOKEN(Gitea 访问令牌)。' }
$token = $env:GITEA_TOKEN.Trim()
$base = $BaseUrl.TrimEnd('/')
$headers = @{ Authorization = "token $token"; 'Content-Type' = 'application/json' }
Write-Host "[1/4] 校验令牌:GET $base/api/v1/user"
$user = Invoke-RestMethod -Uri "$base/api/v1/user" -Headers $headers -Method Get -TimeoutSec 20
if (-not $user.login) { throw '无法从 Gitea 获取当前用户,令牌可能无效。' }
Write-Host " 当前用户:$($user.login)"
Write-Host "[2/4] 创建仓库:$RepoName($(if ($Public) {'公开'} else {'私有'}))"
$exists = $false
try {
Invoke-RestMethod -Uri "$base/api/v1/repos/$($user.login)/$RepoName" -Headers $headers -Method Get -TimeoutSec 15 | Out-Null
$exists = $true
Write-Host ' 仓库已存在,直接推送。'
} catch {
Write-Host " 仓库不存在或探测失败($($_.Exception.Message)),继续尝试创建。"
}
if (-not $exists) {
$body = @{ name = $RepoName; private = (-not $Public); auto_init = $false; description = 'ClassIsland / ClassHelper 灵动岛提示词套件(静态分析产物)' } | ConvertTo-Json
try {
$repo = Invoke-RestMethod -Uri "$base/api/v1/user/repos" -Headers $headers -Method Post -Body $body -TimeoutSec 20
Write-Host " 已创建:$($repo.full_name)"
Write-Host " 网页地址:$($repo.html_url)"
} catch {
if ($_.Exception.Response -and $_.Exception.Response.StatusCode.value__ -eq 409) {
Write-Host ' 仓库已存在(409),继续推送。'
} else {
throw "创建仓库失败:$($_.Exception.Message)"
}
}
}
Write-Host '[3/4] 推送 main 分支'
$scheme = ($base -split '://')[0]
$hostPart = ($base -split '://')[1]
$cleanUrl = "$base/$($user.login)/$RepoName.git"
$authUrl = "${scheme}://$([uri]::EscapeDataString($user.login)):$([uri]::EscapeDataString($token))@$hostPart/$($user.login)/$RepoName.git"
git remote remove origin 2>$null
git remote add origin $authUrl
try {
git push -u origin HEAD:main
} finally {
git remote set-url origin $cleanUrl
Write-Host " 已将 origin 还原为不含凭据的地址:$cleanUrl"
}
Write-Host '[4/4] 完成'
Write-Host "仓库:$cleanUrl"
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env node
/** 对 parts/*.md 做机械质检:长度、标题、证据密度、占位符、编号一致性、关键数值出现情况 */
import { readdirSync, readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
const dir = join(root, 'parts');
const files = readdirSync(dir).filter((f) => /^\d{2}-.+\.md$/.test(f)).sort();
const placeholders = [/TODO/i, /待补充/, /待完善/, /占位/, /省略/, /\.\.\.\.\.\./, /示例文本/];
const rows = [];
for (const f of files) {
const text = readFileSync(join(dir, f), 'utf8');
const noWs = text.replace(/\s/g, '');
const num = f.slice(0, 2);
const h2 = [...text.matchAll(/^## (.+)$/gm)].map((m) => m[1]);
const evidence = (text.match(/repos\/(ClassIsland|ClassHelper-master)\/[^\s`::,,))]+/g) || []).length;
const inferred = (text.match(/【推断】/g) || []).length;
const ph = placeholders.filter((r) => r.test(text)).map((r) => r.source);
const firstHeadingOk = h2.length > 0 && new RegExp(`^${Number(num)}\\.`).test(h2[0]);
rows.push({ f, chars: [...noWs].length, lines: text.split('\n').length, h2: h2.length, evidence, inferred, ph, firstHeadingOk, firstH2: h2[0] || '' });
}
console.log('file'.padEnd(26), 'chars'.padStart(7), 'lines'.padStart(6), 'h2'.padStart(3), 'evid'.padStart(5), 'infer'.padStart(5), 'numOK', 'placeholders');
for (const r of rows) {
console.log(
r.f.padEnd(26),
String(r.chars).padStart(7),
String(r.lines).padStart(6),
String(r.h2).padStart(3),
String(r.evidence).padStart(5),
String(r.inferred).padStart(5),
String(r.firstHeadingOk).padStart(5),
r.ph.join('|') || '-',
);
}
const total = rows.reduce((a, r) => a + r.chars, 0);
console.log(`\n合计 ${rows.length} 个 part,去空白字符 ${total}`);
const missing = [];
for (let i = 0; i <= 19; i++) {
const n = String(i).padStart(2, '0');
if (!files.some((f) => f.startsWith(n + '-'))) missing.push(n);
}
console.log('缺失 part:', missing.join(',') || '无');
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env node
/** 术语统一:自动化 Action 统一为「行动」;非自动化语境保留「动作」。 */
import { readFileSync, writeFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
const dir = join(root, 'parts');
function edit(file, fn) {
const p = join(dir, file);
const before = readFileSync(p, 'utf8');
const after = fn(before);
if (after !== before) {
writeFileSync(p, after, 'utf8');
console.log(`updated ${file}`);
}
}
// 纯自动化语境:全文 动作→行动
for (const f of ['01-overview.md', '02-tech-stack.md', '10-settings.md', '13-data-storage.md', '16-acceptance.md', '17-milestones.md', '18-forbidden.md']) {
edit(f, (t) => t.split('动作').join('行动'));
}
// 04:进入/退出「动作」指步骤;表格列指操作
edit('04-main-window.md', (t) =>
t.replaceAll('进入动作(按序)', '进入步骤(按序)').replaceAll('退出动作:', '退出步骤:').replace('| # | 文本 | 图标 glyph | 可见条件 | 动作 |', '| # | 文本 | 图标 glyph | 可见条件 | 操作 |'),
);
// 12:自动化语境→行动;交互语境改成事件/操作
edit('12-plugins-ipc.md', (t) =>
t
.split('自动化触发器/动作').join('自动化触发器/行动')
.split('触发器/动作').join('触发器/行动')
.split('注册触发器/动作').join('注册触发器/行动')
.split('自动化动作').join('自动化行动')
.split('触发自动化动作').join('触发自动化行动')
.split('(岛动作、心跳、交互上报)').join('(岛事件、心跳、交互上报)')
.split('上报用户动作与命中信息').join('上报用户操作与命中信息')
.split('所有自愈动作必须写日志').join('所有自愈操作必须写日志'),
);
// 15:教程行动→行动;贴纸「角色_动作」保持
edit('15-assets.md', (t) =>
t
.replace('进入段落时执行的动作', '进入段落时执行的行动')
.replace('动作列表,元素为', '行动列表,元素为')
.replace('动作 `Kind` 的具体枚举含义', '行动 `Kind` 的具体枚举含义'),
);
// 11:教程行动统一(如有)
edit('11-other-windows.md', (t) => (t.includes('动作') ? t.split('动作').join('行动') : t));
console.log('done');