Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/release-notes/v3.19.1-29-zh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# CCSwitchMulti v3.19.1-29

本版本修复 Codex Sub-Agent V2 为包含标点的第三方模型生成角色文件时,角色可能被 Codex 整体忽略的问题。

## 修复内容

- 生成 `nickname_candidates` 时遵循 Codex 的字符约束:仅保留 ASCII 字母、数字、空格、连字符和下划线。
- 对模型标识中的点号等不支持字符做安全归一化。例如 `qwen3.8` 现在生成昵称 `Qwen3 8`,不会再触发 malformed agent role definition。
- 当归一化后没有可用字符时,使用安全的默认昵称,保证生成的角色文件仍可被 Codex 加载。
- 新增 `qwen3.8` 回归测试,覆盖角色生成和 TOML 序列化结果。

## 验证

- `codex_subagent_profiles::tests`:85 项通过,0 项失败。
- Rust 格式检查和 Git whitespace 检查通过。
16 changes: 16 additions & 0 deletions docs/release-notes/v3.19.1-30-zh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# CCSwitchMulti v3.19.1-30

本版本将 Sub-Agent V2 自动昵称修复扩展到所有模型标识,不再针对单个 Qwen 模型做特殊处理。

## 修复内容

- 自动昵称从完整模型标识生成,合法的连字符与下划线会保留。
- 点号、斜杠、冒号及其他不符合 Codex 昵称语法的字符统一安全归一化。
- 没有可用 ASCII 字符的标识使用安全默认昵称;无法生成合法 roleName 的模型仍会在更早阶段明确拒绝。
- 自动昵称和用户显式昵称共用同一条 Codex 字符规则,避免校验逻辑漂移。
- 增加多模型表驱动测试和最终生成角色不变量测试,覆盖 `qwen3.8`、`gpt-4.1`、`qwen2.5-coder`、斜杠/冒号模型标识及 Unicode 边界。

## 验证

- `codex_subagent_profiles::tests`:87 项通过,0 项失败。
- Rust 格式检查与 Git whitespace 检查通过。
21 changes: 21 additions & 0 deletions docs/release-notes/v3.19.1-31-zh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# CCSwitchMulti v3.19.1-31

本版本修复 Qwen 等 Responses-to-Chat 路由在长任务中看似“做到一半卡住”的传输根因。

## 修复内容

- Hosted Web Search 或 Image Generation 全局启用时,不再把每个 Codex 流式请求无条件改成 `stream=false`。
- 流式 `tool_choice=auto` 请求优先保留增量输出;只移除当前流式桥无法安全拦截的 OpenAI Hosted 工具,终端、文件、Git、MCP 和其他客户端工具保持可用。
- 显式选择 `web_search`/`image_generation` 时继续使用原有的有界 Hosted Tool loop;客户端原本就是非流式时也保持原行为。
- 流式判定同时遵循请求体和 HTTP 头,不假设 `stream` 字段一定存在。

## 验证

- 新增 RED/GREEN 回归,覆盖流式 auto、显式 Hosted 工具选择和非流式 auto 三条边界。
- 原有 Hosted Web Search loop 回归通过。
- Rust library:3000 项通过,0 项失败,2 项忽略。
- `cargo check --lib`、rustfmt 和 Git whitespace 检查通过。

## 相关远端修复

