@rezti/dsh-rez-suite 0.1.48 → 0.1.50

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 (64) hide show
  1. package/CHANGELOG.md +12 -2
  2. package/README.md +3 -3
  3. package/README.zh.md +2 -2
  4. package/cordis.patch.yml +1 -1
  5. package/lib/client.d.ts +37 -3
  6. package/lib/client.js +533 -8
  7. package/lib/index.js +247 -19
  8. package/lib/style.css +18 -0
  9. package/package.json +4 -3
  10. package/src/boss/mount.ts +4 -3
  11. package/src/boss/register.ts +1 -1
  12. package/src/boss/seed.ts +29 -0
  13. package/src/changelog.ts +48 -0
  14. package/src/channel-board.ts +55 -0
  15. package/src/client/api.ts +15 -0
  16. package/src/client/locales.ts +74 -6
  17. package/src/client/panel/BoardTab.tsx +136 -0
  18. package/src/client/panel/ConfigTab.tsx +137 -2
  19. package/src/client/panel/StatusTab.tsx +31 -0
  20. package/src/client/panel/panel.module.css +18 -0
  21. package/src/client/settings-card.tsx +4 -1
  22. package/src/esphome-lab.ts +4 -3
  23. package/src/index.ts +3 -2
  24. package/src/observe-mcp.ts +1 -1
  25. package/src/protocol.ts +37 -0
  26. package/src/routes.ts +7 -1
  27. package/src/tools.ts +49 -6
  28. package/src/wecom-cli.ts +125 -0
  29. package/templates/boss/ops/AGENTS.md +2 -0
  30. package/templates/shared/wecom-cli.SOURCE.md +5 -0
  31. package/templates/shared/wecom-office/SKILL.md +31 -0
  32. package/templates/shared/wecomcli-calendar/SKILL.md +303 -0
  33. package/templates/shared/wecomcli-contact/SKILL.md +58 -0
  34. package/templates/shared/wecomcli-disk/SKILL.md +389 -0
  35. package/templates/shared/wecomcli-doc/SKILL.md +137 -0
  36. package/templates/shared/wecomcli-doc-manage/SKILL.md +132 -0
  37. package/templates/shared/wecomcli-email/SKILL.md +218 -0
  38. package/templates/shared/wecomcli-media/SKILL.md +98 -0
  39. package/templates/shared/wecomcli-meeting/SKILL.md +373 -0
  40. package/templates/shared/wecomcli-message/SKILL.md +200 -0
  41. package/templates/shared/wecomcli-shared/SKILL.md +73 -0
  42. package/templates/shared/wecomcli-sheet/SKILL.md +172 -0
  43. package/templates/shared/wecomcli-smartpage/SKILL.md +170 -0
  44. package/templates/shared/wecomcli-smartsheet/SKILL.md +154 -0
  45. package/templates/shared/wecomcli-todo/SKILL.md +76 -0
  46. package/templates/staff/design/AGENTS.md +2 -1
  47. package/templates/staff/ecommerce/.agents/skills/ops-ecommerce/SKILL.md +6 -0
  48. package/templates/staff/ecommerce/AGENTS.md +48 -0
  49. package/templates/staff/ecommerce/BOOTSTRAP.md +23 -0
  50. package/templates/staff/ecommerce/IDENTITY.md +8 -0
  51. package/templates/staff/ecommerce/MEMORY.md +9 -0
  52. package/templates/staff/ecommerce/PRIORITIES.md +3 -0
  53. package/templates/staff/ecommerce/SOUL.md +5 -0
  54. package/templates/staff/ecommerce/USER.md +8 -0
  55. package/templates/staff/hr/.agents/skills/staff-onboard-keys/SKILL.md +9 -7
  56. package/templates/staff/lab/AGENTS.md +1 -1
  57. package/templates/staff/publish/.agents/skills/ops-publish/SKILL.md +6 -0
  58. package/templates/staff/publish/AGENTS.md +56 -0
  59. package/templates/staff/publish/BOOTSTRAP.md +23 -0
  60. package/templates/staff/publish/IDENTITY.md +8 -0
  61. package/templates/staff/publish/MEMORY.md +9 -0
  62. package/templates/staff/publish/PRIORITIES.md +3 -0
  63. package/templates/staff/publish/SOUL.md +6 -0
  64. package/templates/staff/publish/USER.md +8 -0
package/lib/index.js CHANGED
@@ -3,9 +3,9 @@ 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, 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
+ import { spawn, spawnSync } from "node:child_process";
6
7
  import { DatabaseSync } from "node:sqlite";
7
8
  import { fileURLToPath } from "node:url";
8
- import { spawn } from "node:child_process";
9
9
  //#region ../../node_modules/.pnpm/@deepseek-ai+cosmokit@1.8.2/node_modules/@deepseek-ai/cosmokit/lib/index.js
10
10
  /** Return true when a value is `null` or `undefined`. */
