@rezti/dsh-rez-suite 0.1.27 → 0.1.34
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/README.md +3 -1
- package/lib/client.d.ts +8 -1
- package/lib/client.js +393 -192
- package/lib/index.d.ts +4 -0
- package/lib/index.js +186 -13
- package/lib/style.css +47 -0
- package/package.json +17 -17
- package/src/boss/mount.ts +2 -0
- package/src/client/assets/rez-mark.png +0 -0
- package/src/client/brand-mark.ts +4 -0
- package/src/client/brand.module.css +51 -0
- package/src/client/brand.ts +135 -0
- package/src/client/index.ts +11 -0
- package/src/client/locales.ts +16 -2
- package/src/client/panel/AuditTab.tsx +20 -19
- package/src/client/panel/ConfigTab.tsx +59 -43
- package/src/client/panel/StatusTab.tsx +11 -10
- package/src/client/panel/WeixinTab.tsx +9 -8
- package/src/client/panel/helpers.ts +20 -5
- package/src/client/settings-card.tsx +48 -42
- package/src/client/upgrade-button.tsx +24 -12
- package/src/index.ts +21 -4
- package/src/mcp-host.ts +1 -1
- package/src/observe-mcp.ts +33 -3
- package/src/protocol.ts +2 -0
- package/src/qcc.ts +95 -0
- package/src/rooms.ts +65 -0
- package/src/store.ts +6 -0
- package/src/tools.ts +3 -2
- package/templates/boss/money/.agents/skills/boss-money/SKILL.md +1 -1
- package/templates/boss/money/AGENTS.md +3 -0
- package/templates/boss/ops/.agents/skills/boss-ops/SKILL.md +1 -1
- package/templates/boss/ops/AGENTS.md +3 -0
- package/templates/boss/product/.agents/skills/boss-product/SKILL.md +1 -1
- package/templates/boss/product/AGENTS.md +5 -0
- package/templates/staff/finance/.agents/skills/staff-finance/SKILL.md +1 -1
- package/templates/staff/finance/AGENTS.md +7 -0
- package/templates/staff/finance/MEMORY.md +1 -1
- package/templates/staff/hr/.agents/skills/staff-hr/SKILL.md +1 -1
- package/templates/staff/hr/AGENTS.md +7 -0
- package/templates/staff/hr/MEMORY.md +1 -1
- package/templates/staff/legal/.agents/skills/staff-legal/SKILL.md +1 -1
- package/templates/staff/legal/AGENTS.md +12 -2
- package/templates/staff/legal/MEMORY.md +1 -1
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
import { useEffect, useState } from 'react'
|
|
6
6
|
import type { RezApi } from '../api.ts'
|
|
7
7
|
import type { RezWeixinStatus } from '../../protocol.ts'
|
|
8
|
-
import { errorMessage,
|
|
8
|
+
import { errorMessage, useT } from './helpers.ts'
|
|
9
9
|
import css from './panel.module.css'
|
|
10
10
|
|
|
11
11
|
export function WeixinTab({ api }: { api: RezApi }) {
|
|
12
|
+
const t = useT()
|
|
12
13
|
const [status, setStatus] = useState<RezWeixinStatus | null>(null)
|
|
13
14
|
const [error, setError] = useState('')
|
|
14
15
|
const [code, setCode] = useState('')
|
|
@@ -68,32 +69,32 @@ export function WeixinTab({ api }: { api: RezApi }) {
|
|
|
68
69
|
|
|
69
70
|
return (
|
|
70
71
|
<div className={css.tabBody}>
|
|
71
|
-
<p className={css.message}>{
|
|
72
|
+
<p className={css.message}>{t('weixin.intro')}</p>
|
|
72
73
|
{error !== '' && <div className={css.error}>{error}</div>}
|
|
73
74
|
{status !== null && <p className={css.message}>{status.hint}</p>}
|
|
74
75
|
{status?.qrDataUrl !== undefined && (
|
|
75
|
-
<img className={css.qr} src={status.qrDataUrl} alt={
|
|
76
|
+
<img className={css.qr} src={status.qrDataUrl} alt={t('weixin.qrAlt')} />
|
|
76
77
|
)}
|
|
77
78
|
{status?.qrContent !== undefined && (
|
|
78
79
|
<p className={css.message}><a href={status.qrContent} target="_blank" rel="noreferrer">{status.qrContent}</a></p>
|
|
79
80
|
)}
|
|
80
81
|
{status?.phase === 'need_verify' && (
|
|
81
82
|
<div className={css.toolbar}>
|
|
82
|
-
<input className={css.input} value={code} onChange={event => { setCode(event.target.value) }} placeholder={
|
|
83
|
-
<button type="button" className={css.button} disabled={busy} onClick={() => { void verify() }}>{
|
|
83
|
+
<input className={css.input} value={code} onChange={event => { setCode(event.target.value) }} placeholder={t('weixin.verifyPlaceholder')} />
|
|
84
|
+
<button type="button" className={css.button} disabled={busy} onClick={() => { void verify() }}>{t('weixin.verify')}</button>
|
|
84
85
|
</div>
|
|
85
86
|
)}
|
|
86
87
|
<div className={css.toolbar}>
|
|
87
88
|
<button type="button" className={css.button} disabled={busy} onClick={() => { void login() }}>
|
|
88
|
-
{status?.loggedIn === true ?
|
|
89
|
+
{status?.loggedIn === true ? t('weixin.reconnect') : t('weixin.login')}
|
|
89
90
|
</button>
|
|
90
91
|
{status?.loggedIn === true && (
|
|
91
|
-
<button type="button" className={css.button} disabled={busy} onClick={() => { void logout() }}>{
|
|
92
|
+
<button type="button" className={css.button} disabled={busy} onClick={() => { void logout() }}>{t('weixin.logout')}</button>
|
|
92
93
|
)}
|
|
93
94
|
</div>
|
|
94
95
|
{status !== null && (
|
|
95
96
|
<p className={css.message}>
|
|
96
|
-
{status.listening ?
|
|
97
|
+
{status.listening ? t('weixin.listening') : status.loggedIn ? t('weixin.loggedIn') : t('weixin.loggedOut')}
|
|
97
98
|
</p>
|
|
98
99
|
)}
|
|
99
100
|
</div>
|
|
@@ -1,18 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Official DSH locale seat for ReZ UI copy.
|
|
3
|
+
*
|
|
4
|
+
* Slot components receive `t` from `locale: 'dsh-rez-suite'`. Fallback copy
|
|
5
|
+
* is Chinese only — do not read `<html lang>`. The Host stores zh/en in
|
|
6
|
+
* `locale.preference`, which does not necessarily match the document language.
|
|
3
7
|
*/
|
|
4
8
|
|
|
5
|
-
import {
|
|
9
|
+
import { createContext, useContext } from 'react'
|
|
10
|
+
import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
|
|
11
|
+
import { t as fill, zh, type RezKey } from '../locales.ts'
|
|
6
12
|
|
|
7
13
|
export type TranslateValues = Record<string, string | number>
|
|
14
|
+
export type RezTranslate = TranslateNS<'dsh-rez-suite'>
|
|
8
15
|
|
|
16
|
+
export const RezT = createContext<RezTranslate | null>(null)
|
|
17
|
+
|
|
18
|
+
/** Fallback for tests / code outside a slot. Prefer {@link useT}. */
|
|
9
19
|
export function dictionary(): Record<string, string> {
|
|
10
|
-
|
|
11
|
-
return lang.toLowerCase().startsWith('en') ? { ...en } : { ...zh }
|
|
20
|
+
return { ...zh }
|
|
12
21
|
}
|
|
13
22
|
|
|
14
23
|
export function tt(key: RezKey, values?: TranslateValues): string {
|
|
15
|
-
return
|
|
24
|
+
return fill(dictionary(), key, values)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function useT(): RezTranslate {
|
|
28
|
+
const injected = useContext(RezT)
|
|
29
|
+
if (injected) return injected
|
|
30
|
+
return (key, params) => fill(dictionary(), key as RezKey, params as TranslateValues | undefined)
|
|
16
31
|
}
|
|
17
32
|
|
|
18
33
|
export function errorMessage(error: unknown): string {
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ReZ settings pages: one accordion card in 设置 → 插件 → 插件配置,
|
|
3
3
|
* plus a dedicated Plugins tab for the same content.
|
|
4
|
+
*
|
|
5
|
+
* `t` is the official locale seat (zh/en follows 设置 → 语言).
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
8
|
import { useMemo, useState } from 'react'
|
|
9
|
+
import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
|
|
7
10
|
import { RezApi } from './api.ts'
|
|
8
|
-
import {
|
|
11
|
+
import { RezT } from './panel/helpers.ts'
|
|
9
12
|
import { AuditTab } from './panel/AuditTab.tsx'
|
|
10
13
|
import { ConfigTab } from './panel/ConfigTab.tsx'
|
|
11
14
|
import { StatusTab } from './panel/StatusTab.tsx'
|
|
@@ -14,55 +17,58 @@ import css from './panel/panel.module.css'
|
|
|
14
17
|
|
|
15
18
|
export type RezTab = 'config' | 'weixin' | 'status' | 'audit'
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
{ id: 'config', label: () => tt('tab.config') },
|
|
19
|
-
{ id: 'weixin', label: () => tt('tab.weixin') },
|
|
20
|
-
{ id: 'status', label: () => tt('tab.status') },
|
|
21
|
-
{ id: 'audit', label: () => tt('tab.audit') },
|
|
22
|
-
]
|
|
23
|
-
|
|
24
|
-
export function RezSettingsPage() {
|
|
20
|
+
export function RezSettingsPage({ t }: { t: TranslateNS<'dsh-rez-suite'> }) {
|
|
25
21
|
const api = useMemo(() => new RezApi(), [])
|
|
26
22
|
const [activeTab, setActiveTab] = useState<RezTab>('config')
|
|
23
|
+
const tabs: ReadonlyArray<{ id: RezTab; label: string }> = [
|
|
24
|
+
{ id: 'config', label: t('tab.config') },
|
|
25
|
+
{ id: 'weixin', label: t('tab.weixin') },
|
|
26
|
+
{ id: 'status', label: t('tab.status') },
|
|
27
|
+
{ id: 'audit', label: t('tab.audit') },
|
|
28
|
+
]
|
|
27
29
|
return (
|
|
28
|
-
<
|
|
29
|
-
<div className={css.
|
|
30
|
-
{
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
30
|
+
<RezT.Provider value={t}>
|
|
31
|
+
<div className={css.shell}>
|
|
32
|
+
<div className={css.tabBar} role="tablist">
|
|
33
|
+
{tabs.map(tab => (
|
|
34
|
+
<button
|
|
35
|
+
key={tab.id}
|
|
36
|
+
type="button"
|
|
37
|
+
role="tab"
|
|
38
|
+
aria-selected={activeTab === tab.id}
|
|
39
|
+
data-active={activeTab === tab.id ? '' : undefined}
|
|
40
|
+
className={css.tab}
|
|
41
|
+
onClick={() => { setActiveTab(tab.id) }}
|
|
42
|
+
>
|
|
43
|
+
{tab.label}
|
|
44
|
+
</button>
|
|
45
|
+
))}
|
|
46
|
+
</div>
|
|
47
|
+
{activeTab === 'config' && <ConfigTab api={api} />}
|
|
48
|
+
{activeTab === 'weixin' && <WeixinTab api={api} />}
|
|
49
|
+
{activeTab === 'status' && <StatusTab api={api} />}
|
|
50
|
+
{activeTab === 'audit' && <AuditTab api={api} />}
|
|
43
51
|
</div>
|
|
44
|
-
|
|
45
|
-
{activeTab === 'weixin' && <WeixinTab api={api} />}
|
|
46
|
-
{activeTab === 'status' && <StatusTab api={api} />}
|
|
47
|
-
{activeTab === 'audit' && <AuditTab api={api} />}
|
|
48
|
-
</div>
|
|
52
|
+
</RezT.Provider>
|
|
49
53
|
)
|
|
50
54
|
}
|
|
51
55
|
|
|
52
|
-
export function RezPluginCard() {
|
|
56
|
+
export function RezPluginCard({ t }: { t: TranslateNS<'dsh-rez-suite'> }) {
|
|
53
57
|
const [open, setOpen] = useState(false)
|
|
54
58
|
return (
|
|
55
|
-
<
|
|
56
|
-
<
|
|
57
|
-
<
|
|
58
|
-
<div className={css.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
59
|
+
<RezT.Provider value={t}>
|
|
60
|
+
<li className={css.card + (open ? ' ' + css.cardOpen : '')}>
|
|
61
|
+
<button type="button" className={css.header} onClick={() => { setOpen(!open) }}>
|
|
62
|
+
<div className={css.headText}>
|
|
63
|
+
<div className={css.name}>{t('settings.title')}</div>
|
|
64
|
+
<div className={css.description}>{t('settings.description')}</div>
|
|
65
|
+
</div>
|
|
66
|
+
<svg className={css.chevron + (open ? ' ' + css.chevronOpen : '')} viewBox="0 0 14 14" width="14" height="14" aria-hidden="true">
|
|
67
|
+
<path d="M3 5l4 4 4-4" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
|
|
68
|
+
</svg>
|
|
69
|
+
</button>
|
|
70
|
+
{open ? <div className={css.body}><RezSettingsPage t={t} /></div> : null}
|
|
71
|
+
</li>
|
|
72
|
+
</RezT.Provider>
|
|
67
73
|
)
|
|
68
74
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Sidebar footer action: upgrade @rezti/dsh-rez-suite without a terminal.
|
|
3
3
|
* Host code still needs a process respawn; the button does that after pnpm add.
|
|
4
|
+
*
|
|
5
|
+
* `t` is the official locale seat (zh/en follows 设置 → 语言).
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
8
|
import { useEffect, useMemo, useState } from 'react'
|
|
9
|
+
import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
|
|
7
10
|
import { RezApi } from './api.ts'
|
|
8
|
-
import { tt } from './panel/helpers.ts'
|
|
9
11
|
import css from './panel/panel.module.css'
|
|
10
12
|
import type { RezUpdateStatus } from '../protocol.ts'
|
|
11
13
|
|
|
@@ -24,7 +26,11 @@ function UpgradeIcon() {
|
|
|
24
26
|
)
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
async function waitUntilHostBack(
|
|
29
|
+
async function waitUntilHostBack(
|
|
30
|
+
api: RezApi,
|
|
31
|
+
timeoutMessage: string,
|
|
32
|
+
timeoutMs = 60_000,
|
|
33
|
+
): Promise<void> {
|
|
28
34
|
const deadline = Date.now() + timeoutMs
|
|
29
35
|
await new Promise((resolve) => setTimeout(resolve, 800))
|
|
30
36
|
while (Date.now() < deadline) {
|
|
@@ -35,10 +41,16 @@ async function waitUntilHostBack(api: RezApi, timeoutMs = 60_000): Promise<void>
|
|
|
35
41
|
await new Promise((resolve) => setTimeout(resolve, 700))
|
|
36
42
|
}
|
|
37
43
|
}
|
|
38
|
-
throw new Error(
|
|
44
|
+
throw new Error(timeoutMessage)
|
|
39
45
|
}
|
|
40
46
|
|
|
41
|
-
export function UpgradeButton({
|
|
47
|
+
export function UpgradeButton({
|
|
48
|
+
wide,
|
|
49
|
+
t,
|
|
50
|
+
}: {
|
|
51
|
+
wide: boolean
|
|
52
|
+
t: TranslateNS<'dsh-rez-suite'>
|
|
53
|
+
}) {
|
|
42
54
|
const api = useMemo(() => new RezApi(), [])
|
|
43
55
|
const [info, setInfo] = useState<RezUpdateStatus>()
|
|
44
56
|
const [busy, setBusy] = useState(false)
|
|
@@ -55,10 +67,10 @@ export function UpgradeButton({ wide }: { wide: boolean }) {
|
|
|
55
67
|
}, [api])
|
|
56
68
|
|
|
57
69
|
const label = busy
|
|
58
|
-
?
|
|
70
|
+
? t('upgrade.running')
|
|
59
71
|
: info?.outdated === true
|
|
60
|
-
?
|
|
61
|
-
:
|
|
72
|
+
? t('upgrade.label')
|
|
73
|
+
: t('upgrade.current')
|
|
62
74
|
|
|
63
75
|
const meta = note !== ''
|
|
64
76
|
? note
|
|
@@ -76,18 +88,18 @@ export function UpgradeButton({ wide }: { wide: boolean }) {
|
|
|
76
88
|
const snapshot = info ?? await api.updateStatus()
|
|
77
89
|
setInfo(snapshot)
|
|
78
90
|
if (!snapshot.outdated) {
|
|
79
|
-
setNote(
|
|
91
|
+
setNote(t('upgrade.already', { version: snapshot.installed }))
|
|
80
92
|
return
|
|
81
93
|
}
|
|
82
94
|
const result = await api.applyUpdate()
|
|
83
95
|
setInfo(result)
|
|
84
96
|
if (result.restarting) {
|
|
85
|
-
setNote(
|
|
86
|
-
await waitUntilHostBack(api)
|
|
97
|
+
setNote(t('upgrade.reloading'))
|
|
98
|
+
await waitUntilHostBack(api, t('upgrade.reloadTimeout'))
|
|
87
99
|
window.location.reload()
|
|
88
100
|
return
|
|
89
101
|
}
|
|
90
|
-
setNote(
|
|
102
|
+
setNote(t('upgrade.already', { version: result.installed }))
|
|
91
103
|
} catch (error) {
|
|
92
104
|
setNote(error instanceof Error ? error.message : String(error))
|
|
93
105
|
} finally {
|
|
@@ -101,7 +113,7 @@ export function UpgradeButton({ wide }: { wide: boolean }) {
|
|
|
101
113
|
type="button"
|
|
102
114
|
className={css.upgradeButton}
|
|
103
115
|
data-outdated={info?.outdated === true ? '' : undefined}
|
|
104
|
-
aria-label={
|
|
116
|
+
aria-label={t('upgrade.aria')}
|
|
105
117
|
disabled={busy}
|
|
106
118
|
onClick={() => { void onClick() }}
|
|
107
119
|
>
|
package/src/index.ts
CHANGED
|
@@ -30,6 +30,8 @@ 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
|
+
import { mountQccMcp, QCC_COMPANY_MCP_URL } from './qcc.ts'
|
|
34
|
+
import { qccRoomGuard } from './rooms.ts'
|
|
33
35
|
|
|
34
36
|
export const name = 'rez-suite'
|
|
35
37
|
|
|
@@ -80,10 +82,14 @@ export const Config: z<RezConfig> = z.object({
|
|
|
80
82
|
enabled: z.boolean().default(true),
|
|
81
83
|
siteUrl: z.string().default(REZ_DEFAULT_CONNECTIONS.gsc.siteUrl),
|
|
82
84
|
}).default(REZ_DEFAULT_CONNECTIONS.gsc),
|
|
85
|
+
qcc: z.object({
|
|
86
|
+
enabled: z.boolean().default(true),
|
|
87
|
+
url: z.string().default(REZ_DEFAULT_CONNECTIONS.qcc?.url ?? QCC_COMPANY_MCP_URL),
|
|
88
|
+
}).default(REZ_DEFAULT_CONNECTIONS.qcc ?? { enabled: true, url: QCC_COMPANY_MCP_URL }),
|
|
83
89
|
tianyancha: z.object({
|
|
84
90
|
enabled: z.boolean().default(true),
|
|
85
|
-
url: z.string().default(REZ_DEFAULT_CONNECTIONS.tianyancha.
|
|
86
|
-
}).default(REZ_DEFAULT_CONNECTIONS.tianyancha),
|
|
91
|
+
url: z.string().default(REZ_DEFAULT_CONNECTIONS.tianyancha?.url ?? 'https://mcp.tianyancha.com/mcp'),
|
|
92
|
+
}).default(REZ_DEFAULT_CONNECTIONS.tianyancha ?? { enabled: true, url: 'https://mcp.tianyancha.com/mcp' }),
|
|
87
93
|
}).default(REZ_DEFAULT_CONNECTIONS),
|
|
88
94
|
billing: z.object({
|
|
89
95
|
inputCostPer1k: z.number().default(0.001),
|
|
@@ -101,6 +107,8 @@ export const REZ_GUIDANCE_CORE = [
|
|
|
101
107
|
'个人微信(QClaw/ClawBot):打开 设置 → 插件 → ReZ-TI → 微信 扫码,然后直接在微信里说话。消息进入网页左侧「微信」文件夹并延续同一会话,模型跟网页默认。网页归档了再从微信说话会自动恢复到侧栏。发「重启对话」会新开一条可见会话并把旧会话归档。网页端必须开着。不必再跑命令、也不要调用微信 MCP 工具。',
|
|
102
108
|
'意图用 rez_set_intent(erp/files/chat/home/auto)。审计用 rez_audit。',
|
|
103
109
|
'涉及资金、对外发送、设备控制、生产写入时先征得用户批准。',
|
|
110
|
+
'Match the user language: Chinese or English. Do not force 中文 when they write English.',
|
|
111
|
+
'制度、手册、出差/报销标准、素材路径:打开 Nextcloud Documents/Standards/INDEX.yaml,按 code 取当前 effective_from 最新且未被 replace 的条目,再按相对 uri 打开。引用带路径+生效日。找不到就说「标准文件未归档」,不要用 MEMORY 旧数字。素材文件夹用首次创建标题,不用会变的 Odoo/销售名。',
|
|
104
112
|
].join('')
|
|
105
113
|
|
|
106
114
|
export function rezGuidanceFor(role: RezConfig['role']): string {
|
|
@@ -115,12 +123,13 @@ function normalize(input: Partial<RezConfig> | undefined): RezConfig {
|
|
|
115
123
|
}
|
|
116
124
|
|
|
117
125
|
function delegatedHost(ctx: Context, getConfig: () => RezConfig): McpHost {
|
|
118
|
-
const snapshot = () => observeComposedServers({
|
|
126
|
+
const snapshot = (room?: string) => observeComposedServers({
|
|
119
127
|
config: getConfig(),
|
|
120
128
|
toolNames: listRegisteredToolNames(ctx.tools),
|
|
129
|
+
room,
|
|
121
130
|
})
|
|
122
131
|
return {
|
|
123
|
-
status: () => snapshot(),
|
|
132
|
+
status: (room?: string) => snapshot(room),
|
|
124
133
|
test: async () => testComposedServers({
|
|
125
134
|
config: getConfig(),
|
|
126
135
|
toolNames: listRegisteredToolNames(ctx.tools),
|
|
@@ -133,11 +142,16 @@ function delegatedHost(ctx: Context, getConfig: () => RezConfig): McpHost {
|
|
|
133
142
|
export function apply(ctx: Context, config?: RezConfig): void {
|
|
134
143
|
let current: () => RezConfig = () => normalize(config)
|
|
135
144
|
const syncWorkspaces = mountRezWorkspaces(ctx, () => current().role)
|
|
145
|
+
const syncQcc = mountQccMcp(ctx)
|
|
136
146
|
const syncTianyancha = mountTianyanchaMcp(ctx, () => current().role)
|
|
137
147
|
|
|
138
148
|
const tokens = new TokenManager(joinTokenDb())
|
|
139
149
|
const host = delegatedHost(ctx, () => current())
|
|
140
150
|
ctx.effect(() => () => { tokens.close() }, 'dsh-rez-suite: ledger view')
|
|
151
|
+
const toolsHost = ctx.tools as { guard?: (guard: typeof qccRoomGuard) => () => void }
|
|
152
|
+
if (typeof toolsHost.guard === 'function') {
|
|
153
|
+
ctx.effect(() => toolsHost.guard!(qccRoomGuard), 'dsh-rez-suite: qcc room guard')
|
|
154
|
+
}
|
|
141
155
|
|
|
142
156
|
const tools = [
|
|
143
157
|
rezStatusTool(host, () => current()),
|
|
@@ -151,6 +165,7 @@ export function apply(ctx: Context, config?: RezConfig): void {
|
|
|
151
165
|
saveConfig(value)
|
|
152
166
|
current = () => value
|
|
153
167
|
syncWorkspaces()
|
|
168
|
+
syncQcc()
|
|
154
169
|
syncTianyancha()
|
|
155
170
|
try {
|
|
156
171
|
const settings = ctx.get('settings') as { update?: (namespace: unknown, value: unknown) => Promise<void> } | undefined
|
|
@@ -205,11 +220,13 @@ export function apply(ctx: Context, config?: RezConfig): void {
|
|
|
205
220
|
current = source
|
|
206
221
|
sync()
|
|
207
222
|
syncWorkspaces()
|
|
223
|
+
syncQcc()
|
|
208
224
|
syncTianyancha()
|
|
209
225
|
},
|
|
210
226
|
onChange: () => {
|
|
211
227
|
sync()
|
|
212
228
|
syncWorkspaces()
|
|
229
|
+
syncQcc()
|
|
213
230
|
syncTianyancha()
|
|
214
231
|
},
|
|
215
232
|
})
|
package/src/mcp-host.ts
CHANGED
|
@@ -368,7 +368,7 @@ export class McpHost {
|
|
|
368
368
|
return results
|
|
369
369
|
}
|
|
370
370
|
|
|
371
|
-
status(): RezServerStatus[] {
|
|
371
|
+
status(_room?: string): RezServerStatus[] {
|
|
372
372
|
const ids = new Set<string>([...this.enabledIds, ...this.connections.keys()])
|
|
373
373
|
return [...ids].sort().map(id => {
|
|
374
374
|
const connection = this.connections.get(id)
|
package/src/observe-mcp.ts
CHANGED
|
@@ -8,12 +8,14 @@
|
|
|
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, QCC_STATUS_ROOM_OFF } from './rooms.ts'
|
|
11
12
|
|
|
12
|
-
export const COMPOSED_SERVERS = ['odoo', 'nextcloud', 'wechat', 'homeassistant', 'tapd', 'gsc', 'tianyancha'] as const
|
|
13
|
+
export const COMPOSED_SERVERS = ['odoo', 'nextcloud', 'wechat', 'homeassistant', 'tapd', 'gsc', 'qcc', 'tianyancha'] as const
|
|
13
14
|
|
|
14
15
|
export type ComposedServer = (typeof COMPOSED_SERVERS)[number]
|
|
15
16
|
|
|
16
17
|
export function mcpToolPrefix(server: string): string {
|
|
18
|
+
if (server === 'qcc') return 'mcp__qcc-'
|
|
17
19
|
return 'mcp__' + server + '__'
|
|
18
20
|
}
|
|
19
21
|
|
|
@@ -39,6 +41,8 @@ export function listRegisteredToolNames(tools: { schemas?: () => ReadonlyArray<{
|
|
|
39
41
|
export interface ObserveInput {
|
|
40
42
|
config: RezConfig
|
|
41
43
|
toolNames: readonly string[]
|
|
44
|
+
/** Session cwd or folder name. Settings panel omits this. */
|
|
45
|
+
room?: string
|
|
42
46
|
secretPresent?: (server: RezSystem) => boolean
|
|
43
47
|
startError?: (server: RezSystem) => string | undefined
|
|
44
48
|
}
|
|
@@ -101,11 +105,37 @@ export function observeServer(
|
|
|
101
105
|
}
|
|
102
106
|
}
|
|
103
107
|
|
|
104
|
-
|
|
108
|
+
function composedServersFor(input: ObserveInput): ComposedServer[] {
|
|
105
109
|
const servers = input.config.role === 'boss'
|
|
106
110
|
? COMPOSED_SERVERS
|
|
107
111
|
: COMPOSED_SERVERS.filter((server) => server !== 'tianyancha')
|
|
108
|
-
|
|
112
|
+
if (input.room !== undefined && !isLegalAssistantRoom(input.room)) {
|
|
113
|
+
return servers.filter((server) => server !== 'qcc')
|
|
114
|
+
}
|
|
115
|
+
return [...servers]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function observeQccOutsideLegalRoom(input: ObserveInput): RezServerStatus {
|
|
119
|
+
if (isEnabled(input.config, 'qcc') === false) {
|
|
120
|
+
return observeServer('qcc', input)
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
server: 'qcc',
|
|
124
|
+
enabled: true,
|
|
125
|
+
state: 'disconnected',
|
|
126
|
+
toolCount: 0,
|
|
127
|
+
registeredTools: 0,
|
|
128
|
+
lastError: QCC_STATUS_ROOM_OFF,
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function observeComposedServers(input: ObserveInput): RezServerStatus[] {
|
|
133
|
+
return composedServersFor(input).map((server) => {
|
|
134
|
+
if (server === 'qcc' && !isLegalAssistantRoom(input.room)) {
|
|
135
|
+
return observeQccOutsideLegalRoom(input)
|
|
136
|
+
}
|
|
137
|
+
return observeServer(server, input)
|
|
138
|
+
})
|
|
109
139
|
}
|
|
110
140
|
|
|
111
141
|
export function testComposedServers(input: ObserveInput): RezTestResult[] {
|
package/src/protocol.ts
CHANGED
|
@@ -48,6 +48,7 @@ export interface RezConnections {
|
|
|
48
48
|
homeassistant: { enabled: boolean; url: string }
|
|
49
49
|
tapd: { enabled: boolean; workspaceId: string; nickName: string }
|
|
50
50
|
gsc: { enabled: boolean; siteUrl: string }
|
|
51
|
+
qcc: { enabled: boolean; url: string }
|
|
51
52
|
tianyancha: { enabled: boolean; url: string }
|
|
52
53
|
}
|
|
53
54
|
|
|
@@ -91,6 +92,7 @@ export interface RezPublicConfig {
|
|
|
91
92
|
homeassistant: RezPublicConnection
|
|
92
93
|
tapd: RezPublicConnection
|
|
93
94
|
gsc: RezPublicConnection
|
|
95
|
+
qcc: RezPublicConnection
|
|
94
96
|
tianyancha: RezPublicConnection
|
|
95
97
|
}
|
|
96
98
|
billing: { inputCostPer1k: number; outputCostPer1k: number; monthlyBudget: number }
|
package/src/qcc.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Qichacha official MCP for the 法务助理 workspace.
|
|
3
|
+
* Reuse https://agent.qcc.com/mcp/{company,risk}/stream via dsh-mcp-client.
|
|
4
|
+
* One API key starts two streamable-http servers (qcc-company, qcc-risk).
|
|
5
|
+
* Start for every suite role so 法务助理 can query. Execution is room-gated
|
|
6
|
+
* (`ctx.tools.guard` in rooms.ts). Do not dump into the global prompt.
|
|
7
|
+
*/
|
|
8
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
9
|
+
import { loadRezConnections, startMcpWhenSecret } from '@rezti/dsh-rez-sso'
|
|
10
|
+
|
|
11
|
+
export const QCC_COMPANY_MCP_URL = 'https://agent.qcc.com/mcp/company/stream'
|
|
12
|
+
export const QCC_RISK_MCP_URL = 'https://agent.qcc.com/mcp/risk/stream'
|
|
13
|
+
|
|
14
|
+
export function bearerAuthorization(secret: string): string {
|
|
15
|
+
const trimmed = secret.trim()
|
|
16
|
+
if (/^bearer\s+/i.test(trimmed)) return 'Bearer ' + trimmed.replace(/^bearer\s+/i, '')
|
|
17
|
+
return 'Bearer ' + trimmed
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Risk stream URL: swap /company/ for /risk/ when the company URL is official-shaped. */
|
|
21
|
+
export function qccRiskUrlFrom(companyUrl: string): string {
|
|
22
|
+
const trimmed = companyUrl.trim()
|
|
23
|
+
if (trimmed.includes('/company/')) return trimmed.replace('/company/', '/risk/')
|
|
24
|
+
return QCC_RISK_MCP_URL
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function qccHeaders(secret: string): Record<string, string> {
|
|
28
|
+
return { Authorization: bearerAuthorization(secret) }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function qccCompanyUrl(): string {
|
|
32
|
+
const url = loadRezConnections().qcc?.url
|
|
33
|
+
return typeof url === 'string' && url.length > 0 ? url : QCC_COMPANY_MCP_URL
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function qccCompanyMcpConfig(secret: string): Record<string, unknown> {
|
|
37
|
+
return {
|
|
38
|
+
serverName: 'qcc-company',
|
|
39
|
+
transport: 'streamable-http',
|
|
40
|
+
url: qccCompanyUrl(),
|
|
41
|
+
headers: qccHeaders(secret),
|
|
42
|
+
failOnStartupError: false,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function qccRiskMcpConfig(secret: string): Record<string, unknown> {
|
|
47
|
+
const companyUrl = qccCompanyUrl()
|
|
48
|
+
return {
|
|
49
|
+
serverName: 'qcc-risk',
|
|
50
|
+
transport: 'streamable-http',
|
|
51
|
+
url: qccRiskUrlFrom(companyUrl),
|
|
52
|
+
headers: qccHeaders(secret),
|
|
53
|
+
failOnStartupError: false,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Start company + risk clients whenever the Qichacha secret exists. */
|
|
58
|
+
export function mountQccMcp(
|
|
59
|
+
ctx: Context,
|
|
60
|
+
start: typeof startMcpWhenSecret = startMcpWhenSecret,
|
|
61
|
+
): () => void {
|
|
62
|
+
let session: (() => void) | undefined
|
|
63
|
+
let pending = false
|
|
64
|
+
|
|
65
|
+
const ensure = (): void => {
|
|
66
|
+
void (async () => {
|
|
67
|
+
if (session !== undefined || pending) return
|
|
68
|
+
pending = true
|
|
69
|
+
try {
|
|
70
|
+
const disposeCompany = await start(ctx, {
|
|
71
|
+
system: 'qcc',
|
|
72
|
+
label: '企查查·企业',
|
|
73
|
+
config: qccCompanyMcpConfig,
|
|
74
|
+
})
|
|
75
|
+
const disposeRisk = await start(ctx, {
|
|
76
|
+
system: 'qcc',
|
|
77
|
+
label: '企查查·风险',
|
|
78
|
+
config: qccRiskMcpConfig,
|
|
79
|
+
})
|
|
80
|
+
session = () => {
|
|
81
|
+
disposeCompany()
|
|
82
|
+
disposeRisk()
|
|
83
|
+
}
|
|
84
|
+
} catch (error: unknown) {
|
|
85
|
+
const why = error instanceof Error ? error.message : String(error)
|
|
86
|
+
console.error('[dsh-rez-suite] qcc MCP start failed:', why)
|
|
87
|
+
} finally {
|
|
88
|
+
pending = false
|
|
89
|
+
}
|
|
90
|
+
})()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
ensure()
|
|
94
|
+
return ensure
|
|
95
|
+
}
|
package/src/rooms.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace room identity for execution-time MCP gates.
|
|
3
|
+
* Session cwd is the workspace folder (e.g. ~/.dsh/workspaces/法务助理).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const LEGAL_ROOM_FOLDER = '法务助理'
|
|
7
|
+
|
|
8
|
+
export const QCC_ROOM_DENY = '企查查仅限法务助理房间使用,请切换到法务助理再查询。'
|
|
9
|
+
|
|
10
|
+
export const QCC_STATUS_ROOM_OFF = '房间未启用'
|
|
11
|
+
|
|
12
|
+
const ROOM_SEP = /[/\\]/
|
|
13
|
+
|
|
14
|
+
/** Last path segment of a workspace cwd or folder name. */
|
|
15
|
+
export function roomFolderFromPath(value: string | undefined): string | undefined {
|
|
16
|
+
if (typeof value !== 'string') return undefined
|
|
17
|
+
const trimmed = value.trim().replace(/[/\\]+$/, '')
|
|
18
|
+
if (trimmed.length === 0) return undefined
|
|
19
|
+
const parts = trimmed.split(ROOM_SEP).filter((part) => part.length > 0)
|
|
20
|
+
return parts.at(-1)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isLegalAssistantRoom(hint: string | undefined): boolean {
|
|
24
|
+
return roomFolderFromPath(hint) === LEGAL_ROOM_FOLDER
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isQccToolName(name: string): boolean {
|
|
28
|
+
return name.startsWith('mcp__qcc-') || name.startsWith('mcp__qcc__')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Session cwd from a tool execution. Missing / unknown shapes → undefined. */
|
|
32
|
+
export function cwdFromExecution(execution: { agent?: unknown }): string | undefined {
|
|
33
|
+
const agent = execution.agent
|
|
34
|
+
if (typeof agent !== 'object' || agent === null) return undefined
|
|
35
|
+
const record = agent as Record<string, unknown>
|
|
36
|
+
const session = record.session
|
|
37
|
+
const header = typeof session === 'object' && session !== null
|
|
38
|
+
? (session as Record<string, unknown>).header
|
|
39
|
+
: undefined
|
|
40
|
+
const candidates = [
|
|
41
|
+
typeof header === 'object' && header !== null ? (header as Record<string, unknown>).cwd : undefined,
|
|
42
|
+
typeof session === 'object' && session !== null ? (session as Record<string, unknown>).cwd : undefined,
|
|
43
|
+
record.cwd,
|
|
44
|
+
]
|
|
45
|
+
for (const value of candidates) {
|
|
46
|
+
if (typeof value === 'string' && value.trim().length > 0) return value
|
|
47
|
+
}
|
|
48
|
+
return undefined
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Official `ctx.tools.guard` reason. Only Qichacha tools are gated.
|
|
53
|
+
* Must never throw: this guard is global and a throw would block WeChat turns.
|
|
54
|
+
* Missing cwd fails closed — not the legal room.
|
|
55
|
+
*/
|
|
56
|
+
export function qccRoomGuard(execution: { name?: unknown; agent?: unknown }): string | undefined {
|
|
57
|
+
const name = typeof execution?.name === 'string' ? execution.name : ''
|
|
58
|
+
if (!isQccToolName(name)) return undefined
|
|
59
|
+
try {
|
|
60
|
+
if (isLegalAssistantRoom(cwdFromExecution(execution))) return undefined
|
|
61
|
+
return QCC_ROOM_DENY
|
|
62
|
+
} catch {
|
|
63
|
+
return QCC_ROOM_DENY
|
|
64
|
+
}
|
|
65
|
+
}
|
package/src/store.ts
CHANGED
|
@@ -125,6 +125,12 @@ export function publicConfig(config: RezConfig): RezPublicConfig {
|
|
|
125
125
|
secretConfigured: secretConfigured('gsc'),
|
|
126
126
|
secretRef: REZ_CREDENTIAL_REFS.gsc,
|
|
127
127
|
},
|
|
128
|
+
qcc: {
|
|
129
|
+
enabled: connections.qcc.enabled,
|
|
130
|
+
url: connections.qcc.url,
|
|
131
|
+
secretConfigured: secretConfigured('qcc'),
|
|
132
|
+
secretRef: REZ_CREDENTIAL_REFS.qcc,
|
|
133
|
+
},
|
|
128
134
|
tianyancha: {
|
|
129
135
|
enabled: connections.tianyancha.enabled,
|
|
130
136
|
url: connections.tianyancha.url,
|