@ai-wendao/ant 0.4.0 → 0.4.1

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/client.js CHANGED
@@ -239,6 +239,8 @@ window.__ModuleLoader__.load({
239
239
  const bindingRevision = useRef(0)
240
240
  const [message, setMessage] = useState('')
241
241
  const [pluginAlerts, setPluginAlerts] = useState([])
242
+ const [delegationRevision, setDelegationRevision] = useState(0)
243
+ const [legacyOpen, setLegacyOpen] = useState(false)
242
244
  useEffect(() => {
243
245
  let disposed = false
244
246
  const revision = bindingRevision.current
@@ -353,10 +355,483 @@ window.__ModuleLoader__.load({
353
355
  onClick: () => openInstalledPlugin(plugin.package_name), style: secondaryButtonStyle }, `在 DSH 中查看 ${plugin.title}`))) : null,
354
356
  h(ModelSyncFooter, { remote, serviceUrl, bindingReady: binding.state === 'ready',
355
357
  onServiceUrlChange: setServiceUrl, onConnectionChange }),
356
- h(DelegationSettings, { remote }))
358
+ h(DelegationCards, { remote, revision: delegationRevision,
359
+ openImSettings: () => openInstalledPlugin('@xmanrui/dsh-im'),
360
+ openPrReview: () => { setLegacyOpen(true)
361
+ window.document?.getElementById('ant-pr-review-settings')?.scrollIntoView({ behavior: 'smooth', block: 'start' }) },
362
+ onGrantChanged: () => setDelegationRevision((value) => value + 1) }),
363
+ h(DelegationSettings, { remote, revision: delegationRevision, open: legacyOpen, onOpenChange: setLegacyOpen,
364
+ onChanged: () => setDelegationRevision((value) => value + 1) }))
357
365
  }
358
366
 
