sintbot 1.4.4 → 1.4.6
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/CHANGELOG.en.json +50 -0
- package/CHANGELOG.json +50 -0
- package/docs/AI-ASSISTED-INSTALL.md +2 -2
- package/docs/install.md +9 -6
- package/docs/zh-CN/AI-ASSISTED-INSTALL.md +2 -2
- package/docs/zh-CN/install.md +9 -6
- package/install.sh +1 -1
- package/package.json +1 -1
- package/src/agent/codexapp.js +49 -3
- package/src/agent/codexstatus.js +130 -5
- package/src/c2configcontrol.js +86 -10
- package/src/c2console.js +52 -4
- package/src/core/c2durableruntime.js +12 -2
- package/src/core/v2engine.js +177 -15
- package/src/ui/console.html +64 -21
- package/src/ui/serverbase.js +59 -19
package/src/ui/console.html
CHANGED
|
@@ -1039,7 +1039,7 @@ const IC=new Proxy({},{get:(_,k)=>`<svg class="ic ic-${k}" aria-hidden="true"><u
|
|
|
1039
1039
|
const PLAT_LOGO={feishu:'data:image/png;base64,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',slack:'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAAAiCAMAAADiW5DOAAABDlBMVEUAAAD/xgD/swD/xgD/wgDgCnDrCmxIt1hDtFrjB2o+ulrkB2tEu13mBmr5wQbkBWdCuFdBtFr6vwUAsv8Asf8AtP8Asv8As/8AtP9AuFb7vwTnB23/vgMAsv//wANBtVdBtVnmBmwAs//8wQMAtP8As//jBWsAs//jB2xDtVf6wAUAsv/lBmnmBmxDt1nmB2vkB2pBt1gAtP/9wgNCt1n8wAP8wANCtlgAs/8AtP8Atf8Atv8At/8AuP9BtlhBt1hCuFlCuVlCuVpCulpDu1pDvFtEvVxEvlzjBmrkBmrkBmvlBmvnBmzoBmzqBm3rBm7tBm/8wAP9wQP/wgP/wwP/xAP/xQP/xgP/xwP/yAN1pSH6AAAAOHRSTlMACQoSFRkaICIkJSYpKS0vMjM4PD5BQkNERERLS0xNT1ZaXmJjZWZsbW9xfn6Cio2OkZaam6qutiE9PJgAAAEnSURBVDjLrdBnV8MgFAbg1733qHvvvSWNiaMOqq2WVq3+/z8i3EgCiuegx/fLvQeehAtAlqGIRcOqmSvEN51wZZSp5ID1WKXbZUIyIUAkvnYZlkSb2GUCD7PpYXDgYXT+aHIhS5NfsMzReXzVL+sIs7JkmFOq7UBkm7xhknqbPl2ab+YCOLNJoLcKurkDxmyzDOzT1rg2bXLo6cD4y6q66Y48YPbzgpcD+MesFbmMbHZl2VMrxyJLdUouzHOemC2q28CJsDIIlLW5p1oEbCLeAK5N2nwxNeBJf56aqm3qQAftrBhm0TYNcuiuR16ehGEw82JM02S+QWZ+zq8MnXHoYcSGh3n2MMJlSkRK+g3rLtNHpgeYINPsHLq18lBpUU3ve+21MVv/AMnhmxQ0lrxLAAAAAElFTkSuQmCC',wecom:'data:image/png;base64,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'};
|
|
1040
1040
|
const platLogo=(k)=>`<img class="plat-logo" src="${PLAT_LOGO[k]}" alt="" width="18" height="18">`;
|
|
1041
1041
|
const esc=s=>String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
|
|
1042
|
-
let S=null, activity=[], usage={}, codexStatus={}; // codexStatus[name] = {loggedIn,mode,usageText} — 点「验证登录」后填,跨渲染保留
|
|
1042
|
+
let S=null, activity=[], usage={}, codexStatus={}; // codexStatus[name] = {loggedIn,mode,dead,verified,text,usageText} — 点「验证登录」后填,跨渲染保留
|
|
1043
1043
|
const pad2=n=>String(n).padStart(2,'0');
|
|
1044
1044
|
/* 活动行等时间戳统一按 bot 工作时区(S.time.offsetMin,默认北京)显示 —— 和顶栏时钟、
|
|
1045
1045
|
群内时间口径一致;state 未加载时退回浏览器本地。 */
|
|
@@ -1162,7 +1162,7 @@ function renderOverview(){
|
|
|
1162
1162
|
<span class="meta" title="${esc(g.workdir)}">${esc(shortPath(g.workdir))}</span>
|
|
1163
1163
|
${liveOn&&!g.paused?`<button class="iconbtn sm" data-osay="${esc(g.chatId)}" title="向这个群插一句 —— 空闲时马上处理;正在忙时,加入当前任务或排在它后面(看「忙时跟进」)" style="flex:none">${IC.say}</button>`:''}
|
|
1164
1164
|
${liveOn?`<button class="iconbtn sm" data-ostop="${esc(g.chatId)}" title="停止当前任务(保留会话)" style="flex:none">${IC.square}</button>
|
|
1165
|
-
<button class="iconbtn sm" data-onew="${esc(g.chatId)}" title="开新会话 — 运行中会先中断当前任务并清空队列(同群内 /new)" style="flex:none">${IC.rotate}</button>`:''}</div>`).join('')
|
|
1165
|
+
<button class="iconbtn sm" data-onew="${esc(g.chatId)}" title="${S.runtimeCapabilities?'开新会话 — 仅在该群空闲、记录全部结账后可用;运行中请先停止,结束后再开(同群内 /new)':'开新会话 — 运行中会先中断当前任务并清空队列(同群内 /new)'}" style="flex:none">${IC.rotate}</button>`:''}</div>`).join('')
|
|
1166
1166
|
:'<div class="empty">还没有群项目 · 去「群·项目」配置</div>';
|
|
1167
1167
|
$('#overview').innerHTML=`${startCardHtml()}<div id="ovInboxWrap">${inboxHtml()}</div><div class="grid cols2 stagger">
|
|
1168
1168
|
<div class="card" id="ovHealth"><div class="lbl">健康</div>
|
|
@@ -1216,7 +1216,8 @@ function renderOverview(){
|
|
|
1216
1216
|
try{const r=await api('/api/groups/stop','POST',{chatId:b.dataset.ostop});toast(S.runtimeCapabilities?(r.ok?'已请求停止,等待结束确认':'当前没有可停止的运行任务'):(r.ok?'已停止当前任务':'没有正在运行的任务'),'ok');}catch(e){toast(e.message,'err');}});
|
|
1217
1217
|
$$('#overview [data-onew]').forEach(b=>b.onclick=async()=>{
|
|
1218
1218
|
const lv=S.live&&S.live.groups&&S.live.groups[b.dataset.onew];
|
|
1219
|
-
|
|
1219
|
+
// 持久处理流程里停止要等结账确认,不能当场接着重置会话:忙时由服务端照实拒绝,不弹「中断并开新」。
|
|
1220
|
+
if(!S.runtimeCapabilities&&lv&&lv.busy&&!confirm('该群任务运行中 — 中断当前任务、清空队列并开新会话?'))return;
|
|
1220
1221
|
try{const r=await api('/api/groups/interrupt','POST',{chatId:b.dataset.onew});toast(r.wasBusy?'已中断并开新会话':'已开新会话','ok');}catch(e){toast(e.message,'err');}});
|
|
1221
1222
|
updateOverviewLive();
|
|
1222
1223
|
}
|
|
@@ -1540,7 +1541,7 @@ function makeSortable(grid,{item,key,save}){
|
|
|
1540
1541
|
c.classList.remove('dragging');drag=null;
|
|
1541
1542
|
const order=cards().map(key);
|
|
1542
1543
|
if(order.join()===startOrder.join())return; // 没动(含 Esc 后浏览器自身取消),不保存
|
|
1543
|
-
try{
|
|
1544
|
+
try{const r=await save(order);S=r;toast('顺序已保存'+runtimeChangeNote(r&&r.providerChange),'ok');}
|
|
1544
1545
|
catch(err){restore(startOrder);toast(err.message,'err');}
|
|
1545
1546
|
});
|
|
1546
1547
|
}
|
|
@@ -1603,9 +1604,10 @@ function fmtReset(w){if(!w||w.resetsAt==null)return'';const ms=w.resetsAt>1e9?(w
|
|
|
1603
1604
|
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';}
|
|
1604
1605
|
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(' │ ');}
|
|
1605
1606
|
// 页面加载时从 /api/state 缓存回填 codex 登录态+额度 → 刷新不丢(客户端已有则不覆盖,避免盖掉刚验证的)。
|
|
1606
|
-
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,usageText};});}
|
|
1607
|
+
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};});}
|
|
1607
1608
|
// codex ⚙ 面板「Codex 订阅」区的状态 HTML(登录态 + 额度),点「刷新」后就地更新。
|
|
1608
|
-
|
|
1609
|
+
// 三态(服务端文案 text 是单一事实源):已验证 / 登录态已失效(服务端拒绝令牌 ⇒ 要重新登录)/ 未验证(探不到,不等于未登录)。
|
|
1610
|
+
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>`:''}`;}
|
|
1609
1611
|
// 首次打开控制台、服务端无缓存时,后台自动验一次 codex 登录+额度(查询不耗消息额度,服务端缓存),卡片状态无需手动点。
|
|
1610
1612
|
// 开局自动探一次 Claude 登录态(`claude auth status --json`,0.2s、不吃订阅额度),
|
|
1611
1613
|
// 与 autoVerifyCodex 对称 —— 让卡片一进来就说真话,而不是永远挂着「未验证」等人去点。
|
|
@@ -1616,7 +1618,7 @@ async function autoVerifyClaude(){
|
|
|
1616
1618
|
if(!has)return;
|
|
1617
1619
|
try{S.claudeStatus=await api('/api/claude/verify','POST',{});renderProviders();}catch(e){}
|
|
1618
1620
|
}
|
|
1619
|
-
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,usageText});renderProviders();}catch(e){}}
|
|
1621
|
+
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){}}
|
|
1620
1622
|
// Claude 订阅登录 chip:此前只能写死「登录态·未验证」(判据是「没填 baseUrl 就当登录了」),
|
|
1621
1623
|
// 真没登录也照样绿。现在有 `claude auth status --json` 这个官方非交互探针,报真状态;
|
|
1622
1624
|
// 探不到(老版本 CLI / 不在 PATH)显式说「未验证」,**不当成未登录** —— 两种情况要分得开。
|
|
@@ -1635,7 +1637,7 @@ function claudeChip(){
|
|
|
1635
1637
|
function renderProviders(){
|
|
1636
1638
|
const cards=S.providers.filter(p=>showAll||(!p.hidden&&p.configured)).map(p=>{
|
|
1637
1639
|
const cs=p.runner==='codex'?codexStatus[p.name]:null;
|
|
1638
|
-
const chip=p.runner==='codex'?(cs&&cs.loggedIn?`<span class="chip set" title="
|
|
1640
|
+
const chip=p.runner==='codex'?(cs&&cs.loggedIn&&cs.verified?`<span class="chip set" title="${esc(cs.text||'已验证')}">订阅登录 ${IC.check}</span>`:cs&&cs.dead?`<span class="chip none" title="${esc(cs.text||'')}">登录态已失效</span>`:'<span class="chip" title="点下方「验证登录」确认 codex 订阅登录态 + 拉取额度">订阅登录·未验证</span>'):p.loggedIn?claudeChip():`<span class="chip ${p.keyStatus}">${p.keyStatus==='set'?'key 已设':p.keyStatus==='env'?'env:'+esc(p.keyRef):'无 key'}</span>`;
|
|
1639
1641
|
return `<div class="card pcard${p.hidden?' is-hidden':(!p.configured?' is-unconfig':'')}" data-prov="${esc(p.name)}">
|
|
1640
1642
|
<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>
|
|
1641
1643
|
<div class="inline" style="gap:8px">${chip}<button class="iconbtn cfg" data-act="edit" title="配置模型">${IC.settings}</button></div></div>
|
|
@@ -1653,7 +1655,7 @@ function renderProviders(){
|
|
|
1653
1655
|
const extra=S.providers.filter(p=>p.hidden||!p.configured).length;
|
|
1654
1656
|
const toggle=extra?`<button class="btn ghost" id="showAll">${showAll?'只看已配置':`显示全部 (${extra})`}</button>`:'';
|
|
1655
1657
|
$('#providers').innerHTML=`<div class="inline" style="justify-content:space-between;align-items:flex-start;margin:0 2px 14px;gap:10px">
|
|
1656
|
-
<div class="hint" style="flex:1">${S.runtimeCapabilities?esc(RUNTIME_UI_TEXT.providers):`管理模型 · 拖 ${IC.grip} 排序 · ${IC.settings} 配端点/key/思考深度 · 模型按群在「群·项目」选`}</div>
|
|
1658
|
+
<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>
|
|
1657
1659
|
<div class="inline" style="gap:8px">${toggle}<button class="btn primary" id="addModel">${IC.plus} 添加模型</button></div></div>
|
|
1658
1660
|
<div class="grid cols2 stagger" id="pgrid">${cards}</div>
|
|
1659
1661
|
<h2 class="sec" style="margin-top:32px">插件库 · MCP</h2>
|
|
@@ -1691,7 +1693,7 @@ function renderProviders(){
|
|
|
1691
1693
|
$('#styleNew').onclick=()=>openStyleModal(null);
|
|
1692
1694
|
if($('#showAll'))$('#showAll').onclick=()=>{showAll=!showAll;renderProviders();};
|
|
1693
1695
|
$('#addModel').onclick=()=>openProviderModal(null);
|
|
1694
|
-
if(
|
|
1696
|
+
if(runtimeAllows('POST','/api/providers/order'))makeSortable($('#pgrid'),{item:'.pcard',key:c=>c.dataset.prov,save:o=>api('/api/providers/order','POST',{order:o})});
|
|
1695
1697
|
$$('#pgrid .pcard').forEach(card=>{
|
|
1696
1698
|
const name=card.dataset.prov, slot=$('.reply-slot',card);
|
|
1697
1699
|
card.addEventListener('click',async e=>{
|
|
@@ -1705,7 +1707,7 @@ function renderProviders(){
|
|
|
1705
1707
|
if(cx){const r=await api('/api/codex/verify','POST',{force:true});const lg=r.login||{};text=fmtUsage(r.usage);
|
|
1706
1708
|
/* 探测失败(fmtUsage 给空串)时保住原来的额度:这一行的 DOM 留着旧值,内存态却清空的话,
|
|
1707
1709
|
下次重画整行会凭空消失 —— 同一份状态两个消费者,漏一个就自相矛盾。 */
|
|
1708
|
-
codexStatus[name]={loggedIn:lg.loggedIn,mode:lg.mode,usageText:text||((codexStatus[name]||{}).usageText||'')};}
|
|
1710
|
+
codexStatus[name]={loggedIn:lg.loggedIn,mode:lg.mode,dead:lg.dead,verified:lg.verified,text:lg.text,usageText:text||(lg.dead?'':((codexStatus[name]||{}).usageText||''))};}
|
|
1709
1711
|
else{const r=await api('/api/usage/refresh','POST',{name});if(r.usage){usage[name]=r.usage;text=r.usage.value;}}
|
|
1710
1712
|
/* 就地改这一行,不走 renderProviders —— 整页重画会把这颗按钮连同它的 finally 一起换掉,
|
|
1711
1713
|
还会打断 hover 与试跑回执。拉不到时**留着旧值**:清空会被读成「这个模型没额度」。 */
|
|
@@ -1886,9 +1888,9 @@ function openProviderModal(name,opts){
|
|
|
1886
1888
|
})();
|
|
1887
1889
|
$('#amCancel').onclick=closeModal;
|
|
1888
1890
|
if(isCodex){$('#cxVerify').onclick=async()=>{const cb=$('#cxVerify');cb.disabled=true;const old=cb.innerHTML;cb.innerHTML='<span class="spin"></span>验证中';
|
|
1889
|
-
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,usageText};
|
|
1891
|
+
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};
|
|
1890
1892
|
if($('#cxStatus'))$('#cxStatus').innerHTML=cxStatusHtml(cxKey);
|
|
1891
|
-
toast(lg.loggedIn?('Codex 订阅 ✓'+(usageText?' · '+usageText:(r.usage===null?'(额度取用失败:app-server?)':''))):'未登录 — 机器上跑 codex login',lg.loggedIn?'ok':'err');
|
|
1893
|
+
toast(lg.loggedIn?('Codex 订阅 ✓'+(usageText?' · '+usageText:(r.usage===null?'(额度取用失败:app-server?)':''))):(lg.dead?'登录态已失效 — 机器上重新跑 codex login':'未登录 — 机器上跑 codex login'),lg.loggedIn?'ok':'err');
|
|
1892
1894
|
renderProviders();
|
|
1893
1895
|
}catch(e){toast(e.message,'err');}finally{cb.disabled=false;cb.innerHTML=old;}};}
|
|
1894
1896
|
if(edit)$('#amDel').onclick=async()=>{if(!confirm('移除模型 '+name+' ?'))return;try{S=await api('/api/providers?name='+encodeURIComponent(name),'DELETE');toast('已移除','ok');closeModal();renderProviders();}catch(e){toast(e.message,'err');}};
|
|
@@ -1914,7 +1916,9 @@ function openProviderModal(name,opts){
|
|
|
1914
1916
|
if(edit)b.hidden=$('#amHide').checked;
|
|
1915
1917
|
try{const r=await api('/api/providers/add','POST',b);S=r;
|
|
1916
1918
|
const pc=r.providerChange||{};let msg=edit?'已保存':'已添加 '+nm;
|
|
1917
|
-
|
|
1919
|
+
// 持久处理流程:服务端照实报「这次写入重置了几个群的会话」,不发群内通知,也没有「启动后生效」这回事。
|
|
1920
|
+
if(S.runtimeCapabilities)msg+=runtimeChangeNote(pc);
|
|
1921
|
+
else if(pc.affected){
|
|
1918
1922
|
if(pc.modelChanged)msg+=pc.live?` · ${pc.affected} 个群随之切换,会话已重置并已在群内通知`:` · ${pc.affected} 个群使用此模型(daemon 未运行,启动后生效)`;
|
|
1919
1923
|
else if(pc.reChanged)msg+=pc.live?` · 已通知 ${pc.affected} 个使用中的群`:'';
|
|
1920
1924
|
}
|
|
@@ -2606,8 +2610,8 @@ function cronFullHtml(text){
|
|
|
2606
2610
|
}).join('');
|
|
2607
2611
|
}
|
|
2608
2612
|
const RUNTIME_UI_TEXT={
|
|
2609
|
-
"summary": "
|
|
2610
|
-
"groups": "
|
|
2613
|
+
"summary": "当前支持多群消息、流式卡片回复、忙时跟进、停止任务、空闲群开新会话、状态查看、认证管理、绑定新群、空闲群的模型(Codex / Claude Code)和项目目录切换、模型的添加与设置,以及后台运行时的软件升级。解除绑定、移除模型、控制台插话、定时任务、生图、画布与预览暂不可用。",
|
|
2614
|
+
"groups": "可绑定新群,修改模型、项目目录、权限、数据输出和忙时跟进;该群须没有进行中的任务和未结记录。切模型或项目会开始新的一段对话。停止任务和开新会话在「概览」,开新会话须等该群空闲。群里发 /model 也能切换模型。",
|
|
2611
2615
|
"skills": "当前支持飞书文字消息、忙时跟进、任务回复和停止。定时任务、生图、群消息管理、画布与预览暂未接入。",
|
|
2612
2616
|
"cron": "当前不执行定时任务;已有定义仅供查看。",
|
|
2613
2617
|
"board": "画布创建、同步与反馈暂未接入;已有链接仅供查看。",
|
|
@@ -2617,7 +2621,8 @@ const RUNTIME_UI_TEXT={
|
|
|
2617
2621
|
"resumeLabel": "恢复",
|
|
2618
2622
|
"steer": "群成员的后续文字消息可加入当前任务;只有确认消息已进入模型后才算送达。控制台插话和定时任务暂不可用。",
|
|
2619
2623
|
"preview": "当前版本未接入预览服务。",
|
|
2620
|
-
"providers": "
|
|
2624
|
+
"providers": "可添加模型,修改型号、端点、密钥、思考深度与推理强度,测试、隐藏或拖动排序;各群的模型在「群·项目」里切换。改型号、后端或端点时,用它的群须空闲,并会开始新的一段对话;默认模型和群正在用的模型不能隐藏。移除模型暂不可用。",
|
|
2625
|
+
"providersView": "当前可查看模型配置与登录状态;各群的模型在「群·项目」里切换。添加或修改模型暂不可用。",
|
|
2621
2626
|
"unavailable": "当前版本暂不支持此操作。"
|
|
2622
2627
|
};
|
|
2623
2628
|
function renderGroups(){
|
|
@@ -4271,8 +4276,8 @@ updateThemeBtn();
|
|
|
4271
4276
|
const I18N_EN=[
|
|
4272
4277
|
["需要处理", "Needs attention"],
|
|
4273
4278
|
["当前可用操作", "Available controls"],
|
|
4274
|
-
["
|
|
4275
|
-
["
|
|
4279
|
+
["当前支持多群消息、流式卡片回复、忙时跟进、停止任务、空闲群开新会话、状态查看、认证管理、绑定新群、空闲群的模型(Codex / Claude Code)和项目目录切换、模型的添加与设置,以及后台运行时的软件升级。解除绑定、移除模型、控制台插话、定时任务、生图、画布与预览暂不可用。", "Available: group messages, streaming reply cards, busy-turn followups, stopping tasks, new sessions for idle groups, live status, authentication, binding new groups, switching the model (Codex / Claude Code) or project of an idle group, adding and configuring models, and software upgrades in a background run. Unbinding, removing models, console messages, schedules, image generation, canvas and previews are unavailable."],
|
|
4280
|
+
["可绑定新群,修改模型、项目目录、权限、数据输出和忙时跟进;该群须没有进行中的任务和未结记录。切模型或项目会开始新的一段对话。停止任务和开新会话在「概览」,开新会话须等该群空闲。群里发 /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."],
|
|
4276
4281
|
["当前支持飞书文字消息、忙时跟进、任务回复和停止。定时任务、生图、群消息管理、画布与预览暂未接入。", "Feishu text messages, busy-turn followups, task replies and stopping are supported. Schedules, image generation, group message management, canvas and previews are unavailable."],
|
|
4277
4282
|
["当前不执行定时任务;已有定义仅供查看。", "Schedules are not executed. Existing definitions are shown for reference only."],
|
|
4278
4283
|
["画布创建、同步与反馈暂未接入;已有链接仅供查看。", "Canvas creation, sync and feedback are unavailable. Existing links are view-only."],
|
|
@@ -4283,6 +4288,10 @@ const I18N_EN=[
|
|
|
4283
4288
|
["群成员的后续文字消息可加入当前任务;只有确认消息已进入模型后才算送达。控制台插话和定时任务暂不可用。", "Followup text messages from group members can join the current task. Delivery is confirmed only when the model receives the message. Console messages and schedules are unavailable."],
|
|
4284
4289
|
["当前版本未接入预览服务。", "Preview services are unavailable in this version."],
|
|
4285
4290
|
["当前可查看模型配置与登录状态;各群的模型在「群·项目」里切换。添加或修改模型暂不可用。", "Model configuration and login status are shown here. Switch each group's model under Groups. Adding or changing models is unavailable."],
|
|
4291
|
+
["可添加模型,修改型号、端点、密钥、思考深度与推理强度,测试、隐藏或拖动排序;各群的模型在「群·项目」里切换。改型号、后端或端点时,用它的群须空闲,并会开始新的一段对话;默认模型和群正在用的模型不能隐藏。移除模型暂不可用。", "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."],
|
|
4292
|
+
["当前只能添加或修改 Codex 或 Claude Code 模型", "Only Codex or Claude Code models can be added or changed right now."],
|
|
4293
|
+
["默认模型须是列表中未隐藏的 Codex 或 Claude Code 模型", "The default model must be a visible Codex or Claude Code model from the list."],
|
|
4294
|
+
["有群因未解决的故障暂停处理,它用的模型、后端或端点要等处理完并重启后才能改", "A group is held after an unresolved fault; the model, backend or endpoint it uses can change only after the fault is handled and the service restarts."],
|
|
4286
4295
|
["当前版本暂不支持此操作。", "This operation is unavailable in the current version."],
|
|
4287
4296
|
|
|
4288
4297
|
["控制台","Console"],
|
|
@@ -4346,6 +4355,10 @@ const I18N_EN=[
|
|
|
4346
4355
|
["向这个群插一句 —— 像真人消息那样驱动一轮","Interject in this group — drives a turn like a real message"],
|
|
4347
4356
|
["停止当前任务(保留会话)","Stop the current task (keeps the session)"],
|
|
4348
4357
|
["开新会话 — 运行中会先中断当前任务并清空队列(同群内 /new)","New session — if running, interrupts the current task and clears the queue first (same as /new in the group)"],
|
|
4358
|
+
["开新会话 — 仅在该群空闲、记录全部结账后可用;运行中请先停止,结束后再开(同群内 /new)","New session — only when this group is idle and every record is settled; if it is running, stop it first and start the new session once it ends (same as /new in the group)"],
|
|
4359
|
+
["该群有任务进行中或记录未结账,先停止或等它结束再开新会话","This group has a task running or an unsettled record. Stop it or wait for it to finish, then start a new session."],
|
|
4360
|
+
["此群未运行,未开新会话","This group is not running; no new session was started."],
|
|
4361
|
+
["此群当前未开放,未开新会话","This group is not open right now; no new session was started."],
|
|
4349
4362
|
["还没有群项目 · 去「群·项目」配置","No group projects yet · set one up on the Groups · Projects page"],
|
|
4350
4363
|
["健康","Health"],
|
|
4351
4364
|
["并发","Concurrency"],
|
|
@@ -4398,6 +4411,10 @@ const I18N_EN=[
|
|
|
4398
4411
|
["重新拉取额度","Refresh quota"],
|
|
4399
4412
|
["点卡片右上角「配置」里的「检测登录」确认这台机器的 Claude 登录态","Use \"Check sign-in\" under \"Configure\" (top right of the card) to confirm this machine's Claude sign-in state"],
|
|
4400
4413
|
["订阅登录·未验证","Subscription · unverified"],
|
|
4414
|
+
["已验证","Verified"],
|
|
4415
|
+
["登录态已失效","Sign-in expired"],
|
|
4416
|
+
["登录态已失效 — 机器上重新跑 codex login","Sign-in expired — run codex login again on this machine"],
|
|
4417
|
+
["未验证(本机有登录文件,服务端暂时没探到)","Unverified (this machine has a sign-in file; the server couldn't be reached just now)"],
|
|
4401
4418
|
["这台机器还没登录 Claude —— 跑 claude auth login","This machine is not signed in to Claude — run claude auth login"],
|
|
4402
4419
|
["订阅登录","Subscription sign-in"],
|
|
4403
4420
|
["点下方「验证登录」确认 codex 订阅登录态 + 拉取额度","Use \"Verify sign-in\" below to confirm the codex subscription sign-in and fetch the quota"],
|
|
@@ -5506,6 +5523,12 @@ const I18N_EN_RX=[
|
|
|
5506
5523
|
[/^闲置 ([\d.]+) 分钟自动停$/,"Auto-stops after $1 min idle"],
|
|
5507
5524
|
[/^最长 ([\d.]+) 小时自动停$/,"Auto-stops after $1 h max"],
|
|
5508
5525
|
[/^已登录 · (.+)$/,"Signed in · $1"],
|
|
5526
|
+
// codex 登录态三态(src/agent/codexstatus.js describeLogin 的全部句式;尾巴「 · 套餐 · 邮箱」「 — 原因」原样带过去)。
|
|
5527
|
+
// ⚠ 必须排在下面那条通用「未验证 — …」之前,否则带具体原因的句子只会被译掉开头两个字
|
|
5528
|
+
[/^订阅登录已验证( · [\s\S]+)?$/,"Subscription sign-in verified$1"],
|
|
5529
|
+
[/^登录态已失效\(服务端拒绝了这台机器的令牌\)— 在这台机器上重新跑 codex login( · [\s\S]+)?$/,"Sign-in expired (the server rejected this machine's token) — run codex login again on this machine$1"],
|
|
5530
|
+
[/^未验证 — 本机没有登录文件\(可能未登录\)( — [\s\S]*)?$/,"Unverified — no sign-in file on this machine (possibly not signed in)$1"],
|
|
5531
|
+
[/^未验证且令牌已过期\((\d{4}-\d{2}-\d{2})\),很可能要重新登录( — [\s\S]*)?$/,"Unverified and the token expired ($1) — you most likely need to sign in again$2"],
|
|
5509
5532
|
[/^未验证 — ([\s\S]*)$/,"Unverified — $1"],
|
|
5510
5533
|
[/^codex login status: (.+) · 已验证$/,"codex login status: $1 · verified"],
|
|
5511
5534
|
[/^窗口:(.*)$/,"Window:$1"],
|
|
@@ -5528,6 +5551,16 @@ const I18N_EN_RX=[
|
|
|
5528
5551
|
[/^已添加 (\S+) · (\d+) 个群随之切换,会话已重置并已在群内通知$/,"Added $1 · $2 group(s) switched with it; sessions reset and the groups notified"],
|
|
5529
5552
|
[/^已添加 (\S+) · (\d+) 个群使用此模型\(daemon 未运行,启动后生效\)$/,"Added $1 · $2 group(s) use this model (daemon not running; takes effect on start)"],
|
|
5530
5553
|
[/^已添加 (\S+) · 已通知 (\d+) 个使用中的群$/,"Added $1 · $2 active group(s) notified"],
|
|
5554
|
+
[/^已保存 · (\d+) 个群已开始新的一段对话$/,"Saved · $1 group(s) started a new conversation"],
|
|
5555
|
+
[/^已添加 (\S+) · (\d+) 个群已开始新的一段对话$/,"Added $1 · $2 group(s) started a new conversation"],
|
|
5556
|
+
[/^模型「(.+)」的后端不受支持;只能添加或修改 Codex 或 Claude Code 模型$/,"The backend of model “$1” is not supported; only Codex or Claude Code models can be added or changed."],
|
|
5557
|
+
[/^「(.+)」是默认模型,先把默认模型换成别的再隐藏$/,"“$1” is the default model. Choose another default model before hiding it."],
|
|
5558
|
+
[/^「(.+)」是默认模型,先把默认模型换成别的再移除$/,"“$1” is the default model. Choose another default model before removing it."],
|
|
5559
|
+
[/^「(.+)」正被 (\d+) 个群使用,先把这些群切到其它模型再隐藏$/,"“$1” is used by $2 group(s). Switch them to another model before hiding it."],
|
|
5560
|
+
[/^「(.+)」正被 (\d+) 个群使用,先把这些群切到其它模型再移除$/,"“$1” is used by $2 group(s). Switch them to another model before removing it."],
|
|
5561
|
+
[/^「(.+)」有任务进行中或记录未结账,等它结束后再改它用的模型、后端或端点$/,"“$1” has a task running or an unsettled record. Wait for it to finish before changing the model, backend or endpoint it uses."],
|
|
5562
|
+
[/^默认模型已改为 (\S+)$/,"Default model changed to $1"],
|
|
5563
|
+
[/^(\d+) 个群已开始新的一段对话$/,"$1 group(s) started a new conversation"],
|
|
5531
5564
|
[/^已添加 (\S+)$/,"Added $1"],
|
|
5532
5565
|
[/^叫法:(.+)$/,"Naming: $1"],
|
|
5533
5566
|
[/^(\d+) 个图标 · 语义名 (\d+) 条 · 兜底 (.+)$/,"$1 icons · $2 semantic names · fallback $3"],
|
|
@@ -5873,7 +5906,17 @@ async function fetchCodexModels(){if(CX_MODELS)return CX_MODELS;try{CX_MODELS=aw
|
|
|
5873
5906
|
function cxDefEffortLabel(model){const m=CX_MODELS&&(CX_MODELS.models||[]).find(x=>x.model===model);return '默认'+(m&&m.defEffort?'('+m.defEffort+')':'');}
|
|
5874
5907
|
/* 有 codex provider 时启动即预热模型列表(server 缓存 10min)→ 卡片默认档标注补齐 + 面板秒开 */
|
|
5875
5908
|
function warmCodexModels(){if(!CX_MODELS&&S&&(S.providers||[]).some(x=>x.runner==='codex'&&!x.hidden))fetchCodexModels().then(()=>renderProviders());}
|
|
5876
|
-
const RUNTIME_DISABLED_SELECTORS='[data-osay]
|
|
5909
|
+
const RUNTIME_DISABLED_SELECTORS='[data-osay],.gpv,[data-rm],[data-gcfg],[data-cronnew],[data-cronadd],[data-crontgl],[data-cronrm],[data-reposave],[data-repotest],[data-iconuse],[data-iconsys],[data-iconstyle],[data-stylecollect],#mNewChat,#amDel,#mNewGo,#pgSend,#pgNew,#fSave,#slSave,#wSave,#bSave,#styleNew,[data-styleedit],[data-styledup],[data-styledel],#nsSave,#nsExtract,#clSave,[data-pvstop],[data-lvkill],[data-lvkillall]';
|
|
5910
|
+
// 持久处理流程按服务端白名单放行:前端照同一份名单判断某个动作能不能做,而不是一刀切。
|
|
5911
|
+
function runtimeAllows(method,path){
|
|
5912
|
+
const caps=S&&S.runtimeCapabilities;if(!caps)return true;
|
|
5913
|
+
return !!(caps.allowed&&(caps.allowed[method]||[]).includes(path));
|
|
5914
|
+
}
|
|
5915
|
+
// 持久处理流程下模型库写入的回执:默认模型被自动补上、有群开始了新的一段对话,都照实说出来。
|
|
5916
|
+
function runtimeChangeNote(pc){
|
|
5917
|
+
if(!pc)return '';
|
|
5918
|
+
return (pc.defaultProvider?' · 默认模型已改为 '+pc.defaultProvider:'')+(pc.resetGroups?' · '+pc.resetGroups+' 个群已开始新的一段对话':'');
|
|
5919
|
+
}
|
|
5877
5920
|
function disableRuntimeControl(el,reason){
|
|
5878
5921
|
el.disabled=true;el.title=trText(reason);el.dataset.runtimeDisabled='1';
|
|
5879
5922
|
el.setAttribute('aria-disabled','true');
|
|
@@ -5889,7 +5932,7 @@ function applyRuntimeCapabilities(){
|
|
|
5889
5932
|
if(!S||!S.runtimeCapabilities)return;
|
|
5890
5933
|
if(banner){const body=banner.querySelector('[data-runtime-summary]'),text=trText(RUNTIME_UI_TEXT.summary);if(body&&body.textContent!==text)body.textContent=text;}
|
|
5891
5934
|
document.querySelectorAll(RUNTIME_DISABLED_SELECTORS).forEach(el=>disableRuntimeControl(el,RUNTIME_UI_TEXT.unavailable));
|
|
5892
|
-
document.querySelectorAll('#pgrid .drag').forEach(el=>{el.draggable=false;el.title=trText(RUNTIME_UI_TEXT.unavailable);});
|
|
5935
|
+
if(!runtimeAllows('POST','/api/providers/order'))document.querySelectorAll('#pgrid .drag').forEach(el=>{el.draggable=false;el.title=trText(RUNTIME_UI_TEXT.unavailable);});
|
|
5893
5936
|
}
|
|
5894
5937
|
document.addEventListener('click',event=>{
|
|
5895
5938
|
if(!S||!S.runtimeCapabilities)return;
|
package/src/ui/serverbase.js
CHANGED
|
@@ -303,6 +303,25 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
303
303
|
if (runtimePolicy) runtimePolicy.applyConfig(obj)
|
|
304
304
|
else if (onConfigChange) { try { onConfigChange() } catch (e) { log('reload hook error: ' + (e && e.message)) } }
|
|
305
305
|
}
|
|
306
|
+
// 默认模型的兜底。旧守护进程:默认模型隐藏了 / 在本进程看来没配好(如 key 是读不到的 env:X)/
|
|
307
|
+
// 文件里没写(跟随内置的 claude),都改成第一个可用的。持久处理流程里这会把跟随默认模型的群
|
|
308
|
+
// 悄悄换到另一个后端并重置会话(拖一下排序、加一个不相干的模型都会触发)⇒ 那里**只在默认模型
|
|
309
|
+
// 已不存在**(文件和内置都没有这个名字)时补一个,其余一律不动;隐藏默认模型由写入方直接拒绝。
|
|
310
|
+
// 返回 { from, to } = 这次自动改了默认模型(调用方据此如实报告),null = 没改。
|
|
311
|
+
const settleDefault = (raw) => {
|
|
312
|
+
if (!runtimePolicy) { fixDefault(raw); return null }
|
|
313
|
+
const current = raw.defaultProvider || DEFAULTS.defaultProvider
|
|
314
|
+
if ({ ...DEFAULTS.providers, ...(raw.providers || {}) }[current]) return null
|
|
315
|
+
const next = pickDefault(raw.providers || {})
|
|
316
|
+
if (!next || next === raw.defaultProvider) return null
|
|
317
|
+
const from = raw.defaultProvider || null
|
|
318
|
+
raw.defaultProvider = next
|
|
319
|
+
return { from, to: next }
|
|
320
|
+
}
|
|
321
|
+
// 持久处理流程下模型库写入的如实回执:这次写入重置了几个群的会话、默认模型有没有被自动补上。
|
|
322
|
+
const runtimeLibraryChange = (defaultChange) => runtimePolicy ? {
|
|
323
|
+
resetGroups: runtimePolicy.appliedResets ? runtimePolicy.appliedResets().length : 0,
|
|
324
|
+
...(defaultChange ? { defaultProvider: defaultChange.to } : {}) } : null
|
|
306
325
|
// 群内通知统一出口:暂停群不打扰(暂停语义=完全静默,配置变更通知也不例外 —— 2026-07-22
|
|
307
326
|
// 真机踩过:暂停群仍收到「模型已更新」;operator 在控制台自己改的,群成员恢复后清单会重注入,
|
|
308
327
|
// 静默无损)。唯 force:true 豁免 —— 暂停/恢复动作本身的那条告知必须送达。
|
|
@@ -505,15 +524,19 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
505
524
|
const hasCodex = providers.some((p) => !p.hidden && p.runner === 'codex')
|
|
506
525
|
const cv = hasCodex ? await probeCodexCached() : null
|
|
507
526
|
// 「可用模型」= 能用的,不只是配了的(1.0.3;此前没登录的 Claude 订阅、试跑失败的 key 都算在内)。
|
|
508
|
-
// key 型看最近一次验证:失败的不算,还没验证的先算上;Claude
|
|
509
|
-
//
|
|
527
|
+
// key 型看最近一次验证:失败的不算,还没验证的先算上;Claude 订阅看本机登录态,探不出来(没装 / 超时)不扣。
|
|
528
|
+
// codex 用服务端判据(与 doctor、卡片「验证登录」同一个探针,按 bin 共用一份缓存):`codex login status`
|
|
529
|
+
// 只读本地文件,令牌被吊销后照样「已登录」。只有服务端明确拒绝(dead)才扣;探不到(没装 / 超时 / 连不上)不扣。
|
|
530
|
+
// ⚠ 概览**绝不等**这个探针(起进程出网,冷缓存 2s、连不上 12s):peekAuthProbe 只读 10 分钟内的缓存,
|
|
531
|
+
// 没有就按「未验证」算(不扣)并在后台刷新一次,下一次重建就是真结果。claude 那条是 60s 缓存的本地调用,照旧等。
|
|
510
532
|
const live = providers.filter((p) => !p.hidden && p.configured)
|
|
511
533
|
const subLogin = live.some((p) => !p.baseUrl && p.runner !== 'codex') ? await claudestatus.cachedLoginStatus(cfg.agent.bin).catch(() => null) : null
|
|
512
|
-
const cxLogin = live.some((p) => p.runner === 'codex') ?
|
|
534
|
+
const cxLogin = live.some((p) => p.runner === 'codex') ? codexstatus.peekAuthProbe('codex', 10 * 60_000) : null
|
|
513
535
|
const loggedOut = (st) => !!(st && st.ok && st.loggedIn === false)
|
|
514
536
|
const unusable = (p) => {
|
|
515
537
|
if (p.baseUrl) { const v = verify.lookup(verify.providerFp(cfg.providers[p.name] || {}), vopts); return !!(v && !v.ok) }
|
|
516
|
-
|
|
538
|
+
if (p.runner === 'codex') return !!(cxLogin && cxLogin.dead)
|
|
539
|
+
return loggedOut(subLogin)
|
|
517
540
|
}
|
|
518
541
|
const badProv = live.filter(unusable).map((p) => p.name)
|
|
519
542
|
const usableProv = live.length - badProv.length
|
|
@@ -525,7 +548,7 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
525
548
|
...(v ? [{ status: v.ok ? 'ok' : 'fail', label: 'Claude CLI', detail: v.ok ? v.version : v.error }] : []),
|
|
526
549
|
...(cv ? [{ status: cv.ok ? 'ok' : 'fail', label: 'Codex CLI', detail: cv.ok ? `${cv.version} · GPT 订阅` : cv.error }] : []),
|
|
527
550
|
{ status: !usableProv ? 'fail' : badProv.length ? 'warn' : 'ok', label: '可用模型',
|
|
528
|
-
detail: (usableProv ? `${usableProv} 个能用${
|
|
551
|
+
detail: (usableProv ? `${usableProv} 个能用${live.some((p) => p.runner === 'codex' && !badProv.includes(p.name)) ? '(含 Codex)' : ''}` : '无可用模型 — 去「模型 · 库」页添加 / 测试')
|
|
529
552
|
+ (badProv.length ? `;${badProv.join('、')} 没登录或验证失败` : '') },
|
|
530
553
|
{ status: usableGroups ? 'ok' : 'warn', label: '可用群 / 项目', detail: usableGroups ? `${usableGroups} 个已绑定` : '无绑定群/项目 — 去「群·项目」页创建' },
|
|
531
554
|
...connRows,
|
|
@@ -915,6 +938,10 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
915
938
|
if (p === '/api/providers/add' && req.method === 'POST') {
|
|
916
939
|
const { name, baseUrl, model, apiKey, env, toolQuality, effort, reasoningEffort, hidden, vision, modelClear, runner } = await body(req)
|
|
917
940
|
if (!name || !/^[\w.-]+$/.test(name)) return sendJSON(res, 400, { error: '名称必填,且只用字母/数字/._-' })
|
|
941
|
+
// 持久处理流程只跑 Codex / Claude Code:别的后端名不能被下面的白名单悄悄改写成 Claude 存进去。
|
|
942
|
+
if (runtimePolicy && runner !== undefined && runner !== '' && !runtimePolicy.providerSupported({ runner })) {
|
|
943
|
+
return sendJSON(res, 400, { error: '当前只能添加或修改 Codex 或 Claude Code 模型' })
|
|
944
|
+
}
|
|
918
945
|
const raw = readRaw(); raw.providers = raw.providers || {}
|
|
919
946
|
// 编辑前快照:model / 推理强度变了要联动受影响群(通知 + model 变更重置会话)
|
|
920
947
|
const prev = raw.providers[name] ? { model: raw.providers[name].model || '', re: raw.providers[name].reasoningEffort || '' } : null
|
|
@@ -943,7 +970,7 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
943
970
|
// 不传 ⇒ 保持原值(编辑已有 codex provider 的常态,别把它降级成 claude 后端)。
|
|
944
971
|
if (runner !== undefined) { if (runner === 'codex') pr.runner = 'codex'; else delete pr.runner }
|
|
945
972
|
raw.providers[name] = pr
|
|
946
|
-
|
|
973
|
+
const defaultChange = settleDefault(raw); writeRaw(raw)
|
|
947
974
|
// 变更联动(仅编辑已有 provider 时):受影响群 = 用着它(显式或经 defaultProvider)
|
|
948
975
|
// 且无 per-group model 覆盖的群。model 变 = 换脑 → 按「切模型即重置」的既有拍板,
|
|
949
976
|
// 重置这些群会话 + 群内一行通知;仅推理强度变 → 通知不重置。daemon 未运行(无
|
|
@@ -967,7 +994,9 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
967
994
|
} catch (e) { log('providerChange notify: ' + (e && e.message)) }
|
|
968
995
|
}
|
|
969
996
|
}
|
|
970
|
-
|
|
997
|
+
// 持久处理流程:会话重置由配置写入方按「谁实际在用、型号/后端/端点是否变了」决定并已完成,
|
|
998
|
+
// 这里如实报它重置了几个群、默认模型有没有被补上(上面的 affected 是旧守护进程的通知口径)。
|
|
999
|
+
return sendJSON(res, 200, { ...(await buildState()), providerChange: { modelChanged: !!modelChanged, reChanged: !!reChanged, affected: affected.length, live, ...(runtimeLibraryChange(defaultChange) || {}) } })
|
|
971
1000
|
}
|
|
972
1001
|
if (p === '/api/providers' && req.method === 'DELETE') {
|
|
973
1002
|
const name = url.searchParams.get('name'); const raw = readRaw()
|
|
@@ -980,13 +1009,17 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
980
1009
|
return sendJSON(res, 400, { error: `「${name}」是内置默认模型,移不掉 —— 不想让它出现在列表里就用「隐藏」` })
|
|
981
1010
|
}
|
|
982
1011
|
if (raw.providers) delete raw.providers[name]
|
|
983
|
-
|
|
1012
|
+
const defaultChange = settleDefault(raw); writeRaw(raw)
|
|
1013
|
+
const change = runtimeLibraryChange(defaultChange)
|
|
1014
|
+
return sendJSON(res, 200, { ...(await buildState()), ...(change ? { providerChange: change } : {}) })
|
|
984
1015
|
}
|
|
985
1016
|
if (p === '/api/providers/hidden' && req.method === 'POST') {
|
|
986
1017
|
const { name, hidden } = await body(req); const raw = readRaw()
|
|
987
1018
|
if (!raw.providers || !raw.providers[name]) return sendJSON(res, 404, { error: 'no such provider' })
|
|
988
1019
|
if (hidden) raw.providers[name].hidden = true; else delete raw.providers[name].hidden
|
|
989
|
-
|
|
1020
|
+
const defaultChange = settleDefault(raw); writeRaw(raw)
|
|
1021
|
+
const change = runtimeLibraryChange(defaultChange)
|
|
1022
|
+
return sendJSON(res, 200, { ...(await buildState()), ...(change ? { providerChange: change } : {}) })
|
|
990
1023
|
}
|
|
991
1024
|
if (p === '/api/providers/order' && req.method === 'POST') {
|
|
992
1025
|
const { order } = await body(req); const raw = readRaw()
|
|
@@ -995,8 +1028,12 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
995
1028
|
for (const n of order) if (raw.providers[n]) next[n] = raw.providers[n]
|
|
996
1029
|
for (const [n, v] of Object.entries(raw.providers)) if (!(n in next)) next[n] = v // keep any not listed
|
|
997
1030
|
raw.providers = next
|
|
998
|
-
|
|
1031
|
+
// ordering doesn't set default; just keep the invisible fallback valid
|
|
1032
|
+
// (durable mode: only when the default no longer exists — see settleDefault)
|
|
1033
|
+
const defaultChange = settleDefault(raw)
|
|
999
1034
|
writeRaw(raw)
|
|
1035
|
+
const change = runtimeLibraryChange(defaultChange)
|
|
1036
|
+
if (change) return sendJSON(res, 200, { ...(await buildState()), providerChange: change })
|
|
1000
1037
|
}
|
|
1001
1038
|
return sendJSON(res, 200, await buildState())
|
|
1002
1039
|
}
|
|
@@ -2250,19 +2287,22 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
2250
2287
|
}
|
|
2251
2288
|
return sendJSON(res, 200, { ...claudemodels.listModels({ loggedIn: !!(login && login.ok && login.loggedIn) }), login })
|
|
2252
2289
|
}
|
|
2253
|
-
// codex
|
|
2254
|
-
//
|
|
2290
|
+
// codex 卡片「验证登录」:带令牌问一次服务端(app-server account/rateLimits/read),登录态 + 订阅用量
|
|
2291
|
+
// 一次拿到。无需 API key;探针缓存 60s(app-server 有 spawn 开销)。
|
|
2255
2292
|
if (p === '/api/codex/verify' && req.method === 'POST') {
|
|
2256
2293
|
// force = 卡片额度行的 ↻:点按钮就是要此刻的数,绕过下面的 60s 去抖
|
|
2257
2294
|
// (去抖是给开局自动探测用的 —— app-server spawn 不便宜,但手点一次的开销该认)。
|
|
2258
2295
|
const { force } = await body(req).catch(() => ({}))
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2296
|
+
// 登录态与额度合成**一次**带令牌的服务端询问(额度接口)。此前先 `codex login status` 再取额度 ——
|
|
2297
|
+
// 前者只读本地文件,令牌被吊销后照样「已登录」,卡片一直绿着。现在服务端明确拒绝 ⇒ dead:true、loggedIn:false;
|
|
2298
|
+
// 探不到(超时)⇒ verified:false,保留上一份额度、文案「未验证」,不当成未登录。
|
|
2299
|
+
if (force) codexstatus.clearCache()
|
|
2300
|
+
const probe = await codexstatus.cachedAuthProbe('codex')
|
|
2301
|
+
const file = codexstatus.readAuthFile()
|
|
2302
|
+
const d = codexstatus.describeLogin(probe, file)
|
|
2303
|
+
const login = { ok: true, verified: !!probe.verified, loggedIn: !!(probe.ok || (!probe.verified && file.exists && !file.expired)), dead: !!probe.dead, mode: file.mode || 'chatgpt', level: d.level, text: d.text }
|
|
2304
|
+
let usage = probe.usage || null
|
|
2305
|
+
if (!usage && !probe.dead && codexStatusCache && codexStatusCache.usage) usage = codexStatusCache.usage // 探不到时保住旧额度(同前端那条「同一份状态两个消费者」的教训)
|
|
2266
2306
|
codexStatusCache = { at: Date.now(), login, usage } // cached into /api/state so the card survives a page reload
|
|
2267
2307
|
return sendJSON(res, 200, { login, usage })
|
|
2268
2308
|
}
|