@ntdgy/dsh-remote-bridge-plugin 0.0.0-stage → 0.1.0
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/cordis.patch.yml +28 -0
- package/lib/client.js +336 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.ts +106 -0
- package/lib/index.js +535 -0
- package/lib/index.js.map +1 -0
- package/lib/local/sessions.d.ts +52 -0
- package/lib/local/sessions.js +25 -0
- package/lib/local/sessions.js.map +1 -0
- package/lib/relay/client.d.ts +262 -0
- package/lib/relay/client.js +825 -0
- package/lib/relay/client.js.map +1 -0
- package/lib/relay/dispatcher.d.ts +74 -0
- package/lib/relay/dispatcher.js +192 -0
- package/lib/relay/dispatcher.js.map +1 -0
- package/lib/relay/errors.d.ts +37 -0
- package/lib/relay/errors.js +71 -0
- package/lib/relay/errors.js.map +1 -0
- package/lib/relay/follow.d.ts +176 -0
- package/lib/relay/follow.js +307 -0
- package/lib/relay/follow.js.map +1 -0
- package/lib/relay/http-forward.d.ts +30 -0
- package/lib/relay/http-forward.js +183 -0
- package/lib/relay/http-forward.js.map +1 -0
- package/lib/relay/route-observer.d.ts +66 -0
- package/lib/relay/route-observer.js +122 -0
- package/lib/relay/route-observer.js.map +1 -0
- package/lib/relay/ws-forward.d.ts +28 -0
- package/lib/relay/ws-forward.js +138 -0
- package/lib/relay/ws-forward.js.map +1 -0
- package/lib/types/client/StatusPanel.d.ts +36 -0
- package/lib/types/client/index.d.ts +27 -0
- package/lib/types/client/types.d.ts +40 -0
- package/package.json +68 -4
- package/README.md +0 -3
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# dsh-remote-bridge 的 bundle 补丁层。
|
|
2
|
+
#
|
|
3
|
+
# ⚠️ 本文件默认是**空数组**,有意为之,请勿往这里加 config。
|
|
4
|
+
#
|
|
5
|
+
# 语义(cordis-plugin-include: applyEntryPatches):
|
|
6
|
+
# - 带 "insert" 的 patch 是**新增**条目;
|
|
7
|
+
# - 不带 "insert" 的 patch 只能**修改**已存在的同 id 条目。
|
|
8
|
+
#
|
|
9
|
+
# 为什么留空:patch 层是**按数组顺序追加**的,先到先得,同 id 里第一个
|
|
10
|
+
# fiber 胜出,后来者抛「service "remoteBridge" has been registered」被静默丢弃。
|
|
11
|
+
# 本包同时又是 profile 里的一个条目时,它一定会出现在 patch 层内部,位置由
|
|
12
|
+
# 数组顺序决定——因此本文件再放同 id 条目,只会制造一个"可能赢、也可能输"的
|
|
13
|
+
# 重复注册,把 config 的归属变成掷硬币。P1 实机验收就是被这个坑卡住的:
|
|
14
|
+
# 插件带着占位 instanceKey 出站 → 网关 4401 → 无限重连,全链路没有任何日志。
|
|
15
|
+
#
|
|
16
|
+
# 正确接法(使用方在**自己的** profile 里做,本包不替它决定):
|
|
17
|
+
# ~/.dsh/profiles/<name>/cordis.patch.yml:
|
|
18
|
+
# - insert:
|
|
19
|
+
# - id: remote-bridge
|
|
20
|
+
# name: '@ntdgy/dsh-remote-bridge-plugin'
|
|
21
|
+
# config:
|
|
22
|
+
# relayUrl: wss://gateway.example.com/agent
|
|
23
|
+
# instanceId: dsh-prod-01
|
|
24
|
+
# instanceKey: <从环境变量注入,不要写死>
|
|
25
|
+
#
|
|
26
|
+
# 插件的 Config schema 已把 relayUrl / instanceId / instanceKey 标成 required(),
|
|
27
|
+
# 构造函数再兜一道显式校验:缺任何一项都会在 dsh 启动诊断里报错,不再静默空转。
|
|
28
|
+
[]
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@ntdgy/dsh-remote-bridge-plugin",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
+
//#region src/client/StatusPanel.tsx
|
|
10
|
+
function timeOf(ms) {
|
|
11
|
+
return ms === null ? "—" : new Date(ms).toLocaleString();
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* 把一次读取结果转成展示行。
|
|
15
|
+
*
|
|
16
|
+
* 读取失败时**只显示错误**,不显示上一次的快照 ——
|
|
17
|
+
* 否则「relay 已断但面板还显示旧的正常数据」会被误读成一切正常。
|
|
18
|
+
*/
|
|
19
|
+
function rowsOf(outcome) {
|
|
20
|
+
if (outcome.kind === "error") return [{
|
|
21
|
+
label: "读取状态失败",
|
|
22
|
+
value: outcome.message,
|
|
23
|
+
bad: true
|
|
24
|
+
}];
|
|
25
|
+
const s = outcome.status;
|
|
26
|
+
const disconnected = !s.connection.registered;
|
|
27
|
+
const closePrefix = disconnected ? "断线" : "上次断线";
|
|
28
|
+
return [
|
|
29
|
+
{
|
|
30
|
+
label: "连接状态",
|
|
31
|
+
value: s.connection.registered ? "已注册" : "未注册",
|
|
32
|
+
bad: !s.connection.registered
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
label: "instanceId",
|
|
36
|
+
value: s.identity.instanceId
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
label: "relayUrl",
|
|
40
|
+
value: s.identity.relayUrl
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
label: "心跳间隔",
|
|
44
|
+
value: s.heartbeat.intervalMs === null ? "—" : s.heartbeat.intervalMs + " ms"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
label: "认证失败次数",
|
|
48
|
+
value: String(s.authFailures),
|
|
49
|
+
bad: s.authFailures > 0
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
label: "丢弃帧数",
|
|
53
|
+
value: String(s.droppedFrames),
|
|
54
|
+
bad: s.droppedFrames > 0
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
label: "已上报前缀",
|
|
58
|
+
value: s.httpPrefixes.reported.length === 0 ? "(无)" : s.httpPrefixes.reported.join("、")
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
label: closePrefix + "原因",
|
|
62
|
+
value: s.lastClose.reason ?? "—",
|
|
63
|
+
bad: disconnected && s.lastClose.reason !== null
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
label: closePrefix + "时刻",
|
|
67
|
+
value: timeOf(s.lastClose.at)
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
label: "重连次数",
|
|
71
|
+
value: String(s.connection.reconnects)
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
label: "sessionController",
|
|
75
|
+
value: s.sessionControllerReady ? "可用" : "不可用",
|
|
76
|
+
bad: !s.sessionControllerReady
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
label: "数据时刻",
|
|
80
|
+
value: timeOf(s.sampledAt)
|
|
81
|
+
}
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 面板组件。
|
|
86
|
+
*
|
|
87
|
+
* 颜色全部走 token:
|
|
88
|
+
* 标题 / 值 → --dsw-alias-label-primary
|
|
89
|
+
* 标签列 → --dsw-alias-label-secondary
|
|
90
|
+
* 异常值 → --dsw-alias-state-error-primary
|
|
91
|
+
* 面板底色 → --dsw-specific-sidebar-fill(与侧边栏同底,避免突兀色块)
|
|
92
|
+
* 分隔线 → --dsw-alias-border-l3
|
|
93
|
+
*/
|
|
94
|
+
function StatusPanel(props) {
|
|
95
|
+
const rows = rowsOf(props.outcome);
|
|
96
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
97
|
+
style: {
|
|
98
|
+
padding: "8px 8px 10px",
|
|
99
|
+
fontSize: 12,
|
|
100
|
+
lineHeight: 1.7,
|
|
101
|
+
color: "var(--dsw-alias-label-primary)",
|
|
102
|
+
background: "var(--dsw-specific-sidebar-fill)",
|
|
103
|
+
borderTop: "0.5px solid var(--dsw-alias-border-l3)",
|
|
104
|
+
marginTop: 4
|
|
105
|
+
},
|
|
106
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
107
|
+
style: {
|
|
108
|
+
fontWeight: 600,
|
|
109
|
+
marginBottom: 6
|
|
110
|
+
},
|
|
111
|
+
children: "Remote Bridge 状态(只读)"
|
|
112
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("table", {
|
|
113
|
+
style: {
|
|
114
|
+
borderCollapse: "collapse",
|
|
115
|
+
width: "100%"
|
|
116
|
+
},
|
|
117
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: rows.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
118
|
+
style: {
|
|
119
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
120
|
+
paddingRight: 12,
|
|
121
|
+
whiteSpace: "nowrap",
|
|
122
|
+
verticalAlign: "top"
|
|
123
|
+
},
|
|
124
|
+
children: row.label
|
|
125
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
126
|
+
style: {
|
|
127
|
+
color: row.bad ? "var(--dsw-alias-state-error-primary)" : "inherit",
|
|
128
|
+
wordBreak: "break-all"
|
|
129
|
+
},
|
|
130
|
+
children: row.value
|
|
131
|
+
})] }, row.label)) })
|
|
132
|
+
})]
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/client/index.tsx
|
|
137
|
+
const ENTRY_ID = "bridge-plugin";
|
|
138
|
+
const STATUS_PATH = "/bridge-plugin/status";
|
|
139
|
+
/** 轮询间隔:入口与面板共用同一份数据,不会重复轮询。 */
|
|
140
|
+
const POLL_MS = 3e3;
|
|
141
|
+
const inject = ["slots"];
|
|
142
|
+
async function readStatus() {
|
|
143
|
+
try {
|
|
144
|
+
const response = await fetch(STATUS_PATH, { cache: "no-store" });
|
|
145
|
+
if (!response.ok) return {
|
|
146
|
+
kind: "error",
|
|
147
|
+
message: "HTTP " + response.status
|
|
148
|
+
};
|
|
149
|
+
return {
|
|
150
|
+
kind: "ok",
|
|
151
|
+
status: await response.json()
|
|
152
|
+
};
|
|
153
|
+
} catch (error) {
|
|
154
|
+
return {
|
|
155
|
+
kind: "error",
|
|
156
|
+
message: error instanceof Error ? error.message : String(error)
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* 桥接图标:与原生侧边栏图标同规格。
|
|
162
|
+
*
|
|
163
|
+
* size 16 / viewBox 24 / stroke-width 1.5 —— 前两项实测自 sidebar bundle
|
|
164
|
+
* (IconPanelLeftOutlineRegular 传 size:16),线宽沿用 DS 图标集的常规值。
|
|
165
|
+
* stroke=currentColor 跟随 label 色,自动适配深浅主题。
|
|
166
|
+
*/
|
|
167
|
+
function BridgeIcon() {
|
|
168
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
169
|
+
width: "16",
|
|
170
|
+
height: "16",
|
|
171
|
+
viewBox: "0 0 24 24",
|
|
172
|
+
fill: "none",
|
|
173
|
+
stroke: "currentColor",
|
|
174
|
+
strokeWidth: "1.5",
|
|
175
|
+
strokeLinecap: "round",
|
|
176
|
+
strokeLinejoin: "round",
|
|
177
|
+
"aria-hidden": "true",
|
|
178
|
+
children: [
|
|
179
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M2 12h4" }),
|
|
180
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M18 12h4" }),
|
|
181
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M6 7v10" }),
|
|
182
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M18 7v10" }),
|
|
183
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M6 12h12" }),
|
|
184
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
185
|
+
cx: "6",
|
|
186
|
+
cy: "12",
|
|
187
|
+
r: "1.2",
|
|
188
|
+
fill: "currentColor",
|
|
189
|
+
stroke: "none"
|
|
190
|
+
}),
|
|
191
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
192
|
+
cx: "18",
|
|
193
|
+
cy: "12",
|
|
194
|
+
r: "1.2",
|
|
195
|
+
fill: "currentColor",
|
|
196
|
+
stroke: "none"
|
|
197
|
+
})
|
|
198
|
+
]
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
/** 右侧副信息:状态点 + 文字,样式对齐原生列表项的次要文字。 */
|
|
202
|
+
function StatusBadge(props) {
|
|
203
|
+
const connected = props.outcome.kind === "ok" && props.outcome.status.connection.registered;
|
|
204
|
+
const color = connected ? "var(--dsw-alias-state-success-primary)" : "var(--dsw-alias-state-error-primary)";
|
|
205
|
+
const label = props.outcome.kind === "error" ? "不可用" : connected ? "已连接" : "已断开";
|
|
206
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
207
|
+
style: {
|
|
208
|
+
marginLeft: "auto",
|
|
209
|
+
flex: "none",
|
|
210
|
+
display: "inline-flex",
|
|
211
|
+
alignItems: "center",
|
|
212
|
+
gap: 4,
|
|
213
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
214
|
+
fontSize: 12,
|
|
215
|
+
lineHeight: "18px"
|
|
216
|
+
},
|
|
217
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
218
|
+
"aria-hidden": "true",
|
|
219
|
+
style: {
|
|
220
|
+
width: 6,
|
|
221
|
+
height: 6,
|
|
222
|
+
borderRadius: "50%",
|
|
223
|
+
background: color,
|
|
224
|
+
flex: "none"
|
|
225
|
+
}
|
|
226
|
+
}), label]
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
/** 侧边栏脚部入口:图标 + 文字 + 右侧状态,宽度随容器 stretch。 */
|
|
230
|
+
function BridgeEntry() {
|
|
231
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
232
|
+
const [outcome, setOutcome] = (0, react.useState)({
|
|
233
|
+
kind: "error",
|
|
234
|
+
message: "尚未读取"
|
|
235
|
+
});
|
|
236
|
+
(0, react.useEffect)(() => {
|
|
237
|
+
let cancelled = false;
|
|
238
|
+
const tick = async () => {
|
|
239
|
+
const next = await readStatus();
|
|
240
|
+
if (!cancelled) setOutcome(next);
|
|
241
|
+
};
|
|
242
|
+
tick();
|
|
243
|
+
const timer = setInterval(() => void tick(), POLL_MS);
|
|
244
|
+
return () => {
|
|
245
|
+
cancelled = true;
|
|
246
|
+
clearInterval(timer);
|
|
247
|
+
};
|
|
248
|
+
}, []);
|
|
249
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
250
|
+
style: {
|
|
251
|
+
display: "flex",
|
|
252
|
+
flexDirection: "column",
|
|
253
|
+
alignItems: "stretch",
|
|
254
|
+
width: "100%"
|
|
255
|
+
},
|
|
256
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
257
|
+
type: "button",
|
|
258
|
+
onClick: () => setOpen((v) => !v),
|
|
259
|
+
title: "Remote Bridge 状态(只读)",
|
|
260
|
+
"aria-expanded": open,
|
|
261
|
+
style: {
|
|
262
|
+
boxSizing: "border-box",
|
|
263
|
+
borderRadius: "var(--dsw-radius-md)",
|
|
264
|
+
minHeight: 36,
|
|
265
|
+
color: "var(--dsw-alias-label-primary)",
|
|
266
|
+
font: "inherit",
|
|
267
|
+
textAlign: "left",
|
|
268
|
+
cursor: "pointer",
|
|
269
|
+
background: "transparent",
|
|
270
|
+
border: "none",
|
|
271
|
+
display: "flex",
|
|
272
|
+
alignItems: "center",
|
|
273
|
+
gap: 8,
|
|
274
|
+
margin: "0 2px",
|
|
275
|
+
padding: "7px 8px",
|
|
276
|
+
lineHeight: "22px",
|
|
277
|
+
width: "100%"
|
|
278
|
+
},
|
|
279
|
+
onMouseEnter: (e) => {
|
|
280
|
+
e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-hover)";
|
|
281
|
+
},
|
|
282
|
+
onMouseLeave: (e) => {
|
|
283
|
+
e.currentTarget.style.background = "transparent";
|
|
284
|
+
},
|
|
285
|
+
onFocus: (e) => {
|
|
286
|
+
e.currentTarget.style.outline = "var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary))";
|
|
287
|
+
e.currentTarget.style.outlineOffset = "-2px";
|
|
288
|
+
},
|
|
289
|
+
onBlur: (e) => {
|
|
290
|
+
e.currentTarget.style.outline = "none";
|
|
291
|
+
},
|
|
292
|
+
children: [
|
|
293
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
294
|
+
style: {
|
|
295
|
+
flex: "none",
|
|
296
|
+
display: "inline-flex",
|
|
297
|
+
justifyContent: "center",
|
|
298
|
+
alignItems: "center"
|
|
299
|
+
},
|
|
300
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BridgeIcon, {})
|
|
301
|
+
}),
|
|
302
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
303
|
+
style: {
|
|
304
|
+
textOverflow: "ellipsis",
|
|
305
|
+
whiteSpace: "nowrap",
|
|
306
|
+
minWidth: 0,
|
|
307
|
+
overflow: "hidden"
|
|
308
|
+
},
|
|
309
|
+
children: "桥接状态"
|
|
310
|
+
}),
|
|
311
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusBadge, { outcome })
|
|
312
|
+
]
|
|
313
|
+
}), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusPanel, { outcome }) : null]
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
function apply(ctx) {
|
|
317
|
+
ctx.slots.inject("sidebar.footer.action", () => {
|
|
318
|
+
try {
|
|
319
|
+
return ctx.slots.register({
|
|
320
|
+
name: "sidebar.footer.action",
|
|
321
|
+
id: ENTRY_ID
|
|
322
|
+
}, BridgeEntry);
|
|
323
|
+
} catch {
|
|
324
|
+
return () => {};
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
//#endregion
|
|
329
|
+
exports.apply = apply;
|
|
330
|
+
exports.inject = inject;
|
|
331
|
+
exports.readStatus = readStatus;
|
|
332
|
+
return module.exports;
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","names":["useState"],"sources":["../src/client/StatusPanel.tsx","../src/client/index.tsx"],"sourcesContent":["/**\n * bridge-plugin 只读状态面板的视图。\n *\n * 全部颜色取自 DSH 的 design token(--dsw-*),**没有任何硬编码十六进制** ——\n * DSH 支持深浅主题切换,写死的颜色在另一套主题下会不可读。\n *\n * 纯只读:没有任何按钮会写状态。\n */\nimport type { StatusOutcome } from './types.ts'\n\n/** 一行展示项。 */\nexport interface StatusRow {\n label: string\n value: string\n /** 异常项用警示色,便于一眼看出问题。 */\n bad?: boolean\n}\n\nfunction timeOf(ms: number | null): string {\n return ms === null ? '—' : new Date(ms).toLocaleString()\n}\n\n/**\n * 把一次读取结果转成展示行。\n *\n * 读取失败时**只显示错误**,不显示上一次的快照 ——\n * 否则「relay 已断但面板还显示旧的正常数据」会被误读成一切正常。\n */\nexport function rowsOf(outcome: StatusOutcome): StatusRow[] {\n if (outcome.kind === 'error') {\n return [{ label: '读取状态失败', value: outcome.message, bad: true }]\n }\n const s = outcome.status\n // 断线记录是**历史**,不是当前状态:已连接时它只说明\"上次为什么断过\",\n // 标红会让运维误以为现在不正常。只有当前确实断开时,断线原因才解释现状,\n // 那时才该醒目。\n const disconnected = !s.connection.registered\n const closePrefix = disconnected ? '断线' : '上次断线'\n return [\n { label: '连接状态', value: s.connection.registered ? '已注册' : '未注册', bad: !s.connection.registered },\n { label: 'instanceId', value: s.identity.instanceId },\n { label: 'relayUrl', value: s.identity.relayUrl },\n { label: '心跳间隔', value: s.heartbeat.intervalMs === null ? '—' : s.heartbeat.intervalMs + ' ms' },\n { label: '认证失败次数', value: String(s.authFailures), bad: s.authFailures > 0 },\n { label: '丢弃帧数', value: String(s.droppedFrames), bad: s.droppedFrames > 0 },\n { label: '已上报前缀', value: s.httpPrefixes.reported.length === 0 ? '(无)' : s.httpPrefixes.reported.join('、') },\n { label: closePrefix + '原因', value: s.lastClose.reason ?? '—', bad: disconnected && s.lastClose.reason !== null },\n { label: closePrefix + '时刻', value: timeOf(s.lastClose.at) },\n { label: '重连次数', value: String(s.connection.reconnects) },\n { label: 'sessionController', value: s.sessionControllerReady ? '可用' : '不可用', bad: !s.sessionControllerReady },\n { label: '数据时刻', value: timeOf(s.sampledAt) },\n ]\n}\n\n/**\n * 面板组件。\n *\n * 颜色全部走 token:\n * 标题 / 值 → --dsw-alias-label-primary\n * 标签列 → --dsw-alias-label-secondary\n * 异常值 → --dsw-alias-state-error-primary\n * 面板底色 → --dsw-specific-sidebar-fill(与侧边栏同底,避免突兀色块)\n * 分隔线 → --dsw-alias-border-l3\n */\nexport function StatusPanel(props: { outcome: StatusOutcome }): React.ReactElement {\n const rows = rowsOf(props.outcome)\n return (\n <div\n style={{\n padding: '8px 8px 10px',\n fontSize: 12,\n lineHeight: 1.7,\n color: 'var(--dsw-alias-label-primary)',\n background: 'var(--dsw-specific-sidebar-fill)',\n borderTop: '0.5px solid var(--dsw-alias-border-l3)',\n marginTop: 4,\n }}\n >\n <div style={{ fontWeight: 600, marginBottom: 6 }}>Remote Bridge 状态(只读)</div>\n <table style={{ borderCollapse: 'collapse', width: '100%' }}>\n <tbody>\n {rows.map((row) => (\n <tr key={row.label}>\n <td\n style={{\n color: 'var(--dsw-alias-label-secondary)',\n paddingRight: 12,\n whiteSpace: 'nowrap',\n verticalAlign: 'top',\n }}\n >\n {row.label}\n </td>\n <td\n style={{\n color: row.bad ? 'var(--dsw-alias-state-error-primary)' : 'inherit',\n wordBreak: 'break-all',\n }}\n >\n {row.value}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )\n}\n","/**\n * bridge-plugin 只读状态面板(浏览器侧)。\n *\n * 样式全部取自 DSH 的 design token 与原生 CSS(--dsw-*),不硬编码颜色 ——\n * DSH 支持深浅主题切换,写死的十六进制在另一套主题下会不可读。\n *\n * 入口对齐原生 sidebar 的 .panelRow(插件 / 自动化任务 那一列),逐项照抄数值:\n * box-sizing:border-box; border-radius:var(--dsw-radius-md); min-height:36px;\n * gap:8px; margin:0 2px; padding:7px 8px; line-height:22px; font:inherit; border:none;\n * 悬停 background:var(--dsw-alias-interactive-bg-hover)。\n *\n * 宽度:原生 .panelRow **没有** width 声明 —— 它是 .panelList(column flex)的子项,\n * 默认 stretch 所以撑满。这里用同样的办法:外层 column flex + align-items:stretch,\n * 不写死 100%(写死会在将来加内边距时错位)。\n *\n * 图标:原生用 size:16(IconPanelLeftOutlineRegular 等,实测其 size 参数为 16),\n * viewBox 24、stroke-width 1.5 —— 与 DS 图标集一致。\n *\n * 纯只读:不调用任何写接口。数据来自本插件注册的 GET /bridge-plugin/status。\n */\nimport type { Context as ClientContext } from '@deepseek-ai/cordis'\nimport type {} from '@deepseek-ai/dsh-client-ui-renderer/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-sidebar/client'\nimport { useEffect, useState } from 'react'\nimport { StatusPanel, type StatusRow } from './StatusPanel.tsx'\nimport type { StatusOutcome } from './types.ts'\n\nconst ENTRY_ID = 'bridge-plugin'\nconst STATUS_PATH = '/bridge-plugin/status'\n/** 轮询间隔:入口与面板共用同一份数据,不会重复轮询。 */\nconst POLL_MS = 3_000\n\nexport const inject = ['slots']\n\nexport async function readStatus(): Promise<StatusOutcome> {\n try {\n const response = await fetch(STATUS_PATH, { cache: 'no-store' })\n if (!response.ok) return { kind: 'error', message: 'HTTP ' + response.status }\n return { kind: 'ok', status: (await response.json()) as never }\n } catch (error) {\n return { kind: 'error', message: error instanceof Error ? error.message : String(error) }\n }\n}\n\n/**\n * 桥接图标:与原生侧边栏图标同规格。\n *\n * size 16 / viewBox 24 / stroke-width 1.5 —— 前两项实测自 sidebar bundle\n * (IconPanelLeftOutlineRegular 传 size:16),线宽沿用 DS 图标集的常规值。\n * stroke=currentColor 跟随 label 色,自动适配深浅主题。\n */\nfunction BridgeIcon(): React.ReactElement {\n return (\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M2 12h4\" />\n <path d=\"M18 12h4\" />\n <path d=\"M6 7v10\" />\n <path d=\"M18 7v10\" />\n <path d=\"M6 12h12\" />\n <circle cx=\"6\" cy=\"12\" r=\"1.2\" fill=\"currentColor\" stroke=\"none\" />\n <circle cx=\"18\" cy=\"12\" r=\"1.2\" fill=\"currentColor\" stroke=\"none\" />\n </svg>\n )\n}\n\n/** 右侧副信息:状态点 + 文字,样式对齐原生列表项的次要文字。 */\nfunction StatusBadge(props: { outcome: StatusOutcome }): React.ReactElement {\n const connected = props.outcome.kind === 'ok' && props.outcome.status.connection.registered\n const color = connected\n ? 'var(--dsw-alias-state-success-primary)'\n : 'var(--dsw-alias-state-error-primary)'\n const label = props.outcome.kind === 'error' ? '不可用' : connected ? '已连接' : '已断开'\n return (\n <span\n style={{\n marginLeft: 'auto',\n flex: 'none',\n display: 'inline-flex',\n alignItems: 'center',\n gap: 4,\n color: 'var(--dsw-alias-label-secondary)',\n fontSize: 12,\n lineHeight: '18px',\n }}\n >\n <span\n aria-hidden=\"true\"\n style={{ width: 6, height: 6, borderRadius: '50%', background: color, flex: 'none' }}\n />\n {label}\n </span>\n )\n}\n\n/** 侧边栏脚部入口:图标 + 文字 + 右侧状态,宽度随容器 stretch。 */\nfunction BridgeEntry(): React.ReactElement {\n const [open, setOpen] = useState(false)\n const [outcome, setOutcome] = useState<StatusOutcome>({ kind: 'error', message: '尚未读取' })\n\n useEffect(() => {\n // 常驻轮询:入口右侧的状态需要它,面板打开时复用同一份,不重复轮询。\n let cancelled = false\n const tick = async (): Promise<void> => {\n const next = await readStatus()\n if (!cancelled) setOutcome(next)\n }\n void tick()\n const timer = setInterval(() => void tick(), POLL_MS)\n return () => {\n cancelled = true\n clearInterval(timer)\n }\n }, [])\n\n const entryStyle: React.CSSProperties = {\n boxSizing: 'border-box',\n borderRadius: 'var(--dsw-radius-md)',\n minHeight: 36,\n color: 'var(--dsw-alias-label-primary)',\n font: 'inherit',\n textAlign: 'left',\n cursor: 'pointer',\n background: 'transparent',\n border: 'none',\n display: 'flex',\n alignItems: 'center',\n gap: 8,\n margin: '0 2px',\n padding: '7px 8px',\n lineHeight: '22px',\n // 与原生一致:不写死 width,靠父容器的 stretch 撑满。\n width: '100%',\n }\n\n return (\n <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'stretch', width: '100%' }}>\n <button\n type=\"button\"\n onClick={() => setOpen((v) => !v)}\n title=\"Remote Bridge 状态(只读)\"\n aria-expanded={open}\n style={entryStyle}\n onMouseEnter={(e) => { e.currentTarget.style.background = 'var(--dsw-alias-interactive-bg-hover)' }}\n onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent' }}\n onFocus={(e) => { e.currentTarget.style.outline = 'var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary))'; e.currentTarget.style.outlineOffset = '-2px' }}\n onBlur={(e) => { e.currentTarget.style.outline = 'none' }}\n >\n <span style={{ flex: 'none', display: 'inline-flex', justifyContent: 'center', alignItems: 'center' }}>\n <BridgeIcon />\n </span>\n <span style={{ textOverflow: 'ellipsis', whiteSpace: 'nowrap', minWidth: 0, overflow: 'hidden' }}>\n 桥接状态\n </span>\n <StatusBadge outcome={outcome} />\n </button>\n {open ? <StatusPanel outcome={outcome} /> : null}\n </div>\n )\n}\n\nexport function apply(ctx: ClientContext): void {\n ctx.slots.inject('sidebar.footer.action', () => {\n try {\n return ctx.slots.register({ name: 'sidebar.footer.action', id: ENTRY_ID }, BridgeEntry)\n } catch {\n return () => {}\n }\n })\n}\n\nexport type { StatusRow }\n"],"mappings":"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"}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-remote-bridge —— DSH host 侧插件。
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* 1. 主动出站连接到公网网关(DSH → VPS,VPS 永不主动连入)
|
|
6
|
+
* 2. 自证身份(同进程内直接使用 sessionController,不走 HTTP、不碰 token)
|
|
7
|
+
* 3. 只响应白名单 RPC,绝不透传任意请求
|
|
8
|
+
*
|
|
9
|
+
* 硬性纪律见 docs/decisions/ADR-002-no-http-fallback.md。
|
|
10
|
+
*/
|
|
11
|
+
import { Service, type Context } from '@deepseek-ai/cordis';
|
|
12
|
+
import z from '@deepseek-ai/schemastery';
|
|
13
|
+
import { Dispatcher } from './relay/dispatcher.ts';
|
|
14
|
+
import { FollowRegistry } from './relay/follow.ts';
|
|
15
|
+
export declare const name = "remote-bridge";
|
|
16
|
+
/**
|
|
17
|
+
* 只注入我们真正需要的服务。connection 用于兜底自证,sessionController 是主路径。
|
|
18
|
+
*
|
|
19
|
+
* cordis 语义(@deepseek-ai/cordis 4.0.5,fiber.ts:611-623 `_refresh`):
|
|
20
|
+
* 任一 inject 服务缺失,fiber 停在 PENDING —— 不报错、不加载、不打日志。
|
|
21
|
+
* 也就是说"服务名写错"的表现是**插件静默不工作**,而不是启动失败。
|
|
22
|
+
* 因此服务名必须与实装一致(0.2.1-rc.2:`super(ctx, "sessionController")` lib/index.js:2952,
|
|
23
|
+
* `connection` lib/index.js:710),任何服务名调整都要同时改这里。
|
|
24
|
+
*
|
|
25
|
+
* ── 为什么 webServer **不在这里**(I-3)────────────────────────────────
|
|
26
|
+
*
|
|
27
|
+
* 它是**可选能力**,不是主能力。JS 侧三条使用点全部是防御式读取:
|
|
28
|
+
* - resolveUpstreamOrigin / collectHttpPrefixes:ctx.get('webServer'),取不到即降级;
|
|
29
|
+
* - 状态路由:走 ctx.inject(['webServer'], …),本来就是可选注入。
|
|
30
|
+
* 既然没有任何一条路径依赖它"必须就绪",把它写进 inject 只会带来一个后果:
|
|
31
|
+
* 在**没有 web 服务的 profile** 里整插件停在 PENDING(cordis 语义:任一 inject 缺失
|
|
32
|
+
* 即不加载、不报错、不打日志)。那等于用一个可选能力换掉主能力——
|
|
33
|
+
* 窄 RPC 会一起消失,而且**没有任何日志**说明为什么。
|
|
34
|
+
*
|
|
35
|
+
* 这正是本文件历史上踩过的坑:同一段代码上方曾有过一条"webServer 不在 inject 里,
|
|
36
|
+
* 这是刻意的"的注释,而 inject 里却真的写着 webServer —— 注释与代码互相矛盾,
|
|
37
|
+
* 直到有部署在没有 webServer 的环境里静默不工作才可能暴露。
|
|
38
|
+
* 现在两者一致:**inject 里每一项都必须被主路径无条件需要**,可选能力一律走 ctx.inject。
|
|
39
|
+
*
|
|
40
|
+
* 代价(明确接受):在非 web profile 里 ctx.get('webServer') 返回 undefined,
|
|
41
|
+
* 转发与状态路由不可用。这是可接受的:没有 web 服务时本来也没有 GUI 可转发。
|
|
42
|
+
* 调用方会回明确的 upstream_unreachable / 打印 "webServer unavailable",前端能看到原因。
|
|
43
|
+
*/
|
|
44
|
+
export declare const inject: string[];
|
|
45
|
+
export interface Config {
|
|
46
|
+
relayUrl: string;
|
|
47
|
+
instanceId: string;
|
|
48
|
+
instanceKey: string;
|
|
49
|
+
reconnectBaseMs?: number;
|
|
50
|
+
reconnectMaxMs?: number;
|
|
51
|
+
/** 首次心跳间隔;网关 hello 回执会回传权威值并覆盖它。 */
|
|
52
|
+
heartbeatIntervalMs?: number;
|
|
53
|
+
}
|
|
54
|
+
export default class RemoteBridge extends Service {
|
|
55
|
+
static Config: z<Config>;
|
|
56
|
+
private client?;
|
|
57
|
+
/**
|
|
58
|
+
* 面板用的运行时快照(只读)。
|
|
59
|
+
*
|
|
60
|
+
* 全部取自既有状态,不新增采集:连接态、断线原因、各类计数都在这里汇合,
|
|
61
|
+
* 供 /bridge-plugin/status 路由读取。
|
|
62
|
+
*/
|
|
63
|
+
private panelState;
|
|
64
|
+
/**
|
|
65
|
+
* 以下三个引用只用于**观测**(诊断/测试),不参与业务逻辑。
|
|
66
|
+
*
|
|
67
|
+
* T14 的教训:dispatcher / follows 曾完全是 ctx.effect 闭包里的局部量,
|
|
68
|
+
* 插件实例上完全不可达,导致 "Cannot read properties of undefined (reading 'follow')"
|
|
69
|
+
* 这类问题无法从实例外部观察,只能靠复刻缩小范围。暴露成只读字段是为了让同类
|
|
70
|
+
* 问题下次能一眼看到。
|
|
71
|
+
*/
|
|
72
|
+
private dispatcherRef?;
|
|
73
|
+
private followsRegistry?;
|
|
74
|
+
private readonly config;
|
|
75
|
+
constructor(ctx: Context, config: Config);
|
|
76
|
+
/** 供调试/测试读取当前连接状态(已收到 hello 回执才算就绪)。 */
|
|
77
|
+
get connected(): boolean;
|
|
78
|
+
/**
|
|
79
|
+
* 组装面板所需的只读状态。
|
|
80
|
+
*
|
|
81
|
+
* 七项全部取自既有运行态,**不新增采集、不猜值**:
|
|
82
|
+
* 连接状态 / instanceId + relayUrl / 心跳间隔 / authFailures /
|
|
83
|
+
* droppedFrames + 已上报前缀 / 断线原因 / sessionControllerReady
|
|
84
|
+
*
|
|
85
|
+
* 安全:绝不包含 instanceKey 或任何凭据。
|
|
86
|
+
*/
|
|
87
|
+
private buildStatusPayload;
|
|
88
|
+
/** 只读诊断:当前 dispatcher(插件未激活时为 undefined)。 */
|
|
89
|
+
get dispatcher(): Dispatcher | undefined;
|
|
90
|
+
/** 只读诊断:当前帧流注册表(插件未激活时为 undefined)。 */
|
|
91
|
+
get follows(): FollowRegistry | undefined;
|
|
92
|
+
/**
|
|
93
|
+
* 只读诊断:sessionController 当下是否可解析且具备 follow()。
|
|
94
|
+
*
|
|
95
|
+
* 不抛错——诊断接口不该在健康检查时炸掉调用方。
|
|
96
|
+
*/
|
|
97
|
+
/**
|
|
98
|
+
* 即刻探测 sessionController 是否可用。
|
|
99
|
+
*
|
|
100
|
+
* 每次调用都真实取一次服务并检查形状,不复用历史结果 ——
|
|
101
|
+
* 面板要回答的是「现在能不能用」。
|
|
102
|
+
*/
|
|
103
|
+
private probeSessionController;
|
|
104
|
+
/** 供调试/测试读取当前 sessionController 可用性(实时探测)。 */
|
|
105
|
+
get sessionControllerReady(): boolean;
|
|
106
|
+
}
|