first commit

This commit is contained in:
douboer
2026-03-21 18:57:10 +08:00
commit c49aa1a5e9
570 changed files with 107167 additions and 0 deletions

View File

@@ -0,0 +1,256 @@
/* global module */
const CODEX_BOOTSTRAP_TOKEN_DIR_MISSING = "__RC_CODEX_DIR_MISSING__";
const CODEX_BOOTSTRAP_TOKEN_CODEX_MISSING = "__RC_CODEX_BIN_MISSING__";
const CODEX_BOOTSTRAP_TOKEN_READY = "__RC_CODEX_READY__";
const AI_RUNTIME_EXIT_OSC_IDENT = 633;
const DEFAULT_AI_PROVIDER = "codex";
const DEFAULT_CODEX_SANDBOX_MODE = "workspace-write";
const DEFAULT_COPILOT_PERMISSION_MODE = "default";
const AI_PROVIDER_VALUES = new Set(["codex", "copilot"]);
const CODEX_SANDBOX_MODE_VALUES = new Set(["read-only", "workspace-write", "danger-full-access"]);
const COPILOT_PERMISSION_MODE_VALUES = new Set(["default", "experimental", "allow-all"]);
const AI_RUNTIME_EXIT_MARKERS = {
codex: `\u001b]${AI_RUNTIME_EXIT_OSC_IDENT};RemoteConn;ai-exit=codex\u0007`,
copilot: `\u001b]${AI_RUNTIME_EXIT_OSC_IDENT};RemoteConn;ai-exit=copilot\u0007`
};
/**
* AI 默认入口只允许 Codex / Copilot 两档:
* 1. 新装默认走 Codex
* 2. 历史空值和脏值统一回退到 Codex。
*/
function normalizeAiProvider(value) {
const normalized = String(value || "")
.trim()
.toLowerCase();
return AI_PROVIDER_VALUES.has(normalized) ? normalized : DEFAULT_AI_PROVIDER;
}
/**
* Codex sandbox 模式只接受三档预设值:
* 1. 旧版本/脏数据回落到项目目录读写;
* 2. 保持与当前弹窗默认选项一致,避免升级后行为突变。
*/
function normalizeCodexSandboxMode(value) {
const normalized = String(value || "").trim();
return CODEX_SANDBOX_MODE_VALUES.has(normalized) ? normalized : DEFAULT_CODEX_SANDBOX_MODE;
}
/**
* Copilot 权限模式同样收口为固定三档:
* 1. 默认;
* 2. experimental
* 3. allow-all。
*/
function normalizeCopilotPermissionMode(value) {
const normalized = String(value || "").trim();
return COPILOT_PERMISSION_MODE_VALUES.has(normalized) ? normalized : DEFAULT_COPILOT_PERMISSION_MODE;
}
/**
* 对 shell 参数做最小安全转义,避免单引号截断。
*/
function shellQuote(value) {
return `'${String(value).replace(/'/g, "'\\''")}'`;
}
/**
* 构造项目目录切换命令:
* 1. `~` 和 `~/...` 需要保留 HOME 展开语义;
* 2. 其他路径统一做单引号转义,避免空格或特殊字符破坏命令。
*/
function buildCdCommand(projectPath) {
const normalized = String(projectPath || "~").trim() || "~";
if (normalized === "~") {
return 'cd "$HOME"';
}
if (normalized.startsWith("~/")) {
const relative = normalized.slice(2);
return relative ? `cd "$HOME"/${shellQuote(relative)}` : 'cd "$HOME"';
}
return `cd ${shellQuote(normalized)}`;
}
/**
* 由全局配置推导 Copilot 启动命令:
* 1. 默认模式直接执行 `copilot`
* 2. 其它模式只追加稳定 flag不在页面层散落命令字符串。
*/
function buildCopilotCommand(mode) {
const normalized = normalizeCopilotPermissionMode(mode);
if (normalized === "experimental") return "copilot --experimental";
if (normalized === "allow-all") return "copilot --allow-all";
return "copilot";
}
/**
* AI 前台态退出标记:
* 1. 使用未知 OSC既能被前端原始流识别也不会污染终端可见内容
* 2. 远端 AI 进程退出回到 shell 时打印一次,用于解除“当前已有 AI 在前台”的保护。
*/
function buildAiRuntimeExitMarker(provider) {
const normalized = normalizeAiProvider(provider);
return AI_RUNTIME_EXIT_MARKERS[normalized] || AI_RUNTIME_EXIT_MARKERS[DEFAULT_AI_PROVIDER];
}
function buildAiRuntimeExitPrintfCommand(provider) {
const normalized = normalizeAiProvider(provider);
return `printf '\\033]${AI_RUNTIME_EXIT_OSC_IDENT};RemoteConn;ai-exit=${normalized}\\a'`;
}
/**
* AI 启动前的“会话可发命令”判定:
* 1. 不能只看页面状态是否显示为 connected因为首段 banner/stdout 可能早于 shell 真正 ready
* 2. 小程序自动建链后立刻点 AI 的场景里,必须等待网关显式 `control.connected`
* 3. 这样可以避免把 Codex/Copilot 启动命令打进尚未就绪的 shell导致后续 bootstrap token 永远等不到。
*/
function isAiSessionReady(status, hasClient, shellReady) {
return String(status || "").trim() === "connected" && !!hasClient && shellReady === true;
}
/**
* 将脚本文本安全嵌入到 `sh -lc "..."` 的双引号参数中。
* 说明:远端默认 shell 可能不是 POSIX shell因此需要显式把 bootstrap
* 转交给 `sh -lc` 解释,避免 csh/tcsh 在重定向语法上报错。
*/
function escapeForDoubleQuotedShellArg(script) {
return String(script || "")
.replace(/\\/g, "\\\\")
.replace(/"/g, '\\"')
.replace(/\$/g, "\\$")
.replace(/`/g, "\\`")
.replace(/!/g, "\\!");
}
/**
* 从原始 stdout/stderr 中消费 AI 退出标记:
* 1. 已完成的 OSC 标记直接剥离,并返回退出的 provider
* 2. 若 chunk 末尾只收到半段标记,则仅缓存那段 `ESC ] ...` 尾巴;
* 3. 不延迟普通文本输出,避免为等标记而卡住终端刷新。
*/
function consumeAiRuntimeExitMarkers(input, previousCarry) {
let working = `${String(previousCarry || "")}${String(input || "")}`;
const exitedProviders = [];
let changed = true;
while (changed) {
changed = false;
for (const provider of Object.keys(AI_RUNTIME_EXIT_MARKERS)) {
const marker = AI_RUNTIME_EXIT_MARKERS[provider];
const index = working.indexOf(marker);
if (index < 0) {
continue;
}
exitedProviders.push(provider);
working = `${working.slice(0, index)}${working.slice(index + marker.length)}`;
changed = true;
}
}
let carry = "";
const lastEscIndex = working.lastIndexOf("\u001b");
if (lastEscIndex >= 0) {
const suffix = working.slice(lastEscIndex);
const mayBeMarkerTail = Object.values(AI_RUNTIME_EXIT_MARKERS).some((marker) =>
marker.startsWith(suffix)
);
if (mayBeMarkerTail) {
carry = suffix;
working = working.slice(0, lastEscIndex);
}
}
return {
text: working,
carry,
exitedProviders
};
}
/**
* 生成小程序端 Codex 启动预检命令。
* 目标与 Web 端保持一致:
* 1. 先验证项目目录是否存在;
* 2. 再验证服务器是否安装 `codex`
* 3. 仅在两项都通过时,进入目标目录并启动 Codex。
*/
function buildCodexBootstrapCommand(projectPath, sandbox) {
const normalizedPath = String(projectPath || "~").trim() || "~";
const cdCommand = buildCdCommand(normalizedPath);
const runCommand = `codex --sandbox ${normalizeCodexSandboxMode(sandbox)}`;
const exitPrintfCommand = buildAiRuntimeExitPrintfCommand("codex");
const bootstrapScript =
`__rc_codex_path_ok=1; __rc_codex_bin_ok=1; ${cdCommand} >/dev/null 2>&1 || __rc_codex_path_ok=0; ` +
`command -v codex >/dev/null 2>&1 || __rc_codex_bin_ok=0; ` +
`[ "$__rc_codex_path_ok" -eq 1 ] || printf '${CODEX_BOOTSTRAP_TOKEN_DIR_MISSING}\\n'; ` +
`[ "$__rc_codex_bin_ok" -eq 1 ] || printf '${CODEX_BOOTSTRAP_TOKEN_CODEX_MISSING}\\n'; ` +
`if [ "$__rc_codex_path_ok" -eq 1 ] && [ "$__rc_codex_bin_ok" -eq 1 ]; then printf '${CODEX_BOOTSTRAP_TOKEN_READY}\\n'; ${runCommand}; __rc_ai_exit_code=$?; ${exitPrintfCommand}; exit "$__rc_ai_exit_code"; fi`;
return {
projectPath: normalizedPath,
command: `sh -lc "${escapeForDoubleQuotedShellArg(bootstrapScript)}"`
};
}
/**
* 生成小程序端 Codex 恢复命令:
* 1. 仅用于“旧 SSH 未续上,但本地记得上一轮前台是 Codex”的场景
* 2. 走 `codex resume --last --sandbox ...`,显式保持上次会话权限;
* 3. 无论恢复成功还是快速失败,都会打印退出标记,便于页面解除前台锁。
*/
function buildCodexResumeCommand(projectPath, sandbox) {
const normalizedPath = String(projectPath || "~").trim() || "~";
const exitPrintfCommand = buildAiRuntimeExitPrintfCommand("codex");
const script =
`${buildCdCommand(normalizedPath)} && codex resume --last --sandbox ${normalizeCodexSandboxMode(sandbox)}; ` +
`__rc_ai_exit_code=$?; ${exitPrintfCommand}; exit "$__rc_ai_exit_code"`;
return {
projectPath: normalizedPath,
command: `sh -lc "${escapeForDoubleQuotedShellArg(script)}"`
};
}
/**
* Copilot 没有 Codex 那套独立的 bootstrap token但仍需要
* 1. 先切到项目目录;
* 2. 退出时打印同源 OSC 标记,方便前端解除互斥保护。
*/
function buildCopilotLaunchCommand(projectPath, mode) {
const normalizedPath = String(projectPath || "~").trim() || "~";
const runCommand = buildCopilotCommand(mode);
const script = `${buildCdCommand(normalizedPath)} && ${runCommand}; __rc_ai_exit_code=$?; ${buildAiRuntimeExitPrintfCommand(
"copilot"
)}; exit "$__rc_ai_exit_code"`;
return {
projectPath: normalizedPath,
command: `sh -lc "${escapeForDoubleQuotedShellArg(script)}"`
};
}
module.exports = {
CODEX_BOOTSTRAP_TOKEN_DIR_MISSING,
CODEX_BOOTSTRAP_TOKEN_CODEX_MISSING,
CODEX_BOOTSTRAP_TOKEN_READY,
AI_RUNTIME_EXIT_OSC_IDENT,
DEFAULT_AI_PROVIDER,
DEFAULT_CODEX_SANDBOX_MODE,
DEFAULT_COPILOT_PERMISSION_MODE,
shellQuote,
normalizeAiProvider,
normalizeCodexSandboxMode,
normalizeCopilotPermissionMode,
buildCdCommand,
buildAiRuntimeExitMarker,
consumeAiRuntimeExitMarkers,
isAiSessionReady,
buildCopilotCommand,
buildCopilotLaunchCommand,
buildCodexResumeCommand,
escapeForDoubleQuotedShellArg,
buildCodexBootstrapCommand
};