@fa-software/semi2-dsh 0.0.14-preview.20260927035651 → 0.0.15

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
@@ -23,9 +23,9 @@ Redmine 走另一条路:地址与密钥都来自 hub,插件按 hub 下发的
23
23
  | `credentialRetryIntervalMs` | 150 | 凭据重试间隔(ms) |
24
24
  | `webFenceTimeoutMs` | 5000 | `webServer` 在而校验接口迟迟不出现时补一条诊断日志的上限 |
25
25
 
26
- 状态与工作区映射都不落盘:状态段是一行内存覆盖(与模型路由同一条通道),工作区映射只在进程内,
27
- 启动时按 dsh 工作区基线重新登记。本端既无网页服务也无 dsh 工作区服务(NPC 一次性容器)时不发状态行。
28
- 设置写入只剩盘上自铸内容的认领清理(本插件写过的状态段、带着自铸名字的路由与密钥引用)。
26
+ 状态与工作区映射都不落盘:状态由 `GET /semi2/status` 按请求现算(浏览器半边拉取),工作区映射只在进程内,
27
+ 启动时按 dsh 工作区基线重新登记。本端无网页服务(NPC 一次性容器)时不注册状态路由。
28
+ 设置写入只剩带着自铸名字的路由与密钥引用的维护。
29
29
 
30
30
  ## 提供的服务与事件
31
31
 
