@rezti/dsh-rez-suite 0.1.49 → 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 (60) hide show
  1. package/CHANGELOG.md +8 -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 +31 -3
  6. package/lib/client.js +329 -7
  7. package/lib/index.js +241 -15
  8. package/lib/style.css +7 -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 +33 -0
  14. package/src/channel-board.ts +55 -0
  15. package/src/client/locales.ts +62 -6
  16. package/src/client/panel/BoardTab.tsx +136 -0
  17. package/src/client/panel/ConfigTab.tsx +2 -2
  18. package/src/client/panel/StatusTab.tsx +31 -0
  19. package/src/client/panel/panel.module.css +7 -0
  20. package/src/client/settings-card.tsx +4 -1
  21. package/src/index.ts +3 -2
  22. package/src/protocol.ts +14 -0
  23. package/src/routes.ts +7 -1
  24. package/src/tools.ts +49 -6
  25. package/src/wecom-cli.ts +125 -0
  26. package/templates/boss/ops/AGENTS.md +2 -0
  27. package/templates/shared/wecom-cli.SOURCE.md +5 -0
  28. package/templates/shared/wecom-office/SKILL.md +31 -0
  29. package/templates/shared/wecomcli-calendar/SKILL.md +303 -0
  30. package/templates/shared/wecomcli-contact/SKILL.md +58 -0
  31. package/templates/shared/wecomcli-disk/SKILL.md +389 -0
  32. package/templates/shared/wecomcli-doc/SKILL.md +137 -0
  33. package/templates/shared/wecomcli-doc-manage/SKILL.md +132 -0
  34. package/templates/shared/wecomcli-email/SKILL.md +218 -0
  35. package/templates/shared/wecomcli-media/SKILL.md +98 -0
  36. package/templates/shared/wecomcli-meeting/SKILL.md +373 -0
  37. package/templates/shared/wecomcli-message/SKILL.md +200 -0
  38. package/templates/shared/wecomcli-shared/SKILL.md +73 -0
  39. package/templates/shared/wecomcli-sheet/SKILL.md +172 -0
  40. package/templates/shared/wecomcli-smartpage/SKILL.md +170 -0
  41. package/templates/shared/wecomcli-smartsheet/SKILL.md +154 -0
  42. package/templates/shared/wecomcli-todo/SKILL.md +76 -0
  43. package/templates/staff/design/AGENTS.md +2 -1
  44. package/templates/staff/ecommerce/.agents/skills/ops-ecommerce/SKILL.md +6 -0
  45. package/templates/staff/ecommerce/AGENTS.md +48 -0
  46. package/templates/staff/ecommerce/BOOTSTRAP.md +23 -0
  47. package/templates/staff/ecommerce/IDENTITY.md +8 -0
  48. package/templates/staff/ecommerce/MEMORY.md +9 -0
  49. package/templates/staff/ecommerce/PRIORITIES.md +3 -0
  50. package/templates/staff/ecommerce/SOUL.md +5 -0
  51. package/templates/staff/ecommerce/USER.md +8 -0
  52. package/templates/staff/hr/.agents/skills/staff-onboard-keys/SKILL.md +1 -1
  53. package/templates/staff/publish/.agents/skills/ops-publish/SKILL.md +6 -0
  54. package/templates/staff/publish/AGENTS.md +56 -0
  55. package/templates/staff/publish/BOOTSTRAP.md +23 -0
  56. package/templates/staff/publish/IDENTITY.md +8 -0
  57. package/templates/staff/publish/MEMORY.md +9 -0
  58. package/templates/staff/publish/PRIORITIES.md +3 -0
  59. package/templates/staff/publish/SOUL.md +6 -0
  60. package/templates/staff/publish/USER.md +8 -0
