@ai-wendao/ant 0.0.0-stage → 0.3.9

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.
@@ -0,0 +1,3 @@
1
+ - insert:
2
+ - id: ant
3
+ name: '@ai-wendao/ant'
package/index.js ADDED
@@ -0,0 +1,678 @@
1
+ export const name = 'ant'
2
+
3
+ export const inject = []
4
+
5
+ const MODEL_SYNC_PATH = '/api/ant-teams/models'
6
+ const SERVICE_INFO_PATH = '/api/ant-teams/service-info'
7
+ const BUNDLE_RELEASE_PATH = '/api/ant-teams/bundle-release'
8
+ const DISCONNECT_PATH = '/api/ant-teams/disconnect'
9
+ const REQUEST_CODE_PATH = '/api/ant-teams/request-code'
10
+ const CONNECT_PATH = '/api/ant-teams/connect'
11
+ const FEISHU_LINK_CODE_PATH = '/api/ant-teams/feishu/link-code'
12
+ const FEISHU_MEMBER_APPS_PATH = '/api/ant-teams/feishu/apps'
13
+ const FEISHU_CLAIM_PATH = '/api/ant-teams/feishu/tasks/claim'
14
+ const FEISHU_HEARTBEAT_PATH = '/api/ant-teams/feishu/tasks/heartbeat'
15
+ const FEISHU_COMPLETE_PATH = '/api/ant-teams/feishu/tasks/complete'
16
+ const FEISHU_FINISH_PATH = '/api/ant-teams/feishu/tasks/finish'
17
+ const ANT_NPM_PACKAGE_NAME = '@ai-wendao/ant'
18
+ const MAX_MODEL_RESPONSE_BYTES = 1024 * 1024
19
+ const MODEL_TOKEN_REF = 'ANT_MODEL_API_KEY'
20
+ const SERVICE_RECORD_KEY = 'ant/team-service'
21
+ const SESSION_HEARTBEAT_INTERVAL_MS = 30_000
22
+ const FEISHU_POLL_INTERVAL_MS = 5_000
23
+ const FEISHU_TASK_HEARTBEAT_INTERVAL_MS = 30_000
24
+ const FEISHU_TASK_MAX_RUNTIME_MS = 30 * 60_000
25
+
26
+ function jsonResponse(status, value) {
27
+ return new Response(JSON.stringify(value), {
28
+ status,
29
+ headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' },
30
+ })
31
+ }
32
+
33
+ function normalizeServiceUrl(value) {
34
+ let url
35
+ try { url = new URL(value) } catch { return null }
36
+ const localHttp = url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)
37
+ if ((url.protocol !== 'https:' && !localHttp) || url.username || url.password || url.search || url.hash || (url.pathname !== '/' && url.pathname !== '')) return null
38
+ return url.origin
39
+ }
40
+
41
+ function serviceUrlFromModelSettings(settings) {
42
+ const descriptor = settings.describe().find((entry) => entry.ns === 'llm-pi-ai')
43
+ const providers = descriptor?.value?.providers
44
+ if (!providers || typeof providers !== 'object') return null
45
+ const origins = new Set()
46
+ for (const provider of Object.values(providers)) {
47
+ if (provider?.apiKeyEnv !== MODEL_TOKEN_REF || typeof provider.baseURL !== 'string') continue
48
+ let url
49
+ try { url = new URL(provider.baseURL) } catch { return null }
50
+ const localHttp = url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)
51
+ if ((url.protocol !== 'https:' && !localHttp) || url.username || url.password || url.search || url.hash
52
+ || !['/model-gateway/openai/v1', '/model-gateway/anthropic'].includes(url.pathname)) return null
53
+ origins.add(url.origin)
54
+ }
55
+ return origins.size === 1 ? [...origins][0] : null
56
+ }
57
+
58
+ async function pinServiceUrl(credentials, serviceUrl) {
59
+ await credentials.modifyRecord(SERVICE_RECORD_KEY, async () => ({
60
+ kind: 'grant',
61
+ payload: { service_url: serviceUrl },
62
+ }))
63
+ }
64
+
65
+ async function readMemberConnection(credentials, settings) {
66
+ const credential = await credentials.resolve(MODEL_TOKEN_REF)
67
+ const token = typeof credential?.value === 'string' ? credential.value : null
68
+ if (!token) return null
69
+ const record = await credentials.readRecord(SERVICE_RECORD_KEY)
70
+ const pinnedUrl = record?.kind === 'grant' && typeof record.payload?.service_url === 'string'
71
+ ? normalizeServiceUrl(record.payload.service_url)
72
+ : null
73
+ const serviceUrl = pinnedUrl || serviceUrlFromModelSettings(settings)
74
+ return serviceUrl ? { serviceUrl, token } : null
75
+ }
76
+
77
+ async function updateModelSession(serviceUrl, token, action) {
78
+ return fetch(new URL(`/auth/dsh-session/${action}`, serviceUrl), {
79
+ method: 'POST',
80
+ headers: { authorization: `Bearer ${token}`, accept: 'application/json' },
81
+ redirect: 'manual',
82
+ signal: AbortSignal.timeout(10_000),
83
+ })
84
+ }
85
+
86
+ async function readLimitedBody(response) {
87
+ if (!response.body) return ''
88
+ const reader = response.body.getReader()
89
+ const chunks = []
90
+ let size = 0
91
+ try {
92
+ while (true) {
93
+ const { done, value } = await reader.read()
94
+ if (done) break
95
+ size += value.byteLength
96
+ if (size > MAX_MODEL_RESPONSE_BYTES) {
97
+ await reader.cancel()
98
+ return null
99
+ }
100
+ chunks.push(value)
101
+ }
102
+ } finally {
103
+ reader.releaseLock()
104
+ }
105
+ const bytes = new Uint8Array(size)
106
+ let offset = 0
107
+ for (const chunk of chunks) {
108
+ bytes.set(chunk, offset)
109
+ offset += chunk.byteLength
110
+ }
111
+ return new TextDecoder().decode(bytes)
112
+ }
113
+
114
+ async function fetchMemberModels(request, credentials, settings) {
115
+ if (request.method !== 'POST') return jsonResponse(405, { message: '只支持 POST 请求' })
116
+ let token
117
+ let pinnedUrl
118
+ let serviceUrl
119
+ try {
120
+ token = await credentials.resolve(MODEL_TOKEN_REF)
121
+ const record = await credentials.readRecord(SERVICE_RECORD_KEY)
122
+ pinnedUrl = record?.kind === 'grant' && typeof record.payload?.service_url === 'string'
123
+ ? normalizeServiceUrl(record.payload.service_url)
124
+ : null
125
+ serviceUrl = pinnedUrl || serviceUrlFromModelSettings(settings)
126
+ } catch {
127
+ return jsonResponse(500, { message: 'DSH 无法读取 ANT 连接配置。' })
128
+ }
129
+ if (!token || !serviceUrl) return jsonResponse(401, { message: '当前 DSH 未连接 ANT,请验证工作邮箱。' })
130
+
131
+ try {
132
+ const session = await updateModelSession(serviceUrl, token.value, 'heartbeat')
133
+ if (session.status === 401 || session.status === 403) {
134
+ return jsonResponse(401, { message: 'ANT 成员凭证已失效,请重新验证工作邮箱。' })
135
+ }
136
+ if (!session.ok) return jsonResponse(502, { message: '无法激活 ANT 模型会话。' })
137
+ const upstream = await fetch(new URL('/api/me/models', serviceUrl), {
138
+ headers: { authorization: `Bearer ${token.value}`, accept: 'application/json' },
139
+ redirect: 'manual',
140
+ signal: AbortSignal.timeout(10_000),
141
+ })
142
+ const text = await readLimitedBody(upstream)
143
+ if (text === null) return jsonResponse(502, { message: '团队模型目录响应过大' })
144
+ if (upstream.status === 401 || upstream.status === 403) {
145
+ return jsonResponse(401, { message: 'ANT 成员凭证已失效,请重新验证工作邮箱。' })
146
+ }
147
+ if (!upstream.ok) return jsonResponse(502, { message: 'ANT 暂时无法提供团队模型目录。' })
148
+ let result
149
+ try { result = JSON.parse(text) } catch { return jsonResponse(502, { message: 'ANT 团队模型目录格式无效。' }) }
150
+ if (!Array.isArray(result?.models) || normalizeServiceUrl(result.service_url) !== serviceUrl) {
151
+ return jsonResponse(502, { message: 'ANT 团队模型目录与已连接服务不匹配。' })
152
+ }
153
+ if (!pinnedUrl) {
154
+ try { await pinServiceUrl(credentials, serviceUrl) } catch {
155
+ return jsonResponse(500, { message: 'DSH 无法保存已连接的 ANT 服务地址' })
156
+ }
157
+ }
158
+ return jsonResponse(200, { service_url: serviceUrl, models: result.models })
159
+ } catch {
160
+ return jsonResponse(502, { message: '无法连接 ANT 团队服务' })
161
+ }
162
+ }
163
+
164
+ async function fetchServiceInfo(request, credentials, settings) {
165
+ if (request.method !== 'POST') return jsonResponse(405, { message: '只支持 POST 请求' })
166
+ let body
167
+ try {
168
+ const text = await request.text()
169
+ if (text.length > 4096) return jsonResponse(413, { message: '版本协商请求过大。' })
170
+ body = JSON.parse(text)
171
+ } catch {
172
+ return jsonResponse(400, { message: '版本协商请求格式无效。' })
173
+ }
174
+ const hasServiceUrl = typeof body?.service_url === 'string'
175
+ const suppliedServiceUrl = hasServiceUrl && body.service_url.trim()
176
+ ? normalizeServiceUrl(body.service_url)
177
+ : null
178
+ const bundleVersion = typeof body?.bundle_version === 'string'
179
+ && /^[0-9A-Za-z][0-9A-Za-z.+-]{0,63}$/u.test(body.bundle_version)
180
+ ? body.bundle_version
181
+ : null
182
+ const clientProtocols = Array.isArray(body?.api_protocol_versions)
183
+ && body.api_protocol_versions.length <= 16
184
+ && body.api_protocol_versions.every((version) => Number.isInteger(version) && version > 0 && version <= 65535)
185
+ ? [...new Set(body.api_protocol_versions)]
186
+ : null
187
+ if ((hasServiceUrl && body.service_url.trim() && !suppliedServiceUrl) || !bundleVersion || !clientProtocols?.length) {
188
+ return jsonResponse(400, { message: '团队服务地址或客户端版本信息无效。' })
189
+ }
190
+
191
+ let serviceUrl = suppliedServiceUrl
192
+ if (!serviceUrl) {
193
+ try {
194
+ serviceUrl = (await readMemberConnection(credentials, settings))?.serviceUrl || null
195
+ } catch {
196
+ return jsonResponse(500, { message: 'DSH 无法读取已保存的 ANT 服务地址。' })
197
+ }
198
+ }
199
+ if (!serviceUrl) return jsonResponse(400, { message: '请先填写 ANT 服务地址或连接团队。' })
200
+
201
+ try {
202
+ const upstream = await fetch(new URL('/.well-known/ant', serviceUrl), {
203
+ headers: {
204
+ accept: 'application/json',
205
+ 'x-ant-bundle-version': bundleVersion,
206
+ 'x-ant-api-protocol-versions': clientProtocols.join(','),
207
+ },
208
+ redirect: 'manual',
209
+ signal: AbortSignal.timeout(10_000),
210
+ })
211
+ const text = await readLimitedBody(upstream)
212
+ if (text === null) return jsonResponse(502, { message: 'ANT 版本信息响应过大。' })
213
+ if (!upstream.ok) return jsonResponse(502, { message: `ANT 版本信息请求失败(${upstream.status})。` })
214
+ let info
215
+ try { info = JSON.parse(text) } catch { return jsonResponse(502, { message: 'ANT 版本信息格式无效。' }) }
216
+ if (info?.service !== 'ant-server' || typeof info.version !== 'string' || info.version.length > 64) {
217
+ return jsonResponse(502, { message: '目标地址未返回有效的 ANT 服务信息。' })
218
+ }
219
+ const rawServerProtocols = info.api_protocol_versions
220
+ const validServerProtocols = Array.isArray(rawServerProtocols)
221
+ && rawServerProtocols.every((version) => Number.isInteger(version) && version > 0 && version <= 65535)
222
+ const serverProtocols = validServerProtocols ? [...new Set(rawServerProtocols)] : []
223
+ const sharedProtocols = serverProtocols.filter((version) => clientProtocols.includes(version))
224
+ const compatibility = validServerProtocols ? sharedProtocols.length > 0 : null
225
+ return jsonResponse(200, {
226
+ server_version: info.version,
227
+ bundle_version: bundleVersion,
228
+ server_api_protocol_versions: serverProtocols,
229
+ client_api_protocol_versions: clientProtocols,
230
+ shared_api_protocol_versions: sharedProtocols,
231
+ compatibility,
232
+ })
233
+ } catch {
234
+ return jsonResponse(502, { message: '无法连接 ANT 服务以检查版本兼容性。' })
235
+ }
236
+ }
237
+
238
+ async function fetchBundleRelease(request, credentials, settings) {
239
+ if (request.method !== 'GET') return jsonResponse(405, { message: '只支持 GET 请求' })
240
+ let connection
241
+ try { connection = await readMemberConnection(credentials, settings) } catch {
242
+ return jsonResponse(500, { message: 'DSH 无法读取 ANT 连接配置。' })
243
+ }
244
+ if (!connection) return jsonResponse(401, { message: '请先在 ANT 插件中连接团队。' })
245
+ try {
246
+ const session = await updateModelSession(connection.serviceUrl, connection.token, 'heartbeat')
247
+ if (session.status === 401 || session.status === 403) return jsonResponse(401, { message: 'ANT 成员凭证已失效。' })
248
+ if (!session.ok) return jsonResponse(502, { message: '无法激活 ANT 模型会话。' })
249
+ const headers = { authorization: `Bearer ${connection.token}`, accept: 'application/json' }
250
+ if (request.headers.get('x-ant-bundle-capabilities') === 'direct-package-install') {
251
+ headers['x-ant-bundle-capabilities'] = 'direct-package-install'
252
+ }
253
+ const upstream = await fetch(new URL('/api/me/bundle-release', connection.serviceUrl), {
254
+ headers,
255
+ redirect: 'manual',
256
+ signal: AbortSignal.timeout(10_000),
257
+ })
258
+ const text = await readLimitedBody(upstream)
259
+ if (text === null) return jsonResponse(502, { message: 'ANT 更新信息响应过大。' })
260
+ if (upstream.status === 401 || upstream.status === 403) return jsonResponse(401, { message: 'ANT 成员凭证已失效。' })
261
+ if (upstream.status === 426) {
262
+ return jsonResponse(426, { message: '当前 ANT 插件版本不能安全自动安装 scoped Bundle。请通过 DSH 官方插件管理器完成一次包名迁移。' })
263
+ }
264
+ if (!upstream.ok) return jsonResponse(502, { message: 'ANT 暂时无法提供插件更新信息。' })
265
+ let release
266
+ try { release = JSON.parse(text) } catch { return jsonResponse(502, { message: 'ANT 更新信息格式无效。' }) }
267
+ const validPackageName = release?.package_name === ANT_NPM_PACKAGE_NAME
268
+ const validVersion = typeof release?.version === 'string'
269
+ && (release.version === '' || /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/u.test(release.version))
270
+ const installSpec = release?.package_name === 'ant'
271
+ ? `ant@${release?.version}`
272
+ : `ant@npm:${release?.package_name}@${release?.version}`
273
+ const directInstallSpec = release?.package_name && release?.version
274
+ ? `${release.package_name}@${release.version}`
275
+ : ''
276
+ if (release?.bundle_name !== 'ant' || !validPackageName || !validVersion
277
+ || typeof release?.install_spec !== 'string' || typeof release?.release_notes !== 'string'
278
+ || (release.version && release.install_spec !== installSpec)
279
+ || (!release.version && release.install_spec !== '')
280
+ || (release.direct_install_spec !== undefined && release.direct_install_spec !== directInstallSpec)) {
281
+ return jsonResponse(502, { message: 'ANT 更新信息字段无效。' })
282
+ }
283
+ return jsonResponse(200, release)
284
+ } catch {
285
+ return jsonResponse(502, { message: '无法连接 ANT 更新服务。' })
286
+ }
287
+ }
288
+
289
+ async function disconnectMember(request, credentials, settings) {
290
+ if (request.method !== 'POST') return jsonResponse(405, { message: '只支持 POST 请求' })
291
+ let connection
292
+ try { connection = await readMemberConnection(credentials, settings) } catch {
293
+ return jsonResponse(500, { message: 'DSH 无法读取 ANT 连接配置。' })
294
+ }
295
+ if (!connection) return jsonResponse(200, { revoked: false })
296
+ try {
297
+ const response = await fetch(new URL('/auth/dsh-session/disconnect', connection.serviceUrl), {
298
+ method: 'POST',
299
+ headers: { authorization: `Bearer ${connection.token}`, accept: 'application/json' },
300
+ redirect: 'manual',
301
+ signal: AbortSignal.timeout(10_000),
302
+ })
303
+ if (response.status === 204 || response.status === 401 || response.status === 403) {
304
+ return jsonResponse(200, { revoked: response.status === 204 })
305
+ }
306
+ return jsonResponse(200, { revoked: false })
307
+ } catch {
308
+ return jsonResponse(200, { revoked: false })
309
+ }
310
+ }
311
+
312
+ async function relayFeishuRequest(request, path, credentials, settings) {
313
+ if (request.method !== 'POST') return jsonResponse(405, { message: '只支持 POST 请求' })
314
+ let connection
315
+ try { connection = await readMemberConnection(credentials, settings) } catch {
316
+ return jsonResponse(500, { message: 'DSH 无法读取 ANT 连接配置。' })
317
+ }
318
+ if (!connection) return jsonResponse(401, { message: '请先在 ANT 插件中连接团队。' })
319
+ try {
320
+ const session = await updateModelSession(connection.serviceUrl, connection.token, 'heartbeat')
321
+ if (session.status === 401 || session.status === 403) return jsonResponse(401, { message: 'ANT 成员凭证已失效。' })
322
+ if (!session.ok) return jsonResponse(502, { message: '无法激活 ANT 模型会话。' })
323
+ const body = await request.text()
324
+ if (body.length > 64 * 1024) return jsonResponse(413, { message: '飞书任务请求过大。' })
325
+ const upstream = await fetch(new URL(path, connection.serviceUrl), {
326
+ method: 'POST',
327
+ headers: {
328
+ authorization: `Bearer ${connection.token}`,
329
+ ...(body ? { 'content-type': 'application/json' } : {}),
330
+ accept: 'application/json',
331
+ },
332
+ ...(body ? { body } : {}),
333
+ redirect: 'manual',
334
+ signal: AbortSignal.timeout(15_000),
335
+ })
336
+ const text = await readLimitedBody(upstream)
337
+ if (text === null) return jsonResponse(502, { message: 'ANT 飞书任务响应过大。' })
338
+ return new Response(text, {
339
+ status: upstream.status,
340
+ headers: { 'content-type': upstream.headers.get('content-type') || 'application/json', 'cache-control': 'no-store' },
341
+ })
342
+ } catch {
343
+ return jsonResponse(502, { message: '无法连接 ANT 飞书任务服务。' })
344
+ }
345
+ }
346
+
347
+ async function requestFeishuJson(serviceUrl, token, path, body, timeout = 15_000) {
348
+ const response = await fetch(new URL(path, serviceUrl), {
349
+ method: 'POST',
350
+ headers: {
351
+ authorization: `Bearer ${token}`,
352
+ accept: 'application/json',
353
+ ...(body === undefined ? {} : { 'content-type': 'application/json' }),
354
+ },
355
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
356
+ redirect: 'manual',
357
+ signal: AbortSignal.timeout(timeout),
358
+ })
359
+ const text = await readLimitedBody(response)
360
+ if (text === null) throw new Error('ANT 飞书任务响应过大')
361
+ if (!response.ok) throw new Error(`ANT 飞书任务请求失败(${response.status})`)
362
+ if (response.status === 204 || text === '') return null
363
+ try { return JSON.parse(text) } catch { throw new Error('ANT 飞书任务响应格式无效') }
364
+ }
365
+
366
+ async function syncModelsToHost(settings, serviceUrl, models) {
367
+ if (typeof settings?.mutate !== 'function') throw new Error('当前 DSH 版本未提供模型设置接口。')
368
+ const grouped = {
369
+ openai: models.filter((model) => model.protocol === 'openai'),
370
+ anthropic: models.filter((model) => model.protocol === 'anthropic'),
371
+ }
372
+ const ops = []
373
+ for (const protocol of ['openai', 'anthropic']) {
374
+ const route = protocol === 'openai' ? 'ant-team-openai' : 'ant-team-anthropic'
375
+ const protocolModels = grouped[protocol]
376
+ if (protocolModels.length === 0) {
377
+ ops.push({ op: 'unset', path: ['providers', route] })
378
+ continue
379
+ }
380
+ ops.push({ op: 'set', path: ['providers', route], value: {
381
+ displayName: protocol === 'openai' ? 'Lakala (OpenAI)' : 'Lakala (Anthropic)',
382
+ apiKeyEnv: 'ANT_MODEL_API_KEY',
383
+ api: protocol === 'openai' ? 'openai-completions' : 'anthropic-messages',
384
+ baseURL: protocol === 'openai'
385
+ ? new URL('/model-gateway/openai/v1', serviceUrl).toString()
386
+ : new URL('/model-gateway/anthropic', serviceUrl).toString(),
387
+ models: protocolModels.map((model) => ({ id: model.model_id, name: model.display_name })),
388
+ } })
389
+ }
390
+ const result = await settings.mutate('llm-pi-ai', ops, undefined)
391
+ if (!result.ok) throw new Error('DSH 无法更新 ANT 获准模型。')
392
+ }
393
+
394
+ function assistantMessageText(event) {
395
+ const content = event?.data?.message?.content
396
+ if (!Array.isArray(content)) return ''
397
+ return content.filter((part) => part?.type === 'text' && typeof part.text === 'string')
398
+ .map((part) => part.text).join('')
399
+ }
400
+
401
+ async function executeFeishuTask(task, connection, settings, sessionController, stoppedSignal) {
402
+ let sessionId
403
+ let promptAccepted = false
404
+ let followAbort
405
+ let taskHeartbeat
406
+ let runtimeTimeout
407
+ let timedOut = false
408
+ let stopSignalListener
409
+ try {
410
+ const catalogResponse = await fetch(new URL('/api/me/models', connection.serviceUrl), {
411
+ headers: { authorization: `Bearer ${connection.token}`, accept: 'application/json' },
412
+ redirect: 'manual',
413
+ signal: AbortSignal.timeout(10_000),
414
+ })
415
+ const text = await readLimitedBody(catalogResponse)
416
+ if (!catalogResponse.ok || text === null) throw new Error('ANT 当前无法返回获准模型。')
417
+ const catalog = JSON.parse(text)
418
+ if (normalizeServiceUrl(catalog.service_url) !== connection.serviceUrl || !Array.isArray(catalog.models)) {
419
+ throw new Error('ANT 返回的模型目录无效。')
420
+ }
421
+ await syncModelsToHost(settings, connection.serviceUrl, catalog.models)
422
+ const model = task.models?.[0]
423
+ if (!model || !catalog.models.some((item) => item.protocol === model.protocol && item.model_id === model.model_id)) {
424
+ throw new Error('任务模型已不在成员授权目录中。')
425
+ }
426
+ if (typeof sessionController?.create !== 'function' || typeof sessionController?.follow !== 'function'
427
+ || typeof sessionController?.selectModel !== 'function' || typeof sessionController?.prompt !== 'function') {
428
+ throw new Error('当前 DSH 版本未提供 Session Controller 接口。')
429
+ }
430
+ sessionId = await sessionController.create(task.session_id ? { sessionId: task.session_id } : {})
431
+ const provider = model.protocol === 'openai' ? 'ant-team-openai' : 'ant-team-anthropic'
432
+ await sessionController.selectModel({ sessionId, provider, model: model.model_id })
433
+ followAbort = new AbortController()
434
+ stopSignalListener = () => followAbort.abort()
435
+ stoppedSignal.addEventListener('abort', stopSignalListener, { once: true })
436
+ const follow = sessionController.follow({ address: { kind: 'session', sessionId } }, followAbort.signal)
437
+ const iterator = follow[Symbol.asyncIterator]()
438
+ const opening = await iterator.next()
439
+ if (opening.done || opening.value?.type !== 'snapshot') throw new Error('DSH 会话无法开始跟随。')
440
+ if (stoppedSignal.aborted) throw new Error('DSH 客户端正在关闭。')
441
+
442
+ await sessionController.prompt({
443
+ requestId: globalThis.crypto.randomUUID(),
444
+ sessionId,
445
+ mode: 'queue',
446
+ content: [{ type: 'text', text: task.prompt }],
447
+ }, followAbort.signal)
448
+ promptAccepted = true
449
+ taskHeartbeat = setInterval(() => {
450
+ void updateModelSession(connection.serviceUrl, connection.token, 'heartbeat')
451
+ .then((response) => {
452
+ if (!response.ok) throw new Error('ANT model session expired')
453
+ return requestFeishuJson(connection.serviceUrl, connection.token, '/api/feishu/tasks/heartbeat', { task_id: task.id })
454
+ })
455
+ .catch(() => followAbort.abort())
456
+ }, FEISHU_TASK_HEARTBEAT_INTERVAL_MS)
457
+ taskHeartbeat.unref?.()
458
+ runtimeTimeout = setTimeout(() => { timedOut = true; followAbort.abort() }, FEISHU_TASK_MAX_RUNTIME_MS)
459
+ runtimeTimeout.unref?.()
460
+
461
+ let result = ''
462
+ while (!stoppedSignal.aborted) {
463
+ const next = await iterator.next()
464
+ if (next.done) throw new Error('DSH 会话结果流意外结束。')
465
+ const frame = next.value
466
+ if (frame?.type !== 'event') continue
467
+ const event = frame.event
468
+ if (event?.type === 'assistant/message') {
469
+ const message = assistantMessageText(event)
470
+ if (message) result = message
471
+ }
472
+ if (event?.type === 'turn/end') {
473
+ if (event.data?.reason?.kind !== 'completed' || !result.trim()) throw new Error('DSH 任务未能正常完成。')
474
+ await requestFeishuJson(connection.serviceUrl, connection.token, '/api/feishu/tasks/complete', {
475
+ task_id: task.id,
476
+ session_id: sessionId,
477
+ result,
478
+ })
479
+ return
480
+ }
481
+ }
482
+ if (stoppedSignal.aborted) throw new Error('DSH 客户端正在关闭。')
483
+ } catch {
484
+ const status = promptAccepted ? 'unknown' : 'failed'
485
+ try {
486
+ await requestFeishuJson(connection.serviceUrl, connection.token, '/api/feishu/tasks/finish', {
487
+ task_id: task.id,
488
+ status: timedOut && promptAccepted ? 'unknown' : status,
489
+ })
490
+ } catch {}
491
+ } finally {
492
+ clearInterval(taskHeartbeat)
493
+ clearTimeout(runtimeTimeout)
494
+ if (stopSignalListener) stoppedSignal.removeEventListener('abort', stopSignalListener)
495
+ followAbort?.abort()
496
+ }
497
+ }
498
+
499
+ function installFeishuTaskWorker(credentials, settings, sessionController) {
500
+ let stopped = false
501
+ let busy = false
502
+ let taskAbort
503
+ let work = Promise.resolve()
504
+ let pollWork = Promise.resolve()
505
+ const poll = async () => {
506
+ if (stopped || busy) return
507
+ busy = true
508
+ try {
509
+ const connection = await readMemberConnection(credentials, settings)
510
+ if (!connection) return
511
+ const session = await updateModelSession(connection.serviceUrl, connection.token, 'heartbeat')
512
+ if (!session.ok || stopped) return
513
+ const claim = await requestFeishuJson(connection.serviceUrl, connection.token, '/api/feishu/tasks/claim')
514
+ if (!claim?.task || stopped) return
515
+ taskAbort = new AbortController()
516
+ work = executeFeishuTask(claim.task, connection, settings, sessionController, taskAbort.signal)
517
+ await work
518
+ } catch {
519
+ // A transient polling error is retried on the next interval.
520
+ } finally {
521
+ busy = false
522
+ }
523
+ }
524
+ const schedulePoll = () => {
525
+ if (stopped || busy) return
526
+ pollWork = poll()
527
+ }
528
+ const timer = setInterval(schedulePoll, FEISHU_POLL_INTERVAL_MS)
529
+ timer.unref?.()
530
+ schedulePoll()
531
+ return async () => {
532
+ stopped = true
533
+ clearInterval(timer)
534
+ taskAbort?.abort()
535
+ await pollWork
536
+ await work
537
+ }
538
+ }
539
+
540
+ async function fetchAuthRequest(request, endpoint, credentials) {
541
+ if (request.method !== 'POST') return jsonResponse(405, { message: '只支持 POST 请求' })
542
+ let body
543
+ try { body = await request.json() } catch { return jsonResponse(400, { message: '请求格式无效' }) }
544
+ const serviceUrl = typeof body?.service_url === 'string' ? normalizeServiceUrl(body.service_url) : null
545
+ const payload = endpoint === 'request-code'
546
+ ? { email: body?.email }
547
+ : { email: body?.email, code: body?.code, device_id: body?.device_id }
548
+ if (!serviceUrl || typeof payload.email !== 'string' || payload.email.length > 320
549
+ || (endpoint !== 'request-code' && (typeof payload.code !== 'string' || payload.code.length !== 8
550
+ || typeof payload.device_id !== 'string' || payload.device_id.length > 64))) {
551
+ return jsonResponse(400, { message: '团队服务地址或邮箱验证信息无效' })
552
+ }
553
+
554
+ try {
555
+ const upstream = await fetch(new URL(`/auth/${endpoint}`, serviceUrl), {
556
+ method: 'POST',
557
+ headers: {
558
+ accept: 'application/json',
559
+ 'content-type': 'application/json',
560
+ origin: serviceUrl,
561
+ },
562
+ body: JSON.stringify(payload),
563
+ redirect: 'manual',
564
+ signal: AbortSignal.timeout(10_000),
565
+ })
566
+ const text = await readLimitedBody(upstream)
567
+ if (text === null) return jsonResponse(502, { message: 'ANT 服务响应过大' })
568
+ if (endpoint === 'dsh-connect' && upstream.ok) {
569
+ let result
570
+ try { result = JSON.parse(text) } catch { return jsonResponse(502, { message: 'ANT 连接响应格式无效' }) }
571
+ if (typeof result?.token !== 'string' || !result.token.startsWith('antm_') || !Array.isArray(result.models)) {
572
+ return jsonResponse(502, { message: 'ANT 连接响应格式无效' })
573
+ }
574
+ try {
575
+ await pinServiceUrl(credentials, serviceUrl)
576
+ } catch {
577
+ return jsonResponse(500, { message: 'DSH 无法保存已连接的 ANT 服务地址' })
578
+ }
579
+ try {
580
+ const session = await updateModelSession(serviceUrl, result.token, 'heartbeat')
581
+ if (!session.ok) return jsonResponse(502, { message: '无法激活 ANT 模型会话。' })
582
+ } catch {
583
+ return jsonResponse(502, { message: '无法连接 ANT 模型会话服务。' })
584
+ }
585
+ }
586
+ return new Response(text, {
587
+ status: upstream.status,
588
+ headers: { 'content-type': upstream.headers.get('content-type') || 'application/json', 'cache-control': 'no-store' },
589
+ })
590
+ } catch {
591
+ return jsonResponse(502, { message: '无法连接 ANT 团队服务' })
592
+ }
593
+ }
594
+
595
+ export function apply(ctx) {
596
+ ctx.inject(['connection', 'credentials', 'settings', 'sessionController'], (connectionCtx) => {
597
+ connectionCtx.effect(() => connectionCtx.connection.fetch.register({
598
+ path: SERVICE_INFO_PATH,
599
+ methods: ['POST'],
600
+ requestBody: 'buffered',
601
+ fetch: (request) => fetchServiceInfo(request, connectionCtx.credentials, connectionCtx.settings),
602
+ }), 'ant-teams: ANT client-server version handshake')
603
+ connectionCtx.effect(() => connectionCtx.connection.fetch.register({
604
+ path: MODEL_SYNC_PATH,
605
+ methods: ['POST'],
606
+ requestBody: 'buffered',
607
+ fetch: (request) => fetchMemberModels(request, connectionCtx.credentials, connectionCtx.settings),
608
+ }), 'ant-teams: authenticated model catalog relay')
609
+ connectionCtx.effect(() => connectionCtx.connection.fetch.register({
610
+ path: BUNDLE_RELEASE_PATH,
611
+ methods: ['GET'],
612
+ fetch: (request) => fetchBundleRelease(request, connectionCtx.credentials, connectionCtx.settings),
613
+ }), 'ant-teams: authenticated Bundle release relay')
614
+ connectionCtx.effect(() => connectionCtx.connection.fetch.register({
615
+ path: DISCONNECT_PATH,
616
+ methods: ['POST'],
617
+ fetch: (request) => disconnectMember(request, connectionCtx.credentials, connectionCtx.settings),
618
+ }), 'ant-teams: revoke this device connection')
619
+ connectionCtx.effect(() => connectionCtx.connection.fetch.register({
620
+ path: REQUEST_CODE_PATH,
621
+ methods: ['POST'],
622
+ requestBody: 'buffered',
623
+ fetch: (request) => fetchAuthRequest(request, 'request-code', connectionCtx.credentials),
624
+ }), 'ant-teams: relay email verification code request')
625
+ connectionCtx.effect(() => connectionCtx.connection.fetch.register({
626
+ path: CONNECT_PATH,
627
+ methods: ['POST'],
628
+ requestBody: 'buffered',
629
+ fetch: (request) => fetchAuthRequest(request, 'dsh-connect', connectionCtx.credentials),
630
+ }), 'ant-teams: relay DSH account connection')
631
+ for (const [path, endpoint] of [
632
+ [FEISHU_MEMBER_APPS_PATH, '/api/feishu/member-apps'],
633
+ [FEISHU_LINK_CODE_PATH, '/api/feishu/link-code'],
634
+ [FEISHU_CLAIM_PATH, '/api/feishu/tasks/claim'],
635
+ [FEISHU_HEARTBEAT_PATH, '/api/feishu/tasks/heartbeat'],
636
+ [FEISHU_COMPLETE_PATH, '/api/feishu/tasks/complete'],
637
+ [FEISHU_FINISH_PATH, '/api/feishu/tasks/finish'],
638
+ ]) {
639
+ connectionCtx.effect(() => connectionCtx.connection.fetch.register({
640
+ path,
641
+ methods: ['POST'],
642
+ requestBody: 'buffered',
643
+ fetch: (request) => relayFeishuRequest(request, endpoint, connectionCtx.credentials, connectionCtx.settings),
644
+ }), `ant-teams: relay Feishu ${endpoint.split('/').at(-1)}`)
645
+ }
646
+ connectionCtx.effect(() => {
647
+ let stopped = false
648
+ let inFlight = Promise.resolve()
649
+ let refreshing = false
650
+ const heartbeat = () => {
651
+ if (stopped || refreshing) return
652
+ refreshing = true
653
+ inFlight = readMemberConnection(connectionCtx.credentials, connectionCtx.settings)
654
+ .then((connection) => connection
655
+ ? updateModelSession(connection.serviceUrl, connection.token, 'heartbeat')
656
+ : undefined)
657
+ .catch(() => undefined)
658
+ .finally(() => { refreshing = false })
659
+ }
660
+ heartbeat()
661
+ const timer = setInterval(heartbeat, SESSION_HEARTBEAT_INTERVAL_MS)
662
+ timer.unref?.()
663
+ return async () => {
664
+ stopped = true
665
+ clearInterval(timer)
666
+ await inFlight
667
+ try {
668
+ const connection = await readMemberConnection(connectionCtx.credentials, connectionCtx.settings)
669
+ if (connection) await updateModelSession(connection.serviceUrl, connection.token, 'revoke')
670
+ } catch {}
671
+ }
672
+ }, 'ant-teams: ANT model activity session')
673
+ connectionCtx.effect(
674
+ () => installFeishuTaskWorker(connectionCtx.credentials, connectionCtx.settings, connectionCtx.sessionController),
675
+ 'ant-teams: DSH-owned Feishu task worker',
676
+ )
677
+ })
678
+ }