@roaming-ai/dsh-group-chat 0.3.1 → 0.3.2

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
@@ -1,5 +1,7 @@
1
1
  # dsh-group-chat
2
2
 
3
+ ![模型群聊面板](assets/banner.png)
4
+
3
5
  DSH Web GUI 的「模型群聊」插件:多模型角色群组对话面板。
4
6
 
5
7
  兼容:`dsh >= 0.1.5-rc.1`(Web profile)
@@ -18,7 +20,7 @@ DSH Web GUI 的「模型群聊」插件:多模型角色群组对话面板。
18
20
  - **@点名**:裸 `@` 弹出成员候选(↑↓ 选择、Enter/Tab 插入);`@` 后继续键入则在群工作区内检索文件,插入 `@path` 芯片(只写入路径,不把文件正文再灌进 prompt)
19
21
  - **三区工作台**:左栏导航(搜索 + 目录树);中栏会话流(色环头像 + 模型徽章 + 相对时间,贴底自动跟随、可回到底部);右栏上下文(角色卡 + 工作区目录)。左右栏均可经接缝钮收起,窄面板时右栏覆盖式呈现
20
22
  - **群组工作区目录**:每个群组指定一个目录(群级、全会话共享;无独立笔记或文件清单)。角色发言时自动读取目录**根下一层**文本文件(扩展名白名单、跳过隐藏项、最多 20 个;单文件 16k / 总量 48k 截断)注入全体角色 system 提示词的「共享资料」块;支持 `~`、绝对路径与相对 DSH 工作区根解析,右栏内置目录选择浏览器;未设置则不注入、不暴露工具
21
- - **工具执行(docs/TOOLS.md)**:设置工作区后角色可调用 `read_file` / `list_dir` 主动查看工作区(realpath 硬边界,逃逸/软链越界一律拒绝);**权限档位**于消息输入框下方左下角芯片切换(对齐主会话 `/permission` 三档:仅可查看 / 工作区内修改 / 完全权限;默认仅可查看,按群组记忆持久化)——工作区内修改档角色可请求执行 shell 命令(cwd 固定为工作区、超时 120s),**每条命令在会话流内逐条确认**(允许 / 拒绝 / 停止,命令全文展示不截断);完全权限档免确认直接执行,切换时经主会话同款风险确认弹窗;工具调用以折叠行内嵌于角色消息(含耗时与状态),摘要注入后续发言上下文;不支持 tools 的模型自动降级为纯文本
23
+ - **工具执行(docs/TOOLS.md)**:设置工作区后角色可调用 `read_file` / `list_dir` 主动查看工作区(realpath 硬边界,逃逸/软链越界一律拒绝);**权限档位**于消息输入框下方左下角芯片切换(对齐主会话 `/permission` 三档:仅可查看 / 工作区内修改 / 完全权限;默认仅可查看,按群组记忆持久化)——工作区内修改档角色可请求执行 shell 命令(经 DSH `ctx.shell` 沙箱执行器:cwd 与沙箱策略均收紧到工作区、超时 120s),**每条命令在会话流内逐条确认**(允许 / 拒绝 / 停止,命令全文展示不截断);完全权限档免确认直接执行(对齐 DSH danger-full-access 语义),切换时经主会话同款风险确认弹窗;工具调用以折叠行内嵌于角色消息(含耗时与状态),摘要注入后续发言上下文;不支持 tools 的模型自动降级为纯文本
22
24
  - **设置启用**:设置页「模型群聊」分区提供启停开关(持久化于 settings.yaml);关闭时隐藏侧边栏入口并中止进行中的对话
23
25
 
24
26
  ## 架构
@@ -112,8 +114,8 @@ dsh plugin --profile web remove @roaming-ai/dsh-group-chat
112
114
  - **session-\<uuid\>.json**:名称/主题/`createdAt`/消息;可选 `namePinned` / `topicPinned`、`constraints` / `constraintsUpToSeq`(空数组与水位 0 省略)、消息上的 `toolCalls` / `error` / `failedRoleId`
113
115
  - 工作区文件内容不写入本目录;每次发言由 materials 实时读盘。全量保留,不裁剪
114
116
  - 事件驱动写:变更按文件标脏,同一 tick 内合并;流式 partial 不落盘。会话文件与 `roles.json` 紧凑序列化,ledger pretty
115
- - 原子写(tmp + fsync + rename,每个实际发生 rename 的目录一次 fsync)、单实例 `.lock`(PID 存活检测,崩溃残留自动清理)
116
- - 写失败保留脏标记,下次 flush 自动重试;插件卸载时先唤醒确认等待并 kill 子进程,再同步最终 flush、释放锁
117
+ - 原子写(`@deepseek-ai/dsh-atomic-write` 的 `writeFileAtomic`:wx 独占创建 + 随机后缀 tmp + rename,无 per-file fsync 对齐 DSH 基座标准;每个实际发生 rename 的目录一次 fsync)、单实例 `.lock`(PID 存活检测,崩溃残留自动清理)
118
+ - 写失败保留脏标记,下次 flush 自动重试;插件卸载时先唤醒确认等待并 kill 子进程,再等待挂起 flush 并最终落盘、释放锁(异步 disposer,cordis await)
117
119
  - 损坏处理:单会话文件坏仅丢该会话消息(隔离 `.corrupt-*`,每前缀保留最近 1 份);ledger 有 id 而文件缺失则空消息重建;`roles.json` 坏仅空该群角色;ledger 坏可扫描群组目录回收(仅丢群组名);行级校验丢弃孤儿条目;hydrate 清 `.tmp-*`
