sintbot 1.4.15 → 1.4.17

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.
@@ -970,6 +970,28 @@ button.btn:not(:disabled):active{transform:translateY(1px)}
970
970
  @media(max-width:430px){nav{margin:0 0 22px;padding:4px;overflow-x:auto;border-radius:12px}nav button:not(.navplay){padding:10px 11px}.grid{gap:12px}.card{padding:16px}h2.sec{margin:24px 0 14px}.modal{padding:20px 16px}}
971
971
  @media(prefers-reduced-motion:reduce){button.btn:not(:disabled):hover,button.btn:not(:disabled):active{transform:none}}
972
972
 
973
+
974
+ /* Feishu setup: a short connection summary, then optional help. */
975
+ .feishu-heading{display:flex;justify-content:space-between;align-items:flex-start;gap:18px}
976
+ .feishu-heading .lbl{display:flex;align-items:center;gap:8px;margin-bottom:10px}
977
+ .feishu-title{font-size:20px;font-weight:600;line-height:1.4;overflow-wrap:anywhere}
978
+ .feishu-current{display:flex;align-items:center;gap:10px 18px;flex-wrap:wrap;font-size:12px;margin:18px 0;color:var(--muted)}
979
+ .feishu-current strong{font-weight:500;color:var(--ink)}
980
+ .feishu-actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:14px 0}
981
+ .feishu-actions .btn{display:inline-flex;align-items:center;justify-content:center;text-decoration:none;min-height:40px}
982
+ .feishu-actions input{flex:1;min-width:180px}
983
+ .feishu-details{border-top:1px solid var(--line);margin-top:18px;padding-top:4px}
984
+ .feishu-details>summary{cursor:pointer;padding:14px 0;font-size:13px;font-weight:500;min-height:44px}
985
+ .feishu-steps{padding-left:22px;margin:8px 0 18px;display:grid;gap:16px;font-size:13px}
986
+ .feishu-steps li{padding-left:6px;line-height:1.7}
987
+ .feishu-steps b{display:block;font-weight:600;color:var(--ink)}
988
+ .feishu-steps span{display:block;color:var(--muted)}
989
+ .feishu-help{font-size:12px;line-height:1.8;margin:8px 0 16px}
990
+ .feishu-help dt{font-weight:600;margin-top:13px}
991
+ .feishu-help dd{margin:3px 0;color:var(--muted);overflow-wrap:anywhere}
992
+ .feishu-uri{overflow-wrap:anywhere;word-break:break-word;flex:1;min-width:0}
993
+ @media(max-width:600px){.feishu-heading{gap:10px}.feishu-title{font-size:18px}.feishu-actions{align-items:stretch}.feishu-actions .btn{min-height:44px}.feishu-actions input{flex-basis:100%}.feishu-current{gap:8px 12px}}
994
+
973
995
  </style>
974
996
  <script>/* 防 FOUC:首屏绘制前定主题(默认 light;用户切换后按 localStorage)与界面语言
975
997
  (存过就按 localStorage,没存过跟随浏览器:zh* ⇒ 中文,其余 ⇒ 英文)*/
@@ -1562,15 +1584,24 @@ function claudeModelOptions(list,cur){
1562
1584
  return list.map(m=>`<option value="${esc(m.id)}" ${m.id===(cur||'')?'selected':''}>${esc(m.name)}${m.note?' — '+esc(m.note):''}${m.extra?' ★':''}</option>`)
1563
1585
  .concat(`<option value="__custom__" ${known?'':'selected'}>自定义…</option>`).join('');
1564
1586
  }
1587
+ // 环境冲突(B5):守护进程 env / ~/.claude/settings.json / ~/.codex/config.toml 里可能改接入点或型号的键。
1588
+ // 服务端只给键名与一句原因(核心 env_conflicts),这里只列、不改 —— buildEnv 清什么照旧。
1589
+ function envWarnHtml(list){
1590
+ if(!Array.isArray(list)||!list.length)return '';
1591
+ return `<div style="margin-top:5px;font-size:11.5px;color:var(--warn)" title="这些设置在 SintBot 之外,可能改变 CLI 的接入点或型号;只列键名,不含值">环境冲突:${list.map(w=>`<div><span class="mono" data-i18n-skip>${esc(w.key)}</span> — ${esc(w.reason)}</div>`).join('')}</div>`;
1592
+ }
1593
+ // codex 卡逐 provider 取(服务端按 baseUrl/key 各算一份):r = /api/codex/verify 的响应或 S.codexStatus。
1594
+ const cxEnvWarn=(r,name)=>(((r&&r.envWarningsByProvider)||{})[name])||[];
1565
1595
  // Claude 登录态区的内容(与卡片 chip 同一份 S.claudeStatus,措辞不各写一套)
1566
1596
  function clStatusHtml(){
1567
1597
  const s=S.claudeStatus;
1568
1598
  if(!s) return '<span class="faint">未检测 — 点下方按钮</span>';
1569
1599
  if(!s.ok) return `<span style="color:var(--warn)">未验证</span><div class="faint" style="margin-top:4px;font-size:11.5px">${esc(s.error||'')}</div>`;
1570
- if(!s.loggedIn) return '<span style="color:var(--fail)">'+IC.x+' 未登录</span><div class="faint" style="margin-top:4px;font-size:11.5px">在这台机器上跑 <span class="mono">claude auth login</span></div>';
1600
+ if(!s.loggedIn) return '<span style="color:var(--fail)">'+IC.x+' 未登录</span><div class="faint" style="margin-top:4px;font-size:11.5px">在这台机器上跑 <span class="mono">claude auth login</span></div>'+envWarnHtml(s.envWarnings);
1571
1601
  return `<span style="color:var(--ok);font-weight:600">${IC.check} 已登录${s.plan?' · '+esc(s.plan):''}</span>`
1572
1602
  +`<div class="faint" style="margin-top:4px;font-size:11.5px">${esc(s.email||'')}${s.org?' · '+esc(s.org):''}${s.method?' · '+esc(s.method):''}</div>`
1573
- +(s.usage?`<div style="margin-top:5px">额度:<span style="color:var(--ok);font-weight:600" title="百分比 = 已用,括号 = 距重置 —— 与这台机器上交互式 Claude Code 共享的订阅窗口">${esc(s.usage)}</span></div>`:'');
1603
+ +(s.usage?`<div style="margin-top:5px">额度:<span style="color:var(--ok);font-weight:600" title="百分比 = 已用,括号 = 距重置 —— 与这台机器上交互式 Claude Code 共享的订阅窗口">${esc(s.usage)}</span></div>`:'')
1604
+ +envWarnHtml(s.envWarnings);
1574
1605
  }
1575
1606
  // codex 推理强度(-c model_reasoning_effort);''=默认(模型内置)。
1576
1607
  // RE_LEGAL = 服务端真正接受的合法集,2026-07-20 从 API 参数校验报错里逼出来的权威清单:
