Skip to content

Releases: yupaoa/dsh-persona-switcher

0.3.4 — 输入栏人设按钮与「当前无人设」修复

Choose a tag to compare

@yupaoa yupaoa released this 29 Oct 11:31

中文说明

  • 输入栏人设按钮:在权限与表情按钮之后、模型选择器之前新增「人设」入口,可直接搜索并切换当前会话角色;未绑定时显示「人设」,绑定后显示角色名。支持名称 / id / 描述搜索、当前角色勾选、键盘操作、失败重试与空库提示。
  • 设置页开关:设置 → 人设切换 → 常规新增「输入框」卡片,可关闭输入栏的人设选择器。这是本机偏好,刷新与跨窗口保持,不影响 /role 命令或已绑定人设。
  • /role none 修复:切到 none 后立绘现在会立即消失;重启恢复、切换会话或继承默认角色时也不会残留旧立绘。
  • 明确的「当前无人设」段落:/role none 不再只清空人设段,而是明确告诉模型——部署人设才是当前身份,角色目录只是菜单,不能从目录里挑一个角色来扮演。修复切到 none 后模型有时自称阿米娅、有时自称鲸鱼娘的问题。
  • 文档与版本:README 整理,补充 0.3.4 变更说明与输入栏开关文档。

English

  • Composer persona picker: a new Persona button after permissions / reactions and before the model selector. It searches and switches the current session's role, showing the role name when bound and Persona otherwise, with search by name / id / description, current-role tick, keyboard support, retry and empty-library states.
  • Settings toggle: Settings → Persona Switcher → General now has a Composer card to hide the picker. This is a per-browser preference that survives refreshes and syncs across windows; /role and an already-bound persona are unaffected.
  • /role none fixes: the mascot now disappears immediately, including after restart, session switches, or when a default role was inherited.
  • Explicit no-role section: /role none no longer leaves the persona section empty. It states that the deployment persona is in force and that the role catalogue is a menu, not a list of candidate identities. This fixes the model occasionally answering as Amiya or whale-girl after switching to none.
  • Docs and version: README cleanup plus 0.3.4 notes and composer-toggle documentation.

升级说明 / Upgrade

安装或升级后请完全退出并重启 DSH,仅刷新页面不够。
Fully quit and restart DSH after installing or upgrading; a page refresh is not enough.

0.3.1 — 斜杠菜单点选角色

Choose a tag to compare

@yupaoa yupaoa released this 21 Oct 13:40

新增

  • /role 可以从菜单点选了。 输入框打出 / 后的命令列表里点 role,弹出角色选择器:顶部搜索框(角色名 / id / 描述片段都能搜),当前生效的角色带勾并标「当前」,第一行固定是 none(回到部署人设),点一下就执行 /role <id>;列表每次打开都重读角色库,设置页里增删角色即时生效。
  • 装饰宿主命令,而不是替换它。 /role list、/role <id>、/role default <id>、/role none 全部照旧落到宿主 handler;只有裸 /role 改由弹窗回答。点选本身就是一次真实命令执行,所以角色变更提示、sessions.json 记忆写入和转录里的命令节点都保留,浏览器侧没有复制切换逻辑。
  • GET /session 新增 liveRoleId。 roleId 仍是记忆里的选择,liveRoleId 是此刻在生效的角色;列表的勾读后者,所以默认角色 / /role 切换 / /role none 三种状态都显示正确。
  • 弹窗只在真正执行后才关闭。 宿主「收了命令但没匹配上」(value.matched === false)时不再静默关闭,而是保留列表与搜索词、显示原因,回车即可重试;判断用 === false,旧宿主返回结构没有该字段时不会被误判成失败。

升级说明

装到桌面端后需完整重启 DSH(宿主半边新增了 /session 的 liveRoleId 字段),再刷新页面。

v0.3.0 — 角色切换翻纸动画与动画三档开关

Choose a tag to compare

@yupaoa yupaoa released this 18 Oct 13:43

