@ai-wendao/ant 0.4.0 → 0.4.4
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 +5 -3
- package/client.js +528 -203
- package/conversation.js +981 -0
- package/cordis.patch.yml +14 -1
- package/delegation.js +1045 -47
- package/index.js +193 -13
- package/package.json +2 -2
package/client.js
CHANGED
|
@@ -160,6 +160,7 @@ window.__ModuleLoader__.load({
|
|
|
160
160
|
const inject = ['slots', 'layout', 'pluginNavigation', 'remote', 'remote.credentials', 'remote.settings', 'remote.pluginManager']
|
|
161
161
|
|
|
162
162
|
async function readLocalJsonResponse(response) {
|
|
163
|
+
if (response.status === 204) return null
|
|
163
164
|
const contentType = response.headers?.get?.('content-type') || ''
|
|
164
165
|
const isJson = /^(?:application|text)\/(?:[a-z0-9!#$&^_.+-]+\+)?json(?:\s*;|$)/iu.test(contentType.trim())
|
|
165
166
|
if (contentType && !isJson) {
|
|
@@ -239,6 +240,7 @@ window.__ModuleLoader__.load({
|
|
|
239
240
|
const bindingRevision = useRef(0)
|
|
240
241
|
const [message, setMessage] = useState('')
|
|
241
242
|
const [pluginAlerts, setPluginAlerts] = useState([])
|
|
243
|
+
const [delegationRevision, setDelegationRevision] = useState(0)
|
|
242
244
|
useEffect(() => {
|
|
243
245
|
let disposed = false
|
|
244
246
|
const revision = bindingRevision.current
|
|
@@ -313,6 +315,7 @@ window.__ModuleLoader__.load({
|
|
|
313
315
|
const onConnectionChange = (url) => {
|
|
314
316
|
bindingRevision.current += 1
|
|
315
317
|
setBinding({ state: 'ready', url: url || null })
|
|
318
|
+
setDelegationRevision((value) => value + 1)
|
|
316
319
|
if (url) {
|
|
317
320
|
setServiceUrl(url)
|
|
318
321
|
setDraft(url)
|
|
@@ -353,272 +356,594 @@ window.__ModuleLoader__.load({
|
|
|
353
356
|
onClick: () => openInstalledPlugin(plugin.package_name), style: secondaryButtonStyle }, `在 DSH 中查看 ${plugin.title}`))) : null,
|
|
354
357
|
h(ModelSyncFooter, { remote, serviceUrl, bindingReady: binding.state === 'ready',
|
|
355
358
|
onServiceUrlChange: setServiceUrl, onConnectionChange }),
|
|
356
|
-
h(
|
|
359
|
+
h(DelegationCards, { remote, revision: delegationRevision,
|
|
360
|
+
openImSettings: () => openInstalledPlugin('@xmanrui/dsh-im') }))
|
|
357
361
|
}
|
|
358
362
|
|
|
359
|
-
function
|
|
363
|
+
function DelegationCards({ remote, revision, openImSettings }) {
|
|
360
364
|
const [bots, setBots] = useState([])
|
|
361
|
-
const [
|
|
365
|
+
const [enrolled, setEnrolled] = useState([])
|
|
362
366
|
const [groups, setGroups] = useState([])
|
|
363
|
-
const [
|
|
367
|
+
const [cards, setCards] = useState([])
|
|
364
368
|
const [tasks, setTasks] = useState([])
|
|
365
|
-
const [
|
|
369
|
+
const [executors, setExecutors] = useState([])
|
|
370
|
+
const [defaults, setDefaults] = useState(null)
|
|
371
|
+
const [localConcurrency, setLocalConcurrency] = useState(null)
|
|
372
|
+
const [repositoryCacheLimitGib, setRepositoryCacheLimitGib] = useState(10)
|
|
366
373
|
const [botId, setBotId] = useState('')
|
|
374
|
+
const [trustedOrigin, setTrustedOrigin] = useState('')
|
|
375
|
+
const [bindingCode, setBindingCode] = useState('')
|
|
367
376
|
const [conversationKind, setConversationKind] = useState('dm')
|
|
368
377
|
const [groupId, setGroupId] = useState('')
|
|
369
|
-
const [
|
|
370
|
-
const [
|
|
371
|
-
const [
|
|
372
|
-
const [
|
|
373
|
-
const [
|
|
378
|
+
const [chatGroupId, setChatGroupId] = useState('')
|
|
379
|
+
const [title, setTitle] = useState('')
|
|
380
|
+
const [intent, setIntent] = useState('')
|
|
381
|
+
const [examples, setExamples] = useState('')
|
|
382
|
+
const [taskPrompt, setTaskPrompt] = useState('')
|
|
383
|
+
const [runner, setRunner] = useState('dsh')
|
|
384
|
+
const [parameters, setParameters] = useState([])
|
|
385
|
+
const [giteaToken, setGiteaToken] = useState('')
|
|
386
|
+
const [tokenConfigured, setTokenConfigured] = useState(false)
|
|
374
387
|
const [senderScope, setSenderScope] = useState('any')
|
|
375
388
|
const [senderIds, setSenderIds] = useState('')
|
|
376
389
|
const [days, setDays] = useState(7)
|
|
377
|
-
const [token, setToken] = useState('')
|
|
378
|
-
const [tokenReady, setTokenReady] = useState(false)
|
|
379
390
|
const [busy, setBusy] = useState(false)
|
|
391
|
+
const [loading, setLoading] = useState(true)
|
|
392
|
+
const [botLoadError, setBotLoadError] = useState(false)
|
|
393
|
+
const [loadFailures, setLoadFailures] = useState([])
|
|
394
|
+
const [createOpen, setCreateOpen] = useState(false)
|
|
380
395
|
const [message, setMessage] = useState('')
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
396
|
+
const load = async () => {
|
|
397
|
+
setLoading(true)
|
|
398
|
+
setMessage('')
|
|
399
|
+
try {
|
|
400
|
+
const paths = ['bots', 'enrolled-bots', 'groups', 'cards', 'settings', 'executors', 'local-settings', 'status']
|
|
401
|
+
const responses = await Promise.all(paths.map((path) =>
|
|
402
|
+
fetch(`/api/ant-teams/delegation/${path}`, { cache: 'no-store',
|
|
403
|
+
...(path === 'enrolled-bots' ? { headers: { 'x-ant-include-unbound': 'true' } } : {}) })
|
|
404
|
+
.catch(() => null)))
|
|
405
|
+
const data = await Promise.all(responses.map((response) => response
|
|
406
|
+
? readLocalJsonResponse(response).catch(() => null) : null))
|
|
407
|
+
const valid = [
|
|
408
|
+
(value) => Array.isArray(value?.bots),
|
|
409
|
+
Array.isArray, Array.isArray, Array.isArray,
|
|
410
|
+
(value) => value && typeof value === 'object' && !Array.isArray(value),
|
|
411
|
+
Array.isArray,
|
|
412
|
+
(value) => value && typeof value === 'object' && !Array.isArray(value),
|
|
413
|
+
(value) => Array.isArray(value?.items),
|
|
414
|
+
]
|
|
415
|
+
const readable = responses.map((response, index) => Boolean(response?.ok && valid[index](data[index])))
|
|
416
|
+
setLoadFailures(paths.filter((path, index) => !readable[index]))
|
|
417
|
+
setBots(readable[0] ? data[0].bots : [])
|
|
418
|
+
setBotLoadError(!readable[0])
|
|
419
|
+
setEnrolled(readable[1] ? data[1] : [])
|
|
420
|
+
setGroups(readable[2] ? data[2] : [])
|
|
421
|
+
setCards(readable[3] ? data[3] : [])
|
|
422
|
+
setDefaults(readable[4] ? data[4] : null)
|
|
423
|
+
setExecutors(readable[5] ? data[5] : [])
|
|
424
|
+
setLocalConcurrency(readable[6] ? data[6].max_concurrency : null)
|
|
425
|
+
setRepositoryCacheLimitGib(readable[6] ? data[6].repository_cache_limit_gib ?? 10 : 10)
|
|
426
|
+
setTasks(readable[7] ? data[7].items : [])
|
|
427
|
+
let credential = null
|
|
428
|
+
try { credential = await remote?.credentials?.describe?.(['ANT_GITEA_API_TOKEN']) } catch {}
|
|
429
|
+
setTokenConfigured(Boolean(credential?.ok && credential.value?.ANT_GITEA_API_TOKEN?.configured))
|
|
430
|
+
} finally { setLoading(false) }
|
|
406
431
|
}
|
|
407
|
-
useEffect(() => { void
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
432
|
+
useEffect(() => { void load().catch((error) => setMessage(error.message)) }, [remote, revision])
|
|
433
|
+
const selectedBot = bots.find((bot) => bot.bot_id === botId)
|
|
434
|
+
const botConnected = Boolean(selectedBot?.available && selectedBot?.connected)
|
|
435
|
+
const selectedEnrollment = enrolled.find((bot) => bot.bot_fingerprint === selectedBot?.fingerprint)
|
|
436
|
+
const selectedEnrollmentBound = selectedEnrollment?.device_bound_to_current === true
|
|
437
|
+
const availableGroups = groups.filter((group) => group.bot_fingerprint === selectedBot?.fingerprint)
|
|
438
|
+
const selectedChatGroup = availableGroups.find((group) => group.group_id === chatGroupId)
|
|
439
|
+
const activeCards = cards.filter((card) => !card.revoked_at && card.expires_at * 1000 > Date.now())
|
|
440
|
+
const selectedGroupHasTasks = Boolean(selectedBot && chatGroupId && (
|
|
441
|
+
activeCards.some((card) => card.bot_fingerprint === selectedBot.fingerprint
|
|
442
|
+
&& card.conversation_kind === 'group' && card.conversation_id === chatGroupId)))
|
|
443
|
+
const connectedBots = bots.filter((bot) => bot.available && bot.connected)
|
|
444
|
+
const authorizationLoadError = loadFailures.includes('cards')
|
|
445
|
+
const enrollmentLoadError = loadFailures.includes('enrolled-bots')
|
|
446
|
+
const enrolledCount = enrolled.filter((item) => item.device_bound_to_current
|
|
447
|
+
&& connectedBots.some((bot) => bot.fingerprint === item.bot_fingerprint)).length
|
|
448
|
+
const selectedTaskCount = activeCards.filter((card) => card.bot_fingerprint === selectedBot?.fingerprint).length
|
|
449
|
+
const selectedRunnerNeedsPr = ['codex', 'claude-code'].includes(runner)
|
|
450
|
+
const codexAvailable = executors.some((item) => item.id === 'codex-readonly' && item.available)
|
|
451
|
+
const claudeCodeAvailable = executors.some((item) => item.id === 'claude-code' && item.available)
|
|
452
|
+
const nativePrUnavailableReason = executors.find((item) => item.reason)?.reason || null
|
|
453
|
+
const requesterSummary = senderScope === 'allowlist'
|
|
454
|
+
? `仅指定成员:${senderIds.trim() || '尚未填写成员 ID'}`
|
|
455
|
+
: conversationKind === 'group' ? `群 ${groupId || '尚未选择'} 的所有成员` : '任何能私聊此机器人的人'
|
|
456
|
+
const resultRouteSummary = conversationKind === 'group' ? '回复到原群(群内可见)' : '回复到发起任务的私聊'
|
|
457
|
+
const cardBlockReason = enrollmentLoadError ? '暂时无法核对机器人是否已启用协作,请刷新状态后再试。'
|
|
458
|
+
: !botConnected || !selectedEnrollmentBound ? '先在上方把机器人绑定到这台 DSH。'
|
|
459
|
+
: conversationKind === 'group' && loadFailures.includes('groups') ? '暂时无法核对目标群,请刷新状态后再试。'
|
|
460
|
+
: conversationKind === 'group' && !groupId ? '请在“参数与授权范围”中选择目标群;若列表为空,让群成员先 @机器人,再刷新群列表。'
|
|
461
|
+
: senderScope === 'allowlist' && !senderIds.trim() ? '请在“参数与授权范围”中填写可请求成员的 IM ID。'
|
|
462
|
+
: parameters.some((rule) => rule.kind === 'gitea_pr') && !tokenConfigured ? '请先在“参数与授权范围”中保存本机 Gitea 只读令牌。'
|
|
463
|
+
: parameters.some((rule) => rule.kind === 'gitea_pr') && !selectedEnrollment?.gitea_origin
|
|
464
|
+
? '请先登记这个机器人的可信 Gitea 地址。'
|
|
465
|
+
: parameters.some((rule) => rule.kind === 'gitea_pr')
|
|
466
|
+
&& normalizeServiceUrl(trustedOrigin) !== selectedEnrollment?.gitea_origin
|
|
467
|
+
? '请先保存机器人级可信 Gitea 地址;PR 参数将自动使用这个地址。'
|
|
468
|
+
: selectedRunnerNeedsPr && (parameters.length !== 1 || parameters[0].kind !== 'gitea_pr')
|
|
469
|
+
? 'Codex 和 Claude Code 目前只支持单个 Gitea PR 审查任务。'
|
|
470
|
+
: selectedRunnerNeedsPr && !parameters[0].required
|
|
471
|
+
? '本机代码审查的 Gitea PR 编号必须设为必填。'
|
|
472
|
+
: runner === 'codex' && !codexAvailable
|
|
473
|
+
? '这台 DSH 尚未找到可用的 Codex CLI,请先安装或检查 DSH 的 PATH。'
|
|
474
|
+
: runner === 'claude-code' && !claudeCodeAvailable
|
|
475
|
+
? '这台 DSH 尚未注册 Claude Code 子代理,请先在 DSH 中安装并启用它。'
|
|
476
|
+
: ''
|
|
477
|
+
const loadNames = { bots: '机器人', 'enrolled-bots': '协作启用状态', groups: '群聊列表',
|
|
478
|
+
cards: '任务授权', settings: '团队默认值', executors: '本机执行器',
|
|
479
|
+
'local-settings': '本机设置', status: '最近任务' }
|
|
480
|
+
const updateParameter = (index, patch) => setParameters((current) => current.map((rule, i) => i === index ? { ...rule, ...patch } : rule))
|
|
481
|
+
const preparePrExample = () => {
|
|
482
|
+
setTitle('审查 Gitea PR 差异')
|
|
483
|
+
setIntent('同事希望查看指定 Gitea PR 的代码差异并获得审查意见。')
|
|
484
|
+
setExamples('请帮我审查 123 号 PR\n看看刚才提到的 PR 有什么风险')
|
|
485
|
+
setTaskPrompt('结合已核验 PR 的完整代码快照和差异,给出可定位的问题;没有足够上下文时说明局限,不修改代码。')
|
|
486
|
+
setParameters([{ name: 'pr_number', kind: 'gitea_pr', required: true,
|
|
487
|
+
description: '要审查的 PR 编号', min: '1', max: '', choices: '',
|
|
488
|
+
gitea_origin: selectedEnrollment?.gitea_origin || trustedOrigin || '', repository: '' }])
|
|
489
|
+
setRunner(codexAvailable ? 'codex' : claudeCodeAvailable ? 'claude-code' : 'dsh')
|
|
490
|
+
setCreateOpen(true)
|
|
491
|
+
window.document?.getElementById('ant-new-task')?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
492
|
+
}
|
|
493
|
+
const saveGiteaToken = async () => {
|
|
494
|
+
if (!giteaToken.trim()) return
|
|
411
495
|
setBusy(true)
|
|
412
496
|
try {
|
|
413
|
-
const result = await remote.credentials.set('ANT_GITEA_API_TOKEN',
|
|
414
|
-
if (!result?.ok) throw new Error('DSH 无法安全保存 Gitea
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
setMessage('Gitea 访问令牌已保存在这台 DSH。')
|
|
497
|
+
const result = await remote.credentials.set('ANT_GITEA_API_TOKEN', giteaToken.trim())
|
|
498
|
+
if (!result?.ok) throw new Error('DSH 无法安全保存 Gitea 令牌。')
|
|
499
|
+
setGiteaToken(''); setTokenConfigured(true)
|
|
500
|
+
setMessage('Gitea 令牌已保存在这台 DSH,不会提交给 ANT 服务端。')
|
|
418
501
|
} catch (error) { setMessage(error.message) }
|
|
419
502
|
finally { setBusy(false) }
|
|
420
503
|
}
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
if (
|
|
426
|
-
setMessage('
|
|
427
|
-
return
|
|
428
|
-
}
|
|
429
|
-
if (conversationKind === 'group' && !groups.some((item) => item.bot_fingerprint === bot.fingerprint && item.group_id === groupId)) {
|
|
430
|
-
setMessage('请先让群成员在目标群 @ 机器人一次,然后选择待启用群。')
|
|
431
|
-
return
|
|
432
|
-
}
|
|
433
|
-
if (normalizeServiceUrl(origin) !== selectedEnrollment.gitea_origin) {
|
|
434
|
-
setMessage('当前地址与机器人已登记的可信 Gitea 地址不同。请先点击“更新可信地址”,或恢复原地址。')
|
|
504
|
+
const enrollBot = async () => {
|
|
505
|
+
if (!selectedBot) return
|
|
506
|
+
const requestedOrigin = trustedOrigin
|
|
507
|
+
const origin = requestedOrigin?.trim() ? normalizeServiceUrl(requestedOrigin) : ''
|
|
508
|
+
if (requestedOrigin?.trim() && !origin) {
|
|
509
|
+
setMessage('请填写有效的 HTTPS Gitea 地址后再启用协作。')
|
|
435
510
|
return
|
|
436
511
|
}
|
|
437
|
-
const trustedOrigin = selectedEnrollment.gitea_origin
|
|
438
|
-
if (!window.confirm(conversationKind === 'group'
|
|
439
|
-
? `确认允许所选群的成员请求 ${trustedOrigin} 上 ${repository} PR #${prNumber} 的只读审查?结果将公开回复原群/原话题,直到你撤销或授权到期。目前没有每日请求次数上限。`
|
|
440
|
-
: `开启后,此飞书机器人将由 ANT 接收消息,只处理你授权的 PR 审查;它不再运行 DSH-IM 的普通聊天。Gitea 令牌会发送到 ${origin},请核对地址。目前没有每日请求次数上限。请确认这是专用协作机器人。`)) return
|
|
441
512
|
setBusy(true)
|
|
442
513
|
try {
|
|
443
|
-
const response = await fetch('/api/ant-teams/delegation/
|
|
514
|
+
const response = await fetch('/api/ant-teams/delegation/enrolled-bots', {
|
|
444
515
|
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
445
|
-
body: JSON.stringify({ bot_id:
|
|
446
|
-
gitea_origin:
|
|
447
|
-
sender_ids: senderScope === 'allowlist' ? senderIds.split(/[\s,,]+/u).filter(Boolean) : [],
|
|
448
|
-
conversation_kind: conversationKind,
|
|
449
|
-
...(conversationKind === 'group' ? { conversation_id: groupId } : {}),
|
|
450
|
-
expires_at: Math.floor(Date.now() / 1000) + Number(days) * 86400 }),
|
|
516
|
+
body: JSON.stringify({ bot_id: selectedBot.bot_id, bot_fingerprint: selectedBot.fingerprint,
|
|
517
|
+
gitea_origin: origin }),
|
|
451
518
|
})
|
|
452
519
|
const result = await readLocalJsonResponse(response)
|
|
453
|
-
if (!response.ok) throw new Error(result.message || '
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
: '委托已保存;机器人连接后可私聊发送“审查 PR 编号”试一次,以核对 Gitea 令牌和实际回复。')
|
|
458
|
-
await refresh()
|
|
520
|
+
if (!response.ok) throw new Error(result.message || '协作机器人启用失败。')
|
|
521
|
+
await load()
|
|
522
|
+
if (origin) setTrustedOrigin(origin)
|
|
523
|
+
setMessage('协作机器人已启用。你可以开放能力;私聊时也能正常聊天。')
|
|
459
524
|
} catch (error) { setMessage(error.message) }
|
|
460
525
|
finally { setBusy(false) }
|
|
461
526
|
}
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
if (!
|
|
467
|
-
if (!window.confirm('此机器人会切换为 ANT 独占收件模式,停止 DSH-IM 原有普通聊天。更换机器人账号或可信 Gitea 地址时,不匹配的旧授权会撤销。请确认这是专用协作机器人。')) return
|
|
527
|
+
const saveTrustedOrigin = async () => {
|
|
528
|
+
if (!selectedBot || !selectedEnrollmentBound) return
|
|
529
|
+
const origin = normalizeServiceUrl(trustedOrigin)
|
|
530
|
+
if (!origin) { setMessage('请填写有效的 HTTPS Gitea 地址。'); return }
|
|
531
|
+
if (!window.confirm(`将这个机器人的可信 Gitea 地址设为 ${origin}?已开放能力中的 PR 参数需要重新核对。`)) return
|
|
468
532
|
setBusy(true)
|
|
469
533
|
try {
|
|
470
534
|
const response = await fetch('/api/ant-teams/delegation/enrolled-bots', {
|
|
471
535
|
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
472
|
-
body: JSON.stringify({ bot_id:
|
|
473
|
-
gitea_origin:
|
|
536
|
+
body: JSON.stringify({ bot_id: selectedBot.bot_id, bot_fingerprint: selectedBot.fingerprint,
|
|
537
|
+
gitea_origin: origin }),
|
|
538
|
+
})
|
|
539
|
+
const result = await readLocalJsonResponse(response)
|
|
540
|
+
if (!response.ok) throw new Error(result.message || '可信地址保存失败。')
|
|
541
|
+
await load(); setTrustedOrigin(origin); setMessage('可信 Gitea 地址已保存。')
|
|
542
|
+
} catch (error) { setMessage(error.message) }
|
|
543
|
+
finally { setBusy(false) }
|
|
544
|
+
}
|
|
545
|
+
const issueBindingCode = async () => {
|
|
546
|
+
if (!selectedBot || !selectedEnrollmentBound) return
|
|
547
|
+
setBusy(true)
|
|
548
|
+
try {
|
|
549
|
+
const response = await fetch('/api/ant-teams/delegation/owner-challenge', {
|
|
550
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
551
|
+
body: JSON.stringify({ bot_id: selectedBot.bot_id, bot_fingerprint: selectedBot.fingerprint }),
|
|
552
|
+
})
|
|
553
|
+
const result = await readLocalJsonResponse(response)
|
|
554
|
+
if (!response.ok) throw new Error(result.message || '无法获取身份绑定码。')
|
|
555
|
+
setBindingCode(result.code)
|
|
556
|
+
setMessage('绑定码 5 分钟有效。请你本人私聊这个机器人完成绑定。')
|
|
557
|
+
} catch (error) { setMessage(error.message) }
|
|
558
|
+
finally { setBusy(false) }
|
|
559
|
+
}
|
|
560
|
+
const saveGroupChat = async () => {
|
|
561
|
+
if (!selectedBot || !selectedEnrollmentBound || !chatGroupId.trim()) return
|
|
562
|
+
const enabled = !selectedChatGroup?.chat_enabled
|
|
563
|
+
if (!enabled && selectedGroupHasTasks) {
|
|
564
|
+
setMessage('这个群还有开放能力。请先停止这些能力,再关闭群聊。')
|
|
565
|
+
return
|
|
566
|
+
}
|
|
567
|
+
if (!window.confirm(enabled
|
|
568
|
+
? selectedGroupHasTasks
|
|
569
|
+
? '这个群当前因任务授权可以聊天。要在任务到期后仍允许普通聊天吗?聊天会使用团队模型额度。'
|
|
570
|
+
: '允许这个群与机器人自然聊天?群成员仍不能执行未开放的能力,聊天会使用团队模型额度。'
|
|
571
|
+
: '关闭这个群的普通聊天?已有能力不会自动撤销。')) return
|
|
572
|
+
setBusy(true)
|
|
573
|
+
try {
|
|
574
|
+
const response = await fetch('/api/ant-teams/delegation/groups', {
|
|
575
|
+
method: 'PUT', headers: { 'content-type': 'application/json' },
|
|
576
|
+
body: JSON.stringify({ bot_id: selectedBot.bot_id,
|
|
577
|
+
bot_fingerprint: selectedBot.fingerprint, group_id: chatGroupId.trim(), enabled }),
|
|
474
578
|
})
|
|
475
579
|
if (!response.ok) {
|
|
476
580
|
const result = await readLocalJsonResponse(response)
|
|
477
|
-
throw new Error(result.message || '
|
|
581
|
+
throw new Error(result.message || '群聊设置保存失败。')
|
|
478
582
|
}
|
|
479
|
-
|
|
480
|
-
setMessage('
|
|
481
|
-
await refresh()
|
|
583
|
+
await load()
|
|
584
|
+
setMessage(enabled ? '这个群现在可以与机器人聊天;可执行能力仍需逐项开放。' : '已关闭这个群的普通聊天。')
|
|
482
585
|
} catch (error) { setMessage(error.message) }
|
|
483
586
|
finally { setBusy(false) }
|
|
484
587
|
}
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
if (!bot || !window.confirm('停止此机器人的 ANT 群聊接收,并撤销它的全部委托授权?若需恢复普通聊天,还要在 DSH-IM 中切回普通模式。')) return
|
|
588
|
+
const saveLocalProcessingSettings = async (event) => {
|
|
589
|
+
event.preventDefault()
|
|
488
590
|
setBusy(true)
|
|
489
591
|
try {
|
|
490
|
-
const response = await fetch('/api/ant-teams/delegation/
|
|
491
|
-
method: '
|
|
592
|
+
const response = await fetch('/api/ant-teams/delegation/local-settings', {
|
|
593
|
+
method: 'PUT', headers: { 'content-type': 'application/json' },
|
|
594
|
+
body: JSON.stringify({
|
|
595
|
+
max_concurrency: localConcurrency === '' ? null : Number(localConcurrency),
|
|
596
|
+
repository_cache_limit_gib: Number(repositoryCacheLimitGib),
|
|
597
|
+
}),
|
|
492
598
|
})
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
599
|
+
const result = await readLocalJsonResponse(response)
|
|
600
|
+
if (!response.ok) throw new Error(result.message || '本机处理设置保存失败。')
|
|
601
|
+
setLocalConcurrency(result.max_concurrency)
|
|
602
|
+
setRepositoryCacheLimitGib(result.repository_cache_limit_gib)
|
|
603
|
+
setMessage('本机并发与仓库缓存设置已保存。')
|
|
497
604
|
} catch (error) { setMessage(error.message) }
|
|
498
605
|
finally { setBusy(false) }
|
|
499
606
|
}
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
if (!
|
|
607
|
+
const createCard = async (event) => {
|
|
608
|
+
event.preventDefault()
|
|
609
|
+
if (!selectedBot || !botConnected || !selectedEnrollmentBound) { setMessage('请先在这台 DSH 绑定选中的协作机器人。'); return }
|
|
610
|
+
if (!title.trim() || !intent.trim() || !taskPrompt.trim()) {
|
|
611
|
+
setMessage('请填写能力名称、适用请求说明和处理要求。'); return
|
|
612
|
+
}
|
|
613
|
+
const bytes = (value) => new TextEncoder().encode(value).length
|
|
614
|
+
if (bytes(title.trim()) > 100 || bytes(intent.trim()) > 1000 || bytes(taskPrompt.trim()) > 4000) {
|
|
615
|
+
setMessage('能力名称、请求说明或处理要求过长,请缩短后再提交。'); return
|
|
616
|
+
}
|
|
617
|
+
const exampleLines = examples.split('\n').map((item) => item.trim()).filter(Boolean)
|
|
618
|
+
if (exampleLines.length > 8 || exampleLines.some((item) => bytes(item) > 500)) {
|
|
619
|
+
setMessage('同事示例最多写 8 句,每句请保持简短。'); return
|
|
620
|
+
}
|
|
621
|
+
const normalized = parameters.map((rule) => ({ name: rule.name.trim(), kind: rule.kind,
|
|
622
|
+
description: rule.description.trim() || null,
|
|
623
|
+
required: rule.required, min: rule.min === '' ? null : Number(rule.min),
|
|
624
|
+
max: rule.max === '' ? null : Number(rule.max),
|
|
625
|
+
choices: rule.kind === 'choice' ? rule.choices.split(/[,,\n]/u).map((item) => item.trim()).filter(Boolean) : null,
|
|
626
|
+
gitea_origin: rule.kind === 'gitea_pr' ? selectedEnrollment?.gitea_origin || null : null,
|
|
627
|
+
repository: rule.kind === 'gitea_pr' ? rule.repository.trim().toLowerCase() : null }))
|
|
628
|
+
if (normalized.some((rule) => !/^[A-Za-z][A-Za-z0-9_]{0,31}$/u.test(rule.name))) {
|
|
629
|
+
setMessage('参数键请用英文字母开头,只能包含英文字母、数字和下划线;给同事看的中文问题可填在“参数说明”。'); return
|
|
630
|
+
}
|
|
631
|
+
if (new Set(normalized.map((rule) => rule.name)).size !== normalized.length) {
|
|
632
|
+
setMessage('参数键不能重复,请为每项参数填写不同的英文键。'); return
|
|
633
|
+
}
|
|
634
|
+
if (normalized.some((rule) => rule.description && bytes(rule.description) > 100)) {
|
|
635
|
+
setMessage('参数说明过长,请缩短给同事看的问题。'); return
|
|
636
|
+
}
|
|
637
|
+
if (normalized.some((rule) => rule.kind === 'choice' && (!rule.choices?.length || rule.choices.length > 32
|
|
638
|
+
|| rule.choices.some((choice) => bytes(choice) > 100)
|
|
639
|
+
|| new Set(rule.choices).size !== rule.choices.length))) {
|
|
640
|
+
setMessage('选项参数请填写 1–32 个不重复的可选值,每项保持简短。'); return
|
|
641
|
+
}
|
|
642
|
+
if (normalized.some((rule) => (rule.min != null && !Number.isInteger(rule.min))
|
|
643
|
+
|| (rule.max != null && !Number.isInteger(rule.max))
|
|
644
|
+
|| (rule.min != null && rule.max != null && rule.min > rule.max)
|
|
645
|
+
|| (rule.kind === 'text' && rule.max != null && (rule.max < 1 || rule.max > 4096))
|
|
646
|
+
|| (rule.kind === 'gitea_pr' && ((rule.min != null && rule.min < 1)
|
|
647
|
+
|| (rule.max != null && rule.max > 1_000_000_000))))) {
|
|
648
|
+
setMessage('参数范围无效:最小值不能大于最大值;文本最长 4096 字节,PR 编号须为正整数。'); return
|
|
649
|
+
}
|
|
650
|
+
if (normalized.some((rule) => rule.kind === 'gitea_pr' && (!rule.gitea_origin || !rule.repository))) {
|
|
651
|
+
setMessage('PR 参数还需要填写可信 Gitea 地址和仓库。'); return
|
|
652
|
+
}
|
|
653
|
+
const validRepository = (value) => {
|
|
654
|
+
const parts = value.split('/')
|
|
655
|
+
return parts.length === 2 && parts.every((part) => part !== '.' && part !== '..'
|
|
656
|
+
&& /^[A-Za-z0-9_.-]{1,100}$/u.test(part))
|
|
657
|
+
}
|
|
658
|
+
if (normalized.some((rule) => rule.kind === 'gitea_pr' && !validRepository(rule.repository))) {
|
|
659
|
+
setMessage('Gitea 仓库请填写“团队名/仓库名”,不要包含空格或多余路径。'); return
|
|
660
|
+
}
|
|
661
|
+
if (['codex', 'claude-code'].includes(runner) && (normalized.length !== 1
|
|
662
|
+
|| normalized[0].kind !== 'gitea_pr' || !normalized[0].required)) {
|
|
663
|
+
setMessage('Codex 和 Claude Code 目前需要一个必填的 Gitea PR 编号参数。'); return
|
|
664
|
+
}
|
|
665
|
+
if (runner === 'codex' && !codexAvailable) {
|
|
666
|
+
setMessage('这台 DSH 尚未找到可用的 Codex CLI。'); return
|
|
667
|
+
}
|
|
668
|
+
if (runner === 'claude-code' && !claudeCodeAvailable) {
|
|
669
|
+
setMessage('这台 DSH 尚未注册 Claude Code 子代理。'); return
|
|
670
|
+
}
|
|
671
|
+
const scope = conversationKind === 'group' ? `群 ${groupId}` : '私聊'
|
|
672
|
+
const resultDestination = conversationKind === 'group' ? '原群(群内可见)' : '发起任务的私聊'
|
|
673
|
+
const executionInfo = runner === 'codex'
|
|
674
|
+
? 'ANT 会使用这台电脑已登录的 Codex,在已核验 PR Head 的临时完整代码快照中固定只读权限;会传入所有者要求、请求人原话和 PR 差异,不传 Gitea 令牌。'
|
|
675
|
+
: runner === 'claude-code'
|
|
676
|
+
? 'ANT 会调用这台 DSH 已注册的 Claude Code 子代理,在已核验 PR Head 的临时完整代码快照中审查;会传入所有者要求、请求人原话和 PR 差异,不传 Gitea 令牌。它沿用 DSH 中 Claude Code 的本机权限设置;请确认该设置适合由获准成员请求审查。'
|
|
677
|
+
: '这项任务仅由受限 DSH 处理已提供的文字。'
|
|
678
|
+
if (!window.confirm(`开放“${title}”后,${requesterSummary}可在${scope}请求,有效期 ${days} 天;结果${resultDestination}。${executionInfo}请确认。`)) return
|
|
503
679
|
setBusy(true)
|
|
504
680
|
try {
|
|
505
|
-
const response = await fetch('/api/ant-teams/delegation/
|
|
681
|
+
const response = await fetch('/api/ant-teams/delegation/cards', {
|
|
506
682
|
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
507
|
-
body: JSON.stringify({ bot_id:
|
|
683
|
+
body: JSON.stringify({ bot_id: selectedBot.bot_id, bot_fingerprint: selectedBot.fingerprint,
|
|
684
|
+
title, intent, examples: exampleLines,
|
|
685
|
+
parameters: normalized, task_prompt: taskPrompt, runner, workspace: null,
|
|
686
|
+
sender_scope: senderScope, sender_ids: senderScope === 'allowlist'
|
|
687
|
+
? senderIds.split(/[\s,,]+/u).filter(Boolean) : [], conversation_kind: conversationKind,
|
|
688
|
+
...(conversationKind === 'group' ? { conversation_id: groupId } : {}),
|
|
689
|
+
expires_at: Math.floor(Date.now() / 1000) + Number(days) * 86400 }),
|
|
508
690
|
})
|
|
509
691
|
const result = await readLocalJsonResponse(response)
|
|
510
|
-
if (!response.ok
|
|
511
|
-
|
|
512
|
-
|
|
692
|
+
if (!response.ok && runner === 'claude-code' && result.code === 'INVALID_REQUEST') {
|
|
693
|
+
throw new Error('当前团队服务端可能尚不支持 Claude Code 执行方。请管理员升级服务端后刷新重试。')
|
|
694
|
+
}
|
|
695
|
+
if (!response.ok) throw new Error(result.message || '能力保存失败。')
|
|
696
|
+
setTitle(''); setIntent(''); setExamples(''); setTaskPrompt(''); setParameters([]); setRunner('dsh')
|
|
697
|
+
await load()
|
|
698
|
+
setCreateOpen(false)
|
|
699
|
+
setMessage('能力已开放。同事可自然地提出这类请求;参数不完整时机器人会追问。')
|
|
513
700
|
} catch (error) { setMessage(error.message) }
|
|
514
701
|
finally { setBusy(false) }
|
|
515
702
|
}
|
|
516
|
-
const
|
|
517
|
-
if (!window.confirm(
|
|
703
|
+
const revokeCard = async (card) => {
|
|
704
|
+
if (!window.confirm(`停止开放“${card.title}”?正在处理的任务无法撤销已经发生的读取。`)) return
|
|
518
705
|
setBusy(true)
|
|
519
706
|
try {
|
|
520
|
-
const response = await fetch('/api/ant-teams/delegation/
|
|
521
|
-
method: 'DELETE', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id:
|
|
707
|
+
const response = await fetch('/api/ant-teams/delegation/cards', {
|
|
708
|
+
method: 'DELETE', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: card.id }),
|
|
522
709
|
})
|
|
523
|
-
if (!response.ok) throw new Error('
|
|
524
|
-
setMessage('
|
|
525
|
-
await refresh()
|
|
710
|
+
if (!response.ok) throw new Error('停止能力失败,请重试。')
|
|
711
|
+
await load(); setMessage('能力已停止开放。')
|
|
526
712
|
} catch (error) { setMessage(error.message) }
|
|
527
713
|
finally { setBusy(false) }
|
|
528
714
|
}
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
h('
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
h('
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
715
|
+
const cancelTask = async (task) => {
|
|
716
|
+
if (!window.confirm(`取消任务 ${task.task_id}?已经发生的读取不能撤回。`)) return
|
|
717
|
+
setBusy(true)
|
|
718
|
+
try {
|
|
719
|
+
const response = await fetch('/api/ant-teams/delegation/cancel', { method: 'POST',
|
|
720
|
+
headers: { 'content-type': 'application/json' }, body: JSON.stringify({ task_id: task.task_id }) })
|
|
721
|
+
const result = await readLocalJsonResponse(response)
|
|
722
|
+
if (!response.ok || !result.canceled) throw new Error(result.message || '任务已结束或无法取消,请刷新状态。')
|
|
723
|
+
await load(); setMessage('已请求取消任务。')
|
|
724
|
+
} catch (error) { setMessage(error.message) }
|
|
725
|
+
finally { setBusy(false) }
|
|
726
|
+
}
|
|
727
|
+
return h('section', { style: panelStyle, 'aria-label': '协作机器人与开放能力' },
|
|
728
|
+
h('h3', { style: { margin: 0, fontSize: 16 } }, '协作机器人'),
|
|
729
|
+
h('p', { style: mutedTextStyle }, '同事可以像平常一样和机器人聊天。DSH 负责理解与回答;只有准备执行你开放的能力时,ANT 才核对请求人、群、参数和权限。群里的任务结果会回复原群。'),
|
|
730
|
+
h('p', { role: 'status', style: mutedTextStyle }, '每项能力都单独设定执行方和请求范围。普通对话不会自动获得读取电脑、调用工具或访问其他群的权限。'),
|
|
731
|
+
loadFailures.length ? h('p', { role: 'status', style: mutedTextStyle },
|
|
732
|
+
`部分协作信息暂时无法读取:${loadFailures.map((path) => loadNames[path]).join('、')}。已读取的信息仍显示,请稍后刷新状态。`) : null,
|
|
733
|
+
h('div', { style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, padding: 12,
|
|
734
|
+
display: 'grid', gap: 8 } },
|
|
735
|
+
h('strong', null, '机器人和聊天范围'),
|
|
736
|
+
h('span', { role: 'status', style: mutedTextStyle }, loading ? '正在读取机器人与任务状态…'
|
|
737
|
+
: botLoadError ? '暂时无法读取机器人状态。请刷新后再试。'
|
|
738
|
+
: connectedBots.length === 0 ? '尚未发现已连接的机器人。请先在 DSH-IM 中连接飞书机器人。'
|
|
739
|
+
: selectedBot && !botConnected ? `“${selectedBot.name}”当前离线;请先在 DSH-IM 恢复连接。`
|
|
740
|
+
: enrollmentLoadError ? selectedBot
|
|
741
|
+
? `“${selectedBot.name}”已连接;暂时无法确认协作是否已启用,请刷新状态。`
|
|
742
|
+
: '机器人已连接;协作启用状态暂未读取,请刷新状态。'
|
|
743
|
+
: selectedBot && !selectedEnrollmentBound ? selectedEnrollment
|
|
744
|
+
? `“${selectedBot.name}”已连接;原协作配置尚未绑定到这台 DSH,请在本机重新启用。若原设备令牌曾被撤销,旧任务权限已关闭,绑定后需重新开放。`
|
|
745
|
+
: `“${selectedBot.name}”已连接;请先启用本机协作。启用后,它仍能正常聊天。`
|
|
746
|
+
: enrolledCount === 0 ? '机器人已连接;请选择并启用协作。启用后,它仍能正常聊天。'
|
|
747
|
+
: !selectedBot ? '已有协作机器人;请选择一台,查看已开放的能力并私聊自测。'
|
|
748
|
+
: authorizationLoadError ? `“${selectedBot.name}”已启用协作;已开放任务暂未读取,请刷新状态。普通聊天仍可自测。`
|
|
749
|
+
: selectedTaskCount === 0 ? `“${selectedBot.name}”已启用协作;下一步开放一项能力。你也可以先私聊自测普通聊天。`
|
|
750
|
+
: `“${selectedBot.name}”已启用协作 · ${selectedTaskCount} 项能力已开放。可以私聊自测。`),
|
|
751
|
+
bots.length ? h('label', null, '我的机器人', h('select', { value: botId,
|
|
752
|
+
onChange: (event) => { setBotId(event.target.value); setGroupId(''); setChatGroupId(''); setBindingCode('');
|
|
753
|
+
setTrustedOrigin(enrolled.find((bot) => bot.bot_id === event.target.value)?.gitea_origin || '') }, style: inputStyle },
|
|
754
|
+
h('option', { value: '' }, '请选择机器人'),
|
|
755
|
+
...bots.map((bot) => h('option', { key: bot.bot_id, value: bot.bot_id,
|
|
756
|
+
disabled: !bot.available || !bot.connected }, `${bot.name}${bot.available && bot.connected ? '' : '(未连接)'}`)))) : null,
|
|
757
|
+
connectedBots.length === 0 && !loading && !botLoadError ? h('button', { type: 'button',
|
|
758
|
+
onClick: openImSettings, style: secondaryButtonStyle }, '打开 DSH-IM 设置') : null,
|
|
759
|
+
connectedBots.length === 0 && !loading && !botLoadError ? h('span', { style: mutedTextStyle },
|
|
760
|
+
'若尚未安装 DSH-IM,请先到 DSH 的插件列表安装并连接机器人。') : null,
|
|
761
|
+
selectedBot && !botConnected && connectedBots.length ? h('button', { type: 'button', onClick: openImSettings,
|
|
762
|
+
style: secondaryButtonStyle }, '检查 DSH-IM 连接') : null,
|
|
763
|
+
selectedBot && !selectedEnrollmentBound ? h('button', { type: 'button', disabled: busy || !botConnected || enrollmentLoadError,
|
|
764
|
+
onClick: () => void enrollBot(), style: secondaryButtonStyle },
|
|
765
|
+
selectedEnrollment ? '绑定原协作配置到这台 DSH' : '启用并绑定到这台 DSH') : null,
|
|
766
|
+
selectedEnrollmentBound && botConnected ? h('p', { style: mutedTextStyle },
|
|
767
|
+
`自测:私聊“${selectedBot.name}”发送“你能做什么”;发起任务后可问“刚才的任务怎么样了”。群聊请先在目标群 @机器人,再开放能力时选择该群。`) : null,
|
|
768
|
+
selectedEnrollmentBound && botConnected ? h('div', { style: { display: 'grid', gap: 8 } },
|
|
769
|
+
h('strong', null, '允许聊天的群'),
|
|
770
|
+
h('span', { style: mutedTextStyle }, '群成员可以正常聊天;是否能让机器人办事,由每项能力单独决定。若机器人尚未发现群,可以直接填群 ID。'),
|
|
771
|
+
h('select', { 'aria-label': '已记录的群', value: availableGroups.some((group) => group.group_id === chatGroupId) ? chatGroupId : '',
|
|
772
|
+
onChange: (event) => setChatGroupId(event.target.value), style: inputStyle },
|
|
773
|
+
h('option', { value: '' }, '从已记录的群中选择'),
|
|
774
|
+
...availableGroups.map((group) => h('option', { key: group.group_id, value: group.group_id },
|
|
775
|
+
`${group.group_code} · ${group.chat_enabled ? '已允许聊天' : '未允许聊天'}`))),
|
|
776
|
+
h('input', { 'aria-label': '群 ID', value: chatGroupId, maxLength: 256,
|
|
777
|
+
onChange: (event) => setChatGroupId(event.target.value.trim()),
|
|
778
|
+
placeholder: '也可以手动填写群 ID', style: inputStyle }),
|
|
779
|
+
h('button', { type: 'button', disabled: busy || !chatGroupId.trim()
|
|
780
|
+
|| (selectedChatGroup?.chat_enabled && selectedGroupHasTasks),
|
|
781
|
+
onClick: () => void saveGroupChat(), style: secondaryButtonStyle },
|
|
782
|
+
selectedChatGroup?.chat_enabled ? '关闭此群聊天'
|
|
783
|
+
: selectedGroupHasTasks ? '能力到期后仍允许聊天' : '允许此群聊天'),
|
|
784
|
+
selectedChatGroup?.chat_enabled && selectedGroupHasTasks ? h('span', { style: mutedTextStyle },
|
|
785
|
+
'本群还有开放能力;关闭聊天前请先停止这些能力。') : null) : null,
|
|
786
|
+
h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } },
|
|
787
|
+
h('button', { type: 'button', onClick: () => { setCreateOpen(true)
|
|
788
|
+
window.document?.getElementById('ant-new-task')?.scrollIntoView({ behavior: 'smooth', block: 'start' }) },
|
|
789
|
+
style: primaryButtonStyle }, '开放新能力'),
|
|
790
|
+
h('button', { type: 'button', onClick: preparePrExample, style: secondaryButtonStyle }, '使用 PR 差异审查示例'),
|
|
791
|
+
h('button', { type: 'button', onClick: () => void load().catch((error) => setMessage(error.message)),
|
|
792
|
+
style: secondaryButtonStyle }, '刷新状态'))),
|
|
793
|
+
h('h4', { style: { marginBottom: 0 } }, `我开放的能力 · ${authorizationLoadError ? '暂未读取' : activeCards.length}`),
|
|
794
|
+
...activeCards.map((card) => h('div', { key: card.id, style: { display: 'flex', gap: 12,
|
|
795
|
+
alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: 8 } },
|
|
796
|
+
h('span', { style: { flex: '1 1 280px', minWidth: 0, overflowWrap: 'anywhere' } }, `${card.title} · ${bots.find((bot) => bot.fingerprint === card.bot_fingerprint)?.name || '机器人'} · ${card.conversation_kind === 'group' ? `群 ${groups.find((group) => group.group_id === card.conversation_id)?.group_code || card.conversation_id}` : '私聊'} · ${card.sender_scope === 'allowlist' ? '仅指定成员可请求' : card.conversation_kind === 'group' ? '群成员可请求' : '可私聊者可请求'} · 结果回复原${card.conversation_kind === 'group' ? '群' : '私聊'} · ${card.runner === 'codex' ? 'Codex 只读 PR 完整快照审查' : card.runner === 'claude-code' ? 'Claude Code PR 完整快照审查试用' : 'DSH 文字处理'} · ${new Date(card.expires_at * 1000).toLocaleString('zh-CN')} 到期`),
|
|
797
|
+
h('button', { type: 'button', disabled: busy, onClick: () => void revokeCard(card), style: secondaryButtonStyle }, '停止'))),
|
|
798
|
+
activeCards.length === 0 ? h('p', { style: mutedTextStyle },
|
|
799
|
+
authorizationLoadError ? '暂时无法确认已开放的能力,请刷新状态后再试。'
|
|
800
|
+
: '还没有开放能力。先选机器人,再开放一项能力;PR 审查只是可选示例。') : null,
|
|
801
|
+
h('h4', { style: { marginBottom: 0 } }, '最近任务'),
|
|
802
|
+
...(tasks.filter((task) => task.task_id).length ? tasks.filter((task) => task.task_id)
|
|
803
|
+
.map((task) => h('div', { key: task.task_id, style: { display: 'flex', gap: 12, alignItems: 'center',
|
|
804
|
+
justifyContent: 'space-between', flexWrap: 'wrap', borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: 8 } },
|
|
805
|
+
h('span', { style: { flex: '1 1 280px', minWidth: 0, overflowWrap: 'anywhere' } }, `${task.task_id} · ${task.title || (task.pr_number ? `PR #${task.pr_number} 差异审查` : '任务')} · ${task.outcome === 'canceled' ? '已取消' : task.outcome === 'failed' ? '未完成' : ({
|
|
806
|
+
pending: '排队中', running: '处理中', reply_pending: '结果待发送', reply_attempted: '回复状态待核对', done: '已回复',
|
|
807
|
+
}[task.status] || task.status)}`),
|
|
808
|
+
['card_task', 'legacy_pr_task'].includes(task.kind) && ['pending', 'running'].includes(task.status) ? h('button', { type: 'button', disabled: busy,
|
|
809
|
+
onClick: () => void cancelTask(task), style: secondaryButtonStyle }, '取消任务') : null))
|
|
810
|
+
: [h('p', { key: 'none', style: mutedTextStyle }, loadFailures.includes('status')
|
|
811
|
+
? '任务记录暂时无法读取,请刷新状态。' : '暂无任务。')]),
|
|
812
|
+
h('details', { id: 'ant-new-task', open: createOpen,
|
|
813
|
+
onToggle: (event) => setCreateOpen(event.currentTarget.open),
|
|
814
|
+
style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, padding: 12 } },
|
|
815
|
+
h('summary', { style: { cursor: 'pointer', fontWeight: 600 } }, '开放一项能力'),
|
|
816
|
+
h('form', { onSubmit: createCard, style: { display: 'grid', gap: 9, maxWidth: 760 } },
|
|
817
|
+
h('p', { style: mutedTextStyle }, selectedBot
|
|
818
|
+
? `当前机器人:${selectedBot.name}${selectedEnrollmentBound ? ' · 已绑定此设备' : ' · 请先绑定到此设备'}`
|
|
819
|
+
: '请先在上方选择机器人并启用协作。'),
|
|
820
|
+
selectedEnrollmentBound ? h('p', { style: mutedTextStyle },
|
|
821
|
+
'机器人及其任务能力只在这台 DSH 设备接收和执行;同一账号的其他设备不会继承本机授权。') : null,
|
|
822
|
+
selectedEnrollment && !selectedEnrollmentBound ? h('p', { style: mutedTextStyle },
|
|
823
|
+
'这是尚未绑定到设备的机器人。请在要继续使用它的电脑重新启用;Gitea 地址和配置记录会保留。若旧设备访问曾被撤销,原任务权限不会恢复,需要重新开放。') : null,
|
|
824
|
+
selectedBot && conversationKind === 'group' && !selectedBot.history_available ? h('p', { role: 'status', style: mutedTextStyle },
|
|
825
|
+
'这个机器人当前不能读取平台群历史;群聊只能参考实际送达的消息和本机近期记录。请检查 DSH-IM 权限。') : null,
|
|
826
|
+
h('label', null, '能力名称', h('input', { required: true, maxLength: 100, value: title,
|
|
827
|
+
onChange: (event) => setTitle(event.target.value), placeholder: '例如:总结项目进度', style: inputStyle })),
|
|
828
|
+
h('label', null, '什么请求属于这项能力', h('textarea', { required: true, maxLength: 1000, rows: 2, value: intent,
|
|
829
|
+
onChange: (event) => setIntent(event.target.value), placeholder: '例如:把群里讨论的本周进展整理成摘要', style: inputStyle })),
|
|
830
|
+
h('label', null, '同事可能怎么说(每行一句,可选)', h('textarea', { rows: 2, value: examples,
|
|
831
|
+
onChange: (event) => setExamples(event.target.value), placeholder: '帮我总结一下刚才的讨论', style: inputStyle })),
|
|
832
|
+
h('label', null, '处理要求', h('textarea', { required: true, maxLength: 4000, rows: 3, value: taskPrompt,
|
|
833
|
+
onChange: (event) => setTaskPrompt(event.target.value), placeholder: '只使用当前会话可见的内容,列出结论和待办', style: inputStyle })),
|
|
834
|
+
h('label', null, '执行方', h('select', { value: runner,
|
|
835
|
+
onChange: (event) => setRunner(event.target.value), style: inputStyle },
|
|
836
|
+
h('option', { value: 'dsh' }, 'DSH · 已提供文字'),
|
|
837
|
+
h('option', { value: 'codex',
|
|
838
|
+
disabled: !codexAvailable }, 'Codex · 单个 Gitea PR 完整快照,只读'),
|
|
839
|
+
h('option', { value: 'claude-code', disabled: !claudeCodeAvailable },
|
|
840
|
+
'Claude Code · 单个 Gitea PR 完整快照试用'))),
|
|
841
|
+
nativePrUnavailableReason ? h('p', { role: 'status', style: mutedTextStyle },
|
|
842
|
+
`${nativePrUnavailableReason},已禁用 Codex/Claude Code 完整快照选项;可选择 DSH 差异审查。`) : null,
|
|
843
|
+
runner === 'codex' ? h('p', { role: 'status', style: mutedTextStyle },
|
|
844
|
+
'Codex 使用这台电脑的登录账号及其用量;ANT 为每项能力维护仓库缓存,每次审查创建固定到 PR Head 的临时代码快照。Codex 只能只读查看该快照;不会收到 Gitea 令牌。Git LFS 实际文件不会下载。') : null,
|
|
845
|
+
runner === 'claude-code' ? h('p', { role: 'status', style: mutedTextStyle },
|
|
846
|
+
'Claude Code 通过 DSH 已注册的子代理运行,可查看本次 PR 的临时代码快照。ANT 不会传 Gitea 令牌;Claude Code 沿用 DSH 提供方的本机权限,不具备 Codex 路径相同的固定只读隔离。Git LFS 实际文件不会下载。') : null,
|
|
847
|
+
selectedRunnerNeedsPr ? h('p', { role: 'status', style: mutedTextStyle },
|
|
848
|
+
'本机 PR 执行方需要 Gitea granular token 的只读权限 read:repository 和 read:user;read:user 仅用于这台 DSH 获取 Git HTTPS 登录名。') : null,
|
|
849
|
+
h('p', { role: 'status', style: mutedTextStyle },
|
|
850
|
+
`开放后:${requesterSummary}可请求 · 有效期 ${days} 天 · ${resultRouteSummary}`),
|
|
851
|
+
h('details', { style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, padding: 10 } },
|
|
852
|
+
h('summary', { style: { cursor: 'pointer' } }, '参数与授权范围'),
|
|
853
|
+
h('div', null, h('strong', null, '需要从请求中确认的参数'),
|
|
854
|
+
h('p', { style: mutedTextStyle }, '可不添加参数。参数键使用英文,给同事看的中文问题填在参数说明;需要指定 PR 时选“Gitea PR 编号”,系统会核对它是否存在。自由文本留在本机,服务端只核对长度和摘要。')),
|
|
855
|
+
...parameters.map((rule, index) => h('div', { key: index, style: { display: 'flex', flexWrap: 'wrap', gap: 8,
|
|
856
|
+
padding: 8, border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8 } },
|
|
857
|
+
h('input', { 'aria-label': '参数键(英文)', value: rule.name, maxLength: 32, placeholder: '英文键,例如 pr_number',
|
|
858
|
+
onChange: (event) => updateParameter(index, { name: event.target.value }), style: { ...inputStyle, flex: '1 1 150px' } }),
|
|
859
|
+
h('input', { 'aria-label': '参数说明', value: rule.description, maxLength: 100,
|
|
860
|
+
onChange: (event) => updateParameter(index, { description: event.target.value }),
|
|
861
|
+
placeholder: '给同事看的问题,例如要审查哪个 PR', style: { ...inputStyle, flex: '1 1 230px' } }),
|
|
862
|
+
h('select', { 'aria-label': '参数类型', value: rule.kind,
|
|
863
|
+
onChange: (event) => updateParameter(index, { kind: event.target.value }), style: { ...inputStyle, flex: '1 1 150px' } },
|
|
864
|
+
h('option', { value: 'text' }, '文本'), h('option', { value: 'integer' }, '整数'),
|
|
865
|
+
h('option', { value: 'choice' }, '选项'), h('option', { value: 'gitea_pr' }, 'Gitea PR 编号')),
|
|
866
|
+
h('label', null, h('input', { type: 'checkbox', checked: rule.required,
|
|
867
|
+
onChange: (event) => updateParameter(index, { required: event.target.checked }) }), ' 必填'),
|
|
868
|
+
['integer', 'gitea_pr'].includes(rule.kind) ? h('input', { 'aria-label': '最小值', type: 'number', value: rule.min,
|
|
869
|
+
onChange: (event) => updateParameter(index, { min: event.target.value }), placeholder: '最小值', style: { ...inputStyle, width: 120 } }) : null,
|
|
870
|
+
['text', 'integer', 'gitea_pr'].includes(rule.kind) ? h('input', { 'aria-label': '最大值', type: 'number', value: rule.max,
|
|
871
|
+
onChange: (event) => updateParameter(index, { max: event.target.value }), placeholder: '最大值', style: { ...inputStyle, width: 120 } }) : null,
|
|
872
|
+
rule.kind === 'choice' ? h('input', { 'aria-label': '可选值', value: rule.choices,
|
|
873
|
+
onChange: (event) => updateParameter(index, { choices: event.target.value }), placeholder: '用逗号分隔选项', style: { ...inputStyle, flex: '1 1 250px' } }) : null,
|
|
874
|
+
rule.kind === 'gitea_pr' ? h('span', { style: mutedTextStyle },
|
|
875
|
+
`使用机器人的可信 Gitea 地址:${selectedEnrollment?.gitea_origin || trustedOrigin || '请在下方登记一次'}`) : null,
|
|
876
|
+
rule.kind === 'gitea_pr' ? h('input', { 'aria-label': 'Gitea 仓库', value: rule.repository,
|
|
877
|
+
onChange: (event) => updateParameter(index, { repository: event.target.value }), placeholder: '团队/仓库', style: { ...inputStyle, flex: '1 1 180px' } }) : null,
|
|
878
|
+
h('button', { type: 'button', style: secondaryButtonStyle,
|
|
879
|
+
onClick: () => setParameters((current) => current.filter((_, i) => i !== index)) }, '移除参数'))),
|
|
880
|
+
h('button', { type: 'button', disabled: parameters.length >= 12, style: secondaryButtonStyle,
|
|
881
|
+
onClick: () => setParameters((current) => [...current, { name: '', kind: 'text', required: true,
|
|
882
|
+
description: '', min: '', max: '', choices: '', gitea_origin: '', repository: '' }]) }, '添加参数'),
|
|
883
|
+
parameters.some((rule) => rule.kind === 'gitea_pr') ? h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } },
|
|
884
|
+
h('input', { 'aria-label': '这个机器人的可信 Gitea 地址', type: 'url', value: trustedOrigin,
|
|
885
|
+
onChange: (event) => setTrustedOrigin(event.target.value), placeholder: '可信 Gitea 地址',
|
|
886
|
+
style: { ...inputStyle, flex: '1 1 260px' } }),
|
|
887
|
+
h('button', { type: 'button', disabled: busy || !trustedOrigin.trim() || !selectedBot,
|
|
888
|
+
onClick: () => void (selectedEnrollmentBound ? saveTrustedOrigin() : enrollBot()), style: secondaryButtonStyle },
|
|
889
|
+
selectedEnrollmentBound ? '保存可信地址' : '绑定到这台 DSH 并保存地址')) : null,
|
|
890
|
+
parameters.some((rule) => rule.kind === 'gitea_pr') ? h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } },
|
|
891
|
+
h('input', { 'aria-label': '只读 Gitea 访问令牌', type: 'password', autoComplete: 'off', value: giteaToken,
|
|
892
|
+
onChange: (event) => setGiteaToken(event.target.value), placeholder: tokenConfigured ? '本机已保存 Gitea 令牌' : '先保存只读 Gitea 令牌',
|
|
893
|
+
style: { ...inputStyle, flex: '1 1 260px' } }),
|
|
894
|
+
h('button', { type: 'button', disabled: busy || !giteaToken.trim(),
|
|
895
|
+
onClick: () => void saveGiteaToken(), style: secondaryButtonStyle }, '保存令牌')) : null,
|
|
896
|
+
parameters.some((rule) => rule.kind === 'gitea_pr') ? h('p', { style: mutedTextStyle },
|
|
897
|
+
'令牌只保存在这台 DSH,用于读取 PR,不会提交到 ANT 服务端或交给执行方。') : null,
|
|
898
|
+
h('label', null, '使用场景', h('select', { value: conversationKind,
|
|
571
899
|
onChange: (event) => { setConversationKind(event.target.value); setGroupId('') }, style: inputStyle },
|
|
572
|
-
h('option', { value: 'dm' }, '
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
'
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
h('button', { type: 'button', disabled: busy, onClick: () => void
|
|
583
|
-
|
|
584
|
-
h('
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
h('label', { htmlFor: 'ant-delegation-pr' }, '允许审查的 PR 编号'),
|
|
588
|
-
h('input', { id: 'ant-delegation-pr', type: 'number', min: 1, required: true, value: prNumber,
|
|
589
|
-
onChange: (event) => setPrNumber(event.target.value), style: inputStyle }),
|
|
590
|
-
h('label', { htmlFor: 'ant-delegation-audience' }, '谁可以请求'),
|
|
591
|
-
h('select', { id: 'ant-delegation-audience', value: senderScope,
|
|
900
|
+
h('option', { value: 'dm' }, '私聊'), h('option', { value: 'group' }, '指定群聊'))),
|
|
901
|
+
conversationKind === 'group' ? h('label', null, '群聊', h('select', { required: true, value: groupId,
|
|
902
|
+
onChange: (event) => setGroupId(event.target.value), style: inputStyle },
|
|
903
|
+
h('option', { value: '' }, '让群成员先 @ 机器人,再刷新列表'),
|
|
904
|
+
...availableGroups.map((group) => h('option', { key: group.group_id, value: group.group_id },
|
|
905
|
+
`识别码 ${group.group_code} · ${group.group_id}`)))) : null,
|
|
906
|
+
conversationKind === 'group' ? h('button', { type: 'button', disabled: busy,
|
|
907
|
+
onClick: () => void load().catch((error) => setMessage(error.message)), style: secondaryButtonStyle }, '刷新群列表') : null,
|
|
908
|
+
conversationKind === 'group' && selectedEnrollmentBound ? h('div', { style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' } },
|
|
909
|
+
h('span', { style: mutedTextStyle }, selectedEnrollment.owner_bound ? '已绑定你的飞书身份,可在群内批准任务' : '群内批准前需绑定你的飞书身份'),
|
|
910
|
+
h('button', { type: 'button', disabled: busy, onClick: () => void issueBindingCode(), style: secondaryButtonStyle },
|
|
911
|
+
selectedEnrollment.owner_bound ? '重新获取绑定码' : '获取身份绑定码')) : null,
|
|
912
|
+
conversationKind === 'group' && bindingCode ? h('p', { role: 'status', style: mutedTextStyle },
|
|
913
|
+
`请你本人私聊机器人发送“绑定身份 ${bindingCode}”。完成后在群里 @机器人发送“在本群启用任务 任务名称”,并按提示确认;结果会公开回复该群。`) : null,
|
|
914
|
+
h('label', null, '谁可以请求', h('select', { value: senderScope,
|
|
592
915
|
onChange: (event) => setSenderScope(event.target.value), style: inputStyle },
|
|
593
|
-
h('option', { value: 'any' }, conversationKind === 'group' ? '
|
|
594
|
-
h('option', { value: 'allowlist' }, '
|
|
595
|
-
senderScope === 'allowlist' ? h('input', { 'aria-label': '
|
|
916
|
+
h('option', { value: 'any' }, conversationKind === 'group' ? '此群任何成员' : '任何能私聊此机器人者'),
|
|
917
|
+
h('option', { value: 'allowlist' }, '指定 IM 用户 ID'))),
|
|
918
|
+
senderScope === 'allowlist' ? h('input', { 'aria-label': '可请求的 IM 用户 ID', required: true, value: senderIds,
|
|
596
919
|
onChange: (event) => setSenderIds(event.target.value), placeholder: '多个 ID 用逗号分隔', style: inputStyle }) : null,
|
|
597
|
-
h('label', {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
h('
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
h('
|
|
610
|
-
|
|
611
|
-
: (
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
920
|
+
h('label', null, '有效期', h('select', { value: days, onChange: (event) => setDays(Number(event.target.value)), style: inputStyle },
|
|
921
|
+
h('option', { value: 1 }, '1 天'), h('option', { value: 7 }, '7 天'), h('option', { value: 30 }, '30 天')))),
|
|
922
|
+
cardBlockReason ? h('p', { role: 'status', style: mutedTextStyle }, cardBlockReason) : null,
|
|
923
|
+
h('button', { disabled: busy || Boolean(cardBlockReason),
|
|
924
|
+
style: primaryButtonStyle }, '保存并开放能力'))),
|
|
925
|
+
h('details', { style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, padding: 12 } },
|
|
926
|
+
h('summary', { style: { cursor: 'pointer', fontWeight: 600 } }, '本机处理设置'),
|
|
927
|
+
h('p', { style: mutedTextStyle }, '本机并发上限只影响这台电脑接收的新任务;PR 仓库缓存上限按这台 DSH 的长期仓库缓存总量计算,不包括任务临时工作目录。'),
|
|
928
|
+
h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' } },
|
|
929
|
+
h('span', { style: mutedTextStyle }, loadFailures.includes('settings') ? '团队默认值暂未读取。'
|
|
930
|
+
: `团队默认:先看 ${defaults?.context_messages ?? 10} 条前文、最多翻阅 ${defaults?.history_max_records ?? 100} 条平台记录、同时处理 ${defaults?.max_concurrency ?? 3} 项任务。`),
|
|
931
|
+
h('button', { type: 'button', onClick: () => void load().catch((error) => setMessage(error.message)), style: secondaryButtonStyle }, '刷新')),
|
|
932
|
+
loadFailures.includes('local-settings') ? h('p', { role: 'status', style: mutedTextStyle },
|
|
933
|
+
'本机处理设置暂未读取;请刷新后重试。')
|
|
934
|
+
: h('form', { onSubmit: saveLocalProcessingSettings, style: { display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'end' } },
|
|
935
|
+
h('label', null, '这台电脑最多同时处理',
|
|
936
|
+
h('select', { value: localConcurrency == null ? '' : String(localConcurrency),
|
|
937
|
+
onChange: (event) => setLocalConcurrency(event.target.value), style: inputStyle },
|
|
938
|
+
h('option', { value: '' }, '跟随团队默认'),
|
|
939
|
+
...Array.from({ length: 8 }, (_, index) => h('option', { key: index + 1, value: index + 1 }, `${index + 1} 项任务`)))),
|
|
940
|
+
h('label', null, '这台电脑 PR 仓库缓存上限(GiB)',
|
|
941
|
+
h('input', { type: 'number', min: 1, max: 100, step: 1, required: true,
|
|
942
|
+
value: repositoryCacheLimitGib,
|
|
943
|
+
onChange: (event) => setRepositoryCacheLimitGib(event.target.value), style: inputStyle }),
|
|
944
|
+
h('span', { style: mutedTextStyle }, '默认 10 GiB,范围 1–100 GiB,按长期仓库文件逻辑大小计算(不含临时工作区和文件系统分配开销)。日常不会淘汰仍启用能力的缓存;停用或到期能力会在刷新时回收。单次拉取若实际越界,为恢复预算会清理这项能力的仓库缓存(旧 PR 可重新下载)。')),
|
|
945
|
+
h('button', { disabled: busy, style: secondaryButtonStyle }, '保存本机处理设置')),
|
|
946
|
+
h('p', { style: mutedTextStyle }, `本机可用执行方:${executors.map((item) => `${item.title}${item.available ? '(可用)' : `(${item.reason || '未启用或未找到'})`}`).join('、') || '暂未读取到'}。可用选项会出现在对应能力的执行方设置中。`)),
|
|
622
947
|
message ? h('p', { role: 'status', style: mutedTextStyle }, message) : null)
|
|
623
948
|
}
|
|
624
949
|
|