@rezti/dsh-rez-suite 0.1.45 → 0.1.47

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +9 -10
  3. package/README.zh.md +7 -3
  4. package/cordis.patch.yml +3 -1
  5. package/lib/client.d.ts +8 -2
  6. package/lib/client.js +191 -22
  7. package/lib/index.d.ts +4 -0
  8. package/lib/index.js +269 -16
  9. package/lib/style.css +43 -0
  10. package/package.json +7 -5
  11. package/src/boss/mount.ts +44 -4
  12. package/src/boss/register.ts +2 -0
  13. package/src/boss/seed.ts +29 -1
  14. package/src/changelog.ts +104 -0
  15. package/src/client/locales.ts +16 -4
  16. package/src/client/panel/ChangelogTab.tsx +57 -0
  17. package/src/client/panel/ConfigTab.tsx +16 -9
  18. package/src/client/panel/panel.module.css +43 -0
  19. package/src/client/settings-card.tsx +4 -1
  20. package/src/esphome-lab.ts +181 -0
  21. package/src/index.ts +17 -4
  22. package/src/observe-mcp.ts +12 -6
  23. package/src/presets.ts +11 -5
  24. package/src/protocol.ts +2 -0
  25. package/src/rooms.ts +29 -5
  26. package/src/store.ts +6 -0
  27. package/src/tools.ts +41 -0
  28. package/templates/boss/ops/AGENTS.md +1 -0
  29. package/templates/boss/product/AGENTS.md +5 -2
  30. package/templates/staff/design/.agents/skills/ops-design/SKILL.md +6 -0
  31. package/templates/staff/design/AGENTS.md +71 -0
  32. package/templates/staff/design/BOOTSTRAP.md +23 -0
  33. package/templates/staff/design/IDENTITY.md +8 -0
  34. package/templates/staff/design/MEMORY.md +9 -0
  35. package/templates/staff/design/PRIORITIES.md +3 -0
  36. package/templates/staff/design/SOUL.md +6 -0
  37. package/templates/staff/design/USER.md +8 -0
  38. package/templates/staff/hr/.agents/skills/staff-hr/SKILL.md +1 -1
  39. package/templates/staff/hr/.agents/skills/staff-onboard-keys/SKILL.md +99 -0
  40. package/templates/staff/hr/AGENTS.md +10 -0
  41. package/templates/staff/lab/.agents/skills/engineer-lab/SKILL.md +6 -0
  42. package/templates/staff/lab/AGENTS.md +34 -0
  43. package/templates/staff/lab/BOOTSTRAP.md +23 -0
  44. package/templates/staff/lab/IDENTITY.md +8 -0
  45. package/templates/staff/lab/MEMORY.md +9 -0
  46. package/templates/staff/lab/PRIORITIES.md +3 -0
  47. package/templates/staff/lab/SOUL.md +5 -0
  48. package/templates/staff/lab/USER.md +7 -0
