mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 02:36:01 -10:00
Merge pull request #14073 from SivanCola/fix/website-cli-changelog
fix(website): restore CLI changelog sync / 恢复官网 1.x 更新日志同步
This commit is contained in:
5 files changed
+586
-32
No files matched your search
@@ -18,4 +18,17 @@ npm test
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Production publication is manual and accepted only from the `website` branch.
|
||||
Production publication runs on pushes to `website` or manual dispatch from that branch.
|
||||
|
||||
Production builds synchronize both CLI 1.x and Studio 2.x changelogs from
|
||||
published GitHub tags. CLI notes come from the tag's reviewed
|
||||
`release-notes/releases.json`; Studio notes come from its versioned Markdown.
|
||||
Synchronization needs GitHub read access (`gh auth login` locally or
|
||||
`GITHUB_TOKEN` in CI), not a model API key. Missing source notes fail the build.
|
||||
The existing CLI publication-marker check still controls public visibility.
|
||||
|
||||
生产构建会从已发布的 GitHub 标签同步 CLI 1.x 和 Studio 2.x 更新日志。
|
||||
CLI 读取标签中已审核的 `release-notes/releases.json`,Studio 读取对应版本的
|
||||
Markdown。同步只需要 GitHub 读取权限(本地使用 `gh auth login`,CI 使用
|
||||
`GITHUB_TOKEN`),不需要模型 API 密钥。缺失源日志时构建失败;CLI 是否公开
|
||||
仍由原有的发布完成标记校验决定。
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"prebuild": "node scripts/releases/sync-releases.mjs --studio-only && node scripts/fetch-community.mjs && node scripts/fetch-publications.mjs",
|
||||
"prebuild": "node scripts/releases/sync-releases.mjs && node scripts/fetch-community.mjs && node scripts/fetch-publications.mjs",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"test": "node --test src/scripts/*.test.mjs scripts/releases/*.test.mjs",
|
||||
|
||||
@@ -4,14 +4,16 @@ import { appendFile } from 'node:fs/promises';
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { parseStudioNotes } from '../../src/lib/studio-notes.mjs';
|
||||
import { compareVersionsDesc, loadCatalog, upsertRelease } from './release-notes.mjs';
|
||||
import { compareVersionsDesc, loadCatalog, upsertRelease, validateCatalog } from './release-notes.mjs';
|
||||
|
||||
const repository = 'esengine/DeepSeek-Reasonix';
|
||||
const root = resolve(import.meta.dirname, '../../..');
|
||||
const git = (...args) => execFileSync('git', args, { cwd: root, encoding: 'utf8' }).trim();
|
||||
|
||||
export function missingReleases(published, catalog, { studioOnly = false } = {}) {
|
||||
const existing = new Set(catalog.releases.map((r) => `${r.product || 'cli'}:${r.version}`));
|
||||
// The archive starts after the earliest GitHub tags. Do not require source
|
||||
// catalogs from releases that predate the website's supported history.
|
||||
const oldestCLI = catalog.releases.filter((r) => r.product !== 'studio' && r.channel === 'stable')
|
||||
.map((r) => r.version).sort(compareVersionsDesc).at(-1);
|
||||
const missing = new Map();
|
||||
for (const release of published) {
|
||||
if (release.draft || !release.published_at) continue;
|
||||
@@ -21,6 +23,7 @@ export function missingReleases(published, catalog, { studioOnly = false } = {})
|
||||
const product = studio ? 'studio' : 'cli';
|
||||
if (studioOnly && product !== 'studio') continue;
|
||||
const version = (studio || cli)[1];
|
||||
if (product === 'cli' && oldestCLI && compareVersionsDesc(version, oldestCLI) > 0) continue;
|
||||
const key = `${product}:${version}`;
|
||||
if (existing.has(key)) continue;
|
||||
// Prefer the canonical CLI release over duplicate desktop/package tags.
|
||||
@@ -67,6 +70,22 @@ export async function readStudioSource(release, api) {
|
||||
return { sha, markdown };
|
||||
}
|
||||
|
||||
export async function readCLISource(release, api) {
|
||||
const { sha } = JSON.parse(await api(`commits/${release.tag_name}`));
|
||||
if (!/^[0-9a-f]{40}$/.test(sha)) throw new Error(`Invalid source commit for ${release.tag_name}`);
|
||||
const catalog = JSON.parse(await api(`contents/release-notes/releases.json?ref=${sha}`, true));
|
||||
const record = catalog.releases?.find((r) => r.version === release.version && r.product !== 'studio');
|
||||
if (!record) throw new Error(`Missing CLI source for ${release.tag_name}`);
|
||||
validateCatalog({ schemaVersion: catalog.schemaVersion, releases: [record] });
|
||||
// Keep reviewed records gated by fetch-publications.mjs and its all-surface
|
||||
// marker. A GitHub release alone must not promote them to published.
|
||||
return record;
|
||||
}
|
||||
|
||||
export function requireSync(result) {
|
||||
if (result.pending.length) throw new Error(`Changelog sync incomplete: ${result.pending.join(', ')}`);
|
||||
}
|
||||
|
||||
export function requireStudioSync(result, catalog, expectedVersion) {
|
||||
if (result.pending.length) throw new Error(`Studio changelog sync incomplete: ${result.pending.join(', ')}`);
|
||||
const version = expectedVersion?.replace(/^v/, '');
|
||||
@@ -75,7 +94,7 @@ export function requireStudioSync(result, catalog, expectedVersion) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function syncCatalog({ catalog, published, studioOnly = false, readStudio, save, generateCLI, hasModelKey }) {
|
||||
export async function syncCatalog({ catalog, published, studioOnly = false, readStudio, readCLI, save }) {
|
||||
const added = [], pending = [], failures = [];
|
||||
for (const release of missingReleases(published, catalog, { studioOnly })) {
|
||||
const key = `${release.product}:${release.version}`;
|
||||
@@ -84,9 +103,7 @@ export async function syncCatalog({ catalog, published, studioOnly = false, read
|
||||
const { markdown, sha } = await readStudio(release);
|
||||
await save(studioRecord(release, markdown, sha));
|
||||
} else {
|
||||
if (!hasModelKey) { pending.push(key); failures.push({ key, reason: 'no model key configured' }); continue; }
|
||||
const generated = await generateCLI(release);
|
||||
await save({ ...generated, status: 'published' });
|
||||
await save(await readCLI(release));
|
||||
}
|
||||
added.push(key);
|
||||
} catch (error) {
|
||||
@@ -107,18 +124,14 @@ async function main() {
|
||||
const result = await syncCatalog({
|
||||
catalog: await loadCatalog(), published, studioOnly,
|
||||
readStudio: (release) => readStudioSource(release, api),
|
||||
save: (release) => upsertRelease(release), hasModelKey: Boolean(process.env.DEEPSEEK_API_KEY),
|
||||
generateCLI: async (release) => {
|
||||
const releaseTag = git('rev-parse', '--verify', `refs/tags/${release.tag_name}^{commit}`);
|
||||
// Each range ends at the published tag, never today's product branch tip.
|
||||
execFileSync(process.execPath, ['site/scripts/releases/generate-release-notes.mjs', '--version', release.version, '--to', releaseTag, '--tag', release.tag_name, '--date', release.published_at.slice(0, 10)], { cwd: root, stdio: 'inherit' });
|
||||
return (await loadCatalog()).releases.find((r) => r.version === release.version && r.product !== 'studio');
|
||||
},
|
||||
save: (release) => upsertRelease(release),
|
||||
readCLI: (release) => readCLISource(release, api),
|
||||
});
|
||||
const summary = `## Website changelog sync\n\nAdded: ${result.added.join(', ') || 'none'}\n\nPending (retried next run): ${result.pending.join(', ') || 'none'}\n${result.failures.map((f) => `\n- ${f.key}: ${f.reason}`).join('')}\n`;
|
||||
console.log(summary);
|
||||
if (process.env.GITHUB_STEP_SUMMARY) await appendFile(process.env.GITHUB_STEP_SUMMARY, summary);
|
||||
if (studioOnly) requireStudioSync(result, await loadCatalog(), process.env.STUDIO_RELEASE_VERSION);
|
||||
requireSync(result);
|
||||
requireStudioSync(result, await loadCatalog(), process.env.STUDIO_RELEASE_VERSION);
|
||||
}
|
||||
|
||||
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { test } from 'node:test';
|
||||
import { describeFailure, missingReleases, readStudioSource, requireStudioSync, studioRecord, syncCatalog } from './sync-releases.mjs';
|
||||
import { describeFailure, missingReleases, readCLISource, readStudioSource, requireSync, requireStudioSync, studioRecord, syncCatalog } from './sync-releases.mjs';
|
||||
import { loadCatalog, validateCatalog, renderGitHubRelease } from './release-notes.mjs';
|
||||
|
||||
const published = (tag, extra = {}) => ({ tag_name: tag, draft: false, prerelease: false, published_at: '2026-09-30T12:00:00Z', ...extra });
|
||||
@@ -34,18 +34,19 @@ test('Studio preserves Chinese source verbatim and validates without CLI targeti
|
||||
assert.throws(() => validateCatalog({ schemaVersion: 1, releases: [{ ...release, sourceNotes: undefined }] }));
|
||||
});
|
||||
|
||||
test('batch sync continues Studio backfill without a model key and reports CLI versions for retry', async () => {
|
||||
test('batch sync imports reviewed CLI and Studio source without model generation', async () => {
|
||||
const catalog = await cliOnlyCatalog();
|
||||
const saved = [];
|
||||
const result = await syncCatalog({
|
||||
catalog, published: [published('v1.99.0'), published('studio-v2.24.0')],
|
||||
readStudio: async () => ({ sha: 'a'.repeat(40), markdown: '中立更新。\n' }),
|
||||
save: async (release) => saved.push(release), generateCLI: async () => { throw new Error('must not call'); },
|
||||
hasModelKey: false,
|
||||
save: async (release) => saved.push(release),
|
||||
readCLI: async () => ({ version: '1.99.0', status: 'reviewed' }),
|
||||
});
|
||||
assert.deepEqual(saved.map((r) => r.version), ['2.24.0']);
|
||||
assert.deepEqual(result.pending, ['cli:1.99.0']);
|
||||
assert.deepEqual(result.added, ['studio:2.24.0']);
|
||||
assert.deepEqual(saved.map((r) => r.version), ['1.99.0', '2.24.0']);
|
||||
assert.equal(saved[0].status, 'reviewed');
|
||||
assert.deepEqual(result.pending, []);
|
||||
assert.deepEqual(result.added, ['cli:1.99.0', 'studio:2.24.0']);
|
||||
});
|
||||
|
||||
test('existing catalog entries are never regenerated', async () => {
|
||||
@@ -74,21 +75,21 @@ test('CLI range selection uses the earlier CLI release even with newer Studio an
|
||||
] }, '1.39.3').version, '1.39.2');
|
||||
});
|
||||
|
||||
test('an upstream published CLI release becomes public only through the saved review draft', async () => {
|
||||
test('an upstream published CLI release preserves its publication-marker gate', async () => {
|
||||
const saved = [];
|
||||
const result = await syncCatalog({
|
||||
catalog: await loadCatalog(), published: [published('v1.99.0')], hasModelKey: true,
|
||||
generateCLI: async () => ({ version: '1.99.0', product: 'cli', status: 'reviewed' }),
|
||||
catalog: await loadCatalog(), published: [published('v1.99.0')],
|
||||
readCLI: async () => ({ version: '1.99.0', product: 'cli', status: 'reviewed' }),
|
||||
save: async (release) => saved.push(release),
|
||||
});
|
||||
assert.deepEqual(result.added, ['cli:1.99.0']);
|
||||
assert.equal(saved[0].status, 'published');
|
||||
assert.equal(saved[0].status, 'reviewed');
|
||||
});
|
||||
|
||||
test('failure reasons carry the error class and a message but never secrets', async () => {
|
||||
const result = await syncCatalog({
|
||||
catalog: await loadCatalog(), published: [published('v1.99.0')], hasModelKey: true,
|
||||
generateCLI: async () => { throw new TypeError('request failed with Bearer abc123token and key sk-live-999\nstack line'); },
|
||||
catalog: await loadCatalog(), published: [published('v1.99.0')],
|
||||
readCLI: async () => { throw new TypeError('request failed with Bearer abc123token and key sk-live-999\nstack line'); },
|
||||
save: async () => {},
|
||||
});
|
||||
assert.deepEqual(result.pending, ['cli:1.99.0']);
|
||||
@@ -139,11 +140,62 @@ test('a failed or incomplete Studio sync blocks publication of a stale catalog',
|
||||
assert.throws(() => requireStudioSync({ pending: ['studio:2.45.0'] }, catalog), /studio:2\.45\.0/);
|
||||
});
|
||||
|
||||
test('production builds synchronize Studio before rendering and deployments require the website branch', async () => {
|
||||
test('production builds synchronize both products before rendering and deployments require the website branch', async () => {
|
||||
const pkg = JSON.parse(await readFile(new URL('../../package.json', import.meta.url), 'utf8'));
|
||||
assert.ok(pkg.scripts.prebuild.startsWith('node scripts/releases/sync-releases.mjs --studio-only &&'));
|
||||
assert.ok(pkg.scripts.prebuild.startsWith('node scripts/releases/sync-releases.mjs &&'));
|
||||
const workflow = await readFile(new URL('../../../.github/workflows/pages.yml', import.meta.url), 'utf8');
|
||||
assert.match(workflow, /workflow_dispatch:\n inputs:/);
|
||||
assert.match(workflow, /if: github\.ref == 'refs\/heads\/website'/);
|
||||
assert.match(workflow, /STUDIO_RELEASE_VERSION: \$\{\{ inputs\.studio_version \}\}/);
|
||||
});
|
||||
|
||||
test('CLI sources use exact published tag commits and preserve reviewed bilingual content', async () => {
|
||||
const catalog = await loadCatalog();
|
||||
const record = catalog.releases.find((r) => r.version === '1.39.7');
|
||||
const sha = 'c'.repeat(40);
|
||||
for (const tag of ['v1.39.7', 'desktop-v1.39.7']) {
|
||||
const result = await readCLISource({ ...published(tag), version: '1.39.7' }, async (endpoint, raw) => {
|
||||
if (endpoint === `commits/${tag}`) return JSON.stringify({ sha });
|
||||
assert.equal(endpoint, `contents/release-notes/releases.json?ref=${sha}`);
|
||||
assert.equal(raw, true);
|
||||
return JSON.stringify({ schemaVersion: 1, releases: [record] });
|
||||
});
|
||||
assert.deepEqual(result, record);
|
||||
}
|
||||
});
|
||||
|
||||
test('missing or invalid CLI source fails closed while independent Studio sync continues', async () => {
|
||||
const release = { ...published('v1.99.0'), version: '1.99.0' };
|
||||
const api = async (endpoint) => endpoint.startsWith('commits/')
|
||||
? JSON.stringify({ sha: 'd'.repeat(40) })
|
||||
: JSON.stringify({ schemaVersion: 1, releases: [] });
|
||||
const result = await syncCatalog({
|
||||
catalog: await cliOnlyCatalog(), published: [release, published('studio-v2.99.0')],
|
||||
readCLI: (r) => readCLISource(r, api),
|
||||
readStudio: async () => ({ sha: 'e'.repeat(40), markdown: '更新说明。' }),
|
||||
save: async () => {},
|
||||
});
|
||||
assert.deepEqual(result.pending, ['cli:1.99.0']);
|
||||
assert.deepEqual(result.added, ['studio:2.99.0']);
|
||||
assert.throws(() => requireSync(result), /cli:1\.99\.0/);
|
||||
await assert.rejects(readCLISource(release, async () => JSON.stringify({ sha: 'invalid' })), /Invalid source commit/);
|
||||
await assert.rejects(readCLISource(release, async (endpoint) => endpoint.startsWith('commits/')
|
||||
? JSON.stringify({ sha: 'd'.repeat(40) })
|
||||
: JSON.stringify({ schemaVersion: 1, releases: [{ version: '1.99.0' }] })), /date/);
|
||||
});
|
||||
|
||||
test('build sync respects the archive history boundary and never imports unpublished CLI drafts', async () => {
|
||||
const catalog = { schemaVersion: 1, releases: [{ version: '1.39.7', channel: 'stable' }] };
|
||||
const releases = [published('v1.0.0'), published('v1.39.8'), published('desktop-v1.39.8'),
|
||||
published('v1.39.9'), published('v1.39.10', { draft: true }), published('v1.39.11', { prerelease: true })];
|
||||
const run = () => syncCatalog({
|
||||
catalog, published: releases,
|
||||
readCLI: async (r) => ({ version: r.version, channel: 'stable', status: 'reviewed' }),
|
||||
save: async (r) => catalog.releases.unshift(r),
|
||||
});
|
||||
assert.deepEqual((await run()).added, ['cli:1.39.8', 'cli:1.39.9']);
|
||||
assert.deepEqual(catalog.releases.map((r) => r.version), ['1.39.9', '1.39.8', '1.39.7']);
|
||||
const repeated = await run();
|
||||
assert.deepEqual(repeated, { added: [], pending: [], failures: [] });
|
||||
assert.doesNotThrow(() => requireSync(repeated));
|
||||
});
|
||||
@@ -2601,6 +2601,482 @@
|
||||
"download": "https://reasonix.io/#start"
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "1.39.9",
|
||||
"date": "2026-10-20",
|
||||
"channel": "stable",
|
||||
"targetingVersion": 1,
|
||||
"title": {
|
||||
"en": "Reasonix 1.39.9",
|
||||
"zh": "Reasonix 1.39.9"
|
||||
},
|
||||
"summary": {
|
||||
"en": "Fixes for replies that ended with only thinking, large file attachments ending the desktop service, sending being blocked after a manual compaction, tool calls cut off mid-stream treated as finished, Studio's imported sessions listed as extras in 1.x, duplicate recovered conversations, stale recovery status, goal work time stuck at zero, ! commands missing login-shell programs, remote session badges, and settings that disagreed with model capabilities.",
|
||||
"zh": "修复只有思考内容的回复被当作完成、附加大文件让桌面服务退出、手动压缩后无法发送、流中途被截断的工具调用被当作完成、Studio 导入的会话在 1.x 里显示成多余会话、恢复出来的对话重复出现、恢复状态残留、目标模式工作时长一直为 0、! 命令找不到登录 shell 的程序、远程会话徽标,以及设置与模型实际能力不一致的问题。"
|
||||
},
|
||||
"surfaces": [
|
||||
"desktop",
|
||||
"cli"
|
||||
],
|
||||
"guides": [],
|
||||
"highlights": [
|
||||
{
|
||||
"kind": "fixed",
|
||||
"targets": [
|
||||
"desktop",
|
||||
"cli"
|
||||
],
|
||||
"title": {
|
||||
"en": "A reply that contained only thinking now gets one retry, then a clear error",
|
||||
"zh": "只有思考内容的回复现在会重试一次,仍无答案时给出明确提示"
|
||||
},
|
||||
"body": {
|
||||
"en": "Some models ended a turn with only reasoning and no answer or tool call, and the turn was shown as completed, so you were left with a row of thinking and nothing else. The host now asks the model once more to give its answer, keeping any tool results already collected. If that also produces no answer, the turn fails with an error in English, Simplified Chinese or Traditional Chinese that says no answer was produced, instead of looking finished, and the error is still there when you reopen the session. Cancelling and the usual limits behave as before. Exported diagnostics also gain short, content-free response summaries (finish reason, sizes, tool counts) to help report such cases; they never enter the model's context.",
|
||||
"zh": "有些模型只输出思考过程、没有答案也没有工具调用,这一轮却显示为已完成,你只会看到一行思考。现在会再请模型给出一次答案,已经取得的工具结果保留;如果仍然没有答案,这一轮会以明确的错误结束(支持英文、简体中文、繁体中文),说明没有产生答案,不再看起来像是完成了,重新打开会话时这条错误依然在。取消和各项上限的行为不变。导出的诊断信息还会增加不含内容的简短回复摘要(结束原因、大小、工具数量),方便反馈这类问题;它们不会进入模型上下文。"
|
||||
},
|
||||
"refs": [
|
||||
13966
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "fixed",
|
||||
"targets": [
|
||||
"desktop"
|
||||
],
|
||||
"title": {
|
||||
"en": "Attaching a very large file no longer shuts down the desktop service",
|
||||
"zh": "附加超大文件不再让桌面服务退出"
|
||||
},
|
||||
"body": {
|
||||
"en": "Attaching a large file (an APK, an archive) could end the desktop service, because the whole file was read into memory and encoded before any size check. The file picker, paste and drop now attach the file by its path when it has one, without copying it. A file that only exists as data in the page (some pastes and drops) is streamed to disk instead of being held in memory, up to a safety ceiling of 1 GiB per upload; set [attachments] max_stream_bytes in your own config.toml to change it (0 removes the ceiling). Project configuration cannot change it. A file over the limit is refused before it is read, with a message naming the file and the limit. Images keep their 64 MB limit.",
|
||||
"zh": "附加大文件(比如安装包、压缩包)可能让桌面服务退出,因为整份文件会先被读进内存并编码,之后才检查大小。现在文件选择、粘贴和拖入只要文件有路径,就直接按路径附加,不再复制一份;只以数据形式存在于页面里的文件(部分粘贴和拖入)会流式写入磁盘,不再整份放在内存里,每次上传的安全上限默认为 1 GiB,可以在你自己的 config.toml 里用 [attachments] max_stream_bytes 调整(设为 0 即取消上限),项目配置不能修改它。超过限制的文件会在读取之前被拒绝,提示里会写明文件名和限制。图片仍保持 64 MB 的限制。"
|
||||
},
|
||||
"refs": [
|
||||
13543,
|
||||
12418
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "fixed",
|
||||
"targets": [
|
||||
"desktop"
|
||||
],
|
||||
"title": {
|
||||
"en": "Sending after a manual compaction works again, and the compact threshold applies at once",
|
||||
"zh": "手动压缩后可以继续发送,压缩阈值修改后立即生效"
|
||||
},
|
||||
"body": {
|
||||
"en": "After compacting a conversation by hand the next message could be blocked and the model could not be reached. That is fixed. Changing the compaction threshold in Settings now applies to the open session straight away without replacing it, and changing unrelated preferences no longer restarts the session's runtime. Sub-agents that were already running keep the threshold they started with; new ones use the new value. When a session is replaced for another reason, the read-only access you granted to folders outside the project is carried over unchanged and no new access is granted.",
|
||||
"zh": "手动压缩对话之后,下一条消息可能被挡住、无法与模型沟通,现在已修复。在设置里修改压缩阈值会立即作用于正在打开的会话,不需要替换它;修改无关的偏好也不再重启会话的运行环境。已经在运行的子代理保持启动时的阈值,新建的子代理使用新值。会话因其他原因被替换时,你授予的项目之外文件夹的只读访问会原样保留,不会新增任何访问权限。"
|
||||
},
|
||||
"refs": [
|
||||
12501
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "fixed",
|
||||
"targets": [
|
||||
"desktop"
|
||||
],
|
||||
"title": {
|
||||
"en": "Sessions no longer become unreadable when their integrity index files are lost",
|
||||
"zh": "会话的完整性索引文件丢失后,不再让所有会话打不开"
|
||||
},
|
||||
"body": {
|
||||
"en": "Each stored attachment or history block has a small integrity index next to it. If those index files went missing (for example after a cleanup or another program removed them) while the content itself was intact, every conversation failed to load with a transport error. The index is now rebuilt from the content on the first read, and only when both the content and the rebuilt index match the checksums recorded with the session; damaged content is still refused.",
|
||||
"zh": "每个附件或历史块旁边都有一个很小的完整性索引文件。如果这些索引文件丢失(比如清理之后,或被其他程序删除),而内容本身完好,所有会话都会因为传输错误而打不开。现在第一次读取时会从内容重建索引,并且只有内容和重建出的索引都与会话里记录的校验值一致才会接受;内容损坏时仍然拒绝。"
|
||||
},
|
||||
"refs": [
|
||||
14018,
|
||||
14019
|
||||
]
|
||||
}
|
||||
],
|
||||
"changes": {
|
||||
"new": [],
|
||||
"improved": [],
|
||||
"fixed": [
|
||||
{
|
||||
"targets": [
|
||||
"desktop"
|
||||
],
|
||||
"title": {
|
||||
"en": "Model settings agree with what each model can do and keep your defaults",
|
||||
"zh": "模型设置与各模型的实际能力一致,并保留你的默认值"
|
||||
},
|
||||
"body": {
|
||||
"en": "Choosing an image-understanding model now accepts the models Reasonix already knows can read images (built-in or discovered), while a model you explicitly disabled, or an official endpoint that restricts it, is still refused. Sub-agent reasoning choices come from each model's own capabilities, and a stale choice can be cleared. A planner you assigned stays visible even when it is the same as the default model, and choosing Follow conversation model clears it. Renaming a connection or editing other unrelated fields no longer resets its default model and reasoning effort.",
|
||||
"zh": "选择图片理解模型时,现在会接受 Reasonix 已知能读图的模型(内置的或探测到的);你明确停用的模型,或官方端点有限制的,仍会被拒绝。子代理的推理强度选项来自各模型自己的能力,过期的选择可以清除。你指定的规划模型即使与默认模型相同也会保持可见,选择「跟随对话模型」会清除这项指定。重命名连接或修改其他无关字段,不再重置它的默认模型和默认推理强度。"
|
||||
},
|
||||
"refs": [
|
||||
14005
|
||||
]
|
||||
},
|
||||
{
|
||||
"targets": [
|
||||
"desktop",
|
||||
"cli"
|
||||
],
|
||||
"title": {
|
||||
"en": "A tool call cut off mid-stream is no longer taken for a finished answer",
|
||||
"zh": "工具调用在流中途被截断时,不再被当作已完成的回答"
|
||||
},
|
||||
"body": {
|
||||
"en": "With models served over the Responses protocol, a stream that ended while a tool call was still open, or that carried a malformed call, could be accepted as a normal answer: the turn finished with only the text before the call, or a neighbouring call ran. Such a stream is now refused as a protocol error and nothing from that attempt is executed or saved as a final answer. It is not retried automatically; you can continue the conversation yourself. Ordinary text-only answers and complete calls are unaffected.",
|
||||
"zh": "使用 Responses 协议的模型,如果流在工具调用尚未完成时就结束,或带着格式错误的调用,以前可能被当作正常回答接受:这一轮只留下调用之前的文字,或者相邻的另一个调用被执行。现在这样的流会作为协议错误被拒绝,该次尝试里没有任何东西被执行,也不会存成最终回答。它不会自动重试,你可以自己继续对话。普通的纯文字回答和完整的调用不受影响。"
|
||||
},
|
||||
"refs": [
|
||||
12526
|
||||
]
|
||||
},
|
||||
{
|
||||
"targets": [
|
||||
"desktop",
|
||||
"cli"
|
||||
],
|
||||
"title": {
|
||||
"en": "Studio's imported sessions no longer appear as extra sessions in 1.x",
|
||||
"zh": "Studio 导入的会话不再在 1.x 里显示成多余的会话"
|
||||
},
|
||||
"body": {
|
||||
"en": "Studio 2.x writes the sessions it imports into the project's sessions folder as files named v4-<id>.jsonl. 1.x listed those files as ordinary sessions titled by file name (111 extra rows in one report), duplicating conversations it already had. 1.x now treats any file whose name starts with v4- as Studio's: it is not listed, and 1.x's cleanup of stale lock and lease files leaves it alone.",
|
||||
"zh": "Studio 2.x 把它导入的会话以 v4-<id>.jsonl 的文件名写进项目的会话文件夹。1.x 之前会把这些文件当作普通会话、用文件名作标题列出来(有报告称多出 111 行),与它本来就有的对话重复。现在 1.x 把文件名以 v4- 开头的文件视为 Studio 的:不再列出,1.x 清理过期锁文件和租约文件时也不会碰它们。"
|
||||
},
|
||||
"refs": [
|
||||
13444
|
||||
]
|
||||
},
|
||||
{
|
||||
"targets": [
|
||||
"desktop"
|
||||
],
|
||||
"title": {
|
||||
"en": "Recovered older conversations no longer appear as duplicates or change places in the sidebar",
|
||||
"zh": "恢复出来的旧对话不再重复出现,也不再在侧栏里变换身份"
|
||||
},
|
||||
"body": {
|
||||
"en": "After an update, or when the session list was rebuilt on a fresh cache, copies made by conflict recovery could be listed as separate conversations (13 conversations became 25 rows in one report). The copies a parent already covers are now hidden again once the check has run in the background. A recovered conversation also keeps one identity in the sidebar after its content is verified or its parent is deleted, and a rebuild or shutdown of the session list no longer waits on a job that was already replaced. Nothing in your session files is rewritten.",
|
||||
"zh": "更新之后,或会话列表在全新缓存上重建时,冲突恢复产生的副本可能被当成单独的对话列出来(有报告里 13 个对话变成 25 行)。现在后台检查完成后,已被其父会话覆盖的副本会重新隐藏。恢复出来的对话在内容校验或父会话被删除之后,也在侧栏里保持同一个身份;会话列表重建或退出时,不再等待一个已被替换的任务。不会改写你的任何会话文件。"
|
||||
},
|
||||
"refs": [
|
||||
12822,
|
||||
13594
|
||||
]
|
||||
},
|
||||
{
|
||||
"targets": [
|
||||
"desktop"
|
||||
],
|
||||
"title": {
|
||||
"en": "The recovery status clears after an import retry succeeds",
|
||||
"zh": "导入重试成功后,恢复状态不再残留"
|
||||
},
|
||||
"body": {
|
||||
"en": "When an older session failed to import because of a workspace conflict and a later retry (or replay of an already completed import) succeeded, the earlier recovery status could stay behind. It is now cleared once the import is verified for exactly that source, version and target; failures of other sources stay pending. This does not bring back sessions that were never imported.",
|
||||
"zh": "旧会话因工作区冲突导入失败,之后重试(或重放一次已完成的导入)成功时,之前的恢复状态可能一直留着。现在一旦确认这次导入对应的正是那个来源、版本和目标,就会清除它;其他来源的失败仍保持待处理。这并不会找回从未导入过的会话。"
|
||||
},
|
||||
"refs": [
|
||||
12503
|
||||
]
|
||||
},
|
||||
{
|
||||
"targets": [
|
||||
"desktop"
|
||||
],
|
||||
"title": {
|
||||
"en": "Goal runs show their accumulated working time",
|
||||
"zh": "目标模式会显示累计的工作时长"
|
||||
},
|
||||
"body": {
|
||||
"en": "A goal that had worked through several automatic rounds still showed 0s of work time. The time spent actively working on the goal is now added up (pauses and time the app was closed do not count), kept with the session, and shown again after a restart. It is not given to the model.",
|
||||
"zh": "一个已经自动进行了好几轮的目标,工作时长仍显示 0s。现在会累计在目标上实际工作的时间(暂停期间和应用关闭的时间不算),随会话保存,重启后仍会显示。这个数值不会提供给模型。"
|
||||
},
|
||||
"refs": [
|
||||
12502
|
||||
]
|
||||
},
|
||||
{
|
||||
"targets": [
|
||||
"desktop"
|
||||
],
|
||||
"title": {
|
||||
"en": "Commands starting with ! find the programs on your login shell's PATH",
|
||||
"zh": "以 ! 开头的命令能找到登录 shell 的 PATH 里的程序"
|
||||
},
|
||||
"body": {
|
||||
"en": "When the desktop app is launched from the Dock or a similar launcher, it does not inherit the PATH your login shell sets up in .zshrc or .bashrc, so a command typed with ! could not find tools installed there. The login shell's PATH is now read before such a command runs and merged with the inherited one, keeping the existing entries and dropping duplicates; if the login shell cannot be queried, the inherited PATH is used as before.",
|
||||
"zh": "从程序坞等启动器打开桌面应用时,它不会继承你在 .zshrc 或 .bashrc 里设置的登录 shell 的 PATH,所以用 ! 输入的命令找不到装在那里的工具。现在运行这类命令之前会读取登录 shell 的 PATH,并与继承来的 PATH 合并,保留原有项并去重;如果无法查询登录 shell,就像以前一样使用继承来的 PATH。"
|
||||
},
|
||||
"refs": [
|
||||
13927
|
||||
]
|
||||
},
|
||||
{
|
||||
"targets": [
|
||||
"desktop",
|
||||
"cli"
|
||||
],
|
||||
"title": {
|
||||
"en": "Remote sessions show an unread dot, and switching sessions no longer shows false connection warnings",
|
||||
"zh": "远程会话也会显示未读圆点,切换会话不再出现误报的连接警告"
|
||||
},
|
||||
"body": {
|
||||
"en": "A remote session now behaves like a local one: when a response finishes in the background an unread dot appears, and it clears once you read it. Session lists refresh when a background session finishes or ends, whether or not the one in front is running. Ordinary session switches no longer leave a false connection warning, a late reply from the session you switched away from can no longer overwrite the one in front, and real connection failures stay visible and clear on their own once the connection recovers. Update the remote Serve as well as the desktop app for the full behaviour; with an older Serve the dots fall back to timestamps.",
|
||||
"zh": "远程会话现在和本地会话表现一致:后台的回复完成时会出现未读圆点,读过之后消失。后台会话完成或结束时会刷新会话列表,不论前台那个是否正在运行。普通的会话切换不再留下误报的连接警告,切走的会话迟到的回复也不会覆盖前台的状态,真实的连接失败仍然可见,连接恢复后会自行消除。要获得完整效果,需要同时更新远程的 Serve 和桌面应用;Serve 较旧时,圆点会退回按时间戳判断。"
|
||||
},
|
||||
"refs": [
|
||||
11513
|
||||
]
|
||||
},
|
||||
{
|
||||
"targets": [
|
||||
"desktop",
|
||||
"cli"
|
||||
],
|
||||
"title": {
|
||||
"en": "Go toolchain and network library updated to versions with published security fixes",
|
||||
"zh": "Go 工具链和网络库升级到已发布安全修复的版本"
|
||||
},
|
||||
"body": {
|
||||
"en": "The Go toolchain moves to 1.26.9 and golang.org/x/net to 0.60.0 (with the golang.org/x modules it requires), which clear the standard-library and x/net vulnerabilities the vulnerability scan reported on this line. The scan reports none reachable from Reasonix afterwards.",
|
||||
"zh": "Go 工具链升级到 1.26.9,golang.org/x/net 升级到 0.60.0(连同它要求的 golang.org/x 各模块),修复了漏洞扫描在这条线上报出的标准库和 x/net 的漏洞;升级之后,扫描没有再报出 Reasonix 可达的漏洞。"
|
||||
},
|
||||
"refs": [
|
||||
12951
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"upgrade": [],
|
||||
"risks": [
|
||||
{
|
||||
"targets": [
|
||||
"desktop"
|
||||
],
|
||||
"title": {
|
||||
"en": "A recovered group's sidebar id can change once",
|
||||
"zh": "恢复组在侧栏里的标识可能变化一次"
|
||||
},
|
||||
"body": {
|
||||
"en": "Repairing an older cached identity for a recovered conversation group whose records carry no topic id can change that group's id once. An id written explicitly in your session files, and the files themselves, are never changed.",
|
||||
"zh": "修复旧缓存里没有主题标识的恢复对话组的身份时,该组的标识可能变化一次。你的会话文件里显式写下的标识以及文件本身都不会被改动。"
|
||||
},
|
||||
"refs": [
|
||||
13594
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributors": [
|
||||
"SivanCola",
|
||||
"PeterHepta",
|
||||
"XTLine",
|
||||
"xxoingr"
|
||||
],
|
||||
"links": {
|
||||
"github": "https://github.com/esengine/DeepSeek-Reasonix/releases/tag/desktop-v1.39.9",
|
||||
"compare": "https://github.com/esengine/DeepSeek-Reasonix/compare/desktop-v1.39.8...desktop-v1.39.9",
|
||||
"download": "https://reasonix.io/?download=desktop&channel=stable#start"
|
||||
},
|
||||
"releaseId": "1.39.9",
|
||||
"baseVersion": "1.39.9",
|
||||
"status": "reviewed",
|
||||
"previousRelease": "1.39.8",
|
||||
"builds": {
|
||||
"cli": "v1.39.9",
|
||||
"desktop": "v1.39.9",
|
||||
"npm": "1.39.9"
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "1.39.8",
|
||||
"date": "2026-10-07",
|
||||
"channel": "stable",
|
||||
"targetingVersion": 1,
|
||||
"title": {
|
||||
"en": "Reasonix 1.39.8",
|
||||
"zh": "Reasonix 1.39.8"
|
||||
},
|
||||
"summary": {
|
||||
"en": "Fixes for pasted images and tool-result images blocking every later message on models that never declared image support, sessions that could not be opened after an interrupted reply, QQ approval requests that never arrived, session delete and archive failing in projects shared with Studio, archiving a legacy recovery chain in one action, sub-agent tool calls showing up in the main conversation, and an intermittent Command Code error.",
|
||||
"zh": "修复未声明读图能力的模型被图片卡住后续所有消息、回复被中断后会话无法打开、QQ 审批请求收不到、与 Studio 共用的项目里删除和归档会话反复失败,旧版恢复链一次归档,子代理工具调用出现在主对话里,以及 Command Code 的一个偶发错误。"
|
||||
},
|
||||
"surfaces": [
|
||||
"desktop",
|
||||
"cli"
|
||||
],
|
||||
"guides": [],
|
||||
"highlights": [
|
||||
{
|
||||
"kind": "fixed",
|
||||
"targets": [
|
||||
"desktop"
|
||||
],
|
||||
"title": {
|
||||
"en": "Deleting or archiving a session no longer fails over and over after the catalog starts rescanning",
|
||||
"zh": "目录反复重扫时,删除或归档会话不再一直失败"
|
||||
},
|
||||
"body": {
|
||||
"en": "In some projects (seen when the same project folder was also opened by Studio 2.x) the session catalog rescanned the folder every few seconds even though no session had changed, and every delete or archive then failed with \"This session is already performing the same operation\". Lock files, temporary files, sidecar files and subdirectory timestamps no longer trigger a rescan; only session transcripts, their metadata and event logs do. Rescans of one folder now back off from 1 s up to 60 s, a real delete or rename still skips the backoff, and the archive path waits up to 1.5 s for a busy lock instead of giving up at once. No session file is written, moved or deleted.",
|
||||
"zh": "在部分项目里(同一个项目文件夹也被 Studio 2.x 打开过时出现),目录没有任何会话变化,却每隔几秒被重新扫描一次,随后任何删除或归档都会失败,提示「该会话正在执行相同操作」。现在锁文件、临时文件、附属文件和子目录的时间戳不再触发重扫,只有会话记录、元数据和事件日志的变化才会;同一目录的重扫会从 1 秒起逐步退避到 60 秒,真实的删除和改名不受退避影响;归档遇到忙碌的锁时最多等待 1.5 秒,不再立刻放弃。不会写入、移动或删除任何会话文件。"
|
||||
},
|
||||
"refs": [
|
||||
11693,
|
||||
11722
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "fixed",
|
||||
"targets": [
|
||||
"desktop"
|
||||
],
|
||||
"title": {
|
||||
"en": "Archiving a legacy recovery row now archives the whole recovery chain in one action",
|
||||
"zh": "归档旧版恢复行时,整条恢复链一次归档"
|
||||
},
|
||||
"body": {
|
||||
"en": "Archiving one row of an old recovery lineage used to retire only one to four snapshots, so each click left a new copy behind and the same set had to be archived dozens of times. The whole chain is now found by walking the parent links, and archived in one action. The lock is released between snapshots, so other tabs keep working while a long chain is archived. If a snapshot cannot be archived the rest still are, the result reports how many remain, and running the action again finishes it. Source files are never deleted or rewritten, and archived copies stay restorable from Archived.",
|
||||
"zh": "归档旧版恢复链里的某一行时,原来只会处理一到四个快照,每点一次都会留下新的副本,同一批要归档几十次。现在会顺着父节点找到整条恢复链,并一次归档。每个快照之间会释放锁,所以归档很长的链时,其他标签仍可正常使用。个别快照归档失败时其余照常归档,结果会报告还剩几个,再执行一次即可补完。源文件不会被删除或改写,已归档的副本仍可从「已归档」恢复。"
|
||||
},
|
||||
"refs": [
|
||||
11581,
|
||||
11717
|
||||
]
|
||||
}
|
||||
],
|
||||
"changes": {
|
||||
"new": [],
|
||||
"improved": [],
|
||||
"fixed": [
|
||||
{
|
||||
"targets": [
|
||||
"desktop",
|
||||
"cli"
|
||||
],
|
||||
"title": {
|
||||
"en": "Images are blocked only for models declared as text-only",
|
||||
"zh": "只有声明为不读图的模型才会拦截图片"
|
||||
},
|
||||
"body": {
|
||||
"en": "A model with no declared image capability (a custom gateway or a model missing from the catalog) was treated as unable to read images: attaching one failed with \"no image understanding model is configured\", and once a conversation held any image, even from a tool result such as view_image or a browser screenshot, every later message including plain text failed the same way. Only a model declared text-only (vision = false, a vision_models list that omits it, or the catalog) is now blocked; an undeclared model receives the image and the provider answers, and when a configured image model exists it is still used as before. Images from tool results and earlier messages that no model can read are replaced in the request by a short note, and the saved conversation is not changed. A new image attached on a model declared text-only is still refused. Models not declared as image-capable are now left to the provider: if the provider truly cannot read images it will still refuse, and you will need to switch to another model.",
|
||||
"zh": "没有声明读图能力的模型(自建网关,或目录里没有的模型)以前会被当作不能读图:附加图片会报「没有配置图片理解模型」,而且对话里一旦出现图片(包括 view_image、浏览器截图等工具结果里的图片),之后的每条消息,连纯文字也一样失败。现在只有明确声明为不读图的模型(vision = false、vision_models 列表里没有它,或目录里如此标注)才会被拦截;未声明的模型会直接收到图片,由服务商回答;已配置图片模型时仍按原样使用它。工具结果和历史消息里没有任何模型能读的图片,会在请求里换成一句简短的说明,已保存的对话不会被改动。在声明为不读图的模型上新附加图片仍会被拒绝。未声明读图能力的模型现在交给服务商处理:如果服务商确实不能读图,仍会拒绝,此时需要换用其他模型。"
|
||||
},
|
||||
"refs": [
|
||||
11743,
|
||||
11859,
|
||||
12302
|
||||
]
|
||||
},
|
||||
{
|
||||
"targets": [
|
||||
"desktop",
|
||||
"cli"
|
||||
],
|
||||
"title": {
|
||||
"en": "Fixed a transcript record-id collision caused by interrupted replies",
|
||||
"zh": "修复被中断的回复造成的会话记录标识冲突"
|
||||
},
|
||||
"body": {
|
||||
"en": "Two interrupted replies saved without a message id derived the same internal record identity, so the transcript was rejected as a whole and the session could not be opened. Such records now get distinct identities. Genuinely duplicated message or tool records are still rejected, and no session file is rewritten. This fixes the collision found in #10456; other causes of that report are not covered.",
|
||||
"zh": "两条被中断的回复在保存时没有消息标识,会推导出相同的内部记录标识,导致整份会话记录被拒绝、会话打不开。现在这类记录会得到各自不同的标识。真正重复的消息或工具记录仍会被拒绝,也不会改写任何会话文件。这只修复了 #10456 中发现的这一处冲突,该报告的其他成因不在此列。"
|
||||
},
|
||||
"refs": [
|
||||
10456,
|
||||
11783
|
||||
]
|
||||
},
|
||||
{
|
||||
"targets": [
|
||||
"desktop",
|
||||
"cli"
|
||||
],
|
||||
"title": {
|
||||
"en": "QQ approval buttons use the correct message format, with a plain-text fallback",
|
||||
"zh": "QQ 审批按钮改用正确的消息格式,并增加纯文本回退"
|
||||
},
|
||||
"body": {
|
||||
"en": "In QQ conversations the message with approval buttons was rejected by QQ (HTTP 500, code 50015001) while ordinary replies still went through, and it was never retried. The buttons are now sent in the format the QQ message API expects, and a rejected rich message is resent as plain text that keeps the approval instructions. Who may press a button and who may approve are unchanged.",
|
||||
"zh": "在 QQ 对话里,带审批按钮的消息会被 QQ 拒绝(HTTP 500,错误码 50015001),而普通回复照常到达,且不会重试。现在按钮会按 QQ 消息接口要求的格式发送,被拒绝的富文本消息会改用保留审批说明的纯文本重发。谁能点按钮、谁能批准保持不变。"
|
||||
},
|
||||
"refs": [
|
||||
11583
|
||||
]
|
||||
},
|
||||
{
|
||||
"targets": [
|
||||
"desktop"
|
||||
],
|
||||
"title": {
|
||||
"en": "Sub-agent tool calls stay inside their sub-agent card",
|
||||
"zh": "子代理的工具调用留在各自的子代理卡片里"
|
||||
},
|
||||
"body": {
|
||||
"en": "Tool calls made by a background sub-agent (task or fleet) appeared as separate rows in the main conversation, without arguments or results, also after reopening a session. The parent id of each call was dropped when a session is read from its transcript records; it is now kept, so the calls nest under their sub-agent card again.",
|
||||
"zh": "后台子代理(task 或 fleet)发出的工具调用会作为独立的行出现在主对话里,没有参数和结果,重新打开会话后也一样。原因是从会话记录读取时丢掉了每个调用的父级标识;现在保留了它,这些调用重新嵌套在各自的子代理卡片里。"
|
||||
},
|
||||
"refs": [
|
||||
11338,
|
||||
11690
|
||||
]
|
||||
},
|
||||
{
|
||||
"targets": [
|
||||
"desktop",
|
||||
"cli"
|
||||
],
|
||||
"title": {
|
||||
"en": "Command Code requests retry once after an intermittent anonymous 400",
|
||||
"zh": "Command Code 的请求遇到偶发的匿名 400 时会重试一次"
|
||||
},
|
||||
"body": {
|
||||
"en": "Command Code's Responses route sometimes answers an unchanged request body with an anonymous 400 that succeeds when sent again. For that host and that error shape the request is now resent once with the same body; named schema errors, other hosts and other statuses are not retried.",
|
||||
"zh": "Command Code 的 Responses 接口偶尔会对同一份请求体返回一个没有具体信息的 400,再发一次就成功。现在对这个服务商和这种错误形态,会用同一份请求体重发一次;带具体信息的参数错误、其他服务商和其他状态码都不会重试。"
|
||||
},
|
||||
"refs": [
|
||||
11617,
|
||||
11666
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"upgrade": [],
|
||||
"risks": [
|
||||
{
|
||||
"targets": [
|
||||
"desktop"
|
||||
],
|
||||
"title": {
|
||||
"en": "Archiving a very long recovery chain can take a while",
|
||||
"zh": "归档很长的恢复链可能需要一段时间"
|
||||
},
|
||||
"body": {
|
||||
"en": "Each snapshot of the chain is archived separately (about 2 MB each in the reported case), so a chain of dozens can take tens of seconds. Other work continues in the meantime; if the action reports that some snapshots remain, run it again.",
|
||||
"zh": "恢复链里的每个快照都会单独归档(报告的案例里每个约 2 MB),几十个快照可能要花几十秒。期间其他操作可以继续;如果提示还有快照未完成,再执行一次即可。"
|
||||
},
|
||||
"refs": [
|
||||
11717
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributors": [
|
||||
"aron-intframe",
|
||||
"SilenceLurker",
|
||||
"Harbor404"
|
||||
],
|
||||
"links": {
|
||||
"github": "https://github.com/esengine/DeepSeek-Reasonix/releases/tag/desktop-v1.39.8",
|
||||
"compare": "https://github.com/esengine/DeepSeek-Reasonix/compare/desktop-v1.39.7...desktop-v1.39.8",
|
||||
"download": "https://reasonix.io/?download=desktop&channel=stable#start"
|
||||
},
|
||||
"releaseId": "1.39.8",
|
||||
"baseVersion": "1.39.8",
|
||||
"status": "reviewed",
|
||||
"previousRelease": "1.39.7",
|
||||
"builds": {
|
||||
"cli": "v1.39.8",
|
||||
"desktop": "v1.39.8",
|
||||
"npm": "1.39.8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "1.39.7",
|
||||
"date": "2026-10-02",
|
||||
|
||||
Reference in new issue
Block a user