@ai-wendao/ant 0.4.15 → 0.5.1

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 (4) hide show
  1. package/README.md +8 -2
  2. package/client.js +406 -81
  3. package/index.js +37 -0
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -4,10 +4,16 @@ 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
 
11
+ ## 企业品牌
12
+
13
+ 团队管理员可在 ANT 管理台的“企业品牌”页设置 DSH 侧栏 Logo、品牌名称、品牌徽标和新会话标题。Logo 接受 PNG、JPEG 或 WebP,最大 350 KB;图片和文字保存在当前团队的 ANT 服务端。成员 Bundle 启动时立即同步,DSH 保持打开时每 5 分钟通过 ETag 检查版本;网络暂时不可用时继续使用本机最近一次成功配置,不阻止 DSH 启动。
14
+
15
+ Logo 与侧栏品牌使用 DSH 正式 UI 插槽。新会话标题目前没有官方文本插槽,Bundle 只在页面中精确匹配 DSH 当前标题原文后替换;上游更改该文案时可能需要更新 ANT Bundle。此项不影响会话和工具功能,也不控制 DSH 原生欢迎/登录窗口、菜单或应用图标。
16
+
11
17
  飞书和企业微信的机器人连接、消息收发由 DSH-IM 负责;ANT Bundle 不保存机器人密钥或直连 IM 平台。ANT 继续负责团队成员身份和模型授权,并可在成员显式开启时接管一个专用飞书机器人的文本收件,按成员授权范围处理协作请求。
12
18
 
13
19
  ## 协作机器人与任务卡
@@ -26,7 +32,7 @@ PR 快照任务使用已核验的 PR Head SHA 创建独立工作树。Codex 使
26
32
 
27
33
  ## 企业精选技能
28
34
 
29
- 团队管理员可在 ANT 管理台从 GitHub 目录、ZIP、TAR、TAR.GZ/TGZ 或单份 `SKILL.md` 导入技能,核对来源、文件和依赖后发布。已连接团队的成员从 DSH 左侧“企业扩展”入口进入统一目录,默认同时浏览技能和插件,并可按类型、状态筛选或搜索;ANT 插件设置页只处理连接、模型和更新。ANT Host 将固定版本的完整文件集放进 DSH 的用户技能目录,并重新核对本机文件;启用本地技能的 DSH 会话随后由原生技能提供者发现和使用。ANT 不用 Host 全局技能列表判断会话是否已发现技能。
35
+ 团队管理员可在 ANT 管理台从 GitHub 目录、ZIP、TAR、TAR.GZ/TGZ 或单份 `SKILL.md` 导入技能,核对来源、文件和依赖后发布,并为企业技能和插件指定主要用途类别。成员从 DSH 左侧“企业扩展”进入目录,可组合筛选办公文档、编程开发、设计与多媒体、数据分析、协作沟通、写作与营销、自动化与效率、运维与安全或其他类别,同时按来源范围、技能/插件类型和安装状态筛选或搜索。类别只用于发现内容;“本企业”“全市场精选”和“DSH 官方”是独立来源范围。ANT 插件设置页只处理连接、模型和更新。ANT Host 将固定版本的完整文件集放进 DSH 的用户技能目录,并重新核对本机文件;启用本地技能的 DSH 会话随后由原生技能提供者发现和使用。ANT 不用 Host 全局技能列表判断会话是否已发现技能。
30
36
 
31
37
  安装不会自动运行技能脚本或安装 Python、Node 等系统依赖。ANT 只更新或卸载自己管理且本机内容未被修改的技能目录;发现同名个人技能或本机改动时会停止覆盖。
32
38
 
package/client.js CHANGED
@@ -12,7 +12,10 @@ window.__ModuleLoader__.load({
12
12
  return value.replace(suffix, '').trim() || 'OpenAI'
13
13
  }
14
14
  const SERVICE_URL_STORAGE_KEY = 'ant-team-service-url'
15
+ const BRANDING_CACHE_PREFIX = 'ant-team-branding:'
16
+ const BRANDING_SYNC_PATH = '/api/ant-teams/branding'
15
17
  const PROVIDER_ROUTES_STORAGE_KEY = 'ant-team-provider-routes'
18
+ const PROVIDER_SYNC_EXCLUSIONS_KEY = 'ant-team-provider-sync-exclusions'
16
19
  const DEVICE_ID_STORAGE_KEY = 'ant-team-device-id'
17
20
  const DEFAULT_MODEL_INITIALIZED_KEY = 'ant-team-default-model-initialized'
18
21
  const ANT_BUNDLE_PACKAGE_NAME = '@ai-wendao/ant'
@@ -31,32 +34,41 @@ window.__ModuleLoader__.load({
31
34
  } catch { return 'connection' }
32
35
  }
33
36
  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': '策划',
37
+ const extensionCategories = [
38
+ ['office', '办公文档'], ['development', '编程开发'], ['design', '设计与多媒体'],
39
+ ['data', '数据分析'], ['collaboration', '协作沟通'], ['content', '写作与营销'],
40
+ ['automation', '自动化与效率'], ['operations', '运维与安全'], ['other', '其他'],
41
+ ]
42
+ const extensionCategoryLabels = Object.fromEntries(extensionCategories)
43
+ const legacyMarketSkillCategories = {
44
+ 'ant-ppt-master': 'office', 'ant-frontend-slides': 'design', 'ant-slidev': 'development',
45
+ 'ant-word-docx': 'office', 'ant-excel-xlsx': 'office', 'ant-pdf': 'office',
46
+ 'ant-internal-comms': 'collaboration', 'ant-formatted-report-writer': 'office',
47
+ 'ant-agent-browser': 'automation', 'ant-vercel-react-best-practices': 'development',
48
+ 'ant-vercel-composition-patterns': 'development', 'ant-supabase-postgres-best-practices': 'data',
49
+ 'ant-diagnosing-bugs': 'development', 'ant-improve-codebase-architecture': 'development',
50
+ 'ant-code-review': 'development', 'ant-systematic-debugging': 'development',
51
+ 'ant-test-driven-development': 'development', 'ant-verification-before-completion': 'development',
52
+ 'ant-writing-plans': 'development', 'ant-to-spec': 'development', 'ant-skill-creator': 'automation',
53
+ 'ant-frontend-design': 'design', 'ant-web-design-guidelines': 'design',
54
+ 'ant-ui-ux-pro-max': 'design', 'ant-emil-design-eng': 'design',
55
+ 'ant-review-animations': 'design', 'ant-shadcn': 'design',
56
+ 'ant-theme-factory': 'design', 'ant-brand-guidelines': 'design',
57
+ 'ant-copywriting': 'content', 'ant-seo-audit': 'content', 'ant-brainstorming': 'content',
49
58
  }
59
+ const categoryOf = (entry) => extensionCategoryLabels[entry?.category]
60
+ ? entry.category : legacyMarketSkillCategories[entry?.dsh_name] || 'other'
61
+ const categoryLabel = (category) => extensionCategoryLabels[category] || extensionCategoryLabels.other
50
62
  const officialOfficeGuide = 'https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.1-rc.2/packages/skill/skill-office/README.zh.md'
51
63
  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
64
  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' },
65
+ { name: 'office', title: 'Office 办公入口', category: 'office', since: '0.2.1-rc.2', description: '根据任务选择 Word、PPT、Excel 或文件处理能力。', prompt: '帮我完成这份办公文件' },
66
+ { name: 'office-docx', title: 'Word 文档', category: 'office', since: '0.2.0-rc.2', description: '创建、修改和检查 DOCX 文档。', prompt: '帮我写一份 Word 报告' },
67
+ { name: 'office-pptx', title: 'PPT 演示', category: 'office', since: '0.2.0-rc.2', description: '创建、修改和检查 PPTX 演示文稿。', prompt: '帮我做一份项目汇报 PPT' },
68
+ { name: 'office-xlsx', title: 'Excel 表格', category: 'office', since: '0.2.0-rc.2', description: '创建、修改和检查 XLSX 工作簿。', prompt: '帮我整理这份 Excel 表格' },
69
+ { name: 'office-render', title: '文件预览', category: 'office', since: '0.2.1-rc.2', description: '把 Office 文件渲染成图片,检查实际外观。', prompt: '请预览这份文档并检查排版' },
70
+ { name: 'office-recalculate', title: '公式重算', category: 'office', since: '0.2.1-rc.2', description: '重新计算表格公式并核对结果。', prompt: '请重新计算这份表格的公式' },
71
+ { name: 'office-export-pdf', title: '导出 PDF', category: 'office', since: '0.2.1-rc.2', description: '将 Office 文件导出为 PDF。', prompt: '把这份 Word 文档导出成 PDF' },
60
72
  ]
