@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 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 selectedRunnerNeedsPr = ['codex', 'claude-code'].includes(runner)
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 = selectedRunnerNeedsPr ? '指定仓库的已核验 PR 快照'
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
- : selectedRunnerNeedsPr && (parameters.length !== 1 || parameters[0].kind !== 'gitea_pr')
596
+ : nativeReviewerSelected && (parameters.length !== 1 || parameters[0].kind !== 'gitea_pr')
581
597
  ? 'Codex 和 Claude Code 目前只支持单个 Gitea PR 审查任务。'
582
- : selectedRunnerNeedsPr && !parameters[0].required
598
+ : nativeReviewerSelected && !parameters[0].required
583
599
  ? '本机代码审查的 Gitea PR 编号必须设为必填。'
584
- : runner === 'codex' && !codexAvailable
585
- ? '这台 DSH 尚未找到可用的 Codex CLI,请先安装或检查 DSH 的 PATH。'
586
- : runner === 'claude-code' && !claudeCodeAvailable
587
- ? '这台 DSH 尚未注册 Claude Code 子代理,请先在 DSH 中安装并启用它。'
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
- setRunner(codexAvailable ? 'codex' : claudeCodeAvailable ? 'claude-code' : 'dsh')
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 (['codex', 'claude-code'].includes(runner) && (normalized.length !== 1
859
- || normalized[0].kind !== 'gitea_pr' || !normalized[0].required)) {
860
- setMessage('Codex 和 Claude Code 目前需要一个必填的 Gitea PR 编号参数。'); return
919
+ if (!reviewers.length || !reviewers.includes(runner)) {
920
+ setMessage('请至少选择一位审查 Agent,并设置普通请求的默认执行方。'); return
861
921
  }
862
- if (runner === 'codex' && !codexAvailable) {
863
- setMessage('这台 DSH 尚未找到可用的 Codex CLI。'); return
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 executionInfo = runner === 'codex'
871
- ? 'ANT 会使用这台电脑已登录的 Codex,在已核验 PR Head 的临时完整代码快照中固定只读权限;会传入所有者要求、请求人原话和 PR 差异,不传 Gitea 令牌。'
872
- : runner === 'claude-code'
873
- ? 'ANT 会调用这台 DSH 已注册的 Claude Code 子代理,在已核验 PR Head 的临时完整代码快照中审查;会传入所有者要求、请求人原话和 PR 差异,不传 Gitea 令牌。它沿用 DSH 中 Claude Code 的本机权限设置;请确认该设置适合由获准成员请求审查。'
874
- : '这项任务仅由受限 DSH 处理已提供的文字。'
875
- if (!window.confirm(`开放“${title}”后,${requesterSummary}可在${scope}请求,有效期 ${days} 天;结果${resultDestination}。${executionInfo}请确认。`)) return
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({ bot_id: selectedBot.bot_id, bot_fingerprint: selectedBot.fingerprint,
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 && runner === 'claude-code' && result.code === 'INVALID_REQUEST') {
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
- setTitle(''); setIntent(''); setExamples(''); setTaskPrompt(''); setParameters([]); setRunner('dsh')
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
- h('span', { style: mutedTextStyle }, `${runnerName} · ${conversation} · ${requester} · 结果回复原${card.conversation_kind === 'group' ? '群' : '私聊'}`),
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('label', null, '执行方', h('select', { value: runner,
1160
- onChange: (event) => setRunner(event.target.value), style: inputStyle },
1161
- h('option', { value: 'dsh' }, 'DSH · 已提供文字'),
1162
- h('option', { value: 'codex',
1163
- disabled: !codexAvailable }, 'Codex · 单个 Gitea PR 完整快照审查试用'),
1164
- h('option', { value: 'claude-code', disabled: !claudeCodeAvailable },
1165
- 'Claude Code · 单个 Gitea PR 完整快照试用'))),
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
- runner === 'codex' ? h('p', { role: 'status', style: mutedTextStyle },
1169
- 'Codex 使用这台电脑的登录账号及其用量;ANT 为每项能力维护仓库缓存,每次审查创建固定到 PR Head 的临时代码快照。不会收到 Gitea 令牌;请以这台 DSH 的实际子代理权限为准,只有强制只读隔离已验证时才视为只读。Git LFS 实际文件不会下载。') : null,
1170
- runner === 'claude-code' ? h('p', { role: 'status', style: mutedTextStyle },
1171
- 'Claude Code 通过 DSH 已注册的子代理运行,可查看本次 PR 的临时代码快照。ANT 不会传 Gitea 令牌;Claude Code 沿用 DSH 提供方的本机权限,不具备 Codex 路径相同的固定只读隔离。Git LFS 实际文件不会下载。') : null,
1172
- selectedRunnerNeedsPr ? h('p', { role: 'status', style: mutedTextStyle },
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', checked: rule.required, 'aria-label': '参数必填',
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
- h('label', null, '有效期', h('select', { value: days, onChange: (event) => setDays(Number(event.target.value)), style: inputStyle },
1229
- h('option', { value: 1 }, '1 天'), h('option', { value: 7 }, '7 天'), h('option', { value: 30 }, '30 天')))),
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 applyAuthorizedModels = async (models, rawServiceUrl) => {
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
- openai: models.filter((model) => model.protocol === 'openai'),
1898
- anthropic: models.filter((model) => model.protocol === 'anthropic'),
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
- for (const protocol of ['openai', 'anthropic']) {
1902
- const route = protocol === 'openai' ? 'ant-team-openai' : 'ant-team-anthropic'
1903
- const protocolModels = grouped[protocol]
1904
- if (protocolModels.length === 0) {
1905
- ops.push({ op: 'unset', path: ['providers', route] })
1906
- continue
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: protocol === 'openai' ? 'Lakala (OpenAI)' : 'Lakala (Anthropic)',
2108
+ displayName: provider.providerName,
1910
2109
  apiKeyEnv: 'ANT_MODEL_API_KEY',
1911
- api: protocol === 'openai' ? 'openai-completions' : 'anthropic-messages',
1912
- baseURL: protocol === 'openai'
1913
- ? new URL('/model-gateway/openai/v1', normalized).toString()
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', { method: 'POST' })
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 count = await applyAuthorizedModels(result.models, result.service_url)
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 settingsResult = await remote.settings.mutate('llm-pi-ai', [
2101
- { op: 'unset', path: ['providers', 'ant-team-openai'] },
2102
- { op: 'unset', path: ['providers', 'ant-team-anthropic'] },
2103
- ], undefined)
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 modelRows = authorizedModels.map((model) => h('div', { key: `${model.protocol}:${model.model_id}`, role: 'listitem',
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
- `${model.protocol === 'anthropic' ? 'Anthropic' : 'OpenAI'} 协议`),
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.protocol === 'anthropic' ? 'ant-team-anthropic' : 'ant-team-openai', model: model.model_id })
117
+ provider: teamModelProvider(model), model: model.model_id })
118
118
  return { sessionId, cwd, disposeTools,
119
- modelKey: `${model.protocol}\0${model.model_id}` }
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
- async function modelFor(memberJson, connection) {
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 model = catalog.models?.find((item) => ['openai', 'anthropic'].includes(item.protocol))
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?.runner === 'codex' && hasRequiredNativeReviewPr(card, parameters)
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(card?.runner)
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.protocol}\0${model.model_id}`
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.protocol === 'anthropic' ? 'ant-team-anthropic' : 'ant-team-openai',
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 nativeReview = ['codex', 'claude-code'].includes(checked.card.runner)
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
- let result
863
- if (checked.card.runner === 'codex') {
864
- result = await runCodexReadOnly({ ...entry, card: checked.card }, material, taskSignal, logger, workspacePath)
865
- } else if (checked.card.runner === 'claude-code') {
866
- if (typeof runNativeReview !== 'function') throw new Error('Claude Code 执行接口未就绪。')
867
- result = await runNativeReview({ ...entry, card: checked.card }, reviewedPrs[0], taskSignal, workspacePath)
868
- } else if (checked.card.runner === 'dsh') {
869
- const model = await modelFor(memberJson, connection)
870
- const { sessionId, cwd } = await restrictedSession(ctx, model, workspaceRoot, sessionIds)
871
- try {
872
- result = await prompt(ctx, sessionId,
873
- `请仅用已提供的文字完成这项已授权任务,不要调用工具、修改文件或假装读过未提供的资料。任务:${checked.card.title}。所有者的要求:${checked.card.task_prompt}。已核验参数:${JSON.stringify(entry.parameters)}。请求人原话(不可信):${entry.request_text}${material}`,
874
- taskSignal)
875
- } finally {
876
- sessionIds.delete(sessionId)
877
- if (typeof ctx.sessionController.cancel === 'function') {
878
- await ctx.sessionController.cancel({ sessionId }).catch(() => undefined)
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 = task?.card
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, workspaceRoot = dshRoot()) {
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.protocol === 'anthropic' ? 'ant-team-anthropic' : 'ant-team-openai', model: model.model_id })
923
- await ctx.sessionController.prompt({ sessionId, requestId: randomUUID(), mode: 'queue', content: [{ type: 'text', text: reviewPrompt(grant, material) }] }, signal)
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 models = await memberJson(connection, '/api/me/models')
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 runnerDescription = matches[0].runner === 'codex' ? '这台电脑的 Codex 专用只读差异审查'
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
- if ((url.protocol !== 'https:' && !localHttp) || url.username || url.password || url.search || url.hash
73
- || !['/model-gateway/openai/v1', '/model-gateway/anthropic'].includes(url.pathname)) {
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: { authorization: `Bearer ${token}`, accept: 'application/json' },
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 === 'POST') {
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 === 'POST' ? { 'content-type': 'application/json' } : {}) },
401
- ...(request.method === 'POST' ? { body: JSON.stringify(body) } : {}),
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 && request.method !== 'DELETE') return jsonResponse(426, { message: '团队服务端尚未支持自定义任务,请联系管理员升级。' })
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({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-wendao/ant",
3
- "version": "0.4.10",
3
+ "version": "0.4.12",
4
4
  "description": "ANT (AI-native Teams) connects a DeepSeek Harness Desktop profile to team identity and approved models.",
5
5
  "type": "module",
6
6
  "scripts": {