package/lib/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join, relative } from "node:path";
4
- import { REZ_CREDENTIAL_REFS, REZ_DEFAULT_CONNECTIONS, REZ_SYSTEMS, lastMcpStartError, loadRezConnections, mergeConnections, secretConfigured, startMcpWhenSecret, writeManagedCredential } from "@rezti/dsh-rez-sso";
4
+ import { REZ_CREDENTIAL_REFS, REZ_DEFAULT_CONNECTIONS, REZ_SYSTEMS, lastMcpStartError, loadRezConnections, mergeConnections, readManagedCredential, secretConfigured, startMcpWhenSecret, writeManagedCredential } from "@rezti/dsh-rez-sso";
5
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";
@@ -2850,7 +2850,9 @@ defineMethod("transform", [
2850
2850
  * Session cwd is the workspace folder (e.g. ~/.dsh/workspaces/法务助理).
2851
2851
  */
2852
2852
  const LEGAL_ROOM_FOLDER = "法务助理";
2853
+ const LAB_ROOM_FOLDER = "实验室";
2853
2854
  const QCC_ROOM_DENY = "企查查仅限法务助理房间使用,请切换到法务助理再查询。";
2855
+ const ESPHOME_LAB_ROOM_DENY = "ESPHome 编译仅限实验室房间使用,请切换到实验室再上传 YAML。";
2854
2856
  const QCC_STATUS_ROOM_OFF = "房间未启用";
2855
2857
  const ROOM_SEP = /[/\\]/;
2856
2858
  /** Last path segment of a workspace cwd or folder name. */
@@ -2860,9 +2862,15 @@ function roomFolderFromPath(value) {
2860
2862
  if (trimmed.length === 0) return void 0;
2861
2863
  return trimmed.split(ROOM_SEP).filter((part) => part.length > 0).at(-1);
2862
2864
  }
2865
+ function isLabRoom(hint) {
2866
+ return roomFolderFromPath(hint) === LAB_ROOM_FOLDER;
2867
+ }
2863
2868
  function isLegalAssistantRoom(hint) {
2864
2869
  return roomFolderFromPath(hint) === LEGAL_ROOM_FOLDER;
2865
2870
  }
2871
+ function isEsphomeCompileToolName(name) {
2872
+ return name === "rez_esphome_compile" || /^mcp__esphome(?:-|__|_)/.test(name);
2873
+ }
2866
2874
  function isQccToolName(name) {
2867
2875
  return /^mcp__qcc(?:-|__|_)/.test(name);
2868
2876
  }
@@ -2882,11 +2890,6 @@ function readCwdFromObject(value) {
2882
2890
  function cwdFromExecution(execution) {
2883
2891
  return readCwdFromObject(execution) ?? readCwdFromObject(execution.session) ?? readCwdFromObject(execution.agent);
2884
2892
  }
2885
- /**
2886
- * Official `ctx.tools.guard` reason. Only Qichacha tools are gated.
2887
- * Must never throw: this guard is global and a throw would block WeChat turns.
2888
- * Missing cwd fails closed — not the legal room.
2889
- */
2890
2893
  function qccRoomGuard(execution) {
2891
2894
  if (!isQccToolName(typeof execution?.name === "string" ? execution.name : "")) return void 0;
2892
2895
  try {
@@ -2896,6 +2899,22 @@ function qccRoomGuard(execution) {
2896
2899
  return QCC_ROOM_DENY;
2897
2900
  }
2898
2901
  }
2902
+ function esphomeLabRoomGuard(execution) {
2903
+ if (!isEsphomeCompileToolName(typeof execution?.name === "string" ? execution.name : "")) return void 0;
2904
+ try {
2905
+ if (isLabRoom(cwdFromExecution(execution))) return void 0;
2906
+ return ESPHOME_LAB_ROOM_DENY;
2907
+ } catch {
2908
+ return ESPHOME_LAB_ROOM_DENY;
2909
+ }
2910
+ }
2911
+ /**
2912
+ * Official `ctx.tools.guard` reason. Qichacha + ESPHome lab compile are room-gated.
2913
+ * Must never throw: this guard is global and a throw would block WeChat turns.
2914
+ */
2915
+ function suiteRoomGuard(execution) {
2916
+ return qccRoomGuard(execution) ?? esphomeLabRoomGuard(execution);
2917
+ }
2899
2918
  //#endregion
2900
2919
  //#region src/observe-mcp.ts
2901
2920
  /**
@@ -2913,8 +2932,11 @@ const COMPOSED_SERVERS = [
2913
2932
  "tapd",
2914
2933
  "gsc",
2915
2934
  "qcc",
2916
- "tianyancha"
2935
+ "tianyancha",
2936
+ "esphome"
2917
2937
  ];
2938
+ /** Front panel hides these until install/docs are ready. Backend MCP can still start if a secret exists. TAPD 1154488930001001224. */
2939
+ const FRONT_HIDDEN_SERVERS = /* @__PURE__ */ new Set(["wechat"]);
2918
2940
  function mcpToolPrefix(server) {
2919
2941
  if (server === "qcc") return "mcp__qcc-";
2920
2942
  return "mcp__" + server + "__";
@@ -2981,9 +3003,10 @@ function observeServer(server, input) {
2981
3003
  };
2982
3004
  }
2983
3005
  function composedServersFor(input) {
2984
- const servers = input.config.role === "boss" ? COMPOSED_SERVERS : COMPOSED_SERVERS.filter((server) => server !== "tianyancha");
2985
- if (input.room !== void 0 && !isLegalAssistantRoom(input.room)) return servers.filter((server) => server !== "qcc");
2986
- return [...servers];
3006
+ let servers = input.config.role === "boss" ? [...COMPOSED_SERVERS] : COMPOSED_SERVERS.filter((server) => server !== "tianyancha");
3007
+ if (input.room !== void 0 && !isLegalAssistantRoom(input.room)) servers = servers.filter((server) => server !== "qcc");
3008
+ if (input.room !== void 0 && !isLabRoom(input.room)) servers = servers.filter((server) => server !== "esphome");
3009
+ return servers.filter((server) => !FRONT_HIDDEN_SERVERS.has(server));
2987
3010
  }
2988
3011
  function observeQccOutsideLegalRoom(input) {
2989
3012
  if (isEnabled(input.config, "qcc") === false) return observeServer("qcc", input);
@@ -3141,6 +3164,12 @@ function publicConfig(config) {
3141
3164
  url: connections.tianyancha.url,
3142
3165
  secretConfigured: secretConfigured("tianyancha"),
3143
3166
  secretRef: REZ_CREDENTIAL_REFS.tianyancha
3167
+ },
3168
+ esphome: {
3169
+ enabled: connections.esphome.enabled,
3170
+ url: connections.esphome.url,
3171
+ secretConfigured: secretConfigured("esphome"),
3172
+ secretRef: REZ_CREDENTIAL_REFS.esphome
3144
3173
  }
3145
3174
  },
3146
3175
  billing: { ...config.billing }
@@ -7688,6 +7717,128 @@ function toolAbortedBeforeDispatchResult(prior) {
7688
7717
  };
7689
7718
  }
7690
7719
  //#endregion
7720
+ //#region src/esphome-lab.ts
7721
+ /**
7722
+ * ESPHome lab: official dsh-mcp-client → lab MCP, plus HTTP fallback.
7723
+ * Lab server runs official `esphome compile`. Never puts firmware bytes
7724
+ * into the model context; the tool returns a click-later download link.
7725
+ */
7726
+ const ESPHOME_MCP_URL = "http://127.0.0.1:8100/mcp";
7727
+ /** POST /v1/compile 只交 YAML、立刻回链接。真机 `esphome compile` 在后台,最长见实验室 `ESPHOME_COMPILE_TIMEOUT`(默认 20 分钟)。 */
7728
+ const ESPHOME_COMPILE_TIMEOUT_MS = 120 * 1e3;
7729
+ function esphomeLabUrl() {
7730
+ const url = loadRezConnections().esphome?.url;
7731
+ return typeof url === "string" && url.length > 0 ? url.replace(/\/+$/, "") : ESPHOME_MCP_URL;
7732
+ }
7733
+ /** MCP is :8100/mcp; click-later HTTP lives on :8099. */
7734
+ function esphomeHttpOrigin(mcpUrl = esphomeLabUrl()) {
7735
+ try {
7736
+ const url = new URL(mcpUrl);
7737
+ if (url.port === "8100") url.port = "8099";
7738
+ return url.origin;
7739
+ } catch {
7740
+ return "http://127.0.0.1:8099";
7741
+ }
7742
+ }
7743
+ function labToken() {
7744
+ const env = process.env.REZ_ESPHOME_TOKEN;
7745
+ if (typeof env === "string" && env.length > 0) return env;
7746
+ return readManagedCredential(REZ_CREDENTIAL_REFS.esphome);
7747
+ }
7748
+ function bearerAuthorization$2(secret) {
7749
+ const trimmed = secret.trim();
7750
+ if (/^bearer\s+/i.test(trimmed)) return "Bearer " + trimmed.replace(/^bearer\s+/i, "");
7751
+ return "Bearer " + trimmed;
7752
+ }
7753
+ function esphomeMcpConfig(secret) {
7754
+ return {
7755
+ serverName: "esphome",
7756
+ transport: "streamable-http",
7757
+ url: esphomeLabUrl(),
7758
+ headers: { Authorization: bearerAuthorization$2(secret) },
7759
+ failOnStartupError: false,
7760
+ toolCallTimeoutMs: ESPHOME_COMPILE_TIMEOUT_MS
7761
+ };
7762
+ }
7763
+ /** Submit YAML; return the click-later link. Does not fetch firmware bytes. */
7764
+ async function startEsphomeCompile(input) {
7765
+ const token = labToken();
7766
+ if (token === void 0) throw new Error("未配置 REZ_ESPHOME_TOKEN。设置 → 插件 → ReZ-TI → ESPHome 实验室 填 Token。");
7767
+ const fetchImpl = input.fetchImpl ?? fetch;
7768
+ const controller = new AbortController();
7769
+ const timer = setTimeout(() => {
7770
+ controller.abort();
7771
+ }, ESPHOME_COMPILE_TIMEOUT_MS);
7772
+ try {
7773
+ const response = await fetchImpl(esphomeHttpOrigin() + "/v1/compile", {
7774
+ method: "POST",
7775
+ headers: {
7776
+ authorization: bearerAuthorization$2(token),
7777
+ "content-type": "application/yaml"
7778
+ },
7779
+ body: input.yaml,
7780
+ signal: controller.signal
7781
+ });
7782
+ const compiled = await response.json();
7783
+ if (!response.ok || compiled.ok !== true || typeof compiled.id !== "string") throw new Error(compiled.error ?? "compile HTTP " + response.status);
7784
+ const download = compiled.download ?? compiled.nextcloud ?? compiled.direct ?? esphomeHttpOrigin() + "/v1/jobs/" + compiled.id + "/download";
7785
+ return {
7786
+ jobId: compiled.id,
7787
+ device: compiled.device ?? "device",
7788
+ status: compiled.status ?? "compiling",
7789
+ download,
7790
+ direct: compiled.direct,
7791
+ nextcloud: compiled.nextcloud,
7792
+ size: compiled.size,
7793
+ sha256: compiled.sha256,
7794
+ logTail: compiled.log_tail,
7795
+ error: compiled.error
7796
+ };
7797
+ } finally {
7798
+ clearTimeout(timer);
7799
+ }
7800
+ }
7801
+ /** @deprecated use startEsphomeCompile */
7802
+ const compileEsphomeFirmware = startEsphomeCompile;
7803
+ /** Start for every identity; execution is room-gated to 实验室. */
7804
+ function mountEsphomeMcp(ctx, start = startMcpWhenSecret) {
7805
+ let session;
7806
+ let pending = false;
7807
+ let retryTimer;
7808
+ const hasRezSso = () => {
7809
+ const host = ctx;
7810
+ if (typeof host.get !== "function") return true;
7811
+ return host.get("rezSso") !== void 0;
7812
+ };
7813
+ const ensure = () => {
7814
+ (async () => {
7815
+ if (session !== void 0 || pending) return;
7816
+ if (!hasRezSso()) {
7817
+ if (retryTimer === void 0) retryTimer = setTimeout(() => {
7818
+ retryTimer = void 0;
7819
+ ensure();
7820
+ }, 300);
7821
+ return;
7822
+ }
7823
+ pending = true;
7824
+ try {
7825
+ session = await start(ctx, {
7826
+ system: "esphome",
7827
+ label: "ESPHome 实验室",
7828
+ config: esphomeMcpConfig
7829
+ });
7830
+ } catch (error) {
7831
+ const why = error instanceof Error ? error.message : String(error);
7832
+ console.error("[dsh-rez-suite] esphome MCP start failed:", why);
7833
+ } finally {
7834
+ pending = false;
7835
+ }
7836
+ })();
7837
+ };
7838
+ ensure();
7839
+ return ensure;
7840
+ }
7841
+ //#endregion
7691
7842
  //#region src/tools.ts
7692
7843
  /**
7693
7844
  * Agent-facing suite tools: status, configuration view, connectivity test,
@@ -7799,6 +7950,56 @@ function rezConfigTool(config) {
7799
7950
  }
7800
7951
  });
7801
7952
  }
7953
+ function rezEsphomeCompileTool() {
7954
+ return defineTool({
7955
+ name: "rez_esphome_compile",
7956
+ description: "Submit ESPHome YAML to the lab (official esphome compile). Returns a download link immediately. Click later: firmware.bin if ok, README.md while compiling or on failure. Only for the 实验室 room. Prefer mcp__esphome__compile when that MCP is connected.",
7957
+ parameters: { yaml: {
7958
+ type: "string",
7959
+ description: "Full ESPHome YAML text (must include esphome.name)."
7960
+ } },
7961
+ output: {
7962
+ schema: {
7963
+ type: "object",
7964
+ properties: {
7965
+ device: {
7966
+ type: "string",
7967
+ required: true
7968
+ },
7969
+ download: {
7970
+ type: "string",
7971
+ required: true
7972
+ },
7973
+ status: {
7974
+ type: "string",
7975
+ required: true
7976
+ },
7977
+ jobId: {
7978
+ type: "string",
7979
+ required: true
7980
+ },
7981
+ nextcloud: { type: "string" },
7982
+ direct: { type: "string" }
7983
+ },
7984
+ additionalProperties: false
7985
+ },
7986
+ render: (_args, value) => text("device: " + (value.device ?? "") + "\nstatus: " + (value.status ?? "") + "\ndownload: " + (value.download ?? ""))
7987
+ },
7988
+ async execute(args) {
7989
+ const yaml = typeof args?.yaml === "string" ? args.yaml.trim() : "";
7990
+ if (yaml.length === 0) throw new Error("yaml is required");
7991
+ const result = await compileEsphomeFirmware({ yaml });
7992
+ return {
7993
+ device: result.device,
7994
+ download: result.download,
7995
+ status: result.status,
7996
+ jobId: result.jobId,
7997
+ nextcloud: result.nextcloud,
7998
+ direct: result.direct
7999
+ };
8000
+ }
8001
+ });
8002
+ }
7802
8003
  //#endregion
7803
8004
  //#region src/boss/register.ts
7804
8005
  /**
@@ -7806,6 +8007,8 @@ function rezConfigTool(config) {
7806
8007
  * `workspaceRegistry.create` is idempotent: same canonical path returns the
7807
8008
  * existing record. New records are prepended, so we register in reverse then
7808
8009
  * `insertBefore` to pin 搞钱 → 产品 → 运营 → 人事助理 → 财务助理 → 法务助理
8010
+ * (运营身份:设计 → 人事助理 → 财务助理 → 法务助理;
8011
+ * 工程师身份:实验室 → 人事助理 → 财务助理 → 法务助理)
7809
8012
  * (微信 stays after that block). `delete` only drops the registry row —
7810
8013
  * directories and session logs stay.
7811
8014
  */
@@ -7881,6 +8084,16 @@ const STAFF_ROOMS = [
7881
8084
  folder: "法务助理"
7882
8085
  }
7883
8086
  ];
8087
+ /** 运营身份专用。不是全员助理,也不是老板「运营」简报室。 */
8088
+ const OPS_ROOMS = [{
8089
+ id: "design",
8090
+ folder: "设计"
8091
+ }];
8092
+ /** 工程师身份专用。ESPHome YAML → 实验室服务器编译 firmware.bin。 */
8093
+ const ENGINEER_ROOMS = [{
8094
+ id: "lab",
8095
+ folder: "实验室"
8096
+ }];
7884
8097
  /** User-owned files. Re-seeding never overwrites these. */
7885
8098
  const LIVING_BASENAMES = /* @__PURE__ */ new Set([
7886
8099
  "USER.md",
@@ -7917,6 +8130,12 @@ function seedBossWorkspaces(options) {
7917
8130
  function seedStaffWorkspaces(options) {
7918
8131
  return seedRooms(options.destRoot, STAFF_ROOMS, options.templatesRoot ?? defaultStaffTemplatesRoot());
7919
8132
  }
8133
+ function seedOpsWorkspaces(options) {
8134
+ return seedRooms(options.destRoot, OPS_ROOMS, options.templatesRoot ?? defaultStaffTemplatesRoot());
8135
+ }
8136
+ function seedEngineerWorkspaces(options) {
8137
+ return seedRooms(options.destRoot, ENGINEER_ROOMS, options.templatesRoot ?? defaultStaffTemplatesRoot());
8138
+ }
7920
8139
  function seedRoom(templatesRoot, destRoot, room) {
7921
8140
  const src = join(templatesRoot, room.id);
7922
8141
  const dest = join(destRoot, room.folder);
@@ -7983,12 +8202,18 @@ const BOSS_GUIDANCE = [
7983
8202
  "每个文件夹是独立作战室,官方 dsh 会注入该目录 AGENTS.md。",
7984
8203
  "若该室仍有 BOOTSTRAP.md:本会话先做短问三拍,沉淀到 PRIORITIES.md / MEMORY.md,然后删除 BOOTSTRAP.md。",
7985
8204
  "已初始化后:读 PRIORITIES.md + MEMORY.md + 今日/昨日 memory/;老板三室回答固定四行:结论 → 数字(哪家公司、哪段时间)→ 拍板 1~3 件事 → 未执行任何过账/付款/对外发送。",
8205
+ "拍图与品牌规范终审在产品室。运营岗在「设计」房间交待审;你说审批了哪份之后,shot 与 clean 同时写入 RAW(产品图走 SKU 路径,品牌规范/VI 走 Marketing/RAW/Brand/)。不要在运营室做修图。",
7986
8206
  "微信文件夹仍是收件箱,不要在那边查账、报销或砍 SKU。",
7987
8207
  "用户用中文就中文、用英文就英文;不要因为工作区文件是中文就强制中文。"
7988
8208
  ].join("");
7989
- const BOSS_ROLE_HINT = "搞钱 / 产品 / 运营仅角色选「老板」时出现。人事 / 财务 / 法务助理所有角色都会登记。";
8209
+ const BOSS_ROLE_HINT = "搞钱 / 产品 / 运营仅角色选「老板」时出现。人事 / 财务 / 法务助理所有角色都会登记。设计工作房间仅「运营」身份出现。实验室仅「工程师」身份出现。";
8210
+ const ENGINEER_LAB_GUIDANCE = ["当前身份是工程师。侧栏第一间是实验室:对方说板型/功能就自己写 ESPHome YAML,或用回形针/粘贴。调 mcp__esphome__compile(或 rez_esphome_compile)。", "立刻把下载链接发给对方。编译要时间:先点开可能是 README.md,编完刷新就是 firmware.bin;失败也是 README(含日志)。不要把 bin 贴进对话。未开通就说未开通。不要自写编译器,不要自写 Device Builder WebSocket,不要默认 OTA 刷机。"].join("");
8211
+ const OPS_DESIGN_GUIDANCE = ["当前身份是运营。侧栏第一间是设计工作房间:产品拍摄图、品牌规范、公司 VI 都在这里交。拍原片或 VI 图用官方 Comfy MCP 只修一次;规范 PDF / 源文件用回形针、不修。员工声明类型、Inbox 待审并转凯文。终审只在老板产品室。", "不要代批、不要直接写 RAW、不要写 Publish、不要做图生场景或白底。搞钱 / 产品 / 运营仅老板身份出现。"].join("");
7990
8212
  function workspaceGuidanceFor(role) {
7991
- return role === "boss" ? BOSS_GUIDANCE : STAFF_GUIDANCE + BOSS_ROLE_HINT;
8213
+ if (role === "boss") return BOSS_GUIDANCE;
8214
+ if (role === "operations") return STAFF_GUIDANCE + OPS_DESIGN_GUIDANCE;
8215
+ if (role === "engineer") return STAFF_GUIDANCE + ENGINEER_LAB_GUIDANCE;
8216
+ return STAFF_GUIDANCE + BOSS_ROLE_HINT;
7992
8217
  }
7993
8218
  function destRoot(env = process.env) {
7994
8219
  if (env.REZ_BOSS_WORKSPACES !== void 0 && env.REZ_BOSS_WORKSPACES !== "") return env.REZ_BOSS_WORKSPACES;
@@ -8007,11 +8232,21 @@ function expectedBossRooms(env = process.env) {
8007
8232
  function expectedStaffRooms(env = process.env) {
8008
8233
  return refsFor(STAFF_ROOMS, env);
8009
8234
  }
8235
+ function expectedOpsRooms(env = process.env) {
8236
+ return refsFor(OPS_ROOMS, env);
8237
+ }
8238
+ function expectedEngineerRooms(env = process.env) {
8239
+ return refsFor(ENGINEER_ROOMS, env);
8240
+ }
8010
8241
  async function syncRezWorkspaces(registry, role, env = process.env) {
8011
8242
  await registerWorkspaces(registry, seedStaffWorkspaces({ destRoot: destRoot(env) }).rooms);
8012
8243
  if (role === "boss") await registerWorkspaces(registry, seedBossWorkspaces({ destRoot: destRoot(env) }).rooms);
8013
8244
  else await unregisterWorkspaces(registry, expectedBossRooms(env));
8014
- await orderWorkspaces(registry, role === "boss" ? [...expectedBossRooms(env), ...expectedStaffRooms(env)] : expectedStaffRooms(env));
8245
+ if (role === "operations") await registerWorkspaces(registry, seedOpsWorkspaces({ destRoot: destRoot(env) }).rooms);
8246
+ else await unregisterWorkspaces(registry, expectedOpsRooms(env));
8247
+ if (role === "engineer") await registerWorkspaces(registry, seedEngineerWorkspaces({ destRoot: destRoot(env) }).rooms);
8248
+ else await unregisterWorkspaces(registry, expectedEngineerRooms(env));
8249
+ await orderWorkspaces(registry, role === "boss" ? [...expectedBossRooms(env), ...expectedStaffRooms(env)] : role === "operations" ? [...expectedOpsRooms(env), ...expectedStaffRooms(env)] : role === "engineer" ? [...expectedEngineerRooms(env), ...expectedStaffRooms(env)] : expectedStaffRooms(env));
8015
8250
  }
8016
8251
  /** Seed/register staff for every role; boss rooms only when role=boss. */
8017
8252
  function mountRezWorkspaces(ctx, getRole) {
@@ -8387,6 +8622,13 @@ const Config = Schema.object({
8387
8622
  }).default(REZ_DEFAULT_CONNECTIONS.tianyancha ?? {
8388
8623
  enabled: true,
8389
8624
  url: "https://mcp.tianyancha.com/mcp"
8625
+ }),
8626
+ esphome: Schema.object({
8627
+ enabled: Schema.boolean().default(true),
8628
+ url: Schema.string().default(REZ_DEFAULT_CONNECTIONS.esphome?.url ?? "http://127.0.0.1:8100/mcp")
8629
+ }).default(REZ_DEFAULT_CONNECTIONS.esphome ?? {
8630
+ enabled: true,
8631
+ url: "http://127.0.0.1:8100/mcp"
8390
8632
  })
8391
8633
  }).default(REZ_DEFAULT_CONNECTIONS),
8392
8634
  billing: Schema.object({
@@ -8408,7 +8650,10 @@ const REZ_GUIDANCE_CORE = [
8408
8650
  "意图用 rez_set_intent(erp/files/chat/home/auto)。审计用 rez_audit。",
8409
8651
  "涉及资金、对外发送、设备控制、生产写入时先征得用户批准。",
8410
8652
  "Match the user language: Chinese or English. Do not force 中文 when they write English.",
8411
- "制度、手册、出差/报销标准、素材路径:打开 Nextcloud Documents/Standards/INDEX.yaml,按 code 取当前 effective_from 最新且未被 replace 的条目,再按相对 uri 打开。引用带路径+生效日。找不到就说「标准文件未归档」,不要用 MEMORY 旧数字。素材文件夹用首次创建标题,不用会变的 Odoo/销售名。"
8653
+ "制度、手册、出差/报销标准、素材路径:打开 Nextcloud Documents/Standards/INDEX.yaml,按 code 取当前 effective_from 最新且未被 replace 的条目,再按相对 uri 打开。引用带路径+生效日。找不到就说「标准文件未归档」,不要用 MEMORY 旧数字。素材文件夹用首次创建标题,不用会变的 Odoo/销售名。",
8654
+ "同事交说明书/规格书:用输入栏回形针上传原 PDF(不要拖进对话——官方只收图;也不要用微信发文件)。不要直接写 Marketing/Publish。先找到该产品对应 TAPD 需求(Wiki 写死的说明书/规格书 TAPD ID;没有则按首次创建标题搜「产品使用说明书」或「技术手册」),把 PDF 放到 Marketing/Inbox/{分类}/{首次创建标题}/manual/ 或 datasheet/(zh.pdf 或 en.pdf),评论该需求、状态改测试中(status_3)、处理人改凯文智能家居。规划中先转到开发中再测试中;已实现要先退回开发中。凯文说审批了哪份(产品、说明书或规格书、中英)之后,再把 Inbox 文件写入 Marketing/Publish 对应路径,TAPD 改已实现。拒绝则改已拒绝、不写 Publish。Wiki 缺 NC RAW、NC Publish、Git products/{slug}/models 与 motion 就说「Wiki 未归档」。这不是一件发布;不要拷下游、不要上渠道。",
8655
+ "同事交拍摄原片:对话里交图即可。先调官方 Comfy MCP 只修一次,得到高精度干净原片;Harness 不做推理,不要自写 WebSocket。这不是图生场景图也不是图生白底图。修完后员工必须声明是什么图(white_bg/scene/label/gallery/packaging,写进 TAPD 评论);未声明不准进审批。不要直接写 Marketing/RAW。两种都先放到 Marketing/Inbox/{分类}/{首次创建标题}/photo/shot/ 与 photo/clean/。对应 TAPD(Wiki 写死的拍摄图 TAPD ID;没有则按首次创建标题搜「产品拍摄」;还没有则评论 1154488930001001218)改测试中、处理人改凯文智能家居。凯文说审批了哪份之后,shot 与 clean 同时写入 Marketing/RAW 对应路径,TAPD 改已实现。拒绝不写 RAW。不要写 Publish、不要上渠道。现有 photo/ 根目录或产品根目录样板不强制搬家。",
8656
+ "同事交品牌规范或公司 VI:这是公司级,写 Marketing/Brand/,不要写进某个产品 SKU 目录。规范 PDF 和 psd/ai 源文件用回形针;VI 图在对话里交,才调官方 Comfy MCP 只修一次。声明类型(guideline 或 logo/wordmark/icon/color/font/template)后,两种先入 Marketing/Inbox/Brand/guideline/ 或 vi/ 的 shot 与 clean。评论 1154488930001001220,改测试中、处理人凯文智能家居。凯文说审批了哪份之后,shot 与 clean 同时写入 Marketing/RAW/Brand 对应路径。拒绝不写 RAW。不要写 Publish。根上 VI/ 与 MKT/VI 是历史盘,不搬家、不再当正式路径。不要把规范抄进 Standards 正文。"
8412
8657
  ].join("");
8413
8658
  function rezGuidanceFor(role) {
8414
8659
  return REZ_GUIDANCE_CORE + workspaceGuidanceFor(role);
@@ -8439,14 +8684,19 @@ function apply(ctx, config) {
8439
8684
  const syncWorkspaces = mountRezWorkspaces(ctx, () => current().role);
8440
8685
  const syncQcc = mountQccMcp(ctx);
8441
8686
  const syncTianyancha = mountTianyanchaMcp(ctx, () => current().role);
8687
+ const syncEsphome = mountEsphomeMcp(ctx);
8442
8688
  const tokens = new TokenManager(joinTokenDb());
8443
8689
  const host = delegatedHost(ctx, () => current());
8444
8690
  ctx.effect(() => () => {
8445
8691
  tokens.close();
8446
8692
  }, "dsh-rez-suite: ledger view");
8447
8693
  const toolsHost = ctx.tools;
8448
- if (typeof toolsHost.guard === "function") ctx.effect(() => toolsHost.guard(qccRoomGuard), "dsh-rez-suite: qcc room guard");
8449
- const tools = [rezStatusTool(host, () => current()), rezConfigTool(() => current())];
8694
+ if (typeof toolsHost.guard === "function") ctx.effect(() => toolsHost.guard(suiteRoomGuard), "dsh-rez-suite: room guard");
8695
+ const tools = [
8696
+ rezStatusTool(host, () => current()),
8697
+ rezConfigTool(() => current()),
8698
+ rezEsphomeCompileTool()
8699
+ ];
8450
8700
  const routes = makeRoutes({
8451
8701
  getConfig: () => current(),
8452
8702
  updateConfig: async (next) => {
@@ -8456,6 +8706,7 @@ function apply(ctx, config) {
8456
8706
  syncWorkspaces();
8457
8707
  syncQcc();
8458
8708
  syncTianyancha();
8709
+ syncEsphome();
8459
8710
  try {
8460
8711
  const settings = ctx.get("settings");
8461
8712
  if (settings?.update !== void 0) await settings.update(REZ_SETTINGS_NAMESPACE, value);
@@ -8515,12 +8766,14 @@ function apply(ctx, config) {
8515
8766
  syncWorkspaces();
8516
8767
  syncQcc();
8517
8768
  syncTianyancha();
8769
+ syncEsphome();
8518
8770
  },
8519
8771
  onChange: () => {
8520
8772
  sync();
8521
8773
  syncWorkspaces();
8522
8774
  syncQcc();
8523
8775
  syncTianyancha();
8776
+ syncEsphome();
8524
8777
  }
8525
8778
  });
8526
8779
  sync();
package/lib/style.css CHANGED
@@ -415,3 +415,46 @@
415
415
  width: 7px;
416
416
  height: 7px;
417
417
  }
418
+
419
+ .LOjPTW_changelogDate {
420
+ font-variant-numeric: tabular-nums;
421
+ color: var(--dsw-alias-label-tertiary);
422
+ margin-left: 8px;
423
+ font-weight: 500;
424
+ }
425
+
426
+ .LOjPTW_changelogBlock {
427
+ flex-direction: column;
428
+ gap: 6px;
429
+ display: flex;
430
+ }
431
+
432
+ .LOjPTW_changelogHeading {
433
+ color: var(--dsw-alias-label-secondary);
434
+ font-size: 12.5px;
435
+ font-weight: 600;
436
+ }
437
+
438
+ .LOjPTW_changelogList {
439
+ color: var(--dsw-alias-label-primary);
440
+ flex-direction: column;
441
+ gap: 6px;
442
+ margin: 0;
443
+ padding-left: 18px;
444
+ font-size: 13px;
445
+ line-height: 1.55;
446
+ display: flex;
447
+ }
448
+
449
+ .LOjPTW_changelogList li + li {
450
+ margin: 0;
451
+ }
452
+
453
+ .LOjPTW_changelogCode {
454
+ background: var(--dsw-alias-bg-layer-3);
455
+ border: 1px solid var(--dsw-alias-border-l2);
456
+ word-break: break-all;
457
+ border-radius: 6px;
458
+ padding: 1px 6px;
459
+ font-size: 12px;
460
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rezti/dsh-rez-suite",
3
- "description": "ReZ-TI one-package install for DeepSeek Harness. Update with: dsh plugin --profile web add @rezti/dsh-rez-suite@latest",
4
- "version": "0.1.45",
3
+ "description": "ReZ-TI one-package install for DeepSeek Harness. Update with: dsh plugin --profile web add @rezti/dsh-rez-suite@0.1.47",
4
+ "version": "0.1.47",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -40,12 +40,13 @@
40
40
  "@rezti/dsh-rez-gsc": "^0.1.1",
41
41
  "@rezti/dsh-rez-ha-bridge": "^0.1.9",
42
42
  "@rezti/dsh-rez-intent": "^0.1.5",
43
- "@rezti/dsh-rez-nextcloud": "^0.1.10",
43
+ "@rezti/dsh-rez-nextcloud": "^0.1.11",
44
44
  "@rezti/dsh-rez-odoo": "^0.1.9",
45
- "@rezti/dsh-rez-sso": "^0.1.13",
45
+ "@rezti/dsh-rez-sso": "^0.1.14",
46
46
  "@rezti/dsh-rez-tapd": "^0.1.1",
47
47
  "@rezti/dsh-rez-token-manager": "^0.1.4",
48
48
  "@rezti/dsh-rez-wechat": "^0.1.14",
49
+ "@rezti/dsh-web-file-uploader": "0.2.2",
49
50
  "@modelcontextprotocol/sdk": "^1.30.0",
50
51
  "zod": "^4.4.3"
51
52
  },
@@ -89,7 +90,8 @@
89
90
  "templates/staff/**/*",
90
91
  "cordis.patch.yml",
91
92
  "README.md",
92
- "README.zh.md"
93
+ "README.zh.md",
94
+ "CHANGELOG.md"
93
95
  ],
94
96
  "license": "Apache-2.0",
95
97
  "publishConfig": {
package/src/boss/mount.ts CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  type RoomRef,
10
10
  type WorkspaceRegistrar,
11
11
  } from './register.ts'
12
- import { BOSS_ROOMS, STAFF_ROOMS, seedBossWorkspaces, seedStaffWorkspaces } from './seed.ts'
12
+ import { BOSS_ROOMS, ENGINEER_ROOMS, OPS_ROOMS, STAFF_ROOMS, seedBossWorkspaces, seedEngineerWorkspaces, seedOpsWorkspaces, seedStaffWorkspaces } from './seed.ts'
13
13
 
14
14
  export const STAFF_GUIDANCE = [
15
15
  '侧栏已登记人事助理 / 财务助理 / 法务助理。装完、选身份后自动出现,不必手动「添加工作区」、也不必拖排序。',
@@ -23,14 +23,28 @@ export const BOSS_GUIDANCE = [
23
23
  '每个文件夹是独立作战室,官方 dsh 会注入该目录 AGENTS.md。',
24
24
  '若该室仍有 BOOTSTRAP.md:本会话先做短问三拍,沉淀到 PRIORITIES.md / MEMORY.md,然后删除 BOOTSTRAP.md。',
25
25
  '已初始化后:读 PRIORITIES.md + MEMORY.md + 今日/昨日 memory/;老板三室回答固定四行:结论 → 数字(哪家公司、哪段时间)→ 拍板 1~3 件事 → 未执行任何过账/付款/对外发送。',
26
+ '拍图与品牌规范终审在产品室。运营岗在「设计」房间交待审;你说审批了哪份之后,shot 与 clean 同时写入 RAW(产品图走 SKU 路径,品牌规范/VI 走 Marketing/RAW/Brand/)。不要在运营室做修图。',
26
27
  '微信文件夹仍是收件箱,不要在那边查账、报销或砍 SKU。',
27
28
  '用户用中文就中文、用英文就英文;不要因为工作区文件是中文就强制中文。',
28
29
  ].join('')
29
30
 
30
- export const BOSS_ROLE_HINT = '搞钱 / 产品 / 运营仅角色选「老板」时出现。人事 / 财务 / 法务助理所有角色都会登记。'
31
+ export const BOSS_ROLE_HINT = '搞钱 / 产品 / 运营仅角色选「老板」时出现。人事 / 财务 / 法务助理所有角色都会登记。设计工作房间仅「运营」身份出现。实验室仅「工程师」身份出现。'
32
+
33
+ export const ENGINEER_LAB_GUIDANCE = [
34
+ '当前身份是工程师。侧栏第一间是实验室:对方说板型/功能就自己写 ESPHome YAML,或用回形针/粘贴。调 mcp__esphome__compile(或 rez_esphome_compile)。',
35
+ '立刻把下载链接发给对方。编译要时间:先点开可能是 README.md,编完刷新就是 firmware.bin;失败也是 README(含日志)。不要把 bin 贴进对话。未开通就说未开通。不要自写编译器,不要自写 Device Builder WebSocket,不要默认 OTA 刷机。',
36
+ ].join('')
37
+
38
+ export const OPS_DESIGN_GUIDANCE = [
39
+ '当前身份是运营。侧栏第一间是设计工作房间:产品拍摄图、品牌规范、公司 VI 都在这里交。拍原片或 VI 图用官方 Comfy MCP 只修一次;规范 PDF / 源文件用回形针、不修。员工声明类型、Inbox 待审并转凯文。终审只在老板产品室。',
40
+ '不要代批、不要直接写 RAW、不要写 Publish、不要做图生场景或白底。搞钱 / 产品 / 运营仅老板身份出现。',
41
+ ].join('')
31
42
 
32
43
  export function workspaceGuidanceFor(role: RezRoleId): string {
33
- return role === 'boss' ? BOSS_GUIDANCE : STAFF_GUIDANCE + BOSS_ROLE_HINT
44
+ if (role === 'boss') return BOSS_GUIDANCE
45
+ if (role === 'operations') return STAFF_GUIDANCE + OPS_DESIGN_GUIDANCE
46
+ if (role === 'engineer') return STAFF_GUIDANCE + ENGINEER_LAB_GUIDANCE
47
+ return STAFF_GUIDANCE + BOSS_ROLE_HINT
34
48
  }
35
49
 
36
50
  /** @deprecated use workspaceGuidanceFor */
@@ -57,6 +71,14 @@ export function expectedStaffRooms(env: NodeJS.ProcessEnv = process.env): RoomRe
57
71
  return refsFor(STAFF_ROOMS, env)
58
72
  }
59
73
 
74
+ export function expectedOpsRooms(env: NodeJS.ProcessEnv = process.env): RoomRef[] {
75
+ return refsFor(OPS_ROOMS, env)
76
+ }
77
+
78
+ export function expectedEngineerRooms(env: NodeJS.ProcessEnv = process.env): RoomRef[] {
79
+ return refsFor(ENGINEER_ROOMS, env)
80
+ }
81
+
60
82
  export async function syncRezWorkspaces(
61
83
  registry: WorkspaceRegistrar,
62
84
  role: RezRoleId,
@@ -72,9 +94,27 @@ export async function syncRezWorkspaces(
72
94
  await unregisterWorkspaces(registry, expectedBossRooms(env))
73
95
  }
74
96
 
97
+ if (role === 'operations') {
98
+ const ops = seedOpsWorkspaces({ destRoot: destRoot(env) })
99
+ await registerWorkspaces(registry, ops.rooms)
100
+ } else {
101
+ await unregisterWorkspaces(registry, expectedOpsRooms(env))
102
+ }
103
+
104
+ if (role === 'engineer') {
105
+ const engineer = seedEngineerWorkspaces({ destRoot: destRoot(env) })
106
+ await registerWorkspaces(registry, engineer.rooms)
107
+ } else {
108
+ await unregisterWorkspaces(registry, expectedEngineerRooms(env))
109
+ }
110
+
75
111
  const visible = role === 'boss'
76
112
  ? [...expectedBossRooms(env), ...expectedStaffRooms(env)]
77
- : expectedStaffRooms(env)
113
+ : role === 'operations'
114
+ ? [...expectedOpsRooms(env), ...expectedStaffRooms(env)]
115
+ : role === 'engineer'
116
+ ? [...expectedEngineerRooms(env), ...expectedStaffRooms(env)]
117
+ : expectedStaffRooms(env)
78
118
  await orderWorkspaces(registry, visible)
79
119
  }
80
120
 
@@ -3,6 +3,8 @@
3
3
  * `workspaceRegistry.create` is idempotent: same canonical path returns the
4
4
  * existing record. New records are prepended, so we register in reverse then
5
5
  * `insertBefore` to pin 搞钱 → 产品 → 运营 → 人事助理 → 财务助理 → 法务助理
6
+ * (运营身份:设计 → 人事助理 → 财务助理 → 法务助理;
7
+ * 工程师身份:实验室 → 人事助理 → 财务助理 → 法务助理)
6
8
  * (微信 stays after that block). `delete` only drops the registry row —
7
9
  * directories and session logs stay.
8
10
  */
package/src/boss/seed.ts CHANGED
@@ -14,9 +14,21 @@ export const STAFF_ROOMS = [
14
14
  { id: 'legal', folder: '法务助理' },
15
15
  ] as const
16
16
 
17
+ /** 运营身份专用。不是全员助理,也不是老板「运营」简报室。 */
18
+ export const OPS_ROOMS = [
19
+ { id: 'design', folder: '设计' },
20
+ ] as const
21
+
22
+ /** 工程师身份专用。ESPHome YAML → 实验室服务器编译 firmware.bin。 */
23
+ export const ENGINEER_ROOMS = [
24
+ { id: 'lab', folder: '实验室' },
25
+ ] as const
26
+
17
27
  export type BossRoomId = (typeof BOSS_ROOMS)[number]['id']
18
28
  export type StaffRoomId = (typeof STAFF_ROOMS)[number]['id']
19
- export type RoomId = BossRoomId | StaffRoomId
29
+ export type OpsRoomId = (typeof OPS_ROOMS)[number]['id']
30
+ export type EngineerRoomId = (typeof ENGINEER_ROOMS)[number]['id']
31
+ export type RoomId = BossRoomId | StaffRoomId | OpsRoomId | EngineerRoomId
20
32
  export type RoomSpec = { id: string; folder: string }
21
33
 
22
34
  /** User-owned files. Re-seeding never overwrites these. */
@@ -96,6 +108,22 @@ export function seedStaffWorkspaces(options: SeedOptions): SeedResult {
96
108
  )
97
109
  }
98
110
 
111
+ export function seedOpsWorkspaces(options: SeedOptions): SeedResult {
112
+ return seedRooms(
113
+ options.destRoot,
114
+ OPS_ROOMS,
115
+ options.templatesRoot ?? defaultStaffTemplatesRoot(),
116
+ )
117
+ }
118
+
119
+ export function seedEngineerWorkspaces(options: SeedOptions): SeedResult {
120
+ return seedRooms(
121
+ options.destRoot,
122
+ ENGINEER_ROOMS,
123
+ options.templatesRoot ?? defaultStaffTemplatesRoot(),
124
+ )
125
+ }
126
+
99
127
  function seedRoom(templatesRoot: string, destRoot: string, room: RoomSpec): SeedRoomResult {
100
128
  const src = join(templatesRoot, room.id)
101
129
  const dest = join(destRoot, room.folder)