@rezti/dsh-rez-suite 0.1.55 → 0.1.57

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/src/boss/seed.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
2
2
  import { dirname, join, relative } from 'node:path'
3
3
  import { fileURLToPath } from 'node:url'
4
+ import { writeWecomCliShims } from '../wecom-cli.ts'
4
5
 
5
6
  export const BOSS_ROOMS = [
6
7
  { id: 'money', folder: '搞钱' },
@@ -165,6 +166,7 @@ function seedRoom(templatesRoot: string, destRoot: string, room: RoomSpec): Seed
165
166
  }
166
167
 
167
168
  seedSharedSkills(dest, written)
169
+ writeWecomCliShims(dest, written)
168
170
 
169
171
  return {
170
172
  id: room.id,
package/src/changelog.ts CHANGED
@@ -20,6 +20,45 @@ export interface ChangelogRelease {
20
20
  }
21
21
 
22
22
  export const SUITE_CHANGELOG: readonly ChangelogRelease[] = [
23
+ {
24
+ version: '0.1.57',
25
+ date: '2026-08-21',
26
+ sections: [
27
+ {
28
+ title: { zh: 'Windows 升级与企微 CLI', en: 'Windows upgrade + WeCom CLI' },
29
+ items: [
30
+ {
31
+ zh: '启动脚本每次装 npm 上已满 24 小时的最新 suite,不再钉 0.1.48。左下角「升级」仍可当天装。Windows 升级用 PowerShell 重启。办公 wecom-cli 用 node wecom.js + 房间垫片;禁止 npm install -g。',
32
+ en: 'Launchers install the newest suite published on npm for >=24h; no 0.1.48 pin. Sidebar Upgrade can still install a same-day build. Windows Upgrade respawns via PowerShell. Office wecom-cli runs as node wecom.js; never npm install -g.',
33
+ },
34
+ ],
35
+ },
36
+ {
37
+ title: { zh: '对话贴图不再整轮失败', en: 'GUI paste vision' },
38
+ items: [
39
+ {
40
+ zh: '对话里直接贴图不再被 DeepSeek 适配器 UNSUPPORTED_CONTENT 整轮打断:先走 Kimi/Qwen 转成文字再交给当前网页模型。本机路径仍可用 mcp__vision__view_image。不要换网页模型,不要 plugin add github。',
41
+ en: 'Pasted GUI images are transcribed before the DeepSeek adapter, so UNSUPPORTED_CONTENT no longer aborts the turn. Local paths can still use mcp__vision__view_image. Do not switch the chat model or add GitHub vision plugins.',
42
+ },
43
+ ],
44
+ },
45
+ ],
46
+ },
47
+ {
48
+ version: '0.1.56',
49
+ date: '2026-08-21',
50
+ sections: [
51
+ {
52
+ title: { zh: 'PDF 抽字、看图旁路', en: 'PDF extract + vision sidecar' },
53
+ items: [
54
+ {
55
+ zh: '读 PDF 用 Nextcloud MCP parse_document。回形针入库后立刻把抽出的文字交回本轮。看图走 mcp__vision__view_image(默认 kimi-k2.7-code,可切 Qwen-VL),网页对话模型不要改。DeepSeek 看不见像素。扫描件本期不开 OCR。不改员工机安装包。',
56
+ en: 'Read PDFs via Nextcloud MCP parse_document. After a paperclip Inbox write, parse immediately. Look at images with mcp__vision__view_image (kimi-k2.7-code by default, Qwen-VL switchable); do not change the chat model. DeepSeek is text-only. No OCR for scans this round. Do not bump the installer.',
57
+ },
58
+ ],
59
+ },
60
+ ],
61
+ },
23
62
  {
24
63
  version: '0.1.55',
25
64
  date: '2026-08-21',
@@ -6,7 +6,7 @@ export const zh = {
6
6
  'entry.label': 'Rez',
7
7
  'entry.tooltip': 'Rez Suite 配置与审计面板',
8
8
  'settings.title': 'ReZ-TI',
9
- 'settings.description': '公司 MCP(Odoo / Nextcloud / HA / TAPD / GSC)和企业微信智能机器人。个人微信在「微信」页签扫码。',
9
+ 'settings.description': '公司 MCP(Odoo / Nextcloud / HA / TAPD / GSC / 看图旁路)和企业微信智能机器人。个人微信在「微信」页签扫码。',
10
10
  'panel.title': 'Rez Suite',
11
11
  'tab.config': '配置',
12
12
  'tab.board': '看板',
@@ -30,7 +30,7 @@ export const zh = {
30
30
  'config.env': '环境变量 (JSON)',
31
31
  'config.headers': 'Headers (JSON)',
32
32
  'config.jsonInvalid': 'JSON 格式错误:{error}',
33
- 'config.mcpHint': '公司 MCP 在这里配。密钥写入 ~/.dsh/.credentials.yaml(REZ_*)。新填的密钥会立刻拉起连接;改 URL / 项目 / 站点后如果已经连上,需要重启 dsh web',
33
+ 'config.mcpHint': '公司 MCP 在这里配。密钥写入 ~/.dsh/.credentials.yaml(REZ_*)。新填的密钥会立刻拉起连接;改 URL / 项目 / 站点后如果已经连上,需要重启 dsh web。看图旁路配好后,网页对话模型不用改。',
34
34
  'config.secretConfigured': '已配置(留空则保持)',
35
35
  'config.secretMissing': '未配置',
36
36
  'config.odoo': 'Odoo',
@@ -69,6 +69,14 @@ export const zh = {
69
69
  'config.esphome': 'ESPHome 实验室',
70
70
  'config.esphome.url': 'MCP URL',
71
71
  'config.esphome.token': '实验室 Token',
72
+ 'config.vision': '看图旁路',
73
+ 'config.vision.vendor': '视觉厂家',
74
+ 'config.vision.vendor.kimi': 'Kimi(月之暗面)',
75
+ 'config.vision.vendor.qwen': '通义 Qwen-VL',
76
+ 'config.vision.model': '模型(可空)',
77
+ 'config.vision.modelHint': '空则默认 kimi-k2.7-code;更快可填 kimi-k2.7-code-highspeed',
78
+ 'config.vision.token': 'API Key',
79
+ 'config.vision.hint': '网页对话模型保持 DeepSeek。对话框直接贴图或回形针图片会自动转成文字再交给 DeepSeek;本机路径也可调 mcp__vision__view_image。图会发到所选厂家。Kimi 填 Moonshot Key;Qwen 填 DashScope Key。改厂家后需重启 dsh web。',
72
80
  'config.fs': '本地文件 (fs-mcp-server)',
73
81
  'config.fs.root': '根目录',
74
82
  'config.sqlite': 'SQLite (sqlite-mcp-server)',
@@ -176,7 +184,7 @@ export const en: Record<RezKey, string> = {
176
184
  'entry.label': 'Rez',
177
185
  'entry.tooltip': 'Rez Suite configuration and audit panel',
178
186
  'settings.title': 'ReZ-TI',
179
- 'settings.description': 'Company MCP (Odoo / Nextcloud / HA / TAPD / GSC) and WeCom AI bots. Personal WeChat is the WeChat tab.',
187
+ 'settings.description': 'Company MCP (Odoo / Nextcloud / HA / TAPD / GSC / vision sidecar) and WeCom AI bots. Personal WeChat is the WeChat tab.',
180
188
  'panel.title': 'Rez Suite',
181
189
  'tab.config': 'Config',
182
190
  'tab.board': 'Board',
@@ -199,7 +207,7 @@ export const en: Record<RezKey, string> = {
199
207
  'config.env': 'Env vars (JSON)',
200
208
  'config.headers': 'Headers (JSON)',
201
209
  'config.jsonInvalid': 'Invalid JSON: {error}',
202
- 'config.mcpHint': 'Company MCP lives here. Secrets go to ~/.dsh/.credentials.yaml (REZ_*). A newly saved secret starts the connection immediately; URL / workspace / site changes after a live connection need a dsh web restart.',
210
+ 'config.mcpHint': 'Company MCP lives here. Secrets go to ~/.dsh/.credentials.yaml (REZ_*). A newly saved secret starts the connection immediately; URL / workspace / site changes after a live connection need a dsh web restart. After the vision sidecar is configured, leave the chat model as-is.',
203
211
  'config.secretConfigured': 'Configured (leave blank to keep)',
204
212
  'config.secretMissing': 'Not configured',
205
213
  'config.odoo': 'Odoo',
@@ -238,6 +246,14 @@ export const en: Record<RezKey, string> = {
238
246
  'config.esphome': 'ESPHome lab',
239
247
  'config.esphome.url': 'MCP URL',
240
248
  'config.esphome.token': 'Lab token',
249
+ 'config.vision': 'Vision sidecar',
250
+ 'config.vision.vendor': 'Vision vendor',
251
+ 'config.vision.vendor.kimi': 'Kimi (Moonshot)',
252
+ 'config.vision.vendor.qwen': 'Qwen-VL',
253
+ 'config.vision.model': 'Model (optional)',
254
+ 'config.vision.modelHint': 'Blank defaults to kimi-k2.7-code; faster option is kimi-k2.7-code-highspeed',
255
+ 'config.vision.token': 'API key',
256
+ 'config.vision.hint': 'Keep DeepSeek as the chat model. Pasted or paperclip images are transcribed automatically before DeepSeek; mcp__vision__view_image still works for local paths. Images leave the machine to that vendor. Kimi uses a Moonshot key; Qwen uses a DashScope key. Restart dsh web after switching vendor.',
241
257
  'config.fs': 'Local files (fs-mcp-server)',
242
258
  'config.fs.root': 'Root directory',
243
259
  'config.sqlite': 'SQLite (sqlite-mcp-server)',
@@ -27,9 +27,10 @@ type SecretDrafts = {
27
27
  qcc: string
28
28
  tianyancha: string
29
29
  esphome: string
30
+ vision: string
30
31
  }
31
32
 
32
- const EMPTY_SECRETS: SecretDrafts = { odoo: '', nextcloud: '', wechat: '', homeassistant: '', tapd: '', gsc: '', qcc: '', tianyancha: '', esphome: '' }
33
+ const EMPTY_SECRETS: SecretDrafts = { odoo: '', nextcloud: '', wechat: '', homeassistant: '', tapd: '', gsc: '', qcc: '', tianyancha: '', esphome: '', vision: '' }
33
34
 
34
35
  const WECOM_SECRET_MASK = '********'
35
36
 
@@ -301,6 +302,12 @@ export function ConfigTab({ api }: { api: RezApi }) {
301
302
  qcc: { ...config.connections.qcc, secret: secrets.qcc || undefined },
302
303
  tianyancha: { ...config.connections.tianyancha, secret: secrets.tianyancha || undefined },
303
304
  esphome: { ...config.connections.esphome, secret: secrets.esphome || undefined },
305
+ vision: {
306
+ ...config.connections.vision,
307
+ secret: secrets.vision || undefined,
308
+ vendor: config.connections.vision.vendor,
309
+ model: config.connections.vision.model,
310
+ },
304
311
  },
305
312
  }
306
313
  const saved = await api.saveConfig(payload)
@@ -359,6 +366,37 @@ export function ConfigTab({ api }: { api: RezApi }) {
359
366
  </Field>
360
367
  </Section>
361
368
 
369
+ <McpSection
370
+ title={t('config.vision')}
371
+ row={config.connections.vision}
372
+ secret={secrets.vision}
373
+ onEnabled={enabled => { patchConnection('vision', { enabled }) }}
374
+ extra={(
375
+ <>
376
+ <Field label={t('config.vision.vendor')}>
377
+ <select
378
+ className={css.select}
379
+ value={config.connections.vision.vendor ?? 'kimi'}
380
+ onChange={event => { patchConnection('vision', { vendor: event.target.value as 'kimi' | 'qwen' }) }}
381
+ >
382
+ <option value="kimi">{t('config.vision.vendor.kimi')}</option>
383
+ <option value="qwen">{t('config.vision.vendor.qwen')}</option>
384
+ </select>
385
+ </Field>
386
+ <Field label={t('config.vision.model')}>
387
+ <input
388
+ className={css.input}
389
+ value={config.connections.vision.model ?? ''}
390
+ placeholder={t('config.vision.modelHint')}
391
+ onChange={event => { patchConnection('vision', { model: event.target.value }) }}
392
+ />
393
+ </Field>
394
+ <p className={css.message}>{t('config.vision.hint')}</p>
395
+ </>
396
+ )}
397
+ onSecret={value => { setSecrets(prev => ({ ...prev, vision: value })) }}
398
+ secretLabel={t('config.vision.token')}
399
+ />
362
400
  <McpSection
363
401
  title={t('config.odoo')}
364
402
  row={config.connections.odoo}
package/src/index.ts CHANGED
@@ -96,6 +96,11 @@ export const Config: z<RezConfig> = z.object({
96
96
  enabled: z.boolean().default(true),
97
97
  url: z.string().default(REZ_DEFAULT_CONNECTIONS.esphome?.url ?? 'http://127.0.0.1:8100/mcp'),
98
98
  }).default(REZ_DEFAULT_CONNECTIONS.esphome ?? { enabled: true, url: 'http://127.0.0.1:8100/mcp' }),
99
+ vision: z.object({
100
+ enabled: z.boolean().default(true),
101
+ vendor: z.union(['kimi', 'qwen'] as const).default('kimi'),
102
+ model: z.string().default(''),
103
+ }).default(REZ_DEFAULT_CONNECTIONS.vision ?? { enabled: true, vendor: 'kimi', model: '' }),
99
104
  }).default(REZ_DEFAULT_CONNECTIONS),
100
105
  billing: z.object({
101
106
  inputCostPer1k: z.number().default(0.001),
@@ -107,16 +112,18 @@ export const Config: z<RezConfig> = z.object({
107
112
  const SECTION_ORDER = 150
108
113
 
109
114
  export const REZ_GUIDANCE_CORE = [
110
- '本机已安装 ReZ-TI 拆分包:工作身份由 dsh-rez-sso 提供,MCP 由官方 dsh-mcp-client 接入 Odoo / Nextcloud / Home Assistant / TAPD / GSC',
111
- '工具名 mcp__odoo__*、mcp__nextcloud__*、mcp__homeassistant__*、mcp__tapd__*、mcp__gsc__*。',
112
- '企业微信办公能力不是 MCP:走官方 wecom-cli >=1.1(通讯录/在线文档/在线表格/智能表格/智能文档/待办/日程/会议/邮件/微盘/消息)。本企业已开通这些品类。看 rez_status 的 wecomCli.modules,按房间 wecomcli-* 技能执行。不要因为没有 mcp__wechat__* 就说没有企微能力,也不要说「超过 10 人只能用文档」。CLI 报某模块未授权才说该能力未开通。设置页 BotID+Secret 只是房间消息通道。旧群 webhook 兼容才会出现 mcp__wechat__*。',
113
- '密钥:REZ_ODOO_TOKEN、REZ_NEXTCLOUD_PASSWORD、REZ_WECHAT_WEBHOOK、REZ_HA_TOKEN、REZ_TAPD_TOKEN、REZ_GSC_KEY。',
115
+ '本机已安装 ReZ-TI 拆分包:工作身份由 dsh-rez-sso 提供,MCP 由官方 dsh-mcp-client 接入 Odoo / Nextcloud / Home Assistant / TAPD / GSC / 看图旁路。',
116
+ '工具名 mcp__odoo__*、mcp__nextcloud__*、mcp__homeassistant__*、mcp__tapd__*、mcp__gsc__*、mcp__vision__*。',
117
+ '企业微信有两层,不要混。① 房间消息通道 = 设置页 BotID+Secret(同事在企微跟那个智能机器人说话,进网页左侧对应房间)。② 办公能力不是 MCP:走官方 wecom-cli >=1.1(通讯录/在线文档/在线表格/智能表格/智能文档/待办/日程/会议/邮件/微盘/消息)。本企业已开通这些品类。看 rez_status 的 wecomCli.bin / modules,优先 node $REZ_WECOM_CLI 或房间 .agents/bin/wecom-cli(Windows 用 .cmd)。禁止 npm install -g(Windows 沙箱会 EPERM 写 npm-cache)。不要因为没有 mcp__wechat__* 就说没有企微能力,也不要说「超过 10 人只能用文档」。CLI 报某模块未授权才说该能力未开通。旧群 webhook 兼容才会出现 mcp__wechat__*。',
118
+ '密钥:REZ_ODOO_TOKEN、REZ_NEXTCLOUD_PASSWORD、REZ_WECHAT_WEBHOOK、REZ_HA_TOKEN、REZ_TAPD_TOKEN、REZ_GSC_KEY、REZ_VISION_KEY。',
114
119
  '个人微信(QClaw/ClawBot):打开 设置 → 插件 → ReZ-TI → 微信 扫码,然后直接在微信里说话。消息进入网页左侧「微信」文件夹并延续同一会话,模型跟网页默认。网页归档了再从微信说话会自动恢复到侧栏。发「重启对话」会新开一条可见会话并把旧会话归档。网页端必须开着。不必再跑命令、也不要调用微信 MCP 工具。',
115
120
  '意图用 rez_set_intent(erp/files/chat/home/auto)。审计用 rez_audit。',
116
121
  '涉及资金、对外发送、设备控制、生产写入时先征得用户批准。',
117
122
  'Match the user language: Chinese or English. Do not force 中文 when they write English.',
118
123
  '制度、手册、出差/报销标准、素材路径:打开 Nextcloud Documents/Standards/INDEX.yaml,按 code 取当前 effective_from 最新且未被 replace 的条目,再按相对 uri 打开。引用带路径+生效日。找不到就说「标准文件未归档」,不要用 MEMORY 旧数字。素材文件夹用首次创建标题,不用会变的 Odoo/销售名。',
119
- '同事交说明书/规格书:用输入栏回形针上传原 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 未归档」。这不是一件发布;不要拷下游、不要上渠道。',
124
+ ' PDF(Kimi/Cursor 同款:先抽字再问):mcp__nextcloud__nc_webdav_read_file,parse_document auto;表格/标题再试 markdown。不要把 PDF 当图或 base64 塞进对话,内置 read 会当二进制拒掉。parse_status 不是 parsed 或正文空,就说没有文字层(扫描件本期不开 OCR),不要假装读过。不要 plugin add github 看图/PDF 插件。跨很多本说明书检索走 Dify 发布知识库(1154488930001001237),不要另建向量库。',
125
+ '看产品图/标签/截图:网页对话模型不要改。DeepSeek 官方聊天是纯文本,看不见像素;对话里直接贴图或回形针图片会先走看图旁路转成文字,再交给 DeepSeek,不要因为 UNSUPPORTED_CONTENT 就换网页模型。本机文件路径也可调 mcp__vision__view_image。旁路默认 Kimi Code(kimi-k2.7-code),设置里可切通义 Qwen-VL。没有看图旁路就说本机未配 REZ_VISION_KEY(设置 → 插件 → ReZ-TI),不要切网页模型假装能看见。官方对话只收图。Comfy MCP 只修图,不拿来认字。',
126
+ '同事交说明书/规格书:用输入栏回形针上传原 PDF(不要拖进对话——官方只收图,PDF 不要当图塞;也不要用微信发文件)。不要直接写 Marketing/Publish。先找到该产品对应 TAPD 需求(Wiki 写死的说明书/规格书 TAPD ID;没有则按首次创建标题搜「产品使用说明书」或「技术手册」),把 PDF 放到 Marketing/Inbox/{分类}/{首次创建标题}/manual/ 或 datasheet/(zh.pdf 或 en.pdf),立刻用 mcp__nextcloud__nc_webdav_read_file(parse_document=auto)抽出文字交回本轮,不要只说已入库。评论该需求、状态改测试中(status_3)、处理人改凯文智能家居。规划中先转到开发中再测试中;已实现要先退回开发中。凯文说审批了哪份(产品、说明书或规格书、中英)之后,再把 Inbox 文件写入 Marketing/Publish 对应路径,TAPD 改已实现。拒绝则改已拒绝、不写 Publish。Wiki 缺 NC RAW、NC Publish、Git products/{slug}/models 与 motion 就说「Wiki 未归档」。这不是一件发布;不要拷下游、不要上渠道。',
120
127
  '同事交拍摄原片:对话里交图即可。先调官方 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/ 根目录或产品根目录样板不强制搬家。',
121
128
  '同事交品牌规范或公司 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 正文。',
122
129
  ].join('')
@@ -10,7 +10,7 @@ import { lastMcpStartError, REZ_CREDENTIAL_REFS, secretConfigured, type RezSyste
10
10
  import type { RezConfig, RezServerStatus, RezTestResult } from './protocol.ts'
11
11
  import { isLabRoom, isLegalAssistantRoom, isQccToolName, QCC_STATUS_ROOM_OFF } from './rooms.ts'
12
12
 
13
- export const COMPOSED_SERVERS = ['odoo', 'nextcloud', 'wechat', 'homeassistant', 'tapd', 'gsc', 'qcc', 'tianyancha', 'esphome'] as const
13
+ export const COMPOSED_SERVERS = ['odoo', 'nextcloud', 'wechat', 'homeassistant', 'tapd', 'gsc', 'qcc', 'tianyancha', 'esphome', 'vision'] as const
14
14
 
15
15
  export type ComposedServer = (typeof COMPOSED_SERVERS)[number]
16
16
 
package/src/presets.ts CHANGED
@@ -47,6 +47,7 @@ const RULES: Record<Exclude<RezRoleId, 'all' | 'boss'>, RoleRule[]> = {
47
47
  { server: 'nextcloud', pattern: '*list*' },
48
48
  { server: 'nextcloud', pattern: '*download*' },
49
49
  { server: 'nextcloud', pattern: '*read*' },
50
+ { server: 'vision', pattern: '*' },
50
51
  ],
51
52
  sales: [
52
53
  // Odoo CRM / sales objects.
@@ -61,6 +62,7 @@ const RULES: Record<Exclude<RezRoleId, 'all' | 'boss'>, RoleRule[]> = {
61
62
  { server: 'nextcloud', pattern: '*write*' },
62
63
  { server: 'nextcloud', pattern: '*mkdir*' },
63
64
  { server: 'nextcloud', pattern: '*move*' },
65
+ { server: 'vision', pattern: '*' },
64
66
  // WeCom one-to-one / group sends.
65
67
  { server: 'wecom', pattern: '*send*' },
66
68
  { server: 'wecom', pattern: '*message*' },
@@ -100,6 +102,7 @@ const RULES: Record<Exclude<RezRoleId, 'all' | 'boss'>, RoleRule[]> = {
100
102
  { server: 'nextcloud', pattern: '*upload*' },
101
103
  { server: 'nextcloud', pattern: '*write*' },
102
104
  { server: 'nextcloud', pattern: '*mkdir*' },
105
+ { server: 'vision', pattern: '*' },
103
106
  // WeCom broadcasts.
104
107
  { server: 'wecom', pattern: '*broadcast*' },
105
108
  { server: 'wecom', pattern: '*send*' },
package/src/protocol.ts CHANGED
@@ -51,6 +51,7 @@ export interface RezConnections {
51
51
  qcc: { enabled: boolean; url: string }
52
52
  tianyancha: { enabled: boolean; url: string }
53
53
  esphome: { enabled: boolean; url: string }
54
+ vision: { enabled: boolean; vendor: 'kimi' | 'qwen'; model: string }
54
55
  }
55
56
 
56
57
  export interface RezPublicConnection {
@@ -60,6 +61,8 @@ export interface RezPublicConnection {
60
61
  workspaceId?: string
61
62
  nickName?: string
62
63
  siteUrl?: string
64
+ vendor?: 'kimi' | 'qwen'
65
+ model?: string
63
66
  secretConfigured: boolean
64
67
  secretRef: string
65
68
  }
@@ -96,6 +99,7 @@ export interface RezPublicConfig {
96
99
  qcc: RezPublicConnection
97
100
  tianyancha: RezPublicConnection
98
101
  esphome: RezPublicConnection
102
+ vision: RezPublicConnection
99
103
  }
100
104
  billing: { inputCostPer1k: number; outputCostPer1k: number; monthlyBudget: number }
101
105
  }
@@ -134,6 +138,8 @@ export interface RezWecomCliStatus {
134
138
  authorized: boolean
135
139
  modules: RezWecomCliModule[]
136
140
  hint: string
141
+ /** Absolute wecom.js path. Agent should run: node "<bin>" … */
142
+ bin?: string
137
143
  }
138
144
 
139
145
  /** Complete /api/dsh-rez-suite/status response. */
@@ -55,10 +55,24 @@ export function shellQuote(value: string): string {
55
55
  return `'${value.replaceAll("'", `'\\''`)}'`
56
56
  }
57
57
 
58
+ export function powershellQuote(value: string): string {
59
+ return `'${value.replaceAll("'", "''")}'`
60
+ }
61
+
58
62
  export function relaunchScript(argv: string[], parentPid: number): string {
59
63
  return `while kill -0 ${parentPid} 2>/dev/null; do sleep 0.15; done; sleep 0.25; exec ${argv.map(shellQuote).join(' ')}`
60
64
  }
61
65
 
66
+ /** Windows staff EXE has no /bin/bash. Wait for the old node, then respawn argv. */
67
+ export function windowsRelaunchScript(argv: string[], parentPid: number): string {
68
+ const cmd = argv.map(powershellQuote).join(' ')
69
+ return [
70
+ `while (Get-Process -Id ${parentPid} -ErrorAction SilentlyContinue) { Start-Sleep -Milliseconds 200 }`,
71
+ 'Start-Sleep -Milliseconds 300',
72
+ `& ${cmd}`,
73
+ ].join('; ')
74
+ }
75
+
62
76
  export async function fetchLatestVersion(fetchImpl: typeof fetch = fetch): Promise<string> {
63
77
  const response = await fetchImpl(NPM_LATEST, { headers: { accept: 'application/json' } })
64
78
  if (!response.ok) throw new Error(`npm registry HTTP ${response.status}`)
@@ -89,12 +103,20 @@ export function runPnpmAdd(cwd: string, spec: string, env: NodeJS.ProcessEnv = p
89
103
  }
90
104
 
91
105
  export function spawnRelaunch(argv: string[], parentPid: number, cwd: string, env: NodeJS.ProcessEnv = process.env): void {
92
- const child = spawn('/bin/bash', ['-c', relaunchScript(argv, parentPid)], {
93
- cwd,
94
- env,
95
- detached: true,
96
- stdio: 'ignore',
97
- })
106
+ const child = process.platform === 'win32'
107
+ ? spawn('powershell.exe', ['-NoProfile', '-WindowStyle', 'Hidden', '-Command', windowsRelaunchScript(argv, parentPid)], {
108
+ cwd,
109
+ env,
110
+ detached: true,
111
+ stdio: 'ignore',
112
+ windowsHide: true,
113
+ })
114
+ : spawn('/bin/bash', ['-c', relaunchScript(argv, parentPid)], {
115
+ cwd,
116
+ env,
117
+ detached: true,
118
+ stdio: 'ignore',
119
+ })
98
120
  child.unref()
99
121
  }
100
122
 
package/src/store.ts CHANGED
@@ -143,6 +143,13 @@ export function publicConfig(config: RezConfig): RezPublicConfig {
143
143
  secretConfigured: secretConfigured('esphome'),
144
144
  secretRef: REZ_CREDENTIAL_REFS.esphome,
145
145
  },
146
+ vision: {
147
+ enabled: connections.vision.enabled,
148
+ vendor: connections.vision.vendor,
149
+ model: connections.vision.model,
150
+ secretConfigured: secretConfigured('vision'),
151
+ secretRef: REZ_CREDENTIAL_REFS.vision,
152
+ },
146
153
  },
147
154
  billing: { ...config.billing },
148
155
  }
package/src/tools.ts CHANGED
@@ -34,9 +34,11 @@ function renderTest(results: RezTestResult[]): string {
34
34
  function renderWecomCli(cli: RezWecomCliStatus | undefined): string {
35
35
  const row = cli ?? { installed: false, authorized: false, modules: [], hint: '' }
36
36
  const modules = (row.modules ?? []).map((item) => `${item.label}(${item.id})`).join('、')
37
+ const bin = row.bin ? `bin: node "${row.bin}"` : 'bin: (missing — click Upgrade, do not npm install -g)'
37
38
  return [
38
39
  'wecom-cli(企微办公能力,不是 MCP): installed=' + (row.installed ? 'yes' : 'no')
39
40
  + ' authorized=' + (row.authorized ? 'yes' : 'no'),
41
+ bin,
40
42
  modules.length > 0 ? 'modules: ' + modules : 'modules: (官方目录见 wecomcli-* 技能)',
41
43
  row.hint,
42
44
  ].join('\n')
package/src/wecom-cli.ts CHANGED
@@ -6,10 +6,16 @@
6
6
  *
7
7
  * Suite depends on @wecom/cli@1.1.0 so the profile gets a modern binary.
8
8
  * Prefer that over a global Homebrew 0.1.x that lacks `mail` / `auth`.
9
+ *
10
+ * Windows: the package entry is wecom.js (not wecom-cli.exe). spawnSync of a
11
+ * .js path is ENOENT; agent shells looking for wecom-cli also miss it. Always
12
+ * invoke `node wecom.js`, put node_modules/.bin on PATH, and seed a workspace
13
+ * shim. Never npm install -g: workspace-write sandbox cannot write npm-cache.
9
14
  */
10
15
 
11
16
  import { spawnSync } from 'node:child_process'
12
- import { existsSync } from 'node:fs'
17
+ import { chmodSync, existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs'
18
+ import { homedir } from 'node:os'
13
19
  import { dirname, join } from 'node:path'
14
20
  import { createRequire } from 'node:module'
15
21
  import { fileURLToPath } from 'node:url'
@@ -24,7 +30,7 @@ export interface WecomCliStatus {
24
30
  authorized: boolean
25
31
  modules: WecomCliModule[]
26
32
  hint: string
27
- /** Absolute path preferred for agent shell (suite-bundled). */
33
+ /** Absolute path of bundled wecom.js; agent should run: node "<bin>" … */
28
34
  bin?: string
29
35
  }
30
36
 
@@ -37,6 +43,11 @@ export interface WecomCliRunResult {
37
43
 
38
44
  export type WecomCliRunner = (argv: string[]) => WecomCliRunResult
39
45
 
46
+ export interface WecomCliSpawn {
47
+ command: string
48
+ prefixArgs: string[]
49
+ }
50
+
40
51
  /** Official @wecom/cli >=1.1 catalog. Do not invent extra HTTP APIs. */
41
52
  export const WECOM_CLI_CATALOG: readonly WecomCliModule[] = [
42
53
  { id: 'contact', label: '通讯录' },
@@ -58,27 +69,28 @@ const MODULE_LABELS = WECOM_CLI_CATALOG.map((row) => row.label).join('/')
58
69
 
59
70
  const HINT_MISSING = [
60
71
  `未找到可用的 wecom-cli >=1.1(企微办公:${MODULE_LABELS})。`,
61
- 'suite 已依赖 @wecom/cli@1.1.0:请重装 @rezti/dsh-rez-suite,或确认 profile 的 node_modules 里有 @wecom/cli。',
72
+ 'suite 已依赖 @wecom/cli@1.1.0:请点网页左下角「升级」,或确认 profile 的 node_modules 里有 @wecom/cli。',
73
+ 'Windows 用 node %REZ_WECOM_CLI% 或房间 .agents/bin/wecom-cli.cmd。禁止 npm install -g(沙箱 workspace-write 会 EPERM 写 npm-cache)。',
62
74
  '不要用 Homebrew 全局 0.1.x(没有 mail/auth)。不要因为没有 wechat MCP 就说没有企微能力。',
63
75
  ].join('')
64
76
 
65
77
  const HINT_OUTDATED = [
66
78
  'PATH 上的 wecom-cli 过旧(<1.1,没有 mail 等子命令)。',
67
- '请用 suite 自带的 @wecom/cli@1.1.0:重装 suite,或把 profile node_modules/.bin 放在 PATH 最前。',
68
- '不要 npm i -g 覆盖到旧 Homebrew 路径前面仍是 0.1.9。',
79
+ '请用 suite 自带的 @wecom/cli@1.1.0:点左下角升级,或把 profile node_modules/.bin 放在 PATH 最前。',
80
+ '禁止 npm i -g(Windows 沙箱会挡 npm-cache;也容易仍被旧 Homebrew 盖住)。',
69
81
  ].join('')
70
82
 
71
83
  const HINT_UNAUTH = [
72
- '已安装 wecom-cli >=1.1,尚未授权。跑 wecom-cli auth init(可扫码)。',
84
+ '已安装 wecom-cli >=1.1,尚未授权。跑 wecom-cli auth init(可扫码)。Windows:node %REZ_WECOM_CLI% auth init。',
73
85
  `授权后可用:${MODULE_LABELS}。本企业已开通这些品类。`,
74
86
  'CLI 报某模块未授权才说该能力未开通;不要说「没有企微 MCP」,也不要说 10 人限制。',
75
87
  ].join('')
76
88
 
77
89
  const HINT_OK = [
78
90
  `企微办公能力走 wecom-cli >=1.1(${MODULE_LABELS})。本企业已开通。suite 已带 @wecom/cli。`,
79
- '按房间里官方 wecomcli-* 技能执行,命令以 wecom-cli <模块> --help 为准。',
80
- '不要用旧帮助中心「超过 10 人只能用文档」的说法。CLI 报未授权才说该模块未开通。',
81
- '设置页 BotID+Secret 只是房间消息通道。不要把没有 mcp__wechat__* 当成没有企微能力。',
91
+ '调用:node $REZ_WECOM_CLI 或房间 .agents/bin/wecom-cli(Windows .cmd)。禁止 npm install -g。',
92
+ '按房间里官方 wecomcli-* 技能执行。不要用旧帮助中心「超过 10 人只能用文档」的说法。CLI 报未授权才说该模块未开通。',
93
+ '设置页 BotID+Secret 只是房间消息通道,不是查邮件。不要把没有 mcp__wechat__* 当成没有企微能力。',
82
94
  ].join('')
83
95
 
84
96
  export function wecomCliCatalog(): WecomCliModule[] {
@@ -93,7 +105,7 @@ export function parseWecomCliHelpModules(helpText: string): WecomCliModule[] {
93
105
  })
94
106
  }
95
107
 
96
- /** Resolve suite-bundled @wecom/cli binary; undefined if not installed next to suite. */
108
+ /** Resolve suite-bundled @wecom/cli wecom.js; undefined if not installed next to suite. */
97
109
  export function resolveWecomCliBin(): string | undefined {
98
110
  const require = createRequire(import.meta.url)
99
111
  try {
@@ -103,7 +115,6 @@ export function resolveWecomCliBin(): string | undefined {
103
115
  } catch {
104
116
  // not hoisted next to this module
105
117
  }
106
- // Fallbacks when published layout differs
107
118
  const here = dirname(fileURLToPath(import.meta.url))
108
119
  for (const candidate of [
109
120
  join(here, '..', 'node_modules', '@wecom', 'cli', 'bin', 'wecom.js'),
@@ -115,27 +126,86 @@ export function resolveWecomCliBin(): string | undefined {
115
126
  return undefined
116
127
  }
117
128
 
129
+ /** node_modules/.bin (wecom-cli.cmd) plus the package bin dir (wecom.js). */
130
+ export function wecomCliPathDirs(jsPath: string): string[] {
131
+ const pkgBin = dirname(jsPath)
132
+ const nmBin = join(pkgBin, '..', '..', '..', '.bin')
133
+ return [nmBin, pkgBin]
134
+ }
135
+
136
+ export function workspaceWecomShimDirs(home = process.env.DSH_HOME ?? join(homedir(), '.dsh')): string[] {
137
+ const root = join(home, 'workspaces')
138
+ if (!existsSync(root)) return []
139
+ return readdirSync(root, { withFileTypes: true })
140
+ .filter((entry) => entry.isDirectory())
141
+ .map((entry) => join(root, entry.name, '.agents', 'bin'))
142
+ .filter((dir) => existsSync(dir))
143
+ }
144
+
145
+ export function prependPathDirs(dirs: string[], env: NodeJS.ProcessEnv = process.env): string {
146
+ const sep = process.platform === 'win32' ? ';' : ':'
147
+ const current = env.PATH ?? env.Path ?? ''
148
+ const parts = current.split(sep).filter(Boolean)
149
+ const head = dirs.filter((dir) => dir.length > 0 && !parts.includes(dir))
150
+ const joined = [...head, ...parts].join(sep)
151
+ env.PATH = joined
152
+ if (process.platform === 'win32') env.Path = joined
153
+ return joined
154
+ }
155
+
156
+ /** Windows cannot spawn wecom.js as argv0; always use the running Node. */
157
+ export function wecomCliSpawn(bundled: string | undefined, nodePath = process.execPath): WecomCliSpawn {
158
+ if (bundled !== undefined) return { command: nodePath, prefixArgs: [bundled] }
159
+ return { command: 'wecom-cli', prefixArgs: [] }
160
+ }
161
+
162
+ export function wecomCliShimScripts(opts: { nodePath: string; jsPath?: string }): { unix: string; cmd: string } {
163
+ if (opts.jsPath === undefined || opts.jsPath === '') {
164
+ const msg = 'wecom-cli is bundled in ReZ-TI suite. Click Upgrade. Do not npm install -g.'
165
+ return {
166
+ unix: `#!/bin/sh\necho "${msg}" >&2\nexit 1\n`,
167
+ cmd: `@echo off\r\necho ${msg}\r\nexit /b 1\r\n`,
168
+ }
169
+ }
170
+ const node = opts.nodePath.replaceAll('"', '\\"')
171
+ const js = opts.jsPath.replaceAll('"', '\\"')
172
+ return {
173
+ unix: `#!/bin/sh\nexec "${node}" "${js}" "$@"\n`,
174
+ cmd: `@echo off\r\n"${opts.nodePath}" "${opts.jsPath}" %*\r\n`,
175
+ }
176
+ }
177
+
178
+ export function writeWecomCliShims(dest: string, written: string[], jsPath = resolveWecomCliBin()): void {
179
+ const scripts = wecomCliShimScripts({ nodePath: process.execPath, jsPath })
180
+ const dirRel = join('.agents', 'bin')
181
+ mkdirSync(join(dest, dirRel), { recursive: true })
182
+ const unixRel = join(dirRel, 'wecom-cli')
183
+ const cmdRel = join(dirRel, 'wecom-cli.cmd')
184
+ writeFileSync(join(dest, unixRel), scripts.unix)
185
+ writeFileSync(join(dest, cmdRel), scripts.cmd)
186
+ try { chmodSync(join(dest, unixRel), 0o755) } catch { /* win */ }
187
+ written.push(unixRel, cmdRel)
188
+ }
189
+
118
190
  /** Put suite-bundled wecom-cli ahead of global Homebrew 0.1.x on PATH. */
119
191
  export function ensureWecomCliOnPath(): string | undefined {
120
192
  const bin = resolveWecomCliBin()
121
- if (bin === undefined) return undefined
122
- const dir = dirname(bin)
123
- const pathKey = process.platform === 'win32' ? 'Path' : 'PATH'
124
- const current = process.env[pathKey] ?? ''
125
- const parts = current.split(process.platform === 'win32' ? ';' : ':').filter(Boolean)
126
- if (parts[0] !== dir) {
127
- process.env[pathKey] = [dir, ...parts.filter((p) => p !== dir)].join(process.platform === 'win32' ? ';' : ':')
128
- }
193
+ if (bin !== undefined) process.env.REZ_WECOM_CLI = bin
194
+ prependPathDirs([
195
+ ...(bin === undefined ? [] : wecomCliPathDirs(bin)),
196
+ ...workspaceWecomShimDirs(),
197
+ ])
129
198
  return bin
130
199
  }
131
200
 
132
201
  export function defaultWecomCliRunner(argv: string[]): WecomCliRunResult {
133
202
  const bundled = resolveWecomCliBin()
134
- const cmd = bundled ?? 'wecom-cli'
135
- const result = spawnSync(cmd, argv, {
203
+ const spawn = wecomCliSpawn(bundled)
204
+ const result = spawnSync(spawn.command, [...spawn.prefixArgs, ...argv], {
136
205
  encoding: 'utf8',
137
206
  timeout: 4000,
138
207
  env: process.env,
208
+ windowsHide: true,
139
209
  })
140
210
  const error = result.error as NodeJS.ErrnoException | undefined
141
211
  return {
@@ -41,7 +41,7 @@
41
41
  - TAPD 只读阻塞和等老板拍板的;Odoo 只读退货/质量相关。
42
42
  - 新产品或改产品 Wiki:Wiki **必须**同时写死 Nextcloud RAW 路径、Nextcloud Publish 路径(说明书 / 规格书 / 打标 / 渠道),Git 3D 源路径 `products/{slug}/models` 与 `products/{slug}/motion`,以及说明书/规格书对应的 TAPD 需求 ID。缺任一项就说「Wiki 未归档」,不要猜路径。
43
43
  - 问某产品素材在哪:打开 Nextcloud `Documents/Standards/INDEX.yaml`,再按相对 uri 读路径规则;加上该产品 Wiki **首次创建标题**,不要用会变的 Odoo `name` / 销售标题。RAW 在 `Marketing/RAW/…`,待审在 `Marketing/Inbox/…`,给团队用的正式件在 `Marketing/Publish/…`。
44
- - 同事交说明书/规格书:让他们用输入栏**回形针**上传原 PDF(不要拖进对话,官方只收图;不要用微信发文件),并说清产品、说明书还是规格书、中英。**不要直接写 Publish。** 文件先放 `Marketing/Inbox/…`,对应 TAPD 需求改测试中、处理人凯文智能家居。你跟 Harness 说「审批了哪份」之后,再写入 `Marketing/Publish/…/manual/` 或 `datasheet/`。拒绝不写 Publish。这是入库门禁,不是一件发布。Wiki 未写死路径或说明书/规格书 TAPD ID 就说「Wiki 未归档」。
44
+ - 同事交说明书/规格书:让他们用输入栏**回形针**上传原 PDF(不要拖进对话,官方只收图,PDF 不要当图塞;不要用微信发文件),并说清产品、说明书还是规格书、中英。**不要直接写 Publish。** 文件先放 `Marketing/Inbox/…`,立刻用 `mcp__nextcloud__nc_webdav_read_file`(`parse_document=auto`)抽出文字交回本轮。对应 TAPD 需求改测试中、处理人凯文智能家居。你跟 Harness 说「审批了哪份」之后,再写入 `Marketing/Publish/…/manual/` 或 `datasheet/`。拒绝不写 Publish。这是入库门禁,不是一件发布。Wiki 未写死路径或说明书/规格书 TAPD ID 就说「Wiki 未归档」。看产品图/标签请调 `mcp__vision__view_image`(网页对话模型不要改;DeepSeek 看不见图)。
45
45
  - 同事交拍摄图:运营岗在「设计」房间完成拍原片、Comfy 只修一次、声明类型、Inbox 待审。**本室只做终审**:你跟 Harness 说「审批了哪份」之后,shot 与 clean **同时**写入 `Marketing/RAW/…/photo/shot/` 与 `photo/clean/`。拒绝不写 RAW。不要在产品室代运营修图,不要写 Publish、不要上渠道。缺拍摄图 TAPD 用 `1154488930001001218`,不算 Wiki 未归档。
46
46
  - 同事交品牌规范或公司 VI:运营岗在「设计」房间交 Inbox `Brand/guideline|vi/shot|clean/`。**本室只做终审**:你跟 Harness 说「审批了哪份」之后,shot 与 clean **同时**写入 `Marketing/RAW/Brand/guideline/` 或 `vi/` 的 `shot/` 与 `clean/`。拒绝不写 RAW。不要写 Publish。不要把规范抄进 Standards 正文。根上 `VI/`、`MKT/VI/` 不搬家。缺 TAPD 用 `1154488930001001220`。
47
47
  - 禁止在这里写固件、开辅线、做 HA Add-on、做微信通道。那些走仓库和员工机。
@@ -24,9 +24,9 @@ description: 企业微信办公能力索引。官方 wecom-cli v1.1+ 已面向
24
24
  | `wecomcli-message` | 消息 |
25
25
  | `wecomcli-media` | 媒体文件 |
26
26
 
27
- 1. 先看 `rez_status` 的 `wecomCli.modules`,或跑 `wecom-cli auth show`。
27
+ 1. 先看 `rez_status` 的 `wecomCli.bin` / `modules`,用 `node <bin> auth show`(Windows 同样)。不要 `npm install -g`。
28
28
  2. 先读 `wecomcli-shared`,再读对应 `wecomcli-*`。子命令以 `wecom-cli <模块> --help` 为准。
29
29
  3. CLI 返回未授权 / 无权限,才说**该模块未开通**。不要总结成「当前系统没有企业微信 MCP」,也不要说「超过 10 人没有通讯录」。
30
- 4. 设置页 **BotID + Secret** 是房间**消息通道**(同事在企微跟哪个 bot 说话进哪个 Harness 房间)。**继续用手填绑定,不要改成扫码。**
31
- 5. 办公 API:suite 已带 `@wecom/cli@1.1.0`。授权跑 `wecom-cli auth init`(可扫码)。若 PATH 上是 Homebrew `0.1.9`,改用 profile / `npx --no-install wecom-cli`。两套凭证不要混。
30
+ 4. 设置页 **BotID + Secret** 是房间**消息通道**(同事在企微跟哪个 bot 说话进哪个 Harness 房间)。**继续用手填绑定,不要改成扫码。** 查邮件/文档走 wecom-cli,不是这条通道。
31
+ 5. 办公 API:suite 已带 `@wecom/cli@1.1.0`。授权跑 `node $REZ_WECOM_CLI auth init`(可扫码)。找不到命令就让用户点左下角升级,禁止 npm。两套凭证不要混。
32
32
  6. 未另批前不要群发、不要改通讯录结构、不要对外发邮件。