package/lib/client.js CHANGED
@@ -8,43 +8,40 @@ window.__ModuleLoader__.load({
8
8
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
9
9
  let react_jsx_runtime = require("react/jsx-runtime");
10
10
  //#region src/contract.ts
11
- /**
12
- * 插件自身设置段的条目 id:dsh 的设置命名空间就是 Profile 插件条目 id(bundle patch 里那一行的 id),
13
- * 状态 / 健康 / 工作区映射都写在这一段里。host 与浏览器半边共用这一个常量。
14
- */
15
- const SEMI2_SETTINGS_ENTRY = "semi2-dsh";
16
11
  /** 动作通道前缀:登录 / 登出 / 同步都经官方 connection 的逻辑 RPC,一次调用落在 POST <前缀>/<endpoint>。 */
17
12
  const SEMI2_ACTION_CHANNEL = "/semi2";
18
13
  //#endregion
19
14
  //#region src/client/projection.ts
20
- /** 空快照:段被 serve 但宿主还没发布状态行(页面据此停在登录态)。 */
21
- function emptySnapshot(snapshot) {
15
+ /** 空快照:还没拿到过任何数据(页面据此停在登录态 / 错误卡)。 */
16
+ function emptySnapshot(frame) {
22
17
  return {
23
18
  fields: {},
24
19
  tabs: [],
25
- writable: snapshot.writable === true,
26
- revision: snapshot.revision,
20
+ writable: frame.writable === true,
27
21
  connected: false
28
22
  };
29
23
  }
30
- /** 投影一帧设置段快照;空值沿用上一轮,其余按 `status` 决定是否进控制台。 */
31
- function projectSnapshot(snapshot, previous) {
32
- const value = snapshot.value;
33
- if (value === void 0 || typeof value.status !== "string" || value.status === "") return previous === void 0 ? emptySnapshot(snapshot) : {
24
+ /** 投影一帧状态应答;空值沿用上一轮,其余按 `status` 决定是否进控制台。 */
25
+ function projectSnapshot(frame, previous) {
26
+ const value = frame.section;
27
+ if (value === void 0 || typeof value.status !== "string" || value.status === "") return previous === void 0 ? emptySnapshot(frame) : {
34
28
  ...previous,
35
- revision: snapshot.revision
29
+ writable: frame.writable === true
36
30
  };
37
31
  return {
38
32
  fields: value,
39
33
  tabs: Array.isArray(value.workspaceTabs) ? value.workspaceTabs : [],
40
- writable: snapshot.writable === true,
41
- revision: snapshot.revision,
34
+ writable: frame.writable === true,
42
35
  connected: value.status === "ready"
43
36
  };
44
37
  }
45
38
  //#endregion
46
39
  //#region src/client/controller.ts
47
40
  const OVERVIEW_KEY = "overview";
41
+ /** 状态路由:唯一的状态来源。 */
42
+ const STATUS_PATH = "/semi2/status";
43
+ /** 页面可见时的慢心跳:状态页不是仪表盘,30s 足够接住会话期事实(项目画像等)。 */
44
+ const REFRESH_INTERVAL_MS = 3e4;
48
45
  /** 运行平台原值(数据层只存 cnb / host)→ 展示标签(本地化)。 */
49
46
  function platformText(t, platform) {
50
47
  if (typeof platform !== "string" || platform === "") return "—";
@@ -117,24 +114,42 @@ window.__ModuleLoader__.load({
117
114
  }
118
115
  var Semi2Controller = class {
119
116
  ctx;
120
- scope;
121
117
  listeners = /* @__PURE__ */ new Set();
122
- unsubscribe;
118
+ disposers = [];
119
+ doFetch;
123
120
  projected;
121
+ view = "loading";
124
122
  loginBusy = false;
125
123
  logoutBusy = false;
126
124
  actionError = null;
127
125
  constructor(ctx) {
128
126
  this.ctx = ctx;
129
- this.scope = ctx.configForms.get(SEMI2_SETTINGS_ENTRY);
130
- this.projected = projectSnapshot(this.scope.getSnapshot(), void 0);
131
- this.unsubscribe = this.scope.subscribe(() => {
132
- this.projected = projectSnapshot(this.scope.getSnapshot(), this.projected);
133
- for (const listener of this.listeners) listener();
127
+ this.doFetch = globalThis.fetch.bind(globalThis);
128
+ this.projected = projectSnapshot({}, void 0);
129
+ const offReset = ctx.on?.("connection/reset", () => {
130
+ this.refresh();
134
131
  });
132
+ if (offReset !== void 0) this.disposers.push(() => {
133
+ offReset();
134
+ });
135
+ const state = ctx.connection?.state;
136
+ if (state !== void 0) this.disposers.push(state.subscribe(() => {
137
+ if (state.getSnapshot() === "connected") this.refresh();
138
+ }));
139
+ const browser = globalThis;
140
+ if (browser.document !== void 0) {
141
+ const timer = setInterval(() => {
142
+ if (browser.document?.visibilityState !== "hidden") this.refresh();
143
+ }, REFRESH_INTERVAL_MS);
144
+ this.disposers.push(() => {
145
+ clearInterval(timer);
146
+ });
147
+ }
148
+ this.refresh();
135
149
  }
136
150
  dispose() {
137
- this.unsubscribe();
151
+ for (const dispose of this.disposers) dispose();
152
+ this.disposers.length = 0;
138
153
  this.listeners.clear();
139
154
  }
140
155
  subscribe(listener) {
@@ -146,6 +161,7 @@ window.__ModuleLoader__.load({
146
161
  getSnapshot() {
147
162
  return {
148
163
  projected: this.projected,
164
+ view: this.view,
149
165
  loginBusy: this.loginBusy,
150
166
  logoutBusy: this.logoutBusy,
151
167
  actionError: this.actionError
@@ -154,7 +170,25 @@ window.__ModuleLoader__.load({
154
170
  notify() {
155
171
  for (const listener of this.listeners) listener();
156
172
  }
157
- /** 登录:草稿只在提交时发出;成功清空密码,失败保留草稿并报错。 */
173
+ /** 拉一帧状态:失败只落 `view`(从未拿到过数据才显示错误卡),拿到过数据就沿用旧值。 */
174
+ async refresh() {
175
+ try {
176
+ const response = await this.doFetch(STATUS_PATH, { headers: { accept: "application/json" } });
177
+ if (!response.ok) throw new Error(`HTTP ${String(response.status)}`);
178
+ const body = await response.json();
179
+ if (body === null || typeof body !== "object" || Array.isArray(body)) throw new Error("invalid payload");
180
+ const section = body;
181
+ this.projected = projectSnapshot({
182
+ section,
183
+ writable: section.writable === true
184
+ }, this.projected);
185
+ this.view = "data";
186
+ } catch {
187
+ if (this.view !== "data") this.view = "error";
188
+ }
189
+ this.notify();
190
+ }
191
+ /** 登录:草稿只在提交时发出;成功清空密码,失败保留草稿并报错;动作后重拉状态。 */
158
192
  async login(draft) {
159
193
  if (this.loginBusy) return;
160
194
  this.loginBusy = true;
@@ -164,6 +198,7 @@ window.__ModuleLoader__.load({
164
198
  this.loginBusy = false;
165
199
  if (!outcome.ok) this.actionError = outcome.code;
166
200
  this.notify();
201
+ await this.refresh();
167
202
  return outcome;
168
203
  }
169
204
  async logout() {
@@ -175,6 +210,7 @@ window.__ModuleLoader__.load({
175
210
  this.logoutBusy = false;
176
211
  if (!outcome.ok) this.actionError = outcome.code;
177
212
  this.notify();
213
+ await this.refresh();
178
214
  return outcome;
179
215
  }
180
216
  /**
@@ -188,6 +224,7 @@ window.__ModuleLoader__.load({
188
224
  this.actionError = outcome.code;
189
225
  this.notify();
190
226
  }
227
+ await this.refresh();
191
228
  }
192
229
  };
193
230
  /** useSyncExternalStore 适配(控制器快照是稳定引用,动作后手动 bump)。 */
@@ -562,10 +599,40 @@ window.__ModuleLoader__.load({
562
599
  ]
563
600
  });
564
601
  }
565
- function OverviewTab({ t, projected, controller, actionError }) {
602
+ /** 从未拿到过数据且拉取失败:错误卡 + 手动重试(不伪装成登录表单)。 */
603
+ function ErrorCard({ t, controller }) {
604
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
605
+ className: "semi2-card",
606
+ children: [
607
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
608
+ className: "semi2-cardTitle",
609
+ children: t("statusUnavailableTitle")
610
+ }),
611
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
612
+ className: "semi2-hint",
613
+ children: t("statusUnavailableHint")
614
+ }),
615
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
616
+ className: "semi2-formActions",
617
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
618
+ variant: "primary",
619
+ onClick: () => {
620
+ controller.refresh();
621
+ },
622
+ children: t("retry")
623
+ })
624
+ })
625
+ ]
626
+ });
627
+ }
628
+ function OverviewTab({ t, projected, controller, actionError, view }) {
566
629
  const { fields } = projected;
567
630
  const [diagnosticsOpen, setDiagnosticsOpen] = (0, react.useState)(false);
568
631
  const { showLogin } = connectionMode(projected);
632
+ if (view === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorCard, {
633
+ t,
634
+ controller
635
+ });
569
636
  if (showLogin) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoginCard, {
570
637
  t,
571
638
  projected,
@@ -683,7 +750,8 @@ window.__ModuleLoader__.load({
683
750
  */
684
751
  function Semi2Page(props) {
685
752
  const { t, controller, renderSlot } = props;
686
- const { projected } = useController(controller);
753
+ const state = useController(controller);
754
+ const { projected } = state;
687
755
  const [active, setActive] = (0, react.useState)(OVERVIEW_KEY);
688
756
  const tabs = (0, react.useMemo)(() => projected.tabs.filter((tab) => tab.title !== ""), [projected.tabs]);
689
757
  const keys = (0, react.useMemo)(() => [OVERVIEW_KEY, ...tabs.map(tabKey)], [tabs]);
@@ -741,7 +809,8 @@ window.__ModuleLoader__.load({
741
809
  t,
742
810
  projected,
743
811
  controller,
744
- actionError: showLogin ? actionError : void 0
812
+ actionError: showLogin ? actionError : void 0,
813
+ view: state.view
745
814
  }) : renderSlot !== void 0 ? renderSlot("semi2.workspace", { view: "page" }, { entryKey: active }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceTab, {
746
815
  t,
747
816
  controller,
@@ -798,6 +867,9 @@ window.__ModuleLoader__.load({
798
867
  workspaceSyncedAt: "同步时间",
799
868
  workspaceUnresolved: "未解析(不归 semi2-hub 管)",
800
869
  workspaceNone: "没有已登记的工作区",
870
+ statusUnavailableTitle: "状态暂不可用",
871
+ statusUnavailableHint: "取不到宿主状态(宿主插件未就绪或版本不匹配),可重试",
872
+ retry: "重试",
801
873
  domainModels: "模型",
802
874
  domainSecrets: "模型密钥",
803
875
  domainSkills: "技能",
@@ -867,6 +939,9 @@ window.__ModuleLoader__.load({
867
939
  workspaceSyncedAt: "Synced at",
868
940
  workspaceUnresolved: "Unresolved (not managed by semi2-hub)",
869
941
  workspaceNone: "No registered workspaces",
942
+ statusUnavailableTitle: "Status unavailable",
943
+ statusUnavailableHint: "Cannot read host status (host plugin not ready or version mismatch); retry",
944
+ retry: "Retry",
870
945
  domainModels: "Models",
871
946
  domainSecrets: "Model keys",
872
947
  domainSkills: "Skills",
@@ -989,7 +1064,6 @@ window.__ModuleLoader__.load({
989
1064
  const PANEL_ID = "semi2";
990
1065
  const inject = [
991
1066
  "slots",
992
- "configForms",
993
1067
  "connection",
994
1068
  "locale"
995
1069
  ];
@@ -1031,9 +1105,9 @@ window.__ModuleLoader__.load({
1031
1105
  workspaceEntries.set(key, dispose);
1032
1106
  }
1033
1107
  };
1034
- ctx.effect(() => ctx.configForms.whileServed([SEMI2_SETTINGS_ENTRY], () => {
1035
- const registered = /* @__PURE__ */ new Map();
1108
+ ctx.effect(() => {
1036
1109
  syncWorkspaceEntries();
1110
+ const registered = /* @__PURE__ */ new Map();
1037
1111
  registered.set("tabs", controller.subscribe(syncWorkspaceEntries));
1038
1112
  registered.set("panel", ctx.slots.inject("main", () => ctx.slots.register({
1039
1113
  name: "main",
@@ -1058,10 +1132,11 @@ window.__ModuleLoader__.load({
1058
1132
  for (const dispose of workspaceEntries.values()) dispose();
1059
1133
  workspaceEntries.clear();
1060
1134
  };
1061
- }), "semi2: sidebar entry + main page");
1135
+ }, "semi2: sidebar entry + main page");
1062
1136
  }
1063
1137
  //#endregion
1064
1138
  exports.PANEL_ID = PANEL_ID;
1139
+ exports.Semi2Controller = Semi2Controller;
1065
1140
  exports.apply = apply;
1066
1141
  exports.inject = inject;
1067
1142
  return module.exports;