61
73
  const curatedSkillIntroductions = {
62
74
  "ant-ppt-master": {"ref": "c350bc31e6d43b7bcf5234183d650ba8f9f4c9d7", "description": "制作和编辑 PowerPoint 演示文稿。"},
@@ -115,7 +127,7 @@ window.__ModuleLoader__.load({
115
127
  }
116
128
  return null
117
129
  }
118
- const skillCategory = (skill) => marketSkillCategories[skill.dsh_name] || '其他'
130
+ const skillCategory = (skill) => categoryOf(skill)
119
131
  const panelStyle = {
120
132
  display: 'grid', alignContent: 'start', gap: 14, padding: 18,
121
133
  border: '0.5px solid var(--dsw-alias-border-l2)', borderRadius: 14,
@@ -128,6 +140,9 @@ window.__ModuleLoader__.load({
128
140
  borderLeft: '3px solid var(--dsw-alias-state-business-primary)',
129
141
  }
130
142
  const mutedTextStyle = { margin: 0, color: 'var(--dsw-alias-label-secondary)', fontSize: 13, lineHeight: 1.6 }
143
+ const feedbackNoticeStyle = { ...mutedTextStyle, position: 'sticky', top: 0, zIndex: 8,
144
+ padding: '9px 12px', border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8,
145
+ backgroundColor: 'var(--dsw-alias-bg-base)', boxShadow: '0 4px 14px #173d3510', overflowWrap: 'anywhere' }
131
146
  const sectionTitleStyle = { margin: 0, fontSize: 16, fontWeight: 650, lineHeight: 1.4 }
132
147
  const inputStyle = {
133
148
  width: '100%', boxSizing: 'border-box', padding: '10px 12px',
@@ -393,9 +408,8 @@ window.__ModuleLoader__.load({
393
408
  binding.state === 'checking' ? h('p', { role: 'status', style: mutedTextStyle }, '正在核对这台 DSH 绑定的团队…') : null,
394
409
  binding.state === 'unverified' ? h('p', { role: 'status', style: mutedTextStyle }, '本机仍有 ANT 凭证,但无法核对所属团队。请联系原团队管理员核查并撤销旧设备;确认后可在下方断开或仅清理本机。') : null,
395
410
  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))
411
+ message ? h('p', { role: 'status', style: feedbackNoticeStyle }, message) : null,
412
+ link ? h('a', { href: link, target: '_blank', rel: 'noopener noreferrer', style: { color: 'var(--dsw-alias-link)', fontSize: 13 } }, '打开团队管理 ↗') : null)
399
413
  return h('section', { style: { display: 'grid', gap: 16, width: '100%', maxWidth: 1120 } },
400
414
  h('div', { style: { display: 'grid', gap: 4 } },
401
415
  h('h2', { style: { margin: 0, fontSize: 20, fontWeight: 650, lineHeight: 1.35 } }, '团队与协作'),
@@ -1220,7 +1234,7 @@ window.__ModuleLoader__.load({
1220
1234
  return h('section', { style: { ...settingsPanelStyle,
1221
1235
  display: ['collaboration', 'local'].includes(activeTab) ? 'grid' : 'none' },
1222
1236
  'aria-label': '群聊协作设置' },
1223
- message ? h('p', { role: 'status', style: mutedTextStyle }, message) : null,
1237
+ message ? h('p', { role: 'status', style: feedbackNoticeStyle }, message) : null,
1224
1238
  h('div', { id: 'ant-chat-entry', role: 'region', 'aria-label': '聊天入口和机器人状态', tabIndex: -1,
1225
1239
  style: { display: activeTab === 'collaboration' ? 'grid' : 'none', gap: 12 } },
1226
1240
  h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'start', gap: 12, flexWrap: 'wrap' } },
@@ -1588,8 +1602,6 @@ window.__ModuleLoader__.load({
1588
1602
  && (scopeFilter === 'all' || scopeFilter === 'official')
1589
1603
  && (categoryFilter === 'all' || skill.category === categoryFilter)
1590
1604
  })
1591
- const categories = [...new Set([...skills.map(skillCategory), ...officialOfficeSkills.map((skill) => skill.category)])]
1592
- .sort((a, b) => a.localeCompare(b, 'zh-CN'))
1593
1605
  const statusLabel = { installed: '已放入本机目录', 'update-available': '可更新', 'not-installed': '未安装', conflict: '本机冲突' }
1594
1606
  if (originalView) return h('section', { 'aria-label': '技能原文', style: {
1595
1607
  height: '100%', overflowY: 'auto', boxSizing: 'border-box',
@@ -1638,19 +1650,20 @@ window.__ModuleLoader__.load({
1638
1650
  h('button', { key: value, type: 'button', onClick: () => setFilter(value), 'aria-pressed': filter === value,
1639
1651
  style: { ...secondaryButtonStyle, minHeight: 34, padding: '6px 12px', backgroundColor: filter === value ? 'var(--dsw-alias-bg-layer-2)' : secondaryButtonStyle.backgroundColor } }, label)),
1640
1652
  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 } },
1653
+ h('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 } },
1642
1654
  h('span', { style: mutedTextStyle }, '来源'),
1643
1655
  ...[['all', '全部'], ['official', 'DSH 官方'], ['market', '全市场精选'], ['team', '本企业']].map(([value, label]) =>
1644
1656
  h('button', { key: value, type: 'button', onClick: () => setScopeFilter(value), 'aria-pressed': scopeFilter === value,
1645
1657
  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 } },
1658
+ h('label', { style: { ...mutedTextStyle, marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 6 } }, '用途类别',
1659
+ h('select', { value: categoryFilter, onChange: (event) => setCategoryFilter(event.target.value),
1660
+ 'aria-label': '筛选扩展用途类别', style: { ...inputStyle, width: 'auto', minHeight: 34 } },
1648
1661
  h('option', { value: 'all' }, '全部用途'),
1649
- ...categories.map((category) => h('option', { key: category, value: category }, category))))),
1650
- ),
1662
+ ...extensionCategories.map(([category, label]) => h('option', { key: category, value: category }, label)))),
1663
+ )),
1651
1664
  includePlugins ? h('h2', { style: { margin: 0, fontSize: 20 } }, loading ? '技能 · 读取中…' : `技能 · ${visible.length + visibleOfficial.length}`) : null,
1652
1665
  message && (!includePlugins || loadError || skills.length || (!loading && !query.trim()))
1653
- ? h('p', { role: loadError ? 'alert' : 'status', style: mutedTextStyle }, message) : null,
1666
+ ? h('p', { role: loadError ? 'alert' : 'status', style: feedbackNoticeStyle }, message) : null,
1654
1667
  visible.length && !includePlugins ? h('h2', { style: { margin: 0, fontSize: 20 } }, '全市场与本企业技能') : null,
1655
1668
  visible.length ? h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 420px), 1fr))', gap: 16 } },
1656
1669
  ...visible.map((skill) => {
@@ -1677,7 +1690,7 @@ window.__ModuleLoader__.load({
1677
1690
  h('div', { style: { display: 'flex', alignItems: 'baseline', flexWrap: 'wrap', gap: 8 } },
1678
1691
  h('h2', { style: { margin: 0, fontSize: 19 } }, skill.title),
1679
1692
  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))),
1693
+ h('span', { style: mutedTextStyle }, categoryLabel(skillCategory(skill)))),
1681
1694
  h('span', { style: mutedTextStyle }, marketPending
1682
1695
  ? skill.team_connected ? '待企业启用' : '连接企业后查看启用状态'
1683
1696
  : statusLabel[skill.local_status] || '状态未知')),
@@ -1742,20 +1755,24 @@ window.__ModuleLoader__.load({
1742
1755
  h('div', { style: { display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', alignItems: 'baseline', gap: 8 } },
1743
1756
  h('h3', { style: { margin: 0, fontSize: 17 } }, skill.title),
1744
1757
  h('span', { style: { ...mutedTextStyle, padding: '2px 8px', borderRadius: 6,
1745
- backgroundColor: 'var(--dsw-alias-bg-layer-2)' } }, 'DSH 自带 · 无需在 ANT 安装')),
1758
+ backgroundColor: 'var(--dsw-alias-bg-layer-2)' } }, 'DSH 自带 · 无需在 ANT 安装'),
1759
+ h('span', { style: { ...mutedTextStyle, padding: '2px 7px', borderRadius: 6,
1760
+ backgroundColor: 'var(--dsw-alias-bg-layer-2)' } }, categoryLabel(skill.category))),
1746
1761
  h('p', { style: mutedTextStyle }, skill.description),
1747
1762
  h('p', { style: mutedTextStyle }, `在新会话中说:“${skill.prompt}”。也可输入 /${skill.name}。`),
1748
1763
  h('p', { style: mutedTextStyle }, `DSH ${skill.since} 起提供;如果无法使用,请在 DSH 检查更新。`),
1749
1764
  h('a', { href: officialOfficeEnglish(skill), target: '_blank', rel: 'noopener noreferrer',
1750
1765
  style: { color: 'var(--dsw-alias-link)', fontSize: 13, lineHeight: 1.6, padding: '4px 0' } }, '英文原文 ↗'))))) : null,
