Rename AI Gateway mode to Single Agent

This commit is contained in:
Haitao Pan 2026-03-23 10:11:53 +08:00
parent ae1faa03d0
commit 6628bca6a2
28 changed files with 242 additions and 210 deletions

View File

@ -17,14 +17,14 @@
### Highlights
- 本地配置、Gateway 凭证和 Assistant 任务会话改为以 secure storage 管理的密钥做加密持久化,重启和覆盖安装后不再丢失。
- `仅 AI Gateway` 线程补齐本地技能自动发现和当前线程可选技能列表恢复,线程状态与模型选择继续保持隔离。
- `单机智能体` 线程补齐本地技能自动发现和当前线程可选技能列表恢复,线程状态与模型选择继续保持隔离。
- Flutter Web assistant shell、Web Chrome 会话持久化和移动端安全控件一起补齐,多端可用性明显提升。
- Assistant composer 高度自适应、执行目标切换即时刷新、侧栏默认宽度等桌面交互问题已收敛。
- Windows / Linux parity、macOS DMG 打包和多平台构建发布流程持续补强。
### Current Delivery Scope
- 已交付:加密后的本地 settings snapshot、assistant threads 和 sealed backup 恢复链路。
- 已交付:Gateway-only 线程技能自动发现、线程状态清理和重启恢复。
- 已交付:Single Agent 线程技能自动发现、线程状态清理和重启恢复。
- 已交付Flutter Web assistant shell、Web 持久化修复、移动端安全壳控件和桌面布局微调。
- 已交付Windows / Linux parity 修复、多平台 build and release workflow、macOS 安装与分发产物。
@ -45,13 +45,13 @@
### Highlights
- Assistant 任务线程升级为持续会话:支持流式回复、继续追问、线程归档和重启恢复。
- 任务列表按 `仅 AI Gateway / 本地 OpenClaw Gateway / 远程 OpenClaw Gateway` 分组,保持极简列表布局。
- 任务列表按 `单机智能体 / 本地 OpenClaw Gateway / 远程 OpenClaw Gateway` 分组,保持极简列表布局。
- Multi-Agent 协作正式升级为 `Architect / Engineer / Tester`,并可选 `ARIS` 作为最强协作框架。
- ARIS bundle 作为只读资产内嵌进 App`skills/` 直接复用 upstream`llm-chat` 与 `claude-review` 切到 Go bridge。
- `Ollama Cloud` 文案与默认地址统一,打包后的 `.app` 会随同分发 `xworkmate-aris-bridge` helper。
### Current Delivery Scope
- 已交付:AI Gateway-only streaming threads、OpenClaw 本地/远程任务线程、手动归档与持续会话恢复。
- 已交付:Single Agent streaming threads、OpenClaw 本地/远程任务线程、手动归档与持续会话恢复。
- 已交付Multi-Agent managed runtime、ARIS framework preset、本地优先 Ollama 回退、Go bridge runtime 和打包分发。
- 已交付Settings / Assistant 里的 ARIS 轻量状态展示、任务分组、Ollama Cloud 设置迁移。
- 保持 truth-firstScheduled Tasks 仍是 `cron.list` 只读视图Memory 仍是 `memory/sync` 同步能力,不宣传 CRUD。

View File

@ -6,7 +6,7 @@ XWorkmate is an AI workspace shell built with Flutter.
## v0.5 Highlights
- Assistant 任务线程支持流式回复、继续追问和手动归档,不再是一问一答即结束。
- 任务列表按 `仅 AI Gateway / 本地 OpenClaw Gateway / 远程 OpenClaw Gateway` 分组显示。
- 任务列表按 `单机智能体 / 本地 OpenClaw Gateway / 远程 OpenClaw Gateway` 分组显示。
- Multi-Agent 协作支持 `Architect / Engineer / Tester`,并可切换 `Native / ARIS` 框架。
- ARIS `skills/` 直接随 App 内置,`llm-chat` 与 `claude-review` 统一由 Go bridge 驱动。
- `Ollama Cloud` 设置、ARIS helper bundling、macOS DMG 打包与安装链路已打通。
@ -14,12 +14,12 @@ XWorkmate is an AI workspace shell built with Flutter.
## Current Scope
### Shipping in v0.5
- AI Gateway-only streaming assistant threads
- Single Agent streaming assistant threads
- OpenClaw local/remote task threads with persistent context
- Multi-Agent orchestration with optional ARIS preset
- Bundled ARIS skills, Go bridge helper, `llm-chat` reviewer, and `claude-review`
- Ollama Cloud settings, task grouping, and macOS packaged delivery
- Flutter Web shell with `Assistant` + `Settings` only, supporting `Direct AI Gateway` and `Relay OpenClaw Gateway`
- Flutter Web shell with `Assistant` + `Settings` only, supporting `Single Agent` and `Relay OpenClaw Gateway`
### Not Yet Implemented
- Built-in Codex runtime through Rust FFI
@ -58,7 +58,7 @@ Web keeps the Assistant-first entry flow, but only exposes:
- `Assistant`
- `Settings`
- `Direct AI Gateway`
- `Single Agent`
- `Relay OpenClaw Gateway`
Web does not expose local CLI, workspace file access, native runtime orchestration, or desktop-only diagnostics.

View File

@ -37,7 +37,7 @@ flowchart LR
C["会话头部"] --> B
D["底部 composer 工具栏"] --> B
A1["分组:仅 AI Gateway / 本地 / 远程"]
A1["分组:单机智能体 / 本地 / 远程"]
A2["新对话"]
A3["任务卡片"]
A4["归档动作"]
@ -133,7 +133,7 @@ flowchart LR
| 维度 | 当前状态 | 说明 |
| --- | --- | --- |
| 消息历史 | 是 | 每个线程独立保存 / 解析历史 |
| 执行模式 | 是 | `AI Gateway Only / Local / Remote` 跟线程绑定 |
| 执行模式 | 是 | `Single Agent / Local / Remote` 跟线程绑定 |
| Skills | 是 | 已导入 / 已选 skills 跟线程绑定 |
| 模型 | 是 | `assistantModelId` 跟线程绑定,没设时回退到默认模型 |
| 顶部连接状态 | 是 | 只显示当前线程解析出的连接状态 |

View File

@ -318,14 +318,14 @@ state.
3.1 Execution target / work mode
Meaning:
- AI Gateway only
- Single Agent
- Local OpenClaw Gateway
- Remote OpenClaw Gateway
Platform availability:
- Desktop: aiGatewayOnly, local, remote
- Desktop: singleAgent, local, remote
- Mobile: remote
- Web: aiGatewayOnly, remote
- Web: singleAgent, remote
Primary resolver:
assistantExecutionTargetForSession(sessionKey)
@ -345,7 +345,7 @@ has changed unless the current thread record is also synchronized.
Important separation:
- `assistantExecutionTarget` is the work-mode default / thread override axis
- it is not a pointer into `gatewayProfiles`
- AI Gateway only has no OpenClaw profile
- Single Agent has no OpenClaw profile
- there is no implicit local-to-remote or AI-to-remote profile fallback
3.1.1 OpenClaw gateway profile list
@ -388,7 +388,7 @@ Resolution order:
2. resolved model for current execution target
Fallback rules:
- If target is aiGatewayOnly, use resolvedAiGatewayModel
- If target is singleAgent, use resolvedAiGatewayModel
- If target is local or remote, use resolvedDefaultModel
Interpretation:
@ -589,7 +589,7 @@ switchSession(sessionKey) must synchronize:
6.4 What must never happen implicitly
- local OpenClaw selectedAgentId must not silently fall back to remote
- AI Gateway only mode must not silently borrow a gateway profile
- Single Agent mode must not silently borrow a gateway profile
- gatewayProfiles changes must not silently overwrite the current thread mode
- platform capability filtering must not invent unsupported work modes

View File

@ -2,7 +2,7 @@
这组案例用于手动验证 `XWorkmate` 当前的多 Agent 协作链路,覆盖:
- `仅 AI Gateway`
- `单机智能体`
- `本地 OpenClaw Gateway`
- `远程 OpenClaw Gateway`
- `ARIS + 本地 Ollama`
@ -34,7 +34,7 @@
## 建议记录项
- 当前使用的框架:`原生` 或 `ARIS`
- 当前执行模式:`仅 AI Gateway` / `本地 OpenClaw Gateway` / `远程 OpenClaw Gateway`
- 当前执行模式:`单机智能体` / `本地 OpenClaw Gateway` / `远程 OpenClaw Gateway`
- 参与角色的 CLI 组合
- 是否看到流式输出
- 是否发生自动回退

View File

@ -7,7 +7,7 @@
## 推荐配置
- 框架:`ARIS`
- 执行模式:`仅 AI Gateway` 或 `本地 OpenClaw Gateway`
- 执行模式:`单机智能体` 或 `本地 OpenClaw Gateway`
- Ollama 端点:`http://127.0.0.1:11434`
- Architect`gemini`
- Engineer`opencode`

