feat: 增加NAS Codex环境引导工具

This commit is contained in:
sunlei 2026-06-25 02:20:47 +08:00
parent 30222fd85e
commit d53b17f6f1
3 changed files with 246 additions and 27 deletions

View File

@ -16,7 +16,7 @@
- 生成接口测试计划:接口改动后输出真实调用命令和统一返回结构断言。 - 生成接口测试计划:接口改动后输出真实调用命令和统一返回结构断言。
- 生成业务链路测试计划:固化 Admin 登录、博客 CRUD、QQBot 扫码/自动回复、更新登录 SSE、FFLogs 命令、系统日志可视化、Web/Playground 回跳。 - 生成业务链路测试计划:固化 Admin 登录、博客 CRUD、QQBot 扫码/自动回复、更新登录 SSE、FFLogs 命令、系统日志可视化、Web/Playground 回跳。
- 静态检查 NapCat 设备身份护栏:确认 API 仍保留 QQNT 可见 hostname、实体 OUI 风格 MAC、QQNT `machine-info` 写入、持久化 runtime dir 和 `DB_TIMEZONE` 默认值。 - 静态检查 NapCat 设备身份护栏:确认 API 仍保留 QQNT 可见 hostname、实体 OUI 风格 MAC、QQNT `machine-info` 写入、持久化 runtime dir 和 `DB_TIMEZONE` 默认值。
- 生成 NapCatQQ 上游同步与 runtime 发布自动化入口MCP/CLI 默认 dry-run覆盖 upstream audit、sync candidate review、runtime readiness、remote dev handoff 和 NAS Codex bootstrap planupstream audit 在显式 `execute=true``useCodex=true` 时可运行 Codex 外,其余 Task 5 入口只返回 prompt/context/只读命令骨架 - 生成 NapCatQQ 上游同步与 runtime 发布自动化入口MCP/CLI 默认 dry-run覆盖 upstream audit、sync candidate review、runtime readiness、remote dev handoff 和 NAS Codex bootstrap planupstream audit 在显式 `execute=true``useCodex=true` 时可运行 CodexNAS Codex bootstrap 在显式 `execute=true` 时只返回目录准备和 CLI 包检查/安装计划,不复制 secrets、不写 systemd、不触发发布
- 生成卡点固化记录:把超时、卡进程、远程命令误写、重复失败整理成“问题点 / 稳定解法 / 后续入口 / 验证证据”,避免原样重试。 - 生成卡点固化记录:把超时、卡进程、远程命令误写、重复失败整理成“问题点 / 稳定解法 / 后续入口 / 验证证据”,避免原样重试。
- 生成改动文档同步计划:按变更文件自动提示需要同步的 README、API、AGENTS、docs、Obsidian、skill 和 ktWorkflow 入口。 - 生成改动文档同步计划:按变更文件自动提示需要同步的 README、API、AGENTS、docs、Obsidian、skill 和 ktWorkflow 入口。
- 生成多仓库提交/推送计划:按仓库分组、建议提交信息、列出提交和推送前检查。 - 生成多仓库提交/推送计划:按仓库分组、建议提交信息、列出提交和推送前检查。
@ -105,7 +105,7 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login
| `kt_napcat_sync_candidate_review` | 生成 NapCat sync candidate review 的 prompt/context/只读命令骨架Task 5 不执行 Codex 或 Git 写操作 | | `kt_napcat_sync_candidate_review` | 生成 NapCat sync candidate review 的 prompt/context/只读命令骨架Task 5 不执行 Codex 或 Git 写操作 |
| `kt_napcat_runtime_release_readiness` | 生成 NapCat runtime release readiness 的 prompt/context/只读检查骨架,不自动构建或发布 | | `kt_napcat_runtime_release_readiness` | 生成 NapCat runtime release readiness 的 prompt/context/只读检查骨架,不自动构建或发布 |
| `kt_napcat_remote_dev_handoff` | 生成 NapCat 自动化远程开发 handoff 的 prompt/context 骨架,不创建线程或同步仓库 | | `kt_napcat_remote_dev_handoff` | 生成 NapCat 自动化远程开发 handoff 的 prompt/context 骨架,不创建线程或同步仓库 |
| `kt_nas_codex_bootstrap_plan` | 生成可信 NAS Codex bootstrap dry-run planTask 5 仅输出只读检查和占位计划 | | `kt_nas_codex_bootstrap_plan` | 生成可信 NAS Codex bootstrap dry-run/execute command planexecute 只规划目录准备和 CLI 包检查/安装,不复制 secrets、不写 systemd |
| `kt_napcat_device_profile_check` | 静态检查 API NapCat 设备身份护栏,覆盖 hostname、MAC、`machine-info`、runtime dir 和 DB timezone | | `kt_napcat_device_profile_check` | 静态检查 API NapCat 设备身份护栏,覆盖 hostname、MAC、`machine-info`、runtime dir 和 DB timezone |
| `kt_blocker_resolution` | 生成或写入卡点固化记录,提醒停止原样重试 | | `kt_blocker_resolution` | 生成或写入卡点固化记录,提醒停止原样重试 |
| `kt_remote_health_check` | 生成或执行远程只读健康检查命令 | | `kt_remote_health_check` | 生成或执行远程只读健康检查命令 |
@ -128,7 +128,7 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login
| `pnpm run napcat-sync-candidate-review` | 输出 sync candidate review 的 prompt/context/只读命令骨架,不执行 Codex 或 Git 写操作。 | | `pnpm run napcat-sync-candidate-review` | 输出 sync candidate review 的 prompt/context/只读命令骨架,不执行 Codex 或 Git 写操作。 |
| `pnpm run napcat-runtime-release-readiness` | 输出 runtime release readiness 的 prompt/context/只读检查骨架,不构建镜像、不调用 kubectl、不发布。 | | `pnpm run napcat-runtime-release-readiness` | 输出 runtime release readiness 的 prompt/context/只读检查骨架,不构建镜像、不调用 kubectl、不发布。 |
| `pnpm run napcat-remote-dev-handoff` | 输出远程开发 handoff 的 prompt/context 骨架,不创建线程、不同步仓库、不执行 Codex。 | | `pnpm run napcat-remote-dev-handoff` | 输出远程开发 handoff 的 prompt/context 骨架,不创建线程、不同步仓库、不执行 Codex。 |
| `pnpm run nas-codex-bootstrap` | 输出可信 NAS Codex bootstrap dry-run plan真实安装、同步和 systemd 接入留给后续实现。 | | `pnpm run nas-codex-bootstrap` | 输出可信 NAS Codex bootstrap dry-run plan`--execute` 只生成目录准备和 CLI 包检查/安装命令计划,不真实执行 SSH、不复制 secrets、不写 systemd。 |
| `pnpm run napcat-device-profile-check` | 静态检查 API NapCat 设备身份护栏,防止回退到未知设备风险配置。 | | `pnpm run napcat-device-profile-check` | 静态检查 API NapCat 设备身份护栏,防止回退到未知设备风险配置。 |
| `pnpm run deploy-observation` | 默认 dry-run 输出只读 NAS 观测命令;传 `--execute` 时执行 Jenkins/K8s/health/smoke 观测并写入 `.kt-workspace/test-artifacts/deploy-observation`。 | | `pnpm run deploy-observation` | 默认 dry-run 输出只读 NAS 观测命令;传 `--execute` 时执行 Jenkins/K8s/health/smoke 观测并写入 `.kt-workspace/test-artifacts/deploy-observation`。 |
| `pnpm run obsidian-context` | 输出 Obsidian 索引上下文;可传 `--module Admin`、`--query QQBot`、`--max-documents 10`。 | | `pnpm run obsidian-context` | 输出 Obsidian 索引上下文;可传 `--module Admin`、`--query QQBot`、`--max-documents 10`。 |

