Merge remote-tracking branch 'origin/master' into codex/clear-retired-tables-phase2
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled

This commit is contained in:
2026-09-22 20:22:09 +08:00
7 changed files with 80 additions and 8 deletions
@@ -203,12 +203,51 @@ pub(in crate::agent) fn game_creator_codex_cli_version_at(
Ok(version.to_string())
}
/// 开发态允许从宿主 PATH 里找到 Codex,但宿主必须持有可锚定的绝对文件:
/// 裸命令名按 PATH 解析成真实路径,否则 `bind_codex_executor` 的 canonicalize 会按 CWD 解析并失败。
/// 发行构建不走这段,候选顺序、校验与返回值都与原先一致(打包环境用内置侧车/npm 绝对路径)。
#[cfg(debug_assertions)]
fn anchor_codex_cli_executable_candidate(
candidate: &Path,
path: Option<&std::ffi::OsStr>,
) -> Option<PathBuf> {
let is_bare_command_name = candidate
.parent()
.is_some_and(|parent| parent.as_os_str().is_empty());
if !is_bare_command_name {
return candidate.is_file().then(|| candidate.to_path_buf());
}
let name = candidate.as_os_str();
for directory in path.into_iter().flat_map(std::env::split_paths) {
if directory.as_os_str().is_empty() {
continue;
}
let target = directory.join(name);
if target.is_file() {
// 第一个实际命中的项就是 OS 会执行的项;锚定失败时不再从 PATH 里换另一个。
return target.canonicalize().ok();
}
}
None
}
pub(crate) fn game_creator_codex_cli_executable_path() -> Result<PathBuf, String> {
let mut last_error = None;
let mut seen = std::collections::HashSet::new();
let bundled =
game_creator_bundled_codex_cli_path(game_creator_bundled_resource_dir().as_deref());
for candidate in game_creator_codex_cli_executable_candidates() {
#[cfg(debug_assertions)]
let candidate = match anchor_codex_cli_executable_candidate(
&candidate,
std::env::var_os("PATH").as_deref(),
) {
Some(candidate) => candidate,
None => {
last_error = Some("候选执行器不是可锚定的文件".to_string());
continue;
}
};
let identity = candidate.to_string_lossy().to_ascii_lowercase();
if !seen.insert(identity) {
continue;
@@ -6,6 +6,7 @@ import {
Component,
createContext,
isValidElement,
memo,
useContext,
} from 'react';
import ReactMarkdown, { type Components } from 'react-markdown';
@@ -302,7 +303,15 @@ const streamingMarkdownComponents: Components = {
p: StreamingMarkdownParagraph,
};
export function ChatMarkdownMessage({
/**
* 解析器的输入只有 `text` / `role` / `streaming` / `preserveBlankLines` 这几个标量,所以
* 内容没变的旧消息在父级重渲染时可以直接跳过:Markdown 解析与语法高亮是这个组件里最贵的
* 两件事(`react-markdown` 每次渲染都会重建 `unified()` 处理器并重跑全部插件),而旧消息
* 的文本永远不会再改。
*/
export const ChatMarkdownMessage = memo(ChatMarkdownMessageImpl);
function ChatMarkdownMessageImpl({
text,
role,
streaming = false,
@@ -317,8 +326,13 @@ export function ChatMarkdownMessage({
<ReactMarkdown
skipHtml
remarkPlugins={[remarkGfm]}
// 流式中不高亮:高亮要跑一遍完整 AST + highlight.js,而流式增量会让同一段代码
// 每来一个 chunk 就重新高亮一次(文本还在长,结果立刻作废)。文本定稿时
// `streaming` 变回 false,这一笔高亮自然补上。
rehypePlugins={
text.length <= MAX_HIGHLIGHT_CHARACTERS ? [rehypeHighlight] : []
streaming || text.length > MAX_HIGHLIGHT_CHARACTERS
? []
: [rehypeHighlight]
}
components={
streaming ? streamingMarkdownComponents : markdownComponents
@@ -1,5 +1,5 @@
import { ChevronDown, Lightbulb } from 'lucide-react';
import { useState } from 'react';
import { memo, useMemo, useState } from 'react';
import { AgentMessageContent } from '../../../../../../../../packages/shared/src/components/AgentMessageContent';
import { AgentProcessSummary } from '../../../../../../../../packages/shared/src/components/AgentProcessSummary';
@@ -12,7 +12,9 @@ import { agentProcessPreview } from '../../../../../features/project-workspace/a
* 折叠态是单行纯文本预览(Markdown 只取可见文字,符号不进预览);展开态复用助手正文的
* 安全 Markdown 链路。这里只有表现与展开态,思考内容本身由两条产品路径各自的事实源提供。
*/
export function AgentReasoning({
export const AgentReasoning = memo(AgentReasoningImpl);
function AgentReasoningImpl({
text,
label = '思考过程',
testId,
@@ -22,7 +24,12 @@ export function AgentReasoning({
testId?: string;
}) {
const [expanded, setExpanded] = useState(false);
const preview = agentProcessPreview(text);
/*
折叠态那一行预览是**完整 Markdown AST 解析**`remark-parse` + `unified`),和展开态
`react-markdown` 那次解析是两笔开销;上面那层 `memo` 让内容没变的思考块整个跳过,
这里的 `useMemo` 再兜一层,避免同一文本在真正重渲染时被重算。
*/
const preview = useMemo(() => agentProcessPreview(text), [text]);
return (
<AgentMessageContent
as="details"
+1
View File
@@ -89,6 +89,7 @@ Cargo registry 的压缩包与索引、npm `_cacache` 使用稳定命名、`shar
```bash
sudo apt-get install docker-buildx
# Buildx 0.30.1 的 inspect 不支持 --format;脚本读取普通输出的 Driver 字段。
# 替换为运维已验证的完整 Image ID;只提取 registry/cache、registry/index 和 npm/_cacache。
sudo bash scripts/gitea-ci-job-image.sh seed-downloads 'sha256:<可信镜像的64位摘要>'
```
@@ -96,6 +96,8 @@ SpacetimeDB 任务统一先读取 `.codex/skills/genarrative-spacetimedb/SKILL.m
## Gitea CI 依赖闭合
Buildx 0.30.1 的 `inspect` 不支持 `--format`,builder 驱动校验读取普通输出的 `Driver:` 字段。相关命令须在宿主真实插件上验证;测试替身应拒绝不支持的参数,避免把模拟命令成功误当兼容性证据。
Gitea 基础镜像通过专用 `genarrative-ci-images` Buildx builder 持久复用 Cargo/npm 下载缓存;稳定 cache mount 与 commit、lock 哈希无关,以 `sharing=locked` 隔离并发写入,仅供可信宿主构建、不开放给 PR。最终镜像显式物化当前依赖下载快照,仍不包含 node_modules/target 或上一版 sccache 层。首次可用 `seed-downloads` 从可信完整 Image ID 提取包缓存,操作账号须与维护服务一致;部署要求及 builder GC 空间目标见 `deploy/container/README.md`。构建上下文必须覆盖 AGC vendor 与编辑器 bridge 的全部本地 path manifest,普通源码变化不应使依赖层失效。维护 journal 提供阶段耗时和失败 build.log 定位。
Gitea Rust 缓存自动维护由宿主 `genarrative-ci-cache.timer` 收集同一 master push run 六个 Rust job 的原生 V4 缓存产物,不重复执行 Cargo 预热。只传本轮新 key,命中对象只传使用时间;宿主与真实来源镜像对象合并、去重、按新近使用时间裁剪到 4 GiB,从无对象缓存基础镜像重新组装。源 run 不要求全绿,但取消、缺组、旧 attempt、未完成上传或混用来源镜像不得采用。网关暂停新 FetchTask、在途领取结束、持久化任务账本清空且内层活动容器为空才切换,不打断运行中的 CI。首次接入/升级网关需空闲窗口;Token 只需普通仓库 `write:repository`,不查管理员 API。候选装载后清理已收集 artifact,遗留项保留 7 天;真实 master CI 验证后才清理旧镜像,保留当前、一个回滚版、基础镜像及容器引用。部署入口见 `deploy/container/README.md`,合并代码不等于服务启用。
+1 -1
View File
@@ -18,7 +18,7 @@ prepare_builder() {
--driver-opt image=moby/buildkit:v0.23.2@sha256:ddd1ca44b21eda906e81ab14a3d467fa6c39cd73b9a39df1196210edcb8db59e \
--buildkitd-config "${repo_root}/deploy/container/gitea-ci-buildkitd.toml"
fi
if [[ "$(docker buildx inspect "${builder_name}" --format '{{.Driver}}')" != docker-container ]]; then
if [[ "$(docker buildx inspect "${builder_name}" | awk '$1 == "Driver:" { print $2 }')" != docker-container ]]; then
echo "${builder_name} must use the isolated docker-container driver" >&2
return 1
fi
+11 -2
View File
@@ -90,7 +90,8 @@ class GiteaCiImageContextTest(unittest.TestCase):
set -eu
if [[ "$1" == buildx && "$2" == version ]]; then exit 0; fi
if [[ "$1" == buildx && "$2" == inspect ]]; then
if [[ " $* " == *" --format "* ]]; then printf 'docker-container\\n'; fi
if [[ "$#" != 3 ]]; then echo 'unsupported buildx inspect arguments' >&2; exit 2; fi
printf 'Name: genarrative-ci-images\\nDriver: %s\\n' "${FAKE_BUILDX_DRIVER:-docker-container}"
exit 0
fi
if [[ "$1" == buildx && "$2" == build ]]; then
@@ -108,8 +109,10 @@ class GiteaCiImageContextTest(unittest.TestCase):
).encode("utf-8"))
docker.chmod(docker.stat().st_mode | stat.S_IXUSR)
def run_script(self, script: Path, *arguments: str, capture_context: bool = False) -> subprocess.CompletedProcess[str]:
def run_script(self, script: Path, *arguments: str, capture_context: bool = False,
builder_driver: str = "docker-container") -> subprocess.CompletedProcess[str]:
exports = [f"export PATH={shlex.quote(self.execution_bin)}:\"$PATH\""]
exports.append(f"export FAKE_BUILDX_DRIVER={shlex.quote(builder_driver)}")
if capture_context:
exports.append(f"export TAR_CAPTURE={shlex.quote(self.wsl_path(self.context_archive))}")
command = "; ".join(exports) + "; cd /; exec bash " + shlex.quote(self.wsl_path(script))
@@ -129,6 +132,12 @@ class GiteaCiImageContextTest(unittest.TestCase):
yield dependency["path"]
yield from GiteaCiImageContextTest.dependency_paths(child)
def test_build_rejects_a_builder_with_the_wrong_driver(self) -> None:
result = self.run_script(IMAGE_SCRIPT, "build", capture_context=True, builder_driver="docker")
self.assertNotEqual(result.returncode, 0)
self.assertIn("must use the isolated docker-container driver", result.stderr)
self.assertFalse(self.context_archive.exists())
def test_build_context_contains_all_local_dependency_manifests_and_no_source(self) -> None:
result = self.run_script(IMAGE_SCRIPT, "build", capture_context=True)
self.assertEqual(result.returncode, 0, result.stderr)