Merge branch 'codex/prune-external-acp-deps'

This commit is contained in:
Haitao Pan 2026-03-25 17:37:37 +08:00
commit 6ea68a4846
2 changed files with 117 additions and 179 deletions

View File

@ -1,210 +1,119 @@
# 外部 ACP 接入配置脚本
# 外部 ACP Endpoint 预配置脚本
这个 how-to 对应仓库内的独立工具:
这个工具是一个**外置 pre 动作**
```bash
dart tool/configure_external_acp.dart
```
它只做一件事:生成或更新 XWorkmate 本地 `settings.yaml` 里的 `externalAcpEndpoints`,不改 Flutter 运行时代码,不碰 secrets
它只负责生成或更新 XWorkmate `settings.yaml` 里的 `externalAcpEndpoints`
前提
它**不**做这些事
- 在仓库根目录执行。
- 首次在新 clone 上使用前,先跑一次 `flutter pub get`,确保 `.dart_tool/package_config.json` 已生成。
- 不修改 Flutter runtime 代码
- 不往 `.app` bundle、DMG 或打包脚本写任何内容
- 不启动任何外部 provider、bridge、daemon 或 CLI
- 不写入 token、password、API key 等 secrets
## App Store 对齐约束
## App Store 对齐边界
这次工具链按 App Store 分发边界设计,约束如下
当前脚本按 App Store 边界收敛为“纯配置助手”
- 这是仓库外置脚本,不是 app bundle 内功能。
- `codex`、`opencode`、`supergateway`、`mcp-bridge`、`gemini-bridge` 都必须作为用户侧或运维侧前置依赖,不能打包进 XWorkmate 的 App Store 构建产物。
- 脚本只改用户态 `settings.yaml`,不改 entitlements、不改打包脚本、不往 DMG / `.app` bundle 写入任何第三方二进制或 secrets。
- 外部 bridge 进程必须由用户在安装后手动启动;这次方案不要求、也不允许 App Store 包内自动拉起这些进程。
- 脚本在 app 外运行
- 只写用户态配置文件
- 不再内置或推荐任何第三方 bridge 依赖
- Claude / Gemini 在这里只是 endpoint 槽位,不绑定特定实现
## 默认 provider 映射
如果某个 provider 以后要接入,要求是:
脚本默认会把 4 个内置 provider 写成下面的 endpoint
- 由你自行准备一个兼容的外部 endpoint
- XWorkmate 只消费 endpoint不负责拉起依赖
| Provider | Endpoint | 约定启动方式 |
| --- | --- | --- |
| Codex | `ws://127.0.0.1:9001` | `codex app-server --listen ws://127.0.0.1:9001` |
| OpenCode | `http://127.0.0.1:4096` | `opencode serve --port 4096` |
| Claude | `ws://127.0.0.1:9011` | 本地 bridge 槽位 |
| Gemini | `ws://127.0.0.1:9012` | 本地 bridge 槽位 |
## 默认 provider 槽位
注意:
| Provider | 默认 endpoint |
| --- | --- |
| Codex | `ws://127.0.0.1:9001` |
| OpenCode | `http://127.0.0.1:4096` |
| Claude | `ws://127.0.0.1:9011` |
| Gemini | `ws://127.0.0.1:9012` |
- 这里把 OpenCode 固定写成 `127.0.0.1:4096`。如果你之前记成 `27.0.0.1:4096`,这里应为 loopback 地址 `127.0.0.1:4096`
- 当前 XWorkmate external ACP 路径保存的是 provider endpoint脚本负责“写配置”不负责替你守护进程。
- `settings.yaml` 是非敏感配置源。token、password、API key 仍不应该写进去。
说明:
## 默认路径
- 这些值只是默认槽位,不代表脚本会安装或启动任何 provider。
- `Codex` / `OpenCode` 的本地地址被保留为示例默认值。
- `Claude` / `Gemini` 仅保留 endpoint 占位,不再绑定第三方桥接包说明。
脚本默认按宿主平台定位 `settings.yaml`
## macOS 路径策略
- macOS: `~/Library/Application Support/xworkmate/config/settings.yaml`
- Linux: `${XDG_CONFIG_HOME:-~/.config}/xworkmate/config/settings.yaml`
- Windows: `%APPDATA%\\xworkmate\\config\\settings.yaml`
macOS 默认增加了 App Sandbox 感知:
也可以显式传 `--settings-file` 覆盖。
- `--settings-scope auto`
优先写 `~/Library/Containers/plus.svc.xworkmate/Data/Library/Application Support/xworkmate/config/settings.yaml`
如果容器目录还不存在,再退回 `~/Library/Application Support/xworkmate/config/settings.yaml`
- `--settings-scope sandbox`
强制写 App Sandbox 容器路径
- `--settings-scope user`
强制写非沙盒用户目录路径
这让脚本既能服务 Mac App Store 安装版,也保留非沙盒构建的旧路径。
## 前置条件
- 在仓库根目录执行
- 首次在新 clone 上使用前,先跑一次 `flutter pub get`
## 常用命令
先只看计划,不落盘:
查看将使用哪个配置文件,以及要写入哪些 endpoint
```bash
dart tool/configure_external_acp.dart print
```
按默认 endpoint 落盘,并自动备份原文件:
自动路径策略写入
```bash
dart tool/configure_external_acp.dart apply
```
强制写入 Mac App Store 容器路径:
```bash
dart tool/configure_external_acp.dart apply --settings-scope sandbox
```
强制写入旧的用户目录路径:
```bash
dart tool/configure_external_acp.dart apply --settings-scope user
```
指定自定义 endpoint
```bash
dart tool/configure_external_acp.dart apply \
--codex-endpoint ws://127.0.0.1:9001 \
--opencode-endpoint http://127.0.0.1:4096 \
--claude-endpoint ws://127.0.0.1:19111 \
--gemini-endpoint ws://127.0.0.1:19112
```
只打印将要写入的 YAML不真正写文件
只打印结果 YAML不落盘
```bash
dart tool/configure_external_acp.dart apply --dry-run
```
禁用某个 provider 槽位:
禁用某个槽位:
```bash
dart tool/configure_external_acp.dart apply --disable-claude
```
## 原生 provider
### Codex
启动:
```bash
codex app-server --listen ws://127.0.0.1:9001
```
写配置:
```bash
dart tool/configure_external_acp.dart apply \
--codex-endpoint ws://127.0.0.1:9001
```
### OpenCode
启动:
```bash
opencode serve --port 4096
```
写配置:
```bash
dart tool/configure_external_acp.dart apply \
--opencode-endpoint http://127.0.0.1:4096
```
说明:
- 这是按当前约定写入的原生 OpenCode endpoint。
- XWorkmate 当前 single-agent 运行时会把 `http/https` endpoint 归一化成 `ws/wss` 连接再发 JSON-RPC 请求,所以 OpenCode 端是否完全兼容,取决于你前面的桥是否提供了 XWorkmate 当前期望的方法集。
- 这次变更不改项目代码,因此这里只负责把槽位写好,不额外实现 OpenCode 协议适配。
## 桥接 provider
### Gemini
你给出的要求是:
1. 本地起一个 `stdio` MCP server
2. 再把它桥到 XWorkmate 使用的本地 endpoint
推荐的最小链路:
1. 安装 Gemini CLI 并登录
2. 安装 `gemini-bridge`
3. 用 `supergateway` 把本地 `stdio` MCP server 暴露到 WebSocket 端口
参考命令:
```bash
npm install -g @google/gemini-cli
gemini auth login
pip install gemini-bridge
npx -y supergateway \
--stdio "uvx gemini-bridge" \
--outputTransport ws \
--port 9012 \
--messagePath /
```
然后写入:
```bash
dart tool/configure_external_acp.dart apply \
--gemini-endpoint ws://127.0.0.1:9012
```
### Claude
你给出的要求同样是:
1. 本地起一个 `stdio` bridge
2. 再桥到 XWorkmate 的本地 endpoint
按这次文档约定Claude 槽位使用:
- 本地 bridge 命令:`mcp-bridge`
- WebSocket 暴露:`supergateway`
- 本地 endpoint`ws://127.0.0.1:9011`
参考命令:
```bash
pip install mcp-bridge
mcp-bridge init
```
编辑:
```text
~/.config/mcp-bridge/config.json
```
填入你的远程 MCP URL 和认证信息后,再启动本地桥:
```bash
npx -y supergateway \
--stdio "mcp-bridge" \
--outputTransport ws \
--port 9011 \
--messagePath /
```
然后写入:
```bash
dart tool/configure_external_acp.dart apply \
--claude-endpoint ws://127.0.0.1:9011
```
## 兼容性边界
这次提交只补“配置脚本 + 使用说明”,不改 XWorkmate runtime所以边界需要说清楚
- 脚本保证 `externalAcpEndpoints` 写法正确,并保留非内置 custom provider 条目。
- 脚本不会自动拉起 `codex`、`opencode`、`supergateway`、`mcp-bridge` 或 `gemini-bridge`
- 脚本不会写入任何 secret。
- Claude / Gemini 这里走的是“本地 bridge endpoint 槽位”方案。是否能被当前 XWorkmate runtime 直接消费,取决于桥后的协议是否与当前 external ACP 路径兼容。
- 如果后续要把 Claude / Gemini 做成真正的开箱即用 provider需要再补一层项目内协议适配这次明确不做。
- 这个脚本只负责 `externalAcpEndpoints`
- 它会保留非内置 custom provider 条目
- 它不会判断某个 endpoint 背后是否真的可用
- 它不会绕过 XWorkmate 在 App Store 构建里对外部 CLI / 本地 runtime 的禁用策略

