@ai-wendao/ant 0.4.15 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/client.js +236 -81
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -4,7 +4,7 @@ ANT (AI-native Teams) connects a DeepSeek Harness Desktop profile to its team's
4
4
 
5
5
  ## 团队模型默认值
6
6
 
7
- 团队管理员在 ANT 控制台设置团队默认模型,并授权成员可用模型。成员第一次在 ANT 插件中同步模型时,团队默认会写入 DSH 的全局默认模型;成员之后可在“团队模型”页选另一项已获准模型。普通同步保留成员选择;若模型被撤销,插件会回到仍获准的团队默认。该默认写入 DSH profile,因此也影响同一 profile 新建的普通 DSH 会话。ANT 对话和任务会额外校验当前用户的获准模型;Codex 与 Claude Code 使用各自执行器的默认模型。
7
+ 团队管理员在 ANT 控制台设置团队默认模型,并授权成员可用模型。成员第一次在 ANT 插件中同步模型时,团队默认会写入 DSH 的全局默认模型;成员之后可在“团队模型”页选另一项已获准模型。成员也可按 provider 选择同步到本机 DSH 的模型路由;首次升级保留已有路由,关闭某项并刷新后会从本机 Profile 移除,保存了该路由的历史会话可能暂时无法调用,重新启用并刷新可恢复。普通同步保留成员默认选择;若模型被撤销,插件会回到仍获准的团队默认。该默认写入 DSH profile,因此也影响同一 profile 新建的普通 DSH 会话。ANT 对话和任务会额外校验当前用户的获准模型;Codex 与 Claude Code 使用各自执行器的默认模型。
8
8
 
9
9
  每个 OpenAI 兼容模型可配置 Chat Completions 或 Responses API。旧模型记录在升级后按 Chat Completions 解释;ANT 服务端继续使用原配置的 `/chat/completions` URL,并从相同 API 根路径派生 `/responses` 上游路径。
10
10
 
@@ -26,7 +26,7 @@ PR 快照任务使用已核验的 PR Head SHA 创建独立工作树。Codex 使
26
26
 
27
27
  ## 企业精选技能
28
28
 
29
- 团队管理员可在 ANT 管理台从 GitHub 目录、ZIP、TAR、TAR.GZ/TGZ 或单份 `SKILL.md` 导入技能,核对来源、文件和依赖后发布。已连接团队的成员从 DSH 左侧“企业扩展”入口进入统一目录,默认同时浏览技能和插件,并可按类型、状态筛选或搜索;ANT 插件设置页只处理连接、模型和更新。ANT Host 将固定版本的完整文件集放进 DSH 的用户技能目录,并重新核对本机文件;启用本地技能的 DSH 会话随后由原生技能提供者发现和使用。ANT 不用 Host 全局技能列表判断会话是否已发现技能。
29
+ 团队管理员可在 ANT 管理台从 GitHub 目录、ZIP、TAR、TAR.GZ/TGZ 或单份 `SKILL.md` 导入技能,核对来源、文件和依赖后发布,并为企业技能和插件指定主要用途类别。成员从 DSH 左侧“企业扩展”进入目录,可组合筛选办公文档、编程开发、设计与多媒体、数据分析、协作沟通、写作与营销、自动化与效率、运维与安全或其他类别,同时按来源范围、技能/插件类型和安装状态筛选或搜索。类别只用于发现内容;“本企业”“全市场精选”和“DSH 官方”是独立来源范围。ANT 插件设置页只处理连接、模型和更新。ANT Host 将固定版本的完整文件集放进 DSH 的用户技能目录,并重新核对本机文件;启用本地技能的 DSH 会话随后由原生技能提供者发现和使用。ANT 不用 Host 全局技能列表判断会话是否已发现技能。
30
30
 
31
31
  安装不会自动运行技能脚本或安装 Python、Node 等系统依赖。ANT 只更新或卸载自己管理且本机内容未被修改的技能目录;发现同名个人技能或本机改动时会停止覆盖。
32
32
 