Added

  • Replacing a role's mascot now turns a card instead of hard-cutting. A /role switch (or an artwork
    replacement from the settings page) used to swap the <img> src the instant the client noticed it — one
    frame of the old art, then the new one, with nothing in between. The mascot now keeps the OLD artwork on
    screen as the front face of a card and turns the NEW one into view as the back face: the card rotates 180°
    about its own vertical mid-line over ~1s, eased in-out so it moves fastest through the edge-on 90°, where
    the axis reads as the picture's own centre. The card stays flat throughout — no lift, no shadow. Detection
    is untouched: the /revision counter still notices a switch in about a quarter of a second and the turn
    starts then — this is a presentation layer over it, not a change to it.
  • The turn is self-healing and never gets stuck. The new artwork is decoded BEFORE the turn starts (so the
    back face is never an empty box), the turn commits on the card's own animationend, and a fallback timer
    commits it FLIP_TOTAL_MS + 200ms after the newest back face is set — a hidden tab pausing CSS animations
    can no longer leave the mascot mid-turn showing two half-faded faces. A URL that will not decode skips the
    turn and settles directly, letting the settled image's own onError drop a truly broken role. Switches that
    arrive mid-turn retarget the back face (latest wins); a superseded preload never commits.
  • Only a REPLACEMENT turns. A first appearance (nothing → art) and an unbind (art → nothing) keep the plain
    opacity fade the wrapper already had — there is no card to turn when nothing is on screen. Under
    prefers-reduced-motion the swap happens in place, same as the hover growth: the feedback survives, the 1s
    turn does not (Chromium infers this from Windows' own animation setting, so "reduce" is not rare — see the
    0.2.1 note on the hover lesson). A new settings control overrides that reading per-plugin, described next.
  • A per-plugin animation switch: 跟随系统 / 始终开 / 始终关 (System / Always / Never). The reduced-motion
    reading above is a system-wide accessibility preference, and a machine whose Windows animation switch is off
    (Chromium then reports prefers-reduced-motion: reduce) would lose the turn with no way to keep it short of
    flipping a global setting. The mascot card on the settings page now carries a three-way choice stored under
    dsh-persona-switcher:mascot-motion: always turns even under system reduce, never swaps in place even
    where the system allows animation, and the default system follows the OS exactly as before. A corrupt or
    hand-edited value folds back onto system — it can never force a choice in either direction.
  • None of it costs the coexistence rules. No CSS filter anywhere (nor any shadow — the turn is a plain
    rotation), the turn
    animates a new inner stage/card/face layer so the wrapper's transform: scale() stays the size multiplier and
    the drag maths never sees it, the faces are pointer-events: none so they never eat the drag, and hovering
    keeps growing the settled artwork exactly as before — but never a face mid-turn, where a hover scale would
    fight the rotation.

Changed

  • The mascot client checks ask a different question about a switch. A replacement is on screen (as the back
    face) before the turn commits, so the switch-detection polls now ask "is this art painted by any face"
    (paintsSrc) rather than "is it the first <img>" — the old question conflated detection latency with the
    animation. The flip itself is verified by a new section (the pure planFlip state machine, a first show
    rendering no card, a replacement rendering front=old/back=new with the box already sized to the incoming art,
    the animationend commit, the fallback-timer commit, and reduced motion swapping in place) plus stylesheet
    assertions pinning the axis, the duration, and the absence of filter.

v0.2.1

Choose a tag to compare

@yupaoa yupaoa released this 17 Oct 02:56

新增:屏幕吉祥物变得「即时」且「有反馈」

本版把屏幕吉祥物从"能用"推到"顺手":

  • 🐳 /role 切换后吉祥物立即跟随。 以前切角色后,立绘要退出会话再进来才换——因为客户端只在
    屏幕上那个会话变化时读一次绑定角色。现在宿主新开了一个计数器端点 GET /persona-switcher/revision
    (只返回一个整数,不读磁盘),客户端每 250 ms 问一次,只有数字变了才重新读角色。
    于是切换大约四分之一秒就落到屏幕上。
    旧版宿主没有这个端点时会回 404,客户端把它当正常结果,自动降级为每 1.5 s 重读一次——不需要任何配置。
  • 🖱️ 鼠标悬停时吉祥物微微放大。 立绘 transform: scale(1.06)、120 ms 缓动,给指针一个"我响应你"的
    小确认;放大只作用在图片上,不作用在拖拽框上,所以框的大小、位置计算和元素本身都不受影响。
  • 📊 新增计数器端点:GET /persona-switcher/revision → {"ok":true,"revision":<int>}。

