dsh-monkeyai-sessions 0.1.0 → 0.1.2
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/README.md +3 -1
- package/README.zh.md +96 -0
- package/client.js +136 -136
- package/lib/client-runtime.cjs +136 -136
- package/package.json +4 -3
- package/src/client/index.js +119 -119
- package/src/host/monkeyai-remote.js +76 -25
- package/src/shared/projection.mjs +10 -10
|
@@ -390,6 +390,45 @@ export class MonkeyAIRemoteService extends TypertRemoteService {
|
|
|
390
390
|
return this.exchangeAuthorization(auth, callbackUrl)
|
|
391
391
|
}
|
|
392
392
|
|
|
393
|
+
// The runtime requires an expectedRevision on every configuration write and
|
|
394
|
+
// rejects a stale one with configuration_conflict. The MonkeyAI client (or
|
|
395
|
+
// another panel) can advance the revision at any time, so read the current
|
|
396
|
+
// value from the conflict payload — or from the HTTP session state when we
|
|
397
|
+
// have none — and retry instead of failing the switch.
|
|
398
|
+
async setSessionModel({ acp, sessionId, model, httpBase, ticket, revision, signal, onRevision }) {
|
|
399
|
+
let expected = revision
|
|
400
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
401
|
+
if (!Number.isSafeInteger(expected)) {
|
|
402
|
+
expected = await readSessionConfigurationRevision({ httpBase, ticket, sessionId, signal })
|
|
403
|
+
}
|
|
404
|
+
if (!Number.isSafeInteger(expected)) throw new Error('无法读取会话配置版本,暂时不能切换模型')
|
|
405
|
+
try {
|
|
406
|
+
const result = await acp.request('_monkeyai/session/model/set', {
|
|
407
|
+
sessionId,
|
|
408
|
+
model,
|
|
409
|
+
_meta: { expectedRevision: expected },
|
|
410
|
+
}, { signal, timeoutMs: 120_000 })
|
|
411
|
+
const next = result?._meta?.configurationRevision
|
|
412
|
+
if (Number.isSafeInteger(next)) onRevision(next)
|
|
413
|
+
return typeof result?.model === 'string' ? result.model : model
|
|
414
|
+
} catch (error) {
|
|
415
|
+
if (signal?.aborted) throw error
|
|
416
|
+
const current = error?.data?.configurationRevision
|
|
417
|
+
if (error?.data?.reason === 'configuration_conflict' && Number.isSafeInteger(current) && current !== expected) {
|
|
418
|
+
expected = current
|
|
419
|
+
onRevision(current)
|
|
420
|
+
continue
|
|
421
|
+
}
|
|
422
|
+
if (error?.data?.reason === 'configuration_version_required') {
|
|
423
|
+
expected = undefined
|
|
424
|
+
continue
|
|
425
|
+
}
|
|
426
|
+
throw error
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
throw new Error('会话配置持续被其他客户端修改,请重试切换模型')
|
|
430
|
+
}
|
|
431
|
+
|
|
393
432
|
async exchangeAuthorization(auth, callbackUrl) {
|
|
394
433
|
const callback = parseAuthorizationCallback(callbackUrl, auth)
|
|
395
434
|
const tokens = await requestJson(auth.serverOrigin, '/oauth/token', {
|
|
@@ -620,7 +659,7 @@ export class MonkeyAIRemoteService extends TypertRemoteService {
|
|
|
620
659
|
}
|
|
621
660
|
return {
|
|
622
661
|
switched: false,
|
|
623
|
-
message: '
|
|
662
|
+
message: '该 MonkeyAI runtime 不支持在活跃会话上切换模型,请新建会话使用其他模型。',
|
|
624
663
|
}
|
|
625
664
|
} finally {
|
|
626
665
|
acp.close()
|
|
@@ -665,7 +704,7 @@ export class MonkeyAIRemoteService extends TypertRemoteService {
|
|
|
665
704
|
// oversized single frame; both end the stream.
|
|
666
705
|
const pushed = events.push(frame)
|
|
667
706
|
if (pushed === false) {
|
|
668
|
-
const reason = new Error('
|
|
707
|
+
const reason = new Error('远程会话事件队列超出上限')
|
|
669
708
|
controller.abort(reason)
|
|
670
709
|
currentAcp?.close(reason)
|
|
671
710
|
return false
|
|
@@ -793,7 +832,7 @@ export class MonkeyAIRemoteService extends TypertRemoteService {
|
|
|
793
832
|
return
|
|
794
833
|
}
|
|
795
834
|
reconnectsUsed += 1
|
|
796
|
-
emit({ type: 'reconnecting', sessionId: selectedSession, message:
|
|
835
|
+
emit({ type: 'reconnecting', sessionId: selectedSession, message: `历史回放中断,正在重连以继续加载(${reconnectsUsed}/${MAX_REPLAY_RECONNECTS})` })
|
|
797
836
|
void (async () => {
|
|
798
837
|
try {
|
|
799
838
|
await establishAcp(true)
|
|
@@ -802,7 +841,7 @@ export class MonkeyAIRemoteService extends TypertRemoteService {
|
|
|
802
841
|
type: 'error',
|
|
803
842
|
sessionId: selectedSession,
|
|
804
843
|
code: 'acp_connection_closed',
|
|
805
|
-
message: reconnectError instanceof Error ? reconnectError.message : '
|
|
844
|
+
message: reconnectError instanceof Error ? reconnectError.message : '远程会话连接已关闭',
|
|
806
845
|
})
|
|
807
846
|
controller.abort(reason)
|
|
808
847
|
events.close()
|
|
@@ -814,7 +853,7 @@ export class MonkeyAIRemoteService extends TypertRemoteService {
|
|
|
814
853
|
// keep working (the runtime binds sessions per connection).
|
|
815
854
|
if (loadCompleted && reconnectsUsed < MAX_REPLAY_RECONNECTS) {
|
|
816
855
|
reconnectsUsed += 1
|
|
817
|
-
emit({ type: 'reconnecting', sessionId: selectedSession, message: '
|
|
856
|
+
emit({ type: 'reconnecting', sessionId: selectedSession, message: '远程连接中断,正在重连' })
|
|
818
857
|
void (async () => {
|
|
819
858
|
try {
|
|
820
859
|
await establishAcp(true)
|
|
@@ -824,7 +863,7 @@ export class MonkeyAIRemoteService extends TypertRemoteService {
|
|
|
824
863
|
type: 'error',
|
|
825
864
|
sessionId: selectedSession,
|
|
826
865
|
code: 'acp_connection_closed',
|
|
827
|
-
message: reconnectError instanceof Error ? reconnectError.message : '
|
|
866
|
+
message: reconnectError instanceof Error ? reconnectError.message : '远程会话连接已关闭',
|
|
828
867
|
})
|
|
829
868
|
controller.abort(reason)
|
|
830
869
|
events.close()
|
|
@@ -836,14 +875,14 @@ export class MonkeyAIRemoteService extends TypertRemoteService {
|
|
|
836
875
|
type: 'error',
|
|
837
876
|
sessionId: selectedSession,
|
|
838
877
|
code: 'acp_connection_closed',
|
|
839
|
-
message: '
|
|
878
|
+
message: '远程会话连接已关闭',
|
|
840
879
|
})
|
|
841
880
|
controller.abort(reason)
|
|
842
881
|
events.close()
|
|
843
882
|
})
|
|
844
883
|
unsubscribe = () => { unsubscribeThis(); unsubscribe = () => {} }
|
|
845
884
|
unsubscribeClose = () => { unsubscribeCloseThis(); unsubscribeClose = () => {} }
|
|
846
|
-
if (signal.aborted) throw signal.reason ?? new Error('
|
|
885
|
+
if (signal.aborted) throw signal.reason ?? new Error('远程会话已取消')
|
|
847
886
|
|
|
848
887
|
const initialized = await acp.request('initialize', {
|
|
849
888
|
protocolVersion: 1,
|
|
@@ -883,7 +922,7 @@ export class MonkeyAIRemoteService extends TypertRemoteService {
|
|
|
883
922
|
type: 'session_missing',
|
|
884
923
|
sessionId: selectedSession,
|
|
885
924
|
code: 'session_not_found',
|
|
886
|
-
message: '
|
|
925
|
+
message: '该会话在所选设备上已不存在(runtime 可能已重置),请新建会话。',
|
|
887
926
|
})
|
|
888
927
|
connection.sessionMissing = true
|
|
889
928
|
return
|
|
@@ -902,7 +941,7 @@ export class MonkeyAIRemoteService extends TypertRemoteService {
|
|
|
902
941
|
type: 'history_incomplete',
|
|
903
942
|
sessionId: selectedSession,
|
|
904
943
|
code: 'history_load_failed',
|
|
905
|
-
message: '
|
|
944
|
+
message: '历史回放被远程 runtime 反复中断,仅显示已收到的消息。',
|
|
906
945
|
})
|
|
907
946
|
loadCompleted = true
|
|
908
947
|
}
|
|
@@ -914,12 +953,12 @@ export class MonkeyAIRemoteService extends TypertRemoteService {
|
|
|
914
953
|
grantRef = await this.getGrant(accountId)
|
|
915
954
|
this.requireScope(grantRef, 'device:connect')
|
|
916
955
|
this.requireScope(grantRef, 'session:read')
|
|
917
|
-
if (signal.aborted) throw signal.reason ?? new Error('
|
|
956
|
+
if (signal.aborted) throw signal.reason ?? new Error('远程会话已取消')
|
|
918
957
|
|
|
919
958
|
const token = await this.refreshAccessToken(accountId, signal)
|
|
920
959
|
const ticket = await this.requestConnectionTicket(accountId, grantRef, token, target, { signal })
|
|
921
960
|
ticketValue = ticket.ticket
|
|
922
|
-
if (signal.aborted) throw signal.reason ?? new Error('
|
|
961
|
+
if (signal.aborted) throw signal.reason ?? new Error('远程会话已取消')
|
|
923
962
|
|
|
924
963
|
// Load the full history over the runtime's HTTP read channel before the
|
|
925
964
|
// ACP connection starts. The ACP session/load replay only streams a
|
|
@@ -959,7 +998,7 @@ export class MonkeyAIRemoteService extends TypertRemoteService {
|
|
|
959
998
|
try {
|
|
960
999
|
const currentGrant = await this.getGrant(accountId)
|
|
961
1000
|
if (!currentGrant.token.scopes.includes('session:read')) {
|
|
962
|
-
emit({ type: 'error', sessionId: selectedSession, code: 'read_permission_revoked', message: '
|
|
1001
|
+
emit({ type: 'error', sessionId: selectedSession, code: 'read_permission_revoked', message: '会话读取权限已失效' })
|
|
963
1002
|
controller.abort(new Error('Session read permission was revoked'))
|
|
964
1003
|
currentAcp?.close(new Error('Session read permission was revoked'))
|
|
965
1004
|
events.close()
|
|
@@ -973,18 +1012,20 @@ export class MonkeyAIRemoteService extends TypertRemoteService {
|
|
|
973
1012
|
continue
|
|
974
1013
|
}
|
|
975
1014
|
|
|
976
|
-
if (!currentAcp) throw new Error('ACP
|
|
1015
|
+
if (!currentAcp) throw new Error('ACP 连接正在重建')
|
|
977
1016
|
|
|
978
1017
|
if (command.type === 'switch_model') {
|
|
979
1018
|
try {
|
|
980
|
-
const
|
|
1019
|
+
const model = await this.setSessionModel({
|
|
1020
|
+
acp: currentAcp,
|
|
981
1021
|
sessionId: selectedSession,
|
|
982
1022
|
model: command.model,
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1023
|
+
httpBase: ticket.httpBase,
|
|
1024
|
+
ticket: ticketValue,
|
|
1025
|
+
revision: httpConfigurationRevision,
|
|
1026
|
+
signal,
|
|
1027
|
+
onRevision: value => { httpConfigurationRevision = value },
|
|
1028
|
+
})
|
|
988
1029
|
currentLoadedModel = model
|
|
989
1030
|
emit({ type: 'model_switched', sessionId: selectedSession, data: { model } })
|
|
990
1031
|
} catch (switchError) {
|
|
@@ -1017,7 +1058,7 @@ export class MonkeyAIRemoteService extends TypertRemoteService {
|
|
|
1017
1058
|
type: 'delivery',
|
|
1018
1059
|
sessionId: selectedSession,
|
|
1019
1060
|
data: { accepted: false, code: 'remote_rejected' },
|
|
1020
|
-
message: promptError instanceof Error ? promptError.message : '
|
|
1061
|
+
message: promptError instanceof Error ? promptError.message : '远程 runtime 拒绝了该指令',
|
|
1021
1062
|
})
|
|
1022
1063
|
}
|
|
1023
1064
|
continue
|
|
@@ -1043,7 +1084,7 @@ export class MonkeyAIRemoteService extends TypertRemoteService {
|
|
|
1043
1084
|
continue
|
|
1044
1085
|
}
|
|
1045
1086
|
|
|
1046
|
-
emit({ type: 'error', sessionId: selectedSession, code: 'invalid_command', message: '
|
|
1087
|
+
emit({ type: 'error', sessionId: selectedSession, code: 'invalid_command', message: '不支持该远程命令' })
|
|
1047
1088
|
} catch (error) {
|
|
1048
1089
|
if (signal.aborted) return
|
|
1049
1090
|
const requestId = command.type === 'respond' ? command.requestId : undefined
|
|
@@ -1061,9 +1102,9 @@ export class MonkeyAIRemoteService extends TypertRemoteService {
|
|
|
1061
1102
|
})()
|
|
1062
1103
|
commandPump.catch(() => {
|
|
1063
1104
|
if (signal.aborted) return
|
|
1064
|
-
emit({ type: 'error', sessionId: selectedSession, code: 'command_channel_closed', message: '
|
|
1065
|
-
controller.abort(new Error('
|
|
1066
|
-
currentAcp?.close(new Error('
|
|
1105
|
+
emit({ type: 'error', sessionId: selectedSession, code: 'command_channel_closed', message: '远程命令通道已关闭' })
|
|
1106
|
+
controller.abort(new Error('远程命令通道已关闭'))
|
|
1107
|
+
currentAcp?.close(new Error('远程命令通道已关闭'))
|
|
1067
1108
|
events.close()
|
|
1068
1109
|
})
|
|
1069
1110
|
|
|
@@ -1223,6 +1264,16 @@ async function readHistoryBodyText(response, maxBytes = HISTORY_PAGE_MAX_BYTES)
|
|
|
1223
1264
|
return new TextDecoder().decode(joined)
|
|
1224
1265
|
}
|
|
1225
1266
|
|
|
1267
|
+
// Reads the session's current configuration revision over the HTTP read
|
|
1268
|
+
// channel. Configuration writes must carry it, and the value changes whenever
|
|
1269
|
+
// any client edits the session.
|
|
1270
|
+
async function readSessionConfigurationRevision({ httpBase, ticket, sessionId, signal }) {
|
|
1271
|
+
const timeout = AbortSignal.any([AbortSignal.timeout(20_000), ...(signal ? [signal] : [])])
|
|
1272
|
+
const result = await readSessionHistoryPage({ httpBase, ticket, sessionId, limit: 1, signal: timeout })
|
|
1273
|
+
const revision = result.session?.configurationRevision
|
|
1274
|
+
return Number.isSafeInteger(revision) ? revision : undefined
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1226
1277
|
async function readSessionHistoryPage({ httpBase, ticket, sessionId, cursor, direction, limit = 50, signal }) {
|
|
1227
1278
|
const params = { sessionId, limit, ...(cursor ? { cursor, direction } : {}) }
|
|
1228
1279
|
const response = await fetch(`${httpBase.endsWith('/') ? httpBase : `${httpBase}/`}read/session`, {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const MESSAGE_UPDATES = Object.freeze({
|
|
2
|
-
user_message_chunk: { role: 'user', label: '
|
|
2
|
+
user_message_chunk: { role: 'user', label: '我', mode: 'append' },
|
|
3
3
|
agent_message_chunk: { role: 'assistant', label: 'MonkeyAI', mode: 'append' },
|
|
4
|
-
user_message: { role: 'user', label: '
|
|
4
|
+
user_message: { role: 'user', label: '我', mode: 'replace' },
|
|
5
5
|
agent_message: { role: 'assistant', label: 'MonkeyAI', mode: 'replace' },
|
|
6
6
|
})
|
|
7
7
|
|
|
@@ -11,7 +11,7 @@ const THOUGHT_UPDATES = Object.freeze({
|
|
|
11
11
|
})
|
|
12
12
|
|
|
13
13
|
export const TOOL_STATUS_LABEL = Object.freeze({
|
|
14
|
-
pending: '
|
|
14
|
+
pending: '排队中', in_progress: '执行中', completed: '完成', failed: '失败', cancelled: '已取消',
|
|
15
15
|
})
|
|
16
16
|
|
|
17
17
|
function contentText(content) {
|
|
@@ -124,20 +124,20 @@ function projectFrames(frames) {
|
|
|
124
124
|
entries.push({ kind: 'raw', label: kind || 'session/update', data: update, key: `raw:${entries.length}` })
|
|
125
125
|
continue
|
|
126
126
|
}
|
|
127
|
-
if (frame.type === 'ready') { if (!seenReady) { seenReady = true; entries.push({ kind: 'system', label: '
|
|
128
|
-
if (frame.type === 'history_loaded') { if (!seenHistoryLoaded) { seenHistoryLoaded = true; entries.push({ kind: 'system', label: '
|
|
129
|
-
if (frame.type === 'history_incomplete') { entries.push({ kind: 'system', label: '
|
|
130
|
-
if (frame.type === 'reconnecting') { if (!seenReconnecting) { seenReconnecting = true; entries.push({ kind: 'system', label: frame.message || '
|
|
127
|
+
if (frame.type === 'ready') { if (!seenReady) { seenReady = true; entries.push({ kind: 'system', label: '已连接到远程 runtime', key: `sys-ready:${entries.length}` }) } continue }
|
|
128
|
+
if (frame.type === 'history_loaded') { if (!seenHistoryLoaded) { seenHistoryLoaded = true; entries.push({ kind: 'system', label: '历史已加载', key: `sys-hist:${entries.length}` }) } continue }
|
|
129
|
+
if (frame.type === 'history_incomplete') { entries.push({ kind: 'system', label: '历史不完整,部分更早的消息可能缺失', key: `sys-inc:${entries.length}` }); continue }
|
|
130
|
+
if (frame.type === 'reconnecting') { if (!seenReconnecting) { seenReconnecting = true; entries.push({ kind: 'system', label: frame.message || '正在重连远程 runtime', key: `sys-reconn:${entries.length}` }) } continue }
|
|
131
131
|
if (frame.type === 'reconnected') { continue }
|
|
132
132
|
if (frame.type === 'delivery') {
|
|
133
133
|
const accepted = Boolean(frame.data?.accepted)
|
|
134
134
|
if (accepted) continue // turn completion is shown in the composer status line
|
|
135
|
-
entries.push({ kind: 'system', label: 'MonkeyAI
|
|
135
|
+
entries.push({ kind: 'system', label: 'MonkeyAI 未接受该指令', key: `sys-del:${entries.length}` })
|
|
136
136
|
continue
|
|
137
137
|
}
|
|
138
|
-
if (frame.type === 'cancel_sent') { entries.push({ kind: 'system', label: '
|
|
138
|
+
if (frame.type === 'cancel_sent') { entries.push({ kind: 'system', label: '已请求停止', key: `sys-cancel:${entries.length}` }); continue }
|
|
139
139
|
if (frame.type === 'request') { continue } // rendered as an approval card from pendingRequests
|
|
140
|
-
if (frame.type === 'model_switched') { entries.push({ kind: 'system', label:
|
|
140
|
+
if (frame.type === 'model_switched') { entries.push({ kind: 'system', label: `已切换模型:${frame.data?.model ?? '所选模型'}`, key: `sys-model:${entries.length}` }); continue }
|
|
141
141
|
if (frame.type === 'model_switch_failed') { continue } // surfaced via the error banner
|
|
142
142
|
if (frame.type === 'notification' && typeof frame.method === 'string' && frame.method.startsWith('_monkeyai/')) { continue } // internal runtime state notifications
|
|
143
143
|
if (frame.type === 'error') { entries.push({ kind: 'raw', label: frame.code || 'error', data: { message: frame.message || frame.code }, key: `sys-err:${entries.length}` }); continue }
|