@ai-wendao/ant 0.4.10 → 0.4.11
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 +328 -70
- package/conversation.js +94 -31
- package/delegation.js +18 -11
- package/index.js +17 -7
- 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
|
@@ -7,6 +7,7 @@ window.__ModuleLoader__.load({
|
|
|
7
7
|
const h = createElement
|
|
8
8
|
const SERVICE_URL_STORAGE_KEY = 'ant-team-service-url'
|
|
9
9
|
const DEVICE_ID_STORAGE_KEY = 'ant-team-device-id'
|
|
10
|
+
const DEFAULT_MODEL_INITIALIZED_KEY = 'ant-team-default-model-initialized'
|
|
10
11
|
const ANT_BUNDLE_PACKAGE_NAME = '@ai-wendao/ant'
|
|
11
12
|
const ANT_API_PROTOCOL_VERSIONS = [1]
|
|
12
13
|
const PLUGIN_INSTALL_RECORD_PREFIX = 'ant-reviewed-plugin-install:'
|
|
@@ -473,12 +474,15 @@ window.__ModuleLoader__.load({
|
|
|
473
474
|
const [examples, setExamples] = useState('')
|
|
474
475
|
const [taskPrompt, setTaskPrompt] = useState('')
|
|
475
476
|
const [runner, setRunner] = useState('dsh')
|
|
477
|
+
const [reviewers, setReviewers] = useState(['dsh'])
|
|
476
478
|
const [parameters, setParameters] = useState([])
|
|
477
479
|
const [giteaToken, setGiteaToken] = useState('')
|
|
478
480
|
const [tokenConfigured, setTokenConfigured] = useState(false)
|
|
479
481
|
const [senderScope, setSenderScope] = useState('any')
|
|
480
482
|
const [senderIds, setSenderIds] = useState('')
|
|
481
483
|
const [days, setDays] = useState(7)
|
|
484
|
+
const [editingCardId, setEditingCardId] = useState(null)
|
|
485
|
+
const [editingExpiresAt, setEditingExpiresAt] = useState(null)
|
|
482
486
|
const [busy, setBusy] = useState(false)
|
|
483
487
|
const [loading, setLoading] = useState(true)
|
|
484
488
|
const [botLoadError, setBotLoadError] = useState(false)
|
|
@@ -554,17 +558,28 @@ window.__ModuleLoader__.load({
|
|
|
554
558
|
const enrolledCount = enrolled.filter((item) => item.device_bound_to_current
|
|
555
559
|
&& connectedBots.some((bot) => bot.fingerprint === item.bot_fingerprint)).length
|
|
556
560
|
const selectedTaskCount = activeCards.filter((card) => card.bot_fingerprint === selectedBot?.fingerprint).length
|
|
557
|
-
const
|
|
561
|
+
const nativeReviewerSelected = reviewers.some((item) => ['codex', 'claude-code'].includes(item))
|
|
562
|
+
const hasPrParameter = parameters.some((item) => item.kind === 'gitea_pr')
|
|
558
563
|
const codexAvailable = executors.some((item) => item.id === 'codex-readonly' && item.available)
|
|
559
564
|
const claudeCodeAvailable = executors.some((item) => item.id === 'claude-code' && item.available)
|
|
565
|
+
const dshAvailable = executors.some((item) => item.id === 'dsh' && item.available)
|
|
560
566
|
const nativePrUnavailableReason = executors.find((item) => item.reason)?.reason || null
|
|
567
|
+
const reviewerLabel = (id) => ({ dsh: 'DSH', codex: 'Codex', 'claude-code': 'Claude Code' })[id] || id
|
|
568
|
+
const cardReviewerSummary = (card) => {
|
|
569
|
+
const selected = Array.isArray(card.reviewers) && card.reviewers.length ? card.reviewers : [card.runner]
|
|
570
|
+
return selected.length > 1
|
|
571
|
+
? `交叉可用:${selected.map(reviewerLabel).join('、')};默认 ${reviewerLabel(card.runner)}`
|
|
572
|
+
: card.runner === 'codex' ? 'Codex PR 快照审查'
|
|
573
|
+
: card.runner === 'claude-code' ? 'Claude Code PR 快照审查'
|
|
574
|
+
: 'DSH 文字处理'
|
|
575
|
+
}
|
|
561
576
|
const requesterSummary = senderScope === 'allowlist'
|
|
562
577
|
? `仅指定成员:${senderIds.trim() || '尚未填写成员 ID'}`
|
|
563
578
|
: conversationKind === 'group' ? `群 ${groupId || '尚未选择'} 的所有成员` : '任何能私聊此机器人的人'
|
|
564
579
|
const resultRouteSummary = conversationKind === 'group' ? '回复到原群(群内可见)' : '回复到发起任务的私聊'
|
|
565
580
|
const executorSummary = runner === 'codex' ? '本机 Codex · PR 快照'
|
|
566
581
|
: runner === 'claude-code' ? '本机 Claude Code · PR 快照' : 'DSH · 当前授权模型'
|
|
567
|
-
const resourceSummary =
|
|
582
|
+
const resourceSummary = nativeReviewerSelected ? '指定仓库的已核验 PR 快照'
|
|
568
583
|
: '当前会话中 DSH 实际可见的内容'
|
|
569
584
|
const cardBlockReason = enrollmentLoadError ? '暂时无法核对机器人是否已启用协作,请刷新状态后再试。'
|
|
570
585
|
: !botConnected || !selectedEnrollmentBound ? '先在上方把机器人绑定到这台 DSH。'
|
|
@@ -577,20 +592,56 @@ window.__ModuleLoader__.load({
|
|
|
577
592
|
: parameters.some((rule) => rule.kind === 'gitea_pr')
|
|
578
593
|
&& normalizeServiceUrl(trustedOrigin) !== selectedEnrollment?.gitea_origin
|
|
579
594
|
? '请先保存机器人级可信 Gitea 地址;PR 参数会自动使用这个地址。'
|
|
580
|
-
:
|
|
595
|
+
: nativeReviewerSelected && (parameters.length !== 1 || parameters[0].kind !== 'gitea_pr')
|
|
581
596
|
? 'Codex 和 Claude Code 目前只支持单个 Gitea PR 审查任务。'
|
|
582
|
-
:
|
|
597
|
+
: nativeReviewerSelected && !parameters[0].required
|
|
583
598
|
? '本机代码审查的 Gitea PR 编号必须设为必填。'
|
|
584
|
-
:
|
|
585
|
-
? '这台 DSH
|
|
586
|
-
|
|
587
|
-
|
|
599
|
+
: reviewers.includes('dsh') && !dshAvailable
|
|
600
|
+
? '这台 DSH 尚未准备好文字审查执行器。'
|
|
601
|
+
: reviewers.includes('codex') && !codexAvailable
|
|
602
|
+
? '这台 DSH 尚未找到可用的 Codex CLI,请先安装或检查 DSH 的 PATH。'
|
|
603
|
+
: reviewers.includes('claude-code') && !claudeCodeAvailable
|
|
604
|
+
? '这台 DSH 尚未注册 Claude Code 子代理,请先在 DSH 中安装并启用它。'
|
|
588
605
|
: ''
|
|
589
606
|
const loadNames = { bots: '机器人', 'enrolled-bots': '协作启用状态', groups: '群聊列表',
|
|
590
607
|
cards: '任务授权', settings: '团队默认值', executors: '本机执行器',
|
|
591
608
|
'local-settings': '本机设置', status: '最近任务' }
|
|
592
609
|
const updateParameter = (index, patch) => setParameters((current) => current.map((rule, i) => i === index ? { ...rule, ...patch } : rule))
|
|
610
|
+
const changeReviewer = (id, enabled) => {
|
|
611
|
+
const next = enabled ? [...reviewers, id] : reviewers.filter((item) => item !== id)
|
|
612
|
+
if (!next.length) { setMessage('至少保留一位审查 Agent。'); return }
|
|
613
|
+
setReviewers([...new Set(next)])
|
|
614
|
+
if (!next.includes(runner)) setRunner(next[0])
|
|
615
|
+
}
|
|
616
|
+
const startNewCard = () => {
|
|
617
|
+
setEditingCardId(null); setEditingExpiresAt(null)
|
|
618
|
+
setDraftNotice('')
|
|
619
|
+
setTitle(''); setIntent(''); setExamples(''); setTaskPrompt(''); setRunner('dsh')
|
|
620
|
+
setReviewers(['dsh'])
|
|
621
|
+
setParameters([]); setConversationKind('dm'); setGroupId('')
|
|
622
|
+
setSenderScope('any'); setSenderIds(''); setDays(7); setCreateOpen(true)
|
|
623
|
+
}
|
|
624
|
+
const editCard = (card, copy = false) => {
|
|
625
|
+
setBotId(card.bot_id)
|
|
626
|
+
setTrustedOrigin(enrolled.find((bot) => bot.bot_id === card.bot_id)?.gitea_origin || '')
|
|
627
|
+
setEditingCardId(copy ? null : card.id); setEditingExpiresAt(copy ? null : card.expires_at)
|
|
628
|
+
setTitle(copy ? `${card.title.slice(0, 94)}(副本)` : card.title)
|
|
629
|
+
if (copy) setDays(7)
|
|
630
|
+
setIntent(card.intent); setExamples((card.examples || []).join('\n'))
|
|
631
|
+
setTaskPrompt(card.task_prompt); setRunner(card.runner)
|
|
632
|
+
setReviewers(Array.isArray(card.reviewers) && card.reviewers.length ? card.reviewers : [card.runner])
|
|
633
|
+
setParameters((card.parameters || []).map((rule) => ({
|
|
634
|
+
name: rule.name, kind: rule.kind, required: rule.required,
|
|
635
|
+
description: rule.description || '', min: rule.min == null ? '' : String(rule.min),
|
|
636
|
+
max: rule.max == null ? '' : String(rule.max), choices: (rule.choices || []).join(', '),
|
|
637
|
+
gitea_origin: rule.gitea_origin || '', repository: rule.repository || '',
|
|
638
|
+
})))
|
|
639
|
+
setConversationKind(card.conversation_kind); setGroupId(card.conversation_id || '')
|
|
640
|
+
setSenderScope(card.sender_scope); setSenderIds((card.sender_ids || []).join(', '))
|
|
641
|
+
setCreateOpen(true)
|
|
642
|
+
}
|
|
593
643
|
const preparePrExample = () => {
|
|
644
|
+
setEditingCardId(null); setEditingExpiresAt(null)
|
|
594
645
|
setTitle('审查 Gitea PR 差异')
|
|
595
646
|
setIntent('同事希望查看指定 Gitea PR 的代码差异并获得审查意见。')
|
|
596
647
|
setExamples('请帮我审查 123 号 PR\n看看刚才提到的 PR 有什么风险')
|
|
@@ -598,7 +649,13 @@ window.__ModuleLoader__.load({
|
|
|
598
649
|
setParameters([{ name: 'pr_number', kind: 'gitea_pr', required: true,
|
|
599
650
|
description: '要审查的 PR 编号', min: '1', max: '', choices: '',
|
|
600
651
|
gitea_origin: selectedEnrollment?.gitea_origin || trustedOrigin || '', repository: '' }])
|
|
601
|
-
|
|
652
|
+
const availableReviewers = [
|
|
653
|
+
...(codexAvailable ? ['codex'] : []),
|
|
654
|
+
...(claudeCodeAvailable ? ['claude-code'] : []),
|
|
655
|
+
]
|
|
656
|
+
const selected = availableReviewers.length ? availableReviewers : ['dsh']
|
|
657
|
+
setReviewers(selected)
|
|
658
|
+
setRunner(selected[0])
|
|
602
659
|
setCreateOpen(true)
|
|
603
660
|
}
|
|
604
661
|
const saveGiteaToken = async () => {
|
|
@@ -804,6 +861,9 @@ window.__ModuleLoader__.load({
|
|
|
804
861
|
event.preventDefault()
|
|
805
862
|
setDraftNotice('')
|
|
806
863
|
if (!selectedBot || !botConnected || !selectedEnrollmentBound) { setMessage('请先在这台 DSH 绑定选中的协作机器人。'); return }
|
|
864
|
+
if (editingCardId && (!editingExpiresAt || editingExpiresAt <= Math.floor(Date.now() / 1000))) {
|
|
865
|
+
setMessage('这项能力已到期,无法继续编辑;请返回后重新开放一项能力。'); return
|
|
866
|
+
}
|
|
807
867
|
if (!title.trim() || !intent.trim() || !taskPrompt.trim()) {
|
|
808
868
|
setMessage('请填写能力名称、适用请求说明和处理要求。'); return
|
|
809
869
|
}
|
|
@@ -855,45 +915,67 @@ window.__ModuleLoader__.load({
|
|
|
855
915
|
if (normalized.some((rule) => rule.kind === 'gitea_pr' && !validRepository(rule.repository))) {
|
|
856
916
|
setMessage('Gitea 仓库请填写“团队名/仓库名”,不要包含空格或多余路径。'); return
|
|
857
917
|
}
|
|
858
|
-
if (
|
|
859
|
-
|
|
860
|
-
setMessage('Codex 和 Claude Code 目前需要一个必填的 Gitea PR 编号参数。'); return
|
|
918
|
+
if (!reviewers.length || !reviewers.includes(runner)) {
|
|
919
|
+
setMessage('请至少选择一位审查 Agent,并设置普通请求的默认执行方。'); return
|
|
861
920
|
}
|
|
862
|
-
if (
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
if (runner === 'claude-code' && !claudeCodeAvailable) {
|
|
866
|
-
setMessage('这台 DSH 尚未注册 Claude Code 子代理。'); return
|
|
921
|
+
if (nativeReviewerSelected && (normalized.length !== 1
|
|
922
|
+
|| normalized[0].kind !== 'gitea_pr' || !normalized[0].required)) {
|
|
923
|
+
setMessage('选择 Codex 或 Claude Code 时,需要一个必填的 Gitea PR 编号参数。'); return
|
|
867
924
|
}
|
|
868
925
|
const scope = conversationKind === 'group' ? `群 ${groupId}` : '私聊'
|
|
869
926
|
const resultDestination = conversationKind === 'group' ? '原群(群内可见)' : '发起任务的私聊'
|
|
870
|
-
const
|
|
871
|
-
|
|
872
|
-
:
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
927
|
+
const executionDetails = {
|
|
928
|
+
dsh: 'DSH 只根据已提供的 PR diff 审查,不读取完整仓库。',
|
|
929
|
+
codex: 'Codex 使用已核验 PR Head 的临时代码快照,并启用本机固定只读权限;不接收 Gitea 令牌。',
|
|
930
|
+
'claude-code': 'Claude Code 使用同一份临时代码快照,沿用这台 DSH 的本机权限设置;请确认该权限适合由获准成员请求审查。',
|
|
931
|
+
}
|
|
932
|
+
const executionInfo = reviewers.length > 1
|
|
933
|
+
? `普通请求默认由 ${reviewerLabel(runner)} 执行;请求人明确说“交叉 review”时,会并行启动 ${reviewers.map(reviewerLabel).join('、')},分别返回审查结果。${reviewers.map((item) => executionDetails[item]).join(' ')}`
|
|
934
|
+
: executionDetails[reviewers[0]]
|
|
935
|
+
const actionDescription = editingCardId
|
|
936
|
+
? `更新“${title}”后,${requesterSummary}可在${scope}使用新配置;旧待确认请求将失效,已提交任务继续使用提交时的配置。结果${resultDestination}。到期时间保持为 ${new Date(editingExpiresAt * 1000).toLocaleString('zh-CN')}。`
|
|
937
|
+
: `开放“${title}”后,${requesterSummary}可在${scope}请求,有效期 ${days} 天;结果${resultDestination}。`
|
|
938
|
+
if (!window.confirm(`${actionDescription}${executionInfo}请确认。`)) return
|
|
876
939
|
setBusy(true)
|
|
877
940
|
try {
|
|
878
941
|
const response = await fetch('/api/ant-teams/delegation/cards', {
|
|
879
|
-
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
880
|
-
body: JSON.stringify({
|
|
942
|
+
method: editingCardId ? 'PUT' : 'POST', headers: { 'content-type': 'application/json' },
|
|
943
|
+
body: JSON.stringify({ ...(editingCardId ? { id: editingCardId } : {}),
|
|
944
|
+
bot_id: selectedBot.bot_id, bot_fingerprint: selectedBot.fingerprint,
|
|
881
945
|
title, intent, examples: exampleLines,
|
|
882
|
-
parameters: normalized, task_prompt: taskPrompt, runner, workspace: null,
|
|
946
|
+
parameters: normalized, task_prompt: taskPrompt, runner, reviewers, workspace: null,
|
|
883
947
|
sender_scope: senderScope, sender_ids: senderScope === 'allowlist'
|
|
884
948
|
? senderIds.split(/[\s,,]+/u).filter(Boolean) : [], conversation_kind: conversationKind,
|
|
885
949
|
...(conversationKind === 'group' ? { conversation_id: groupId } : {}),
|
|
886
|
-
expires_at: Math.floor(Date.now() / 1000) + Number(days) * 86400 }),
|
|
950
|
+
expires_at: editingCardId ? editingExpiresAt : Math.floor(Date.now() / 1000) + Number(days) * 86400 }),
|
|
887
951
|
})
|
|
888
952
|
const result = await readLocalJsonResponse(response)
|
|
889
|
-
if (!response.ok &&
|
|
890
|
-
throw new Error('当前团队服务端可能尚不支持 Claude Code
|
|
953
|
+
if (!response.ok && reviewers.includes('claude-code') && result.code === 'INVALID_REQUEST') {
|
|
954
|
+
throw new Error('当前团队服务端可能尚不支持 Claude Code 审查配置。请管理员升级服务端后刷新重试。')
|
|
891
955
|
}
|
|
892
956
|
if (!response.ok) throw new Error(result.message || '能力保存失败。')
|
|
893
|
-
|
|
957
|
+
const savedReviewers = Array.isArray(result.reviewers) && result.reviewers.length
|
|
958
|
+
? result.reviewers : [result.runner]
|
|
959
|
+
if ([...savedReviewers].sort().join('\0') !== [...reviewers].sort().join('\0')) {
|
|
960
|
+
if (!editingCardId && typeof result.id === 'string') {
|
|
961
|
+
const cleanup = await fetch('/api/ant-teams/delegation/cards', {
|
|
962
|
+
method: 'DELETE', headers: { 'content-type': 'application/json' },
|
|
963
|
+
body: JSON.stringify({ id: result.id }),
|
|
964
|
+
}).catch(() => null)
|
|
965
|
+
if (!cleanup?.ok) {
|
|
966
|
+
await load().catch(() => undefined)
|
|
967
|
+
throw new Error('团队服务端尚不支持多 Agent 审查配置;旧服务端可能已保存单 Agent 授权,请刷新列表并停止它,再升级 ANT 服务端。')
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
throw new Error('当前团队服务端尚不支持多 Agent 审查配置;请管理员升级 ANT 服务端后重试。')
|
|
971
|
+
}
|
|
972
|
+
setTitle(''); setIntent(''); setExamples(''); setTaskPrompt(''); setParameters([]); setRunner('dsh'); setReviewers(['dsh'])
|
|
973
|
+
setEditingCardId(null); setEditingExpiresAt(null)
|
|
894
974
|
await load()
|
|
895
975
|
setCreateOpen(false)
|
|
896
|
-
setMessage(
|
|
976
|
+
setMessage(editingCardId
|
|
977
|
+
? '能力配置已更新。新请求将使用新配置,已提交任务保留原配置。'
|
|
978
|
+
: '能力已开放。同事可自然地提出这类请求;参数不完整时机器人会追问。')
|
|
897
979
|
} catch (error) { setMessage(error.message) }
|
|
898
980
|
finally { setBusy(false) }
|
|
899
981
|
}
|
|
@@ -910,6 +992,7 @@ window.__ModuleLoader__.load({
|
|
|
910
992
|
finally { setBusy(false) }
|
|
911
993
|
}
|
|
912
994
|
const copyCardToDraft = (card) => {
|
|
995
|
+
setEditingCardId(null); setEditingExpiresAt(null)
|
|
913
996
|
const cardBot = bots.find((bot) => bot.fingerprint === card.bot_fingerprint)
|
|
914
997
|
const cardEnrollment = enrolled.find((bot) => bot.bot_fingerprint === card.bot_fingerprint)
|
|
915
998
|
const titleChars = Array.from(`复制:${card.title || '未命名能力'}`)
|
|
@@ -921,6 +1004,7 @@ window.__ModuleLoader__.load({
|
|
|
921
1004
|
setExamples(Array.isArray(card.examples) ? card.examples.join('\n') : '')
|
|
922
1005
|
setTaskPrompt(card.task_prompt || '')
|
|
923
1006
|
setRunner(['dsh', 'codex', 'claude-code'].includes(card.runner) ? card.runner : 'dsh')
|
|
1007
|
+
setReviewers(Array.isArray(card.reviewers) && card.reviewers.length ? card.reviewers : [card.runner || 'dsh'])
|
|
924
1008
|
setParameters(Array.isArray(card.parameters) ? card.parameters.map((rule) => ({
|
|
925
1009
|
...rule, min: String(rule.min ?? ''), max: String(rule.max ?? ''), choices: Array.isArray(rule.choices) ? rule.choices.join(',') : String(rule.choices ?? ''),
|
|
926
1010
|
})) : [])
|
|
@@ -1067,8 +1151,6 @@ window.__ModuleLoader__.load({
|
|
|
1067
1151
|
? `群 ${groups.find((group) => group.group_id === card.conversation_id)?.group_code || card.conversation_id}` : '私聊'
|
|
1068
1152
|
const requester = card.sender_scope === 'allowlist' ? '指定成员可请求'
|
|
1069
1153
|
: card.conversation_kind === 'group' ? '群成员可请求' : '可私聊者可请求'
|
|
1070
|
-
const runnerName = card.runner === 'codex' ? '本机 Codex · PR 快照'
|
|
1071
|
-
: card.runner === 'claude-code' ? '本机 Claude Code · PR 快照' : 'DSH 文字处理'
|
|
1072
1154
|
const parametersText = (card.parameters || []).map((rule) => {
|
|
1073
1155
|
const constraint = rule.kind === 'gitea_pr' && rule.repository ? `仓库 ${rule.repository}`
|
|
1074
1156
|
: rule.kind === 'choice' ? `选项 ${(rule.choices || []).join('、')}`
|
|
@@ -1087,7 +1169,7 @@ window.__ModuleLoader__.load({
|
|
|
1087
1169
|
h('strong', { style: { overflowWrap: 'anywhere' } }, card.title || '未命名能力'),
|
|
1088
1170
|
h('span', { style: { ...mutedTextStyle, padding: '2px 8px', borderRadius: 999,
|
|
1089
1171
|
backgroundColor: 'var(--dsw-alias-bg-layer-2)', color: remainingDaysLabel, whiteSpace: 'nowrap' } }, expiry)),
|
|
1090
|
-
|
|
1172
|
+
h('span', { style: mutedTextStyle }, `${cardReviewerSummary(card)} · ${conversation} · ${requester} · 结果回复原${card.conversation_kind === 'group' ? '群' : '私聊'}`),
|
|
1091
1173
|
h('details', null,
|
|
1092
1174
|
h('summary', { style: { cursor: 'pointer', color: 'var(--dsw-alias-link)', fontSize: 13 } }, '查看配置'),
|
|
1093
1175
|
h('div', { style: { display: 'grid', gap: 4, paddingTop: 6 } },
|
|
@@ -1098,6 +1180,7 @@ window.__ModuleLoader__.load({
|
|
|
1098
1180
|
examplesText ? h('p', { style: mutedTextStyle }, `请求示例:${examplesText}`) : null,
|
|
1099
1181
|
card.task_prompt ? h('p', { style: mutedTextStyle }, `处理要求:${card.task_prompt}`) : null))),
|
|
1100
1182
|
h('div', { style: { display: 'flex', gap: 6, flexWrap: 'wrap', justifyContent: 'flex-end' } },
|
|
1183
|
+
h('button', { type: 'button', disabled: busy, onClick: () => editCard(card), style: secondaryButtonStyle }, '编辑'),
|
|
1101
1184
|
h('button', { type: 'button', disabled: busy, onClick: () => copyCardToDraft(card), style: secondaryButtonStyle }, '复制'),
|
|
1102
1185
|
h('button', { type: 'button', disabled: busy, onClick: () => void revokeCard(card), style: secondaryButtonStyle }, '停止')))
|
|
1103
1186
|
}),
|
|
@@ -1133,10 +1216,12 @@ window.__ModuleLoader__.load({
|
|
|
1133
1216
|
h('details', { id: 'ant-new-task', open: createOpen,
|
|
1134
1217
|
onToggle: (event) => setCreateOpen(event.currentTarget.open),
|
|
1135
1218
|
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 } }, '开放一项能力'),
|
|
1219
|
+
h('summary', { id: 'ant-new-task-summary', style: { cursor: 'pointer', fontWeight: 600 } }, editingCardId ? '编辑能力配置' : '开放一项能力'),
|
|
1137
1220
|
h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 320px), 1fr))',
|
|
1138
1221
|
gap: 16, alignItems: 'start', marginTop: 12 } },
|
|
1139
1222
|
h('form', { id: 'ant-capability-create-form', onSubmit: createCard, style: { display: 'grid', gap: 9, minWidth: 0 } },
|
|
1223
|
+
editingCardId ? h('p', { role: 'status', style: mutedTextStyle },
|
|
1224
|
+
`保存会生成新的授权版本并停止旧版本;到期时间保持不变:${new Date(editingExpiresAt * 1000).toLocaleString('zh-CN')}。已提交任务继续使用原配置。`) : null,
|
|
1140
1225
|
h('p', { style: mutedTextStyle }, selectedBot
|
|
1141
1226
|
? `当前机器人:${selectedBot.name}${selectedEnrollmentBound ? ' · 已绑定此设备' : ' · 请先绑定到此设备'}`
|
|
1142
1227
|
: '请先在上方选择机器人并启用协作。'),
|
|
@@ -1156,20 +1241,29 @@ window.__ModuleLoader__.load({
|
|
|
1156
1241
|
onChange: (event) => setExamples(event.target.value), placeholder: '帮我总结一下刚才的讨论', style: inputStyle })),
|
|
1157
1242
|
h('label', null, '处理要求', h('textarea', { required: true, maxLength: 4000, rows: 3, value: taskPrompt,
|
|
1158
1243
|
onChange: (event) => setTaskPrompt(event.target.value), placeholder: '只使用当前会话可见的内容,列出结论和待办', style: inputStyle })),
|
|
1159
|
-
h('
|
|
1160
|
-
|
|
1161
|
-
h('
|
|
1162
|
-
h('
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
'
|
|
1244
|
+
h('fieldset', { style: { ...panelStyle, padding: 10, display: 'grid', gap: 7 } },
|
|
1245
|
+
h('legend', null, '审查 Agent(可多选)'),
|
|
1246
|
+
h('p', { style: mutedTextStyle }, '普通请求使用默认 Agent;明确说“交叉 review”时,所有勾选项会并行审查同一份 PR 快照(至少勾选两位才构成交叉审查,最多三位,并会同时消耗各自的本机或团队模型额度)。'),
|
|
1247
|
+
h('div', { style: { display: 'flex', gap: 14, flexWrap: 'wrap' } },
|
|
1248
|
+
h('label', null, h('input', { type: 'checkbox', 'aria-label': '审查 Agent DSH', checked: reviewers.includes('dsh'), disabled: !dshAvailable && !reviewers.includes('dsh'),
|
|
1249
|
+
onChange: (event) => changeReviewer('dsh', event.target.checked) }), ' DSH · 仅审查已提供的 diff'),
|
|
1250
|
+
h('label', null, h('input', { type: 'checkbox', 'aria-label': '审查 Agent Codex', checked: reviewers.includes('codex'),
|
|
1251
|
+
disabled: (!codexAvailable || !hasPrParameter) && !reviewers.includes('codex'),
|
|
1252
|
+
onChange: (event) => changeReviewer('codex', event.target.checked) }), ' Codex · 完整快照只读审查'),
|
|
1253
|
+
h('label', null, h('input', { type: 'checkbox', 'aria-label': '审查 Agent Claude Code', checked: reviewers.includes('claude-code'),
|
|
1254
|
+
disabled: (!claudeCodeAvailable || !hasPrParameter) && !reviewers.includes('claude-code'),
|
|
1255
|
+
onChange: (event) => changeReviewer('claude-code', event.target.checked) }), ' Claude Code · 使用本机权限审查')),
|
|
1256
|
+
!hasPrParameter ? h('p', { style: mutedTextStyle }, 'Codex 与 Claude Code 需要先添加一个必填的 Gitea PR 编号参数。') : null,
|
|
1257
|
+
reviewers.length > 1 ? h('label', null, '普通请求默认执行方', h('select', { value: runner,
|
|
1258
|
+
onChange: (event) => setRunner(event.target.value), style: inputStyle },
|
|
1259
|
+
...reviewers.map((item) => h('option', { key: item, value: item }, reviewerLabel(item))))) : null),
|
|
1166
1260
|
nativePrUnavailableReason ? h('p', { role: 'status', style: mutedTextStyle },
|
|
1167
1261
|
`${nativePrUnavailableReason},已禁用 Codex/Claude Code 完整快照选项;可选择 DSH 差异审查。`) : null,
|
|
1168
|
-
|
|
1169
|
-
'Codex 使用这台电脑的登录账号及其用量;ANT 为每项能力维护仓库缓存,每次审查创建固定到 PR Head 的临时代码快照。不会收到 Gitea
|
|
1170
|
-
|
|
1171
|
-
'Claude Code 通过 DSH
|
|
1172
|
-
|
|
1262
|
+
reviewers.includes('codex') ? h('p', { role: 'status', style: mutedTextStyle },
|
|
1263
|
+
'Codex 使用这台电脑的登录账号及其用量;ANT 为每项能力维护仓库缓存,每次审查创建固定到 PR Head 的临时代码快照。不会收到 Gitea 令牌;Git LFS 实际文件不会下载。') : null,
|
|
1264
|
+
reviewers.includes('claude-code') ? h('p', { role: 'status', style: mutedTextStyle },
|
|
1265
|
+
'Claude Code 通过 DSH 已注册的子代理运行,可查看同一 PR 快照。ANT 不会传 Gitea 令牌;它沿用 DSH 的本机权限设置,不具备 Codex 路径相同的固定只读隔离。Git LFS 实际文件不会下载。') : null,
|
|
1266
|
+
nativeReviewerSelected ? h('p', { role: 'status', style: mutedTextStyle },
|
|
1173
1267
|
'本机 PR 执行方需要 Gitea granular token 的只读权限 read:repository 和 read:user;read:user 仅用于这台 DSH 获取 Git HTTPS 登录名。') : null,
|
|
1174
1268
|
h('details', { style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, padding: 10 } },
|
|
1175
1269
|
h('summary', { style: { cursor: 'pointer' } }, '参数与授权范围'),
|
|
@@ -1186,7 +1280,7 @@ window.__ModuleLoader__.load({
|
|
|
1186
1280
|
onChange: (event) => updateParameter(index, { kind: event.target.value }), style: { ...inputStyle, flex: '1 1 150px' } },
|
|
1187
1281
|
h('option', { value: 'text' }, '文本'), h('option', { value: 'integer' }, '整数'),
|
|
1188
1282
|
h('option', { value: 'choice' }, '选项'), h('option', { value: 'gitea_pr' }, 'Gitea PR 编号')),
|
|
1189
|
-
h('label', null, h('input', { type: 'checkbox',
|
|
1283
|
+
h('label', null, h('input', { type: 'checkbox', 'aria-label': '参数必填', checked: rule.required,
|
|
1190
1284
|
onChange: (event) => updateParameter(index, { required: event.target.checked }) }), ' 必填'),
|
|
1191
1285
|
['integer', 'gitea_pr'].includes(rule.kind) ? h('input', { 'aria-label': '最小值', type: 'number', value: rule.min,
|
|
1192
1286
|
onChange: (event) => updateParameter(index, { min: event.target.value }), placeholder: '最小值', style: { ...inputStyle, width: 120 } }) : null,
|
|
@@ -1225,8 +1319,9 @@ window.__ModuleLoader__.load({
|
|
|
1225
1319
|
h('option', { value: 'allowlist' }, '指定 IM 用户 ID'))),
|
|
1226
1320
|
senderScope === 'allowlist' ? h('input', { 'aria-label': '可请求的 IM 用户 ID', required: true, value: senderIds,
|
|
1227
1321
|
onChange: (event) => setSenderIds(event.target.value), placeholder: '多个 ID 用逗号分隔', style: inputStyle }) : null,
|
|
1228
|
-
|
|
1229
|
-
h('
|
|
1322
|
+
editingCardId ? h('p', { style: mutedTextStyle }, `有效期:${new Date(editingExpiresAt * 1000).toLocaleString('zh-CN')}(编辑时保持不变)`)
|
|
1323
|
+
: h('label', null, '有效期', h('select', { value: days, onChange: (event) => setDays(Number(event.target.value)), style: inputStyle },
|
|
1324
|
+
h('option', { value: 1 }, '1 天'), h('option', { value: 7 }, '7 天'), h('option', { value: 30 }, '30 天')))),
|
|
1230
1325
|
),
|
|
1231
1326
|
h('aside', { id: 'ant-authorization-review', role: 'region', 'aria-label': '开放前复核', tabIndex: -1,
|
|
1232
1327
|
style: { ...settingsPanelStyle, padding: 14, gap: 10,
|
|
@@ -1242,7 +1337,7 @@ window.__ModuleLoader__.load({
|
|
|
1242
1337
|
['可请求人', requesterSummary],
|
|
1243
1338
|
['执行方', executorSummary],
|
|
1244
1339
|
['读取资料', resourceSummary],
|
|
1245
|
-
['有效期', `${days} 天`],
|
|
1340
|
+
['有效期', editingCardId ? new Date(editingExpiresAt * 1000).toLocaleString('zh-CN') : `${days} 天`],
|
|
1246
1341
|
['回复位置', resultRouteSummary],
|
|
1247
1342
|
].map(([label, value]) => h('div', { key: label, style: { display: 'grid', gridTemplateColumns: '84px minmax(0, 1fr)', gap: 8 } },
|
|
1248
1343
|
h('dt', { style: mutedTextStyle }, label),
|
|
@@ -1250,7 +1345,7 @@ window.__ModuleLoader__.load({
|
|
|
1250
1345
|
cardBlockReason ? h('p', { role: 'status', style: { ...mutedTextStyle,
|
|
1251
1346
|
color: 'var(--dsw-alias-state-warning-primary, var(--dsw-alias-label-secondary))' } }, cardBlockReason) : null,
|
|
1252
1347
|
h('button', { type: 'submit', form: 'ant-capability-create-form', disabled: busy || Boolean(cardBlockReason),
|
|
1253
|
-
style: { ...primaryButtonStyle, width: '100%' } }, '确认并开放能力'))))),
|
|
1348
|
+
style: { ...primaryButtonStyle, width: '100%' } }, editingCardId ? '保存能力修改' : '确认并开放能力'))))),
|
|
1254
1349
|
h('div', { role: 'region', 'aria-label': '本机处理设置', style: { display: activeTab === 'local' ? 'grid' : 'none', gap: 12 } },
|
|
1255
1350
|
h('section', { style: { ...settingsPanelStyle, padding: 14 } },
|
|
1256
1351
|
h('strong', null, 'Gitea 本机凭据'),
|
|
@@ -1816,6 +1911,11 @@ window.__ModuleLoader__.load({
|
|
|
1816
1911
|
const [bundleRelease, setBundleRelease] = useState(null)
|
|
1817
1912
|
const [bundleUpdate, setBundleUpdate] = useState({ state: 'idle', currentVersion: '', message: '' })
|
|
1818
1913
|
const [authorizedModels, setAuthorizedModels] = useState([])
|
|
1914
|
+
const [serverDefaultModel, setServerDefaultModel] = useState(null)
|
|
1915
|
+
const [dshDefaultSelection, setDshDefaultSelection] = useState(null)
|
|
1916
|
+
const [defaultModelDraft, setDefaultModelDraft] = useState('')
|
|
1917
|
+
const [defaultModelBusy, setDefaultModelBusy] = useState(false)
|
|
1918
|
+
const [defaultModelMessage, setDefaultModelMessage] = useState('')
|
|
1819
1919
|
const [lastModelSync, setLastModelSync] = useState(null)
|
|
1820
1920
|
const [serviceCompatibility, setServiceCompatibility] = useState({ state: 'idle', message: '' })
|
|
1821
1921
|
const [pinnedServiceUrl, setPinnedServiceUrl] = useState(null)
|
|
@@ -1889,27 +1989,87 @@ window.__ModuleLoader__.load({
|
|
|
1889
1989
|
return status
|
|
1890
1990
|
}
|
|
1891
1991
|
|
|
1892
|
-
const
|
|
1992
|
+
const modelProvider = (model) => model.protocol === 'anthropic' ? 'ant-team-anthropic'
|
|
1993
|
+
: model.openai_api_mode === 'responses' ? 'ant-team-openai-responses' : 'ant-team-openai-chat-completions'
|
|
1994
|
+
const modelSelectionKey = (model) => JSON.stringify({ protocol: model.protocol, model_id: model.model_id })
|
|
1995
|
+
const defaultPreferenceKey = (rawServiceUrl) => `${DEFAULT_MODEL_INITIALIZED_KEY}:${normalizeServiceUrl(rawServiceUrl)}:${savedDeviceId() || ''}`
|
|
1996
|
+
const readDefaultPreference = (rawServiceUrl) => {
|
|
1997
|
+
try { return JSON.parse(localStorage.getItem(defaultPreferenceKey(rawServiceUrl)) || 'null') } catch { return null }
|
|
1998
|
+
}
|
|
1999
|
+
const saveDefaultPreference = (rawServiceUrl, value) => {
|
|
2000
|
+
try { localStorage.setItem(defaultPreferenceKey(rawServiceUrl), JSON.stringify(value)) } catch {}
|
|
2001
|
+
}
|
|
2002
|
+
const modelForProviderSelection = (selection, models) => {
|
|
2003
|
+
if (!selection) return null
|
|
2004
|
+
const exact = models.find((model) => model.model_id === selection.model && modelProvider(model) === selection.provider)
|
|
2005
|
+
if (exact) return exact
|
|
2006
|
+
const legacyProtocol = selection.provider === 'ant-team-openai' || selection.provider?.startsWith('ant-team-openai-')
|
|
2007
|
+
? 'openai' : selection.provider === 'ant-team-anthropic' ? 'anthropic' : null
|
|
2008
|
+
return legacyProtocol ? models.find((model) => model.protocol === legacyProtocol && model.model_id === selection.model) : null
|
|
2009
|
+
}
|
|
2010
|
+
const readDshDefaultSelection = async () => {
|
|
2011
|
+
if (typeof remote?.settings?.describe !== 'function') throw new Error('当前 DSH 未提供全局默认模型读取接口。')
|
|
2012
|
+
const response = await remote.settings.describe()
|
|
2013
|
+
if (!response?.ok) throw new Error(response?.error?.message || '无法读取这台 DSH 的全局默认模型。')
|
|
2014
|
+
const value = response.value?.namespaces?.find((entry) => entry.ns === 'agent-default-model')?.value
|
|
2015
|
+
return value && typeof value.provider === 'string' && typeof value.model === 'string'
|
|
2016
|
+
? { provider: value.provider, model: value.model,
|
|
2017
|
+
...(typeof value.reasoningEffort === 'string' ? { reasoningEffort: value.reasoningEffort } : {}) } : null
|
|
2018
|
+
}
|
|
2019
|
+
const writeDshDefaultModel = async (model) => {
|
|
2020
|
+
const response = await remote.settings.mutate('agent-default-model', [
|
|
2021
|
+
{ op: 'set', path: ['provider'], value: modelProvider(model) },
|
|
2022
|
+
{ op: 'set', path: ['model'], value: model.model_id },
|
|
2023
|
+
{ op: 'unset', path: ['reasoningEffort'] },
|
|
2024
|
+
], undefined)
|
|
2025
|
+
if (!response?.ok) throw new Error(response?.error?.message || 'DSH 无法保存全局默认模型。')
|
|
2026
|
+
const selection = { provider: modelProvider(model), model: model.model_id }
|
|
2027
|
+
setDshDefaultSelection(selection)
|
|
2028
|
+
setDefaultModelDraft(modelSelectionKey(model))
|
|
2029
|
+
return selection
|
|
2030
|
+
}
|
|
2031
|
+
const applyUserDefaultModel = async (model) => {
|
|
2032
|
+
if (!model || !authorizedModels.some((item) => modelSelectionKey(item) === modelSelectionKey(model))) {
|
|
2033
|
+
setDefaultModelMessage('请选择当前已获准的模型。')
|
|
2034
|
+
return
|
|
2035
|
+
}
|
|
2036
|
+
setDefaultModelBusy(true)
|
|
2037
|
+
try {
|
|
2038
|
+
const selection = await writeDshDefaultModel(model)
|
|
2039
|
+
const preference = readDefaultPreference(serviceUrl) || { initialized: true, serverDefault: '' }
|
|
2040
|
+
const isServerDefault = serverDefaultModel && modelSelectionKey(model) === modelSelectionKey(serverDefaultModel)
|
|
2041
|
+
if (isServerDefault) preference.serverDefault = modelSelectionKey(serverDefaultModel)
|
|
2042
|
+
preference.initialized = true
|
|
2043
|
+
saveDefaultPreference(serviceUrl, preference)
|
|
2044
|
+
setDshDefaultSelection(selection)
|
|
2045
|
+
setDefaultModelMessage(`已将 ${model.display_name || model.model_id} 设为 DSH 全局默认模型。`)
|
|
2046
|
+
} catch (error) {
|
|
2047
|
+
setDefaultModelMessage(error instanceof Error ? error.message : '保存本机默认模型失败。')
|
|
2048
|
+
} finally { setDefaultModelBusy(false) }
|
|
2049
|
+
}
|
|
2050
|
+
const applyAuthorizedModels = async (models, rawServiceUrl, rawDefaultModel = null) => {
|
|
1893
2051
|
const normalized = normalizeServiceUrl(rawServiceUrl)
|
|
1894
2052
|
if (!normalized || !Array.isArray(models)) throw new Error('ANT 返回的团队模型目录格式无效。')
|
|
1895
2053
|
if (typeof remote?.settings?.mutate !== 'function') throw new Error('当前 DSH 版本未提供模型设置接口。')
|
|
1896
|
-
const
|
|
1897
|
-
openai:
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
const protocolModels =
|
|
2054
|
+
const groups = [
|
|
2055
|
+
{ route: 'ant-team-openai-chat-completions', protocol: 'openai', mode: 'chat_completions', api: 'openai-completions' },
|
|
2056
|
+
{ route: 'ant-team-openai-responses', protocol: 'openai', mode: 'responses', api: 'openai-responses' },
|
|
2057
|
+
{ route: 'ant-team-anthropic', protocol: 'anthropic', api: 'anthropic-messages' },
|
|
2058
|
+
]
|
|
2059
|
+
const ops = [{ op: 'unset', path: ['providers', 'ant-team-openai'] }]
|
|
2060
|
+
for (const group of groups) {
|
|
2061
|
+
const protocolModels = models.filter((model) => model.protocol === group.protocol
|
|
2062
|
+
&& (group.protocol !== 'openai' || (model.openai_api_mode || 'chat_completions') === group.mode))
|
|
2063
|
+
const route = group.route
|
|
1904
2064
|
if (protocolModels.length === 0) {
|
|
1905
2065
|
ops.push({ op: 'unset', path: ['providers', route] })
|
|
1906
2066
|
continue
|
|
1907
2067
|
}
|
|
1908
2068
|
ops.push({ op: 'set', path: ['providers', route], value: {
|
|
1909
|
-
displayName: protocol === '
|
|
2069
|
+
displayName: group.protocol === 'anthropic' ? 'ANT 团队 · Anthropic' : `ANT 团队 · OpenAI · ${group.mode === 'responses' ? 'Responses' : 'Chat Completions'}`,
|
|
1910
2070
|
apiKeyEnv: 'ANT_MODEL_API_KEY',
|
|
1911
|
-
api:
|
|
1912
|
-
baseURL: protocol === 'openai'
|
|
2071
|
+
api: group.api,
|
|
2072
|
+
baseURL: group.protocol === 'openai'
|
|
1913
2073
|
? new URL('/model-gateway/openai/v1', normalized).toString()
|
|
1914
2074
|
: new URL('/model-gateway/anthropic', normalized).toString(),
|
|
1915
2075
|
models: protocolModels.map((model) => ({ id: model.model_id, name: model.display_name })),
|
|
@@ -1917,9 +2077,56 @@ window.__ModuleLoader__.load({
|
|
|
1917
2077
|
}
|
|
1918
2078
|
const settingsWrite = await remote.settings.mutate('llm-pi-ai', ops, undefined)
|
|
1919
2079
|
if (!settingsWrite.ok) throw new Error(settingsWrite.error?.message || 'DSH 无法更新团队模型设置。')
|
|
2080
|
+
const defaultMatch = rawDefaultModel && models.find((model) => model.protocol === rawDefaultModel.protocol
|
|
2081
|
+
&& model.model_id === rawDefaultModel.model_id)
|
|
2082
|
+
const serverDefault = defaultMatch || null
|
|
2083
|
+
const initialDefault = defaultMatch || (rawDefaultModel == null ? models[0] : null)
|
|
2084
|
+
setServerDefaultModel(serverDefault)
|
|
2085
|
+
const preference = readDefaultPreference(normalized)
|
|
2086
|
+
let statusMessage = ''
|
|
2087
|
+
let selection
|
|
2088
|
+
if (!preference?.initialized && initialDefault) {
|
|
2089
|
+
const previousDefault = await readDshDefaultSelection()
|
|
2090
|
+
selection = await writeDshDefaultModel(initialDefault)
|
|
2091
|
+
saveDefaultPreference(normalized, { initialized: true, serverDefault: modelSelectionKey(serverDefault || initialDefault), previousDefault })
|
|
2092
|
+
statusMessage = serverDefault
|
|
2093
|
+
? '已按团队默认模型设置 DSH 全局默认。该 Profile 的新会话会使用此模型;你之后可在此更改,刷新不会覆盖选择。'
|
|
2094
|
+
: '当前无法使用团队默认模型;已用首个获准模型初始化 DSH 全局默认。请管理员检查默认设置和成员授权。'
|
|
2095
|
+
} else {
|
|
2096
|
+
selection = await readDshDefaultSelection()
|
|
2097
|
+
const currentAuthorized = modelForProviderSelection(selection, models)
|
|
2098
|
+
const baseline = models.find((model) => modelSelectionKey(model) === preference?.serverDefault)
|
|
2099
|
+
const stillUsingBaseline = baseline && currentAuthorized && modelSelectionKey(currentAuthorized) === modelSelectionKey(baseline)
|
|
2100
|
+
if (currentAuthorized && selection?.provider !== modelProvider(currentAuthorized)) {
|
|
2101
|
+
selection = await writeDshDefaultModel(currentAuthorized)
|
|
2102
|
+
}
|
|
2103
|
+
if (serverDefault && stillUsingBaseline && modelSelectionKey(serverDefault) !== preference.serverDefault) {
|
|
2104
|
+
selection = await writeDshDefaultModel(serverDefault)
|
|
2105
|
+
saveDefaultPreference(normalized, { ...preference, initialized: true, serverDefault: modelSelectionKey(serverDefault) })
|
|
2106
|
+
statusMessage = '团队默认模型已更新;你尚未设置个人覆盖,因此已同步到 DSH 全局默认。'
|
|
2107
|
+
} else if (selection?.provider?.startsWith('ant-team-') && !currentAuthorized && models.length) {
|
|
2108
|
+
const fallback = serverDefault || models[0]
|
|
2109
|
+
selection = await writeDshDefaultModel(fallback)
|
|
2110
|
+
saveDefaultPreference(normalized, { ...preference, initialized: true,
|
|
2111
|
+
serverDefault: modelSelectionKey(serverDefault || fallback) })
|
|
2112
|
+
statusMessage = serverDefault
|
|
2113
|
+
? '当前默认模型已失去授权,已恢复为团队默认模型。'
|
|
2114
|
+
: '当前默认模型已失去授权,已改用首个获准模型;请管理员设置团队默认模型。'
|
|
2115
|
+
}
|
|
2116
|
+
const effectiveAuthorized = modelForProviderSelection(selection, models)
|
|
2117
|
+
setDshDefaultSelection(selection)
|
|
2118
|
+
setDefaultModelDraft(effectiveAuthorized ? modelSelectionKey(effectiveAuthorized) : '')
|
|
2119
|
+
if (!serverDefault && !statusMessage) statusMessage = rawDefaultModel
|
|
2120
|
+
? '团队默认模型不在这台设备的获准范围内;请联系管理员调整模型权限。'
|
|
2121
|
+
: '团队默认模型尚未设置;请联系管理员设置,或在下方选择本机可用的默认模型。'
|
|
2122
|
+
else if (!statusMessage) statusMessage = effectiveAuthorized
|
|
2123
|
+
? '已保留你选择的 DSH 全局默认模型。'
|
|
2124
|
+
: '当前 DSH 默认不属于 ANT 获准模型;ANT 对话和任务会使用服务端默认模型。'
|
|
2125
|
+
}
|
|
2126
|
+
setDefaultModelMessage(statusMessage)
|
|
1920
2127
|
try { localStorage.setItem(SERVICE_URL_STORAGE_KEY, normalized) } catch {}
|
|
1921
2128
|
onServiceUrlChange(normalized)
|
|
1922
|
-
return models.length
|
|
2129
|
+
return { count: models.length, defaultSelection: selection }
|
|
1923
2130
|
}
|
|
1924
2131
|
|
|
1925
2132
|
const refreshModels = async () => {
|
|
@@ -1937,13 +2144,13 @@ window.__ModuleLoader__.load({
|
|
|
1937
2144
|
if (response.status === 401) setNeedsReconnect(true)
|
|
1938
2145
|
throw new Error(result.message || `刷新失败(${response.status})`)
|
|
1939
2146
|
}
|
|
1940
|
-
const
|
|
2147
|
+
const sync = await applyAuthorizedModels(result.models, result.service_url, result.default_model)
|
|
1941
2148
|
setAuthorizedModels(result.models)
|
|
1942
2149
|
setLastModelSync(new Date())
|
|
1943
2150
|
setCredentialConfigured(true)
|
|
1944
2151
|
setNeedsReconnect(false)
|
|
1945
2152
|
setReauthenticate(false)
|
|
1946
|
-
setModelMessage(`模型目录已刷新,共 ${count} 个获准模型。`)
|
|
2153
|
+
setModelMessage(`模型目录已刷新,共 ${sync.count} 个获准模型。`)
|
|
1947
2154
|
} catch (error) {
|
|
1948
2155
|
setModelMessage(error instanceof Error ? error.message : '刷新模型失败,请稍后重试。')
|
|
1949
2156
|
} finally {
|
|
@@ -2097,7 +2304,32 @@ window.__ModuleLoader__.load({
|
|
|
2097
2304
|
|
|
2098
2305
|
const clearLocalConnection = async () => {
|
|
2099
2306
|
if (typeof remote?.settings?.mutate !== 'function') throw new Error('DSH 未提供模型设置接口。')
|
|
2307
|
+
const selection = await readDshDefaultSelection()
|
|
2308
|
+
if (selection?.provider?.startsWith('ant-team-')) {
|
|
2309
|
+
const previous = readDefaultPreference(serviceUrl)?.previousDefault
|
|
2310
|
+
const restore = previous && typeof previous.provider === 'string' && typeof previous.model === 'string'
|
|
2311
|
+
&& !previous.provider.startsWith('ant-team-')
|
|
2312
|
+
const operations = restore
|
|
2313
|
+
? [
|
|
2314
|
+
{ op: 'set', path: ['provider'], value: previous.provider },
|
|
2315
|
+
{ op: 'set', path: ['model'], value: previous.model },
|
|
2316
|
+
previous.reasoningEffort
|
|
2317
|
+
? { op: 'set', path: ['reasoningEffort'], value: previous.reasoningEffort }
|
|
2318
|
+
: { op: 'unset', path: ['reasoningEffort'] },
|
|
2319
|
+
]
|
|
2320
|
+
: [
|
|
2321
|
+
{ op: 'unset', path: ['provider'] },
|
|
2322
|
+
{ op: 'unset', path: ['model'] },
|
|
2323
|
+
{ op: 'unset', path: ['reasoningEffort'] },
|
|
2324
|
+
]
|
|
2325
|
+
const reset = await remote.settings.mutate('agent-default-model', [
|
|
2326
|
+
...operations,
|
|
2327
|
+
], undefined)
|
|
2328
|
+
if (!reset.ok) throw new Error(reset.error?.message || 'DSH 全局默认模型未能恢复。')
|
|
2329
|
+
}
|
|
2100
2330
|
const settingsResult = await remote.settings.mutate('llm-pi-ai', [
|
|
2331
|
+
{ op: 'unset', path: ['providers', 'ant-team-openai-chat-completions'] },
|
|
2332
|
+
{ op: 'unset', path: ['providers', 'ant-team-openai-responses'] },
|
|
2101
2333
|
{ op: 'unset', path: ['providers', 'ant-team-openai'] },
|
|
2102
2334
|
{ op: 'unset', path: ['providers', 'ant-team-anthropic'] },
|
|
2103
2335
|
], undefined)
|
|
@@ -2106,6 +2338,8 @@ window.__ModuleLoader__.load({
|
|
|
2106
2338
|
const credentialResult = await remote.credentials.unset('ANT_MODEL_API_KEY')
|
|
2107
2339
|
if (!credentialResult.ok) throw new Error(credentialResult.error?.message || 'ANT 成员凭证未能清除。')
|
|
2108
2340
|
setCredentialConfigured(false)
|
|
2341
|
+
setAuthorizedModels([]); setServerDefaultModel(null); setDshDefaultSelection(null); setDefaultModelDraft('')
|
|
2342
|
+
try { localStorage.removeItem(`${DEFAULT_MODEL_INITIALIZED_KEY}:${normalizeServiceUrl(serviceUrl)}:${savedDeviceId() || ''}`) } catch {}
|
|
2109
2343
|
onConnectionChange?.(null)
|
|
2110
2344
|
setNeedsReconnect(false)
|
|
2111
2345
|
setReauthenticate(false)
|
|
@@ -2285,7 +2519,7 @@ window.__ModuleLoader__.load({
|
|
|
2285
2519
|
const credentialWrite = await remote.credentials.set('ANT_MODEL_API_KEY', result.token)
|
|
2286
2520
|
if (!credentialWrite.ok) throw new Error(credentialWrite.error?.message || 'DSH 无法安全保存 ANT 凭证。')
|
|
2287
2521
|
credentialSaved = true
|
|
2288
|
-
await applyAuthorizedModels(result.models, normalized)
|
|
2522
|
+
await applyAuthorizedModels(result.models, normalized, result.default_model)
|
|
2289
2523
|
setAuthorizedModels(result.models)
|
|
2290
2524
|
setLastModelSync(new Date())
|
|
2291
2525
|
setCode('')
|
|
@@ -2316,13 +2550,14 @@ window.__ModuleLoader__.load({
|
|
|
2316
2550
|
const canCheckUpdate = bindingReady && credentialConfigured === true && !needsReconnect
|
|
2317
2551
|
const updateGuideUrl = pinnedServiceUrl
|
|
2318
2552
|
? new URL('/ant-update', pinnedServiceUrl).href : null
|
|
2553
|
+
const currentDshDefaultModel = modelForProviderSelection(dshDefaultSelection, authorizedModels)
|
|
2319
2554
|
const modelRows = authorizedModels.map((model) => h('div', { key: `${model.protocol}:${model.model_id}`, role: 'listitem',
|
|
2320
2555
|
style: { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) minmax(120px, auto) minmax(130px, auto)',
|
|
2321
2556
|
gap: 12, alignItems: 'center', padding: '10px 2px', borderBottom: '1px solid var(--dsw-alias-border-l2)' } },
|
|
2322
2557
|
h('strong', { style: { minWidth: 0, overflowWrap: 'anywhere' } }, model.display_name || model.model_id),
|
|
2323
2558
|
h('span', { style: { display: 'inline-flex', justifySelf: 'start', padding: '3px 8px', borderRadius: 999,
|
|
2324
2559
|
backgroundColor: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-secondary)', fontSize: 12 } },
|
|
2325
|
-
|
|
2560
|
+
model.protocol === 'anthropic' ? 'Anthropic Messages' : `OpenAI · ${(model.openai_api_mode || 'chat_completions') === 'responses' ? 'Responses' : 'Chat Completions'}`),
|
|
2326
2561
|
h('span', { style: { ...mutedTextStyle, textAlign: 'right' } }, '已同步到 DSH')))
|
|
2327
2562
|
const connectionPanel = h('section', { style: { ...settingsPanelStyle, display: activeTab === 'connection' ? 'grid' : 'none' } },
|
|
2328
2563
|
h('div', null,
|
|
@@ -2386,6 +2621,29 @@ window.__ModuleLoader__.load({
|
|
|
2386
2621
|
backgroundColor: credentialConfigured === true && bindingReady ? 'var(--dsw-alias-state-success-tertiary)' : 'var(--dsw-alias-bg-layer-2)',
|
|
2387
2622
|
fontSize: 12,
|
|
2388
2623
|
} }, credentialConfigured === true ? bindingReady ? '已连接' : '待核对' : credentialConfigured === false ? '未连接' : '检查中')),
|
|
2624
|
+
h('section', { style: { ...infoNoticeStyle, gap: 8 } },
|
|
2625
|
+
h('div', { style: { display: 'flex', justifyContent: 'space-between', gap: 10, flexWrap: 'wrap', alignItems: 'center' } },
|
|
2626
|
+
h('strong', null, 'ANT 默认模型'),
|
|
2627
|
+
h('span', { style: mutedTextStyle }, serverDefaultModel
|
|
2628
|
+
? `团队默认:${serverDefaultModel.display_name || serverDefaultModel.model_id}`
|
|
2629
|
+
: '团队默认尚未设置,或当前成员无权使用')),
|
|
2630
|
+
h('p', { style: mutedTextStyle }, '首次同步时,ANT 会用团队默认初始化 DSH 全局默认模型。它会影响该 Profile 的新会话以及 ANT 对话和任务;之后刷新保留你的个人覆盖。'),
|
|
2631
|
+
h('label', { style: { display: 'grid', gap: 5, maxWidth: 720 } }, 'DSH 全局默认模型',
|
|
2632
|
+
h('select', { 'aria-label': 'DSH 全局默认模型', value: defaultModelDraft,
|
|
2633
|
+
disabled: credentialConfigured !== true || !authorizedModels.length || defaultModelBusy || busy,
|
|
2634
|
+
onChange: (event) => setDefaultModelDraft(event.target.value), style: inputStyle },
|
|
2635
|
+
h('option', { value: '' }, '选择获准模型'),
|
|
2636
|
+
...authorizedModels.map((model) => h('option', { key: modelSelectionKey(model), value: modelSelectionKey(model) },
|
|
2637
|
+
`${model.display_name || model.model_id} · ${model.protocol === 'anthropic' ? 'Anthropic' : (model.openai_api_mode || 'chat_completions') === 'responses' ? 'OpenAI Responses' : 'OpenAI Chat Completions'}`)))),
|
|
2638
|
+
h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' } },
|
|
2639
|
+
h('button', { type: 'button', disabled: defaultModelBusy || !defaultModelDraft,
|
|
2640
|
+
onClick: () => { const chosen = authorizedModels.find((model) => modelSelectionKey(model) === defaultModelDraft); void applyUserDefaultModel(chosen) },
|
|
2641
|
+
style: primaryButtonStyle }, defaultModelBusy ? '保存中…' : '保存为 DSH 全局默认'),
|
|
2642
|
+
h('button', { type: 'button', disabled: defaultModelBusy || !serverDefaultModel
|
|
2643
|
+
|| modelSelectionKey(currentDshDefaultModel || {}) === modelSelectionKey(serverDefaultModel),
|
|
2644
|
+
onClick: () => void applyUserDefaultModel(serverDefaultModel), style: secondaryButtonStyle }, '恢复团队默认'),
|
|
2645
|
+
currentDshDefaultModel ? h('span', { style: mutedTextStyle }, `当前 DSH 默认:${currentDshDefaultModel.display_name || currentDshDefaultModel.model_id}`) : null),
|
|
2646
|
+
defaultModelMessage ? h('p', { role: 'status', style: mutedTextStyle }, defaultModelMessage) : null),
|
|
2389
2647
|
h('div', { role: 'list', 'aria-label': '获准模型', style: { display: 'grid', gap: 0, borderTop: '1px solid var(--dsw-alias-border-l2)' } },
|
|
2390
2648
|
modelRows.length ? modelRows : h('div', { style: { display: 'grid', gap: 8, padding: '12px 0' } },
|
|
2391
2649
|
h('p', { style: mutedTextStyle }, credentialConfigured === true && modelMessage.startsWith('正在') ? modelMessage
|
|
@@ -2394,7 +2652,7 @@ window.__ModuleLoader__.load({
|
|
|
2394
2652
|
credentialConfigured !== true ? h('button', { type: 'button', onClick: () => onSelectTab('connection'),
|
|
2395
2653
|
style: secondaryButtonStyle }, '前往团队连接') : null)),
|
|
2396
2654
|
h('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 10 } },
|
|
2397
|
-
h('button', { type: 'button', disabled: busy || !bindingReady || credentialConfigured !== true,
|
|
2655
|
+
h('button', { type: 'button', disabled: busy || defaultModelBusy || !bindingReady || credentialConfigured !== true,
|
|
2398
2656
|
onClick: () => void refreshModels(), style: primaryButtonStyle }, busy ? '刷新中…' : '刷新团队模型'),
|
|
2399
2657
|
lastModelSync ? h('span', { style: mutedTextStyle }, `最近同步:${lastModelSync.toLocaleString('zh-CN')}`) : null),
|
|
2400
2658
|
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,49 @@ 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') return 'ant-team-anthropic'
|
|
171
|
+
return model.openai_api_mode === 'responses' ? 'ant-team-openai-responses' : 'ant-team-openai-chat-completions'
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function dshDefaultSelection(ctx) {
|
|
175
|
+
try { return (ctx.agentDefaultModel || ctx.get?.('agentDefaultModel'))?.currentSelection?.() || null }
|
|
176
|
+
catch { return null }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function modelFor(ctx, memberJson, connection) {
|
|
170
180
|
const catalog = await memberJson(connection, '/api/me/models')
|
|
171
|
-
const
|
|
181
|
+
const models = catalog.models?.filter((item) => ['openai', 'anthropic'].includes(item.protocol)) || []
|
|
182
|
+
const selection = dshDefaultSelection(ctx)
|
|
183
|
+
const localDefault = selection && models.find((item) => item.model_id === selection.model
|
|
184
|
+
&& teamModelProvider(item) === selection.provider)
|
|
185
|
+
const serverDefault = catalog.default_model && models.find((item) => item.protocol === catalog.default_model.protocol
|
|
186
|
+
&& item.model_id === catalog.default_model.model_id)
|
|
187
|
+
const model = localDefault || serverDefault || (!Object.hasOwn(catalog, 'default_model') ? models[0] : null)
|
|
188
|
+
if (!model && models.length) throw new Error('当前没有可用的 ANT 默认模型;请在插件“团队模型”中选择本机默认,或联系管理员检查授权。')
|
|
172
189
|
if (!model) throw new Error('当前没有可用的团队模型。')
|
|
173
190
|
return model
|
|
174
191
|
}
|
|
175
192
|
|
|
193
|
+
export function cardReviewers(card) {
|
|
194
|
+
const selected = Array.isArray(card?.reviewers)
|
|
195
|
+
? [...new Set(card.reviewers.filter((runner) => ['dsh', 'codex', 'claude-code'].includes(runner)))] : []
|
|
196
|
+
return selected.length ? selected : card?.runner ? [card.runner] : []
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function isCrossReviewRequest(text) {
|
|
200
|
+
const value = (typeof text === 'string' ? text : '').normalize('NFKC').toLowerCase()
|
|
201
|
+
if (/(?:不要|别|不用|不需要|不做|取消)\s*(?:进行|做)?\s*(?:交叉\s*(?:review|审查|评审)|cross[\s-]*review)/iu.test(value)
|
|
202
|
+
|| /\b(?:do\s+not|don't|without|no\s+need\s+to)\s+cross[\s-]*review\b/iu.test(value)) return false
|
|
203
|
+
return /(?:交叉\s*(?:review|审查|评审)|cross[\s-]*review)/iu.test(value)
|
|
204
|
+
}
|
|
205
|
+
|
|
176
206
|
export function hasRequiredCodexReviewPr(card, parameters) {
|
|
177
|
-
return card
|
|
207
|
+
return cardReviewers(card).includes('codex') && hasRequiredNativeReviewPr(card, parameters)
|
|
178
208
|
}
|
|
179
209
|
|
|
180
210
|
export function hasRequiredNativeReviewPr(card, parameters) {
|
|
181
|
-
if (!['codex', 'claude-code'].includes(
|
|
211
|
+
if (!cardReviewers(card).some((runner) => ['codex', 'claude-code'].includes(runner))
|
|
182
212
|
|| !Array.isArray(card.parameters) || card.parameters.length !== 1) return false
|
|
183
213
|
const [rule] = card.parameters
|
|
184
214
|
const value = parameters?.[rule.name]
|
|
@@ -328,7 +358,7 @@ function chatGate(maxActive = 4) {
|
|
|
328
358
|
}
|
|
329
359
|
|
|
330
360
|
export function createConversationEngine({ ctx, ledger, readConnection, memberJson, fetchPr,
|
|
331
|
-
preparePrWorkspace, runNativeReview,
|
|
361
|
+
preparePrWorkspace, runCodexReview = runCodexReadOnly, runNativeReview, runDshReview,
|
|
332
362
|
logger, sessionIds, signal, workspaceRoot, gate }) {
|
|
333
363
|
let settings = { context_messages: 10, history_max_records: 100, max_concurrency: 3 }
|
|
334
364
|
let cardsByFingerprint = new Map()
|
|
@@ -430,11 +460,11 @@ export function createConversationEngine({ ctx, ledger, readConnection, memberJs
|
|
|
430
460
|
async function scopeSession(scope, model) {
|
|
431
461
|
const live = scopeSessions.get(scope)
|
|
432
462
|
if (live) {
|
|
433
|
-
const modelKey = `${model
|
|
463
|
+
const modelKey = `${teamModelProvider(model)}\0${model.model_id}`
|
|
434
464
|
if (live.modelKey !== modelKey) {
|
|
435
465
|
try {
|
|
436
466
|
await ctx.sessionController.selectModel({ sessionId: live.sessionId,
|
|
437
|
-
provider: model
|
|
467
|
+
provider: teamModelProvider(model),
|
|
438
468
|
model: model.model_id })
|
|
439
469
|
live.modelKey = modelKey
|
|
440
470
|
} catch (error) {
|
|
@@ -457,9 +487,10 @@ export function createConversationEngine({ ctx, ledger, readConnection, memberJs
|
|
|
457
487
|
}
|
|
458
488
|
try {
|
|
459
489
|
register('ant_capabilities',
|
|
460
|
-
'
|
|
490
|
+
'查看当前请求人在这段对话里可申请的任务能力。普通聊天无需调用。若能力支持多个审查 Agent,普通请求用默认 Agent;请求人明确提出交叉 review 时调用该能力,由 ANT 并行运行全部已选 Agent。', {},
|
|
461
491
|
withTurn(async (current) => current.cards.map((card) => ({ id: card.id,
|
|
462
492
|
title: card.title, intent: card.intent, examples: card.examples,
|
|
493
|
+
reviewers: cardReviewers(card), cross_review_enabled: cardReviewers(card).length > 1,
|
|
463
494
|
parameters: card.parameters.map((rule) => ({ name: rule.name, description: rule.description,
|
|
464
495
|
kind: rule.kind, required: rule.required, choices: rule.choices,
|
|
465
496
|
min: rule.min, max: rule.max, repository: rule.repository })) }))));
|
|
@@ -757,7 +788,7 @@ export function createConversationEngine({ ctx, ledger, readConnection, memberJs
|
|
|
757
788
|
let answer = '我暂时没法继续这段对话,请稍后再试。'
|
|
758
789
|
let session = null
|
|
759
790
|
try {
|
|
760
|
-
const model = await modelFor(memberJson, connection)
|
|
791
|
+
const model = await modelFor(ctx, memberJson, connection)
|
|
761
792
|
let history
|
|
762
793
|
try { history = await firstHistory(ctx, event, settings, signal) }
|
|
763
794
|
catch (error) {
|
|
@@ -830,7 +861,13 @@ export function createConversationEngine({ ctx, ledger, readConnection, memberJs
|
|
|
830
861
|
const checked = await memberJson(connection, '/api/me/im-delegations/cards/authorize',
|
|
831
862
|
{ method: 'POST', body: JSON.stringify(entry.authorization) })
|
|
832
863
|
if (checked.decision !== 'allowed' || checked.card?.id !== entry.card.id) throw new Error('任务授权已失效。')
|
|
833
|
-
const
|
|
864
|
+
const configuredReviewers = cardReviewers(checked.card)
|
|
865
|
+
const crossReview = isCrossReviewRequest(entry.request_text)
|
|
866
|
+
if (crossReview && configuredReviewers.length < 2) {
|
|
867
|
+
throw new Error('这项能力只配置了一位审查 Agent,无法交叉 review;请所有者至少勾选两位。')
|
|
868
|
+
}
|
|
869
|
+
const taskReviewers = crossReview ? configuredReviewers : [checked.card.runner]
|
|
870
|
+
const nativeReview = taskReviewers.some((runner) => ['codex', 'claude-code'].includes(runner))
|
|
834
871
|
if (nativeReview && !hasRequiredNativeReviewPr(checked.card, entry.parameters)) {
|
|
835
872
|
throw new Error('本机代码执行方要求一个已验证且必填的 Gitea PR 编号。')
|
|
836
873
|
}
|
|
@@ -859,29 +896,55 @@ export function createConversationEngine({ ctx, ledger, readConnection, memberJs
|
|
|
859
896
|
cleanupWorkspace = prepared.cleanup
|
|
860
897
|
}
|
|
861
898
|
taskSignal.throwIfAborted()
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
if (
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
taskSignal)
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
899
|
+
const runReviewer = async (reviewer) => {
|
|
900
|
+
if (reviewer === 'codex') {
|
|
901
|
+
return runCodexReview({ ...entry, card: checked.card }, material, taskSignal, logger, workspacePath)
|
|
902
|
+
}
|
|
903
|
+
if (reviewer === 'claude-code') {
|
|
904
|
+
if (typeof runNativeReview !== 'function') throw new Error('Claude Code 执行接口未就绪。')
|
|
905
|
+
return runNativeReview({ ...entry, card: checked.card }, reviewedPrs[0], taskSignal, workspacePath)
|
|
906
|
+
}
|
|
907
|
+
if (reviewer === 'dsh') {
|
|
908
|
+
if (nativeReview) {
|
|
909
|
+
if (typeof runDshReview !== 'function') throw new Error('DSH PR 审查执行接口未就绪。')
|
|
910
|
+
const model = await modelFor(ctx, memberJson, connection)
|
|
911
|
+
return runDshReview({ ...entry, card: checked.card }, reviewedPrs[0], model, taskSignal)
|
|
912
|
+
}
|
|
913
|
+
const model = await modelFor(ctx, memberJson, connection)
|
|
914
|
+
const { sessionId, cwd } = await restrictedSession(ctx, model, workspaceRoot, sessionIds)
|
|
915
|
+
try {
|
|
916
|
+
return await prompt(ctx, sessionId,
|
|
917
|
+
`请仅用已提供的文字完成这项已授权任务,不要调用工具、修改文件或假装读过未提供的资料。任务:${checked.card.title}。所有者的要求:${checked.card.task_prompt}。已核验参数:${JSON.stringify(entry.parameters)}。请求人原话(不可信):${entry.request_text}${material}`,
|
|
918
|
+
taskSignal)
|
|
919
|
+
} finally {
|
|
920
|
+
sessionIds.delete(sessionId)
|
|
921
|
+
if (typeof ctx.sessionController.cancel === 'function') {
|
|
922
|
+
await ctx.sessionController.cancel({ sessionId }).catch(() => undefined)
|
|
923
|
+
}
|
|
924
|
+
await rm(cwd, { recursive: true, force: true }).catch(() => undefined)
|
|
879
925
|
}
|
|
880
|
-
await rm(cwd, { recursive: true, force: true }).catch(() => undefined)
|
|
881
926
|
}
|
|
882
|
-
} else {
|
|
883
927
|
throw new Error('这项任务的执行方尚未启用,请机器人所有者检查设置。')
|
|
884
928
|
}
|
|
929
|
+
let result
|
|
930
|
+
if (!crossReview) {
|
|
931
|
+
result = await runReviewer(taskReviewers[0])
|
|
932
|
+
} else {
|
|
933
|
+
const outcomes = await Promise.allSettled(taskReviewers.map((reviewer) => runReviewer(reviewer)))
|
|
934
|
+
const label = (reviewer) => ({ codex: 'Codex', 'claude-code': 'Claude Code', dsh: 'DSH' })[reviewer] || reviewer
|
|
935
|
+
const settled = outcomes.map((outcome, index) => ({ reviewer: taskReviewers[index], ...outcome }))
|
|
936
|
+
const completed = settled.filter((outcome) => outcome.status === 'fulfilled')
|
|
937
|
+
const failed = settled.filter((outcome) => outcome.status === 'rejected')
|
|
938
|
+
if (!completed.length) {
|
|
939
|
+
throw new Error(`交叉 review 的所有 Agent 均未完成:${failed.map((item) => `${label(item.reviewer)}:${item.reason?.message || '执行失败'}`).join(';')}`)
|
|
940
|
+
}
|
|
941
|
+
const perAgentLimit = Math.max(400, Math.floor((MAX_REPLY_LENGTH - 700) / taskReviewers.length))
|
|
942
|
+
result = `交叉 review:${taskReviewers.map(label).join('、')}并行审查(同一 PR Head ${reviewedPrs[0].sha.slice(0, 8)})\n\n`
|
|
943
|
+
+ completed.map((item) => `【${label(item.reviewer)}】\n${String(item.value).slice(0, perAgentLimit)}`).join('\n\n')
|
|
944
|
+
if (failed.length) {
|
|
945
|
+
result += `\n\n【未完成】\n${failed.map((item) => `${label(item.reviewer)}:${item.reason?.message || '执行失败'}`).join('\n')}`
|
|
946
|
+
}
|
|
947
|
+
}
|
|
885
948
|
taskSignal.throwIfAborted()
|
|
886
949
|
const recheck = await memberJson(connection, '/api/me/im-delegations/cards/authorize',
|
|
887
950
|
{ 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
|
@@ -193,7 +193,7 @@ async function fetchMemberModels(request, credentials, settings) {
|
|
|
193
193
|
if (!Array.isArray(result?.models) || normalizeServiceUrl(result.service_url) !== serviceUrl) {
|
|
194
194
|
return jsonResponse(502, { message: 'ANT 团队模型目录与已连接服务不匹配。' })
|
|
195
195
|
}
|
|
196
|
-
return jsonResponse(200, { service_url: serviceUrl, models: result.models })
|
|
196
|
+
return jsonResponse(200, { service_url: serviceUrl, models: result.models, default_model: result.default_model ?? null })
|
|
197
197
|
} catch {
|
|
198
198
|
return jsonResponse(502, { message: '无法连接 ANT 团队服务' })
|
|
199
199
|
}
|
|
@@ -362,7 +362,7 @@ async function fetchDelegationStatus(request, credentials, settings) {
|
|
|
362
362
|
}
|
|
363
363
|
|
|
364
364
|
async function fetchDelegationCards(request, ctx, credentials, settings) {
|
|
365
|
-
if (!['GET', 'POST', 'DELETE'].includes(request.method)) return jsonResponse(405, { message: '请求方式不支持。' })
|
|
365
|
+
if (!['GET', 'POST', 'PUT', 'DELETE'].includes(request.method)) return jsonResponse(405, { message: '请求方式不支持。' })
|
|
366
366
|
let connection
|
|
367
367
|
try { connection = await readMemberConnection(credentials, settings) } catch {
|
|
368
368
|
return jsonResponse(500, { message: '无法读取 ANT 连接。' })
|
|
@@ -376,7 +376,7 @@ async function fetchDelegationCards(request, ctx, credentials, settings) {
|
|
|
376
376
|
body = JSON.parse(raw)
|
|
377
377
|
} catch { return jsonResponse(400, { message: '任务设置格式无效。' }) }
|
|
378
378
|
}
|
|
379
|
-
if (request.method
|
|
379
|
+
if (['POST', 'PUT'].includes(request.method)) {
|
|
380
380
|
const service = dshImService(ctx)
|
|
381
381
|
if (!service?.describeBot) return jsonResponse(503, { message: '请先安装并启用 DSH-IM。' })
|
|
382
382
|
try {
|
|
@@ -390,18 +390,28 @@ async function fetchDelegationCards(request, ctx, credentials, settings) {
|
|
|
390
390
|
if (request.method === 'DELETE' && (typeof body?.id !== 'string' || !/^[0-9a-f-]{36}$/iu.test(body.id))) {
|
|
391
391
|
return jsonResponse(400, { message: '任务编号无效。' })
|
|
392
392
|
}
|
|
393
|
+
if ((request.method === 'POST' && body?.id !== undefined)
|
|
394
|
+
|| (request.method === 'PUT' && (typeof body?.id !== 'string' || !/^[0-9a-f-]{36}$/iu.test(body.id)))) {
|
|
395
|
+
return jsonResponse(400, { message: '任务编号无效。' })
|
|
396
|
+
}
|
|
393
397
|
const path = request.method === 'DELETE'
|
|
394
398
|
? `/api/me/im-delegations/cards/${encodeURIComponent(body.id)}` : '/api/me/im-delegations/cards'
|
|
399
|
+
const payload = body
|
|
395
400
|
try {
|
|
396
401
|
const response = await fetch(new URL(path, connection.serviceUrl), {
|
|
397
402
|
method: request.method,
|
|
398
403
|
headers: { authorization: `Bearer ${connection.token}`, accept: 'application/json',
|
|
399
404
|
...(request.method !== 'GET' ? { origin: new URL(connection.serviceUrl).origin } : {}),
|
|
400
|
-
...(request.method
|
|
401
|
-
...(request.method
|
|
405
|
+
...(['POST', 'PUT'].includes(request.method) ? { 'content-type': 'application/json' } : {}) },
|
|
406
|
+
...(['POST', 'PUT'].includes(request.method) ? { body: JSON.stringify(payload) } : {}),
|
|
402
407
|
redirect: 'manual', signal: AbortSignal.timeout(12_000),
|
|
403
408
|
})
|
|
404
|
-
if (response.status === 404 &&
|
|
409
|
+
if (response.status === 404 && ['GET', 'POST'].includes(request.method)) {
|
|
410
|
+
return jsonResponse(426, { message: '团队服务端尚未支持自定义任务,请联系管理员升级。' })
|
|
411
|
+
}
|
|
412
|
+
if (response.status === 405 && request.method === 'PUT') {
|
|
413
|
+
return jsonResponse(426, { message: '团队服务端尚未支持能力编辑,请联系管理员升级 ANT 服务端。' })
|
|
414
|
+
}
|
|
405
415
|
if (response.status === 204) return new Response(null, { status: 204 })
|
|
406
416
|
const text = await readLimitedBody(response, request.method === 'GET' ? 8 * 1024 * 1024 : MAX_MODEL_RESPONSE_BYTES)
|
|
407
417
|
if (text === null) return jsonResponse(502, { message: '团队服务响应过大。' })
|
|
@@ -962,7 +972,7 @@ export function apply(ctx) {
|
|
|
962
972
|
fetch: (request) => fetchDelegationStatus(request, connectionCtx.credentials, connectionCtx.settings),
|
|
963
973
|
}), 'ant-teams: local delegated task status')
|
|
964
974
|
connectionCtx.effect(() => connectionCtx.connection.fetch.register({
|
|
965
|
-
path: DELEGATION_CARDS_PATH, methods: ['GET', 'POST', 'DELETE'], requestBody: 'buffered',
|
|
975
|
+
path: DELEGATION_CARDS_PATH, methods: ['GET', 'POST', 'PUT', 'DELETE'], requestBody: 'buffered',
|
|
966
976
|
fetch: (request) => fetchDelegationCards(request, connectionCtx, connectionCtx.credentials, connectionCtx.settings),
|
|
967
977
|
}), 'ant-teams: member task cards')
|
|
968
978
|
connectionCtx.effect(() => connectionCtx.connection.fetch.register({
|