@ai-wendao/ant 0.0.0-stage → 0.3.8

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