@ai-wendao/ant 0.5.0 → 0.5.2

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 +192 -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,197 @@ 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
+ let timer = null
3103
+ let retryTimer = null
3104
+ let disposed = false
3105
+ const isHeroHeadlineNode = (node) => {
3106
+ let container = node?.parentElement
3107
+ const compact = (value) => value?.replace(/\s+/gu, '') || ''
3108
+ const aliases = [node?.textContent?.trim(), originalHeadlines.get(node)?.original,
3109
+ originalHeadlines.get(node)?.current, '探索未至之境', 'Into the Unknown'].filter(Boolean).map(compact)
3110
+ for (let depth = 0; container && depth < 6; depth += 1, container = container.parentElement) {
3111
+ if (!container.querySelector) continue
3112
+ const badges = [...container.querySelectorAll('span, small, label')]
3113
+ .filter((element) => ['预览版', 'Preview'].includes(element.textContent?.trim()))
3114
+ const marks = container.querySelectorAll('svg, img')
3115
+ const wholeRow = compact(container.textContent)
3116
+ if (badges.length === 1 && marks.length === 1
3117
+ && aliases.some((title) => ['预览版', 'Preview'].some((badge) =>
3118
+ wholeRow === `${title}${compact(badge)}` || wholeRow === `${compact(badge)}${title}`))) return true
3119
+ }
3120
+ return false
3121
+ }
3122
+ const replaceSlot = (name, value, component) => {
3123
+ if (slotValues.get(name) === value) return
3124
+ slotDisposers.get(name)?.()
3125
+ slotDisposers.delete(name); slotValues.delete(name)
3126
+ if (!value) return
3127
+ const dispose = ctx.slots.inject(name, () => ctx.slots.register({ name, priority: -1 }, component))
3128
+ slotDisposers.set(name, dispose); slotValues.set(name, value)
3129
+ }
3130
+ const replaceHeadlineNode = (node) => {
3131
+ if (node?.nodeType !== Node.TEXT_NODE) return
3132
+ const text = node.textContent?.trim()
3133
+ const prior = originalHeadlines.get(node)
3134
+ const matchesHeadline = text === '探索未至之境' || text === 'Into the Unknown'
3135
+ || text === prior?.original || text === prior?.current
3136
+ if (applied?.headline && text !== applied.headline && matchesHeadline && isHeroHeadlineNode(node)) {
3137
+ originalHeadlines.set(node, { original: prior?.original || text, current: applied.headline })
3138
+ node.textContent = applied.headline
3139
+ } else if (!applied?.headline && prior && text === prior.current) {
3140
+ node.textContent = prior.original
3141
+ originalHeadlines.delete(node)
3142
+ }
3143
+ }
3144
+ const replaceHeadlineSubtree = (root) => {
3145
+ if (!root) return
3146
+ if (root.nodeType === Node.TEXT_NODE) { replaceHeadlineNode(root); return }
3147
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
3148
+ let node
3149
+ while ((node = walker.nextNode())) replaceHeadlineNode(node)
3150
+ }
3151
+ const updateHeadline = (records = null) => {
3152
+ if (typeof document === 'undefined' || !document.body) return
3153
+ if (!applied?.headline) {
3154
+ for (const [node, prior] of originalHeadlines) {
3155
+ if (node.textContent?.trim() === prior.current) {
3156
+ node.textContent = prior.original
3157
+ originalHeadlines.delete(node)
3158
+ }
3159
+ }
3160
+ return
3161
+ }
3162
+ if (!records) { replaceHeadlineSubtree(document.body); return }
3163
+ for (const record of records) {
3164
+ if (record.type === 'characterData') replaceHeadlineNode(record.target)
3165
+ else for (const node of record.addedNodes) replaceHeadlineSubtree(node)
3166
+ }
3167
+ for (const node of originalHeadlines.keys()) if (!node.isConnected) originalHeadlines.delete(node)
3168
+ }
3169
+ const apply = (value) => {
3170
+ const branding = normalizeBranding(value)
3171
+ applied = branding ? { ...branding } : null
3172
+ const logo = branding?.logo_data_url || null
3173
+ const nameValue = branding && (branding.wordmark || branding.badge)
3174
+ ? `${branding.wordmark || ''}\u0000${branding.badge || ''}` : null
3175
+ const mark = ({ size = 24, className }) => logo
3176
+ ? h('img', { src: logo, alt: '', className, draggable: false,
3177
+ style: { display: 'block', width: size, height: size, objectFit: 'contain' } }) : null
3178
+ const brandName = () => h('div', { style: { display: 'flex', alignItems: 'center', gap: 6, whiteSpace: 'nowrap' } },
3179
+ h('span', { style: { color: 'var(--dsw-alias-brand-primary)', fontWeight: 650 } }, branding?.wordmark || 'DEEPSEEK'),
3180
+ h('span', { style: { padding: '2px 5px', borderRadius: 4, color: 'var(--dsw-alias-label-primary-inverted)',
3181
+ background: 'var(--dsw-alias-brand-primary)', fontSize: 10, fontWeight: 650, letterSpacing: '.04em' } }, branding?.badge || 'HARNESS'))
3182
+ replaceSlot('sidebar.brand.mark', logo, mark)
3183
+ replaceSlot('conversation.hero.brand.mark', logo, mark)
3184
+ replaceSlot('sidebar.brand.name', nameValue, brandName)
3185
+ if (branding?.headline && !headlineObserver && typeof document !== 'undefined' && document.body) {
3186
+ headlineObserver = new MutationObserver(updateHeadline)
3187
+ headlineObserver.observe(document.body, { childList: true, subtree: true, characterData: true })
3188
+ }
3189
+ updateHeadline()
3190
+ }
3191
+
3192
+ const cleanup = () => {
3193
+ if (disposed) return
3194
+ disposed = true
3195
+ if (timer !== null) clearInterval(timer)
3196
+ if (retryTimer !== null) clearTimeout(retryTimer)
3197
+ headlineObserver?.disconnect()
3198
+ for (const [node, prior] of originalHeadlines) {
3199
+ if (node.textContent?.trim() === prior.current) node.textContent = prior.original
3200
+ }
3201
+ for (const dispose of slotDisposers.values()) dispose?.()
3202
+ }
3203
+
3204
+ try {
3205
+ apply(readBrandingCache())
3206
+ } catch (error) {
3207
+ cleanup()
3208
+ console.warn('ANT branding setup failed; continuing with the default DSH branding.', error)
3209
+ return cleanup
3210
+ }
3211
+
3212
+ let etag = applied?.revision ? `"${applied.revision}"` : null
3213
+ let retryCount = 0
3214
+ const refresh = async () => {
3215
+ if (disposed) return
3216
+ let retryable = true
3217
+ try {
3218
+ if (!loadServiceUrl()) {
3219
+ const originResponse = await fetch('/api/ant-teams/connection-origin', { cache: 'no-store' })
3220
+ if (disposed) return
3221
+ if (!originResponse.ok) return
3222
+ const originInfo = await readLocalJsonResponse(originResponse)
3223
+ if (disposed) return
3224
+ const origin = normalizeServiceUrl(originInfo?.service_url || '')
3225
+ if (!origin) return
3226
+ try { localStorage.setItem(SERVICE_URL_STORAGE_KEY, origin) } catch {}
3227
+ }
3228
+ const response = await fetch(BRANDING_SYNC_PATH, { cache: 'no-store',
3229
+ headers: etag ? { 'if-none-match': etag } : {} })
3230
+ if (disposed) return
3231
+ if (response.status === 304) { retryCount = 0; return }
3232
+ if (!response.ok) {
3233
+ retryable = [404, 426, 502, 503].includes(response.status)
3234
+ throw new Error('branding sync unavailable')
3235
+ }
3236
+ const branding = normalizeBranding(await readLocalJsonResponse(response))
3237
+ if (disposed || !branding) return
3238
+ etag = response.headers.get('etag') || `"${branding.revision}"`
3239
+ const key = brandingCacheKey()
3240
+ if (key) {
3241
+ try { localStorage.setItem(key, JSON.stringify(branding)) } catch {}
3242
+ }
3243
+ apply(branding)
3244
+ retryCount = 0
3245
+ } catch {
3246
+ if (!disposed && retryable && retryCount < 3) {
3247
+ const delay = [3_000, 10_000, 30_000][retryCount]
3248
+ retryCount += 1
3249
+ clearTimeout(retryTimer)
3250
+ retryTimer = setTimeout(() => void refresh(), delay)
3251
+ }
3252
+ }
3253
+ }
3254
+ void refresh()
3255
+ timer = setInterval(() => void refresh(), 5 * 60 * 1000)
3256
+ return cleanup
3257
+ }
3258
+
3068
3259
  function apply(ctx) {
3260
+ ctx.effect(() => installEnterpriseBranding(ctx), 'ANT enterprise branding synchronization')
3069
3261
  const remote = {
3070
3262
  credentials: ctx.remote.credentials,
3071
3263
  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.2",
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": {