sintbot 1.4.0 → 1.4.2
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 +38 -0
- package/CHANGELOG.json +38 -0
- package/bin/sintbot-v2.js +94 -16
- package/docs/AI-ASSISTED-INSTALL.md +2 -2
- package/docs/install.md +13 -5
- package/docs/zh-CN/AI-ASSISTED-INSTALL.md +2 -2
- package/docs/zh-CN/install.md +13 -5
- package/install.sh +1 -1
- package/package.json +5 -1
- package/src/agent/claudecode.js +7 -2
- package/src/agent/codexapp.js +7 -2
- package/src/agent/codexcode.js +8 -3
- package/src/c2configcontrol.js +13 -3
- package/src/c2console.js +15 -7
- package/src/c2incident.js +125 -0
- package/src/c2install.js +1 -1
- package/src/c2multigroupservice.js +157 -26
- package/src/c2updater.js +361 -0
- package/src/c2upgradeswitch.js +169 -0
- package/src/core/c2sourcequarantine.js +22 -3
- package/src/core/v2engine.js +23 -1
- package/src/ui/console.html +41 -18
- package/src/updater.js +243 -0
package/src/ui/console.html
CHANGED
|
@@ -392,6 +392,7 @@ code{font-family:var(--mono);font-size:.95em} /* 中文说明里内嵌的命令/
|
|
|
392
392
|
}
|
|
393
393
|
.tag{font-family:var(--mono);font-size:10.5px;letter-spacing:.5px;padding:3px 8px;border-radius:5px;border:1px solid var(--tag-line,var(--line-hi));color:var(--tag-ink,var(--dim));background:var(--tag-bg,transparent);white-space:nowrap}
|
|
394
394
|
.tag.busy{color:var(--ember);border-color:rgba(var(--ember-rgb),.4);box-shadow:0 0 10px rgba(var(--ember-rgb),.12) inset}
|
|
395
|
+
.tag.held{color:var(--fail);border-color:rgba(var(--fail-rgb),.42);background:rgba(var(--fail-rgb),.08)}
|
|
395
396
|
.tag.idle{color:var(--ok);border-color:rgba(var(--ok-rgb),.28)}
|
|
396
397
|
.tag.pz{color:var(--muted);border-color:var(--line-hi);background:transparent} /* 暂停态:中性灰,不是错误(bad 红留给真异常) */
|
|
397
398
|
.card.gpaused{opacity:.55;transition:opacity .15s} /* 暂停群卡整体降透明度示意下线;hover 提回可读 —— 配置项不禁用,暂停期间照样可调 */
|
|
@@ -1353,9 +1354,9 @@ function updateOverviewLive(){
|
|
|
1353
1354
|
const a=$('#ovActive'); if(a)a.textContent=active;
|
|
1354
1355
|
const q=$('#ovQueue'); if(q)q.textContent=live.queue||0;
|
|
1355
1356
|
$$('#overview [data-g]').forEach(el=>{const lv=(live.groups&&live.groups[el.dataset.g])||{};
|
|
1356
|
-
el.className='tag glive '+(lv.busy?'busy':'idle');
|
|
1357
|
-
el.textContent=lv.queueFaulted?'待处理异常':lv.waitingForCapacity?'等待容量':lv.finalizing?'收尾中':(lv.busy?('busy '+(lv.sinceMs?Math.round(lv.sinceMs/1000)+'s':'')):'idle');
|
|
1358
|
-
el.title=(S.runtimeCapabilities?(lv.queueFaulted?'服务当前未开放此群':lv.waitingForCapacity?'消息已登记,等待并发名额':lv.finalizing?'正在确认发送与结账结果':lv.busy?'运行中':'空闲'):(lv.finalizing?'答案已发送 · 进程收尾中(新消息会排队)':(lv.busy?'运行中':'空闲')))+(lv.sessionId?(' · 会话 #'+String(lv.sessionId).slice(-6)):' · 无会话');});
|
|
1357
|
+
el.className='tag glive '+(lv.held?'held':lv.busy?'busy':'idle');
|
|
1358
|
+
el.textContent=lv.held?'需要处理':lv.queueFaulted?'待处理异常':lv.waitingForCapacity?'等待容量':lv.finalizing?'收尾中':(lv.busy?('busy '+(lv.sinceMs?Math.round(lv.sinceMs/1000)+'s':'')):'idle');
|
|
1359
|
+
el.title=lv.held?('上一个任务结果不明,此群已暂停,其他群照常。原因:'+(lv.heldReason||'未知')+'。处理:先重启服务,能自动收尾的会恢复;重启后仍需处理,再停止服务,运行 sintbot incident seal --group '+el.dataset.g+',然后启动。'):(S.runtimeCapabilities?(lv.queueFaulted?'服务当前未开放此群':lv.waitingForCapacity?'消息已登记,等待并发名额':lv.finalizing?'正在确认发送与结账结果':lv.busy?'运行中':'空闲'):(lv.finalizing?'答案已发送 · 进程收尾中(新消息会排队)':(lv.busy?'运行中':'空闲')))+(lv.sessionId?(' · 会话 #'+String(lv.sessionId).slice(-6)):' · 无会话');});
|
|
1359
1360
|
// Self-heal stuck 'running' rows: a done SSE frame can be lost on a reconnect gap and
|
|
1360
1361
|
// SSE has no replay, so the row would spin forever. Per-group turns are serialized, so a
|
|
1361
1362
|
// 'running' row whose group is no longer busy has finished (busy=false ⇔ the turn settled
|
|
@@ -2579,7 +2580,7 @@ function cronFullHtml(text){
|
|
|
2579
2580
|
}).join('');
|
|
2580
2581
|
}
|
|
2581
2582
|
const RUNTIME_UI_TEXT={
|
|
2582
|
-
"summary": "
|
|
2583
|
+
"summary": "当前支持多群消息、流式卡片回复、忙时跟进、停止任务、状态查看、认证管理、绑定新群、空闲群的模型(Codex / Claude Code)和项目目录切换,以及后台运行时的软件升级。新会话、解除绑定、控制台插话、定时任务、生图、画布与预览暂不可用。",
|
|
2583
2584
|
"groups": "可绑定新群,修改模型、项目目录、权限、数据输出和忙时跟进;该群须没有进行中的任务和未结记录。切模型或项目会开始新的一段对话。停止任务在「概览」。群里发 /model 也能切换模型。",
|
|
2584
2585
|
"skills": "当前支持飞书文字消息、忙时跟进、任务回复和停止。定时任务、生图、群消息管理、画布与预览暂未接入。",
|
|
2585
2586
|
"cron": "当前不执行定时任务;已有定义仅供查看。",
|
|
@@ -3484,21 +3485,27 @@ function renderSettings(){
|
|
|
3484
3485
|
<div class="hint">代理第一次填,新的对话轮次就生效;改已有的地址要重启后台服务(sintbot service restart)。</div></div>
|
|
3485
3486
|
</div>`;
|
|
3486
3487
|
const updateJob=upd&&upd.job;
|
|
3487
|
-
|
|
3488
|
-
const
|
|
3488
|
+
// 1.4.x 多群服务的升级器带 mode:后台托管才能一键升级,前台与开发版只给说明;不在群里播报。
|
|
3489
|
+
const updMode=upd&&upd.mode||'';
|
|
3490
|
+
const updateBusy=!!(updateJob&&['queued','installing','switching','restarting','restart-needed'].includes(updateJob.state));
|
|
3491
|
+
const updCanInstall=!!(upd&&(updMode?upd.upgradeSupported:true));
|
|
3492
|
+
const updateText=!upd?'后台版本检查不可用':updateJob&&updateJob.state!=='done'||updateJob&&!upd.available&&updMode?esc(updateJob.message||updateJob.state):upd.available?`检测到新版本 v${esc(upd.latestVersion)} · 当前 v${esc(upd.currentVersion)}`:updMode==='dev'&&upd.latestVersion?`最新正式版 v${esc(upd.latestVersion)} · 当前开发版 v${esc(upd.currentVersion)}`:upd.checkedAt&&upd.latestVersion?`当前为最新版本 v${esc(upd.currentVersion)}`:upd.checkedAt?'暂时无法确认最新版本':'尚未检查新版';
|
|
3493
|
+
const updHint=updMode==='managed'?'只安装检测到的稳定版本:等所有群空闲、记录结清后装到单独的目录,再切换后台服务;新版本没在 90 秒内连上全部群会自动换回。旧版本保留用于回滚,不提权,不在群里播报。'
|
|
3494
|
+
:updMode==='foreground'?'前台运行不能在控制台升级:在运行 SintBot 的终端按 Ctrl-C 等待退出,重新运行一行安装命令 curl -fsSL https://sintbot.com/install.sh | bash,再用原来的启动命令启动。'
|
|
3495
|
+
:updMode==='dev'?'开发版不在控制台升级,请用部署脚本。':'只安装检测到的稳定版本;所有群任务与队列清空后才会安装。需要可写的全局安装目录,不会提权;后台服务会自动重启,前台运行需手动重启。';
|
|
3489
3496
|
const updateCard=`<div class="card" style="grid-column:1/-1" id="updateCard"><div class="lbl">软件更新</div>
|
|
3490
3497
|
<div id="updateStatus" class="hint" style="margin:0 0 12px;color:var(--ink)">${updateText}</div>
|
|
3491
3498
|
${upd&&upd.available?`<div class="hint" style="margin:-4px 0 12px">版本差异与发布说明:<a href="https://github.com/sintlab/sintbot/releases/tag/v${encodeURIComponent(upd.latestVersion)}" target="_blank" rel="noopener" style="color:var(--ember-soft)">查看 v${esc(upd.latestVersion)} 发布说明</a></div>`:''}
|
|
3492
3499
|
<div class="inline" style="gap:14px;flex-wrap:wrap;margin-bottom:12px">
|
|
3493
3500
|
<span class="dim">自动检查稳定版(启动时及每天)</span>
|
|
3494
|
-
|
|
3501
|
+
${upd&&upd.announceSupported===false?'':`<label class="inline" style="gap:6px;cursor:pointer"><input type="checkbox" id="updAnnounce" ${upd&&upd.announce!==false?'checked':''} style="width:auto"/> <span class="dim">升级后向群里播报(记住选择)</span></label>`}
|
|
3495
3502
|
</div>
|
|
3496
3503
|
<div class="inline" style="gap:8px;flex-wrap:wrap">
|
|
3497
3504
|
<button class="btn" id="updCheck" ${!upd?'disabled':''}>检查新版</button>
|
|
3498
|
-
|
|
3505
|
+
${updCanInstall?`<button class="btn primary" id="updInstall" ${!upd||!upd.available||updateBusy?'disabled':''}>现在升级</button>`:''}
|
|
3499
3506
|
<button class="btn" id="updIgnore" ${!upd||!upd.available?'disabled':''}>忽略此版本</button>
|
|
3500
3507
|
</div>
|
|
3501
|
-
<div class="hint" style="margin-top:10px"
|
|
3508
|
+
<div class="hint" style="margin-top:10px">${updHint}</div>
|
|
3502
3509
|
<div class="update-history"><div class="subhead">版本历史</div>
|
|
3503
3510
|
<div id="verhistBox"><div class="empty">加载中…</div></div></div>
|
|
3504
3511
|
</div>`;
|
|
@@ -3546,18 +3553,19 @@ function renderSettings(){
|
|
|
3546
3553
|
}catch(e){toast(e.message,'err');b.disabled=false;}
|
|
3547
3554
|
};
|
|
3548
3555
|
const saveUpdatePrefs=async(patch)=>{try{S=await api('/api/ui/update-settings','POST',patch);renderSettings();}catch(e){toast(e.message,'err');}};
|
|
3549
|
-
$('#updAnnounce').onchange=()=>saveUpdatePrefs({announce:$('#updAnnounce').checked});
|
|
3556
|
+
if($('#updAnnounce'))$('#updAnnounce').onchange=()=>saveUpdatePrefs({announce:$('#updAnnounce').checked});
|
|
3550
3557
|
$('#updCheck').onclick=async()=>{const b=$('#updCheck');b.disabled=true;b.textContent='检查中…';
|
|
3551
3558
|
try{await api('/api/update/check','POST',{});S=await api('/api/state');await refreshInbox();renderSettings();renderOverview();toast('版本检查完成','ok');}
|
|
3552
3559
|
catch(e){toast(e.message,'err');b.disabled=false;b.textContent='检查新版';}};
|
|
3553
|
-
$('#updInstall').onclick=async()=>{const u=S.update||{},version=u.latestVersion;if(!version)return;
|
|
3554
|
-
const announce=$('#updAnnounce').checked;
|
|
3555
|
-
|
|
3560
|
+
if($('#updInstall'))$('#updInstall').onclick=async()=>{const u=S.update||{},version=u.latestVersion;if(!version)return;
|
|
3561
|
+
const announce=$('#updAnnounce')?$('#updAnnounce').checked:false;
|
|
3562
|
+
const what=u.mode==='managed'?'等所有群空闲、记录结清后装到单独的目录,再切换后台服务;切换期间约一分钟不处理新消息,新版本没连上全部群会自动换回。':`${announce?'安装完成后会向未暂停的群播报;':'本次不向群播报。'}所有群任务结束后才开始安装,后台服务会重启。`;
|
|
3563
|
+
if(!confirm(`将从 v${u.currentVersion} 升级到 v${version}。${what}继续?`))return;
|
|
3556
3564
|
const b=$('#updInstall');b.disabled=true;
|
|
3557
|
-
try{const r=await api('/api/update/start','POST',{version,announce});S.update={...r,announce};await refreshInbox();renderSettings();renderOverview();toast('更新已排队,群任务空闲后开始','ok');}
|
|
3565
|
+
try{const r=await api('/api/update/start','POST',{version,announce});S.update={...r,announce};await refreshInbox();renderSettings();renderOverview();toast(u.mode==='managed'?'升级已排队,所有群空闲后开始':'更新已排队,群任务空闲后开始','ok');}
|
|
3558
3566
|
catch(e){toast(e.message,'err');b.disabled=false;}};
|
|
3559
3567
|
$('#updIgnore').onclick=async()=>{const version=(S.update||{}).latestVersion;if(!version)return;
|
|
3560
|
-
try{const r=await api('/api/update/ignore','POST',{version});S.update={...r,announce:$('#updAnnounce').checked};await refreshInbox();renderSettings();renderOverview();toast('已忽略此版本','ok');}
|
|
3568
|
+
try{const r=await api('/api/update/ignore','POST',{version});S.update={...r,announce:$('#updAnnounce')?$('#updAnnounce').checked:false};await refreshInbox();renderSettings();renderOverview();toast('已忽略此版本','ok');}
|
|
3561
3569
|
catch(e){toast(e.message,'err');}};
|
|
3562
3570
|
// 只开着控制台的守护进程(还没接平台):第一次接上平台时说一句接下来会发生什么(服务托管的会自己重启连上)
|
|
3563
3571
|
{const pl=S.platforms||{};if(S.setupMode&&(pl.feishu||pl.slack||pl.wecom)&&!window._sbRestartToast){window._sbRestartToast=1;toast(S.setupMode.supervised?'已接上聊天平台 —— 后台服务几秒后自动重启并连上':'已接上聊天平台 —— 在终端里重启 sintbot start --ui 连上','ok');}}
|
|
@@ -4029,8 +4037,9 @@ updateThemeBtn();
|
|
|
4029
4037
|
/* I18N_DICT_BEGIN —— 合并翻译片段时整块替换这两个数组,不要改此标记之外的代码
|
|
4030
4038
|
⚠ 正则一律 ^…$ 整串、不加 g 标志;更具体的条目必须排在泛化条之前。 */
|
|
4031
4039
|
const I18N_EN=[
|
|
4040
|
+
["需要处理", "Needs attention"],
|
|
4032
4041
|
["当前可用操作", "Available controls"],
|
|
4033
|
-
["
|
|
4042
|
+
["当前支持多群消息、流式卡片回复、忙时跟进、停止任务、状态查看、认证管理、绑定新群、空闲群的模型(Codex / Claude Code)和项目目录切换,以及后台运行时的软件升级。新会话、解除绑定、控制台插话、定时任务、生图、画布与预览暂不可用。", "Available: group messages, streaming reply cards, busy-turn followups, stopping tasks, live status, authentication, binding new groups, switching the model (Codex / Claude Code) or project of an idle group, and software upgrades in a background run. New sessions, unbinding, console messages, schedules, image generation, canvas and previews are unavailable."],
|
|
4034
4043
|
["可绑定新群,修改模型、项目目录、权限、数据输出和忙时跟进;该群须没有进行中的任务和未结记录。切模型或项目会开始新的一段对话。停止任务在「概览」。群里发 /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 in Overview. Members can also switch models with /model in the group."],
|
|
4035
4044
|
["当前支持飞书文字消息、忙时跟进、任务回复和停止。定时任务、生图、群消息管理、画布与预览暂未接入。", "Feishu text messages, busy-turn followups, task replies and stopping are supported. Schedules, image generation, group message management, canvas and previews are unavailable."],
|
|
4036
4045
|
["当前不执行定时任务;已有定义仅供查看。", "Schedules are not executed. Existing definitions are shown for reference only."],
|
|
@@ -4763,6 +4772,11 @@ const I18N_EN=[
|
|
|
4763
4772
|
["更新已排队,群任务空闲后开始","Update queued; installation will start when all group tasks are idle"],
|
|
4764
4773
|
["已忽略此版本","This version was ignored"],
|
|
4765
4774
|
["版本检查完成","Version check complete"],
|
|
4775
|
+
["暂时无法确认最新版本","The latest version could not be confirmed yet"],
|
|
4776
|
+
["升级已排队,所有群空闲后开始","Upgrade queued; it starts when every group is idle"],
|
|
4777
|
+
["只安装检测到的稳定版本:等所有群空闲、记录结清后装到单独的目录,再切换后台服务;新版本没在 90 秒内连上全部群会自动换回。旧版本保留用于回滚,不提权,不在群里播报。","Installs only the detected stable version: once every group is idle and settled, it goes into its own folder and the background service switches to it; if the new version does not connect every group within 90 seconds, the previous one is restored. The previous version is kept for rollback. No elevated privileges and no group announcements."],
|
|
4778
|
+
["前台运行不能在控制台升级:在运行 SintBot 的终端按 Ctrl-C 等待退出,重新运行一行安装命令 curl -fsSL https://sintbot.com/install.sh | bash,再用原来的启动命令启动。","A foreground run cannot upgrade from the console: press Ctrl-C in the terminal running SintBot and wait for it to exit, rerun the one-line installer curl -fsSL https://sintbot.com/install.sh | bash, then start with your usual command."],
|
|
4779
|
+
["开发版不在控制台升级,请用部署脚本。","Development builds are not upgraded from the console; use the deployment scripts."],
|
|
4766
4780
|
["访问地址正在切换;页面即将重载","Changing the access address; this page will reload"],
|
|
4767
4781
|
["访问设置已保存","Access settings saved"],
|
|
4768
4782
|
["开启局域网访问前要设置至少 6 位访问密码","Set an access password of at least 6 characters before enabling LAN access"],
|
|
@@ -5378,16 +5392,25 @@ const I18N_EN_RX=[
|
|
|
5378
5392
|
[/^登录失败 \(HTTP (\d+)\)$/,"Sign-in failed (HTTP $1)"],
|
|
5379
5393
|
[/^检测到新版本 v([\d.]+) · 当前 v([\d.]+)$/,"New version v$1 · current v$2"],
|
|
5380
5394
|
[/^当前为最新版本 v([\d.]+)$/,"v$1 is the latest version"],
|
|
5395
|
+
[/^最新正式版 v([\d.]+) · 当前开发版 v(\S+)$/,"Latest release v$1 · this development build v$2"],
|
|
5381
5396
|
[/^等待所有群任务结束后更新$/,"Waiting for all group tasks to finish"],
|
|
5382
5397
|
[/^正在检查安装位置并安装指定版本$/,"Checking the install location and installing the selected version"],
|
|
5383
5398
|
[/^已安装,正在重启后台服务$/,"Installed; restarting the background service"],
|
|
5384
5399
|
[/^已安装新版;后台服务未能自动重启,请手动重启$/,"The new version is installed, but the background service did not restart; restart it manually"],
|
|
5385
5400
|
[/^已安装新版;当前不是受托管服务,请手动重启 SintBot$/,"The new version is installed; restart SintBot manually"],
|
|
5386
5401
|
[/^更新完成并已重启$/,"Update completed and restarted"],
|
|
5402
|
+
[/^等所有群空闲、记录结清后升级$/,"Waiting until every group is idle and settled"],
|
|
5403
|
+
[/^正在安装 v([\d.]+)$/,"Installing v$1"],
|
|
5404
|
+
[/^v([\d.]+) 已安装,等所有群空闲后切换$/,"v$1 is installed; switching when every group is idle"],
|
|
5405
|
+
[/^正在切换到 v([\d.]+);期间的消息会提示稍后重发$/,"Switching to v$1; messages meanwhile are asked to be resent later"],
|
|
5406
|
+
[/^已升级到 v([\d.]+)$/,"Upgraded to v$1"],
|
|
5407
|
+
[/^没有切换到 v([\d.]+):([\s\S]*);已换回 v([\d.]+)$/,"Did not switch to v$1 ($2); v$3 was restored"],
|
|
5408
|
+
[/^等待群空闲超时;可在空闲后重新发起$/,"Timed out waiting for idle groups; retry when they are idle"],
|
|
5409
|
+
[/^有暂停的群,升级已取消;先处理暂停的群$/,"A group is paused, so the upgrade was cancelled; handle the paused group first"],
|
|
5387
5410
|
[/^更新后未能自动重启,请手动重启服务$/,"The update could not restart automatically; restart the service manually"],
|
|
5388
5411
|
[/^等待群任务结束超时;可在任务空闲后重新发起$/,"Timed out waiting for group tasks; retry when they are idle"],
|
|
5389
5412
|
[/^当前运行的是开发目录或非全局副本;请在终端手动更新$/,"This is a development or non-global install; update it from a terminal"],
|
|
5390
|
-
[/^将从 v([\d.]+) 升级到 v([\d.]+)。([\s\S]*)$/,(a,from,to,what)=>`Upgrade from v${from} to v${to}. ${what.replace('所有群任务结束后才开始安装,后台服务会重启。','Installation waits until all group tasks finish, then restarts the background service.').replace('本次不向群播报。','This update will not be announced in groups.').replace('安装完成后会向未暂停的群播报;','After installation, it will be announced in active groups;')}`],
|
|
5413
|
+
[/^将从 v([\d.]+) 升级到 v([\d.]+)。([\s\S]*)$/,(a,from,to,what)=>`Upgrade from v${from} to v${to}. ${what.replace('等所有群空闲、记录结清后装到单独的目录,再切换后台服务;切换期间约一分钟不处理新消息,新版本没连上全部群会自动换回。','Once every group is idle and settled, it installs into its own folder and switches the background service; new messages wait about a minute during the switch, and the previous version is restored if the new one does not connect every group. ').replace('所有群任务结束后才开始安装,后台服务会重启。','Installation waits until all group tasks finish, then restarts the background service.').replace('本次不向群播报。','This update will not be announced in groups.').replace('安装完成后会向未暂停的群播报;','After installation, it will be announced in active groups;')}`],
|
|
5391
5414
|
];
|
|
5392
5415
|
/* I18N_DICT_END */
|
|
5393
5416
|
|
|
@@ -5588,7 +5611,7 @@ async function fetchCodexModels(){if(CX_MODELS)return CX_MODELS;try{CX_MODELS=aw
|
|
|
5588
5611
|
function cxDefEffortLabel(model){const m=CX_MODELS&&(CX_MODELS.models||[]).find(x=>x.model===model);return '默认'+(m&&m.defEffort?'('+m.defEffort+')':'');}
|
|
5589
5612
|
/* 有 codex provider 时启动即预热模型列表(server 缓存 10min)→ 卡片默认档标注补齐 + 面板秒开 */
|
|
5590
5613
|
function warmCodexModels(){if(!CX_MODELS&&S&&(S.providers||[]).some(x=>x.runner==='codex'&&!x.hidden))fetchCodexModels().then(()=>renderProviders());}
|
|
5591
|
-
const RUNTIME_DISABLED_SELECTORS='[data-osay],[data-onew],.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,#addModel,.pcard [data-act="edit"],.pcard [data-act="test"],#amSave,#amDel,#mNewGo,#pgSend,#pgNew,#fSave,#slSave,#wSave,#bSave,#styleNew,[data-styleedit],[data-styledup],[data-styledel],#nsSave,#nsExtract,#clSave
|
|
5614
|
+
const RUNTIME_DISABLED_SELECTORS='[data-osay],[data-onew],.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,#addModel,.pcard [data-act="edit"],.pcard [data-act="test"],#amSave,#amDel,#mNewGo,#pgSend,#pgNew,#fSave,#slSave,#wSave,#bSave,#styleNew,[data-styleedit],[data-styledup],[data-styledel],#nsSave,#nsExtract,#clSave,[data-pvstop],[data-lvkill],[data-lvkillall]';
|
|
5592
5615
|
function disableRuntimeControl(el,reason){
|
|
5593
5616
|
el.disabled=true;el.title=trText(reason);el.dataset.runtimeDisabled='1';
|
|
5594
5617
|
el.setAttribute('aria-disabled','true');
|
package/src/updater.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// Stable-version checks and guarded in-place upgrades for the global package.
|
|
2
|
+
// Checking is read-only. Installation is explicit, exact-version only, waits for
|
|
3
|
+
// every group queue to drain, and never escalates privileges.
|
|
4
|
+
|
|
5
|
+
const fs = require('node:fs')
|
|
6
|
+
const path = require('node:path')
|
|
7
|
+
const { execFile } = require('node:child_process')
|
|
8
|
+
const { promisify } = require('node:util')
|
|
9
|
+
const { atomicWriteJSON } = require('./core/store')
|
|
10
|
+
|
|
11
|
+
const execFileAsync = promisify(execFile)
|
|
12
|
+
const PACKAGE = 'sintbot'
|
|
13
|
+
const REGISTRIES = [
|
|
14
|
+
{ url: 'https://registry.npmjs.org/sintbot/latest', source: 'npm' },
|
|
15
|
+
{ url: 'https://registry.npmmirror.com/sintbot/latest', source: 'mirror' },
|
|
16
|
+
]
|
|
17
|
+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000
|
|
18
|
+
const CHECK_TIMEOUT_MS = 10_000
|
|
19
|
+
const QUEUE_TIMEOUT_MS = 24 * 60 * 60 * 1000
|
|
20
|
+
|
|
21
|
+
function stableVersion(value) {
|
|
22
|
+
const s = String(value || '').trim().replace(/^v/, '')
|
|
23
|
+
return /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(s) ? s : null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function compareVersion(a, b) {
|
|
27
|
+
const av = stableVersion(a); const bv = stableVersion(b)
|
|
28
|
+
if (!av || !bv) return 0
|
|
29
|
+
const aa = av.split('.').map(BigInt); const bb = bv.split('.').map(BigInt)
|
|
30
|
+
for (let i = 0; i < 3; i++) if (aa[i] !== bb[i]) return aa[i] > bb[i] ? 1 : -1
|
|
31
|
+
return 0
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function hasPendingWork(live) {
|
|
35
|
+
// Updating while the engine is missing or its snapshot is incomplete is unsafe:
|
|
36
|
+
// a busy group could be hidden by a failed state read. Treat unknown as busy.
|
|
37
|
+
if (!live || live.running !== true || !Number.isFinite(Number(live.queue)) || !live.groups || typeof live.groups !== 'object') return true
|
|
38
|
+
if (Number(live.queue) > 0) return true
|
|
39
|
+
return Object.values(live.groups).some((g) => !g || g.busy || !Number.isFinite(Number(g.queue)) || Number(g.queue) > 0)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function fetchVersion({ fetchImpl = globalThis.fetch, url, timeoutMs = CHECK_TIMEOUT_MS } = {}) {
|
|
43
|
+
const ac = new AbortController()
|
|
44
|
+
const timer = setTimeout(() => ac.abort(), timeoutMs)
|
|
45
|
+
try {
|
|
46
|
+
const res = await fetchImpl(url, { headers: { accept: 'application/json' }, signal: ac.signal, redirect: 'error' })
|
|
47
|
+
if (!res || !res.ok) throw new Error(`HTTP ${res && res.status || 'error'}`)
|
|
48
|
+
const data = await res.json()
|
|
49
|
+
const version = stableVersion(data && (data.version || (data['dist-tags'] && data['dist-tags'].latest)))
|
|
50
|
+
if (!version) throw new Error('registry returned no stable version')
|
|
51
|
+
return version
|
|
52
|
+
} finally { clearTimeout(timer) }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readState(file) {
|
|
56
|
+
try {
|
|
57
|
+
const v = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
58
|
+
return v && typeof v === 'object' ? v : {}
|
|
59
|
+
} catch { return {} }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function createUpdater({
|
|
63
|
+
currentVersion,
|
|
64
|
+
stateFile,
|
|
65
|
+
getLiveState = () => null,
|
|
66
|
+
fetchImpl = globalThis.fetch,
|
|
67
|
+
registries = REGISTRIES,
|
|
68
|
+
now = Date.now,
|
|
69
|
+
waitMs = 5000,
|
|
70
|
+
queueTimeoutMs = QUEUE_TIMEOUT_MS,
|
|
71
|
+
checkIntervalMs = CHECK_INTERVAL_MS,
|
|
72
|
+
isGlobalInstall = defaultGlobalInstall,
|
|
73
|
+
installExact = defaultInstallExact,
|
|
74
|
+
readInstalledVersion = defaultReadInstalledVersion,
|
|
75
|
+
getManagedService = () => false,
|
|
76
|
+
restartManagedService = async () => ({ ok: false }),
|
|
77
|
+
setAnnouncementBaseline = () => {},
|
|
78
|
+
log = () => {},
|
|
79
|
+
} = {}) {
|
|
80
|
+
const file = stateFile || path.join(require('node:os').homedir(), '.sbot', 'update.json')
|
|
81
|
+
const current = stableVersion(currentVersion) || '0.0.0'
|
|
82
|
+
let data = readState(file)
|
|
83
|
+
let queueTimer = null
|
|
84
|
+
let checking = null
|
|
85
|
+
let closed = false
|
|
86
|
+
|
|
87
|
+
// If a previous daemon was replaced during the restart step, the new package
|
|
88
|
+
// version is the evidence that installation and restart both completed.
|
|
89
|
+
if (data.job && data.job.state === 'restarting') {
|
|
90
|
+
data.job = data.job.to === current
|
|
91
|
+
? { ...data.job, state: 'done', message: '更新完成并已重启', updatedAt: now() }
|
|
92
|
+
: { ...data.job, state: 'failed', message: '更新后未能自动重启,请手动重启服务', updatedAt: now() }
|
|
93
|
+
persist()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function persist() { try { atomicWriteJSON(file, data, { mode: 0o600 }) } catch (e) { log('update state: ' + (e && e.message)) } }
|
|
97
|
+
function status() {
|
|
98
|
+
const latest = stableVersion(data.latestVersion)
|
|
99
|
+
return {
|
|
100
|
+
currentVersion: current,
|
|
101
|
+
latestVersion: latest,
|
|
102
|
+
checkedAt: Number(data.checkedAt) || null,
|
|
103
|
+
source: data.source || '',
|
|
104
|
+
ignoredVersion: data.ignoredVersion || '',
|
|
105
|
+
available: !!latest && compareVersion(latest, current) > 0 && data.ignoredVersion !== latest,
|
|
106
|
+
job: data.job ? { ...data.job } : null,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function check() {
|
|
111
|
+
if (closed) return status()
|
|
112
|
+
if (checking) return checking
|
|
113
|
+
checking = (async () => {
|
|
114
|
+
let lastError = null
|
|
115
|
+
for (const r of registries) {
|
|
116
|
+
try {
|
|
117
|
+
const version = await fetchVersion({ fetchImpl, url: r.url })
|
|
118
|
+
data.latestVersion = version
|
|
119
|
+
data.checkedAt = now()
|
|
120
|
+
data.source = r.source || ''
|
|
121
|
+
if (data.ignoredVersion && data.ignoredVersion !== version) data.ignoredVersion = ''
|
|
122
|
+
persist()
|
|
123
|
+
return status()
|
|
124
|
+
} catch (e) { lastError = e }
|
|
125
|
+
}
|
|
126
|
+
// Network errors are deliberately quiet and never erase the last success.
|
|
127
|
+
if (lastError) log('update check unavailable: ' + (lastError && lastError.message || 'network error'))
|
|
128
|
+
return status()
|
|
129
|
+
})().finally(() => { checking = null })
|
|
130
|
+
return checking
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function ignore(version) {
|
|
134
|
+
const v = stableVersion(version)
|
|
135
|
+
if (!v || v !== stableVersion(data.latestVersion)) throw new Error('只能忽略当前检测到的版本')
|
|
136
|
+
data.ignoredVersion = v
|
|
137
|
+
persist()
|
|
138
|
+
return status()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function startUpgrade({ version, announce = true } = {}) {
|
|
142
|
+
const v = stableVersion(version)
|
|
143
|
+
if (!v || v !== stableVersion(data.latestVersion) || compareVersion(v, current) <= 0) throw new Error('版本信息已变化,请先重新检查')
|
|
144
|
+
if (data.job && ['queued', 'installing', 'restarting', 'restart-needed'].includes(data.job.state)) throw new Error('已有更新需要完成或重启后再试')
|
|
145
|
+
data.job = { id: `${now()}-${Math.random().toString(36).slice(2, 8)}`, state: 'queued', from: current, to: v,
|
|
146
|
+
announce: announce !== false, createdAt: now(), updatedAt: now(), message: '等待所有群任务结束后更新' }
|
|
147
|
+
persist()
|
|
148
|
+
void processQueue()
|
|
149
|
+
return status()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function scheduleQueueCheck() {
|
|
153
|
+
if (closed || queueTimer || !data.job || data.job.state !== 'queued') return
|
|
154
|
+
queueTimer = setTimeout(() => { queueTimer = null; void processQueue() }, waitMs)
|
|
155
|
+
if (queueTimer.unref) queueTimer.unref()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function processQueue() {
|
|
159
|
+
const job = data.job
|
|
160
|
+
if (closed || !job || job.state !== 'queued') return
|
|
161
|
+
if (now() - job.createdAt > queueTimeoutMs) {
|
|
162
|
+
data.job = { ...job, state: 'failed', message: '等待群任务结束超时;可在任务空闲后重新发起', updatedAt: now() }
|
|
163
|
+
persist(); return
|
|
164
|
+
}
|
|
165
|
+
let live
|
|
166
|
+
try { live = getLiveState() }
|
|
167
|
+
catch (e) { log('update queue: live state unavailable: ' + (e && e.message)); return scheduleQueueCheck() }
|
|
168
|
+
if (hasPendingWork(live)) return scheduleQueueCheck()
|
|
169
|
+
if (queueTimer) { clearInterval(queueTimer); queueTimer = null }
|
|
170
|
+
data.job = { ...job, state: 'installing', message: '正在检查安装位置并安装指定版本', updatedAt: now() }
|
|
171
|
+
persist()
|
|
172
|
+
try {
|
|
173
|
+
const checkResult = await isGlobalInstall()
|
|
174
|
+
if (!checkResult || !checkResult.ok) throw new Error((checkResult && checkResult.reason) || '当前运行位置不是可写的全局安装')
|
|
175
|
+
const result = await installExact(PACKAGE, job.to, checkResult)
|
|
176
|
+
if (!result || result.ok === false) throw new Error((result && result.reason) || '安装失败;检查包管理器日志后可手动重试')
|
|
177
|
+
const installed = await readInstalledVersion(checkResult)
|
|
178
|
+
if (stableVersion(installed) !== job.to) throw new Error('安装完成后的版本核对不符;请手动检查后重启')
|
|
179
|
+
if (!job.announce) { try { setAnnouncementBaseline(job.to) } catch (e) { log('update announcement baseline: ' + (e && e.message)) } }
|
|
180
|
+
const managed = await getManagedService()
|
|
181
|
+
if (managed) {
|
|
182
|
+
data.job = { ...job, state: 'restarting', message: '已安装,正在重启后台服务', updatedAt: now() }
|
|
183
|
+
persist()
|
|
184
|
+
const restarted = await restartManagedService()
|
|
185
|
+
if (restarted && restarted.ok) return status()
|
|
186
|
+
data.job = { ...job, state: 'restart-needed', message: '已安装新版;后台服务未能自动重启,请手动重启', updatedAt: now() }
|
|
187
|
+
} else {
|
|
188
|
+
data.job = { ...job, state: 'restart-needed', message: '已安装新版;当前不是受托管服务,请手动重启 SintBot', updatedAt: now() }
|
|
189
|
+
}
|
|
190
|
+
persist()
|
|
191
|
+
} catch (e) {
|
|
192
|
+
data.job = { ...job, state: 'failed', message: String(e && e.message || e).slice(0, 300), updatedAt: now() }
|
|
193
|
+
persist()
|
|
194
|
+
}
|
|
195
|
+
return status()
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (data.job && data.job.state === 'queued') void processQueue()
|
|
199
|
+
const checkTimer = setInterval(() => { void check() }, checkIntervalMs)
|
|
200
|
+
if (checkTimer.unref) checkTimer.unref()
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
status,
|
|
204
|
+
check,
|
|
205
|
+
ignore,
|
|
206
|
+
startUpgrade,
|
|
207
|
+
start() { return check() },
|
|
208
|
+
close() { closed = true; clearInterval(checkTimer); if (queueTimer) clearTimeout(queueTimer); queueTimer = null },
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function defaultGlobalInstall() {
|
|
213
|
+
const { execFileSync } = require('node:child_process')
|
|
214
|
+
const rootOut = execFileSync('npm', ['root', '-g'], { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'pipe'] }).trim()
|
|
215
|
+
const globalRoot = fs.realpathSync(rootOut)
|
|
216
|
+
const globalPackage = path.join(globalRoot, PACKAGE)
|
|
217
|
+
const runningPackage = fs.realpathSync(path.resolve(__dirname, '..'))
|
|
218
|
+
if (runningPackage !== fs.realpathSync(globalPackage)) return { ok: false, reason: '当前运行的是开发目录或非全局副本;请在终端手动更新' }
|
|
219
|
+
fs.accessSync(globalRoot, fs.constants.W_OK | fs.constants.X_OK)
|
|
220
|
+
fs.accessSync(path.dirname(globalPackage), fs.constants.W_OK | fs.constants.X_OK)
|
|
221
|
+
return { ok: true, globalRoot, globalPackage }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function defaultInstallExact(name, version) {
|
|
225
|
+
const env = {}
|
|
226
|
+
const allowed = ['PATH', 'HOME', 'USERPROFILE', 'APPDATA', 'LOCALAPPDATA', 'TEMP', 'TMP', 'SystemRoot', 'PATHEXT',
|
|
227
|
+
'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy', 'LANG', 'LC_ALL']
|
|
228
|
+
for (const k of allowed) if (process.env[k] !== undefined) env[k] = process.env[k]
|
|
229
|
+
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'
|
|
230
|
+
try {
|
|
231
|
+
await execFileAsync(npm, ['install', '--global', `${name}@${version}`, '--omit=dev', '--no-audit', '--no-fund'],
|
|
232
|
+
{ timeout: 10 * 60 * 1000, maxBuffer: 2 * 1024 * 1024, windowsHide: true, env })
|
|
233
|
+
return { ok: true }
|
|
234
|
+
} catch (e) {
|
|
235
|
+
return { ok: false, reason: `安装失败(退出码 ${e && e.code != null ? e.code : '未知'});未提权,也未改用其他版本` }
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function defaultReadInstalledVersion(info) {
|
|
240
|
+
try { return JSON.parse(fs.readFileSync(path.join(info.globalPackage, 'package.json'), 'utf8')).version || null } catch { return null }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
module.exports = { PACKAGE, REGISTRIES, CHECK_INTERVAL_MS, stableVersion, compareVersion, hasPendingWork, fetchVersion, createUpdater, defaultGlobalInstall, defaultInstallExact, defaultReadInstalledVersion }
|