View File

@ -10,13 +10,13 @@
## 需要覆盖的三种模式
- `仅 AI Gateway`
- `单机智能体`
- `本地 OpenClaw Gateway`
- `远程 OpenClaw Gateway`
## 建议步骤
### 场景 A仅 AI Gateway
### 场景 A单机智能体
发送:
@ -26,7 +26,7 @@
确认:
- 顶部状态显示 `仅 AI Gateway`
- 顶部状态显示 `单机智能体`
- 不显示 `已连接 openclaw ...`
- 模型标签来自 AI Gateway 当前模型
@ -64,7 +64,7 @@
## 通过标准
- 切换模式后,模型显示会跟着变
- `仅 AI Gateway` 不会错误显示 OpenClaw 已连接
- `单机智能体` 不会错误显示 OpenClaw 已连接
- 三种模式下线程都能继续追问
- 任务列表分组归属与实际提交模式一致
- 右上角状态只反映当前线程,不沿用别的线程连接结果

View File

@ -10,7 +10,7 @@ The Web app keeps only:
- `Assistant`
- `Settings`
- `Direct AI Gateway`
- `Single Agent`
- `Relay OpenClaw Gateway`
The following remain desktop-only:
@ -48,7 +48,7 @@ flutter build web --release --base-href /
## Network Requirements
- `Direct AI Gateway` must be browser-reachable from the end user device.
- `Single Agent` must be browser-reachable from the end user device.
- Direct gateway endpoints must allow the Web origin with correct CORS headers.
- If a provider cannot satisfy browser reachability or CORS constraints, users must use `Relay OpenClaw Gateway` instead.
- Relay endpoints should stay on TLS in production and must not silently downgrade to insecure transport for remote usage.

View File

