@ai-wendao/ant 0.5.8 → 0.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +1 -1
  2. package/client.js +226 -92
  3. package/index.js +6 -2
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -20,7 +20,7 @@ Logo 与侧栏品牌使用 DSH 正式 UI 插槽。新会话标题目前没有官
20
20
 
21
21
  飞书机器人连接和消息收发由 DSH-IM 负责;ANT Bundle 不保存机器人密钥,也不直连 IM 平台。成员在 ANT 插件的“协作”页选择机器人、启用本机协作,并设置私聊和各群的普通聊天开关。群第一次 @ 机器人后会出现在群列表。群聊天开关只控制普通聊天;任务卡还要单独指定私聊或群、请求人范围和有效期。
22
22
 
23
- 开放任务时,成员用自然语言描述什么时候执行、要看哪些资料、怎样处理和返回结果。描述中的 Gitea PR 链接、仓库和编号也用自然语言表达。点击“识别规则”会调用一次当前获准的 DSH 团队模型,只生成草稿,不保存任务;草稿包含明确触发短语,由机器人主人检查后再开放。机器人主人指在本人 DSH 中连接并管理该 DSH-IM 机器人的 ANT 成员,不代表群管理员身份;群内谁能请求任务仍由规则中的群范围和请求人范围决定。任务只在请求消息以已配置短语开头时执行;检查摘要不合意时,修改描述并重新识别。任务名称、触发条件、PR 信息和参数由识别结果转成内部规则,界面不要求逐项填写。执行方、使用范围、请求人和有效期由通用控件控制。
23
+ 开放任务时,成员用自然语言描述什么时候执行、要看哪些资料、怎样处理和返回结果。描述中的 Gitea PR 链接、仓库和编号也用自然语言表达。点击“识别规则”会调用一次当前获准的 DSH 团队模型,只生成草稿,不保存任务;草稿包含明确触发短语,由机器人主人检查后再开放。触发短语会显示在任务概览顶部,允许机器人主人直接修改;修改描述后重新识别会更新摘要,保留手动调整的短语并提示复核。只有请求消息以已保存短语开头时任务才会执行。机器人主人指在本人 DSH 中连接并管理该 DSH-IM 机器人的 ANT 成员,不代表群管理员身份;群内谁能请求任务仍由规则中的群范围和请求人范围决定。任务名称、意图、PR 信息和其他参数由识别结果转成内部规则,不要求逐项编辑。执行方式、使用范围、请求人和有效期由固定控件控制;新任务有效期可选 1、7、30 或 90 天,编辑已有任务时保留原到期时间。
24
24
 
25
25
  群任务和群聊天设置只在 ANT 协作页配置。当前版本不设群管理员身份绑定,也不支持在群里开放任务、确认授权或开关群聊。群成员可以普通聊天,或按已开放任务的群范围、请求人范围、触发条件和有效期发起任务;未开放的任务不会因为群成员的消息而执行。关闭群普通聊天不会撤销已开放任务。服务端升级时会停用没有明确触发方式的旧任务卡;要继续使用这类任务,需在新界面重新描述规则并开放。
26
26
 
