dsh-ant 0.6.0 → 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) {
@@ -622,15 +695,18 @@ window.__ModuleLoader__.load({
622
695
  const [defaults, setDefaults] = useState(null)
623
696
  const [localConcurrency, setLocalConcurrency] = useState(null)
624
697
  const [repositoryCacheLimitGib, setRepositoryCacheLimitGib] = useState(10)
698
+ const [ownerChallengeCode, setOwnerChallengeCode] = useState('')
699
+ const ownerChallengeMetaRef = useRef(null)
700
+ const ownerChallengeTimerRef = useRef(null)
625
701
  const [botId, setBotId] = useState('')
702
+ const botIdRef = useRef('')
626
703
  const [conversationKind, setConversationKind] = useState('dm')
627
704
  const [groupId, setGroupId] = useState('')
628
705
  const [ruleDescription, setRuleDescription] = useState('')
629
706
  const recognizedBaselineRef = useRef(null)
630
707
  const recognizedDescriptionRef = useRef(null)
631
- const autoAnalyzeDescriptionRef = useRef(null)
632
708
  const recognitionRequestRef = useRef(0)
633
- const recognitionTimerRef = useRef(null)
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,8 +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([])
645
- const [parallelAgentsOpen, setParallelAgentsOpen] = useState(false)
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
+ }, [])
646
745
  const [giteaToken, setGiteaToken] = useState('')
647
746
  const [tokenConfigured, setTokenConfigured] = useState(false)
648
747
  const [senderScope, setSenderScope] = useState('any')
@@ -662,9 +761,12 @@ window.__ModuleLoader__.load({
662
761
  setLoading(true)
663
762
  setMessage('')
664
763
  try {
665
- 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'] : [])]
666
767
  const responses = await Promise.all(paths.map((path) =>
667
- 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',
668
770
  ...(path === 'enrolled-bots' ? { headers: { 'x-ant-include-unbound': 'true' } } : {}) })
669
771
  .catch(() => null)))
670
772
  const data = await Promise.all(responses.map((response) => response
@@ -676,12 +778,30 @@ window.__ModuleLoader__.load({
676
778
  Array.isArray,
677
779
  (value) => value && typeof value === 'object' && !Array.isArray(value),
678
780
  (value) => Array.isArray(value?.items),
781
+ ...(ownerChallengeAtLoad ? [(value) => value && typeof value === 'object'
782
+ && typeof value.pending === 'boolean'] : []),
679
783
  ]
680
784
  const readable = responses.map((response, index) => Boolean(response?.ok && valid[index](data[index])))
681
- setLoadFailures(paths.filter((path, index) => !readable[index]))
785
+ setLoadFailures(paths.filter((path, index) => !readable[index] && path !== 'owner-challenge'))
682
786
  setBots(readable[0] ? data[0].bots : [])
683
787
  setBotLoadError(!readable[0])
684
- 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
+ }
685
805
  setGroups(readable[2] ? data[2] : [])
686
806
  setCards(readable[3] ? data[3] : [])
687
807
  setDefaults(readable[4] ? data[4] : null)
@@ -697,12 +817,16 @@ window.__ModuleLoader__.load({
697
817
  useEffect(() => { void load().catch((error) => setMessage(error.message)) }, [remote, revision])
698
818
  useEffect(() => { setMessage('') }, [activeTab])
699
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')
700
823
  const botConnected = Boolean(selectedBot?.available && selectedBot?.connected)
701
824
  const selectedEnrollment = enrolled.find((bot) => bot.bot_fingerprint === selectedBot?.fingerprint)
702
825
  const selectedGiteaOrigin = normalizeServiceUrl(selectedEnrollment?.gitea_origin)
703
826
  const taskPrOrigins = [...new Set(parameters.filter((item) => item.kind === 'gitea_pr')
704
827
  .map((item) => normalizeServiceUrl(item.gitea_origin)).filter(Boolean))]
705
828
  const selectedEnrollmentBound = selectedEnrollment?.device_bound_to_current === true
829
+ const ownerApprovalBound = selectedEnrollment?.owner_bound === true
706
830
  const dmChatSettingAvailable = typeof selectedEnrollment?.dm_chat_enabled === 'boolean'
707
831
  const dmChatEnabled = selectedEnrollment?.dm_chat_enabled !== false
708
832
  const dmChatStatus = !selectedBot ? '先选择机器人'
@@ -714,7 +838,7 @@ window.__ModuleLoader__.load({
714
838
  : !selectedEnrollmentBound ? '请先把机器人绑定到这台 DSH,再修改私聊设置。'
715
839
  : !dmChatSettingAvailable ? '当前团队服务端尚未提供私聊开关,请先升级服务端。'
716
840
  : dmChatEnabled
717
- ? '允许此机器人接收私聊;任务仍按已开放的规则执行。'
841
+ ? '允许此机器人接收私聊。匹配任务卡的请求按预授权执行;没有匹配卡片的明确任务可转给已绑定主人逐次审批。'
718
842
  : '新私聊消息不会交给 DSH;已提交任务仍会在原私聊回复。已开放能力不撤销,重新开启后未过期能力恢复触发。'
719
843
  const availableGroups = groups.filter((group) => group.bot_fingerprint === selectedBot?.fingerprint)
720
844
  const groupLabel = (group) => {
@@ -744,9 +868,8 @@ window.__ModuleLoader__.load({
744
868
  : !selectedBot ? '已有协作机器人;请选择一台,查看已开放的能力和聊天范围。'
745
869
  : authorizationLoadError ? `“${selectedBot.name}”已启用协作;已开放任务暂未读取,请刷新状态。`
746
870
  : !dmChatEnabled ? `“${selectedBot.name}”已启用协作;私聊已关闭,群聊仍按各自设置工作。`
747
- : selectedTaskCount === 0 ? `“${selectedBot.name}”已启用协作;下一步开放一项能力。你也可以先私聊自测普通聊天。`
748
- : `“${selectedBot.name}”已启用协作 · ${selectedTaskCount} 项能力已开放。可以私聊自测。`
749
- const nativeReviewerSelected = reviewers.some((item) => ['codex', 'claude-code'].includes(item))
871
+ : selectedTaskCount === 0 ? `“${selectedBot.name}”已启用协作;绑定审批私聊后,未配置的明确任务会先问你。常用任务可建卡直接放行。`
872
+ : `“${selectedBot.name}”已启用协作 · ${selectedTaskCount} 项预授权能力。其他明确任务可交给主人逐次审批。`
750
873
  const hasPrParameter = parameters.some((item) => item.kind === 'gitea_pr')
751
874
  const hasSelectedPrRule = activeCards.some((card) => card.bot_fingerprint === selectedBot?.fingerprint
752
875
  && Array.isArray(card.parameters) && card.parameters.some((rule) => rule.kind === 'gitea_pr'))
@@ -756,17 +879,25 @@ window.__ModuleLoader__.load({
756
879
  const codexAvailable = executors.some((item) => item.id === 'codex' && item.available)
757
880
  const claudeCodeAvailable = executors.some((item) => item.id === 'claude-code' && item.available)
758
881
  const dshAvailable = executors.some((item) => item.id === 'dsh' && item.available)
759
- const nativePrUnavailableReason = executors.find((item) => item.reason)?.reason || null
882
+ const executorUnavailableReason = executors.find((item) => item.reason)?.reason || null
760
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))
761
885
  const cardReviewerSummary = (card) => {
762
886
  const selected = Array.isArray(card.reviewers) && card.reviewers.length ? card.reviewers : [card.runner]
763
- const unavailable = selected.filter((runner) => runner !== 'dsh')
764
- if (card.runner !== 'dsh') return translateLocaleText('{executor} 默认执行方暂不可用',
887
+ const unavailable = selected.filter((runner) => !executors.some((item) => item.id === runner && item.available))
888
+ if (unavailable.includes(card.runner)) return translateLocaleText('{executor} 默认执行方暂不可用',
765
889
  { executor: reviewerLabel(card.runner) })
766
- return unavailable.length
767
- ? translateLocaleText('默认 DSH;{executors} 暂不可用',
768
- { executors: unavailable.map(reviewerLabel).join('、') })
769
- : translateLocaleText('DSH 任务 Agent')
890
+ const additional = selected.filter((runner) => runner !== card.runner)
891
+ const selectedSummary = additional.length
892
+ ? translateLocaleText('默认 {executor};交叉审查:{reviewers}',
893
+ { executor: reviewerLabel(card.runner), reviewers: additional.map(reviewerLabel).join('、') })
894
+ : translateLocaleText('{executor} 默认执行', { executor: reviewerLabel(card.runner) })
895
+ const summary = unavailable.length
896
+ ? `${selectedSummary};${unavailable.map(reviewerLabel).join('、')} 暂不可用`
897
+ : selectedSummary
898
+ return card.owner_approval_required !== false
899
+ && selected.some((id) => ['codex', 'claude-code'].includes(id))
900
+ ? translateLocaleText('{summary};启动前需要主人审批', { summary }) : summary
770
901
  }
771
902
  const requesterSummary = senderScope === 'allowlist'
772
903
  ? translateLocaleText('仅指定成员:{member_ids}',
@@ -775,8 +906,10 @@ window.__ModuleLoader__.load({
775
906
  const resultRouteSummary = translateLocaleText(conversationKind === 'group'
776
907
  ? '回复到原群(群内可见)' : '回复到发起任务的私聊')
777
908
  const ruleSummaryCurrent = recognizedDescriptionRef.current === ruleDescription.trim()
778
- const ruleSummaryPending = Boolean(ruleDescription.trim() && !ruleSummaryCurrent)
779
- const ruleSummaryReady = Boolean(ruleSummaryCurrent && (title || intent || parameters.length || taskPrompt))
909
+ const ruleSummaryPending = recognitionBusy
910
+ const ruleSummaryDirty = Boolean(ruleDescription.trim() && !ruleSummaryCurrent)
911
+ const ruleSummaryReady = Boolean(!ruleSummaryPending && ruleSummaryCurrent
912
+ && (title || intent || parameters.length || taskPrompt))
780
913
  const missingHarnessInputs = [...new Set([
781
914
  ...missingRequired,
782
915
  ...(parameters.some((rule) => rule.kind === 'gitea_pr'
@@ -814,21 +947,22 @@ window.__ModuleLoader__.load({
814
947
  const executorSummary = translateLocaleText(runner === 'codex' ? 'DSH Codex 子 Agent'
815
948
  : runner === 'claude-code' ? 'DSH Claude Code 子 Agent' : 'DSH · 当前授权模型'
816
949
  )
817
- const parallelAgentsSummary = reviewers.filter((item) => item !== runner).map(reviewerLabel).join('、')
818
950
  const cardBlockReason = enrollmentLoadError ? '暂时无法核对机器人是否已启用协作,请刷新状态后再试。'
819
951
  : !botConnected || !selectedEnrollmentBound ? '先在上方把机器人绑定到这台 DSH。'
820
952
  : loadFailures.includes('settings') || defaults?.task_rule_schema_version !== 3
821
953
  ? '团队服务端尚未支持 AI 任务 Harness 与运行时授权核验;请先升级 ANT 服务端。'
822
954
  : !ruleDescription.trim() ? '请先用自然语言描述要做的任务。'
955
+ : recognitionBusy ? 'AI 正在检查当前描述;分析完成后再开放。'
823
956
  : recognizedDescriptionRef.current !== ruleDescription.trim()
824
- ? 'AI 正在根据最新描述检查任务 Harness;分析完成后再开放。'
957
+ ? '描述已修改,请点击“立即分析”更新任务理解后再开放。'
825
958
  : !title.trim() || !intent.trim() || !taskPrompt.trim() ? '等待 AI 生成任务理解和处理要求。'
826
- : missingHarnessInputs.length ? `任务范围还有必需信息未明确:${missingHarnessInputs.join('、')}。请补充描述。`
959
+ : usesExternalExecutor && ownerApprovalRequired && !ownerApprovalBound
960
+ ? '这项任务需要主人审批,请先绑定审批私聊,或取消“启动前需要主人审批”。'
827
961
  : conversationKind === 'group' && loadFailures.includes('groups') ? '暂时无法核对目标群,请刷新状态后再试。'
828
962
  : conversationKind === 'group' && !groupId ? '请在“请求范围”中选择目标群;若列表为空,让群成员先 @机器人,再刷新群列表。'
829
963
  : senderScope === 'allowlist' && !senderIds.trim() ? '请填写可请求成员的 IM ID。'
830
- : reviewers.some((item) => item !== 'dsh')
831
- ? '当前只允许 DSH 执行协作任务。请将默认执行方改为 DSH,并移除未获准的外部 Agent。'
964
+ : reviewers.some((item) => !executors.some((executor) => executor.id === item && executor.available))
965
+ ? `这台 DSH 尚未启用所选执行器。${executorUnavailableReason || '请先启用对应 DSH 子 Agent 提供方。'}`
832
966
  : parameters.some((rule) => rule.kind === 'gitea_pr') && !tokenConfigured ? '请先在“本机与任务”中保存本机 Gitea 只读令牌。'
833
967
  : parameters.some((rule) => rule.kind === 'gitea_pr'
834
968
  && (typeof rule.gitea_origin !== 'string' || !normalizeServiceUrl(rule.gitea_origin) || !rule.repository))
@@ -838,11 +972,6 @@ window.__ModuleLoader__.load({
838
972
  ? `${translateLocaleText('任务描述中的 Gitea 来源必须与此机器人的连接器主机一致({origin});为保护令牌,ANT 不会向其他主机发送凭据。', { origin: selectedGiteaOrigin })} ${translateLocaleText('请在描述中改用该主机下的 PR 链接并重新分析。')}`
839
973
  : taskPrOrigins.length > 1
840
974
  ? '同一任务中的 PR 资源必须来自同一个 Gitea 主机。'
841
- : nativeReviewerSelected && (parameters.filter((rule) => rule.kind === 'gitea_pr').length !== 1
842
- || parameters.some((rule) => !['gitea_pr', 'git_sha'].includes(rule.kind)))
843
- ? 'Codex 和 Claude Code 当前只支持一个 Gitea PR 资源连接器;任务具体做审查、总结或解释仍由自然语言描述决定。'
844
- : nativeReviewerSelected && !parameters.find((rule) => rule.kind === 'gitea_pr')?.required
845
- ? '使用本机 Provider 时,Gitea PR 资源必须是必填项。'
846
975
  : reviewers.includes('dsh') && !dshAvailable
847
976
  ? '这台 DSH 尚未准备好任务执行器。'
848
977
  : ''
@@ -856,10 +985,8 @@ window.__ModuleLoader__.load({
856
985
  if (!next.includes(runner)) setRunner(next[0])
857
986
  }
858
987
  const selectRunner = (id) => {
988
+ if (!reviewers.includes(id)) return
859
989
  setRunner(id)
860
- setReviewers((current) => current.length > 1
861
- ? [...new Set([...current, id])]
862
- : [id])
863
990
  }
864
991
  const startNewCard = (context = {}) => {
865
992
  setEditingCardId(null); setEditingExpiresAt(null)
@@ -868,16 +995,16 @@ window.__ModuleLoader__.load({
868
995
  setRecognitionMessage('')
869
996
  recognizedBaselineRef.current = null
870
997
  recognizedDescriptionRef.current = null
871
- autoAnalyzeDescriptionRef.current = null
998
+ cancelRecognitionRequest()
872
999
  recognitionRequestRef.current += 1
873
1000
  setRuleDescription('')
874
1001
  setMissingRequired([]); setSuggestions([]); setRuleExamples([])
875
1002
  setTitle(''); setIntent(''); setTaskPrompt(''); setRunner('dsh')
876
- setReviewers(['dsh']); setParallelAgentsOpen(false)
1003
+ setReviewers(['dsh']); setOwnerApprovalRequired(true)
877
1004
  setParameters([]); setConversationKind(context.conversationKind || 'dm')
878
1005
  setGroupId(context.groupId || '')
879
1006
  setSenderScope('any'); setSenderIds(''); setDays(7); setCreateOpen(true)
880
- if (context.botId) setBotId(context.botId)
1007
+ if (context.botId) selectBot(context.botId)
881
1008
  }
882
1009
  const taskRuleDraftFromCard = (card, title) => {
883
1010
  const rules = (card.parameters || []).map((rule) => ({
@@ -901,9 +1028,10 @@ window.__ModuleLoader__.load({
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('')
906
- autoAnalyzeDescriptionRef.current = null
1034
+ cancelRecognitionRequest()
907
1035
  recognitionRequestRef.current += 1
908
1036
  setEditingCardId(copy ? null : card.id); setEditingExpiresAt(copy ? null : card.expires_at)
909
1037
  const taskDraft = taskRuleDraftFromCard(card, copy ? `${card.title.slice(0, 94)}(副本)` : card.title)
@@ -911,7 +1039,7 @@ window.__ModuleLoader__.load({
911
1039
  const existingDescription = card.rule_description || ''
912
1040
  recognizedDescriptionRef.current = existingDescription.trim() || null
913
1041
  setDraftNotice(existingDescription.trim() ? ''
914
- : '这项旧任务没有自然语言描述。请补充任务描述,让 AI 生成 Harness 后再开放。')
1042
+ : translateLocaleText('这项旧任务没有自然语言描述。请补充任务描述,让 AI 生成 Harness 后再开放。'))
915
1043
  setRuleDescription(existingDescription)
916
1044
  setMissingRequired([]); setSuggestions(taskDraft.suggestions); setRuleExamples(taskDraft.examples)
917
1045
  setTitle(taskDraft.title)
@@ -919,7 +1047,7 @@ window.__ModuleLoader__.load({
919
1047
  setIntent(taskDraft.intent)
920
1048
  setTaskPrompt(taskDraft.task_prompt); setRunner(card.runner)
921
1049
  setReviewers(Array.isArray(card.reviewers) && card.reviewers.length ? card.reviewers : [card.runner])
922
- setParallelAgentsOpen(false)
1050
+ setOwnerApprovalRequired(card.owner_approval_required !== false)
923
1051
  setParameters(taskDraft.parameters)
924
1052
  setConversationKind(card.conversation_kind); setGroupId(card.conversation_id || '')
925
1053
  setSenderScope(card.sender_scope); setSenderIds((card.sender_ids || []).join(', '))
@@ -940,13 +1068,18 @@ window.__ModuleLoader__.load({
940
1068
  setRecognitionMessage('先描述这项任务何时触发、要做什么以及涉及哪些资料。')
941
1069
  return
942
1070
  }
943
- if (recognitionTimerRef.current) clearTimeout(recognitionTimerRef.current)
1071
+ cancelRecognitionRequest()
944
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)
945
1077
  setRecognitionBusy(true); setMessage(''); setRecognitionMessage('AI 正在分析必需范围、可选条件和执行方式…')
946
1078
  try {
947
1079
  const response = await fetch('/api/ant-teams/delegation/recognize', {
948
1080
  method: 'POST', headers: { 'content-type': 'application/json' },
949
1081
  body: JSON.stringify({ description: requestedDescription }),
1082
+ signal: controller.signal,
950
1083
  })
951
1084
  const result = await readLocalJsonResponse(response)
952
1085
  if (!response.ok) throw new Error(result.message || '规则识别失败。')
@@ -996,19 +1129,14 @@ window.__ModuleLoader__.load({
996
1129
  ? 'AI 已更新任务理解。请复核必需信息、可选建议和运行方式;任务仍未开放。'
997
1130
  : 'AI 已生成待复核的任务 Harness。检查摘要和范围后再开放。')
998
1131
  } catch (error) {
999
- if (requestNumber === recognitionRequestRef.current) setRecognitionMessage(error.message)
1132
+ if (requestNumber === recognitionRequestRef.current) setRecognitionMessage(timedOut
1133
+ ? translateLocaleText('AI 分析等待超时,请检查团队模型后重试。') : error.message)
1000
1134
  } finally {
1135
+ clearTimeout(timeout)
1136
+ if (recognitionControllerRef.current === controller) recognitionControllerRef.current = null
1001
1137
  if (requestNumber === recognitionRequestRef.current) setRecognitionBusy(false)
1002
1138
  }
1003
1139
  }
1004
- useEffect(() => {
1005
- if (autoAnalyzeDescriptionRef.current !== ruleDescription || !ruleDescription.trim()) return undefined
1006
- const description = ruleDescription
1007
- recognitionTimerRef.current = setTimeout(() => {
1008
- if (autoAnalyzeDescriptionRef.current === description) void identifyRule(description)
1009
- }, 900)
1010
- return () => clearTimeout(recognitionTimerRef.current)
1011
- }, [ruleDescription])
1012
1140
  const saveGiteaToken = async () => {
1013
1141
  if (!giteaToken.trim()) return
1014
1142
  setBusy(true)
@@ -1033,10 +1161,41 @@ window.__ModuleLoader__.load({
1033
1161
  const result = await readLocalJsonResponse(response)
1034
1162
  if (!response.ok) throw new Error(result.message || '协作机器人启用失败。')
1035
1163
  await load()
1036
- setMessage('协作机器人已启用。你可以分别设置私聊和群聊,再开放需要的能力。')
1164
+ setMessage('协作机器人已启用。任务卡可预授权固定任务;绑定审批私聊后,没有匹配卡片的明确任务会逐次询问主人。')
1037
1165
  } catch (error) { setMessage(error.message) }
1038
1166
  finally { setBusy(false) }
1039
1167
  }
1168
+ const requestOwnerApprovalBinding = async () => {
1169
+ if (!selectedBot || !selectedEnrollmentBound) return
1170
+ const requestedBot = selectedBot
1171
+ setBusy(true); clearOwnerChallenge()
1172
+ try {
1173
+ const response = await fetch('/api/ant-teams/delegation/owner-challenge', {
1174
+ method: 'POST', headers: { 'content-type': 'application/json' },
1175
+ body: JSON.stringify({ bot_id: requestedBot.bot_id, bot_fingerprint: requestedBot.fingerprint }),
1176
+ })
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
+ }
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
1189
+ setOwnerChallengeCode(result.code)
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) }
1197
+ finally { setBusy(false) }
1198
+ }
1040
1199
  const saveGroupChat = async (enabled, targetGroupId) => {
1041
1200
  if (!selectedBot || !selectedEnrollmentBound || !targetGroupId.trim()) return
1042
1201
  const targetHasTasks = activeCards.some((card) => card.bot_fingerprint === selectedBot.fingerprint
@@ -1131,6 +1290,15 @@ window.__ModuleLoader__.load({
1131
1290
  ...collaborationGridStyle, gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 420px), 1fr))',
1132
1291
  }
1133
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)))
1134
1302
  const setupStepper = h('section', { 'aria-label': '首次设置引导', style: { display: 'grid', gap: 6 } },
1135
1303
  h('p', { style: mutedTextStyle }, '首次设置可按顺序完成;你也可以直接切换页面。普通聊天不需要先开放任务。'),
1136
1304
  h('ol', { 'aria-label': '设置步骤', style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 150px), 1fr))',
@@ -1163,7 +1331,26 @@ window.__ModuleLoader__.load({
1163
1331
  disabled: busy || !selectedBot || !selectedEnrollmentBound || enrollmentLoadError || !dmChatSettingAvailable,
1164
1332
  onChange: (event) => void saveDmChat(event.target.checked), 'aria-label': '允许机器人私聊' }),
1165
1333
  h('strong', null, '允许私聊普通聊天'), h('span', { style: mutedTextStyle }, dmChatStatus)),
1166
- 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)
1167
1354
  const createCard = async (event) => {
1168
1355
  event.preventDefault()
1169
1356
  setDraftNotice('')
@@ -1173,14 +1360,11 @@ window.__ModuleLoader__.load({
1173
1360
  setSubmissionError(localizedReason); setMessage(localizedReason)
1174
1361
  }
1175
1362
  if (!selectedBot || !botConnected || !selectedEnrollmentBound) { reject('请先在这台 DSH 绑定选中的协作机器人。'); return }
1176
- if (reviewers.some((item) => item !== 'dsh')) {
1177
- reject('当前无法核验外部子 Agent 的逐任务文件、Git 和网络权限;请将任务执行方设为 DSH。'); return
1178
- }
1179
1363
  if (editingCardId && (!editingExpiresAt || editingExpiresAt <= Math.floor(Date.now() / 1000))) {
1180
1364
  reject('这项能力已到期,无法继续编辑;请返回后重新开放一项能力。'); return
1181
1365
  }
1182
1366
  if (!ruleDescription.trim() || !title.trim() || !intent.trim() || !taskPrompt.trim()) {
1183
- reject('请填写任务描述并点击“识别规则”,检查摘要后再开放。'); return
1367
+ reject('请填写任务描述并点击“立即分析”,检查摘要后再开放。'); return
1184
1368
  }
1185
1369
  const bytes = (value) => new TextEncoder().encode(value).length
1186
1370
  if (bytes(ruleDescription.trim()) > 4000 || bytes(title.trim()) > 100
@@ -1245,12 +1429,6 @@ window.__ModuleLoader__.load({
1245
1429
  if (!reviewers.length || !reviewers.includes(runner)) {
1246
1430
  reject('请至少选择一位任务执行 Agent,并设置普通请求的默认执行方。'); return
1247
1431
  }
1248
- if (nativeReviewerSelected && !((normalized.length === 1
1249
- && normalized[0].kind === 'gitea_pr' && normalized[0].required)
1250
- || (normalized.length === 2 && normalized.some((rule) => rule.kind === 'gitea_pr' && rule.required)
1251
- && normalized.some((rule) => rule.kind === 'git_sha' && rule.name === 'head_sha' && !rule.required)))) {
1252
- reject('Codex 和 Claude Code 当前只支持一个必填 Gitea PR 资源连接器;任务行为仍由自然语言描述决定。'); return
1253
- }
1254
1432
  const scope = conversationKind === 'group'
1255
1433
  ? translateLocaleText('群 {group_id}', { group_id: groupId })
1256
1434
  : translateLocaleText('私聊')
@@ -1258,8 +1436,8 @@ window.__ModuleLoader__.load({
1258
1436
  ? '原群(群内可见)' : '发起任务的私聊')
1259
1437
  const executionDetails = {
1260
1438
  dsh: translateLocaleText('DSH 使用获准的团队模型处理对话内容和 ANT 已核验后提供的资源。'),
1261
- codex: translateLocaleText('Codex 当前不能用于协作任务,因为 ANT 无法核验其逐任务文件、Git 和网络权限。'),
1262
- 'claude-code': translateLocaleText('Claude Code 当前不能用于协作任务,因为 ANT 无法核验其逐任务文件、Git 和网络权限。'),
1439
+ codex: translateLocaleText('Codex 使用这台 DSH 的 Provider Profile 权限;可访问范围由 Codex/DSH 本机配置决定。任务描述不会限制操作系统权限。'),
1440
+ 'claude-code': translateLocaleText('Claude Code 使用这台 DSH 的 Provider Profile 权限;可访问范围由 Claude Code/DSH 本机配置决定。任务描述不会限制操作系统权限。'),
1263
1441
  }
1264
1442
  const executionInfo = reviewers.length > 1
1265
1443
  ? translateLocaleText('普通请求默认由 {runner} 执行;请求人明确要求多 Agent 分别处理时,会并行启动 {reviewers},分别返回结果。{details}',
@@ -1280,7 +1458,8 @@ window.__ModuleLoader__.load({
1280
1458
  bot_id: selectedBot.bot_id, bot_fingerprint: selectedBot.fingerprint,
1281
1459
  title, rule_description: ruleDescription.trim(), trigger_phrases: [],
1282
1460
  intent, examples: ruleExamples,
1283
- parameters: normalized, task_prompt: taskPrompt, runner, reviewers, workspace: null,
1461
+ parameters: normalized, task_prompt: taskPrompt, runner, reviewers,
1462
+ owner_approval_required: ownerApprovalRequired, workspace: null,
1284
1463
  sender_scope: senderScope, sender_ids: senderScope === 'allowlist'
1285
1464
  ? senderIds.split(/[\s,,]+/u).filter(Boolean) : [], conversation_kind: conversationKind,
1286
1465
  ...(conversationKind === 'group' ? { conversation_id: groupId } : {}),
@@ -1288,7 +1467,7 @@ window.__ModuleLoader__.load({
1288
1467
  })
1289
1468
  const result = await readLocalJsonResponse(response)
1290
1469
  if (!response.ok && result.code === 'IM_TASK_RULE_REQUIRED') {
1291
- throw new Error('当前 ANT 服务端不支持自然语言任务 Harness,请升级 ANT 服务端后再开放任务。')
1470
+ throw new Error('当前 ANT 服务端版本过旧,请升级服务端后再开放任务。')
1292
1471
  }
1293
1472
  if (!response.ok && reviewers.includes('claude-code') && result.code === 'INVALID_REQUEST') {
1294
1473
  throw new Error('当前团队服务端可能尚不支持 Claude Code 审查配置。请管理员升级服务端后刷新重试。')
@@ -1296,26 +1475,35 @@ window.__ModuleLoader__.load({
1296
1475
  if (!response.ok) throw new Error(result.message || '能力保存失败。')
1297
1476
  const savedReviewers = Array.isArray(result.reviewers) && result.reviewers.length
1298
1477
  ? result.reviewers : [result.runner]
1299
- if ([...savedReviewers].sort().join('\0') !== [...reviewers].sort().join('\0')) {
1300
- 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') {
1301
1482
  const cleanup = await fetch('/api/ant-teams/delegation/cards', {
1302
1483
  method: 'DELETE', headers: { 'content-type': 'application/json' },
1303
1484
  body: JSON.stringify({ id: result.id }),
1304
1485
  }).catch(() => null)
1305
1486
  if (!cleanup?.ok) {
1306
1487
  await load().catch(() => undefined)
1307
- 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)
1308
1495
  }
1309
1496
  }
1310
- throw new Error('当前团队服务端尚不支持多 Agent 审查配置;请管理员升级 ANT 服务端后重试。')
1497
+ throw new Error(reviewersDropped
1498
+ ? '当前团队服务端尚不支持多 Agent 审查配置;请管理员升级 ANT 服务端后重试。'
1499
+ : '当前团队服务端尚不支持任务卡主人审批;请管理员升级 ANT 服务端后重试。')
1311
1500
  }
1312
1501
  recognizedBaselineRef.current = null
1313
1502
  recognizedDescriptionRef.current = null
1314
- autoAnalyzeDescriptionRef.current = null
1503
+ cancelRecognitionRequest()
1315
1504
  recognitionRequestRef.current += 1
1316
1505
  setRuleDescription(''); setMissingRequired([]); setSuggestions([]); setRuleExamples([])
1317
- setTitle(''); setIntent(''); setTaskPrompt(''); setParameters([]); setRunner('dsh'); setReviewers(['dsh'])
1318
- setParallelAgentsOpen(false)
1506
+ setTitle(''); setIntent(''); setTaskPrompt(''); setParameters([]); setRunner('dsh'); setReviewers(['dsh']); setOwnerApprovalRequired(true)
1319
1507
  setEditingCardId(null); setEditingExpiresAt(null)
1320
1508
  await load()
1321
1509
  setCreateOpen(false)
@@ -1340,22 +1528,22 @@ window.__ModuleLoader__.load({
1340
1528
  }
1341
1529
  const copyCardToDraft = (card) => {
1342
1530
  setEditingCardId(null); setEditingExpiresAt(null)
1343
- setParallelAgentsOpen(false)
1344
1531
  const copiedDescription = card.rule_description || ''
1345
1532
  recognizedDescriptionRef.current = copiedDescription.trim() || null
1346
- autoAnalyzeDescriptionRef.current = null
1533
+ cancelRecognitionRequest()
1347
1534
  recognitionRequestRef.current += 1
1348
1535
  setDraftNotice(copiedDescription.trim()
1349
- ? '已复制到新草稿。复制不保留原有效期;请检查任务描述和请求范围。'
1350
- : '这项旧任务没有自然语言描述。请补充任务描述,让 AI 生成 Harness 后再开放。')
1536
+ ? translateLocaleText('已复制到新草稿。复制不保留原有效期;请检查任务描述和请求范围。')
1537
+ : translateLocaleText('这项旧任务没有自然语言描述。请补充任务描述,让 AI 生成 Harness 后再开放。'))
1351
1538
  const cardBot = bots.find((bot) => bot.fingerprint === card.bot_fingerprint)
1539
+ const copiedBotId = card.bot_id || cardBot?.bot_id || ''
1540
+ selectBot(copiedBotId)
1352
1541
  const titleChars = Array.from(translateLocaleText('复制:{title}', {
1353
1542
  title: card.title || translateLocaleText('未命名能力'),
1354
1543
  }))
1355
1544
  while (new TextEncoder().encode(titleChars.join('')).length > 100) titleChars.pop()
1356
1545
  const taskDraft = taskRuleDraftFromCard(card, titleChars.join(''))
1357
1546
  recognizedBaselineRef.current = taskDraft
1358
- setBotId(card.bot_id || cardBot?.bot_id || '')
1359
1547
  setRuleDescription(copiedDescription)
1360
1548
  setMissingRequired([]); setSuggestions(taskDraft.suggestions); setRuleExamples(taskDraft.examples)
1361
1549
  setTitle(taskDraft.title)
@@ -1363,6 +1551,7 @@ window.__ModuleLoader__.load({
1363
1551
  setTaskPrompt(taskDraft.task_prompt)
1364
1552
  setRunner(['dsh', 'codex', 'claude-code'].includes(card.runner) ? card.runner : 'dsh')
1365
1553
  setReviewers(Array.isArray(card.reviewers) && card.reviewers.length ? card.reviewers : [card.runner || 'dsh'])
1554
+ setOwnerApprovalRequired(card.owner_approval_required !== false)
1366
1555
  setParameters(taskDraft.parameters)
1367
1556
  setConversationKind(card.conversation_kind === 'group' ? 'group' : 'dm')
1368
1557
  setGroupId(card.conversation_id || '')
@@ -1392,10 +1581,10 @@ window.__ModuleLoader__.load({
1392
1581
  loading ? '读取中…' : '刷新状态')),
1393
1582
  h('p', { role: 'status', style: mutedTextStyle }, collaborationStatus),
1394
1583
  bots.length ? h('label', null, '我的机器人', h('select', { id: 'ant-bot-selector', value: botId,
1395
- onChange: (event) => { setBotId(event.target.value); setGroupId('') }, style: inputStyle },
1584
+ onChange: (event) => { selectBot(event.target.value); setGroupId('') }, style: inputStyle },
1396
1585
  h('option', { value: '' }, '请选择机器人'),
1397
1586
  ...bots.map((bot) => h('option', { key: bot.bot_id, value: bot.bot_id,
1398
- disabled: !bot.available || !bot.connected }, rawLocaleText(bot.name),
1587
+ disabled: !bot.available || !bot.connected }, rawLocaleText(bot.name), ' · ', botPlatformLabel(bot),
1399
1588
  bot.available && bot.connected ? null : '(未连接)')))) : null,
1400
1589
  connectedBots.length === 0 && !loading && !botLoadError ? h('div', { style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' } },
1401
1590
  h('button', { type: 'button', onClick: openImSettings, style: secondaryButtonStyle }, '打开 DSH-IM 设置'),
@@ -1404,7 +1593,9 @@ window.__ModuleLoader__.load({
1404
1593
  style: secondaryButtonStyle }, '检查 DSH-IM 连接') : null,
1405
1594
  selectedBot && !selectedEnrollmentBound ? h('button', { type: 'button', disabled: busy || !botConnected || enrollmentLoadError,
1406
1595
  onClick: () => void enrollBot(), style: primaryButtonStyle },
1407
- selectedEnrollment ? '绑定原协作配置到这台 DSH' : '启用并绑定到这台 DSH') : null)
1596
+ selectedEnrollment ? '绑定原协作配置到这台 DSH' : '启用并绑定到这台 DSH') : null,
1597
+ selectedEnrollmentBound && botConnected ? h('p', { style: mutedTextStyle },
1598
+ '任务能力和设备绑定状态请在此处查看;主人审批身份在“私聊入口”中管理。') : null)
1408
1599
  const groupTaskStatusPanel = selectedEnrollmentBound && botConnected ? h('section', {
1409
1600
  id: 'ant-group-task-status', role: 'region', 'aria-label': '各群聊天与任务状态', tabIndex: -1,
1410
1601
  style: { ...collaborationCardStyle, gap: 10 } },
@@ -1415,7 +1606,7 @@ window.__ModuleLoader__.load({
1415
1606
  loading ? '读取中…' : '刷新群列表')),
1416
1607
  h('p', { style: mutedTextStyle }, groupChatLoadFailed
1417
1608
  ? '群列表暂不可用,请刷新状态后重试。'
1418
- : availableGroups.length ? '每个群分别控制普通聊天和任务;名称取自 DSH-IM 已保存目标,括号内为识别码末两位。未匹配名称时显示完整识别码。'
1609
+ : availableGroups.length ? '此开关只控制普通聊天。任务卡可预授权固定任务;绑定主人审批私聊后,群成员 @机器人发起的其他明确任务也可逐次审批。名称取自 DSH-IM 已保存目标。'
1419
1610
  : '先在目标群 @机器人,再刷新状态发现群。'),
1420
1611
  availableGroups.length ? h('div', { id: 'ant-group-card-grid', style: collaborationGridStyle },
1421
1612
  ...availableGroups.map((group) => {
@@ -1447,9 +1638,10 @@ window.__ModuleLoader__.load({
1447
1638
  h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'start', gap: 12, flexWrap: 'wrap' } },
1448
1639
  h('div', { style: { display: 'grid', gap: 4 } },
1449
1640
  h('h3', { style: sectionTitleStyle }, '协作总览'),
1450
- h('p', { style: mutedTextStyle }, '选择机器人,设置聊天入口,再按需要开放任务规则。聊天和任务授权分别控制。')),
1641
+ h('p', { style: mutedTextStyle }, '选择机器人并绑定一次主人审批私聊。任务卡可预授权常用任务;其他请求由主人逐次决定。')),
1451
1642
  h('span', { style: { ...mutedTextStyle, padding: '4px 9px', borderRadius: 999,
1452
- 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)) : '尚未选择机器人')),
1453
1645
  loadFailures.length ? h('p', { role: 'status', style: mutedTextStyle },
1454
1646
  `部分协作信息暂时无法读取:${loadFailures.map((path) => loadNames[path]).join('、')}。已读取的信息仍显示,请稍后刷新状态。`) : null,
1455
1647
  h('div', { id: 'ant-collaboration-settings-grid', style: collaborationSettingsGridStyle },
@@ -1485,7 +1677,10 @@ window.__ModuleLoader__.load({
1485
1677
  : translateLocaleText('私聊')
1486
1678
  const requester = translateLocaleText(card.sender_scope === 'allowlist' ? '指定成员可请求'
1487
1679
  : card.conversation_kind === 'group' ? '群成员可请求' : '可私聊者可请求')
1488
- const blockedExternalExecutor = card.runner !== 'dsh' || card.cross_review_always === true
1680
+ const configuredCardRunners = Array.isArray(card.reviewers) && card.reviewers.length
1681
+ ? card.reviewers : [card.runner]
1682
+ const blockedExternalExecutor = !configuredCardRunners.every((runner) =>
1683
+ executors.some((executor) => executor.id === runner && executor.available))
1489
1684
  const examplesText = Array.isArray(card.examples) ? card.examples.join('、') : ''
1490
1685
  const requesterIds = card.sender_scope === 'allowlist' && Array.isArray(card.sender_ids) ? card.sender_ids.join('、') : ''
1491
1686
  const remainingDaysLabel = remainingDays <= 7 ? 'var(--dsw-alias-state-warning-primary, var(--dsw-alias-label-secondary))'
@@ -1504,7 +1699,7 @@ window.__ModuleLoader__.load({
1504
1699
  })),
1505
1700
  blockedExternalExecutor ? h('p', { role: 'status', style: { ...mutedTextStyle,
1506
1701
  color: 'var(--dsw-alias-state-warning-primary, var(--dsw-alias-label-secondary))' } },
1507
- '此任务暂不可执行:当前无法核验外部 Agent 的逐任务权限。点击“编辑”,改用 DSH 并移除外部 Agent 后可重新开放。') : null,
1702
+ '此任务暂不可执行:这台 DSH 尚未启用所选的全部执行器。请启用对应的 DSH 子 Agent 提供方后刷新。') : null,
1508
1703
  card.rule_description
1509
1704
  ? h('span', { style: { ...mutedTextStyle, overflowWrap: 'anywhere' } }, rawLocaleText(card.rule_description))
1510
1705
  : h('span', { role: 'status', style: { ...mutedTextStyle, color: 'var(--dsw-alias-state-warning-primary, var(--dsw-alias-label-secondary))' } },
@@ -1543,9 +1738,10 @@ window.__ModuleLoader__.load({
1543
1738
  ? task.repository
1544
1739
  ? translateLocaleText('{repository} PR #{pr_number} 差异审查', { repository: task.repository, pr_number: task.pr_number })
1545
1740
  : translateLocaleText('PR #{pr_number} 差异审查', { pr_number: task.pr_number })
1546
- : '任务'
1741
+ : translateLocaleText('任务')
1547
1742
  const status = translateLocaleText(task.outcome === 'canceled' ? '已取消' : task.outcome === 'failed' ? '未完成' : ({
1548
- pending: '排队中', running: '处理中', reply_pending: '结果待发送', reply_attempted: '回复状态待核对', done: '已回复',
1743
+ pending: '排队中', claimed: '排队中', running: '处理中', finishing: '结果待核对', completed: '已完成',
1744
+ reply_pending: '结果待发送', reply_attempted: '回复状态待核对', done: '已回复',
1549
1745
  }[task.status] || task.status))
1550
1746
  const createdAt = task.created_at && Number.isFinite(Date.parse(task.created_at))
1551
1747
  ? formatLocaleDate(task.created_at) : ''
@@ -1559,7 +1755,9 @@ window.__ModuleLoader__.load({
1559
1755
  h('div', { style: mutedTextStyle }, taskStatusLine),
1560
1756
  task.failure_reason ? h('div', { role: 'alert', style: warningNoticeStyle },
1561
1757
  h('span', { style: { ...mutedTextStyle, whiteSpace: 'pre-line' } }, translateLocaleText(
1562
- '任务未完成:{reason}\n处理建议:按错误提示修复配置或请求内容后,在原群或私聊重新发起。',
1758
+ task.status === 'finishing'
1759
+ ? '结果状态待确认:{reason}\n处理建议:恢复原团队连接或审批私聊权限后刷新状态;不要重复发起相同任务。'
1760
+ : '任务未完成:{reason}\n处理建议:按错误提示修复配置或请求内容后,在原群或私聊重新发起。',
1563
1761
  { reason: task.failure_reason }))) : null,
1564
1762
  task.delivery_error ? h('div', { role: 'alert', style: warningNoticeStyle },
1565
1763
  h('span', { style: { ...mutedTextStyle, whiteSpace: 'pre-line' } }, translateLocaleText(
@@ -1568,7 +1766,8 @@ window.__ModuleLoader__.load({
1568
1766
  task.status === 'reply_attempted' && !task.delivery_error ? h('div', { role: 'status', style: warningNoticeStyle },
1569
1767
  h('span', { style: mutedTextStyle }, translateLocaleText(
1570
1768
  'ANT 尚未确认消息平台是否收到结果。请先检查原群或私聊,再决定是否重新发起。'))) : null),
1571
- ['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,
1572
1771
  onClick: () => void cancelTask(task), style: secondaryButtonStyle }, '取消任务') : null)
1573
1772
  })
1574
1773
  : [h('p', { key: 'none', style: mutedTextStyle }, loadFailures.includes('status')
@@ -1576,11 +1775,21 @@ window.__ModuleLoader__.load({
1576
1775
  h('div', { role: 'region', 'aria-label': '任务规则编辑器', style: { display: activeTab === 'collaboration' ? 'grid' : 'none' } },
1577
1776
  h('details', { id: 'ant-new-task', open: createOpen,
1578
1777
  onToggle: (event) => setCreateOpen(event.currentTarget.open),
1579
- style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, padding: 12 } },
1580
- 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('草稿 · 未开放'))),
1581
1790
  h('div', { style: { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr)',
1582
- gap: 12, alignItems: 'start', marginTop: 12 } },
1583
- 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 } },
1584
1793
  editingCardId ? h('p', { role: 'status', style: mutedTextStyle },
1585
1794
  `保存会生成新的授权版本并停止旧版本;到期时间保持不变:${formatLocaleDate(editingExpiresAt * 1000)}。已提交任务继续使用原配置。`) : null,
1586
1795
  h('p', { style: mutedTextStyle }, selectedBot
@@ -1588,78 +1797,104 @@ window.__ModuleLoader__.load({
1588
1797
  : '请先在上方选择机器人并启用协作。'),
1589
1798
  draftNotice ? h('p', { role: 'status', style: { ...mutedTextStyle,
1590
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,
1591
1805
  selectedEnrollmentBound ? h('p', { style: mutedTextStyle },
1592
1806
  '机器人及其任务能力只在这台 DSH 设备接收和执行;同一账号的其他设备不会继承本机授权。') : null,
1593
1807
  selectedEnrollment && !selectedEnrollmentBound ? h('p', { style: mutedTextStyle },
1594
1808
  '这是尚未绑定到设备的机器人。请在要继续使用它的电脑重新启用;原协作配置会保留。若旧设备访问曾被撤销,原任务权限不会恢复,需要重新开放。') : null,
1595
1809
  selectedBot && conversationKind === 'group' && !selectedBot.history_available ? h('p', { role: 'status', style: mutedTextStyle },
1596
1810
  '这个机器人当前不能读取平台群历史;群聊只能参考实际送达的消息和本机近期记录。请检查 DSH-IM 权限。') : null,
1597
- h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 320px), 1fr))',
1598
- gap: 12, alignItems: 'start' } },
1599
- h('div', { style: { display: 'grid', gap: 9, minWidth: 0 } },
1600
- h('label', null, '描述规则', h('textarea', { required: true, maxLength: 4000, rows: 4, value: ruleDescription,
1601
- onChange: (event) => {
1602
- autoAnalyzeDescriptionRef.current = event.target.value
1603
- recognitionRequestRef.current += 1
1604
- setRuleDescription(event.target.value)
1605
- setRecognitionBusy(false)
1606
- setRecognitionMessage(event.target.value.trim() ? '你暂停输入后,AI 会自动检查任务是否完整。' : '')
1607
- },
1608
- placeholder: '用日常语言说明要完成什么、可处理哪些资料和希望怎样返回。例如:审查来自 https://gitea.example.com/team/shop 的 PR,指出可定位的问题,不修改代码;请求人可指定 Head SHA,未指定时用最新 Head。', style: inputStyle })),
1609
- h('p', { style: mutedTextStyle }, 'AI 会在输入停顿后分析必需范围、缺失信息和可选条件。执行方、请求人、群范围和有效期由下方通用授权控制。'),
1610
- h('div', { style: { display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' } },
1611
- h('button', { type: 'button', disabled: recognitionBusy || busy || !ruleDescription.trim(),
1612
- onClick: () => void identifyRule(), style: secondaryButtonStyle }, recognitionBusy ? 'AI 正在分析…' : ruleSummaryReady ? '立即重新分析' : '立即分析'),
1613
- h('span', { style: mutedTextStyle }, '自动分析和手动分析都会调用当前团队模型;只生成建议和待复核草稿,不会开放任务。')),
1614
- recognitionMessage ? h('p', { role: 'status', 'aria-live': 'polite', style: feedbackNoticeStyle }, recognitionMessage) : null),
1615
- 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)' } },
1616
1826
  h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } },
1617
- h('strong', null, ruleSummaryPending ? 'AI 正在检查当前描述' : title || 'AI 任务助手'),
1618
- h('span', { style: mutedTextStyle }, ruleSummaryPending ? '描述已更新' : ruleSummaryReady ? '待复核 · 尚未开放' : '填写描述后开始分析')),
1619
- h('p', { style: { ...mutedTextStyle, overflowWrap: 'anywhere' } }, ruleSummaryPending
1620
- ? 'AI 正在根据当前描述更新任务理解。上一次分析结果暂不用于授权开放。' : ruleSummaryNarrative),
1621
- ruleSummaryReady && missingHarnessInputs.length ? h('p', { role: 'status', style: { ...mutedTextStyle,
1622
- color: 'var(--dsw-alias-state-warning-primary, var(--dsw-alias-label-secondary))' } },
1623
- `还需要明确:${missingHarnessInputs.join(';')}`) : ruleSummaryReady
1624
- ? h('p', { role: 'status', style: mutedTextStyle }, 'AI 暂未发现必需信息缺项;请确认任务来源和处理范围符合预期。') : null,
1625
- ruleSummaryReady && suggestions.length ? h('p', { style: mutedTextStyle }, `可选补充:${suggestions.join(';')}`) : null,
1626
- ruleSummaryReady && ruleExamples.length ? h('p', { style: mutedTextStyle }, `自然请求示例:${ruleExamples.join(';')}`) : null,
1627
- h('p', { style: mutedTextStyle }, ruleSummaryReady
1628
- ? '草稿未开放。检查任务理解、必需范围和建议;有误就改描述,AI 会重新分析。'
1629
- : ruleSummaryPending ? '分析完成前不能开放任务。' : 'AI 会整理任务理解;草稿不会自动开放。'))),
1630
- h('section', { style: { ...panelStyle, padding: 10, gap: 9, display: 'grid',
1631
- gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 280px), 1fr))' } },
1632
- h('strong', { style: { gridColumn: '1 / -1' } }, '通用授权'),
1633
- h('p', { style: { ...mutedTextStyle, gridColumn: '1 / -1' } },
1634
- '上方描述规则任务要做什么;这里限定谁能请求、在哪里请求、由谁执行和授权多久。'),
1635
- h('label', null, '执行方', h('select', { value: runner, onChange: (event) => selectRunner(event.target.value),
1636
- style: inputStyle },
1637
- h('option', { value: 'dsh', disabled: !dshAvailable && runner !== 'dsh' }, 'DSH · 当前团队模型'),
1638
- h('option', { value: 'codex', disabled: runner !== 'codex' }, 'Codex · DSH 子 Agent'),
1639
- h('option', { value: 'claude-code', disabled: runner !== 'claude-code' }, 'Claude Code · DSH 子 Agent')),
1640
- h('span', { style: mutedTextStyle }, runner === 'codex'
1641
- ? 'Codex 暂不可用于协作任务:ANT 无法核验它的逐任务文件、Git 和网络权限。'
1642
- : runner === 'claude-code' ? 'Claude Code 暂不可用于协作任务:ANT 无法核验它的逐任务文件、Git 和网络权限。'
1643
- : 'DSH 使用获准团队模型处理对话内容和 ANT 已核验后提供的资源。')),
1644
- h('details', { id: 'ant-parallel-agents-details', open: parallelAgentsOpen,
1645
- onToggle: (event) => setParallelAgentsOpen(event.currentTarget.open),
1646
- style: { gridColumn: '1 / -1', border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, padding: 10 } },
1647
- h('summary', { style: { cursor: 'pointer', fontWeight: 600 } }, parallelAgentsSummary
1648
- ? `并行执行:${parallelAgentsSummary}` : '多 Agent 并行(可选)'),
1649
- h('p', { style: mutedTextStyle }, '当前只开放 DSH。外部 Agent 的逐任务文件、Git 和网络权限无法由 ANT 核验;编辑旧任务时可取消其选择并改用 DSH。'),
1650
- h('div', { style: { display: 'flex', gap: 14, flexWrap: 'wrap' } },
1651
- h('label', null, h('input', { type: 'checkbox', 'aria-label': '任务 Agent DSH', checked: reviewers.includes('dsh'),
1652
- disabled: !dshAvailable && !reviewers.includes('dsh'),
1653
- onChange: (event) => changeReviewer('dsh', event.target.checked) }), ' DSH'),
1654
- h('label', null, h('input', { type: 'checkbox', 'aria-label': '任务 Agent Codex', checked: reviewers.includes('codex'),
1655
- disabled: !reviewers.includes('codex'),
1656
- onChange: (event) => changeReviewer('codex', event.target.checked) }), ' Codex'),
1657
- h('label', null, h('input', { type: 'checkbox', 'aria-label': '任务 Agent Claude Code', checked: reviewers.includes('claude-code'),
1658
- disabled: !reviewers.includes('claude-code'),
1659
- onChange: (event) => changeReviewer('claude-code', event.target.checked) }), ' Claude Code')),
1660
- null),
1661
- nativePrUnavailableReason ? h('p', { role: 'status', style: { ...mutedTextStyle, gridColumn: '1 / -1' } },
1662
- `${nativePrUnavailableReason}。当前仍可使用 DSH 团队模型处理任务。`) : null,
1827
+ h('strong', null, 'AI 任务助手'),
1828
+ h('span', { style: mutedTextStyle }, ruleSummaryPending ? '正在分析' : ruleSummaryReady ? '待复核 · 尚未开放'
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('普通请求只运行默认执行方;发起人明确要求交叉审查时,会并行运行其他已选执行方。')),
1869
+ h('div', { role: 'group', 'aria-label': '执行方', style: { display: 'grid', gap: 7 } },
1870
+ ...[
1871
+ { id: 'dsh', title: 'DSH', detail: '使用获准的团队模型处理对话内容和 ANT 已核验资源。', available: dshAvailable },
1872
+ { id: 'codex', title: 'Codex', detail: '使用本机 DSH Provider Profile 权限;ANT 不会将文件、Git 和网络权限缩小到单项任务。', available: codexAvailable },
1873
+ { id: 'claude-code', title: 'Claude Code', detail: '使用本机 DSH Provider Profile 权限;ANT 不会将文件、Git 和网络权限缩小到单项任务。', available: claudeCodeAvailable },
1874
+ ].map((option) => {
1875
+ const checked = reviewers.includes(option.id)
1876
+ return h('div', { key: option.id, style: { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) auto',
1877
+ gap: 8, alignItems: 'center', border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8,
1878
+ padding: '9px 11px' } },
1879
+ h('label', { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' } },
1880
+ h('input', { type: 'checkbox', 'aria-label': `任务 Agent ${option.title}`, checked,
1881
+ disabled: !option.available && !checked,
1882
+ onChange: (event) => changeReviewer(option.id, event.target.checked) }),
1883
+ h('strong', null, option.title),
1884
+ h('span', { style: mutedTextStyle }, option.available ? '可用' : '未在本机启用')),
1885
+ h('label', { style: { display: 'flex', alignItems: 'center', gap: 6, color: mutedTextStyle.color } },
1886
+ h('input', { type: 'radio', name: 'ant-default-runner', 'aria-label': `默认执行方 ${option.title}`,
1887
+ checked: runner === option.id, disabled: !checked || !option.available,
1888
+ onChange: () => selectRunner(option.id) }), '默认'),
1889
+ checked ? h('span', { style: { ...mutedTextStyle, gridColumn: '1 / -1' } }, option.detail) : null)
1890
+ }),
1891
+ ),
1892
+ executorUnavailableReason ? h('p', { role: 'status', style: { ...mutedTextStyle, gridColumn: '1 / -1' } },
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' } },
1663
1898
  h('label', null, '使用场景', h('select', { value: conversationKind,
1664
1899
  onChange: (event) => { setConversationKind(event.target.value); setGroupId('') }, style: inputStyle },
1665
1900
  h('option', { value: 'dm' }, '私聊'), h('option', { value: 'group' }, '指定群聊'))),
@@ -1683,10 +1918,16 @@ window.__ModuleLoader__.load({
1683
1918
  `有效期:${formatLocaleDate(editingExpiresAt * 1000)}(编辑时保持不变)`)
1684
1919
  : h('label', null, '有效期', h('select', { value: days, onChange: (event) => setDays(Number(event.target.value)), style: inputStyle },
1685
1920
  h('option', { value: 1 }, '1 天'), h('option', { value: 7 }, '7 天'), h('option', { value: 30 }, '30 天'))),
1686
- (runner === 'codex' || runner === 'claude-code') ? h('p', { style: { ...mutedTextStyle, gridColumn: '1 / -1' } },
1687
- 'Provider Profile 的本机权限由 DSH 管理;ANT 当前不会用 Codex 或 Claude Code 执行协作任务,也不会把 Gitea 凭据交给它们。') : null),
1688
- !hasPrParameter ? h('p', { style: mutedTextStyle },
1689
- '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', '开放前复核', '确认任务理解、执行方式和请求范围后再开放。'),
1690
1931
  h('aside', { id: 'ant-authorization-review', role: 'region', 'aria-label': '开放前复核', tabIndex: -1,
1691
1932
  style: { ...settingsPanelStyle, padding: 14, gap: 10, alignSelf: 'stretch' } },
1692
1933
  h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8 } },
@@ -1724,14 +1965,14 @@ window.__ModuleLoader__.load({
1724
1965
  'aria-describedby': cardBlockReason ? 'ant-capability-block-reason'
1725
1966
  : submissionError ? 'ant-capability-submission-error' : undefined,
1726
1967
  style: { ...primaryButtonStyle, width: '100%' } }, busy ? translateLocaleText('正在处理…')
1727
- : editingCardId ? '保存能力修改' : '确认并开放能力'))))),
1968
+ : editingCardId ? '保存能力修改' : '确认并开放能力'))))))),
1728
1969
  h('div', { role: 'region', 'aria-label': '本机处理设置', style: { display: activeTab === 'local' ? 'grid' : 'none', gap: 12 } },
1729
1970
  showPrSecuritySettings ? h('section', { style: { ...settingsPanelStyle, padding: 14 } },
1730
1971
  h('strong', null, 'PR 只读凭据(本机)'),
1731
1972
  h('p', { style: mutedTextStyle }, tokenConfigured
1732
1973
  ? '令牌由 DSH 本机凭据服务保存。ANT Host 在读取已授权 PR 时会使用它;不会同步到 ANT 服务端或写入审查提示词。'
1733
1974
  : '读取 Gitea PR 需要只读访问令牌。保存后由 DSH 本机凭据服务管理,ANT Host 只在读取已授权 PR 时使用。'),
1734
- h('p', { role: 'note', style: mutedTextStyle }, 'DSH 默认将凭据保存在 DSH Home 的私有文件中,环境变量也可作为回退来源。文件权限只隔离其他 OS 用户;同一用户权限下的 Agent 进程可能读取它。ANT 当前只允许无 DSH 工具的任务会话处理对话内容和 Host 核验后提供的资源。Codex 和 Claude Code 子 Agent 暂停用于协作任务,因为 ANT 无法核验其逐任务文件、Git 和网络权限。ANT Gitea 凭据只由 Host Connector 使用,不会放入 Agent 请求。'),
1975
+ h('p', { role: 'note', style: mutedTextStyle }, 'DSH 默认将凭据保存在 DSH Home 的私有文件中,环境变量也可作为回退来源。文件权限只隔离其他 OS 用户;同一用户权限下的 Agent 进程可能读取它。Codex 和 Claude Code 会使用本机 DSH Provider Profile 的文件、Git、网络及工具权限;任务描述和单次审批不会创建操作系统沙箱,也不会缩小这些权限。ANT Gitea 凭据只由 Host Connector 使用,不会放入 Agent 请求。'),
1735
1976
  h('p', { role: 'note', style: mutedTextStyle }, selectedGiteaOrigin
1736
1977
  ? `这枚只读令牌仅用于已绑定主机 ${selectedGiteaOrigin};任务来源来自自然语言描述,但必须匹配该主机。`
1737
1978
  : '首次确认并开放包含 PR 来源的任务时,会将该来源绑定到这台机器人;确认前 ANT 不会把只读令牌发送到任务描述中的地址。'),
@@ -1765,26 +2006,26 @@ window.__ModuleLoader__.load({
1765
2006
  style: { ...inputStyle, width: 128, maxWidth: '100%', boxSizing: 'border-box' } })),
1766
2007
  h('button', { disabled: busy, style: secondaryButtonStyle }, '保存本机处理设置'),
1767
2008
  h('p', { style: { ...mutedTextStyle, flex: '1 1 100%', margin: 0 } },
1768
- '当前不拉取 PR 仓库快照;刷新授权状态时仍会清理已停用或到期能力的旧缓存。')),
2009
+ '需要完整 PR 文件时,ANT 会在本机准备已核验 Head 的临时快照;刷新授权状态时会清理已停用或到期能力的旧缓存。')),
1769
2010
  ),
1770
2011
  h('section', { 'aria-label': '执行方状态与权限边界', style: { ...settingsPanelStyle, padding: 14, gap: 8 } },
1771
2012
  h('strong', null, '执行方状态与权限边界'),
1772
- h('p', { style: mutedTextStyle }, '这里显示 DSH 本机的执行器状态;Codex 或 Claude Code 已注册,不代表它们获准执行协作任务。'),
2013
+ h('p', { style: mutedTextStyle }, '这里显示 DSH 本机的执行器状态。已注册的 Codex 或 Claude Code 可由主人选入预授权任务卡,或在本次 IM 审批中选择。'),
1773
2014
  loadFailures.includes('executors') ? h('p', { role: 'status', style: mutedTextStyle }, '暂时无法读取执行方状态,请刷新后重试。') : null,
1774
2015
  ...executors.filter((item) => ['dsh', 'codex', 'claude-code'].includes(item.id)).map((item) => h('div', {
1775
2016
  key: item.id, style: { display: 'grid', gap: 3, paddingTop: 8, borderTop: '1px solid var(--dsw-alias-border-l2)' } },
1776
2017
  h('strong', null, item.title + ' · ' + (item.id === 'dsh'
1777
2018
  ? item.available ? '可用' : item.reason || '未就绪'
1778
- : item.available ? '已注册 · 协作任务暂不可用' : '协作任务暂不可用')),
2019
+ : item.available ? '已注册 · 可用于协作' : '尚未注册')),
1779
2020
  h('span', { style: mutedTextStyle }, item.id === 'codex'
1780
- ? 'ANT 暂不启动 Codex 子 Agent,因为当前无法核验逐任务文件、Git 和网络权限。'
2021
+ ? 'Codex 使用本机 Provider Profile 权限;ANT 不会把文件、Git 或网络权限缩小到任务范围。'
1781
2022
  : item.id === 'claude-code'
1782
- ? 'ANT 暂不启动 Claude Code 子 Agent,因为当前无法核验逐任务文件、Git 和网络权限。'
2023
+ ? 'Claude Code 使用本机 Provider Profile 权限;ANT 不会把文件、Git 或网络权限缩小到任务范围。'
1783
2024
  : '处理授权任务提供的内容,不调用本机工具。'))))))
1784
2025
  }
1785
2026
 
1786
2027
  function SkillCenterPage({ remote, onBack, onAll, onSkills, onPlugins, openBundle,
1787
- includePlugins, query, onQueryChange }) {
2028
+ includePlugins, query, onQueryChange, pluginPageState }) {
1788
2029
  const [skills, setSkills] = useState([])
1789
2030
  const [loading, setLoading] = useState(false)
1790
2031
  const [loadError, setLoadError] = useState(false)
@@ -2133,6 +2374,7 @@ window.__ModuleLoader__.load({
2133
2374
  style: { color: 'var(--dsw-alias-link)', fontSize: 13, lineHeight: 1.6, padding: '4px 0' } }, '英文原文 ↗'))))) : null,
2134
2375
  includePlugins ? h(PluginCenterPage, { remote, onBack, onAll, onSkills, openBundle,
2135
2376
  embedded: true, query, onQueryChange, externalFilter: filter, externalCategoryFilter: categoryFilter,
2377
+ pluginPageState,
2136
2378
  externalScopeFilter: scopeFilter,
2137
2379
  hasSkillMatches: visible.length + visibleOfficial.length > 0, skillCatalogReady: !loading && !loadError,
2138
2380
  refreshToken }) : null))
@@ -2141,49 +2383,193 @@ window.__ModuleLoader__.load({
2141
2383
  function PluginCenterPage({ remote, onBack, onAll, onSkills, openBundle,
2142
2384
  embedded = false, query, onQueryChange, externalFilter = 'all',
2143
2385
  externalCategoryFilter = 'all', externalScopeFilter = 'all',
2144
- 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
2145
2392
  const [plugins, setPlugins] = useState([])
2146
2393
  const [bundles, setBundles] = useState([])
2147
2394
  const [loading, setLoading] = useState(true)
2148
2395
  const [loadError, setLoadError] = useState(false)
2149
- const [workingId, setWorkingId] = useState('')
2150
- const [uncertainId, setUncertainId] = useState('')
2151
- const [completedId, setCompletedId] = useState('')
2152
- const [message, setMessage] = useState('')
2396
+ const [bundleListReady, setBundleListReady] = useState(false)
2153
2397
  const [filter, setFilter] = useState('all')
2154
2398
  const [categoryFilter, setCategoryFilter] = useState('all')
2155
- const refreshGeneration = useRef(0)
2156
- 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) => {
2157
2409
  const generation = ++refreshGeneration.current
2410
+ const uncertaintySnapshot = uncertainInstallsRef.current
2411
+ const messageVersionAtStart = messageVersion.current
2158
2412
  setLoading(true)
2413
+ setBundleListReady(false)
2414
+ let catalogLoaded = false
2159
2415
  try {
2160
2416
  const manager = remote?.pluginManager
2161
2417
  if (typeof manager?.listBundles !== 'function') throw new Error('当前 DSH 未提供插件管理接口。')
2162
- const [response, listed] = await Promise.all([
2163
- fetch('/api/ant-teams/plugins', { cache: 'no-store' }), manager.listBundles(),
2164
- ])
2418
+ const response = await fetch('/api/ant-teams/plugins', { cache: 'no-store' })
2165
2419
  const result = await readLocalJsonResponse(response)
2166
2420
  if (!response.ok) throw new Error(result.message || `读取插件目录失败(${response.status})。`)
2167
2421
  if (!Array.isArray(result.plugins)) throw new Error('企业插件目录格式无效。')
2168
- if (!listed.ok) throw new Error(listed.error?.message || '无法读取 DSH 本机插件。')
2169
2422
  if (generation !== refreshGeneration.current) return
2170
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)
2171
2503
  setBundles(listed.value)
2172
2504
  setLoadError(false)
2173
- 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
+ }
2174
2534
  } catch (error) {
2175
2535
  if (generation !== refreshGeneration.current) return
2176
- setPlugins([])
2177
- setBundles([])
2178
- setLoadError(true)
2179
- 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)
2180
2544
  } finally { if (generation === refreshGeneration.current) setLoading(false) }
2181
2545
  }
2182
- 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
+ }
2183
2567
  const install = async (plugin) => {
2184
2568
  const artifactUrl = approvedPluginArtifactUrl(plugin)
2185
2569
  const installSpec = pluginInstallSpec(plugin)
2186
- 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
2187
2573
  const manager = remote?.pluginManager
2188
2574
  if (typeof manager?.installBundle !== 'function') {
2189
2575
  setMessage('当前 DSH 未提供插件安装接口,请联系管理员。')
@@ -2203,8 +2589,15 @@ window.__ModuleLoader__.load({
2203
2589
  '在这台 DSH {action}团队审核的 {plugin_title} {version}?{source_notice}{update_notice} 插件将在本机运行,完成后需要完全重启 DSH。',
2204
2590
  { action: translateLocaleText(action), plugin_title: plugin.title, version: plugin.version, source_notice: sourceNotice, update_notice: updateNotice })
2205
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
+ }
2206
2598
  setWorkingId(plugin.id)
2207
- setMessage(translateLocaleText('正在安装 {plugin_title},请不要退出 DSH…', { plugin_title: plugin.title }))
2599
+ setInstallMessage(translateLocaleText('正在安装 {plugin_title},请不要退出 DSH…', { plugin_title: plugin.title }),
2600
+ '本机有一项插件操作仍在完成;结束前不能启动其他插件安装。')
2208
2601
  try {
2209
2602
  const response = await fetch('/api/ant-teams/plugins', { cache: 'no-store' })
2210
2603
  const catalog = await readLocalJsonResponse(response)
@@ -2215,6 +2608,9 @@ window.__ModuleLoader__.load({
2215
2608
  throw new Error('插件审核信息已变化,请刷新目录后重试。')
2216
2609
  }
2217
2610
  const requestId = crypto.randomUUID()
2611
+ let relatedRequestIds = []
2612
+ const markCurrentInstallUncertain = (previous = null, expectedPreviousSource = null) =>
2613
+ markInstallUncertain(plugin, previous, expectedPreviousSource, requestId, relatedRequestIds)
2218
2614
  const requestReference = requestId.slice(0, 8).toUpperCase()
2219
2615
  const previousSourceIsRegistry = Boolean(local && (!local.source
2220
2616
  || local.source === plugin.package_name || local.source.startsWith(`${plugin.package_name}@`)))
@@ -2254,17 +2650,26 @@ window.__ModuleLoader__.load({
2254
2650
  if (installed !== null || !previousInstallSpec) return 'unknown'
2255
2651
  if (previousSourceIsRegistry && !previousRegistry) return 'unknown'
2256
2652
  const restoreRequestId = crypto.randomUUID()
2257
- try {
2258
- const restoreOptions = {
2259
- enabled: local.enabled !== false, requestId: restoreRequestId,
2260
- saveExact: true,
2261
- }
2262
- if (previousSourceIsRegistry && previousRegistry) restoreOptions.registry = previousRegistry
2263
- await manager.installBundle(previousInstallSpec, restoreOptions)
2264
- if (typeof manager.waitForInstall === 'function') {
2265
- await manager.waitForInstall(restoreRequestId)
2266
- }
2267
- } 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
+ }
2268
2673
  installed = await currentBundle()
2269
2674
  return isPreviousVersion(installed) && installed.enabled === (local.enabled !== false)
2270
2675
  ? 'restored' : 'unknown'
@@ -2287,7 +2692,7 @@ window.__ModuleLoader__.load({
2287
2692
  } catch (error) { installFailure = error; result = null }
2288
2693
  } else {
2289
2694
  if (replacingInstalled && typeof manager.removeBundle !== 'function') {
2290
- setMessage('当前 DSH 不支持通过 ANT 安全替换已安装插件。请在 DSH 插件管理查看状态,或联系管理员。')
2695
+ setInstallMessage('当前 DSH 不支持通过 ANT 安全替换已安装插件。请在 DSH 插件管理查看状态,或联系管理员。')
2291
2696
  return
2292
2697
  }
2293
2698
  if (replacingInstalled) {
@@ -2298,7 +2703,7 @@ window.__ModuleLoader__.load({
2298
2703
  if (!removed?.ok || !['applied', 'restart-required'].includes(removed.value?.application)) {
2299
2704
  const diagnostic = safeInstallDiagnostic(removed?.value?.error || removed?.error || removeFailure)
2300
2705
  const recovery = await restorePreviousVersion()
2301
- if (recovery !== 'restored') setUncertainId(plugin.id)
2706
+ if (recovery !== 'restored') markCurrentInstallUncertain(local, expectedPreviousSource)
2302
2707
  const recoveryNotice = recovery === 'restored'
2303
2708
  ? translateLocaleText('旧版本号和原启用状态已恢复。{source_notice}', {
2304
2709
  source_notice: previousSourceIsRegistry
@@ -2306,7 +2711,7 @@ window.__ModuleLoader__.load({
2306
2711
  : recovery === 'target' ? translateLocaleText(
2307
2712
  '本机列表显示审核版本 {version} 已安装,请核对后再操作。', { version: plugin.version })
2308
2713
  : translateLocaleText('无法确认旧版本及启用状态,请先在 DSH 插件管理核对。')
2309
- setMessage(`无法开始更新 ${plugin.title}:DSH 未能确认旧版本移除完成。${diagnostic
2714
+ setInstallMessage(`无法开始更新 ${plugin.title}:DSH 未能确认旧版本移除完成。${diagnostic
2310
2715
  ? `DSH 诊断:${diagnostic}。` : ''}${recoveryNotice}`)
2311
2716
  return
2312
2717
  }
@@ -2327,7 +2732,6 @@ window.__ModuleLoader__.load({
2327
2732
  } catch (error) { waitFailure = error }
2328
2733
  }
2329
2734
  if (!recovered) {
2330
- setUncertainId(plugin.id)
2331
2735
  let restoreNotice = ''
2332
2736
  if (removedForUpdate) {
2333
2737
  const restoreState = await restorePreviousVersion()
@@ -2340,23 +2744,27 @@ window.__ModuleLoader__.load({
2340
2744
  '本机列表显示审核版本 {version} 已安装,但操作结果未确认。', { version: plugin.version })}`
2341
2745
  : ` ${translateLocaleText('DSH 未能确认旧版本恢复,请先在插件列表核对。')}`
2342
2746
  }
2747
+ markCurrentInstallUncertain(local, expectedPreviousSource)
2343
2748
  const details = [installFailure && `installBundle:${safeInstallDiagnostic(installFailure)}`,
2344
2749
  waitFailure && `waitForInstall:${safeInstallDiagnostic(waitFailure)}`].filter(Boolean)
2345
- setMessage(`暂时无法确认 ${plugin.title} 的安装结果(请求编号 ${requestReference})。${details.length
2346
- ? `本机诊断:${details.join(';')}。` : ''}${restoreNotice}请先在 DSH 插件列表核对,勿重复点击安装。`)
2750
+ setInstallMessage(`暂时无法确认 ${plugin.title} 的安装结果(请求编号 ${requestReference})。${details.length
2751
+ ? `本机诊断:${details.join(';')}。` : ''}${restoreNotice}请先在 DSH 插件列表核对,勿重复点击安装。`,
2752
+ '本机插件安装结果待核对,请先确认本机状态,勿重复安装。')
2347
2753
  return
2348
2754
  }
2349
2755
  result = recovered
2350
2756
  }
2351
2757
  const outcome = result.value
2352
2758
  if (!outcome || typeof outcome !== 'object') {
2353
- setUncertainId(plugin.id)
2354
- setMessage(`DSH 未返回 ${plugin.title} 的安装结果。请先在插件列表核对,勿重复点击安装。`)
2759
+ markCurrentInstallUncertain(local, expectedPreviousSource)
2760
+ setInstallMessage(`DSH 未返回 ${plugin.title} 的安装结果。请先在插件列表核对,勿重复点击安装。`,
2761
+ '本机插件安装结果待核对,请先确认本机状态,勿重复安装。')
2355
2762
  return
2356
2763
  }
2357
2764
  if (outcome.application === 'failed' || outcome.application === 'cancelled') {
2358
2765
  if (outcome.error?.code === 'ambiguous-install' && local?.version === plugin.version) {
2359
- setMessage(`DSH 无法确定 ${plugin.title} 本次安装的唯一结果,未确认完成。请先在 DSH 插件列表核对;若已经正常使用同版则无需重复安装,仍需核对来源时请联系管理员。`)
2766
+ setInstallMessage(`DSH 无法确定 ${plugin.title} 本次安装的唯一结果,未确认完成。请先在 DSH 插件列表核对;若已经正常使用同版则无需重复安装,仍需核对来源时请联系管理员。`,
2767
+ '本机插件安装结果待核对,请先确认本机状态,勿重复安装。')
2360
2768
  return
2361
2769
  }
2362
2770
  const diagnostic = safeInstallDiagnostic({ name: outcome.error?.name,
@@ -2364,11 +2772,14 @@ window.__ModuleLoader__.load({
2364
2772
  message: outcome.error?.diagnostic || outcome.error?.message || outcome.packageResult?.output })
2365
2773
  let restoreNotice = ''
2366
2774
  if (updateRestore) {
2367
- 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
2368
2778
  ? ` ${translateLocaleText('DSH 已恢复旧版本 {version}。', { version: local.version })}`
2369
2779
  : ` ${translateLocaleText('DSH 未能确认旧版本恢复,请先在插件列表核对。')}`
2370
2780
  } else if (removedForUpdate) {
2371
2781
  const restoreState = await restorePreviousVersion()
2782
+ if (restoreState !== 'restored') markCurrentInstallUncertain(local, expectedPreviousSource)
2372
2783
  restoreNotice = restoreState === 'restored'
2373
2784
  ? translateLocaleText('DSH 已恢复旧版本号 {version}。{source_notice}', {
2374
2785
  version: local.version,
@@ -2378,14 +2789,15 @@ window.__ModuleLoader__.load({
2378
2789
  '本机列表显示审核版本 {version} 已安装,但结果未确认。', { version: plugin.version })}`
2379
2790
  : ` ${translateLocaleText('DSH 未能确认旧版本恢复,请先在插件列表核对。')}`
2380
2791
  }
2381
- setMessage(`${plugin.title} 安装未完成(请求编号 ${requestReference})。${diagnostic
2792
+ setInstallMessage(`${plugin.title} 安装未完成(请求编号 ${requestReference})。${diagnostic
2382
2793
  ? `DSH 诊断:${diagnostic}。` : 'DSH 没有返回失败诊断。'}${restoreNotice}请在 DSH 插件管理查看状态或联系管理员。`)
2383
2794
  return
2384
2795
  }
2385
2796
  if (outcome.bundle !== plugin.package_name
2386
2797
  || !['restart-required', 'applied'].includes(outcome.application)) {
2387
- setUncertainId(plugin.id)
2388
- setMessage(`DSH 未确认 ${plugin.title} 已安装。请先在插件列表核对,勿重复点击安装。`)
2798
+ markCurrentInstallUncertain(local, expectedPreviousSource)
2799
+ setInstallMessage(`DSH 未确认 ${plugin.title} 已安装。请先在插件列表核对,勿重复点击安装。`,
2800
+ '本机插件安装结果待核对,请先确认本机状态,勿重复安装。')
2389
2801
  return
2390
2802
  }
2391
2803
  let observed = null
@@ -2396,19 +2808,24 @@ window.__ModuleLoader__.load({
2396
2808
  }
2397
2809
  } catch {}
2398
2810
  if ((outcome.version && outcome.version !== plugin.version) || observed?.version !== plugin.version) {
2399
- setUncertainId(plugin.id)
2400
- setMessage(`DSH 未确认 ${plugin.title} 的审核版本已安装。请先在插件列表核对,勿重复点击安装。`)
2811
+ markCurrentInstallUncertain(local, expectedPreviousSource)
2812
+ setInstallMessage(`DSH 未确认 ${plugin.title} 的审核版本已安装。请先在插件列表核对,勿重复点击安装。`,
2813
+ '本机插件安装结果待核对,请先确认本机状态,勿重复安装。')
2401
2814
  return
2402
2815
  }
2403
- await refresh()
2404
2816
  if (artifactUrl && plugin.artifact_sha256) {
2405
2817
  try { localStorage.setItem(`${PLUGIN_INSTALL_RECORD_PREFIX}${plugin.id}`, plugin.artifact_sha256) } catch {}
2406
2818
  }
2407
- setCompletedId(plugin.id)
2408
- setMessage(`${plugin.title} 已安装。请完全退出并重新启动 DSH 后使用。`)
2819
+ if (isOperationScopeCurrent()) setCompletedId(plugin.id)
2820
+ setInstallMessage(`${plugin.title} 已安装。请完全退出并重新启动 DSH 后使用。`,
2821
+ '本机插件已安装或更新;请完全退出并重新启动 DSH 后使用。')
2822
+ requestPluginRefresh()
2409
2823
  } catch (error) {
2410
- setMessage(error instanceof Error ? error.message : '插件安装未完成,请稍后重试。')
2411
- } finally { setWorkingId('') }
2824
+ setInstallMessage(error instanceof Error ? error.message : '插件安装未完成,请稍后重试。',
2825
+ '本机此前启动的插件操作已结束;请刷新本机状态后再操作。')
2826
+ } finally {
2827
+ setWorkingId('')
2828
+ }
2412
2829
  }
2413
2830
  const activeFilter = embedded ? externalFilter : filter
2414
2831
  const activeCategoryFilter = embedded ? externalCategoryFilter : categoryFilter
@@ -2467,13 +2884,18 @@ window.__ModuleLoader__.load({
2467
2884
  h('div', null,
2468
2885
  h('h1', { style: { margin: 0, fontSize: 28 } }, '企业精选插件'),
2469
2886
  h('p', { style: { ...mutedTextStyle, marginTop: 6 } }, '这里展示团队审核过的插件版本及本机状态。DSH 0.2.0-rc.2 不会自动更新 Bundle;插件自身的更新器可能绕过此目录安装其他版本。')),
2470
- 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 ? '读取中…' : '刷新目录')),
2471
2891
  !loading && !plugins.length && !loadError ? embedded
2472
2892
  ? h('p', { role: 'status', style: mutedTextStyle }, '团队暂未发布精选插件。')
2473
2893
  : h('section', { role: 'status', style: panelStyle },
2474
2894
  h('h2', { style: { margin: 0, fontSize: 18 } }, '团队暂未发布精选插件'),
2475
2895
  h('p', { style: mutedTextStyle }, '需要某个插件?请联系企业管理员,说明用途或提供插件地址。审核发布后会显示在这里。')) : null,
2476
- !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,
2477
2899
  plugins.length && !embedded ? h(ExtensionRequestNotice) : null,
2478
2900
  plugins.length && !embedded ? h('div', { style: panelStyle },
2479
2901
  h('input', { type: 'search', value: query, onChange: (event) => onQueryChange(event.target.value), placeholder: '搜索插件名称或用途', 'aria-label': '搜索企业精选插件', style: inputStyle }),
@@ -2486,7 +2908,6 @@ window.__ModuleLoader__.load({
2486
2908
  'aria-label': '筛选插件用途类别', style: { ...inputStyle, width: 'auto', minHeight: 34 } },
2487
2909
  h('option', { value: 'all' }, '全部用途'),
2488
2910
  ...extensionCategories.map(([category, label]) => h('option', { key: category, value: category }, label)))))) : null,
2489
- plugins.length && message ? h('p', { role: 'status', style: feedbackNoticeStyle }, message) : null,
2490
2911
  visible.length ? h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 420px), 1fr))', gap: 16 } },
2491
2912
  ...visible.map((plugin) => {
2492
2913
  const local = bundles.find((bundle) => bundle.name === plugin.package_name && bundle.installed)
@@ -2495,11 +2916,13 @@ window.__ModuleLoader__.load({
2495
2916
  const artifactUrl = approvedPluginArtifactUrl(plugin)
2496
2917
  const installSpec = pluginInstallSpec(plugin)
2497
2918
  const localAhead = local?.version && newerVersion(local.version, plugin.version)
2919
+ const uncertainInstall = uncertainInstallFor(plugin)
2498
2920
  const sourceKnown = typeof local?.source === 'string'
2499
2921
  const verifiedLocal = sameVersion && artifactUrl && sourceKnown && local.source === artifactUrl
2500
2922
  const recordedInstall = sameVersion && !sourceKnown && previouslyInstalledReviewedPlugin(plugin)
2501
2923
  const status = security ? '已废弃(安全风险)'
2502
- : 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
2503
2926
  ? '本机版本高于团队目录版本' : sameVersion
2504
2927
  ? verifiedLocal ? local.enabled ? '本机已装(团队来源,已启用)' : '本机已装(团队来源,未启用)'
2505
2928
  : sourceKnown ? '本机同版,来源不同' : recordedInstall
@@ -2536,14 +2959,18 @@ window.__ModuleLoader__.load({
2536
2959
  h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8 } },
2537
2960
  installSpec && (!local || (local.version && local.version !== plugin.version && (!localAhead || artifactUrl))
2538
2961
  || (artifactUrl && sameVersion && !verifiedLocal))
2539
- ? 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,
2540
2964
  onClick: () => void install(plugin), style: primaryButtonStyle },
2541
- workingId === plugin.id ? '安装中…' : uncertainId === plugin.id ? '先核对安装结果'
2965
+ workingId === plugin.id ? '安装中…' : uncertainInstall ? '先核对安装结果'
2542
2966
  : completedId === plugin.id ? '重启 DSH 后生效'
2543
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,
2544
2971
  plugin.status === 'published' && !local && !installSpec
2545
2972
  ? h('p', { style: mutedTextStyle }, '如需安装,请联系企业管理员安排。') : null,
2546
- 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,
2547
2974
  local && !sameVersion && plugin.status === 'published' && !installSpec
2548
2975
  ? h('p', { style: mutedTextStyle }, '本机版本与团队审核版本不同;请联系企业管理员核对。') : null))
2549
2976
  })) : !loading && plugins.length && (!embedded || !hasSkillMatches)
@@ -3316,6 +3743,7 @@ window.__ModuleLoader__.load({
3316
3743
  setMessage(`未清除本机凭证和模型设置:${error instanceof Error ? error.message : '请稍后重试。'}`)
3317
3744
  return
3318
3745
  }
3746
+ setPluginRuntimeServiceScope('')
3319
3747
  try {
3320
3748
  await clearLocalConnection()
3321
3749
  setMessage('此设备的 ANT 凭证已撤销,ANT 模型设置已清除。团队服务地址已保留。')
@@ -3738,7 +4166,8 @@ window.__ModuleLoader__.load({
3738
4166
  dshCompatibilityCard,
3739
4167
  showReleaseNotes ? h('section', { style: { ...infoNoticeStyle, padding: 12 } },
3740
4168
  h('strong', { style: { fontSize: 14, lineHeight: 1.4 } }, '本版更新'),
3741
- h('p', { style: mutedTextStyle }, bundleRelease.release_notes)) : null,
4169
+ h('div', { 'aria-label': '更新说明内容', style: { display: 'grid', gap: 8 } },
4170
+ ...releaseNoteBlocks(bundleRelease.release_notes))) : null,
3742
4171
  ['incompatible', 'unknown'].includes(serviceCompatibility.state) ? h('p', { role: 'status', style: {
3743
4172
  ...feedbackNoticeStyle,
3744
4173
  color: serviceCompatibility.state === 'incompatible' ? 'var(--dsw-alias-state-error-primary)'
@@ -3781,14 +4210,86 @@ window.__ModuleLoader__.load({
3781
4210
  useLocaleRevision()
3782
4211
  const [tab, setTab] = useState('all')
3783
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 }
3784
4284
  const navigation = {
3785
4285
  onAll: () => setTab('all'), onSkills: () => setTab('skills'),
3786
4286
  onPlugins: () => setTab('plugins'),
3787
4287
  }
3788
4288
  return tab === 'plugins'
3789
- ? h(PluginCenterPage, { remote, onBack, openBundle, query, onQueryChange: setQuery, ...navigation })
4289
+ ? h(PluginCenterPage, { remote, onBack, openBundle, query, onQueryChange: setQuery,
4290
+ pluginPageState, ...navigation })
3790
4291
  : h(SkillCenterPage, { remote, onBack, openBundle, query, onQueryChange: setQuery,
3791
- includePlugins: tab === 'all', ...navigation })
4292
+ pluginPageState, includePlugins: tab === 'all', ...navigation })
3792
4293
  }
3793
4294
 
3794
4295
  function brandingCacheKey() {