@linxin666/dsh-pet 0.1.6 → 0.1.8

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 CHANGED
@@ -47,8 +47,8 @@ dsh-pet/
47
47
  | |-- persist.ts # 持久化($DSH_HOME/pet.json,原子写入)
48
48
  | |-- routes.ts # /api/pet/* JSON API + /pet/whale/* 素材静态路由
49
49
  | `-- client/ # 浏览器半区
50
- | |-- index.ts # slots 注册 + 轮询(800ms)+ 交互接线(fetch)
51
- | |-- PetDockEntry.tsx # conversation.input.selector.context 挂载点
50
+ | |-- index.ts # 全局挂载(createRoot body)+ 轮询(800ms)+ 交互接线(fetch)
51
+ | |-- PetDockEntry.tsx # 全局浮层入口(document.body,无会话/新会话/会话中全程显示)
52
52
  | |-- WhalePet.tsx # 浮层组件(portal + rAF 帧动画 + 拖动)
53
53
  | |-- spritesheet.ts # 图集几何 + 每状态动画轨道(帧/时长)
54
54
  | `-- pet.module.css
@@ -61,13 +61,13 @@ dsh-pet/
61
61
  ```
62
62
  activity/status session 事件(原 working-activity 插件发布) --> PetService(host)
63
63
  | /api/pet/* JSON
64
- conversation.input.selector.context 槽位 <-- 轮询 800ms -- pet-client(浏览器)
64
+ global React root(createRoot → document.body) <-- 轮询 800ms -- pet-client(浏览器)
65
65
  |
66
66
  WhalePet 浮层(portal + rAF)
67
67
  ```
68
68
 
69
69
  - **状态源**:监听 `activity/status` 会话事件(phase: idle/waiting/thinking/tool/done + 状态短语),由 host 半区消费;该事件曾由 working-activity 插件发布,插件已从本仓库移除,未安装时宠物只随会话生命周期变化。
70
- - **挂载点**:`conversation.input.selector.context`(list 槽位,输入选择行,无会话/新会话/会话中全程挂载——旧挂载点 `conversation.composer.dock` 只在活跃会话渲染,导致新会话界面看不到宠物),组件内部 `createPortal` 渲染全局浮层。
70
+ - **挂载点**:`document.body`(全局 React root,无会话/新会话/会话中全程显示——旧挂载点 `conversation.composer.dock` 只在活跃会话渲染,导致新会话界面看不到宠物),组件内部 `createPortal` 渲染全局浮层。
71
71
  - **渲染**:CSS sprite(background-position)逐帧动画,帧时长来自 `spritesheet.ts` 的轨道定义。
72
72
  - **通信**:浏览器 ↔ host 走同源 `/api/pet/*` JSON 端点(state/interact/set-visible/set-config),图集从 `/pet/whale/spritesheet.webp` 加载——RPC 域与 `/plugins/` 静态服务都是平台注册的,插件自足地提供自己的 API 与素材(与 dsh-remote-web-ui 的 `/api/pair` 同一模式)。
73
73
 
