@ai-wendao/ant 0.4.14 → 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 +3 -3
  2. package/client.js +265 -94
  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
 
@@ -14,7 +14,7 @@ ANT (AI-native Teams) connects a DeepSeek Harness Desktop profile to its team's
14
14
 
15
15
  飞书机器人连接和消息收发由 DSH-IM 负责;ANT Bundle 不保存机器人密钥,也不直连 IM 平台。成员在 ANT 插件的“协作”页选择机器人、启用本机协作,并设置私聊和各群的普通聊天开关。群第一次 @ 机器人后会出现在群列表。群聊天开关只控制普通聊天;任务卡还要单独指定私聊或群、请求人范围和有效期。
16
16
 
17
- 开放任务时,成员用自然语言描述什么时候执行、要看哪些资料、怎样处理和返回结果。描述中的 Gitea PR 链接、仓库和编号也用自然语言表达。点击“识别规则”会调用一次当前获准的 DSH 团队模型,只生成草稿,不保存任务;检查摘要不合意时,修改描述并重新识别。任务名称、触发条件、PR 信息和参数由识别结果转成内部规则,界面不要求逐项填写。执行方、使用范围、请求人和有效期由通用控件控制。
17
+ 开放任务时,成员用自然语言描述什么时候执行、要看哪些资料、怎样处理和返回结果。描述中的 Gitea PR 链接、仓库和编号也用自然语言表达。点击“识别规则”会调用一次当前获准的 DSH 团队模型,只生成草稿,不保存任务;草稿包含明确触发短语,由机器人主人检查后再开放。机器人主人指在本人 DSH 中连接并管理该 DSH-IM 机器人的 ANT 成员,不代表群管理员身份;群内谁能请求任务仍由规则中的群范围和请求人范围决定。任务只在请求消息以已配置短语开头时执行;检查摘要不合意时,修改描述并重新识别。任务名称、触发条件、PR 信息和参数由识别结果转成内部规则,界面不要求逐项填写。执行方、使用范围、请求人和有效期由通用控件控制。
18
18
 
19
19
  群任务和群聊天设置只在 ANT 协作页配置。当前版本不设群管理员身份绑定,也不支持在群里开放任务、确认授权或开关群聊。群成员可以普通聊天,或按已开放任务的群范围、请求人范围、触发条件和有效期发起任务;未开放的任务不会因为群成员的消息而执行。关闭群普通聊天不会撤销已开放任务。服务端升级时会停用没有明确触发方式的旧任务卡;要继续使用这类任务,需在新界面重新描述规则并开放。
20
20
 
