@ai-wendao/ant 0.4.9 → 0.4.11

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
@@ -7,6 +7,7 @@ window.__ModuleLoader__.load({
7
7
  const h = createElement
8
8
  const SERVICE_URL_STORAGE_KEY = 'ant-team-service-url'
9
9
  const DEVICE_ID_STORAGE_KEY = 'ant-team-device-id'
10
+ const DEFAULT_MODEL_INITIALIZED_KEY = 'ant-team-default-model-initialized'
10
11
  const ANT_BUNDLE_PACKAGE_NAME = '@ai-wendao/ant'
11
12
  const ANT_API_PROTOCOL_VERSIONS = [1]
12
13
  const PLUGIN_INSTALL_RECORD_PREFIX = 'ant-reviewed-plugin-install:'
@@ -227,6 +228,12 @@ window.__ModuleLoader__.load({
227
228
  ? url.href : null
228
229
  }
229
230
 
231
+ function pluginInstallSpec(plugin) {
232
+ if (plugin?.status !== 'published' || typeof plugin.package_name !== 'string'
233
+ || typeof plugin.version !== 'string' || !plugin.package_name || !plugin.version) return ''
234
+ return approvedPluginArtifactUrl(plugin) || directInstallSpec(plugin)
235
+ }
236
+
230
237
  function previouslyInstalledReviewedPlugin(plugin) {
231
238
  try { return localStorage.getItem(`${PLUGIN_INSTALL_RECORD_PREFIX}${plugin.id}`) === plugin.artifact_sha256 }
232
239
  catch { return false }
@@ -467,12 +474,15 @@ window.__ModuleLoader__.load({
467
474
  const [examples, setExamples] = useState('')
468
475
  const [taskPrompt, setTaskPrompt] = useState('')
469
476
  const [runner, setRunner] = useState('dsh')
477
+ const [reviewers, setReviewers] = useState(['dsh'])
470
478
  const [parameters, setParameters] = useState([])
471
479
  const [giteaToken, setGiteaToken] = useState('')
472
480
  const [tokenConfigured, setTokenConfigured] = useState(false)
473
481
  const [senderScope, setSenderScope] = useState('any')
474
482
  const [senderIds, setSenderIds] = useState('')
475
483
  const [days, setDays] = useState(7)
484
+ const [editingCardId, setEditingCardId] = useState(null)
485
+ const [editingExpiresAt, setEditingExpiresAt] = useState(null)
476
486
  const [busy, setBusy] = useState(false)
477
487
  const [loading, setLoading] = useState(true)
478
488
  const [botLoadError, setBotLoadError] = useState(false)
@@ -548,17 +558,28 @@ window.__ModuleLoader__.load({
548
558
  const enrolledCount = enrolled.filter((item) => item.device_bound_to_current
549
559
  && connectedBots.some((bot) => bot.fingerprint === item.bot_fingerprint)).length
550
560
  const selectedTaskCount = activeCards.filter((card) => card.bot_fingerprint === selectedBot?.fingerprint).length
551
- const selectedRunnerNeedsPr = ['codex', 'claude-code'].includes(runner)
561
+ const nativeReviewerSelected = reviewers.some((item) => ['codex', 'claude-code'].includes(item))
562
+ const hasPrParameter = parameters.some((item) => item.kind === 'gitea_pr')
552
563
  const codexAvailable = executors.some((item) => item.id === 'codex-readonly' && item.available)
553
564
  const claudeCodeAvailable = executors.some((item) => item.id === 'claude-code' && item.available)
565
+ const dshAvailable = executors.some((item) => item.id === 'dsh' && item.available)
554
566
  const nativePrUnavailableReason = executors.find((item) => item.reason)?.reason || null
567
+ const reviewerLabel = (id) => ({ dsh: 'DSH', codex: 'Codex', 'claude-code': 'Claude Code' })[id] || id
568
+ const cardReviewerSummary = (card) => {
569
+ const selected = Array.isArray(card.reviewers) && card.reviewers.length ? card.reviewers : [card.runner]
570
+ return selected.length > 1
571
+ ? `交叉可用:${selected.map(reviewerLabel).join('、')};默认 ${reviewerLabel(card.runner)}`
572
+ : card.runner === 'codex' ? 'Codex PR 快照审查'
573
+ : card.runner === 'claude-code' ? 'Claude Code PR 快照审查'
574
+ : 'DSH 文字处理'
575
+ }
555
576
  const requesterSummary = senderScope === 'allowlist'
556
577
  ? `仅指定成员:${senderIds.trim() || '尚未填写成员 ID'}`
557
578
  : conversationKind === 'group' ? `群 ${groupId || '尚未选择'} 的所有成员` : '任何能私聊此机器人的人'
558
579
  const resultRouteSummary = conversationKind === 'group' ? '回复到原群(群内可见)' : '回复到发起任务的私聊'
559
580
  const executorSummary = runner === 'codex' ? '本机 Codex · PR 快照'
560
581
  : runner === 'claude-code' ? '本机 Claude Code · PR 快照' : 'DSH · 当前授权模型'
561
- const resourceSummary = selectedRunnerNeedsPr ? '指定仓库的已核验 PR 快照'
582
+ const resourceSummary = nativeReviewerSelected ? '指定仓库的已核验 PR 快照'
562
583
  : '当前会话中 DSH 实际可见的内容'
563
584
  const cardBlockReason = enrollmentLoadError ? '暂时无法核对机器人是否已启用协作,请刷新状态后再试。'
564
585
  : !botConnected || !selectedEnrollmentBound ? '先在上方把机器人绑定到这台 DSH。'
@@ -571,20 +592,56 @@ window.__ModuleLoader__.load({
571
592
  : parameters.some((rule) => rule.kind === 'gitea_pr')
572
593
  && normalizeServiceUrl(trustedOrigin) !== selectedEnrollment?.gitea_origin
573
594
  ? '请先保存机器人级可信 Gitea 地址;PR 参数会自动使用这个地址。'
574
- : selectedRunnerNeedsPr && (parameters.length !== 1 || parameters[0].kind !== 'gitea_pr')
595
+ : nativeReviewerSelected && (parameters.length !== 1 || parameters[0].kind !== 'gitea_pr')
575
596
  ? 'Codex 和 Claude Code 目前只支持单个 Gitea PR 审查任务。'
576
- : selectedRunnerNeedsPr && !parameters[0].required
597
+ : nativeReviewerSelected && !parameters[0].required
577
598
  ? '本机代码审查的 Gitea PR 编号必须设为必填。'
578
- : runner === 'codex' && !codexAvailable
579
- ? '这台 DSH 尚未找到可用的 Codex CLI,请先安装或检查 DSH 的 PATH。'
580
- : runner === 'claude-code' && !claudeCodeAvailable
581
- ? '这台 DSH 尚未注册 Claude Code 子代理,请先在 DSH 中安装并启用它。'
599
+ : reviewers.includes('dsh') && !dshAvailable
600
+ ? '这台 DSH 尚未准备好文字审查执行器。'
601
+ : reviewers.includes('codex') && !codexAvailable
602
+ ? '这台 DSH 尚未找到可用的 Codex CLI,请先安装或检查 DSH 的 PATH。'
603
+ : reviewers.includes('claude-code') && !claudeCodeAvailable
604
+ ? '这台 DSH 尚未注册 Claude Code 子代理,请先在 DSH 中安装并启用它。'
582
605
  : ''
583
606
  const loadNames = { bots: '机器人', 'enrolled-bots': '协作启用状态', groups: '群聊列表',
584
607
  cards: '任务授权', settings: '团队默认值', executors: '本机执行器',
585
608
  'local-settings': '本机设置', status: '最近任务' }
586
609
  const updateParameter = (index, patch) => setParameters((current) => current.map((rule, i) => i === index ? { ...rule, ...patch } : rule))
610
+ const changeReviewer = (id, enabled) => {
611
+ const next = enabled ? [...reviewers, id] : reviewers.filter((item) => item !== id)
612
+ if (!next.length) { setMessage('至少保留一位审查 Agent。'); return }
613
+ setReviewers([...new Set(next)])
614
+ if (!next.includes(runner)) setRunner(next[0])
615
+ }
616
+ const startNewCard = () => {
617
+ setEditingCardId(null); setEditingExpiresAt(null)
618
+ setDraftNotice('')
619
+ setTitle(''); setIntent(''); setExamples(''); setTaskPrompt(''); setRunner('dsh')
620
+ setReviewers(['dsh'])
621
+ setParameters([]); setConversationKind('dm'); setGroupId('')
622
+ setSenderScope('any'); setSenderIds(''); setDays(7); setCreateOpen(true)
623
+ }
624
+ const editCard = (card, copy = false) => {
625
+ setBotId(card.bot_id)
626
+ setTrustedOrigin(enrolled.find((bot) => bot.bot_id === card.bot_id)?.gitea_origin || '')
627
+ setEditingCardId(copy ? null : card.id); setEditingExpiresAt(copy ? null : card.expires_at)
628
+ setTitle(copy ? `${card.title.slice(0, 94)}(副本)` : card.title)
629
+ if (copy) setDays(7)
630
+ setIntent(card.intent); setExamples((card.examples || []).join('\n'))
631
+ setTaskPrompt(card.task_prompt); setRunner(card.runner)
632
+ setReviewers(Array.isArray(card.reviewers) && card.reviewers.length ? card.reviewers : [card.runner])
633
+ setParameters((card.parameters || []).map((rule) => ({
634
+ name: rule.name, kind: rule.kind, required: rule.required,
635
+ description: rule.description || '', min: rule.min == null ? '' : String(rule.min),
636
+ max: rule.max == null ? '' : String(rule.max), choices: (rule.choices || []).join(', '),
637
+ gitea_origin: rule.gitea_origin || '', repository: rule.repository || '',
638
+ })))
639
+ setConversationKind(card.conversation_kind); setGroupId(card.conversation_id || '')
640
+ setSenderScope(card.sender_scope); setSenderIds((card.sender_ids || []).join(', '))
641
+ setCreateOpen(true)
642
+ }
587
643
  const preparePrExample = () => {
644
+ setEditingCardId(null); setEditingExpiresAt(null)
588
645
  setTitle('审查 Gitea PR 差异')
589
646
  setIntent('同事希望查看指定 Gitea PR 的代码差异并获得审查意见。')
590
647
  setExamples('请帮我审查 123 号 PR\n看看刚才提到的 PR 有什么风险')
@@ -592,7 +649,13 @@ window.__ModuleLoader__.load({
592
649
  setParameters([{ name: 'pr_number', kind: 'gitea_pr', required: true,
593
650
  description: '要审查的 PR 编号', min: '1', max: '', choices: '',
594
651
  gitea_origin: selectedEnrollment?.gitea_origin || trustedOrigin || '', repository: '' }])
595
- setRunner(codexAvailable ? 'codex' : claudeCodeAvailable ? 'claude-code' : 'dsh')
652
+ const availableReviewers = [
653
+ ...(codexAvailable ? ['codex'] : []),
654
+ ...(claudeCodeAvailable ? ['claude-code'] : []),
655
+ ]
656
+ const selected = availableReviewers.length ? availableReviewers : ['dsh']
657
+ setReviewers(selected)
658
+ setRunner(selected[0])
596
659
  setCreateOpen(true)
597
660
  }
598
661
  const saveGiteaToken = async () => {
@@ -798,6 +861,9 @@ window.__ModuleLoader__.load({
798
861
  event.preventDefault()
799
862
  setDraftNotice('')
800
863
  if (!selectedBot || !botConnected || !selectedEnrollmentBound) { setMessage('请先在这台 DSH 绑定选中的协作机器人。'); return }
864
+ if (editingCardId && (!editingExpiresAt || editingExpiresAt <= Math.floor(Date.now() / 1000))) {
865
+ setMessage('这项能力已到期,无法继续编辑;请返回后重新开放一项能力。'); return
866
+ }
801
867
  if (!title.trim() || !intent.trim() || !taskPrompt.trim()) {
802
868
  setMessage('请填写能力名称、适用请求说明和处理要求。'); return
803
869
  }
@@ -849,45 +915,67 @@ window.__ModuleLoader__.load({
849
915
  if (normalized.some((rule) => rule.kind === 'gitea_pr' && !validRepository(rule.repository))) {
850
916
  setMessage('Gitea 仓库请填写“团队名/仓库名”,不要包含空格或多余路径。'); return
851
917
  }
852
- if (['codex', 'claude-code'].includes(runner) && (normalized.length !== 1
853
- || normalized[0].kind !== 'gitea_pr' || !normalized[0].required)) {
854
- setMessage('Codex 和 Claude Code 目前需要一个必填的 Gitea PR 编号参数。'); return
855
- }
856
- if (runner === 'codex' && !codexAvailable) {
857
- setMessage('这台 DSH 尚未找到可用的 Codex CLI。'); return
918
+ if (!reviewers.length || !reviewers.includes(runner)) {
919
+ setMessage('请至少选择一位审查 Agent,并设置普通请求的默认执行方。'); return
858
920
  }
859
- if (runner === 'claude-code' && !claudeCodeAvailable) {
860
- setMessage('这台 DSH 尚未注册 Claude Code 子代理。'); return
921
+ if (nativeReviewerSelected && (normalized.length !== 1
922
+ || normalized[0].kind !== 'gitea_pr' || !normalized[0].required)) {
923
+ setMessage('选择 Codex 或 Claude Code 时,需要一个必填的 Gitea PR 编号参数。'); return
861
924
  }
862
925
  const scope = conversationKind === 'group' ? `群 ${groupId}` : '私聊'
863
926
  const resultDestination = conversationKind === 'group' ? '原群(群内可见)' : '发起任务的私聊'
864
- const executionInfo = runner === 'codex'
865
- ? 'ANT 会使用这台电脑已登录的 Codex,在已核验 PR Head 的临时完整代码快照中固定只读权限;会传入所有者要求、请求人原话和 PR 差异,不传 Gitea 令牌。'
866
- : runner === 'claude-code'
867
- ? 'ANT 会调用这台 DSH 已注册的 Claude Code 子代理,在已核验 PR Head 的临时完整代码快照中审查;会传入所有者要求、请求人原话和 PR 差异,不传 Gitea 令牌。它沿用 DSH 中 Claude Code 的本机权限设置;请确认该设置适合由获准成员请求审查。'
868
- : '这项任务仅由受限 DSH 处理已提供的文字。'
869
- if (!window.confirm(`开放“${title}”后,${requesterSummary}可在${scope}请求,有效期 ${days} 天;结果${resultDestination}。${executionInfo}请确认。`)) return
927
+ const executionDetails = {
928
+ dsh: 'DSH 只根据已提供的 PR diff 审查,不读取完整仓库。',
929
+ codex: 'Codex 使用已核验 PR Head 的临时代码快照,并启用本机固定只读权限;不接收 Gitea 令牌。',
930
+ 'claude-code': 'Claude Code 使用同一份临时代码快照,沿用这台 DSH 的本机权限设置;请确认该权限适合由获准成员请求审查。',
931
+ }
932
+ const executionInfo = reviewers.length > 1
933
+ ? `普通请求默认由 ${reviewerLabel(runner)} 执行;请求人明确说“交叉 review”时,会并行启动 ${reviewers.map(reviewerLabel).join('、')},分别返回审查结果。${reviewers.map((item) => executionDetails[item]).join(' ')}`
934
+ : executionDetails[reviewers[0]]
935
+ const actionDescription = editingCardId
936
+ ? `更新“${title}”后,${requesterSummary}可在${scope}使用新配置;旧待确认请求将失效,已提交任务继续使用提交时的配置。结果${resultDestination}。到期时间保持为 ${new Date(editingExpiresAt * 1000).toLocaleString('zh-CN')}。`
937
+ : `开放“${title}”后,${requesterSummary}可在${scope}请求,有效期 ${days} 天;结果${resultDestination}。`
938
+ if (!window.confirm(`${actionDescription}${executionInfo}请确认。`)) return
870
939
  setBusy(true)
871
940
  try {
872
941
  const response = await fetch('/api/ant-teams/delegation/cards', {
873
- method: 'POST', headers: { 'content-type': 'application/json' },
874
- body: JSON.stringify({ bot_id: selectedBot.bot_id, bot_fingerprint: selectedBot.fingerprint,
942
+ method: editingCardId ? 'PUT' : 'POST', headers: { 'content-type': 'application/json' },
943
+ body: JSON.stringify({ ...(editingCardId ? { id: editingCardId } : {}),
944
+ bot_id: selectedBot.bot_id, bot_fingerprint: selectedBot.fingerprint,
875
945
  title, intent, examples: exampleLines,
876
- parameters: normalized, task_prompt: taskPrompt, runner, workspace: null,
946
+ parameters: normalized, task_prompt: taskPrompt, runner, reviewers, workspace: null,
877
947
  sender_scope: senderScope, sender_ids: senderScope === 'allowlist'
878
948
  ? senderIds.split(/[\s,,]+/u).filter(Boolean) : [], conversation_kind: conversationKind,
879
949
  ...(conversationKind === 'group' ? { conversation_id: groupId } : {}),
880
- expires_at: Math.floor(Date.now() / 1000) + Number(days) * 86400 }),
950
+ expires_at: editingCardId ? editingExpiresAt : Math.floor(Date.now() / 1000) + Number(days) * 86400 }),
881
951
  })
882
952
  const result = await readLocalJsonResponse(response)
883
- if (!response.ok && runner === 'claude-code' && result.code === 'INVALID_REQUEST') {
884
- throw new Error('当前团队服务端可能尚不支持 Claude Code 执行方。请管理员升级服务端后刷新重试。')
953
+ if (!response.ok && reviewers.includes('claude-code') && result.code === 'INVALID_REQUEST') {
954
+ throw new Error('当前团队服务端可能尚不支持 Claude Code 审查配置。请管理员升级服务端后刷新重试。')
885
955
  }
886
956
  if (!response.ok) throw new Error(result.message || '能力保存失败。')
887
- setTitle(''); setIntent(''); setExamples(''); setTaskPrompt(''); setParameters([]); setRunner('dsh')
957
+ const savedReviewers = Array.isArray(result.reviewers) && result.reviewers.length
958
+ ? result.reviewers : [result.runner]
959
+ if ([...savedReviewers].sort().join('\0') !== [...reviewers].sort().join('\0')) {
960
+ if (!editingCardId && typeof result.id === 'string') {
961
+ const cleanup = await fetch('/api/ant-teams/delegation/cards', {
962
+ method: 'DELETE', headers: { 'content-type': 'application/json' },
963
+ body: JSON.stringify({ id: result.id }),
964
+ }).catch(() => null)
965
+ if (!cleanup?.ok) {
966
+ await load().catch(() => undefined)
967
+ throw new Error('团队服务端尚不支持多 Agent 审查配置;旧服务端可能已保存单 Agent 授权,请刷新列表并停止它,再升级 ANT 服务端。')
968
+ }
969
+ }
970
+ throw new Error('当前团队服务端尚不支持多 Agent 审查配置;请管理员升级 ANT 服务端后重试。')
971
+ }
972
+ setTitle(''); setIntent(''); setExamples(''); setTaskPrompt(''); setParameters([]); setRunner('dsh'); setReviewers(['dsh'])
973
+ setEditingCardId(null); setEditingExpiresAt(null)
888
974
  await load()
889
975
  setCreateOpen(false)
890
- setMessage('能力已开放。同事可自然地提出这类请求;参数不完整时机器人会追问。')
976
+ setMessage(editingCardId
977
+ ? '能力配置已更新。新请求将使用新配置,已提交任务保留原配置。'
978
+ : '能力已开放。同事可自然地提出这类请求;参数不完整时机器人会追问。')
891
979
  } catch (error) { setMessage(error.message) }
892
980
  finally { setBusy(false) }
893
981
  }
@@ -904,6 +992,7 @@ window.__ModuleLoader__.load({
904
992
  finally { setBusy(false) }
905
993
  }
906
994
  const copyCardToDraft = (card) => {
995
+ setEditingCardId(null); setEditingExpiresAt(null)
907
996
  const cardBot = bots.find((bot) => bot.fingerprint === card.bot_fingerprint)
908
997
  const cardEnrollment = enrolled.find((bot) => bot.bot_fingerprint === card.bot_fingerprint)
909
998
  const titleChars = Array.from(`复制:${card.title || '未命名能力'}`)
@@ -915,6 +1004,7 @@ window.__ModuleLoader__.load({
915
1004
  setExamples(Array.isArray(card.examples) ? card.examples.join('\n') : '')
916
1005
  setTaskPrompt(card.task_prompt || '')
917
1006
  setRunner(['dsh', 'codex', 'claude-code'].includes(card.runner) ? card.runner : 'dsh')
1007
+ setReviewers(Array.isArray(card.reviewers) && card.reviewers.length ? card.reviewers : [card.runner || 'dsh'])
918
1008
  setParameters(Array.isArray(card.parameters) ? card.parameters.map((rule) => ({
919
1009
  ...rule, min: String(rule.min ?? ''), max: String(rule.max ?? ''), choices: Array.isArray(rule.choices) ? rule.choices.join(',') : String(rule.choices ?? ''),
920
1010
  })) : [])
@@ -1061,8 +1151,6 @@ window.__ModuleLoader__.load({
1061
1151
  ? `群 ${groups.find((group) => group.group_id === card.conversation_id)?.group_code || card.conversation_id}` : '私聊'
1062
1152
  const requester = card.sender_scope === 'allowlist' ? '指定成员可请求'
1063
1153
  : card.conversation_kind === 'group' ? '群成员可请求' : '可私聊者可请求'
1064
- const runnerName = card.runner === 'codex' ? '本机 Codex · PR 快照'
1065
- : card.runner === 'claude-code' ? '本机 Claude Code · PR 快照' : 'DSH 文字处理'
1066
1154
  const parametersText = (card.parameters || []).map((rule) => {
1067
1155
  const constraint = rule.kind === 'gitea_pr' && rule.repository ? `仓库 ${rule.repository}`
1068
1156
  : rule.kind === 'choice' ? `选项 ${(rule.choices || []).join('、')}`
@@ -1081,7 +1169,7 @@ window.__ModuleLoader__.load({
1081
1169
  h('strong', { style: { overflowWrap: 'anywhere' } }, card.title || '未命名能力'),
1082
1170
  h('span', { style: { ...mutedTextStyle, padding: '2px 8px', borderRadius: 999,
1083
1171
  backgroundColor: 'var(--dsw-alias-bg-layer-2)', color: remainingDaysLabel, whiteSpace: 'nowrap' } }, expiry)),
1084
- h('span', { style: mutedTextStyle }, `${runnerName} · ${conversation} · ${requester} · 结果回复原${card.conversation_kind === 'group' ? '群' : '私聊'}`),
1172
+ h('span', { style: mutedTextStyle }, `${cardReviewerSummary(card)} · ${conversation} · ${requester} · 结果回复原${card.conversation_kind === 'group' ? '群' : '私聊'}`),
1085
1173
  h('details', null,
1086
1174
  h('summary', { style: { cursor: 'pointer', color: 'var(--dsw-alias-link)', fontSize: 13 } }, '查看配置'),
1087
1175
  h('div', { style: { display: 'grid', gap: 4, paddingTop: 6 } },
@@ -1092,6 +1180,7 @@ window.__ModuleLoader__.load({
1092
1180
  examplesText ? h('p', { style: mutedTextStyle }, `请求示例:${examplesText}`) : null,
1093
1181
  card.task_prompt ? h('p', { style: mutedTextStyle }, `处理要求:${card.task_prompt}`) : null))),
1094
1182
  h('div', { style: { display: 'flex', gap: 6, flexWrap: 'wrap', justifyContent: 'flex-end' } },
1183
+ h('button', { type: 'button', disabled: busy, onClick: () => editCard(card), style: secondaryButtonStyle }, '编辑'),
1095
1184
  h('button', { type: 'button', disabled: busy, onClick: () => copyCardToDraft(card), style: secondaryButtonStyle }, '复制'),
1096
1185
  h('button', { type: 'button', disabled: busy, onClick: () => void revokeCard(card), style: secondaryButtonStyle }, '停止')))
1097
1186
  }),
@@ -1127,10 +1216,12 @@ window.__ModuleLoader__.load({
1127
1216
  h('details', { id: 'ant-new-task', open: createOpen,
1128
1217
  onToggle: (event) => setCreateOpen(event.currentTarget.open),
1129
1218
  style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, padding: 12 } },
1130
- h('summary', { id: 'ant-new-task-summary', style: { cursor: 'pointer', fontWeight: 600 } }, '开放一项能力'),
1219
+ h('summary', { id: 'ant-new-task-summary', style: { cursor: 'pointer', fontWeight: 600 } }, editingCardId ? '编辑能力配置' : '开放一项能力'),
1131
1220
  h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 320px), 1fr))',
1132
1221
  gap: 16, alignItems: 'start', marginTop: 12 } },
1133
1222
  h('form', { id: 'ant-capability-create-form', onSubmit: createCard, style: { display: 'grid', gap: 9, minWidth: 0 } },
1223
+ editingCardId ? h('p', { role: 'status', style: mutedTextStyle },
1224
+ `保存会生成新的授权版本并停止旧版本;到期时间保持不变:${new Date(editingExpiresAt * 1000).toLocaleString('zh-CN')}。已提交任务继续使用原配置。`) : null,
1134
1225
  h('p', { style: mutedTextStyle }, selectedBot
1135
1226
  ? `当前机器人:${selectedBot.name}${selectedEnrollmentBound ? ' · 已绑定此设备' : ' · 请先绑定到此设备'}`
1136
1227
  : '请先在上方选择机器人并启用协作。'),
@@ -1150,20 +1241,29 @@ window.__ModuleLoader__.load({
1150
1241
  onChange: (event) => setExamples(event.target.value), placeholder: '帮我总结一下刚才的讨论', style: inputStyle })),
1151
1242
  h('label', null, '处理要求', h('textarea', { required: true, maxLength: 4000, rows: 3, value: taskPrompt,
1152
1243
  onChange: (event) => setTaskPrompt(event.target.value), placeholder: '只使用当前会话可见的内容,列出结论和待办', style: inputStyle })),
1153
- h('label', null, '执行方', h('select', { value: runner,
1154
- onChange: (event) => setRunner(event.target.value), style: inputStyle },
1155
- h('option', { value: 'dsh' }, 'DSH · 已提供文字'),
1156
- h('option', { value: 'codex',
1157
- disabled: !codexAvailable }, 'Codex · 单个 Gitea PR 完整快照审查试用'),
1158
- h('option', { value: 'claude-code', disabled: !claudeCodeAvailable },
1159
- 'Claude Code · 单个 Gitea PR 完整快照试用'))),
1244
+ h('fieldset', { style: { ...panelStyle, padding: 10, display: 'grid', gap: 7 } },
1245
+ h('legend', null, '审查 Agent(可多选)'),
1246
+ h('p', { style: mutedTextStyle }, '普通请求使用默认 Agent;明确说“交叉 review”时,所有勾选项会并行审查同一份 PR 快照(至少勾选两位才构成交叉审查,最多三位,并会同时消耗各自的本机或团队模型额度)。'),
1247
+ h('div', { style: { display: 'flex', gap: 14, flexWrap: 'wrap' } },
1248
+ h('label', null, h('input', { type: 'checkbox', 'aria-label': '审查 Agent DSH', checked: reviewers.includes('dsh'), disabled: !dshAvailable && !reviewers.includes('dsh'),
1249
+ onChange: (event) => changeReviewer('dsh', event.target.checked) }), ' DSH · 仅审查已提供的 diff'),
1250
+ h('label', null, h('input', { type: 'checkbox', 'aria-label': '审查 Agent Codex', checked: reviewers.includes('codex'),
1251
+ disabled: (!codexAvailable || !hasPrParameter) && !reviewers.includes('codex'),
1252
+ onChange: (event) => changeReviewer('codex', event.target.checked) }), ' Codex · 完整快照只读审查'),
1253
+ h('label', null, h('input', { type: 'checkbox', 'aria-label': '审查 Agent Claude Code', checked: reviewers.includes('claude-code'),
1254
+ disabled: (!claudeCodeAvailable || !hasPrParameter) && !reviewers.includes('claude-code'),
1255
+ onChange: (event) => changeReviewer('claude-code', event.target.checked) }), ' Claude Code · 使用本机权限审查')),
1256
+ !hasPrParameter ? h('p', { style: mutedTextStyle }, 'Codex 与 Claude Code 需要先添加一个必填的 Gitea PR 编号参数。') : null,
1257
+ reviewers.length > 1 ? h('label', null, '普通请求默认执行方', h('select', { value: runner,
1258
+ onChange: (event) => setRunner(event.target.value), style: inputStyle },
1259
+ ...reviewers.map((item) => h('option', { key: item, value: item }, reviewerLabel(item))))) : null),
1160
1260
  nativePrUnavailableReason ? h('p', { role: 'status', style: mutedTextStyle },
1161
1261
  `${nativePrUnavailableReason},已禁用 Codex/Claude Code 完整快照选项;可选择 DSH 差异审查。`) : null,
1162
- runner === 'codex' ? h('p', { role: 'status', style: mutedTextStyle },
1163
- 'Codex 使用这台电脑的登录账号及其用量;ANT 为每项能力维护仓库缓存,每次审查创建固定到 PR Head 的临时代码快照。不会收到 Gitea 令牌;请以这台 DSH 的实际子代理权限为准,只有强制只读隔离已验证时才视为只读。Git LFS 实际文件不会下载。') : null,
1164
- runner === 'claude-code' ? h('p', { role: 'status', style: mutedTextStyle },
1165
- 'Claude Code 通过 DSH 已注册的子代理运行,可查看本次 PR 的临时代码快照。ANT 不会传 Gitea 令牌;Claude Code 沿用 DSH 提供方的本机权限,不具备 Codex 路径相同的固定只读隔离。Git LFS 实际文件不会下载。') : null,
1166
- selectedRunnerNeedsPr ? h('p', { role: 'status', style: mutedTextStyle },
1262
+ reviewers.includes('codex') ? h('p', { role: 'status', style: mutedTextStyle },
1263
+ 'Codex 使用这台电脑的登录账号及其用量;ANT 为每项能力维护仓库缓存,每次审查创建固定到 PR Head 的临时代码快照。不会收到 Gitea 令牌;Git LFS 实际文件不会下载。') : null,
1264
+ reviewers.includes('claude-code') ? h('p', { role: 'status', style: mutedTextStyle },
1265
+ 'Claude Code 通过 DSH 已注册的子代理运行,可查看同一 PR 快照。ANT 不会传 Gitea 令牌;它沿用 DSH 的本机权限设置,不具备 Codex 路径相同的固定只读隔离。Git LFS 实际文件不会下载。') : null,
1266
+ nativeReviewerSelected ? h('p', { role: 'status', style: mutedTextStyle },
1167
1267
  '本机 PR 执行方需要 Gitea granular token 的只读权限 read:repository 和 read:user;read:user 仅用于这台 DSH 获取 Git HTTPS 登录名。') : null,
1168
1268
  h('details', { style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, padding: 10 } },
1169
1269
  h('summary', { style: { cursor: 'pointer' } }, '参数与授权范围'),
@@ -1180,7 +1280,7 @@ window.__ModuleLoader__.load({
1180
1280
  onChange: (event) => updateParameter(index, { kind: event.target.value }), style: { ...inputStyle, flex: '1 1 150px' } },
1181
1281
  h('option', { value: 'text' }, '文本'), h('option', { value: 'integer' }, '整数'),
1182
1282
  h('option', { value: 'choice' }, '选项'), h('option', { value: 'gitea_pr' }, 'Gitea PR 编号')),
1183
- h('label', null, h('input', { type: 'checkbox', checked: rule.required, 'aria-label': '参数必填',
1283
+ h('label', null, h('input', { type: 'checkbox', 'aria-label': '参数必填', checked: rule.required,
1184
1284
  onChange: (event) => updateParameter(index, { required: event.target.checked }) }), ' 必填'),
1185
1285
  ['integer', 'gitea_pr'].includes(rule.kind) ? h('input', { 'aria-label': '最小值', type: 'number', value: rule.min,
1186
1286
  onChange: (event) => updateParameter(index, { min: event.target.value }), placeholder: '最小值', style: { ...inputStyle, width: 120 } }) : null,
@@ -1219,8 +1319,9 @@ window.__ModuleLoader__.load({
1219
1319
  h('option', { value: 'allowlist' }, '指定 IM 用户 ID'))),
1220
1320
  senderScope === 'allowlist' ? h('input', { 'aria-label': '可请求的 IM 用户 ID', required: true, value: senderIds,
1221
1321
  onChange: (event) => setSenderIds(event.target.value), placeholder: '多个 ID 用逗号分隔', style: inputStyle }) : null,
1222
- h('label', null, '有效期', h('select', { value: days, onChange: (event) => setDays(Number(event.target.value)), style: inputStyle },
1223
- h('option', { value: 1 }, '1 天'), h('option', { value: 7 }, '7 天'), h('option', { value: 30 }, '30 天')))),
1322
+ editingCardId ? h('p', { style: mutedTextStyle }, `有效期:${new Date(editingExpiresAt * 1000).toLocaleString('zh-CN')}(编辑时保持不变)`)
1323
+ : h('label', null, '有效期', h('select', { value: days, onChange: (event) => setDays(Number(event.target.value)), style: inputStyle },
1324
+ h('option', { value: 1 }, '1 天'), h('option', { value: 7 }, '7 天'), h('option', { value: 30 }, '30 天')))),
1224
1325
  ),
1225
1326
  h('aside', { id: 'ant-authorization-review', role: 'region', 'aria-label': '开放前复核', tabIndex: -1,
1226
1327
  style: { ...settingsPanelStyle, padding: 14, gap: 10,
@@ -1236,7 +1337,7 @@ window.__ModuleLoader__.load({
1236
1337
  ['可请求人', requesterSummary],
1237
1338
  ['执行方', executorSummary],
1238
1339
  ['读取资料', resourceSummary],
1239
- ['有效期', `${days} 天`],
1340
+ ['有效期', editingCardId ? new Date(editingExpiresAt * 1000).toLocaleString('zh-CN') : `${days} 天`],
1240
1341
  ['回复位置', resultRouteSummary],
1241
1342
  ].map(([label, value]) => h('div', { key: label, style: { display: 'grid', gridTemplateColumns: '84px minmax(0, 1fr)', gap: 8 } },
1242
1343
  h('dt', { style: mutedTextStyle }, label),
@@ -1244,7 +1345,7 @@ window.__ModuleLoader__.load({
1244
1345
  cardBlockReason ? h('p', { role: 'status', style: { ...mutedTextStyle,
1245
1346
  color: 'var(--dsw-alias-state-warning-primary, var(--dsw-alias-label-secondary))' } }, cardBlockReason) : null,
1246
1347
  h('button', { type: 'submit', form: 'ant-capability-create-form', disabled: busy || Boolean(cardBlockReason),
1247
- style: { ...primaryButtonStyle, width: '100%' } }, '确认并开放能力'))))),
1348
+ style: { ...primaryButtonStyle, width: '100%' } }, editingCardId ? '保存能力修改' : '确认并开放能力'))))),
1248
1349
  h('div', { role: 'region', 'aria-label': '本机处理设置', style: { display: activeTab === 'local' ? 'grid' : 'none', gap: 12 } },
1249
1350
  h('section', { style: { ...settingsPanelStyle, padding: 14 } },
1250
1351
  h('strong', null, 'Gitea 本机凭据'),
@@ -1596,15 +1697,21 @@ window.__ModuleLoader__.load({
1596
1697
  useEffect(() => { void refresh() }, [refreshToken])
1597
1698
  const install = async (plugin) => {
1598
1699
  const artifactUrl = approvedPluginArtifactUrl(plugin)
1599
- if (!artifactUrl || workingId || uncertainId === plugin.id || completedId === plugin.id) return
1700
+ const installSpec = pluginInstallSpec(plugin)
1701
+ if (!installSpec || workingId || uncertainId === plugin.id || completedId === plugin.id) return
1600
1702
  const manager = remote?.pluginManager
1601
1703
  if (typeof manager?.installBundle !== 'function') {
1602
1704
  setMessage('当前 DSH 未提供插件安装接口,请联系管理员。')
1603
1705
  return
1604
1706
  }
1605
1707
  const local = bundles.find((bundle) => bundle.name === plugin.package_name && bundle.installed)
1606
- const action = local ? local.version === plugin.version ? '重新安装' : '更新' : '安装'
1607
- if (!window.confirm(`在这台 DSH ${action}团队审核的 ${plugin.title} ${plugin.version}?插件将在本机运行,完成后需要完全重启 DSH。`)) return
1708
+ const localAhead = local?.version && newerVersion(local.version, plugin.version)
1709
+ const action = localAhead ? '回退到团队版本' : local ? local.version === plugin.version ? '重新安装' : '更新' : '安装'
1710
+ const sourceNotice = artifactUrl
1711
+ ? 'DSH 将安装 ANT 固定的顶层制品。'
1712
+ : `DSH 将优先从 npm 官方源以 ${installSpec} 安装此精确版本,并按其配置回退 registry;依赖版本可能因设备不同。`
1713
+ const updateNotice = 'ANT 不会批准依赖构建脚本;插件自身的更新器或 DSH 原生管理页之后仍可安装其他版本。'
1714
+ if (!window.confirm(`在这台 DSH ${action}团队审核的 ${plugin.title} ${plugin.version}?${sourceNotice}${updateNotice} 插件将在本机运行,完成后需要完全重启 DSH。`)) return
1608
1715
  setWorkingId(plugin.id)
1609
1716
  setMessage(`正在安装 ${plugin.title},请不要退出 DSH…`)
1610
1717
  try {
@@ -1613,13 +1720,15 @@ window.__ModuleLoader__.load({
1613
1720
  const latest = Array.isArray(catalog?.plugins)
1614
1721
  ? catalog.plugins.find((item) => item.id === plugin.id) : null
1615
1722
  if (!response.ok || latest?.package_name !== plugin.package_name
1616
- || latest?.version !== plugin.version || latest?.artifact_sha256 !== plugin.artifact_sha256
1617
- || approvedPluginArtifactUrl(latest) !== artifactUrl) {
1723
+ || latest?.version !== plugin.version || pluginInstallSpec(latest) !== installSpec) {
1618
1724
  throw new Error('插件审核信息已变化,请刷新目录后重试。')
1619
1725
  }
1620
1726
  const requestId = crypto.randomUUID()
1621
1727
  let result
1622
- try { result = await manager.installBundle(artifactUrl, { enabled: true, requestId }) }
1728
+ const options = artifactUrl
1729
+ ? { enabled: true, requestId }
1730
+ : { enabled: true, requestId, registry: 'https://registry.npmjs.org/' }
1731
+ try { result = await manager.installBundle(installSpec, options) }
1623
1732
  catch { result = null }
1624
1733
  if (!result?.ok) {
1625
1734
  let recovered = null
@@ -1671,7 +1780,9 @@ window.__ModuleLoader__.load({
1671
1780
  return
1672
1781
  }
1673
1782
  await refresh()
1674
- try { localStorage.setItem(`${PLUGIN_INSTALL_RECORD_PREFIX}${plugin.id}`, plugin.artifact_sha256) } catch {}
1783
+ if (artifactUrl && plugin.artifact_sha256) {
1784
+ try { localStorage.setItem(`${PLUGIN_INSTALL_RECORD_PREFIX}${plugin.id}`, plugin.artifact_sha256) } catch {}
1785
+ }
1675
1786
  setCompletedId(plugin.id)
1676
1787
  setMessage(`${plugin.title} 已安装。请完全退出并重新启动 DSH 后使用。`)
1677
1788
  } catch (error) {
@@ -1682,10 +1793,12 @@ window.__ModuleLoader__.load({
1682
1793
  const visible = plugins.filter((plugin) => {
1683
1794
  const installed = bundles.find((bundle) => bundle.name === plugin.package_name && bundle.installed)
1684
1795
  const artifactUrl = approvedPluginArtifactUrl(plugin)
1796
+ const installSpec = pluginInstallSpec(plugin)
1797
+ const localAhead = installed?.version && newerVersion(installed.version, plugin.version)
1685
1798
  const matches = !query.trim() || `${plugin.title} ${plugin.description} ${plugin.package_name}`.toLowerCase().includes(query.trim().toLowerCase())
1686
1799
  return matches && (activeFilter === 'all' || activeFilter === 'installed' && Boolean(installed)
1687
- || activeFilter === 'available' && artifactUrl && (!installed || (installed.version && installed.version !== plugin.version))
1688
- || activeFilter === 'updates' && artifactUrl && installed && installed.version && installed.version !== plugin.version
1800
+ || activeFilter === 'available' && installSpec && !localAhead && (!installed || (installed.version && installed.version !== plugin.version))
1801
+ || activeFilter === 'updates' && installSpec && installed && installed.version && installed.version !== plugin.version && !localAhead
1689
1802
  || activeFilter === 'retired' && plugin.status !== 'published')
1690
1803
  })
1691
1804
  const noMatches = embedded
@@ -1707,7 +1820,7 @@ window.__ModuleLoader__.load({
1707
1820
  embedded ? null : h('div', { style: { display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', gap: 16 } },
1708
1821
  h('div', null,
1709
1822
  h('h1', { style: { margin: 0, fontSize: 28 } }, '企业精选插件'),
1710
- h('p', { style: { ...mutedTextStyle, marginTop: 6 } }, '这里展示团队审核过的插件版本及本机状态。')),
1823
+ h('p', { style: { ...mutedTextStyle, marginTop: 6 } }, '这里展示团队审核过的插件版本及本机状态。DSH 0.2.0-rc.2 不会自动更新 Bundle;插件自身的更新器可能绕过此目录安装其他版本。')),
1711
1824
  h('button', { type: 'button', disabled: loading, onClick: () => void refresh(), style: secondaryButtonStyle }, loading ? '读取中…' : '刷新目录')),
1712
1825
  !loading && !plugins.length && !loadError ? embedded
1713
1826
  ? h('p', { role: 'status', style: mutedTextStyle }, '团队暂未发布精选插件。')
@@ -1729,16 +1842,19 @@ window.__ModuleLoader__.load({
1729
1842
  const sameVersion = local?.version === plugin.version
1730
1843
  const security = plugin.status === 'security_withdrawn'
1731
1844
  const artifactUrl = approvedPluginArtifactUrl(plugin)
1845
+ const installSpec = pluginInstallSpec(plugin)
1846
+ const localAhead = local?.version && newerVersion(local.version, plugin.version)
1732
1847
  const sourceKnown = typeof local?.source === 'string'
1733
1848
  const verifiedLocal = sameVersion && artifactUrl && sourceKnown && local.source === artifactUrl
1734
1849
  const recordedInstall = sameVersion && !sourceKnown && previouslyInstalledReviewedPlugin(plugin)
1735
1850
  const status = security ? '已废弃(安全风险)'
1736
- : plugin.status === 'deprecated' ? '已废弃' : local?.error ? '加载异常' : sameVersion
1851
+ : plugin.status === 'deprecated' ? '已废弃' : local?.error ? '加载异常' : localAhead
1852
+ ? '本机版本高于团队目录版本' : sameVersion
1737
1853
  ? verifiedLocal ? local.enabled ? '本机已装(团队来源,已启用)' : '本机已装(团队来源,未启用)'
1738
1854
  : sourceKnown ? '本机同版,来源不同' : recordedInstall
1739
1855
  ? '本机同版,曾由企业扩展安装;当前 DSH 无法复核来源' : '本机同版,当前 DSH 无法核对来源'
1740
- : artifactUrl ? local ? local.version ? `可更新 · 本机 ${local.version}` : '本机版本暂无法读取' : '可安装'
1741
- : local ? `本机版本 ${local.version || '暂无法读取'}` : '已审核,暂需管理员安装'
1856
+ : installSpec ? local ? local.version ? `可更新 · 本机 ${local.version}` : '本机版本暂无法读取' : artifactUrl ? '可安装' : '可从 npm 安装'
1857
+ : local ? `本机版本 ${local.version || '暂无法读取'}` : '已发布版本暂不可安装'
1742
1858
  return h('article', { key: plugin.id, style: security
1743
1859
  ? { ...panelStyle, borderColor: 'var(--dsw-alias-label-danger, #d44)' } : panelStyle },
1744
1860
  h('div', { style: { display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', gap: 8 } },
@@ -1751,6 +1867,10 @@ window.__ModuleLoader__.load({
1751
1867
  h('p', { style: mutedTextStyle }, `${plugin.package_name}@${plugin.version} · 许可证:${plugin.license || '未声明'}`),
1752
1868
  h('p', { style: mutedTextStyle }, `来源:${plugin.source_url || '未提供'} · DSH 版本要求:${plugin.peer_range || '包未声明'}`),
1753
1869
  h('p', { style: mutedTextStyle }, `额外配置:${plugin.requirements || '无'}`),
1870
+ installSpec && !artifactUrl ? h('p', { role: 'note', style: mutedTextStyle },
1871
+ 'ANT 通过 DSH 包管理器安装目录中的精确 npm 版本;实际下载和依赖解析由 DSH 执行。ANT 不批准依赖构建脚本;若 DSH 因此拒绝安装,请按 DSH 提示在原生插件管理页处理。插件自身更新器或 DSH 原生管理也可更新到目录外的版本。') : null,
1872
+ plugin.dependencies && artifactUrl ? h('p', { role: 'note', style: mutedTextStyle },
1873
+ '此插件声明了依赖。ANT 固定插件包本身;DSH 会在本机按当前配置解析依赖,不同设备可能得到不同版本。依赖构建脚本由 DSH 单独请求批准。') : null,
1754
1874
  plugin.scripts ? h('p', { style: mutedTextStyle }, `包自身的安装脚本:${plugin.scripts}`) : null,
1755
1875
  plugin.status !== 'published' ? h('p', { role: 'status', style: security
1756
1876
  ? { ...mutedTextStyle, color: 'var(--dsw-alias-label-danger, #d44)' } : mutedTextStyle },
@@ -1760,16 +1880,17 @@ window.__ModuleLoader__.load({
1760
1880
  ? '此前曾由企业扩展安装;当前 DSH 无法再次核对来源。正常使用无需重复安装;若后来换过来源,可以重新安装。'
1761
1881
  : '已装同版且使用正常时无需重复安装;若想改用团队审核包,可自愿安装。') : null,
1762
1882
  h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8 } },
1763
- artifactUrl && (!local || (local.version && !verifiedLocal))
1883
+ installSpec && (!local || (local.version && local.version !== plugin.version && (!localAhead || artifactUrl))
1884
+ || (artifactUrl && sameVersion && !verifiedLocal))
1764
1885
  ? h('button', { type: 'button', disabled: Boolean(workingId) || uncertainId === plugin.id || completedId === plugin.id,
1765
1886
  onClick: () => void install(plugin), style: primaryButtonStyle },
1766
1887
  workingId === plugin.id ? '安装中…' : uncertainId === plugin.id ? '先核对安装结果'
1767
1888
  : completedId === plugin.id ? '重启 DSH 后生效'
1768
- : local ? sameVersion ? '按团队审核包重新安装' : '更新到审核版本' : '安装到这台 DSH') : null,
1769
- plugin.status === 'published' && !local && !artifactUrl
1889
+ : local ? sameVersion || localAhead ? '按团队审核包重新安装' : '更新到审核版本' : '安装到这台 DSH') : null,
1890
+ plugin.status === 'published' && !local && !installSpec
1770
1891
  ? h('p', { style: mutedTextStyle }, '如需安装,请联系企业管理员安排。') : null,
1771
1892
  local || uncertainId === plugin.id ? h('button', { type: 'button', onClick: () => openBundle(plugin.package_name), style: secondaryButtonStyle }, '打开 DSH 插件管理') : null,
1772
- local && !sameVersion && plugin.status === 'published' && !artifactUrl
1893
+ local && !sameVersion && plugin.status === 'published' && !installSpec
1773
1894
  ? h('p', { style: mutedTextStyle }, '本机版本与团队审核版本不同;请联系企业管理员核对。') : null))
1774
1895
  })) : !loading && plugins.length && (!embedded || !hasSkillMatches)
1775
1896
  ? h('p', { style: mutedTextStyle }, noMatches) : null))
@@ -1790,6 +1911,11 @@ window.__ModuleLoader__.load({
1790
1911
  const [bundleRelease, setBundleRelease] = useState(null)
1791
1912
  const [bundleUpdate, setBundleUpdate] = useState({ state: 'idle', currentVersion: '', message: '' })
1792
1913
  const [authorizedModels, setAuthorizedModels] = useState([])
1914
+ const [serverDefaultModel, setServerDefaultModel] = useState(null)
1915
+ const [dshDefaultSelection, setDshDefaultSelection] = useState(null)
1916
+ const [defaultModelDraft, setDefaultModelDraft] = useState('')
1917
+ const [defaultModelBusy, setDefaultModelBusy] = useState(false)
1918
+ const [defaultModelMessage, setDefaultModelMessage] = useState('')
1793
1919
  const [lastModelSync, setLastModelSync] = useState(null)
1794
1920
  const [serviceCompatibility, setServiceCompatibility] = useState({ state: 'idle', message: '' })
1795
1921
  const [pinnedServiceUrl, setPinnedServiceUrl] = useState(null)
@@ -1863,27 +1989,87 @@ window.__ModuleLoader__.load({
1863
1989
  return status
1864
1990
  }
1865
1991
 
1866
- const applyAuthorizedModels = async (models, rawServiceUrl) => {
1992
+ const modelProvider = (model) => model.protocol === 'anthropic' ? 'ant-team-anthropic'
1993
+ : model.openai_api_mode === 'responses' ? 'ant-team-openai-responses' : 'ant-team-openai-chat-completions'
1994
+ const modelSelectionKey = (model) => JSON.stringify({ protocol: model.protocol, model_id: model.model_id })
1995
+ const defaultPreferenceKey = (rawServiceUrl) => `${DEFAULT_MODEL_INITIALIZED_KEY}:${normalizeServiceUrl(rawServiceUrl)}:${savedDeviceId() || ''}`
1996
+ const readDefaultPreference = (rawServiceUrl) => {
1997
+ try { return JSON.parse(localStorage.getItem(defaultPreferenceKey(rawServiceUrl)) || 'null') } catch { return null }
1998
+ }
1999
+ const saveDefaultPreference = (rawServiceUrl, value) => {
2000
+ try { localStorage.setItem(defaultPreferenceKey(rawServiceUrl), JSON.stringify(value)) } catch {}
2001
+ }
2002
+ const modelForProviderSelection = (selection, models) => {
2003
+ if (!selection) return null
2004
+ const exact = models.find((model) => model.model_id === selection.model && modelProvider(model) === selection.provider)
2005
+ if (exact) return exact
2006
+ const legacyProtocol = selection.provider === 'ant-team-openai' || selection.provider?.startsWith('ant-team-openai-')
2007
+ ? 'openai' : selection.provider === 'ant-team-anthropic' ? 'anthropic' : null
2008
+ return legacyProtocol ? models.find((model) => model.protocol === legacyProtocol && model.model_id === selection.model) : null
2009
+ }
2010
+ const readDshDefaultSelection = async () => {
2011
+ if (typeof remote?.settings?.describe !== 'function') throw new Error('当前 DSH 未提供全局默认模型读取接口。')
2012
+ const response = await remote.settings.describe()
2013
+ if (!response?.ok) throw new Error(response?.error?.message || '无法读取这台 DSH 的全局默认模型。')
2014
+ const value = response.value?.namespaces?.find((entry) => entry.ns === 'agent-default-model')?.value
2015
+ return value && typeof value.provider === 'string' && typeof value.model === 'string'
2016
+ ? { provider: value.provider, model: value.model,
2017
+ ...(typeof value.reasoningEffort === 'string' ? { reasoningEffort: value.reasoningEffort } : {}) } : null
2018
+ }
2019
+ const writeDshDefaultModel = async (model) => {
2020
+ const response = await remote.settings.mutate('agent-default-model', [
2021
+ { op: 'set', path: ['provider'], value: modelProvider(model) },
2022
+ { op: 'set', path: ['model'], value: model.model_id },
2023
+ { op: 'unset', path: ['reasoningEffort'] },
2024
+ ], undefined)
2025
+ if (!response?.ok) throw new Error(response?.error?.message || 'DSH 无法保存全局默认模型。')
2026
+ const selection = { provider: modelProvider(model), model: model.model_id }
2027
+ setDshDefaultSelection(selection)
2028
+ setDefaultModelDraft(modelSelectionKey(model))
2029
+ return selection
2030
+ }
2031
+ const applyUserDefaultModel = async (model) => {
2032
+ if (!model || !authorizedModels.some((item) => modelSelectionKey(item) === modelSelectionKey(model))) {
2033
+ setDefaultModelMessage('请选择当前已获准的模型。')
2034
+ return
2035
+ }
2036
+ setDefaultModelBusy(true)
2037
+ try {
2038
+ const selection = await writeDshDefaultModel(model)
2039
+ const preference = readDefaultPreference(serviceUrl) || { initialized: true, serverDefault: '' }
2040
+ const isServerDefault = serverDefaultModel && modelSelectionKey(model) === modelSelectionKey(serverDefaultModel)
2041
+ if (isServerDefault) preference.serverDefault = modelSelectionKey(serverDefaultModel)
2042
+ preference.initialized = true
2043
+ saveDefaultPreference(serviceUrl, preference)
2044
+ setDshDefaultSelection(selection)
2045
+ setDefaultModelMessage(`已将 ${model.display_name || model.model_id} 设为 DSH 全局默认模型。`)
2046
+ } catch (error) {
2047
+ setDefaultModelMessage(error instanceof Error ? error.message : '保存本机默认模型失败。')
2048
+ } finally { setDefaultModelBusy(false) }
2049
+ }
2050
+ const applyAuthorizedModels = async (models, rawServiceUrl, rawDefaultModel = null) => {
1867
2051
  const normalized = normalizeServiceUrl(rawServiceUrl)
1868
2052
  if (!normalized || !Array.isArray(models)) throw new Error('ANT 返回的团队模型目录格式无效。')
1869
2053
  if (typeof remote?.settings?.mutate !== 'function') throw new Error('当前 DSH 版本未提供模型设置接口。')
1870
- const grouped = {
1871
- openai: models.filter((model) => model.protocol === 'openai'),
1872
- anthropic: models.filter((model) => model.protocol === 'anthropic'),
1873
- }
1874
- const ops = []
1875
- for (const protocol of ['openai', 'anthropic']) {
1876
- const route = protocol === 'openai' ? 'ant-team-openai' : 'ant-team-anthropic'
1877
- const protocolModels = grouped[protocol]
2054
+ const groups = [
2055
+ { route: 'ant-team-openai-chat-completions', protocol: 'openai', mode: 'chat_completions', api: 'openai-completions' },
2056
+ { route: 'ant-team-openai-responses', protocol: 'openai', mode: 'responses', api: 'openai-responses' },
2057
+ { route: 'ant-team-anthropic', protocol: 'anthropic', api: 'anthropic-messages' },
2058
+ ]
2059
+ const ops = [{ op: 'unset', path: ['providers', 'ant-team-openai'] }]
2060
+ for (const group of groups) {
2061
+ const protocolModels = models.filter((model) => model.protocol === group.protocol
2062
+ && (group.protocol !== 'openai' || (model.openai_api_mode || 'chat_completions') === group.mode))
2063
+ const route = group.route
1878
2064
  if (protocolModels.length === 0) {
1879
2065
  ops.push({ op: 'unset', path: ['providers', route] })
1880
2066
  continue
1881
2067
  }
1882
2068
  ops.push({ op: 'set', path: ['providers', route], value: {
1883
- displayName: protocol === 'openai' ? 'Lakala (OpenAI)' : 'Lakala (Anthropic)',
2069
+ displayName: group.protocol === 'anthropic' ? 'ANT 团队 · Anthropic' : `ANT 团队 · OpenAI · ${group.mode === 'responses' ? 'Responses' : 'Chat Completions'}`,
1884
2070
  apiKeyEnv: 'ANT_MODEL_API_KEY',
1885
- api: protocol === 'openai' ? 'openai-completions' : 'anthropic-messages',
1886
- baseURL: protocol === 'openai'
2071
+ api: group.api,
2072
+ baseURL: group.protocol === 'openai'
1887
2073
  ? new URL('/model-gateway/openai/v1', normalized).toString()
1888
2074
  : new URL('/model-gateway/anthropic', normalized).toString(),
1889
2075
  models: protocolModels.map((model) => ({ id: model.model_id, name: model.display_name })),
@@ -1891,9 +2077,56 @@ window.__ModuleLoader__.load({
1891
2077
  }
1892
2078
  const settingsWrite = await remote.settings.mutate('llm-pi-ai', ops, undefined)
1893
2079
  if (!settingsWrite.ok) throw new Error(settingsWrite.error?.message || 'DSH 无法更新团队模型设置。')
2080
+ const defaultMatch = rawDefaultModel && models.find((model) => model.protocol === rawDefaultModel.protocol
2081
+ && model.model_id === rawDefaultModel.model_id)
2082
+ const serverDefault = defaultMatch || null
2083
+ const initialDefault = defaultMatch || (rawDefaultModel == null ? models[0] : null)
2084
+ setServerDefaultModel(serverDefault)
2085
+ const preference = readDefaultPreference(normalized)
2086
+ let statusMessage = ''
2087
+ let selection
2088
+ if (!preference?.initialized && initialDefault) {
2089
+ const previousDefault = await readDshDefaultSelection()
2090
+ selection = await writeDshDefaultModel(initialDefault)
2091
+ saveDefaultPreference(normalized, { initialized: true, serverDefault: modelSelectionKey(serverDefault || initialDefault), previousDefault })
2092
+ statusMessage = serverDefault
2093
+ ? '已按团队默认模型设置 DSH 全局默认。该 Profile 的新会话会使用此模型;你之后可在此更改,刷新不会覆盖选择。'
2094
+ : '当前无法使用团队默认模型;已用首个获准模型初始化 DSH 全局默认。请管理员检查默认设置和成员授权。'
2095
+ } else {
2096
+ selection = await readDshDefaultSelection()
2097
+ const currentAuthorized = modelForProviderSelection(selection, models)
2098
+ const baseline = models.find((model) => modelSelectionKey(model) === preference?.serverDefault)
2099
+ const stillUsingBaseline = baseline && currentAuthorized && modelSelectionKey(currentAuthorized) === modelSelectionKey(baseline)
2100
+ if (currentAuthorized && selection?.provider !== modelProvider(currentAuthorized)) {
2101
+ selection = await writeDshDefaultModel(currentAuthorized)
2102
+ }
2103
+ if (serverDefault && stillUsingBaseline && modelSelectionKey(serverDefault) !== preference.serverDefault) {
2104
+ selection = await writeDshDefaultModel(serverDefault)
2105
+ saveDefaultPreference(normalized, { ...preference, initialized: true, serverDefault: modelSelectionKey(serverDefault) })
2106
+ statusMessage = '团队默认模型已更新;你尚未设置个人覆盖,因此已同步到 DSH 全局默认。'
2107
+ } else if (selection?.provider?.startsWith('ant-team-') && !currentAuthorized && models.length) {
2108
+ const fallback = serverDefault || models[0]
2109
+ selection = await writeDshDefaultModel(fallback)
2110
+ saveDefaultPreference(normalized, { ...preference, initialized: true,
2111
+ serverDefault: modelSelectionKey(serverDefault || fallback) })
2112
+ statusMessage = serverDefault
2113
+ ? '当前默认模型已失去授权,已恢复为团队默认模型。'
2114
+ : '当前默认模型已失去授权,已改用首个获准模型;请管理员设置团队默认模型。'
2115
+ }
2116
+ const effectiveAuthorized = modelForProviderSelection(selection, models)
2117
+ setDshDefaultSelection(selection)
2118
+ setDefaultModelDraft(effectiveAuthorized ? modelSelectionKey(effectiveAuthorized) : '')
2119
+ if (!serverDefault && !statusMessage) statusMessage = rawDefaultModel
2120
+ ? '团队默认模型不在这台设备的获准范围内;请联系管理员调整模型权限。'
2121
+ : '团队默认模型尚未设置;请联系管理员设置,或在下方选择本机可用的默认模型。'
2122
+ else if (!statusMessage) statusMessage = effectiveAuthorized
2123
+ ? '已保留你选择的 DSH 全局默认模型。'
2124
+ : '当前 DSH 默认不属于 ANT 获准模型;ANT 对话和任务会使用服务端默认模型。'
2125
+ }
2126
+ setDefaultModelMessage(statusMessage)
1894
2127
  try { localStorage.setItem(SERVICE_URL_STORAGE_KEY, normalized) } catch {}
1895
2128
  onServiceUrlChange(normalized)
1896
- return models.length
2129
+ return { count: models.length, defaultSelection: selection }
1897
2130
  }
1898
2131
 
1899
2132
  const refreshModels = async () => {
@@ -1911,13 +2144,13 @@ window.__ModuleLoader__.load({
1911
2144
  if (response.status === 401) setNeedsReconnect(true)
1912
2145
  throw new Error(result.message || `刷新失败(${response.status})`)
1913
2146
  }
1914
- const count = await applyAuthorizedModels(result.models, result.service_url)
2147
+ const sync = await applyAuthorizedModels(result.models, result.service_url, result.default_model)
1915
2148
  setAuthorizedModels(result.models)
1916
2149
  setLastModelSync(new Date())
1917
2150
  setCredentialConfigured(true)
1918
2151
  setNeedsReconnect(false)
1919
2152
  setReauthenticate(false)
1920
- setModelMessage(`模型目录已刷新,共 ${count} 个获准模型。`)
2153
+ setModelMessage(`模型目录已刷新,共 ${sync.count} 个获准模型。`)
1921
2154
  } catch (error) {
1922
2155
  setModelMessage(error instanceof Error ? error.message : '刷新模型失败,请稍后重试。')
1923
2156
  } finally {
@@ -2071,7 +2304,32 @@ window.__ModuleLoader__.load({
2071
2304
 
2072
2305
  const clearLocalConnection = async () => {
2073
2306
  if (typeof remote?.settings?.mutate !== 'function') throw new Error('DSH 未提供模型设置接口。')
2307
+ const selection = await readDshDefaultSelection()
2308
+ if (selection?.provider?.startsWith('ant-team-')) {
2309
+ const previous = readDefaultPreference(serviceUrl)?.previousDefault
2310
+ const restore = previous && typeof previous.provider === 'string' && typeof previous.model === 'string'
2311
+ && !previous.provider.startsWith('ant-team-')
2312
+ const operations = restore
2313
+ ? [
2314
+ { op: 'set', path: ['provider'], value: previous.provider },
2315
+ { op: 'set', path: ['model'], value: previous.model },
2316
+ previous.reasoningEffort
2317
+ ? { op: 'set', path: ['reasoningEffort'], value: previous.reasoningEffort }
2318
+ : { op: 'unset', path: ['reasoningEffort'] },
2319
+ ]
2320
+ : [
2321
+ { op: 'unset', path: ['provider'] },
2322
+ { op: 'unset', path: ['model'] },
2323
+ { op: 'unset', path: ['reasoningEffort'] },
2324
+ ]
2325
+ const reset = await remote.settings.mutate('agent-default-model', [
2326
+ ...operations,
2327
+ ], undefined)
2328
+ if (!reset.ok) throw new Error(reset.error?.message || 'DSH 全局默认模型未能恢复。')
2329
+ }
2074
2330
  const settingsResult = await remote.settings.mutate('llm-pi-ai', [
2331
+ { op: 'unset', path: ['providers', 'ant-team-openai-chat-completions'] },
2332
+ { op: 'unset', path: ['providers', 'ant-team-openai-responses'] },
2075
2333
  { op: 'unset', path: ['providers', 'ant-team-openai'] },
2076
2334
  { op: 'unset', path: ['providers', 'ant-team-anthropic'] },
2077
2335
  ], undefined)
@@ -2080,6 +2338,8 @@ window.__ModuleLoader__.load({
2080
2338
  const credentialResult = await remote.credentials.unset('ANT_MODEL_API_KEY')
2081
2339
  if (!credentialResult.ok) throw new Error(credentialResult.error?.message || 'ANT 成员凭证未能清除。')
2082
2340
  setCredentialConfigured(false)
2341
+ setAuthorizedModels([]); setServerDefaultModel(null); setDshDefaultSelection(null); setDefaultModelDraft('')
2342
+ try { localStorage.removeItem(`${DEFAULT_MODEL_INITIALIZED_KEY}:${normalizeServiceUrl(serviceUrl)}:${savedDeviceId() || ''}`) } catch {}
2083
2343
  onConnectionChange?.(null)
2084
2344
  setNeedsReconnect(false)
2085
2345
  setReauthenticate(false)
@@ -2259,7 +2519,7 @@ window.__ModuleLoader__.load({
2259
2519
  const credentialWrite = await remote.credentials.set('ANT_MODEL_API_KEY', result.token)
2260
2520
  if (!credentialWrite.ok) throw new Error(credentialWrite.error?.message || 'DSH 无法安全保存 ANT 凭证。')
2261
2521
  credentialSaved = true
2262
- await applyAuthorizedModels(result.models, normalized)
2522
+ await applyAuthorizedModels(result.models, normalized, result.default_model)
2263
2523
  setAuthorizedModels(result.models)
2264
2524
  setLastModelSync(new Date())
2265
2525
  setCode('')
@@ -2290,13 +2550,14 @@ window.__ModuleLoader__.load({
2290
2550
  const canCheckUpdate = bindingReady && credentialConfigured === true && !needsReconnect
2291
2551
  const updateGuideUrl = pinnedServiceUrl
2292
2552
  ? new URL('/ant-update', pinnedServiceUrl).href : null
2553
+ const currentDshDefaultModel = modelForProviderSelection(dshDefaultSelection, authorizedModels)
2293
2554
  const modelRows = authorizedModels.map((model) => h('div', { key: `${model.protocol}:${model.model_id}`, role: 'listitem',
2294
2555
  style: { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) minmax(120px, auto) minmax(130px, auto)',
2295
2556
  gap: 12, alignItems: 'center', padding: '10px 2px', borderBottom: '1px solid var(--dsw-alias-border-l2)' } },
2296
2557
  h('strong', { style: { minWidth: 0, overflowWrap: 'anywhere' } }, model.display_name || model.model_id),
2297
2558
  h('span', { style: { display: 'inline-flex', justifySelf: 'start', padding: '3px 8px', borderRadius: 999,
2298
2559
  backgroundColor: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-secondary)', fontSize: 12 } },
2299
- `${model.protocol === 'anthropic' ? 'Anthropic' : 'OpenAI'} 协议`),
2560
+ model.protocol === 'anthropic' ? 'Anthropic Messages' : `OpenAI · ${(model.openai_api_mode || 'chat_completions') === 'responses' ? 'Responses' : 'Chat Completions'}`),
2300
2561
  h('span', { style: { ...mutedTextStyle, textAlign: 'right' } }, '已同步到 DSH')))
2301
2562
  const connectionPanel = h('section', { style: { ...settingsPanelStyle, display: activeTab === 'connection' ? 'grid' : 'none' } },
2302
2563
  h('div', null,
@@ -2360,6 +2621,29 @@ window.__ModuleLoader__.load({
2360
2621
  backgroundColor: credentialConfigured === true && bindingReady ? 'var(--dsw-alias-state-success-tertiary)' : 'var(--dsw-alias-bg-layer-2)',
2361
2622
  fontSize: 12,
2362
2623
  } }, credentialConfigured === true ? bindingReady ? '已连接' : '待核对' : credentialConfigured === false ? '未连接' : '检查中')),
2624
+ h('section', { style: { ...infoNoticeStyle, gap: 8 } },
2625
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', gap: 10, flexWrap: 'wrap', alignItems: 'center' } },
2626
+ h('strong', null, 'ANT 默认模型'),
2627
+ h('span', { style: mutedTextStyle }, serverDefaultModel
2628
+ ? `团队默认:${serverDefaultModel.display_name || serverDefaultModel.model_id}`
2629
+ : '团队默认尚未设置,或当前成员无权使用')),
2630
+ h('p', { style: mutedTextStyle }, '首次同步时,ANT 会用团队默认初始化 DSH 全局默认模型。它会影响该 Profile 的新会话以及 ANT 对话和任务;之后刷新保留你的个人覆盖。'),
2631
+ h('label', { style: { display: 'grid', gap: 5, maxWidth: 720 } }, 'DSH 全局默认模型',
2632
+ h('select', { 'aria-label': 'DSH 全局默认模型', value: defaultModelDraft,
2633
+ disabled: credentialConfigured !== true || !authorizedModels.length || defaultModelBusy || busy,
2634
+ onChange: (event) => setDefaultModelDraft(event.target.value), style: inputStyle },
2635
+ h('option', { value: '' }, '选择获准模型'),
2636
+ ...authorizedModels.map((model) => h('option', { key: modelSelectionKey(model), value: modelSelectionKey(model) },
2637
+ `${model.display_name || model.model_id} · ${model.protocol === 'anthropic' ? 'Anthropic' : (model.openai_api_mode || 'chat_completions') === 'responses' ? 'OpenAI Responses' : 'OpenAI Chat Completions'}`)))),
2638
+ h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' } },
2639
+ h('button', { type: 'button', disabled: defaultModelBusy || !defaultModelDraft,
2640
+ onClick: () => { const chosen = authorizedModels.find((model) => modelSelectionKey(model) === defaultModelDraft); void applyUserDefaultModel(chosen) },
2641
+ style: primaryButtonStyle }, defaultModelBusy ? '保存中…' : '保存为 DSH 全局默认'),
2642
+ h('button', { type: 'button', disabled: defaultModelBusy || !serverDefaultModel
2643
+ || modelSelectionKey(currentDshDefaultModel || {}) === modelSelectionKey(serverDefaultModel),
2644
+ onClick: () => void applyUserDefaultModel(serverDefaultModel), style: secondaryButtonStyle }, '恢复团队默认'),
2645
+ currentDshDefaultModel ? h('span', { style: mutedTextStyle }, `当前 DSH 默认:${currentDshDefaultModel.display_name || currentDshDefaultModel.model_id}`) : null),
2646
+ defaultModelMessage ? h('p', { role: 'status', style: mutedTextStyle }, defaultModelMessage) : null),
2363
2647
  h('div', { role: 'list', 'aria-label': '获准模型', style: { display: 'grid', gap: 0, borderTop: '1px solid var(--dsw-alias-border-l2)' } },
2364
2648
  modelRows.length ? modelRows : h('div', { style: { display: 'grid', gap: 8, padding: '12px 0' } },
2365
2649
  h('p', { style: mutedTextStyle }, credentialConfigured === true && modelMessage.startsWith('正在') ? modelMessage
@@ -2368,7 +2652,7 @@ window.__ModuleLoader__.load({
2368
2652
  credentialConfigured !== true ? h('button', { type: 'button', onClick: () => onSelectTab('connection'),
2369
2653
  style: secondaryButtonStyle }, '前往团队连接') : null)),
2370
2654
  h('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 10 } },
2371
- h('button', { type: 'button', disabled: busy || !bindingReady || credentialConfigured !== true,
2655
+ h('button', { type: 'button', disabled: busy || defaultModelBusy || !bindingReady || credentialConfigured !== true,
2372
2656
  onClick: () => void refreshModels(), style: primaryButtonStyle }, busy ? '刷新中…' : '刷新团队模型'),
2373
2657
  lastModelSync ? h('span', { style: mutedTextStyle }, `最近同步:${lastModelSync.toLocaleString('zh-CN')}`) : null),
2374
2658
  modelMessage && !modelMessage.startsWith('正在') ? h('p', { role: 'status', style: mutedTextStyle }, modelMessage) : null)