@ai-wendao/ant 0.4.1 → 0.4.5
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.
- package/README.md +3 -3
- package/client.js +537 -552
- package/conversation.js +522 -199
- package/cordis.patch.yml +3 -3
- package/delegation.js +670 -60
- package/index.js +51 -12
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -10,6 +10,17 @@ window.__ModuleLoader__.load({
|
|
|
10
10
|
const ANT_BUNDLE_PACKAGE_NAME = '@ai-wendao/ant'
|
|
11
11
|
const ANT_API_PROTOCOL_VERSIONS = [1]
|
|
12
12
|
const PLUGIN_INSTALL_RECORD_PREFIX = 'ant-reviewed-plugin-install:'
|
|
13
|
+
const ANT_SETTINGS_TAB_STORAGE_KEY = 'ant-settings-tab'
|
|
14
|
+
const ANT_SETTINGS_TABS = [
|
|
15
|
+
['connection', '团队连接'], ['models', '团队模型'], ['bot-chat', '机器人与聊天'],
|
|
16
|
+
['capability', '能力授权'], ['local', '本机与任务'], ['updates', 'ANT 更新'],
|
|
17
|
+
]
|
|
18
|
+
const loadAntSettingsTab = () => {
|
|
19
|
+
try {
|
|
20
|
+
const saved = localStorage.getItem(ANT_SETTINGS_TAB_STORAGE_KEY)
|
|
21
|
+
return ANT_SETTINGS_TABS.some(([id]) => id === saved) ? saved : 'connection'
|
|
22
|
+
} catch { return 'connection' }
|
|
23
|
+
}
|
|
13
24
|
const fileCountFormat = new Intl.NumberFormat('zh-CN')
|
|
14
25
|
const marketSkillCategories = {
|
|
15
26
|
'ant-ppt-master': '演示', 'ant-frontend-slides': '演示', 'ant-slidev': '演示',
|
|
@@ -160,6 +171,7 @@ window.__ModuleLoader__.load({
|
|
|
160
171
|
const inject = ['slots', 'layout', 'pluginNavigation', 'remote', 'remote.credentials', 'remote.settings', 'remote.pluginManager']
|
|
161
172
|
|
|
162
173
|
async function readLocalJsonResponse(response) {
|
|
174
|
+
if (response.status === 204) return null
|
|
163
175
|
const contentType = response.headers?.get?.('content-type') || ''
|
|
164
176
|
const isJson = /^(?:application|text)\/(?:[a-z0-9!#$&^_.+-]+\+)?json(?:\s*;|$)/iu.test(contentType.trim())
|
|
165
177
|
if (contentType && !isJson) {
|
|
@@ -233,6 +245,7 @@ window.__ModuleLoader__.load({
|
|
|
233
245
|
}
|
|
234
246
|
|
|
235
247
|
function TeamServiceConfig({ remote, openInstalledPlugin }) {
|
|
248
|
+
const [activeTab, setActiveTab] = useState(loadAntSettingsTab)
|
|
236
249
|
const [serviceUrl, setServiceUrl] = useState(loadServiceUrl)
|
|
237
250
|
const [draft, setDraft] = useState(serviceUrl)
|
|
238
251
|
const [binding, setBinding] = useState({ state: 'checking', url: null })
|
|
@@ -240,7 +253,24 @@ window.__ModuleLoader__.load({
|
|
|
240
253
|
const [message, setMessage] = useState('')
|
|
241
254
|
const [pluginAlerts, setPluginAlerts] = useState([])
|
|
242
255
|
const [delegationRevision, setDelegationRevision] = useState(0)
|
|
243
|
-
const [
|
|
256
|
+
const [updateSummary, setUpdateSummary] = useState(null)
|
|
257
|
+
const selectSettingsTab = (tab) => {
|
|
258
|
+
setActiveTab(tab)
|
|
259
|
+
try { localStorage.setItem(ANT_SETTINGS_TAB_STORAGE_KEY, tab) } catch {}
|
|
260
|
+
}
|
|
261
|
+
const moveSettingsTabFocus = (event, index) => {
|
|
262
|
+
const key = event.key
|
|
263
|
+
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(key)) return
|
|
264
|
+
event.preventDefault()
|
|
265
|
+
const step = key === 'ArrowLeft' ? -1 : key === 'ArrowRight' ? 1 : 0
|
|
266
|
+
const next = key === 'Home' ? 0 : key === 'End' ? ANT_SETTINGS_TABS.length - 1
|
|
267
|
+
: (index + step + ANT_SETTINGS_TABS.length) % ANT_SETTINGS_TABS.length
|
|
268
|
+
const [tab] = ANT_SETTINGS_TABS[next]
|
|
269
|
+
selectSettingsTab(tab)
|
|
270
|
+
event.currentTarget.parentElement?.querySelectorAll('[role="tab"]')?.[next]?.focus()
|
|
271
|
+
}
|
|
272
|
+
const updateNeedsAttention = ['available', 'artifact-missing', 'revoked', 'migration-required',
|
|
273
|
+
'unknown', 'failed', 'installing', 'done'].includes(updateSummary?.state)
|
|
244
274
|
useEffect(() => {
|
|
245
275
|
let disposed = false
|
|
246
276
|
const revision = bindingRevision.current
|
|
@@ -312,78 +342,114 @@ window.__ModuleLoader__.load({
|
|
|
312
342
|
}
|
|
313
343
|
}
|
|
314
344
|
const link = binding.state === 'ready' ? consoleUrl(binding.url || serviceUrl) : null
|
|
345
|
+
const bindingSummary = binding.state === 'checking' ? '正在确认这台 DSH 的团队连接…'
|
|
346
|
+
: binding.state === 'error' ? '暂时无法确认团队连接状态'
|
|
347
|
+
: binding.state === 'unverified' ? '本机有 ANT 凭证,但设备归属待核对'
|
|
348
|
+
: binding.url ? `团队服务已连接 · ${new URL(binding.url).host}` : '尚未连接团队服务'
|
|
349
|
+
const bindingReadyForSummary = binding.state === 'ready' && Boolean(binding.url)
|
|
315
350
|
const onConnectionChange = (url) => {
|
|
316
351
|
bindingRevision.current += 1
|
|
317
352
|
setBinding({ state: 'ready', url: url || null })
|
|
353
|
+
setDelegationRevision((value) => value + 1)
|
|
318
354
|
if (url) {
|
|
319
355
|
setServiceUrl(url)
|
|
320
356
|
setDraft(url)
|
|
321
357
|
try { localStorage.setItem(SERVICE_URL_STORAGE_KEY, url) } catch {}
|
|
322
358
|
}
|
|
323
359
|
}
|
|
324
|
-
|
|
325
|
-
h('
|
|
326
|
-
|
|
327
|
-
h('
|
|
328
|
-
|
|
329
|
-
h('p', { style: { ...mutedTextStyle, marginTop: 4 } }, '由团队管理员提供服务地址;管理员可在控制台管理成员、模型和协作设置。')),
|
|
330
|
-
h('form', { onSubmit: save, style: { display: 'grid', gridTemplateColumns: binding.url ? '1fr' : 'minmax(0, 1fr) auto', alignItems: 'end', gap: 10 } },
|
|
360
|
+
const teamServicePanel = h('section', { style: { ...panelStyle, display: activeTab === 'connection' ? 'grid' : 'none' } },
|
|
361
|
+
h('div', null,
|
|
362
|
+
h('h3', { style: { margin: 0, fontSize: 16, fontWeight: 650 } }, '团队服务'),
|
|
363
|
+
h('p', { style: { ...mutedTextStyle, marginTop: 4 } }, '由团队管理员提供服务地址;管理员可在控制台管理成员、模型和协作设置。')),
|
|
364
|
+
h('form', { onSubmit: save, style: { display: 'grid', gridTemplateColumns: binding.url ? '1fr' : 'minmax(0, 1fr) auto', alignItems: 'end', gap: 10 } },
|
|
331
365
|
h('label', { htmlFor: 'ant-team-service-url', style: { gridColumn: '1 / -1', fontSize: 13, fontWeight: 550 } }, binding.url ? '当前连接的团队服务地址' : '服务地址'),
|
|
332
|
-
h('input', {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
required: true,
|
|
336
|
-
value: binding.url || draft,
|
|
337
|
-
readOnly: binding.state !== 'ready' || Boolean(binding.url),
|
|
338
|
-
placeholder: 'https://ant.example.com',
|
|
339
|
-
onChange: (event) => { if (binding.state === 'ready' && !binding.url) setDraft(event.target.value) },
|
|
340
|
-
style: inputStyle,
|
|
341
|
-
}),
|
|
366
|
+
h('input', { id: 'ant-team-service-url', type: 'url', required: true, value: binding.url || draft,
|
|
367
|
+
readOnly: binding.state !== 'ready' || Boolean(binding.url), placeholder: 'https://ant.example.com',
|
|
368
|
+
onChange: (event) => { if (binding.state === 'ready' && !binding.url) setDraft(event.target.value) }, style: inputStyle }),
|
|
342
369
|
binding.url ? null : h('button', { type: 'submit', disabled: binding.state !== 'ready', style: primaryButtonStyle }, '保存地址')),
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
370
|
+
binding.url ? h('p', { style: mutedTextStyle }, '若要切换团队,请先在下方断开当前设备,再填写新地址并验证邮箱。') : null,
|
|
371
|
+
binding.state === 'checking' ? h('p', { role: 'status', style: mutedTextStyle }, '正在核对这台 DSH 绑定的团队…') : null,
|
|
372
|
+
binding.state === 'unverified' ? h('p', { role: 'status', style: mutedTextStyle }, '本机仍有 ANT 凭证,但无法核对所属团队。请联系原团队管理员核查并撤销旧设备;确认后可在下方断开或仅清理本机。') : null,
|
|
373
|
+
binding.state === 'error' ? h('p', { role: 'status', style: mutedTextStyle }, '暂时无法核对这台 DSH 的团队绑定,请重新打开 ANT 或联系管理员。') : null,
|
|
374
|
+
h('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 12 } },
|
|
375
|
+
message ? h('p', { role: 'status', style: mutedTextStyle }, message) : null,
|
|
376
|
+
link ? h('a', { href: link, target: '_blank', rel: 'noopener noreferrer', style: { color: 'var(--dsw-alias-link)', fontSize: 13 } }, '打开团队管理 ↗') : null))
|
|
377
|
+
return h('section', { style: { display: 'grid', gap: 16, width: '100%', maxWidth: 1120 } },
|
|
378
|
+
h('p', { style: mutedTextStyle }, '连接团队服务后,即可同步获准模型并使用团队协作。'),
|
|
379
|
+
h('div', { role: 'status', style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
380
|
+
flexWrap: 'wrap', gap: 10, padding: '10px 12px', borderRadius: 8,
|
|
381
|
+
backgroundColor: bindingReadyForSummary ? 'var(--dsw-alias-state-success-tertiary)' : 'var(--dsw-alias-bg-layer-2)' } },
|
|
382
|
+
h('span', null, bindingSummary),
|
|
383
|
+
!bindingReadyForSummary ? h('button', { type: 'button', onClick: () => selectSettingsTab('connection'),
|
|
384
|
+
style: secondaryButtonStyle }, binding.state === 'error' ? '检查连接' : '查看连接设置') : null),
|
|
385
|
+
h('div', { style: { position: 'sticky', top: 0, zIndex: 8, display: 'grid', gap: 8, padding: '6px 0',
|
|
386
|
+
backgroundColor: 'var(--dsw-alias-bg-module-platform)' } },
|
|
387
|
+
pluginAlerts.length ? h('section', { role: 'alert', style: { ...panelStyle,
|
|
388
|
+
borderColor: 'var(--dsw-alias-label-danger, #d44)', maxHeight: '28vh', overflowY: 'auto' } },
|
|
351
389
|
h('h3', { style: { margin: 0, fontSize: 16 } }, '已安装插件存在安全撤回公告'),
|
|
352
390
|
...pluginAlerts.map((plugin) => h('p', { key: plugin.id, style: mutedTextStyle },
|
|
353
391
|
`${plugin.title} ${plugin.version}${plugin.versionUnknown ? '(本机版本暂无法读取,请核对是否受影响)' : ''}:${plugin.withdrawal_reason}`)),
|
|
354
392
|
...pluginAlerts.map((plugin) => h('button', { key: `${plugin.id}-open`, type: 'button',
|
|
355
393
|
onClick: () => openInstalledPlugin(plugin.package_name), style: secondaryButtonStyle }, `在 DSH 中查看 ${plugin.title}`))) : null,
|
|
356
|
-
h(
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
394
|
+
updateNeedsAttention ? h('section', { role: 'status', 'aria-live': 'polite', style: {
|
|
395
|
+
...panelStyle, display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap',
|
|
396
|
+
gap: 12, padding: 12, borderColor: updateSummary.state === 'revoked'
|
|
397
|
+
? 'var(--dsw-alias-state-error-primary)' : 'var(--dsw-alias-state-warning-primary, var(--dsw-alias-border-l2))',
|
|
398
|
+
backgroundColor: updateSummary.state === 'revoked'
|
|
399
|
+
? 'var(--dsw-alias-state-error-secondary, var(--dsw-alias-bg-module-platform))'
|
|
400
|
+
: 'var(--dsw-alias-state-warning-secondary, var(--dsw-alias-bg-module-platform))',
|
|
401
|
+
} },
|
|
402
|
+
h('div', { style: { display: 'grid', gap: 3, flex: '1 1 300px' } },
|
|
403
|
+
h('strong', null, updateSummary.state === 'available'
|
|
404
|
+
? `有新版 ANT:${updateSummary.currentVersion || '当前版本未知'} → ${updateSummary.version}`
|
|
405
|
+
: updateSummary.state === 'done' ? `ANT ${updateSummary.version} 已安装,需重启 DSH`
|
|
406
|
+
: updateSummary.state === 'revoked' ? '当前 ANT 安装包已撤回' : 'ANT 更新需要处理'),
|
|
407
|
+
h('span', { style: mutedTextStyle }, updateSummary.message || '打开 ANT 更新查看处理方式。')),
|
|
408
|
+
h('button', { type: 'button', onClick: () => selectSettingsTab('updates'), style: primaryButtonStyle },
|
|
409
|
+
updateSummary.state === 'available' ? '查看并更新' : '查看处理方式')) : null,
|
|
410
|
+
h('nav', { role: 'tablist', 'aria-label': 'ANT 插件设置', style: {
|
|
411
|
+
display: 'flex', gap: 4, overflowX: 'auto', borderBottom: '1px solid var(--dsw-alias-border-l2)',
|
|
412
|
+
} },
|
|
413
|
+
...ANT_SETTINGS_TABS.map(([tab, label], index) => h('button', { key: tab, id: `ant-settings-tab-${tab}`,
|
|
414
|
+
type: 'button', role: 'tab', tabIndex: activeTab === tab ? 0 : -1,
|
|
415
|
+
'aria-controls': 'ant-settings-content', 'aria-selected': String(activeTab === tab),
|
|
416
|
+
onKeyDown: (event) => moveSettingsTabFocus(event, index), onClick: () => selectSettingsTab(tab),
|
|
417
|
+
style: { ...secondaryButtonStyle, minHeight: 38, padding: '7px 12px', whiteSpace: 'nowrap',
|
|
418
|
+
borderRadius: '8px 8px 0 0', color: activeTab === tab ? 'var(--dsw-alias-state-business-primary)' : secondaryButtonStyle.color,
|
|
419
|
+
borderBottom: activeTab === tab ? '2px solid var(--dsw-alias-state-business-primary)' : secondaryButtonStyle.border,
|
|
420
|
+
backgroundColor: activeTab === tab ? 'var(--dsw-alias-bg-layer-2)' : secondaryButtonStyle.backgroundColor,
|
|
421
|
+
} }, tab === 'updates' && updateNeedsAttention ? `${label} · ${updateSummary.state === 'available' ? '有新版' : '需处理'}` : label)))),
|
|
422
|
+
h('div', { id: 'ant-settings-content', role: 'tabpanel', 'aria-labelledby': `ant-settings-tab-${activeTab}` },
|
|
423
|
+
teamServicePanel,
|
|
424
|
+
h(ModelSyncFooter, { remote, serviceUrl, bindingReady: binding.state === 'ready', activeTab,
|
|
425
|
+
onServiceUrlChange: setServiceUrl, onConnectionChange,
|
|
426
|
+
onUpdateStateChange: setUpdateSummary, onSelectTab: selectSettingsTab }),
|
|
427
|
+
h(DelegationCards, { remote, revision: delegationRevision,
|
|
428
|
+
activeTab, onSelectTab: selectSettingsTab,
|
|
429
|
+
openImSettings: () => openInstalledPlugin('@xmanrui/dsh-im')})))
|
|
365
430
|
}
|
|
366
431
|
|
|
367
|
-
function DelegationCards({ remote, revision,
|
|
432
|
+
function DelegationCards({ remote, revision, activeTab, onSelectTab, openImSettings }) {
|
|
368
433
|
const [bots, setBots] = useState([])
|
|
369
434
|
const [enrolled, setEnrolled] = useState([])
|
|
370
435
|
const [groups, setGroups] = useState([])
|
|
371
436
|
const [cards, setCards] = useState([])
|
|
372
|
-
const [grants, setGrants] = useState([])
|
|
373
437
|
const [tasks, setTasks] = useState([])
|
|
374
438
|
const [executors, setExecutors] = useState([])
|
|
375
439
|
const [defaults, setDefaults] = useState(null)
|
|
376
440
|
const [localConcurrency, setLocalConcurrency] = useState(null)
|
|
377
|
-
const [
|
|
441
|
+
const [repositoryCacheLimitGib, setRepositoryCacheLimitGib] = useState(10)
|
|
378
442
|
const [botId, setBotId] = useState('')
|
|
379
443
|
const [trustedOrigin, setTrustedOrigin] = useState('')
|
|
380
444
|
const [bindingCode, setBindingCode] = useState('')
|
|
381
445
|
const [conversationKind, setConversationKind] = useState('dm')
|
|
382
446
|
const [groupId, setGroupId] = useState('')
|
|
447
|
+
const [chatGroupId, setChatGroupId] = useState('')
|
|
383
448
|
const [title, setTitle] = useState('')
|
|
384
449
|
const [intent, setIntent] = useState('')
|
|
385
450
|
const [examples, setExamples] = useState('')
|
|
386
451
|
const [taskPrompt, setTaskPrompt] = useState('')
|
|
452
|
+
const [runner, setRunner] = useState('dsh')
|
|
387
453
|
const [parameters, setParameters] = useState([])
|
|
388
454
|
const [giteaToken, setGiteaToken] = useState('')
|
|
389
455
|
const [tokenConfigured, setTokenConfigured] = useState(false)
|
|
@@ -393,48 +459,106 @@ window.__ModuleLoader__.load({
|
|
|
393
459
|
const [busy, setBusy] = useState(false)
|
|
394
460
|
const [loading, setLoading] = useState(true)
|
|
395
461
|
const [botLoadError, setBotLoadError] = useState(false)
|
|
462
|
+
const [loadFailures, setLoadFailures] = useState([])
|
|
396
463
|
const [createOpen, setCreateOpen] = useState(false)
|
|
397
464
|
const [message, setMessage] = useState('')
|
|
398
465
|
const load = async () => {
|
|
399
466
|
setLoading(true)
|
|
467
|
+
setMessage('')
|
|
400
468
|
try {
|
|
401
|
-
const paths = ['bots', 'enrolled-bots', 'groups', 'cards', 'settings', 'executors', 'local-settings', 'status'
|
|
402
|
-
const responses = await Promise.all(paths.map((path) =>
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
469
|
+
const paths = ['bots', 'enrolled-bots', 'groups', 'cards', 'settings', 'executors', 'local-settings', 'status']
|
|
470
|
+
const responses = await Promise.all(paths.map((path) =>
|
|
471
|
+
fetch(`/api/ant-teams/delegation/${path}`, { cache: 'no-store',
|
|
472
|
+
...(path === 'enrolled-bots' ? { headers: { 'x-ant-include-unbound': 'true' } } : {}) })
|
|
473
|
+
.catch(() => null)))
|
|
474
|
+
const data = await Promise.all(responses.map((response) => response
|
|
475
|
+
? readLocalJsonResponse(response).catch(() => null) : null))
|
|
476
|
+
const valid = [
|
|
477
|
+
(value) => Array.isArray(value?.bots),
|
|
478
|
+
Array.isArray, Array.isArray, Array.isArray,
|
|
479
|
+
(value) => value && typeof value === 'object' && !Array.isArray(value),
|
|
480
|
+
Array.isArray,
|
|
481
|
+
(value) => value && typeof value === 'object' && !Array.isArray(value),
|
|
482
|
+
(value) => Array.isArray(value?.items),
|
|
483
|
+
]
|
|
484
|
+
const readable = responses.map((response, index) => Boolean(response?.ok && valid[index](data[index])))
|
|
485
|
+
setLoadFailures(paths.filter((path, index) => !readable[index]))
|
|
486
|
+
setBots(readable[0] ? data[0].bots : [])
|
|
487
|
+
setBotLoadError(!readable[0])
|
|
488
|
+
setEnrolled(readable[1] ? data[1] : [])
|
|
489
|
+
setGroups(readable[2] ? data[2] : [])
|
|
490
|
+
setCards(readable[3] ? data[3] : [])
|
|
491
|
+
setDefaults(readable[4] ? data[4] : null)
|
|
492
|
+
setExecutors(readable[5] ? data[5] : [])
|
|
493
|
+
setLocalConcurrency(readable[6] ? data[6].max_concurrency : null)
|
|
494
|
+
setRepositoryCacheLimitGib(readable[6] ? data[6].repository_cache_limit_gib ?? 10 : 10)
|
|
495
|
+
setTasks(readable[7] ? data[7].items : [])
|
|
416
496
|
let credential = null
|
|
417
497
|
try { credential = await remote?.credentials?.describe?.(['ANT_GITEA_API_TOKEN']) } catch {}
|
|
418
498
|
setTokenConfigured(Boolean(credential?.ok && credential.value?.ANT_GITEA_API_TOKEN?.configured))
|
|
419
499
|
} finally { setLoading(false) }
|
|
420
500
|
}
|
|
421
501
|
useEffect(() => { void load().catch((error) => setMessage(error.message)) }, [remote, revision])
|
|
502
|
+
useEffect(() => { setMessage('') }, [activeTab])
|
|
422
503
|
const selectedBot = bots.find((bot) => bot.bot_id === botId)
|
|
423
504
|
const botConnected = Boolean(selectedBot?.available && selectedBot?.connected)
|
|
424
505
|
const selectedEnrollment = enrolled.find((bot) => bot.bot_fingerprint === selectedBot?.fingerprint)
|
|
506
|
+
const selectedEnrollmentBound = selectedEnrollment?.device_bound_to_current === true
|
|
425
507
|
const availableGroups = groups.filter((group) => group.bot_fingerprint === selectedBot?.fingerprint)
|
|
508
|
+
const selectedChatGroup = availableGroups.find((group) => group.group_id === chatGroupId)
|
|
426
509
|
const activeCards = cards.filter((card) => !card.revoked_at && card.expires_at * 1000 > Date.now())
|
|
427
|
-
const
|
|
510
|
+
const selectedGroupHasTasks = Boolean(selectedBot && chatGroupId && (
|
|
511
|
+
activeCards.some((card) => card.bot_fingerprint === selectedBot.fingerprint
|
|
512
|
+
&& card.conversation_kind === 'group' && card.conversation_id === chatGroupId)))
|
|
428
513
|
const connectedBots = bots.filter((bot) => bot.available && bot.connected)
|
|
429
|
-
const
|
|
514
|
+
const authorizationLoadError = loadFailures.includes('cards')
|
|
515
|
+
const enrollmentLoadError = loadFailures.includes('enrolled-bots')
|
|
516
|
+
const enrolledCount = enrolled.filter((item) => item.device_bound_to_current
|
|
517
|
+
&& connectedBots.some((bot) => bot.fingerprint === item.bot_fingerprint)).length
|
|
430
518
|
const selectedTaskCount = activeCards.filter((card) => card.bot_fingerprint === selectedBot?.fingerprint).length
|
|
431
|
-
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
519
|
+
const selectedRunnerNeedsPr = ['codex', 'claude-code'].includes(runner)
|
|
520
|
+
const codexAvailable = executors.some((item) => item.id === 'codex-readonly' && item.available)
|
|
521
|
+
const claudeCodeAvailable = executors.some((item) => item.id === 'claude-code' && item.available)
|
|
522
|
+
const nativePrUnavailableReason = executors.find((item) => item.reason)?.reason || null
|
|
523
|
+
const requesterSummary = senderScope === 'allowlist'
|
|
524
|
+
? `仅指定成员:${senderIds.trim() || '尚未填写成员 ID'}`
|
|
525
|
+
: conversationKind === 'group' ? `群 ${groupId || '尚未选择'} 的所有成员` : '任何能私聊此机器人的人'
|
|
526
|
+
const resultRouteSummary = conversationKind === 'group' ? '回复到原群(群内可见)' : '回复到发起任务的私聊'
|
|
527
|
+
const cardBlockReason = enrollmentLoadError ? '暂时无法核对机器人是否已启用协作,请刷新状态后再试。'
|
|
528
|
+
: !botConnected || !selectedEnrollmentBound ? '先在上方把机器人绑定到这台 DSH。'
|
|
529
|
+
: conversationKind === 'group' && loadFailures.includes('groups') ? '暂时无法核对目标群,请刷新状态后再试。'
|
|
530
|
+
: conversationKind === 'group' && !groupId ? '请在“参数与授权范围”中选择目标群;若列表为空,让群成员先 @机器人,再刷新群列表。'
|
|
531
|
+
: senderScope === 'allowlist' && !senderIds.trim() ? '请在“参数与授权范围”中填写可请求成员的 IM ID。'
|
|
532
|
+
: parameters.some((rule) => rule.kind === 'gitea_pr') && !tokenConfigured ? '请先在“本机与任务”中保存本机 Gitea 只读令牌。'
|
|
533
|
+
: parameters.some((rule) => rule.kind === 'gitea_pr') && !selectedEnrollment?.gitea_origin
|
|
534
|
+
? '请先在“机器人与聊天”中登记这个机器人的可信 Gitea 地址。'
|
|
535
|
+
: parameters.some((rule) => rule.kind === 'gitea_pr')
|
|
536
|
+
&& normalizeServiceUrl(trustedOrigin) !== selectedEnrollment?.gitea_origin
|
|
537
|
+
? '请先保存机器人级可信 Gitea 地址;PR 参数会自动使用这个地址。'
|
|
538
|
+
: selectedRunnerNeedsPr && (parameters.length !== 1 || parameters[0].kind !== 'gitea_pr')
|
|
539
|
+
? 'Codex 和 Claude Code 目前只支持单个 Gitea PR 审查任务。'
|
|
540
|
+
: selectedRunnerNeedsPr && !parameters[0].required
|
|
541
|
+
? '本机代码审查的 Gitea PR 编号必须设为必填。'
|
|
542
|
+
: runner === 'codex' && !codexAvailable
|
|
543
|
+
? '这台 DSH 尚未找到可用的 Codex CLI,请先安装或检查 DSH 的 PATH。'
|
|
544
|
+
: runner === 'claude-code' && !claudeCodeAvailable
|
|
545
|
+
? '这台 DSH 尚未注册 Claude Code 子代理,请先在 DSH 中安装并启用它。'
|
|
436
546
|
: ''
|
|
547
|
+
const loadNames = { bots: '机器人', 'enrolled-bots': '协作启用状态', groups: '群聊列表',
|
|
548
|
+
cards: '任务授权', settings: '团队默认值', executors: '本机执行器',
|
|
549
|
+
'local-settings': '本机设置', status: '最近任务' }
|
|
437
550
|
const updateParameter = (index, patch) => setParameters((current) => current.map((rule, i) => i === index ? { ...rule, ...patch } : rule))
|
|
551
|
+
const preparePrExample = () => {
|
|
552
|
+
setTitle('审查 Gitea PR 差异')
|
|
553
|
+
setIntent('同事希望查看指定 Gitea PR 的代码差异并获得审查意见。')
|
|
554
|
+
setExamples('请帮我审查 123 号 PR\n看看刚才提到的 PR 有什么风险')
|
|
555
|
+
setTaskPrompt('结合已核验 PR 的完整代码快照和差异,给出可定位的问题;没有足够上下文时说明局限,不修改代码。')
|
|
556
|
+
setParameters([{ name: 'pr_number', kind: 'gitea_pr', required: true,
|
|
557
|
+
description: '要审查的 PR 编号', min: '1', max: '', choices: '',
|
|
558
|
+
gitea_origin: selectedEnrollment?.gitea_origin || trustedOrigin || '', repository: '' }])
|
|
559
|
+
setRunner(codexAvailable ? 'codex' : claudeCodeAvailable ? 'claude-code' : 'dsh')
|
|
560
|
+
setCreateOpen(true)
|
|
561
|
+
}
|
|
438
562
|
const saveGiteaToken = async () => {
|
|
439
563
|
if (!giteaToken.trim()) return
|
|
440
564
|
setBusy(true)
|
|
@@ -448,24 +572,32 @@ window.__ModuleLoader__.load({
|
|
|
448
572
|
}
|
|
449
573
|
const enrollBot = async () => {
|
|
450
574
|
if (!selectedBot) return
|
|
575
|
+
const requestedOrigin = trustedOrigin
|
|
576
|
+
const origin = requestedOrigin?.trim() ? normalizeServiceUrl(requestedOrigin) : ''
|
|
577
|
+
if (requestedOrigin?.trim() && !origin) {
|
|
578
|
+
setMessage('请填写有效的 HTTPS Gitea 地址后再启用协作。')
|
|
579
|
+
return
|
|
580
|
+
}
|
|
451
581
|
setBusy(true)
|
|
452
582
|
try {
|
|
453
583
|
const response = await fetch('/api/ant-teams/delegation/enrolled-bots', {
|
|
454
584
|
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
455
|
-
body: JSON.stringify({ bot_id: selectedBot.bot_id, bot_fingerprint: selectedBot.fingerprint
|
|
585
|
+
body: JSON.stringify({ bot_id: selectedBot.bot_id, bot_fingerprint: selectedBot.fingerprint,
|
|
586
|
+
gitea_origin: origin }),
|
|
456
587
|
})
|
|
457
588
|
const result = await readLocalJsonResponse(response)
|
|
458
589
|
if (!response.ok) throw new Error(result.message || '协作机器人启用失败。')
|
|
459
590
|
await load()
|
|
460
|
-
|
|
591
|
+
if (origin) setTrustedOrigin(origin)
|
|
592
|
+
setMessage('协作机器人已启用。你可以开放能力;私聊时也能正常聊天。')
|
|
461
593
|
} catch (error) { setMessage(error.message) }
|
|
462
594
|
finally { setBusy(false) }
|
|
463
595
|
}
|
|
464
596
|
const saveTrustedOrigin = async () => {
|
|
465
|
-
if (!selectedBot || !
|
|
597
|
+
if (!selectedBot || !selectedEnrollmentBound) return
|
|
466
598
|
const origin = normalizeServiceUrl(trustedOrigin)
|
|
467
599
|
if (!origin) { setMessage('请填写有效的 HTTPS Gitea 地址。'); return }
|
|
468
|
-
if (!window.confirm(`将这个机器人的可信 Gitea 地址设为 ${origin}
|
|
600
|
+
if (!window.confirm(`将这个机器人的可信 Gitea 地址设为 ${origin}?已开放能力中的 PR 参数需要重新核对。`)) return
|
|
469
601
|
setBusy(true)
|
|
470
602
|
try {
|
|
471
603
|
const response = await fetch('/api/ant-teams/delegation/enrolled-bots', {
|
|
@@ -480,7 +612,7 @@ window.__ModuleLoader__.load({
|
|
|
480
612
|
finally { setBusy(false) }
|
|
481
613
|
}
|
|
482
614
|
const issueBindingCode = async () => {
|
|
483
|
-
if (!selectedBot || !
|
|
615
|
+
if (!selectedBot || !selectedEnrollmentBound) return
|
|
484
616
|
setBusy(true)
|
|
485
617
|
try {
|
|
486
618
|
const response = await fetch('/api/ant-teams/delegation/owner-challenge', {
|
|
@@ -494,53 +626,62 @@ window.__ModuleLoader__.load({
|
|
|
494
626
|
} catch (error) { setMessage(error.message) }
|
|
495
627
|
finally { setBusy(false) }
|
|
496
628
|
}
|
|
497
|
-
const
|
|
498
|
-
|
|
629
|
+
const saveGroupChat = async () => {
|
|
630
|
+
if (!selectedBot || !selectedEnrollmentBound || !chatGroupId.trim()) return
|
|
631
|
+
const enabled = !selectedChatGroup?.chat_enabled
|
|
632
|
+
if (!enabled && selectedGroupHasTasks) {
|
|
633
|
+
setMessage('这个群还有开放能力。请先停止这些能力,再关闭群聊。')
|
|
634
|
+
return
|
|
635
|
+
}
|
|
636
|
+
if (!window.confirm(enabled
|
|
637
|
+
? selectedGroupHasTasks
|
|
638
|
+
? '这个群当前因任务授权可以聊天。要在任务到期后仍允许普通聊天吗?聊天会使用团队模型额度。'
|
|
639
|
+
: '允许这个群与机器人自然聊天?群成员仍不能执行未开放的能力,聊天会使用团队模型额度。'
|
|
640
|
+
: '关闭这个群的普通聊天?已有能力不会自动撤销。')) return
|
|
499
641
|
setBusy(true)
|
|
500
642
|
try {
|
|
501
|
-
const response = await fetch('/api/ant-teams/delegation/
|
|
643
|
+
const response = await fetch('/api/ant-teams/delegation/groups', {
|
|
502
644
|
method: 'PUT', headers: { 'content-type': 'application/json' },
|
|
503
|
-
body: JSON.stringify({
|
|
645
|
+
body: JSON.stringify({ bot_id: selectedBot.bot_id,
|
|
646
|
+
bot_fingerprint: selectedBot.fingerprint, group_id: chatGroupId.trim(), enabled }),
|
|
504
647
|
})
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
648
|
+
if (!response.ok) {
|
|
649
|
+
const result = await readLocalJsonResponse(response)
|
|
650
|
+
throw new Error(result.message || '群聊设置保存失败。')
|
|
651
|
+
}
|
|
652
|
+
await load()
|
|
653
|
+
setMessage(enabled ? '这个群现在可以与机器人聊天;可执行能力仍需逐项开放。' : '已关闭这个群的普通聊天。')
|
|
509
654
|
} catch (error) { setMessage(error.message) }
|
|
510
655
|
finally { setBusy(false) }
|
|
511
656
|
}
|
|
512
|
-
const
|
|
657
|
+
const saveLocalProcessingSettings = async (event) => {
|
|
513
658
|
event.preventDefault()
|
|
514
|
-
if (prReviewExecutor !== 'dsh') {
|
|
515
|
-
if (!executors.some((item) => item.id === prReviewExecutor && item.available)) {
|
|
516
|
-
setMessage('所选执行器尚未在这台 DSH 启用。'); return
|
|
517
|
-
}
|
|
518
|
-
if (!window.confirm(`启用 ${prReviewExecutor} 的指定 PR 差异审查试用?这会应用于本机所有现有的指定 PR 授权。同事只能请求已授权的 PR,ANT 会固定审查内容和临时目录;但执行器仍沿用你本机的权限,可能读取或修改其他本机文件。结果会回复原私聊或群。`)) return
|
|
519
|
-
}
|
|
520
659
|
setBusy(true)
|
|
521
660
|
try {
|
|
522
661
|
const response = await fetch('/api/ant-teams/delegation/local-settings', {
|
|
523
662
|
method: 'PUT', headers: { 'content-type': 'application/json' },
|
|
524
|
-
body: JSON.stringify({
|
|
525
|
-
|
|
663
|
+
body: JSON.stringify({
|
|
664
|
+
max_concurrency: localConcurrency === '' ? null : Number(localConcurrency),
|
|
665
|
+
repository_cache_limit_gib: Number(repositoryCacheLimitGib),
|
|
666
|
+
}),
|
|
526
667
|
})
|
|
527
668
|
const result = await readLocalJsonResponse(response)
|
|
528
|
-
if (!response.ok) throw new Error(result.message || '
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
669
|
+
if (!response.ok) throw new Error(result.message || '本机处理设置保存失败。')
|
|
670
|
+
setLocalConcurrency(result.max_concurrency)
|
|
671
|
+
setRepositoryCacheLimitGib(result.repository_cache_limit_gib)
|
|
672
|
+
setMessage('本机并发与仓库缓存设置已保存。')
|
|
532
673
|
} catch (error) { setMessage(error.message) }
|
|
533
674
|
finally { setBusy(false) }
|
|
534
675
|
}
|
|
535
676
|
const createCard = async (event) => {
|
|
536
677
|
event.preventDefault()
|
|
537
|
-
if (!selectedBot || !botConnected || !
|
|
678
|
+
if (!selectedBot || !botConnected || !selectedEnrollmentBound) { setMessage('请先在这台 DSH 绑定选中的协作机器人。'); return }
|
|
538
679
|
if (!title.trim() || !intent.trim() || !taskPrompt.trim()) {
|
|
539
|
-
setMessage('
|
|
680
|
+
setMessage('请填写能力名称、适用请求说明和处理要求。'); return
|
|
540
681
|
}
|
|
541
682
|
const bytes = (value) => new TextEncoder().encode(value).length
|
|
542
683
|
if (bytes(title.trim()) > 100 || bytes(intent.trim()) > 1000 || bytes(taskPrompt.trim()) > 4000) {
|
|
543
|
-
setMessage('
|
|
684
|
+
setMessage('能力名称、请求说明或处理要求过长,请缩短后再提交。'); return
|
|
544
685
|
}
|
|
545
686
|
const exampleLines = examples.split('\n').map((item) => item.trim()).filter(Boolean)
|
|
546
687
|
if (exampleLines.length > 8 || exampleLines.some((item) => bytes(item) > 500)) {
|
|
@@ -551,7 +692,7 @@ window.__ModuleLoader__.load({
|
|
|
551
692
|
required: rule.required, min: rule.min === '' ? null : Number(rule.min),
|
|
552
693
|
max: rule.max === '' ? null : Number(rule.max),
|
|
553
694
|
choices: rule.kind === 'choice' ? rule.choices.split(/[,,\n]/u).map((item) => item.trim()).filter(Boolean) : null,
|
|
554
|
-
gitea_origin: rule.kind === 'gitea_pr' ?
|
|
695
|
+
gitea_origin: rule.kind === 'gitea_pr' ? selectedEnrollment?.gitea_origin || null : null,
|
|
555
696
|
repository: rule.kind === 'gitea_pr' ? rule.repository.trim().toLowerCase() : null }))
|
|
556
697
|
if (normalized.some((rule) => !/^[A-Za-z][A-Za-z0-9_]{0,31}$/u.test(rule.name))) {
|
|
557
698
|
setMessage('参数键请用英文字母开头,只能包含英文字母、数字和下划线;给同事看的中文问题可填在“参数说明”。'); return
|
|
@@ -586,50 +727,57 @@ window.__ModuleLoader__.load({
|
|
|
586
727
|
if (normalized.some((rule) => rule.kind === 'gitea_pr' && !validRepository(rule.repository))) {
|
|
587
728
|
setMessage('Gitea 仓库请填写“团队名/仓库名”,不要包含空格或多余路径。'); return
|
|
588
729
|
}
|
|
730
|
+
if (['codex', 'claude-code'].includes(runner) && (normalized.length !== 1
|
|
731
|
+
|| normalized[0].kind !== 'gitea_pr' || !normalized[0].required)) {
|
|
732
|
+
setMessage('Codex 和 Claude Code 目前需要一个必填的 Gitea PR 编号参数。'); return
|
|
733
|
+
}
|
|
734
|
+
if (runner === 'codex' && !codexAvailable) {
|
|
735
|
+
setMessage('这台 DSH 尚未找到可用的 Codex CLI。'); return
|
|
736
|
+
}
|
|
737
|
+
if (runner === 'claude-code' && !claudeCodeAvailable) {
|
|
738
|
+
setMessage('这台 DSH 尚未注册 Claude Code 子代理。'); return
|
|
739
|
+
}
|
|
589
740
|
const scope = conversationKind === 'group' ? `群 ${groupId}` : '私聊'
|
|
590
|
-
|
|
741
|
+
const resultDestination = conversationKind === 'group' ? '原群(群内可见)' : '发起任务的私聊'
|
|
742
|
+
const executionInfo = runner === 'codex'
|
|
743
|
+
? 'ANT 会使用这台电脑已登录的 Codex,在已核验 PR Head 的临时完整代码快照中固定只读权限;会传入所有者要求、请求人原话和 PR 差异,不传 Gitea 令牌。'
|
|
744
|
+
: runner === 'claude-code'
|
|
745
|
+
? 'ANT 会调用这台 DSH 已注册的 Claude Code 子代理,在已核验 PR Head 的临时完整代码快照中审查;会传入所有者要求、请求人原话和 PR 差异,不传 Gitea 令牌。它沿用 DSH 中 Claude Code 的本机权限设置;请确认该设置适合由获准成员请求审查。'
|
|
746
|
+
: '这项任务仅由受限 DSH 处理已提供的文字。'
|
|
747
|
+
if (!window.confirm(`开放“${title}”后,${requesterSummary}可在${scope}请求,有效期 ${days} 天;结果${resultDestination}。${executionInfo}请确认。`)) return
|
|
591
748
|
setBusy(true)
|
|
592
749
|
try {
|
|
593
750
|
const response = await fetch('/api/ant-teams/delegation/cards', {
|
|
594
751
|
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
595
752
|
body: JSON.stringify({ bot_id: selectedBot.bot_id, bot_fingerprint: selectedBot.fingerprint,
|
|
596
753
|
title, intent, examples: exampleLines,
|
|
597
|
-
parameters: normalized, task_prompt: taskPrompt, runner
|
|
754
|
+
parameters: normalized, task_prompt: taskPrompt, runner, workspace: null,
|
|
598
755
|
sender_scope: senderScope, sender_ids: senderScope === 'allowlist'
|
|
599
756
|
? senderIds.split(/[\s,,]+/u).filter(Boolean) : [], conversation_kind: conversationKind,
|
|
600
757
|
...(conversationKind === 'group' ? { conversation_id: groupId } : {}),
|
|
601
758
|
expires_at: Math.floor(Date.now() / 1000) + Number(days) * 86400 }),
|
|
602
759
|
})
|
|
603
760
|
const result = await readLocalJsonResponse(response)
|
|
604
|
-
if (!response.ok
|
|
605
|
-
|
|
761
|
+
if (!response.ok && runner === 'claude-code' && result.code === 'INVALID_REQUEST') {
|
|
762
|
+
throw new Error('当前团队服务端可能尚不支持 Claude Code 执行方。请管理员升级服务端后刷新重试。')
|
|
763
|
+
}
|
|
764
|
+
if (!response.ok) throw new Error(result.message || '能力保存失败。')
|
|
765
|
+
setTitle(''); setIntent(''); setExamples(''); setTaskPrompt(''); setParameters([]); setRunner('dsh')
|
|
606
766
|
await load()
|
|
607
767
|
setCreateOpen(false)
|
|
608
|
-
setMessage('
|
|
768
|
+
setMessage('能力已开放。同事可自然地提出这类请求;参数不完整时机器人会追问。')
|
|
609
769
|
} catch (error) { setMessage(error.message) }
|
|
610
770
|
finally { setBusy(false) }
|
|
611
771
|
}
|
|
612
772
|
const revokeCard = async (card) => {
|
|
613
|
-
if (!window.confirm(
|
|
773
|
+
if (!window.confirm(`停止开放“${card.title}”?正在处理的任务无法撤销已经发生的读取。`)) return
|
|
614
774
|
setBusy(true)
|
|
615
775
|
try {
|
|
616
776
|
const response = await fetch('/api/ant-teams/delegation/cards', {
|
|
617
777
|
method: 'DELETE', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: card.id }),
|
|
618
778
|
})
|
|
619
|
-
if (!response.ok) throw new Error('
|
|
620
|
-
await load(); setMessage('
|
|
621
|
-
} catch (error) { setMessage(error.message) }
|
|
622
|
-
finally { setBusy(false) }
|
|
623
|
-
}
|
|
624
|
-
const revokeGrant = async (grant) => {
|
|
625
|
-
if (!window.confirm(`停止 ${grant.repository} PR #${grant.pr_number} 的新审查请求?正在处理的任务不会因此撤销。`)) return
|
|
626
|
-
setBusy(true)
|
|
627
|
-
try {
|
|
628
|
-
const response = await fetch('/api/ant-teams/delegation/grants', {
|
|
629
|
-
method: 'DELETE', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: grant.id }),
|
|
630
|
-
})
|
|
631
|
-
if (!response.ok) throw new Error('停止 PR 授权失败,请重试。')
|
|
632
|
-
await load(); onGrantChanged?.(); setMessage('已停止这项 PR 授权;其他聊天和任务不受影响。')
|
|
779
|
+
if (!response.ok) throw new Error('停止能力失败,请重试。')
|
|
780
|
+
await load(); setMessage('能力已停止开放。')
|
|
633
781
|
} catch (error) { setMessage(error.message) }
|
|
634
782
|
finally { setBusy(false) }
|
|
635
783
|
}
|
|
@@ -645,24 +793,41 @@ window.__ModuleLoader__.load({
|
|
|
645
793
|
} catch (error) { setMessage(error.message) }
|
|
646
794
|
finally { setBusy(false) }
|
|
647
795
|
}
|
|
648
|
-
return h('section', { style: panelStyle,
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
h('p', { style: mutedTextStyle },
|
|
796
|
+
return h('section', { style: { ...panelStyle,
|
|
797
|
+
display: ['bot-chat', 'capability', 'local'].includes(activeTab) ? 'grid' : 'none' },
|
|
798
|
+
'aria-label': '协作机器人与开放能力' },
|
|
799
|
+
message ? h('p', { role: 'status', style: mutedTextStyle }, message) : null,
|
|
800
|
+
h('div', { role: 'region', 'aria-label': '机器人连接和聊天范围', style: { display: activeTab === 'bot-chat' ? 'grid' : 'none', gap: 14 } },
|
|
801
|
+
h('h3', { style: { margin: 0, fontSize: 16 } }, '协作机器人'),
|
|
802
|
+
h('p', { style: mutedTextStyle }, '同事可以像平常一样和机器人聊天。DSH 负责理解与回答;只有准备执行你开放的能力时,ANT 才核对请求人、群、参数和权限。群里的任务结果会回复原群。'),
|
|
803
|
+
h('p', { role: 'status', style: mutedTextStyle }, '每项能力都单独设定执行方和请求范围。普通对话不会自动获得读取电脑、调用工具或访问其他群的权限。'),
|
|
804
|
+
loadFailures.length ? h('p', { role: 'status', style: mutedTextStyle },
|
|
805
|
+
`部分协作信息暂时无法读取:${loadFailures.map((path) => loadNames[path]).join('、')}。已读取的信息仍显示,请稍后刷新状态。`) : null,
|
|
652
806
|
h('div', { style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, padding: 12,
|
|
653
807
|
display: 'grid', gap: 8 } },
|
|
654
|
-
h('strong', null, '
|
|
808
|
+
h('strong', null, '机器人和聊天范围'),
|
|
809
|
+
h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 240px), 1fr))', gap: 8 } },
|
|
810
|
+
h('div', { style: { padding: 10, borderRadius: 8, backgroundColor: 'var(--dsw-alias-bg-layer-2)' } },
|
|
811
|
+
h('strong', null, '私聊'), h('p', { style: { ...mutedTextStyle, marginTop: 4 } }, '默认可用,可正常聊天;执行任务仍需单独授权。')),
|
|
812
|
+
h('div', { style: { padding: 10, borderRadius: 8, backgroundColor: 'var(--dsw-alias-bg-layer-2)' } },
|
|
813
|
+
h('strong', null, '群聊'), h('p', { style: { ...mutedTextStyle, marginTop: 4 } }, '可同时启用;群成员是否能执行任务由每项能力授权决定。'))),
|
|
655
814
|
h('span', { role: 'status', style: mutedTextStyle }, loading ? '正在读取机器人与任务状态…'
|
|
656
815
|
: botLoadError ? '暂时无法读取机器人状态。请刷新后再试。'
|
|
657
816
|
: connectedBots.length === 0 ? '尚未发现已连接的机器人。请先在 DSH-IM 中连接飞书机器人。'
|
|
658
817
|
: selectedBot && !botConnected ? `“${selectedBot.name}”当前离线;请先在 DSH-IM 恢复连接。`
|
|
659
|
-
:
|
|
818
|
+
: enrollmentLoadError ? selectedBot
|
|
819
|
+
? `“${selectedBot.name}”已连接;暂时无法确认协作是否已启用,请刷新状态。`
|
|
820
|
+
: '机器人已连接;协作启用状态暂未读取,请刷新状态。'
|
|
821
|
+
: selectedBot && !selectedEnrollmentBound ? selectedEnrollment
|
|
822
|
+
? `“${selectedBot.name}”已连接;原协作配置尚未绑定到这台 DSH,请在本机重新启用。若原设备令牌曾被撤销,旧任务权限已关闭,绑定后需重新开放。`
|
|
823
|
+
: `“${selectedBot.name}”已连接;请先启用本机协作。启用后,它仍能正常聊天。`
|
|
660
824
|
: enrolledCount === 0 ? '机器人已连接;请选择并启用协作。启用后,它仍能正常聊天。'
|
|
661
|
-
|
|
662
|
-
:
|
|
663
|
-
|
|
825
|
+
: !selectedBot ? '已有协作机器人;请选择一台,查看已开放的能力并私聊自测。'
|
|
826
|
+
: authorizationLoadError ? `“${selectedBot.name}”已启用协作;已开放任务暂未读取,请刷新状态。普通聊天仍可自测。`
|
|
827
|
+
: selectedTaskCount === 0 ? `“${selectedBot.name}”已启用协作;下一步开放一项能力。你也可以先私聊自测普通聊天。`
|
|
828
|
+
: `“${selectedBot.name}”已启用协作 · ${selectedTaskCount} 项能力已开放。可以私聊自测。`),
|
|
664
829
|
bots.length ? h('label', null, '我的机器人', h('select', { value: botId,
|
|
665
|
-
onChange: (event) => { setBotId(event.target.value); setGroupId(''); setBindingCode('');
|
|
830
|
+
onChange: (event) => { setBotId(event.target.value); setGroupId(''); setChatGroupId(''); setBindingCode('');
|
|
666
831
|
setTrustedOrigin(enrolled.find((bot) => bot.bot_id === event.target.value)?.gitea_origin || '') }, style: inputStyle },
|
|
667
832
|
h('option', { value: '' }, '请选择机器人'),
|
|
668
833
|
...bots.map((bot) => h('option', { key: bot.bot_id, value: bot.bot_id,
|
|
@@ -673,62 +838,118 @@ window.__ModuleLoader__.load({
|
|
|
673
838
|
'若尚未安装 DSH-IM,请先到 DSH 的插件列表安装并连接机器人。') : null,
|
|
674
839
|
selectedBot && !botConnected && connectedBots.length ? h('button', { type: 'button', onClick: openImSettings,
|
|
675
840
|
style: secondaryButtonStyle }, '检查 DSH-IM 连接') : null,
|
|
676
|
-
selectedBot && !
|
|
677
|
-
onClick: () => void enrollBot(), style: secondaryButtonStyle },
|
|
678
|
-
|
|
679
|
-
|
|
841
|
+
selectedBot && !selectedEnrollmentBound ? h('button', { type: 'button', disabled: busy || !botConnected || enrollmentLoadError,
|
|
842
|
+
onClick: () => void enrollBot(), style: secondaryButtonStyle },
|
|
843
|
+
selectedEnrollment ? '绑定原协作配置到这台 DSH' : '启用并绑定到这台 DSH') : null,
|
|
844
|
+
selectedEnrollmentBound && botConnected ? h('p', { style: mutedTextStyle },
|
|
845
|
+
`自测:私聊“${selectedBot.name}”发送“你能做什么”;发起任务后可问“刚才的任务怎么样了”。群聊请先在目标群 @机器人,再开放能力时选择该群。`) : null,
|
|
846
|
+
selectedBot ? h('div', { style: { display: 'grid', gap: 8, padding: 10,
|
|
847
|
+
border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8 } },
|
|
848
|
+
h('strong', null, `可信 Gitea 地址 · ${selectedBot.name}`),
|
|
849
|
+
h('p', { style: mutedTextStyle }, '为这个机器人登记一次。PR 能力只需配置仓库和 PR 编号,不必重复填写地址。'),
|
|
850
|
+
h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } },
|
|
851
|
+
h('input', { 'aria-label': '可信 Gitea 地址', type: 'url', value: trustedOrigin,
|
|
852
|
+
onChange: (event) => setTrustedOrigin(event.target.value), placeholder: 'https://git.example.com',
|
|
853
|
+
style: { ...inputStyle, flex: '1 1 280px' } }),
|
|
854
|
+
h('button', { type: 'button', disabled: busy || !trustedOrigin.trim() || (!selectedEnrollmentBound && !botConnected),
|
|
855
|
+
onClick: () => void (selectedEnrollmentBound ? saveTrustedOrigin() : enrollBot()), style: secondaryButtonStyle },
|
|
856
|
+
selectedEnrollmentBound ? '保存可信地址' : botConnected ? '绑定机器人并保存地址' : '先连接机器人'))) : null,
|
|
857
|
+
selectedEnrollmentBound && botConnected ? h('div', { style: { display: 'grid', gap: 8 } },
|
|
858
|
+
h('strong', null, '允许聊天的群'),
|
|
859
|
+
h('span', { style: mutedTextStyle }, '群成员可以正常聊天;是否能让机器人办事,由每项能力单独决定。若机器人尚未发现群,可以直接填群 ID。'),
|
|
860
|
+
h('select', { 'aria-label': '已记录的群', value: availableGroups.some((group) => group.group_id === chatGroupId) ? chatGroupId : '',
|
|
861
|
+
onChange: (event) => setChatGroupId(event.target.value), style: inputStyle },
|
|
862
|
+
h('option', { value: '' }, '从已记录的群中选择'),
|
|
863
|
+
...availableGroups.map((group) => h('option', { key: group.group_id, value: group.group_id },
|
|
864
|
+
`${group.group_code} · ${group.chat_enabled ? '已允许聊天' : '未允许聊天'}`))),
|
|
865
|
+
h('input', { 'aria-label': '群 ID', value: chatGroupId, maxLength: 256,
|
|
866
|
+
onChange: (event) => setChatGroupId(event.target.value.trim()),
|
|
867
|
+
placeholder: '也可以手动填写群 ID', style: inputStyle }),
|
|
868
|
+
h('button', { type: 'button', disabled: busy || !chatGroupId.trim()
|
|
869
|
+
|| (selectedChatGroup?.chat_enabled && selectedGroupHasTasks),
|
|
870
|
+
onClick: () => void saveGroupChat(), style: secondaryButtonStyle },
|
|
871
|
+
selectedChatGroup?.chat_enabled ? '关闭此群聊天'
|
|
872
|
+
: selectedGroupHasTasks ? '能力到期后仍允许聊天' : '允许此群聊天'),
|
|
873
|
+
selectedChatGroup?.chat_enabled && selectedGroupHasTasks ? h('span', { style: mutedTextStyle },
|
|
874
|
+
'本群还有开放能力;关闭聊天前请先停止这些能力。') : null) : null,
|
|
680
875
|
h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } },
|
|
681
|
-
h('button', { type: 'button', onClick: () => { setCreateOpen(true)
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
h('button', { type: 'button', onClick: openPrReview, style: secondaryButtonStyle }, '开放指定 PR 审查'),
|
|
876
|
+
h('button', { type: 'button', onClick: () => { setCreateOpen(true); onSelectTab('capability') },
|
|
877
|
+
style: primaryButtonStyle }, '开放新能力'),
|
|
878
|
+
h('button', { type: 'button', onClick: () => { preparePrExample(); onSelectTab('capability') }, style: secondaryButtonStyle }, '使用 PR 差异审查示例'),
|
|
685
879
|
h('button', { type: 'button', onClick: () => void load().catch((error) => setMessage(error.message)),
|
|
686
|
-
style: secondaryButtonStyle }, '刷新状态'))),
|
|
687
|
-
h('
|
|
880
|
+
style: secondaryButtonStyle }, '刷新状态')))),
|
|
881
|
+
h('div', { role: 'region', 'aria-label': '已开放能力列表', style: { display: activeTab === 'capability' ? 'grid' : 'none', gap: 12 } },
|
|
882
|
+
h('h4', { style: { marginBottom: 0 } }, `我开放的能力 · ${authorizationLoadError ? '暂未读取' : activeCards.length}`),
|
|
688
883
|
...activeCards.map((card) => h('div', { key: card.id, style: { display: 'flex', gap: 12,
|
|
689
884
|
alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: 8 } },
|
|
690
|
-
h('span', { style: { flex: '1 1 280px', minWidth: 0, overflowWrap: 'anywhere' } }, `${card.title} · ${bots.find((bot) => bot.fingerprint === card.bot_fingerprint)?.name || '机器人'} · ${card.conversation_kind === 'group' ? `群 ${groups.find((group) => group.group_id === card.conversation_id)?.group_code || card.conversation_id}` : '私聊'} · ${card.sender_scope === 'allowlist' ? '仅指定成员可请求' : card.conversation_kind === 'group' ? '群成员可请求' : '可私聊者可请求'} · 结果回复原${card.conversation_kind === 'group' ? '群' : '私聊'} · DSH 文字处理 · ${new Date(card.expires_at * 1000).toLocaleString('zh-CN')} 到期`),
|
|
885
|
+
h('span', { style: { flex: '1 1 280px', minWidth: 0, overflowWrap: 'anywhere' } }, `${card.title} · ${bots.find((bot) => bot.fingerprint === card.bot_fingerprint)?.name || '机器人'} · ${card.conversation_kind === 'group' ? `群 ${groups.find((group) => group.group_id === card.conversation_id)?.group_code || card.conversation_id}` : '私聊'} · ${card.sender_scope === 'allowlist' ? '仅指定成员可请求' : card.conversation_kind === 'group' ? '群成员可请求' : '可私聊者可请求'} · 结果回复原${card.conversation_kind === 'group' ? '群' : '私聊'} · ${card.runner === 'codex' ? 'Codex PR 快照审查试用' : card.runner === 'claude-code' ? 'Claude Code PR 完整快照审查试用' : 'DSH 文字处理'} · ${new Date(card.expires_at * 1000).toLocaleString('zh-CN')} 到期`),
|
|
691
886
|
h('button', { type: 'button', disabled: busy, onClick: () => void revokeCard(card), style: secondaryButtonStyle }, '停止'))),
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
h('button', { type: 'button', onClick: openPrReview, style: secondaryButtonStyle }, '查看设置'),
|
|
697
|
-
h('button', { type: 'button', disabled: busy, onClick: () => void revokeGrant(grant),
|
|
698
|
-
style: secondaryButtonStyle }, '停止')))),
|
|
699
|
-
activeCards.length + activeGrants.length === 0 ? h('p', { style: mutedTextStyle },
|
|
700
|
-
'还没有开放任务。先选机器人,再用上方按钮创建文字任务或指定 PR 审查。') : null,
|
|
887
|
+
activeCards.length === 0 ? h('p', { style: mutedTextStyle },
|
|
888
|
+
authorizationLoadError ? '暂时无法确认已开放的能力,请刷新状态后再试。'
|
|
889
|
+
: '还没有开放能力。先选机器人,再开放一项能力;PR 审查只是可选示例。') : null),
|
|
890
|
+
h('div', { role: 'region', 'aria-label': '最近任务', style: { display: activeTab === 'local' ? 'grid' : 'none', gap: 12 } },
|
|
701
891
|
h('h4', { style: { marginBottom: 0 } }, '最近任务'),
|
|
702
892
|
...(tasks.filter((task) => task.task_id).length ? tasks.filter((task) => task.task_id)
|
|
703
|
-
.map((task) =>
|
|
704
|
-
|
|
705
|
-
|
|
893
|
+
.map((task) => {
|
|
894
|
+
const title = task.title || (task.pr_number
|
|
895
|
+
? `${task.repository ? `${task.repository} ` : ''}PR #${task.pr_number} 差异审查` : '任务')
|
|
896
|
+
const status = task.outcome === 'canceled' ? '已取消' : task.outcome === 'failed' ? '未完成' : ({
|
|
706
897
|
pending: '排队中', running: '处理中', reply_pending: '结果待发送', reply_attempted: '回复状态待核对', done: '已回复',
|
|
707
|
-
}[task.status] || task.status)
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
898
|
+
}[task.status] || task.status)
|
|
899
|
+
const createdAt = task.created_at && Number.isFinite(Date.parse(task.created_at))
|
|
900
|
+
? new Date(task.created_at).toLocaleString('zh-CN') : ''
|
|
901
|
+
return h('div', { key: task.task_id, style: { display: 'flex', gap: 12, alignItems: 'center',
|
|
902
|
+
justifyContent: 'space-between', flexWrap: 'wrap', borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: 8 } },
|
|
903
|
+
h('div', { style: { flex: '1 1 280px', minWidth: 0, overflowWrap: 'anywhere' } },
|
|
904
|
+
h('strong', null, title),
|
|
905
|
+
h('div', { style: mutedTextStyle }, `${status}${createdAt ? ` · ${createdAt}` : ''} · 任务 ${task.task_id}`)),
|
|
906
|
+
['card_task', 'legacy_pr_task'].includes(task.kind) && ['pending', 'running'].includes(task.status) ? h('button', { type: 'button', disabled: busy,
|
|
907
|
+
onClick: () => void cancelTask(task), style: secondaryButtonStyle }, '取消任务') : null)
|
|
908
|
+
})
|
|
909
|
+
: [h('p', { key: 'none', style: mutedTextStyle }, loadFailures.includes('status')
|
|
910
|
+
? '任务记录暂时无法读取,请刷新状态。' : '暂无任务。')])),
|
|
911
|
+
h('div', { role: 'region', 'aria-label': '新建能力', style: { display: activeTab === 'capability' ? 'grid' : 'none' } },
|
|
711
912
|
h('details', { id: 'ant-new-task', open: createOpen,
|
|
712
913
|
onToggle: (event) => setCreateOpen(event.currentTarget.open),
|
|
713
914
|
style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, padding: 12 } },
|
|
714
|
-
h('summary', { style: { cursor: 'pointer', fontWeight: 600 } }, '
|
|
915
|
+
h('summary', { style: { cursor: 'pointer', fontWeight: 600 } }, '开放一项能力'),
|
|
715
916
|
h('form', { onSubmit: createCard, style: { display: 'grid', gap: 9, maxWidth: 760 } },
|
|
716
917
|
h('p', { style: mutedTextStyle }, selectedBot
|
|
717
|
-
? `当前机器人:${selectedBot.name}${
|
|
918
|
+
? `当前机器人:${selectedBot.name}${selectedEnrollmentBound ? ' · 已绑定此设备' : ' · 请先绑定到此设备'}`
|
|
718
919
|
: '请先在上方选择机器人并启用协作。'),
|
|
719
|
-
|
|
920
|
+
selectedEnrollmentBound ? h('p', { style: mutedTextStyle },
|
|
921
|
+
'机器人及其任务能力只在这台 DSH 设备接收和执行;同一账号的其他设备不会继承本机授权。') : null,
|
|
922
|
+
selectedEnrollment && !selectedEnrollmentBound ? h('p', { style: mutedTextStyle },
|
|
923
|
+
'这是尚未绑定到设备的机器人。请在要继续使用它的电脑重新启用;Gitea 地址和配置记录会保留。若旧设备访问曾被撤销,原任务权限不会恢复,需要重新开放。') : null,
|
|
720
924
|
selectedBot && conversationKind === 'group' && !selectedBot.history_available ? h('p', { role: 'status', style: mutedTextStyle },
|
|
721
925
|
'这个机器人当前不能读取平台群历史;群聊只能参考实际送达的消息和本机近期记录。请检查 DSH-IM 权限。') : null,
|
|
722
|
-
h('label', null, '
|
|
926
|
+
h('label', null, '能力名称', h('input', { required: true, maxLength: 100, value: title,
|
|
723
927
|
onChange: (event) => setTitle(event.target.value), placeholder: '例如:总结项目进度', style: inputStyle })),
|
|
724
|
-
h('label', null, '
|
|
928
|
+
h('label', null, '什么请求属于这项能力', h('textarea', { required: true, maxLength: 1000, rows: 2, value: intent,
|
|
725
929
|
onChange: (event) => setIntent(event.target.value), placeholder: '例如:把群里讨论的本周进展整理成摘要', style: inputStyle })),
|
|
726
930
|
h('label', null, '同事可能怎么说(每行一句,可选)', h('textarea', { rows: 2, value: examples,
|
|
727
931
|
onChange: (event) => setExamples(event.target.value), placeholder: '帮我总结一下刚才的讨论', style: inputStyle })),
|
|
728
932
|
h('label', null, '处理要求', h('textarea', { required: true, maxLength: 4000, rows: 3, value: taskPrompt,
|
|
729
933
|
onChange: (event) => setTaskPrompt(event.target.value), placeholder: '只使用当前会话可见的内容,列出结论和待办', style: inputStyle })),
|
|
934
|
+
h('label', null, '执行方', h('select', { value: runner,
|
|
935
|
+
onChange: (event) => setRunner(event.target.value), style: inputStyle },
|
|
936
|
+
h('option', { value: 'dsh' }, 'DSH · 已提供文字'),
|
|
937
|
+
h('option', { value: 'codex',
|
|
938
|
+
disabled: !codexAvailable }, 'Codex · 单个 Gitea PR 完整快照审查试用'),
|
|
939
|
+
h('option', { value: 'claude-code', disabled: !claudeCodeAvailable },
|
|
940
|
+
'Claude Code · 单个 Gitea PR 完整快照试用'))),
|
|
941
|
+
nativePrUnavailableReason ? h('p', { role: 'status', style: mutedTextStyle },
|
|
942
|
+
`${nativePrUnavailableReason},已禁用 Codex/Claude Code 完整快照选项;可选择 DSH 差异审查。`) : null,
|
|
943
|
+
runner === 'codex' ? h('p', { role: 'status', style: mutedTextStyle },
|
|
944
|
+
'Codex 使用这台电脑的登录账号及其用量;ANT 为每项能力维护仓库缓存,每次审查创建固定到 PR Head 的临时代码快照。不会收到 Gitea 令牌;请以这台 DSH 的实际子代理权限为准,只有强制只读隔离已验证时才视为只读。Git LFS 实际文件不会下载。') : null,
|
|
945
|
+
runner === 'claude-code' ? h('p', { role: 'status', style: mutedTextStyle },
|
|
946
|
+
'Claude Code 通过 DSH 已注册的子代理运行,可查看本次 PR 的临时代码快照。ANT 不会传 Gitea 令牌;Claude Code 沿用 DSH 提供方的本机权限,不具备 Codex 路径相同的固定只读隔离。Git LFS 实际文件不会下载。') : null,
|
|
947
|
+
selectedRunnerNeedsPr ? h('p', { role: 'status', style: mutedTextStyle },
|
|
948
|
+
'本机 PR 执行方需要 Gitea granular token 的只读权限 read:repository 和 read:user;read:user 仅用于这台 DSH 获取 Git HTTPS 登录名。') : null,
|
|
949
|
+
h('p', { role: 'status', style: mutedTextStyle },
|
|
950
|
+
`开放后:${requesterSummary}可请求 · 有效期 ${days} 天 · ${resultRouteSummary}`),
|
|
730
951
|
h('details', { style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, padding: 10 } },
|
|
731
|
-
h('summary', { style: { cursor: 'pointer' } }, '
|
|
952
|
+
h('summary', { style: { cursor: 'pointer' } }, '参数与授权范围'),
|
|
732
953
|
h('div', null, h('strong', null, '需要从请求中确认的参数'),
|
|
733
954
|
h('p', { style: mutedTextStyle }, '可不添加参数。参数键使用英文,给同事看的中文问题填在参数说明;需要指定 PR 时选“Gitea PR 编号”,系统会核对它是否存在。自由文本留在本机,服务端只核对长度和摘要。')),
|
|
734
955
|
...parameters.map((rule, index) => h('div', { key: index, style: { display: 'flex', flexWrap: 'wrap', gap: 8,
|
|
@@ -750,8 +971,8 @@ window.__ModuleLoader__.load({
|
|
|
750
971
|
onChange: (event) => updateParameter(index, { max: event.target.value }), placeholder: '最大值', style: { ...inputStyle, width: 120 } }) : null,
|
|
751
972
|
rule.kind === 'choice' ? h('input', { 'aria-label': '可选值', value: rule.choices,
|
|
752
973
|
onChange: (event) => updateParameter(index, { choices: event.target.value }), placeholder: '用逗号分隔选项', style: { ...inputStyle, flex: '1 1 250px' } }) : null,
|
|
753
|
-
rule.kind === 'gitea_pr' ? h('
|
|
754
|
-
|
|
974
|
+
rule.kind === 'gitea_pr' ? h('span', { style: mutedTextStyle },
|
|
975
|
+
`使用机器人的可信 Gitea 地址:${selectedEnrollment?.gitea_origin || trustedOrigin || '请在下方登记一次'}`) : null,
|
|
755
976
|
rule.kind === 'gitea_pr' ? h('input', { 'aria-label': 'Gitea 仓库', value: rule.repository,
|
|
756
977
|
onChange: (event) => updateParameter(index, { repository: event.target.value }), placeholder: '团队/仓库', style: { ...inputStyle, flex: '1 1 180px' } }) : null,
|
|
757
978
|
h('button', { type: 'button', style: secondaryButtonStyle,
|
|
@@ -759,20 +980,12 @@ window.__ModuleLoader__.load({
|
|
|
759
980
|
h('button', { type: 'button', disabled: parameters.length >= 12, style: secondaryButtonStyle,
|
|
760
981
|
onClick: () => setParameters((current) => [...current, { name: '', kind: 'text', required: true,
|
|
761
982
|
description: '', min: '', max: '', choices: '', gitea_origin: '', repository: '' }]) }, '添加参数'),
|
|
762
|
-
parameters.some((rule) => rule.kind === 'gitea_pr')
|
|
763
|
-
h('
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
h('button', { type: 'button',
|
|
767
|
-
|
|
768
|
-
parameters.some((rule) => rule.kind === 'gitea_pr') ? h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } },
|
|
769
|
-
h('input', { 'aria-label': '只读 Gitea 访问令牌', type: 'password', autoComplete: 'off', value: giteaToken,
|
|
770
|
-
onChange: (event) => setGiteaToken(event.target.value), placeholder: tokenConfigured ? '本机已保存 Gitea 令牌' : '先保存只读 Gitea 令牌',
|
|
771
|
-
style: { ...inputStyle, flex: '1 1 260px' } }),
|
|
772
|
-
h('button', { type: 'button', disabled: busy || !giteaToken.trim(),
|
|
773
|
-
onClick: () => void saveGiteaToken(), style: secondaryButtonStyle }, '保存令牌')) : null,
|
|
774
|
-
parameters.some((rule) => rule.kind === 'gitea_pr') ? h('p', { style: mutedTextStyle },
|
|
775
|
-
'执行时会用这台 DSH 保存的只读令牌向你登记的可信 Gitea 地址读取 PR;标题和差异会交给当前团队模型处理。') : null,
|
|
983
|
+
parameters.some((rule) => rule.kind === 'gitea_pr') ? h('div', { style: { display: 'grid', gap: 6 } },
|
|
984
|
+
h('p', { style: mutedTextStyle }, selectedEnrollment?.gitea_origin
|
|
985
|
+
? `将使用机器人已登记的可信 Gitea 地址:${selectedEnrollment.gitea_origin}`
|
|
986
|
+
: '请先在“机器人与聊天”中为这个机器人登记可信 Gitea 地址。'),
|
|
987
|
+
!tokenConfigured ? h('button', { type: 'button', onClick: () => onSelectTab('local'), style: secondaryButtonStyle },
|
|
988
|
+
'前往本机与任务保存 Gitea 令牌') : h('p', { style: mutedTextStyle }, '本机 Gitea 只读令牌已保存。')) : null,
|
|
776
989
|
h('label', null, '使用场景', h('select', { value: conversationKind,
|
|
777
990
|
onChange: (event) => { setConversationKind(event.target.value); setGroupId('') }, style: inputStyle },
|
|
778
991
|
h('option', { value: 'dm' }, '私聊'), h('option', { value: 'group' }, '指定群聊'))),
|
|
@@ -783,7 +996,7 @@ window.__ModuleLoader__.load({
|
|
|
783
996
|
`识别码 ${group.group_code} · ${group.group_id}`)))) : null,
|
|
784
997
|
conversationKind === 'group' ? h('button', { type: 'button', disabled: busy,
|
|
785
998
|
onClick: () => void load().catch((error) => setMessage(error.message)), style: secondaryButtonStyle }, '刷新群列表') : null,
|
|
786
|
-
conversationKind === 'group' &&
|
|
999
|
+
conversationKind === 'group' && selectedEnrollmentBound ? h('div', { style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' } },
|
|
787
1000
|
h('span', { style: mutedTextStyle }, selectedEnrollment.owner_bound ? '已绑定你的飞书身份,可在群内批准任务' : '群内批准前需绑定你的飞书身份'),
|
|
788
1001
|
h('button', { type: 'button', disabled: busy, onClick: () => void issueBindingCode(), style: secondaryButtonStyle },
|
|
789
1002
|
selectedEnrollment.owner_bound ? '重新获取绑定码' : '获取身份绑定码')) : null,
|
|
@@ -799,313 +1012,42 @@ window.__ModuleLoader__.load({
|
|
|
799
1012
|
h('option', { value: 1 }, '1 天'), h('option', { value: 7 }, '7 天'), h('option', { value: 30 }, '30 天')))),
|
|
800
1013
|
cardBlockReason ? h('p', { role: 'status', style: mutedTextStyle }, cardBlockReason) : null,
|
|
801
1014
|
h('button', { disabled: busy || Boolean(cardBlockReason),
|
|
802
|
-
style: primaryButtonStyle }, '
|
|
1015
|
+
style: primaryButtonStyle }, '保存并开放能力')))),
|
|
1016
|
+
h('div', { role: 'region', 'aria-label': '本机处理设置', style: { display: activeTab === 'local' ? 'grid' : 'none', gap: 12 } },
|
|
1017
|
+
h('section', { style: { ...panelStyle, padding: 14 } },
|
|
1018
|
+
h('strong', null, 'Gitea 本机凭据'),
|
|
1019
|
+
h('p', { style: mutedTextStyle }, tokenConfigured
|
|
1020
|
+
? '只读访问令牌已保存在这台 DSH 的安全凭据中;不会同步到 ANT 服务端或发送给执行器。'
|
|
1021
|
+
: 'PR 审查能力需要只读访问令牌。令牌只保存到这台 DSH。'),
|
|
1022
|
+
h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } },
|
|
1023
|
+
h('input', { 'aria-label': 'Gitea 只读访问令牌', type: 'password', autoComplete: 'new-password', value: giteaToken,
|
|
1024
|
+
onChange: (event) => setGiteaToken(event.target.value),
|
|
1025
|
+
placeholder: tokenConfigured ? '已保存;输入新令牌可替换' : '粘贴 Gitea 只读令牌',
|
|
1026
|
+
style: { ...inputStyle, flex: '1 1 280px' } }),
|
|
1027
|
+
h('button', { type: 'button', disabled: busy || !giteaToken.trim(),
|
|
1028
|
+
onClick: () => void saveGiteaToken(), style: secondaryButtonStyle }, tokenConfigured ? '替换本机令牌' : '保存本机令牌'))),
|
|
803
1029
|
h('details', { style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, padding: 12 } },
|
|
804
|
-
h('summary', { style: { cursor: 'pointer', fontWeight: 600 } }, '
|
|
805
|
-
h('p', { style: mutedTextStyle }, '
|
|
1030
|
+
h('summary', { style: { cursor: 'pointer', fontWeight: 600 } }, '本机处理设置'),
|
|
1031
|
+
h('p', { style: mutedTextStyle }, '本机并发上限只影响这台电脑接收的新任务;PR 仓库缓存上限按这台 DSH 的长期仓库缓存总量计算,不包括任务临时工作目录。'),
|
|
806
1032
|
h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' } },
|
|
807
|
-
h('span', { style: mutedTextStyle },
|
|
1033
|
+
h('span', { style: mutedTextStyle }, loadFailures.includes('settings') ? '团队默认值暂未读取。'
|
|
1034
|
+
: `团队默认:先看 ${defaults?.context_messages ?? 10} 条前文、最多翻阅 ${defaults?.history_max_records ?? 100} 条平台记录、同时处理 ${defaults?.max_concurrency ?? 3} 项任务。`),
|
|
808
1035
|
h('button', { type: 'button', onClick: () => void load().catch((error) => setMessage(error.message)), style: secondaryButtonStyle }, '刷新')),
|
|
809
|
-
|
|
1036
|
+
loadFailures.includes('local-settings') ? h('p', { role: 'status', style: mutedTextStyle },
|
|
1037
|
+
'本机处理设置暂未读取;请刷新后重试。')
|
|
1038
|
+
: h('form', { onSubmit: saveLocalProcessingSettings, style: { display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'end' } },
|
|
810
1039
|
h('label', null, '这台电脑最多同时处理',
|
|
811
1040
|
h('select', { value: localConcurrency == null ? '' : String(localConcurrency),
|
|
812
1041
|
onChange: (event) => setLocalConcurrency(event.target.value), style: inputStyle },
|
|
813
1042
|
h('option', { value: '' }, '跟随团队默认'),
|
|
814
1043
|
...Array.from({ length: 8 }, (_, index) => h('option', { key: index + 1, value: index + 1 }, `${index + 1} 项任务`)))),
|
|
815
|
-
h('
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
h('option', { value: 'dsh' }, 'DSH 受限文字审查(默认)'),
|
|
823
|
-
...[['codex', 'Codex'], ['claude-code', 'Claude Code']].map(([id, title]) => {
|
|
824
|
-
const available = executors.some((item) => item.id === id && item.available)
|
|
825
|
-
return h('option', { key: id, value: id, disabled: !available },
|
|
826
|
-
`${title}${available ? ' · 本机权限试用' : ' · 未启用或未读取到'}`)
|
|
827
|
-
}))),
|
|
828
|
-
h('button', { disabled: busy, style: secondaryButtonStyle }, '保存审查执行方')),
|
|
829
|
-
prReviewExecutor !== 'dsh' ? h('p', { role: 'status', style: mutedTextStyle },
|
|
830
|
-
'选择本机执行器试用后,它沿用你在 Codex/Claude Code 中已有的权限;ANT 固定 PR 和启动参数,但不能保证执行器只读。') : null),
|
|
831
|
-
message ? h('p', { role: 'status', style: mutedTextStyle }, message) : null)
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
function DelegationSettings({ remote, revision, onChanged, open, onOpenChange }) {
|
|
835
|
-
const [bots, setBots] = useState([])
|
|
836
|
-
const [enrolledBots, setEnrolledBots] = useState([])
|
|
837
|
-
const [groups, setGroups] = useState([])
|
|
838
|
-
const [grants, setGrants] = useState([])
|
|
839
|
-
const [tasks, setTasks] = useState([])
|
|
840
|
-
const [teamReady, setTeamReady] = useState(false)
|
|
841
|
-
const [botId, setBotId] = useState('')
|
|
842
|
-
const [conversationKind, setConversationKind] = useState('dm')
|
|
843
|
-
const [groupId, setGroupId] = useState('')
|
|
844
|
-
const [bindingCode, setBindingCode] = useState('')
|
|
845
|
-
const [groupReady, setGroupReady] = useState(false)
|
|
846
|
-
const [groupError, setGroupError] = useState('')
|
|
847
|
-
const [origin, setOrigin] = useState('')
|
|
848
|
-
const [repository, setRepository] = useState('')
|
|
849
|
-
const [prNumber, setPrNumber] = useState('')
|
|
850
|
-
const [senderScope, setSenderScope] = useState('any')
|
|
851
|
-
const [senderIds, setSenderIds] = useState('')
|
|
852
|
-
const [days, setDays] = useState(7)
|
|
853
|
-
const [token, setToken] = useState('')
|
|
854
|
-
const [tokenReady, setTokenReady] = useState(false)
|
|
855
|
-
const [busy, setBusy] = useState(false)
|
|
856
|
-
const [message, setMessage] = useState('')
|
|
857
|
-
const refresh = async () => {
|
|
858
|
-
const [botResponse, grantResponse, credential, statusResponse, enrolledResponse, groupResponse] = await Promise.all([
|
|
859
|
-
fetch('/api/ant-teams/delegation/bots', { cache: 'no-store' }),
|
|
860
|
-
fetch('/api/ant-teams/delegation/grants', { cache: 'no-store' }),
|
|
861
|
-
remote?.credentials?.describe?.(['ANT_GITEA_API_TOKEN']),
|
|
862
|
-
fetch('/api/ant-teams/delegation/status', { cache: 'no-store' }),
|
|
863
|
-
fetch('/api/ant-teams/delegation/enrolled-bots', { cache: 'no-store' }),
|
|
864
|
-
fetch('/api/ant-teams/delegation/groups', { cache: 'no-store' }),
|
|
865
|
-
])
|
|
866
|
-
const botData = await readLocalJsonResponse(botResponse)
|
|
867
|
-
const grantData = await readLocalJsonResponse(grantResponse)
|
|
868
|
-
const statusData = await readLocalJsonResponse(statusResponse)
|
|
869
|
-
const enrolledData = await readLocalJsonResponse(enrolledResponse)
|
|
870
|
-
const groupData = await readLocalJsonResponse(groupResponse)
|
|
871
|
-
if (!grantResponse.ok) { setTeamReady(false); throw new Error(grantData.message || '请先连接 ANT 团队。') }
|
|
872
|
-
setTeamReady(true)
|
|
873
|
-
setBots(botResponse.ok && Array.isArray(botData.bots) ? botData.bots : [])
|
|
874
|
-
setGroupReady(enrolledResponse.ok && groupResponse.ok)
|
|
875
|
-
setGroupError(enrolledResponse.ok && groupResponse.ok ? ''
|
|
876
|
-
: enrolledResponse.status === 426 || groupResponse.status === 426
|
|
877
|
-
? '团队服务端版本不支持旧版 PR 群聊设置,请联系管理员升级。'
|
|
878
|
-
: enrolledData?.message || groupData?.message || '暂时无法读取群聊设置,请稍后刷新。')
|
|
879
|
-
setEnrolledBots(enrolledResponse.ok && Array.isArray(enrolledData) ? enrolledData : [])
|
|
880
|
-
setGroups(groupResponse.ok && Array.isArray(groupData) ? groupData : [])
|
|
881
|
-
setGrants(Array.isArray(grantData) ? grantData : [])
|
|
882
|
-
setTasks(statusResponse.ok && Array.isArray(statusData.items) ? statusData.items : [])
|
|
883
|
-
setTokenReady(Boolean(credential?.ok && credential.value?.ANT_GITEA_API_TOKEN?.configured))
|
|
884
|
-
if (!botResponse.ok) setMessage(botData.message || '请先配置 DSH-IM 飞书机器人。')
|
|
885
|
-
else if (!statusResponse.ok) setMessage(statusData.message || '本机任务记录暂时无法读取。')
|
|
886
|
-
}
|
|
887
|
-
useEffect(() => { void refresh().catch((error) => { setTeamReady(false); setMessage(error.message) }) }, [remote, revision])
|
|
888
|
-
const saveToken = async (event) => {
|
|
889
|
-
event.preventDefault()
|
|
890
|
-
if (!token.trim()) return
|
|
891
|
-
setBusy(true)
|
|
892
|
-
try {
|
|
893
|
-
const result = await remote.credentials.set('ANT_GITEA_API_TOKEN', token.trim())
|
|
894
|
-
if (!result?.ok) throw new Error('DSH 无法安全保存 Gitea 访问令牌。')
|
|
895
|
-
setToken('')
|
|
896
|
-
setTokenReady(true)
|
|
897
|
-
setMessage('Gitea 访问令牌已保存在这台 DSH。')
|
|
898
|
-
} catch (error) { setMessage(error.message) }
|
|
899
|
-
finally { setBusy(false) }
|
|
900
|
-
}
|
|
901
|
-
const create = async (event) => {
|
|
902
|
-
event.preventDefault()
|
|
903
|
-
const bot = bots.find((item) => item.bot_id === botId && item.available && item.connected)
|
|
904
|
-
if (!teamReady || !bot || !tokenReady) { setMessage('请先连接 ANT 团队,选择可用的专用飞书机器人,并保存 Gitea 访问令牌。'); return }
|
|
905
|
-
if (!groupReady || !selectedEnrolled) {
|
|
906
|
-
setMessage('请先为这个专用机器人启用协作收件,并固定可信 Gitea 地址。')
|
|
907
|
-
return
|
|
908
|
-
}
|
|
909
|
-
if (conversationKind === 'group' && !groups.some((item) => item.bot_fingerprint === bot.fingerprint && item.group_id === groupId)) {
|
|
910
|
-
setMessage('请先让群成员在目标群 @ 机器人一次,然后选择待启用群。')
|
|
911
|
-
return
|
|
912
|
-
}
|
|
913
|
-
if (normalizeServiceUrl(origin) !== selectedEnrollment.gitea_origin) {
|
|
914
|
-
setMessage('当前地址与机器人已登记的可信 Gitea 地址不同。请先点击“更新可信地址”,或恢复原地址。')
|
|
915
|
-
return
|
|
916
|
-
}
|
|
917
|
-
const trustedOrigin = selectedEnrollment.gitea_origin
|
|
918
|
-
if (!window.confirm(conversationKind === 'group'
|
|
919
|
-
? `确认允许所选群的成员请求 ${trustedOrigin} 上 ${repository} PR #${prNumber} 的差异审查?执行方按上方本机设置运行,结果将公开回复原群/原话题,直到你撤销或授权到期。目前没有每日请求次数上限。`
|
|
920
|
-
: `开启后,此飞书机器人由 ANT 接收消息并继续处理普通聊天;只有你授权的 PR 才能被请求审查。Gitea 令牌会发送到 ${origin},请核对地址。目前没有每日请求次数上限。请确认这是专用协作机器人。`)) return
|
|
921
|
-
setBusy(true)
|
|
922
|
-
try {
|
|
923
|
-
const response = await fetch('/api/ant-teams/delegation/grants', {
|
|
924
|
-
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
925
|
-
body: JSON.stringify({ bot_id: bot.bot_id, bot_fingerprint: bot.fingerprint,
|
|
926
|
-
gitea_origin: trustedOrigin, repository, pr_number: Number(prNumber), sender_scope: senderScope,
|
|
927
|
-
sender_ids: senderScope === 'allowlist' ? senderIds.split(/[\s,,]+/u).filter(Boolean) : [],
|
|
928
|
-
conversation_kind: conversationKind,
|
|
929
|
-
...(conversationKind === 'group' ? { conversation_id: groupId } : {}),
|
|
930
|
-
expires_at: Math.floor(Date.now() / 1000) + Number(days) * 86400 }),
|
|
931
|
-
})
|
|
932
|
-
const result = await readLocalJsonResponse(response)
|
|
933
|
-
if (!response.ok) throw new Error(result.message || '委托授权未能创建。')
|
|
934
|
-
setPrNumber('')
|
|
935
|
-
setMessage(conversationKind === 'group'
|
|
936
|
-
? '本群已启用。在群里 @机器人发送“审查 PR 编号”可试一次;结果会公开回复原群/原话题。'
|
|
937
|
-
: '委托已保存;机器人连接后可私聊发送“审查 PR 编号”试一次,以核对 Gitea 令牌和实际回复。')
|
|
938
|
-
await refresh()
|
|
939
|
-
onChanged?.()
|
|
940
|
-
} catch (error) { setMessage(error.message) }
|
|
941
|
-
finally { setBusy(false) }
|
|
942
|
-
}
|
|
943
|
-
const enrollBot = async () => {
|
|
944
|
-
const bot = bots.find((item) => item.bot_id === botId && item.available && item.connected)
|
|
945
|
-
if (!bot || !groupReady) { setMessage('请先选择可用机器人,并确认团队服务端已支持群聊委托。'); return }
|
|
946
|
-
const trustedOrigin = normalizeServiceUrl(origin)
|
|
947
|
-
if (!trustedOrigin) { setMessage('请先填写有效的 Gitea 服务地址(HTTPS,或本机测试地址)。'); return }
|
|
948
|
-
if (!window.confirm('启用后,ANT 会接收这个机器人的消息并继续处理普通聊天;DSH-IM 原有会话桥不再单独回复。只有你开放的任务才可执行。更换机器人账号或可信 Gitea 地址时,不匹配的旧授权会撤销。')) return
|
|
949
|
-
setBusy(true)
|
|
950
|
-
try {
|
|
951
|
-
const response = await fetch('/api/ant-teams/delegation/enrolled-bots', {
|
|
952
|
-
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
953
|
-
body: JSON.stringify({ bot_id: bot.bot_id, bot_fingerprint: bot.fingerprint,
|
|
954
|
-
gitea_origin: trustedOrigin }),
|
|
955
|
-
})
|
|
956
|
-
if (!response.ok) {
|
|
957
|
-
const result = await readLocalJsonResponse(response)
|
|
958
|
-
throw new Error(result.message || '无法启用机器人。')
|
|
959
|
-
}
|
|
960
|
-
setOrigin(trustedOrigin)
|
|
961
|
-
setMessage('协作机器人已启用,私聊可以正常对话。要开放任务,请先创建授权;要在群里使用,请让群成员先 @它一次。')
|
|
962
|
-
await refresh()
|
|
963
|
-
onChanged?.()
|
|
964
|
-
} catch (error) { setMessage(error.message) }
|
|
965
|
-
finally { setBusy(false) }
|
|
966
|
-
}
|
|
967
|
-
const stopBot = async () => {
|
|
968
|
-
const bot = enrolledBots.find((item) => item.bot_id === botId)
|
|
969
|
-
if (!bot || !window.confirm('停止 ANT 接收这个机器人的协作消息,并撤销全部委托授权?ANT 处理的普通聊天也会停止;如需恢复 DSH-IM 原有聊天,还要在 DSH-IM 中切回普通模式。')) return
|
|
970
|
-
setBusy(true)
|
|
971
|
-
try {
|
|
972
|
-
const response = await fetch('/api/ant-teams/delegation/enrolled-bots', {
|
|
973
|
-
method: 'DELETE', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ bot_id: bot.bot_id }),
|
|
974
|
-
})
|
|
975
|
-
if (!response.ok) throw new Error('停止机器人失败,请重试。')
|
|
976
|
-
setMessage('ANT 已停止接收这个机器人的消息并撤销任务授权。若要恢复 DSH-IM 原有聊天,请到 DSH-IM 切回普通模式。')
|
|
977
|
-
setBindingCode('')
|
|
978
|
-
await refresh()
|
|
979
|
-
onChanged?.()
|
|
980
|
-
} catch (error) { setMessage(error.message) }
|
|
981
|
-
finally { setBusy(false) }
|
|
982
|
-
}
|
|
983
|
-
const issueBindingCode = async () => {
|
|
984
|
-
const bot = bots.find((item) => item.bot_id === botId && item.available && item.connected)
|
|
985
|
-
if (!bot || !enrolledBots.some((item) => item.bot_fingerprint === bot.fingerprint)) return
|
|
986
|
-
setBusy(true)
|
|
987
|
-
try {
|
|
988
|
-
const response = await fetch('/api/ant-teams/delegation/owner-challenge', {
|
|
989
|
-
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
990
|
-
body: JSON.stringify({ bot_id: bot.bot_id, bot_fingerprint: bot.fingerprint }),
|
|
991
|
-
})
|
|
992
|
-
const result = await readLocalJsonResponse(response)
|
|
993
|
-
if (!response.ok) throw new Error(result.message || '无法生成绑定码。')
|
|
994
|
-
setBindingCode(result.code)
|
|
995
|
-
setMessage('绑定码 5 分钟有效。私聊发送后,点击下方“刷新状态”查看绑定结果。')
|
|
996
|
-
} catch (error) { setMessage(error.message) }
|
|
997
|
-
finally { setBusy(false) }
|
|
998
|
-
}
|
|
999
|
-
const revoke = async (grant) => {
|
|
1000
|
-
if (!window.confirm(`停止授权 ${grant.repository} 的 PR #${grant.pr_number}?`)) return
|
|
1001
|
-
setBusy(true)
|
|
1002
|
-
try {
|
|
1003
|
-
const response = await fetch('/api/ant-teams/delegation/grants', {
|
|
1004
|
-
method: 'DELETE', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: grant.id }),
|
|
1005
|
-
})
|
|
1006
|
-
if (!response.ok) throw new Error('停止授权失败,请重试。')
|
|
1007
|
-
setMessage('已停止这项 PR 授权;机器人其他聊天和任务不受影响。')
|
|
1008
|
-
await refresh()
|
|
1009
|
-
onChanged?.()
|
|
1010
|
-
} catch (error) { setMessage(error.message) }
|
|
1011
|
-
finally { setBusy(false) }
|
|
1012
|
-
}
|
|
1013
|
-
const active = grants.filter((grant) => !grant.revoked_at && grant.expires_at * 1000 > Date.now())
|
|
1014
|
-
const selectedBot = bots.find((bot) => bot.bot_id === botId)
|
|
1015
|
-
const selectedEnrollment = enrolledBots.find((item) => item.bot_fingerprint === selectedBot?.fingerprint)
|
|
1016
|
-
const selectedEnrolled = Boolean(selectedEnrollment)
|
|
1017
|
-
const botGroups = groups.filter((item) => item.bot_fingerprint === selectedBot?.fingerprint)
|
|
1018
|
-
return h('details', { id: 'ant-pr-review-settings', open,
|
|
1019
|
-
onToggle: (event) => onOpenChange?.(event.currentTarget.open),
|
|
1020
|
-
style: panelStyle, 'aria-label': '指定 PR 差异审查授权' },
|
|
1021
|
-
h('summary', { style: { cursor: 'pointer', fontWeight: 600 } }, '指定 PR 差异审查授权'),
|
|
1022
|
-
h('p', { style: mutedTextStyle }, '为指定仓库和 PR 编号开放审查。已有授权继续有效;文字任务请在上方创建。'),
|
|
1023
|
-
h('p', { style: mutedTextStyle }, '这里管理谁能请求指定 PR;执行方在上方“本机与审查设置”选择。PR 标题和差异会交给所选执行方;ANT 不会主动传入 Gitea 令牌,但本机执行器仍沿用它已有的文件权限。'),
|
|
1024
|
-
h('form', { onSubmit: saveToken, style: { display: 'flex', flexWrap: 'wrap', gap: 8 } },
|
|
1025
|
-
h('label', { style: { width: '100%', fontSize: 13 } }, `Gitea 访问令牌(仅保存在本机 DSH)${tokenReady ? ' · 已配置' : ''}`),
|
|
1026
|
-
h('input', { type: 'password', autoComplete: 'off', value: token, onChange: (event) => setToken(event.target.value),
|
|
1027
|
-
placeholder: '只读访问令牌', style: { ...inputStyle, flex: '1 1 260px' } }),
|
|
1028
|
-
h('button', { type: 'submit', disabled: busy || !token.trim(), style: secondaryButtonStyle }, '保存令牌')),
|
|
1029
|
-
h('form', { onSubmit: create, style: { display: 'grid', gap: 9, maxWidth: 640 } },
|
|
1030
|
-
h('label', { htmlFor: 'ant-delegation-bot' }, '专用飞书机器人'),
|
|
1031
|
-
h('select', { id: 'ant-delegation-bot', required: true, value: botId,
|
|
1032
|
-
onChange: (event) => { setBotId(event.target.value); setGroupId(''); setBindingCode('');
|
|
1033
|
-
setOrigin(enrolledBots.find((item) => item.bot_id === event.target.value)?.gitea_origin || '') }, style: inputStyle },
|
|
1034
|
-
h('option', { value: '' }, '选择已连接的机器人'),
|
|
1035
|
-
...bots.filter((bot) => bot.available && bot.connected).map((bot) => h('option', { key: bot.bot_id, value: bot.bot_id }, bot.name))),
|
|
1036
|
-
h('label', { htmlFor: 'ant-delegation-origin' }, '可信 Gitea 服务地址'),
|
|
1037
|
-
h('input', { id: 'ant-delegation-origin', type: 'url', required: true, value: origin,
|
|
1038
|
-
onChange: (event) => setOrigin(event.target.value), placeholder: 'https://git.example.com', style: inputStyle }),
|
|
1039
|
-
groupReady ? h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' } },
|
|
1040
|
-
h('span', { style: mutedTextStyle }, selectedEnrolled
|
|
1041
|
-
? `${selectedEnrollment.owner_bound ? '飞书身份已绑定,可在群内批准' : '协作收件已启用,群内批准需先绑定身份'} · 可信 Gitea:${selectedEnrollment.gitea_origin}`
|
|
1042
|
-
: '协作机器人尚未启用'),
|
|
1043
|
-
!selectedEnrolled ? h('button', { type: 'button', disabled: busy || !botId,
|
|
1044
|
-
onClick: () => void enrollBot(), style: secondaryButtonStyle }, '启用协作机器人') : null,
|
|
1045
|
-
selectedEnrolled ? h('button', { type: 'button', disabled: busy,
|
|
1046
|
-
onClick: () => void enrollBot(), style: secondaryButtonStyle }, '更新可信地址') : null,
|
|
1047
|
-
selectedEnrolled ? h('button', { type: 'button', disabled: busy,
|
|
1048
|
-
onClick: () => void issueBindingCode(), style: secondaryButtonStyle },
|
|
1049
|
-
selectedEnrollment.owner_bound ? '重新绑定飞书身份' : '绑定我的飞书身份') : null,
|
|
1050
|
-
selectedEnrolled ? h('button', { type: 'button', disabled: busy,
|
|
1051
|
-
onClick: () => void stopBot(), style: secondaryButtonStyle }, '停止接收并撤销授权') : null) :
|
|
1052
|
-
h('p', { style: mutedTextStyle }, groupError),
|
|
1053
|
-
bindingCode && selectedEnrolled ? h('p', { role: 'status', style: mutedTextStyle },
|
|
1054
|
-
`请你本人私聊这个机器人发送“绑定身份 ${bindingCode}”;5 分钟内有效,不要把码发在群里。绑定后可在群里 @它发送“在本群启用 <完整 PR 链接> 审查”,再按提示确认。`) : null,
|
|
1055
|
-
h('label', { htmlFor: 'ant-delegation-kind' }, '授权给谁使用'),
|
|
1056
|
-
h('select', { id: 'ant-delegation-kind', value: conversationKind,
|
|
1057
|
-
onChange: (event) => { setConversationKind(event.target.value); setGroupId('') }, style: inputStyle },
|
|
1058
|
-
h('option', { value: 'dm' }, '私聊这个机器人'),
|
|
1059
|
-
groupReady ? h('option', { value: 'group' }, '仅所选飞书群') : null),
|
|
1060
|
-
conversationKind === 'group' ? h('p', { style: mutedTextStyle },
|
|
1061
|
-
'让群成员先在目标群 @机器人一次,然后刷新列表。向群成员核对机器人回复的识别码;启用后,该群成员都能看到审查结果。') : null,
|
|
1062
|
-
conversationKind === 'group' ? h('div', { style: { display: 'flex', gap: 8 } },
|
|
1063
|
-
h('select', { 'aria-label': '待启用群', required: true, value: groupId,
|
|
1064
|
-
onChange: (event) => setGroupId(event.target.value), style: { ...inputStyle, flex: 1 } },
|
|
1065
|
-
h('option', { value: '' }, '选择待启用群'),
|
|
1066
|
-
...botGroups.map((group) => h('option', { key: group.group_id, value: group.group_id },
|
|
1067
|
-
`识别码 ${group.group_code} · ${new Date(group.last_seen_at * 1000).toLocaleString('zh-CN')} · ${group.group_id}${group.active_grants ? ' · 已有授权' : ' · 待启用'}`))),
|
|
1068
|
-
h('button', { type: 'button', disabled: busy, onClick: () => void refresh().catch((error) => setMessage(error.message)),
|
|
1069
|
-
style: secondaryButtonStyle }, '刷新群列表')) : null,
|
|
1070
|
-
h('label', { htmlFor: 'ant-delegation-repository' }, '仓库'),
|
|
1071
|
-
h('input', { id: 'ant-delegation-repository', required: true, value: repository,
|
|
1072
|
-
onChange: (event) => setRepository(event.target.value), placeholder: '团队名/仓库名', style: inputStyle }),
|
|
1073
|
-
h('label', { htmlFor: 'ant-delegation-pr' }, '允许审查的 PR 编号'),
|
|
1074
|
-
h('input', { id: 'ant-delegation-pr', type: 'number', min: 1, required: true, value: prNumber,
|
|
1075
|
-
onChange: (event) => setPrNumber(event.target.value), style: inputStyle }),
|
|
1076
|
-
h('label', { htmlFor: 'ant-delegation-audience' }, '谁可以请求'),
|
|
1077
|
-
h('select', { id: 'ant-delegation-audience', value: senderScope,
|
|
1078
|
-
onChange: (event) => setSenderScope(event.target.value), style: inputStyle },
|
|
1079
|
-
h('option', { value: 'any' }, conversationKind === 'group' ? '所选群的所有成员' : '任何能私聊这个专用机器人的人'),
|
|
1080
|
-
h('option', { value: 'allowlist' }, '仅指定飞书用户 ID')),
|
|
1081
|
-
senderScope === 'allowlist' ? h('input', { 'aria-label': '飞书用户 ID', required: true, value: senderIds,
|
|
1082
|
-
onChange: (event) => setSenderIds(event.target.value), placeholder: '多个 ID 用逗号分隔', style: inputStyle }) : null,
|
|
1083
|
-
h('label', { htmlFor: 'ant-delegation-days' }, '有效期'),
|
|
1084
|
-
h('select', { id: 'ant-delegation-days', value: days, onChange: (event) => setDays(Number(event.target.value)), style: inputStyle },
|
|
1085
|
-
h('option', { value: 1 }, '1 天'), h('option', { value: 7 }, '7 天'), h('option', { value: 30 }, '30 天')),
|
|
1086
|
-
conversationKind === 'group' ? h('p', { style: mutedTextStyle },
|
|
1087
|
-
'请确认该 PR 的标题、差异和审查结果可以向整个群公开;“指定发送者”只限制谁能发起,不会把结果改成私聊。目前没有每日请求次数上限。')
|
|
1088
|
-
: senderScope === 'any' ? h('p', { style: mutedTextStyle },
|
|
1089
|
-
'PR 审查结果会回复给发信人;请确认该 PR 可以向这些人披露。') : null,
|
|
1090
|
-
h('button', { type: 'submit', disabled: busy || !teamReady || !tokenReady || !selectedEnrolled ||
|
|
1091
|
-
(conversationKind === 'group' && !groupId), style: primaryButtonStyle }, '开启这项委托')),
|
|
1092
|
-
h('h4', { style: { marginBottom: 0 } }, `当前有效授权 · ${active.length}`),
|
|
1093
|
-
...active.map((grant) => h('div', { key: grant.id, style: { display: 'flex', justifyContent: 'space-between', gap: 12,
|
|
1094
|
-
alignItems: 'center', borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: 10 } },
|
|
1095
|
-
h('span', null, `${grant.repository} · PR #${grant.pr_number} · ${grant.conversation_kind === 'group'
|
|
1096
|
-
? `群 ${grant.conversation_id}(${grant.sender_scope === 'any' ? '全群' : '指定发送者'})`
|
|
1097
|
-
: (grant.sender_scope === 'any' ? '任何私聊者' : '指定发送者')} · ${new Date(grant.expires_at * 1000).toLocaleString('zh-CN')} 到期`),
|
|
1098
|
-
h('button', { type: 'button', disabled: busy, onClick: () => void revoke(grant), style: secondaryButtonStyle }, '停止授权'))),
|
|
1099
|
-
h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8 } },
|
|
1100
|
-
h('h4', { style: { margin: 0 } }, '最近任务'),
|
|
1101
|
-
h('button', { type: 'button', disabled: busy, onClick: () => void refresh().catch((error) => { setTeamReady(false); setMessage(error.message) }),
|
|
1102
|
-
style: secondaryButtonStyle }, '刷新状态')),
|
|
1103
|
-
...(tasks.length ? tasks.map((task, index) => h('p', { key: `${task.created_at}-${index}`, style: mutedTextStyle },
|
|
1104
|
-
`${task.created_at ? new Date(task.created_at).toLocaleString('zh-CN') + ' · ' : ''}${task.repository || '请求'}${task.pr_number ? ` · PR #${task.pr_number}` : ''} · ${task.outcome === 'canceled' ? '已取消' : task.outcome === 'failed' ? '未完成' : ({
|
|
1105
|
-
pending: '排队中', running: '处理中', reply_pending: '待回复', reply_attempted: '回复状态待核对,请查看飞书原会话',
|
|
1106
|
-
done: '已回复',
|
|
1107
|
-
}[task.status] || task.status)}`)) : [h('p', { style: mutedTextStyle }, '暂无任务记录。')]),
|
|
1108
|
-
message ? h('p', { role: 'status', style: mutedTextStyle }, message) : null)
|
|
1044
|
+
h('label', null, '这台电脑 PR 仓库缓存上限(GiB)',
|
|
1045
|
+
h('input', { type: 'number', min: 1, max: 100, step: 1, required: true,
|
|
1046
|
+
value: repositoryCacheLimitGib,
|
|
1047
|
+
onChange: (event) => setRepositoryCacheLimitGib(event.target.value), style: inputStyle }),
|
|
1048
|
+
h('span', { style: mutedTextStyle }, '默认 10 GiB,范围 1–100 GiB,按长期仓库文件逻辑大小计算(不含临时工作区和文件系统分配开销)。日常不会淘汰仍启用能力的缓存;停用或到期能力会在刷新时回收。单次拉取若实际越界,为恢复预算会清理这项能力的仓库缓存(旧 PR 可重新下载)。')),
|
|
1049
|
+
h('button', { disabled: busy, style: secondaryButtonStyle }, '保存本机处理设置')),
|
|
1050
|
+
h('p', { style: mutedTextStyle }, `本机检测到的执行方:${executors.map((item) => `${item.title}${item.available ? '(可用)' : `(${item.reason || '未启用或未找到'})`}`).join('、') || '暂未读取到'}。只有能力配置下拉框中列出的执行方能接收委托。`))))
|
|
1109
1051
|
}
|
|
1110
1052
|
|
|
1111
1053
|
function SkillCenterPage({ remote, onBack, onAll, onSkills, onPlugins, openBundle,
|
|
@@ -1598,10 +1540,12 @@ window.__ModuleLoader__.load({
|
|
|
1598
1540
|
? h('p', { style: mutedTextStyle }, noMatches) : null))
|
|
1599
1541
|
}
|
|
1600
1542
|
|
|
1601
|
-
function ModelSyncFooter({ remote, serviceUrl, bindingReady, onServiceUrlChange,
|
|
1543
|
+
function ModelSyncFooter({ remote, serviceUrl, bindingReady, activeTab, onServiceUrlChange,
|
|
1544
|
+
onConnectionChange, onUpdateStateChange, onSelectTab }) {
|
|
1602
1545
|
const [email, setEmail] = useState('')
|
|
1603
1546
|
const [code, setCode] = useState('')
|
|
1604
1547
|
const [message, setMessage] = useState('')
|
|
1548
|
+
const [modelMessage, setModelMessage] = useState('')
|
|
1605
1549
|
const [busy, setBusy] = useState(false)
|
|
1606
1550
|
const [codeRequested, setCodeRequested] = useState(false)
|
|
1607
1551
|
const [credentialConfigured, setCredentialConfigured] = useState(null)
|
|
@@ -1610,9 +1554,15 @@ window.__ModuleLoader__.load({
|
|
|
1610
1554
|
const [allowLocalCleanup, setAllowLocalCleanup] = useState(false)
|
|
1611
1555
|
const [bundleRelease, setBundleRelease] = useState(null)
|
|
1612
1556
|
const [bundleUpdate, setBundleUpdate] = useState({ state: 'idle', currentVersion: '', message: '' })
|
|
1557
|
+
const [authorizedModels, setAuthorizedModels] = useState([])
|
|
1558
|
+
const [lastModelSync, setLastModelSync] = useState(null)
|
|
1613
1559
|
const [serviceCompatibility, setServiceCompatibility] = useState({ state: 'idle', message: '' })
|
|
1614
1560
|
const [pinnedServiceUrl, setPinnedServiceUrl] = useState(null)
|
|
1615
1561
|
|
|
1562
|
+
useEffect(() => {
|
|
1563
|
+
onUpdateStateChange?.({ ...bundleUpdate, version: bundleRelease?.version || '' })
|
|
1564
|
+
}, [bundleUpdate, bundleRelease, onUpdateStateChange])
|
|
1565
|
+
|
|
1616
1566
|
const connectionErrorMessage = (result, status) => {
|
|
1617
1567
|
const message = result?.message || `请求失败(${status})`
|
|
1618
1568
|
if (!['LEGACY_DEVICE_REVOKE_REQUIRED', 'ANT_CONNECTION_CLEANUP_REQUIRED'].includes(result?.code)) return message
|
|
@@ -1717,7 +1667,7 @@ window.__ModuleLoader__.load({
|
|
|
1717
1667
|
return
|
|
1718
1668
|
}
|
|
1719
1669
|
setBusy(true)
|
|
1720
|
-
|
|
1670
|
+
setModelMessage('正在从 ANT 刷新获准模型…')
|
|
1721
1671
|
try {
|
|
1722
1672
|
await requireCompatibleService(serviceUrl, true)
|
|
1723
1673
|
const response = await fetch('/api/ant-teams/models', { method: 'POST' })
|
|
@@ -1727,12 +1677,14 @@ window.__ModuleLoader__.load({
|
|
|
1727
1677
|
throw new Error(result.message || `刷新失败(${response.status})`)
|
|
1728
1678
|
}
|
|
1729
1679
|
const count = await applyAuthorizedModels(result.models, result.service_url)
|
|
1680
|
+
setAuthorizedModels(result.models)
|
|
1681
|
+
setLastModelSync(new Date())
|
|
1730
1682
|
setCredentialConfigured(true)
|
|
1731
1683
|
setNeedsReconnect(false)
|
|
1732
1684
|
setReauthenticate(false)
|
|
1733
|
-
|
|
1685
|
+
setModelMessage(`模型目录已刷新,共 ${count} 个获准模型。`)
|
|
1734
1686
|
} catch (error) {
|
|
1735
|
-
|
|
1687
|
+
setModelMessage(error instanceof Error ? error.message : '刷新模型失败,请稍后重试。')
|
|
1736
1688
|
} finally {
|
|
1737
1689
|
setBusy(false)
|
|
1738
1690
|
}
|
|
@@ -2073,6 +2025,8 @@ window.__ModuleLoader__.load({
|
|
|
2073
2025
|
if (!credentialWrite.ok) throw new Error(credentialWrite.error?.message || 'DSH 无法安全保存 ANT 凭证。')
|
|
2074
2026
|
credentialSaved = true
|
|
2075
2027
|
await applyAuthorizedModels(result.models, normalized)
|
|
2028
|
+
setAuthorizedModels(result.models)
|
|
2029
|
+
setLastModelSync(new Date())
|
|
2076
2030
|
setCode('')
|
|
2077
2031
|
onServiceUrlChange(normalized)
|
|
2078
2032
|
setCodeRequested(false)
|
|
@@ -2101,86 +2055,117 @@ window.__ModuleLoader__.load({
|
|
|
2101
2055
|
const canCheckUpdate = bindingReady && credentialConfigured === true && !needsReconnect
|
|
2102
2056
|
const updateGuideUrl = pinnedServiceUrl
|
|
2103
2057
|
? new URL('/ant-update', pinnedServiceUrl).href : null
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
? '
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
style:
|
|
2124
|
-
|
|
2125
|
-
color: serviceCompatibility.state === 'incompatible' ? 'var(--dsw-alias-state-error-primary)' : mutedTextStyle.color,
|
|
2126
|
-
},
|
|
2127
|
-
}, serviceCompatibility.message) : null,
|
|
2128
|
-
credentialConfigured === true && !showConnectForm ? h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8 } },
|
|
2129
|
-
h('button', { type: 'button', disabled: busy || !bindingReady, onClick: () => void refreshModels(), style: primaryButtonStyle }, busy ? '刷新中…' : '刷新团队模型'),
|
|
2130
|
-
h('button', { type: 'button', disabled: busy || !bindingReady, onClick: () => { setReauthenticate(true); setMessage('') }, style: secondaryButtonStyle }, '重新验证邮箱'),
|
|
2131
|
-
h('button', { type: 'button', disabled: busy, onClick: () => { if (window.confirm('断开当前设备与 ANT 的连接?本机 ANT 凭证和模型设置将被清除,团队服务地址会保留。')) void disconnect() }, style: secondaryButtonStyle }, '断开当前设备')) : null,
|
|
2132
|
-
h('section', { style: { display: 'grid', gap: 8, paddingTop: 12, borderTop: '0.5px solid var(--dsw-alias-border-l2)' } },
|
|
2133
|
-
h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12 } },
|
|
2134
|
-
h('h4', { style: { margin: 0, fontSize: 14, fontWeight: 650 } }, 'ANT 插件更新'),
|
|
2135
|
-
h('button', { type: 'button', disabled: busy || !canCheckUpdate || bundleUpdate.state === 'checking', onClick: () => void checkBundleUpdate(), style: secondaryButtonStyle }, bundleUpdate.state === 'checking' ? '检查中…' : '检查更新')),
|
|
2136
|
-
!canCheckUpdate ? h('p', { role: 'status', style: mutedTextStyle }, '连接团队后可在这里检查并安装 ANT 更新;需要先恢复插件时,可打开团队管理台的更新指引。') : null,
|
|
2137
|
-
updateGuideUrl ? h('a', { href: updateGuideUrl, target: '_blank', rel: 'noopener noreferrer', style: { color: 'var(--dsw-alias-link)', fontSize: 13 } }, `打开 ${pinnedServiceUrl} 的更新指引 ↗`)
|
|
2138
|
-
: !canCheckUpdate ? h('p', { style: mutedTextStyle }, '无法核对原团队地址时,请向企业管理员索取更新指引。') : null,
|
|
2139
|
-
bundleUpdate.message ? h('p', { role: 'status', style: bundleUpdate.state === 'revoked'
|
|
2140
|
-
? { ...mutedTextStyle, color: 'var(--dsw-alias-state-error-primary)' } : mutedTextStyle }, bundleUpdate.message) : null,
|
|
2141
|
-
canCheckUpdate && bundleRelease?.version && ['available', 'artifact-missing'].includes(bundleUpdate.state) ? h('div', { style: { display: 'grid', gap: 8 } },
|
|
2142
|
-
h('p', { style: mutedTextStyle }, `发布版本:${bundleRelease.version} · 精确包名:${directInstallSpec(bundleRelease)}`),
|
|
2143
|
-
bundleRelease.release_notes ? h('p', { style: mutedTextStyle }, bundleRelease.release_notes) : null,
|
|
2144
|
-
bundleUpdate.state === 'available'
|
|
2145
|
-
? h('button', { type: 'button', disabled: busy, onClick: () => void installBundleUpdate(), style: primaryButtonStyle }, busy ? '更新中…' : '更新 ANT') : null) : null),
|
|
2146
|
-
showConnectForm ? h('form', { onSubmit: requestCode, style: { display: 'grid', gap: 9, maxWidth: 560 } },
|
|
2147
|
-
h('label', { htmlFor: 'ant-team-model-email', style: { fontSize: 13, fontWeight: 550 } }, '工作邮箱'),
|
|
2148
|
-
h('input', {
|
|
2149
|
-
id: 'ant-team-model-email', type: 'email', autoComplete: 'email', required: true, value: email,
|
|
2150
|
-
onChange: (event) => setEmail(event.target.value), style: inputStyle,
|
|
2151
|
-
}),
|
|
2152
|
-
h('button', { type: 'submit', disabled: busy || !bindingReady, style: primaryButtonStyle }, busy ? '发送中…' : '发送验证码')) : null,
|
|
2153
|
-
credentialConfigured === true && showConnectForm ? h('button', {
|
|
2154
|
-
type: 'button', disabled: busy,
|
|
2058
|
+
const modelRows = authorizedModels.map((model) => h('div', { key: `${model.protocol}:${model.model_id}`, role: 'listitem',
|
|
2059
|
+
style: { display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', gap: 12, alignItems: 'center',
|
|
2060
|
+
padding: '10px 2px', borderBottom: '1px solid var(--dsw-alias-border-l2)' } },
|
|
2061
|
+
h('strong', null, model.display_name || model.model_id),
|
|
2062
|
+
h('span', { style: mutedTextStyle }, model.protocol === 'anthropic' ? 'Anthropic' : 'OpenAI'),
|
|
2063
|
+
h('span', { style: mutedTextStyle }, '已同步到 DSH')))
|
|
2064
|
+
const connectionPanel = h('section', { style: { ...panelStyle, display: activeTab === 'connection' ? 'grid' : 'none' } },
|
|
2065
|
+
h('div', null,
|
|
2066
|
+
h('h3', { style: { margin: 0, fontSize: 16, fontWeight: 650 } }, 'ANT 团队账号'),
|
|
2067
|
+
h('p', { style: { ...mutedTextStyle, marginTop: 4 } }, '邮箱验证建立这台 DSH 的成员凭证;断开后会清除此设备凭证和同步的模型设置。')),
|
|
2068
|
+
h('span', { role: 'status', style: mutedTextStyle }, credentialConfigured === true && !bindingReady
|
|
2069
|
+
? '本机有 ANT 凭证,但尚未确认它绑定的团队;请完成上方的绑定检查。'
|
|
2070
|
+
: credentialConfigured === true ? 'DSH 已安全保存此设备的 ANT 团队凭证。'
|
|
2071
|
+
: credentialConfigured === false ? '验证工作邮箱后即可连接团队服务。' : '正在检查 ANT 连接状态…'),
|
|
2072
|
+
serviceCompatibility.state !== 'idle' ? h('p', { role: 'status', style: { ...mutedTextStyle,
|
|
2073
|
+
color: serviceCompatibility.state === 'incompatible' ? 'var(--dsw-alias-state-error-primary)' : mutedTextStyle.color } },
|
|
2074
|
+
serviceCompatibility.message) : null,
|
|
2075
|
+
credentialConfigured === true && !showConnectForm ? h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8 } },
|
|
2076
|
+
h('button', { type: 'button', disabled: busy || !bindingReady,
|
|
2077
|
+
onClick: () => { setReauthenticate(true); setMessage('') }, style: secondaryButtonStyle }, '重新验证邮箱'),
|
|
2078
|
+
h('button', { type: 'button', disabled: busy,
|
|
2155
2079
|
onClick: () => { if (window.confirm('断开当前设备与 ANT 的连接?本机 ANT 凭证和模型设置将被清除,团队服务地址会保留。')) void disconnect() },
|
|
2156
|
-
style: secondaryButtonStyle,
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2080
|
+
style: secondaryButtonStyle }, '断开当前设备')) : null,
|
|
2081
|
+
showConnectForm ? h('form', { onSubmit: requestCode, style: { display: 'grid', gap: 9, maxWidth: 560 } },
|
|
2082
|
+
h('label', { htmlFor: 'ant-team-model-email', style: { fontSize: 13, fontWeight: 550 } }, '工作邮箱'),
|
|
2083
|
+
h('input', { id: 'ant-team-model-email', type: 'email', autoComplete: 'email', required: true, value: email,
|
|
2084
|
+
onChange: (event) => setEmail(event.target.value), style: inputStyle }),
|
|
2085
|
+
h('button', { type: 'submit', disabled: busy || !bindingReady, style: primaryButtonStyle }, busy ? '发送中…' : '发送验证码')) : null,
|
|
2086
|
+
showConnectForm && codeRequested ? h('form', { onSubmit: connect, style: { display: 'grid', gap: 9, maxWidth: 560 } },
|
|
2087
|
+
h('label', { htmlFor: 'ant-team-model-code', style: { fontSize: 13, fontWeight: 550 } }, '邮箱验证码'),
|
|
2088
|
+
h('input', { id: 'ant-team-model-code', type: 'text', inputMode: 'numeric', autoComplete: 'one-time-code',
|
|
2089
|
+
required: true, maxLength: 8, value: code, placeholder: '8 位验证码',
|
|
2090
|
+
onChange: (event) => setCode(event.target.value.replace(/\D/gu, '').slice(0, 8)), style: inputStyle }),
|
|
2091
|
+
h('button', { type: 'submit', disabled: busy || !bindingReady || code.length !== 8, style: primaryButtonStyle }, busy ? '连接中…' : '验证并连接')) : null,
|
|
2092
|
+
credentialConfigured === true && showConnectForm ? h('button', { type: 'button', disabled: busy,
|
|
2093
|
+
onClick: () => { if (window.confirm('断开当前设备与 ANT 的连接?本机 ANT 凭证和模型设置将被清除,团队服务地址会保留。')) void disconnect() },
|
|
2094
|
+
style: secondaryButtonStyle }, '断开当前设备') : null,
|
|
2095
|
+
allowLocalCleanup ? h('section', { style: { ...panelStyle, padding: 14, gap: 9 } },
|
|
2096
|
+
h('p', { style: mutedTextStyle }, savedDeviceId()
|
|
2097
|
+
? `已保存的设备编号:${savedDeviceId()}。请交给原团队管理员核对。`
|
|
2098
|
+
: '本机无法确认旧设备编号。请让原团队管理员按成员查看设备清单,勿用新编号猜测。'),
|
|
2099
|
+
h('p', { style: mutedTextStyle }, '仅清理本机不会确认服务端令牌已撤销;请先取得管理员确认。'),
|
|
2100
|
+
h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8 } },
|
|
2101
|
+
h('button', { type: 'button', disabled: busy, onClick: () => void clearOnlyLocal(), style: secondaryButtonStyle }, '仅清理本机'),
|
|
2102
|
+
h('button', { type: 'button', disabled: busy || !savedDeviceId(), onClick: async () => {
|
|
2103
|
+
try {
|
|
2104
|
+
const deviceId = savedDeviceId()
|
|
2105
|
+
if (!deviceId) throw new Error('device id unavailable')
|
|
2106
|
+
if (typeof window.navigator?.clipboard?.writeText !== 'function') throw new Error('clipboard unavailable')
|
|
2107
|
+
await window.navigator.clipboard.writeText(deviceId)
|
|
2108
|
+
setMessage('设备编号已复制,请交给原团队管理员。')
|
|
2109
|
+
} catch { setMessage(savedDeviceId() ? `请复制设备编号:${savedDeviceId()}` : '本机无法确认旧设备编号,请联系原团队管理员核查。') }
|
|
2110
|
+
}, style: secondaryButtonStyle }, '复制设备编号'))) : null,
|
|
2111
|
+
message ? h('p', { role: 'status', style: mutedTextStyle }, message) : null)
|
|
2112
|
+
|
|
2113
|
+
const modelPanel = h('section', { style: { ...panelStyle, display: activeTab === 'models' ? 'grid' : 'none' } },
|
|
2114
|
+
h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12 } },
|
|
2115
|
+
h('div', null,
|
|
2116
|
+
h('h3', { style: { margin: 0, fontSize: 16, fontWeight: 650 } }, '团队模型'),
|
|
2117
|
+
h('p', { style: { ...mutedTextStyle, marginTop: 4 } }, '每个获准模型单独列出;刷新后同步到 DSH 模型选择器。')),
|
|
2118
|
+
h('span', { style: { flexShrink: 0, padding: '4px 9px', borderRadius: 999,
|
|
2119
|
+
color: credentialConfigured === true && bindingReady ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-label-secondary)',
|
|
2120
|
+
backgroundColor: credentialConfigured === true && bindingReady ? 'var(--dsw-alias-state-success-tertiary)' : 'var(--dsw-alias-bg-layer-2)',
|
|
2121
|
+
fontSize: 12,
|
|
2122
|
+
} }, credentialConfigured === true ? bindingReady ? '已连接' : '待核对' : credentialConfigured === false ? '未连接' : '检查中')),
|
|
2123
|
+
h('div', { role: 'list', 'aria-label': '获准模型', style: { display: 'grid', gap: 0, borderTop: '1px solid var(--dsw-alias-border-l2)' } },
|
|
2124
|
+
modelRows.length ? modelRows : h('div', { style: { display: 'grid', gap: 8, padding: '12px 0' } },
|
|
2125
|
+
h('p', { style: mutedTextStyle }, credentialConfigured === true && modelMessage.startsWith('正在') ? modelMessage
|
|
2126
|
+
: credentialConfigured === true ? '当前没有已获准模型,请刷新或联系团队管理员。'
|
|
2127
|
+
: '连接并验证工作邮箱后,这里会列出逐个获准模型。'),
|
|
2128
|
+
credentialConfigured !== true ? h('button', { type: 'button', onClick: () => onSelectTab('connection'),
|
|
2129
|
+
style: secondaryButtonStyle }, '前往团队连接') : null)),
|
|
2130
|
+
h('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 10 } },
|
|
2131
|
+
h('button', { type: 'button', disabled: busy || !bindingReady || credentialConfigured !== true,
|
|
2132
|
+
onClick: () => void refreshModels(), style: primaryButtonStyle }, busy ? '刷新中…' : '刷新团队模型'),
|
|
2133
|
+
lastModelSync ? h('span', { style: mutedTextStyle }, `最近同步:${lastModelSync.toLocaleString('zh-CN')}`) : null),
|
|
2134
|
+
modelMessage && !modelMessage.startsWith('正在') ? h('p', { role: 'status', style: mutedTextStyle }, modelMessage) : null)
|
|
2135
|
+
|
|
2136
|
+
const updatePanel = h('section', { style: { ...panelStyle, display: activeTab === 'updates' ? 'grid' : 'none' } },
|
|
2137
|
+
h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12 } },
|
|
2138
|
+
h('div', null,
|
|
2139
|
+
h('h3', { style: { margin: 0, fontSize: 16, fontWeight: 650 } }, 'ANT 插件更新'),
|
|
2140
|
+
h('p', { style: { ...mutedTextStyle, marginTop: 4 } }, '团队连接就绪后自动检查;有新版时顶部会持续提醒。')),
|
|
2141
|
+
h('span', { style: { ...mutedTextStyle, color: bundleUpdate.state === 'revoked'
|
|
2142
|
+
? 'var(--dsw-alias-state-error-primary)' : 'var(--dsw-alias-state-warning-primary, var(--dsw-alias-label-secondary))' } },
|
|
2143
|
+
bundleUpdate.state === 'available' ? '有新版' : bundleUpdate.state === 'done' ? '需重启 DSH'
|
|
2144
|
+
: bundleUpdate.state === 'current' ? '已是最新' : bundleUpdate.state === 'revoked' ? '安装包已撤回' : '更新状态')),
|
|
2145
|
+
bundleUpdate.currentVersion ? h('p', { style: mutedTextStyle }, `当前安装版本:${bundleUpdate.currentVersion}`) : null,
|
|
2146
|
+
bundleRelease?.version ? h('p', { style: mutedTextStyle }, `最新发布版本:${bundleRelease.version} · 精确包名:${directInstallSpec(bundleRelease)}`) : null,
|
|
2147
|
+
serviceCompatibility.state !== 'idle' ? h('p', { role: 'status', style: {
|
|
2148
|
+
...mutedTextStyle,
|
|
2149
|
+
color: serviceCompatibility.state === 'incompatible' ? 'var(--dsw-alias-state-error-primary)' : mutedTextStyle.color,
|
|
2150
|
+
} }, `当前已安装版本与团队服务端协议:${serviceCompatibility.message}`) : null,
|
|
2151
|
+
bundleUpdate.message ? h('p', { role: 'status', style: bundleUpdate.state === 'revoked'
|
|
2152
|
+
? { ...mutedTextStyle, color: 'var(--dsw-alias-state-error-primary)' } : mutedTextStyle }, bundleUpdate.message) : null,
|
|
2153
|
+
!canCheckUpdate && updateGuideUrl ? h('a', { href: updateGuideUrl, target: '_blank', rel: 'noopener noreferrer',
|
|
2154
|
+
style: { color: 'var(--dsw-alias-link)', fontSize: 13 } }, `打开 ${pinnedServiceUrl} 的更新指引 ↗`) : null,
|
|
2155
|
+
!canCheckUpdate && !updateGuideUrl ? h('p', { style: mutedTextStyle }, '无法核对原团队地址时,请向企业管理员索取更新指引。') : null,
|
|
2156
|
+
!canCheckUpdate ? h('button', { type: 'button', onClick: () => onSelectTab('connection'), style: secondaryButtonStyle },
|
|
2157
|
+
'前往团队连接') : null,
|
|
2158
|
+
canCheckUpdate && bundleRelease?.version && ['available', 'artifact-missing'].includes(bundleUpdate.state)
|
|
2159
|
+
? h('div', { style: { display: 'grid', gap: 8 } },
|
|
2160
|
+
bundleRelease.release_notes ? h('div', { style: { ...panelStyle, padding: 12 } },
|
|
2161
|
+
h('strong', null, '更新说明'), h('p', { style: mutedTextStyle }, bundleRelease.release_notes)) : null,
|
|
2162
|
+
bundleUpdate.state === 'available'
|
|
2163
|
+
? h('button', { type: 'button', disabled: busy, onClick: () => void installBundleUpdate(), style: primaryButtonStyle }, busy ? '更新中…' : '更新 ANT') : null) : null,
|
|
2164
|
+
h('button', { type: 'button', disabled: busy || !canCheckUpdate || bundleUpdate.state === 'checking',
|
|
2165
|
+
onClick: () => void checkBundleUpdate(), style: secondaryButtonStyle }, bundleUpdate.state === 'checking' ? '检查中…' : '重新检查更新'))
|
|
2166
|
+
|
|
2167
|
+
return h('div', { style: { display: 'grid', gap: 16, width: '100%' } },
|
|
2168
|
+
connectionPanel, modelPanel, updatePanel)
|
|
2184
2169
|
|
|
2185
2170
|
}
|
|
2186
2171
|
|