@rezti/dsh-rez-suite 0.1.25 → 0.1.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join, relative } from "node:path";
4
- import { REZ_CREDENTIAL_REFS, REZ_DEFAULT_CONNECTIONS, REZ_SYSTEMS, lastMcpStartError, mergeConnections, secretConfigured, writeManagedCredential } from "@rezti/dsh-rez-sso";
4
+ import { REZ_CREDENTIAL_REFS, REZ_DEFAULT_CONNECTIONS, REZ_SYSTEMS, lastMcpStartError, loadRezConnections, mergeConnections, secretConfigured, startMcpWhenSecret, writeManagedCredential } from "@rezti/dsh-rez-sso";
5
5
  import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs";
6
6
  import { DatabaseSync } from "node:sqlite";
7
7
  import { fileURLToPath } from "node:url";
8
+ import { spawn } from "node:child_process";
8
9
  //#region ../../node_modules/.pnpm/@deepseek-ai+cosmokit@1.8.2/node_modules/@deepseek-ai/cosmokit/lib/index.js
9
10
  /** Return true when a value is `null` or `undefined`. */
10
11
  function isNullable(value) {
@@ -2857,7 +2858,8 @@ const COMPOSED_SERVERS = [
2857
2858
  "wechat",
2858
2859
  "homeassistant",
2859
2860
  "tapd",
2860
- "gsc"
2861
+ "gsc",
2862
+ "tianyancha"
2861
2863
  ];
2862
2864
  function mcpToolPrefix(server) {
2863
2865
  return "mcp__" + server + "__";
@@ -2919,7 +2921,7 @@ function observeServer(server, input) {
2919
2921
  };
2920
2922
  }
2921
2923
  function observeComposedServers(input) {
2922
- return COMPOSED_SERVERS.map((server) => observeServer(server, input));
2924
+ return (input.config.role === "boss" ? COMPOSED_SERVERS : COMPOSED_SERVERS.filter((server) => server !== "tianyancha")).map((server) => observeServer(server, input));
2923
2925
  }
2924
2926
  function testComposedServers(input) {
2925
2927
  return observeComposedServers(input).map((row) => {
@@ -2957,7 +2959,8 @@ const REZ_API = {
2957
2959
  audit: "/api/dsh-rez-suite/audit",
2958
2960
  auditReset: "/api/dsh-rez-suite/audit/reset",
2959
2961
  weixin: "/api/dsh-rez-suite/weixin",
2960
- weixinVerify: "/api/dsh-rez-suite/weixin/verify"
2962
+ weixinVerify: "/api/dsh-rez-suite/weixin/verify",
2963
+ update: "/api/dsh-rez-suite/update"
2961
2964
  };
2962
2965
  //#endregion
2963
2966
  //#region src/store.ts
@@ -3047,6 +3050,12 @@ function publicConfig(config) {
3047
3050
  siteUrl: connections.gsc.siteUrl,
3048
3051
  secretConfigured: secretConfigured("gsc"),
3049
3052
  secretRef: REZ_CREDENTIAL_REFS.gsc
3053
+ },
3054
+ tianyancha: {
3055
+ enabled: connections.tianyancha.enabled,
3056
+ url: connections.tianyancha.url,
3057
+ secretConfigured: secretConfigured("tianyancha"),
3058
+ secretRef: REZ_CREDENTIAL_REFS.tianyancha
3050
3059
  }
3051
3060
  },
3052
3061
  billing: { ...config.billing }
@@ -3220,7 +3229,7 @@ function applyPublicPatch(current, patch) {
3220
3229
  }
3221
3230
  /** Build every /api/dsh-rez-suite route (exact paths). */
3222
3231
  function makeRoutes(deps) {
3223
- const { getConfig, updateConfig, host, tokens, onCredentialWritten } = deps;
3232
+ const { getConfig, updateConfig, host, tokens, onCredentialWritten, selfUpdate } = deps;
3224
3233
  const guard = (req, res, method) => {
3225
3234
  if (!isLoopbackRequest(req)) {
3226
3235
  writeJson(res, 403, { error: "forbidden: loopback-only" });
@@ -3292,6 +3301,31 @@ function makeRoutes(deps) {
3292
3301
  tokens.reset();
3293
3302
  writeJson(res, 200, { ok: true });
3294
3303
  }
3304
+ },
3305
+ {
3306
+ kind: "exact",
3307
+ path: REZ_API.update,
3308
+ handler: async (req, res) => {
3309
+ const method = req.method === "POST" ? "POST" : "GET";
3310
+ if (!guard(req, res, method)) return;
3311
+ if (selfUpdate === void 0) {
3312
+ writeJson(res, 501, { error: "self-update is not available" });
3313
+ return;
3314
+ }
3315
+ try {
3316
+ if (method === "GET") {
3317
+ writeJson(res, 200, await selfUpdate.status());
3318
+ return;
3319
+ }
3320
+ const result = await selfUpdate.apply();
3321
+ writeJson(res, 200, result);
3322
+ if (result.restarting) setTimeout(() => {
3323
+ selfUpdate.relaunch();
3324
+ }, 400);
3325
+ } catch (error) {
3326
+ writeJson(res, 502, { error: error instanceof Error ? error.message : String(error) });
3327
+ }
3328
+ }
3295
3329
  }
3296
3330
  ];
3297
3331
  }
@@ -7906,6 +7940,212 @@ function mountRezWorkspaces(ctx, getRole) {
7906
7940
  return run;
7907
7941
  }
7908
7942
  //#endregion
7943
+ //#region src/self-update.ts
7944
+ /**
7945
+ * In-place upgrade of @rezti/dsh-rez-suite inside the running dsh profile.
7946
+ * pnpm add writes the new bits; Node cannot replace an already-imported host
7947
+ * module, so we respawn the same argv after the HTTP response flushes.
7948
+ */
7949
+ const SUITE_PACKAGE = "@rezti/dsh-rez-suite";
7950
+ const NPM_LATEST = "https://registry.npmjs.org/@rezti/dsh-rez-suite/latest";
7951
+ const PNPM_TIMEOUT_MS = 18e4;
7952
+ function compareVersions(left, right) {
7953
+ const a = left.split(".").map((part) => Number.parseInt(part, 10) || 0);
7954
+ const b = right.split(".").map((part) => Number.parseInt(part, 10) || 0);
7955
+ const n = Math.max(a.length, b.length);
7956
+ for (let i = 0; i < n; i += 1) {
7957
+ const av = a[i] ?? 0;
7958
+ const bv = b[i] ?? 0;
7959
+ if (av > bv) return 1;
7960
+ if (av < bv) return -1;
7961
+ }
7962
+ return 0;
7963
+ }
7964
+ function readInstalledVersion(packageDir) {
7965
+ const raw = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8"));
7966
+ if (typeof raw.version !== "string" || raw.version === "") throw new Error("suite package.json missing version");
7967
+ return raw.version;
7968
+ }
7969
+ function findProfileDir(startDir) {
7970
+ let dir = startDir;
7971
+ for (let i = 0; i < 12; i += 1) {
7972
+ const manifest = join(dir, "package.json");
7973
+ if (existsSync(manifest)) try {
7974
+ const raw = JSON.parse(readFileSync(manifest, "utf8"));
7975
+ if (Array.isArray(raw.dsh?.profile?.bundles)) return dir;
7976
+ } catch {}
7977
+ const parent = dirname(dir);
7978
+ if (parent === dir) break;
7979
+ dir = parent;
7980
+ }
7981
+ throw new Error("cannot find dsh profile directory (no package.json with dsh.profile.bundles)");
7982
+ }
7983
+ function shellQuote(value) {
7984
+ return `'${value.replaceAll("'", `'\\''`)}'`;
7985
+ }
7986
+ function relaunchScript(argv, parentPid) {
7987
+ return `while kill -0 ${parentPid} 2>/dev/null; do sleep 0.15; done; sleep 0.25; exec ${argv.map(shellQuote).join(" ")}`;
7988
+ }
7989
+ async function fetchLatestVersion(fetchImpl = fetch) {
7990
+ const response = await fetchImpl(NPM_LATEST, { headers: { accept: "application/json" } });
7991
+ if (!response.ok) throw new Error(`npm registry HTTP ${response.status}`);
7992
+ const body = await response.json();
7993
+ if (typeof body.version !== "string" || body.version === "") throw new Error("npm registry missing version");
7994
+ return body.version;
7995
+ }
7996
+ function runPnpmAdd(cwd, spec, env = process.env) {
7997
+ return new Promise((resolve, reject) => {
7998
+ const child = spawn("pnpm", ["add", spec], {
7999
+ cwd,
8000
+ env,
8001
+ stdio: [
8002
+ "ignore",
8003
+ "pipe",
8004
+ "pipe"
8005
+ ]
8006
+ });
8007
+ let stderr = "";
8008
+ child.stderr.on("data", (chunk) => {
8009
+ stderr += chunk.toString();
8010
+ });
8011
+ const timer = setTimeout(() => {
8012
+ child.kill("SIGTERM");
8013
+ reject(/* @__PURE__ */ new Error("pnpm add timed out after 3 minutes"));
8014
+ }, PNPM_TIMEOUT_MS);
8015
+ child.on("error", (error) => {
8016
+ clearTimeout(timer);
8017
+ reject(error);
8018
+ });
8019
+ child.on("close", (code) => {
8020
+ clearTimeout(timer);
8021
+ if (code === 0) resolve();
8022
+ else reject(new Error(stderr.trim() || `pnpm add exited ${String(code)}`));
8023
+ });
8024
+ });
8025
+ }
8026
+ function spawnRelaunch(argv, parentPid, cwd, env = process.env) {
8027
+ spawn("/bin/bash", ["-c", relaunchScript(argv, parentPid)], {
8028
+ cwd,
8029
+ env,
8030
+ detached: true,
8031
+ stdio: "ignore"
8032
+ }).unref();
8033
+ }
8034
+ let inflight;
8035
+ function createSelfUpdate(io) {
8036
+ const status = async () => {
8037
+ const installed = readInstalledVersion(io.packageDir);
8038
+ const latest = await io.fetchLatest();
8039
+ return {
8040
+ package: SUITE_PACKAGE,
8041
+ installed,
8042
+ latest,
8043
+ outdated: compareVersions(latest, installed) > 0
8044
+ };
8045
+ };
8046
+ const apply = async () => {
8047
+ if (inflight !== void 0) throw new Error("upgrade already running");
8048
+ const work = (async () => {
8049
+ const snapshot = await status();
8050
+ if (!snapshot.outdated) return {
8051
+ ...snapshot,
8052
+ restarting: false
8053
+ };
8054
+ const profileDir = findProfileDir(io.packageDir);
8055
+ await io.install(profileDir, `${SUITE_PACKAGE}@${snapshot.latest}`);
8056
+ return {
8057
+ ...snapshot,
8058
+ installed: snapshot.latest,
8059
+ outdated: false,
8060
+ restarting: true
8061
+ };
8062
+ })();
8063
+ inflight = work;
8064
+ try {
8065
+ return await work;
8066
+ } finally {
8067
+ inflight = void 0;
8068
+ }
8069
+ };
8070
+ return {
8071
+ status,
8072
+ apply,
8073
+ relaunch: io.relaunch
8074
+ };
8075
+ }
8076
+ function liveSelfUpdate() {
8077
+ return createSelfUpdate({
8078
+ packageDir: dirname(fileURLToPath(new URL(".", import.meta.url))),
8079
+ fetchLatest: () => fetchLatestVersion(),
8080
+ install: (profileDir, spec) => runPnpmAdd(profileDir, spec),
8081
+ relaunch: () => {
8082
+ spawnRelaunch(process.argv, process.pid, process.cwd());
8083
+ setTimeout(() => {
8084
+ process.exit(0);
8085
+ }, 50);
8086
+ }
8087
+ });
8088
+ }
8089
+ //#endregion
8090
+ //#region src/tianyancha.ts
8091
+ const TIANYANCHA_MCP_URL = "https://mcp.tianyancha.com/mcp";
8092
+ function shouldStartTianyancha(role) {
8093
+ return role === "boss";
8094
+ }
8095
+ function bearerAuthorization(secret) {
8096
+ const trimmed = secret.trim();
8097
+ if (/^bearer\s+/i.test(trimmed)) return "Bearer " + trimmed.replace(/^bearer\s+/i, "");
8098
+ return "Bearer " + trimmed;
8099
+ }
8100
+ function tianyanchaMcpConfig(secret) {
8101
+ const { url } = loadRezConnections().tianyancha;
8102
+ return {
8103
+ serverName: "tianyancha",
8104
+ transport: "streamable-http",
8105
+ url: url.length > 0 ? url : TIANYANCHA_MCP_URL,
8106
+ headers: { Authorization: bearerAuthorization(secret) },
8107
+ failOnStartupError: false
8108
+ };
8109
+ }
8110
+ /** Start when role is boss; dispose the mcp-client fiber when the identity leaves. */
8111
+ function mountTianyanchaMcp(ctx, getRole, start = startMcpWhenSecret) {
8112
+ let session;
8113
+ let pending = false;
8114
+ const ensure = () => {
8115
+ (async () => {
8116
+ if (!shouldStartTianyancha(getRole())) {
8117
+ session?.();
8118
+ session = void 0;
8119
+ return;
8120
+ }
8121
+ if (session !== void 0 || pending) return;
8122
+ pending = true;
8123
+ let raced = false;
8124
+ try {
8125
+ const dispose = await start(ctx, {
8126
+ system: "tianyancha",
8127
+ label: "天眼查",
8128
+ config: tianyanchaMcpConfig
8129
+ });
8130
+ if (!shouldStartTianyancha(getRole())) {
8131
+ raced = true;
8132
+ dispose();
8133
+ return;
8134
+ }
8135
+ session = dispose;
8136
+ } catch (error) {
8137
+ const why = error instanceof Error ? error.message : String(error);
8138
+ console.error("[dsh-rez-suite] tianyancha MCP start failed:", why);
8139
+ } finally {
8140
+ pending = false;
8141
+ if (raced && shouldStartTianyancha(getRole()) && session === void 0) ensure();
8142
+ }
8143
+ })();
8144
+ };
8145
+ ensure();
8146
+ return ensure;
8147
+ }
8148
+ //#endregion
7909
8149
  //#region src/index.ts
7910
8150
  const name = "rez-suite";
7911
8151
  const inject = [
@@ -7959,7 +8199,11 @@ const Config = Schema.object({
7959
8199
  gsc: Schema.object({
7960
8200
  enabled: Schema.boolean().default(true),
7961
8201
  siteUrl: Schema.string().default(REZ_DEFAULT_CONNECTIONS.gsc.siteUrl)
7962
- }).default(REZ_DEFAULT_CONNECTIONS.gsc)
8202
+ }).default(REZ_DEFAULT_CONNECTIONS.gsc),
8203
+ tianyancha: Schema.object({
8204
+ enabled: Schema.boolean().default(true),
8205
+ url: Schema.string().default(REZ_DEFAULT_CONNECTIONS.tianyancha.url)
8206
+ }).default(REZ_DEFAULT_CONNECTIONS.tianyancha)
7963
8207
  }).default(REZ_DEFAULT_CONNECTIONS),
7964
8208
  billing: Schema.object({
7965
8209
  inputCostPer1k: Schema.number().default(.001),
@@ -8006,6 +8250,7 @@ function delegatedHost(ctx, getConfig) {
8006
8250
  function apply(ctx, config) {
8007
8251
  let current = () => normalize(config);
8008
8252
  const syncWorkspaces = mountRezWorkspaces(ctx, () => current().role);
8253
+ const syncTianyancha = mountTianyanchaMcp(ctx, () => current().role);
8009
8254
  const tokens = new TokenManager(joinTokenDb());
8010
8255
  const host = delegatedHost(ctx, () => current());
8011
8256
  ctx.effect(() => () => {
@@ -8019,6 +8264,7 @@ function apply(ctx, config) {
8019
8264
  saveConfig(value);
8020
8265
  current = () => value;
8021
8266
  syncWorkspaces();
8267
+ syncTianyancha();
8022
8268
  try {
8023
8269
  const settings = ctx.get("settings");
8024
8270
  if (settings?.update !== void 0) await settings.update(REZ_SETTINGS_NAMESPACE, value);
@@ -8032,7 +8278,8 @@ function apply(ctx, config) {
8032
8278
  onCredentialWritten: (ref) => {
8033
8279
  const emit = ctx.emit;
8034
8280
  emit("credentials/updated", ref);
8035
- }
8281
+ },
8282
+ selfUpdate: liveSelfUpdate()
8036
8283
  });
8037
8284
  let disposeRoutes;
8038
8285
  let disposeTools;
@@ -8075,10 +8322,12 @@ function apply(ctx, config) {
8075
8322
  current = source;
8076
8323
  sync();
8077
8324
  syncWorkspaces();
8325
+ syncTianyancha();
8078
8326
  },
8079
8327
  onChange: () => {
8080
8328
  sync();
8081
8329
  syncWorkspaces();
8330
+ syncTianyancha();
8082
8331
  }
8083
8332
  });
8084
8333
  sync();
package/lib/style.css CHANGED
@@ -287,3 +287,84 @@
287
287
  font-size: 15px;
288
288
  font-weight: 600;
289
289
  }
290
+
291
+ .LOjPTW_upgradeLayer {
292
+ flex: none;
293
+ align-items: center;
294
+ width: 100%;
295
+ height: 42px;
296
+ margin: 8px 0 0;
297
+ display: flex;
298
+ position: relative;
299
+ }
300
+
301
+ .LOjPTW_upgradeLayerRail {
302
+ width: 36px;
303
+ height: 36px;
304
+ margin: 0;
305
+ }
306
+
307
+ .LOjPTW_upgradeButton {
308
+ width: calc(100% + 4px);
309
+ height: 42px;
310
+ color: var(--dsw-alias-label-primary);
311
+ cursor: pointer;
312
+ background: none;
313
+ border: none;
314
+ border-radius: 12px;
315
+ align-items: center;
316
+ gap: 8px;
317
+ margin: 0 -2px;
318
+ padding: 0 10px 0 8px;
319
+ font-family: inherit;
320
+ font-size: 14px;
321
+ display: inline-flex;
322
+ overflow: hidden;
323
+ }
324
+
325
+ .LOjPTW_upgradeButton:hover, .LOjPTW_upgradeButton[data-outdated] {
326
+ background: var(--dsw-alias-interactive-bg-hover);
327
+ }
328
+
329
+ .LOjPTW_upgradeButton:disabled {
330
+ cursor: progress;
331
+ opacity: .72;
332
+ }
333
+
334
+ .LOjPTW_upgradeButton:focus-visible {
335
+ outline: 2px solid var(--dsw-alias-brand-primary);
336
+ outline-offset: -2px;
337
+ }
338
+
339
+ .LOjPTW_upgradeLayerRail .LOjPTW_upgradeButton {
340
+ border-radius: 50%;
341
+ justify-content: center;
342
+ gap: 0;
343
+ width: 36px;
344
+ height: 36px;
345
+ padding: 0;
346
+ }
347
+
348
+ .LOjPTW_upgradeLabel {
349
+ text-overflow: ellipsis;
350
+ white-space: nowrap;
351
+ min-width: 0;
352
+ overflow: hidden;
353
+ }
354
+
355
+ .LOjPTW_upgradeMeta {
356
+ color: var(--dsw-alias-label-tertiary);
357
+ font-variant-numeric: tabular-nums;
358
+ flex: none;
359
+ margin-left: auto;
360
+ font-size: 12px;
361
+ line-height: 16px;
362
+ }
363
+
364
+ .LOjPTW_upgradeDot {
365
+ background: var(--dsw-alias-brand-primary);
366
+ border-radius: 50%;
367
+ flex: none;
368
+ width: 7px;
369
+ height: 7px;
370
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rezti/dsh-rez-suite",
3
3
  "description": "ReZ-TI one-package install for DeepSeek Harness. Update with: dsh plugin --profile web update @rezti/dsh-rez-suite",
4
- "version": "0.1.25",
4
+ "version": "0.1.27",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -30,7 +30,8 @@
30
30
  "@deepseek-ai/dsh-client-connection",
31
31
  "@deepseek-ai/dsh-client-locale",
32
32
  "@deepseek-ai/dsh-client-ui-settings",
33
- "@deepseek-ai/dsh-client-ui-settings-plugins"
33
+ "@deepseek-ai/dsh-client-ui-settings-plugins",
34
+ "@deepseek-ai/dsh-client-ui-sidebar"
34
35
  ],
35
36
  "platform": "web"
36
37
  }
@@ -38,15 +39,15 @@
38
39
  "dependencies": {
39
40
  "@modelcontextprotocol/sdk": "^1.30.0",
40
41
  "zod": "^4.4.3",
41
- "@rezti/dsh-rez-sso": "0.1.9",
42
- "@rezti/dsh-rez-token-manager": "0.1.4",
43
- "@rezti/dsh-rez-ha-bridge": "0.1.9",
44
42
  "@rezti/dsh-rez-nextcloud": "0.1.9",
45
- "@rezti/dsh-rez-tapd": "0.1.0",
43
+ "@rezti/dsh-rez-gsc": "0.1.0",
46
44
  "@rezti/dsh-rez-intent": "0.1.5",
45
+ "@rezti/dsh-rez-ha-bridge": "0.1.9",
46
+ "@rezti/dsh-rez-tapd": "0.1.0",
47
47
  "@rezti/dsh-rez-odoo": "0.1.9",
48
+ "@rezti/dsh-rez-token-manager": "0.1.4",
48
49
  "@rezti/dsh-rez-wechat": "0.1.14",
49
- "@rezti/dsh-rez-gsc": "0.1.0"
50
+ "@rezti/dsh-rez-sso": "0.1.10"
50
51
  },
51
52
  "peerDependencies": {
52
53
  "react": "^18.2.0",
package/src/client/api.ts CHANGED
@@ -9,6 +9,8 @@ import {
9
9
  type RezServerStatus,
10
10
  type RezStatusResponse,
11
11
  type RezTestResult,
12
+ type RezUpdateApplyResult,
13
+ type RezUpdateStatus,
12
14
  type RezWeixinStatus,
13
15
  } from '../protocol.ts'
14
16
 
@@ -97,4 +99,14 @@ export class RezApi {
97
99
  const response = await fetch(REZ_API.weixin, { method: 'DELETE' })
98
100
  return readJson<RezWeixinStatus>(response)
99
101
  }
102
+
103
+ async updateStatus(): Promise<RezUpdateStatus> {
104
+ const response = await fetch(REZ_API.update)
105
+ return readJson<RezUpdateStatus>(response)
106
+ }
107
+
108
+ async applyUpdate(): Promise<RezUpdateApplyResult> {
109
+ const response = await fetch(REZ_API.update, { method: 'POST' })
110
+ return readJson<RezUpdateApplyResult>(response)
111
+ }
100
112
  }
@@ -11,6 +11,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
11
11
  import type {} from '@deepseek-ai/dsh-client-ui-slots'
12
12
  import { en, zh, type RezKey } from './locales.ts'
13
13
  import { RezPluginCard, RezSettingsPage } from './settings-card.tsx'
14
+ import { UpgradeButton } from './upgrade-button.tsx'
14
15
 
15
16
  const NS = 'dsh-rez-suite'
16
17
 
@@ -29,6 +30,11 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
29
30
  scope: 'root'
30
31
  owner: { children?: never }
31
32
  }
33
+ 'sidebar.footer.action': {
34
+ kind: 'list'
35
+ scope: 'root'
36
+ owner: { wide: boolean }
37
+ }
32
38
  }
33
39
  }
34
40
 
@@ -53,4 +59,12 @@ export function apply(ctx: ClientContext): void {
53
59
  label: () => t('settings.title'),
54
60
  locale: NS,
55
61
  }, RezSettingsPage))
62
+
63
+ ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({
64
+ name: 'sidebar.footer.action',
65
+ id: 'rez-upgrade',
66
+ order: 10,
67
+ label: () => t('upgrade.label'),
68
+ locale: NS,
69
+ }, UpgradeButton))
56
70
  }
@@ -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.tianyancha': '天眼查(法务助理 · 仅老板)',
56
+ 'config.tianyancha.url': '天眼查 MCP URL',
57
+ 'config.tianyancha.token': 'OpenAPI Token',
55
58
  'config.fs': '本地文件 (fs-mcp-server)',
56
59
  'config.fs.root': '根目录',
57
60
  'config.sqlite': 'SQLite (sqlite-mcp-server)',
@@ -111,6 +114,13 @@ export const zh = {
111
114
  'weixin.listening': '通道在听:微信 → 网页「微信」会话(同一条,不另开窗口)→ 回微信。',
112
115
  'weixin.loggedIn': '已登录,通道未在听。点「重新连接」即可拉起,不必再扫码。',
113
116
  'weixin.loggedOut': '尚未登录。',
117
+ 'upgrade.label': '升级',
118
+ 'upgrade.current': '已最新',
119
+ 'upgrade.aria': '升级 ReZ-TI 插件',
120
+ 'upgrade.running': '升级中…',
121
+ 'upgrade.reloading': '正在重载',
122
+ 'upgrade.already': '已是 {version}',
123
+ 'upgrade.reloadTimeout': '升级完成,但网页还没重新连上。请刷新这一页。',
114
124
  } as const
115
125
 
116
126
  export type RezKey = keyof typeof zh
@@ -164,6 +174,9 @@ export const en: Record<RezKey, string> = {
164
174
  'config.gsc': 'Google Search Console',
165
175
  'config.gsc.siteUrl': 'Site URL',
166
176
  'config.gsc.key': 'Service-account JSON or key file path',
177
+ 'config.tianyancha': 'Tianyancha (Legal assistant, boss only)',
178
+ 'config.tianyancha.url': 'Tianyancha MCP URL',
179
+ 'config.tianyancha.token': 'OpenAPI token',
167
180
  'config.fs': 'Local files (fs-mcp-server)',
168
181
  'config.fs.root': 'Root directory',
169
182
  'config.sqlite': 'SQLite (sqlite-mcp-server)',
@@ -220,6 +233,13 @@ export const en: Record<RezKey, string> = {
220
233
  'weixin.listening': 'Channel is live: WeChat → the WeChat folder session on the left → WeChat.',
221
234
  'weixin.loggedIn': 'Logged in, listener idle. Click Reconnect — no need to scan again.',
222
235
  'weixin.loggedOut': 'Not logged in.',
236
+ 'upgrade.label': 'Upgrade',
237
+ 'upgrade.current': 'Up to date',
238
+ 'upgrade.aria': 'Upgrade the ReZ-TI plugin',
239
+ 'upgrade.running': 'Updating…',
240
+ 'upgrade.reloading': 'Reloading',
241
+ 'upgrade.already': 'Already {version}',
242
+ 'upgrade.reloadTimeout': 'Update finished, but the page did not reconnect. Refresh this tab.',
223
243
  }
224
244
 
225
245
  /** Minimal {name} interpolator shared by the panel helpers. */
@@ -24,9 +24,10 @@ type SecretDrafts = {
24
24
  homeassistant: string
25
25
  tapd: string
26
26
  gsc: string
27
+ tianyancha: string
27
28
  }
28
29
 
29
- const EMPTY_SECRETS: SecretDrafts = { odoo: '', nextcloud: '', wechat: '', homeassistant: '', tapd: '', gsc: '' }
30
+ const EMPTY_SECRETS: SecretDrafts = { odoo: '', nextcloud: '', wechat: '', homeassistant: '', tapd: '', gsc: '', tianyancha: '' }
30
31
 
31
32
  function Field({ label, children }: { label: string; children: ReactNode }) {
32
33
  return (
@@ -170,6 +171,7 @@ export function ConfigTab({ api }: { api: RezApi }) {
170
171
  homeassistant: { ...config.connections.homeassistant, secret: secrets.homeassistant || undefined },
171
172
  tapd: { ...config.connections.tapd, secret: secrets.tapd || undefined },
172
173
  gsc: { ...config.connections.gsc, secret: secrets.gsc || undefined },
174
+ tianyancha: { ...config.connections.tianyancha, secret: secrets.tianyancha || undefined },
173
175
  },
174
176
  }
175
177
  const saved = await api.saveConfig(payload)
@@ -290,6 +292,18 @@ export function ConfigTab({ api }: { api: RezApi }) {
290
292
  onSecret={value => { setSecrets(prev => ({ ...prev, gsc: value })) }}
291
293
  secretLabel={tt('config.gsc.key')}
292
294
  />
295
+ {config.role === 'boss' && (
296
+ <McpSection
297
+ title={tt('config.tianyancha')}
298
+ row={config.connections.tianyancha}
299
+ secret={secrets.tianyancha}
300
+ onEnabled={enabled => { patchConnection('tianyancha', { enabled }) }}
301
+ onUrl={url => { patchConnection('tianyancha', { url }) }}
302
+ onSecret={value => { setSecrets(prev => ({ ...prev, tianyancha: value })) }}
303
+ urlLabel={tt('config.tianyancha.url')}
304
+ secretLabel={tt('config.tianyancha.token')}
305
+ />
306
+ )}
293
307
 
294
308
  <Section title={tt('config.billing')}>
295
309
  <Field label={tt('config.billing.input')}>