dsh-ant 0.6.1 → 0.6.3

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
@@ -12,6 +12,37 @@ window.__ModuleLoader__.load({
12
12
  let localeMetadata = {}
13
13
  let localeTranslate = (key) => key
14
14
  let localeMessagePatterns = []
15
+ const pluginScopeBusyMessage = '本机有一项插件操作仍在完成;结束前不能启动其他插件安装。'
16
+ const pluginRuntimeState = {
17
+ uncertainInstalls: new Map(), uncertainInstallsRef: { current: {} },
18
+ serviceUrl: '', scopeRevision: 0, workingId: '', workingServiceUrl: '', completedId: '',
19
+ message: '', messagePluginId: { current: '' }, messagePackageName: { current: '' },
20
+ messageClearOnResolve: { current: false },
21
+ refreshMessage: '', refreshMessageIsError: false,
22
+ messageVersion: { current: 0 }, refreshGeneration: { current: 0 }, refreshRevision: 0,
23
+ listeners: new Set(),
24
+ }
25
+ const publishPluginRuntimeState = () => {
26
+ pluginRuntimeState.uncertainInstallsRef.current = Object.fromEntries(pluginRuntimeState.uncertainInstalls)
27
+ for (const listener of [...pluginRuntimeState.listeners]) listener()
28
+ }
29
+ const setPluginRuntimeServiceScope = (serviceUrl) => {
30
+ const nextServiceUrl = normalizeServiceUrl(serviceUrl) || ''
31
+ if (nextServiceUrl === pluginRuntimeState.serviceUrl) return
32
+ pluginRuntimeState.serviceUrl = nextServiceUrl
33
+ pluginRuntimeState.scopeRevision += 1
34
+ pluginRuntimeState.completedId = ''
35
+ pluginRuntimeState.message = pluginRuntimeState.workingId ? pluginScopeBusyMessage : ''
36
+ pluginRuntimeState.messagePluginId.current = ''
37
+ pluginRuntimeState.messagePackageName.current = ''
38
+ pluginRuntimeState.messageClearOnResolve.current = false
39
+ pluginRuntimeState.refreshMessage = ''
40
+ pluginRuntimeState.refreshMessageIsError = false
41
+ pluginRuntimeState.messageVersion.current += 1
42
+ pluginRuntimeState.refreshGeneration.current += 1
43
+ pluginRuntimeState.refreshRevision += 1
44
+ publishPluginRuntimeState()
45
+ }
15
46
  const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&')
16
47
  const compileLocalePattern = (source) => {
17
48
  const names = []
@@ -54,6 +85,45 @@ window.__ModuleLoader__.load({
54
85
  }
55
86
  return createElement(type, localizedProps, ...children.map(localizedChild))
56
87
  }
88
+ const releaseNoteBlocks = (value) => {
89
+ const blocks = []
90
+ let paragraph = []
91
+ let list = []
92
+ let key = 0
93
+ const flushParagraph = () => {
94
+ if (!paragraph.length) return
95
+ blocks.push(h('p', { key: key++, style: { ...mutedTextStyle, whiteSpace: 'pre-line' } },
96
+ rawLocaleText(paragraph.join('\n'))))
97
+ paragraph = []
98
+ }
99
+ const flushList = () => {
100
+ if (!list.length) return
101
+ blocks.push(h('ul', { key: key++, style: { margin: 0, paddingLeft: 22, display: 'grid', gap: 4,
102
+ color: 'var(--dsw-alias-label-secondary)', fontSize: 13, lineHeight: 1.6 } },
103
+ ...list.map((item) => h('li', { key: item.key }, rawLocaleText(item.text)))))
104
+ list = []
105
+ }
106
+ for (const line of String(value ?? '').replace(/\r\n?/gu, '\n').split('\n')) {
107
+ const heading = /^\s{0,3}(#{1,3})\s+(.+)$/u.exec(line)
108
+ const bullet = /^\s*(?:[-*•]|\d+[.)])\s+(.+)$/u.exec(line)
109
+ if (heading) {
110
+ flushParagraph(); flushList()
111
+ const level = heading[1].length
112
+ const tag = level === 1 ? 'h3' : level === 2 ? 'h4' : 'h5'
113
+ blocks.push(h(tag, { key: key++, style: { margin: 0,
114
+ fontSize: level === 1 ? 15 : 13, lineHeight: 1.45 } }, rawLocaleText(heading[2].trim())))
115
+ } else if (bullet) {
116
+ flushParagraph()
117
+ list.push({ key: key++, text: bullet[1].trim() })
118
+ } else if (!line.trim()) {
119
+ flushParagraph(); flushList()
120
+ } else {
121
+ flushList(); paragraph.push(line)
122
+ }
123
+ }
124
+ flushParagraph(); flushList()
125
+ return blocks
126
+ }
57
127
  const confirmLocalized = (message) => window.confirm(translateLocaleText(message))
58
128
  const promptLocalized = (message, ...args) => window.prompt(translateLocaleText(message), ...args)
59
129
  const useLocaleRevision = () => {
@@ -444,6 +514,7 @@ window.__ModuleLoader__.load({
444
514
  if (result.service_url != null && !pinned) throw new Error()
445
515
  if (pinned) {
446
516
  if (current()) {
517
+ setPluginRuntimeServiceScope(pinned)
447
518
  setBinding({ state: 'ready', url: pinned })
448
519
  setServiceUrl(pinned)
449
520
  setDraft(pinned)
@@ -494,6 +565,7 @@ window.__ModuleLoader__.load({
494
565
  }
495
566
  try {
496
567
  localStorage.setItem(SERVICE_URL_STORAGE_KEY, normalized)
568
+ setPluginRuntimeServiceScope(normalized)
497
569
  setServiceUrl(normalized)
498
570
  setDraft(normalized)
499
571
  setMessage('团队服务地址已保存。')
@@ -509,6 +581,7 @@ window.__ModuleLoader__.load({
509
581
  const bindingReadyForSummary = binding.state === 'ready' && Boolean(binding.url)
510
582
  const onConnectionChange = (url) => {
511
583
  bindingRevision.current += 1
584
+ setPluginRuntimeServiceScope(url || '')
512
585
  setBinding({ state: 'ready', url: url || null })
513
586
  setDelegationRevision((value) => value + 1)
514
587
  if (url) {
@@ -623,14 +696,17 @@ window.__ModuleLoader__.load({
623
696
  const [localConcurrency, setLocalConcurrency] = useState(null)
624
697
  const [repositoryCacheLimitGib, setRepositoryCacheLimitGib] = useState(10)
625
698
  const [ownerChallengeCode, setOwnerChallengeCode] = useState('')
699
+ const ownerChallengeMetaRef = useRef(null)
700
+ const ownerChallengeTimerRef = useRef(null)
626
701
  const [botId, setBotId] = useState('')
702
+ const botIdRef = useRef('')
627
703
  const [conversationKind, setConversationKind] = useState('dm')
628
704
  const [groupId, setGroupId] = useState('')
629
705
  const [ruleDescription, setRuleDescription] = useState('')
630
- const [triggerPhrases, setTriggerPhrases] = useState('')
631
706
  const recognizedBaselineRef = useRef(null)
632
707
  const recognizedDescriptionRef = useRef(null)
633
708
  const recognitionRequestRef = useRef(0)
709
+ const recognitionControllerRef = useRef(null)
634
710
  const [recognitionBusy, setRecognitionBusy] = useState(false)
635
711
  const [recognitionMessage, setRecognitionMessage] = useState('')
636
712
  const [missingRequired, setMissingRequired] = useState([])
@@ -641,7 +717,31 @@ window.__ModuleLoader__.load({
641
717
  const [taskPrompt, setTaskPrompt] = useState('')
642
718
  const [runner, setRunner] = useState('dsh')
643
719
  const [reviewers, setReviewers] = useState(['dsh'])
720
+ const [ownerApprovalRequired, setOwnerApprovalRequired] = useState(true)
644
721
  const [parameters, setParameters] = useState([])
722
+ const cancelRecognitionRequest = () => {
723
+ recognitionControllerRef.current?.abort()
724
+ recognitionControllerRef.current = null
725
+ setRecognitionBusy(false)
726
+ }
727
+ const clearOwnerChallenge = () => {
728
+ if (ownerChallengeTimerRef.current !== null) clearTimeout(ownerChallengeTimerRef.current)
729
+ ownerChallengeTimerRef.current = null
730
+ ownerChallengeMetaRef.current = null
731
+ setOwnerChallengeCode('')
732
+ }
733
+ const selectBot = (nextBotId) => {
734
+ if (nextBotId !== botIdRef.current) {
735
+ clearOwnerChallenge()
736
+ botIdRef.current = nextBotId
737
+ }
738
+ setBotId(nextBotId)
739
+ }
740
+ useEffect(() => () => {
741
+ recognitionRequestRef.current += 1
742
+ recognitionControllerRef.current?.abort()
743
+ if (ownerChallengeTimerRef.current !== null) clearTimeout(ownerChallengeTimerRef.current)
744
+ }, [])
645
745
  const [giteaToken, setGiteaToken] = useState('')
646
746
  const [tokenConfigured, setTokenConfigured] = useState(false)
647
747
  const [senderScope, setSenderScope] = useState('any')
@@ -661,9 +761,12 @@ window.__ModuleLoader__.load({
661
761
  setLoading(true)
662
762
  setMessage('')
663
763
  try {
664
- const paths = ['bots', 'enrolled-bots', 'groups', 'cards', 'settings', 'executors', 'local-settings', 'status']
764
+ const ownerChallengeAtLoad = ownerChallengeMetaRef.current
765
+ const paths = ['bots', 'enrolled-bots', 'groups', 'cards', 'settings', 'executors', 'local-settings', 'status',
766
+ ...(ownerChallengeAtLoad ? ['owner-challenge'] : [])]
665
767
  const responses = await Promise.all(paths.map((path) =>
666
- fetch(`/api/ant-teams/delegation/${path}`, { cache: 'no-store',
768
+ fetch(`/api/ant-teams/delegation/${path}${path === 'owner-challenge'
769
+ ? `?bot_fingerprint=${encodeURIComponent(ownerChallengeAtLoad.fingerprint)}` : ''}`, { cache: 'no-store',
667
770
  ...(path === 'enrolled-bots' ? { headers: { 'x-ant-include-unbound': 'true' } } : {}) })
668
771
  .catch(() => null)))
669
772
  const data = await Promise.all(responses.map((response) => response
@@ -675,12 +778,30 @@ window.__ModuleLoader__.load({
675
778
  Array.isArray,
676
779
  (value) => value && typeof value === 'object' && !Array.isArray(value),
677
780
  (value) => Array.isArray(value?.items),
781
+ ...(ownerChallengeAtLoad ? [(value) => value && typeof value === 'object'
782
+ && typeof value.pending === 'boolean'] : []),
678
783
  ]
679
784
  const readable = responses.map((response, index) => Boolean(response?.ok && valid[index](data[index])))
680
- setLoadFailures(paths.filter((path, index) => !readable[index]))
785
+ setLoadFailures(paths.filter((path, index) => !readable[index] && path !== 'owner-challenge'))
681
786
  setBots(readable[0] ? data[0].bots : [])
682
787
  setBotLoadError(!readable[0])
683
- setEnrolled(readable[1] ? data[1] : [])
788
+ const nextEnrolled = readable[1] ? data[1] : []
789
+ setEnrolled(nextEnrolled)
790
+ const ownerChallenge = ownerChallengeAtLoad
791
+ if (ownerChallenge) {
792
+ const selectedFingerprint = readable[0] ? data[0].bots.find((bot) => bot.bot_id === botId)?.fingerprint : null
793
+ const enrollmentRead = readable[1]
794
+ const nowBound = enrollmentRead && nextEnrolled.some((enrollment) => enrollment.bot_fingerprint === ownerChallenge.fingerprint
795
+ && enrollment.owner_bound)
796
+ const challengeStatusIndex = paths.indexOf('owner-challenge')
797
+ const challengePending = challengeStatusIndex >= 0 && readable[challengeStatusIndex]
798
+ ? data[challengeStatusIndex].pending : null
799
+ if (ownerChallengeMetaRef.current === ownerChallenge
800
+ && (readable[0] && selectedFingerprint !== ownerChallenge.fingerprint
801
+ || Date.now() >= ownerChallenge.expiresAt
802
+ || (!ownerChallenge.ownerWasBound && nowBound)
803
+ || challengePending === false)) clearOwnerChallenge()
804
+ }
684
805
  setGroups(readable[2] ? data[2] : [])
685
806
  setCards(readable[3] ? data[3] : [])
686
807
  setDefaults(readable[4] ? data[4] : null)
@@ -696,6 +817,9 @@ window.__ModuleLoader__.load({
696
817
  useEffect(() => { void load().catch((error) => setMessage(error.message)) }, [remote, revision])
697
818
  useEffect(() => { setMessage('') }, [activeTab])
698
819
  const selectedBot = bots.find((bot) => bot.bot_id === botId)
820
+ const botPlatformLabel = (bot) => bot?.channel === 'feishu' || !bot?.channel
821
+ ? translateLocaleText('飞书')
822
+ : bot.channel === 'wecom' ? translateLocaleText('企业微信') : translateLocaleText('其他 IM')
699
823
  const botConnected = Boolean(selectedBot?.available && selectedBot?.connected)
700
824
  const selectedEnrollment = enrolled.find((bot) => bot.bot_fingerprint === selectedBot?.fingerprint)
701
825
  const selectedGiteaOrigin = normalizeServiceUrl(selectedEnrollment?.gitea_origin)
@@ -757,6 +881,7 @@ window.__ModuleLoader__.load({
757
881
  const dshAvailable = executors.some((item) => item.id === 'dsh' && item.available)
758
882
  const executorUnavailableReason = executors.find((item) => item.reason)?.reason || null
759
883
  const reviewerLabel = (id) => ({ dsh: 'DSH', codex: 'Codex', 'claude-code': 'Claude Code' })[id] || id
884
+ const usesExternalExecutor = reviewers.some((id) => ['codex', 'claude-code'].includes(id))
760
885
  const cardReviewerSummary = (card) => {
761
886
  const selected = Array.isArray(card.reviewers) && card.reviewers.length ? card.reviewers : [card.runner]
762
887
  const unavailable = selected.filter((runner) => !executors.some((item) => item.id === runner && item.available))
@@ -767,9 +892,12 @@ window.__ModuleLoader__.load({
767
892
  ? translateLocaleText('默认 {executor};交叉审查:{reviewers}',
768
893
  { executor: reviewerLabel(card.runner), reviewers: additional.map(reviewerLabel).join('、') })
769
894
  : translateLocaleText('{executor} 默认执行', { executor: reviewerLabel(card.runner) })
770
- return unavailable.length
895
+ const summary = unavailable.length
771
896
  ? `${selectedSummary};${unavailable.map(reviewerLabel).join('、')} 暂不可用`
772
897
  : selectedSummary
898
+ return card.owner_approval_required !== false
899
+ && selected.some((id) => ['codex', 'claude-code'].includes(id))
900
+ ? translateLocaleText('{summary};启动前需要主人审批', { summary }) : summary
773
901
  }
774
902
  const requesterSummary = senderScope === 'allowlist'
775
903
  ? translateLocaleText('仅指定成员:{member_ids}',
@@ -780,9 +908,8 @@ window.__ModuleLoader__.load({
780
908
  const ruleSummaryCurrent = recognizedDescriptionRef.current === ruleDescription.trim()
781
909
  const ruleSummaryPending = recognitionBusy
782
910
  const ruleSummaryDirty = Boolean(ruleDescription.trim() && !ruleSummaryCurrent)
783
- const ruleTriggerSummary = triggerPhrases.split('\n').map((item) => item.trim()).filter(Boolean)
784
- const ruleSummaryReady = Boolean(ruleSummaryCurrent
785
- && (title || intent || ruleTriggerSummary.length || parameters.length || taskPrompt))
911
+ const ruleSummaryReady = Boolean(!ruleSummaryPending && ruleSummaryCurrent
912
+ && (title || intent || parameters.length || taskPrompt))
786
913
  const missingHarnessInputs = [...new Set([
787
914
  ...missingRequired,
788
915
  ...(parameters.some((rule) => rule.kind === 'gitea_pr'
@@ -815,7 +942,7 @@ window.__ModuleLoader__.load({
815
942
  ? `请求时还需提供:${parameters.map(parameterSummary).join(';')}`
816
943
  : '无需额外参数,按请求中的内容处理'
817
944
  const ruleSummaryNarrative = ruleSummaryReady
818
- ? `${ruleTriggerSummary.length ? `消息以“${ruleTriggerSummary.join('”或“')}”开头时,` : ''}${intent.trim() || title.trim()}${taskPrompt.trim() ? `;${taskPrompt.trim()}` : ''};${recognizedInputsSummary}`
945
+ ? `${intent.trim() || title.trim()}${taskPrompt.trim() ? `;${taskPrompt.trim()}` : ''};${recognizedInputsSummary}`
819
946
  : '识别后会用一段摘要呈现系统理解的任务。'
820
947
  const executorSummary = translateLocaleText(runner === 'codex' ? 'DSH Codex 子 Agent'
821
948
  : runner === 'claude-code' ? 'DSH Claude Code 子 Agent' : 'DSH · 当前授权模型'
@@ -823,13 +950,14 @@ window.__ModuleLoader__.load({
823
950
  const cardBlockReason = enrollmentLoadError ? '暂时无法核对机器人是否已启用协作,请刷新状态后再试。'
824
951
  : !botConnected || !selectedEnrollmentBound ? '先在上方把机器人绑定到这台 DSH。'
825
952
  : loadFailures.includes('settings') || defaults?.task_rule_schema_version !== 3
826
- ? '团队服务端尚未支持任务触发规则与运行时授权核验;请先升级 ANT 服务端。'
953
+ ? '团队服务端尚未支持 AI 任务 Harness 与运行时授权核验;请先升级 ANT 服务端。'
827
954
  : !ruleDescription.trim() ? '请先用自然语言描述要做的任务。'
955
+ : recognitionBusy ? 'AI 正在检查当前描述;分析完成后再开放。'
828
956
  : recognizedDescriptionRef.current !== ruleDescription.trim()
829
- ? recognitionBusy ? 'AI 正在检查当前描述;分析完成后再开放。' : '描述已修改,请点击“立即分析”更新任务理解后再开放。'
957
+ ? '描述已修改,请点击“立即分析”更新任务理解后再开放。'
830
958
  : !title.trim() || !intent.trim() || !taskPrompt.trim() ? '等待 AI 生成任务理解和处理要求。'
831
- : !ruleTriggerSummary.length ? '任务没有明确触发开头;请在描述中写清楚请求消息以什么短语开头,再重新分析。'
832
- : missingHarnessInputs.length ? `任务范围还有必需信息未明确:${missingHarnessInputs.join('、')}。请补充描述。`
959
+ : usesExternalExecutor && ownerApprovalRequired && !ownerApprovalBound
960
+ ? '这项任务需要主人审批,请先绑定审批私聊,或取消“启动前需要主人审批”。'
833
961
  : conversationKind === 'group' && loadFailures.includes('groups') ? '暂时无法核对目标群,请刷新状态后再试。'
834
962
  : conversationKind === 'group' && !groupId ? '请在“请求范围”中选择目标群;若列表为空,让群成员先 @机器人,再刷新群列表。'
835
963
  : senderScope === 'allowlist' && !senderIds.trim() ? '请填写可请求成员的 IM ID。'
@@ -867,16 +995,16 @@ window.__ModuleLoader__.load({
867
995
  setRecognitionMessage('')
868
996
  recognizedBaselineRef.current = null
869
997
  recognizedDescriptionRef.current = null
998
+ cancelRecognitionRequest()
870
999
  recognitionRequestRef.current += 1
871
1000
  setRuleDescription('')
872
- setTriggerPhrases('')
873
1001
  setMissingRequired([]); setSuggestions([]); setRuleExamples([])
874
1002
  setTitle(''); setIntent(''); setTaskPrompt(''); setRunner('dsh')
875
- setReviewers(['dsh'])
1003
+ setReviewers(['dsh']); setOwnerApprovalRequired(true)
876
1004
  setParameters([]); setConversationKind(context.conversationKind || 'dm')
877
1005
  setGroupId(context.groupId || '')
878
1006
  setSenderScope('any'); setSenderIds(''); setDays(7); setCreateOpen(true)
879
- if (context.botId) setBotId(context.botId)
1007
+ if (context.botId) selectBot(context.botId)
880
1008
  }
881
1009
  const taskRuleDraftFromCard = (card, title) => {
882
1010
  const rules = (card.parameters || []).map((rule) => ({
@@ -895,32 +1023,31 @@ window.__ModuleLoader__.load({
895
1023
  else if (headIndex < 0 && rules.length < 12) rules.push(headRule)
896
1024
  }
897
1025
  return { title: title || '', intent: card.intent || '', task_prompt: card.task_prompt || '',
898
- trigger_phrases: Array.isArray(card.trigger_phrases) ? card.trigger_phrases.map((item) => item.trim()).filter(Boolean) : [],
899
1026
  parameters: rules, examples: Array.isArray(card.examples) ? card.examples : [],
900
1027
  missing_required: [], suggestions: [] }
901
1028
  }
902
1029
  const editCard = (card, copy = false) => {
903
1030
  setSubmissionError('')
904
- setBotId(card.bot_id)
1031
+ const cardBotId = card.bot_id || bots.find((bot) => bot.fingerprint === card.bot_fingerprint)?.bot_id || ''
1032
+ selectBot(cardBotId)
905
1033
  setRecognitionMessage('')
1034
+ cancelRecognitionRequest()
906
1035
  recognitionRequestRef.current += 1
907
1036
  setEditingCardId(copy ? null : card.id); setEditingExpiresAt(copy ? null : card.expires_at)
908
1037
  const taskDraft = taskRuleDraftFromCard(card, copy ? `${card.title.slice(0, 94)}(副本)` : card.title)
909
1038
  recognizedBaselineRef.current = taskDraft
910
1039
  const existingDescription = card.rule_description || ''
911
1040
  recognizedDescriptionRef.current = existingDescription.trim() || null
912
- setDraftNotice(!taskDraft.trigger_phrases.length
913
- ? '这项旧任务没有明确触发短语;新版不会按它启动任务。请重新分析描述并检查生成的触发开头。'
914
- : existingDescription.trim() ? ''
915
- : '这项旧任务没有自然语言描述。请补充任务描述,让 AI 生成 Harness 后再开放。')
1041
+ setDraftNotice(existingDescription.trim() ? ''
1042
+ : translateLocaleText('这项旧任务没有自然语言描述。请补充任务描述,让 AI 生成 Harness 后再开放。'))
916
1043
  setRuleDescription(existingDescription)
917
- setTriggerPhrases(taskDraft.trigger_phrases.join('\n'))
918
1044
  setMissingRequired([]); setSuggestions(taskDraft.suggestions); setRuleExamples(taskDraft.examples)
919
1045
  setTitle(taskDraft.title)
920
1046
  if (copy) setDays(7)
921
1047
  setIntent(taskDraft.intent)
922
1048
  setTaskPrompt(taskDraft.task_prompt); setRunner(card.runner)
923
1049
  setReviewers(Array.isArray(card.reviewers) && card.reviewers.length ? card.reviewers : [card.runner])
1050
+ setOwnerApprovalRequired(card.owner_approval_required !== false)
924
1051
  setParameters(taskDraft.parameters)
925
1052
  setConversationKind(card.conversation_kind); setGroupId(card.conversation_id || '')
926
1053
  setSenderScope(card.sender_scope); setSenderIds((card.sender_ids || []).join(', '))
@@ -929,7 +1056,6 @@ window.__ModuleLoader__.load({
929
1056
  const applyRuleDraft = (draft) => {
930
1057
  setTitle(draft.title)
931
1058
  setIntent(draft.intent)
932
- setTriggerPhrases(draft.trigger_phrases.join('\n'))
933
1059
  setTaskPrompt(draft.task_prompt)
934
1060
  setParameters(draft.parameters)
935
1061
  setMissingRequired(draft.missing_required || [])
@@ -942,19 +1068,24 @@ window.__ModuleLoader__.load({
942
1068
  setRecognitionMessage('先描述这项任务何时触发、要做什么以及涉及哪些资料。')
943
1069
  return
944
1070
  }
1071
+ cancelRecognitionRequest()
945
1072
  const requestNumber = ++recognitionRequestRef.current
1073
+ const controller = new AbortController()
1074
+ let timedOut = false
1075
+ recognitionControllerRef.current = controller
1076
+ const timeout = setTimeout(() => { timedOut = true; controller.abort() }, 190_000)
946
1077
  setRecognitionBusy(true); setMessage(''); setRecognitionMessage('AI 正在分析必需范围、可选条件和执行方式…')
947
1078
  try {
948
1079
  const response = await fetch('/api/ant-teams/delegation/recognize', {
949
1080
  method: 'POST', headers: { 'content-type': 'application/json' },
950
1081
  body: JSON.stringify({ description: requestedDescription }),
1082
+ signal: controller.signal,
951
1083
  })
952
1084
  const result = await readLocalJsonResponse(response)
953
1085
  if (!response.ok) throw new Error(result.message || '规则识别失败。')
954
1086
  const candidate = {
955
1087
  title: typeof result.title === 'string' ? result.title : '',
956
1088
  intent: typeof result.intent === 'string' ? result.intent : '',
957
- trigger_phrases: Array.isArray(result.trigger_phrases) ? result.trigger_phrases : [],
958
1089
  task_prompt: typeof result.task_prompt === 'string' ? result.task_prompt : '',
959
1090
  missing_required: Array.isArray(result.missing_required) ? result.missing_required : [],
960
1091
  suggestions: Array.isArray(result.suggestions) ? result.suggestions : [],
@@ -987,8 +1118,8 @@ window.__ModuleLoader__.load({
987
1118
  }
988
1119
  }
989
1120
  if (requestNumber !== recognitionRequestRef.current || requestedDescription !== ruleDescription.trim()) return
990
- if (!candidate.title || !candidate.intent || !candidate.task_prompt || !candidate.trigger_phrases.length) {
991
- throw new Error('AI 返回的任务理解或触发开头不完整。请补充描述后重试。')
1121
+ if (!candidate.title || !candidate.intent || !candidate.task_prompt) {
1122
+ throw new Error('AI 返回的任务理解不完整。请补充描述后重试。')
992
1123
  }
993
1124
  const previous = recognizedBaselineRef.current
994
1125
  applyRuleDraft(candidate)
@@ -998,8 +1129,11 @@ window.__ModuleLoader__.load({
998
1129
  ? 'AI 已更新任务理解。请复核必需信息、可选建议和运行方式;任务仍未开放。'
999
1130
  : 'AI 已生成待复核的任务 Harness。检查摘要和范围后再开放。')
1000
1131
  } catch (error) {
1001
- if (requestNumber === recognitionRequestRef.current) setRecognitionMessage(error.message)
1132
+ if (requestNumber === recognitionRequestRef.current) setRecognitionMessage(timedOut
1133
+ ? translateLocaleText('AI 分析等待超时,请检查团队模型后重试。') : error.message)
1002
1134
  } finally {
1135
+ clearTimeout(timeout)
1136
+ if (recognitionControllerRef.current === controller) recognitionControllerRef.current = null
1003
1137
  if (requestNumber === recognitionRequestRef.current) setRecognitionBusy(false)
1004
1138
  }
1005
1139
  }
@@ -1033,17 +1167,33 @@ window.__ModuleLoader__.load({
1033
1167
  }
1034
1168
  const requestOwnerApprovalBinding = async () => {
1035
1169
  if (!selectedBot || !selectedEnrollmentBound) return
1036
- setBusy(true); setOwnerChallengeCode('')
1170
+ const requestedBot = selectedBot
1171
+ setBusy(true); clearOwnerChallenge()
1037
1172
  try {
1038
1173
  const response = await fetch('/api/ant-teams/delegation/owner-challenge', {
1039
1174
  method: 'POST', headers: { 'content-type': 'application/json' },
1040
- body: JSON.stringify({ bot_id: selectedBot.bot_id, bot_fingerprint: selectedBot.fingerprint }),
1175
+ body: JSON.stringify({ bot_id: requestedBot.bot_id, bot_fingerprint: requestedBot.fingerprint }),
1041
1176
  })
1042
1177
  const result = await readLocalJsonResponse(response)
1178
+ if (botIdRef.current !== requestedBot.bot_id) {
1179
+ await fetch(`/api/ant-teams/delegation/owner-challenge?bot_fingerprint=${encodeURIComponent(requestedBot.fingerprint)}`,
1180
+ { method: 'DELETE', cache: 'no-store' }).catch(() => null)
1181
+ return
1182
+ }
1043
1183
  if (!response.ok || typeof result.code !== 'string') throw new Error(result.message || '审批私聊绑定码获取失败。')
1184
+ // The server owns challenge validity. Use a response-relative local display window so clock skew
1185
+ // cannot make a valid server challenge disappear before the owner can use it.
1186
+ const expiresAt = Date.now() + 5 * 60_000
1187
+ const ownerChallenge = { fingerprint: requestedBot.fingerprint, ownerWasBound: ownerApprovalBound, expiresAt }
1188
+ ownerChallengeMetaRef.current = ownerChallenge
1044
1189
  setOwnerChallengeCode(result.code)
1045
- setMessage(`请在 5 分钟内私聊此机器人,发送“绑定审批身份 ${result.code}”。绑定成功后,它才能把未配置任务的审批请求发给你。`)
1046
- } catch (error) { setMessage(error.message) }
1190
+ ownerChallengeTimerRef.current = setTimeout(() => {
1191
+ if (ownerChallengeMetaRef.current !== ownerChallenge) return
1192
+ clearOwnerChallenge()
1193
+ setMessage(translateLocaleText('绑定码已过期,请重新生成。'))
1194
+ }, Math.max(0, expiresAt - Date.now()))
1195
+ setMessage('绑定码已生成;请按“私聊入口”中的提示完成绑定。')
1196
+ } catch (error) { if (botIdRef.current === requestedBot.bot_id) setMessage(error.message) }
1047
1197
  finally { setBusy(false) }
1048
1198
  }
1049
1199
  const saveGroupChat = async (enabled, targetGroupId) => {
@@ -1140,6 +1290,15 @@ window.__ModuleLoader__.load({
1140
1290
  ...collaborationGridStyle, gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 420px), 1fr))',
1141
1291
  }
1142
1292
  const collaborationCardStyle = { ...settingsPanelStyle, padding: 12, gap: 10 }
1293
+ const capabilityStepStyle = { display: 'grid', gap: 12, borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: 14 }
1294
+ const capabilityStepHeading = (number, title, description) => h('div', { style: {
1295
+ display: 'flex', gap: 10, alignItems: 'flex-start',
1296
+ } },
1297
+ h('span', { style: { display: 'grid', placeItems: 'center', width: 26, height: 26, flex: '0 0 auto',
1298
+ borderRadius: '50%', backgroundColor: 'var(--dsw-alias-state-business-tertiary)',
1299
+ color: 'var(--dsw-alias-state-business-primary)', fontWeight: 700 } }, number),
1300
+ h('div', { style: { display: 'grid', gap: 3 } },
1301
+ h('strong', null, title), h('p', { style: mutedTextStyle }, description)))
1143
1302
  const setupStepper = h('section', { 'aria-label': '首次设置引导', style: { display: 'grid', gap: 6 } },
1144
1303
  h('p', { style: mutedTextStyle }, '首次设置可按顺序完成;你也可以直接切换页面。普通聊天不需要先开放任务。'),
1145
1304
  h('ol', { 'aria-label': '设置步骤', style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 150px), 1fr))',
@@ -1172,7 +1331,26 @@ window.__ModuleLoader__.load({
1172
1331
  disabled: busy || !selectedBot || !selectedEnrollmentBound || enrollmentLoadError || !dmChatSettingAvailable,
1173
1332
  onChange: (event) => void saveDmChat(event.target.checked), 'aria-label': '允许机器人私聊' }),
1174
1333
  h('strong', null, '允许私聊普通聊天'), h('span', { style: mutedTextStyle }, dmChatStatus)),
1175
- h('p', { style: mutedTextStyle }, dmChatDescription))
1334
+ h('p', { style: mutedTextStyle }, dmChatDescription),
1335
+ selectedEnrollmentBound && botConnected ? h('section', { role: 'group', 'aria-label': '主人审批私聊', style: {
1336
+ display: 'grid', gap: 9, padding: 12, border: '1px solid var(--dsw-alias-state-business-tertiary)',
1337
+ borderRadius: 10, backgroundColor: 'var(--dsw-alias-state-business-tertiary)',
1338
+ } },
1339
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8 } },
1340
+ h('strong', null, '主人审批私聊'),
1341
+ h('span', { style: { ...mutedTextStyle, padding: '3px 8px', borderRadius: 999,
1342
+ backgroundColor: 'var(--dsw-alias-bg-layer-1)' } }, ownerApprovalBound ? '已绑定' : '未绑定')),
1343
+ h('p', { style: mutedTextStyle }, ownerApprovalBound
1344
+ ? '没有匹配任务卡的明确任务会先发给你逐次审批;批准前不会启动执行器。'
1345
+ : '绑定一次后,没有匹配任务卡的明确任务会先发给你审批。发送验证码后,点击“刷新状态”确认绑定。'),
1346
+ h('div', { style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' } },
1347
+ h('button', { type: 'button', disabled: busy,
1348
+ onClick: () => void requestOwnerApprovalBinding(), style: secondaryButtonStyle },
1349
+ ownerApprovalBound ? '重新绑定审批私聊' : '生成绑定码'),
1350
+ ownerChallengeCode ? h('span', { style: mutedTextStyle }, '有效期 5 分钟') : null),
1351
+ ownerChallengeCode ? h('div', { role: 'status', style: { ...feedbackNoticeStyle, position: 'static' } },
1352
+ translateLocaleText('请在 5 分钟内私聊此机器人发送完整指令:“绑定审批身份 {code}”。如果这台 DSH 未重启,也可直接发送 12 位验证码 {code}。',
1353
+ { code: ownerChallengeCode })) : null) : null)
1176
1354
  const createCard = async (event) => {
1177
1355
  event.preventDefault()
1178
1356
  setDraftNotice('')
@@ -1193,11 +1371,6 @@ window.__ModuleLoader__.load({
1193
1371
  || bytes(intent.trim()) > 1000 || bytes(taskPrompt.trim()) > 4000) {
1194
1372
  reject('任务描述或识别摘要过长,请缩短描述并重新识别。'); return
1195
1373
  }
1196
- const triggerLines = triggerPhrases.split('\n').map((item) => item.normalize('NFKC').trim()).filter(Boolean)
1197
- if (!triggerLines.length || triggerLines.length > 8 || triggerLines.some((item) => bytes(item) > 120)
1198
- || new Set(triggerLines.map((item) => item.toLowerCase())).size !== triggerLines.length) {
1199
- reject('任务没有明确、唯一的触发开头;请在描述中写清楚消息以什么短语开头,再重新分析。'); return
1200
- }
1201
1374
  const normalized = parameters.map((rule) => ({ name: rule.name.trim(), kind: rule.kind,
1202
1375
  description: rule.description.trim() || null,
1203
1376
  required: rule.required, min: rule.min == null || rule.min === '' ? null : Number(rule.min),
@@ -1261,8 +1434,6 @@ window.__ModuleLoader__.load({
1261
1434
  : translateLocaleText('私聊')
1262
1435
  const resultDestination = translateLocaleText(conversationKind === 'group'
1263
1436
  ? '原群(群内可见)' : '发起任务的私聊')
1264
- const triggerSummary = translateLocaleText('只有请求以“{triggers}”开头时才会触发此任务',
1265
- { triggers: triggerLines.join('”或“') })
1266
1437
  const executionDetails = {
1267
1438
  dsh: translateLocaleText('DSH 使用获准的团队模型处理对话内容和 ANT 已核验后提供的资源。'),
1268
1439
  codex: translateLocaleText('Codex 使用这台 DSH 的 Provider Profile 权限;可访问范围由 Codex/DSH 本机配置决定。任务描述不会限制操作系统权限。'),
@@ -1274,10 +1445,10 @@ window.__ModuleLoader__.load({
1274
1445
  details: reviewers.map((item) => executionDetails[item]).join(' ') })
1275
1446
  : executionDetails[reviewers[0]]
1276
1447
  const actionDescription = editingCardId
1277
- ? translateLocaleText('更新“{title}”后,{requesterSummary}可在{scope}{triggerSummary}。旧待确认请求将失效,已提交任务继续使用提交时的配置。结果{resultDestination}。到期时间保持为 {expiry}。',
1278
- { title, requesterSummary, scope, triggerSummary, resultDestination, expiry: formatLocaleDate(editingExpiresAt * 1000) })
1279
- : translateLocaleText('开放“{title}”后,{requesterSummary}可在{scope}{triggerSummary}有效期 {days} 天;结果{resultDestination}。',
1280
- { title, requesterSummary, scope, triggerSummary, resultDestination, days: formatLocaleNumber(days) })
1448
+ ? translateLocaleText('更新“{title}”后,{requesterSummary}可在{scope}按任务描述中的规则发起请求。旧待确认请求将失效,已提交任务继续使用提交时的配置。结果{resultDestination}。到期时间保持为 {expiry}。',
1449
+ { title, requesterSummary, scope, resultDestination, expiry: formatLocaleDate(editingExpiresAt * 1000) })
1450
+ : translateLocaleText('开放“{title}”后,{requesterSummary}可在{scope}按任务描述中的规则发起请求,有效期 {days} 天;结果{resultDestination}。',
1451
+ { title, requesterSummary, scope, resultDestination, days: formatLocaleNumber(days) })
1281
1452
  if (!confirmLocalized(`${actionDescription}${executionInfo}${translateLocaleText('请确认。')}`)) return
1282
1453
  setBusy(true)
1283
1454
  try {
@@ -1285,9 +1456,10 @@ window.__ModuleLoader__.load({
1285
1456
  method: editingCardId ? 'PUT' : 'POST', headers: { 'content-type': 'application/json' },
1286
1457
  body: JSON.stringify({ ...(editingCardId ? { id: editingCardId } : {}),
1287
1458
  bot_id: selectedBot.bot_id, bot_fingerprint: selectedBot.fingerprint,
1288
- title, rule_description: ruleDescription.trim(), trigger_phrases: triggerLines,
1459
+ title, rule_description: ruleDescription.trim(), trigger_phrases: [],
1289
1460
  intent, examples: ruleExamples,
1290
- parameters: normalized, task_prompt: taskPrompt, runner, reviewers, workspace: null,
1461
+ parameters: normalized, task_prompt: taskPrompt, runner, reviewers,
1462
+ owner_approval_required: ownerApprovalRequired, workspace: null,
1291
1463
  sender_scope: senderScope, sender_ids: senderScope === 'allowlist'
1292
1464
  ? senderIds.split(/[\s,,]+/u).filter(Boolean) : [], conversation_kind: conversationKind,
1293
1465
  ...(conversationKind === 'group' ? { conversation_id: groupId } : {}),
@@ -1295,7 +1467,7 @@ window.__ModuleLoader__.load({
1295
1467
  })
1296
1468
  const result = await readLocalJsonResponse(response)
1297
1469
  if (!response.ok && result.code === 'IM_TASK_RULE_REQUIRED') {
1298
- throw new Error('当前 ANT 服务端不支持任务触发规则,请升级服务端后再开放任务。')
1470
+ throw new Error('当前 ANT 服务端版本过旧,请升级服务端后再开放任务。')
1299
1471
  }
1300
1472
  if (!response.ok && reviewers.includes('claude-code') && result.code === 'INVALID_REQUEST') {
1301
1473
  throw new Error('当前团队服务端可能尚不支持 Claude Code 审查配置。请管理员升级服务端后刷新重试。')
@@ -1303,24 +1475,35 @@ window.__ModuleLoader__.load({
1303
1475
  if (!response.ok) throw new Error(result.message || '能力保存失败。')
1304
1476
  const savedReviewers = Array.isArray(result.reviewers) && result.reviewers.length
1305
1477
  ? result.reviewers : [result.runner]
1306
- if ([...savedReviewers].sort().join('\0') !== [...reviewers].sort().join('\0')) {
1307
- if (!editingCardId && typeof result.id === 'string') {
1478
+ const reviewersDropped = [...savedReviewers].sort().join('\0') !== [...reviewers].sort().join('\0')
1479
+ const approvalDropped = usesExternalExecutor && ownerApprovalRequired && result.owner_approval_required !== true
1480
+ if (reviewersDropped || approvalDropped) {
1481
+ if ((!editingCardId || approvalDropped) && typeof result.id === 'string') {
1308
1482
  const cleanup = await fetch('/api/ant-teams/delegation/cards', {
1309
1483
  method: 'DELETE', headers: { 'content-type': 'application/json' },
1310
1484
  body: JSON.stringify({ id: result.id }),
1311
1485
  }).catch(() => null)
1312
1486
  if (!cleanup?.ok) {
1313
1487
  await load().catch(() => undefined)
1314
- throw new Error('团队服务端尚不支持多 Agent 审查配置;旧服务端可能已保存单 Agent 授权,请刷新列表并停止它,再升级 ANT 服务端。')
1488
+ throw new Error(reviewersDropped
1489
+ ? '团队服务端尚不支持多 Agent 审查配置;旧服务端可能已保存单 Agent 授权,请刷新列表并停止它,再升级 ANT 服务端。'
1490
+ : '团队服务端尚不支持任务卡主人审批;旧服务端可能已保存不经审批的授权,请刷新列表并停止它,再升级 ANT 服务端。')
1491
+ }
1492
+ if (editingCardId) {
1493
+ setEditingCardId(null); setEditingExpiresAt(null)
1494
+ await load().catch(() => undefined)
1315
1495
  }
1316
1496
  }
1317
- throw new Error('当前团队服务端尚不支持多 Agent 审查配置;请管理员升级 ANT 服务端后重试。')
1497
+ throw new Error(reviewersDropped
1498
+ ? '当前团队服务端尚不支持多 Agent 审查配置;请管理员升级 ANT 服务端后重试。'
1499
+ : '当前团队服务端尚不支持任务卡主人审批;请管理员升级 ANT 服务端后重试。')
1318
1500
  }
1319
1501
  recognizedBaselineRef.current = null
1320
1502
  recognizedDescriptionRef.current = null
1503
+ cancelRecognitionRequest()
1321
1504
  recognitionRequestRef.current += 1
1322
1505
  setRuleDescription(''); setMissingRequired([]); setSuggestions([]); setRuleExamples([])
1323
- setTitle(''); setIntent(''); setTaskPrompt(''); setParameters([]); setRunner('dsh'); setReviewers(['dsh'])
1506
+ setTitle(''); setIntent(''); setTaskPrompt(''); setParameters([]); setRunner('dsh'); setReviewers(['dsh']); setOwnerApprovalRequired(true)
1324
1507
  setEditingCardId(null); setEditingExpiresAt(null)
1325
1508
  await load()
1326
1509
  setCreateOpen(false)
@@ -1347,29 +1530,28 @@ window.__ModuleLoader__.load({
1347
1530
  setEditingCardId(null); setEditingExpiresAt(null)
1348
1531
  const copiedDescription = card.rule_description || ''
1349
1532
  recognizedDescriptionRef.current = copiedDescription.trim() || null
1533
+ cancelRecognitionRequest()
1350
1534
  recognitionRequestRef.current += 1
1351
- const hasTriggerRule = Array.isArray(card.trigger_phrases) && card.trigger_phrases.length > 0
1352
- setDraftNotice(!hasTriggerRule
1353
- ? '已复制旧规则。它没有明确触发短语;请重新分析并检查生成的触发开头。'
1354
- : copiedDescription.trim()
1355
- ? '已复制到新草稿。复制不保留原有效期;请检查任务描述和请求范围。'
1356
- : '这项旧任务没有自然语言描述。请补充任务描述,让 AI 生成 Harness 后再开放。')
1535
+ setDraftNotice(copiedDescription.trim()
1536
+ ? translateLocaleText('已复制到新草稿。复制不保留原有效期;请检查任务描述和请求范围。')
1537
+ : translateLocaleText('这项旧任务没有自然语言描述。请补充任务描述,让 AI 生成 Harness 后再开放。'))
1357
1538
  const cardBot = bots.find((bot) => bot.fingerprint === card.bot_fingerprint)
1539
+ const copiedBotId = card.bot_id || cardBot?.bot_id || ''
1540
+ selectBot(copiedBotId)
1358
1541
  const titleChars = Array.from(translateLocaleText('复制:{title}', {
1359
1542
  title: card.title || translateLocaleText('未命名能力'),
1360
1543
  }))
1361
1544
  while (new TextEncoder().encode(titleChars.join('')).length > 100) titleChars.pop()
1362
1545
  const taskDraft = taskRuleDraftFromCard(card, titleChars.join(''))
1363
1546
  recognizedBaselineRef.current = taskDraft
1364
- setBotId(card.bot_id || cardBot?.bot_id || '')
1365
1547
  setRuleDescription(copiedDescription)
1366
- setTriggerPhrases(taskDraft.trigger_phrases.join('\n'))
1367
1548
  setMissingRequired([]); setSuggestions(taskDraft.suggestions); setRuleExamples(taskDraft.examples)
1368
1549
  setTitle(taskDraft.title)
1369
1550
  setIntent(taskDraft.intent)
1370
1551
  setTaskPrompt(taskDraft.task_prompt)
1371
1552
  setRunner(['dsh', 'codex', 'claude-code'].includes(card.runner) ? card.runner : 'dsh')
1372
1553
  setReviewers(Array.isArray(card.reviewers) && card.reviewers.length ? card.reviewers : [card.runner || 'dsh'])
1554
+ setOwnerApprovalRequired(card.owner_approval_required !== false)
1373
1555
  setParameters(taskDraft.parameters)
1374
1556
  setConversationKind(card.conversation_kind === 'group' ? 'group' : 'dm')
1375
1557
  setGroupId(card.conversation_id || '')
@@ -1399,10 +1581,10 @@ window.__ModuleLoader__.load({
1399
1581
  loading ? '读取中…' : '刷新状态')),
1400
1582
  h('p', { role: 'status', style: mutedTextStyle }, collaborationStatus),
1401
1583
  bots.length ? h('label', null, '我的机器人', h('select', { id: 'ant-bot-selector', value: botId,
1402
- onChange: (event) => { setBotId(event.target.value); setGroupId('') }, style: inputStyle },
1584
+ onChange: (event) => { selectBot(event.target.value); setGroupId('') }, style: inputStyle },
1403
1585
  h('option', { value: '' }, '请选择机器人'),
1404
1586
  ...bots.map((bot) => h('option', { key: bot.bot_id, value: bot.bot_id,
1405
- disabled: !bot.available || !bot.connected }, rawLocaleText(bot.name),
1587
+ disabled: !bot.available || !bot.connected }, rawLocaleText(bot.name), ' · ', botPlatformLabel(bot),
1406
1588
  bot.available && bot.connected ? null : '(未连接)')))) : null,
1407
1589
  connectedBots.length === 0 && !loading && !botLoadError ? h('div', { style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' } },
1408
1590
  h('button', { type: 'button', onClick: openImSettings, style: secondaryButtonStyle }, '打开 DSH-IM 设置'),
@@ -1412,15 +1594,8 @@ window.__ModuleLoader__.load({
1412
1594
  selectedBot && !selectedEnrollmentBound ? h('button', { type: 'button', disabled: busy || !botConnected || enrollmentLoadError,
1413
1595
  onClick: () => void enrollBot(), style: primaryButtonStyle },
1414
1596
  selectedEnrollment ? '绑定原协作配置到这台 DSH' : '启用并绑定到这台 DSH') : null,
1415
- selectedEnrollmentBound && botConnected ? h('div', { style: { display: 'grid', gap: 6 } },
1416
- h('p', { style: mutedTextStyle }, ownerApprovalBound
1417
- ? '审批私聊已绑定;没有匹配任务卡的明确任务会发给你逐次审批。'
1418
- : '绑定一次你的 IM 私聊身份后,就能审批没有匹配任务卡的明确任务。'),
1419
- h('button', { type: 'button', disabled: busy,
1420
- onClick: () => void requestOwnerApprovalBinding(), style: secondaryButtonStyle },
1421
- ownerApprovalBound ? '重新绑定审批私聊' : '绑定审批私聊'),
1422
- ownerChallengeCode ? h('p', { role: 'status', style: feedbackNoticeStyle },
1423
- `请在 5 分钟内私聊此机器人发送:绑定审批身份 ${ownerChallengeCode}`) : null) : null)
1597
+ selectedEnrollmentBound && botConnected ? h('p', { style: mutedTextStyle },
1598
+ '任务能力和设备绑定状态请在此处查看;主人审批身份在“私聊入口”中管理。') : null)
1424
1599
  const groupTaskStatusPanel = selectedEnrollmentBound && botConnected ? h('section', {
1425
1600
  id: 'ant-group-task-status', role: 'region', 'aria-label': '各群聊天与任务状态', tabIndex: -1,
1426
1601
  style: { ...collaborationCardStyle, gap: 10 } },
@@ -1465,7 +1640,8 @@ window.__ModuleLoader__.load({
1465
1640
  h('h3', { style: sectionTitleStyle }, '协作总览'),
1466
1641
  h('p', { style: mutedTextStyle }, '选择机器人并绑定一次主人审批私聊。任务卡可预授权常用任务;其他请求由主人逐次决定。')),
1467
1642
  h('span', { style: { ...mutedTextStyle, padding: '4px 9px', borderRadius: 999,
1468
- backgroundColor: 'var(--dsw-alias-bg-layer-2)' } }, selectedBot ? selectedBot.name : '尚未选择机器人')),
1643
+ backgroundColor: 'var(--dsw-alias-bg-layer-2)' } }, selectedBot
1644
+ ? h('span', null, rawLocaleText(selectedBot.name), ' · ', botPlatformLabel(selectedBot)) : '尚未选择机器人')),
1469
1645
  loadFailures.length ? h('p', { role: 'status', style: mutedTextStyle },
1470
1646
  `部分协作信息暂时无法读取:${loadFailures.map((path) => loadNames[path]).join('、')}。已读取的信息仍显示,请稍后刷新状态。`) : null,
1471
1647
  h('div', { id: 'ant-collaboration-settings-grid', style: collaborationSettingsGridStyle },
@@ -1501,14 +1677,12 @@ window.__ModuleLoader__.load({
1501
1677
  : translateLocaleText('私聊')
1502
1678
  const requester = translateLocaleText(card.sender_scope === 'allowlist' ? '指定成员可请求'
1503
1679
  : card.conversation_kind === 'group' ? '群成员可请求' : '可私聊者可请求')
1504
- const missingTriggerPhrase = !Array.isArray(card.trigger_phrases) || card.trigger_phrases.length === 0
1505
1680
  const configuredCardRunners = Array.isArray(card.reviewers) && card.reviewers.length
1506
1681
  ? card.reviewers : [card.runner]
1507
1682
  const blockedExternalExecutor = !configuredCardRunners.every((runner) =>
1508
1683
  executors.some((executor) => executor.id === runner && executor.available))
1509
1684
  const examplesText = Array.isArray(card.examples) ? card.examples.join('、') : ''
1510
1685
  const requesterIds = card.sender_scope === 'allowlist' && Array.isArray(card.sender_ids) ? card.sender_ids.join('、') : ''
1511
- const triggerSummary = Array.isArray(card.trigger_phrases) ? card.trigger_phrases.join('、') : ''
1512
1686
  const remainingDaysLabel = remainingDays <= 7 ? 'var(--dsw-alias-state-warning-primary, var(--dsw-alias-label-secondary))'
1513
1687
  : 'var(--dsw-alias-label-secondary)'
1514
1688
  return h('article', { key: card.id, style: { ...collaborationCardStyle, padding: 12, gap: 8 } },
@@ -1526,9 +1700,6 @@ window.__ModuleLoader__.load({
1526
1700
  blockedExternalExecutor ? h('p', { role: 'status', style: { ...mutedTextStyle,
1527
1701
  color: 'var(--dsw-alias-state-warning-primary, var(--dsw-alias-label-secondary))' } },
1528
1702
  '此任务暂不可执行:这台 DSH 尚未启用所选的全部执行器。请启用对应的 DSH 子 Agent 提供方后刷新。') : null,
1529
- missingTriggerPhrase ? h('p', { role: 'status', style: { ...mutedTextStyle,
1530
- color: 'var(--dsw-alias-state-warning-primary, var(--dsw-alias-label-secondary))' } },
1531
- '这项旧任务没有明确触发开头,不会自动启动。点击“编辑”,重新分析并复核触发短语。') : null,
1532
1703
  card.rule_description
1533
1704
  ? h('span', { style: { ...mutedTextStyle, overflowWrap: 'anywhere' } }, rawLocaleText(card.rule_description))
1534
1705
  : h('span', { role: 'status', style: { ...mutedTextStyle, color: 'var(--dsw-alias-state-warning-primary, var(--dsw-alias-label-secondary))' } },
@@ -1537,7 +1708,6 @@ window.__ModuleLoader__.load({
1537
1708
  h('summary', { style: { cursor: 'pointer', color: 'var(--dsw-alias-link)', fontSize: 13 } }, '查看规则说明'),
1538
1709
  h('div', { style: { display: 'grid', gap: 4, paddingTop: 6 } },
1539
1710
  card.rule_description ? h('p', { style: { ...mutedTextStyle, overflowWrap: 'anywhere' } }, rawLocaleText(card.rule_description)) : null,
1540
- triggerSummary ? h('p', { style: { ...mutedTextStyle, overflowWrap: 'anywhere' } }, '触发开头:', rawLocaleText(triggerSummary)) : null,
1541
1711
  requesterIds ? h('p', { style: { ...mutedTextStyle, overflowWrap: 'anywhere' } }, '允许请求的 IM 用户:', rawLocaleText(requesterIds)) : null,
1542
1712
  examplesText ? h('p', { style: mutedTextStyle }, '请求示例:', rawLocaleText(examplesText)) : null,
1543
1713
  Array.isArray(card.parameters) && card.parameters.length
@@ -1568,9 +1738,10 @@ window.__ModuleLoader__.load({
1568
1738
  ? task.repository
1569
1739
  ? translateLocaleText('{repository} PR #{pr_number} 差异审查', { repository: task.repository, pr_number: task.pr_number })
1570
1740
  : translateLocaleText('PR #{pr_number} 差异审查', { pr_number: task.pr_number })
1571
- : '任务'
1741
+ : translateLocaleText('任务')
1572
1742
  const status = translateLocaleText(task.outcome === 'canceled' ? '已取消' : task.outcome === 'failed' ? '未完成' : ({
1573
- pending: '排队中', running: '处理中', reply_pending: '结果待发送', reply_attempted: '回复状态待核对', done: '已回复',
1743
+ pending: '排队中', claimed: '排队中', running: '处理中', finishing: '结果待核对', completed: '已完成',
1744
+ reply_pending: '结果待发送', reply_attempted: '回复状态待核对', done: '已回复',
1574
1745
  }[task.status] || task.status))
1575
1746
  const createdAt = task.created_at && Number.isFinite(Date.parse(task.created_at))
1576
1747
  ? formatLocaleDate(task.created_at) : ''
@@ -1584,7 +1755,9 @@ window.__ModuleLoader__.load({
1584
1755
  h('div', { style: mutedTextStyle }, taskStatusLine),
1585
1756
  task.failure_reason ? h('div', { role: 'alert', style: warningNoticeStyle },
1586
1757
  h('span', { style: { ...mutedTextStyle, whiteSpace: 'pre-line' } }, translateLocaleText(
1587
- '任务未完成:{reason}\n处理建议:按错误提示修复配置或请求内容后,在原群或私聊重新发起。',
1758
+ task.status === 'finishing'
1759
+ ? '结果状态待确认:{reason}\n处理建议:恢复原团队连接或审批私聊权限后刷新状态;不要重复发起相同任务。'
1760
+ : '任务未完成:{reason}\n处理建议:按错误提示修复配置或请求内容后,在原群或私聊重新发起。',
1588
1761
  { reason: task.failure_reason }))) : null,
1589
1762
  task.delivery_error ? h('div', { role: 'alert', style: warningNoticeStyle },
1590
1763
  h('span', { style: { ...mutedTextStyle, whiteSpace: 'pre-line' } }, translateLocaleText(
@@ -1593,7 +1766,8 @@ window.__ModuleLoader__.load({
1593
1766
  task.status === 'reply_attempted' && !task.delivery_error ? h('div', { role: 'status', style: warningNoticeStyle },
1594
1767
  h('span', { style: mutedTextStyle }, translateLocaleText(
1595
1768
  'ANT 尚未确认消息平台是否收到结果。请先检查原群或私聊,再决定是否重新发起。'))) : null),
1596
- ['card_task', 'legacy_pr_task'].includes(task.kind) && ['pending', 'running'].includes(task.status) ? h('button', { type: 'button', disabled: busy,
1769
+ ['card_task', 'legacy_pr_task', 'owner_approval'].includes(task.kind)
1770
+ && ['pending', 'claimed', 'running'].includes(task.status) ? h('button', { type: 'button', disabled: busy,
1597
1771
  onClick: () => void cancelTask(task), style: secondaryButtonStyle }, '取消任务') : null)
1598
1772
  })
1599
1773
  : [h('p', { key: 'none', style: mutedTextStyle }, loadFailures.includes('status')
@@ -1601,11 +1775,21 @@ window.__ModuleLoader__.load({
1601
1775
  h('div', { role: 'region', 'aria-label': '任务规则编辑器', style: { display: activeTab === 'collaboration' ? 'grid' : 'none' } },
1602
1776
  h('details', { id: 'ant-new-task', open: createOpen,
1603
1777
  onToggle: (event) => setCreateOpen(event.currentTarget.open),
1604
- style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, padding: 12 } },
1605
- h('summary', { id: 'ant-new-task-summary', style: { cursor: 'pointer', fontWeight: 600 } }, editingCardId ? '编辑能力配置' : '开放一项能力'),
1778
+ style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 12, padding: 16,
1779
+ backgroundColor: 'var(--dsw-alias-bg-layer-1)' } },
1780
+ h('summary', { id: 'ant-new-task-summary', style: { display: 'flex', justifyContent: 'space-between',
1781
+ alignItems: 'center', gap: 12, flexWrap: 'wrap', cursor: 'pointer', listStylePosition: 'inside' } },
1782
+ h('span', { 'aria-hidden': 'true', style: { color: 'var(--dsw-alias-label-secondary)', flex: '0 0 auto' } },
1783
+ createOpen ? '▾' : '▸'),
1784
+ h('span', { style: { display: 'grid', gap: 3 } },
1785
+ h('strong', null, editingCardId ? '编辑能力配置' : '开放一项能力'),
1786
+ h('span', { style: mutedTextStyle }, '任务卡可预授权常用任务;其他明确任务由主人逐次审批。')),
1787
+ h('span', { style: { ...mutedTextStyle, padding: '3px 8px', borderRadius: 999,
1788
+ backgroundColor: 'var(--dsw-alias-bg-layer-2)' } }, editingCardId
1789
+ ? translateLocaleText('编辑中 · 尚未保存') : translateLocaleText('草稿 · 未开放'))),
1606
1790
  h('div', { style: { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr)',
1607
- gap: 12, alignItems: 'start', marginTop: 12 } },
1608
- h('form', { id: 'ant-capability-create-form', onSubmit: createCard, style: { display: 'grid', gap: 9, minWidth: 0 } },
1791
+ gap: 12, alignItems: 'start', marginTop: 14 } },
1792
+ h('form', { id: 'ant-capability-create-form', onSubmit: createCard, style: { display: 'grid', gap: 15, minWidth: 0 } },
1609
1793
  editingCardId ? h('p', { role: 'status', style: mutedTextStyle },
1610
1794
  `保存会生成新的授权版本并停止旧版本;到期时间保持不变:${formatLocaleDate(editingExpiresAt * 1000)}。已提交任务继续使用原配置。`) : null,
1611
1795
  h('p', { style: mutedTextStyle }, selectedBot
@@ -1613,55 +1797,76 @@ window.__ModuleLoader__.load({
1613
1797
  : '请先在上方选择机器人并启用协作。'),
1614
1798
  draftNotice ? h('p', { role: 'status', style: { ...mutedTextStyle,
1615
1799
  color: 'var(--dsw-alias-state-warning-primary, var(--dsw-alias-label-secondary))' } }, draftNotice) : null,
1800
+ !ruleDescription.trim() && draftNotice
1801
+ === translateLocaleText('这项旧任务没有自然语言描述。请补充任务描述,让 AI 生成 Harness 后再开放。') && title
1802
+ ? h('p', { role: 'status', style: mutedTextStyle }, translateLocaleText('已复制任务标题:{title}', {
1803
+ title,
1804
+ })) : null,
1616
1805
  selectedEnrollmentBound ? h('p', { style: mutedTextStyle },
1617
1806
  '机器人及其任务能力只在这台 DSH 设备接收和执行;同一账号的其他设备不会继承本机授权。') : null,
1618
1807
  selectedEnrollment && !selectedEnrollmentBound ? h('p', { style: mutedTextStyle },
1619
1808
  '这是尚未绑定到设备的机器人。请在要继续使用它的电脑重新启用;原协作配置会保留。若旧设备访问曾被撤销,原任务权限不会恢复,需要重新开放。') : null,
1620
1809
  selectedBot && conversationKind === 'group' && !selectedBot.history_available ? h('p', { role: 'status', style: mutedTextStyle },
1621
1810
  '这个机器人当前不能读取平台群历史;群聊只能参考实际送达的消息和本机近期记录。请检查 DSH-IM 权限。') : null,
1622
- h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 320px), 1fr))',
1623
- gap: 12, alignItems: 'start' } },
1624
- h('div', { style: { display: 'grid', gap: 9, minWidth: 0 } },
1625
- h('label', null, '描述规则', h('textarea', { required: true, maxLength: 4000, rows: 4, value: ruleDescription,
1626
- onChange: (event) => {
1627
- recognitionRequestRef.current += 1
1628
- setRuleDescription(event.target.value)
1629
- setRecognitionBusy(false)
1630
- setRecognitionMessage(event.target.value.trim() ? '描述已更新,点击“立即分析”检查任务。' : '')
1631
- },
1632
- placeholder: '用日常语言说明要完成什么、可处理哪些资料和希望怎样返回。例如:审查来自 https://gitea.example.com/team/shop 的 PR,指出可定位的问题,不修改代码;请求人可指定 Head SHA,未指定时用最新 Head。', style: inputStyle })),
1633
- h('p', { style: mutedTextStyle }, 'AI 不会自动分析;点击“立即分析”后才会检查必需范围、缺失信息和可选条件。执行方、请求人、群范围和有效期由下方通用授权控制。'),
1634
- h('div', { style: { display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' } },
1635
- h('button', { type: 'button', disabled: recognitionBusy || busy || !ruleDescription.trim(),
1636
- onClick: () => void identifyRule(), style: secondaryButtonStyle }, recognitionBusy ? 'AI 正在分析…' : ruleSummaryReady ? '立即重新分析' : '立即分析'),
1637
- h('span', { style: mutedTextStyle }, '每次点击都会调用当前团队模型并计入用量;只生成建议和待复核草稿,不会开放任务。修改描述后需再次点击。')),
1638
- recognitionMessage ? h('p', { role: 'status', 'aria-live': 'polite', style: feedbackNoticeStyle }, recognitionMessage) : null),
1639
- h('section', { role: 'region', 'aria-label': 'AI 任务助手', style: { ...panelStyle, padding: 10, gap: 7, minWidth: 0 } },
1811
+ capabilityStepHeading('1', '描述任务', '写清要完成什么、处理范围和结果形式。'),
1812
+ h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 320px), 1.35fr))',
1813
+ gap: 12, alignItems: 'stretch' } },
1814
+ h('label', { style: { display: 'grid', alignContent: 'start', gap: 7, minWidth: 0 } }, '描述规则',
1815
+ h('textarea', { required: true, maxLength: 4000, rows: 5, value: ruleDescription,
1816
+ onChange: (event) => {
1817
+ cancelRecognitionRequest()
1818
+ recognitionRequestRef.current += 1
1819
+ setRuleDescription(event.target.value)
1820
+ setRecognitionBusy(false)
1821
+ setRecognitionMessage(event.target.value.trim() ? '描述已更新,点击“立即分析”检查任务。' : '')
1822
+ },
1823
+ placeholder: '用日常语言说明要完成什么、可处理哪些资料和希望怎样返回。例如:审查来自 https://gitea.example.com/team/shop 的 PR,指出可定位的问题,不修改代码;请求人可指定 Head SHA,未指定时用最新 Head。', style: { ...inputStyle, minHeight: 120, resize: 'vertical' } })),
1824
+ h('section', { role: 'region', 'aria-label': 'AI 任务助手', style: { ...panelStyle, padding: 14,
1825
+ gap: 9, minWidth: 0, border: '1px solid var(--dsw-alias-border-l2)', backgroundColor: 'var(--dsw-alias-bg-layer-2)' } },
1640
1826
  h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } },
1641
- h('strong', null, ruleSummaryPending ? 'AI 正在检查当前描述' : title || 'AI 任务助手'),
1827
+ h('strong', null, 'AI 任务助手'),
1642
1828
  h('span', { style: mutedTextStyle }, ruleSummaryPending ? '正在分析' : ruleSummaryReady ? '待复核 · 尚未开放'
1643
- : ruleSummaryDirty ? '描述已更新' : '填写描述后点击分析')),
1644
- h('p', { style: { ...mutedTextStyle, overflowWrap: 'anywhere' } }, ruleSummaryPending
1645
- ? 'AI 正在根据当前描述更新任务理解。上一次分析结果暂不用于授权开放。'
1646
- : ruleSummaryDirty ? '描述已更新。点击“立即分析”生成当前任务理解;旧结果暂不用于授权开放。' : ruleSummaryNarrative),
1647
- ruleSummaryReady && missingHarnessInputs.length ? h('p', { role: 'status', style: { ...mutedTextStyle,
1648
- color: 'var(--dsw-alias-state-warning-primary, var(--dsw-alias-label-secondary))' } },
1649
- `还需要明确:${missingHarnessInputs.join(';')}`) : ruleSummaryReady
1650
- ? h('p', { role: 'status', style: mutedTextStyle }, 'AI 暂未发现必需信息缺项;请确认任务来源和处理范围符合预期。') : null,
1651
- ruleSummaryReady && suggestions.length ? h('p', { style: mutedTextStyle }, `可选补充:${suggestions.join(';')}`) : null,
1652
- ruleSummaryReady && ruleExamples.length ? h('p', { style: mutedTextStyle }, `自然请求示例:${ruleExamples.join(';')}`) : null,
1653
- h('p', { style: mutedTextStyle }, ruleSummaryReady
1654
- ? '草稿未开放。检查任务理解、必需范围和建议;有误就改描述,AI 会重新分析。'
1655
- : ruleSummaryPending ? '分析完成前不能开放任务。'
1656
- : ruleSummaryDirty ? '点击“立即分析”完成复核后才能开放任务。' : '填写描述后点击“立即分析”生成任务理解;草稿不会自动开放。'))),
1657
- h('section', { style: { ...panelStyle, padding: 10, gap: 9, display: 'grid',
1658
- gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 280px), 1fr))' } },
1659
- h('strong', { style: { gridColumn: '1 / -1' } }, '通用授权'),
1660
- h('p', { style: { ...mutedTextStyle, gridColumn: '1 / -1' } },
1661
- '上方描述规则任务要做什么;这里限定谁能请求、在哪里请求、由谁执行和授权多久。'),
1829
+ : ruleSummaryDirty ? '描述已更新' : '待分析')),
1830
+ h('p', { style: mutedTextStyle }, ruleSummaryPending
1831
+ ? 'AI 正在根据当前描述更新任务理解。上一次结果暂不用于授权。'
1832
+ : '分析只生成待复核草稿,不会自动开放能力。修改描述后需重新分析。'),
1833
+ h('div', { style: { display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 10, flexWrap: 'wrap' } },
1834
+ h('span', { style: mutedTextStyle }, '调用团队模型并计入用量'),
1835
+ h('button', { type: 'button', disabled: recognitionBusy || busy || !ruleDescription.trim(),
1836
+ onClick: () => void identifyRule(), style: primaryButtonStyle },
1837
+ recognitionBusy ? 'AI 正在分析…' : ruleSummaryReady ? '立即重新分析' : '立即分析')),
1838
+ recognitionMessage ? h('p', { role: 'status', 'aria-live': 'polite', style: feedbackNoticeStyle }, recognitionMessage) : null)),
1839
+ h('section', { role: 'region', 'aria-label': 'AI 分析结果', style: { display: 'grid', gap: 9, padding: 14,
1840
+ border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 10,
1841
+ backgroundColor: 'var(--dsw-alias-bg-layer-1)', minWidth: 0,
1842
+ height: ruleSummaryReady ? 'min(480px, 55vh)' : undefined,
1843
+ gridTemplateRows: ruleSummaryReady ? 'auto minmax(0, 1fr)' : undefined,
1844
+ overflow: ruleSummaryReady ? 'hidden' : undefined } },
1845
+ h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } },
1846
+ h('strong', null, 'AI 分析结果'),
1847
+ h('span', { style: mutedTextStyle }, ruleSummaryReady ? '草稿 · 未开放' : ruleSummaryPending ? '正在分析' : '等待分析')),
1848
+ ruleSummaryReady ? h('div', { style: { display: 'grid', alignContent: 'start', gap: 8,
1849
+ minHeight: 0, overflowY: 'auto', overscrollBehavior: 'contain', overflowWrap: 'anywhere' } },
1850
+ title ? h('strong', { style: { color: 'var(--dsw-alias-label-primary)' } }, rawLocaleText(title)) : null,
1851
+ h('p', { style: { ...mutedTextStyle, color: 'var(--dsw-alias-label-primary)' } }, ruleSummaryNarrative),
1852
+ missingHarnessInputs.length ? h('p', { role: 'status', style: { ...mutedTextStyle,
1853
+ color: 'var(--dsw-alias-state-warning-primary, var(--dsw-alias-label-secondary))' } },
1854
+ translateLocaleText('待补信息:{items}。一般资料和偏好可留到执行时向请求人追问;PR 来源和仓库仍须先通过资源范围校验。',
1855
+ { items: missingHarnessInputs.join(';') }))
1856
+ : h('p', { role: 'status', style: mutedTextStyle }, translateLocaleText(
1857
+ 'AI 暂未发现运行时待补信息;开放后,执行时如缺少必要资料,机器人仍会向请求人追问。')),
1858
+ suggestions.length ? h('p', { style: mutedTextStyle }, `可选补充:${suggestions.join(';')}`) : null,
1859
+ ruleExamples.length ? h('p', { style: mutedTextStyle }, `自然请求示例:${ruleExamples.join(';')}`) : null,
1860
+ h('p', { style: mutedTextStyle }, translateLocaleText(
1861
+ '待补信息和可选建议仅供复核,不会阻止开放。确定的授权范围与 PR 资源边界仍会单独校验。')))
1862
+ : h('p', { role: 'status', style: mutedTextStyle }, ruleSummaryPending
1863
+ ? '分析完成后,摘要、缺失范围和可选条件会显示在这里。'
1864
+ : '填写任务描述后点击右侧“立即分析”,结果会显示在这里。')),
1865
+ h('section', { style: capabilityStepStyle },
1866
+ capabilityStepHeading('2', '执行方', '选择可用于此任务的执行器,并从已勾选项中设一个默认。'),
1867
+ h('div', { style: { ...panelStyle, padding: 12, gap: 8 } },
1868
+ h('p', { style: mutedTextStyle }, translateLocaleText('普通请求只运行默认执行方;发起人明确要求交叉审查时,会并行运行其他已选执行方。')),
1662
1869
  h('div', { role: 'group', 'aria-label': '执行方', style: { display: 'grid', gap: 7 } },
1663
- h('strong', null, '执行方 · 可多选'),
1664
- h('p', { style: mutedTextStyle }, '勾选可用于此任务的执行方,并从已勾选项中设一个默认。普通请求只运行默认项;请求人明确要求交叉审查时并行运行其他已选项。'),
1665
1870
  ...[
1666
1871
  { id: 'dsh', title: 'DSH', detail: '使用获准的团队模型处理对话内容和 ANT 已核验资源。', available: dshAvailable },
1667
1872
  { id: 'codex', title: 'Codex', detail: '使用本机 DSH Provider Profile 权限;ANT 不会将文件、Git 和网络权限缩小到单项任务。', available: codexAvailable },
@@ -1685,7 +1890,11 @@ window.__ModuleLoader__.load({
1685
1890
  }),
1686
1891
  ),
1687
1892
  executorUnavailableReason ? h('p', { role: 'status', style: { ...mutedTextStyle, gridColumn: '1 / -1' } },
1688
- `${executorUnavailableReason} 当前仍可使用 DSH 团队模型。`) : null,
1893
+ `${executorUnavailableReason} 当前仍可使用 DSH 团队模型。`) : null)),
1894
+ h('section', { style: capabilityStepStyle },
1895
+ capabilityStepHeading('3', '请求范围', '限定从哪里请求、哪些人可以请求,以及授权多久。'),
1896
+ h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 250px), 1fr))',
1897
+ gap: 12, alignItems: 'start' } },
1689
1898
  h('label', null, '使用场景', h('select', { value: conversationKind,
1690
1899
  onChange: (event) => { setConversationKind(event.target.value); setGroupId('') }, style: inputStyle },
1691
1900
  h('option', { value: 'dm' }, '私聊'), h('option', { value: 'group' }, '指定群聊'))),
@@ -1709,10 +1918,16 @@ window.__ModuleLoader__.load({
1709
1918
  `有效期:${formatLocaleDate(editingExpiresAt * 1000)}(编辑时保持不变)`)
1710
1919
  : h('label', null, '有效期', h('select', { value: days, onChange: (event) => setDays(Number(event.target.value)), style: inputStyle },
1711
1920
  h('option', { value: 1 }, '1 天'), h('option', { value: 7 }, '7 天'), h('option', { value: 30 }, '30 天'))),
1712
- (runner === 'codex' || runner === 'claude-code') ? h('p', { style: { ...mutedTextStyle, gridColumn: '1 / -1' } },
1713
- 'Provider Profile 的本机权限由 DSH 管理;ANT 当前不会用 Codex 或 Claude Code 执行协作任务,也不会把 Gitea 凭据交给它们。') : null),
1714
- !hasPrParameter ? h('p', { style: mutedTextStyle },
1715
- 'Codex 和 Claude Code 即使已在 DSH 注册,目前也不用于任何协作任务。') : null),
1921
+ usesExternalExecutor ? h('p', { style: { ...mutedTextStyle, gridColumn: '1 / -1' } },
1922
+ 'Codex 和 Claude Code 沿用本机 DSH Provider Profile 权限;任务授权不会把文件、Git 或网络权限缩小到单项任务。') : null,
1923
+ usesExternalExecutor ? h('label', { style: { gridColumn: '1 / -1' } },
1924
+ h('input', { type: 'checkbox', 'aria-label': '启动前需要主人审批', checked: ownerApprovalRequired,
1925
+ onChange: (event) => setOwnerApprovalRequired(event.target.checked) }),
1926
+ ' ', '启动前需要主人审批') : null,
1927
+ usesExternalExecutor && !ownerApprovalRequired ? h('p', { role: 'note', style: { ...mutedTextStyle, gridColumn: '1 / -1' } },
1928
+ '取消后,模型匹配到这项任务就会直接启动 Codex 或 Claude Code,主人不再逐次确认。') : null)),
1929
+ h('section', { style: capabilityStepStyle },
1930
+ capabilityStepHeading('4', '开放前复核', '确认任务理解、执行方式和请求范围后再开放。'),
1716
1931
  h('aside', { id: 'ant-authorization-review', role: 'region', 'aria-label': '开放前复核', tabIndex: -1,
1717
1932
  style: { ...settingsPanelStyle, padding: 14, gap: 10, alignSelf: 'stretch' } },
1718
1933
  h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8 } },
@@ -1750,7 +1965,7 @@ window.__ModuleLoader__.load({
1750
1965
  'aria-describedby': cardBlockReason ? 'ant-capability-block-reason'
1751
1966
  : submissionError ? 'ant-capability-submission-error' : undefined,
1752
1967
  style: { ...primaryButtonStyle, width: '100%' } }, busy ? translateLocaleText('正在处理…')
1753
- : editingCardId ? '保存能力修改' : '确认并开放能力'))))),
1968
+ : editingCardId ? '保存能力修改' : '确认并开放能力'))))))),
1754
1969
  h('div', { role: 'region', 'aria-label': '本机处理设置', style: { display: activeTab === 'local' ? 'grid' : 'none', gap: 12 } },
1755
1970
  showPrSecuritySettings ? h('section', { style: { ...settingsPanelStyle, padding: 14 } },
1756
1971
  h('strong', null, 'PR 只读凭据(本机)'),
@@ -1810,7 +2025,7 @@ window.__ModuleLoader__.load({
1810
2025
  }
1811
2026
 
1812
2027
  function SkillCenterPage({ remote, onBack, onAll, onSkills, onPlugins, openBundle,
1813
- includePlugins, query, onQueryChange }) {
2028
+ includePlugins, query, onQueryChange, pluginPageState }) {
1814
2029
  const [skills, setSkills] = useState([])
1815
2030
  const [loading, setLoading] = useState(false)
1816
2031
  const [loadError, setLoadError] = useState(false)
@@ -2159,6 +2374,7 @@ window.__ModuleLoader__.load({
2159
2374
  style: { color: 'var(--dsw-alias-link)', fontSize: 13, lineHeight: 1.6, padding: '4px 0' } }, '英文原文 ↗'))))) : null,
2160
2375
  includePlugins ? h(PluginCenterPage, { remote, onBack, onAll, onSkills, openBundle,
2161
2376
  embedded: true, query, onQueryChange, externalFilter: filter, externalCategoryFilter: categoryFilter,
2377
+ pluginPageState,
2162
2378
  externalScopeFilter: scopeFilter,
2163
2379
  hasSkillMatches: visible.length + visibleOfficial.length > 0, skillCatalogReady: !loading && !loadError,
2164
2380
  refreshToken }) : null))
@@ -2167,49 +2383,193 @@ window.__ModuleLoader__.load({
2167
2383
  function PluginCenterPage({ remote, onBack, onAll, onSkills, openBundle,
2168
2384
  embedded = false, query, onQueryChange, externalFilter = 'all',
2169
2385
  externalCategoryFilter = 'all', externalScopeFilter = 'all',
2170
- hasSkillMatches = false, skillCatalogReady = false, refreshToken }) {
2386
+ hasSkillMatches = false, skillCatalogReady = false, refreshToken, pluginPageState }) {
2387
+ const { uncertainInstalls, setUncertainInstalls, uncertainInstallsRef, workingId, setWorkingId,
2388
+ completedId, setCompletedId, message, setMessage, refreshMessage, refreshMessageIsError,
2389
+ setRefreshFeedback, messageVersion, messagePluginId, messagePackageName,
2390
+ messageClearOnResolve, refreshGeneration, refreshRevision, requestPluginRefresh,
2391
+ workingAny } = pluginPageState
2171
2392
  const [plugins, setPlugins] = useState([])
2172
2393
  const [bundles, setBundles] = useState([])
2173
2394
  const [loading, setLoading] = useState(true)
2174
2395
  const [loadError, setLoadError] = useState(false)
2175
- const [workingId, setWorkingId] = useState('')
2176
- const [uncertainId, setUncertainId] = useState('')
2177
- const [completedId, setCompletedId] = useState('')
2178
- const [message, setMessage] = useState('')
2396
+ const [bundleListReady, setBundleListReady] = useState(false)
2179
2397
  const [filter, setFilter] = useState('all')
2180
2398
  const [categoryFilter, setCategoryFilter] = useState('all')
2181
- const refreshGeneration = useRef(0)
2182
- const refresh = async () => {
2399
+ const waitForInstallBeforeDeadline = (manager, requestId) => {
2400
+ let timeoutId
2401
+ const waitPromise = Promise.resolve().then(() => manager.waitForInstall(requestId))
2402
+ const timeoutPromise = new Promise((_, reject) => {
2403
+ timeoutId = setTimeout(() => reject(new Error()), 30_000)
2404
+ })
2405
+ return Promise.race([waitPromise, timeoutPromise])
2406
+ .finally(() => { if (timeoutId !== undefined) clearTimeout(timeoutId) })
2407
+ }
2408
+ const refresh = async (focusPluginId = null) => {
2183
2409
  const generation = ++refreshGeneration.current
2410
+ const uncertaintySnapshot = uncertainInstallsRef.current
2411
+ const messageVersionAtStart = messageVersion.current
2184
2412
  setLoading(true)
2413
+ setBundleListReady(false)
2414
+ let catalogLoaded = false
2185
2415
  try {
2186
2416
  const manager = remote?.pluginManager
2187
2417
  if (typeof manager?.listBundles !== 'function') throw new Error('当前 DSH 未提供插件管理接口。')
2188
- const [response, listed] = await Promise.all([
2189
- fetch('/api/ant-teams/plugins', { cache: 'no-store' }), manager.listBundles(),
2190
- ])
2418
+ const response = await fetch('/api/ant-teams/plugins', { cache: 'no-store' })
2191
2419
  const result = await readLocalJsonResponse(response)
2192
2420
  if (!response.ok) throw new Error(result.message || `读取插件目录失败(${response.status})。`)
2193
2421
  if (!Array.isArray(result.plugins)) throw new Error('企业插件目录格式无效。')
2194
- if (!listed.ok) throw new Error(listed.error?.message || '无法读取 DSH 本机插件。')
2195
2422
  if (generation !== refreshGeneration.current) return
2196
2423
  setPlugins(result.plugins)
2424
+ setLoadError(false)
2425
+ catalogLoaded = true
2426
+ const initialListed = await manager.listBundles()
2427
+ if (!initialListed.ok) throw new Error(initialListed.error?.message || '无法读取 DSH 本机插件。')
2428
+ if (generation !== refreshGeneration.current) return
2429
+ setBundles(initialListed.value)
2430
+ setBundleListReady(true)
2431
+ const catalogPackageNames = new Set(result.plugins
2432
+ .map((plugin) => plugin.package_name).filter((name) => typeof name === 'string' && name))
2433
+ const catalogUncertaintySnapshot = Object.fromEntries(Object.entries(uncertaintySnapshot)
2434
+ .filter(([, uncertainInstall]) => catalogPackageNames.has(uncertainInstall.packageName)))
2435
+ const requestIds = [...new Set(Object.values(catalogUncertaintySnapshot)
2436
+ .flatMap((uncertainInstall) => [uncertainInstall.requestId,
2437
+ ...(uncertainInstall.relatedRequestIds || [])])
2438
+ .filter((requestId) => typeof requestId === 'string' && requestId))]
2439
+ if (requestIds.length) {
2440
+ if (typeof manager.waitForInstall !== 'function') {
2441
+ if (generation === refreshGeneration.current) setRefreshFeedback(translateLocaleText(
2442
+ '当前 DSH 无法确认上次安装是否结束;请稍后重新核对,勿重复安装。'), true)
2443
+ return
2444
+ }
2445
+ setRefreshFeedback(translateLocaleText('正在等待 DSH 上一次插件安装请求结束,再读取本机状态。'))
2446
+ for (const requestId of requestIds) {
2447
+ let waitResult
2448
+ try { waitResult = await waitForInstallBeforeDeadline(manager, requestId) }
2449
+ catch {
2450
+ if (generation === refreshGeneration.current) setRefreshFeedback(translateLocaleText(
2451
+ '无法确认 DSH 上一项插件安装是否已结束;待核对锁保持,请稍后重试。'), true)
2452
+ return
2453
+ }
2454
+ if (!waitResult?.ok) {
2455
+ if (generation === refreshGeneration.current) setRefreshFeedback(translateLocaleText(
2456
+ '无法确认 DSH 上一项插件安装是否已结束;待核对锁保持,请稍后重试。'), true)
2457
+ return
2458
+ }
2459
+ if (generation !== refreshGeneration.current) return
2460
+ }
2461
+ }
2462
+ if (generation !== refreshGeneration.current) return
2463
+ let listed = initialListed
2464
+ if (requestIds.length) {
2465
+ listed = await manager.listBundles()
2466
+ if (!listed.ok) throw new Error(listed.error?.message || '无法读取 DSH 本机插件。')
2467
+ }
2468
+ if (generation !== refreshGeneration.current) return
2469
+ const canUpdateMessage = messageVersion.current === messageVersionAtStart
2470
+ const currentUncertainties = uncertainInstallsRef.current
2471
+ const currentCatalogUncertainties = Object.fromEntries(Object.entries(currentUncertainties)
2472
+ .filter(([, uncertainInstall]) => catalogPackageNames.has(uncertainInstall.packageName)))
2473
+ const focusPackageName = focusPluginId
2474
+ ? plugins.find((plugin) => plugin.id === focusPluginId)?.package_name : null
2475
+ let nextUncertainties = currentUncertainties
2476
+ const resolvedPluginIds = new Set()
2477
+ for (const [pluginId, uncertainInstall] of Object.entries(catalogUncertaintySnapshot)) {
2478
+ if (currentUncertainties[pluginId] !== uncertainInstall) continue
2479
+ const installed = listed.value.find((bundle) => bundle.name === uncertainInstall.packageName && bundle.installed)
2480
+ const resolved = !installed
2481
+ || (installed.version === uncertainInstall.targetVersion
2482
+ && (!uncertainInstall.expectedSource || installed.source === uncertainInstall.expectedSource))
2483
+ || Boolean(uncertainInstall.previousVersion
2484
+ && installed.version === uncertainInstall.previousVersion
2485
+ && (installed.source === uncertainInstall.previousSource
2486
+ || installed.source === uncertainInstall.expectedPreviousSource)
2487
+ && installed.enabled === uncertainInstall.previousEnabled)
2488
+ || Boolean(focusPluginId
2489
+ && (pluginId === focusPluginId || uncertainInstall.packageName === focusPackageName)
2490
+ && installed && typeof installed.version === 'string' && installed.version.trim()
2491
+ && installed.version !== uncertainInstall.targetVersion
2492
+ && installed.version !== uncertainInstall.previousVersion)
2493
+ if (!resolved) continue
2494
+ if (nextUncertainties === currentUncertainties) nextUncertainties = { ...currentUncertainties }
2495
+ delete nextUncertainties[pluginId]
2496
+ resolvedPluginIds.add(pluginId)
2497
+ }
2498
+ if (nextUncertainties !== currentUncertainties) {
2499
+ uncertainInstallsRef.current = nextUncertainties
2500
+ setUncertainInstalls(nextUncertainties)
2501
+ }
2502
+ setPlugins(result.plugins)
2197
2503
  setBundles(listed.value)
2198
2504
  setLoadError(false)
2199
- setMessage(result.plugins.length ? '' : '团队暂未发布精选插件。')
2505
+ if (canUpdateMessage && messageClearOnResolve.current && messagePluginId.current
2506
+ && (resolvedPluginIds.has(messagePluginId.current)
2507
+ || [...resolvedPluginIds].some((pluginId) => uncertaintySnapshot[pluginId]?.packageName
2508
+ === messagePackageName.current))) setMessage('')
2509
+ const focusedUncertainty = focusPluginId
2510
+ ? catalogUncertaintySnapshot[focusPluginId] || Object.values(catalogUncertaintySnapshot)
2511
+ .find((item) => item.packageName === focusPackageName) : null
2512
+ const focusedUncertaintyId = focusedUncertainty?.id
2513
+ const hasFocusedUncertainty = Boolean(focusedUncertainty
2514
+ && currentUncertainties[focusedUncertaintyId] === focusedUncertainty)
2515
+ const refreshSnapshotIsCurrent = Object.keys(currentCatalogUncertainties).length
2516
+ === Object.keys(catalogUncertaintySnapshot).length
2517
+ && Object.entries(catalogUncertaintySnapshot)
2518
+ .every(([pluginId, uncertainInstall]) => currentCatalogUncertainties[pluginId] === uncertainInstall)
2519
+ const hasUncertainInstalls = Object.values(nextUncertainties)
2520
+ .some((uncertainInstall) => catalogPackageNames.has(uncertainInstall.packageName))
2521
+ if (canUpdateMessage && hasFocusedUncertainty && refreshSnapshotIsCurrent) {
2522
+ if (resolvedPluginIds.has(focusedUncertaintyId)) {
2523
+ setRefreshFeedback(hasUncertainInstalls
2524
+ ? translateLocaleText('已重新读取本机列表,但安装状态仍无法确认;请在 DSH 插件管理核对后再操作。')
2525
+ : translateLocaleText('本机状态已确认;可以再次检查或按目录版本操作。'))
2526
+ } else {
2527
+ setRefreshFeedback(translateLocaleText('已重新读取本机列表,但安装状态仍无法确认;请在 DSH 插件管理核对后再操作。'))
2528
+ }
2529
+ } else if (canUpdateMessage && !focusPluginId && refreshSnapshotIsCurrent) {
2530
+ setRefreshFeedback(hasUncertainInstalls
2531
+ ? translateLocaleText('已重新读取本机列表,但安装状态仍无法确认;请在 DSH 插件管理核对后再操作。')
2532
+ : result.plugins.length ? '' : '团队暂未发布精选插件。')
2533
+ }
2200
2534
  } catch (error) {
2201
2535
  if (generation !== refreshGeneration.current) return
2202
- setPlugins([])
2203
- setBundles([])
2204
- setLoadError(true)
2205
- setMessage(error instanceof Error ? error.message : '无法读取企业精选插件。')
2536
+ setBundleListReady(false)
2537
+ const errorMessage = error instanceof Error ? error.message : '无法读取企业精选插件。'
2538
+ if (!catalogLoaded) {
2539
+ setPlugins([])
2540
+ setBundles([])
2541
+ setLoadError(true)
2542
+ }
2543
+ setRefreshFeedback(errorMessage, true)
2206
2544
  } finally { if (generation === refreshGeneration.current) setLoading(false) }
2207
2545
  }
2208
- useEffect(() => { void refresh() }, [refreshToken])
2546
+ useEffect(() => { void refresh() }, [refreshToken, refreshRevision])
2547
+ const uncertainInstallFor = (plugin) => uncertainInstalls[plugin.id]
2548
+ || Object.values(uncertainInstallsRef.current).find((item) => item.packageName === plugin.package_name)
2549
+ const markInstallUncertain = (plugin, previous = null, expectedPreviousSource = null,
2550
+ requestId = null, relatedRequestIds = []) => {
2551
+ const uncertainInstall = {
2552
+ id: plugin.id,
2553
+ packageName: plugin.package_name,
2554
+ targetVersion: plugin.version,
2555
+ requestId,
2556
+ relatedRequestIds: [...new Set(relatedRequestIds.filter((value) => typeof value === 'string' && value))],
2557
+ expectedSource: approvedPluginArtifactUrl(plugin),
2558
+ previousVersion: previous?.version || null,
2559
+ previousSource: previous?.source,
2560
+ expectedPreviousSource,
2561
+ previousEnabled: previous ? previous.enabled !== false : null,
2562
+ }
2563
+ const next = { ...uncertainInstallsRef.current, [plugin.id]: uncertainInstall }
2564
+ uncertainInstallsRef.current = next
2565
+ setUncertainInstalls(next)
2566
+ }
2209
2567
  const install = async (plugin) => {
2210
2568
  const artifactUrl = approvedPluginArtifactUrl(plugin)
2211
2569
  const installSpec = pluginInstallSpec(plugin)
2212
- if (!installSpec || workingId || uncertainId === plugin.id || completedId === plugin.id) return
2570
+ const uncertainInstall = uncertainInstallFor(plugin)
2571
+ if (!installSpec || workingId || pluginRuntimeState.workingId
2572
+ || uncertainInstall || completedId === plugin.id) return
2213
2573
  const manager = remote?.pluginManager
2214
2574
  if (typeof manager?.installBundle !== 'function') {
2215
2575
  setMessage('当前 DSH 未提供插件安装接口,请联系管理员。')
@@ -2229,8 +2589,15 @@ window.__ModuleLoader__.load({
2229
2589
  '在这台 DSH {action}团队审核的 {plugin_title} {version}?{source_notice}{update_notice} 插件将在本机运行,完成后需要完全重启 DSH。',
2230
2590
  { action: translateLocaleText(action), plugin_title: plugin.title, version: plugin.version, source_notice: sourceNotice, update_notice: updateNotice })
2231
2591
  if (!confirmLocalized(installConfirmation)) return
2592
+ const operationScopeRevision = pluginRuntimeState.scopeRevision
2593
+ const isOperationScopeCurrent = () => operationScopeRevision === pluginRuntimeState.scopeRevision
2594
+ const setInstallMessage = (value, staleScopeValue = '本机此前启动的插件操作已结束;请查看当前插件目录和本机状态。') => {
2595
+ setMessage(isOperationScopeCurrent() ? value : staleScopeValue,
2596
+ plugin.id, plugin.package_name, Boolean(uncertainInstallFor(plugin)))
2597
+ }
2232
2598
  setWorkingId(plugin.id)
2233
- setMessage(translateLocaleText('正在安装 {plugin_title},请不要退出 DSH…', { plugin_title: plugin.title }))
2599
+ setInstallMessage(translateLocaleText('正在安装 {plugin_title},请不要退出 DSH…', { plugin_title: plugin.title }),
2600
+ '本机有一项插件操作仍在完成;结束前不能启动其他插件安装。')
2234
2601
  try {
2235
2602
  const response = await fetch('/api/ant-teams/plugins', { cache: 'no-store' })
2236
2603
  const catalog = await readLocalJsonResponse(response)
@@ -2241,6 +2608,9 @@ window.__ModuleLoader__.load({
2241
2608
  throw new Error('插件审核信息已变化,请刷新目录后重试。')
2242
2609
  }
2243
2610
  const requestId = crypto.randomUUID()
2611
+ let relatedRequestIds = []
2612
+ const markCurrentInstallUncertain = (previous = null, expectedPreviousSource = null) =>
2613
+ markInstallUncertain(plugin, previous, expectedPreviousSource, requestId, relatedRequestIds)
2244
2614
  const requestReference = requestId.slice(0, 8).toUpperCase()
2245
2615
  const previousSourceIsRegistry = Boolean(local && (!local.source
2246
2616
  || local.source === plugin.package_name || local.source.startsWith(`${plugin.package_name}@`)))
@@ -2280,17 +2650,26 @@ window.__ModuleLoader__.load({
2280
2650
  if (installed !== null || !previousInstallSpec) return 'unknown'
2281
2651
  if (previousSourceIsRegistry && !previousRegistry) return 'unknown'
2282
2652
  const restoreRequestId = crypto.randomUUID()
2283
- try {
2284
- const restoreOptions = {
2285
- enabled: local.enabled !== false, requestId: restoreRequestId,
2286
- saveExact: true,
2287
- }
2288
- if (previousSourceIsRegistry && previousRegistry) restoreOptions.registry = previousRegistry
2289
- await manager.installBundle(previousInstallSpec, restoreOptions)
2290
- if (typeof manager.waitForInstall === 'function') {
2291
- await manager.waitForInstall(restoreRequestId)
2292
- }
2293
- } catch {}
2653
+ const restoreOptions = {
2654
+ enabled: local.enabled !== false, requestId: restoreRequestId,
2655
+ saveExact: true,
2656
+ }
2657
+ if (previousSourceIsRegistry && previousRegistry) restoreOptions.registry = previousRegistry
2658
+ try { await manager.installBundle(previousInstallSpec, restoreOptions) } catch {}
2659
+ if (typeof manager.waitForInstall !== 'function') {
2660
+ relatedRequestIds = [restoreRequestId]
2661
+ return 'unknown'
2662
+ }
2663
+ let restoreWaitResult
2664
+ try { restoreWaitResult = await manager.waitForInstall(restoreRequestId) }
2665
+ catch {
2666
+ relatedRequestIds = [restoreRequestId]
2667
+ return 'unknown'
2668
+ }
2669
+ if (!restoreWaitResult?.ok) {
2670
+ relatedRequestIds = [restoreRequestId]
2671
+ return 'unknown'
2672
+ }
2294
2673
  installed = await currentBundle()
2295
2674
  return isPreviousVersion(installed) && installed.enabled === (local.enabled !== false)
2296
2675
  ? 'restored' : 'unknown'
@@ -2313,7 +2692,7 @@ window.__ModuleLoader__.load({
2313
2692
  } catch (error) { installFailure = error; result = null }
2314
2693
  } else {
2315
2694
  if (replacingInstalled && typeof manager.removeBundle !== 'function') {
2316
- setMessage('当前 DSH 不支持通过 ANT 安全替换已安装插件。请在 DSH 插件管理查看状态,或联系管理员。')
2695
+ setInstallMessage('当前 DSH 不支持通过 ANT 安全替换已安装插件。请在 DSH 插件管理查看状态,或联系管理员。')
2317
2696
  return
2318
2697
  }
2319
2698
  if (replacingInstalled) {
@@ -2324,7 +2703,7 @@ window.__ModuleLoader__.load({
2324
2703
  if (!removed?.ok || !['applied', 'restart-required'].includes(removed.value?.application)) {
2325
2704
  const diagnostic = safeInstallDiagnostic(removed?.value?.error || removed?.error || removeFailure)
2326
2705
  const recovery = await restorePreviousVersion()
2327
- if (recovery !== 'restored') setUncertainId(plugin.id)
2706
+ if (recovery !== 'restored') markCurrentInstallUncertain(local, expectedPreviousSource)
2328
2707
  const recoveryNotice = recovery === 'restored'
2329
2708
  ? translateLocaleText('旧版本号和原启用状态已恢复。{source_notice}', {
2330
2709
  source_notice: previousSourceIsRegistry
@@ -2332,7 +2711,7 @@ window.__ModuleLoader__.load({
2332
2711
  : recovery === 'target' ? translateLocaleText(
2333
2712
  '本机列表显示审核版本 {version} 已安装,请核对后再操作。', { version: plugin.version })
2334
2713
  : translateLocaleText('无法确认旧版本及启用状态,请先在 DSH 插件管理核对。')
2335
- setMessage(`无法开始更新 ${plugin.title}:DSH 未能确认旧版本移除完成。${diagnostic
2714
+ setInstallMessage(`无法开始更新 ${plugin.title}:DSH 未能确认旧版本移除完成。${diagnostic
2336
2715
  ? `DSH 诊断:${diagnostic}。` : ''}${recoveryNotice}`)
2337
2716
  return
2338
2717
  }
@@ -2353,7 +2732,6 @@ window.__ModuleLoader__.load({
2353
2732
  } catch (error) { waitFailure = error }
2354
2733
  }
2355
2734
  if (!recovered) {
2356
- setUncertainId(plugin.id)
2357
2735
  let restoreNotice = ''
2358
2736
  if (removedForUpdate) {
2359
2737
  const restoreState = await restorePreviousVersion()
@@ -2366,23 +2744,27 @@ window.__ModuleLoader__.load({
2366
2744
  '本机列表显示审核版本 {version} 已安装,但操作结果未确认。', { version: plugin.version })}`
2367
2745
  : ` ${translateLocaleText('DSH 未能确认旧版本恢复,请先在插件列表核对。')}`
2368
2746
  }
2747
+ markCurrentInstallUncertain(local, expectedPreviousSource)
2369
2748
  const details = [installFailure && `installBundle:${safeInstallDiagnostic(installFailure)}`,
2370
2749
  waitFailure && `waitForInstall:${safeInstallDiagnostic(waitFailure)}`].filter(Boolean)
2371
- setMessage(`暂时无法确认 ${plugin.title} 的安装结果(请求编号 ${requestReference})。${details.length
2372
- ? `本机诊断:${details.join(';')}。` : ''}${restoreNotice}请先在 DSH 插件列表核对,勿重复点击安装。`)
2750
+ setInstallMessage(`暂时无法确认 ${plugin.title} 的安装结果(请求编号 ${requestReference})。${details.length
2751
+ ? `本机诊断:${details.join(';')}。` : ''}${restoreNotice}请先在 DSH 插件列表核对,勿重复点击安装。`,
2752
+ '本机插件安装结果待核对,请先确认本机状态,勿重复安装。')
2373
2753
  return
2374
2754
  }
2375
2755
  result = recovered
2376
2756
  }
2377
2757
  const outcome = result.value
2378
2758
  if (!outcome || typeof outcome !== 'object') {
2379
- setUncertainId(plugin.id)
2380
- setMessage(`DSH 未返回 ${plugin.title} 的安装结果。请先在插件列表核对,勿重复点击安装。`)
2759
+ markCurrentInstallUncertain(local, expectedPreviousSource)
2760
+ setInstallMessage(`DSH 未返回 ${plugin.title} 的安装结果。请先在插件列表核对,勿重复点击安装。`,
2761
+ '本机插件安装结果待核对,请先确认本机状态,勿重复安装。')
2381
2762
  return
2382
2763
  }
2383
2764
  if (outcome.application === 'failed' || outcome.application === 'cancelled') {
2384
2765
  if (outcome.error?.code === 'ambiguous-install' && local?.version === plugin.version) {
2385
- setMessage(`DSH 无法确定 ${plugin.title} 本次安装的唯一结果,未确认完成。请先在 DSH 插件列表核对;若已经正常使用同版则无需重复安装,仍需核对来源时请联系管理员。`)
2766
+ setInstallMessage(`DSH 无法确定 ${plugin.title} 本次安装的唯一结果,未确认完成。请先在 DSH 插件列表核对;若已经正常使用同版则无需重复安装,仍需核对来源时请联系管理员。`,
2767
+ '本机插件安装结果待核对,请先确认本机状态,勿重复安装。')
2386
2768
  return
2387
2769
  }
2388
2770
  const diagnostic = safeInstallDiagnostic({ name: outcome.error?.name,
@@ -2390,11 +2772,14 @@ window.__ModuleLoader__.load({
2390
2772
  message: outcome.error?.diagnostic || outcome.error?.message || outcome.packageResult?.output })
2391
2773
  let restoreNotice = ''
2392
2774
  if (updateRestore) {
2393
- restoreNotice = ['applied', 'restart-required'].includes(updateRestore.application)
2775
+ const restoreConfirmed = ['applied', 'restart-required'].includes(updateRestore.application)
2776
+ if (!restoreConfirmed) markCurrentInstallUncertain(local, expectedPreviousSource)
2777
+ restoreNotice = restoreConfirmed
2394
2778
  ? ` ${translateLocaleText('DSH 已恢复旧版本 {version}。', { version: local.version })}`
2395
2779
  : ` ${translateLocaleText('DSH 未能确认旧版本恢复,请先在插件列表核对。')}`
2396
2780
  } else if (removedForUpdate) {
2397
2781
  const restoreState = await restorePreviousVersion()
2782
+ if (restoreState !== 'restored') markCurrentInstallUncertain(local, expectedPreviousSource)
2398
2783
  restoreNotice = restoreState === 'restored'
2399
2784
  ? translateLocaleText('DSH 已恢复旧版本号 {version}。{source_notice}', {
2400
2785
  version: local.version,
@@ -2404,14 +2789,15 @@ window.__ModuleLoader__.load({
2404
2789
  '本机列表显示审核版本 {version} 已安装,但结果未确认。', { version: plugin.version })}`
2405
2790
  : ` ${translateLocaleText('DSH 未能确认旧版本恢复,请先在插件列表核对。')}`
2406
2791
  }
2407
- setMessage(`${plugin.title} 安装未完成(请求编号 ${requestReference})。${diagnostic
2792
+ setInstallMessage(`${plugin.title} 安装未完成(请求编号 ${requestReference})。${diagnostic
2408
2793
  ? `DSH 诊断:${diagnostic}。` : 'DSH 没有返回失败诊断。'}${restoreNotice}请在 DSH 插件管理查看状态或联系管理员。`)
2409
2794
  return
2410
2795
  }
2411
2796
  if (outcome.bundle !== plugin.package_name
2412
2797
  || !['restart-required', 'applied'].includes(outcome.application)) {
2413
- setUncertainId(plugin.id)
2414
- setMessage(`DSH 未确认 ${plugin.title} 已安装。请先在插件列表核对,勿重复点击安装。`)
2798
+ markCurrentInstallUncertain(local, expectedPreviousSource)
2799
+ setInstallMessage(`DSH 未确认 ${plugin.title} 已安装。请先在插件列表核对,勿重复点击安装。`,
2800
+ '本机插件安装结果待核对,请先确认本机状态,勿重复安装。')
2415
2801
  return
2416
2802
  }
2417
2803
  let observed = null
@@ -2422,19 +2808,24 @@ window.__ModuleLoader__.load({
2422
2808
  }
2423
2809
  } catch {}
2424
2810
  if ((outcome.version && outcome.version !== plugin.version) || observed?.version !== plugin.version) {
2425
- setUncertainId(plugin.id)
2426
- setMessage(`DSH 未确认 ${plugin.title} 的审核版本已安装。请先在插件列表核对,勿重复点击安装。`)
2811
+ markCurrentInstallUncertain(local, expectedPreviousSource)
2812
+ setInstallMessage(`DSH 未确认 ${plugin.title} 的审核版本已安装。请先在插件列表核对,勿重复点击安装。`,
2813
+ '本机插件安装结果待核对,请先确认本机状态,勿重复安装。')
2427
2814
  return
2428
2815
  }
2429
- await refresh()
2430
2816
  if (artifactUrl && plugin.artifact_sha256) {
2431
2817
  try { localStorage.setItem(`${PLUGIN_INSTALL_RECORD_PREFIX}${plugin.id}`, plugin.artifact_sha256) } catch {}
2432
2818
  }
2433
- setCompletedId(plugin.id)
2434
- setMessage(`${plugin.title} 已安装。请完全退出并重新启动 DSH 后使用。`)
2819
+ if (isOperationScopeCurrent()) setCompletedId(plugin.id)
2820
+ setInstallMessage(`${plugin.title} 已安装。请完全退出并重新启动 DSH 后使用。`,
2821
+ '本机插件已安装或更新;请完全退出并重新启动 DSH 后使用。')
2822
+ requestPluginRefresh()
2435
2823
  } catch (error) {
2436
- setMessage(error instanceof Error ? error.message : '插件安装未完成,请稍后重试。')
2437
- } finally { setWorkingId('') }
2824
+ setInstallMessage(error instanceof Error ? error.message : '插件安装未完成,请稍后重试。',
2825
+ '本机此前启动的插件操作已结束;请刷新本机状态后再操作。')
2826
+ } finally {
2827
+ setWorkingId('')
2828
+ }
2438
2829
  }
2439
2830
  const activeFilter = embedded ? externalFilter : filter
2440
2831
  const activeCategoryFilter = embedded ? externalCategoryFilter : categoryFilter
@@ -2493,13 +2884,18 @@ window.__ModuleLoader__.load({
2493
2884
  h('div', null,
2494
2885
  h('h1', { style: { margin: 0, fontSize: 28 } }, '企业精选插件'),
2495
2886
  h('p', { style: { ...mutedTextStyle, marginTop: 6 } }, '这里展示团队审核过的插件版本及本机状态。DSH 0.2.0-rc.2 不会自动更新 Bundle;插件自身的更新器可能绕过此目录安装其他版本。')),
2496
- h('button', { type: 'button', disabled: loading, onClick: () => void refresh(), style: secondaryButtonStyle }, loading ? '读取中…' : '刷新目录')),
2887
+ h('button', { type: 'button', disabled: loading, onClick: () => {
2888
+ if (!workingAny) setMessage('')
2889
+ void refresh()
2890
+ }, style: secondaryButtonStyle }, loading ? '读取中…' : '刷新目录')),
2497
2891
  !loading && !plugins.length && !loadError ? embedded
2498
2892
  ? h('p', { role: 'status', style: mutedTextStyle }, '团队暂未发布精选插件。')
2499
2893
  : h('section', { role: 'status', style: panelStyle },
2500
2894
  h('h2', { style: { margin: 0, fontSize: 18 } }, '团队暂未发布精选插件'),
2501
2895
  h('p', { style: mutedTextStyle }, '需要某个插件?请联系企业管理员,说明用途或提供插件地址。审核发布后会显示在这里。')) : null,
2502
- !loading && loadError ? h('p', { role: 'alert', style: feedbackNoticeStyle }, message) : null,
2896
+ refreshMessage && (refreshMessageIsError || plugins.length)
2897
+ ? h('p', { role: refreshMessageIsError ? 'alert' : 'status', style: feedbackNoticeStyle }, refreshMessage) : null,
2898
+ message ? h('p', { role: 'status', style: feedbackNoticeStyle }, message) : null,
2503
2899
  plugins.length && !embedded ? h(ExtensionRequestNotice) : null,
2504
2900
  plugins.length && !embedded ? h('div', { style: panelStyle },
2505
2901
  h('input', { type: 'search', value: query, onChange: (event) => onQueryChange(event.target.value), placeholder: '搜索插件名称或用途', 'aria-label': '搜索企业精选插件', style: inputStyle }),
@@ -2512,7 +2908,6 @@ window.__ModuleLoader__.load({
2512
2908
  'aria-label': '筛选插件用途类别', style: { ...inputStyle, width: 'auto', minHeight: 34 } },
2513
2909
  h('option', { value: 'all' }, '全部用途'),
2514
2910
  ...extensionCategories.map(([category, label]) => h('option', { key: category, value: category }, label)))))) : null,
2515
- plugins.length && message ? h('p', { role: 'status', style: feedbackNoticeStyle }, message) : null,
2516
2911
  visible.length ? h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 420px), 1fr))', gap: 16 } },
2517
2912
  ...visible.map((plugin) => {
2518
2913
  const local = bundles.find((bundle) => bundle.name === plugin.package_name && bundle.installed)
@@ -2521,11 +2916,13 @@ window.__ModuleLoader__.load({
2521
2916
  const artifactUrl = approvedPluginArtifactUrl(plugin)
2522
2917
  const installSpec = pluginInstallSpec(plugin)
2523
2918
  const localAhead = local?.version && newerVersion(local.version, plugin.version)
2919
+ const uncertainInstall = uncertainInstallFor(plugin)
2524
2920
  const sourceKnown = typeof local?.source === 'string'
2525
2921
  const verifiedLocal = sameVersion && artifactUrl && sourceKnown && local.source === artifactUrl
2526
2922
  const recordedInstall = sameVersion && !sourceKnown && previouslyInstalledReviewedPlugin(plugin)
2527
2923
  const status = security ? '已废弃(安全风险)'
2528
- : plugin.status === 'deprecated' ? local?.version ? `已废弃 · 本机 ${local.version}` : '已废弃' : local?.error ? '加载异常' : localAhead
2924
+ : !bundleListReady ? loading ? '读取中…' : translateLocaleText('本机状态无法读取')
2925
+ : plugin.status === 'deprecated' ? local?.version ? `已废弃 · 本机 ${local.version}` : '已废弃' : local?.error ? '加载异常' : localAhead
2529
2926
  ? '本机版本高于团队目录版本' : sameVersion
2530
2927
  ? verifiedLocal ? local.enabled ? '本机已装(团队来源,已启用)' : '本机已装(团队来源,未启用)'
2531
2928
  : sourceKnown ? '本机同版,来源不同' : recordedInstall
@@ -2562,14 +2959,18 @@ window.__ModuleLoader__.load({
2562
2959
  h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8 } },
2563
2960
  installSpec && (!local || (local.version && local.version !== plugin.version && (!localAhead || artifactUrl))
2564
2961
  || (artifactUrl && sameVersion && !verifiedLocal))
2565
- ? h('button', { type: 'button', disabled: Boolean(workingId) || uncertainId === plugin.id || completedId === plugin.id,
2962
+ ? h('button', { type: 'button', disabled: loading || !bundleListReady
2963
+ || workingAny || Boolean(uncertainInstall) || completedId === plugin.id,
2566
2964
  onClick: () => void install(plugin), style: primaryButtonStyle },
2567
- workingId === plugin.id ? '安装中…' : uncertainId === plugin.id ? '先核对安装结果'
2965
+ workingId === plugin.id ? '安装中…' : uncertainInstall ? '先核对安装结果'
2568
2966
  : completedId === plugin.id ? '重启 DSH 后生效'
2569
2967
  : local ? sameVersion || localAhead ? '按团队审核包重新安装' : '更新到审核版本' : '安装到这台 DSH') : null,
2968
+ uncertainInstall ? h('button', { type: 'button', disabled: loading || workingAny,
2969
+ onClick: () => { if (!workingAny) setMessage(''); void refresh(plugin.id) },
2970
+ style: secondaryButtonStyle }, translateLocaleText('重新核对本机状态')) : null,
2570
2971
  plugin.status === 'published' && !local && !installSpec
2571
2972
  ? h('p', { style: mutedTextStyle }, '如需安装,请联系企业管理员安排。') : null,
2572
- local || uncertainId === plugin.id ? h('button', { type: 'button', onClick: () => openBundle(plugin.package_name), style: secondaryButtonStyle }, '打开 DSH 插件管理') : null,
2973
+ local || uncertainInstall ? h('button', { type: 'button', onClick: () => openBundle(plugin.package_name), style: secondaryButtonStyle }, '打开 DSH 插件管理') : null,
2573
2974
  local && !sameVersion && plugin.status === 'published' && !installSpec
2574
2975
  ? h('p', { style: mutedTextStyle }, '本机版本与团队审核版本不同;请联系企业管理员核对。') : null))
2575
2976
  })) : !loading && plugins.length && (!embedded || !hasSkillMatches)
@@ -3342,6 +3743,7 @@ window.__ModuleLoader__.load({
3342
3743
  setMessage(`未清除本机凭证和模型设置:${error instanceof Error ? error.message : '请稍后重试。'}`)
3343
3744
  return
3344
3745
  }
3746
+ setPluginRuntimeServiceScope('')
3345
3747
  try {
3346
3748
  await clearLocalConnection()
3347
3749
  setMessage('此设备的 ANT 凭证已撤销,ANT 模型设置已清除。团队服务地址已保留。')
@@ -3764,7 +4166,8 @@ window.__ModuleLoader__.load({
3764
4166
  dshCompatibilityCard,
3765
4167
  showReleaseNotes ? h('section', { style: { ...infoNoticeStyle, padding: 12 } },
3766
4168
  h('strong', { style: { fontSize: 14, lineHeight: 1.4 } }, '本版更新'),
3767
- h('p', { style: mutedTextStyle }, bundleRelease.release_notes)) : null,
4169
+ h('div', { 'aria-label': '更新说明内容', style: { display: 'grid', gap: 8 } },
4170
+ ...releaseNoteBlocks(bundleRelease.release_notes))) : null,
3768
4171
  ['incompatible', 'unknown'].includes(serviceCompatibility.state) ? h('p', { role: 'status', style: {
3769
4172
  ...feedbackNoticeStyle,
3770
4173
  color: serviceCompatibility.state === 'incompatible' ? 'var(--dsw-alias-state-error-primary)'
@@ -3807,14 +4210,86 @@ window.__ModuleLoader__.load({
3807
4210
  useLocaleRevision()
3808
4211
  const [tab, setTab] = useState('all')
3809
4212
  const [query, setQuery] = useState('')
4213
+ const serviceUrl = loadServiceUrl()
4214
+ const [, forcePluginRuntimeRender] = useState(0)
4215
+ useEffect(() => {
4216
+ const refresh = () => forcePluginRuntimeRender((revision) => revision + 1)
4217
+ pluginRuntimeState.listeners.add(refresh)
4218
+ setPluginRuntimeServiceScope(serviceUrl)
4219
+ return () => pluginRuntimeState.listeners.delete(refresh)
4220
+ }, [serviceUrl])
4221
+ const runtimeScopeMatches = pluginRuntimeState.serviceUrl === serviceUrl
4222
+ const uncertainInstalls = pluginRuntimeState.uncertainInstallsRef.current
4223
+ const uncertainInstallsRef = pluginRuntimeState.uncertainInstallsRef
4224
+ const workingAny = Boolean(pluginRuntimeState.workingId)
4225
+ const workingId = runtimeScopeMatches && pluginRuntimeState.workingServiceUrl === serviceUrl
4226
+ ? pluginRuntimeState.workingId : ''
4227
+ const completedId = runtimeScopeMatches ? pluginRuntimeState.completedId : ''
4228
+ const message = runtimeScopeMatches ? pluginRuntimeState.message : ''
4229
+ const refreshMessage = runtimeScopeMatches ? pluginRuntimeState.refreshMessage : ''
4230
+ const refreshMessageIsError = runtimeScopeMatches && pluginRuntimeState.refreshMessageIsError
4231
+ const setUncertainInstalls = (next) => {
4232
+ const value = typeof next === 'function' ? next(pluginRuntimeState.uncertainInstallsRef.current) : next
4233
+ pluginRuntimeState.uncertainInstalls.clear()
4234
+ for (const [pluginId, uncertainInstall] of Object.entries(value)) {
4235
+ pluginRuntimeState.uncertainInstalls.set(pluginId, uncertainInstall)
4236
+ }
4237
+ publishPluginRuntimeState()
4238
+ }
4239
+ const setWorkingId = (value) => {
4240
+ pluginRuntimeState.workingId = value
4241
+ pluginRuntimeState.workingServiceUrl = value ? serviceUrl : ''
4242
+ if (!value && pluginRuntimeState.message === pluginScopeBusyMessage) {
4243
+ pluginRuntimeState.message = ''
4244
+ pluginRuntimeState.messagePluginId.current = ''
4245
+ pluginRuntimeState.messagePackageName.current = ''
4246
+ pluginRuntimeState.messageClearOnResolve.current = false
4247
+ pluginRuntimeState.messageVersion.current += 1
4248
+ }
4249
+ publishPluginRuntimeState()
4250
+ }
4251
+ const setCompletedId = (value) => {
4252
+ pluginRuntimeState.completedId = value
4253
+ publishPluginRuntimeState()
4254
+ }
4255
+ const setMessage = (value, pluginId = '', packageName = '', clearOnResolve = false) => {
4256
+ pluginRuntimeState.message = value
4257
+ pluginRuntimeState.messagePluginId.current = value ? pluginId : ''
4258
+ pluginRuntimeState.messagePackageName.current = value ? packageName : ''
4259
+ pluginRuntimeState.messageClearOnResolve.current = Boolean(value) && clearOnResolve
4260
+ pluginRuntimeState.messageVersion.current += 1
4261
+ pluginRuntimeState.refreshMessage = ''
4262
+ pluginRuntimeState.refreshMessageIsError = false
4263
+ publishPluginRuntimeState()
4264
+ }
4265
+ const setRefreshFeedback = (value, isError = false) => {
4266
+ pluginRuntimeState.refreshMessage = value
4267
+ pluginRuntimeState.refreshMessageIsError = isError
4268
+ publishPluginRuntimeState()
4269
+ }
4270
+ const requestPluginRefresh = () => {
4271
+ pluginRuntimeState.refreshRevision += 1
4272
+ publishPluginRuntimeState()
4273
+ }
4274
+ const pluginPageState = { uncertainInstalls, setUncertainInstalls, uncertainInstallsRef,
4275
+ workingId, setWorkingId, completedId, setCompletedId, message, setMessage,
4276
+ refreshMessage, refreshMessageIsError, setRefreshFeedback, requestPluginRefresh,
4277
+ workingAny, serviceUrl,
4278
+ messageVersion: pluginRuntimeState.messageVersion,
4279
+ messagePluginId: pluginRuntimeState.messagePluginId,
4280
+ messagePackageName: pluginRuntimeState.messagePackageName,
4281
+ messageClearOnResolve: pluginRuntimeState.messageClearOnResolve,
4282
+ refreshGeneration: pluginRuntimeState.refreshGeneration,
4283
+ refreshRevision: pluginRuntimeState.refreshRevision }
3810
4284
  const navigation = {
3811
4285
  onAll: () => setTab('all'), onSkills: () => setTab('skills'),
3812
4286
  onPlugins: () => setTab('plugins'),
3813
4287
  }
3814
4288
  return tab === 'plugins'
3815
- ? h(PluginCenterPage, { remote, onBack, openBundle, query, onQueryChange: setQuery, ...navigation })
4289
+ ? h(PluginCenterPage, { remote, onBack, openBundle, query, onQueryChange: setQuery,
4290
+ pluginPageState, ...navigation })
3816
4291
  : h(SkillCenterPage, { remote, onBack, openBundle, query, onQueryChange: setQuery,
3817
- includePlugins: tab === 'all', ...navigation })
4292
+ pluginPageState, includePlugins: tab === 'all', ...navigation })
3818
4293
  }
3819
4294
 
3820
4295
  function brandingCacheKey() {