@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.
- package/CHANGELOG.md +12 -2
- package/README.md +3 -3
- package/README.zh.md +2 -2
- package/cordis.patch.yml +1 -1
- package/lib/client.d.ts +37 -3
- package/lib/client.js +533 -8
- package/lib/index.js +247 -19
- package/lib/style.css +18 -0
- package/package.json +4 -3
- package/src/boss/mount.ts +4 -3
- package/src/boss/register.ts +1 -1
- package/src/boss/seed.ts +29 -0
- package/src/changelog.ts +48 -0
- package/src/channel-board.ts +55 -0
- package/src/client/api.ts +15 -0
- package/src/client/locales.ts +74 -6
- package/src/client/panel/BoardTab.tsx +136 -0
- package/src/client/panel/ConfigTab.tsx +137 -2
- package/src/client/panel/StatusTab.tsx +31 -0
- package/src/client/panel/panel.module.css +18 -0
- package/src/client/settings-card.tsx +4 -1
- package/src/esphome-lab.ts +4 -3
- package/src/index.ts +3 -2
- package/src/observe-mcp.ts +1 -1
- package/src/protocol.ts +37 -0
- package/src/routes.ts +7 -1
- package/src/tools.ts +49 -6
- package/src/wecom-cli.ts +125 -0
- package/templates/boss/ops/AGENTS.md +2 -0
- package/templates/shared/wecom-cli.SOURCE.md +5 -0
- package/templates/shared/wecom-office/SKILL.md +31 -0
- package/templates/shared/wecomcli-calendar/SKILL.md +303 -0
- package/templates/shared/wecomcli-contact/SKILL.md +58 -0
- package/templates/shared/wecomcli-disk/SKILL.md +389 -0
- package/templates/shared/wecomcli-doc/SKILL.md +137 -0
- package/templates/shared/wecomcli-doc-manage/SKILL.md +132 -0
- package/templates/shared/wecomcli-email/SKILL.md +218 -0
- package/templates/shared/wecomcli-media/SKILL.md +98 -0
- package/templates/shared/wecomcli-meeting/SKILL.md +373 -0
- package/templates/shared/wecomcli-message/SKILL.md +200 -0
- package/templates/shared/wecomcli-shared/SKILL.md +73 -0
- package/templates/shared/wecomcli-sheet/SKILL.md +172 -0
- package/templates/shared/wecomcli-smartpage/SKILL.md +170 -0
- package/templates/shared/wecomcli-smartsheet/SKILL.md +154 -0
- package/templates/shared/wecomcli-todo/SKILL.md +76 -0
- package/templates/staff/design/AGENTS.md +2 -1
- package/templates/staff/ecommerce/.agents/skills/ops-ecommerce/SKILL.md +6 -0
- package/templates/staff/ecommerce/AGENTS.md +48 -0
- package/templates/staff/ecommerce/BOOTSTRAP.md +23 -0
- package/templates/staff/ecommerce/IDENTITY.md +8 -0
- package/templates/staff/ecommerce/MEMORY.md +9 -0
- package/templates/staff/ecommerce/PRIORITIES.md +3 -0
- package/templates/staff/ecommerce/SOUL.md +5 -0
- package/templates/staff/ecommerce/USER.md +8 -0
- package/templates/staff/hr/.agents/skills/staff-onboard-keys/SKILL.md +9 -7
- package/templates/staff/lab/AGENTS.md +1 -1
- package/templates/staff/publish/.agents/skills/ops-publish/SKILL.md +6 -0
- package/templates/staff/publish/AGENTS.md +56 -0
- package/templates/staff/publish/BOOTSTRAP.md +23 -0
- package/templates/staff/publish/IDENTITY.md +8 -0
- package/templates/staff/publish/MEMORY.md +9 -0
- package/templates/staff/publish/PRIORITIES.md +3 -0
- package/templates/staff/publish/SOUL.md +6 -0
- package/templates/staff/publish/USER.md +8 -0
package/src/client/locales.ts
CHANGED
|
@@ -6,16 +6,17 @@ 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
|
+
'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,6 +44,12 @@ export const zh = {
|
|
|
43
44
|
'config.wecom.botId': 'Bot ID',
|
|
44
45
|
'config.wecom.secret': 'Secret',
|
|
45
46
|
'config.wecom.webhook': '群机器人 Webhook',
|
|
47
|
+
'config.wecom.hint': 'Accio 同款:一个房间绑一个智能机器人。同事在企微跟哪个 bot 说话,就进网页左侧那个房间的同一条会话。密钥只存本机 ~/.dsh/wecom-channels.json,不要写进 compose。BotID / Secret 在企业微信管理后台 → 智能机器人。',
|
|
48
|
+
'config.wecom.room': '房间',
|
|
49
|
+
'config.wecom.add': '添加绑定',
|
|
50
|
+
'config.wecom.remove': '删除',
|
|
51
|
+
'config.wecom.empty': '还没有绑定。给人事/财务/法务等房间各建一个企微智能机器人,就能变成不同角色。',
|
|
52
|
+
'config.wecom.status': '通道',
|
|
46
53
|
'config.ha': 'Home Assistant',
|
|
47
54
|
'config.ha.url': 'HA MCP URL',
|
|
48
55
|
'config.ha.token': '长期访问令牌',
|
|
@@ -78,7 +85,7 @@ export const zh = {
|
|
|
78
85
|
'config.testOk': '成功({tools} 个工具, {latency}ms)',
|
|
79
86
|
'config.testFail': '失败:{error}',
|
|
80
87
|
// status
|
|
81
|
-
'status.hint': '绿标表示本机已注册 mcp__<系统>__* 工具。远端 MCP 通了但这里仍红,说明 dsh-mcp-client
|
|
88
|
+
'status.hint': '绿标表示本机已注册 mcp__<系统>__* 工具。远端 MCP 通了但这里仍红,说明 dsh-mcp-client 还没挂上。企微办公能力看下方 wecom-cli,不是 MCP 表。企查查只在法务助理房间可用;其它房间不显示或显示「房间未启用」。',
|
|
82
89
|
'status.total': '当前角色 {role},共注册 {count} 个 MCP 工具',
|
|
83
90
|
'status.col.server': 'Server',
|
|
84
91
|
'status.col.state': '状态',
|
|
@@ -86,6 +93,12 @@ export const zh = {
|
|
|
86
93
|
'status.col.registered': '已注册',
|
|
87
94
|
'status.col.error': '错误',
|
|
88
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': '品类',
|
|
89
102
|
// audit
|
|
90
103
|
'audit.totalCalls': '调用次数',
|
|
91
104
|
'audit.inputTokens': '输入 token',
|
|
@@ -108,6 +121,27 @@ export const zh = {
|
|
|
108
121
|
'audit.empty': '暂无记录',
|
|
109
122
|
'changelog.current': '当前 {version}',
|
|
110
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': '后续',
|
|
111
145
|
// common
|
|
112
146
|
'common.close': '关闭',
|
|
113
147
|
'common.loading': '加载中…',
|
|
@@ -142,15 +176,16 @@ export const en: Record<RezKey, string> = {
|
|
|
142
176
|
'entry.label': 'Rez',
|
|
143
177
|
'entry.tooltip': 'Rez Suite configuration and audit panel',
|
|
144
178
|
'settings.title': 'ReZ-TI',
|
|
145
|
-
'settings.description': 'Company MCP (Odoo / Nextcloud / HA / TAPD / GSC)
|
|
179
|
+
'settings.description': 'Company MCP (Odoo / Nextcloud / HA / TAPD / GSC) and WeCom AI bots. Personal WeChat is the WeChat tab.',
|
|
146
180
|
'panel.title': 'Rez Suite',
|
|
147
181
|
'tab.config': 'Config',
|
|
182
|
+
'tab.board': 'Board',
|
|
148
183
|
'tab.weixin': 'WeChat',
|
|
149
184
|
'tab.status': 'Status',
|
|
150
185
|
'tab.audit': 'Usage',
|
|
151
186
|
'tab.changelog': 'Changelog',
|
|
152
187
|
'config.role': 'Role',
|
|
153
|
-
'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.',
|
|
154
189
|
'role.all': 'All',
|
|
155
190
|
'role.boss': 'Boss',
|
|
156
191
|
'role.engineer': 'Engineer',
|
|
@@ -178,6 +213,12 @@ export const en: Record<RezKey, string> = {
|
|
|
178
213
|
'config.wecom.botId': 'Bot ID',
|
|
179
214
|
'config.wecom.secret': 'Secret',
|
|
180
215
|
'config.wecom.webhook': 'Group bot webhook',
|
|
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.',
|
|
217
|
+
'config.wecom.room': 'Room',
|
|
218
|
+
'config.wecom.add': 'Add binding',
|
|
219
|
+
'config.wecom.remove': 'Remove',
|
|
220
|
+
'config.wecom.empty': 'No bots yet. Bind a WeCom AI bot to HR / finance / legal / … so each room is a different role.',
|
|
221
|
+
'config.wecom.status': 'Channel',
|
|
181
222
|
'config.ha': 'Home Assistant',
|
|
182
223
|
'config.ha.url': 'HA MCP URL',
|
|
183
224
|
'config.ha.token': 'Long-lived access token',
|
|
@@ -212,7 +253,7 @@ export const en: Record<RezKey, string> = {
|
|
|
212
253
|
'config.testResult': '{server}: {status}',
|
|
213
254
|
'config.testOk': 'OK ({tools} tools, {latency}ms)',
|
|
214
255
|
'config.testFail': 'Failed: {error}',
|
|
215
|
-
'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”.',
|
|
216
257
|
'status.total': 'Role {role}, {count} MCP tools registered',
|
|
217
258
|
'status.col.server': 'Server',
|
|
218
259
|
'status.col.state': 'State',
|
|
@@ -220,6 +261,12 @@ export const en: Record<RezKey, string> = {
|
|
|
220
261
|
'status.col.registered': 'Registered',
|
|
221
262
|
'status.col.error': 'Error',
|
|
222
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',
|
|
223
270
|
'audit.totalCalls': 'Calls',
|
|
224
271
|
'audit.inputTokens': 'Input tokens',
|
|
225
272
|
'audit.outputTokens': 'Output tokens',
|
|
@@ -241,6 +288,27 @@ export const en: Record<RezKey, string> = {
|
|
|
241
288
|
'audit.empty': 'No records',
|
|
242
289
|
'changelog.current': 'Now {version}',
|
|
243
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',
|
|
244
312
|
'common.close': 'Close',
|
|
245
313
|
'common.loading': 'Loading…',
|
|
246
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
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { useEffect, useState, type ReactNode } from 'react'
|
|
7
7
|
import type { RezApi } from '../api.ts'
|
|
8
|
-
import type { RezPublicConfig, RezPublicConnection, RezRoleId, RezTestResult } from '../../protocol.ts'
|
|
8
|
+
import type { RezPublicConfig, RezPublicConnection, RezRoleId, RezTestResult, RezWecomBindingStatus, RezWecomBotPublic } from '../../protocol.ts'
|
|
9
9
|
import { errorMessage, useT } from './helpers.ts'
|
|
10
10
|
import css from './panel.module.css'
|
|
11
11
|
|
|
@@ -31,6 +31,115 @@ type SecretDrafts = {
|
|
|
31
31
|
|
|
32
32
|
const EMPTY_SECRETS: SecretDrafts = { odoo: '', nextcloud: '', wechat: '', homeassistant: '', tapd: '', gsc: '', qcc: '', tianyancha: '', esphome: '' }
|
|
33
33
|
|
|
34
|
+
const WECOM_SECRET_MASK = '********'
|
|
35
|
+
|
|
36
|
+
type WecomDraft = {
|
|
37
|
+
key: string
|
|
38
|
+
room: string
|
|
39
|
+
botId: string
|
|
40
|
+
secret: string
|
|
41
|
+
secretConfigured: boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function WecomSection({
|
|
45
|
+
rooms,
|
|
46
|
+
drafts,
|
|
47
|
+
status,
|
|
48
|
+
onChange,
|
|
49
|
+
}: {
|
|
50
|
+
rooms: string[]
|
|
51
|
+
drafts: WecomDraft[]
|
|
52
|
+
status: RezWecomBindingStatus[]
|
|
53
|
+
onChange: (next: WecomDraft[]) => void
|
|
54
|
+
}) {
|
|
55
|
+
const t = useT()
|
|
56
|
+
const used = new Set(drafts.map(row => row.room))
|
|
57
|
+
const unused = rooms.filter(room => !used.has(room))
|
|
58
|
+
const hintFor = (room: string): string => status.find(row => row.room === room)?.hint ?? ''
|
|
59
|
+
|
|
60
|
+
const add = (): void => {
|
|
61
|
+
const room = unused[0]
|
|
62
|
+
if (room === undefined) return
|
|
63
|
+
onChange([...drafts, { key: `new-${room}-${Date.now()}`, room, botId: '', secret: '', secretConfigured: false }])
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<Section title={t('config.wecom')}>
|
|
68
|
+
<p className={css.message}>{t('config.wecom.hint')}</p>
|
|
69
|
+
{drafts.length === 0 && <p className={css.message}>{t('config.wecom.empty')}</p>}
|
|
70
|
+
{drafts.map((row, index) => {
|
|
71
|
+
const options = rooms.filter(room => room === row.room || !used.has(room))
|
|
72
|
+
return (
|
|
73
|
+
<div key={row.key} className={css.wecomRow}>
|
|
74
|
+
<Field label={t('config.wecom.room')}>
|
|
75
|
+
<select
|
|
76
|
+
className={css.select}
|
|
77
|
+
value={row.room}
|
|
78
|
+
onChange={event => {
|
|
79
|
+
const next = drafts.slice()
|
|
80
|
+
next[index] = { ...row, room: event.target.value }
|
|
81
|
+
onChange(next)
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
{options.map(room => <option key={room} value={room}>{room}</option>)}
|
|
85
|
+
</select>
|
|
86
|
+
</Field>
|
|
87
|
+
<Field label={t('config.wecom.botId')}>
|
|
88
|
+
<input
|
|
89
|
+
className={css.input}
|
|
90
|
+
autoComplete="off"
|
|
91
|
+
value={row.botId}
|
|
92
|
+
onChange={event => {
|
|
93
|
+
const next = drafts.slice()
|
|
94
|
+
next[index] = { ...row, botId: event.target.value }
|
|
95
|
+
onChange(next)
|
|
96
|
+
}}
|
|
97
|
+
/>
|
|
98
|
+
</Field>
|
|
99
|
+
<Field label={t('config.wecom.secret')}>
|
|
100
|
+
<input
|
|
101
|
+
className={css.input}
|
|
102
|
+
type="password"
|
|
103
|
+
autoComplete="off"
|
|
104
|
+
value={row.secret}
|
|
105
|
+
placeholder={row.secretConfigured ? t('config.secretConfigured') : t('config.secretMissing')}
|
|
106
|
+
onChange={event => {
|
|
107
|
+
const next = drafts.slice()
|
|
108
|
+
next[index] = { ...row, secret: event.target.value }
|
|
109
|
+
onChange(next)
|
|
110
|
+
}}
|
|
111
|
+
/>
|
|
112
|
+
</Field>
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
className={css.button}
|
|
116
|
+
onClick={() => { onChange(drafts.filter((_, i) => i !== index)) }}
|
|
117
|
+
>
|
|
118
|
+
{t('config.wecom.remove')}
|
|
119
|
+
</button>
|
|
120
|
+
{hintFor(row.room) !== '' && <p className={css.message}>{hintFor(row.room)}</p>}
|
|
121
|
+
</div>
|
|
122
|
+
)
|
|
123
|
+
})}
|
|
124
|
+
{unused.length > 0 && (
|
|
125
|
+
<div className={css.toolbar}>
|
|
126
|
+
<button type="button" className={css.button} onClick={add}>{t('config.wecom.add')}</button>
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
</Section>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function toWecomDrafts(bots: RezWecomBotPublic[]): WecomDraft[] {
|
|
134
|
+
return bots.map((bot, index) => ({
|
|
135
|
+
key: `${bot.room}-${index}`,
|
|
136
|
+
room: bot.room,
|
|
137
|
+
botId: bot.botId,
|
|
138
|
+
secret: '',
|
|
139
|
+
secretConfigured: bot.secretConfigured,
|
|
140
|
+
}))
|
|
141
|
+
}
|
|
142
|
+
|
|
34
143
|
function Field({ label, children }: { label: string; children: ReactNode }) {
|
|
35
144
|
return (
|
|
36
145
|
<div className={css.field}>
|
|
@@ -133,10 +242,23 @@ export function ConfigTab({ api }: { api: RezApi }) {
|
|
|
133
242
|
const [message, setMessage] = useState('')
|
|
134
243
|
const [error, setError] = useState('')
|
|
135
244
|
const [results, setResults] = useState<RezTestResult[]>([])
|
|
245
|
+
const [wecomRooms, setWecomRooms] = useState<string[]>([])
|
|
246
|
+
const [wecomDrafts, setWecomDrafts] = useState<WecomDraft[]>([])
|
|
247
|
+
const [wecomStatus, setWecomStatus] = useState<RezWecomBindingStatus[]>([])
|
|
136
248
|
|
|
137
249
|
const load = async (): Promise<void> => {
|
|
138
250
|
try {
|
|
139
251
|
setConfig(await api.getConfig())
|
|
252
|
+
try {
|
|
253
|
+
const wecom = await api.wecomChannels()
|
|
254
|
+
setWecomRooms(wecom.rooms.length > 0 ? wecom.rooms : [
|
|
255
|
+
'搞钱', '产品', '运营', '人事助理', '财务助理', '法务助理', '设计', '发布', '电商', '实验室',
|
|
256
|
+
])
|
|
257
|
+
setWecomDrafts(toWecomDrafts(wecom.bots))
|
|
258
|
+
setWecomStatus(wecom.status)
|
|
259
|
+
} catch {
|
|
260
|
+
setWecomRooms(['搞钱', '产品', '运营', '人事助理', '财务助理', '法务助理', '设计', '发布', '电商', '实验室'])
|
|
261
|
+
}
|
|
140
262
|
} catch (err) {
|
|
141
263
|
setError(errorMessage(err))
|
|
142
264
|
} finally {
|
|
@@ -184,6 +306,14 @@ export function ConfigTab({ api }: { api: RezApi }) {
|
|
|
184
306
|
const saved = await api.saveConfig(payload)
|
|
185
307
|
setConfig(saved)
|
|
186
308
|
setSecrets(EMPTY_SECRETS)
|
|
309
|
+
const wecomSaved = await api.saveWecomChannels(wecomDrafts.map(row => ({
|
|
310
|
+
room: row.room,
|
|
311
|
+
botId: row.botId.trim(),
|
|
312
|
+
secret: row.secret.trim().length > 0 ? row.secret.trim() : (row.secretConfigured ? WECOM_SECRET_MASK : ''),
|
|
313
|
+
})))
|
|
314
|
+
setWecomRooms(wecomSaved.rooms)
|
|
315
|
+
setWecomDrafts(toWecomDrafts(wecomSaved.bots))
|
|
316
|
+
setWecomStatus(wecomSaved.status)
|
|
187
317
|
setMessage(t('config.saved'))
|
|
188
318
|
} catch (err) {
|
|
189
319
|
setError(errorMessage(err))
|
|
@@ -251,7 +381,12 @@ export function ConfigTab({ api }: { api: RezApi }) {
|
|
|
251
381
|
usernameLabel={t('config.nextcloud.username')}
|
|
252
382
|
secretLabel={t('config.nextcloud.appPassword')}
|
|
253
383
|
/>
|
|
254
|
-
|
|
384
|
+
<WecomSection
|
|
385
|
+
rooms={wecomRooms}
|
|
386
|
+
drafts={wecomDrafts}
|
|
387
|
+
status={wecomStatus}
|
|
388
|
+
onChange={setWecomDrafts}
|
|
389
|
+
/>
|
|
255
390
|
<McpSection
|
|
256
391
|
title={t('config.ha')}
|
|
257
392
|
row={config.connections.homeassistant}
|
|
@@ -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
|
}
|
|
@@ -169,6 +169,17 @@
|
|
|
169
169
|
height: 15px;
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
.wecomRow {
|
|
173
|
+
display: grid;
|
|
174
|
+
grid-template-columns: minmax(7rem, 0.8fr) minmax(8rem, 1fr) minmax(8rem, 1fr) auto;
|
|
175
|
+
gap: 8px;
|
|
176
|
+
align-items: end;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.wecomRow .message {
|
|
180
|
+
grid-column: 1 / -1;
|
|
181
|
+
}
|
|
182
|
+
|
|
172
183
|
.toolbar {
|
|
173
184
|
display: flex;
|
|
174
185
|
align-items: center;
|
|
@@ -255,6 +266,13 @@
|
|
|
255
266
|
vertical-align: top;
|
|
256
267
|
}
|
|
257
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
|
+
|
|
258
276
|
.badge {
|
|
259
277
|
display: inline-block;
|
|
260
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/esphome-lab.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ESPHome lab: official dsh-mcp-client → lab MCP
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* ESPHome lab client in Harness: official dsh-mcp-client → rezti-lab MCP.
|
|
3
|
+
* The compiler lives in Mac Mini Docker (`deploy/lab`, compose name rezti-lab).
|
|
4
|
+
* Missing lab / missing token must not block `dsh web` (failOnStartupError: false).
|
|
5
|
+
* Never puts firmware bytes into the model context; the tool returns a click-later download link.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { loadRezConnections, readManagedCredential, REZ_CREDENTIAL_REFS, startMcpWhenSecret } from '@rezti/dsh-rez-sso'
|
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 /
|
|
110
|
-
'工具名 mcp__odoo__*、mcp__nextcloud__*、
|
|
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/observe-mcp.ts
CHANGED
|
@@ -14,7 +14,7 @@ export const COMPOSED_SERVERS = ['odoo', 'nextcloud', 'wechat', 'homeassistant',
|
|
|
14
14
|
|
|
15
15
|
export type ComposedServer = (typeof COMPOSED_SERVERS)[number]
|
|
16
16
|
|
|
17
|
-
/** Front panel hides
|
|
17
|
+
/** Front panel hides webhook MCP wechat status; AI bots live in the Config WeCom bindings. TAPD 1154488930001001231. */
|
|
18
18
|
export const FRONT_HIDDEN_SERVERS: ReadonlySet<ComposedServer> = new Set(['wechat'])
|
|
19
19
|
|
|
20
20
|
export function mcpToolPrefix(server: string): string {
|