修复

  • 收工审计:1 个 Critical(配置成空前缀会劫持整个 Web GUI)与一批 Important
    (Windows 上的原子写、路径护送、轮询泄漏、崩溃隔离)已修。详见下方明细。
  • 另外修了一个真实机器上暴露的问题:开启"减弱动画"的用户悬停放大完全不生效——reduce 分支把
    transform 也一起取消了。现在只去掉过渡动画,不再取消放大本身。
完整变更明细(英文)

[0.2.1] - 2026-10-16

Added

  • The mascot now follows /role immediately. Switching a role used to change the artwork only after you
    left the session and came back, because the client read the bound role once, when the on-screen session
    changed. It now asks a new counter endpoint, GET /persona-switcher/revision — one integer, no disk read —
    every 250 ms, and re-reads the role only when the number moves. A switch therefore reaches the screen in
    about a quarter of a second. The endpoint is described under Fixed below.
  • The mascot reacts to the pointer. Hovering the artwork scales it to 1.06 over a 120 ms ease — a small
    confirmation that it is interactive, on the image only, never on the dragged box.

Fixed

  • A configuration value can no longer take the Web GUI down. routePrefix was normalized but never validated,
    so "" or "/" registered a PREFIX route with an empty path. The Host matches a prefix route with
    pathname.startsWith(prefix + '/'), which an empty prefix satisfies for every URL the app does not otherwise
    claim — this plugin then answered 404 for the frontend's own assets and shadowed the SPA fallback, and a second
    plugin registering the same path would have failed. An unusable prefix is now refused (one path segment, or
    nothing is mounted) with a warning saying so.

  • mascot.image URLs now follow the mounted prefix instead of a hardcoded one. The route was mounted at the
    configured prefix while every artwork URL was built as /persona-switcher/..., so any other prefix served every
    thumbnail as a 404. Both the host half and the client half are pinned to the same default by a test, so the two
    cannot drift apart silently again.

  • The session memory is written atomically on Windows. save() claimed "temp file plus rename", but rename
    does not replace an existing file on Windows (typically EPERM/EACCES, more often while a scanner or an editor
    holds it) and the temp name was a fixed ${file}.tmp shared by concurrent writers. Two sessions switching at
    nearly the same moment could therefore lose the write, surfacing as Role switch failed or "could not be
    remembered" with the persona already installed. The destination is removed first, the temp name is unique, the
    file is cleaned up on failure, and writes are serialized.

  • The artwork map is written atomically as well, and its writers are serialized. It was written straight to the
    map file: two concurrent imports truncated the same path, so one of them returned 200 for an entry that was
    gone — and a torn file degrades to "empty map", which would hide every role's mascot at once.

  • GET /role and DELETE /role wait for the first library scan. Both used the synchronous lookup while every
    other role route used the async one, so a request landing before the initial scan reported a role that IS on
    disk as unknown.

  • Destructive routes re-validate a role id before it is joined onto a path. The catalogue is built from files a
    person can place by hand and DELETE /role removes RECURSIVELY, so a role file declaring id: ../.. aimed that
    removal outside the role library. Such an id is now dropped while the library is scanned, and the delete and
    artwork-pruning paths refuse anything that is not a single path segment.

  • GET /mascot-file/<name> validates the RESOLVED path, not the string. ..%2F..%2Fsessions.json passed the
    string test; it was only harmless because no layer decoded the request and the extension map rejected .json.
    The guard now survives any future decoding.

  • A hand-edited box is normalized or reported, never silently dropped. "9999x9999" (out of range) used to
    read as "this role has no artwork" with no warning at all, which is indistinguishable from a missing file; a
    padded "0080x0120" is now normalized to 80x120 rather than discarded.

  • A blank DSH_HOME no longer scatters data next to the app. ?? only guards null/undefined, so DSH_HOME=''
    made every path relative to the working directory.

  • The request wrapper contains every failure. A route that throws now answers 500 JSON instead of relying on
    the Host's own catch (which answers a bodiless 400), and no request can be left hanging. The wrapper is driven
    directly with a throwing handler in the tests, because no route is reachable in a state that throws.

  • The mascot's role read backs off when the Host stops answering. A failed read was collapsed into "the answer
    is null", so the retry branch was unreachable: a Host that went away while the fast cadence was live was asked
    four times a second, indefinitely. Failures are now reported as failures, the cadence is read from a ref rather
    than a captured value (so an old Host really does slow down), and a hidden tab is slow for the session read too.

  • Each artwork import no longer adds a permanent poll loop. Every CHANGED_EVENT broadcast started a second
    /roles chain while the previous timer was still pending, and the orphaned timer was unreachable from cleanup —
    ten imports meant ten concurrent loops. A broadcast now cancels the queued read first, and only one chain exists.

  • A visibilitychange no longer doubles the revision poll. The timer id was left in place while its callback
    awaited a response, so returning to the tab armed a second chain that grew with every hide/show.

  • The master switch reacts to its own in-window broadcast. Its listeners guarded on event.key, which a
    CustomEvent does not have, so the plugin's own event was rejected as "a storage event for another key" and a
    second settings surface kept a stale toggle until the page was reloaded.

  • The settings screen cannot be taken down by a hand-edited role file. A role with no name threw
    editing.name.trim() during render, and a non-array /roles response would have thrown in roles.map. Both
    are guarded now, like the mascot half already was.

  • The mascot no longer disappears when its host effect is run twice. Symptom on a real machine: no mascot at
    all, no error anywhere, settings page perfectly fine. The dock element was created and then removed. Cause: the
    disposer returned by installMascot deleted the dock node, so a second install (a reload, a re-apply, any host
    that re-runs effects) created a NEW React root on the SAME node while the first root's deferred unmount was still
    queued — and that stale teardown then removed the shared node.
    The dock and its root are now owned by the MODULE: an install reuses them, and disposal unmounts the CONTENT
    without removing the node. A synchronous render failure still cleans both up.
    This slipped through because nothing in the suite ran the built bundle through install → dispose → install, and
    the first guard written for it checked "does getElementById know the id" — which a detaching removeChild
    leaves true, so the guard proved nothing until it was rewritten to test parentNode.
    scripts/simulate-client.mjs now applies lib/client.js the way the host does and asserts the dock survives
    that cycle; scripts/verify-mascot-client.mjs asserts the disposer empties the dock but keeps the node.

  • The artwork read keeps retrying after a failure, instead of stopping for good. The /roles poll set its
    next timer AFTER the await, so a single failed read (a restarting host, a transient error) ended the loop
    permanently — it could only come back if the on-screen session changed. The visible symptom would have been
    "I imported artwork for this role and the mascot never appeared". The loop now reschedules from its outcome
    (5 s after success, 8 s after a failure), ignores a second read while one is in flight, and stops on unmount.
    scripts/verify-mascot-client.mjs fails the artwork read once and asserts both the retry and that the backoff
    is not a hot loop.

  • The mascot now follows a /role switch immediately, without re-entering the conversation. /role <id>
    rebinds a session inside the Host process and broadcasts nothing to the browser, so the client can only ask.
    It now asks a new counter endpoint, GET /persona-switcher/revision — one integer, no disk read — every
    250 ms, and re-reads the role only when the number MOVES. The counter is bumped in exactly the two places the
    mascot draws from: a session's role binding (the /role path, /role none included) and the artwork map
    (import, clear, or a hand-edited mascot-map.json). A switch therefore reaches the screen in about a quarter
    of a second, and the previous behaviour — read the role once, when the seat changed — is gone. Only a CHANGE
    republishes, so an identical answer every cycle neither re-renders the mascot nor restarts the artwork's fade.
    Nothing is scheduled while the tab is hidden.

  • A host without the counter endpoint still works. An older build answers 404, which the client treats as a
    normal outcome rather than an error: it falls back to re-reading the role every 1.5 s (backing off to 8 s when
    a read fails) and still catches a live switch within a few seconds. No configuration, no version negotiation.

  • **The mascot grows slightly ...

