@rezti/dsh-rez-suite 0.1.17 → 0.1.19

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
@@ -12,4 +12,4 @@ dsh plugin --profile web update @rezti/dsh-rez-suite
12
12
 
13
13
  包名是 `@rezti/dsh-rez-suite`,不是 `dsh-rezti-suite`。不要对同一 profile 再单独 add 子包,会重复注册。
14
14
 
15
- 拆分包(`dsh-rez-odoo` 等)给开发改代码用。Web 面板(含微信扫码)暂时仍在本包。老板三室(搞钱 / 产品 / 运营)也在本包内播种并登记侧栏,**不要**再装 `@rezti/dsh-rez-boss-desk`。
15
+ 拆分包(`dsh-rez-odoo` 等)给开发改代码用。Web 面板(含微信扫码)暂时仍在本包。设置里角色选「老板」后,才会在侧栏登记搞钱 / 产品 / 运营;切走老板会拿掉这三间(文件夹保留)。**不要**再装 `@rezti/dsh-rez-boss-desk`。
package/lib/client.d.ts CHANGED
@@ -7262,8 +7262,9 @@ declare const zh: {
7262
7262
  readonly 'tab.status': "状态";
7263
7263
  readonly 'tab.audit': "用量";
7264
7264
  readonly 'config.role': "角色";
7265
- readonly 'config.roleHint': "决定注册给模型的 MCP 工具";
7265
+ readonly 'config.roleHint': "选「老板」会在左侧出现搞钱 / 产品 / 运营三室;其他角色决定 MCP 工具";
7266
7266
  readonly 'role.all': "全部";
7267
+ readonly 'role.boss': "老板";
7267
7268
  readonly 'role.engineer': "工程师";
7268
7269
  readonly 'role.sales': "销售";
7269
7270
  readonly 'role.operations': "运营";
package/lib/client.js CHANGED
@@ -23,8 +23,9 @@ const zh = {
23
23
  "tab.status": "状态",
24
24
  "tab.audit": "用量",
25
25
  "config.role": "角色",
26
- "config.roleHint": "决定注册给模型的 MCP 工具",
26
+ "config.roleHint": "选「老板」会在左侧出现搞钱 / 产品 / 运营三室;其他角色决定 MCP 工具",
27
27
  "role.all": "全部",
28
+ "role.boss": "老板",
28
29
  "role.engineer": "工程师",
29
30
  "role.sales": "销售",
30
31
  "role.operations": "运营",
@@ -121,8 +122,9 @@ const en = {
121
122
  "tab.status": "Status",
122
123
  "tab.audit": "Usage",
123
124
  "config.role": "Role",
124
- "config.roleHint": "Which MCP tools are registered for the model",
125
+ "config.roleHint": "Boss adds 搞钱 / 产品 / 运营 workspaces; other roles filter MCP tools",
125
126
  "role.all": "All",
127
+ "role.boss": "Boss",
126
128
  "role.engineer": "Engineer",
127
129
  "role.sales": "Sales",
128
130
  "role.operations": "Operations",
@@ -515,6 +517,10 @@ const ROLES = [
515
517
  value: "all",
516
518
  label: "role.all"
517
519
  },
520
+ {
521
+ value: "boss",
522
+ label: "role.boss"
523
+ },
518
524
  {
519
525
  value: "engineer",
520
526
  label: "role.engineer"
package/lib/index.d.ts CHANGED
@@ -1678,7 +1678,8 @@ declare abstract class SettingsProvider extends Service {
1678
1678
  //#endregion
1679
1679
  //#region src/protocol.d.ts
1680
1680
  /** Agent role, the preset-loader key. */
1681
- type RezRoleId = 'engineer' | 'sales' | 'operations' | 'all';
1681
+ declare const REZ_ROLE_IDS: readonly ["all", "boss", "engineer", "sales", "operations"];
1682
+ type RezRoleId = (typeof REZ_ROLE_IDS)[number];
1682
1683
  /** Transport kinds the in-plugin MCP host can connect. */
1683
1684
  type RezMcpTransport = 'stdio' | 'streamable-http';
1684
1685
  /** One MCP server launch descriptor (standard MCP-style config). */
@@ -1740,7 +1741,8 @@ declare const name = "rez-suite";
1740
1741
  declare const inject: string[];
1741
1742
  declare const REZ_SETTINGS_NAMESPACE: SettingsNamespace;
1742
1743
  declare const Config: Schema<RezConfig>;
1743
- declare const REZ_GUIDANCE: string;
1744
+ declare const REZ_GUIDANCE_CORE: string;
1745
+ declare function rezGuidanceFor(role: RezConfig['role']): string;
1744
1746
  declare function apply(ctx: Context, config?: RezConfig): void;
1745
1747
  //#endregion
1746
- export { Config, REZ_GUIDANCE, REZ_SETTINGS_NAMESPACE, apply, inject, name };
1748
+ export { Config, REZ_GUIDANCE_CORE, REZ_SETTINGS_NAMESPACE, apply, inject, name, rezGuidanceFor };
package/lib/index.js CHANGED
@@ -2,7 +2,7 @@ import { createRequire } from "node:module";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join, relative } from "node:path";
4
4
  import { REZ_CREDENTIAL_REFS, REZ_DEFAULT_CONNECTIONS, REZ_SYSTEMS, lastMcpStartError, mergeConnections, secretConfigured, writeManagedCredential } from "@rezti/dsh-rez-sso";
5
- import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
5
+ import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs";
6
6
  import { DatabaseSync } from "node:sqlite";
7
7
  import { fileURLToPath } from "node:url";
8
8
  //#region ../../node_modules/.pnpm/@deepseek-ai+cosmokit@1.8.2/node_modules/@deepseek-ai/cosmokit/lib/index.js
@@ -2937,6 +2937,17 @@ function testComposedServers(input) {
2937
2937
  }
2938
2938
  //#endregion
2939
2939
  //#region src/protocol.ts
2940
+ /** Agent role, the preset-loader key. */
2941
+ const REZ_ROLE_IDS = [
2942
+ "all",
2943
+ "boss",
2944
+ "engineer",
2945
+ "sales",
2946
+ "operations"
2947
+ ];
2948
+ function isRezRoleId(value) {
2949
+ return typeof value === "string" && REZ_ROLE_IDS.includes(value);
2950
+ }
2940
2951
  const REZ_API = {
2941
2952
  config: "/api/dsh-rez-suite/config",
2942
2953
  test: "/api/dsh-rez-suite/test",
@@ -3033,7 +3044,7 @@ function mergeConfig(base, override) {
3033
3044
  const result = JSON.parse(JSON.stringify(base));
3034
3045
  if (typeof value.enabled === "boolean") result.enabled = value.enabled;
3035
3046
  if (typeof value.announceToAgent === "boolean") result.announceToAgent = value.announceToAgent;
3036
- if (value.role === "engineer" || value.role === "sales" || value.role === "operations" || value.role === "all") result.role = value.role;
3047
+ if (isRezRoleId(value.role)) result.role = value.role;
3037
3048
  result.connections = mergeConnections(value.connections ?? result.connections);
3038
3049
  if (typeof value.servers === "object" && value.servers !== null) for (const [id, raw] of Object.entries(value.servers)) {
3039
3050
  if (typeof raw !== "object" || raw === null) continue;
@@ -3145,7 +3156,7 @@ function applyPublicPatch(current, patch) {
3145
3156
  const next = JSON.parse(JSON.stringify(current));
3146
3157
  const secrets = [];
3147
3158
  if (typeof patch.enabled === "boolean") next.enabled = patch.enabled;
3148
- if (patch.role === "engineer" || patch.role === "sales" || patch.role === "operations" || patch.role === "all") next.role = patch.role;
3159
+ if (isRezRoleId(patch.role)) next.role = patch.role;
3149
3160
  if (typeof patch.connections === "object" && patch.connections !== null) {
3150
3161
  const incoming = patch.connections;
3151
3162
  next.connections = mergeConnections({
@@ -7656,9 +7667,32 @@ function rezConfigTool(config) {
7656
7667
  }
7657
7668
  //#endregion
7658
7669
  //#region src/boss/register.ts
7670
+ /**
7671
+ * Register or drop the three boss rooms in the official workspace sidebar.
7672
+ * `workspaceRegistry.create` is idempotent: same canonical path returns the
7673
+ * existing record. New records are prepended, so we register in reverse to
7674
+ * keep 搞钱 → 产品 → 运营 at the top of the sidebar.
7675
+ * `delete` only drops the registry row — directories and session logs stay.
7676
+ */
7677
+ function canon(path) {
7678
+ try {
7679
+ return realpathSync(path);
7680
+ } catch {
7681
+ return path;
7682
+ }
7683
+ }
7684
+ function isRegisteredBossRoom(workspace, rooms) {
7685
+ return new Set(rooms.map((room) => canon(room.path))).has(canon(workspace.path));
7686
+ }
7659
7687
  async function registerBossWorkspaces(registry, rooms) {
7660
7688
  for (const room of [...rooms].reverse()) await registry.create(room.path, room.folder);
7661
7689
  }
7690
+ async function unregisterBossWorkspaces(registry, rooms) {
7691
+ for (const workspace of registry.list()) {
7692
+ if (!isRegisteredBossRoom(workspace, rooms)) continue;
7693
+ await registry.delete(workspace.id);
7694
+ }
7695
+ }
7662
7696
  //#endregion
7663
7697
  //#region src/boss/seed.ts
7664
7698
  const BOSS_ROOMS = [
@@ -7755,33 +7789,47 @@ function listFiles(root) {
7755
7789
  }
7756
7790
  //#endregion
7757
7791
  //#region src/boss/mount.ts
7792
+ const BOSS_ROLE_HINT = "设置 → 插件 → ReZ-TI → 角色选「老板」后,左侧会出现搞钱 / 产品 / 运营三室。不必手动「添加工作区」。";
7758
7793
  const BOSS_GUIDANCE = [
7759
- "老板三室已播种并登记为侧栏工作区:~/.dsh/workspaces/搞钱、产品、运营。不必手动「添加工作区」。",
7760
- "每个文件夹是独立作战室。官方 dsh 会注入该目录 AGENTS.md。",
7794
+ "当前角色是老板。侧栏已登记三室:~/.dsh/workspaces/搞钱、产品、运营。不必手动「添加工作区」。",
7795
+ "每个文件夹是独立作战室,官方 dsh 会注入该目录 AGENTS.md。",
7761
7796
  "若该室仍有 BOOTSTRAP.md:本会话先做短问三拍,沉淀到 PRIORITIES.md / MEMORY.md,然后删除 BOOTSTRAP.md。",
7762
7797
  "已初始化后:读 PRIORITIES.md + MEMORY.md + 今日/昨日 memory/;回答固定四行:结论 → 数字(哪家公司、哪段时间)→ 拍板 1~3 件事 → 未执行任何过账/付款/对外发送。",
7763
7798
  "微信文件夹仍是收件箱,不要在那边查账或砍 SKU。"
7764
7799
  ].join("");
7800
+ function bossGuidanceFor(role) {
7801
+ return role === "boss" ? BOSS_GUIDANCE : BOSS_ROLE_HINT;
7802
+ }
7765
7803
  function destRoot(env = process.env) {
7766
7804
  if (env.REZ_BOSS_WORKSPACES !== void 0 && env.REZ_BOSS_WORKSPACES !== "") return env.REZ_BOSS_WORKSPACES;
7767
7805
  return join(env.DSH_HOME !== void 0 && env.DSH_HOME !== "" ? env.DSH_HOME : join(homedir(), ".dsh"), "workspaces");
7768
7806
  }
7769
- function tryWorkspaceRegistry(ctx) {
7770
- try {
7771
- return ctx.get("workspaceRegistry");
7772
- } catch {
7807
+ function expectedBossRooms(env = process.env) {
7808
+ const root = destRoot(env);
7809
+ return BOSS_ROOMS.map((room) => ({
7810
+ path: join(root, room.folder),
7811
+ folder: room.folder
7812
+ }));
7813
+ }
7814
+ async function syncBossWorkspaces(registry, role, env = process.env) {
7815
+ if (role === "boss") {
7816
+ await registerBossWorkspaces(registry, seedBossWorkspaces({ destRoot: destRoot(env) }).rooms);
7773
7817
  return;
7774
7818
  }
7819
+ await unregisterBossWorkspaces(registry, expectedBossRooms(env));
7775
7820
  }
7776
- /** Seed files and register official workspaces. Safe when registry is absent. */
7777
- function mountBossDesk(ctx) {
7778
- const seeded = seedBossWorkspaces({ destRoot: destRoot() });
7779
- const registry = tryWorkspaceRegistry(ctx);
7780
- if (registry === void 0) return;
7781
- registerBossWorkspaces(registry, seeded.rooms).catch((error) => {
7782
- const why = error instanceof Error ? error.message : String(error);
7783
- console.error("[dsh-rez-suite] workspaceRegistry.create failed:", why);
7784
- });
7821
+ /** Seed/register 三室 only for role=boss; drop sidebar rows otherwise. Returns a re-sync function. */
7822
+ function mountBossDesk(ctx, getRole) {
7823
+ const run = () => {
7824
+ ctx.inject(["workspaceRegistry"], (inner) => {
7825
+ syncBossWorkspaces(inner.get("workspaceRegistry"), getRole()).catch((error) => {
7826
+ const why = error instanceof Error ? error.message : String(error);
7827
+ console.error("[dsh-rez-suite] boss workspace sync failed:", why);
7828
+ });
7829
+ });
7830
+ };
7831
+ run();
7832
+ return run;
7785
7833
  }
7786
7834
  //#endregion
7787
7835
  //#region src/index.ts
@@ -7807,10 +7855,11 @@ const Config = Schema.object({
7807
7855
  enabled: Schema.boolean().default(true),
7808
7856
  announceToAgent: Schema.boolean().default(true),
7809
7857
  role: Schema.union([
7858
+ "all",
7859
+ "boss",
7810
7860
  "engineer",
7811
7861
  "sales",
7812
- "operations",
7813
- "all"
7862
+ "operations"
7814
7863
  ]).default("all"),
7815
7864
  servers: Schema.dict(serverSchema).default({}),
7816
7865
  connections: Schema.object({
@@ -7840,15 +7889,17 @@ const Config = Schema.object({
7840
7889
  })
7841
7890
  });
7842
7891
  const SECTION_ORDER = 150;
7843
- const REZ_GUIDANCE = [
7892
+ const REZ_GUIDANCE_CORE = [
7844
7893
  "本机已安装 ReZ-TI 拆分包:工作身份由 dsh-rez-sso 提供,MCP 由官方 dsh-mcp-client 接入 Odoo / Nextcloud / 企业微信 / Home Assistant。",
7845
7894
  "工具名 mcp__odoo__*、mcp__nextcloud__*、mcp__wechat__*、mcp__homeassistant__*。",
7846
7895
  "密钥:REZ_ODOO_TOKEN、REZ_NEXTCLOUD_PASSWORD、REZ_WECHAT_WEBHOOK、REZ_HA_TOKEN。",
7847
7896
  "个人微信(QClaw/ClawBot):打开 设置 → 插件 → ReZ-TI → 微信 扫码,然后直接在微信里说话。消息进入网页左侧「微信」文件夹并延续同一会话,模型跟网页默认。网页归档了再从微信说话会自动恢复到侧栏。发「重启对话」会新开一条可见会话并把旧会话归档。网页端必须开着。不必再跑命令、也不要调用微信 MCP 工具。",
7848
7897
  "意图用 rez_set_intent(erp/files/chat/home/auto)。审计用 rez_audit。",
7849
- "涉及资金、对外发送、设备控制、生产写入时先征得用户批准。",
7850
- BOSS_GUIDANCE
7898
+ "涉及资金、对外发送、设备控制、生产写入时先征得用户批准。"
7851
7899
  ].join("");
7900
+ function rezGuidanceFor(role) {
7901
+ return REZ_GUIDANCE_CORE + bossGuidanceFor(role);
7902
+ }
7852
7903
  function normalize(input) {
7853
7904
  return mergeConfig(defaultConfig(), input ?? {});
7854
7905
  }
@@ -7868,8 +7919,8 @@ function delegatedHost(ctx, getConfig) {
7868
7919
  };
7869
7920
  }
7870
7921
  function apply(ctx, config) {
7871
- mountBossDesk(ctx);
7872
7922
  let current = () => normalize(config);
7923
+ const syncBossDesk = mountBossDesk(ctx, () => current().role);
7873
7924
  const tokens = new TokenManager(joinTokenDb());
7874
7925
  const host = delegatedHost(ctx, () => current());
7875
7926
  ctx.effect(() => () => {
@@ -7882,6 +7933,7 @@ function apply(ctx, config) {
7882
7933
  const value = normalize(next);
7883
7934
  saveConfig(value);
7884
7935
  current = () => value;
7936
+ syncBossDesk();
7885
7937
  try {
7886
7938
  const settings = ctx.get("settings");
7887
7939
  if (settings?.update !== void 0) await settings.update(REZ_SETTINGS_NAMESPACE, value);
@@ -7918,7 +7970,7 @@ function apply(ctx, config) {
7918
7970
  if (value.announceToAgent) disposeSection = ctx.systemPrompt.section({
7919
7971
  name: "plugin:dsh-rez-suite",
7920
7972
  order: SECTION_ORDER,
7921
- text: REZ_GUIDANCE
7973
+ text: rezGuidanceFor(value.role)
7922
7974
  });
7923
7975
  disposeRoutes = ctx.effect(() => {
7924
7976
  const disposers = routes.map((route) => ctx.webServer.register(route));
@@ -7937,8 +7989,12 @@ function apply(ctx, config) {
7937
7989
  setSource: (source) => {
7938
7990
  current = source;
7939
7991
  sync();
7992
+ syncBossDesk();
7940
7993
  },
7941
- onChange: sync
7994
+ onChange: () => {
7995
+ sync();
7996
+ syncBossDesk();
7997
+ }
7942
7998
  });
7943
7999
  sync();
7944
8000
  }
@@ -7946,4 +8002,4 @@ function joinTokenDb() {
7946
8002
  return join(homedir(), ".dsh", "dsh-rez-token-manager.sqlite");
7947
8003
  }
7948
8004
  //#endregion
7949
- export { Config, REZ_GUIDANCE, REZ_SETTINGS_NAMESPACE, apply, inject, name };
8005
+ export { Config, REZ_GUIDANCE_CORE, REZ_SETTINGS_NAMESPACE, apply, inject, name, rezGuidanceFor };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rezti/dsh-rez-suite",
3
3
  "description": "ReZ-TI one-package install for DeepSeek Harness. Update with: dsh plugin --profile web update @rezti/dsh-rez-suite",
4
- "version": "0.1.17",
4
+ "version": "0.1.19",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -38,13 +38,13 @@
38
38
  "dependencies": {
39
39
  "@modelcontextprotocol/sdk": "^1.30.0",
40
40
  "zod": "^4.4.3",
41
- "@rezti/dsh-rez-intent": "0.1.4",
42
- "@rezti/dsh-rez-sso": "0.1.7",
43
- "@rezti/dsh-rez-token-manager": "0.1.4",
41
+ "@rezti/dsh-rez-ha-bridge": "0.1.7",
44
42
  "@rezti/dsh-rez-odoo": "0.1.7",
43
+ "@rezti/dsh-rez-intent": "0.1.4",
45
44
  "@rezti/dsh-rez-nextcloud": "0.1.7",
45
+ "@rezti/dsh-rez-sso": "0.1.7",
46
46
  "@rezti/dsh-rez-wechat": "0.1.11",
47
- "@rezti/dsh-rez-ha-bridge": "0.1.7"
47
+ "@rezti/dsh-rez-token-manager": "0.1.4"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "react": "^18.2.0",
package/src/boss/mount.ts CHANGED
@@ -1,38 +1,64 @@
1
1
  import { homedir } from 'node:os'
2
2
  import { join } from 'node:path'
3
3
  import type { Context } from '@deepseek-ai/cordis'
4
- import { registerBossWorkspaces, type WorkspaceRegistrar } from './register.ts'
5
- import { seedBossWorkspaces } from './seed.ts'
4
+ import type { RezRoleId } from '../protocol.ts'
5
+ import {
6
+ registerBossWorkspaces,
7
+ unregisterBossWorkspaces,
8
+ type BossRoomRef,
9
+ type WorkspaceRegistrar,
10
+ } from './register.ts'
11
+ import { BOSS_ROOMS, seedBossWorkspaces } from './seed.ts'
12
+
13
+ export const BOSS_ROLE_HINT = '设置 → 插件 → ReZ-TI → 角色选「老板」后,左侧会出现搞钱 / 产品 / 运营三室。不必手动「添加工作区」。'
6
14
 
7
15
  export const BOSS_GUIDANCE = [
8
- '老板三室已播种并登记为侧栏工作区:~/.dsh/workspaces/搞钱、产品、运营。不必手动「添加工作区」。',
9
- '每个文件夹是独立作战室。官方 dsh 会注入该目录 AGENTS.md。',
16
+ '当前角色是老板。侧栏已登记三室:~/.dsh/workspaces/搞钱、产品、运营。不必手动「添加工作区」。',
17
+ '每个文件夹是独立作战室,官方 dsh 会注入该目录 AGENTS.md。',
10
18
  '若该室仍有 BOOTSTRAP.md:本会话先做短问三拍,沉淀到 PRIORITIES.md / MEMORY.md,然后删除 BOOTSTRAP.md。',
11
19
  '已初始化后:读 PRIORITIES.md + MEMORY.md + 今日/昨日 memory/;回答固定四行:结论 → 数字(哪家公司、哪段时间)→ 拍板 1~3 件事 → 未执行任何过账/付款/对外发送。',
12
20
  '微信文件夹仍是收件箱,不要在那边查账或砍 SKU。',
13
21
  ].join('')
14
22
 
23
+ export function bossGuidanceFor(role: RezRoleId): string {
24
+ return role === 'boss' ? BOSS_GUIDANCE : BOSS_ROLE_HINT
25
+ }
26
+
15
27
  function destRoot(env: NodeJS.ProcessEnv = process.env): string {
16
28
  if (env.REZ_BOSS_WORKSPACES !== undefined && env.REZ_BOSS_WORKSPACES !== '') return env.REZ_BOSS_WORKSPACES
17
29
  const home = env.DSH_HOME !== undefined && env.DSH_HOME !== '' ? env.DSH_HOME : join(homedir(), '.dsh')
18
30
  return join(home, 'workspaces')
19
31
  }
20
32
 
21
- function tryWorkspaceRegistry(ctx: Context): WorkspaceRegistrar | undefined {
22
- try {
23
- return ctx.get('workspaceRegistry') as WorkspaceRegistrar
24
- } catch {
25
- return undefined
33
+ export function expectedBossRooms(env: NodeJS.ProcessEnv = process.env): BossRoomRef[] {
34
+ const root = destRoot(env)
35
+ return BOSS_ROOMS.map((room) => ({ path: join(root, room.folder), folder: room.folder }))
36
+ }
37
+
38
+ export async function syncBossWorkspaces(
39
+ registry: WorkspaceRegistrar,
40
+ role: RezRoleId,
41
+ env: NodeJS.ProcessEnv = process.env,
42
+ ): Promise<void> {
43
+ if (role === 'boss') {
44
+ const seeded = seedBossWorkspaces({ destRoot: destRoot(env) })
45
+ await registerBossWorkspaces(registry, seeded.rooms)
46
+ return
26
47
  }
48
+ await unregisterBossWorkspaces(registry, expectedBossRooms(env))
27
49
  }
28
50
 
29
- /** Seed files and register official workspaces. Safe when registry is absent. */
30
- export function mountBossDesk(ctx: Context): void {
31
- const seeded = seedBossWorkspaces({ destRoot: destRoot() })
32
- const registry = tryWorkspaceRegistry(ctx)
33
- if (registry === undefined) return
34
- void registerBossWorkspaces(registry, seeded.rooms).catch((error: unknown) => {
35
- const why = error instanceof Error ? error.message : String(error)
36
- console.error('[dsh-rez-suite] workspaceRegistry.create failed:', why)
37
- })
51
+ /** Seed/register 三室 only for role=boss; drop sidebar rows otherwise. Returns a re-sync function. */
52
+ export function mountBossDesk(ctx: Context, getRole: () => RezRoleId): () => void {
53
+ const run = (): void => {
54
+ ctx.inject(['workspaceRegistry'], (inner) => {
55
+ const registry = inner.get('workspaceRegistry') as WorkspaceRegistrar
56
+ void syncBossWorkspaces(registry, getRole()).catch((error: unknown) => {
57
+ const why = error instanceof Error ? error.message : String(error)
58
+ console.error('[dsh-rez-suite] boss workspace sync failed:', why)
59
+ })
60
+ })
61
+ }
62
+ run()
63
+ return run
38
64
  }
@@ -1,18 +1,54 @@
1
1
  /**
2
- * Register seeded room directories as official dsh workspaces.
2
+ * Register or drop the three boss rooms in the official workspace sidebar.
3
3
  * `workspaceRegistry.create` is idempotent: same canonical path returns the
4
4
  * existing record. New records are prepended, so we register in reverse to
5
5
  * keep 搞钱 → 产品 → 运营 at the top of the sidebar.
6
+ * `delete` only drops the registry row — directories and session logs stay.
6
7
  */
8
+ import { realpathSync } from 'node:fs'
9
+
10
+ export interface WorkspaceRecord {
11
+ id: string
12
+ path: string
13
+ title: string
14
+ }
15
+
7
16
  export interface WorkspaceRegistrar {
8
17
  create(path: string, title?: string): Promise<unknown>
18
+ list(): WorkspaceRecord[]
19
+ delete(id: string): Promise<boolean>
20
+ }
21
+
22
+ export type BossRoomRef = { path: string; folder: string }
23
+
24
+ function canon(path: string): string {
25
+ try {
26
+ return realpathSync(path)
27
+ } catch {
28
+ return path
29
+ }
30
+ }
31
+
32
+ export function isRegisteredBossRoom(workspace: WorkspaceRecord, rooms: ReadonlyArray<BossRoomRef>): boolean {
33
+ const wanted = new Set(rooms.map((room) => canon(room.path)))
34
+ return wanted.has(canon(workspace.path))
9
35
  }
10
36
 
11
37
  export async function registerBossWorkspaces(
12
38
  registry: WorkspaceRegistrar,
13
- rooms: ReadonlyArray<{ path: string; folder: string }>,
39
+ rooms: ReadonlyArray<BossRoomRef>,
14
40
  ): Promise<void> {
15
41
  for (const room of [...rooms].reverse()) {
16
42
  await registry.create(room.path, room.folder)
17
43
  }
18
44
  }
45
+
46
+ export async function unregisterBossWorkspaces(
47
+ registry: WorkspaceRegistrar,
48
+ rooms: ReadonlyArray<BossRoomRef>,
49
+ ): Promise<void> {
50
+ for (const workspace of registry.list()) {
51
+ if (!isRegisteredBossRoom(workspace, rooms)) continue
52
+ await registry.delete(workspace.id)
53
+ }
54
+ }
@@ -14,8 +14,9 @@ export const zh = {
14
14
  'tab.audit': '用量',
15
15
  // config
16
16
  'config.role': '角色',
17
- 'config.roleHint': '决定注册给模型的 MCP 工具',
17
+ 'config.roleHint': '选「老板」会在左侧出现搞钱 / 产品 / 运营三室;其他角色决定 MCP 工具',
18
18
  'role.all': '全部',
19
+ 'role.boss': '老板',
19
20
  'role.engineer': '工程师',
20
21
  'role.sales': '销售',
21
22
  'role.operations': '运营',
@@ -118,8 +119,9 @@ export const en: Record<RezKey, string> = {
118
119
  'tab.status': 'Status',
119
120
  'tab.audit': 'Usage',
120
121
  'config.role': 'Role',
121
- 'config.roleHint': 'Which MCP tools are registered for the model',
122
+ 'config.roleHint': 'Boss adds 搞钱 / 产品 / 运营 workspaces; other roles filter MCP tools',
122
123
  'role.all': 'All',
124
+ 'role.boss': 'Boss',
123
125
  'role.engineer': 'Engineer',
124
126
  'role.sales': 'Sales',
125
127
  'role.operations': 'Operations',
@@ -11,6 +11,7 @@ import css from './panel.module.css'
11
11
 
12
12
  const ROLES: Array<{ value: RezRoleId; label: string }> = [
13
13
  { value: 'all', label: 'role.all' },
14
+ { value: 'boss', label: 'role.boss' },
14
15
  { value: 'engineer', label: 'role.engineer' },
15
16
  { value: 'sales', label: 'role.sales' },
16
17
  { value: 'operations', label: 'role.operations' },
package/src/index.ts CHANGED
@@ -27,7 +27,7 @@ import { defaultConfig, mergeConfig, saveConfig } from './store.ts'
27
27
  import { TokenManager } from './token-manager.ts'
28
28
  import { rezConfigTool, rezStatusTool } from './tools.ts'
29
29
  import type { RezConfig } from './protocol.ts'
30
- import { BOSS_GUIDANCE, mountBossDesk } from './boss/mount.ts'
30
+ import { bossGuidanceFor, mountBossDesk } from './boss/mount.ts'
31
31
 
32
32
  export const name = 'rez-suite'
33
33
 
@@ -50,7 +50,7 @@ const serverSchema = z.object({
50
50
  export const Config: z<RezConfig> = z.object({
51
51
  enabled: z.boolean().default(true),
52
52
  announceToAgent: z.boolean().default(true),
53
- role: z.union(['engineer', 'sales', 'operations', 'all'] as const).default('all'),
53
+ role: z.union(['all', 'boss', 'engineer', 'sales', 'operations'] as const).default('all'),
54
54
  servers: z.dict(serverSchema).default({}),
55
55
  connections: z.object({
56
56
  odoo: z.object({
@@ -79,16 +79,19 @@ export const Config: z<RezConfig> = z.object({
79
79
 
80
80
  const SECTION_ORDER = 150
81
81
 
82
- export const REZ_GUIDANCE = [
82
+ export const REZ_GUIDANCE_CORE = [
83
83
  '本机已安装 ReZ-TI 拆分包:工作身份由 dsh-rez-sso 提供,MCP 由官方 dsh-mcp-client 接入 Odoo / Nextcloud / 企业微信 / Home Assistant。',
84
84
  '工具名 mcp__odoo__*、mcp__nextcloud__*、mcp__wechat__*、mcp__homeassistant__*。',
85
85
  '密钥:REZ_ODOO_TOKEN、REZ_NEXTCLOUD_PASSWORD、REZ_WECHAT_WEBHOOK、REZ_HA_TOKEN。',
86
86
  '个人微信(QClaw/ClawBot):打开 设置 → 插件 → ReZ-TI → 微信 扫码,然后直接在微信里说话。消息进入网页左侧「微信」文件夹并延续同一会话,模型跟网页默认。网页归档了再从微信说话会自动恢复到侧栏。发「重启对话」会新开一条可见会话并把旧会话归档。网页端必须开着。不必再跑命令、也不要调用微信 MCP 工具。',
87
87
  '意图用 rez_set_intent(erp/files/chat/home/auto)。审计用 rez_audit。',
88
88
  '涉及资金、对外发送、设备控制、生产写入时先征得用户批准。',
89
- BOSS_GUIDANCE,
90
89
  ].join('')
91
90
 
91
+ export function rezGuidanceFor(role: RezConfig['role']): string {
92
+ return REZ_GUIDANCE_CORE + bossGuidanceFor(role)
93
+ }
94
+
92
95
  function normalize(input: Partial<RezConfig> | undefined): RezConfig {
93
96
  return mergeConfig(defaultConfig(), input ?? {})
94
97
  }
@@ -110,8 +113,8 @@ function delegatedHost(ctx: Context, getConfig: () => RezConfig): McpHost {
110
113
  }
111
114
 
112
115
  export function apply(ctx: Context, config?: RezConfig): void {
113
- mountBossDesk(ctx)
114
116
  let current: () => RezConfig = () => normalize(config)
117
+ const syncBossDesk = mountBossDesk(ctx, () => current().role)
115
118
 
116
119
  const tokens = new TokenManager(joinTokenDb())
117
120
  const host = delegatedHost(ctx, () => current())
@@ -128,6 +131,7 @@ export function apply(ctx: Context, config?: RezConfig): void {
128
131
  const value = normalize(next as Partial<RezConfig>)
129
132
  saveConfig(value)
130
133
  current = () => value
134
+ syncBossDesk()
131
135
  try {
132
136
  const settings = ctx.get('settings') as { update?: (namespace: unknown, value: unknown) => Promise<void> } | undefined
133
137
  if (settings?.update !== undefined) await settings.update(REZ_SETTINGS_NAMESPACE, value)
@@ -160,7 +164,7 @@ export function apply(ctx: Context, config?: RezConfig): void {
160
164
  disposeSection = ctx.systemPrompt.section({
161
165
  name: 'plugin:dsh-rez-suite',
162
166
  order: SECTION_ORDER,
163
- text: REZ_GUIDANCE,
167
+ text: rezGuidanceFor(value.role),
164
168
  })
165
169
  }
166
170
 
@@ -179,8 +183,12 @@ export function apply(ctx: Context, config?: RezConfig): void {
179
183
  setSource: (source) => {
180
184
  current = source
181
185
  sync()
186
+ syncBossDesk()
187
+ },
188
+ onChange: () => {
189
+ sync()
190
+ syncBossDesk()
182
191
  },
183
- onChange: sync,
184
192
  })
185
193
 
186
194
  sync()
package/src/presets.ts CHANGED
@@ -13,7 +13,7 @@ interface RoleRule {
13
13
  pattern: string
14
14
  }
15
15
 
16
- const RULES: Record<Exclude<RezRoleId, 'all'>, RoleRule[]> = {
16
+ const RULES: Record<Exclude<RezRoleId, 'all' | 'boss'>, RoleRule[]> = {
17
17
  engineer: [
18
18
  // Odoo lookups and reads.
19
19
  { server: 'odoo', pattern: '*search*' },
@@ -111,7 +111,7 @@ function compilePattern(pattern: string): RegExp {
111
111
  return new RegExp(source, 'i')
112
112
  }
113
113
 
114
- const COMPILED: Record<Exclude<RezRoleId, 'all'>, { server: RezServerId; test: RegExp }[]> = {
114
+ const COMPILED: Record<Exclude<RezRoleId, 'all' | 'boss'>, { server: RezServerId; test: RegExp }[]> = {
115
115
  engineer: RULES.engineer.map(rule => ({ server: rule.server, test: compilePattern(rule.pattern) })),
116
116
  sales: RULES.sales.map(rule => ({ server: rule.server, test: compilePattern(rule.pattern) })),
117
117
  operations: RULES.operations.map(rule => ({ server: rule.server, test: compilePattern(rule.pattern) })),
@@ -119,7 +119,7 @@ const COMPILED: Record<Exclude<RezRoleId, 'all'>, { server: RezServerId; test: R
119
119
 
120
120
  /** Whether one MCP tool may be registered for the selected role. */
121
121
  export function isToolAllowed(role: RezRoleId, server: string, rawName: string): boolean {
122
- if (role === 'all') return true
122
+ if (role === 'all' || role === 'boss') return true
123
123
  const rules = COMPILED[role]
124
124
  return rules.some(rule => rule.server === server && rule.test.test(rawName))
125
125
  }
package/src/protocol.ts CHANGED
@@ -8,7 +8,12 @@
8
8
  export type RezServerId = string
9
9
 
10
10
  /** Agent role, the preset-loader key. */
11
- export type RezRoleId = 'engineer' | 'sales' | 'operations' | 'all'
11
+ export const REZ_ROLE_IDS = ['all', 'boss', 'engineer', 'sales', 'operations'] as const
12
+ export type RezRoleId = (typeof REZ_ROLE_IDS)[number]
13
+
14
+ export function isRezRoleId(value: unknown): value is RezRoleId {
15
+ return typeof value === 'string' && (REZ_ROLE_IDS as readonly string[]).includes(value)
16
+ }
12
17
 
13
18
  /** Transport kinds the in-plugin MCP host can connect. */
14
19
  export type RezMcpTransport = 'stdio' | 'streamable-http'
package/src/routes.ts CHANGED
@@ -13,8 +13,8 @@ import {
13
13
  writeManagedCredential,
14
14
  type RezSystem,
15
15
  } from '@rezti/dsh-rez-sso'
16
- import type { RezConfig, RezMcpServerSpec, RezRoleId } from './protocol.ts'
17
- import { REZ_API } from './protocol.ts'
16
+ import type { RezConfig, RezMcpServerSpec } from './protocol.ts'
17
+ import { isRezRoleId, REZ_API } from './protocol.ts'
18
18
  import type { McpHost } from './mcp-host.ts'
19
19
  import type { TokenManager } from './token-manager.ts'
20
20
  import { publicConfig } from './store.ts'
@@ -85,7 +85,7 @@ function applyPublicPatch(current: RezConfig, patch: Record<string, unknown>): {
85
85
  const next: RezConfig = JSON.parse(JSON.stringify(current)) as RezConfig
86
86
  const secrets: Array<{ system: RezSystem; value: string }> = []
87
87
  if (typeof patch.enabled === 'boolean') next.enabled = patch.enabled
88
- if (patch.role === 'engineer' || patch.role === 'sales' || patch.role === 'operations' || patch.role === 'all') next.role = patch.role as RezRoleId
88
+ if (isRezRoleId(patch.role)) next.role = patch.role
89
89
 
90
90
  if (typeof patch.connections === 'object' && patch.connections !== null) {
91
91
  const incoming = patch.connections as Record<string, unknown>
package/src/store.ts CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  secretConfigured,
16
16
  } from '@rezti/dsh-rez-sso'
17
17
  import type { RezConfig, RezMcpServerSpec, RezPublicConfig } from './protocol.ts'
18
+ import { isRezRoleId } from './protocol.ts'
18
19
 
19
20
  /** File format version. */
20
21
  const FORMAT_VERSION = 1
@@ -128,7 +129,7 @@ export function mergeConfig(base: RezConfig, override: unknown): RezConfig {
128
129
  const result: RezConfig = JSON.parse(JSON.stringify(base)) as RezConfig
129
130
  if (typeof value.enabled === 'boolean') result.enabled = value.enabled
130
131
  if (typeof value.announceToAgent === 'boolean') result.announceToAgent = value.announceToAgent
131
- if (value.role === 'engineer' || value.role === 'sales' || value.role === 'operations' || value.role === 'all') result.role = value.role
132
+ if (isRezRoleId(value.role)) result.role = value.role
132
133
 
133
134
  result.connections = mergeConnections(value.connections ?? result.connections)
134
135