# dsh-rp-tools 给 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(dsh)用的 **跑团 / DM 工具插件**: **中立随机裁决**(`rp_random`)+ **本地 ComfyUI 配图**(场景、NPC 立绘、道具线索、氛围图)+ **按会话隔离的战役配置**(世界设定 / 角色卡 / 随机表 / 风格偏好)+ **世界书与状态追踪** + **PNG 故事书(SillyTavern 角色卡)导入** —— 选一张卡就能开团。 面向通用 DM 活动:任何模组、任何战役都能用;配图完全走本机 ComfyUI,不依赖任何云端服务。 > 设计取向:**只依赖 ComfyUI 本身**(直连 `POST /prompt` + 轮询 `/history`), > 不使用 dsh-comfyui 插件的工作流库,因此两者可以各自独立使用。 --- ## 特性 | | | |---|---| | 🎲 **中立随机** | `rp_random`:骰子表达式(`2d6+3` / `d20` / `3d8+1d4-2`)、区间、加权抽取、布尔翻转;`seed` 可复现,`count` 可批量(1..20,**越界报错而不是静默截断**);每一掷都给**可核对**的逐颗明细(`11(2d6[6,2] + 3)`),骰式必须整串合法(`2dd6` / `2d6++3` 一律拒绝) | | 🖼 **本地生图** | `rp_illustrate`:直连本机 ComfyUI,Krea-2 Turbo(8 步 / CFG 1)+ Qwen3-VL 文本编码;1024² 约 13–18 秒,1344×768 约 25 秒 | | 🎨 **10 种风格** | `manga`(黑白漫画,默认,无 LoRA)+ 9 个官方 Krea-2 风格 LoRA(水墨 / 点绘 / 蜡笔 / 抽象 / 雨窗 / 复古动画 / 水彩 / 运动模糊 / 塔罗);每个风格 = 一套生图工作流,含触发词 / CFG / 步数 / 尺寸预设 | | 🧑 **角色卡** | `rp_character`:登记「名字 + 外观」,之后**任何画面描述里提到该名字就自动补外观** —— 保持角色长相一致的主要手段,并可直接出立绘 | | 📖 **PNG 故事书导入** | 工作区那一行的「📖 导入 PNG 故事书」:从本地卡库(SillyTavern PNG 角色卡)选一张 → **世界书**追加进本会话自己的 `rp-sessions/<会话 id>/rp-worldbook.md`、卡全文与卡面落进 `rp-sessions/<会话 id>/cards/`、角色卡/世界写进会话配置、**自动切 `dm` 预设并把开场指令发给 DM**。实测 3269 张卡库:解析 160/160 成功(中位 1 ms) | | 📚 **世界书** | 会话工作区的 `rp-worldbook.md`:`##` 分条,`keys` / `constant` / `order` / `prob` 标记;**只有命中的条目进上下文**(每轮预算 12 条 / 6000 字),被裁的列标题供按需补读 | | 📌 **状态追踪** | `rp_state`:场景 / 时间 / 地点 / 在场 / 线索 + 队伍(状态·持有·伤病·目标)+ 自由旗标;空串即清除;注入在每轮上下文最前 | | 🌍 **会话隔离** | 世界设定 / 角色卡 / 随机表 / 提示词前缀 / 会话默认风格**按会话独立**,互不干扰;真正全局的只有风格库、ComfyUI 地址、全局负面词、全局默认风格、卡库目录 | | 🎬 **整幕批量** | `rp_scenes`:吃 `scenes[].panels[]` 结构(含每格 positive/seed/宽高),一次出一整幕,单格失败不中断 | | 🎲 **随机表** | `rp_table`:遭遇表 / 掉落表 / 情绪表…… 定义(表名 + 骰式 + 条目)、掷表、`count`/`seed`;面板上也能掷 | | 🖥 **三处界面** | 设置页「RP工具」(全局配置 + 风格库 + 工具清单);DM 会话头部的「🎲 RP」按钮(右侧栏面板:世界 / 角色卡 / 随机表 / 本会话生图配置;**只有 DM 会话看得到这个入口**);工作区那一行的「📖 导入 PNG 故事书」 | | 🔒 **只进 DM 会话** | RP 工具**只在 `dm` 预设作用域注册**,其它预设的会话既看不到工具、也没有任何 RP 界面(故事书导入入口只在**空白会话**或 DM 会话出现 —— 否则没法从零开团) | --- ## 安装 ```bash # 从 GitHub 装 dsh plugin --profile web add github:SiriusWJ/dsh-rp-tools ``` 开发期用本地目录(本仓库即源码): ```jsonc // profiles//package.json "dependencies": { "dsh-rp-tools": "file:D:/Code/dsh/rp-tools-plugin" } ``` 安装/改动后**必须重启 `dsh web`**(宿主代码与客户端 bundle 都只在启动时装载)。 ### 前提:本机 ComfyUI(Comfy Desktop 亦可) 模型放在 ComfyUI 的模型目录(`models/`): ``` models/diffusion_models/krea2_turbo_fp8_scaled.safetensors ← 生图主模型(12.2GB) models/text_encoders/qwen3vl_4b_fp8_scaled.safetensors ← 文本编码(4.9GB) models/vae/qwen_image_vae.safetensors ← VAE(0.24GB) models/loras/krea2_*.safetensors ← 9 个风格 LoRA(各约 448MB,可选) ``` 来源:(国内可用 )。 没有 LoRA 也能跑:`manga` 风格不使用 LoRA。ComfyUI 地址默认 `http://127.0.0.1:8188`,可在设置页改。 --- ## 工具 11 个工具,全部以 `rp_` 开头,**全部只在 `dm` 预设作用域注册**。设置页「RP工具 → 工具列表」会实时展示同款清单(含每个参数的说明)。 | 工具 | 作用 | 主要参数 | |---|---|---| | `rp_random` | 中立随机裁决(骰子 / 区间 / 抽取 / 布尔) | `kind?` `dice?` `choices?` `weights?` `min?` `max?` `count?` `seed?` | | `rp_styles` | 列出风格(触发词 / CFG / 步数 / 尺寸预设) | — | | `rp_illustrate` | 按风格生成一张图(并自动存进资源库) | `prompt`* `style?` `seed?` `aspect?` `width?` `height?` `label?` `tags?` `kind?` | | `rp_assets` | **浏览资源库**(找回来复用,不必重出) | `action?` `kind?` `characters?` `tags?` `q?` `limit?` `id?` `label?` | | `rp_character` | 角色卡增删查 + 出立绘 | `action`* `name?` `appearance?` `portrait?` `style?` | | `rp_state` | 状态追踪(场景 / 时间 / 地点 / 在场 / 线索 + 队伍 + 旗标) | `action`* `field?` `value?` `party?` `party_mode?` `party_remove?` `flags?` | | `rp_lore` | 世界书按条读取 / 生成模板 | `action`* `query?` `limit?` | | `rp_session` | 本会话设置(世界 / 前缀 / 会话默认风格 / 风格备注 / 战役名) | `action`* `world?` `prompt_prefix?` `default_style?` `style_notes?` `campaign_name?` | | `rp_scenes` | 按场景文件逐格批量出图(`scenes[].scene_id` + `panels[].panel_id`;整幕共享一个组存进资源库) | `scenesFile`* `sceneId?` `style?` `limit?` `label?` `tags?` | | `rp_config` | **全局**配置(负面词 / 全局默认风格 / ComfyUI 地址 / 单个风格的触发词·步数·CFG) | `action`* `negative?` `default_style?` `base_url?` `style_key?` `trigger?` `steps?` `cfg?` | | `rp_table` | 随机表定义与掷表 | `action`* `name?` `dice?` `entries?` `count?` `seed?` | \* = 必填。 --- ## PNG 故事书导入 界面入口在**工作区那一行**(输入框上方,只在空白会话或 DM 会话出现)。点开 → 在本地卡库里搜卡 → 预览(世界书条数 / 开场白来源 / 世界与性格摘要)→ 「导入并开始」。 导入做的事: 1. 卡里的 `character_book` → **追加合并**进会话工作区的 `rp-worldbook.md` (按标题去重,**绝不覆盖你自己写的条目**;无 `keys` 的条目**不补 constant** —— 运行时用标题当触发词, 而导入来的条目一律标 `source: card`,**不占系统提示**,命中了才进上下文); 条目名优先用卡给的,卡没给名字时依次用**正文里的第一个标题 → 触发词 → 正文首行** (所以不会出现「条目 4」「条目 5」这种认不出是什么的名字); 2. 卡全文 → `<工作区>/rp-sessions/<会话 id>/cards/.md`(世界书有 60 条 / 6 万字预算,超出的设定在这里按需 `read`); 3. 卡面 → `<工作区>/rp-sessions/<会话 id>/cards/.png`,登记成会话封面(**不是**某个角色的立绘); 4. 角色卡 / 世界设定 / 战役名 → 会话配置(`creator_notes` 里的广告、社群号、CC 协议**逐行剔掉**, 只有真正的说明才进【世界设定】;整段都是广告就不注入,全文仍留在卡文件里); 5. 开场引导文件 → `<工作区>/rp-sessions/<会话 id>/cards/.launch.md` (选定开场 + 文件清单 + 已写入什么 + 待人工确认),并切成 `dm` 预设、只把**这个路径**发给 DM (首条消息约 60 字,绝不内联开场白)。 > **导入之后要收尾的事全在 `launch.md` 里**(DM 读那份文件就有全部开局任务):角色字段归位、 > 世界书过滤(状态/历史类改成触发式、删空壳、补触发词、`constant` 只留 1–3 条)、 > 以及**按当前语言收拾标题与属性**(`name:` → `名称:`、`gender: Female` → `性别:女`; > `条目 4` → 「战斗」这类实义名)。整本一起收拾用 `rp_lore(action:"localize")` / > `rp_lore(action:"rename_unnamed")`,一次调用做完。 > 所以面板上**没有**「属性中文化」「重命名条目」这类按钮 —— 同一件事两个入口会让人以为没做。 ### 注入分层(省 token 的关键) | 通道 | 放什么 | |---|---| | **system standing**(每轮都在、字节稳定) | 通用 DM 规则、`session.dm.prompt`、战役名、世界设定、**紧凑人物索引**、路径指针、生图策略、**手写且在预算内的常驻世界书** | | **runtime context**(每轮重新装配) | 当前状态、在场人物的详细卡、命中触发词的世界书条目、**导入来的条目**(含原卡标了 `constant` 的) | | **文件(按需 read)** | 卡全文、全部备用开场白、被预算挡下的条目、导入映射 | 两条硬规则:**导入来的世界书正文不因 `constant` 自动进系统提示**(它按触发词走 runtime); **system standing 里的常驻世界书有 6000 字预算**,超出的降级为「按需」,而不是截断成残句。 同一个人物若既在世界书里、又作为人物卡在本轮展开,装配时会**跳过世界书那份**(诊断里记为 `character-duplicate`),避免同一批正文注入两遍。 卡库位置在**设置页 → RP工具 → 卡库目录**(默认是会话工作区下的 `rp-cards`)。 目录结构是 `cards/<分类>/*.png`;没有私有索引文件时退回按文件名扫目录,功能一样可用。 ### 出图:默认尺寸与立绘复用 出图默认尺寸(设置页「图像」可改):场景 **768×432**、立绘 **512×768**、道具 **512×512**。 出图时间基本正比于像素,而聊天里也渲染不到 1024 宽,所以 1.12.8 起调小了(单张大约 8~14 秒); 老配置里**没动过**的那一档会跟着换成新值,**自己改过的保持原样**。 **立绘复用**:常驻段的【本会话设定】里有一行「已有可用图」—— 角色已有的生成立绘、 **玩家自己导入的图**、以及**导入卡的卡面**(对角色卡来说那张 PNG 就是它的立绘)都在那里。 DM 第一次出场时直接展示它,不必再花十几秒重出一张;确实没有图时才调 `rp_illustrate`。 DM 也可以在叙事里直接把这些图摆进回复(零成本),不必为了「让角色露个脸」重新生图。 **立绘是纵向的**:`rp_illustrate` 不传 `width`/`height`/`aspect` 时,画面里**只提到一个已登记角色** 就按 `portrait`(纵向,默认 512×768)出,否则按场景横幅 —— 所以「给某人出一张立绘」不必自己算比例。 DM 出的第一张单人图会**自动记成那个角色的立绘**(已有立绘时**不覆盖**)。 角色卡编辑器里可以:**生成立绘 / 重新生成**(覆盖,纵向)、**导入图片**(png / jpeg / webp,≤8MB, 存进 `<工作区>/rp-sessions/<会话 id>/portraits/`)、或直接删掉这个角色(会顺手清掉它的立绘记录)。 ### 资源库:出过的图都留下来,能找回来 **只记引用会在几周后变成一堆死链**,所以出图后插件会把图**真抓一份**存进会话目录,并按分类分文件夹: ``` <工作区>/rp-sessions/<会话 id>/ ├── assets.json 索引:一张图一条(id / 分类 / 标签 / 角色 / 尺寸 / 风格 / 提示词 / 时间) └── assets/ ├── portraits/.png 角色 ├── scenes/.png 场景 ├── items/.png 道具 └── other/.png 其他 ``` 入库的四个入口:`rp_illustrate` 出图、`rp_scenes` 每格(整幕共享一个 `group`)、 `rp_character(portrait:true)`、面板「导入图片」。导入卡的卡面**不入库** —— 它属于卡库。 **按内容去重**:入库前比对 `sha256`,同一个分类里字节完全相同的图**复用已有那条**(合并标签、不写第二份文件)。 本地出图是「同 seed + 同提示词 → 同一张图」,没有这一步,重出一张一样的就会在库里留下两条只有 id 不同的记录 —— 图墙看着两张、磁盘占两份、人还分不出区别。查重放在写锁内(否则并发归档同一张会各写一份)。 `rp_scenes` 的场景文件**字段名是固定的**:幕的 id 是 `scene_id`(不是 `id`),分镜是 `panel_id`; `sceneId` 参数筛的就是它(也兼容 `id`/`title` 这类常见写法)。**筛不到会直接报错**并列出文件里实际有哪些 id —— 不会静默返回 0 张(那看起来像生图服务坏了)。字段名与最小合法示例写在工具的 `description` 里。 **DM 侧**用 `rp_assets` 按 `kind` / `characters` / `tags` / `q` 查,返回的每行都带能直接放进 `dsh-ui` image 组件的地址;常驻段里**只报条数**(`资源库:本会话已有 23 张图(角色 6、场景 14、道具 3)`) —— 常驻内容是每轮都发的,把上百条列进来会白白吃掉几千字。**DM 不能删图**,删除是玩家在面板里做的事。 **玩家侧**面板有一张「资源」卡片:分类筛选 + 搜索 + 图墙(服务端降采样缩略图)+ 点开看原图, 能改名称/标签、**显示到对话**(拼成 `dsh-ui` 围栏填进输入框,不自动发送)、 **设为某角色的立绘**(不复制文件,只改引用;会清掉该角色旧的生成立绘,否则读取端会优先显示旧的)、 **删除**(连磁盘文件一起删,并自动解除指向它的立绘引用,不留死链)。 > ⚠️ 索引是 read-modify-write,而 `rp_illustrate` 是**并发安全**的(宿主并行池最多 10 个在飞)—— > 所以所有写索引的路径都过一把**按会话串行的写队列**。没有它,同时出 5 张图可能只入库 2 张, > 而且不报错。 需要一次出多张时,DM 会在**同一步**里并发发出多个 `rp_illustrate`(插件已把这两个工具声明为 并发安全,宿主才会真的并行调度)。ComfyUI 是单卡队列,**GPU 总时长不变** —— 省掉的是每张图 之间那几轮模型往返(长局里一步就是几万 input token)。 > ⚠️ 实测结论(3269 张卡,见 `docs/PNG-CARD-DECODE.md`):**`first_mes` 100% 被广告污染** > (`deepseektavern.com`),所以导入一律改用 `alternate_greetings` 的第一条; > 40% 的卡正文只在 `character_book` 里,所以导入的主战场是**世界书**而不是角色字段。 --- ## 备份 / 会话包 长一点的团需要最低限度的保障。**会话配置在全局数据目录、世界书与资源图在工作区** —— 两处分离,手工备份必漏一半,所以插件把它们打成一个 zip: ``` MANIFEST.json 格式与版本 / 导出时间 / 原会话 id / 每个文件的 sha256 session.json 会话配置 rp-worldbook.md 世界书(可能没有) assets.json 资源库索引 assets/<分类>/. 出过的图与导入的图(1.13.0 起真存了一份,所以包是自包含的) cards/.{md,json,launch.md,png} 导入卡产物(含卡面与开局引导) ``` - **导出**:面板「备份 / 会话包 → 导出会话包」是一个 ``,浏览器自己存盘。 - **快照**:同一个卡片里的「拍快照」把包写进 `<工作区>/rp-sessions//snapshots/`, **只保留最近 5 个**;只管自己写的 `snapshot-*.zip`,你放进这个目录的别的包不会被当成快照、也不会被删。 - **导入**:选一个 zip。**默认不覆盖** —— 目标会话已有内容时宿主回 409,界面问一句, 确认后才带 `overwrite:true` 重来,而且**覆盖前会自动拍一个快照**兜底。 - 包是 **STORE(不压缩)** 的 zip:里面装的是已经压过的 PNG,再压一遍没意义。 用别的工具重新打包时会默认压缩 → 导入会明确报「只支持 STORE 包」,不会给你一堆乱码。 > ⚠️ 解包是**外部输入**:条目名可能带 `../`(zip-slip)。所以每个条目名都要过 > `safeEntryName()`(逐段判定 `..`/绝对路径/盘符),落盘时**再做一次**目标路径前缀校验 —— > 两道锁都留着,单点失效不至于写穿会话目录。清单里的 sha256 也会逐个核对。 --- ## 轻量地图(可选) **只在模组本身有地点结构时才用**(地牢、宅邸、城镇)。没有地图就正常叙事 —— 不给每个场景造图。 ``` <工作区>/rp-sessions/<会话 id>/ ├── rp-map.json 静态结构:nodes(id/label/public)+ edges(id/a/b/label/state) └── rp-map-state.json 运行时:node 当前节点 / revealed 已揭示 / edges 变化的边 / tokens 标记位置 ``` 首版**不加 `rp_map` 工具**:两个文件都由 DM 用通用的 read/write 读写。插件只做两件事: 1. **每轮注入一行摘要**(跟在「本场当前状态」之后): `【地图】旧钟旅店·大堂|已揭示 4/5|可走:厨房(木门)、二楼客房(楼梯)|队伍@大堂、老板@大堂` —— 实测 64 字/轮。DM 不必为了看一眼「我在哪、能去哪」去读整个文件。 2. **结构出问题时把话说明白**(而不是静默或渲染垃圾): `【地图】⚠ node="nope" 不是 rp-map.json 里的节点`。两条纪律都是踩出来的: **文件坏了 ≠ 还没建**(状态文件存在但 JSON 坏了时,不能说「还没初始化」,否则 DM 会覆盖一份 本可救回的文件);**坏数据不渲染摘要**(拿 `node="nope"` 画出一行看起来正常的地图, 比什么都不显示更危险)。 **为什么状态不放进 `rp_state.flags`**:`applyStateUpdates` 写旗标时是 `String(value)` → 对象直接变成 `"[object Object]"`(写进去了、不报错、永久丢失),数组被压成逗号串; 再叠上 `FLAG_VALUE_CHARS = 120` 的截断与 `FLAGS_MAX_SHOWN = 16` 的显示上限 (20 个地图键会把剧情旗标挤到对 DM 不可见)。实测: ``` 写 flags:map_edge_states=[object Object];map_tokens=[object Object] ← 第一回合就死 ``` 两个地图文件都**计入会话包**(导出/快照会带上)。DM 侧的约定(何时建图、怎么画、 按钮只从当前节点的邻接边生成、action 命名 `<地图id>:move:<节点id>`)写在 dm 预设的 「## 地图」小节里。 --- ## 数据与配置 ``` ~/.dsh/data/dsh-rp-tools/ ├── styles.json 全局:风格库 + 全局负面词 + ComfyUI 地址 + 全局默认风格 + 卡库目录 ├── sessions/.json 会话级:世界设定 / 角色卡 / 随机表 / 前缀 / 会话默认风格 / 状态 / 立绘登记 ├── dm-sessions.json DM 会话登记表(界面据此决定是否显示 RP 入口) └── _agent-probe.json 诊断用:agent/created 事件里可读到的字段快照 <会话工作区>/ ├── rp-worldbook.md 世界书(可手写;导入的故事书条目也追加在这里) └── rp-sessions/<会话 id>/ ├── cards/.{md,json,png} 导入产物:卡全文 / 规范化结果 / 卡面 ├── assets.json 资源库索引(一张图一条) ├── assets/<分类>/. 出过的图与导入的图(portraits / scenes / items / other) └── snapshots/snapshot-<时间>.zip 恢复点(只保留最近 5 个;只删自己写的那些) ``` `styles.json` 关键字段: ```jsonc { "comfyui": { "baseUrl": "http://127.0.0.1:8188", "dshOrigin": "http://127.0.0.1:3080" }, "defaultStyle": "manga", "negative": "low quality, worst quality, blurry, ... , lowres", // 全局负面词(预置一套) "styles": { "manga": { "label": "黑白漫画", "workflow": "krea2", "lora": null, "cfg": 1, "steps": 8, "trigger": "black and white manga panel, screentone shading, crisp ink lineart, ...", "sizes": { "scene": [1344,768], "portrait": [768,1024], "item": [1024,1024] } } } } ``` > ⚠️ **负面词与 CFG**:Krea-2 Turbo 建议 CFG=1,**此时负向条件在数学上不参与计算**(官方模板也如此)。 > 想让全局负面词真正生效,把对应风格的 `cfg` 调到 `1.5~2.5`(过高会让 turbo 模型过曝/崩坏)。 --- ## DM 预设接线(关键,否则工具不出现) RP 工具**只在 `dm` 预设作用域注册**。需要在预设目录做两件事: **1. 放行工具**(`~/.dsh/.agent-presets/dm/agent.cordis.yml`)—— 该预设默认 deny 掉所有全局工具,只保留白名单: ```yaml - id: dm-filter name: ./session-filter.mjs config: keepGlobalTools: - render_ui - validate_dsh_ui - web_search ``` > 全部 `rp_*` 工具现在都在本预设作用域注册(由 `rp-bridge.mjs` 调用 `registerRpTools`), > 所以白名单里**不再需要放行任何 `rp_*`** —— 它只用于保留少数几个全局工具。 **2. 挂桥接插件**(把 RP 工具注册进本会话 + 登记 DM 会话): ```yaml - id: rp-bridge name: ./rp-bridge.mjs ``` `rp-bridge.mjs` 的副本见本仓库 [`preset/rp-bridge.mjs`](preset/rp-bridge.mjs)。 --- ## 架构 ``` dsh-rp-tools/ ├── lib/index.js 宿主半侧(ESM) │ ├── apply(ctx) 全局:**不注册任何模型工具**,只挂 HTTP 路由 + 监听 session/created(fork 继承、记录工作区) │ ├── registerRpTools(ctx) dm 作用域(由 rp-bridge 调用):全部 10 个 rp_ 工具 + 两条提示词注入通道 │ ├── 生图链路 组装 API 工作流 → ComfyUI POST /prompt → 轮询 /history → 同源媒体 URL │ └── 配置层 styles.json(全局) / sessions/.json(会话) ├── lib/card-png.js PNG 角色卡解码(tEXt / iTXt / zTXt,ccv3 优先,截断容错) ├── lib/card-import.js 卡 → 会话配置的映射(丢广告开场白、无 keys 条目补 constant、限量 + 全文导出) ├── client/client.js 客户端半侧(plain JS + React.createElement,无构建) │ ├── settings.section「RP工具」 全局配置 + 风格库 + 卡库目录 + 工具清单 │ ├── conversation.session.header.utilities 仅 DM 会话渲染的「🎲 RP」按钮(打开右侧栏面板) │ │ 右侧栏标签**类型**随该按钮挂载/卸载注册与注销(引用计数), │ │ 因此非 DM 会话连空面板的引导页里也看不到这个入口 │ └── conversation.input.dock「📖 导入 PNG 故事书」 空白会话 / DM 会话里的故事书导入入口 ├── preset/rp-bridge.mjs dm 预设作用域桥接插件(副本,供安装参考) ├── cordis.patch.yml bundle 补丁层 └── docs/ 交接文档(HANDOFF)/ 状态(STATUS)/ 卡格式实测(PNG-CARD-DECODE) ``` **HTTP 路由**(POST 全部同源保护,`Origin` 必须等于 `Host`): | 路由 | 方法 | 用途 | |---|---|---| | `/rp-tools/state` | GET | 全局配置 + 风格摘要(同时学习浏览器 origin,用于拼媒体 URL) | | `/rp-tools/config` | POST | 写全局配置(含 `negative` / `baseUrl` / `cards.root` / 风格字段与增删) | | `/rp-tools/reset` | POST | 恢复默认全局配置 | | `/rp-tools/check` | GET | ComfyUI 连通性(版本 / GPU / 显存) | | `/rp-tools/inject` | GET | 注入自检:某个会话**会**被注入什么(只读,不参与运行) | | `/rp-tools/loras` | GET | 本地 LoRA 清单(读 ComfyUI `/object_info`) | | `/rp-tools/session` | GET/POST | 读写某个会话的 RP 配置(角色卡 / 世界 / 随机表 / 状态 / 立绘) | | `/rp-tools/dm-mark` | POST | 登记某会话为 DM 会话 | | `/rp-tools/tools` | GET | 工具清单 + 参数说明(设置页用) | | `/rp-tools/roll` | POST | 掷随机表(面板用) | | `/rp-tools/media` | GET | **同源媒体代理**:把 ComfyUI `/view` 转成同源,图片才能在聊天里渲染 | | `/rp-tools/portrait` | POST | 登记/清除某个角色的立绘(只存 ComfyUI 三要素,媒体仍走 `/rp-tools/media`;保存时也会归档进资源库) | | `/rp-tools/portrait-upload` | POST | 导入外部立绘(`/rp-tools/asset-upload` 的别名,等价于 `kind=portrait` + 角色名) | | `/rp-tools/portrait-image` | GET | 把**登记过**的导入立绘发回浏览器(只认会话配置里的相对路径 + 会话目录前缀校验) | | `/rp-tools/assets` | GET/POST | 资源库:列出(可筛分类/角色/标签/关键词)/ 改名称与标签 / 删除 / 设为某角色的立绘 | | `/rp-tools/asset-upload` | POST | 导入外部图进资源库(data URL → `assets/<分类>/.`,只收 png/jpeg/webp、≤8MB) | | `/rp-tools/asset-image` | GET | 发资源图(按 `id`;`thumb=1&width=N` 走服务端降采样,图墙用它) | | `/rp-tools/export` | GET | 下载会话包(STORE-only zip:配置 + 世界书 + 资源库 + 导入卡产物) | | `/rp-tools/snapshots` | GET | 列恢复点(只列自己写的 `snapshot-*.zip`) | | `/rp-tools/snapshot` | POST | 拍一个恢复点并修剪到最近 5 个;删不掉的如实报在 `failed` 里 | | `/rp-tools/import` | POST | 导入会话包(`path`=会话目录内的包,或 `dataUrl`);**默认不覆盖**,覆盖前自动拍快照 | | `/rp-tools/preview` | POST | 试出一张(设置页 / 面板用,可带 sessionId;`sizeKey` 选场景/立绘/道具档) | | `/rp-tools/cards` | GET | 列卡库(服务端搜索 / 分类 / 分页) | | `/rp-tools/card` | GET | 解析单张卡 → 摘要与预览(不落盘) | | `/rp-tools/card-import` | POST | 导入到某个会话(写世界书 / 卡全文 / 卡面 + 更新会话配置,返回开场指令) | | `/rp-tools/card-image` | GET | 卡面图(只服务卡库内的 `.png`) | > ⚠️ `/rp-tools/card*` 三条会把磁盘内容交给浏览器,路径一律经 `safeCardPath()`(`resolve` 后前缀比对卡库根 + 只认 `.png`); > 逃逸 / 绝对路径 / 非 png 全部 400。 > `/rp-tools/portrait-image` 同理,而且是**双锁**:只发会话配置里登记过的那张, > 再把相对路径解析到会话目录下做前缀校验;文件名由宿主用 `portraitFileSlug()` 生成(用户给的名字不进路径)。 --- ## 开发 > **所有改动都在这个 git 仓库里做**(`D:\Code\dsh\rp-tools-plugin`,它本身就是 > `github.com/SiriusWJ/dsh-rp-tools` 的克隆)。**不要改 profile 里那份安装副本** > (`~/.dsh/profiles/web/node_modules/dsh-rp-tools`)—— 它是重装时会被覆盖的产物, > 改了既不进版本库,下次安装就没了。 ```bash node --check lib/index.js && node --check client/client.js # 语法检查 ``` 这个插件在 profile 里是**从 GitHub 装的**(`"dsh-rp-tools": "github:SiriusWJ/dsh-rp-tools"`), 所以「本地源码 → GitHub → profile」是一条链,**本地不再是权威副本**: ```powershell # 1) 改完 → 提交并推送(profile 装的就是 push 上去的那个 commit) git -C D:\Code\dsh\rp-tools-plugin add -A git -C D:\Code\dsh\rp-tools-plugin commit -m "feat(x): …" git -C D:\Code\dsh\rp-tools-plugin push origin main # 2) 重装,让 profile 跟上新 commit(不重装的话它还停在旧 commit) dsh plugin --profile web add github:SiriusWJ/dsh-rp-tools # 3) 改的是 dm 预设那一半,还要同步**活动预设目录**(它不属于这个包,只能手动拷) Copy-Item preset\agent.cordis.yml "$env:USERPROFILE\.dsh\.agent-presets\dm\agent.cordis.yml" -Force Copy-Item preset\session-filter-v2.mjs "$env:USERPROFILE\.dsh\.agent-presets\dm\session-filter-v2.mjs" -Force Copy-Item preset\rp-bridge.mjs "$env:USERPROFILE\.dsh\.agent-presets\dm\rp-bridge.mjs" -Force ``` 重启 `dsh web` 后生效(`lib/` 与 `preset/` 在启动时装载;`client/` 只需刷新页面)。 > 只改了文档(`docs/`、`README.md`)时第 2 步可以跳过 —— 安装副本里的文档不参与运行。 > 想跳过「push + 重装」这两步(改成改完即生效):把依赖换成 > `dsh plugin --profile web add link:D:/Code/dsh/rp-tools-plugin`。 > 代价是 profile 直接读源码目录,与「商店里声明的是 GitHub」不一致 —— 二选一。 > > ⚠️ 本机到 `codeload.github.com`(GitHub 打包下载域名)吞吐只有 ~25KB/s,且 Node 的 fetch > 比系统下载慢十倍量级 —— **仓库 tarball 必须保持小**(这也是 `temp_output/` 被移出仓库的原因: > 四张试出图占了 4.3MB,会让 `github:` 安装卡满超时)。 测试: ```bash node tools/smoke-dm.mjs # 宿主:作用域隔离 / 路由 / 世界书 / 状态 / 风格库 / 卡库导入 node tools/smoke-card.mjs # PNG 卡解码 + 映射 + 开场指令 / 引导文件(合成 PNG 字节) node tools/smoke-client.mjs # 客户端 bundle:样式注入时机 / 槽位注册 / 面板渲染 node tools/verify-roundtrip.mjs # 导入↔解析往返(需 profile 里那份) node tools/probe-cardlib.mjs # 真卡库探针(只读 + 临时目录,手动跑) ``` 当前开发状态、验证记录、已知问题与路线图见 **[docs/STATUS.md](docs/STATUS.md)**, 给新会话的交接文档见 **[docs/HANDOFF.md](docs/HANDOFF.md)**。 ## License MIT