View File

@ -209,6 +209,81 @@ export async function runSelfTest(): Promise<void> {
) { ) {
throw new Error("NAS Codex bootstrap default alignment self-check failed"); throw new Error("NAS Codex bootstrap default alignment self-check failed");
} }
const bootstrap = buildNasCodexBootstrapPlan({ execute: false });
const bootstrapCommandText = bootstrap.commands
.map((item) => item.command)
.join("\n");
if (!bootstrapCommandText.includes("/vol1/docker/kt-codex/home/.codex")) {
throw new Error("NAS Codex bootstrap CODEX_HOME self-check failed");
}
if (bootstrapCommandText.includes("C:\\Users")) {
throw new Error("NAS Codex bootstrap Windows path guard self-check failed");
}
for (const expected of [
"node --version",
"corepack --version",
"pnpm --version",
"git --version",
"docker --version",
"codex --version",
"test -d /vol1/docker/kt-codex/workspace/KT",
"test -d /vol1/docker/kt-codex/home/.codex",
]) {
if (!bootstrapCommandText.includes(expected)) {
throw new Error(`NAS Codex bootstrap dry-run command missing: ${expected}`);
}
}
for (const command of bootstrap.commands) {
if (!command.readOnly) {
throw new Error("NAS Codex bootstrap dry-run command must be read-only");
}
}
const executeBootstrap = buildNasCodexBootstrapPlan({ execute: true });
const executeBootstrapCommandText = executeBootstrap.commands
.map((item) => item.command)
.join("\n");
if (
executeBootstrap.execute !== true ||
executeBootstrap.commands.every((command) => command.readOnly)
) {
throw new Error("NAS Codex bootstrap execute plan self-check failed");
}
if (
!executeBootstrapCommandText.includes("install -d -m 700 /vol1/docker/kt-codex/home/.codex") ||
!executeBootstrapCommandText.includes("npm install -g @openai/codex")
) {
throw new Error("NAS Codex bootstrap execute package plan self-check failed");
}
for (const forbidden of [
"git push",
"git merge",
"docker build",
"docker pull",
"kubectl",
"systemctl enable",
"systemctl start",
"cp ~/.codex",
"scp ~/.codex",
"C:\\Users",
]) {
if (executeBootstrapCommandText.includes(forbidden)) {
throw new Error(`NAS Codex bootstrap forbidden command leaked: ${forbidden}`);
}
}
let windowsBootstrapPathRejected = false;
try {
buildNasCodexBootstrapPlan({
codexHome: "C:\\Users\\example\\.codex",
execute: false,
});
} catch (error) {
windowsBootstrapPathRejected = String(error).includes(
"Unsafe NAS Codex bootstrap path",
);
}
if (!windowsBootstrapPathRejected) {
throw new Error("NAS Codex bootstrap unsafe Windows path self-check failed");
}
const napcatCandidateOverride = buildNapcatSyncCandidateReview({ const napcatCandidateOverride = buildNapcatSyncCandidateReview({
candidateBranch: "custom-candidate", candidateBranch: "custom-candidate",
lastAcceptedUpstreamBase: "custom-base", lastAcceptedUpstreamBase: "custom-base",

View File

@ -333,21 +333,54 @@ export function buildNapcatRemoteDevHandoff(
}); });
} }
interface NasCodexBootstrapContext {
codexHome: string;
serviceName: string;
workspaceRoot: string;
}
/** /**
* Builds a non-mutating NAS Codex bootstrap plan placeholder for later automation tasks. * Builds a safe NAS Codex bootstrap command plan without running SSH locally.
* @param input - Trusted NAS workspace/Codex-home hints and optional service name; execution is intentionally not implemented in Task 5. * @param input - Trusted NAS workspace/Codex-home hints, optional service name, and explicit execution intent.
* @returns A dry-run bootstrap checklist and read-only verification commands. * @returns A dry-run or execute-mode command plan; execute mode only emits bounded NAS preparation commands for a trusted operator to run.
*/ */
export function buildNasCodexBootstrapPlan(input: NasCodexBootstrapPlanInput) { export function buildNasCodexBootstrapPlan(input: NasCodexBootstrapPlanInput) {
const artifactRoot = const artifactRoot =
input.artifactRoot?.trim() || input.artifactRoot?.trim() ||
`${napcatAutomationSkeletonArtifactRoot}/nas-codex-bootstrap`; `${napcatAutomationSkeletonArtifactRoot}/nas-codex-bootstrap`;
const context = { const context: NasCodexBootstrapContext = {
codexHome: input.codexHome ?? napcatAutomationDefaults.codexHome, codexHome: resolveNasBootstrapPath(
serviceName: input.serviceName ?? napcatAutomationDefaults.bootstrapServiceName, 'codexHome',
workspaceRoot: input.workspaceRoot ?? napcatAutomationDefaults.workspaceRoot, input.codexHome,
napcatAutomationDefaults.codexHome,
),
serviceName: resolveNasBootstrapServiceName(
input.serviceName ?? napcatAutomationDefaults.bootstrapServiceName,
),
workspaceRoot: resolveNasBootstrapPath(
'workspaceRoot',
input.workspaceRoot,
napcatAutomationDefaults.workspaceRoot,
),
}; };
const requestedExecute = input.execute === true; const requestedExecute = input.execute === true;
const commands = [
{
command: buildNasBootstrapDryRunCommand(context),
description:
'Run bounded read-only NAS checks for Linux tools, KT workspace, and trusted CODEX_HOME.',
readOnly: true,
},
];
if (requestedExecute) {
commands.push({
command: buildNasBootstrapExecuteCommand(context),
description:
'Create trusted NAS Codex directories and install/check CLI packages without touching business repos, secrets, deploys, or systemd.',
readOnly: false,
});
}
return { return {
artifacts: [ artifacts: [
@ -360,32 +393,143 @@ export function buildNasCodexBootstrapPlan(input: NasCodexBootstrapPlanInput) {
type: 'json', type: 'json',
}, },
], ],
commands: [ commands,
{
command: 'ssh nas "command -v codex && codex --version"',
description: 'Read the trusted NAS Codex CLI version without installing or changing files.',
readOnly: true,
},
{
command:
'ssh nas "test -d /vol1/docker/kt-codex/workspace/KT && git -C /vol1/docker/kt-codex/workspace/KT status --short --branch"',
description: 'Read the trusted NAS KT workspace state without syncing or committing.',
readOnly: true,
},
],
context, context,
execute: false, execute: requestedExecute,
notes: [ notes: [
'Task 5 only exposes a dry-run bootstrap plan. Task 6 must implement real NAS bootstrap logic before any mutation is allowed.', 'The builder only returns command plans; it does not open SSH sessions or mutate NAS state by itself.',
'Dry-run mode emits Linux-only version and directory checks and never copies Codex secrets.',
requestedExecute requestedExecute
? 'execute=true was requested, but no bootstrap mutation is performed by this placeholder.' ? 'execute=true adds a bounded preparation plan that may create trusted directories and install/check Node/Corepack/pnpm/Codex packages, but still does not push, merge, deploy, edit business repos, copy secrets, or write systemd.'
: 'execute defaults to false.', : 'execute defaults to false.',
], ],
requestedExecute, requestedExecute,
summary: 'NAS Codex bootstrap dry-run plan generated.', summary: requestedExecute
? 'NAS Codex bootstrap execute-mode command plan generated.'
: 'NAS Codex bootstrap dry-run plan generated.',
}; };
} }
/**
* Resolves and validates a NAS bootstrap Linux path before embedding it in remote shell text.
* @param label - Human-readable input name used in validation errors.
* @param value - Optional caller override for a trusted NAS filesystem path.
* @param fallback - Stabilized default path used when no override is provided.
* @returns A normalized absolute Linux path under `/vol1/docker/kt-codex`.
*/
function resolveNasBootstrapPath(
label: string,
value: string | undefined,
fallback: string,
): string {
const resolved = (value?.trim() || fallback).replace(/\/+$/, '');
if (
/^[a-zA-Z]:[\\/]/.test(resolved) ||
resolved.includes('\\') ||
resolved.includes('~') ||
resolved.includes('$') ||
/[\r\n]/.test(resolved) ||
!/^\/[A-Za-z0-9._/-]+$/.test(resolved) ||
!resolved.startsWith('/vol1/docker/kt-codex/') ||
hasParentPathSegment(resolved)
) {
throw new Error(`Unsafe NAS Codex bootstrap path "${label}"`);
}
return resolved;
}
/**
* Validates the future service label while Task 6 deliberately avoids writing systemd units.
* @param value - Service name from CLI/MCP input or the stabilized default.
* @returns A compact service label safe to carry in JSON context.
*/
function resolveNasBootstrapServiceName(value: string): string {
const resolved = value.trim();
if (!/^[A-Za-z0-9._-]+$/.test(resolved)) {
throw new Error('Unsafe NAS Codex bootstrap serviceName');
}
return resolved;
}
/**
* Builds the bounded PowerShell-to-NAS here-string wrapper used by bootstrap plans.
* @param bodyLines - Linux shell commands that will run on NAS only when a human executes the returned plan.
* @param timeoutSeconds - Remote `timeout` budget that prevents hanging checks or installs.
* @returns A copy-pastable PowerShell command using the KT NAS CRLF-safe SSH pattern.
*/
function buildNasBootstrapSshPlanCommand(
bodyLines: string[],
timeoutSeconds: number,
): string {
return [
"$remoteScript = @'",
'set -euo pipefail',
...bodyLines,
"'@",
`$remoteScript | ssh nas "tr -d '\\015' | timeout ${timeoutSeconds}s bash -s"`,
].join('\n');
}
/**
* Builds the read-only NAS Codex bootstrap dry-run command required by Task 6.
* @param context - Trusted NAS workspace and CODEX_HOME paths.
* @returns A bounded SSH here-string containing Linux-only version and directory checks.
*/
function buildNasBootstrapDryRunCommand(
context: NasCodexBootstrapContext,
): string {
return buildNasBootstrapSshPlanCommand(
[
"printf '%s\\n' '[ktWorkflow] NAS Codex bootstrap dry-run checks'",
'node --version',
'corepack --version',
'pnpm --version',
'git --version',
'docker --version',
'codex --version',
`test -d ${context.workspaceRoot}`,
`test -d ${context.codexHome}`,
`git -C ${context.workspaceRoot} status --short --branch`,
],
120,
);
}
/**
* Builds the explicit execute-mode NAS preparation plan without running it locally.
* @param context - Trusted NAS workspace, Codex home, and service label context.
* @returns A bounded SSH here-string that creates only kt-codex directories and checks/installs CLI packages.
*/
function buildNasBootstrapExecuteCommand(
context: NasCodexBootstrapContext,
): string {
const workspaceParent = path.posix.dirname(context.workspaceRoot);
return buildNasBootstrapSshPlanCommand(
[
"printf '%s\\n' '[ktWorkflow] NAS Codex bootstrap execute plan'",
`install -d -m 755 ${workspaceParent}`,
`install -d -m 700 ${context.codexHome}`,
`install -d -m 755 ${napcatNasCodexArtifactRoot}`,
'node --version',
'corepack --version',
'corepack enable',
'corepack prepare pnpm@10.28.2 --activate',
'pnpm --version',
'git --version',
'docker --version',
'if ! command -v codex >/dev/null 2>&1; then',
' npm install -g @openai/codex',
'fi',
'codex --version',
`test -d ${context.workspaceRoot}`,
`test -d ${context.codexHome}`,
`git -C ${context.workspaceRoot} status --short --branch`,
],
300,
);
}
/** /**
* Quotes a POSIX shell path argument for NAS-side Codex automation commands. * Quotes a POSIX shell path argument for NAS-side Codex automation commands.
* @param value - Absolute or repo-relative filesystem path that will be interpreted by a Linux shell. * @param value - Absolute or repo-relative filesystem path that will be interpreted by a Linux shell.