11
11
  function isNullable(value) {
@@ -2935,7 +2935,7 @@ const COMPOSED_SERVERS = [
2935
2935
  "tianyancha",
2936
2936
  "esphome"
2937
2937
  ];
2938
- /** Front panel hides these until install/docs are ready. Backend MCP can still start if a secret exists. TAPD 1154488930001001224. */
2938
+ /** Front panel hides webhook MCP wechat status; AI bots live in the Config WeCom bindings. TAPD 1154488930001001231. */
2939
2939
  const FRONT_HIDDEN_SERVERS = /* @__PURE__ */ new Set(["wechat"]);
2940
2940
  function mcpToolPrefix(server) {
2941
2941
  if (server === "qcc") return "mcp__qcc-";
@@ -3062,6 +3062,7 @@ const REZ_API = {
3062
3062
  auditReset: "/api/dsh-rez-suite/audit/reset",
3063
3063
  weixin: "/api/dsh-rez-suite/weixin",
3064
3064
  weixinVerify: "/api/dsh-rez-suite/weixin/verify",
3065
+ wecomChannels: "/api/dsh-rez-suite/wecom/channels",
3065
3066
  update: "/api/dsh-rez-suite/update"
3066
3067
  };
3067
3068
  //#endregion
@@ -3229,6 +3230,145 @@ function saveConfig(config) {
3229
3230
  renameSync(tmp, path);
3230
3231
  }
3231
3232
  //#endregion
3233
+ //#region src/wecom-cli.ts
3234
+ /**
3235
+ * Official WeCom office APIs are @wecom/cli, not mcp__wechat__*.
3236
+ * TAPD bug 1154488930001000054: missing MCP row was treated as “no WeCom”.
3237
+ * Official CLI v1.1+ opened 通讯录 / 在线文档 etc. for every company size
3238
+ * (https://github.com/WecomTeam/wecom-cli). Kelvin enabled those modules.
3239
+ */
3240
+ /** Official @wecom/cli >=1.1 catalog. Do not invent extra HTTP APIs. */
3241
+ const WECOM_CLI_CATALOG = [
3242
+ {
3243
+ id: "contact",
3244
+ label: "通讯录"
3245
+ },
3246
+ {
3247
+ id: "doc",
3248
+ label: "在线文档"
3249
+ },
3250
+ {
3251
+ id: "doc-manage",
3252
+ label: "文档管理"
3253
+ },
3254
+ {
3255
+ id: "sheet",
3256
+ label: "在线表格"
3257
+ },
3258
+ {
3259
+ id: "smartsheet",
3260
+ label: "智能表格"
3261
+ },
3262
+ {
3263
+ id: "smartpage",
3264
+ label: "智能文档"
3265
+ },
3266
+ {
3267
+ id: "todo",
3268
+ label: "待办"
3269
+ },
3270
+ {
3271
+ id: "calendar",
3272
+ label: "日程"
3273
+ },
3274
+ {
3275
+ id: "meeting",
3276
+ label: "会议"
3277
+ },
3278
+ {
3279
+ id: "email",
3280
+ label: "邮件"
3281
+ },
3282
+ {
3283
+ id: "disk",
3284
+ label: "微盘"
3285
+ },
3286
+ {
3287
+ id: "message",
3288
+ label: "消息"
3289
+ },
3290
+ {
3291
+ id: "media",
3292
+ label: "媒体文件"
3293
+ }
3294
+ ];
3295
+ const MODULE_LABELS = WECOM_CLI_CATALOG.map((row) => row.label).join("/");
3296
+ const HINT_MISSING = [
3297
+ `未安装官方 wecom-cli(@wecom/cli >=1.1)。企微办公能力(${MODULE_LABELS})走 CLI,不是 MCP。`,
3298
+ "安装:npm i -g @wecom/cli ,然后 wecom-cli auth init。",
3299
+ "官方已面向各规模企业开放通讯录等能力,不要再说「超过 10 人只能用文档」。",
3300
+ "不要因为 rez_status 没有 wechat MCP 行就说没有企微能力。"
3301
+ ].join("");
3302
+ const HINT_UNAUTH = [
3303
+ "已安装 wecom-cli,尚未授权。跑 wecom-cli auth init。",
3304
+ `授权后可用:${MODULE_LABELS}。本企业已开通这些品类。`,
3305
+ "CLI 报某模块未授权才说该能力未开通;不要说「没有企微 MCP」,也不要说 10 人限制。"
3306
+ ].join("");
3307
+ const HINT_OK = [
3308
+ `企微办公能力走 wecom-cli >=1.1(${MODULE_LABELS})。本企业已开通。`,
3309
+ "按房间里官方 wecomcli-* 技能执行,命令以 wecom-cli <模块> --help 为准。",
3310
+ "不要用旧帮助中心「超过 10 人只能用文档」的说法。CLI 报未授权才说该模块未开通。",
3311
+ "设置页 BotID+Secret 只是房间消息通道。不要把没有 mcp__wechat__* 当成没有企微能力。"
3312
+ ].join("");
3313
+ function wecomCliCatalog() {
3314
+ return WECOM_CLI_CATALOG.map((row) => ({ ...row }));
3315
+ }
3316
+ function defaultWecomCliRunner(argv) {
3317
+ const result = spawnSync("wecom-cli", argv, {
3318
+ encoding: "utf8",
3319
+ timeout: 4e3,
3320
+ env: process.env
3321
+ });
3322
+ const error = result.error;
3323
+ return {
3324
+ status: result.status,
3325
+ stdout: typeof result.stdout === "string" ? result.stdout : "",
3326
+ stderr: typeof result.stderr === "string" ? result.stderr : "",
3327
+ errorCode: error?.code
3328
+ };
3329
+ }
3330
+ function looksUnauthorized(text, status) {
3331
+ if (status !== 0) return true;
3332
+ return /未授权|未登录|未初始化|not (logged|auth)|unauthorized|请先.*auth|auth init/i.test(text);
3333
+ }
3334
+ function withCatalog(partial) {
3335
+ return {
3336
+ ...partial,
3337
+ modules: wecomCliCatalog()
3338
+ };
3339
+ }
3340
+ function probeWecomCli(run = defaultWecomCliRunner) {
3341
+ const help = run(["--help"]);
3342
+ if (help.errorCode === "ENOENT") return withCatalog({
3343
+ installed: false,
3344
+ authorized: false,
3345
+ hint: HINT_MISSING
3346
+ });
3347
+ const helpText = help.stdout + help.stderr;
3348
+ if (help.status !== 0 && helpText.trim().length === 0) return withCatalog({
3349
+ installed: false,
3350
+ authorized: false,
3351
+ hint: HINT_MISSING
3352
+ });
3353
+ const auth = run(["auth", "show"]);
3354
+ const text = auth.stdout + auth.stderr;
3355
+ if (auth.errorCode === "ENOENT") return withCatalog({
3356
+ installed: false,
3357
+ authorized: false,
3358
+ hint: HINT_MISSING
3359
+ });
3360
+ if (looksUnauthorized(text, auth.status)) return withCatalog({
3361
+ installed: true,
3362
+ authorized: false,
3363
+ hint: HINT_UNAUTH
3364
+ });
3365
+ return withCatalog({
3366
+ installed: true,
3367
+ authorized: true,
3368
+ hint: HINT_OK
3369
+ });
3370
+ }
3371
+ //#endregion
3232
3372
  //#region src/routes.ts
3233
3373
  const MAX_JSON_BODY_BYTES = 256 * 1024;
3234
3374
  const SECRET_MASK = "********";
@@ -3395,7 +3535,8 @@ function makeRoutes(deps) {
3395
3535
  writeJson(res, 200, {
3396
3536
  role: getConfig().role,
3397
3537
  servers,
3398
- totalRegisteredTools
3538
+ totalRegisteredTools,
3539
+ wecomCli: probeWecomCli()
3399
3540
  });
3400
3541
  }
3401
3542
  },
@@ -7719,9 +7860,10 @@ function toolAbortedBeforeDispatchResult(prior) {
7719
7860
  //#endregion
7720
7861
  //#region src/esphome-lab.ts
7721
7862
  /**
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.
7863
+ * ESPHome lab client in Harness: official dsh-mcp-client → rezti-lab MCP.
7864
+ * The compiler lives in Mac Mini Docker (`deploy/lab`, compose name rezti-lab).
7865
+ * Missing lab / missing token must not block `dsh web` (failOnStartupError: false).
7866
+ * Never puts firmware bytes into the model context; the tool returns a click-later download link.
7725
7867
  */
7726
7868
  const ESPHOME_MCP_URL = "http://127.0.0.1:8100/mcp";
7727
7869
  /** POST /v1/compile 只交 YAML、立刻回链接。真机 `esphome compile` 在后台,最长见实验室 `ESPHOME_COMPILE_TIMEOUT`(默认 20 分钟)。 */
@@ -7852,10 +7994,24 @@ function text(value) {
7852
7994
  text: value
7853
7995
  }];
7854
7996
  }
