dsh-stall-guard 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,44 @@
5
5
  格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
6
6
  版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
7
7
 
8
+ ## [0.1.5] - 2026-10-21
9
+
10
+ ### 界面
11
+
12
+ - **浮动模式变成鱼缸**:图标外框现在是一缸水 —— 半透明蓝色水体、顶部水面亮线、
13
+ 缓慢左右摆动的波光、向下扫过的波纹,以及向上浮动的水泡。
14
+ 全部用 CSS 关键帧实现,空闲时不占用 JavaScript;`prefers-reduced-motion` 下自动停止。
15
+ - **卡顿水花**:警报时一圈水花炸开 + 5 滴水珠飞散,播一次后自动清除,不会循环闪。
16
+ - **图标恢复为最初的圆鲸鱼**:圆身体、翘尾鳍、顶鳍、大眼与微笑。这是用户偏好的形态,
17
+ 之前两次重画(侧游小鱼、官方剪影风)都已撤掉。仅补上尾鳍/顶鳍的动画钩子。
18
+ - 尾鳍随状态摆动:闲置慢摆(5.4 秒)、监视中小幅(3.2 秒)、卡顿急摆(0.3 秒)。
19
+ - 水泡改为按比例尺寸,小图标下也看得见。
20
+
21
+ ### 修复
22
+
23
+ - **切到浮动后图标消失**:标题栏座位的守卫 `supervisorOff || floating` 把 overlay 座位
24
+ 也挡掉了(它收到的 floating 恒为 true)。现区分「本座位该画的形态」与「用户选择的形态」。
25
+ - **拖动图标会带动标题栏**:浮动态改由 `shell.overlay` 渲染,脱离文档流。
26
+ - **调整大小时看不到实时预览**:设置面板从标题栏下方展开(`top:118px`),不再压住图标。
27
+ 外观状态统一走 `useAppearance()`(`useSyncExternalStore`),三个消费点同步重绘。
28
+ - **气泡点击会打开面板**:改为关闭提示(`acknowledge`)。点鱼图标才是打开设置。
29
+ - **气泡被挤成竖排**:固定宽度 200px,最多 4 行。
30
+ - **气泡在 `<button>` 内部**:移到按钮外作为兄弟节点,独立处理点击与键盘激活。
31
+
32
+ ## [0.1.4] - 2026-10-21
33
+
34
+ ### 修复
35
+
36
+ - **切到「浮动」后图标消失**:0.1.3 把浮动态拆到 overlay 渲染时,标题栏座位的守卫
37
+ `if (supervisorOff || floating) return null` 同时也把 overlay 座位挡掉了 ——
38
+ 因为 overlay 座位收到的 `floating` 恒为 true。结果是图标彻底不画。
39
+ 现在区分「这个座位要画的形态(forcedFloat)」与「用户选择的形态(wantsFloat)」:
40
+ 标题栏在 `wantsFloat && !forcedFloat` 时让位,overlay 座位永不让位。
41
+ - **两个座位可能同时画图标**:overlay 座位现在只在 `place === 'float'` 时渲染。
42
+ - 外观状态改为共享 `useAppearance()` 钩子(`useSyncExternalStore`),
43
+ 三个消费点(标题栏、浮动态、面板)对同一份记录同步重绘,
44
+ 修掉面板改尺寸时图标不实时变化的问题。
45
+
8
46
  ## [0.1.3] - 2026-10-21
9
47
 
10
48
  ### 修复
package/README.md CHANGED
@@ -124,7 +124,18 @@ $env:DSH_PROFILE # 桌面版一般是 desktop
124
124
  |---|---|---|
125
125
  | `dot` | 状态栏 6px 半透明点 | 变红脉冲 |
126
126
  | `icon` | 静止图标 | 弹跳 + 发光 |