1751
1766
  includePlugins ? h(PluginCenterPage, { remote, onBack, onAll, onSkills, openBundle,
1752
- embedded: true, query, onQueryChange, externalFilter: filter,
1767
+ embedded: true, query, onQueryChange, externalFilter: filter, externalCategoryFilter: categoryFilter,
1768
+ externalScopeFilter: scopeFilter,
1753
1769
  hasSkillMatches: visible.length + visibleOfficial.length > 0, skillCatalogReady: !loading && !loadError,
1754
1770
  refreshToken }) : null))
1755
1771
  }
1756
1772
 
1757
1773
  function PluginCenterPage({ remote, onBack, onAll, onSkills, openBundle,
1758
1774
  embedded = false, query, onQueryChange, externalFilter = 'all',
1775
+ externalCategoryFilter = 'all', externalScopeFilter = 'all',
1759
1776
  hasSkillMatches = false, skillCatalogReady = false, refreshToken }) {
1760
1777
  const [plugins, setPlugins] = useState([])
1761
1778
  const [bundles, setBundles] = useState([])
@@ -1766,6 +1783,7 @@ window.__ModuleLoader__.load({
1766
1783
  const [completedId, setCompletedId] = useState('')
1767
1784
  const [message, setMessage] = useState('')
1768
1785
  const [filter, setFilter] = useState('all')
1786
+ const [categoryFilter, setCategoryFilter] = useState('all')
1769
1787
  const refreshGeneration = useRef(0)
1770
1788
  const refresh = async () => {
1771
1789
  const generation = ++refreshGeneration.current
@@ -1889,21 +1907,46 @@ window.__ModuleLoader__.load({
1889
1907
  } finally { setWorkingId('') }
1890
1908
  }
1891
1909
  const activeFilter = embedded ? externalFilter : filter
1910
+ const activeCategoryFilter = embedded ? externalCategoryFilter : categoryFilter
1911
+ const activeScopeFilter = embedded ? externalScopeFilter : 'team'
1912
+ const currentPluginByPackage = new Map()
1913
+ const publishedPluginPackages = new Set()
1914
+ for (const plugin of plugins) {
1915
+ if (!currentPluginByPackage.has(plugin.package_name)
1916
+ || plugin.status === 'published' && !publishedPluginPackages.has(plugin.package_name)) {
1917
+ currentPluginByPackage.set(plugin.package_name, plugin.id)
1918
+ }
1919
+ if (plugin.status === 'published') publishedPluginPackages.add(plugin.package_name)
1920
+ }
1921
+ if (activeFilter === 'installed') {
1922
+ for (const plugin of plugins) {
1923
+ if (publishedPluginPackages.has(plugin.package_name)) continue
1924
+ const installed = bundles.find((bundle) => bundle.name === plugin.package_name && bundle.installed)
1925
+ if (!installed?.version || installed.version !== plugin.version) continue
1926
+ currentPluginByPackage.set(plugin.package_name, plugin.id)
1927
+ }
1928
+ }
1892
1929
  const visible = plugins.filter((plugin) => {
1930
+ const isCurrentVersion = currentPluginByPackage.get(plugin.package_name) === plugin.id
1893
1931
  const installed = bundles.find((bundle) => bundle.name === plugin.package_name && bundle.installed)
1894
1932
  const artifactUrl = approvedPluginArtifactUrl(plugin)
1895
1933
  const installSpec = pluginInstallSpec(plugin)
1896
1934
  const localAhead = installed?.version && newerVersion(installed.version, plugin.version)
1897
1935
  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')
1936
+ return matches && (activeScopeFilter === 'all' || activeScopeFilter === 'team')
1937
+ && (activeCategoryFilter === 'all' || categoryOf(plugin) === activeCategoryFilter)
1938
+ && (activeFilter === 'retired'
1939
+ ? plugin.status !== 'published'
1940
+ : isCurrentVersion && (activeFilter === 'all'
1941
+ || activeFilter === 'installed' && Boolean(installed)
1942
+ || activeFilter === 'available' && plugin.status === 'published' && installSpec && !localAhead && (!installed || (installed.version && installed.version !== plugin.version))
1943
+ || activeFilter === 'updates' && plugin.status === 'published' && installSpec && installed && installed.version && installed.version !== plugin.version && !localAhead)
1944
+ )
1902
1945
  })
1903
1946
  const noMatches = embedded
1904
1947
  ? skillCatalogReady ? '没有找到符合条件的企业扩展。' : '插件中没有符合条件的结果。'
1905
1948
  : activeFilter === 'available'
1906
- ? '当前没有可一键安装的插件。可在“全部”查看审核目录,或联系企业管理员。'
1949
+ ? '当前没有可一键安装的插件。可在“目录概览”查看审核目录,或联系企业管理员。'
1907
1950
  : '没有符合筛选条件的插件。'
1908
1951
  return h('section', { 'aria-label': '企业精选插件', style: embedded
1909
1952
  ? { display: 'grid', gap: 16 }
@@ -1926,15 +1969,20 @@ window.__ModuleLoader__.load({
1926
1969
  : h('section', { role: 'status', style: panelStyle },
1927
1970
  h('h2', { style: { margin: 0, fontSize: 18 } }, '团队暂未发布精选插件'),
1928
1971
  h('p', { style: mutedTextStyle }, '需要某个插件?请联系企业管理员,说明用途或提供插件地址。审核发布后会显示在这里。')) : null,
1929
- !loading && loadError ? h('p', { role: 'alert', style: mutedTextStyle }, message) : null,
1972
+ !loading && loadError ? h('p', { role: 'alert', style: feedbackNoticeStyle }, message) : null,
1930
1973
  plugins.length && !embedded ? h(ExtensionRequestNotice) : null,
1931
1974
  plugins.length && !embedded ? h('div', { style: panelStyle },
1932
1975
  h('input', { type: 'search', value: query, onChange: (event) => onQueryChange(event.target.value), placeholder: '搜索插件名称或用途', 'aria-label': '搜索企业精选插件', style: inputStyle }),
1933
1976
  h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8 } },
1934
- ...[['all', '全部'], ['available', '可安装或升级'], ['installed', '已安装'], ['retired', '已废弃']].map(([value, label]) =>
1977
+ ...[['all', '目录概览'], ['available', '可安装或升级'], ['installed', '已安装'], ['retired', '已废弃']].map(([value, label]) =>
1935
1978
  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,
1979
+ style: { ...secondaryButtonStyle, minHeight: 34, backgroundColor: filter === value ? 'var(--dsw-alias-bg-layer-2)' : secondaryButtonStyle.backgroundColor } }, label)),
1980
+ h('label', { style: { ...mutedTextStyle, marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 6 } }, '用途类别',
1981
+ h('select', { value: categoryFilter, onChange: (event) => setCategoryFilter(event.target.value),
1982
+ 'aria-label': '筛选插件用途类别', style: { ...inputStyle, width: 'auto', minHeight: 34 } },
1983
+ h('option', { value: 'all' }, '全部用途'),
1984
+ ...extensionCategories.map(([category, label]) => h('option', { key: category, value: category }, label)))))) : null,
1985
+ plugins.length && message ? h('p', { role: 'status', style: feedbackNoticeStyle }, message) : null,
1938
1986
  visible.length ? h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 420px), 1fr))', gap: 16 } },
1939
1987
  ...visible.map((plugin) => {
1940
1988
  const local = bundles.find((bundle) => bundle.name === plugin.package_name && bundle.installed)
@@ -1947,7 +1995,7 @@ window.__ModuleLoader__.load({
1947
1995
  const verifiedLocal = sameVersion && artifactUrl && sourceKnown && local.source === artifactUrl
1948
1996
  const recordedInstall = sameVersion && !sourceKnown && previouslyInstalledReviewedPlugin(plugin)
1949
1997
  const status = security ? '已废弃(安全风险)'
1950
- : plugin.status === 'deprecated' ? '已废弃' : local?.error ? '加载异常' : localAhead
1998
+ : plugin.status === 'deprecated' ? local?.version ? `已废弃 · 本机 ${local.version}` : '已废弃' : local?.error ? '加载异常' : localAhead
1951
1999
  ? '本机版本高于团队目录版本' : sameVersion
1952
2000
  ? verifiedLocal ? local.enabled ? '本机已装(团队来源,已启用)' : '本机已装(团队来源,未启用)'
1953
2001
  : sourceKnown ? '本机同版,来源不同' : recordedInstall
@@ -1959,7 +2007,9 @@ window.__ModuleLoader__.load({
1959
2007
  h('div', { style: { display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', gap: 8 } },
1960
2008
  h('div', { style: { display: 'flex', alignItems: 'baseline', flexWrap: 'wrap', gap: 8 } },
1961
2009
  h('h2', { style: { margin: 0, fontSize: 19 } }, plugin.title),
1962
- embedded ? h('span', { style: mutedTextStyle }, '插件') : null),
2010
+ embedded ? h('span', { style: mutedTextStyle }, '插件') : null,
2011
+ h('span', { style: { ...mutedTextStyle, padding: '2px 7px', borderRadius: 6,
2012
+ backgroundColor: 'var(--dsw-alias-bg-layer-2)' } }, categoryLabel(categoryOf(plugin)))),
1963
2013
  h('span', { style: security
1964
2014
  ? { ...mutedTextStyle, color: 'var(--dsw-alias-label-danger, #d44)', fontWeight: 650 } : mutedTextStyle }, status)),
1965
2015
  h('p', { style: mutedTextStyle }, plugin.description),
@@ -2010,6 +2060,9 @@ window.__ModuleLoader__.load({
2010
2060
  const [bundleRelease, setBundleRelease] = useState(null)
2011
2061
  const [bundleUpdate, setBundleUpdate] = useState({ state: 'idle', currentVersion: '', message: '' })
2012
2062
  const [authorizedModels, setAuthorizedModels] = useState([])
2063
+ const [providerSyncGroups, setProviderSyncGroups] = useState([])
2064
+ const [appliedProviderSyncExclusions, setAppliedProviderSyncExclusions] = useState([])
2065
+ const [providerSyncDraft, setProviderSyncDraft] = useState([])
2013
2066
  const [serverDefaultModel, setServerDefaultModel] = useState(null)
2014
2067
  const [dshDefaultSelection, setDshDefaultSelection] = useState(null)
2015
2068
  const [defaultModelDraft, setDefaultModelDraft] = useState('')
@@ -2095,17 +2148,27 @@ window.__ModuleLoader__.load({
2095
2148
  : `ant-team-openai-${model.provider_id}`
2096
2149
  const modelSelectionKey = (model) => JSON.stringify({ provider_id: model.provider_id || '', protocol: model.protocol, model_id: model.model_id })
2097
2150
  const defaultPreferenceKey = (rawServiceUrl) => `${DEFAULT_MODEL_INITIALIZED_KEY}:${normalizeServiceUrl(rawServiceUrl)}:${savedDeviceId() || ''}`
2151
+ const providerSyncPreferenceKey = (rawServiceUrl) => `${PROVIDER_SYNC_EXCLUSIONS_KEY}:${normalizeServiceUrl(rawServiceUrl)}:${savedDeviceId() || ''}`
2098
2152
  const readDefaultPreference = (rawServiceUrl) => {
2099
2153
  try { return JSON.parse(localStorage.getItem(defaultPreferenceKey(rawServiceUrl)) || 'null') } catch { return null }
2100
2154
  }
2101
2155
  const saveDefaultPreference = (rawServiceUrl, value) => {
2102
2156
  try { localStorage.setItem(defaultPreferenceKey(rawServiceUrl), JSON.stringify(value)) } catch {}
2103
2157
  }
2104
- const modelForProviderSelection = (selection, models) => {
2158
+ const modelForProviderSelection = (selection, models, preferredModel = null) => {
2105
2159
  if (!selection) return null
2106
2160
  const exact = models.find((model) => model.model_id === selection.model && modelProvider(model) === selection.provider)
2107
2161
  if (exact) return exact
2108
- const legacyProvider = ['ant-team-openai', 'ant-team-openai-chat-completions', 'ant-team-openai-responses'].includes(selection.provider)
2162
+ if (['ant-team-openai', 'ant-team-openai-chat-completions'].includes(selection.provider)) {
2163
+ const chatCandidates = models.filter((model) => model.protocol === 'openai'
2164
+ && model.openai_api_mode !== 'responses' && model.model_id === selection.model)
2165
+ const preferredChat = preferredModel && chatCandidates.find((model) =>
2166
+ modelSelectionKey(model) === modelSelectionKey(preferredModel))
2167
+ if (preferredChat) return preferredChat
2168
+ const canonicalChat = chatCandidates.find((model) => model.provider_id === 'openai-default')
2169
+ return canonicalChat || (chatCandidates.length === 1 ? chatCandidates[0] : null)
2170
+ }
2171
+ const legacyProvider = selection.provider === 'ant-team-openai-responses'
2109
2172
  ? 'openai-default' : selection.provider === 'ant-team-anthropic' ? 'anthropic-default' : null
2110
2173
  return legacyProvider ? models.find((model) => model.provider_id === legacyProvider && model.model_id === selection.model) : null
2111
2174
  }
@@ -2131,8 +2194,8 @@ window.__ModuleLoader__.load({
2131
2194
  return selection
2132
2195
  }
2133
2196
  const applyUserDefaultModel = async (model) => {
2134
- if (!model || !authorizedModels.some((item) => modelSelectionKey(item) === modelSelectionKey(model))) {
2135
- setDefaultModelMessage('请选择当前已获准的模型。')
2197
+ if (!model || !syncedModels.some((item) => modelSelectionKey(item) === modelSelectionKey(model))) {
2198
+ setDefaultModelMessage('请选择当前已同步到 DSH 的模型。')
2136
2199
  return
2137
2200
  }
2138
2201
  setDefaultModelBusy(true)
@@ -2149,7 +2212,7 @@ window.__ModuleLoader__.load({
2149
2212
  setDefaultModelMessage(error instanceof Error ? error.message : '保存本机默认模型失败。')
2150
2213
  } finally { setDefaultModelBusy(false) }
2151
2214
  }
2152
- const applyAuthorizedModels = async (models, rawServiceUrl, rawDefaultModel = null) => {
2215
+ const applyAuthorizedModels = async (models, rawServiceUrl, rawDefaultModel = null, options = {}) => {
2153
2216
  const normalized = normalizeServiceUrl(rawServiceUrl)
2154
2217
  if (!normalized || !Array.isArray(models)) throw new Error('ANT 返回的团队模型目录格式无效。')
2155
2218
  if (typeof remote?.settings?.mutate !== 'function') throw new Error('当前 DSH 版本未提供模型设置接口。')
@@ -2183,6 +2246,8 @@ window.__ModuleLoader__.load({
2183
2246
  : provider.providerId === 'anthropic-default'
2184
2247
  ? 'ant-team-anthropic' : `ant-team-${provider.protocol}-${provider.providerId}`
2185
2248
  const routeConfigurations = new Map()
2249
+ const compatibilityRoutes = []
2250
+ const providerGroups = []
2186
2251
  for (const provider of providers) {
2187
2252
  const route = routeForProvider(provider)
2188
2253
  const api = provider.protocol === 'openai'
@@ -2200,9 +2265,17 @@ window.__ModuleLoader__.load({
2200
2265
  baseURL,
2201
2266
  models: provider.models.map((model) => ({ id: model.model_id, name: model.display_name })),
2202
2267
  })
2268
+ providerGroups.push({
2269
+ key: route,
2270
+ label: `${provider.providerName} · ${provider.protocol === 'anthropic' ? 'Anthropic Messages' : provider.apiMode === 'responses' ? 'OpenAI Responses' : 'OpenAI Chat Completions'}`,
2271
+ routes: [route],
2272
+ modelCount: provider.models.length,
2273
+ legacy: false,
2274
+ })
2203
2275
  if (provider.protocol === 'openai' && provider.providerId === 'openai-default') {
2204
2276
  const aliases = provider.apiMode === 'responses'
2205
2277
  ? ['ant-team-openai-chat-completions', 'ant-team-openai'] : ['ant-team-openai']
2278
+ compatibilityRoutes.push(...aliases)
2206
2279
  const legacyChat = {
2207
2280
  displayName: `${provider.providerName} · Chat(旧会话兼容)`,
2208
2281
  apiKeyEnv: 'ANT_MODEL_API_KEY',
@@ -2213,13 +2286,56 @@ window.__ModuleLoader__.load({
2213
2286
  for (const alias of aliases) routeConfigurations.set(alias, legacyChat)
2214
2287
  }
2215
2288
  }
2216
- const routes = [...routeConfigurations.keys()]
2217
2289
  const ops = []
2218
2290
  let previousRoutes = []
2219
2291
  try {
2220
2292
  const parsed = JSON.parse(localStorage.getItem(PROVIDER_ROUTES_STORAGE_KEY) || '[]')
2221
2293
  if (Array.isArray(parsed)) previousRoutes = parsed.filter((route) => typeof route === 'string')
2222
2294
  } catch {}
2295
+ const compatibilityGroup = compatibilityRoutes.length ? [{
2296
+ key: 'legacy-openai-chat', label: 'ANT OpenAI · Chat(旧会话兼容)', routes: compatibilityRoutes,
2297
+ modelCount: providers.find((provider) => provider.providerId === 'openai-default')?.models.length || 0,
2298
+ legacy: true,
2299
+ }] : []
2300
+ const allGroups = [...providerGroups, ...compatibilityGroup]
2301
+ let excludedGroups = options.excludedGroups
2302
+ if (!Array.isArray(excludedGroups)) {
2303
+ try {
2304
+ const saved = JSON.parse(localStorage.getItem(providerSyncPreferenceKey(normalized)) || 'null')
2305
+ if (Array.isArray(saved)) excludedGroups = saved
2306
+ } catch {}
2307
+ }
2308
+ if (!Array.isArray(excludedGroups)) {
2309
+ // Existing installs keep their current legacy alias group; first-time installs get all routes.
2310
+ excludedGroups = previousRoutes.length && compatibilityRoutes.length
2311
+ && !compatibilityRoutes.some((route) => previousRoutes.includes(route)) ? ['legacy-openai-chat'] : []
2312
+ }
2313
+ excludedGroups = [...new Set(excludedGroups.filter((key) => allGroups.some((group) => group.key === key)))]
2314
+ const excludedGroupSet = new Set(excludedGroups)
2315
+ const excluded = new Set(allGroups.filter((group) => excludedGroupSet.has(group.key)).flatMap((group) => group.routes))
2316
+ const routes = [...routeConfigurations.keys()].filter((route) => !excluded.has(route))
2317
+ const syncedModels = models.filter((model) => !excluded.has(modelProvider(model)))
2318
+ if (models.length && !syncedModels.length) throw new Error('请至少保留一个常规模型 provider 同步到本机 DSH。')
2319
+ const defaultMatch = rawDefaultModel && models.find((model) =>
2320
+ (rawDefaultModel.provider_id ? model.provider_id === rawDefaultModel.provider_id : model.protocol === rawDefaultModel.protocol)
2321
+ && model.protocol === rawDefaultModel.protocol && model.model_id === rawDefaultModel.model_id)
2322
+ const syncedDefaultMatch = defaultMatch && syncedModels.some((model) => modelSelectionKey(model) === modelSelectionKey(defaultMatch))
2323
+ ? defaultMatch : null
2324
+ const serverDefault = syncedDefaultMatch || null
2325
+ const initialDefault = syncedDefaultMatch || (rawDefaultModel == null ? syncedModels[0] : null)
2326
+ if (Array.isArray(options.excludedGroups)) {
2327
+ const intentionallyRemovedRoutes = [...excluded].filter((route) => previousRoutes.includes(route))
2328
+ if (intentionallyRemovedRoutes.length && !window.confirm(
2329
+ '关闭这些 provider 会从本机 DSH 移除对应模型路由。保存了这些路由的历史会话可能暂时无法继续;重新开启同步可以恢复。仍要应用吗?')) {
2330
+ throw new Error('已取消同步范围更改;本机 DSH 设置未修改。')
2331
+ }
2332
+ const currentSelection = await readDshDefaultSelection()
2333
+ if (currentSelection?.provider && excluded.has(currentSelection.provider)) {
2334
+ const replacement = modelForProviderSelection(currentSelection, syncedModels, serverDefault) || serverDefault || syncedModels[0]
2335
+ if (!replacement) throw new Error('请先选择一个保留同步的模型,再移除当前 DSH 默认 provider。')
2336
+ await writeDshDefaultModel(replacement)
2337
+ }
2338
+ }
2223
2339
  const knownRoutes = new Set([...previousRoutes, 'ant-team-openai', 'ant-team-openai-chat-completions',
2224
2340
  'ant-team-openai-responses', 'ant-team-anthropic'])
2225
2341
  for (const route of knownRoutes) {
@@ -2231,11 +2347,10 @@ window.__ModuleLoader__.load({
2231
2347
  const settingsWrite = await remote.settings.mutate('llm-pi-ai', ops, undefined)
2232
2348
  if (!settingsWrite.ok) throw new Error(settingsWrite.error?.message || 'DSH 无法更新团队模型设置。')
2233
2349
  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)
2350
+ try { localStorage.setItem(providerSyncPreferenceKey(normalized), JSON.stringify(excludedGroups)) } catch {}
2351
+ setProviderSyncGroups(allGroups)
2352
+ setAppliedProviderSyncExclusions(excludedGroups)
2353
+ setProviderSyncDraft(excludedGroups)
2239
2354
  setServerDefaultModel(serverDefault)
2240
2355
  const preference = readDefaultPreference(normalized)
2241
2356
  let statusMessage = ''
@@ -2249,8 +2364,8 @@ window.__ModuleLoader__.load({
2249
2364
  : '当前无法使用团队默认模型;已用首个获准模型初始化 DSH 全局默认。请管理员检查默认设置和成员授权。'
2250
2365
  } else {
2251
2366
  selection = await readDshDefaultSelection()
2252
- const currentAuthorized = modelForProviderSelection(selection, models)
2253
- const baseline = models.find((model) => modelSelectionKey(model) === preference?.serverDefault)
2367
+ const currentAuthorized = modelForProviderSelection(selection, syncedModels, serverDefault)
2368
+ const baseline = syncedModels.find((model) => modelSelectionKey(model) === preference?.serverDefault)
2254
2369
  const stillUsingBaseline = baseline && currentAuthorized && modelSelectionKey(currentAuthorized) === modelSelectionKey(baseline)
2255
2370
  if (currentAuthorized && selection?.provider !== modelProvider(currentAuthorized)) {
2256
2371
  selection = await writeDshDefaultModel(currentAuthorized)
@@ -2259,8 +2374,8 @@ window.__ModuleLoader__.load({
2259
2374
  selection = await writeDshDefaultModel(serverDefault)
2260
2375
  saveDefaultPreference(normalized, { ...preference, initialized: true, serverDefault: modelSelectionKey(serverDefault) })
2261
2376
  statusMessage = '团队默认模型已更新;你尚未设置个人覆盖,因此已同步到 DSH 全局默认。'
2262
- } else if (selection?.provider?.startsWith('ant-team-') && !currentAuthorized && models.length) {
2263
- const fallback = serverDefault || models[0]
2377
+ } else if (selection?.provider?.startsWith('ant-team-') && !currentAuthorized && syncedModels.length) {
2378
+ const fallback = serverDefault || syncedModels[0]
2264
2379
  selection = await writeDshDefaultModel(fallback)
2265
2380
  saveDefaultPreference(normalized, { ...preference, initialized: true,
2266
2381
  serverDefault: modelSelectionKey(serverDefault || fallback) })
@@ -2268,11 +2383,11 @@ window.__ModuleLoader__.load({
2268
2383
  ? '当前默认模型已失去授权,已恢复为团队默认模型。'
2269
2384
  : '当前默认模型已失去授权,已改用首个获准模型;请管理员设置团队默认模型。'
2270
2385
  }
2271
- const effectiveAuthorized = modelForProviderSelection(selection, models)
2386
+ const effectiveAuthorized = modelForProviderSelection(selection, syncedModels)
2272
2387
  setDshDefaultSelection(selection)
2273
2388
  setDefaultModelDraft(effectiveAuthorized ? modelSelectionKey(effectiveAuthorized) : '')
2274
2389
  if (!serverDefault && !statusMessage) statusMessage = rawDefaultModel
2275
- ? '团队默认模型不在这台设备的获准范围内;请联系管理员调整模型权限。'
2390
+ ? '团队默认模型当前未同步到这台 DSH;请在下方选择一个已同步模型作为本机默认。'
2276
2391
  : '团队默认模型尚未设置;请联系管理员设置,或在下方选择本机可用的默认模型。'
2277
2392
  else if (!statusMessage) statusMessage = effectiveAuthorized
2278
2393
  ? '已保留你选择的 DSH 全局默认模型。'
@@ -2281,10 +2396,10 @@ window.__ModuleLoader__.load({
2281
2396
  setDefaultModelMessage(statusMessage)
2282
2397
  try { localStorage.setItem(SERVICE_URL_STORAGE_KEY, normalized) } catch {}
2283
2398
  onServiceUrlChange(normalized)
2284
- return { count: models.length, defaultSelection: selection }
2399
+ return { count: syncedModels.length, defaultSelection: selection }
2285
2400
  }
2286
2401
 
2287
- const refreshModels = async () => {
2402
+ const refreshModels = async (applyDraft = false) => {
2288
2403
  if (!bindingReady) {
2289
2404
  setMessage('请先等待团队绑定检查完成,再刷新团队模型。')
2290
2405
  return
@@ -2301,13 +2416,14 @@ window.__ModuleLoader__.load({
2301
2416
  if (response.status === 401) setNeedsReconnect(true)
2302
2417
  throw new Error(result.message || `刷新失败(${response.status})`)
2303
2418
  }
2304
- const sync = await applyAuthorizedModels(result.models, result.service_url, result.default_model)
2419
+ const sync = await applyAuthorizedModels(result.models, result.service_url, result.default_model,
2420
+ applyDraft ? { excludedGroups: providerSyncDraft } : {})
2305
2421
  setAuthorizedModels(result.models)
2306
2422
  setLastModelSync(new Date())
2307
2423
  setCredentialConfigured(true)
2308
2424
  setNeedsReconnect(false)
2309
2425
  setReauthenticate(false)
2310
- setModelMessage(`模型目录已刷新,共 ${sync.count} 个获准模型。`)
2426
+ setModelMessage(`模型目录已刷新,共同步 ${sync.count} 个获准模型。`)
2311
2427
  } catch (error) {
2312
2428
  setModelMessage(error instanceof Error ? error.message : '刷新模型失败,请稍后重试。')
2313
2429
  } finally {
@@ -2509,6 +2625,7 @@ window.__ModuleLoader__.load({
2509
2625
  if (!credentialResult.ok) throw new Error(credentialResult.error?.message || 'ANT 成员凭证未能清除。')
2510
2626
  setCredentialConfigured(false)
2511
2627
  setAuthorizedModels([]); setServerDefaultModel(null); setDshDefaultSelection(null); setDefaultModelDraft('')
2628
+ setProviderSyncGroups([]); setAppliedProviderSyncExclusions([]); setProviderSyncDraft([])
2512
2629
  try { localStorage.removeItem(`${DEFAULT_MODEL_INITIALIZED_KEY}:${normalizeServiceUrl(serviceUrl)}:${savedDeviceId() || ''}`) } catch {}
2513
2630
  onConnectionChange?.(null)
2514
2631
  setNeedsReconnect(false)
@@ -2720,11 +2837,35 @@ window.__ModuleLoader__.load({
2720
2837
  const canCheckUpdate = bindingReady && credentialConfigured === true && !needsReconnect
2721
2838
  const updateGuideUrl = pinnedServiceUrl
2722
2839
  ? new URL('/ant-update', pinnedServiceUrl).href : null
2723
- const currentDshDefaultModel = modelForProviderSelection(dshDefaultSelection, authorizedModels)
2840
+ const currentDshDefaultModel = modelForProviderSelection(dshDefaultSelection, authorizedModels, serverDefaultModel)
2841
+ const excludedGroupSet = new Set(providerSyncDraft)
2842
+ const excludedRouteSet = new Set(providerSyncGroups.filter((group) => excludedGroupSet.has(group.key))
2843
+ .flatMap((group) => group.routes))
2844
+ const appliedExcludedGroupSet = new Set(appliedProviderSyncExclusions)
2845
+ const appliedExcludedRouteSet = new Set(providerSyncGroups.filter((group) => appliedExcludedGroupSet.has(group.key))
2846
+ .flatMap((group) => group.routes))
2847
+ const syncedModels = authorizedModels.filter((model) => !appliedExcludedRouteSet.has(modelProvider(model)))
2848
+ const hasPendingSyncChanges = [...new Set([...providerSyncDraft, ...appliedProviderSyncExclusions])]
2849
+ .some((key) => excludedGroupSet.has(key) !== appliedExcludedGroupSet.has(key))
2850
+ const activePrimaryGroupCount = providerSyncGroups.filter((group) => !group.legacy && !excludedGroupSet.has(group.key)).length
2851
+ const pendingServerDefaultRemoval = hasPendingSyncChanges && serverDefaultModel
2852
+ ? providerSyncGroups.some((group) => group.routes.includes(modelProvider(serverDefaultModel))
2853
+ && !appliedExcludedGroupSet.has(group.key) && excludedGroupSet.has(group.key))
2854
+ : false
2855
+ const selectedDefaultDraft = syncedModels.find((model) => modelSelectionKey(model) === defaultModelDraft)
2856
+ const toggleProviderSyncGroup = (group, checked) => {
2857
+ setProviderSyncDraft((current) => {
2858
+ const next = new Set(current)
2859
+ if (checked) next.delete(group.key)
2860
+ else next.add(group.key)
2861
+ return [...next]
2862
+ })
2863
+ }
2724
2864
  const modelRows = authorizedModels.map((model) => {
2725
2865
  const providerName = model.protocol === 'openai'
2726
2866
  ? compactOpenaiProviderName(model.provider_name)
2727
2867
  : model.provider_name
2868
+ const isSynced = !excludedRouteSet.has(modelProvider(model))
2728
2869
  return h('div', { key: `${model.provider_id || ''}:${model.protocol}:${model.model_id}`, role: 'listitem',
2729
2870
  style: { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) minmax(120px, auto) minmax(130px, auto)',
2730
2871
  gap: 12, alignItems: 'center', padding: '10px 2px', borderBottom: '1px solid var(--dsw-alias-border-l2)' } },
@@ -2732,7 +2873,9 @@ window.__ModuleLoader__.load({
2732
2873
  h('span', { style: { display: 'inline-flex', justifySelf: 'start', padding: '3px 8px', borderRadius: 999,
2733
2874
  backgroundColor: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-secondary)', fontSize: 12 } },
2734
2875
  model.protocol === 'anthropic' ? 'Anthropic Messages' : `OpenAI · ${model.openai_api_mode === 'responses' ? 'Responses' : 'Chat Completions'}`),
2735
- h('span', { style: { ...mutedTextStyle, textAlign: 'right' } }, '已同步到 DSH'))
2876
+ h('span', { style: { ...mutedTextStyle, textAlign: 'right' } }, hasPendingSyncChanges
2877
+ ? isSynced ? '刷新后同步到 DSH' : '刷新后从 DSH 移除'
2878
+ : isSynced ? '已同步到 DSH' : '未同步到 DSH'))
2736
2879
  })
2737
2880
  const connectionPanel = h('section', { style: { ...settingsPanelStyle, display: activeTab === 'connection' ? 'grid' : 'none' } },
2738
2881
  h('div', null,
@@ -2784,7 +2927,7 @@ window.__ModuleLoader__.load({
2784
2927
  setMessage('设备编号已复制,请交给原团队管理员。')
2785
2928
  } catch { setMessage(savedDeviceId() ? `请复制设备编号:${savedDeviceId()}` : '本机无法确认旧设备编号,请联系原团队管理员核查。') }
2786
2929
  }, style: secondaryButtonStyle }, '复制设备编号'))) : null,
2787
- message ? h('p', { role: 'status', style: mutedTextStyle }, message) : null)
2930
+ message ? h('p', { role: 'status', style: feedbackNoticeStyle }, message) : null)
2788
2931
 
2789
2932
  const modelPanel = h('section', { style: { ...settingsPanelStyle, display: activeTab === 'models' ? 'grid' : 'none' } },
2790
2933
  h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12 } },
@@ -2806,10 +2949,10 @@ window.__ModuleLoader__.load({
2806
2949
  h('p', { style: mutedTextStyle }, '首次同步时,ANT 会用团队默认初始化 DSH 全局默认模型。它会影响该 Profile 的新会话以及 ANT 对话和任务;之后刷新保留你的个人覆盖。'),
2807
2950
  h('label', { style: { display: 'grid', gap: 5, maxWidth: 720 } }, 'DSH 全局默认模型',
2808
2951
  h('select', { 'aria-label': 'DSH 全局默认模型', value: defaultModelDraft,
2809
- disabled: credentialConfigured !== true || !authorizedModels.length || defaultModelBusy || busy,
2952
+ disabled: credentialConfigured !== true || !syncedModels.length || defaultModelBusy || busy || hasPendingSyncChanges,
2810
2953
  onChange: (event) => setDefaultModelDraft(event.target.value), style: inputStyle },
2811
2954
  h('option', { value: '' }, '选择获准模型'),
2812
- ...authorizedModels.map((model) => {
2955
+ ...syncedModels.map((model) => {
2813
2956
  const providerName = model.protocol === 'openai'
2814
2957
  ? compactOpenaiProviderName(model.provider_name)
2815
2958
  : model.provider_name
@@ -2818,14 +2961,28 @@ window.__ModuleLoader__.load({
2818
2961
  `${providerName ? `${providerName} · ` : ''}${model.display_name || model.model_id} · ${protocolName}`)
2819
2962
  }))),
2820
2963
  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) },
2964
+ h('button', { type: 'button', disabled: defaultModelBusy || !selectedDefaultDraft || hasPendingSyncChanges,
2965
+ onClick: () => void applyUserDefaultModel(selectedDefaultDraft),
2823
2966
  style: primaryButtonStyle }, defaultModelBusy ? '保存中…' : '保存为 DSH 全局默认'),
2824
- h('button', { type: 'button', disabled: defaultModelBusy || !serverDefaultModel
2967
+ h('button', { type: 'button', disabled: busy || defaultModelBusy || pendingServerDefaultRemoval || !serverDefaultModel
2825
2968
  || modelSelectionKey(currentDshDefaultModel || {}) === modelSelectionKey(serverDefaultModel),
2826
2969
  onClick: () => void applyUserDefaultModel(serverDefaultModel), style: secondaryButtonStyle }, '恢复团队默认'),
2827
2970
  currentDshDefaultModel ? h('span', { style: mutedTextStyle }, `当前 DSH 默认:${currentDshDefaultModel.display_name || currentDshDefaultModel.model_id}`) : null),
2971
+ pendingServerDefaultRemoval ? h('p', { role: 'status', style: mutedTextStyle },
2972
+ '团队默认 provider 正准备移除;先刷新应用同步范围,或重新勾选该 provider。') : null,
2828
2973
  defaultModelMessage ? h('p', { role: 'status', style: mutedTextStyle }, defaultModelMessage) : null),
2974
+ providerSyncGroups.length ? h('section', { style: { ...infoNoticeStyle, gap: 8 } },
2975
+ h('strong', null, '同步到本机 DSH 的 provider'),
2976
+ h('p', { style: mutedTextStyle }, '首次升级会保留当前已同步的旧会话兼容路由。取消勾选并刷新后,该 provider 会从本机 DSH 移除;使用它的历史会话可能暂时无法继续,重新勾选并刷新可恢复。'),
2977
+ h('p', { style: mutedTextStyle }, '至少保留一个常规模型 provider;旧会话兼容组只供已有会话使用。'),
2978
+ hasPendingSyncChanges ? h('p', { role: 'status', style: mutedTextStyle }, '同步范围尚未应用;点击“刷新团队模型”后生效。') : null,
2979
+ ...providerSyncGroups.map((group) => h('label', { key: group.key,
2980
+ style: { display: 'flex', alignItems: 'center', gap: 9, padding: '5px 0', cursor: busy ? 'default' : 'pointer' } },
2981
+ h('input', { type: 'checkbox', 'aria-label': group.label,
2982
+ checked: !excludedGroupSet.has(group.key), disabled: busy
2983
+ || (!group.legacy && !excludedGroupSet.has(group.key) && activePrimaryGroupCount <= 1),
2984
+ onChange: (event) => toggleProviderSyncGroup(group, event.target.checked) }),
2985
+ h('span', null, `${group.label} · ${group.modelCount} 个模型`)))) : null,
2829
2986
  h('div', { role: 'list', 'aria-label': '获准模型', style: { display: 'grid', gap: 0, borderTop: '1px solid var(--dsw-alias-border-l2)' } },
2830
2987
  modelRows.length ? modelRows : h('div', { style: { display: 'grid', gap: 8, padding: '12px 0' } },
2831
2988
  h('p', { style: mutedTextStyle }, credentialConfigured === true && modelMessage.startsWith('正在') ? modelMessage
@@ -2835,7 +2992,7 @@ window.__ModuleLoader__.load({
2835
2992
  style: secondaryButtonStyle }, '前往团队连接') : null)),
2836
2993
  h('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 10 } },
2837
2994
  h('button', { type: 'button', disabled: busy || defaultModelBusy || !bindingReady || credentialConfigured !== true,
2838
- onClick: () => void refreshModels(), style: primaryButtonStyle }, busy ? '刷新中…' : '刷新团队模型'),
2995
+ onClick: () => void refreshModels(true), style: primaryButtonStyle }, busy ? '刷新中…' : '刷新团队模型'),
2839
2996
  lastModelSync ? h('span', { style: mutedTextStyle }, `最近同步:${lastModelSync.toLocaleString('zh-CN')}`) : null),
2840
2997
  modelMessage && !modelMessage.startsWith('正在') ? h('p', { role: 'status', style: mutedTextStyle }, modelMessage) : null)
2841
2998
 
@@ -2869,12 +3026,12 @@ window.__ModuleLoader__.load({
2869
3026
  h('strong', { style: { fontSize: 14, lineHeight: 1.4 } }, '本版更新'),
2870
3027
  h('p', { style: mutedTextStyle }, bundleRelease.release_notes)) : null,
2871
3028
  ['incompatible', 'unknown'].includes(serviceCompatibility.state) ? h('p', { role: 'status', style: {
2872
- ...mutedTextStyle,
3029
+ ...feedbackNoticeStyle,
2873
3030
  color: serviceCompatibility.state === 'incompatible' ? 'var(--dsw-alias-state-error-primary)'
2874
3031
  : 'var(--dsw-alias-state-warning-primary, var(--dsw-alias-label-secondary))',
2875
3032
  } }, `服务端兼容性:${serviceCompatibility.message}`) : null,
2876
3033
  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,
3034
+ ? { ...feedbackNoticeStyle, color: 'var(--dsw-alias-state-error-primary)' } : feedbackNoticeStyle }, bundleUpdate.message) : null,
2878
3035
  !canCheckUpdate && updateGuideUrl ? h('a', { href: updateGuideUrl, target: '_blank', rel: 'noopener noreferrer',
2879
3036
  style: { color: 'var(--dsw-alias-link)', fontSize: 13 } }, '打开更新指引 ↗') : null,
2880
3037
  !canCheckUpdate && !updateGuideUrl ? h('p', { style: mutedTextStyle }, '无法核对原团队地址时,请向企业管理员索取更新指引。') : null,
@@ -2910,7 +3067,175 @@ window.__ModuleLoader__.load({
2910
3067
  includePlugins: tab === 'all', ...navigation })
2911
3068
  }
2912
3069
 
3070
+ function brandingCacheKey() {
3071
+ const serviceUrl = loadServiceUrl()
3072
+ return serviceUrl ? `${BRANDING_CACHE_PREFIX}${serviceUrl}` : null
3073
+ }
3074
+
3075
+ function readBrandingCache() {
3076
+ const key = brandingCacheKey()
3077
+ if (!key) return null
3078
+ try {
3079
+ const value = JSON.parse(localStorage.getItem(key) || 'null')
3080
+ if (!value || !Number.isSafeInteger(value.revision) || value.revision < 0) return null
3081
+ return value
3082
+ } catch { return null }
3083
+ }
3084
+
3085
+ function normalizeBranding(value) {
3086
+ if (!value || !Number.isSafeInteger(value.revision) || value.revision < 0) return null
3087
+ const fields = ['wordmark', 'badge', 'headline']
3088
+ if (fields.some((field) => value[field] != null && typeof value[field] !== 'string')) return null
3089
+ if (value.logo_data_url != null && (typeof value.logo_data_url !== 'string'
3090
+ || !/^data:image\/(?:png|jpeg|webp);base64,[A-Za-z0-9+/]+=*$/u.test(value.logo_data_url)
3091
+ || value.logo_data_url.length > 512 * 1024)) return null
3092
+ return { revision: value.revision, logo_data_url: value.logo_data_url || null,
3093
+ wordmark: value.wordmark || null, badge: value.badge || null, headline: value.headline || null }
3094
+ }
3095
+
3096
+ function installEnterpriseBranding(ctx) {
3097
+ const slotDisposers = new Map()
3098
+ const slotValues = new Map()
3099
+ const originalHeadlines = new Map()
3100
+ let headlineObserver = null
3101
+ let applied = null
3102
+ const isHeroHeadlineNode = (node) => {
3103
+ let container = node?.parentElement
3104
+ const compact = (value) => value?.replace(/\s+/gu, '') || ''
3105
+ const aliases = [node?.textContent?.trim(), originalHeadlines.get(node)?.original,
3106
+ originalHeadlines.get(node)?.current, '探索未至之境', 'Into the Unknown'].filter(Boolean).map(compact)
3107
+ for (let depth = 0; container && depth < 6; depth += 1, container = container.parentElement) {
3108
+ if (!container.querySelector) continue
3109
+ const badges = [...container.querySelectorAll('span, small, label')]
3110
+ .filter((element) => ['预览版', 'Preview'].includes(element.textContent?.trim()))
3111
+ const marks = container.querySelectorAll('svg, img')
3112
+ const wholeRow = compact(container.textContent)
3113
+ if (badges.length === 1 && marks.length === 1
3114
+ && aliases.some((title) => ['预览版', 'Preview'].some((badge) =>
3115
+ wholeRow === `${title}${compact(badge)}` || wholeRow === `${compact(badge)}${title}`))) return true
3116
+ }
3117
+ return false
3118
+ }
3119
+ const replaceSlot = (name, value, component) => {
3120
+ if (slotValues.get(name) === value) return
3121
+ slotDisposers.get(name)?.()
3122
+ slotDisposers.delete(name); slotValues.delete(name)
3123
+ if (!value) return
3124
+ const dispose = ctx.slots.inject(name, () => ctx.slots.register({ name }, component))
3125
+ slotDisposers.set(name, dispose); slotValues.set(name, value)
3126
+ }
3127
+ const replaceHeadlineNode = (node) => {
3128
+ if (node?.nodeType !== Node.TEXT_NODE) return
3129
+ const text = node.textContent?.trim()
3130
+ const prior = originalHeadlines.get(node)
3131
+ const matchesHeadline = text === '探索未至之境' || text === 'Into the Unknown'
3132
+ || text === prior?.original || text === prior?.current
3133
+ if (applied?.headline && text !== applied.headline && matchesHeadline && isHeroHeadlineNode(node)) {
3134
+ originalHeadlines.set(node, { original: prior?.original || text, current: applied.headline })
3135
+ node.textContent = applied.headline
3136
+ } else if (!applied?.headline && prior && text === prior.current) {
3137
+ node.textContent = prior.original
3138
+ originalHeadlines.delete(node)
3139
+ }
3140
+ }
3141
+ const replaceHeadlineSubtree = (root) => {
3142
+ if (!root) return
3143
+ if (root.nodeType === Node.TEXT_NODE) { replaceHeadlineNode(root); return }
3144
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
3145
+ let node
3146
+ while ((node = walker.nextNode())) replaceHeadlineNode(node)
3147
+ }
3148
+ const updateHeadline = (records = null) => {
3149
+ if (typeof document === 'undefined' || !document.body) return
3150
+ if (!records) { replaceHeadlineSubtree(document.body); return }
3151
+ for (const record of records) {
3152
+ if (record.type === 'characterData') replaceHeadlineNode(record.target)
3153
+ else for (const node of record.addedNodes) replaceHeadlineSubtree(node)
3154
+ }
3155
+ for (const node of originalHeadlines.keys()) if (!node.isConnected) originalHeadlines.delete(node)
3156
+ }
3157
+ const apply = (value) => {
3158
+ const branding = normalizeBranding(value)
3159
+ const previousHeadline = applied?.headline
3160
+ applied = branding ? { ...branding } : null
3161
+ const logo = branding?.logo_data_url || null
3162
+ const nameValue = branding && (branding.wordmark || branding.badge)
3163
+ ? `${branding.wordmark || ''}\u0000${branding.badge || ''}` : null
3164
+ const mark = ({ size = 24, className }) => logo
3165
+ ? h('img', { src: logo, alt: '', className, draggable: false,
3166
+ style: { display: 'block', width: size, height: size, objectFit: 'contain' } }) : null
3167
+ const brandName = () => h('div', { style: { display: 'flex', alignItems: 'center', gap: 6, whiteSpace: 'nowrap' } },
3168
+ h('span', { style: { color: 'var(--dsw-alias-brand-primary)', fontWeight: 650 } }, branding?.wordmark || 'DEEPSEEK'),
3169
+ h('span', { style: { padding: '2px 5px', borderRadius: 4, color: 'var(--dsw-alias-label-primary-inverted)',
3170
+ background: 'var(--dsw-alias-brand-primary)', fontSize: 10, fontWeight: 650, letterSpacing: '.04em' } }, branding?.badge || 'HARNESS'))
3171
+ replaceSlot('sidebar.brand.mark', logo, mark)
3172
+ replaceSlot('conversation.hero.brand.mark', logo, mark)
3173
+ replaceSlot('sidebar.brand.name', nameValue, brandName)
3174
+ if (branding?.headline && !headlineObserver && typeof document !== 'undefined' && document.body) {
3175
+ headlineObserver = new MutationObserver(updateHeadline)
3176
+ headlineObserver.observe(document.body, { childList: true, subtree: true, characterData: true })
3177
+ }
3178
+ updateHeadline()
3179
+ }
3180
+ apply(readBrandingCache())
3181
+ let etag = applied?.revision ? `"${applied.revision}"` : null
3182
+ let retryCount = 0
3183
+ let retryTimer = null
3184
+ let disposed = false
3185
+ const refresh = async () => {
3186
+ if (disposed) return
3187
+ let retryable = true
3188
+ try {
3189
+ if (!loadServiceUrl()) {
3190
+ const originResponse = await fetch('/api/ant-teams/connection-origin', { cache: 'no-store' })
3191
+ if (disposed) return
3192
+ if (!originResponse.ok) return
3193
+ const originInfo = await readLocalJsonResponse(originResponse)
3194
+ if (disposed) return
3195
+ const origin = normalizeServiceUrl(originInfo?.service_url || '')
3196
+ if (!origin) return
3197
+ try { localStorage.setItem(SERVICE_URL_STORAGE_KEY, origin) } catch {}
3198
+ }
3199
+ const response = await fetch(BRANDING_SYNC_PATH, { cache: 'no-store',
3200
+ headers: etag ? { 'if-none-match': etag } : {} })
3201
+ if (disposed) return
3202
+ if (response.status === 304) { retryCount = 0; return }
3203
+ if (!response.ok) {
3204
+ retryable = [404, 426, 502, 503].includes(response.status)
3205
+ throw new Error('branding sync unavailable')
3206
+ }
3207
+ const branding = normalizeBranding(await readLocalJsonResponse(response))
3208
+ if (disposed || !branding) return
3209
+ etag = response.headers.get('etag') || `"${branding.revision}"`
3210
+ const key = brandingCacheKey()
3211
+ if (key) {
3212
+ try { localStorage.setItem(key, JSON.stringify(branding)) } catch {}
3213
+ }
3214
+ apply(branding)
3215
+ retryCount = 0
3216
+ } catch {
3217
+ if (!disposed && retryable && retryCount < 3) {
3218
+ const delay = [3_000, 10_000, 30_000][retryCount]
3219
+ retryCount += 1
3220
+ clearTimeout(retryTimer)
3221
+ retryTimer = setTimeout(() => void refresh(), delay)
3222
+ }
3223
+ }
3224
+ }
3225
+ void refresh()
3226
+ const timer = setInterval(() => void refresh(), 5 * 60 * 1000)
3227
+ return () => {
3228
+ disposed = true
3229
+ clearInterval(timer); clearTimeout(retryTimer); headlineObserver?.disconnect()
3230
+ for (const [node, prior] of originalHeadlines) {
3231
+ if (node.textContent?.trim() === prior.current) node.textContent = prior.original
3232
+ }
3233
+ for (const dispose of slotDisposers.values()) dispose?.()
3234
+ }
3235
+ }
3236
+
2913
3237
  function apply(ctx) {
3238
+ ctx.effect(() => installEnterpriseBranding(ctx), 'ANT enterprise branding synchronization')
2914
3239
  const remote = {
2915
3240
  credentials: ctx.remote.credentials,
2916
3241
  settings: ctx.remote.settings,
package/index.js CHANGED
@@ -10,6 +10,7 @@ import { readLocalDelegationSettings, recentDelegationStatus, startDelegationHos
10
10
  export const inject = []
11
11
 
12
12
  const MODEL_SYNC_PATH = '/api/ant-teams/models'
13
+ const BRANDING_PATH = '/api/ant-teams/branding'
13
14
  const SERVICE_INFO_PATH = '/api/ant-teams/service-info'
14
15
  const BUNDLE_RELEASE_PATH = '/api/ant-teams/bundle-release'
15
16
  const CONNECTION_ORIGIN_PATH = '/api/ant-teams/connection-origin'
@@ -842,6 +843,38 @@ async function fetchMemberPlugins(request, credentials, settings) {
842
843
  }
843
844
  }
844
845
 
846
+ async function fetchMemberBranding(request, credentials, settings) {
847
+ if (request.method !== 'GET') return jsonResponse(405, { message: '只支持 GET 请求。' })
848
+ try {
849
+ const connection = await readMemberConnection(credentials, settings)
850
+ if (!connection) return jsonResponse(401, { message: '请先连接 ANT 团队。' })
851
+ const etag = request.headers.get('if-none-match')
852
+ const upstream = await fetch(new URL('/api/me/branding', connection.serviceUrl), {
853
+ headers: { authorization: `Bearer ${connection.token}`, accept: 'application/json',
854
+ ...(etag ? { 'if-none-match': etag } : {}) },
855
+ redirect: 'manual', signal: AbortSignal.timeout(10_000),
856
+ })
857
+ if (upstream.status === 304) {
858
+ return new Response(null, { status: 304, headers: { etag: upstream.headers.get('etag') || etag || '' } })
859
+ }
860
+ if (!upstream.ok) return jsonResponse(upstream.status === 401 || upstream.status === 403 ? 401 : 502,
861
+ { message: '暂时无法读取企业品牌设置。' })
862
+ const text = await readLimitedBody(upstream, 600 * 1024)
863
+ if (text === null) return jsonResponse(502, { message: '企业品牌设置响应过大。' })
864
+ const branding = JSON.parse(text)
865
+ if (!Number.isSafeInteger(branding?.revision) || branding.revision < 0
866
+ || ![null, undefined].includes(branding.logo_data_url) && typeof branding.logo_data_url !== 'string'
867
+ || ![null, undefined].includes(branding.wordmark) && typeof branding.wordmark !== 'string'
868
+ || ![null, undefined].includes(branding.badge) && typeof branding.badge !== 'string'
869
+ || ![null, undefined].includes(branding.headline) && typeof branding.headline !== 'string') {
870
+ return jsonResponse(502, { message: '企业品牌设置格式无效。' })
871
+ }
872
+ return new Response(text, { status: 200,
873
+ headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store',
874
+ ...(upstream.headers.get('etag') ? { etag: upstream.headers.get('etag') } : {}) } })
875
+ } catch { return jsonResponse(502, { message: '暂时无法读取企业品牌设置。' }) }
876
+ }
877
+
845
878
  async function fetchAuthRequest(request, endpoint, credentials, settings) {
846
879
  if (request.method !== 'POST') return jsonResponse(405, { message: '只支持 POST 请求' })
847
880
  let body
@@ -941,6 +974,10 @@ export function apply(ctx) {
941
974
  requestBody: 'buffered',
942
975
  fetch: (request) => fetchMemberModels(request, connectionCtx.credentials, connectionCtx.settings),
943
976
  }), 'ant-teams: authenticated model catalog relay')
977
+ connectionCtx.effect(() => connectionCtx.connection.fetch.register({
978
+ path: BRANDING_PATH, methods: ['GET'], requestBody: 'buffered',
979
+ fetch: (request) => fetchMemberBranding(request, connectionCtx.credentials, connectionCtx.settings),
980
+ }), 'ant-teams: conditional enterprise branding relay')
944
981
  connectionCtx.effect(() => connectionCtx.connection.fetch.register({
945
982
  path: BUNDLE_RELEASE_PATH,
946
983
  methods: ['GET'],
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.1",
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": {