mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-07 00:05:49 +00:00
Compare commits
4
Commits
update-index
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81de0f5331 | ||
|
|
69c1ee15bb | ||
|
|
ba87cfde6a | ||
|
|
d0a588fbc3 |
@@ -19,7 +19,7 @@ description: Codex Proxy RS 的版本规划、发布说明、正式或预发行
|
||||
|
||||
- Codex 兼容性变化按[问题与方案依据](../../../CONTRIBUTING.md#问题与方案依据)核对已有依据与验证范围,缺少依据列为发行风险,不以 CI 通过代替
|
||||
- `release/publish --check <version>` 只读检查发行准备条件;不带 `--check` 会提交、打 tag、推送并触发工作流,实际发布仍需相应授权
|
||||
- 同名版本、超时或中断先查本地与远端状态,不删已发布 tag、不移动 tag、不强推掩盖冲突
|
||||
- 同版本失败先区分[续跑原任务与修复源码后重发](references/operations.md#恢复与验收):前者沿用原提交;后者仅在版本尚未公开时清理失败的版本提交,再生成唯一的版本提交。已发布的 tag 和产物保持不变
|
||||
- 备版只更新源码的构建版本;通道快照是版本 Release 的 `version.yaml` 附件,验收时核对固定预发行页 `update-index` 指向的公开快照
|
||||
- 预发行不得覆盖 GitHub Latest 或镜像 `latest`,完成后回读核对
|
||||
- `release/notes.md` 只承载当前发行的说明;常规文档修改另按开发入口执行[文档检查](../cpr-dev-guide/references/documentation.md)
|
||||
|
||||
@@ -38,12 +38,25 @@ release/publish "$release_version"
|
||||
|
||||
## 恢复与验收
|
||||
|
||||
- 脚本中断或网络超时时,先核对本地提交与 tag、远端分支与 tag、对应工作流 run 及 Release,确定已完成到哪一步。
|
||||
若本地已创建版本提交或 tag,不直接重跑入口,也不通过删 tag、回退版本或强推来消除报错。
|
||||
- 提交和 tag 已推送但工作流未触发时,核对目标后使用脚本提示的 dispatch 命令补触发;
|
||||
工作流已存在则跟踪或恢复对应 run,避免重复发布。发现 tag 指向不同提交时报告冲突,不移动已发布 tag。
|
||||
- 跟踪与本次 tag 对应的 run ID,核对工作流解析的目标提交。触发工作流成功只表示已开始,不能当作发布完成。
|
||||
- 构建与打包中间产物保留 7 天,在有效期内可重跑失败任务;产物过期后重跑完整工作流,不移动原 tag。
|
||||
先核对本地与远端的分支头、版本提交、tag,以及对应的工作流、GitHub Release 和版本镜像。超时或任务失败不等于版本未公开;即使 Release 还是草稿,版本镜像也可能已经推送
|
||||
|
||||
### 续跑原任务
|
||||
|
||||
源码无需修改时保留版本提交和 tag,核对两者指向后从断点继续:
|
||||
|
||||
- 尚未推送:补齐缺失的步骤,按入口原子推送分支和 tag
|
||||
- 已推送但未触发:使用入口提示的 dispatch 命令
|
||||
- 已有 run:跟踪对应提交的任务,失败时重跑失败任务。构建产物保留 7 天,过期后从原 tag 重跑完整工作流
|
||||
|
||||
### 修复源码后同版本重发
|
||||
|
||||
1. 确认旧工作流已停止、该版本没有公开 Release 或版本镜像,且待清理提交不在其他已发布 tag 的历史中。已有公开产物时保留原提交和 tag,代码修复另发新版本;发布状态不明时先查清
|
||||
2. 从失败版本提交的父提交重新接上后续功能和修复,跳过失败的 `chore(release): vX.Y.Z`。保留最终说明为未提交改动,版本号和插件弃用清单交给发行入口。核对差异,确保修复未丢失、失败提交已不在待发历史中
|
||||
3. 只清理本次失败的 tag 和草稿。涉及远端改写时,先核对授权,以事先记录的分支头和 tag 对象 SHA 作显式租约,原子更新分支并删除未发行 tag。租约不匹配就停止,不合并旧线
|
||||
4. 修复验证通过后执行 `release/publish --check <version>`,再按[执行发版](#执行发版)重发。核对上一发行至新 tag 的历史中只有一个本版本的发行提交,且与工作流构建的提交一致
|
||||
|
||||
### 验收
|
||||
|
||||
- 发布完成后核对 GitHub Release 的版本、正式版/预发行状态和中文变更正文;对照平台清单与工作流确认归档、校验和及镜像产物。
|
||||
工作流附加的安装说明和自动提交列表按其当前配置处理,不用它们替代手写的中文变更说明。
|
||||
- 回读固定预发行页 `update-index`(不是 GitHub Latest),按正文 JSON 的 `current` 下载对应 `v<current>` 的 `version.yaml`。核对快照版本及 `published`:正式版参与同一大版本四个普通通道的比较;RC 参与 RC、Beta、Alpha,Beta 参与 Beta、Alpha,Alpha 只参与自身。实验版只推进 `exp["X.Y.Z"]`。晚完成的发行不得回退较高候选或丢失其他发行线
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
读取当前 [notes.md](../../../../release/notes.md)与[项目约定](../../../../CONTRIBUTING.md#项目约定)中的发布说明条目
|
||||
|
||||
- 只在发布说明任务中修改 `release/notes.md`,替换为本次版本内容,首行为目标 tag,正文使用中文
|
||||
- 按用户可感知的最终变化归并,省略空章节,不逐条翻译提交,不写排查过程或未经验证的承诺
|
||||
- 常规版本按「新增功能」「优化改进」「问题修复」「升级须知」归类,省略空章节;单一改动可直接写一句。安装命令和文档入口由工作流附加
|
||||
- 每条用一句短语讲清一个用户可感知的结果,不加句号,例如「系统更新:确认当前平台有安装包后才提示升级」
|
||||
- 同一变化只写一次,保留会影响使用或升级的默认值和限制,不展开实现过程、逐条翻译提交或写未经验证的承诺
|
||||
- 剔除回退、重复及未进入发行范围的内容,升级事项须能追溯到实际迁移、配置或兼容变化
|
||||
- 只翻译已有准确说明时保持事实与范围,不混入其他变更
|
||||
- 常规文档仍只描述当前状态;必要修订执行[文档检查](../../cpr-dev-guide/references/documentation.md),不复制发布说明中的版本变化列表
|
||||
@@ -26,6 +28,8 @@
|
||||
|
||||
进入实际发行准备后,读取目标分支的 [release/publish](../../../../release/publish)、[工作流](../../../../.github/workflows/release.yml)与[平台清单](../../../../release/platforms.yaml),按当前入口执行,不另写脚本或绕过检查
|
||||
|
||||
在上一发行基线到 HEAD 的提交中检查是否已有完整标题为 `chore(release): v<目标版本>` 的提交,即使同名 tag 已不存在也要查。命中时先核对该提交对应的发行状态,按[失败恢复](operations.md#恢复与验收)处理,不在旧提交上再叠一个同名提交
|
||||
|
||||
发布说明准备好、其他代码已提交后,执行 `release/publish --check <version>`,检查版本、说明、工作区、远端分支与 tag、工具链和插件兼容窗口。
|
||||
预检不修改文件、Git 引用或远端状态;远端存在本地未知提交时先同步再检查。它不替代 CI、发行物验证或实际发布授权
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ module ReleasePackage
|
||||
default_image = "${CPR_IMAGE:-#{image}:latest}"
|
||||
raise 'Expected exactly one default application image in deploy/compose.yaml' unless compose.scan(default_image).length == 1
|
||||
File.write("#{release_dir}/compose.yaml", compose.sub(default_image, "${CPR_IMAGE:-#{image}:#{ENV.fetch('DOCKER_VERSION')}}"))
|
||||
FileUtils.cp(MANIFEST, release_dir)
|
||||
|
||||
platforms.each do |platform|
|
||||
raise "Unsupported archive format: #{platform['archive']}" unless platform['archive'] == 'tar.gz'
|
||||
|
||||
@@ -87,7 +87,7 @@ module PublishedChannels
|
||||
platforms = YAML.safe_load(Base64.decode64(data.fetch('content'))).fetch('platforms')
|
||||
raise "Missing platforms for #{tag}" if platforms.empty?
|
||||
expected = platforms.map { |p| ReleaseMetadata.archive_name(version, p) }
|
||||
expected.concat(%w[checksums.txt compose.yaml config.example.yaml plugin-release-manifest.json])
|
||||
expected.concat(%w[checksums.txt compose.yaml config.example.yaml])
|
||||
expected << 'version.yaml' if index
|
||||
assets = release.fetch('assets').select { |a| a.fetch('state') == 'uploaded' && a.fetch('size').positive? }.to_h { |a| [a.fetch('name'), a] }
|
||||
missing = expected - assets.keys
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
require 'tmpdir'
|
||||
require_relative '../published-channels'
|
||||
|
||||
# 测试只替换 GitHub 读写边界,执行真实的发布状态转换
|
||||
class PublishedChannelsTest
|
||||
def assert_equal(expected, actual)
|
||||
raise "expected #{expected.inspect}, got #{actual.inspect}" unless expected == actual
|
||||
end
|
||||
|
||||
def assert_rejected
|
||||
yield
|
||||
rescue RuntimeError
|
||||
return
|
||||
else
|
||||
raise 'expected publication to be rejected'
|
||||
end
|
||||
|
||||
def initialize
|
||||
@pointer = nil
|
||||
@releases = {}
|
||||
@files = {}
|
||||
@calls = []
|
||||
@platforms = [{ 'os' => 'linux', 'arch' => 'amd64', 'archive' => 'tar.gz' }]
|
||||
end
|
||||
|
||||
def api(endpoint, method: 'GET', data: nil, optional: false)
|
||||
path = endpoint.delete_prefix('repos/test/repo/')
|
||||
@calls << [method, path]
|
||||
case [method, path]
|
||||
when ['GET', 'releases/tags/update-index'] then @pointer
|
||||
when ['POST', 'releases']
|
||||
raise 'duplicate pointer' if @pointer
|
||||
@pointer = data.transform_keys(&:to_s).merge('id' => 12, 'draft' => false)
|
||||
when ['PATCH', 'releases/12']
|
||||
@pointer.merge!(data.transform_keys(&:to_s))
|
||||
when ['GET', 'releases/latest'] then @releases['3.41.2']
|
||||
else
|
||||
return @releases[path.delete_prefix('releases/tags/v')] if method == 'GET' && path.start_with?('releases/tags/v')
|
||||
return { 'content' => Base64.encode64(YAML.dump({ 'platforms' => @platforms })) } if path.start_with?('contents/release/platforms.yaml?ref=v')
|
||||
raise "unexpected #{method} #{path}"
|
||||
end
|
||||
end
|
||||
|
||||
def publish_assets(version, content = nil)
|
||||
names = %w[checksums.txt compose.yaml config.example.yaml plugin-release-manifest.json]
|
||||
names << ReleaseMetadata.archive_name(version, @platforms.first)
|
||||
@files[version] = content if content
|
||||
assets = names.map { |name| { 'name' => name, 'size' => 100, 'state' => 'uploaded' } }
|
||||
assets << { 'name' => 'version.yaml', 'size' => content.bytesize, 'digest' => "sha256:#{Digest::SHA256.hexdigest(content)}", 'state' => 'uploaded' } if content
|
||||
@releases[version] = { 'tag_name' => "v#{version}", 'draft' => false, 'prerelease' => ReleaseMetadata.prerelease?(version), 'assets' => assets }
|
||||
end
|
||||
|
||||
def publisher
|
||||
github = self
|
||||
files = @files
|
||||
Class.new(PublishedChannels::Publisher) do
|
||||
define_method(:api) { |endpoint, **args| github.api(endpoint, **args) }
|
||||
define_method(:download) { |version| files.fetch(version) }
|
||||
end.new('test/repo')
|
||||
end
|
||||
|
||||
def prepare(version)
|
||||
Dir.mktmpdir do |dir|
|
||||
File.write(File.join(dir, 'checksums.txt'), '')
|
||||
publisher.prepare(version, dir)
|
||||
content = File.read(File.join(dir, 'version.yaml'))
|
||||
assert_equal "#{Digest::SHA256.hexdigest(content)} version.yaml\n", File.read(File.join(dir, 'checksums.txt'))
|
||||
content
|
||||
end
|
||||
end
|
||||
|
||||
def current = JSON.parse(@pointer.fetch('body')).fetch('current')
|
||||
|
||||
def test_interrupted_publication_recovers_without_losing_another_channel
|
||||
publish_assets('3.41.2')
|
||||
stable = prepare('3.42.0')
|
||||
assert_equal nil, current
|
||||
assert_rejected { publisher.finish('3.42.0') }
|
||||
publish_assets('3.42.0', stable)
|
||||
# 模拟公开后更新指针中断,下一次发布在同一把锁内补写,再合并自己的通道
|
||||
beta = prepare('3.43.0-beta.1')
|
||||
assert_equal '3.42.0', current
|
||||
assert_equal({ 'stable' => '3.42.0', 'rc' => '3.42.0', 'beta' => '3.43.0-beta.1', 'alpha' => '3.43.0-beta.1' },
|
||||
YAML.safe_load(beta).dig('published', '3'))
|
||||
publish_assets('3.43.0-beta.1', beta)
|
||||
publisher.finish('3.43.0-beta.1')
|
||||
publisher.finish('3.43.0-beta.1')
|
||||
assert_rejected { publisher.finish('3.42.0') }
|
||||
assert_equal '3.43.0-beta.1', current
|
||||
experiment = YAML.safe_load(prepare('3.44.0-exp.1')).dig('published', '3')
|
||||
assert_equal({ '3.44.0' => '3.44.0-exp.1' }, experiment.fetch('exp'))
|
||||
assert_equal '3.43.0-beta.1', experiment.fetch('beta')
|
||||
assert_equal false, @calls.include?(['GET', 'releases'])
|
||||
end
|
||||
|
||||
def test_incomplete_corrupt_or_draft_release_keeps_last_public_snapshot
|
||||
publish_assets('3.42.0', prepare('3.42.0'))
|
||||
publisher.finish('3.42.0')
|
||||
beta = prepare('3.43.0-beta.1')
|
||||
publish_assets('3.43.0-beta.1', beta)
|
||||
release = @releases.fetch('3.43.0-beta.1')
|
||||
release['draft'] = true
|
||||
assert_rejected { publisher.finish('3.43.0-beta.1') }
|
||||
release['draft'] = false
|
||||
missing = release.fetch('assets').shift
|
||||
assert_rejected { publisher.finish('3.43.0-beta.1') }
|
||||
release.fetch('assets') << missing
|
||||
publish_assets('3.43.0-beta.1', beta.sub('3.42.0', '3.10.0'))
|
||||
assert_rejected { publisher.finish('3.43.0-beta.1') }
|
||||
assert_equal '3.42.0', current
|
||||
end
|
||||
end
|
||||
PublishedChannelsTest.new.test_interrupted_publication_recovers_without_losing_another_channel
|
||||
PublishedChannelsTest.new.test_incomplete_corrupt_or_draft_release_keeps_last_public_snapshot
|
||||
puts 'Published channel recovery and integrity checks passed'
|
||||
@@ -258,5 +258,4 @@ jobs:
|
||||
for script in .github/**/*.sh; do bash -n "$script"; done
|
||||
for script in .github/scripts/*.rb; do ruby -c "$script"; done
|
||||
ruby .github/scripts/toolchains.rb
|
||||
ruby .github/scripts/tests/published-channels-test.rb
|
||||
ruby -c backend/crates/gateway-plugin/runtime/plugin-api-lifecycle.rb
|
||||
|
||||
@@ -508,24 +508,12 @@ jobs:
|
||||
shell: bash
|
||||
env:
|
||||
INSTALLATION_NOTES: |
|
||||
## 📥 安装
|
||||
## Linux 一键安装(Docker Compose)
|
||||
|
||||
**Docker 部署:**
|
||||
```bash
|
||||
# 从 GitHub 容器镜像仓库拉取
|
||||
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.release-info.outputs.docker_version }}
|
||||
curl -fsSLO https://raw.githubusercontent.com/${{ github.repository }}/${{ needs.release-info.outputs.tag }}/deploy/install.sh && CPR_RELEASE_TAG=${{ needs.release-info.outputs.tag }} bash install.sh
|
||||
```
|
||||
|
||||
请从本版本附件下载 `compose.yaml` 和 `config.example.yaml`。Compose 默认固定到本版本镜像;升级前请对比模板与已有配置。
|
||||
|
||||
**手动下载:**
|
||||
请从下方附件中下载适合当前平台的归档包。
|
||||
|
||||
附件包含与当前宿主版本及提交绑定的 `plugin-release-manifest.json`,当前没有默认内置插件。
|
||||
教学示例由独立的 `codex-proxy-plugins` 仓库维护和发布,不随宿主发行物分发。
|
||||
|
||||
发布归档提供 GitHub 构建产物证明。Docker 镜像附带软件物料清单(SBOM)、构建来源证明和 cosign 无密钥签名。
|
||||
|
||||
## 📚 文档
|
||||
|
||||
- [GitHub 仓库](https://github.com/${{ github.repository }})
|
||||
|
||||
@@ -129,18 +129,15 @@ async fn preparing_diagnostics_never_wait_for_the_serialized_prepare_io() {
|
||||
#[tokio::test]
|
||||
async fn diagnostics_report_a_published_process_fault_without_plugin_details() {
|
||||
let (cache, store, runtime) = super::setup().await;
|
||||
let controls = tempfile::tempdir().unwrap();
|
||||
let exit = controls.path().join("exit");
|
||||
let mut snapshot = store.snapshot.lock().unwrap().clone();
|
||||
snapshot.instances[0].configuration = serde_json::json!({"exit_after_ready_ms":20});
|
||||
snapshot.instances[0].configuration = serde_json::json!({"exit_after_ready_signals":[exit]});
|
||||
let active = PluginPreparation::prepare(&runtime, snapshot.clone())
|
||||
.await
|
||||
.expect("candidate starts before the controlled exit");
|
||||
tokio::time::timeout(std::time::Duration::from_secs(2), async {
|
||||
while active.is_ready() {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("worker exit observed");
|
||||
std::fs::write(exit, b"exit").unwrap();
|
||||
wait_until_unready(&active).await;
|
||||
|
||||
let diagnostics = gateway_admin::ports::plugins::PluginRuntimeDiagnostics::runtime_diagnostics(
|
||||
&runtime,
|
||||
@@ -171,17 +168,20 @@ async fn diagnostics_report_a_published_process_fault_without_plugin_details() {
|
||||
#[tokio::test]
|
||||
async fn a_published_process_crash_preserves_the_serving_snapshot_and_other_plugin_status() {
|
||||
let (cache, store, runtime) = super::setup().await;
|
||||
let controls = tempfile::tempdir().unwrap();
|
||||
let exit = controls.path().join("exit");
|
||||
let snapshot = {
|
||||
let mut snapshot = store.snapshot.lock().unwrap();
|
||||
let mut failed = snapshot.instances[0].clone();
|
||||
failed.id = "crashing-plugin".into();
|
||||
failed.configuration = serde_json::json!({"exit_after_ready_ms":100});
|
||||
failed.configuration = serde_json::json!({"exit_after_ready_signals":[exit]});
|
||||
snapshot.instances.push(failed);
|
||||
snapshot.clone()
|
||||
};
|
||||
let generation = ExtensionPreparationPort::prepare(&runtime, ConfigRevision::new(1).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
std::fs::write(exit, b"exit").unwrap();
|
||||
wait_until_unready(&generation).await;
|
||||
assert!(generation.can_serve());
|
||||
assert!(!generation.is_ready());
|
||||
|
||||
@@ -94,29 +94,32 @@ async fn registration_with_a_changed_contribution_id_is_rejected() {
|
||||
async fn restart_circuit_caps_short_lived_crashes_and_a_new_revision_recovers() {
|
||||
let restart_circuit = gateway_plugin_runtime::PluginRestartCircuitConfig {
|
||||
maximum_failures: NonZeroUsize::new(3).unwrap(),
|
||||
stability_window: Duration::from_secs(1),
|
||||
// 本用例只验证连续崩溃的预算,不让宿主调度延迟触发稳定运行后的重置
|
||||
stability_window: Duration::MAX,
|
||||
};
|
||||
let (cache, store, runtime) = super::setup_with_restart_circuit(restart_circuit).await;
|
||||
let controls = tempfile::tempdir().unwrap();
|
||||
let marker = controls.path().join("starts.jsonl");
|
||||
let exits = [1, 2, 3].map(|start| controls.path().join(format!("exit-{start}")));
|
||||
let mut snapshot = store.snapshot.lock().unwrap().clone();
|
||||
snapshot.instances[0].configuration = serde_json::json!({
|
||||
"startup_marker": marker,
|
||||
"exit_after_ready_ms": 20,
|
||||
"exit_after_ready_signals": exits,
|
||||
});
|
||||
let mut active = PluginPreparation::prepare(&runtime, snapshot.clone())
|
||||
.await
|
||||
.expect("first incarnation");
|
||||
for expected_start in 2..=3 {
|
||||
for (index, exit) in exits.iter().enumerate() {
|
||||
assert_eq!(startup_count(&marker), index + 1);
|
||||
// Ready 早于注册完成;只有宿主拿到这一代后,才让对应的进程退出
|
||||
std::fs::write(exit, b"exit").unwrap();
|
||||
wait_until_unready(&active).await;
|
||||
let replacement = PluginPreparation::prepare(&runtime, snapshot.clone())
|
||||
.await
|
||||
.expect("failure budget still permits a restart");
|
||||
drop(active);
|
||||
active = replacement;
|
||||
assert_eq!(startup_count(&marker), expected_start);
|
||||
if index + 1 < exits.len() {
|
||||
active = PluginPreparation::prepare(&runtime, snapshot.clone())
|
||||
.await
|
||||
.expect("failure budget still permits a restart");
|
||||
}
|
||||
}
|
||||
wait_until_unready(&active).await;
|
||||
assert!(
|
||||
PluginPreparation::prepare(&runtime, snapshot.clone())
|
||||
.await
|
||||
@@ -434,7 +437,6 @@ async fn abandoned_candidate_keeps_shared_process_failure_in_restart_budget() {
|
||||
let mut snapshot = store.snapshot.lock().unwrap().clone();
|
||||
snapshot.instances[0].configuration = serde_json::json!({
|
||||
"startup_marker":marker,
|
||||
"exit_after_ready_delays_ms":[null,null],
|
||||
"exit_after_ready_signals":[old_exit,null],
|
||||
});
|
||||
let mut other = snapshot.instances[0].clone();
|
||||
|
||||
@@ -1546,11 +1546,6 @@ async fn main() {
|
||||
resumptions: Mutex::new(BTreeMap::new()),
|
||||
streams: Mutex::new(BTreeMap::new()),
|
||||
});
|
||||
let exit_after_ready = peer.configuration["exit_after_ready_delays_ms"]
|
||||
.as_array()
|
||||
.and_then(|delays| delays.get(startup))
|
||||
.and_then(Value::as_u64)
|
||||
.or_else(|| peer.configuration["exit_after_ready_ms"].as_u64());
|
||||
let exit_after_ready_signal = peer.configuration["exit_after_ready_signals"]
|
||||
.as_array()
|
||||
.and_then(|signals| signals.get(startup))
|
||||
@@ -1571,12 +1566,7 @@ async fn main() {
|
||||
vec![],
|
||||
)
|
||||
.await;
|
||||
if let Some(delay) = exit_after_ready {
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_millis(delay)).await;
|
||||
std::process::exit(7);
|
||||
});
|
||||
} else if let Some(signal) = exit_after_ready_signal {
|
||||
if let Some(signal) = exit_after_ready_signal {
|
||||
tokio::spawn(async move {
|
||||
while !std::path::Path::new(&signal).exists() {
|
||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
||||
|
||||
+1
-1
@@ -943,7 +943,7 @@ Release 必须提供当前 OS/架构的 `codex-proxy-rs_<version>_<os>_<arch>.ta
|
||||
|
||||
| 文件或目录 | 用途 |
|
||||
| --- | --- |
|
||||
| `plugin-release-manifest.json` | 固定宿主版本、提交、平台与支持的插件合同;插件列表当前为空 |
|
||||
| `plugins/official/plugin-release-manifest.json` | 随平台安装包和镜像分发,记录宿主版本、提交与支持的插件合同 |
|
||||
| 可执行文件同目录的 `plugins/official` | 随受信宿主发行物部署的只读目录,与二进制、Web 资源成套更新或恢复 |
|
||||
|
||||
- 下载更新时校验发行身份,插件合同不兼容不阻止安装。点击重启时检查已安装目标版本,列出不兼容插件的名称和原因;确认后重启,不兼容实例保持未启动,启用配置、绑定、密钥和私有数据保留
|
||||
|
||||
+20
-15
@@ -1,24 +1,29 @@
|
||||
# v3.42.0
|
||||
|
||||
## 账号调度与额度
|
||||
## 新增功能
|
||||
|
||||
- Key 和账号可以分别设置排队超时。开启「尝试换号」后,OpenAI 或 xAI 账号队满或超时时,网关会再找一次能立即接单的账号;没有空位则返回原错误。Key 队列、固定账号、原生续写和必须跟随主会话的子线程不会因此换号
|
||||
- OpenAI 调度减少了大号池选号和排队时的数据库与 Redis 读取。优先亲和模式下,主账号短时繁忙时临时分流,主账号失效或额度长期耗尽时,在后备账号接单后迁移绑定。没有显式会话 ID 的 Chat 和 Responses 请求,也可在同一 Key 下按缓存键或对话开头匹配账号;宽松模式仍按普通策略选号
|
||||
- Codex OAuth 账号的「自动使用点数」默认关闭。需要在套餐额度耗尽后继续使用点数时,请在「账号管理 → 展开账号 → 额度重置」为对应账号开启。只有上游确认点数可用且没有支出限制时才会继续调度
|
||||
- 并发排队:Key 和账号可分别设置超时,OpenAI 或 xAI 账号队满或超时时可尝试换号一次(默认关闭)
|
||||
- Codex OAuth:可在账号的「额度重置」中开启「自动使用点数」(默认关闭),套餐额度用尽且上游确认点数可用、无支出限制时生效
|
||||
- OpenAI Key:可设置安装身份种子,让每把 Key 在每个账号上使用独立、稳定的安装身份
|
||||
|
||||
## OpenAI 请求与安装身份
|
||||
## 优化改进
|
||||
|
||||
- 修复部分 Chat 客户端经 Codex OAuth 转发时,因携带默认采样参数或 `stream_options.include_usage` 而收到 400 的问题。旧式 token 上限会转为 `max_output_tokens`;显式截断、非空停止序列等策略仍交由上游校验。新增的字段处理仅用于 OAuth
|
||||
- OpenAI Key 可配置安装身份种子。同一把 Key 使用同一账号时安装身份保持稳定;换 Key 或换账号时使用不同身份。未配置种子的 Key 继续使用账号原有身份
|
||||
- 系统更新:按通道查询,无需翻页;新版本公开且当前平台的安装包和校验文件齐全后才提示升级
|
||||
- 安装更新:遇到 GitHub 限流时显示可重试时间;安装包下载后,校验和安装无需再访问 GitHub
|
||||
- OpenAI 调度:大账号池选号和排队时减少数据库与 Redis 读取
|
||||
- OpenAI 优先亲和:主账号繁忙时临时分流,失效或额度长期耗尽时改绑接单的账号
|
||||
- OpenAI 会话:优先亲和模式下,未提供会话 ID 的 Chat 和 Responses 请求也会尽量复用同一账号
|
||||
- 账号分组:悬停或点击色块即可查看组名和停用状态
|
||||
|
||||
## 系统更新与管理端
|
||||
## 问题修复
|
||||
|
||||
- 检查更新时按通道读取已发布版本,不再逐页遍历 GitHub Release。发现更高候选时按精确 tag 核对公开 Release、当前平台包和校验文件后才提示;遇到 GitHub 限流时显示可重试时间。安装包下载后,校验、安装和重启不再需要访问 GitHub
|
||||
- 编辑账号模型限制时,取消勾选的自定义模型会保留到本次编辑结束,可以在保存前重新勾选。账号分组色块支持悬停或点击查看组名及停用状态
|
||||
- 修复部分 Chat 客户端通过 Codex OAuth 转发时,默认采样参数或 `stream_options.include_usage` 导致请求返回 400 的问题
|
||||
- 修复编辑模型限制时,取消勾选的自定义模型在保存前就从列表消失的问题
|
||||
|
||||
## 升级事项
|
||||
## 升级须知
|
||||
|
||||
- 从 v3.41.2 升级时,两层排队超时都继承原值,换号默认关闭。两层独立计时,先后在两层排队的总等待时间可能变长。通过管理 API 或插件 SDK 更新运行设置时,用 `keyQueueTimeoutSeconds`、`accountQueueTimeoutSeconds` 和 `switchAccountOnQueueFailure` 替换 `concurrencyWaitTimeoutSeconds`
|
||||
- 已配置的 OpenAI Key 安装 ID 会自动转为种子;管理 API 调用方请将 `installationId` 改为 `installationSeed`。这些 Key 的出站安装身份及关联会话、缓存标识会变化,已有 WebSocket 精确续写需重新发送完整上下文
|
||||
- 使用自定义更新仓库的实例,请使用固定的 `update-index` 预发行页和版本 Release 的 `version.yaml` 附件提供通道索引。本仓库的发布工作流会自动维护,不需要额外分支
|
||||
- 从 v3.41.2 正式版升级只新增迁移 0039。若源码部署执行过中间版 main 的 `0039_queue_timeouts.sql`(包括随后执行过 0040 或 0041 的实例),其迁移记录与发行包不兼容。请勿直接覆盖升级或修改数据库中的校验和;应先在独立数据库验证恢复或数据迁移方案
|
||||
- **源码部署:** 执行过中间版 `main` 的 `0039_queue_timeouts.sql` 时,请先在独立数据库验证迁移,勿直接覆盖升级或修改校验和(从正式版 v3.41.2 升级不受影响)
|
||||
- **排队设置:** Key 与账号超时各自继承原值,先后排队的总等待时间可能变长
|
||||
- **管理 API 和插件 SDK:** 将 `concurrencyWaitTimeoutSeconds` 换为 `keyQueueTimeoutSeconds` 和 `accountQueueTimeoutSeconds`,并传入换号开关 `switchAccountOnQueueFailure`
|
||||
- **已设置安装 ID 的 Key:** 原值自动转为种子,API 字段由 `installationId` 改为 `installationSeed`;出站身份、会话和缓存标识随之变化,已有 WebSocket 精确续写需重发完整上下文
|
||||
- **自定义更新仓库:** 用 `update-index` 预发行页指向版本 Release 的 `version.yaml` 附件,本仓库的发布工作流会自动维护
|
||||
|
||||
Reference in New Issue
Block a user