diff --git a/FEATURES.md b/FEATURES.md index e1d93431..2c582089 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -30,7 +30,7 @@ - [x] **Ask 与 Execute 工作模式**:Ask 保持只读;Execute 是用户对当前本机或 SSH 账号可用工具、进程、网络和可写路径的完整授权。 - [x] **专家与 Subagent**:支持显式专家、团队分析和最多三个只读专家并行分析;聊天先展示可逐项展开的专家完整输出,再在其下展示总 Agent 的综合结果,并随会话保存。 - [x] **角色绑定模型连接**:每个角色可继承默认模型或选择独立文本模型连接,失效连接安全回退默认模型,综合角色始终继承默认模型。 -- [x] **多协议模型配置**:支持 Anthropic Messages、OpenAI Responses、OpenAI Chat Completions、OpenAI Images 和无认证本机模型;新用户默认连接为本机 Ollama 兼容地址,不预置第三方云模型服务。升级时只替换从未配置凭据且仍完全等于旧内置值的历史默认连接,用户显式保存或加密凭据的连接保持不变。“保存并测试模型”会发送有界的真实文本或图片生成请求并校验生成结果,而不是只检查 HTTP 连通性,因此可能产生少量服务商用量费用。 +- [x] **多协议模型配置**:支持 Anthropic Messages、OpenAI Responses、OpenAI Chat Completions、OpenAI Images 和无认证本机模型;新用户默认连接为本机 Ollama 兼容地址,不预置第三方云模型服务。升级时只替换从未配置凭据且仍完全等于旧内置值的历史默认连接,用户显式保存或加密凭据的连接保持不变;已有部署仅使用兼容环境变量提供凭据时继续沿用对应的历史连接参数,通用模型环境变量仍优先。“保存并测试模型”会发送有界的真实文本或图片生成请求并校验生成结果,而不是只检查 HTTP 连通性,因此可能产生少量服务商用量费用。 - [x] **上下文用量与自动压缩**:直连模型按每次成功调用更新供应商用量,图片与工具轮次使用同一口径,供应商缺失 usage 时才回退估算;界面明确区分“本次模型调用”和“压缩后对话估算”,压缩线始终根据当前设置与所选模型窗口即时计算,不在每个对话中保存旧配置;压缩标识的前后值使用同一估算口径,运行记录仍保留各次模型调用的供应商 usage。对话与多轮工具 Agent 可在已完成调用越过阈值后自动重复压缩,规划时先为固定提示、工具定义和摘要预留预算;同一回复会分别保留 Agent 工具上下文与对话历史的压缩标识,并在应用重启或较早消息滚出本地历史窗口后继续复用摘要。 - [x] **Main-only 凭据保护**:API Key 使用系统安全存储加密,不暴露给 Renderer。密钥随对应模型连接保存,修改服务地址或临时切换为无需认证不会要求重新输入;只有用户显式清除凭据或删除连接时才移除。 - [x] **OpenCode Runtime 定制**:GoodBuddy 管理的内置 OpenCode 可发现原生 Agents、Tools、Commands、LSP、Formatters、MCP、Skills、Prompts 与 Resources;Tools 单独显示读取、文件修改、命令、网络、Agent 编排等类型、来源及 Ask/Execute 可用性,并隐藏 OpenCode 内部 `invalid` 与 GoodBuddy 临时 MCP 工具。支持保存默认 Agent、每次请求覆盖 Agent、通过原生 SDK 执行 Command、显示上下文用量并调用有总时限的原生 Compact;并发外部 Server 对话的提问使用请求级公开 ID 映射,回答不会串到其他会话。外部 OpenCode Server 只报告连接状态,不宣称原生清单可读。任意插件安装、Session Share、自动 Worktree 和 OpenCode 原生会话持久化仍不开放。 diff --git a/build/loongarch-cross/README.md b/build/loongarch-cross/README.md index 06466382..774d5b84 100644 --- a/build/loongarch-cross/README.md +++ b/build/loongarch-cross/README.md @@ -68,7 +68,7 @@ The preview: - uses a separate application name and user-data directory; - includes the direct model path and Continue runtime; -- cross-builds node-pty against Electron 42.3.0; +- cross-builds the input lockfile's node-pty version against Electron 42.3.0; - includes Koffi's LoongArch glibc binding; - does not include GoodBuddy Agent, Remote Runtime, OpenCode, or native Canvas; - is not part of the production release matrix. diff --git a/build/loongarch-cross/preview-deb.sh b/build/loongarch-cross/preview-deb.sh index 1d1640c9..74fdb028 100644 --- a/build/loongarch-cross/preview-deb.sh +++ b/build/loongarch-cross/preview-deb.sh @@ -9,7 +9,6 @@ output_root="${3:-/preview-output}" electron_version="42.3.0" electron_sha256="92b0ca0c9c18ed90166918a4ac1970266c4fa967aee9277031b3b250b905526e" koffi_version="3.1.4" -node_pty_version="1.1.0" node_gyp_version="11.4.2" preview_iteration="1" application_directory="GoodBuddy-LoongArch-Preview" @@ -68,6 +67,10 @@ project_version="$( node -p \ "require('${input_root}/package.json').version" )" +node_pty_version="$( + node -p \ + "require('${input_root}/package-lock.json').packages['node_modules/node-pty'].version" +)" preview_version="${project_version}-loong64-preview.${preview_iteration}" deb_version="${project_version}~loong64preview${preview_iteration}" artifact_name="GoodBuddy-${project_version}-linux-loong64-preview.deb" @@ -171,7 +174,7 @@ tar \ koffi_binding="${koffi_target}/linux_loong64/koffi.node" assert_loongarch_elf "${koffi_binding}" -echo "== cross-build node-pty for Electron ${electron_version} ==" +echo "== cross-build node-pty ${node_pty_version} for Electron ${electron_version} ==" node_gyp_root="${work_root}/node-gyp" npm install \ --ignore-scripts \ diff --git a/docs/development/loongarch-preview-build.md b/docs/development/loongarch-preview-build.md index 8333a7f5..b30438dc 100644 --- a/docs/development/loongarch-preview-build.md +++ b/docs/development/loongarch-preview-build.md @@ -42,7 +42,7 @@ Debian `loong64` 桌面,不代表 GoodBuddy 已将龙芯纳入正式发布支 | React 界面和主进程业务代码 | 已打包 | 生产 bundle 构建、测试、类型检查和 lint 已通过 | | 直接模型调用路径 | 已打包 | JavaScript 代码与生产依赖已包含;待龙芯真机端到端验证 | | Continue Runtime | 已打包 | CLI JavaScript 入口及 npm Runtime 已包含;待真机执行验证 | -| 本地终端能力 | 已打包 | node-pty 1.1.0 已针对 Electron 42.3.0 交叉编译并验证为 LoongArch ELF;待真机 PTY 验证 | +| 本地终端能力 | 已打包 | 输入锁文件固定的 node-pty(当前为 1.2.0-beta.15)已针对 Electron 42.3.0 交叉编译并验证为 LoongArch ELF;待真机 PTY 验证 | | Koffi glibc 原生绑定 | 已打包 | Koffi 3.1.4 LoongArch glibc binding 已校验为 LoongArch ELF | | 内置 Skills | 已打包 | 静态资源已进入 DEB | | GoodBuddy Agent | **不包含** | 当前 Agent 的正式构建和双 glibc/musl 产物契约不支持 LoongArch | @@ -198,7 +198,8 @@ SHA256SUMS 1. 校验 Electron zip 的固定 SHA-256。 2. 删除 npm 错误解析出的宿主架构可选原生包。 3. 按 lockfile integrity 校验并安装 Koffi LoongArch glibc binding。 -4. 使用上游 node-gyp 11.4.2 和 Electron 42.3.0 headers 交叉编译 node-pty。 +4. 读取输入 lockfile 固定的 node-pty 版本,并使用上游 node-gyp 11.4.2 + 和 Electron 42.3.0 headers 交叉编译。 5. 拒绝 `node_modules` 中任何不是 LoongArch ELF 的 `.node` 文件。 6. 拒绝意外进入包内的 Agent、Remote Runtime 或 OpenCode 目录。 7. 生成 `Architecture: loong64` 的 DEB,解包后再次校验关键 ELF。 diff --git a/src/main/remote-agent/agent-package-manager.ts b/src/main/remote-agent/agent-package-manager.ts index 8a8ea98d..7ce4f9f1 100644 --- a/src/main/remote-agent/agent-package-manager.ts +++ b/src/main/remote-agent/agent-package-manager.ts @@ -191,7 +191,7 @@ export class AgentPackageManager { latestVersion, updateAvailable: this.#updateAvailable( latestVersion, - record + record?.verified.descriptor.version ?? null ), remoteRuntimeVersion: record?.verified.descriptor.remoteRuntime.version ?? null, @@ -210,8 +210,27 @@ export class AgentPackageManager { async getSnapshot( options: { refresh?: boolean } = {} ): Promise { - await this.#checkCatalog() - return this.getInventory(options) + const catalogCheck = this.#checkCatalog() + const inventory = await this.getInventory(options) + await catalogCheck + return agentPackageInventorySchema.parse({ + ...inventory, + checkedAt: this.#now().toISOString(), + catalog: this.#publicCatalogState(), + entries: inventory.entries.map((entry) => { + const latestVersion = this.#latestVersion( + entry.architecture + ) + return { + ...entry, + latestVersion, + updateAvailable: this.#updateAvailable( + latestVersion, + entry.version + ) + } + }) + }) } async getExpectedCatalog( @@ -791,13 +810,13 @@ export class AgentPackageManager { #updateAvailable( latestVersion: string | null, - installed: InstalledRecord | undefined + installedVersion: string | null ): boolean { - return installed !== undefined && + return installedVersion !== null && latestVersion !== null && compareSemanticVersions( latestVersion, - installed.verified.descriptor.version + installedVersion ) > 0 } diff --git a/src/main/runtime-settings-store.test.ts b/src/main/runtime-settings-store.test.ts index f33b8ce6..6be88d0d 100644 --- a/src/main/runtime-settings-store.test.ts +++ b/src/main/runtime-settings-store.test.ts @@ -137,6 +137,27 @@ describe('RuntimeSettingsStore', () => { }) }) + it('preserves the historical connection for a legacy environment key', async () => { + const { filePath, store } = await createStore() + await store.update(settings()) + const version19 = JSON.parse( + await readFile(filePath, 'utf8') + ) as Record + version19.version = 19 + await writeFile(filePath, JSON.stringify(version19), 'utf8') + + const migrated = new RuntimeSettingsStore(filePath, cipher, { + GOODBUDDY_BIGTOKEN_API_KEY: 'legacy-key' + }) + await expect(migrated.getResolvedSettings()).resolves.toMatchObject({ + modelBaseUrl: 'https://bigtoken.ai', + modelName: 'sonnet-5', + modelProtocol: 'anthropic-messages', + modelAuthentication: 'api-key', + apiKey: 'legacy-key' + }) + }) + it('restores an exact credential-bearing snapshot after a failed activation', async () => { const { store } = await createStore() await store.update( diff --git a/src/main/runtime-settings-store.ts b/src/main/runtime-settings-store.ts index 398a5167..270c2e57 100644 --- a/src/main/runtime-settings-store.ts +++ b/src/main/runtime-settings-store.ts @@ -375,6 +375,9 @@ const encryptSavedApiKey = ( }) const platformHarnessProfileId = 'goodbuddy-platform-harness' +const historicalDefaultModelBaseUrl = 'https://bigtoken.ai' +const historicalDefaultModelName = 'sonnet-5' +const historicalDefaultModelProtocol = 'anthropic-messages' export type CredentialCipher = SettingsCredentialCipher @@ -694,9 +697,9 @@ function migrateVersion19( modelProfiles: settings.modelProfiles.map((profile) => profile.id === defaultModelProfileId && profile.name === '默认模型' && - profile.baseUrl === 'https://bigtoken.ai' && - profile.modelName === 'sonnet-5' && - profile.protocol === 'anthropic-messages' && + profile.baseUrl === historicalDefaultModelBaseUrl && + profile.modelName === historicalDefaultModelName && + profile.protocol === historicalDefaultModelProtocol && profile.authentication === 'api-key' && profile.supportsImageInput === false && profile.imageGenerationQuality === 'auto' && @@ -1480,23 +1483,47 @@ export class RuntimeSettingsStore { throw new Error(`模型连接不存在:${profile.id}`) } const environmentManaged = credential.source === 'environment' - const environmentBaseUrl = - this.environment.GOODBUDDY_MODEL_BASE_URL?.trim() || + const genericEnvironmentApiKey = + this.environment.GOODBUDDY_MODEL_API_KEY?.trim() + const legacyEnvironmentApiKey = + this.environment.GOODBUDDY_BIGTOKEN_API_KEY?.trim() + const genericEnvironmentBaseUrl = + this.environment.GOODBUDDY_MODEL_BASE_URL?.trim() + const legacyEnvironmentBaseUrl = this.environment.GOODBUDDY_BIGTOKEN_BASE_URL?.trim() - const environmentModel = - this.environment.GOODBUDDY_MODEL_NAME?.trim() || + const genericEnvironmentModel = + this.environment.GOODBUDDY_MODEL_NAME?.trim() + const legacyEnvironmentModel = this.environment.GOODBUDDY_BIGTOKEN_MODEL?.trim() + const legacyEnvironmentConnection = + environmentManaged && + !genericEnvironmentApiKey && + Boolean(legacyEnvironmentApiKey) && + !genericEnvironmentBaseUrl && + !genericEnvironmentModel + const environmentBaseUrl = + genericEnvironmentBaseUrl || legacyEnvironmentBaseUrl + const environmentModel = + genericEnvironmentModel || legacyEnvironmentModel const baseUrl = environmentManaged - ? environmentBaseUrl || defaultRuntimeSettings.modelBaseUrl + ? environmentBaseUrl || + (legacyEnvironmentConnection + ? historicalDefaultModelBaseUrl + : defaultRuntimeSettings.modelBaseUrl) : profile.baseUrl const model = environmentManaged - ? environmentModel || defaultRuntimeSettings.modelName + ? environmentModel || + (legacyEnvironmentConnection + ? historicalDefaultModelName + : defaultRuntimeSettings.modelName) : profile.modelName return { apiKey: credential.activeApiKey, baseUrl, model, - protocol: profile.protocol, + protocol: legacyEnvironmentConnection + ? historicalDefaultModelProtocol + : profile.protocol, authentication: environmentManaged ? 'api-key' : profile.authentication, diff --git a/tests/loongarch-cross.test.ts b/tests/loongarch-cross.test.ts index c7368f19..df77ef75 100644 --- a/tests/loongarch-cross.test.ts +++ b/tests/loongarch-cross.test.ts @@ -91,6 +91,12 @@ describe('LoongArch preview package contract', () => { expect(previewScript).toContain( 'assert_loongarch_elf "${node_pty_binding}"' ) + expect(previewScript).toContain( + "packages['node_modules/node-pty'].version" + ) + expect(previewScript).not.toContain( + 'node_pty_version="1.1.0"' + ) expect(previewScript).toContain( 'assert_loongarch_elf "${koffi_binding}"' )