Read more

v0.1.9 — 角色不再遮蔽部署身份行:改用自有 prompt section

Choose a tag to compare

@yupaoa yupaoa released this 13 Oct 13:27

安装

dsh plugin add dsh-persona-switcher

[0.1.9] - 2026-10-13

Fixed

  • A role no longer deletes the deployment's own identity line. A persona was installed as a per-agent
    deployment:persona-prefix prompt section — the same name and order as the deployment's own registration —
    which shadows it for that session. That section carries the deployment's personaPrefix
    (You are a coding agent powered by the {{model}} model.), so every switched session lost the line and the
    model could no longer say which model it was running on; only /role none, which disposes the override,
    brought it back. A role now registers this plugin's own section, persona-switcher:persona, at order 137 —
    after the deployment persona prefix (DEPLOYMENT_PERSONA_PREFIX, 0) and before the first-party guidance
    (PLAN_POLICY, 500) — so the deployment prefix stays in force for every session and a switch adds persona
    prose after it instead of replacing it. /role none now means an empty persona rather than an empty
    identity: nothing is registered for the session, and the deployment's line is still there. role_probe's
    report, the README, the diagram and the guards follow suit, and npm run verify now fails the build if the
    deployment persona prefix is registered from here again.

v0.1.8 — 角色变更声明覆盖全部方向:/role none 退回部署人设也会声明

