自动化 Skill 指纹同步并发布 AGC 0.1.10

新增 skill-pack-manifest 脚本统一计算并校验内置 AGC Skill 内容摘要,--write 模式自动递增清单版本并同步 SHA-256。

新增 check-skill-pack 只读门禁与 Node 回归测试,无参数默认只读,只有单个 --write 才进入写入模式。

AGC typecheck 与 release build 接入 Skill 清单指纹校验,内容漂移时直接列出 Skill 与实际摘要阻断构建。

修正 agc-client-projection 清单指纹并升级 Skill pack 版本到 2026-08-26.3。

升级 AGC 标准版到 0.1.10,同步 package、Cargo、Tauri 与 npm 工作区锁文件。

首页空输入占位符锚定到编辑区,避免整页滚动后占位文本脱离输入框。

更新 AGC 实施计划中 Skill 指纹同步与只读门禁说明。
This commit is contained in:
2026-08-26 23:23:24 +08:00
parent 92165a36ae
commit d75547b36d
16 changed files with 345 additions and 17 deletions
+5 -2
View File
@@ -1,13 +1,16 @@
{
"name": "@genarrative/ai-game-creator-shell",
"private": true,
"version": "0.1.9",
"version": "0.1.10",
"type": "module",
"scripts": {
"dev": "node scripts/start-tauri-dev.mjs",
"dev-server": "node scripts/start-dev-server.mjs",
"dev-stack": "node scripts/start-dev-stack.mjs",
"build": "npm --prefix ../.. exec tauri -- build",
"skill-pack:check": "node scripts/check-skill-pack.mjs",
"skill-pack:sync": "node scripts/check-skill-pack.mjs --write",
"skill-pack:test": "node --test scripts/check-skill-pack.test.mjs",
"llm-status": "node scripts/run-cli-with-config.mjs --llm-status",
"agent-task": "node scripts/run-cli-with-config.mjs --agent-task",
"chat": "node scripts/run-cli-with-config.mjs --swarm-chat",
@@ -31,7 +34,7 @@
"agent-runtime:supervisor-swarm-tool-plan-handoff-runner-kill-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-tool-plan-handoff-runner-kill",
"agent-runtime:steer-real-e2e": "node scripts/agent-runtime-steer-real-e2e.mjs",
"agent-runtime:steer-runner-kill-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite steer-runner-kill",
"typecheck": "tsc -p tsconfig.json --noEmit && node scripts/check-config.mjs"
"typecheck": "tsc -p tsconfig.json --noEmit && npm run skill-pack:check && node scripts/check-config.mjs"
},
"dependencies": {
"@cubone/react-file-manager": "^1.35.0",
@@ -1533,11 +1533,13 @@ if (
}
if (
tauriConfig.version !== '0.1.9' ||
packageConfig.version !== '0.1.9' ||
cargoPackageVersion !== '0.1.9'
tauriConfig.version !== '0.1.10' ||
packageConfig.version !== '0.1.10' ||
cargoPackageVersion !== '0.1.10'
) {
throw new Error('AI game creator standard release must remain version 0.1.9');
throw new Error(
'AI game creator standard release must remain version 0.1.10',
);
}
const devServerSource = fs.readFileSync(
@@ -0,0 +1,48 @@
import process from 'node:process';
import {
inspectSkillPack,
syncSkillPackManifest,
} from './skill-pack-manifest.mjs';
const argumentsList = process.argv.slice(2);
const writeMode = argumentsList.length === 1 && argumentsList[0] === '--write';
if (
argumentsList.length > 1 ||
(argumentsList.length === 1 && argumentsList[0] !== '--write')
) {
console.error('用法:node scripts/check-skill-pack.mjs [--write]');
process.exit(1);
}
try {
if (writeMode) {
const result = syncSkillPackManifest();
if (!result.changed) {
console.log(`[skill-pack] 已是最新(version=${result.version}`);
} else {
console.log(
`[skill-pack] 已同步 ${result.mismatches.map((item) => item.name).join('、')}version=${result.version}`,
);
}
} else {
const result = inspectSkillPack();
if (result.mismatches.length > 0) {
console.error('[skill-pack] 内容指纹与 manifest 不一致:');
for (const mismatch of result.mismatches) {
console.error(
`- ${mismatch.name}: manifest=${mismatch.expected} actual=${mismatch.actual}`,
);
}
console.error('[skill-pack] 内容变更后运行:npm run agc:skill-pack:sync');
process.exitCode = 1;
} else {
console.log(`[skill-pack] OKversion=${result.manifest.version}`);
}
}
} catch (error) {
console.error(
`[skill-pack] ${error instanceof Error ? error.message : String(error)}`,
);
process.exitCode = 1;
}
@@ -0,0 +1,49 @@
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
import {
computeSkillContentFingerprint,
inspectSkillPack,
} from './skill-pack-manifest.mjs';
test('bundled skill pack manifest is synchronized', () => {
const result = inspectSkillPack();
assert.deepEqual(result.mismatches, []);
});
test('skill content fingerprint canonicalizes CRLF', () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agc-skill-pack-'));
try {
fs.mkdirSync(path.join(root, 'demo'), { recursive: true });
const entry = {
name: 'demo',
files: ['SKILL.md'],
};
fs.writeFileSync(path.join(root, 'demo', 'SKILL.md'), 'line 1\nline 2\n');
const lf = computeSkillContentFingerprint(root, entry);
fs.writeFileSync(
path.join(root, 'demo', 'SKILL.md'),
'line 1\r\nline 2\r\n',
);
assert.equal(computeSkillContentFingerprint(root, entry), lf);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
test('check command without arguments remains read-only', () => {
const scriptPath = path.join(
path.dirname(fileURLToPath(import.meta.url)),
'check-skill-pack.mjs',
);
const result = spawnSync(process.execPath, [scriptPath], {
encoding: 'utf8',
});
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /\[skill-pack\] OK/u);
});
@@ -0,0 +1,213 @@
import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { TextDecoder } from 'node:util';
export const SKILL_PACK_SCHEMA_VERSION = 'agc-skill-pack.v1';
export const EXPECTED_SKILL_NAMES = Object.freeze([
'agc-browser-playtest',
'agc-client-projection',
'agc-project-structure',
'agc-web-game-development',
'taonier-art-assets',
]);
const utf8Decoder = new TextDecoder('utf-8', { fatal: true });
const defaultRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
'../src-tauri/resources/agc-skills',
);
function canonicalTextBytes(filePath) {
const decoded = utf8Decoder.decode(fs.readFileSync(filePath));
return Buffer.from(decoded.replaceAll('\r\n', '\n'), 'utf8');
}
export function isSafeSkillRelativePath(value) {
if (
typeof value !== 'string' ||
value.length === 0 ||
value.includes('\\') ||
value.includes(':') ||
value.startsWith('/')
) {
return false;
}
return value
.split('/')
.every(
(segment) => segment.length > 0 && segment !== '.' && segment !== '..',
);
}
function skillFilePath(rootDir, skillName, relativePath) {
if (!isSafeSkillRelativePath(relativePath)) {
throw new Error(`Skill ${skillName} 包含不安全相对路径: ${relativePath}`);
}
const target = path.resolve(rootDir, skillName, ...relativePath.split('/'));
const skillRoot = path.resolve(rootDir, skillName);
const prefix = `${skillRoot}${path.sep}`;
if (!target.startsWith(prefix)) {
throw new Error(`Skill ${skillName} 路径越过审核根目录: ${relativePath}`);
}
return target;
}
export function computeSkillContentFingerprint(rootDir, entry) {
const digest = crypto.createHash('sha256');
for (const relativePath of [...entry.files].sort()) {
const filePath = skillFilePath(rootDir, entry.name, relativePath);
const bytes = canonicalTextBytes(filePath);
digest.update(relativePath, 'utf8');
digest.update(Buffer.from([0]));
digest.update(bytes);
digest.update(Buffer.from([0]));
}
return digest.digest('hex');
}
function collectBundledFiles(rootDir) {
const files = [];
const walk = (directory, prefix) => {
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
const absolutePath = path.join(directory, entry.name);
if (entry.isSymbolicLink()) {
throw new Error(`内置 AGC Skill 不允许符号链接: ${relativePath}`);
}
if (entry.isDirectory()) {
walk(absolutePath, relativePath);
} else if (entry.isFile()) {
files.push(relativePath.replaceAll('\\', '/'));
} else {
throw new Error(`内置 AGC Skill 文件类型不受支持: ${relativePath}`);
}
}
};
walk(rootDir, '');
return files.filter((file) => file !== 'manifest.json').sort();
}
function readManifest(rootDir) {
const manifestPath = path.join(rootDir, 'manifest.json');
return {
manifestPath,
manifest: JSON.parse(fs.readFileSync(manifestPath, 'utf8')),
};
}
function validateManifestShape(rootDir, manifest) {
if (manifest?.schemaVersion !== SKILL_PACK_SCHEMA_VERSION) {
throw new Error('内置 AGC Skill 清单 schemaVersion 不受支持');
}
if (typeof manifest.version !== 'string' || manifest.version.trim() === '') {
throw new Error('内置 AGC Skill 清单缺少版本');
}
if (!Array.isArray(manifest.skills)) {
throw new Error('内置 AGC Skill 清单缺少 skills 数组');
}
const names = manifest.skills.map((entry) => entry?.name);
if (
names.length !== EXPECTED_SKILL_NAMES.length ||
[...names].sort().join('\n') !== [...EXPECTED_SKILL_NAMES].sort().join('\n')
) {
throw new Error('内置 AGC Skill 清单不等于审核白名单');
}
const declaredFiles = new Set();
const mismatches = [];
for (const entry of manifest.skills) {
if (
typeof entry.name !== 'string' ||
!Array.isArray(entry.files) ||
entry.files.length === 0 ||
!entry.files.includes('SKILL.md') ||
new Set(entry.files).size !== entry.files.length
) {
throw new Error(
`内置 AGC Skill ${entry.name ?? '<unknown>'} 元数据不完整`,
);
}
for (const relativePath of entry.files) {
if (!isSafeSkillRelativePath(relativePath)) {
throw new Error(
`内置 AGC Skill ${entry.name} 包含不安全相对路径: ${relativePath}`,
);
}
declaredFiles.add(`${entry.name}/${relativePath}`);
}
const actual = computeSkillContentFingerprint(rootDir, entry);
if (actual !== entry.sha256) {
mismatches.push({
name: entry.name,
expected: entry.sha256,
actual,
});
}
}
const bundledFiles = collectBundledFiles(rootDir);
if (
declaredFiles.size !== bundledFiles.length ||
[...declaredFiles].sort().join('\n') !== bundledFiles.join('\n')
) {
throw new Error('内置 AGC Skill 文件集合与审核清单不一致');
}
return { mismatches };
}
export function inspectSkillPack(rootDir = defaultRoot) {
const resolvedRoot = path.resolve(rootDir);
const { manifestPath, manifest } = readManifest(resolvedRoot);
const { mismatches } = validateManifestShape(resolvedRoot, manifest);
return { manifestPath, manifest, mismatches };
}
function incrementPackVersion(version) {
const match = /^(\d{4}-\d{2}-\d{2})\.(\d+)$/u.exec(version);
if (!match) {
throw new Error(
`无法自动递增 Skill pack 版本 ${version},请使用 YYYY-MM-DD.N 格式`,
);
}
return `${match[1]}.${Number(match[2]) + 1}`;
}
export function syncSkillPackManifest(rootDir = defaultRoot) {
const inspection = inspectSkillPack(rootDir);
if (inspection.mismatches.length === 0) {
return {
changed: false,
version: inspection.manifest.version,
mismatches: [],
};
}
const mismatchByName = new Map(
inspection.mismatches.map((mismatch) => [mismatch.name, mismatch.actual]),
);
const nextManifest = {
...inspection.manifest,
version: incrementPackVersion(inspection.manifest.version),
skills: inspection.manifest.skills.map((entry) =>
mismatchByName.has(entry.name)
? { ...entry, sha256: mismatchByName.get(entry.name) }
: entry,
),
};
fs.writeFileSync(
inspection.manifestPath,
`${JSON.stringify(nextManifest, null, 2)}\n`,
'utf8',
);
const verified = inspectSkillPack(rootDir);
if (verified.mismatches.length > 0) {
throw new Error('Skill pack manifest 同步后仍存在内容指纹不匹配');
}
return {
changed: true,
version: nextManifest.version,
mismatches: inspection.mismatches,
};
}
+1 -1
View File
@@ -1695,7 +1695,7 @@ dependencies = [
[[package]]
name = "genarrative-ai-game-creator-shell"
version = "0.1.9"
version = "0.1.10"
dependencies = [
"agent-runtime-core",
"axum",
@@ -1,6 +1,6 @@
[package]
name = "genarrative-ai-game-creator-shell"
version = "0.1.9"
version = "0.1.10"
edition = "2021"
publish = false
@@ -1,6 +1,6 @@
{
"schemaVersion": "agc-skill-pack.v1",
"version": "2026-08-26.2",
"version": "2026-08-26.3",
"skills": [
{
"name": "agc-project-structure",
@@ -104,7 +104,7 @@
"agents/openai.yaml",
"references/projection-contract.md"
],
"sha256": "790d0788a8b1585e95b7d2181b0d09af611b2c673596a56e46a853bea736a4da"
"sha256": "2e11baf232bd1a786cc3189a9183e3a687b846c4e0816393e5b5687551a5eeb7"
}
]
}
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Genarrative AI Game Creator",
"version": "0.1.9",
"version": "0.1.10",
"identifier": "world.genarrative.ai-game-creator",
"build": {
"beforeDevCommand": "npm --prefix ../.. run agc:serve",
@@ -239,7 +239,7 @@ export default function RichInputArea(props: RichInputAreaProps) {
},
}}
>
<div className="grid min-h-9 gap-2">
<div className="relative grid min-h-9 gap-2">
<RichTextPlugin
contentEditable={
<ContentEditable
@@ -248,7 +248,7 @@ export default function RichInputArea(props: RichInputAreaProps) {
/>
}
placeholder={
<span className="pointer-events-none absolute text-[13px] text-(--platform-text-muted)">
<span className="pointer-events-none absolute inset-x-0 top-0 text-[13px] text-(--platform-text-muted)">
{props.placeholder}
</span>
}
@@ -28,6 +28,17 @@ import {
} from './harness';
export function registerClientHomeTests() {
it('anchors the empty home input placeholder to the editor while the page scrolls', () => {
renderLauncherAt('/?launcher');
const placeholder = screen.getByText('今天想把什么灵感做成游戏');
expect(placeholder.classList.contains('absolute')).toBe(true);
expect(placeholder.classList.contains('top-0')).toBe(true);
expect(placeholder.parentElement?.classList.contains('relative')).toBe(
true,
);
});
it('shows the built-in inspiration masonry gallery and opens a dismissible preview without requesting the retired feed', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch');
renderLauncherAt('/?launcher');
@@ -1178,7 +1178,7 @@ game-project/
- 普通项目对话只由一个 project-bound Codex app-server thread 执行。客户端系统提示词只放最小工程合同、当前游戏源码有界快照、项目 prompts 和审核 Skill 索引;不再批量读取项目 `.codex/.agents/.hermes` Skill 正文,也不恢复 Supervisor、专业 Agent 或 harness。
- 首页恢复“做游戏 / 做素材 / 做方案”三个创作类型,默认“做游戏”。该选择与设置页的 Agent Runtime 模式无关;每次首页提交仍只自动创建一个新项目并进入项目工作台。用户正文原样进入项目对话,`game|art|doc` 仅作为受限结构化首轮上下文传给同一 Codex thread,不拼接“初始意图”文案、不产生首页对话、不切换 Provider 或恢复旧 Runtime 编排。
- `agc-skill-pack.v1` 只包含项目结构、陶泥儿美术、Web 游戏实现、真实浏览器试玩、客户端资源投影五项 Skill。清单记录用途、触发条件、所需工具、版本和内容 SHA-256;审核文本按 UTF-8 读取并将 CRLF 规范为 LF 后计算指纹和安装,避免混合换行造成 Windows / Linux 构建结果漂移,语义内容变化时必须同步重算对应清单指纹并提升版本。客户端把审核文件安装到隔离目录后通过 app-server `skills/extraRoots/set + skills/list` 注册并复核,完整正文由 Codex 原生 Skill 机制按意图加载,一层引用只能经 `agc_read_skill_resource` 读取清单内 Markdown。引用路径按平台无关规则拒绝反斜杠、盘符、UNC、绝对路径和 `..`,不能依赖当前宿主的 `std::path` 语义判断其它平台路径。
- `agc-skill-pack.v1` 只包含项目结构、陶泥儿美术、Web 游戏实现、真实浏览器试玩、客户端资源投影五项 Skill。清单记录用途、触发条件、所需工具、版本和内容 SHA-256;审核文本按 UTF-8 读取并将 CRLF 规范为 LF 后计算指纹和安装,避免混合换行造成 Windows / Linux 构建结果漂移,语义内容变化时必须同步重算对应清单指纹并提升版本。同步统一运行 `npm run agc:skill-pack:sync`,只读校验由 AGC `typecheck` 和 release build 自动执行,发现漂移时直接列出 Skill 与实际摘要,不让失配内容进入构建产物。客户端把审核文件安装到隔离目录后通过 app-server `skills/extraRoots/set + skills/list` 注册并复核,完整正文由 Codex 原生 Skill 机制按意图加载,一层引用只能经 `agc_read_skill_resource` 读取清单内 Markdown。引用路径按平台无关规则拒绝反斜杠、盘符、UNC、绝对路径和 `..`,不能依赖当前宿主的 `std::path` 语义判断其它平台路径。
- DirectProject 只连接客户端内置的 `agc_tools` STDIO MCP,工具固定为审核引用读取、标准陶泥儿美术准备、已登记资源有界查询、视频 / 角色动画 / 音效 / BGM 的 create-or-derive 语义生成、已登记图片去背景和 desktop/mobile 浏览器试玩。MCP 进程只做协议;真实浏览器与付费 External v1 调用通过随机 loopback 地址回到客户端主进程,因此不复制 GUI 登录态、开发者 Key、项目路径、revision、operation 或幂等键到模型上下文。已登记工具固定自动批准,但付费资源工具仍由客户端绑定稳定回合身份、限制单回合请求数、串行执行并优先恢复匹配账本;通用 shell、Codex 原生 webSearch、任意网络、多 Agent、插件和外部 MCP 继续关闭。`codex_app_server` 模式要求 `llm.webSearchEnabled=false`
- 陶泥儿生成继续复用持久幂等账本、operation 恢复、来源/下载/PNG 解码和 manifest 登记;普通客户端优先使用当前 AGC 登录会话及账号路由,只有受控的 ExternalDeveloper 发布模式才在客户端内部使用按服务器 origin 隔离的私有 Key。用户和模型都不需要提供或配置 API Key;凭据失效、来源不明或结果未知时失败关闭,不能自动换 Key 或重新扣费。
- 自定义 LLM API Key 路由只在 DirectHome/DirectProject 经 loopback `/responses` 流式代理转发。代理不注入 Key,只要求请求自带 Bearer,并剥离开发网关错误携带的 `X-Codex-*` ChatGPT 账户额度头,防止隔离 app-server 把 API Provider 误判为余额 0;旧 ToolHost 保持原 Provider 行为。
+1 -1
View File
@@ -93,7 +93,7 @@
},
"apps/ai-game-creator-shell": {
"name": "@genarrative/ai-game-creator-shell",
"version": "0.1.9",
"version": "0.1.10",
"dependencies": {
"@cubone/react-file-manager": "^1.35.0",
"@genarrative/image-canvas-core": "0.1.0",
+2
View File
@@ -158,6 +158,8 @@
"agc:backend": "node scripts/dev.mjs backend",
"agc:config": "npm --prefix apps/ai-game-creator-shell run config --",
"agc:build": "npm --prefix apps/ai-game-creator-shell run build --",
"agc:skill-pack:check": "npm --prefix apps/ai-game-creator-shell run skill-pack:check",
"agc:skill-pack:sync": "npm --prefix apps/ai-game-creator-shell run skill-pack:sync",
"agc:check": "npm run ai-game-creator-shell:check",
"agc:typecheck": "npm --prefix apps/ai-game-creator-shell run typecheck",
"agc:test": "npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-e2e --",
+1 -1
View File
@@ -174,7 +174,7 @@ export function collectNpmWorkspaceErrors(rootDir) {
);
}
const expectedWorkspaceVersion =
workspacePath === 'apps/ai-game-creator-shell' ? '0.1.9' : '0.1.0';
workspacePath === 'apps/ai-game-creator-shell' ? '0.1.10' : '0.1.0';
if (manifest.version !== expectedWorkspaceVersion) {
errors.push(
`${manifestPath}: workspace version must be ${expectedWorkspaceVersion}`,
+1 -1
View File
@@ -79,7 +79,7 @@ function createValidFixture() {
name: workspaceNames[workspacePath],
private: true,
version:
workspacePath === 'apps/ai-game-creator-shell' ? '0.1.9' : '0.1.0',
workspacePath === 'apps/ai-game-creator-shell' ? '0.1.10' : '0.1.0',
dependencies: localDependencies[workspacePath],
};
writeJson(rootDir, `${workspacePath}/package.json`, manifest);