@rezti/dsh-rez-suite 0.1.45 → 0.1.46
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 +44 -0
- package/README.md +9 -10
- package/README.zh.md +7 -3
- package/cordis.patch.yml +3 -1
- package/lib/client.d.ts +8 -2
- package/lib/client.js +175 -22
- package/lib/index.d.ts +4 -0
- package/lib/index.js +269 -16
- package/lib/style.css +43 -0
- package/package.json +7 -5
- package/src/boss/mount.ts +44 -4
- package/src/boss/register.ts +2 -0
- package/src/boss/seed.ts +29 -1
- package/src/changelog.ts +85 -0
- package/src/client/locales.ts +16 -4
- package/src/client/panel/ChangelogTab.tsx +57 -0
- package/src/client/panel/ConfigTab.tsx +16 -9
- package/src/client/panel/panel.module.css +43 -0
- package/src/client/settings-card.tsx +4 -1
- package/src/esphome-lab.ts +181 -0
- package/src/index.ts +17 -4
- package/src/observe-mcp.ts +12 -6
- package/src/presets.ts +11 -5
- package/src/protocol.ts +2 -0
- package/src/rooms.ts +29 -5
- package/src/store.ts +6 -0
- package/src/tools.ts +41 -0
- package/templates/boss/ops/AGENTS.md +1 -0
- package/templates/boss/product/AGENTS.md +5 -2
- package/templates/staff/design/.agents/skills/ops-design/SKILL.md +6 -0
- package/templates/staff/design/AGENTS.md +71 -0
- package/templates/staff/design/BOOTSTRAP.md +23 -0
- package/templates/staff/design/IDENTITY.md +8 -0
- package/templates/staff/design/MEMORY.md +9 -0
- package/templates/staff/design/PRIORITIES.md +3 -0
- package/templates/staff/design/SOUL.md +6 -0
- package/templates/staff/design/USER.md +8 -0
- package/templates/staff/hr/.agents/skills/staff-hr/SKILL.md +1 -1
- package/templates/staff/hr/.agents/skills/staff-onboard-keys/SKILL.md +99 -0
- package/templates/staff/hr/AGENTS.md +10 -0
- package/templates/staff/lab/.agents/skills/engineer-lab/SKILL.md +6 -0
- package/templates/staff/lab/AGENTS.md +34 -0
- package/templates/staff/lab/BOOTSTRAP.md +23 -0
- package/templates/staff/lab/IDENTITY.md +8 -0
- package/templates/staff/lab/MEMORY.md +9 -0
- package/templates/staff/lab/PRIORITIES.md +3 -0
- package/templates/staff/lab/SOUL.md +5 -0
- package/templates/staff/lab/USER.md +7 -0
package/src/changelog.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-facing suite notes. Keep in sync with CHANGELOG.md (npm / git).
|
|
3
|
+
* The settings tab renders this; do not put release copy only in markdown.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface ChangelogCopy {
|
|
7
|
+
zh: string
|
|
8
|
+
en: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ChangelogSection {
|
|
12
|
+
title: ChangelogCopy
|
|
13
|
+
items: ChangelogCopy[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ChangelogRelease {
|
|
17
|
+
version: string
|
|
18
|
+
date: string
|
|
19
|
+
sections: ChangelogSection[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const SUITE_CHANGELOG: readonly ChangelogRelease[] = [
|
|
23
|
+
{
|
|
24
|
+
version: '0.1.46',
|
|
25
|
+
date: '2026-08-19',
|
|
26
|
+
sections: [
|
|
27
|
+
{
|
|
28
|
+
title: { zh: '员工会碰到的', en: 'What you will see' },
|
|
29
|
+
items: [
|
|
30
|
+
{
|
|
31
|
+
zh: '回形针上传 PDF:说明书 / 规格书走输入栏回形针,先入 Marketing/Inbox,对应 TAPD 改成测试中并转凯文;凯文说审批了哪份,再进 Marketing/Publish。不要用微信发 PDF。',
|
|
32
|
+
en: 'Paperclip PDF uploads: manuals go to Marketing/Inbox first. TAPD moves to 测试中 and Kelvin; after he names the file, it lands in Marketing/Publish. Do not send PDFs over WeChat.',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
zh: '运营多了「设计」房间:拍摄原片 Comfy 只修一次,shot / clean 先入 Inbox;凯文终审后同时写入 Marketing/RAW。',
|
|
36
|
+
en: 'Operations gets a Design room. Retouch shots once in Comfy; shot/clean stay in Inbox until Kelvin dual-saves them to Marketing/RAW.',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
zh: '品牌 / VI 走 Marketing/Inbox/Brand/,终审后写入 Marketing/RAW/Brand/,不要写进某个 SKU。',
|
|
40
|
+
en: 'Brand / VI files go Marketing/Inbox/Brand/, then Marketing/RAW/Brand/ after approval — not into a SKU folder.',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
zh: '工程师多了「实验室」:对话里给 YAML,立刻拿到下载链接。还在编是说明文件;成功是 firmware.bin;失败是带日志的 README。不要在对话里等 bin,不要默认 OTA。',
|
|
44
|
+
en: 'Engineers get a Lab room. Submit YAML and get a download link immediately. Compiling shows a README; success is firmware.bin; failure includes the log. Do not wait for the bin in chat. Do not OTA by default.',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
zh: '人事助理可引导新人领本机安装包、自己申请 API Key。',
|
|
48
|
+
en: 'HR assistant can walk new staff through the desktop installer and API key request.',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
zh: '设置页暂时不显示企业微信配置栏(已填过的 webhook 不会被擦掉)。',
|
|
52
|
+
en: 'WeCom settings are hidden for now. An existing webhook is left as-is.',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
zh: '运营仍可改 TAPD 状态、写评论、往 Inbox 传文件;不能删 TAPD / Nextcloud。',
|
|
56
|
+
en: 'Operations can still update TAPD and write Inbox files, but cannot delete TAPD or Nextcloud items.',
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
title: { zh: '安装与审批机', en: 'Installers and approver machines' },
|
|
62
|
+
items: [
|
|
63
|
+
{
|
|
64
|
+
zh: '员工 Mac zip / Windows exe、审批人 Docker:每次启动拉钉死的 0.1.46,不要 latest。',
|
|
65
|
+
en: 'Staff Mac zip / Windows exe and approver Docker pin suite 0.1.46. Do not use latest.',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
zh: '安装包只是启动壳,第一次会从外网拉 Node 和 dsh。',
|
|
69
|
+
en: 'The zip/exe is a launcher. The first run downloads Node and dsh.',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
zh: '回形针依赖还在 GitHub。装 suite 要能访问 GitHub;公司机禁外网会失败。',
|
|
73
|
+
en: 'The paperclip plugin still comes from GitHub. Installing suite needs GitHub access.',
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
export function latestSuiteChangelog(): ChangelogRelease {
|
|
82
|
+
const latest = SUITE_CHANGELOG[0]
|
|
83
|
+
if (latest === undefined) throw new Error('SUITE_CHANGELOG is empty')
|
|
84
|
+
return latest
|
|
85
|
+
}
|
package/src/client/locales.ts
CHANGED
|
@@ -6,15 +6,16 @@ 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 /
|
|
9
|
+
'settings.description': '公司 MCP(Odoo / Nextcloud / HA / TAPD / GSC)。企业微信栏暂隐。个人微信在「微信」页签扫码。',
|
|
10
10
|
'panel.title': 'Rez Suite',
|
|
11
11
|
'tab.config': '配置',
|
|
12
12
|
'tab.weixin': '微信',
|
|
13
13
|
'tab.status': '状态',
|
|
14
14
|
'tab.audit': '用量',
|
|
15
|
+
'tab.changelog': '更新日志',
|
|
15
16
|
// config
|
|
16
17
|
'config.role': '角色',
|
|
17
|
-
'config.roleHint': '
|
|
18
|
+
'config.roleHint': '选身份后左侧自动出现工作区。人事/财务/法务助理全员可见;设计房间仅「运营」;实验室仅「工程师」;搞钱/产品/运营仅「老板」。',
|
|
18
19
|
'role.all': '全部',
|
|
19
20
|
'role.boss': '老板',
|
|
20
21
|
'role.engineer': '工程师',
|
|
@@ -58,6 +59,9 @@ export const zh = {
|
|
|
58
59
|
'config.tianyancha': '天眼查(法务助理 · 仅老板)',
|
|
59
60
|
'config.tianyancha.url': '天眼查 MCP URL',
|
|
60
61
|
'config.tianyancha.token': 'OpenAPI Token',
|
|
62
|
+
'config.esphome': 'ESPHome 实验室',
|
|
63
|
+
'config.esphome.url': 'MCP URL',
|
|
64
|
+
'config.esphome.token': '实验室 Token',
|
|
61
65
|
'config.fs': '本地文件 (fs-mcp-server)',
|
|
62
66
|
'config.fs.root': '根目录',
|
|
63
67
|
'config.sqlite': 'SQLite (sqlite-mcp-server)',
|
|
@@ -102,6 +106,8 @@ export const zh = {
|
|
|
102
106
|
'audit.reset': '清空账本',
|
|
103
107
|
'audit.resetConfirm': '确定清空全部用量记录吗?',
|
|
104
108
|
'audit.empty': '暂无记录',
|
|
109
|
+
'changelog.current': '当前 {version}',
|
|
110
|
+
'changelog.upgrade': '升级:',
|
|
105
111
|
// common
|
|
106
112
|
'common.close': '关闭',
|
|
107
113
|
'common.loading': '加载中…',
|
|
@@ -136,14 +142,15 @@ export const en: Record<RezKey, string> = {
|
|
|
136
142
|
'entry.label': 'Rez',
|
|
137
143
|
'entry.tooltip': 'Rez Suite configuration and audit panel',
|
|
138
144
|
'settings.title': 'ReZ-TI',
|
|
139
|
-
'settings.description': 'Company MCP (Odoo / Nextcloud /
|
|
145
|
+
'settings.description': 'Company MCP (Odoo / Nextcloud / HA / TAPD / GSC). WeCom settings are hidden for now. Personal WeChat is the WeChat tab.',
|
|
140
146
|
'panel.title': 'Rez Suite',
|
|
141
147
|
'tab.config': 'Config',
|
|
142
148
|
'tab.weixin': 'WeChat',
|
|
143
149
|
'tab.status': 'Status',
|
|
144
150
|
'tab.audit': 'Usage',
|
|
151
|
+
'tab.changelog': 'Changelog',
|
|
145
152
|
'config.role': 'Role',
|
|
146
|
-
'config.roleHint': 'Identity registers workspaces in the sidebar. Staff assistants for every role; 搞钱 / 产品 / 运营 for Boss only.',
|
|
153
|
+
'config.roleHint': 'Identity registers workspaces in the sidebar. Staff assistants for every role; 设计 for Operations only; 实验室 for Engineer only; 搞钱 / 产品 / 运营 for Boss only.',
|
|
147
154
|
'role.all': 'All',
|
|
148
155
|
'role.boss': 'Boss',
|
|
149
156
|
'role.engineer': 'Engineer',
|
|
@@ -187,6 +194,9 @@ export const en: Record<RezKey, string> = {
|
|
|
187
194
|
'config.tianyancha': 'Tianyancha (Legal assistant, boss only)',
|
|
188
195
|
'config.tianyancha.url': 'Tianyancha MCP URL',
|
|
189
196
|
'config.tianyancha.token': 'OpenAPI token',
|
|
197
|
+
'config.esphome': 'ESPHome lab',
|
|
198
|
+
'config.esphome.url': 'MCP URL',
|
|
199
|
+
'config.esphome.token': 'Lab token',
|
|
190
200
|
'config.fs': 'Local files (fs-mcp-server)',
|
|
191
201
|
'config.fs.root': 'Root directory',
|
|
192
202
|
'config.sqlite': 'SQLite (sqlite-mcp-server)',
|
|
@@ -229,6 +239,8 @@ export const en: Record<RezKey, string> = {
|
|
|
229
239
|
'audit.reset': 'Clear ledger',
|
|
230
240
|
'audit.resetConfirm': 'Clear all usage records?',
|
|
231
241
|
'audit.empty': 'No records',
|
|
242
|
+
'changelog.current': 'Now {version}',
|
|
243
|
+
'changelog.upgrade': 'Upgrade:',
|
|
232
244
|
'common.close': 'Close',
|
|
233
245
|
'common.loading': 'Loading…',
|
|
234
246
|
'common.error': 'Error: {error}',
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings tab: what this suite version changed. Copy lives in src/changelog.ts.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useEffect, useState } from 'react'
|
|
6
|
+
import type { RezApi } from '../api.ts'
|
|
7
|
+
import { SUITE_CHANGELOG, latestSuiteChangelog } from '../../changelog.ts'
|
|
8
|
+
import { zh } from '../locales.ts'
|
|
9
|
+
import { useT } from './helpers.ts'
|
|
10
|
+
import css from './panel.module.css'
|
|
11
|
+
|
|
12
|
+
export function ChangelogTab({ api }: { api: RezApi }) {
|
|
13
|
+
const t = useT()
|
|
14
|
+
const lang = t('tab.config') === zh['tab.config'] ? 'zh' : 'en'
|
|
15
|
+
const latest = latestSuiteChangelog()
|
|
16
|
+
const [installed, setInstalled] = useState('')
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
let cancelled = false
|
|
20
|
+
void api.updateStatus().then((status) => {
|
|
21
|
+
if (!cancelled) setInstalled(status.installed)
|
|
22
|
+
}).catch(() => {
|
|
23
|
+
if (!cancelled) setInstalled(latest.version)
|
|
24
|
+
})
|
|
25
|
+
return () => { cancelled = true }
|
|
26
|
+
}, [api, latest.version])
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className={css.tabBody}>
|
|
30
|
+
<p className={css.message}>
|
|
31
|
+
{t('changelog.current', { version: installed || latest.version })}
|
|
32
|
+
{' · '}
|
|
33
|
+
{t('changelog.upgrade')}
|
|
34
|
+
{' '}
|
|
35
|
+
<code className={css.changelogCode}>dsh plugin --profile web add @rezti/dsh-rez-suite@{latest.version}</code>
|
|
36
|
+
</p>
|
|
37
|
+
{SUITE_CHANGELOG.map((release) => (
|
|
38
|
+
<section key={release.version} className={css.section}>
|
|
39
|
+
<h3 className={css.sectionTitle}>
|
|
40
|
+
{release.version}
|
|
41
|
+
<span className={css.changelogDate}>{release.date}</span>
|
|
42
|
+
</h3>
|
|
43
|
+
{release.sections.map((section) => (
|
|
44
|
+
<div key={section.title[lang]} className={css.changelogBlock}>
|
|
45
|
+
<div className={css.changelogHeading}>{section.title[lang]}</div>
|
|
46
|
+
<ul className={css.changelogList}>
|
|
47
|
+
{section.items.map((item) => (
|
|
48
|
+
<li key={item[lang]}>{item[lang]}</li>
|
|
49
|
+
))}
|
|
50
|
+
</ul>
|
|
51
|
+
</div>
|
|
52
|
+
))}
|
|
53
|
+
</section>
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
@@ -26,9 +26,10 @@ type SecretDrafts = {
|
|
|
26
26
|
gsc: string
|
|
27
27
|
qcc: string
|
|
28
28
|
tianyancha: string
|
|
29
|
+
esphome: string
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
const EMPTY_SECRETS: SecretDrafts = { odoo: '', nextcloud: '', wechat: '', homeassistant: '', tapd: '', gsc: '', qcc: '', tianyancha: '' }
|
|
32
|
+
const EMPTY_SECRETS: SecretDrafts = { odoo: '', nextcloud: '', wechat: '', homeassistant: '', tapd: '', gsc: '', qcc: '', tianyancha: '', esphome: '' }
|
|
32
33
|
|
|
33
34
|
function Field({ label, children }: { label: string; children: ReactNode }) {
|
|
34
35
|
return (
|
|
@@ -177,6 +178,7 @@ export function ConfigTab({ api }: { api: RezApi }) {
|
|
|
177
178
|
gsc: { ...config.connections.gsc, secret: secrets.gsc || undefined },
|
|
178
179
|
qcc: { ...config.connections.qcc, secret: secrets.qcc || undefined },
|
|
179
180
|
tianyancha: { ...config.connections.tianyancha, secret: secrets.tianyancha || undefined },
|
|
181
|
+
esphome: { ...config.connections.esphome, secret: secrets.esphome || undefined },
|
|
180
182
|
},
|
|
181
183
|
}
|
|
182
184
|
const saved = await api.saveConfig(payload)
|
|
@@ -249,14 +251,7 @@ export function ConfigTab({ api }: { api: RezApi }) {
|
|
|
249
251
|
usernameLabel={t('config.nextcloud.username')}
|
|
250
252
|
secretLabel={t('config.nextcloud.appPassword')}
|
|
251
253
|
/>
|
|
252
|
-
|
|
253
|
-
title={t('config.wecom')}
|
|
254
|
-
row={config.connections.wechat}
|
|
255
|
-
secret={secrets.wechat}
|
|
256
|
-
onEnabled={enabled => { patchConnection('wechat', { enabled }) }}
|
|
257
|
-
onSecret={value => { setSecrets(prev => ({ ...prev, wechat: value })) }}
|
|
258
|
-
secretLabel={t('config.wecom.webhook')}
|
|
259
|
-
/>
|
|
254
|
+
{/* WeCom (企业微信) 设置栏暂隐:安装用法未定,TAPD 1154488930001001224。保存时仍回写已有 wechat 配置,不擦掉。 */}
|
|
260
255
|
<McpSection
|
|
261
256
|
title={t('config.ha')}
|
|
262
257
|
row={config.connections.homeassistant}
|
|
@@ -320,6 +315,18 @@ export function ConfigTab({ api }: { api: RezApi }) {
|
|
|
320
315
|
secretLabel={t('config.tianyancha.token')}
|
|
321
316
|
/>
|
|
322
317
|
)}
|
|
318
|
+
{(config.role === 'engineer' || config.role === 'boss') && (
|
|
319
|
+
<McpSection
|
|
320
|
+
title={t('config.esphome')}
|
|
321
|
+
row={config.connections.esphome}
|
|
322
|
+
secret={secrets.esphome}
|
|
323
|
+
onEnabled={enabled => { patchConnection('esphome', { enabled }) }}
|
|
324
|
+
onUrl={url => { patchConnection('esphome', { url }) }}
|
|
325
|
+
onSecret={value => { setSecrets(prev => ({ ...prev, esphome: value })) }}
|
|
326
|
+
urlLabel={t('config.esphome.url')}
|
|
327
|
+
secretLabel={t('config.esphome.token')}
|
|
328
|
+
/>
|
|
329
|
+
)}
|
|
323
330
|
|
|
324
331
|
<Section title={t('config.billing')}>
|
|
325
332
|
<Field label={t('config.billing.input')}>
|
|
@@ -376,3 +376,46 @@
|
|
|
376
376
|
background: var(--dsw-alias-brand-primary);
|
|
377
377
|
flex: none;
|
|
378
378
|
}
|
|
379
|
+
|
|
380
|
+
.changelogDate {
|
|
381
|
+
margin-left: 8px;
|
|
382
|
+
font-weight: 500;
|
|
383
|
+
font-variant-numeric: tabular-nums;
|
|
384
|
+
color: var(--dsw-alias-label-tertiary);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.changelogBlock {
|
|
388
|
+
display: flex;
|
|
389
|
+
flex-direction: column;
|
|
390
|
+
gap: 6px;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.changelogHeading {
|
|
394
|
+
font-size: 12.5px;
|
|
395
|
+
font-weight: 600;
|
|
396
|
+
color: var(--dsw-alias-label-secondary);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.changelogList {
|
|
400
|
+
margin: 0;
|
|
401
|
+
padding-left: 18px;
|
|
402
|
+
display: flex;
|
|
403
|
+
flex-direction: column;
|
|
404
|
+
gap: 6px;
|
|
405
|
+
font-size: 13px;
|
|
406
|
+
line-height: 1.55;
|
|
407
|
+
color: var(--dsw-alias-label-primary);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.changelogList li + li {
|
|
411
|
+
margin: 0;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.changelogCode {
|
|
415
|
+
font-size: 12px;
|
|
416
|
+
padding: 1px 6px;
|
|
417
|
+
border-radius: 6px;
|
|
418
|
+
background: var(--dsw-alias-bg-layer-3);
|
|
419
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
420
|
+
word-break: break-all;
|
|
421
|
+
}
|
|
@@ -10,12 +10,13 @@ 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 { ChangelogTab } from './panel/ChangelogTab.tsx'
|
|
13
14
|
import { ConfigTab } from './panel/ConfigTab.tsx'
|
|
14
15
|
import { StatusTab } from './panel/StatusTab.tsx'
|
|
15
16
|
import { WeixinTab } from './panel/WeixinTab.tsx'
|
|
16
17
|
import css from './panel/panel.module.css'
|
|
17
18
|
|
|
18
|
-
export type RezTab = 'config' | 'weixin' | 'status' | 'audit'
|
|
19
|
+
export type RezTab = 'config' | 'weixin' | 'status' | 'audit' | 'changelog'
|
|
19
20
|
|
|
20
21
|
export function RezSettingsPage({ t }: { t: TranslateNS<'dsh-rez-suite'> }) {
|
|
21
22
|
const api = useMemo(() => new RezApi(), [])
|
|
@@ -25,6 +26,7 @@ export function RezSettingsPage({ t }: { t: TranslateNS<'dsh-rez-suite'> }) {
|
|
|
25
26
|
{ id: 'weixin', label: t('tab.weixin') },
|
|
26
27
|
{ id: 'status', label: t('tab.status') },
|
|
27
28
|
{ id: 'audit', label: t('tab.audit') },
|
|
29
|
+
{ id: 'changelog', label: t('tab.changelog') },
|
|
28
30
|
]
|
|
29
31
|
return (
|
|
30
32
|
<RezT.Provider value={t}>
|
|
@@ -48,6 +50,7 @@ export function RezSettingsPage({ t }: { t: TranslateNS<'dsh-rez-suite'> }) {
|
|
|
48
50
|
{activeTab === 'weixin' && <WeixinTab api={api} />}
|
|
49
51
|
{activeTab === 'status' && <StatusTab api={api} />}
|
|
50
52
|
{activeTab === 'audit' && <AuditTab api={api} />}
|
|
53
|
+
{activeTab === 'changelog' && <ChangelogTab api={api} />}
|
|
51
54
|
</div>
|
|
52
55
|
</RezT.Provider>
|
|
53
56
|
)
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESPHome lab: official dsh-mcp-client → lab MCP, plus HTTP fallback.
|
|
3
|
+
* Lab server runs official `esphome compile`. Never puts firmware bytes
|
|
4
|
+
* into the model context; the tool returns a click-later download link.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { loadRezConnections, readManagedCredential, REZ_CREDENTIAL_REFS, startMcpWhenSecret } from '@rezti/dsh-rez-sso'
|
|
8
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
9
|
+
|
|
10
|
+
export const ESPHOME_MCP_URL = 'http://127.0.0.1:8100/mcp'
|
|
11
|
+
/** POST /v1/compile 只交 YAML、立刻回链接。真机 `esphome compile` 在后台,最长见实验室 `ESPHOME_COMPILE_TIMEOUT`(默认 20 分钟)。 */
|
|
12
|
+
export const ESPHOME_COMPILE_TIMEOUT_MS = 120 * 1000
|
|
13
|
+
|
|
14
|
+
export interface EsphomeCompileResult {
|
|
15
|
+
jobId: string
|
|
16
|
+
device: string
|
|
17
|
+
status: string
|
|
18
|
+
download: string
|
|
19
|
+
direct?: string
|
|
20
|
+
nextcloud?: string
|
|
21
|
+
size?: number
|
|
22
|
+
sha256?: string
|
|
23
|
+
logTail?: string
|
|
24
|
+
error?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function esphomeLabUrl(): string {
|
|
28
|
+
const url = loadRezConnections().esphome?.url
|
|
29
|
+
return typeof url === 'string' && url.length > 0 ? url.replace(/\/+$/, '') : ESPHOME_MCP_URL
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** MCP is :8100/mcp; click-later HTTP lives on :8099. */
|
|
33
|
+
export function esphomeHttpOrigin(mcpUrl: string = esphomeLabUrl()): string {
|
|
34
|
+
try {
|
|
35
|
+
const url = new URL(mcpUrl)
|
|
36
|
+
if (url.port === '8100') url.port = '8099'
|
|
37
|
+
return url.origin
|
|
38
|
+
} catch {
|
|
39
|
+
return 'http://127.0.0.1:8099'
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function labToken(): string | undefined {
|
|
44
|
+
const env = process.env.REZ_ESPHOME_TOKEN
|
|
45
|
+
if (typeof env === 'string' && env.length > 0) return env
|
|
46
|
+
return readManagedCredential(REZ_CREDENTIAL_REFS.esphome)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function esphomeLabConfigured(): boolean {
|
|
50
|
+
const connections = loadRezConnections().esphome
|
|
51
|
+
if (connections?.enabled === false) return false
|
|
52
|
+
return labToken() !== undefined
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function bearerAuthorization(secret: string): string {
|
|
56
|
+
const trimmed = secret.trim()
|
|
57
|
+
if (/^bearer\s+/i.test(trimmed)) return 'Bearer ' + trimmed.replace(/^bearer\s+/i, '')
|
|
58
|
+
return 'Bearer ' + trimmed
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function esphomeMcpConfig(secret: string): Record<string, unknown> {
|
|
62
|
+
return {
|
|
63
|
+
serverName: 'esphome',
|
|
64
|
+
transport: 'streamable-http',
|
|
65
|
+
url: esphomeLabUrl(),
|
|
66
|
+
headers: { Authorization: bearerAuthorization(secret) },
|
|
67
|
+
failOnStartupError: false,
|
|
68
|
+
toolCallTimeoutMs: ESPHOME_COMPILE_TIMEOUT_MS,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface CompileJson {
|
|
73
|
+
ok?: boolean
|
|
74
|
+
id?: string
|
|
75
|
+
device?: string
|
|
76
|
+
status?: string
|
|
77
|
+
download?: string
|
|
78
|
+
direct?: string
|
|
79
|
+
nextcloud?: string
|
|
80
|
+
size?: number
|
|
81
|
+
sha256?: string
|
|
82
|
+
log_tail?: string
|
|
83
|
+
error?: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Submit YAML; return the click-later link. Does not fetch firmware bytes. */
|
|
87
|
+
export async function startEsphomeCompile(input: {
|
|
88
|
+
yaml: string
|
|
89
|
+
fetchImpl?: typeof fetch
|
|
90
|
+
}): Promise<EsphomeCompileResult> {
|
|
91
|
+
const token = labToken()
|
|
92
|
+
if (token === undefined) {
|
|
93
|
+
throw new Error('未配置 REZ_ESPHOME_TOKEN。设置 → 插件 → ReZ-TI → ESPHome 实验室 填 Token。')
|
|
94
|
+
}
|
|
95
|
+
const fetchImpl = input.fetchImpl ?? fetch
|
|
96
|
+
const controller = new AbortController()
|
|
97
|
+
const timer = setTimeout(() => { controller.abort() }, ESPHOME_COMPILE_TIMEOUT_MS)
|
|
98
|
+
try {
|
|
99
|
+
const response = await fetchImpl(esphomeHttpOrigin() + '/v1/compile', {
|
|
100
|
+
method: 'POST',
|
|
101
|
+
headers: {
|
|
102
|
+
authorization: bearerAuthorization(token),
|
|
103
|
+
'content-type': 'application/yaml',
|
|
104
|
+
},
|
|
105
|
+
body: input.yaml,
|
|
106
|
+
signal: controller.signal,
|
|
107
|
+
})
|
|
108
|
+
const compiled = await response.json() as CompileJson
|
|
109
|
+
if (!response.ok || compiled.ok !== true || typeof compiled.id !== 'string') {
|
|
110
|
+
throw new Error(compiled.error ?? ('compile HTTP ' + response.status))
|
|
111
|
+
}
|
|
112
|
+
const download = compiled.download
|
|
113
|
+
?? compiled.nextcloud
|
|
114
|
+
?? compiled.direct
|
|
115
|
+
?? (esphomeHttpOrigin() + '/v1/jobs/' + compiled.id + '/download')
|
|
116
|
+
return {
|
|
117
|
+
jobId: compiled.id,
|
|
118
|
+
device: compiled.device ?? 'device',
|
|
119
|
+
status: compiled.status ?? 'compiling',
|
|
120
|
+
download,
|
|
121
|
+
direct: compiled.direct,
|
|
122
|
+
nextcloud: compiled.nextcloud,
|
|
123
|
+
size: compiled.size,
|
|
124
|
+
sha256: compiled.sha256,
|
|
125
|
+
logTail: compiled.log_tail,
|
|
126
|
+
error: compiled.error,
|
|
127
|
+
}
|
|
128
|
+
} finally {
|
|
129
|
+
clearTimeout(timer)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** @deprecated use startEsphomeCompile */
|
|
134
|
+
export const compileEsphomeFirmware = startEsphomeCompile
|
|
135
|
+
|
|
136
|
+
/** Start for every identity; execution is room-gated to 实验室. */
|
|
137
|
+
export function mountEsphomeMcp(
|
|
138
|
+
ctx: Context,
|
|
139
|
+
start: typeof startMcpWhenSecret = startMcpWhenSecret,
|
|
140
|
+
): () => void {
|
|
141
|
+
let session: (() => void) | undefined
|
|
142
|
+
let pending = false
|
|
143
|
+
let retryTimer: ReturnType<typeof setTimeout> | undefined
|
|
144
|
+
|
|
145
|
+
const hasRezSso = (): boolean => {
|
|
146
|
+
const host = ctx as Context & { get?: (name: string) => unknown }
|
|
147
|
+
if (typeof host.get !== 'function') return true
|
|
148
|
+
return host.get('rezSso') !== undefined
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const ensure = (): void => {
|
|
152
|
+
void (async () => {
|
|
153
|
+
if (session !== undefined || pending) return
|
|
154
|
+
if (!hasRezSso()) {
|
|
155
|
+
if (retryTimer === undefined) {
|
|
156
|
+
retryTimer = setTimeout(() => {
|
|
157
|
+
retryTimer = undefined
|
|
158
|
+
ensure()
|
|
159
|
+
}, 300)
|
|
160
|
+
}
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
pending = true
|
|
164
|
+
try {
|
|
165
|
+
session = await start(ctx, {
|
|
166
|
+
system: 'esphome',
|
|
167
|
+
label: 'ESPHome 实验室',
|
|
168
|
+
config: esphomeMcpConfig,
|
|
169
|
+
})
|
|
170
|
+
} catch (error: unknown) {
|
|
171
|
+
const why = error instanceof Error ? error.message : String(error)
|
|
172
|
+
console.error('[dsh-rez-suite] esphome MCP start failed:', why)
|
|
173
|
+
} finally {
|
|
174
|
+
pending = false
|
|
175
|
+
}
|
|
176
|
+
})()
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
ensure()
|
|
180
|
+
return ensure
|
|
181
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -25,13 +25,14 @@ import {
|
|
|
25
25
|
import { makeRoutes } from './routes.ts'
|
|
26
26
|
import { defaultConfig, mergeConfig, saveConfig } from './store.ts'
|
|
27
27
|
import { TokenManager } from './token-manager.ts'
|
|
28
|
-
import { rezConfigTool, rezStatusTool } from './tools.ts'
|
|
28
|
+
import { rezConfigTool, rezEsphomeCompileTool, rezStatusTool } from './tools.ts'
|
|
29
29
|
import type { RezConfig } from './protocol.ts'
|
|
30
30
|
import { workspaceGuidanceFor, mountRezWorkspaces } from './boss/mount.ts'
|
|
31
31
|
import { liveSelfUpdate } from './self-update.ts'
|
|
32
32
|
import { mountTianyanchaMcp } from './tianyancha.ts'
|
|
33
33
|
import { mountQccMcp, QCC_COMPANY_MCP_URL } from './qcc.ts'
|
|
34
|
-
import {
|
|
34
|
+
import { mountEsphomeMcp } from './esphome-lab.ts'
|
|
35
|
+
import { suiteRoomGuard } from './rooms.ts'
|
|
35
36
|
|
|
36
37
|
export const name = 'rez-suite'
|
|
37
38
|
|
|
@@ -90,6 +91,10 @@ export const Config: z<RezConfig> = z.object({
|
|
|
90
91
|
enabled: z.boolean().default(true),
|
|
91
92
|
url: z.string().default(REZ_DEFAULT_CONNECTIONS.tianyancha?.url ?? 'https://mcp.tianyancha.com/mcp'),
|
|
92
93
|
}).default(REZ_DEFAULT_CONNECTIONS.tianyancha ?? { enabled: true, url: 'https://mcp.tianyancha.com/mcp' }),
|
|
94
|
+
esphome: z.object({
|
|
95
|
+
enabled: z.boolean().default(true),
|
|
96
|
+
url: z.string().default(REZ_DEFAULT_CONNECTIONS.esphome?.url ?? 'http://127.0.0.1:8100/mcp'),
|
|
97
|
+
}).default(REZ_DEFAULT_CONNECTIONS.esphome ?? { enabled: true, url: 'http://127.0.0.1:8100/mcp' }),
|
|
93
98
|
}).default(REZ_DEFAULT_CONNECTIONS),
|
|
94
99
|
billing: z.object({
|
|
95
100
|
inputCostPer1k: z.number().default(0.001),
|
|
@@ -109,6 +114,9 @@ export const REZ_GUIDANCE_CORE = [
|
|
|
109
114
|
'涉及资金、对外发送、设备控制、生产写入时先征得用户批准。',
|
|
110
115
|
'Match the user language: Chinese or English. Do not force 中文 when they write English.',
|
|
111
116
|
'制度、手册、出差/报销标准、素材路径:打开 Nextcloud Documents/Standards/INDEX.yaml,按 code 取当前 effective_from 最新且未被 replace 的条目,再按相对 uri 打开。引用带路径+生效日。找不到就说「标准文件未归档」,不要用 MEMORY 旧数字。素材文件夹用首次创建标题,不用会变的 Odoo/销售名。',
|
|
117
|
+
'同事交说明书/规格书:用输入栏回形针上传原 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 未归档」。这不是一件发布;不要拷下游、不要上渠道。',
|
|
118
|
+
'同事交拍摄原片:对话里交图即可。先调官方 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/ 根目录或产品根目录样板不强制搬家。',
|
|
119
|
+
'同事交品牌规范或公司 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 正文。',
|
|
112
120
|
].join('')
|
|
113
121
|
|
|
114
122
|
export function rezGuidanceFor(role: RezConfig['role']): string {
|
|
@@ -144,18 +152,20 @@ export function apply(ctx: Context, config?: RezConfig): void {
|
|
|
144
152
|
const syncWorkspaces = mountRezWorkspaces(ctx, () => current().role)
|
|
145
153
|
const syncQcc = mountQccMcp(ctx)
|
|
146
154
|
const syncTianyancha = mountTianyanchaMcp(ctx, () => current().role)
|
|
155
|
+
const syncEsphome = mountEsphomeMcp(ctx)
|
|
147
156
|
|
|
148
157
|
const tokens = new TokenManager(joinTokenDb())
|
|
149
158
|
const host = delegatedHost(ctx, () => current())
|
|
150
159
|
ctx.effect(() => () => { tokens.close() }, 'dsh-rez-suite: ledger view')
|
|
151
|
-
const toolsHost = ctx.tools as { guard?: (guard: typeof
|
|
160
|
+
const toolsHost = ctx.tools as { guard?: (guard: typeof suiteRoomGuard) => () => void }
|
|
152
161
|
if (typeof toolsHost.guard === 'function') {
|
|
153
|
-
ctx.effect(() => toolsHost.guard!(
|
|
162
|
+
ctx.effect(() => toolsHost.guard!(suiteRoomGuard), 'dsh-rez-suite: room guard')
|
|
154
163
|
}
|
|
155
164
|
|
|
156
165
|
const tools = [
|
|
157
166
|
rezStatusTool(host, () => current()),
|
|
158
167
|
rezConfigTool(() => current()),
|
|
168
|
+
rezEsphomeCompileTool(),
|
|
159
169
|
]
|
|
160
170
|
|
|
161
171
|
const routes = makeRoutes({
|
|
@@ -167,6 +177,7 @@ export function apply(ctx: Context, config?: RezConfig): void {
|
|
|
167
177
|
syncWorkspaces()
|
|
168
178
|
syncQcc()
|
|
169
179
|
syncTianyancha()
|
|
180
|
+
syncEsphome()
|
|
170
181
|
try {
|
|
171
182
|
const settings = ctx.get('settings') as { update?: (namespace: unknown, value: unknown) => Promise<void> } | undefined
|
|
172
183
|
if (settings?.update !== undefined) await settings.update(REZ_SETTINGS_NAMESPACE, value)
|
|
@@ -222,12 +233,14 @@ export function apply(ctx: Context, config?: RezConfig): void {
|
|
|
222
233
|
syncWorkspaces()
|
|
223
234
|
syncQcc()
|
|
224
235
|
syncTianyancha()
|
|
236
|
+
syncEsphome()
|
|
225
237
|
},
|
|
226
238
|
onChange: () => {
|
|
227
239
|
sync()
|
|
228
240
|
syncWorkspaces()
|
|
229
241
|
syncQcc()
|
|
230
242
|
syncTianyancha()
|
|
243
|
+
syncEsphome()
|
|
231
244
|
},
|
|
232
245
|
})
|
|
233
246
|
|
package/src/observe-mcp.ts
CHANGED
|
@@ -8,12 +8,15 @@
|
|
|
8
8
|
|
|
9
9
|
import { lastMcpStartError, REZ_CREDENTIAL_REFS, secretConfigured, type RezSystem } from '@rezti/dsh-rez-sso'
|
|
10
10
|
import type { RezConfig, RezServerStatus, RezTestResult } from './protocol.ts'
|
|
11
|
-
import { isLegalAssistantRoom, isQccToolName, QCC_STATUS_ROOM_OFF } from './rooms.ts'
|
|
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'] as const
|
|
13
|
+
export const COMPOSED_SERVERS = ['odoo', 'nextcloud', 'wechat', 'homeassistant', 'tapd', 'gsc', 'qcc', 'tianyancha', 'esphome'] as const
|
|
14
14
|
|
|
15
15
|
export type ComposedServer = (typeof COMPOSED_SERVERS)[number]
|
|
16
16
|
|
|
17
|
+
/** Front panel hides these until install/docs are ready. Backend MCP can still start if a secret exists. TAPD 1154488930001001224. */
|
|
18
|
+
export const FRONT_HIDDEN_SERVERS: ReadonlySet<ComposedServer> = new Set(['wechat'])
|
|
19
|
+
|
|
17
20
|
export function mcpToolPrefix(server: string): string {
|
|
18
21
|
if (server === 'qcc') return 'mcp__qcc-'
|
|
19
22
|
return 'mcp__' + server + '__'
|
|
@@ -109,13 +112,16 @@ export function observeServer(
|
|
|
109
112
|
}
|
|
110
113
|
|
|
111
114
|
function composedServersFor(input: ObserveInput): ComposedServer[] {
|
|
112
|
-
|
|
113
|
-
? COMPOSED_SERVERS
|
|
115
|
+
let servers: ComposedServer[] = input.config.role === 'boss'
|
|
116
|
+
? [...COMPOSED_SERVERS]
|
|
114
117
|
: COMPOSED_SERVERS.filter((server) => server !== 'tianyancha')
|
|
115
118
|
if (input.room !== undefined && !isLegalAssistantRoom(input.room)) {
|
|
116
|
-
|
|
119
|
+
servers = servers.filter((server) => server !== 'qcc')
|
|
120
|
+
}
|
|
121
|
+
if (input.room !== undefined && !isLabRoom(input.room)) {
|
|
122
|
+
servers = servers.filter((server) => server !== 'esphome')
|
|
117
123
|
}
|
|
118
|
-
return
|
|
124
|
+
return servers.filter((server) => !FRONT_HIDDEN_SERVERS.has(server))
|
|
119
125
|
}
|
|
120
126
|
|
|
121
127
|
function observeQccOutsideLegalRoom(input: ObserveInput): RezServerStatus {
|