@ai-wendao/ant 0.4.13 → 0.4.15

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
@@ -20,12 +20,13 @@ window.__ModuleLoader__.load({
20
20
  const PLUGIN_INSTALL_RECORD_PREFIX = 'ant-reviewed-plugin-install:'
21
21
  const ANT_SETTINGS_TAB_STORAGE_KEY = 'ant-settings-tab'
22
22
  const ANT_SETTINGS_TABS = [
23
- ['connection', '团队连接'], ['models', '团队模型'], ['bot-chat', '机器人与聊天'],
24
- ['capability', '能力授权'], ['local', '本机与任务'], ['updates', '关于'],
23
+ ['connection', '团队连接'], ['models', '团队模型'], ['collaboration', '协作'],
24
+ ['local', '本机与任务'], ['updates', '关于'],
25
25
  ]
26
26
  const loadAntSettingsTab = () => {
27
27
  try {
28
28
  const saved = localStorage.getItem(ANT_SETTINGS_TAB_STORAGE_KEY)
29
+ if (saved === 'bot-chat' || saved === 'capability') return 'collaboration'
29
30
  return ANT_SETTINGS_TABS.some(([id]) => id === saved) ? saved : 'connection'
30
31
  } catch { return 'connection' }
31
32
  }
@@ -405,7 +406,7 @@ window.__ModuleLoader__.load({
405
406
  h('span', null, bindingSummary),
406
407
  !bindingReadyForSummary ? h('button', { type: 'button', onClick: () => selectSettingsTab('connection'),
407
408
  style: secondaryButtonStyle }, binding.state === 'error' ? '检查连接' : '查看连接设置')
408
- : activeTab === 'connection' ? h('button', { type: 'button', onClick: () => selectSettingsTab('bot-chat'),
409
+ : activeTab === 'connection' ? h('button', { type: 'button', onClick: () => selectSettingsTab('collaboration'),
409
410
  style: secondaryButtonStyle }, '设置或管理机器人') : null),
410
411
  h('div', { style: { position: 'sticky', top: 0, zIndex: 8, display: 'grid', gap: 8, padding: '6px 0',
411
412
  backgroundColor: 'var(--dsw-alias-bg-base)' } },
@@ -472,17 +473,20 @@ window.__ModuleLoader__.load({
472
473
  const [repositoryCacheLimitGib, setRepositoryCacheLimitGib] = useState(10)
473
474
  const [botId, setBotId] = useState('')
474
475
  const [trustedOrigin, setTrustedOrigin] = useState('')
475
- const [bindingCode, setBindingCode] = useState('')
476
476
  const [conversationKind, setConversationKind] = useState('dm')
477
477
  const [groupId, setGroupId] = useState('')
478
- const [chatGroupId, setChatGroupId] = useState('')
478
+ const [ruleDescription, setRuleDescription] = useState('')
479
+ const [triggerPhrases, setTriggerPhrases] = useState('')
480
+ const recognizedBaselineRef = useRef(null)
481
+ const recognizedDescriptionRef = useRef(null)
482
+ const [recognitionBusy, setRecognitionBusy] = useState(false)
479
483
  const [title, setTitle] = useState('')
480
484
  const [intent, setIntent] = useState('')
481
- const [examples, setExamples] = useState('')
482
485
  const [taskPrompt, setTaskPrompt] = useState('')
483
486
  const [runner, setRunner] = useState('dsh')
484
487
  const [reviewers, setReviewers] = useState(['dsh'])
485
488
  const [parameters, setParameters] = useState([])
489
+ const [crossReviewOpen, setCrossReviewOpen] = useState(false)
486
490
  const [giteaToken, setGiteaToken] = useState('')
487
491
  const [tokenConfigured, setTokenConfigured] = useState(false)
488
492
  const [senderScope, setSenderScope] = useState('any')
@@ -550,23 +554,41 @@ window.__ModuleLoader__.load({
550
554
  : !selectedEnrollmentBound ? '请先把机器人绑定到这台 DSH,再修改私聊设置。'
551
555
  : !dmChatSettingAvailable ? '当前团队服务端尚未提供私聊开关,请先升级服务端。'
552
556
  : dmChatEnabled
553
- ? '允许此机器人接收私聊;任务仍按能力授权执行。'
554
- : '新私聊消息不会交给 DSH;所有者身份绑定仍可用,已提交任务仍会在原私聊回复。已开放能力不撤销,重新开启后未过期能力恢复触发。'
557
+ ? '允许此机器人接收私聊;任务仍按已开放的规则执行。'
558
+ : '新私聊消息不会交给 DSH;已提交任务仍会在原私聊回复。已开放能力不撤销,重新开启后未过期能力恢复触发。'
555
559
  const availableGroups = groups.filter((group) => group.bot_fingerprint === selectedBot?.fingerprint)
556
- const selectedChatGroup = availableGroups.find((group) => group.group_id === chatGroupId)
557
560
  const groupChatLoadFailed = loadFailures.includes('groups')
558
561
  const activeCards = cards.filter((card) => !card.revoked_at && card.expires_at * 1000 > Date.now())
559
- const selectedGroupHasTasks = Boolean(selectedBot && chatGroupId && (
560
- activeCards.some((card) => card.bot_fingerprint === selectedBot.fingerprint
561
- && card.conversation_kind === 'group' && card.conversation_id === chatGroupId)))
562
562
  const connectedBots = bots.filter((bot) => bot.available && bot.connected)
563
563
  const authorizationLoadError = loadFailures.includes('cards')
564
564
  const enrollmentLoadError = loadFailures.includes('enrolled-bots')
565
565
  const enrolledCount = enrolled.filter((item) => item.device_bound_to_current
566
566
  && connectedBots.some((bot) => bot.fingerprint === item.bot_fingerprint)).length
567
567
  const selectedTaskCount = activeCards.filter((card) => card.bot_fingerprint === selectedBot?.fingerprint).length
568
+ const legacyTriggerCardCount = activeCards.filter((card) => card.bot_fingerprint === selectedBot?.fingerprint
569
+ && (!Array.isArray(card.trigger_phrases) || !card.trigger_phrases.length)).length
570
+ const collaborationStatus = loading ? '正在读取机器人与任务状态…'
571
+ : botLoadError ? '暂时无法读取机器人状态。请刷新后再试。'
572
+ : connectedBots.length === 0 ? '尚未发现已连接的机器人。请先在 DSH-IM 中连接飞书机器人。'
573
+ : selectedBot && !botConnected ? `“${selectedBot.name}”当前离线;请先在 DSH-IM 恢复连接。`
574
+ : enrollmentLoadError ? selectedBot
575
+ ? `“${selectedBot.name}”已连接;暂时无法确认协作是否已启用,请刷新状态。`
576
+ : '机器人已连接;协作启用状态暂未读取,请刷新状态。'
577
+ : selectedBot && !selectedEnrollmentBound ? selectedEnrollment
578
+ ? `“${selectedBot.name}”已连接;原协作配置尚未绑定到这台 DSH,请在本机重新启用。若原设备令牌曾被撤销,旧任务权限已关闭,绑定后需重新开放。`
579
+ : `“${selectedBot.name}”已连接;请先启用本机协作。`
580
+ : enrolledCount === 0 ? '机器人已连接;请选择并启用协作。'
581
+ : !selectedBot ? '已有协作机器人;请选择一台,查看已开放的能力和聊天范围。'
582
+ : authorizationLoadError ? `“${selectedBot.name}”已启用协作;已开放任务暂未读取,请刷新状态。`
583
+ : !dmChatEnabled ? `“${selectedBot.name}”已启用协作;私聊已关闭,群聊仍按各自设置工作。`
584
+ : selectedTaskCount === 0 ? `“${selectedBot.name}”已启用协作;下一步开放一项能力。你也可以先私聊自测普通聊天。`
585
+ : `“${selectedBot.name}”已启用协作 · ${selectedTaskCount} 项能力已开放。可以私聊自测。`
568
586
  const nativeReviewerSelected = reviewers.some((item) => ['codex', 'claude-code'].includes(item))
569
587
  const hasPrParameter = parameters.some((item) => item.kind === 'gitea_pr')
588
+ const hasSelectedPrRule = activeCards.some((card) => card.bot_fingerprint === selectedBot?.fingerprint
589
+ && Array.isArray(card.parameters) && card.parameters.some((rule) => rule.kind === 'gitea_pr'))
590
+ const showPrSecuritySettings = Boolean(selectedBot && (hasPrParameter || hasSelectedPrRule))
591
+ const selectedGroupSummary = availableGroups.find((group) => group.group_id === groupId)?.group_code || groupId
570
592
  const codexAvailable = executors.some((item) => item.id === 'codex-readonly' && item.available)
571
593
  const claudeCodeAvailable = executors.some((item) => item.id === 'claude-code' && item.available)
572
594
  const dshAvailable = executors.some((item) => item.id === 'dsh' && item.available)
@@ -582,23 +604,57 @@ window.__ModuleLoader__.load({
582
604
  }
583
605
  const requesterSummary = senderScope === 'allowlist'
584
606
  ? `仅指定成员:${senderIds.trim() || '尚未填写成员 ID'}`
585
- : conversationKind === 'group' ? `群 ${groupId || '尚未选择'} 的所有成员` : '任何能私聊此机器人的人'
607
+ : conversationKind === 'group' ? '群内所有成员' : '任何能私聊此机器人的人'
586
608
  const resultRouteSummary = conversationKind === 'group' ? '回复到原群(群内可见)' : '回复到发起任务的私聊'
609
+ const ruleTriggerSummary = triggerPhrases.split('\n').map((item) => item.trim()).filter(Boolean)
610
+ const ruleSummaryReady = Boolean(title || intent || ruleTriggerSummary.length || parameters.length || taskPrompt)
611
+ const parameterSummary = (rule) => {
612
+ const parts = [rule.required === false ? '可选' : '必填']
613
+ const choices = Array.isArray(rule.choices) ? rule.choices
614
+ : typeof rule.choices === 'string' ? rule.choices.split(/[,,\n]/u).map((item) => item.trim()).filter(Boolean) : []
615
+ if (rule.kind === 'gitea_pr' && rule.repository) parts.push(`仓库 ${rule.repository}`)
616
+ else if (rule.kind === 'choice' && choices.length) parts.push(`选项 ${choices.join('、')}`)
617
+ else if (rule.kind === 'integer') {
618
+ const minValue = rule.min == null || rule.min === '' ? null : Number(rule.min)
619
+ const maxValue = rule.max == null || rule.max === '' ? null : Number(rule.max)
620
+ const minimum = minValue != null && Number.isFinite(minValue) ? minValue : null
621
+ const maximum = maxValue != null && Number.isFinite(maxValue) ? maxValue : null
622
+ if (minimum != null && maximum != null) parts.push(`范围 ${minimum}–${maximum}`)
623
+ else if (minimum != null) parts.push(`最少 ${minimum}`)
624
+ else if (maximum != null) parts.push(`最多 ${maximum}`)
625
+ } else if (rule.kind === 'text' && rule.max != null && rule.max !== ''
626
+ && Number.isFinite(Number(rule.max))) {
627
+ parts.push(`文字最多 ${Number(rule.max)} 字节`)
628
+ }
629
+ return `${rule.description?.trim() || rule.name}(${parts.join(';')})`
630
+ }
631
+ const recognizedInputsSummary = parameters.length
632
+ ? `请求时还需提供:${parameters.map(parameterSummary).join(';')}`
633
+ : '无需额外参数,按请求中的内容处理'
634
+ const ruleSummaryNarrative = ruleSummaryReady
635
+ ? `${ruleTriggerSummary.length ? `消息以“${ruleTriggerSummary.join('”或“')}”开头时,` : ''}${intent.trim() || title.trim()}${taskPrompt.trim() ? `;${taskPrompt.trim()}` : ''};${recognizedInputsSummary}`
636
+ : '识别后会用一段摘要呈现系统理解的任务。'
587
637
  const executorSummary = runner === 'codex' ? '本机 Codex · PR 快照'
588
638
  : runner === 'claude-code' ? '本机 Claude Code · PR 快照' : 'DSH · 当前授权模型'
589
- const resourceSummary = nativeReviewerSelected ? '指定仓库的已核验 PR 快照'
590
- : '当前会话中 DSH 实际可见的内容'
639
+ const crossReviewSummary = reviewers.filter((item) => item !== runner).map(reviewerLabel).join('、')
591
640
  const cardBlockReason = enrollmentLoadError ? '暂时无法核对机器人是否已启用协作,请刷新状态后再试。'
592
641
  : !botConnected || !selectedEnrollmentBound ? '先在上方把机器人绑定到这台 DSH。'
642
+ : loadFailures.includes('settings') || defaults?.task_rule_schema_version !== 2
643
+ ? '团队服务端尚未支持触发规则的持久化与执行前核验;请先升级 ANT 服务端。'
644
+ : !ruleDescription.trim() ? '请先用自然语言描述要做的任务。'
645
+ : recognizedDescriptionRef.current !== null && recognizedDescriptionRef.current !== ruleDescription.trim()
646
+ ? '描述已修改,请重新识别,让执行规则与描述保持一致。'
647
+ : !title.trim() || !intent.trim() || !taskPrompt.trim() ? '先识别任务描述并检查摘要,再开放任务。'
648
+ : !triggerPhrases.trim() ? '描述里还没有明确写出何时触发。请补充触发方式并重新识别。'
593
649
  : conversationKind === 'group' && loadFailures.includes('groups') ? '暂时无法核对目标群,请刷新状态后再试。'
594
- : conversationKind === 'group' && !groupId ? '请在“参数与授权范围”中选择目标群;若列表为空,让群成员先 @机器人,再刷新群列表。'
595
- : senderScope === 'allowlist' && !senderIds.trim() ? '请在“参数与授权范围”中填写可请求成员的 IM ID。'
650
+ : conversationKind === 'group' && !groupId ? '请在“请求范围”中选择目标群;若列表为空,让群成员先 @机器人,再刷新群列表。'
651
+ : senderScope === 'allowlist' && !senderIds.trim() ? '请填写可请求成员的 IM ID。'
596
652
  : parameters.some((rule) => rule.kind === 'gitea_pr') && !tokenConfigured ? '请先在“本机与任务”中保存本机 Gitea 只读令牌。'
597
- : parameters.some((rule) => rule.kind === 'gitea_pr') && !selectedEnrollment?.gitea_origin
598
- ? '请先在“机器人与聊天”中登记这个机器人的可信 Gitea 地址。'
653
+ : parameters.some((rule) => rule.kind === 'gitea_pr') && !selectedEnrollment?.gitea_origin
654
+ ? 'PR 来源尚未信任。请到“本机与任务”设置可信 Gitea 来源。'
599
655
  : parameters.some((rule) => rule.kind === 'gitea_pr')
600
656
  && normalizeServiceUrl(trustedOrigin) !== selectedEnrollment?.gitea_origin
601
- ? '请先保存机器人级可信 Gitea 地址;PR 参数会自动使用这个地址。'
657
+ ? '请先在“本机与任务”保存可信 Gitea 来源;任务描述中的 PR 链接会使用这个来源。'
602
658
  : nativeReviewerSelected && (parameters.length !== 1 || parameters[0].kind !== 'gitea_pr')
603
659
  ? 'Codex 和 Claude Code 目前只支持单个 Gitea PR 审查任务。'
604
660
  : nativeReviewerSelected && !parameters[0].required
@@ -613,57 +669,115 @@ window.__ModuleLoader__.load({
613
669
  const loadNames = { bots: '机器人', 'enrolled-bots': '协作启用状态', groups: '群聊列表',
614
670
  cards: '任务授权', settings: '团队默认值', executors: '本机执行器',
615
671
  'local-settings': '本机设置', status: '最近任务' }
616
- const updateParameter = (index, patch) => setParameters((current) => current.map((rule, i) => i === index ? { ...rule, ...patch } : rule))
617
672
  const changeReviewer = (id, enabled) => {
618
673
  const next = enabled ? [...reviewers, id] : reviewers.filter((item) => item !== id)
619
674
  if (!next.length) { setMessage('至少保留一位审查 Agent。'); return }
620
675
  setReviewers([...new Set(next)])
621
676
  if (!next.includes(runner)) setRunner(next[0])
622
677
  }
623
- const startNewCard = () => {
678
+ const selectRunner = (id) => {
679
+ setRunner(id)
680
+ setReviewers((current) => current.length > 1
681
+ ? [...new Set([...current, id])]
682
+ : [id])
683
+ }
684
+ const startNewCard = (context = {}) => {
624
685
  setEditingCardId(null); setEditingExpiresAt(null)
625
686
  setDraftNotice('')
626
- setTitle(''); setIntent(''); setExamples(''); setTaskPrompt(''); setRunner('dsh')
627
- setReviewers(['dsh'])
628
- setParameters([]); setConversationKind('dm'); setGroupId('')
687
+ recognizedBaselineRef.current = null
688
+ recognizedDescriptionRef.current = null
689
+ setRuleDescription('')
690
+ setTriggerPhrases('')
691
+ setTitle(''); setIntent(''); setTaskPrompt(''); setRunner('dsh')
692
+ setReviewers(['dsh']); setCrossReviewOpen(false)
693
+ setParameters([]); setConversationKind(context.conversationKind || 'dm')
694
+ setGroupId(context.groupId || '')
629
695
  setSenderScope('any'); setSenderIds(''); setDays(7); setCreateOpen(true)
696
+ if (context.botId) setBotId(context.botId)
630
697
  }
698
+ const taskRuleDraftFromCard = (card, title) => ({
699
+ title: title || '', intent: card.intent || '', task_prompt: card.task_prompt || '',
700
+ trigger_phrases: Array.isArray(card.trigger_phrases) ? card.trigger_phrases.map((item) => item.trim()).filter(Boolean) : [],
701
+ parameters: (card.parameters || []).map((rule) => ({
702
+ name: rule.name, kind: rule.kind, required: rule.required,
703
+ description: rule.description || '', min: rule.min == null ? '' : String(rule.min),
704
+ max: rule.max == null ? '' : String(rule.max),
705
+ choices: Array.isArray(rule.choices) ? rule.choices.join(', ') : String(rule.choices ?? ''),
706
+ gitea_origin: rule.gitea_origin || '', repository: rule.repository || '',
707
+ })),
708
+ })
631
709
  const editCard = (card, copy = false) => {
632
710
  setBotId(card.bot_id)
633
711
  setTrustedOrigin(enrolled.find((bot) => bot.bot_id === card.bot_id)?.gitea_origin || '')
634
712
  setEditingCardId(copy ? null : card.id); setEditingExpiresAt(copy ? null : card.expires_at)
635
- setTitle(copy ? `${card.title.slice(0, 94)}(副本)` : card.title)
713
+ const taskDraft = taskRuleDraftFromCard(card, copy ? `${card.title.slice(0, 94)}(副本)` : card.title)
714
+ recognizedBaselineRef.current = taskDraft
715
+ const existingDescription = card.rule_description || card.intent || ''
716
+ recognizedDescriptionRef.current = existingDescription.trim() || null
717
+ const legacyTriggerRule = !Array.isArray(card.trigger_phrases) || !card.trigger_phrases.length
718
+ setDraftNotice(legacyTriggerRule
719
+ ? '这项旧任务没有明确触发方式;新版插件不会按它启动任务。请升级 ANT 服务端后重新描述、识别并开放规则。'
720
+ : '')
721
+ setRuleDescription(existingDescription)
722
+ setTriggerPhrases(taskDraft.trigger_phrases.join('\n'))
723
+ setTitle(taskDraft.title)
636
724
  if (copy) setDays(7)
637
- setIntent(card.intent); setExamples((card.examples || []).join('\n'))
638
- setTaskPrompt(card.task_prompt); setRunner(card.runner)
725
+ setIntent(taskDraft.intent)
726
+ setTaskPrompt(taskDraft.task_prompt); setRunner(card.runner)
639
727
  setReviewers(Array.isArray(card.reviewers) && card.reviewers.length ? card.reviewers : [card.runner])
640
- setParameters((card.parameters || []).map((rule) => ({
641
- name: rule.name, kind: rule.kind, required: rule.required,
642
- description: rule.description || '', min: rule.min == null ? '' : String(rule.min),
643
- max: rule.max == null ? '' : String(rule.max), choices: (rule.choices || []).join(', '),
644
- gitea_origin: rule.gitea_origin || '', repository: rule.repository || '',
645
- })))
728
+ setCrossReviewOpen(false)
729
+ setParameters(taskDraft.parameters)
646
730
  setConversationKind(card.conversation_kind); setGroupId(card.conversation_id || '')
647
731
  setSenderScope(card.sender_scope); setSenderIds((card.sender_ids || []).join(', '))
648
732
  setCreateOpen(true)
649
733
  }
650
- const preparePrExample = () => {
651
- setEditingCardId(null); setEditingExpiresAt(null)
652
- setTitle('审查 Gitea PR 差异')
653
- setIntent('同事希望查看指定 Gitea PR 的代码差异并获得审查意见。')
654
- setExamples('请帮我审查 123 号 PR\n看看刚才提到的 PR 有什么风险')
655
- setTaskPrompt('结合已核验 PR 的完整代码快照和差异,给出可定位的问题;没有足够上下文时说明局限,不修改代码。')
656
- setParameters([{ name: 'pr_number', kind: 'gitea_pr', required: true,
657
- description: '要审查的 PR 编号', min: '1', max: '', choices: '',
658
- gitea_origin: selectedEnrollment?.gitea_origin || trustedOrigin || '', repository: '' }])
659
- const availableReviewers = [
660
- ...(codexAvailable ? ['codex'] : []),
661
- ...(claudeCodeAvailable ? ['claude-code'] : []),
662
- ]
663
- const selected = availableReviewers.length ? availableReviewers : ['dsh']
664
- setReviewers(selected)
665
- setRunner(selected[0])
666
- setCreateOpen(true)
734
+ const applyRuleDraft = (draft) => {
735
+ setTitle(draft.title)
736
+ setIntent(draft.intent)
737
+ setTriggerPhrases(draft.trigger_phrases.join('\n'))
738
+ setTaskPrompt(draft.task_prompt)
739
+ setParameters(draft.parameters)
740
+ }
741
+ const identifyRule = async () => {
742
+ if (!ruleDescription.trim()) { setMessage('先描述这项任务何时触发、要做什么以及涉及哪些资料。'); return }
743
+ setRecognitionBusy(true); setMessage('')
744
+ try {
745
+ const response = await fetch('/api/ant-teams/delegation/recognize', {
746
+ method: 'POST', headers: { 'content-type': 'application/json' },
747
+ body: JSON.stringify({ description: ruleDescription }),
748
+ })
749
+ const result = await readLocalJsonResponse(response)
750
+ if (!response.ok) throw new Error(result.message || '规则识别失败。')
751
+ const candidate = {
752
+ title: typeof result.title === 'string' ? result.title : '',
753
+ intent: typeof result.intent === 'string' ? result.intent : '',
754
+ trigger_phrases: Array.isArray(result.trigger_phrases) ? result.trigger_phrases : [],
755
+ task_prompt: typeof result.task_prompt === 'string' ? result.task_prompt : '',
756
+ parameters: Array.isArray(result.parameters) ? result.parameters.map((rule) => ({
757
+ name: typeof rule?.name === 'string' ? rule.name : '',
758
+ kind: typeof rule?.kind === 'string' ? rule.kind : 'text',
759
+ required: rule?.required !== false,
760
+ description: typeof rule?.description === 'string' ? rule.description : '',
761
+ min: rule?.min == null ? '' : String(rule.min),
762
+ max: rule?.max == null ? '' : String(rule.max),
763
+ choices: Array.isArray(rule?.choices) ? rule.choices.join(', ')
764
+ : typeof rule?.choices === 'string' ? rule.choices : '',
765
+ gitea_origin: typeof rule?.gitea_origin === 'string' ? rule.gitea_origin : '',
766
+ repository: typeof rule?.repository === 'string' ? rule.repository : '',
767
+ })) : [],
768
+ }
769
+ if (!candidate.title || !candidate.intent || !candidate.task_prompt || !candidate.trigger_phrases.length) {
770
+ throw new Error('识别结果不完整。请在描述中写清楚何时执行,再重新识别。')
771
+ }
772
+ const previous = recognizedBaselineRef.current
773
+ applyRuleDraft(candidate)
774
+ recognizedBaselineRef.current = candidate
775
+ recognizedDescriptionRef.current = ruleDescription.trim()
776
+ setMessage(previous
777
+ ? '已根据新描述更新草稿,请检查摘要和授权范围。'
778
+ : '已识别为任务草稿。检查摘要和授权范围;此操作不会开放任务。')
779
+ } catch (error) { setMessage(error.message) }
780
+ finally { setRecognitionBusy(false) }
667
781
  }
668
782
  const saveGiteaToken = async () => {
669
783
  if (!giteaToken.trim()) return
@@ -717,29 +831,12 @@ window.__ModuleLoader__.load({
717
831
  } catch (error) { setMessage(error.message) }
718
832
  finally { setBusy(false) }
719
833
  }
720
- const issueBindingCode = async () => {
721
- if (!selectedBot || !selectedEnrollmentBound) return
722
- setBusy(true)
723
- try {
724
- const response = await fetch('/api/ant-teams/delegation/owner-challenge', {
725
- method: 'POST', headers: { 'content-type': 'application/json' },
726
- body: JSON.stringify({ bot_id: selectedBot.bot_id, bot_fingerprint: selectedBot.fingerprint }),
727
- })
728
- const result = await readLocalJsonResponse(response)
729
- if (!response.ok) throw new Error(result.message || '无法获取身份绑定码。')
730
- setBindingCode(result.code)
731
- setMessage('')
732
- } catch (error) { setMessage(error.message) }
733
- finally { setBusy(false) }
734
- }
735
- const saveGroupChat = async (enabled) => {
736
- if (!selectedBot || !selectedEnrollmentBound || !chatGroupId.trim()) return
737
- if (!enabled && selectedGroupHasTasks) {
738
- setMessage('这个群还有开放能力。请先停止这些能力,再关闭群聊。')
739
- return
740
- }
834
+ const saveGroupChat = async (enabled, targetGroupId) => {
835
+ if (!selectedBot || !selectedEnrollmentBound || !targetGroupId.trim()) return
836
+ const targetHasTasks = activeCards.some((card) => card.bot_fingerprint === selectedBot.fingerprint
837
+ && card.conversation_kind === 'group' && card.conversation_id === targetGroupId)
741
838
  if (!window.confirm(enabled
742
- ? selectedGroupHasTasks
839
+ ? targetHasTasks
743
840
  ? '这个群当前因任务授权可以聊天。要在任务到期后仍允许普通聊天吗?聊天会使用团队模型额度。'
744
841
  : '允许这个群与机器人自然聊天?群成员仍不能执行未开放的能力,聊天会使用团队模型额度。'
745
842
  : '关闭这个群的普通聊天?已有能力不会自动撤销。')) return
@@ -748,7 +845,7 @@ window.__ModuleLoader__.load({
748
845
  const response = await fetch('/api/ant-teams/delegation/groups', {
749
846
  method: 'PUT', headers: { 'content-type': 'application/json' },
750
847
  body: JSON.stringify({ bot_id: selectedBot.bot_id,
751
- bot_fingerprint: selectedBot.fingerprint, group_id: chatGroupId.trim(), enabled }),
848
+ bot_fingerprint: selectedBot.fingerprint, group_id: targetGroupId.trim(), enabled }),
752
849
  })
753
850
  if (!response.ok) {
754
851
  const result = await readLocalJsonResponse(response)
@@ -762,8 +859,8 @@ window.__ModuleLoader__.load({
762
859
  const saveDmChat = async (enabled) => {
763
860
  if (!selectedBot || !selectedEnrollmentBound || !dmChatSettingAvailable) return
764
861
  if (!window.confirm(enabled
765
- ? '开启后,此机器人会在私聊中正常对话;私聊任务仍逐项按能力授权核验。'
766
- : '关闭后,新私聊消息不会进入 DSH,包括普通聊天、任务请求和状态查询;所有者身份绑定命令仍可用。关闭前已提交的任务不会被撤销,仍按原私聊回复。已开放的私聊能力不会删除;重新开启后,未过期的能力会恢复触发。群聊设置不受影响。')) return
862
+ ? '开启后,此机器人会在私聊中正常对话;私聊任务仍逐项按已开放规则核验。'
863
+ : '关闭后,新私聊消息不会进入 DSH,包括普通聊天、任务请求和状态查询。关闭前已提交的任务不会被撤销,仍按原私聊回复。已开放的私聊能力不会删除;重新开启后,未过期的能力会恢复触发。群聊设置不受影响。')) return
767
864
  setBusy(true)
768
865
  try {
769
866
  const response = await fetch('/api/ant-teams/delegation/dm-chat', {
@@ -808,19 +905,33 @@ window.__ModuleLoader__.load({
808
905
  })
809
906
  }
810
907
  const goToSetupStep = (index) => {
811
- if (index === 0) return focusAfterTabChange('bot-chat', selectedBot ? 'ant-bot-selector' : 'ant-chat-entry')
812
- if (index === 1) return focusAfterTabChange('bot-chat', 'ant-chat-scope-panel')
813
- if (authorizationLoadError || selectedTaskCount > 0) return focusAfterTabChange('capability', 'ant-capability-list')
908
+ if (index === 0) return focusAfterTabChange('collaboration', selectedBot ? 'ant-bot-selector' : 'ant-chat-entry')
909
+ if (index === 1) return focusAfterTabChange('collaboration',
910
+ selectedEnrollmentBound && botConnected ? 'ant-group-task-status' : 'ant-chat-scope-panel')
911
+ if (index === 2 && (authorizationLoadError || selectedTaskCount > 0)) {
912
+ return focusAfterTabChange('collaboration', 'ant-capability-list')
913
+ }
914
+ if (index === 2) {
915
+ startNewCard()
916
+ return focusAfterTabChange('collaboration', 'ant-new-task-summary')
917
+ }
814
918
  setCreateOpen(true)
815
- focusAfterTabChange('capability', index === 2 ? 'ant-new-task-summary' : 'ant-authorization-review')
919
+ focusAfterTabChange('collaboration', 'ant-authorization-review')
816
920
  }
921
+ const collaborationGridStyle = {
922
+ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 300px), 1fr))', gap: 12,
923
+ }
924
+ const collaborationSettingsGridStyle = {
925
+ ...collaborationGridStyle, gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 420px), 1fr))',
926
+ }
927
+ const collaborationCardStyle = { ...settingsPanelStyle, padding: 12, gap: 10 }
817
928
  const setupStepper = h('section', { 'aria-label': '首次设置引导', style: { display: 'grid', gap: 6 } },
818
929
  h('p', { style: mutedTextStyle }, '首次设置可按顺序完成;你也可以直接切换页面。普通聊天不需要先开放任务。'),
819
930
  h('ol', { 'aria-label': '设置步骤', style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 150px), 1fr))',
820
931
  gap: 8, padding: 0, margin: 0, listStyle: 'none' } },
821
932
  ...[
822
933
  { label: '机器人和设备', detail: !selectedBot ? '选择已连接的机器人' : !botConnected ? '检查 DSH-IM 连接' : selectedEnrollmentBound ? '已绑定到这台 DSH' : '绑定到这台 DSH' },
823
- { label: '聊天入口', detail: !selectedBot || !selectedEnrollmentBound ? '绑定设备后设置私聊和群聊' : groupChatLoadFailed ? '群列表暂不可用,可手动输入群 ID' : `${dmChatEnabled ? '私聊可用' : '私聊已关闭'} · ${availableGroups.length} 个已发现群` },
934
+ { label: '聊天入口', detail: !selectedBot || !selectedEnrollmentBound ? '绑定设备后设置私聊和群聊' : groupChatLoadFailed ? '群列表暂不可用,请刷新状态' : `${dmChatEnabled ? '私聊可用' : '私聊已关闭'} · ${availableGroups.length} 个已发现群` },
824
935
  { label: '开放能力', detail: !botConnected || !selectedEnrollmentBound ? '完成机器人绑定后再开放' : authorizationLoadError ? '授权列表暂不可读,请刷新状态' : selectedTaskCount ? `${selectedTaskCount} 项已开放,可查看或停止` : '按需要创建任务' },
825
936
  { label: '复核授权', detail: authorizationLoadError ? '先确认已保存授权' : createOpen ? '检查请求范围后确认' : selectedTaskCount ? '查看已开放范围' : '请求人、执行方和期限' },
826
937
  ].map((step, index) => {
@@ -838,32 +949,15 @@ window.__ModuleLoader__.load({
838
949
  h('span', { style: { fontSize: 12, color: mutedTextStyle.color } }, state)),
839
950
  h('span', { style: mutedTextStyle }, step.detail)))
840
951
  })))
841
- const chatScopePanel = h('div', { id: 'ant-chat-scope-panel', tabIndex: -1, style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, padding: 12,
842
- display: 'grid', gap: 8 } },
843
- h('strong', null, '机器人和聊天范围'),
844
- h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 240px), 1fr))', gap: 8 } },
845
- h('div', { style: { display: 'grid', gap: 5, padding: 10, borderRadius: 8,
846
- border: '1px solid var(--dsw-alias-border-l2)', backgroundColor: 'var(--dsw-alias-bg-layer-1)' } },
847
- h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8 } },
848
- h('label', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
849
- h('input', { type: 'checkbox', checked: dmChatEnabled,
850
- disabled: busy || !selectedBot || !selectedEnrollmentBound
851
- || enrollmentLoadError || !dmChatSettingAvailable,
852
- onChange: (event) => void saveDmChat(event.target.checked),
853
- 'aria-label': '允许机器人私聊' }), h('strong', null, '私聊')),
854
- h('span', { style: { ...mutedTextStyle, whiteSpace: 'nowrap' } }, dmChatStatus)),
855
- h('p', { style: mutedTextStyle }, dmChatDescription)),
856
- h('div', { style: { display: 'grid', gap: 5, padding: 10, borderRadius: 8,
952
+ const chatScopePanel = h('section', { id: 'ant-chat-scope-panel', tabIndex: -1, style: collaborationCardStyle },
953
+ h('strong', null, '私聊入口'),
954
+ h('label', { style: { display: 'flex', gap: 8, alignItems: 'center', padding: 10, borderRadius: 8,
857
955
  border: '1px solid var(--dsw-alias-border-l2)', backgroundColor: 'var(--dsw-alias-bg-layer-1)' } },
858
- h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8 } },
859
- h('label', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
860
- h('input', { type: 'checkbox', checked: Boolean(selectedChatGroup?.chat_enabled),
861
- disabled: busy || groupChatLoadFailed || !selectedEnrollmentBound || !botConnected || !chatGroupId.trim()
862
- || (Boolean(selectedChatGroup?.chat_enabled) && selectedGroupHasTasks),
863
- onChange: (event) => void saveGroupChat(event.target.checked),
864
- 'aria-label': '允许所选群进行普通聊天' }), h('strong', null, '群聊')),
865
- h('span', { style: { ...mutedTextStyle, whiteSpace: 'nowrap' } }, `${availableGroups.length} 个已发现`)),
866
- h('p', { style: mutedTextStyle }, '选择一个群后可单独启用;不影响私聊设置,群任务仍需单独授权。'))))
956
+ h('input', { type: 'checkbox', checked: dmChatEnabled,
957
+ disabled: busy || !selectedBot || !selectedEnrollmentBound || enrollmentLoadError || !dmChatSettingAvailable,
958
+ onChange: (event) => void saveDmChat(event.target.checked), 'aria-label': '允许机器人私聊' }),
959
+ h('strong', null, '允许私聊普通聊天'), h('span', { style: mutedTextStyle }, dmChatStatus)),
960
+ h('p', { style: mutedTextStyle }, dmChatDescription))
867
961
  const createCard = async (event) => {
868
962
  event.preventDefault()
869
963
  setDraftNotice('')
@@ -871,37 +965,40 @@ window.__ModuleLoader__.load({
871
965
  if (editingCardId && (!editingExpiresAt || editingExpiresAt <= Math.floor(Date.now() / 1000))) {
872
966
  setMessage('这项能力已到期,无法继续编辑;请返回后重新开放一项能力。'); return
873
967
  }
874
- if (!title.trim() || !intent.trim() || !taskPrompt.trim()) {
875
- setMessage('请填写能力名称、适用请求说明和处理要求。'); return
968
+ if (!ruleDescription.trim() || !title.trim() || !intent.trim() || !taskPrompt.trim()) {
969
+ setMessage('请填写任务描述并点击“识别规则”,检查摘要后再开放。'); return
876
970
  }
877
971
  const bytes = (value) => new TextEncoder().encode(value).length
878
- if (bytes(title.trim()) > 100 || bytes(intent.trim()) > 1000 || bytes(taskPrompt.trim()) > 4000) {
879
- setMessage('能力名称、请求说明或处理要求过长,请缩短后再提交。'); return
972
+ if (bytes(ruleDescription.trim()) > 4000 || bytes(title.trim()) > 100
973
+ || bytes(intent.trim()) > 1000 || bytes(taskPrompt.trim()) > 4000) {
974
+ setMessage('任务描述或识别摘要过长,请缩短描述并重新识别。'); return
880
975
  }
881
- const exampleLines = examples.split('\n').map((item) => item.trim()).filter(Boolean)
882
- if (exampleLines.length > 8 || exampleLines.some((item) => bytes(item) > 500)) {
883
- setMessage('同事示例最多写 8 句,每句请保持简短。'); return
976
+ const triggerLines = triggerPhrases.split('\n').map((item) => item.normalize('NFKC').trim()).filter(Boolean)
977
+ if (!triggerLines.length || triggerLines.length > 8 || triggerLines.some((item) => bytes(item) > 120)
978
+ || new Set(triggerLines.map((item) => item.toLowerCase())).size !== triggerLines.length) {
979
+ setMessage('没能从描述中识别出清楚的触发方式。请写明消息以什么开头时执行,再重新识别。'); return
884
980
  }
885
981
  const normalized = parameters.map((rule) => ({ name: rule.name.trim(), kind: rule.kind,
886
982
  description: rule.description.trim() || null,
887
- required: rule.required, min: rule.min === '' ? null : Number(rule.min),
888
- max: rule.max === '' ? null : Number(rule.max),
889
- choices: rule.kind === 'choice' ? rule.choices.split(/[,,\n]/u).map((item) => item.trim()).filter(Boolean) : null,
983
+ required: rule.required, min: rule.min == null || rule.min === '' ? null : Number(rule.min),
984
+ max: rule.max == null || rule.max === '' ? null : Number(rule.max),
985
+ choices: rule.kind === 'choice' ? (Array.isArray(rule.choices) ? rule.choices
986
+ : String(rule.choices ?? '').split(/[,,\n]/u)).map((item) => item.trim()).filter(Boolean) : null,
890
987
  gitea_origin: rule.kind === 'gitea_pr' ? selectedEnrollment?.gitea_origin || null : null,
891
988
  repository: rule.kind === 'gitea_pr' ? rule.repository.trim().toLowerCase() : null }))
892
989
  if (normalized.some((rule) => !/^[A-Za-z][A-Za-z0-9_]{0,31}$/u.test(rule.name))) {
893
- setMessage('参数键请用英文字母开头,只能包含英文字母、数字和下划线;给同事看的中文问题可填在“参数说明”。'); return
990
+ setMessage('没能从描述中整理出有效的请求信息。请说明同事需要提供什么,再重新识别。'); return
894
991
  }
895
992
  if (new Set(normalized.map((rule) => rule.name)).size !== normalized.length) {
896
- setMessage('参数键不能重复,请为每项参数填写不同的英文键。'); return
993
+ setMessage('请求信息识别有重复项。请修改任务描述并重新识别。'); return
897
994
  }
898
995
  if (normalized.some((rule) => rule.description && bytes(rule.description) > 100)) {
899
- setMessage('参数说明过长,请缩短给同事看的问题。'); return
996
+ setMessage('请求信息说明过长。请缩短任务描述并重新识别。'); return
900
997
  }
901
998
  if (normalized.some((rule) => rule.kind === 'choice' && (!rule.choices?.length || rule.choices.length > 32
902
999
  || rule.choices.some((choice) => bytes(choice) > 100)
903
1000
  || new Set(rule.choices).size !== rule.choices.length))) {
904
- setMessage('选项参数请填写 1–32 个不重复的可选值,每项保持简短。'); return
1001
+ setMessage('描述中的可选范围不清楚或重复。请修改描述并重新识别。'); return
905
1002
  }
906
1003
  if (normalized.some((rule) => (rule.min != null && !Number.isInteger(rule.min))
907
1004
  || (rule.max != null && !Number.isInteger(rule.max))
@@ -909,10 +1006,10 @@ window.__ModuleLoader__.load({
909
1006
  || (rule.kind === 'text' && rule.max != null && (rule.max < 1 || rule.max > 4096))
910
1007
  || (rule.kind === 'gitea_pr' && ((rule.min != null && rule.min < 1)
911
1008
  || (rule.max != null && rule.max > 1_000_000_000))))) {
912
- setMessage('参数范围无效:最小值不能大于最大值;文本最长 4096 字节,PR 编号须为正整数。'); return
1009
+ setMessage('描述中的请求范围无效。请明确范围或提供完整 PR 链接,再重新识别。'); return
913
1010
  }
914
1011
  if (normalized.some((rule) => rule.kind === 'gitea_pr' && (!rule.gitea_origin || !rule.repository))) {
915
- setMessage('PR 参数还需要填写可信 Gitea 地址和仓库。'); return
1012
+ setMessage('没能从描述中识别 PR 来源或仓库。请补充完整 PR 链接或仓库名,再重新识别。'); return
916
1013
  }
917
1014
  const validRepository = (value) => {
918
1015
  const parts = value.split('/')
@@ -920,14 +1017,14 @@ window.__ModuleLoader__.load({
920
1017
  && /^[A-Za-z0-9_.-]{1,100}$/u.test(part))
921
1018
  }
922
1019
  if (normalized.some((rule) => rule.kind === 'gitea_pr' && !validRepository(rule.repository))) {
923
- setMessage('Gitea 仓库请填写“团队名/仓库名”,不要包含空格或多余路径。'); return
1020
+ setMessage('仓库名无法识别。请在描述中写明“团队名/仓库名”或粘贴完整 PR 链接,再重新识别。'); return
924
1021
  }
925
1022
  if (!reviewers.length || !reviewers.includes(runner)) {
926
1023
  setMessage('请至少选择一位审查 Agent,并设置普通请求的默认执行方。'); return
927
1024
  }
928
1025
  if (nativeReviewerSelected && (normalized.length !== 1
929
1026
  || normalized[0].kind !== 'gitea_pr' || !normalized[0].required)) {
930
- setMessage('选择 Codex 或 Claude Code 时,需要一个必填的 Gitea PR 编号参数。'); return
1027
+ setMessage('Codex 和 Claude Code 目前只支持审查一个 PR。请在任务描述中写明 PR 链接,再重新识别。'); return
931
1028
  }
932
1029
  const scope = conversationKind === 'group' ? `群 ${groupId}` : '私聊'
933
1030
  const resultDestination = conversationKind === 'group' ? '原群(群内可见)' : '发起任务的私聊'
@@ -940,8 +1037,8 @@ window.__ModuleLoader__.load({
940
1037
  ? `普通请求默认由 ${reviewerLabel(runner)} 执行;请求人明确说“交叉 review”时,会并行启动 ${reviewers.map(reviewerLabel).join('、')},分别返回审查结果。${reviewers.map((item) => executionDetails[item]).join(' ')}`
941
1038
  : executionDetails[reviewers[0]]
942
1039
  const actionDescription = editingCardId
943
- ? `更新“${title}”后,${requesterSummary}可在${scope}使用新配置;旧待确认请求将失效,已提交任务继续使用提交时的配置。结果${resultDestination}。到期时间保持为 ${new Date(editingExpiresAt * 1000).toLocaleString('zh-CN')}。`
944
- : `开放“${title}”后,${requesterSummary}可在${scope}请求,有效期 ${days} 天;结果${resultDestination}。`
1040
+ ? `更新“${title}”后,${requesterSummary}可在${scope}按任务描述中的规则发起请求。旧待确认请求将失效,已提交任务继续使用提交时的配置。结果${resultDestination}。到期时间保持为 ${new Date(editingExpiresAt * 1000).toLocaleString('zh-CN')}。`
1041
+ : `开放“${title}”后,${requesterSummary}可在${scope}按任务描述中的规则发起请求,有效期 ${days} 天;结果${resultDestination}。`
945
1042
  if (!window.confirm(`${actionDescription}${executionInfo}请确认。`)) return
946
1043
  setBusy(true)
947
1044
  try {
@@ -949,7 +1046,8 @@ window.__ModuleLoader__.load({
949
1046
  method: editingCardId ? 'PUT' : 'POST', headers: { 'content-type': 'application/json' },
950
1047
  body: JSON.stringify({ ...(editingCardId ? { id: editingCardId } : {}),
951
1048
  bot_id: selectedBot.bot_id, bot_fingerprint: selectedBot.fingerprint,
952
- title, intent, examples: exampleLines,
1049
+ title, rule_description: ruleDescription.trim(), trigger_phrases: triggerLines,
1050
+ intent, examples: [],
953
1051
  parameters: normalized, task_prompt: taskPrompt, runner, reviewers, workspace: null,
954
1052
  sender_scope: senderScope, sender_ids: senderScope === 'allowlist'
955
1053
  ? senderIds.split(/[\s,,]+/u).filter(Boolean) : [], conversation_kind: conversationKind,
@@ -957,6 +1055,9 @@ window.__ModuleLoader__.load({
957
1055
  expires_at: editingCardId ? editingExpiresAt : Math.floor(Date.now() / 1000) + Number(days) * 86400 }),
958
1056
  })
959
1057
  const result = await readLocalJsonResponse(response)
1058
+ if (!response.ok && result.code === 'IM_TASK_RULE_REQUIRED') {
1059
+ throw new Error('当前 ANT 插件版本不支持任务触发规则,请升级插件后再开放任务。')
1060
+ }
960
1061
  if (!response.ok && reviewers.includes('claude-code') && result.code === 'INVALID_REQUEST') {
961
1062
  throw new Error('当前团队服务端可能尚不支持 Claude Code 审查配置。请管理员升级服务端后刷新重试。')
962
1063
  }
@@ -976,13 +1077,17 @@ window.__ModuleLoader__.load({
976
1077
  }
977
1078
  throw new Error('当前团队服务端尚不支持多 Agent 审查配置;请管理员升级 ANT 服务端后重试。')
978
1079
  }
979
- setTitle(''); setIntent(''); setExamples(''); setTaskPrompt(''); setParameters([]); setRunner('dsh'); setReviewers(['dsh'])
1080
+ recognizedBaselineRef.current = null
1081
+ recognizedDescriptionRef.current = null
1082
+ setRuleDescription(''); setTriggerPhrases('')
1083
+ setTitle(''); setIntent(''); setTaskPrompt(''); setParameters([]); setRunner('dsh'); setReviewers(['dsh'])
1084
+ setCrossReviewOpen(false)
980
1085
  setEditingCardId(null); setEditingExpiresAt(null)
981
1086
  await load()
982
1087
  setCreateOpen(false)
983
1088
  setMessage(editingCardId
984
1089
  ? '能力配置已更新。新请求将使用新配置,已提交任务保留原配置。'
985
- : '能力已开放。同事可自然地提出这类请求;参数不完整时机器人会追问。')
1090
+ : '任务已开放。机器人会按已确认的描述判断请求;缺少必要信息时会向请求人追问。')
986
1091
  } catch (error) { setMessage(error.message) }
987
1092
  finally { setBusy(false) }
988
1093
  }
@@ -1000,29 +1105,36 @@ window.__ModuleLoader__.load({
1000
1105
  }
1001
1106
  const copyCardToDraft = (card) => {
1002
1107
  setEditingCardId(null); setEditingExpiresAt(null)
1108
+ setCrossReviewOpen(false)
1109
+ const hasTriggerRule = Array.isArray(card.trigger_phrases) && card.trigger_phrases.length > 0
1110
+ const copiedDescription = card.rule_description || card.intent || ''
1111
+ recognizedDescriptionRef.current = hasTriggerRule ? copiedDescription.trim() || null : null
1112
+ setDraftNotice(hasTriggerRule
1113
+ ? '已复制到新草稿。复制不保留原有效期;请检查任务描述和请求范围。'
1114
+ : '复制的旧任务没有明确触发方式。请修改描述并重新识别,再在支持触发规则的服务端开放;副本不保留原有效期。')
1003
1115
  const cardBot = bots.find((bot) => bot.fingerprint === card.bot_fingerprint)
1004
1116
  const cardEnrollment = enrolled.find((bot) => bot.bot_fingerprint === card.bot_fingerprint)
1005
1117
  const titleChars = Array.from(`复制:${card.title || '未命名能力'}`)
1006
1118
  while (new TextEncoder().encode(titleChars.join('')).length > 100) titleChars.pop()
1119
+ const taskDraft = taskRuleDraftFromCard(card, titleChars.join(''))
1120
+ recognizedBaselineRef.current = hasTriggerRule ? taskDraft : null
1007
1121
  setBotId(card.bot_id || cardBot?.bot_id || '')
1008
1122
  setTrustedOrigin(cardEnrollment?.gitea_origin || '')
1009
- setTitle(titleChars.join(''))
1010
- setIntent(card.intent || '')
1011
- setExamples(Array.isArray(card.examples) ? card.examples.join('\n') : '')
1012
- setTaskPrompt(card.task_prompt || '')
1123
+ setRuleDescription(copiedDescription)
1124
+ setTriggerPhrases(taskDraft.trigger_phrases.join('\n'))
1125
+ setTitle(taskDraft.title)
1126
+ setIntent(taskDraft.intent)
1127
+ setTaskPrompt(taskDraft.task_prompt)
1013
1128
  setRunner(['dsh', 'codex', 'claude-code'].includes(card.runner) ? card.runner : 'dsh')
1014
1129
  setReviewers(Array.isArray(card.reviewers) && card.reviewers.length ? card.reviewers : [card.runner || 'dsh'])
1015
- setParameters(Array.isArray(card.parameters) ? card.parameters.map((rule) => ({
1016
- ...rule, min: String(rule.min ?? ''), max: String(rule.max ?? ''), choices: Array.isArray(rule.choices) ? rule.choices.join(',') : String(rule.choices ?? ''),
1017
- })) : [])
1130
+ setParameters(taskDraft.parameters)
1018
1131
  setConversationKind(card.conversation_kind === 'group' ? 'group' : 'dm')
1019
1132
  setGroupId(card.conversation_id || '')
1020
1133
  setSenderScope(card.sender_scope === 'allowlist' ? 'allowlist' : 'any')
1021
1134
  setSenderIds(Array.isArray(card.sender_ids) ? card.sender_ids.join('\n') : '')
1022
1135
  setDays(7)
1023
- setDraftNotice('已复制到新草稿。复制不保留原有效期;开放前请重新核对群、请求人和执行方,默认有效期为 7 天。')
1024
1136
  setCreateOpen(true)
1025
- onSelectTab('capability')
1137
+ onSelectTab('collaboration')
1026
1138
  }
1027
1139
  const cancelTask = async (task) => {
1028
1140
  if (!window.confirm(`取消任务 ${task.task_id}?已经发生的读取不能撤回。`)) return
@@ -1036,120 +1148,112 @@ window.__ModuleLoader__.load({
1036
1148
  } catch (error) { setMessage(error.message) }
1037
1149
  finally { setBusy(false) }
1038
1150
  }
1151
+ const robotCard = h('section', { style: collaborationCardStyle, 'aria-label': '机器人和设备' },
1152
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8 } },
1153
+ h('strong', null, '机器人和设备'),
1154
+ h('button', { type: 'button', disabled: loading || busy,
1155
+ onClick: () => void load().catch((error) => setMessage(error.message)), style: secondaryButtonStyle },
1156
+ loading ? '读取中…' : '刷新状态')),
1157
+ h('p', { role: 'status', style: mutedTextStyle }, collaborationStatus),
1158
+ bots.length ? h('label', null, '我的机器人', h('select', { id: 'ant-bot-selector', value: botId,
1159
+ onChange: (event) => { setBotId(event.target.value); setGroupId('');
1160
+ setTrustedOrigin(enrolled.find((bot) => bot.bot_id === event.target.value)?.gitea_origin || '') }, style: inputStyle },
1161
+ h('option', { value: '' }, '请选择机器人'),
1162
+ ...bots.map((bot) => h('option', { key: bot.bot_id, value: bot.bot_id,
1163
+ disabled: !bot.available || !bot.connected }, `${bot.name}${bot.available && bot.connected ? '' : '(未连接)'}`)))) : null,
1164
+ connectedBots.length === 0 && !loading && !botLoadError ? h('div', { style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' } },
1165
+ h('button', { type: 'button', onClick: openImSettings, style: secondaryButtonStyle }, '打开 DSH-IM 设置'),
1166
+ h('span', { style: mutedTextStyle }, '先在 DSH 的插件列表安装并连接飞书机器人。')) : null,
1167
+ selectedBot && !botConnected && connectedBots.length ? h('button', { type: 'button', onClick: openImSettings,
1168
+ style: secondaryButtonStyle }, '检查 DSH-IM 连接') : null,
1169
+ selectedBot && !selectedEnrollmentBound ? h('button', { type: 'button', disabled: busy || !botConnected || enrollmentLoadError,
1170
+ onClick: () => void enrollBot(), style: primaryButtonStyle },
1171
+ selectedEnrollment ? '绑定原协作配置到这台 DSH' : '启用并绑定到这台 DSH') : null)
1172
+ const trustedOriginCard = showPrSecuritySettings ? h('section', { style: collaborationCardStyle },
1173
+ h('strong', null, 'PR 读取来源(可选)'),
1174
+ h('p', { style: mutedTextStyle }, '只有任务需要读取 Gitea PR 时才需设置。PR 链接、仓库和编号写在任务描述里;这里仅登记本机允许读取的 Gitea 来源。'),
1175
+ h('label', null, '机器人', h('select', { 'aria-label': 'Gitea 来源对应机器人', value: botId,
1176
+ onChange: (event) => { setBotId(event.target.value); setTrustedOrigin(
1177
+ enrolled.find((bot) => bot.bot_id === event.target.value)?.gitea_origin || '') }, style: inputStyle },
1178
+ h('option', { value: '' }, '请选择机器人'),
1179
+ ...bots.map((bot) => h('option', { key: bot.bot_id, value: bot.bot_id,
1180
+ disabled: !bot.available || !bot.connected }, `${bot.name}${bot.available && bot.connected ? '' : '(未连接)'}`)))),
1181
+ selectedBot ? h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } },
1182
+ h('input', { 'aria-label': '可信 Gitea 地址', type: 'url', value: trustedOrigin,
1183
+ onChange: (event) => setTrustedOrigin(event.target.value), placeholder: 'https://git.example.com',
1184
+ style: { ...inputStyle, flex: '1 1 220px' } }),
1185
+ h('button', { type: 'button', disabled: busy || !trustedOrigin.trim() || (!selectedEnrollmentBound && !botConnected),
1186
+ onClick: () => void (selectedEnrollmentBound ? saveTrustedOrigin() : enrollBot()), style: secondaryButtonStyle },
1187
+ selectedEnrollmentBound ? '保存可信来源' : botConnected ? '启用并保存来源' : '先连接机器人'))
1188
+ : null) : null
1189
+ const groupTaskStatusPanel = selectedEnrollmentBound && botConnected ? h('section', {
1190
+ id: 'ant-group-task-status', role: 'region', 'aria-label': '各群聊天与任务状态', tabIndex: -1,
1191
+ style: { ...collaborationCardStyle, gap: 10 } },
1192
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8 } },
1193
+ h('strong', null, '群聊'),
1194
+ h('button', { type: 'button', disabled: busy || loading,
1195
+ onClick: () => void load().catch((error) => setMessage(error.message)), style: secondaryButtonStyle },
1196
+ loading ? '读取中…' : '刷新群列表')),
1197
+ h('p', { style: mutedTextStyle }, groupChatLoadFailed
1198
+ ? '群列表暂不可用,请刷新状态后重试。'
1199
+ : availableGroups.length ? '每个群分别控制普通聊天和任务;关闭普通聊天不会撤销已开放规则。'
1200
+ : '先在目标群 @机器人,再刷新状态发现群。'),
1201
+ availableGroups.length ? h('div', { id: 'ant-group-card-grid', style: collaborationGridStyle },
1202
+ ...availableGroups.map((group) => {
1203
+ const taskCount = activeCards.filter((card) => card.bot_fingerprint === selectedBot.fingerprint
1204
+ && card.conversation_kind === 'group' && card.conversation_id === group.group_id).length
1205
+ return h('article', { key: group.group_id, style: { ...collaborationCardStyle, padding: 10, gap: 8 } },
1206
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8 } },
1207
+ h('strong', null, group.group_code),
1208
+ h('span', { style: mutedTextStyle }, `已开放任务 ${taskCount} 项`)),
1209
+ h('label', { style: { display: 'flex', alignItems: 'center', gap: 8 } },
1210
+ h('input', { type: 'checkbox', checked: group.chat_enabled,
1211
+ disabled: busy || groupChatLoadFailed,
1212
+ onChange: (event) => void saveGroupChat(event.target.checked, group.group_id),
1213
+ 'aria-label': '允许群 ' + group.group_code + ' 普通聊天' }), '普通聊天'),
1214
+ h('button', { type: 'button', disabled: busy, style: secondaryButtonStyle,
1215
+ onClick: () => { startNewCard({ conversationKind: 'group', groupId: group.group_id }); focusAfterTabChange('collaboration', 'ant-new-task-summary') } },
1216
+ '为此群开放任务'))
1217
+ })) : h('div', { style: infoNoticeStyle },
1218
+ h('strong', null, '还没有发现群'),
1219
+ h('span', { style: mutedTextStyle }, '在目标群 @机器人后刷新列表。'))) : null
1039
1220
  return h('section', { style: { ...settingsPanelStyle,
1040
- display: ['bot-chat', 'capability', 'local'].includes(activeTab) ? 'grid' : 'none' },
1041
- 'aria-label': '协作机器人与开放能力' },
1221
+ display: ['collaboration', 'local'].includes(activeTab) ? 'grid' : 'none' },
1222
+ 'aria-label': '群聊协作设置' },
1042
1223
  message ? h('p', { role: 'status', style: mutedTextStyle }, message) : null,
1043
1224
  h('div', { id: 'ant-chat-entry', role: 'region', 'aria-label': '聊天入口和机器人状态', tabIndex: -1,
1044
- style: { display: activeTab === 'bot-chat' ? 'grid' : 'none', gap: 14 } },
1045
- h('h3', { style: sectionTitleStyle }, '聊天入口'),
1046
- h('p', { style: mutedTextStyle }, '在所有者开启的私聊或群聊中,同事可以像平常一样和机器人聊天。DSH 负责理解与回答;只有准备执行你开放的能力时,ANT 才核对请求人、群、参数和权限。群里的任务结果会回复原群。'),
1047
- h('p', { role: 'status', style: mutedTextStyle }, '私聊与群聊可同时开启;每项能力一次只授权给一个私聊或一个群。普通对话不会自动获得读取电脑、调用工具或访问其他群的权限。'),
1225
+ style: { display: activeTab === 'collaboration' ? 'grid' : 'none', gap: 12 } },
1226
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'start', gap: 12, flexWrap: 'wrap' } },
1227
+ h('div', { style: { display: 'grid', gap: 4 } },
1228
+ h('h3', { style: sectionTitleStyle }, '协作总览'),
1229
+ h('p', { style: mutedTextStyle }, '选择机器人,设置聊天入口,再按需要开放任务规则。聊天和任务授权分别控制。')),
1230
+ h('span', { style: { ...mutedTextStyle, padding: '4px 9px', borderRadius: 999,
1231
+ backgroundColor: 'var(--dsw-alias-bg-layer-2)' } }, selectedBot ? selectedBot.name : '尚未选择机器人')),
1232
+ legacyTriggerCardCount ? h('p', { role: 'status', style: { ...mutedTextStyle,
1233
+ color: 'var(--dsw-alias-state-warning-primary, var(--dsw-alias-label-secondary))' } },
1234
+ '当前有 ' + legacyTriggerCardCount + ' 项旧任务规则没有明确触发方式;新版插件不会按这些旧规则触发任务。升级 ANT 服务端后,再重新描述并开放需要的规则。') : null,
1048
1235
  loadFailures.length ? h('p', { role: 'status', style: mutedTextStyle },
1049
1236
  `部分协作信息暂时无法读取:${loadFailures.map((path) => loadNames[path]).join('、')}。已读取的信息仍显示,请稍后刷新状态。`) : null,