Choose a tag to compare

@yupaoa yupaoa released this 09 Oct 09:09

安装

dsh plugin add dsh-persona-switcher

[0.1.8] - 2026-10-09

Fixed

  • /role none now announces the change, like every other persona transition. The notice was
    wired into the switch path alone, so it covered role-to-role switches and nothing else: the clear
    path dropped the binding and wrote the opt-out without ever queueing a notice, and the notice
    builder itself returned an empty string whenever the from side was unknown, which made the
    omission structural rather than accidental. The symptom was precisely what the notice exists to
    prevent — the prompt goes back to the deployment persona while the reply keeps arriving in the
    dismissed role's voice, because this conversation's own earlier assistant turns still demonstrate
    that voice far more concretely than a system prefix establishes the new one. The notice is now
    built from both sides of the transition (null standing for the deployment persona), and every
    deliberate change queues one: role → role, deployment persona → role, role → /role none, and a
    remembered role that vanished from the library handing the session to another one. The cases that
    must stay quiet still do: an automatic rebind that leaves the session's role unchanged (the same
    role restored after a restart, or a configured default applied to a session that never chose one)
    changes nothing the model can read, so announcing it would put a bogus "role change" message in
    front of every resumed session.

Added

  • scripts/verify-role-notice.mjs, a behavioural guard for the notice contract. Seven checks: the
    /role none notice and its one-shot nature, the deployment-persona → role direction, the
    role-to-role notice naming both roles, silence when the session already ran the deployment persona
    (no-op opt-out), silence under announceRoleChange: false, silence for a same-role restore, and
    the vanished-role hand-over. Run against the 0.1.7 code it fails exactly three of them, which is what makes
    it a regression guard rather than a description. Wired into npm test and prepublishOnly.

v0.1.7 — 切换后注入角色变更声明 + README 补权能边界

Choose a tag to compare

@yupaoa yupaoa released this 07 Oct 15:34

安装

dsh plugin add dsh-persona-switcher

Changed

  • announceRoleChange now defaults to true. The role-change notice used to
    be off by default, on the assumption that replacing the head system node is
    enough: the superseded persona text does leave the prompt, so the model cannot
    read it again. A measured switch shows that assumption is only half right. On a
    session switched from whale-girl to amiya, the next step's system prompt
    already carried the new persona — zero occurrences of the old persona prose;
    the single hit for the old role's name was the role catalogue listing — and the
    reply still came back in the old role's voice. The transcript's own earlier
    assistant turns demonstrate that voice far more concretely than a system prefix
    establishes the new one, so the model follows the evidence closest to the
    conversation. The notice is the in-band statement that resolves the conflict in
    favour of the current role, which makes it the right default; set
    announceRoleChange: false only where the prompt swap alone is trusted and the
    extra message is unwanted weight. A profile that already pinned the old default
    explicitly is unaffected. Verified end-to-end afterwards in a headless profile
    that pins nothing: a real /role troll-helper switch produced exactly one
    notice in the transcript
    (source: {kind: "persona-switcher", form: "role-change", from: "whale-girl", to: "troll-helper"})
    and the next reply came back in the new role's voice.