7855
- function rezStatusTool(host, config) {
7997
+ function renderWecomCli(cli) {
7998
+ const row = cli ?? {
7999
+ installed: false,
8000
+ authorized: false,
8001
+ modules: [],
8002
+ hint: ""
8003
+ };
8004
+ const modules = (row.modules ?? []).map((item) => `${item.label}(${item.id})`).join("、");
8005
+ return [
8006
+ "wecom-cli(企微办公能力,不是 MCP): installed=" + (row.installed ? "yes" : "no") + " authorized=" + (row.authorized ? "yes" : "no"),
8007
+ modules.length > 0 ? "modules: " + modules : "modules: (官方目录见 wecomcli-* 技能)",
8008
+ row.hint
8009
+ ].join("\n");
8010
+ }
8011
+ function rezStatusTool(host, config, wecomCli = probeWecomCli) {
7856
8012
  return defineTool({
7857
8013
  name: "rez_status",
7858
- description: "List the dsh-rez-suite MCP server statuses and how many tools each server registered for the current role. Triggers: rez, MCP, 连接状态.",
8014
+ description: "List MCP server statuses plus WeCom office APIs (official wecom-cli, not mcp__wechat__). Triggers: rez, MCP, 连接状态, 企业微信, 企微能力.",
7859
8015
  parameters: {},
7860
8016
  output: {
7861
8017
  schema: {
@@ -7896,6 +8052,43 @@ function rezStatusTool(host, config) {
7896
8052
  startedAt: { type: "integer" }
7897
8053
  }
7898
8054
  }
8055
+ },
8056
+ wecomCli: {
8057
+ type: "object",
8058
+ required: true,
8059
+ additionalProperties: false,
8060
+ properties: {
8061
+ installed: {
8062
+ type: "boolean",
8063
+ required: true
8064
+ },
8065
+ authorized: {
8066
+ type: "boolean",
8067
+ required: true
8068
+ },
8069
+ hint: {
8070
+ type: "string",
8071
+ required: true
8072
+ },
8073
+ modules: {
8074
+ type: "array",
8075
+ required: true,
8076
+ items: {
8077
+ type: "object",
8078
+ additionalProperties: false,
8079
+ properties: {
8080
+ id: {
8081
+ type: "string",
8082
+ required: true
8083
+ },
8084
+ label: {
8085
+ type: "string",
8086
+ required: true
8087
+ }
8088
+ }
8089
+ }
8090
+ }
8091
+ }
7899
8092
  }
7900
8093
  },
7901
8094
  additionalProperties: false
@@ -7912,7 +8105,7 @@ function rezStatusTool(host, config) {
7912
8105
  "server | state | discovered | registered | error",
7913
8106
  "--- | --- | --- | --- | ---",
7914
8107
  ...rows
7915
- ].join("\n"));
8108
+ ].join("\n") + "\n\n" + renderWecomCli(value.wecomCli));
7916
8109
  }