1050
- h('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'flex-start', justifyContent: 'space-between', gap: 8 } },
1051
- h('span', { role: 'status', style: { ...mutedTextStyle, flex: '1 1 260px', minWidth: 0 } }, loading ? '正在读取机器人与任务状态…'
1052
- : botLoadError ? '暂时无法读取机器人状态。请刷新后再试。'
1053
- : connectedBots.length === 0 ? '尚未发现已连接的机器人。请先在 DSH-IM 中连接飞书机器人。'
1054
- : selectedBot && !botConnected ? `“${selectedBot.name}”当前离线;请先在 DSH-IM 恢复连接。`
1055
- : enrollmentLoadError ? selectedBot
1056
- ? `“${selectedBot.name}”已连接;暂时无法确认协作是否已启用,请刷新状态。`
1057
- : '机器人已连接;协作启用状态暂未读取,请刷新状态。'
1058
- : selectedBot && !selectedEnrollmentBound ? selectedEnrollment
1059
- ? `“${selectedBot.name}”已连接;原协作配置尚未绑定到这台 DSH,请在本机重新启用。若原设备令牌曾被撤销,旧任务权限已关闭,绑定后需重新开放。`
1060
- : `“${selectedBot.name}”已连接;请先启用本机协作。`
1061
- : enrolledCount === 0 ? '机器人已连接;请选择并启用协作。'
1062
- : !selectedBot ? '已有协作机器人;请选择一台,查看已开放的能力和聊天范围。'
1063
- : authorizationLoadError ? `“${selectedBot.name}”已启用协作;已开放任务暂未读取,请刷新状态。`
1064
- : !dmChatEnabled ? `“${selectedBot.name}”已启用协作;私聊已关闭,群聊仍按各自设置工作。`
1065
- : selectedTaskCount === 0 ? `“${selectedBot.name}”已启用协作;下一步开放一项能力。你也可以先私聊自测普通聊天。`
1066
- : `“${selectedBot.name}”已启用协作 · ${selectedTaskCount} 项能力已开放。可以私聊自测。`),
1067
- h('button', { type: 'button', disabled: loading || busy,
1068
- onClick: () => void load().catch((error) => setMessage(error.message)),
1069
- style: { ...secondaryButtonStyle, whiteSpace: 'nowrap' } },
1070
- loading ? '正在刷新…' : '刷新状态')),
1071
- bots.length ? h('label', null, '我的机器人', h('select', { id: 'ant-bot-selector', value: botId,
1072
- onChange: (event) => { setBotId(event.target.value); setGroupId(''); setChatGroupId(''); setBindingCode('');
1073
- setTrustedOrigin(enrolled.find((bot) => bot.bot_id === event.target.value)?.gitea_origin || '') }, style: inputStyle },
1074
- h('option', { value: '' }, '请选择机器人'),
1075
- ...bots.map((bot) => h('option', { key: bot.bot_id, value: bot.bot_id,
1076
- disabled: !bot.available || !bot.connected }, `${bot.name}${bot.available && bot.connected ? '' : '(未连接)'}`)))) : null,
1077
- connectedBots.length === 0 && !loading && !botLoadError ? h('button', { type: 'button',
1078
- onClick: openImSettings, style: secondaryButtonStyle }, '打开 DSH-IM 设置') : null,
1079
- connectedBots.length === 0 && !loading && !botLoadError ? h('span', { style: mutedTextStyle },
1080
- '若尚未安装 DSH-IM,请先到 DSH 的插件列表安装并连接机器人。') : null,
1081
- selectedBot && !botConnected && connectedBots.length ? h('button', { type: 'button', onClick: openImSettings,
1082
- style: secondaryButtonStyle }, '检查 DSH-IM 连接') : null,
1083
- selectedBot && !selectedEnrollmentBound ? h('button', { type: 'button', disabled: busy || !botConnected || enrollmentLoadError,
1084
- onClick: () => void enrollBot(), style: secondaryButtonStyle },
1085
- selectedEnrollment ? '绑定原协作配置到这台 DSH' : '启用并绑定到这台 DSH') : null,
1086
- selectedBot ? h('div', { style: { display: 'grid', gap: 8, padding: 10,
1087
- border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8 } },
1088
- h('strong', null, `可信 Gitea 地址 · ${selectedBot.name}`),
1089
- h('p', { style: mutedTextStyle }, '为这个机器人登记一次。PR 能力只需配置仓库和 PR 编号,不必重复填写地址。'),
1090
- h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } },
1091
- h('input', { 'aria-label': '可信 Gitea 地址', type: 'url', value: trustedOrigin,
1092
- onChange: (event) => setTrustedOrigin(event.target.value), placeholder: 'https://git.example.com',
1093
- style: { ...inputStyle, flex: '1 1 280px' } }),
1094
- h('button', { type: 'button', disabled: busy || !trustedOrigin.trim() || (!selectedEnrollmentBound && !botConnected),
1095
- onClick: () => void (selectedEnrollmentBound ? saveTrustedOrigin() : enrollBot()), style: secondaryButtonStyle },
1096
- selectedEnrollmentBound ? '保存可信地址' : botConnected ? '绑定机器人并保存地址' : '先连接机器人'))) : null,
1097
- selectedBot && selectedEnrollmentBound ? h('div', { role: 'region', 'aria-label': '群任务所有者身份验证',
1098
- style: { display: 'grid', gap: 8, padding: 10, border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8,
1099
- backgroundColor: 'var(--dsw-alias-bg-layer-1)' } },
1100
- h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8 } },
1101
- h('strong', null, '群任务所有者身份'),
1102
- h('span', { role: 'status', style: { ...mutedTextStyle, color: selectedEnrollment.owner_bound
1103
- ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-warning-primary, var(--dsw-alias-label-secondary))' } },
1104
- selectedEnrollment.owner_bound ? '已绑定' : '尚未绑定')),
1105
- h('p', { style: mutedTextStyle }, '绑定只针对当前机器人,所有群共用。只有允许群成员请求已开放能力时才需要;普通群聊无需绑定。绑定后,群任务仍需你在目标群按机器人提示确认。'),
1106
- h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' } },
1107
- h('button', { type: 'button', disabled: busy || !botConnected || enrollmentLoadError,
1108
- onClick: () => void issueBindingCode(), style: secondaryButtonStyle },
1109
- selectedEnrollment.owner_bound ? '重新获取绑定码' : '获取绑定码'),
1110
- bindingCode ? h('span', { role: 'status', style: mutedTextStyle },
1111
- `请你本人私聊“${selectedBot.name}”发送“绑定身份 ${bindingCode}”。绑定码 5 分钟有效。`) : null)) : null,
1237
+ h('div', { id: 'ant-collaboration-settings-grid', style: collaborationSettingsGridStyle },
1238
+ robotCard,
1239
+ chatScopePanel,
1240
+ ),
1112
1241
  setupStepper,