127
- | `avatar` | 闭眼小鲸鱼 | 弹跳 + 气泡说出原因 |
127
+ | `avatar` | 闭眼圆鲸鱼 | 变红、尾鳍急摆、水花炸开 |
128
+
129
+ ### 鱼缸(浮动模式)
130
+
131
+ 把图标设为「浮动」后,它外面会套一个鱼缸:
132
+
133
+ - 半透明蓝色水体、顶部水面亮线(呼吸)、缓慢左右摆的波光、向下扫的波纹
134
+ - 5 颗按比例缩放的**上浮水泡**
135
+ - 卡顿时一圈**水花炸开 + 5 滴水珠飞散**,播一次就停,不循环闪
136
+
137
+ 全部是 CSS 关键帧,空闲时不跑 JavaScript;系统开启「减少动态效果」时自动停止。
138
+ 图标本身也有动作:尾鳍闲置慢摆、监视中小幅、卡顿急摆。
128
139
 
129
140
  ### 显示开关
130
141
 
package/client.js CHANGED
@@ -931,6 +931,9 @@ window.__ModuleLoader__.load({
931
931
  /* ---- supervisor seat ---- */
932
932
  '.dsg-seat{position:relative;display:inline-flex;flex:none;align-items:center;justify-content:center;pointer-events:auto;overflow:visible}',
933
933
  '.dsg-orb{box-sizing:border-box;display:inline-flex;position:relative;align-items:center;justify-content:center;margin:0;padding:0;border:none;background:transparent;font-family:inherit;cursor:pointer;overflow:visible;border-radius:var(--dsw-radius-sm,6px);pointer-events:auto}',
934
+ /* The floating figure is a tank: clip the water to its frame and stack the whale above it. */
935
+ '.dsg-orb[data-floating="yes"]{overflow:hidden}',
936
+ '.dsg-orb[data-floating="yes"]>svg{position:relative;z-index:1}',
934
937
  '.dsg-orb:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4d6bfe);outline-offset:2px}',
935
938
  '.dsg-orb[data-display="dot"]{width:22px;height:22px}',
936
939
  '.dsg-orb[data-display="icon"]{width:24px;height:24px}',
@@ -1052,6 +1055,37 @@ window.__ModuleLoader__.load({
1052
1055
  '@keyframes dsg-glow{0%,100%{filter:drop-shadow(0 0 0 rgba(236,19,19,0))}50%{filter:drop-shadow(0 0 5px rgba(236,19,19,.85))}}',
1053
1056
  '@keyframes dsg-sway{0%,100%{transform:rotate(-6deg)}50%{transform:rotate(6deg)}}',
1054
1057
  '@keyframes dsg-bounce-big{0%,100%{transform:translateY(0) scale(1)}30%{transform:translateY(-8px) scale(1.06)}60%{transform:translateY(0) scale(1)}80%{transform:translateY(-3px) scale(1)}}',
1058
+ /* ---- aquarium: the floating posture reads as a fish tank ---- */
1059
+ /* Three layers behind the whale: a translucent water body, a caustic shimmer that
1060
+ * suggests light through water, and bubbles rising on their own keyframes. All are
1061
+ * pointer-events:none so the tank never eats a click meant for the figure. */
1062
+ '.dsg-tank{position:absolute;inset:0;display:block;pointer-events:none;border-radius:var(--dsw-radius-sm,10px);overflow:hidden;background:linear-gradient(180deg,rgba(86,140,220,.28) 0%,rgba(40,88,170,.42) 60%,rgba(18,46,110,.5) 100%);border:1px solid rgba(150,200,255,.35);box-shadow:inset 0 2px 10px rgba(0,20,60,.45),0 2px 12px rgba(0,0,0,.32)}',
1063
+ '.dsg-tank::before{content:"";position:absolute;inset:0;pointer-events:none;background:radial-gradient(ellipse 60% 40% at 50% 0%,rgba(190,225,255,.35),transparent 70%);mix-blend-mode:screen;animation:dsg-caustic 7s ease-in-out infinite}',
1064
+ '.dsg-tank::after{content:"";position:absolute;left:0;right:0;top:-40%;height:34%;pointer-events:none;background:linear-gradient(180deg,transparent,rgba(150,205,255,.22),transparent);animation:dsg-wave 5.5s linear infinite}',
1065
+ '.dsg-tank-waterline{position:absolute;left:0;right:0;top:0;height:2px;pointer-events:none;background:linear-gradient(90deg,transparent,rgba(190,230,255,.75),transparent);opacity:.85;animation:dsg-waterline 3.6s ease-in-out infinite}',
1066
+ '.dsg-tank-bubble{position:absolute;bottom:6%;border-radius:50%;pointer-events:none;background:radial-gradient(circle at 32% 28%,rgba(255,255,255,.9),rgba(180,220,255,.35) 55%,rgba(120,180,255,.18) 100%);box-shadow:0 0 3px rgba(180,220,255,.5);animation:dsg-rise linear infinite}',
1067
+ '@keyframes dsg-rise{0%{transform:translateY(0) scale(.7);opacity:0}12%{opacity:.85}70%{opacity:.7}100%{transform:translateY(-260%) scale(1.05);opacity:0}}',
1068
+ '@keyframes dsg-caustic{0%,100%{opacity:.5;transform:translateX(-4%)}50%{opacity:.85;transform:translateX(4%)}}',
1069
+ '@keyframes dsg-wave{0%{transform:translateY(0)}100%{transform:translateY(320%)}}',
1070
+ '@keyframes dsg-waterline{0%,100%{opacity:.55}50%{opacity:1}}',
1071
+ /* One-shot splash for an alarm: droplets fly outward, then the class is removed. */
1072
+ '.dsg-splash{position:absolute;inset:0;pointer-events:none;border-radius:50%;border:2px solid rgba(190,232,255,.85);opacity:0;animation:dsg-splash-out .62s ease-out forwards}',
1073
+ '.dsg-splash-drop{position:absolute;left:50%;top:50%;width:3px;height:3px;border-radius:50%;pointer-events:none;background:rgba(215,240,255,.95);animation:dsg-drop-out .62s ease-out forwards}',
1074
+ '@keyframes dsg-splash-out{0%{opacity:.9;transform:scale(.35);border-width:3px}100%{opacity:0;transform:scale(1.15);border-width:1px}}',
1075
+ '@keyframes dsg-drop-out{0%{opacity:1;transform:translate(-50%,-50%) scale(1)}100%{opacity:0;transform:translate(calc(-50% + var(--dx)),calc(-50% + var(--dy))) scale(.4)}}',
1076
+ '@media (prefers-reduced-motion:reduce){.dsg-tank::before,.dsg-tank::after,.dsg-tank-waterline,.dsg-tank-bubble,.dsg-splash,.dsg-splash-drop{animation:none}}',
1077
+ /* ---- the whale: gentle when calm, a flick when alarmed ---- */
1078
+ '.dsg-avatar{overflow:visible}',
1079
+ '.dsg-avatar[data-mode="idle"] .dsg-whale-tail{animation:dsg-whale-drift 5.4s ease-in-out infinite;transform-origin:19.6px 13px}',
1080
+ '.dsg-avatar[data-mode="watching"] .dsg-whale-tail{animation:dsg-whale-swish 3.2s ease-in-out infinite;transform-origin:19.6px 13px}',
1081
+ '.dsg-avatar[data-mode="alert"] .dsg-whale-tail{animation:dsg-whale-flick .3s ease-in-out infinite;transform-origin:19.6px 13px}',
1082
+ '.dsg-avatar[data-mode="watching"] .dsg-whale-fin{animation:dsg-fin-wave 2.6s ease-in-out infinite;transform-origin:14px 8px}',
1083
+ '.dsg-avatar[data-mode="alert"] .dsg-whale-fin{animation:dsg-fin-wave .8s ease-in-out infinite;transform-origin:14px 8px}',
1084
+ '@keyframes dsg-whale-drift{0%,100%{transform:rotate(-4deg)}50%{transform:rotate(5deg)}}',
1085
+ '@keyframes dsg-whale-swish{0%,100%{transform:rotate(-8deg)}50%{transform:rotate(9deg)}}',
1086
+ '@keyframes dsg-whale-flick{0%,100%{transform:rotate(-22deg) scaleY(1)}50%{transform:rotate(22deg) scaleY(.85)}}',
1087
+ '@keyframes dsg-fin-wave{0%,100%{transform:rotate(-4deg)}50%{transform:rotate(6deg)}}',
1088
+ '@media (prefers-reduced-motion:reduce){.dsg-whale-tail,.dsg-whale-fin{animation:none!important}}',
1055
1089
  '@keyframes dsg-bubble-in{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}',
1056
1090
  '@media (prefers-reduced-motion:reduce){.dsg-orb[data-mode] .dsg-dot,.dsg-orb[data-mode] .dsg-glyph,.dsg-orb[data-mode] .dsg-avatar,.dsg-bubble{animation:none}}',
1057
1091
  ].join('\n');
@@ -1101,6 +1135,21 @@ window.__ModuleLoader__.load({
1101
1135
  * 28px original DeepSeek-blue whale. Eyes are closed with a line while the
1102
1136
  * supervisor is idle and open dots otherwise; nothing external is loaded.
1103
1137
  */
1138
+ /**
1139
+ * A fish, drawn for the tank.
1140
+ *
1141
+ * Three moods, driven by `mode`:
1142
+ * idle - slow bob, closed eye, tail drifting
1143
+ * watching - upright, eye open, gentle fin motion
1144
+ * alert - red body, mouth open, fast tail flick and a small scale burst
1145
+ */
1146
+ /**
1147
+ * The whale, as originally drawn.
1148
+ *
1149
+ * A plump 28x28 whale: round body, raised tail fluke, top fin, one eye, a small smile.
1150
+ * The user preferred this silhouette over later redraws, so it is kept verbatim; only
1151
+ * class names are added to the two fins so the tank animations can target them.
1152
+ */
1104
1153
  function WhaleGlyph(props) {
1105
1154
  const size = props.size || 28;
1106
1155
  const asleep = props.mode === 'idle';
@@ -1115,14 +1164,17 @@ window.__ModuleLoader__.load({
1115
1164
  xmlns: 'http://www.w3.org/2000/svg',
1116
1165
  'aria-hidden': 'true',
1117
1166
  focusable: 'false',
1167
+ 'data-mode': props.mode || 'idle',
1118
1168
  },
1119
1169
  /* tail fluke */
1120
1170
  h('path', {
1171
+ className: 'dsg-whale-tail',
1121
1172
  d: 'M19.6 12.4c2.9-3.4 5.1-3.1 5.1.5 0 3.3-2.4 3.7-5.1.6z',
1122
1173
  fill: fin,
1123
1174
  }),
1124
1175
  /* top fin */
1125
1176
  h('path', {
1177
+ className: 'dsg-whale-fin',
1126
1178
  d: 'M11.8 6.4c2.3-2.5 4.4-2.5 4.8 1.1-2.1 1-3.9.6-4.8-1.1z',
1127
1179
  fill: BLUE_LIGHT,
1128
1180
  }),
@@ -1188,6 +1240,19 @@ window.__ModuleLoader__.load({
1188
1240
  }
1189
1241
  }
1190
1242
 
1243
+ /**
1244
+ * Subscribe to the appearance record.
1245
+ *
1246
+ * The record lives outside the polled snapshot, so it needs its own subscription. A
1247
+ * shared hook keeps every consumer honest: an appearance change re-renders all of them.
1248
+ */
1249
+ function useAppearance() {
1250
+ return useSyncExternalStore(
1251
+ (listener) => store.subscribeAppearance(listener),
1252
+ () => store.getAppearance(),
1253
+ );
1254
+ }
1255
+
1191
1256
  function useTranslate(props) {
1192
1257
  return props.t !== undefined && typeof props.t === 'function' ? props.t : (boundT || ((key) => key));
1193
1258
  }
@@ -1208,15 +1273,24 @@ window.__ModuleLoader__.load({
1208
1273
  const snapshot = useStore();
1209
1274
  const orbRef = useRef(null);
1210
1275
  const [bubbleBelow, setBubbleBelow] = useState(false);
1211
- const [appearance, setAppearance] = useState(readAppearance);
1276
+ const appearance = useAppearance();
1212
1277
  const [dragging, setDragging] = useState(false);
1278
+ /* The splash is a one-shot: raised when the alarm starts, cleared on animation end. */
1279
+ const [splash, setSplash] = useState(false);
1280
+ const previousModeRef = useRef(null);
1213
1281
 
1214
1282
  useEffect(() => store.start(), []);
1215
- useEffect(() => store.subscribeAppearance(setAppearance), []);
1216
1283
 
1217
1284
  const config = snapshot.config;
1218
1285
  const supervisorOff = config !== null && config.supervisor.show === false;
1219
1286
  const mode = snapshot.state === null ? 'idle' : snapshot.state.supervisor.mode;
1287
+ /* Raised once per alarm. Declared after `mode` on purpose: a hook that reads a
1288
+ * `const` before its declaration is a TDZ error that takes the whole module down. */
1289
+ useEffect(() => {
1290
+ if (previousModeRef.current === mode) return;
1291
+ previousModeRef.current = mode;
1292
+ if (mode === 'alert') setSplash(true);
1293
+ }, [mode]);
1220
1294
  const display = config === null ? 'avatar' : config.supervisor.display;
1221
1295
  const message = snapshot.state === null ? '' : snapshot.state.supervisor.message;
1222
1296
  const bubbleOn = config === null ? true : config.supervisor.bubble !== false;
@@ -1238,8 +1312,16 @@ window.__ModuleLoader__.load({
1238
1312
 
1239
1313
  /* Float placement: drag moves the control, and the position is clamped into the
1240
1314
  * viewport on release so a window resize cannot strand it off-screen. */
1241
- // The overlay seat forces the floating posture; the header seat reads the setting.
1242
- const floating = props.forcePlace === 'float' || appearance.place === 'float';
1315
+ /**
1316
+ * Which posture this render draws.
1317
+ *
1318
+ * The overlay seat forces floating; the header seat reads the user's setting. The two
1319
+ * must be distinguished, because the header seat has to bail out when the chosen
1320
+ * posture is floating — while the overlay seat must not bail out for the same reason.
1321
+ */
1322
+ const forcedFloat = props.forcePlace === 'float';
1323
+ const wantsFloat = appearance.place === 'float';
1324
+ const floating = forcedFloat || wantsFloat;
1243
1325
  const startDrag = useCallback((event) => {
1244
1326
  if (!floating) return;
1245
1327
  if (event.button !== undefined && event.button !== 0) return;
@@ -1270,10 +1352,11 @@ window.__ModuleLoader__.load({
1270
1352
  event.preventDefault();
1271
1353
  }, [floating]);
1272
1354
 
1273
- // Hidden, or drawn by the overlay because it floats: the header renders nothing in
1274
- // both cases. A floating figure inside the header's flow would push the header row
1275
- // around as it moved, which is exactly the bug this split fixes.
1276
- if (supervisorOff || floating) return null;
1355
+ // The header seat renders nothing when the figure is hidden OR when the overlay owns
1356
+ // it — a floating figure inside the header's flow would push that row around as it
1357
+ // moved. The overlay seat asked for this render, so it never bails out here.
1358
+ if (supervisorOff) return null;
1359
+ if (wantsFloat && !forcedFloat) return null;
1277
1360
 
1278
1361
  const label = t('supervisor') + ' — ' + t(MODE_KEY[mode]);
1279
1362
  const size = appearance.size;
@@ -1284,7 +1367,9 @@ window.__ModuleLoader__.load({
1284
1367
  top: appearance.y < 0 ? 'auto' : appearance.y + 'px',
1285
1368
  right: appearance.x < 0 ? '14px' : 'auto',
1286
1369
  bottom: appearance.y < 0 ? '96px' : 'auto',
1287
- zIndex: 68,
1370
+ /* Above the settings panel (70): the figure must stay visible and draggable
1371
+ * even when it is dragged under the panel. */
1372
+ zIndex: 75,
1288
1373
  }
1289
1374
  : undefined;
1290
1375
 
@@ -1313,7 +1398,32 @@ window.__ModuleLoader__.load({
1313
1398
  display === 'dot' ? h('span', { className: 'dsg-dot' }) : null,
1314
1399
  display === 'icon' ? h(ShieldGlyph, { size: Math.round(size * 0.64) }) : null,
1315
1400
  display === 'avatar' ? h(WhaleGlyph, { size: Math.round(size * 0.82), mode }) : null,
1401
+ /* One-shot splash per alarm. `onAnimationEnd` clears it so it never loops. */
1402
+ splash
1403
+ ? h('span', { className: 'dsg-splash', 'aria-hidden': 'true',
1404
+ onAnimationEnd: () => setSplash(false),
1405
+ },
1406
+ h('span', { className: 'dsg-splash-drop', style: { '--dx': '-14px', '--dy': '-16px' } }),
1407
+ h('span', { className: 'dsg-splash-drop', style: { '--dx': '13px', '--dy': '-18px' } }),
1408
+ h('span', { className: 'dsg-splash-drop', style: { '--dx': '-16px', '--dy': '8px' } }),
1409
+ h('span', { className: 'dsg-splash-drop', style: { '--dx': '17px', '--dy': '6px' } }),
1410
+ h('span', { className: 'dsg-splash-drop', style: { '--dx': '0px', '--dy': '-22px' } }),
1411
+ )
1412
+ : null,
1316
1413
  ),
1414
+ /* Aquarium: only in the floating posture, where the rounded frame reads as a tank.
1415
+ * Everything here paints behind the whale and ignores pointer events. */
1416
+ floating
1417
+ ? h('span', { className: 'dsg-tank', 'aria-hidden': 'true' },
1418
+ h('span', { className: 'dsg-tank-waterline' }),
1419
+ h('span', { className: 'dsg-tank-bubble', style: { width: '15%', height: '15%', left: '16%', animationDuration: '4.2s', animationDelay: '0s' } }),
1420
+ h('span', { className: 'dsg-tank-bubble', style: { width: '11%', height: '11%', left: '33%', animationDuration: '5.6s', animationDelay: '1.1s' } }),
1421
+ h('span', { className: 'dsg-tank-bubble', style: { width: '18%', height: '18%', left: '51%', animationDuration: '4.8s', animationDelay: '0.4s' } }),
1422
+ h('span', { className: 'dsg-tank-bubble', style: { width: '10%', height: '10%', left: '69%', animationDuration: '6.2s', animationDelay: '2.0s' } }),
1423
+ h('span', { className: 'dsg-tank-bubble', style: { width: '14%', height: '14%', left: '83%', animationDuration: '5.1s', animationDelay: '1.6s' } })
1424
+ )
1425
+ : null,
1426
+
1317
1427
  /* The bubble is a SIBLING of the button, not a child: interactive content inside a
1318
1428
  * button is invalid and browsers route its clicks unpredictably, and content painted
1319
1429
  * outside the button box is not part of its hit area. */
@@ -1354,9 +1464,13 @@ window.__ModuleLoader__.load({
1354
1464
  function FloatingSeat(props) {
1355
1465
  const t = useTranslate(props);
1356
1466
  const snapshot = useStore();
1467
+ /* The overlay draws the figure only in the floating posture. Without this check both
1468
+ * seats would render it, and the header would show a second copy. */
1469
+ const appearance = useAppearance();
1357
1470
  useEffect(() => store.start(), []);
1358
1471
  if (snapshot.config === null) return null;
1359
1472
  if (snapshot.config.supervisor.show === false) return null;
1473
+ if (appearance.place !== 'float') return null;
1360
1474
  return h(SupervisorSeat, { t, forcePlace: 'float' });
1361
1475
  }
1362
1476
 
@@ -1401,8 +1515,7 @@ window.__ModuleLoader__.load({
1401
1515
  const presetLocale = hostLocale() === 'en' ? 'en' : 'zh';
1402
1516
  /* Appearance is browser-local, so the panel mirrors it separately from the
1403
1517
  * Host draft; saving the config must not overwrite it and vice versa. */
1404
- const [localAppearance, setLocalAppearance] = useState(() => store.getAppearance());
1405
- useEffect(() => store.subscribeAppearance(setLocalAppearance), []);
1518
+ const localAppearance = useAppearance();
1406
1519
  const [dirty, setDirty] = useState(false);
1407
1520
  const [status, setStatus] = useState({ kind: 'idle', text: '' });
1408
1521
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-stall-guard",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "DSH 卡顿守卫:下载类命令(git clone / pnpm install / pip / curl)超时后自动中止,并把「换个下载源重试」的提示写进工具结果给智能体看,解决 GitHub 在国内网络不稳导致智能体一直干等的问题。另可按工具类型自定义超时秒数,并附一个只在卡顿时唤醒的 DeepSeek 监督员。",
5
5
  "keywords": [
6
6
  "dsh",