profileIdentities.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. import WebSocket from 'ws'
  2. import type { ServerConfig } from './config.js'
  3. import { HttpError } from './errors.js'
  4. import type { UpstreamServiceSession } from './localAuth.js'
  5. import {
  6. agentIdentityFromProfile,
  7. encodeAgentAvatar,
  8. } from '../shared/agentIdentity.js'
  9. export interface RemoteProfileIdentity {
  10. name: string
  11. displayName: string
  12. model: string
  13. color?: string
  14. avatar?: string
  15. }
  16. type JsonObject = Record<string, unknown>
  17. function object(value: unknown): JsonObject {
  18. return value && typeof value === 'object' && !Array.isArray(value)
  19. ? value as JsonObject : {}
  20. }
  21. export class UpstreamProfileIdentityService {
  22. constructor(
  23. readonly config: ServerConfig,
  24. readonly upstream: UpstreamServiceSession,
  25. ) {}
  26. async load(): Promise<RemoteProfileIdentity[]> {
  27. const credential = await this.upstream.webSocketCredential()
  28. const url = new URL(this.config.upstream)
  29. url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'
  30. const prefix = this.config.upstream.pathname === '/'
  31. ? '' : this.config.upstream.pathname.replace(/\/$/, '')
  32. url.pathname = `${prefix}/api/ws`
  33. url.search = ''
  34. url.searchParams.set(credential.name, credential.value)
  35. return new Promise<RemoteProfileIdentity[]>((resolve, reject) => {
  36. const socket = new WebSocket(url, {
  37. agent: this.upstream.client.directAgent,
  38. headers: { Origin: this.config.upstream.origin },
  39. handshakeTimeout: 15_000,
  40. maxPayload: 36 * 1_024 * 1_024,
  41. })
  42. const identities = new Map<string, RemoteProfileIdentity>()
  43. let requestedProfiles = false
  44. let finished = false
  45. const timeout = setTimeout(() => finishError(new Error('profile identity request timed out')), 30_000)
  46. timeout.unref()
  47. const cleanup = () => {
  48. clearTimeout(timeout)
  49. if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) socket.close()
  50. }
  51. const finish = () => {
  52. if (finished) return
  53. finished = true
  54. cleanup()
  55. resolve([...identities.values()])
  56. }
  57. const finishError = (error: Error) => {
  58. if (finished) return
  59. finished = true
  60. cleanup()
  61. reject(new HttpError(502, `无法读取机器人身份:${error.message}`, 'profile_identity_failed'))
  62. }
  63. const send = (id: string, method: string, params: JsonObject) => {
  64. socket.send(JSON.stringify({ jsonrpc: '2.0', id, method, params }))
  65. }
  66. socket.on('message', raw => {
  67. let frame: JsonObject
  68. try { frame = object(JSON.parse(String(raw))) } catch { return }
  69. if (!requestedProfiles && object(frame.params).type === 'gateway.ready') {
  70. requestedProfiles = true
  71. send('profiles', 'profiles.list', { include_sessions: false })
  72. return
  73. }
  74. if (String(frame.id ?? '') === 'profiles') {
  75. const profiles = Array.isArray(object(frame.result).profiles)
  76. ? object(frame.result).profiles as unknown[] : []
  77. for (const entry of profiles) {
  78. const outer = object(entry)
  79. const profile = Object.keys(object(outer.profile)).length
  80. ? object(outer.profile) : outer
  81. const name = typeof profile.name === 'string' ? profile.name.trim() : ''
  82. if (!name) continue
  83. const agentIdentity = agentIdentityFromProfile(profile)
  84. const identity: RemoteProfileIdentity = {
  85. name,
  86. displayName: agentIdentity.displayName,
  87. model: typeof profile.model === 'string' ? profile.model : '',
  88. color: agentIdentity.color,
  89. avatar: encodeAgentAvatar(agentIdentity),
  90. }
  91. identities.set(name, identity)
  92. }
  93. finish()
  94. return
  95. }
  96. })
  97. socket.once('error', finishError)
  98. socket.once('unexpected-response', () => finishError(new Error('Hermes rejected identity socket')))
  99. socket.once('close', () => {
  100. if (!finished) finishError(new Error('Hermes closed identity socket'))
  101. })
  102. })
  103. }
  104. }