1113
- chatScopePanel,
1242
+ groupTaskStatusPanel,
1114
1243
  selectedEnrollmentBound && botConnected ? h('p', { style: mutedTextStyle },
1115
1244
  dmChatEnabled
1116
- ? `自测:私聊“${selectedBot.name}”发送“你能做什么”;发起任务后可问“刚才的任务怎么样了”。群聊请先在目标群 @机器人,再开放能力时选择该群。`
1117
- : '私聊当前已关闭;如要恢复,请在上方开启。群聊请先在目标群 @机器人,再开放能力时选择该群。') : null,
1118
- selectedEnrollmentBound && botConnected ? h('div', { role: 'region', 'aria-label': '群聊白名单',
1119
- style: { display: 'grid', gap: 8 } },
1120
- h('strong', null, '群聊白名单'),
1121
- h('span', { role: 'status', style: mutedTextStyle }, groupChatLoadFailed
1122
- ? '群聊列表与当前权限暂时无法读取,请刷新后再更改;仍可填写群 ID。'
1123
- : availableGroups.length
1124
- ? '从机器人已发现的群中选择;仍可手动填写群 ID。允许普通聊天不等于允许群成员执行任务。'
1125
- : '先在目标群 @机器人并刷新列表;若平台暂未返回群列表,也可以手动填写群 ID。'),
1126
- h('div', { style: { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) auto', gap: 8, alignItems: 'end' } },
1127
- h('label', { style: { display: 'grid', gap: 5 } },
1128
- h('span', { style: mutedTextStyle }, '选择已记录的群'),
1129
- h('select', { 'aria-label': '已记录的群', value: availableGroups.some((group) => group.group_id === chatGroupId) ? chatGroupId : '',
1130
- onChange: (event) => setChatGroupId(event.target.value), style: inputStyle },
1131
- h('option', { value: '' }, '选择已发现的群聊'),
1132
- ...availableGroups.map((group) => h('option', { key: group.group_id, value: group.group_id },
1133
- `${group.group_code} · ${group.chat_enabled ? '已允许普通聊天' : '未允许普通聊天'}`)))),
1134
- h('button', { type: 'button', disabled: busy || !selectedEnrollmentBound,
1135
- onClick: () => void load().catch((error) => setMessage(error.message)), style: secondaryButtonStyle }, '刷新群列表')),
1136
- h('label', { style: { display: 'grid', gap: 5 } },
1137
- h('span', { style: mutedTextStyle }, '群 ID(平台未返回群名时可手动填写)'),
1138
- h('input', { 'aria-label': '群 ID', value: chatGroupId, maxLength: 256,
1139
- onChange: (event) => setChatGroupId(event.target.value.trim()),
1140
- placeholder: '输入群 ID', style: inputStyle })),
1141
- selectedChatGroup?.chat_enabled && selectedGroupHasTasks ? h('span', { style: mutedTextStyle },
1142
- '本群还有开放能力;关闭普通聊天前请先停止这些能力。') : null) : null),
1143
- h('div', { id: 'ant-capability-list', role: 'region', 'aria-label': '已开放能力列表', tabIndex: -1,
1144
- style: { display: activeTab === 'capability' ? 'grid' : 'none', gap: 12 } },
1245
+ ? `自测:私聊“${selectedBot.name}”发送“你能做什么”;发起任务后可问“刚才的任务怎么样了”。`
1246
+ : '私聊当前已关闭;如要恢复,请在上方开启。') : null,
1247
+ ),
1248
+ h('div', { id: 'ant-capability-list', role: 'region', 'aria-label': '已开放任务规则', tabIndex: -1,
1249
+ style: { display: activeTab === 'collaboration' ? 'grid' : 'none', gap: 12 } },
1145
1250
  h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 10, flexWrap: 'wrap' } },