359
- function DelegationSettings({ remote }) {
367
+ function DelegationCards({ remote, revision, openImSettings, openPrReview, onGrantChanged }) {
368
+ const [bots, setBots] = useState([])
369
+ const [enrolled, setEnrolled] = useState([])
370
+ const [groups, setGroups] = useState([])
371
+ const [cards, setCards] = useState([])
372
+ const [grants, setGrants] = useState([])
373
+ const [tasks, setTasks] = useState([])
374
+ const [executors, setExecutors] = useState([])
375
+ const [defaults, setDefaults] = useState(null)
376
+ const [localConcurrency, setLocalConcurrency] = useState(null)
377
+ const [prReviewExecutor, setPrReviewExecutor] = useState('dsh')
378
+ const [botId, setBotId] = useState('')
379
+ const [trustedOrigin, setTrustedOrigin] = useState('')
380
+ const [bindingCode, setBindingCode] = useState('')
381
+ const [conversationKind, setConversationKind] = useState('dm')
382
+ const [groupId, setGroupId] = useState('')
383
+ const [title, setTitle] = useState('')
384
+ const [intent, setIntent] = useState('')
385
+ const [examples, setExamples] = useState('')
386
+ const [taskPrompt, setTaskPrompt] = useState('')
387
+ const [parameters, setParameters] = useState([])
388
+ const [giteaToken, setGiteaToken] = useState('')
389
+ const [tokenConfigured, setTokenConfigured] = useState(false)
390
+ const [senderScope, setSenderScope] = useState('any')
391
+ const [senderIds, setSenderIds] = useState('')
392
+ const [days, setDays] = useState(7)
393
+ const [busy, setBusy] = useState(false)
394
+ const [loading, setLoading] = useState(true)
395
+ const [botLoadError, setBotLoadError] = useState(false)
396
+ const [createOpen, setCreateOpen] = useState(false)
397
+ const [message, setMessage] = useState('')
398
+ const load = async () => {
399
+ setLoading(true)
400
+ try {
401
+ const paths = ['bots', 'enrolled-bots', 'groups', 'cards', 'settings', 'executors', 'local-settings', 'status', 'grants']
402
+ const responses = await Promise.all(paths.map((path) => fetch(`/api/ant-teams/delegation/${path}`, { cache: 'no-store' })))
403
+ const data = await Promise.all(responses.map(readLocalJsonResponse))
404
+ if (!responses[3].ok) throw new Error(data[3]?.message || '暂时无法读取任务卡。')
405
+ setBots(responses[0].ok ? data[0].bots || [] : [])
406
+ setBotLoadError(!responses[0].ok)
407
+ setEnrolled(responses[1].ok && Array.isArray(data[1]) ? data[1] : [])
408
+ setGroups(responses[2].ok && Array.isArray(data[2]) ? data[2] : [])
409
+ setCards(Array.isArray(data[3]) ? data[3] : [])
410
+ setDefaults(responses[4].ok ? data[4] : null)
411
+ setExecutors(responses[5].ok && Array.isArray(data[5]) ? data[5] : [])
412
+ setLocalConcurrency(responses[6].ok ? data[6].max_concurrency : null)
413
+ setPrReviewExecutor(responses[6].ok ? data[6].pr_review_executor || 'dsh' : 'dsh')
414
+ setTasks(responses[7].ok && Array.isArray(data[7].items) ? data[7].items : [])
415
+ setGrants(responses[8].ok && Array.isArray(data[8]) ? data[8] : [])
416
+ let credential = null
417
+ try { credential = await remote?.credentials?.describe?.(['ANT_GITEA_API_TOKEN']) } catch {}
418
+ setTokenConfigured(Boolean(credential?.ok && credential.value?.ANT_GITEA_API_TOKEN?.configured))
419
+ } finally { setLoading(false) }
420
+ }
421
+ useEffect(() => { void load().catch((error) => setMessage(error.message)) }, [remote, revision])
422
+ const selectedBot = bots.find((bot) => bot.bot_id === botId)
423
+ const botConnected = Boolean(selectedBot?.available && selectedBot?.connected)
424
+ const selectedEnrollment = enrolled.find((bot) => bot.bot_fingerprint === selectedBot?.fingerprint)
425
+ const availableGroups = groups.filter((group) => group.bot_fingerprint === selectedBot?.fingerprint)
426
+ const activeCards = cards.filter((card) => !card.revoked_at && card.expires_at * 1000 > Date.now())
427
+ const activeGrants = grants.filter((grant) => !grant.revoked_at && grant.expires_at * 1000 > Date.now())
428
+ const connectedBots = bots.filter((bot) => bot.available && bot.connected)
429
+ const enrolledCount = enrolled.filter((item) => connectedBots.some((bot) => bot.fingerprint === item.bot_fingerprint)).length
430
+ const selectedTaskCount = activeCards.filter((card) => card.bot_fingerprint === selectedBot?.fingerprint).length
431
+ + activeGrants.filter((grant) => grant.bot_fingerprint === selectedBot?.fingerprint).length
432
+ const cardBlockReason = !botConnected || !selectedEnrollment ? '先在上方连接并启用机器人。'
433
+ : conversationKind === 'group' && !groupId ? '请先在“更多设置”中选择目标群;若列表为空,让群成员先 @机器人,再刷新群列表。'
434
+ : senderScope === 'allowlist' && !senderIds.trim() ? '请在“更多设置”中填写可请求的成员 ID。'
435
+ : parameters.some((rule) => rule.kind === 'gitea_pr') && !tokenConfigured ? '请先在“更多设置”中保存本机 Gitea 只读令牌。'
436
+ : ''
437
+ const updateParameter = (index, patch) => setParameters((current) => current.map((rule, i) => i === index ? { ...rule, ...patch } : rule))
438
+ const saveGiteaToken = async () => {
439
+ if (!giteaToken.trim()) return
440
+ setBusy(true)
441
+ try {
442
+ const result = await remote.credentials.set('ANT_GITEA_API_TOKEN', giteaToken.trim())
443
+ if (!result?.ok) throw new Error('DSH 无法安全保存 Gitea 令牌。')
444
+ setGiteaToken(''); setTokenConfigured(true)
445
+ setMessage('Gitea 令牌已保存在这台 DSH,不会提交给 ANT 服务端。')
446
+ } catch (error) { setMessage(error.message) }
447
+ finally { setBusy(false) }
448
+ }
449
+ const enrollBot = async () => {
450
+ if (!selectedBot) return
451
+ setBusy(true)
452
+ try {
453
+ const response = await fetch('/api/ant-teams/delegation/enrolled-bots', {
454
+ method: 'POST', headers: { 'content-type': 'application/json' },
455
+ body: JSON.stringify({ bot_id: selectedBot.bot_id, bot_fingerprint: selectedBot.fingerprint }),
456
+ })
457
+ const result = await readLocalJsonResponse(response)
458
+ if (!response.ok) throw new Error(result.message || '协作机器人启用失败。')
459
+ await load()
460
+ setMessage('协作机器人已启用。你可以创建任务卡;私聊时也能正常聊天。')
461
+ } catch (error) { setMessage(error.message) }
462
+ finally { setBusy(false) }
463
+ }
464
+ const saveTrustedOrigin = async () => {
465
+ if (!selectedBot || !selectedEnrollment) return
466
+ const origin = normalizeServiceUrl(trustedOrigin)
467
+ if (!origin) { setMessage('请填写有效的 HTTPS Gitea 地址。'); return }
468
+ if (!window.confirm(`将这个机器人的可信 Gitea 地址设为 ${origin}?不匹配的旧版 PR 授权会被撤销,已有任务卡的 PR 参数也需重新核对。`)) return
469
+ setBusy(true)
470
+ try {
471
+ const response = await fetch('/api/ant-teams/delegation/enrolled-bots', {
472
+ method: 'POST', headers: { 'content-type': 'application/json' },
473
+ body: JSON.stringify({ bot_id: selectedBot.bot_id, bot_fingerprint: selectedBot.fingerprint,
474
+ gitea_origin: origin }),
475
+ })
476
+ const result = await readLocalJsonResponse(response)
477
+ if (!response.ok) throw new Error(result.message || '可信地址保存失败。')
478
+ await load(); setTrustedOrigin(origin); setMessage('可信 Gitea 地址已保存。')
479
+ } catch (error) { setMessage(error.message) }
480
+ finally { setBusy(false) }
481
+ }
482
+ const issueBindingCode = async () => {
483
+ if (!selectedBot || !selectedEnrollment) return
484
+ setBusy(true)
485
+ try {
486
+ const response = await fetch('/api/ant-teams/delegation/owner-challenge', {
487
+ method: 'POST', headers: { 'content-type': 'application/json' },
488
+ body: JSON.stringify({ bot_id: selectedBot.bot_id, bot_fingerprint: selectedBot.fingerprint }),
489
+ })
490
+ const result = await readLocalJsonResponse(response)
491
+ if (!response.ok) throw new Error(result.message || '无法获取身份绑定码。')
492
+ setBindingCode(result.code)
493
+ setMessage('绑定码 5 分钟有效。请你本人私聊这个机器人完成绑定。')
494
+ } catch (error) { setMessage(error.message) }
495
+ finally { setBusy(false) }
496
+ }
497
+ const saveConcurrency = async (event) => {
498
+ event.preventDefault()
499
+ setBusy(true)
500
+ try {
501
+ const response = await fetch('/api/ant-teams/delegation/local-settings', {
502
+ method: 'PUT', headers: { 'content-type': 'application/json' },
503
+ body: JSON.stringify({ max_concurrency: localConcurrency === '' ? null : Number(localConcurrency) }),
504
+ })
505
+ const result = await readLocalJsonResponse(response)
506
+ if (!response.ok) throw new Error(result.message || '并发设置保存失败。')
507
+ setLocalConcurrency(result.max_concurrency)
508
+ setMessage('本机并发设置已保存,稍后会应用于新任务。')
509
+ } catch (error) { setMessage(error.message) }
510
+ finally { setBusy(false) }
511
+ }
512
+ const savePrReviewExecutor = async (event) => {
513
+ event.preventDefault()
514
+ if (prReviewExecutor !== 'dsh') {
515
+ if (!executors.some((item) => item.id === prReviewExecutor && item.available)) {
516
+ setMessage('所选执行器尚未在这台 DSH 启用。'); return
517
+ }
518
+ if (!window.confirm(`启用 ${prReviewExecutor} 的指定 PR 差异审查试用?这会应用于本机所有现有的指定 PR 授权。同事只能请求已授权的 PR,ANT 会固定审查内容和临时目录;但执行器仍沿用你本机的权限,可能读取或修改其他本机文件。结果会回复原私聊或群。`)) return
519
+ }
520
+ setBusy(true)
521
+ try {
522
+ const response = await fetch('/api/ant-teams/delegation/local-settings', {
523
+ method: 'PUT', headers: { 'content-type': 'application/json' },
524
+ body: JSON.stringify({ pr_review_executor: prReviewExecutor,
525
+ accept_native_permissions: prReviewExecutor !== 'dsh' }),
526
+ })
527
+ const result = await readLocalJsonResponse(response)
528
+ if (!response.ok) throw new Error(result.message || '审查执行方保存失败。')
529
+ setPrReviewExecutor(result.pr_review_executor)
530
+ setMessage(result.pr_review_executor === 'dsh' ? '已关闭本机执行器试用;新请求继续由受限 DSH 审查。'
531
+ : `已启用 ${result.pr_review_executor} 的指定 PR 差异审查试用;请先用你自己的机器人验证。`)
532
+ } catch (error) { setMessage(error.message) }
533
+ finally { setBusy(false) }
534
+ }
535
+ const createCard = async (event) => {
536
+ event.preventDefault()
537
+ if (!selectedBot || !botConnected || !selectedEnrollment) { setMessage('请先连接并启用选中的协作机器人。'); return }
538
+ if (!title.trim() || !intent.trim() || !taskPrompt.trim()) {
539
+ setMessage('请填写任务名称、什么请求属于这项任务,以及处理要求。'); return
540
+ }
541
+ const bytes = (value) => new TextEncoder().encode(value).length
542
+ if (bytes(title.trim()) > 100 || bytes(intent.trim()) > 1000 || bytes(taskPrompt.trim()) > 4000) {
543
+ setMessage('任务名称、请求说明或处理要求过长,请缩短后再提交。'); return
544
+ }
545
+ const exampleLines = examples.split('\n').map((item) => item.trim()).filter(Boolean)
546
+ if (exampleLines.length > 8 || exampleLines.some((item) => bytes(item) > 500)) {
547
+ setMessage('同事示例最多写 8 句,每句请保持简短。'); return
548
+ }
549
+ const normalized = parameters.map((rule) => ({ name: rule.name.trim(), kind: rule.kind,
550
+ description: rule.description.trim() || null,
551
+ required: rule.required, min: rule.min === '' ? null : Number(rule.min),
552
+ max: rule.max === '' ? null : Number(rule.max),
553
+ choices: rule.kind === 'choice' ? rule.choices.split(/[,,\n]/u).map((item) => item.trim()).filter(Boolean) : null,
554
+ gitea_origin: rule.kind === 'gitea_pr' ? normalizeServiceUrl(rule.gitea_origin) : null,
555
+ repository: rule.kind === 'gitea_pr' ? rule.repository.trim().toLowerCase() : null }))
556
+ if (normalized.some((rule) => !/^[A-Za-z][A-Za-z0-9_]{0,31}$/u.test(rule.name))) {
557
+ setMessage('参数键请用英文字母开头,只能包含英文字母、数字和下划线;给同事看的中文问题可填在“参数说明”。'); return
558
+ }
559
+ if (new Set(normalized.map((rule) => rule.name)).size !== normalized.length) {
560
+ setMessage('参数键不能重复,请为每项参数填写不同的英文键。'); return
561
+ }
562
+ if (normalized.some((rule) => rule.description && bytes(rule.description) > 100)) {
563
+ setMessage('参数说明过长,请缩短给同事看的问题。'); return
564
+ }
565
+ if (normalized.some((rule) => rule.kind === 'choice' && (!rule.choices?.length || rule.choices.length > 32
566
+ || rule.choices.some((choice) => bytes(choice) > 100)
567
+ || new Set(rule.choices).size !== rule.choices.length))) {
568
+ setMessage('选项参数请填写 1–32 个不重复的可选值,每项保持简短。'); return
569
+ }
570
+ if (normalized.some((rule) => (rule.min != null && !Number.isInteger(rule.min))
571
+ || (rule.max != null && !Number.isInteger(rule.max))
572
+ || (rule.min != null && rule.max != null && rule.min > rule.max)
573
+ || (rule.kind === 'text' && rule.max != null && (rule.max < 1 || rule.max > 4096))
574
+ || (rule.kind === 'gitea_pr' && ((rule.min != null && rule.min < 1)
575
+ || (rule.max != null && rule.max > 1_000_000_000))))) {
576
+ setMessage('参数范围无效:最小值不能大于最大值;文本最长 4096 字节,PR 编号须为正整数。'); return
577
+ }
578
+ if (normalized.some((rule) => rule.kind === 'gitea_pr' && (!rule.gitea_origin || !rule.repository))) {
579
+ setMessage('PR 参数还需要填写可信 Gitea 地址和仓库。'); return
580
+ }
581
+ const validRepository = (value) => {
582
+ const parts = value.split('/')
583
+ return parts.length === 2 && parts.every((part) => part !== '.' && part !== '..'
584
+ && /^[A-Za-z0-9_.-]{1,100}$/u.test(part))
585
+ }
586
+ if (normalized.some((rule) => rule.kind === 'gitea_pr' && !validRepository(rule.repository))) {
587
+ setMessage('Gitea 仓库请填写“团队名/仓库名”,不要包含空格或多余路径。'); return
588
+ }
589
+ const scope = conversationKind === 'group' ? `群 ${groupId}` : '私聊'
590
+ if (!window.confirm(`开启“${title}”后,${scope}中符合条件的人可请机器人执行这个仅用文字处理的任务。${conversationKind === 'group' ? '结果会公开回复原群。' : '结果回复请求人。'}请确认。`)) return
591
+ setBusy(true)
592
+ try {
593
+ const response = await fetch('/api/ant-teams/delegation/cards', {
594
+ method: 'POST', headers: { 'content-type': 'application/json' },
595
+ body: JSON.stringify({ bot_id: selectedBot.bot_id, bot_fingerprint: selectedBot.fingerprint,
596
+ title, intent, examples: exampleLines,
597
+ parameters: normalized, task_prompt: taskPrompt, runner: 'dsh', workspace: null,
598
+ sender_scope: senderScope, sender_ids: senderScope === 'allowlist'
599
+ ? senderIds.split(/[\s,,]+/u).filter(Boolean) : [], conversation_kind: conversationKind,
600
+ ...(conversationKind === 'group' ? { conversation_id: groupId } : {}),
601
+ expires_at: Math.floor(Date.now() / 1000) + Number(days) * 86400 }),
602
+ })
603
+ const result = await readLocalJsonResponse(response)
604
+ if (!response.ok) throw new Error(result.message || '任务卡保存失败。')
605
+ setTitle(''); setIntent(''); setExamples(''); setTaskPrompt(''); setParameters([])
606
+ await load()
607
+ setCreateOpen(false)
608
+ setMessage('任务卡已启用。同事可自然地提出这类请求;参数不完整时机器人会追问。')
609
+ } catch (error) { setMessage(error.message) }
610
+ finally { setBusy(false) }
611
+ }
612
+ const revokeCard = async (card) => {
613
+ if (!window.confirm(`停止“${card.title}”的新请求?正在处理的任务无法撤销已经发生的读取。`)) return
614
+ setBusy(true)
615
+ try {
616
+ const response = await fetch('/api/ant-teams/delegation/cards', {
617
+ method: 'DELETE', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: card.id }),
618
+ })
619
+ if (!response.ok) throw new Error('停止任务卡失败,请重试。')
620
+ await load(); setMessage('任务卡已停止。')
621
+ } catch (error) { setMessage(error.message) }
622
+ finally { setBusy(false) }
623
+ }
624
+ const revokeGrant = async (grant) => {
625
+ if (!window.confirm(`停止 ${grant.repository} PR #${grant.pr_number} 的新审查请求?正在处理的任务不会因此撤销。`)) return
626
+ setBusy(true)
627
+ try {
628
+ const response = await fetch('/api/ant-teams/delegation/grants', {
629
+ method: 'DELETE', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: grant.id }),
630
+ })
631
+ if (!response.ok) throw new Error('停止 PR 授权失败,请重试。')
632
+ await load(); onGrantChanged?.(); setMessage('已停止这项 PR 授权;其他聊天和任务不受影响。')
633
+ } catch (error) { setMessage(error.message) }
634
+ finally { setBusy(false) }
635
+ }
636
+ const cancelTask = async (task) => {
637
+ if (!window.confirm(`取消任务 ${task.task_id}?已经发生的读取不能撤回。`)) return
638
+ setBusy(true)
639
+ try {
640
+ const response = await fetch('/api/ant-teams/delegation/cancel', { method: 'POST',
641
+ headers: { 'content-type': 'application/json' }, body: JSON.stringify({ task_id: task.task_id }) })
642
+ const result = await readLocalJsonResponse(response)
643
+ if (!response.ok || !result.canceled) throw new Error(result.message || '任务已结束或无法取消,请刷新状态。')
644
+ await load(); setMessage('已请求取消任务。')
645
+ } catch (error) { setMessage(error.message) }
646
+ finally { setBusy(false) }
647
+ }
648
+ return h('section', { style: panelStyle, 'aria-label': '协作机器人和可执行任务' },
649
+ h('h3', { style: { margin: 0, fontSize: 16 } }, '协作机器人与任务'),
650
+ h('p', { style: mutedTextStyle }, '同事可以像平常一样聊天。只有你开放的任务才会经过参数与权限检查;群里的任务结果公开回复原群。新建任务卡目前使用无本机工具的 DSH 文字处理。'),
651
+ h('p', { style: mutedTextStyle }, '机器人用模型判断一句话是否属于某项任务,话题判断可能有误;请求人、群、参数类型和数值范围由 ANT 再次校验。敏感内容请缩小授权范围。'),
652
+ h('div', { style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, padding: 12,
653
+ display: 'grid', gap: 8 } },
654
+ h('strong', null, '开始使用'),
655
+ h('span', { role: 'status', style: mutedTextStyle }, loading ? '正在读取机器人与任务状态…'
656
+ : botLoadError ? '暂时无法读取机器人状态。请刷新后再试。'
657
+ : connectedBots.length === 0 ? '尚未发现已连接的机器人。请先在 DSH-IM 中连接飞书机器人。'
658
+ : selectedBot && !botConnected ? `“${selectedBot.name}”当前离线;请先在 DSH-IM 恢复连接。`
659
+ : selectedBot && !selectedEnrollment ? `“${selectedBot.name}”已连接;请先启用协作。启用后,它仍能正常聊天。`
660
+ : enrolledCount === 0 ? '机器人已连接;请选择并启用协作。启用后,它仍能正常聊天。'
661
+ : !selectedBot ? '已有协作机器人;请选择一台,查看它的任务并私聊自测。'
662
+ : selectedTaskCount === 0 ? `“${selectedBot.name}”已启用协作;下一步开放一项任务。你也可以先私聊自测普通聊天。`
663
+ : `“${selectedBot.name}”已启用协作 · ${selectedTaskCount} 项有效授权。可以私聊自测。`),
664
+ bots.length ? h('label', null, '我的机器人', h('select', { value: botId,
665
+ onChange: (event) => { setBotId(event.target.value); setGroupId(''); setBindingCode('');
666
+ setTrustedOrigin(enrolled.find((bot) => bot.bot_id === event.target.value)?.gitea_origin || '') }, style: inputStyle },
667
+ h('option', { value: '' }, '请选择机器人'),
668
+ ...bots.map((bot) => h('option', { key: bot.bot_id, value: bot.bot_id,
669
+ disabled: !bot.available || !bot.connected }, `${bot.name}${bot.available && bot.connected ? '' : '(未连接)'}`)))) : null,
670
+ connectedBots.length === 0 && !loading && !botLoadError ? h('button', { type: 'button',
671
+ onClick: openImSettings, style: secondaryButtonStyle }, '打开 DSH-IM 设置') : null,
672
+ connectedBots.length === 0 && !loading && !botLoadError ? h('span', { style: mutedTextStyle },
673
+ '若尚未安装 DSH-IM,请先到 DSH 的插件列表安装并连接机器人。') : null,
674
+ selectedBot && !botConnected && connectedBots.length ? h('button', { type: 'button', onClick: openImSettings,
675
+ style: secondaryButtonStyle }, '检查 DSH-IM 连接') : null,
676
+ selectedBot && !selectedEnrollment ? h('button', { type: 'button', disabled: busy || !botConnected,
677
+ onClick: () => void enrollBot(), style: secondaryButtonStyle }, '启用这个协作机器人') : null,
678
+ selectedEnrollment && botConnected ? h('p', { style: mutedTextStyle },
679
+ `自测:私聊“${selectedBot.name}”发送“你能做什么”;发起任务后可问“刚才的任务怎么样了”。群聊请先在目标群 @机器人,并在下方任务设置中启用该群。`) : null,
680
+ h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } },
681
+ h('button', { type: 'button', onClick: () => { setCreateOpen(true)
682
+ window.document?.getElementById('ant-new-task')?.scrollIntoView({ behavior: 'smooth', block: 'start' }) },
683
+ style: primaryButtonStyle }, '开放文字任务'),
684
+ h('button', { type: 'button', onClick: openPrReview, style: secondaryButtonStyle }, '开放指定 PR 审查'),
685
+ h('button', { type: 'button', onClick: () => void load().catch((error) => setMessage(error.message)),
686
+ style: secondaryButtonStyle }, '刷新状态'))),
687
+ h('h4', { style: { marginBottom: 0 } }, `我开放的任务 · ${activeCards.length + activeGrants.length}`),
688
+ ...activeCards.map((card) => h('div', { key: card.id, style: { display: 'flex', gap: 12,
689
+ alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: 8 } },
690
+ 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' ? '群' : '私聊'} · DSH 文字处理 · ${new Date(card.expires_at * 1000).toLocaleString('zh-CN')} 到期`),
691
+ h('button', { type: 'button', disabled: busy, onClick: () => void revokeCard(card), style: secondaryButtonStyle }, '停止'))),
692
+ ...activeGrants.map((grant) => h('div', { key: grant.id, style: { display: 'flex', gap: 12,
693
+ alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: 8 } },
694
+ h('span', { style: { flex: '1 1 280px', minWidth: 0, overflowWrap: 'anywhere' } }, `指定 PR 差异审查 · ${grant.repository} #${grant.pr_number} · ${bots.find((bot) => bot.fingerprint === grant.bot_fingerprint)?.name || '机器人'} · ${grant.sender_scope === 'allowlist' ? '仅指定成员可请求' : grant.conversation_kind === 'group' ? '群成员可请求' : '可私聊者可请求'} · 结果回复原${grant.conversation_kind === 'group' ? '群' : '私聊'} · ${prReviewExecutor === 'dsh' ? 'DSH' : prReviewExecutor + ' 本机试用'} · ${new Date(grant.expires_at * 1000).toLocaleString('zh-CN')} 到期`),
695
+ h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } },
696
+ h('button', { type: 'button', onClick: openPrReview, style: secondaryButtonStyle }, '查看设置'),
697
+ h('button', { type: 'button', disabled: busy, onClick: () => void revokeGrant(grant),
698
+ style: secondaryButtonStyle }, '停止')))),
699
+ activeCards.length + activeGrants.length === 0 ? h('p', { style: mutedTextStyle },
700
+ '还没有开放任务。先选机器人,再用上方按钮创建文字任务或指定 PR 审查。') : null,
701
+ h('h4', { style: { marginBottom: 0 } }, '最近任务'),
702
+ ...(tasks.filter((task) => task.task_id).length ? tasks.filter((task) => task.task_id)
703
+ .map((task) => h('div', { key: task.task_id, style: { display: 'flex', gap: 12, alignItems: 'center',
704
+ justifyContent: 'space-between', flexWrap: 'wrap', borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: 8 } },
705
+ 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' ? '未完成' : ({
706
+ pending: '排队中', running: '处理中', reply_pending: '结果待发送', reply_attempted: '回复状态待核对', done: '已回复',
707
+ }[task.status] || task.status)}`),
708
+ ['card_task', 'legacy_pr_task'].includes(task.kind) && ['pending', 'running'].includes(task.status) ? h('button', { type: 'button', disabled: busy,
709
+ onClick: () => void cancelTask(task), style: secondaryButtonStyle }, '取消任务') : null))
710
+ : [h('p', { key: 'none', style: mutedTextStyle }, '暂无任务。')]),
711
+ h('details', { id: 'ant-new-task', open: createOpen,
712
+ onToggle: (event) => setCreateOpen(event.currentTarget.open),
713
+ style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, padding: 12 } },
714
+ h('summary', { style: { cursor: 'pointer', fontWeight: 600 } }, '新建文字任务'),
715
+ h('form', { onSubmit: createCard, style: { display: 'grid', gap: 9, maxWidth: 760 } },
716
+ h('p', { style: mutedTextStyle }, selectedBot
717
+ ? `当前机器人:${selectedBot.name}${selectedEnrollment ? ' · 协作已启用' : ' · 请先在上方启用协作'}`
718
+ : '请先在上方选择机器人并启用协作。'),
719
+ selectedEnrollment ? h('p', { style: mutedTextStyle }, '此机器人已由 ANT 接收协作消息;普通聊天由受限 DSH 对话处理。') : null,
720
+ selectedBot && conversationKind === 'group' && !selectedBot.history_available ? h('p', { role: 'status', style: mutedTextStyle },
721
+ '这个机器人当前不能读取平台群历史;群聊只能参考实际送达的消息和本机近期记录。请检查 DSH-IM 权限。') : null,
722
+ h('label', null, '任务名称', h('input', { required: true, maxLength: 100, value: title,
723
+ onChange: (event) => setTitle(event.target.value), placeholder: '例如:总结项目进度', style: inputStyle })),
724
+ h('label', null, '什么请求属于这项任务', h('textarea', { required: true, maxLength: 1000, rows: 2, value: intent,
725
+ onChange: (event) => setIntent(event.target.value), placeholder: '例如:把群里讨论的本周进展整理成摘要', style: inputStyle })),
726
+ h('label', null, '同事可能怎么说(每行一句,可选)', h('textarea', { rows: 2, value: examples,
727
+ onChange: (event) => setExamples(event.target.value), placeholder: '帮我总结一下刚才的讨论', style: inputStyle })),
728
+ h('label', null, '处理要求', h('textarea', { required: true, maxLength: 4000, rows: 3, value: taskPrompt,
729
+ onChange: (event) => setTaskPrompt(event.target.value), placeholder: '只使用当前会话可见的内容,列出结论和待办', style: inputStyle })),
730
+ h('details', { style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, padding: 10 } },
731
+ h('summary', { style: { cursor: 'pointer' } }, '更多设置:参数、群聊与请求人'),
732
+ h('div', null, h('strong', null, '需要从请求中确认的参数'),
733
+ h('p', { style: mutedTextStyle }, '可不添加参数。参数键使用英文,给同事看的中文问题填在参数说明;需要指定 PR 时选“Gitea PR 编号”,系统会核对它是否存在。自由文本留在本机,服务端只核对长度和摘要。')),
734
+ ...parameters.map((rule, index) => h('div', { key: index, style: { display: 'flex', flexWrap: 'wrap', gap: 8,
735
+ padding: 8, border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8 } },
736
+ h('input', { 'aria-label': '参数键(英文)', value: rule.name, maxLength: 32, placeholder: '英文键,例如 pr_number',
737
+ onChange: (event) => updateParameter(index, { name: event.target.value }), style: { ...inputStyle, flex: '1 1 150px' } }),
738
+ h('input', { 'aria-label': '参数说明', value: rule.description, maxLength: 100,
739
+ onChange: (event) => updateParameter(index, { description: event.target.value }),
740
+ placeholder: '给同事看的问题,例如要审查哪个 PR', style: { ...inputStyle, flex: '1 1 230px' } }),
741
+ h('select', { 'aria-label': '参数类型', value: rule.kind,
742
+ onChange: (event) => updateParameter(index, { kind: event.target.value }), style: { ...inputStyle, flex: '1 1 150px' } },
743
+ h('option', { value: 'text' }, '文本'), h('option', { value: 'integer' }, '整数'),
744
+ h('option', { value: 'choice' }, '选项'), h('option', { value: 'gitea_pr' }, 'Gitea PR 编号')),
745
+ h('label', null, h('input', { type: 'checkbox', checked: rule.required,
746
+ onChange: (event) => updateParameter(index, { required: event.target.checked }) }), ' 必填'),
747
+ ['integer', 'gitea_pr'].includes(rule.kind) ? h('input', { 'aria-label': '最小值', type: 'number', value: rule.min,
748
+ onChange: (event) => updateParameter(index, { min: event.target.value }), placeholder: '最小值', style: { ...inputStyle, width: 120 } }) : null,
749
+ ['text', 'integer', 'gitea_pr'].includes(rule.kind) ? h('input', { 'aria-label': '最大值', type: 'number', value: rule.max,
750
+ onChange: (event) => updateParameter(index, { max: event.target.value }), placeholder: '最大值', style: { ...inputStyle, width: 120 } }) : null,
751
+ rule.kind === 'choice' ? h('input', { 'aria-label': '可选值', value: rule.choices,
752
+ onChange: (event) => updateParameter(index, { choices: event.target.value }), placeholder: '用逗号分隔选项', style: { ...inputStyle, flex: '1 1 250px' } }) : null,
753
+ rule.kind === 'gitea_pr' ? h('input', { 'aria-label': '可信 Gitea 地址', type: 'url', value: rule.gitea_origin,
754
+ onChange: (event) => updateParameter(index, { gitea_origin: event.target.value }), placeholder: 'https://git.example.com', style: { ...inputStyle, flex: '1 1 220px' } }) : null,
755
+ rule.kind === 'gitea_pr' ? h('input', { 'aria-label': 'Gitea 仓库', value: rule.repository,
756
+ onChange: (event) => updateParameter(index, { repository: event.target.value }), placeholder: '团队/仓库', style: { ...inputStyle, flex: '1 1 180px' } }) : null,
757
+ h('button', { type: 'button', style: secondaryButtonStyle,
758
+ onClick: () => setParameters((current) => current.filter((_, i) => i !== index)) }, '移除参数'))),
759
+ h('button', { type: 'button', disabled: parameters.length >= 12, style: secondaryButtonStyle,
760
+ onClick: () => setParameters((current) => [...current, { name: '', kind: 'text', required: true,
761
+ description: '', min: '', max: '', choices: '', gitea_origin: '', repository: '' }]) }, '添加参数'),
762
+ parameters.some((rule) => rule.kind === 'gitea_pr') && selectedEnrollment ? h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } },
763
+ h('input', { 'aria-label': '这个机器人的可信 Gitea 地址', type: 'url', value: trustedOrigin,
764
+ onChange: (event) => setTrustedOrigin(event.target.value), placeholder: '可信 Gitea 地址',
765
+ style: { ...inputStyle, flex: '1 1 260px' } }),
766
+ h('button', { type: 'button', disabled: busy || !trustedOrigin.trim(),
767
+ onClick: () => void saveTrustedOrigin(), style: secondaryButtonStyle }, '保存可信地址')) : null,
768
+ parameters.some((rule) => rule.kind === 'gitea_pr') ? h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } },
769
+ h('input', { 'aria-label': '只读 Gitea 访问令牌', type: 'password', autoComplete: 'off', value: giteaToken,
770
+ onChange: (event) => setGiteaToken(event.target.value), placeholder: tokenConfigured ? '本机已保存 Gitea 令牌' : '先保存只读 Gitea 令牌',
771
+ style: { ...inputStyle, flex: '1 1 260px' } }),
772
+ h('button', { type: 'button', disabled: busy || !giteaToken.trim(),
773
+ onClick: () => void saveGiteaToken(), style: secondaryButtonStyle }, '保存令牌')) : null,
774
+ parameters.some((rule) => rule.kind === 'gitea_pr') ? h('p', { style: mutedTextStyle },
775
+ '执行时会用这台 DSH 保存的只读令牌向你登记的可信 Gitea 地址读取 PR;标题和差异会交给当前团队模型处理。') : null,
776
+ h('label', null, '使用场景', h('select', { value: conversationKind,
777
+ onChange: (event) => { setConversationKind(event.target.value); setGroupId('') }, style: inputStyle },
778
+ h('option', { value: 'dm' }, '私聊'), h('option', { value: 'group' }, '指定群聊'))),
779
+ conversationKind === 'group' ? h('label', null, '群聊', h('select', { required: true, value: groupId,
780
+ onChange: (event) => setGroupId(event.target.value), style: inputStyle },
781
+ h('option', { value: '' }, '让群成员先 @ 机器人,再刷新列表'),
782
+ ...availableGroups.map((group) => h('option', { key: group.group_id, value: group.group_id },
783
+ `识别码 ${group.group_code} · ${group.group_id}`)))) : null,
784
+ conversationKind === 'group' ? h('button', { type: 'button', disabled: busy,
785
+ onClick: () => void load().catch((error) => setMessage(error.message)), style: secondaryButtonStyle }, '刷新群列表') : null,
786
+ conversationKind === 'group' && selectedEnrollment ? h('div', { style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' } },
787
+ h('span', { style: mutedTextStyle }, selectedEnrollment.owner_bound ? '已绑定你的飞书身份,可在群内批准任务' : '群内批准前需绑定你的飞书身份'),
788
+ h('button', { type: 'button', disabled: busy, onClick: () => void issueBindingCode(), style: secondaryButtonStyle },
789
+ selectedEnrollment.owner_bound ? '重新获取绑定码' : '获取身份绑定码')) : null,
790
+ conversationKind === 'group' && bindingCode ? h('p', { role: 'status', style: mutedTextStyle },
791
+ `请你本人私聊机器人发送“绑定身份 ${bindingCode}”。完成后在群里 @机器人发送“在本群启用任务 任务名称”,并按提示确认;结果会公开回复该群。`) : null,
792
+ h('label', null, '谁可以请求', h('select', { value: senderScope,
793
+ onChange: (event) => setSenderScope(event.target.value), style: inputStyle },
794
+ h('option', { value: 'any' }, conversationKind === 'group' ? '此群任何成员' : '任何能私聊此机器人者'),
795
+ h('option', { value: 'allowlist' }, '指定 IM 用户 ID'))),
796
+ senderScope === 'allowlist' ? h('input', { 'aria-label': '可请求的 IM 用户 ID', required: true, value: senderIds,
797
+ onChange: (event) => setSenderIds(event.target.value), placeholder: '多个 ID 用逗号分隔', style: inputStyle }) : null,
798
+ h('label', null, '有效期', h('select', { value: days, onChange: (event) => setDays(Number(event.target.value)), style: inputStyle },
799
+ h('option', { value: 1 }, '1 天'), h('option', { value: 7 }, '7 天'), h('option', { value: 30 }, '30 天')))),
800
+ cardBlockReason ? h('p', { role: 'status', style: mutedTextStyle }, cardBlockReason) : null,
801
+ h('button', { disabled: busy || Boolean(cardBlockReason),
802
+ style: primaryButtonStyle }, '开启这项任务'))),
803
+ h('details', { style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, padding: 12 } },
804
+ h('summary', { style: { cursor: 'pointer', fontWeight: 600 } }, '本机与指定 PR 审查设置'),
805
+ h('p', { style: mutedTextStyle }, '本机并发影响新任务;下方的 Codex/Claude Code 试用仅影响“指定 PR 差异审查授权”,不影响文字任务。'),
806
+ h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' } },
807
+ h('span', { style: mutedTextStyle }, `团队默认:先看 ${defaults?.context_messages ?? 10} 条前文、最多翻阅 ${defaults?.history_max_records ?? 100} 条平台记录、同时处理 ${defaults?.max_concurrency ?? 3} 项任务。`),
808
+ h('button', { type: 'button', onClick: () => void load().catch((error) => setMessage(error.message)), style: secondaryButtonStyle }, '刷新')),
809
+ h('form', { onSubmit: saveConcurrency, style: { display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'end' } },
810
+ h('label', null, '这台电脑最多同时处理',
811
+ h('select', { value: localConcurrency == null ? '' : String(localConcurrency),
812
+ onChange: (event) => setLocalConcurrency(event.target.value), style: inputStyle },
813
+ h('option', { value: '' }, '跟随团队默认'),
814
+ ...Array.from({ length: 8 }, (_, index) => h('option', { key: index + 1, value: index + 1 }, `${index + 1} 项任务`)))),
815
+ h('button', { disabled: busy, style: secondaryButtonStyle }, '保存本机并发')),
816
+ h('p', { style: mutedTextStyle }, `DSH 官方执行器:${executors.map((item) => `${item.title}${item.available ? '已接入 DSH' : '本机未启用'}`).join('、') || '暂未读取到'}。通用任务卡仍只使用受限 DSH;Codex/Claude Code 仅可在下方为已授权的指定 PR 审查手动开启试用。`),
817
+ h('form', { id: 'ant-pr-review-executor-form', onSubmit: savePrReviewExecutor,
818
+ style: { display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'end' } },
819
+ h('label', null, '指定 PR 差异审查使用',
820
+ h('select', { id: 'ant-pr-review-executor', value: prReviewExecutor,
821
+ onChange: (event) => setPrReviewExecutor(event.target.value), style: inputStyle },
822
+ h('option', { value: 'dsh' }, 'DSH 受限文字审查(默认)'),
823
+ ...[['codex', 'Codex'], ['claude-code', 'Claude Code']].map(([id, title]) => {
824
+ const available = executors.some((item) => item.id === id && item.available)
825
+ return h('option', { key: id, value: id, disabled: !available },
826
+ `${title}${available ? ' · 本机权限试用' : ' · 未启用或未读取到'}`)
827
+ }))),
828
+ h('button', { disabled: busy, style: secondaryButtonStyle }, '保存审查执行方')),
829
+ prReviewExecutor !== 'dsh' ? h('p', { role: 'status', style: mutedTextStyle },
830
+ '选择本机执行器试用后,它沿用你在 Codex/Claude Code 中已有的权限;ANT 固定 PR 和启动参数,但不能保证执行器只读。') : null),
831
+ message ? h('p', { role: 'status', style: mutedTextStyle }, message) : null)
832
+ }
833
+
834
+ function DelegationSettings({ remote, revision, onChanged, open, onOpenChange }) {
360
835
  const [bots, setBots] = useState([])
361
836
  const [enrolledBots, setEnrolledBots] = useState([])
362
837
  const [groups, setGroups] = useState([])
@@ -368,6 +843,7 @@ window.__ModuleLoader__.load({
368
843
  const [groupId, setGroupId] = useState('')
369
844
  const [bindingCode, setBindingCode] = useState('')
370
845
  const [groupReady, setGroupReady] = useState(false)
846
+ const [groupError, setGroupError] = useState('')
371
847
  const [origin, setOrigin] = useState('')
372
848
  const [repository, setRepository] = useState('')
373
849
  const [prNumber, setPrNumber] = useState('')
@@ -396,6 +872,10 @@ window.__ModuleLoader__.load({
396
872
  setTeamReady(true)
397
873
  setBots(botResponse.ok && Array.isArray(botData.bots) ? botData.bots : [])
398
874
  setGroupReady(enrolledResponse.ok && groupResponse.ok)
875
+ setGroupError(enrolledResponse.ok && groupResponse.ok ? ''
876
+ : enrolledResponse.status === 426 || groupResponse.status === 426
877
+ ? '团队服务端版本不支持旧版 PR 群聊设置,请联系管理员升级。'
878
+ : enrolledData?.message || groupData?.message || '暂时无法读取群聊设置,请稍后刷新。')
399
879
  setEnrolledBots(enrolledResponse.ok && Array.isArray(enrolledData) ? enrolledData : [])
400
880
  setGroups(groupResponse.ok && Array.isArray(groupData) ? groupData : [])
401
881
  setGrants(Array.isArray(grantData) ? grantData : [])
@@ -404,7 +884,7 @@ window.__ModuleLoader__.load({
404
884
  if (!botResponse.ok) setMessage(botData.message || '请先配置 DSH-IM 飞书机器人。')
405
885
  else if (!statusResponse.ok) setMessage(statusData.message || '本机任务记录暂时无法读取。')
406
886
  }
407
- useEffect(() => { void refresh().catch((error) => { setTeamReady(false); setMessage(error.message) }) }, [remote])
887
+ useEffect(() => { void refresh().catch((error) => { setTeamReady(false); setMessage(error.message) }) }, [remote, revision])
408
888
  const saveToken = async (event) => {
409
889
  event.preventDefault()
410
890
  if (!token.trim()) return
@@ -436,8 +916,8 @@ window.__ModuleLoader__.load({
436
916
  }
437
917
  const trustedOrigin = selectedEnrollment.gitea_origin
438
918
  if (!window.confirm(conversationKind === 'group'
439
- ? `确认允许所选群的成员请求 ${trustedOrigin} 上 ${repository} PR #${prNumber} 的只读审查?结果将公开回复原群/原话题,直到你撤销或授权到期。目前没有每日请求次数上限。`
440
- : `开启后,此飞书机器人将由 ANT 接收消息,只处理你授权的 PR 审查;它不再运行 DSH-IM 的普通聊天。Gitea 令牌会发送到 ${origin},请核对地址。目前没有每日请求次数上限。请确认这是专用协作机器人。`)) return
919
+ ? `确认允许所选群的成员请求 ${trustedOrigin} 上 ${repository} PR #${prNumber} 的差异审查?执行方按上方本机设置运行,结果将公开回复原群/原话题,直到你撤销或授权到期。目前没有每日请求次数上限。`
920
+ : `开启后,此飞书机器人由 ANT 接收消息并继续处理普通聊天;只有你授权的 PR 才能被请求审查。Gitea 令牌会发送到 ${origin},请核对地址。目前没有每日请求次数上限。请确认这是专用协作机器人。`)) return
441
921
  setBusy(true)