roglinux 的 Qwen3.8 透明代理同步完成 Tool Guard 根修:模型识别改为读取实际运行环境和可配置别名;模型切换事务会重启共享 EnvironmentFile 的 proxy/dashboard 消费者,避免实际模型已经是 `qwen3.8`、代理仍按 `qwen3.6` 判断。真实 canary 已验证 Guard 生效且正常最终回答不被强制调用工具。
10 changes: 10 additions & 0 deletions memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -3491,6 +3491,16 @@
- 固定构建前完整门禁:Rust library 2996/2996,Vitest 123 files / 1002 tests,原生 Windows PowerShell 5.1 下 release-build-config 6/6、事务安装 47/47;`cargo check --lib`、typecheck、Prettier、rustfmt 和 `git diff --check` 通过。Pester 3.4.0 的 `Should Throw` 在 PowerShell 7 下会误报,必须按仓库既定边界使用 Windows PowerShell 5.1,不能把运行器不兼容当生产失败或伪装成通过。
- 第一次 v28 本地流水线虽 exit 0,但构建日志出现 `__TAURI_BUNDLE_TYPE variable not found`;事务 `ccsm-20260815-121651-5dd2541f85cc43c69cbdf94b84c45dae` 发现安装态哈希仍等于 raw EXE、没有完成 `UNK -> NSS` 标记,按设计回滚到 v27,`RollbackError=null`,回滚后 FileVersion/ProductVersion 为 `3.19.1-27`、哈希 `CD0ED804D3D1CABD10144E31CC674A84A7855889BCF9ECF49F58C140D5167BC4`、health 200。
- 根因是 worktree 的 `node_modules` junction 指向主工作树:声明与 lock 已固定 `@tauri-apps/cli 2.10.1`,实际安装包仍为 `2.8.1` 且命令报告 `tauri-cli 2.8.0`,与 Rust `tauri-utils 2.8.3` marker 机制不匹配。普通 frozen install 在 junction 重建确认下可无变更返回 0,因此发布流水线必须先执行非交互 `pnpm install --frozen-lockfile --force`,再同时核对声明版本、实际安装 package 版本和 CLI 自报版本,之后才允许 typecheck/export;不能把事务 expected hash 改成 raw 来掩盖 updater bundle type 缺失。实际依赖重建后 package 与 CLI 均为 `2.10.1`。
## 2026-08-15 Qwen3.8 中途停顿与 Hosted Tools 流式根修

- 目标会话约 189K prompt tokens、945 KB 请求体、139 条 Chat message 和 41 个工具,仍低于 Qwen3.8 的 262144 上下文;现场没有 429、5xx、context overflow 或转换丢失。长上下文只放大等待,不是根因。
- roglinux 透明代理把 thinking/Tool Guard 模型硬编码为 `qwen3.6`,且模型切换只重启 vLLM worker:环境文件已写 `VLLM_SERVED_MODEL_NAME=qwen3.8`,长驻 proxy 进程仍持有 `qwen3.6`。独立修复仓库 `C:\Users\sunda\Documents\LLMservice\qwen38-tool-guard-fix` 用 RED/GREEN 将系统 Guard、tail Guard、流过滤和 generation limit 统一到运行时模型解析器,并让 controller/兼容 shell 切换事务重启 proxy/dashboard。生产 canary 日志为 `codex_tool_guard_applied=true`、`system_applied+tail_applied`,同时正常最终文本成功,不能用全局 `tool_choice=required` 代替。
- CCSwitch 根因位于 `forwarder.rs`:只要原始请求带 Hosted Web Search/Image Generation 且开关启用,就把全部 Responses-to-Chat 请求改成 `stream=false`;Codex 显示流式但上游没有增量,长上下文时表现为持续“正在思考”。v31 改为语义化传输策略:流式 `tool_choice=auto` 保留 SSE 并从 Chat 投影中移除 hosted-only 工具,普通客户端工具不受影响;显式 hosted tool choice 与真正非流式请求继续走有界 loop。不得根据用户文本猜测是否搜索。
- RED/GREEN 提交为 `9ca173a0` / `c45d0dfa`(从 v30 基线重放后的哈希)。完整 Rust library 为 3000 passed / 0 failed / 2 ignored,`cargo check --lib`、rustfmt 与 `git diff --check` 通过。安装验收必须看到同一 trace 的 `streaming=true` 和 `upstream_stream=true`,并分别验证普通工具循环、显式 Hosted 工具、正常最终回答与长上下文。
- v31 本地 release 构建日志明确出现 Tauri `Patching ... with bundle type information: nsis`,安装包 SHA-256 为 `665CDBF69AE889CAA5AD3473A3AB71CAD4B99C79633EB41CDF93B86E15FE88F5`。事务 `ccsm-20260815-214338-8abca640d04e4137bf314eaa3d95264d` 返回 `Success`、`Error=null`、`RollbackError=null`;安装版 PID/15721 owner 均为 `48992`,ProductVersion `3.19.1-31`,SHA-256 `DE307C845D02CE59AF334DFEE98C2A2BC193E9A1A0981266BEFC59A5C0754A96`,health HTTP 200。
- 安装版真实 Qwen canary 使用 `stream=true`、`tool_choice=auto`,同时携带 hosted `web_search` 与普通 function。首个 SSE 事件 0.593 秒到达,共 50 个事件并以 `response.completed` 结束;router trace `ef86c36b-0970-4665-9a50-2c8b7371365d` 显示 `/responses -> /chat/completions`、Qwen route HTTP 200、`streaming=true`、`upstream_stream=true`。这证明全局 Hosted Tools 不再让普通 Agent 请求失去增量输出。
- 第二个安装版 canary 在相同 hosted `web_search` 广告下要求普通 `report_marker`,实际收到流式 `response.function_call_arguments.done`、正确工具名和 `CCSM_QWEN38_TOOL_OK` 参数;trace `ed4f112d-8604-4857-b8b3-9f81c00d38c2` 同样为 HTTP 200、`streaming=true`、`upstream_stream=true`。因此策略只移除 hosted-only 定义,没有误删 Codex 的终端/文件/MCP 类客户端工具。
- 发布流水线曾在进程启动时读取 v30,随后 worktree 提升 v31,导致实际成功构建 v31 但导出阶段仍寻找 v30;重新执行 `export-latest-ccswitchmulti.ps1 -SkipBuild` 后按当前 v31 正确生成安装包、签名和 `latest.json`。以后版本提升必须发生在启动发布流水线之前,不能在持锁构建期间改变版本源。