1146
- h('h4', { style: sectionTitleStyle }, `我开放的能力 · ${authorizationLoadError ? '暂未读取' : activeCards.length}`),
1147
- h('div', { role: 'toolbar', 'aria-label': '能力授权操作', style: { display: 'flex', gap: 8, flexWrap: 'wrap' } },
1148
- h('button', { type: 'button', onClick: () => { setCreateOpen(true); onSelectTab('capability') },
1149
- style: primaryButtonStyle }, '开放新能力'),
1150
- h('button', { type: 'button', onClick: () => { preparePrExample(); onSelectTab('capability') },
1151
- style: secondaryButtonStyle }, '使用 PR 差异审查示例'))),
1251
+ h('h4', { style: sectionTitleStyle }, '我的任务规则 · ' + (authorizationLoadError ? '暂未读取' : activeCards.length)),
1252
+ h('div', { role: 'toolbar', 'aria-label': '任务规则操作', style: { display: 'flex', gap: 8, flexWrap: 'wrap' } },
1253
+ h('button', { type: 'button', onClick: () => { startNewCard(); focusAfterTabChange('collaboration', 'ant-new-task-summary') },
1254
+ style: primaryButtonStyle }, '新建任务规则'))),
1152
1255
  h('p', { style: mutedTextStyle }, '当前一项能力只能绑定一个范围:私聊,或某一个群。若要给其他群或私聊开放相同任务,目前需分别建立能力卡;它们的请求人和有效期独立。'),