442
922
  try {
443
923
  const response = await fetch('/api/ant-teams/delegation/grants', {
@@ -456,6 +936,7 @@ window.__ModuleLoader__.load({
456
936
  ? '本群已启用。在群里 @机器人发送“审查 PR 编号”可试一次;结果会公开回复原群/原话题。'
457
937
  : '委托已保存;机器人连接后可私聊发送“审查 PR 编号”试一次,以核对 Gitea 令牌和实际回复。')
458
938
  await refresh()
939
+ onChanged?.()
459
940
  } catch (error) { setMessage(error.message) }
460
941
  finally { setBusy(false) }
461
942
  }
@@ -464,7 +945,7 @@ window.__ModuleLoader__.load({
464
945
  if (!bot || !groupReady) { setMessage('请先选择可用机器人,并确认团队服务端已支持群聊委托。'); return }
465
946
  const trustedOrigin = normalizeServiceUrl(origin)
466
947
  if (!trustedOrigin) { setMessage('请先填写有效的 Gitea 服务地址(HTTPS,或本机测试地址)。'); return }
467
- if (!window.confirm('此机器人会切换为 ANT 独占收件模式,停止 DSH-IM 原有普通聊天。更换机器人账号或可信 Gitea 地址时,不匹配的旧授权会撤销。请确认这是专用协作机器人。')) return
948
+ if (!window.confirm('启用后,ANT 会接收这个机器人的消息并继续处理普通聊天;DSH-IM 原有会话桥不再单独回复。只有你开放的任务才可执行。更换机器人账号或可信 Gitea 地址时,不匹配的旧授权会撤销。')) return
468
949
  setBusy(true)
469
950
  try {
470
951
  const response = await fetch('/api/ant-teams/delegation/enrolled-bots', {
@@ -477,23 +958,25 @@ window.__ModuleLoader__.load({
477
958
  throw new Error(result.message || '无法启用机器人。')
478
959
  }
479
960
  setOrigin(trustedOrigin)
480
- setMessage('专用机器人已启用,可创建私聊授权。若要开放群聊,请让目标群成员先 @它一次。')
961
+ setMessage('协作机器人已启用,私聊可以正常对话。要开放任务,请先创建授权;要在群里使用,请让群成员先 @它一次。')
481
962
  await refresh()
963
+ onChanged?.()
482
964
  } catch (error) { setMessage(error.message) }
483
965
  finally { setBusy(false) }
484
966
  }
485
967
  const stopBot = async () => {
486
968
  const bot = enrolledBots.find((item) => item.bot_id === botId)
487
- if (!bot || !window.confirm('停止此机器人的 ANT 群聊接收,并撤销它的全部委托授权?若需恢复普通聊天,还要在 DSH-IM 中切回普通模式。')) return
969
+ if (!bot || !window.confirm('停止 ANT 接收这个机器人的协作消息,并撤销全部委托授权?ANT 处理的普通聊天也会停止;如需恢复 DSH-IM 原有聊天,还要在 DSH-IM 中切回普通模式。')) return
488
970
  setBusy(true)
489
971
  try {
490
972
  const response = await fetch('/api/ant-teams/delegation/enrolled-bots', {
491
973
  method: 'DELETE', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ bot_id: bot.bot_id }),
492
974
  })
493
975
  if (!response.ok) throw new Error('停止机器人失败,请重试。')
494
- setMessage('已撤销此机器人的委托授权。若需普通聊天,请在 DSH-IM 中切回普通模式。')
976
+ setMessage('ANT 已停止接收这个机器人的消息并撤销任务授权。若要恢复 DSH-IM 原有聊天,请到 DSH-IM 切回普通模式。')
495
977
  setBindingCode('')
496
978
  await refresh()
979
+ onChanged?.()
497
980
  } catch (error) { setMessage(error.message) }
498
981
  finally { setBusy(false) }
499
982
  }
@@ -521,8 +1004,9 @@ window.__ModuleLoader__.load({
521
1004
  method: 'DELETE', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: grant.id }),
522
1005
  })
523
1006
  if (!response.ok) throw new Error('停止授权失败,请重试。')
524
- setMessage('已停止新的委托请求。专用机器人若需恢复普通聊天,请在 DSH-IM 中切回普通模式。')
1007
+ setMessage('已停止这项 PR 授权;机器人其他聊天和任务不受影响。')
525
1008
  await refresh()
1009
+ onChanged?.()
526
1010
  } catch (error) { setMessage(error.message) }
527
1011
  finally { setBusy(false) }
528
1012
  }
