1555 lines
51 KiB
Dart
1555 lines
51 KiB
Dart
// ignore_for_file: unused_import, unnecessary_import
|
|
|
|
import 'dart:convert';
|
|
import '../i18n/app_language.dart';
|
|
import '../models/app_models.dart';
|
|
import 'runtime_models_connection.dart';
|
|
import 'runtime_models_profiles.dart';
|
|
import 'runtime_models_configs.dart';
|
|
import 'runtime_models_settings_snapshot.dart';
|
|
import 'runtime_models_gateway_entities.dart';
|
|
import 'runtime_models_multi_agent.dart';
|
|
|
|
class GatewayConnectionSnapshot {
|
|
const GatewayConnectionSnapshot({
|
|
required this.status,
|
|
required this.mode,
|
|
required this.statusText,
|
|
required this.serverName,
|
|
required this.remoteAddress,
|
|
required this.mainSessionKey,
|
|
required this.lastError,
|
|
required this.lastErrorCode,
|
|
required this.lastErrorDetailCode,
|
|
required this.lastConnectedAtMs,
|
|
required this.deviceId,
|
|
required this.authRole,
|
|
required this.authScopes,
|
|
required this.connectAuthMode,
|
|
required this.connectAuthFields,
|
|
required this.connectAuthSources,
|
|
required this.hasSharedAuth,
|
|
required this.hasDeviceToken,
|
|
required this.healthPayload,
|
|
required this.statusPayload,
|
|
});
|
|
|
|
final RuntimeConnectionStatus status;
|
|
final RuntimeConnectionMode mode;
|
|
final String statusText;
|
|
final String? serverName;
|
|
final String? remoteAddress;
|
|
final String? mainSessionKey;
|
|
final String? lastError;
|
|
final String? lastErrorCode;
|
|
final String? lastErrorDetailCode;
|
|
final int? lastConnectedAtMs;
|
|
final String? deviceId;
|
|
final String? authRole;
|
|
final List<String> authScopes;
|
|
final String? connectAuthMode;
|
|
final List<String> connectAuthFields;
|
|
final List<String> connectAuthSources;
|
|
final bool hasSharedAuth;
|
|
final bool hasDeviceToken;
|
|
final Map<String, dynamic>? healthPayload;
|
|
final Map<String, dynamic>? statusPayload;
|
|
|
|
factory GatewayConnectionSnapshot.initial({
|
|
RuntimeConnectionMode mode = RuntimeConnectionMode.unconfigured,
|
|
}) {
|
|
return GatewayConnectionSnapshot(
|
|
status: RuntimeConnectionStatus.offline,
|
|
mode: mode,
|
|
statusText: 'Offline',
|
|
serverName: null,
|
|
remoteAddress: null,
|
|
mainSessionKey: null,
|
|
lastError: null,
|
|
lastErrorCode: null,
|
|
lastErrorDetailCode: null,
|
|
lastConnectedAtMs: null,
|
|
deviceId: null,
|
|
authRole: null,
|
|
authScopes: const <String>[],
|
|
connectAuthMode: null,
|
|
connectAuthFields: const <String>[],
|
|
connectAuthSources: const <String>[],
|
|
hasSharedAuth: false,
|
|
hasDeviceToken: false,
|
|
healthPayload: null,
|
|
statusPayload: null,
|
|
);
|
|
}
|
|
|
|
GatewayConnectionSnapshot copyWith({
|
|
RuntimeConnectionStatus? status,
|
|
RuntimeConnectionMode? mode,
|
|
String? statusText,
|
|
String? serverName,
|
|
String? remoteAddress,
|
|
String? mainSessionKey,
|
|
String? lastError,
|
|
String? lastErrorCode,
|
|
String? lastErrorDetailCode,
|
|
int? lastConnectedAtMs,
|
|
String? deviceId,
|
|
String? authRole,
|
|
List<String>? authScopes,
|
|
String? connectAuthMode,
|
|
List<String>? connectAuthFields,
|
|
List<String>? connectAuthSources,
|
|
bool? hasSharedAuth,
|
|
bool? hasDeviceToken,
|
|
Map<String, dynamic>? healthPayload,
|
|
Map<String, dynamic>? statusPayload,
|
|
bool clearServerName = false,
|
|
bool clearRemoteAddress = false,
|
|
bool clearMainSessionKey = false,
|
|
bool clearLastError = false,
|
|
bool clearLastErrorCode = false,
|
|
bool clearLastErrorDetailCode = false,
|
|
}) {
|
|
return GatewayConnectionSnapshot(
|
|
status: status ?? this.status,
|
|
mode: mode ?? this.mode,
|
|
statusText: statusText ?? this.statusText,
|
|
serverName: clearServerName ? null : (serverName ?? this.serverName),
|
|
remoteAddress: clearRemoteAddress
|
|
? null
|
|
: (remoteAddress ?? this.remoteAddress),
|
|
mainSessionKey: clearMainSessionKey
|
|
? null
|
|
: (mainSessionKey ?? this.mainSessionKey),
|
|
lastError: clearLastError ? null : (lastError ?? this.lastError),
|
|
lastErrorCode: clearLastErrorCode
|
|
? null
|
|
: (lastErrorCode ?? this.lastErrorCode),
|
|
lastErrorDetailCode: clearLastErrorDetailCode
|
|
? null
|
|
: (lastErrorDetailCode ?? this.lastErrorDetailCode),
|
|
lastConnectedAtMs: lastConnectedAtMs ?? this.lastConnectedAtMs,
|
|
deviceId: deviceId ?? this.deviceId,
|
|
authRole: authRole ?? this.authRole,
|
|
authScopes: authScopes ?? this.authScopes,
|
|
connectAuthMode: connectAuthMode ?? this.connectAuthMode,
|
|
connectAuthFields: connectAuthFields ?? this.connectAuthFields,
|
|
connectAuthSources: connectAuthSources ?? this.connectAuthSources,
|
|
hasSharedAuth: hasSharedAuth ?? this.hasSharedAuth,
|
|
hasDeviceToken: hasDeviceToken ?? this.hasDeviceToken,
|
|
healthPayload: healthPayload ?? this.healthPayload,
|
|
statusPayload: statusPayload ?? this.statusPayload,
|
|
);
|
|
}
|
|
|
|
GatewayConnectionSnapshot normalizedForConnectedState() {
|
|
if (status != RuntimeConnectionStatus.connected) {
|
|
return this;
|
|
}
|
|
if (lastError == null &&
|
|
lastErrorCode == null &&
|
|
lastErrorDetailCode == null) {
|
|
return this;
|
|
}
|
|
return copyWith(
|
|
clearLastError: true,
|
|
clearLastErrorCode: true,
|
|
clearLastErrorDetailCode: true,
|
|
);
|
|
}
|
|
|
|
bool get gatewayTokenMissing {
|
|
if (status == RuntimeConnectionStatus.connected) {
|
|
return false;
|
|
}
|
|
final detailCode = lastErrorDetailCode?.trim().toUpperCase();
|
|
final errorText = lastError?.toLowerCase() ?? '';
|
|
return detailCode == 'AUTH_TOKEN_MISSING' ||
|
|
errorText.contains('gateway token missing');
|
|
}
|
|
|
|
String get connectAuthSummary {
|
|
final mode = connectAuthMode?.trim() ?? 'none';
|
|
final fields = connectAuthFields.isEmpty
|
|
? 'none'
|
|
: connectAuthFields.join(', ');
|
|
final sources = connectAuthSources.isEmpty
|
|
? 'none'
|
|
: connectAuthSources.join(' · ');
|
|
return '$mode | fields: $fields | sources: $sources';
|
|
}
|
|
}
|
|
|
|
class RuntimePackageInfo {
|
|
const RuntimePackageInfo({
|
|
required this.appName,
|
|
required this.packageName,
|
|
required this.version,
|
|
required this.buildNumber,
|
|
});
|
|
|
|
final String appName;
|
|
final String packageName;
|
|
final String version;
|
|
final String buildNumber;
|
|
}
|
|
|
|
class RuntimeDeviceInfo {
|
|
const RuntimeDeviceInfo({
|
|
required this.platform,
|
|
required this.platformVersion,
|
|
required this.deviceFamily,
|
|
required this.modelIdentifier,
|
|
});
|
|
|
|
final String platform;
|
|
final String platformVersion;
|
|
final String deviceFamily;
|
|
final String modelIdentifier;
|
|
|
|
String get platformLabel {
|
|
final version = platformVersion.trim();
|
|
if (version.isEmpty) {
|
|
return platform;
|
|
}
|
|
return '$platform $version';
|
|
}
|
|
}
|
|
|
|
class RuntimeLogEntry {
|
|
const RuntimeLogEntry({
|
|
required this.timestampMs,
|
|
required this.level,
|
|
required this.category,
|
|
required this.message,
|
|
});
|
|
|
|
final int timestampMs;
|
|
final String level;
|
|
final String category;
|
|
final String message;
|
|
|
|
String get timeLabel {
|
|
final date = DateTime.fromMillisecondsSinceEpoch(timestampMs);
|
|
return '${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}:${date.second.toString().padLeft(2, '0')}';
|
|
}
|
|
|
|
String get line => '[$timeLabel] ${level.toUpperCase()} $category $message';
|
|
}
|
|
|
|
class GatewayAgentSummary {
|
|
const GatewayAgentSummary({
|
|
required this.id,
|
|
required this.name,
|
|
required this.emoji,
|
|
required this.theme,
|
|
});
|
|
|
|
final String id;
|
|
final String name;
|
|
final String emoji;
|
|
final String theme;
|
|
}
|
|
|
|
class GatewaySessionSummary {
|
|
const GatewaySessionSummary({
|
|
required this.key,
|
|
required this.kind,
|
|
required this.displayName,
|
|
required this.surface,
|
|
required this.subject,
|
|
required this.room,
|
|
required this.space,
|
|
required this.updatedAtMs,
|
|
required this.sessionId,
|
|
required this.systemSent,
|
|
required this.abortedLastRun,
|
|
required this.thinkingLevel,
|
|
required this.verboseLevel,
|
|
required this.inputTokens,
|
|
required this.outputTokens,
|
|
required this.totalTokens,
|
|
required this.model,
|
|
required this.contextTokens,
|
|
required this.derivedTitle,
|
|
required this.lastMessagePreview,
|
|
});
|
|
|
|
final String key;
|
|
final String? kind;
|
|
final String? displayName;
|
|
final String? surface;
|
|
final String? subject;
|
|
final String? room;
|
|
final String? space;
|
|
final double? updatedAtMs;
|
|
final String? sessionId;
|
|
final bool? systemSent;
|
|
final bool? abortedLastRun;
|
|
final String? thinkingLevel;
|
|
final String? verboseLevel;
|
|
final int? inputTokens;
|
|
final int? outputTokens;
|
|
final int? totalTokens;
|
|
final String? model;
|
|
final int? contextTokens;
|
|
final String? derivedTitle;
|
|
final String? lastMessagePreview;
|
|
|
|
String get label {
|
|
final candidates = [derivedTitle, displayName, subject, room, space, key];
|
|
return candidates.firstWhere(
|
|
(item) => item != null && item.trim().isNotEmpty,
|
|
orElse: () => key,
|
|
)!;
|
|
}
|
|
}
|
|
|
|
class GatewayChatMessage {
|
|
const GatewayChatMessage({
|
|
required this.id,
|
|
required this.role,
|
|
required this.text,
|
|
required this.timestampMs,
|
|
required this.toolCallId,
|
|
required this.toolName,
|
|
required this.stopReason,
|
|
required this.pending,
|
|
required this.error,
|
|
});
|
|
|
|
final String id;
|
|
final String role;
|
|
final String text;
|
|
final double? timestampMs;
|
|
final String? toolCallId;
|
|
final String? toolName;
|
|
final String? stopReason;
|
|
final bool pending;
|
|
final bool error;
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'id': id,
|
|
'role': role,
|
|
'text': text,
|
|
'timestampMs': timestampMs,
|
|
'toolCallId': toolCallId,
|
|
'toolName': toolName,
|
|
'stopReason': stopReason,
|
|
'pending': pending,
|
|
'error': error,
|
|
};
|
|
}
|
|
|
|
factory GatewayChatMessage.fromJson(Map<String, dynamic> json) {
|
|
double? asDouble(Object? value) {
|
|
if (value is num) {
|
|
return value.toDouble();
|
|
}
|
|
return double.tryParse(value?.toString() ?? '');
|
|
}
|
|
|
|
return GatewayChatMessage(
|
|
id: json['id']?.toString() ?? '',
|
|
role: json['role']?.toString() ?? 'assistant',
|
|
text: json['text']?.toString() ?? '',
|
|
timestampMs: asDouble(json['timestampMs']),
|
|
toolCallId: json['toolCallId']?.toString(),
|
|
toolName: json['toolName']?.toString(),
|
|
stopReason: json['stopReason']?.toString(),
|
|
pending: json['pending'] as bool? ?? false,
|
|
error: json['error'] as bool? ?? false,
|
|
);
|
|
}
|
|
|
|
GatewayChatMessage copyWith({
|
|
String? id,
|
|
String? role,
|
|
String? text,
|
|
double? timestampMs,
|
|
String? toolCallId,
|
|
String? toolName,
|
|
String? stopReason,
|
|
bool? pending,
|
|
bool? error,
|
|
}) {
|
|
return GatewayChatMessage(
|
|
id: id ?? this.id,
|
|
role: role ?? this.role,
|
|
text: text ?? this.text,
|
|
timestampMs: timestampMs ?? this.timestampMs,
|
|
toolCallId: toolCallId ?? this.toolCallId,
|
|
toolName: toolName ?? this.toolName,
|
|
stopReason: stopReason ?? this.stopReason,
|
|
pending: pending ?? this.pending,
|
|
error: error ?? this.error,
|
|
);
|
|
}
|
|
}
|
|
|
|
const int taskThreadSchemaVersion = 20260411;
|
|
|
|
enum ThreadRealm { local, remote }
|
|
|
|
extension ThreadRealmCopy on ThreadRealm {
|
|
static ThreadRealm fromJsonValue(String? value) {
|
|
return ThreadRealm.values.firstWhere(
|
|
(item) => item.name == value?.trim(),
|
|
orElse: () => ThreadRealm.local,
|
|
);
|
|
}
|
|
}
|
|
|
|
enum ThreadSubjectType { tenant, user }
|
|
|
|
extension ThreadSubjectTypeCopy on ThreadSubjectType {
|
|
static ThreadSubjectType fromJsonValue(String? value) {
|
|
return ThreadSubjectType.values.firstWhere(
|
|
(item) => item.name == value?.trim(),
|
|
orElse: () => ThreadSubjectType.user,
|
|
);
|
|
}
|
|
}
|
|
|
|
enum WorkspaceKind { localFs, remoteFs }
|
|
|
|
extension WorkspaceKindCopy on WorkspaceKind {
|
|
static WorkspaceKind fromJsonValue(String? value) {
|
|
final normalized = value?.trim();
|
|
switch (normalized) {
|
|
case 'localPath':
|
|
case 'local_fs':
|
|
case 'localFs':
|
|
return WorkspaceKind.localFs;
|
|
case 'remotePath':
|
|
case 'objectStore':
|
|
case 'remote_fs':
|
|
case 'remoteFs':
|
|
return WorkspaceKind.remoteFs;
|
|
default:
|
|
return WorkspaceKind.localFs;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool isLegacyAutoThreadExecutionModeValue(String? value) {
|
|
return value?.trim().toLowerCase() == 'auto';
|
|
}
|
|
|
|
enum ThreadExecutionMode { agent, gateway }
|
|
|
|
extension ThreadExecutionModeCopy on ThreadExecutionMode {
|
|
static ThreadExecutionMode fromJsonValue(String? value) {
|
|
return ThreadExecutionMode.values.firstWhere(
|
|
(item) => item.name == value?.trim(),
|
|
orElse: () => ThreadExecutionMode.gateway,
|
|
);
|
|
}
|
|
}
|
|
|
|
enum ThreadSelectionSource { inherited, explicit }
|
|
|
|
extension ThreadSelectionSourceCopy on ThreadSelectionSource {
|
|
static ThreadSelectionSource fromJsonValue(String? value) {
|
|
return ThreadSelectionSource.values.firstWhere(
|
|
(item) => item.name == value?.trim(),
|
|
orElse: () => ThreadSelectionSource.inherited,
|
|
);
|
|
}
|
|
}
|
|
|
|
class ThreadOwnerScope {
|
|
const ThreadOwnerScope({
|
|
required this.realm,
|
|
required this.subjectType,
|
|
required this.subjectId,
|
|
required this.displayName,
|
|
});
|
|
|
|
final ThreadRealm realm;
|
|
final ThreadSubjectType subjectType;
|
|
final String subjectId;
|
|
final String displayName;
|
|
|
|
ThreadOwnerScope copyWith({
|
|
ThreadRealm? realm,
|
|
ThreadSubjectType? subjectType,
|
|
String? subjectId,
|
|
String? displayName,
|
|
}) {
|
|
return ThreadOwnerScope(
|
|
realm: realm ?? this.realm,
|
|
subjectType: subjectType ?? this.subjectType,
|
|
subjectId: subjectId ?? this.subjectId,
|
|
displayName: displayName ?? this.displayName,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return <String, dynamic>{
|
|
'realm': realm.name,
|
|
'subjectType': subjectType.name,
|
|
'subjectId': subjectId,
|
|
'displayName': displayName,
|
|
};
|
|
}
|
|
|
|
factory ThreadOwnerScope.fromJson(Map<String, dynamic> json) {
|
|
return ThreadOwnerScope(
|
|
realm: ThreadRealmCopy.fromJsonValue(json['realm']?.toString()),
|
|
subjectType: ThreadSubjectTypeCopy.fromJsonValue(
|
|
json['subjectType']?.toString(),
|
|
),
|
|
subjectId: json['subjectId']?.toString() ?? '',
|
|
displayName: json['displayName']?.toString() ?? '',
|
|
);
|
|
}
|
|
}
|
|
|
|
class WorkspaceBinding {
|
|
const WorkspaceBinding({
|
|
required this.workspaceId,
|
|
required this.workspaceKind,
|
|
required this.workspacePath,
|
|
required this.displayPath,
|
|
required this.writable,
|
|
});
|
|
|
|
final String workspaceId;
|
|
final WorkspaceKind workspaceKind;
|
|
final String workspacePath;
|
|
final String displayPath;
|
|
final bool writable;
|
|
|
|
bool get isComplete =>
|
|
workspaceId.trim().isNotEmpty && workspacePath.trim().isNotEmpty;
|
|
|
|
WorkspaceBinding copyWith({
|
|
String? workspaceId,
|
|
WorkspaceKind? workspaceKind,
|
|
String? workspacePath,
|
|
String? displayPath,
|
|
bool? writable,
|
|
}) {
|
|
return WorkspaceBinding(
|
|
workspaceId: workspaceId ?? this.workspaceId,
|
|
workspaceKind: workspaceKind ?? this.workspaceKind,
|
|
workspacePath: workspacePath ?? this.workspacePath,
|
|
displayPath: displayPath ?? this.displayPath,
|
|
writable: writable ?? this.writable,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return <String, dynamic>{
|
|
'workspaceId': workspaceId,
|
|
'workspaceKind': workspaceKind.name,
|
|
'workspacePath': workspacePath,
|
|
'displayPath': displayPath,
|
|
'writable': writable,
|
|
};
|
|
}
|
|
|
|
factory WorkspaceBinding.fromJson(Map<String, dynamic> json) {
|
|
final path = json['workspacePath']?.toString() ?? '';
|
|
final workspaceId = json['workspaceId']?.toString() ?? '';
|
|
final workspaceKindValue = json['workspaceKind']?.toString();
|
|
if (workspaceId.trim().isEmpty ||
|
|
path.trim().isEmpty ||
|
|
workspaceKindValue == null ||
|
|
workspaceKindValue.trim().isEmpty) {
|
|
throw const FormatException('TaskThread.workspaceBinding is incomplete.');
|
|
}
|
|
return WorkspaceBinding(
|
|
workspaceId: workspaceId,
|
|
workspaceKind: WorkspaceKindCopy.fromJsonValue(workspaceKindValue),
|
|
workspacePath: path,
|
|
displayPath: json['displayPath']?.toString() ?? path,
|
|
writable: json['writable'] as bool? ?? true,
|
|
);
|
|
}
|
|
}
|
|
|
|
class ExecutionBinding {
|
|
const ExecutionBinding({
|
|
required this.executionMode,
|
|
required this.executorId,
|
|
required this.providerId,
|
|
required this.endpointId,
|
|
this.executionModeSource = ThreadSelectionSource.inherited,
|
|
this.providerSource = ThreadSelectionSource.inherited,
|
|
});
|
|
|
|
final ThreadExecutionMode executionMode;
|
|
final String executorId;
|
|
final String providerId;
|
|
final String endpointId;
|
|
final ThreadSelectionSource executionModeSource;
|
|
final ThreadSelectionSource providerSource;
|
|
|
|
ExecutionBinding copyWith({
|
|
ThreadExecutionMode? executionMode,
|
|
String? executorId,
|
|
String? providerId,
|
|
String? endpointId,
|
|
ThreadSelectionSource? executionModeSource,
|
|
ThreadSelectionSource? providerSource,
|
|
}) {
|
|
return ExecutionBinding(
|
|
executionMode: executionMode ?? this.executionMode,
|
|
executorId: executorId ?? this.executorId,
|
|
providerId: providerId ?? this.providerId,
|
|
endpointId: endpointId ?? this.endpointId,
|
|
executionModeSource: executionModeSource ?? this.executionModeSource,
|
|
providerSource: providerSource ?? this.providerSource,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return <String, dynamic>{
|
|
'executionMode': executionMode.name,
|
|
'executorId': executorId,
|
|
'providerId': providerId,
|
|
'endpointId': endpointId,
|
|
'executionModeSource': executionModeSource.name,
|
|
'providerSource': providerSource.name,
|
|
};
|
|
}
|
|
|
|
factory ExecutionBinding.fromJson(Map<String, dynamic> json) {
|
|
return ExecutionBinding(
|
|
executionMode: ThreadExecutionModeCopy.fromJsonValue(
|
|
json['executionMode']?.toString(),
|
|
),
|
|
executorId: json['executorId']?.toString() ?? '',
|
|
providerId: json['providerId']?.toString() ?? '',
|
|
endpointId: json['endpointId']?.toString() ?? '',
|
|
executionModeSource: ThreadSelectionSourceCopy.fromJsonValue(
|
|
json['executionModeSource']?.toString(),
|
|
),
|
|
providerSource: ThreadSelectionSourceCopy.fromJsonValue(
|
|
json['providerSource']?.toString(),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
ThreadExecutionMode threadExecutionModeFromAssistantExecutionTarget(
|
|
AssistantExecutionTarget target,
|
|
) {
|
|
return switch (target) {
|
|
AssistantExecutionTarget.agent => ThreadExecutionMode.agent,
|
|
AssistantExecutionTarget.gateway => ThreadExecutionMode.gateway,
|
|
};
|
|
}
|
|
|
|
AssistantExecutionTarget assistantExecutionTargetFromExecutionMode(
|
|
ThreadExecutionMode mode,
|
|
) {
|
|
return switch (mode) {
|
|
ThreadExecutionMode.agent => AssistantExecutionTarget.agent,
|
|
ThreadExecutionMode.gateway => AssistantExecutionTarget.gateway,
|
|
};
|
|
}
|
|
|
|
WorkspaceRefKind workspaceRefKindFromWorkspaceKind(WorkspaceKind kind) {
|
|
return switch (kind) {
|
|
WorkspaceKind.localFs => WorkspaceRefKind.localPath,
|
|
WorkspaceKind.remoteFs => WorkspaceRefKind.remotePath,
|
|
};
|
|
}
|
|
|
|
class ThreadContextState {
|
|
const ThreadContextState({
|
|
required this.messages,
|
|
required this.selectedModelId,
|
|
required this.selectedSkillKeys,
|
|
required this.permissionLevel,
|
|
required this.messageViewMode,
|
|
required this.latestResolvedRuntimeModel,
|
|
required this.latestResolvedProviderId,
|
|
this.selectedModelSource = ThreadSelectionSource.inherited,
|
|
this.selectedSkillsSource = ThreadSelectionSource.inherited,
|
|
this.gatewayEntryState,
|
|
this.lastRemoteWorkingDirectory,
|
|
this.lastRemoteWorkspaceRefKind,
|
|
this.lastArtifactSyncAtMs,
|
|
this.lastArtifactSyncStatus,
|
|
this.lastTaskArtifactRelativePaths = const <String>[],
|
|
this.openClawTaskAssociation,
|
|
this.taskInputAttachments = const <TaskInputAttachmentRecord>[],
|
|
});
|
|
|
|
final List<GatewayChatMessage> messages;
|
|
final String selectedModelId;
|
|
final List<String> selectedSkillKeys;
|
|
final AssistantPermissionLevel permissionLevel;
|
|
final AssistantMessageViewMode messageViewMode;
|
|
final String latestResolvedRuntimeModel;
|
|
final String latestResolvedProviderId;
|
|
final ThreadSelectionSource selectedModelSource;
|
|
final ThreadSelectionSource selectedSkillsSource;
|
|
final String? gatewayEntryState;
|
|
final String? lastRemoteWorkingDirectory;
|
|
final WorkspaceRefKind? lastRemoteWorkspaceRefKind;
|
|
final double? lastArtifactSyncAtMs;
|
|
final String? lastArtifactSyncStatus;
|
|
final List<String> lastTaskArtifactRelativePaths;
|
|
final OpenClawTaskAssociation? openClawTaskAssociation;
|
|
final List<TaskInputAttachmentRecord> taskInputAttachments;
|
|
|
|
ThreadContextState copyWith({
|
|
List<GatewayChatMessage>? messages,
|
|
String? selectedModelId,
|
|
List<String>? selectedSkillKeys,
|
|
AssistantPermissionLevel? permissionLevel,
|
|
AssistantMessageViewMode? messageViewMode,
|
|
String? latestResolvedRuntimeModel,
|
|
String? latestResolvedProviderId,
|
|
ThreadSelectionSource? selectedModelSource,
|
|
ThreadSelectionSource? selectedSkillsSource,
|
|
String? gatewayEntryState,
|
|
bool clearGatewayEntryState = false,
|
|
String? lastRemoteWorkingDirectory,
|
|
WorkspaceRefKind? lastRemoteWorkspaceRefKind,
|
|
double? lastArtifactSyncAtMs,
|
|
String? lastArtifactSyncStatus,
|
|
List<String>? lastTaskArtifactRelativePaths,
|
|
OpenClawTaskAssociation? openClawTaskAssociation,
|
|
bool clearOpenClawTaskAssociation = false,
|
|
List<TaskInputAttachmentRecord>? taskInputAttachments,
|
|
}) {
|
|
return ThreadContextState(
|
|
messages: messages ?? this.messages,
|
|
selectedModelId: selectedModelId ?? this.selectedModelId,
|
|
selectedSkillKeys: selectedSkillKeys ?? this.selectedSkillKeys,
|
|
permissionLevel: permissionLevel ?? this.permissionLevel,
|
|
messageViewMode: messageViewMode ?? this.messageViewMode,
|
|
latestResolvedRuntimeModel:
|
|
latestResolvedRuntimeModel ?? this.latestResolvedRuntimeModel,
|
|
latestResolvedProviderId:
|
|
latestResolvedProviderId ?? this.latestResolvedProviderId,
|
|
selectedModelSource: selectedModelSource ?? this.selectedModelSource,
|
|
selectedSkillsSource: selectedSkillsSource ?? this.selectedSkillsSource,
|
|
gatewayEntryState: clearGatewayEntryState
|
|
? null
|
|
: (gatewayEntryState ?? this.gatewayEntryState),
|
|
lastRemoteWorkingDirectory:
|
|
lastRemoteWorkingDirectory ?? this.lastRemoteWorkingDirectory,
|
|
lastRemoteWorkspaceRefKind:
|
|
lastRemoteWorkspaceRefKind ?? this.lastRemoteWorkspaceRefKind,
|
|
lastArtifactSyncAtMs: lastArtifactSyncAtMs ?? this.lastArtifactSyncAtMs,
|
|
lastArtifactSyncStatus:
|
|
lastArtifactSyncStatus ?? this.lastArtifactSyncStatus,
|
|
lastTaskArtifactRelativePaths: lastTaskArtifactRelativePaths == null
|
|
? this.lastTaskArtifactRelativePaths
|
|
: _stringListFromJson(lastTaskArtifactRelativePaths),
|
|
openClawTaskAssociation: clearOpenClawTaskAssociation
|
|
? null
|
|
: (openClawTaskAssociation ?? this.openClawTaskAssociation),
|
|
taskInputAttachments: taskInputAttachments ?? this.taskInputAttachments,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return <String, dynamic>{
|
|
'messages': messages.map((item) => item.toJson()).toList(growable: false),
|
|
'selectedModelId': selectedModelId,
|
|
'selectedSkillKeys': selectedSkillKeys,
|
|
'permissionLevel': permissionLevel.name,
|
|
'messageViewMode': messageViewMode.name,
|
|
'latestResolvedRuntimeModel': latestResolvedRuntimeModel,
|
|
'latestResolvedProviderId': latestResolvedProviderId,
|
|
'selectedModelSource': selectedModelSource.name,
|
|
'selectedSkillsSource': selectedSkillsSource.name,
|
|
'gatewayEntryState': gatewayEntryState,
|
|
'lastRemoteWorkingDirectory': lastRemoteWorkingDirectory,
|
|
'lastRemoteWorkspaceRefKind': lastRemoteWorkspaceRefKind?.name,
|
|
'lastArtifactSyncAtMs': lastArtifactSyncAtMs,
|
|
'lastArtifactSyncStatus': lastArtifactSyncStatus,
|
|
'lastTaskArtifactRelativePaths': lastTaskArtifactRelativePaths,
|
|
'openClawTaskAssociation': openClawTaskAssociation?.toJson(),
|
|
'taskInputAttachments': taskInputAttachments
|
|
.map((item) => item.toJson())
|
|
.toList(growable: false),
|
|
};
|
|
}
|
|
|
|
factory ThreadContextState.fromJson(Map<String, dynamic> json) {
|
|
double? asDouble(Object? value) {
|
|
if (value is num) {
|
|
return value.toDouble();
|
|
}
|
|
return double.tryParse(value?.toString() ?? '');
|
|
}
|
|
|
|
final rawMessages = json['messages'];
|
|
final messages = rawMessages is List
|
|
? rawMessages
|
|
.whereType<Map>()
|
|
.map(
|
|
(item) =>
|
|
GatewayChatMessage.fromJson(item.cast<String, dynamic>()),
|
|
)
|
|
.toList(growable: false)
|
|
: const <GatewayChatMessage>[];
|
|
final rawSelectedSkillKeys = json['selectedSkillKeys'];
|
|
final selectedSkillKeys = rawSelectedSkillKeys is List
|
|
? rawSelectedSkillKeys
|
|
.map((item) => item?.toString().trim() ?? '')
|
|
.where((item) => item.isNotEmpty)
|
|
.toSet()
|
|
.toList(growable: false)
|
|
: const <String>[];
|
|
return ThreadContextState(
|
|
messages: messages,
|
|
selectedModelId: json['selectedModelId']?.toString() ?? '',
|
|
selectedSkillKeys: selectedSkillKeys,
|
|
permissionLevel: AssistantPermissionLevelCopy.fromJsonValue(
|
|
json['permissionLevel']?.toString(),
|
|
),
|
|
messageViewMode: AssistantMessageViewModeCopy.fromJsonValue(
|
|
json['messageViewMode']?.toString(),
|
|
),
|
|
latestResolvedRuntimeModel:
|
|
json['latestResolvedRuntimeModel']?.toString() ?? '',
|
|
latestResolvedProviderId:
|
|
json['latestResolvedProviderId']?.toString() ?? '',
|
|
selectedModelSource: ThreadSelectionSourceCopy.fromJsonValue(
|
|
json['selectedModelSource']?.toString(),
|
|
),
|
|
selectedSkillsSource: ThreadSelectionSourceCopy.fromJsonValue(
|
|
json['selectedSkillsSource']?.toString(),
|
|
),
|
|
gatewayEntryState: json['gatewayEntryState']?.toString(),
|
|
lastRemoteWorkingDirectory: json['lastRemoteWorkingDirectory']
|
|
?.toString(),
|
|
lastRemoteWorkspaceRefKind: (() {
|
|
final rawValue =
|
|
json['lastRemoteWorkspaceRefKind']?.toString().trim() ?? '';
|
|
if (rawValue.isEmpty) {
|
|
return null;
|
|
}
|
|
return WorkspaceRefKindCopy.fromJsonValue(rawValue);
|
|
})(),
|
|
lastArtifactSyncAtMs: asDouble(json['lastArtifactSyncAtMs']),
|
|
lastArtifactSyncStatus: json['lastArtifactSyncStatus']?.toString(),
|
|
lastTaskArtifactRelativePaths: _stringListFromJson(
|
|
json['lastTaskArtifactRelativePaths'],
|
|
),
|
|
openClawTaskAssociation: OpenClawTaskAssociation.fromJsonOrNull(
|
|
json['openClawTaskAssociation'],
|
|
),
|
|
taskInputAttachments: _taskInputAttachmentRecordsFromJson(
|
|
json['taskInputAttachments'],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class TaskInputAttachmentRecord {
|
|
const TaskInputAttachmentRecord({
|
|
required this.name,
|
|
required this.mimeType,
|
|
required this.sha256,
|
|
required this.type,
|
|
required this.uploadedAtMs,
|
|
});
|
|
|
|
final String name;
|
|
final String mimeType;
|
|
final String sha256;
|
|
final String type;
|
|
final double uploadedAtMs;
|
|
|
|
String get key => sha256.trim().toLowerCase();
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return <String, dynamic>{
|
|
'name': name.trim(),
|
|
'mimeType': mimeType.trim(),
|
|
'sha256': key,
|
|
'type': type.trim(),
|
|
'uploadedAtMs': uploadedAtMs,
|
|
};
|
|
}
|
|
|
|
factory TaskInputAttachmentRecord.fromJson(Map<String, dynamic> json) {
|
|
double uploadedAtMs(Object? value) {
|
|
if (value is num) {
|
|
return value.toDouble();
|
|
}
|
|
return double.tryParse(value?.toString() ?? '') ?? 0;
|
|
}
|
|
|
|
return TaskInputAttachmentRecord(
|
|
name: json['name']?.toString().trim() ?? '',
|
|
mimeType: json['mimeType']?.toString().trim() ?? '',
|
|
sha256: json['sha256']?.toString().trim().toLowerCase() ?? '',
|
|
type: json['type']?.toString().trim() ?? '',
|
|
uploadedAtMs: uploadedAtMs(json['uploadedAtMs']),
|
|
);
|
|
}
|
|
}
|
|
|
|
List<TaskInputAttachmentRecord> _taskInputAttachmentRecordsFromJson(
|
|
Object? value,
|
|
) {
|
|
if (value is! List) {
|
|
return const <TaskInputAttachmentRecord>[];
|
|
}
|
|
final byKey = <String, TaskInputAttachmentRecord>{};
|
|
for (final item in value.whereType<Map>()) {
|
|
final record = TaskInputAttachmentRecord.fromJson(
|
|
item.cast<String, dynamic>(),
|
|
);
|
|
if (record.key.isEmpty || record.name.isEmpty) {
|
|
continue;
|
|
}
|
|
byKey.putIfAbsent(record.key, () => record);
|
|
}
|
|
return byKey.values.toList(growable: false);
|
|
}
|
|
|
|
class OpenClawTaskAssociation {
|
|
const OpenClawTaskAssociation({
|
|
required this.sessionId,
|
|
required this.threadId,
|
|
required this.turnId,
|
|
required this.runId,
|
|
required this.artifactScope,
|
|
required this.artifactDirectory,
|
|
required this.gatewayProviderId,
|
|
required this.startedAtMs,
|
|
required this.status,
|
|
required this.appThreadKey,
|
|
required this.openclawSessionKey,
|
|
this.taskLoadClass = '',
|
|
this.requiredArtifactExtensions = const <String>[],
|
|
this.expectedArtifactExtensions = const <String>[],
|
|
this.requiresArtifactExport = false,
|
|
});
|
|
|
|
final String sessionId;
|
|
final String threadId;
|
|
final String turnId;
|
|
final String runId;
|
|
final String artifactScope;
|
|
final String artifactDirectory;
|
|
final String gatewayProviderId;
|
|
final double startedAtMs;
|
|
final String status;
|
|
final String appThreadKey;
|
|
final String openclawSessionKey;
|
|
final String taskLoadClass;
|
|
final List<String> requiredArtifactExtensions;
|
|
final List<String> expectedArtifactExtensions;
|
|
final bool requiresArtifactExport;
|
|
|
|
bool get isTerminal {
|
|
final normalized = status.trim().toLowerCase();
|
|
return normalized == 'completed' ||
|
|
normalized == 'failed' ||
|
|
normalized == 'cancelled' ||
|
|
normalized == 'canceled';
|
|
}
|
|
|
|
OpenClawTaskAssociation copyWith({String? status}) {
|
|
return OpenClawTaskAssociation(
|
|
sessionId: sessionId,
|
|
threadId: threadId,
|
|
turnId: turnId,
|
|
runId: runId,
|
|
artifactScope: artifactScope,
|
|
artifactDirectory: artifactDirectory,
|
|
gatewayProviderId: gatewayProviderId,
|
|
startedAtMs: startedAtMs,
|
|
status: status ?? this.status,
|
|
appThreadKey: appThreadKey,
|
|
openclawSessionKey: openclawSessionKey,
|
|
taskLoadClass: taskLoadClass,
|
|
requiredArtifactExtensions: requiredArtifactExtensions,
|
|
expectedArtifactExtensions: expectedArtifactExtensions,
|
|
requiresArtifactExport: requiresArtifactExport,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return <String, dynamic>{
|
|
'sessionId': sessionId,
|
|
'threadId': threadId,
|
|
'turnId': turnId,
|
|
'runId': runId,
|
|
'artifactScope': artifactScope,
|
|
'artifactDirectory': artifactDirectory,
|
|
'gatewayProviderId': gatewayProviderId,
|
|
'startedAtMs': startedAtMs,
|
|
'status': status,
|
|
'appThreadKey': appThreadKey,
|
|
'openclawSessionKey': openclawSessionKey,
|
|
'taskLoadClass': taskLoadClass,
|
|
'requiredArtifactExtensions': requiredArtifactExtensions,
|
|
'expectedArtifactExtensions': expectedArtifactExtensions,
|
|
'requiresArtifactExport': requiresArtifactExport,
|
|
};
|
|
}
|
|
|
|
Map<String, dynamic> toTaskGetParams() {
|
|
return <String, dynamic>{
|
|
'runId': runId,
|
|
'appThreadKey': appThreadKey,
|
|
'openclawSessionKey': openclawSessionKey,
|
|
'includeArtifacts': true,
|
|
if (artifactScope.trim().isNotEmpty) 'artifactScope': artifactScope,
|
|
if (artifactDirectory.trim().isNotEmpty)
|
|
'artifactDirectory': artifactDirectory,
|
|
if (gatewayProviderId.trim().isNotEmpty)
|
|
'gatewayProviderId': gatewayProviderId,
|
|
if (requiresArtifactExport) 'requiresArtifactExport': true,
|
|
if (expectedArtifactExtensions.isNotEmpty)
|
|
'expectedArtifactExtensions': expectedArtifactExtensions,
|
|
if (requiredArtifactExtensions.isNotEmpty)
|
|
'requiredArtifactExtensions': requiredArtifactExtensions,
|
|
};
|
|
}
|
|
|
|
static OpenClawTaskAssociation? fromJsonOrNull(Object? value) {
|
|
if (value is! Map) {
|
|
return null;
|
|
}
|
|
final json = value.cast<String, dynamic>();
|
|
final runId = json['runId']?.toString().trim() ?? '';
|
|
final appThreadKey = json['appThreadKey']?.toString().trim() ?? '';
|
|
final openclawSessionKey =
|
|
json['openclawSessionKey']?.toString().trim() ?? '';
|
|
if (runId.isEmpty || appThreadKey.isEmpty || openclawSessionKey.isEmpty) {
|
|
return null;
|
|
}
|
|
double asDouble(Object? raw) {
|
|
if (raw is num) {
|
|
return raw.toDouble();
|
|
}
|
|
return double.tryParse(raw?.toString() ?? '') ?? 0;
|
|
}
|
|
|
|
return OpenClawTaskAssociation(
|
|
sessionId: json['sessionId']?.toString().trim() ?? '',
|
|
threadId: json['threadId']?.toString().trim() ?? '',
|
|
turnId: json['turnId']?.toString().trim() ?? '',
|
|
runId: runId,
|
|
artifactScope: json['artifactScope']?.toString().trim() ?? '',
|
|
artifactDirectory: json['artifactDirectory']?.toString().trim() ?? '',
|
|
gatewayProviderId:
|
|
json['gatewayProviderId']?.toString().trim().isNotEmpty == true
|
|
? json['gatewayProviderId'].toString().trim()
|
|
: (json['resolvedGatewayProviderId']?.toString().trim().isNotEmpty ==
|
|
true
|
|
? json['resolvedGatewayProviderId'].toString().trim()
|
|
: 'openclaw'),
|
|
startedAtMs: asDouble(json['startedAtMs']),
|
|
status: json['status']?.toString().trim().isNotEmpty == true
|
|
? json['status'].toString().trim()
|
|
: 'running',
|
|
appThreadKey: appThreadKey,
|
|
openclawSessionKey: openclawSessionKey,
|
|
taskLoadClass: json['taskLoadClass']?.toString().trim() ?? '',
|
|
requiredArtifactExtensions: _stringListFromJson(
|
|
json['requiredArtifactExtensions'],
|
|
),
|
|
expectedArtifactExtensions: _stringListFromJson(
|
|
json['expectedArtifactExtensions'],
|
|
),
|
|
requiresArtifactExport:
|
|
_boolFromJson(json['requiresArtifactExport']) ??
|
|
_boolFromJson(json['requiresExportBeforeFinalResponse']) ??
|
|
false,
|
|
);
|
|
}
|
|
}
|
|
|
|
List<String> _stringListFromJson(Object? value) {
|
|
if (value is! List) {
|
|
return const <String>[];
|
|
}
|
|
final seen = <String>{};
|
|
final items = <String>[];
|
|
for (final item in value) {
|
|
final normalized = item?.toString().trim().replaceAll('\\', '/') ?? '';
|
|
if (normalized.isEmpty || normalized.startsWith('/')) {
|
|
continue;
|
|
}
|
|
if (normalized.split('/').any((segment) => segment == '..')) {
|
|
continue;
|
|
}
|
|
if (seen.add(normalized)) {
|
|
items.add(normalized);
|
|
}
|
|
}
|
|
return items;
|
|
}
|
|
|
|
bool? _boolFromJson(Object? value) {
|
|
if (value is bool) {
|
|
return value;
|
|
}
|
|
final normalized = value?.toString().trim().toLowerCase() ?? '';
|
|
if (normalized == 'true' || normalized == '1' || normalized == 'yes') {
|
|
return true;
|
|
}
|
|
if (normalized == 'false' || normalized == '0' || normalized == 'no') {
|
|
return false;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
class ThreadLifecycleState {
|
|
const ThreadLifecycleState({
|
|
required this.archived,
|
|
required this.status,
|
|
required this.lastRunAtMs,
|
|
required this.lastResultCode,
|
|
});
|
|
|
|
final bool archived;
|
|
final String status;
|
|
final double? lastRunAtMs;
|
|
final String? lastResultCode;
|
|
|
|
ThreadLifecycleState copyWith({
|
|
bool? archived,
|
|
String? status,
|
|
double? lastRunAtMs,
|
|
String? lastResultCode,
|
|
bool clearLastResultCode = false,
|
|
}) {
|
|
return ThreadLifecycleState(
|
|
archived: archived ?? this.archived,
|
|
status: status ?? this.status,
|
|
lastRunAtMs: lastRunAtMs ?? this.lastRunAtMs,
|
|
lastResultCode: clearLastResultCode
|
|
? null
|
|
: (lastResultCode ?? this.lastResultCode),
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return <String, dynamic>{
|
|
'archived': archived,
|
|
'status': status,
|
|
'lastRunAtMs': lastRunAtMs,
|
|
'lastResultCode': lastResultCode,
|
|
};
|
|
}
|
|
|
|
factory ThreadLifecycleState.fromJson(Map<String, dynamic> json) {
|
|
double? asDouble(Object? value) {
|
|
if (value is num) {
|
|
return value.toDouble();
|
|
}
|
|
return double.tryParse(value?.toString() ?? '');
|
|
}
|
|
|
|
return ThreadLifecycleState(
|
|
archived: json['archived'] as bool? ?? false,
|
|
status: json['status']?.toString() ?? 'ready',
|
|
lastRunAtMs: asDouble(json['lastRunAtMs']),
|
|
lastResultCode: json['lastResultCode']?.toString(),
|
|
);
|
|
}
|
|
}
|
|
|
|
class TaskThread {
|
|
TaskThread({
|
|
required String threadId,
|
|
String? title,
|
|
ThreadOwnerScope? ownerScope,
|
|
required WorkspaceBinding workspaceBinding,
|
|
ExecutionBinding? executionBinding,
|
|
ThreadContextState? contextState,
|
|
ThreadLifecycleState? lifecycleState,
|
|
double? createdAtMs,
|
|
this.updatedAtMs,
|
|
List<GatewayChatMessage>? messages,
|
|
bool? archived,
|
|
AssistantMessageViewMode? messageViewMode,
|
|
List<String>? selectedSkillKeys,
|
|
String? assistantModelId,
|
|
String? gatewayEntryState,
|
|
AssistantPermissionLevel? permissionLevel,
|
|
String? latestResolvedRuntimeModel,
|
|
String? latestResolvedProviderId,
|
|
double? lastRunAtMs,
|
|
String? lastResultCode,
|
|
String? lastRemoteWorkingDirectory,
|
|
WorkspaceRefKind? lastRemoteWorkspaceRefKind,
|
|
double? lastArtifactSyncAtMs,
|
|
String? lastArtifactSyncStatus,
|
|
List<String>? lastTaskArtifactRelativePaths,
|
|
OpenClawTaskAssociation? openClawTaskAssociation,
|
|
List<TaskInputAttachmentRecord>? taskInputAttachments,
|
|
}) : threadId = _resolveThreadId(threadId),
|
|
title = title ?? '',
|
|
ownerScope =
|
|
ownerScope ??
|
|
const ThreadOwnerScope(
|
|
realm: ThreadRealm.local,
|
|
subjectType: ThreadSubjectType.user,
|
|
subjectId: '',
|
|
displayName: '',
|
|
),
|
|
workspaceBinding = _validateWorkspaceBinding(workspaceBinding),
|
|
executionBinding =
|
|
executionBinding ??
|
|
ExecutionBinding(
|
|
executionMode: ThreadExecutionMode.gateway,
|
|
executorId: SingleAgentProvider.unspecified.providerId,
|
|
providerId: SingleAgentProvider.unspecified.providerId,
|
|
endpointId: '',
|
|
),
|
|
contextState =
|
|
contextState ??
|
|
ThreadContextState(
|
|
messages: messages ?? const <GatewayChatMessage>[],
|
|
selectedModelId: assistantModelId?.trim() ?? '',
|
|
selectedSkillKeys: selectedSkillKeys ?? const <String>[],
|
|
permissionLevel:
|
|
permissionLevel ?? AssistantPermissionLevel.defaultAccess,
|
|
messageViewMode:
|
|
messageViewMode ?? AssistantMessageViewMode.rendered,
|
|
latestResolvedRuntimeModel:
|
|
latestResolvedRuntimeModel?.trim() ?? '',
|
|
latestResolvedProviderId: latestResolvedProviderId?.trim() ?? '',
|
|
gatewayEntryState: gatewayEntryState?.trim(),
|
|
lastRemoteWorkingDirectory:
|
|
lastRemoteWorkingDirectory?.trim().isNotEmpty == true
|
|
? lastRemoteWorkingDirectory!.trim()
|
|
: null,
|
|
lastRemoteWorkspaceRefKind: lastRemoteWorkspaceRefKind,
|
|
lastArtifactSyncAtMs: lastArtifactSyncAtMs,
|
|
lastArtifactSyncStatus:
|
|
lastArtifactSyncStatus?.trim().isNotEmpty == true
|
|
? lastArtifactSyncStatus!.trim()
|
|
: null,
|
|
lastTaskArtifactRelativePaths: _stringListFromJson(
|
|
lastTaskArtifactRelativePaths,
|
|
),
|
|
openClawTaskAssociation: openClawTaskAssociation,
|
|
taskInputAttachments:
|
|
taskInputAttachments ?? const <TaskInputAttachmentRecord>[],
|
|
),
|
|
lifecycleState =
|
|
lifecycleState ??
|
|
ThreadLifecycleState(
|
|
archived: archived ?? false,
|
|
status: 'ready',
|
|
lastRunAtMs: lastRunAtMs,
|
|
lastResultCode: lastResultCode?.trim(),
|
|
),
|
|
createdAtMs =
|
|
createdAtMs ??
|
|
updatedAtMs ??
|
|
DateTime.now().millisecondsSinceEpoch.toDouble();
|
|
|
|
final String threadId;
|
|
final String title;
|
|
final ThreadOwnerScope ownerScope;
|
|
final WorkspaceBinding workspaceBinding;
|
|
final ExecutionBinding executionBinding;
|
|
final ThreadContextState contextState;
|
|
final ThreadLifecycleState lifecycleState;
|
|
final double createdAtMs;
|
|
final double? updatedAtMs;
|
|
|
|
String get sessionKey => threadId;
|
|
List<GatewayChatMessage> get messages => contextState.messages;
|
|
List<String> get selectedSkillKeys => contextState.selectedSkillKeys;
|
|
String get assistantModelId => contextState.selectedModelId;
|
|
AssistantMessageViewMode get messageViewMode => contextState.messageViewMode;
|
|
String? get gatewayEntryState => contextState.gatewayEntryState;
|
|
String? get lastRemoteWorkingDirectory =>
|
|
contextState.lastRemoteWorkingDirectory;
|
|
WorkspaceRefKind? get lastRemoteWorkspaceRefKind =>
|
|
contextState.lastRemoteWorkspaceRefKind;
|
|
double? get lastArtifactSyncAtMs => contextState.lastArtifactSyncAtMs;
|
|
String? get lastArtifactSyncStatus => contextState.lastArtifactSyncStatus;
|
|
List<String> get lastTaskArtifactRelativePaths =>
|
|
contextState.lastTaskArtifactRelativePaths;
|
|
OpenClawTaskAssociation? get openClawTaskAssociation =>
|
|
contextState.openClawTaskAssociation;
|
|
List<TaskInputAttachmentRecord> get taskInputAttachments =>
|
|
contextState.taskInputAttachments;
|
|
String get latestResolvedRuntimeModel =>
|
|
contextState.latestResolvedRuntimeModel;
|
|
String get latestResolvedProviderId => contextState.latestResolvedProviderId;
|
|
bool get hasExplicitExecutionTargetSelection =>
|
|
executionBinding.executionModeSource == ThreadSelectionSource.explicit;
|
|
bool get hasExplicitProviderSelection =>
|
|
executionBinding.providerSource == ThreadSelectionSource.explicit &&
|
|
executionBinding.providerId.trim().isNotEmpty;
|
|
bool get hasExplicitModelSelection =>
|
|
contextState.selectedModelSource == ThreadSelectionSource.explicit;
|
|
bool get hasExplicitSkillSelection =>
|
|
contextState.selectedSkillsSource == ThreadSelectionSource.explicit;
|
|
bool get archived => lifecycleState.archived;
|
|
String get workspacePath => workspaceBinding.workspacePath;
|
|
String get displayPath => workspaceBinding.displayPath;
|
|
WorkspaceKind get workspaceKind => workspaceBinding.workspaceKind;
|
|
|
|
TaskThread copyWith({
|
|
String? threadId,
|
|
String? title,
|
|
ThreadOwnerScope? ownerScope,
|
|
WorkspaceBinding? workspaceBinding,
|
|
ExecutionBinding? executionBinding,
|
|
ThreadContextState? contextState,
|
|
ThreadLifecycleState? lifecycleState,
|
|
double? createdAtMs,
|
|
double? updatedAtMs,
|
|
List<GatewayChatMessage>? messages,
|
|
bool? archived,
|
|
AssistantMessageViewMode? messageViewMode,
|
|
List<String>? selectedSkillKeys,
|
|
String? assistantModelId,
|
|
ThreadSelectionSource? assistantModelSource,
|
|
ThreadSelectionSource? selectedSkillsSource,
|
|
String? gatewayEntryState,
|
|
bool clearGatewayEntryState = false,
|
|
String? latestResolvedRuntimeModel,
|
|
String? latestResolvedProviderId,
|
|
String? lastRemoteWorkingDirectory,
|
|
WorkspaceRefKind? lastRemoteWorkspaceRefKind,
|
|
double? lastArtifactSyncAtMs,
|
|
String? lastArtifactSyncStatus,
|
|
List<String>? lastTaskArtifactRelativePaths,
|
|
OpenClawTaskAssociation? openClawTaskAssociation,
|
|
bool clearOpenClawTaskAssociation = false,
|
|
List<TaskInputAttachmentRecord>? taskInputAttachments,
|
|
}) {
|
|
return TaskThread(
|
|
threadId: threadId ?? this.threadId,
|
|
title: title ?? this.title,
|
|
ownerScope: ownerScope ?? this.ownerScope,
|
|
workspaceBinding: workspaceBinding ?? this.workspaceBinding,
|
|
executionBinding: executionBinding ?? this.executionBinding,
|
|
contextState: (contextState ?? this.contextState).copyWith(
|
|
messages: messages,
|
|
messageViewMode: messageViewMode,
|
|
selectedSkillKeys: selectedSkillKeys,
|
|
selectedModelId: assistantModelId,
|
|
selectedModelSource: assistantModelSource,
|
|
selectedSkillsSource: selectedSkillsSource,
|
|
latestResolvedRuntimeModel: latestResolvedRuntimeModel,
|
|
latestResolvedProviderId: latestResolvedProviderId,
|
|
gatewayEntryState: gatewayEntryState,
|
|
clearGatewayEntryState: clearGatewayEntryState,
|
|
lastRemoteWorkingDirectory: lastRemoteWorkingDirectory,
|
|
lastRemoteWorkspaceRefKind: lastRemoteWorkspaceRefKind,
|
|
lastArtifactSyncAtMs: lastArtifactSyncAtMs,
|
|
lastArtifactSyncStatus: lastArtifactSyncStatus,
|
|
lastTaskArtifactRelativePaths: lastTaskArtifactRelativePaths,
|
|
openClawTaskAssociation: openClawTaskAssociation,
|
|
clearOpenClawTaskAssociation: clearOpenClawTaskAssociation,
|
|
taskInputAttachments: taskInputAttachments,
|
|
),
|
|
lifecycleState: (lifecycleState ?? this.lifecycleState).copyWith(
|
|
archived: archived,
|
|
),
|
|
createdAtMs: createdAtMs ?? this.createdAtMs,
|
|
updatedAtMs: updatedAtMs ?? this.updatedAtMs,
|
|
);
|
|
}
|
|
|
|
static String _resolveThreadId(String threadId) {
|
|
return threadId.trim();
|
|
}
|
|
|
|
static WorkspaceBinding _validateWorkspaceBinding(
|
|
WorkspaceBinding workspaceBinding,
|
|
) {
|
|
if (!workspaceBinding.isComplete) {
|
|
throw StateError(
|
|
'TaskThread requires a complete workspaceBinding at create/load time.',
|
|
);
|
|
}
|
|
return workspaceBinding;
|
|
}
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return <String, dynamic>{
|
|
'schemaVersion': taskThreadSchemaVersion,
|
|
'threadId': threadId,
|
|
'title': title,
|
|
'ownerScope': ownerScope.toJson(),
|
|
'workspaceBinding': workspaceBinding.toJson(),
|
|
'executionBinding': executionBinding.toJson(),
|
|
'contextState': contextState.toJson(),
|
|
'lifecycleState': lifecycleState.toJson(),
|
|
'createdAtMs': createdAtMs,
|
|
'updatedAtMs': updatedAtMs,
|
|
};
|
|
}
|
|
|
|
factory TaskThread.fromJson(Map<String, dynamic> json) {
|
|
double? asDouble(Object? value) {
|
|
if (value is num) {
|
|
return value.toDouble();
|
|
}
|
|
return double.tryParse(value?.toString() ?? '');
|
|
}
|
|
|
|
Map<String, dynamic> workspaceBindingJson() {
|
|
final nested =
|
|
(json['workspaceBinding'] as Map?)?.cast<String, dynamic>() ??
|
|
const <String, dynamic>{};
|
|
if (nested.isNotEmpty) {
|
|
return nested;
|
|
}
|
|
final workspacePath =
|
|
json['workspacePath']?.toString().trim().isNotEmpty == true
|
|
? json['workspacePath'].toString().trim()
|
|
: (json['workspaceRef']?.toString().trim() ?? '');
|
|
final workspaceKindValue =
|
|
json['workspaceKind']?.toString().trim().isNotEmpty == true
|
|
? json['workspaceKind'].toString().trim()
|
|
: (json['workspaceRefKind']?.toString().trim() ?? '');
|
|
return <String, dynamic>{
|
|
'workspaceId': json['workspaceId']?.toString().trim().isNotEmpty == true
|
|
? json['workspaceId']
|
|
: (json['threadId']?.toString().trim() ?? ''),
|
|
'workspaceKind': workspaceKindValue,
|
|
'workspacePath': workspacePath,
|
|
'displayPath': json['displayPath']?.toString().trim().isNotEmpty == true
|
|
? json['displayPath']
|
|
: workspacePath,
|
|
'writable': json['writable'] as bool? ?? true,
|
|
};
|
|
}
|
|
|
|
Map<String, dynamic> executionBindingJson() {
|
|
final nested =
|
|
(json['executionBinding'] as Map?)?.cast<String, dynamic>() ??
|
|
const <String, dynamic>{};
|
|
if (nested.isNotEmpty &&
|
|
isLegacyAutoThreadExecutionModeValue(
|
|
nested['executionMode']?.toString(),
|
|
)) {
|
|
throw const FormatException(
|
|
'TaskThread.executionBinding.executionMode "auto" is no longer supported.',
|
|
);
|
|
}
|
|
if (nested.isNotEmpty) {
|
|
return nested;
|
|
}
|
|
throw const FormatException('TaskThread.executionBinding is required.');
|
|
}
|
|
|
|
Map<String, dynamic> contextStateJson() {
|
|
final nested =
|
|
(json['contextState'] as Map?)?.cast<String, dynamic>() ??
|
|
const <String, dynamic>{};
|
|
if (nested.isNotEmpty) {
|
|
return nested;
|
|
}
|
|
return <String, dynamic>{
|
|
'messages': json['messages'],
|
|
'selectedModelId': json['assistantModelId'],
|
|
'selectedSkillKeys': json['selectedSkillKeys'],
|
|
'permissionLevel': json['permissionLevel'],
|
|
'messageViewMode': json['messageViewMode'],
|
|
'latestResolvedRuntimeModel': json['latestResolvedRuntimeModel'],
|
|
'latestResolvedProviderId': json['latestResolvedProviderId'],
|
|
'selectedModelSource': json['assistantModelSource'],
|
|
'selectedSkillsSource': json['selectedSkillsSource'],
|
|
'gatewayEntryState': json['gatewayEntryState'],
|
|
'lastRemoteWorkingDirectory': json['lastRemoteWorkingDirectory'],
|
|
'lastRemoteWorkspaceRefKind': json['lastRemoteWorkspaceRefKind'],
|
|
'lastArtifactSyncAtMs': json['lastArtifactSyncAtMs'],
|
|
'lastArtifactSyncStatus': json['lastArtifactSyncStatus'],
|
|
'lastTaskArtifactRelativePaths': json['lastTaskArtifactRelativePaths'],
|
|
'openClawTaskAssociation': json['openClawTaskAssociation'],
|
|
'taskInputAttachments': json['taskInputAttachments'],
|
|
};
|
|
}
|
|
|
|
Map<String, dynamic> lifecycleStateJson() {
|
|
final nested =
|
|
(json['lifecycleState'] as Map?)?.cast<String, dynamic>() ??
|
|
const <String, dynamic>{};
|
|
if (nested.isNotEmpty) {
|
|
return nested;
|
|
}
|
|
return <String, dynamic>{
|
|
'archived': json['archived'],
|
|
'status': json['status'],
|
|
'lastRunAtMs': json['lastRunAtMs'],
|
|
'lastResultCode': json['lastResultCode'],
|
|
};
|
|
}
|
|
|
|
return TaskThread(
|
|
threadId: json['threadId']?.toString() ?? '',
|
|
title: json['title']?.toString() ?? '',
|
|
ownerScope: ThreadOwnerScope.fromJson(
|
|
(json['ownerScope'] as Map?)?.cast<String, dynamic>() ??
|
|
const <String, dynamic>{},
|
|
),
|
|
workspaceBinding: WorkspaceBinding.fromJson(workspaceBindingJson()),
|
|
executionBinding: ExecutionBinding.fromJson(executionBindingJson()),
|
|
contextState: ThreadContextState.fromJson(contextStateJson()),
|
|
lifecycleState: ThreadLifecycleState.fromJson(lifecycleStateJson()),
|
|
createdAtMs:
|
|
asDouble(json['createdAtMs']) ??
|
|
asDouble(json['updatedAtMs']) ??
|
|
DateTime.now().millisecondsSinceEpoch.toDouble(),
|
|
updatedAtMs: asDouble(json['updatedAtMs']),
|
|
);
|
|
}
|
|
}
|
|
|
|
const int kDefaultTaskTitleMaxLength = 32;
|
|
|
|
bool isNewConversationTaskTitle(String title) {
|
|
final trimmed = title.trim();
|
|
return trimmed == '新对话' || trimmed == 'New conversation';
|
|
}
|
|
|
|
String firstUserMessageTaskTitle(
|
|
Iterable<GatewayChatMessage> messages, {
|
|
String fallback = '',
|
|
}) {
|
|
for (final message in messages) {
|
|
if (message.role.trim().toLowerCase() != 'user') {
|
|
continue;
|
|
}
|
|
final text = message.text.trim();
|
|
if (text.isEmpty) {
|
|
continue;
|
|
}
|
|
if (text.length <= kDefaultTaskTitleMaxLength) {
|
|
return text;
|
|
}
|
|
return '${text.substring(0, kDefaultTaskTitleMaxLength)}...';
|
|
}
|
|
return fallback.trim().isEmpty
|
|
? appText('新对话', 'New conversation')
|
|
: fallback;
|
|
}
|
|
|
|
String derivePersistedTaskTitle(
|
|
String currentTitle,
|
|
Iterable<GatewayChatMessage> messages, {
|
|
String fallback = '',
|
|
bool hasCustomTitle = false,
|
|
}) {
|
|
if (hasCustomTitle) {
|
|
return currentTitle.trim();
|
|
}
|
|
final trimmedCurrent = currentTitle.trim();
|
|
if (trimmedCurrent.isNotEmpty &&
|
|
!isNewConversationTaskTitle(trimmedCurrent)) {
|
|
return trimmedCurrent;
|
|
}
|
|
return firstUserMessageTaskTitle(messages, fallback: fallback);
|
|
}
|