1256
+ h('div', { id: 'ant-capability-cards', style: collaborationGridStyle },
1153
1257
  ...activeCards.map((card) => {
1154
1258
  const expiresAt = Number(card.expires_at) * 1000
1155
1259
  const remainingDays = Math.max(1, Math.ceil((expiresAt - Date.now()) / 86_400_000))
@@ -1158,46 +1262,46 @@ window.__ModuleLoader__.load({
1158
1262
  ? `群 ${groups.find((group) => group.group_id === card.conversation_id)?.group_code || card.conversation_id}` : '私聊'
1159
1263
  const requester = card.sender_scope === 'allowlist' ? '指定成员可请求'
1160
1264
  : card.conversation_kind === 'group' ? '群成员可请求' : '可私聊者可请求'
1161
- const parametersText = (card.parameters || []).map((rule) => {
1162
- const constraint = rule.kind === 'gitea_pr' && rule.repository ? `仓库 ${rule.repository}`
1163
- : rule.kind === 'choice' ? `选项 ${(rule.choices || []).join('、')}`
1164
- : ['integer', 'gitea_pr'].includes(rule.kind)
1165
- ? `范围 ${rule.min ?? 1}–${rule.max ?? '无上限'}` : ''
1166
- return `${rule.name}:${rule.description || rule.kind}${rule.required ? '(必填)' : '(可选)'}${constraint ? ` · ${constraint}` : ''}`
1167
- }).join(';')
1168
1265
  const examplesText = Array.isArray(card.examples) ? card.examples.join('、') : ''
1169
1266
  const requesterIds = card.sender_scope === 'allowlist' && Array.isArray(card.sender_ids) ? card.sender_ids.join('、') : ''
1170
1267
  const remainingDaysLabel = remainingDays <= 7 ? 'var(--dsw-alias-state-warning-primary, var(--dsw-alias-label-secondary))'
1171
1268
  : 'var(--dsw-alias-label-secondary)'
1172
- return h('div', { key: card.id, style: { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) auto',
1173
- alignItems: 'start', gap: 12, borderTop: '1px solid var(--dsw-alias-border-l2)', padding: '10px 0' } },
1174
- h('div', { style: { display: 'grid', gap: 4, minWidth: 0 } },
1269
+ return h('article', { key: card.id, style: { ...collaborationCardStyle, padding: 12, gap: 8 } },
1270
+ h('div', { style: { display: 'grid', gap: 6, minWidth: 0 } },
1175
1271
  h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' } },
1176
1272
  h('strong', { style: { overflowWrap: 'anywhere' } }, card.title || '未命名能力'),
1177
1273
  h('span', { style: { ...mutedTextStyle, padding: '2px 8px', borderRadius: 999,
1178
1274
  backgroundColor: 'var(--dsw-alias-bg-layer-2)', color: remainingDaysLabel, whiteSpace: 'nowrap' } }, expiry)),
1179
1275
  h('span', { style: mutedTextStyle }, `${cardReviewerSummary(card)} · ${conversation} · ${requester} · 结果回复原${card.conversation_kind === 'group' ? '群' : '私聊'}`),
1276
+ card.rule_description
1277
+ ? h('span', { style: { ...mutedTextStyle, overflowWrap: 'anywhere' } }, card.rule_description)
1278
+ : h('span', { role: 'status', style: { ...mutedTextStyle, color: 'var(--dsw-alias-state-warning-primary, var(--dsw-alias-label-secondary))' } },
1279
+ '旧规则没有明确触发方式;新版插件不会按它启动任务。升级 ANT 服务端后需重新描述并开放。'),
1180
1280
  h('details', null,
1181
- h('summary', { style: { cursor: 'pointer', color: 'var(--dsw-alias-link)', fontSize: 13 } }, '查看配置'),
1281
+ h('summary', { style: { cursor: 'pointer', color: 'var(--dsw-alias-link)', fontSize: 13 } }, '查看规则说明'),
1182
1282
  h('div', { style: { display: 'grid', gap: 4, paddingTop: 6 } },
1183
- card.intent ? h('p', { style: mutedTextStyle }, `适用请求:${card.intent}`) : null,
1184
- parametersText ? h('p', { style: { ...mutedTextStyle, overflowWrap: 'anywhere' } }, `请求参数:${parametersText}`)
1185
- : h('p', { style: mutedTextStyle }, '请求参数:无'),
1283
+ card.rule_description ? h('p', { style: { ...mutedTextStyle, overflowWrap: 'anywhere' } }, card.rule_description) : null,
1186
1284
  requesterIds ? h('p', { style: { ...mutedTextStyle, overflowWrap: 'anywhere' } }, `允许请求的 IM 用户:${requesterIds}`) : null,
1187
1285
  examplesText ? h('p', { style: mutedTextStyle }, `请求示例:${examplesText}`) : null,
1286
+ Array.isArray(card.parameters) && card.parameters.length
1287
+ ? h('p', { style: { ...mutedTextStyle, overflowWrap: 'anywhere' } },
1288
+ `请求时还需提供:${card.parameters.map(parameterSummary).join(';')}`)
1289
+ : h('p', { style: mutedTextStyle }, '无需额外参数,按请求中的内容处理'),
1188
1290
  card.task_prompt ? h('p', { style: mutedTextStyle }, `处理要求:${card.task_prompt}`) : null))),
1189
- h('div', { style: { display: 'flex', gap: 6, flexWrap: 'wrap', justifyContent: 'flex-end' } },
1291
+ h('div', { style: { display: 'flex', gap: 6, flexWrap: 'wrap', justifyContent: 'flex-end', paddingTop: 4,
1292
+ borderTop: '1px solid var(--dsw-alias-border-l2)' } },
1190
1293
  h('button', { type: 'button', disabled: busy, onClick: () => editCard(card), style: secondaryButtonStyle }, '编辑'),
1191
1294
  h('button', { type: 'button', disabled: busy, onClick: () => copyCardToDraft(card), style: secondaryButtonStyle }, '复制'),
1192
1295
  h('button', { type: 'button', disabled: busy, onClick: () => void revokeCard(card), style: secondaryButtonStyle }, '停止')))
1193
1296
  }),
1194
- activeCards.length === 0 ? h('div', { style: { display: 'grid', justifyItems: 'start', gap: 8 } },
1297
+ activeCards.length === 0 ? h('div', { style: { ...collaborationCardStyle, gridColumn: '1 / -1', justifyItems: 'start' } },
1195
1298
  h('p', { style: mutedTextStyle }, authorizationLoadError
1196
1299
  ? '暂时无法确认已开放的能力。刷新授权状态后再试;列表未核实前请勿重复创建。'
1197
- : '还没有开放能力。先选机器人,再开放一项能力;PR 审查只是可选示例。'),
1300
+ : '还没有开放任务。先描述要做什么,再设置请求范围和有效期。'),
1198
1301
  authorizationLoadError ? h('button', { type: 'button', disabled: loading,
1199
1302
  onClick: () => void load().catch((error) => setMessage(error.message)), style: secondaryButtonStyle },
1200
1303
  loading ? '正在刷新…' : '刷新授权状态') : null) : null),
1304
+ ),
1201
1305
  h('div', { role: 'region', 'aria-label': '最近任务', style: { display: activeTab === 'local' ? 'grid' : 'none', gap: 12 } },
1202
1306
  h('h4', { style: sectionTitleStyle }, '最近任务'),
1203
1307
  ...(tasks.filter((task) => task.task_id).length ? tasks.filter((task) => task.task_id)
@@ -1219,13 +1323,13 @@ window.__ModuleLoader__.load({
1219
1323
  })
1220
1324
  : [h('p', { key: 'none', style: mutedTextStyle }, loadFailures.includes('status')
1221
1325
  ? '任务记录暂时无法读取,请刷新状态。' : '暂无任务。')])),
1222
- h('div', { role: 'region', 'aria-label': '新建能力', style: { display: activeTab === 'capability' ? 'grid' : 'none' } },
1326
+ h('div', { role: 'region', 'aria-label': '任务规则编辑器', style: { display: activeTab === 'collaboration' ? 'grid' : 'none' } },
1223
1327
  h('details', { id: 'ant-new-task', open: createOpen,
1224
1328
  onToggle: (event) => setCreateOpen(event.currentTarget.open),
1225
1329
  style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, padding: 12 } },
1226
1330
  h('summary', { id: 'ant-new-task-summary', style: { cursor: 'pointer', fontWeight: 600 } }, editingCardId ? '编辑能力配置' : '开放一项能力'),
1227
- h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 320px), 1fr))',
1228
- gap: 16, alignItems: 'start', marginTop: 12 } },
1331
+ h('div', { style: { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr)',
1332
+ gap: 12, alignItems: 'start', marginTop: 12 } },
1229
1333
  h('form', { id: 'ant-capability-create-form', onSubmit: createCard, style: { display: 'grid', gap: 9, minWidth: 0 } },
1230
1334
  editingCardId ? h('p', { role: 'status', style: mutedTextStyle },
1231
1335
  `保存会生成新的授权版本并停止旧版本;到期时间保持不变:${new Date(editingExpiresAt * 1000).toLocaleString('zh-CN')}。已提交任务继续使用原配置。`) : null,
@@ -1240,132 +1344,108 @@ window.__ModuleLoader__.load({
1240
1344
  '这是尚未绑定到设备的机器人。请在要继续使用它的电脑重新启用;Gitea 地址和配置记录会保留。若旧设备访问曾被撤销,原任务权限不会恢复,需要重新开放。') : null,
1241
1345
  selectedBot && conversationKind === 'group' && !selectedBot.history_available ? h('p', { role: 'status', style: mutedTextStyle },
1242
1346
  '这个机器人当前不能读取平台群历史;群聊只能参考实际送达的消息和本机近期记录。请检查 DSH-IM 权限。') : null,
1243
- h('label', null, '能力名称', h('input', { required: true, maxLength: 100, value: title,
1244
- onChange: (event) => setTitle(event.target.value), placeholder: '例如:总结项目进度', style: inputStyle })),
1245
- h('label', null, '什么请求属于这项能力', h('textarea', { required: true, maxLength: 1000, rows: 2, value: intent,
1246
- onChange: (event) => setIntent(event.target.value), placeholder: '例如:把群里讨论的本周进展整理成摘要', style: inputStyle })),
1247
- h('label', null, '同事可能怎么说(每行一句,可选)', h('textarea', { rows: 2, value: examples,
1248
- onChange: (event) => setExamples(event.target.value), placeholder: '帮我总结一下刚才的讨论', style: inputStyle })),
1249
- h('label', null, '处理要求', h('textarea', { required: true, maxLength: 4000, rows: 3, value: taskPrompt,
1250
- onChange: (event) => setTaskPrompt(event.target.value), placeholder: '只使用当前会话可见的内容,列出结论和待办', style: inputStyle })),
1251
- h('fieldset', { style: { ...panelStyle, padding: 10, display: 'grid', gap: 7 } },
1252
- h('legend', null, '审查 Agent(可多选)'),
1253
- h('p', { style: mutedTextStyle }, '普通请求使用默认 Agent;明确说“交叉 review”时,所有勾选项会并行审查同一份 PR 快照(至少勾选两位才构成交叉审查,最多三位,并会同时消耗各自的本机或团队模型额度)。'),
1254
- h('div', { style: { display: 'flex', gap: 14, flexWrap: 'wrap' } },
1255
- h('label', null, h('input', { type: 'checkbox', 'aria-label': '审查 Agent DSH', checked: reviewers.includes('dsh'), disabled: !dshAvailable && !reviewers.includes('dsh'),
1256
- onChange: (event) => changeReviewer('dsh', event.target.checked) }), ' DSH · 仅审查已提供的 diff'),
1257
- h('label', null, h('input', { type: 'checkbox', 'aria-label': '审查 Agent Codex', checked: reviewers.includes('codex'),
1258
- disabled: (!codexAvailable || !hasPrParameter) && !reviewers.includes('codex'),
1259
- onChange: (event) => changeReviewer('codex', event.target.checked) }), ' Codex · 完整快照只读审查'),
1260
- h('label', null, h('input', { type: 'checkbox', 'aria-label': '审查 Agent Claude Code', checked: reviewers.includes('claude-code'),
1261
- disabled: (!claudeCodeAvailable || !hasPrParameter) && !reviewers.includes('claude-code'),
1262
- onChange: (event) => changeReviewer('claude-code', event.target.checked) }), ' Claude Code · 使用本机权限审查')),
1263
- !hasPrParameter ? h('p', { style: mutedTextStyle }, 'Codex 与 Claude Code 需要先添加一个必填的 Gitea PR 编号参数。') : null,
1264
- reviewers.length > 1 ? h('label', null, '普通请求默认执行方', h('select', { value: runner,
1265
- onChange: (event) => setRunner(event.target.value), style: inputStyle },
1266
- ...reviewers.map((item) => h('option', { key: item, value: item }, reviewerLabel(item))))) : null),
1267
- nativePrUnavailableReason ? h('p', { role: 'status', style: mutedTextStyle },
1268
- `${nativePrUnavailableReason},已禁用 Codex/Claude Code 完整快照选项;可选择 DSH 差异审查。`) : null,
1269
- reviewers.includes('codex') ? h('p', { role: 'status', style: mutedTextStyle },
1270
- 'Codex 使用这台电脑的登录账号及其用量;ANT 为每项能力维护仓库缓存,每次审查创建固定到 PR Head 的临时代码快照。不会收到 Gitea 令牌;Git LFS 实际文件不会下载。') : null,
1271
- reviewers.includes('claude-code') ? h('p', { role: 'status', style: mutedTextStyle },
1272
- 'Claude Code 通过 DSH 已注册的子代理运行,可查看同一 PR 快照。ANT 不会传 Gitea 令牌;它沿用 DSH 的本机权限设置,不具备 Codex 路径相同的固定只读隔离。Git LFS 实际文件不会下载。') : null,
1273
- nativeReviewerSelected ? h('p', { role: 'status', style: mutedTextStyle },
1274
- '本机 PR 执行方需要 Gitea granular token 的只读权限 read:repository 和 read:user;read:user 仅用于这台 DSH 获取 Git HTTPS 登录名。') : null,
1275
- h('details', { style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, padding: 10 } },
1276
- h('summary', { style: { cursor: 'pointer' } }, '参数与授权范围'),
1277
- h('div', null, h('strong', null, '需要从请求中确认的参数'),
1278
- h('p', { style: mutedTextStyle }, '可不添加参数。参数键使用英文,给同事看的中文问题填在参数说明;需要指定 PR 时选“Gitea PR 编号”,系统会核对它是否存在。自由文本留在本机,服务端只核对长度和摘要。')),
1279
- ...parameters.map((rule, index) => h('div', { key: index, style: { display: 'flex', flexWrap: 'wrap', gap: 8,
1280
- padding: 8, border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8 } },
1281
- h('input', { 'aria-label': '参数键(英文)', value: rule.name, maxLength: 32, placeholder: '英文键,例如 pr_number',
1282
- onChange: (event) => updateParameter(index, { name: event.target.value }), style: { ...inputStyle, flex: '1 1 150px' } }),
1283
- h('input', { 'aria-label': '参数说明', value: rule.description, maxLength: 100,
1284
- onChange: (event) => updateParameter(index, { description: event.target.value }),
1285
- placeholder: '给同事看的问题,例如要审查哪个 PR', style: { ...inputStyle, flex: '1 1 230px' } }),
1286
- h('select', { 'aria-label': '参数类型', value: rule.kind,
1287
- onChange: (event) => updateParameter(index, { kind: event.target.value }), style: { ...inputStyle, flex: '1 1 150px' } },
1288
- h('option', { value: 'text' }, '文本'), h('option', { value: 'integer' }, '整数'),
1289
- h('option', { value: 'choice' }, '选项'), h('option', { value: 'gitea_pr' }, 'Gitea PR 编号')),
1290
- h('label', null, h('input', { type: 'checkbox', 'aria-label': '参数必填', checked: rule.required,
1291
- onChange: (event) => updateParameter(index, { required: event.target.checked }) }), ' 必填'),
1292
- ['integer', 'gitea_pr'].includes(rule.kind) ? h('input', { 'aria-label': '最小值', type: 'number', value: rule.min,
1293
- onChange: (event) => updateParameter(index, { min: event.target.value }), placeholder: '最小值', style: { ...inputStyle, width: 120 } }) : null,
1294
- ['text', 'integer', 'gitea_pr'].includes(rule.kind) ? h('input', { 'aria-label': '最大值', type: 'number', value: rule.max,
1295
- onChange: (event) => updateParameter(index, { max: event.target.value }), placeholder: '最大值', style: { ...inputStyle, width: 120 } }) : null,
1296
- rule.kind === 'choice' ? h('input', { 'aria-label': '可选值', value: rule.choices,
1297
- onChange: (event) => updateParameter(index, { choices: event.target.value }), placeholder: '用逗号分隔选项', style: { ...inputStyle, flex: '1 1 250px' } }) : null,
1298
- rule.kind === 'gitea_pr' ? h('span', { style: mutedTextStyle },
1299
- `使用机器人的可信 Gitea 地址:${selectedEnrollment?.gitea_origin || trustedOrigin || '请在下方登记一次'}`) : null,
1300
- rule.kind === 'gitea_pr' ? h('input', { 'aria-label': 'Gitea 仓库', value: rule.repository,
1301
- onChange: (event) => updateParameter(index, { repository: event.target.value }), placeholder: '团队/仓库', style: { ...inputStyle, flex: '1 1 180px' } }) : null,
1302
- h('button', { type: 'button', style: secondaryButtonStyle,
1303
- onClick: () => setParameters((current) => current.filter((_, i) => i !== index)) }, '移除参数'))),
1304
- h('button', { type: 'button', disabled: parameters.length >= 12, style: secondaryButtonStyle,
1305
- onClick: () => setParameters((current) => [...current, { name: '', kind: 'text', required: true,
1306
- description: '', min: '', max: '', choices: '', gitea_origin: '', repository: '' }]) }, '添加参数'),
1307
- parameters.some((rule) => rule.kind === 'gitea_pr') ? h('div', { style: { display: 'grid', gap: 6 } },
1308
- h('p', { style: mutedTextStyle }, selectedEnrollment?.gitea_origin
1309
- ? `将使用机器人已登记的可信 Gitea 地址:${selectedEnrollment.gitea_origin}`
1310
- : '请先在“机器人与聊天”中为这个机器人登记可信 Gitea 地址。'),
1311
- !tokenConfigured ? h('button', { type: 'button', onClick: () => onSelectTab('local'), style: secondaryButtonStyle },
1312
- '前往本机与任务保存 Gitea 令牌') : h('p', { style: mutedTextStyle }, '本机 Gitea 只读令牌已保存。')) : null,
1313
- h('label', null, '使用场景', h('select', { value: conversationKind,
1314
- onChange: (event) => { setConversationKind(event.target.value); setGroupId('') }, style: inputStyle },
1315
- h('option', { value: 'dm' }, '私聊'), h('option', { value: 'group' }, '指定群聊'))),
1316
- conversationKind === 'group' ? h('label', null, '群聊', h('select', { required: true, value: groupId,
1317
- onChange: (event) => setGroupId(event.target.value), style: inputStyle },
1318
- h('option', { value: '' }, '让群成员先 @ 机器人,再刷新列表'),
1319
- ...availableGroups.map((group) => h('option', { key: group.group_id, value: group.group_id },
1320
- `识别码 ${group.group_code} · ${group.group_id}`)))) : null,
1321
- conversationKind === 'group' ? h('button', { type: 'button', disabled: busy,
1322
- onClick: () => void load().catch((error) => setMessage(error.message)), style: secondaryButtonStyle }, '刷新群列表') : null,
1323
- h('label', null, '谁可以请求', h('select', { value: senderScope,
1324
- onChange: (event) => setSenderScope(event.target.value), style: inputStyle },
1325
- h('option', { value: 'any' }, conversationKind === 'group' ? '此群任何成员' : '任何能私聊此机器人者'),
1326
- h('option', { value: 'allowlist' }, '指定 IM 用户 ID'))),
1327
- senderScope === 'allowlist' ? h('input', { 'aria-label': '可请求的 IM 用户 ID', required: true, value: senderIds,
1328
- onChange: (event) => setSenderIds(event.target.value), placeholder: '多个 ID 用逗号分隔', style: inputStyle }) : null,
1329
- editingCardId ? h('p', { style: mutedTextStyle }, `有效期:${new Date(editingExpiresAt * 1000).toLocaleString('zh-CN')}(编辑时保持不变)`)
1330
- : h('label', null, '有效期', h('select', { value: days, onChange: (event) => setDays(Number(event.target.value)), style: inputStyle },
1331
- h('option', { value: 1 }, '1 天'), h('option', { value: 7 }, '7 天'), h('option', { value: 30 }, '30 天')))),
1347
+ h('label', null, '描述规则', h('textarea', { required: true, maxLength: 4000, rows: 4, value: ruleDescription,
1348
+ onChange: (event) => setRuleDescription(event.target.value),
1349
+ placeholder: '用日常的话说明何时执行、要参考哪些资料和结果怎么写。例如:群里有人以“审查 PR”开头并贴出完整 PR 链接时,检查代码差异,列出可定位的问题,不修改代码。', style: inputStyle })),
1350
+ h('p', { style: mutedTextStyle }, '描述只写任务何时执行和怎么处理;执行方由下方选项决定,未触发消息是否普通聊天由上方聊天开关决定。'),
1351
+ h('div', { style: { display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' } },
1352
+ h('button', { type: 'button', disabled: recognitionBusy || busy || !ruleDescription.trim(),
1353
+ onClick: () => void identifyRule(), style: secondaryButtonStyle }, recognitionBusy ? '正在识别…' : ruleSummaryReady ? '重新识别' : '识别规则'),
1354
+ h('span', { style: mutedTextStyle }, '每次点击调用一次团队模型并计入用量;只整理草稿,不会开放任务。改完描述后可重新识别。')),
1355
+ h('section', { role: 'region', 'aria-label': '识别结果摘要', style: { ...panelStyle, padding: 10, gap: 7 } },
1356
+ h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } },
1357
+ h('strong', null, ruleSummaryReady ? (title || '识别结果') : '识别结果预览'),
1358
+ h('span', { style: mutedTextStyle }, ruleSummaryReady ? '草稿 · 尚未开放' : '填写描述后可识别')),
1359
+ h('p', { style: { ...mutedTextStyle, overflowWrap: 'anywhere' } }, ruleSummaryNarrative),
1360
+ h('p', { style: mutedTextStyle }, ruleSummaryReady
1361
+ ? '草稿未开放。检查这段理解;有误就改上方描述并重新识别。'
1362
+ : '填写描述后识别;识别只生成草稿,不会开放任务。')),
1363
+ h('section', { style: { ...panelStyle, padding: 10, gap: 9, display: 'grid',
1364
+ gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 280px), 1fr))' } },
1365
+ h('strong', { style: { gridColumn: '1 / -1' } }, '通用授权'),
1366
+ h('p', { style: { ...mutedTextStyle, gridColumn: '1 / -1' } },
1367
+ '上方描述规则任务要做什么;这里限定谁能请求、在哪里请求、由谁执行和授权多久。'),
1368
+ h('label', null, '执行方', h('select', { value: runner, onChange: (event) => selectRunner(event.target.value),
1369
+ style: inputStyle },
1370
+ h('option', { value: 'dsh', disabled: !dshAvailable && runner !== 'dsh' }, 'DSH · 当前团队模型'),
1371
+ h('option', { value: 'codex', disabled: (!codexAvailable || !hasPrParameter) && runner !== 'codex' }, 'Codex · PR 快照只读'),
1372
+ h('option', { value: 'claude-code', disabled: (!claudeCodeAvailable || !hasPrParameter) && runner !== 'claude-code' }, 'Claude Code · 本机权限')),
1373
+ h('span', { style: mutedTextStyle }, runner === 'codex'
1374
+ ? 'Codex 使用本机登录,仅读取已核验的 PR 快照。'
1375
+ : runner === 'claude-code' ? 'Claude Code 沿用这台 DSH 的权限设置。'
1376
+ : 'DSH 使用获准团队模型处理会话中可见或已提供的内容。')),
1377
+ h('details', { id: 'ant-cross-review-details', open: crossReviewOpen,
1378
+ onToggle: (event) => setCrossReviewOpen(event.currentTarget.open),
1379
+ style: { gridColumn: '1 / -1', border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, padding: 10 } },
1380
+ h('summary', { style: { cursor: 'pointer', fontWeight: 600 } }, crossReviewSummary
1381
+ ? `交叉审查:${crossReviewSummary}` : '交叉审查(可选)'),
1382
+ h('p', { style: mutedTextStyle }, '普通请求只运行所选执行方;请求人明确提出“交叉 review”时才并行运行所选 Agent。'),
1383
+ h('div', { style: { display: 'flex', gap: 14, flexWrap: 'wrap' } },
1384
+ h('label', null, h('input', { type: 'checkbox', 'aria-label': '审查 Agent DSH', checked: reviewers.includes('dsh'),
1385
+ disabled: !dshAvailable && !reviewers.includes('dsh'),
1386
+ onChange: (event) => changeReviewer('dsh', event.target.checked) }), ' DSH'),
1387
+ h('label', null, h('input', { type: 'checkbox', 'aria-label': '审查 Agent Codex', checked: reviewers.includes('codex'),
1388
+ disabled: (!codexAvailable || !hasPrParameter) && !reviewers.includes('codex'),
1389
+ onChange: (event) => changeReviewer('codex', event.target.checked) }), ' Codex'),
1390
+ h('label', null, h('input', { type: 'checkbox', 'aria-label': '审查 Agent Claude Code', checked: reviewers.includes('claude-code'),
1391
+ disabled: (!claudeCodeAvailable || !hasPrParameter) && !reviewers.includes('claude-code'),
1392
+ onChange: (event) => changeReviewer('claude-code', event.target.checked) }), ' Claude Code')),
1393
+ !hasPrParameter ? h('p', { style: mutedTextStyle }, '从描述识别出 PR 任务后,才能使用 Codex 或 Claude Code。') : null),
1394
+ nativePrUnavailableReason ? h('p', { role: 'status', style: { ...mutedTextStyle, gridColumn: '1 / -1' } },
1395
+ `${nativePrUnavailableReason},已禁用 Codex/Claude Code 完整快照选项;可选择 DSH 差异审查。`) : null,
1396
+ h('label', null, '使用场景', h('select', { value: conversationKind,
1397
+ onChange: (event) => { setConversationKind(event.target.value); setGroupId('') }, style: inputStyle },
1398
+ h('option', { value: 'dm' }, '私聊'), h('option', { value: 'group' }, '指定群聊'))),
1399
+ conversationKind === 'group' ? h('div', { style: { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) auto',
1400
+ gap: 8, alignItems: 'end' } },
1401
+ h('label', null, '群聊', h('select', { required: true, value: groupId,
1402
+ onChange: (event) => setGroupId(event.target.value), style: inputStyle },
1403
+ h('option', { value: '' }, '让群成员先 @ 机器人,再刷新列表'),
1404
+ ...availableGroups.map((group) => h('option', { key: group.group_id, value: group.group_id },
1405
+ `识别码 ${group.group_code}`)))),
1406
+ h('button', { type: 'button', disabled: busy,
1407
+ onClick: () => void load().catch((error) => setMessage(error.message)), style: secondaryButtonStyle }, '刷新群列表')) : null,
1408
+ h('label', null, '谁可以请求', h('select', { value: senderScope,
1409
+ onChange: (event) => setSenderScope(event.target.value), style: inputStyle },
1410
+ h('option', { value: 'any' }, conversationKind === 'group' ? '此群任何成员' : '任何能私聊此机器人者'),
1411
+ h('option', { value: 'allowlist' }, '指定 IM 用户 ID'))),
1412
+ senderScope === 'allowlist' ? h('label', null, '可请求成员 ID', h('input', { 'aria-label': '可请求的 IM 用户 ID',
1413
+ required: true, value: senderIds, onChange: (event) => setSenderIds(event.target.value),
1414
+ placeholder: '多个 ID 用逗号分隔', style: inputStyle })) : null,
1415
+ editingCardId ? h('p', { style: { ...mutedTextStyle, alignSelf: 'end' } },
1416
+ `有效期:${new Date(editingExpiresAt * 1000).toLocaleString('zh-CN')}(编辑时保持不变)`)
1417
+ : h('label', null, '有效期', h('select', { value: days, onChange: (event) => setDays(Number(event.target.value)), style: inputStyle },
1418
+ h('option', { value: 1 }, '1 天'), h('option', { value: 7 }, '7 天'), h('option', { value: 30 }, '30 天'))),
1419
+ nativeReviewerSelected ? h('p', { style: { ...mutedTextStyle, gridColumn: '1 / -1' } },
1420
+ '本机 PR 审查需要 Gitea 只读权限 read:repository 和 read:user;ANT 不会把令牌交给执行方。') : null),
1332
1421
  ),
