@rezti/dsh-rez-suite 0.1.18 → 0.1.20

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/lib/style.css CHANGED
@@ -287,3 +287,84 @@
287
287
  font-size: 15px;
288
288
  font-weight: 600;
289
289
  }
290
+
291
+ .LOjPTW_upgradeLayer {
292
+ flex: none;
293
+ align-items: center;
294
+ width: 100%;
295
+ height: 42px;
296
+ margin: 8px 0 0;
297
+ display: flex;
298
+ position: relative;
299
+ }
300
+
301
+ .LOjPTW_upgradeLayerRail {
302
+ width: 36px;
303
+ height: 36px;
304
+ margin: 0;
305
+ }
306
+
307
+ .LOjPTW_upgradeButton {
308
+ width: calc(100% + 4px);
309
+ height: 42px;
310
+ color: var(--dsw-alias-label-primary);
311
+ cursor: pointer;
312
+ background: none;
313
+ border: none;
314
+ border-radius: 12px;
315
+ align-items: center;
316
+ gap: 8px;
317
+ margin: 0 -2px;
318
+ padding: 0 10px 0 8px;
319
+ font-family: inherit;
320
+ font-size: 14px;
321
+ display: inline-flex;
322
+ overflow: hidden;
323
+ }
324
+
325
+ .LOjPTW_upgradeButton:hover, .LOjPTW_upgradeButton[data-outdated] {
326
+ background: var(--dsw-alias-interactive-bg-hover);
327
+ }
328
+
329
+ .LOjPTW_upgradeButton:disabled {
330
+ cursor: progress;
331
+ opacity: .72;
332
+ }
333
+
334
+ .LOjPTW_upgradeButton:focus-visible {
335
+ outline: 2px solid var(--dsw-alias-brand-primary);
336
+ outline-offset: -2px;
337
+ }
338
+
339
+ .LOjPTW_upgradeLayerRail .LOjPTW_upgradeButton {
340
+ border-radius: 50%;
341
+ justify-content: center;
342
+ gap: 0;
343
+ width: 36px;
344
+ height: 36px;
345
+ padding: 0;
346
+ }
347
+
348
+ .LOjPTW_upgradeLabel {
349
+ text-overflow: ellipsis;
350
+ white-space: nowrap;
351
+ min-width: 0;
352
+ overflow: hidden;
353
+ }
354
+
355
+ .LOjPTW_upgradeMeta {
356
+ color: var(--dsw-alias-label-tertiary);
357
+ font-variant-numeric: tabular-nums;
358
+ flex: none;
359
+ margin-left: auto;
360
+ font-size: 12px;
361
+ line-height: 16px;
362
+ }
363
+
364
+ .LOjPTW_upgradeDot {
365
+ background: var(--dsw-alias-brand-primary);
366
+ border-radius: 50%;
367
+ flex: none;
368
+ width: 7px;
369
+ height: 7px;
370
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rezti/dsh-rez-suite",
3
3
  "description": "ReZ-TI one-package install for DeepSeek Harness. Update with: dsh plugin --profile web update @rezti/dsh-rez-suite",
4
- "version": "0.1.18",
4
+ "version": "0.1.20",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -30,7 +30,8 @@
30
30
  "@deepseek-ai/dsh-client-connection",
31
31
  "@deepseek-ai/dsh-client-locale",
32
32
  "@deepseek-ai/dsh-client-ui-settings",
33
- "@deepseek-ai/dsh-client-ui-settings-plugins"
33
+ "@deepseek-ai/dsh-client-ui-settings-plugins",
34
+ "@deepseek-ai/dsh-client-ui-sidebar"
34
35
  ],
35
36
  "platform": "web"
36
37
  }
