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/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 +291 -149
- package/dist/core/agent.d.ts +38 -4
- package/dist/core/agent.js +231 -49
- package/dist/core/agentKernelRunner.d.ts +3 -0
- package/dist/core/agentKernelRunner.js +49 -83
- package/dist/core/config.js +1 -1
- 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 +11 -3
- package/dist/launcher.js +14 -11
- package/dist/main.js +15 -3
- package/dist/providers/chat-completions.adapter.js +6 -2
- package/dist/providers/responses.adapter.js +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +543 -17
- package/dist/toolchain/registry-seeder.js +3 -1
- package/dist/tools/index.js +11 -5
- package/dist/tools/nativeTools.js +1 -1
- package/dist/ui/index.html +88 -101
- package/dist/wsl-agent-host.bundle.cjs +291 -149
- package/package.json +3 -1
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
|
|
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
|
-
|
|
225
|
+
res.on('close', () => {
|
|
115
226
|
clearInterval(heartbeat);
|
|
116
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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
|
|
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:
|
|
770
|
-
status:
|
|
771
|
-
conversations:
|
|
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
|
|
862
|
-
console.log(`
|
|
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://${
|
|
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
|