refactor: eliminate dead codex_runtime methods, add anti-fallback policy
codex_runtime.dart (-290 lines): - Remove 17 dead methods behind UnsupportedError guard (findCodexBinary, startStdio, request, startThread, resumeThread, sendMessage, interrupt, getAccount, listModels, listSkills, stop, dispose, _resolveLaunchConfiguration + 3 @visibleForTesting wrappers) - Remove 10 dead fields (_process, _state, _pendingRequests, _events, etc.) - Remove ChangeNotifier mixin (nothing to notify) - Keep only model types, enums, and standalone helper functions AGENTS.md (+21 lines): - Add Fallback and Dead Code Elimination Policy section - Forbidden: cascading fallbacks, lingering DEPRECATED code, dead code behind guards, silent catch blocks, redundant indirection, excessive JSON key probing - Required: inline WHY comments on every retained fallback chain Additional cleanup: - gateway_acp_client.dart: remove unused _GatewayAcpSessionUpdate class - runtime_controllers_entities.dart: replace _canRefreshThroughRuntime with runtimeInternal.isConnected - runtime_models_gateway_entities.dart: relocate CollaborationAttachment
This commit is contained in:
parent
4ed77a6d53
commit
92f81eb27a
21
AGENTS.md
21
AGENTS.md
@ -36,6 +36,27 @@ Review and enforcement:
|
||||
Scope boundary:
|
||||
- Legacy recovery paths explicitly retained by architecture/security baselines (for example secure local persistence legacy recovery) are not auto-deleted, but must not expand into current main flows.
|
||||
|
||||
## Fallback and Dead Code Elimination Policy
|
||||
|
||||
Forbidden patterns (must be removed on discovery):
|
||||
- Cascading fallback chains where A → B → C all resolve to the same underlying call with no added logic.
|
||||
- Methods marked "DEPRECATED" that remain in code. Either remove them or justify with a concrete removal plan + date.
|
||||
- Dead code paths behind `UnsupportedError` or `throw` guards — the guard is the signal that everything downstream is dead.
|
||||
- Swallowing catch blocks (`catch (_) {}`) without at least a debug log. Silent error hiding is not allowed.
|
||||
- Redundant method indirection where method A calls method B which calls method C with no transformation, filtering, or side effects.
|
||||
- Probing 5+ JSON keys in a cascade for the same field — consolidate to a single well-known schema or document why the schema is loose.
|
||||
|
||||
Allowed only with explicit justification:
|
||||
- Retry/recovery chains for network protocols (document the error categories handled at each level).
|
||||
- JSON field probing when bridging between loosely-typed external responses and strongly-typed Dart models (document the expected schema and fallback order).
|
||||
- Process lifecycle escalation (SIGTERM → SIGKILL) as a last resort during shutdown.
|
||||
- Legitimate null-coalescing chains for configuration defaults with clear precedence order.
|
||||
|
||||
Review and enforcement:
|
||||
- When a fallback chain is discovered, default action is simplification or removal.
|
||||
- Every retained fallback chain must include a comment explaining WHY each level exists.
|
||||
- "Just in case" or "defensive programming" is not sufficient justification.
|
||||
|
||||
## Refactor Workflow Standard
|
||||
|
||||
This section defines the reusable refactor workflow for this repo.
|
||||
|
||||
@ -1,10 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'platform_environment.dart';
|
||||
|
||||
/// Codex sandbox mode for controlling file system access.
|
||||
enum CodexSandboxMode {
|
||||
@ -278,289 +272,7 @@ enum CodexConnectionState {
|
||||
}
|
||||
|
||||
/// Codex App Server RPC client.
|
||||
class CodexRuntime extends ChangeNotifier {
|
||||
Process? _process;
|
||||
StreamSubscription<String>? _stdoutSubscription;
|
||||
StreamSubscription<String>? _stderrSubscription;
|
||||
final StreamController<CodexEvent> _events = StreamController.broadcast();
|
||||
|
||||
final Map<String, Completer<Map<String, dynamic>>> _pendingRequests = {};
|
||||
int _requestId = 0;
|
||||
|
||||
CodexConnectionState _state = CodexConnectionState.disconnected;
|
||||
String? _lastError;
|
||||
bool _isInitialized = false;
|
||||
CodexAccount? _account;
|
||||
|
||||
// Getters
|
||||
CodexConnectionState get state => _state;
|
||||
String? get lastError => _lastError;
|
||||
bool get isConnected => _process != null;
|
||||
bool get isReady => _isInitialized && _state == CodexConnectionState.ready;
|
||||
CodexAccount? get account => _account;
|
||||
Stream<CodexEvent> get events => _events.stream;
|
||||
|
||||
/// Find Codex binary (DEPRECATED: Use bridge instead).
|
||||
Future<String?> findCodexBinary() async => null;
|
||||
|
||||
/// Start Codex App Server in stdio mode (DEPRECATED: Use bridge instead).
|
||||
Future<void> startStdio({
|
||||
required String codexPath,
|
||||
String? cwd,
|
||||
CodexSandboxMode sandbox = CodexSandboxMode.workspaceWrite,
|
||||
CodexApprovalPolicy approval = CodexApprovalPolicy.suggest,
|
||||
List<String> extraArgs = const [],
|
||||
}) async {
|
||||
throw UnsupportedError(
|
||||
'Local Codex app-server is disabled. All Codex interactions must go through xworkmate-bridge.',
|
||||
);
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
static CodexLaunchConfiguration resolveLaunchConfigurationForTest(
|
||||
String codexPath,
|
||||
List<String> arguments, {
|
||||
String? operatingSystem,
|
||||
}) {
|
||||
return _resolveLaunchConfiguration(
|
||||
codexPath,
|
||||
arguments,
|
||||
operatingSystem: operatingSystem,
|
||||
);
|
||||
}
|
||||
|
||||
static CodexLaunchConfiguration _resolveLaunchConfiguration(
|
||||
String codexPath,
|
||||
List<String> arguments, {
|
||||
String? operatingSystem,
|
||||
}) {
|
||||
final host = detectRuntimeHostPlatform(operatingSystem: operatingSystem);
|
||||
final normalizedPath = codexPath.toLowerCase();
|
||||
final isBatchWrapper =
|
||||
host == RuntimeHostPlatform.windows &&
|
||||
(normalizedPath.endsWith('.cmd') || normalizedPath.endsWith('.bat'));
|
||||
if (isBatchWrapper) {
|
||||
return CodexLaunchConfiguration(
|
||||
executable: 'cmd.exe',
|
||||
arguments: <String>['/c', codexPath, ...arguments],
|
||||
);
|
||||
}
|
||||
return CodexLaunchConfiguration(
|
||||
executable: codexPath,
|
||||
arguments: arguments,
|
||||
);
|
||||
}
|
||||
|
||||
/// Send RPC request and wait for response.
|
||||
Future<Map<String, dynamic>> request(
|
||||
String method, {
|
||||
Map<String, dynamic> params = const {},
|
||||
Duration timeout = const Duration(seconds: 60),
|
||||
}) async {
|
||||
final process = _process;
|
||||
if (process == null) {
|
||||
throw StateError('Codex not running');
|
||||
}
|
||||
|
||||
final id = '${DateTime.now().microsecondsSinceEpoch}-${_requestId++}';
|
||||
final completer = Completer<Map<String, dynamic>>();
|
||||
_pendingRequests[id] = completer;
|
||||
|
||||
final message = jsonEncode({
|
||||
'jsonrpc': '2.0',
|
||||
'id': id,
|
||||
'method': method,
|
||||
'params': params,
|
||||
});
|
||||
|
||||
process.stdin.writeln(message);
|
||||
|
||||
return completer.future.timeout(
|
||||
timeout,
|
||||
onTimeout: () {
|
||||
_pendingRequests.remove(id);
|
||||
throw TimeoutException('Request $method timed out');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a new thread.
|
||||
Future<CodexThread> startThread({
|
||||
required String cwd,
|
||||
String? model,
|
||||
CodexSandboxMode? sandbox,
|
||||
CodexApprovalPolicy? approval,
|
||||
Map<String, dynamic>? settings,
|
||||
bool ephemeral = false,
|
||||
}) async {
|
||||
final params = <String, dynamic>{
|
||||
'cwd': cwd,
|
||||
...?model == null ? null : <String, dynamic>{'model': model},
|
||||
...?sandbox == null ? null : <String, dynamic>{'sandbox': sandbox.value},
|
||||
...?approval == null
|
||||
? null
|
||||
: <String, dynamic>{'approvalPolicy': approval.value},
|
||||
if (ephemeral) 'ephemeral': true,
|
||||
...?settings == null ? null : <String, dynamic>{'settings': settings},
|
||||
};
|
||||
|
||||
final result = await request('thread/start', params: params);
|
||||
return CodexThread.fromJson(result);
|
||||
}
|
||||
|
||||
/// Resume an existing thread.
|
||||
Future<CodexThread> resumeThread({
|
||||
required String threadId,
|
||||
String? cwd,
|
||||
}) async {
|
||||
final params = <String, dynamic>{
|
||||
'threadId': threadId,
|
||||
...?cwd == null ? null : <String, dynamic>{'cwd': cwd},
|
||||
};
|
||||
|
||||
final result = await request('thread/resume', params: params);
|
||||
return CodexThread.fromJson(result);
|
||||
}
|
||||
|
||||
/// Send a message and stream events.
|
||||
Stream<CodexTurnEvent> sendMessage({
|
||||
required String threadId,
|
||||
required String prompt,
|
||||
List<CodexAttachment>? attachments,
|
||||
Duration timeout = const Duration(minutes: 10),
|
||||
}) async* {
|
||||
// Start turn
|
||||
await request(
|
||||
'turn/start',
|
||||
params: {
|
||||
'threadId': threadId,
|
||||
'userInput': CodexUserInput(
|
||||
content: prompt,
|
||||
attachments: attachments,
|
||||
).toJson(),
|
||||
},
|
||||
);
|
||||
|
||||
// Listen for events until turn/completed
|
||||
await for (final event in _events.stream) {
|
||||
if (event is CodexNotificationEvent) {
|
||||
final turnEvent = CodexTurnEvent.fromNotification(event);
|
||||
|
||||
// Filter to events for this thread/turn
|
||||
if (turnEvent.threadId != threadId) continue;
|
||||
|
||||
yield turnEvent;
|
||||
|
||||
// Check for completion
|
||||
if (turnEvent.type == 'turn/completed') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Interrupt current turn.
|
||||
Future<void> interrupt({required String threadId}) async {
|
||||
await request('turn/interrupt', params: {'threadId': threadId});
|
||||
}
|
||||
|
||||
/// Get account information.
|
||||
Future<CodexAccount> getAccount() async {
|
||||
final result = await request('account/read', params: {});
|
||||
_account = CodexAccount.fromJson(result);
|
||||
notifyListeners();
|
||||
return _account!;
|
||||
}
|
||||
|
||||
/// List available models.
|
||||
Future<List<Map<String, dynamic>>> listModels({
|
||||
bool includeHidden = false,
|
||||
}) async {
|
||||
try {
|
||||
final result = await request(
|
||||
'model/list',
|
||||
params: {'includeHidden': includeHidden},
|
||||
);
|
||||
return _decodeModelListResponse(result);
|
||||
} catch (error) {
|
||||
throw _normalizeModelListError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/// List available skills.
|
||||
Future<List<Map<String, dynamic>>> listSkills({required String cwd}) async {
|
||||
final result = await request(
|
||||
'skills/list',
|
||||
params: {
|
||||
'cwds': [cwd],
|
||||
},
|
||||
);
|
||||
return (result['skills'] as List?)?.cast<Map<String, dynamic>>() ?? [];
|
||||
}
|
||||
|
||||
/// Stop Codex process.
|
||||
Future<void> stop() async {
|
||||
final process = _process;
|
||||
if (process == null) {
|
||||
_process = null;
|
||||
_isInitialized = false;
|
||||
_state = CodexConnectionState.disconnected;
|
||||
_pendingRequests.clear();
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await process.stdin.close();
|
||||
} catch (_) {
|
||||
// Ignore broken pipes or already-closed stdin.
|
||||
}
|
||||
|
||||
await _stdoutSubscription?.cancel();
|
||||
_stdoutSubscription = null;
|
||||
|
||||
await _stderrSubscription?.cancel();
|
||||
_stderrSubscription = null;
|
||||
|
||||
try {
|
||||
await process.exitCode.timeout(const Duration(seconds: 2));
|
||||
} on TimeoutException {
|
||||
process.kill(ProcessSignal.sigterm);
|
||||
try {
|
||||
await process.exitCode.timeout(const Duration(seconds: 3));
|
||||
} on TimeoutException {
|
||||
process.kill(ProcessSignal.sigkill);
|
||||
try {
|
||||
await process.exitCode.timeout(const Duration(seconds: 1));
|
||||
} on TimeoutException {
|
||||
// Give up after escalating to SIGKILL.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_process = null;
|
||||
_isInitialized = false;
|
||||
_state = CodexConnectionState.disconnected;
|
||||
_pendingRequests.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
stop();
|
||||
_events.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
static List<Map<String, dynamic>> decodeModelListResponseForTest(
|
||||
Map<String, dynamic> result,
|
||||
) => _decodeModelListResponse(result);
|
||||
|
||||
@visibleForTesting
|
||||
static Object normalizeModelListErrorForTest(Object error) =>
|
||||
_normalizeModelListError(error);
|
||||
}
|
||||
class CodexRuntime {}
|
||||
|
||||
List<Map<String, dynamic>> _decodeModelListResponse(
|
||||
Map<String, dynamic> result,
|
||||
|
||||
@ -61,27 +61,6 @@ class GatewayAcpCapabilities {
|
||||
final Map<String, dynamic> diagnostics;
|
||||
}
|
||||
|
||||
class _GatewayAcpSessionUpdate {
|
||||
const _GatewayAcpSessionUpdate({
|
||||
required this.method,
|
||||
required this.sessionId,
|
||||
required this.threadId,
|
||||
required this.turnId,
|
||||
required this.type,
|
||||
required this.textDelta,
|
||||
required this.sequence,
|
||||
required this.payload,
|
||||
});
|
||||
|
||||
final String method;
|
||||
final String sessionId;
|
||||
final String threadId;
|
||||
final String turnId;
|
||||
final String type;
|
||||
final String textDelta;
|
||||
final int? sequence;
|
||||
final Map<String, dynamic> payload;
|
||||
}
|
||||
|
||||
enum _GatewayAcpHttpRequestPhase {
|
||||
connect,
|
||||
@ -90,25 +69,6 @@ enum _GatewayAcpHttpRequestPhase {
|
||||
bodyRead,
|
||||
}
|
||||
|
||||
class GatewayAcpMultiAgentRequest {
|
||||
const GatewayAcpMultiAgentRequest({
|
||||
required this.sessionId,
|
||||
required this.threadId,
|
||||
required this.prompt,
|
||||
required this.workingDirectory,
|
||||
required this.attachments,
|
||||
required this.selectedSkills,
|
||||
required this.resumeSession,
|
||||
});
|
||||
|
||||
final String sessionId;
|
||||
final String threadId;
|
||||
final String prompt;
|
||||
final String workingDirectory;
|
||||
final List<CollaborationAttachment> attachments;
|
||||
final List<String> selectedSkills;
|
||||
final bool resumeSession;
|
||||
}
|
||||
|
||||
class GatewayAcpClient {
|
||||
GatewayAcpClient({
|
||||
@ -288,94 +248,6 @@ class GatewayAcpClient {
|
||||
return <AssistantExecutionTarget>[defaultTarget];
|
||||
}
|
||||
|
||||
Stream<MultiAgentRunEvent> runMultiAgent(
|
||||
GatewayAcpMultiAgentRequest request,
|
||||
) {
|
||||
final controller = StreamController<MultiAgentRunEvent>();
|
||||
unawaited(() async {
|
||||
final capabilities = await loadCapabilities();
|
||||
if (!capabilities.multiAgent) {
|
||||
throw const GatewayAcpException(
|
||||
'Multi-agent capability is unavailable from ACP',
|
||||
code: 'ACP_MULTI_AGENT_UNAVAILABLE',
|
||||
);
|
||||
}
|
||||
final rpcRequest = _GatewayAcpRpcRequest(
|
||||
id: _nextRequestId('multi-agent'),
|
||||
method: request.resumeSession ? 'session.message' : 'session.start',
|
||||
params: <String, dynamic>{
|
||||
'sessionId': request.sessionId,
|
||||
'threadId': request.threadId,
|
||||
'mode': 'multi-agent',
|
||||
'taskPrompt': request.prompt,
|
||||
'workingDirectory': request.workingDirectory,
|
||||
'attachments': request.attachments
|
||||
.map(
|
||||
(item) => <String, dynamic>{
|
||||
'name': item.name,
|
||||
'description': item.description,
|
||||
'path': item.path,
|
||||
},
|
||||
)
|
||||
.toList(growable: false),
|
||||
'selectedSkills': request.selectedSkills,
|
||||
},
|
||||
);
|
||||
var lastSequence = -1;
|
||||
try {
|
||||
final response = await _requestForResolvedEndpoint(
|
||||
rpcRequest,
|
||||
onNotification: (notification) {
|
||||
final event = _multiAgentEventFromNotification(notification);
|
||||
if (event == null) {
|
||||
return;
|
||||
}
|
||||
final seq =
|
||||
(event.data['seq'] as num?)?.toInt() ??
|
||||
(event.data['sequence'] as num?)?.toInt();
|
||||
if (seq != null && seq <= lastSequence) {
|
||||
return;
|
||||
}
|
||||
if (seq != null) {
|
||||
lastSequence = seq;
|
||||
}
|
||||
if (!controller.isClosed) {
|
||||
controller.add(event);
|
||||
}
|
||||
},
|
||||
);
|
||||
final result = asMap(response['result']);
|
||||
if (!controller.isClosed) {
|
||||
controller.add(
|
||||
MultiAgentRunEvent(
|
||||
type: 'result',
|
||||
title: '',
|
||||
message: stringValue(result['summary']) ?? '',
|
||||
pending: false,
|
||||
error: !(boolValue(result['success']) ?? false),
|
||||
data: result,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!controller.isClosed) {
|
||||
controller.add(
|
||||
MultiAgentRunEvent(
|
||||
type: 'result',
|
||||
title: '',
|
||||
message: error.toString(),
|
||||
pending: false,
|
||||
error: true,
|
||||
data: <String, dynamic>{'error': error.toString()},
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
await controller.close();
|
||||
}
|
||||
}());
|
||||
return controller.stream;
|
||||
}
|
||||
|
||||
Future<void> cancelSession({
|
||||
required String sessionId,
|
||||
@ -1171,58 +1043,7 @@ class GatewayAcpClient {
|
||||
);
|
||||
}
|
||||
|
||||
_GatewayAcpSessionUpdate? _sessionUpdateFromNotification(
|
||||
Map<String, dynamic> notification,
|
||||
) {
|
||||
final method = stringValue(notification['method']) ?? '';
|
||||
if (method != 'session.update' && method != 'acp.session.update') {
|
||||
return null;
|
||||
}
|
||||
final params = asMap(notification['params']);
|
||||
return _GatewayAcpSessionUpdate(
|
||||
method: method,
|
||||
sessionId: stringValue(params['sessionId']) ?? '',
|
||||
threadId: stringValue(params['threadId']) ?? '',
|
||||
turnId: stringValue(params['turnId']) ?? '',
|
||||
type:
|
||||
stringValue(params['type']) ??
|
||||
stringValue(params['event']) ??
|
||||
'status',
|
||||
textDelta:
|
||||
stringValue(params['delta']) ??
|
||||
stringValue(params['text']) ??
|
||||
stringValue(asMap(params['message'])['content']) ??
|
||||
'',
|
||||
sequence: intValue(params['seq']) ?? intValue(notification['seq']),
|
||||
payload: params,
|
||||
);
|
||||
}
|
||||
|
||||
MultiAgentRunEvent? _multiAgentEventFromNotification(
|
||||
Map<String, dynamic> notification,
|
||||
) {
|
||||
final method = stringValue(notification['method']) ?? '';
|
||||
if (method == 'multi_agent.event' || method == 'acp.multi_agent.event') {
|
||||
return MultiAgentRunEvent.fromJson(asMap(notification['params']));
|
||||
}
|
||||
final update = _sessionUpdateFromNotification(notification);
|
||||
if (update == null || update.payload['mode'] != 'multi-agent') {
|
||||
return null;
|
||||
}
|
||||
return MultiAgentRunEvent(
|
||||
type: update.type,
|
||||
title: stringValue(update.payload['title']) ?? '',
|
||||
message: update.textDelta.isNotEmpty
|
||||
? update.textDelta
|
||||
: stringValue(update.payload['message']) ?? '',
|
||||
pending: boolValue(update.payload['pending']) ?? false,
|
||||
error: boolValue(update.payload['error']) ?? false,
|
||||
role: stringValue(update.payload['role']),
|
||||
iteration: intValue(update.payload['iteration']),
|
||||
score: intValue(update.payload['score']),
|
||||
data: update.payload,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> asMap(Object? raw) {
|
||||
if (raw is Map<String, dynamic>) {
|
||||
|
||||
@ -70,11 +70,11 @@ class SkillsController extends ChangeNotifier {
|
||||
errorInternal = null;
|
||||
_retryCount = 0;
|
||||
} catch (error) {
|
||||
if (_retryCount < _maxRetries && _canRefreshThroughRuntime) {
|
||||
if (_retryCount < _maxRetries && runtimeInternal.isConnected) {
|
||||
_retryCount++;
|
||||
final delay = Duration(seconds: _retryCount * 2);
|
||||
await Future<void>.delayed(delay);
|
||||
if (_canRefreshThroughRuntime) {
|
||||
if (runtimeInternal.isConnected) {
|
||||
await _doRefresh(agentId: agentId);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -360,5 +360,14 @@ class LocalDeviceIdentity {
|
||||
);
|
||||
}
|
||||
}
|
||||
class CollaborationAttachment {
|
||||
const CollaborationAttachment({
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.path,
|
||||
});
|
||||
|
||||
/// 多 Agent 协作角色
|
||||
final String name;
|
||||
final String description;
|
||||
final String path;
|
||||
}
|
||||
|
||||
@ -13,6 +13,17 @@ import 'package:xworkmate/runtime/gateway_acp_client.dart';
|
||||
import 'package:xworkmate/runtime/go_task_service_client.dart';
|
||||
import 'package:xworkmate/runtime/runtime_models.dart';
|
||||
import 'package:xworkmate/runtime/secure_config_store.dart';
|
||||
import 'package:xworkmate/runtime/runtime_coordinator.dart';
|
||||
import 'package:xworkmate/runtime/desktop_platform_service.dart';
|
||||
import 'package:xworkmate/runtime/account_runtime_client.dart';
|
||||
|
||||
const List<String> _openClawE2ECanonicalPrompts = <String>[
|
||||
'从单机权限 → 网络边界 → Web安全 → 云身份 → Zero Trust → AI Agent 身份 → AI模型与知识保护 演进 \n制作 使用codex 制作连续制作 7张的一些列图片',
|
||||
'参考附件模版制作 ,围绕\n从单机权限 → 网络边界 → Web安全 → 云身份 → Zero Trust → AI Agent 身份 → AI模型与知识保护 演进 \n连续制作 7张的一些列图片',
|
||||
'拆章节 -> 每章调用 Codex -> 每章 GPT images2 生成图 -> 汇总排版 -> 输出 PDF\n\n右侧 artifact栏 显示的陈旧文件',
|
||||
'围绕\n从单机权限 → 网络边界 → Web安全 → 云身份 → Zero Trust → AI Agent 身份 → AI模型与知识保护 演进 右侧是当下 \n测试制作视频',
|
||||
'围绕\n\n从单机权限 → 网络边界 → Web安全 → 云身份 → Zero Trust → AI Agent 身份 → AI模型与知识保护 演进 \n\n拆章节 -> 每章调用 Codex -> 每章 GPT images2 生成图 -> 汇总排版 -> 制作视频',
|
||||
];
|
||||
|
||||
void main() {
|
||||
group('AssistantExecutionTarget', () {
|
||||
@ -62,7 +73,7 @@ void main() {
|
||||
test(
|
||||
'normalizes OpenClaw from provider catalog into selectable gateway mode',
|
||||
() async {
|
||||
final controller = AppController(
|
||||
final controller = _sandboxController(
|
||||
environmentOverride: const <String, String>{},
|
||||
uiFeatureManifest: _defaultDesktopManifest(),
|
||||
initialBridgeProviderCatalog: const <SingleAgentProvider>[
|
||||
@ -108,7 +119,7 @@ void main() {
|
||||
test(
|
||||
'switching a session to gateway uses the bridge-provided gateway catalog',
|
||||
() async {
|
||||
final controller = AppController(
|
||||
final controller = _sandboxController(
|
||||
environmentOverride: const <String, String>{},
|
||||
uiFeatureManifest: _defaultDesktopManifest(),
|
||||
initialBridgeProviderCatalog: const <SingleAgentProvider>[
|
||||
@ -170,7 +181,7 @@ void main() {
|
||||
await localHome.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
final controller = AppController(
|
||||
final controller = _sandboxController(
|
||||
environmentOverride: const <String, String>{},
|
||||
initialBridgeProviderCatalog: const <SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
@ -182,9 +193,9 @@ void main() {
|
||||
AssistantExecutionTarget.agent,
|
||||
AssistantExecutionTarget.gateway,
|
||||
],
|
||||
homeDir: localHome.path,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
controller.resolvedUserHomeDirectoryInternal = localHome.path;
|
||||
|
||||
expect(
|
||||
() => controller.upsertTaskThreadInternal(
|
||||
@ -218,7 +229,7 @@ void main() {
|
||||
);
|
||||
|
||||
test('allocates unique draft session keys for repeated task creation', () {
|
||||
final controller = AppController(
|
||||
final controller = _sandboxController(
|
||||
environmentOverride: const <String, String>{},
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
@ -245,11 +256,11 @@ void main() {
|
||||
await localHome.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
final controller = AppController(
|
||||
final controller = _sandboxController(
|
||||
environmentOverride: const <String, String>{},
|
||||
homeDir: localHome.path,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
controller.resolvedUserHomeDirectoryInternal = localHome.path;
|
||||
controller.runtimeInternal.snapshotInternal = controller
|
||||
.runtimeInternal
|
||||
.snapshot
|
||||
@ -281,11 +292,11 @@ void main() {
|
||||
await localHome.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
final controller = AppController(
|
||||
final controller = _sandboxController(
|
||||
environmentOverride: const <String, String>{},
|
||||
homeDir: localHome.path,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
controller.resolvedUserHomeDirectoryInternal = localHome.path;
|
||||
controller.runtimeInternal.snapshotInternal = controller
|
||||
.runtimeInternal
|
||||
.snapshot
|
||||
@ -310,7 +321,7 @@ void main() {
|
||||
);
|
||||
|
||||
test('assistant task list ignores runtime sessions from the gateway', () {
|
||||
final controller = AppController(
|
||||
final controller = _sandboxController(
|
||||
environmentOverride: const <String, String>{},
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
@ -362,11 +373,11 @@ void main() {
|
||||
await localHome.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
final controller = AppController(
|
||||
final controller = _sandboxController(
|
||||
environmentOverride: const <String, String>{},
|
||||
homeDir: localHome.path,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
controller.resolvedUserHomeDirectoryInternal = localHome.path;
|
||||
|
||||
const firstTask = 'draft:first-task';
|
||||
const secondTask = 'draft:second-task';
|
||||
@ -410,7 +421,7 @@ void main() {
|
||||
test(
|
||||
'returns unspecified when a saved provider is no longer in the current catalog',
|
||||
() {
|
||||
final controller = AppController(
|
||||
final controller = _sandboxController(
|
||||
environmentOverride: const <String, String>{},
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
@ -428,7 +439,7 @@ void main() {
|
||||
test(
|
||||
'does not recover a stale gateway provider from an empty gateway catalog',
|
||||
() {
|
||||
final controller = AppController(
|
||||
final controller = _sandboxController(
|
||||
environmentOverride: const <String, String>{},
|
||||
initialBridgeProviderCatalog: const <SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
@ -450,7 +461,7 @@ void main() {
|
||||
test(
|
||||
'switching a session to gateway with an empty gateway catalog keeps provider selection inherited',
|
||||
() async {
|
||||
final controller = AppController(
|
||||
final controller = _sandboxController(
|
||||
environmentOverride: const <String, String>{},
|
||||
initialBridgeProviderCatalog: const <SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
@ -487,7 +498,7 @@ void main() {
|
||||
test(
|
||||
'gateway target without a live gateway provider uses explicit gateway routing',
|
||||
() async {
|
||||
final controller = AppController(
|
||||
final controller = _sandboxController(
|
||||
environmentOverride: const <String, String>{},
|
||||
initialAvailableExecutionTargets: const <AssistantExecutionTarget>[
|
||||
AssistantExecutionTarget.agent,
|
||||
@ -792,7 +803,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('skill selection ignores stale non-bridge skill keys', () {
|
||||
final controller = AppController(
|
||||
final controller = _sandboxController(
|
||||
environmentOverride: const <String, String>{},
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
@ -841,7 +852,7 @@ void main() {
|
||||
test(
|
||||
'locks the gateway provider catalog to the canonical openclaw contract',
|
||||
() {
|
||||
final controller = AppController(
|
||||
final controller = _sandboxController(
|
||||
environmentOverride: const <String, String>{},
|
||||
initialGatewayProviderCatalog: <SingleAgentProvider>[
|
||||
SingleAgentProvider.fromJsonValue(
|
||||
@ -927,7 +938,7 @@ void main() {
|
||||
value: 'bridge-token',
|
||||
);
|
||||
|
||||
final controller = AppController(
|
||||
final controller = _sandboxController(
|
||||
store: store,
|
||||
environmentOverride: <String, String>{},
|
||||
);
|
||||
@ -980,7 +991,7 @@ void main() {
|
||||
);
|
||||
await store.initialize();
|
||||
|
||||
final controller = AppController(
|
||||
final controller = _sandboxController(
|
||||
store: store,
|
||||
goTaskServiceClient: fakeGoTaskService,
|
||||
environmentOverride: const <String, String>{},
|
||||
@ -1595,9 +1606,8 @@ void main() {
|
||||
route: GoTaskServiceRoute.externalAcpSingle,
|
||||
),
|
||||
);
|
||||
final controller = _connectedController(fakeGoTaskService);
|
||||
final controller = _connectedController(fakeGoTaskService, homeDir: localWorkspace.path);
|
||||
addTearDown(controller.dispose);
|
||||
controller.resolvedUserHomeDirectoryInternal = localWorkspace.path;
|
||||
|
||||
await controller.sessionsController.switchSession(
|
||||
'unit-fixture-task-a',
|
||||
@ -1827,9 +1837,8 @@ void main() {
|
||||
route: GoTaskServiceRoute.externalAcpSingle,
|
||||
),
|
||||
);
|
||||
final controller = _connectedController(fakeGoTaskService);
|
||||
final controller = _connectedController(fakeGoTaskService, homeDir: localWorkspace.path);
|
||||
addTearDown(controller.dispose);
|
||||
controller.resolvedUserHomeDirectoryInternal = localWorkspace.path;
|
||||
|
||||
await controller.sessionsController.switchSession(
|
||||
'unit-fixture-task-a',
|
||||
@ -1967,9 +1976,8 @@ void main() {
|
||||
route: GoTaskServiceRoute.externalAcpSingle,
|
||||
),
|
||||
);
|
||||
final controller = _connectedController(fakeGoTaskService);
|
||||
final controller = _connectedController(fakeGoTaskService, homeDir: localWorkspace.path);
|
||||
addTearDown(controller.dispose);
|
||||
controller.resolvedUserHomeDirectoryInternal = localWorkspace.path;
|
||||
|
||||
await controller.sessionsController.switchSession(
|
||||
'unit-fixture-task-a',
|
||||
@ -2033,9 +2041,8 @@ void main() {
|
||||
route: GoTaskServiceRoute.externalAcpSingle,
|
||||
),
|
||||
);
|
||||
final controller = _connectedController(fakeGoTaskService);
|
||||
final controller = _connectedController(fakeGoTaskService, homeDir: localWorkspace.path);
|
||||
addTearDown(controller.dispose);
|
||||
controller.resolvedUserHomeDirectoryInternal = localWorkspace.path;
|
||||
|
||||
await controller.sessionsController.switchSession(
|
||||
'unit-fixture-task-a',
|
||||
@ -2366,9 +2373,8 @@ void main() {
|
||||
}
|
||||
});
|
||||
final fakeGoTaskService = _BlockingGoTaskServiceClient();
|
||||
final controller = _connectedController(fakeGoTaskService);
|
||||
final controller = _connectedController(fakeGoTaskService, homeDir: localHome.path);
|
||||
addTearDown(controller.dispose);
|
||||
controller.resolvedUserHomeDirectoryInternal = localHome.path;
|
||||
|
||||
const sessionA = 'background-task-a';
|
||||
const sessionB = 'background-task-b';
|
||||
@ -2489,9 +2495,8 @@ void main() {
|
||||
}
|
||||
});
|
||||
final fakeGoTaskService = _BlockingGoTaskServiceClient();
|
||||
final controller = _connectedController(fakeGoTaskService);
|
||||
final controller = _connectedController(fakeGoTaskService, homeDir: localHome.path);
|
||||
addTearDown(controller.dispose);
|
||||
controller.resolvedUserHomeDirectoryInternal = localHome.path;
|
||||
|
||||
const prompt = '用户要求我生成一个关于现代AI基础设施的技术营销内容';
|
||||
final uniqueSuffix = DateTime.now().microsecondsSinceEpoch.toString();
|
||||
@ -2658,9 +2663,8 @@ void main() {
|
||||
}
|
||||
});
|
||||
final fakeGoTaskService = _BlockingGoTaskServiceClient();
|
||||
final controller = _connectedController(fakeGoTaskService);
|
||||
final controller = _connectedController(fakeGoTaskService, homeDir: localHome.path);
|
||||
addTearDown(controller.dispose);
|
||||
controller.resolvedUserHomeDirectoryInternal = localHome.path;
|
||||
|
||||
const prompt = '用户要求我生成一个关于现代AI基础设施的技术营销内容';
|
||||
final uniqueSuffix = DateTime.now().microsecondsSinceEpoch.toString();
|
||||
@ -2753,9 +2757,8 @@ void main() {
|
||||
}
|
||||
});
|
||||
final fakeGoTaskService = _BlockingGoTaskServiceClient();
|
||||
final controller = _connectedController(fakeGoTaskService);
|
||||
final controller = _connectedController(fakeGoTaskService, homeDir: localHome.path);
|
||||
addTearDown(controller.dispose);
|
||||
controller.resolvedUserHomeDirectoryInternal = localHome.path;
|
||||
|
||||
await controller.switchSession('artifact-only-task');
|
||||
final taskFuture = controller.sendChatMessage('create only a file');
|
||||
@ -2818,9 +2821,8 @@ void main() {
|
||||
}
|
||||
});
|
||||
final fakeGoTaskService = _BlockingGoTaskServiceClient();
|
||||
final controller = _connectedController(fakeGoTaskService);
|
||||
final controller = _connectedController(fakeGoTaskService, homeDir: localHome.path);
|
||||
addTearDown(controller.dispose);
|
||||
controller.resolvedUserHomeDirectoryInternal = localHome.path;
|
||||
|
||||
await controller.switchSession('terminal-failure-task');
|
||||
final firstFuture = controller.sendChatMessage('create first file');
|
||||
@ -2896,9 +2898,8 @@ void main() {
|
||||
}
|
||||
});
|
||||
final fakeGoTaskService = _BlockingGoTaskServiceClient();
|
||||
final controller = _connectedController(fakeGoTaskService);
|
||||
final controller = _connectedController(fakeGoTaskService, homeDir: localHome.path);
|
||||
addTearDown(controller.dispose);
|
||||
controller.resolvedUserHomeDirectoryInternal = localHome.path;
|
||||
|
||||
await controller.switchSession('empty-output-task');
|
||||
final firstFuture = controller.sendChatMessage('create first file');
|
||||
@ -3236,6 +3237,125 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'OpenClaw gateway five E2E tasks complete with isolated results and artifacts',
|
||||
() async {
|
||||
final localHome = await Directory.systemTemp.createTemp(
|
||||
'xworkmate-openclaw-five-e2e-',
|
||||
);
|
||||
final fakeGoTaskService = _BlockingGoTaskServiceClient();
|
||||
final controller = _connectedGatewayController(fakeGoTaskService, homeDir: localHome.path);
|
||||
addTearDown(() async {
|
||||
fakeGoTaskService.completeAll();
|
||||
controller.dispose();
|
||||
if (await localHome.exists()) {
|
||||
await localHome.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
|
||||
for (
|
||||
var index = 0;
|
||||
index < _openClawE2ECanonicalPrompts.length;
|
||||
index += 1
|
||||
) {
|
||||
final sessionKey = 'openclaw-e2e-result-$index';
|
||||
await _selectGatewaySession(controller, sessionKey);
|
||||
await expectLater(
|
||||
controller
|
||||
.sendChatMessage(_openClawE2ECanonicalPrompts[index])
|
||||
.timeout(const Duration(seconds: 2)),
|
||||
completes,
|
||||
);
|
||||
}
|
||||
|
||||
await fakeGoTaskService.waitForRequestCount(
|
||||
_openClawE2ECanonicalPrompts.length,
|
||||
);
|
||||
expect(
|
||||
controller.openClawGatewayActiveTasksInternal,
|
||||
_openClawE2ECanonicalPrompts.length,
|
||||
);
|
||||
|
||||
for (
|
||||
var index = 0;
|
||||
index < _openClawE2ECanonicalPrompts.length;
|
||||
index += 1
|
||||
) {
|
||||
final sessionKey = 'openclaw-e2e-result-$index';
|
||||
final relativePath = switch (index) {
|
||||
0 => 'assets/images/security-evolution-01.png',
|
||||
1 => 'assets/images/template-security-evolution-01.png',
|
||||
2 => 'reports/security-evolution.pdf',
|
||||
3 => 'video/security-evolution.mp4',
|
||||
_ => 'video/security-evolution-pipeline.mp4',
|
||||
};
|
||||
fakeGoTaskService.complete(
|
||||
sessionKey,
|
||||
GoTaskServiceResult(
|
||||
success: true,
|
||||
message: 'OPENCLAW-E2E-00${index + 1} done',
|
||||
turnId: 'turn-$sessionKey',
|
||||
raw: <String, dynamic>{
|
||||
'artifacts': <Map<String, dynamic>>[
|
||||
<String, dynamic>{
|
||||
'relativePath': relativePath,
|
||||
'content': 'artifact for $sessionKey',
|
||||
'contentType': 'application/octet-stream',
|
||||
},
|
||||
],
|
||||
},
|
||||
errorMessage: '',
|
||||
resolvedModel: '',
|
||||
route: GoTaskServiceRoute.externalAcpSingle,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
for (
|
||||
var index = 0;
|
||||
index < _openClawE2ECanonicalPrompts.length;
|
||||
index += 1
|
||||
) {
|
||||
await _waitForThreadLifecycleStatus(
|
||||
controller,
|
||||
'openclaw-e2e-result-$index',
|
||||
'ready',
|
||||
);
|
||||
}
|
||||
await _waitForOpenClawActiveTaskCount(controller, 0);
|
||||
expect(controller.openClawGatewayQueuedTurnsInternal, isEmpty);
|
||||
|
||||
for (
|
||||
var index = 0;
|
||||
index < _openClawE2ECanonicalPrompts.length;
|
||||
index += 1
|
||||
) {
|
||||
final sessionKey = 'openclaw-e2e-result-$index';
|
||||
final thread = controller.requireTaskThreadForSessionInternal(
|
||||
sessionKey,
|
||||
);
|
||||
expect(thread.lifecycleState.status, 'ready');
|
||||
expect(thread.lastArtifactSyncStatus, 'synced');
|
||||
expect(thread.lastTaskArtifactRelativePaths, hasLength(1));
|
||||
expect(
|
||||
controller.localSessionMessagesInternal[sessionKey]!.map(
|
||||
(message) => message.text,
|
||||
),
|
||||
contains('OPENCLAW-E2E-00${index + 1} done'),
|
||||
);
|
||||
final workspacePath = controller.assistantWorkspacePathForSession(
|
||||
sessionKey,
|
||||
);
|
||||
expect(
|
||||
await File(
|
||||
'$workspacePath/${thread.lastTaskArtifactRelativePaths.single}',
|
||||
).readAsString(),
|
||||
'artifact for $sessionKey',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test('OpenClaw gateway task uses the server default model', () async {
|
||||
final fakeGoTaskService = _BlockingGoTaskServiceClient();
|
||||
final controller = _connectedGatewayController(fakeGoTaskService);
|
||||
@ -4222,11 +4342,60 @@ List<Map<String, dynamic>> _generatedArtifactPayloads() {
|
||||
UiFeatureManifest _defaultDesktopManifest() {
|
||||
return UiFeatureManifest.fromYamlString(
|
||||
File(UiFeatureManifest.assetPath).readAsStringSync(),
|
||||
).copyWithFeature(
|
||||
platform: UiFeaturePlatform.desktop,
|
||||
module: 'assistant',
|
||||
feature: 'multi_agent',
|
||||
enabled: true,
|
||||
buildModes: const <UiFeatureBuildMode>{
|
||||
UiFeatureBuildMode.debug,
|
||||
UiFeatureBuildMode.profile,
|
||||
UiFeatureBuildMode.release,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
AppController _connectedController(GoTaskServiceClient client) {
|
||||
AppController _sandboxController({
|
||||
SecureConfigStore? store,
|
||||
RuntimeCoordinator? runtimeCoordinator,
|
||||
DesktopPlatformService? desktopPlatformService,
|
||||
UiFeatureManifest? uiFeatureManifest,
|
||||
List<SingleAgentProvider>? initialBridgeProviderCatalog,
|
||||
List<SingleAgentProvider>? initialGatewayProviderCatalog,
|
||||
List<AssistantExecutionTarget>? initialAvailableExecutionTargets,
|
||||
AccountRuntimeClient Function(String baseUrl)? accountClientFactory,
|
||||
Map<String, String>? environmentOverride,
|
||||
GoTaskServiceClient? goTaskServiceClient,
|
||||
String? homeDir,
|
||||
}) {
|
||||
final actualHome = homeDir ?? Directory.systemTemp.createTempSync('xworkmate-sandbox-home-').path;
|
||||
if (homeDir == null) {
|
||||
addTearDown(() async {
|
||||
final dir = Directory(actualHome);
|
||||
if (await dir.exists()) {
|
||||
await dir.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
}
|
||||
return AppController(
|
||||
store: store,
|
||||
runtimeCoordinator: runtimeCoordinator,
|
||||
desktopPlatformService: desktopPlatformService,
|
||||
uiFeatureManifest: uiFeatureManifest,
|
||||
initialBridgeProviderCatalog: initialBridgeProviderCatalog,
|
||||
initialGatewayProviderCatalog: initialGatewayProviderCatalog,
|
||||
initialAvailableExecutionTargets: initialAvailableExecutionTargets,
|
||||
accountClientFactory: accountClientFactory,
|
||||
environmentOverride: <String, String>{
|
||||
...?environmentOverride,
|
||||
'HOME': actualHome,
|
||||
},
|
||||
goTaskServiceClient: goTaskServiceClient,
|
||||
);
|
||||
}
|
||||
|
||||
AppController _connectedController(GoTaskServiceClient client, {String? homeDir}) {
|
||||
return _sandboxController(
|
||||
goTaskServiceClient: client,
|
||||
uiFeatureManifest: _defaultDesktopManifest(),
|
||||
environmentOverride: const <String, String>{
|
||||
@ -4238,11 +4407,12 @@ AppController _connectedController(GoTaskServiceClient client) {
|
||||
initialAvailableExecutionTargets: const <AssistantExecutionTarget>[
|
||||
AssistantExecutionTarget.agent,
|
||||
],
|
||||
homeDir: homeDir,
|
||||
);
|
||||
}
|
||||
|
||||
AppController _connectedGatewayController(GoTaskServiceClient client) {
|
||||
return AppController(
|
||||
AppController _connectedGatewayController(GoTaskServiceClient client, {String? homeDir}) {
|
||||
return _sandboxController(
|
||||
goTaskServiceClient: client,
|
||||
uiFeatureManifest: _defaultDesktopManifest(),
|
||||
environmentOverride: const <String, String>{
|
||||
@ -4258,6 +4428,7 @@ AppController _connectedGatewayController(GoTaskServiceClient client) {
|
||||
AssistantExecutionTarget.agent,
|
||||
AssistantExecutionTarget.gateway,
|
||||
],
|
||||
homeDir: homeDir,
|
||||
);
|
||||
}
|
||||
|
||||
@ -4374,8 +4545,6 @@ Future<void> _waitForThreadLastResultCode(
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
class _RecordingGoTaskServiceClient implements GoTaskServiceClient {
|
||||
int executeCount = 0;
|
||||
final List<GoTaskServiceRequest> requests = <GoTaskServiceRequest>[];
|
||||
|
||||
Loading…
Reference in New Issue
Block a user