@ -239,7 +239,7 @@ class AppController extends ChangeNotifier {
String get settingsDraftStatusMessage => _settingsDraftStatusMessage;
LegacyRecoveryReport get legacyRecoveryReport => _store.lastRecoveryReport;
List<GatewayAgentSummary> get agents => _agentsController.agents;
List<GatewaySessionSummary> get sessions => isAiGatewayOnlyMode
List<GatewaySessionSummary> get sessions => isSingleAgentMode
? _assistantSessionSummaries()
: _sessionsController.sessions;
List<GatewaySessionSummary> get assistantSessions => _assistantSessions();
@ -275,8 +275,8 @@ class AppController extends ChangeNotifier {
String get aiGatewayUrl => settings.aiGateway.baseUrl.trim();
bool get hasStoredAiGatewayApiKey =>
_settingsController.secureRefs.containsKey('ai_gateway_api_key');
bool get isAiGatewayOnlyMode =>
currentAssistantExecutionTarget == AssistantExecutionTarget.aiGatewayOnly;
bool get isSingleAgentMode =>
currentAssistantExecutionTarget == AssistantExecutionTarget.singleAgent;
bool get isCodexBridgeBusy => _isCodexBridgeBusy;
String? get codexBridgeError => _codexBridgeError;
String? get codexRuntimeWarning => _codexRuntimeWarning;
@ -344,7 +344,7 @@ class AppController extends ChangeNotifier {
}
String _resolvedAssistantModelForTarget(AssistantExecutionTarget target) {
if (target == AssistantExecutionTarget.aiGatewayOnly) {
if (target == AssistantExecutionTarget.singleAgent) {
return resolvedAiGatewayModel;
}
final resolved = resolvedDefaultModel.trim();
@ -397,7 +397,7 @@ class AppController extends ChangeNotifier {
}
String get assistantConversationOwnerLabel {
if (!isAiGatewayOnlyMode) {
if (!isSingleAgentMode) {
return activeAgentName;
}
final model = resolvedAssistantModel;
@ -412,7 +412,7 @@ class AppController extends ChangeNotifier {
) {
final normalizedSessionKey = _normalizedAssistantSessionKey(sessionKey);
final target = assistantExecutionTargetForSession(normalizedSessionKey);
if (target == AssistantExecutionTarget.aiGatewayOnly) {
if (target == AssistantExecutionTarget.singleAgent) {
final model = assistantModelForSession(normalizedSessionKey);
final host = _aiGatewayHostLabel(settings.aiGateway.baseUrl);
final detail = _joinConnectionParts(<String>[model, host]);
@ -674,7 +674,7 @@ class AppController extends ChangeNotifier {
List<String> _assistantModelChoicesForSession(String sessionKey) {
final target = assistantExecutionTargetForSession(sessionKey);
if (target == AssistantExecutionTarget.aiGatewayOnly) {
if (target == AssistantExecutionTarget.singleAgent) {
return aiGatewayConversationModelChoices;
}
final runtimeModels = connectedGatewayModelChoices;
@ -714,7 +714,7 @@ class AppController extends ChangeNotifier {
bool get canQuickConnectGateway {
final target = currentAssistantExecutionTarget;
if (target == AssistantExecutionTarget.aiGatewayOnly) {
if (target == AssistantExecutionTarget.singleAgent) {
return false;
}
final profile = _gatewayProfileForAssistantExecutionTarget(target);
@ -729,7 +729,7 @@ class AppController extends ChangeNotifier {
return true;
}
final defaults = switch (target) {
AssistantExecutionTarget.aiGatewayOnly =>
AssistantExecutionTarget.singleAgent =>
GatewayConnectionProfile.emptySlot(index: kGatewayRemoteProfileIndex),
AssistantExecutionTarget.local =>
GatewayConnectionProfile.defaultsLocal(),
@ -773,11 +773,11 @@ class AppController extends ChangeNotifier {
_sessionsController.currentSessionKey,
);
final items = List<GatewayChatMessage>.from(
isAiGatewayOnlyMode
isSingleAgentMode
? (_gatewayHistoryCache[sessionKey] ?? const <GatewayChatMessage>[])
: _chatController.messages,
);
final threadItems = isAiGatewayOnlyMode
final threadItems = isSingleAgentMode
? _assistantThreadMessages[sessionKey]
: null;
if (threadItems != null && threadItems.isNotEmpty) {
@ -787,7 +787,7 @@ class AppController extends ChangeNotifier {
if (localItems != null && localItems.isNotEmpty) {
items.addAll(localItems);
}
final streaming = isAiGatewayOnlyMode
final streaming = isSingleAgentMode
? (_aiGatewayStreamingTextBySession[sessionKey]?.trim() ?? '')
: (_chatController.streamingAssistantText?.trim() ?? '');
if (streaming.isNotEmpty) {
@ -876,7 +876,7 @@ class AppController extends ChangeNotifier {
bool assistantSessionHasPendingRun(String sessionKey) {
final normalized = _normalizedAssistantSessionKey(sessionKey);
if (assistantExecutionTargetForSession(normalized) ==
AssistantExecutionTarget.aiGatewayOnly) {
AssistantExecutionTarget.singleAgent) {
return _aiGatewayPendingSessionKeys.contains(normalized);
}
return (_chatController.hasPendingRun || _multiAgentRunPending) &&
@ -1246,7 +1246,7 @@ class AppController extends ChangeNotifier {
Future<void> connectSavedGateway() async {
final target = currentAssistantExecutionTarget;
if (target == AssistantExecutionTarget.aiGatewayOnly) {
if (target == AssistantExecutionTarget.singleAgent) {
return;
}
await _connectProfile(_gatewayProfileForAssistantExecutionTarget(target));
@ -1322,7 +1322,7 @@ class AppController extends ChangeNotifier {
Future<void> selectAgent(String? agentId) async {
_agentsController.selectAgent(agentId);
if (currentAssistantExecutionTarget !=
AssistantExecutionTarget.aiGatewayOnly) {
AssistantExecutionTarget.singleAgent) {
final target = currentAssistantExecutionTarget;
final nextProfile = _gatewayProfileForAssistantExecutionTarget(
target,
@ -1368,7 +1368,7 @@ class AppController extends ChangeNotifier {
final nextTarget = assistantExecutionTargetForSession(nextSessionKey);
final nextViewMode = assistantMessageViewModeForSession(nextSessionKey);
if (!isAiGatewayOnlyMode) {
if (!isSingleAgentMode) {
_preserveGatewayHistoryForSession(previousSessionKey);
}
@ -1384,7 +1384,7 @@ class AppController extends ChangeNotifier {
sessionKey: nextSessionKey,
persistDefaultSelection: false,
);
if (nextTarget == AssistantExecutionTarget.aiGatewayOnly) {
if (nextTarget == AssistantExecutionTarget.singleAgent) {
await discoverGatewayOnlySkillsForSession(nextSessionKey);
} else {
await dismissDiscoveredSkillsForSession(nextSessionKey);
@ -1398,7 +1398,7 @@ class AppController extends ChangeNotifier {
List<GatewayChatAttachmentPayload> attachments =
const <GatewayChatAttachmentPayload>[],
}) async {
if (isAiGatewayOnlyMode) {
if (isSingleAgentMode) {
await _sendAiGatewayMessage(
message,
thinking: thinking,
@ -1436,7 +1436,7 @@ class AppController extends ChangeNotifier {
_notifyIfActive();
return;
}
if (isAiGatewayOnlyMode) {
if (isSingleAgentMode) {
await _abortAiGatewayRun(_sessionsController.currentSessionKey);
return;
}
@ -1466,7 +1466,7 @@ class AppController extends ChangeNotifier {
sessionKey: _sessionsController.currentSessionKey,
persistDefaultSelection: true,
);
if (resolvedTarget == AssistantExecutionTarget.aiGatewayOnly) {
if (resolvedTarget == AssistantExecutionTarget.singleAgent) {
await discoverGatewayOnlySkillsForSession(
_sessionsController.currentSessionKey,
);
@ -1531,7 +1531,7 @@ class AppController extends ChangeNotifier {
);
}
if (resolvedTarget == AssistantExecutionTarget.aiGatewayOnly) {
if (resolvedTarget == AssistantExecutionTarget.singleAgent) {
if (_runtime.isConnected) {
_preserveGatewayHistoryForSession(normalizedSessionKey);
}
@ -1641,7 +1641,7 @@ class AppController extends ChangeNotifier {
Future<void> discoverGatewayOnlySkillsForSession(String sessionKey) async {
final normalizedSessionKey = _normalizedAssistantSessionKey(sessionKey);
if (assistantExecutionTargetForSession(normalizedSessionKey) !=
AssistantExecutionTarget.aiGatewayOnly) {
AssistantExecutionTarget.singleAgent) {
_upsertAssistantThreadRecord(
normalizedSessionKey,
discoveredSkills: const <AssistantThreadSkillEntry>[],
@ -2138,13 +2138,13 @@ class AppController extends ChangeNotifier {
String tokenOverride = '',
String passwordOverride = '',
}) async {
if (executionTarget == AssistantExecutionTarget.aiGatewayOnly ||
if (executionTarget == AssistantExecutionTarget.singleAgent ||
profile.mode == RuntimeConnectionMode.unconfigured) {
return (
state: 'inactive',
message: appText(
'当前模式仅使用 AI Gateway,不建立 OpenClaw Gateway 会话。',
'The current mode uses AI Gateway only and does not open an OpenClaw Gateway session.',
'当前模式使用单机智能体,不建立 OpenClaw Gateway 会话。',
'The current mode uses Single Agent and does not open an OpenClaw Gateway session.',
),
endpoint: '',
);
@ -2389,7 +2389,7 @@ class AppController extends ChangeNotifier {
);
await _restoreInitialAssistantSessionSelection();
await _ensureActiveAssistantThread();
if (isAiGatewayOnlyMode) {
if (isSingleAgentMode) {
await discoverGatewayOnlySkillsForSession(currentSessionKey);
}
_runtimeEventsSubscription = _runtimeCoordinator.gateway.events.listen(
@ -2399,7 +2399,7 @@ class AppController extends ChangeNotifier {
startupTarget,
);
final shouldAutoConnect =
startupTarget != AssistantExecutionTarget.aiGatewayOnly &&
startupTarget != AssistantExecutionTarget.singleAgent &&
startupProfile != null &&
startupProfile.useSetupCode &&
startupProfile.setupCode.trim().isNotEmpty;
@ -2596,7 +2596,7 @@ class AppController extends ChangeNotifier {
sessionKey: sessionKey,
persistDefaultSelection: false,
);
if (target == AssistantExecutionTarget.aiGatewayOnly) {
if (target == AssistantExecutionTarget.singleAgent) {
await discoverGatewayOnlySkillsForSession(sessionKey);
} else {
await dismissDiscoveredSkillsForSession(sessionKey);
@ -2620,7 +2620,7 @@ class AppController extends ChangeNotifier {
}
Future<void> _ensureActiveAssistantThread() async {
if (!isAiGatewayOnlyMode ||
if (!isSingleAgentMode ||
!isAssistantTaskArchived(_sessionsController.currentSessionKey)) {
return;
}
@ -3857,7 +3857,7 @@ class AppController extends ChangeNotifier {
return GatewayMode.offline;
}
return switch (currentAssistantExecutionTarget) {
AssistantExecutionTarget.aiGatewayOnly => GatewayMode.offline,
AssistantExecutionTarget.singleAgent => GatewayMode.offline,
AssistantExecutionTarget.local => GatewayMode.local,
AssistantExecutionTarget.remote => GatewayMode.remote,
};
@ -4011,7 +4011,7 @@ class AppController extends ChangeNotifier {
) {
return switch (mode) {
RuntimeConnectionMode.unconfigured =>
AssistantExecutionTarget.aiGatewayOnly,
AssistantExecutionTarget.singleAgent,
RuntimeConnectionMode.local => AssistantExecutionTarget.local,
RuntimeConnectionMode.remote => AssistantExecutionTarget.remote,
};
@ -4023,8 +4023,8 @@ class AppController extends ChangeNotifier {
return switch (target) {
AssistantExecutionTarget.local => settings.primaryLocalGatewayProfile,
AssistantExecutionTarget.remote => settings.primaryRemoteGatewayProfile,
AssistantExecutionTarget.aiGatewayOnly => throw StateError(
'AI Gateway only target has no OpenClaw gateway profile.',
AssistantExecutionTarget.singleAgent => throw StateError(
'Single Agent target has no OpenClaw gateway profile.',
),
};
}
@ -4033,8 +4033,8 @@ class AppController extends ChangeNotifier {
return switch (target) {
AssistantExecutionTarget.local => kGatewayLocalProfileIndex,
AssistantExecutionTarget.remote => kGatewayRemoteProfileIndex,
AssistantExecutionTarget.aiGatewayOnly => throw StateError(
'AI Gateway only target has no OpenClaw gateway profile index.',
AssistantExecutionTarget.singleAgent => throw StateError(
'Single Agent target has no OpenClaw gateway profile index.',
),
};
}

View File

@ -136,8 +136,8 @@ class AppController extends ChangeNotifier {
_currentRecord.executionTarget ?? _settings.assistantExecutionTarget;
AssistantExecutionTarget get currentAssistantExecutionTarget =>
assistantExecutionTarget;
bool get isAiGatewayOnlyMode =>
assistantExecutionTarget == AssistantExecutionTarget.aiGatewayOnly;
bool get isSingleAgentMode =>
assistantExecutionTarget == AssistantExecutionTarget.singleAgent;
List<GatewayChatMessage> get chatMessages {
final base = List<GatewayChatMessage>.from(_currentRecord.messages);
final streaming = _streamingTextBySession[_currentSessionKey]?.trim() ?? '';
@ -172,7 +172,7 @@ class AppController extends ChangeNotifier {
DateTime.now().millisecondsSinceEpoch.toDouble(),
executionTarget:
_sanitizeTarget(record.executionTarget) ??
AssistantExecutionTarget.aiGatewayOnly,
AssistantExecutionTarget.singleAgent,
pending: _pendingSessionKeys.contains(record.sessionKey),
current: record.sessionKey == _currentSessionKey,
),
@ -233,7 +233,7 @@ class AppController extends ChangeNotifier {
AssistantThreadConnectionState get currentAssistantConnectionState {
final target = currentAssistantExecutionTarget;
if (target == AssistantExecutionTarget.aiGatewayOnly) {
if (target == AssistantExecutionTarget.singleAgent) {
final host = _hostLabel(_settings.aiGateway.baseUrl);
final model = resolvedAiGatewayModel;
final detail = _joinConnectionParts(<String>[model, host]);
@ -244,7 +244,7 @@ class AppController extends ChangeNotifier {
: RuntimeConnectionStatus.offline,
primaryLabel: target.label,
detailLabel: detail.isEmpty
? appText('Direct AI 未配置', 'Direct AI not configured')
? appText('单机智能体未配置', 'Single Agent not configured')
: detail,
ready: canUseAiGatewayConversation,
pairingRequired: false,
@ -287,8 +287,8 @@ class AppController extends ChangeNotifier {
);
}
return appText(
'当前会话列表会在浏览器本地保存,刷新后仍可恢复 Direct AI / Relay 的历史入口。',
'Conversation history is stored in this browser so Direct AI and Relay entries remain available after reload.',
'当前会话列表会在浏览器本地保存,刷新后仍可恢复单机智能体 / Relay 的历史入口。',
'Conversation history is stored in this browser so Single Agent and Relay entries remain available after reload.',
);
}
@ -301,7 +301,7 @@ class AppController extends ChangeNotifier {
}
final target =
_sanitizeTarget(_settings.assistantExecutionTarget) ??
AssistantExecutionTarget.aiGatewayOnly;
AssistantExecutionTarget.singleAgent;
final record = _newRecord(target: target);
_threadRecords[record.sessionKey] = record;
_currentSessionKey = record.sessionKey;
@ -548,7 +548,7 @@ class AppController extends ChangeNotifier {
AssistantExecutionTarget target,
) async {
final resolvedTarget =
_sanitizeTarget(target) ?? AssistantExecutionTarget.aiGatewayOnly;
_sanitizeTarget(target) ?? AssistantExecutionTarget.singleAgent;
_settings = _settings.copyWith(assistantExecutionTarget: resolvedTarget);
_replaceCurrentRecord(
_currentRecord.copyWith(executionTarget: resolvedTarget),
@ -570,7 +570,7 @@ class AppController extends ChangeNotifier {
defaultProvider: provider.trim().isEmpty ? 'gateway' : provider.trim(),
defaultModel: defaultModel.trim(),
aiGateway: _settings.aiGateway.copyWith(
name: name.trim().isEmpty ? 'Direct AI' : name.trim(),
name: name.trim().isEmpty ? 'Single Agent' : name.trim(),
baseUrl: normalizedBaseUrl?.toString() ?? baseUrl.trim(),
),
);
@ -625,7 +625,7 @@ class AppController extends ChangeNotifier {
defaultProvider: provider.trim().isEmpty ? 'gateway' : provider.trim(),
defaultModel: resolvedDefaultModel,
aiGateway: _settings.aiGateway.copyWith(
name: name.trim().isEmpty ? 'Direct AI' : name.trim(),
name: name.trim().isEmpty ? 'Single Agent' : name.trim(),
baseUrl:
_aiGatewayClient.normalizeBaseUrl(baseUrl)?.toString() ??
baseUrl.trim(),
@ -836,12 +836,12 @@ class AppController extends ChangeNotifier {
notifyListeners();
try {
if (target == AssistantExecutionTarget.aiGatewayOnly) {
if (target == AssistantExecutionTarget.singleAgent) {
if (!canUseAiGatewayConversation) {
throw Exception(
appText(
'请先在 Settings 配置 Direct AI 的地址、API Key 和默认模型。',
'Configure Direct AI endpoint, API key, and default model first.',
'请先在 Settings 配置单机智能体所需的 AI Gateway 地址、API Key 和默认模型。',
'Configure the Single Agent AI Gateway endpoint, API key, and default model first.',
),
);
}
@ -915,7 +915,7 @@ class AppController extends ChangeNotifier {
SettingsSnapshot _sanitizeSettings(SettingsSnapshot snapshot) {
final target =
_sanitizeTarget(snapshot.assistantExecutionTarget) ??
AssistantExecutionTarget.aiGatewayOnly;
AssistantExecutionTarget.singleAgent;
final normalizedSessionBaseUrl =
RemoteWebSessionRepository.normalizeBaseUrl(
snapshot.webSessionPersistence.remoteBaseUrl,
@ -942,7 +942,7 @@ class AppController extends ChangeNotifier {
AssistantThreadRecord _sanitizeRecord(AssistantThreadRecord record) {
final target =
_sanitizeTarget(record.executionTarget) ??
AssistantExecutionTarget.aiGatewayOnly;
AssistantExecutionTarget.singleAgent;
return record.copyWith(
executionTarget: target,
title: record.title.trim().isEmpty
@ -954,9 +954,9 @@ class AppController extends ChangeNotifier {
AssistantExecutionTarget? _sanitizeTarget(AssistantExecutionTarget? target) {
return switch (target) {
AssistantExecutionTarget.remote => AssistantExecutionTarget.remote,
AssistantExecutionTarget.aiGatewayOnly =>
AssistantExecutionTarget.aiGatewayOnly,
_ => AssistantExecutionTarget.aiGatewayOnly,
AssistantExecutionTarget.singleAgent =>
AssistantExecutionTarget.singleAgent,
_ => AssistantExecutionTarget.singleAgent,
};
}

View File

@ -960,7 +960,7 @@ class UiFeatureAccess {
List<AssistantExecutionTarget> get availableExecutionTargets {
final targets = <AssistantExecutionTarget>[];
if (supportsDirectAi) {
targets.add(AssistantExecutionTarget.aiGatewayOnly);
targets.add(AssistantExecutionTarget.singleAgent);
}
if (supportsLocalGateway) {
targets.add(AssistantExecutionTarget.local);
@ -980,12 +980,12 @@ class UiFeatureAccess {
}
final preferredOrder = platform == UiFeaturePlatform.web
? const <AssistantExecutionTarget>[
AssistantExecutionTarget.aiGatewayOnly,
AssistantExecutionTarget.singleAgent,
AssistantExecutionTarget.remote,
]
: const <AssistantExecutionTarget>[
AssistantExecutionTarget.local,
AssistantExecutionTarget.aiGatewayOnly,
AssistantExecutionTarget.singleAgent,
AssistantExecutionTarget.remote,
];
for (final candidate in preferredOrder) {
@ -994,7 +994,7 @@ class UiFeatureAccess {
}
}
return platform == UiFeaturePlatform.web
? AssistantExecutionTarget.aiGatewayOnly
? AssistantExecutionTarget.singleAgent
: AssistantExecutionTarget.local;
}
}

View File

@ -668,7 +668,7 @@ class _AssistantPageState extends State<AssistantPage> {
}
final shouldUseGatewayAgent =
executionTarget != AssistantExecutionTarget.aiGatewayOnly;
executionTarget != AssistantExecutionTarget.singleAgent;
final autoAgent = shouldUseGatewayAgent
? _pickAutoAgent(controller, rawPrompt)
: null;
@ -706,7 +706,7 @@ class _AssistantPageState extends State<AssistantPage> {
preview: rawPrompt,
status:
controller.hasAssistantPendingRun ||
executionTarget == AssistantExecutionTarget.aiGatewayOnly ||
executionTarget == AssistantExecutionTarget.singleAgent ||
connectionState.connected
? 'running'
: 'queued',
@ -826,7 +826,7 @@ class _AssistantPageState extends State<AssistantPage> {
}
List<_ComposerSkillOption> _availableSkillOptions(AppController controller) {
if (controller.isAiGatewayOnlyMode) {
if (controller.isSingleAgentMode) {
return controller
.assistantImportedSkillsForSession(controller.currentSessionKey)
.map(_skillOptionFromThreadSkill)
@ -1275,7 +1275,7 @@ class _AssistantPageState extends State<AssistantPage> {
String _buildDraftSessionKey(AppController controller) {
final stamp = DateTime.now().millisecondsSinceEpoch;
if (controller.isAiGatewayOnlyMode) {
if (controller.isSingleAgentMode) {
return 'draft:$stamp';
}
final selectedAgentId = controller.selectedAgentId.trim();
@ -2329,27 +2329,27 @@ class _AssistantEmptyState extends StatelessWidget {
Widget build(BuildContext context) {
final theme = Theme.of(context);
final connectionState = controller.currentAssistantConnectionState;
final aiGatewayOnly = connectionState.isAiGatewayOnly;
final singleAgent = connectionState.isSingleAgent;
final connected = connectionState.connected;
final reconnectAvailable = controller.canQuickConnectGateway;
final title = aiGatewayOnly
final title = singleAgent
? connected
? appText('开始 AI 对话', 'Start an AI conversation')
? appText('开始单机智能体任务', 'Start a single-agent task')
: appText('先配置 AI Gateway', 'Configure AI Gateway first')
: connected
? appText('开始对话或运行任务', 'Start a chat or run a task')
: connectionState.status == RuntimeConnectionStatus.error
? appText('Gateway 连接失败', 'Gateway connection failed')
: appText('先连接 Gateway', 'Connect a gateway first');
final description = aiGatewayOnly
final description = singleAgent
? connected
? appText(
'当前模式只通过 AI Gateway 处理当前任务,不会建立 OpenClaw Gateway 会话。',
'This mode handles the current task through AI Gateway only and does not open an OpenClaw Gateway session.',
'当前模式使用单机智能体处理当前任务,不会建立 OpenClaw Gateway 会话。',
'This mode uses a single agent for the current task and does not open an OpenClaw Gateway session.',
)
: appText(
'请先在 设置 -> 集成 中配置 AI Gateway 地址、API Key 和默认模型,然后继续当前任务。',
'Set the AI Gateway URL, API key, and default model in Settings -> Integrations, then continue this task.',
'请先在 设置 -> 集成 中配置 AI Gateway 地址、API Key 和默认模型,然后以单机智能体模式继续当前任务。',
'Set the AI Gateway URL, API key, and default model in Settings -> Integrations, then continue this task in Single Agent mode.',
)
: connected
? appText(
@ -2402,7 +2402,7 @@ class _AssistantEmptyState extends StatelessWidget {
FilledButton.icon(
onPressed: connected
? onFocusComposer
: aiGatewayOnly
: singleAgent
? onOpenAiGatewaySettings
: reconnectAvailable
? () async {
@ -2412,7 +2412,7 @@ class _AssistantEmptyState extends StatelessWidget {
icon: Icon(
connected
? Icons.edit_rounded
: aiGatewayOnly
: singleAgent
? Icons.tune_rounded
: reconnectAvailable
? Icons.refresh_rounded
@ -2421,7 +2421,7 @@ class _AssistantEmptyState extends StatelessWidget {
label: Text(
connected
? appText('开始输入', 'Start typing')
: aiGatewayOnly
: singleAgent
? appText('打开配置中心', 'Open settings')
: reconnectAvailable
? appText('重新连接', 'Reconnect')
@ -2440,16 +2440,16 @@ class _AssistantEmptyState extends StatelessWidget {
),
if (!connected)
OutlinedButton.icon(
onPressed: aiGatewayOnly
onPressed: singleAgent
? onOpenAiGatewaySettings
: onOpenGateway,
icon: Icon(
aiGatewayOnly
singleAgent
? Icons.hub_outlined
: Icons.settings_rounded,
),
label: Text(
aiGatewayOnly
singleAgent
? appText('打开设置中心', 'Open settings')
: appText('编辑连接', 'Edit connection'),
),
@ -2570,7 +2570,7 @@ class _ComposerBarState extends State<_ComposerBar> {
resolveUiFeaturePlatformFromContext(context),
);
final connectionState = controller.currentAssistantConnectionState;
final aiGatewayOnly = connectionState.isAiGatewayOnly;
final singleAgent = connectionState.isSingleAgent;
final connected = connectionState.connected;
final reconnectAvailable = controller.canQuickConnectGateway;
final connecting = connectionState.connecting;
@ -2582,7 +2582,7 @@ class _ComposerBarState extends State<_ComposerBar> {
final discoveredCount = widget.discoveredSkills.length;
final submitLabel = connected
? appText('提交', 'Submit')
: aiGatewayOnly
: singleAgent
? appText('配置 AI Gateway', 'Configure AI Gateway')
: connecting
? appText('连接中…', 'Connecting…')
@ -2801,7 +2801,7 @@ class _ComposerBarState extends State<_ComposerBar> {
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (aiGatewayOnly && discoveredCount > 0) ...[
if (singleAgent && discoveredCount > 0) ...[
InkWell(
key: const Key('assistant-discovered-skills-button'),
borderRadius: BorderRadius.circular(AppRadius.chip),
@ -2953,7 +2953,7 @@ class _ComposerBarState extends State<_ComposerBar> {
? null
: connected
? widget.onSend
: aiGatewayOnly
: singleAgent
? widget.onOpenAiGatewaySettings
: reconnectAvailable
? () async {
@ -2976,7 +2976,7 @@ class _ComposerBarState extends State<_ComposerBar> {
Icon(
connected
? Icons.arrow_upward_rounded
: aiGatewayOnly
: singleAgent
? Icons.hub_outlined
: reconnectAvailable
? Icons.refresh_rounded
@ -3427,7 +3427,7 @@ class _ComposerToolbarChipState extends State<_ComposerToolbarChip> {
extension on AssistantExecutionTarget {
IconData get icon => switch (this) {
AssistantExecutionTarget.aiGatewayOnly => Icons.hub_outlined,
AssistantExecutionTarget.singleAgent => Icons.hub_outlined,
AssistantExecutionTarget.local => Icons.computer_outlined,
AssistantExecutionTarget.remote => Icons.cloud_outlined,
};
@ -3933,7 +3933,7 @@ class _ConnectionChip extends StatelessWidget {
Widget build(BuildContext context) {
final theme = Theme.of(context);
final connectionState = controller.currentAssistantConnectionState;
final color = connectionState.isAiGatewayOnly
final color = connectionState.isSingleAgent
? (connectionState.connected
? context.palette.accentMuted
: context.palette.surfaceSecondary)

View File

@ -31,13 +31,13 @@ extension RuntimeConnectionStatusCopy on RuntimeConnectionStatus {
};
}
enum AssistantExecutionTarget { aiGatewayOnly, local, remote }
enum AssistantExecutionTarget { singleAgent, local, remote }
extension AssistantExecutionTargetCopy on AssistantExecutionTarget {
String get label => switch (this) {
AssistantExecutionTarget.aiGatewayOnly => appText(
'仅 AI Gateway',
'AI Gateway Only',
AssistantExecutionTarget.singleAgent => appText(
'单机智能体',
'Single Agent',
),
AssistantExecutionTarget.local => appText(
'本地 OpenClaw Gateway',
@ -50,16 +50,26 @@ extension AssistantExecutionTargetCopy on AssistantExecutionTarget {
};
String get promptValue => switch (this) {
AssistantExecutionTarget.aiGatewayOnly => 'ai-gateway-only',
AssistantExecutionTarget.singleAgent => 'single-agent',
AssistantExecutionTarget.local => 'local',
AssistantExecutionTarget.remote => 'remote',
};
static AssistantExecutionTarget fromJsonValue(String? value) {
return AssistantExecutionTarget.values.firstWhere(
(item) => item.name == value,
orElse: () => AssistantExecutionTarget.local,
);
final normalized = value?.trim() ?? '';
switch (normalized) {
case 'singleAgent':
case 'aiGatewayOnly':
case 'single-agent':
case 'ai-gateway-only':
return AssistantExecutionTarget.singleAgent;
case 'local':
return AssistantExecutionTarget.local;
case 'remote':
return AssistantExecutionTarget.remote;
default:
return AssistantExecutionTarget.local;
}
}
}
@ -84,13 +94,13 @@ class AssistantThreadConnectionState {
final bool gatewayTokenMissing;
final String? lastError;
bool get isAiGatewayOnly =>
executionTarget == AssistantExecutionTarget.aiGatewayOnly;
bool get isSingleAgent =>
executionTarget == AssistantExecutionTarget.singleAgent;
bool get connected => ready;
bool get connecting =>
!isAiGatewayOnly && status == RuntimeConnectionStatus.connecting;
!isSingleAgent && status == RuntimeConnectionStatus.connecting;
}
enum AssistantMessageViewMode { rendered, raw }
@ -1493,7 +1503,7 @@ class SettingsSnapshot {
AssistantExecutionTarget target,
) {
return switch (target) {
AssistantExecutionTarget.aiGatewayOnly => null,
AssistantExecutionTarget.singleAgent => null,
AssistantExecutionTarget.local => primaryLocalGatewayProfile,
AssistantExecutionTarget.remote => primaryRemoteGatewayProfile,
};
@ -1515,7 +1525,7 @@ class SettingsSnapshot {
final index = switch (target) {
AssistantExecutionTarget.local => kGatewayLocalProfileIndex,
AssistantExecutionTarget.remote => kGatewayRemoteProfileIndex,
AssistantExecutionTarget.aiGatewayOnly => null,
AssistantExecutionTarget.singleAgent => null,
};
if (index == null) {
return this;
@ -2084,6 +2094,17 @@ class AssistantThreadRecord {
return keys;
}
String? normalizeGatewayEntryState(Object? value) {
final normalized = value?.toString().trim() ?? '';
if (normalized.isEmpty) {
return null;
}
if (normalized == 'ai-gateway-only') {
return 'single-agent';
}
return normalized;
}
return AssistantThreadRecord(
sessionKey: json['sessionKey']?.toString() ?? '',
messages: messages,
@ -2102,7 +2123,7 @@ class AssistantThreadRecord {
importedSkills: normalizeSkillEntries(json['importedSkills']),
selectedSkillKeys: normalizeSkillKeys(json['selectedSkillKeys']),
assistantModelId: json['assistantModelId']?.toString() ?? '',
gatewayEntryState: json['gatewayEntryState']?.toString(),
gatewayEntryState: normalizeGatewayEntryState(json['gatewayEntryState']),
);
}
}

View File

@ -123,7 +123,7 @@ class WebAiGatewayClient {
provider:
_stringValue(map['provider']) ??
_stringValue(map['owned_by']) ??
'Direct AI',
'Single Agent',
contextWindow:
_intValue(map['contextWindow']) ??
_intValue(map['context_window']),

View File

@ -42,7 +42,7 @@ class _WebAssistantPageState extends State<WebAssistantPage> {
builder: (context, _) {
final uiFeatures = controller.featuresFor(UiFeaturePlatform.web);
final allDirect = controller.conversationsForTarget(
AssistantExecutionTarget.aiGatewayOnly,
AssistantExecutionTarget.singleAgent,
);
final allRelay = controller.conversationsForTarget(
AssistantExecutionTarget.remote,
@ -53,12 +53,12 @@ class _WebAssistantPageState extends State<WebAssistantPage> {
final availableTargets = uiFeatures.availableExecutionTargets
.where(
(target) =>
target == AssistantExecutionTarget.aiGatewayOnly ||
target == AssistantExecutionTarget.singleAgent ||
target == AssistantExecutionTarget.remote,
)
.toList(growable: false);
final connected =
currentTarget == AssistantExecutionTarget.aiGatewayOnly
currentTarget == AssistantExecutionTarget.singleAgent
? controller.canUseAiGatewayConversation
: controller.connection.status == RuntimeConnectionStatus.connected;
final currentMessages = controller.chatMessages;
@ -83,8 +83,8 @@ class _WebAssistantPageState extends State<WebAssistantPage> {
eyebrow: appText('Web Workspace', 'Web Workspace'),
title: appText('助手', 'Assistant'),
subtitle: appText(
'Direct AI 与 Relay Gateway 共用一个入口,左侧保留会话/任务历史。',
'Use one Assistant surface for Direct AI and Relay Gateway, with embedded conversation history on the left.',
'单机智能体与 Relay Gateway 共用一个入口,左侧保留会话/任务历史。',
'Use one Assistant surface for Single Agent and Relay Gateway, with embedded conversation history on the left.',
),
toolbar: Wrap(
spacing: 10,
@ -232,12 +232,12 @@ class _ConversationRail extends StatelessWidget {
children: [
if (showDirect)
_ConversationGroup(
title: appText('Direct AI Gateway', 'Direct AI Gateway'),
title: appText('Single Agent', 'Single Agent'),
icon: Icons.hub_rounded,
items: direct,
emptyLabel: appText(
'还没有 Direct AI 对话',
'No Direct AI conversations yet',
'还没有单机智能体对话',
'No Single Agent conversations yet',
),
onSelect: controller.switchConversation,
),
@ -381,7 +381,7 @@ class _ConversationPanel extends StatelessWidget {
Widget build(BuildContext context) {
final palette = context.palette;
final currentTarget = controller.assistantExecutionTarget;
final targetReady = currentTarget == AssistantExecutionTarget.aiGatewayOnly
final targetReady = currentTarget == AssistantExecutionTarget.singleAgent
? controller.canUseAiGatewayConversation
: controller.connection.status == RuntimeConnectionStatus.connected;
@ -431,10 +431,10 @@ class _ConversationPanel extends StatelessWidget {
const SizedBox(width: 12),
Expanded(
child: Text(
currentTarget == AssistantExecutionTarget.aiGatewayOnly
currentTarget == AssistantExecutionTarget.singleAgent
? appText(
'当前 Direct AI 配置还不完整,请先在 Settings 中保存地址、API Key 和默认模型。',
'Direct AI is not ready yet. Save the endpoint, API key, and default model in Settings first.',
'当前单机智能体配置还不完整,请先在 Settings 中保存 AI Gateway 地址、API Key 和默认模型。',
'Single Agent is not ready yet. Save the AI Gateway endpoint, API key, and default model in Settings first.',
)
: appText(
'当前 Relay Gateway 尚未连接,请先在 Settings 中保存配置并连接。',
@ -506,10 +506,10 @@ class _ConversationPanel extends StatelessWidget {
Expanded(
child: Text(
currentTarget ==
AssistantExecutionTarget.aiGatewayOnly
AssistantExecutionTarget.singleAgent
? appText(
'Web 端 Direct AI 只保留纯网络能力,不提供本地文件和 CLI。',
'Direct AI on web keeps network-only capabilities and does not expose local files or CLI.',
'Web 端单机智能体只保留纯网络能力,不提供本地文件和 CLI。',
'Single Agent on web keeps network-only capabilities and does not expose local files or CLI.',
)
: appText(
'Web 端 Relay 模式使用远程 OpenClaw Gateway不区分 local / remote。',
@ -637,9 +637,9 @@ class _TargetChip extends StatelessWidget {
String _targetLabel(AssistantExecutionTarget target) {
return switch (target) {
AssistantExecutionTarget.aiGatewayOnly => appText(
'Direct AI Gateway',
'Direct AI Gateway',
AssistantExecutionTarget.singleAgent => appText(
'Single Agent',
'Single Agent',
),
AssistantExecutionTarget.remote => appText(
'Relay OpenClaw Gateway',

View File

@ -143,8 +143,8 @@ class _WebSettingsPageState extends State<WebSettingsPage> {
eyebrow: appText('Web Preferences', 'Web Preferences'),
title: appText('设置', 'Settings'),
subtitle: appText(
'Web 版只保留 Direct AI / Relay Gateway、界面偏好和基础信息。',
'The web app keeps only Direct AI, Relay Gateway, appearance preferences, and basic product info.',
'Web 版只保留单机智能体 / Relay Gateway、界面偏好和基础信息。',
'The web app keeps only Single Agent, Relay Gateway, appearance preferences, and basic product info.',
),
toolbar: Wrap(
spacing: 10,
@ -227,7 +227,7 @@ class _WebSettingsPageState extends State<WebSettingsPage> {
.availableExecutionTargets
.where(
(target) =>
target == AssistantExecutionTarget.aiGatewayOnly ||
target == AssistantExecutionTarget.singleAgent ||
target == AssistantExecutionTarget.remote,
)
.toList(growable: false);
@ -401,7 +401,7 @@ class _WebSettingsPageState extends State<WebSettingsPage> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
appText('Direct AI', 'Direct AI'),
appText('单机智能体', 'Single Agent'),
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 12),
@ -750,8 +750,8 @@ class _WebSettingsPageState extends State<WebSettingsPage> {
const SizedBox(height: 8),
Text(
appText(
'Root SPA 目标部署到 https://xworkmate.svc.plus/ 。Direct AI 需要浏览器可达且支持 CORS否则请使用 Relay 模式。',
'The root SPA targets https://xworkmate.svc.plus/ . Direct AI endpoints must be browser-reachable and CORS-compatible; otherwise use relay mode.',
'Root SPA 目标部署到 https://xworkmate.svc.plus/ 。单机智能体依赖的 AI Gateway endpoint 需要浏览器可达且支持 CORS否则请使用 Relay 模式。',
'The root SPA targets https://xworkmate.svc.plus/ . Single Agent AI Gateway endpoints must be browser-reachable and CORS-compatible; otherwise use relay mode.',
),
),
],
@ -782,9 +782,9 @@ String _themeLabel(ThemeMode mode) {
String _targetLabel(AssistantExecutionTarget target) {
return switch (target) {
AssistantExecutionTarget.aiGatewayOnly => appText(
'Direct AI Gateway',
'Direct AI Gateway',
AssistantExecutionTarget.singleAgent => appText(
'Single Agent',
'Single Agent',
),
AssistantExecutionTarget.remote => appText(
'Relay OpenClaw Gateway',

View File

@ -9,7 +9,7 @@
## Release Focus
- 持续 Assistant 任务线程与流式 AI Gateway 对话
- `仅 AI Gateway / 本地 OpenClaw Gateway / 远程 OpenClaw Gateway` 三模式统一
- `单机智能体 / 本地 OpenClaw Gateway / 远程 OpenClaw Gateway` 三模式统一
- `Architect / Engineer / Tester` 多 Agent 协作
- 可选 `ARIS` 框架、内嵌 skills、Go bridge runtime
- `Ollama Cloud` 文案和默认地址统一

View File

@ -70,7 +70,7 @@ void main() {
expect(
desktopAccess.availableExecutionTargets,
equals(<AssistantExecutionTarget>[
AssistantExecutionTarget.aiGatewayOnly,
AssistantExecutionTarget.singleAgent,
AssistantExecutionTarget.local,
AssistantExecutionTarget.remote,
]),
@ -82,7 +82,7 @@ void main() {
expect(
webAccess.availableExecutionTargets,
equals(<AssistantExecutionTarget>[
AssistantExecutionTarget.aiGatewayOnly,
AssistantExecutionTarget.singleAgent,
AssistantExecutionTarget.remote,
]),
);

View File

@ -196,7 +196,7 @@ void main() {
await _pumpForUiSync(tester);
await controller.setAssistantExecutionTarget(
AssistantExecutionTarget.aiGatewayOnly,
AssistantExecutionTarget.singleAgent,
);
await _pumpForUiSync(tester);
@ -212,7 +212,7 @@ void main() {
await _pumpForUiSync(tester);
final aiGroup = find.byKey(
const ValueKey<String>('assistant-task-group-aiGatewayOnly'),
const ValueKey<String>('assistant-task-group-singleAgent'),
);
final localGroup = find.byKey(
const ValueKey<String>('assistant-task-group-local'),
@ -246,7 +246,7 @@ void main() {
);
expect(
find.byKey(const ValueKey<String>('assistant-task-group-aiGatewayOnly')),
find.byKey(const ValueKey<String>('assistant-task-group-singleAgent')),
findsOneWidget,
);
expect(
@ -450,7 +450,7 @@ void main() {
);
await _pumpForUiSync(tester);
expect(find.text('仅 AI Gateway'), findsWidgets);
expect(find.text('单机智能体'), findsWidgets);
expect(find.text('本地 OpenClaw Gateway'), findsWidgets);
expect(find.text('远程 OpenClaw Gateway'), findsWidgets);
});
@ -586,7 +586,7 @@ void main() {
await _pumpForUiSync(tester);
await controller.setAssistantExecutionTarget(
AssistantExecutionTarget.aiGatewayOnly,
AssistantExecutionTarget.singleAgent,
);
await _pumpForUiSync(tester);
@ -619,11 +619,11 @@ void main() {
expect(
find.descendant(
of: find.byKey(const Key('assistant-execution-target-button')),
matching: find.text('仅 AI Gateway'),
matching: find.text('单机智能体'),
),
findsOneWidget,
);
expect(find.textContaining('仅 AI Gateway'), findsWidgets);
expect(find.textContaining('单机智能体'), findsWidgets);
},
skip: true,
);
@ -655,7 +655,7 @@ void main() {
sessionKey: 'main',
title: '研发任务',
archived: false,
executionTarget: AssistantExecutionTarget.aiGatewayOnly,
executionTarget: AssistantExecutionTarget.singleAgent,
messageViewMode: AssistantMessageViewMode.rendered,
updatedAtMs: 1700000000000,
messages: <GatewayChatMessage>[
@ -711,7 +711,7 @@ void main() {
// Known flutter_tester host-exit hang in this widget scenario.
testWidgets(
'AssistantPage shows AI Gateway-only chip and keeps task rows minimal',
'AssistantPage shows Single Agent chip and keeps task rows minimal',
(WidgetTester tester) async {
final controller = await createTestController(tester);
await controller.settingsController.saveAiGatewayApiKey('live-key');
@ -723,7 +723,7 @@ void main() {
selectedModels: const <String>['qwen2.5-coder:latest'],
),
defaultModel: 'qwen2.5-coder:latest',
assistantExecutionTarget: AssistantExecutionTarget.aiGatewayOnly,
assistantExecutionTarget: AssistantExecutionTarget.singleAgent,
),
refreshAfterSave: false,
);
@ -738,7 +738,7 @@ void main() {
findsOneWidget,
);
expect(
find.text('仅 AI Gateway · qwen2.5-coder:latest · 127.0.0.1:11434'),
find.text('单机智能体 · qwen2.5-coder:latest · 127.0.0.1:11434'),
findsOneWidget,
);
expect(find.text('等待描述这个任务的第一条消息'), findsNothing);
@ -773,7 +773,7 @@ Future<AppController> _createControllerWithThreadRecords({
availableModels: const <String>['qwen2.5-coder:latest'],
selectedModels: const <String>['qwen2.5-coder:latest'],
),
assistantExecutionTarget: AssistantExecutionTarget.aiGatewayOnly,
assistantExecutionTarget: AssistantExecutionTarget.singleAgent,
defaultModel: 'qwen2.5-coder:latest',
),
);

View File

@ -17,7 +17,7 @@ import 'package:xworkmate/runtime/secure_config_store.dart';
void main() {
test(
'AppController streams and restores persistent AI Gateway-only conversation turns',
'AppController streams and restores persistent Single Agent conversation turns',
() async {
SharedPreferences.setMockInitialValues(<String, Object>{});
final tempDirectory = await Directory.systemTemp.createTemp(
@ -62,12 +62,12 @@ void main() {
refreshAfterSave: false,
);
await controller.setAssistantExecutionTarget(
AssistantExecutionTarget.aiGatewayOnly,
AssistantExecutionTarget.singleAgent,
);
const firstQuestion =
'Execution context:\n'
'- target: ai-gateway-only\n'
'- target: single-agent\n'
'- workspace_root: /opt/data/workspace\n'
'- permission: full-access\n\n'
'今天聊点什么';
@ -114,7 +114,7 @@ void main() {
expect(secondController.chatMessages.last.text, 'FIRST_REPLY');
expect(
secondController.settings.assistantExecutionTarget,
AssistantExecutionTarget.aiGatewayOnly,
AssistantExecutionTarget.singleAgent,
);
final secondTurn = secondController.sendChatMessage(
@ -152,7 +152,7 @@ void main() {
secondController.connection.status,
RuntimeConnectionStatus.offline,
);
expect(secondController.assistantConnectionStatusLabel, '仅 AI Gateway');
expect(secondController.assistantConnectionStatusLabel, '单机智能体');
expect(
secondController.assistantConnectionTargetLabel,
'qwen2.5-coder:latest · 127.0.0.1:${server.port}',
@ -208,7 +208,7 @@ void main() {
refreshAfterSave: false,
);
await controller.setAssistantExecutionTarget(
AssistantExecutionTarget.aiGatewayOnly,
AssistantExecutionTarget.singleAgent,
);
await controller.sendChatMessage('你好', thinking: 'low');
@ -226,7 +226,7 @@ void main() {
);
test(
'AppController abortRun stops AI Gateway-only streaming requests',
'AppController abortRun stops Single Agent streaming requests',
() async {
SharedPreferences.setMockInitialValues(<String, Object>{});
final tempDirectory = await Directory.systemTemp.createTemp(
@ -270,7 +270,7 @@ void main() {
refreshAfterSave: false,
);
await controller.setAssistantExecutionTarget(
AssistantExecutionTarget.aiGatewayOnly,
AssistantExecutionTarget.singleAgent,
);
final pendingTurn = controller.sendChatMessage('今天聊点什么', thinking: 'low');

View File

@ -66,7 +66,7 @@ void main() {
selectedModels: const <String>['qwen2.5-coder:latest'],
),
defaultModel: 'gpt-5.4',
assistantExecutionTarget: AssistantExecutionTarget.aiGatewayOnly,
assistantExecutionTarget: AssistantExecutionTarget.singleAgent,
),
);

View File

@ -192,7 +192,7 @@ void main() {
await controller.saveSettings(
_withRemoteGatewayProfile(
controller.settings.copyWith(
assistantExecutionTarget: AssistantExecutionTarget.aiGatewayOnly,
assistantExecutionTarget: AssistantExecutionTarget.singleAgent,
aiGateway: controller.settings.aiGateway.copyWith(
baseUrl: 'http://127.0.0.1:11434/v1',
availableModels: const <String>['qwen2.5-coder:latest'],
@ -262,18 +262,18 @@ void main() {
);
await controller.setAssistantExecutionTarget(
AssistantExecutionTarget.aiGatewayOnly,
AssistantExecutionTarget.singleAgent,
);
expect(
controller.settings.assistantExecutionTarget,
AssistantExecutionTarget.aiGatewayOnly,
AssistantExecutionTarget.singleAgent,
);
expect(
controller.settings.primaryRemoteGatewayProfile.host,
'gateway.example.com',
reason:
'AI Gateway-only mode should preserve the saved remote endpoint.',
'Single Agent mode should preserve the saved remote endpoint.',
);
expect(controller.settings.primaryRemoteGatewayProfile.port, 9443);
expect(controller.settings.primaryRemoteGatewayProfile.tls, isTrue);
@ -282,7 +282,7 @@ void main() {
RuntimeConnectionMode.remote,
);
expect(gateway.disconnectCount, 1);
expect(controller.assistantConnectionStatusLabel, '仅 AI Gateway');
expect(controller.assistantConnectionStatusLabel, '单机智能体');
expect(
controller.assistantConnectionTargetLabel,
'qwen2.5-coder:latest · 127.0.0.1:11434',
@ -290,7 +290,7 @@ void main() {
expect(
gateway.connectedProfiles,
hasLength(2),
reason: 'AI Gateway-only mode should not open another gateway session.',
reason: 'Single Agent mode should not open another gateway session.',
);
await controller.setAssistantExecutionTarget(
@ -539,7 +539,7 @@ void main() {
);
test(
'AppController notifies aiGatewayOnly target changes before disconnect completes',
'AppController notifies singleAgent target changes before disconnect completes',
() async {
SharedPreferences.setMockInitialValues(<String, Object>{});
final tempDirectory = await Directory.systemTemp.createTemp(
@ -567,7 +567,7 @@ void main() {
await controller.saveSettings(
_withRemoteGatewayProfile(
controller.settings.copyWith(
assistantExecutionTarget: AssistantExecutionTarget.aiGatewayOnly,
assistantExecutionTarget: AssistantExecutionTarget.singleAgent,
aiGateway: controller.settings.aiGateway.copyWith(
baseUrl: 'http://127.0.0.1:11434/v1',
availableModels: const <String>['qwen2.5-coder:latest'],
@ -598,7 +598,7 @@ void main() {
gateway.holdNextDisconnect(disconnectGate);
final switchFuture = controller.setAssistantExecutionTarget(
AssistantExecutionTarget.aiGatewayOnly,
AssistantExecutionTarget.singleAgent,
);
var completed = false;
switchFuture.then((_) {
@ -611,9 +611,9 @@ void main() {
expect(notificationCount, greaterThan(0));
expect(
controller.assistantExecutionTarget,
AssistantExecutionTarget.aiGatewayOnly,
AssistantExecutionTarget.singleAgent,
);
expect(controller.assistantConnectionStatusLabel, '仅 AI Gateway');
expect(controller.assistantConnectionStatusLabel, '单机智能体');
expect(completed, isFalse);
} finally {
if (!disconnectGate.isCompleted) {
@ -625,13 +625,13 @@ void main() {
expect(
controller.settings.assistantExecutionTarget,
AssistantExecutionTarget.aiGatewayOnly,
AssistantExecutionTarget.singleAgent,
);
expect(
controller.assistantExecutionTarget,
AssistantExecutionTarget.aiGatewayOnly,
AssistantExecutionTarget.singleAgent,
);
expect(controller.assistantConnectionStatusLabel, '仅 AI Gateway');
expect(controller.assistantConnectionStatusLabel, '单机智能体');
},
);
@ -683,7 +683,7 @@ void main() {
controller.initializeAssistantThreadContext(
'main',
executionTarget: AssistantExecutionTarget.aiGatewayOnly,
executionTarget: AssistantExecutionTarget.singleAgent,
);
controller.initializeAssistantThreadContext(
'remote-thread',
@ -707,10 +707,10 @@ void main() {
expect(
controller.assistantExecutionTarget,
AssistantExecutionTarget.aiGatewayOnly,
AssistantExecutionTarget.singleAgent,
);
expect(gateway.disconnectCount, 1);
expect(controller.assistantConnectionStatusLabel, '仅 AI Gateway');
expect(controller.assistantConnectionStatusLabel, '单机智能体');
expect(
controller.settings.assistantExecutionTarget,
AssistantExecutionTarget.local,
@ -798,11 +798,11 @@ void main() {
controller.initializeAssistantThreadContext(
'main',
executionTarget: AssistantExecutionTarget.aiGatewayOnly,
executionTarget: AssistantExecutionTarget.singleAgent,
);
await controller.switchSession('main');
expect(controller.assistantConnectionStatusLabel, '仅 AI Gateway');
expect(controller.assistantConnectionStatusLabel, '单机智能体');
expect(
controller.assistantConnectionTargetLabel,
'qwen2.5-coder:latest · 127.0.0.1:11434',
@ -897,7 +897,7 @@ void main() {
firstController.initializeAssistantThreadContext(
'draft:alpha',
title: 'Alpha',
executionTarget: AssistantExecutionTarget.aiGatewayOnly,
executionTarget: AssistantExecutionTarget.singleAgent,
);
firstController.initializeAssistantThreadContext(
'draft:beta',

View File

@ -56,7 +56,7 @@ void main() {
await _waitFor(() => !controller.initializing);
await controller.setAssistantExecutionTarget(
AssistantExecutionTarget.aiGatewayOnly,
AssistantExecutionTarget.singleAgent,
);
expect(
@ -116,7 +116,7 @@ void main() {
await _waitFor(() => !controller.initializing);
await controller.setAssistantExecutionTarget(
AssistantExecutionTarget.aiGatewayOnly,
AssistantExecutionTarget.singleAgent,
);
final firstSessionKey = controller.currentSessionKey;
expect(
@ -138,7 +138,7 @@ void main() {
controller.initializeAssistantThreadContext(
'draft:thread-2',
title: 'Thread 2',
executionTarget: AssistantExecutionTarget.aiGatewayOnly,
executionTarget: AssistantExecutionTarget.singleAgent,
messageViewMode: AssistantMessageViewMode.rendered,
);
await controller.switchSession('draft:thread-2');

View File

@ -699,7 +699,7 @@ void main() {
],
selectedSkillKeys: <String>['/tmp/imported-skill'],
assistantModelId: 'gpt-5.4-mini',
gatewayEntryState: 'ai-gateway-only',
gatewayEntryState: 'single-agent',
updatedAtMs: 1700000000000,
messages: <GatewayChatMessage>[
GatewayChatMessage(
@ -757,7 +757,7 @@ void main() {
'/tmp/imported-skill',
]);
expect(reloadedRecords.first.assistantModelId, 'gpt-5.4-mini');
expect(reloadedRecords.first.gatewayEntryState, 'ai-gateway-only');
expect(reloadedRecords.first.gatewayEntryState, 'single-agent');
expect(reloadedRecords.first.messages, hasLength(2));
expect(reloadedRecords.first.messages.last.text, '第一条回复');
},
@ -782,18 +782,29 @@ void main() {
'updatedAtMs': 1700000000000,
'title': 'Legacy',
'archived': false,
'executionTarget': 'local',
'executionTarget': 'aiGatewayOnly',
'messageViewMode': 'rendered',
'gatewayEntryState': 'ai-gateway-only',
});
expect(decoded.executionTarget, AssistantExecutionTarget.singleAgent);
expect(decoded.discoveredSkills, isEmpty);
expect(decoded.importedSkills, isEmpty);
expect(decoded.selectedSkillKeys, isEmpty);
expect(decoded.assistantModelId, isEmpty);
expect(decoded.gatewayEntryState, isNull);
expect(decoded.gatewayEntryState, 'single-agent');
},
);
test('SettingsSnapshot keeps compatibility with legacy target json values', () {
final decoded = SettingsSnapshot.fromJson(<String, dynamic>{
...SettingsSnapshot.defaults().toJson(),
'assistantExecutionTarget': 'aiGatewayOnly',
});
expect(decoded.assistantExecutionTarget, AssistantExecutionTarget.singleAgent);
});
test(
'SecureConfigStore restores assistant state from durable files when sqlite entries are missing',
() async {
@ -820,7 +831,7 @@ void main() {
sessionKey: 'draft:backup-1',
title: '备份线程',
archived: false,
executionTarget: AssistantExecutionTarget.aiGatewayOnly,
executionTarget: AssistantExecutionTarget.singleAgent,
messageViewMode: AssistantMessageViewMode.rendered,
updatedAtMs: 1700000000000,
messages: <GatewayChatMessage>[

View File

@ -62,7 +62,7 @@ void main() {
updatedAtMs: 1,
title: 'hello',
archived: false,
executionTarget: AssistantExecutionTarget.aiGatewayOnly,
executionTarget: AssistantExecutionTarget.singleAgent,
messageViewMode: AssistantMessageViewMode.rendered,
),
];

View File

@ -12,7 +12,7 @@ import 'package:xworkmate/web/web_store.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test('web controller persists direct and relay configuration', () async {
test('web controller persists single-agent and relay configuration', () async {
SharedPreferences.setMockInitialValues(<String, Object>{});
final remoteRecords = <AssistantThreadRecord>[];
@ -24,7 +24,7 @@ void main() {
await _waitForReady(controller);
await controller.saveAiGatewayConfiguration(
name: 'Direct AI',
name: 'Single Agent',
baseUrl: 'https://api.example.com/v1',
provider: 'openai-compatible',
apiKey: 'sk-test-web',
@ -46,7 +46,7 @@ void main() {
AssistantExecutionTarget.remote,
);
await controller.createConversation(
target: AssistantExecutionTarget.aiGatewayOnly,
target: AssistantExecutionTarget.singleAgent,
);
final reloaded = AppController(
@ -133,7 +133,7 @@ void main() {
updatedAtMs: 1,
title: 'stale browser cache',
archived: false,
executionTarget: AssistantExecutionTarget.aiGatewayOnly,
executionTarget: AssistantExecutionTarget.singleAgent,
messageViewMode: AssistantMessageViewMode.rendered,
),
]);

View File

@ -20,7 +20,7 @@
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta
name="description"
content="XWorkmate Web keeps the Assistant-first workflow with Direct AI Gateway and Relay OpenClaw Gateway."
content="XWorkmate Web keeps the Assistant-first workflow with Single Agent and Relay OpenClaw Gateway."
>
<!-- iOS meta tags & icons -->

View File

@ -5,7 +5,7 @@
"display": "standalone",
"background_color": "#0175C2",
"theme_color": "#0175C2",
"description": "Assistant-first Flutter Web shell for Direct AI Gateway and Relay OpenClaw Gateway.",
"description": "Assistant-first Flutter Web shell for Single Agent and Relay OpenClaw Gateway.",
"orientation": "portrait-primary",
"prefer_related_applications": false,
"icons": [