newmark-agent 0.4.6 → 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/app-icon-dark.svg +6 -0
- package/dist/assets/app-icon-dark.svg +6 -0
- package/dist/cli-commands.js +2 -1
- package/dist/cli-discovery.js +2 -0
- package/dist/conversation-utility-host.bundle.cjs +577 -166
- package/dist/conversation-utility-host.js +3 -0
- package/dist/core/agent.d.ts +40 -4
- package/dist/core/agent.js +251 -49
- package/dist/core/agentKernelRunner.d.ts +3 -0
- package/dist/core/agentKernelRunner.js +74 -91
- package/dist/core/config.js +1 -1
- package/dist/core/conversationKernel.d.ts +40 -0
- package/dist/core/conversationKernel.js +219 -8
- package/dist/core/electronUtilityAgentClient.d.ts +8 -0
- package/dist/core/electronUtilityAgentClient.js +5 -1
- package/dist/core/electronUtilityRuntimePool.d.ts +15 -0
- package/dist/core/electronUtilityRuntimePool.js +11 -0
- package/dist/core/installUpdate.js +11 -8
- package/dist/core/mobilePairing.d.ts +1 -0
- package/dist/core/mobilePairing.js +15 -1
- package/dist/core/subagent.d.ts +10 -3
- package/dist/core/subagent.js +23 -8
- package/dist/core/toolPolicy.js +14 -3
- package/dist/core/utilityAgentProtocol.d.ts +29 -1
- package/dist/core/utilityHostToolRouter.js +18 -0
- package/dist/core/wslAgentClient.d.ts +8 -0
- package/dist/core/wslAgentClient.js +5 -1
- package/dist/core/wslAgentProtocol.d.ts +18 -1
- package/dist/core/wslAgentRuntimePool.d.ts +15 -0
- package/dist/core/wslAgentRuntimePool.js +11 -0
- package/dist/launcher.js +14 -11
- package/dist/main.js +207 -9
- package/dist/providers/chat-completions.adapter.js +6 -2
- package/dist/providers/responses.adapter.js +1 -0
- package/dist/server.d.ts +33 -1
- package/dist/server.js +696 -48
- package/dist/toolchain/registry-seeder.js +3 -1
- package/dist/tools/index.js +57 -6
- package/dist/tools/nativeTools.js +2 -1
- package/dist/ui/index.html +88 -101
- package/dist/wsl-agent-host.bundle.cjs +577 -166
- package/dist/wsl-agent-host.js +3 -0
- package/package.json +4 -2
package/dist/server.js
CHANGED
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.runServer = runServer;
|
|
37
|
+
exports.setHostedServerAutomation = setHostedServerAutomation;
|
|
37
38
|
const http = __importStar(require("http"));
|
|
38
39
|
const fs = __importStar(require("fs"));
|
|
39
40
|
const path = __importStar(require("path"));
|
|
@@ -54,6 +55,28 @@ let automation = null;
|
|
|
54
55
|
let workspaceFileRouter = null;
|
|
55
56
|
let mobileToken = '';
|
|
56
57
|
let appRoot = '';
|
|
58
|
+
const mobileWorkEventSubscribers = new Set();
|
|
59
|
+
const mobileScopedRuntimes = new Map();
|
|
60
|
+
let hostedWorkEventSink = null;
|
|
61
|
+
let unsubscribeHostedWorkEvents = null;
|
|
62
|
+
let hostedConversationUiState;
|
|
63
|
+
let hostedConversationUiAction;
|
|
64
|
+
let hostedConversationPrompt;
|
|
65
|
+
function mobileRuntimeKey(workspaceId, conversationId) {
|
|
66
|
+
return `${String(workspaceId || '')}::${String(conversationId || '')}`;
|
|
67
|
+
}
|
|
68
|
+
function publishMobileWorkEvent(event) {
|
|
69
|
+
for (const subscriber of mobileWorkEventSubscribers) {
|
|
70
|
+
try {
|
|
71
|
+
subscriber(event);
|
|
72
|
+
}
|
|
73
|
+
catch { /* ignore disconnected mobile listeners */ }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function publishServerWorkEvent(event) {
|
|
77
|
+
publishMobileWorkEvent(event);
|
|
78
|
+
hostedWorkEventSink?.(event);
|
|
79
|
+
}
|
|
57
80
|
function mobileAuthorized(req) {
|
|
58
81
|
if (!mobileToken)
|
|
59
82
|
return false;
|
|
@@ -77,6 +100,103 @@ function mobileJson(res, data, code = 200) {
|
|
|
77
100
|
});
|
|
78
101
|
res.end(body);
|
|
79
102
|
}
|
|
103
|
+
function resolveMobileWorkspace(current, workspaceId) {
|
|
104
|
+
const clean = String(workspaceId || '');
|
|
105
|
+
return [...current.workspace.internal, ...current.workspace.external].find(workspace => workspace.id === clean)
|
|
106
|
+
|| (current.workspace.current?.id === clean ? current.workspace.current : null);
|
|
107
|
+
}
|
|
108
|
+
const MOBILE_EDITABLE_EXTENSIONS = new Set([
|
|
109
|
+
'.txt', '.md', '.markdown', '.json', '.jsonc', '.yaml', '.yml', '.toml', '.ini', '.cfg', '.conf',
|
|
110
|
+
'.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.css', '.scss', '.html', '.htm', '.xml', '.svg',
|
|
111
|
+
'.kt', '.kts', '.java', '.py', '.rb', '.rs', '.go', '.c', '.h', '.cpp', '.hpp', '.cs', '.sh',
|
|
112
|
+
'.bash', '.zsh', '.ps1', '.bat', '.cmd', '.sql', '.tex', '.typ', '.properties', '.gradle', '.gitignore',
|
|
113
|
+
]);
|
|
114
|
+
const MOBILE_EDITOR_MAX_BYTES = 1024 * 1024;
|
|
115
|
+
function resolveMobileWorkspacePath(ws, relativePath) {
|
|
116
|
+
const root = path.resolve(ws.path);
|
|
117
|
+
const clean = String(relativePath || '').replace(/\\/g, '/').replace(/^\/+/, '');
|
|
118
|
+
const target = path.resolve(root, clean || '.');
|
|
119
|
+
if (target !== root && !target.startsWith(root + path.sep))
|
|
120
|
+
throw new Error('Path escapes workspace');
|
|
121
|
+
return { root, target, relative: path.relative(root, target).replace(/\\/g, '/') };
|
|
122
|
+
}
|
|
123
|
+
async function assertMobileRealPathContained(root, target) {
|
|
124
|
+
const realRoot = await fs.promises.realpath(root);
|
|
125
|
+
const realTarget = await fs.promises.realpath(target);
|
|
126
|
+
if (realTarget !== realRoot && !realTarget.startsWith(realRoot + path.sep))
|
|
127
|
+
throw new Error('Path escapes workspace through a symbolic link');
|
|
128
|
+
}
|
|
129
|
+
function mobileEditableFile(target) {
|
|
130
|
+
const base = path.basename(target).toLowerCase();
|
|
131
|
+
return MOBILE_EDITABLE_EXTENSIONS.has(path.extname(base)) || MOBILE_EDITABLE_EXTENSIONS.has(base);
|
|
132
|
+
}
|
|
133
|
+
function mobileRightSidebarState(current, ws, conversationId) {
|
|
134
|
+
const scoped = mobileScopedAgent(ws, conversationId);
|
|
135
|
+
return {
|
|
136
|
+
workspace: { id: ws.id, name: ws.name, path: ws.path, isInternal: ws.isInternal },
|
|
137
|
+
conversationId,
|
|
138
|
+
conversationPlan: scoped.getConversationPlan(conversationId),
|
|
139
|
+
linkedPlan: scoped.getLinkedPlan(conversationId),
|
|
140
|
+
subagents: scoped.subagents.listAll()
|
|
141
|
+
.filter(record => !record.conversationId || record.conversationId === conversationId)
|
|
142
|
+
.map(record => ({
|
|
143
|
+
id: record.id,
|
|
144
|
+
name: record.name,
|
|
145
|
+
displayName: record.displayName,
|
|
146
|
+
status: record.status,
|
|
147
|
+
model: record.model,
|
|
148
|
+
mode: record.agentMode,
|
|
149
|
+
inputMode: record.inputMode,
|
|
150
|
+
result: record.result,
|
|
151
|
+
error: record.error || '',
|
|
152
|
+
messageCount: record.messages.length,
|
|
153
|
+
messages: record.messages.slice(-40),
|
|
154
|
+
})),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function mobileWorkspaceConversationRows(current, ws) {
|
|
158
|
+
const activeConversationId = current.activeConversationIdForWorkspace(ws);
|
|
159
|
+
const isRuntimeWorkspace = !!current.workspace.current
|
|
160
|
+
&& path.resolve(current.workspace.current.path) === path.resolve(ws.path);
|
|
161
|
+
const activeRuntimeStatus = current.status === 'working' ? 'running' : current.status;
|
|
162
|
+
return current.listWorkspaceConversationStates(ws).map(conversation => {
|
|
163
|
+
const scopedRuntime = mobileScopedRuntimes.get(mobileRuntimeKey(String(ws.id || ''), conversation.id));
|
|
164
|
+
const scopedStatus = scopedRuntime?.agent.status === 'working' ? 'running' : scopedRuntime?.agent.status || '';
|
|
165
|
+
const runtimeStatus = scopedRuntime
|
|
166
|
+
? scopedStatus
|
|
167
|
+
: isRuntimeWorkspace && conversation.id === current.activeConversationId && activeRuntimeStatus !== 'idle'
|
|
168
|
+
? activeRuntimeStatus
|
|
169
|
+
: '';
|
|
170
|
+
return {
|
|
171
|
+
...conversation,
|
|
172
|
+
active: conversation.id === activeConversationId || !!scopedRuntime,
|
|
173
|
+
runtimeStatus,
|
|
174
|
+
running: ['running', 'stopping', 'force_restarting'].includes(runtimeStatus),
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
function mobileScopedAgent(ws, conversationId) {
|
|
179
|
+
const scoped = new agent_1.Agent(appRoot, {
|
|
180
|
+
agentOnly: true,
|
|
181
|
+
workspaceRegistryMode: 'detached',
|
|
182
|
+
readOnlyConfig: true,
|
|
183
|
+
conversationId,
|
|
184
|
+
});
|
|
185
|
+
scoped.workspace.current = { ...ws };
|
|
186
|
+
scoped.config.loadWorkspaceConfig(ws.path);
|
|
187
|
+
scoped.setConversationFromStorage(conversationId);
|
|
188
|
+
return scoped;
|
|
189
|
+
}
|
|
190
|
+
function mobileConversationRuntimeBusy(current, ws, conversationId) {
|
|
191
|
+
const scoped = mobileScopedRuntimes.get(mobileRuntimeKey(String(ws.id || ''), conversationId));
|
|
192
|
+
if (scoped && scoped.agent.status !== 'idle')
|
|
193
|
+
return true;
|
|
194
|
+
const currentWs = current.workspace.current;
|
|
195
|
+
return !!currentWs
|
|
196
|
+
&& path.resolve(currentWs.path) === path.resolve(ws.path)
|
|
197
|
+
&& current.activeConversationId === conversationId
|
|
198
|
+
&& current.status !== 'idle';
|
|
199
|
+
}
|
|
80
200
|
function handleMobileEvents(req, res) {
|
|
81
201
|
if (agent && !agent.config.getBool('remote', 'touch_enabled')) {
|
|
82
202
|
mobileJson(res, { error: 'Remote touch disabled' }, 403);
|
|
@@ -97,23 +217,24 @@ function handleMobileEvents(req, res) {
|
|
|
97
217
|
'Access-Control-Allow-Origin': '*',
|
|
98
218
|
});
|
|
99
219
|
res.write('retry: 3000\n\n');
|
|
100
|
-
const
|
|
220
|
+
const subscriber = (event) => {
|
|
101
221
|
try {
|
|
102
222
|
res.write(`event: work\ndata: ${JSON.stringify(event)}\n\n`);
|
|
103
223
|
}
|
|
104
224
|
catch {
|
|
105
225
|
// socket is gone; the close handler will clean up
|
|
106
226
|
}
|
|
107
|
-
}
|
|
227
|
+
};
|
|
228
|
+
mobileWorkEventSubscribers.add(subscriber);
|
|
108
229
|
const heartbeat = setInterval(() => {
|
|
109
230
|
try {
|
|
110
231
|
res.write(': ping\n\n');
|
|
111
232
|
}
|
|
112
233
|
catch { }
|
|
113
234
|
}, 15000);
|
|
114
|
-
|
|
235
|
+
res.on('close', () => {
|
|
115
236
|
clearInterval(heartbeat);
|
|
116
|
-
|
|
237
|
+
mobileWorkEventSubscribers.delete(subscriber);
|
|
117
238
|
});
|
|
118
239
|
}
|
|
119
240
|
function resolveAppPath(root, targetPath) {
|
|
@@ -705,6 +826,7 @@ async function handleApi(req, res, body) {
|
|
|
705
826
|
hostname: os.hostname(),
|
|
706
827
|
platform: process.platform,
|
|
707
828
|
tailscaleIpv4: (0, mobilePairing_1.tailscaleIpv4)(),
|
|
829
|
+
lanIpv4: (0, mobilePairing_1.lanIpv4)(),
|
|
708
830
|
workspace: agent.workspace.current ? {
|
|
709
831
|
id: agent.workspace.current.id,
|
|
710
832
|
name: agent.workspace.current.name,
|
|
@@ -717,9 +839,17 @@ async function handleApi(req, res, body) {
|
|
|
717
839
|
}
|
|
718
840
|
case '/api/mobile/state': {
|
|
719
841
|
const active = agent.getConversationSnapshot(agent.activeConversationId, { window: 200 });
|
|
842
|
+
// 完整对话信息:workRuns 用持久化记录透出(含被中断的构建,不依赖运行时内存)
|
|
843
|
+
const persistedRuns = agent.getPersistedConversationWorkRuns(agent.activeConversationId);
|
|
844
|
+
if (persistedRuns.length)
|
|
845
|
+
active.workRuns = persistedRuns;
|
|
720
846
|
mobileJson(res, {
|
|
721
847
|
mode: agent.mode,
|
|
722
848
|
model: agent.modelSelectionValue(),
|
|
849
|
+
modelLabel: agent.modelLabel(),
|
|
850
|
+
models: agent.allModelNames(),
|
|
851
|
+
providers: (0, config_1.sanitizeProvidersForState)(agent.config.providers()),
|
|
852
|
+
intelligence: agent.intelligence,
|
|
723
853
|
status: agent.status,
|
|
724
854
|
activeConversationId: agent.activeConversationId,
|
|
725
855
|
conversations: agent.listConversationStates(),
|
|
@@ -729,6 +859,11 @@ async function handleApi(req, res, body) {
|
|
|
729
859
|
chatMessages: active.chatMessages,
|
|
730
860
|
totalMessages: active.totalMessages,
|
|
731
861
|
conversationLocked: agent.isConversationLocked(),
|
|
862
|
+
// workRuns 透出:完整 Build Block 记录(含 interrupted/force_interrupted),供移动端渲染被中断的对话
|
|
863
|
+
workRuns: active.workRuns,
|
|
864
|
+
// goal bar / flow bar:目标状态与当前 Flow 选择
|
|
865
|
+
goal: active.goal,
|
|
866
|
+
flowSelection: active.flowSelection,
|
|
732
867
|
});
|
|
733
868
|
return;
|
|
734
869
|
}
|
|
@@ -737,12 +872,37 @@ async function handleApi(req, res, body) {
|
|
|
737
872
|
return;
|
|
738
873
|
}
|
|
739
874
|
case '/api/mobile/conversation': {
|
|
740
|
-
const
|
|
875
|
+
const workspaceId = url.searchParams.get('workspaceId') || '';
|
|
741
876
|
const windowParam = url.searchParams.get('window');
|
|
742
877
|
const beforeParam = url.searchParams.get('before');
|
|
743
878
|
const windowSize = windowParam ? Math.max(1, Math.min(500, Number(windowParam) || 200)) : 200;
|
|
744
879
|
const before = beforeParam ? Math.max(0, Number(beforeParam) || 0) : undefined;
|
|
745
|
-
const
|
|
880
|
+
const current = agent;
|
|
881
|
+
const ws = workspaceId ? resolveMobileWorkspace(current, workspaceId) : null;
|
|
882
|
+
if (workspaceId && !ws) {
|
|
883
|
+
mobileJson(res, { error: 'Unknown workspace' }, 404);
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
const conversationId = url.searchParams.get('conversationId')
|
|
887
|
+
|| (ws ? current.activeConversationIdForWorkspace(ws) : current.activeConversationId);
|
|
888
|
+
if (ws && !current.hasConversationInWorkspace(String(conversationId), ws)) {
|
|
889
|
+
mobileJson(res, { error: 'Conversation not found in workspace' }, 404);
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
// GUI conversations normally execute inside isolated runtime pools.
|
|
893
|
+
// Their durable target state can be newer than the hosted main Agent's
|
|
894
|
+
// foreground memory, so workspace-bound mobile reads must hydrate a
|
|
895
|
+
// fresh read-only target Agent from storage.
|
|
896
|
+
const snapshotAgent = ws ? mobileScopedAgent(ws, String(conversationId)) : current;
|
|
897
|
+
const snapshot = snapshotAgent.getConversationSnapshot(String(conversationId), {
|
|
898
|
+
window: windowSize,
|
|
899
|
+
before,
|
|
900
|
+
workspace: ws || undefined,
|
|
901
|
+
});
|
|
902
|
+
// 完整对话信息:workRuns 统一用持久化记录透出(含被中断的构建)
|
|
903
|
+
const persistedRuns = snapshotAgent.getPersistedConversationWorkRuns(String(conversationId), ws || undefined);
|
|
904
|
+
if (persistedRuns.length)
|
|
905
|
+
snapshot.workRuns = persistedRuns;
|
|
746
906
|
mobileJson(res, snapshot);
|
|
747
907
|
return;
|
|
748
908
|
}
|
|
@@ -750,6 +910,305 @@ async function handleApi(req, res, body) {
|
|
|
750
910
|
mobileJson(res, { internal: agent.workspace.internal, external: agent.workspace.external, current: agent.workspace.current });
|
|
751
911
|
return;
|
|
752
912
|
}
|
|
913
|
+
case '/api/mobile/workspace-conversations': {
|
|
914
|
+
const workspaceId = url.searchParams.get('workspaceId') || '';
|
|
915
|
+
const current = agent;
|
|
916
|
+
const ws = resolveMobileWorkspace(current, workspaceId);
|
|
917
|
+
if (!ws) {
|
|
918
|
+
mobileJson(res, { error: 'Unknown workspace' }, 404);
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
const conversations = mobileWorkspaceConversationRows(current, ws);
|
|
922
|
+
mobileJson(res, { workspace: { id: ws.id, name: ws.name, path: ws.path, isInternal: ws.isInternal }, conversations });
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
case '/api/mobile/right-sidebar-state': {
|
|
926
|
+
const workspaceId = url.searchParams.get('workspaceId') || '';
|
|
927
|
+
const conversationId = url.searchParams.get('conversationId') || '';
|
|
928
|
+
const ws = resolveMobileWorkspace(agent, workspaceId);
|
|
929
|
+
if (!ws) {
|
|
930
|
+
mobileJson(res, { error: 'Unknown workspace' }, 404);
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
if (!conversationId || !agent.hasConversationInWorkspace(conversationId, ws)) {
|
|
934
|
+
mobileJson(res, { error: 'Conversation not found in workspace' }, 404);
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
mobileJson(res, mobileRightSidebarState(agent, ws, conversationId));
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
case '/api/mobile/conversation-ui-state': {
|
|
941
|
+
const workspaceId = url.searchParams.get('workspaceId') || '';
|
|
942
|
+
const conversationId = url.searchParams.get('conversationId') || '';
|
|
943
|
+
const ws = resolveMobileWorkspace(agent, workspaceId);
|
|
944
|
+
if (!ws) {
|
|
945
|
+
mobileJson(res, { error: 'Unknown workspace' }, 404);
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
if (!conversationId || !agent.hasConversationInWorkspace(conversationId, ws)) {
|
|
949
|
+
mobileJson(res, { error: 'Conversation not found in workspace' }, 404);
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
if (hostedConversationUiState) {
|
|
953
|
+
mobileJson(res, await hostedConversationUiState({ workspaceId, conversationId }));
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
const scoped = mobileScopedAgent(ws, conversationId);
|
|
957
|
+
const snapshot = scoped.getConversationSnapshot(conversationId, { workspace: ws, window: 1 });
|
|
958
|
+
mobileJson(res, {
|
|
959
|
+
goal: snapshot.goal,
|
|
960
|
+
flowSelection: snapshot.flowSelection,
|
|
961
|
+
flow: null,
|
|
962
|
+
queued: { steering: [], followUp: [] },
|
|
963
|
+
runtime: null,
|
|
964
|
+
mode: scoped.mode,
|
|
965
|
+
status: scoped.status,
|
|
966
|
+
inputMode: scoped.inputMode,
|
|
967
|
+
});
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
case '/api/mobile/workspace-files': {
|
|
971
|
+
const workspaceId = url.searchParams.get('workspaceId') || '';
|
|
972
|
+
const relativePath = url.searchParams.get('path') || '';
|
|
973
|
+
const ws = resolveMobileWorkspace(agent, workspaceId);
|
|
974
|
+
if (!ws) {
|
|
975
|
+
mobileJson(res, { error: 'Unknown workspace' }, 404);
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
const resolved = resolveMobileWorkspacePath(ws, relativePath);
|
|
979
|
+
await assertMobileRealPathContained(resolved.root, resolved.target);
|
|
980
|
+
const stat = await fs.promises.stat(resolved.target);
|
|
981
|
+
if (!stat.isDirectory()) {
|
|
982
|
+
mobileJson(res, { error: 'Path is not a directory' }, 400);
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
const entries = await fs.promises.readdir(resolved.target, { withFileTypes: true });
|
|
986
|
+
const rows = entries
|
|
987
|
+
.filter(entry => entry.name !== '.git' && entry.name !== 'node_modules')
|
|
988
|
+
.map(entry => ({
|
|
989
|
+
name: entry.name,
|
|
990
|
+
path: path.posix.join(resolved.relative, entry.name).replace(/^\.\//, ''),
|
|
991
|
+
directory: entry.isDirectory(),
|
|
992
|
+
}))
|
|
993
|
+
.sort((a, b) => Number(b.directory) - Number(a.directory) || a.name.localeCompare(b.name));
|
|
994
|
+
mobileJson(res, { workspaceId, path: resolved.relative, entries: rows });
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
case '/api/mobile/workspace-file': {
|
|
998
|
+
if (req.method === 'GET') {
|
|
999
|
+
const workspaceId = url.searchParams.get('workspaceId') || '';
|
|
1000
|
+
const relativePath = url.searchParams.get('path') || '';
|
|
1001
|
+
const ws = resolveMobileWorkspace(agent, workspaceId);
|
|
1002
|
+
if (!ws) {
|
|
1003
|
+
mobileJson(res, { error: 'Unknown workspace' }, 404);
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
const resolved = resolveMobileWorkspacePath(ws, relativePath);
|
|
1007
|
+
await assertMobileRealPathContained(resolved.root, resolved.target);
|
|
1008
|
+
const stat = await fs.promises.stat(resolved.target);
|
|
1009
|
+
if (!stat.isFile() || stat.size > MOBILE_EDITOR_MAX_BYTES || !mobileEditableFile(resolved.target)) {
|
|
1010
|
+
mobileJson(res, { error: 'File is not an editable text file' }, 415);
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
const content = (await fs.promises.readFile(resolved.target, 'utf-8')).replace(/^\uFEFF/, '');
|
|
1014
|
+
mobileJson(res, { workspaceId, path: resolved.relative, content, size: stat.size });
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
const params = JSON.parse(body || '{}');
|
|
1018
|
+
const workspaceId = String(params.workspaceId || '');
|
|
1019
|
+
const relativePath = String(params.path || '');
|
|
1020
|
+
const content = String(params.content ?? '');
|
|
1021
|
+
const ws = resolveMobileWorkspace(agent, workspaceId);
|
|
1022
|
+
if (!ws) {
|
|
1023
|
+
mobileJson(res, { error: 'Unknown workspace' }, 404);
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
if (Buffer.byteLength(content, 'utf-8') > MOBILE_EDITOR_MAX_BYTES) {
|
|
1027
|
+
mobileJson(res, { error: 'File exceeds mobile editor size limit' }, 413);
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
const resolved = resolveMobileWorkspacePath(ws, relativePath);
|
|
1031
|
+
await assertMobileRealPathContained(resolved.root, resolved.target);
|
|
1032
|
+
if (!mobileEditableFile(resolved.target)) {
|
|
1033
|
+
mobileJson(res, { error: 'File type is not editable' }, 415);
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
const stat = await fs.promises.stat(resolved.target);
|
|
1037
|
+
if (!stat.isFile()) {
|
|
1038
|
+
mobileJson(res, { error: 'Path is not a file' }, 400);
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
await fs.promises.writeFile(resolved.target, content, 'utf-8');
|
|
1042
|
+
mobileJson(res, { ok: true, workspaceId, path: resolved.relative, size: Buffer.byteLength(content, 'utf-8') });
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
case '/api/mobile/conversation-plan-update': {
|
|
1046
|
+
const params = JSON.parse(body || '{}');
|
|
1047
|
+
const workspaceId = String(params.workspaceId || '');
|
|
1048
|
+
const conversationId = String(params.conversationId || '');
|
|
1049
|
+
const ws = resolveMobileWorkspace(agent, workspaceId);
|
|
1050
|
+
if (!ws) {
|
|
1051
|
+
mobileJson(res, { error: 'Unknown workspace' }, 404);
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
if (!conversationId || !agent.hasConversationInWorkspace(conversationId, ws)) {
|
|
1055
|
+
mobileJson(res, { error: 'Conversation not found in workspace' }, 404);
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
const scoped = mobileScopedAgent(ws, conversationId);
|
|
1059
|
+
const plan = scoped.updateConversationPlan({ items: Array.isArray(params.items) ? params.items : [] }, conversationId);
|
|
1060
|
+
mobileJson(res, { ok: true, conversationPlan: plan });
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
case '/api/mobile/conversation-create': {
|
|
1064
|
+
const params = JSON.parse(body || '{}');
|
|
1065
|
+
const workspaceId = String(params.workspaceId || '');
|
|
1066
|
+
if (!workspaceId) {
|
|
1067
|
+
mobileJson(res, { error: 'No workspaceId' }, 400);
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
const ws = resolveMobileWorkspace(agent, workspaceId);
|
|
1071
|
+
if (!ws) {
|
|
1072
|
+
mobileJson(res, { error: 'Unknown workspace' }, 404);
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
const created = agent.createConversationInWorkspace(ws, String(params.title || ''));
|
|
1076
|
+
mobileJson(res, {
|
|
1077
|
+
ok: true,
|
|
1078
|
+
workspace: { id: ws.id, name: ws.name, path: ws.path, isInternal: ws.isInternal },
|
|
1079
|
+
conversation: created,
|
|
1080
|
+
activeConversationId: created.id,
|
|
1081
|
+
conversations: mobileWorkspaceConversationRows(agent, ws),
|
|
1082
|
+
});
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
case '/api/mobile/model': {
|
|
1086
|
+
const next = String(JSON.parse(body || '{}').model || '');
|
|
1087
|
+
if (!next) {
|
|
1088
|
+
mobileJson(res, { error: 'Model is required' }, 400);
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
agent.setModel(next);
|
|
1092
|
+
mobileJson(res, {
|
|
1093
|
+
ok: true,
|
|
1094
|
+
model: agent.modelSelectionValue(),
|
|
1095
|
+
modelLabel: agent.modelLabel(),
|
|
1096
|
+
});
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
case '/api/mobile/intelligence': {
|
|
1100
|
+
const tier = String(JSON.parse(body || '{}').tier || 'medium');
|
|
1101
|
+
agent.setIntelligence(tier, true);
|
|
1102
|
+
mobileJson(res, { ok: true, intelligence: agent.intelligence });
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
case '/api/mobile/conversation-ui-action': {
|
|
1106
|
+
const params = JSON.parse(body || '{}');
|
|
1107
|
+
const workspaceId = String(params.workspaceId || '');
|
|
1108
|
+
const conversationId = String(params.conversationId || '');
|
|
1109
|
+
const action = String(params.action || '');
|
|
1110
|
+
const allowed = new Set(['goal_update', 'goal_guide', 'conversation_guide', 'goal_toggle_pause', 'goal_clear', 'flow_pause', 'flow_resume', 'flow_guide', 'conversation_stop', 'input_mode', 'queue_enqueue', 'queue_update', 'queue_delete', 'queue_toggle_pause', 'queue_guide']);
|
|
1111
|
+
if (!workspaceId || !conversationId || !allowed.has(action)) {
|
|
1112
|
+
mobileJson(res, { error: 'workspaceId, conversationId, and a valid action are required' }, 400);
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
const ws = resolveMobileWorkspace(agent, workspaceId);
|
|
1116
|
+
if (!ws) {
|
|
1117
|
+
mobileJson(res, { error: 'Unknown workspace' }, 404);
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
if (!agent.hasConversationInWorkspace(conversationId, ws)) {
|
|
1121
|
+
mobileJson(res, { error: 'Conversation not found in workspace' }, 404);
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
if (!hostedConversationUiAction) {
|
|
1125
|
+
mobileJson(res, { error: 'This action requires the GUI-hosted mobile server' }, 409);
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
mobileJson(res, await hostedConversationUiAction({ workspaceId, conversationId }, action, String(params.value || ''), params));
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
case '/api/mobile/conversation-rename': {
|
|
1132
|
+
const params = JSON.parse(body || '{}');
|
|
1133
|
+
const workspaceId = String(params.workspaceId || '');
|
|
1134
|
+
const conversationId = String(params.conversationId || '');
|
|
1135
|
+
const title = String(params.title || '');
|
|
1136
|
+
if (!workspaceId || !conversationId || !title.trim()) {
|
|
1137
|
+
mobileJson(res, { error: 'workspaceId, conversationId, and title are required' }, 400);
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
const ws = resolveMobileWorkspace(agent, workspaceId);
|
|
1141
|
+
if (!ws) {
|
|
1142
|
+
mobileJson(res, { error: 'Unknown workspace' }, 404);
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
const ok = agent.renameConversation(conversationId, title, ws);
|
|
1146
|
+
if (!ok) {
|
|
1147
|
+
mobileJson(res, { ok: false, error: 'Conversation rename failed' }, 404);
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
mobileJson(res, { ok: true, conversations: mobileWorkspaceConversationRows(agent, ws) });
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
case '/api/mobile/conversation-pin': {
|
|
1154
|
+
const params = JSON.parse(body || '{}');
|
|
1155
|
+
const workspaceId = String(params.workspaceId || '');
|
|
1156
|
+
const conversationId = String(params.conversationId || '');
|
|
1157
|
+
if (!workspaceId || !conversationId) {
|
|
1158
|
+
mobileJson(res, { error: 'workspaceId and conversationId are required' }, 400);
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
const ws = resolveMobileWorkspace(agent, workspaceId);
|
|
1162
|
+
if (!ws) {
|
|
1163
|
+
mobileJson(res, { error: 'Unknown workspace' }, 404);
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
const pinned = params.pinned === true;
|
|
1167
|
+
const ok = agent.setConversationPinned(conversationId, pinned, ws);
|
|
1168
|
+
if (!ok) {
|
|
1169
|
+
mobileJson(res, { ok: false, error: 'Conversation pin update failed' }, 404);
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
mobileJson(res, { ok: true, pinned, conversations: mobileWorkspaceConversationRows(agent, ws) });
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
case '/api/mobile/conversation-reorder': {
|
|
1176
|
+
const params = JSON.parse(body || '{}');
|
|
1177
|
+
const workspaceId = String(params.workspaceId || '');
|
|
1178
|
+
const conversationIds = Array.isArray(params.conversationIds)
|
|
1179
|
+
? params.conversationIds.map((id) => String(id || ''))
|
|
1180
|
+
: [];
|
|
1181
|
+
if (!workspaceId || conversationIds.length < 2 || conversationIds.some((id) => !id)) {
|
|
1182
|
+
mobileJson(res, { error: 'workspaceId and at least two conversationIds are required' }, 400);
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
if (new Set(conversationIds).size !== conversationIds.length) {
|
|
1186
|
+
mobileJson(res, { error: 'conversationIds must be unique' }, 400);
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
const ws = resolveMobileWorkspace(agent, workspaceId);
|
|
1190
|
+
if (!ws) {
|
|
1191
|
+
mobileJson(res, { error: 'Unknown workspace' }, 404);
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
const current = agent;
|
|
1195
|
+
if (conversationIds.some((id) => !current.hasConversationInWorkspace(id, ws))) {
|
|
1196
|
+
mobileJson(res, { error: 'Conversation not found in workspace' }, 404);
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
const listedById = new Map(current.listWorkspaceConversationStates(ws).map(item => [item.id, item]));
|
|
1200
|
+
if (new Set(conversationIds.map((id) => !!listedById.get(id)?.pinned)).size !== 1) {
|
|
1201
|
+
mobileJson(res, { error: 'Conversations must remain within one pinned group' }, 409);
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
const ok = current.reorderWorkspaceConversationGroup(conversationIds, ws);
|
|
1205
|
+
if (!ok) {
|
|
1206
|
+
mobileJson(res, { ok: false, error: 'Conversation reorder failed' }, 409);
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
mobileJson(res, { ok: true, conversations: mobileWorkspaceConversationRows(current, ws) });
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
753
1212
|
case '/api/mobile/send': {
|
|
754
1213
|
const params = JSON.parse(body || '{}');
|
|
755
1214
|
const message = String(params.message || '');
|
|
@@ -757,23 +1216,188 @@ async function handleApi(req, res, body) {
|
|
|
757
1216
|
mobileJson(res, { error: 'No message' }, 400);
|
|
758
1217
|
return;
|
|
759
1218
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
1219
|
+
const workspaceId = String(params.workspaceId || '');
|
|
1220
|
+
let requestAgent = agent;
|
|
1221
|
+
let requestWorkspace = agent.workspace.current;
|
|
1222
|
+
let resolvedWorkspace = null;
|
|
1223
|
+
if (workspaceId) {
|
|
1224
|
+
const ws = resolveMobileWorkspace(agent, workspaceId);
|
|
1225
|
+
if (!ws) {
|
|
1226
|
+
mobileJson(res, { error: 'Unknown workspace' }, 404);
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
resolvedWorkspace = ws;
|
|
1230
|
+
requestWorkspace = ws;
|
|
1231
|
+
}
|
|
1232
|
+
const conversationId = String(params.conversationId
|
|
1233
|
+
|| (resolvedWorkspace ? agent.activeConversationIdForWorkspace(resolvedWorkspace) : agent.activeConversationId));
|
|
1234
|
+
if (resolvedWorkspace) {
|
|
1235
|
+
const ws = resolvedWorkspace;
|
|
1236
|
+
if (!agent.hasConversationInWorkspace(conversationId, ws)) {
|
|
1237
|
+
mobileJson(res, { error: 'Conversation not found in workspace' }, 404);
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
const currentWs = agent.workspace.current;
|
|
1241
|
+
if (!currentWs || path.resolve(currentWs.path) !== path.resolve(ws.path)) {
|
|
1242
|
+
requestAgent = mobileScopedAgent(ws, conversationId);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
if (hostedConversationPrompt && resolvedWorkspace) {
|
|
1246
|
+
const result = await hostedConversationPrompt({ workspaceId, conversationId }, message, {
|
|
1247
|
+
requestedMode: String(params.requestedMode || ''),
|
|
1248
|
+
goalObjective: String(params.goalObjective || ''),
|
|
1249
|
+
inputMode: String(params.inputMode || ''),
|
|
1250
|
+
});
|
|
1251
|
+
mobileJson(res, { ok: true, conversationId, ...result });
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
if (requestAgent === agent && params.conversationId)
|
|
1255
|
+
requestAgent.setConversation(conversationId);
|
|
1256
|
+
let scopedRuntimeKey = '';
|
|
1257
|
+
let scopedRuntime = null;
|
|
1258
|
+
let unsubscribeScoped = null;
|
|
1259
|
+
if (requestAgent !== agent && requestWorkspace) {
|
|
1260
|
+
scopedRuntimeKey = mobileRuntimeKey(String(requestWorkspace.id || ''), conversationId);
|
|
1261
|
+
unsubscribeScoped = requestAgent.subscribeWorkEvents(event => publishServerWorkEvent({
|
|
1262
|
+
...event,
|
|
1263
|
+
workspaceId: String(requestWorkspace?.id || ''),
|
|
1264
|
+
conversationId,
|
|
1265
|
+
}));
|
|
1266
|
+
scopedRuntime = {
|
|
1267
|
+
agent: requestAgent,
|
|
1268
|
+
workspaceId: String(requestWorkspace.id || ''),
|
|
1269
|
+
conversationId,
|
|
1270
|
+
unsubscribe: unsubscribeScoped,
|
|
1271
|
+
};
|
|
1272
|
+
mobileScopedRuntimes.set(scopedRuntimeKey, scopedRuntime);
|
|
1273
|
+
}
|
|
1274
|
+
let tokens;
|
|
1275
|
+
try {
|
|
1276
|
+
tokens = await requestAgent.process(message);
|
|
1277
|
+
}
|
|
1278
|
+
finally {
|
|
1279
|
+
if (scopedRuntimeKey && mobileScopedRuntimes.get(scopedRuntimeKey) === scopedRuntime) {
|
|
1280
|
+
mobileScopedRuntimes.delete(scopedRuntimeKey);
|
|
1281
|
+
}
|
|
1282
|
+
if (unsubscribeScoped)
|
|
1283
|
+
unsubscribeScoped();
|
|
1284
|
+
}
|
|
1285
|
+
const snapshot = requestAgent.getConversationSnapshot(conversationId, {
|
|
1286
|
+
window: 200,
|
|
1287
|
+
workspace: requestWorkspace,
|
|
1288
|
+
});
|
|
764
1289
|
mobileJson(res, {
|
|
765
1290
|
ok: true,
|
|
766
|
-
conversationId
|
|
1291
|
+
conversationId,
|
|
767
1292
|
response: tokens.map(token => token.text).join(''),
|
|
768
1293
|
tokens: tokens.map(token => ({ type: token.type, text: token.text })),
|
|
769
|
-
options:
|
|
770
|
-
status:
|
|
771
|
-
conversations:
|
|
1294
|
+
options: requestAgent.pendingOptions,
|
|
1295
|
+
status: requestAgent.status,
|
|
1296
|
+
conversations: requestWorkspace
|
|
1297
|
+
? mobileWorkspaceConversationRows(requestAgent, requestWorkspace)
|
|
1298
|
+
: requestAgent.listConversationStates(),
|
|
772
1299
|
chatMessages: snapshot.chatMessages,
|
|
773
1300
|
totalMessages: snapshot.totalMessages,
|
|
774
1301
|
});
|
|
775
1302
|
return;
|
|
776
1303
|
}
|
|
1304
|
+
case '/api/mobile/conversation-branch-inspect':
|
|
1305
|
+
case '/api/mobile/conversation-branch-activate':
|
|
1306
|
+
case '/api/mobile/conversation-branch-create': {
|
|
1307
|
+
const params = JSON.parse(body || '{}');
|
|
1308
|
+
const workspaceId = String(params.workspaceId || '');
|
|
1309
|
+
const conversationId = String(params.conversationId || '');
|
|
1310
|
+
if (!workspaceId || !conversationId) {
|
|
1311
|
+
mobileJson(res, { error: 'workspaceId and conversationId are required' }, 400);
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
const ws = resolveMobileWorkspace(agent, workspaceId);
|
|
1315
|
+
if (!ws) {
|
|
1316
|
+
mobileJson(res, { error: 'Unknown workspace' }, 404);
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
if (!agent.hasConversationInWorkspace(conversationId, ws)) {
|
|
1320
|
+
mobileJson(res, { error: 'Conversation not found in workspace' }, 404);
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
const scoped = mobileScopedAgent(ws, conversationId);
|
|
1324
|
+
if (pathname === '/api/mobile/conversation-branch-inspect') {
|
|
1325
|
+
const branchId = String(params.branchId || '');
|
|
1326
|
+
if (!branchId) {
|
|
1327
|
+
mobileJson(res, { error: 'branchId is required' }, 400);
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
mobileJson(res, scoped.inspectConversationBranch(conversationId, branchId, String(params.branchGroupId || '')));
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
if (mobileConversationRuntimeBusy(agent, ws, conversationId)) {
|
|
1334
|
+
mobileJson(res, { error: 'Conversation is running' }, 423);
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
if (pathname === '/api/mobile/conversation-branch-activate') {
|
|
1338
|
+
const branchId = String(params.branchId || '');
|
|
1339
|
+
if (!branchId) {
|
|
1340
|
+
mobileJson(res, { error: 'branchId is required' }, 400);
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
mobileJson(res, scoped.switchConversationBranch(conversationId, branchId, String(params.branchGroupId || '')));
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
const messageIndex = Math.floor(Number(params.messageIndex));
|
|
1347
|
+
const editedText = String(params.editedText || '').trim();
|
|
1348
|
+
if (!Number.isFinite(messageIndex) || messageIndex < 0 || !editedText) {
|
|
1349
|
+
mobileJson(res, { error: 'messageIndex and editedText are required' }, 400);
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
const branchNodePath = Array.isArray(params.branchNodePath)
|
|
1353
|
+
? params.branchNodePath.map((id) => String(id || '')).filter(Boolean)
|
|
1354
|
+
: [];
|
|
1355
|
+
const snapshot = scoped.branchConversation(conversationId, messageIndex, editedText, {
|
|
1356
|
+
messageId: String(params.messageId || '') || undefined,
|
|
1357
|
+
guideId: String(params.guideId || '') || undefined,
|
|
1358
|
+
clientMessageId: String(params.clientMessageId || '') || undefined,
|
|
1359
|
+
runId: String(params.runId || '') || undefined,
|
|
1360
|
+
branchNodePath,
|
|
1361
|
+
});
|
|
1362
|
+
mobileJson(res, snapshot);
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
case '/api/mobile/conversation-archive': {
|
|
1366
|
+
const params = JSON.parse(body || '{}');
|
|
1367
|
+
const conversationId = String(params.conversationId || '');
|
|
1368
|
+
const workspaceId = String(params.workspaceId || '');
|
|
1369
|
+
if (!workspaceId || !conversationId) {
|
|
1370
|
+
mobileJson(res, { error: 'workspaceId and conversationId are required' }, 400);
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
const ws = resolveMobileWorkspace(agent, workspaceId);
|
|
1374
|
+
if (!ws) {
|
|
1375
|
+
mobileJson(res, { error: 'Unknown workspace' }, 404);
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
if (!agent.hasConversationInWorkspace(conversationId, ws)) {
|
|
1379
|
+
mobileJson(res, { ok: false, error: 'Conversation not found in workspace' }, 404);
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
// 运行中拒绝:与移动端 running 判定一致(activeConversationId + agent.status 非 idle)
|
|
1383
|
+
if (mobileConversationRuntimeBusy(agent, ws, conversationId)) {
|
|
1384
|
+
mobileJson(res, { ok: false, error: 'Conversation is running' }, 423);
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
const filename = await agent.archiveConversationAsync(conversationId, ws);
|
|
1388
|
+
if (!filename) {
|
|
1389
|
+
mobileJson(res, { ok: false, error: 'Conversation archive could not be written.' }, 500);
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
mobileJson(res, {
|
|
1393
|
+
ok: true,
|
|
1394
|
+
fileName: filename,
|
|
1395
|
+
conversationId,
|
|
1396
|
+
activeConversationId: agent.activeConversationIdForWorkspace(ws),
|
|
1397
|
+
conversations: mobileWorkspaceConversationRows(agent, ws),
|
|
1398
|
+
});
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
777
1401
|
default:
|
|
778
1402
|
jsonResponse(res, { error: 'Unknown API' }, 404);
|
|
779
1403
|
}
|
|
@@ -785,39 +1409,51 @@ async function handleApi(req, res, body) {
|
|
|
785
1409
|
jsonResponse(res, { error: e.message }, 500);
|
|
786
1410
|
}
|
|
787
1411
|
}
|
|
788
|
-
function startServer(root) {
|
|
1412
|
+
function startServer(root, options = {}) {
|
|
789
1413
|
mobileToken = (0, mobilePairing_1.ensureMobileToken)(root);
|
|
790
1414
|
appRoot = root;
|
|
791
|
-
agent = new agent_1.Agent(root);
|
|
1415
|
+
agent = options.agent || new agent_1.Agent(root);
|
|
1416
|
+
hostedWorkEventSink = options.onWorkEvent || null;
|
|
1417
|
+
hostedConversationUiState = options.conversationUiState;
|
|
1418
|
+
hostedConversationUiAction = options.conversationUiAction;
|
|
1419
|
+
hostedConversationPrompt = options.conversationPrompt;
|
|
1420
|
+
unsubscribeHostedWorkEvents?.();
|
|
1421
|
+
unsubscribeHostedWorkEvents = options.subscribeWorkEvents?.(publishMobileWorkEvent) || null;
|
|
792
1422
|
workspaceFileRouter = new workspaceFileRouter_1.WorkspaceFileRouter(() => path.resolve(agent?.workspace.current?.path || root));
|
|
793
|
-
automation =
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
try {
|
|
808
|
-
const tokens = await agent.process(prompt);
|
|
809
|
-
return tokens.map(t => t.text).join('');
|
|
810
|
-
}
|
|
811
|
-
finally {
|
|
1423
|
+
automation = options.automation || null;
|
|
1424
|
+
if (!automation && !options.agent)
|
|
1425
|
+
automation = new automation_1.AutomationManager(agent.config, async (prompt, model, item) => {
|
|
1426
|
+
if (!agent)
|
|
1427
|
+
return '';
|
|
1428
|
+
const previousModel = agent.model;
|
|
1429
|
+
const previousWorkspace = agent.workspace.current?.id || agent.workspace.current?.path || '';
|
|
1430
|
+
const previousConversation = agent.activeConversationId;
|
|
1431
|
+
agent.selectWorkspaceFromStorage(item.workspaceId);
|
|
1432
|
+
const conversationId = item.conversationMode === 'existing'
|
|
1433
|
+
? item.conversationId
|
|
1434
|
+
: `automation-${item.id}-${Date.now().toString(36)}`;
|
|
1435
|
+
agent.setConversationFromStorage(conversationId);
|
|
1436
|
+
automation?.update(item.id, { lastConversationId: conversationId });
|
|
812
1437
|
if (model)
|
|
813
|
-
agent.setModel(
|
|
814
|
-
|
|
815
|
-
agent.
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
1438
|
+
agent.setModel(model);
|
|
1439
|
+
try {
|
|
1440
|
+
const tokens = await agent.process(prompt);
|
|
1441
|
+
return tokens.map(t => t.text).join('');
|
|
1442
|
+
}
|
|
1443
|
+
finally {
|
|
1444
|
+
if (model)
|
|
1445
|
+
agent.setModel(previousModel);
|
|
1446
|
+
if (previousWorkspace)
|
|
1447
|
+
agent.selectWorkspaceFromStorage(previousWorkspace);
|
|
1448
|
+
agent.setConversationFromStorage(previousConversation);
|
|
1449
|
+
}
|
|
1450
|
+
});
|
|
1451
|
+
agent.subscribeWorkEvents(publishServerWorkEvent);
|
|
1452
|
+
if (automation) {
|
|
1453
|
+
agent.setAutomationManager(automation);
|
|
1454
|
+
if (!options.automation)
|
|
1455
|
+
automation.start();
|
|
1456
|
+
}
|
|
821
1457
|
const uiDir = path.join(__dirname, 'ui');
|
|
822
1458
|
const server = http.createServer(async (req, res) => {
|
|
823
1459
|
if (req.method === 'OPTIONS') {
|
|
@@ -853,19 +1489,31 @@ function startServer(root) {
|
|
|
853
1489
|
});
|
|
854
1490
|
const bindHost = process.env.NEWMARK_BIND_HOST || '0.0.0.0';
|
|
855
1491
|
const tailscale = (0, mobilePairing_1.tailscaleIpv4)();
|
|
1492
|
+
const lan = (0, mobilePairing_1.lanIpv4)();
|
|
1493
|
+
const accessHost = tailscale || lan || '<lan-or-tailscale-ip>';
|
|
856
1494
|
const tokenPath = path.join(root, '.newmark-mobile-token');
|
|
857
1495
|
server.listen(PORT, bindHost, () => {
|
|
858
1496
|
console.log(`\n Newmark Agent v1.0 - Server Mode`);
|
|
859
1497
|
console.log(` Bind: ${bindHost}:${PORT}`);
|
|
860
1498
|
console.log(` GUI: http://localhost:${PORT}`);
|
|
861
|
-
console.log(` Tailscale IPv4: ${tailscale || 'not detected
|
|
862
|
-
console.log(`
|
|
1499
|
+
console.log(` Tailscale IPv4: ${tailscale || 'not detected'}`);
|
|
1500
|
+
console.log(` LAN IPv4: ${lan || 'not detected'}`);
|
|
1501
|
+
console.log(` Mobile endpoint: http://${accessHost}:${PORT}/api/mobile/hello?token=<token>`);
|
|
863
1502
|
console.log(` Mobile token file: ${tokenPath}`);
|
|
864
|
-
console.log(` Mobile events (SSE): http://${
|
|
1503
|
+
console.log(` Mobile events (SSE): http://${accessHost}:${PORT}/api/mobile/events?token=<token>`);
|
|
865
1504
|
console.log(` Press Ctrl+C to stop\n`);
|
|
866
1505
|
});
|
|
867
1506
|
}
|
|
868
|
-
|
|
869
|
-
|
|
1507
|
+
let hostedServerStarted = false;
|
|
1508
|
+
/** 托管启动 server(GUI/TUI 内嵌调用;幂等防重入,进程常驻即服务常驻) */
|
|
1509
|
+
function runServer(root, options = {}) {
|
|
1510
|
+
if (hostedServerStarted)
|
|
1511
|
+
return;
|
|
1512
|
+
hostedServerStarted = true;
|
|
1513
|
+
startServer(root, options);
|
|
1514
|
+
}
|
|
1515
|
+
/** Attach the GUI-owned automation manager after deferred startup. */
|
|
1516
|
+
function setHostedServerAutomation(manager) {
|
|
1517
|
+
automation = manager;
|
|
870
1518
|
}
|
|
871
1519
|
//# sourceMappingURL=server.js.map
|