1333
1422
  h('aside', { id: 'ant-authorization-review', role: 'region', 'aria-label': '开放前复核', tabIndex: -1,
1334
- style: { ...settingsPanelStyle, padding: 14, gap: 10,
1335
- position: 'sticky', top: 12, alignSelf: 'start' } },
1423
+ style: { ...settingsPanelStyle, padding: 14, gap: 10, alignSelf: 'stretch' } },
1336
1424
  h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8 } },
1337
- h('h4', { style: sectionTitleStyle }, '开放前复核'),
1425
+ h('h4', { style: sectionTitleStyle }, '开放前看一眼'),
1338
1426
  h('span', { style: { ...mutedTextStyle, padding: '3px 8px', borderRadius: 999,
1339
1427
  backgroundColor: 'var(--dsw-alias-bg-layer-2)' } }, '草稿 · 未开放')),
1340
- h('p', { style: mutedTextStyle }, '保存后,机器人只会在以下范围内执行这项能力。'),
1341
- h('dl', { style: { display: 'grid', gap: 8, margin: 0 } },
1342
- ...[
1343
- ['触发位置', conversationKind === 'group' ? `群 ${groupId || '尚未选择'}` : '私聊'],
1344
- ['可请求人', requesterSummary],
1345
- ['执行方', executorSummary],
1346
- ['读取资料', resourceSummary],
1347
- ['有效期', editingCardId ? new Date(editingExpiresAt * 1000).toLocaleString('zh-CN') : `${days} 天`],
1348
- ['回复位置', resultRouteSummary],
1349
- ].map(([label, value]) => h('div', { key: label, style: { display: 'grid', gridTemplateColumns: '84px minmax(0, 1fr)', gap: 8 } },
1350
- h('dt', { style: mutedTextStyle }, label),
1351
- h('dd', { style: { margin: 0, minWidth: 0, overflowWrap: 'anywhere' } }, value)))),
1428
+ h('p', { style: mutedTextStyle }, `这项任务将在${conversationKind === 'group'
1429
+ ? `群“${selectedGroupSummary || '尚未选择'}”` : '私聊'}开放给${requesterSummary},由${executorSummary}执行;`
1430
+ + `有效期 ${editingCardId ? new Date(editingExpiresAt * 1000).toLocaleString('zh-CN') : `${days} 天`},${resultRouteSummary}。`),
1352
1431
  cardBlockReason ? h('p', { role: 'status', style: { ...mutedTextStyle,
1353
1432
  color: 'var(--dsw-alias-state-warning-primary, var(--dsw-alias-label-secondary))' } }, cardBlockReason) : null,