7917
8110
  },
7918
8111
  async execute(_args, exec) {
@@ -7924,7 +8117,8 @@ function rezStatusTool(host, config) {
7924
8117
  });
7925
8118
  return {
7926
8119
  role: config().role,
7927
- servers
8120
+ servers,
8121
+ wecomCli: wecomCli()
7928
8122
  };
7929
8123
  }
7930
8124
  });
@@ -8007,7 +8201,7 @@ function rezEsphomeCompileTool() {
8007
8201
  * `workspaceRegistry.create` is idempotent: same canonical path returns the
8008
8202
  * existing record. New records are prepended, so we register in reverse then
8009
8203
  * `insertBefore` to pin 搞钱 → 产品 → 运营 → 人事助理 → 财务助理 → 法务助理
8010
- * (运营身份:设计 → 人事助理 → 财务助理 → 法务助理;
8204
+ * (运营身份:设计 → 发布 → 电商 → 人事助理 → 财务助理 → 法务助理;
8011
8205
  * 工程师身份:实验室 → 人事助理 → 财务助理 → 法务助理)
8012
8206
  * (微信 stays after that block). `delete` only drops the registry row —
8013
8207
  * directories and session logs stay.
@@ -8085,10 +8279,20 @@ const STAFF_ROOMS = [
8085
8279
  }
8086
8280
  ];
8087
8281
  /** 运营身份专用。不是全员助理,也不是老板「运营」简报室。 */
8088
- const OPS_ROOMS = [{
8089
- id: "design",
8090
- folder: "设计"
8091
- }];
8282
+ const OPS_ROOMS = [
8283
+ {
8284
+ id: "design",
8285
+ folder: "设计"
8286
+ },
8287
+ {
8288
+ id: "publish",
8289
+ folder: "发布"
8290
+ },
8291
+ {
8292
+ id: "ecommerce",
8293
+ folder: "电商"
8294
+ }
8295
+ ];
8092
8296
  /** 工程师身份专用。ESPHome YAML → 实验室服务器编译 firmware.bin。 */
8093
8297
  const ENGINEER_ROOMS = [{
8094
8298
  id: "lab",
@@ -8166,6 +8370,7 @@ function seedRoom(templatesRoot, destRoot, room) {
8166
8370
  writeFileSync(target, readFileSync(abs));
8167
8371
  written.push(rel);
8168
8372
  }
8373
+ seedSharedSkills(dest, written);
8169
8374
  return {
8170
8375
  id: room.id,
8171
8376
  folder: room.folder,
@@ -8176,6 +8381,24 @@ function seedRoom(templatesRoot, destRoot, room) {
8176
8381
  skipped
8177
8382
  };
8178
8383
  }
8384
+ function defaultSharedTemplatesRoot() {
8385
+ const here = fileURLToPath(new URL(".", import.meta.url));
8386
+ const candidates = [join(here, "../templates/shared"), join(here, "../../templates/shared")];
8387
+ for (const dir of candidates) if (existsSync(join(dir, "wecom-office", "SKILL.md"))) return dir;
8388
+ throw new Error("missing shared workspace templates (expected templates/shared/wecom-office/SKILL.md)");
8389
+ }
8390
+ function seedSharedSkills(dest, written) {
8391
+ const root = defaultSharedTemplatesRoot();
8392
+ for (const name of readdirSync(root).sort()) {
8393
+ const src = join(root, name, "SKILL.md");
8394
+ if (!existsSync(src)) continue;
8395
+ const rel = join(".agents", "skills", name, "SKILL.md");
8396
+ const target = join(dest, rel);
8397
+ mkdirSync(dirname(target), { recursive: true });
8398
+ writeFileSync(target, readFileSync(src));
8399
+ written.push(rel);
8400
+ }
8401
+ }
8179
8402
  function listFiles(root) {
8180
8403
  const out = [];
8181
8404
  const walk = (dir) => {
@@ -8206,9 +8429,13 @@ const BOSS_GUIDANCE = [
8206
8429
  "微信文件夹仍是收件箱,不要在那边查账、报销或砍 SKU。",
8207
8430
  "用户用中文就中文、用英文就英文;不要因为工作区文件是中文就强制中文。"
8208
8431
  ].join("");
8209
- const BOSS_ROLE_HINT = "搞钱 / 产品 / 运营仅角色选「老板」时出现。人事 / 财务 / 法务助理所有角色都会登记。设计工作房间仅「运营」身份出现。实验室仅「工程师」身份出现。";
8432
+ const BOSS_ROLE_HINT = "搞钱 / 产品 / 运营仅角色选「老板」时出现。人事 / 财务 / 法务助理所有角色都会登记。设计 / 发布 / 电商工作房间仅「运营」身份出现。实验室仅「工程师」身份出现。";
8210
8433
  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("");
8434
+ const OPS_DESIGN_GUIDANCE = [
8435
+ "当前身份是运营。侧栏先是设计工作房间:产品拍摄图、品牌规范、公司 VI 都在这里交。拍原片或 VI 图用官方 Comfy MCP 只修一次;规范 PDF / 源文件用回形针、不修。员工声明类型、Inbox 待审并转凯文。终审只在老板产品室。",
8436
+ "接着是发布工作间(媒体连接):内容/社媒 PCP 与 MediaJob,preview 后确认才 submit。不要另建图库,不要做日历 UI;排期写企微一个共享发布日历。",
8437
+ "再接着是电商工作间(ec operation):一期只做阿里巴巴国际站 ListingJob。Amazon 现在没有号,看板标后续,有号再开。设置页「看板」看两室连接与 Odoo/企微权限。不要代批、不要直接写 RAW、不要做图生场景或白底。搞钱 / 产品 / 运营仅老板身份出现。"
8438
+ ].join("");
8212
8439
  function workspaceGuidanceFor(role) {
8213
8440
  if (role === "boss") return BOSS_GUIDANCE;
8214
8441
  if (role === "operations") return STAFF_GUIDANCE + OPS_DESIGN_GUIDANCE;
@@ -8643,8 +8870,9 @@ const Config = Schema.object({
8643
8870
  });
8644
8871
  const SECTION_ORDER = 150;
8645
8872
  const REZ_GUIDANCE_CORE = [
8646
- "本机已安装 ReZ-TI 拆分包:工作身份由 dsh-rez-sso 提供,MCP 由官方 dsh-mcp-client 接入 Odoo / Nextcloud / 企业微信 / Home Assistant / TAPD / GSC。",
8647
- "工具名 mcp__odoo__*、mcp__nextcloud__*、mcp__wechat__*、mcp__homeassistant__*、mcp__tapd__*、mcp__gsc__*。",
8873
+ "本机已安装 ReZ-TI 拆分包:工作身份由 dsh-rez-sso 提供,MCP 由官方 dsh-mcp-client 接入 Odoo / Nextcloud / Home Assistant / TAPD / GSC。",
8874
+ "工具名 mcp__odoo__*、mcp__nextcloud__*、mcp__homeassistant__*、mcp__tapd__*、mcp__gsc__*。",
8875
+ "企业微信办公能力不是 MCP:走官方 wecom-cli >=1.1(通讯录/在线文档/在线表格/智能表格/智能文档/待办/日程/会议/邮件/微盘/消息)。本企业已开通这些品类。看 rez_status 的 wecomCli.modules,按房间 wecomcli-* 技能执行。不要因为没有 mcp__wechat__* 就说没有企微能力,也不要说「超过 10 人只能用文档」。CLI 报某模块未授权才说该能力未开通。设置页 BotID+Secret 只是房间消息通道。旧群 webhook 兼容才会出现 mcp__wechat__*。",
8648
8876
  "密钥:REZ_ODOO_TOKEN、REZ_NEXTCLOUD_PASSWORD、REZ_WECHAT_WEBHOOK、REZ_HA_TOKEN、REZ_TAPD_TOKEN、REZ_GSC_KEY。",
8649
8877
  "个人微信(QClaw/ClawBot):打开 设置 → 插件 → ReZ-TI → 微信 扫码,然后直接在微信里说话。消息进入网页左侧「微信」文件夹并延续同一会话,模型跟网页默认。网页归档了再从微信说话会自动恢复到侧栏。发「重启对话」会新开一条可见会话并把旧会话归档。网页端必须开着。不必再跑命令、也不要调用微信 MCP 工具。",
8650
8878
  "意图用 rez_set_intent(erp/files/chat/home/auto)。审计用 rez_audit。",
package/lib/style.css CHANGED
@@ -209,6 +209,17 @@
209
209
  height: 15px;
210
210
  }
211
211
 
212
+ .LOjPTW_wecomRow {
213
+ grid-template-columns: minmax(7rem, .8fr) minmax(8rem, 1fr) minmax(8rem, 1fr) auto;
214
+ align-items: end;
215
+ gap: 8px;
216
+ display: grid;
217
+ }
218
+
219
+ .LOjPTW_wecomRow .LOjPTW_message {
220
+ grid-column: 1 / -1;
221
+ }
222
+
212
223
  .LOjPTW_toolbar {
213
224
  flex-wrap: wrap;
214
225
  flex: none;
@@ -295,6 +306,13 @@
295
306
  padding: 8px 10px;
296
307
  }
297
308
 
309
+ .LOjPTW_boardTitle {
310
+ color: var(--dsw-alias-label-primary);
311
+ margin: 16px 0 4px;
312
+ font-size: 13px;
313
+ font-weight: 600;
314
+ }
315
+
298
316
  .LOjPTW_badge {
299
317
  border-radius: 999px;
300
318
  padding: 2px 8px;
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@0.1.48",
4
- "version": "0.1.48",
3
+ "description": "ReZ-TI one-package install for DeepSeek Harness. Update with: dsh plugin --profile web add @rezti/dsh-rez-suite@0.1.50",
4
+ "version": "0.1.50",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -45,7 +45,7 @@
45
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
- "@rezti/dsh-rez-wechat": "^0.1.14",
48
+ "@rezti/dsh-rez-wechat": "^0.1.16",
49
49
  "@rezti/dsh-web-file-uploader": "0.2.3",
50
50
  "@modelcontextprotocol/sdk": "^1.30.0",
51
51
  "zod": "^4.4.3"
@@ -88,6 +88,7 @@
88
88
  "servers",
89
89
  "templates/boss/**/*",
90
90
  "templates/staff/**/*",
91
+ "templates/shared/**/*",
91
92
  "cordis.patch.yml",
92
93
  "README.md",
93
94
  "README.zh.md",
package/src/boss/mount.ts CHANGED
@@ -28,7 +28,7 @@ export const BOSS_GUIDANCE = [
28
28
  '用户用中文就中文、用英文就英文;不要因为工作区文件是中文就强制中文。',
29
29
  ].join('')
30
30
 
31
- export const BOSS_ROLE_HINT = '搞钱 / 产品 / 运营仅角色选「老板」时出现。人事 / 财务 / 法务助理所有角色都会登记。设计工作房间仅「运营」身份出现。实验室仅「工程师」身份出现。'
31
+ export const BOSS_ROLE_HINT = '搞钱 / 产品 / 运营仅角色选「老板」时出现。人事 / 财务 / 法务助理所有角色都会登记。设计 / 发布 / 电商工作房间仅「运营」身份出现。实验室仅「工程师」身份出现。'
32
32
 
33
33
  export const ENGINEER_LAB_GUIDANCE = [
34
34
  '当前身份是工程师。侧栏第一间是实验室:对方说板型/功能就自己写 ESPHome YAML,或用回形针/粘贴。调 mcp__esphome__compile(或 rez_esphome_compile)。',
@@ -36,8 +36,9 @@ export const ENGINEER_LAB_GUIDANCE = [
36
36
  ].join('')
37
37
 
38
38
  export const OPS_DESIGN_GUIDANCE = [
39
- '当前身份是运营。侧栏第一间是设计工作房间:产品拍摄图、品牌规范、公司 VI 都在这里交。拍原片或 VI 图用官方 Comfy MCP 只修一次;规范 PDF / 源文件用回形针、不修。员工声明类型、Inbox 待审并转凯文。终审只在老板产品室。',
40
- '不要代批、不要直接写 RAW、不要写 Publish、不要做图生场景或白底。搞钱 / 产品 / 运营仅老板身份出现。',
39
+ '当前身份是运营。侧栏先是设计工作房间:产品拍摄图、品牌规范、公司 VI 都在这里交。拍原片或 VI 图用官方 Comfy MCP 只修一次;规范 PDF / 源文件用回形针、不修。员工声明类型、Inbox 待审并转凯文。终审只在老板产品室。',
40
+ '接着是发布工作间(媒体连接):内容/社媒 PCP 与 MediaJob,preview 后确认才 submit。不要另建图库,不要做日历 UI;排期写企微一个共享发布日历。',
41
+ '再接着是电商工作间(ec operation):一期只做阿里巴巴国际站 ListingJob。Amazon 现在没有号,看板标后续,有号再开。设置页「看板」看两室连接与 Odoo/企微权限。不要代批、不要直接写 RAW、不要做图生场景或白底。搞钱 / 产品 / 运营仅老板身份出现。',
41
42
  ].join('')
42
43
 
43
44
  export function workspaceGuidanceFor(role: RezRoleId): string {
@@ -3,7 +3,7 @@
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
- * (运营身份:设计 → 人事助理 → 财务助理 → 法务助理;
6
+ * (运营身份:设计 → 发布 → 电商 → 人事助理 → 财务助理 → 法务助理;
7
7
  * 工程师身份:实验室 → 人事助理 → 财务助理 → 法务助理)
8
8
  * (微信 stays after that block). `delete` only drops the registry row —
9
9
  * directories and session logs stay.
package/src/boss/seed.ts CHANGED
@@ -17,6 +17,8 @@ export const STAFF_ROOMS = [
17
17
  /** 运营身份专用。不是全员助理,也不是老板「运营」简报室。 */
18
18
  export const OPS_ROOMS = [
19
19
  { id: 'design', folder: '设计' },
20
+ { id: 'publish', folder: '发布' },
21
+ { id: 'ecommerce', folder: '电商' },
20
22
  ] as const
21
23
 
22
24
  /** 工程师身份专用。ESPHome YAML → 实验室服务器编译 firmware.bin。 */
@@ -162,6 +164,8 @@ function seedRoom(templatesRoot: string, destRoot: string, room: RoomSpec): Seed
162
164
  written.push(rel)
163
165
  }
164
166
 
167
+ seedSharedSkills(dest, written)
168
+
165
169
  return {
166
170
  id: room.id,
167
171
  folder: room.folder,
@@ -173,6 +177,31 @@ function seedRoom(templatesRoot: string, destRoot: string, room: RoomSpec): Seed
173
177
  }
174
178
  }
175
179
 
180
+ export function defaultSharedTemplatesRoot(): string {
181
+ const here = fileURLToPath(new URL('.', import.meta.url))
182
+ const candidates = [
183
+ join(here, '../templates/shared'),
184
+ join(here, '../../templates/shared'),
185
+ ]
186
+ for (const dir of candidates) {
187
+ if (existsSync(join(dir, 'wecom-office', 'SKILL.md'))) return dir
188
+ }
189
+ throw new Error('missing shared workspace templates (expected templates/shared/wecom-office/SKILL.md)')
190
+ }
191
+
192
+ function seedSharedSkills(dest: string, written: string[]): void {
193
+ const root = defaultSharedTemplatesRoot()
194
+ for (const name of readdirSync(root).sort()) {
195
+ const src = join(root, name, 'SKILL.md')
196
+ if (!existsSync(src)) continue
197
+ const rel = join('.agents', 'skills', name, 'SKILL.md')
198
+ const target = join(dest, rel)
199
+ mkdirSync(dirname(target), { recursive: true })
200
+ writeFileSync(target, readFileSync(src))
201
+ written.push(rel)
202
+ }
203
+ }
204
+
176
205
  function listFiles(root: string): string[] {
177
206
  const out: string[] = []
178
207
  const walk = (dir: string): void => {
package/src/changelog.ts CHANGED
@@ -20,6 +20,54 @@ export interface ChangelogRelease {
20
20
  }
21
21
 
22
22
  export const SUITE_CHANGELOG: readonly ChangelogRelease[] = [
23
+ {
24
+ version: '0.1.50',
25
+ date: '2026-08-20',
26
+ sections: [
27
+ {
28
+ title: { zh: '运营发布工作间', en: 'Operations publish room' },
29
+ items: [
30
+ {
31
+ zh: '运营身份侧栏多了「发布」房间(合同 Post editor / 媒体连接)和「电商」房间(一期只阿里巴巴国际站;Amazon 等有号)。设置页「看板」看两室状态与 Odoo/企微权限。不建图库,排期写企微共享日历。员工机安装包仍钉已发布版本,不要改审批镜像。',
32
+ en: 'Operations now gets 发布 (media / Post editor) and 电商 (phase 1: Alibaba.com; Amazon waits for an account). The settings Board tab shows channel status plus Odoo/WeCom identity. No media library; schedule goes to one shared WeCom calendar. Do not bump the installer SUITE_VERSION.',
33
+ },
34
+ ],
35
+ },
36
+ {
37
+ title: { zh: '企微固定对话框', en: 'Sticky WeCom sessions' },
38
+ items: [
39
+ {
40
+ zh: '同一房间连续说话复用网页左侧同一条会话。只有「重启对话」才新开。',
41
+ en: 'WeCom turns in one room reuse the same sidebar session. Only “重启对话” opens a new one.',
42
+ },
43
+ ],
44
+ },
45
+ {
46
+ title: { zh: '企微办公能力', en: 'WeCom office APIs' },
47
+ items: [
48
+ {
49
+ zh: 'rez_status / 状态页报官方 wecom-cli >=1.1 品类(通讯录/在线文档/表格/待办/日程/会议/邮件/微盘等)。房间种入官方 wecomcli-* 技能。不要把「没有 wechat MCP」或旧的 10 人限制当成没有企微能力。BotID 只是房间消息通道。不改员工机安装包。',
50
+ en: 'rez_status lists official wecom-cli >=1.1 modules (contacts/docs/sheets/todos/calendar/meetings/mail/disk). Rooms get upstream wecomcli-* skills. A missing wechat MCP row is not “no WeCom”. BotID is only the room chat channel. Do not bump the installer.',
51
+ },
52
+ ],
53
+ },
54
+ ],
55
+ },
56
+ {
57
+ version: '0.1.49',
58
+ date: '2026-08-19',
59
+ sections: [
60
+ {
61
+ title: { zh: '企业微信智能机器人', en: 'WeCom AI bots' },
62
+ items: [
63
+ {
64
+ zh: '设置页按房间绑定企微智能机器人(BotID + Secret)。同事在企微跟哪个 bot 说话,就进网页左侧那个房间。密钥只存本机,不进 compose。',
65
+ en: 'Bind a WeCom AI bot (BotID + Secret) per Harness room. Talk to that bot in WeCom and the thread lands in that sidebar folder. Secrets stay on disk, not in compose.',
66
+ },
67
+ ],
68
+ },
69
+ ],
70
+ },
23
71
  {
24
72
  version: '0.1.48',
25
73
  date: '2026-08-19',
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Contract Account connections → two ops rooms + the settings 看板.
3
+ * Phase-1 ecommerce is Alibaba.com only (Amazon waits for an account). Media stays in 发布.
4
+ * Secrets never live here.
5
+ */
6
+
7
+ export type ChannelKind = 'media' | 'ecommerce'
8
+ export type ChannelPhase = '1' | 'later'
9
+
10
+ export interface ChannelSpec {
11
+ id: string
12
+ nameZh: string
13
+ nameEn: string
14
+ kind: ChannelKind
15
+ room: '发布' | '电商'
16
+ phase: ChannelPhase
17
+ }
18
+
19
+ export const MEDIA_ROOM = '发布'
20
+ export const ECOMMERCE_ROOM = '电商'
21
+
22
+ export const CHANNEL_BOARD: readonly ChannelSpec[] = [
23
+ {
24
+ id: 'media_via_publish',
25
+ nameZh: '内容 / 社媒分发',
26
+ nameEn: 'Content / social',
27
+ kind: 'media',
28
+ room: MEDIA_ROOM,
29
+ phase: '1',
30
+ },
31
+ {
32
+ id: 'alibaba_icbu',
33
+ nameZh: '阿里巴巴国际站',
34
+ nameEn: 'Alibaba.com',
35
+ kind: 'ecommerce',
36
+ room: ECOMMERCE_ROOM,
37
+ phase: '1',
38
+ },
39
+ {
40
+ id: 'amazon',
41
+ nameZh: 'Amazon',
42
+ nameEn: 'Amazon',
43
+ kind: 'ecommerce',
44
+ room: ECOMMERCE_ROOM,
45
+ phase: 'later',
46
+ },
47
+ ]
48
+
49
+ export function channelsFor(kind: ChannelKind): ChannelSpec[] {
50
+ return CHANNEL_BOARD.filter((row) => row.kind === kind)
51
+ }
52
+
53
+ export function wecomBound(room: string, bots: ReadonlyArray<{ room: string }>): boolean {
54
+ return bots.some((bot) => bot.room === room)
55
+ }
package/src/client/api.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  type RezUpdateApplyResult,
13
13
  type RezUpdateStatus,
14
14
  type RezWeixinStatus,
15
+ type RezWecomChannelsResponse,
15
16
  } from '../protocol.ts'
16
17
 
17
18
  export class RezApiError extends Error {
@@ -100,6 +101,20 @@ export class RezApi {
100
101
  return readJson<RezWeixinStatus>(response)
101
102
  }
102
103
 
104
+ async wecomChannels(): Promise<RezWecomChannelsResponse> {
105
+ const response = await fetch(REZ_API.wecomChannels)
106
+ return readJson<RezWecomChannelsResponse>(response)
107
+ }
108
+
109
+ async saveWecomChannels(bots: Array<{ room: string; botId: string; secret?: string }>): Promise<RezWecomChannelsResponse> {
110
+ const response = await fetch(REZ_API.wecomChannels, {
111
+ method: 'PUT',
112
+ headers: { 'content-type': 'application/json' },
113
+ body: JSON.stringify({ bots }),
114
+ })
115
+ return readJson<RezWecomChannelsResponse>(response)
116
+ }
117
+
103
118
  async updateStatus(): Promise<RezUpdateStatus> {
104
119
  const response = await fetch(REZ_API.update)
105
120
  return readJson<RezUpdateStatus>(response)