@rezti/dsh-rez-suite 0.1.33 → 0.1.35
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 +4 -1
- package/lib/client.js +33 -3
- package/lib/index.d.ts +4 -0
- package/lib/index.js +179 -10
- package/package.json +17 -17
- package/src/client/locales.ts +8 -2
- package/src/client/panel/ConfigTab.tsx +14 -2
- package/src/index.ts +19 -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 +1 -0
- package/templates/boss/ops/.agents/skills/boss-ops/SKILL.md +1 -1
- package/templates/boss/ops/AGENTS.md +1 -0
- package/templates/boss/product/.agents/skills/boss-product/SKILL.md +1 -1
- package/templates/boss/product/AGENTS.md +1 -0
- package/templates/staff/finance/.agents/skills/staff-finance/SKILL.md +1 -1
- package/templates/staff/finance/AGENTS.md +1 -0
- package/templates/staff/hr/.agents/skills/staff-hr/SKILL.md +1 -1
- package/templates/staff/hr/AGENTS.md +1 -0
- package/templates/staff/legal/.agents/skills/staff-legal/SKILL.md +1 -1
- package/templates/staff/legal/AGENTS.md +6 -2
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# @rezti/dsh-rez-suite
|
|
2
2
|
|
|
3
|
-
雷泽插件的**唯一安装入口**。Odoo / 企微 / HA / TAPD / GSC / 天眼查(仅老板) / 工作区(老板三室 + 全员助理)都由这一包拉齐,不要再逐个 `plugin add`,**不要**再装 `@rezti/dsh-rez-boss-desk`。
|
|
3
|
+
雷泽插件的**唯一安装入口**。Odoo / 企微 / HA / TAPD / GSC / 企查查(仅法务助理房间) / 天眼查(仅老板) / 工作区(老板三室 + 全员助理)都由这一包拉齐,不要再逐个 `plugin add`,**不要**再装 `@rezti/dsh-rez-boss-desk`。
|
|
4
|
+
|
|
5
|
+
发版顺序:先发 `@rezti/dsh-rez-sso@0.1.11`,再发 `@rezti/dsh-rez-suite@0.1.34`(或更高)。suite 读取 SSO 的 `REZ_DEFAULT_CONNECTIONS.qcc`;缺字段时用官方 URL 兜底,避免插件加载失败把微信面板一起拖垮。
|
|
4
6
|
|
|
5
7
|
设置里选身份后,左侧自己出现对应房间(全员三助理;老板另加搞钱/产品/运营),不必手动添加工作区。
|
|
6
8
|
|
package/lib/client.d.ts
CHANGED
|
@@ -7300,6 +7300,9 @@ declare const zh: {
|
|
|
7300
7300
|
readonly 'config.gsc': "Google Search Console";
|
|
7301
7301
|
readonly 'config.gsc.siteUrl': "站点 URL";
|
|
7302
7302
|
readonly 'config.gsc.key': "服务账号 JSON 或密钥文件路径";
|
|
7303
|
+
readonly 'config.qcc': "企查查(法务助理)";
|
|
7304
|
+
readonly 'config.qcc.url': "企查查企业 MCP URL";
|
|
7305
|
+
readonly 'config.qcc.token': "API Key";
|
|
7303
7306
|
readonly 'config.tianyancha': "天眼查(法务助理 · 仅老板)";
|
|
7304
7307
|
readonly 'config.tianyancha.url': "天眼查 MCP URL";
|
|
7305
7308
|
readonly 'config.tianyancha.token': "OpenAPI Token";
|
|
@@ -7318,7 +7321,7 @@ declare const zh: {
|
|
|
7318
7321
|
readonly 'config.testResult': "{server}: {status}";
|
|
7319
7322
|
readonly 'config.testOk': "成功({tools} 个工具, {latency}ms)";
|
|
7320
7323
|
readonly 'config.testFail': "失败:{error}";
|
|
7321
|
-
readonly 'status.hint': "绿标表示本机已注册 mcp__<系统>__* 工具。远端 MCP 通了但这里仍红,说明 dsh-mcp-client
|
|
7324
|
+
readonly 'status.hint': "绿标表示本机已注册 mcp__<系统>__* 工具。远端 MCP 通了但这里仍红,说明 dsh-mcp-client 还没挂上。企查查只在法务助理房间可用;其它房间不显示或显示「房间未启用」。";
|
|
7322
7325
|
readonly 'status.total': "当前角色 {role},共注册 {count} 个 MCP 工具";
|
|
7323
7326
|
readonly 'status.col.server': "Server";
|
|
7324
7327
|
readonly 'status.col.state': "状态";
|
package/lib/client.js
CHANGED
|
@@ -184,6 +184,9 @@ const zh = {
|
|
|
184
184
|
"config.gsc": "Google Search Console",
|
|
185
185
|
"config.gsc.siteUrl": "站点 URL",
|
|
186
186
|
"config.gsc.key": "服务账号 JSON 或密钥文件路径",
|
|
187
|
+
"config.qcc": "企查查(法务助理)",
|
|
188
|
+
"config.qcc.url": "企查查企业 MCP URL",
|
|
189
|
+
"config.qcc.token": "API Key",
|
|
187
190
|
"config.tianyancha": "天眼查(法务助理 · 仅老板)",
|
|
188
191
|
"config.tianyancha.url": "天眼查 MCP URL",
|
|
189
192
|
"config.tianyancha.token": "OpenAPI Token",
|
|
@@ -202,7 +205,7 @@ const zh = {
|
|
|
202
205
|
"config.testResult": "{server}: {status}",
|
|
203
206
|
"config.testOk": "成功({tools} 个工具, {latency}ms)",
|
|
204
207
|
"config.testFail": "失败:{error}",
|
|
205
|
-
"status.hint": "绿标表示本机已注册 mcp__<系统>__* 工具。远端 MCP 通了但这里仍红,说明 dsh-mcp-client
|
|
208
|
+
"status.hint": "绿标表示本机已注册 mcp__<系统>__* 工具。远端 MCP 通了但这里仍红,说明 dsh-mcp-client 还没挂上。企查查只在法务助理房间可用;其它房间不显示或显示「房间未启用」。",
|
|
206
209
|
"status.total": "当前角色 {role},共注册 {count} 个 MCP 工具",
|
|
207
210
|
"status.col.server": "Server",
|
|
208
211
|
"status.col.state": "状态",
|
|
@@ -304,6 +307,9 @@ const en = {
|
|
|
304
307
|
"config.gsc": "Google Search Console",
|
|
305
308
|
"config.gsc.siteUrl": "Site URL",
|
|
306
309
|
"config.gsc.key": "Service-account JSON or key file path",
|
|
310
|
+
"config.qcc": "Qichacha (Legal assistant)",
|
|
311
|
+
"config.qcc.url": "Qichacha company MCP URL",
|
|
312
|
+
"config.qcc.token": "API key",
|
|
307
313
|
"config.tianyancha": "Tianyancha (Legal assistant, boss only)",
|
|
308
314
|
"config.tianyancha.url": "Tianyancha MCP URL",
|
|
309
315
|
"config.tianyancha.token": "OpenAPI token",
|
|
@@ -322,7 +328,7 @@ const en = {
|
|
|
322
328
|
"config.testResult": "{server}: {status}",
|
|
323
329
|
"config.testOk": "OK ({tools} tools, {latency}ms)",
|
|
324
330
|
"config.testFail": "Failed: {error}",
|
|
325
|
-
"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.",
|
|
331
|
+
"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”.",
|
|
326
332
|
"status.total": "Role {role}, {count} MCP tools registered",
|
|
327
333
|
"status.col.server": "Server",
|
|
328
334
|
"status.col.state": "State",
|
|
@@ -696,7 +702,7 @@ function AuditTab({ api }) {
|
|
|
696
702
|
//#endregion
|
|
697
703
|
//#region src/client/panel/ConfigTab.tsx
|
|
698
704
|
/**
|
|
699
|
-
* Config tab: company MCP connections (Odoo / Nextcloud / WeCom / HA / TAPD / GSC),
|
|
705
|
+
* Config tab: company MCP connections (Odoo / Nextcloud / WeCom / HA / TAPD / GSC / QCC),
|
|
700
706
|
* role, and billing. Secrets are write-only and land in credentials.yaml.
|
|
701
707
|
*/
|
|
702
708
|
const ROLES = [
|
|
@@ -728,6 +734,7 @@ const EMPTY_SECRETS = {
|
|
|
728
734
|
homeassistant: "",
|
|
729
735
|
tapd: "",
|
|
730
736
|
gsc: "",
|
|
737
|
+
qcc: "",
|
|
731
738
|
tianyancha: ""
|
|
732
739
|
};
|
|
733
740
|
function Field({ label, children }) {
|
|
@@ -895,6 +902,10 @@ function ConfigTab({ api }) {
|
|
|
895
902
|
...config.connections.gsc,
|
|
896
903
|
secret: secrets.gsc || void 0
|
|
897
904
|
},
|
|
905
|
+
qcc: {
|
|
906
|
+
...config.connections.qcc,
|
|
907
|
+
secret: secrets.qcc || void 0
|
|
908
|
+
},
|
|
898
909
|
tianyancha: {
|
|
899
910
|
...config.connections.tianyancha,
|
|
900
911
|
secret: secrets.tianyancha || void 0
|
|
@@ -1110,6 +1121,25 @@ function ConfigTab({ api }) {
|
|
|
1110
1121
|
},
|
|
1111
1122
|
secretLabel: t("config.gsc.key")
|
|
1112
1123
|
}),
|
|
1124
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(McpSection, {
|
|
1125
|
+
title: t("config.qcc"),
|
|
1126
|
+
row: config.connections.qcc,
|
|
1127
|
+
secret: secrets.qcc,
|
|
1128
|
+
onEnabled: (enabled) => {
|
|
1129
|
+
patchConnection("qcc", { enabled });
|
|
1130
|
+
},
|
|
1131
|
+
onUrl: (url) => {
|
|
1132
|
+
patchConnection("qcc", { url });
|
|
1133
|
+
},
|
|
1134
|
+
onSecret: (value) => {
|
|
1135
|
+
setSecrets((prev) => ({
|
|
1136
|
+
...prev,
|
|
1137
|
+
qcc: value
|
|
1138
|
+
}));
|
|
1139
|
+
},
|
|
1140
|
+
urlLabel: t("config.qcc.url"),
|
|
1141
|
+
secretLabel: t("config.qcc.token")
|
|
1142
|
+
}),
|
|
1113
1143
|
config.role === "boss" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(McpSection, {
|
|
1114
1144
|
title: t("config.tianyancha"),
|
|
1115
1145
|
row: config.connections.tianyancha,
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -2844,6 +2844,57 @@ defineMethod("transform", [
|
|
|
2844
2844
|
"preserve"
|
|
2845
2845
|
], ({ inner }, isInner) => inner.toString(isInner));
|
|
2846
2846
|
//#endregion
|
|
2847
|
+
//#region src/rooms.ts
|
|
2848
|
+
/**
|
|
2849
|
+
* Workspace room identity for execution-time MCP gates.
|
|
2850
|
+
* Session cwd is the workspace folder (e.g. ~/.dsh/workspaces/法务助理).
|
|
2851
|
+
*/
|
|
2852
|
+
const LEGAL_ROOM_FOLDER = "法务助理";
|
|
2853
|
+
const QCC_ROOM_DENY = "企查查仅限法务助理房间使用,请切换到法务助理再查询。";
|
|
2854
|
+
const QCC_STATUS_ROOM_OFF = "房间未启用";
|
|
2855
|
+
const ROOM_SEP = /[/\\]/;
|
|
2856
|
+
/** Last path segment of a workspace cwd or folder name. */
|
|
2857
|
+
function roomFolderFromPath(value) {
|
|
2858
|
+
if (typeof value !== "string") return void 0;
|
|
2859
|
+
const trimmed = value.trim().replace(/[/\\]+$/, "");
|
|
2860
|
+
if (trimmed.length === 0) return void 0;
|
|
2861
|
+
return trimmed.split(ROOM_SEP).filter((part) => part.length > 0).at(-1);
|
|
2862
|
+
}
|
|
2863
|
+
function isLegalAssistantRoom(hint) {
|
|
2864
|
+
return roomFolderFromPath(hint) === LEGAL_ROOM_FOLDER;
|
|
2865
|
+
}
|
|
2866
|
+
function isQccToolName(name) {
|
|
2867
|
+
return name.startsWith("mcp__qcc-") || name.startsWith("mcp__qcc__");
|
|
2868
|
+
}
|
|
2869
|
+
/** Session cwd from a tool execution. Missing / unknown shapes → undefined. */
|
|
2870
|
+
function cwdFromExecution(execution) {
|
|
2871
|
+
const agent = execution.agent;
|
|
2872
|
+
if (typeof agent !== "object" || agent === null) return void 0;
|
|
2873
|
+
const record = agent;
|
|
2874
|
+
const session = record.session;
|
|
2875
|
+
const header = typeof session === "object" && session !== null ? session.header : void 0;
|
|
2876
|
+
const candidates = [
|
|
2877
|
+
typeof header === "object" && header !== null ? header.cwd : void 0,
|
|
2878
|
+
typeof session === "object" && session !== null ? session.cwd : void 0,
|
|
2879
|
+
record.cwd
|
|
2880
|
+
];
|
|
2881
|
+
for (const value of candidates) if (typeof value === "string" && value.trim().length > 0) return value;
|
|
2882
|
+
}
|
|
2883
|
+
/**
|
|
2884
|
+
* Official `ctx.tools.guard` reason. Only Qichacha tools are gated.
|
|
2885
|
+
* Must never throw: this guard is global and a throw would block WeChat turns.
|
|
2886
|
+
* Missing cwd fails closed — not the legal room.
|
|
2887
|
+
*/
|
|
2888
|
+
function qccRoomGuard(execution) {
|
|
2889
|
+
if (!isQccToolName(typeof execution?.name === "string" ? execution.name : "")) return void 0;
|
|
2890
|
+
try {
|
|
2891
|
+
if (isLegalAssistantRoom(cwdFromExecution(execution))) return void 0;
|
|
2892
|
+
return QCC_ROOM_DENY;
|
|
2893
|
+
} catch {
|
|
2894
|
+
return QCC_ROOM_DENY;
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
//#endregion
|
|
2847
2898
|
//#region src/observe-mcp.ts
|
|
2848
2899
|
/**
|
|
2849
2900
|
* Live MCP status without a second host.
|
|
@@ -2859,9 +2910,11 @@ const COMPOSED_SERVERS = [
|
|
|
2859
2910
|
"homeassistant",
|
|
2860
2911
|
"tapd",
|
|
2861
2912
|
"gsc",
|
|
2913
|
+
"qcc",
|
|
2862
2914
|
"tianyancha"
|
|
2863
2915
|
];
|
|
2864
2916
|
function mcpToolPrefix(server) {
|
|
2917
|
+
if (server === "qcc") return "mcp__qcc-";
|
|
2865
2918
|
return "mcp__" + server + "__";
|
|
2866
2919
|
}
|
|
2867
2920
|
/** Count tools whose public name belongs to one composed MCP server. */
|
|
@@ -2920,8 +2973,27 @@ function observeServer(server, input) {
|
|
|
2920
2973
|
lastError: startError !== void 0 ? ref + " 已配置,但 MCP 未挂上:" + startError : ref + " 已配置,但本机尚未注册 " + mcpToolPrefix(server) + "*(dsh-mcp-client 未挂上)"
|
|
2921
2974
|
};
|
|
2922
2975
|
}
|
|
2976
|
+
function composedServersFor(input) {
|
|
2977
|
+
const servers = input.config.role === "boss" ? COMPOSED_SERVERS : COMPOSED_SERVERS.filter((server) => server !== "tianyancha");
|
|
2978
|
+
if (input.room !== void 0 && !isLegalAssistantRoom(input.room)) return servers.filter((server) => server !== "qcc");
|
|
2979
|
+
return [...servers];
|
|
2980
|
+
}
|
|
2981
|
+
function observeQccOutsideLegalRoom(input) {
|
|
2982
|
+
if (isEnabled(input.config, "qcc") === false) return observeServer("qcc", input);
|
|
2983
|
+
return {
|
|
2984
|
+
server: "qcc",
|
|
2985
|
+
enabled: true,
|
|
2986
|
+
state: "disconnected",
|
|
2987
|
+
toolCount: 0,
|
|
2988
|
+
registeredTools: 0,
|
|
2989
|
+
lastError: QCC_STATUS_ROOM_OFF
|
|
2990
|
+
};
|
|
2991
|
+
}
|
|
2923
2992
|
function observeComposedServers(input) {
|
|
2924
|
-
return (input
|
|
2993
|
+
return composedServersFor(input).map((server) => {
|
|
2994
|
+
if (server === "qcc" && !isLegalAssistantRoom(input.room)) return observeQccOutsideLegalRoom(input);
|
|
2995
|
+
return observeServer(server, input);
|
|
2996
|
+
});
|
|
2925
2997
|
}
|
|
2926
2998
|
function testComposedServers(input) {
|
|
2927
2999
|
return observeComposedServers(input).map((row) => {
|
|
@@ -3051,6 +3123,12 @@ function publicConfig(config) {
|
|
|
3051
3123
|
secretConfigured: secretConfigured("gsc"),
|
|
3052
3124
|
secretRef: REZ_CREDENTIAL_REFS.gsc
|
|
3053
3125
|
},
|
|
3126
|
+
qcc: {
|
|
3127
|
+
enabled: connections.qcc.enabled,
|
|
3128
|
+
url: connections.qcc.url,
|
|
3129
|
+
secretConfigured: secretConfigured("qcc"),
|
|
3130
|
+
secretRef: REZ_CREDENTIAL_REFS.qcc
|
|
3131
|
+
},
|
|
3054
3132
|
tianyancha: {
|
|
3055
3133
|
enabled: connections.tianyancha.enabled,
|
|
3056
3134
|
url: connections.tianyancha.url,
|
|
@@ -7679,8 +7757,8 @@ function rezStatusTool(host, config) {
|
|
|
7679
7757
|
].join("\n"));
|
|
7680
7758
|
}
|
|
7681
7759
|
},
|
|
7682
|
-
async execute() {
|
|
7683
|
-
const servers = host.status().map((server) => {
|
|
7760
|
+
async execute(_args, exec) {
|
|
7761
|
+
const servers = host.status(cwdFromExecution(exec ?? {})).map((server) => {
|
|
7684
7762
|
const clean = { ...server };
|
|
7685
7763
|
if (clean.lastError === void 0) delete clean.lastError;
|
|
7686
7764
|
if (clean.startedAt === void 0) delete clean.startedAt;
|
|
@@ -8094,7 +8172,7 @@ const TIANYANCHA_MCP_URL = "https://mcp.tianyancha.com/mcp";
|
|
|
8094
8172
|
function shouldStartTianyancha(role) {
|
|
8095
8173
|
return role === "boss";
|
|
8096
8174
|
}
|
|
8097
|
-
function bearerAuthorization(secret) {
|
|
8175
|
+
function bearerAuthorization$1(secret) {
|
|
8098
8176
|
const trimmed = secret.trim();
|
|
8099
8177
|
if (/^bearer\s+/i.test(trimmed)) return "Bearer " + trimmed.replace(/^bearer\s+/i, "");
|
|
8100
8178
|
return "Bearer " + trimmed;
|
|
@@ -8105,7 +8183,7 @@ function tianyanchaMcpConfig(secret) {
|
|
|
8105
8183
|
serverName: "tianyancha",
|
|
8106
8184
|
transport: "streamable-http",
|
|
8107
8185
|
url: url.length > 0 ? url : TIANYANCHA_MCP_URL,
|
|
8108
|
-
headers: { Authorization: bearerAuthorization(secret) },
|
|
8186
|
+
headers: { Authorization: bearerAuthorization$1(secret) },
|
|
8109
8187
|
failOnStartupError: false
|
|
8110
8188
|
};
|
|
8111
8189
|
}
|
|
@@ -8148,6 +8226,80 @@ function mountTianyanchaMcp(ctx, getRole, start = startMcpWhenSecret) {
|
|
|
8148
8226
|
return ensure;
|
|
8149
8227
|
}
|
|
8150
8228
|
//#endregion
|
|
8229
|
+
//#region src/qcc.ts
|
|
8230
|
+
const QCC_COMPANY_MCP_URL = "https://agent.qcc.com/mcp/company/stream";
|
|
8231
|
+
const QCC_RISK_MCP_URL = "https://agent.qcc.com/mcp/risk/stream";
|
|
8232
|
+
function bearerAuthorization(secret) {
|
|
8233
|
+
const trimmed = secret.trim();
|
|
8234
|
+
if (/^bearer\s+/i.test(trimmed)) return "Bearer " + trimmed.replace(/^bearer\s+/i, "");
|
|
8235
|
+
return "Bearer " + trimmed;
|
|
8236
|
+
}
|
|
8237
|
+
/** Risk stream URL: swap /company/ for /risk/ when the company URL is official-shaped. */
|
|
8238
|
+
function qccRiskUrlFrom(companyUrl) {
|
|
8239
|
+
const trimmed = companyUrl.trim();
|
|
8240
|
+
if (trimmed.includes("/company/")) return trimmed.replace("/company/", "/risk/");
|
|
8241
|
+
return QCC_RISK_MCP_URL;
|
|
8242
|
+
}
|
|
8243
|
+
function qccHeaders(secret) {
|
|
8244
|
+
return { Authorization: bearerAuthorization(secret) };
|
|
8245
|
+
}
|
|
8246
|
+
function qccCompanyUrl() {
|
|
8247
|
+
const url = loadRezConnections().qcc?.url;
|
|
8248
|
+
return typeof url === "string" && url.length > 0 ? url : QCC_COMPANY_MCP_URL;
|
|
8249
|
+
}
|
|
8250
|
+
function qccCompanyMcpConfig(secret) {
|
|
8251
|
+
return {
|
|
8252
|
+
serverName: "qcc-company",
|
|
8253
|
+
transport: "streamable-http",
|
|
8254
|
+
url: qccCompanyUrl(),
|
|
8255
|
+
headers: qccHeaders(secret),
|
|
8256
|
+
failOnStartupError: false
|
|
8257
|
+
};
|
|
8258
|
+
}
|
|
8259
|
+
function qccRiskMcpConfig(secret) {
|
|
8260
|
+
return {
|
|
8261
|
+
serverName: "qcc-risk",
|
|
8262
|
+
transport: "streamable-http",
|
|
8263
|
+
url: qccRiskUrlFrom(qccCompanyUrl()),
|
|
8264
|
+
headers: qccHeaders(secret),
|
|
8265
|
+
failOnStartupError: false
|
|
8266
|
+
};
|
|
8267
|
+
}
|
|
8268
|
+
/** Start company + risk clients whenever the Qichacha secret exists. */
|
|
8269
|
+
function mountQccMcp(ctx, start = startMcpWhenSecret) {
|
|
8270
|
+
let session;
|
|
8271
|
+
let pending = false;
|
|
8272
|
+
const ensure = () => {
|
|
8273
|
+
(async () => {
|
|
8274
|
+
if (session !== void 0 || pending) return;
|
|
8275
|
+
pending = true;
|
|
8276
|
+
try {
|
|
8277
|
+
const disposeCompany = await start(ctx, {
|
|
8278
|
+
system: "qcc",
|
|
8279
|
+
label: "企查查·企业",
|
|
8280
|
+
config: qccCompanyMcpConfig
|
|
8281
|
+
});
|
|
8282
|
+
const disposeRisk = await start(ctx, {
|
|
8283
|
+
system: "qcc",
|
|
8284
|
+
label: "企查查·风险",
|
|
8285
|
+
config: qccRiskMcpConfig
|
|
8286
|
+
});
|
|
8287
|
+
session = () => {
|
|
8288
|
+
disposeCompany();
|
|
8289
|
+
disposeRisk();
|
|
8290
|
+
};
|
|
8291
|
+
} catch (error) {
|
|
8292
|
+
const why = error instanceof Error ? error.message : String(error);
|
|
8293
|
+
console.error("[dsh-rez-suite] qcc MCP start failed:", why);
|
|
8294
|
+
} finally {
|
|
8295
|
+
pending = false;
|
|
8296
|
+
}
|
|
8297
|
+
})();
|
|
8298
|
+
};
|
|
8299
|
+
ensure();
|
|
8300
|
+
return ensure;
|
|
8301
|
+
}
|
|
8302
|
+
//#endregion
|
|
8151
8303
|
//#region src/index.ts
|
|
8152
8304
|
const name = "rez-suite";
|
|
8153
8305
|
const inject = [
|
|
@@ -8202,10 +8354,20 @@ const Config = Schema.object({
|
|
|
8202
8354
|
enabled: Schema.boolean().default(true),
|
|
8203
8355
|
siteUrl: Schema.string().default(REZ_DEFAULT_CONNECTIONS.gsc.siteUrl)
|
|
8204
8356
|
}).default(REZ_DEFAULT_CONNECTIONS.gsc),
|
|
8357
|
+
qcc: Schema.object({
|
|
8358
|
+
enabled: Schema.boolean().default(true),
|
|
8359
|
+
url: Schema.string().default(REZ_DEFAULT_CONNECTIONS.qcc?.url ?? "https://agent.qcc.com/mcp/company/stream")
|
|
8360
|
+
}).default(REZ_DEFAULT_CONNECTIONS.qcc ?? {
|
|
8361
|
+
enabled: true,
|
|
8362
|
+
url: "https://agent.qcc.com/mcp/company/stream"
|
|
8363
|
+
}),
|
|
8205
8364
|
tianyancha: Schema.object({
|
|
8206
8365
|
enabled: Schema.boolean().default(true),
|
|
8207
|
-
url: Schema.string().default(REZ_DEFAULT_CONNECTIONS.tianyancha.
|
|
8208
|
-
}).default(REZ_DEFAULT_CONNECTIONS.tianyancha
|
|
8366
|
+
url: Schema.string().default(REZ_DEFAULT_CONNECTIONS.tianyancha?.url ?? "https://mcp.tianyancha.com/mcp")
|
|
8367
|
+
}).default(REZ_DEFAULT_CONNECTIONS.tianyancha ?? {
|
|
8368
|
+
enabled: true,
|
|
8369
|
+
url: "https://mcp.tianyancha.com/mcp"
|
|
8370
|
+
})
|
|
8209
8371
|
}).default(REZ_DEFAULT_CONNECTIONS),
|
|
8210
8372
|
billing: Schema.object({
|
|
8211
8373
|
inputCostPer1k: Schema.number().default(.001),
|
|
@@ -8237,12 +8399,13 @@ function normalize(input) {
|
|
|
8237
8399
|
return mergeConfig(defaultConfig(), input ?? {});
|
|
8238
8400
|
}
|
|
8239
8401
|
function delegatedHost(ctx, getConfig) {
|
|
8240
|
-
const snapshot = () => observeComposedServers({
|
|
8402
|
+
const snapshot = (room) => observeComposedServers({
|
|
8241
8403
|
config: getConfig(),
|
|
8242
|
-
toolNames: listRegisteredToolNames(ctx.tools)
|
|
8404
|
+
toolNames: listRegisteredToolNames(ctx.tools),
|
|
8405
|
+
room
|
|
8243
8406
|
});
|
|
8244
8407
|
return {
|
|
8245
|
-
status: () => snapshot(),
|
|
8408
|
+
status: (room) => snapshot(room),
|
|
8246
8409
|
test: async () => testComposedServers({
|
|
8247
8410
|
config: getConfig(),
|
|
8248
8411
|
toolNames: listRegisteredToolNames(ctx.tools)
|
|
@@ -8254,12 +8417,15 @@ function delegatedHost(ctx, getConfig) {
|
|
|
8254
8417
|
function apply(ctx, config) {
|
|
8255
8418
|
let current = () => normalize(config);
|
|
8256
8419
|
const syncWorkspaces = mountRezWorkspaces(ctx, () => current().role);
|
|
8420
|
+
const syncQcc = mountQccMcp(ctx);
|
|
8257
8421
|
const syncTianyancha = mountTianyanchaMcp(ctx, () => current().role);
|
|
8258
8422
|
const tokens = new TokenManager(joinTokenDb());
|
|
8259
8423
|
const host = delegatedHost(ctx, () => current());
|
|
8260
8424
|
ctx.effect(() => () => {
|
|
8261
8425
|
tokens.close();
|
|
8262
8426
|
}, "dsh-rez-suite: ledger view");
|
|
8427
|
+
const toolsHost = ctx.tools;
|
|
8428
|
+
if (typeof toolsHost.guard === "function") ctx.effect(() => toolsHost.guard(qccRoomGuard), "dsh-rez-suite: qcc room guard");
|
|
8263
8429
|
const tools = [rezStatusTool(host, () => current()), rezConfigTool(() => current())];
|
|
8264
8430
|
const routes = makeRoutes({
|
|
8265
8431
|
getConfig: () => current(),
|
|
@@ -8268,6 +8434,7 @@ function apply(ctx, config) {
|
|
|
8268
8434
|
saveConfig(value);
|
|
8269
8435
|
current = () => value;
|
|
8270
8436
|
syncWorkspaces();
|
|
8437
|
+
syncQcc();
|
|
8271
8438
|
syncTianyancha();
|
|
8272
8439
|
try {
|
|
8273
8440
|
const settings = ctx.get("settings");
|
|
@@ -8326,11 +8493,13 @@ function apply(ctx, config) {
|
|
|
8326
8493
|
current = source;
|
|
8327
8494
|
sync();
|
|
8328
8495
|
syncWorkspaces();
|
|
8496
|
+
syncQcc();
|
|
8329
8497
|
syncTianyancha();
|
|
8330
8498
|
},
|
|
8331
8499
|
onChange: () => {
|
|
8332
8500
|
sync();
|
|
8333
8501
|
syncWorkspaces();
|
|
8502
|
+
syncQcc();
|
|
8334
8503
|
syncTianyancha();
|
|
8335
8504
|
}
|
|
8336
8505
|
});
|
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.
|
|
4
|
+
"version": "0.1.35",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -37,17 +37,17 @@
|
|
|
37
37
|
}
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
+
"@rezti/dsh-rez-gsc": "^0.1.1",
|
|
41
|
+
"@rezti/dsh-rez-ha-bridge": "^0.1.9",
|
|
42
|
+
"@rezti/dsh-rez-intent": "^0.1.5",
|
|
43
|
+
"@rezti/dsh-rez-nextcloud": "^0.1.9",
|
|
44
|
+
"@rezti/dsh-rez-odoo": "^0.1.9",
|
|
45
|
+
"@rezti/dsh-rez-sso": "^0.1.11",
|
|
46
|
+
"@rezti/dsh-rez-tapd": "^0.1.1",
|
|
47
|
+
"@rezti/dsh-rez-token-manager": "^0.1.4",
|
|
48
|
+
"@rezti/dsh-rez-wechat": "^0.1.14",
|
|
40
49
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
41
|
-
"zod": "^4.4.3"
|
|
42
|
-
"@rezti/dsh-rez-gsc": "0.1.0",
|
|
43
|
-
"@rezti/dsh-rez-ha-bridge": "0.1.9",
|
|
44
|
-
"@rezti/dsh-rez-odoo": "0.1.9",
|
|
45
|
-
"@rezti/dsh-rez-nextcloud": "0.1.9",
|
|
46
|
-
"@rezti/dsh-rez-token-manager": "0.1.4",
|
|
47
|
-
"@rezti/dsh-rez-tapd": "0.1.0",
|
|
48
|
-
"@rezti/dsh-rez-wechat": "0.1.14",
|
|
49
|
-
"@rezti/dsh-rez-sso": "0.1.10",
|
|
50
|
-
"@rezti/dsh-rez-intent": "0.1.5"
|
|
50
|
+
"zod": "^4.4.3"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
53
|
"react": "^18.2.0",
|
|
@@ -95,16 +95,16 @@
|
|
|
95
95
|
"publishConfig": {
|
|
96
96
|
"access": "public"
|
|
97
97
|
},
|
|
98
|
-
"repository": {
|
|
99
|
-
"type": "git",
|
|
100
|
-
"url": "git+https://github.com/ReZ-TI/deepseek-harness.git",
|
|
101
|
-
"directory": "packages/dsh-rez-suite"
|
|
102
|
-
},
|
|
103
98
|
"scripts": {
|
|
104
99
|
"build": "tsc -p tsconfig.build.json && tsdown --config-loader native && node scripts/wrap-client.mjs",
|
|
105
100
|
"bundle": "tsdown --config-loader native",
|
|
106
101
|
"watch": "tsdown --config-loader native --watch",
|
|
107
102
|
"typecheck": "tsc --noEmit",
|
|
108
103
|
"test": "vitest run"
|
|
104
|
+
},
|
|
105
|
+
"repository": {
|
|
106
|
+
"type": "git",
|
|
107
|
+
"url": "git+https://github.com/ReZ-TI/deepseek-harness.git",
|
|
108
|
+
"directory": "packages/dsh-rez-suite"
|
|
109
109
|
}
|
|
110
|
-
}
|
|
110
|
+
}
|
package/src/client/locales.ts
CHANGED
|
@@ -52,6 +52,9 @@ export const zh = {
|
|
|
52
52
|
'config.gsc': 'Google Search Console',
|
|
53
53
|
'config.gsc.siteUrl': '站点 URL',
|
|
54
54
|
'config.gsc.key': '服务账号 JSON 或密钥文件路径',
|
|
55
|
+
'config.qcc': '企查查(法务助理)',
|
|
56
|
+
'config.qcc.url': '企查查企业 MCP URL',
|
|
57
|
+
'config.qcc.token': 'API Key',
|
|
55
58
|
'config.tianyancha': '天眼查(法务助理 · 仅老板)',
|
|
56
59
|
'config.tianyancha.url': '天眼查 MCP URL',
|
|
57
60
|
'config.tianyancha.token': 'OpenAPI Token',
|
|
@@ -71,7 +74,7 @@ export const zh = {
|
|
|
71
74
|
'config.testOk': '成功({tools} 个工具, {latency}ms)',
|
|
72
75
|
'config.testFail': '失败:{error}',
|
|
73
76
|
// status
|
|
74
|
-
'status.hint': '绿标表示本机已注册 mcp__<系统>__* 工具。远端 MCP 通了但这里仍红,说明 dsh-mcp-client
|
|
77
|
+
'status.hint': '绿标表示本机已注册 mcp__<系统>__* 工具。远端 MCP 通了但这里仍红,说明 dsh-mcp-client 还没挂上。企查查只在法务助理房间可用;其它房间不显示或显示「房间未启用」。',
|
|
75
78
|
'status.total': '当前角色 {role},共注册 {count} 个 MCP 工具',
|
|
76
79
|
'status.col.server': 'Server',
|
|
77
80
|
'status.col.state': '状态',
|
|
@@ -178,6 +181,9 @@ export const en: Record<RezKey, string> = {
|
|
|
178
181
|
'config.gsc': 'Google Search Console',
|
|
179
182
|
'config.gsc.siteUrl': 'Site URL',
|
|
180
183
|
'config.gsc.key': 'Service-account JSON or key file path',
|
|
184
|
+
'config.qcc': 'Qichacha (Legal assistant)',
|
|
185
|
+
'config.qcc.url': 'Qichacha company MCP URL',
|
|
186
|
+
'config.qcc.token': 'API key',
|
|
181
187
|
'config.tianyancha': 'Tianyancha (Legal assistant, boss only)',
|
|
182
188
|
'config.tianyancha.url': 'Tianyancha MCP URL',
|
|
183
189
|
'config.tianyancha.token': 'OpenAPI token',
|
|
@@ -196,7 +202,7 @@ export const en: Record<RezKey, string> = {
|
|
|
196
202
|
'config.testResult': '{server}: {status}',
|
|
197
203
|
'config.testOk': 'OK ({tools} tools, {latency}ms)',
|
|
198
204
|
'config.testFail': 'Failed: {error}',
|
|
199
|
-
'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.',
|
|
205
|
+
'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”.',
|
|
200
206
|
'status.total': 'Role {role}, {count} MCP tools registered',
|
|
201
207
|
'status.col.server': 'Server',
|
|
202
208
|
'status.col.state': 'State',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Config tab: company MCP connections (Odoo / Nextcloud / WeCom / HA / TAPD / GSC),
|
|
2
|
+
* Config tab: company MCP connections (Odoo / Nextcloud / WeCom / HA / TAPD / GSC / QCC),
|
|
3
3
|
* role, and billing. Secrets are write-only and land in credentials.yaml.
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -24,10 +24,11 @@ type SecretDrafts = {
|
|
|
24
24
|
homeassistant: string
|
|
25
25
|
tapd: string
|
|
26
26
|
gsc: string
|
|
27
|
+
qcc: string
|
|
27
28
|
tianyancha: string
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
const EMPTY_SECRETS: SecretDrafts = { odoo: '', nextcloud: '', wechat: '', homeassistant: '', tapd: '', gsc: '', tianyancha: '' }
|
|
31
|
+
const EMPTY_SECRETS: SecretDrafts = { odoo: '', nextcloud: '', wechat: '', homeassistant: '', tapd: '', gsc: '', qcc: '', tianyancha: '' }
|
|
31
32
|
|
|
32
33
|
function Field({ label, children }: { label: string; children: ReactNode }) {
|
|
33
34
|
return (
|
|
@@ -174,6 +175,7 @@ export function ConfigTab({ api }: { api: RezApi }) {
|
|
|
174
175
|
homeassistant: { ...config.connections.homeassistant, secret: secrets.homeassistant || undefined },
|
|
175
176
|
tapd: { ...config.connections.tapd, secret: secrets.tapd || undefined },
|
|
176
177
|
gsc: { ...config.connections.gsc, secret: secrets.gsc || undefined },
|
|
178
|
+
qcc: { ...config.connections.qcc, secret: secrets.qcc || undefined },
|
|
177
179
|
tianyancha: { ...config.connections.tianyancha, secret: secrets.tianyancha || undefined },
|
|
178
180
|
},
|
|
179
181
|
}
|
|
@@ -296,6 +298,16 @@ export function ConfigTab({ api }: { api: RezApi }) {
|
|
|
296
298
|
onSecret={value => { setSecrets(prev => ({ ...prev, gsc: value })) }}
|
|
297
299
|
secretLabel={t('config.gsc.key')}
|
|
298
300
|
/>
|
|
301
|
+
<McpSection
|
|
302
|
+
title={t('config.qcc')}
|
|
303
|
+
row={config.connections.qcc}
|
|
304
|
+
secret={secrets.qcc}
|
|
305
|
+
onEnabled={enabled => { patchConnection('qcc', { enabled }) }}
|
|
306
|
+
onUrl={url => { patchConnection('qcc', { url }) }}
|
|
307
|
+
onSecret={value => { setSecrets(prev => ({ ...prev, qcc: value })) }}
|
|
308
|
+
urlLabel={t('config.qcc.url')}
|
|
309
|
+
secretLabel={t('config.qcc.token')}
|
|
310
|
+
/>
|
|
299
311
|
{config.role === 'boss' && (
|
|
300
312
|
<McpSection
|
|
301
313
|
title={t('config.tianyancha')}
|
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),
|
|
@@ -117,12 +123,13 @@ function normalize(input: Partial<RezConfig> | undefined): RezConfig {
|
|
|
117
123
|
}
|
|
118
124
|
|
|
119
125
|
function delegatedHost(ctx: Context, getConfig: () => RezConfig): McpHost {
|
|
120
|
-
const snapshot = () => observeComposedServers({
|
|
126
|
+
const snapshot = (room?: string) => observeComposedServers({
|
|
121
127
|
config: getConfig(),
|
|
122
128
|
toolNames: listRegisteredToolNames(ctx.tools),
|
|
129
|
+
room,
|
|
123
130
|
})
|
|
124
131
|
return {
|
|
125
|
-
status: () => snapshot(),
|
|
132
|
+
status: (room?: string) => snapshot(room),
|
|
126
133
|
test: async () => testComposedServers({
|
|
127
134
|
config: getConfig(),
|
|
128
135
|
toolNames: listRegisteredToolNames(ctx.tools),
|
|
@@ -135,11 +142,16 @@ function delegatedHost(ctx: Context, getConfig: () => RezConfig): McpHost {
|
|
|
135
142
|
export function apply(ctx: Context, config?: RezConfig): void {
|
|
136
143
|
let current: () => RezConfig = () => normalize(config)
|
|
137
144
|
const syncWorkspaces = mountRezWorkspaces(ctx, () => current().role)
|
|
145
|
+
const syncQcc = mountQccMcp(ctx)
|
|
138
146
|
const syncTianyancha = mountTianyanchaMcp(ctx, () => current().role)
|
|
139
147
|
|
|
140
148
|
const tokens = new TokenManager(joinTokenDb())
|
|
141
149
|
const host = delegatedHost(ctx, () => current())
|
|
142
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
|
+
}
|
|
143
155
|
|
|
144
156
|
const tools = [
|
|
145
157
|
rezStatusTool(host, () => current()),
|
|
@@ -153,6 +165,7 @@ export function apply(ctx: Context, config?: RezConfig): void {
|
|
|
153
165
|
saveConfig(value)
|
|
154
166
|
current = () => value
|
|
155
167
|
syncWorkspaces()
|
|
168
|
+
syncQcc()
|
|
156
169
|
syncTianyancha()
|
|
157
170
|
try {
|
|
158
171
|
const settings = ctx.get('settings') as { update?: (namespace: unknown, value: unknown) => Promise<void> } | undefined
|
|
@@ -207,11 +220,13 @@ export function apply(ctx: Context, config?: RezConfig): void {
|
|
|
207
220
|
current = source
|
|
208
221
|
sync()
|
|
209
222
|
syncWorkspaces()
|
|
223
|
+
syncQcc()
|
|
210
224
|
syncTianyancha()
|
|
211
225
|
},
|
|
212
226
|
onChange: () => {
|
|
213
227
|
sync()
|
|
214
228
|
syncWorkspaces()
|
|
229
|
+
syncQcc()
|
|
215
230
|
syncTianyancha()
|
|
216
231
|
},
|
|
217
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,
|
package/src/tools.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { RezConfig, RezTestResult } from './protocol.ts'
|
|
|
12
12
|
import type { McpHost } from './mcp-host.ts'
|
|
13
13
|
import type { TokenManager } from './token-manager.ts'
|
|
14
14
|
import { publicConfig } from './store.ts'
|
|
15
|
+
import { cwdFromExecution } from './rooms.ts'
|
|
15
16
|
|
|
16
17
|
function text(value: string): ContentBlock[] {
|
|
17
18
|
return [{ type: 'text', text: value }]
|
|
@@ -69,8 +70,8 @@ export function rezStatusTool(host: McpHost, config: () => RezConfig) {
|
|
|
69
70
|
return text('role: ' + (value.role ?? '') + '\n' + ['server | state | discovered | registered | error', '--- | --- | --- | --- | ---', ...rows].join('\n'))
|
|
70
71
|
},
|
|
71
72
|
},
|
|
72
|
-
async execute() {
|
|
73
|
-
const servers = host.status().map(server => {
|
|
73
|
+
async execute(_args: unknown, exec?: { agent?: unknown }) {
|
|
74
|
+
const servers = host.status(cwdFromExecution(exec ?? {})).map(server => {
|
|
74
75
|
const clean: Record<string, unknown> = { ...server }
|
|
75
76
|
if (clean.lastError === undefined) delete clean.lastError
|
|
76
77
|
if (clean.startedAt === undefined) delete clean.startedAt
|
|
@@ -3,4 +3,4 @@ name: boss-money
|
|
|
3
3
|
description: 老板·搞钱作战室。回款/应收/库存占用简报;初始化后四段回答;Odoo 只读。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
本 Skill 的完整规矩在工作区根目录 `AGENTS.md`。有 `BOOTSTRAP.md` 时先做三拍仪式。记忆写在 `PRIORITIES.md`、`MEMORY.md`、`memory/YYYY-MM-DD.md
|
|
6
|
+
本 Skill 的完整规矩在工作区根目录 `AGENTS.md`。有 `BOOTSTRAP.md` 时先做三拍仪式。记忆写在 `PRIORITIES.md`、`MEMORY.md`、`memory/YYYY-MM-DD.md`。工商查询请切到法务助理,避免串房间。
|
|
@@ -3,4 +3,4 @@ name: boss-ops
|
|
|
3
3
|
description: 老板·运营作战室。只报红灯(缺料/逾期/超账期/系统);初始化后四段回答。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
完整规矩在工作区 `AGENTS.md`。有 `BOOTSTRAP.md` 时先做三拍。记忆写在 `PRIORITIES.md`、`MEMORY.md`、`memory/YYYY-MM-DD.md
|
|
6
|
+
完整规矩在工作区 `AGENTS.md`。有 `BOOTSTRAP.md` 时先做三拍。记忆写在 `PRIORITIES.md`、`MEMORY.md`、`memory/YYYY-MM-DD.md`。工商查询请切到法务助理,避免串房间。
|
|
@@ -3,4 +3,4 @@ name: boss-product
|
|
|
3
3
|
description: 老板·产品作战室。主销 SKU / 质量认证 / 该砍该保;初始化后四段回答。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
完整规矩在工作区 `AGENTS.md`。有 `BOOTSTRAP.md` 时先做三拍。记忆写在 `PRIORITIES.md`、`MEMORY.md`、`memory/YYYY-MM-DD.md
|
|
6
|
+
完整规矩在工作区 `AGENTS.md`。有 `BOOTSTRAP.md` 时先做三拍。记忆写在 `PRIORITIES.md`、`MEMORY.md`、`memory/YYYY-MM-DD.md`。工商查询请切到法务助理,避免串房间。
|
|
@@ -42,3 +42,4 @@
|
|
|
42
42
|
- 新产品或改产品 Wiki:Wiki **必须**同时写死 Nextcloud RAW 路径,以及 Git 3D 源路径 `products/{slug}/models` 与 `products/{slug}/motion`。缺任一项就说「Wiki 未归档」,不要猜路径。
|
|
43
43
|
- 问某产品素材在哪:打开 Nextcloud `Documents/Standards/INDEX.yaml`,再按相对 uri 读路径规则;加上该产品 Wiki **首次创建标题**,不要用会变的 Odoo `name` / 销售标题。
|
|
44
44
|
- 禁止在这里写固件、开辅线、做 HA Add-on、做微信通道。那些走仓库和员工机。
|
|
45
|
+
- 工商查询请切到法务助理,避免串房间。
|
|
@@ -3,4 +3,4 @@ name: staff-finance
|
|
|
3
3
|
description: 财务助理。全员报销;审批/报表/过账仅当当前 Odoo 帐号能看见对应单据。不自审不自过账。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
完整规矩在工作区 `AGENTS.md`。先 `mcp__odoo__get_current_context`。报销走 `hr.expense` / `hr.expense.sheet`。搞钱室不要在这里做。问制度/报销标准先打开 Nextcloud `Documents/Standards/INDEX.yaml`,再按相对 uri 读,不要把手册写入 MEMORY
|
|
6
|
+
完整规矩在工作区 `AGENTS.md`。先 `mcp__odoo__get_current_context`。报销走 `hr.expense` / `hr.expense.sheet`。搞钱室不要在这里做。问制度/报销标准先打开 Nextcloud `Documents/Standards/INDEX.yaml`,再按相对 uri 读,不要把手册写入 MEMORY。工商查询请切到法务助理,避免串房间。用户用中文就中文、用英文就英文。
|
|
@@ -3,4 +3,4 @@ name: staff-hr
|
|
|
3
3
|
description: 人事助理。全员请假/查自己;主管批假;权限完全跟当前 Odoo 帐号。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
完整规矩在工作区 `AGENTS.md`。先 `mcp__odoo__get_current_context`。有 `BOOTSTRAP.md` 时正事优先再三拍。问制度/出差标准先打开 Nextcloud `Documents/Standards/INDEX.yaml`,再按相对 uri 读,不要把手册写入 MEMORY
|
|
6
|
+
完整规矩在工作区 `AGENTS.md`。先 `mcp__odoo__get_current_context`。有 `BOOTSTRAP.md` 时正事优先再三拍。问制度/出差标准先打开 Nextcloud `Documents/Standards/INDEX.yaml`,再按相对 uri 读,不要把手册写入 MEMORY。工商查询请切到法务助理,避免串房间。用户用中文就中文、用英文就英文。
|
|
@@ -3,4 +3,4 @@ name: staff-legal
|
|
|
3
3
|
description: 法务助理。全员可问制度、整理风险;能看哪些合同跟当前 Odoo/Nextcloud 帐号。不下具约束力的法律结论。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
完整规矩在工作区 `AGENTS.md`。先 `mcp__odoo__get_current_context`。不是律师。问制度先打开 Nextcloud `Documents/Standards/INDEX.yaml`,再按相对 uri 读,不要把手册写入 MEMORY
|
|
6
|
+
完整规矩在工作区 `AGENTS.md`。先 `mcp__odoo__get_current_context`。不是律师。问制度先打开 Nextcloud `Documents/Standards/INDEX.yaml`,再按相对 uri 读,不要把手册写入 MEMORY。查对方公司(仅本法务房间):有 `mcp__qcc-*` 先用企查查;否则有 `mcp__tianyancha__*` 再用天眼查(仅老板会挂上天眼查);都没有就说工商查询未开通。用户用中文就中文、用英文就英文。
|
|
@@ -21,9 +21,13 @@
|
|
|
21
21
|
|
|
22
22
|
没有独立的法务 App 时,不要假装有「案件模块」。用订单、伙伴、Nextcloud 文档凑齐材料即可。
|
|
23
23
|
|
|
24
|
-
##
|
|
24
|
+
## 工商查询(企查查优先)
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
企查查只在**本法务助理房间**可用。查对方公司时,**哪个 MCP 挂上了就用哪个;默认先企查查**。扣积分,不要无意义翻页。
|
|
27
|
+
|
|
28
|
+
1. 有 `mcp__qcc-company__*` / `mcp__qcc-risk__*`:先用企查查。企业概况走 company,风险/司法走 risk。
|
|
29
|
+
2. 没有企查查、有 `mcp__tianyancha__*`:再用天眼查。官方 `tools/list` 只有搜索、公司画像、`call_tool` 等入口,不要枚举 162 个工具。
|
|
30
|
+
3. 两者都没有:明确说「工商查询未开通」,不要编造,也不要点名没挂上的供应商。
|
|
27
31
|
|
|
28
32
|
## 每次回答
|
|
29
33
|
|