package/client.js CHANGED
@@ -533,7 +533,9 @@ window.__ModuleLoader__.load({
533
533
  const [reviewers, setReviewers] = useState(['dsh'])
534
534
  const [crossReviewAlways, setCrossReviewAlways] = useState(false)
535
535
  const [parameters, setParameters] = useState([])
536
- const [crossReviewOpen, setCrossReviewOpen] = useState(false)
536
+ const [multiAgentMode, setMultiAgentMode] = useState(false)
537
+ const [formError, setFormError] = useState(null)
538
+ const triggerPhrasesEditedRef = useRef(false)
537
539
  const [giteaToken, setGiteaToken] = useState('')
538
540
  const [tokenConfigured, setTokenConfigured] = useState(false)
539
541
  const [senderScope, setSenderScope] = useState('any')
@@ -641,8 +643,18 @@ window.__ModuleLoader__.load({
641
643
  const codexAvailable = executors.some((item) => item.id === 'codex-readonly' && item.available)
642
644
  const claudeCodeAvailable = executors.some((item) => item.id === 'claude-code' && item.available)
643
645
  const dshAvailable = executors.some((item) => item.id === 'dsh' && item.available)
644
- const nativePrUnavailableReason = executors.find((item) => item.reason)?.reason || null
645
646
  const reviewerLabel = (id) => ({ dsh: 'DSH', codex: 'Codex', 'claude-code': 'Claude Code' })[id] || id
647
+ const executorReason = (id) => {
648
+ if (loadFailures.includes('executors')) return '无法读取这台 DSH 的执行方状态,请刷新后重试。'
649
+ const reasons = []
650
+ if (['codex', 'claude-code'].includes(id) && !hasPrParameter) {
651
+ reasons.push('请在任务描述中写明 Gitea PR 链接或编号并重新识别')
652
+ }
653
+ const executorId = id === 'codex' ? 'codex-readonly' : id
654
+ const executor = executors.find((item) => item.id === executorId)
655
+ if (!executor?.available) reasons.push(executor?.reason || `这台 DSH 尚未启用 ${reviewerLabel(id)}`)
656
+ return reasons.join(';')
657
+ }
646
658
  const cardReviewerSummary = (card) => {
647
659
  const selected = Array.isArray(card.reviewers) && card.reviewers.length ? card.reviewers : [card.runner]
648
660
  return selected.length > 1
@@ -657,8 +669,11 @@ window.__ModuleLoader__.load({
657
669
  ? `仅指定成员:${senderIds.trim() || '尚未填写成员 ID'}`
658
670
  : conversationKind === 'group' ? '群内所有成员' : '任何能私聊此机器人的人'
659
671
  const resultRouteSummary = conversationKind === 'group' ? '回复到原群(群内可见)' : '回复到发起任务的私聊'
660
- const ruleTriggerSummary = triggerPhrases.split('\n').map((item) => item.trim()).filter(Boolean)
661
- const ruleSummaryReady = Boolean(title || intent || ruleTriggerSummary.length || parameters.length || taskPrompt)
672
+ const triggerPhraseEntries = triggerPhrases ? triggerPhrases.split('\n') : ['']
673
+ const ruleTriggerSummary = triggerPhraseEntries.map((item) => item.trim()).filter(Boolean)
674
+ const ruleSummaryReady = Boolean(title.trim() && intent.trim() && taskPrompt.trim())
675
+ const ruleDescriptionNeedsRecognition = recognizedDescriptionRef.current !== null
676
+ && recognizedDescriptionRef.current !== ruleDescription.trim()
662
677
  const parameterSummary = (rule) => {
663
678
  const parts = [rule.required === false ? '可选' : '必填']
664
679
  const choices = Array.isArray(rule.choices) ? rule.choices
@@ -683,26 +698,26 @@ window.__ModuleLoader__.load({
683
698
  ? `请求时还需提供:${parameters.map(parameterSummary).join(';')}`
684
699
  : '无需额外参数,按请求中的内容处理'
685
700
  const ruleSummaryNarrative = ruleSummaryReady
686
- ? `${ruleTriggerSummary.length ? `消息以“${ruleTriggerSummary.join('”或“')}”开头时,` : ''}${intent.trim() || title.trim()}${taskPrompt.trim() ? `;${taskPrompt.trim()}` : ''};${recognizedInputsSummary}`
701
+ ? intent.trim() || title.trim()
687
702
  : '识别后会用一段摘要呈现系统理解的任务。'
688
- const executorSummary = reviewers.length > 1
689
- ? crossReviewAlways ? `每次并行交叉审查(${reviewers.map(reviewerLabel).join('、')})`
690
- : `默认 ${reviewerLabel(runner)};可逐次交叉审查`
691
- : runner === 'codex' ? '本机 Codex · PR 快照'
692
- : runner === 'claude-code' ? '本机 Claude Code · PR 快照' : 'DSH · 当前授权模型'
693
- const crossReviewSummary = reviewers.filter((item) => item !== runner).map(reviewerLabel).join('、')
694
- const crossReviewHeading = crossReviewAlways && reviewers.length > 1
695
- ? `每次交叉审查:${reviewers.map(reviewerLabel).join('、')}`
696
- : crossReviewSummary ? `交叉审查:${crossReviewSummary}` : '交叉审查(可选)'
703
+ const reviewModeSummary = multiAgentMode || reviewers.length > 1
704
+ ? reviewers.length < 2 ? '交叉审查尚未配置完成,至少再选择一位可用 Agent'
705
+ : crossReviewAlways
706
+ ? `每次任务请求都并行运行 ${reviewers.map(reviewerLabel).join('、')}(会增加调用量)`
707
+ : `普通请求由 ${reviewerLabel(runner)} 执行;请求人明确提出“交叉 review”时并行运行 ${reviewers.map(reviewerLabel).join('、')}`
708
+ : `${reviewerLabel(runner)} 单 Agent 执行`
697
709
  const cardBlockReason = enrollmentLoadError ? '暂时无法核对机器人是否已启用协作,请刷新状态后再试。'
698
710
  : !botConnected || !selectedEnrollmentBound ? '先在上方把机器人绑定到这台 DSH。'
699
711
  : loadFailures.includes('settings') || defaults?.task_rule_schema_version !== 2
700
712
  ? '团队服务端尚未支持触发规则的持久化与执行前核验;请先升级 ANT 服务端。'
701
713
  : !ruleDescription.trim() ? '请先用自然语言描述要做的任务。'
702
- : recognizedDescriptionRef.current !== null && recognizedDescriptionRef.current !== ruleDescription.trim()
714
+ : ruleDescriptionNeedsRecognition
703
715
  ? '描述已修改,请重新识别,让执行规则与描述保持一致。'
704
716
  : !title.trim() || !intent.trim() || !taskPrompt.trim() ? '先识别任务描述并检查摘要,再开放任务。'
705
- : !triggerPhrases.trim() ? '描述里还没有明确写出何时触发。请补充触发方式并重新识别。'
717
+ : !triggerPhrases.trim() ? '请填写至少一个触发短语。'
718
+ : multiAgentMode && reviewers.length < 2 ? '交叉审查至少选择两位可用 Agent。'
719
+ : crossReviewAlways && defaults?.cross_review_policy_version !== 1
720
+ ? '当前团队服务端尚不支持“每次都交叉审查”,请先升级 ANT 服务端。'
706
721
  : conversationKind === 'group' && loadFailures.includes('groups') ? '暂时无法核对目标群,请刷新状态后再试。'
707
722
  : conversationKind === 'group' && !groupId ? '请在“请求范围”中选择目标群;若列表为空,让群成员先 @机器人,再刷新群列表。'
708
723
  : senderScope === 'allowlist' && !senderIds.trim() ? '请填写可请求成员的 IM ID。'
@@ -726,18 +741,60 @@ window.__ModuleLoader__.load({
726
741
  const loadNames = { bots: '机器人', 'enrolled-bots': '协作启用状态', groups: '群聊列表',
727
742
  cards: '任务授权', settings: '团队默认值', executors: '本机执行器',
728
743
  'local-settings': '本机设置', status: '最近任务' }
744
+ const reportFormError = (field, text) => {
745
+ setFormError({ field, text })
746
+ if (typeof document !== 'undefined') setTimeout(() => {
747
+ const target = document.getElementById(field)
748
+ target?.scrollIntoView?.({ block: 'center', behavior: 'smooth' })
749
+ target?.focus?.()
750
+ }, 0)
751
+ }
752
+ const clearFormError = (field) => setFormError((current) => current?.field === field ? null : current)
753
+ const setTriggerPhraseAt = (index, value) => {
754
+ const next = [...triggerPhraseEntries]
755
+ next[index] = value
756
+ triggerPhrasesEditedRef.current = true
757
+ setTriggerPhrases(next.join('\n'))
758
+ clearFormError('ant-trigger-phrases')
759
+ }
760
+ const addTriggerPhrase = () => {
761
+ triggerPhrasesEditedRef.current = true
762
+ setTriggerPhrases([...triggerPhraseEntries, ''].join('\n'))
763
+ clearFormError('ant-trigger-phrases')
764
+ }
765
+ const removeTriggerPhrase = (index) => {
766
+ triggerPhrasesEditedRef.current = true
767
+ setTriggerPhrases(triggerPhraseEntries.filter((_item, itemIndex) => itemIndex !== index).join('\n'))
768
+ clearFormError('ant-trigger-phrases')
769
+ }
729
770
  const changeReviewer = (id, enabled) => {
730
771
  const next = enabled ? [...reviewers, id] : reviewers.filter((item) => item !== id)
731
- if (!next.length) { setMessage('至少保留一位审查 Agent。'); return }
732
- setReviewers([...new Set(next)])
733
- if (next.length < 2) setCrossReviewAlways(false)
734
- if (!next.includes(runner)) setRunner(next[0])
772
+ const unique = [...new Set(next)]
773
+ setReviewers(unique)
774
+ if (unique.length && !unique.includes(runner)) setRunner(unique[0])
775
+ if (multiAgentMode && unique.length < 2) {
776
+ reportFormError('ant-reviewer-list', '交叉审查至少选择两位可用 Agent。')
777
+ setCrossReviewAlways(false)
778
+ } else clearFormError('ant-reviewer-list')
735
779
  }
736
780
  const selectRunner = (id) => {
737
781
  setRunner(id)
738
- setReviewers((current) => current.length > 1
739
- ? [...new Set([...current, id])]
782
+ setReviewers((current) => multiAgentMode
783
+ ? current.includes(id) ? current : [...current, id]
740
784
  : [id])
785
+ clearFormError('ant-agent-mode')
786
+ }
787
+ const selectExecutionMode = (mode) => {
788
+ if (mode === 'single') {
789
+ setMultiAgentMode(false)
790
+ setReviewers([runner])
791
+ setCrossReviewAlways(false)
792
+ } else {
793
+ setMultiAgentMode(true)
794
+ if (reviewers.length < 2) setCrossReviewAlways(false)
795
+ }
796
+ clearFormError('ant-agent-mode')
797
+ clearFormError('ant-reviewer-list')
741
798
  }
742
799
  const startNewCard = (context = {}) => {
743
800
  setEditingCardId(null); setEditingExpiresAt(null)
@@ -745,10 +802,12 @@ window.__ModuleLoader__.load({
745
802
  setRecognitionMessage('')
746
803
  recognizedBaselineRef.current = null
747
804
  recognizedDescriptionRef.current = null
805
+ triggerPhrasesEditedRef.current = false
806
+ setFormError(null)
748
807
  setRuleDescription('')
749
808
  setTriggerPhrases('')
750
809
  setTitle(''); setIntent(''); setTaskPrompt(''); setRunner('dsh')
751
- setReviewers(['dsh']); setCrossReviewAlways(false); setCrossReviewOpen(false)
810
+ setReviewers(['dsh']); setCrossReviewAlways(false); setMultiAgentMode(false)
752
811
  setParameters([]); setConversationKind(context.conversationKind || 'dm')
753
812
  setGroupId(context.groupId || '')
754
813
  setSenderScope('any'); setSenderIds(''); setDays(7); setCreateOpen(true)
@@ -768,6 +827,8 @@ window.__ModuleLoader__.load({
768
827
  const editCard = (card, copy = false) => {
769
828
  setBotId(card.bot_id)
770
829
  setRecognitionMessage('')
830
+ setFormError(null)
831
+ triggerPhrasesEditedRef.current = false
771
832
  setTrustedOrigin(enrolled.find((bot) => bot.bot_id === card.bot_id)?.gitea_origin || '')
772
833
  setEditingCardId(copy ? null : card.id); setEditingExpiresAt(copy ? null : card.expires_at)
773
834
  const taskDraft = taskRuleDraftFromCard(card, copy ? `${card.title.slice(0, 94)}(副本)` : card.title)
@@ -784,9 +845,10 @@ window.__ModuleLoader__.load({
784
845
  if (copy) setDays(7)
785
846
  setIntent(taskDraft.intent)
786
847
  setTaskPrompt(taskDraft.task_prompt); setRunner(card.runner)
787
- setReviewers(Array.isArray(card.reviewers) && card.reviewers.length ? card.reviewers : [card.runner])
848
+ const selectedReviewers = Array.isArray(card.reviewers) && card.reviewers.length ? card.reviewers : [card.runner]
849
+ setReviewers(selectedReviewers)
850
+ setMultiAgentMode(selectedReviewers.length > 1)
788
851
  setCrossReviewAlways(card.cross_review_always === true)
789
- setCrossReviewOpen(false)
790
852
  setParameters(taskDraft.parameters)
791
853
  setConversationKind(card.conversation_kind); setGroupId(card.conversation_id || '')
792
854
  setSenderScope(card.sender_scope); setSenderIds((card.sender_ids || []).join(', '))
@@ -795,7 +857,7 @@ window.__ModuleLoader__.load({
795
857
  const applyRuleDraft = (draft) => {
796
858
  setTitle(draft.title)
797
859
  setIntent(draft.intent)
798
- setTriggerPhrases(draft.trigger_phrases.join('\n'))
860
+ if (!triggerPhrasesEditedRef.current) setTriggerPhrases(draft.trigger_phrases.join('\n'))
799
861
  setTaskPrompt(draft.task_prompt)
800
862
  setParameters(draft.parameters)
801
863
  }
@@ -804,7 +866,7 @@ window.__ModuleLoader__.load({
804
866
  setRecognitionMessage('先描述这项任务何时触发、要做什么以及涉及哪些资料。')
805
867
  return
806
868
  }
807
- setRecognitionBusy(true); setMessage(''); setRecognitionMessage('正在调用团队模型识别规则…')
869
+ setRecognitionBusy(true); setFormError(null); setRecognitionMessage('正在调用团队模型识别规则…')
808
870
  try {
809
871
  const response = await fetch('/api/ant-teams/delegation/recognize', {
810
872
  method: 'POST', headers: { 'content-type': 'application/json' },
@@ -837,9 +899,11 @@ window.__ModuleLoader__.load({
837
899
  applyRuleDraft(candidate)
838
900
  recognizedBaselineRef.current = candidate
839
901
  recognizedDescriptionRef.current = ruleDescription.trim()
840
- setRecognitionMessage(previous
841
- ? '已根据新描述更新草稿,请检查摘要和授权范围。'
842
- : '已识别为任务草稿。检查摘要和授权范围;此操作不会开放任务。')
902
+ setRecognitionMessage(triggerPhrasesEditedRef.current
903
+ ? '已更新任务草稿;保留了手动调整的触发短语,请核对它仍符合新描述。'
904
+ : previous
905
+ ? '已根据新描述更新草稿,请检查摘要和授权范围。'
906
+ : '已识别为任务草稿。检查摘要和授权范围;此操作不会开放任务。')
843
907
  } catch (error) { setRecognitionMessage(error.message) }
844
908
  finally { setRecognitionBusy(false) }
845
909
  }
@@ -980,7 +1044,7 @@ window.__ModuleLoader__.load({
980
1044
  return focusAfterTabChange('collaboration', 'ant-new-task-summary')
981
1045
  }
982
1046
  setCreateOpen(true)
983
- focusAfterTabChange('collaboration', 'ant-authorization-review')
1047
+ focusAfterTabChange('collaboration', 'ant-open-confirm')
984
1048
  }
985
1049
  const collaborationGridStyle = {
986
1050
  display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 300px), 1fr))', gap: 12,
@@ -1025,25 +1089,28 @@ window.__ModuleLoader__.load({
1025
1089
  const createCard = async (event) => {
1026
1090
  event.preventDefault()
1027
1091
  setDraftNotice('')
1028
- if (!selectedBot || !botConnected || !selectedEnrollmentBound) { setMessage('请先在这台 DSH 绑定选中的协作机器人。'); return }
1092
+ setFormError(null)
1093
+ if (!selectedBot || !botConnected || !selectedEnrollmentBound) {
1094
+ reportFormError('ant-bot-selector', '请先在这台 DSH 绑定选中的协作机器人。'); return
1095
+ }
1029
1096
  if (crossReviewAlways && defaults?.cross_review_policy_version !== 1) {
1030
- setMessage('当前团队服务端尚不支持“每次都交叉审查”,请先升级 ANT 服务端。'); return
1097
+ reportFormError('ant-open-confirm', '当前团队服务端尚不支持“每次都交叉审查”,请先升级 ANT 服务端。'); return
1031
1098
  }
1032
1099
  if (editingCardId && (!editingExpiresAt || editingExpiresAt <= Math.floor(Date.now() / 1000))) {
1033
- setMessage('这项能力已到期,无法继续编辑;请返回后重新开放一项能力。'); return
1100
+ reportFormError('ant-open-confirm', '这项能力已到期,无法继续编辑;请返回后重新开放一项能力。'); return
1034
1101
  }
1035
1102
  if (!ruleDescription.trim() || !title.trim() || !intent.trim() || !taskPrompt.trim()) {
1036
- setMessage('请填写任务描述并点击“识别规则”,检查摘要后再开放。'); return
1103
+ reportFormError('ant-rule-description', '请填写任务描述并点击“识别规则”,检查摘要后再开放。'); return
1037
1104
  }
1038
1105
  const bytes = (value) => new TextEncoder().encode(value).length
1039
1106
  if (bytes(ruleDescription.trim()) > 4000 || bytes(title.trim()) > 100
1040
1107
  || bytes(intent.trim()) > 1000 || bytes(taskPrompt.trim()) > 4000) {
1041
- setMessage('任务描述或识别摘要过长,请缩短描述并重新识别。'); return
1108
+ reportFormError('ant-rule-description', '任务描述或识别摘要过长,请缩短描述并重新识别。'); return
1042
1109
  }
1043
1110
  const triggerLines = triggerPhrases.split('\n').map((item) => item.normalize('NFKC').trim()).filter(Boolean)
1044
1111
  if (!triggerLines.length || triggerLines.length > 8 || triggerLines.some((item) => bytes(item) > 120)
1045
1112
  || new Set(triggerLines.map((item) => item.toLowerCase())).size !== triggerLines.length) {
1046
- setMessage('没能从描述中识别出清楚的触发方式。请写明消息以什么开头时执行,再重新识别。'); return
1113
+ reportFormError('ant-trigger-phrases', '触发短语需填写 1–8 条,每条不超过 120 字节且不能重复。'); return
1047
1114
  }
1048
1115
  const normalized = parameters.map((rule) => ({ name: rule.name.trim(), kind: rule.kind,
1049
1116
  description: rule.description.trim() || null,
@@ -1054,18 +1121,18 @@ window.__ModuleLoader__.load({
1054
1121
  gitea_origin: rule.kind === 'gitea_pr' ? selectedEnrollment?.gitea_origin || null : null,
1055
1122
  repository: rule.kind === 'gitea_pr' ? rule.repository.trim().toLowerCase() : null }))
1056
1123
  if (normalized.some((rule) => !/^[A-Za-z][A-Za-z0-9_]{0,31}$/u.test(rule.name))) {
1057
- setMessage('没能从描述中整理出有效的请求信息。请说明同事需要提供什么,再重新识别。'); return
1124
+ reportFormError('ant-rule-description', '没能从描述中整理出有效的请求信息。请说明同事需要提供什么,再重新识别。'); return
1058
1125
  }
1059
1126
  if (new Set(normalized.map((rule) => rule.name)).size !== normalized.length) {
1060
- setMessage('请求信息识别有重复项。请修改任务描述并重新识别。'); return
1127
+ reportFormError('ant-rule-description', '请求信息识别有重复项。请修改任务描述并重新识别。'); return
1061
1128
  }
1062
1129
  if (normalized.some((rule) => rule.description && bytes(rule.description) > 100)) {
1063
- setMessage('请求信息说明过长。请缩短任务描述并重新识别。'); return
1130
+ reportFormError('ant-rule-description', '请求信息说明过长。请缩短任务描述并重新识别。'); return
1064
1131
  }
1065
1132
  if (normalized.some((rule) => rule.kind === 'choice' && (!rule.choices?.length || rule.choices.length > 32
1066
1133
  || rule.choices.some((choice) => bytes(choice) > 100)
1067
1134
  || new Set(rule.choices).size !== rule.choices.length))) {
1068
- setMessage('描述中的可选范围不清楚或重复。请修改描述并重新识别。'); return
1135
+ reportFormError('ant-rule-description', '描述中的可选范围不清楚或重复。请修改描述并重新识别。'); return
1069
1136
  }
1070
1137
  if (normalized.some((rule) => (rule.min != null && !Number.isInteger(rule.min))
1071
1138
  || (rule.max != null && !Number.isInteger(rule.max))
@@ -1073,10 +1140,10 @@ window.__ModuleLoader__.load({
1073
1140
  || (rule.kind === 'text' && rule.max != null && (rule.max < 1 || rule.max > 4096))
1074
1141
  || (rule.kind === 'gitea_pr' && ((rule.min != null && rule.min < 1)
1075
1142
  || (rule.max != null && rule.max > 1_000_000_000))))) {
1076
- setMessage('描述中的请求范围无效。请明确范围或提供完整 PR 链接,再重新识别。'); return
1143
+ reportFormError('ant-rule-description', '描述中的请求范围无效。请明确范围或提供完整 PR 链接,再重新识别。'); return
1077
1144
  }
1078
1145
  if (normalized.some((rule) => rule.kind === 'gitea_pr' && (!rule.gitea_origin || !rule.repository))) {
1079
- setMessage('没能从描述中识别 PR 来源或仓库。请补充完整 PR 链接或仓库名,再重新识别。'); return
1146
+ reportFormError('ant-rule-description', '没能从描述中识别 PR 来源或仓库。请补充完整 PR 链接或仓库名,再重新识别。'); return
1080
1147
  }
1081
1148
  const validRepository = (value) => {
1082
1149
  const parts = value.split('/')
@@ -1084,14 +1151,17 @@ window.__ModuleLoader__.load({
1084
1151
  && /^[A-Za-z0-9_.-]{1,100}$/u.test(part))
1085
1152
  }
1086
1153
  if (normalized.some((rule) => rule.kind === 'gitea_pr' && !validRepository(rule.repository))) {
1087
- setMessage('仓库名无法识别。请在描述中写明“团队名/仓库名”或粘贴完整 PR 链接,再重新识别。'); return
1154
+ reportFormError('ant-rule-description', '仓库名无法识别。请在描述中写明“团队名/仓库名”或粘贴完整 PR 链接,再重新识别。'); return
1088
1155
  }
1089
- if (!reviewers.length || !reviewers.includes(runner)) {
1090
- setMessage('请至少选择一位审查 Agent,并设置普通请求的默认执行方。'); return
1156
+ if (!reviewers.length || !reviewers.includes(runner)
1157
+ || (multiAgentMode && reviewers.length < 2)
1158
+ || (!multiAgentMode && reviewers.length !== 1)) {
1159
+ reportFormError('ant-agent-mode', multiAgentMode
1160
+ ? '交叉审查至少选择两位可用 Agent。' : '单 Agent 模式请选择一位执行 Agent。'); return
1091
1161
  }
1092
1162
  if (nativeReviewerSelected && (normalized.length !== 1
1093
1163
  || normalized[0].kind !== 'gitea_pr' || !normalized[0].required)) {
1094
- setMessage('Codex 和 Claude Code 目前只支持审查一个 PR。请在任务描述中写明 PR 链接,再重新识别。'); return
1164
+ reportFormError('ant-rule-description', 'Codex 和 Claude Code 目前只支持审查一个 PR。请在任务描述中写明 PR 链接,再重新识别。'); return
1095
1165
  }
1096
1166
  const scope = conversationKind === 'group' ? `群 ${groupId}` : '私聊'
1097
1167
  const resultDestination = conversationKind === 'group' ? '原群(群内可见)' : '发起任务的私聊'
@@ -1100,14 +1170,11 @@ window.__ModuleLoader__.load({
1100
1170
  codex: 'Codex 使用已核验 PR Head 的临时代码快照,并启用本机固定只读权限;不接收 Gitea 令牌。',
1101
1171
  'claude-code': 'Claude Code 使用同一份临时代码快照,沿用这台 DSH 的本机权限设置;请确认该权限适合由获准成员请求审查。',
1102
1172
  }
1103
- const executionInfo = reviewers.length > 1
1104
- ? crossReviewAlways
1105
- ? `每次任务请求都会并行启动 ${reviewers.map(reviewerLabel).join('、')},分别返回审查结果,无需请求人额外提出交叉 review。${reviewers.map((item) => executionDetails[item]).join(' ')}`
1106
- : `普通请求默认由 ${reviewerLabel(runner)} 执行;请求人明确说“交叉 review”时,会并行启动 ${reviewers.map(reviewerLabel).join('、')},分别返回审查结果。${reviewers.map((item) => executionDetails[item]).join(' ')}`
1107
- : executionDetails[reviewers[0]]
1173
+ const executionInfo = `${reviewModeSummary}。${reviewers.map((item) => executionDetails[item]).join(' ')}`
1174
+ const triggerSummary = `消息以“${triggerLines.join('”或“')}”开头时触发。`
1108
1175
  const actionDescription = editingCardId
1109
- ? `更新“${title}”后,${requesterSummary}可在${scope}按任务描述中的规则发起请求。旧待确认请求将失效,已提交任务继续使用提交时的配置。结果${resultDestination}。到期时间保持为 ${new Date(editingExpiresAt * 1000).toLocaleString('zh-CN')}。`
1110
- : `开放“${title}”后,${requesterSummary}可在${scope}按任务描述中的规则发起请求,有效期 ${days} 天;结果${resultDestination}。`
1176
+ ? `更新“${title}”后,${requesterSummary}可在${scope}按任务描述中的规则发起请求。${triggerSummary}旧待确认请求将失效,已提交任务继续使用提交时的配置。结果${resultDestination}。到期时间保持为 ${new Date(editingExpiresAt * 1000).toLocaleString('zh-CN')}。`
1177
+ : `开放“${title}”后,${requesterSummary}可在${scope}按任务描述中的规则发起请求。${triggerSummary}有效期 ${days} 天;结果${resultDestination}。`
1111
1178
  if (!window.confirm(`${actionDescription}${executionInfo}请确认。`)) return
1112
1179
  setBusy(true)
1113
1180
  try {
@@ -1150,17 +1217,18 @@ window.__ModuleLoader__.load({
1150
1217
  }
1151
1218
  recognizedBaselineRef.current = null
1152
1219
  recognizedDescriptionRef.current = null
1220
+ triggerPhrasesEditedRef.current = false
1153
1221
  setRuleDescription(''); setTriggerPhrases('')
1154
1222
  setTitle(''); setIntent(''); setTaskPrompt(''); setParameters([]); setRunner('dsh'); setReviewers(['dsh'])
1155
1223
  setCrossReviewAlways(false)
1156
- setCrossReviewOpen(false)
1224
+ setMultiAgentMode(false)
1157
1225
  setEditingCardId(null); setEditingExpiresAt(null)
1158
1226
  await load()
1159
1227
  setCreateOpen(false)
1160
1228
  setMessage(editingCardId
1161
1229
  ? '能力配置已更新。新请求将使用新配置,已提交任务保留原配置。'
1162
1230
  : '任务已开放。机器人会按已确认的描述判断请求;缺少必要信息时会向请求人追问。')
1163
- } catch (error) { setMessage(error.message) }
1231
+ } catch (error) { reportFormError('ant-open-confirm', error.message) }
1164
1232
  finally { setBusy(false) }
1165
1233
  }
1166
1234
  const revokeCard = async (card) => {
@@ -1177,7 +1245,7 @@ window.__ModuleLoader__.load({
1177
1245
  }
1178
1246
  const copyCardToDraft = (card) => {
1179
1247
  setEditingCardId(null); setEditingExpiresAt(null)
1180
- setCrossReviewOpen(false)
1248
+ setFormError(null); triggerPhrasesEditedRef.current = false
1181
1249
  const hasTriggerRule = Array.isArray(card.trigger_phrases) && card.trigger_phrases.length > 0
1182
1250
  const copiedDescription = card.rule_description || card.intent || ''
1183
1251
  recognizedDescriptionRef.current = hasTriggerRule ? copiedDescription.trim() || null : null
@@ -1198,7 +1266,9 @@ window.__ModuleLoader__.load({
1198
1266
  setIntent(taskDraft.intent)
1199
1267
  setTaskPrompt(taskDraft.task_prompt)
1200
1268
  setRunner(['dsh', 'codex', 'claude-code'].includes(card.runner) ? card.runner : 'dsh')
1201
- setReviewers(Array.isArray(card.reviewers) && card.reviewers.length ? card.reviewers : [card.runner || 'dsh'])
1269
+ const copiedReviewers = Array.isArray(card.reviewers) && card.reviewers.length ? card.reviewers : [card.runner || 'dsh']
1270
+ setReviewers(copiedReviewers)
1271
+ setMultiAgentMode(copiedReviewers.length > 1)
1202
1272
  setCrossReviewAlways(card.cross_review_always === true)
1203
1273
  setParameters(taskDraft.parameters)
1204
1274
  setConversationKind(card.conversation_kind === 'group' ? 'group' : 'dm')
@@ -1353,6 +1423,8 @@ window.__ModuleLoader__.load({
1353
1423
  ? h('span', { style: { ...mutedTextStyle, overflowWrap: 'anywhere' } }, card.rule_description)
1354
1424
  : h('span', { role: 'status', style: { ...mutedTextStyle, color: 'var(--dsw-alias-state-warning-primary, var(--dsw-alias-label-secondary))' } },
1355
1425
  '旧规则没有明确触发方式;新版插件不会按它启动任务。升级 ANT 服务端后需重新描述并开放。'),
1426
+ Array.isArray(card.trigger_phrases) && card.trigger_phrases.length
1427
+ ? h('span', { style: { overflowWrap: 'anywhere' } }, '触发短语:' + card.trigger_phrases.join('、')) : null,
1356
1428
  h('details', null,
1357
1429
  h('summary', { style: { cursor: 'pointer', color: 'var(--dsw-alias-link)', fontSize: 13 } }, '查看规则说明'),
1358
1430
  h('div', { style: { display: 'grid', gap: 4, paddingTop: 6 } },
@@ -1412,6 +1484,7 @@ window.__ModuleLoader__.load({
1412
1484
  h('p', { style: mutedTextStyle }, selectedBot
1413
1485
  ? `当前机器人:${selectedBot.name}${selectedEnrollmentBound ? ' · 已绑定此设备' : ' · 请先绑定到此设备'}`
1414
1486
  : '请先在上方选择机器人并启用协作。'),
1487
+ formError?.field === 'ant-bot-selector' ? h('p', { role: 'alert', style: feedbackNoticeStyle }, formError.text) : null,
1415
1488
  draftNotice ? h('p', { role: 'status', style: { ...mutedTextStyle,
1416
1489
  color: 'var(--dsw-alias-state-warning-primary, var(--dsw-alias-label-secondary))' } }, draftNotice) : null,
1417
1490
  selectedEnrollmentBound ? h('p', { style: mutedTextStyle },
@@ -1420,29 +1493,68 @@ window.__ModuleLoader__.load({
1420
1493
  '这是尚未绑定到设备的机器人。请在要继续使用它的电脑重新启用;Gitea 地址和配置记录会保留。若旧设备访问曾被撤销,原任务权限不会恢复,需要重新开放。') : null,
1421
1494
  selectedBot && conversationKind === 'group' && !selectedBot.history_available ? h('p', { role: 'status', style: mutedTextStyle },
1422
1495
  '这个机器人当前不能读取平台群历史;群聊只能参考实际送达的消息和本机近期记录。请检查 DSH-IM 权限。') : null,
1423
- h('label', null, '描述规则', h('textarea', { required: true, maxLength: 4000, rows: 4, value: ruleDescription,
1424
- onChange: (event) => { setRuleDescription(event.target.value); setRecognitionMessage('') },
1425
- placeholder: '用日常的话说明何时执行、要参考哪些资料和结果怎么写。例如:群里有人以“审查 PR”开头并贴出完整 PR 链接时,检查代码差异,列出可定位的问题,不修改代码。', style: inputStyle })),
1426
- h('p', { style: mutedTextStyle }, '描述只写任务何时执行和怎么处理;执行方由下方选项决定,未触发消息是否普通聊天由上方聊天开关决定。'),
1496
+ h('section', { id: 'ant-task-context', role: 'region', 'aria-label': '当前任务内容',
1497
+ style: { display: 'grid', gap: 8, padding: 12, borderLeft: '4px solid var(--dsw-alias-state-business-primary)',
1498
+ backgroundColor: 'var(--dsw-alias-bg-layer-1)', position: 'sticky', top: 0, zIndex: 2 } },
1499
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 8, flexWrap: 'wrap' } },
1500
+ h('strong', { style: { fontSize: 17 } }, title || (ruleSummaryReady ? '识别后的任务' : '当前任务')),
1501
+ h('span', { style: ruleDescriptionNeedsRecognition ? { ...mutedTextStyle,
1502
+ color: 'var(--dsw-alias-state-warning-primary, var(--dsw-alias-label-secondary))' } : mutedTextStyle },
1503
+ ruleDescriptionNeedsRecognition ? '描述已修改 · 需重新识别'
1504
+ : ruleSummaryReady ? '草稿 · 尚未开放' : '填写描述并识别')),
1505
+ h('p', { style: { ...mutedTextStyle, overflowWrap: 'anywhere', display: '-webkit-box',
1506
+ WebkitBoxOrient: 'vertical', WebkitLineClamp: 2, overflow: 'hidden' } }, ruleSummaryNarrative)),
1507
+ h('div', { style: { display: 'grid', gap: 4, padding: '0 4px' } },
1508
+ h('strong', null, '识别详情'),
1509
+ h('p', { style: { ...mutedTextStyle, overflowWrap: 'anywhere' } }, taskPrompt.trim() || '执行要求会在识别后显示。'),
1510
+ h('p', { style: { ...mutedTextStyle, overflowWrap: 'anywhere' } }, recognizedInputsSummary)),
1511
+ h('section', { style: { display: 'grid', gap: 6, padding: 10, borderRadius: 8,
1512
+ border: '1px solid var(--dsw-alias-border-l2)', backgroundColor: 'var(--dsw-alias-bg-layer-1)' } },
1513
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 8, flexWrap: 'wrap' } },
1514
+ h('strong', null, '触发短语(必填)'), h('span', { style: mutedTextStyle }, '模型建议 · 可修改')),
1515
+ ...triggerPhraseEntries.map((phrase, index) => h('div', {
1516
+ key: 'trigger-' + index, style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' } },
1517
+ h('input', { id: index === 0 ? 'ant-trigger-phrases' : undefined,
1518
+ type: 'text', 'aria-label': '触发短语 ' + (index + 1), value: phrase,
1519
+ 'aria-invalid': formError?.field === 'ant-trigger-phrases' ? 'true' : undefined,
1520
+ maxLength: 120, placeholder: '填写触发短语', style: { ...inputStyle, flex: '1 1 220px' },
1521
+ onChange: (event) => setTriggerPhraseAt(index, event.target.value) }),
1522
+ triggerPhraseEntries.length > 1 ? h('button', { type: 'button', disabled: busy,
1523
+ onClick: () => removeTriggerPhrase(index), style: secondaryButtonStyle }, '移除') : null)),
1524
+ h('button', { type: 'button', disabled: busy || triggerPhraseEntries.length >= 8,
1525
+ onClick: addTriggerPhrase, style: { ...secondaryButtonStyle, width: 'fit-content' } }, '添加触发短语'),
1526
+ formError?.field === 'ant-trigger-phrases' ? h('p', { role: 'alert', style: feedbackNoticeStyle }, formError.text) : null,
1527
+ h('p', { style: mutedTextStyle }, '只有消息以这里的短语开头才会触发任务。模型会根据描述给出建议,也可以直接调整。')),
1528
+ h('div', { style: { padding: 10, borderRadius: 8, backgroundColor: 'var(--dsw-alias-bg-layer-2)' } },
1529
+ h('strong', null, '描述提示:'),
1530
+ h('span', null, '写清触发消息以什么开头、要做什么、需要哪些资料和结果怎样返回。识别只整理草稿,开放前请核对。')),
1531
+ h('label', { htmlFor: 'ant-rule-description' }, '描述规则', h('textarea', {
1532
+ id: 'ant-rule-description', required: true, maxLength: 4000, rows: 4, value: ruleDescription,
1533
+ 'aria-invalid': formError?.field === 'ant-rule-description' ? 'true' : undefined,
1534
+ onChange: (event) => { setRuleDescription(event.target.value); setRecognitionMessage(''); clearFormError('ant-rule-description') },
1535
+ placeholder: '用日常的话说明什么时候触发、任务要做什么、需要哪些资料以及结果怎么返回。', style: inputStyle })),
1536
+ formError?.field === 'ant-rule-description' ? h('p', { role: 'alert', style: feedbackNoticeStyle }, formError.text) : null,
1427
1537
  h('div', { style: { display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' } },
1428
1538
  h('button', { type: 'button', disabled: recognitionBusy || busy || !ruleDescription.trim(),
1429
1539
  onClick: () => void identifyRule(), style: secondaryButtonStyle }, recognitionBusy ? '正在识别…' : ruleSummaryReady ? '重新识别' : '识别规则'),
1430
1540
  h('span', { style: mutedTextStyle }, '每次点击调用一次团队模型并计入用量;只整理草稿,不会开放任务。改完描述后可重新识别。')),
1431
1541
  recognitionMessage ? h('p', { role: 'status', 'aria-live': 'polite', style: feedbackNoticeStyle }, recognitionMessage) : null,
1432
- h('section', { role: 'region', 'aria-label': '识别结果摘要', style: { ...panelStyle, padding: 10, gap: 7 } },
1433
- h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } },
1434
- h('strong', null, ruleSummaryReady ? (title || '识别结果') : '识别结果预览'),
1435
- h('span', { style: mutedTextStyle }, ruleSummaryReady ? '草稿 · 尚未开放' : '填写描述后可识别')),
1436
- h('p', { style: { ...mutedTextStyle, overflowWrap: 'anywhere' } }, ruleSummaryNarrative),
1437
- h('p', { style: mutedTextStyle }, ruleSummaryReady
1438
- ? '草稿未开放。检查这段理解;有误就改上方描述并重新识别。'
1439
- : '填写描述后识别;识别只生成草稿,不会开放任务。')),
1440
1542
  h('section', { style: { ...panelStyle, padding: 10, gap: 9, display: 'grid',
1441
1543
  gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 280px), 1fr))' } },
1442
- h('strong', { style: { gridColumn: '1 / -1' } }, '通用授权'),
1544
+ h('strong', { style: { gridColumn: '1 / -1' } }, 'Agent 运行方式'),
1443
1545
  h('p', { style: { ...mutedTextStyle, gridColumn: '1 / -1' } },
1444
- '上方描述规则任务要做什么;这里限定谁能请求、在哪里请求、由谁执行和授权多久。'),
1445
- h('label', null, '执行方', h('select', { value: runner, onChange: (event) => selectRunner(event.target.value),
1546
+ '选择一位 Agent 处理任务,或选择多位 Agent 按需/每次并行审查。'),
1547
+ h('div', { id: 'ant-agent-mode', role: 'group', 'aria-label': 'Agent 运行方式', tabIndex: -1,
1548
+ style: { display: 'flex', gap: 8, flexWrap: 'wrap' } },
1549
+ h('button', { type: 'button', 'aria-pressed': !multiAgentMode,
1550
+ onClick: () => selectExecutionMode('single'),
1551
+ style: !multiAgentMode ? primaryButtonStyle : secondaryButtonStyle }, '单 Agent'),
1552
+ h('button', { type: 'button', 'aria-pressed': multiAgentMode,
1553
+ onClick: () => selectExecutionMode('multi'),
1554
+ style: multiAgentMode ? primaryButtonStyle : secondaryButtonStyle }, '多 Agent 交叉审查')),
1555
+ formError?.field === 'ant-agent-mode' ? h('p', { role: 'alert', style: feedbackNoticeStyle }, formError.text) : null,
1556
+ multiAgentMode && crossReviewAlways ? null : h('label', null,
1557
+ multiAgentMode ? '普通请求默认由' : '执行 Agent', h('select', { value: runner, onChange: (event) => selectRunner(event.target.value),
1446
1558
  style: inputStyle },
1447
1559
  h('option', { value: 'dsh', disabled: !dshAvailable && runner !== 'dsh' }, 'DSH · 当前团队模型'),
1448
1560
  h('option', { value: 'codex', disabled: (!codexAvailable || !hasPrParameter) && runner !== 'codex' }, 'Codex · PR 快照只读'),
@@ -1451,11 +1563,15 @@ window.__ModuleLoader__.load({
1451
1563
  ? 'Codex 使用本机登录,仅读取已核验的 PR 快照。'
1452
1564
  : runner === 'claude-code' ? 'Claude Code 沿用这台 DSH 的权限设置。'
1453
1565
  : 'DSH 使用获准团队模型处理会话中可见或已提供的内容。')),
1454
- h('details', { id: 'ant-cross-review-details', open: crossReviewOpen,
1455
- onToggle: (event) => setCrossReviewOpen(event.currentTarget.open),
1456
- style: { gridColumn: '1 / -1', border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, padding: 10 } },
1457
- h('summary', { style: { cursor: 'pointer', fontWeight: 600 } }, crossReviewHeading),
1458
- h('p', { style: mutedTextStyle }, '多个 Agent 可按需要设置为每次并行,或由请求人逐次提出交叉审查。'),
1566
+ !multiAgentMode && !dshAvailable ? h('p', { role: 'status', style: mutedTextStyle }, executorReason('dsh')) : null,
1567
+ !multiAgentMode && (!codexAvailable || !hasPrParameter) ? h('p', { role: 'status', style: mutedTextStyle },
1568
+ `Codex 暂不可选:${executorReason('codex')}`) : null,
1569
+ !multiAgentMode && (!claudeCodeAvailable || !hasPrParameter) ? h('p', { role: 'status', style: mutedTextStyle },
1570
+ `Claude Code 暂不可选:${executorReason('claude-code')}`) : null,
1571
+ h('div', { id: 'ant-reviewer-list', role: 'group', 'aria-label': '交叉审查 Agent', tabIndex: -1,
1572
+ style: { gridColumn: '1 / -1', display: multiAgentMode ? 'grid' : 'none', gap: 8 } },
1573
+ h('strong', null, '参与交叉审查的 Agent(至少选择两位)'),
1574
+ h('p', { style: mutedTextStyle }, '请求人需要在触发短语开头的任务消息中明确提出“交叉 review”,按需策略才会并行运行。'),
1459
1575
  h('div', { style: { display: 'flex', gap: 14, flexWrap: 'wrap' } },
1460
1576
  h('label', null, h('input', { type: 'checkbox', 'aria-label': '审查 Agent DSH', checked: reviewers.includes('dsh'),
1461
1577
  disabled: !dshAvailable && !reviewers.includes('dsh'),
@@ -1466,6 +1582,15 @@ window.__ModuleLoader__.load({
1466
1582
  h('label', null, h('input', { type: 'checkbox', 'aria-label': '审查 Agent Claude Code', checked: reviewers.includes('claude-code'),
1467
1583
  disabled: (!claudeCodeAvailable || !hasPrParameter) && !reviewers.includes('claude-code'),
1468
1584
  onChange: (event) => changeReviewer('claude-code', event.target.checked) }), ' Claude Code')),
1585
+ !dshAvailable ? h('p', { role: 'status', style: mutedTextStyle }, executorReason('dsh')) : null,
1586
+ !codexAvailable || !hasPrParameter ? h('p', { role: 'status', style: mutedTextStyle },
1587
+ `Codex 暂不可选:${executorReason('codex')}`) : null,
1588
+ !claudeCodeAvailable || !hasPrParameter ? h('p', { role: 'status', style: mutedTextStyle },
1589
+ `Claude Code 暂不可选:${executorReason('claude-code')}`) : null,
1590
+ multiAgentMode && reviewers.length < 2 && formError?.field !== 'ant-reviewer-list'
1591
+ ? h('p', { role: 'alert', style: feedbackNoticeStyle },
1592
+ '交叉审查至少选择两位可用 Agent。') : null,
1593
+ formError?.field === 'ant-reviewer-list' ? h('p', { role: 'alert', style: feedbackNoticeStyle }, formError.text) : null,
1469
1594
  reviewers.length > 1 && defaults?.cross_review_policy_version === 1
1470
1595
  ? h('fieldset', { style: { margin: 0, padding: '8px 10px', border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8 } },
1471
1596
  h('legend', { style: mutedTextStyle }, '运行策略'),
@@ -1479,45 +1604,54 @@ window.__ModuleLoader__.load({
1479
1604
  h('span', null, '每次任务都并行运行全部已选 Agent;请求人无需额外提出交叉审查。')))
1480
1605
  : reviewers.length > 1 ? h('p', { style: mutedTextStyle }, '每次并行模式需升级 ANT 服务端;当前仍可由请求人逐次提出交叉审查。') : null,
1481
1606
  !hasPrParameter ? h('p', { style: mutedTextStyle }, '从描述识别出 PR 任务后,才能使用 Codex 或 Claude Code。') : null),
1482
- nativePrUnavailableReason ? h('p', { role: 'status', style: { ...mutedTextStyle, gridColumn: '1 / -1' } },
1483
- `${nativePrUnavailableReason},已禁用 Codex/Claude Code 完整快照选项;可选择 DSH 差异审查。`) : null,
1607
+ h('strong', { style: { gridColumn: '1 / -1', paddingTop: 8, borderTop: '1px solid var(--dsw-alias-border-l2)' } }, '请求范围'),
1484
1608
  h('label', null, '使用场景', h('select', { value: conversationKind,
1485
1609
  onChange: (event) => { setConversationKind(event.target.value); setGroupId('') }, style: inputStyle },
1486
1610
  h('option', { value: 'dm' }, '私聊'), h('option', { value: 'group' }, '指定群聊'))),
1487
- conversationKind === 'group' ? h('div', { style: { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) auto',
1488
- gap: 8, alignItems: 'end' } },
1489
- h('label', null, '群聊', h('select', { required: true, value: groupId,
1490
- onChange: (event) => setGroupId(event.target.value), style: inputStyle },
1611
+ conversationKind === 'group' ? h('div', { style: { gridColumn: '1 / -1', display: 'grid',
1612
+ gridTemplateColumns: 'minmax(0, 1fr) auto', gap: 8, alignItems: 'end' } },
1613
+ h('label', null, '目标群', h('select', { id: 'ant-group-selector', required: true, value: groupId,
1614
+ onChange: (event) => { setGroupId(event.target.value); clearFormError('ant-group-selector') }, style: inputStyle },
1491
1615
  h('option', { value: '' }, '让群成员先 @ 机器人,再刷新列表'),
1492
1616
  ...availableGroups.map((group) => h('option', { key: group.group_id, value: group.group_id, title: group.group_code },
1493
1617
  groupSummary(group))))),
1494
1618
  h('button', { type: 'button', disabled: busy,
1495
- onClick: () => void load().catch((error) => setMessage(error.message)), style: secondaryButtonStyle }, '刷新群列表')) : null,
1619
+ onClick: () => void load().catch((error) => reportFormError('ant-group-selector', error.message)), style: secondaryButtonStyle }, '刷新群列表')) : null,
1620
+ conversationKind === 'group' && !groupId ? h('p', { role: 'status', style: mutedTextStyle },
1621
+ '请选择目标群;列表为空时,让群成员先 @ 机器人,再刷新群列表。') : null,
1622
+ formError?.field === 'ant-group-selector' ? h('p', { role: 'alert', style: feedbackNoticeStyle }, formError.text) : null,
1496
1623
  h('label', null, '谁可以请求', h('select', { value: senderScope,
1497
- onChange: (event) => setSenderScope(event.target.value), style: inputStyle },
1624
+ onChange: (event) => { setSenderScope(event.target.value); clearFormError('ant-sender-ids') }, style: inputStyle },
1498
1625
  h('option', { value: 'any' }, conversationKind === 'group' ? '此群任何成员' : '任何能私聊此机器人者'),
1499
1626
  h('option', { value: 'allowlist' }, '指定 IM 用户 ID'))),
1500
- senderScope === 'allowlist' ? h('label', null, '可请求成员 ID', h('input', { 'aria-label': '可请求的 IM 用户 ID',
1501
- required: true, value: senderIds, onChange: (event) => setSenderIds(event.target.value),
1627
+ senderScope === 'allowlist' ? h('label', null, '可请求成员 ID', h('input', { id: 'ant-sender-ids', 'aria-label': '可请求的 IM 用户 ID',
1628
+ required: true, value: senderIds, onChange: (event) => { setSenderIds(event.target.value); clearFormError('ant-sender-ids') },
1502
1629
  placeholder: '多个 ID 用逗号分隔', style: inputStyle })) : null,
1630
+ senderScope === 'allowlist' && !senderIds.trim() ? h('p', { role: 'status', style: mutedTextStyle },
1631
+ '填写至少一个可请求成员的 IM 用户 ID。') : null,
1632
+ formError?.field === 'ant-sender-ids' ? h('p', { role: 'alert', style: feedbackNoticeStyle }, formError.text) : null,
1503
1633
  editingCardId ? h('p', { style: { ...mutedTextStyle, alignSelf: 'end' } },
1504
1634
  `有效期:${new Date(editingExpiresAt * 1000).toLocaleString('zh-CN')}(编辑时保持不变)`)
1505
1635
  : h('label', null, '有效期', h('select', { value: days, onChange: (event) => setDays(Number(event.target.value)), style: inputStyle },
1506
- h('option', { value: 1 }, '1 天'), h('option', { value: 7 }, '7 天'), h('option', { value: 30 }, '30 天'))),
1636
+ h('option', { value: 1 }, '1 天'), h('option', { value: 7 }, '7 天'),
1637
+ h('option', { value: 30 }, '30 天'), h('option', { value: 90 }, '90 天'))),
1507
1638
  nativeReviewerSelected ? h('p', { style: { ...mutedTextStyle, gridColumn: '1 / -1' } },
1508
1639
  '本机 PR 审查需要 Gitea 只读权限 read:repository 和 read:user;ANT 不会把令牌交给执行方。') : null),
1509
1640
  ),
1510
- h('aside', { id: 'ant-authorization-review', role: 'region', 'aria-label': '开放前复核', tabIndex: -1,
1641
+ h('aside', { id: 'ant-open-confirm', role: 'region', 'aria-label': '开放前复核', tabIndex: -1,
1511
1642
  style: { ...settingsPanelStyle, padding: 14, gap: 10, alignSelf: 'stretch' } },
1512
1643
  h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8 } },
1513
- h('h4', { style: sectionTitleStyle }, '开放前看一眼'),
1644
+ h('h4', { style: sectionTitleStyle }, '开放前确认'),
1514
1645
  h('span', { style: { ...mutedTextStyle, padding: '3px 8px', borderRadius: 999,
1515
1646
  backgroundColor: 'var(--dsw-alias-bg-layer-2)' } }, '草稿 · 未开放')),
1647
+ h('p', { style: { ...mutedTextStyle, overflowWrap: 'anywhere' } },
1648
+ (title || '未命名任务') + ' · 触发短语“' + (ruleTriggerSummary.join('”或“') || '尚未填写') + '”'),
1516
1649
  h('p', { style: mutedTextStyle }, `这项任务将在${conversationKind === 'group'
1517
- ? `群“${selectedGroupSummary || '尚未选择'}”` : '私聊'}开放给${requesterSummary},由${executorSummary}执行;`
1650
+ ? `群“${selectedGroupSummary || '尚未选择'}”` : '私聊'}开放给${requesterSummary};${reviewModeSummary};`
1518
1651
  + `有效期 ${editingCardId ? new Date(editingExpiresAt * 1000).toLocaleString('zh-CN') : `${days} 天`},${resultRouteSummary}。`),
1519
1652
  cardBlockReason ? h('p', { role: 'status', style: { ...mutedTextStyle,
1520
1653
  color: 'var(--dsw-alias-state-warning-primary, var(--dsw-alias-label-secondary))' } }, cardBlockReason) : null,
1654
+ formError?.field === 'ant-open-confirm' ? h('p', { role: 'alert', style: feedbackNoticeStyle }, formError.text) : null,
1521
1655
  h('button', { type: 'submit', form: 'ant-capability-create-form', disabled: busy || Boolean(cardBlockReason),
1522
1656
  style: { ...primaryButtonStyle, width: '100%' } }, editingCardId ? '保存能力修改' : '确认并开放能力'))))),
1523
1657
  h('div', { role: 'region', 'aria-label': '本机处理设置', style: { display: activeTab === 'local' ? 'grid' : 'none', gap: 12 } },
package/index.js CHANGED
@@ -530,14 +530,18 @@ async function fetchDelegationExecutors(request, ctx) {
530
530
  const nativePrSnapshotSupported = process.platform !== 'win32'
531
531
  const nativePrUnavailableReason = nativePrSnapshotSupported ? null
532
532
  : '当前 Windows 环境无法限制 Git 仓库缓存容量'
533
+ const codexReadonlyReason = nativePrUnavailableReason
534
+ || (codexCliAvailable ? null : '这台 DSH 没有可用的固定只读 Codex CLI,请检查安装和 PATH')
535
+ const claudeCodeReason = nativePrUnavailableReason
536
+ || (available.has('claude-code') ? null : '这台 DSH 尚未注册 Claude Code 子代理,请先在 DSH 中安装并启用它')
533
537
  return jsonResponse(200, [
534
538
  { id: 'dsh', title: 'DSH', available: Boolean(optionalHostService(ctx, 'sessionController')
535
539
  && optionalHostService(ctx, 'agents')) },
536
540
  { id: 'codex', title: 'Codex', available: available.has('codex') },
537
541
  { id: 'codex-readonly', title: 'Codex 专用只读差异审查', available: codexCliAvailable && nativePrSnapshotSupported,
538
- ...(nativePrUnavailableReason ? { reason: nativePrUnavailableReason } : {}) },
542
+ ...(codexReadonlyReason ? { reason: codexReadonlyReason } : {}) },
539
543
  { id: 'claude-code', title: 'Claude Code', available: available.has('claude-code') && nativePrSnapshotSupported,
540
- ...(nativePrUnavailableReason ? { reason: nativePrUnavailableReason } : {}) },
544
+ ...(claudeCodeReason ? { reason: claudeCodeReason } : {}) },
541
545
  { id: 'acp', title: 'DSH ACP', available: available.has('acp') },
542
546
  { id: 'dsh-sdk', title: 'DSH SDK 子代理', available: available.has('dsh-sdk') },
543
547
  ])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-wendao/ant",
3
- "version": "0.5.8",
3
+ "version": "0.5.9",
4
4
  "description": "ANT (AI-native Teams) connects a DeepSeek Harness Desktop profile to team identity and approved models.",
5
5
  "type": "module",
6
6
  "scripts": {