@@ -9,13 +9,14 @@ export const zh = {
9
9
  'settings.description': '公司 MCP(Odoo / Nextcloud / HA / TAPD / GSC)和企业微信智能机器人。个人微信在「微信」页签扫码。',
10
10
  'panel.title': 'Rez Suite',
11
11
  'tab.config': '配置',
12
+ 'tab.board': '看板',
12
13
  'tab.weixin': '微信',
13
14
  'tab.status': '状态',
14
15
  'tab.audit': '用量',
15
16
  'tab.changelog': '更新日志',
16
17
  // config
17
18
  'config.role': '角色',
18
- 'config.roleHint': '选身份后左侧自动出现工作区。人事/财务/法务助理全员可见;设计房间仅「运营」;实验室仅「工程师」;搞钱/产品/运营仅「老板」。',
19
+ 'config.roleHint': '选身份后左侧自动出现工作区。人事/财务/法务助理全员可见;设计、发布、电商房间仅「运营」;实验室仅「工程师」;搞钱/产品/运营仅「老板」。',
19
20
  'role.all': '全部',
20
21
  'role.boss': '老板',
21
22
  'role.engineer': '工程师',
@@ -43,7 +44,7 @@ export const zh = {
43
44
  'config.wecom.botId': 'Bot ID',
44
45
  'config.wecom.secret': 'Secret',
45
46
  'config.wecom.webhook': '群机器人 Webhook',
46
- 'config.wecom.hint': 'Accio 同款:一个房间绑一个智能机器人。同事在企微跟哪个 bot 说话,就进网页左侧那个房间。密钥只存本机 ~/.dsh/wecom-channels.json,不要写进 compose。BotID / Secret 在企业微信管理后台 → 智能机器人。',
47
+ 'config.wecom.hint': 'Accio 同款:一个房间绑一个智能机器人。同事在企微跟哪个 bot 说话,就进网页左侧那个房间的同一条会话。密钥只存本机 ~/.dsh/wecom-channels.json,不要写进 compose。BotID / Secret 在企业微信管理后台 → 智能机器人。',
47
48
  'config.wecom.room': '房间',
48
49
  'config.wecom.add': '添加绑定',
49
50
  'config.wecom.remove': '删除',
@@ -84,7 +85,7 @@ export const zh = {
84
85
  'config.testOk': '成功({tools} 个工具, {latency}ms)',
85
86
  'config.testFail': '失败:{error}',
86
87
  // status
87
- 'status.hint': '绿标表示本机已注册 mcp__<系统>__* 工具。远端 MCP 通了但这里仍红,说明 dsh-mcp-client 还没挂上。企查查只在法务助理房间可用;其它房间不显示或显示「房间未启用」。',
88
+ 'status.hint': '绿标表示本机已注册 mcp__<系统>__* 工具。远端 MCP 通了但这里仍红,说明 dsh-mcp-client 还没挂上。企微办公能力看下方 wecom-cli,不是 MCP 表。企查查只在法务助理房间可用;其它房间不显示或显示「房间未启用」。',
88
89
  'status.total': '当前角色 {role},共注册 {count} 个 MCP 工具',
89
90
  'status.col.server': 'Server',
90
91
  'status.col.state': '状态',
@@ -92,6 +93,12 @@ export const zh = {
92
93
  'status.col.registered': '已注册',
93
94
  'status.col.error': '错误',
94
95
  'status.none': '暂无状态',
96
+ 'status.wecomCli': '企业微信办公(官方 wecom-cli,不是 MCP)',
97
+ 'status.wecomCli.installed': 'CLI',
98
+ 'status.wecomCli.authorized': '授权',
99
+ 'status.wecomCli.yes': '已就绪',
100
+ 'status.wecomCli.no': '未就绪',
101
+ 'status.wecomCli.modules': '品类',
95
102
  // audit
96
103
  'audit.totalCalls': '调用次数',
97
104
  'audit.inputTokens': '输入 token',
@@ -114,6 +121,27 @@ export const zh = {
114
121
  'audit.empty': '暂无记录',
115
122
  'changelog.current': '当前 {version}',
116
123
  'changelog.upgrade': '升级:',
124
+ 'board.hint': '合同 Account 拆两室:媒体走「发布」,电商走「电商」(一期只阿里巴巴国际站;Amazon 等有号)。不是独立后台。密钥不在这里。',
125
+ 'board.people': 'People / 权限',
126
+ 'board.peopleHint': '继续 Odoo 员工帐号(公司/记录规则)+ 企业微信一房间一 bot。不新建权限库,不改 Odoo 权限组。',
127
+ 'board.role': '当前 Harness 身份:{role}(只决定侧栏房间)',
128
+ 'board.media': '媒体连接',
129
+ 'board.mediaHint': '内容/社媒在「发布」工作间(Post editor)。不建图库。',
130
+ 'board.ecommerce': '电商平台',
131
+ 'board.ecommerceHint': 'Listing 在「电商」工作间。一期只有阿里巴巴国际站。渠道商品 ID 写回 Odoo,库存从 Odoo 推到阿里。Amazon 没有号,标后续。',
132
+ 'board.rag': 'RAG',
133
+ 'board.ragHint': '基于已有 Dify,知识库对接 Nextcloud 锁死路径(Standards + Marketing/Publish)。Harness 不做推理。',
134
+ 'board.inbox': 'Message inbox',
135
+ 'board.inboxHint': '一期只做 WhatsApp Business,会话落到 Odoo(浙江雷泽,Business 号已有)。不用官方收费模块,走社区 add-on(OCA mail_gateway_whatsapp)。禁止自写 Meta 客户端。生产安装另开窗口。',
136
+ 'board.col.source': '来源',
137
+ 'board.col.state': '状态',
138
+ 'board.col.channel': '渠道',
139
+ 'board.col.room': '房间',
140
+ 'board.col.phase': '阶段',
141
+ 'board.bound': '已绑 bot',
142
+ 'board.unbound': '未绑定',
143
+ 'board.phase1': '一期',
144
+ 'board.phaseLater': '后续',
117
145
  // common
118
146
  'common.close': '关闭',
119
147
  'common.loading': '加载中…',
@@ -151,12 +179,13 @@ export const en: Record<RezKey, string> = {
151
179
  'settings.description': 'Company MCP (Odoo / Nextcloud / HA / TAPD / GSC) and WeCom AI bots. Personal WeChat is the WeChat tab.',
152
180
  'panel.title': 'Rez Suite',
153
181
  'tab.config': 'Config',
182
+ 'tab.board': 'Board',
154
183
  'tab.weixin': 'WeChat',
155
184
  'tab.status': 'Status',
156
185
  'tab.audit': 'Usage',
157
186
  'tab.changelog': 'Changelog',
158
187
  'config.role': 'Role',
159
- 'config.roleHint': 'Identity registers workspaces in the sidebar. Staff assistants for every role; 设计 for Operations only; 实验室 for Engineer only; 搞钱 / 产品 / 运营 for Boss only.',
188
+ 'config.roleHint': 'Identity registers workspaces in the sidebar. Staff assistants for every role; 设计 / 发布 / 电商 for Operations only; 实验室 for Engineer only; 搞钱 / 产品 / 运营 for Boss only.',
160
189
  'role.all': 'All',
161
190
  'role.boss': 'Boss',
162
191
  'role.engineer': 'Engineer',
@@ -184,7 +213,7 @@ export const en: Record<RezKey, string> = {
184
213
  'config.wecom.botId': 'Bot ID',
185
214
  'config.wecom.secret': 'Secret',
186
215
  'config.wecom.webhook': 'Group bot webhook',
187
- 'config.wecom.hint': 'Accio-style: one WeCom AI bot per Harness room. Whoever talks to that bot lands in that sidebar folder. Secrets stay in ~/.dsh/wecom-channels.json, not compose. BotID / Secret come from WeCom admin → AI bot.',
216
+ 'config.wecom.hint': 'Accio-style: one WeCom AI bot per Harness room. Whoever talks to that bot lands in the same sidebar session. Secrets stay in ~/.dsh/wecom-channels.json, not compose. BotID / Secret come from WeCom admin → AI bot.',
188
217
  'config.wecom.room': 'Room',
189
218
  'config.wecom.add': 'Add binding',
190
219
  'config.wecom.remove': 'Remove',
@@ -224,7 +253,7 @@ export const en: Record<RezKey, string> = {
224
253
  'config.testResult': '{server}: {status}',
225
254
  'config.testOk': 'OK ({tools} tools, {latency}ms)',
226
255
  'config.testFail': 'Failed: {error}',
227
- 'status.hint': 'Green means this host registered mcp__<system>__* tools. A live remote MCP with a red row means dsh-mcp-client has not attached yet. Qichacha is only for the Legal assistant room; other rooms hide the row or show “room not enabled”.',
256
+ 'status.hint': 'Green means this host registered mcp__<system>__* tools. A live remote MCP with a red row means dsh-mcp-client has not attached yet. WeCom office APIs are the wecom-cli row below, not the MCP table. Qichacha is only for the Legal assistant room; other rooms hide the row or show “room not enabled”.',
228
257
  'status.total': 'Role {role}, {count} MCP tools registered',
229
258
  'status.col.server': 'Server',
230
259
  'status.col.state': 'State',
@@ -232,6 +261,12 @@ export const en: Record<RezKey, string> = {
232
261
  'status.col.registered': 'Registered',
233
262
  'status.col.error': 'Error',
234
263
  'status.none': 'No status yet',
264
+ 'status.wecomCli': 'WeCom office APIs (official wecom-cli, not MCP)',
265
+ 'status.wecomCli.installed': 'CLI',
266
+ 'status.wecomCli.authorized': 'Auth',
267
+ 'status.wecomCli.yes': 'Ready',
268
+ 'status.wecomCli.no': 'Not ready',
269
+ 'status.wecomCli.modules': 'Modules',
235
270
  'audit.totalCalls': 'Calls',
236
271
  'audit.inputTokens': 'Input tokens',
237
272
  'audit.outputTokens': 'Output tokens',
@@ -253,6 +288,27 @@ export const en: Record<RezKey, string> = {
253
288
  'audit.empty': 'No records',
254
289
  'changelog.current': 'Now {version}',
255
290
  'changelog.upgrade': 'Upgrade:',
291
+ 'board.hint': 'Account connections split into two rooms: media in 发布, ecommerce in 电商 (phase 1: Alibaba.com only; Amazon waits for an account). Not a separate admin site. Secrets stay off this board.',
292
+ 'board.people': 'People / permissions',
293
+ 'board.peopleHint': 'Keep Odoo employee accounts (company / record rules) plus one WeCom AI bot per room. No new ACL database. Do not change Odoo groups.',
294
+ 'board.role': 'Current Harness identity: {role} (sidebar rooms only)',
295
+ 'board.media': 'Media connections',
296
+ 'board.mediaHint': 'Content / social lives in the 发布 room (Post editor). No media library.',
297
+ 'board.ecommerce': 'Ecommerce platforms',
298
+ 'board.ecommerceHint': 'Listings live in the 电商 room. Phase 1 is Alibaba.com. Write channel product IDs back to Odoo and push stock Odoo → Alibaba. Amazon is later until we have an account.',
299
+ 'board.rag': 'RAG',
300
+ 'board.ragHint': 'Reuse Dify. Index Nextcloud locked paths (Standards + Marketing/Publish). Harness does not run embeddings.',
301
+ 'board.inbox': 'Message inbox',
302
+ 'board.inboxHint': 'Phase 1 is WhatsApp Business into Odoo (Zhejiang ReZ-TI; WABA already exists). Skip the paid official module; reuse OCA mail_gateway_whatsapp. No homemade Meta client. Production install needs a separate window.',
303
+ 'board.col.source': 'Source',
304
+ 'board.col.state': 'State',
305
+ 'board.col.channel': 'Channel',
306
+ 'board.col.room': 'Room',
307
+ 'board.col.phase': 'Phase',
308
+ 'board.bound': 'Bot bound',
309
+ 'board.unbound': 'Unbound',
310
+ 'board.phase1': 'Phase 1',
311
+ 'board.phaseLater': 'Later',
256
312
  'common.close': 'Close',
257
313
  'common.loading': 'Loading…',
258
314
  'common.error': 'Error: {error}',
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Account board: media (发布) vs ecommerce (电商; phase 1 Alibaba, Amazon later).
3
+ * People = Odoo user + WeCom room bots. No new ACL store.
4
+ */
5
+
6
+ import { useEffect, useState } from 'react'
7
+ import type { RezApi } from '../api.ts'
8
+ import type { RezPublicConfig, RezWecomBotPublic } from '../../protocol.ts'
9
+ import { ECOMMERCE_ROOM, MEDIA_ROOM, channelsFor, wecomBound } from '../../channel-board.ts'
10
+ import { errorMessage, useT } from './helpers.ts'
11
+ import css from './panel.module.css'
12
+
13
+ export function BoardTab({ api }: { api: RezApi }) {
14
+ const t = useT()
15
+ const [config, setConfig] = useState<RezPublicConfig | null>(null)
16
+ const [bots, setBots] = useState<RezWecomBotPublic[]>([])
17
+ const [error, setError] = useState('')
18
+ const [loading, setLoading] = useState(true)
19
+
20
+ const load = async (): Promise<void> => {
21
+ setLoading(true)
22
+ setError('')
23
+ try {
24
+ const next = await api.getConfig()
25
+ setConfig(next)
26
+ try {
27
+ const wecom = await api.wecomChannels()
28
+ setBots(wecom.bots)
29
+ } catch {
30
+ setBots([])
31
+ }
32
+ } catch (err) {
33
+ setError(errorMessage(err))
34
+ } finally {
35
+ setLoading(false)
36
+ }
37
+ }
38
+
39
+ useEffect(() => { void load() }, [])
40
+
41
+ if (loading) return <div className={css.message}>{t('common.loading')}</div>
42
+ if (config === null) return <div className={css.error}>{error}</div>
43
+
44
+ const odooOk = config.connections.odoo.secretConfigured
45
+ const lang = t('tab.config') === '配置' ? 'zh' : 'en'
46
+
47
+ return (
48
+ <div className={css.tabBody}>
49
+ <div className={css.toolbar}>
50
+ <span className={css.message}>{t('board.hint')}</span>
51
+ <button type="button" className={css.button} onClick={() => { void load() }}>{t('common.refresh')}</button>
52
+ </div>
53
+ {error !== '' && <div className={css.error}>{error}</div>}
54
+
55
+ <h3 className={css.boardTitle}>{t('board.people')}</h3>
56
+ <p className={css.message}>{t('board.peopleHint')}</p>
57
+ <div className={css.tableWrap}>
58
+ <table className={css.table}>
59
+ <thead>
60
+ <tr>
61
+ <th>{t('board.col.source')}</th>
62
+ <th>{t('board.col.state')}</th>
63
+ </tr>
64
+ </thead>
65
+ <tbody>
66
+ <tr>
67
+ <td>Odoo</td>
68
+ <td>
69
+ <span className={css.badge + ' ' + (odooOk ? css.badgeOk : css.badgeFail)}>
70
+ {odooOk ? t('config.secretConfigured') : t('config.secretMissing')}
71
+ </span>
72
+ </td>
73
+ </tr>
74
+ <tr>
75
+ <td>WeCom · {MEDIA_ROOM}</td>
76
+ <td>
77
+ <span className={css.badge + ' ' + (wecomBound(MEDIA_ROOM, bots) ? css.badgeOk : css.badgeFail)}>
78
+ {wecomBound(MEDIA_ROOM, bots) ? t('board.bound') : t('board.unbound')}
79
+ </span>
80
+ </td>
81
+ </tr>
82
+ <tr>
83
+ <td>WeCom · {ECOMMERCE_ROOM}</td>
84
+ <td>
85
+ <span className={css.badge + ' ' + (wecomBound(ECOMMERCE_ROOM, bots) ? css.badgeOk : css.badgeFail)}>
86
+ {wecomBound(ECOMMERCE_ROOM, bots) ? t('board.bound') : t('board.unbound')}
87
+ </span>
88
+ </td>
89
+ </tr>
90
+ </tbody>
91
+ </table>
92
+ </div>
93
+ <p className={css.message}>{t('board.role', { role: config.role })}</p>
94
+
95
+ <h3 className={css.boardTitle}>{t('board.media')}</h3>
96
+ <p className={css.message}>{t('board.mediaHint')}</p>
97
+ <ChannelTable kind="media" lang={lang} />
98
+
99
+ <h3 className={css.boardTitle}>{t('board.ecommerce')}</h3>
100
+ <p className={css.message}>{t('board.ecommerceHint')}</p>
101
+ <ChannelTable kind="ecommerce" lang={lang} />
102
+
103
+ <h3 className={css.boardTitle}>{t('board.rag')}</h3>
104
+ <p className={css.message}>{t('board.ragHint')}</p>
105
+ <h3 className={css.boardTitle}>{t('board.inbox')}</h3>
106
+ <p className={css.message}>{t('board.inboxHint')}</p>
107
+ </div>
108
+ )
109
+ }
110
+
111
+ function ChannelTable({ kind, lang }: { kind: 'media' | 'ecommerce'; lang: 'zh' | 'en' }) {
112
+ const t = useT()
113
+ const rows = channelsFor(kind)
114
+ return (
115
+ <div className={css.tableWrap}>
116
+ <table className={css.table}>
117
+ <thead>
118
+ <tr>
119
+ <th>{t('board.col.channel')}</th>
120
+ <th>{t('board.col.room')}</th>
121
+ <th>{t('board.col.phase')}</th>
122
+ </tr>
123
+ </thead>
124
+ <tbody>
125
+ {rows.map((row) => (
126
+ <tr key={row.id}>
127
+ <td>{lang === 'zh' ? row.nameZh : row.nameEn}</td>
128
+ <td>{row.room}</td>
129
+ <td>{row.phase === '1' ? t('board.phase1') : t('board.phaseLater')}</td>
130
+ </tr>
131
+ ))}
132
+ </tbody>
133
+ </table>
134
+ </div>
135
+ )
136
+ }
@@ -252,12 +252,12 @@ export function ConfigTab({ api }: { api: RezApi }) {
252
252
  try {
253
253
  const wecom = await api.wecomChannels()
254
254
  setWecomRooms(wecom.rooms.length > 0 ? wecom.rooms : [
255
- '搞钱', '产品', '运营', '人事助理', '财务助理', '法务助理', '设计', '实验室',
255
+ '搞钱', '产品', '运营', '人事助理', '财务助理', '法务助理', '设计', '发布', '电商', '实验室',
256
256
  ])
257
257
  setWecomDrafts(toWecomDrafts(wecom.bots))
258
258
  setWecomStatus(wecom.status)
259
259
  } catch {
260
- setWecomRooms(['搞钱', '产品', '运营', '人事助理', '财务助理', '法务助理', '设计', '实验室'])
260
+ setWecomRooms(['搞钱', '产品', '运营', '人事助理', '财务助理', '法务助理', '设计', '发布', '电商', '实验室'])
261
261
  }
262
262
  } catch (err) {
263
263
  setError(errorMessage(err))
@@ -63,6 +63,37 @@ export function StatusTab({ api }: { api: RezApi }) {
63
63
  </tbody>
64
64
  </table>
65
65
  </div>
66
+ {status.wecomCli !== undefined && (
67
+ <div className={css.tableWrap}>
68
+ <p className={css.message}>{t('status.wecomCli')}</p>
69
+ <table className={css.table}>
70
+ <thead>
71
+ <tr>
72
+ <th>{t('status.wecomCli.installed')}</th>
73
+ <th>{t('status.wecomCli.authorized')}</th>
74
+ <th>{t('status.wecomCli.modules')}</th>
75
+ <th>{t('status.col.error')}</th>
76
+ </tr>
77
+ </thead>
78
+ <tbody>
79
+ <tr>
80
+ <td>
81
+ <span className={css.badge + ' ' + (status.wecomCli.installed ? css.badgeOk : css.badgeFail)}>
82
+ {status.wecomCli.installed ? t('status.wecomCli.yes') : t('status.wecomCli.no')}
83
+ </span>
84
+ </td>
85
+ <td>
86
+ <span className={css.badge + ' ' + (status.wecomCli.authorized ? css.badgeOk : css.badgeFail)}>
87
+ {status.wecomCli.authorized ? t('status.wecomCli.yes') : t('status.wecomCli.no')}
88
+ </span>
89
+ </td>
90
+ <td>{(status.wecomCli.modules ?? []).map(row => row.label).join('、')}</td>
91
+ <td>{status.wecomCli.hint}</td>
92
+ </tr>
93
+ </tbody>
94
+ </table>
95
+ </div>
96
+ )}
66
97
  </div>
67
98
  )
68
99
  }
@@ -266,6 +266,13 @@
266
266
  vertical-align: top;
267
267
  }
268
268
 
269
+ .boardTitle {
270
+ margin: 16px 0 4px;
271
+ font-size: 13px;
272
+ font-weight: 600;
273
+ color: var(--dsw-alias-label-primary);
274
+ }
275
+
269
276
  .badge {
270
277
  display: inline-block;
271
278
  padding: 2px 8px;
@@ -10,19 +10,21 @@ import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
10
10
  import { RezApi } from './api.ts'
11
11
  import { RezT } from './panel/helpers.ts'
12
12
  import { AuditTab } from './panel/AuditTab.tsx'
13
+ import { BoardTab } from './panel/BoardTab.tsx'
13
14
  import { ChangelogTab } from './panel/ChangelogTab.tsx'
14
15
  import { ConfigTab } from './panel/ConfigTab.tsx'
15
16
  import { StatusTab } from './panel/StatusTab.tsx'
16
17
  import { WeixinTab } from './panel/WeixinTab.tsx'
17
18
  import css from './panel/panel.module.css'
18
19
 
19
- export type RezTab = 'config' | 'weixin' | 'status' | 'audit' | 'changelog'
20
+ export type RezTab = 'config' | 'board' | 'weixin' | 'status' | 'audit' | 'changelog'
20
21
 
21
22
  export function RezSettingsPage({ t }: { t: TranslateNS<'dsh-rez-suite'> }) {
22
23
  const api = useMemo(() => new RezApi(), [])
23
24
  const [activeTab, setActiveTab] = useState<RezTab>('config')
24
25
  const tabs: ReadonlyArray<{ id: RezTab; label: string }> = [
25
26
  { id: 'config', label: t('tab.config') },
27
+ { id: 'board', label: t('tab.board') },
26
28
  { id: 'weixin', label: t('tab.weixin') },
27
29
  { id: 'status', label: t('tab.status') },
28
30
  { id: 'audit', label: t('tab.audit') },
@@ -47,6 +49,7 @@ export function RezSettingsPage({ t }: { t: TranslateNS<'dsh-rez-suite'> }) {
47
49
  ))}
48
50
  </div>
49
51
  {activeTab === 'config' && <ConfigTab api={api} />}
52
+ {activeTab === 'board' && <BoardTab api={api} />}
50
53
  {activeTab === 'weixin' && <WeixinTab api={api} />}
51
54
  {activeTab === 'status' && <StatusTab api={api} />}
52
55
  {activeTab === 'audit' && <AuditTab api={api} />}
package/src/index.ts CHANGED
@@ -106,8 +106,9 @@ export const Config: z<RezConfig> = z.object({
106
106
  const SECTION_ORDER = 150
107
107
 
108
108
  export const REZ_GUIDANCE_CORE = [
109
- '本机已安装 ReZ-TI 拆分包:工作身份由 dsh-rez-sso 提供,MCP 由官方 dsh-mcp-client 接入 Odoo / Nextcloud / 企业微信 / Home Assistant / TAPD / GSC。',
110
- '工具名 mcp__odoo__*、mcp__nextcloud__*、mcp__wechat__*、mcp__homeassistant__*、mcp__tapd__*、mcp__gsc__*。',
109
+ '本机已安装 ReZ-TI 拆分包:工作身份由 dsh-rez-sso 提供,MCP 由官方 dsh-mcp-client 接入 Odoo / Nextcloud / Home Assistant / TAPD / GSC。',
110
+ '工具名 mcp__odoo__*、mcp__nextcloud__*、mcp__homeassistant__*、mcp__tapd__*、mcp__gsc__*。',
111
+ '企业微信办公能力不是 MCP:走官方 wecom-cli >=1.1(通讯录/在线文档/在线表格/智能表格/智能文档/待办/日程/会议/邮件/微盘/消息)。本企业已开通这些品类。看 rez_status 的 wecomCli.modules,按房间 wecomcli-* 技能执行。不要因为没有 mcp__wechat__* 就说没有企微能力,也不要说「超过 10 人只能用文档」。CLI 报某模块未授权才说该能力未开通。设置页 BotID+Secret 只是房间消息通道。旧群 webhook 兼容才会出现 mcp__wechat__*。',
111
112
  '密钥:REZ_ODOO_TOKEN、REZ_NEXTCLOUD_PASSWORD、REZ_WECHAT_WEBHOOK、REZ_HA_TOKEN、REZ_TAPD_TOKEN、REZ_GSC_KEY。',
112
113
  '个人微信(QClaw/ClawBot):打开 设置 → 插件 → ReZ-TI → 微信 扫码,然后直接在微信里说话。消息进入网页左侧「微信」文件夹并延续同一会话,模型跟网页默认。网页归档了再从微信说话会自动恢复到侧栏。发「重启对话」会新开一条可见会话并把旧会话归档。网页端必须开着。不必再跑命令、也不要调用微信 MCP 工具。',
113
114
  '意图用 rez_set_intent(erp/files/chat/home/auto)。审计用 rez_audit。',
package/src/protocol.ts CHANGED
@@ -123,11 +123,25 @@ export interface RezServerStatus {
123
123
  startedAt?: number
124
124
  }
125
125
 
126
+ /** Official @wecom/cli office APIs. Not mcp__wechat__*. */
127
+ export interface RezWecomCliModule {
128
+ id: string
129
+ label: string
130
+ }
131
+
132
+ export interface RezWecomCliStatus {
133
+ installed: boolean
134
+ authorized: boolean
135
+ modules: RezWecomCliModule[]
136
+ hint: string
137
+ }
138
+
126
139
  /** Complete /api/dsh-rez-suite/status response. */
127
140
  export interface RezStatusResponse {
128
141
  role: RezRoleId
129
142
  servers: RezServerStatus[]
130
143
  totalRegisteredTools: number
144
+ wecomCli: RezWecomCliStatus
131
145
  }
132
146
 
133
147
  /** QClaw-style WeChat channel status (scan QR, then talk in WeChat). */
package/src/routes.ts CHANGED
@@ -18,6 +18,7 @@ import { isRezRoleId, REZ_API } from './protocol.ts'
18
18
  import type { McpHost } from './mcp-host.ts'
19
19
  import type { TokenManager } from './token-manager.ts'
20
20
  import { publicConfig } from './store.ts'
21
+ import { probeWecomCli } from './wecom-cli.ts'
21
22
 
22
23
  const MAX_JSON_BODY_BYTES = 256 * 1024
23
24
  const SECRET_MASK = '********'
@@ -200,7 +201,12 @@ export function makeRoutes(deps: RezRoutesDeps): WebRoute[] {
200
201
  if (!guard(req, res, 'GET')) return
201
202
  const servers = host.status()
202
203
  const totalRegisteredTools = servers.reduce((sum, server) => sum + server.registeredTools, 0)
203
- writeJson(res, 200, { role: getConfig().role, servers, totalRegisteredTools })
204
+ writeJson(res, 200, {
205
+ role: getConfig().role,
206
+ servers,
207
+ totalRegisteredTools,
208
+ wecomCli: probeWecomCli(),
209
+ })
204
210
  },
205
211
  },
206
212
  {
package/src/tools.ts CHANGED
@@ -8,12 +8,13 @@ import { defineTool as defineToolOriginal } from '@deepseek-ai/dsh-tools'
8
8
  /** Loosen the typed helper for the suite's small fixed tools. */
9
9
  const defineTool = defineToolOriginal as unknown as (options: any) => any
10
10
  import type { ContentBlock } from '@deepseek-ai/dsh-llm'
11
- import type { RezConfig, RezTestResult } from './protocol.ts'
11
+ import type { RezConfig, RezTestResult, RezWecomCliStatus } from './protocol.ts'
12
12
  import type { McpHost } from './mcp-host.ts'
13
13
  import type { TokenManager } from './token-manager.ts'
14
14
  import { publicConfig } from './store.ts'
15
15
  import { cwdFromExecution } from './rooms.ts'
16
16
  import { compileEsphomeFirmware } from './esphome-lab.ts'
17
+ import { probeWecomCli } from './wecom-cli.ts'
17
18
 
18
19
  function text(value: string): ContentBlock[] {
19
20
  return [{ type: 'text', text: value }]
@@ -30,10 +31,25 @@ function renderTest(results: RezTestResult[]): string {
30
31
  }).join('\n')
31
32
  }
32
33
 
33
- export function rezStatusTool(host: McpHost, config: () => RezConfig) {
34
+ function renderWecomCli(cli: RezWecomCliStatus | undefined): string {
35
+ const row = cli ?? { installed: false, authorized: false, modules: [], hint: '' }
36
+ const modules = (row.modules ?? []).map((item) => `${item.label}(${item.id})`).join('、')
37
+ return [
38
+ 'wecom-cli(企微办公能力,不是 MCP): installed=' + (row.installed ? 'yes' : 'no')
39
+ + ' authorized=' + (row.authorized ? 'yes' : 'no'),
40
+ modules.length > 0 ? 'modules: ' + modules : 'modules: (官方目录见 wecomcli-* 技能)',
41
+ row.hint,
42
+ ].join('\n')
43
+ }
44
+
45
+ export function rezStatusTool(
46
+ host: McpHost,
47
+ config: () => RezConfig,
48
+ wecomCli: () => RezWecomCliStatus = probeWecomCli,
49
+ ) {
34
50
  return defineTool({
35
51
  name: 'rez_status',
36
- description: 'List the dsh-rez-suite MCP server statuses and how many tools each server registered for the current role. Triggers: rez, MCP, 连接状态.',
52
+ description: 'List MCP server statuses plus WeCom office APIs (official wecom-cli, not mcp__wechat__). Triggers: rez, MCP, 连接状态, 企业微信, 企微能力.',
37
53
  parameters: {},
38
54
  output: {
39
55
  schema: {
@@ -57,10 +73,36 @@ export function rezStatusTool(host: McpHost, config: () => RezConfig) {
57
73
  },
58
74
  },
59
75
  },
76
+ wecomCli: {
77
+ type: 'object',
78
+ required: true,
79
+ additionalProperties: false,
80
+ properties: {
81
+ installed: { type: 'boolean', required: true },
82
+ authorized: { type: 'boolean', required: true },
83
+ hint: { type: 'string', required: true },
84
+ modules: {
85
+ type: 'array',
86
+ required: true,
87
+ items: {
88
+ type: 'object',
89
+ additionalProperties: false,
90
+ properties: {
91
+ id: { type: 'string', required: true },
92
+ label: { type: 'string', required: true },
93
+ },
94
+ },
95
+ },
96
+ },
97
+ },
60
98
  },
61
99
  additionalProperties: false,
62
100
  },
63
- render: (_args: any, value: { role?: string; servers?: Array<Record<string, unknown>> }) => {
101
+ render: (_args: any, value: {
102
+ role?: string
103
+ servers?: Array<Record<string, unknown>>
104
+ wecomCli?: RezWecomCliStatus
105
+ }) => {
64
106
  const rows = (value.servers ?? []).map(server => [
65
107
  server.server,
66
108
  server.state,
@@ -68,7 +110,8 @@ export function rezStatusTool(host: McpHost, config: () => RezConfig) {
68
110
  server.registeredTools,
69
111
  server.lastError ?? '',
70
112
  ].join(' | '))
71
- return text('role: ' + (value.role ?? '') + '\n' + ['server | state | discovered | registered | error', '--- | --- | --- | --- | ---', ...rows].join('\n'))
113
+ const mcp = 'role: ' + (value.role ?? '') + '\n' + ['server | state | discovered | registered | error', '--- | --- | --- | --- | ---', ...rows].join('\n')
114
+ return text(mcp + '\n\n' + renderWecomCli(value.wecomCli))
72
115
  },
73
116
  },
74
117
  async execute(_args: unknown, exec?: { agent?: unknown }) {
@@ -78,7 +121,7 @@ export function rezStatusTool(host: McpHost, config: () => RezConfig) {
78
121
  if (clean.startedAt === undefined) delete clean.startedAt
79
122
  return clean
80
123
  })
81
- return { role: config().role, servers }
124
+ return { role: config().role, servers, wecomCli: wecomCli() }
82
125
  },
83
126
  })
84
127
  }