@@ -38,13 +39,13 @@
38
39
  "dependencies": {
39
40
  "@modelcontextprotocol/sdk": "^1.30.0",
40
41
  "zod": "^4.4.3",
41
- "@rezti/dsh-rez-ha-bridge": "0.1.7",
42
- "@rezti/dsh-rez-intent": "0.1.4",
43
- "@rezti/dsh-rez-nextcloud": "0.1.7",
44
42
  "@rezti/dsh-rez-token-manager": "0.1.4",
43
+ "@rezti/dsh-rez-sso": "0.1.7",
44
+ "@rezti/dsh-rez-intent": "0.1.4",
45
45
  "@rezti/dsh-rez-wechat": "0.1.11",
46
- "@rezti/dsh-rez-odoo": "0.1.7",
47
- "@rezti/dsh-rez-sso": "0.1.7"
46
+ "@rezti/dsh-rez-ha-bridge": "0.1.7",
47
+ "@rezti/dsh-rez-nextcloud": "0.1.7",
48
+ "@rezti/dsh-rez-odoo": "0.1.7"
48
49
  },
49
50
  "peerDependencies": {
50
51
  "react": "^18.2.0",
package/src/boss/mount.ts CHANGED
@@ -1,31 +1,64 @@
1
1
  import { homedir } from 'node:os'
2
2
  import { join } from 'node:path'
3
3
  import type { Context } from '@deepseek-ai/cordis'
4
- import { registerBossWorkspaces, type WorkspaceRegistrar } from './register.ts'
5
- import { seedBossWorkspaces } from './seed.ts'
4
+ import type { RezRoleId } from '../protocol.ts'
5
+ import {
6
+ registerBossWorkspaces,
7
+ unregisterBossWorkspaces,
8
+ type BossRoomRef,
9
+ type WorkspaceRegistrar,
10
+ } from './register.ts'
11
+ import { BOSS_ROOMS, seedBossWorkspaces } from './seed.ts'
12
+
13
+ export const BOSS_ROLE_HINT = '设置 → 插件 → ReZ-TI → 角色选「老板」后,左侧会出现搞钱 / 产品 / 运营三室。不必手动「添加工作区」。'
6
14
 
7
15
  export const BOSS_GUIDANCE = [
8
- '老板三室已播种并登记为侧栏工作区:~/.dsh/workspaces/搞钱、产品、运营。不必手动「添加工作区」。',
9
- '设置 → 插件 → ReZ-TI → 角色选「老板」。每个文件夹是独立作战室,官方 dsh 会注入该目录 AGENTS.md。',
16
+ '当前角色是老板。侧栏已登记三室:~/.dsh/workspaces/搞钱、产品、运营。不必手动「添加工作区」。',
17
+ '每个文件夹是独立作战室,官方 dsh 会注入该目录 AGENTS.md。',
10
18
  '若该室仍有 BOOTSTRAP.md:本会话先做短问三拍,沉淀到 PRIORITIES.md / MEMORY.md,然后删除 BOOTSTRAP.md。',
11
19
  '已初始化后:读 PRIORITIES.md + MEMORY.md + 今日/昨日 memory/;回答固定四行:结论 → 数字(哪家公司、哪段时间)→ 拍板 1~3 件事 → 未执行任何过账/付款/对外发送。',
12
20
  '微信文件夹仍是收件箱,不要在那边查账或砍 SKU。',
13
21
  ].join('')
14
22
 
23
+ export function bossGuidanceFor(role: RezRoleId): string {
24
+ return role === 'boss' ? BOSS_GUIDANCE : BOSS_ROLE_HINT
25
+ }
26
+
15
27
  function destRoot(env: NodeJS.ProcessEnv = process.env): string {
16
28
  if (env.REZ_BOSS_WORKSPACES !== undefined && env.REZ_BOSS_WORKSPACES !== '') return env.REZ_BOSS_WORKSPACES
17
29
  const home = env.DSH_HOME !== undefined && env.DSH_HOME !== '' ? env.DSH_HOME : join(homedir(), '.dsh')
18
30
  return join(home, 'workspaces')
19
31
  }
20
32
 
21
- /** Seed files, then register official workspaces once workspaceRegistry is ready. */
22
- export function mountBossDesk(ctx: Context): void {
23
- const seeded = seedBossWorkspaces({ destRoot: destRoot() })
24
- ctx.inject(['workspaceRegistry'], (inner) => {
25
- const registry = inner.get('workspaceRegistry') as WorkspaceRegistrar
26
- void registerBossWorkspaces(registry, seeded.rooms).catch((error: unknown) => {
27
- const why = error instanceof Error ? error.message : String(error)
28
- console.error('[dsh-rez-suite] workspaceRegistry.create failed:', why)
33
+ export function expectedBossRooms(env: NodeJS.ProcessEnv = process.env): BossRoomRef[] {
34
+ const root = destRoot(env)
35
+ return BOSS_ROOMS.map((room) => ({ path: join(root, room.folder), folder: room.folder }))
36
+ }
37
+
38
+ export async function syncBossWorkspaces(
39
+ registry: WorkspaceRegistrar,
40
+ role: RezRoleId,
41
+ env: NodeJS.ProcessEnv = process.env,
42
+ ): Promise<void> {
43
+ if (role === 'boss') {
44
+ const seeded = seedBossWorkspaces({ destRoot: destRoot(env) })
45
+ await registerBossWorkspaces(registry, seeded.rooms)
46
+ return
47
+ }
48
+ await unregisterBossWorkspaces(registry, expectedBossRooms(env))
49
+ }
50
+
51
+ /** Seed/register 三室 only for role=boss; drop sidebar rows otherwise. Returns a re-sync function. */
52
+ export function mountBossDesk(ctx: Context, getRole: () => RezRoleId): () => void {
53
+ const run = (): void => {
54
+ ctx.inject(['workspaceRegistry'], (inner) => {
55
+ const registry = inner.get('workspaceRegistry') as WorkspaceRegistrar
56
+ void syncBossWorkspaces(registry, getRole()).catch((error: unknown) => {
57
+ const why = error instanceof Error ? error.message : String(error)
58
+ console.error('[dsh-rez-suite] boss workspace sync failed:', why)
59
+ })
29
60
  })
30
- })
61
+ }
62
+ run()
63
+ return run
31
64
  }
@@ -1,18 +1,54 @@
1
1
  /**
2
- * Register seeded room directories as official dsh workspaces.
2
+ * Register or drop the three boss rooms in the official workspace sidebar.
3
3
  * `workspaceRegistry.create` is idempotent: same canonical path returns the
4
4
  * existing record. New records are prepended, so we register in reverse to
5
5
  * keep 搞钱 → 产品 → 运营 at the top of the sidebar.
6
+ * `delete` only drops the registry row — directories and session logs stay.
6
7
  */
8
+ import { realpathSync } from 'node:fs'
9
+
10
+ export interface WorkspaceRecord {
11
+ id: string
12
+ path: string
13
+ title: string
14
+ }
15
+
7
16
  export interface WorkspaceRegistrar {
8
17
  create(path: string, title?: string): Promise<unknown>
18
+ list(): WorkspaceRecord[]
19
+ delete(id: string): Promise<boolean>
20
+ }
21
+
22
+ export type BossRoomRef = { path: string; folder: string }
23
+
24
+ function canon(path: string): string {
25
+ try {
26
+ return realpathSync(path)
27
+ } catch {
28
+ return path
29
+ }
30
+ }
31
+
32
+ export function isRegisteredBossRoom(workspace: WorkspaceRecord, rooms: ReadonlyArray<BossRoomRef>): boolean {
33
+ const wanted = new Set(rooms.map((room) => canon(room.path)))
34
+ return wanted.has(canon(workspace.path))
9
35
  }
10
36
 
11
37
  export async function registerBossWorkspaces(
12
38
  registry: WorkspaceRegistrar,
13
- rooms: ReadonlyArray<{ path: string; folder: string }>,
39
+ rooms: ReadonlyArray<BossRoomRef>,
14
40
  ): Promise<void> {
15
41
  for (const room of [...rooms].reverse()) {
16
42
  await registry.create(room.path, room.folder)
17
43
  }
18
44
  }
45
+
46
+ export async function unregisterBossWorkspaces(
47
+ registry: WorkspaceRegistrar,
48
+ rooms: ReadonlyArray<BossRoomRef>,
49
+ ): Promise<void> {
50
+ for (const workspace of registry.list()) {
51
+ if (!isRegisteredBossRoom(workspace, rooms)) continue
52
+ await registry.delete(workspace.id)
53
+ }
54
+ }
package/src/client/api.ts CHANGED
@@ -9,6 +9,8 @@ import {
9
9
  type RezServerStatus,
10
10
  type RezStatusResponse,
11
11
  type RezTestResult,
12
+ type RezUpdateApplyResult,
13
+ type RezUpdateStatus,
12
14
  type RezWeixinStatus,
13
15
  } from '../protocol.ts'
14
16
 
@@ -97,4 +99,14 @@ export class RezApi {
97
99
  const response = await fetch(REZ_API.weixin, { method: 'DELETE' })
98
100
  return readJson<RezWeixinStatus>(response)
99
101
  }
102
+
103
+ async updateStatus(): Promise<RezUpdateStatus> {
104
+ const response = await fetch(REZ_API.update)
105
+ return readJson<RezUpdateStatus>(response)
106
+ }
107
+
108
+ async applyUpdate(): Promise<RezUpdateApplyResult> {
109
+ const response = await fetch(REZ_API.update, { method: 'POST' })
110
+ return readJson<RezUpdateApplyResult>(response)
111
+ }
100
112
  }
@@ -11,6 +11,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
11
11
  import type {} from '@deepseek-ai/dsh-client-ui-slots'
12
12
  import { en, zh, type RezKey } from './locales.ts'
13
13
  import { RezPluginCard, RezSettingsPage } from './settings-card.tsx'
14
+ import { UpgradeButton } from './upgrade-button.tsx'
14
15
 
15
16
  const NS = 'dsh-rez-suite'
16
17
 
@@ -29,6 +30,11 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
29
30
  scope: 'root'
30
31
  owner: { children?: never }
31
32
  }
33
+ 'sidebar.footer.action': {
34
+ kind: 'list'
35
+ scope: 'root'
36
+ owner: { wide: boolean }
37
+ }
32
38
  }
33
39
  }