1354
1433
  h('button', { type: 'submit', form: 'ant-capability-create-form', disabled: busy || Boolean(cardBlockReason),
1355
1434
  style: { ...primaryButtonStyle, width: '100%' } }, editingCardId ? '保存能力修改' : '确认并开放能力'))))),
1356
1435
  h('div', { role: 'region', 'aria-label': '本机处理设置', style: { display: activeTab === 'local' ? 'grid' : 'none', gap: 12 } },
1357
- h('section', { style: { ...settingsPanelStyle, padding: 14 } },
1358
- h('strong', null, 'Gitea 本机凭据'),
1436
+ trustedOriginCard,
1437
+ showPrSecuritySettings ? h('section', { style: { ...settingsPanelStyle, padding: 14 } },
1438
+ h('strong', null, 'PR 只读凭据(本机)'),
1359
1439
  h('p', { style: mutedTextStyle }, tokenConfigured
1360
1440
  ? '只读访问令牌已保存在这台 DSH 的安全凭据中;不会同步到 ANT 服务端或发送给执行器。'
1361
- : 'PR 审查能力需要只读访问令牌。令牌只保存到这台 DSH。'),
1441
+ : '读取 Gitea PR 需要只读访问令牌。令牌只保存在这台 DSH,不提交给 ANT 服务端或执行器。'),
1362
1442
  h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } },
1363
1443
  h('input', { 'aria-label': 'Gitea 只读访问令牌', type: 'password', autoComplete: 'new-password', value: giteaToken,
1364
1444
  onChange: (event) => setGiteaToken(event.target.value),
1365
1445
  placeholder: tokenConfigured ? '已保存;输入新令牌可替换' : '粘贴 Gitea 只读令牌',
1366
1446
  style: { ...inputStyle, flex: '1 1 280px' } }),
1367
1447
  h('button', { type: 'button', disabled: busy || !giteaToken.trim(),
1368
- onClick: () => void saveGiteaToken(), style: secondaryButtonStyle }, tokenConfigured ? '替换本机令牌' : '保存本机令牌'))),
1448
+ onClick: () => void saveGiteaToken(), style: secondaryButtonStyle }, tokenConfigured ? '替换本机令牌' : '保存本机令牌'))) : null,
1369
1449
  h('details', { style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, padding: 12 } },
1370
1450
  h('summary', { style: { cursor: 'pointer', fontWeight: 600 } }, '本机处理设置'),
1371
1451
  h('p', { style: mutedTextStyle }, '本机并发上限只影响这台电脑接收的新任务;PR 仓库缓存上限按这台 DSH 的长期仓库缓存总量计算,不包括任务临时工作目录。'),
@@ -1390,7 +1470,19 @@ window.__ModuleLoader__.load({
1390
1470
  h('button', { disabled: busy, style: secondaryButtonStyle }, '保存本机处理设置'),
1391
1471
  h('p', { style: { ...mutedTextStyle, flex: '1 1 100%', margin: 0 } },
1392
1472
  '默认 10 GiB,范围 1–100 GiB,按长期仓库文件逻辑大小计算(不含临时工作区和文件系统分配开销)。日常不会淘汰仍启用能力的缓存;停用或到期能力会在刷新时回收。单次拉取若实际越界,为恢复预算会清理这项能力的仓库缓存(旧 PR 可重新下载)。')),
1393
- h('p', { style: mutedTextStyle }, `本机检测到的执行方:${executors.map((item) => `${item.title}${item.available ? '(可用)' : `(${item.reason || '未启用或未找到'})`}`).join('、') || '暂未读取到'}。只有能力配置下拉框中列出的执行方能接收委托。`))))
1473
+ ),
1474
+ h('section', { style: { ...settingsPanelStyle, padding: 14, gap: 8 } },
1475
+ h('strong', null, '执行方状态与权限边界'),
1476
+ h('p', { style: mutedTextStyle }, '这里显示本机可用情况;具体哪些执行方能处理某项任务,由“协作”页中的规则决定。'),
1477
+ loadFailures.includes('executors') ? h('p', { role: 'status', style: mutedTextStyle }, '暂时无法读取执行方状态,请刷新后重试。') : null,
1478
+ ...executors.filter((item) => ['dsh', 'codex-readonly', 'claude-code'].includes(item.id)).map((item) => h('div', {
1479
+ key: item.id, style: { display: 'grid', gap: 3, paddingTop: 8, borderTop: '1px solid var(--dsw-alias-border-l2)' } },
1480
+ h('strong', null, item.title + ' · ' + (item.available ? '可用' : item.reason || '未启用')),
1481
+ h('span', { style: mutedTextStyle }, item.id === 'codex-readonly'
1482
+ ? '读取固定到 PR Head 的快照,使用固定只读配置;Gitea 令牌不会交给 Codex。'
1483
+ : item.id === 'claude-code'
1484
+ ? '读取相同 PR 快照,但沿用本机 DSH 权限设置,不具备 Codex 路径的固定只读隔离。'
1485
+ : '处理授权任务提供的内容,不调用本机工具。'))))))
1394
1486
  }
1395
1487
 
1396
1488
  function SkillCenterPage({ remote, onBack, onAll, onSkills, onPlugins, openBundle,
@@ -2090,18 +2182,7 @@ window.__ModuleLoader__.load({
2090
2182
  ? provider.apiMode === 'responses' ? 'ant-team-openai-responses' : 'ant-team-openai-chat-completions'
2091
2183
  : provider.providerId === 'anthropic-default'
2092
2184
  ? 'ant-team-anthropic' : `ant-team-${provider.protocol}-${provider.providerId}`
2093
- const routes = providers.map(routeForProvider)
2094
- const ops = []
2095
- let previousRoutes = []
2096
- try {
2097
- const parsed = JSON.parse(localStorage.getItem(PROVIDER_ROUTES_STORAGE_KEY) || '[]')
2098
- if (Array.isArray(parsed)) previousRoutes = parsed.filter((route) => typeof route === 'string')
2099
- } catch {}
2100
- const knownRoutes = new Set([...previousRoutes, 'ant-team-openai', 'ant-team-openai-chat-completions',
2101
- 'ant-team-openai-responses', 'ant-team-anthropic'])
2102
- for (const route of knownRoutes) {
2103
- if (!routes.includes(route)) ops.push({ op: 'unset', path: ['providers', route] })
2104
- }
2185
+ const routeConfigurations = new Map()
2105
2186
  for (const provider of providers) {
2106
2187
  const route = routeForProvider(provider)
2107
2188
  const api = provider.protocol === 'openai'
@@ -2112,13 +2193,40 @@ window.__ModuleLoader__.load({
2112
2193
  : provider.protocol === 'openai'
2113
2194
  ? new URL(`/model-gateway/openai/providers/${encodeURIComponent(provider.providerId)}/v1`, normalized).toString()
2114
2195
  : new URL(`/model-gateway/anthropic/providers/${encodeURIComponent(provider.providerId)}`, normalized).toString()
2115
- ops.push({ op: 'set', path: ['providers', route], value: {
2196
+ routeConfigurations.set(route, {
2116
2197
  displayName: provider.providerName,
2117
2198
  apiKeyEnv: 'ANT_MODEL_API_KEY',
2118
2199
  api,
2119
2200
  baseURL,
2120
2201
  models: provider.models.map((model) => ({ id: model.model_id, name: model.display_name })),
2121
- } })
2202
+ })
2203
+ if (provider.protocol === 'openai' && provider.providerId === 'openai-default') {
2204
+ const aliases = provider.apiMode === 'responses'
2205
+ ? ['ant-team-openai-chat-completions', 'ant-team-openai'] : ['ant-team-openai']
2206
+ const legacyChat = {
2207
+ displayName: `${provider.providerName} · Chat(旧会话兼容)`,
2208
+ apiKeyEnv: 'ANT_MODEL_API_KEY',
2209
+ api: 'openai-completions',
2210
+ baseURL: new URL('/model-gateway/openai/v1', normalized).toString(),
2211
+ models: provider.models.map((model) => ({ id: model.model_id, name: model.display_name })),
2212
+ }
2213
+ for (const alias of aliases) routeConfigurations.set(alias, legacyChat)
2214
+ }
2215
+ }
2216
+ const routes = [...routeConfigurations.keys()]
2217
+ const ops = []
2218
+ let previousRoutes = []
2219
+ try {
2220
+ const parsed = JSON.parse(localStorage.getItem(PROVIDER_ROUTES_STORAGE_KEY) || '[]')
2221
+ if (Array.isArray(parsed)) previousRoutes = parsed.filter((route) => typeof route === 'string')
2222
+ } catch {}
2223
+ const knownRoutes = new Set([...previousRoutes, 'ant-team-openai', 'ant-team-openai-chat-completions',
2224
+ 'ant-team-openai-responses', 'ant-team-anthropic'])
2225
+ for (const route of knownRoutes) {
2226
+ if (!routes.includes(route)) ops.push({ op: 'unset', path: ['providers', route] })
2227
+ }
2228
+ for (const [route, value] of routeConfigurations) {
2229
+ ops.push({ op: 'set', path: ['providers', route], value })
2122
2230
  }
2123
2231
  const settingsWrite = await remote.settings.mutate('llm-pi-ai', ops, undefined)
2124
2232
  if (!settingsWrite.ok) throw new Error(settingsWrite.error?.message || 'DSH 无法更新团队模型设置。')