sintbot 1.4.11 → 1.4.13
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 +54 -0
- package/CHANGELOG.json +54 -0
- package/bin/sintbot-v2.js +12 -3
- package/docs/AI-ASSISTED-INSTALL.md +2 -2
- package/docs/install.md +14 -4
- package/docs/zh-CN/AI-ASSISTED-INSTALL.md +2 -2
- package/docs/zh-CN/install.md +12 -4
- package/install.sh +1 -1
- package/package.json +7 -1
- package/src/agent/claudecode.js +6 -0
- package/src/agent/codexapp.js +5 -1
- package/src/agent/privatecore.js +1 -1
- package/src/agent/privatecore.wasm +0 -0
- package/src/agent/runner.js +3 -1
- package/src/c2console.js +85 -15
- package/src/c2updater.js +13 -5
- package/src/consolesay.js +6 -6
- package/src/consoletrials.js +43 -0
- package/src/core/c2cardterminalpath.js +2 -1
- package/src/core/c2durablejournal.js +10 -0
- package/src/core/v2engine.js +70 -19
- package/src/feishucli.js +13 -1
- package/src/modelcontext.js +13 -0
- package/src/platform/feishu.js +29 -9
- package/src/recordedanswers.js +27 -0
- package/src/runtimefacts.js +11 -0
- package/src/ui/console.html +49 -29
- package/src/ui/liveactivity-client.js +276 -0
- package/src/ui/liveactivity.js +194 -0
- package/src/ui/serverbase.js +41 -14
package/src/ui/console.html
CHANGED
|
@@ -1008,7 +1008,6 @@ d.dataset.theme=t;if(l!=='en'&&l!=='zh')l=/^zh/i.test((navigator.language||''))?
|
|
|
1008
1008
|
<button data-tab="playground" class="navplay" title="试跑 — 从控制台直接跑一轮" aria-label="试跑"><svg class="ic ic-play" aria-hidden="true"><use href="#lu-play"/></svg></button>
|
|
1009
1009
|
</nav>
|
|
1010
1010
|
|
|
1011
|
-
<div id="runtimeCapabilitiesNotice" class="card" hidden style="margin-bottom:16px"><div class="lbl">当前可用操作</div><div class="hint" data-runtime-summary></div></div>
|
|
1012
1011
|
|
|
1013
1012
|
<section id="overview" class="tab on"><div class="empty" style="padding:48px;text-align:center"><span class="spin"></span> 加载中…</div></section>
|
|
1014
1013
|
<section id="providers" class="tab"></section>
|
|
@@ -1027,6 +1026,7 @@ d.dataset.theme=t;if(l!=='en'&&l!=='zh')l=/^zh/i.test((navigator.language||''))?
|
|
|
1027
1026
|
/* 令牌来源:本机环回免密时服务端注入;设了密码时注入空串,退而用登录(密码 / 飞书)签发的会话令牌 ——
|
|
1028
1027
|
记在 localStorage(服务端落盘 30 天,重启不登出);过期或被吊销的令牌只会 401 → 重新登录。 */
|
|
1029
1028
|
let TOKEN='__SBOT_TOKEN__'; // 本机环回免密时服务端注入;为空 = 要登录(⚠ 这一行的形状被服务端单测当锚点抠 token,别改)
|
|
1029
|
+
/*__LIVE_ACTIVITY__*/
|
|
1030
1030
|
const INJECTED_TOKEN=TOKEN; // 记住「是不是注入的」:注入态不显示退出登录
|
|
1031
1031
|
TOKEN=TOKEN||localStorage.getItem('sbot-token')||sessionStorage.getItem('sbot-token')||''; // 登录签发的令牌记在 localStorage(30 天)
|
|
1032
1032
|
const FEISHU_LOGIN='__SBOT_FEISHU_LOGIN__'; // 服务端算好:公网地址 + 凭证 + 管理员名单三样齐才是 '1'
|
|
@@ -1149,6 +1149,7 @@ function startCardHtml(){
|
|
|
1149
1149
|
</div>`;
|
|
1150
1150
|
}
|
|
1151
1151
|
function renderOverview(){
|
|
1152
|
+
if(typeof activityPanel!=='undefined')activityPanel.preserve();
|
|
1152
1153
|
const h=S.health;
|
|
1153
1154
|
const cap=Math.max((S.agent&&S.agent.maxConcurrent)||0,0); // 0 = 不限(无固定槽位 → 不画 gauge)
|
|
1154
1155
|
const segs=cap>0?Array.from({length:cap},()=>'<div class="seg"></div>').join(''):'';
|
|
@@ -1164,13 +1165,13 @@ function renderOverview(){
|
|
|
1164
1165
|
${liveOn?`<button class="iconbtn sm" data-ostop="${esc(g.chatId)}" title="停止当前任务(保留会话)" style="flex:none">${IC.square}</button>
|
|
1165
1166
|
<button class="iconbtn sm" data-onew="${esc(g.chatId)}" title="${S.runtimeCapabilities?'开新会话 — 仅在该群空闲、记录全部结账后可用;运行中请先停止,结束后再开(同群内 /new)':'开新会话 — 运行中会先中断当前任务并清空队列(同群内 /new)'}" style="flex:none">${IC.rotate}</button>`:''}</div>`).join('')
|
|
1166
1167
|
:'<div class="empty">还没有群项目 · 去「群·项目」配置</div>';
|
|
1167
|
-
$('#overview').innerHTML=`${startCardHtml()}<div id="ovInboxWrap">${inboxHtml()}</div><div class="grid cols2 stagger">
|
|
1168
|
+
$('#overview').innerHTML=`${startCardHtml()}<div id="ovInboxWrap">${inboxHtml()}</div><div class="grid ${S.runtimeCapabilities?'':'cols2'} stagger">
|
|
1168
1169
|
<div class="card" id="ovHealth"><div class="lbl">健康</div>
|
|
1169
1170
|
<span class="hpill ${healthClass(h)}" id="ovHealthPill"><span class="led"></span><span id="ovHealthTxt">${healthText(h)}</span></span>
|
|
1170
1171
|
<div class="hrows" id="ovHrows">${h.rows.map(r=>`<div class="hrow" data-hk="${esc(r.label)}"><span class="dot ${r.status}"></span>
|
|
1171
1172
|
<span class="k">${esc(r.label)}</span><span class="v" title="${esc(r.detail)}">${esc(r.detail)}</span></div>`).join('')}</div>
|
|
1172
1173
|
</div>
|
|
1173
|
-
|
|
1174
|
+
${S.runtimeCapabilities?'':`<div class="grid" style="gap:16px">
|
|
1174
1175
|
<div class="card">
|
|
1175
1176
|
<div class="inline" style="justify-content:space-between;align-items:baseline">
|
|
1176
1177
|
<span class="lbl" style="margin:0">并发</span>
|
|
@@ -1179,14 +1180,15 @@ function renderOverview(){
|
|
|
1179
1180
|
${cap>0?`<div class="gauge" id="ovGauge" style="margin-top:13px">${segs}</div>`:''}
|
|
1180
1181
|
</div>
|
|
1181
1182
|
<div class="card ovgroups"><div class="lbl">群 · 项目</div>${gRows}</div>
|
|
1182
|
-
</div
|
|
1183
|
+
</div>`}
|
|
1183
1184
|
</div>
|
|
1184
|
-
<h2 class="sec">实时活动</h2><div class="card" id="ovActivity">${activityHtml()}</div
|
|
1185
|
+
${S.runtimeCapabilities?'<div id="liveActivity"></div>':`<h2 class="sec">实时活动</h2><div class="card" id="ovActivity">${activityHtml()}</div>`}
|
|
1185
1186
|
${issuesHtml()}
|
|
1186
1187
|
${previewsHtml()}
|
|
1187
1188
|
${leftoversHtml()}`;
|
|
1188
1189
|
lastActSig='';
|
|
1189
1190
|
bindActivityClicks();
|
|
1191
|
+
if(S.runtimeCapabilities&&typeof activityPanel!=='undefined')activityPanel.mount();
|
|
1190
1192
|
bindInbox();
|
|
1191
1193
|
// 插话:入口挂在「群·项目」每一行的 ✎ 上 —— 动作长在对象身上,目标群由那一行决定,不必再选。
|
|
1192
1194
|
$$('#overview [data-osay]').forEach(b=>b.onclick=()=>openSay(b.dataset.osay));
|
|
@@ -1374,6 +1376,7 @@ function bindActivityClicks(){
|
|
|
1374
1376
|
let lastActSig='';
|
|
1375
1377
|
function updateOverviewLive(){
|
|
1376
1378
|
if(!S) return;
|
|
1379
|
+
if(S.runtimeCapabilities&&typeof activityPanel!=='undefined')activityPanel.live(S.live);
|
|
1377
1380
|
if(sayRefresh&&$('#sayT')) sayRefresh();
|
|
1378
1381
|
const live=S.live||{};
|
|
1379
1382
|
const active=(live.concurrency&&live.concurrency.active)||0;
|
|
@@ -2610,7 +2613,6 @@ function cronFullHtml(text){
|
|
|
2610
2613
|
}).join('');
|
|
2611
2614
|
}
|
|
2612
2615
|
const RUNTIME_UI_TEXT={
|
|
2613
|
-
"summary": "当前支持多群消息、流式卡片回复、忙时跟进、停止任务、空闲群开新会话、空闲群的控制台插话(这句话和回复都进群)、状态查看、认证管理、绑定新群、空闲群的模型(Codex / Claude Code)和项目目录切换、模型的添加与设置,以及后台运行时的软件升级。群忙时的插话、静默插话、解除绑定、移除模型、定时任务、生图、画布与预览暂不可用。",
|
|
2614
2616
|
"groups": "可绑定新群,修改模型、项目目录、权限、数据输出和忙时跟进;该群须没有进行中的任务和未结记录。切模型或项目会开始新的一段对话。停止任务和开新会话在「概览」,开新会话须等该群空闲。群里发 /model 也能切换模型。",
|
|
2615
2617
|
"skills": "当前支持飞书文字消息、忙时跟进、任务回复和停止。定时任务、生图、群消息管理、画布与预览暂未接入。",
|
|
2616
2618
|
"cron": "当前不执行定时任务;已有定义仅供查看。",
|
|
@@ -2619,7 +2621,7 @@ const RUNTIME_UI_TEXT={
|
|
|
2619
2621
|
"paused": "已暂停:不接收本群新消息,配置与会话保留。",
|
|
2620
2622
|
"resume": "恢复本群:重新接收本群消息,配置与会话保留。",
|
|
2621
2623
|
"resumeLabel": "恢复",
|
|
2622
|
-
"steer": "Codex 与 Claude Code
|
|
2624
|
+
"steer": "Codex 与 Claude Code 默认开启忙时跟进;长命令执行期间的补充会在工具步骤之间读取。只有确认模型已读取才显示已加入;不支持此能力时排队。控制台插话同样支持忙时跟进;关闭或暂不支持时排队。",
|
|
2623
2625
|
"preview": "当前版本未接入预览服务。",
|
|
2624
2626
|
"providers": "可添加模型,修改型号、端点、密钥、思考深度与推理强度,测试、隐藏或拖动排序;各群的模型在「群·项目」里切换。改型号、后端或端点时,用它的群须空闲,并会开始新的一段对话;默认模型和群正在用的模型不能隐藏。移除模型暂不可用。",
|
|
2625
2627
|
"providersView": "当前可查看模型配置与登录状态;各群的模型在「群·项目」里切换。添加或修改模型暂不可用。",
|
|
@@ -3337,18 +3339,17 @@ function sayPlan(chatId,reply){
|
|
|
3337
3339
|
function runsCodex(g){const pr=(S.providers||[]).find(pp=>pp.name===g.effectiveProvider);return !!pr&&pr.runner==='codex';}
|
|
3338
3340
|
function sayAge(ms){const m=Math.floor((ms||0)/60e3);if(m<1)return '不到 1 分钟';if(m<60)return m+' 分钟';return Math.floor(m/60)+' 小时'+(m%60?' '+(m%60)+' 分钟':'');}
|
|
3339
3341
|
let sayRefresh=null; // 浮层开着时,概览的实时刷新顺手重算一遍(打字的这会儿任务可能刚好跑完)
|
|
3340
|
-
/*
|
|
3341
|
-
直接说明白、按钮不可点 —— 不再说「加入当前任务 / 排队」,那是旧流程的行为。原因由服务端实时状态
|
|
3342
|
-
(live.groups[chatId].sayBlocked,与插话接口同一个判据)给出;没有实时状态时以接口回报为准。 */
|
|
3342
|
+
/* 插话按服务端准入显示阻塞原因;正常忙时允许提交,是否已加入只以模型读取确认判断。 */
|
|
3343
3343
|
const SAY_V1={
|
|
3344
|
-
ok:'
|
|
3345
|
-
busy:'
|
|
3344
|
+
ok:'这句话(标着「管理员」)和回复都会发进群。任务进行中也可以插话;确认模型读取后才算已加入。',
|
|
3345
|
+
busy:'该群正在停止、跳过或处理未结记录,请稍后再插话。',
|
|
3346
|
+
'queue-full':'该群等待处理的消息已满,请稍后再插话。',
|
|
3346
3347
|
held:'该群上一个任务结果不明,已暂停处理、等待人工确认;现在不能插话。',
|
|
3347
3348
|
upgrading:'正在升级,现在不能插话,请一分钟后再试。',
|
|
3348
3349
|
paused:'这个群项目已暂停,先恢复再插话。',
|
|
3349
3350
|
closed:'服务正在启动或停止,或该群当前未开放;现在不能插话。'
|
|
3350
3351
|
};
|
|
3351
|
-
function sayV1Blocked(chatId){const lv=((S.live||{}).groups||{})[chatId]||{};return lv.sayBlocked||
|
|
3352
|
+
function sayV1Blocked(chatId){const lv=((S.live||{}).groups||{})[chatId]||{};return lv.sayBlocked||null;}
|
|
3352
3353
|
/* 每次提交一个 32 位十六进制幂等键:同一段文字没成功前重点一次,沿用同一个键,服务端据此不重复处理 */
|
|
3353
3354
|
function sayRequestId(){const a=new Uint8Array(16);crypto.getRandomValues(a);return Array.from(a,b=>b.toString(16).padStart(2,'0')).join('');}
|
|
3354
3355
|
/* 「向群里插一句」浮层。入口在概览「群·项目」每一行的 ✎ —— 动作长在对象身上,
|
|
@@ -3378,12 +3379,13 @@ function openSay(chatId){
|
|
|
3378
3379
|
const v1=!!S.runtimeCapabilities; let sending=false, req=null;
|
|
3379
3380
|
// 当前版本两个开关固定勾上:只支持「这句话进群 + 回复进群」,开关留着,让人看见这句会以什么样子出现
|
|
3380
3381
|
if(v1){ for(const c of [sw,rp]){ c.checked=true; c.disabled=true; c.parentElement.title='当前版本固定:这句话和回复都发进群'; } }
|
|
3381
|
-
const preview=()=>{ const a=sw.checked,b=rp.checked, plan=
|
|
3382
|
+
const preview=()=>{ const a=sw.checked,b=rp.checked, plan=sayPlan(chatId,b);
|
|
3382
3383
|
if(v1){
|
|
3383
3384
|
const why=sayV1Blocked(chatId), lv=((S.live||{}).groups||{})[chatId]||{};
|
|
3384
|
-
bz.innerHTML=
|
|
3385
|
-
|
|
3386
|
-
|
|
3385
|
+
bz.innerHTML=lv.busy?`<span class="dot"></span><span>${lv.finalizing?'正在收尾':'正在处理任务'}</span> · ${fmtDur(lv.sinceMs)}`:why==='held'?'<span class="dot"></span>待人工确认':'';
|
|
3386
|
+
const detail=lv.busy?(lv.followups?'将尝试跟进当前任务;长命令执行期间需等待工具步骤结束,实际进度请查看实时活动。':'当前任务暂不接收跟进,这句话会排队处理。'):'';
|
|
3387
|
+
pv.innerHTML=why?esc(SAY_V1[why]||SAY_V1.closed):`<span>${esc(detail)}</span> <span>${esc(SAY_V1.ok)}</span>`;
|
|
3388
|
+
if(!sending){ go.disabled=!!why; goTxt.textContent=why?'暂不能插':lv.busy?(lv.followups?'发送补充':'排队'):'插入'; }
|
|
3387
3389
|
return;
|
|
3388
3390
|
}
|
|
3389
3391
|
const vis = a&&b ? '群里会看到:你插的这句话(标着「管理员」)和它做完的回复。'
|
|
@@ -3420,7 +3422,10 @@ function openSay(chatId){
|
|
|
3420
3422
|
// 回报照实说清两件事:这句**落在哪**(服务端按引擎实际落点回报),以及群里会/不会出现什么 —— 只说「已插入」等于没说
|
|
3421
3423
|
const vis = inj&&inj.show ? (inj.reply?'群里能看到这句话和回复':'群里只看到这句话') : (inj&&inj.reply?'群里只会出现回复':'群里不会有任何动静');
|
|
3422
3424
|
const w=(inj&&inj.where)||{};
|
|
3423
|
-
const head = w.state==='
|
|
3425
|
+
const head = w.state==='following' ? '已受理 · 等待模型读取,尚未确认加入'
|
|
3426
|
+
: w.state==='accepted' ? '已受理 · 处理状态请查看实时活动'
|
|
3427
|
+
: w.state==='done' ? '已处理'
|
|
3428
|
+
: w.state==='joined' ? '已加入当前任务'+(inj&&inj.show?' · 回复卡片会跟到这句话下面':'')
|
|
3424
3429
|
: w.state==='queued' ? (w.join ? '已排队 · 当前任务一就绪就会加入' : '群正忙,已排队'+(w.ahead?`(前面还有 ${w.ahead} 条)`:'')+' · 当前任务结束后处理')
|
|
3425
3430
|
: '已插入';
|
|
3426
3431
|
toast(head+' · '+vis,'ok');
|
|
@@ -4300,9 +4305,11 @@ updateThemeBtn();
|
|
|
4300
4305
|
/* I18N_DICT_BEGIN —— 合并翻译片段时整块替换这两个数组,不要改此标记之外的代码
|
|
4301
4306
|
⚠ 正则一律 ^…$ 整串、不加 g 标志;更具体的条目必须排在泛化条之前。 */
|
|
4302
4307
|
const I18N_EN=[
|
|
4308
|
+
["正在处理任务", "Task in progress"],
|
|
4309
|
+
["已受理", "Accepted"],
|
|
4310
|
+
["等待模型读取,尚未确认加入", "Waiting for the model to read it; not yet confirmed joined"],
|
|
4311
|
+
["处理状态请查看实时活动", "Check live activity for processing status"],
|
|
4303
4312
|
["需要处理", "Needs attention"],
|
|
4304
|
-
["当前可用操作", "Available controls"],
|
|
4305
|
-
["当前支持多群消息、流式卡片回复、忙时跟进、停止任务、空闲群开新会话、空闲群的控制台插话(这句话和回复都进群)、状态查看、认证管理、绑定新群、空闲群的模型(Codex / Claude Code)和项目目录切换、模型的添加与设置,以及后台运行时的软件升级。群忙时的插话、静默插话、解除绑定、移除模型、定时任务、生图、画布与预览暂不可用。", "Available: group messages, streaming reply cards, busy-turn followups, stopping tasks, new sessions for idle groups, console messages to idle groups (the line and the reply both appear in the group), 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. Console messages to busy groups, silent console messages, unbinding, removing models, schedules, image generation, canvas and previews are unavailable."],
|
|
4306
4313
|
["可绑定新群,修改模型、项目目录、权限、数据输出和忙时跟进;该群须没有进行中的任务和未结记录。切模型或项目会开始新的一段对话。停止任务和开新会话在「概览」,开新会话须等该群空闲。群里发 /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."],
|
|
4307
4314
|
["当前支持飞书文字消息、忙时跟进、任务回复和停止。定时任务、生图、群消息管理、画布与预览暂未接入。", "Feishu text messages, busy-turn followups, task replies and stopping are supported. Schedules, image generation, group message management, canvas and previews are unavailable."],
|
|
4308
4315
|
["当前不执行定时任务;已有定义仅供查看。", "Schedules are not executed. Existing definitions are shown for reference only."],
|
|
@@ -4311,7 +4318,7 @@ const I18N_EN=[
|
|
|
4311
4318
|
["已暂停:不接收本群新消息,配置与会话保留。", "Paused: no new group messages are accepted; configuration and sessions are retained."],
|
|
4312
4319
|
["恢复本群:重新接收本群消息,配置与会话保留。", "Resume this group: its messages are accepted again; configuration and sessions are retained."],
|
|
4313
4320
|
["恢复", "Resume"],
|
|
4314
|
-
["Codex 与 Claude Code
|
|
4321
|
+
["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."],
|
|
4315
4322
|
["当前版本未接入预览服务。", "Preview services are unavailable in this version."],
|
|
4316
4323
|
["当前可查看模型配置与登录状态;各群的模型在「群·项目」里切换。添加或修改模型暂不可用。", "Model configuration and login status are shown here. Switch each group's model under Groups. Adding or changing models is unavailable."],
|
|
4317
4324
|
["可添加模型,修改型号、端点、密钥、思考深度与推理强度,测试、隐藏或拖动排序;各群的模型在「群·项目」里切换。改型号、后端或端点时,用它的群须空闲,并会开始新的一段对话;默认模型和群正在用的模型不能隐藏。移除模型暂不可用。", "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."],
|
|
@@ -4388,6 +4395,12 @@ const I18N_EN=[
|
|
|
4388
4395
|
["此群当前未开放,未开新会话","This group is not open right now; no new session was started."],
|
|
4389
4396
|
["还没有群项目 · 去「群·项目」配置","No group projects yet · set one up on the Groups · Projects page"],
|
|
4390
4397
|
["健康","Health"],
|
|
4398
|
+
["模型已隐藏,不能试运行","This model is hidden and cannot be tested."],
|
|
4399
|
+
["服务写入权限已失效,试运行结果未保存","The service lost write access; the trial result was not saved."],
|
|
4400
|
+
["服务未就绪或正在维护,暂不能试运行","The service is not ready or is under maintenance; trials are unavailable."],
|
|
4401
|
+
["试运行或模型并发名额已满,请稍后重试","The trial or model concurrency limit is reached. Try again later."],
|
|
4402
|
+
["当前服务未接入试运行并发准入","This service has no trial admission control."],
|
|
4403
|
+
["试运行进程关闭状态未知,结果未保存,并发名额已保留","Trial process closure is unknown; the result was not saved and its capacity slot remains reserved."],
|
|
4391
4404
|
["并发","Concurrency"],
|
|
4392
4405
|
["活动","Active"],
|
|
4393
4406
|
["· 无上限","· No limit"],
|
|
@@ -4986,6 +4999,14 @@ const I18N_EN=[
|
|
|
4986
4999
|
["输入文件夹名","Enter a folder name"],
|
|
4987
5000
|
["要它做什么,像跟人说话那样写…","What should it do? Write it the way you would tell a person…"],
|
|
4988
5001
|
["群里可见","Visible in the group"],
|
|
5002
|
+
["发送补充", "Send followup"],
|
|
5003
|
+
["将尝试跟进当前任务;长命令执行期间需等待工具步骤结束,实际进度请查看实时活动。", "Will try to follow up on the current task. Long commands must finish first; check live activity for progress."],
|
|
5004
|
+
["当前任务暂不接收跟进,这句话会排队处理。", "The current task is not accepting followups; this message will queue."],
|
|
5005
|
+
["已受理 · 等待模型读取,尚未确认加入", "Accepted · waiting for the model to read it; not yet confirmed joined"],
|
|
5006
|
+
["已受理 · 处理状态请查看实时活动", "Accepted · check live activity for processing status"],
|
|
5007
|
+
["该群等待处理的消息已满,请稍后再插话。", "The message queue is full. Try again shortly."],
|
|
5008
|
+
["该群等待处理的消息已满,请稍后再插话", "The message queue is full. Try again shortly."],
|
|
5009
|
+
["该群正在停止、跳过或处理未结记录,请稍后再插话", "This group is stopping, skipping, or settling pending records. Try again shortly."],
|
|
4989
5010
|
["你插的这句话本身要不要发进群(会标着「管理员」)","Whether the line you interject is itself posted in the group (marked \"admin\")"],
|
|
4990
5011
|
["插话","Interject"],
|
|
4991
5012
|
["它做完之后的回复要不要发进群;取消后连报错也不会说","Whether its reply is posted in the group when it finishes; untick and even errors stay silent"],
|
|
@@ -5000,8 +5021,8 @@ const I18N_EN=[
|
|
|
5000
5021
|
["什么都不会出现","shows nothing at all"],
|
|
5001
5022
|
[",连出错也不会说 —— 状态去「实时活动」看。",", not even errors — check the status under \"Live activity\"."],
|
|
5002
5023
|
["插入中…","Inserting…"],
|
|
5003
|
-
["
|
|
5004
|
-
["
|
|
5024
|
+
["这句话(标着「管理员」)和回复都会发进群。任务进行中也可以插话;确认模型读取后才算已加入。","The line (marked \"admin\") and its reply are posted to the group. You can send while a task is running; joined means the model has read it."],
|
|
5025
|
+
["该群正在停止、跳过或处理未结记录,请稍后再插话。","This group is stopping, skipping, or settling pending records. Try again shortly."],
|
|
5005
5026
|
["该群上一个任务结果不明,已暂停处理、等待人工确认;现在不能插话。","The outcome of this group's last task is unknown; the group is paused for operator review, so console messages are unavailable."],
|
|
5006
5027
|
["正在升级,现在不能插话,请一分钟后再试。","An upgrade is in progress; try the console message again in a minute."],
|
|
5007
5028
|
["这个群项目已暂停,先恢复再插话。","This group project is paused; resume it before sending a console message."],
|
|
@@ -5965,12 +5986,9 @@ function disableRuntimeControl(el,reason){
|
|
|
5965
5986
|
}
|
|
5966
5987
|
}
|
|
5967
5988
|
function applyRuntimeCapabilities(){
|
|
5968
|
-
const banner=document.getElementById('runtimeCapabilitiesNotice');
|
|
5969
|
-
if(banner)banner.hidden=!(S&&S.runtimeCapabilities);
|
|
5970
5989
|
if(!S||!S.runtimeCapabilities)return;
|
|
5971
|
-
if(banner){const body=banner.querySelector('[data-runtime-summary]'),text=trText(RUNTIME_UI_TEXT.summary);if(body&&body.textContent!==text)body.textContent=text;}
|
|
5972
5990
|
document.querySelectorAll(RUNTIME_DISABLED_SELECTORS).forEach(el=>disableRuntimeControl(el,RUNTIME_UI_TEXT.unavailable));
|
|
5973
|
-
// 插话照服务端白名单放行(
|
|
5991
|
+
// 插话照服务端白名单放行(忙时去向在浮层里按实时状态说明)
|
|
5974
5992
|
if(!runtimeAllows('POST','/api/groups/say'))document.querySelectorAll('[data-osay]').forEach(el=>disableRuntimeControl(el,RUNTIME_UI_TEXT.unavailable));
|
|
5975
5993
|
if(!runtimeAllows('POST','/api/providers/order'))document.querySelectorAll('#pgrid .drag').forEach(el=>{el.draggable=false;el.title=trText(RUNTIME_UI_TEXT.unavailable);});
|
|
5976
5994
|
}
|
|
@@ -6002,6 +6020,7 @@ function applyTurnEvent(a){
|
|
|
6002
6020
|
// reconnect drops, and rows the self-heal resolved to a bare '✓' without their real done
|
|
6003
6021
|
// data. Idempotent (id-keyed merge). Called on load + on every SSE (re)connect.
|
|
6004
6022
|
async function syncActivity(repaint){
|
|
6023
|
+
if(S?.runtimeCapabilities&&typeof activityPanel!=='undefined')return activityPanel.sync();
|
|
6005
6024
|
try{const d=await api('/api/activity'); if(d&&d.activity){d.activity.forEach(applyTurnEvent); if(repaint&&$('#overview').innerHTML)updateOverviewLive();}}catch(e){}
|
|
6006
6025
|
}
|
|
6007
6026
|
|
|
@@ -6012,10 +6031,11 @@ function startLive(){
|
|
|
6012
6031
|
if(esRef){try{esRef.close();}catch{}}
|
|
6013
6032
|
helloN=0;
|
|
6014
6033
|
const es=new EventSource('/api/events?token='+encodeURIComponent(TOKEN));esRef=es;
|
|
6034
|
+
es.addEventListener('activity-update',ev=>{try{activityPanel.receive(JSON.parse(ev.data));}catch{}});
|
|
6015
6035
|
// hello fires on first connect AND every native reconnect → replay the buffer to
|
|
6016
6036
|
// recover whatever SSE dropped while we were away (SSE has no replay of its own);
|
|
6017
6037
|
// 重连(非首连)还拉一次全量 state —— 断流期间的 live/conn 变化就地补齐,不整页重渲染。
|
|
6018
|
-
es.addEventListener('hello',()=>{$('#conn').classList.add('live');syncActivity(true);
|
|
6038
|
+
es.addEventListener('hello',()=>{$('#conn').classList.add('live');if(typeof activityPanel!=='undefined')activityPanel.connected(true);syncActivity(true);
|
|
6019
6039
|
if(++helloN>1)api('/api/state').then(r=>{S=r;updateDaemon();updateTopUpdateBadge();updateOverviewLive();updateConnLive();startClock();}).catch(()=>{});});
|
|
6020
6040
|
es.addEventListener('tick',ev=>{try{const d=JSON.parse(ev.data);if(S){if(d.live)S.live=d.live;if('conn' in d)S.conn=d.conn;}if(d.activity)d.activity.forEach(applyTurnEvent);updateDaemon();updateOverviewLive();updateConnLive();}catch{}});
|
|
6021
6041
|
startAutoRefresh();
|
|
@@ -6023,7 +6043,7 @@ function startLive(){
|
|
|
6023
6043
|
es.addEventListener('turn',ev=>{try{const d=JSON.parse(ev.data);applyTurnEvent(d);updateOverviewLive();
|
|
6024
6044
|
// 收件箱跟着轮次走:刚失败的要尽快出现,重说成功的要尽快消失 —— 不等 60 秒的例行刷新;只取这一块,不拉整份 state
|
|
6025
6045
|
if(d.kind==='done')refreshInbox();}catch(e){}});
|
|
6026
|
-
es.onerror=()=>{$('#conn').classList.remove('live');checkStaleAuth();};
|
|
6046
|
+
es.onerror=()=>{$('#conn').classList.remove('live');if(typeof activityPanel!=='undefined')activityPanel.connected(false);checkStaleAuth();};
|
|
6027
6047
|
}catch(e){}
|
|
6028
6048
|
}
|
|
6029
6049
|
/* SSE 断流自愈:daemon 重启会作废内存里的 token(页面 token / 登录 session 都是),
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/* Injected into the console's existing script; no framework or extra connection. */
|
|
2
|
+
const activityPanel = (() => {
|
|
3
|
+
let root, groupBody, listBody, taskBody, selected = '', count = 5
|
|
4
|
+
try { selected = sessionStorage.getItem('sintbot.activity.group') || '' } catch {}
|
|
5
|
+
let epoch = '', revision = -1, loaded = false, pending = null, frames = [], scheduled = false
|
|
6
|
+
const rows = new Map(), groups = new Map(), detailCache = new Map(), detailPending = new Set(), detailErrors = new Map(), open = new Set()
|
|
7
|
+
const groupNodes = new Map(), rowNodes = new Map(), actionPending = new Set()
|
|
8
|
+
let lang = '', error = false, connected = true, focusBeforeRender = null, groupScroll = 0
|
|
9
|
+
const key = r => r.chatId + ':' + r.id
|
|
10
|
+
const en = () => langOf() === 'en'
|
|
11
|
+
const tr = (zh, english) => en() ? english : zh
|
|
12
|
+
const labels = {
|
|
13
|
+
queued: ['排队', 'Queued'], running: ['运行中', 'Running'], sending: ['回复发送中', 'Sending reply'],
|
|
14
|
+
answered: ['已回复', 'Replied'], silent: ['无需回复', 'No reply needed'], stopped: ['已停止', 'Stopped'],
|
|
15
|
+
recalled: ['已撤回', 'Recalled'], timeout: ['已超时', 'Timed out'], error: ['失败', 'Failed'],
|
|
16
|
+
'not-started': ['未启动', 'Not started'], unknown: ['结果待核实', 'Unconfirmed'],
|
|
17
|
+
'已结账': ['历史记录已结账', 'Historical receipt settled'], '等待处理': ['等待处理', 'Waiting'],
|
|
18
|
+
}
|
|
19
|
+
const status = s => labels[s] ? tr(...labels[s]) : s
|
|
20
|
+
const active = r => ['queued', 'running', 'sending', '等待处理'].includes(r.status)
|
|
21
|
+
const num = n => n == null ? '—' : n >= 1000 ? (n / 1000).toFixed(1).replace(/\.0$/, '') + 'k' : String(n)
|
|
22
|
+
const stamp = ts => ts ? new Date(ts).toLocaleTimeString(en() ? 'en-GB' : 'zh-CN', { hour: '2-digit', minute: '2-digit', timeZone: S?.timezone || 'Asia/Shanghai' }) : '—'
|
|
23
|
+
const node = (tag, cls, text) => { const n = document.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n }
|
|
24
|
+
const text = (n, value) => { if (n.textContent !== value) n.textContent = value }
|
|
25
|
+
function schedule() {
|
|
26
|
+
if (scheduled || document.hidden) return
|
|
27
|
+
scheduled = true; requestAnimationFrame(() => { scheduled = false; paint() })
|
|
28
|
+
}
|
|
29
|
+
function setup() {
|
|
30
|
+
if (root) return
|
|
31
|
+
root = node('section', 'la'); root.setAttribute('data-i18n-skip', '')
|
|
32
|
+
const style = node('style'); style.textContent = `
|
|
33
|
+
.la{margin:20px 0}.la [hidden]{display:none!important}.la .la-panel{padding:0;overflow:hidden}
|
|
34
|
+
.la .la-head{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:20px 24px;border-bottom:1px solid var(--line)}
|
|
35
|
+
.la h2{font-size:15px;font-weight:600;letter-spacing:0;margin:0}.la .la-capacity{font-size:12px;color:var(--dim);font-variant-numeric:tabular-nums;text-align:right}
|
|
36
|
+
.la .la-select,.la .la-toggle{font:inherit;width:100%;border:0;border-radius:0;background:transparent;color:inherit;text-align:left;cursor:pointer;min-height:48px}
|
|
37
|
+
.la .la-group{position:relative;display:flex;align-items:center;border-bottom:1px solid var(--line);transition:background .18s ease}.la .la-select{flex:1;min-width:0;display:grid;grid-template-columns:minmax(160px,1.2fr) minmax(120px,1fr) minmax(160px,1fr);gap:20px;padding:15px 20px 15px 24px}.la .la-select>div{min-width:0}
|
|
38
|
+
.la .la-group:last-child{border-bottom:0}.la .la-group:before{content:'';position:absolute;inset:12px auto 12px 0;width:3px;border-radius:0 3px 3px 0;background:var(--ember);opacity:0;transform:scaleY(.4);transition:opacity .18s,transform .18s}
|
|
39
|
+
.la .la-select strong,.la .la-select small,.la .la-model>span,.la .la-context>span{display:block;white-space:normal;overflow-wrap:anywhere}.la .la-tags{display:flex;flex-wrap:wrap;gap:5px;margin-top:7px}.la .la-tags .tag{font-size:10px;padding:2px 6px}.la .la-tags [data-status=running]{color:var(--ember);border-color:color-mix(in srgb,var(--ember) 30%,var(--line))}.la .la-tags [data-status=attention]{color:var(--fail)}.la .la-path{font-family:var(--mono);font-size:10.5px}
|
|
40
|
+
.la .la-group:hover,.la .la-toggle:hover{background:color-mix(in srgb,var(--ember) 4%,transparent)}
|
|
41
|
+
.la .la-group.selected{background:color-mix(in srgb,var(--ember) 7%,transparent)}.la .la-group.selected:before{opacity:1;transform:scaleY(1)}
|
|
42
|
+
.la button:focus-visible{outline:2px solid var(--ember);outline-offset:-3px}.la button:active:not(:disabled){background:color-mix(in srgb,var(--ember) 11%,transparent)}
|
|
43
|
+
.la small{display:block;color:var(--dim);font-size:11px;line-height:1.5;margin-top:4px}.la strong{font-size:13px;font-weight:600}.la .la-model{font-size:12px}.la .la-model small[data-status=running]{color:var(--ember)}.la .la-model small[data-status=attention]{color:var(--fail)}.la .la-context{font-size:12px;font-variant-numeric:tabular-nums}
|
|
44
|
+
.la .la-track{height:3px;margin-top:8px;background:var(--line);overflow:hidden;border-radius:2px}.la .la-fill{height:100%;background:var(--ember);transform-origin:left;transition:transform .3s ease-out;transform:scaleX(0)}
|
|
45
|
+
.la .la-groups{max-height:560px;overflow:auto;scrollbar-width:thin;scrollbar-color:var(--line) transparent}
|
|
46
|
+
.la .la-task-head{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:22px 0 12px}.la .la-task-head strong{font-size:14px;overflow-wrap:anywhere}
|
|
47
|
+
.la .la-actions{display:flex;gap:6px;flex-shrink:0;padding-right:24px}.la .la-actions .iconbtn{padding:0;transition:background .15s,border-color .15s,transform .15s}.la .la-actions .iconbtn:active:not(:disabled){transform:scale(.92)}.la button:disabled{cursor:default;opacity:.45}
|
|
48
|
+
.la .la-list{padding:0;overflow:hidden}.la .la-row{border-bottom:1px solid var(--line)}.la .la-row:last-child{border-bottom:0}
|
|
49
|
+
.la .la-toggle{padding:16px 20px;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:16px;transition:background .15s}
|
|
50
|
+
.la .la-title{font-size:13px;line-height:1.6;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;margin-top:5px;overflow-wrap:anywhere}
|
|
51
|
+
.la .la-preview{font-size:12px;color:var(--dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-top:5px}
|
|
52
|
+
.la .la-state{font-size:11px;color:var(--dim);white-space:nowrap;padding-top:4px}.la .la-state:after{content:'';display:inline-block;width:5px;height:5px;border-right:1px solid currentColor;border-bottom:1px solid currentColor;transform:rotate(45deg);margin:0 2px 3px 14px;transition:transform .18s}
|
|
53
|
+
.la [aria-expanded=true] .la-state:after{transform:rotate(225deg)}.la .la-state[data-state=running],.la .la-state[data-state=sending]{color:var(--ember)}
|
|
54
|
+
.la .la-state[data-state=error],.la .la-state[data-state=unknown],.la .la-state[data-state=timeout]{color:var(--fail)}
|
|
55
|
+
.la .la-fold{display:grid;grid-template-rows:0fr;opacity:0;transition:grid-template-rows .2s ease,opacity .2s ease}.la .la-fold.open{grid-template-rows:1fr;opacity:1}
|
|
56
|
+
.la .la-fold-inner{min-height:0;overflow:hidden}.la .la-detail{padding:0 20px 18px;font-size:13px;line-height:1.7;overflow-wrap:anywhere}
|
|
57
|
+
.la .la-detail .aexp{margin:0;max-height:360px;white-space:normal}.la .la-detail .aereply{white-space:pre-wrap}.la .la-detail .aemeta{gap:6px 16px;white-space:normal}.la .la-detail .la-source{margin-top:10px;border-top:1px solid var(--line);padding-top:8px}.la .la-source summary{cursor:pointer;color:var(--dim);font-size:11px}.la .la-source summary:focus-visible{outline:2px solid var(--ember);outline-offset:3px}.la .la-detail .la-error{color:var(--fail)}.la .la-detail p{white-space:pre-wrap;margin:8px 0}.la .la-detail h3{font-size:11px;color:var(--dim);margin:16px 0 5px;font-weight:500}.la .la-notice{font-size:12px;color:var(--dim);margin:12px 0;line-height:1.6}
|
|
58
|
+
.la .la-more{margin:12px auto 0;display:block;min-height:40px;font-size:12px}
|
|
59
|
+
@media(max-width:640px){.la .la-head{padding:16px;gap:10px}.la .la-capacity{font-size:11px}.la .la-group{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:start}.la .la-select{grid-column:1/-1;grid-row:1;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:10px 16px;padding:14px 16px}.la .la-select>div:first-child{padding-right:94px;grid-column:1/-1}.la .la-actions{grid-column:2;grid-row:1;z-index:1;padding:14px 16px 0 0}.la .la-model{grid-column:1/-1}.la .la-context{grid-column:1/-1;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:0 8px}.la .la-context small{margin:0;font-size:10px}.la .la-track{grid-column:1/-1;margin-top:6px}.la .la-task-head{align-items:flex-start;flex-direction:column;gap:10px}.la .la-actions{flex-wrap:wrap}.la .la-toggle{grid-template-columns:minmax(0,1fr);gap:7px;padding:14px 16px}.la .la-state{white-space:normal;padding:0}.la .la-state:after{float:right}.la .la-detail{padding:0 16px 16px}}
|
|
60
|
+
@media(prefers-reduced-motion:reduce){.la *{transition:none!important;animation:none!important}}`
|
|
61
|
+
root.append(style)
|
|
62
|
+
const panel = node('div', 'card la-panel'), head = node('div', 'la-head')
|
|
63
|
+
head.append(node('h2'), node('span', 'la-capacity')); panel.append(head)
|
|
64
|
+
groupBody = node('div', 'la-groups'); panel.append(groupBody); root.append(panel)
|
|
65
|
+
const note = node('div', 'la-notice'); note.dataset.la = 'note'; note.setAttribute('role', 'status'); root.append(note)
|
|
66
|
+
taskBody = node('div', 'la-task-body'); taskBody.hidden = true
|
|
67
|
+
const bar = node('div', 'la-task-head'), title = node('strong'); title.dataset.la = 'activity'; bar.append(title)
|
|
68
|
+
taskBody.append(bar)
|
|
69
|
+
listBody = node('div', 'card la-list'); taskBody.append(listBody)
|
|
70
|
+
const more = node('button', 'btn la-more'); more.dataset.la = 'more'; more.onclick = () => { count += 5; schedule() }; taskBody.append(more)
|
|
71
|
+
root.append(taskBody)
|
|
72
|
+
}
|
|
73
|
+
async function act(action, button, chatId) {
|
|
74
|
+
if (!chatId || button.disabled) return
|
|
75
|
+
if (action === 'say') { openSay(chatId); return }
|
|
76
|
+
const actionKey = chatId + ':' + action
|
|
77
|
+
actionPending.add(actionKey); button.disabled = true
|
|
78
|
+
try {
|
|
79
|
+
const result = await api(action === 'stop' ? '/api/groups/stop' : '/api/groups/interrupt','POST',{chatId})
|
|
80
|
+
toast(action === 'new' ? tr('已开新会话','New session started') : result.ok ? tr('已请求停止,等待结束确认','Stop requested; waiting for confirmation') : tr('当前没有可停止的运行任务','No running task to stop'),'ok')
|
|
81
|
+
await sync()
|
|
82
|
+
} catch (e) { toast(e.message,'err') }
|
|
83
|
+
finally { actionPending.delete(actionKey); schedule() }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function groupNode(g) {
|
|
87
|
+
let n = groupNodes.get(g.chatId)
|
|
88
|
+
if (!n) {
|
|
89
|
+
n = node('div', 'la-group'); n.dataset.chatId = g.chatId
|
|
90
|
+
const select = node('button', 'la-select'); select.type = 'button'
|
|
91
|
+
const a = node('div'), b = node('div', 'la-model'), c = node('div', 'la-context')
|
|
92
|
+
const tags = node('div','la-tags'); tags.append(node('span','tag prov'),node('span','tag'))
|
|
93
|
+
a.append(node('strong'), tags, node('small','la-path')); b.append(node('span'), node('small'))
|
|
94
|
+
const track = node('div', 'la-track'); track.append(node('div', 'la-fill'))
|
|
95
|
+
c.append(node('span'), node('small'), track); select.append(a, b, c); n.append(select)
|
|
96
|
+
const actions = node('div','la-actions')
|
|
97
|
+
for (const [action,icon] of [['say','say'],['stop','square'],['new','rotate']]) {
|
|
98
|
+
const button = node('button','iconbtn sm'); button.type = 'button'; button.dataset.action = action
|
|
99
|
+
button.innerHTML = IC[icon]; button.onclick = () => void act(action,button,g.chatId); actions.append(button)
|
|
100
|
+
}
|
|
101
|
+
n.append(actions)
|
|
102
|
+
select.onclick = () => {
|
|
103
|
+
if (selected === g.chatId) return
|
|
104
|
+
selected = g.chatId; count = 5
|
|
105
|
+
try { sessionStorage.setItem('sintbot.activity.group', selected) } catch {}
|
|
106
|
+
schedule(); requestAnimationFrame(() => {
|
|
107
|
+
if (!matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
108
|
+
taskBody.getAnimations().forEach(animation => animation.cancel())
|
|
109
|
+
taskBody.animate([{opacity:0,transform:'translateY(4px)'},{opacity:1,transform:'translateY(0)'}],{duration:180,easing:'ease-out'})
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
groupNodes.set(g.chatId, n)
|
|
114
|
+
}
|
|
115
|
+
const [a,b,c] = n.children[0].children, ctx = g.context
|
|
116
|
+
text(a.children[0], g.name); text(a.children[2], g.workdir ? shortPath(g.workdir) : g.project)
|
|
117
|
+
a.children[2].title = g.workdir || g.project
|
|
118
|
+
text(a.children[1].children[0], g.runner === 'codex' ? 'codex' : 'claude')
|
|
119
|
+
const state = a.children[1].children[1]
|
|
120
|
+
text(state, g.held || g.queueFaulted ? tr('需要处理','Needs attention') : g.paused ? tr('已暂停','Paused') : g.finalizing ? tr('收尾中','Finishing') : g.waitingForCapacity ? tr('等待容量','Waiting for capacity') : g.busy ? 'busy' : 'idle')
|
|
121
|
+
state.dataset.status = g.held || g.queueFaulted ? 'attention' : g.busy ? 'running' : 'idle'
|
|
122
|
+
if (g.busy) text(state, state.textContent + ' ' + Math.floor((g.sinceMs || 0)/1000) + 's')
|
|
123
|
+
if (g.queue) text(state, state.textContent + ' · ' + tr('排队 ','Queued ') + g.queue)
|
|
124
|
+
text(b.children[0], g.model || tr('执行器默认(未回报实际型号)','Executor default (actual model unknown)'))
|
|
125
|
+
text(b.children[1], tr('思考深度 · ','Reasoning · ') + (g.reasoningEffort || tr('默认','Default')))
|
|
126
|
+
b.children[1].title = tr('当前模型配置;未指定时使用模型默认值','Current model configuration; unspecified uses the model default')
|
|
127
|
+
const blocked = !S?.live?.running || g.paused || g.held || g.queueFaulted
|
|
128
|
+
const busy = g.busy || g.pending > 0 || g.queue > 0 || g.waitingForCapacity || g.finalizing
|
|
129
|
+
for (const [action,zh,eng] of [['say','插话','Message'],['stop','停止任务','Stop task'],['new','新会话','New session']]) {
|
|
130
|
+
const button = n.querySelector('[data-action='+action+']')
|
|
131
|
+
button.disabled = actionPending.has(g.chatId + ':' + action) || blocked || (action === 'stop' ? !busy : action === 'new' ? !!busy : !!g.sayBlocked)
|
|
132
|
+
button.setAttribute('aria-label',tr(zh,eng) + ' · ' + g.name)
|
|
133
|
+
button.title = tr(zh,eng) + ' · ' + (button.disabled ? tr('当前状态不可操作','Unavailable in the current state') : action === 'say' ? tr('这句话和回复都会发进该群','Your message and reply will be sent to this group') : action === 'new' ? tr('开始新会话,清除当前会话上下文','Start a new session with fresh context') : tr('停止当前任务,保留会话','Stop the task and keep the session'))
|
|
134
|
+
}
|
|
135
|
+
const valid = ctx?.usedTokens != null, capacity = ctx?.windowTokens > 0
|
|
136
|
+
const percent = valid && capacity ? Math.round(ctx.usedTokens / ctx.windowTokens * 100) : null
|
|
137
|
+
text(c.children[0], valid ? `${num(ctx.usedTokens)}${capacity ? ' / ' + num(ctx.windowTokens) + ' · ' + percent + '%' : ' · ' + tr('容量未知','capacity unknown')}` : ctx?.quality === 'compacted' ? tr('压缩后待上报','Awaiting usage after compaction') : !g.sessionId && !g.busy ? tr('未开始','No session') : tr('待上报','Awaiting usage'))
|
|
138
|
+
const quality = ctx?.quality === 'reported' ? tr('上下文 · 最新上报','Context · Latest report') : ctx?.quality === 'historical' ? tr('上下文 · 历史采样','Context · Historical sample') : tr('上下文 · 上次上报','Context · Last report')
|
|
139
|
+
text(c.children[1], ctx ? quality + ' · ' + stamp(ctx.measuredAt) : tr('上下文','Context'))
|
|
140
|
+
c.children[2].children[0].style.transform = 'scaleX(' + (percent == null ? 0 : Math.max(0, Math.min(100, percent)) / 100) + ')'
|
|
141
|
+
n.title = g.name + ' · ' + g.project + ' · ' + g.model + ' · ' + g.chatId.slice(-6)
|
|
142
|
+
n.classList.toggle('selected',selected === g.chatId); n.children[0].setAttribute('aria-pressed', String(selected === g.chatId)); return n
|
|
143
|
+
}
|
|
144
|
+
async function showDetail(r, n) {
|
|
145
|
+
const k = key(r), body = n.querySelector('.la-detail')
|
|
146
|
+
if (!open.has(k)) return
|
|
147
|
+
if (!r.details) { text(body, tr('历史记录未保存消息和回复详情。','Message and reply details were not recorded.')); return }
|
|
148
|
+
if (detailErrors.get(k) === r.detailRevision) return
|
|
149
|
+
let d = detailCache.get(k)
|
|
150
|
+
if (!d || d.detailRevision !== r.detailRevision) {
|
|
151
|
+
if (detailPending.has(k)) return
|
|
152
|
+
detailPending.add(k)
|
|
153
|
+
text(body, tr('加载详情…','Loading details…'))
|
|
154
|
+
try {
|
|
155
|
+
const value = await api('/api/activity/detail?chatId=' + encodeURIComponent(r.chatId) + '&id=' + encodeURIComponent(r.id))
|
|
156
|
+
if (epoch !== value.epoch) return
|
|
157
|
+
d = { ...value, lang: langOf() }; detailCache.set(k, d)
|
|
158
|
+
while (detailCache.size > 40) detailCache.delete(detailCache.keys().next().value)
|
|
159
|
+
} catch { detailErrors.set(k,r.detailRevision);text(body, tr('详情暂不可用,收起后可重新打开。','Details unavailable. Close and reopen to retry.')); return }
|
|
160
|
+
finally { detailPending.delete(k) }
|
|
161
|
+
}
|
|
162
|
+
if (!open.has(k) || !n.isConnected) return
|
|
163
|
+
const sig = d.detailRevision + ':' + langOf()
|
|
164
|
+
if (body.dataset.rev === sig) return
|
|
165
|
+
body.dataset.rev = sig; body.replaceChildren()
|
|
166
|
+
const box = node('div','aexp'); body.append(box)
|
|
167
|
+
if (d.tools?.length) {
|
|
168
|
+
const tools = node('div','aetools')
|
|
169
|
+
for (const tool of d.tools) {
|
|
170
|
+
const chip = node('span','tchip'); chip.innerHTML = IC.wrench
|
|
171
|
+
chip.append(document.createTextNode(' ' + tool.name + (tool.count > 1 ? ' ×' + tool.count : ''))); tools.append(chip)
|
|
172
|
+
}
|
|
173
|
+
box.append(tools)
|
|
174
|
+
}
|
|
175
|
+
box.append(node('div',d.reply ? 'aereply' : 'faint',d.reply || (active(d) ? tr('任务进行中,等待回复…','Task in progress; awaiting reply…') : tr('(无回复内容)','(No reply content)'))))
|
|
176
|
+
if (d.err) box.append(node('p','la-error',d.err))
|
|
177
|
+
const meta = node('div','aemeta')
|
|
178
|
+
function metric(label,value) { const item = node('span'); item.append(node('b','',label+' '),document.createTextNode(value)); meta.append(item) }
|
|
179
|
+
if (d.sender) metric(tr('发送者','Sender'),d.sender)
|
|
180
|
+
metric(tr('状态','Status'),status(d.status))
|
|
181
|
+
if (d.startedAt) metric(tr('排队','Queued'),(Math.max(0,d.startedAt-d.ts)/1000).toFixed(1)+'s')
|
|
182
|
+
if (d.ms != null) metric(tr('耗时','Duration'),(d.ms/1000).toFixed(1)+'s')
|
|
183
|
+
if (d.tokens) {
|
|
184
|
+
const cr=d.tokens.cr||0,cw=d.tokens.cw||0
|
|
185
|
+
metric('tokens',`↑${num((d.tokens.in||0)+cr+cw)} ↓${num(d.tokens.out||0)}` + (cr||cw ? ' · '+tr('缓存读 ','Cache read ')+num(cr)+tr(' / 写 ',' / write ')+num(cw) : ''))
|
|
186
|
+
}
|
|
187
|
+
box.append(meta)
|
|
188
|
+
if (d.messages?.length || d.milestones?.length) {
|
|
189
|
+
const source = node('details','la-source'); source.append(node('summary','',tr('请求、补充与阶段进度','Request, follow-ups and progress')))
|
|
190
|
+
for (const message of d.messages || []) source.append(node('p','',(message.sender || '—')+' · '+(message.state==='landed'?tr('已读到','Read'):tr('已受理','Accepted'))+'\n'+message.text))
|
|
191
|
+
for (const milestone of d.milestones || []) source.append(node('p','',milestone))
|
|
192
|
+
box.append(source)
|
|
193
|
+
}
|
|
194
|
+
if (d.truncated) box.append(node('p','faint',tr('内容较长,记录已截断。','Long content was truncated in this record.')))
|
|
195
|
+
|
|
196
|
+
}
|
|
197
|
+
function rowNode(r) {
|
|
198
|
+
const k = key(r); let n = rowNodes.get(k)
|
|
199
|
+
if (!n) {
|
|
200
|
+
n = node('article', 'la-row'); const button = node('button','la-toggle'); button.type = 'button'
|
|
201
|
+
const lead = node('div'); lead.append(node('small'),node('div','la-title'),node('div','la-preview'))
|
|
202
|
+
button.append(lead,node('span','la-state')); n.append(button)
|
|
203
|
+
const fold = node('div','la-fold'), inner = node('div','la-fold-inner'), detail = node('div','la-detail')
|
|
204
|
+
fold.id = 'la-detail-' + rowNodes.size + '-' + Math.random().toString(36).slice(2,8)
|
|
205
|
+
inner.append(detail); fold.append(inner); n.append(fold)
|
|
206
|
+
button.setAttribute('aria-controls', fold.id)
|
|
207
|
+
button.onclick = () => { open.has(k) ? open.delete(k) : open.add(k);detailErrors.delete(k);schedule() }
|
|
208
|
+
rowNodes.set(k,n)
|
|
209
|
+
}
|
|
210
|
+
const b = n.children[0], lead = b.children[0], group = groups.get(r.chatId)
|
|
211
|
+
text(lead.children[0], stamp(r.ts) + ' · ' + (r.sender || '—'))
|
|
212
|
+
text(lead.children[1], r.text || tr('内容未记录','Content not recorded'))
|
|
213
|
+
text(lead.children[2], active(r) ? (r.doing || []).join(' · ') : r.preview || '')
|
|
214
|
+
text(b.children[1], status(r.status) + (r.messageCount > 1 ? ' · ' + tr('消息 ','messages ') + r.messageCount : '') + (r.ms ? ' · ' + (r.ms/1000).toFixed(1)+'s' : ''))
|
|
215
|
+
b.children[1].dataset.state = r.status; b.setAttribute('aria-expanded', String(open.has(k)))
|
|
216
|
+
n.children[1].classList.toggle('open', open.has(k)); n.children[1].inert = !open.has(k)
|
|
217
|
+
if (open.has(k)) void showDetail(r,n)
|
|
218
|
+
return n
|
|
219
|
+
}
|
|
220
|
+
function reconcile(container, nodes) {
|
|
221
|
+
const wanted = new Set(nodes)
|
|
222
|
+
for (const child of [...container.children]) if (!wanted.has(child)) child.remove()
|
|
223
|
+
nodes.forEach((n,i) => { if (container.children[i] !== n) container.insertBefore(n,container.children[i] || null) })
|
|
224
|
+
}
|
|
225
|
+
function paint() {
|
|
226
|
+
if (!root?.isConnected) return
|
|
227
|
+
lang = langOf()
|
|
228
|
+
text(root.querySelector('h2'),tr('群 · 项目','Groups · Projects'))
|
|
229
|
+
const concurrency = S?.live?.concurrency, capacity = concurrency?.max ?? S?.agent?.maxConcurrent ?? 0
|
|
230
|
+
text(root.querySelector('.la-capacity'), tr('运行 ','Running ') + (concurrency?.active ?? [...groups.values()].filter(g=>g.busy).length) + (capacity > 0 ? ' / ' + capacity : '') + ' · ' + tr('排队 ','Queued ') + (S?.live?.queue || 0))
|
|
231
|
+
if (loaded && selected && !groups.has(selected)) { selected = ''; try { sessionStorage.removeItem('sintbot.activity.group') } catch {} }
|
|
232
|
+
reconcile(groupBody, groups.size ? [...groups.values()].map(groupNode) : [node('div','empty',loaded ? tr('暂无群项目','No projects') : tr('正在同步…','Syncing…'))])
|
|
233
|
+
const group = groups.get(selected)
|
|
234
|
+
taskBody.hidden = !group
|
|
235
|
+
text(root.querySelector('[data-la=activity]'), (group?.name || '') + ' · ' + tr('任务活动','Task activity'))
|
|
236
|
+
const all = [...rows.values()].filter(r => selected && r.chatId===selected)
|
|
237
|
+
.sort((a,b) => Number(!active(a))-Number(!active(b)) || b.ts-a.ts)
|
|
238
|
+
const shown = all.filter(active).concat(all.filter(r=>!active(r)).slice(0,count))
|
|
239
|
+
reconcile(listBody, shown.length ? shown.map(rowNode) : [node('div','empty',loaded ? tr('该群暂无任务活动','No task activity for this group') : tr('正在同步…','Syncing…'))])
|
|
240
|
+
const more = root.querySelector('[data-la=more]'); more.hidden=shown.length>=all.length;text(more,tr('查看更早的任务','Show earlier tasks'))
|
|
241
|
+
const note = root.querySelector('[data-la=note]')
|
|
242
|
+
text(note, !connected ? tr('连接中断,当前为最后收到的记录','Disconnected — showing last received records') : error ? tr('部分活动记录暂不可用','Some activity records are unavailable') : !selected ? tr('选择一个群项目,查看任务活动','Select a project to view its tasks') : '')
|
|
243
|
+
note.hidden = !note.textContent
|
|
244
|
+
for (const id of groupNodes.keys()) if (!groups.has(id)) groupNodes.delete(id)
|
|
245
|
+
for (const k of rowNodes.keys()) if (!rows.has(k)) { rowNodes.delete(k); open.delete(k); detailCache.delete(k);detailErrors.delete(k) }
|
|
246
|
+
}
|
|
247
|
+
function apply(data, full) {
|
|
248
|
+
if (!data?.epoch) return
|
|
249
|
+
if (loaded && data.epoch===epoch && data.revision<revision) return
|
|
250
|
+
if (full || data.epoch!==epoch) { rows.clear(); groups.clear() }
|
|
251
|
+
if (data.epoch!==epoch) { detailCache.clear(); open.clear(); revision=-1 }
|
|
252
|
+
epoch=data.epoch;revision=data.revision;error=!!data.storageError
|
|
253
|
+
for (const r of data.activity || []) rows.set(key(r),r)
|
|
254
|
+
for (const g of data.groups || []) groups.set(g.chatId,g)
|
|
255
|
+
for (const k of data.removed || []) rows.delete(k)
|
|
256
|
+
loaded=true;schedule()
|
|
257
|
+
}
|
|
258
|
+
function sync() {
|
|
259
|
+
if (pending) return pending
|
|
260
|
+
frames=[]
|
|
261
|
+
pending=api('/api/activity?limit=100').then(data=>{
|
|
262
|
+
apply(data,true); for(const frame of frames)apply(frame,false)
|
|
263
|
+
}).catch(()=>{error=true;schedule()}).finally(()=>{pending=null;frames=[]})
|
|
264
|
+
return pending
|
|
265
|
+
}
|
|
266
|
+
document.addEventListener('visibilitychange',()=>{if(!document.hidden){schedule();void sync()}})
|
|
267
|
+
new MutationObserver(schedule).observe(document.documentElement,{attributes:true,attributeFilter:['data-lang']})
|
|
268
|
+
return {
|
|
269
|
+
preserve(){focusBeforeRender=root?.contains(document.activeElement)?document.activeElement:null;groupScroll=groupBody?.scrollTop||0},
|
|
270
|
+
mount(){setup();const target=document.getElementById('liveActivity');if(target&&root.parentNode!==target)target.append(root);groupBody.scrollTop=groupScroll;if(focusBeforeRender?.isConnected)focusBeforeRender.focus({preventScroll:true});focusBeforeRender=null;schedule();void sync()},
|
|
271
|
+
sync,
|
|
272
|
+
receive(data){if(pending){frames.push(data);if(frames.length>200)frames.shift()}else apply(data,false)},
|
|
273
|
+
live(live){for(const [id,value]of Object.entries(live?.groups||{})){const g=groups.get(id);if(g)Object.assign(g,value)}schedule()},
|
|
274
|
+
connected(value){connected=value;schedule()},
|
|
275
|
+
}
|
|
276
|
+
})()
|