View File

@ -2,6 +2,8 @@ import 'dart:io';
import 'package:yaml/yaml.dart';
const String _macosBundleIdentifier = 'plus.svc.xworkmate';
const Map<String, String> _providerLabels = <String, String>{
'codex': 'Codex',
'opencode': 'OpenCode',
@ -16,15 +18,6 @@ const Map<String, String> _defaultEndpoints = <String, String>{
'gemini': 'ws://127.0.0.1:9012',
};
const Map<String, String> _defaultLaunchCommands = <String, String>{
'codex': 'codex app-server --listen ws://127.0.0.1:9001',
'opencode': 'opencode serve --port 4096',
'claude':
'npx -y supergateway --stdio "mcp-bridge" --outputTransport ws --port 9011 --messagePath /',
'gemini':
'npx -y supergateway --stdio "uvx gemini-bridge" --outputTransport ws --port 9012 --messagePath /',
};
void main(List<String> args) async {
final options = _CliOptions.parse(args);
if (options.showHelp) {
@ -37,6 +30,7 @@ void main(List<String> args) async {
_defaultSettingsFile(
environment: Platform.environment,
operatingSystem: Platform.operatingSystem,
scope: options.settingsScope,
);
final resolvedEndpoints = <String, String>{
for (final entry in _defaultEndpoints.entries)
@ -49,6 +43,7 @@ void main(List<String> args) async {
settingsFile: settingsFile,
endpoints: resolvedEndpoints,
modeLabel: 'print-only',
settingsScope: options.settingsScope,
),
);
return;
@ -82,6 +77,7 @@ void main(List<String> args) async {
settingsFile: settingsFile,
endpoints: resolvedEndpoints,
modeLabel: 'applied',
settingsScope: options.settingsScope,
),
);
}
@ -94,10 +90,11 @@ Usage:
Commands:
apply Update XWorkmate settings.yaml externalAcpEndpoints.
print Print the resolved endpoint plan and suggested launch commands.
print Print the resolved endpoint plan.
Options:
--settings-file <path> Override settings.yaml path.
--settings-scope <scope> macOS only: auto | sandbox | user.
--codex-endpoint <url> Default: ${_defaultEndpoints['codex']}
--opencode-endpoint <url> Default: ${_defaultEndpoints['opencode']}
--claude-endpoint <url> Default: ${_defaultEndpoints['claude']}
@ -113,11 +110,11 @@ Options:
Notes:
- This tool only updates the externalAcpEndpoints block and preserves all
other settings keys.
- App Store-safe usage means running this tool outside the shipped app bundle
and keeping Codex/OpenCode/Supergateway/MCP bridge binaries outside the
bundle as user-managed prerequisites.
- Default macOS settings path:
~/Library/Application Support/xworkmate/config/settings.yaml
- This is a pre-config tool. Starting external providers is out of scope.
- App Store-safe usage means running this tool outside the shipped app bundle.
- macOS path selection with --settings-scope auto:
~/Library/Containers/$_macosBundleIdentifier/Data/Library/Application Support/xworkmate/config/settings.yaml
falls back to ~/Library/Application Support/xworkmate/config/settings.yaml
- Default Linux settings path:
~/.config/xworkmate/config/settings.yaml
''';
@ -127,32 +124,36 @@ String _renderPlan({
required File settingsFile,
required Map<String, String> endpoints,
required String modeLabel,
required _SettingsScope settingsScope,
}) {
final buffer = StringBuffer()
..writeln()
..writeln('Settings file: ${settingsFile.path}')
..writeln('Mode: $modeLabel')
..writeln('Settings scope: ${settingsScope.name}')
..writeln('Provider endpoint plan:');
for (final provider in _providerLabels.keys) {
buffer.writeln('- ${_providerLabels[provider]}: ${endpoints[provider]}');
buffer.writeln(' launch: ${_defaultLaunchCommands[provider]}');
}
buffer
..writeln()
..writeln('Bridge notes:')
..writeln('Scope notes:')
..writeln(
'- Codex is configured for native app-server WebSocket on port 9001.',
'- This tool configures endpoint slots only. Provider launch and bridge orchestration stay external to the app.',
)
..writeln(
'- OpenCode is configured to the native HTTP server on port 4096.',
'- On macOS, auto scope prefers the App Sandbox container after the app has launched at least once.',
)
..writeln(
'- Claude and Gemini are assigned local bridge slots; see docs/howto/external-acp-bridge-config.md for install and compatibility notes.',
'- App Store alignment: no external runtime binary is bundled or auto-started by this tool.',
)
..writeln(
'- App Store alignment: all bridge binaries stay outside the app bundle and must be started manually after install.',
'- Claude and Gemini remain plain endpoint slots; this tool no longer prescribes any third-party bridge package.',
)
..writeln(
'- Codex and OpenCode defaults are retained as local endpoint examples.',
);
return buffer.toString();
}
@ -227,12 +228,25 @@ Future<Map<String, dynamic>> _readExistingSettings(File settingsFile) async {
File _defaultSettingsFile({
required Map<String, String> environment,
required String operatingSystem,
required _SettingsScope scope,
}) {
final home = environment['HOME']?.trim() ?? '';
if (operatingSystem == 'macos' && home.isNotEmpty) {
return File(
final sandboxContainer = Directory(
'$home/Library/Containers/$_macosBundleIdentifier',
);
final sandboxed = File(
'${sandboxContainer.path}/Data/Library/Application Support/xworkmate/config/settings.yaml',
);
final userScoped = File(
'$home/Library/Application Support/xworkmate/config/settings.yaml',
);
return switch (scope) {
_SettingsScope.sandbox => sandboxed,
_SettingsScope.user => userScoped,
_SettingsScope.auto =>
sandboxContainer.existsSync() ? sandboxed : userScoped,
};
}
if (operatingSystem == 'linux' && home.isNotEmpty) {
final xdgConfigHome = environment['XDG_CONFIG_HOME']?.trim() ?? '';
@ -257,6 +271,8 @@ File _defaultSettingsFile({
enum _Command { apply, printPlan }
enum _SettingsScope { auto, sandbox, user }
class _CliOptions {
const _CliOptions({
required this.command,
@ -264,6 +280,7 @@ class _CliOptions {
required this.dryRun,
required this.backup,
required this.settingsFile,
required this.settingsScope,
required this.endpoints,
required this.enableProviders,
});
@ -273,6 +290,7 @@ class _CliOptions {
final bool dryRun;
final bool backup;
final File? settingsFile;
final _SettingsScope settingsScope;
final Map<String, String> endpoints;
final Map<String, bool> enableProviders;
@ -284,6 +302,7 @@ class _CliOptions {
dryRun: false,
backup: true,
settingsFile: null,
settingsScope: _SettingsScope.auto,
endpoints: const <String, String>{},
enableProviders: const <String, bool>{},
);
@ -305,6 +324,7 @@ class _CliOptions {
var dryRun = false;
var backup = true;
File? settingsFile;
var settingsScope = _SettingsScope.auto;
final endpoints = <String, String>{};
final enableProviders = <String, bool>{};
@ -321,6 +341,7 @@ class _CliOptions {
dryRun: dryRun,
backup: backup,
settingsFile: settingsFile,
settingsScope: settingsScope,
endpoints: endpoints,
enableProviders: enableProviders,
);
@ -356,6 +377,13 @@ class _CliOptions {
case '--settings-file':
settingsFile = File(value);
break;
case '--settings-scope':
settingsScope = switch (value.trim().toLowerCase()) {
'sandbox' => _SettingsScope.sandbox,
'user' => _SettingsScope.user,
_ => _SettingsScope.auto,
};
break;
case '--codex-endpoint':
endpoints['codex'] = value;
break;
@ -380,6 +408,7 @@ class _CliOptions {
dryRun: dryRun,
backup: backup,
settingsFile: settingsFile,
settingsScope: settingsScope,
endpoints: endpoints,
enableProviders: enableProviders,
);