@ai-wendao/ant 0.4.10 → 0.4.12
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 +8 -0
- package/client.js +395 -83
- package/conversation.js +101 -31
- package/delegation.js +18 -11
- package/index.js +28 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,12 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
ANT (AI-native Teams) connects a DeepSeek Harness Desktop profile to its team's membership and approved models.
|
|
4
4
|
|
|
5
|
+
## 团队模型默认值
|
|
6
|
+
|
|
7
|
+
团队管理员在 ANT 控制台设置团队默认模型,并授权成员可用模型。成员第一次在 ANT 插件中同步模型时,团队默认会写入 DSH 的全局默认模型;成员之后可在“团队模型”页选另一项已获准模型。普通同步保留成员选择;若模型被撤销,插件会回到仍获准的团队默认。该默认写入 DSH profile,因此也影响同一 profile 新建的普通 DSH 会话。ANT 对话和任务会额外校验当前用户的获准模型;Codex 与 Claude Code 使用各自执行器的默认模型。
|
|
8
|
+
|
|
9
|
+
每个 OpenAI 兼容模型可配置 Chat Completions 或 Responses API。旧模型记录在升级后按 Chat Completions 解释;ANT 服务端继续使用原配置的 `/chat/completions` URL,并从相同 API 根路径派生 `/responses` 上游路径。
|
|
10
|
+
|
|
5
11
|
飞书和企业微信的机器人连接、消息收发由 DSH-IM 负责;ANT Bundle 不保存机器人密钥或直连 IM 平台。ANT 继续负责团队成员身份和模型授权,并可在成员显式开启时接管一个专用飞书机器人的文本收件,按成员授权范围处理协作请求。
|
|
6
12
|
|
|
7
13
|
## 协作机器人与任务卡
|
|
8
14
|
|
|
9
15
|
成员在 ANT 插件设置页选择一个 DSH-IM 飞书机器人并启用协作收件。每段私聊、群聊或话题由同一受限 DSH 会话自然回答和追问;只有 DSH 准备执行任务时,才通过 ANT 工具提交任务卡 ID 和参数,由服务端再次核对真实请求人、群、参数及授权。成员按需创建任务卡,设置意图、参数、请求人、适用会话和有效期;PR 审查只是可选示例。普通文字任务由没有本机工具权限的 DSH 会话处理。单个 Gitea PR 参数还可选择 Codex 专用只读 CLI,或 DSH 已注册的 Claude Code 子代理试用:ANT 用只读 Gitea 令牌为每项 PR 审查能力维护本机仓库缓存,并按已核验的 PR Head SHA 创建独立工作树;令牌不会交给执行器。Codex 使用固定只读权限配置,Claude Code 沿用 DSH 提供方的本机权限设置。执行器可查看完整源码和已核验 diff;工作区不含 `.git`、符号链接或子模块,也不会运行仓库脚本。Claude Code 仍是所有者手动开放的本机试用,不宣称具备操作系统级只读隔离。成员可查询或取消任务;每台电脑默认同时处理三项任务,成员可调整,旧版 PR 审查与新任务共用名额。
|
|
10
16
|
|
|
17
|
+
PR 任务卡可勾选 DSH、Codex 和 Claude Code 等这台 DSH 可用的审查 Agent,并指定普通请求的默认执行方。普通审查只启动默认 Agent;请求人明确提出“交叉 review”时,ANT 会并行启动所有勾选项、提供分别标明执行方的结果。交叉审查至少需要两位 Agent;个别 Agent 失败时保留成功结果并标明失败项。Codex 和 Claude Code 只支持一个必填的 Gitea PR 参数;DSH 审查已提供的 diff,不读取完整仓库。
|
|
18
|
+
|
|
11
19
|
私聊默认开启,所有者可按机器人单独关闭或重新开启;关闭后新私聊消息不会进入 DSH,机器人每五分钟最多向同一请求人发送一次静态恢复指引;所有者身份绑定命令仍可用。关闭前已提交的任务不会撤销,仍回复原私聊;群聊设置不受影响。群聊由所有者逐群启用普通聊天,任务授权再按卡单独设置。群里有人 @ 机器人后,群 ID 出现在插件列表;所有者也可手动填写群 ID。若所有者在群里,可先私聊机器人绑定飞书身份,再在群内提出“在本群启用聊天”,或按提示确认“在本群启用任务 任务名称”。群消息只在明确 @ 时处理,任务结果公开回复原群/原话题;要求私聊回复的群消息会提示请求人改为私聊发起。当前 DSH-IM 未提供可核验的机器人进/出群发起者,所以不会因被拉群自动启用。不同群、话题和私聊的上下文默认隔离;每项能力当前只能关联一个范围,即私聊或一个群;要让多个群或私聊都能执行,需分别建立能力卡,并分别设定请求人和有效期。群历史按需读取,默认先看十条、最多检查一百条平台记录,实际可见范围由飞书权限决定。
|
|
12
20
|
|
|
13
21
|
新建任务卡的 Codex 与 Claude Code 执行方目前只接受一个必填 Gitea PR 参数。Gitea PR 源仓库和目标仓库大小按 API KiB 换算为 bytes,并在 fetch 前与 512 MiB 单仓库快照上限及本机总缓存预算核对。每台 DSH 的长期 bare 仓库缓存默认最多 10 GiB,可在插件的“本机处理设置”调整为 1–100 GiB;按缓存文件逻辑大小计算,不含临时工作目录及文件系统分配开销。预算也为即将拉取的仓库预留空间。Git fetch 强制使用单 pack;POSIX 系统用进程文件大小限制约束 pack 与 index 的总预算,并监控缓存总量。正常情况下不会淘汰仍启用能力的缓存;若实际 fetch 越界,会清理本项能力的仓库缓存(旧 PR 可在后续任务重新下载)。Windows 暂未实现等效的原生 fetch 硬限制,Codex/Claude Code PR 快照会失败关闭,界面会禁用对应选项并提示 DSH 差异审查。停用/过期能力的缓存会在刷新时回收。任务工作树是短期目录,受单快照 512 MiB、最多 10,000 个文件、单文件最多 64 MiB 限制,不计入长期缓存预算,完成后删除。快照不支持符号链接或子模块,也不下载 Git LFS 实际文件;快照中的 LFS 指针不能代表已审查的实际文件内容。Codex/Claude Code 的 granular Gitea Token 还需 `read:repository` 和 `read:user`,后者仅用于本机获取 Git HTTPS 用户名。超限时任务失败,不降级为 diff-only。快照与权限验收范围见[Claude Code 任务卡计划](../../docs/plans/2026-10-22-claude-code-task-card.md)及[执行方隔离](../../docs/decisions/2026-10-22-im-native-executor-isolation.md)。普通对话和 DSH 文字任务不直接获得文件、命令或网络工具。机器人独占收件模式由 DSH-IM 保存;停止 ANT 授权后若需恢复 DSH-IM 原有聊天,需要在 DSH-IM 中切回普通模式。
|
package/client.js
CHANGED
|
@@ -6,7 +6,9 @@ window.__ModuleLoader__.load({
|
|
|
6
6
|
const { createElement, useEffect, useRef, useState } = require('react')
|
|
7
7
|
const h = createElement
|
|
8
8
|
const SERVICE_URL_STORAGE_KEY = 'ant-team-service-url'
|
|
9
|
+
const PROVIDER_ROUTES_STORAGE_KEY = 'ant-team-provider-routes'
|
|
9
10
|
const DEVICE_ID_STORAGE_KEY = 'ant-team-device-id'
|
|
11
|
+
const DEFAULT_MODEL_INITIALIZED_KEY = 'ant-team-default-model-initialized'
|
|
10
12
|
const ANT_BUNDLE_PACKAGE_NAME = '@ai-wendao/ant'
|
|
11
13
|
const ANT_API_PROTOCOL_VERSIONS = [1]
|
|
12
14
|
const PLUGIN_INSTALL_RECORD_PREFIX = 'ant-reviewed-plugin-install:'
|
|
@@ -473,12 +475,15 @@ window.__ModuleLoader__.load({
|
|
|
473
475
|
const [examples, setExamples] = useState('')
|
|
474
476
|
const [taskPrompt, setTaskPrompt] = useState('')
|
|
475
477
|
const [runner, setRunner] = useState('dsh')
|
|
478
|
+
const [reviewers, setReviewers] = useState(['dsh'])
|
|
476
479
|
const [parameters, setParameters] = useState([])
|
|
477
480
|
const [giteaToken, setGiteaToken] = useState('')
|
|
478
481
|
const [tokenConfigured, setTokenConfigured] = useState(false)
|
|
479
482
|
const [senderScope, setSenderScope] = useState('any')
|
|
480
483
|
const [senderIds, setSenderIds] = useState('')
|
|
481
484
|
const [days, setDays] = useState(7)
|
|
485
|
+
const [editingCardId, setEditingCardId] = useState(null)
|
|
486
|
+
const [editingExpiresAt, setEditingExpiresAt] = useState(null)
|
|
482
487
|
const [busy, setBusy] = useState(false)
|
|
483
488
|
const [loading, setLoading] = useState(true)
|
|
484
489
|
const [botLoadError, setBotLoadError] = useState(false)
|
|
@@ -554,17 +559,28 @@ window.__ModuleLoader__.load({
|
|
|
554
559
|
const enrolledCount = enrolled.filter((item) => item.device_bound_to_current
|
|
555
560
|
&& connectedBots.some((bot) => bot.fingerprint === item.bot_fingerprint)).length
|
|
556
561
|
const selectedTaskCount = activeCards.filter((card) => card.bot_fingerprint === selectedBot?.fingerprint).length
|
|
557
|
-
const
|
|
562
|
+
const nativeReviewerSelected = reviewers.some((item) => ['codex', 'claude-code'].includes(item))
|
|
563
|
+
const hasPrParameter = parameters.some((item) => item.kind === 'gitea_pr')
|
|
558
564
|
const codexAvailable = executors.some((item) => item.id === 'codex-readonly' && item.available)
|
|
559
565
|
const claudeCodeAvailable = executors.some((item) => item.id === 'claude-code' && item.available)
|
|
566
|
+
const dshAvailable = executors.some((item) => item.id === 'dsh' && item.available)
|
|
560
567
|
const nativePrUnavailableReason = executors.find((item) => item.reason)?.reason || null
|
|
568
|
+
const reviewerLabel = (id) => ({ dsh: 'DSH', codex: 'Codex', 'claude-code': 'Claude Code' })[id] || id
|
|
569
|
+
const cardReviewerSummary = (card) => {
|
|
570
|
+
const selected = Array.isArray(card.reviewers) && card.reviewers.length ? card.reviewers : [card.runner]
|
|
571
|
+
return selected.length > 1
|
|
572
|
+
? `交叉可用:${selected.map(reviewerLabel).join('、')};默认 ${reviewerLabel(card.runner)}`
|
|
573
|
+
: card.runner === 'codex' ? 'Codex PR 快照审查'
|
|
574
|
+
: card.runner === 'claude-code' ? 'Claude Code PR 快照审查'
|
|
575
|
+
: 'DSH 文字处理'
|
|
576
|
+
}
|
|
561
577
|
const requesterSummary = senderScope === 'allowlist'
|
|
562
578
|
? `仅指定成员:${senderIds.trim() || '尚未填写成员 ID'}`
|
|
563
579
|
: conversationKind === 'group' ? `群 ${groupId || '尚未选择'} 的所有成员` : '任何能私聊此机器人的人'
|
|
564
580
|
const resultRouteSummary = conversationKind === 'group' ? '回复到原群(群内可见)' : '回复到发起任务的私聊'
|
|
565
581
|
const executorSummary = runner === 'codex' ? '本机 Codex · PR 快照'
|
|
566
582
|
: runner === 'claude-code' ? '本机 Claude Code · PR 快照' : 'DSH · 当前授权模型'
|
|
567
|
-
const resourceSummary =
|
|
583
|
+
const resourceSummary = nativeReviewerSelected ? '指定仓库的已核验 PR 快照'
|
|
568
584
|
: '当前会话中 DSH 实际可见的内容'
|
|
569
585
|
const cardBlockReason = enrollmentLoadError ? '暂时无法核对机器人是否已启用协作,请刷新状态后再试。'
|
|
570
586
|
: !botConnected || !selectedEnrollmentBound ? '先在上方把机器人绑定到这台 DSH。'
|
|
@@ -577,20 +593,56 @@ window.__ModuleLoader__.load({
|
|
|
577
593
|
: parameters.some((rule) => rule.kind === 'gitea_pr')
|
|
578
594
|
&& normalizeServiceUrl(trustedOrigin) !== selectedEnrollment?.gitea_origin
|
|
579
595
|
? '请先保存机器人级可信 Gitea 地址;PR 参数会自动使用这个地址。'
|
|
580
|
-
:
|
|
596
|
+
: nativeReviewerSelected && (parameters.length !== 1 || parameters[0].kind !== 'gitea_pr')
|
|
581
597
|
? 'Codex 和 Claude Code 目前只支持单个 Gitea PR 审查任务。'
|
|
582
|
-
:
|
|
598
|
+
: nativeReviewerSelected && !parameters[0].required
|
|
583
599
|
? '本机代码审查的 Gitea PR 编号必须设为必填。'
|
|
584
|
-
:
|
|
585
|
-
? '这台 DSH
|
|
586
|
-
|
|
587
|
-
|
|
600
|
+
: reviewers.includes('dsh') && !dshAvailable
|
|
601
|
+
? '这台 DSH 尚未准备好文字审查执行器。'
|
|
602
|
+
: reviewers.includes('codex') && !codexAvailable
|
|
603
|
+
? '这台 DSH 尚未找到可用的 Codex CLI,请先安装或检查 DSH 的 PATH。'
|
|
604
|
+
: reviewers.includes('claude-code') && !claudeCodeAvailable
|
|
605
|
+
? '这台 DSH 尚未注册 Claude Code 子代理,请先在 DSH 中安装并启用它。'
|
|
588
606
|
: ''
|
|
589
607
|
const loadNames = { bots: '机器人', 'enrolled-bots': '协作启用状态', groups: '群聊列表',
|
|
590
608
|
cards: '任务授权', settings: '团队默认值', executors: '本机执行器',
|
|
591
609
|
'local-settings': '本机设置', status: '最近任务' }
|
|
592
610
|
const updateParameter = (index, patch) => setParameters((current) => current.map((rule, i) => i === index ? { ...rule, ...patch } : rule))
|
|
611
|
+
const changeReviewer = (id, enabled) => {
|
|
612
|
+
const next = enabled ? [...reviewers, id] : reviewers.filter((item) => item !== id)
|
|
613
|
+
if (!next.length) { setMessage('至少保留一位审查 Agent。'); return }
|
|
614
|
+
setReviewers([...new Set(next)])
|
|
615
|
+
if (!next.includes(runner)) setRunner(next[0])
|
|
616
|
+
}
|
|
617
|
+
const startNewCard = () => {
|
|
618
|
+
setEditingCardId(null); setEditingExpiresAt(null)
|
|
619
|
+
setDraftNotice('')
|
|
620
|
+
setTitle(''); setIntent(''); setExamples(''); setTaskPrompt(''); setRunner('dsh')
|
|
621
|
+
setReviewers(['dsh'])
|
|
622
|
+
setParameters([]); setConversationKind('dm'); setGroupId('')
|
|
623
|
+
setSenderScope('any'); setSenderIds(''); setDays(7); setCreateOpen(true)
|
|
624
|
+
}
|
|
625
|
+
const editCard = (card, copy = false) => {
|
|
626
|
+
setBotId(card.bot_id)
|
|
627
|
+
setTrustedOrigin(enrolled.find((bot) => bot.bot_id === card.bot_id)?.gitea_origin || '')
|
|
628
|
+
setEditingCardId(copy ? null : card.id); setEditingExpiresAt(copy ? null : card.expires_at)
|
|
629
|
+
setTitle(copy ? `${card.title.slice(0, 94)}(副本)` : card.title)
|
|
630
|
+
if (copy) setDays(7)
|
|
631
|
+
setIntent(card.intent); setExamples((card.examples || []).join('\n'))
|
|
632
|
+
setTaskPrompt(card.task_prompt); setRunner(card.runner)
|
|
633
|
+
setReviewers(Array.isArray(card.reviewers) && card.reviewers.length ? card.reviewers : [card.runner])
|
|
634
|
+
setParameters((card.parameters || []).map((rule) => ({
|
|
635
|
+
name: rule.name, kind: rule.kind, required: rule.required,
|
|
636
|
+
description: rule.description || '', min: rule.min == null ? '' : String(rule.min),
|
|
637
|
+
max: rule.max == null ? '' : String(rule.max), choices: (rule.choices || []).join(', '),
|
|
638
|
+
gitea_origin: rule.gitea_origin || '', repository: rule.repository || '',
|
|
639
|
+
})))
|
|
640
|
+
setConversationKind(card.conversation_kind); setGroupId(card.conversation_id || '')
|
|
641
|
+
setSenderScope(card.sender_scope); setSenderIds((card.sender_ids || []).join(', '))
|
|
642
|
+
setCreateOpen(true)
|
|
643
|
+
}
|
|
593
644
|
const preparePrExample = () => {
|
|
645
|
+
setEditingCardId(null); setEditingExpiresAt(null)
|
|
594
646
|
setTitle('审查 Gitea PR 差异')
|
|
595
647
|
setIntent('同事希望查看指定 Gitea PR 的代码差异并获得审查意见。')
|
|
596
648
|
setExamples('请帮我审查 123 号 PR\n看看刚才提到的 PR 有什么风险')
|
|
@@ -598,7 +650,13 @@ window.__ModuleLoader__.load({
|
|
|
598
650
|
setParameters([{ name: 'pr_number', kind: 'gitea_pr', required: true,
|
|
599
651
|
description: '要审查的 PR 编号', min: '1', max: '', choices: '',
|
|
600
652
|
gitea_origin: selectedEnrollment?.gitea_origin || trustedOrigin || '', repository: '' }])
|
|
601
|
-
|
|
653
|
+
const availableReviewers = [
|
|
654
|
+
...(codexAvailable ? ['codex'] : []),
|
|
655
|
+
...(claudeCodeAvailable ? ['claude-code'] : []),
|
|
656
|
+
]
|
|
657
|
+
const selected = availableReviewers.length ? availableReviewers : ['dsh']
|
|
658
|
+
setReviewers(selected)
|
|
659
|
+
setRunner(selected[0])
|
|
602
660
|
setCreateOpen(true)
|
|
603
661
|
}
|
|
604
662
|
const saveGiteaToken = async () => {
|
|
@@ -804,6 +862,9 @@ window.__ModuleLoader__.load({
|
|
|
804
862
|
event.preventDefault()
|
|
805
863
|
setDraftNotice('')
|
|
806
864
|
if (!selectedBot || !botConnected || !selectedEnrollmentBound) { setMessage('请先在这台 DSH 绑定选中的协作机器人。'); return }
|
|
865
|
+
if (editingCardId && (!editingExpiresAt || editingExpiresAt <= Math.floor(Date.now() / 1000))) {
|
|
866
|
+
setMessage('这项能力已到期,无法继续编辑;请返回后重新开放一项能力。'); return
|
|
867
|
+
}
|
|
807
868
|
if (!title.trim() || !intent.trim() || !taskPrompt.trim()) {
|
|
808
869
|
setMessage('请填写能力名称、适用请求说明和处理要求。'); return
|
|
809
870
|
}
|
|
@@ -855,45 +916,67 @@ window.__ModuleLoader__.load({
|
|
|
855
916
|
if (normalized.some((rule) => rule.kind === 'gitea_pr' && !validRepository(rule.repository))) {
|
|
856
917
|
setMessage('Gitea 仓库请填写“团队名/仓库名”,不要包含空格或多余路径。'); return
|
|
857
918
|
}
|
|
858
|
-
if (
|
|
859
|
-
|
|
860
|
-
setMessage('Codex 和 Claude Code 目前需要一个必填的 Gitea PR 编号参数。'); return
|
|
919
|
+
if (!reviewers.length || !reviewers.includes(runner)) {
|
|
920
|
+
setMessage('请至少选择一位审查 Agent,并设置普通请求的默认执行方。'); return
|
|
861
921
|
}
|
|
862
|
-
if (
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
if (runner === 'claude-code' && !claudeCodeAvailable) {
|
|
866
|
-
setMessage('这台 DSH 尚未注册 Claude Code 子代理。'); return
|
|
922
|
+
if (nativeReviewerSelected && (normalized.length !== 1
|
|
923
|
+
|| normalized[0].kind !== 'gitea_pr' || !normalized[0].required)) {
|
|
924
|
+
setMessage('选择 Codex 或 Claude Code 时,需要一个必填的 Gitea PR 编号参数。'); return
|
|
867
925
|
}
|
|
868
926
|
const scope = conversationKind === 'group' ? `群 ${groupId}` : '私聊'
|
|
869
927
|
const resultDestination = conversationKind === 'group' ? '原群(群内可见)' : '发起任务的私聊'
|
|
870
|
-
const
|
|
871
|
-
|
|
872
|
-
:
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
928
|
+
const executionDetails = {
|
|
929
|
+
dsh: 'DSH 只根据已提供的 PR diff 审查,不读取完整仓库。',
|
|
930
|
+
codex: 'Codex 使用已核验 PR Head 的临时代码快照,并启用本机固定只读权限;不接收 Gitea 令牌。',
|
|
931
|
+
'claude-code': 'Claude Code 使用同一份临时代码快照,沿用这台 DSH 的本机权限设置;请确认该权限适合由获准成员请求审查。',
|
|
932
|
+
}
|
|
933
|
+
const executionInfo = reviewers.length > 1
|
|
934
|
+
? `普通请求默认由 ${reviewerLabel(runner)} 执行;请求人明确说“交叉 review”时,会并行启动 ${reviewers.map(reviewerLabel).join('、')},分别返回审查结果。${reviewers.map((item) => executionDetails[item]).join(' ')}`
|
|
935
|
+
: executionDetails[reviewers[0]]
|
|
936
|
+
const actionDescription = editingCardId
|
|
937
|
+
? `更新“${title}”后,${requesterSummary}可在${scope}使用新配置;旧待确认请求将失效,已提交任务继续使用提交时的配置。结果${resultDestination}。到期时间保持为 ${new Date(editingExpiresAt * 1000).toLocaleString('zh-CN')}。`
|
|
938
|
+
: `开放“${title}”后,${requesterSummary}可在${scope}请求,有效期 ${days} 天;结果${resultDestination}。`
|
|
939
|
+
if (!window.confirm(`${actionDescription}${executionInfo}请确认。`)) return
|
|
876
940
|
setBusy(true)
|
|
877
941
|
try {
|
|
878
942
|
const response = await fetch('/api/ant-teams/delegation/cards', {
|
|
879
|
-
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
880
|
-
body: JSON.stringify({
|
|
943
|
+
method: editingCardId ? 'PUT' : 'POST', headers: { 'content-type': 'application/json' },
|
|
944
|
+
body: JSON.stringify({ ...(editingCardId ? { id: editingCardId } : {}),
|
|
945
|
+
bot_id: selectedBot.bot_id, bot_fingerprint: selectedBot.fingerprint,
|
|
881
946
|
title, intent, examples: exampleLines,
|
|
882
|
-
parameters: normalized, task_prompt: taskPrompt, runner, workspace: null,
|
|
947
|
+
parameters: normalized, task_prompt: taskPrompt, runner, reviewers, workspace: null,
|
|
883
948
|
sender_scope: senderScope, sender_ids: senderScope === 'allowlist'
|
|
884
949
|
? senderIds.split(/[\s,,]+/u).filter(Boolean) : [], conversation_kind: conversationKind,
|
|
885
950
|
...(conversationKind === 'group' ? { conversation_id: groupId } : {}),
|
|
886
|
-
expires_at: Math.floor(Date.now() / 1000) + Number(days) * 86400 }),
|
|
951
|
+
expires_at: editingCardId ? editingExpiresAt : Math.floor(Date.now() / 1000) + Number(days) * 86400 }),
|
|
887
952
|
})
|
|
888
953
|
const result = await readLocalJsonResponse(response)
|
|
889
|
-
if (!response.ok &&
|
|
890
|
-
throw new Error('当前团队服务端可能尚不支持 Claude Code
|
|
954
|
+
if (!response.ok && reviewers.includes('claude-code') && result.code === 'INVALID_REQUEST') {
|
|
955
|
+
throw new Error('当前团队服务端可能尚不支持 Claude Code 审查配置。请管理员升级服务端后刷新重试。')
|
|
891
956
|
}
|
|
892
957
|
if (!response.ok) throw new Error(result.message || '能力保存失败。')
|
|
893
|
-
|
|
958
|
+
const savedReviewers = Array.isArray(result.reviewers) && result.reviewers.length
|
|
959
|
+
? result.reviewers : [result.runner]
|
|
960
|
+
if ([...savedReviewers].sort().join('\0') !== [...reviewers].sort().join('\0')) {
|
|
961
|
+
if (!editingCardId && typeof result.id === 'string') {
|
|
962
|
+
const cleanup = await fetch('/api/ant-teams/delegation/cards', {
|
|
963
|
+
method: 'DELETE', headers: { 'content-type': 'application/json' },
|
|
964
|
+
body: JSON.stringify({ id: result.id }),
|
|
965
|
+
}).catch(() => null)
|
|
966
|
+
if (!cleanup?.ok) {
|
|
967
|
+
await load().catch(() => undefined)
|
|
968
|
+
throw new Error('团队服务端尚不支持多 Agent 审查配置;旧服务端可能已保存单 Agent 授权,请刷新列表并停止它,再升级 ANT 服务端。')
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
throw new Error('当前团队服务端尚不支持多 Agent 审查配置;请管理员升级 ANT 服务端后重试。')
|
|
972
|
+
}
|
|
973
|
+
setTitle(''); setIntent(''); setExamples(''); setTaskPrompt(''); setParameters([]); setRunner('dsh'); setReviewers(['dsh'])
|
|
974
|
+
setEditingCardId(null); setEditingExpiresAt(null)
|
|
894
975
|
await load()
|
|
895
976
|
setCreateOpen(false)
|
|
896
|
-
setMessage(
|
|
977
|
+
setMessage(editingCardId
|
|
978
|
+
? '能力配置已更新。新请求将使用新配置,已提交任务保留原配置。'
|
|
979
|
+
: '能力已开放。同事可自然地提出这类请求;参数不完整时机器人会追问。')
|
|
897
980
|
} catch (error) { setMessage(error.message) }
|
|
898
981
|
finally { setBusy(false) }
|
|
899
982
|
}
|
|
@@ -910,6 +993,7 @@ window.__ModuleLoader__.load({
|
|
|
910
993
|
finally { setBusy(false) }
|
|
911
994
|
}
|
|
912
995
|
const copyCardToDraft = (card) => {
|
|
996
|
+
setEditingCardId(null); setEditingExpiresAt(null)
|
|
913
997
|
const cardBot = bots.find((bot) => bot.fingerprint === card.bot_fingerprint)
|
|
914
998
|
const cardEnrollment = enrolled.find((bot) => bot.bot_fingerprint === card.bot_fingerprint)
|
|
915
999
|
const titleChars = Array.from(`复制:${card.title || '未命名能力'}`)
|
|
@@ -921,6 +1005,7 @@ window.__ModuleLoader__.load({
|
|
|
921
1005
|
setExamples(Array.isArray(card.examples) ? card.examples.join('\n') : '')
|
|
922
1006
|
setTaskPrompt(card.task_prompt || '')
|
|
923
1007
|
setRunner(['dsh', 'codex', 'claude-code'].includes(card.runner) ? card.runner : 'dsh')
|
|
1008
|
+
setReviewers(Array.isArray(card.reviewers) && card.reviewers.length ? card.reviewers : [card.runner || 'dsh'])
|
|
924
1009
|
setParameters(Array.isArray(card.parameters) ? card.parameters.map((rule) => ({
|
|
925
1010
|
...rule, min: String(rule.min ?? ''), max: String(rule.max ?? ''), choices: Array.isArray(rule.choices) ? rule.choices.join(',') : String(rule.choices ?? ''),
|
|
926
1011
|
})) : [])
|
|
@@ -1067,8 +1152,6 @@ window.__ModuleLoader__.load({
|
|
|
1067
1152
|
? `群 ${groups.find((group) => group.group_id === card.conversation_id)?.group_code || card.conversation_id}` : '私聊'
|
|
1068
1153
|
const requester = card.sender_scope === 'allowlist' ? '指定成员可请求'
|
|
1069
1154
|
: card.conversation_kind === 'group' ? '群成员可请求' : '可私聊者可请求'
|
|
1070
|
-
const runnerName = card.runner === 'codex' ? '本机 Codex · PR 快照'
|
|
1071
|
-
: card.runner === 'claude-code' ? '本机 Claude Code · PR 快照' : 'DSH 文字处理'
|
|
1072
1155
|
const parametersText = (card.parameters || []).map((rule) => {
|
|
1073
1156
|
const constraint = rule.kind === 'gitea_pr' && rule.repository ? `仓库 ${rule.repository}`
|
|
1074
1157
|
: rule.kind === 'choice' ? `选项 ${(rule.choices || []).join('、')}`
|
|
@@ -1087,7 +1170,7 @@ window.__ModuleLoader__.load({
|
|
|
1087
1170
|
h('strong', { style: { overflowWrap: 'anywhere' } }, card.title || '未命名能力'),
|
|
1088
1171
|
h('span', { style: { ...mutedTextStyle, padding: '2px 8px', borderRadius: 999,
|
|
1089
1172
|
backgroundColor: 'var(--dsw-alias-bg-layer-2)', color: remainingDaysLabel, whiteSpace: 'nowrap' } }, expiry)),
|
|
1090
|
-
|
|
1173
|
+
h('span', { style: mutedTextStyle }, `${cardReviewerSummary(card)} · ${conversation} · ${requester} · 结果回复原${card.conversation_kind === 'group' ? '群' : '私聊'}`),
|
|
1091
1174
|
h('details', null,
|
|
1092
1175
|
h('summary', { style: { cursor: 'pointer', color: 'var(--dsw-alias-link)', fontSize: 13 } }, '查看配置'),
|
|
1093
1176
|
h('div', { style: { display: 'grid', gap: 4, paddingTop: 6 } },
|
|
@@ -1098,6 +1181,7 @@ window.__ModuleLoader__.load({
|
|
|
1098
1181
|
examplesText ? h('p', { style: mutedTextStyle }, `请求示例:${examplesText}`) : null,
|
|
1099
1182
|
card.task_prompt ? h('p', { style: mutedTextStyle }, `处理要求:${card.task_prompt}`) : null))),
|
|
1100
1183
|
h('div', { style: { display: 'flex', gap: 6, flexWrap: 'wrap', justifyContent: 'flex-end' } },
|
|
1184
|
+
h('button', { type: 'button', disabled: busy, onClick: () => editCard(card), style: secondaryButtonStyle }, '编辑'),
|
|
1101
1185
|
h('button', { type: 'button', disabled: busy, onClick: () => copyCardToDraft(card), style: secondaryButtonStyle }, '复制'),
|
|
1102
1186
|
h('button', { type: 'button', disabled: busy, onClick: () => void revokeCard(card), style: secondaryButtonStyle }, '停止')))
|
|
1103
1187
|
}),
|
|
@@ -1133,10 +1217,12 @@ window.__ModuleLoader__.load({
|
|
|
1133
1217
|
h('details', { id: 'ant-new-task', open: createOpen,
|
|
1134
1218
|
onToggle: (event) => setCreateOpen(event.currentTarget.open),
|
|
1135
1219
|
style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, padding: 12 } },
|
|
1136
|
-
h('summary', { id: 'ant-new-task-summary', style: { cursor: 'pointer', fontWeight: 600 } }, '开放一项能力'),
|
|
1220
|
+
h('summary', { id: 'ant-new-task-summary', style: { cursor: 'pointer', fontWeight: 600 } }, editingCardId ? '编辑能力配置' : '开放一项能力'),
|
|
1137
1221
|
h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 320px), 1fr))',
|
|
1138
1222
|
gap: 16, alignItems: 'start', marginTop: 12 } },
|
|
1139
1223
|
h('form', { id: 'ant-capability-create-form', onSubmit: createCard, style: { display: 'grid', gap: 9, minWidth: 0 } },
|
|
1224
|
+
editingCardId ? h('p', { role: 'status', style: mutedTextStyle },
|
|
1225
|
+
`保存会生成新的授权版本并停止旧版本;到期时间保持不变:${new Date(editingExpiresAt * 1000).toLocaleString('zh-CN')}。已提交任务继续使用原配置。`) : null,
|
|
1140
1226
|
h('p', { style: mutedTextStyle }, selectedBot
|
|
1141
1227
|
? `当前机器人:${selectedBot.name}${selectedEnrollmentBound ? ' · 已绑定此设备' : ' · 请先绑定到此设备'}`
|
|
1142
1228
|
: '请先在上方选择机器人并启用协作。'),
|
|
@@ -1156,20 +1242,29 @@ window.__ModuleLoader__.load({
|
|
|
1156
1242
|
onChange: (event) => setExamples(event.target.value), placeholder: '帮我总结一下刚才的讨论', style: inputStyle })),
|
|
1157
1243
|
h('label', null, '处理要求', h('textarea', { required: true, maxLength: 4000, rows: 3, value: taskPrompt,
|
|
1158
1244
|
onChange: (event) => setTaskPrompt(event.target.value), placeholder: '只使用当前会话可见的内容,列出结论和待办', style: inputStyle })),
|
|
1159
|
-
h('
|
|
1160
|
-
|
|
1161
|
-
h('
|
|
1162
|
-
h('
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
'
|
|
1245
|
+
h('fieldset', { style: { ...panelStyle, padding: 10, display: 'grid', gap: 7 } },
|
|
1246
|
+
h('legend', null, '审查 Agent(可多选)'),
|
|
1247
|
+
h('p', { style: mutedTextStyle }, '普通请求使用默认 Agent;明确说“交叉 review”时,所有勾选项会并行审查同一份 PR 快照(至少勾选两位才构成交叉审查,最多三位,并会同时消耗各自的本机或团队模型额度)。'),
|
|
1248
|
+
h('div', { style: { display: 'flex', gap: 14, flexWrap: 'wrap' } },
|
|
1249
|
+
h('label', null, h('input', { type: 'checkbox', 'aria-label': '审查 Agent DSH', checked: reviewers.includes('dsh'), disabled: !dshAvailable && !reviewers.includes('dsh'),
|
|
1250
|
+
onChange: (event) => changeReviewer('dsh', event.target.checked) }), ' DSH · 仅审查已提供的 diff'),
|
|
1251
|
+
h('label', null, h('input', { type: 'checkbox', 'aria-label': '审查 Agent Codex', checked: reviewers.includes('codex'),
|
|
1252
|
+
disabled: (!codexAvailable || !hasPrParameter) && !reviewers.includes('codex'),
|
|
1253
|
+
onChange: (event) => changeReviewer('codex', event.target.checked) }), ' Codex · 完整快照只读审查'),
|
|
1254
|
+
h('label', null, h('input', { type: 'checkbox', 'aria-label': '审查 Agent Claude Code', checked: reviewers.includes('claude-code'),
|
|
1255
|
+
disabled: (!claudeCodeAvailable || !hasPrParameter) && !reviewers.includes('claude-code'),
|
|
1256
|
+
onChange: (event) => changeReviewer('claude-code', event.target.checked) }), ' Claude Code · 使用本机权限审查')),
|
|
1257
|
+
!hasPrParameter ? h('p', { style: mutedTextStyle }, 'Codex 与 Claude Code 需要先添加一个必填的 Gitea PR 编号参数。') : null,
|
|
1258
|
+
reviewers.length > 1 ? h('label', null, '普通请求默认执行方', h('select', { value: runner,
|
|
1259
|
+
onChange: (event) => setRunner(event.target.value), style: inputStyle },
|
|
1260
|
+
...reviewers.map((item) => h('option', { key: item, value: item }, reviewerLabel(item))))) : null),
|
|
1166
1261
|
nativePrUnavailableReason ? h('p', { role: 'status', style: mutedTextStyle },
|
|
1167
1262
|
`${nativePrUnavailableReason},已禁用 Codex/Claude Code 完整快照选项;可选择 DSH 差异审查。`) : null,
|
|
1168
|
-
|
|
1169
|
-
'Codex 使用这台电脑的登录账号及其用量;ANT 为每项能力维护仓库缓存,每次审查创建固定到 PR Head 的临时代码快照。不会收到 Gitea
|
|
1170
|
-
|
|
1171
|
-
'Claude Code 通过 DSH
|
|
1172
|
-
|
|
1263
|
+
reviewers.includes('codex') ? h('p', { role: 'status', style: mutedTextStyle },
|
|
1264
|
+
'Codex 使用这台电脑的登录账号及其用量;ANT 为每项能力维护仓库缓存,每次审查创建固定到 PR Head 的临时代码快照。不会收到 Gitea 令牌;Git LFS 实际文件不会下载。') : null,
|
|
1265
|
+
reviewers.includes('claude-code') ? h('p', { role: 'status', style: mutedTextStyle },
|
|
1266
|
+
'Claude Code 通过 DSH 已注册的子代理运行,可查看同一 PR 快照。ANT 不会传 Gitea 令牌;它沿用 DSH 的本机权限设置,不具备 Codex 路径相同的固定只读隔离。Git LFS 实际文件不会下载。') : null,
|
|
1267
|
+
nativeReviewerSelected ? h('p', { role: 'status', style: mutedTextStyle },
|
|
1173
1268
|
'本机 PR 执行方需要 Gitea granular token 的只读权限 read:repository 和 read:user;read:user 仅用于这台 DSH 获取 Git HTTPS 登录名。') : null,
|
|
1174
1269
|
h('details', { style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, padding: 10 } },
|
|
1175
1270
|
h('summary', { style: { cursor: 'pointer' } }, '参数与授权范围'),
|
|
@@ -1186,7 +1281,7 @@ window.__ModuleLoader__.load({
|
|
|
1186
1281
|
onChange: (event) => updateParameter(index, { kind: event.target.value }), style: { ...inputStyle, flex: '1 1 150px' } },
|
|
1187
1282
|
h('option', { value: 'text' }, '文本'), h('option', { value: 'integer' }, '整数'),
|
|
1188
1283
|
h('option', { value: 'choice' }, '选项'), h('option', { value: 'gitea_pr' }, 'Gitea PR 编号')),
|
|
1189
|
-
h('label', null, h('input', { type: 'checkbox',
|
|
1284
|
+
h('label', null, h('input', { type: 'checkbox', 'aria-label': '参数必填', checked: rule.required,
|
|
1190
1285
|
onChange: (event) => updateParameter(index, { required: event.target.checked }) }), ' 必填'),
|
|
1191
1286
|
['integer', 'gitea_pr'].includes(rule.kind) ? h('input', { 'aria-label': '最小值', type: 'number', value: rule.min,
|
|
1192
1287
|
onChange: (event) => updateParameter(index, { min: event.target.value }), placeholder: '最小值', style: { ...inputStyle, width: 120 } }) : null,
|
|
@@ -1225,8 +1320,9 @@ window.__ModuleLoader__.load({
|
|
|
1225
1320
|
h('option', { value: 'allowlist' }, '指定 IM 用户 ID'))),
|
|
1226
1321
|
senderScope === 'allowlist' ? h('input', { 'aria-label': '可请求的 IM 用户 ID', required: true, value: senderIds,
|
|
1227
1322
|
onChange: (event) => setSenderIds(event.target.value), placeholder: '多个 ID 用逗号分隔', style: inputStyle }) : null,
|
|
1228
|
-
|
|
1229
|
-
h('
|
|
1323
|
+
editingCardId ? h('p', { style: mutedTextStyle }, `有效期:${new Date(editingExpiresAt * 1000).toLocaleString('zh-CN')}(编辑时保持不变)`)
|
|
1324
|
+
: h('label', null, '有效期', h('select', { value: days, onChange: (event) => setDays(Number(event.target.value)), style: inputStyle },
|
|
1325
|
+
h('option', { value: 1 }, '1 天'), h('option', { value: 7 }, '7 天'), h('option', { value: 30 }, '30 天')))),
|
|
1230
1326
|
),
|
|
1231
1327
|
h('aside', { id: 'ant-authorization-review', role: 'region', 'aria-label': '开放前复核', tabIndex: -1,
|
|
1232
1328
|
style: { ...settingsPanelStyle, padding: 14, gap: 10,
|
|
@@ -1242,7 +1338,7 @@ window.__ModuleLoader__.load({
|
|
|
1242
1338
|
['可请求人', requesterSummary],
|
|
1243
1339
|
['执行方', executorSummary],
|
|
1244
1340
|
['读取资料', resourceSummary],
|
|
1245
|
-
['有效期', `${days} 天`],
|
|
1341
|
+
['有效期', editingCardId ? new Date(editingExpiresAt * 1000).toLocaleString('zh-CN') : `${days} 天`],
|
|
1246
1342
|
['回复位置', resultRouteSummary],
|
|
1247
1343
|
].map(([label, value]) => h('div', { key: label, style: { display: 'grid', gridTemplateColumns: '84px minmax(0, 1fr)', gap: 8 } },
|
|
1248
1344
|
h('dt', { style: mutedTextStyle }, label),
|
|
@@ -1250,7 +1346,7 @@ window.__ModuleLoader__.load({
|
|
|
1250
1346
|
cardBlockReason ? h('p', { role: 'status', style: { ...mutedTextStyle,
|
|
1251
1347
|
color: 'var(--dsw-alias-state-warning-primary, var(--dsw-alias-label-secondary))' } }, cardBlockReason) : null,
|
|
1252
1348
|
h('button', { type: 'submit', form: 'ant-capability-create-form', disabled: busy || Boolean(cardBlockReason),
|
|
1253
|
-
style: { ...primaryButtonStyle, width: '100%' } }, '确认并开放能力'))))),
|
|
1349
|
+
style: { ...primaryButtonStyle, width: '100%' } }, editingCardId ? '保存能力修改' : '确认并开放能力'))))),
|
|
1254
1350
|
h('div', { role: 'region', 'aria-label': '本机处理设置', style: { display: activeTab === 'local' ? 'grid' : 'none', gap: 12 } },
|
|
1255
1351
|
h('section', { style: { ...settingsPanelStyle, padding: 14 } },
|
|
1256
1352
|
h('strong', null, 'Gitea 本机凭据'),
|
|
@@ -1816,6 +1912,11 @@ window.__ModuleLoader__.load({
|
|
|
1816
1912
|
const [bundleRelease, setBundleRelease] = useState(null)
|
|
1817
1913
|
const [bundleUpdate, setBundleUpdate] = useState({ state: 'idle', currentVersion: '', message: '' })
|
|
1818
1914
|
const [authorizedModels, setAuthorizedModels] = useState([])
|
|
1915
|
+
const [serverDefaultModel, setServerDefaultModel] = useState(null)
|
|
1916
|
+
const [dshDefaultSelection, setDshDefaultSelection] = useState(null)
|
|
1917
|
+
const [defaultModelDraft, setDefaultModelDraft] = useState('')
|
|
1918
|
+
const [defaultModelBusy, setDefaultModelBusy] = useState(false)
|
|
1919
|
+
const [defaultModelMessage, setDefaultModelMessage] = useState('')
|
|
1819
1920
|
const [lastModelSync, setLastModelSync] = useState(null)
|
|
1820
1921
|
const [serviceCompatibility, setServiceCompatibility] = useState({ state: 'idle', message: '' })
|
|
1821
1922
|
const [pinnedServiceUrl, setPinnedServiceUrl] = useState(null)
|
|
@@ -1889,37 +1990,182 @@ window.__ModuleLoader__.load({
|
|
|
1889
1990
|
return status
|
|
1890
1991
|
}
|
|
1891
1992
|
|
|
1892
|
-
const
|
|
1993
|
+
const modelProvider = (model) => model.protocol === 'anthropic'
|
|
1994
|
+
? model.provider_id === 'anthropic-default' || !model.provider_id ? 'ant-team-anthropic' : `ant-team-anthropic-${model.provider_id}`
|
|
1995
|
+
: model.provider_id === 'openai-default' || !model.provider_id
|
|
1996
|
+
? model.openai_api_mode === 'responses' ? 'ant-team-openai-responses' : 'ant-team-openai-chat-completions'
|
|
1997
|
+
: `ant-team-openai-${model.provider_id}`
|
|
1998
|
+
const modelSelectionKey = (model) => JSON.stringify({ provider_id: model.provider_id || '', protocol: model.protocol, model_id: model.model_id })
|
|
1999
|
+
const defaultPreferenceKey = (rawServiceUrl) => `${DEFAULT_MODEL_INITIALIZED_KEY}:${normalizeServiceUrl(rawServiceUrl)}:${savedDeviceId() || ''}`
|
|
2000
|
+
const readDefaultPreference = (rawServiceUrl) => {
|
|
2001
|
+
try { return JSON.parse(localStorage.getItem(defaultPreferenceKey(rawServiceUrl)) || 'null') } catch { return null }
|
|
2002
|
+
}
|
|
2003
|
+
const saveDefaultPreference = (rawServiceUrl, value) => {
|
|
2004
|
+
try { localStorage.setItem(defaultPreferenceKey(rawServiceUrl), JSON.stringify(value)) } catch {}
|
|
2005
|
+
}
|
|
2006
|
+
const modelForProviderSelection = (selection, models) => {
|
|
2007
|
+
if (!selection) return null
|
|
2008
|
+
const exact = models.find((model) => model.model_id === selection.model && modelProvider(model) === selection.provider)
|
|
2009
|
+
if (exact) return exact
|
|
2010
|
+
const legacyProvider = ['ant-team-openai', 'ant-team-openai-chat-completions', 'ant-team-openai-responses'].includes(selection.provider)
|
|
2011
|
+
? 'openai-default' : selection.provider === 'ant-team-anthropic' ? 'anthropic-default' : null
|
|
2012
|
+
return legacyProvider ? models.find((model) => model.provider_id === legacyProvider && model.model_id === selection.model) : null
|
|
2013
|
+
}
|
|
2014
|
+
const readDshDefaultSelection = async () => {
|
|
2015
|
+
if (typeof remote?.settings?.describe !== 'function') throw new Error('当前 DSH 未提供全局默认模型读取接口。')
|
|
2016
|
+
const response = await remote.settings.describe()
|
|
2017
|
+
if (!response?.ok) throw new Error(response?.error?.message || '无法读取这台 DSH 的全局默认模型。')
|
|
2018
|
+
const value = response.value?.namespaces?.find((entry) => entry.ns === 'agent-default-model')?.value
|
|
2019
|
+
return value && typeof value.provider === 'string' && typeof value.model === 'string'
|
|
2020
|
+
? { provider: value.provider, model: value.model,
|
|
2021
|
+
...(typeof value.reasoningEffort === 'string' ? { reasoningEffort: value.reasoningEffort } : {}) } : null
|
|
2022
|
+
}
|
|
2023
|
+
const writeDshDefaultModel = async (model) => {
|
|
2024
|
+
const response = await remote.settings.mutate('agent-default-model', [
|
|
2025
|
+
{ op: 'set', path: ['provider'], value: modelProvider(model) },
|
|
2026
|
+
{ op: 'set', path: ['model'], value: model.model_id },
|
|
2027
|
+
{ op: 'unset', path: ['reasoningEffort'] },
|
|
2028
|
+
], undefined)
|
|
2029
|
+
if (!response?.ok) throw new Error(response?.error?.message || 'DSH 无法保存全局默认模型。')
|
|
2030
|
+
const selection = { provider: modelProvider(model), model: model.model_id }
|
|
2031
|
+
setDshDefaultSelection(selection)
|
|
2032
|
+
setDefaultModelDraft(modelSelectionKey(model))
|
|
2033
|
+
return selection
|
|
2034
|
+
}
|
|
2035
|
+
const applyUserDefaultModel = async (model) => {
|
|
2036
|
+
if (!model || !authorizedModels.some((item) => modelSelectionKey(item) === modelSelectionKey(model))) {
|
|
2037
|
+
setDefaultModelMessage('请选择当前已获准的模型。')
|
|
2038
|
+
return
|
|
2039
|
+
}
|
|
2040
|
+
setDefaultModelBusy(true)
|
|
2041
|
+
try {
|
|
2042
|
+
const selection = await writeDshDefaultModel(model)
|
|
2043
|
+
const preference = readDefaultPreference(serviceUrl) || { initialized: true, serverDefault: '' }
|
|
2044
|
+
const isServerDefault = serverDefaultModel && modelSelectionKey(model) === modelSelectionKey(serverDefaultModel)
|
|
2045
|
+
if (isServerDefault) preference.serverDefault = modelSelectionKey(serverDefaultModel)
|
|
2046
|
+
preference.initialized = true
|
|
2047
|
+
saveDefaultPreference(serviceUrl, preference)
|
|
2048
|
+
setDshDefaultSelection(selection)
|
|
2049
|
+
setDefaultModelMessage(`已将 ${model.display_name || model.model_id} 设为 DSH 全局默认模型。`)
|
|
2050
|
+
} catch (error) {
|
|
2051
|
+
setDefaultModelMessage(error instanceof Error ? error.message : '保存本机默认模型失败。')
|
|
2052
|
+
} finally { setDefaultModelBusy(false) }
|
|
2053
|
+
}
|
|
2054
|
+
const applyAuthorizedModels = async (models, rawServiceUrl, rawDefaultModel = null) => {
|
|
1893
2055
|
const normalized = normalizeServiceUrl(rawServiceUrl)
|
|
1894
2056
|
if (!normalized || !Array.isArray(models)) throw new Error('ANT 返回的团队模型目录格式无效。')
|
|
1895
2057
|
if (typeof remote?.settings?.mutate !== 'function') throw new Error('当前 DSH 版本未提供模型设置接口。')
|
|
1896
|
-
const grouped =
|
|
1897
|
-
|
|
1898
|
-
|
|
2058
|
+
const grouped = new Map()
|
|
2059
|
+
for (const model of models) {
|
|
2060
|
+
const protocol = model.protocol
|
|
2061
|
+
if (!['openai', 'anthropic'].includes(protocol)) continue
|
|
2062
|
+
const providerId = model.provider_id || (protocol === 'openai' ? 'openai-default' : 'anthropic-default')
|
|
2063
|
+
const key = `${providerId}\u0000${protocol}`
|
|
2064
|
+
let provider = grouped.get(key)
|
|
2065
|
+
if (!provider) {
|
|
2066
|
+
provider = {
|
|
2067
|
+
providerId,
|
|
2068
|
+
providerName: model.provider_name || (protocol === 'openai'
|
|
2069
|
+
? `ANT 团队 · OpenAI · ${model.openai_api_mode === 'responses' ? 'Responses' : 'Chat Completions'}`
|
|
2070
|
+
: 'ANT 团队 · Anthropic'),
|
|
2071
|
+
protocol,
|
|
2072
|
+
apiMode: model.openai_api_mode === 'responses' ? 'responses' : 'chat_completions',
|
|
2073
|
+
legacyEndpoint: typeof model.provider_id !== 'string' || !model.provider_id,
|
|
2074
|
+
models: [],
|
|
2075
|
+
}
|
|
2076
|
+
grouped.set(key, provider)
|
|
2077
|
+
}
|
|
2078
|
+
provider.models.push(model)
|
|
1899
2079
|
}
|
|
2080
|
+
const providers = [...grouped.values()]
|
|
2081
|
+
const routeForProvider = (provider) => provider.providerId === 'openai-default'
|
|
2082
|
+
? provider.apiMode === 'responses' ? 'ant-team-openai-responses' : 'ant-team-openai-chat-completions'
|
|
2083
|
+
: provider.providerId === 'anthropic-default'
|
|
2084
|
+
? 'ant-team-anthropic' : `ant-team-${provider.protocol}-${provider.providerId}`
|
|
2085
|
+
const routes = providers.map(routeForProvider)
|
|
1900
2086
|
const ops = []
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
const
|
|
1904
|
-
if (
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
2087
|
+
let previousRoutes = []
|
|
2088
|
+
try {
|
|
2089
|
+
const parsed = JSON.parse(localStorage.getItem(PROVIDER_ROUTES_STORAGE_KEY) || '[]')
|
|
2090
|
+
if (Array.isArray(parsed)) previousRoutes = parsed.filter((route) => typeof route === 'string')
|
|
2091
|
+
} catch {}
|
|
2092
|
+
const knownRoutes = new Set([...previousRoutes, 'ant-team-openai', 'ant-team-openai-chat-completions',
|
|
2093
|
+
'ant-team-openai-responses', 'ant-team-anthropic'])
|
|
2094
|
+
for (const route of knownRoutes) {
|
|
2095
|
+
if (!routes.includes(route)) ops.push({ op: 'unset', path: ['providers', route] })
|
|
2096
|
+
}
|
|
2097
|
+
for (const provider of providers) {
|
|
2098
|
+
const route = routeForProvider(provider)
|
|
2099
|
+
const api = provider.protocol === 'openai'
|
|
2100
|
+
? provider.apiMode === 'responses' ? 'openai-responses' : 'openai-completions'
|
|
2101
|
+
: 'anthropic-messages'
|
|
2102
|
+
const baseURL = provider.legacyEndpoint
|
|
2103
|
+
? new URL(provider.protocol === 'openai' ? '/model-gateway/openai/v1' : '/model-gateway/anthropic', normalized).toString()
|
|
2104
|
+
: provider.protocol === 'openai'
|
|
2105
|
+
? new URL(`/model-gateway/openai/providers/${encodeURIComponent(provider.providerId)}/v1`, normalized).toString()
|
|
2106
|
+
: new URL(`/model-gateway/anthropic/providers/${encodeURIComponent(provider.providerId)}`, normalized).toString()
|
|
1908
2107
|
ops.push({ op: 'set', path: ['providers', route], value: {
|
|
1909
|
-
displayName:
|
|
2108
|
+
displayName: provider.providerName,
|
|
1910
2109
|
apiKeyEnv: 'ANT_MODEL_API_KEY',
|
|
1911
|
-
api
|
|
1912
|
-
baseURL
|
|
1913
|
-
|
|
1914
|
-
: new URL('/model-gateway/anthropic', normalized).toString(),
|
|
1915
|
-
models: protocolModels.map((model) => ({ id: model.model_id, name: model.display_name })),
|
|
2110
|
+
api,
|
|
2111
|
+
baseURL,
|
|
2112
|
+
models: provider.models.map((model) => ({ id: model.model_id, name: model.display_name })),
|
|
1916
2113
|
} })
|
|
1917
2114
|
}
|
|
1918
2115
|
const settingsWrite = await remote.settings.mutate('llm-pi-ai', ops, undefined)
|
|
1919
2116
|
if (!settingsWrite.ok) throw new Error(settingsWrite.error?.message || 'DSH 无法更新团队模型设置。')
|
|
2117
|
+
try { localStorage.setItem(PROVIDER_ROUTES_STORAGE_KEY, JSON.stringify(routes)) } catch {}
|
|
2118
|
+
const defaultMatch = rawDefaultModel && models.find((model) =>
|
|
2119
|
+
(rawDefaultModel.provider_id ? model.provider_id === rawDefaultModel.provider_id : model.protocol === rawDefaultModel.protocol)
|
|
2120
|
+
&& model.protocol === rawDefaultModel.protocol && model.model_id === rawDefaultModel.model_id)
|
|
2121
|
+
const serverDefault = defaultMatch || null
|
|
2122
|
+
const initialDefault = defaultMatch || (rawDefaultModel == null ? models[0] : null)
|
|
2123
|
+
setServerDefaultModel(serverDefault)
|
|
2124
|
+
const preference = readDefaultPreference(normalized)
|
|
2125
|
+
let statusMessage = ''
|
|
2126
|
+
let selection
|
|
2127
|
+
if (!preference?.initialized && initialDefault) {
|
|
2128
|
+
const previousDefault = await readDshDefaultSelection()
|
|
2129
|
+
selection = await writeDshDefaultModel(initialDefault)
|
|
2130
|
+
saveDefaultPreference(normalized, { initialized: true, serverDefault: modelSelectionKey(serverDefault || initialDefault), previousDefault })
|
|
2131
|
+
statusMessage = serverDefault
|
|
2132
|
+
? '已按团队默认模型设置 DSH 全局默认。该 Profile 的新会话会使用此模型;你之后可在此更改,刷新不会覆盖选择。'
|
|
2133
|
+
: '当前无法使用团队默认模型;已用首个获准模型初始化 DSH 全局默认。请管理员检查默认设置和成员授权。'
|
|
2134
|
+
} else {
|
|
2135
|
+
selection = await readDshDefaultSelection()
|
|
2136
|
+
const currentAuthorized = modelForProviderSelection(selection, models)
|
|
2137
|
+
const baseline = models.find((model) => modelSelectionKey(model) === preference?.serverDefault)
|
|
2138
|
+
const stillUsingBaseline = baseline && currentAuthorized && modelSelectionKey(currentAuthorized) === modelSelectionKey(baseline)
|
|
2139
|
+
if (currentAuthorized && selection?.provider !== modelProvider(currentAuthorized)) {
|
|
2140
|
+
selection = await writeDshDefaultModel(currentAuthorized)
|
|
2141
|
+
}
|
|
2142
|
+
if (serverDefault && stillUsingBaseline && modelSelectionKey(serverDefault) !== preference.serverDefault) {
|
|
2143
|
+
selection = await writeDshDefaultModel(serverDefault)
|
|
2144
|
+
saveDefaultPreference(normalized, { ...preference, initialized: true, serverDefault: modelSelectionKey(serverDefault) })
|
|
2145
|
+
statusMessage = '团队默认模型已更新;你尚未设置个人覆盖,因此已同步到 DSH 全局默认。'
|
|
2146
|
+
} else if (selection?.provider?.startsWith('ant-team-') && !currentAuthorized && models.length) {
|
|
2147
|
+
const fallback = serverDefault || models[0]
|
|
2148
|
+
selection = await writeDshDefaultModel(fallback)
|
|
2149
|
+
saveDefaultPreference(normalized, { ...preference, initialized: true,
|
|
2150
|
+
serverDefault: modelSelectionKey(serverDefault || fallback) })
|
|
2151
|
+
statusMessage = serverDefault
|
|
2152
|
+
? '当前默认模型已失去授权,已恢复为团队默认模型。'
|
|
2153
|
+
: '当前默认模型已失去授权,已改用首个获准模型;请管理员设置团队默认模型。'
|
|
2154
|
+
}
|
|
2155
|
+
const effectiveAuthorized = modelForProviderSelection(selection, models)
|
|
2156
|
+
setDshDefaultSelection(selection)
|
|
2157
|
+
setDefaultModelDraft(effectiveAuthorized ? modelSelectionKey(effectiveAuthorized) : '')
|
|
2158
|
+
if (!serverDefault && !statusMessage) statusMessage = rawDefaultModel
|
|
2159
|
+
? '团队默认模型不在这台设备的获准范围内;请联系管理员调整模型权限。'
|
|
2160
|
+
: '团队默认模型尚未设置;请联系管理员设置,或在下方选择本机可用的默认模型。'
|
|
2161
|
+
else if (!statusMessage) statusMessage = effectiveAuthorized
|
|
2162
|
+
? '已保留你选择的 DSH 全局默认模型。'
|
|
2163
|
+
: '当前 DSH 默认不属于 ANT 获准模型;ANT 对话和任务会使用服务端默认模型。'
|
|
2164
|
+
}
|
|
2165
|
+
setDefaultModelMessage(statusMessage)
|
|
1920
2166
|
try { localStorage.setItem(SERVICE_URL_STORAGE_KEY, normalized) } catch {}
|
|
1921
2167
|
onServiceUrlChange(normalized)
|
|
1922
|
-
return models.length
|
|
2168
|
+
return { count: models.length, defaultSelection: selection }
|
|
1923
2169
|
}
|
|
1924
2170
|
|
|
1925
2171
|
const refreshModels = async () => {
|
|
@@ -1931,19 +2177,21 @@ window.__ModuleLoader__.load({
|
|
|
1931
2177
|
setModelMessage('正在从 ANT 刷新获准模型…')
|
|
1932
2178
|
try {
|
|
1933
2179
|
await requireCompatibleService(serviceUrl, true)
|
|
1934
|
-
const response = await fetch('/api/ant-teams/models', {
|
|
2180
|
+
const response = await fetch('/api/ant-teams/models', {
|
|
2181
|
+
method: 'POST', headers: { 'x-ant-model-provider-config': '1' },
|
|
2182
|
+
})
|
|
1935
2183
|
const result = await readLocalJsonResponse(response)
|
|
1936
2184
|
if (!response.ok) {
|
|
1937
2185
|
if (response.status === 401) setNeedsReconnect(true)
|
|
1938
2186
|
throw new Error(result.message || `刷新失败(${response.status})`)
|
|
1939
2187
|
}
|
|
1940
|
-
const
|
|
2188
|
+
const sync = await applyAuthorizedModels(result.models, result.service_url, result.default_model)
|
|
1941
2189
|
setAuthorizedModels(result.models)
|
|
1942
2190
|
setLastModelSync(new Date())
|
|
1943
2191
|
setCredentialConfigured(true)
|
|
1944
2192
|
setNeedsReconnect(false)
|
|
1945
2193
|
setReauthenticate(false)
|
|
1946
|
-
setModelMessage(`模型目录已刷新,共 ${count} 个获准模型。`)
|
|
2194
|
+
setModelMessage(`模型目录已刷新,共 ${sync.count} 个获准模型。`)
|
|
1947
2195
|
} catch (error) {
|
|
1948
2196
|
setModelMessage(error instanceof Error ? error.message : '刷新模型失败,请稍后重试。')
|
|
1949
2197
|
} finally {
|
|
@@ -2097,15 +2345,55 @@ window.__ModuleLoader__.load({
|
|
|
2097
2345
|
|
|
2098
2346
|
const clearLocalConnection = async () => {
|
|
2099
2347
|
if (typeof remote?.settings?.mutate !== 'function') throw new Error('DSH 未提供模型设置接口。')
|
|
2100
|
-
const
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2348
|
+
const selection = await readDshDefaultSelection()
|
|
2349
|
+
if (selection?.provider?.startsWith('ant-team-')) {
|
|
2350
|
+
const previous = readDefaultPreference(serviceUrl)?.previousDefault
|
|
2351
|
+
const restore = previous && typeof previous.provider === 'string' && typeof previous.model === 'string'
|
|
2352
|
+
&& !previous.provider.startsWith('ant-team-')
|
|
2353
|
+
const operations = restore
|
|
2354
|
+
? [
|
|
2355
|
+
{ op: 'set', path: ['provider'], value: previous.provider },
|
|
2356
|
+
{ op: 'set', path: ['model'], value: previous.model },
|
|
2357
|
+
previous.reasoningEffort
|
|
2358
|
+
? { op: 'set', path: ['reasoningEffort'], value: previous.reasoningEffort }
|
|
2359
|
+
: { op: 'unset', path: ['reasoningEffort'] },
|
|
2360
|
+
]
|
|
2361
|
+
: [
|
|
2362
|
+
{ op: 'unset', path: ['provider'] },
|
|
2363
|
+
{ op: 'unset', path: ['model'] },
|
|
2364
|
+
{ op: 'unset', path: ['reasoningEffort'] },
|
|
2365
|
+
]
|
|
2366
|
+
const reset = await remote.settings.mutate('agent-default-model', [
|
|
2367
|
+
...operations,
|
|
2368
|
+
], undefined)
|
|
2369
|
+
if (!reset.ok) throw new Error(reset.error?.message || 'DSH 全局默认模型未能恢复。')
|
|
2370
|
+
}
|
|
2371
|
+
let managedRoutes = []
|
|
2372
|
+
try {
|
|
2373
|
+
const parsed = JSON.parse(localStorage.getItem(PROVIDER_ROUTES_STORAGE_KEY) || '[]')
|
|
2374
|
+
if (Array.isArray(parsed)) managedRoutes = parsed.filter((route) => typeof route === 'string')
|
|
2375
|
+
} catch {}
|
|
2376
|
+
if (typeof remote?.settings?.describe === 'function') {
|
|
2377
|
+
const described = await remote.settings.describe()
|
|
2378
|
+
if (!described?.ok) throw new Error(described?.error?.message || '无法读取 DSH 中的 ANT 提供方列表。')
|
|
2379
|
+
const providers = described.value?.namespaces?.find((item) => item.ns === 'llm-pi-ai')?.value?.providers
|
|
2380
|
+
for (const [route, provider] of Object.entries(providers || {})) {
|
|
2381
|
+
if (route.startsWith('ant-team-') && provider?.apiKeyEnv === 'ANT_MODEL_API_KEY') managedRoutes.push(route)
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
const routes = new Set([...managedRoutes, 'ant-team-openai-chat-completions', 'ant-team-openai-responses',
|
|
2385
|
+
'ant-team-openai', 'ant-team-anthropic'])
|
|
2386
|
+
const settingsResult = await remote.settings.mutate('llm-pi-ai', [...routes].map((route) => ({
|
|
2387
|
+
op: 'unset', path: ['providers', route],
|
|
2388
|
+
})), undefined)
|
|
2104
2389
|
if (!settingsResult.ok) throw new Error(settingsResult.error?.message || 'ANT 模型设置未能清除。')
|
|
2390
|
+
try { localStorage.removeItem(PROVIDER_ROUTES_STORAGE_KEY) } catch {}
|
|
2105
2391
|
if (typeof remote?.credentials?.unset !== 'function') throw new Error('DSH 未提供凭证删除接口。')
|
|
2106
2392
|
const credentialResult = await remote.credentials.unset('ANT_MODEL_API_KEY')
|
|
2107
2393
|
if (!credentialResult.ok) throw new Error(credentialResult.error?.message || 'ANT 成员凭证未能清除。')
|
|
2108
2394
|
setCredentialConfigured(false)
|
|
2395
|
+
setAuthorizedModels([]); setServerDefaultModel(null); setDshDefaultSelection(null); setDefaultModelDraft('')
|
|
2396
|
+
try { localStorage.removeItem(`${DEFAULT_MODEL_INITIALIZED_KEY}:${normalizeServiceUrl(serviceUrl)}:${savedDeviceId() || ''}`) } catch {}
|
|
2109
2397
|
onConnectionChange?.(null)
|
|
2110
2398
|
setNeedsReconnect(false)
|
|
2111
2399
|
setReauthenticate(false)
|
|
@@ -2266,7 +2554,7 @@ window.__ModuleLoader__.load({
|
|
|
2266
2554
|
localStorage.setItem(SERVICE_URL_STORAGE_KEY, normalized)
|
|
2267
2555
|
const response = await fetch('/api/ant-teams/connect', {
|
|
2268
2556
|
method: 'POST',
|
|
2269
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2557
|
+
headers: { 'Content-Type': 'application/json', 'x-ant-model-provider-config': '1' },
|
|
2270
2558
|
body: JSON.stringify({
|
|
2271
2559
|
service_url: normalized,
|
|
2272
2560
|
email: normalizedEmail,
|
|
@@ -2285,7 +2573,7 @@ window.__ModuleLoader__.load({
|
|
|
2285
2573
|
const credentialWrite = await remote.credentials.set('ANT_MODEL_API_KEY', result.token)
|
|
2286
2574
|
if (!credentialWrite.ok) throw new Error(credentialWrite.error?.message || 'DSH 无法安全保存 ANT 凭证。')
|
|
2287
2575
|
credentialSaved = true
|
|
2288
|
-
await applyAuthorizedModels(result.models, normalized)
|
|
2576
|
+
await applyAuthorizedModels(result.models, normalized, result.default_model)
|
|
2289
2577
|
setAuthorizedModels(result.models)
|
|
2290
2578
|
setLastModelSync(new Date())
|
|
2291
2579
|
setCode('')
|
|
@@ -2316,13 +2604,14 @@ window.__ModuleLoader__.load({
|
|
|
2316
2604
|
const canCheckUpdate = bindingReady && credentialConfigured === true && !needsReconnect
|
|
2317
2605
|
const updateGuideUrl = pinnedServiceUrl
|
|
2318
2606
|
? new URL('/ant-update', pinnedServiceUrl).href : null
|
|
2319
|
-
const
|
|
2607
|
+
const currentDshDefaultModel = modelForProviderSelection(dshDefaultSelection, authorizedModels)
|
|
2608
|
+
const modelRows = authorizedModels.map((model) => h('div', { key: `${model.provider_id || ''}:${model.protocol}:${model.model_id}`, role: 'listitem',
|
|
2320
2609
|
style: { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) minmax(120px, auto) minmax(130px, auto)',
|
|
2321
2610
|
gap: 12, alignItems: 'center', padding: '10px 2px', borderBottom: '1px solid var(--dsw-alias-border-l2)' } },
|
|
2322
|
-
h('strong', { style: { minWidth: 0, overflowWrap: 'anywhere' } }, model.display_name || model.model_id),
|
|
2611
|
+
h('strong', { style: { minWidth: 0, overflowWrap: 'anywhere' } }, `${model.provider_name ? `${model.provider_name} · ` : ''}${model.display_name || model.model_id}`),
|
|
2323
2612
|
h('span', { style: { display: 'inline-flex', justifySelf: 'start', padding: '3px 8px', borderRadius: 999,
|
|
2324
2613
|
backgroundColor: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-secondary)', fontSize: 12 } },
|
|
2325
|
-
|
|
2614
|
+
model.protocol === 'anthropic' ? 'Anthropic Messages' : `OpenAI · ${model.openai_api_mode === 'responses' ? 'Responses' : 'Chat Completions'}`),
|
|
2326
2615
|
h('span', { style: { ...mutedTextStyle, textAlign: 'right' } }, '已同步到 DSH')))
|
|
2327
2616
|
const connectionPanel = h('section', { style: { ...settingsPanelStyle, display: activeTab === 'connection' ? 'grid' : 'none' } },
|
|
2328
2617
|
h('div', null,
|
|
@@ -2386,6 +2675,29 @@ window.__ModuleLoader__.load({
|
|
|
2386
2675
|
backgroundColor: credentialConfigured === true && bindingReady ? 'var(--dsw-alias-state-success-tertiary)' : 'var(--dsw-alias-bg-layer-2)',
|
|
2387
2676
|
fontSize: 12,
|
|
2388
2677
|
} }, credentialConfigured === true ? bindingReady ? '已连接' : '待核对' : credentialConfigured === false ? '未连接' : '检查中')),
|
|
2678
|
+
h('section', { style: { ...infoNoticeStyle, gap: 8 } },
|
|
2679
|
+
h('div', { style: { display: 'flex', justifyContent: 'space-between', gap: 10, flexWrap: 'wrap', alignItems: 'center' } },
|
|
2680
|
+
h('strong', null, 'ANT 默认模型'),
|
|
2681
|
+
h('span', { style: mutedTextStyle }, serverDefaultModel
|
|
2682
|
+
? `团队默认:${serverDefaultModel.display_name || serverDefaultModel.model_id}`
|
|
2683
|
+
: '团队默认尚未设置,或当前成员无权使用')),
|
|
2684
|
+
h('p', { style: mutedTextStyle }, '首次同步时,ANT 会用团队默认初始化 DSH 全局默认模型。它会影响该 Profile 的新会话以及 ANT 对话和任务;之后刷新保留你的个人覆盖。'),
|
|
2685
|
+
h('label', { style: { display: 'grid', gap: 5, maxWidth: 720 } }, 'DSH 全局默认模型',
|
|
2686
|
+
h('select', { 'aria-label': 'DSH 全局默认模型', value: defaultModelDraft,
|
|
2687
|
+
disabled: credentialConfigured !== true || !authorizedModels.length || defaultModelBusy || busy,
|
|
2688
|
+
onChange: (event) => setDefaultModelDraft(event.target.value), style: inputStyle },
|
|
2689
|
+
h('option', { value: '' }, '选择获准模型'),
|
|
2690
|
+
...authorizedModels.map((model) => h('option', { key: modelSelectionKey(model), value: modelSelectionKey(model) },
|
|
2691
|
+
`${model.provider_name ? `${model.provider_name} · ` : ''}${model.display_name || model.model_id} · ${model.protocol === 'anthropic' ? 'Anthropic' : model.openai_api_mode === 'responses' ? 'OpenAI Responses' : 'OpenAI Chat Completions'}`)))),
|
|
2692
|
+
h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' } },
|
|
2693
|
+
h('button', { type: 'button', disabled: defaultModelBusy || !defaultModelDraft,
|
|
2694
|
+
onClick: () => { const chosen = authorizedModels.find((model) => modelSelectionKey(model) === defaultModelDraft); void applyUserDefaultModel(chosen) },
|
|
2695
|
+
style: primaryButtonStyle }, defaultModelBusy ? '保存中…' : '保存为 DSH 全局默认'),
|
|
2696
|
+
h('button', { type: 'button', disabled: defaultModelBusy || !serverDefaultModel
|
|
2697
|
+
|| modelSelectionKey(currentDshDefaultModel || {}) === modelSelectionKey(serverDefaultModel),
|
|
2698
|
+
onClick: () => void applyUserDefaultModel(serverDefaultModel), style: secondaryButtonStyle }, '恢复团队默认'),
|
|
2699
|
+
currentDshDefaultModel ? h('span', { style: mutedTextStyle }, `当前 DSH 默认:${currentDshDefaultModel.display_name || currentDshDefaultModel.model_id}`) : null),
|
|
2700
|
+
defaultModelMessage ? h('p', { role: 'status', style: mutedTextStyle }, defaultModelMessage) : null),
|
|
2389
2701
|
h('div', { role: 'list', 'aria-label': '获准模型', style: { display: 'grid', gap: 0, borderTop: '1px solid var(--dsw-alias-border-l2)' } },
|
|
2390
2702
|
modelRows.length ? modelRows : h('div', { style: { display: 'grid', gap: 8, padding: '12px 0' } },
|
|
2391
2703
|
h('p', { style: mutedTextStyle }, credentialConfigured === true && modelMessage.startsWith('正在') ? modelMessage
|
|
@@ -2394,7 +2706,7 @@ window.__ModuleLoader__.load({
|
|
|
2394
2706
|
credentialConfigured !== true ? h('button', { type: 'button', onClick: () => onSelectTab('connection'),
|
|
2395
2707
|
style: secondaryButtonStyle }, '前往团队连接') : null)),
|
|
2396
2708
|
h('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 10 } },
|
|
2397
|
-
h('button', { type: 'button', disabled: busy || !bindingReady || credentialConfigured !== true,
|
|
2709
|
+
h('button', { type: 'button', disabled: busy || defaultModelBusy || !bindingReady || credentialConfigured !== true,
|
|
2398
2710
|
onClick: () => void refreshModels(), style: primaryButtonStyle }, busy ? '刷新中…' : '刷新团队模型'),
|
|
2399
2711
|
lastModelSync ? h('span', { style: mutedTextStyle }, `最近同步:${lastModelSync.toLocaleString('zh-CN')}`) : null),
|
|
2400
2712
|
modelMessage && !modelMessage.startsWith('正在') ? h('p', { role: 'status', style: mutedTextStyle }, modelMessage) : null)
|
package/conversation.js
CHANGED
|
@@ -114,9 +114,9 @@ async function restrictedSession(ctx, model, root, sessionIds, configure = null,
|
|
|
114
114
|
? undefined : 'ANT 协作对话不能调用本机工具。')
|
|
115
115
|
disposeTools = configure ? configure(agent, sessionId) : []
|
|
116
116
|
await ctx.sessionController.selectModel({ sessionId,
|
|
117
|
-
provider: model
|
|
117
|
+
provider: teamModelProvider(model), model: model.model_id })
|
|
118
118
|
return { sessionId, cwd, disposeTools,
|
|
119
|
-
modelKey: `${model
|
|
119
|
+
modelKey: `${teamModelProvider(model)}\0${model.model_id}` }
|
|
120
120
|
} catch (error) {
|
|
121
121
|
for (const dispose of disposeTools) dispose?.()
|
|
122
122
|
sessionIds.delete(sessionId)
|
|
@@ -166,19 +166,56 @@ async function prompt(ctx, sessionId, content, signal) {
|
|
|
166
166
|
throw new Error('DSH 对话等待超时。')
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
|
|
169
|
+
export function teamModelProvider(model) {
|
|
170
|
+
if (model.protocol === 'anthropic') {
|
|
171
|
+
return !model.provider_id || model.provider_id === 'anthropic-default'
|
|
172
|
+
? 'ant-team-anthropic' : `ant-team-anthropic-${model.provider_id}`
|
|
173
|
+
}
|
|
174
|
+
if (!model.provider_id || model.provider_id === 'openai-default') {
|
|
175
|
+
return model.openai_api_mode === 'responses' ? 'ant-team-openai-responses' : 'ant-team-openai-chat-completions'
|
|
176
|
+
}
|
|
177
|
+
return `ant-team-openai-${model.provider_id}`
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function dshDefaultSelection(ctx) {
|
|
181
|
+
try { return (ctx.agentDefaultModel || ctx.get?.('agentDefaultModel'))?.currentSelection?.() || null }
|
|
182
|
+
catch { return null }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function modelFor(ctx, memberJson, connection) {
|
|
170
186
|
const catalog = await memberJson(connection, '/api/me/models')
|
|
171
|
-
const
|
|
187
|
+
const models = catalog.models?.filter((item) => ['openai', 'anthropic'].includes(item.protocol)) || []
|
|
188
|
+
const selection = dshDefaultSelection(ctx)
|
|
189
|
+
const localDefault = selection && models.find((item) => item.model_id === selection.model
|
|
190
|
+
&& teamModelProvider(item) === selection.provider)
|
|
191
|
+
const serverDefault = catalog.default_model && models.find((item) =>
|
|
192
|
+
(catalog.default_model.provider_id ? item.provider_id === catalog.default_model.provider_id : item.protocol === catalog.default_model.protocol)
|
|
193
|
+
&& item.protocol === catalog.default_model.protocol && item.model_id === catalog.default_model.model_id)
|
|
194
|
+
const model = localDefault || serverDefault || (!Object.hasOwn(catalog, 'default_model') ? models[0] : null)
|
|
195
|
+
if (!model && models.length) throw new Error('当前没有可用的 ANT 默认模型;请在插件“团队模型”中选择本机默认,或联系管理员检查授权。')
|
|
172
196
|
if (!model) throw new Error('当前没有可用的团队模型。')
|
|
173
197
|
return model
|
|
174
198
|
}
|
|
175
199
|
|
|
200
|
+
export function cardReviewers(card) {
|
|
201
|
+
const selected = Array.isArray(card?.reviewers)
|
|
202
|
+
? [...new Set(card.reviewers.filter((runner) => ['dsh', 'codex', 'claude-code'].includes(runner)))] : []
|
|
203
|
+
return selected.length ? selected : card?.runner ? [card.runner] : []
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function isCrossReviewRequest(text) {
|
|
207
|
+
const value = (typeof text === 'string' ? text : '').normalize('NFKC').toLowerCase()
|
|
208
|
+
if (/(?:不要|别|不用|不需要|不做|取消)\s*(?:进行|做)?\s*(?:交叉\s*(?:review|审查|评审)|cross[\s-]*review)/iu.test(value)
|
|
209
|
+
|| /\b(?:do\s+not|don't|without|no\s+need\s+to)\s+cross[\s-]*review\b/iu.test(value)) return false
|
|
210
|
+
return /(?:交叉\s*(?:review|审查|评审)|cross[\s-]*review)/iu.test(value)
|
|
211
|
+
}
|
|
212
|
+
|
|
176
213
|
export function hasRequiredCodexReviewPr(card, parameters) {
|
|
177
|
-
return card
|
|
214
|
+
return cardReviewers(card).includes('codex') && hasRequiredNativeReviewPr(card, parameters)
|
|
178
215
|
}
|
|
179
216
|
|
|
180
217
|
export function hasRequiredNativeReviewPr(card, parameters) {
|
|
181
|
-
if (!['codex', 'claude-code'].includes(
|
|
218
|
+
if (!cardReviewers(card).some((runner) => ['codex', 'claude-code'].includes(runner))
|
|
182
219
|
|| !Array.isArray(card.parameters) || card.parameters.length !== 1) return false
|
|
183
220
|
const [rule] = card.parameters
|
|
184
221
|
const value = parameters?.[rule.name]
|
|
@@ -328,7 +365,7 @@ function chatGate(maxActive = 4) {
|
|
|
328
365
|
}
|
|
329
366
|
|
|
330
367
|
export function createConversationEngine({ ctx, ledger, readConnection, memberJson, fetchPr,
|
|
331
|
-
preparePrWorkspace, runNativeReview,
|
|
368
|
+
preparePrWorkspace, runCodexReview = runCodexReadOnly, runNativeReview, runDshReview,
|
|
332
369
|
logger, sessionIds, signal, workspaceRoot, gate }) {
|
|
333
370
|
let settings = { context_messages: 10, history_max_records: 100, max_concurrency: 3 }
|
|
334
371
|
let cardsByFingerprint = new Map()
|
|
@@ -430,11 +467,11 @@ export function createConversationEngine({ ctx, ledger, readConnection, memberJs
|
|
|
430
467
|
async function scopeSession(scope, model) {
|
|
431
468
|
const live = scopeSessions.get(scope)
|
|
432
469
|
if (live) {
|
|
433
|
-
const modelKey = `${model
|
|
470
|
+
const modelKey = `${teamModelProvider(model)}\0${model.model_id}`
|
|
434
471
|
if (live.modelKey !== modelKey) {
|
|
435
472
|
try {
|
|
436
473
|
await ctx.sessionController.selectModel({ sessionId: live.sessionId,
|
|
437
|
-
provider: model
|
|
474
|
+
provider: teamModelProvider(model),
|
|
438
475
|
model: model.model_id })
|
|
439
476
|
live.modelKey = modelKey
|
|
440
477
|
} catch (error) {
|
|
@@ -457,9 +494,10 @@ export function createConversationEngine({ ctx, ledger, readConnection, memberJs
|
|
|
457
494
|
}
|
|
458
495
|
try {
|
|
459
496
|
register('ant_capabilities',
|
|
460
|
-
'
|
|
497
|
+
'查看当前请求人在这段对话里可申请的任务能力。普通聊天无需调用。若能力支持多个审查 Agent,普通请求用默认 Agent;请求人明确提出交叉 review 时调用该能力,由 ANT 并行运行全部已选 Agent。', {},
|
|
461
498
|
withTurn(async (current) => current.cards.map((card) => ({ id: card.id,
|
|
462
499
|
title: card.title, intent: card.intent, examples: card.examples,
|
|
500
|
+
reviewers: cardReviewers(card), cross_review_enabled: cardReviewers(card).length > 1,
|
|
463
501
|
parameters: card.parameters.map((rule) => ({ name: rule.name, description: rule.description,
|
|
464
502
|
kind: rule.kind, required: rule.required, choices: rule.choices,
|
|
465
503
|
min: rule.min, max: rule.max, repository: rule.repository })) }))));
|
|
@@ -757,7 +795,7 @@ export function createConversationEngine({ ctx, ledger, readConnection, memberJs
|
|
|
757
795
|
let answer = '我暂时没法继续这段对话,请稍后再试。'
|
|
758
796
|
let session = null
|
|
759
797
|
try {
|
|
760
|
-
const model = await modelFor(memberJson, connection)
|
|
798
|
+
const model = await modelFor(ctx, memberJson, connection)
|
|
761
799
|
let history
|
|
762
800
|
try { history = await firstHistory(ctx, event, settings, signal) }
|
|
763
801
|
catch (error) {
|
|
@@ -830,7 +868,13 @@ export function createConversationEngine({ ctx, ledger, readConnection, memberJs
|
|
|
830
868
|
const checked = await memberJson(connection, '/api/me/im-delegations/cards/authorize',
|
|
831
869
|
{ method: 'POST', body: JSON.stringify(entry.authorization) })
|
|
832
870
|
if (checked.decision !== 'allowed' || checked.card?.id !== entry.card.id) throw new Error('任务授权已失效。')
|
|
833
|
-
const
|
|
871
|
+
const configuredReviewers = cardReviewers(checked.card)
|
|
872
|
+
const crossReview = isCrossReviewRequest(entry.request_text)
|
|
873
|
+
if (crossReview && configuredReviewers.length < 2) {
|
|
874
|
+
throw new Error('这项能力只配置了一位审查 Agent,无法交叉 review;请所有者至少勾选两位。')
|
|
875
|
+
}
|
|
876
|
+
const taskReviewers = crossReview ? configuredReviewers : [checked.card.runner]
|
|
877
|
+
const nativeReview = taskReviewers.some((runner) => ['codex', 'claude-code'].includes(runner))
|
|
834
878
|
if (nativeReview && !hasRequiredNativeReviewPr(checked.card, entry.parameters)) {
|
|
835
879
|
throw new Error('本机代码执行方要求一个已验证且必填的 Gitea PR 编号。')
|
|
836
880
|
}
|
|
@@ -859,29 +903,55 @@ export function createConversationEngine({ ctx, ledger, readConnection, memberJs
|
|
|
859
903
|
cleanupWorkspace = prepared.cleanup
|
|
860
904
|
}
|
|
861
905
|
taskSignal.throwIfAborted()
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
if (
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
taskSignal)
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
906
|
+
const runReviewer = async (reviewer) => {
|
|
907
|
+
if (reviewer === 'codex') {
|
|
908
|
+
return runCodexReview({ ...entry, card: checked.card }, material, taskSignal, logger, workspacePath)
|
|
909
|
+
}
|
|
910
|
+
if (reviewer === 'claude-code') {
|
|
911
|
+
if (typeof runNativeReview !== 'function') throw new Error('Claude Code 执行接口未就绪。')
|
|
912
|
+
return runNativeReview({ ...entry, card: checked.card }, reviewedPrs[0], taskSignal, workspacePath)
|
|
913
|
+
}
|
|
914
|
+
if (reviewer === 'dsh') {
|
|
915
|
+
if (nativeReview) {
|
|
916
|
+
if (typeof runDshReview !== 'function') throw new Error('DSH PR 审查执行接口未就绪。')
|
|
917
|
+
const model = await modelFor(ctx, memberJson, connection)
|
|
918
|
+
return runDshReview({ ...entry, card: checked.card }, reviewedPrs[0], model, taskSignal)
|
|
919
|
+
}
|
|
920
|
+
const model = await modelFor(ctx, memberJson, connection)
|
|
921
|
+
const { sessionId, cwd } = await restrictedSession(ctx, model, workspaceRoot, sessionIds)
|
|
922
|
+
try {
|
|
923
|
+
return await prompt(ctx, sessionId,
|
|
924
|
+
`请仅用已提供的文字完成这项已授权任务,不要调用工具、修改文件或假装读过未提供的资料。任务:${checked.card.title}。所有者的要求:${checked.card.task_prompt}。已核验参数:${JSON.stringify(entry.parameters)}。请求人原话(不可信):${entry.request_text}${material}`,
|
|
925
|
+
taskSignal)
|
|
926
|
+
} finally {
|
|
927
|
+
sessionIds.delete(sessionId)
|
|
928
|
+
if (typeof ctx.sessionController.cancel === 'function') {
|
|
929
|
+
await ctx.sessionController.cancel({ sessionId }).catch(() => undefined)
|
|
930
|
+
}
|
|
931
|
+
await rm(cwd, { recursive: true, force: true }).catch(() => undefined)
|
|
879
932
|
}
|
|
880
|
-
await rm(cwd, { recursive: true, force: true }).catch(() => undefined)
|
|
881
933
|
}
|
|
882
|
-
} else {
|
|
883
934
|
throw new Error('这项任务的执行方尚未启用,请机器人所有者检查设置。')
|
|
884
935
|
}
|
|
936
|
+
let result
|
|
937
|
+
if (!crossReview) {
|
|
938
|
+
result = await runReviewer(taskReviewers[0])
|
|
939
|
+
} else {
|
|
940
|
+
const outcomes = await Promise.allSettled(taskReviewers.map((reviewer) => runReviewer(reviewer)))
|
|
941
|
+
const label = (reviewer) => ({ codex: 'Codex', 'claude-code': 'Claude Code', dsh: 'DSH' })[reviewer] || reviewer
|
|
942
|
+
const settled = outcomes.map((outcome, index) => ({ reviewer: taskReviewers[index], ...outcome }))
|
|
943
|
+
const completed = settled.filter((outcome) => outcome.status === 'fulfilled')
|
|
944
|
+
const failed = settled.filter((outcome) => outcome.status === 'rejected')
|
|
945
|
+
if (!completed.length) {
|
|
946
|
+
throw new Error(`交叉 review 的所有 Agent 均未完成:${failed.map((item) => `${label(item.reviewer)}:${item.reason?.message || '执行失败'}`).join(';')}`)
|
|
947
|
+
}
|
|
948
|
+
const perAgentLimit = Math.max(400, Math.floor((MAX_REPLY_LENGTH - 700) / taskReviewers.length))
|
|
949
|
+
result = `交叉 review:${taskReviewers.map(label).join('、')}并行审查(同一 PR Head ${reviewedPrs[0].sha.slice(0, 8)})\n\n`
|
|
950
|
+
+ completed.map((item) => `【${label(item.reviewer)}】\n${String(item.value).slice(0, perAgentLimit)}`).join('\n\n')
|
|
951
|
+
if (failed.length) {
|
|
952
|
+
result += `\n\n【未完成】\n${failed.map((item) => `${label(item.reviewer)}:${item.reason?.message || '执行失败'}`).join('\n')}`
|
|
953
|
+
}
|
|
954
|
+
}
|
|
885
955
|
taskSignal.throwIfAborted()
|
|
886
956
|
const recheck = await memberJson(connection, '/api/me/im-delegations/cards/authorize',
|
|
887
957
|
{ method: 'POST', body: JSON.stringify(entry.authorization) })
|
package/delegation.js
CHANGED
|
@@ -4,7 +4,7 @@ import { homedir } from 'node:os'
|
|
|
4
4
|
import { dirname, join, resolve } from 'node:path'
|
|
5
5
|
import { spawn } from 'node:child_process'
|
|
6
6
|
import { CONVERSATION_TOOL_NAMES, conversationKey, createConversationEngine,
|
|
7
|
-
isHelpRequest, requestsPrivateReply } from './conversation.js'
|
|
7
|
+
isHelpRequest, modelFor, requestsPrivateReply, teamModelProvider } from './conversation.js'
|
|
8
8
|
|
|
9
9
|
const MAX_DIFF_BYTES = 240_000
|
|
10
10
|
const MAX_GITEA_JSON_BYTES = 64_000
|
|
@@ -815,8 +815,8 @@ export async function cleanupPrWorkspaceCaches(workspaceRoot, cards, entries, ge
|
|
|
815
815
|
})
|
|
816
816
|
}
|
|
817
817
|
|
|
818
|
-
function reviewPrompt(grant, material, task = null) {
|
|
819
|
-
const snapshot =
|
|
818
|
+
function reviewPrompt(grant, material, task = null, snapshotAvailable = Boolean(task?.card)) {
|
|
819
|
+
const snapshot = snapshotAvailable
|
|
820
820
|
? '本次 PR 的完整代码快照位于当前任务工作目录;可以只读查看目录内相关源码、测试和配置。'
|
|
821
821
|
: '当前工作目录不包含完整仓库;只能根据下列已提供的 PR 差异审查,并说明上下文局限。'
|
|
822
822
|
const ownerInstructions = task?.card?.task_prompt
|
|
@@ -899,7 +899,8 @@ export async function runNativePrReview(ctx, executor, grant, material, sessionI
|
|
|
899
899
|
}
|
|
900
900
|
}
|
|
901
901
|
|
|
902
|
-
export async function runDshPrReview(ctx, grant, material, model, sessionIds, signal,
|
|
902
|
+
export async function runDshPrReview(ctx, grant, material, model, sessionIds, signal,
|
|
903
|
+
workspaceRoot = dshRoot(), task = null) {
|
|
903
904
|
signal.throwIfAborted()
|
|
904
905
|
const workspace = join(workspaceRoot, 'ant-delegation', 'workspaces', randomUUID())
|
|
905
906
|
await mkdir(workspace, { recursive: true, mode: 0o700 })
|
|
@@ -919,8 +920,9 @@ export async function runDshPrReview(ctx, grant, material, model, sessionIds, si
|
|
|
919
920
|
}
|
|
920
921
|
agent.ctx.tools.restrict({ allow: [] })
|
|
921
922
|
agent.ctx.tools.guard(() => 'ANT 委托审查不能调用 DSH 工具。')
|
|
922
|
-
await ctx.sessionController.selectModel({ sessionId, provider: model
|
|
923
|
-
await ctx.sessionController.prompt({ sessionId, requestId: randomUUID(), mode: 'queue',
|
|
923
|
+
await ctx.sessionController.selectModel({ sessionId, provider: teamModelProvider(model), model: model.model_id })
|
|
924
|
+
await ctx.sessionController.prompt({ sessionId, requestId: randomUUID(), mode: 'queue',
|
|
925
|
+
content: [{ type: 'text', text: reviewPrompt(grant, material, task, false) }] }, signal)
|
|
924
926
|
const deadline = Date.now() + MODEL_TIMEOUT_MS
|
|
925
927
|
while (Date.now() < deadline) {
|
|
926
928
|
if (signal.aborted) throw new Error('任务已取消。')
|
|
@@ -1075,6 +1077,8 @@ export function startDelegationHost(ctx, readConnection, { ledger = new Delegati
|
|
|
1075
1077
|
fetchPr: fetchGiteaPr,
|
|
1076
1078
|
preparePrWorkspace: (card, taskId, pr, taskSignal) =>
|
|
1077
1079
|
preparePrWorkspace(ctx, card, taskId, pr, taskSignal, workspaceRoot, readSettings),
|
|
1080
|
+
runDshReview: (entry, pr, model, taskSignal) => runDshPrReview(ctx, pr.target,
|
|
1081
|
+
{ title: pr.title, sha: pr.sha, diff: pr.diff }, model, sessionIds, taskSignal, workspaceRoot, entry),
|
|
1078
1082
|
runNativeReview: (entry, pr, taskSignal, workspacePath) => runNativePrReview(ctx, 'claude-code', {
|
|
1079
1083
|
...pr.target,
|
|
1080
1084
|
}, { title: pr.title, sha: pr.sha, diff: pr.diff }, sessionIds, taskSignal, workspaceRoot,
|
|
@@ -1113,9 +1117,7 @@ export function startDelegationHost(ctx, readConnection, { ledger = new Delegati
|
|
|
1113
1117
|
const credential = await ctx.credentials.resolve('ANT_GITEA_API_TOKEN')
|
|
1114
1118
|
const material = await fetchGiteaPr(auth.grant, credential?.value)
|
|
1115
1119
|
if (executor === 'dsh') {
|
|
1116
|
-
const
|
|
1117
|
-
const model = models.models?.find((item) => item.protocol === 'openai' || item.protocol === 'anthropic')
|
|
1118
|
-
if (!model) throw new Error('当前没有可用的团队模型。')
|
|
1120
|
+
const model = await modelFor(ctx, memberJson, connection)
|
|
1119
1121
|
reply = await runDshPrReview(ctx, auth.grant, material, model, sessionIds, taskSignal, workspaceRoot)
|
|
1120
1122
|
} else {
|
|
1121
1123
|
reply = await runNativePrReview(ctx, executor, auth.grant, material, sessionIds, taskSignal, workspaceRoot, logger)
|
|
@@ -1396,7 +1398,12 @@ export function startDelegationHost(ctx, readConnection, { ledger = new Delegati
|
|
|
1396
1398
|
approval = { code, owner_id: event.actor.id, group_id: groupId, card_id: matches[0].id,
|
|
1397
1399
|
expires_at: Date.now() + 300_000 }
|
|
1398
1400
|
kind = 'group_card_approval_offer'
|
|
1399
|
-
const
|
|
1401
|
+
const reviewers = Array.isArray(matches[0].reviewers) && matches[0].reviewers.length
|
|
1402
|
+
? matches[0].reviewers : [matches[0].runner]
|
|
1403
|
+
const reviewerLabel = (id) => ({ dsh: 'DSH', codex: 'Codex', 'claude-code': 'Claude Code' })[id] || id
|
|
1404
|
+
const runnerDescription = reviewers.length > 1
|
|
1405
|
+
? `普通请求默认由${reviewerLabel(matches[0].runner)}执行;明确说“交叉 review”时并行启动${reviewers.map(reviewerLabel).join('、')}`
|
|
1406
|
+
: matches[0].runner === 'codex' ? '这台电脑的 Codex 专用只读差异审查'
|
|
1400
1407
|
: matches[0].runner === 'claude-code' ? '这台 DSH 已注册的 Claude Code 子代理试用'
|
|
1401
1408
|
: '受限 DSH'
|
|
1402
1409
|
reply = `确认后,本群任何成员可请求“${matches[0].title}”;执行方为${runnerDescription},参数规则沿用私聊任务,结果公开回复原群/原话题,授权最长 7 天。请在 5 分钟内 @我回复“确认启用 ${code}”。`
|
|
@@ -1425,7 +1432,7 @@ export function startDelegationHost(ctx, readConnection, { ledger = new Delegati
|
|
|
1425
1432
|
method: 'POST', body: JSON.stringify({ bot_id: event.botId, bot_fingerprint: event.fingerprint,
|
|
1426
1433
|
title: source.title, intent: source.intent, examples: source.examples,
|
|
1427
1434
|
parameters: source.parameters, task_prompt: source.task_prompt,
|
|
1428
|
-
runner: source.runner, workspace: null, sender_scope: 'any', sender_ids: [],
|
|
1435
|
+
runner: source.runner, reviewers: source.reviewers, workspace: null, sender_scope: 'any', sender_ids: [],
|
|
1429
1436
|
conversation_kind: 'group', conversation_id: groupId,
|
|
1430
1437
|
expires_at: Math.min(source.expires_at, Math.floor(Date.now() / 1000) + 7 * 86400) }),
|
|
1431
1438
|
})
|
package/index.js
CHANGED
|
@@ -69,8 +69,10 @@ function modelServiceBinding(settings) {
|
|
|
69
69
|
let url
|
|
70
70
|
try { url = new URL(provider.baseURL) } catch { return { configured, serviceUrl: null } }
|
|
71
71
|
const localHttp = url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)
|
|
72
|
-
|
|
73
|
-
||
|
|
72
|
+
const knownRoute = ['/model-gateway/openai/v1', '/model-gateway/anthropic'].includes(url.pathname)
|
|
73
|
+
|| /^\/model-gateway\/openai\/providers\/[A-Za-z0-9-]{1,64}\/v1$/u.test(url.pathname)
|
|
74
|
+
|| /^\/model-gateway\/anthropic\/providers\/[A-Za-z0-9-]{1,64}$/u.test(url.pathname)
|
|
75
|
+
if ((url.protocol !== 'https:' && !localHttp) || url.username || url.password || url.search || url.hash || !knownRoute) {
|
|
74
76
|
return { configured, serviceUrl: null }
|
|
75
77
|
}
|
|
76
78
|
origins.add(url.origin)
|
|
@@ -178,7 +180,11 @@ async function fetchMemberModels(request, credentials, settings) {
|
|
|
178
180
|
}
|
|
179
181
|
if (!session.ok) return jsonResponse(502, { message: '无法激活 ANT 模型会话。' })
|
|
180
182
|
const upstream = await fetch(new URL('/api/me/models', serviceUrl), {
|
|
181
|
-
headers: {
|
|
183
|
+
headers: {
|
|
184
|
+
authorization: `Bearer ${token}`,
|
|
185
|
+
accept: 'application/json',
|
|
186
|
+
...(request.headers.get('x-ant-model-provider-config') === '1' ? { 'x-ant-model-provider-config': '1' } : {}),
|
|
187
|
+
},
|
|
182
188
|
redirect: 'manual',
|
|
183
189
|
signal: AbortSignal.timeout(10_000),
|
|
184
190
|
})
|
|
@@ -193,7 +199,7 @@ async function fetchMemberModels(request, credentials, settings) {
|
|
|
193
199
|
if (!Array.isArray(result?.models) || normalizeServiceUrl(result.service_url) !== serviceUrl) {
|
|
194
200
|
return jsonResponse(502, { message: 'ANT 团队模型目录与已连接服务不匹配。' })
|
|
195
201
|
}
|
|
196
|
-
return jsonResponse(200, { service_url: serviceUrl, models: result.models })
|
|
202
|
+
return jsonResponse(200, { service_url: serviceUrl, models: result.models, default_model: result.default_model ?? null })
|
|
197
203
|
} catch {
|
|
198
204
|
return jsonResponse(502, { message: '无法连接 ANT 团队服务' })
|
|
199
205
|
}
|
|
@@ -362,7 +368,7 @@ async function fetchDelegationStatus(request, credentials, settings) {
|
|
|
362
368
|
}
|
|
363
369
|
|
|
364
370
|
async function fetchDelegationCards(request, ctx, credentials, settings) {
|
|
365
|
-
if (!['GET', 'POST', 'DELETE'].includes(request.method)) return jsonResponse(405, { message: '请求方式不支持。' })
|
|
371
|
+
if (!['GET', 'POST', 'PUT', 'DELETE'].includes(request.method)) return jsonResponse(405, { message: '请求方式不支持。' })
|
|
366
372
|
let connection
|
|
367
373
|
try { connection = await readMemberConnection(credentials, settings) } catch {
|
|
368
374
|
return jsonResponse(500, { message: '无法读取 ANT 连接。' })
|
|
@@ -376,7 +382,7 @@ async function fetchDelegationCards(request, ctx, credentials, settings) {
|
|
|
376
382
|
body = JSON.parse(raw)
|
|
377
383
|
} catch { return jsonResponse(400, { message: '任务设置格式无效。' }) }
|
|
378
384
|
}
|
|
379
|
-
if (request.method
|
|
385
|
+
if (['POST', 'PUT'].includes(request.method)) {
|
|
380
386
|
const service = dshImService(ctx)
|
|
381
387
|
if (!service?.describeBot) return jsonResponse(503, { message: '请先安装并启用 DSH-IM。' })
|
|
382
388
|
try {
|
|
@@ -390,18 +396,28 @@ async function fetchDelegationCards(request, ctx, credentials, settings) {
|
|
|
390
396
|
if (request.method === 'DELETE' && (typeof body?.id !== 'string' || !/^[0-9a-f-]{36}$/iu.test(body.id))) {
|
|
391
397
|
return jsonResponse(400, { message: '任务编号无效。' })
|
|
392
398
|
}
|
|
399
|
+
if ((request.method === 'POST' && body?.id !== undefined)
|
|
400
|
+
|| (request.method === 'PUT' && (typeof body?.id !== 'string' || !/^[0-9a-f-]{36}$/iu.test(body.id)))) {
|
|
401
|
+
return jsonResponse(400, { message: '任务编号无效。' })
|
|
402
|
+
}
|
|
393
403
|
const path = request.method === 'DELETE'
|
|
394
404
|
? `/api/me/im-delegations/cards/${encodeURIComponent(body.id)}` : '/api/me/im-delegations/cards'
|
|
405
|
+
const payload = body
|
|
395
406
|
try {
|
|
396
407
|
const response = await fetch(new URL(path, connection.serviceUrl), {
|
|
397
408
|
method: request.method,
|
|
398
409
|
headers: { authorization: `Bearer ${connection.token}`, accept: 'application/json',
|
|
399
410
|
...(request.method !== 'GET' ? { origin: new URL(connection.serviceUrl).origin } : {}),
|
|
400
|
-
...(request.method
|
|
401
|
-
...(request.method
|
|
411
|
+
...(['POST', 'PUT'].includes(request.method) ? { 'content-type': 'application/json' } : {}) },
|
|
412
|
+
...(['POST', 'PUT'].includes(request.method) ? { body: JSON.stringify(payload) } : {}),
|
|
402
413
|
redirect: 'manual', signal: AbortSignal.timeout(12_000),
|
|
403
414
|
})
|
|
404
|
-
if (response.status === 404 &&
|
|
415
|
+
if (response.status === 404 && ['GET', 'POST'].includes(request.method)) {
|
|
416
|
+
return jsonResponse(426, { message: '团队服务端尚未支持自定义任务,请联系管理员升级。' })
|
|
417
|
+
}
|
|
418
|
+
if (response.status === 405 && request.method === 'PUT') {
|
|
419
|
+
return jsonResponse(426, { message: '团队服务端尚未支持能力编辑,请联系管理员升级 ANT 服务端。' })
|
|
420
|
+
}
|
|
405
421
|
if (response.status === 204) return new Response(null, { status: 204 })
|
|
406
422
|
const text = await readLimitedBody(response, request.method === 'GET' ? 8 * 1024 * 1024 : MAX_MODEL_RESPONSE_BYTES)
|
|
407
423
|
if (text === null) return jsonResponse(502, { message: '团队服务响应过大。' })
|
|
@@ -855,6 +871,8 @@ async function fetchAuthRequest(request, endpoint, credentials, settings) {
|
|
|
855
871
|
accept: 'application/json',
|
|
856
872
|
'content-type': 'application/json',
|
|
857
873
|
origin: serviceUrl,
|
|
874
|
+
...(endpoint === 'dsh-connect' && request.headers.get('x-ant-model-provider-config') === '1'
|
|
875
|
+
? { 'x-ant-model-provider-config': '1' } : {}),
|
|
858
876
|
},
|
|
859
877
|
body: JSON.stringify(payload),
|
|
860
878
|
redirect: 'manual',
|
|
@@ -962,7 +980,7 @@ export function apply(ctx) {
|
|
|
962
980
|
fetch: (request) => fetchDelegationStatus(request, connectionCtx.credentials, connectionCtx.settings),
|
|
963
981
|
}), 'ant-teams: local delegated task status')
|
|
964
982
|
connectionCtx.effect(() => connectionCtx.connection.fetch.register({
|
|
965
|
-
path: DELEGATION_CARDS_PATH, methods: ['GET', 'POST', 'DELETE'], requestBody: 'buffered',
|
|
983
|
+
path: DELEGATION_CARDS_PATH, methods: ['GET', 'POST', 'PUT', 'DELETE'], requestBody: 'buffered',
|
|
966
984
|
fetch: (request) => fetchDelegationCards(request, connectionCtx, connectionCtx.credentials, connectionCtx.settings),
|
|
967
985
|
}), 'ant-teams: member task cards')
|
|
968
986
|
connectionCtx.effect(() => connectionCtx.connection.fetch.register({
|