@ai-wendao/ant 0.4.12 → 0.4.14

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
@@ -5,6 +5,12 @@ window.__ModuleLoader__.load({
5
5
  var exports = module.exports
6
6
  const { createElement, useEffect, useRef, useState } = require('react')
7
7
  const h = createElement
8
+ const compactOpenaiProviderName = (name) => {
9
+ const value = typeof name === 'string' ? name.trim() : ''
10
+ if (!value) return value
11
+ const suffix = /\s*(?:[·•::|—–-]\s*)?(?:Responses|Chat Completions)$/iu
12
+ return value.replace(suffix, '').trim() || 'OpenAI'
13
+ }
8
14
  const SERVICE_URL_STORAGE_KEY = 'ant-team-service-url'
9
15
  const PROVIDER_ROUTES_STORAGE_KEY = 'ant-team-provider-routes'
10
16
  const DEVICE_ID_STORAGE_KEY = 'ant-team-device-id'
@@ -14,12 +20,13 @@ window.__ModuleLoader__.load({
14
20
  const PLUGIN_INSTALL_RECORD_PREFIX = 'ant-reviewed-plugin-install:'
15
21
  const ANT_SETTINGS_TAB_STORAGE_KEY = 'ant-settings-tab'
16
22
  const ANT_SETTINGS_TABS = [
17
- ['connection', '团队连接'], ['models', '团队模型'], ['bot-chat', '机器人与聊天'],
18
- ['capability', '能力授权'], ['local', '本机与任务'], ['updates', '关于'],
23
+ ['connection', '团队连接'], ['models', '团队模型'], ['collaboration', '协作'],
24
+ ['local', '本机与任务'], ['updates', '关于'],
19
25
  ]
20
26
  const loadAntSettingsTab = () => {
21
27
  try {
22
28
  const saved = localStorage.getItem(ANT_SETTINGS_TAB_STORAGE_KEY)
29
+ if (saved === 'bot-chat' || saved === 'capability') return 'collaboration'
23
30
  return ANT_SETTINGS_TABS.some(([id]) => id === saved) ? saved : 'connection'
24
31
  } catch { return 'connection' }
25
32
  }
@@ -399,7 +406,7 @@ window.__ModuleLoader__.load({
399
406
  h('span', null, bindingSummary),
400
407
  !bindingReadyForSummary ? h('button', { type: 'button', onClick: () => selectSettingsTab('connection'),
401
408
  style: secondaryButtonStyle }, binding.state === 'error' ? '检查连接' : '查看连接设置')
402
- : activeTab === 'connection' ? h('button', { type: 'button', onClick: () => selectSettingsTab('bot-chat'),
409
+ : activeTab === 'connection' ? h('button', { type: 'button', onClick: () => selectSettingsTab('collaboration'),
403
410
  style: secondaryButtonStyle }, '设置或管理机器人') : null),
404
411
  h('div', { style: { position: 'sticky', top: 0, zIndex: 8, display: 'grid', gap: 8, padding: '6px 0',
405
412
  backgroundColor: 'var(--dsw-alias-bg-base)' } },
@@ -466,17 +473,20 @@ window.__ModuleLoader__.load({
466
473
  const [repositoryCacheLimitGib, setRepositoryCacheLimitGib] = useState(10)
467
474
  const [botId, setBotId] = useState('')
468
475
  const [trustedOrigin, setTrustedOrigin] = useState('')
469
- const [bindingCode, setBindingCode] = useState('')
470
476
  const [conversationKind, setConversationKind] = useState('dm')
471
477
  const [groupId, setGroupId] = useState('')
472
- 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)
473
483
  const [title, setTitle] = useState('')
474
484
  const [intent, setIntent] = useState('')
475
- const [examples, setExamples] = useState('')
476
485
  const [taskPrompt, setTaskPrompt] = useState('')
477
486
  const [runner, setRunner] = useState('dsh')
478
487
  const [reviewers, setReviewers] = useState(['dsh'])
479
488
  const [parameters, setParameters] = useState([])
489
+ const [crossReviewOpen, setCrossReviewOpen] = useState(false)
480
490
  const [giteaToken, setGiteaToken] = useState('')
481
491
  const [tokenConfigured, setTokenConfigured] = useState(false)
482
492
  const [senderScope, setSenderScope] = useState('any')
@@ -544,23 +554,41 @@ window.__ModuleLoader__.load({
544
554
  : !selectedEnrollmentBound ? '请先把机器人绑定到这台 DSH,再修改私聊设置。'
545
555
  : !dmChatSettingAvailable ? '当前团队服务端尚未提供私聊开关,请先升级服务端。'
546
556
  : dmChatEnabled
547
- ? '允许此机器人接收私聊;任务仍按能力授权执行。'
548
- : '新私聊消息不会交给 DSH;所有者身份绑定仍可用,已提交任务仍会在原私聊回复。已开放能力不撤销,重新开启后未过期能力恢复触发。'
557
+ ? '允许此机器人接收私聊;任务仍按已开放的规则执行。'
558
+ : '新私聊消息不会交给 DSH;已提交任务仍会在原私聊回复。已开放能力不撤销,重新开启后未过期能力恢复触发。'
549
559
  const availableGroups = groups.filter((group) => group.bot_fingerprint === selectedBot?.fingerprint)
550
- const selectedChatGroup = availableGroups.find((group) => group.group_id === chatGroupId)
551
560
  const groupChatLoadFailed = loadFailures.includes('groups')
552
561
  const activeCards = cards.filter((card) => !card.revoked_at && card.expires_at * 1000 > Date.now())
553
- const selectedGroupHasTasks = Boolean(selectedBot && chatGroupId && (
554
- activeCards.some((card) => card.bot_fingerprint === selectedBot.fingerprint
555
- && card.conversation_kind === 'group' && card.conversation_id === chatGroupId)))
556
562
  const connectedBots = bots.filter((bot) => bot.available && bot.connected)
557
563
  const authorizationLoadError = loadFailures.includes('cards')
558
564
  const enrollmentLoadError = loadFailures.includes('enrolled-bots')
559
565
  const enrolledCount = enrolled.filter((item) => item.device_bound_to_current
560
566
  && connectedBots.some((bot) => bot.fingerprint === item.bot_fingerprint)).length
561
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} 项能力已开放。可以私聊自测。`
562
586
  const nativeReviewerSelected = reviewers.some((item) => ['codex', 'claude-code'].includes(item))
563
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
564
592
  const codexAvailable = executors.some((item) => item.id === 'codex-readonly' && item.available)
565
593
  const claudeCodeAvailable = executors.some((item) => item.id === 'claude-code' && item.available)
566
594
  const dshAvailable = executors.some((item) => item.id === 'dsh' && item.available)
@@ -576,23 +604,57 @@ window.__ModuleLoader__.load({
576
604
  }
577
605
  const requesterSummary = senderScope === 'allowlist'
578
606
  ? `仅指定成员:${senderIds.trim() || '尚未填写成员 ID'}`
579
- : conversationKind === 'group' ? `群 ${groupId || '尚未选择'} 的所有成员` : '任何能私聊此机器人的人'
607
+ : conversationKind === 'group' ? '群内所有成员' : '任何能私聊此机器人的人'
580
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
+ : '识别后会用一段摘要呈现系统理解的任务。'
581
637
  const executorSummary = runner === 'codex' ? '本机 Codex · PR 快照'
582
638
  : runner === 'claude-code' ? '本机 Claude Code · PR 快照' : 'DSH · 当前授权模型'
583
- const resourceSummary = nativeReviewerSelected ? '指定仓库的已核验 PR 快照'
584
- : '当前会话中 DSH 实际可见的内容'
639
+ const crossReviewSummary = reviewers.filter((item) => item !== runner).map(reviewerLabel).join('、')
585
640
  const cardBlockReason = enrollmentLoadError ? '暂时无法核对机器人是否已启用协作,请刷新状态后再试。'
586
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() ? '描述里还没有明确写出何时触发。请补充触发方式并重新识别。'
587
649
  : conversationKind === 'group' && loadFailures.includes('groups') ? '暂时无法核对目标群,请刷新状态后再试。'
588
- : conversationKind === 'group' && !groupId ? '请在“参数与授权范围”中选择目标群;若列表为空,让群成员先 @机器人,再刷新群列表。'
589
- : senderScope === 'allowlist' && !senderIds.trim() ? '请在“参数与授权范围”中填写可请求成员的 IM ID。'
650
+ : conversationKind === 'group' && !groupId ? '请在“请求范围”中选择目标群;若列表为空,让群成员先 @机器人,再刷新群列表。'
651
+ : senderScope === 'allowlist' && !senderIds.trim() ? '请填写可请求成员的 IM ID。'
590
652
  : parameters.some((rule) => rule.kind === 'gitea_pr') && !tokenConfigured ? '请先在“本机与任务”中保存本机 Gitea 只读令牌。'
591
- : parameters.some((rule) => rule.kind === 'gitea_pr') && !selectedEnrollment?.gitea_origin
592
- ? '请先在“机器人与聊天”中登记这个机器人的可信 Gitea 地址。'
653
+ : parameters.some((rule) => rule.kind === 'gitea_pr') && !selectedEnrollment?.gitea_origin
654
+ ? 'PR 来源尚未信任。请到“本机与任务”设置可信 Gitea 来源。'
593
655
  : parameters.some((rule) => rule.kind === 'gitea_pr')
594
656
  && normalizeServiceUrl(trustedOrigin) !== selectedEnrollment?.gitea_origin
595
- ? '请先保存机器人级可信 Gitea 地址;PR 参数会自动使用这个地址。'
657
+ ? '请先在“本机与任务”保存可信 Gitea 来源;任务描述中的 PR 链接会使用这个来源。'
596
658
  : nativeReviewerSelected && (parameters.length !== 1 || parameters[0].kind !== 'gitea_pr')
597
659
  ? 'Codex 和 Claude Code 目前只支持单个 Gitea PR 审查任务。'
598
660
  : nativeReviewerSelected && !parameters[0].required
@@ -607,57 +669,115 @@ window.__ModuleLoader__.load({
607
669
  const loadNames = { bots: '机器人', 'enrolled-bots': '协作启用状态', groups: '群聊列表',
608
670
  cards: '任务授权', settings: '团队默认值', executors: '本机执行器',
609
671
  'local-settings': '本机设置', status: '最近任务' }
610
- const updateParameter = (index, patch) => setParameters((current) => current.map((rule, i) => i === index ? { ...rule, ...patch } : rule))
611
672
  const changeReviewer = (id, enabled) => {
612
673
  const next = enabled ? [...reviewers, id] : reviewers.filter((item) => item !== id)
613
674
  if (!next.length) { setMessage('至少保留一位审查 Agent。'); return }
614
675
  setReviewers([...new Set(next)])
615
676
  if (!next.includes(runner)) setRunner(next[0])
616
677
  }
617
- 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 = {}) => {
618
685
  setEditingCardId(null); setEditingExpiresAt(null)
619
686
  setDraftNotice('')
620
- setTitle(''); setIntent(''); setExamples(''); setTaskPrompt(''); setRunner('dsh')
621
- setReviewers(['dsh'])
622
- 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 || '')
623
695
  setSenderScope('any'); setSenderIds(''); setDays(7); setCreateOpen(true)
696
+ if (context.botId) setBotId(context.botId)
624
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
+ })
625
709
  const editCard = (card, copy = false) => {
626
710
  setBotId(card.bot_id)
627
711
  setTrustedOrigin(enrolled.find((bot) => bot.bot_id === card.bot_id)?.gitea_origin || '')
628
712
  setEditingCardId(copy ? null : card.id); setEditingExpiresAt(copy ? null : card.expires_at)
629
- 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)
630
724
  if (copy) setDays(7)
631
- setIntent(card.intent); setExamples((card.examples || []).join('\n'))
632
- setTaskPrompt(card.task_prompt); setRunner(card.runner)
725
+ setIntent(taskDraft.intent)
726
+ setTaskPrompt(taskDraft.task_prompt); setRunner(card.runner)
633
727
  setReviewers(Array.isArray(card.reviewers) && card.reviewers.length ? card.reviewers : [card.runner])
634
- setParameters((card.parameters || []).map((rule) => ({
635
- name: rule.name, kind: rule.kind, required: rule.required,
636
- description: rule.description || '', min: rule.min == null ? '' : String(rule.min),
637
- max: rule.max == null ? '' : String(rule.max), choices: (rule.choices || []).join(', '),
638
- gitea_origin: rule.gitea_origin || '', repository: rule.repository || '',
639
- })))
728
+ setCrossReviewOpen(false)
729
+ setParameters(taskDraft.parameters)
640
730
  setConversationKind(card.conversation_kind); setGroupId(card.conversation_id || '')
641
731
  setSenderScope(card.sender_scope); setSenderIds((card.sender_ids || []).join(', '))
642
732
  setCreateOpen(true)
643
733
  }
644
- const preparePrExample = () => {
645
- setEditingCardId(null); setEditingExpiresAt(null)
646
- setTitle('审查 Gitea PR 差异')
647
- setIntent('同事希望查看指定 Gitea PR 的代码差异并获得审查意见。')
648
- setExamples('请帮我审查 123 号 PR\n看看刚才提到的 PR 有什么风险')
649
- setTaskPrompt('结合已核验 PR 的完整代码快照和差异,给出可定位的问题;没有足够上下文时说明局限,不修改代码。')
650
- setParameters([{ name: 'pr_number', kind: 'gitea_pr', required: true,
651
- description: '要审查的 PR 编号', min: '1', max: '', choices: '',
652
- gitea_origin: selectedEnrollment?.gitea_origin || trustedOrigin || '', repository: '' }])
653
- const availableReviewers = [
654
- ...(codexAvailable ? ['codex'] : []),
655
- ...(claudeCodeAvailable ? ['claude-code'] : []),
656
- ]
657
- const selected = availableReviewers.length ? availableReviewers : ['dsh']
658
- setReviewers(selected)
659
- setRunner(selected[0])
660
- 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) }
661
781
  }
662
782
  const saveGiteaToken = async () => {
663
783
  if (!giteaToken.trim()) return
@@ -711,29 +831,12 @@ window.__ModuleLoader__.load({
711
831
  } catch (error) { setMessage(error.message) }
712
832
  finally { setBusy(false) }
713
833
  }
714
- const issueBindingCode = async () => {
715
- if (!selectedBot || !selectedEnrollmentBound) return
716
- setBusy(true)
717
- try {
718
- const response = await fetch('/api/ant-teams/delegation/owner-challenge', {
719
- method: 'POST', headers: { 'content-type': 'application/json' },
720
- body: JSON.stringify({ bot_id: selectedBot.bot_id, bot_fingerprint: selectedBot.fingerprint }),
721
- })
722
- const result = await readLocalJsonResponse(response)
723
- if (!response.ok) throw new Error(result.message || '无法获取身份绑定码。')
724
- setBindingCode(result.code)
725
- setMessage('')
726
- } catch (error) { setMessage(error.message) }
727
- finally { setBusy(false) }
728
- }
729
- const saveGroupChat = async (enabled) => {
730
- if (!selectedBot || !selectedEnrollmentBound || !chatGroupId.trim()) return
731
- if (!enabled && selectedGroupHasTasks) {
732
- setMessage('这个群还有开放能力。请先停止这些能力,再关闭群聊。')
733
- return
734
- }
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)
735
838
  if (!window.confirm(enabled
736
- ? selectedGroupHasTasks
839
+ ? targetHasTasks
737
840
  ? '这个群当前因任务授权可以聊天。要在任务到期后仍允许普通聊天吗?聊天会使用团队模型额度。'
738
841
  : '允许这个群与机器人自然聊天?群成员仍不能执行未开放的能力,聊天会使用团队模型额度。'
739
842
  : '关闭这个群的普通聊天?已有能力不会自动撤销。')) return
@@ -742,7 +845,7 @@ window.__ModuleLoader__.load({
742
845
  const response = await fetch('/api/ant-teams/delegation/groups', {
743
846
  method: 'PUT', headers: { 'content-type': 'application/json' },
744
847
  body: JSON.stringify({ bot_id: selectedBot.bot_id,
745
- bot_fingerprint: selectedBot.fingerprint, group_id: chatGroupId.trim(), enabled }),
848
+ bot_fingerprint: selectedBot.fingerprint, group_id: targetGroupId.trim(), enabled }),
746
849
  })
747
850
  if (!response.ok) {
748
851
  const result = await readLocalJsonResponse(response)
@@ -756,8 +859,8 @@ window.__ModuleLoader__.load({
756
859
  const saveDmChat = async (enabled) => {
757
860
  if (!selectedBot || !selectedEnrollmentBound || !dmChatSettingAvailable) return
758
861
  if (!window.confirm(enabled
759
- ? '开启后,此机器人会在私聊中正常对话;私聊任务仍逐项按能力授权核验。'
760
- : '关闭后,新私聊消息不会进入 DSH,包括普通聊天、任务请求和状态查询;所有者身份绑定命令仍可用。关闭前已提交的任务不会被撤销,仍按原私聊回复。已开放的私聊能力不会删除;重新开启后,未过期的能力会恢复触发。群聊设置不受影响。')) return
862
+ ? '开启后,此机器人会在私聊中正常对话;私聊任务仍逐项按已开放规则核验。'
863
+ : '关闭后,新私聊消息不会进入 DSH,包括普通聊天、任务请求和状态查询。关闭前已提交的任务不会被撤销,仍按原私聊回复。已开放的私聊能力不会删除;重新开启后,未过期的能力会恢复触发。群聊设置不受影响。')) return
761
864
  setBusy(true)
762
865
  try {
763
866
  const response = await fetch('/api/ant-teams/delegation/dm-chat', {
@@ -802,19 +905,33 @@ window.__ModuleLoader__.load({
802
905
  })
803
906
  }
804
907
  const goToSetupStep = (index) => {
805
- if (index === 0) return focusAfterTabChange('bot-chat', selectedBot ? 'ant-bot-selector' : 'ant-chat-entry')
806
- if (index === 1) return focusAfterTabChange('bot-chat', 'ant-chat-scope-panel')
807
- 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
+ }
808
918
  setCreateOpen(true)
809
- focusAfterTabChange('capability', index === 2 ? 'ant-new-task-summary' : 'ant-authorization-review')
919
+ focusAfterTabChange('collaboration', 'ant-authorization-review')
920
+ }
921
+ const collaborationGridStyle = {
922
+ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 300px), 1fr))', gap: 12,
810
923
  }
924
+ const collaborationSettingsGridStyle = {
925
+ ...collaborationGridStyle, gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 420px), 1fr))',
926
+ }
927
+ const collaborationCardStyle = { ...settingsPanelStyle, padding: 12, gap: 10 }
811
928
  const setupStepper = h('section', { 'aria-label': '首次设置引导', style: { display: 'grid', gap: 6 } },
812
929
  h('p', { style: mutedTextStyle }, '首次设置可按顺序完成;你也可以直接切换页面。普通聊天不需要先开放任务。'),
813
930
  h('ol', { 'aria-label': '设置步骤', style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 150px), 1fr))',
814
931
  gap: 8, padding: 0, margin: 0, listStyle: 'none' } },
815
932
  ...[
816
933
  { label: '机器人和设备', detail: !selectedBot ? '选择已连接的机器人' : !botConnected ? '检查 DSH-IM 连接' : selectedEnrollmentBound ? '已绑定到这台 DSH' : '绑定到这台 DSH' },
817
- { label: '聊天入口', detail: !selectedBot || !selectedEnrollmentBound ? '绑定设备后设置私聊和群聊' : groupChatLoadFailed ? '群列表暂不可用,可手动输入群 ID' : `${dmChatEnabled ? '私聊可用' : '私聊已关闭'} · ${availableGroups.length} 个已发现群` },
934
+ { label: '聊天入口', detail: !selectedBot || !selectedEnrollmentBound ? '绑定设备后设置私聊和群聊' : groupChatLoadFailed ? '群列表暂不可用,请刷新状态' : `${dmChatEnabled ? '私聊可用' : '私聊已关闭'} · ${availableGroups.length} 个已发现群` },
818
935
  { label: '开放能力', detail: !botConnected || !selectedEnrollmentBound ? '完成机器人绑定后再开放' : authorizationLoadError ? '授权列表暂不可读,请刷新状态' : selectedTaskCount ? `${selectedTaskCount} 项已开放,可查看或停止` : '按需要创建任务' },
819
936
  { label: '复核授权', detail: authorizationLoadError ? '先确认已保存授权' : createOpen ? '检查请求范围后确认' : selectedTaskCount ? '查看已开放范围' : '请求人、执行方和期限' },
820
937
  ].map((step, index) => {
@@ -832,32 +949,15 @@ window.__ModuleLoader__.load({
832
949
  h('span', { style: { fontSize: 12, color: mutedTextStyle.color } }, state)),
833
950
  h('span', { style: mutedTextStyle }, step.detail)))
834
951
  })))
835
- const chatScopePanel = h('div', { id: 'ant-chat-scope-panel', tabIndex: -1, style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, padding: 12,
836
- display: 'grid', gap: 8 } },
837
- h('strong', null, '机器人和聊天范围'),
838
- h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 240px), 1fr))', gap: 8 } },
839
- h('div', { style: { display: 'grid', gap: 5, padding: 10, borderRadius: 8,
840
- border: '1px solid var(--dsw-alias-border-l2)', backgroundColor: 'var(--dsw-alias-bg-layer-1)' } },
841
- h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8 } },
842
- h('label', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
843
- h('input', { type: 'checkbox', checked: dmChatEnabled,
844
- disabled: busy || !selectedBot || !selectedEnrollmentBound
845
- || enrollmentLoadError || !dmChatSettingAvailable,
846
- onChange: (event) => void saveDmChat(event.target.checked),
847
- 'aria-label': '允许机器人私聊' }), h('strong', null, '私聊')),
848
- h('span', { style: { ...mutedTextStyle, whiteSpace: 'nowrap' } }, dmChatStatus)),
849
- h('p', { style: mutedTextStyle }, dmChatDescription)),
850
- 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,
851
955
  border: '1px solid var(--dsw-alias-border-l2)', backgroundColor: 'var(--dsw-alias-bg-layer-1)' } },
852
- h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8 } },
853
- h('label', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
854
- h('input', { type: 'checkbox', checked: Boolean(selectedChatGroup?.chat_enabled),
855
- disabled: busy || groupChatLoadFailed || !selectedEnrollmentBound || !botConnected || !chatGroupId.trim()
856
- || (Boolean(selectedChatGroup?.chat_enabled) && selectedGroupHasTasks),
857
- onChange: (event) => void saveGroupChat(event.target.checked),
858
- 'aria-label': '允许所选群进行普通聊天' }), h('strong', null, '群聊')),
859
- h('span', { style: { ...mutedTextStyle, whiteSpace: 'nowrap' } }, `${availableGroups.length} 个已发现`)),
860
- 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))
861
961
  const createCard = async (event) => {
862
962
  event.preventDefault()
863
963
  setDraftNotice('')
@@ -865,37 +965,40 @@ window.__ModuleLoader__.load({
865
965
  if (editingCardId && (!editingExpiresAt || editingExpiresAt <= Math.floor(Date.now() / 1000))) {
866
966
  setMessage('这项能力已到期,无法继续编辑;请返回后重新开放一项能力。'); return
867
967
  }
868
- if (!title.trim() || !intent.trim() || !taskPrompt.trim()) {
869
- setMessage('请填写能力名称、适用请求说明和处理要求。'); return
968
+ if (!ruleDescription.trim() || !title.trim() || !intent.trim() || !taskPrompt.trim()) {
969
+ setMessage('请填写任务描述并点击“识别规则”,检查摘要后再开放。'); return
870
970
  }
871
971
  const bytes = (value) => new TextEncoder().encode(value).length
872
- if (bytes(title.trim()) > 100 || bytes(intent.trim()) > 1000 || bytes(taskPrompt.trim()) > 4000) {
873
- setMessage('能力名称、请求说明或处理要求过长,请缩短后再提交。'); return
972
+ if (bytes(ruleDescription.trim()) > 4000 || bytes(title.trim()) > 100
973
+ || bytes(intent.trim()) > 1000 || bytes(taskPrompt.trim()) > 4000) {
974
+ setMessage('任务描述或识别摘要过长,请缩短描述并重新识别。'); return
874
975
  }
875
- const exampleLines = examples.split('\n').map((item) => item.trim()).filter(Boolean)
876
- if (exampleLines.length > 8 || exampleLines.some((item) => bytes(item) > 500)) {
877
- 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
878
980
  }
879
981
  const normalized = parameters.map((rule) => ({ name: rule.name.trim(), kind: rule.kind,
880
982
  description: rule.description.trim() || null,
881
- required: rule.required, min: rule.min === '' ? null : Number(rule.min),
882
- max: rule.max === '' ? null : Number(rule.max),
883
- 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,
884
987
  gitea_origin: rule.kind === 'gitea_pr' ? selectedEnrollment?.gitea_origin || null : null,
885
988
  repository: rule.kind === 'gitea_pr' ? rule.repository.trim().toLowerCase() : null }))
886
989
  if (normalized.some((rule) => !/^[A-Za-z][A-Za-z0-9_]{0,31}$/u.test(rule.name))) {
887
- setMessage('参数键请用英文字母开头,只能包含英文字母、数字和下划线;给同事看的中文问题可填在“参数说明”。'); return
990
+ setMessage('没能从描述中整理出有效的请求信息。请说明同事需要提供什么,再重新识别。'); return
888
991
  }
889
992
  if (new Set(normalized.map((rule) => rule.name)).size !== normalized.length) {
890
- setMessage('参数键不能重复,请为每项参数填写不同的英文键。'); return
993
+ setMessage('请求信息识别有重复项。请修改任务描述并重新识别。'); return
891
994
  }
892
995
  if (normalized.some((rule) => rule.description && bytes(rule.description) > 100)) {
893
- setMessage('参数说明过长,请缩短给同事看的问题。'); return
996
+ setMessage('请求信息说明过长。请缩短任务描述并重新识别。'); return
894
997
  }
895
998
  if (normalized.some((rule) => rule.kind === 'choice' && (!rule.choices?.length || rule.choices.length > 32
896
999
  || rule.choices.some((choice) => bytes(choice) > 100)
897
1000
  || new Set(rule.choices).size !== rule.choices.length))) {
898
- setMessage('选项参数请填写 1–32 个不重复的可选值,每项保持简短。'); return
1001
+ setMessage('描述中的可选范围不清楚或重复。请修改描述并重新识别。'); return
899
1002
  }
900
1003
  if (normalized.some((rule) => (rule.min != null && !Number.isInteger(rule.min))
901
1004
  || (rule.max != null && !Number.isInteger(rule.max))
@@ -903,10 +1006,10 @@ window.__ModuleLoader__.load({
903
1006
  || (rule.kind === 'text' && rule.max != null && (rule.max < 1 || rule.max > 4096))
904
1007
  || (rule.kind === 'gitea_pr' && ((rule.min != null && rule.min < 1)
905
1008
  || (rule.max != null && rule.max > 1_000_000_000))))) {
906
- setMessage('参数范围无效:最小值不能大于最大值;文本最长 4096 字节,PR 编号须为正整数。'); return
1009
+ setMessage('描述中的请求范围无效。请明确范围或提供完整 PR 链接,再重新识别。'); return
907
1010
  }
908
1011
  if (normalized.some((rule) => rule.kind === 'gitea_pr' && (!rule.gitea_origin || !rule.repository))) {
909
- setMessage('PR 参数还需要填写可信 Gitea 地址和仓库。'); return
1012
+ setMessage('没能从描述中识别 PR 来源或仓库。请补充完整 PR 链接或仓库名,再重新识别。'); return
910
1013
  }
911
1014
  const validRepository = (value) => {
912
1015
  const parts = value.split('/')
@@ -914,14 +1017,14 @@ window.__ModuleLoader__.load({
914
1017
  && /^[A-Za-z0-9_.-]{1,100}$/u.test(part))
915
1018
  }
916
1019
  if (normalized.some((rule) => rule.kind === 'gitea_pr' && !validRepository(rule.repository))) {
917
- setMessage('Gitea 仓库请填写“团队名/仓库名”,不要包含空格或多余路径。'); return
1020
+ setMessage('仓库名无法识别。请在描述中写明“团队名/仓库名”或粘贴完整 PR 链接,再重新识别。'); return
918
1021
  }
919
1022
  if (!reviewers.length || !reviewers.includes(runner)) {
920
1023
  setMessage('请至少选择一位审查 Agent,并设置普通请求的默认执行方。'); return
921
1024
  }
922
1025
  if (nativeReviewerSelected && (normalized.length !== 1
923
1026
  || normalized[0].kind !== 'gitea_pr' || !normalized[0].required)) {
924
- setMessage('选择 Codex 或 Claude Code 时,需要一个必填的 Gitea PR 编号参数。'); return
1027
+ setMessage('Codex 和 Claude Code 目前只支持审查一个 PR。请在任务描述中写明 PR 链接,再重新识别。'); return
925
1028
  }
926
1029
  const scope = conversationKind === 'group' ? `群 ${groupId}` : '私聊'
927
1030
  const resultDestination = conversationKind === 'group' ? '原群(群内可见)' : '发起任务的私聊'
@@ -934,8 +1037,8 @@ window.__ModuleLoader__.load({
934
1037
  ? `普通请求默认由 ${reviewerLabel(runner)} 执行;请求人明确说“交叉 review”时,会并行启动 ${reviewers.map(reviewerLabel).join('、')},分别返回审查结果。${reviewers.map((item) => executionDetails[item]).join(' ')}`
935
1038
  : executionDetails[reviewers[0]]
936
1039
  const actionDescription = editingCardId
937
- ? `更新“${title}”后,${requesterSummary}可在${scope}使用新配置;旧待确认请求将失效,已提交任务继续使用提交时的配置。结果${resultDestination}。到期时间保持为 ${new Date(editingExpiresAt * 1000).toLocaleString('zh-CN')}。`
938
- : `开放“${title}”后,${requesterSummary}可在${scope}请求,有效期 ${days} 天;结果${resultDestination}。`
1040
+ ? `更新“${title}”后,${requesterSummary}可在${scope}按任务描述中的规则发起请求。旧待确认请求将失效,已提交任务继续使用提交时的配置。结果${resultDestination}。到期时间保持为 ${new Date(editingExpiresAt * 1000).toLocaleString('zh-CN')}。`
1041
+ : `开放“${title}”后,${requesterSummary}可在${scope}按任务描述中的规则发起请求,有效期 ${days} 天;结果${resultDestination}。`
939
1042
  if (!window.confirm(`${actionDescription}${executionInfo}请确认。`)) return
940
1043
  setBusy(true)
941
1044
  try {
@@ -943,7 +1046,8 @@ window.__ModuleLoader__.load({
943
1046
  method: editingCardId ? 'PUT' : 'POST', headers: { 'content-type': 'application/json' },
944
1047
  body: JSON.stringify({ ...(editingCardId ? { id: editingCardId } : {}),
945
1048
  bot_id: selectedBot.bot_id, bot_fingerprint: selectedBot.fingerprint,
946
- title, intent, examples: exampleLines,
1049
+ title, rule_description: ruleDescription.trim(), trigger_phrases: triggerLines,
1050
+ intent, examples: [],
947
1051
  parameters: normalized, task_prompt: taskPrompt, runner, reviewers, workspace: null,
948
1052
  sender_scope: senderScope, sender_ids: senderScope === 'allowlist'
949
1053
  ? senderIds.split(/[\s,,]+/u).filter(Boolean) : [], conversation_kind: conversationKind,
@@ -951,6 +1055,9 @@ window.__ModuleLoader__.load({
951
1055
  expires_at: editingCardId ? editingExpiresAt : Math.floor(Date.now() / 1000) + Number(days) * 86400 }),
952
1056
  })
953
1057
  const result = await readLocalJsonResponse(response)
1058
+ if (!response.ok && result.code === 'IM_TASK_RULE_REQUIRED') {
1059
+ throw new Error('当前 ANT 插件版本不支持任务触发规则,请升级插件后再开放任务。')
1060
+ }
954
1061
  if (!response.ok && reviewers.includes('claude-code') && result.code === 'INVALID_REQUEST') {
955
1062
  throw new Error('当前团队服务端可能尚不支持 Claude Code 审查配置。请管理员升级服务端后刷新重试。')
956
1063
  }
@@ -970,13 +1077,17 @@ window.__ModuleLoader__.load({
970
1077
  }
971
1078
  throw new Error('当前团队服务端尚不支持多 Agent 审查配置;请管理员升级 ANT 服务端后重试。')
972
1079
  }
973
- 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)
974
1085
  setEditingCardId(null); setEditingExpiresAt(null)
975
1086
  await load()
976
1087
  setCreateOpen(false)
977
1088
  setMessage(editingCardId
978
1089
  ? '能力配置已更新。新请求将使用新配置,已提交任务保留原配置。'
979
- : '能力已开放。同事可自然地提出这类请求;参数不完整时机器人会追问。')
1090
+ : '任务已开放。机器人会按已确认的描述判断请求;缺少必要信息时会向请求人追问。')
980
1091
  } catch (error) { setMessage(error.message) }
981
1092
  finally { setBusy(false) }
982
1093
  }
@@ -994,29 +1105,36 @@ window.__ModuleLoader__.load({
994
1105
  }
995
1106
  const copyCardToDraft = (card) => {
996
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
+ : '复制的旧任务没有明确触发方式。请修改描述并重新识别,再在支持触发规则的服务端开放;副本不保留原有效期。')
997
1115
  const cardBot = bots.find((bot) => bot.fingerprint === card.bot_fingerprint)
998
1116
  const cardEnrollment = enrolled.find((bot) => bot.bot_fingerprint === card.bot_fingerprint)
999
1117
  const titleChars = Array.from(`复制:${card.title || '未命名能力'}`)
1000
1118
  while (new TextEncoder().encode(titleChars.join('')).length > 100) titleChars.pop()
1119
+ const taskDraft = taskRuleDraftFromCard(card, titleChars.join(''))
1120
+ recognizedBaselineRef.current = hasTriggerRule ? taskDraft : null
1001
1121
  setBotId(card.bot_id || cardBot?.bot_id || '')
1002
1122
  setTrustedOrigin(cardEnrollment?.gitea_origin || '')
1003
- setTitle(titleChars.join(''))
1004
- setIntent(card.intent || '')
1005
- setExamples(Array.isArray(card.examples) ? card.examples.join('\n') : '')
1006
- 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)
1007
1128
  setRunner(['dsh', 'codex', 'claude-code'].includes(card.runner) ? card.runner : 'dsh')
1008
1129
  setReviewers(Array.isArray(card.reviewers) && card.reviewers.length ? card.reviewers : [card.runner || 'dsh'])
1009
- setParameters(Array.isArray(card.parameters) ? card.parameters.map((rule) => ({
1010
- ...rule, min: String(rule.min ?? ''), max: String(rule.max ?? ''), choices: Array.isArray(rule.choices) ? rule.choices.join(',') : String(rule.choices ?? ''),
1011
- })) : [])
1130
+ setParameters(taskDraft.parameters)
1012
1131
  setConversationKind(card.conversation_kind === 'group' ? 'group' : 'dm')
1013
1132
  setGroupId(card.conversation_id || '')
1014
1133
  setSenderScope(card.sender_scope === 'allowlist' ? 'allowlist' : 'any')
1015
1134
  setSenderIds(Array.isArray(card.sender_ids) ? card.sender_ids.join('\n') : '')
1016
1135
  setDays(7)
1017
- setDraftNotice('已复制到新草稿。复制不保留原有效期;开放前请重新核对群、请求人和执行方,默认有效期为 7 天。')
1018
1136
  setCreateOpen(true)
1019
- onSelectTab('capability')
1137
+ onSelectTab('collaboration')
1020
1138
  }
1021
1139
  const cancelTask = async (task) => {
1022
1140
  if (!window.confirm(`取消任务 ${task.task_id}?已经发生的读取不能撤回。`)) return
@@ -1030,120 +1148,112 @@ window.__ModuleLoader__.load({
1030
1148
  } catch (error) { setMessage(error.message) }
1031
1149
  finally { setBusy(false) }
1032
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
1033
1220
  return h('section', { style: { ...settingsPanelStyle,
1034
- display: ['bot-chat', 'capability', 'local'].includes(activeTab) ? 'grid' : 'none' },
1035
- 'aria-label': '协作机器人与开放能力' },
1221
+ display: ['collaboration', 'local'].includes(activeTab) ? 'grid' : 'none' },
1222
+ 'aria-label': '群聊协作设置' },
1036
1223
  message ? h('p', { role: 'status', style: mutedTextStyle }, message) : null,
1037
1224
  h('div', { id: 'ant-chat-entry', role: 'region', 'aria-label': '聊天入口和机器人状态', tabIndex: -1,
1038
- style: { display: activeTab === 'bot-chat' ? 'grid' : 'none', gap: 14 } },
1039
- h('h3', { style: sectionTitleStyle }, '聊天入口'),
1040
- h('p', { style: mutedTextStyle }, '在所有者开启的私聊或群聊中,同事可以像平常一样和机器人聊天。DSH 负责理解与回答;只有准备执行你开放的能力时,ANT 才核对请求人、群、参数和权限。群里的任务结果会回复原群。'),
1041
- 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,
1042
1235
  loadFailures.length ? h('p', { role: 'status', style: mutedTextStyle },
1043
1236
  `部分协作信息暂时无法读取:${loadFailures.map((path) => loadNames[path]).join('、')}。已读取的信息仍显示,请稍后刷新状态。`) : null,
1044
- h('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'flex-start', justifyContent: 'space-between', gap: 8 } },
1045
- h('span', { role: 'status', style: { ...mutedTextStyle, flex: '1 1 260px', minWidth: 0 } }, loading ? '正在读取机器人与任务状态…'
1046
- : botLoadError ? '暂时无法读取机器人状态。请刷新后再试。'
1047
- : connectedBots.length === 0 ? '尚未发现已连接的机器人。请先在 DSH-IM 中连接飞书机器人。'
1048
- : selectedBot && !botConnected ? `“${selectedBot.name}”当前离线;请先在 DSH-IM 恢复连接。`
1049
- : enrollmentLoadError ? selectedBot
1050
- ? `“${selectedBot.name}”已连接;暂时无法确认协作是否已启用,请刷新状态。`
1051
- : '机器人已连接;协作启用状态暂未读取,请刷新状态。'
1052
- : selectedBot && !selectedEnrollmentBound ? selectedEnrollment
1053
- ? `“${selectedBot.name}”已连接;原协作配置尚未绑定到这台 DSH,请在本机重新启用。若原设备令牌曾被撤销,旧任务权限已关闭,绑定后需重新开放。`
1054
- : `“${selectedBot.name}”已连接;请先启用本机协作。`
1055
- : enrolledCount === 0 ? '机器人已连接;请选择并启用协作。'
1056
- : !selectedBot ? '已有协作机器人;请选择一台,查看已开放的能力和聊天范围。'
1057
- : authorizationLoadError ? `“${selectedBot.name}”已启用协作;已开放任务暂未读取,请刷新状态。`
1058
- : !dmChatEnabled ? `“${selectedBot.name}”已启用协作;私聊已关闭,群聊仍按各自设置工作。`
1059
- : selectedTaskCount === 0 ? `“${selectedBot.name}”已启用协作;下一步开放一项能力。你也可以先私聊自测普通聊天。`
1060
- : `“${selectedBot.name}”已启用协作 · ${selectedTaskCount} 项能力已开放。可以私聊自测。`),
1061
- h('button', { type: 'button', disabled: loading || busy,
1062
- onClick: () => void load().catch((error) => setMessage(error.message)),
1063
- style: { ...secondaryButtonStyle, whiteSpace: 'nowrap' } },
1064
- loading ? '正在刷新…' : '刷新状态')),
1065
- bots.length ? h('label', null, '我的机器人', h('select', { id: 'ant-bot-selector', value: botId,
1066
- onChange: (event) => { setBotId(event.target.value); setGroupId(''); setChatGroupId(''); setBindingCode('');
1067
- setTrustedOrigin(enrolled.find((bot) => bot.bot_id === event.target.value)?.gitea_origin || '') }, style: inputStyle },
1068
- h('option', { value: '' }, '请选择机器人'),
1069
- ...bots.map((bot) => h('option', { key: bot.bot_id, value: bot.bot_id,
1070
- disabled: !bot.available || !bot.connected }, `${bot.name}${bot.available && bot.connected ? '' : '(未连接)'}`)))) : null,
1071
- connectedBots.length === 0 && !loading && !botLoadError ? h('button', { type: 'button',
1072
- onClick: openImSettings, style: secondaryButtonStyle }, '打开 DSH-IM 设置') : null,
1073
- connectedBots.length === 0 && !loading && !botLoadError ? h('span', { style: mutedTextStyle },
1074
- '若尚未安装 DSH-IM,请先到 DSH 的插件列表安装并连接机器人。') : null,
1075
- selectedBot && !botConnected && connectedBots.length ? h('button', { type: 'button', onClick: openImSettings,
1076
- style: secondaryButtonStyle }, '检查 DSH-IM 连接') : null,
1077
- selectedBot && !selectedEnrollmentBound ? h('button', { type: 'button', disabled: busy || !botConnected || enrollmentLoadError,
1078
- onClick: () => void enrollBot(), style: secondaryButtonStyle },
1079
- selectedEnrollment ? '绑定原协作配置到这台 DSH' : '启用并绑定到这台 DSH') : null,
1080
- selectedBot ? h('div', { style: { display: 'grid', gap: 8, padding: 10,
1081
- border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8 } },
1082
- h('strong', null, `可信 Gitea 地址 · ${selectedBot.name}`),
1083
- h('p', { style: mutedTextStyle }, '为这个机器人登记一次。PR 能力只需配置仓库和 PR 编号,不必重复填写地址。'),
1084
- h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } },
1085
- h('input', { 'aria-label': '可信 Gitea 地址', type: 'url', value: trustedOrigin,
1086
- onChange: (event) => setTrustedOrigin(event.target.value), placeholder: 'https://git.example.com',
1087
- style: { ...inputStyle, flex: '1 1 280px' } }),
1088
- h('button', { type: 'button', disabled: busy || !trustedOrigin.trim() || (!selectedEnrollmentBound && !botConnected),
1089
- onClick: () => void (selectedEnrollmentBound ? saveTrustedOrigin() : enrollBot()), style: secondaryButtonStyle },
1090
- selectedEnrollmentBound ? '保存可信地址' : botConnected ? '绑定机器人并保存地址' : '先连接机器人'))) : null,
1091
- selectedBot && selectedEnrollmentBound ? h('div', { role: 'region', 'aria-label': '群任务所有者身份验证',
1092
- style: { display: 'grid', gap: 8, padding: 10, border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8,
1093
- backgroundColor: 'var(--dsw-alias-bg-layer-1)' } },
1094
- h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8 } },
1095
- h('strong', null, '群任务所有者身份'),
1096
- h('span', { role: 'status', style: { ...mutedTextStyle, color: selectedEnrollment.owner_bound
1097
- ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-warning-primary, var(--dsw-alias-label-secondary))' } },
1098
- selectedEnrollment.owner_bound ? '已绑定' : '尚未绑定')),
1099
- h('p', { style: mutedTextStyle }, '绑定只针对当前机器人,所有群共用。只有允许群成员请求已开放能力时才需要;普通群聊无需绑定。绑定后,群任务仍需你在目标群按机器人提示确认。'),
1100
- h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' } },
1101
- h('button', { type: 'button', disabled: busy || !botConnected || enrollmentLoadError,
1102
- onClick: () => void issueBindingCode(), style: secondaryButtonStyle },
1103
- selectedEnrollment.owner_bound ? '重新获取绑定码' : '获取绑定码'),
1104
- bindingCode ? h('span', { role: 'status', style: mutedTextStyle },
1105
- `请你本人私聊“${selectedBot.name}”发送“绑定身份 ${bindingCode}”。绑定码 5 分钟有效。`) : null)) : null,
1237
+ h('div', { id: 'ant-collaboration-settings-grid', style: collaborationSettingsGridStyle },
1238
+ robotCard,
1239
+ chatScopePanel,
1240
+ ),
1106
1241
  setupStepper,
1107
- chatScopePanel,
1242
+ groupTaskStatusPanel,
1108
1243
  selectedEnrollmentBound && botConnected ? h('p', { style: mutedTextStyle },
1109
1244
  dmChatEnabled
1110
- ? `自测:私聊“${selectedBot.name}”发送“你能做什么”;发起任务后可问“刚才的任务怎么样了”。群聊请先在目标群 @机器人,再开放能力时选择该群。`
1111
- : '私聊当前已关闭;如要恢复,请在上方开启。群聊请先在目标群 @机器人,再开放能力时选择该群。') : null,
1112
- selectedEnrollmentBound && botConnected ? h('div', { role: 'region', 'aria-label': '群聊白名单',
1113
- style: { display: 'grid', gap: 8 } },
1114
- h('strong', null, '群聊白名单'),
1115
- h('span', { role: 'status', style: mutedTextStyle }, groupChatLoadFailed
1116
- ? '群聊列表与当前权限暂时无法读取,请刷新后再更改;仍可填写群 ID。'
1117
- : availableGroups.length
1118
- ? '从机器人已发现的群中选择;仍可手动填写群 ID。允许普通聊天不等于允许群成员执行任务。'
1119
- : '先在目标群 @机器人并刷新列表;若平台暂未返回群列表,也可以手动填写群 ID。'),
1120
- h('div', { style: { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) auto', gap: 8, alignItems: 'end' } },
1121
- h('label', { style: { display: 'grid', gap: 5 } },
1122
- h('span', { style: mutedTextStyle }, '选择已记录的群'),
1123
- h('select', { 'aria-label': '已记录的群', value: availableGroups.some((group) => group.group_id === chatGroupId) ? chatGroupId : '',
1124
- onChange: (event) => setChatGroupId(event.target.value), style: inputStyle },
1125
- h('option', { value: '' }, '选择已发现的群聊'),
1126
- ...availableGroups.map((group) => h('option', { key: group.group_id, value: group.group_id },
1127
- `${group.group_code} · ${group.chat_enabled ? '已允许普通聊天' : '未允许普通聊天'}`)))),
1128
- h('button', { type: 'button', disabled: busy || !selectedEnrollmentBound,
1129
- onClick: () => void load().catch((error) => setMessage(error.message)), style: secondaryButtonStyle }, '刷新群列表')),
1130
- h('label', { style: { display: 'grid', gap: 5 } },
1131
- h('span', { style: mutedTextStyle }, '群 ID(平台未返回群名时可手动填写)'),
1132
- h('input', { 'aria-label': '群 ID', value: chatGroupId, maxLength: 256,
1133
- onChange: (event) => setChatGroupId(event.target.value.trim()),
1134
- placeholder: '输入群 ID', style: inputStyle })),
1135
- selectedChatGroup?.chat_enabled && selectedGroupHasTasks ? h('span', { style: mutedTextStyle },
1136
- '本群还有开放能力;关闭普通聊天前请先停止这些能力。') : null) : null),
1137
- h('div', { id: 'ant-capability-list', role: 'region', 'aria-label': '已开放能力列表', tabIndex: -1,
1138
- 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 } },
1139
1250
  h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 10, flexWrap: 'wrap' } },
1140
- h('h4', { style: sectionTitleStyle }, `我开放的能力 · ${authorizationLoadError ? '暂未读取' : activeCards.length}`),
1141
- h('div', { role: 'toolbar', 'aria-label': '能力授权操作', style: { display: 'flex', gap: 8, flexWrap: 'wrap' } },
1142
- h('button', { type: 'button', onClick: () => { setCreateOpen(true); onSelectTab('capability') },
1143
- style: primaryButtonStyle }, '开放新能力'),
1144
- h('button', { type: 'button', onClick: () => { preparePrExample(); onSelectTab('capability') },
1145
- 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 }, '新建任务规则'))),
1146
1255
  h('p', { style: mutedTextStyle }, '当前一项能力只能绑定一个范围:私聊,或某一个群。若要给其他群或私聊开放相同任务,目前需分别建立能力卡;它们的请求人和有效期独立。'),
1256
+ h('div', { id: 'ant-capability-cards', style: collaborationGridStyle },
1147
1257
  ...activeCards.map((card) => {
1148
1258
  const expiresAt = Number(card.expires_at) * 1000
1149
1259
  const remainingDays = Math.max(1, Math.ceil((expiresAt - Date.now()) / 86_400_000))
@@ -1152,46 +1262,46 @@ window.__ModuleLoader__.load({
1152
1262
  ? `群 ${groups.find((group) => group.group_id === card.conversation_id)?.group_code || card.conversation_id}` : '私聊'
1153
1263
  const requester = card.sender_scope === 'allowlist' ? '指定成员可请求'
1154
1264
  : card.conversation_kind === 'group' ? '群成员可请求' : '可私聊者可请求'
1155
- const parametersText = (card.parameters || []).map((rule) => {
1156
- const constraint = rule.kind === 'gitea_pr' && rule.repository ? `仓库 ${rule.repository}`
1157
- : rule.kind === 'choice' ? `选项 ${(rule.choices || []).join('、')}`
1158
- : ['integer', 'gitea_pr'].includes(rule.kind)
1159
- ? `范围 ${rule.min ?? 1}–${rule.max ?? '无上限'}` : ''
1160
- return `${rule.name}:${rule.description || rule.kind}${rule.required ? '(必填)' : '(可选)'}${constraint ? ` · ${constraint}` : ''}`
1161
- }).join(';')
1162
1265
  const examplesText = Array.isArray(card.examples) ? card.examples.join('、') : ''
1163
1266
  const requesterIds = card.sender_scope === 'allowlist' && Array.isArray(card.sender_ids) ? card.sender_ids.join('、') : ''
1164
1267
  const remainingDaysLabel = remainingDays <= 7 ? 'var(--dsw-alias-state-warning-primary, var(--dsw-alias-label-secondary))'
1165
1268
  : 'var(--dsw-alias-label-secondary)'
1166
- return h('div', { key: card.id, style: { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) auto',
1167
- alignItems: 'start', gap: 12, borderTop: '1px solid var(--dsw-alias-border-l2)', padding: '10px 0' } },
1168
- 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 } },
1169
1271
  h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' } },
1170
1272
  h('strong', { style: { overflowWrap: 'anywhere' } }, card.title || '未命名能力'),
1171
1273
  h('span', { style: { ...mutedTextStyle, padding: '2px 8px', borderRadius: 999,
1172
1274
  backgroundColor: 'var(--dsw-alias-bg-layer-2)', color: remainingDaysLabel, whiteSpace: 'nowrap' } }, expiry)),
1173
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 服务端后需重新描述并开放。'),
1174
1280
  h('details', null,
1175
- 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 } }, '查看规则说明'),
1176
1282
  h('div', { style: { display: 'grid', gap: 4, paddingTop: 6 } },
1177
- card.intent ? h('p', { style: mutedTextStyle }, `适用请求:${card.intent}`) : null,
1178
- parametersText ? h('p', { style: { ...mutedTextStyle, overflowWrap: 'anywhere' } }, `请求参数:${parametersText}`)
1179
- : h('p', { style: mutedTextStyle }, '请求参数:无'),
1283
+ card.rule_description ? h('p', { style: { ...mutedTextStyle, overflowWrap: 'anywhere' } }, card.rule_description) : null,
1180
1284
  requesterIds ? h('p', { style: { ...mutedTextStyle, overflowWrap: 'anywhere' } }, `允许请求的 IM 用户:${requesterIds}`) : null,
1181
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 }, '无需额外参数,按请求中的内容处理'),
1182
1290
  card.task_prompt ? h('p', { style: mutedTextStyle }, `处理要求:${card.task_prompt}`) : null))),
1183
- 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)' } },
1184
1293
  h('button', { type: 'button', disabled: busy, onClick: () => editCard(card), style: secondaryButtonStyle }, '编辑'),
1185
1294
  h('button', { type: 'button', disabled: busy, onClick: () => copyCardToDraft(card), style: secondaryButtonStyle }, '复制'),
1186
1295
  h('button', { type: 'button', disabled: busy, onClick: () => void revokeCard(card), style: secondaryButtonStyle }, '停止')))
1187
1296
  }),
1188
- 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' } },
1189
1298
  h('p', { style: mutedTextStyle }, authorizationLoadError
1190
1299
  ? '暂时无法确认已开放的能力。刷新授权状态后再试;列表未核实前请勿重复创建。'
1191
- : '还没有开放能力。先选机器人,再开放一项能力;PR 审查只是可选示例。'),
1300
+ : '还没有开放任务。先描述要做什么,再设置请求范围和有效期。'),
1192
1301
  authorizationLoadError ? h('button', { type: 'button', disabled: loading,
1193
1302
  onClick: () => void load().catch((error) => setMessage(error.message)), style: secondaryButtonStyle },
1194
1303
  loading ? '正在刷新…' : '刷新授权状态') : null) : null),
1304
+ ),
1195
1305
  h('div', { role: 'region', 'aria-label': '最近任务', style: { display: activeTab === 'local' ? 'grid' : 'none', gap: 12 } },
1196
1306
  h('h4', { style: sectionTitleStyle }, '最近任务'),
1197
1307
  ...(tasks.filter((task) => task.task_id).length ? tasks.filter((task) => task.task_id)
@@ -1213,13 +1323,13 @@ window.__ModuleLoader__.load({
1213
1323
  })
1214
1324
  : [h('p', { key: 'none', style: mutedTextStyle }, loadFailures.includes('status')
1215
1325
  ? '任务记录暂时无法读取,请刷新状态。' : '暂无任务。')])),
1216
- 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' } },
1217
1327
  h('details', { id: 'ant-new-task', open: createOpen,
1218
1328
  onToggle: (event) => setCreateOpen(event.currentTarget.open),
1219
1329
  style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, padding: 12 } },
1220
1330
  h('summary', { id: 'ant-new-task-summary', style: { cursor: 'pointer', fontWeight: 600 } }, editingCardId ? '编辑能力配置' : '开放一项能力'),
1221
- h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 320px), 1fr))',
1222
- gap: 16, alignItems: 'start', marginTop: 12 } },
1331
+ h('div', { style: { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr)',
1332
+ gap: 12, alignItems: 'start', marginTop: 12 } },
1223
1333
  h('form', { id: 'ant-capability-create-form', onSubmit: createCard, style: { display: 'grid', gap: 9, minWidth: 0 } },
1224
1334
  editingCardId ? h('p', { role: 'status', style: mutedTextStyle },
1225
1335
  `保存会生成新的授权版本并停止旧版本;到期时间保持不变:${new Date(editingExpiresAt * 1000).toLocaleString('zh-CN')}。已提交任务继续使用原配置。`) : null,
@@ -1234,132 +1344,108 @@ window.__ModuleLoader__.load({
1234
1344
  '这是尚未绑定到设备的机器人。请在要继续使用它的电脑重新启用;Gitea 地址和配置记录会保留。若旧设备访问曾被撤销,原任务权限不会恢复,需要重新开放。') : null,
1235
1345
  selectedBot && conversationKind === 'group' && !selectedBot.history_available ? h('p', { role: 'status', style: mutedTextStyle },
1236
1346
  '这个机器人当前不能读取平台群历史;群聊只能参考实际送达的消息和本机近期记录。请检查 DSH-IM 权限。') : null,
1237
- h('label', null, '能力名称', h('input', { required: true, maxLength: 100, value: title,
1238
- onChange: (event) => setTitle(event.target.value), placeholder: '例如:总结项目进度', style: inputStyle })),
1239
- h('label', null, '什么请求属于这项能力', h('textarea', { required: true, maxLength: 1000, rows: 2, value: intent,
1240
- onChange: (event) => setIntent(event.target.value), placeholder: '例如:把群里讨论的本周进展整理成摘要', style: inputStyle })),
1241
- h('label', null, '同事可能怎么说(每行一句,可选)', h('textarea', { rows: 2, value: examples,
1242
- onChange: (event) => setExamples(event.target.value), placeholder: '帮我总结一下刚才的讨论', style: inputStyle })),
1243
- h('label', null, '处理要求', h('textarea', { required: true, maxLength: 4000, rows: 3, value: taskPrompt,
1244
- onChange: (event) => setTaskPrompt(event.target.value), placeholder: '只使用当前会话可见的内容,列出结论和待办', style: inputStyle })),
1245
- h('fieldset', { style: { ...panelStyle, padding: 10, display: 'grid', gap: 7 } },
1246
- h('legend', null, '审查 Agent(可多选)'),
1247
- h('p', { style: mutedTextStyle }, '普通请求使用默认 Agent;明确说“交叉 review”时,所有勾选项会并行审查同一份 PR 快照(至少勾选两位才构成交叉审查,最多三位,并会同时消耗各自的本机或团队模型额度)。'),
1248
- h('div', { style: { display: 'flex', gap: 14, flexWrap: 'wrap' } },
1249
- h('label', null, h('input', { type: 'checkbox', 'aria-label': '审查 Agent DSH', checked: reviewers.includes('dsh'), disabled: !dshAvailable && !reviewers.includes('dsh'),
1250
- onChange: (event) => changeReviewer('dsh', event.target.checked) }), ' DSH · 仅审查已提供的 diff'),
1251
- h('label', null, h('input', { type: 'checkbox', 'aria-label': '审查 Agent Codex', checked: reviewers.includes('codex'),
1252
- disabled: (!codexAvailable || !hasPrParameter) && !reviewers.includes('codex'),
1253
- onChange: (event) => changeReviewer('codex', event.target.checked) }), ' Codex · 完整快照只读审查'),
1254
- h('label', null, h('input', { type: 'checkbox', 'aria-label': '审查 Agent Claude Code', checked: reviewers.includes('claude-code'),
1255
- disabled: (!claudeCodeAvailable || !hasPrParameter) && !reviewers.includes('claude-code'),
1256
- onChange: (event) => changeReviewer('claude-code', event.target.checked) }), ' Claude Code · 使用本机权限审查')),
1257
- !hasPrParameter ? h('p', { style: mutedTextStyle }, 'Codex 与 Claude Code 需要先添加一个必填的 Gitea PR 编号参数。') : null,
1258
- reviewers.length > 1 ? h('label', null, '普通请求默认执行方', h('select', { value: runner,
1259
- onChange: (event) => setRunner(event.target.value), style: inputStyle },
1260
- ...reviewers.map((item) => h('option', { key: item, value: item }, reviewerLabel(item))))) : null),
1261
- nativePrUnavailableReason ? h('p', { role: 'status', style: mutedTextStyle },
1262
- `${nativePrUnavailableReason},已禁用 Codex/Claude Code 完整快照选项;可选择 DSH 差异审查。`) : null,
1263
- reviewers.includes('codex') ? h('p', { role: 'status', style: mutedTextStyle },
1264
- 'Codex 使用这台电脑的登录账号及其用量;ANT 为每项能力维护仓库缓存,每次审查创建固定到 PR Head 的临时代码快照。不会收到 Gitea 令牌;Git LFS 实际文件不会下载。') : null,
1265
- reviewers.includes('claude-code') ? h('p', { role: 'status', style: mutedTextStyle },
1266
- 'Claude Code 通过 DSH 已注册的子代理运行,可查看同一 PR 快照。ANT 不会传 Gitea 令牌;它沿用 DSH 的本机权限设置,不具备 Codex 路径相同的固定只读隔离。Git LFS 实际文件不会下载。') : null,
1267
- nativeReviewerSelected ? h('p', { role: 'status', style: mutedTextStyle },
1268
- '本机 PR 执行方需要 Gitea granular token 的只读权限 read:repository 和 read:user;read:user 仅用于这台 DSH 获取 Git HTTPS 登录名。') : null,
1269
- h('details', { style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, padding: 10 } },
1270
- h('summary', { style: { cursor: 'pointer' } }, '参数与授权范围'),
1271
- h('div', null, h('strong', null, '需要从请求中确认的参数'),
1272
- h('p', { style: mutedTextStyle }, '可不添加参数。参数键使用英文,给同事看的中文问题填在参数说明;需要指定 PR 时选“Gitea PR 编号”,系统会核对它是否存在。自由文本留在本机,服务端只核对长度和摘要。')),
1273
- ...parameters.map((rule, index) => h('div', { key: index, style: { display: 'flex', flexWrap: 'wrap', gap: 8,
1274
- padding: 8, border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8 } },
1275
- h('input', { 'aria-label': '参数键(英文)', value: rule.name, maxLength: 32, placeholder: '英文键,例如 pr_number',
1276
- onChange: (event) => updateParameter(index, { name: event.target.value }), style: { ...inputStyle, flex: '1 1 150px' } }),
1277
- h('input', { 'aria-label': '参数说明', value: rule.description, maxLength: 100,
1278
- onChange: (event) => updateParameter(index, { description: event.target.value }),
1279
- placeholder: '给同事看的问题,例如要审查哪个 PR', style: { ...inputStyle, flex: '1 1 230px' } }),
1280
- h('select', { 'aria-label': '参数类型', value: rule.kind,
1281
- onChange: (event) => updateParameter(index, { kind: event.target.value }), style: { ...inputStyle, flex: '1 1 150px' } },
1282
- h('option', { value: 'text' }, '文本'), h('option', { value: 'integer' }, '整数'),
1283
- h('option', { value: 'choice' }, '选项'), h('option', { value: 'gitea_pr' }, 'Gitea PR 编号')),
1284
- h('label', null, h('input', { type: 'checkbox', 'aria-label': '参数必填', checked: rule.required,
1285
- onChange: (event) => updateParameter(index, { required: event.target.checked }) }), ' 必填'),
1286
- ['integer', 'gitea_pr'].includes(rule.kind) ? h('input', { 'aria-label': '最小值', type: 'number', value: rule.min,
1287
- onChange: (event) => updateParameter(index, { min: event.target.value }), placeholder: '最小值', style: { ...inputStyle, width: 120 } }) : null,
1288
- ['text', 'integer', 'gitea_pr'].includes(rule.kind) ? h('input', { 'aria-label': '最大值', type: 'number', value: rule.max,
1289
- onChange: (event) => updateParameter(index, { max: event.target.value }), placeholder: '最大值', style: { ...inputStyle, width: 120 } }) : null,
1290
- rule.kind === 'choice' ? h('input', { 'aria-label': '可选值', value: rule.choices,
1291
- onChange: (event) => updateParameter(index, { choices: event.target.value }), placeholder: '用逗号分隔选项', style: { ...inputStyle, flex: '1 1 250px' } }) : null,
1292
- rule.kind === 'gitea_pr' ? h('span', { style: mutedTextStyle },
1293
- `使用机器人的可信 Gitea 地址:${selectedEnrollment?.gitea_origin || trustedOrigin || '请在下方登记一次'}`) : null,
1294
- rule.kind === 'gitea_pr' ? h('input', { 'aria-label': 'Gitea 仓库', value: rule.repository,
1295
- onChange: (event) => updateParameter(index, { repository: event.target.value }), placeholder: '团队/仓库', style: { ...inputStyle, flex: '1 1 180px' } }) : null,
1296
- h('button', { type: 'button', style: secondaryButtonStyle,
1297
- onClick: () => setParameters((current) => current.filter((_, i) => i !== index)) }, '移除参数'))),
1298
- h('button', { type: 'button', disabled: parameters.length >= 12, style: secondaryButtonStyle,
1299
- onClick: () => setParameters((current) => [...current, { name: '', kind: 'text', required: true,
1300
- description: '', min: '', max: '', choices: '', gitea_origin: '', repository: '' }]) }, '添加参数'),
1301
- parameters.some((rule) => rule.kind === 'gitea_pr') ? h('div', { style: { display: 'grid', gap: 6 } },
1302
- h('p', { style: mutedTextStyle }, selectedEnrollment?.gitea_origin
1303
- ? `将使用机器人已登记的可信 Gitea 地址:${selectedEnrollment.gitea_origin}`
1304
- : '请先在“机器人与聊天”中为这个机器人登记可信 Gitea 地址。'),
1305
- !tokenConfigured ? h('button', { type: 'button', onClick: () => onSelectTab('local'), style: secondaryButtonStyle },
1306
- '前往本机与任务保存 Gitea 令牌') : h('p', { style: mutedTextStyle }, '本机 Gitea 只读令牌已保存。')) : null,
1307
- h('label', null, '使用场景', h('select', { value: conversationKind,
1308
- onChange: (event) => { setConversationKind(event.target.value); setGroupId('') }, style: inputStyle },
1309
- h('option', { value: 'dm' }, '私聊'), h('option', { value: 'group' }, '指定群聊'))),
1310
- conversationKind === 'group' ? h('label', null, '群聊', h('select', { required: true, value: groupId,
1311
- onChange: (event) => setGroupId(event.target.value), style: inputStyle },
1312
- h('option', { value: '' }, '让群成员先 @ 机器人,再刷新列表'),
1313
- ...availableGroups.map((group) => h('option', { key: group.group_id, value: group.group_id },
1314
- `识别码 ${group.group_code} · ${group.group_id}`)))) : null,
1315
- conversationKind === 'group' ? h('button', { type: 'button', disabled: busy,
1316
- onClick: () => void load().catch((error) => setMessage(error.message)), style: secondaryButtonStyle }, '刷新群列表') : null,
1317
- h('label', null, '谁可以请求', h('select', { value: senderScope,
1318
- onChange: (event) => setSenderScope(event.target.value), style: inputStyle },
1319
- h('option', { value: 'any' }, conversationKind === 'group' ? '此群任何成员' : '任何能私聊此机器人者'),
1320
- h('option', { value: 'allowlist' }, '指定 IM 用户 ID'))),
1321
- senderScope === 'allowlist' ? h('input', { 'aria-label': '可请求的 IM 用户 ID', required: true, value: senderIds,
1322
- onChange: (event) => setSenderIds(event.target.value), placeholder: '多个 ID 用逗号分隔', style: inputStyle }) : null,
1323
- editingCardId ? h('p', { style: mutedTextStyle }, `有效期:${new Date(editingExpiresAt * 1000).toLocaleString('zh-CN')}(编辑时保持不变)`)
1324
- : h('label', null, '有效期', h('select', { value: days, onChange: (event) => setDays(Number(event.target.value)), style: inputStyle },
1325
- 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),
1326
1421
  ),
1327
1422
  h('aside', { id: 'ant-authorization-review', role: 'region', 'aria-label': '开放前复核', tabIndex: -1,
1328
- style: { ...settingsPanelStyle, padding: 14, gap: 10,
1329
- position: 'sticky', top: 12, alignSelf: 'start' } },
1423
+ style: { ...settingsPanelStyle, padding: 14, gap: 10, alignSelf: 'stretch' } },
1330
1424
  h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8 } },
1331
- h('h4', { style: sectionTitleStyle }, '开放前复核'),
1425
+ h('h4', { style: sectionTitleStyle }, '开放前看一眼'),
1332
1426
  h('span', { style: { ...mutedTextStyle, padding: '3px 8px', borderRadius: 999,
1333
1427
  backgroundColor: 'var(--dsw-alias-bg-layer-2)' } }, '草稿 · 未开放')),
1334
- h('p', { style: mutedTextStyle }, '保存后,机器人只会在以下范围内执行这项能力。'),
1335
- h('dl', { style: { display: 'grid', gap: 8, margin: 0 } },
1336
- ...[
1337
- ['触发位置', conversationKind === 'group' ? `群 ${groupId || '尚未选择'}` : '私聊'],
1338
- ['可请求人', requesterSummary],
1339
- ['执行方', executorSummary],
1340
- ['读取资料', resourceSummary],
1341
- ['有效期', editingCardId ? new Date(editingExpiresAt * 1000).toLocaleString('zh-CN') : `${days} 天`],
1342
- ['回复位置', resultRouteSummary],
1343
- ].map(([label, value]) => h('div', { key: label, style: { display: 'grid', gridTemplateColumns: '84px minmax(0, 1fr)', gap: 8 } },
1344
- h('dt', { style: mutedTextStyle }, label),
1345
- 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}。`),
1346
1431
  cardBlockReason ? h('p', { role: 'status', style: { ...mutedTextStyle,
1347
1432
  color: 'var(--dsw-alias-state-warning-primary, var(--dsw-alias-label-secondary))' } }, cardBlockReason) : null,
1348
1433
  h('button', { type: 'submit', form: 'ant-capability-create-form', disabled: busy || Boolean(cardBlockReason),
1349
1434
  style: { ...primaryButtonStyle, width: '100%' } }, editingCardId ? '保存能力修改' : '确认并开放能力'))))),
1350
1435
  h('div', { role: 'region', 'aria-label': '本机处理设置', style: { display: activeTab === 'local' ? 'grid' : 'none', gap: 12 } },
1351
- h('section', { style: { ...settingsPanelStyle, padding: 14 } },
1352
- h('strong', null, 'Gitea 本机凭据'),
1436
+ trustedOriginCard,
1437
+ showPrSecuritySettings ? h('section', { style: { ...settingsPanelStyle, padding: 14 } },
1438
+ h('strong', null, 'PR 只读凭据(本机)'),
1353
1439
  h('p', { style: mutedTextStyle }, tokenConfigured
1354
1440
  ? '只读访问令牌已保存在这台 DSH 的安全凭据中;不会同步到 ANT 服务端或发送给执行器。'
1355
- : 'PR 审查能力需要只读访问令牌。令牌只保存到这台 DSH。'),
1441
+ : '读取 Gitea PR 需要只读访问令牌。令牌只保存在这台 DSH,不提交给 ANT 服务端或执行器。'),
1356
1442
  h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } },
1357
1443
  h('input', { 'aria-label': 'Gitea 只读访问令牌', type: 'password', autoComplete: 'new-password', value: giteaToken,
1358
1444
  onChange: (event) => setGiteaToken(event.target.value),
1359
1445
  placeholder: tokenConfigured ? '已保存;输入新令牌可替换' : '粘贴 Gitea 只读令牌',
1360
1446
  style: { ...inputStyle, flex: '1 1 280px' } }),
1361
1447
  h('button', { type: 'button', disabled: busy || !giteaToken.trim(),
1362
- onClick: () => void saveGiteaToken(), style: secondaryButtonStyle }, tokenConfigured ? '替换本机令牌' : '保存本机令牌'))),
1448
+ onClick: () => void saveGiteaToken(), style: secondaryButtonStyle }, tokenConfigured ? '替换本机令牌' : '保存本机令牌'))) : null,
1363
1449
  h('details', { style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, padding: 12 } },
