AGC 编辑器分支产物归位(M3,待 Windows 验收) #524

Merged
suzmii merged 187 commits from feat/agc-bundled-resources-m3 into fix/agc-rust-staging-idempotency 2026-09-30 17:47:42 +08:00
6 changed files with 127 additions and 16 deletions
Showing only changes of commit 110f088d41 - Show all commits
@@ -5,6 +5,8 @@ import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { listForbiddenBundledResourceFiles } from './macos-bundle-policy.mjs';
// 只操作临时复制品;不启动 GUI、不读取开发机凭据、不访问 Provider。
assert.equal(process.platform, 'darwin', '此验证必须在 macOS 执行');
const source = path.resolve(process.argv[2] || '');
@@ -287,22 +289,32 @@ try {
]) {
assert.ok(fs.existsSync(path.join(plugin, file)), file);
}
// 随包 Node 的 npm 是包里唯一允许出现的 node_modules:除了 npm 目录自身与它的子项,
// 还要放行它的上级目录 `game-runtime/node/node_modules`(recursive readdir 会列出目录项,
// 少了这一条会让整个门禁对合法包失败——#439 引入后一直没被跑到,直到 2026-09-21 才暴露)。
const allowedNodeModules = (file) =>
file === 'game-runtime/node/node_modules' ||
file === 'game-runtime/node/node_modules/npm' ||
file.startsWith('game-runtime/node/node_modules/npm/');
const claudeAgentRoot = path.join(resources, 'claude-agent');
const claudeAgentSdk = path.join(
claudeAgentRoot,
'node_modules/@anthropic-ai/claude-agent-sdk',
);
const claudeAgentBinaryPackage = `claude-agent-sdk-darwin-${architecture === 'arm64' ? 'arm64' : 'x64'}`;
const claudeAgentBinary = path.join(
claudeAgentRoot,
'node_modules/@anthropic-ai',
claudeAgentBinaryPackage,
'claude',
);
for (const required of [
path.join(claudeAgentRoot, 'index.mjs'),
path.join(claudeAgentSdk, 'sdk.mjs'),
claudeAgentBinary,
]) {
assert.ok(fs.existsSync(required), required);
}
// 随包只允许 Node runtime 的 npm 与 Claude Agent SDK sidecar 这两棵
// node_modules 子树;任何其它 node_modules 都是构建残留或不可控依赖。
const packageFiles = fs.readdirSync(resources, { recursive: true });
assert.ok(
!packageFiles.some(
(file) =>
/(^|\/)(\.env[^/]*|auth\.json|target|\.git)(\/|$)|\.(exe|dll)$/.test(
file,
) ||
(/(^|\/)node_modules(\/|$)/.test(file) && !allowedNodeModules(file)),
),
assert.deepEqual(
listForbiddenBundledResourceFiles(packageFiles),
[],
'安装包包含禁止资源',
);
assert.equal(run(executable, ['--version']).stdout.trim(), manifest.version);
assert.equal(
@@ -0,0 +1,46 @@
/**
* macOS 安装包内容策略。
*
* `check-macos-bundle.mjs` 会扫描随包 resources。Node runtime 与 Claude Agent SDK
* sidecar 都需要 `node_modules` 形式的目录,但其它构建残留或第三方依赖不能被
* 静默带入安装包,因此这里只放行两棵明确的生产子树。
*/
const NODE_RUNTIME_NODE_MODULES = 'game-runtime/node/node_modules';
const CLAUDE_AGENT_NODE_MODULES = 'claude-agent/node_modules';
const CLAUDE_AGENT_SCOPE = `${CLAUDE_AGENT_NODE_MODULES}/@anthropic-ai`;
const CLAUDE_AGENT_PACKAGE_PATTERN =
/^(?:claude-agent-sdk|claude-agent-sdk-darwin-(?:arm64|x64))(?:\/.*)?$/u;
function isAllowedClaudeAgentNodeModulesPath(file) {
if (file === CLAUDE_AGENT_NODE_MODULES || file === CLAUDE_AGENT_SCOPE) {
return true;
}
if (!file.startsWith(`${CLAUDE_AGENT_SCOPE}/`)) {
return false;
}
return CLAUDE_AGENT_PACKAGE_PATTERN.test(
file.slice(`${CLAUDE_AGENT_SCOPE}/`.length),
);
}
export function isAllowedBundledNodeModulesPath(file) {
return (
file === NODE_RUNTIME_NODE_MODULES ||
file === `${NODE_RUNTIME_NODE_MODULES}/npm` ||
file.startsWith(`${NODE_RUNTIME_NODE_MODULES}/npm/`) ||
isAllowedClaudeAgentNodeModulesPath(file)
);
}
export function listForbiddenBundledResourceFiles(files) {
return files.filter(
(file) =>
/(^|\/)(\.env[^/]*|auth\.json|target|\.git)(\/|$)|\.(exe|dll)$/u.test(
file,
) ||
(/(^|\/)node_modules(\/|$)/u.test(file) &&
!isAllowedBundledNodeModulesPath(file)),
);
}
@@ -1,6 +1,7 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { listForbiddenBundledResourceFiles } from './macos-bundle-policy.mjs';
import {
assertMacosAppMatchesChannelIdentity,
assertManifestArtifactMatchesExpected,
@@ -8,6 +9,42 @@ import {
readMacosAppInfoIdentity,
} from './macos-release-identity.mjs';
test('allows only the production node_modules subtrees in the macOS bundle', () => {
assert.deepEqual(
listForbiddenBundledResourceFiles([
'game-runtime/node/node_modules',
'game-runtime/node/node_modules/npm/bin/npm-cli.js',
'claude-agent/node_modules',
'claude-agent/node_modules/@anthropic-ai',
'claude-agent/node_modules/@anthropic-ai/claude-agent-sdk/sdk.mjs',
'claude-agent/node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64/claude',
]),
[],
);
assert.deepEqual(
listForbiddenBundledResourceFiles([
'claude-agent/node_modules/unexpected/index.mjs',
'claude-agent/node_modules/@anthropic-ai/unexpected/index.mjs',
'plugins/agc-cocos-editor/node_modules/leftover/index.mjs',
'game-runtime/node/.env.local',
'claude-agent/auth.json',
'claude-agent/target/debug/claude',
'claude-agent/tool.exe',
'claude-agent/tool.dll',
]),
[
'claude-agent/node_modules/unexpected/index.mjs',
'claude-agent/node_modules/@anthropic-ai/unexpected/index.mjs',
'plugins/agc-cocos-editor/node_modules/leftover/index.mjs',
'game-runtime/node/.env.local',
'claude-agent/auth.json',
'claude-agent/target/debug/claude',
'claude-agent/tool.exe',
'claude-agent/tool.dll',
],
);
});
const DEV_IDENTITY = {
productName: '陶泥儿开发版',
identifier: 'world.genarrative.ai-game-creator',
@@ -59,8 +59,17 @@
- **处理**:两侧都补了保留上限。服务端:`production-api-deploy.sh` 新增 `--keep-releases`(默认 `2`),发布成功后保留 `current` 目标与最近 1 个历史 release,只删除同时含 `api-server` 或 `web` 标记的旧目录,清理失败只告警、不改变发布结论。CI 侧:`Genarrative-Api-Deploy` / `Genarrative-Web-Deploy` / `Genarrative-Stdb-Module-Publish` 在各自部署 / 发布步骤成功后只保留最近 2 个 `build/<version>/`,失败时不清理以便诊断和重跑。`npm run check:production-api-deploy` 增加默认值、显式值和非法值三类夹具,`npm run check:production-ops` 增加对应合同。
- **不要踩的坑**:直接按 mtime 排序删除会连带删掉发布根目录下不属于发布产物的目录(例如 `dev-mcp-host-*`),必须用 `api-server`/`web` 标记筛选;脚本里的 `mv -T`、`find -printf` 都是 GNU 语义,`npm run check:production-api-deploy` 需要 `sha256sum` 和 `/usr/bin/cp`,Windows 本地跑不了,只在 Linux CI / Linux 检出上有效(本地最低限度用 `bash -n` + `npm run check:production-ops`)。
- **写 Jenkins 内联 shell 的两个坑**:① Groovy 会处理 `sh '''…'''` / `sh """…"""` 里的反斜杠转义——`\n` 到 shell 手上会变成真实换行(`Jenkinsfile.production-stdb-module-build` 里必须写 `printf "\\r"` 就是同一件事),所以内联片段要么完全不用 `\`,要么写 `\\`;`"""` 是 GString,shell 变量必须写 `\$name`,而 `'''` 不插值、保持 `${name}`。② `set -euo pipefail` 下 `ls build/*/` 在 glob 不匹配时会因 pipefail 把整个部署步骤判失败(实测:`build/` 为空时清理步骤会把一次成功发布判成失败),必须用 `if [ -d build ]` 守卫 + `|| true` 兜底。
- **GString 里的命令替换同样要转义**:`sh """…"""` 内的 shell 命令替换必须写 `\$(...)`。写成裸 `$(...)` 时 Jenkins/Groovy 会在加载 Jenkinsfile 时直接报 `illegal string body character after dollar sign`,构建不会进入任何 stage;`npm run check:production-ops` 现已钉住 Stdb Publish 的暂存清理命令。
- **关联**:`scripts/deploy/production-api-deploy.sh`、`scripts/check-production-api-deploy.mjs`、`scripts/check-production-ops-guardrails.mjs`、`jenkins/Jenkinsfile.production-api-deploy`、`jenkins/Jenkinsfile.production-web-deploy`、`jenkins/Jenkinsfile.production-stdb-module-publish`。
## 2026-09-30 AGC macOS 包内容门禁必须识别随包 Claude Agent SDK
- **现象**:`Genarrative-Agc-MacOS-Build #80` 已成功生成并核对 `陶泥儿开发版.app` 的身份与版本,却在 `check-macos-bundle.mjs` 的 resources 白名单断言处失败,Jenkins 只打印一条无文件名的 `AssertionError`。
- **原因**:新增 Claude Agent SDK sidecar 后,构建会把 SDK 与匹配平台的 Claude runtime 放到 `claude-agent/node_modules/@anthropic-ai/`;macOS 包内容门禁仍按旧口径把除 Node runtime npm 以外的所有 `node_modules` 都判为禁止。
- **处理**:把包内容策略抽成纯函数,只放行 `game-runtime/node/node_modules/npm` 和 `claude-agent/node_modules/@anthropic-ai/claude-agent-sdk[-darwin-*]` 两棵明确子树;同时显式要求 sidecar 入口、SDK 与平台 runtime 存在,并让违规时输出具体相对路径。
- **验证**:`node --test apps/ai-game-creator-shell/scripts/macos-release-identity.test.mjs apps/ai-game-creator-shell/scripts/prepare-macos-codex.test.mjs`;macOS 实包再由 `check-macos-bundle.mjs` 复核。
- **关联**:`apps/ai-game-creator-shell/scripts/macos-bundle-policy.mjs`、`apps/ai-game-creator-shell/scripts/check-macos-bundle.mjs`、`apps/ai-game-creator-shell/scripts/macos-release-identity.test.mjs`、`apps/ai-game-creator-shell/src-tauri/build.rs`、`jenkins/Jenkinsfile.ai-game-creator-shell-macos-build`。
## 2026-09-29 dev 上的 JNLP inbound agent 是历史残留,会在死端口上无限重连刷爆 syslog
- **现象**:dev 的 `/var/log/syslog` 约 250MB/天,内容是 `jenkins-inbound-agent-start[pid]` 反复输出指向 `http://127.0.0.1:18080/tcpSlaveAgentListener/` 的 `Connection refused` 完整栈(2.6 天 61 万行,其中 `genarrative-release-deploy-01` 占 53 万行)。
@@ -151,7 +151,7 @@ pipeline {
# 只保留最近 2 个 build/<version> 暂存:每次发布都用 copyArtifacts 重新落一份,
# 历史暂存不参与发布、只占目标机磁盘;发布失败时不清理,便于诊断和重跑。
if [ -d build ]; then
stale_list="$(ls -1dt build/*/ 2>/dev/null | tail -n +3 || true)"
stale_list="\$(ls -1dt build/*/ 2>/dev/null | tail -n +3 || true)"
while IFS= read -r stale_staging; do
[ -n "\$stale_staging" ] || continue
echo "[staging-cleanup] 清理历史构建暂存: \$stale_staging"
@@ -458,6 +458,13 @@ const checks = [
reason:
'Stdb Publish Job 必须清理历史 build/<version> 暂存,避免目标机被 copyArtifacts 暂存撑满。',
},
{
file: 'jenkins/Jenkinsfile.production-stdb-module-publish',
includes:
'stale_list="\\$(ls -1dt build/*/ 2>/dev/null | tail -n +3 || true)"',
reason:
'Stdb Publish 的 GString 内联 shell 必须把命令替换的 $ 写成 \\$;否则 Jenkinsfile 会在进入 stage 前因 Groovy 编译失败。',
},
{
file: 'jenkins/Jenkinsfile.production-full-build-and-deploy',
includes: