@gavinc-cn/touchstone-dsh 0.1.1 → 0.2.0
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/LICENSE +0 -0
- package/README.md +21 -1
- package/README.zh.md +11 -1
- package/auth.py +0 -0
- package/board.py +775 -71
- package/builtin_assets.py +0 -0
- package/builtin_prompts/free_style/flow.md +0 -0
- package/builtin_prompts/free_style/load_test.md +0 -0
- package/builtin_prompts/free_style/templates/bug_report_template.md +0 -0
- package/builtin_prompts/free_style/templates/case_template.md +0 -0
- package/builtin_prompts/free_style/templates/index_template.md +0 -0
- package/builtin_prompts/free_style/templates/load_charts_template.md +0 -0
- package/builtin_prompts/free_style/templates/load_index_template.md +0 -0
- package/builtin_prompts/free_style/templates/load_plan_template.md +0 -0
- package/builtin_prompts/free_style/templates/load_scenario_template.md +0 -0
- package/builtin_prompts/free_style/templates/status_template.md +0 -0
- package/chat.py +590 -17
- package/db.py +62 -11
- package/dsh-plugin/cordis.patch.yml +7 -3
- package/dsh-plugin/install.sh +8 -5
- package/dsh-plugin/lib/agent-driver.js +710 -89
- package/dsh-plugin/lib/client.bundle.js +94 -9
- package/dsh-plugin/lib/index.js +242 -22
- package/dsh-plugin/scripts/build.mjs +0 -0
- package/dsh-plugin/scripts/prepack-check.mjs +0 -0
- package/dsh-plugin/scripts/select-bundle.mjs +0 -0
- package/dsh-plugin/src/client.js +94 -9
- package/dshdriver.py +74 -5
- package/dshevents.py +65 -3
- package/export_cases.py +0 -0
- package/extensions/dsh-autocommit/asset.json +0 -0
- package/extensions/dsh-autocommit/pkg/lib/index.mjs +0 -0
- package/extensions/dsh-autocommit/pkg/package.json +0 -0
- package/feishu.py +1197 -34
- package/feishu_conv.py +920 -0
- package/lib.py +0 -0
- package/lifecycle.py +0 -0
- package/loadcase.py +0 -0
- package/loadgen.py +0 -0
- package/localbus.py +0 -0
- package/migrate_p7b.py +0 -0
- package/package.json +1 -1
- package/platcompat.py +0 -0
- package/prompts.py +0 -0
- package/rag.py +0 -0
- package/requirements.txt +2 -1
- package/server.py +218 -26
- package/sessparse.py +150 -44
- package/touchstone.py +6 -3
- package/waitq.py +23 -0
- package/webui/dist-plugin/assets/index-JLwyZZxC.js +484 -0
- package/webui/dist-plugin/assets/index-tJ0TlvQ3.css +1 -0
- package/webui/dist-plugin/index.html +2 -2
- package/worktree.py +0 -0
- package/webui/dist-plugin/assets/index-B0WoP1Qy.css +0 -1
- package/webui/dist-plugin/assets/index-DtwKoi1T.js +0 -473
|
@@ -112,20 +112,103 @@ function toggleByKey(e) {
|
|
|
112
112
|
/** 宿主页 keydown 监听体(capture 阶段; 函数引用稳定, 便于 dispose 时精确解绑)。 */
|
|
113
113
|
function onHostKey(e) { toggleByKey(e); }
|
|
114
114
|
|
|
115
|
+
// ── 侧栏入口按钮的配色与几何(2026-10-09 修) ──────────────────────────────────
|
|
116
|
+
// 血案: 原先写 color:'var(--dsw-alias-text-l1, #e8e8e8)' —— dsh **没有** text-l1 这族
|
|
117
|
+
// token(真实族是 --dsw-alias-label-primary/secondary/tertiary/caption/dimmed), var() 取不到
|
|
118
|
+
// 就一直吃 fallback: 浅色档在 #f9fafb 的侧栏底上画 #e8e8e8 的字(实测对比度 1.17, 同排的
|
|
119
|
+
// 「插件」「设置」是 18.08), 只有深色档碰巧能看。教训: 引用宿主 token 一律先核实名字。
|
|
120
|
+
// 现行口径(明暗两档都跟宿主走):
|
|
121
|
+
// · 颜色**只取 dsh 语义 token 且不写死 fallback** —— token 缺席时该声明失效并回落到继承
|
|
122
|
+
// (侧栏根 .hHd-Xa_root 已设 label-primary), 而不是钉死一个只对某一档成立的灰;
|
|
123
|
+
// · 尺寸/圆角/gap/行高逐条对齐 dsh 自己的 .SidebarRoot_panelRow(min-height 36 / 内边距
|
|
124
|
+
// 7px 8px / margin 0 2px / gap 8 / line-height 22 / --dsw-radius-md), 收起态对齐
|
|
125
|
+
// .hHd-Xa_collapsed .panelRow(36x36 居中);
|
|
126
|
+
// · 图标盒 16x16 —— 与 dsh 侧栏 SVG 图标同尺寸; 文字继承侧栏根的 14px。
|
|
127
|
+
const ENTRY_TITLE = 'Touchstone 测试平台(Alt+T 开关)';
|
|
128
|
+
const ENTRY_GLYPH_STYLE = {
|
|
129
|
+
flex: '0 0 16px', width: 16, height: 16, display: 'inline-flex',
|
|
130
|
+
alignItems: 'center', justifyContent: 'center', fontSize: 16, lineHeight: '16px',
|
|
131
|
+
};
|
|
132
|
+
const ENTRY_LABEL_STYLE = {
|
|
133
|
+
minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// 交互态样式(悬停/开合/键盘聚焦环): 伪类没法写在 inline style 里, 落一张最小样式表。
|
|
137
|
+
// 取值逐条对齐 dsh 侧栏行(.hHd-Xa_panelRow:hover 与 :focus-visible)。
|
|
138
|
+
const ENTRY_CSS = [
|
|
139
|
+
'.ts-sb-entry{background:transparent}',
|
|
140
|
+
'.ts-sb-entry:hover,.ts-sb-entry.ts-sb-on{background:'
|
|
141
|
+
+ 'var(--dsw-alias-interactive-bg-hover,color-mix(in srgb,currentColor 8%,transparent))}',
|
|
142
|
+
'.ts-sb-entry:focus-visible{outline-width:var(--dsw-focus-ring-width,2px);outline-style:solid;'
|
|
143
|
+
+ 'outline-color:var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));'
|
|
144
|
+
+ 'outline-offset:-2px}',
|
|
145
|
+
].join('');
|
|
146
|
+
const ENTRY_CSS_ID = 'touchstone/sidebar-entry.css';
|
|
147
|
+
|
|
148
|
+
/** 幂等挂上入口行交互态样式表(形态与 dsh 插件自身的注入方式一致)。
|
|
149
|
+
* **失败一律吞掉**: 挂不上只是少了悬停/聚焦环的观感, 绝不能让侧栏入口或面板受影响
|
|
150
|
+
* (桩 DOM 自检环境里 document 面本就不全, 真机异常同理)。
|
|
151
|
+
* @returns 摘除函数(插件卸载时调用; 已存在/挂不上时返回空操作)。 */
|
|
152
|
+
function mountEntryStyle() {
|
|
153
|
+
try {
|
|
154
|
+
if (typeof document === 'undefined' || !document.head
|
|
155
|
+
|| typeof document.createElement !== 'function'
|
|
156
|
+
|| typeof document.querySelector !== 'function') {
|
|
157
|
+
return () => {};
|
|
158
|
+
}
|
|
159
|
+
if (document.querySelector('style[data-plugin-css=' + JSON.stringify(ENTRY_CSS_ID) + ']')) {
|
|
160
|
+
return () => {};
|
|
161
|
+
}
|
|
162
|
+
const tag = document.createElement('style');
|
|
163
|
+
tag.dataset.plugin = 'touchstone';
|
|
164
|
+
tag.dataset.pluginCss = ENTRY_CSS_ID;
|
|
165
|
+
tag.textContent = ENTRY_CSS;
|
|
166
|
+
document.head.appendChild(tag);
|
|
167
|
+
return () => tag.remove();
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.warn('[touchstone] 侧栏入口样式注入失败(只影响悬停/聚焦观感):', error);
|
|
170
|
+
return () => {};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
115
174
|
// 侧栏入口按钮(footer.action 槽; wide=侧栏展开态显示文字)
|
|
116
175
|
function Toggle(props) {
|
|
117
|
-
const wide = props && props.wide;
|
|
176
|
+
const wide = !!(props && props.wide);
|
|
118
177
|
const isOpen = useOpen();
|
|
178
|
+
const style = {
|
|
179
|
+
boxSizing: 'border-box', display: 'flex', alignItems: 'center',
|
|
180
|
+
border: 'none', cursor: 'pointer', textAlign: 'left',
|
|
181
|
+
color: 'var(--dsw-alias-label-primary)', // 无 fallback: 取不到即回落继承(见上注)
|
|
182
|
+
borderRadius: 'var(--dsw-radius-md, 8px)',
|
|
183
|
+
fontSize: 'inherit', // 侧栏根是 14px, 不再自带 13px
|
|
184
|
+
};
|
|
185
|
+
if (wide) {
|
|
186
|
+
style.gap = 8;
|
|
187
|
+
style.margin = '0 2px';
|
|
188
|
+
style.padding = '7px 8px';
|
|
189
|
+
style.minHeight = 36;
|
|
190
|
+
style.lineHeight = '22px';
|
|
191
|
+
style.flex = '1 1 auto';
|
|
192
|
+
style.minWidth = 0;
|
|
193
|
+
} else {
|
|
194
|
+
style.width = 36;
|
|
195
|
+
style.height = 36;
|
|
196
|
+
style.margin = 0;
|
|
197
|
+
style.padding = 0;
|
|
198
|
+
style.flex = 'none';
|
|
199
|
+
style.justifyContent = 'center';
|
|
200
|
+
}
|
|
119
201
|
return h('button', {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
title: 'Touchstone 测试平台(Alt+T 开关)',
|
|
202
|
+
type: 'button',
|
|
203
|
+
className: 'ts-sb-entry' + (isOpen ? ' ts-sb-on' : ''),
|
|
204
|
+
style,
|
|
205
|
+
title: ENTRY_TITLE,
|
|
206
|
+
'aria-label': ENTRY_TITLE,
|
|
207
|
+
'aria-expanded': isOpen,
|
|
127
208
|
onClick: () => setOpen(!isOpen),
|
|
128
|
-
},
|
|
209
|
+
},
|
|
210
|
+
h('span', { style: ENTRY_GLYPH_STYLE, 'aria-hidden': 'true' }, '🧪'),
|
|
211
|
+
wide ? h('span', { style: ENTRY_LABEL_STYLE }, isOpen ? '关闭 Touchstone' : 'Touchstone') : null);
|
|
129
212
|
}
|
|
130
213
|
|
|
131
214
|
// 关闭按钮自动隐藏(2026-10-03): 常显会盖住被嵌 SPA 侧栏底部的用户名, 所以平时完全隐藏,
|
|
@@ -337,6 +420,8 @@ function Panel() {
|
|
|
337
420
|
|
|
338
421
|
// cordis 客户端插件入口: 等 slots 服务就位后注册两个 UI 槽, 再挂快捷键
|
|
339
422
|
function apply(ctx) {
|
|
423
|
+
// 侧栏入口的交互态样式表(悬停/开合/聚焦环), 与本插件同生命周期
|
|
424
|
+
ctx.effect(() => mountEntryStyle(), 'touchstone: 侧栏入口样式');
|
|
340
425
|
const slots = ctx.get('slots');
|
|
341
426
|
if (!slots) return;
|
|
342
427
|
slots.inject('sidebar.footer.action', () => slots.register(
|
package/dsh-plugin/lib/index.js
CHANGED
|
@@ -22,9 +22,11 @@
|
|
|
22
22
|
* 两条与正常 dispose 共用同一个幂等 `teardown()`, 因此正常路径行为不变。
|
|
23
23
|
*/
|
|
24
24
|
import { spawn } from 'node:child_process';
|
|
25
|
-
import { existsSync, readFileSync
|
|
25
|
+
import { createWriteStream, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync }
|
|
26
|
+
from 'node:fs';
|
|
26
27
|
import http from 'node:http';
|
|
27
|
-
import {
|
|
28
|
+
import { homedir } from 'node:os';
|
|
29
|
+
import { dirname, join, resolve } from 'node:path';
|
|
28
30
|
import { fileURLToPath } from 'node:url';
|
|
29
31
|
import { AgentDriver, loadUserMessageFactory, loadModelSelectionInstaller }
|
|
30
32
|
from './agent-driver.js';
|
|
@@ -45,6 +47,10 @@ const BACKEND_EXIT_GRACE_MS = 5000;
|
|
|
45
47
|
/** 掉线自检节拍(ms):热重载把本条目换下却不 dispose 时的兜底发现窗口 */
|
|
46
48
|
const UNMOUNT_WATCH_MS = 2000;
|
|
47
49
|
|
|
50
|
+
/** 插件形态后端日志(2026-10-08):与库同目录追加写;启动时超过该体积先轮转一份 `.1` */
|
|
51
|
+
const BACKEND_LOG_NAME = 'plugin-backend.log';
|
|
52
|
+
const BACKEND_LOG_MAX_BYTES = 5 * 1024 * 1024;
|
|
53
|
+
|
|
48
54
|
/** 进程级存活壳注册表键(同一 dsh 进程内只允许一份有效壳) */
|
|
49
55
|
const LIVE_KEY = Symbol.for('touchstone.live');
|
|
50
56
|
|
|
@@ -62,6 +68,30 @@ const LIVE_KEY = Symbol.for('touchstone.live');
|
|
|
62
68
|
const FATAL_PY_MODULES = ['zstandard', 'requests'];
|
|
63
69
|
const PREFLIGHT_TIMEOUT_MS = 10000;
|
|
64
70
|
|
|
71
|
+
/**
|
|
72
|
+
* 缺省解释器探测顺序(未配置 config.pythonPath 时): `python` → `python3`。
|
|
73
|
+
*
|
|
74
|
+
* 为什么 `python` 排第一(2026-10-11 用户裁定「兜底应该用 python, 不要用 python3」):
|
|
75
|
+
* - **Windows 上没有 `python3` 这个名字**(官方安装器只装 `python.exe` / `py.exe`),
|
|
76
|
+
* 桌面端零配置安装下, 写死 `python3` 会让 `spawn` 直接 `ENOENT` —— 比「找到解释器但
|
|
77
|
+
* 缺依赖」更糟:预检连跑都跑不起来, 只能走 `skip()` 放行, 然后后端在导入期崩掉,
|
|
78
|
+
* 用户既看不到缺什么、也拿不到可复制的修复命令。
|
|
79
|
+
* - Linux 上两者都在(本机用户 shell 的 `python`/`python3` 同为 conda my_pyenv),
|
|
80
|
+
* 顺序换成 `python` 优先不改变命中结果。
|
|
81
|
+
* - 用 `python3` 当第二档是为了兜住「只有 python3、没有 python」的环境
|
|
82
|
+
* (部分精简发行版/容器镜像如此), 比单点写死更稳。
|
|
83
|
+
* 探测方式 = 真 spawn 一次 `-c pass`(受 PATH 解析、能识破 Microsoft Store 的
|
|
84
|
+
* `python3.exe` 占位别名——它是 GUI 应用, 跑起来不会正常退出), 而不是查文件表。
|
|
85
|
+
*/
|
|
86
|
+
const PYTHON_CANDIDATES = ['python', 'python3'];
|
|
87
|
+
const PYTHON_PROBE_TIMEOUT_MS = 5000;
|
|
88
|
+
/**
|
|
89
|
+
* 探测用的命令。带一个可识别的标记:测试里的假解释器要能把「探测调用」与
|
|
90
|
+
* 「依赖预检调用」区分开(两者都是 `-c`),否则探测会污染预检的调用记录。
|
|
91
|
+
* 标记写成 Python 注释, 对真解释器没有任何语义影响。
|
|
92
|
+
*/
|
|
93
|
+
const PYTHON_PROBE_CODE = 'pass # touchstone-python-probe';
|
|
94
|
+
|
|
65
95
|
/** 子进程句柄与解析出的后端端口(每 profile 一个插件实例, 模块级单例即可) */
|
|
66
96
|
let child = null;
|
|
67
97
|
let backendPort = 0;
|
|
@@ -78,6 +108,51 @@ function escapeHtml(text) {
|
|
|
78
108
|
(c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
|
79
109
|
}
|
|
80
110
|
|
|
111
|
+
/**
|
|
112
|
+
* 探一个候选解释器能不能真跑起来(`-c pass`, 5s 超时)。
|
|
113
|
+
*
|
|
114
|
+
* 为什么不是「查文件在不在」: spawn 走 PATH 解析, 而 Windows 的 Microsoft Store
|
|
115
|
+
* 别名会在 PATH 上放一个 `python3.exe` 占位(是 GUI 应用, 跑起来不返回)——
|
|
116
|
+
* 只有真跑一次才知道它是不是能用的解释器。任何异常(ENOENT/超时)都算不可用。
|
|
117
|
+
* `env` 由调用方传(用 `{...process.env, ...extraEnv}`): profile 的 extraEnv 可能改 PATH,
|
|
118
|
+
* 探测必须与随后真正 spawn 后端的环境一致, 否则会出现「探测到 A、实际跑 B」。
|
|
119
|
+
*/
|
|
120
|
+
function canRunPython(name, env) {
|
|
121
|
+
return new Promise((resolve) => {
|
|
122
|
+
let settled = false;
|
|
123
|
+
const done = (ok) => {
|
|
124
|
+
if (settled) return;
|
|
125
|
+
settled = true;
|
|
126
|
+
resolve(ok);
|
|
127
|
+
};
|
|
128
|
+
let proc = null;
|
|
129
|
+
try {
|
|
130
|
+
proc = spawn(name, ['-c', PYTHON_PROBE_CODE], { stdio: 'ignore', env });
|
|
131
|
+
} catch {
|
|
132
|
+
done(false);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const timer = setTimeout(() => {
|
|
136
|
+
try { proc.kill('SIGKILL'); } catch { /* 已退出:忽略 */ }
|
|
137
|
+
done(false);
|
|
138
|
+
}, PYTHON_PROBE_TIMEOUT_MS);
|
|
139
|
+
if (timer.unref) timer.unref();
|
|
140
|
+
proc.on('error', () => { clearTimeout(timer); done(false); });
|
|
141
|
+
proc.on('close', (code) => { clearTimeout(timer); done(code === 0); });
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 解析未配置 pythonPath 时该用哪个解释器: 按 PYTHON_CANDIDATES 顺序探测, 返回第一个能跑的。
|
|
147
|
+
* 全都跑不起来(或本平台一个都没有)时返回 null, 由调用方给明确的配置指引。
|
|
148
|
+
*/
|
|
149
|
+
async function resolveDefaultPython(env = process.env) {
|
|
150
|
+
for (const name of PYTHON_CANDIDATES) {
|
|
151
|
+
if (await canRunPython(name, env)) return name;
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
81
156
|
/**
|
|
82
157
|
* 读 requirements.txt 的发行名清单(注释/空行/选项行跳过, 剥掉版本与 extras 约束)。
|
|
83
158
|
* 读不到就返回空数组 —— 预检降级为「只验硬依赖」, 不影响启动。
|
|
@@ -101,7 +176,7 @@ function readRequirementNames(repoDir) {
|
|
|
101
176
|
* advisory —— requirements.txt 里缺失的可选依赖(只影响对应功能)
|
|
102
177
|
* skipped —— 预检本身没跑成(解释器缺失/超时/输出不可解析)⇒ 调用方照常启动
|
|
103
178
|
*/
|
|
104
|
-
function preflightDeps(pythonPath, repoDir, logger) {
|
|
179
|
+
function preflightDeps(pythonPath, repoDir, logger, env) {
|
|
105
180
|
const advisory = readRequirementNames(repoDir)
|
|
106
181
|
.filter((name) => !FATAL_PY_MODULES.includes(name.toLowerCase()));
|
|
107
182
|
const script = [
|
|
@@ -130,7 +205,9 @@ function preflightDeps(pythonPath, repoDir, logger) {
|
|
|
130
205
|
logger.warn(`touchstone: 依赖预检未完成(${why}), 照常启动后端`);
|
|
131
206
|
settle({ fatal: [], advisory: [], skipped: true });
|
|
132
207
|
};
|
|
133
|
-
|
|
208
|
+
// env 必须显式传(与探测/启动后端同一份): profile 的 extraEnv 可能改 PATH,
|
|
209
|
+
// 不传就出现「探测用 A、预检用 B」的分裂(2026-10-11 自检抓到的真 bug)。
|
|
210
|
+
const proc = spawn(pythonPath, argv, { cwd: repoDir, env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
134
211
|
let stdout = '';
|
|
135
212
|
const timer = setTimeout(() => {
|
|
136
213
|
skip(`超过 ${PREFLIGHT_TIMEOUT_MS}ms`);
|
|
@@ -171,8 +248,11 @@ function preflightDeps(pythonPath, repoDir, logger) {
|
|
|
171
248
|
/**
|
|
172
249
|
* 依赖缺失时的面板提示页(代替 503 JSON): 缺什么 + 一条可直接复制的修复命令
|
|
173
250
|
* + 修完怎么让插件重来(停用再启用即会重跑 apply, 按 spec §8.8 不必重启 dsh web)。
|
|
251
|
+
*
|
|
252
|
+
* `viaDefault` = 这个解释器是缺省兜底探到的(用户没配 `pythonPath`), 页面上要说明一句 ——
|
|
253
|
+
* 否则用户修完命令仍可能指错解释器, 且不知道该往哪里配。
|
|
174
254
|
*/
|
|
175
|
-
function renderDepHint(pythonPath, repoDir, missing) {
|
|
255
|
+
function renderDepHint(pythonPath, repoDir, missing, viaDefault) {
|
|
176
256
|
const cmd = `${pythonPath} -m pip install -r ${join(repoDir, 'requirements.txt')}`;
|
|
177
257
|
return '<!doctype html><html lang="zh"><head><meta charset="utf-8">'
|
|
178
258
|
+ '<title>Touchstone 后端未启动</title></head>'
|
|
@@ -184,11 +264,93 @@ function renderDepHint(pythonPath, repoDir, missing) {
|
|
|
184
264
|
+ '<pre style="background:#0d0f12;border:1px solid #2a2f36;border-radius:6px;'
|
|
185
265
|
+ `padding:.8rem 1rem;overflow:auto">${escapeHtml(cmd)}</pre>`
|
|
186
266
|
+ '<p>装完把本插件<b>停用再启用</b>(Plugins 面板里那一行的开关)即会重试,不必重启 dsh web。</p>'
|
|
187
|
-
+
|
|
188
|
-
|
|
267
|
+
+ (viaDefault
|
|
268
|
+
? '<p style="color:#8b949e">上面这个解释器是<b>缺省探测</b>得到的(未配置 '
|
|
269
|
+
+ '<code>pythonPath</code>:依次试 <code>python</code>、<code>python3</code>)。'
|
|
270
|
+
+ '若它不是你机器上装了依赖的那个,直接在它里面装依赖最省事(上面这条命令就是);'
|
|
271
|
+
+ '也可以改用另一个解释器 —— 在 profile patch 的 <code>touchstone-dsh</code> 条目里写 '
|
|
272
|
+
+ '<code>pythonPath</code> 指向它。</p>'
|
|
273
|
+
: '<p style="color:#8b949e">若这个解释器不对,可在 profile patch 的 <code>touchstone-dsh</code> 条目里配置 '
|
|
274
|
+
+ '<code>pythonPath</code> 指向含这些依赖的解释器。</p>')
|
|
275
|
+
+ '</body></html>';
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* 缺省解释器一个都跑不起来时的提示页: 这不是「缺依赖」而是「找不到 Python」,
|
|
280
|
+
* 要给的是安装/配置指引, 不是 pip 命令(此时 pip 也没得跑)。
|
|
281
|
+
*/
|
|
282
|
+
function renderNoPythonHint() {
|
|
283
|
+
return '<!doctype html><html lang="zh"><head><meta charset="utf-8">'
|
|
284
|
+
+ '<title>Touchstone 后端未启动</title></head>'
|
|
285
|
+
+ '<body style="margin:0;padding:2rem;font:14px/1.75 ui-monospace,Menlo,Consolas,monospace;'
|
|
286
|
+
+ 'background:#14161a;color:#e6e6e6">'
|
|
287
|
+
+ '<h2 style="color:#ffb454;margin:0 0 .9rem">Touchstone 后端未启动:找不到 Python 解释器</h2>'
|
|
288
|
+
+ '<p>依次探测 <code>python</code>、<code>python3</code> 都没能跑起来(未配置 '
|
|
289
|
+
+ '<code>pythonPath</code>)。</p>'
|
|
290
|
+
+ '<p>请先安装 Python 3.12+(<code>python.org</code>,Windows 上安装时勾选 '
|
|
291
|
+
+ '<code>Add python.exe to PATH</code>),然后在 profile patch 的 '
|
|
292
|
+
+ '<code>touchstone-dsh</code> 条目里写 <code>pythonPath</code> 指向它;'
|
|
293
|
+
+ '若解释器已装好只是不在 PATH 上,也要靠这个配置项指过去。</p>'
|
|
294
|
+
+ '<p style="color:#8b949e">配置后把本插件<b>停用再启用</b>即会重试,不必重启 dsh web。</p>'
|
|
189
295
|
+ '</body></html>';
|
|
190
296
|
}
|
|
191
297
|
|
|
298
|
+
/**
|
|
299
|
+
* 插件形态后端日志路径:与子进程用的**同一个库**同目录(`<库目录>/plugin-backend.log`)。
|
|
300
|
+
* 不落 node_modules 下的 .run:装机目录可能只读、重装即清,而且日志跟着库走才对得上
|
|
301
|
+
* 首启口令与运行审计。库路径优先级与 server.py 侧一致:extraEnv(profile 显式配置)
|
|
302
|
+
* > 宿主环境 > 缺省 `~/.touchstone/touchstone.db`。
|
|
303
|
+
*/
|
|
304
|
+
function backendLogPath(extraEnv) {
|
|
305
|
+
const dbPath = resolve(extraEnv.TOUCHSTONE_DB || process.env.TOUCHSTONE_DB
|
|
306
|
+
|| join(homedir(), '.touchstone', 'touchstone.db'));
|
|
307
|
+
return join(dirname(dbPath), BACKEND_LOG_NAME);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* 打开后端日志追加流(必要时先把超限的旧日志轮转成 `.1`)。任何失败都只 warn 并返回
|
|
312
|
+
* null —— 日志不可用**绝不阻断后端启动**,调用方退化为只走 dsh 日志。
|
|
313
|
+
*/
|
|
314
|
+
function openBackendLog(logPath, logger) {
|
|
315
|
+
try {
|
|
316
|
+
mkdirSync(dirname(logPath), { recursive: true, mode: 0o700 });
|
|
317
|
+
if (existsSync(logPath) && statSync(logPath).size > BACKEND_LOG_MAX_BYTES) {
|
|
318
|
+
try {
|
|
319
|
+
rmSync(logPath + '.1', { force: true }); // Windows 不允许改名覆盖已存在文件
|
|
320
|
+
renameSync(logPath, logPath + '.1');
|
|
321
|
+
} catch { /* 轮转失败照样追加:不因轮转挡启动 */ }
|
|
322
|
+
}
|
|
323
|
+
const stream = createWriteStream(logPath, { flags: 'a', mode: 0o600 });
|
|
324
|
+
let broken = false;
|
|
325
|
+
stream.on('error', (error) => {
|
|
326
|
+
if (broken) return; // 只报一次,避免刷屏
|
|
327
|
+
broken = true;
|
|
328
|
+
logger.warn(`touchstone: 后端日志写入失败(${error && error.message}), 后续只走 dsh 日志`);
|
|
329
|
+
});
|
|
330
|
+
return stream;
|
|
331
|
+
} catch (error) {
|
|
332
|
+
logger.warn(`touchstone: 后端日志不可用(${error && error.message}), 输出只走 dsh 日志: ${logPath}`);
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* 行缓冲转发子进程输出。为什么不直接 `split('\n')`:data 事件按 chunk 到达,**行可能
|
|
339
|
+
* 被切开**——`TOUCHSTONE_LISTEN` 被切成两段就永远匹配不上,端口解析不到、面板固定 503
|
|
340
|
+
* (2026-10-08 缺陷定位时一并修)。每个流各持一段 carry,拼齐整行才交给 onLine。
|
|
341
|
+
*/
|
|
342
|
+
function pumpLines(stream, onLine) {
|
|
343
|
+
let carry = '';
|
|
344
|
+
stream.setEncoding('utf8'); // 内部 StringDecoder:多字节字符跨 chunk 不截断乱码
|
|
345
|
+
stream.on('data', (chunk) => {
|
|
346
|
+
carry += chunk;
|
|
347
|
+
const lines = carry.split('\n');
|
|
348
|
+
carry = lines.pop(); // 末段不完整,留到下一 chunk
|
|
349
|
+
for (const line of lines) onLine(line);
|
|
350
|
+
});
|
|
351
|
+
stream.on('end', () => { if (carry) onLine(carry); });
|
|
352
|
+
}
|
|
353
|
+
|
|
192
354
|
/**
|
|
193
355
|
* 反代 /touchstone/* 到本机 server.py 子进程: 剥掉 /touchstone 前缀后原样转发
|
|
194
356
|
* (路径/查询串原样; Node http 管道流式转发不缓冲, SSE 事件实时透传)。
|
|
@@ -272,7 +434,8 @@ function stopChild(proc, logger, why) {
|
|
|
272
434
|
/**
|
|
273
435
|
* cordis 插件入口。config 来自 profile patch insert 条目(install.sh 写入):
|
|
274
436
|
* repoDir Touchstone 平台根(可选; 缺省 = 包自身目录, 因为包是自包含的)
|
|
275
|
-
* pythonPath Python
|
|
437
|
+
* pythonPath Python 解释器(可选; 缺省按 `python` → `python3` 顺序真跑一次探一个,
|
|
438
|
+
* 本机该指含 zstandard 的 conda env 或已 pip 装过依赖的那支)
|
|
276
439
|
* extraEnv 透传子进程的额外环境变量(如 TOUCHSTONE_DB 指向隔离实例库)
|
|
277
440
|
* 返回组合 disposer(注销路由 + 断开驱动 + SIGTERM 子进程),dsh 停用插件时调用;
|
|
278
441
|
* 另有「启用即回收上一份壳」与「掉线自检」两条自愈路径(见文件头注释)。
|
|
@@ -324,6 +487,14 @@ export async function apply(ctx, config = {}) {
|
|
|
324
487
|
'--parent-watch', // 父死感知: dsh 退出/崩溃时后端自主退出
|
|
325
488
|
];
|
|
326
489
|
const extraEnv = { ...(config.extraEnv || {}) };
|
|
490
|
+
// 插件形态默认「空口令 admin」(2026-10-08 用户口径):面板本来就是免登 admin,
|
|
491
|
+
// 而随机一次性口令只印在启动横幅里、用户拿不到 —— 首装必被「强制改密门」锁死
|
|
492
|
+
// (横幅+门+薄壳丢 stdout 三者叠加,见 bug_report/20261008_1856)。这里下发开关:
|
|
493
|
+
// db.seed_admin 种子时存空口令、不置 must_change_pw;用户在设置页设了密码就按设置的来。
|
|
494
|
+
// 部署方显式配了 TS_ADMIN_PASSWORD 时无需让位(db 侧以该变量优先)。
|
|
495
|
+
if (!('TS_ADMIN_PASSWORDLESS' in extraEnv)) {
|
|
496
|
+
extraEnv.TS_ADMIN_PASSWORDLESS = '1';
|
|
497
|
+
}
|
|
327
498
|
if (driverUrl) {
|
|
328
499
|
// 驱动契约经环境变量下发(不写配置文件): 重启 server.py 即重新握手, 无陈旧状态
|
|
329
500
|
extraEnv.TS_AGENT_DRIVER_URL = driverUrl;
|
|
@@ -333,11 +504,25 @@ export async function apply(ctx, config = {}) {
|
|
|
333
504
|
// 依赖预检(2026-10-07): 缺硬依赖时**不 spawn** —— spawn 了也必然在导入期退出,
|
|
334
505
|
// 日志里只留一行 traceback。改为把「缺什么 + 一条可复制的修复命令」同时送进
|
|
335
506
|
// 日志与面板(backendHint 由 proxy 渲染), 让 npm 安装真正做到「装完就知道缺什么」。
|
|
336
|
-
|
|
337
|
-
|
|
507
|
+
// 解释器(2026-10-11 改): 配置优先; 没配就按 `python` → `python3` 真探一次
|
|
508
|
+
// (Windows 上没有 python3 这个名字, 写死它会直接 ENOENT——那样连缺依赖都报不出来)。
|
|
509
|
+
// 探测用「与随后 spawn 后端同一份 env」, extraEnv 里改过 PATH 时两者才一致。
|
|
510
|
+
const childEnv = { ...process.env, ...extraEnv };
|
|
511
|
+
const pythonPath = config.pythonPath || await resolveDefaultPython(childEnv);
|
|
512
|
+
if (!pythonPath) {
|
|
513
|
+
backendHint = renderNoPythonHint();
|
|
514
|
+
logger.warn(`touchstone: 找不到 Python 解释器(依次试过 ${PYTHON_CANDIDATES.join(', ')}), 插件不启动后端`);
|
|
515
|
+
logger.warn('touchstone: 请安装 Python 3.12+ 或在本插件 config 里配 pythonPath, 然后停用再启用');
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
const viaDefault = !config.pythonPath;
|
|
519
|
+
if (viaDefault) logger.info(`touchstone: 未配置 pythonPath, 缺省探到解释器: ${pythonPath}`);
|
|
520
|
+
const deps = await preflightDeps(pythonPath, repoDir, logger, childEnv);
|
|
338
521
|
let myChild = null;
|
|
522
|
+
/** 本次壳的开的后端日志流(teardown 关闭;null = 不可用/未启动) */
|
|
523
|
+
let myLog = null;
|
|
339
524
|
if (deps.fatal.length) {
|
|
340
|
-
backendHint = renderDepHint(pythonPath, repoDir, deps.fatal);
|
|
525
|
+
backendHint = renderDepHint(pythonPath, repoDir, deps.fatal, viaDefault);
|
|
341
526
|
logger.warn(`touchstone: 后端未启动 —— Python 运行依赖缺失: ${deps.fatal.join(', ')}`);
|
|
342
527
|
logger.warn(`touchstone: 修复: ${pythonPath} -m pip install -r ${join(repoDir, 'requirements.txt')}`);
|
|
343
528
|
logger.warn('touchstone: 装完把本插件停用再启用即会重试(不必重启 dsh web)');
|
|
@@ -352,22 +537,38 @@ export async function apply(ctx, config = {}) {
|
|
|
352
537
|
// EOF 即自主退出(实测父 kill -9 后 1.94s 退出)。改成 'ignore' 会让该通道失效。
|
|
353
538
|
myChild = spawn(pythonPath, args, {
|
|
354
539
|
cwd: repoDir,
|
|
355
|
-
env:
|
|
540
|
+
env: childEnv,
|
|
356
541
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
357
542
|
});
|
|
358
543
|
child = myChild;
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
544
|
+
// 子进程输出落盘(2026-10-08):stdout 此前只用来解析 marker,**其余行全部丢弃**——
|
|
545
|
+
// 首启一次性口令横幅(设计上唯一出口,见 server._seed_hint)与 [board]/[waitq]/[runner]
|
|
546
|
+
// 运行诊断都在这些行里。现在 stdout/stderr 都逐行追加到 <库目录>/plugin-backend.log。
|
|
547
|
+
myLog = openBackendLog(backendLogPath(extraEnv), logger);
|
|
548
|
+
const writeLog = (line) => {
|
|
549
|
+
if (!myLog) return;
|
|
550
|
+
try { myLog.write(line + '\n'); } catch { /* 流已坏:忽略,绝不打断读取 */ }
|
|
551
|
+
};
|
|
552
|
+
pumpLines(myChild.stdout, (line) => {
|
|
553
|
+
writeLog(line);
|
|
554
|
+
const m = line.match(/^TOUCHSTONE_LISTEN (\d+)/);
|
|
555
|
+
if (m) {
|
|
556
|
+
backendPort = Number(m[1]);
|
|
557
|
+
logger.info(`touchstone: 后端就绪 127.0.0.1:${backendPort}`);
|
|
366
558
|
}
|
|
367
559
|
});
|
|
368
|
-
myChild.stderr
|
|
369
|
-
|
|
370
|
-
|
|
560
|
+
pumpLines(myChild.stderr, (line) => {
|
|
561
|
+
writeLog(line);
|
|
562
|
+
if (line.trim()) logger.warn(`touchstone[py]: ${line.trim()}`);
|
|
563
|
+
});
|
|
564
|
+
myChild.on('exit', (code, sig) => {
|
|
565
|
+
writeLog(`[touchstone] 后端退出 code=${code} sig=${sig}`);
|
|
566
|
+
logger.warn(`touchstone: 后端退出 code=${code} sig=${sig}`);
|
|
567
|
+
});
|
|
568
|
+
myChild.on('error', (err) => {
|
|
569
|
+
writeLog(`[touchstone] 后端拉起失败: ${err.message}`);
|
|
570
|
+
logger.warn(`touchstone: 后端拉起失败: ${err.message}`);
|
|
571
|
+
});
|
|
371
572
|
}
|
|
372
573
|
|
|
373
574
|
let disposeRoute = null;
|
|
@@ -399,6 +600,10 @@ export async function apply(ctx, config = {}) {
|
|
|
399
600
|
logger.warn(`touchstone: 驱动释放失败(忽略): ${error && error.message}`);
|
|
400
601
|
}
|
|
401
602
|
await stopChild(myChild, logger, why);
|
|
603
|
+
if (myLog) { // 子进程停稳后再收日志流:最后几行不丢
|
|
604
|
+
try { myLog.end(); } catch { /* 已关闭:忽略 */ }
|
|
605
|
+
myLog = null;
|
|
606
|
+
}
|
|
402
607
|
logger.info(`touchstone: 壳已停(${why})`);
|
|
403
608
|
};
|
|
404
609
|
|
|
@@ -441,3 +646,18 @@ export async function apply(ctx, config = {}) {
|
|
|
441
646
|
// 注意: agents 服务**不在这里声明**——本插件的反代职责不应因「某 profile 没有 agent loop」
|
|
442
647
|
// 而整包不加载; agent 驱动改用 ctx.inject(['agents'], ...) 延迟注入(见 agent-driver.js)。
|
|
443
648
|
export const inject = ['webServer'];
|
|
649
|
+
|
|
650
|
+
// 自检入口(2026-10-11): `node lib/index.js --resolve-python` 打印缺省解释器解析结果
|
|
651
|
+
// —— 给 scripts/dev-check-python.mjs 用真 PATH 驱动「python → python3」探测顺序的判决,
|
|
652
|
+
// 也方便排障时在用户机器上一条命令看到「本机探到哪支」。命中输出名字、退出码 0;
|
|
653
|
+
// 一个都探不到输出一行错误、退出码 1。被 dsh import 时 process.argv[1] 不是本文件, 不触发。
|
|
654
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
655
|
+
&& process.argv.includes('--resolve-python')) {
|
|
656
|
+
const found = await resolveDefaultPython();
|
|
657
|
+
if (found) {
|
|
658
|
+
console.log(found);
|
|
659
|
+
process.exit(0);
|
|
660
|
+
}
|
|
661
|
+
console.error(`no python interpreter found (tried: ${PYTHON_CANDIDATES.join(', ')})`);
|
|
662
|
+
process.exit(1);
|
|
663
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/dsh-plugin/src/client.js
CHANGED
|
@@ -105,20 +105,103 @@ function toggleByKey(e) {
|
|
|
105
105
|
/** 宿主页 keydown 监听体(capture 阶段; 函数引用稳定, 便于 dispose 时精确解绑)。 */
|
|
106
106
|
function onHostKey(e) { toggleByKey(e); }
|
|
107
107
|
|
|
108
|
+
// ── 侧栏入口按钮的配色与几何(2026-10-09 修) ──────────────────────────────────
|
|
109
|
+
// 血案: 原先写 color:'var(--dsw-alias-text-l1, #e8e8e8)' —— dsh **没有** text-l1 这族
|
|
110
|
+
// token(真实族是 --dsw-alias-label-primary/secondary/tertiary/caption/dimmed), var() 取不到
|
|
111
|
+
// 就一直吃 fallback: 浅色档在 #f9fafb 的侧栏底上画 #e8e8e8 的字(实测对比度 1.17, 同排的
|
|
112
|
+
// 「插件」「设置」是 18.08), 只有深色档碰巧能看。教训: 引用宿主 token 一律先核实名字。
|
|
113
|
+
// 现行口径(明暗两档都跟宿主走):
|
|
114
|
+
// · 颜色**只取 dsh 语义 token 且不写死 fallback** —— token 缺席时该声明失效并回落到继承
|
|
115
|
+
// (侧栏根 .hHd-Xa_root 已设 label-primary), 而不是钉死一个只对某一档成立的灰;
|
|
116
|
+
// · 尺寸/圆角/gap/行高逐条对齐 dsh 自己的 .SidebarRoot_panelRow(min-height 36 / 内边距
|
|
117
|
+
// 7px 8px / margin 0 2px / gap 8 / line-height 22 / --dsw-radius-md), 收起态对齐
|
|
118
|
+
// .hHd-Xa_collapsed .panelRow(36x36 居中);
|
|
119
|
+
// · 图标盒 16x16 —— 与 dsh 侧栏 SVG 图标同尺寸; 文字继承侧栏根的 14px。
|
|
120
|
+
const ENTRY_TITLE = 'Touchstone 测试平台(Alt+T 开关)';
|
|
121
|
+
const ENTRY_GLYPH_STYLE = {
|
|
122
|
+
flex: '0 0 16px', width: 16, height: 16, display: 'inline-flex',
|
|
123
|
+
alignItems: 'center', justifyContent: 'center', fontSize: 16, lineHeight: '16px',
|
|
124
|
+
};
|
|
125
|
+
const ENTRY_LABEL_STYLE = {
|
|
126
|
+
minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// 交互态样式(悬停/开合/键盘聚焦环): 伪类没法写在 inline style 里, 落一张最小样式表。
|
|
130
|
+
// 取值逐条对齐 dsh 侧栏行(.hHd-Xa_panelRow:hover 与 :focus-visible)。
|
|
131
|
+
const ENTRY_CSS = [
|
|
132
|
+
'.ts-sb-entry{background:transparent}',
|
|
133
|
+
'.ts-sb-entry:hover,.ts-sb-entry.ts-sb-on{background:'
|
|
134
|
+
+ 'var(--dsw-alias-interactive-bg-hover,color-mix(in srgb,currentColor 8%,transparent))}',
|
|
135
|
+
'.ts-sb-entry:focus-visible{outline-width:var(--dsw-focus-ring-width,2px);outline-style:solid;'
|
|
136
|
+
+ 'outline-color:var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));'
|
|
137
|
+
+ 'outline-offset:-2px}',
|
|
138
|
+
].join('');
|
|
139
|
+
const ENTRY_CSS_ID = 'touchstone/sidebar-entry.css';
|
|
140
|
+
|
|
141
|
+
/** 幂等挂上入口行交互态样式表(形态与 dsh 插件自身的注入方式一致)。
|
|
142
|
+
* **失败一律吞掉**: 挂不上只是少了悬停/聚焦环的观感, 绝不能让侧栏入口或面板受影响
|
|
143
|
+
* (桩 DOM 自检环境里 document 面本就不全, 真机异常同理)。
|
|
144
|
+
* @returns 摘除函数(插件卸载时调用; 已存在/挂不上时返回空操作)。 */
|
|
145
|
+
function mountEntryStyle() {
|
|
146
|
+
try {
|
|
147
|
+
if (typeof document === 'undefined' || !document.head
|
|
148
|
+
|| typeof document.createElement !== 'function'
|
|
149
|
+
|| typeof document.querySelector !== 'function') {
|
|
150
|
+
return () => {};
|
|
151
|
+
}
|
|
152
|
+
if (document.querySelector('style[data-plugin-css=' + JSON.stringify(ENTRY_CSS_ID) + ']')) {
|
|
153
|
+
return () => {};
|
|
154
|
+
}
|
|
155
|
+
const tag = document.createElement('style');
|
|
156
|
+
tag.dataset.plugin = 'touchstone';
|
|
157
|
+
tag.dataset.pluginCss = ENTRY_CSS_ID;
|
|
158
|
+
tag.textContent = ENTRY_CSS;
|
|
159
|
+
document.head.appendChild(tag);
|
|
160
|
+
return () => tag.remove();
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.warn('[touchstone] 侧栏入口样式注入失败(只影响悬停/聚焦观感):', error);
|
|
163
|
+
return () => {};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
108
167
|
// 侧栏入口按钮(footer.action 槽; wide=侧栏展开态显示文字)
|
|
109
168
|
function Toggle(props) {
|
|
110
|
-
const wide = props && props.wide;
|
|
169
|
+
const wide = !!(props && props.wide);
|
|
111
170
|
const isOpen = useOpen();
|
|
171
|
+
const style = {
|
|
172
|
+
boxSizing: 'border-box', display: 'flex', alignItems: 'center',
|
|
173
|
+
border: 'none', cursor: 'pointer', textAlign: 'left',
|
|
174
|
+
color: 'var(--dsw-alias-label-primary)', // 无 fallback: 取不到即回落继承(见上注)
|
|
175
|
+
borderRadius: 'var(--dsw-radius-md, 8px)',
|
|
176
|
+
fontSize: 'inherit', // 侧栏根是 14px, 不再自带 13px
|
|
177
|
+
};
|
|
178
|
+
if (wide) {
|
|
179
|
+
style.gap = 8;
|
|
180
|
+
style.margin = '0 2px';
|
|
181
|
+
style.padding = '7px 8px';
|
|
182
|
+
style.minHeight = 36;
|
|
183
|
+
style.lineHeight = '22px';
|
|
184
|
+
style.flex = '1 1 auto';
|
|
185
|
+
style.minWidth = 0;
|
|
186
|
+
} else {
|
|
187
|
+
style.width = 36;
|
|
188
|
+
style.height = 36;
|
|
189
|
+
style.margin = 0;
|
|
190
|
+
style.padding = 0;
|
|
191
|
+
style.flex = 'none';
|
|
192
|
+
style.justifyContent = 'center';
|
|
193
|
+
}
|
|
112
194
|
return h('button', {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
title: 'Touchstone 测试平台(Alt+T 开关)',
|
|
195
|
+
type: 'button',
|
|
196
|
+
className: 'ts-sb-entry' + (isOpen ? ' ts-sb-on' : ''),
|
|
197
|
+
style,
|
|
198
|
+
title: ENTRY_TITLE,
|
|
199
|
+
'aria-label': ENTRY_TITLE,
|
|
200
|
+
'aria-expanded': isOpen,
|
|
120
201
|
onClick: () => setOpen(!isOpen),
|
|
121
|
-
},
|
|
202
|
+
},
|
|
203
|
+
h('span', { style: ENTRY_GLYPH_STYLE, 'aria-hidden': 'true' }, '🧪'),
|
|
204
|
+
wide ? h('span', { style: ENTRY_LABEL_STYLE }, isOpen ? '关闭 Touchstone' : 'Touchstone') : null);
|
|
122
205
|
}
|
|
123
206
|
|
|
124
207
|
// 关闭按钮自动隐藏(2026-10-03): 常显会盖住被嵌 SPA 侧栏底部的用户名, 所以平时完全隐藏,
|
|
@@ -330,6 +413,8 @@ function Panel() {
|
|
|
330
413
|
|
|
331
414
|
// cordis 客户端插件入口: 等 slots 服务就位后注册两个 UI 槽, 再挂快捷键
|
|
332
415
|
function apply(ctx) {
|
|
416
|
+
// 侧栏入口的交互态样式表(悬停/开合/聚焦环), 与本插件同生命周期
|
|
417
|
+
ctx.effect(() => mountEntryStyle(), 'touchstone: 侧栏入口样式');
|
|
333
418
|
const slots = ctx.get('slots');
|
|
334
419
|
if (!slots) return;
|
|
335
420
|
slots.inject('sidebar.footer.action', () => slots.register(
|