feat: ClassIsland / ClassHelper 灵动岛提示词套件
- 重构版主提示词(14 parts,面向 ClassHelper 灵动岛重构) - ClassIsland 全量规格提示词(20 parts,含分卷与逐章版本) - 投喂指南、分卷、逐节文件 - 调研记录、一致性基线、生成与质检脚本
This commit is contained in:
commit
22c1784bab
117 files changed
+47759
No files matched your search
@@ -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} 个)`);
|
||||
}
|
||||
@@ -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} 个)`);
|
||||
}
|
||||
@@ -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'`"));
|
||||
@@ -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 + '...');
|
||||
@@ -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');
|
||||
@@ -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');
|
||||
@@ -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 蓝色圆形秒数徽标。'],
|
||||
]);
|
||||
@@ -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 去重、通知/叫人双播不重复、课件传输优先、默认验证不创建可见岛或托盘、冒烟假执行不真关机。'],
|
||||
]);
|
||||
@@ -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 规格;默认按用户升级策略选择;设置页可切换。'],
|
||||
]);
|
||||
@@ -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);消息到达/展开/多条列表均不改变窗口尺寸。'],
|
||||
]);
|
||||
@@ -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');
|
||||
@@ -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');
|
||||
@@ -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');
|
||||
@@ -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` 全文比对'),
|
||||
);
|
||||
@@ -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');
|
||||
@@ -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');
|
||||
@@ -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'));
|
||||
@@ -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"
|
||||
@@ -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(',') || '无');
|
||||
@@ -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');
|
||||
Reference in new issue
Block a user