@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 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
@@ -1730,6 +1730,10 @@ interface RezConnections {
1730
1730
  enabled: boolean;
1731
1731
  siteUrl: string;
1732
1732
  };
1733
+ qcc: {
1734
+ enabled: boolean;
1735
+ url: string;
1736
+ };
1733
1737
  tianyancha: {
1734
1738
  enabled: boolean;
1735
1739
  url: string;
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.config.role === "boss" ? COMPOSED_SERVERS : COMPOSED_SERVERS.filter((server) => server !== "tianyancha")).map((server) => observeServer(server, 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.url)
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.33",
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
+ }
@@ -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.url),
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)
@@ -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
- export function observeComposedServers(input: ObserveInput): RezServerStatus[] {
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
- return servers.map(server => observeServer(server, input))
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`。工商查询请切到法务助理,避免串房间。
@@ -49,6 +49,7 @@
49
49
  - 谁付钱、钱在哪、卡在哪:回款、逾期应收、未交货金额、库存占用。
50
50
  - Odoo **只读**。多公司必须带 `company_id`。
51
51
  - 禁止过账、付款、自动给客户发消息、在这里写代码或控 HA。
52
+ - 工商查询请切到法务助理,避免串房间。
52
53
 
53
54
  ## 公司(问数字时点名)
54
55
 
@@ -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`。工商查询请切到法务助理,避免串房间。
@@ -40,6 +40,7 @@
40
40
  - 缺料、逾期交货、应收超账期、系统连不上。
41
41
  - Odoo 只读采购在途 / 库存 / 逾期;TAPD 只看迭代红灯。
42
42
  - 禁止改库存、群发、自动上架、在这里控 HA。企微只出「需要老板出面」的摘要。
43
+ - 工商查询请切到法务助理,避免串房间。
43
44
 
44
45
  ## 公司
45
46
 
@@ -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。工商查询请切到法务助理,避免串房间。用户用中文就中文、用英文就英文。
@@ -63,6 +63,7 @@
63
63
  - 只用 `mcp__odoo__*`。费用模型已开放读写;工资单模型若列表里没有就不要编。
64
64
  - `AGENTS.md` 仓级红线:不擅自改生产库表;过账/付款先说明再执行。
65
65
  - 对外付款、给供应商打款 = 资金操作,必须用户明确选「执行」且 Odoo 允许。
66
+ - 工商查询请切到法务助理,避免串房间。
66
67
 
67
68
  ## 公司
68
69
 
@@ -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。工商查询请切到法务助理,避免串房间。用户用中文就中文、用英文就英文。
@@ -46,6 +46,7 @@
46
46
 
47
47
  - 走现成 `mcp__odoo__*`(`hr.leave`、`hr.leave.allocation`、`hr.employee` 等)。不自写人事系统。
48
48
  - 工资审批若出现,请转到 **财务助理**(工资单 MCP 未开放时据实说做不了)。
49
+ - 工商查询请切到法务助理,避免串房间。
49
50
  - 禁止伪造审批、禁止把别人的假期余额讲给无权限的人。
50
51
 
51
52
  ## 公司
@@ -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。有 `mcp__tianyancha__*` 才查工商(仅老板身份会挂上)。用户用中文就中文、用英文就英文。
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
- 出现 `mcp__tianyancha__*` 时,才用来查对方公司工商/司法。官方 `tools/list` 只有搜索、公司画像、`call_tool` 等入口,不要枚举 162 个工具。没有这些工具就当没开通,不要提天眼查。
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