@@ -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 版本未提供模型设置接口。')
@@ -2182,18 +2243,9 @@ window.__ModuleLoader__.load({
2182
2243
  ? provider.apiMode === 'responses' ? 'ant-team-openai-responses' : 'ant-team-openai-chat-completions'
2183
2244
  : provider.providerId === 'anthropic-default'
2184
2245
  ? 'ant-team-anthropic' : `ant-team-${provider.protocol}-${provider.providerId}`
2185
- const routes = providers.map(routeForProvider)
2186
- const ops = []
2187
- let previousRoutes = []
2188
- try {
2189
- const parsed = JSON.parse(localStorage.getItem(PROVIDER_ROUTES_STORAGE_KEY) || '[]')
2190
- if (Array.isArray(parsed)) previousRoutes = parsed.filter((route) => typeof route === 'string')
2191
- } catch {}
2192
- const knownRoutes = new Set([...previousRoutes, 'ant-team-openai', 'ant-team-openai-chat-completions',
2193
- 'ant-team-openai-responses', 'ant-team-anthropic'])
2194
- for (const route of knownRoutes) {
2195
- if (!routes.includes(route)) ops.push({ op: 'unset', path: ['providers', route] })
2196
- }
2246
+ const routeConfigurations = new Map()
2247
+ const compatibilityRoutes = []
2248
+ const providerGroups = []
2197
2249
  for (const provider of providers) {
2198
2250
  const route = routeForProvider(provider)
2199
2251
  const api = provider.protocol === 'openai'
@@ -2204,22 +2256,99 @@ window.__ModuleLoader__.load({
2204
2256
  : provider.protocol === 'openai'
2205
2257
  ? new URL(`/model-gateway/openai/providers/${encodeURIComponent(provider.providerId)}/v1`, normalized).toString()
2206
2258
  : new URL(`/model-gateway/anthropic/providers/${encodeURIComponent(provider.providerId)}`, normalized).toString()
2207
- ops.push({ op: 'set', path: ['providers', route], value: {
2259
+ routeConfigurations.set(route, {
2208
2260
  displayName: provider.providerName,
2209
2261
  apiKeyEnv: 'ANT_MODEL_API_KEY',
2210
2262
  api,
2211
2263
  baseURL,
2212
2264
  models: provider.models.map((model) => ({ id: model.model_id, name: model.display_name })),
2213
- } })
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
+ })
2273
+ if (provider.protocol === 'openai' && provider.providerId === 'openai-default') {
2274
+ const aliases = provider.apiMode === 'responses'
2275
+ ? ['ant-team-openai-chat-completions', 'ant-team-openai'] : ['ant-team-openai']
2276
+ compatibilityRoutes.push(...aliases)
2277
+ const legacyChat = {
2278
+ displayName: `${provider.providerName} · Chat(旧会话兼容)`,
2279
+ apiKeyEnv: 'ANT_MODEL_API_KEY',
2280
+ api: 'openai-completions',
2281
+ baseURL: new URL('/model-gateway/openai/v1', normalized).toString(),
2282
+ models: provider.models.map((model) => ({ id: model.model_id, name: model.display_name })),
2283
+ }
2284
+ for (const alias of aliases) routeConfigurations.set(alias, legacyChat)
2285
+ }
2214
2286
  }
2215
- const settingsWrite = await remote.settings.mutate('llm-pi-ai', ops, undefined)
2216
- if (!settingsWrite.ok) throw new Error(settingsWrite.error?.message || 'DSH 无法更新团队模型设置。')
2217
- try { localStorage.setItem(PROVIDER_ROUTES_STORAGE_KEY, JSON.stringify(routes)) } catch {}
2287
+ const ops = []
2288
+ let previousRoutes = []
2289
+ try {
2290
+ const parsed = JSON.parse(localStorage.getItem(PROVIDER_ROUTES_STORAGE_KEY) || '[]')
2291
+ if (Array.isArray(parsed)) previousRoutes = parsed.filter((route) => typeof route === 'string')
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。')
2218
2317
  const defaultMatch = rawDefaultModel && models.find((model) =>
2219
2318
  (rawDefaultModel.provider_id ? model.provider_id === rawDefaultModel.provider_id : model.protocol === rawDefaultModel.protocol)
2220
2319
  && model.protocol === rawDefaultModel.protocol && model.model_id === rawDefaultModel.model_id)
2221
- const serverDefault = defaultMatch || null
2222
- const initialDefault = defaultMatch || (rawDefaultModel == null ? models[0] : null)
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
+ }
2337
+ const knownRoutes = new Set([...previousRoutes, 'ant-team-openai', 'ant-team-openai-chat-completions',
2338
+ 'ant-team-openai-responses', 'ant-team-anthropic'])
2339
+ for (const route of knownRoutes) {
2340
+ if (!routes.includes(route)) ops.push({ op: 'unset', path: ['providers', route] })
2341
+ }
2342
+ for (const [route, value] of routeConfigurations) {
2343
+ ops.push({ op: 'set', path: ['providers', route], value })
2344
+ }
2345
+ const settingsWrite = await remote.settings.mutate('llm-pi-ai', ops, undefined)
2346
+ if (!settingsWrite.ok) throw new Error(settingsWrite.error?.message || 'DSH 无法更新团队模型设置。')
2347
+ try { localStorage.setItem(PROVIDER_ROUTES_STORAGE_KEY, JSON.stringify(routes)) } catch {}
2348
+ try { localStorage.setItem(providerSyncPreferenceKey(normalized), JSON.stringify(excludedGroups)) } catch {}
2349
+ setProviderSyncGroups(allGroups)
2350
+ setAppliedProviderSyncExclusions(excludedGroups)
2351
+ setProviderSyncDraft(excludedGroups)
2223
2352
  setServerDefaultModel(serverDefault)
2224
2353
  const preference = readDefaultPreference(normalized)
2225
2354
  let statusMessage = ''
@@ -2233,8 +2362,8 @@ window.__ModuleLoader__.load({
2233
2362
  : '当前无法使用团队默认模型;已用首个获准模型初始化 DSH 全局默认。请管理员检查默认设置和成员授权。'
2234
2363
  } else {
2235
2364
  selection = await readDshDefaultSelection()
2236
- const currentAuthorized = modelForProviderSelection(selection, models)
2237
- 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)
2238
2367
  const stillUsingBaseline = baseline && currentAuthorized && modelSelectionKey(currentAuthorized) === modelSelectionKey(baseline)
2239
2368
  if (currentAuthorized && selection?.provider !== modelProvider(currentAuthorized)) {
2240
2369
  selection = await writeDshDefaultModel(currentAuthorized)
@@ -2243,8 +2372,8 @@ window.__ModuleLoader__.load({
2243
2372
  selection = await writeDshDefaultModel(serverDefault)
2244
2373
  saveDefaultPreference(normalized, { ...preference, initialized: true, serverDefault: modelSelectionKey(serverDefault) })
2245
2374
  statusMessage = '团队默认模型已更新;你尚未设置个人覆盖,因此已同步到 DSH 全局默认。'
2246
- } else if (selection?.provider?.startsWith('ant-team-') && !currentAuthorized && models.length) {
2247
- const fallback = serverDefault || models[0]
2375
+ } else if (selection?.provider?.startsWith('ant-team-') && !currentAuthorized && syncedModels.length) {
2376
+ const fallback = serverDefault || syncedModels[0]
2248
2377
  selection = await writeDshDefaultModel(fallback)
2249
2378
  saveDefaultPreference(normalized, { ...preference, initialized: true,
2250
2379
  serverDefault: modelSelectionKey(serverDefault || fallback) })
@@ -2252,11 +2381,11 @@ window.__ModuleLoader__.load({
2252
2381
  ? '当前默认模型已失去授权,已恢复为团队默认模型。'
2253
2382
  : '当前默认模型已失去授权,已改用首个获准模型;请管理员设置团队默认模型。'
2254
2383
  }
2255
- const effectiveAuthorized = modelForProviderSelection(selection, models)
2384
+ const effectiveAuthorized = modelForProviderSelection(selection, syncedModels)
2256
2385
  setDshDefaultSelection(selection)
2257
2386
  setDefaultModelDraft(effectiveAuthorized ? modelSelectionKey(effectiveAuthorized) : '')
2258
2387
  if (!serverDefault && !statusMessage) statusMessage = rawDefaultModel
2259
- ? '团队默认模型不在这台设备的获准范围内;请联系管理员调整模型权限。'
2388
+ ? '团队默认模型当前未同步到这台 DSH;请在下方选择一个已同步模型作为本机默认。'
2260
2389
  : '团队默认模型尚未设置;请联系管理员设置,或在下方选择本机可用的默认模型。'
2261
2390
  else if (!statusMessage) statusMessage = effectiveAuthorized
2262
2391
  ? '已保留你选择的 DSH 全局默认模型。'
@@ -2265,10 +2394,10 @@ window.__ModuleLoader__.load({
2265
2394
  setDefaultModelMessage(statusMessage)
2266
2395
  try { localStorage.setItem(SERVICE_URL_STORAGE_KEY, normalized) } catch {}
2267
2396
  onServiceUrlChange(normalized)
2268
- return { count: models.length, defaultSelection: selection }
2397
+ return { count: syncedModels.length, defaultSelection: selection }
2269
2398
  }
2270
2399
 
2271
- const refreshModels = async () => {
2400
+ const refreshModels = async (applyDraft = false) => {
2272
2401
  if (!bindingReady) {
2273
2402
  setMessage('请先等待团队绑定检查完成,再刷新团队模型。')
2274
2403
  return
@@ -2285,13 +2414,14 @@ window.__ModuleLoader__.load({
2285
2414
  if (response.status === 401) setNeedsReconnect(true)
2286
2415
  throw new Error(result.message || `刷新失败(${response.status})`)
2287
2416
  }
2288
- 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 } : {})
2289
2419
  setAuthorizedModels(result.models)
2290
2420
  setLastModelSync(new Date())
2291
2421
  setCredentialConfigured(true)
2292
2422
  setNeedsReconnect(false)
2293
2423
  setReauthenticate(false)
2294
- setModelMessage(`模型目录已刷新,共 ${sync.count} 个获准模型。`)
2424
+ setModelMessage(`模型目录已刷新,共同步 ${sync.count} 个获准模型。`)
2295
2425
  } catch (error) {
2296
2426
  setModelMessage(error instanceof Error ? error.message : '刷新模型失败,请稍后重试。')
2297
2427
  } finally {
@@ -2493,6 +2623,7 @@ window.__ModuleLoader__.load({
2493
2623
  if (!credentialResult.ok) throw new Error(credentialResult.error?.message || 'ANT 成员凭证未能清除。')
2494
2624
  setCredentialConfigured(false)
2495
2625
  setAuthorizedModels([]); setServerDefaultModel(null); setDshDefaultSelection(null); setDefaultModelDraft('')
2626
+ setProviderSyncGroups([]); setAppliedProviderSyncExclusions([]); setProviderSyncDraft([])
2496
2627
  try { localStorage.removeItem(`${DEFAULT_MODEL_INITIALIZED_KEY}:${normalizeServiceUrl(serviceUrl)}:${savedDeviceId() || ''}`) } catch {}
2497
2628
  onConnectionChange?.(null)
2498
2629
  setNeedsReconnect(false)
@@ -2704,11 +2835,35 @@ window.__ModuleLoader__.load({
2704
2835
  const canCheckUpdate = bindingReady && credentialConfigured === true && !needsReconnect
2705
2836
  const updateGuideUrl = pinnedServiceUrl
2706
2837
  ? new URL('/ant-update', pinnedServiceUrl).href : null
2707
- 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
+ }
2708
2862
  const modelRows = authorizedModels.map((model) => {
2709
2863
  const providerName = model.protocol === 'openai'
2710
2864
  ? compactOpenaiProviderName(model.provider_name)
2711
2865
  : model.provider_name
2866
+ const isSynced = !excludedRouteSet.has(modelProvider(model))
2712
2867
  return h('div', { key: `${model.provider_id || ''}:${model.protocol}:${model.model_id}`, role: 'listitem',
2713
2868
  style: { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) minmax(120px, auto) minmax(130px, auto)',
2714
2869
  gap: 12, alignItems: 'center', padding: '10px 2px', borderBottom: '1px solid var(--dsw-alias-border-l2)' } },
@@ -2716,7 +2871,9 @@ window.__ModuleLoader__.load({
2716
2871
  h('span', { style: { display: 'inline-flex', justifySelf: 'start', padding: '3px 8px', borderRadius: 999,
2717
2872
  backgroundColor: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-secondary)', fontSize: 12 } },
2718
2873
  model.protocol === 'anthropic' ? 'Anthropic Messages' : `OpenAI · ${model.openai_api_mode === 'responses' ? 'Responses' : 'Chat Completions'}`),
2719
- h('span', { style: { ...mutedTextStyle, textAlign: 'right' } }, '已同步到 DSH'))
2874
+ h('span', { style: { ...mutedTextStyle, textAlign: 'right' } }, hasPendingSyncChanges
2875
+ ? isSynced ? '刷新后同步到 DSH' : '刷新后从 DSH 移除'
2876
+ : isSynced ? '已同步到 DSH' : '未同步到 DSH'))
2720
2877
  })
2721
2878
  const connectionPanel = h('section', { style: { ...settingsPanelStyle, display: activeTab === 'connection' ? 'grid' : 'none' } },
2722
2879
  h('div', null,
@@ -2768,7 +2925,7 @@ window.__ModuleLoader__.load({
2768
2925
  setMessage('设备编号已复制,请交给原团队管理员。')
2769
2926
  } catch { setMessage(savedDeviceId() ? `请复制设备编号:${savedDeviceId()}` : '本机无法确认旧设备编号,请联系原团队管理员核查。') }
2770
2927
  }, style: secondaryButtonStyle }, '复制设备编号'))) : null,
2771
- message ? h('p', { role: 'status', style: mutedTextStyle }, message) : null)
2928
+ message ? h('p', { role: 'status', style: feedbackNoticeStyle }, message) : null)
2772
2929
 
2773
2930
  const modelPanel = h('section', { style: { ...settingsPanelStyle, display: activeTab === 'models' ? 'grid' : 'none' } },
2774
2931
  h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12 } },
@@ -2790,10 +2947,10 @@ window.__ModuleLoader__.load({
2790
2947
  h('p', { style: mutedTextStyle }, '首次同步时,ANT 会用团队默认初始化 DSH 全局默认模型。它会影响该 Profile 的新会话以及 ANT 对话和任务;之后刷新保留你的个人覆盖。'),
2791
2948
  h('label', { style: { display: 'grid', gap: 5, maxWidth: 720 } }, 'DSH 全局默认模型',
2792
2949
  h('select', { 'aria-label': 'DSH 全局默认模型', value: defaultModelDraft,
2793
- disabled: credentialConfigured !== true || !authorizedModels.length || defaultModelBusy || busy,
2950
+ disabled: credentialConfigured !== true || !syncedModels.length || defaultModelBusy || busy || hasPendingSyncChanges,
2794
2951
  onChange: (event) => setDefaultModelDraft(event.target.value), style: inputStyle },
2795
2952
  h('option', { value: '' }, '选择获准模型'),
2796
- ...authorizedModels.map((model) => {
2953
+ ...syncedModels.map((model) => {
2797
2954
  const providerName = model.protocol === 'openai'
2798
2955
  ? compactOpenaiProviderName(model.provider_name)
2799
2956
  : model.provider_name
@@ -2802,14 +2959,28 @@ window.__ModuleLoader__.load({
2802
2959
  `${providerName ? `${providerName} · ` : ''}${model.display_name || model.model_id} · ${protocolName}`)
2803
2960
  }))),
2804
2961
  h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' } },
2805
- h('button', { type: 'button', disabled: defaultModelBusy || !defaultModelDraft,
2806
- 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),
2807
2964
  style: primaryButtonStyle }, defaultModelBusy ? '保存中…' : '保存为 DSH 全局默认'),
2808
- h('button', { type: 'button', disabled: defaultModelBusy || !serverDefaultModel
2965
+ h('button', { type: 'button', disabled: busy || defaultModelBusy || pendingServerDefaultRemoval || !serverDefaultModel
2809
2966
  || modelSelectionKey(currentDshDefaultModel || {}) === modelSelectionKey(serverDefaultModel),
2810
2967
  onClick: () => void applyUserDefaultModel(serverDefaultModel), style: secondaryButtonStyle }, '恢复团队默认'),
2811
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,
2812
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,
2813
2984
  h('div', { role: 'list', 'aria-label': '获准模型', style: { display: 'grid', gap: 0, borderTop: '1px solid var(--dsw-alias-border-l2)' } },
2814
2985
  modelRows.length ? modelRows : h('div', { style: { display: 'grid', gap: 8, padding: '12px 0' } },
2815
2986
  h('p', { style: mutedTextStyle }, credentialConfigured === true && modelMessage.startsWith('正在') ? modelMessage
@@ -2819,7 +2990,7 @@ window.__ModuleLoader__.load({
2819
2990
  style: secondaryButtonStyle }, '前往团队连接') : null)),
2820
2991
  h('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 10 } },
2821
2992
  h('button', { type: 'button', disabled: busy || defaultModelBusy || !bindingReady || credentialConfigured !== true,
2822
- onClick: () => void refreshModels(), style: primaryButtonStyle }, busy ? '刷新中…' : '刷新团队模型'),
2993
+ onClick: () => void refreshModels(true), style: primaryButtonStyle }, busy ? '刷新中…' : '刷新团队模型'),
2823
2994
  lastModelSync ? h('span', { style: mutedTextStyle }, `最近同步:${lastModelSync.toLocaleString('zh-CN')}`) : null),
2824
2995
  modelMessage && !modelMessage.startsWith('正在') ? h('p', { role: 'status', style: mutedTextStyle }, modelMessage) : null)
2825
2996
 
@@ -2853,12 +3024,12 @@ window.__ModuleLoader__.load({
2853
3024
  h('strong', { style: { fontSize: 14, lineHeight: 1.4 } }, '本版更新'),
2854
3025
  h('p', { style: mutedTextStyle }, bundleRelease.release_notes)) : null,
2855
3026
  ['incompatible', 'unknown'].includes(serviceCompatibility.state) ? h('p', { role: 'status', style: {
2856
- ...mutedTextStyle,
3027
+ ...feedbackNoticeStyle,
2857
3028
  color: serviceCompatibility.state === 'incompatible' ? 'var(--dsw-alias-state-error-primary)'
2858
3029
  : 'var(--dsw-alias-state-warning-primary, var(--dsw-alias-label-secondary))',
2859
3030
  } }, `服务端兼容性:${serviceCompatibility.message}`) : null,
2860
3031
  bundleUpdate.message && updateStatusNeedsDetails.includes(bundleUpdate.state) ? h('p', { role: 'status', style: bundleUpdate.state === 'revoked'
2861
- ? { ...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,
2862
3033
  !canCheckUpdate && updateGuideUrl ? h('a', { href: updateGuideUrl, target: '_blank', rel: 'noopener noreferrer',
2863
3034
  style: { color: 'var(--dsw-alias-link)', fontSize: 13 } }, '打开更新指引 ↗') : null,
2864
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.14",
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": {