## 2026-08-15 v3.19.1-28 可信构建、事务安装与 UI 验收

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cc-switch-multi",
"version": "3.19.1-28",
"version": "3.19.1-31",
"description": "All-in-One Assistant for Claude Code, Codex & Gemini CLI",
"type": "module",
"scripts": {
Expand Down
49 changes: 49 additions & 0 deletions scripts/run-v31-local-install.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
$ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest

$repoRoot = Split-Path -Parent $PSScriptRoot
. (Join-Path $PSScriptRoot "release-build-config.ps1")

$installer = "C:\Users\sunda\Documents\LLMservice\最新版ccswitchmulti\windows\installer\CCSwitchMulti_3.19.1-31_x64-setup.exe"
$rawExecutable = Join-Path $repoRoot "src-tauri\target\release\cc-switch.exe"
$installedExecutable = "C:\Users\sunda\AppData\Local\CCSwitchMulti\cc-switch.exe"
$installDirectory = Split-Path -Parent $installedExecutable
$uninstallExecutable = Join-Path $installDirectory "uninstall.exe"
$listener = Get-NetTCPConnection -State Listen -LocalPort 15721 | Select-Object -First 1
if (-not $listener) { throw "CCSwitchMulti is not listening on port 15721" }

$transactionId = "ccsm-20260815-qwen38-stream-v31"
$backupRoot = Join-Path "C:\Users\sunda\AppData\Local\CCSwitchMultiTransactionBackups" $transactionId
New-Item -ItemType Directory -Force -Path $backupRoot | Out-Null
$resultPath = Join-Path $backupRoot "transaction-result.json"
$stderrPath = "$resultPath.stderr"

$arguments = @(
"-NoProfile", "-ExecutionPolicy", "Bypass",
"-File", (Join-Path $PSScriptRoot "install-ccswitchmulti-transaction.ps1"),
"-InstallerPath", $installer,
"-ExpectedInstallerHash", (Get-ReleaseFileSha256 -Path $installer),
"-ExpectedCurrentVersion", (Get-Item -LiteralPath $installedExecutable).VersionInfo.ProductVersion,
"-ExpectedCurrentHash", (Get-ReleaseFileSha256 -Path $installedExecutable),
"-ExpectedInstalledVersion", "3.19.1-31",
"-ExpectedInstalledHash", (Get-TauriNsisInstalledExeSha256 -Path $rawExecutable),
"-CurrentPid", [string]$listener.OwningProcess,
"-InstalledExecutable", $installedExecutable,
"-InstallDirectory", $installDirectory,
"-UninstallExecutable", $uninstallExecutable,
"-ConfigPath", "C:\Users\sunda\.cc-switch",
"-RegistryKey", "HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\CCSwitchMulti",
"-Port", "15721",
"-HealthUri", "http://127.0.0.1:15721/health",
"-TimeoutSeconds", "120",
"-BackupRoot", $backupRoot
)

$process = Start-Process powershell.exe -WindowStyle Hidden -PassThru -ArgumentList $arguments `
-RedirectStandardOutput $resultPath -RedirectStandardError $stderrPath

[pscustomobject]@{
TransactionId = $transactionId
ProcessId = $process.Id
ResultPath = $resultPath
} | ConvertTo-Json -Compress
110 changes: 110 additions & 0 deletions scripts/verify_qwen38_streaming.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import json
import time
import urllib.request


payload = {
"model": "qwen3.8",
"stream": True,
"tool_choice": "auto",
"input": [
{
"role": "user",
"content": [
{
"type": "input_text",
"text": "Reply only with CCSM_QWEN38_STREAM_OK. Do not call a tool.",
}
],
}
],
"tools": [
{"type": "web_search"},
{
"type": "function",
"name": "report_marker",
"description": "Report a marker only when explicitly requested.",
"parameters": {
"type": "object",
"properties": {"marker": {"type": "string"}},
"required": ["marker"],
"additionalProperties": False,
},
},
],
}
request = urllib.request.Request(
"http://127.0.0.1:15721/v1/responses",
data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": "Bearer PROXY_MANAGED",
"Content-Type": "application/json",
"Accept": "text/event-stream",
"User-Agent": "Codex Desktop/qwen38-stream-canary",
"session_id": "qwen38-stream-rootfix-20260815",
},
)

started = time.monotonic()
events = []
with urllib.request.urlopen(request, timeout=180) as response:
assert response.headers.get_content_type() == "text/event-stream", response.headers
first_event_seconds = None
for raw_line in response:
line = raw_line.decode("utf-8").strip()
if not line.startswith("data:"):
continue
if first_event_seconds is None:
first_event_seconds = time.monotonic() - started
data = line[5:].strip()
if data != "[DONE]":
events.append(json.loads(data))

event_types = [event.get("type") for event in events]
assert "response.completed" in event_types, event_types
serialized = json.dumps(events, ensure_ascii=False)
assert "CCSM_QWEN38_STREAM_OK" in serialized, serialized[-2000:]
print(
json.dumps(
{
"status": "CCSM_QWEN38_STREAM_OK",
"first_event_seconds": round(first_event_seconds or 0.0, 3),
"event_count": len(events),
"event_types": event_types,
},
ensure_ascii=False,
)
)

tool_payload = json.loads(json.dumps(payload))
tool_payload["input"][0]["content"][0]["text"] = (
"Call report_marker exactly once with marker CCSM_QWEN38_TOOL_OK. Do not answer with text."
)
tool_request = urllib.request.Request(
"http://127.0.0.1:15721/v1/responses",
data=json.dumps(tool_payload).encode("utf-8"),
headers={
"Authorization": "Bearer PROXY_MANAGED",
"Content-Type": "application/json",
"Accept": "text/event-stream",
"User-Agent": "Codex Desktop/qwen38-tool-canary",
"session_id": "qwen38-client-tool-rootfix-20260815",
},
)
tool_events = []
with urllib.request.urlopen(tool_request, timeout=180) as response:
for raw_line in response:
line = raw_line.decode("utf-8").strip()
if not line.startswith("data:"):
continue
data = line[5:].strip()
if data != "[DONE]":
tool_events.append(json.loads(data))

tool_serialized = json.dumps(tool_events, ensure_ascii=False)
assert "response.function_call_arguments.done" in [
event.get("type") for event in tool_events
], tool_serialized[-2000:]
assert "report_marker" in tool_serialized, tool_serialized[-2000:]
assert "CCSM_QWEN38_TOOL_OK" in tool_serialized, tool_serialized[-2000:]
print("CCSM_QWEN38_TOOL_OK")
2 changes: 1 addition & 1 deletion src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "cc-switch"
version = "3.19.1-28"
version = "3.19.1-31"
description = "All-in-One Assistant for Claude Code, Codex & Gemini CLI"
authors = ["Jason Young"]
license = "MIT"
Expand Down
9 changes: 7 additions & 2 deletions src-tauri/src/codex_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1377,7 +1377,10 @@ fn codex_official_models_cache() -> Option<Vec<Value>> {
}
_ => existing_cache,
}?;
let models = official_cache.get("models").and_then(Value::as_array)?.clone();
let models = official_cache
.get("models")
.and_then(Value::as_array)?
.clone();
Some(models)
}

Expand Down Expand Up @@ -6193,7 +6196,9 @@ mod tests {
let sol_models = sol.as_array().unwrap().clone();
let sol_capability = official_reasoning_capability_for_model("gpt-5.6-sol", &sol_models)
.expect("sol official capability");
assert!(sol_capability.supported_efforts.contains(&"ultra".to_string()));
assert!(sol_capability
.supported_efforts
.contains(&"ultra".to_string()));
// 不匹配的 slug 返回 None
assert!(official_reasoning_capability_for_model("gpt-5.6-sol", &models).is_none());
}
Expand Down
Loading
Loading