取消 DirectProject 成功回复脱敏

DirectProject 流式文本和最终回复只移除 thinking 包装,成功正文按用户项目内容原样展示。

删除路径、链接和凭据占位符替换以及回复长度截断逻辑。

同步删除最终回复必须脱敏的文档要求,并更新回归测试验证项目内容保持原样。
This commit is contained in:
2026-09-02 14:43:54 +08:00
parent 529a1a0a77
commit 479094ef89
2 changed files with 20 additions and 41 deletions
@@ -14,7 +14,6 @@ const DIRECT_AGC_ENGINEERING_GUIDANCE: &str = "AGC 工程合同(仅说明项
const DIRECT_CODEX_ART_SPEC_ASSET_PATH: &str = "assets/art-spec.png";
const DIRECT_CODEX_BACKGROUND_ASSET_PATH: &str = "assets/direct-game-background.png";
const DIRECT_CODEX_SPRITESHEET_ASSET_PATH: &str = "assets/art-spritesheet.png";
const MAX_DIRECT_VISIBLE_REPLY_CHARS: usize = 16 * 1024;
const PLATFORM_GENERATION_SOURCE_PRESERVED_NO_RETRY_PREFIX: &str =
"platform-generation-source-preserved-no-retry:";
const DIRECT_TAONIER_LOCAL_RECONCILIATION_PREFIX: &str =
@@ -3661,24 +3660,15 @@ fn sync_direct_codex_project_outputs_at(
sync_direct_codex_project_file_projection_at(root, previous_output_fingerprint)
}
/// Project Codex text into the only form that may cross the DirectProject UI
/// boundary. The app-server stream can contain reasoning blocks, URLs,
/// credentials, or host paths before the final reply is known; those values
/// must never be emitted as an intermediate chat message or persisted as the
/// user-visible assistant turn.
fn project_direct_codex_visible_text(root: &Path, value: &str) -> Option<String> {
/// Project Codex text for the user-visible DirectProject stream and reply.
/// Reasoning wrappers are still removed because they are not reply text, but
/// the user owns the project and the resulting reply is not redacted here.
fn project_direct_codex_visible_text(value: &str) -> Option<String> {
let stripped = strip_incomplete_direct_thinking_marker(&strip_llm_thinking_blocks(value));
if stripped.trim().is_empty() {
return None;
}
let redacted = redact_agent_runtime_error(root, &stripped, MAX_DIRECT_VISIBLE_REPLY_CHARS)
.replace("<redacted-url>", "(链接已隐藏)")
.replace("$PROJECT_ROOT", "(项目路径已隐藏)")
.replace("<absolute-path>", "(路径已隐藏)")
.replace("[redacted-secret]", "(敏感信息已隐藏)")
.replace("[redacted-sensitive-field]", "(敏感字段已隐藏)")
.replace("[redacted sensitive context]", "(内部信息已隐藏)");
let visible = redacted.trim().to_string();
let visible = stripped.trim().to_string();
(!visible.is_empty()).then_some(visible)
}
@@ -3695,14 +3685,13 @@ fn strip_incomplete_direct_thinking_marker(value: &str) -> String {
}
fn project_direct_codex_accumulated_text(
root: &Path,
stream_enabled: bool,
accumulated_text: &str,
) -> Option<String> {
if !stream_enabled {
return None;
}
project_direct_codex_visible_text(root, accumulated_text)
project_direct_codex_visible_text(accumulated_text)
}
pub(crate) fn build_direct_codex_system_prompt(root: &Path) -> Result<String, String> {
@@ -3915,7 +3904,7 @@ async fn run_direct_game_creator_turn_inner(
let mut observer = move |observation: DirectCodexTurnObservation| match observation {
DirectCodexTurnObservation::AccumulatedText(accumulated_text) => {
let visible_text =
project_direct_codex_accumulated_text(root, stream_enabled, &accumulated_text);
project_direct_codex_accumulated_text(stream_enabled, &accumulated_text);
if visible_text.is_none() {
return;
}
@@ -3926,7 +3915,7 @@ async fn run_direct_game_creator_turn_inner(
}
DirectCodexTurnObservation::IntermediateText(intermediate_text) => {
let visible_text = if stream_enabled {
project_direct_codex_visible_text(root, &intermediate_text)
project_direct_codex_visible_text(&intermediate_text)
} else {
None
};
@@ -3963,7 +3952,7 @@ async fn run_direct_game_creator_turn_inner(
.await
}
.map_err(|error| DirectCodexTurnFailure::new(DirectCodexFailureStage::CodeGeneration, error))?;
let visible_reply = project_direct_codex_visible_text(root, &reply).ok_or_else(|| {
let visible_reply = project_direct_codex_visible_text(&reply).ok_or_else(|| {
DirectCodexTurnFailure::new(
DirectCodexFailureStage::CodeGeneration,
"陶泥儿未返回可展示的回复".to_string(),
@@ -4662,58 +4651,48 @@ mod tests {
}
#[test]
fn direct_visible_stream_projection_hides_internal_content() {
let root = tempfile::tempdir().expect("direct stream root");
let project_file = root.path().join("game/index.html");
fn direct_visible_stream_projection_keeps_user_project_reply_content() {
let project_file = std::path::Path::new("game/index.html");
let raw = format!(
"先说一句\n<think>内部推理不应显示</think>\n来源 https://example.test/a\n路径 {}\nauthorization: Bearer secret-value-123",
project_file.display()
);
let visible =
project_direct_codex_visible_text(root.path(), &raw).expect("safe visible stream text");
let visible = project_direct_codex_visible_text(&raw).expect("visible stream text");
assert!(visible.contains("先说一句"), "{visible}");
assert!(!visible.contains("内部推理"), "{visible}");
assert!(!visible.contains("https://example.test"), "{visible}");
assert!(visible.contains("https://example.test"), "{visible}");
assert!(
!visible.contains(project_file.to_string_lossy().as_ref()),
"{visible}"
);
assert!(!visible.contains("secret-value-123"), "{visible}");
assert!(visible.contains("链接已隐藏"), "{visible}");
assert!(
visible.contains("项目路径已隐藏") || visible.contains("路径已隐藏"),
visible.contains(project_file.to_string_lossy().as_ref()),
"{visible}"
);
assert!(visible.contains("secret-value-123"), "{visible}");
}
#[test]
fn direct_visible_stream_projection_drops_unclosed_thinking_only_delta() {
let root = tempfile::tempdir().expect("direct stream root");
assert_eq!(
project_direct_codex_visible_text(root.path(), "<think>secret reasoning"),
project_direct_codex_visible_text("<think>secret reasoning"),
None
);
}
#[test]
fn direct_visible_stream_projection_hides_partial_thinking_tag() {
let root = tempfile::tempdir().expect("direct stream root");
assert_eq!(
project_direct_codex_visible_text(root.path(), "已公开内容\n<thi"),
project_direct_codex_visible_text("已公开内容\n<thi"),
Some("已公开内容".to_string())
);
}
#[test]
fn direct_accumulated_text_respects_the_explicit_stream_setting() {
let root = tempfile::tempdir().expect("direct stream root");
assert_eq!(
project_direct_codex_accumulated_text(root.path(), false, "阶段性回复"),
project_direct_codex_accumulated_text(false, "阶段性回复"),
None,
"stream=false 只能保留阶段状态,不能向聊天窗口发增量文本"
);
assert_eq!(
project_direct_codex_accumulated_text(root.path(), true, "阶段性回复"),
project_direct_codex_accumulated_text(true, "阶段性回复"),
Some("阶段性回复".to_string())
);
}
@@ -206,7 +206,7 @@ Supervisor 认领该回执后,由父 run 自己为每个原 delivery 逐一创
- 安装包侧车:Windows x64 release 固定随 Tauri resource 打包 `@openai/codex@0.147.0` 的原生 `codex.exe`Rust build script 从 AGC 子包锁定依赖 stage 到 resource,并写入版本与 SHA-256 清单。Windows 侧车映射只写入 `tauri.windows.conf.json`,通用 `tauri.conf.json` 不得让 Linux / macOS 构建依赖未生成的 Windows 二进制。运行时只在文件摘要和 `codex-cli` 版本同时匹配清单时优先选内置侧车;缺失、损坏或版本漂移时跳过它,按既有 npm 安装、PATH 顺序回退。安装包同时携带 Apache-2.0 第三方声明;API Key、`auth.json`、Cookie、Token、用户 `CODEX_HOME`、用户配置和项目数据绝不打包。
- Windows x64 release 安装包只生成 NSIS,不生成 MSI:`tauri.windows.conf.json``bundle.targets` 固定为 `["nsis"]`,通用配置继续保留其它平台的默认打包目标。
- CLI 安全边界:CLI 固定使用 argv 启动,禁止 shell 拼接;工作目录使用本次请求专用的空临时目录,不把游戏项目绝对路径写入 prompt、stdout、stderr 或持久记录。调用固定使用 ephemeral、忽略用户配置和 exec rules、read-only sandbox、never approval,并关闭 Codex shell tool;只继承 CLI 运行和认证所需的最小环境,显式移除宿主 `CODEX_API_KEY`。用户级 Codex 登录态继续由本机 Codex 自己读取,API Key、auth 文件、Cookie、Token、`CODEX_HOME` 私有内容不得复制到项目配置、Runtime sidecar、Agent DB、conversation 或日志;stdout / stderr 无换行时也受硬上限约束,stderr 诊断只记录固定分类、字节数和 SHA-256。
- 协议边界:Runtime 把既有 `LlmRunRequest` 的消息和当前函数目录编码为有界 prompt,并从同一函数 JSON Schema 生成 Codex structured-output schema。CLI 输出转换为现有 `LlmRunResponse / LlmToolCall` 后,继续经过 native tool / MCP 参数校验、动作上限、权限、pending、receipt、验证与格式修复链;最终回复仍走现有脱敏和唯一提交路径,不新增平行响应协议。
- 协议边界:Runtime 把既有 `LlmRunRequest` 的消息和当前函数目录编码为有界 prompt,并从同一函数 JSON Schema 生成 Codex structured-output schema。CLI 输出转换为现有 `LlmRunResponse / LlmToolCall` 后,继续经过 native tool / MCP 参数校验、动作上限、权限、pending、receipt、验证与格式修复链;最终回复仍走唯一提交路径,不新增平行响应协议。
- 取消与恢复:Codex 子进程绑定当前 Provider request lifecycle,取消、暂停、Runner draining 或 GUI owner 丢失时终止并回收当前进程;started 后没有可信终态仍沿现有 Provider reconciliation 处理。`agentMode`、CLI 可执行身份和影响输出的 Codex 参数进入 `providerConfigFingerprint`,模式切换不得消费另一模式遗留的 retry/handoff。
- 可用性与兼容:`check_game_creator_llm_config``codex_cli` 模式检查 Codex 可执行文件和版本,不再要求 HTTP API Key;在 `provider` 模式保持原有 API Key、base URL、model 和逐 Agent override 校验。设置面板始终保留旧 Provider 配置,模式切回后不得丢失既有密钥或逐 Agent设置。配置向导若写入 HTTP Provider 凭据,必须显式把模式切到 `provider`
- 验收:覆盖缺省模式、显式 Provider 回退、配置 round-trip、CLI 缺失/非零退出/损坏 JSONL/超限输出、structured tool calls、普通最终回复、进程取消、模式指纹漂移和旧 Provider 回归;同时执行 AGC 定向 Rust 测试、前端配置测试与 typecheck、配置门禁、编码检查和 `git diff --check`