34
40
 
@@ -53,4 +59,12 @@ export function apply(ctx: ClientContext): void {
53
59
  label: () => t('settings.title'),
54
60
  locale: NS,
55
61
  }, RezSettingsPage))
62
+
63
+ ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({
64
+ name: 'sidebar.footer.action',
65
+ id: 'rez-upgrade',
66
+ order: 10,
67
+ label: () => t('upgrade.label'),
68
+ locale: NS,
69
+ }, UpgradeButton))
56
70
  }
@@ -104,6 +104,13 @@ export const zh = {
104
104
  'weixin.listening': '通道在听:微信 → 网页「微信」会话(同一条,不另开窗口)→ 回微信。',
105
105
  'weixin.loggedIn': '已登录,通道未在听。点「重新连接」即可拉起,不必再扫码。',
106
106
  'weixin.loggedOut': '尚未登录。',
107
+ 'upgrade.label': '升级',
108
+ 'upgrade.current': '已最新',
109
+ 'upgrade.aria': '升级 ReZ-TI 插件',
110
+ 'upgrade.running': '升级中…',
111
+ 'upgrade.reloading': '正在重载',
112
+ 'upgrade.already': '已是 {version}',
113
+ 'upgrade.reloadTimeout': '升级完成,但网页还没重新连上。请刷新这一页。',
107
114
  } as const
