@ai-wendao/ant 0.5.0 → 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 +6 -0
  2. package/client.js +170 -0
  3. package/index.js +37 -0
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -8,6 +8,12 @@ ANT (AI-native Teams) connects a DeepSeek Harness Desktop profile to its team's
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
  ## 协作机器人与任务卡
package/client.js CHANGED
@@ -12,6 +12,8 @@ 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'
16
18
  const PROVIDER_SYNC_EXCLUSIONS_KEY = 'ant-team-provider-sync-exclusions'
17
19
  const DEVICE_ID_STORAGE_KEY = 'ant-team-device-id'
@@ -3065,7 +3067,175 @@ window.__ModuleLoader__.load({
3065
3067
  includePlugins: tab === 'all', ...navigation })
3066
3068
  }
3067
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
+
3068
3237
  function apply(ctx) {
3238
+ ctx.effect(() => installEnterpriseBranding(ctx), 'ANT enterprise branding synchronization')
3069
3239
  const remote = {
3070
3240
  credentials: ctx.remote.credentials,
3071
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.5.0",
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": {