Added

  • A "what it touches on disk" boundary section in the README (Chinese and
    English). It names the four write locations — the session state file, a role's
    ROLE.md, a role directory on delete, and the profile patch file that the host
    writes on the plugin's behalf — plus the read-only surfaces, the inbound
    loopback route and its four endpoints, the fact that the route carries no auth
    of its own, the extra capability role_probe holds (its command argument
    executes any registered slash command, not just /role), and how to tighten or
    fully undo all of it. The point is that the plugin's reach can be audited from
    the README before someone decides to trust it.

Fixed

  • Removed a duplicated sentence in the middle of the switching walkthrough.

Published to npm as dsh-persona-switcher@0.1.7 from this tag by
.github/workflows/release.yml, with provenance.

v0.1.6

Choose a tag to compare

@yupaoa yupaoa released this 07 Oct 11:23

安装

dsh plugin add dsh-persona-switcher

Fixed

  • The plugin inventory now shows this plugin's own title and description. The
    Settings plugin inventory, the Plugin Manager card and the bundle details read
    that text from locale/en.json and locale/zh.json without activating the
    plugin, resolving every file through the package's exports map
    (<name>/locale/en.json). ./locale/*.json was not an exported subpath, so
    that lookup failed and the row silently fell back to package.json name and
    description — it read dsh-persona-switcher with the bilingual npm
    description instead of 人设切换 / Persona Switcher. The subpath is exported
    now, so the row reads 人设切换 in Chinese and Persona Switcher in English.
    Nothing in the runtime code changed: this release is the manifest plus locale
    data.

Added

  • scripts/verify-bundle.mjs hard rule 5: package.json must export
    ./locale/*.json and ./package.json, both locale files must carry a
    non-empty meta.title and meta.description, and files must ship locale/.
    The fallback to package.json happens without any diagnostic, so this
    invariant is what turns the same regression into a red build instead of a row
    that quietly shows the package name.

Changed

  • The 0.1.3, 0.1.4 and 0.1.5 headings are dated 2026-10-07, the day those
    versions were released; they had been dated one day ahead.

v0.1.5 — 重启后第一步即带人设

Choose a tag to compare

@yupaoa yupaoa released this 07 Oct 06:57

人设(角色)现在在 agent 创建时就装好,抢在宿主组装提示词之前。DSH 完全重启后,该会话的第一步回复就已经是人设,不再迟一步。

Fixed

  • 修掉「重启后第一步仍是部署人设」的一步延迟:恢复动作从 agent/pre-step 提前到 agent/created。

Added

  • 加载时扫描已存活的 agent(热重载、或行加载前就已恢复的会话)并同样绑定。
  • scripts/verify-created-bind.mjs:8 项检查,覆盖创建期绑定的幂等性、子 agent 不受影响、/role none 的选择不被 defaultRole 抢回、未知角色被遗忘,以及「创建期监听器永不 reject,因此不会回滚会话创建事务」。
  • verify-bundle 硬规则扩展:缺少 agent/created 或活 agent 扫描即构建失败。

同时包含 0.1.4(此前未单独发布)

  • 角色选择落盘到 $DSH_HOME/persona-switcher/sessions.json,重启后按会话恢复。
  • /role none 记为 {"role": null}(显式选择部署人设),不会再被 defaultRole 抢回。
  • 新增 lib/session-state.js:原子写入、损坏文件隔离为 .corrupt、180 天过期清理。

安装

dsh plugin add --profile desktop dsh-persona-switcher@0.1.5

若本机 npm 源指向镜像站且尚未同步到 0.1.5,追加 --registry https://registry.npmjs.org/。

校验

  • 经 GitHub Actions 可信发布(OIDC trusted publishing),带 SLSA provenance 证明。
  • tarball shasum bdd36824166d90eeb607ec02227c90b0a9e823f2,17 files,unpackedSize 234778。
  • 实机验证:装好后完全重启 DSH,回同一个会话,第一步的提示词即含人设(7382 字符、三条人设探针全中),与重启前一致。

完整变更见 CHANGELOG.md。

v0.1.2

Choose a tag to compare

@yupaoa yupaoa released this 07 Oct 04:16

Packaging-only release; the plugin code is identical to 0.1.1.

Changed

  • @deepseek-ai/cordis is declared as ^4.0.4 instead of ~4.0.4, matching the range style used by published DSH plugins and keeping the plugin installable when the host moves to a later 4.x line.

Published to npm with provenance: https://www.npmjs.com/package/dsh-persona-switcher/v/0.1.2