dsh-session-conductor 0.0.0-stage → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +165 -2
- package/cordis.patch.yml +48 -0
- package/lib/apply.js +1361 -0
- package/lib/client-parts/apply.js +43 -0
- package/lib/client-parts/components/compaction.js +124 -0
- package/lib/client-parts/components/conductor-settings.js +62 -0
- package/lib/client-parts/components/group.js +115 -0
- package/lib/client-parts/components/panel.js +1265 -0
- package/lib/client-parts/components/processing.js +98 -0
- package/lib/client-parts/components/session-icon.js +20 -0
- package/lib/client-parts/components/settings.js +415 -0
- package/lib/client-parts/foundation/bootstrap.js +48 -0
- package/lib/client-parts/foundation/i18n.js +74 -0
- package/lib/client-parts/foundation/list-cache.js +28 -0
- package/lib/client-parts/foundation/styles.js +236 -0
- package/lib/client.js +2534 -0
- package/lib/core/config.js +159 -0
- package/lib/core/domain.js +157 -0
- package/lib/core/lazy.js +20 -0
- package/lib/core/state.js +106 -0
- package/lib/core.js +82 -0
- package/lib/eio-repair.js +204 -0
- package/lib/features/context-usage/index.js +219 -0
- package/lib/features/continue/eligibility.js +49 -0
- package/lib/features/continue/scan.js +134 -0
- package/lib/features/continue/session.js +235 -0
- package/lib/features/inject/preset.js +20 -0
- package/lib/features/member-model/index.js +14 -0
- package/lib/features/member-model/locate.js +157 -0
- package/lib/features/member-model/override.js +76 -0
- package/lib/features/member-model/setup.js +55 -0
- package/lib/features/member-model/switch.js +186 -0
- package/lib/features/rename/analysis.js +303 -0
- package/lib/features/rename/title.js +158 -0
- package/lib/features/rename/value.js +77 -0
- package/lib/features/search.js +119 -0
- package/lib/features/session-ops-routes.js +218 -0
- package/lib/features/session-ops.js +414 -0
- package/lib/group.js +303 -0
- package/lib/guards.js +28 -0
- package/lib/http.js +24 -0
- package/lib/i18n/en.json +204 -0
- package/lib/i18n/zh.json +204 -0
- package/lib/index.js +78 -0
- package/lib/repair.js +398 -0
- package/lib/seq-gap-repair.js +224 -0
- package/lib/session-codec.js +37 -0
- package/lib/session-log.js +46 -0
- package/lib/sessions/detach.js +113 -0
- package/lib/sessions/events.js +73 -0
- package/lib/sessions/interruption.js +73 -0
- package/lib/sessions/list-cache.js +175 -0
- package/lib/sessions/list.js +187 -0
- package/lib/sessions/route.js +60 -0
- package/lib/sessions/turn.js +21 -0
- package/lib/shared/constants.js +20 -0
- package/lib/shared/log.js +17 -0
- package/lib/shared/settings.js +14 -0
- package/lib/shared/test-hooks.js +93 -0
- package/lib/shared/util.js +6 -0
- package/lib/template-inject.js +289 -0
- package/lib/value.js +430 -0
- package/lib/zstd-frames.js +177 -0
- package/package.json +65 -3
- package/skills/dsh-session-conductor-functions.md +197 -0
- package/skills/dsh-session-conductor.md +151 -0
- package/skills/dsh-session-official-doc.md +245 -0
- package/skills/task-completion-report.md +56 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,2534 @@
|
|
|
1
|
+
// dsh-session-conductor —— 会话管理增强插件(浏览器半边)。
|
|
2
|
+
// dsh-skip-quality(conv-log:fallback 行为 UI 文案内嵌 JSON,非文档注释,属规则误报)
|
|
3
|
+
//
|
|
4
|
+
// 侧边栏底部(设置上方)新增「会话管理」入口,点击展开浮层面板:
|
|
5
|
+
// · 全部会话:列出每个会话(标题/目录/时间/状态),可归档、删除
|
|
6
|
+
// · 已归档:列出被归档的会话,可恢复(取消归档)、删除
|
|
7
|
+
// · 说明:用大白话解释内置菜单 重命名 / 分叉会话 / 归档会话 的意思
|
|
8
|
+
// 数据源为宿主路由 /api/session-conductor/list,操作后自动刷新。
|
|
9
|
+
// 纯内联样式 + 主题 CSS 变量(--dsw-*),不依赖任何哈希类名。
|
|
10
|
+
|
|
11
|
+
// —— 语言包外置(lib/i18n/*.json,语言包外置做法)——
|
|
12
|
+
// 浏览器端 ModuleLoader 的 require 不支持相对路径 JSON,字典由宿主侧读外置 JSON 经
|
|
13
|
+
// GET /api/session-conductor/i18n 暴露、客户端 fetch 拉取合并;本处仅提供共享容器,
|
|
14
|
+
// 各 NS 块的 zh/en 用 Object.assign 原地填充(引用不变 → locale.register 后内容自动生效)。
|
|
15
|
+
window.__SC_FALLBACK__ = { zh: {"sessionConductor":{"settings.nav":"会话管理","settings.title":"会话管理","settings.desc":"模板注入(方案 / 收尾)设置:三种导入方式(本地设备上传 / 在线网址 / DSH 目录浏览),导入内容可再编辑。同为设置左侧栏独立页。","trigger.label":"会话管理","panel.title":"会话管理","panel.subtitle":"归档可恢复 · 支持删除 · 中断自动续跑","tab.all":"全部会话","tab.archived":"已归档","tab.active":"活跃","tab.help":"说明","badge.archived":"已归档","badge.running":"运行中","badge.live":"进行中","badge.auto":"自动","badge.interrupted":"已中断","help.repair.desc":"会话日志可能因历史版本的工具输出格式而损坏(history unavailable)。此功能扫描全部冷会话、用与 DSH 相同的校验规则检出问题并自动修复,可安全重复执行。","help.repairEio.desc":"底层存储(如 BTRFS csum 损坏 / iSCSI LUN I/O 错误)会导致会话日志某些 4KB 块读取抛 EIO,网关报「EIO: i/o error, read」。此功能块级探测坏块边界、截断到完好数据(COW 换新块),由 DSH 自动收尾 torn tail。可安全重复执行。","help.repairDual.desc":"会话目录同时存在 session.jsonl(明文)与 session.jsonl.zstd(压缩)时,DSH 官方 listArtifacts() 会抛 encodingMismatch 导致整个会话列表失败(侧边栏会话全消失)。此功能纯磁盘级扫描(旁路 persistence),把多余明文移入 .dual-format-backup/ 保留 zstd 官方格式,可安全重复执行。","confirm.releaseAll.title":"确定要释放全部空闲会话吗?","confirm.delete.title":"确定要永久删除会话「{title}」吗?","confirm.undo.title":"撤回最后一条消息?","help.undo.title":"撤回最后一条消息(本插件新增)","help.undo.desc":"把会话最后一条用户消息(连同它触发的整轮回复)从会话记录中移除,等效消息撤回。直接操作会话日志文件(session.jsonl.zstd),删除前二次确认、先预览再执行,原文件备份在 .undo-backup/ 可找回。运行中的会话不能撤回(等回合结束再试)。","help.delete.title":"删除会话(本插件新增)","help.delete.desc":"内置界面没有任何删除会话的入口。本插件补上了:拒绝删除运行中的会话,删除前二次确认,删除后日志一并清除、不可恢复。","help.autoRename.title":"自动重命名(本插件新增)","help.autoRename.desc":"给会话勾选「自动重命名」后,每个回合结束,插件会用 AI 分析最近会话内容;发现主题/方向已明显偏离当前标题时,自动生成新标题并改掉。标题会带上实时状态后缀:「(运行中)」= 正在执行回合、「(已中断)」= 崩溃/错误中断待续跑;回合结束自动更新。分析带 5 分钟最小间隔和新增消息数门槛,避免频繁调用浪费额度。","help.autoContinue.title":"自动续跑(本插件新增)","help.autoContinue.desc":"勾选「自动续跑」后,插件会读取会话记录:当最后一次回合属于「非人为中断」(进程崩溃、本轮运行失败——任意请求错误如限流/超时/服务端/鉴权/上下文超限等、系统原因中止)时,自动让 AI 继续完成它原本的任务。⚠️ 自动续跑默认关闭,需在每条会话的「⋯」菜单里逐条开启;开启后,若识别到本轮运行失败(error),会等待 30 秒再自动续跑,避免立即重试。⚠️ 该开关是临时设置:只保存在当前运行内存里,DSH 重启后回到默认关闭、不写入磁盘;需要长期开启请用配置项 defaultAutoContinue。用户手动点「停止」、暂停目标等不算非人为中断,不会自动续跑。防护:同会话冷却 15 分钟、每会话最多自动续 3 次、全局同时续跑不超过 2 个、活跃会话总数达到上限(默认 12)时暂停自动续跑——防止死循环和资源堆积。也可以随时手动点「继续」立即续跑任意被中断的会话。","help.release.title":"置为不活跃(本插件新增)","help.release.desc":"DSH 里会话一旦被打开就会一直挂在内存(活跃数只增不减)。本插件的「置为不活跃」/「释放全部空闲」能把空闲会话的 agent 拆下来、回到磁盘上的冷状态:会话记录完整保留,会话仍在列表里,重新打开或发消息会自动恢复挂载。运行中的会话不会被释放。","help.grouped.title":"全部会话按工作区分组","help.grouped.desc":"「全部会话」视图按会话工作目录(cwd)的最深祖先分组分节展示:分组标题带会话数、可点击折叠/展开,0 会话分组不显示,未匹配的归「未分组」。","help.newSession.title":"新建会话默认工作区","help.newSession.desc":"新建会话不再被屏蔽:未指定分组时默认落在 .dsh 上一层 workspace 文件夹工作区(不存在自动创建)。","help.value.title":"会话价值分析","help.value.desc":"会话太多时按价值分类:已完成(最后回复含 ✅ 任务完成/已解答 + ═ 分隔线)、未完成任务(⚠️ 未完成/❌ 失败标记或会话中断)、久未活跃(超过 3 天无活动)、活跃中——每项附最后回复摘要。API:POST /api/session-conductor/value-analysis。","deleteBatch.confirm.title":"确定要永久删除选中的 {n} 个会话吗?","deleteByRule.label.archivedOnly":"仅已归档会话","deleteByRule.label.inactiveDays":"超过 {n} 天未活跃(0=不限)","deleteByRule.label.cwdPrefix":"目录前缀(可选,限定分组)","deleteByRule.label.lowValue":"仅低价值会话(价值分析判定,灰尘/重复可删)","deleteByRule.label.inactiveDays.pre":"超过 ","deleteByRule.label.inactiveDays.post":" 天未活跃(0=不限)"},"sessionGroup":{"title":"会话分组","desc":"按工作区分组显示会话;可「在分组下新建会话」(不带分组 = 上次会话工作区)。分组创建/管理用 DSH 官方 workspace 机制。"},"sessionConductorCompaction":{"title":"压缩模型","desc":"上下文自动压缩用的模型:选便宜模型(如 agnes)可省 pro 成本;默认跟随会话模型。"},"sessionConductorSettings":{"title":"会话管理","desc":"会话管理插件的集中设置页:额外注入 md(本地 md/在线网址/工作区目录内选,内容常驻会话上下文)。","mdTitle":"额外注入系统提示词","noteTitle":"其他设置","noteBody":"压缩模型 等仍在 设置 → 插件 → 插件配置 对应卡片;侧边栏底部「会话管理」按钮是会话操作面板(归档/删除/自动续跑)。","scopeHint":"改动即时保存;注入在下一回合生效(无需重启)。","tpl.importFail":"导入失败:","tpl.dirFail":"目录读取失败:","tpl.enforceDone":"强制门禁已切换","tpl.saveFail":"保存失败:","tpl.modeLocal":"本地文件(从设备选择上传)","tpl.modeUrl":"在线 md 网址","tpl.modeDir":"DSH 目录浏览","tpl.injecting":"注入中 ✓","tpl.injectOff":"开启注入","tpl.enforceOn":"强制门禁开 ✓","tpl.enforceOff":"强制门禁关","tpl.import":"导入","tpl.emptyHint":"(未导入内容。导入后显示在下方,可再次编辑)","tpl.saveEdit":"保存编辑内容","tpl.clear":"清空","tpl.current":"|当前:{name}","tpl.title":"模板注入(方案 / 收尾)","tpl.desc":"两个固定模板槽位:方案模板(修改代码前先出提案)、收尾模板(任务完成汇报格式)。三种导入方式统一为一个入口:下拉选择导入方式,点「导入」后弹出对应悬浮窗(本地文件 = 从设备选择上传 / 在线网址 = 网址输入 / DSH 目录 = 目录浏览),转存正文;导入内容显示在下方编辑框(动态高度、自动换行),可再次编辑并保存。开启后注入系统提示词,AI 一开始就看见。单个 ≤256KB。","tpl.planTitle":"📋 方案模板","tpl.planDesc":"代码修改前按此模板出方案提案(含目标/文件/影响/风险/实施/验证)。","tpl.closingTitle":"✅ 收尾模板","tpl.closingDesc":"任务完成汇报格式(分隔线 + 完成标记 + 交付/验证/遗留)。","tpl.loading":"加载中…","tpl.pickerUrl":"导入在线 md 网址({slot})","tpl.pickerDir":"在 DSH 目录内选 md({slot})","tpl.pickerPath":"路径选择({slot})","tpl.urlPlaceholder":"https://…(在线 md 文件网址)","tpl.noPath":"(未选择路径)","tpl.upDir":"⬆ 上级目录","tpl.emptyDir":"(空目录)","tpl.dirHint":"点目录进入;点 .md 文件选中,再点「导入」转存(只读 DSH 主目录树内)。","tpl.importSelected":"导入选中文件","tpl.imported":"模板「{slot}」已导入","tpl.toggled":"模板「{slot}」已切换","tpl.updated":"模板「{slot}」内容已更新","tpl.removed":"模板「{slot}」已清空","tpl.planShort":"方案模板","tpl.closingShort":"收尾模板","saved":"已保存","saveFail":"保存失败","armTitle":"自动重命名模型","armDesc":"自动重命名(每条真人消息后 AI 分析会话主题精炼标题)使用的模型。默认跟随会话所用模型;可指定便宜/专用模型,选择立即保存、下次分析生效。","armFollow":"跟随会话模型(默认)","armProvider":"(选择 Provider)","armModel":"(选择模型)","armSet":"设为自动重命名模型","armCurFollow":"当前:跟随会话模型","armCurNone":"当前:未选择","renderFail":"设置页渲染失败:"}}, en: {"sessionConductor":{"settings.nav":"Session Conductor","settings.title":"Session Conductor","settings.desc":"Template injection (plan / closing) settings: three import methods (pick from device / online URL / DSH directory browse), imported content stays editable. A Settings sidebar page.","trigger.label":"Sessions","panel.title":"Session Conductor","panel.subtitle":"Unarchive · Delete · Auto-resume interrupted","tab.all":"All sessions","tab.archived":"Archived","tab.active":"Active","tab.help":"Help","badge.archived":"Archived","badge.running":"Running","badge.live":"Active","badge.auto":"Auto","badge.interrupted":"Interrupted","help.repair.desc":"Session logs can be corrupted by an old tool output format (history unavailable). This scans all cold sessions, detects problems with DSH's own validation rules, and auto-repairs. Safe to re-run.","help.repairEio.desc":"Underlying storage corruption (e.g. BTRFS csum failure / iSCSI LUN I/O error) makes some 4KB blocks of a session log throw EIO on read; the gateway reports \"EIO: i/o error, read\". This scans block-level health, truncates to intact data (COW new block), and lets DSH auto-finish the torn tail. Safe to re-run.","help.repairDual.desc":"When a session dir holds both session.jsonl (plain) and session.jsonl.zstd (compressed), DSH's official listArtifacts() throws encodingMismatch and the entire session list fails (sessions vanish from the sidebar). This scans at disk level (bypassing persistence) and moves the extra plaintext into .dual-format-backup/ keeping the zstd official format. Safe to re-run.","confirm.releaseAll.title":"Release all idle sessions?","confirm.delete.title":"Permanently delete session \"{title}\"?","confirm.undo.title":"Undo the last message?","help.undo.title":"Undo last message (plugin feature)","help.undo.desc":"Removes the last user message (and its reply turn) from the session record, like a message recall. It edits session.jsonl.zstd directly, with preview + confirmation before applying, and backs up the original file to .undo-backup/.","help.delete.title":"Delete session (added by this plugin)","help.delete.desc":"The built-in UI has no way to delete a session. This plugin adds it: refuses to delete a running session, double-checks before deleting, removes the log for good.","help.autoRename.title":"Auto-rename (added by this plugin)","help.autoRename.desc":"Turn on \"Auto-rename\" for a session and after each finished turn the plugin asks an AI to compare recent session content against the current title; when the topic has clearly drifted it generates a new title and renames automatically. Titles carry a live state suffix: \"(运行中)\" = turn in progress, \"(已中断)\" = crashed/errored and waiting to resume; updated at turn boundaries. Rate-limited (5 min interval, needs a few new messages) to keep token costs low.","help.autoContinue.title":"Auto-resume (added by this plugin)","help.autoContinue.desc":"With \"Auto-resume\" checked, the plugin reads session records: when the last turn ended in a non-human interruption (process crash, a failed turn — any request error like rate limit/timeout/server/auth/context-length, or a system abort) it automatically asks the AI to continue the original task. ⚠️ Auto-resume is OFF by default : enable it per session from the \"⋯\" menu; once enabled, a detected failed turn (error) waits 30 seconds before auto-resume, to avoid immediate retry. ⚠️ This switch is TEMPORARY : it lives only in the running process memory and resets to the default (off) when DSH restarts — it is never written to disk; use the defaultAutoContinue config for a lasting default. Manual stops and goal pauses are NOT auto-resumed. Guardrails: 15 min cooldown per session, max 3 auto-resumes per session, max 2 concurrent resumes, and auto-resume pauses when the number of attached sessions reaches the cap (default 12) — no infinite loops, no resource buildup. You can always hit \"Resume\" manually to continue any interrupted session.","help.release.title":"Set inactive (added by this plugin)","help.release.desc":"In DSH a session stays in memory once opened (the attached count only grows). \"Set inactive\" / \"Release all idle\" detaches the agent of an idle session back to cold persisted state: the transcript is fully kept, the session stays listed, and reopening or messaging re-attaches it automatically. Running sessions are never released.","help.grouped.title":"All sessions grouped by workspace ","help.grouped.desc":"The \"All sessions\" view is sectioned by the deepest ancestor group of each session cwd: group headers show counts and collapse, 0-session groups are hidden, unmatched go to \"Ungrouped\".","help.newSession.title":"New session defaults to workspace ","help.newSession.desc":"New sessions are no longer blocked: without a workspaceId they land in the workspace folder one level above .dsh (created automatically if missing).","help.value.title":"Session value analysis ","help.value.desc":"Classify sessions by value: completed (final reply has ✅ done + ═ separator), unfinished (⚠️/❌ marks or interruption), stale (>3 days idle), active — each with a final-reply summary. API: POST /api/session-conductor/value-analysis.","deleteBatch.confirm.title":"Permanently delete the {n} selected sessions?","deleteByRule.label.archivedOnly":"Archived only","deleteByRule.label.inactiveDays":"Inactive for more than {n} days (0 = any)","deleteByRule.label.cwdPrefix":"cwd prefix (optional, group scope)","deleteByRule.label.lowValue":"Low-value only (value analysis; dust/duplicates)","deleteByRule.label.inactiveDays.pre":"Inactive for more than ","deleteByRule.label.inactiveDays.post":" days (0 = any)"},"sessionGroup":{"title":"Session Groups","desc":"Sessions are shown grouped by workspace; \"New session in group\" creates one (no workspaceId = last session's workspace). Group creation/management uses DSH's native workspace mechanism."},"sessionConductorCompaction":{"title":"Compaction Model","desc":"Model used for automatic context compaction: choose a cheap model (e.g. agnes) to save pro cost; default follows the session model."},"sessionConductorSettings":{"title":"Session Conductor","desc":"Central settings: extra injected md (local file or online URL, persistent in session context).","mdTitle":"Extra injected md (context)","noteTitle":"Other settings","noteBody":"Session grouping / migration / compaction model stay under Settings → Plugins → Plugin config cards; the sidebar-bottom \"Session Conductor\" button is the session operations panel.","scopeHint":"Changes save immediately; injection applies from the next turn (no restart).","tpl.importFail":"Import failed: ","tpl.dirFail":"Directory read failed: ","tpl.enforceDone":"Enforce gate toggled","tpl.saveFail":"Save failed: ","tpl.modeLocal":"Local file (pick from device)","tpl.modeUrl":"Online md URL","tpl.modeDir":"DSH directory browse","tpl.injecting":"Injecting ✓","tpl.injectOff":"Enable injection","tpl.enforceOn":"Enforce on ✓","tpl.enforceOff":"Enforce off","tpl.import":"Import","tpl.emptyHint":"(No content yet. Imported content shows below and stays editable)","tpl.saveEdit":"Save edited content","tpl.clear":"Clear","tpl.current":"|Current: {name}","tpl.title":"Template injection (plan / closing)","tpl.desc":"Two fixed template slots: plan template (draft a proposal before changing code) and closing template (task completion report format). One unified entry for the three import methods: pick a method from the dropdown, click Import, then the matching picker opens (local file = pick from device / online URL = URL input / DSH dir = directory browser) and stores the content; imported content shows in the editable box below (auto-height, wraps) and can be re-edited and saved. When enabled, content is injected into the system prompt so the AI sees it from the start. ≤256KB each.","tpl.planTitle":"📋 Plan template","tpl.planDesc":"Draft a plan proposal per this template before changing code (goal/files/impact/risk/steps/verify).","tpl.closingTitle":"✅ Closing template","tpl.closingDesc":"Task completion report format (divider + done marker + delivered/verified/remaining).","tpl.loading":"Loading…","tpl.pickerUrl":"Import online md URL ({slot})","tpl.pickerDir":"Pick md in DSH directory ({slot})","tpl.pickerPath":"Path picker ({slot})","tpl.urlPlaceholder":"https://… (online md file URL)","tpl.noPath":"(no path selected)","tpl.upDir":"⬆ Parent dir","tpl.emptyDir":"(empty dir)","tpl.dirHint":"Click a directory to enter; click a .md file to select it, then click Import to store (read-only DSH home tree).","tpl.importSelected":"Import selected file","tpl.imported":"Template \"{slot}\" imported","tpl.toggled":"Template \"{slot}\" toggled","tpl.updated":"Template \"{slot}\" content updated","tpl.removed":"Template \"{slot}\" cleared","tpl.planShort":"Plan template","tpl.closingShort":"Closing template","saved":"Saved","saveFail":"Save failed","armTitle":"Auto-rename model","armDesc":"Model used by auto-rename (AI refines the session title after each real user message). Follows the session model by default; pick a cheaper/dedicated model, saved immediately, takes effect on next analysis.","armFollow":"Follow session model (default)","armProvider":"(choose provider)","armModel":"(choose model)","armSet":"Set auto-rename model","armCurFollow":"Current: follow session model","armCurNone":"Current: not selected","renderFail":"Settings render failed: "}} };
|
|
16
|
+
window.__SC_I18N__ = window.__SC_I18N__ || { zh: {}, en: {}, loaded: false, waiters: [] };
|
|
17
|
+
if (!window.__SC_I18N__.started) {
|
|
18
|
+
window.__SC_I18N__.started = true;
|
|
19
|
+
(window.__scFetch || fetch)("/api/session-conductor/i18n", { credentials: "same-origin" })
|
|
20
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
21
|
+
.then((body) => {
|
|
22
|
+
if (body && body.ok && body.zh && body.en) {
|
|
23
|
+
window.__SC_I18N__.zh = body.zh;
|
|
24
|
+
window.__SC_I18N__.en = body.en;
|
|
25
|
+
}
|
|
26
|
+
window.__SC_I18N__.loaded = true;
|
|
27
|
+
window.__SC_I18N__.waiters.forEach((fn) => { try { fn(); } catch (e) { console.error("[sc-i18n] waiter:", e); } });
|
|
28
|
+
window.__SC_I18N__.waiters = [];
|
|
29
|
+
})
|
|
30
|
+
.catch(() => {
|
|
31
|
+
window.__SC_I18N__.loaded = true;
|
|
32
|
+
window.__SC_I18N__.waiters.forEach((fn) => { try { fn(); } catch (e) { console.error("[sc-i18n] waiter:", e); } });
|
|
33
|
+
window.__SC_I18N__.waiters = [];
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
window.__scFillI18nDict = (ns, zhRef, enRef) => {
|
|
37
|
+
// 启动兜底:先填导航 fallback(fetch 前标签立即中文),fetch 完成后再用完整字典覆盖
|
|
38
|
+
Object.assign(zhRef, (window.__SC_FALLBACK__?.zh ?? {})[ns] ?? {});
|
|
39
|
+
Object.assign(enRef, (window.__SC_FALLBACK__?.en ?? {})[ns] ?? {});
|
|
40
|
+
const fill = () => {
|
|
41
|
+
Object.assign(zhRef, window.__SC_I18N__.zh[ns] ?? {});
|
|
42
|
+
Object.assign(enRef, window.__SC_I18N__.en[ns] ?? {});
|
|
43
|
+
};
|
|
44
|
+
if (window.__SC_I18N__.loaded) fill();
|
|
45
|
+
else window.__SC_I18N__.waiters.push(fill);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// —— 全文本 i18n 统一取词:跨 NS + 当前语言 + fallback 兜底(fetch 完成前立即中文,fetch 后完整覆盖)——
|
|
49
|
+
// 各组件用它取文案(主面板/设置页/模板区块等),key 放在 lib/i18n/{zh,en}.json 对应 NS。
|
|
50
|
+
window.__SC_TR__ = (key, vars) => {
|
|
51
|
+
let val;
|
|
52
|
+
const i18n = window.__SC_I18N__ || {};
|
|
53
|
+
const lang = window.__SC_LANG__ || "zh";
|
|
54
|
+
const dicts = [i18n[lang] || {}, i18n.zh || {}];
|
|
55
|
+
for (const d of dicts) {
|
|
56
|
+
for (const ns in d) {
|
|
57
|
+
if (d[ns] && d[ns][key] !== undefined) { val = d[ns][key]; break; }
|
|
58
|
+
}
|
|
59
|
+
if (val !== undefined) break;
|
|
60
|
+
}
|
|
61
|
+
if (val === undefined) {
|
|
62
|
+
const fb = window.__SC_FALLBACK__ || {};
|
|
63
|
+
const f = fb[lang] || fb.zh || {};
|
|
64
|
+
for (const ns in f) { if (f[ns] && f[ns][key] !== undefined) { val = f[ns][key]; break; } }
|
|
65
|
+
}
|
|
66
|
+
if (val === undefined) return key;
|
|
67
|
+
if (vars) {
|
|
68
|
+
let s = String(val);
|
|
69
|
+
for (const k in vars) s = s.split("{" + k + "}").join(String(vars[k] ?? ""));
|
|
70
|
+
return s;
|
|
71
|
+
}
|
|
72
|
+
return String(val);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
window.__ModuleLoader__.load({
|
|
76
|
+
id: "dsh-session-conductor",
|
|
77
|
+
factory: (require) => {
|
|
78
|
+
var module = { exports: {} };
|
|
79
|
+
var exports = module.exports;
|
|
80
|
+
let react = require("react");
|
|
81
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
82
|
+
|
|
83
|
+
// ---------- 文案 ----------
|
|
84
|
+
|
|
85
|
+
const NS = "sessionConductor";
|
|
86
|
+
|
|
87
|
+
const zh = {};
|
|
88
|
+
const en = {};
|
|
89
|
+
__scFillI18nDict("sessionConductor", zh, en);
|
|
90
|
+
|
|
91
|
+
// ---------- 工具 ----------
|
|
92
|
+
|
|
93
|
+
// 全局 fetch 超时兜底:DSH API 挂起(重负载/路由未注册)时界面不再无限等待。
|
|
94
|
+
// 与 fetch 同签名(url, opts, timeoutMs),默认 30s;分片统一走 __scFetch。
|
|
95
|
+
const FETCH_TIMEOUT_MS = 30000;
|
|
96
|
+
window.__scFetch = function (url, opts, timeoutMs) {
|
|
97
|
+
const controller = new AbortController();
|
|
98
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs || FETCH_TIMEOUT_MS);
|
|
99
|
+
const next = opts ? Object.assign({}, opts, { signal: controller.signal }) : { signal: controller.signal };
|
|
100
|
+
return fetch(url, next).finally(() => clearTimeout(timer));
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
function basenameOf(p) {
|
|
104
|
+
const rawPath = String(p ?? "");
|
|
105
|
+
const cleaned = rawPath.replace(/[/\\]+$/, "");
|
|
106
|
+
const part = cleaned.split(/[/\\]/).pop();
|
|
107
|
+
return part || rawPath;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function timeAgo(ms, t) {
|
|
111
|
+
if (!Number.isFinite(ms)) return "";
|
|
112
|
+
const diff = Date.now() - ms;
|
|
113
|
+
const minute = 60e3;
|
|
114
|
+
const hour = 3600e3;
|
|
115
|
+
const day = 86400e3;
|
|
116
|
+
const month = 30 * day;
|
|
117
|
+
const year = 365 * day;
|
|
118
|
+
if (diff < minute) return t("meta.time.now");
|
|
119
|
+
if (diff < hour) return t("meta.time.minutes", { n: Math.floor(diff / minute) });
|
|
120
|
+
if (diff < day) return t("meta.time.hours", { n: Math.floor(diff / hour) });
|
|
121
|
+
if (diff < month) return t("meta.time.days", { n: Math.floor(diff / day) });
|
|
122
|
+
if (diff < year) return t("meta.time.months", { n: Math.floor(diff / month) });
|
|
123
|
+
return t("meta.time.years", { n: Math.floor(diff / year) });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ---------- UI 常量(主题串 + 尺寸数值)----------
|
|
127
|
+
// 集中命名:同一主题串/尺寸在内联样式与样式表里重复出现,改一处即全站生效。
|
|
128
|
+
const CSS_LABEL_SECONDARY = "var(--dsw-alias-label-secondary, #9a9aa0)";
|
|
129
|
+
const CSS_LABEL_TERTIARY = "var(--dsw-alias-label-tertiary, #77777d)";
|
|
130
|
+
const CSS_LABEL_PRIMARY = "var(--dsw-alias-label-primary, #eee)";
|
|
131
|
+
const CSS_BG_BASE = "var(--dsw-alias-bg-base, #141518)";
|
|
132
|
+
const CSS_BORDER_L2 = "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.3))";
|
|
133
|
+
const CSS_DANGER = "var(--dsw-state-danger-primary, #d9534f)";
|
|
134
|
+
const CSS_BORDER_HOVER = "1px solid var(--dsw-alias-interactive-bg-hover, #333338)";
|
|
135
|
+
const CONTENT_TYPE_JSON = "application/json";
|
|
136
|
+
const MSG_ACTION_FAILED = "action failed";
|
|
137
|
+
const PADDING_BAR = "8px 12px 0";
|
|
138
|
+
/** 浮层 z-index:压住宿主其它浮层,保证面板在最上。 */
|
|
139
|
+
const OVERLAY_Z_INDEX = 9999;
|
|
140
|
+
/** 触发器行高 / 紧凑行高。 */
|
|
141
|
+
const LINE_HEIGHT_TRIGGER = 1.4;
|
|
142
|
+
const LINE_HEIGHT_COMPACT = 1.35;
|
|
143
|
+
/** 标题字重(半粗)/ 分组标题字重(加粗)/ 表单标签字重。 */
|
|
144
|
+
const FONT_WEIGHT_SEMIBOLD = 600;
|
|
145
|
+
const FONT_WEIGHT_BOLD = 700;
|
|
146
|
+
const LABEL_FONT_WEIGHT = 600;
|
|
147
|
+
/** 顶部状态行最小高度(勾选框与徽章对齐用)。 */
|
|
148
|
+
const ROW_TOP_MIN_HEIGHT = 20;
|
|
149
|
+
/** 胶囊圆角(计数徽章 / 状态徽章共用):足够大即为胶囊形。 */
|
|
150
|
+
const PILL_RADIUS = 999;
|
|
151
|
+
/** 操作开关之间的间距。 */
|
|
152
|
+
const OPS_TOGGLE_GAP = 14;
|
|
153
|
+
/** 输入控件尺寸:最小宽 / 高 / 圆角。 */
|
|
154
|
+
const CONTROL_MIN_WIDTH = 160;
|
|
155
|
+
const CONTROL_HEIGHT = 28;
|
|
156
|
+
const CONTROL_RADIUS = 6;
|
|
157
|
+
/** 按条件删除表单里的小输入框:天数框宽 / cwd 前缀框最小宽 / 小控件高。 */
|
|
158
|
+
const RULE_DAYS_INPUT_WIDTH = 56;
|
|
159
|
+
const RULE_PREFIX_MIN_WIDTH = 140;
|
|
160
|
+
const SMALL_CONTROL_HEIGHT = 26;
|
|
161
|
+
/** 帮助区描述文字最小宽(与按钮同排时的换行阈值)。 */
|
|
162
|
+
const HELP_DESC_MIN_WIDTH = 180;
|
|
163
|
+
/** 设置页:标签最小宽 / 文本域高度区间 / 两个下拉框宽度。 */
|
|
164
|
+
const LABEL_MIN_WIDTH = 90;
|
|
165
|
+
const TEXTAREA_MIN_HEIGHT = 56;
|
|
166
|
+
const TEXTAREA_MAX_HEIGHT = 340;
|
|
167
|
+
const SELECT_WIDTH_PROVIDER = 170;
|
|
168
|
+
const SELECT_WIDTH_MODEL = 210;
|
|
169
|
+
/** 模板 textarea 行高 / 小字号(列表小字、表单提示共用)。 */
|
|
170
|
+
const LINE_HEIGHT_TEXTAREA = 1.55;
|
|
171
|
+
const LINE_HEIGHT_HELP = 1.65;
|
|
172
|
+
const FONT_SIZE_SMALL = 12.5;
|
|
173
|
+
/** i18n 键:模板槽位简称(plan / closing,多处复用)。 */
|
|
174
|
+
const I18N_TPL_PLAN_SHORT = "tpl.planShort";
|
|
175
|
+
const I18N_TPL_CLOSING_SHORT = "tpl.closingShort";
|
|
176
|
+
/** HTTP 报错前缀:`HTTP 502` 这类状态码提示。 */
|
|
177
|
+
const HTTP_STATUS_PREFIX = "HTTP ";
|
|
178
|
+
|
|
179
|
+
// ---------- 样式(主题变量 + 兜底色) ----------
|
|
180
|
+
const S = {
|
|
181
|
+
trigger: (wide) => ({
|
|
182
|
+
display: "flex",
|
|
183
|
+
alignItems: "center",
|
|
184
|
+
gap: 6,
|
|
185
|
+
padding: wide ? "6px 10px" : "6px",
|
|
186
|
+
margin: "0 6px 6px",
|
|
187
|
+
borderRadius: 8,
|
|
188
|
+
border: "1px solid transparent",
|
|
189
|
+
background: "transparent",
|
|
190
|
+
color: CSS_LABEL_SECONDARY,
|
|
191
|
+
fontSize: 12,
|
|
192
|
+
lineHeight: LINE_HEIGHT_TRIGGER,
|
|
193
|
+
cursor: "pointer",
|
|
194
|
+
whiteSpace: "nowrap",
|
|
195
|
+
textAlign: "left",
|
|
196
|
+
width: wide ? "auto" : 32,
|
|
197
|
+
justifyContent: wide ? "flex-start" : "center",
|
|
198
|
+
}),
|
|
199
|
+
overlay: { position: "fixed", inset: 0, zIndex: OVERLAY_Z_INDEX },
|
|
200
|
+
backdrop: { position: "absolute", inset: 0, background: "rgba(0,0,0,0.35)" },
|
|
201
|
+
panel: {
|
|
202
|
+
position: "absolute",
|
|
203
|
+
top: 0,
|
|
204
|
+
right: 0,
|
|
205
|
+
bottom: 0,
|
|
206
|
+
width: 400,
|
|
207
|
+
maxWidth: "94vw",
|
|
208
|
+
background: "var(--dsw-alias-bg-layer-2, #202024)",
|
|
209
|
+
color: "var(--dsw-alias-label-primary, #ececf0)",
|
|
210
|
+
boxShadow: "-10px 0 32px rgba(0,0,0,0.4)",
|
|
211
|
+
display: "flex",
|
|
212
|
+
flexDirection: "column",
|
|
213
|
+
fontFamily: "system-ui, -apple-system, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif",
|
|
214
|
+
fontSize: 13,
|
|
215
|
+
},
|
|
216
|
+
header: {
|
|
217
|
+
padding: "14px 16px 10px",
|
|
218
|
+
borderBottom: CSS_BORDER_HOVER,
|
|
219
|
+
display: "flex",
|
|
220
|
+
alignItems: "flex-start",
|
|
221
|
+
justifyContent: "space-between",
|
|
222
|
+
gap: 8,
|
|
223
|
+
},
|
|
224
|
+
title: { margin: 0, fontSize: 15, fontWeight: FONT_WEIGHT_SEMIBOLD },
|
|
225
|
+
subtitle: { margin: "3px 0 0", fontSize: 12, color: CSS_LABEL_SECONDARY },
|
|
226
|
+
close: {
|
|
227
|
+
background: "transparent",
|
|
228
|
+
border: "none",
|
|
229
|
+
color: CSS_LABEL_SECONDARY,
|
|
230
|
+
fontSize: 16,
|
|
231
|
+
lineHeight: 1,
|
|
232
|
+
cursor: "pointer",
|
|
233
|
+
padding: "2px 6px",
|
|
234
|
+
borderRadius: 6,
|
|
235
|
+
},
|
|
236
|
+
tabs: { display: "flex", gap: 4, padding: "8px 12px 0", borderBottom: CSS_BORDER_HOVER },
|
|
237
|
+
tab: (active) => ({
|
|
238
|
+
background: active ? "var(--dsw-alias-interactive-bg-hover, #333338)" : "transparent",
|
|
239
|
+
border: "none",
|
|
240
|
+
color: active ? "var(--dsw-alias-label-primary, #ececf0)" : CSS_LABEL_SECONDARY,
|
|
241
|
+
padding: "6px 12px",
|
|
242
|
+
borderRadius: "8px 8px 0 0",
|
|
243
|
+
fontSize: FONT_SIZE_SMALL,
|
|
244
|
+
cursor: "pointer",
|
|
245
|
+
}),
|
|
246
|
+
body: { flex: 1, overflowY: "auto", padding: "8px 12px 16px" },
|
|
247
|
+
row: {
|
|
248
|
+
// 会话行改为纵向卡片——顶部状态(checkbox+徽章)/ 中间内容 / 下方操作集中
|
|
249
|
+
display: "flex",
|
|
250
|
+
flexDirection: "column",
|
|
251
|
+
alignItems: "stretch",
|
|
252
|
+
gap: 7,
|
|
253
|
+
padding: "8px 10px",
|
|
254
|
+
borderRadius: 8,
|
|
255
|
+
border: "1px solid var(--dsw-alias-interactive-bg-hover, #2e2e33)",
|
|
256
|
+
marginTop: 6,
|
|
257
|
+
cursor: "pointer",
|
|
258
|
+
},
|
|
259
|
+
// 顶部状态行:批量勾选 + 状态徽章
|
|
260
|
+
rowTop: {
|
|
261
|
+
display: "flex",
|
|
262
|
+
alignItems: "center",
|
|
263
|
+
gap: 6,
|
|
264
|
+
flexShrink: 0,
|
|
265
|
+
minHeight: ROW_TOP_MIN_HEIGHT,
|
|
266
|
+
},
|
|
267
|
+
rowTopBadges: {
|
|
268
|
+
display: "flex",
|
|
269
|
+
alignItems: "center",
|
|
270
|
+
gap: 5,
|
|
271
|
+
flexShrink: 0,
|
|
272
|
+
marginLeft: "auto",
|
|
273
|
+
},
|
|
274
|
+
groupHeader: {
|
|
275
|
+
display: "flex",
|
|
276
|
+
alignItems: "center",
|
|
277
|
+
gap: 6,
|
|
278
|
+
padding: "8px 10px 4px",
|
|
279
|
+
marginTop: 10,
|
|
280
|
+
cursor: "pointer",
|
|
281
|
+
userSelect: "none",
|
|
282
|
+
borderBottom: "1px solid var(--dsw-alias-interactive-bg-hover, #2e2e33)",
|
|
283
|
+
},
|
|
284
|
+
groupCaret: { fontSize: 10, color: CSS_LABEL_SECONDARY, flexShrink: 0 },
|
|
285
|
+
groupTitle: { fontWeight: FONT_WEIGHT_BOLD, fontSize: FONT_SIZE_SMALL, flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
|
|
286
|
+
groupCount: {
|
|
287
|
+
flexShrink: 0,
|
|
288
|
+
fontSize: 10.5,
|
|
289
|
+
padding: "1px 8px",
|
|
290
|
+
borderRadius: PILL_RADIUS,
|
|
291
|
+
border: "1px solid var(--dsw-alias-interactive-bg-hover, #3a3a40)",
|
|
292
|
+
color: CSS_LABEL_SECONDARY,
|
|
293
|
+
},
|
|
294
|
+
groupList: { paddingLeft: 2 },
|
|
295
|
+
rowMain: { flex: 1, minWidth: 0 },
|
|
296
|
+
rowTitle: {
|
|
297
|
+
display: "flex",
|
|
298
|
+
alignItems: "flex-start", // 标题最多 3 行时徽章顶部对齐不被挤压
|
|
299
|
+
gap: 6,
|
|
300
|
+
fontWeight: FONT_WEIGHT_SEMIBOLD,
|
|
301
|
+
fontSize: 13,
|
|
302
|
+
overflow: "hidden",
|
|
303
|
+
lineHeight: LINE_HEIGHT_COMPACT,
|
|
304
|
+
},
|
|
305
|
+
rowMeta: { marginTop: 2, fontSize: 11.5, color: CSS_LABEL_SECONDARY, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
|
|
306
|
+
// 第三行小字 session id(等宽小字号,方便复制/排查)
|
|
307
|
+
rowSid: { marginTop: 2, fontSize: 10.5, fontFamily: "ui-monospace,SFMono-Regular,Menlo,Consolas,monospace", color: "var(--dsw-alias-label-tertiary, #6f6f76)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", userSelect: "text" },
|
|
308
|
+
badge: (kind) => ({
|
|
309
|
+
flexShrink: 0,
|
|
310
|
+
fontSize: 10.5,
|
|
311
|
+
padding: "1px 7px",
|
|
312
|
+
borderRadius: PILL_RADIUS,
|
|
313
|
+
border: "1px solid",
|
|
314
|
+
...(kind === "archived"
|
|
315
|
+
? { color: CSS_LABEL_SECONDARY, borderColor: "#55555c" }
|
|
316
|
+
: kind === "running"
|
|
317
|
+
? { color: "var(--dsw-state-success-primary, #4caf7d)", borderColor: "currentColor" }
|
|
318
|
+
: { color: CSS_LABEL_TERTIARY, borderColor: "currentColor" }),
|
|
319
|
+
}),
|
|
320
|
+
actions: { flexShrink: 0, display: "flex", gap: 6, justifyContent: "flex-end", flexWrap: "wrap" },
|
|
321
|
+
actionsCol: { flexShrink: 0, display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 5 },
|
|
322
|
+
// 下方操作集中区(开关行 + 按钮行,纵向堆叠)
|
|
323
|
+
rowOps: { display: "flex", flexDirection: "column", gap: 6, flexShrink: 0 },
|
|
324
|
+
opsToggles: { display: "flex", alignItems: "center", gap: OPS_TOGGLE_GAP },
|
|
325
|
+
opsButtons: { display: "flex", gap: 6, flexWrap: "wrap", justifyContent: "flex-start" },
|
|
326
|
+
autoToggle: {
|
|
327
|
+
display: "flex",
|
|
328
|
+
alignItems: "center",
|
|
329
|
+
gap: 4,
|
|
330
|
+
fontSize: 11,
|
|
331
|
+
color: CSS_LABEL_SECONDARY,
|
|
332
|
+
cursor: "pointer",
|
|
333
|
+
userSelect: "none",
|
|
334
|
+
whiteSpace: "nowrap",
|
|
335
|
+
},
|
|
336
|
+
actionBtn: (danger) => ({
|
|
337
|
+
background: "transparent",
|
|
338
|
+
border: danger ? "1px solid var(--dsw-state-error-primary, #d9534f)" : "1px solid var(--dsw-alias-interactive-bg-hover, #3a3a40)",
|
|
339
|
+
color: danger ? "var(--dsw-state-error-primary, #d9534f)" : "var(--dsw-alias-label-secondary, #d0d0d6)",
|
|
340
|
+
borderRadius: 6,
|
|
341
|
+
padding: "3px 9px",
|
|
342
|
+
fontSize: 11.5,
|
|
343
|
+
cursor: "pointer",
|
|
344
|
+
}),
|
|
345
|
+
hint: { margin: "10px 0 6px", fontSize: 12, color: CSS_LABEL_SECONDARY },
|
|
346
|
+
helpBlock: {
|
|
347
|
+
marginTop: 10,
|
|
348
|
+
padding: "10px 12px",
|
|
349
|
+
borderRadius: 8,
|
|
350
|
+
background: "var(--dsw-alias-interactive-bg-hover, #2a2a30)",
|
|
351
|
+
border: CSS_BORDER_HOVER,
|
|
352
|
+
},
|
|
353
|
+
helpTitle: { fontWeight: FONT_WEIGHT_SEMIBOLD, fontSize: 13, margin: "0 0 4px" },
|
|
354
|
+
helpDesc: { margin: 0, fontSize: FONT_SIZE_SMALL, lineHeight: LINE_HEIGHT_HELP, color: "var(--dsw-alias-label-secondary, #c8c8ce)" },
|
|
355
|
+
empty: { marginTop: 24, textAlign: "center", color: CSS_LABEL_TERTIARY, fontSize: FONT_SIZE_SMALL },
|
|
356
|
+
error: { marginTop: 10, padding: "8px 10px", borderRadius: 8, border: "1px solid var(--dsw-state-error-primary, #d9534f)", color: "var(--dsw-state-error-primary, #d9534f)", fontSize: 12 },
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
// ---------- 组件 ----------
|
|
360
|
+
|
|
361
|
+
// 会话列表长效缓存(localStorage):插件启动即预取 + 每次刷新落盘,面板重开/页面刷新秒显历史数据
|
|
362
|
+
const LIST_CACHE_KEY = "dsh-session-conductor:list:v1";
|
|
363
|
+
function readListCache() {
|
|
364
|
+
// 2026-10-12 修复:无缓存 / 缓存损坏时必须回**空数组**,不能回 null。
|
|
365
|
+
// 调用方 SessionManagerPanel 把返回值当数组用(sessions 初值),null 会让
|
|
366
|
+
// 流式追加路径的 insertSorted(null, item) 抛
|
|
367
|
+
// "Cannot read properties of null (reading 'filter')",React 更新器抛错后
|
|
368
|
+
// sidebar.footer.action 插槽整块崩溃 —— 表现为「会话管理」入口直接消失。
|
|
369
|
+
try {
|
|
370
|
+
const raw = localStorage.getItem(LIST_CACHE_KEY);
|
|
371
|
+
// if (!raw) return null;
|
|
372
|
+
if (!raw) return [];
|
|
373
|
+
const parsed = JSON.parse(raw);
|
|
374
|
+
// return Array.isArray(parsed?.sessions) ? parsed.sessions : null;
|
|
375
|
+
return Array.isArray(parsed?.sessions) ? parsed.sessions : [];
|
|
376
|
+
} catch {
|
|
377
|
+
// return null;
|
|
378
|
+
return [];
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function writeListCache(sessions) {
|
|
382
|
+
try {
|
|
383
|
+
localStorage.setItem(LIST_CACHE_KEY, JSON.stringify({ at: Date.now(), sessions }));
|
|
384
|
+
} catch {
|
|
385
|
+
/* 存储不可用时静默降级 */
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// 「会话管理」入口图标(学 dsh-side-monitor 的 MonitorIcon:内联 SVG + currentColor 线描)
|
|
390
|
+
/** 会话卡图标内的矩形(24x24 viewBox 坐标系)。 */
|
|
391
|
+
const ICON_RECT_X = 3;
|
|
392
|
+
const ICON_RECT_Y = 4;
|
|
393
|
+
const ICON_RECT_WIDTH = 18;
|
|
394
|
+
const ICON_RECT_HEIGHT = 14;
|
|
395
|
+
const ICON_RECT_RADIUS = 2;
|
|
396
|
+
|
|
397
|
+
function SessionIcon(props) {
|
|
398
|
+
const s = props.size || 16;
|
|
399
|
+
return react_jsx_runtime.jsx("svg", {
|
|
400
|
+
width: s, height: s, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor",
|
|
401
|
+
strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true,
|
|
402
|
+
children: [
|
|
403
|
+
react_jsx_runtime.jsx("rect", { x: ICON_RECT_X, y: ICON_RECT_Y, width: ICON_RECT_WIDTH, height: ICON_RECT_HEIGHT, rx: ICON_RECT_RADIUS }),
|
|
404
|
+
react_jsx_runtime.jsx("path", { d: "M8 20h8" }),
|
|
405
|
+
react_jsx_runtime.jsx("path", { d: "M12 18v2" }),
|
|
406
|
+
],
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function SessionManagerPanel({ wide, t, onOpenSession }) {
|
|
411
|
+
const [open, setOpen] = react.useState(false);
|
|
412
|
+
const [tab, setTab] = react.useState("all");
|
|
413
|
+
const [sessions, setSessions] = react.useState(() => readListCache()); // 启动即从长效缓存加载
|
|
414
|
+
const [groups, setGroups] = react.useState(null); // workspaces[](group/list);null = host 无分组 API
|
|
415
|
+
const [collapsed, setCollapsed] = react.useState(() => new Set()); // 折叠的分组(含 "__ungrouped__")
|
|
416
|
+
const [error, setError] = react.useState(null);
|
|
417
|
+
const [busy, setBusy] = react.useState(null);
|
|
418
|
+
// —— 搜索 + 批量/按条件删除 ——
|
|
419
|
+
const [query, setQuery] = react.useState(""); // 列表过滤关键词
|
|
420
|
+
const [selected, setSelected] = react.useState(() => new Set()); // 批量删除勾选
|
|
421
|
+
const [fullResults, setFullResults] = react.useState(null); // 全文搜索结果(null=未搜索)
|
|
422
|
+
const [fullSearching, setFullSearching] = react.useState(false);
|
|
423
|
+
const [ruleOpen, setRuleOpen] = react.useState(false); // 按条件删除表单展开
|
|
424
|
+
const [ruleArchivedOnly, setRuleArchivedOnly] = react.useState(true);
|
|
425
|
+
const [ruleDays, setRuleDays] = react.useState(7);
|
|
426
|
+
const [rulePrefix, setRulePrefix] = react.useState("");
|
|
427
|
+
const [ruleLowValue, setRuleLowValue] = react.useState(false); // 低价值筛选(复用价值分析)
|
|
428
|
+
const [rulePreview, setRulePreview] = react.useState(null); // dryRun 预览(null=未预览)
|
|
429
|
+
|
|
430
|
+
// list 流式——NDJSON 每行一个会话,读到立即追加显示(每获取一个追加一个,
|
|
431
|
+
// 不等全部);按 updatedAt 倒序插入保持列表顺序;旧一次性 JSON(预览页离线垫片/
|
|
432
|
+
// 缓存路径)走 setSessions 全量。
|
|
433
|
+
const insertSorted = (cur, item) => {
|
|
434
|
+
// 2026-10-12 修复:cur 兜底为数组。历史版本 readListCache() 无缓存时返回 null,
|
|
435
|
+
// 使 sessions 初值为 null,流式追加走到这里就炸(见 foundation/list-cache.js 同款修复)。
|
|
436
|
+
// const next = cur.filter((x) => x.id !== item.id);
|
|
437
|
+
const next = (Array.isArray(cur) ? cur : []).filter((x) => x.id !== item.id);
|
|
438
|
+
let i = 0;
|
|
439
|
+
while (i < next.length && (next[i].updatedAt ?? 0) > (item.updatedAt ?? 0)) i += 1;
|
|
440
|
+
next.splice(i, 0, item);
|
|
441
|
+
return next;
|
|
442
|
+
};
|
|
443
|
+
const refresh = react.useCallback(async () => {
|
|
444
|
+
try {
|
|
445
|
+
const listPromise = window.__scFetch("/api/session-conductor/list", { headers: { Accept: "application/x-ndjson" } });
|
|
446
|
+
const groupPromise = window.__scFetch("/api/session-conductor/group/list")
|
|
447
|
+
.then((r) => (r.ok ? r.json() : { ok: false })).catch(() => ({ ok: false }));
|
|
448
|
+
const listRes = await listPromise;
|
|
449
|
+
if (!listRes.ok) throw new Error("list failed: HTTP " + listRes.status);
|
|
450
|
+
let lastBatch = null;
|
|
451
|
+
if (typeof listRes.body?.getReader === "function") {
|
|
452
|
+
// 流式 NDJSON:逐行解析追加
|
|
453
|
+
const reader = listRes.body.getReader();
|
|
454
|
+
const decoder = new TextDecoder();
|
|
455
|
+
let buf = "";
|
|
456
|
+
const batch = [];
|
|
457
|
+
for (;;) {
|
|
458
|
+
const { done, value } = await reader.read();
|
|
459
|
+
if (done) break;
|
|
460
|
+
buf += decoder.decode(value, { stream: true });
|
|
461
|
+
let nl;
|
|
462
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
463
|
+
const line = buf.slice(0, nl).trim();
|
|
464
|
+
buf = buf.slice(nl + 1);
|
|
465
|
+
if (!line) continue;
|
|
466
|
+
let parsedLine;
|
|
467
|
+
try { parsedLine = JSON.parse(line); } catch { continue; } // 半行/头行跳过
|
|
468
|
+
if (parsedLine && parsedLine.id) {
|
|
469
|
+
batch.push(parsedLine);
|
|
470
|
+
setSessions((cur) => insertSorted(cur, parsedLine)); // 每获取一个追加一个
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
lastBatch = batch;
|
|
475
|
+
} else {
|
|
476
|
+
const body = await listRes.json();
|
|
477
|
+
if (!body.ok) throw new Error(body.error?.message ?? "list failed");
|
|
478
|
+
setSessions(body.sessions ?? []);
|
|
479
|
+
lastBatch = body.sessions ?? [];
|
|
480
|
+
}
|
|
481
|
+
if (lastBatch !== null) writeListCache(lastBatch); // 长效保存:刷新结果落 localStorage
|
|
482
|
+
const groupRes = await groupPromise;
|
|
483
|
+
setGroups(groupRes?.ok === true ? groupRes.workspaces ?? [] : null);
|
|
484
|
+
setError(null);
|
|
485
|
+
} catch (cause) {
|
|
486
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
487
|
+
}
|
|
488
|
+
}, []);
|
|
489
|
+
|
|
490
|
+
// 启动即自动加载(插件挂载就预取一次,不依赖面板打开);打开面板时再刷新一次保证最新
|
|
491
|
+
react.useEffect(() => {
|
|
492
|
+
refresh();
|
|
493
|
+
}, [refresh]);
|
|
494
|
+
react.useEffect(() => {
|
|
495
|
+
if (open) refresh();
|
|
496
|
+
}, [open, refresh]);
|
|
497
|
+
|
|
498
|
+
// 通用会话操作(archive/unarchive/delete):乐观更新——点了立即改 UI,后端异步处理,
|
|
499
|
+
// 失败回滚 + 提示;不阻塞其他会话的操作(不设全局 busy)。
|
|
500
|
+
const act = async (sessionId, action) => {
|
|
501
|
+
let prev = null;
|
|
502
|
+
setSessions((cur) => {
|
|
503
|
+
prev = cur.find((s) => s.id === sessionId) ?? null;
|
|
504
|
+
if (action === "delete") return cur.filter((s) => s.id !== sessionId);
|
|
505
|
+
if (action === "archive" || action === "unarchive") {
|
|
506
|
+
return cur.map((s) => (s.id === sessionId ? { ...s, archived: action === "archive" } : s));
|
|
507
|
+
}
|
|
508
|
+
return cur;
|
|
509
|
+
});
|
|
510
|
+
try {
|
|
511
|
+
const res = await window.__scFetch(`/api/session-conductor/${action}`, {
|
|
512
|
+
method: "POST",
|
|
513
|
+
headers: { "Content-Type": CONTENT_TYPE_JSON },
|
|
514
|
+
body: JSON.stringify({ sessionId }),
|
|
515
|
+
});
|
|
516
|
+
const body = await res.json();
|
|
517
|
+
if (!body.ok) throw new Error(body.error?.message ?? MSG_ACTION_FAILED);
|
|
518
|
+
await refresh();
|
|
519
|
+
} catch (cause) {
|
|
520
|
+
// 回滚乐观更新
|
|
521
|
+
if (prev) {
|
|
522
|
+
setSessions((cur) => {
|
|
523
|
+
if (action === "delete") return cur.some((s) => s.id === sessionId) ? cur : [...cur, prev];
|
|
524
|
+
return cur.map((s) => (s.id === sessionId ? prev : s));
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
528
|
+
window.alert(t("error.action", { message }));
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
const confirmDelete = (session) => {
|
|
533
|
+
const label = session.title || session.cwd || session.id;
|
|
534
|
+
const yes = window.confirm(`${t("confirm.delete.title", { title: label })}\n\n${t("confirm.delete.body")}`);
|
|
535
|
+
if (yes) act(session.id, "delete");
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
// 撤回最后一条消息:dryRun 先预览 → 二次确认 → 执行 → 刷新
|
|
539
|
+
const undoLast = async (session) => {
|
|
540
|
+
setBusy(session.id);
|
|
541
|
+
try {
|
|
542
|
+
const res = await window.__scFetch("/api/session-conductor/undo-message", {
|
|
543
|
+
method: "POST",
|
|
544
|
+
headers: { "Content-Type": CONTENT_TYPE_JSON },
|
|
545
|
+
body: JSON.stringify({ sessionId: session.id, dryRun: true }),
|
|
546
|
+
});
|
|
547
|
+
const body = await res.json();
|
|
548
|
+
if (!body.ok) throw new Error(body.error?.message ?? "preview failed");
|
|
549
|
+
const preview = body.preview || t("undo.preview.empty");
|
|
550
|
+
const yes = window.confirm(`${t("confirm.undo.title")}\n\n${t("confirm.undo.body", { preview })}`);
|
|
551
|
+
if (!yes) return;
|
|
552
|
+
const res2 = await window.__scFetch("/api/session-conductor/undo-message", {
|
|
553
|
+
method: "POST",
|
|
554
|
+
headers: { "Content-Type": CONTENT_TYPE_JSON },
|
|
555
|
+
body: JSON.stringify({ sessionId: session.id }),
|
|
556
|
+
});
|
|
557
|
+
const body2 = await res2.json();
|
|
558
|
+
if (!body2.ok) throw new Error(body2.error?.message ?? "undo failed");
|
|
559
|
+
window.alert(t("undo.done", { preview }));
|
|
560
|
+
await refresh();
|
|
561
|
+
} catch (cause) {
|
|
562
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
563
|
+
window.alert(t("error.action", { message }));
|
|
564
|
+
} finally {
|
|
565
|
+
setBusy(null);
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
// 自动重命名开关:乐观翻转(点了立即变),后端异步处理,失败回滚;不阻塞其他操作。
|
|
570
|
+
const toggleAutoRename = async (sessionId, enabled) => {
|
|
571
|
+
setSessions((cur) => cur.map((s) => (s.id === sessionId ? { ...s, autoRename: enabled } : s)));
|
|
572
|
+
try {
|
|
573
|
+
const res = await window.__scFetch("/api/session-conductor/auto-rename", {
|
|
574
|
+
method: "POST",
|
|
575
|
+
headers: { "Content-Type": CONTENT_TYPE_JSON },
|
|
576
|
+
body: JSON.stringify({ sessionId, enabled }),
|
|
577
|
+
});
|
|
578
|
+
const body = await res.json();
|
|
579
|
+
if (!body.ok) throw new Error(body.error?.message ?? MSG_ACTION_FAILED);
|
|
580
|
+
// 开启时后端立即分析,若已改名(analyzed.after)→ 马上用新标题重显示,
|
|
581
|
+
// 不等 refresh 全量冷扫描(可能数秒~数十秒)
|
|
582
|
+
if (enabled && body.analyzed?.ok && typeof body.analyzed?.after === "string" && body.analyzed.after !== "") {
|
|
583
|
+
setSessions((cur) => cur.map((s) => (s.id === sessionId ? { ...s, title: body.analyzed.after } : s)));
|
|
584
|
+
}
|
|
585
|
+
await refresh();
|
|
586
|
+
} catch (cause) {
|
|
587
|
+
setSessions((cur) => cur.map((s) => (s.id === sessionId ? { ...s, autoRename: !enabled } : s)));
|
|
588
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
589
|
+
window.alert(t("error.action", { message }));
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
const analyzeNow = async (sessionId) => {
|
|
594
|
+
setBusy(sessionId);
|
|
595
|
+
try {
|
|
596
|
+
const res = await window.__scFetch("/api/session-conductor/analyze", {
|
|
597
|
+
method: "POST",
|
|
598
|
+
headers: { "Content-Type": CONTENT_TYPE_JSON },
|
|
599
|
+
body: JSON.stringify({ sessionId }),
|
|
600
|
+
});
|
|
601
|
+
const body = await res.json();
|
|
602
|
+
if (!body.ok) {
|
|
603
|
+
const message = body.error?.message ?? MSG_ACTION_FAILED;
|
|
604
|
+
window.alert(`${t("action.analyze")}: ${message}`);
|
|
605
|
+
} else {
|
|
606
|
+
window.alert(body.renamed ? `${t("action.analyze")}: ${body.reason} → ${body.title}` : `${t("action.analyze")}: ${body.reason}`);
|
|
607
|
+
}
|
|
608
|
+
await refresh();
|
|
609
|
+
} catch (cause) {
|
|
610
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
611
|
+
window.alert(t("error.action", { message }));
|
|
612
|
+
} finally {
|
|
613
|
+
setBusy(null);
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
/** 扫描并修复损坏会话日志(先 dry-run 扫描,确认后真正修复)。 */
|
|
618
|
+
const scanAndRepair = async () => {
|
|
619
|
+
if (busy) return;
|
|
620
|
+
setBusy("__repair__");
|
|
621
|
+
try {
|
|
622
|
+
const scanRes = await window.__scFetch("/api/session-conductor/repair-sessions", {
|
|
623
|
+
method: "POST",
|
|
624
|
+
headers: { "Content-Type": CONTENT_TYPE_JSON },
|
|
625
|
+
body: JSON.stringify({ dryRun: true }),
|
|
626
|
+
});
|
|
627
|
+
const scan = await scanRes.json();
|
|
628
|
+
if (!scan.ok) throw new Error(scan.error?.message ?? "scan failed");
|
|
629
|
+
const corrupt = scan.report?.corrupt ?? 0;
|
|
630
|
+
const msg = corrupt === 0
|
|
631
|
+
? t("repair.scanClean")
|
|
632
|
+
: t("repair.confirm", { n: corrupt });
|
|
633
|
+
if (corrupt > 0 && !window.confirm(msg)) { setBusy(null); return; }
|
|
634
|
+
if (corrupt === 0) { window.alert(msg); setBusy(null); return; }
|
|
635
|
+
const fixRes = await window.__scFetch("/api/session-conductor/repair-sessions", {
|
|
636
|
+
method: "POST",
|
|
637
|
+
headers: { "Content-Type": CONTENT_TYPE_JSON },
|
|
638
|
+
body: JSON.stringify({ dryRun: false }),
|
|
639
|
+
});
|
|
640
|
+
const fix = await fixRes.json();
|
|
641
|
+
if (!fix.ok) throw new Error(fix.error?.message ?? "repair failed");
|
|
642
|
+
window.alert(t("repair.done", { n: fix.report?.fixed ?? 0 }));
|
|
643
|
+
await refresh();
|
|
644
|
+
} catch (cause) {
|
|
645
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
646
|
+
window.alert(t("error.action", { message }));
|
|
647
|
+
} finally {
|
|
648
|
+
setBusy(null);
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
/** 扫描并修复 EIO 坏块会话(先 dry-run 扫描,确认后真正修复)。
|
|
653
|
+
* 需求来源:「failed to observe session xxx: EIO: i/o error, read」+
|
|
654
|
+
* 「把这个功能写进会话管理插件…新增修复会话」。
|
|
655
|
+
* 思路:与 scanAndRepair 同构(scan → confirm → fix),但走 /repair-eio 端点,
|
|
656
|
+
* 该端点做块级 EIO 探测与截断修复(见 lib/eio-repair.js)。 */
|
|
657
|
+
const scanAndRepairEio = async () => {
|
|
658
|
+
if (busy) return;
|
|
659
|
+
setBusy("__repair_eio__");
|
|
660
|
+
try {
|
|
661
|
+
const scanRes = await window.__scFetch("/api/session-conductor/repair-eio", {
|
|
662
|
+
method: "POST",
|
|
663
|
+
headers: { "Content-Type": CONTENT_TYPE_JSON },
|
|
664
|
+
body: JSON.stringify({ dryRun: true }),
|
|
665
|
+
});
|
|
666
|
+
const scan = await scanRes.json();
|
|
667
|
+
if (!scan.ok) throw new Error(scan.error?.message ?? "eio scan failed");
|
|
668
|
+
const eioCount = (scan.eio ?? []).length;
|
|
669
|
+
const msg = eioCount === 0
|
|
670
|
+
? t("repairEio.scanClean")
|
|
671
|
+
: t("repairEio.confirm", { n: eioCount });
|
|
672
|
+
if (eioCount === 0) { window.alert(msg); setBusy(null); return; }
|
|
673
|
+
if (!window.confirm(msg)) { setBusy(null); return; }
|
|
674
|
+
const fixRes = await window.__scFetch("/api/session-conductor/repair-eio", {
|
|
675
|
+
method: "POST",
|
|
676
|
+
headers: { "Content-Type": CONTENT_TYPE_JSON },
|
|
677
|
+
body: JSON.stringify({ dryRun: false }),
|
|
678
|
+
});
|
|
679
|
+
const fix = await fixRes.json();
|
|
680
|
+
if (!fix.ok) throw new Error(fix.error?.message ?? "eio repair failed");
|
|
681
|
+
window.alert(t("repairEio.done", { n: fix.report?.fixed ?? 0 }));
|
|
682
|
+
await refresh();
|
|
683
|
+
} catch (cause) {
|
|
684
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
685
|
+
window.alert(t("error.action", { message }));
|
|
686
|
+
} finally {
|
|
687
|
+
setBusy(null);
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
/** 扫描并修复双格式会话(先 dry-run 扫描,确认后真正修复)。
|
|
692
|
+
* 需求来源:会话目录同时存在 session.jsonl + session.jsonl.zstd 时官方
|
|
693
|
+
* listArtifacts() 抛 encodingMismatch → 会话列表全失败(侧边栏会话消失)。
|
|
694
|
+
* 思路:与 scanAndRepairEio 同构(scan → confirm → fix),但走
|
|
695
|
+
* /repair-dual-format 端点(纯磁盘扫描,旁路 persistence,见 lib/repair.js)。 */
|
|
696
|
+
const scanAndRepairDual = async () => {
|
|
697
|
+
if (busy) return;
|
|
698
|
+
setBusy("__repair_dual__");
|
|
699
|
+
try {
|
|
700
|
+
const scanRes = await window.__scFetch("/api/session-conductor/repair-dual-format", {
|
|
701
|
+
method: "POST",
|
|
702
|
+
headers: { "Content-Type": CONTENT_TYPE_JSON },
|
|
703
|
+
body: JSON.stringify({ dryRun: true }),
|
|
704
|
+
});
|
|
705
|
+
const scan = await scanRes.json();
|
|
706
|
+
if (!scan.ok) throw new Error(scan.error?.message ?? "dual-format scan failed");
|
|
707
|
+
const dualCount = scan.report?.dual ?? 0;
|
|
708
|
+
const msg = dualCount === 0
|
|
709
|
+
? t("repairDual.scanClean")
|
|
710
|
+
: t("repairDual.confirm", { n: dualCount });
|
|
711
|
+
if (dualCount === 0) { window.alert(msg); setBusy(null); return; }
|
|
712
|
+
if (!window.confirm(msg)) { setBusy(null); return; }
|
|
713
|
+
const fixRes = await window.__scFetch("/api/session-conductor/repair-dual-format", {
|
|
714
|
+
method: "POST",
|
|
715
|
+
headers: { "Content-Type": CONTENT_TYPE_JSON },
|
|
716
|
+
body: JSON.stringify({ dryRun: false }),
|
|
717
|
+
});
|
|
718
|
+
const fix = await fixRes.json();
|
|
719
|
+
if (!fix.ok) throw new Error(fix.error?.message ?? "dual-format repair failed");
|
|
720
|
+
window.alert(t("repairDual.done", { n: fix.report?.fixed ?? 0 }));
|
|
721
|
+
await refresh();
|
|
722
|
+
} catch (cause) {
|
|
723
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
724
|
+
window.alert(t("error.action", { message }));
|
|
725
|
+
} finally {
|
|
726
|
+
setBusy(null);
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
// 自动续跑开关:乐观翻转(点了立即变),后端异步处理,失败回滚;不阻塞其他操作。
|
|
731
|
+
const toggleAutoContinue = async (sessionId, enabled) => {
|
|
732
|
+
setSessions((cur) => cur.map((s) => (s.id === sessionId ? { ...s, autoContinue: enabled } : s)));
|
|
733
|
+
try {
|
|
734
|
+
const res = await window.__scFetch("/api/session-conductor/auto-continue", {
|
|
735
|
+
method: "POST",
|
|
736
|
+
headers: { "Content-Type": CONTENT_TYPE_JSON },
|
|
737
|
+
body: JSON.stringify({ sessionId, enabled }),
|
|
738
|
+
});
|
|
739
|
+
const body = await res.json();
|
|
740
|
+
if (!body.ok) throw new Error(body.error?.message ?? MSG_ACTION_FAILED);
|
|
741
|
+
await refresh();
|
|
742
|
+
} catch (cause) {
|
|
743
|
+
setSessions((cur) => cur.map((s) => (s.id === sessionId ? { ...s, autoContinue: !enabled } : s)));
|
|
744
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
745
|
+
window.alert(t("error.action", { message }));
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
const releaseNow = async (sessionId) => {
|
|
750
|
+
setBusy(sessionId);
|
|
751
|
+
try {
|
|
752
|
+
const res = await window.__scFetch("/api/session-conductor/detach", {
|
|
753
|
+
method: "POST",
|
|
754
|
+
headers: { "Content-Type": CONTENT_TYPE_JSON },
|
|
755
|
+
body: JSON.stringify({ sessionId }),
|
|
756
|
+
});
|
|
757
|
+
const body = await res.json();
|
|
758
|
+
if (!body.ok) {
|
|
759
|
+
window.alert(t("error.action", { message: body.error?.message ?? "release failed" }));
|
|
760
|
+
}
|
|
761
|
+
await refresh();
|
|
762
|
+
} catch (cause) {
|
|
763
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
764
|
+
window.alert(t("error.action", { message }));
|
|
765
|
+
} finally {
|
|
766
|
+
setBusy(null);
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
const releaseAll = async () => {
|
|
771
|
+
if (busy) return;
|
|
772
|
+
const label = t("confirm.releaseAll.title");
|
|
773
|
+
const yes = window.confirm(`${label}\n\n${t("confirm.releaseAll.body")}`);
|
|
774
|
+
if (!yes) return;
|
|
775
|
+
setBusy("__all__");
|
|
776
|
+
try {
|
|
777
|
+
const res = await window.__scFetch("/api/session-conductor/detach-all", {
|
|
778
|
+
method: "POST",
|
|
779
|
+
headers: { "Content-Type": CONTENT_TYPE_JSON },
|
|
780
|
+
body: JSON.stringify({}),
|
|
781
|
+
});
|
|
782
|
+
const body = await res.json();
|
|
783
|
+
if (!body.ok) {
|
|
784
|
+
window.alert(t("error.action", { message: body.error?.message ?? "release all failed" }));
|
|
785
|
+
} else if (body.released?.length > 0) {
|
|
786
|
+
window.alert(`${t("action.releaseAll")}: ${body.released.length}`);
|
|
787
|
+
}
|
|
788
|
+
await refresh();
|
|
789
|
+
} catch (cause) {
|
|
790
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
791
|
+
window.alert(t("error.action", { message }));
|
|
792
|
+
} finally {
|
|
793
|
+
setBusy(null);
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
const continueNow = async (sessionId) => {
|
|
798
|
+
setBusy(sessionId);
|
|
799
|
+
try {
|
|
800
|
+
const res = await window.__scFetch("/api/session-conductor/continue", {
|
|
801
|
+
method: "POST",
|
|
802
|
+
headers: { "Content-Type": CONTENT_TYPE_JSON },
|
|
803
|
+
body: JSON.stringify({ sessionId }),
|
|
804
|
+
});
|
|
805
|
+
const body = await res.json();
|
|
806
|
+
if (!body.ok) {
|
|
807
|
+
window.alert(t("error.action", { message: body.error?.message ?? "continue failed" }));
|
|
808
|
+
}
|
|
809
|
+
await refresh();
|
|
810
|
+
// 续跑进行中:轮询刷新几次以反映 continueRunning / running 状态
|
|
811
|
+
const POLL_TIMES = 6; // 续跑后轮询刷新次数
|
|
812
|
+
const POLL_INTERVAL_MS = 3000; // 轮询间隔
|
|
813
|
+
for (let i = 0; i < POLL_TIMES; i++) {
|
|
814
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
815
|
+
await refresh();
|
|
816
|
+
}
|
|
817
|
+
} catch (cause) {
|
|
818
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
819
|
+
window.alert(t("error.action", { message }));
|
|
820
|
+
} finally {
|
|
821
|
+
setBusy(null);
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
// —— 全文搜索(官方 FTS5 优先,不可用时降级内置扫描)——
|
|
826
|
+
const SEARCH_LIMIT = 50; // 单次搜索最大返回条数
|
|
827
|
+
const runFullSearch = async (keyword) => {
|
|
828
|
+
const keywordText = String(keyword ?? query ?? "").trim();
|
|
829
|
+
if (keywordText.length < 2) return;
|
|
830
|
+
setFullSearching(true);
|
|
831
|
+
setFullResults(null);
|
|
832
|
+
try {
|
|
833
|
+
let res = await window.__scFetch("/api/session-search/query", {
|
|
834
|
+
method: "POST",
|
|
835
|
+
headers: { "Content-Type": CONTENT_TYPE_JSON },
|
|
836
|
+
body: JSON.stringify({ query: keywordText, limit: SEARCH_LIMIT }),
|
|
837
|
+
});
|
|
838
|
+
let body = res.ok ? await res.json() : null;
|
|
839
|
+
let usedFallback = false;
|
|
840
|
+
if (!body?.ok || !Array.isArray(body?.items)) {
|
|
841
|
+
// 官方搜索不可用 → 降级到本插件内置扫描
|
|
842
|
+
res = await window.__scFetch("/api/session-conductor/search", {
|
|
843
|
+
method: "POST",
|
|
844
|
+
headers: { "Content-Type": CONTENT_TYPE_JSON },
|
|
845
|
+
body: JSON.stringify({ query: keywordText, scope: "all" }),
|
|
846
|
+
});
|
|
847
|
+
body = res.ok ? await res.json() : null;
|
|
848
|
+
usedFallback = true;
|
|
849
|
+
}
|
|
850
|
+
if (!body?.ok) throw new Error(body?.error?.message ?? "search failed");
|
|
851
|
+
const hits = (body.items ?? []).map((h) => ({
|
|
852
|
+
sessionId: h.sessionId,
|
|
853
|
+
title: h.title ?? null,
|
|
854
|
+
cwd: h.cwd ?? null,
|
|
855
|
+
archived: h.archived === true,
|
|
856
|
+
running: h.running === true,
|
|
857
|
+
updatedAt: h.updatedAt ?? null,
|
|
858
|
+
matches: Array.isArray(h.matches) ? h.matches : (h.match ? [h.match] : []),
|
|
859
|
+
}));
|
|
860
|
+
setFullResults({ query: keywordText, hits, usedFallback });
|
|
861
|
+
} catch (cause) {
|
|
862
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
863
|
+
window.alert(t("error.action", { message }));
|
|
864
|
+
} finally {
|
|
865
|
+
setFullSearching(false);
|
|
866
|
+
}
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
// —— 批量删除选中 ——
|
|
870
|
+
const deleteSelected = async () => {
|
|
871
|
+
const ids = [...selected];
|
|
872
|
+
if (ids.length === 0) return;
|
|
873
|
+
const yes = window.confirm(`${t("deleteBatch.confirm.title", { n: ids.length })}\n\n${t("deleteBatch.confirm.body")}`);
|
|
874
|
+
if (!yes) return;
|
|
875
|
+
setBusy("__batch__");
|
|
876
|
+
try {
|
|
877
|
+
const res = await window.__scFetch("/api/session-conductor/delete-batch", {
|
|
878
|
+
method: "POST",
|
|
879
|
+
headers: { "Content-Type": CONTENT_TYPE_JSON },
|
|
880
|
+
body: JSON.stringify({ sessionIds: ids }),
|
|
881
|
+
});
|
|
882
|
+
const body = await res.json();
|
|
883
|
+
if (!body.ok) throw new Error(body.error?.message ?? "delete-batch failed");
|
|
884
|
+
const skippedText = body.skipped?.length > 0 ? t("deleteBatch.skipped", { n: body.skipped.length }) : "";
|
|
885
|
+
window.alert(t("deleteBatch.done", { deleted: body.deleted?.length ?? 0, skippedText }));
|
|
886
|
+
setSelected(new Set());
|
|
887
|
+
await refresh();
|
|
888
|
+
} catch (cause) {
|
|
889
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
890
|
+
window.alert(t("error.action", { message }));
|
|
891
|
+
} finally {
|
|
892
|
+
setBusy(null);
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
// —— 按条件删除(预览 + 执行)——
|
|
897
|
+
// 预览页离线模式(window.__BACKEND__ === ""):按条件删除在快照数据上本地计算预览
|
|
898
|
+
// (真实 GUI 走后端 API;预览页点预览可立即看到命中,无需后端)
|
|
899
|
+
const previewLocally = () => {
|
|
900
|
+
const ep = window.__ENDPOINTS__ || {};
|
|
901
|
+
const listBody = ep["/api/session-conductor/list"];
|
|
902
|
+
const vaBody = ep["/api/session-conductor/value-analysis"];
|
|
903
|
+
const rows = (listBody?.sessions ?? []).map((s) => ({
|
|
904
|
+
sessionId: s.id,
|
|
905
|
+
title: s.title,
|
|
906
|
+
cwd: s.cwd,
|
|
907
|
+
archived: s.archived === true,
|
|
908
|
+
updatedAt: typeof s.updatedAt === "number" ? s.updatedAt : null,
|
|
909
|
+
}));
|
|
910
|
+
const now = Date.now();
|
|
911
|
+
const lowIds = new Set((vaBody?.low ?? []).map((i) => i.id));
|
|
912
|
+
const prefix = rulePrefix.trim().replace(/\/+$/, "");
|
|
913
|
+
const days = Number(ruleDays) || 0;
|
|
914
|
+
const matched = rows.filter((m) => {
|
|
915
|
+
if (ruleArchivedOnly && !m.archived) return false;
|
|
916
|
+
if (days > 0) {
|
|
917
|
+
const updated = m.updatedAt;
|
|
918
|
+
if (!Number.isFinite(updated) || now - updated < days * 86400000) return false;
|
|
919
|
+
}
|
|
920
|
+
if (prefix !== "") {
|
|
921
|
+
const cwd = String(m.cwd ?? "").replace(/\/+$/, "");
|
|
922
|
+
if (cwd !== prefix && !cwd.startsWith(prefix + "/")) return false;
|
|
923
|
+
}
|
|
924
|
+
if (ruleLowValue && !lowIds.has(m.sessionId)) return false;
|
|
925
|
+
return true;
|
|
926
|
+
});
|
|
927
|
+
return matched;
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
const previewRuleDelete = async () => {
|
|
931
|
+
setBusy("__rule__");
|
|
932
|
+
setRulePreview(null);
|
|
933
|
+
if (typeof window.__BACKEND__ !== "undefined" && window.__BACKEND__ === "") {
|
|
934
|
+
// 预览页离线:本地计算
|
|
935
|
+
try {
|
|
936
|
+
const matched = previewLocally();
|
|
937
|
+
setRulePreview(matched);
|
|
938
|
+
if (matched.length === 0) window.alert(t("deleteByRule.noMatch"));
|
|
939
|
+
} catch (cause) {
|
|
940
|
+
window.alert(t("error.action", { message: cause instanceof Error ? cause.message : String(cause) }));
|
|
941
|
+
} finally {
|
|
942
|
+
setBusy(null);
|
|
943
|
+
}
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
try {
|
|
947
|
+
const res = await window.__scFetch("/api/session-conductor/delete-by-rule", {
|
|
948
|
+
method: "POST",
|
|
949
|
+
headers: { "Content-Type": CONTENT_TYPE_JSON },
|
|
950
|
+
body: JSON.stringify({
|
|
951
|
+
archivedOnly: ruleArchivedOnly,
|
|
952
|
+
inactiveDays: Number(ruleDays) || 0,
|
|
953
|
+
cwdPrefix: rulePrefix.trim(),
|
|
954
|
+
lowValue: ruleLowValue,
|
|
955
|
+
dryRun: true,
|
|
956
|
+
}),
|
|
957
|
+
});
|
|
958
|
+
const body = await res.json();
|
|
959
|
+
if (!body.ok) throw new Error(body.error?.message ?? "preview failed");
|
|
960
|
+
setRulePreview(body.matched ?? []);
|
|
961
|
+
if (!body.matched || body.matched.length === 0) window.alert(t("deleteByRule.noMatch"));
|
|
962
|
+
} catch (cause) {
|
|
963
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
964
|
+
window.alert(t("error.action", { message }));
|
|
965
|
+
} finally {
|
|
966
|
+
setBusy(null);
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
|
|
970
|
+
const runRuleDelete = async () => {
|
|
971
|
+
if (!rulePreview || rulePreview.length === 0) return;
|
|
972
|
+
// 预览页离线模式:不执行真实删除(破坏性操作需真实后端),提示到 DSH GUI
|
|
973
|
+
if (typeof window.__BACKEND__ !== "undefined" && window.__BACKEND__ === "") {
|
|
974
|
+
window.alert(t("deleteByRule.offlineHint"));
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
const yes = window.confirm(t("deleteByRule.preview", { n: rulePreview.length }));
|
|
978
|
+
if (!yes) return;
|
|
979
|
+
setBusy("__rule__");
|
|
980
|
+
try {
|
|
981
|
+
const res = await window.__scFetch("/api/session-conductor/delete-by-rule", {
|
|
982
|
+
method: "POST",
|
|
983
|
+
headers: { "Content-Type": CONTENT_TYPE_JSON },
|
|
984
|
+
body: JSON.stringify({
|
|
985
|
+
archivedOnly: ruleArchivedOnly,
|
|
986
|
+
inactiveDays: Number(ruleDays) || 0,
|
|
987
|
+
cwdPrefix: rulePrefix.trim(),
|
|
988
|
+
lowValue: ruleLowValue,
|
|
989
|
+
dryRun: false,
|
|
990
|
+
}),
|
|
991
|
+
});
|
|
992
|
+
const body = await res.json();
|
|
993
|
+
if (!body.ok) throw new Error(body.error?.message ?? "delete failed");
|
|
994
|
+
window.alert(t("deleteBatch.done", {
|
|
995
|
+
deleted: body.deleted?.length ?? 0,
|
|
996
|
+
skippedText: body.skipped?.length > 0 ? t("deleteBatch.skipped", { n: body.skipped.length }) : "",
|
|
997
|
+
}));
|
|
998
|
+
setRuleOpen(false);
|
|
999
|
+
setRulePreview(null);
|
|
1000
|
+
await refresh();
|
|
1001
|
+
} catch (cause) {
|
|
1002
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
1003
|
+
window.alert(t("error.action", { message }));
|
|
1004
|
+
} finally {
|
|
1005
|
+
setBusy(null);
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
|
|
1009
|
+
const rows = sessions ?? [];
|
|
1010
|
+
// 列表过滤:标题 / cwd / id / 状态词(大小写不敏感)
|
|
1011
|
+
const ql = query.trim().toLowerCase();
|
|
1012
|
+
const matchesQuery = (row) => {
|
|
1013
|
+
if (!ql) return true;
|
|
1014
|
+
const hay = [
|
|
1015
|
+
row.title ?? "", row.cwd ?? "", row.id ?? "",
|
|
1016
|
+
row.archived ? "已归档 archived" : "",
|
|
1017
|
+
row.running ? "运行中 running" : "",
|
|
1018
|
+
row.interruption ? "已中断 interrupted" : "",
|
|
1019
|
+
].join(" ").toLowerCase();
|
|
1020
|
+
return hay.includes(ql);
|
|
1021
|
+
};
|
|
1022
|
+
// tab=active 显示活跃会话(live 挂载中 或 running 运行中),归档的除外
|
|
1023
|
+
const shown = tab === "archived" ? rows.filter((row) => row.archived)
|
|
1024
|
+
: tab === "active" ? rows.filter((row) => !row.archived && (row.live === true || row.running === true))
|
|
1025
|
+
: rows;
|
|
1026
|
+
const filteredShown = shown.filter(matchesQuery);
|
|
1027
|
+
|
|
1028
|
+
/** 通用分桶(「全部会话」与「已归档」共用,避免两套分组实现各写一份):
|
|
1029
|
+
* keyOf(session) 返回桶键(空 → 落 rest「未分组」);makeBucket(key) 造桶对象;
|
|
1030
|
+
* 只保留有会话的桶(0 会话分组不显示)。 */
|
|
1031
|
+
const bucketSessions = (list, keyOf, makeBucket) => {
|
|
1032
|
+
const buckets = [];
|
|
1033
|
+
const index = new Map();
|
|
1034
|
+
const rest = [];
|
|
1035
|
+
for (const s of list) {
|
|
1036
|
+
const key = keyOf(s);
|
|
1037
|
+
if (!key) { rest.push(s); continue; }
|
|
1038
|
+
if (!index.has(key)) {
|
|
1039
|
+
const b = makeBucket(key);
|
|
1040
|
+
index.set(key, b);
|
|
1041
|
+
buckets.push(b);
|
|
1042
|
+
}
|
|
1043
|
+
index.get(key).sessions.push(s);
|
|
1044
|
+
}
|
|
1045
|
+
return { buckets, rest };
|
|
1046
|
+
};
|
|
1047
|
+
|
|
1048
|
+
/** 按工作区分组:会话归入「其 cwd 的最深祖先分组」——
|
|
1049
|
+
* cwd === 分组 path,或 cwd 位于分组 path 目录之下(path 是 cwd 的祖先目录);
|
|
1050
|
+
* 多个分组匹配时取 path 最深者(如 move-test 子目录会话归 move-test,其余归 workspace 根)。
|
|
1051
|
+
* 只保留有会话的分组(0 会话分组不显示);无任何匹配的归入「未分组」。 */
|
|
1052
|
+
const groupByWorkspace = (list) => {
|
|
1053
|
+
if (!Array.isArray(groups)) return null; // host 无分组 API → 平铺
|
|
1054
|
+
const norm = (p) => String(p ?? "").replace(/\/+$/, "");
|
|
1055
|
+
/** 会话归属的分组对象(最深祖先;无匹配返回 null)。 */
|
|
1056
|
+
const groupOfSession = (s) => {
|
|
1057
|
+
const cwd = norm(s.cwd);
|
|
1058
|
+
if (!cwd) return null;
|
|
1059
|
+
let best = null;
|
|
1060
|
+
let bestLen = -1;
|
|
1061
|
+
for (const w of groups) {
|
|
1062
|
+
const p = norm(w.path);
|
|
1063
|
+
if (p !== "" && (cwd === p || cwd.startsWith(p + "/")) && p.length > bestLen) {
|
|
1064
|
+
best = w;
|
|
1065
|
+
bestLen = p.length;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
return best;
|
|
1069
|
+
};
|
|
1070
|
+
return bucketSessions(
|
|
1071
|
+
list,
|
|
1072
|
+
(s) => norm(groupOfSession(s)?.path ?? ""),
|
|
1073
|
+
(path) => {
|
|
1074
|
+
const workspaceEntry = groups.find((x) => norm(x.path) === path) ?? {};
|
|
1075
|
+
return { workspaceId: workspaceEntry.workspaceId, title: workspaceEntry.title, path: workspaceEntry.path, sessions: [] };
|
|
1076
|
+
},
|
|
1077
|
+
);
|
|
1078
|
+
};
|
|
1079
|
+
|
|
1080
|
+
/** 已归档视图分组:**复用上面同一套工作区分组**。
|
|
1081
|
+
* 历史实现按 `s.archiveWs` 分桶,但该字段实测**恒为空**(66 个已归档会话全部为空),
|
|
1082
|
+
* 于是所有会话都落进「未分组」,看起来就是「已归档里没有正确分组」。
|
|
1083
|
+
* 归档会话的 cwd 是有效的(数据层保留),故直接按 cwd 的工作区分组即可正确分节。
|
|
1084
|
+
* host 无分组 API 时退化为平铺(与「全部会话」同口径)。 */
|
|
1085
|
+
const groupByArchiveWs = (list) => groupByWorkspace(list);
|
|
1086
|
+
|
|
1087
|
+
const grouped = tab === "all" ? groupByWorkspace(filteredShown) : tab === "archived" ? groupByArchiveWs(filteredShown) : null;
|
|
1088
|
+
|
|
1089
|
+
const toggleCollapse = (key) => {
|
|
1090
|
+
setCollapsed((prev) => {
|
|
1091
|
+
const next = new Set(prev);
|
|
1092
|
+
if (next.has(key)) next.delete(key);
|
|
1093
|
+
else next.add(key);
|
|
1094
|
+
return next;
|
|
1095
|
+
});
|
|
1096
|
+
};
|
|
1097
|
+
|
|
1098
|
+
const renderRow = (session) => {
|
|
1099
|
+
// 已归档会话标题数据层带「[工作区名] 」前缀,显示层剥离(只显示原标题)
|
|
1100
|
+
let label = session.title || basenameOf(session.cwd) || session.id;
|
|
1101
|
+
if (session.archived && session.archiveWs && label.startsWith(`[${session.archiveWs}] `)) {
|
|
1102
|
+
label = label.slice(session.archiveWs.length + 3); // 去掉 "[工作区名] "
|
|
1103
|
+
}
|
|
1104
|
+
const meta = [session.cwd ? basenameOf(session.cwd) : "", timeAgo(session.updatedAt, t)]
|
|
1105
|
+
.filter(Boolean)
|
|
1106
|
+
.join(" · ");
|
|
1107
|
+
// running 由顶部「● 运行中」独立显示(含 live),archived/interrupted 走 badge
|
|
1108
|
+
const badge = session.archived ? "archived" : session.interruption ? "interrupted" : null;
|
|
1109
|
+
const processing = busy === session.id || session.continueRunning === true;
|
|
1110
|
+
const interruptionTitle = session.interruption ? t("interruption.tooltip", { message: session.interruption.message ?? session.interruption.kind }) : undefined;
|
|
1111
|
+
// 行内改为纵向卡片——顶部(批量勾选+状态徽章)/ 中间(内容)/ 下方(操作集中)
|
|
1112
|
+
return (0, react_jsx_runtime.jsx)("div", {
|
|
1113
|
+
key: session.id, // map 渲染列表时必需(filteredShown.map / g.sessions.map)
|
|
1114
|
+
style: S.row,
|
|
1115
|
+
onClick: () => {
|
|
1116
|
+
if (onOpenSession && !processing) {
|
|
1117
|
+
onOpenSession(session.id);
|
|
1118
|
+
setOpen(false);
|
|
1119
|
+
}
|
|
1120
|
+
},
|
|
1121
|
+
children: [
|
|
1122
|
+
// —— 顶部:批量勾选 + 状态徽章 ——
|
|
1123
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
1124
|
+
style: S.rowTop,
|
|
1125
|
+
children: [
|
|
1126
|
+
(0, react_jsx_runtime.jsx)("label", {
|
|
1127
|
+
style: { display: "flex", alignItems: "center", cursor: "pointer", flex: "none" },
|
|
1128
|
+
title: t("deleteBatch.action", { n: 1 }),
|
|
1129
|
+
onClick: (event) => event.stopPropagation(),
|
|
1130
|
+
children: (0, react_jsx_runtime.jsx)("input", {
|
|
1131
|
+
type: "checkbox",
|
|
1132
|
+
checked: selected.has(session.id),
|
|
1133
|
+
onChange: (event) => {
|
|
1134
|
+
setSelected((prev) => {
|
|
1135
|
+
const next = new Set(prev);
|
|
1136
|
+
if (event.target.checked) next.add(session.id);
|
|
1137
|
+
else next.delete(session.id);
|
|
1138
|
+
return next;
|
|
1139
|
+
});
|
|
1140
|
+
},
|
|
1141
|
+
}),
|
|
1142
|
+
}),
|
|
1143
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
1144
|
+
style: S.rowTopBadges,
|
|
1145
|
+
children: [
|
|
1146
|
+
session.running === true && (0, react_jsx_runtime.jsx)("span", {
|
|
1147
|
+
style: { ...S.badge("running"), display: "inline-flex", alignItems: "center", gap: 4 },
|
|
1148
|
+
children: ["● ", t("badge.running")],
|
|
1149
|
+
}),
|
|
1150
|
+
session.live === true && !session.running && (0, react_jsx_runtime.jsx)("span", {
|
|
1151
|
+
style: { ...S.badge("running"), display: "inline-flex", alignItems: "center", gap: 4 },
|
|
1152
|
+
children: [t("badge.live")],
|
|
1153
|
+
}),
|
|
1154
|
+
badge !== null && (0, react_jsx_runtime.jsx)("span", {
|
|
1155
|
+
style: S.badge(badge),
|
|
1156
|
+
title: badge === "interrupted" ? interruptionTitle : undefined,
|
|
1157
|
+
children: badge === "archived" ? t("badge.archived") : t("badge.interrupted"),
|
|
1158
|
+
}),
|
|
1159
|
+
],
|
|
1160
|
+
}),
|
|
1161
|
+
],
|
|
1162
|
+
}),
|
|
1163
|
+
// —— 中间:会话内容 ——
|
|
1164
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
1165
|
+
style: S.rowMain,
|
|
1166
|
+
children: [
|
|
1167
|
+
(0, react_jsx_runtime.jsx)("div", {
|
|
1168
|
+
style: { ...S.rowTitle, gap: 0 },
|
|
1169
|
+
children: (0, react_jsx_runtime.jsx)("span", {
|
|
1170
|
+
style: { overflow: "hidden", display: "-webkit-box", WebkitLineClamp: 3, WebkitBoxOrient: "vertical", wordBreak: "break-word" },
|
|
1171
|
+
children: label,
|
|
1172
|
+
}),
|
|
1173
|
+
}),
|
|
1174
|
+
(0, react_jsx_runtime.jsx)("div", {
|
|
1175
|
+
style: S.rowMeta,
|
|
1176
|
+
children: meta || session.id,
|
|
1177
|
+
}),
|
|
1178
|
+
(0, react_jsx_runtime.jsx)("div", {
|
|
1179
|
+
style: S.rowSid,
|
|
1180
|
+
title: "session id",
|
|
1181
|
+
children: session.id,
|
|
1182
|
+
}),
|
|
1183
|
+
],
|
|
1184
|
+
}),
|
|
1185
|
+
// —— 下方:操作集中(开关 + 按钮)——
|
|
1186
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
1187
|
+
style: S.rowOps,
|
|
1188
|
+
onClick: (event) => event.stopPropagation(),
|
|
1189
|
+
children: [
|
|
1190
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
1191
|
+
style: S.opsToggles,
|
|
1192
|
+
children: [
|
|
1193
|
+
(0, react_jsx_runtime.jsxs)("label", {
|
|
1194
|
+
style: S.autoToggle,
|
|
1195
|
+
title: t("help.autoRename.title"),
|
|
1196
|
+
children: [
|
|
1197
|
+
(0, react_jsx_runtime.jsx)("input", {
|
|
1198
|
+
type: "checkbox",
|
|
1199
|
+
checked: session.autoRename === true,
|
|
1200
|
+
disabled: processing,
|
|
1201
|
+
onChange: (event) => toggleAutoRename(session.id, event.target.checked),
|
|
1202
|
+
}),
|
|
1203
|
+
(0, react_jsx_runtime.jsx)("span", { children: t("action.autoRename") }),
|
|
1204
|
+
],
|
|
1205
|
+
}),
|
|
1206
|
+
(0, react_jsx_runtime.jsxs)("label", {
|
|
1207
|
+
style: S.autoToggle,
|
|
1208
|
+
title: t("help.autoContinue.title"),
|
|
1209
|
+
children: [
|
|
1210
|
+
(0, react_jsx_runtime.jsx)("input", {
|
|
1211
|
+
type: "checkbox",
|
|
1212
|
+
checked: session.autoContinue === true,
|
|
1213
|
+
disabled: processing,
|
|
1214
|
+
onChange: (event) => toggleAutoContinue(session.id, event.target.checked),
|
|
1215
|
+
}),
|
|
1216
|
+
(0, react_jsx_runtime.jsx)("span", { children: t("action.autoContinue") }),
|
|
1217
|
+
],
|
|
1218
|
+
}),
|
|
1219
|
+
],
|
|
1220
|
+
}),
|
|
1221
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
1222
|
+
style: S.opsButtons,
|
|
1223
|
+
children: [
|
|
1224
|
+
session.autoRename === true &&
|
|
1225
|
+
(0, react_jsx_runtime.jsx)("button", {
|
|
1226
|
+
style: S.actionBtn(false),
|
|
1227
|
+
disabled: processing,
|
|
1228
|
+
onClick: () => analyzeNow(session.id),
|
|
1229
|
+
children: processing ? t("action.busy") : t("action.analyze"),
|
|
1230
|
+
}),
|
|
1231
|
+
session.interruption !== null && session.interruption !== undefined && !session.running &&
|
|
1232
|
+
(0, react_jsx_runtime.jsx)("button", {
|
|
1233
|
+
style: { ...S.actionBtn(false), borderColor: "var(--dsw-state-warning-primary, #e6a23c)", color: "var(--dsw-state-warning-primary, #e6a23c)" },
|
|
1234
|
+
disabled: processing,
|
|
1235
|
+
onClick: () => continueNow(session.id),
|
|
1236
|
+
children: session.continueRunning === true ? t("action.continueRunning") : t("action.continue"),
|
|
1237
|
+
}),
|
|
1238
|
+
session.live === true && !session.running && session.continueRunning !== true &&
|
|
1239
|
+
(0, react_jsx_runtime.jsx)("button", {
|
|
1240
|
+
style: S.actionBtn(false),
|
|
1241
|
+
disabled: processing,
|
|
1242
|
+
onClick: () => releaseNow(session.id),
|
|
1243
|
+
children: processing ? t("action.busy") : t("action.release"),
|
|
1244
|
+
}),
|
|
1245
|
+
session.archived
|
|
1246
|
+
? (0, react_jsx_runtime.jsx)("button", {
|
|
1247
|
+
style: S.actionBtn(false),
|
|
1248
|
+
disabled: processing,
|
|
1249
|
+
onClick: () => act(session.id, "unarchive"),
|
|
1250
|
+
children: processing ? t("action.busy") : t("action.restore"),
|
|
1251
|
+
})
|
|
1252
|
+
: (0, react_jsx_runtime.jsx)("button", {
|
|
1253
|
+
style: S.actionBtn(false),
|
|
1254
|
+
disabled: processing,
|
|
1255
|
+
onClick: () => act(session.id, "archive"),
|
|
1256
|
+
children: processing ? t("action.busy") : t("action.archive"),
|
|
1257
|
+
}),
|
|
1258
|
+
!session.running && !session.continueRunning &&
|
|
1259
|
+
(0, react_jsx_runtime.jsx)("button", {
|
|
1260
|
+
style: S.actionBtn(false),
|
|
1261
|
+
disabled: processing,
|
|
1262
|
+
title: t("help.undo.title"),
|
|
1263
|
+
onClick: () => undoLast(session),
|
|
1264
|
+
children: processing ? t("action.busy") : t("action.undo"),
|
|
1265
|
+
}),
|
|
1266
|
+
(0, react_jsx_runtime.jsx)("button", {
|
|
1267
|
+
style: S.actionBtn(true),
|
|
1268
|
+
disabled: processing || session.running,
|
|
1269
|
+
onClick: () => confirmDelete(session),
|
|
1270
|
+
children: processing ? t("action.busy") : t("action.delete"),
|
|
1271
|
+
}),
|
|
1272
|
+
],
|
|
1273
|
+
}),
|
|
1274
|
+
],
|
|
1275
|
+
}),
|
|
1276
|
+
],
|
|
1277
|
+
});
|
|
1278
|
+
};
|
|
1279
|
+
|
|
1280
|
+
const renderHelp = () => (0, react_jsx_runtime.jsxs)("div", {
|
|
1281
|
+
children: [
|
|
1282
|
+
(0, react_jsx_runtime.jsx)("p", {
|
|
1283
|
+
style: S.hint,
|
|
1284
|
+
children: t("help.tip"),
|
|
1285
|
+
}),
|
|
1286
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
1287
|
+
style: { ...S.helpBlock, display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" },
|
|
1288
|
+
children: [
|
|
1289
|
+
(0, react_jsx_runtime.jsx)("p", {
|
|
1290
|
+
style: { ...S.helpDesc, margin: 0, flex: 1, minWidth: HELP_DESC_MIN_WIDTH },
|
|
1291
|
+
children: t("help.repair.desc"),
|
|
1292
|
+
}),
|
|
1293
|
+
(0, react_jsx_runtime.jsx)("button", {
|
|
1294
|
+
style: S.actionBtn(false),
|
|
1295
|
+
disabled: busy !== null,
|
|
1296
|
+
onClick: scanAndRepair,
|
|
1297
|
+
children: busy !== null ? t("action.busy") : t("action.repair"),
|
|
1298
|
+
}),
|
|
1299
|
+
],
|
|
1300
|
+
}),
|
|
1301
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
1302
|
+
style: { ...S.helpBlock, display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" },
|
|
1303
|
+
children: [
|
|
1304
|
+
(0, react_jsx_runtime.jsx)("p", {
|
|
1305
|
+
style: { ...S.helpDesc, margin: 0, flex: 1, minWidth: HELP_DESC_MIN_WIDTH },
|
|
1306
|
+
children: t("help.repairEio.desc"),
|
|
1307
|
+
}),
|
|
1308
|
+
(0, react_jsx_runtime.jsx)("button", {
|
|
1309
|
+
style: S.actionBtn(false),
|
|
1310
|
+
disabled: busy !== null,
|
|
1311
|
+
onClick: scanAndRepairEio,
|
|
1312
|
+
children: busy !== null ? t("action.busy") : t("action.repairEio"),
|
|
1313
|
+
}),
|
|
1314
|
+
],
|
|
1315
|
+
}),
|
|
1316
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
1317
|
+
style: { ...S.helpBlock, display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" },
|
|
1318
|
+
children: [
|
|
1319
|
+
(0, react_jsx_runtime.jsx)("p", {
|
|
1320
|
+
style: { ...S.helpDesc, margin: 0, flex: 1, minWidth: HELP_DESC_MIN_WIDTH },
|
|
1321
|
+
children: t("help.repairDual.desc"),
|
|
1322
|
+
}),
|
|
1323
|
+
(0, react_jsx_runtime.jsx)("button", {
|
|
1324
|
+
style: S.actionBtn(false),
|
|
1325
|
+
disabled: busy !== null,
|
|
1326
|
+
onClick: scanAndRepairDual,
|
|
1327
|
+
children: busy !== null ? t("action.busy") : t("action.repairDual"),
|
|
1328
|
+
}),
|
|
1329
|
+
],
|
|
1330
|
+
}),
|
|
1331
|
+
(0, react_jsx_runtime.jsx)("p", {
|
|
1332
|
+
style: { ...S.hint, fontWeight: LABEL_FONT_WEIGHT },
|
|
1333
|
+
children: t("help.works"),
|
|
1334
|
+
}),
|
|
1335
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
1336
|
+
style: S.helpBlock,
|
|
1337
|
+
children: [
|
|
1338
|
+
(0, react_jsx_runtime.jsx)("p", { style: S.helpTitle, children: t("help.autoRename.title") }),
|
|
1339
|
+
(0, react_jsx_runtime.jsx)("p", { style: S.helpDesc, children: t("help.autoRename.desc") }),
|
|
1340
|
+
],
|
|
1341
|
+
}),
|
|
1342
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
1343
|
+
style: S.helpBlock,
|
|
1344
|
+
children: [
|
|
1345
|
+
(0, react_jsx_runtime.jsx)("p", { style: S.helpTitle, children: t("help.autoContinue.title") }),
|
|
1346
|
+
(0, react_jsx_runtime.jsx)("p", { style: S.helpDesc, children: t("help.autoContinue.desc") }),
|
|
1347
|
+
],
|
|
1348
|
+
}),
|
|
1349
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
1350
|
+
style: S.helpBlock,
|
|
1351
|
+
children: [
|
|
1352
|
+
(0, react_jsx_runtime.jsx)("p", { style: S.helpTitle, children: t("help.release.title") }),
|
|
1353
|
+
(0, react_jsx_runtime.jsx)("p", { style: S.helpDesc, children: t("help.release.desc") }),
|
|
1354
|
+
],
|
|
1355
|
+
}),
|
|
1356
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
1357
|
+
style: S.helpBlock,
|
|
1358
|
+
children: [
|
|
1359
|
+
(0, react_jsx_runtime.jsx)("p", { style: S.helpTitle, children: t("help.delete.title") }),
|
|
1360
|
+
(0, react_jsx_runtime.jsx)("p", { style: S.helpDesc, children: t("help.delete.desc") }),
|
|
1361
|
+
],
|
|
1362
|
+
}),
|
|
1363
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
1364
|
+
style: S.helpBlock,
|
|
1365
|
+
children: [
|
|
1366
|
+
(0, react_jsx_runtime.jsx)("p", { style: S.helpTitle, children: t("help.grouped.title") }),
|
|
1367
|
+
(0, react_jsx_runtime.jsx)("p", { style: S.helpDesc, children: t("help.grouped.desc") }),
|
|
1368
|
+
],
|
|
1369
|
+
}),
|
|
1370
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
1371
|
+
style: S.helpBlock,
|
|
1372
|
+
children: [
|
|
1373
|
+
(0, react_jsx_runtime.jsx)("p", { style: S.helpTitle, children: t("help.newSession.title") }),
|
|
1374
|
+
(0, react_jsx_runtime.jsx)("p", { style: S.helpDesc, children: t("help.newSession.desc") }),
|
|
1375
|
+
],
|
|
1376
|
+
}),
|
|
1377
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
1378
|
+
style: S.helpBlock,
|
|
1379
|
+
children: [
|
|
1380
|
+
(0, react_jsx_runtime.jsx)("p", { style: S.helpTitle, children: t("help.value.title") }),
|
|
1381
|
+
(0, react_jsx_runtime.jsx)("p", { style: S.helpDesc, children: t("help.value.desc") }),
|
|
1382
|
+
],
|
|
1383
|
+
}),
|
|
1384
|
+
],
|
|
1385
|
+
});
|
|
1386
|
+
|
|
1387
|
+
return (0, react_jsx_runtime.jsxs)(react.Fragment, {
|
|
1388
|
+
children: [
|
|
1389
|
+
(0, react_jsx_runtime.jsx)("button", {
|
|
1390
|
+
title: t("trigger.aria"),
|
|
1391
|
+
"aria-label": t("trigger.aria"),
|
|
1392
|
+
onClick: () => setOpen(true),
|
|
1393
|
+
style: S.trigger(wide),
|
|
1394
|
+
children: [
|
|
1395
|
+
(0, react_jsx_runtime.jsx)(SessionIcon, { size: 16 }),
|
|
1396
|
+
wide ? t("trigger.label") : null,
|
|
1397
|
+
],
|
|
1398
|
+
}),
|
|
1399
|
+
open &&
|
|
1400
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
1401
|
+
style: S.overlay,
|
|
1402
|
+
children: [
|
|
1403
|
+
(0, react_jsx_runtime.jsx)("div", {
|
|
1404
|
+
style: S.backdrop,
|
|
1405
|
+
onClick: () => setOpen(false),
|
|
1406
|
+
}),
|
|
1407
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
1408
|
+
style: S.panel,
|
|
1409
|
+
children: [
|
|
1410
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
1411
|
+
style: S.header,
|
|
1412
|
+
children: [
|
|
1413
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
1414
|
+
children: [
|
|
1415
|
+
(0, react_jsx_runtime.jsx)("h2", { style: S.title, children: t("panel.title") }),
|
|
1416
|
+
(0, react_jsx_runtime.jsx)("p", { style: S.subtitle, children: t("panel.subtitle") }),
|
|
1417
|
+
],
|
|
1418
|
+
}),
|
|
1419
|
+
(0, react_jsx_runtime.jsx)("button", {
|
|
1420
|
+
style: S.close,
|
|
1421
|
+
"aria-label": "close",
|
|
1422
|
+
onClick: () => setOpen(false),
|
|
1423
|
+
children: "✕",
|
|
1424
|
+
}),
|
|
1425
|
+
],
|
|
1426
|
+
}),
|
|
1427
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
1428
|
+
style: S.tabs,
|
|
1429
|
+
children: [
|
|
1430
|
+
(0, react_jsx_runtime.jsx)("button", {
|
|
1431
|
+
style: S.tab(tab === "all"),
|
|
1432
|
+
onClick: () => setTab("all"),
|
|
1433
|
+
children: t("tab.all"),
|
|
1434
|
+
}),
|
|
1435
|
+
(0, react_jsx_runtime.jsx)("button", {
|
|
1436
|
+
style: S.tab(tab === "archived"),
|
|
1437
|
+
onClick: () => setTab("archived"),
|
|
1438
|
+
children: `${t("tab.archived")} (${rows.filter((row) => row.archived).length})`,
|
|
1439
|
+
}),
|
|
1440
|
+
(0, react_jsx_runtime.jsx)("button", {
|
|
1441
|
+
style: S.tab(tab === "active"),
|
|
1442
|
+
onClick: () => setTab("active"),
|
|
1443
|
+
children: `${t("tab.active")} (${rows.filter((row) => !row.archived && (row.live === true || row.running === true)).length})`,
|
|
1444
|
+
}),
|
|
1445
|
+
(0, react_jsx_runtime.jsx)("button", {
|
|
1446
|
+
style: S.tab(tab === "help"),
|
|
1447
|
+
onClick: () => setTab("help"),
|
|
1448
|
+
children: t("tab.help"),
|
|
1449
|
+
}),
|
|
1450
|
+
],
|
|
1451
|
+
}),
|
|
1452
|
+
(0, react_jsx_runtime.jsx)("div", {
|
|
1453
|
+
style: { display: "flex", gap: 6, padding: PADDING_BAR, alignItems: "center", flexWrap: "wrap" },
|
|
1454
|
+
children: [
|
|
1455
|
+
(0, react_jsx_runtime.jsx)("input", {
|
|
1456
|
+
type: "text",
|
|
1457
|
+
value: query,
|
|
1458
|
+
placeholder: t("search.placeholder"),
|
|
1459
|
+
style: { flex: 1, minWidth: CONTROL_MIN_WIDTH, height: CONTROL_HEIGHT, borderRadius: CONTROL_RADIUS, border: CSS_BORDER_L2, background: CSS_BG_BASE, color: CSS_LABEL_PRIMARY, padding: "0 10px", fontSize: FONT_SIZE_SMALL, outline: "none" },
|
|
1460
|
+
onChange: (event) => { setQuery(event.target.value); setFullResults(null); },
|
|
1461
|
+
onKeyDown: (event) => {
|
|
1462
|
+
if (event.key === "Enter") runFullSearch(query);
|
|
1463
|
+
},
|
|
1464
|
+
}),
|
|
1465
|
+
(0, react_jsx_runtime.jsx)("button", {
|
|
1466
|
+
style: S.actionBtn(false),
|
|
1467
|
+
disabled: fullSearching || busy !== null,
|
|
1468
|
+
onClick: () => runFullSearch(query),
|
|
1469
|
+
children: fullSearching ? t("search.fullBusy") : t("search.full"),
|
|
1470
|
+
}),
|
|
1471
|
+
(0, react_jsx_runtime.jsx)("button", {
|
|
1472
|
+
style: { ...S.actionBtn(true), borderColor: CSS_DANGER, color: CSS_DANGER },
|
|
1473
|
+
disabled: selected.size === 0 || busy !== null,
|
|
1474
|
+
onClick: () => deleteSelected(),
|
|
1475
|
+
children: t("deleteBatch.action", { n: selected.size }),
|
|
1476
|
+
}),
|
|
1477
|
+
(0, react_jsx_runtime.jsx)("button", {
|
|
1478
|
+
style: S.actionBtn(false),
|
|
1479
|
+
disabled: busy !== null,
|
|
1480
|
+
onClick: () => { setRuleOpen((v) => !v); setRulePreview(null); },
|
|
1481
|
+
children: t("deleteByRule.action"),
|
|
1482
|
+
}),
|
|
1483
|
+
(0, react_jsx_runtime.jsx)("button", {
|
|
1484
|
+
style: { ...S.actionBtn(false), marginLeft: "auto" },
|
|
1485
|
+
disabled: busy !== null,
|
|
1486
|
+
onClick: () => releaseAll(),
|
|
1487
|
+
children: busy === "__all__" ? t("action.releaseAllBusy") : t("action.releaseAll"),
|
|
1488
|
+
}),
|
|
1489
|
+
],
|
|
1490
|
+
}),
|
|
1491
|
+
// —— 按条件删除表单(展开式) ——
|
|
1492
|
+
ruleOpen &&
|
|
1493
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
1494
|
+
style: { display: "flex", gap: 8, padding: PADDING_BAR, alignItems: "center", flexWrap: "wrap", fontSize: FONT_SIZE_SMALL },
|
|
1495
|
+
children: [
|
|
1496
|
+
(0, react_jsx_runtime.jsxs)("label", {
|
|
1497
|
+
style: { display: "flex", alignItems: "center", gap: 4, color: CSS_LABEL_SECONDARY },
|
|
1498
|
+
children: [
|
|
1499
|
+
(0, react_jsx_runtime.jsx)("input", { type: "checkbox", checked: ruleArchivedOnly, onChange: (e) => setRuleArchivedOnly(e.target.checked) }),
|
|
1500
|
+
t("deleteByRule.label.archivedOnly"),
|
|
1501
|
+
],
|
|
1502
|
+
}),
|
|
1503
|
+
(0, react_jsx_runtime.jsxs)("label", {
|
|
1504
|
+
style: { display: "flex", alignItems: "center", gap: 4, color: CSS_LABEL_SECONDARY },
|
|
1505
|
+
title: t("deleteByRule.label.lowValue"),
|
|
1506
|
+
children: [
|
|
1507
|
+
(0, react_jsx_runtime.jsx)("input", { type: "checkbox", checked: ruleLowValue, onChange: (e) => setRuleLowValue(e.target.checked) }),
|
|
1508
|
+
t("deleteByRule.label.lowValue"),
|
|
1509
|
+
],
|
|
1510
|
+
}),
|
|
1511
|
+
(0, react_jsx_runtime.jsxs)("label", {
|
|
1512
|
+
style: { display: "flex", alignItems: "center", gap: 4, color: CSS_LABEL_SECONDARY, flexWrap: "nowrap" },
|
|
1513
|
+
children: [
|
|
1514
|
+
t("deleteByRule.label.inactiveDays.pre"),
|
|
1515
|
+
(0, react_jsx_runtime.jsx)("input", {
|
|
1516
|
+
type: "number", min: 0, max: 3650, value: ruleDays,
|
|
1517
|
+
style: { width: RULE_DAYS_INPUT_WIDTH, height: SMALL_CONTROL_HEIGHT, borderRadius: CONTROL_RADIUS, border: CSS_BORDER_L2, background: CSS_BG_BASE, color: CSS_LABEL_PRIMARY, padding: "0 6px", fontSize: FONT_SIZE_SMALL },
|
|
1518
|
+
onChange: (e) => setRuleDays(e.target.value),
|
|
1519
|
+
}),
|
|
1520
|
+
t("deleteByRule.label.inactiveDays.post"),
|
|
1521
|
+
],
|
|
1522
|
+
}),
|
|
1523
|
+
(0, react_jsx_runtime.jsx)("input", {
|
|
1524
|
+
type: "text", value: rulePrefix, placeholder: t("deleteByRule.label.cwdPrefix"),
|
|
1525
|
+
style: { flex: 1, minWidth: RULE_PREFIX_MIN_WIDTH, height: SMALL_CONTROL_HEIGHT, borderRadius: CONTROL_RADIUS, border: CSS_BORDER_L2, background: CSS_BG_BASE, color: CSS_LABEL_PRIMARY, padding: "0 8px", fontSize: FONT_SIZE_SMALL, outline: "none" },
|
|
1526
|
+
onChange: (e) => { setRulePrefix(e.target.value); setRulePreview(null); },
|
|
1527
|
+
}),
|
|
1528
|
+
(0, react_jsx_runtime.jsx)("button", {
|
|
1529
|
+
style: S.actionBtn(false),
|
|
1530
|
+
disabled: busy !== null,
|
|
1531
|
+
onClick: previewRuleDelete,
|
|
1532
|
+
children: t("deleteByRule.previewBtn"),
|
|
1533
|
+
}),
|
|
1534
|
+
rulePreview !== null && rulePreview.length > 0 &&
|
|
1535
|
+
(0, react_jsx_runtime.jsx)("button", {
|
|
1536
|
+
style: { ...S.actionBtn(true), borderColor: CSS_DANGER, color: CSS_DANGER },
|
|
1537
|
+
disabled: busy !== null,
|
|
1538
|
+
onClick: runRuleDelete,
|
|
1539
|
+
children: t("deleteByRule.run") + ` (${rulePreview.length})`,
|
|
1540
|
+
}),
|
|
1541
|
+
rulePreview !== null && rulePreview.length > 0 &&
|
|
1542
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
1543
|
+
style: { color: CSS_LABEL_TERTIARY, fontSize: 12 },
|
|
1544
|
+
children: t("search.hits", { n: rulePreview.length }),
|
|
1545
|
+
}),
|
|
1546
|
+
],
|
|
1547
|
+
}),
|
|
1548
|
+
(0, react_jsx_runtime.jsx)("div", {
|
|
1549
|
+
style: S.body,
|
|
1550
|
+
children: tab === "help"
|
|
1551
|
+
? renderHelp()
|
|
1552
|
+
: (0, react_jsx_runtime.jsxs)(react.Fragment, {
|
|
1553
|
+
children: [
|
|
1554
|
+
// —— 全文搜索结果区(官方 FTS5 优先,自研兜底) ——
|
|
1555
|
+
fullResults !== null &&
|
|
1556
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
1557
|
+
style: { margin: PADDING_BAR, borderRadius: 10, border: "1px solid var(--dsw-alias-border-l1, rgba(128,128,128,.3))", overflow: "hidden" },
|
|
1558
|
+
children: [
|
|
1559
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
1560
|
+
style: { display: "flex", alignItems: "center", gap: 8, padding: "6px 10px", background: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.08))" },
|
|
1561
|
+
children: [
|
|
1562
|
+
(0, react_jsx_runtime.jsx)("span", { style: { fontSize: FONT_SIZE_SMALL, fontWeight: FONT_WEIGHT_SEMIBOLD, color: CSS_LABEL_PRIMARY }, children: `${t("search.hits", { n: fullResults.hits.length })}:${fullResults.query}` }),
|
|
1563
|
+
fullResults.usedFallback && (0, react_jsx_runtime.jsx)("span", { style: { fontSize: 11.5, color: CSS_LABEL_TERTIARY }, children: t("search.fallback") }),
|
|
1564
|
+
(0, react_jsx_runtime.jsx)("button", {
|
|
1565
|
+
style: { ...S.actionBtn(false), marginLeft: "auto" },
|
|
1566
|
+
onClick: () => setFullResults(null),
|
|
1567
|
+
children: t("search.close"),
|
|
1568
|
+
}),
|
|
1569
|
+
],
|
|
1570
|
+
}),
|
|
1571
|
+
fullResults.hits.length === 0
|
|
1572
|
+
? (0, react_jsx_runtime.jsx)("div", { style: { ...S.empty, margin: 0, padding: "14px 0" }, children: t("search.noResult") })
|
|
1573
|
+
: fullResults.hits.map((hit) => (0, react_jsx_runtime.jsxs)("div", {
|
|
1574
|
+
style: { padding: "6px 10px", borderTop: "1px solid var(--dsw-alias-border-l1, rgba(128,128,128,.15))", cursor: "pointer" },
|
|
1575
|
+
onClick: () => { if (onOpenSession) { onOpenSession(hit.sessionId); setOpen(false); } },
|
|
1576
|
+
children: [
|
|
1577
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
1578
|
+
style: { fontSize: FONT_SIZE_SMALL, fontWeight: FONT_WEIGHT_SEMIBOLD, color: CSS_LABEL_PRIMARY, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
|
|
1579
|
+
children: [hit.title || basenameOf(hit.cwd) || hit.sessionId, hit.archived ? ` · ${t("badge.archived")}` : "", hit.running ? ` · ${t("badge.running")}` : ""],
|
|
1580
|
+
}),
|
|
1581
|
+
(0, react_jsx_runtime.jsx)("div", { style: { fontSize: 11.5, color: CSS_LABEL_TERTIARY, marginBottom: 2, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: hit.cwd || hit.sessionId }),
|
|
1582
|
+
(hit.matches ?? []).map((m, mi) => (0, react_jsx_runtime.jsxs)("div", {
|
|
1583
|
+
style: { fontSize: 12, lineHeight: 1.5, color: CSS_LABEL_SECONDARY },
|
|
1584
|
+
children: [
|
|
1585
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
1586
|
+
style: { display: "inline-block", marginRight: 6, padding: "0 5px", borderRadius: 4, fontSize: 11, background: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12))", color: CSS_LABEL_TERTIARY },
|
|
1587
|
+
children: m.role === "user" ? t("search.matchRole.user") : m.role === "assistant" ? t("search.matchRole.assistant") : (m.surface ?? m.type ?? ""),
|
|
1588
|
+
}),
|
|
1589
|
+
(0, react_jsx_runtime.jsx)("span", { children: m.snippet ?? m.preview ?? "" }),
|
|
1590
|
+
],
|
|
1591
|
+
}, `${hit.sessionId}-m${mi}`)),
|
|
1592
|
+
],
|
|
1593
|
+
}, hit.sessionId)),
|
|
1594
|
+
],
|
|
1595
|
+
}),
|
|
1596
|
+
error !== null &&
|
|
1597
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
1598
|
+
style: S.error,
|
|
1599
|
+
children: [
|
|
1600
|
+
t("list.error", { message: error }),
|
|
1601
|
+
" ",
|
|
1602
|
+
(0, react_jsx_runtime.jsx)("button", {
|
|
1603
|
+
style: { ...S.actionBtn(false), marginLeft: 6 },
|
|
1604
|
+
onClick: refresh,
|
|
1605
|
+
children: t("action.retry"),
|
|
1606
|
+
}),
|
|
1607
|
+
],
|
|
1608
|
+
}),
|
|
1609
|
+
sessions === null && error === null
|
|
1610
|
+
? (0, react_jsx_runtime.jsx)("div", { style: S.empty, children: t("list.loading") })
|
|
1611
|
+
: filteredShown.length === 0
|
|
1612
|
+
? (0, react_jsx_runtime.jsx)("div", {
|
|
1613
|
+
style: S.empty,
|
|
1614
|
+
children: tab === "archived" ? t("list.archivedEmpty") : tab === "active" ? t("list.activeEmpty") : t("list.empty"),
|
|
1615
|
+
})
|
|
1616
|
+
: grouped === null
|
|
1617
|
+
? filteredShown.map(renderRow)
|
|
1618
|
+
: (0, react_jsx_runtime.jsxs)(react.Fragment, {
|
|
1619
|
+
children: [
|
|
1620
|
+
grouped.buckets.map((g) => (0, react_jsx_runtime.jsxs)("div", {
|
|
1621
|
+
key: g.workspaceId,
|
|
1622
|
+
children: [
|
|
1623
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
1624
|
+
style: S.groupHeader,
|
|
1625
|
+
onClick: () => toggleCollapse(g.workspaceId),
|
|
1626
|
+
children: [
|
|
1627
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
1628
|
+
style: S.groupCaret,
|
|
1629
|
+
children: collapsed.has(g.workspaceId) ? "▸" : "▾",
|
|
1630
|
+
}),
|
|
1631
|
+
(0, react_jsx_runtime.jsx)("span", { style: S.groupTitle, children: (tab === "archived" ? t("badge.archived") + " · " : "") + g.title }),
|
|
1632
|
+
(0, react_jsx_runtime.jsx)("span", { style: S.groupCount, children: g.sessions.length }),
|
|
1633
|
+
],
|
|
1634
|
+
}),
|
|
1635
|
+
!collapsed.has(g.workspaceId) && (0, react_jsx_runtime.jsxs)("div", {
|
|
1636
|
+
style: S.groupList,
|
|
1637
|
+
children: g.sessions.map(renderRow),
|
|
1638
|
+
}),
|
|
1639
|
+
],
|
|
1640
|
+
})),
|
|
1641
|
+
grouped.rest.length > 0 && (0, react_jsx_runtime.jsxs)("div", {
|
|
1642
|
+
key: "__ungrouped__",
|
|
1643
|
+
children: [
|
|
1644
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
1645
|
+
style: S.groupHeader,
|
|
1646
|
+
onClick: () => toggleCollapse("__ungrouped__"),
|
|
1647
|
+
children: [
|
|
1648
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
1649
|
+
style: S.groupCaret,
|
|
1650
|
+
children: collapsed.has("__ungrouped__") ? "▸" : "▾",
|
|
1651
|
+
}),
|
|
1652
|
+
(0, react_jsx_runtime.jsx)("span", { style: S.groupTitle, children: t("group.ungrouped") }),
|
|
1653
|
+
(0, react_jsx_runtime.jsx)("span", { style: S.groupCount, children: grouped.rest.length }),
|
|
1654
|
+
],
|
|
1655
|
+
}),
|
|
1656
|
+
!collapsed.has("__ungrouped__") && (0, react_jsx_runtime.jsxs)("div", {
|
|
1657
|
+
style: S.groupList,
|
|
1658
|
+
children: grouped.rest.map(renderRow),
|
|
1659
|
+
}),
|
|
1660
|
+
],
|
|
1661
|
+
}),
|
|
1662
|
+
],
|
|
1663
|
+
}),
|
|
1664
|
+
],
|
|
1665
|
+
}),
|
|
1666
|
+
}),
|
|
1667
|
+
],
|
|
1668
|
+
}),
|
|
1669
|
+
],
|
|
1670
|
+
}),
|
|
1671
|
+
],
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
// ---------- 插件入口 ----------
|
|
1676
|
+
const inject = ["slots", "locale"];
|
|
1677
|
+
|
|
1678
|
+
// 设置页必须单入口 + React.createElement,禁止 jsx-runtime.jsx。
|
|
1679
|
+
// 外壳对 settings.section 渲染抛错会吞成空白,所以页面自己兜底。
|
|
1680
|
+
const h = react.createElement.bind(react);
|
|
1681
|
+
const settingsCss = [
|
|
1682
|
+
".scs_page{display:flex;flex-direction:column;gap:16px;padding:4px 0 24px}",
|
|
1683
|
+
".scs_hero{display:flex;flex-direction:column;gap:6px}",
|
|
1684
|
+
".scs_title{font-size:20px;font-weight:650;color:var(--dsw-alias-label-primary)}",
|
|
1685
|
+
".scs_desc{font-size:13px;line-height:1.6;color:var(--dsw-alias-label-tertiary);max-width:72ch;margin:0}",
|
|
1686
|
+
".scs_hint{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}",
|
|
1687
|
+
".scs_err{font-size:13px;color:var(--dsw-alias-label-error)}",
|
|
1688
|
+
".scs_card{display:flex;flex-direction:column;gap:12px;padding:16px;border:.5px solid var(--dsw-alias-border-l4);border-radius:16px;background:var(--dsw-alias-bg-layer-3)}",
|
|
1689
|
+
".scs_cardtitle{font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary)}",
|
|
1690
|
+
".scs_note{font-size:12px;line-height:1.7;color:var(--dsw-alias-label-secondary);padding:10px 12px;border:1px dashed var(--dsw-alias-border-l2);border-radius:10px}",
|
|
1691
|
+
".scs_row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
|
|
1692
|
+
".scs_btn{cursor:pointer;border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary);border-radius:8px;height:28px;padding:0 12px;font-size:12px}",
|
|
1693
|
+
".scs_btn.scs_on{border-color:var(--dsw-state-success-primary,#34a853);color:var(--dsw-state-success-primary,#34a853)}",
|
|
1694
|
+
".scs_btn.scs_off{border-color:var(--dsw-state-error-primary,#d9534f);color:var(--dsw-state-error-primary,#e5484d)}",
|
|
1695
|
+
".scs_input{flex:1;min-width:180px;height:28px;border-radius:6px;border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);padding:0 10px;font-size:13px}",
|
|
1696
|
+
".scs_last{font-size:12px;color:var(--dsw-alias-label-tertiary);padding:6px 8px;border:1px dashed var(--dsw-alias-border-l2);border-radius:6px;white-space:pre-wrap;word-break:break-all;max-height:80px;overflow:auto}",
|
|
1697
|
+
".scs_file{display:flex;align-items:center;gap:8px;font-size:12px}",
|
|
1698
|
+
".scs_del{cursor:pointer;border:none;background:transparent;color:#e5484d;font-size:11px}",
|
|
1699
|
+
".scs_select{height:28px;border-radius:6px;border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);font-size:12px;padding:0 8px}",
|
|
1700
|
+
".scs_ta{width:100%;min-height:72px;border-radius:8px;border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);padding:8px 10px;font-size:12px;resize:vertical}",
|
|
1701
|
+
// 悬浮窗(改:参考「选择目录」弹窗样式——mask 遮罩 + 居中 dialog + 面包屑 + 上级 + 列表 + 底部确认)
|
|
1702
|
+
".scs_pickermask{position:fixed;inset:0;z-index:9999;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.4)}",
|
|
1703
|
+
".scs_pickerdialog{width:min(520px,88vw);max-height:70vh;display:flex;flex-direction:column;gap:8px;padding:14px;border:.5px solid var(--dsw-alias-border-l4);border-radius:14px;background:var(--dsw-alias-bg-layer-3);box-shadow:0 12px 40px rgba(0,0,0,.35)}",
|
|
1704
|
+
".scs_pickerhead{display:flex;align-items:center;justify-content:space-between}",
|
|
1705
|
+
".scs_pickertitle{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary)}",
|
|
1706
|
+
".scs_pickerclose{font:inherit;font-size:13px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;padding:2px 6px}",
|
|
1707
|
+
".scs_pickerpath{font-size:11px;color:var(--dsw-alias-label-secondary);word-break:break-all;padding:6px 8px;border:.5px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-base)}",
|
|
1708
|
+
".scs_pickerup{font-size:12px;color:var(--dsw-alias-brand-primary);cursor:pointer;padding:3px 2px}",
|
|
1709
|
+
".scs_pickerlist{flex:1;min-height:120px;max-height:38vh;overflow-y:auto;display:flex;flex-direction:column;gap:2px}",
|
|
1710
|
+
".scs_pickerrow{font-size:12px;color:var(--dsw-alias-label-primary);cursor:pointer;padding:5px 8px;border-radius:6px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
|
|
1711
|
+
".scs_pickerrow:hover{background:color-mix(in srgb,var(--dsw-alias-brand-primary) 12%,transparent)}",
|
|
1712
|
+
".scs_pickerrow.scs_pickerselected{background:color-mix(in srgb,var(--dsw-alias-brand-primary) 18%,transparent);color:var(--dsw-alias-brand-primary)}",
|
|
1713
|
+
".scs_pickerhint{font-size:12px;color:var(--dsw-alias-label-tertiary);padding:8px}",
|
|
1714
|
+
".scs_pickerfoot{display:flex;justify-content:flex-end}",
|
|
1715
|
+
".scs_pickermdfile{font-weight:600}",
|
|
1716
|
+
".scs_rule{display:flex;flex-direction:column;gap:8px;padding:10px;border:.5px dashed var(--dsw-alias-border-l2);border-radius:10px}",
|
|
1717
|
+
].join("");
|
|
1718
|
+
|
|
1719
|
+
function ensureSettingsCss() {
|
|
1720
|
+
if (typeof document === "undefined") return;
|
|
1721
|
+
if (document.querySelector('style[data-plugin-css="dsh-session-conductor"]')) return;
|
|
1722
|
+
const tag = document.createElement("style");
|
|
1723
|
+
tag.dataset.plugin = "dsh-session-conductor";
|
|
1724
|
+
tag.dataset.pluginCss = "dsh-session-conductor";
|
|
1725
|
+
tag.textContent = settingsCss;
|
|
1726
|
+
document.head.appendChild(tag);
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
function trSettings(key) {
|
|
1730
|
+
// 跨 NS + en 统一取词(修复此前只读 zh 的 bug)
|
|
1731
|
+
return window.__SC_TR__(key);
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
// —— 模板注入:槽位卡片(独立组件,避免 MainTemplateSection 单函数过长)——
|
|
1735
|
+
function TemplateSlotCard(props) {
|
|
1736
|
+
const { slot, title, desc, m, busy, edits, importMode, fileRef, MODES, onToggle, onToggleEnforce, onOpenImport, onPickFile, onEdit, onSaveEdit, onRemove } = props;
|
|
1737
|
+
const hasContent = !!m.name || (m.bytes > 0);
|
|
1738
|
+
const text = edits[slot] !== undefined ? edits[slot] : (m.content ?? "");
|
|
1739
|
+
return h("div", { className: "scs_rule", key: slot },
|
|
1740
|
+
h("div", { className: "scs_row" },
|
|
1741
|
+
h("b", { style: { minWidth: LABEL_MIN_WIDTH } }, title),
|
|
1742
|
+
h("button", { type: "button", className: "scs_btn " + (m.enabled ? "scs_on" : "scs_off"), disabled: busy || !hasContent, onClick: () => onToggle(slot, !m.enabled) }, m.enabled ? __SC_TR__("tpl.injecting") : __SC_TR__("tpl.injectOff")),
|
|
1743
|
+
slot === "plan" ? h("button", { type: "button", className: "scs_btn", disabled: busy, onClick: () => onToggleEnforce(!(m.enforce === true)) }, (m.enforce === true ? __SC_TR__("tpl.enforceOn") : __SC_TR__("tpl.enforceOff"))) : null,
|
|
1744
|
+
),
|
|
1745
|
+
// 导入行:下拉选方式 + 单个导入按钮
|
|
1746
|
+
h("div", { className: "scs_row", style: { marginTop: 8 } },
|
|
1747
|
+
h("select", {
|
|
1748
|
+
className: "scs_select", value: importMode[slot] || "local",
|
|
1749
|
+
onChange: (e) => props.onImportMode(slot, e.target.value),
|
|
1750
|
+
}, MODES.map((o) => h("option", { key: o.value, value: o.value }, o.label))),
|
|
1751
|
+
h("button", { type: "button", className: "scs_btn", disabled: busy, onClick: () => onOpenImport(slot) }, __SC_TR__("tpl.import")),
|
|
1752
|
+
h("input", { ref: fileRef, type: "file", accept: ".md,text/markdown", style: { display: "none" }, onChange: (e) => onPickFile(slot, e) }),
|
|
1753
|
+
),
|
|
1754
|
+
// 内容显示 + 可再次编辑(动态高度、自动换行)
|
|
1755
|
+
h("textarea", {
|
|
1756
|
+
className: "scs_ta",
|
|
1757
|
+
style: { width: "100%", minHeight: TEXTAREA_MIN_HEIGHT, maxHeight: TEXTAREA_MAX_HEIGHT, resize: "vertical", whiteSpace: "pre-wrap", overflowWrap: "break-word", wordBreak: "break-word", lineHeight: LINE_HEIGHT_TEXTAREA, fontFamily: "inherit" },
|
|
1758
|
+
value: text,
|
|
1759
|
+
placeholder: hasContent ? "" : __SC_TR__("tpl.emptyHint"),
|
|
1760
|
+
onChange: (e) => onEdit(slot, e.target),
|
|
1761
|
+
}),
|
|
1762
|
+
h("div", { className: "scs_row", style: { marginTop: 6 } },
|
|
1763
|
+
h("button", { type: "button", className: "scs_btn", disabled: busy || edits[slot] === undefined || !(edits[slot] || "").trim(), onClick: () => onSaveEdit(slot) }, __SC_TR__("tpl.saveEdit")),
|
|
1764
|
+
h("button", { type: "button", className: "scs_del", disabled: busy || !hasContent, onClick: () => onRemove(slot) }, __SC_TR__("tpl.clear")),
|
|
1765
|
+
),
|
|
1766
|
+
h("div", { className: "scs_hint" }, desc + (m.name ? __SC_TR__("tpl.current", { name: m.name }) + (m.url ? "(" + m.url + ")" : "") + (m.updatedAt ? " · " + new Date(m.updatedAt).toLocaleString() : "") : "")),
|
|
1767
|
+
);
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
// —— 模板注入:统一悬浮窗(url 网址输入 / dir 目录浏览)——
|
|
1771
|
+
function TemplatePickerModal(props) {
|
|
1772
|
+
const { picker, busy, onClose, onBrowseDir, onPickMd, onImport } = props;
|
|
1773
|
+
return h("div", { className: "scs_pickermask", onClick: (e) => { if (e.target === e.currentTarget) onClose(); } },
|
|
1774
|
+
h("div", { className: "scs_pickerdialog" },
|
|
1775
|
+
h("div", { className: "scs_pickerhead" },
|
|
1776
|
+
h("span", { className: "scs_pickertitle" }, picker.mode === "url" ? __SC_TR__("tpl.pickerUrl", { slot: picker.slot === "plan" ? __SC_TR__(I18N_TPL_PLAN_SHORT) : __SC_TR__(I18N_TPL_CLOSING_SHORT) }) : (picker.mode === "dir" ? __SC_TR__("tpl.pickerDir", { slot: picker.slot === "plan" ? __SC_TR__(I18N_TPL_PLAN_SHORT) : __SC_TR__(I18N_TPL_CLOSING_SHORT) }) : __SC_TR__("tpl.pickerPath", { slot: picker.slot === "plan" ? __SC_TR__(I18N_TPL_PLAN_SHORT) : __SC_TR__(I18N_TPL_CLOSING_SHORT) }))),
|
|
1777
|
+
h("button", { type: "button", className: "scs_pickerclose", onClick: () => onClose() }, "✕"),
|
|
1778
|
+
),
|
|
1779
|
+
picker.mode === "url"
|
|
1780
|
+
? h("div", { className: "scs_row", style: { paddingTop: 4 } },
|
|
1781
|
+
h("input", { className: "scs_input", style: { flex: 1 }, value: picker.url || "", placeholder: __SC_TR__("tpl.urlPlaceholder"), onChange: (e) => props.onUrlChange(e.target.value) }),
|
|
1782
|
+
h("button", { type: "button", className: "scs_btn", disabled: busy || !(picker.url || "").trim(), onClick: () => onImport(picker.slot, "url", (picker.url || "").trim()) }, __SC_TR__("tpl.import")),
|
|
1783
|
+
)
|
|
1784
|
+
: h("div", {},
|
|
1785
|
+
h("div", { className: "scs_pickerpath" }, picker.path || __SC_TR__("tpl.noPath")),
|
|
1786
|
+
h("div", { className: "scs_pickerup", onClick: () => { if (picker.parent) onBrowseDir(picker.parent); } }, __SC_TR__("tpl.upDir")),
|
|
1787
|
+
h("div", { className: "scs_pickerlist" },
|
|
1788
|
+
picker.entries.length === 0 ? h("div", { className: "scs_pickerhint" }, __SC_TR__("tpl.emptyDir")) : null,
|
|
1789
|
+
picker.entries.map((e) => h("div", {
|
|
1790
|
+
key: e.path,
|
|
1791
|
+
className: "scs_pickerrow" + (e.isMd ? " scs_pickermdfile" : "") + (picker.path === e.path ? " scs_pickerselected" : ""),
|
|
1792
|
+
onClick: () => { if (e.isDir) onBrowseDir(e.path); else if (e.isMd) onPickMd(e.path); },
|
|
1793
|
+
}, (e.isDir ? "📁 " : (e.isMd ? "📄 " : "· ")) + e.name + (e.isDir ? "/" : ""))),
|
|
1794
|
+
),
|
|
1795
|
+
h("div", { className: "scs_pickerhint" }, __SC_TR__("tpl.dirHint")),
|
|
1796
|
+
h("div", { className: "scs_pickerfoot" },
|
|
1797
|
+
h("button", { type: "button", className: "scs_btn", disabled: busy || !(picker.path || "").trim(), onClick: () => onImport(picker.slot, "dir", (picker.path || "").trim()) }, __SC_TR__("tpl.importSelected")),
|
|
1798
|
+
),
|
|
1799
|
+
),
|
|
1800
|
+
),
|
|
1801
|
+
);
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
const TEMPLATE_MODES = [
|
|
1805
|
+
{ value: "local", label: __SC_TR__("tpl.modeLocal") },
|
|
1806
|
+
{ value: "url", label: __SC_TR__("tpl.modeUrl") },
|
|
1807
|
+
{ value: "dir", label: __SC_TR__("tpl.modeDir") },
|
|
1808
|
+
];
|
|
1809
|
+
|
|
1810
|
+
function useTemplateFetch(st) {
|
|
1811
|
+
const refresh = react.useCallback(() => {
|
|
1812
|
+
window.__scFetch("/api/session-conductor/templates", { credentials: "same-origin" })
|
|
1813
|
+
.then(async (res) => {
|
|
1814
|
+
const body = await res.json().catch(() => ({}));
|
|
1815
|
+
if (!res.ok || !body.ok) throw new Error((body.error && body.error.message) || (HTTP_STATUS_PREFIX + res.status));
|
|
1816
|
+
st.setSlots(body.slots || {});
|
|
1817
|
+
})
|
|
1818
|
+
.catch((cause) => st.setMsg({ err: (cause && cause.message) || String(cause) }));
|
|
1819
|
+
}, [st]);
|
|
1820
|
+
react.useEffect(() => { refresh(); }, [refresh]);
|
|
1821
|
+
const post = (payload) => window.__scFetch("/api/session-conductor/templates", {
|
|
1822
|
+
method: "POST", credentials: "same-origin",
|
|
1823
|
+
headers: { "Content-Type": CONTENT_TYPE_JSON },
|
|
1824
|
+
body: JSON.stringify(payload),
|
|
1825
|
+
}).then(async (res) => {
|
|
1826
|
+
const body = await res.json().catch(() => ({}));
|
|
1827
|
+
if (!res.ok || !body.ok) throw new Error((body.error && body.error.message) || (HTTP_STATUS_PREFIX + res.status));
|
|
1828
|
+
st.setSlots(body.slots || {});
|
|
1829
|
+
return body;
|
|
1830
|
+
});
|
|
1831
|
+
return { refresh, post };
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
// —— 模板导入动作(本地设备上传 / 在线网址 / DSH 目录浏览)——
|
|
1835
|
+
function useTemplateImports(st, post) {
|
|
1836
|
+
const slotLabel = (slot) => (slot === "plan" ? __SC_TR__(I18N_TPL_PLAN_SHORT) : __SC_TR__(I18N_TPL_CLOSING_SHORT));
|
|
1837
|
+
const onPickFile = (slot, event) => {
|
|
1838
|
+
const f = event.target.files && event.target.files[0];
|
|
1839
|
+
if (!f) return;
|
|
1840
|
+
event.target.value = "";
|
|
1841
|
+
st.setBusy(true); st.setMsg(null);
|
|
1842
|
+
f.text()
|
|
1843
|
+
.then((content) => post({ slot, name: f.name, content }))
|
|
1844
|
+
.then((body) => { st.setMsg({ ok: (body.message) || __SC_TR__("tpl.imported", { slot: slotLabel(slot) }) }); st.setEdits((s) => ({ ...s, [slot]: content })); })
|
|
1845
|
+
.catch((cause) => st.setMsg({ err: __SC_TR__("tpl.importFail") + ((cause && cause.message) || String(cause)) }))
|
|
1846
|
+
.finally(() => st.setBusy(false));
|
|
1847
|
+
};
|
|
1848
|
+
const openImport = (slot) => {
|
|
1849
|
+
const mode = st.importMode[slot] || "local";
|
|
1850
|
+
st.setMsg(null);
|
|
1851
|
+
if (mode === "local") {
|
|
1852
|
+
if (st.fileRefs[slot].current) st.fileRefs[slot].current.click();
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
if (mode === "url") {
|
|
1856
|
+
st.setPicker({ slot, mode: "url", url: "", path: "", parent: null, entries: [] });
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
window.__scFetch("/api/session-conductor/templates/dir", { credentials: "same-origin" })
|
|
1860
|
+
.then(async (res) => {
|
|
1861
|
+
const body = await res.json().catch(() => ({}));
|
|
1862
|
+
if (!res.ok || !body.ok) throw new Error((body.error && body.error.message) || (HTTP_STATUS_PREFIX + res.status));
|
|
1863
|
+
st.setPicker({ slot, mode, path: body.path, parent: body.parent, entries: body.entries || [] });
|
|
1864
|
+
})
|
|
1865
|
+
.catch((cause) => st.setMsg({ err: __SC_TR__("tpl.dirFail") + ((cause && cause.message) || String(cause)) }));
|
|
1866
|
+
};
|
|
1867
|
+
const browseDir = (path) => {
|
|
1868
|
+
window.__scFetch("/api/session-conductor/templates/dir?path=" + encodeURIComponent(path), { credentials: "same-origin" })
|
|
1869
|
+
.then(async (res) => {
|
|
1870
|
+
const body = await res.json().catch(() => ({}));
|
|
1871
|
+
if (!res.ok || !body.ok) throw new Error((body.error && body.error.message) || (HTTP_STATUS_PREFIX + res.status));
|
|
1872
|
+
st.setPicker({ slot: st.picker.slot, mode: st.picker.mode, path: body.path, parent: body.parent, entries: body.entries || [] });
|
|
1873
|
+
})
|
|
1874
|
+
.catch((cause) => st.setMsg({ err: __SC_TR__("tpl.dirFail") + ((cause && cause.message) || String(cause)) }));
|
|
1875
|
+
};
|
|
1876
|
+
const importTemplate = (slot, mode, value) => {
|
|
1877
|
+
st.setBusy(true); st.setMsg(null);
|
|
1878
|
+
post(mode === "url" ? { slot, url: value } : { slot, action: "pickPath", path: value })
|
|
1879
|
+
.then((body) => { st.setMsg({ ok: (body.message) || __SC_TR__("tpl.imported", { slot: slotLabel(slot) }) }); st.setPicker(null); })
|
|
1880
|
+
.catch((cause) => st.setMsg({ err: __SC_TR__("tpl.importFail") + ((cause && cause.message) || String(cause)) }))
|
|
1881
|
+
.finally(() => st.setBusy(false));
|
|
1882
|
+
};
|
|
1883
|
+
return { onPickFile, openImport, browseDir, importTemplate, slotLabel };
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
// —— 模板槽位变更动作(开关 / 强制门禁 / 编辑保存 / 清空)——
|
|
1887
|
+
function useTemplateMutations(st, post, slotLabel) {
|
|
1888
|
+
const toggle = (slot, enabled) => {
|
|
1889
|
+
st.setBusy(true); st.setMsg(null);
|
|
1890
|
+
post({ slot, enabled })
|
|
1891
|
+
.then((body) => st.setMsg({ ok: (body.message) || __SC_TR__("tpl.toggled", { slot: slotLabel(slot) }) }))
|
|
1892
|
+
.catch((cause) => st.setMsg({ err: (cause && cause.message) || String(cause) }))
|
|
1893
|
+
.finally(() => st.setBusy(false));
|
|
1894
|
+
};
|
|
1895
|
+
const toggleEnforce = (enforce) => {
|
|
1896
|
+
st.setBusy(true); st.setMsg(null);
|
|
1897
|
+
post({ slot: "plan", enforce })
|
|
1898
|
+
.then((body) => st.setMsg({ ok: (body.message) || __SC_TR__("tpl.enforceDone") }))
|
|
1899
|
+
.catch((cause) => st.setMsg({ err: (cause && cause.message) || String(cause) }))
|
|
1900
|
+
.finally(() => st.setBusy(false));
|
|
1901
|
+
};
|
|
1902
|
+
const onEdit = (slot, el) => {
|
|
1903
|
+
const v = el.value;
|
|
1904
|
+
st.setEdits((s) => ({ ...s, [slot]: v }));
|
|
1905
|
+
el.style.height = "auto";
|
|
1906
|
+
el.style.height = (el.scrollHeight + 2) + "px";
|
|
1907
|
+
};
|
|
1908
|
+
const saveEdit = (slot) => {
|
|
1909
|
+
const content = (st.edits[slot] ?? "").trim();
|
|
1910
|
+
if (!content) return;
|
|
1911
|
+
st.setBusy(true); st.setMsg(null);
|
|
1912
|
+
post({ slot, content })
|
|
1913
|
+
.then((body) => { st.setMsg({ ok: (body.message) || __SC_TR__("tpl.updated", { slot: slotLabel(slot) }) }); st.setEdits((s) => ({ ...s, [slot]: undefined })); })
|
|
1914
|
+
.catch((cause) => st.setMsg({ err: __SC_TR__("tpl.saveFail") + ((cause && cause.message) || String(cause)) }))
|
|
1915
|
+
.finally(() => st.setBusy(false));
|
|
1916
|
+
};
|
|
1917
|
+
const onRemove = (slot) => {
|
|
1918
|
+
st.setBusy(true); st.setMsg(null);
|
|
1919
|
+
post({ slot, action: "remove" })
|
|
1920
|
+
.then((body) => { st.setMsg({ ok: (body.message) || __SC_TR__("tpl.removed", { slot: slotLabel(slot) }) }); st.setEdits((s) => ({ ...s, [slot]: undefined })); })
|
|
1921
|
+
.catch((cause) => st.setMsg({ err: (cause && cause.message) || String(cause) }))
|
|
1922
|
+
.finally(() => st.setBusy(false));
|
|
1923
|
+
};
|
|
1924
|
+
return { toggle, toggleEnforce, onEdit, saveEdit, onRemove };
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
function MainTemplateSection() {
|
|
1928
|
+
const [slots, setSlots] = react.useState(null);
|
|
1929
|
+
const [busy, setBusy] = react.useState(false);
|
|
1930
|
+
const [msg, setMsg] = react.useState(null);
|
|
1931
|
+
const [edits, setEdits] = react.useState({});
|
|
1932
|
+
const [importMode, setImportMode] = react.useState({ plan: "local", closing: "local" });
|
|
1933
|
+
const [picker, setPicker] = react.useState(null);
|
|
1934
|
+
const fileRefs = { plan: react.useRef(null), closing: react.useRef(null) };
|
|
1935
|
+
const st = { slots, setSlots, busy, setBusy, msg, setMsg, edits, setEdits, importMode, setImportMode, picker, setPicker, fileRefs };
|
|
1936
|
+
const { post } = useTemplateFetch(st);
|
|
1937
|
+
const imports = useTemplateImports(st, post);
|
|
1938
|
+
const muts = useTemplateMutations(st, post, imports.slotLabel);
|
|
1939
|
+
return h("div", { className: "scs_card" },
|
|
1940
|
+
h("div", { className: "scs_cardtitle" }, __SC_TR__("tpl.title")),
|
|
1941
|
+
h("div", { className: "scs_hint" }, __SC_TR__("tpl.desc")),
|
|
1942
|
+
(slots ? [
|
|
1943
|
+
h(TemplateSlotCard, {
|
|
1944
|
+
key: "plan", slot: "plan", title: __SC_TR__("tpl.planTitle"), desc: __SC_TR__("tpl.planDesc"),
|
|
1945
|
+
m: slots.plan || { enabled: false, enforce: false, name: "", url: "", bytes: 0, updatedAt: 0, content: "" },
|
|
1946
|
+
busy, edits, importMode, fileRef: fileRefs.plan, MODES: TEMPLATE_MODES,
|
|
1947
|
+
onToggle: muts.toggle, onToggleEnforce: muts.toggleEnforce, onOpenImport: imports.openImport, onPickFile: imports.onPickFile, onEdit: muts.onEdit, onSaveEdit: muts.saveEdit, onRemove: muts.onRemove,
|
|
1948
|
+
onImportMode: (s, v) => setImportMode((prev) => ({ ...prev, [s]: v })),
|
|
1949
|
+
}),
|
|
1950
|
+
h(TemplateSlotCard, {
|
|
1951
|
+
key: "closing", slot: "closing", title: __SC_TR__("tpl.closingTitle"), desc: __SC_TR__("tpl.closingDesc"),
|
|
1952
|
+
m: slots.closing || { enabled: false, enforce: false, name: "", url: "", bytes: 0, updatedAt: 0, content: "" },
|
|
1953
|
+
busy, edits, importMode, fileRef: fileRefs.closing, MODES: TEMPLATE_MODES,
|
|
1954
|
+
onToggle: muts.toggle, onToggleEnforce: muts.toggleEnforce, onOpenImport: imports.openImport, onPickFile: imports.onPickFile, onEdit: muts.onEdit, onSaveEdit: muts.saveEdit, onRemove: muts.onRemove,
|
|
1955
|
+
onImportMode: (s, v) => setImportMode((prev) => ({ ...prev, [s]: v })),
|
|
1956
|
+
}),
|
|
1957
|
+
] : h("div", { className: "scs_hint" }, __SC_TR__("tpl.loading"))),
|
|
1958
|
+
msg ? h("div", { className: msg.err ? "scs_err" : "scs_hint" }, msg.err || msg.ok) : null,
|
|
1959
|
+
picker ? h(TemplatePickerModal, {
|
|
1960
|
+
picker, busy,
|
|
1961
|
+
onClose: () => setPicker(null),
|
|
1962
|
+
onBrowseDir: imports.browseDir,
|
|
1963
|
+
onPickMd: (p) => setPicker((prev) => ({ ...prev, path: p })),
|
|
1964
|
+
onUrlChange: (v) => setPicker((prev) => ({ ...prev, url: v })),
|
|
1965
|
+
onImport: imports.importTemplate,
|
|
1966
|
+
}) : null,
|
|
1967
|
+
);
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
|
|
1971
|
+
|
|
1972
|
+
// ── 自动重命名模型选择:DSH 同款解析选择器(provider 分组 + 模型下拉)──
|
|
1973
|
+
// 数据来自官方 modelCatalog(GET /api/session-conductor/auto-rename-model 透传),
|
|
1974
|
+
// 选择持久化到插件 domain,自动重命名 resolveRoute 三级优先级最优先。
|
|
1975
|
+
function AutoRenameModelSection() {
|
|
1976
|
+
const [sel, setSel] = react.useState(null); // {provider, model} | null(null=跟随会话模型)
|
|
1977
|
+
const [catalog, setCatalog] = react.useState(null); // 官方 modelCatalog
|
|
1978
|
+
const [provider, setProvider] = react.useState(""); // 编辑态 provider
|
|
1979
|
+
const [model, setModel] = react.useState(""); // 编辑态 model
|
|
1980
|
+
const [busy, setBusy] = react.useState(false);
|
|
1981
|
+
const [msg, setMsg] = react.useState(null);
|
|
1982
|
+
|
|
1983
|
+
react.useEffect(() => {
|
|
1984
|
+
let alive = true;
|
|
1985
|
+
window.__scFetch("/api/session-conductor/auto-rename-model", { headers: { accept: "application/json" } })
|
|
1986
|
+
.then((r) => r.json())
|
|
1987
|
+
.then((data) => {
|
|
1988
|
+
if (!alive || !data?.ok) return;
|
|
1989
|
+
setSel(data.selection ?? null);
|
|
1990
|
+
setCatalog(data.catalog ?? null);
|
|
1991
|
+
if (data.selection) {
|
|
1992
|
+
setProvider(data.selection.provider);
|
|
1993
|
+
setModel(data.selection.model);
|
|
1994
|
+
}
|
|
1995
|
+
})
|
|
1996
|
+
.catch(() => { /* 目录获取失败:仍允许手动选择(下方兜底输入框) */ });
|
|
1997
|
+
return () => { alive = false; };
|
|
1998
|
+
}, []);
|
|
1999
|
+
|
|
2000
|
+
const save = async (next) => {
|
|
2001
|
+
setBusy(true);
|
|
2002
|
+
setMsg(null);
|
|
2003
|
+
try {
|
|
2004
|
+
const r = await window.__scFetch("/api/session-conductor/auto-rename-model", {
|
|
2005
|
+
method: "POST",
|
|
2006
|
+
headers: { "Content-Type": CONTENT_TYPE_JSON },
|
|
2007
|
+
body: JSON.stringify(next),
|
|
2008
|
+
});
|
|
2009
|
+
const payload = await r.json();
|
|
2010
|
+
if (payload?.ok) {
|
|
2011
|
+
setSel(payload.selection ?? null);
|
|
2012
|
+
setMsg({ ok: payload.message || __SC_TR__("saved") });
|
|
2013
|
+
} else {
|
|
2014
|
+
setMsg({ err: payload?.error?.message || __SC_TR__("saveFail") });
|
|
2015
|
+
}
|
|
2016
|
+
} catch (e) {
|
|
2017
|
+
setMsg({ err: String(e?.message ?? e) });
|
|
2018
|
+
}
|
|
2019
|
+
setBusy(false);
|
|
2020
|
+
};
|
|
2021
|
+
|
|
2022
|
+
const groups = catalog?.groups ?? [];
|
|
2023
|
+
const providers = groups.map((g) => ({ id: g.id, name: g.name, models: g.models ?? [] }));
|
|
2024
|
+
const curProvider = providers.find((p) => p.id === provider) ?? null;
|
|
2025
|
+
const models = curProvider?.models ?? [];
|
|
2026
|
+
const follow = sel === null;
|
|
2027
|
+
|
|
2028
|
+
return h("div", { className: "scs_card" },
|
|
2029
|
+
h("div", { className: "scs_cardtitle" }, __SC_TR__("armTitle")),
|
|
2030
|
+
h("div", { className: "scs_hint" }, __SC_TR__("armDesc")),
|
|
2031
|
+
h("div", { className: "scs_row", style: { gap: 8, alignItems: "center" } },
|
|
2032
|
+
h("label", { style: { display: "flex", alignItems: "center", gap: 4 } },
|
|
2033
|
+
h("input", { type: "radio", name: "sc-arm-follow", checked: follow, onChange: () => save({ follow: true }) }),
|
|
2034
|
+
__SC_TR__("armFollow"),
|
|
2035
|
+
),
|
|
2036
|
+
),
|
|
2037
|
+
h("div", { className: "scs_row", style: { gap: 8, alignItems: "center", flexWrap: "wrap" } },
|
|
2038
|
+
h("select", { className: "scs_input", style: { width: SELECT_WIDTH_PROVIDER }, value: provider, disabled: busy, onChange: (e) => { setProvider(e.target.value); setModel(""); } },
|
|
2039
|
+
h("option", { value: "" }, __SC_TR__("armProvider")),
|
|
2040
|
+
providers.map((p) => h("option", { key: p.id, value: p.id }, p.name)),
|
|
2041
|
+
),
|
|
2042
|
+
h("select", { className: "scs_input", style: { width: SELECT_WIDTH_MODEL }, value: model, disabled: busy || !provider, onChange: (e) => setModel(e.target.value) },
|
|
2043
|
+
h("option", { value: "" }, __SC_TR__("armModel")),
|
|
2044
|
+
models.map((m) => h("option", { key: m.id, value: m.id }, m.name || m.id)),
|
|
2045
|
+
),
|
|
2046
|
+
h("button", { type: "button", className: "scs_btn", disabled: busy || !provider || !model, onClick: () => save({ provider, model }) }, __SC_TR__("armSet")),
|
|
2047
|
+
),
|
|
2048
|
+
h("div", { className: "scs_hint" }, follow
|
|
2049
|
+
? __SC_TR__("armCurFollow")
|
|
2050
|
+
: (sel ? `当前:${sel.provider}/${sel.model}` : __SC_TR__("armCurNone"))),
|
|
2051
|
+
msg ? h("div", { className: msg.err ? "scs_err" : "scs_hint" }, msg.err || msg.ok) : null,
|
|
2052
|
+
);
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
class SettingsErrorBoundary extends react.Component {
|
|
2056
|
+
constructor(props) {
|
|
2057
|
+
super(props);
|
|
2058
|
+
this.state = { err: "" };
|
|
2059
|
+
}
|
|
2060
|
+
static getDerivedStateFromError(err) {
|
|
2061
|
+
return { err: (err && err.message) || String(err) };
|
|
2062
|
+
}
|
|
2063
|
+
render() {
|
|
2064
|
+
if (this.state.err) {
|
|
2065
|
+
return h("div", { className: "scs_page" },
|
|
2066
|
+
h("div", { className: "scs_title" }, __SC_TR__("settings.title")),
|
|
2067
|
+
h("div", { className: "scs_err" }, __SC_TR__("renderFail") + this.state.err),
|
|
2068
|
+
);
|
|
2069
|
+
}
|
|
2070
|
+
return this.props.children;
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
function ConductorSettingsPage() {
|
|
2075
|
+
ensureSettingsCss();
|
|
2076
|
+
return h(SettingsErrorBoundary, null,
|
|
2077
|
+
h("div", { className: "scs_page" },
|
|
2078
|
+
h("div", { className: "scs_hero" },
|
|
2079
|
+
h("div", { className: "scs_title" }, trSettings("settings.title")),
|
|
2080
|
+
h("p", { className: "scs_desc" }, trSettings("settings.desc")),
|
|
2081
|
+
h("div", { className: "scs_hint" }, trSettings("settings.scopeHint")),
|
|
2082
|
+
),
|
|
2083
|
+
h(AutoRenameModelSection, null),
|
|
2084
|
+
h(MainTemplateSection, null),
|
|
2085
|
+
h("div", { className: "scs_note" }, trSettings("settings.note")),
|
|
2086
|
+
),
|
|
2087
|
+
);
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
function apply(ctx) {
|
|
2091
|
+
// 插槽排序(slots 顺序权重,小在前):设置左侧栏「会话管理」页 vs 侧边栏底部入口
|
|
2092
|
+
const SETTINGS_SECTION_ORDER = 40;
|
|
2093
|
+
const SIDEBAR_ACTION_ORDER = 90;
|
|
2094
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "session-conductor: dictionaries");
|
|
2095
|
+
const slots = ctx.get("slots");
|
|
2096
|
+
if (slots !== undefined) {
|
|
2097
|
+
slots.inject("settings.section", () => slots.register(
|
|
2098
|
+
{ name: "settings.section", id: "session-conductor", order: SETTINGS_SECTION_ORDER, label: () => trSettings("settings.nav") },
|
|
2099
|
+
() => h(ConductorSettingsPage, null),
|
|
2100
|
+
));
|
|
2101
|
+
}
|
|
2102
|
+
ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
|
|
2103
|
+
name: "sidebar.footer.action",
|
|
2104
|
+
id: "session-conductor",
|
|
2105
|
+
order: SIDEBAR_ACTION_ORDER,
|
|
2106
|
+
locale: NS,
|
|
2107
|
+
inject: () => ({
|
|
2108
|
+
onOpenSession: (sessionId) => {
|
|
2109
|
+
try {
|
|
2110
|
+
ctx.get("sessions")?.open?.(sessionId);
|
|
2111
|
+
} catch {
|
|
2112
|
+
// 打开失败不阻塞面板
|
|
2113
|
+
}
|
|
2114
|
+
},
|
|
2115
|
+
}),
|
|
2116
|
+
}, SessionManagerPanel));
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
exports.apply = apply;
|
|
2120
|
+
exports.inject = inject;
|
|
2121
|
+
|
|
2122
|
+
|
|
2123
|
+
/**
|
|
2124
|
+
* dsh-session-conductor — 「会话分组」精简卡(裁剪后版本)
|
|
2125
|
+
*
|
|
2126
|
+
* 分组管理(创建/重命名/删除/移动会话)已移除,回归 DSH 官方 workspace 机制。
|
|
2127
|
+
* 本卡只保留:
|
|
2128
|
+
* · 分组列表(GET /api/session-conductor/group/list,只读展示)
|
|
2129
|
+
* · 每行「在分组下新建会话」(POST /api/session-conductor/group/new-session {workspaceId})
|
|
2130
|
+
* · 「新建会话(上次会话工作区)」(new-session 不带 workspaceId = 最近活跃会话 cwd 归属分组)
|
|
2131
|
+
*/
|
|
2132
|
+
|
|
2133
|
+
return module.exports;
|
|
2134
|
+
},
|
|
2135
|
+
});
|
|
2136
|
+
window.__ModuleLoader__.load({
|
|
2137
|
+
id: "dsh-session-conductor-group",
|
|
2138
|
+
factory: (require) => {
|
|
2139
|
+
var module = { exports: {} };
|
|
2140
|
+
var exports = module.exports;
|
|
2141
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2142
|
+
|
|
2143
|
+
let react = require("react");
|
|
2144
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
2145
|
+
|
|
2146
|
+
const NS = "sessionGroup";
|
|
2147
|
+
const zh = {};
|
|
2148
|
+
const en = {};
|
|
2149
|
+
__scFillI18nDict("sessionGroup", zh, en);
|
|
2150
|
+
|
|
2151
|
+
const css = `
|
|
2152
|
+
.sg2_card { display:flex; flex-direction:column; gap:10px; }
|
|
2153
|
+
.sg2_row { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
|
|
2154
|
+
.sg2_btn { cursor:pointer; border:1px solid var(--dsw-alias-border-l1, rgba(128,128,128,.35)); background:var(--dsw-specific-tip, transparent); color:var(--dsw-alias-label-secondary, inherit); border-radius:8px; height:28px; padding:0 12px; font-size:12px; line-height:26px; display:inline-flex; align-items:center; gap:4px; white-space:nowrap; }
|
|
2155
|
+
.sg2_btn:hover { background:var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.15)); }
|
|
2156
|
+
.sg2_btn:disabled { opacity:.45; cursor:default; }
|
|
2157
|
+
.sg2_hint { color:var(--dsw-alias-label-tertiary, #888); font-size:11px; line-height:16px; }
|
|
2158
|
+
.sg2_listTitle { font-size:12px; color:var(--dsw-alias-label-secondary, inherit); margin-top:4px; }
|
|
2159
|
+
.sg2_item { display:flex; align-items:center; gap:8px; padding:6px 8px; border-radius:8px; font-size:12px; border:1px solid var(--dsw-alias-border-l1, rgba(128,128,128,.2)); }
|
|
2160
|
+
.sg2_item .sg2_name { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
2161
|
+
.sg2_item .sg2_meta { color:var(--dsw-alias-label-tertiary, #888); font-size:11px; flex:none; }
|
|
2162
|
+
.sg2_msg { font-size:12px; line-height:18px; }
|
|
2163
|
+
.sg2_msg.err { color:var(--dsw-state-error-primary, #d9534f); }
|
|
2164
|
+
.sg2_msg.ok { color:var(--dsw-state-success-primary, #4caf50); }
|
|
2165
|
+
`;
|
|
2166
|
+
const styleId = "dsh-session-group-css";
|
|
2167
|
+
if (typeof document !== "undefined" && !document.getElementById(styleId)) {
|
|
2168
|
+
const s = document.createElement("style");
|
|
2169
|
+
s.id = styleId;
|
|
2170
|
+
s.textContent = css;
|
|
2171
|
+
document.head.appendChild(s);
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
function SessionGroupCard({ t }) {
|
|
2175
|
+
const [workspaces, setWorkspaces] = react.useState(null);
|
|
2176
|
+
const [busy, setBusy] = react.useState(false);
|
|
2177
|
+
const [msg, setMsg] = react.useState(null);
|
|
2178
|
+
const load = () => {
|
|
2179
|
+
window.__scFetch("/api/session-conductor/group/list").then((r) => r.json()).then((b) => {
|
|
2180
|
+
if (b.ok) setWorkspaces(b.workspaces ?? []);
|
|
2181
|
+
}).catch(() => setWorkspaces([]));
|
|
2182
|
+
};
|
|
2183
|
+
react.useEffect(() => { load(); }, []);
|
|
2184
|
+
const createIn = async (workspaceId) => {
|
|
2185
|
+
if (busy) return;
|
|
2186
|
+
setBusy(true); setMsg(null);
|
|
2187
|
+
try {
|
|
2188
|
+
const r = await window.__scFetch("/api/session-conductor/group/new-session", {
|
|
2189
|
+
method: "POST",
|
|
2190
|
+
headers: { "Content-Type": "application/json" },
|
|
2191
|
+
body: JSON.stringify(workspaceId ? { workspaceId } : {}),
|
|
2192
|
+
});
|
|
2193
|
+
const b = await r.json();
|
|
2194
|
+
if (!b.ok) throw new Error(b.error || ("HTTP " + r.status));
|
|
2195
|
+
setMsg({ kind: "ok", text: t("ok") });
|
|
2196
|
+
load();
|
|
2197
|
+
} catch (e) {
|
|
2198
|
+
setMsg({ kind: "err", text: t("fail", { message: (e && e.message) || String(e) }) });
|
|
2199
|
+
} finally { setBusy(false); }
|
|
2200
|
+
};
|
|
2201
|
+
return react_jsx_runtime.jsxs("div", { className: "sg2_card", children: [
|
|
2202
|
+
react_jsx_runtime.jsx("div", { className: "sg2_hint", children: t("desc") }),
|
|
2203
|
+
react_jsx_runtime.jsxs("div", { className: "sg2_row", children: [
|
|
2204
|
+
react_jsx_runtime.jsx("button", { className: "sg2_btn", disabled: busy, onClick: () => createIn(""), children: busy ? t("busy") : t("newDefault") }),
|
|
2205
|
+
react_jsx_runtime.jsx("button", { className: "sg2_btn", disabled: busy, onClick: load, children: t("refresh") }),
|
|
2206
|
+
] }),
|
|
2207
|
+
react_jsx_runtime.jsx("div", { className: "sg2_listTitle", children: t("listTitle") }),
|
|
2208
|
+
!workspaces
|
|
2209
|
+
? react_jsx_runtime.jsx("div", { className: "sg2_hint", children: t("listEmpty") })
|
|
2210
|
+
: workspaces.map((w) => react_jsx_runtime.jsxs("div", { className: "sg2_item", key: w.workspaceId, children: [
|
|
2211
|
+
react_jsx_runtime.jsx("span", { className: "sg2_name", children: w.title || w.path }),
|
|
2212
|
+
react_jsx_runtime.jsx("span", { className: "sg2_meta", children: __SC_TR__("sessionCount", { n: w.sessionIds?.length ?? 0 }) }),
|
|
2213
|
+
react_jsx_runtime.jsx("button", { className: "sg2_btn", disabled: busy, onClick: () => createIn(w.workspaceId), children: busy ? t("busy") : t("newIn") }),
|
|
2214
|
+
] })),
|
|
2215
|
+
msg && react_jsx_runtime.jsx("div", { className: "sg2_msg " + (msg.kind === "err" ? "err" : "ok"), children: msg.text }),
|
|
2216
|
+
] });
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
function apply(ctx) {
|
|
2220
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-session-group: dictionaries");
|
|
2221
|
+
ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
|
|
2222
|
+
name: "settings.plugin.item",
|
|
2223
|
+
key: "session-conductor-group",
|
|
2224
|
+
locale: NS,
|
|
2225
|
+
}, function WrappedCard(props) {
|
|
2226
|
+
const t = ctx.locale.bind(NS);
|
|
2227
|
+
return react_jsx_runtime.jsxs("div", { children: [
|
|
2228
|
+
react_jsx_runtime.jsx("div", { children: t("title") }),
|
|
2229
|
+
react_jsx_runtime.jsx(SessionGroupCard, { t }),
|
|
2230
|
+
] });
|
|
2231
|
+
}));
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
const inject = ["slots", "locale"];
|
|
2235
|
+
exports.apply = apply;
|
|
2236
|
+
exports.inject = inject;
|
|
2237
|
+
return module.exports;
|
|
2238
|
+
}
|
|
2239
|
+
});
|
|
2240
|
+
|
|
2241
|
+
/**
|
|
2242
|
+
* dsh-session-conductor — 「第一条正在处理」队列条目(client 附加模块, 改写)
|
|
2243
|
+
*
|
|
2244
|
+
* DSH 原生排队逻辑:第一条消息(claimed)立即执行、从 host inbox 移除,因此
|
|
2245
|
+
* 原生排队 dock 只显示「后续排队消息」,执行中的第一条不可见。
|
|
2246
|
+
* 本模块在原生 dock 区域**上方**渲染一条与原生 QueueDock 同风格的条目:
|
|
2247
|
+
* 「⚙️ {第一条消息摘要}」——第一条也显示在排队列表里,回合结束(执行完)自动消失。
|
|
2248
|
+
* 排队消息列表仍由原生 dock 负责(不重复渲染)。不改 DSH 原生源码。
|
|
2249
|
+
*/
|
|
2250
|
+
|
|
2251
|
+
window.__ModuleLoader__.load({
|
|
2252
|
+
id: "dsh-session-conductor-processing",
|
|
2253
|
+
factory: (require) => {
|
|
2254
|
+
var module = { exports: {} };
|
|
2255
|
+
var exports = module.exports;
|
|
2256
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2257
|
+
|
|
2258
|
+
let react = require("react");
|
|
2259
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
2260
|
+
|
|
2261
|
+
const NS = "sessionConductorProcessing";
|
|
2262
|
+
const zh = {};
|
|
2263
|
+
const en = {};
|
|
2264
|
+
__scFillI18nDict("sessionConductorProcessing", zh, en);
|
|
2265
|
+
|
|
2266
|
+
/** 预览文本上限(字符):提示条只展示开头一段,避免撑高。 */
|
|
2267
|
+
const PREVIEW_MAX_CHARS = 120;
|
|
2268
|
+
/** 插槽顺序:排在原生排队 dock(order 20)之前,本条显示在排队列表上方。 */
|
|
2269
|
+
const DOCK_ORDER = 19;
|
|
2270
|
+
|
|
2271
|
+
// 样式对齐原生 QueueDock(同容器宽度/圆角/row 高度/字体),保证与原生排队列表视觉连体
|
|
2272
|
+
const css = `
|
|
2273
|
+
.sm-processing-dock { box-sizing: border-box; width: calc(100% - var(--dsh-composer-side-clearance, 0px) - var(--dsh-composer-side-clearance, 0px) - var(--dsh-composer-dock-inset, 0px) - var(--dsh-composer-dock-inset, 0px)); max-width: calc(var(--dsh-composer-card-max-width, 748px) - var(--dsh-composer-dock-inset, 0px) - var(--dsh-composer-dock-inset, 0px)); margin: 0 auto calc(0px - var(--dsh-composer-stack-gap, 0px) - 3px); padding: 0 var(--dsh-composer-dock-inset, 0px); flex: none; }
|
|
2274
|
+
.sm-processing-panel { background: var(--dsw-specific-tip, rgba(128,128,128,.06)); border: 1px solid var(--dsw-alias-border-l1, rgba(128,128,128,.3)); border-bottom: none; border-radius: 12px 12px 0 0; width: 100%; padding: 2px 0; position: relative; }
|
|
2275
|
+
.sm-processing-row { box-sizing: border-box; display: flex; align-items: center; gap: 10px; width: 100%; height: 36px; padding: 4px 12px; border-radius: 8px; color: var(--dsw-alias-label-primary-dimmed, #c9c9ce); font-family: Inter, var(--dsw-font-family); font-size: 13px; }
|
|
2276
|
+
.sm-processing-spin { display: inline-block; width: 12px; height: 12px; border: 2px solid currentColor; border-top-color: transparent; border-radius: 50%; animation: sm-processing-rotate .8s linear infinite; flex: none; }
|
|
2277
|
+
.sm-processing-text { min-width: 0; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; }
|
|
2278
|
+
@keyframes sm-processing-rotate { to { transform: rotate(360deg); } }
|
|
2279
|
+
`;
|
|
2280
|
+
const styleId = "sm-processing-css";
|
|
2281
|
+
if (typeof document !== "undefined" && !document.getElementById(styleId)) {
|
|
2282
|
+
const s = document.createElement("style");
|
|
2283
|
+
s.id = styleId;
|
|
2284
|
+
s.textContent = css;
|
|
2285
|
+
document.head.appendChild(s);
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
/** 从会话节点里提取当前回合(正在执行)的用户消息文本。 */
|
|
2289
|
+
function activeUserText(nodes) {
|
|
2290
|
+
if (!Array.isArray(nodes)) return "";
|
|
2291
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
2292
|
+
const n = nodes[i];
|
|
2293
|
+
if (n === null || n === undefined) continue;
|
|
2294
|
+
const kind = n.kind;
|
|
2295
|
+
if (kind === "assistant" || kind === "assistant-step" || kind === "tool-result" || kind === "command") break;
|
|
2296
|
+
if (kind === "user") {
|
|
2297
|
+
const text = typeof n.text === "string" ? n.text
|
|
2298
|
+
: Array.isArray(n.content) ? n.content.filter((b) => b?.type === "text").map((b) => b.text).join(" ")
|
|
2299
|
+
: "";
|
|
2300
|
+
if (text) return text;
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
return "";
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
function ProcessingBar({ useSession, t }) {
|
|
2307
|
+
const running = useSession((s) => s.running);
|
|
2308
|
+
const nodes = useSession((s) => s.nodes);
|
|
2309
|
+
const active = react.useMemo(() => activeUserText(nodes), [running, nodes]);
|
|
2310
|
+
// 仅当「第一条消息执行中」时显示(执行完自动消失);排队消息由原生 dock 展示
|
|
2311
|
+
if (!running || !active) return null;
|
|
2312
|
+
const preview = active.replace(/\s+/g, " ").trim().slice(0, PREVIEW_MAX_CHARS);
|
|
2313
|
+
const label = t("processing") + ":" + preview;
|
|
2314
|
+
return react_jsx_runtime.jsx("div", {
|
|
2315
|
+
className: "sm-processing-dock",
|
|
2316
|
+
children: react_jsx_runtime.jsx("div", {
|
|
2317
|
+
className: "sm-processing-panel",
|
|
2318
|
+
children: react_jsx_runtime.jsx("div", {
|
|
2319
|
+
className: "sm-processing-row",
|
|
2320
|
+
title: preview,
|
|
2321
|
+
children: [react_jsx_runtime.jsx("span", { className: "sm-processing-spin" }), react_jsx_runtime.jsx("span", { className: "sm-processing-text", children: label })],
|
|
2322
|
+
}),
|
|
2323
|
+
}),
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
function apply(ctx) {
|
|
2328
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "sm-processing: dictionaries");
|
|
2329
|
+
ctx.slots.inject("conversation.input.dock", () => ctx.slots.register({
|
|
2330
|
+
name: "conversation.input.dock",
|
|
2331
|
+
id: "sm-processing",
|
|
2332
|
+
order: DOCK_ORDER, // 在原生排队 dock(order 20)之前:第一条条目显示在排队列表上方
|
|
2333
|
+
locale: NS,
|
|
2334
|
+
}, ProcessingBar));
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
const inject = ["slots", "locale"];
|
|
2338
|
+
exports.apply = apply;
|
|
2339
|
+
exports.inject = inject;
|
|
2340
|
+
return module.exports;
|
|
2341
|
+
}
|
|
2342
|
+
});
|
|
2343
|
+
|
|
2344
|
+
/**
|
|
2345
|
+
* dsh-session-conductor — 压缩模型选择设置卡片
|
|
2346
|
+
* 设置 → 插件配置 → 「压缩模型」:会话模型旁单独选压缩用模型(省 pro 成本)。
|
|
2347
|
+
* 可选:跟随会话模型(默认)/ 指定便宜模型(agnes 等)。写入 /api/session-conductor/compaction-model。
|
|
2348
|
+
*/
|
|
2349
|
+
window.__ModuleLoader__.load({
|
|
2350
|
+
id: "dsh-session-conductor-compaction-model",
|
|
2351
|
+
factory: (require) => {
|
|
2352
|
+
var module = { exports: {} };
|
|
2353
|
+
var exports = module.exports;
|
|
2354
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2355
|
+
let react = require("react");
|
|
2356
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
2357
|
+
|
|
2358
|
+
const NS = "sessionConductorCompaction";
|
|
2359
|
+
const zh = {};
|
|
2360
|
+
const en = {};
|
|
2361
|
+
__scFillI18nDict("sessionConductorCompaction", zh, en);
|
|
2362
|
+
|
|
2363
|
+
/** 自定义模式下 provider / model 输入框宽度(px)。 */
|
|
2364
|
+
const PROVIDER_INPUT_WIDTH = 110;
|
|
2365
|
+
const MODEL_INPUT_WIDTH = 160;
|
|
2366
|
+
|
|
2367
|
+
const css = `
|
|
2368
|
+
.cm_card { border:1px solid var(--dsw-alias-border-l1,rgba(128,128,128,.2)); border-radius:10px; padding:12px; margin-top:8px; background:var(--dsw-alias-bg-base,#141518); }
|
|
2369
|
+
.cm_hint { color:var(--dsw-alias-label-tertiary,#888); font-size:12px; margin:2px 0 10px; line-height:1.5; }
|
|
2370
|
+
.cm_row { display:flex; gap:8px; align-items:center; margin-bottom:8px; flex-wrap:wrap; }
|
|
2371
|
+
.cm_input { height:28px; border-radius:6px; border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3)); background:var(--dsw-alias-bg-base,#141518); color:var(--dsw-alias-label-primary,#eee); padding:0 10px; font-size:12.5px; }
|
|
2372
|
+
.cm_btn { height:28px; border-radius:6px; border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3)); background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.15)); color:var(--dsw-alias-label-primary,#eee); cursor:pointer; padding:0 14px; font-size:12.5px; }
|
|
2373
|
+
.cm_btn:hover { background:rgba(128,128,128,.25); }
|
|
2374
|
+
.cm_msg { font-size:12px; margin-top:6px; }
|
|
2375
|
+
`;
|
|
2376
|
+
const styleId = "sm-compaction-css";
|
|
2377
|
+
if (typeof document !== "undefined" && !document.getElementById(styleId)) {
|
|
2378
|
+
const s = document.createElement("style");
|
|
2379
|
+
s.id = styleId;
|
|
2380
|
+
s.textContent = css;
|
|
2381
|
+
document.head.appendChild(s);
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
function CompactionModelCard({ t }) {
|
|
2385
|
+
const [mode, setMode] = react.useState("follow"); // follow | custom
|
|
2386
|
+
const [provider, setProvider] = react.useState("llm-pi-ai");
|
|
2387
|
+
const [model, setModel] = react.useState("agnes-2.5-flash");
|
|
2388
|
+
const [msg, setMsg] = react.useState(null);
|
|
2389
|
+
const [busy, setBusy] = react.useState(false);
|
|
2390
|
+
|
|
2391
|
+
react.useEffect(() => {
|
|
2392
|
+
(async () => {
|
|
2393
|
+
try {
|
|
2394
|
+
const r = await window.__scFetch("/api/session-conductor/compaction-model", { headers: { accept: "application/json" } });
|
|
2395
|
+
const payload = await r.json();
|
|
2396
|
+
if (payload?.ok && payload.compactionModel) {
|
|
2397
|
+
setMode("custom");
|
|
2398
|
+
setProvider(payload.compactionModel.provider || "");
|
|
2399
|
+
setModel(payload.compactionModel.model || "");
|
|
2400
|
+
} else if (payload?.ok) setMode("follow");
|
|
2401
|
+
} catch { /* 初始加载失败忽略 */ }
|
|
2402
|
+
})();
|
|
2403
|
+
}, []);
|
|
2404
|
+
|
|
2405
|
+
const save = async () => {
|
|
2406
|
+
setBusy(true); setMsg(null);
|
|
2407
|
+
try {
|
|
2408
|
+
const body = mode === "follow"
|
|
2409
|
+
? { follow: true }
|
|
2410
|
+
: { provider, model };
|
|
2411
|
+
const r = await window.__scFetch("/api/session-conductor/compaction-model", {
|
|
2412
|
+
method: "POST",
|
|
2413
|
+
headers: { "Content-Type": "application/json" },
|
|
2414
|
+
body: JSON.stringify(body),
|
|
2415
|
+
});
|
|
2416
|
+
const payload = await r.json();
|
|
2417
|
+
setMsg(payload?.ok ? t("saved") : (payload?.error?.message || t("error")));
|
|
2418
|
+
} catch { setMsg(t("error")); }
|
|
2419
|
+
setBusy(false);
|
|
2420
|
+
};
|
|
2421
|
+
|
|
2422
|
+
return react_jsx_runtime.jsxs("div", { className: "cm_card", children: [
|
|
2423
|
+
react_jsx_runtime.jsx("div", { className: "cm_hint", children: t("desc") }),
|
|
2424
|
+
react_jsx_runtime.jsxs("div", { className: "cm_row", children: [
|
|
2425
|
+
react_jsx_runtime.jsx("label", { children: react_jsx_runtime.jsx("input", { type: "radio", name: "cm-mode", checked: mode === "follow", onChange: () => setMode("follow") }) }),
|
|
2426
|
+
react_jsx_runtime.jsx("span", { children: t("follow") }),
|
|
2427
|
+
react_jsx_runtime.jsx("label", { children: react_jsx_runtime.jsx("input", { type: "radio", name: "cm-mode", checked: mode === "custom", onChange: () => setMode("custom") }) }),
|
|
2428
|
+
react_jsx_runtime.jsx("span", { children: t("custom") }),
|
|
2429
|
+
]}),
|
|
2430
|
+
mode === "custom" && react_jsx_runtime.jsxs("div", { className: "cm_row", children: [
|
|
2431
|
+
react_jsx_runtime.jsx("span", { children: t("provider") }),
|
|
2432
|
+
react_jsx_runtime.jsx("input", { className: "cm_input", value: provider, onChange: (e) => setProvider(e.target.value), style: { width: PROVIDER_INPUT_WIDTH } }),
|
|
2433
|
+
react_jsx_runtime.jsx("span", { children: t("model") }),
|
|
2434
|
+
react_jsx_runtime.jsx("input", { className: "cm_input", value: model, onChange: (e) => setModel(e.target.value), style: { width: MODEL_INPUT_WIDTH } }),
|
|
2435
|
+
]}),
|
|
2436
|
+
mode === "follow" && react_jsx_runtime.jsx("div", { className: "cm_hint", children: t("followNote") }),
|
|
2437
|
+
react_jsx_runtime.jsxs("div", { className: "cm_row", children: [
|
|
2438
|
+
react_jsx_runtime.jsx("button", { className: "cm_btn", disabled: busy, onClick: save, children: t("save") }),
|
|
2439
|
+
msg && react_jsx_runtime.jsx("span", { className: "cm_msg", children: msg }),
|
|
2440
|
+
]}),
|
|
2441
|
+
]});
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
function apply(ctx) {
|
|
2445
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-session-conductor-compaction-model: dictionaries");
|
|
2446
|
+
ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
|
|
2447
|
+
name: "settings.plugin.item",
|
|
2448
|
+
key: "session-conductor-compaction",
|
|
2449
|
+
locale: NS,
|
|
2450
|
+
}, function WrappedCard(props) {
|
|
2451
|
+
const t = ctx.locale.bind(NS);
|
|
2452
|
+
return react_jsx_runtime.jsxs("div", { children: [
|
|
2453
|
+
react_jsx_runtime.jsx("div", { children: t("title") }),
|
|
2454
|
+
react_jsx_runtime.jsx(CompactionModelCard, { t }),
|
|
2455
|
+
] });
|
|
2456
|
+
}));
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
const inject = ["slots", "locale"];
|
|
2460
|
+
exports.apply = apply;
|
|
2461
|
+
exports.inject = inject;
|
|
2462
|
+
return module.exports;
|
|
2463
|
+
}
|
|
2464
|
+
});
|
|
2465
|
+
/**
|
|
2466
|
+
* dsh-session-conductor — 「会话管理」设置侧边栏页( 新增)
|
|
2467
|
+
*
|
|
2468
|
+
* 注册 settings.section 侧边栏导航(设置左栏出现「会话管理」条目),
|
|
2469
|
+
* 点开是完整聚合页——会话管理说明 + 模板注入。
|
|
2470
|
+
* UI 组件 scs_* = 页面级。
|
|
2471
|
+
*/
|
|
2472
|
+
|
|
2473
|
+
window.__ModuleLoader__.load({
|
|
2474
|
+
id: "dsh-session-conductor-settings",
|
|
2475
|
+
factory: (require) => {
|
|
2476
|
+
var module = { exports: {} };
|
|
2477
|
+
var exports = module.exports;
|
|
2478
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2479
|
+
|
|
2480
|
+
let react = require("react");
|
|
2481
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
2482
|
+
|
|
2483
|
+
const NS = "sessionConductorSettings";
|
|
2484
|
+
const zh = {};
|
|
2485
|
+
const en = {};
|
|
2486
|
+
__scFillI18nDict("sessionConductorSettings", zh, en);
|
|
2487
|
+
|
|
2488
|
+
const css = `
|
|
2489
|
+
.scs_page { display:flex; flex-direction:column; gap:16px; }
|
|
2490
|
+
.scs_hero { display:flex; flex-direction:column; gap:6px; }
|
|
2491
|
+
.scs_title { font-size:20px; font-weight:650; color:var(--dsw-alias-label-primary); }
|
|
2492
|
+
.scs_desc { font-size:13px; line-height:1.6; color:var(--dsw-alias-label-tertiary); max-width:72ch; }
|
|
2493
|
+
.scs_hint { font-size:11px; line-height:16px; color:var(--dsw-alias-label-tertiary); }
|
|
2494
|
+
.scs_card { display:flex; flex-direction:column; gap:12px; padding:16px; border:.5px solid var(--dsw-alias-border-l4); border-radius:16px; background:var(--dsw-alias-bg-layer-3); }
|
|
2495
|
+
.scs_cardtitle { font-size:15px; font-weight:600; color:var(--dsw-alias-label-primary); }
|
|
2496
|
+
.scs_note { font-size:12px; line-height:1.7; color:var(--dsw-alias-label-secondary); padding:10px 12px; border:1px dashed var(--dsw-alias-border-l2); border-radius:10px; }
|
|
2497
|
+
`;
|
|
2498
|
+
const styleId = "scs-css";
|
|
2499
|
+
if (typeof document !== "undefined" && !document.getElementById(styleId)) {
|
|
2500
|
+
const s = document.createElement("style");
|
|
2501
|
+
s.id = styleId;
|
|
2502
|
+
s.textContent = css;
|
|
2503
|
+
document.head.appendChild(s);
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
|
|
2507
|
+
|
|
2508
|
+
// ── 设置侧边栏页(settings.section,仿 skill-scoreboard)───────────────
|
|
2509
|
+
function ScsPage({ t }) {
|
|
2510
|
+
return react_jsx_runtime.jsxs("div", { className: "scs_page", children: [
|
|
2511
|
+
react_jsx_runtime.jsxs("div", { className: "scs_hero", children: [
|
|
2512
|
+
react_jsx_runtime.jsx("div", { className: "scs_title", children: t("title") }),
|
|
2513
|
+
react_jsx_runtime.jsx("div", { className: "scs_desc", children: t("desc") }),
|
|
2514
|
+
react_jsx_runtime.jsx("div", { className: "scs_hint", children: t("scopeHint") }),
|
|
2515
|
+
] }),
|
|
2516
|
+
react_jsx_runtime.jsxs("div", { className: "scs_note", children: [
|
|
2517
|
+
react_jsx_runtime.jsx("b", { children: t("noteTitle") + ":" }),
|
|
2518
|
+
t("noteBody"),
|
|
2519
|
+
] }),
|
|
2520
|
+
] });
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
function apply(ctx) {
|
|
2524
|
+
// 设置页只在主入口 dsh-session-conductor 注册。这里再挂 settings.section
|
|
2525
|
+
// 会用 jsx-runtime 把页面打成空白。
|
|
2526
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "session-conductor-settings: dictionaries");
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
const inject = ["slots", "locale"];
|
|
2530
|
+
exports.apply = apply;
|
|
2531
|
+
exports.inject = inject;
|
|
2532
|
+
return module.exports;
|
|
2533
|
+
},
|
|
2534
|
+
});
|