@@ -531,10 +1015,12 @@ window.__ModuleLoader__.load({
531
1015
  const selectedEnrollment = enrolledBots.find((item) => item.bot_fingerprint === selectedBot?.fingerprint)
532
1016
  const selectedEnrolled = Boolean(selectedEnrollment)
533
1017
  const botGroups = groups.filter((item) => item.bot_fingerprint === selectedBot?.fingerprint)
534
- return h('section', { style: panelStyle, 'aria-label': '委托同事请求 DSH 审查' },
535
- h('h3', { style: { margin: 0, fontSize: 16 } }, '委托同事请求 DSH 审查'),
536
- h('p', { style: mutedTextStyle }, '支持飞书私聊和你逐个启用的群聊,只处理指定 Gitea PR 的只读审查。群里的结果默认公开回复原群/原话题。'),
537
- h('p', { style: mutedTextStyle }, '审查时,PR 标题和差异会交给当前获准的团队模型处理;Gitea 令牌只保存在本机,不发送给 ANT 服务端。'),
1018
+ return h('details', { id: 'ant-pr-review-settings', open,
1019
+ onToggle: (event) => onOpenChange?.(event.currentTarget.open),
1020
+ style: panelStyle, 'aria-label': '指定 PR 差异审查授权' },
1021
+ h('summary', { style: { cursor: 'pointer', fontWeight: 600 } }, '指定 PR 差异审查授权'),
1022
+ h('p', { style: mutedTextStyle }, '为指定仓库和 PR 编号开放审查。已有授权继续有效;文字任务请在上方创建。'),
1023
+ h('p', { style: mutedTextStyle }, '这里管理谁能请求指定 PR;执行方在上方“本机与审查设置”选择。PR 标题和差异会交给所选执行方;ANT 不会主动传入 Gitea 令牌,但本机执行器仍沿用它已有的文件权限。'),
538
1024
  h('form', { onSubmit: saveToken, style: { display: 'flex', flexWrap: 'wrap', gap: 8 } },
539
1025
  h('label', { style: { width: '100%', fontSize: 13 } }, `Gitea 访问令牌(仅保存在本机 DSH)${tokenReady ? ' · 已配置' : ''}`),
540
1026
  h('input', { type: 'password', autoComplete: 'off', value: token, onChange: (event) => setToken(event.target.value),
@@ -563,7 +1049,7 @@ window.__ModuleLoader__.load({
563
1049
  selectedEnrollment.owner_bound ? '重新绑定飞书身份' : '绑定我的飞书身份') : null,
564
1050
  selectedEnrolled ? h('button', { type: 'button', disabled: busy,
565
1051
  onClick: () => void stopBot(), style: secondaryButtonStyle }, '停止接收并撤销授权') : null) :
566
- h('p', { style: mutedTextStyle }, '当前团队服务端尚未提供新版委托设置,请联系管理员升级。'),
1052
+ h('p', { style: mutedTextStyle }, groupError),
567
1053
  bindingCode && selectedEnrolled ? h('p', { role: 'status', style: mutedTextStyle },
568
1054
  `请你本人私聊这个机器人发送“绑定身份 ${bindingCode}”;5 分钟内有效,不要把码发在群里。绑定后可在群里 @它发送“在本群启用 <完整 PR 链接> 审查”,再按提示确认。`) : null,
569
1055
  h('label', { htmlFor: 'ant-delegation-kind' }, '授权给谁使用'),
@@ -615,10 +1101,10 @@ window.__ModuleLoader__.load({
615
1101
  h('button', { type: 'button', disabled: busy, onClick: () => void refresh().catch((error) => { setTeamReady(false); setMessage(error.message) }),
616
1102
  style: secondaryButtonStyle }, '刷新状态')),
617
1103
  ...(tasks.length ? tasks.map((task, index) => h('p', { key: `${task.created_at}-${index}`, style: mutedTextStyle },
618
- `${task.created_at ? new Date(task.created_at).toLocaleString('zh-CN') + ' · ' : ''}${task.repository || '请求'}${task.pr_number ? ` · PR #${task.pr_number}` : ''} · ${{
619
- pending: '待审查', reply_pending: '待回复', reply_attempted: '回复状态待核对,请查看飞书原会话',
1104
+ `${task.created_at ? new Date(task.created_at).toLocaleString('zh-CN') + ' · ' : ''}${task.repository || '请求'}${task.pr_number ? ` · PR #${task.pr_number}` : ''} · ${task.outcome === 'canceled' ? '已取消' : task.outcome === 'failed' ? '未完成' : ({
1105
+ pending: '排队中', running: '处理中', reply_pending: '待回复', reply_attempted: '回复状态待核对,请查看飞书原会话',
620
1106
  done: '已回复',
621
- }[task.status] || task.status}`)) : [h('p', { style: mutedTextStyle }, '暂无任务记录。')]),
1107
+ }[task.status] || task.status)}`)) : [h('p', { style: mutedTextStyle }, '暂无任务记录。')]),
622
1108
  message ? h('p', { role: 'status', style: mutedTextStyle }, message) : null)
623
1109
  }
624
1110