@@ -1607,10 +1638,10 @@ function fmtReset(w){if(!w||w.resetsAt==null)return'';const ms=w.resetsAt>1e9?(w
1607
1638
  function winLabel(mins){if(!mins)return'窗口';if(mins>=1440){const d=Math.round(mins/1440);return d===7?'7d':d+'d';}return Math.round(mins/60)+'h';}
1608
1639
  function fmtUsage(u){if(!u)return'';const f=(w)=>{if(!w)return'';const r=fmtReset(w);return `${winLabel(w.windowMins)}:${Math.min(100,Math.max(0,Math.round(w.usedPercent||0)))}%${r?` (${r})`:''}`;};return [f(u.primary),f(u.secondary)].filter(Boolean).join(' │ ');}
1609
1640
  // 页面加载时从 /api/state 缓存回填 codex 登录态+额度 → 刷新不丢(客户端已有则不覆盖,避免盖掉刚验证的)。
1610
- function seedCodexStatus(){if(!S||!S.codexStatus)return;const lg=S.codexStatus.login||{},usageText=fmtUsage(S.codexStatus.usage);(S.providers||[]).filter(p=>p.runner==='codex').forEach(p=>{if(!codexStatus[p.name])codexStatus[p.name]={loggedIn:lg.loggedIn,mode:lg.mode,dead:lg.dead,verified:lg.verified,text:lg.text,usageText};});}
1641
+ function seedCodexStatus(){if(!S||!S.codexStatus)return;const lg=S.codexStatus.login||{},usageText=fmtUsage(S.codexStatus.usage);(S.providers||[]).filter(p=>p.runner==='codex').forEach(p=>{if(!codexStatus[p.name])codexStatus[p.name]={loggedIn:lg.loggedIn,mode:lg.mode,dead:lg.dead,verified:lg.verified,text:lg.text,usageText,envWarnings:cxEnvWarn(S.codexStatus,p.name)};});}
1611
1642
  // codex ⚙ 面板「Codex 订阅」区的状态 HTML(登录态 + 额度),点「刷新」后就地更新。
1612
1643
  // 三态(服务端文案 text 是单一事实源):已验证 / 登录态已失效(服务端拒绝令牌 ⇒ 要重新登录)/ 未验证(探不到,不等于未登录)。
1613
- function cxStatusHtml(name){const c=codexStatus[name];const login=c&&c.loggedIn&&c.verified?`<span style="color:var(--ok);font-weight:600">${IC.check} ${esc(c.text||'已验证')}</span>`:(c&&c.dead?'<span style="color:var(--fail)">'+IC.x+' '+esc(c.text||'登录态已失效 — 机器上重新跑 codex login')+'</span>':(c?`<span class="faint" title="${esc(c.text||'')}">${c.loggedIn?'未验证(本机有登录文件,服务端暂时没探到)':esc(c.text||'未登录 — 机器上跑 codex login')}</span>`:'<span class="faint">未验证</span>'));return `<div>登录:${login}</div>${c&&c.usageText?`<div style="margin-top:5px">额度:<span style="color:var(--ok);font-weight:600" title="百分比 = 已用,括号 = 距重置">${esc(c.usageText)}</span></div>`:''}`;}
1644
+ function cxStatusHtml(name){const c=codexStatus[name];const login=c&&c.loggedIn&&c.verified?`<span style="color:var(--ok);font-weight:600">${IC.check} ${esc(c.text||'已验证')}</span>`:(c&&c.dead?'<span style="color:var(--fail)">'+IC.x+' '+esc(c.text||'登录态已失效 — 机器上重新跑 codex login')+'</span>':(c?`<span class="faint" title="${esc(c.text||'')}">${c.loggedIn?'未验证(本机有登录文件,服务端暂时没探到)':esc(c.text||'未登录 — 机器上跑 codex login')}</span>`:'<span class="faint">未验证</span>'));return `<div>登录:${login}</div>${c&&c.usageText?`<div style="margin-top:5px">额度:<span style="color:var(--ok);font-weight:600" title="百分比 = 已用,括号 = 距重置">${esc(c.usageText)}</span></div>`:''}${envWarnHtml(c&&c.envWarnings)}`;}
1614
1645
  // 首次打开控制台、服务端无缓存时,后台自动验一次 codex 登录+额度(查询不耗消息额度,服务端缓存),卡片状态无需手动点。
1615
1646
  // 开局自动探一次 Claude 登录态(`claude auth status --json`,0.2s、不吃订阅额度),
1616
1647
  // 与 autoVerifyCodex 对称 —— 让卡片一进来就说真话,而不是永远挂着「未验证」等人去点。
@@ -1621,7 +1652,7 @@ async function autoVerifyClaude(){
1621
1652
  if(!has)return;
1622
1653
  try{S.claudeStatus=await api('/api/claude/verify','POST',{});renderProviders();}catch(e){}
1623
1654
  }
1624
- async function autoVerifyCodex(){const cxs=((S&&S.providers)||[]).filter(p=>p.runner==='codex'&&!codexStatus[p.name]);if(!cxs.length)return;try{const r=await api('/api/codex/verify','POST',{});const lg=r.login||{},usageText=fmtUsage(r.usage);cxs.forEach(p=>codexStatus[p.name]={loggedIn:lg.loggedIn,mode:lg.mode,dead:lg.dead,verified:lg.verified,text:lg.text,usageText});renderProviders();}catch(e){}}
1655
+ async function autoVerifyCodex(){const cxs=((S&&S.providers)||[]).filter(p=>p.runner==='codex'&&!codexStatus[p.name]);if(!cxs.length)return;try{const r=await api('/api/codex/verify','POST',{});const lg=r.login||{},usageText=fmtUsage(r.usage);cxs.forEach(p=>codexStatus[p.name]={loggedIn:lg.loggedIn,mode:lg.mode,dead:lg.dead,verified:lg.verified,text:lg.text,usageText,envWarnings:cxEnvWarn(r,p.name)});renderProviders();}catch(e){}}
1625
1656
  // Claude 订阅登录 chip:此前只能写死「登录态·未验证」(判据是「没填 baseUrl 就当登录了」),
1626
1657
  // 真没登录也照样绿。现在有 `claude auth status --json` 这个官方非交互探针,报真状态;
1627
1658
  // 探不到(老版本 CLI / 不在 PATH)显式说「未验证」,**不当成未登录** —— 两种情况要分得开。
@@ -1650,6 +1681,7 @@ function claudeChip(){
1650
1681
  if(!s.loggedIn) return '<span class="chip none" title="这台机器还没登录 Claude —— 跑 claude auth login">未登录</span>';
1651
1682
  return `<span class="chip set" title="claude auth status: ${esc(s.method||'')} · ${esc(s.email||'')}${s.org?' · '+esc(s.org):''}">订阅登录 ${IC.check}${s.plan?' · '+esc(s.plan):''}</span>`;
1652
1683
  }
1684
+ function providerGroupCount(name){return (S.groups||[]).filter(g=>!g.paused&&(g.effectiveProvider||g.provider||S.defaultProvider)===name).length;}
1653
1685
  function renderProviders(){
1654
1686
  const cards=S.providers.filter(p=>showAll||(!p.hidden&&p.configured)).map(p=>{
1655
1687
  const cs=p.runner==='codex'?codexStatus[p.name]:null;
@@ -1657,16 +1689,15 @@ function renderProviders(){
1657
1689
  return `<div class="card pcard${p.hidden?' is-hidden':(!p.configured?' is-unconfig':'')}" data-prov="${esc(p.name)}">
1658
1690
  <div class="ph"><div class="pn"><span class="drag" draggable="true" title="拖动排序">${IC.grip}</span>${esc(p.name)}${p.runner==='codex'?'<span class="cli-tag codex" title="GPT 经 Codex CLI 后端 · 订阅登录">Codex CLI</span>':'<span class="cli-tag claude" title="经 Claude Code CLI · 模型大脑按 env 切换">Claude Code CLI</span>'}</div>
1659
1691
  <div class="inline" style="gap:8px">${chip}<button class="iconbtn cfg" data-act="edit" title="配置模型">${IC.settings}</button></div></div>
1660
- <div class="kv"><span class="kk">型号</span><span class="vv" title="${esc(p.model||(p.loggedIn&&p.runner!=='codex'?'未指定 ⇒ spawn 时不设 ANTHROPIC_MODEL,用 Claude Code 自己的默认模型':''))}">${esc(p.model||(p.loggedIn&&p.runner!=='codex'?'跟随 Claude Code 默认':'—'))}</span></div>
1661
- <div class="kv"><span class="kk">${p.contextWindow?.source==='official'?'模型上限':'当前窗口'}</span><span class="vv" title="${esc(providerWindowText(p,true))} · 只读,不会改变模型容量">${esc(providerWindowText(p))}</span></div>
1662
- ${p.runner==='codex'?`<div class="kv"><span class="kk">窗口设置</span><span class="vv">${p.contextWindowTokens?esc(fmtContextTokens(p.contextWindowTokens)):'执行器默认'}</span></div><div class="kv"><span class="kk">${p.reportedContextTokens?'最近上报可用窗口':p.contextWindowTokens?'实际可用窗口':'目录默认窗口'}</span><span class="vv" title="${esc(providerWindowText(p,true))}">${p.reportedContextTokens?esc(fmtContextTokens(p.reportedContextTokens)):p.contextWindowTokens?'待上报':p.contextWindow?.activeTokens?esc(fmtContextTokens(p.contextWindow.activeTokens)):'待上报'}</span></div>`:''}
1692
+ ${p.baseUrl?`<div class="kv"><span class="kk">型号</span><span class="vv" title="${esc(p.model||'')}">${esc(p.model?modelName(p.model):'—')}</span></div>
1693
+ <div class="kv"><span class="kk">${p.contextWindow?.source==='official'?'模型上限':'当前窗口'}</span><span class="vv" title="${esc(providerWindowText(p,true))} · 只读,不会改变模型容量">${esc(providerWindowText(p))}</span></div>`:`<div class="kv"><span class="kk">型号与深度</span><span class="vv" title="型号与思考深度按群在「群·项目」里选">按群选 · ${providerGroupCount(p.name)} 个群在用</span></div>`}
1694
+ ${p.runner==='codex'?`<div class="kv"><span class="kk">窗口设置</span><span class="vv">${p.contextWindowTokens?esc(fmtContextTokens(p.contextWindowTokens)):'执行器默认'}</span></div>`:''}
1663
1695
  <div class="kv"><span class="kk">${p.runner==='codex'?'压缩阈值':'压缩窗口'}</span><span class="vv" title="${esc(providerCompactText(p))} · 逐模型设置 · ${p.runner==='codex'?'Codex 可能按当前窗口提前压缩':'Claude Code 在压缩窗口满前触发,设置值受模型窗口限制'}">${esc(providerCompactText(p))}</span></div>
1664
1696
  <div class="kv"><span class="kk">连接方式</span><span class="vv" title="${esc(p.runner==='codex'?'Codex CLI(codex exec · 订阅登录)':(p.baseUrl||'本机 Claude Code 的登录态 —— 无需 baseUrl / API key'))}">${esc(p.runner==='codex'?'Codex CLI (订阅登录)':(p.baseUrl?p.baseUrl.replace('https://',''):'Claude 订阅登录'))}</span></div>
1665
- ${p.effort?`<div class="kv"><span class="kk">思考深度</span><span class="vv">${esc(p.effort)}</span></div>`:''}
1666
1697
  ${p.vision===false?'<div class="kv"><span class="kk">识图</span><span class="vv" title="纯文本模型:送给它的图片(群里发的图/网页截图/读取的图片文件)自动降级成占位文本,不会 400 崩掉整轮">不支持 · 图片自动降级</span></div>':''}
1667
- ${p.runner==='codex'?`<div class="kv"><span class="kk">推理强度</span><span class="vv">${esc(p.reasoningEffort||cxDefEffortLabel(p.model))}</span></div>`:''}
1668
1698
  ${usage[p.name]?`<div class="kv"><span class="kk" style="flex:none">${esc(usage[p.name].label)}${usageRefreshBtn(false)}</span><span class="vv" title="${esc(usage[p.name].value)}${usage[p.name].label==='订阅额度'?' —— 百分比 = 已用,括号 = 距重置,与这台机器上交互式 Claude Code 共享的订阅窗口':''}" style="color:var(--ok);font-weight:600;white-space:normal;overflow:visible;max-width:none;flex:1;min-width:0;line-height:1.5">${esc(usage[p.name].value)}</span></div>`:''}
1669
1699
  ${(cs&&cs.usageText)?`<div class="kv"><span class="kk" style="flex:none">订阅额度${usageRefreshBtn(true)}</span><span class="vv" title="百分比 = 已用,括号 = 距重置 —— 与交互式 Codex 共享的订阅窗口(时长随官方口径)" style="color:var(--ok);font-weight:600;white-space:normal;overflow:visible;max-width:none;flex:1;min-width:0;line-height:1.5">${esc(cs.usageText)}</span></div>`:''}
1700
+ ${(()=>{const ew=p.runner==='codex'?(cs&&cs.envWarnings):(p.loggedIn&&S.claudeStatus?S.claudeStatus.envWarnings:null);return (ew&&ew.length)?`<div class="kv"><span class="kk" style="flex:none">环境冲突</span><span class="vv" title="${esc(ew.map(w=>w.key+' — '+w.reason).join('\n'))}" style="color:var(--warn);white-space:normal;overflow:visible;max-width:none;flex:1;min-width:0;line-height:1.5" data-i18n-skip>${ew.map(w=>`<span class="mono">${esc(w.key)}</span>`).join(' · ')}</span></div>`:'';})()}
1670
1701
  <div class="acts"><button class="btn" data-act="test">测试</button>${p.hidden?'<span class="hint" style="align-self:center">已隐藏</span>':''}</div>
1671
1702
  <div class="reply-slot"></div>
1672
1703
  </div>`;
@@ -1674,7 +1705,7 @@ function renderProviders(){
1674
1705
  const extra=S.providers.filter(p=>p.hidden||!p.configured).length;
1675
1706
  const toggle=extra?`<button class="btn ghost" id="showAll">${showAll?'只看已配置':`显示全部 (${extra})`}</button>`:'';
1676
1707
  $('#providers').innerHTML=`<div class="inline" style="justify-content:space-between;align-items:flex-start;margin:0 2px 14px;gap:10px">
1677
- <div class="hint" style="flex:1">${S.runtimeCapabilities?esc(runtimeAllows('POST','/api/providers/add')?RUNTIME_UI_TEXT.providers:RUNTIME_UI_TEXT.providersView):`管理模型 · 拖 ${IC.grip} 排序 · ${IC.settings} 配端点/key/思考深度 · 模型按群在「群·项目」选`}</div>
1708
+ <div class="hint" style="flex:1">${S.runtimeCapabilities?esc(runtimeAllows('POST','/api/providers/add')?RUNTIME_UI_TEXT.providers:RUNTIME_UI_TEXT.providersView):`管理模型 · 拖 ${IC.grip} 排序 · ${IC.settings} 配端点/key/压缩 · 型号与思考深度按群在「群·项目」选`}</div>
1678
1709
  <div class="inline" style="gap:8px">${toggle}<button class="btn primary" id="addModel">${IC.plus} 添加模型</button></div></div>
1679
1710
  <div class="grid cols2 stagger" id="pgrid">${cards}</div>
1680
1711
  <h2 class="sec" style="margin-top:32px">插件库 · MCP</h2>
@@ -1726,7 +1757,7 @@ function renderProviders(){
1726
1757
  if(cx){const r=await api('/api/codex/verify','POST',{force:true});const lg=r.login||{};text=fmtUsage(r.usage);
1727
1758
  /* 探测失败(fmtUsage 给空串)时保住原来的额度:这一行的 DOM 留着旧值,内存态却清空的话,
1728
1759
  下次重画整行会凭空消失 —— 同一份状态两个消费者,漏一个就自相矛盾。 */
1729
- codexStatus[name]={loggedIn:lg.loggedIn,mode:lg.mode,dead:lg.dead,verified:lg.verified,text:lg.text,usageText:text||(lg.dead?'':((codexStatus[name]||{}).usageText||''))};}
1760
+ codexStatus[name]={loggedIn:lg.loggedIn,mode:lg.mode,dead:lg.dead,verified:lg.verified,text:lg.text,usageText:text||(lg.dead?'':((codexStatus[name]||{}).usageText||'')),envWarnings:cxEnvWarn(r,name)};}
1730
1761
  else{const r=await api('/api/usage/refresh','POST',{name});if(r.usage){usage[name]=r.usage;text=r.usage.value;}}
1731
1762
  /* 就地改这一行,不走 renderProviders —— 整页重画会把这颗按钮连同它的 finally 一起换掉,
1732
1763
  还会打断 hover 与试跑回执。拉不到时**留着旧值**:清空会被读成「这个模型没额度」。 */
@@ -1805,21 +1836,26 @@ function openProviderModal(name,opts){
1805
1836
  const preset=edit?'':`<div class="field"><label class="f">预设(自动填端点/模型/调优)</label><select id="amPreset">${Object.keys(PRESETS).map(k=>`<option value="${esc(k)}" ${opts.preset===k?'selected':''}>${esc(k)}</option>`).join('')}</select></div>`;
1806
1837
  const keyPh=edit?(p.keyStatus==='set'?'已设置 · 留空保持':p.keyStatus==='env'?'env:'+esc(p.keyRef)+' · 留空保持':'粘贴 key'):'粘贴 key(存进本地 gitignored 配置)';
1807
1838
  const codexNote=isCodex?'<div class="hint" style="margin-bottom:12px">使用本机 ChatGPT 订阅登录,额度与本机 Codex 共享。</div>':'';
1839
+ // Durable hosts: model and thinking depth are chosen per group (group card / /model);
1840
+ // only a third-party endpoint keeps its model here as connection configuration.
1841
+ const perGroup=!!S.runtimeCapabilities;
1842
+ const showModelSec=!edit||!perGroup||(!isCodex&&!isClaudeLogin);
1808
1843
  $('#modal').innerHTML=`<h3>${edit?'配置 '+esc(name):'添加模型'}</h3>
1809
1844
  ${preset}
1810
1845
  ${codexNote}
1811
- <h4 class="sec">型号与推理</h4>
1846
+ ${showModelSec?`<h4 class="sec">${perGroup?'名称与型号':'型号与推理'}</h4>`:''}
1812
1847
  ${edit?'':'<div class="field"><label class="f" for="amName">名称</label><input id="amName" placeholder="如 glm / my-model"/></div>'}
1813
- ${isCodex?`<div class="field"><label class="f">模型 model</label>
1848
+ ${perGroup?`<div class="hint" style="margin-bottom:12px">型号与思考深度按群在「群·项目」里选${(isCodex||isClaudeLogin)?'':';这个端点的型号在下面填,各群共用'}。</div>`:''}
1849
+ ${isCodex&&perGroup?'':isCodex?`<div class="field"><label class="f">模型 model</label>
1814
1850
  <select id="amModelSel"><option value="${edit?esc(p.model):''}" selected>${edit&&p.model?esc(p.model):'(未设 — 用 codex 默认)'}</option></select>
1815
1851
  <input id="amModel" value="${edit?esc(p.model):''}" placeholder="如 gpt-5.6-sol" style="display:none;margin-top:8px"/>
1816
1852
  <div class="hint" id="amModelSrc" style="margin-top:4px">加载模型列表…</div></div>`
1817
- :isClaudeLogin?`<div class="field" id="clModelWrap"><label class="f">模型 model</label>
1853
+ :isClaudeLogin&&perGroup?'':isClaudeLogin?`<div class="field" id="clModelWrap"><label class="f">模型 model</label>
1818
1854
  <div class="hint">检测登录中…</div></div>`
1819
1855
  :`<div class="field"><label class="f">模型 model</label><input id="amModel" value="${edit?esc(p.model):''}" placeholder="如 glm-5.2[1m]"/></div>`}
1820
- ${isCodex?`<div class="field"><label class="f">推理强度</label><select id="amRE">${RE_CODEX.concat(edit&&p.reasoningEffort&&!RE_CODEX.includes(p.reasoningEffort)?[p.reasoningEffort]:[]).map(v=>`<option value="${v}" ${edit&&v===(p.reasoningEffort||'')?'selected':''}>${v||'默认(模型内置)'}</option>`).join('')}</select>
1856
+ ${isCodex&&!perGroup?`<div class="field"><label class="f">推理强度</label><select id="amRE">${RE_CODEX.concat(edit&&p.reasoningEffort&&!RE_CODEX.includes(p.reasoningEffort)?[p.reasoningEffort]:[]).map(v=>`<option value="${v}" ${edit&&v===(p.reasoningEffort||'')?'selected':''}>${v||'默认(模型内置)'}</option>`).join('')}</select>
1821
1857
  <div class="hint" style="margin-top:4px">默认沿用模型推荐强度;更高强度通常需要更多时间和额度。</div></div>`:''}
1822
- ${isCodex?'':`<div class="field"><label class="f">思考深度</label><select id="amEff">${EFFORTS.map(v=>`<option value="${v}" ${edit&&v===p.effort?'selected':''}>${v||'默认'}</option>`).join('')}</select>
1858
+ ${isCodex||perGroup?'':`<div class="field"><label class="f">思考深度</label><select id="amEff">${EFFORTS.map(v=>`<option value="${v}" ${edit&&v===p.effort?'selected':''}>${v||'默认'}</option>`).join('')}</select>
1823
1859
  <div class="hint" style="margin-top:5px">默认沿用模型推荐强度;更高强度通常需要更多时间和额度。</div></div>`}
1824
1860
  <h4 class="sec">上下文与压缩</h4>
1825
1861
  <details class="field"><summary class="hint" style="cursor:pointer">容量与运行信息</summary><div class="dim" id="amWindow" style="padding:9px 0">${edit?esc(providerWindowText(p,true)):'添加后按模型目录或会话上报显示'}</div></details>
@@ -1929,7 +1965,7 @@ function openProviderModal(name,opts){
1929
1965
  })();
1930
1966
  $('#amCancel').onclick=closeModal;
1931
1967
  if(isCodex){$('#cxVerify').onclick=async()=>{const cb=$('#cxVerify');cb.disabled=true;const old=cb.innerHTML;cb.innerHTML='<span class="spin"></span>验证中';
1932
- try{const r=await api('/api/codex/verify','POST',{});const lg=r.login||{},usageText=fmtUsage(r.usage);codexStatus[cxKey]={loggedIn:lg.loggedIn,mode:lg.mode,dead:lg.dead,verified:lg.verified,text:lg.text,usageText};
1968
+ try{const r=await api('/api/codex/verify','POST',{});const lg=r.login||{},usageText=fmtUsage(r.usage);codexStatus[cxKey]={loggedIn:lg.loggedIn,mode:lg.mode,dead:lg.dead,verified:lg.verified,text:lg.text,usageText,envWarnings:cxEnvWarn(r,cxKey)};
1933
1969
  if($('#cxStatus'))$('#cxStatus').innerHTML=cxStatusHtml(cxKey);
1934
1970
  toast(lg.loggedIn?('Codex 订阅 ✓'+(usageText?' · '+usageText:(r.usage===null?'(额度取用失败:app-server?)':''))):(lg.dead?'登录态已失效 — 机器上重新跑 codex login':'未登录 — 机器上跑 codex login'),lg.loggedIn?'ok':'err');
1935
1971
  renderProviders();
@@ -1946,7 +1982,7 @@ function openProviderModal(name,opts){
1946
1982
  const b={name:nm,autocompactTokens:$('#amCompMode').value==='default'?0:Number($('#amComp').value)};
1947
1983
  if(!Number.isInteger(b.autocompactTokens)||b.autocompactTokens<0||($('#amCompMode').value==='custom'&&(b.autocompactTokens<100000||b.autocompactTokens>1000000))){toast('压缩阈值须为 0 或 10万–100万 token','err');return;}
1948
1984
  if(selModel)b.model=selModel;
1949
- if(isCodex){b.model=selModel||'';b.reasoningEffort=$('#amRE').value;
1985
+ if(isCodex){if(!perGroup){b.model=selModel||'';b.reasoningEffort=$('#amRE').value;}
1950
1986
  const value=$('#amContextMode').value==='default'?'0':$('#amContext').value.trim();b.contextWindowTokens=Number(value);
1951
1987
  if(!value||!Number.isSafeInteger(b.contextWindowTokens)||b.contextWindowTokens<0||($('#amContextMode').value==='custom'&&(b.contextWindowTokens<100000||b.contextWindowTokens>1050000))){toast('自定义窗口须为 10万–105万 token','err');return;}
1952
1988
  // ⚠ 新建时必须显式带 runner —— 少了它建出来的是个"没有端点没有 key 的 claude provider",
@@ -1955,8 +1991,8 @@ function openProviderModal(name,opts){
1955
1991
  else if(isClaudeLogin){
1956
1992
  // 订阅登录:端点/key/识图三项面板里就没有 ⇒ 不回传(回传空串会把已有值清掉)。
1957
1993
  // 型号可以被清空成「跟随 Claude Code 默认」,那要显式 modelClear —— 但只在控件真在时才发。
1958
- b.effort=$('#amEff').value; if(hasModelCtl&&!selModel)b.modelClear=true;
1959
- } else {b.model=selModel;b.baseUrl=$('#amBase').value.trim();b.apiKey=$('#amKey').value.trim();b.effort=$('#amEff').value;b.vision=!$('#amVision').checked;}
1994
+ if(!perGroup){b.effort=$('#amEff').value; if(hasModelCtl&&!selModel)b.modelClear=true;}
1995
+ } else {b.model=selModel;b.baseUrl=$('#amBase').value.trim();b.apiKey=$('#amKey').value.trim();if($('#amEff'))b.effort=$('#amEff').value;b.vision=!$('#amVision').checked;}
1960
1996
  if(!edit)b.env=JSON.parse($('#amPreset').dataset.env||'{}');
1961
1997
  if(edit)b.hidden=$('#amHide').checked;
1962
1998
  try{const r=await api('/api/providers/add','POST',b);S=r;
@@ -2655,7 +2691,7 @@ function cronFullHtml(text){
2655
2691
  }).join('');
2656
2692
  }
2657
2693
  const RUNTIME_UI_TEXT={
2658
- "groups": "可绑定新群,修改模型、项目目录、权限、数据输出和忙时跟进;该群须没有进行中的任务和未结记录。切模型或项目会开始新的一段对话。停止任务和开新会话在「概览」,开新会话须等该群空闲。群里发 /model 也能切换模型。",
2694
+ "groups": "可绑定新群,修改后端、型号、思考深度、项目目录、权限、数据输出和忙时跟进;该群须没有进行中的任务和未结记录。换后端或项目会开始新的一段对话,换型号或调深度接着当前对话。停止任务和开新会话在「概览」,开新会话须等该群空闲。群里发 /model 也能改。",
2659
2695
  "skills": "当前支持飞书文字消息、忙时跟进、任务回复和停止。定时任务、生图、群消息管理、画布与预览暂未接入。",
2660
2696
  "cron": "当前不执行定时任务;已有定义仅供查看。",
2661
2697
  "board": "画布创建、同步与反馈暂未接入;已有链接仅供查看。",
@@ -2665,10 +2701,80 @@ const RUNTIME_UI_TEXT={
2665
2701
  "resumeLabel": "恢复",
2666
2702
  "steer": "Codex 与 Claude Code 默认开启忙时跟进;长命令执行期间的补充会在工具步骤之间读取。只有确认模型已读取才显示已加入;不支持此能力时排队。控制台插话同样支持忙时跟进;关闭或暂不支持时排队。",
2667
2703
  "preview": "当前版本未接入预览服务。",
2668
- "providers": "可添加模型,修改型号、端点、密钥、思考深度与推理强度,测试、隐藏或拖动排序;各群的模型在「群·项目」里切换。改型号、后端或端点时,用它的群须空闲,并会开始新的一段对话;默认模型和群正在用的模型不能隐藏。移除模型暂不可用。",
2704
+ "providers": "可添加模型,修改端点、密钥、上下文与压缩,测试、隐藏或拖动排序;型号与思考深度按群在「群·项目」里选(第三方端点的型号在这里填)。改后端或端点时,用它的群须空闲,并会开始新的一段对话;默认模型和群正在用的模型不能隐藏。移除模型暂不可用。",
2669
2705
  "providersView": "当前可查看模型配置与登录状态;各群的模型在「群·项目」里切换。添加或修改模型暂不可用。",
2670
2706
  "unavailable": "当前版本暂不支持此操作。"
2671
2707
  };
2708
+ // Model display names (claude-opus-5-5 → Opus 5.5): the server injects the same
2709
+ // function the group replies use; stored and sent values stay the official ids.
2710
+ const modelNameRule=/*__SBOT_MODELNAME__*/(s=>String(s==null?'':s));
2711
+ const MODEL_OPTS={}; // provider name → /api/groups/model-options result
2712
+ const modelName=(id)=>{const k=String(id==null?'':id).trim();if(!k)return '';
2713
+ const listed=Object.values(MODEL_OPTS).map(c=>c&&Array.isArray(c.models)&&c.models.find(m=>m&&m.id===k)).find(Boolean);
2714
+ return (S&&S.modelNames&&S.modelNames[k])||(listed&&listed.name)||modelNameRule(k);};
2715
+ // Backend · model · thinking depth on the group card. Choices are stored per
2716
+ // backend; only a backend change starts a new conversation.
2717
+ const MODEL_OPTS_LOADING=new Set();
2718
+ function loadModelOpts(name){
2719
+ if(!name||MODEL_OPTS[name]||MODEL_OPTS_LOADING.has(name))return;
2720
+ MODEL_OPTS_LOADING.add(name);
2721
+ api('/api/groups/model-options?provider='+encodeURIComponent(name)).then(c=>{MODEL_OPTS[name]=c||{ok:false,models:[],efforts:[]};})
2722
+ .catch(()=>{MODEL_OPTS[name]={ok:false,models:[],efforts:[]};})
2723
+ .finally(()=>{MODEL_OPTS_LOADING.delete(name);if($('#groups'))renderGroups();});
2724
+ }
2725
+ function catEfforts(cat,model){
2726
+ if(!cat)return null;
2727
+ const m=Array.isArray(cat.models)?cat.models.find(x=>x.id===model):null;
2728
+ if(m&&Array.isArray(m.efforts))return m.efforts; // [] = this model has no depth setting
2729
+ return cat.efforts||[];
2730
+ }
2731
+ function groupBrainRows(g){
2732
+ const b=g.brain||{provider:g.effectiveProvider||g.provider||S.defaultProvider,model:'',effort:''};
2733
+ const name=b.provider, cat=MODEL_OPTS[name];
2734
+ if(!cat)loadModelOpts(name);
2735
+ const sel='style="width:auto;min-width:110px;max-width:100%;padding:5px 30px 5px 10px;font-size:11.5px"';
2736
+ const unset='<span style="color:var(--fail)">未选</span>';
2737
+ const provs=S.providers.filter(pp=>!pp.hidden&&pp.configured);
2738
+ if(!provs.some(pp=>pp.name===name))provs.unshift({name});
2739
+ const backend=`<select class="gprov" data-g="${esc(g.chatId)}" ${sel}>${provs.map(pp=>`<option value="${esc(pp.name)}" ${pp.name===name?'selected':''}>${esc(pp.name)}</option>`).join('')}</select>`;
2740
+ let model;
2741
+ if(!cat)model=`<span class="vv">${b.model?esc(modelName(b.model)):unset} <span class="faint">· 加载清单…</span></span>`;
2742
+ else if(cat.models===null)model=`<span class="vv" title="这个端点的型号在「模型」页设置,各群共用">${b.model?esc(modelName(b.model)):'—'} <span class="faint">· 在「模型」页改</span></span>`;
2743
+ else{
2744
+ const list=cat.models||[];
2745
+ const opts=list.map(m=>`<option value="${esc(m.id)}" ${m.id===b.model?'selected':''}>${esc(m.name||modelName(m.id))}${cat.recommended&&cat.recommended.model===m.id?' ★':''}</option>`);
2746
+ if(b.model&&!list.some(m=>m.id===b.model))opts.unshift(`<option value="${esc(b.model)}" selected>${esc(modelName(b.model))}</option>`);
2747
+ if(!b.model)opts.unshift('<option value="" selected disabled>未选</option>');
2748
+ opts.push('<option value="__custom__">自定义…</option>');
2749
+ model=`<span class="vv inline" style="gap:6px;flex-wrap:wrap;min-width:0"><select class="gmodel" data-g="${esc(g.chatId)}" ${sel}>${opts.join('')}</select>
2750
+ <span class="gcustom" data-g="${esc(g.chatId)}" hidden><input class="gcustomv" placeholder="型号 id" style="width:150px;padding:5px 8px;font-size:11.5px"/> <button class="btn" data-gcustom="${esc(g.chatId)}" style="padding:3px 9px;font-size:11px">保存</button></span>
2751
+ ${cat.ok===false?'<span class="faint" style="font-size:11px">清单暂时取不到</span>':''}</span>`;
2752
+ }
2753
+ const effs=catEfforts(cat,b.model);
2754
+ let effort;
2755
+ if(effs&&!effs.length&&cat&&Array.isArray(cat.models)&&cat.models.some(m=>m.id===b.model))effort='<span class="vv faint">这个型号不支持</span>';
2756
+ else{
2757
+ const list=(effs&&effs.length?effs:['low','medium','high','xhigh','max']).slice();
2758
+ if(b.effort&&!list.includes(b.effort))list.push(b.effort);
2759
+ effort=`<select class="geffort" data-g="${esc(g.chatId)}" ${sel}>${b.effort?'':'<option value="" selected disabled>未选</option>'}${list.map(e=>`<option value="${esc(e)}" ${e===b.effort?'selected':''}>${esc(e)}</option>`).join('')}</select>`;
2760
+ }
2761
+ const pr=S.providers.find(pp=>pp.name===name)||{};
2762
+ const win=g.contextWindow&&g.contextWindow.tokens?providerWindowText({contextWindow:g.contextWindow,runner:pr.runner},true):'';
2763
+ // One "模型" row: backend and model side by side (they wrap on a narrow screen).
2764
+ return `<div class="kv"><span class="kk">模型</span><span class="vv inline" style="gap:6px;flex-wrap:wrap;min-width:0">${backend}${model}</span></div>
2765
+ <div class="kv"><span class="kk">思考深度</span>${effort}</div>
2766
+ ${win?`<div class="kv"><span class="kk">上下文</span><span class="vv" title="${esc(win)}">${esc(win)}</span></div>`:''}
2767
+ <div class="kv"><span class="kk"></span><span class="faint" style="font-size:11px">换后端会开新对话;换型号或调深度接着当前对话 · 群空闲时才能改</span></div>`;
2768
+ }
2769
+ async function saveGroupBrain(chatId,change){
2770
+ try{
2771
+ const r=await api('/api/groups/brain','POST',{chatId,...change});S=r;renderOverview();renderGroups();
2772
+ const c=r.brainChange||{};
2773
+ const filled=c.filled&&c.filled.length?'(未选的'+c.filled.join('和')+'先用推荐)':'';
2774
+ const what=[c.model?modelName(c.model):'',c.effort||''].filter(Boolean).join(' · ');
2775
+ toast(!c.changed?'没有变化':c.switching?`已切到 ${c.provider}${what?' · '+what:''} · 会话已重置,下条消息生效${filled}`:c.modelChanged?`已改为 ${what} · 接着当前对话,下条消息生效${filled}`:`思考深度已调为 ${c.effort} · 下条消息生效`,'ok');
2776
+ }catch(e){toast(e.message,'err');renderGroups();}
2777
+ }
2672
2778
  function renderGroups(){
2673
2779
  const provOf=g=>S.providers.find(pp=>pp.name===(g.provider||S.defaultProvider));
2674
2780
  const cards=S.groups.map(g=>{
@@ -2885,10 +2991,7 @@ function renderGroups(){
2885
2991
  </label>
2886
2992
  <span class="faint" style="font-size:11px">${g.redactPii===false?'当前:明文':'当前:遮盖'}</span>
2887
2993
  </span></div>
2888
- <div class="kv"><span class="kk">模型</span>
2889
- <select class="gprov" data-g="${esc(g.chatId)}" style="width:auto;min-width:110px;padding:5px 30px 5px 10px;font-size:11.5px">
2890
- ${S.providers.filter(pp=>!pp.hidden&&pp.configured).map(pp=>`<option value="${esc(pp.name)}" ${pp.name===(g.provider||S.defaultProvider)?'selected':''}>${esc(pp.name)}${pp.model?' · '+esc(pp.model):''}</option>`).join('')}
2891
- </select></div>
2994
+ ${groupBrainRows(g)}
2892
2995
  <div class="kv" style="align-items:flex-start"><span class="kk">忙时跟进</span><span class="vv" style="min-width:0">
2893
2996
  <label class="inline" style="gap:6px;cursor:pointer" title="${esc(S.runtimeCapabilities?RUNTIME_UI_TEXT.steer:'开启后,处理任务期间:群成员的后续消息、控制台勾了「回复」的插话,都会加入当前任务 —— 相关要求及时吸收,独立请求按先后回应。定时任务和不回复的插话仍排在后面。')}">
2894
2997
  <input type="checkbox" class="gsteer" data-g="${esc(g.chatId)}"${g.followupEnabled?' checked':''} style="width:auto;margin:0"/>
@@ -2932,7 +3035,14 @@ function renderGroups(){
2932
3035
  const on=b.dataset.p!=='1';
2933
3036
  if(on&&!confirm(S.runtimeCapabilities?'暂停本群?只有任务结束且记录全部结账后才能暂停;暂停期间不处理新消息,配置和会话保留。':'暂停本群?\n期间消息不处理(静默丢弃)、排队清空、定时任务与画布播报停发;进行中的任务会跑完。\n完全可逆:配置/会话/画布全保留,点「恢复」即原样回来。'))return;
2934
3037
  try{S=await api('/api/groups/pause','POST',{chatId:b.dataset.gpause,paused:on});renderGroups();renderOverview();toast(S.runtimeCapabilities?(on?'已暂停':'已恢复'):(on?'已暂停 · 群内已通知':'已恢复 · 群内已通知'),'ok');}catch(e){toast(e.message,'err');}});
2935
- $$('#groups .gprov').forEach(sel=>sel.onchange=async()=>{try{const r=await api('/api/groups/provider','POST',{chatId:sel.dataset.g,provider:sel.value});S=r;renderOverview();renderGroups();toast(r.switched&&r.switched.live?('已切到 '+sel.value+' · 会话已重置,下条消息生效'):('已保存 '+sel.value+' · daemon 未运行下次生效'),'ok');}catch(e){toast(e.message,'err');}});
3038
+ $$('#groups .gprov').forEach(sel=>sel.onchange=()=>saveGroupBrain(sel.dataset.g,{provider:sel.value}));
3039
+ $$('#groups .gmodel').forEach(sel=>sel.onchange=()=>{const box=$('.gcustom[data-g="'+sel.dataset.g+'"]');
3040
+ if(sel.value==='__custom__'){if(box){box.hidden=false;box.querySelector('input').focus();}return;}
3041
+ if(box)box.hidden=true;saveGroupBrain(sel.dataset.g,{model:sel.value});});
3042
+ $$('#groups [data-gcustom]').forEach(b=>b.onclick=()=>{const box=$('.gcustom[data-g="'+b.dataset.gcustom+'"]'),v=box&&box.querySelector('input').value.trim();
3043
+ if(!v){toast('填写型号 id','err');return;}saveGroupBrain(b.dataset.gcustom,{model:v});});
3044
+ $$('#groups .geffort').forEach(sel=>sel.onchange=()=>saveGroupBrain(sel.dataset.g,{effort:sel.value}));
3045
+
2936
3046
  $$('#groups .gsteer').forEach(c=>c.onchange=async()=>{const on=c.checked;
2937
3047
  try{S=await api('/api/groups/steer','POST',{chatId:c.dataset.g,enabled:on});renderGroups();toast(on?'已开启 · 下轮生效':'已关闭 · 下轮恢复排队','ok');}
2938
3048
  catch(e){c.checked=!on;toast(e.message,'err');}});
@@ -3617,6 +3727,7 @@ function renderSettings(){
3617
3727
  <div id="verhistBox"><div class="empty">加载中…</div></div></div>
3618
3728
  </div>`;
3619
3729
  $('#settings').innerHTML=`<div class="grid cols2 stagger">
3730
+ ${fxFeishuCard(fe)}
3620
3731
  ${netCard}
3621
3732
  <div class="card"><div class="lbl">运行限制</div>
3622
3733
  <div class="inline" style="align-items:flex-start">
@@ -3632,10 +3743,8 @@ function renderSettings(){
3632
3743
  <div class="hint" style="margin-top:10px">执行权限已改<b>按群设</b> —— 见「群·项目」每张卡(缺省 <b>dontAsk 只读 · fail-closed</b>)。</div>
3633
3744
  </div>
3634
3745
  ${botCard}
3635
- <div class="fx-sub" style="grid-column:1/-1;margin:6px 0 0"><span style="flex:none">聊天平台</span><span class="faint" style="font-weight:400;letter-spacing:0">配了哪个就接哪个,可以同时在线;每个群项目自己选属于哪边。点卡头展开</span></div>
3636
- ${fxFeishuCard(fe)}
3637
- ${fxSlackCard(S.slack||null)}
3638
- ${fxWecomCard(S.wecom||null)}
3746
+ ${fxFeishuAccessCard(fe)}
3747
+ ${!S.runtimeCapabilities?fxSlackCard(S.slack||null)+fxWecomCard(S.wecom||null):''}
3639
3748
  ${aboutCard}
3640
3749
  ${updateCard}
3641
3750
  </div>`;
@@ -3683,9 +3792,8 @@ function renderSettings(){
3683
3792
  const la=$('#logoutAllBtn');if(la)la.onclick=async()=>{try{const r=await api('/api/logout/all','POST',{});toast('已退出 '+(r.n||0)+' 个登录','ok');}catch(e){toast(e.message,'err');return;}dropToken();TOKEN='';setTimeout(()=>location.reload(),600);};
3684
3793
  $('#aSave').onclick=async()=>{const patch={maxConcurrent:+$('#aConc').value,maxTurnTimeMins:+$('#aWall').value,ackDelayMs:Math.round((+$('#aAck').value||0)*1000),timezone:$('#aTz').value.trim()};
3685
3794
  try{S=await api('/api/agent','POST',patch);toast('已保存','ok');renderGroups();renderSettings();startClock();}catch(e){toast(e.message,'err');}};
3686
- $('#fSave').onclick=async()=>{const b={botOpenId:$('#fBot').value.trim(),onlyWhenMentioned:$('#fGate').value==='1'};const id=$('#fId').value.trim();if(id)b.appId=id;const sec=$('#fSecret').value.trim();if(sec)b.appSecret=sec; // 空 appId 不回传:env: 引用时字段渲染为空,回传空串会清掉凭据
3687
- try{S=await api('/api/feishu','POST',b);toast('飞书已保存','ok');renderAll();}catch(e){toast(e.message,'err');}}; // 管理员单独管理(见下)
3688
3795
  // ── Slack 应用卡 ──(与上面飞书那组同结构:凭证先落盘,预检再读已存配置)
3796
+ if($('#slSave')){
3689
3797
  const slSaveCreds=async()=>{const b={};const bt=$('#slBot').value.trim();if(bt)b.botToken=bt;const at=$('#slApp').value.trim();if(at)b.appToken=at;if(Object.keys(b).length)S=await api('/api/slack','POST',b);};
3690
3798
  $('#slSave').onclick=async()=>{const b={onlyWhenMentioned:$('#slGate').value==='1',replyInThread:$('#slThread').value!=='0',commandAlias:($('#slAlias').value.trim()||'!')};
3691
3799
  const bt=$('#slBot').value.trim();if(bt)b.botToken=bt;const at=$('#slApp').value.trim();if(at)b.appToken=at;
@@ -3697,41 +3805,13 @@ function renderSettings(){
3697
3805
  toast('预检完成','ok');}
3698
3806
  catch(e2){toast(e2.message,'err');}
3699
3807
  finally{btn.disabled=false;btn.textContent='🩺 预检';}};
3700
- // 预检 / 通讯录 都要先落盘当前填的凭证(端点读的是已存配置);env: 引用或空值不覆盖。
3701
- const saveCreds=async()=>{const creds={};const id=$('#fId').value.trim();if(id)creds.appId=id;const sec=$('#fSecret').value.trim();if(sec)creds.appSecret=sec;if(Object.keys(creds).length)await api('/api/feishu','POST',creds);};
3702
- // 应用身份条:头像走 JS 注入(远端图,加载不到就退回 🤖 —— 控制台可能开在断外网的机器上),
3703
- // 刷新只打「身份」这一个端点,不跑整套预检。原地重画,不整页 render:预检清单还在下面开着。
3704
- const paintIdent=()=>{const el=$('.fx-ident');if(!el)return;el.outerHTML=fxIdentity(S.feishu||null);bindIdent();updateTopFxApp();};
3705
- const bindIdent=()=>{
3706
- const id=(S.feishu&&S.feishu.identity)||null,av=$('#fIdentAv');
3707
- if(av&&id&&id.avatar){const img=new Image();img.alt='';img.onload=()=>{av.textContent='';av.appendChild(img);};img.onerror=()=>{av.innerHTML=IC.bot;};img.src=id.avatar;}
3708
- const btn=$('#fIdentRefresh');if(!btn)return;
3709
- btn.onclick=async()=>{const t=btn.innerHTML;btn.disabled=true;btn.innerHTML='<span class="spin"></span> 刷新中…';
3710
- try{await saveCreds();const r=await api('/api/feishu/appinfo','POST',{});
3711
- S.feishu=S.feishu||{};S.feishu.identity=r.identity;
3712
- if(r.botOpenId){S.feishu.botOpenId=r.botOpenId;const f=$('#fBot');if(f&&!f.value)f.value=r.botOpenId;}
3713
- paintIdent();toast('应用身份已更新','ok');
3714
- }catch(e){toast(e.message,'err');btn.disabled=false;btn.innerHTML=t;}};
3715
- };
3716
- bindIdent();
3717
- const fcs=$('#fCopyScopes'); if(fcs) fcs.onclick=(e)=>{e.preventDefault();const txt=fcs.dataset.json||'';if(!txt)return;
3718
- const done=()=>{fcs.textContent='已复制';setTimeout(()=>fcs.textContent='复制导入 JSON',1200);};
3719
- const pick=()=>{const ta=document.createElement('textarea');ta.value=txt;ta.setAttribute('style','position:fixed;top:0;left:0;opacity:0');document.body.appendChild(ta);ta.select();try{document.execCommand('copy');done();}catch(_){toast('复制失败,请手动选择','err');}ta.remove();};
3720
- if(navigator.clipboard&&navigator.clipboard.writeText)navigator.clipboard.writeText(txt).then(done).catch(pick); else pick();};
3721
- $('#fPrecheck').onclick=async()=>{const btn=$('#fPrecheck'),box=$('#fCheck');
3722
- btn.disabled=true;btn.innerHTML='<span class="spin"></span> 预检中…';box.innerHTML='';
3723
- try{await saveCreds();const r=await api('/api/feishu/precheck','POST',{});
3724
- if(r.botOpenId)$('#fBot').value=r.botOpenId;
3725
- if(r.identity){S.feishu=S.feishu||{};S.feishu.identity=r.identity;if(r.botOpenId)S.feishu.botOpenId=r.botOpenId;paintIdent();}
3726
- renderFxCheck(box,r.checks);toast('预检完成','ok');
3727
- }catch(e){box.innerHTML=`<div class="fx-check"><div class="fx-crow"><span class="dot fail"></span><span class="cd" style="color:var(--fail)">${esc(e.message)}</span></div></div>`;toast(e.message,'err');}
3728
- finally{btn.disabled=false;btn.innerHTML=IC.steth+' 预检并自动获取';}};
3808
+ }
3729
3809
  // 管理员(可多个):chips 列表 + 从组织通讯录搜索选人(单一入口)。
3730
3810
  let admins=(fe&&Array.isArray(fe.admins))?fe.admins.slice():[];
3731
3811
  const drawAdmins=()=>{const el=$('#fAdminList');if(!el)return;
3732
3812
  el.innerHTML=admins.length
3733
3813
  ?admins.map((x,i)=>`<span class="fx-admin-chip">${IC.user} ${esc(x.name||x.openId)}<button class="fx-admin-x" data-i="${i}" title="移除">${IC.x}</button></span>`).join('')
3734
- :'<span class="hint">未配置 — 建群需拉入管理员,否则纯 bot 群飞书里谁都看不到</span>';
3814
+ :'<span class="hint">尚未添加管理员;使用公网飞书登录前请先添加。</span>';
3735
3815
  el.querySelectorAll('.fx-admin-x').forEach(b=>b.onclick=async()=>{admins.splice(+b.dataset.i,1);await saveAdmins();});};
3736
3816
  const saveAdmins=async()=>{try{S=await api('/api/feishu/admins','POST',{admins});admins=(S.feishu&&Array.isArray(S.feishu.admins))?S.feishu.admins.slice():admins;drawAdmins();}catch(e){toast(e.message,'err');}};
3737
3817
  const addAdmin=async(openId,name)=>{if(admins.some(x=>x.openId===openId))return;admins.push({openId,name:name||openId});await saveAdmins();};
@@ -3741,7 +3821,7 @@ function renderSettings(){
3741
3821
  $('#fAdminOrg').onclick=async()=>{const btn=$('#fAdminOrg'),pick=$('#fAdminPick');
3742
3822
  if(pick.dataset.open==='1'){pick.innerHTML='';pick.dataset.open='';btn.innerHTML=IC.plus+' 从通讯录添加';return;} // 再点=收起
3743
3823
  btn.disabled=true;btn.innerHTML='<span class="spin"></span> 加载通讯录…';
3744
- try{await saveCreds();const r=await api('/api/feishu/org-users');const users=r.users||[];
3824
+ try{const r=await api('/api/feishu/org-users');const users=r.users||[];
3745
3825
  if(!users.length){toast('通讯录可见 0 人 — 去开放平台把应用「通讯录授权范围/数据权限」放开到全部成员(scope 已够,这是数据授权范围)','err');return;}
3746
3826
  const has=(id)=>admins.some(x=>x.openId===id);
3747
3827
  const rows=(q)=>{const f=users.filter(u=>!q||(u.name||'').toLowerCase().indexOf(q)>-1||u.openId.indexOf(q)>-1);
@@ -3866,6 +3946,7 @@ function fxWecomCard(we){
3866
3946
  </details></div>`;
3867
3947
  }
3868
3948
  function bindWecomCard(){
3949
+ if(!$('#wecomCard'))return;
3869
3950
  if(!$('#wecomCard'))return;
3870
3951
  const parseNames=()=>{const out={};for(const line of $('#wNames').value.split(/\n+/)){const m=line.trim().match(/^(\S+)[\s=:,]+(.+)$/);if(m)out[m[1]]=m[2].trim();}return out;};
3871
3952
  const creds=()=>{const b={};const id=$('#wId').value.trim();if(id)b.botId=id;const sec=$('#wSecret').value.trim();if(sec)b.secret=sec;return b;}; // 空值不回传:env: 引用时字段渲染为空,回传空串会清掉凭据
@@ -3884,88 +3965,56 @@ function bindWecomCard(){
3884
3965
  }catch(e){box.innerHTML=`<div class="fx-check"><div class="fx-crow"><span class="dot fail"></span><span class="cd" style="color:var(--fail)">${esc(e.message)}</span></div></div>`;toast(e.message,'err');}
3885
3966
  finally{btn.disabled=false;btn.innerHTML=IC.steth+' 预检';}};
3886
3967
  }
3887
- // 飞书应用卡:开通指引(可折叠)→ 凭证 → 预检并自动获取(内联 ✓/✗ 清单)→ 身份(应用身份条 + bot/管理员从通讯录选)→ 响应范围。
3968
+ // The connection view is read-only; onboarding is completed on the host Mac.
3969
+ // Public console actions are limited to group management and access administration.
3888
3970
  function fxFeishuCard(fe){
3889
- const fl=Object.assign({},(S.net||{}).feishuLogin||{},{publicUrl:(S.net||{}).publicUrl||''}); // 服务端算好的就绪态与回调地址
3890
- const on=!!(fe&&fe.botOpenId);
3891
- const lvTxt={req:'必需',rec:'推荐',opt:'可选'};
3892
- const scopes=[
3893
- // 逐项对照代码实际调用的 API 核准(2026-07-16 按官方文档核对):send_as_bot 覆盖 发/回复/编辑/撤回自己的消息;
3894
- // readonly 覆盖 拉历史/下载附件/撤回事件;上传(im:resource)与表情(reactions)各有独立 scope,谁都不覆盖谁。
3895
- ['im:message.group_msg','接收群消息(配长连接订阅事件)','req'],
3896
- ['im:message:readonly','拉群历史 / 下载群附件(基础技能 history·download)+ 消息撤回事件(撤回即停)','req'],
3897
- ['im:message:send_as_bot','发送 / 回复 / 编辑 / 撤回自己的消息','req'],
3898
- ['im:resource','上传图片 / 文件(基础技能 send-image·send-file 必需,缺则发不出)','req'],
3899
- ['im:message.reactions:write_only','消息表情回应(基础技能 react 必需)','req'],
3900
- ['im:chat','建群 / 解散 / 群成员管理','req'],
3901
- ['cardkit:card:write','流式打字机卡片(缺则回退纯文本)','req'],
3902
- ['contact:contact.base:readonly','发送者真名 + 通讯录选管理员','req'],
3903
- ['contact:department.base:readonly','通讯录选管理员 · 读部门树','req'],
3904
- // 2026-09-05 两个租户对照验出:没有它,「从通讯录添加」列表与发送者名字全显示成 open_id(scope 够、数据范围也够,就是没名字)。
3905
- ['contact:user.base:readonly','通讯录成员真名(缺则列表与发送者名字显示成 open_id)','req'],
3906
- ['contact:department.organize:readonly','通讯录选人 · 完整部门层级(单层组织可不开)','opt'],
3907
- ['contact:user.id:readonly','邮箱 / 手机号查 openid','opt'],
3908
- // 应用名称/头像/启用状态来自「获取机器人信息」接口,官方标「权限要求:无」——白拿,不用列;
3909
- // 只有「应用描述」在应用信息接口里,才需要下面这条(admin:app.info:readonly 亦可,任开其一)。
3910
- ['application:application:self_manage','显示应用描述(不开则只显示名称 / 头像 / 状态)','opt'],
3911
- // 设计画布(docs/BOARD.md;2026-07-21 真机逐 API 核准)— 不用画布可全不开,开则整组开:
3912
- ['docx:document:create','设计画布 · 创建画布文档','opt'],
3913
- ['docx:document:write_only','设计画布 · 文档内建画板块','opt'],
3914
- ['docs:document.media:upload','设计画布 · 上传画板图片素材','opt'],
3915
- ['board:whiteboard:node:read','设计画布 · 读画板节点(标注)','opt'],
3916
- ['board:whiteboard:node:create','设计画布 · 写画板节点(放图)','opt'],
3917
- ['board:whiteboard:node:delete','设计画布 · 覆写清理节点','opt'],
3918
- ['docs:permission.member:create','设计画布 · 文档分享给群','opt'],
3919
- ['drive:drive.metadata:readonly','设计画布 · 查询文档链接','opt'],
3920
- ['im:chat.tabs:write_only','设计画布 · 画布挂群标签页(缺则仅发链接)','opt'],
3921
- ['docs:document.comment:read','设计画布 · 读文档评论(第二意见通道)','opt'],
3922
- ['docs:document.comment:update','设计画布 · 评论标记已解决(反馈闭环)','opt'],
3923
- ['docs:document.media:download','设计画布 · 下载评论附图/画板素材','opt'],
3924
- ];
3925
- const scopeRows=scopes.map(([sc,us,lv])=>`<div class="fx-scope"><span class="fx-lv ${lv}">${lvTxt[lv]}</span><span class="sc">${sc}</span><span class="us">${us}</span></div>`).join('');
3926
- // 开放平台「批量导入/导出权限」认的格式;全表导出,不用画布的自己删那组(按钮上 data-json,点击处读)
3927
- const scopeJson=JSON.stringify({scopes:{tenant:scopes.map(x=>x[0]),user:[]}});
3928
- const open=fxFoldUser.feishu===undefined?false:fxFoldUser.feishu; // 默认一律收起(用户 10-12 定)
3929
- const brief=on?[(fe.identity&&fe.identity.name)||'',fe.onlyWhenMentioned?'只答 @':'答所有消息',fl.ready?'飞书登录已就绪':'飞书登录未就绪',fe.admins&&fe.admins.length?`管理员 ${fe.admins.length} 人`:''].filter(Boolean).join(' · '):'展开填写';
3930
- return `<div class="card" style="grid-column:1/-1" id="feishuCard"><details class="fx-fold" data-fold="feishu"${open?' open':''}>
3931
- <summary class="fx-head"><div class="lbl" style="margin:0;display:flex;align-items:center;min-width:0"><span class="caret">${IC.chevronRight}</span>${platLogo('feishu')}飞书应用<span class="fold-hint">${esc(brief)}</span></div>
3932
- <span class="fx-status ${on?'on':''}"><span class="dot ${on?'ok':'skip'}"></span>${on?('已配置'+(fe&&fe.identity&&fe.identity.name?' · '+esc(fe.identity.name):'')):'未配置'}</span></summary>
3933
- <details class="fx-guide">
3934
- <summary><span class="caret">${IC.chevronRight}</span><span>如何在飞书开放平台开通</span><span class="sp"></span><a class="lnk" href="https://open.feishu.cn/app" target="_blank" rel="noopener">开放平台 ${IC.ext}</a></summary>
3935
- <div class="fx-body">
3936
- <div class="fx-step"><span class="n">1</span><span class="t">新建<b>自建应用</b> → 「添加应用能力」加上 <b>机器人</b>(否则拉不进群、收不到消息)。建应用时<b>图标必填</b>,选一个内置图标即可。下面这套 scope 在开放平台里全部标「免审权限」,自建应用发版也免审(创建者即企业管理员时自动通过)—— <b>不需要企业认证 / 营业执照</b>,个人用手机号建的团队就能跑通。</span></div>
3937
- <div class="fx-step"><span class="n">2</span><span class="t"><b>事件订阅</b> → 连接方式选 <b>长连接</b> → 订阅事件 <code>im.message.receive_v1</code>(接收群消息);建议同时订阅 <code>im.message.recalled_v1</code>(后台按中文名索引 —— 添加事件时搜「<b data-i18n-skip>撤回</b>」选「消息撤回」;权限即下面清单里的 im:message:readonly。作用:用户撤回后 bot 自动跳过/停止处理该消息;不订阅则此功能不生效)。</span></div>
3938
- <div class="fx-step"><span class="n">3</span><span class="t"><b>回调配置</b>(${IC.warn} 与「事件订阅」<b>并排的另一个页签</b>,极易漏)→ 订阅方式同样选 <b>长连接</b> → 添加「<b>卡片回传交互 card.action.trigger</b>」。作用:群里卡片上的按钮(画布反馈的「同步/忽略」、各类确认按钮)点了才有响应;<b>漏配的症状 = 点按钮弹「尚未配置卡片回调」</b>。这项平台没有查询接口,预检无法自动核对 —— 见到那个报错就是漏了这里(配完照样要发版)。</span></div>
3939
- <div class="fx-step"><span class="n">4</span><span class="t"><b>权限管理 → 权限配置</b> 开通以下 scope(逐条搜索勾选;或点「批量导入/导出权限」,把 <button class="btn" id="fCopyScopes" data-json="${esc(scopeJson)}" style="padding:1px 8px;font-size:11px">复制导入 JSON</button> 粘进去 —— 那个编辑器只认手工粘贴。不用画布可把「设计画布」那组从 JSON 里删掉):<div class="fx-scopes">${scopeRows}</div></span></div>
3940
- <div class="fx-step"><span class="n">5</span><span class="t"><b>权限管理 → 数据权限</b>:把<b>通讯录授权范围</b>设为 <b>全部成员</b>(或指定人)。只开 scope 不设范围,「从通讯录选管理员」和发送者真名会报 <code>40004</code> / 回退 open_id —— <b>scope ≠ 数据授权范围</b>。另:<b>应用发布 → 可用范围</b>须包含要拉进群的管理员,否则建群报 <code>232043 bot invisible</code>(数据授权范围管「读到谁」,可用范围管「bot 对谁可见 / 能拉谁」)。</span></div>
3941
- <div class="fx-step"><span class="n">6</span><span class="t"><b>创建版本并发布</b> —— 机器人能力 / 权限 / <b>事件订阅</b> / <b>回调配置</b>改动都<b>发版后才生效</b>(数据授权范围通常即时);长连接连着 ≠ 收得到新订阅的事件,不发版事件无声无息。填好下面凭证点「预检」逐项核对。</span></div>
3942
- <div class="fx-step"><span class="n">7</span><span class="t">${IC.warn} <b>换了 appId / appSecret(比如换应用、换企业主体)之后必须重启守护进程</b> —— 飞书长连接在启动时就绑死了凭证,这里保存只是热重载配置、不会重建连接。不重启的症状:新群发消息<b>没有任何反应</b>,日志里出现 <code>230002 Bot/User can NOT be out of the chat</code>(旧应用的连接在替新群发通知)。</span></div>
3943
- </div>
3971
+ const configured=!!(fe&&(fe.appId||fe.appIdRef)&&fe.appSecretStatus&&fe.appSecretStatus!=='none');
3972
+ const c=connDict(S.conn).feishu,online=!!(c&&c.state==='connected');
3973
+ const status=online?'已连接':configured?'等待连接':'尚未接入';
3974
+ const app=(fe&&fe.identity)||{};
3975
+ const appUrl=fe&&/^cli_[A-Za-z0-9]+$/.test(fe.appId||'')?'https://open.feishu.cn/app/'+encodeURIComponent(fe.appId):'https://open.feishu.cn/app';
3976
+ const groupCount=(S.groups||[]).filter(g=>!g.platform||g.platform==='feishu').length;
3977
+ return `<div class="card feishu-setup" id="feishuCard" style="grid-column:1/-1">
3978
+ <div class="feishu-heading"><div><div class="lbl">${platLogo('feishu')}飞书</div><div class="feishu-title">${esc(app.name||(configured?'项目群已配置':'扫码,把项目带进飞书'))}</div></div><span class="fx-status ${online?'on':''}"><span class="dot ${online?'ok':configured?'warn':'skip'}"></span>${status}</span></div>
3979
+ <p class="hint">${configured?'在项目群里发任务,机器人会把进度和结果回复到群里。':'用手机飞书确认一次,自动创建机器人和项目群。'}</p>
3980
+ ${configured?`<div class="feishu-current"><span>已绑定项目群</span><strong>${groupCount}</strong><span>消息响应</span><strong>${fe.onlyWhenMentioned?'仅回复 @ 机器人的消息':'接收群内普通消息'}</strong></div>
3981
+ ${!online?'<p class="hint">应用已配置,连接尚未确认。请确认这台 Mac 上的 SintBot 正在运行。</p>':''}
3982
+ <div class="feishu-actions"><button class="btn primary" data-gotab="groups">管理项目群</button><a class="btn" href="https://sintbot.com/guide/usage/" target="_blank" rel="noopener">查看使用指南</a></div>`:''}
3983
+ <details class="feishu-details" ${configured?'':'open'}><summary>首次接入:三步开始</summary><ol class="feishu-steps">
3984
+ <li><b>手机扫码</b><span>按安装指南在 Mac 上开始接入,用手机飞书选择企业或团队身份,确认授权。</span></li>
3985
+ <li><b>自动建群</b><span>接入流程创建机器人应用、项目目录和群,把你加入群并保存配置。</span></li>
3986
+ <li><b>群里发任务</b><span>按提示启动后,在新群发「读取 README 第一行」。收到回复就可以开始工作。</span></li>
3987
+ </ol><div class="feishu-actions"><a class="btn ${configured?'':'primary'}" href="https://sintbot.com/guide/install/#feishu" target="_blank" rel="noopener">查看扫码接入指南</a></div>
3988
+ <p class="hint">已经接入的应用可以继续使用,添加项目群请到「群 · 项目」。</p></details>
3989
+ <details class="feishu-details"><summary>接入遇到问题</summary><dl class="feishu-help">
3990
+ <dt>没有企业或团队身份</dt><dd>先在飞书创建团队或切换到已有团队,再开始扫码。</dd>
3991
+ <dt>需要管理员批准</dt><dd>按飞书页面提示联系组织管理员。权限和发布审核以所在组织的设置为准。</dd>
3992
+ <dt>扫码后没有建群成功</dt><dd>按接入流程给出的提示续做;已有应用可以复用,无需反复新建。</dd>
3993
+ <dt>消息没有回复</dt><dd>确认机器人在群内、项目群已绑定,且这台 Mac 上的 SintBot 正在运行。</dd>
3994
+ <dt>模型卡点了没反应</dt><dd>在应用后台检查卡片回调是否使用长连接,并订阅 card.action.trigger;修改后发布应用版本。</dd>
3995
+ </dl><a class="lnk" href="${appUrl}" target="_blank" rel="noopener">打开飞书应用后台 ${IC.ext}</a></details>
3996
+ <details class="feishu-details"><summary>高级:应用信息与手动接入</summary>
3997
+ <p class="hint">当前应用信息仅供核对;此页不会修改应用凭据或重新扫码。</p>
3998
+ <dl class="feishu-help"><dt>App ID</dt><dd class="mono">${esc(fe&&(fe.appId||(fe.appIdRef?'env:'+fe.appIdRef:''))||'未配置')}</dd>
3999
+ <dt>应用凭据</dt><dd>${configured?'已设置,不显示密钥':'尚未配置完整'}</dd>
4000
+ <dt>Bot ID</dt><dd class="mono">${esc(fe&&fe.botOpenId||'尚未获取')}</dd></dl>
4001
+ <p class="hint">更换应用或凭据需要在这台 Mac 上完成,并在所有任务结束后重新启动。请保留已有配置和任务记录。</p>
4002
+ <p class="hint">手动接入需配置机器人、消息事件和卡片回调、必要权限与可用范围,再发布应用版本。</p>
4003
+ <a class="lnk" href="https://sintbot.com/guide/install/#feishu" target="_blank" rel="noopener">查看完整安装指南 ${IC.ext}</a>
3944
4004
  </details>
3945
- <div class="fx-sub">凭证</div>
3946
- <div class="grid cols2" style="row-gap:0">
3947
- <div class="field"><label class="f">appId</label><input id="fId" placeholder="cli_..." value="${esc(fe?fe.appId:'')}"/>${fe&&fe.appIdRef?`<div class="hint" style="margin-top:5px">当前:env:${esc(fe.appIdRef)}</div>`:''}</div>
3948
- <div class="field"><label class="f">appSecret ${fe&&fe.appSecretStatus!=='none'?'<span class="faint">(留空=保持)</span>':''}</label><input id="fSecret" placeholder="${fe&&fe.appSecretStatus!=='none'?'已设置 · 留空保持':'输入 appSecret'}"/></div>
3949
- </div>
3950
- <div class="inline" style="flex-wrap:wrap;margin:2px 0 2px"><button class="btn" id="fPrecheck">${IC.steth} 预检并自动获取</button><span class="faint" style="font-size:11px">验证凭证 / bot 身份 / 权限,自动回填 botOpenId</span></div>
3951
- <div id="fCheck"></div>
3952
- <div class="fx-sub" style="margin-top:22px">身份</div>
3953
- ${fxIdentity(fe)}
3954
- <div class="field"><label class="f">bot openId</label><input id="fBot" placeholder="ou_…(预检自动获取)" value="${esc(fe?fe.botOpenId:'')}"/></div>
3955
- <div class="field"><label class="f">管理员 <span class="faint" style="text-transform:none;letter-spacing:0">— 可多个 · 建群时可选择拉入</span></label>
3956
- <div id="fAdminList" class="fx-admins"></div>
3957
- <button class="btn" id="fAdminOrg" title="从组织通讯录搜索并选人加为管理员">${IC.plus} 从通讯录添加</button>
3958
- <div id="fAdminPick"></div>
3959
- </div>
3960
- <div class="fx-sub" style="margin-top:22px">用飞书登录控制台</div>
3961
- <div class="field"><label class="f">公网地址 <span class="faint" style="text-transform:none;letter-spacing:0">— https://域名(不带路径);填好后登录页出现「用飞书登录」,只放行上面的管理员</span></label>
3962
- <div class="inline"><input id="fPublicUrl" placeholder="https://console.example.com" value="${esc(fl.publicUrl||'')}" style="flex:1"/><button class="btn" id="fPubSave">保存地址</button></div>
3963
- ${fl.redirectUri?`<div class="hint" style="margin-top:6px">开放平台「安全设置 → 重定向 URL」加这一条:<span class="mono">${esc(fl.redirectUri)}</span> <button class="btn" id="fPubCopy" data-uri="${esc(fl.redirectUri)}" style="padding:2px 8px;margin-left:6px">复制</button></div>`:''}
3964
- <div class="hint" style="margin-top:4px">${fl.ready?'<span style="color:var(--ok)">'+IC.check+' 飞书登录已就绪</span>':'飞书登录未就绪:'+esc(fl.why||'')}</div>
3965
- </div>
3966
- <div class="field" style="margin-top:8px"><label class="f">群消息响应范围</label>
3967
- <select id="fGate"><option value="" ${fe&&fe.onlyWhenMentioned?'':'selected'}>所有消息(仅跳过只@他人的)— 依赖模型 NO_REPLY 自判静默</option><option value="1" ${fe&&fe.onlyWhenMentioned?'selected':''}>仅 @bot 的消息</option></select></div>
3968
- <button class="btn primary" id="fSave">保存飞书</button>
4005
+ </div>`;
4006
+ }
4007
+ function fxFeishuAccessCard(fe){
4008
+ const fl=Object.assign({},(S.net||{}).feishuLogin||{},{publicUrl:(S.net||{}).publicUrl||''});
4009
+ return `<div class="card" id="feishuAccessCard" style="grid-column:1/-1"><details class="fx-fold" data-fold="feishuAccess"${fxFoldUser.feishuAccess?' open':''}>
4010
+ <summary class="fx-head"><div class="lbl" style="margin:0"><span class="caret">${IC.chevronRight}</span>管理员与控制台登录</div><span class="fold-hint">按需设置</span></summary>
4011
+ <p class="hint">机器人在群里工作不需要公网地址。需要从外网管理这台 Mac 时,再配置飞书登录。</p>
4012
+ <div class="fx-sub">允许登录的管理员</div><p class="hint">公网控制台仅允许以下飞书账号登录。扫码接入时会加入扫码确认的账号。</p>
4013
+ <div id="fAdminList" class="fx-admins"></div><button class="btn" id="fAdminOrg">${IC.plus} 从通讯录添加</button><div id="fAdminPick"></div>
4014
+ <div class="fx-sub">公网控制台地址</div><p class="hint">先准备可访问的 HTTPS 地址,再在「服务器」中启用公网飞书登录。</p>
4015
+ <div class="field"><label class="f" for="fPublicUrl">HTTPS 地址</label><div class="feishu-actions"><input id="fPublicUrl" type="url" placeholder="https://console.example.com" value="${esc(fl.publicUrl||'')}"/><button class="btn" id="fPubSave">保存地址</button></div></div>
4016
+ ${fl.redirectUri?`<p class="hint">在飞书应用后台「安全设置 → 重定向 URL」添加:</p><div class="feishu-actions"><code class="feishu-uri">${esc(fl.redirectUri)}</code><button class="btn" id="fPubCopy" data-uri="${esc(fl.redirectUri)}">复制回调地址</button></div>`:''}
4017
+ <p class="hint">${fl.ready?'飞书登录配置已就绪':!fl.publicUrl?'未设置公网地址,仅使用本机控制台时无需填写':'飞书登录未就绪:'+esc(fl.why||'')}</p>
3969
4018
  </details></div>`;
3970
4019
  }
3971
4020
  // Slack 应用身份条:bot 名 + workspace(没预检过就直说"尚未获取",别留白 —— 留白看起来像坏了)
@@ -4345,12 +4394,68 @@ updateThemeBtn();
4345
4394
  /* I18N_DICT_BEGIN —— 合并翻译片段时整块替换这两个数组,不要改此标记之外的代码
4346
4395
  ⚠ 正则一律 ^…$ 整串、不加 g 标志;更具体的条目必须排在泛化条之前。 */
4347
4396
  const I18N_EN=[
4397
+ ["扫码,把项目带进飞书", "Scan to connect your project to Feishu"],
4398
+ ["项目群已配置", "Project groups configured"],
4399
+ ["尚未接入", "Not connected yet"],
4400
+ ["等待连接", "Waiting for connection"],
4401
+ ["在项目群里发任务,机器人会把进度和结果回复到群里。", "Send a task in your project group. The bot replies with progress and results."],
4402
+ ["用手机飞书确认一次,自动创建机器人和项目群。", "Confirm in Feishu on your phone to create the bot and project group automatically."],
4403
+ ["已绑定项目群", "Bound project groups"],
4404
+ ["消息响应", "Messages"],
4405
+ ["仅回复 @ 机器人的消息", "Replies when the bot is mentioned"],
4406
+ ["接收群内普通消息", "Receives regular group messages"],
4407
+ ["应用已配置,连接尚未确认。请确认这台 Mac 上的 SintBot 正在运行。", "The app is configured, but its connection is not confirmed. Check that SintBot is running on this Mac."],
4408
+ ["管理项目群", "Manage project groups"],
4409
+ ["查看使用指南", "Read the usage guide"],
4410
+ ["首次接入:三步开始", "First connection: three steps"],
4411
+ ["手机扫码", "Scan with your phone"],
4412
+ ["按安装指南在 Mac 上开始接入,用手机飞书选择企业或团队身份,确认授权。", "Start the connection on your Mac using the install guide. Select an enterprise or team identity in Feishu on your phone and confirm."],
4413
+ ["自动建群", "Create the group automatically"],
4414
+ ["接入流程创建机器人应用、项目目录和群,把你加入群并保存配置。", "The setup creates the bot app, project folder and group, adds you to the group and saves the configuration."],
4415
+ ["群里发任务", "Send a task in the group"],
4416
+ ["按提示启动后,在新群发「读取 README 第一行」。收到回复就可以开始工作。", "Start as instructed, then ask the new group to read the first line of README. Once it replies, you can start working."],
4417
+ ["查看扫码接入指南", "Read the scan setup guide"],
4418
+ ["已经接入的应用可以继续使用,添加项目群请到「群 · 项目」。", "Keep using your connected app. Add project groups under Groups · Projects."],
4419
+ ["接入遇到问题", "Connection help"],
4420
+ ["没有企业或团队身份", "No enterprise or team identity"],
4421
+ ["先在飞书创建团队或切换到已有团队,再开始扫码。", "Create a team in Feishu or switch to an existing team before scanning."],
4422
+ ["需要管理员批准", "Administrator approval required"],
4423
+ ["按飞书页面提示联系组织管理员。权限和发布审核以所在组织的设置为准。", "Follow the Feishu page and contact your organization administrator. Permissions and publishing approval depend on your organization settings."],
4424
+ ["扫码后没有建群成功", "Group creation did not complete"],
4425
+ ["按接入流程给出的提示续做;已有应用可以复用,无需反复新建。", "Follow the setup instructions to continue. Reuse the existing app rather than creating it again."],
4426
+ ["消息没有回复", "Messages get no reply"],
4427
+ ["确认机器人在群内、项目群已绑定,且这台 Mac 上的 SintBot 正在运行。", "Check that the bot is in the group, the group is bound to a project, and SintBot is running on this Mac."],
4428
+ ["模型卡点了没反应", "Model card taps do not respond"],
4429
+ ["在应用后台检查卡片回调是否使用长连接,并订阅 card.action.trigger;修改后发布应用版本。", "In the app settings, use a long connection for card callbacks and subscribe to card.action.trigger. Publish an app version after changes."],
4430
+ ["打开飞书应用后台", "Open Feishu app settings"],
4431
+ ["高级:应用信息与手动接入", "Advanced: app details and manual setup"],
4432
+ ["当前应用信息仅供核对;此页不会修改应用凭据或重新扫码。", "App details are read-only here. This page does not change credentials or start another scan."],
4433
+ ["应用凭据", "App credentials"],
4434
+ ["已设置,不显示密钥", "Configured; secret is not displayed"],
4435
+ ["尚未配置完整", "Configuration incomplete"],
4436
+ ["更换应用或凭据需要在这台 Mac 上完成,并在所有任务结束后重新启动。请保留已有配置和任务记录。", "Change the app or credentials on this Mac, then restart after all tasks finish. Preserve the existing configuration and task records."],
4437
+ ["手动接入需配置机器人、消息事件和卡片回调、必要权限与可用范围,再发布应用版本。", "Manual setup requires the bot, message events, card callbacks, permissions and app availability, followed by publishing an app version."],
4438
+ ["查看完整安装指南", "Read the full install guide"],
4439
+ ["管理员与控制台登录", "Administrators and console sign-in"],
4440
+ ["按需设置", "Optional"],
4441
+ ["机器人在群里工作不需要公网地址。需要从外网管理这台 Mac 时,再配置飞书登录。", "Group tasks do not require a public URL. Configure Feishu sign-in when you need to manage this Mac remotely."],
4442
+ ["允许登录的管理员", "Administrators allowed to sign in"],
4443
+ ["公网控制台仅允许以下飞书账号登录。扫码接入时会加入扫码确认的账号。", "Only these Feishu accounts can sign in to the public console. Setup adds the account that confirms the scan."],
4444
+ ["公网控制台地址", "Public console URL"],
4445
+ ["先准备可访问的 HTTPS 地址,再在「服务器」中启用公网飞书登录。", "Prepare an accessible HTTPS URL, then enable public Feishu sign-in under Server."],
4446
+ ["HTTPS 地址", "HTTPS URL"],
4447
+ ["在飞书应用后台「安全设置 → 重定向 URL」添加:", "Add this in the Feishu app under Security settings → Redirect URL:"],
4448
+ ["复制回调地址", "Copy callback URL"],
4449
+ ["飞书登录配置已就绪", "Feishu sign-in configuration is ready"],
4450
+ ["未设置公网地址,仅使用本机控制台时无需填写", "No public URL set. Leave it empty when using only the local console."],
4451
+ ["尚未添加管理员;使用公网飞书登录前请先添加。", "No administrators added. Add one before using public Feishu sign-in."],
4452
+
4348
4453
  ["正在处理任务", "Task in progress"],
4349
4454
  ["已受理", "Accepted"],
4350
4455
  ["等待模型读取,尚未确认加入", "Waiting for the model to read it; not yet confirmed joined"],
4351
4456
  ["处理状态请查看实时活动", "Check live activity for processing status"],
4352
4457
  ["需要处理", "Needs attention"],
4353
- ["可绑定新群,修改模型、项目目录、权限、数据输出和忙时跟进;该群须没有进行中的任务和未结记录。切模型或项目会开始新的一段对话。停止任务和开新会话在「概览」,开新会话须等该群空闲。群里发 /model 也能切换模型。", "Bind new groups and change the model, project folder, permissions, data display and busy-turn followups of a group with no running task or unsettled record. Changing the model or project starts a new conversation. Stop tasks and start new sessions in Overview; a new session needs the group to be idle. Members can also switch models with /model in the group."],
4458
+ ["可绑定新群,修改后端、型号、思考深度、项目目录、权限、数据输出和忙时跟进;该群须没有进行中的任务和未结记录。换后端或项目会开始新的一段对话,换型号或调深度接着当前对话。停止任务和开新会话在「概览」,开新会话须等该群空闲。群里发 /model 也能改。", "Bind new groups and change the backend, model, thinking depth, project folder, permissions, data display and busy-turn followups of a group with no running task or unsettled record. Changing the backend or project starts a new conversation; changing the model or depth continues it. Stop tasks and start new sessions in Overview; a new session needs the group to be idle. Members can also change these with /model in the group."],
4354
4459
  ["当前支持飞书文字消息、忙时跟进、任务回复和停止。定时任务、生图、群消息管理、画布与预览暂未接入。", "Feishu text messages, busy-turn followups, task replies and stopping are supported. Schedules, image generation, group message management, canvas and previews are unavailable."],
4355
4460
  ["当前不执行定时任务;已有定义仅供查看。", "Schedules are not executed. Existing definitions are shown for reference only."],
4356
4461
  ["画布创建、同步与反馈暂未接入;已有链接仅供查看。", "Canvas creation, sync and feedback are unavailable. Existing links are view-only."],
@@ -4361,7 +4466,7 @@ const I18N_EN=[
4361
4466
  ["Codex 与 Claude Code 默认开启忙时跟进;长命令执行期间的补充会在工具步骤之间读取。只有确认模型已读取才显示已加入;不支持此能力时排队。控制台插话同样支持忙时跟进;关闭或暂不支持时排队。", "Busy-turn followups are on by default for Codex and Claude Code. Followups are read between tool steps; a long command must finish first. Joined means the model has read the message. Unsupported clients queue followups. Console messages also support busy-turn followups, or queue when followups are disabled or unavailable."],
4362
4467
  ["当前版本未接入预览服务。", "Preview services are unavailable in this version."],
4363
4468
  ["当前可查看模型配置与登录状态;各群的模型在「群·项目」里切换。添加或修改模型暂不可用。", "Model configuration and login status are shown here. Switch each group's model under Groups. Adding or changing models is unavailable."],
4364
- ["可添加模型,修改型号、端点、密钥、思考深度与推理强度,测试、隐藏或拖动排序;各群的模型在「群·项目」里切换。改型号、后端或端点时,用它的群须空闲,并会开始新的一段对话;默认模型和群正在用的模型不能隐藏。移除模型暂不可用。", "Add models; change their model id, endpoint, key, thinking depth and reasoning effort; test, hide or drag to reorder them. Switch each group's model under Groups. Changing a model id, backend or endpoint needs the groups using it to be idle and starts a new conversation for them. The default model and models in use by a group cannot be hidden. Removing models is unavailable."],
4469
+ ["可添加模型,修改端点、密钥、上下文与压缩,测试、隐藏或拖动排序;型号与思考深度按群在「群·项目」里选(第三方端点的型号在这里填)。改后端或端点时,用它的群须空闲,并会开始新的一段对话;默认模型和群正在用的模型不能隐藏。移除模型暂不可用。", "Add models; change their endpoint, key, context and compaction; test, hide or drag to reorder them. Model and thinking depth are chosen per group under Groups (a third-party endpoint's model is set here). Changing a backend or endpoint needs the groups using it to be idle and starts a new conversation for them. The default model and models in use by a group cannot be hidden. Removing models is unavailable."],
4365
4470
  ["默认开启 · 下轮生效", "On by default · Applies next turn"],
4366
4471
  ["当前只能添加或修改 Codex 或 Claude Code 模型", "Only Codex or Claude Code models can be added or changed right now."],
4367
4472
  ["模型上限", "Model limit"],
@@ -4386,6 +4491,9 @@ const I18N_EN=[
4386
4491
  ["型号与推理", "Model and reasoning"],
4387
4492
  ["上下文与压缩", "Context and compaction"],
4388
4493
  ["连接与登录", "Connection and sign-in"],
4494
+ ["环境冲突", "Environment conflicts"],
4495
+ ["环境冲突:", "Environment conflicts: "],
4496
+ ["这些设置在 SintBot 之外,可能改变 CLI 的接入点或型号;只列键名,不含值", "These settings live outside SintBot and may change the CLI endpoint or model; key names only, no values"],
4389
4497
  ["模型容量参考", "Model capacity reference"],
4390
4498
  ["目录默认窗口", "Catalog default context"],
4391
4499
  ["最近上报可用窗口", "Last reported usable context"],
@@ -4579,6 +4687,24 @@ const I18N_EN=[
4579
4687
  ["管理模型 · 拖","Manage models · drag"],
4580
4688
  ["排序 ·","to reorder ·"],
4581
4689
  ["配端点/key/思考深度 · 模型按群在「群·项目」选","sets endpoint / key / thinking depth · models are chosen per group on the Groups · Projects page"],
4690
+ ["配端点/key/压缩 · 型号与思考深度按群在「群·项目」选", "sets endpoint / key / compaction · model and thinking depth are chosen per group on the Groups · Projects page"],
4691
+ ["按群选", "Per group"],
4692
+ ["个群在用", "groups using it"],
4693
+ ["型号与深度", "Model and depth"],
4694
+ ["型号与思考深度按群在「群·项目」里选", "Model and thinking depth are chosen per group under Groups"],
4695
+ [";这个端点的型号在下面填,各群共用", "; this endpoint's model is set below and shared by its groups"],
4696
+ ["名称与型号", "Name and model"],
4697
+ ["后端", "Backend"],
4698
+ ["未选", "Not chosen"],
4699
+ ["加载清单…", "loading list…"],
4700
+ ["在「模型」页改", "change on the Models page"],
4701
+ ["清单暂时取不到", "List unavailable right now"],
4702
+ ["这个型号不支持", "Not supported by this model"],
4703
+ ["上下文", "Context"],
4704
+ ["型号 id", "Model id"],
4705
+ ["换后端会开新对话;换型号或调深度接着当前对话 · 群空闲时才能改", "A backend change starts a new conversation; a model or depth change continues it · only while the group is idle"],
4706
+ ["没有变化", "No change"],
4707
+ ["填写型号 id", "Enter a model id"],
4582
4708
  ["添加模型","Add model"],
4583
4709
  ["插件库 · MCP","Plugin library · MCP"],
4584
4710
  ["本机装好的 MCP · 按运行时分两套 · 命令行安装、即时刷新","MCP servers installed on this machine · two sets, one per runtime · installed from the command line, refreshes instantly"],