package/lib/client.js CHANGED
@@ -4,8 +4,9 @@ window.__ModuleLoader__.load({
4
4
  var module = { exports: {} };
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
- let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
8
7
  let react = require("react");
8
+ let react_dom_client = require("react-dom/client");
9
+ let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
9
10
  let react_dom = require("react-dom");
10
11
  let react_jsx_runtime = require("react/jsx-runtime");
11
12
  //#region src/client/pet-store.ts
@@ -304,7 +305,7 @@ window.__ModuleLoader__.load({
304
305
  }
305
306
  //#endregion
306
307
  //#region \0dsh-css:/home/runner/work/dsh-web-ui/dsh-web-ui/packages/dsh-pet/src/client/pet.module.css.mjs
307
- const css$1 = ".l0P8iq_float{pointer-events:auto;-webkit-user-select:none;user-select:none;flex-direction:column;align-items:center;display:flex;position:fixed}.l0P8iq_sprite{image-rendering:auto;touch-action:none;position:relative}.l0P8iq_bubble{white-space:nowrap;color:#fff;pointer-events:none;border-radius:999px;margin-bottom:6px;padding:4px 10px;font-size:12px;line-height:1.4;animation:2.6s ease-out forwards l0P8iq_pet-bubble-pop;position:absolute;bottom:100%;box-shadow:0 2px 8px #00000040}.l0P8iq_bubblePet{background:#f472b6eb}.l0P8iq_bubbleFeed{background:#38bdf8eb}@keyframes l0P8iq_pet-bubble-pop{0%{opacity:0;transform:translateY(6px)scale(.85)}15%{opacity:1;transform:translateY(0)scale(1.05)}25%{transform:translateY(0)scale(1)}75%{opacity:1}to{opacity:0;transform:translateY(-8px)scale(.95)}}.l0P8iq_panel{color:#e2e8f0;backdrop-filter:blur(6px);background:#0f172aeb;border:1px solid #94a3b859;border-radius:10px;flex-direction:column;gap:6px;min-width:132px;padding:8px 10px;font-size:12px;display:flex;position:absolute;bottom:calc(100% + 4px);box-shadow:0 4px 16px #00000059}.l0P8iq_rankRow{white-space:nowrap;justify-content:space-between;gap:10px;display:flex}.l0P8iq_nameCell{font-weight:600}.l0P8iq_renameRow{align-items:center;gap:6px;display:flex}.l0P8iq_nameInput{color:#e2e8f0;background:#1e293be6;border:1px solid #7dd3fc80;border-radius:6px;outline:none;flex:1;min-width:0;padding:3px 6px;font-size:12px}.l0P8iq_nameInput:focus{border-color:#38bdf8;box-shadow:0 0 0 2px #38bdf873}.l0P8iq_actions{gap:6px;display:flex}.l0P8iq_action{cursor:pointer;color:#0f172a;background:linear-gradient(#7dd3fc,#38bdf8);border:none;border-radius:6px;flex:1;padding:4px 8px;font-size:12px;transition:filter .12s,box-shadow .12s}.l0P8iq_action:hover{filter:brightness(1.08)}.l0P8iq_action:active{filter:brightness(.94)}.l0P8iq_action:focus-visible{outline:none;box-shadow:0 0 0 2px #38bdf8d9}.l0P8iq_summon{color:#7dd3fc;cursor:pointer;background:#0f172abf;border:1px dashed #7dd3fc99;border-radius:999px;padding:2px 10px;font-size:11px;transition:border-color .12s,color .12s,background .12s,box-shadow .12s}.l0P8iq_summon:hover{color:#bae6fd;background:#0f172ae6;border-color:#7dd3fcf2}.l0P8iq_summon:active{color:#7dd3fc;border-color:#7dd3fccc}.l0P8iq_summon:focus-visible{outline:none;box-shadow:0 0 0 2px #38bdf8d9}@media (prefers-reduced-motion:reduce){.l0P8iq_bubble{opacity:1;animation:none}.l0P8iq_action,.l0P8iq_summon{transition:none}}";
308
+ const css$1 = ".l0P8iq_float{pointer-events:auto;-webkit-user-select:none;user-select:none;flex-direction:column;align-items:center;display:flex;position:fixed}.l0P8iq_sprite{image-rendering:auto;touch-action:none;position:relative}.l0P8iq_bubble{white-space:nowrap;color:#fff;pointer-events:none;border-radius:999px;margin-bottom:6px;padding:4px 10px;font-size:12px;line-height:1.4;animation:2.6s ease-out forwards l0P8iq_pet-bubble-pop;position:absolute;bottom:100%;box-shadow:0 2px 8px #00000040}.l0P8iq_bubblePet{background:#f472b6eb}.l0P8iq_bubbleFeed{background:#38bdf8eb}@keyframes l0P8iq_pet-bubble-pop{0%{opacity:0;transform:translateY(6px)scale(.85)}15%{opacity:1;transform:translateY(0)scale(1.05)}25%{transform:translateY(0)scale(1)}75%{opacity:1}to{opacity:0;transform:translateY(-8px)scale(.95)}}.l0P8iq_panel{color:#e2e8f0;backdrop-filter:blur(6px);background:#0f172aeb;border:1px solid #94a3b859;border-radius:10px;flex-direction:column;gap:6px;min-width:132px;padding:8px 10px;font-size:12px;display:flex;position:absolute;bottom:100%;box-shadow:0 4px 16px #00000059}.l0P8iq_panel:after{content:\"\";height:14px;position:absolute;top:100%;left:0;right:0}.l0P8iq_rankRow{white-space:nowrap;justify-content:space-between;gap:10px;display:flex}.l0P8iq_nameCell{font-weight:600}.l0P8iq_renameRow{align-items:center;gap:6px;display:flex}.l0P8iq_nameInput{color:#e2e8f0;background:#1e293be6;border:1px solid #7dd3fc80;border-radius:6px;outline:none;flex:1;min-width:0;padding:3px 6px;font-size:12px}.l0P8iq_nameInput:focus{border-color:#38bdf8;box-shadow:0 0 0 2px #38bdf873}.l0P8iq_actions{gap:6px;display:flex}.l0P8iq_action{cursor:pointer;color:#0f172a;background:linear-gradient(#7dd3fc,#38bdf8);border:none;border-radius:6px;flex:1;padding:4px 8px;font-size:12px;transition:filter .12s,box-shadow .12s}.l0P8iq_action:hover{filter:brightness(1.08)}.l0P8iq_action:active{filter:brightness(.94)}.l0P8iq_action:focus-visible{outline:none;box-shadow:0 0 0 2px #38bdf8d9}.l0P8iq_summon{color:#7dd3fc;cursor:pointer;background:#0f172abf;border:1px dashed #7dd3fc99;border-radius:999px;padding:2px 10px;font-size:11px;transition:border-color .12s,color .12s,background .12s,box-shadow .12s}.l0P8iq_summon:hover{color:#bae6fd;background:#0f172ae6;border-color:#7dd3fcf2}.l0P8iq_summon:active{color:#7dd3fc;border-color:#7dd3fccc}.l0P8iq_summon:focus-visible{outline:none;box-shadow:0 0 0 2px #38bdf8d9}@media (prefers-reduced-motion:reduce){.l0P8iq_bubble{opacity:1;animation:none}.l0P8iq_action,.l0P8iq_summon{transition:none}}";
308
309
  const tagId$1 = "@linxin666/dsh-pet/pet.module.css";
309
310
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
310
311
  const tag = document.createElement("style");
@@ -364,6 +365,7 @@ window.__ModuleLoader__.load({
364
365
  const [nameDraft, setNameDraft] = (0, react.useState)("");
365
366
  const [dragPos, setDragPos] = (0, react.useState)(null);
366
367
  const dragRef = (0, react.useRef)(null);
368
+ const hideTimerRef = (0, react.useRef)(null);
367
369
  const frameRef = (0, react.useRef)({
368
370
  track: null,
369
371
  index: 0,
@@ -440,6 +442,12 @@ window.__ModuleLoader__.load({
440
442
  return () => window.clearTimeout(timer);
441
443
  }, [feedback]);
442
444
  const draggedRef = (0, react.useRef)(false);
445
+ const clearHideTimer = () => {
446
+ if (hideTimerRef.current !== null) {
447
+ window.clearTimeout(hideTimerRef.current);
448
+ hideTimerRef.current = null;
449
+ }
450
+ };
443
451
  const onPointerDown = (e) => {
444
452
  e.preventDefault();
445
453
  e.target.setPointerCapture?.(e.pointerId);
@@ -487,11 +495,15 @@ window.__ModuleLoader__.load({
487
495
  bottom: pos.bottom,
488
496
  zIndex: 2147483e3
489
497
  },
490
- onPointerEnter: () => setHovered(true),
498
+ onPointerEnter: () => {
499
+ clearHideTimer();
500
+ setHovered(true);
501
+ },
491
502
  onPointerLeave: (e) => {
492
503
  const next = e.relatedTarget;
493
504
  if (next instanceof Node && floatRef.current?.contains(next)) return;
494
- setHovered(false);
505
+ clearHideTimer();
506
+ hideTimerRef.current = window.setTimeout(() => setHovered(false), 300);
495
507
  },
496
508
  children: [
497
509
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -522,6 +534,9 @@ window.__ModuleLoader__.load({
522
534
  }, feedback.at),
523
535
  hovered && dragRef.current === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
524
536
  className: pet_module_css_default.panel,
537
+ onPointerEnter: () => {
538
+ clearHideTimer();
539
+ },
525
540
  children: renaming ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
526
541
  className: pet_module_css_default.renameRow,
527
542
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
@@ -598,11 +613,13 @@ window.__ModuleLoader__.load({
598
613
  //#endregion
599
614
  //#region src/client/PetDockEntry.tsx
600
615
  /**
601
- * Dock anchor inside `conversation.input.selector.context`: the input
602
- * selector row mounts in EVERY conversation phase (no-session cold start,
603
- * the blank-session hero, and the active seat), so the floating pet stays on
604
- * screen on the new-conversation screen too. While visible it mounts the
605
- * floating WhalePet (portal); while hidden it renders the summon button.
616
+ * Global floating pet entry. The pet is host-global (its state, display and
617
+ * interactions live on `/api/pet/*` endpoints with no session dimension), so
618
+ * it must not ride a session-scoped slot on the new-conversation screen no
619
+ * session exists to scope a slot by, and the pet would vanish (issue #48).
620
+ * The client half therefore mounts this entry straight onto `document.body`
621
+ * (see index.ts): while visible it renders the floating WhalePet (a portal),
622
+ * while hidden it renders a fixed-position summon button.
606
623
  * @module @linxin666/dsh-pet/client/PetDockEntry
607
624
  */
608
625
  const DEFAULT_DISPLAY = {
@@ -643,9 +660,16 @@ window.__ModuleLoader__.load({
643
660
  t: props.t
644
661
  })
645
662
  });
663
+ const display = snapshot?.display ?? DEFAULT_DISPLAY;
646
664
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
647
665
  type: "button",
648
666
  className: pet_module_css_default.summon,
667
+ style: {
668
+ position: "fixed",
669
+ right: display.right,
670
+ bottom: display.bottom,
671
+ zIndex: 2147483e3
672
+ },
649
673
  onClick: props.summon,
650
674
  "data-testid": "pet-summon",
651
675
  children: props.t("pet.summon", { name: snapshot?.name ?? "鲸鱼娘" })
@@ -1343,6 +1367,28 @@ window.__ModuleLoader__.load({
1343
1367
  "settings.saveFailed": "The deployment did not accept these values; they were left for you to correct.",
1344
1368
  "settings.invalidNumber": "Enter a number, or leave blank to use the default."
1345
1369
  };
1370
+ /**
1371
+ * Active dictionary, picked by the document language at call time. The pet
1372
+ * mounts as a global floating surface (not a session-scoped slot), so it has
1373
+ * no framework locale seat and resolves its copy the same tiny way the
1374
+ * task-board's DOM-injected surface does.
1375
+ */
1376
+ function dictionary() {
1377
+ return (typeof document !== "undefined" ? document.documentElement.lang : "zh").toLowerCase().startsWith("en") ? en : zh;
1378
+ }
1379
+ /**
1380
+ * Translate a key with optional `{name}` template params. Mirrors the slot
1381
+ * `Translate` contract `(key, params?) => string` so it can be handed to the
1382
+ * same components that used to receive the framework-injected `t` seat. The
1383
+ * key is typed loosely (`string`) so the function is assignable to the slot's
1384
+ * `TranslateNS<'pet'>` (whose key domain also spans the shared common
1385
+ * vocabulary); a missing key degrades to the key itself rather than throwing.
1386
+ */
1387
+ function t(key, params) {
1388
+ let text = dictionary()[key] ?? key;
1389
+ if (params !== void 0) for (const [name, value] of Object.entries(params)) text = text.replaceAll(`{${name}}`, String(value));
1390
+ return text;
1391
+ }
1346
1392
  //#endregion
1347
1393
  //#region src/client/index.ts
1348
1394
  /** Same-origin JSON fetch helper (GET without body, POST with JSON body). */
@@ -1376,9 +1422,9 @@ window.__ModuleLoader__.load({
1376
1422
  "remote"
1377
1423
  ];
1378
1424
  /**
1379
- * Client plugin body: register dictionaries, mount the dock entry and poll
1380
- * loop while the plugin is enabled, and seat the settings card in the Web UI
1381
- * plugin group.
1425
+ * Client plugin body: register dictionaries, mount the global pet entry and
1426
+ * poll loop while the plugin is enabled, and seat the settings card in the
1427
+ * Web UI plugin group.
1382
1428
  * @param ctx - client root context.
1383
1429
  */
1384
1430
  function apply(ctx) {
@@ -1485,15 +1531,17 @@ window.__ModuleLoader__.load({
1485
1531
  setFeedback(null);
1486
1532
  }
1487
1533
  });
1488
- const disposeDock = ctx.slots.inject("conversation.input.selector.context", () => ctx.slots.register({
1489
- name: "conversation.input.selector.context",
1490
- id: "pet",
1491
- order: 110,
1492
- inject: injected,
1493
- locale: "pet"
1494
- }, PetDockEntry));
1534
+ const container = document.createElement("div");
1535
+ container.dataset.dshPetRoot = "";
1536
+ document.body.appendChild(container);
1537
+ const petRoot = (0, react_dom_client.createRoot)(container);
1538
+ petRoot.render((0, react.createElement)(PetDockEntry, {
1539
+ ...injected(),
1540
+ t
1541
+ }));
1495
1542
  disposeUi = () => {
1496
- disposeDock();
1543
+ petRoot.unmount();
1544
+ container.remove();
1497
1545
  disposePoll();
1498
1546
  disposeUi = void 0;
1499
1547
  };