1364
1450
  h('summary', { style: { cursor: 'pointer', fontWeight: 600 } }, '本机处理设置'),
1365
1451
  h('p', { style: mutedTextStyle }, '本机并发上限只影响这台电脑接收的新任务;PR 仓库缓存上限按这台 DSH 的长期仓库缓存总量计算,不包括任务临时工作目录。'),
@@ -1384,7 +1470,19 @@ window.__ModuleLoader__.load({
1384
1470
  h('button', { disabled: busy, style: secondaryButtonStyle }, '保存本机处理设置'),
1385
1471
  h('p', { style: { ...mutedTextStyle, flex: '1 1 100%', margin: 0 } },
1386
1472
  '默认 10 GiB,范围 1–100 GiB,按长期仓库文件逻辑大小计算(不含临时工作区和文件系统分配开销)。日常不会淘汰仍启用能力的缓存;停用或到期能力会在刷新时回收。单次拉取若实际越界,为恢复预算会清理这项能力的仓库缓存(旧 PR 可重新下载)。')),
1387
- 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
+ : '处理授权任务提供的内容,不调用本机工具。'))))))
1388
1486
  }
1389
1487
 
1390
1488
  function SkillCenterPage({ remote, onBack, onAll, onSkills, onPlugins, openBundle,
@@ -2063,11 +2161,13 @@ window.__ModuleLoader__.load({
2063
2161
  const key = `${providerId}\u0000${protocol}`
2064
2162
  let provider = grouped.get(key)
2065
2163
  if (!provider) {
2164
+ const providerName = model.provider_name || (protocol === 'openai'
2165
+ ? 'ANT 团队 · OpenAI' : 'ANT 团队 · Anthropic')
2066
2166
  provider = {
2067
2167
  providerId,
2068
- providerName: model.provider_name || (protocol === 'openai'
2069
- ? `ANT 团队 · OpenAI · ${model.openai_api_mode === 'responses' ? 'Responses' : 'Chat Completions'}`
2070
- : 'ANT 团队 · Anthropic'),
2168
+ providerName: protocol === 'openai'
2169
+ ? compactOpenaiProviderName(providerName)
2170
+ : providerName,
2071
2171
  protocol,
2072
2172
  apiMode: model.openai_api_mode === 'responses' ? 'responses' : 'chat_completions',
2073
2173
  legacyEndpoint: typeof model.provider_id !== 'string' || !model.provider_id,
@@ -2605,14 +2705,19 @@ window.__ModuleLoader__.load({
2605
2705
  const updateGuideUrl = pinnedServiceUrl
2606
2706
  ? new URL('/ant-update', pinnedServiceUrl).href : null
2607
2707
  const currentDshDefaultModel = modelForProviderSelection(dshDefaultSelection, authorizedModels)
2608
- const modelRows = authorizedModels.map((model) => h('div', { key: `${model.provider_id || ''}:${model.protocol}:${model.model_id}`, role: 'listitem',
2609
- style: { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) minmax(120px, auto) minmax(130px, auto)',
2610
- gap: 12, alignItems: 'center', padding: '10px 2px', borderBottom: '1px solid var(--dsw-alias-border-l2)' } },
2611
- h('strong', { style: { minWidth: 0, overflowWrap: 'anywhere' } }, `${model.provider_name ? `${model.provider_name} · ` : ''}${model.display_name || model.model_id}`),
2612
- h('span', { style: { display: 'inline-flex', justifySelf: 'start', padding: '3px 8px', borderRadius: 999,
2613
- backgroundColor: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-secondary)', fontSize: 12 } },
2614
- model.protocol === 'anthropic' ? 'Anthropic Messages' : `OpenAI · ${model.openai_api_mode === 'responses' ? 'Responses' : 'Chat Completions'}`),
2615
- h('span', { style: { ...mutedTextStyle, textAlign: 'right' } }, '已同步到 DSH')))
2708
+ const modelRows = authorizedModels.map((model) => {
2709
+ const providerName = model.protocol === 'openai'
2710
+ ? compactOpenaiProviderName(model.provider_name)
2711
+ : model.provider_name
2712
+ return h('div', { key: `${model.provider_id || ''}:${model.protocol}:${model.model_id}`, role: 'listitem',
2713
+ style: { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) minmax(120px, auto) minmax(130px, auto)',
2714
+ gap: 12, alignItems: 'center', padding: '10px 2px', borderBottom: '1px solid var(--dsw-alias-border-l2)' } },
2715
+ h('strong', { style: { minWidth: 0, overflowWrap: 'anywhere' } }, `${providerName ? `${providerName} · ` : ''}${model.display_name || model.model_id}`),
2716
+ h('span', { style: { display: 'inline-flex', justifySelf: 'start', padding: '3px 8px', borderRadius: 999,
2717
+ backgroundColor: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-secondary)', fontSize: 12 } },
2718
+ model.protocol === 'anthropic' ? 'Anthropic Messages' : `OpenAI · ${model.openai_api_mode === 'responses' ? 'Responses' : 'Chat Completions'}`),
2719
+ h('span', { style: { ...mutedTextStyle, textAlign: 'right' } }, '已同步到 DSH'))
2720
+ })
2616
2721
  const connectionPanel = h('section', { style: { ...settingsPanelStyle, display: activeTab === 'connection' ? 'grid' : 'none' } },
2617
2722
  h('div', null,
2618
2723
  h('h3', { style: sectionTitleStyle }, 'ANT 团队账号'),
@@ -2669,7 +2774,8 @@ window.__ModuleLoader__.load({
2669
2774
  h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12 } },
2670
2775
  h('div', null,
2671
2776
  h('h3', { style: sectionTitleStyle }, '团队模型'),
2672
- h('p', { style: { ...mutedTextStyle, marginTop: 4 } }, '每个获准模型单独列出;刷新后同步到 DSH 模型选择器。')),
2777
+ h('p', { style: { ...mutedTextStyle, marginTop: 4 } }, '每个获准模型单独列出;刷新后同步到 DSH 模型选择器。'),
2778
+ h('p', { style: { ...mutedTextStyle, marginTop: 4 } }, 'API 模式由 ANT 团队提供方设置统一控制,作用于该提供方下的所有模型。DSH 设置中的模式只影响当前 Profile,不会修改服务器;模式不匹配会导致请求失败,刷新团队模型会重新应用团队设置。')),
2673
2779
  h('span', { style: { flexShrink: 0, padding: '4px 9px', borderRadius: 999,
2674
2780
  color: credentialConfigured === true && bindingReady ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-label-secondary)',
2675
2781
  backgroundColor: credentialConfigured === true && bindingReady ? 'var(--dsw-alias-state-success-tertiary)' : 'var(--dsw-alias-bg-layer-2)',
@@ -2687,8 +2793,14 @@ window.__ModuleLoader__.load({
2687
2793
  disabled: credentialConfigured !== true || !authorizedModels.length || defaultModelBusy || busy,
2688
2794
  onChange: (event) => setDefaultModelDraft(event.target.value), style: inputStyle },
2689
2795
  h('option', { value: '' }, '选择获准模型'),
2690
- ...authorizedModels.map((model) => h('option', { key: modelSelectionKey(model), value: modelSelectionKey(model) },
2691
- `${model.provider_name ? `${model.provider_name} · ` : ''}${model.display_name || model.model_id} · ${model.protocol === 'anthropic' ? 'Anthropic' : model.openai_api_mode === 'responses' ? 'OpenAI Responses' : 'OpenAI Chat Completions'}`)))),
2796
+ ...authorizedModels.map((model) => {
2797
+ const providerName = model.protocol === 'openai'
2798
+ ? compactOpenaiProviderName(model.provider_name)
2799
+ : model.provider_name
2800
+ const protocolName = model.protocol === 'anthropic' ? 'Anthropic' : `OpenAI ${model.openai_api_mode === 'responses' ? 'Responses' : 'Chat Completions'}`
2801
+ return h('option', { key: modelSelectionKey(model), value: modelSelectionKey(model) },
2802
+ `${providerName ? `${providerName} · ` : ''}${model.display_name || model.model_id} · ${protocolName}`)
2803
+ }))),
2692
2804
  h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' } },
2693
2805
  h('button', { type: 'button', disabled: defaultModelBusy || !defaultModelDraft,
2694
2806
  onClick: () => { const chosen = authorizedModels.find((model) => modelSelectionKey(model) === defaultModelDraft); void applyUserDefaultModel(chosen) },