108
115
 
109
116
  export type RezKey = keyof typeof zh
@@ -206,6 +213,13 @@ export const en: Record<RezKey, string> = {
206
213
  'weixin.listening': 'Channel is live: WeChat → the WeChat folder session on the left → WeChat.',
207
214
  'weixin.loggedIn': 'Logged in, listener idle. Click Reconnect — no need to scan again.',
208
215
  'weixin.loggedOut': 'Not logged in.',
216
+ 'upgrade.label': 'Upgrade',
217
+ 'upgrade.current': 'Up to date',
218
+ 'upgrade.aria': 'Upgrade the ReZ-TI plugin',
219
+ 'upgrade.running': 'Updating…',
220
+ 'upgrade.reloading': 'Reloading',
221
+ 'upgrade.already': 'Already {version}',
222
+ 'upgrade.reloadTimeout': 'Update finished, but the page did not reconnect. Refresh this tab.',
209
223
  }
210
224
 
211
225
  /** Minimal {name} interpolator shared by the panel helpers. */
@@ -294,3 +294,85 @@
294
294
  font-weight: 600;
295
295
  color: var(--dsw-alias-label-primary);
296
296
  }
297
+
298
+ .upgradeLayer {
299
+ flex: none;
300
+ align-items: center;
301
+ width: 100%;
302
+ height: 42px;
303
+ margin: 8px 0 0;
304
+ display: flex;
305
+ position: relative;
306
+ }
307
+
308
+ .upgradeLayerRail {
309
+ width: 36px;
310
+ height: 36px;
311
+ margin: 0;
312
+ }
313
+
314
+ .upgradeButton {
315
+ width: calc(100% + 4px);
316
+ height: 42px;
317
+ color: var(--dsw-alias-label-primary);
318
+ cursor: pointer;
319
+ background: transparent;
320
+ border: none;
321
+ border-radius: 12px;
322
+ align-items: center;
323
+ gap: 8px;
324
+ margin: 0 -2px;
325
+ padding: 0 10px 0 8px;
326
+ font-family: inherit;
327
+ font-size: 14px;
328
+ display: inline-flex;
329
+ overflow: hidden;
330
+ }
331
+
332
+ .upgradeButton:hover,
333
+ .upgradeButton[data-outdated] {
334
+ background: var(--dsw-alias-interactive-bg-hover);
335
+ }
336
+
337
+ .upgradeButton:disabled {
338
+ cursor: progress;
339
+ opacity: 0.72;
340
+ }
341
+
342
+ .upgradeButton:focus-visible {
343
+ outline: 2px solid var(--dsw-alias-brand-primary);
344
+ outline-offset: -2px;
345
+ }
346
+
347
+ .upgradeLayerRail .upgradeButton {
348
+ border-radius: 50%;
349
+ justify-content: center;
350
+ gap: 0;
351
+ width: 36px;
352
+ height: 36px;
353
+ padding: 0;
354
+ }
355
+
356
+ .upgradeLabel {
357
+ text-overflow: ellipsis;
358
+ white-space: nowrap;
359
+ min-width: 0;
360
+ overflow: hidden;
361
+ }
362
+
363
+ .upgradeMeta {
364
+ color: var(--dsw-alias-label-tertiary);
365
+ font-variant-numeric: tabular-nums;
366
+ flex: none;
367
+ margin-left: auto;
368
+ font-size: 12px;
369
+ line-height: 16px;
370
+ }
371
+
372
+ .upgradeDot {
373
+ width: 7px;
374
+ height: 7px;
375
+ border-radius: 50%;
376
+ background: var(--dsw-alias-brand-primary);
377
+ flex: none;
378
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Sidebar footer action: upgrade @rezti/dsh-rez-suite without a terminal.
3
+ * Host code still needs a process respawn; the button does that after pnpm add.
4
+ */
5
+
6
+ import { useEffect, useMemo, useState } from 'react'
7
+ import { RezApi } from './api.ts'
8
+ import { tt } from './panel/helpers.ts'
9
+ import css from './panel/panel.module.css'
10
+ import type { RezUpdateStatus } from '../protocol.ts'
11
+
12
+ function UpgradeIcon() {
13
+ return (
14
+ <svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true">
15
+ <path
16
+ d="M8 12.5V4.8M8 4.8 4.8 8M8 4.8 11.2 8M3 13.2h10"
17
+ fill="none"
18
+ stroke="currentColor"
19
+ strokeWidth="1.4"
20
+ strokeLinecap="round"
21
+ strokeLinejoin="round"
22
+ />
23
+ </svg>
24
+ )
25
+ }
26
+
27
+ async function waitUntilHostBack(api: RezApi, timeoutMs = 60_000): Promise<void> {
28
+ const deadline = Date.now() + timeoutMs
29
+ await new Promise((resolve) => setTimeout(resolve, 800))
30
+ while (Date.now() < deadline) {
31
+ try {
32
+ await api.updateStatus()
33
+ return
34
+ } catch {
35
+ await new Promise((resolve) => setTimeout(resolve, 700))
36
+ }
37
+ }
38
+ throw new Error(tt('upgrade.reloadTimeout'))
39
+ }
40
+
41
+ export function UpgradeButton({ wide }: { wide: boolean }) {
42
+ const api = useMemo(() => new RezApi(), [])
43
+ const [info, setInfo] = useState<RezUpdateStatus>()
44
+ const [busy, setBusy] = useState(false)
45
+ const [note, setNote] = useState('')
46
+
47
+ useEffect(() => {
48
+ let cancelled = false
49
+ void api.updateStatus().then((value) => {
50
+ if (!cancelled) setInfo(value)
51
+ }).catch((error: unknown) => {
52
+ if (!cancelled) setNote(error instanceof Error ? error.message : String(error))
53
+ })
54
+ return () => { cancelled = true }
55
+ }, [api])
56
+
57
+ const label = busy
58
+ ? tt('upgrade.running')
59
+ : info?.outdated === true
60
+ ? tt('upgrade.label')
61
+ : tt('upgrade.current')
62
+
63
+ const meta = note !== ''
64
+ ? note
65
+ : info === undefined
66
+ ? ''
67
+ : info.outdated
68
+ ? info.latest
69
+ : info.installed
70
+
71
+ async function onClick(): Promise<void> {
72
+ if (busy) return
73
+ setBusy(true)
74
+ setNote('')
75
+ try {
76
+ const snapshot = info ?? await api.updateStatus()
77
+ setInfo(snapshot)
78
+ if (!snapshot.outdated) {
79
+ setNote(tt('upgrade.already', { version: snapshot.installed }))
80
+ return
81
+ }
82
+ const result = await api.applyUpdate()
83
+ setInfo(result)
84
+ if (result.restarting) {
85
+ setNote(tt('upgrade.reloading'))
86
+ await waitUntilHostBack(api)
87
+ window.location.reload()
88
+ return
89
+ }
90
+ setNote(tt('upgrade.already', { version: result.installed }))
91
+ } catch (error) {
92
+ setNote(error instanceof Error ? error.message : String(error))
93
+ } finally {
94
+ setBusy(false)
95
+ }
96
+ }
97
+
98
+ return (
99
+ <div className={wide ? css.upgradeLayer : `${css.upgradeLayer} ${css.upgradeLayerRail}`}>
100
+ <button
101
+ type="button"
102
+ className={css.upgradeButton}
103
+ data-outdated={info?.outdated === true ? '' : undefined}
104
+ aria-label={tt('upgrade.aria')}
105
+ disabled={busy}
106
+ onClick={() => { void onClick() }}
107
+ >
108
+ <UpgradeIcon />
109
+ {wide && <span className={css.upgradeLabel}>{label}</span>}
110
+ {wide && meta !== '' && <span className={css.upgradeMeta}>{meta}</span>}
111
+ {!wide && info?.outdated === true && <span className={css.upgradeDot} />}
112
+ </button>
113
+ </div>
114
+ )
115
+ }
package/src/index.ts CHANGED
@@ -27,7 +27,8 @@ import { defaultConfig, mergeConfig, saveConfig } from './store.ts'
27
27
  import { TokenManager } from './token-manager.ts'
28
28
  import { rezConfigTool, rezStatusTool } from './tools.ts'
29
29
  import type { RezConfig } from './protocol.ts'
30
- import { BOSS_GUIDANCE, mountBossDesk } from './boss/mount.ts'
30
+ import { bossGuidanceFor, mountBossDesk } from './boss/mount.ts'
31
+ import { liveSelfUpdate } from './self-update.ts'
31
32
 
32
33
  export const name = 'rez-suite'
33
34
 
@@ -79,16 +80,19 @@ export const Config: z<RezConfig> = z.object({
79
80
 
80
81
  const SECTION_ORDER = 150
81
82
 
82
- export const REZ_GUIDANCE = [
83
+ export const REZ_GUIDANCE_CORE = [
83
84
  '本机已安装 ReZ-TI 拆分包:工作身份由 dsh-rez-sso 提供,MCP 由官方 dsh-mcp-client 接入 Odoo / Nextcloud / 企业微信 / Home Assistant。',
84
85
  '工具名 mcp__odoo__*、mcp__nextcloud__*、mcp__wechat__*、mcp__homeassistant__*。',
85
86
  '密钥:REZ_ODOO_TOKEN、REZ_NEXTCLOUD_PASSWORD、REZ_WECHAT_WEBHOOK、REZ_HA_TOKEN。',
86
87
  '个人微信(QClaw/ClawBot):打开 设置 → 插件 → ReZ-TI → 微信 扫码,然后直接在微信里说话。消息进入网页左侧「微信」文件夹并延续同一会话,模型跟网页默认。网页归档了再从微信说话会自动恢复到侧栏。发「重启对话」会新开一条可见会话并把旧会话归档。网页端必须开着。不必再跑命令、也不要调用微信 MCP 工具。',
87
88
  '意图用 rez_set_intent(erp/files/chat/home/auto)。审计用 rez_audit。',
88
89
  '涉及资金、对外发送、设备控制、生产写入时先征得用户批准。',
89
- BOSS_GUIDANCE,
90
90
  ].join('')
91
91
 
92
+ export function rezGuidanceFor(role: RezConfig['role']): string {
93
+ return REZ_GUIDANCE_CORE + bossGuidanceFor(role)
94
+ }
95
+
92
96
  function normalize(input: Partial<RezConfig> | undefined): RezConfig {
93
97
  return mergeConfig(defaultConfig(), input ?? {})
94
98
  }
@@ -110,8 +114,8 @@ function delegatedHost(ctx: Context, getConfig: () => RezConfig): McpHost {
110
114
  }
111
115
 
112
116
  export function apply(ctx: Context, config?: RezConfig): void {
113
- mountBossDesk(ctx)
114
117
  let current: () => RezConfig = () => normalize(config)
118
+ const syncBossDesk = mountBossDesk(ctx, () => current().role)
115
119
 
116
120
  const tokens = new TokenManager(joinTokenDb())
117
121
  const host = delegatedHost(ctx, () => current())
@@ -128,6 +132,7 @@ export function apply(ctx: Context, config?: RezConfig): void {
128
132
  const value = normalize(next as Partial<RezConfig>)
129
133
  saveConfig(value)
130
134
  current = () => value
135
+ syncBossDesk()
131
136
  try {
132
137
  const settings = ctx.get('settings') as { update?: (namespace: unknown, value: unknown) => Promise<void> } | undefined
133
138
  if (settings?.update !== undefined) await settings.update(REZ_SETTINGS_NAMESPACE, value)
@@ -142,6 +147,7 @@ export function apply(ctx: Context, config?: RezConfig): void {
142
147
  const emit = ctx.emit as unknown as (event: string, value: string) => void
143
148
  emit('credentials/updated', ref)
144
149
  },
150
+ selfUpdate: liveSelfUpdate(),
145
151
  })
146
152
 
147
153
  let disposeRoutes: (() => void) | undefined
@@ -160,7 +166,7 @@ export function apply(ctx: Context, config?: RezConfig): void {
160
166
  disposeSection = ctx.systemPrompt.section({
161
167
  name: 'plugin:dsh-rez-suite',
162
168
  order: SECTION_ORDER,
163
- text: REZ_GUIDANCE,
169
+ text: rezGuidanceFor(value.role),
164
170
  })
165
171
  }
166
172
 
@@ -179,8 +185,12 @@ export function apply(ctx: Context, config?: RezConfig): void {
179
185
  setSource: (source) => {
180
186
  current = source
181
187
  sync()
188
+ syncBossDesk()
189
+ },
190
+ onChange: () => {
191
+ sync()
192
+ syncBossDesk()
182
193
  },
183
- onChange: sync,
184
194
  })
185
195
 
186
196
  sync()
package/src/protocol.ts CHANGED
@@ -161,6 +161,18 @@ export interface ApiErrorBody {
161
161
  error: string
162
162
  }
163
163
 
164
+ /** Installed vs npm latest for the suite package. */
165
+ export interface RezUpdateStatus {
166
+ package: string
167
+ installed: string
168
+ latest: string
169
+ outdated: boolean
170
+ }
171
+
172
+ export interface RezUpdateApplyResult extends RezUpdateStatus {
173
+ restarting: boolean
174
+ }
175
+
164
176
  /** Route paths the client calls (shared literals). */
165
177
  export const REZ_API_BASE = '/api/dsh-rez-suite' as const
166
178
 
@@ -172,4 +184,5 @@ export const REZ_API = {
172
184
  auditReset: REZ_API_BASE + '/audit/reset',
173
185
  weixin: REZ_API_BASE + '/weixin',
174
186
  weixinVerify: REZ_API_BASE + '/weixin/verify',
187
+ update: REZ_API_BASE + '/update',
175
188
  } as const