# dsh-lumina-tarot · 系统设计文档 > 日期:2026-08-19 > 版本:v0.9 > 仓库:`dsh-lumina-tarot`(DeepSeek Harness 插件) > 领域资产来源:`tt-lumina-tarot`(Vue 3 离线塔罗 PWA) > 配套: > - `tt-lumina-tarot/design/2026-04-24-02-系统设计.md`(原 Web App 视觉与流程) > - `tt-lumina-tarot/src/views/SettingsView.vue`(设置页分组与文案,本文件 §5 对照迁移) > - `tt-lumina-tarot/src/views/ShuffleView.vue`(洗牌 CSS 参考;本插件改为点击即自动洗,动画当 loading) > - DeepSeek Harness 官方:[Your first plugin](https://deepseek-harness.github.io/deepseek-harness/en/develop/basic/) · [Build a tool](https://deepseek-harness.github.io/deepseek-harness/en/develop/basic/tool) · [Adding a settings card](https://deepseek-harness.github.io/deepseek-harness/en/reference/cookbook/adding-a-settings-card) · [Adding a conversation node](https://deepseek-harness.github.io/deepseek-harness/en/reference/cookbook/adding-a-conversation-node) > 变更日志: > - v0.9:安装生命周期对齐官方 `dsh plugin`(add / update / remove);明确不自动更新,改完须重启 `dsh web` > - v0.8:右击改为贴牌的**扇形快捷菜单**(四牌阵 + 上次结果 + 查看历史);卡面 / 今日一牌 / 重置位置不进右击,走设置页或命令 > - v0.7:悬浮牌桌面尺寸定为约 **48×83**(仍 5:8.6);DSH overlay 入口比原 PWA sm 档更小,不再用 96×165 > - v0.6:悬浮牌 **单击抽牌**,**右击**打开快捷菜单(阻止浏览器默认菜单) > - v0.5:`floatX` / `floatY` 仅拖拽落盘,设置页不提供手填坐标(可选「重置位置」) > - v0.4:补充安装 / 卸载;明确 Host+Client 是同一 bundle,一条 `add` 两面一起加载 > - v0.3:悬浮入口改为可拖拽的塔罗**牌背**(不是卡通宠物);外观跟 `effectiveCardBack` > - v0.2:点击洗牌后自动播放洗牌动画作为 loading;补充「双面插件」说明;明确不搬长按 / GSAP / 粒子 / Three.js --- ## 1. 目标与边界 ### 1.1 产品目标 把 Lumina 做成 **DeepSeek Harness Web 上的对话占卜插件**:壳上浮着一张可拖拽的塔罗**牌背**,点开后抽牌;半透明面板展示结果,一键让当前会话的模型解读;模型也可以在对话框里直接调工具抽牌。偏好设置嵌进 DSH 设置,分组与语义尽量对齐原 PWA 设置页。 ### 1.2 非目标(v0.1) - **不搬原 PWA 的仪式交互与重引擎**:长按 3 秒触发、pointer capture、GSAP、tsparticles、Three.js。洗牌改为 **点击即自动开始**,用 CSS 牌叠动画当 loading(见 §4.2)。 - 不提供独立 URL 路由(`/tarot`);DSH Web 无第三方页面座椅,SPA fallback 已被占用。 - 不覆盖 DSH 壳的 Appearance / Language;Lumina 主题只作用于插件自己的悬浮牌背、面板与牌面。 - 悬浮入口 **不是** 卡通桌宠 / 人偶,而是一张按 5:8.6 比例绘制的塔罗牌背面。 - 不从原 PWA 的 `localStorage` / IndexedDB 自动导入历史(运行时与存储都不同,见 §5.5)。 - 不打包 Aquatic 牌面图进 npm(CC BY-NC-SA 3.0,仅限非商业)。v0.1 默认极简符号牌面;RWS 图源若引入须单独声明 Public Domain。 ### 1.3 运行约束 | 约束 | 结论 | |------|------| | Harness 形态 | **dual-face**:Host 管能力,Client 管 UI | | 浏览器 | 无 `ctx.agents`,不能直接 `followup` | | 第三方 UI | 只能 `slots.inject` 已有座椅,不能自建 SlotMap | | 设置持久化 | Host `installSettingsSection`;远程非 loopback 浏览器设置 RPC 不可用 | | Electron | 优先 `commands` / `settings` remote,不自建 HTTP | --- ## 2. 使用场景 ### 2.1 用户点悬浮牌背 1. Web 壳右下(默认可拖)浮着一张塔罗牌**背面**(图案随设置的牌背 / 卡面主题)。 2. **拖动**:位移超过阈值只改位置,松手写入 `floatX` / `floatY`,不抽牌、不弹层。 3. **单击(左键)**:立刻按当前默认牌阵抽牌,打开半透明面板进入 **洗牌 loading**(无需再选菜单)。 4. **右击**:阻止浏览器默认菜单,打开贴在牌附近的**扇形快捷菜单**(牌阵 / 上次结果 / 查看历史,见 §4.1)。 5. 洗牌动画结束(与 Host 抽牌结果汇合)→ 翻开展示牌面(正逆位、关键词、牌位名)。 6. 点「让 AI 解读」→ 当前会话收到一条 followup,模型基于**既定牌面**撰写解读。 ### 2.2 模型在对话框唤起 用户说「帮我看看最近的事业运」。模型调用 `lumina_draw`,对话流出现牌阵卡;可选同步打开 overlay 面板。禁止模型自己编造牌 id。 ### 2.3 改偏好 打开 DSH 设置 → 侧栏 **Lumina 塔罗**(独立 section)。分组对齐原「偏好与数据」页:主题、语言、卡面、牌背、小阿风格、动画、数据、关于。另增插件专有项(是否显示悬浮牌背、默认牌阵、逆位率、面板透明度)。 --- ## 3. 架构 ### 3.0 什么是双面插件(dual-face) DSH Web 把「能做什么」和「画成什么样」拆在两个进程: | 半边 | 跑在哪 | 入口 | 本插件放什么 | |------|--------|------|----------------| | **Host** | Node(`dsh web` 主进程) | `exports["."]` → `lib/index.js` | 抽牌算法、tools、slash command、settings schema、解读 prompt | | **Client** | 浏览器(React 壳) | `exports["./client"]` → `lib/client.js` | 悬浮牌背、洗牌 loading、半透明结果页、设置页、对话牌阵卡 | 一句话:**同一 npm 包,两份入口。** Host 先挂进 Loader;若 `package.json` 声明了 `dsh.client.platform: "web"`,壳会再拉 `/plugins/<包名>/client.js` 跑浏览器半边。 为什么必须拆:浏览器 **没有** `ctx.agents` / `ctx.tools.execute`。抽牌、followup 只能在 Host 发生;Client 只能 `slots.inject` 画 UI,经 `remote.commands` / `settingsScope` 回头调 Host。CLI / headless profile 只加载 Host,没有悬浮牌背,但模型仍能调 `lumina_draw`。 不是「两个插件」,也不是前后端分仓库。`cordis.patch.yml` 只 insert **一行** Host 插件;Client 由 `dsh.client` 清单自动跟出。 ### 3.1 目录(双面仍是一个包) ``` dsh-lumina-tarot/ ├── package.json # exports "." + "./client";dsh.bundle + dsh.client ├── cordis.patch.yml # insert Host 插件行 ├── src/ │ ├── index.ts # Host apply:tools / commands / settings / skill │ ├── domain/ # 从 tt-lumina-tarot src/data 导出的 JSON(不 import Vue) │ ├── tools.ts │ ├── commands.ts │ └── settings-schema.ts # 与 Client 共用的 namespace + Config └── src/client/ ├── index.ts # Client apply:overlay + settings.section + toolview ├── dock/ # 可拖拽悬浮牌背 ├── panel/ # 半透明面板:洗牌 loading → 结果 ├── shuffle/ # CSS 牌叠洗牌动画(loading,无 GSAP) ├── settings/ # 设置页(迁移自 SettingsView) └── toolview/ # lumina_draw 对话卡片 ``` `package.json` 要点: ```json { "exports": { ".": "./lib/index.js", "./client": "./lib/client.js" }, "dsh": { "bundle": { "patch": "./cordis.patch.yml" }, "client": { "platform": "web", "inject": [ "@deepseek-ai/dsh-client-ui-layout", "@deepseek-ai/dsh-client-ui-settings" ] } } } ``` Client bundle 必须是 loader 的 lazy-CJS(`window.__ModuleLoader__.load`)。React 从 loader `require('react')` 取得,禁止再打一份 React。仓库外没有官方 `tsdown` client preset,构建需自行复现该产物格式。 ### 3.2 座椅与能力对照 | 需求 | 机制 | 官方程度 | |------|------|----------| | 可拖拽牌背 + 半透明结果页 | Client `shell.overlay` | 座椅官方;拖拽/半透明/洗牌 loading 为组件自绘。社区先例:`dsh-community-hot`(浮钮形态不同,座椅相同) | | 点「解读」 | Host `ctx.commands.register` + Client `ctx.remote.commands.execute` → `agent.followup` | 官方 Human Commands | | AI 抽牌 | Host `ctx.tools.register(defineTool)` | 官方 | | 对话里的牌阵卡 | Client `tool.call.toolview`,key = 工具名 | 官方 | | 设置页 | Host `installSettingsSection` + Client `settings.section` | Host 官方;section 为完整设置页(见 §5.1 为何不用 `settings.plugin.item`) | | 解读规范 | skill + `systemPrompt.section` | 官方 | ### 3.3 Client → Host 禁止与允许 禁止:Client 调 `ctx.agents` / 自建 `/api/lumina/*`(Electron `file://` 会断)。 允许: - 抽牌、查牌:Host tool / `/lumina draw`。Client 点「抽牌」后 **立刻** 播洗牌 loading,同时 `commands.execute('/lumina draw …')`;结果回来且动画播满最短时长后才翻开展示(见 §4.2)。 - 解读:`ctx.remote.commands.execute(sessionId, '/lumina interpret')`。 - 改设置:`ctx.settingsScope.bind({ namespace: 'lumina-tarot' })` 的 `set` / `unset`。 ### 3.4 overlay 与 session `shell.overlay` 是 **root 作用域,没有 `useSession`**。必须在 Client `apply` 里镜像 `sessions.list.current`,经 inject `hooks` 喂给组件(同 `dsh-client-ui-question-history`)。否则「解读」会打到错误会话。 --- ## 4. 界面规格 ### 4.1 悬浮牌背(`shell.overlay` 收起态) 入口是 **一张塔罗牌的背面**,不是图标、不是人偶。 - 比例固定 **5 : 8.6**(与原设计卡牌规范一致)。桌面 overlay 入口约 **48×83**(不是原 PWA sm 的 96×165;壳上要小一档才不像挡对话)。 - **只显示背面**:走与设置相同的 `effectiveCardBack`——`cardArtTheme === 'minimal'` 时用 `cardBack` 五种 SVG 图案;`rws` / `aquatic` 时用该主题配套牌背图(v0.1 无图源则回退极简图案)。 - 圆角随 `theme`(mystic 锐 / minimal 中 / nature 更圆),与原主题规范一致。 - 默认贴视口右下,可拖。**拖到哪里就是哪里**:松手后把百分比坐标写入 `floatX` / `floatY`(clamp 到视口)。这是交互落盘,**不是**设置页里要用户手填的两项。 - **指针手势**(互斥,拖过阈值后本次不触发单击/右击抽牌): - **拖**:位移 ≥ 约 6px → 只改位置,松手 `set('floatX'|'floatY')`。 - **单击(左键,button=0)**:立即 `/lumina draw`(当前 `defaultSpread`),进入洗牌 loading。不要先弹菜单。 - **右击(button=2 / `contextmenu`)**:`preventDefault()`,打开贴在牌附近的**扇形快捷菜单**。不翻到正面,不出现带标题的设置面板。 - **右击菜单内容**(扇形,绕牌排布;牌在视口下半/右半时扇向上/向左,避免出屏): - **牌阵**四张小牌:单张指引 / 三牌时间线 / 十字 / 凯尔特精简。点选后写入 `defaultSpread`,关掉菜单,打开提问框;提交后 `/lumina draw {spreadId}`。 - **上次结果**:`/lumina last`,展开最近一条。 - **查看历史**:打开本插件历史层。 - **不进右击**:卡面主题、今日一牌、重置位置。卡面与重置在 DSH 设置 → Lumina 塔罗;今日一牌走 `/lumina today` / `lumina_today`(结果页「再洗一次」可复用)。 - `showFloatCard: false` 时不渲染这张牌;仍可通过 `/lumina` 与 tools 使用。 - `order` 取高值(如 `1000`)。`full` 可用极轻的悬浮/呼吸阴影;`off` 静态。 ### 4.2 抽牌面板:洗牌 loading → 结果 同一 `shell.overlay` 展开态,分两相,**不出现长按圆环**。 **触发**:悬浮牌 **单击**、右击选牌阵后在提问框提交、或结果页「再洗一次」。一次触发即自动洗,无 3 秒 hold。今日一牌不走右击,走 `/lumina today` / `lumina_today`。 **Loading 相(洗牌效果)** - 打开半透明遮罩后立刻进入。文案沿用「洗牌中…」。 - 视觉:一叠牌背(约 5–7 张)在中心左右错位翻飞。动画用 **CSS `@keyframes`**,可参考原 `ShuffleView.vue` 的 `shuffle-dance`(`--i` 错相、rotate / translate)。**不引入 GSAP / 粒子 / Three.js。** - 语义:这是 loading,不是随机源。真实抽牌在 Host 完成;Client 不得在动画结束前用本地 `Math.random` 当结果。 - 时序:`显示结果 = max(动画最短时长, Host command 返回)`。 - `animationLevel = full`:最短约 1.6–2.0s - `lite`:最短约 0.7s,牌张更少、位移更小 - `off`:无翻飞,短促淡入或静态牌叠 +「抽牌中…」,仍等 Host 返回 - Host 已返回但最短时长未到:继续播完,不提前揭晓。 - Host 失败:停动画,面板内错误态 + 重试,不展示假牌。 - loading 期间禁用再点抽牌 / 解读,避免并发。 **结果相** - 全屏遮罩:`background: rgba(0,0,0, panelOpacity)`,可 `backdrop-filter`。 - 居中卡片:牌阵名、问题(若有)、按位排牌(比例约 5:8.6,档位对齐原设计 sm/md)。 - 单卡:正/逆位标记、关键词、核心含义;详细 love/career/advice 可折叠。可选一次翻开(`full` / `lite` 错开 tens of ms;`off` 直接亮出)。 - 主按钮「让 AI 解读」;次按钮关闭、再洗一次(再次进入 loading 相)。 模型在对话里调 `lumina_draw` 时:对话卡可立即出结构化结果;若 overlay 已打开,可用更短的 lite 洗牌过渡,不必强行 full 时长。 ### 4.3 对话卡片(`tool.call.toolview`) `lumina_draw` / `lumina_today` 的 canonical JSON 出牌 id、位置、正逆;`output.render` 给模型短摘要。UI 卡展示缩略牌阵,不把叙事解读写进 tool 返回值。 ### 4.4 视觉主题作用域 `theme` ∈ `mystic | minimal | nature` **只作用插件 overlay / 牌面 / 设置页内预览**,使用一套插件私有 CSS 变量(可沿用原 HSL token 语义名),**不得**改 `document.documentElement` 去抢 DSH `ui-theme`。 --- ## 5. 设置页迁移 原实现:`tt-lumina-tarot/src/views/SettingsView.vue` + `src/stores/settings.ts`。 DSH 壳:`sidebar.settings` 打开设置模态;`ui-settings-general` 投影 `settings.section` 为侧栏页;Plugins 页用 `settings.plugin.item` 按 **namespace** 挂第三方卡([Adding a settings card](https://deepseek-harness.github.io/deepseek-harness/en/reference/cookbook/adding-a-settings-card))。 ### 5.1 挂载策略(保证可迁移、不重复) | 方案 | 说明 | 本插件 | |------|------|--------| | 仅 `settings.plugin.item` | 官方第三方默认,出现在「插件配置」Tab,空间窄 | 不作为主界面 | | **`settings.section` 独立页** | 侧栏一项「Lumina 塔罗」,可放下视觉选择器 | **采用** | | 两者都注册 | 设置里出现两份 Lumina | 禁止 | Host 仍必须 `installSettingsSection(ctx, 'lumina-tarot', Config, …)`。Plugins Tab 对「已服务、无 card」的 namespace **不渲染**,因此只挂 `settings.section` 不会留下空卡。 不用纯 schema 自动表单:原页是带预览的选择器网格,自动表单迁不过去。卡片/页面 **自绘控件**,经 `settingsScope.set(field, value)` 逐字段写入(官方:一次写入一个字段,无跨字段事务)。 Client 设置页 `inject`:`['slots', 'locale', 'connection', 'remote', 'settingsScope']`。远程非 loopback 时 scope 为 `unavailable`,页展示只读说明,不提供写入。 无法程序化 `openSection('lumina-tarot')`(Discussion #1230)。右击只做牌阵 / 上次结果 / 历史;完整偏好仍靠用户自己打开 DSH 设置 → Lumina 塔罗。 ### 5.2 原设置项 → 插件字段 字段名尽量沿用 `settings.ts`,便于对照与日后双向文档化。 | 原 UI 分组 | 原存储 | 插件字段 | 迁移策略 | |------------|--------|----------|----------| | 主题 mystic / minimal / nature | `localStorage lumina-theme` | `theme` | **迁语义**。默认 `mystic`。只染插件 UI,不改 DSH Appearance | | 语言 zh-CN / en-US | i18n + locale | `locale` + `followDshLocale` | **迁并改默认**。`followDshLocale: true` 时跟 DSH Language;false 时用 `locale`。牌面文案跟插件 locale | | 卡面极简 / 韦特 / 水彩 | `lumina-card-art-theme` | `cardArtTheme` | **迁枚举**。v0.1 默认 `minimal`(符号/emoji)。`rws` / `aquatic` 仅当资源可用;aquatic 保留授权警告文案 | | 牌背 5 种 | `lumina-card-back` | `cardBack` | **迁枚举**。仅 `cardArtTheme === 'minimal'` 时有效;与原页同样隐藏条件 | | 小阿 符号 / 几何 | `lumina-minor-style` | `minorStyle` | **迁枚举**。同上,仅极简卡面时展示 | | 动画 off / lite / full | `lumina-animation-level`;旧键 `lumina-reduced-motion` | `animationLevel` | **迁枚举**。默认:若系统 `prefers-reduced-motion` 则 `lite`,否则 `full`。控制 **洗牌 loading 时长/幅度** 与翻牌入场,不关 DSH 壳,不上粒子 | | 导出 JSON | Dexie readings | 设置页按钮 | **迁动作**。导出插件历史 JSONL/JSON,格式见 §6.3 | | 清空历史 | `readingStore.clearAll` | 设置页按钮 | **迁动作**。确认文案沿用「将清空所有占卜历史…」 | | 关于 + 站点二维码 | 分享站 URL | 关于文案 + 版本 | **弱化**。保留 about 两段文案;不做 PWA 二维码(宿主是 DSH) | 原 `effectiveCardBack` 规则原样保留:`rws` / `aquatic` 锁定主题配套牌背,隐藏「牌背图案」与「小阿插画」两组。 ### 5.3 插件新增项(原 PWA 没有) | 字段 | 类型 | 默认 | 说明 | |------|------|------|------| | `followDshLocale` | boolean | `true` | 避免与 DSH Language 双轨打架 | | `showFloatCard` | boolean | `true` | 关闭后不显示悬浮牌背,只留对话 tools / slash | | `floatX` / `floatY` | number 0–1 | `0.92` / `0.82` | **内部字段**。只由拖拽松手写入;设置页不展示数字输入。可选提供「重置位置」→ `unset('floatX')` + `unset('floatY')` 回到默认右下 | | `panelOpacity` | number 0.2–0.8 | `0.45` | 半透明遮罩 | | `defaultSpread` | `'single' \| 'three-card' \| 'cross' \| 'celtic-lite'` | `'three-card'` | 悬浮牌「抽牌」所用默认阵 | | `reversedRate` | number 0–1 | `0.35` | 对齐 `tt-lumina-tarot/src/lib/tarot.ts` | | `historyLimit` | number | `100` | 对齐原历史 100 条上限 | ### 5.4 Host Schema(示意) namespace:`lumina-tarot`(kebab-case,Host / Client 必须同一字符串)。 ```ts export const LUMINA_NS = settingsNamespace('lumina-tarot') export const Config = z.object({ theme: z.union(['mystic', 'minimal', 'nature']).default('mystic'), followDshLocale: z.boolean().default(true), locale: z.union(['zh-CN', 'en-US']).default('zh-CN'), cardArtTheme: z.union(['minimal', 'rws', 'aquatic']).default('minimal'), cardBack: z.union(['classic', 'celestial', 'sacred', 'floral', 'eye']).default('classic'), minorStyle: z.union(['symbol', 'geometric']).default('symbol'), animationLevel: z.union(['off', 'lite', 'full']).default('full'), showFloatCard: z.boolean().default(true), floatX: z.number().min(0).max(1).default(0.92), floatY: z.number().min(0).max(1).default(0.82), panelOpacity: z.number().min(0.2).max(0.8).default(0.45), defaultSpread: z.union(['single', 'three-card', 'cross', 'celtic-lite']).default('three-card'), reversedRate: z.number().min(0).max(1).default(0.35), historyLimit: z.number().step(1).min(1).max(500).default(100), }) ``` `applies: 'live'`。`cardBack` / `minorStyle` 在非 `minimal` 时 schema 仍保存,UI 隐藏;切回极简后恢复,与原页一致。 `validate`:`reversedRate` 已由 min/max 约束;若以后加「aquatic 且未提供图源」则在 validate 拒绝写入。 ### 5.5 存储对照(不自动迁移) | 原 PWA | 插件 | |--------|------| | 偏好:多把 `localStorage` 键 | 单一 user-settings 文档的 `lumina-tarot` section | | 历史 / daily:Dexie `lumina-tarot` | Host 侧 JSONL(或 settings 不可承载的文件),上限 `historyLimit` | | 进行中的 reading:kv `lumina-current-reading` | overlay 内存 + 可选 command 会话态 | 不读取用户机器上的 `lumina-theme` 等键:那是另一个源的浏览器源。设置页「导出 / 清空」只操作本插件数据。 ### 5.6 设置页信息架构(对齐 SettingsView 卡片顺序) 1. **主题** — 三选一;文案沿用「选择一个最符合你当下心境的氛围」。预览用色板块,不嵌入 `ThemePicker.vue`。 2. **语言** — 开关「跟随 DSH 语言」;关闭后出现 中 / En 分段按钮。 3. **卡面主题** — 三列选择器;非极简时显示 `cardArtImageHint`;aquatic 显示授权警告。 4. **牌背图案** — `cardArtTheme === 'minimal'` 时显示,5 宫格。v0.1 可用简化 SVG,不强制移植全部 `CardBackPattern.vue`。 5. **小阿卡那插画风格** — 同上条件,两列。 6. **动画强度** — 三列 off / lite / full。文案改为强调「洗牌 loading 与翻牌」,不再提粒子背景。 7. **抽牌默认** — 默认牌阵、逆位率、面板透明度、显示悬浮牌背;可选「重置悬浮牌位置」。**不要**放 X/Y 输入框(**新增**,放在原「动画」与「数据」之间)。 8. **数据** — 导出 JSON、清空历史。 9. **关于** — 两段 about + `Lumina Tarot · plugin` 版本行。 控件文案优先复用 `zh-CN.ts` / `en-US.ts` 的 `settings.*` 键,不要另造一套同义标签。 --- ## 6. 领域能力(Host) ### 6.1 Tools | 名称 | 作用 | |------|------| | `lumina_list_spreads` | 列出四种牌阵 | | `lumina_draw` | `spreadId` + 可选 `seed`;返回牌位、正逆、关键词、正逆位字段 | | `lumina_lookup_card` | id 或中英文名 | | `lumina_today` | 日期 seed,对齐原「今日一牌」稳定性 | `execute` 只返回 canonical JSON。叙事留给模型。抽牌算法与 `drawCards(spread, reversedRate)` 同规则;v0.1 在插件内重写,不 import Vue `@/`。牌数据用构建脚本从 `tt-lumina-tarot/src/data` 生成 JSON。 ### 6.2 Commands | 命令 | 行为 | |------|------| | `/lumina draw [spreadId]` | Host 抽牌,持久化「当前结果」,可 `sourceEventSeq` 指向领域事件 | | `/lumina interpret` | 将当前结果 JSON 写入 `followup`,要求按 skill 解读、禁止改牌 | | `/lumina today` | 今日一牌 | Client 面板按钮只 execute 上述行,不拼模型可见的自由文本(避免绕过 tool)。 ### 6.3 历史记录形状 对齐 `ReadingRecord`: ```ts { id: string createdAt: number spreadId: string question?: string mood?: string cards: Array<{ cardId: string; reversed: boolean; positionIndex: number }> note?: string } ``` 导出 JSON 包一层 `{ exportedAt, theme, history }`,与原 `exportJSON()` 同形,便于用户在 PWA 与插件之间**手工**互导。v0.1 不保证 PWA 导入按钮能读插件文件(PWA 侧未改)。 ### 6.4 Skill 边界 - 必须先有抽牌/查牌结果再解读。 - 语气:克制、陪伴、不宿命论。 - 结构:总览 → 按牌位 → 综合 → 可执行建议。 - 不医疗、不法律、不承诺具体事件。 - 牌阵选择:快问 `single`,脉络 `three-card`,抉择 `cross`。 --- ## 7. 数据与版权 - 78 张牌文案、四种牌阵、心情列表:从 `tt-lumina-tarot/src/data` 生成,保持 id(`fool`、`three-card` 等)。 - v0.1 牌面:Unicode / 简化 SVG。 - RWS 图:Public Domain,若引入须走可配置资源目录,不默认打进包。 - Aquatic:默认不启用;设置里保留选项与警告,无资源则选择失败或回退 `minimal`。 --- ## 8. 安装、更新与卸载 **一次 `add` 就够。** Host 与 Client 不是两个插件、也没有两条安装命令。用户装的是 **一个** npm bundle(包名 `dsh-lumina-tarot`): 1. `dsh plugin add` 把该包链进 web profile,并插入 **一行** Host 配置(`cordis.patch.yml`)。 2. 同一包的 `package.json` 里若有 `dsh.client.platform: "web"` 且导出了 `./client`,`dsh web` 启动时会 **自动** 再加载浏览器半边(`/plugins/dsh-lumina-tarot/client.js`)。 没有「再 add 一次 Client」的步骤。`remove` 同样只针对这一个包名,两面一起卸掉。 headless / 非 web profile:同一条 `add` 仍只装这一个包,但壳不拉 Client,所以没有悬浮牌背,Host 的 tools / commands 仍在。 包名:`dsh-lumina-tarot`(与 `dsh plugin remove` 参数一致)。前提:本机已安装 `dsh` CLI。Client UI 只在 **web profile** 出现,对应 `@deepseek-ai/dsh-web-app`。 `dsh plugin --profile …` 会转发到该 profile 目录里的 pnpm。首次对某个 profile 执行 `add` 时会初始化 profile(默认已含 `@deepseek-ai/dsh-base`),并把本包追加进 `dsh.profile.bundles`。 ### 8.1 安装 **npm(普通用户,0.1.2 起可用)** ```sh dsh plugin --profile web add dsh-lumina-tarot ``` 装的是 registry 里打好的 `lib/`,不跑 `prepare`。 桌面版 App 用的是它自己的 `desktop` profile,CLI 拒绝直接操作(`profile "desktop" is managed exclusively by the Electron application`),但 **App 的「插件 → 添加插件」面板可以装**:填同一个包名 `dsh-lumina-tarot`,App 会自己写入该 profile 的 `dependencies`、`dsh.profile.bundles` 和 `cordis.patch.yml`,重启 App 生效。 **本地开发(link 当前仓库)** 在包含本仓库的目录执行: ```sh dsh plugin --profile web add ./dsh-lumina-tarot ``` 若当前目录已是仓库根: ```sh dsh plugin --profile web add . ``` **GitHub(不装 npm 版本时)** ```sh dsh plugin --profile web add github:/dsh-lumina-tarot # 建议钉 commit,避免后续 push 悄悄改掉已装内容 dsh plugin --profile web add github:/dsh-lumina-tarot# ``` Git 安装拉的是源码,**不会**跑普通 `build`。作者必须提供自包含的 `prepare`(打出 `lib/index.js` 与 `lib/client.js`)。pnpm ≥10 默认拦截 git 依赖的 `prepare`,第一次 `add` 可能失败;按 CLI 提示把包名写入该 profile 的 `pnpm-workspace.yaml` 后重跑: ```yaml allowBuilds: dsh-lumina-tarot: true ``` **npm / tarball(预构建,无需 allowBuilds)** ```sh dsh plugin --profile web add dsh-lumina-tarot dsh plugin --profile web add ./dsh-lumina-tarot-0.1.0.tgz # pnpm pack 产物 ``` **核对与启动** ```sh dsh --profile web --dump-config # 应出现 "# == dsh-lumina-tarot" 层 dsh web # 或:dsh --profile web ``` 改 Client 代码后:开发态依赖 HMR;已安装的 link 包通常需重启 `dsh web`,浏览器再刷新。无 Client 的 headless profile 装上后只有 Host tools / commands,没有悬浮牌背。 **不写入 profile 的临时加载(实现期)** ```sh dsh web --patch ./cordis.yml ``` `--patch` 不替代 `dsh plugin add`。交付给用户必须走 bundle 安装。 ### 8.2 更新 DSH **不会**轮询 GitHub / npm / dsh.pub。插件版本写在 profile 的 `dependencies` + lockfile 里;启动只加载这次开机时装上的 Bundle。 `dsh plugin --profile …` 把参数转发给该 profile 目录的 pnpm,成功后再 reconcile `dsh.profile.bundles`。因此更新就是官方这条,不要在插件里自建检测或热替换: ```sh dsh plugin --profile web update dsh-lumina-tarot ``` 然后**重启** `dsh web`。正在跑的进程仍用当前启动的 Bundle 集合。 | 当初怎么装 | `update` 实际做什么 | |---|---| | `github:…/dsh-lumina-tarot`(不钉 commit) | pnpm 再解析默认分支,可能拉到新 HEAD | | `github:…#` | 钉死该 commit,不会跟着默认分支走;要换提交就再 `add` 并写新 SHA | | `add .`(开发链接) | 不走远端 `update`。改源码后 `pnpm build`,重启 `dsh web`,刷新浏览器 | | tgz / 已钉死的 semver | 范围内才升;tgz 要换包就卸掉再 `add` 新文件 | 设置页「关于」只展示这条官方命令,方便已经装上的用户找到步骤;不轮询远端、不声称有新版本。 ### 8.3 卸载 ```sh dsh plugin --profile web remove dsh-lumina-tarot ``` 会同时去掉 profile 的 pnpm 依赖和 `dsh.profile.bundles` 里的层。然后重启 `dsh web`(或刷新已开的页):悬浮牌背、设置里的「Lumina 塔罗」、对话里的 `lumina_*` tools 应消失。 卸载 **不会**自动清掉 user-settings 文档里的 `lumina-tarot` section,也不删 Host 侧历史 JSONL。再次安装会读回旧偏好。若要连数据一起去掉,需在卸载前用设置页「清空全部历史」,并自行删除 settings 文档中对应 namespace(或整份用户设置文件,影响所有插件,不推荐作为默认步骤)。 ### 8.4 开发期从 Harness 源码跑 CLI 若 `dsh` 不在 PATH、而是在 `deepseek-harness` 仓库里:在该仓库根用 `pnpm dsh …` 代替上文的 `dsh …`,profile / add / remove 参数相同。 --- ## 9. 实现顺序 1. dual-face 空包 + `dsh web` 能加载 Client。 2. `shell.overlay` 悬浮牌背:可拖;**单击抽牌**;**右击**扇形快捷菜单;图案跟 `effectiveCardBack`。 3. 半透明面板 + **点击即洗牌 CSS loading** + mock / 真实抽牌汇合。 4. Host `lumina_draw` + 对话 generic/toolview。 5. `/lumina interpret` 接通 followup。 6. `settings.section` 按 §5.6 迁移分组(先枚举控件,预览可后补)。 7. 接入 78 张 JSON 与 skill。 8. 导出/清空历史。 --- ## 10. 风险与验收 ### 10.1 风险 - Harness developer preview,slot / remote / CJS 产物可能破。依赖面收敛在 §3.2 列表。 - overlay 无 session → 必须镜像当前会话。 - 多 overlay 抢 z-index。 - `settingsScope.set` 单字段写入:快速连点可能产生多个 revision;控件需用读到的 revision 做乐观锁。 - 非 loopback 设置不可写。 ### 10.2 验收清单 - [ ] `dsh plugin --profile web add ./dsh-lumina-tarot` 后 `dump-config` 含本包层;重启 `dsh web` 后右下出现可拖**牌背**(5:8.6),刷新后位置仍在。 - [ ] `dsh plugin --profile web remove dsh-lumina-tarot` 并重启后,悬浮牌背与 `lumina_*` tools 消失。 - [ ] 改设置里的牌背 / 卡面主题后,悬浮牌外观跟着变。 - [ ] **单击**进入抽牌流程,不先出右击菜单;**拖动**不抽牌;**右击**打开扇形快捷菜单(牌阵 / 上次结果 / 查看历史)且不出现浏览器默认菜单。 - [ ] 点「抽牌」无需长按,自动进入洗牌 loading;`full` 有牌叠翻飞,`off` 无翻飞仍等待 Host。 - [ ] 洗牌结束后面板展示牌位与正逆;loading 期间无法连点抽第二次。 - [ ] 抽牌面板半透明,结果含牌位与正逆。 - [ ] 「让 AI 解读」在当前对话产生基于该牌面的解读,模型未改牌。 - [x] 对话框「帮我占卜」会调 `lumina_draw`,出现牌阵卡。 - [ ] 设置 → Lumina 塔罗:§5.6 分组齐全;改 `theme` 只变 overlay 不变 DSH 壳。 - [ ] `followDshLocale: true` 时插件文案跟 DSH 语言。 - [ ] `cardArtTheme !== minimal` 时牌背/小阿两组隐藏。 - [ ] 导出 JSON 含 `history` 数组;清空需确认且不可恢复。 - [ ] Headless profile:无 UI 报错,tools 仍注册。 --- ## 11. 参考 - 原设置页:`tt-lumina-tarot/src/views/SettingsView.vue` - 原设置 store:`tt-lumina-tarot/src/stores/settings.ts` - 原文案:`tt-lumina-tarot/src/i18n/locales/zh-CN.ts` → `settings` - 抽牌:`tt-lumina-tarot/src/lib/tarot.ts`(`reversedRate = 0.35`) - 洗牌 CSS 参考:`tt-lumina-tarot/src/views/ShuffleView.vue`(`shuffle-dance`;本插件去掉长按) - 牌阵:`tt-lumina-tarot/src/data/spreads.ts` - DSH 设置 cookbook:`adding-a-settings-card` - DSH 安装插件:[Package and install a plugin](https://deepseek-harness.github.io/deepseek-harness/en/develop/basic/publish) - overlay 先例:`littleboylittlegirl/dsh-community-hot`(`shell.overlay`;本插件改存 settings)