118
120
  - v1 双文件布局(ledger.json + messages.json)首次启动自动迁移(幂等;旧 messages.json 归档为 `.migrated-*`,从不删除)
119
121
  - 锁不可得(双进程)时降级为内存态运行并告警
package/lib/client.js CHANGED
@@ -197,8 +197,8 @@ window.__ModuleLoader__.load({
197
197
  ".dsgc-ph{position:absolute;left:14px;top:6px;max-width:calc(100% - 22px);color:var(--dsw-alias-label-tertiary,inherit);pointer-events:none;user-select:none;font-size:13px;line-height:24px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
198
198
  ".dsgc-chipin{display:inline-flex;align-items:center;gap:4px;background:color-mix(in srgb,var(--role-color,#888) 15%,transparent);border-radius:999px;padding:1px 7px 1px 5px;margin:0 1px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-primary,inherit);white-space:nowrap;user-select:all}",
199
199
  ".dsgc-card .dsgc-sendrow{padding:0 12px 10px}",
200
- ".dsgc-stopbtn{background:var(--dsw-alias-state-error-primary,#e5484d);border-color:transparent;color:#fff;font-weight:600}",
201
- ".dsgc-stopbtn:hover:not(:disabled){filter:brightness(1.08);color:#fff}",
200
+ "button.dsgc-stopbtn{background:var(--dsw-alias-state-error-primary,#e5484d);border-color:transparent;color:#fff;font-weight:600}",
201
+ "button.dsgc-stopbtn:hover:not(:disabled){background:var(--dsw-alias-state-error-primary,#e5484d);filter:brightness(1.08);color:#fff}",
202
202
  ".dsgc-parts{display:flex;align-items:flex-start;gap:6px;min-height:24px}",
203
203
  ".dsgc-partslabel{font-size:12px;line-height:24px;color:var(--dsw-alias-label-tertiary,inherit);flex:none}",
204
204
  ".dsgc-partlist{display:flex;flex-wrap:wrap;align-items:center;gap:6px;row-gap:4px;flex:1;min-width:0}",
@@ -2615,7 +2615,7 @@ window.__ModuleLoader__.load({
2615
2615
  !input.trim() ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2616
2616
  className: "dsgc-ph",
2617
2617
  "aria-hidden": "true",
2618
- children: "发消息给全群,@成员 点名让其回应(留空则让角色自由讨论)…"
2618
+ children: "发消息给全群,@成员 点名让其回应…"
2619
2619
  }) : null,
2620
2620
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2621
2621
  className: "dsgc-edit",
@@ -2698,7 +2698,7 @@ window.__ModuleLoader__.load({
2698
2698
  onClick: () => {
2699
2699
  sendMsg();
2700
2700
  },
2701
- disabled: !participants.length && !mentionedRoles.length || !sess,
2701
+ disabled: !input.trim() || !participants.length && !mentionedRoles.length || !sess,
2702
2702
  children: [Icon(_deepseek_ai_dsh_client_ui_primitives.IconSendOutline16, 16), "发送"]
2703
2703
  })
2704
2704
  ]
@@ -2801,7 +2801,7 @@ window.__ModuleLoader__.load({
2801
2801
  }),
2802
2802
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2803
2803
  className: "dsgc-hint",
2804
- children: enabledRoles.length ? "发送消息开始讨论;@成员 点名让其优先回应;留空直接发送可让角色自由讨论——每轮全体参与角色按顺序各发言一次,可用右下角轮数控制(1–10 轮)" : "先在右侧添加角色(每个角色可绑定不同模型),再回到这里发起讨论。"
2804
+ children: enabledRoles.length ? "发送消息开始讨论;@成员 点名让其优先回应——每轮全体参与角色按顺序各发言一次,可用右下角轮数控制(1–10 轮)" : "先在右侧添加角色(每个角色可绑定不同模型),再回到这里发起讨论。"
2805
2805
  })
2806
2806
  ]
2807
2807
  })
@@ -3528,6 +3528,7 @@ window.__ModuleLoader__.load({
3528
3528
  const { insertChip, insertFileChip } = useMentionChip(inputRef, setMention, setMentionIdx, syncFromDOM);
3529
3529
  const sendMsg = (0, react.useCallback)(async () => {
3530
3530
  if (busyNow || !sess || sendingRef.current) return;
3531
+ if (!input.trim()) return;
3531
3532
  sendingRef.current = true;
3532
3533
  try {
3533
3534
  if (!participants.length && !mentionedRoles.length) {