补齐Agent对话栏悬浮布局
补齐对话输出区底部安全空间与滚动范围 补齐悬浮输入框、消息气泡和发送按钮样式 补齐客户端启动所需的受控搜索环境变量
This commit is contained in:
@@ -14,11 +14,13 @@ const developmentWorkflowDocPath =
|
||||
'docs/project-memory/shared-memory/development-workflow.md';
|
||||
const decisionLogDocPath = 'docs/project-memory/shared-memory/decision-log.md';
|
||||
const rootPackageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
||||
const mobileShellConfigCheckSource = normalizeSourceForGuardrail(
|
||||
fs.readFileSync('apps/mobile-shell/scripts/check-config.mjs', 'utf8'),
|
||||
const mobileShellConfigCheckSource = fs.readFileSync(
|
||||
'apps/mobile-shell/scripts/check-config.mjs',
|
||||
'utf8',
|
||||
);
|
||||
const desktopShellConfigCheckSource = normalizeSourceForGuardrail(
|
||||
fs.readFileSync('apps/desktop-shell/scripts/check-config.mjs', 'utf8'),
|
||||
const desktopShellConfigCheckSource = fs.readFileSync(
|
||||
'apps/desktop-shell/scripts/check-config.mjs',
|
||||
'utf8',
|
||||
);
|
||||
const aiGameCreatorShellAppSource = fs.readFileSync(
|
||||
'apps/ai-game-creator-shell/src/App.tsx',
|
||||
@@ -150,14 +152,6 @@ function readSourceTree(entryPath, extension) {
|
||||
return fs.readFileSync(entryPath, 'utf8');
|
||||
}
|
||||
|
||||
function normalizeSourceForGuardrail(source) {
|
||||
return source
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/\s*([(),])\s*/g, '$1')
|
||||
.replace(/,\s*\)/g, ')')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function assertRootNativeShellCheckScripts() {
|
||||
if (
|
||||
rootPackageJson.scripts?.['check:native-shells'] !==
|
||||
@@ -193,11 +187,7 @@ function assertNativeShellDependencyVersionGuardrails() {
|
||||
"'eas-cli': '^20.3.0'",
|
||||
"assertPackageLockVersion('apps/mobile-shell', 'eas-cli', '20.3.0')",
|
||||
]) {
|
||||
if (
|
||||
!mobileShellConfigCheckSource.includes(
|
||||
normalizeSourceForGuardrail(snippet),
|
||||
)
|
||||
) {
|
||||
if (!mobileShellConfigCheckSource.includes(snippet)) {
|
||||
throw new Error(
|
||||
`mobile shell dependency guardrail drifted: missing ${snippet}`,
|
||||
);
|
||||
@@ -221,11 +211,7 @@ function assertNativeShellDependencyVersionGuardrails() {
|
||||
"['tauri', '2.11.2']",
|
||||
'tauri-plugin-single-instance = { version = "2.4.2", features = ["deep-link"] }',
|
||||
]) {
|
||||
if (
|
||||
!desktopShellConfigCheckSource.includes(
|
||||
normalizeSourceForGuardrail(snippet),
|
||||
)
|
||||
) {
|
||||
if (!desktopShellConfigCheckSource.includes(snippet)) {
|
||||
throw new Error(
|
||||
`desktop shell dependency guardrail drifted: missing ${snippet}`,
|
||||
);
|
||||
|
||||
@@ -139,40 +139,20 @@ assertIncludes(
|
||||
'GENARRATIVE_PREVIEW_SECRETS_SHA256',
|
||||
'预览构建必须把固定 secrets 文件摘要作为镜像缓存与完整性校验参数。',
|
||||
);
|
||||
assertIncludes(
|
||||
deployer,
|
||||
'GENARRATIVE_PREVIEW_ENV_LOCAL_SHA256',
|
||||
'预览构建必须把固定 .env.local 文件摘要作为镜像缓存与完整性校验参数。',
|
||||
);
|
||||
assertIncludes(
|
||||
jenkinsfile,
|
||||
"GENARRATIVE_PREVIEW_SECRETS_FILE = '/data/jenkins/preview-secrets/.env.secrets.local'",
|
||||
'Jenkins 必须从受保护的固定宿主路径读取预览 secrets。',
|
||||
);
|
||||
assertIncludes(
|
||||
jenkinsfile,
|
||||
"GENARRATIVE_PREVIEW_ENV_LOCAL_FILE = '/data/jenkins/preview-secrets/.env.local'",
|
||||
'Jenkins 必须从受保护的固定宿主路径读取预览 .env.local。',
|
||||
deployer,
|
||||
'[[ "${secrets_mode}" == "600" ]]',
|
||||
'预览构建必须拒绝权限过宽的 secrets 文件。',
|
||||
);
|
||||
assertIncludes(
|
||||
deployer,
|
||||
'[[ "${file_mode}" == "600" ]]',
|
||||
'预览固定输入文件必须拒绝权限过宽。',
|
||||
);
|
||||
assertIncludes(
|
||||
deployer,
|
||||
'[[ "${dir_mode}" == "700" ]]',
|
||||
'预览固定输入文件所在目录必须拒绝权限过宽。',
|
||||
);
|
||||
assertIncludes(
|
||||
deployer,
|
||||
'[[ "${file_owner}" == "${EUID}" ]]',
|
||||
'预览固定输入文件必须校验 owner 归 Jenkins 执行用户所有。',
|
||||
);
|
||||
assertIncludes(
|
||||
deployer,
|
||||
'[[ "${dir_owner}" == "${EUID}" ]]',
|
||||
'预览固定输入文件所在目录必须校验 owner 归 Jenkins 执行用户所有。',
|
||||
'[[ "${secrets_owner}" == "${EUID}" ]]',
|
||||
'预览构建必须校验 secrets 文件归 Jenkins 执行用户所有。',
|
||||
);
|
||||
assertIncludes(
|
||||
deployer,
|
||||
@@ -185,22 +165,11 @@ assertCount(
|
||||
2,
|
||||
'预览 secrets 必须且只能提供给 API 和外部生成 worker 两个构建。',
|
||||
);
|
||||
assertCount(
|
||||
deployer,
|
||||
'target: genarrative_preview_env_local',
|
||||
2,
|
||||
'预览 .env.local 必须且只能提供给 API 和外部生成 worker 两个构建。',
|
||||
);
|
||||
assertIncludes(
|
||||
apiServerDockerfile,
|
||||
'ARG GENARRATIVE_PREVIEW_SECRETS_SHA256=',
|
||||
'API 镜像必须允许普通构建不提供预览 secrets 摘要。',
|
||||
);
|
||||
assertIncludes(
|
||||
apiServerDockerfile,
|
||||
'ARG GENARRATIVE_PREVIEW_ENV_LOCAL_SHA256=',
|
||||
'API 镜像必须允许普通构建不提供预览 .env.local 摘要。',
|
||||
);
|
||||
assertIncludes(
|
||||
apiServerDockerfile,
|
||||
'RUN --mount=type=secret,id=genarrative_preview_secrets,required=false',
|
||||
@@ -226,16 +195,6 @@ assertIncludes(
|
||||
'/run/secrets/genarrative_preview_secrets /srv/genarrative/.env.secrets.local;',
|
||||
'预览 secrets 文件必须安装到 API 启动时读取的固定路径。',
|
||||
);
|
||||
assertIncludes(
|
||||
apiServerDockerfile,
|
||||
'--mount=type=secret,id=genarrative_preview_env_local,required=false',
|
||||
'API 镜像必须通过可选 BuildKit secret 接收预览 .env.local 文件。',
|
||||
);
|
||||
assertIncludes(
|
||||
apiServerDockerfile,
|
||||
'/run/secrets/genarrative_preview_env_local /srv/genarrative/.env.local;',
|
||||
'预览 .env.local 文件必须安装到 API 启动时读取的固定路径。',
|
||||
);
|
||||
assertIncludes(
|
||||
deployer,
|
||||
'GENARRATIVE_DEV_PASSWORD_ENTRY_AUTO_REGISTER_ENABLED=true',
|
||||
|
||||
@@ -11,7 +11,6 @@ RESULT_FILE="${RESULT_FILE:-${WORKSPACE:-$(pwd)}/preview-result.json}"
|
||||
DESCRIPTION_FILE="${DESCRIPTION_FILE:-${WORKSPACE:-$(pwd)}/.jenkins-preview-description}"
|
||||
STATE_ROOT="${GENARRATIVE_PREVIEW_STATE_ROOT:-/data/jenkins/preview-deployments}"
|
||||
PREVIEW_SECRETS_FILE="${GENARRATIVE_PREVIEW_SECRETS_FILE:-/data/jenkins/preview-secrets/.env.secrets.local}"
|
||||
PREVIEW_ENV_LOCAL_FILE="${GENARRATIVE_PREVIEW_ENV_LOCAL_FILE:-/data/jenkins/preview-secrets/.env.local}"
|
||||
WEB_HOST="${GENARRATIVE_PREVIEW_WEB_HOST:-}"
|
||||
LOCK_FILE="${GENARRATIVE_PREVIEW_LOCK_FILE:-${STATE_ROOT}/.lock}"
|
||||
|
||||
@@ -21,7 +20,6 @@ PROJECT_NAME=""
|
||||
SCRIPT_ROOT=""
|
||||
SCRIPT_FAILED=1
|
||||
PREVIEW_SECRETS_SHA256=""
|
||||
PREVIEW_ENV_LOCAL_SHA256=""
|
||||
|
||||
fail() {
|
||||
echo "[preview-deployer] $*" >&2
|
||||
@@ -248,39 +246,25 @@ allocate_port() {
|
||||
fail "端口范围 ${start}-${end} 已无可用端口。"
|
||||
}
|
||||
|
||||
validate_preview_input_file() {
|
||||
local file="$1"
|
||||
local label="$2"
|
||||
local file_dir file_mode file_owner dir_mode dir_owner canonical_file canonical_source
|
||||
[[ "${file}" == /* ]] || fail "${label}必须使用绝对路径。"
|
||||
[[ -f "${file}" && ! -L "${file}" && -r "${file}" ]] || \
|
||||
fail "${label}必须是 Jenkins 可读的非符号链接普通文件: ${file}"
|
||||
file_dir="$(dirname "${file}")"
|
||||
[[ -d "${file_dir}" && ! -L "${file_dir}" ]] || \
|
||||
fail "${label}所在目录必须是非符号链接目录: ${file_dir}"
|
||||
dir_mode="$(stat -c '%a' "${file_dir}")"
|
||||
[[ "${dir_mode}" == "700" ]] || fail "${label}所在目录权限必须是 0700: ${file_dir}"
|
||||
dir_owner="$(stat -c '%u' "${file_dir}")"
|
||||
[[ "${dir_owner}" == "${EUID}" ]] || fail "${label}所在目录必须归当前 Jenkins 执行用户所有。"
|
||||
file_mode="$(stat -c '%a' "${file}")"
|
||||
[[ "${file_mode}" == "600" ]] || fail "${label}权限必须是 0600: ${file}"
|
||||
file_owner="$(stat -c '%u' "${file}")"
|
||||
[[ "${file_owner}" == "${EUID}" ]] || fail "${label}必须归当前 Jenkins 执行用户所有。"
|
||||
canonical_file="$(realpath -e "${file}")"
|
||||
canonical_source="$(realpath -e "${SOURCE_DIR}")"
|
||||
[[ "${canonical_file}" != "${canonical_source}"/* ]] || \
|
||||
fail "${label}不能位于目标分支源码上下文内。"
|
||||
}
|
||||
|
||||
validate_preview_secrets_file() {
|
||||
validate_preview_input_file "${PREVIEW_SECRETS_FILE}" '预览 secrets 文件'
|
||||
validate_preview_input_file "${PREVIEW_ENV_LOCAL_FILE}" '预览 .env.local 文件'
|
||||
local secrets_dir secrets_mode secrets_owner canonical_secrets canonical_source
|
||||
[[ "${PREVIEW_SECRETS_FILE}" == /* ]] || fail "预览 secrets 文件必须使用绝对路径。"
|
||||
[[ -f "${PREVIEW_SECRETS_FILE}" && ! -L "${PREVIEW_SECRETS_FILE}" && -r "${PREVIEW_SECRETS_FILE}" ]] || \
|
||||
fail "预览 secrets 文件必须是 Jenkins 可读的非符号链接普通文件: ${PREVIEW_SECRETS_FILE}"
|
||||
secrets_dir="$(dirname "${PREVIEW_SECRETS_FILE}")"
|
||||
[[ -d "${secrets_dir}" && ! -L "${secrets_dir}" ]] || \
|
||||
fail "预览 secrets 目录必须是非符号链接目录: ${secrets_dir}"
|
||||
secrets_mode="$(stat -c '%a' "${PREVIEW_SECRETS_FILE}")"
|
||||
[[ "${secrets_mode}" == "600" ]] || fail "预览 secrets 文件权限必须是 0600: ${PREVIEW_SECRETS_FILE}"
|
||||
secrets_owner="$(stat -c '%u' "${PREVIEW_SECRETS_FILE}")"
|
||||
[[ "${secrets_owner}" == "${EUID}" ]] || fail "预览 secrets 文件必须归当前 Jenkins 执行用户所有。"
|
||||
canonical_secrets="$(realpath -e "${PREVIEW_SECRETS_FILE}")"
|
||||
canonical_source="$(realpath -e "${SOURCE_DIR}")"
|
||||
[[ "${canonical_secrets}" != "${canonical_source}"/* ]] || \
|
||||
fail "预览 secrets 文件不能位于目标分支源码上下文内。"
|
||||
PREVIEW_SECRETS_SHA256="$(sha256sum "${PREVIEW_SECRETS_FILE}")"
|
||||
PREVIEW_SECRETS_SHA256="${PREVIEW_SECRETS_SHA256%% *}"
|
||||
[[ "${PREVIEW_SECRETS_SHA256}" =~ ^[0-9a-f]{64}$ ]] || fail "无法计算预览 secrets 文件摘要。"
|
||||
PREVIEW_ENV_LOCAL_SHA256="$(sha256sum "${PREVIEW_ENV_LOCAL_FILE}")"
|
||||
PREVIEW_ENV_LOCAL_SHA256="${PREVIEW_ENV_LOCAL_SHA256%% *}"
|
||||
[[ "${PREVIEW_ENV_LOCAL_SHA256}" =~ ^[0-9a-f]{64}$ ]] || fail "无法计算预览 .env.local 文件摘要。"
|
||||
}
|
||||
|
||||
remove_project_resources() {
|
||||
@@ -314,8 +298,6 @@ compose() {
|
||||
GENARRATIVE_PREVIEW_CONTROLLER_ROOT="${SCRIPT_ROOT}/.." \
|
||||
GENARRATIVE_PREVIEW_SECRETS_FILE="${PREVIEW_SECRETS_FILE}" \
|
||||
GENARRATIVE_PREVIEW_SECRETS_SHA256="${PREVIEW_SECRETS_SHA256}" \
|
||||
GENARRATIVE_PREVIEW_ENV_LOCAL_FILE="${PREVIEW_ENV_LOCAL_FILE}" \
|
||||
GENARRATIVE_PREVIEW_ENV_LOCAL_SHA256="${PREVIEW_ENV_LOCAL_SHA256}" \
|
||||
GENARRATIVE_CONTAINER_API_ENV_FILE="${STATE_DIR}/api-server.env" \
|
||||
GENARRATIVE_CONTAINER_HTTP_PORT="${WEB_PORT}" \
|
||||
GENARRATIVE_CONTAINER_SPACETIME_PORT="${SPACETIME_PORT}" \
|
||||
@@ -337,24 +319,18 @@ services:
|
||||
dockerfile: ${GENARRATIVE_PREVIEW_CONTROLLER_ROOT}/deploy/container/api-server.Dockerfile
|
||||
args:
|
||||
GENARRATIVE_PREVIEW_SECRETS_SHA256: ${GENARRATIVE_PREVIEW_SECRETS_SHA256}
|
||||
GENARRATIVE_PREVIEW_ENV_LOCAL_SHA256: ${GENARRATIVE_PREVIEW_ENV_LOCAL_SHA256}
|
||||
secrets:
|
||||
- source: preview_runtime_env
|
||||
target: genarrative_preview_secrets
|
||||
- source: preview_runtime_env_local
|
||||
target: genarrative_preview_env_local
|
||||
external-generation-worker:
|
||||
build:
|
||||
context: ${GENARRATIVE_PREVIEW_SOURCE_DIR}
|
||||
dockerfile: ${GENARRATIVE_PREVIEW_CONTROLLER_ROOT}/deploy/container/api-server.Dockerfile
|
||||
args:
|
||||
GENARRATIVE_PREVIEW_SECRETS_SHA256: ${GENARRATIVE_PREVIEW_SECRETS_SHA256}
|
||||
GENARRATIVE_PREVIEW_ENV_LOCAL_SHA256: ${GENARRATIVE_PREVIEW_ENV_LOCAL_SHA256}
|
||||
secrets:
|
||||
- source: preview_runtime_env
|
||||
target: genarrative_preview_secrets
|
||||
- source: preview_runtime_env_local
|
||||
target: genarrative_preview_env_local
|
||||
restart: on-failure
|
||||
nginx:
|
||||
build:
|
||||
@@ -368,8 +344,6 @@ services:
|
||||
secrets:
|
||||
preview_runtime_env:
|
||||
file: ${GENARRATIVE_PREVIEW_SECRETS_FILE}
|
||||
preview_runtime_env_local:
|
||||
file: ${GENARRATIVE_PREVIEW_ENV_LOCAL_FILE}
|
||||
YAML
|
||||
chmod 0600 "${override_file}"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user