@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 +5 -3
- package/lib/client.js +6 -5
- package/lib/client.js.map +1 -1
- package/lib/index.js +110 -89
- package/lib/types/core/types.d.ts +3 -2
- package/lib/types/host/persistence/persistence.d.ts +2 -4
- package/lib/types/host/persistence/store.d.ts +5 -1
- package/lib/types/host/service.d.ts +2 -2
- package/lib/types/host/state.d.ts +1 -0
- package/lib/types/host/tools/tools.d.ts +2 -1
- package/lib/types/index.d.ts +3 -0
- package/package.json +4 -1
- package/src/client/GroupChatPanel.tsx +2 -0
- package/src/client/components/ChatPanel.tsx +1 -1
- package/src/client/components/Composer.tsx +2 -2
- package/src/client/lib/styles.ts +6 -2
- package/src/core/types.ts +3 -2
- package/src/host/engine/conversation.ts +1 -1
- package/src/host/persistence/persistence.ts +57 -34
- package/src/host/persistence/store.ts +5 -1
- package/src/host/service.ts +4 -4
- package/src/host/state.ts +5 -2
- package/src/host/tools/tools.ts +40 -46
- package/src/index.ts +7 -5
package/lib/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { randomUUID } from "node:crypto";
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { basename, dirname, isAbsolute, join, sep } from "node:path";
|
|
6
6
|
import { DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES, DEFAULT_FILE_SEARCH_MAX_ENTRIES, DEFAULT_FILE_SEARCH_MAX_RESULTS, WorkspaceFileSearch } from "@deepseek-ai/dsh-file-reference-local";
|
|
7
|
-
import {
|
|
7
|
+
import { writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
|
|
8
8
|
//#region src/mount-once.ts
|
|
9
9
|
/**
|
|
10
10
|
* Host single-instance guard shared by the plugin family. The family bundle
|
|
@@ -229,6 +229,7 @@ function createHostState(ctx) {
|
|
|
229
229
|
ctx,
|
|
230
230
|
llm: ctx.llm,
|
|
231
231
|
fs: ctx.fs,
|
|
232
|
+
shell: ctx.shell,
|
|
232
233
|
groups: /* @__PURE__ */ new Map(),
|
|
233
234
|
sessions: /* @__PURE__ */ new Map(),
|
|
234
235
|
roles: /* @__PURE__ */ new Map(),
|
|
@@ -243,7 +244,7 @@ function createHostState(ctx) {
|
|
|
243
244
|
queue: [],
|
|
244
245
|
pendingConfirm: null,
|
|
245
246
|
confirmSignal: null,
|
|
246
|
-
|
|
247
|
+
commandAbort: null,
|
|
247
248
|
finished: null,
|
|
248
249
|
replaceMessageId: null
|
|
249
250
|
},
|
|
@@ -1799,7 +1800,7 @@ function createConversation(core, deps) {
|
|
|
1799
1800
|
run.queue = [];
|
|
1800
1801
|
run.pendingConfirm = null;
|
|
1801
1802
|
run.confirmSignal = null;
|
|
1802
|
-
run.
|
|
1803
|
+
run.commandAbort = null;
|
|
1803
1804
|
run.stopping = false;
|
|
1804
1805
|
run.replaceMessageId = null;
|
|
1805
1806
|
touch();
|
|
@@ -2190,7 +2191,11 @@ var Store = class {
|
|
|
2190
2191
|
}
|
|
2191
2192
|
} catch {}
|
|
2192
2193
|
}
|
|
2193
|
-
/**
|
|
2194
|
+
/**
|
|
2195
|
+
* 同步原子写(tmp+fsync+rename):仅供构造期 v1 迁移(migrateV1)与测试
|
|
2196
|
+
* 直用;运行时 flush 走 persistence.ts 的 writeFileAtomic(异步、wx 独占
|
|
2197
|
+
* 创建 + 随机后缀 + 符号链接安全,无 per-file fsync,对齐 DSH 基座标准)。
|
|
2198
|
+
*/
|
|
2194
2199
|
atomicWrite(file, text) {
|
|
2195
2200
|
mkdirSync(dirname(file), { recursive: true });
|
|
2196
2201
|
const tmp = `${file}.tmp-${process.pid}`;
|
|
@@ -2433,6 +2438,11 @@ function emptyGroup(id, name) {
|
|
|
2433
2438
|
* 锁失败/写失败时降级为内存态运行(console 告警)。
|
|
2434
2439
|
* @module dsh-group-chat/host/persistence
|
|
2435
2440
|
*/
|
|
2441
|
+
/** writeFileAtomic 权限位:私有数据 0o600、目录 0o700(对齐 Store.atomicWrite)。 */
|
|
2442
|
+
const WRITE_FILE_OPTS = {
|
|
2443
|
+
mode: 384,
|
|
2444
|
+
dirMode: 448
|
|
2445
|
+
};
|
|
2436
2446
|
/**
|
|
2437
2447
|
* 创建持久化面:构造即完成 store 初始化 + hydrate(v1 迁移 → 残留清理 →
|
|
2438
2448
|
* 清单 → 群组数据 → 会话文件;缺文件空重建;补建默认会话/群组立即落盘)。
|
|
@@ -2445,11 +2455,15 @@ function createPersistence(core) {
|
|
|
2445
2455
|
core.store = null;
|
|
2446
2456
|
}
|
|
2447
2457
|
const store = () => core.store;
|
|
2448
|
-
const dirtySessions = /* @__PURE__ */ new
|
|
2449
|
-
const dirtyRoles = /* @__PURE__ */ new
|
|
2450
|
-
const dirtyWorkspace = /* @__PURE__ */ new
|
|
2451
|
-
let ledgerDirty =
|
|
2458
|
+
const dirtySessions = /* @__PURE__ */ new Map();
|
|
2459
|
+
const dirtyRoles = /* @__PURE__ */ new Map();
|
|
2460
|
+
const dirtyWorkspace = /* @__PURE__ */ new Map();
|
|
2461
|
+
let ledgerDirty = 0;
|
|
2452
2462
|
let flushScheduled = false;
|
|
2463
|
+
let flushChain = Promise.resolve();
|
|
2464
|
+
const mark = (map, key) => {
|
|
2465
|
+
map.set(key, (map.get(key) ?? 0) + 1);
|
|
2466
|
+
};
|
|
2453
2467
|
const ledgerDocument = () => ({
|
|
2454
2468
|
schema: 3,
|
|
2455
2469
|
savedAt: Date.now(),
|
|
@@ -2482,74 +2496,85 @@ function createPersistence(core) {
|
|
|
2482
2496
|
savedAt: Date.now(),
|
|
2483
2497
|
roles: g.roleIds.map((rid) => core.roles.get(rid)).filter((r) => Boolean(r)).map((r) => roleJson(r))
|
|
2484
2498
|
});
|
|
2485
|
-
/**
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2499
|
+
/**
|
|
2500
|
+
* 异步 flush(writeFileAtomic:wx 独占创建 + 随机后缀 tmp + rename,无
|
|
2501
|
+
* per-file fsync——崩溃持久性对齐 DSH 基座标准):写全部脏文件;写成功且
|
|
2502
|
+
* 写入期间未被重新标脏(版本未变)才清脏标记;每个实际发生 rename 的
|
|
2503
|
+
* 目录一次 fsync(调用侧批量执行)。
|
|
2504
|
+
*/
|
|
2505
|
+
const flushAll = async (s) => {
|
|
2489
2506
|
const fsyncDirs = /* @__PURE__ */ new Set();
|
|
2490
|
-
for (const sid of [...dirtySessions]) {
|
|
2507
|
+
for (const [sid, v] of [...dirtySessions]) {
|
|
2491
2508
|
const sess = core.sessions.get(sid);
|
|
2492
2509
|
if (!sess) {
|
|
2493
2510
|
dirtySessions.delete(sid);
|
|
2494
2511
|
continue;
|
|
2495
2512
|
}
|
|
2496
2513
|
try {
|
|
2497
|
-
|
|
2498
|
-
dirtySessions.delete(sid);
|
|
2514
|
+
await writeFileAtomic(s.sessionFile(sess.groupId, sess.id), JSON.stringify(sessionDocument(sess)), WRITE_FILE_OPTS);
|
|
2515
|
+
if (dirtySessions.get(sid) === v) dirtySessions.delete(sid);
|
|
2499
2516
|
fsyncDirs.add(s.sessionsDir(sess.groupId));
|
|
2500
2517
|
} catch (e) {
|
|
2501
2518
|
console.error(`[dsh-group-chat] 会话 ${sid} 落盘失败(保留脏标记待重试):`, e);
|
|
2502
2519
|
}
|
|
2503
2520
|
}
|
|
2504
|
-
for (const gid of [...dirtyRoles]) {
|
|
2521
|
+
for (const [gid, v] of [...dirtyRoles]) {
|
|
2505
2522
|
const g = core.groups.get(gid);
|
|
2506
2523
|
if (!g) {
|
|
2507
2524
|
dirtyRoles.delete(gid);
|
|
2508
2525
|
continue;
|
|
2509
2526
|
}
|
|
2510
2527
|
try {
|
|
2511
|
-
|
|
2512
|
-
dirtyRoles.delete(gid);
|
|
2528
|
+
await writeFileAtomic(s.rolesFile(gid), JSON.stringify(rolesDocument(g)), WRITE_FILE_OPTS);
|
|
2529
|
+
if (dirtyRoles.get(gid) === v) dirtyRoles.delete(gid);
|
|
2513
2530
|
fsyncDirs.add(s.groupDir(gid));
|
|
2514
2531
|
} catch (e) {
|
|
2515
2532
|
console.error(`[dsh-group-chat] 群组 ${gid} roles.json 落盘失败(保留脏标记待重试):`, e);
|
|
2516
2533
|
}
|
|
2517
2534
|
}
|
|
2518
|
-
for (const gid of [...dirtyWorkspace]) {
|
|
2535
|
+
for (const [gid, v] of [...dirtyWorkspace]) {
|
|
2519
2536
|
const g = core.groups.get(gid);
|
|
2520
2537
|
if (!g) {
|
|
2521
2538
|
dirtyWorkspace.delete(gid);
|
|
2522
2539
|
continue;
|
|
2523
2540
|
}
|
|
2524
2541
|
try {
|
|
2525
|
-
|
|
2526
|
-
dirtyWorkspace.delete(gid);
|
|
2542
|
+
await writeFileAtomic(s.workspaceFile(gid), (g.workspaceDir || "") + "\n", WRITE_FILE_OPTS);
|
|
2543
|
+
if (dirtyWorkspace.get(gid) === v) dirtyWorkspace.delete(gid);
|
|
2527
2544
|
fsyncDirs.add(s.groupDir(gid));
|
|
2528
2545
|
} catch (e) {
|
|
2529
2546
|
console.error(`[dsh-group-chat] 群组 ${gid} workspaceDir 落盘失败(保留脏标记待重试):`, e);
|
|
2530
2547
|
}
|
|
2531
2548
|
}
|
|
2532
|
-
if (ledgerDirty)
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2549
|
+
if (ledgerDirty > 0) {
|
|
2550
|
+
const v = ledgerDirty;
|
|
2551
|
+
try {
|
|
2552
|
+
await writeFileAtomic(s.ledgerFile, JSON.stringify(ledgerDocument(), null, 2), WRITE_FILE_OPTS);
|
|
2553
|
+
if (ledgerDirty === v) ledgerDirty = 0;
|
|
2554
|
+
fsyncDirs.add(s.dir);
|
|
2555
|
+
} catch (e) {
|
|
2556
|
+
console.error("[dsh-group-chat] ledger.json 落盘失败(保留脏标记待重试):", e);
|
|
2557
|
+
}
|
|
2538
2558
|
}
|
|
2539
2559
|
for (const d of fsyncDirs) s.fsyncDir(d);
|
|
2540
2560
|
};
|
|
2541
2561
|
/** 事件驱动落盘;同一 tick 内多次变更合并为一次写。 */
|
|
2542
2562
|
const schedulePersist = ({ ledger = false, session = null, roles: roleGroup = null, workspace = null } = {}) => {
|
|
2543
2563
|
if (store() === null) return;
|
|
2544
|
-
if (ledger) ledgerDirty
|
|
2545
|
-
if (session) dirtySessions
|
|
2546
|
-
if (roleGroup) dirtyRoles
|
|
2547
|
-
if (workspace) dirtyWorkspace
|
|
2564
|
+
if (ledger) ledgerDirty++;
|
|
2565
|
+
if (session) mark(dirtySessions, session);
|
|
2566
|
+
if (roleGroup) mark(dirtyRoles, roleGroup);
|
|
2567
|
+
if (workspace) mark(dirtyWorkspace, workspace);
|
|
2548
2568
|
if (flushScheduled) return;
|
|
2549
2569
|
flushScheduled = true;
|
|
2550
2570
|
Promise.resolve().then(() => {
|
|
2551
2571
|
flushScheduled = false;
|
|
2552
|
-
|
|
2572
|
+
flushChain = flushChain.then(async () => {
|
|
2573
|
+
const s = store();
|
|
2574
|
+
if (s !== null) await flushAll(s);
|
|
2575
|
+
}).catch((e) => {
|
|
2576
|
+
console.error("[dsh-group-chat] 落盘 flush 异常(脏标记保留待重试):", e);
|
|
2577
|
+
});
|
|
2553
2578
|
});
|
|
2554
2579
|
};
|
|
2555
2580
|
const dropDirty = ({ session = null, roles: roleGroup = null, workspace = null }) => {
|
|
@@ -2709,18 +2734,18 @@ function createPersistence(core) {
|
|
|
2709
2734
|
hydrate();
|
|
2710
2735
|
return {
|
|
2711
2736
|
schedulePersist,
|
|
2712
|
-
flushNow,
|
|
2713
2737
|
dropDirty,
|
|
2714
|
-
release() {
|
|
2738
|
+
async release() {
|
|
2715
2739
|
const s = store();
|
|
2716
2740
|
if (s === null) return;
|
|
2741
|
+
core.store = null;
|
|
2717
2742
|
try {
|
|
2718
|
-
|
|
2743
|
+
await flushChain;
|
|
2744
|
+
await flushAll(s);
|
|
2719
2745
|
} catch {}
|
|
2720
2746
|
try {
|
|
2721
2747
|
s.release();
|
|
2722
2748
|
} catch {}
|
|
2723
|
-
core.store = null;
|
|
2724
2749
|
}
|
|
2725
2750
|
};
|
|
2726
2751
|
}
|
|
@@ -2729,12 +2754,13 @@ function createPersistence(core) {
|
|
|
2729
2754
|
/**
|
|
2730
2755
|
* 工具执行(TOOLS.md §2:沙箱 / §3:确认闸门):read_file / list_dir /
|
|
2731
2756
|
* run_command 三件套;realpath 硬边界 + 分隔符比较;run_command 按群组
|
|
2732
|
-
*
|
|
2757
|
+
* 权限档位走逐条确认或直接执行,经 `shell` 服务(ctx.shell 沙箱执行器)
|
|
2758
|
+
* 以 per-call sandboxPolicy 收紧到群工作区。
|
|
2733
2759
|
* @module dsh-group-chat/host/tools
|
|
2734
2760
|
*/
|
|
2735
2761
|
/** 创建工具面。 */
|
|
2736
2762
|
function createTools(core, touch) {
|
|
2737
|
-
const { run } = core;
|
|
2763
|
+
const { run, shell } = core;
|
|
2738
2764
|
/** realpath 硬边界:目标必须在群组工作区内(带分隔符比较,防 /ws/foo 放行 /ws/foobar)。 */
|
|
2739
2765
|
const resolveInWorkspace = (root, rawPath) => {
|
|
2740
2766
|
const p = rawPath === void 0 || rawPath === null || String(rawPath).trim() === "" ? "." : String(rawPath);
|
|
@@ -2821,54 +2847,50 @@ function createTools(core, touch) {
|
|
|
2821
2847
|
};
|
|
2822
2848
|
}
|
|
2823
2849
|
};
|
|
2824
|
-
|
|
2825
|
-
|
|
2850
|
+
/**
|
|
2851
|
+
* 经 `shell` 服务执行命令(TOOLS.md §2):cwd 与沙箱均收紧到群工作区根
|
|
2852
|
+
* (workspace_write 档经 sandboxPolicy 强制;full_access 档对齐 DSH
|
|
2853
|
+
* danger-full-access 语义免受限)。超时/中止由执行器 kill 进程并按首因
|
|
2854
|
+
* 分类报告;runner 失效(如 SANDBOX_UNAVAILABLE)经 catch 报错不执行。
|
|
2855
|
+
*/
|
|
2856
|
+
const runCommandTool = async (g, root, command) => {
|
|
2857
|
+
const abort = new AbortController();
|
|
2858
|
+
run.commandAbort = abort;
|
|
2826
2859
|
try {
|
|
2827
|
-
|
|
2860
|
+
const spec = shell.resolve({
|
|
2861
|
+
command,
|
|
2862
|
+
workdir: root,
|
|
2863
|
+
timeoutMs: RUN_CMD_TIMEOUT_MS,
|
|
2864
|
+
stdoutMaxBytes: CMD_CAPTURE_MAX_BYTES,
|
|
2865
|
+
sandboxPolicy: g.permissionTier === "full_access" ? {
|
|
2866
|
+
mode: "danger-full-access",
|
|
2867
|
+
workspaceRoot: root
|
|
2868
|
+
} : {
|
|
2869
|
+
mode: "workspace-write",
|
|
2870
|
+
workspaceRoot: root
|
|
2871
|
+
},
|
|
2872
|
+
signal: abort.signal
|
|
2873
|
+
});
|
|
2874
|
+
const r = await shell.run(spec);
|
|
2875
|
+
let text = r.stdout.text + r.stderr.text;
|
|
2876
|
+
if (r.stdout.truncated || r.stderr.truncated) text += "\n[输出采集超限,已截断(保留末尾)]";
|
|
2877
|
+
if (r.sandbox?.denied) text += "\n[沙箱拦截了越界的文件操作]";
|
|
2878
|
+
if (r.timedOut) text += "\n[执行超时(" + Math.round(RUN_CMD_TIMEOUT_MS / 1e3) + "s),已强制终止]";
|
|
2879
|
+
if (r.exitCode !== 0 && r.exitCode !== null && !r.timedOut) text += "\n[退出码 " + r.exitCode + "]";
|
|
2880
|
+
if (text.length > 8e3) text = text.slice(0, CMD_OUTPUT_MAX_CHARS) + "\n…(输出超长,已截断)";
|
|
2881
|
+
return {
|
|
2882
|
+
status: r.exitCode === 0 ? "ok" : "error",
|
|
2883
|
+
output: text || "(无输出)"
|
|
2884
|
+
};
|
|
2828
2885
|
} catch (e) {
|
|
2829
|
-
|
|
2886
|
+
return {
|
|
2830
2887
|
status: "error",
|
|
2831
2888
|
output: "无法启动命令:" + String(e && e.message || e)
|
|
2832
|
-
}
|
|
2833
|
-
|
|
2889
|
+
};
|
|
2890
|
+
} finally {
|
|
2891
|
+
if (run.commandAbort === abort) run.commandAbort = null;
|
|
2834
2892
|
}
|
|
2835
|
-
|
|
2836
|
-
let out = "";
|
|
2837
|
-
let dropped = 0;
|
|
2838
|
-
let timedOut = false;
|
|
2839
|
-
const onChunk = (chunk) => {
|
|
2840
|
-
if (out.length < 1048576) out += chunk.toString("utf8");
|
|
2841
|
-
else dropped += chunk.length;
|
|
2842
|
-
};
|
|
2843
|
-
child.stdout?.on("data", onChunk);
|
|
2844
|
-
child.stderr?.on("data", onChunk);
|
|
2845
|
-
const timer = setTimeout(() => {
|
|
2846
|
-
timedOut = true;
|
|
2847
|
-
try {
|
|
2848
|
-
child.kill("SIGKILL");
|
|
2849
|
-
} catch {}
|
|
2850
|
-
}, RUN_CMD_TIMEOUT_MS);
|
|
2851
|
-
const finish = (result) => {
|
|
2852
|
-
clearTimeout(timer);
|
|
2853
|
-
if (run.childProc === child) run.childProc = null;
|
|
2854
|
-
resolve(result);
|
|
2855
|
-
};
|
|
2856
|
-
child.on("error", (e) => finish({
|
|
2857
|
-
status: "error",
|
|
2858
|
-
output: "执行失败:" + String(e && e.message || e)
|
|
2859
|
-
}));
|
|
2860
|
-
child.on("close", (code) => {
|
|
2861
|
-
let text = out.length > 1048576 ? out.slice(0, CMD_CAPTURE_MAX_BYTES) : out;
|
|
2862
|
-
if (dropped > 0) text += "\n[输出采集超限,已丢弃 " + dropped + " 字节]";
|
|
2863
|
-
if (timedOut) text += "\n[执行超时(" + Math.round(RUN_CMD_TIMEOUT_MS / 1e3) + "s),已强制终止]";
|
|
2864
|
-
if (code !== 0 && code !== null && !timedOut) text += "\n[退出码 " + code + "]";
|
|
2865
|
-
if (text.length > 8e3) text = text.slice(0, CMD_OUTPUT_MAX_CHARS) + "\n…(输出超长,已截断)";
|
|
2866
|
-
finish({
|
|
2867
|
-
status: code === 0 ? "ok" : "error",
|
|
2868
|
-
output: text || "(无输出)"
|
|
2869
|
-
});
|
|
2870
|
-
});
|
|
2871
|
-
});
|
|
2893
|
+
};
|
|
2872
2894
|
/** run_command 确认闸门:置 pendingConfirm 后无限等待,confirmCommand/stop/dispose 唤醒。 */
|
|
2873
2895
|
const requestConfirmation = (toolCallId, args) => new Promise((resolve) => {
|
|
2874
2896
|
run.pendingConfirm = {
|
|
@@ -2888,8 +2910,8 @@ function createTools(core, touch) {
|
|
|
2888
2910
|
} catch {}
|
|
2889
2911
|
};
|
|
2890
2912
|
const killChild = () => {
|
|
2891
|
-
|
|
2892
|
-
run.
|
|
2913
|
+
try {
|
|
2914
|
+
run.commandAbort?.abort();
|
|
2893
2915
|
} catch {}
|
|
2894
2916
|
};
|
|
2895
2917
|
const executeTool = async (g, root, tc) => {
|
|
@@ -2917,8 +2939,8 @@ function createTools(core, touch) {
|
|
|
2917
2939
|
status: "denied",
|
|
2918
2940
|
output: "用户拒绝了这次命令执行"
|
|
2919
2941
|
};
|
|
2920
|
-
else res = await runCommandTool(root, String(args.command || ""));
|
|
2921
|
-
} else res = await runCommandTool(root, String(args.command || ""));
|
|
2942
|
+
else res = await runCommandTool(g, root, String(args.command || ""));
|
|
2943
|
+
} else res = await runCommandTool(g, root, String(args.command || ""));
|
|
2922
2944
|
} else res = {
|
|
2923
2945
|
status: "error",
|
|
2924
2946
|
output: "未知工具:" + tc.name
|
|
@@ -2988,7 +3010,7 @@ function createGroupChatService(ctx) {
|
|
|
2988
3010
|
tools.killChild();
|
|
2989
3011
|
materials.disposeFileSearch();
|
|
2990
3012
|
bus.dispose();
|
|
2991
|
-
persist.release();
|
|
3013
|
+
return persist.release();
|
|
2992
3014
|
}
|
|
2993
3015
|
};
|
|
2994
3016
|
}
|
|
@@ -2998,6 +3020,7 @@ const name = "group-chat";
|
|
|
2998
3020
|
const inject = [
|
|
2999
3021
|
"llm",
|
|
3000
3022
|
"fs",
|
|
3023
|
+
"shell",
|
|
3001
3024
|
"webServer",
|
|
3002
3025
|
"workspaceRegistry"
|
|
3003
3026
|
];
|
|
@@ -3017,9 +3040,7 @@ const apply = mountOnce("dsh-group-chat", (ctx, config) => {
|
|
|
3017
3040
|
for (const dispose of disposers) dispose();
|
|
3018
3041
|
};
|
|
3019
3042
|
}, "group-chat: host API routes");
|
|
3020
|
-
ctx.effect(() => () =>
|
|
3021
|
-
service.dispose();
|
|
3022
|
-
});
|
|
3043
|
+
ctx.effect(() => () => service.dispose());
|
|
3023
3044
|
let current = () => config ?? {};
|
|
3024
3045
|
const sync = () => {
|
|
3025
3046
|
if (!(current().enabled ?? true)) service.stopAll();
|
|
@@ -128,7 +128,8 @@ export interface RunState {
|
|
|
128
128
|
confirmSignal: {
|
|
129
129
|
resolve: (allowed: boolean) => void;
|
|
130
130
|
} | null;
|
|
131
|
-
|
|
131
|
+
/** 正在执行的 run_command 的中止句柄(stop/dispose 时 abort,执行器 kill 进程)。 */
|
|
132
|
+
commandAbort: AbortController | null;
|
|
132
133
|
/** 最近一次 run 的结束标记:会话列表「已完成/已出错」状态的数据源。 */
|
|
133
134
|
finished: RunFinished | null;
|
|
134
135
|
/** 原地重试时被覆盖的失败消息 id;普通 send 为 null。 */
|
|
@@ -200,7 +201,7 @@ export type SnapshotMessage = Omit<MessageRecord, 'reasoningFull' | 'thinkingSum
|
|
|
200
201
|
/** 发到客户端的全量快照(wire 形态;各表行由领域记录派生,字段增删由编译器同步)。 */
|
|
201
202
|
export interface Snapshot {
|
|
202
203
|
revision: number;
|
|
203
|
-
run: Omit<RunState, 'stopping' | 'queue' | 'confirmSignal' | '
|
|
204
|
+
run: Omit<RunState, 'stopping' | 'queue' | 'confirmSignal' | 'commandAbort'>;
|
|
204
205
|
lastCreated: LastCreated | null;
|
|
205
206
|
groups: GroupRecord[];
|
|
206
207
|
sessions: SnapshotSession[];
|
|
@@ -13,16 +13,14 @@ export interface Persistence {
|
|
|
13
13
|
roles?: string | null;
|
|
14
14
|
workspace?: string | null;
|
|
15
15
|
}) => void;
|
|
16
|
-
/** 同步 flush:写全部脏文件(dispose 最终落盘用)。 */
|
|
17
|
-
flushNow: () => void;
|
|
18
16
|
/** 删除群组/会话后摘除脏标记(对应文件已删/将删,flush 跳过)。 */
|
|
19
17
|
dropDirty: (targets: {
|
|
20
18
|
session?: string | null;
|
|
21
19
|
roles?: string | null;
|
|
22
20
|
workspace?: string | null;
|
|
23
21
|
}) => void;
|
|
24
|
-
/** dispose
|
|
25
|
-
release: () => void
|
|
22
|
+
/** dispose:停止新调度 → 等待挂起 flush → 最终 flush → 释放锁(异步)。 */
|
|
23
|
+
release: () => Promise<void>;
|
|
26
24
|
}
|
|
27
25
|
/**
|
|
28
26
|
* 创建持久化面:构造即完成 store 初始化 + hydrate(v1 迁移 → 残留清理 →
|
|
@@ -32,7 +32,11 @@ export declare class Store {
|
|
|
32
32
|
private acquireLock;
|
|
33
33
|
release(): void;
|
|
34
34
|
fsyncDir(dir: string): void;
|
|
35
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* 同步原子写(tmp+fsync+rename):仅供构造期 v1 迁移(migrateV1)与测试
|
|
37
|
+
* 直用;运行时 flush 走 persistence.ts 的 writeFileAtomic(异步、wx 独占
|
|
38
|
+
* 创建 + 随机后缀 + 符号链接安全,无 per-file fsync,对齐 DSH 基座标准)。
|
|
39
|
+
*/
|
|
36
40
|
atomicWrite(file: string, text: string): void;
|
|
37
41
|
quarantine(file: string): void;
|
|
38
42
|
/** 读 JSON;缺失返回 null;损坏则隔离后返回 null。 */
|
|
@@ -19,8 +19,8 @@ export interface GroupChatService {
|
|
|
19
19
|
subscribePush(push: () => void): () => void;
|
|
20
20
|
/** 设置停用时中止正在进行的群聊。 */
|
|
21
21
|
stopAll(): void;
|
|
22
|
-
/** 卸载/热重载:唤醒确认等待 + kill 子进程 →
|
|
23
|
-
dispose(): void
|
|
22
|
+
/** 卸载/热重载:唤醒确认等待 + kill 子进程 → 异步最终 flush → 释放锁(cordis 会 await)。 */
|
|
23
|
+
dispose(): Promise<void>;
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
26
|
* 创建群聊宿主服务(装配各模块;持久化锁失败时降级内存态运行)。
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 工具执行(TOOLS.md §2:沙箱 / §3:确认闸门):read_file / list_dir /
|
|
3
3
|
* run_command 三件套;realpath 硬边界 + 分隔符比较;run_command 按群组
|
|
4
|
-
*
|
|
4
|
+
* 权限档位走逐条确认或直接执行,经 `shell` 服务(ctx.shell 沙箱执行器)
|
|
5
|
+
* 以 per-call sandboxPolicy 收紧到群工作区。
|
|
5
6
|
* @module dsh-group-chat/host/tools
|
|
6
7
|
*/
|
|
7
8
|
import { TOOL_SCHEMAS } from '../../core/tools.ts';
|
package/lib/types/index.d.ts
CHANGED
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
* (engine/fold.ts;立刻 idle、fire-and-forget,不产生消息)
|
|
15
15
|
* - 群组工作区目录经 `fs` 服务读取(根下一层文本文件,最多 20 个),
|
|
16
16
|
* 以「共享资料」块注入每个角色的 system 提示词;无独立笔记/文件清单
|
|
17
|
+
* - 工具执行(read_file/list_dir/run_command)见 docs/TOOLS.md:run_command
|
|
18
|
+
* 经 `shell` 服务(ctx.shell 沙箱执行器)执行,per-call sandboxPolicy
|
|
19
|
+
* 收紧到群工作区(workspace_write 档)或免受限(full_access 档)
|
|
17
20
|
* - 经 `webServer` 暴露 HTTP API:
|
|
18
21
|
* GET /api/group-chat/state 全量快照
|
|
19
22
|
* POST /api/group-chat/action { kind: mutate|send|stop|confirmCommand|models|efforts|browse|fileSearch, ... }
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@roaming-ai/dsh-group-chat",
|
|
3
3
|
"description": "DSH 模型群聊:多模型角色群组对话面板。角色绑定不同 provider/model,群内共享对话记录;设置页支持启用/停用。",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"README.md"
|
|
31
31
|
],
|
|
32
32
|
"license": "MIT",
|
|
33
|
+
"author": "localSummer (https://github.com/localSummer)",
|
|
33
34
|
"repository": {
|
|
34
35
|
"type": "git",
|
|
35
36
|
"url": "https://github.com/localSummer/dsh-group-chat"
|
|
@@ -55,6 +56,7 @@
|
|
|
55
56
|
}
|
|
56
57
|
},
|
|
57
58
|
"dependencies": {
|
|
59
|
+
"@deepseek-ai/dsh-atomic-write": "0.1.5-rc.2",
|
|
58
60
|
"@deepseek-ai/dsh-file-reference": "0.1.5-rc.2",
|
|
59
61
|
"@deepseek-ai/dsh-file-reference-local": "0.1.5-rc.2",
|
|
60
62
|
"schemastery": "^3.18.0"
|
|
@@ -78,6 +80,7 @@
|
|
|
78
80
|
"@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.1",
|
|
79
81
|
"@deepseek-ai/dsh-llm": "^0.1.5-rc.1",
|
|
80
82
|
"@deepseek-ai/dsh-settings": "^0.1.5-rc.1",
|
|
83
|
+
"@deepseek-ai/dsh-shell": "^0.1.5-rc.1",
|
|
81
84
|
"@deepseek-ai/dsh-workspace": "^0.1.5-rc.1",
|
|
82
85
|
"@types/node": "^22.20.0",
|
|
83
86
|
"@types/react": "~18.3.1",
|
|
@@ -121,6 +121,8 @@ export function GroupChatPanel(): ReactNode {
|
|
|
121
121
|
|
|
122
122
|
const sendMsg = useCallback(async (): Promise<void> => {
|
|
123
123
|
if (busyNow || !sess || sendingRef.current) return
|
|
124
|
+
// 空内容不可发送(按钮 disabled 之外,Enter 路径在此静默拦截)
|
|
125
|
+
if (!input.trim()) return
|
|
124
126
|
sendingRef.current = true
|
|
125
127
|
try {
|
|
126
128
|
if (!participants.length && !mentionedRoles.length) {
|
|
@@ -171,7 +171,7 @@ export function ChatPanel(props: ChatPanelProps): ReactNode {
|
|
|
171
171
|
<div className="dsgc-emptytitle">{sess && sess.topic ? '「' + sess.topic + '」' : '会话已就绪'}</div>
|
|
172
172
|
<div className="dsgc-hint">
|
|
173
173
|
{enabledRoles.length
|
|
174
|
-
? '发送消息开始讨论;@成员
|
|
174
|
+
? '发送消息开始讨论;@成员 点名让其优先回应——每轮全体参与角色按顺序各发言一次,可用右下角轮数控制(1–10 轮)'
|
|
175
175
|
: '先在右侧添加角色(每个角色可绑定不同模型),再回到这里发起讨论。'}
|
|
176
176
|
</div>
|
|
177
177
|
</div>
|
|
@@ -192,7 +192,7 @@ export function Composer(props: ComposerProps): ReactNode {
|
|
|
192
192
|
)
|
|
193
193
|
: null}
|
|
194
194
|
{!input.trim()
|
|
195
|
-
? <div className="dsgc-ph" aria-hidden="true">发消息给全群,@成员
|
|
195
|
+
? <div className="dsgc-ph" aria-hidden="true">发消息给全群,@成员 点名让其回应…</div>
|
|
196
196
|
: null}
|
|
197
197
|
<div
|
|
198
198
|
className="dsgc-edit"
|
|
@@ -233,7 +233,7 @@ export function Composer(props: ComposerProps): ReactNode {
|
|
|
233
233
|
</P.Button>
|
|
234
234
|
)
|
|
235
235
|
: (
|
|
236
|
-
<P.Button variant="primary" onClick={() => { void sendMsg() }} disabled={(!participants.length && !mentionedRoles.length) || !sess}>
|
|
236
|
+
<P.Button variant="primary" onClick={() => { void sendMsg() }} disabled={!input.trim() || (!participants.length && !mentionedRoles.length) || !sess}>
|
|
237
237
|
{Icon(P.IconSendOutline16, 16)}发送
|
|
238
238
|
</P.Button>
|
|
239
239
|
)}
|
package/src/client/lib/styles.ts
CHANGED
|
@@ -201,8 +201,12 @@ export const CSS = [
|
|
|
201
201
|
/* @提及芯片(输入区内原子元素):色点 + 角色色淡底胶囊——弹层候选行同语言 */
|
|
202
202
|
'.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}',
|
|
203
203
|
'.dsgc-card .dsgc-sendrow{padding:0 12px 10px}',
|
|
204
|
-
|
|
205
|
-
|
|
204
|
+
/* 停止/清空按钮:实心红(Button primitive outline 变体的 hover 规则
|
|
205
|
+
background:interactive-bg-hover 特异度 (0,3,0) 会盖掉 (0,1,0) 的红底,
|
|
206
|
+
悬停时按钮变透明;加 button 类型选择器提为 (0,1,1)/(0,3,1),顺序无关
|
|
207
|
+
地压过变体,hover 补回红底并经 brightness 提亮) */
|
|
208
|
+
'button.dsgc-stopbtn{background:var(--dsw-alias-state-error-primary,#e5484d);border-color:transparent;color:#fff;font-weight:600}',
|
|
209
|
+
'button.dsgc-stopbtn:hover:not(:disabled){background:var(--dsw-alias-state-error-primary,#e5484d);filter:brightness(1.08);color:#fff}',
|
|
206
210
|
/* 标签固定在左;芯片在右侧独立换行,后续行与首行芯片左缘对齐(不折到「参与角色」下面)。
|
|
207
211
|
单行 24px;角色过多时随内容增高,避免固定高度把后续行溢出叠到下方输入卡上 */
|
|
208
212
|
'.dsgc-parts{display:flex;align-items:flex-start;gap:6px;min-height:24px}',
|
package/src/core/types.ts
CHANGED
|
@@ -149,7 +149,8 @@ export interface RunState {
|
|
|
149
149
|
queue: string[]
|
|
150
150
|
pendingConfirm: PendingConfirm | null
|
|
151
151
|
confirmSignal: { resolve: (allowed: boolean) => void } | null
|
|
152
|
-
|
|
152
|
+
/** 正在执行的 run_command 的中止句柄(stop/dispose 时 abort,执行器 kill 进程)。 */
|
|
153
|
+
commandAbort: AbortController | null
|
|
153
154
|
/** 最近一次 run 的结束标记:会话列表「已完成/已出错」状态的数据源。 */
|
|
154
155
|
finished: RunFinished | null
|
|
155
156
|
/** 原地重试时被覆盖的失败消息 id;普通 send 为 null。 */
|
|
@@ -210,7 +211,7 @@ export type SnapshotMessage = Omit<MessageRecord, 'reasoningFull' | 'thinkingSum
|
|
|
210
211
|
/** 发到客户端的全量快照(wire 形态;各表行由领域记录派生,字段增删由编译器同步)。 */
|
|
211
212
|
export interface Snapshot {
|
|
212
213
|
revision: number
|
|
213
|
-
run: Omit<RunState, 'stopping' | 'queue' | 'confirmSignal' | '
|
|
214
|
+
run: Omit<RunState, 'stopping' | 'queue' | 'confirmSignal' | 'commandAbort'>
|
|
214
215
|
lastCreated: LastCreated | null
|
|
215
216
|
groups: GroupRecord[]
|
|
216
217
|
sessions: SnapshotSession[]
|
|
@@ -379,7 +379,7 @@ export function createConversation(core: HostState, deps: { touch: () => void, s
|
|
|
379
379
|
run.queue = []
|
|
380
380
|
run.pendingConfirm = null
|
|
381
381
|
run.confirmSignal = null
|
|
382
|
-
run.
|
|
382
|
+
run.commandAbort = null
|
|
383
383
|
run.stopping = false
|
|
384
384
|
run.replaceMessageId = null
|
|
385
385
|
touch()
|