newmark-agent 0.4.6 → 0.4.7

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/dist/server.js CHANGED
@@ -54,6 +54,19 @@ let automation = null;
54
54
  let workspaceFileRouter = null;
55
55
  let mobileToken = '';
56
56
  let appRoot = '';
57
+ const mobileWorkEventSubscribers = new Set();
58
+ const mobileScopedRuntimes = new Map();
59
+ function mobileRuntimeKey(workspaceId, conversationId) {
60
+ return `${String(workspaceId || '')}::${String(conversationId || '')}`;
61
+ }
62
+ function publishMobileWorkEvent(event) {
63
+ for (const subscriber of mobileWorkEventSubscribers) {
64
+ try {
65
+ subscriber(event);
66
+ }
67
+ catch { /* ignore disconnected mobile listeners */ }
68
+ }
69
+ }
57
70
  function mobileAuthorized(req) {
58
71
  if (!mobileToken)
59
72
  return false;
@@ -77,6 +90,103 @@ function mobileJson(res, data, code = 200) {
77
90
  });
78
91
  res.end(body);
79
92
  }
93
+ function resolveMobileWorkspace(current, workspaceId) {
94
+ const clean = String(workspaceId || '');
95
+ return [...current.workspace.internal, ...current.workspace.external].find(workspace => workspace.id === clean)
96
+ || (current.workspace.current?.id === clean ? current.workspace.current : null);
97
+ }
98
+ const MOBILE_EDITABLE_EXTENSIONS = new Set([
99
+ '.txt', '.md', '.markdown', '.json', '.jsonc', '.yaml', '.yml', '.toml', '.ini', '.cfg', '.conf',
100
+ '.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.css', '.scss', '.html', '.htm', '.xml', '.svg',
101
+ '.kt', '.kts', '.java', '.py', '.rb', '.rs', '.go', '.c', '.h', '.cpp', '.hpp', '.cs', '.sh',
102
+ '.bash', '.zsh', '.ps1', '.bat', '.cmd', '.sql', '.tex', '.typ', '.properties', '.gradle', '.gitignore',
103
+ ]);
104
+ const MOBILE_EDITOR_MAX_BYTES = 1024 * 1024;
105
+ function resolveMobileWorkspacePath(ws, relativePath) {
106
+ const root = path.resolve(ws.path);
107
+ const clean = String(relativePath || '').replace(/\\/g, '/').replace(/^\/+/, '');
108
+ const target = path.resolve(root, clean || '.');
109
+ if (target !== root && !target.startsWith(root + path.sep))
110
+ throw new Error('Path escapes workspace');
111
+ return { root, target, relative: path.relative(root, target).replace(/\\/g, '/') };
112
+ }
113
+ async function assertMobileRealPathContained(root, target) {
114
+ const realRoot = await fs.promises.realpath(root);
115
+ const realTarget = await fs.promises.realpath(target);
116
+ if (realTarget !== realRoot && !realTarget.startsWith(realRoot + path.sep))
117
+ throw new Error('Path escapes workspace through a symbolic link');
118
+ }
119
+ function mobileEditableFile(target) {
120
+ const base = path.basename(target).toLowerCase();
121
+ return MOBILE_EDITABLE_EXTENSIONS.has(path.extname(base)) || MOBILE_EDITABLE_EXTENSIONS.has(base);
122
+ }
123
+ function mobileRightSidebarState(current, ws, conversationId) {
124
+ const scoped = mobileScopedAgent(ws, conversationId);
125
+ return {
126
+ workspace: { id: ws.id, name: ws.name, path: ws.path, isInternal: ws.isInternal },
127
+ conversationId,
128
+ conversationPlan: scoped.getConversationPlan(conversationId),
129
+ linkedPlan: scoped.getLinkedPlan(conversationId),
130
+ subagents: scoped.subagents.listAll()
131
+ .filter(record => !record.conversationId || record.conversationId === conversationId)
132
+ .map(record => ({
133
+ id: record.id,
134
+ name: record.name,
135
+ displayName: record.displayName,
136
+ status: record.status,
137
+ model: record.model,
138
+ mode: record.agentMode,
139
+ inputMode: record.inputMode,
140
+ result: record.result,
141
+ error: record.error || '',
142
+ messageCount: record.messages.length,
143
+ messages: record.messages.slice(-40),
144
+ })),
145
+ };
146
+ }
147
+ function mobileWorkspaceConversationRows(current, ws) {
148
+ const activeConversationId = current.activeConversationIdForWorkspace(ws);
149
+ const isRuntimeWorkspace = !!current.workspace.current
150
+ && path.resolve(current.workspace.current.path) === path.resolve(ws.path);
151
+ const activeRuntimeStatus = current.status === 'working' ? 'running' : current.status;
152
+ return current.listWorkspaceConversationStates(ws).map(conversation => {
153
+ const scopedRuntime = mobileScopedRuntimes.get(mobileRuntimeKey(String(ws.id || ''), conversation.id));
154
+ const scopedStatus = scopedRuntime?.agent.status === 'working' ? 'running' : scopedRuntime?.agent.status || '';
155
+ const runtimeStatus = scopedRuntime
156
+ ? scopedStatus
157
+ : isRuntimeWorkspace && conversation.id === current.activeConversationId && activeRuntimeStatus !== 'idle'
158
+ ? activeRuntimeStatus
159
+ : '';
160
+ return {
161
+ ...conversation,
162
+ active: conversation.id === activeConversationId || !!scopedRuntime,
163
+ runtimeStatus,
164
+ running: ['running', 'stopping', 'force_restarting'].includes(runtimeStatus),
165
+ };
166
+ });
167
+ }
168
+ function mobileScopedAgent(ws, conversationId) {
169
+ const scoped = new agent_1.Agent(appRoot, {
170
+ agentOnly: true,
171
+ workspaceRegistryMode: 'detached',
172
+ readOnlyConfig: true,
173
+ conversationId,
174
+ });
175
+ scoped.workspace.current = { ...ws };
176
+ scoped.config.loadWorkspaceConfig(ws.path);
177
+ scoped.setConversationFromStorage(conversationId);
178
+ return scoped;
179
+ }
180
+ function mobileConversationRuntimeBusy(current, ws, conversationId) {
181
+ const scoped = mobileScopedRuntimes.get(mobileRuntimeKey(String(ws.id || ''), conversationId));
182
+ if (scoped && scoped.agent.status !== 'idle')
183
+ return true;
184
+ const currentWs = current.workspace.current;
185
+ return !!currentWs
186
+ && path.resolve(currentWs.path) === path.resolve(ws.path)
187
+ && current.activeConversationId === conversationId
188
+ && current.status !== 'idle';
189
+ }
80
190
  function handleMobileEvents(req, res) {
81
191
  if (agent && !agent.config.getBool('remote', 'touch_enabled')) {
82
192
  mobileJson(res, { error: 'Remote touch disabled' }, 403);
@@ -97,23 +207,24 @@ function handleMobileEvents(req, res) {
97
207
  'Access-Control-Allow-Origin': '*',
98
208
  });
99
209
  res.write('retry: 3000\n\n');
100
- const unsubscribe = agent.subscribeWorkEvents(event => {
210
+ const subscriber = (event) => {
101
211
  try {
102
212
  res.write(`event: work\ndata: ${JSON.stringify(event)}\n\n`);
103
213
  }
104
214
  catch {
105
215
  // socket is gone; the close handler will clean up
106
216
  }
107
- });
217
+ };
218
+ mobileWorkEventSubscribers.add(subscriber);
108
219
  const heartbeat = setInterval(() => {
109
220
  try {
110
221
  res.write(': ping\n\n');
111
222
  }
112
223
  catch { }
113
224
  }, 15000);
114
- req.on('close', () => {
225
+ res.on('close', () => {
115
226
  clearInterval(heartbeat);
116
- unsubscribe();
227
+ mobileWorkEventSubscribers.delete(subscriber);
117
228
  });
118
229
  }
119
230
  function resolveAppPath(root, targetPath) {
@@ -705,6 +816,7 @@ async function handleApi(req, res, body) {
705
816
  hostname: os.hostname(),
706
817
  platform: process.platform,
707
818
  tailscaleIpv4: (0, mobilePairing_1.tailscaleIpv4)(),
819
+ lanIpv4: (0, mobilePairing_1.lanIpv4)(),
708
820
  workspace: agent.workspace.current ? {
709
821
  id: agent.workspace.current.id,
710
822
  name: agent.workspace.current.name,
@@ -717,9 +829,14 @@ async function handleApi(req, res, body) {
717
829
  }
718
830
  case '/api/mobile/state': {
719
831
  const active = agent.getConversationSnapshot(agent.activeConversationId, { window: 200 });
832
+ // 完整对话信息:workRuns 用持久化记录透出(含被中断的构建,不依赖运行时内存)
833
+ const persistedRuns = agent.getPersistedConversationWorkRuns(agent.activeConversationId);
834
+ if (persistedRuns.length)
835
+ active.workRuns = persistedRuns;
720
836
  mobileJson(res, {
721
837
  mode: agent.mode,
722
838
  model: agent.modelSelectionValue(),
839
+ modelLabel: agent.modelLabel(),
723
840
  status: agent.status,
724
841
  activeConversationId: agent.activeConversationId,
725
842
  conversations: agent.listConversationStates(),
@@ -729,6 +846,11 @@ async function handleApi(req, res, body) {
729
846
  chatMessages: active.chatMessages,
730
847
  totalMessages: active.totalMessages,
731
848
  conversationLocked: agent.isConversationLocked(),
849
+ // workRuns 透出:完整 Build Block 记录(含 interrupted/force_interrupted),供移动端渲染被中断的对话
850
+ workRuns: active.workRuns,
851
+ // goal bar / flow bar:目标状态与当前 Flow 选择
852
+ goal: active.goal,
853
+ flowSelection: active.flowSelection,
732
854
  });
733
855
  return;
734
856
  }
@@ -737,12 +859,28 @@ async function handleApi(req, res, body) {
737
859
  return;
738
860
  }
739
861
  case '/api/mobile/conversation': {
740
- const conversationId = url.searchParams.get('conversationId') || agent.activeConversationId;
862
+ const workspaceId = url.searchParams.get('workspaceId') || '';
741
863
  const windowParam = url.searchParams.get('window');
742
864
  const beforeParam = url.searchParams.get('before');
743
865
  const windowSize = windowParam ? Math.max(1, Math.min(500, Number(windowParam) || 200)) : 200;
744
866
  const before = beforeParam ? Math.max(0, Number(beforeParam) || 0) : undefined;
745
- const snapshot = agent.getConversationSnapshot(String(conversationId), { window: windowSize, before });
867
+ const current = agent;
868
+ const ws = workspaceId ? resolveMobileWorkspace(current, workspaceId) : null;
869
+ if (workspaceId && !ws) {
870
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
871
+ return;
872
+ }
873
+ const conversationId = url.searchParams.get('conversationId')
874
+ || (ws ? current.activeConversationIdForWorkspace(ws) : current.activeConversationId);
875
+ if (ws && !current.hasConversationInWorkspace(String(conversationId), ws)) {
876
+ mobileJson(res, { error: 'Conversation not found in workspace' }, 404);
877
+ return;
878
+ }
879
+ const snapshot = current.getConversationSnapshot(String(conversationId), { window: windowSize, before, workspace: ws });
880
+ // 完整对话信息:workRuns 统一用持久化记录透出(含被中断的构建)
881
+ const persistedRuns = current.getPersistedConversationWorkRuns(String(conversationId), ws || undefined);
882
+ if (persistedRuns.length)
883
+ snapshot.workRuns = persistedRuns;
746
884
  mobileJson(res, snapshot);
747
885
  return;
748
886
  }
@@ -750,6 +888,229 @@ async function handleApi(req, res, body) {
750
888
  mobileJson(res, { internal: agent.workspace.internal, external: agent.workspace.external, current: agent.workspace.current });
751
889
  return;
752
890
  }
891
+ case '/api/mobile/workspace-conversations': {
892
+ const workspaceId = url.searchParams.get('workspaceId') || '';
893
+ const current = agent;
894
+ const ws = resolveMobileWorkspace(current, workspaceId);
895
+ if (!ws) {
896
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
897
+ return;
898
+ }
899
+ const conversations = mobileWorkspaceConversationRows(current, ws);
900
+ mobileJson(res, { workspace: { id: ws.id, name: ws.name, path: ws.path, isInternal: ws.isInternal }, conversations });
901
+ return;
902
+ }
903
+ case '/api/mobile/right-sidebar-state': {
904
+ const workspaceId = url.searchParams.get('workspaceId') || '';
905
+ const conversationId = url.searchParams.get('conversationId') || '';
906
+ const ws = resolveMobileWorkspace(agent, workspaceId);
907
+ if (!ws) {
908
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
909
+ return;
910
+ }
911
+ if (!conversationId || !agent.hasConversationInWorkspace(conversationId, ws)) {
912
+ mobileJson(res, { error: 'Conversation not found in workspace' }, 404);
913
+ return;
914
+ }
915
+ mobileJson(res, mobileRightSidebarState(agent, ws, conversationId));
916
+ return;
917
+ }
918
+ case '/api/mobile/workspace-files': {
919
+ const workspaceId = url.searchParams.get('workspaceId') || '';
920
+ const relativePath = url.searchParams.get('path') || '';
921
+ const ws = resolveMobileWorkspace(agent, workspaceId);
922
+ if (!ws) {
923
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
924
+ return;
925
+ }
926
+ const resolved = resolveMobileWorkspacePath(ws, relativePath);
927
+ await assertMobileRealPathContained(resolved.root, resolved.target);
928
+ const stat = await fs.promises.stat(resolved.target);
929
+ if (!stat.isDirectory()) {
930
+ mobileJson(res, { error: 'Path is not a directory' }, 400);
931
+ return;
932
+ }
933
+ const entries = await fs.promises.readdir(resolved.target, { withFileTypes: true });
934
+ const rows = entries
935
+ .filter(entry => entry.name !== '.git' && entry.name !== 'node_modules')
936
+ .map(entry => ({
937
+ name: entry.name,
938
+ path: path.posix.join(resolved.relative, entry.name).replace(/^\.\//, ''),
939
+ directory: entry.isDirectory(),
940
+ }))
941
+ .sort((a, b) => Number(b.directory) - Number(a.directory) || a.name.localeCompare(b.name));
942
+ mobileJson(res, { workspaceId, path: resolved.relative, entries: rows });
943
+ return;
944
+ }
945
+ case '/api/mobile/workspace-file': {
946
+ if (req.method === 'GET') {
947
+ const workspaceId = url.searchParams.get('workspaceId') || '';
948
+ const relativePath = url.searchParams.get('path') || '';
949
+ const ws = resolveMobileWorkspace(agent, workspaceId);
950
+ if (!ws) {
951
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
952
+ return;
953
+ }
954
+ const resolved = resolveMobileWorkspacePath(ws, relativePath);
955
+ await assertMobileRealPathContained(resolved.root, resolved.target);
956
+ const stat = await fs.promises.stat(resolved.target);
957
+ if (!stat.isFile() || stat.size > MOBILE_EDITOR_MAX_BYTES || !mobileEditableFile(resolved.target)) {
958
+ mobileJson(res, { error: 'File is not an editable text file' }, 415);
959
+ return;
960
+ }
961
+ const content = (await fs.promises.readFile(resolved.target, 'utf-8')).replace(/^\uFEFF/, '');
962
+ mobileJson(res, { workspaceId, path: resolved.relative, content, size: stat.size });
963
+ return;
964
+ }
965
+ const params = JSON.parse(body || '{}');
966
+ const workspaceId = String(params.workspaceId || '');
967
+ const relativePath = String(params.path || '');
968
+ const content = String(params.content ?? '');
969
+ const ws = resolveMobileWorkspace(agent, workspaceId);
970
+ if (!ws) {
971
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
972
+ return;
973
+ }
974
+ if (Buffer.byteLength(content, 'utf-8') > MOBILE_EDITOR_MAX_BYTES) {
975
+ mobileJson(res, { error: 'File exceeds mobile editor size limit' }, 413);
976
+ return;
977
+ }
978
+ const resolved = resolveMobileWorkspacePath(ws, relativePath);
979
+ await assertMobileRealPathContained(resolved.root, resolved.target);
980
+ if (!mobileEditableFile(resolved.target)) {
981
+ mobileJson(res, { error: 'File type is not editable' }, 415);
982
+ return;
983
+ }
984
+ const stat = await fs.promises.stat(resolved.target);
985
+ if (!stat.isFile()) {
986
+ mobileJson(res, { error: 'Path is not a file' }, 400);
987
+ return;
988
+ }
989
+ await fs.promises.writeFile(resolved.target, content, 'utf-8');
990
+ mobileJson(res, { ok: true, workspaceId, path: resolved.relative, size: Buffer.byteLength(content, 'utf-8') });
991
+ return;
992
+ }
993
+ case '/api/mobile/conversation-plan-update': {
994
+ const params = JSON.parse(body || '{}');
995
+ const workspaceId = String(params.workspaceId || '');
996
+ const conversationId = String(params.conversationId || '');
997
+ const ws = resolveMobileWorkspace(agent, workspaceId);
998
+ if (!ws) {
999
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
1000
+ return;
1001
+ }
1002
+ if (!conversationId || !agent.hasConversationInWorkspace(conversationId, ws)) {
1003
+ mobileJson(res, { error: 'Conversation not found in workspace' }, 404);
1004
+ return;
1005
+ }
1006
+ const scoped = mobileScopedAgent(ws, conversationId);
1007
+ const plan = scoped.updateConversationPlan({ items: Array.isArray(params.items) ? params.items : [] }, conversationId);
1008
+ mobileJson(res, { ok: true, conversationPlan: plan });
1009
+ return;
1010
+ }
1011
+ case '/api/mobile/conversation-create': {
1012
+ const params = JSON.parse(body || '{}');
1013
+ const workspaceId = String(params.workspaceId || '');
1014
+ if (!workspaceId) {
1015
+ mobileJson(res, { error: 'No workspaceId' }, 400);
1016
+ return;
1017
+ }
1018
+ const ws = resolveMobileWorkspace(agent, workspaceId);
1019
+ if (!ws) {
1020
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
1021
+ return;
1022
+ }
1023
+ const created = agent.createConversationInWorkspace(ws, String(params.title || ''));
1024
+ mobileJson(res, {
1025
+ ok: true,
1026
+ workspace: { id: ws.id, name: ws.name, path: ws.path, isInternal: ws.isInternal },
1027
+ conversation: created,
1028
+ activeConversationId: created.id,
1029
+ conversations: mobileWorkspaceConversationRows(agent, ws),
1030
+ });
1031
+ return;
1032
+ }
1033
+ case '/api/mobile/conversation-rename': {
1034
+ const params = JSON.parse(body || '{}');
1035
+ const workspaceId = String(params.workspaceId || '');
1036
+ const conversationId = String(params.conversationId || '');
1037
+ const title = String(params.title || '');
1038
+ if (!workspaceId || !conversationId || !title.trim()) {
1039
+ mobileJson(res, { error: 'workspaceId, conversationId, and title are required' }, 400);
1040
+ return;
1041
+ }
1042
+ const ws = resolveMobileWorkspace(agent, workspaceId);
1043
+ if (!ws) {
1044
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
1045
+ return;
1046
+ }
1047
+ const ok = agent.renameConversation(conversationId, title, ws);
1048
+ if (!ok) {
1049
+ mobileJson(res, { ok: false, error: 'Conversation rename failed' }, 404);
1050
+ return;
1051
+ }
1052
+ mobileJson(res, { ok: true, conversations: mobileWorkspaceConversationRows(agent, ws) });
1053
+ return;
1054
+ }
1055
+ case '/api/mobile/conversation-pin': {
1056
+ const params = JSON.parse(body || '{}');
1057
+ const workspaceId = String(params.workspaceId || '');
1058
+ const conversationId = String(params.conversationId || '');
1059
+ if (!workspaceId || !conversationId) {
1060
+ mobileJson(res, { error: 'workspaceId and conversationId are required' }, 400);
1061
+ return;
1062
+ }
1063
+ const ws = resolveMobileWorkspace(agent, workspaceId);
1064
+ if (!ws) {
1065
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
1066
+ return;
1067
+ }
1068
+ const pinned = params.pinned === true;
1069
+ const ok = agent.setConversationPinned(conversationId, pinned, ws);
1070
+ if (!ok) {
1071
+ mobileJson(res, { ok: false, error: 'Conversation pin update failed' }, 404);
1072
+ return;
1073
+ }
1074
+ mobileJson(res, { ok: true, pinned, conversations: mobileWorkspaceConversationRows(agent, ws) });
1075
+ return;
1076
+ }
1077
+ case '/api/mobile/conversation-reorder': {
1078
+ const params = JSON.parse(body || '{}');
1079
+ const workspaceId = String(params.workspaceId || '');
1080
+ const conversationIds = Array.isArray(params.conversationIds)
1081
+ ? params.conversationIds.map((id) => String(id || ''))
1082
+ : [];
1083
+ if (!workspaceId || conversationIds.length < 2 || conversationIds.some((id) => !id)) {
1084
+ mobileJson(res, { error: 'workspaceId and at least two conversationIds are required' }, 400);
1085
+ return;
1086
+ }
1087
+ if (new Set(conversationIds).size !== conversationIds.length) {
1088
+ mobileJson(res, { error: 'conversationIds must be unique' }, 400);
1089
+ return;
1090
+ }
1091
+ const ws = resolveMobileWorkspace(agent, workspaceId);
1092
+ if (!ws) {
1093
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
1094
+ return;
1095
+ }
1096
+ const current = agent;
1097
+ if (conversationIds.some((id) => !current.hasConversationInWorkspace(id, ws))) {
1098
+ mobileJson(res, { error: 'Conversation not found in workspace' }, 404);
1099
+ return;
1100
+ }
1101
+ const listedById = new Map(current.listWorkspaceConversationStates(ws).map(item => [item.id, item]));
1102
+ if (new Set(conversationIds.map((id) => !!listedById.get(id)?.pinned)).size !== 1) {
1103
+ mobileJson(res, { error: 'Conversations must remain within one pinned group' }, 409);
1104
+ return;
1105
+ }
1106
+ const ok = current.reorderWorkspaceConversationGroup(conversationIds, ws);
1107
+ if (!ok) {
1108
+ mobileJson(res, { ok: false, error: 'Conversation reorder failed' }, 409);
1109
+ return;
1110
+ }
1111
+ mobileJson(res, { ok: true, conversations: mobileWorkspaceConversationRows(current, ws) });
1112
+ return;
1113
+ }
753
1114
  case '/api/mobile/send': {
754
1115
  const params = JSON.parse(body || '{}');
755
1116
  const message = String(params.message || '');
@@ -757,23 +1118,179 @@ async function handleApi(req, res, body) {
757
1118
  mobileJson(res, { error: 'No message' }, 400);
758
1119
  return;
759
1120
  }
760
- if (params.conversationId)
761
- agent.setConversation(String(params.conversationId));
762
- const tokens = await agent.process(message);
763
- const snapshot = agent.getConversationSnapshot(agent.activeConversationId, { window: 200 });
1121
+ const workspaceId = String(params.workspaceId || '');
1122
+ let requestAgent = agent;
1123
+ let requestWorkspace = agent.workspace.current;
1124
+ let resolvedWorkspace = null;
1125
+ if (workspaceId) {
1126
+ const ws = resolveMobileWorkspace(agent, workspaceId);
1127
+ if (!ws) {
1128
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
1129
+ return;
1130
+ }
1131
+ resolvedWorkspace = ws;
1132
+ requestWorkspace = ws;
1133
+ }
1134
+ const conversationId = String(params.conversationId
1135
+ || (resolvedWorkspace ? agent.activeConversationIdForWorkspace(resolvedWorkspace) : agent.activeConversationId));
1136
+ if (resolvedWorkspace) {
1137
+ const ws = resolvedWorkspace;
1138
+ if (!agent.hasConversationInWorkspace(conversationId, ws)) {
1139
+ mobileJson(res, { error: 'Conversation not found in workspace' }, 404);
1140
+ return;
1141
+ }
1142
+ const currentWs = agent.workspace.current;
1143
+ if (!currentWs || path.resolve(currentWs.path) !== path.resolve(ws.path)) {
1144
+ requestAgent = mobileScopedAgent(ws, conversationId);
1145
+ }
1146
+ }
1147
+ if (requestAgent === agent && params.conversationId)
1148
+ requestAgent.setConversation(conversationId);
1149
+ let scopedRuntimeKey = '';
1150
+ let scopedRuntime = null;
1151
+ let unsubscribeScoped = null;
1152
+ if (requestAgent !== agent && requestWorkspace) {
1153
+ scopedRuntimeKey = mobileRuntimeKey(String(requestWorkspace.id || ''), conversationId);
1154
+ unsubscribeScoped = requestAgent.subscribeWorkEvents(event => publishMobileWorkEvent({
1155
+ ...event,
1156
+ workspaceId: String(requestWorkspace?.id || ''),
1157
+ conversationId,
1158
+ }));
1159
+ scopedRuntime = {
1160
+ agent: requestAgent,
1161
+ workspaceId: String(requestWorkspace.id || ''),
1162
+ conversationId,
1163
+ unsubscribe: unsubscribeScoped,
1164
+ };
1165
+ mobileScopedRuntimes.set(scopedRuntimeKey, scopedRuntime);
1166
+ }
1167
+ let tokens;
1168
+ try {
1169
+ tokens = await requestAgent.process(message);
1170
+ }
1171
+ finally {
1172
+ if (scopedRuntimeKey && mobileScopedRuntimes.get(scopedRuntimeKey) === scopedRuntime) {
1173
+ mobileScopedRuntimes.delete(scopedRuntimeKey);
1174
+ }
1175
+ if (unsubscribeScoped)
1176
+ unsubscribeScoped();
1177
+ }
1178
+ const snapshot = requestAgent.getConversationSnapshot(conversationId, {
1179
+ window: 200,
1180
+ workspace: requestWorkspace,
1181
+ });
764
1182
  mobileJson(res, {
765
1183
  ok: true,
766
- conversationId: agent.activeConversationId,
1184
+ conversationId,
767
1185
  response: tokens.map(token => token.text).join(''),
768
1186
  tokens: tokens.map(token => ({ type: token.type, text: token.text })),
769
- options: agent.pendingOptions,
770
- status: agent.status,
771
- conversations: agent.listConversationStates(),
1187
+ options: requestAgent.pendingOptions,
1188
+ status: requestAgent.status,
1189
+ conversations: requestWorkspace
1190
+ ? mobileWorkspaceConversationRows(requestAgent, requestWorkspace)
1191
+ : requestAgent.listConversationStates(),
772
1192
  chatMessages: snapshot.chatMessages,
773
1193
  totalMessages: snapshot.totalMessages,
774
1194
  });
775
1195
  return;
776
1196
  }
1197
+ case '/api/mobile/conversation-branch-inspect':
1198
+ case '/api/mobile/conversation-branch-activate':
1199
+ case '/api/mobile/conversation-branch-create': {
1200
+ const params = JSON.parse(body || '{}');
1201
+ const workspaceId = String(params.workspaceId || '');
1202
+ const conversationId = String(params.conversationId || '');
1203
+ if (!workspaceId || !conversationId) {
1204
+ mobileJson(res, { error: 'workspaceId and conversationId are required' }, 400);
1205
+ return;
1206
+ }
1207
+ const ws = resolveMobileWorkspace(agent, workspaceId);
1208
+ if (!ws) {
1209
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
1210
+ return;
1211
+ }
1212
+ if (!agent.hasConversationInWorkspace(conversationId, ws)) {
1213
+ mobileJson(res, { error: 'Conversation not found in workspace' }, 404);
1214
+ return;
1215
+ }
1216
+ const scoped = mobileScopedAgent(ws, conversationId);
1217
+ if (pathname === '/api/mobile/conversation-branch-inspect') {
1218
+ const branchId = String(params.branchId || '');
1219
+ if (!branchId) {
1220
+ mobileJson(res, { error: 'branchId is required' }, 400);
1221
+ return;
1222
+ }
1223
+ mobileJson(res, scoped.inspectConversationBranch(conversationId, branchId, String(params.branchGroupId || '')));
1224
+ return;
1225
+ }
1226
+ if (mobileConversationRuntimeBusy(agent, ws, conversationId)) {
1227
+ mobileJson(res, { error: 'Conversation is running' }, 423);
1228
+ return;
1229
+ }
1230
+ if (pathname === '/api/mobile/conversation-branch-activate') {
1231
+ const branchId = String(params.branchId || '');
1232
+ if (!branchId) {
1233
+ mobileJson(res, { error: 'branchId is required' }, 400);
1234
+ return;
1235
+ }
1236
+ mobileJson(res, scoped.switchConversationBranch(conversationId, branchId, String(params.branchGroupId || '')));
1237
+ return;
1238
+ }
1239
+ const messageIndex = Math.floor(Number(params.messageIndex));
1240
+ const editedText = String(params.editedText || '').trim();
1241
+ if (!Number.isFinite(messageIndex) || messageIndex < 0 || !editedText) {
1242
+ mobileJson(res, { error: 'messageIndex and editedText are required' }, 400);
1243
+ return;
1244
+ }
1245
+ const branchNodePath = Array.isArray(params.branchNodePath)
1246
+ ? params.branchNodePath.map((id) => String(id || '')).filter(Boolean)
1247
+ : [];
1248
+ const snapshot = scoped.branchConversation(conversationId, messageIndex, editedText, {
1249
+ messageId: String(params.messageId || '') || undefined,
1250
+ guideId: String(params.guideId || '') || undefined,
1251
+ clientMessageId: String(params.clientMessageId || '') || undefined,
1252
+ runId: String(params.runId || '') || undefined,
1253
+ branchNodePath,
1254
+ });
1255
+ mobileJson(res, snapshot);
1256
+ return;
1257
+ }
1258
+ case '/api/mobile/conversation-archive': {
1259
+ const params = JSON.parse(body || '{}');
1260
+ const conversationId = String(params.conversationId || '');
1261
+ const workspaceId = String(params.workspaceId || '');
1262
+ if (!workspaceId || !conversationId) {
1263
+ mobileJson(res, { error: 'workspaceId and conversationId are required' }, 400);
1264
+ return;
1265
+ }
1266
+ const ws = resolveMobileWorkspace(agent, workspaceId);
1267
+ if (!ws) {
1268
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
1269
+ return;
1270
+ }
1271
+ if (!agent.hasConversationInWorkspace(conversationId, ws)) {
1272
+ mobileJson(res, { ok: false, error: 'Conversation not found in workspace' }, 404);
1273
+ return;
1274
+ }
1275
+ // 运行中拒绝:与移动端 running 判定一致(activeConversationId + agent.status 非 idle)
1276
+ if (mobileConversationRuntimeBusy(agent, ws, conversationId)) {
1277
+ mobileJson(res, { ok: false, error: 'Conversation is running' }, 423);
1278
+ return;
1279
+ }
1280
+ const filename = await agent.archiveConversationAsync(conversationId, ws);
1281
+ if (!filename) {
1282
+ mobileJson(res, { ok: false, error: 'Conversation archive could not be written.' }, 500);
1283
+ return;
1284
+ }
1285
+ mobileJson(res, {
1286
+ ok: true,
1287
+ fileName: filename,
1288
+ conversationId,
1289
+ activeConversationId: agent.activeConversationIdForWorkspace(ws),
1290
+ conversations: mobileWorkspaceConversationRows(agent, ws),
1291
+ });
1292
+ return;
1293
+ }
777
1294
  default:
778
1295
  jsonResponse(res, { error: 'Unknown API' }, 404);
779
1296
  }
@@ -816,6 +1333,7 @@ function startServer(root) {
816
1333
  agent.setConversationFromStorage(previousConversation);
817
1334
  }
818
1335
  });
1336
+ agent.subscribeWorkEvents(publishMobileWorkEvent);
819
1337
  agent.setAutomationManager(automation);
820
1338
  automation.start();
821
1339
  const uiDir = path.join(__dirname, 'ui');
@@ -853,19 +1371,27 @@ function startServer(root) {
853
1371
  });
854
1372
  const bindHost = process.env.NEWMARK_BIND_HOST || '0.0.0.0';
855
1373
  const tailscale = (0, mobilePairing_1.tailscaleIpv4)();
1374
+ const lan = (0, mobilePairing_1.lanIpv4)();
1375
+ const accessHost = tailscale || lan || '<lan-or-tailscale-ip>';
856
1376
  const tokenPath = path.join(root, '.newmark-mobile-token');
857
1377
  server.listen(PORT, bindHost, () => {
858
1378
  console.log(`\n Newmark Agent v1.0 - Server Mode`);
859
1379
  console.log(` Bind: ${bindHost}:${PORT}`);
860
1380
  console.log(` GUI: http://localhost:${PORT}`);
861
- console.log(` Tailscale IPv4: ${tailscale || 'not detected (install/start Tailscale)'}`);
862
- console.log(` Mobile endpoint: http://${tailscale || '<tailscale-ip>'}:${PORT}/api/mobile/hello?token=<token>`);
1381
+ console.log(` Tailscale IPv4: ${tailscale || 'not detected'}`);
1382
+ console.log(` LAN IPv4: ${lan || 'not detected'}`);
1383
+ console.log(` Mobile endpoint: http://${accessHost}:${PORT}/api/mobile/hello?token=<token>`);
863
1384
  console.log(` Mobile token file: ${tokenPath}`);
864
- console.log(` Mobile events (SSE): http://${tailscale || '<tailscale-ip>'}:${PORT}/api/mobile/events?token=<token>`);
1385
+ console.log(` Mobile events (SSE): http://${accessHost}:${PORT}/api/mobile/events?token=<token>`);
865
1386
  console.log(` Press Ctrl+C to stop\n`);
866
1387
  });
867
1388
  }
1389
+ let hostedServerStarted = false;
1390
+ /** 托管启动 server(GUI/TUI 内嵌调用;幂等防重入,进程常驻即服务常驻) */
868
1391
  function runServer(root) {
1392
+ if (hostedServerStarted)
1393
+ return;
1394
+ hostedServerStarted = true;
869
1395
  startServer(root);
870
1396
  }
871
1397
  //# sourceMappingURL=server.js.map