package/client.js CHANGED
@@ -13,6 +13,7 @@ window.__ModuleLoader__.load({
13
13
  }
14
14
  const SERVICE_URL_STORAGE_KEY = 'ant-team-service-url'
15
15
  const PROVIDER_ROUTES_STORAGE_KEY = 'ant-team-provider-routes'
16
+ const PROVIDER_SYNC_EXCLUSIONS_KEY = 'ant-team-provider-sync-exclusions'
16
17
  const DEVICE_ID_STORAGE_KEY = 'ant-team-device-id'
17
18
  const DEFAULT_MODEL_INITIALIZED_KEY = 'ant-team-default-model-initialized'
18
19
  const ANT_BUNDLE_PACKAGE_NAME = '@ai-wendao/ant'
@@ -31,32 +32,41 @@ window.__ModuleLoader__.load({
31
32
  } catch { return 'connection' }
32
33
  }
33
34
  const fileCountFormat = new Intl.NumberFormat('zh-CN')
34
- const marketSkillCategories = {
35
- 'ant-ppt-master': '演示', 'ant-frontend-slides': '演示', 'ant-slidev': '演示',
36
- 'ant-word-docx': '文档', 'ant-excel-xlsx': '表格', 'ant-pdf': 'PDF',
37
- 'ant-internal-comms': '写作', 'ant-formatted-report-writer': '写作',
38
- 'ant-agent-browser': '开发', 'ant-vercel-react-best-practices': '开发',
39
- 'ant-vercel-composition-patterns': '开发', 'ant-supabase-postgres-best-practices': '开发',
40
- 'ant-diagnosing-bugs': '开发', 'ant-improve-codebase-architecture': '开发',
41
- 'ant-code-review': '开发', 'ant-systematic-debugging': '开发',
42
- 'ant-test-driven-development': '开发', 'ant-verification-before-completion': '开发',
43
- 'ant-writing-plans': '开发', 'ant-to-spec': '开发', 'ant-skill-creator': '开发',
44
- 'ant-frontend-design': '设计', 'ant-web-design-guidelines': '设计',
45
- 'ant-ui-ux-pro-max': '设计', 'ant-emil-design-eng': '设计',
46
- 'ant-review-animations': '设计', 'ant-shadcn': '设计',
47
- 'ant-theme-factory': '设计', 'ant-brand-guidelines': '设计',
48
- 'ant-copywriting': '写作', 'ant-seo-audit': '营销', 'ant-brainstorming': '策划',
35
+ const extensionCategories = [
36
+ ['office', '办公文档'], ['development', '编程开发'], ['design', '设计与多媒体'],
37
+ ['data', '数据分析'], ['collaboration', '协作沟通'], ['content', '写作与营销'],
38
+ ['automation', '自动化与效率'], ['operations', '运维与安全'], ['other', '其他'],
39
+ ]
40
+ const extensionCategoryLabels = Object.fromEntries(extensionCategories)
41
+ const legacyMarketSkillCategories = {
42
+ 'ant-ppt-master': 'office', 'ant-frontend-slides': 'design', 'ant-slidev': 'development',
43
+ 'ant-word-docx': 'office', 'ant-excel-xlsx': 'office', 'ant-pdf': 'office',
44
+ 'ant-internal-comms': 'collaboration', 'ant-formatted-report-writer': 'office',
45
+ 'ant-agent-browser': 'automation', 'ant-vercel-react-best-practices': 'development',
46
+ 'ant-vercel-composition-patterns': 'development', 'ant-supabase-postgres-best-practices': 'data',
47
+ 'ant-diagnosing-bugs': 'development', 'ant-improve-codebase-architecture': 'development',
48
+ 'ant-code-review': 'development', 'ant-systematic-debugging': 'development',
49
+ 'ant-test-driven-development': 'development', 'ant-verification-before-completion': 'development',
50
+ 'ant-writing-plans': 'development', 'ant-to-spec': 'development', 'ant-skill-creator': 'automation',
51
+ 'ant-frontend-design': 'design', 'ant-web-design-guidelines': 'design',
52
+ 'ant-ui-ux-pro-max': 'design', 'ant-emil-design-eng': 'design',
53
+ 'ant-review-animations': 'design', 'ant-shadcn': 'design',
54
+ 'ant-theme-factory': 'design', 'ant-brand-guidelines': 'design',
55
+ 'ant-copywriting': 'content', 'ant-seo-audit': 'content', 'ant-brainstorming': 'content',
49
56
  }
57
+ const categoryOf = (entry) => extensionCategoryLabels[entry?.category]
58
+ ? entry.category : legacyMarketSkillCategories[entry?.dsh_name] || 'other'
59
+ const categoryLabel = (category) => extensionCategoryLabels[category] || extensionCategoryLabels.other
50
60
  const officialOfficeGuide = 'https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.1-rc.2/packages/skill/skill-office/README.zh.md'
51
61
  const officialOfficeEnglish = (skill) => `https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v${skill.since}/packages/skill/skill-office/assets/${skill.name}/SKILL.md`
52
62
  const officialOfficeSkills = [
53
- { name: 'office', title: 'Office 办公入口', category: '办公', since: '0.2.1-rc.2', description: '根据任务选择 Word、PPT、Excel 或文件处理能力。', prompt: '帮我完成这份办公文件' },
54
- { name: 'office-docx', title: 'Word 文档', category: '文档', since: '0.2.0-rc.2', description: '创建、修改和检查 DOCX 文档。', prompt: '帮我写一份 Word 报告' },
55
- { name: 'office-pptx', title: 'PPT 演示', category: '演示', since: '0.2.0-rc.2', description: '创建、修改和检查 PPTX 演示文稿。', prompt: '帮我做一份项目汇报 PPT' },
56
- { name: 'office-xlsx', title: 'Excel 表格', category: '表格', since: '0.2.0-rc.2', description: '创建、修改和检查 XLSX 工作簿。', prompt: '帮我整理这份 Excel 表格' },
57
- { name: 'office-render', title: '文件预览', category: '办公', since: '0.2.1-rc.2', description: '把 Office 文件渲染成图片,检查实际外观。', prompt: '请预览这份文档并检查排版' },
58
- { name: 'office-recalculate', title: '公式重算', category: '表格', since: '0.2.1-rc.2', description: '重新计算表格公式并核对结果。', prompt: '请重新计算这份表格的公式' },
59
- { name: 'office-export-pdf', title: '导出 PDF', category: 'PDF', since: '0.2.1-rc.2', description: '将 Office 文件导出为 PDF。', prompt: '把这份 Word 文档导出成 PDF' },
63
+ { name: 'office', title: 'Office 办公入口', category: 'office', since: '0.2.1-rc.2', description: '根据任务选择 Word、PPT、Excel 或文件处理能力。', prompt: '帮我完成这份办公文件' },
64
+ { name: 'office-docx', title: 'Word 文档', category: 'office', since: '0.2.0-rc.2', description: '创建、修改和检查 DOCX 文档。', prompt: '帮我写一份 Word 报告' },
65
+ { name: 'office-pptx', title: 'PPT 演示', category: 'office', since: '0.2.0-rc.2', description: '创建、修改和检查 PPTX 演示文稿。', prompt: '帮我做一份项目汇报 PPT' },
66
+ { name: 'office-xlsx', title: 'Excel 表格', category: 'office', since: '0.2.0-rc.2', description: '创建、修改和检查 XLSX 工作簿。', prompt: '帮我整理这份 Excel 表格' },
67
+ { name: 'office-render', title: '文件预览', category: 'office', since: '0.2.1-rc.2', description: '把 Office 文件渲染成图片,检查实际外观。', prompt: '请预览这份文档并检查排版' },
68
+ { name: 'office-recalculate', title: '公式重算', category: 'office', since: '0.2.1-rc.2', description: '重新计算表格公式并核对结果。', prompt: '请重新计算这份表格的公式' },
69
+ { name: 'office-export-pdf', title: '导出 PDF', category: 'office', since: '0.2.1-rc.2', description: '将 Office 文件导出为 PDF。', prompt: '把这份 Word 文档导出成 PDF' },
60
70
  ]
61
71
  const curatedSkillIntroductions = {
62
72
  "ant-ppt-master": {"ref": "c350bc31e6d43b7bcf5234183d650ba8f9f4c9d7", "description": "制作和编辑 PowerPoint 演示文稿。"},
@@ -115,7 +125,7 @@ window.__ModuleLoader__.load({
115
125
  }
116
126
  return null
117
127
  }
118
- const skillCategory = (skill) => marketSkillCategories[skill.dsh_name] || '其他'
128
+ const skillCategory = (skill) => categoryOf(skill)
119
129
  const panelStyle = {
120
130
  display: 'grid', alignContent: 'start', gap: 14, padding: 18,
121
131
  border: '0.5px solid var(--dsw-alias-border-l2)', borderRadius: 14,
@@ -128,6 +138,9 @@ window.__ModuleLoader__.load({
128
138
  borderLeft: '3px solid var(--dsw-alias-state-business-primary)',
129
139
  }
130
140
  const mutedTextStyle = { margin: 0, color: 'var(--dsw-alias-label-secondary)', fontSize: 13, lineHeight: 1.6 }
141
+ const feedbackNoticeStyle = { ...mutedTextStyle, position: 'sticky', top: 0, zIndex: 8,
142
+ padding: '9px 12px', border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8,
143
+ backgroundColor: 'var(--dsw-alias-bg-base)', boxShadow: '0 4px 14px #173d3510', overflowWrap: 'anywhere' }
131
144
  const sectionTitleStyle = { margin: 0, fontSize: 16, fontWeight: 650, lineHeight: 1.4 }
132
145
  const inputStyle = {
133
146
  width: '100%', boxSizing: 'border-box', padding: '10px 12px',
@@ -393,9 +406,8 @@ window.__ModuleLoader__.load({
393
406
  binding.state === 'checking' ? h('p', { role: 'status', style: mutedTextStyle }, '正在核对这台 DSH 绑定的团队…') : null,
394
407
  binding.state === 'unverified' ? h('p', { role: 'status', style: mutedTextStyle }, '本机仍有 ANT 凭证,但无法核对所属团队。请联系原团队管理员核查并撤销旧设备;确认后可在下方断开或仅清理本机。') : null,
395
408
  binding.state === 'error' ? h('p', { role: 'status', style: mutedTextStyle }, '暂时无法核对这台 DSH 的团队绑定,请重新打开 ANT 或联系管理员。') : null,
396
- h('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 12 } },
397
- message ? h('p', { role: 'status', style: mutedTextStyle }, message) : null,
398
- link ? h('a', { href: link, target: '_blank', rel: 'noopener noreferrer', style: { color: 'var(--dsw-alias-link)', fontSize: 13 } }, '打开团队管理 ↗') : null))
409
+ message ? h('p', { role: 'status', style: feedbackNoticeStyle }, message) : null,
410
+ link ? h('a', { href: link, target: '_blank', rel: 'noopener noreferrer', style: { color: 'var(--dsw-alias-link)', fontSize: 13 } }, '打开团队管理 ↗') : null)
399
411
  return h('section', { style: { display: 'grid', gap: 16, width: '100%', maxWidth: 1120 } },
400
412
  h('div', { style: { display: 'grid', gap: 4 } },
401
413
  h('h2', { style: { margin: 0, fontSize: 20, fontWeight: 650, lineHeight: 1.35 } }, '团队与协作'),
@@ -1220,7 +1232,7 @@ window.__ModuleLoader__.load({
1220
1232
  return h('section', { style: { ...settingsPanelStyle,
1221
1233
  display: ['collaboration', 'local'].includes(activeTab) ? 'grid' : 'none' },
1222
1234
  'aria-label': '群聊协作设置' },
1223
- message ? h('p', { role: 'status', style: mutedTextStyle }, message) : null,
1235
+ message ? h('p', { role: 'status', style: feedbackNoticeStyle }, message) : null,
1224
1236
  h('div', { id: 'ant-chat-entry', role: 'region', 'aria-label': '聊天入口和机器人状态', tabIndex: -1,
1225
1237
  style: { display: activeTab === 'collaboration' ? 'grid' : 'none', gap: 12 } },
1226
1238
  h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'start', gap: 12, flexWrap: 'wrap' } },
@@ -1588,8 +1600,6 @@ window.__ModuleLoader__.load({
1588
1600
  && (scopeFilter === 'all' || scopeFilter === 'official')
1589
1601
  && (categoryFilter === 'all' || skill.category === categoryFilter)
1590
1602
  })
1591
- const categories = [...new Set([...skills.map(skillCategory), ...officialOfficeSkills.map((skill) => skill.category)])]
1592
- .sort((a, b) => a.localeCompare(b, 'zh-CN'))
1593
1603
  const statusLabel = { installed: '已放入本机目录', 'update-available': '可更新', 'not-installed': '未安装', conflict: '本机冲突' }
1594
1604
  if (originalView) return h('section', { 'aria-label': '技能原文', style: {
1595
1605
  height: '100%', overflowY: 'auto', boxSizing: 'border-box',
@@ -1638,19 +1648,20 @@ window.__ModuleLoader__.load({
1638
1648
  h('button', { key: value, type: 'button', onClick: () => setFilter(value), 'aria-pressed': filter === value,
1639
1649
  style: { ...secondaryButtonStyle, minHeight: 34, padding: '6px 12px', backgroundColor: filter === value ? 'var(--dsw-alias-bg-layer-2)' : secondaryButtonStyle.backgroundColor } }, label)),
1640
1650
  includePlugins ? null : h('span', { style: { ...mutedTextStyle, marginLeft: 'auto' } }, `显示 ${visible.length + visibleOfficial.length} / ${skills.length + officialOfficeSkills.length} 个技能`)),
1641
- includePlugins ? null : h('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 } },
1651
+ h('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 } },
1642
1652
  h('span', { style: mutedTextStyle }, '来源'),
1643
1653
  ...[['all', '全部'], ['official', 'DSH 官方'], ['market', '全市场精选'], ['team', '本企业']].map(([value, label]) =>
1644
1654
  h('button', { key: value, type: 'button', onClick: () => setScopeFilter(value), 'aria-pressed': scopeFilter === value,
1645
1655
  style: { ...categoryButtonStyle, backgroundColor: scopeFilter === value ? 'var(--dsw-alias-bg-layer-2)' : categoryButtonStyle.backgroundColor } }, label)),
1646
- h('label', { style: { ...mutedTextStyle, marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 6 } }, '用途',
1647
- h('select', { value: categoryFilter, onChange: (event) => setCategoryFilter(event.target.value), style: { ...inputStyle, width: 'auto', minHeight: 34 } },
1656
+ h('label', { style: { ...mutedTextStyle, marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 6 } }, '用途类别',
1657
+ h('select', { value: categoryFilter, onChange: (event) => setCategoryFilter(event.target.value),
1658
+ 'aria-label': '筛选扩展用途类别', style: { ...inputStyle, width: 'auto', minHeight: 34 } },
1648
1659
  h('option', { value: 'all' }, '全部用途'),
1649
- ...categories.map((category) => h('option', { key: category, value: category }, category))))),
1650
- ),
1660
+ ...extensionCategories.map(([category, label]) => h('option', { key: category, value: category }, label)))),
1661
+ )),
1651
1662
  includePlugins ? h('h2', { style: { margin: 0, fontSize: 20 } }, loading ? '技能 · 读取中…' : `技能 · ${visible.length + visibleOfficial.length}`) : null,
1652
1663
  message && (!includePlugins || loadError || skills.length || (!loading && !query.trim()))
1653
- ? h('p', { role: loadError ? 'alert' : 'status', style: mutedTextStyle }, message) : null,
1664
+ ? h('p', { role: loadError ? 'alert' : 'status', style: feedbackNoticeStyle }, message) : null,
1654
1665
  visible.length && !includePlugins ? h('h2', { style: { margin: 0, fontSize: 20 } }, '全市场与本企业技能') : null,
1655
1666
  visible.length ? h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 420px), 1fr))', gap: 16 } },
1656
1667
  ...visible.map((skill) => {
@@ -1677,7 +1688,7 @@ window.__ModuleLoader__.load({
1677
1688
  h('div', { style: { display: 'flex', alignItems: 'baseline', flexWrap: 'wrap', gap: 8 } },
1678
1689
  h('h2', { style: { margin: 0, fontSize: 19 } }, skill.title),
1679
1690
  h('span', { style: { ...mutedTextStyle, padding: '2px 7px', borderRadius: 6, backgroundColor: 'var(--dsw-alias-bg-layer-2)' } }, skill.scope === 'market' ? '全市场精选' : '本企业'),
1680
- h('span', { style: mutedTextStyle }, skillCategory(skill))),
1691
+ h('span', { style: mutedTextStyle }, categoryLabel(skillCategory(skill)))),
1681
1692
  h('span', { style: mutedTextStyle }, marketPending
1682
1693
  ? skill.team_connected ? '待企业启用' : '连接企业后查看启用状态'
1683
1694
  : statusLabel[skill.local_status] || '状态未知')),
@@ -1742,20 +1753,24 @@ window.__ModuleLoader__.load({
1742
1753
  h('div', { style: { display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', alignItems: 'baseline', gap: 8 } },
1743
1754
  h('h3', { style: { margin: 0, fontSize: 17 } }, skill.title),
1744
1755
  h('span', { style: { ...mutedTextStyle, padding: '2px 8px', borderRadius: 6,
1745
- backgroundColor: 'var(--dsw-alias-bg-layer-2)' } }, 'DSH 自带 · 无需在 ANT 安装')),
1756
+ backgroundColor: 'var(--dsw-alias-bg-layer-2)' } }, 'DSH 自带 · 无需在 ANT 安装'),
1757
+ h('span', { style: { ...mutedTextStyle, padding: '2px 7px', borderRadius: 6,
1758
+ backgroundColor: 'var(--dsw-alias-bg-layer-2)' } }, categoryLabel(skill.category))),
1746
1759
  h('p', { style: mutedTextStyle }, skill.description),
1747
1760
  h('p', { style: mutedTextStyle }, `在新会话中说:“${skill.prompt}”。也可输入 /${skill.name}。`),
1748
1761
  h('p', { style: mutedTextStyle }, `DSH ${skill.since} 起提供;如果无法使用,请在 DSH 检查更新。`),
1749
1762
  h('a', { href: officialOfficeEnglish(skill), target: '_blank', rel: 'noopener noreferrer',
1750
1763
  style: { color: 'var(--dsw-alias-link)', fontSize: 13, lineHeight: 1.6, padding: '4px 0' } }, '英文原文 ↗'))))) : null,
1751
1764
  includePlugins ? h(PluginCenterPage, { remote, onBack, onAll, onSkills, openBundle,
1752
- embedded: true, query, onQueryChange, externalFilter: filter,
1765
+ embedded: true, query, onQueryChange, externalFilter: filter, externalCategoryFilter: categoryFilter,
1766
+ externalScopeFilter: scopeFilter,
1753
1767
  hasSkillMatches: visible.length + visibleOfficial.length > 0, skillCatalogReady: !loading && !loadError,
1754
1768
  refreshToken }) : null))
1755
1769
  }
1756
1770
 
1757
1771
  function PluginCenterPage({ remote, onBack, onAll, onSkills, openBundle,
1758
1772
  embedded = false, query, onQueryChange, externalFilter = 'all',
1773
+ externalCategoryFilter = 'all', externalScopeFilter = 'all',
1759
1774
  hasSkillMatches = false, skillCatalogReady = false, refreshToken }) {
1760
1775
  const [plugins, setPlugins] = useState([])
1761
1776
  const [bundles, setBundles] = useState([])
@@ -1766,6 +1781,7 @@ window.__ModuleLoader__.load({
1766
1781
  const [completedId, setCompletedId] = useState('')
1767
1782
  const [message, setMessage] = useState('')
1768
1783
  const [filter, setFilter] = useState('all')
1784
+ const [categoryFilter, setCategoryFilter] = useState('all')
1769
1785
  const refreshGeneration = useRef(0)
1770
1786
  const refresh = async () => {
1771
1787
  const generation = ++refreshGeneration.current
@@ -1889,21 +1905,46 @@ window.__ModuleLoader__.load({
1889
1905
  } finally { setWorkingId('') }
1890
1906
  }
1891
1907
  const activeFilter = embedded ? externalFilter : filter
1908
+ const activeCategoryFilter = embedded ? externalCategoryFilter : categoryFilter
1909
+ const activeScopeFilter = embedded ? externalScopeFilter : 'team'
1910
+ const currentPluginByPackage = new Map()
1911
+ const publishedPluginPackages = new Set()
1912
+ for (const plugin of plugins) {
1913
+ if (!currentPluginByPackage.has(plugin.package_name)
1914
+ || plugin.status === 'published' && !publishedPluginPackages.has(plugin.package_name)) {
1915
+ currentPluginByPackage.set(plugin.package_name, plugin.id)
1916
+ }
1917
+ if (plugin.status === 'published') publishedPluginPackages.add(plugin.package_name)
1918
+ }
1919
+ if (activeFilter === 'installed') {
1920
+ for (const plugin of plugins) {
1921
+ if (publishedPluginPackages.has(plugin.package_name)) continue
1922
+ const installed = bundles.find((bundle) => bundle.name === plugin.package_name && bundle.installed)
1923
+ if (!installed?.version || installed.version !== plugin.version) continue
1924
+ currentPluginByPackage.set(plugin.package_name, plugin.id)
1925
+ }
1926
+ }
1892
1927
  const visible = plugins.filter((plugin) => {
1928
+ const isCurrentVersion = currentPluginByPackage.get(plugin.package_name) === plugin.id
1893
1929
  const installed = bundles.find((bundle) => bundle.name === plugin.package_name && bundle.installed)
1894
1930
  const artifactUrl = approvedPluginArtifactUrl(plugin)
1895
1931
  const installSpec = pluginInstallSpec(plugin)
1896
1932
  const localAhead = installed?.version && newerVersion(installed.version, plugin.version)
1897
1933
  const matches = !query.trim() || `${plugin.title} ${plugin.description} ${plugin.package_name}`.toLowerCase().includes(query.trim().toLowerCase())
1898
- return matches && (activeFilter === 'all' || activeFilter === 'installed' && Boolean(installed)
1899
- || activeFilter === 'available' && installSpec && !localAhead && (!installed || (installed.version && installed.version !== plugin.version))
1900
- || activeFilter === 'updates' && installSpec && installed && installed.version && installed.version !== plugin.version && !localAhead
1901
- || activeFilter === 'retired' && plugin.status !== 'published')
1934
+ return matches && (activeScopeFilter === 'all' || activeScopeFilter === 'team')
1935
+ && (activeCategoryFilter === 'all' || categoryOf(plugin) === activeCategoryFilter)
1936
+ && (activeFilter === 'retired'
1937
+ ? plugin.status !== 'published'
1938
+ : isCurrentVersion && (activeFilter === 'all'
1939
+ || activeFilter === 'installed' && Boolean(installed)
1940
+ || activeFilter === 'available' && plugin.status === 'published' && installSpec && !localAhead && (!installed || (installed.version && installed.version !== plugin.version))
1941
+ || activeFilter === 'updates' && plugin.status === 'published' && installSpec && installed && installed.version && installed.version !== plugin.version && !localAhead)
1942
+ )
1902
1943
  })
1903
1944
  const noMatches = embedded
1904
1945
  ? skillCatalogReady ? '没有找到符合条件的企业扩展。' : '插件中没有符合条件的结果。'
1905
1946
  : activeFilter === 'available'
1906
- ? '当前没有可一键安装的插件。可在“全部”查看审核目录,或联系企业管理员。'
1947
+ ? '当前没有可一键安装的插件。可在“目录概览”查看审核目录,或联系企业管理员。'
1907
1948
  : '没有符合筛选条件的插件。'
1908
1949
  return h('section', { 'aria-label': '企业精选插件', style: embedded
1909
1950
  ? { display: 'grid', gap: 16 }
@@ -1926,15 +1967,20 @@ window.__ModuleLoader__.load({
1926
1967
  : h('section', { role: 'status', style: panelStyle },
1927
1968
  h('h2', { style: { margin: 0, fontSize: 18 } }, '团队暂未发布精选插件'),
1928
1969
  h('p', { style: mutedTextStyle }, '需要某个插件?请联系企业管理员,说明用途或提供插件地址。审核发布后会显示在这里。')) : null,
1929
- !loading && loadError ? h('p', { role: 'alert', style: mutedTextStyle }, message) : null,
1970
+ !loading && loadError ? h('p', { role: 'alert', style: feedbackNoticeStyle }, message) : null,
1930
1971
  plugins.length && !embedded ? h(ExtensionRequestNotice) : null,
1931
1972
  plugins.length && !embedded ? h('div', { style: panelStyle },
1932
1973
  h('input', { type: 'search', value: query, onChange: (event) => onQueryChange(event.target.value), placeholder: '搜索插件名称或用途', 'aria-label': '搜索企业精选插件', style: inputStyle }),
1933
1974
  h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8 } },
1934
- ...[['all', '全部'], ['available', '可安装或升级'], ['installed', '已安装'], ['retired', '已废弃']].map(([value, label]) =>
1975
+ ...[['all', '目录概览'], ['available', '可安装或升级'], ['installed', '已安装'], ['retired', '已废弃']].map(([value, label]) =>
1935
1976
  h('button', { key: value, type: 'button', onClick: () => setFilter(value), 'aria-pressed': filter === value,
1936
- style: { ...secondaryButtonStyle, minHeight: 34, backgroundColor: filter === value ? 'var(--dsw-alias-bg-layer-2)' : secondaryButtonStyle.backgroundColor } }, label)))) : null,
1937
- plugins.length && message ? h('p', { role: 'status', style: mutedTextStyle }, message) : null,
1977
+ style: { ...secondaryButtonStyle, minHeight: 34, backgroundColor: filter === value ? 'var(--dsw-alias-bg-layer-2)' : secondaryButtonStyle.backgroundColor } }, label)),
1978
+ h('label', { style: { ...mutedTextStyle, marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 6 } }, '用途类别',
1979
+ h('select', { value: categoryFilter, onChange: (event) => setCategoryFilter(event.target.value),
1980
+ 'aria-label': '筛选插件用途类别', style: { ...inputStyle, width: 'auto', minHeight: 34 } },
1981
+ h('option', { value: 'all' }, '全部用途'),
1982
+ ...extensionCategories.map(([category, label]) => h('option', { key: category, value: category }, label)))))) : null,
1983
+ plugins.length && message ? h('p', { role: 'status', style: feedbackNoticeStyle }, message) : null,
1938
1984
  visible.length ? h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 420px), 1fr))', gap: 16 } },
1939
1985
  ...visible.map((plugin) => {
1940
1986
  const local = bundles.find((bundle) => bundle.name === plugin.package_name && bundle.installed)
@@ -1947,7 +1993,7 @@ window.__ModuleLoader__.load({
1947
1993
  const verifiedLocal = sameVersion && artifactUrl && sourceKnown && local.source === artifactUrl
1948
1994
  const recordedInstall = sameVersion && !sourceKnown && previouslyInstalledReviewedPlugin(plugin)
1949
1995
  const status = security ? '已废弃(安全风险)'
1950
- : plugin.status === 'deprecated' ? '已废弃' : local?.error ? '加载异常' : localAhead
1996
+ : plugin.status === 'deprecated' ? local?.version ? `已废弃 · 本机 ${local.version}` : '已废弃' : local?.error ? '加载异常' : localAhead
1951
1997
  ? '本机版本高于团队目录版本' : sameVersion
1952
1998
  ? verifiedLocal ? local.enabled ? '本机已装(团队来源,已启用)' : '本机已装(团队来源,未启用)'
1953
1999
  : sourceKnown ? '本机同版,来源不同' : recordedInstall
@@ -1959,7 +2005,9 @@ window.__ModuleLoader__.load({
1959
2005
  h('div', { style: { display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', gap: 8 } },
1960
2006
  h('div', { style: { display: 'flex', alignItems: 'baseline', flexWrap: 'wrap', gap: 8 } },
1961
2007
  h('h2', { style: { margin: 0, fontSize: 19 } }, plugin.title),
1962
- embedded ? h('span', { style: mutedTextStyle }, '插件') : null),
2008
+ embedded ? h('span', { style: mutedTextStyle }, '插件') : null,
2009
+ h('span', { style: { ...mutedTextStyle, padding: '2px 7px', borderRadius: 6,
2010
+ backgroundColor: 'var(--dsw-alias-bg-layer-2)' } }, categoryLabel(categoryOf(plugin)))),
1963
2011
  h('span', { style: security
1964
2012
  ? { ...mutedTextStyle, color: 'var(--dsw-alias-label-danger, #d44)', fontWeight: 650 } : mutedTextStyle }, status)),
1965
2013
  h('p', { style: mutedTextStyle }, plugin.description),
@@ -2010,6 +2058,9 @@ window.__ModuleLoader__.load({
2010
2058
  const [bundleRelease, setBundleRelease] = useState(null)
2011
2059
  const [bundleUpdate, setBundleUpdate] = useState({ state: 'idle', currentVersion: '', message: '' })
2012
2060
  const [authorizedModels, setAuthorizedModels] = useState([])
2061
+ const [providerSyncGroups, setProviderSyncGroups] = useState([])
2062
+ const [appliedProviderSyncExclusions, setAppliedProviderSyncExclusions] = useState([])
2063
+ const [providerSyncDraft, setProviderSyncDraft] = useState([])
2013
2064
  const [serverDefaultModel, setServerDefaultModel] = useState(null)
2014
2065
  const [dshDefaultSelection, setDshDefaultSelection] = useState(null)
2015
2066
  const [defaultModelDraft, setDefaultModelDraft] = useState('')
@@ -2095,17 +2146,27 @@ window.__ModuleLoader__.load({
2095
2146
  : `ant-team-openai-${model.provider_id}`
2096
2147
  const modelSelectionKey = (model) => JSON.stringify({ provider_id: model.provider_id || '', protocol: model.protocol, model_id: model.model_id })
2097
2148
  const defaultPreferenceKey = (rawServiceUrl) => `${DEFAULT_MODEL_INITIALIZED_KEY}:${normalizeServiceUrl(rawServiceUrl)}:${savedDeviceId() || ''}`
2149
+ const providerSyncPreferenceKey = (rawServiceUrl) => `${PROVIDER_SYNC_EXCLUSIONS_KEY}:${normalizeServiceUrl(rawServiceUrl)}:${savedDeviceId() || ''}`
2098
2150
  const readDefaultPreference = (rawServiceUrl) => {
2099
2151
  try { return JSON.parse(localStorage.getItem(defaultPreferenceKey(rawServiceUrl)) || 'null') } catch { return null }
2100
2152
  }
2101
2153
  const saveDefaultPreference = (rawServiceUrl, value) => {
2102
2154
  try { localStorage.setItem(defaultPreferenceKey(rawServiceUrl), JSON.stringify(value)) } catch {}
2103
2155
  }
2104
- const modelForProviderSelection = (selection, models) => {
2156
+ const modelForProviderSelection = (selection, models, preferredModel = null) => {
2105
2157
  if (!selection) return null
2106
2158
  const exact = models.find((model) => model.model_id === selection.model && modelProvider(model) === selection.provider)
2107
2159
  if (exact) return exact
2108
- const legacyProvider = ['ant-team-openai', 'ant-team-openai-chat-completions', 'ant-team-openai-responses'].includes(selection.provider)
2160
+ if (['ant-team-openai', 'ant-team-openai-chat-completions'].includes(selection.provider)) {
2161
+ const chatCandidates = models.filter((model) => model.protocol === 'openai'
2162
+ && model.openai_api_mode !== 'responses' && model.model_id === selection.model)
2163
+ const preferredChat = preferredModel && chatCandidates.find((model) =>
2164
+ modelSelectionKey(model) === modelSelectionKey(preferredModel))
2165
+ if (preferredChat) return preferredChat
2166
+ const canonicalChat = chatCandidates.find((model) => model.provider_id === 'openai-default')
2167
+ return canonicalChat || (chatCandidates.length === 1 ? chatCandidates[0] : null)
2168
+ }
2169
+ const legacyProvider = selection.provider === 'ant-team-openai-responses'
2109
2170
  ? 'openai-default' : selection.provider === 'ant-team-anthropic' ? 'anthropic-default' : null
2110
2171
  return legacyProvider ? models.find((model) => model.provider_id === legacyProvider && model.model_id === selection.model) : null
2111
2172
  }
@@ -2131,8 +2192,8 @@ window.__ModuleLoader__.load({
2131
2192
  return selection
2132
2193
  }
2133
2194
  const applyUserDefaultModel = async (model) => {
2134
- if (!model || !authorizedModels.some((item) => modelSelectionKey(item) === modelSelectionKey(model))) {
2135
- setDefaultModelMessage('请选择当前已获准的模型。')
2195
+ if (!model || !syncedModels.some((item) => modelSelectionKey(item) === modelSelectionKey(model))) {
2196
+ setDefaultModelMessage('请选择当前已同步到 DSH 的模型。')
2136
2197
  return
2137
2198
  }
2138
2199
  setDefaultModelBusy(true)
@@ -2149,7 +2210,7 @@ window.__ModuleLoader__.load({
2149
2210
  setDefaultModelMessage(error instanceof Error ? error.message : '保存本机默认模型失败。')
2150
2211
  } finally { setDefaultModelBusy(false) }
2151
2212
  }
2152
- const applyAuthorizedModels = async (models, rawServiceUrl, rawDefaultModel = null) => {
2213
+ const applyAuthorizedModels = async (models, rawServiceUrl, rawDefaultModel = null, options = {}) => {
2153
2214
  const normalized = normalizeServiceUrl(rawServiceUrl)
2154
2215
  if (!normalized || !Array.isArray(models)) throw new Error('ANT 返回的团队模型目录格式无效。')
2155
2216
  if (typeof remote?.settings?.mutate !== 'function') throw new Error('当前 DSH 版本未提供模型设置接口。')
@@ -2183,6 +2244,8 @@ window.__ModuleLoader__.load({
2183
2244
  : provider.providerId === 'anthropic-default'
2184
2245
  ? 'ant-team-anthropic' : `ant-team-${provider.protocol}-${provider.providerId}`
2185
2246
  const routeConfigurations = new Map()
2247
+ const compatibilityRoutes = []
2248
+ const providerGroups = []
2186
2249
  for (const provider of providers) {
2187
2250
  const route = routeForProvider(provider)
2188
2251
  const api = provider.protocol === 'openai'
@@ -2200,9 +2263,17 @@ window.__ModuleLoader__.load({
2200
2263
  baseURL,
2201
2264
  models: provider.models.map((model) => ({ id: model.model_id, name: model.display_name })),
2202
2265
  })
2266
+ providerGroups.push({
2267
+ key: route,
2268
+ label: `${provider.providerName} · ${provider.protocol === 'anthropic' ? 'Anthropic Messages' : provider.apiMode === 'responses' ? 'OpenAI Responses' : 'OpenAI Chat Completions'}`,
2269
+ routes: [route],
2270
+ modelCount: provider.models.length,
2271
+ legacy: false,
2272
+ })
2203
2273
  if (provider.protocol === 'openai' && provider.providerId === 'openai-default') {
2204
2274
  const aliases = provider.apiMode === 'responses'
2205
2275
  ? ['ant-team-openai-chat-completions', 'ant-team-openai'] : ['ant-team-openai']
2276
+ compatibilityRoutes.push(...aliases)
2206
2277
  const legacyChat = {
2207
2278
  displayName: `${provider.providerName} · Chat(旧会话兼容)`,
2208
2279
  apiKeyEnv: 'ANT_MODEL_API_KEY',
@@ -2213,13 +2284,56 @@ window.__ModuleLoader__.load({
2213
2284
  for (const alias of aliases) routeConfigurations.set(alias, legacyChat)
2214
2285
  }
2215
2286
  }
2216
- const routes = [...routeConfigurations.keys()]
2217
2287
  const ops = []
2218
2288
  let previousRoutes = []
2219
2289
  try {
2220
2290
  const parsed = JSON.parse(localStorage.getItem(PROVIDER_ROUTES_STORAGE_KEY) || '[]')
2221
2291
  if (Array.isArray(parsed)) previousRoutes = parsed.filter((route) => typeof route === 'string')
2222
2292
  } catch {}
2293
+ const compatibilityGroup = compatibilityRoutes.length ? [{
2294
+ key: 'legacy-openai-chat', label: 'ANT OpenAI · Chat(旧会话兼容)', routes: compatibilityRoutes,
2295
+ modelCount: providers.find((provider) => provider.providerId === 'openai-default')?.models.length || 0,
2296
+ legacy: true,
2297
+ }] : []
2298
+ const allGroups = [...providerGroups, ...compatibilityGroup]
2299
+ let excludedGroups = options.excludedGroups
2300
+ if (!Array.isArray(excludedGroups)) {
2301
+ try {
2302
+ const saved = JSON.parse(localStorage.getItem(providerSyncPreferenceKey(normalized)) || 'null')
2303
+ if (Array.isArray(saved)) excludedGroups = saved
2304
+ } catch {}
2305
+ }
2306
+ if (!Array.isArray(excludedGroups)) {
2307
+ // Existing installs keep their current legacy alias group; first-time installs get all routes.
2308
+ excludedGroups = previousRoutes.length && compatibilityRoutes.length
2309
+ && !compatibilityRoutes.some((route) => previousRoutes.includes(route)) ? ['legacy-openai-chat'] : []
2310
+ }
2311
+ excludedGroups = [...new Set(excludedGroups.filter((key) => allGroups.some((group) => group.key === key)))]
2312
+ const excludedGroupSet = new Set(excludedGroups)
2313
+ const excluded = new Set(allGroups.filter((group) => excludedGroupSet.has(group.key)).flatMap((group) => group.routes))
2314
+ const routes = [...routeConfigurations.keys()].filter((route) => !excluded.has(route))
2315
+ const syncedModels = models.filter((model) => !excluded.has(modelProvider(model)))
2316
+ if (models.length && !syncedModels.length) throw new Error('请至少保留一个常规模型 provider 同步到本机 DSH。')
2317
+ const defaultMatch = rawDefaultModel && models.find((model) =>
2318
+ (rawDefaultModel.provider_id ? model.provider_id === rawDefaultModel.provider_id : model.protocol === rawDefaultModel.protocol)
2319
+ && model.protocol === rawDefaultModel.protocol && model.model_id === rawDefaultModel.model_id)
2320
+ const syncedDefaultMatch = defaultMatch && syncedModels.some((model) => modelSelectionKey(model) === modelSelectionKey(defaultMatch))
2321
+ ? defaultMatch : null
2322
+ const serverDefault = syncedDefaultMatch || null
2323
+ const initialDefault = syncedDefaultMatch || (rawDefaultModel == null ? syncedModels[0] : null)
2324
+ if (Array.isArray(options.excludedGroups)) {
2325
+ const intentionallyRemovedRoutes = [...excluded].filter((route) => previousRoutes.includes(route))
2326
+ if (intentionallyRemovedRoutes.length && !window.confirm(
2327
+ '关闭这些 provider 会从本机 DSH 移除对应模型路由。保存了这些路由的历史会话可能暂时无法继续;重新开启同步可以恢复。仍要应用吗?')) {
2328
+ throw new Error('已取消同步范围更改;本机 DSH 设置未修改。')
2329
+ }
2330
+ const currentSelection = await readDshDefaultSelection()
2331
+ if (currentSelection?.provider && excluded.has(currentSelection.provider)) {
2332
+ const replacement = modelForProviderSelection(currentSelection, syncedModels, serverDefault) || serverDefault || syncedModels[0]
2333
+ if (!replacement) throw new Error('请先选择一个保留同步的模型,再移除当前 DSH 默认 provider。')
2334
+ await writeDshDefaultModel(replacement)
2335
+ }
2336
+ }
2223
2337
  const knownRoutes = new Set([...previousRoutes, 'ant-team-openai', 'ant-team-openai-chat-completions',
2224
2338
  'ant-team-openai-responses', 'ant-team-anthropic'])
2225
2339
  for (const route of knownRoutes) {
@@ -2231,11 +2345,10 @@ window.__ModuleLoader__.load({
2231
2345
  const settingsWrite = await remote.settings.mutate('llm-pi-ai', ops, undefined)
2232
2346
  if (!settingsWrite.ok) throw new Error(settingsWrite.error?.message || 'DSH 无法更新团队模型设置。')
2233
2347
  try { localStorage.setItem(PROVIDER_ROUTES_STORAGE_KEY, JSON.stringify(routes)) } catch {}
2234
- const defaultMatch = rawDefaultModel && models.find((model) =>
2235
- (rawDefaultModel.provider_id ? model.provider_id === rawDefaultModel.provider_id : model.protocol === rawDefaultModel.protocol)
2236
- && model.protocol === rawDefaultModel.protocol && model.model_id === rawDefaultModel.model_id)
2237
- const serverDefault = defaultMatch || null
2238
- const initialDefault = defaultMatch || (rawDefaultModel == null ? models[0] : null)
2348
+ try { localStorage.setItem(providerSyncPreferenceKey(normalized), JSON.stringify(excludedGroups)) } catch {}
2349
+ setProviderSyncGroups(allGroups)
2350
+ setAppliedProviderSyncExclusions(excludedGroups)
2351
+ setProviderSyncDraft(excludedGroups)
2239
2352
  setServerDefaultModel(serverDefault)
2240
2353
  const preference = readDefaultPreference(normalized)
2241
2354
  let statusMessage = ''
@@ -2249,8 +2362,8 @@ window.__ModuleLoader__.load({
2249
2362
  : '当前无法使用团队默认模型;已用首个获准模型初始化 DSH 全局默认。请管理员检查默认设置和成员授权。'
2250
2363
  } else {
2251
2364
  selection = await readDshDefaultSelection()
2252
- const currentAuthorized = modelForProviderSelection(selection, models)
2253
- const baseline = models.find((model) => modelSelectionKey(model) === preference?.serverDefault)
2365
+ const currentAuthorized = modelForProviderSelection(selection, syncedModels, serverDefault)
2366
+ const baseline = syncedModels.find((model) => modelSelectionKey(model) === preference?.serverDefault)
2254
2367
  const stillUsingBaseline = baseline && currentAuthorized && modelSelectionKey(currentAuthorized) === modelSelectionKey(baseline)
2255
2368
  if (currentAuthorized && selection?.provider !== modelProvider(currentAuthorized)) {
2256
2369
  selection = await writeDshDefaultModel(currentAuthorized)
@@ -2259,8 +2372,8 @@ window.__ModuleLoader__.load({
2259
2372
  selection = await writeDshDefaultModel(serverDefault)
2260
2373
  saveDefaultPreference(normalized, { ...preference, initialized: true, serverDefault: modelSelectionKey(serverDefault) })
2261
2374
  statusMessage = '团队默认模型已更新;你尚未设置个人覆盖,因此已同步到 DSH 全局默认。'
2262
- } else if (selection?.provider?.startsWith('ant-team-') && !currentAuthorized && models.length) {
2263
- const fallback = serverDefault || models[0]
2375
+ } else if (selection?.provider?.startsWith('ant-team-') && !currentAuthorized && syncedModels.length) {
2376
+ const fallback = serverDefault || syncedModels[0]
2264
2377
  selection = await writeDshDefaultModel(fallback)
2265
2378
  saveDefaultPreference(normalized, { ...preference, initialized: true,
2266
2379
  serverDefault: modelSelectionKey(serverDefault || fallback) })
@@ -2268,11 +2381,11 @@ window.__ModuleLoader__.load({
2268
2381
  ? '当前默认模型已失去授权,已恢复为团队默认模型。'
2269
2382
  : '当前默认模型已失去授权,已改用首个获准模型;请管理员设置团队默认模型。'
2270
2383
  }
2271
- const effectiveAuthorized = modelForProviderSelection(selection, models)
2384
+ const effectiveAuthorized = modelForProviderSelection(selection, syncedModels)
2272
2385
  setDshDefaultSelection(selection)
2273
2386
  setDefaultModelDraft(effectiveAuthorized ? modelSelectionKey(effectiveAuthorized) : '')
2274
2387
  if (!serverDefault && !statusMessage) statusMessage = rawDefaultModel
2275
- ? '团队默认模型不在这台设备的获准范围内;请联系管理员调整模型权限。'
2388
+ ? '团队默认模型当前未同步到这台 DSH;请在下方选择一个已同步模型作为本机默认。'
2276
2389
  : '团队默认模型尚未设置;请联系管理员设置,或在下方选择本机可用的默认模型。'
2277
2390
  else if (!statusMessage) statusMessage = effectiveAuthorized
2278
2391
  ? '已保留你选择的 DSH 全局默认模型。'
@@ -2281,10 +2394,10 @@ window.__ModuleLoader__.load({
2281
2394
  setDefaultModelMessage(statusMessage)
2282
2395
  try { localStorage.setItem(SERVICE_URL_STORAGE_KEY, normalized) } catch {}
2283
2396
  onServiceUrlChange(normalized)
2284
- return { count: models.length, defaultSelection: selection }
2397
+ return { count: syncedModels.length, defaultSelection: selection }
2285
2398
  }
2286
2399
 
2287
- const refreshModels = async () => {
2400
+ const refreshModels = async (applyDraft = false) => {
2288
2401
  if (!bindingReady) {
2289
2402
  setMessage('请先等待团队绑定检查完成,再刷新团队模型。')
2290
2403
  return
@@ -2301,13 +2414,14 @@ window.__ModuleLoader__.load({
2301
2414
  if (response.status === 401) setNeedsReconnect(true)
2302
2415
  throw new Error(result.message || `刷新失败(${response.status})`)
2303
2416
  }
2304
- const sync = await applyAuthorizedModels(result.models, result.service_url, result.default_model)
2417
+ const sync = await applyAuthorizedModels(result.models, result.service_url, result.default_model,
2418
+ applyDraft ? { excludedGroups: providerSyncDraft } : {})
2305
2419
  setAuthorizedModels(result.models)
2306
2420
  setLastModelSync(new Date())
2307
2421
  setCredentialConfigured(true)
2308
2422
  setNeedsReconnect(false)
2309
2423
  setReauthenticate(false)
2310
- setModelMessage(`模型目录已刷新,共 ${sync.count} 个获准模型。`)
2424
+ setModelMessage(`模型目录已刷新,共同步 ${sync.count} 个获准模型。`)
2311
2425
  } catch (error) {
2312
2426
  setModelMessage(error instanceof Error ? error.message : '刷新模型失败,请稍后重试。')
2313
2427
  } finally {
@@ -2509,6 +2623,7 @@ window.__ModuleLoader__.load({
2509
2623
  if (!credentialResult.ok) throw new Error(credentialResult.error?.message || 'ANT 成员凭证未能清除。')
2510
2624
  setCredentialConfigured(false)
2511
2625
  setAuthorizedModels([]); setServerDefaultModel(null); setDshDefaultSelection(null); setDefaultModelDraft('')
2626
+ setProviderSyncGroups([]); setAppliedProviderSyncExclusions([]); setProviderSyncDraft([])
2512
2627
  try { localStorage.removeItem(`${DEFAULT_MODEL_INITIALIZED_KEY}:${normalizeServiceUrl(serviceUrl)}:${savedDeviceId() || ''}`) } catch {}
2513
2628
  onConnectionChange?.(null)
2514
2629
  setNeedsReconnect(false)
@@ -2720,11 +2835,35 @@ window.__ModuleLoader__.load({
2720
2835
  const canCheckUpdate = bindingReady && credentialConfigured === true && !needsReconnect
2721
2836
  const updateGuideUrl = pinnedServiceUrl
2722
2837
  ? new URL('/ant-update', pinnedServiceUrl).href : null
2723
- const currentDshDefaultModel = modelForProviderSelection(dshDefaultSelection, authorizedModels)
2838
+ const currentDshDefaultModel = modelForProviderSelection(dshDefaultSelection, authorizedModels, serverDefaultModel)
2839
+ const excludedGroupSet = new Set(providerSyncDraft)
2840
+ const excludedRouteSet = new Set(providerSyncGroups.filter((group) => excludedGroupSet.has(group.key))
2841
+ .flatMap((group) => group.routes))
2842
+ const appliedExcludedGroupSet = new Set(appliedProviderSyncExclusions)
2843
+ const appliedExcludedRouteSet = new Set(providerSyncGroups.filter((group) => appliedExcludedGroupSet.has(group.key))
2844
+ .flatMap((group) => group.routes))
2845
+ const syncedModels = authorizedModels.filter((model) => !appliedExcludedRouteSet.has(modelProvider(model)))
2846
+ const hasPendingSyncChanges = [...new Set([...providerSyncDraft, ...appliedProviderSyncExclusions])]
2847
+ .some((key) => excludedGroupSet.has(key) !== appliedExcludedGroupSet.has(key))
2848
+ const activePrimaryGroupCount = providerSyncGroups.filter((group) => !group.legacy && !excludedGroupSet.has(group.key)).length
2849
+ const pendingServerDefaultRemoval = hasPendingSyncChanges && serverDefaultModel
2850
+ ? providerSyncGroups.some((group) => group.routes.includes(modelProvider(serverDefaultModel))
2851
+ && !appliedExcludedGroupSet.has(group.key) && excludedGroupSet.has(group.key))
2852
+ : false
2853
+ const selectedDefaultDraft = syncedModels.find((model) => modelSelectionKey(model) === defaultModelDraft)
2854
+ const toggleProviderSyncGroup = (group, checked) => {
2855
+ setProviderSyncDraft((current) => {
2856
+ const next = new Set(current)
2857
+ if (checked) next.delete(group.key)
2858
+ else next.add(group.key)
2859
+ return [...next]
2860
+ })
2861
+ }
2724
2862
  const modelRows = authorizedModels.map((model) => {
2725
2863
  const providerName = model.protocol === 'openai'
2726
2864
  ? compactOpenaiProviderName(model.provider_name)
2727
2865
  : model.provider_name
2866
+ const isSynced = !excludedRouteSet.has(modelProvider(model))
2728
2867
  return h('div', { key: `${model.provider_id || ''}:${model.protocol}:${model.model_id}`, role: 'listitem',
2729
2868
  style: { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) minmax(120px, auto) minmax(130px, auto)',
2730
2869
  gap: 12, alignItems: 'center', padding: '10px 2px', borderBottom: '1px solid var(--dsw-alias-border-l2)' } },
@@ -2732,7 +2871,9 @@ window.__ModuleLoader__.load({
2732
2871
  h('span', { style: { display: 'inline-flex', justifySelf: 'start', padding: '3px 8px', borderRadius: 999,
2733
2872
  backgroundColor: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-secondary)', fontSize: 12 } },
2734
2873
  model.protocol === 'anthropic' ? 'Anthropic Messages' : `OpenAI · ${model.openai_api_mode === 'responses' ? 'Responses' : 'Chat Completions'}`),
2735
- h('span', { style: { ...mutedTextStyle, textAlign: 'right' } }, '已同步到 DSH'))
2874
+ h('span', { style: { ...mutedTextStyle, textAlign: 'right' } }, hasPendingSyncChanges
2875
+ ? isSynced ? '刷新后同步到 DSH' : '刷新后从 DSH 移除'
2876
+ : isSynced ? '已同步到 DSH' : '未同步到 DSH'))
2736
2877
  })
2737
2878
  const connectionPanel = h('section', { style: { ...settingsPanelStyle, display: activeTab === 'connection' ? 'grid' : 'none' } },
2738
2879
  h('div', null,
@@ -2784,7 +2925,7 @@ window.__ModuleLoader__.load({
2784
2925
  setMessage('设备编号已复制,请交给原团队管理员。')
2785
2926
  } catch { setMessage(savedDeviceId() ? `请复制设备编号:${savedDeviceId()}` : '本机无法确认旧设备编号,请联系原团队管理员核查。') }
2786
2927
  }, style: secondaryButtonStyle }, '复制设备编号'))) : null,
2787
- message ? h('p', { role: 'status', style: mutedTextStyle }, message) : null)
2928
+ message ? h('p', { role: 'status', style: feedbackNoticeStyle }, message) : null)
2788
2929
 
2789
2930
  const modelPanel = h('section', { style: { ...settingsPanelStyle, display: activeTab === 'models' ? 'grid' : 'none' } },
2790
2931
  h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12 } },
@@ -2806,10 +2947,10 @@ window.__ModuleLoader__.load({
2806
2947
  h('p', { style: mutedTextStyle }, '首次同步时,ANT 会用团队默认初始化 DSH 全局默认模型。它会影响该 Profile 的新会话以及 ANT 对话和任务;之后刷新保留你的个人覆盖。'),
2807
2948
  h('label', { style: { display: 'grid', gap: 5, maxWidth: 720 } }, 'DSH 全局默认模型',
2808
2949
  h('select', { 'aria-label': 'DSH 全局默认模型', value: defaultModelDraft,
2809
- disabled: credentialConfigured !== true || !authorizedModels.length || defaultModelBusy || busy,
2950
+ disabled: credentialConfigured !== true || !syncedModels.length || defaultModelBusy || busy || hasPendingSyncChanges,
2810
2951
  onChange: (event) => setDefaultModelDraft(event.target.value), style: inputStyle },
2811
2952
  h('option', { value: '' }, '选择获准模型'),
2812
- ...authorizedModels.map((model) => {
2953
+ ...syncedModels.map((model) => {
2813
2954
  const providerName = model.protocol === 'openai'
2814
2955
  ? compactOpenaiProviderName(model.provider_name)
2815
2956
  : model.provider_name
@@ -2818,14 +2959,28 @@ window.__ModuleLoader__.load({
2818
2959
  `${providerName ? `${providerName} · ` : ''}${model.display_name || model.model_id} · ${protocolName}`)
2819
2960
  }))),
2820
2961
  h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' } },
2821
- h('button', { type: 'button', disabled: defaultModelBusy || !defaultModelDraft,
2822
- onClick: () => { const chosen = authorizedModels.find((model) => modelSelectionKey(model) === defaultModelDraft); void applyUserDefaultModel(chosen) },
2962
+ h('button', { type: 'button', disabled: defaultModelBusy || !selectedDefaultDraft || hasPendingSyncChanges,
2963
+ onClick: () => void applyUserDefaultModel(selectedDefaultDraft),
2823
2964
  style: primaryButtonStyle }, defaultModelBusy ? '保存中…' : '保存为 DSH 全局默认'),
2824
- h('button', { type: 'button', disabled: defaultModelBusy || !serverDefaultModel
2965
+ h('button', { type: 'button', disabled: busy || defaultModelBusy || pendingServerDefaultRemoval || !serverDefaultModel
2825
2966
  || modelSelectionKey(currentDshDefaultModel || {}) === modelSelectionKey(serverDefaultModel),
2826
2967
  onClick: () => void applyUserDefaultModel(serverDefaultModel), style: secondaryButtonStyle }, '恢复团队默认'),
2827
2968
  currentDshDefaultModel ? h('span', { style: mutedTextStyle }, `当前 DSH 默认:${currentDshDefaultModel.display_name || currentDshDefaultModel.model_id}`) : null),
2969
+ pendingServerDefaultRemoval ? h('p', { role: 'status', style: mutedTextStyle },
2970
+ '团队默认 provider 正准备移除;先刷新应用同步范围,或重新勾选该 provider。') : null,
2828
2971
  defaultModelMessage ? h('p', { role: 'status', style: mutedTextStyle }, defaultModelMessage) : null),
2972
+ providerSyncGroups.length ? h('section', { style: { ...infoNoticeStyle, gap: 8 } },
2973
+ h('strong', null, '同步到本机 DSH 的 provider'),
2974
+ h('p', { style: mutedTextStyle }, '首次升级会保留当前已同步的旧会话兼容路由。取消勾选并刷新后,该 provider 会从本机 DSH 移除;使用它的历史会话可能暂时无法继续,重新勾选并刷新可恢复。'),
2975
+ h('p', { style: mutedTextStyle }, '至少保留一个常规模型 provider;旧会话兼容组只供已有会话使用。'),
2976
+ hasPendingSyncChanges ? h('p', { role: 'status', style: mutedTextStyle }, '同步范围尚未应用;点击“刷新团队模型”后生效。') : null,
2977
+ ...providerSyncGroups.map((group) => h('label', { key: group.key,
2978
+ style: { display: 'flex', alignItems: 'center', gap: 9, padding: '5px 0', cursor: busy ? 'default' : 'pointer' } },
2979
+ h('input', { type: 'checkbox', 'aria-label': group.label,
2980
+ checked: !excludedGroupSet.has(group.key), disabled: busy
2981
+ || (!group.legacy && !excludedGroupSet.has(group.key) && activePrimaryGroupCount <= 1),
2982
+ onChange: (event) => toggleProviderSyncGroup(group, event.target.checked) }),
2983
+ h('span', null, `${group.label} · ${group.modelCount} 个模型`)))) : null,
2829
2984
  h('div', { role: 'list', 'aria-label': '获准模型', style: { display: 'grid', gap: 0, borderTop: '1px solid var(--dsw-alias-border-l2)' } },
2830
2985
  modelRows.length ? modelRows : h('div', { style: { display: 'grid', gap: 8, padding: '12px 0' } },
2831
2986
  h('p', { style: mutedTextStyle }, credentialConfigured === true && modelMessage.startsWith('正在') ? modelMessage
@@ -2835,7 +2990,7 @@ window.__ModuleLoader__.load({
2835
2990
  style: secondaryButtonStyle }, '前往团队连接') : null)),
2836
2991
  h('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 10 } },
2837
2992
  h('button', { type: 'button', disabled: busy || defaultModelBusy || !bindingReady || credentialConfigured !== true,
2838
- onClick: () => void refreshModels(), style: primaryButtonStyle }, busy ? '刷新中…' : '刷新团队模型'),
2993
+ onClick: () => void refreshModels(true), style: primaryButtonStyle }, busy ? '刷新中…' : '刷新团队模型'),
2839
2994
  lastModelSync ? h('span', { style: mutedTextStyle }, `最近同步:${lastModelSync.toLocaleString('zh-CN')}`) : null),
2840
2995
  modelMessage && !modelMessage.startsWith('正在') ? h('p', { role: 'status', style: mutedTextStyle }, modelMessage) : null)
2841
2996
 
@@ -2869,12 +3024,12 @@ window.__ModuleLoader__.load({
2869
3024
  h('strong', { style: { fontSize: 14, lineHeight: 1.4 } }, '本版更新'),
2870
3025
  h('p', { style: mutedTextStyle }, bundleRelease.release_notes)) : null,
2871
3026
  ['incompatible', 'unknown'].includes(serviceCompatibility.state) ? h('p', { role: 'status', style: {
2872
- ...mutedTextStyle,
3027
+ ...feedbackNoticeStyle,
2873
3028
  color: serviceCompatibility.state === 'incompatible' ? 'var(--dsw-alias-state-error-primary)'
2874
3029
  : 'var(--dsw-alias-state-warning-primary, var(--dsw-alias-label-secondary))',
2875
3030
  } }, `服务端兼容性:${serviceCompatibility.message}`) : null,
2876
3031
  bundleUpdate.message && updateStatusNeedsDetails.includes(bundleUpdate.state) ? h('p', { role: 'status', style: bundleUpdate.state === 'revoked'
2877
- ? { ...mutedTextStyle, color: 'var(--dsw-alias-state-error-primary)' } : mutedTextStyle }, bundleUpdate.message) : null,
3032
+ ? { ...feedbackNoticeStyle, color: 'var(--dsw-alias-state-error-primary)' } : feedbackNoticeStyle }, bundleUpdate.message) : null,
2878
3033
  !canCheckUpdate && updateGuideUrl ? h('a', { href: updateGuideUrl, target: '_blank', rel: 'noopener noreferrer',
2879
3034
  style: { color: 'var(--dsw-alias-link)', fontSize: 13 } }, '打开更新指引 ↗') : null,
2880
3035
  !canCheckUpdate && !updateGuideUrl ? h('p', { style: mutedTextStyle }, '无法核对原团队地址时,请向企业管理员索取更新指引。') : null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-wendao/ant",
3
- "version": "0.4.15",
3
+ "version": "0.5.0",
4
4
  "description": "ANT (AI-native Teams) connects a DeepSeek Harness Desktop profile to team identity and approved models.",
5
5
  "type": "module",
6
6
  "scripts": {