Rename AI Gateway mode to Single Agent
This commit is contained in:
parent
ae1faa03d0
commit
6628bca6a2
@ -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-first:Scheduled Tasks 仍是 `cron.list` 只读视图;Memory 仍是 `memory/sync` 同步能力,不宣传 CRUD。
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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` 跟线程绑定,没设时回退到默认模型 |
|
||||
| 顶部连接状态 | 是 | 只显示当前线程解析出的连接状态 |
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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 组合
|
||||
- 是否看到流式输出
|
||||
- 是否发生自动回退
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
## 推荐配置
|
||||
|
||||
- 框架:`ARIS`
|
||||
- 执行模式:`仅 AI Gateway` 或 `本地 OpenClaw Gateway`
|
||||
- 执行模式:`单机智能体` 或 `本地 OpenClaw Gateway`
|
||||
- Ollama 端点:`http://127.0.0.1:11434`
|
||||
- Architect:`gemini`
|
||||
- Engineer:`opencode`
|
||||
|
||||
@ -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 已连接
|
||||
- 三种模式下线程都能继续追问
|
||||
- 任务列表分组归属与实际提交模式一致
|
||||
- 右上角状态只反映当前线程,不沿用别的线程连接结果
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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']),
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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` 文案和默认地址统一
|
||||
|
||||
@ -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,
|
||||
]),
|
||||
);
|
||||
|
||||
@ -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',
|
||||
),
|
||||
);
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -66,7 +66,7 @@ void main() {
|
||||
selectedModels: const <String>['qwen2.5-coder:latest'],
|
||||
),
|
||||
defaultModel: 'gpt-5.4',
|
||||
assistantExecutionTarget: AssistantExecutionTarget.aiGatewayOnly,
|
||||
assistantExecutionTarget: AssistantExecutionTarget.singleAgent,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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>[
|
||||
|
||||
@ -62,7 +62,7 @@ void main() {
|
||||
updatedAtMs: 1,
|
||||
title: 'hello',
|
||||
archived: false,
|
||||
executionTarget: AssistantExecutionTarget.aiGatewayOnly,
|
||||
executionTarget: AssistantExecutionTarget.singleAgent,
|
||||
messageViewMode: AssistantMessageViewMode.rendered,
|
||||
),
|
||||
];
|
||||
|
||||
@ -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,
|
||||
),
|
||||
]);
|
||||
|
||||
@ -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 -->
|
||||
|
||||
@ -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": [
|
||||
|
||||
Loading…
Reference in New Issue
Block a user