@gatherthread/dsh-host 0.1.0-alpha.1
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/LICENSE +201 -0
- package/README.md +161 -0
- package/bundle/manifest.json +32 -0
- package/client/client.js +616 -0
- package/cordis.patch.yml +14 -0
- package/dist/bundle/native-plugin.js +4947 -0
- package/dist/bundle/plugin.js +3713 -0
- package/dist/src/plugin.d.ts +9 -0
- package/package.json +69 -0
- package/types/native-plugin.d.ts +30 -0
package/client/client.js
ADDED
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({ id: "@gatherthread/dsh-host", factory: (require) => {
|
|
2
|
+
var module = { exports: {} }; var exports = module.exports;
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const React = require("react");
|
|
6
|
+
const STATUS_PATH = "/api/gatherthread.status";
|
|
7
|
+
const RPC_CHANNEL = "/gatherthread";
|
|
8
|
+
const POLL_INTERVAL_MS = 2000;
|
|
9
|
+
const MAX_TRANSIENT_RETRIES = 3;
|
|
10
|
+
const MAX_RESPONSE_BYTES = 64 * 1024;
|
|
11
|
+
const CONNECTION_STATES = new Set(["connecting", "connected", "offline", "error", "stopped"]);
|
|
12
|
+
const SESSION_STATES = new Set(["connecting", "idle", "running", "offline", "error"]);
|
|
13
|
+
const AUTHORIZATION_STATES = new Set(["unpaired", "pairing", "paired"]);
|
|
14
|
+
|
|
15
|
+
const inject = ["slots", "connection", "sessions"];
|
|
16
|
+
|
|
17
|
+
function apply(ctx) {
|
|
18
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
19
|
+
name: "settings.section",
|
|
20
|
+
id: "gatherthread",
|
|
21
|
+
order: 30,
|
|
22
|
+
label: "GatherThread / 共序",
|
|
23
|
+
}, () => GatherThreadStatusPanel({ connection: ctx.connection, sessions: ctx.sessions })));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function GatherThreadStatusPanel({ connection, sessions }) {
|
|
27
|
+
const [snapshot, setSnapshot] = React.useState(undefined);
|
|
28
|
+
const [nativeState, setNativeState] = React.useState(undefined);
|
|
29
|
+
const [catalog, setCatalog] = React.useState(undefined);
|
|
30
|
+
const [unavailable, setUnavailable] = React.useState(false);
|
|
31
|
+
const [busy, setBusy] = React.useState(false);
|
|
32
|
+
const [notice, setNotice] = React.useState("");
|
|
33
|
+
const [serverUrl, setServerUrl] = React.useState("");
|
|
34
|
+
const [deviceName, setDeviceName] = React.useState("DeepSeek Harness");
|
|
35
|
+
const [provider, setProvider] = React.useState("");
|
|
36
|
+
const [model, setModel] = React.useState("");
|
|
37
|
+
const visibleSessionSignature = React.useRef("");
|
|
38
|
+
|
|
39
|
+
React.useEffect(() => {
|
|
40
|
+
let disposed = false;
|
|
41
|
+
let timer;
|
|
42
|
+
let activeRequest;
|
|
43
|
+
let transientFailures = 0;
|
|
44
|
+
|
|
45
|
+
const refresh = async () => {
|
|
46
|
+
activeRequest = new AbortController();
|
|
47
|
+
let terminalFailure = false;
|
|
48
|
+
try {
|
|
49
|
+
const result = await readHostState(connection, activeRequest.signal);
|
|
50
|
+
terminalFailure = result.terminalFailure;
|
|
51
|
+
if (!disposed) {
|
|
52
|
+
transientFailures = 0;
|
|
53
|
+
const nextSignature = result.nativeState?.runtime.sessions
|
|
54
|
+
.map((session) => session.sessionId)
|
|
55
|
+
.sort()
|
|
56
|
+
.join("\n") ?? "";
|
|
57
|
+
if (nextSignature !== visibleSessionSignature.current) {
|
|
58
|
+
visibleSessionSignature.current = nextSignature;
|
|
59
|
+
void Promise.resolve(sessions.refresh()).catch(() => undefined);
|
|
60
|
+
}
|
|
61
|
+
setSnapshot(result.snapshot);
|
|
62
|
+
setNativeState(result.nativeState);
|
|
63
|
+
setUnavailable(false);
|
|
64
|
+
timer = setTimeout(refresh, POLL_INTERVAL_MS);
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (disposed || activeRequest.signal.aborted) return;
|
|
68
|
+
terminalFailure = error?.terminalFailure === true;
|
|
69
|
+
setSnapshot(undefined);
|
|
70
|
+
setNativeState(undefined);
|
|
71
|
+
setCatalog(undefined);
|
|
72
|
+
setUnavailable(true);
|
|
73
|
+
if (!terminalFailure && transientFailures < MAX_TRANSIENT_RETRIES) {
|
|
74
|
+
transientFailures += 1;
|
|
75
|
+
timer = setTimeout(refresh, POLL_INTERVAL_MS * (2 ** (transientFailures - 1)));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
void refresh();
|
|
81
|
+
return () => {
|
|
82
|
+
disposed = true;
|
|
83
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
84
|
+
activeRequest?.abort();
|
|
85
|
+
};
|
|
86
|
+
}, [connection, sessions]);
|
|
87
|
+
|
|
88
|
+
const runAction = async (endpoint, payload, onSuccess) => {
|
|
89
|
+
if (busy) return;
|
|
90
|
+
const controller = new AbortController();
|
|
91
|
+
setBusy(true);
|
|
92
|
+
setNotice("");
|
|
93
|
+
try {
|
|
94
|
+
const value = await callNative(connection, endpoint, payload, controller.signal);
|
|
95
|
+
const next = endpoint === "catalog/get" ? parseCatalog(value) : parseNativeState(value);
|
|
96
|
+
if (endpoint === "catalog/get") {
|
|
97
|
+
setCatalog(next);
|
|
98
|
+
const firstProvider = next.providers[0];
|
|
99
|
+
setProvider((current) => current || firstProvider?.id || "");
|
|
100
|
+
setModel((current) => current || firstProvider?.models[0]?.id || "");
|
|
101
|
+
} else {
|
|
102
|
+
setNativeState(next);
|
|
103
|
+
setSnapshot(next.runtime);
|
|
104
|
+
if (endpoint === "connection/configure") await sessions.refresh();
|
|
105
|
+
}
|
|
106
|
+
onSuccess?.(next);
|
|
107
|
+
} catch {
|
|
108
|
+
setNotice("操作未完成。请检查服务器地址、当前授权和模型配置。");
|
|
109
|
+
} finally {
|
|
110
|
+
setBusy(false);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const connectionState = snapshot?.connection ?? "connecting";
|
|
115
|
+
const project = snapshot?.projectName ?? "GatherThread";
|
|
116
|
+
const sessionRows = snapshot?.sessions ?? [];
|
|
117
|
+
return React.createElement("section", {
|
|
118
|
+
"data-gatherthread-status": connectionState,
|
|
119
|
+
style: styles.panel,
|
|
120
|
+
},
|
|
121
|
+
React.createElement("div", { style: styles.heading },
|
|
122
|
+
React.createElement("div", null,
|
|
123
|
+
React.createElement("h2", { style: styles.title }, "GatherThread / 共序"),
|
|
124
|
+
React.createElement("p", { style: styles.subtitle }, project)),
|
|
125
|
+
React.createElement("span", { style: badgeStyle(connectionState) }, statusLabel(connectionState))),
|
|
126
|
+
unavailable
|
|
127
|
+
? React.createElement("p", { role: "status", style: styles.notice }, "状态暂不可用。请在 Host 恢复后刷新页面。")
|
|
128
|
+
: null,
|
|
129
|
+
React.createElement("p", { style: styles.summary },
|
|
130
|
+
`活跃会话 ${String(snapshot?.activeSessionCount ?? 0)} / ${String(sessionRows.length)}`),
|
|
131
|
+
React.createElement("div", { style: styles.list },
|
|
132
|
+
...sessionRows.map((session) => React.createElement("div", {
|
|
133
|
+
key: session.sessionId,
|
|
134
|
+
style: styles.row,
|
|
135
|
+
},
|
|
136
|
+
React.createElement("div", { style: styles.sessionText },
|
|
137
|
+
React.createElement("strong", { style: styles.sessionTitle }, session.title),
|
|
138
|
+
React.createElement("span", { style: styles.sessionMeta },
|
|
139
|
+
session.lastSyncedAt === undefined ? "尚未同步" : `最近同步 ${formatTime(session.lastSyncedAt)}`)),
|
|
140
|
+
React.createElement("span", { style: badgeStyle(session.state) }, statusLabel(session.state))))),
|
|
141
|
+
nativeState === undefined || unavailable
|
|
142
|
+
? null
|
|
143
|
+
: renderNativeControls({
|
|
144
|
+
nativeState,
|
|
145
|
+
catalog,
|
|
146
|
+
busy,
|
|
147
|
+
notice,
|
|
148
|
+
serverUrl,
|
|
149
|
+
deviceName,
|
|
150
|
+
provider,
|
|
151
|
+
model,
|
|
152
|
+
setServerUrl,
|
|
153
|
+
setDeviceName,
|
|
154
|
+
setProvider,
|
|
155
|
+
setModel,
|
|
156
|
+
runAction,
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function readHostState(connection, signal) {
|
|
161
|
+
if (typeof connection?.rpc?.call === "function") {
|
|
162
|
+
try {
|
|
163
|
+
const value = await callNative(connection, "status/get", {}, signal);
|
|
164
|
+
const nativeState = parseNativeState(value);
|
|
165
|
+
return { snapshot: nativeState.runtime, nativeState, terminalFailure: false };
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (signal.aborted) throw error;
|
|
168
|
+
const failure = error instanceof Error ? error : new Error("native status unavailable");
|
|
169
|
+
failure.terminalFailure = true;
|
|
170
|
+
throw failure;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const response = await fetch(STATUS_PATH, {
|
|
174
|
+
method: "GET",
|
|
175
|
+
credentials: "same-origin",
|
|
176
|
+
cache: "no-store",
|
|
177
|
+
redirect: "error",
|
|
178
|
+
headers: { accept: "application/json" },
|
|
179
|
+
signal,
|
|
180
|
+
});
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
const error = new Error("status unavailable");
|
|
183
|
+
error.terminalFailure = response.status >= 400 && response.status < 500;
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
const contentLength = Number(response.headers.get("content-length") || "0");
|
|
187
|
+
if (contentLength > MAX_RESPONSE_BYTES) throw new Error("status response too large");
|
|
188
|
+
const text = await response.text();
|
|
189
|
+
if (text.length > MAX_RESPONSE_BYTES) throw new Error("status response too large");
|
|
190
|
+
return { snapshot: parseSnapshot(JSON.parse(text)), nativeState: undefined, terminalFailure: false };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function callNative(connection, endpoint, payload, signal) {
|
|
194
|
+
if (connection === undefined || connection === null || typeof connection.rpc?.call !== "function") {
|
|
195
|
+
throw new Error("native status unavailable");
|
|
196
|
+
}
|
|
197
|
+
const result = await connection.rpc.call(RPC_CHANNEL, endpoint, payload, signal);
|
|
198
|
+
exactObject(result, ["ok", "value", "error"]);
|
|
199
|
+
if (result.ok !== true) throw new Error("native action failed");
|
|
200
|
+
return result.value;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function renderNativeControls(input) {
|
|
204
|
+
const state = input.nativeState;
|
|
205
|
+
const controls = [];
|
|
206
|
+
if (state.authorization === "unpaired") {
|
|
207
|
+
controls.push(
|
|
208
|
+
React.createElement("div", { key: "service-options", style: styles.choiceRow },
|
|
209
|
+
React.createElement("button", {
|
|
210
|
+
type: "button",
|
|
211
|
+
disabled: input.busy || state.officialServerUrl === undefined,
|
|
212
|
+
style: styles.secondaryButton,
|
|
213
|
+
onClick: () => input.setServerUrl(state.officialServerUrl || ""),
|
|
214
|
+
}, state.officialServerUrl === undefined ? "共序官方服务 · 尚未开放" : "使用共序官方服务"),
|
|
215
|
+
React.createElement("span", { style: styles.muted }, "局域网、自托管与 Tailscale 请使用自定义地址。")),
|
|
216
|
+
React.createElement("form", {
|
|
217
|
+
key: "pairing-form",
|
|
218
|
+
style: styles.form,
|
|
219
|
+
onSubmit: (event) => {
|
|
220
|
+
event.preventDefault();
|
|
221
|
+
void input.runAction("pairing/start", {
|
|
222
|
+
serverUrl: input.serverUrl,
|
|
223
|
+
deviceName: input.deviceName,
|
|
224
|
+
});
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
field("GatherThread 服务器地址", React.createElement("input", {
|
|
228
|
+
type: "url",
|
|
229
|
+
required: true,
|
|
230
|
+
maxLength: 2048,
|
|
231
|
+
value: input.serverUrl,
|
|
232
|
+
placeholder: "https://gatherthread.example",
|
|
233
|
+
autoComplete: "url",
|
|
234
|
+
style: styles.input,
|
|
235
|
+
onChange: (event) => input.setServerUrl(event.target.value),
|
|
236
|
+
})),
|
|
237
|
+
field("DSH 设备名称", React.createElement("input", {
|
|
238
|
+
type: "text",
|
|
239
|
+
required: true,
|
|
240
|
+
maxLength: 120,
|
|
241
|
+
value: input.deviceName,
|
|
242
|
+
autoComplete: "off",
|
|
243
|
+
style: styles.input,
|
|
244
|
+
onChange: (event) => input.setDeviceName(event.target.value),
|
|
245
|
+
})),
|
|
246
|
+
React.createElement("button", {
|
|
247
|
+
type: "submit",
|
|
248
|
+
disabled: input.busy,
|
|
249
|
+
style: styles.primaryButton,
|
|
250
|
+
}, input.busy ? "正在创建配对…" : "登录并配对")),
|
|
251
|
+
React.createElement("p", { key: "account-note", style: styles.muted },
|
|
252
|
+
"当前版本使用你已打开的 GatherThread 浏览器会话或邀请身份确认,不假设另有公共账户。长期凭据只保存在 DSH 本机凭据库。"),
|
|
253
|
+
);
|
|
254
|
+
} else if (state.authorization === "pairing") {
|
|
255
|
+
controls.push(
|
|
256
|
+
React.createElement("div", { key: "pairing", style: styles.pairing },
|
|
257
|
+
React.createElement("p", { style: styles.summary }, "在浏览器中确认这次短时、单次配对:"),
|
|
258
|
+
React.createElement("code", { style: styles.code }, state.pairing.userCode),
|
|
259
|
+
React.createElement("a", {
|
|
260
|
+
href: state.pairing.verificationUrl,
|
|
261
|
+
target: "_blank",
|
|
262
|
+
rel: "noopener noreferrer",
|
|
263
|
+
style: styles.linkButton,
|
|
264
|
+
}, "打开 GatherThread 确认"),
|
|
265
|
+
React.createElement("button", {
|
|
266
|
+
type: "button",
|
|
267
|
+
disabled: input.busy,
|
|
268
|
+
style: styles.secondaryButton,
|
|
269
|
+
onClick: () => void input.runAction("pairing/cancel", {}),
|
|
270
|
+
}, "取消配对")),
|
|
271
|
+
);
|
|
272
|
+
} else if (state.route === undefined) {
|
|
273
|
+
const providers = input.catalog?.providers ?? [];
|
|
274
|
+
const selectedProvider = providers.find((entry) => entry.id === input.provider) ?? providers[0];
|
|
275
|
+
const models = selectedProvider?.models ?? [];
|
|
276
|
+
controls.push(
|
|
277
|
+
connectionSummary(state),
|
|
278
|
+
input.catalog === undefined
|
|
279
|
+
? React.createElement("button", {
|
|
280
|
+
key: "load-catalog",
|
|
281
|
+
type: "button",
|
|
282
|
+
disabled: input.busy,
|
|
283
|
+
style: styles.primaryButton,
|
|
284
|
+
onClick: () => void input.runAction("catalog/get", {}),
|
|
285
|
+
}, input.busy ? "正在读取…" : "选择 DSH 模型")
|
|
286
|
+
: React.createElement("form", {
|
|
287
|
+
key: "binding-form",
|
|
288
|
+
style: styles.form,
|
|
289
|
+
onSubmit: (event) => {
|
|
290
|
+
event.preventDefault();
|
|
291
|
+
void input.runAction("connection/configure", {
|
|
292
|
+
provider: input.provider,
|
|
293
|
+
model: input.model,
|
|
294
|
+
}, () => input.setCatalog(undefined));
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
React.createElement("p", { style: styles.muted },
|
|
298
|
+
`将同步全部 ${String(input.catalog.projects.length)} 个可访问的 GatherThread 项目;项目权限分别生效。`),
|
|
299
|
+
field("DSH Provider", selectControl(
|
|
300
|
+
providers,
|
|
301
|
+
input.provider,
|
|
302
|
+
(value) => {
|
|
303
|
+
input.setProvider(value);
|
|
304
|
+
const next = providers.find((entry) => entry.id === value);
|
|
305
|
+
input.setModel(next?.models[0]?.id || "");
|
|
306
|
+
},
|
|
307
|
+
(entry) => entry.id,
|
|
308
|
+
(entry) => entry.name,
|
|
309
|
+
)),
|
|
310
|
+
field("DSH Model", selectControl(
|
|
311
|
+
models,
|
|
312
|
+
input.model,
|
|
313
|
+
input.setModel,
|
|
314
|
+
(entry) => entry.id,
|
|
315
|
+
(entry) => entry.name,
|
|
316
|
+
)),
|
|
317
|
+
React.createElement("button", {
|
|
318
|
+
type: "submit",
|
|
319
|
+
disabled: input.busy || !input.provider || !input.model,
|
|
320
|
+
style: styles.primaryButton,
|
|
321
|
+
}, input.busy ? "正在连接…" : "连接全部可访问项目")),
|
|
322
|
+
disconnectButton(input),
|
|
323
|
+
);
|
|
324
|
+
} else {
|
|
325
|
+
controls.push(
|
|
326
|
+
connectionSummary(state),
|
|
327
|
+
React.createElement("dl", { key: "binding", style: styles.details },
|
|
328
|
+
detail("项目", `${String(state.projectCount)} 个可访问项目`),
|
|
329
|
+
detail("Provider", state.route.provider),
|
|
330
|
+
detail("Model", state.route.model)),
|
|
331
|
+
React.createElement("p", { key: "projects", style: styles.muted },
|
|
332
|
+
state.bindings.map((binding) => binding.projectName).join(" · ") || "当前没有可访问的活跃项目。"),
|
|
333
|
+
disconnectButton(input),
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
return React.createElement("section", { style: styles.connectionCard },
|
|
337
|
+
React.createElement("div", { style: styles.subheading },
|
|
338
|
+
React.createElement("h3", { style: styles.sectionTitle }, "连接共序"),
|
|
339
|
+
React.createElement("span", { style: styles.muted }, "插件主动出站连接,不需要公网访问本机 DSH。")),
|
|
340
|
+
...controls,
|
|
341
|
+
React.createElement("p", { style: styles.muted },
|
|
342
|
+
`已验证兼容:${state.compatibility.package}@${state.compatibility.version} · ${state.compatibility.profile}`),
|
|
343
|
+
input.notice ? React.createElement("p", { role: "alert", style: styles.notice }, input.notice) : null);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function connectionSummary(state) {
|
|
347
|
+
return React.createElement("dl", { key: "connection-summary", style: styles.details },
|
|
348
|
+
detail("服务器", state.serverUrl),
|
|
349
|
+
detail("设备", state.deviceName));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function disconnectButton(input) {
|
|
353
|
+
return React.createElement("button", {
|
|
354
|
+
key: "disconnect",
|
|
355
|
+
type: "button",
|
|
356
|
+
disabled: input.busy,
|
|
357
|
+
style: styles.secondaryButton,
|
|
358
|
+
onClick: () => void input.runAction("connection/disconnect", {}, () => input.setCatalog(undefined)),
|
|
359
|
+
}, "断开此 DSH 的本地配对");
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function field(label, control) {
|
|
363
|
+
return React.createElement("label", { style: styles.field },
|
|
364
|
+
React.createElement("span", { style: styles.fieldLabel }, label),
|
|
365
|
+
control);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function selectControl(entries, value, setValue, keyOf, labelOf) {
|
|
369
|
+
return React.createElement("select", {
|
|
370
|
+
required: true,
|
|
371
|
+
value,
|
|
372
|
+
style: styles.input,
|
|
373
|
+
onChange: (event) => setValue(event.target.value),
|
|
374
|
+
}, ...entries.map((entry) => React.createElement("option", {
|
|
375
|
+
key: keyOf(entry),
|
|
376
|
+
value: keyOf(entry),
|
|
377
|
+
}, labelOf(entry))));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function detail(label, value) {
|
|
381
|
+
return React.createElement("div", { style: styles.detailRow },
|
|
382
|
+
React.createElement("dt", { style: styles.muted }, label),
|
|
383
|
+
React.createElement("dd", { style: styles.detailValue }, value));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function parseNativeState(value) {
|
|
387
|
+
exactObject(value, [
|
|
388
|
+
"schemaVersion", "integration", "authorization", "compatibility", "runtime",
|
|
389
|
+
"officialServerUrl", "serverUrl", "deviceName", "route", "projectCount", "bindings", "pairing", "recoverableError",
|
|
390
|
+
]);
|
|
391
|
+
if (value.schemaVersion !== 2 || value.integration !== "gatherthread") throw new Error("invalid native status identity");
|
|
392
|
+
if (!AUTHORIZATION_STATES.has(value.authorization)) throw new Error("invalid native authorization state");
|
|
393
|
+
exactObject(value.compatibility, ["package", "version", "profile"]);
|
|
394
|
+
if (value.compatibility.package !== "@deepseek-ai/dsh"
|
|
395
|
+
|| value.compatibility.version !== "0.1.2-rc.1"
|
|
396
|
+
|| value.compatibility.profile !== "web") throw new Error("incompatible native DSH Host");
|
|
397
|
+
const runtime = parseSnapshot(value.runtime);
|
|
398
|
+
const optionalUrl = (url) => {
|
|
399
|
+
if (url === undefined) return undefined;
|
|
400
|
+
boundedText(url, 2048);
|
|
401
|
+
const parsed = new URL(url);
|
|
402
|
+
if (!/^https?:$/u.test(parsed.protocol) || parsed.username || parsed.password || parsed.search || parsed.hash) {
|
|
403
|
+
throw new Error("invalid public server URL");
|
|
404
|
+
}
|
|
405
|
+
return parsed.toString().replace(/\/$/u, "");
|
|
406
|
+
};
|
|
407
|
+
const serverUrl = optionalUrl(value.serverUrl);
|
|
408
|
+
const officialServerUrl = optionalUrl(value.officialServerUrl);
|
|
409
|
+
if (value.deviceName !== undefined) boundedText(value.deviceName, 120);
|
|
410
|
+
let route;
|
|
411
|
+
let bindings;
|
|
412
|
+
if (value.route !== undefined || value.bindings !== undefined || value.projectCount !== undefined) {
|
|
413
|
+
if (value.route === undefined || !Number.isSafeInteger(value.projectCount) || value.projectCount < 0
|
|
414
|
+
|| !Array.isArray(value.bindings) || value.bindings.length > 100
|
|
415
|
+
|| value.bindings.length > value.projectCount) {
|
|
416
|
+
throw new Error("invalid native Project bindings");
|
|
417
|
+
}
|
|
418
|
+
exactObject(value.route, ["provider", "model"]);
|
|
419
|
+
boundedText(value.route.provider, 80);
|
|
420
|
+
boundedText(value.route.model, 160);
|
|
421
|
+
route = { ...value.route };
|
|
422
|
+
bindings = value.bindings.map((binding) => {
|
|
423
|
+
exactObject(binding, ["projectId", "projectName", "provider", "model"]);
|
|
424
|
+
boundedText(binding.projectId, 128);
|
|
425
|
+
boundedText(binding.projectName, 160);
|
|
426
|
+
boundedText(binding.provider, 80);
|
|
427
|
+
boundedText(binding.model, 160);
|
|
428
|
+
if (binding.provider !== route.provider || binding.model !== route.model) {
|
|
429
|
+
throw new Error("inconsistent native Project route");
|
|
430
|
+
}
|
|
431
|
+
return { ...binding };
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
let pairing;
|
|
435
|
+
if (value.pairing !== undefined) {
|
|
436
|
+
exactObject(value.pairing, ["schemaVersion", "status", "userCode", "verificationUrl", "expiresAt", "intervalSeconds"]);
|
|
437
|
+
if (value.pairing.schemaVersion !== 1 || value.pairing.status !== "pending"
|
|
438
|
+
|| !/^[A-Z2-9]{4}-[A-Z2-9]{4}$/u.test(value.pairing.userCode)
|
|
439
|
+
|| !Number.isSafeInteger(value.pairing.intervalSeconds)
|
|
440
|
+
|| value.pairing.intervalSeconds < 1 || value.pairing.intervalSeconds > 30) {
|
|
441
|
+
throw new Error("invalid native pairing state");
|
|
442
|
+
}
|
|
443
|
+
boundedText(value.pairing.expiresAt, 64);
|
|
444
|
+
boundedText(value.pairing.verificationUrl, 2048);
|
|
445
|
+
const verificationUrl = new URL(value.pairing.verificationUrl);
|
|
446
|
+
if (!/^https?:$/u.test(verificationUrl.protocol) || verificationUrl.username || verificationUrl.password
|
|
447
|
+
|| verificationUrl.search || verificationUrl.hash !== `#dsh-pair=${value.pairing.userCode}`) {
|
|
448
|
+
throw new Error("invalid native verification URL");
|
|
449
|
+
}
|
|
450
|
+
pairing = { ...value.pairing, verificationUrl: verificationUrl.toString() };
|
|
451
|
+
}
|
|
452
|
+
if (value.authorization === "pairing" && pairing === undefined) throw new Error("missing native pairing state");
|
|
453
|
+
if (value.authorization === "paired" && (serverUrl === undefined || value.deviceName === undefined)) {
|
|
454
|
+
throw new Error("missing native grant labels");
|
|
455
|
+
}
|
|
456
|
+
if (value.recoverableError !== undefined
|
|
457
|
+
&& value.recoverableError !== "pairing_failed"
|
|
458
|
+
&& value.recoverableError !== "connection_failed") throw new Error("invalid native recovery state");
|
|
459
|
+
return {
|
|
460
|
+
schemaVersion: 2,
|
|
461
|
+
integration: "gatherthread",
|
|
462
|
+
authorization: value.authorization,
|
|
463
|
+
compatibility: { ...value.compatibility },
|
|
464
|
+
runtime,
|
|
465
|
+
...(officialServerUrl === undefined ? {} : { officialServerUrl }),
|
|
466
|
+
...(serverUrl === undefined ? {} : { serverUrl }),
|
|
467
|
+
...(value.deviceName === undefined ? {} : { deviceName: value.deviceName }),
|
|
468
|
+
...(route === undefined ? {} : { route, projectCount: value.projectCount, bindings }),
|
|
469
|
+
...(pairing === undefined ? {} : { pairing }),
|
|
470
|
+
...(value.recoverableError === undefined ? {} : { recoverableError: value.recoverableError }),
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function parseCatalog(value) {
|
|
475
|
+
exactObject(value, ["schemaVersion", "projects", "providers"]);
|
|
476
|
+
if (value.schemaVersion !== 1 || !Array.isArray(value.projects) || value.projects.length > 100
|
|
477
|
+
|| !Array.isArray(value.providers) || value.providers.length > 64) throw new Error("invalid native catalog");
|
|
478
|
+
const projects = value.projects.map((project) => {
|
|
479
|
+
exactObject(project, ["id", "name", "role", "state", "sessionCount"]);
|
|
480
|
+
boundedText(project.id, 128);
|
|
481
|
+
boundedText(project.name, 160);
|
|
482
|
+
if (!new Set(["owner", "participant", "viewer"]).has(project.role) || project.state !== "active"
|
|
483
|
+
|| !Number.isSafeInteger(project.sessionCount) || project.sessionCount < 0) throw new Error("invalid native Project");
|
|
484
|
+
return { ...project };
|
|
485
|
+
});
|
|
486
|
+
let modelCount = 0;
|
|
487
|
+
const providers = value.providers.map((provider) => {
|
|
488
|
+
exactObject(provider, ["id", "name", "models"]);
|
|
489
|
+
boundedText(provider.id, 80);
|
|
490
|
+
boundedText(provider.name, 160);
|
|
491
|
+
if (!Array.isArray(provider.models)) throw new Error("invalid native models");
|
|
492
|
+
const models = provider.models.map((model) => {
|
|
493
|
+
modelCount += 1;
|
|
494
|
+
if (modelCount > 256) throw new Error("native catalog too large");
|
|
495
|
+
exactObject(model, ["id", "name"]);
|
|
496
|
+
boundedText(model.id, 160);
|
|
497
|
+
boundedText(model.name, 160);
|
|
498
|
+
return { ...model };
|
|
499
|
+
});
|
|
500
|
+
return { id: provider.id, name: provider.name, models };
|
|
501
|
+
});
|
|
502
|
+
return { schemaVersion: 1, projects, providers };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function parseSnapshot(value) {
|
|
506
|
+
exactObject(value, [
|
|
507
|
+
"schemaVersion", "integration", "connection", "bindingMode", "projectName",
|
|
508
|
+
"activeSessionCount", "sessions", "updatedAt",
|
|
509
|
+
]);
|
|
510
|
+
if (value.schemaVersion !== 1 || value.integration !== "gatherthread") throw new Error("invalid status identity");
|
|
511
|
+
if (!CONNECTION_STATES.has(value.connection)) throw new Error("invalid connection state");
|
|
512
|
+
if (value.bindingMode !== "single" && value.bindingMode !== "project") throw new Error("invalid binding mode");
|
|
513
|
+
boundedText(value.projectName, 160);
|
|
514
|
+
boundedText(value.updatedAt, 32);
|
|
515
|
+
if (!Number.isSafeInteger(value.activeSessionCount) || value.activeSessionCount < 0) {
|
|
516
|
+
throw new Error("invalid active Session count");
|
|
517
|
+
}
|
|
518
|
+
if (!Array.isArray(value.sessions) || value.sessions.length > 100) throw new Error("invalid Sessions");
|
|
519
|
+
const sessions = value.sessions.map((session) => {
|
|
520
|
+
exactObject(session, ["sessionId", "title", "state", "lastSyncedAt"]);
|
|
521
|
+
boundedText(session.sessionId, 128);
|
|
522
|
+
boundedText(session.title, 160);
|
|
523
|
+
if (!SESSION_STATES.has(session.state)) throw new Error("invalid Session state");
|
|
524
|
+
if (session.lastSyncedAt !== undefined) boundedText(session.lastSyncedAt, 32);
|
|
525
|
+
return {
|
|
526
|
+
sessionId: session.sessionId,
|
|
527
|
+
title: session.title,
|
|
528
|
+
state: session.state,
|
|
529
|
+
...(session.lastSyncedAt === undefined ? {} : { lastSyncedAt: session.lastSyncedAt }),
|
|
530
|
+
};
|
|
531
|
+
});
|
|
532
|
+
if (value.activeSessionCount > sessions.length) throw new Error("invalid active Session count");
|
|
533
|
+
return {
|
|
534
|
+
schemaVersion: 1,
|
|
535
|
+
integration: "gatherthread",
|
|
536
|
+
connection: value.connection,
|
|
537
|
+
bindingMode: value.bindingMode,
|
|
538
|
+
projectName: value.projectName,
|
|
539
|
+
activeSessionCount: value.activeSessionCount,
|
|
540
|
+
sessions,
|
|
541
|
+
updatedAt: value.updatedAt,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function exactObject(value, allowed) {
|
|
546
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("invalid status object");
|
|
547
|
+
const allowedKeys = new Set(allowed);
|
|
548
|
+
if (Object.keys(value).some((key) => !allowedKeys.has(key))) throw new Error("unexpected status field");
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function boundedText(value, maximum) {
|
|
552
|
+
if (typeof value !== "string" || value.length === 0 || value.length > maximum) throw new Error("invalid status text");
|
|
553
|
+
for (const character of value) {
|
|
554
|
+
const code = character.codePointAt(0) || 0;
|
|
555
|
+
if (code < 32 || (code >= 127 && code <= 159)) throw new Error("invalid status text");
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function statusLabel(state) {
|
|
560
|
+
return ({
|
|
561
|
+
connecting: "连接中",
|
|
562
|
+
connected: "已连接",
|
|
563
|
+
idle: "空闲",
|
|
564
|
+
running: "运行中",
|
|
565
|
+
offline: "离线",
|
|
566
|
+
error: "错误",
|
|
567
|
+
stopped: "已停止",
|
|
568
|
+
})[state] || "未知";
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function badgeStyle(state) {
|
|
572
|
+
const emphasis = state === "connected" || state === "idle"
|
|
573
|
+
? 0.92
|
|
574
|
+
: state === "running" || state === "connecting" ? 0.78 : 0.62;
|
|
575
|
+
return { ...styles.badge, opacity: emphasis };
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function formatTime(value) {
|
|
579
|
+
const date = new Date(value);
|
|
580
|
+
return Number.isFinite(date.getTime()) ? date.toLocaleTimeString() : "未知";
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const styles = {
|
|
584
|
+
panel: { display: "grid", gap: 16, padding: 8, color: "inherit" },
|
|
585
|
+
heading: { display: "flex", justifyContent: "space-between", gap: 16, alignItems: "center" },
|
|
586
|
+
title: { margin: 0, fontSize: 20, lineHeight: 1.3 },
|
|
587
|
+
subtitle: { margin: "4px 0 0", opacity: 0.68, fontSize: 13 },
|
|
588
|
+
summary: { margin: 0, fontSize: 13, opacity: 0.72 },
|
|
589
|
+
notice: { margin: 0, padding: "10px 12px", borderRadius: 10, background: "rgba(127,127,127,.12)", color: "inherit", border: "1px solid rgba(127,127,127,.2)" },
|
|
590
|
+
list: { display: "grid", gap: 8 },
|
|
591
|
+
row: { display: "flex", justifyContent: "space-between", alignItems: "center", gap: 12, padding: "12px 14px", border: "1px solid rgba(127,127,127,.2)", borderRadius: 12 },
|
|
592
|
+
sessionText: { minWidth: 0, display: "grid", gap: 3 },
|
|
593
|
+
sessionTitle: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", fontSize: 14 },
|
|
594
|
+
sessionMeta: { opacity: 0.62, fontSize: 12 },
|
|
595
|
+
badge: { flex: "none", padding: "4px 8px", borderRadius: 999, fontSize: 12, fontWeight: 600, background: "rgba(127,127,127,.12)", color: "inherit", border: "1px solid rgba(127,127,127,.2)" },
|
|
596
|
+
connectionCard: { display: "grid", gap: 12, padding: "14px", border: "1px solid rgba(127,127,127,.2)", borderRadius: 14, background: "rgba(127,127,127,.06)" },
|
|
597
|
+
subheading: { display: "grid", gap: 4 },
|
|
598
|
+
sectionTitle: { margin: 0, fontSize: 16, lineHeight: 1.35 },
|
|
599
|
+
form: { display: "grid", gap: 12 },
|
|
600
|
+
field: { display: "grid", gap: 6 },
|
|
601
|
+
fieldLabel: { fontSize: 12, fontWeight: 600, opacity: 0.76 },
|
|
602
|
+
input: { width: "100%", minHeight: 36, boxSizing: "border-box", padding: "7px 10px", borderRadius: 9, border: "1px solid rgba(127,127,127,.28)", background: "transparent", color: "inherit", font: "inherit" },
|
|
603
|
+
primaryButton: { minHeight: 36, padding: "8px 12px", borderRadius: 9, border: "1px solid Highlight", background: "Highlight", color: "HighlightText", WebkitTextFillColor: "HighlightText", font: "inherit", fontWeight: 600, cursor: "pointer" },
|
|
604
|
+
secondaryButton: { minHeight: 36, padding: "8px 12px", borderRadius: 9, border: "1px solid rgba(127,127,127,.3)", background: "transparent", color: "inherit", font: "inherit", cursor: "pointer" },
|
|
605
|
+
linkButton: { display: "inline-flex", alignItems: "center", justifyContent: "center", minHeight: 36, padding: "0 12px", borderRadius: 9, border: "1px solid rgba(127,127,127,.3)", color: "inherit", textDecoration: "none", fontWeight: 600 },
|
|
606
|
+
muted: { margin: 0, fontSize: 12, lineHeight: 1.55, opacity: 0.68 },
|
|
607
|
+
choiceRow: { display: "grid", gap: 6 },
|
|
608
|
+
pairing: { display: "grid", gap: 10 },
|
|
609
|
+
code: { justifySelf: "start", padding: "8px 12px", borderRadius: 8, border: "1px solid rgba(127,127,127,.24)", background: "rgba(127,127,127,.1)", fontSize: 18, letterSpacing: ".08em" },
|
|
610
|
+
details: { display: "grid", gap: 7, margin: 0 },
|
|
611
|
+
detailRow: { display: "grid", gridTemplateColumns: "88px minmax(0,1fr)", gap: 10, alignItems: "baseline" },
|
|
612
|
+
detailValue: { margin: 0, overflowWrap: "anywhere", fontSize: 13 },
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
module.exports = { inject, apply };
|
|
616
|
+
return module.exports; } });
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Official GatherThread profile layer for the published DSH Web profile.
|
|
2
|
+
# Installing this package is the explicit opt-in; no GatherThread credential is
|
|
3
|
+
# present in this patch. Pairing and model selection happen inside DSH; every
|
|
4
|
+
# active Project accessible to that identity is reconciled independently.
|
|
5
|
+
|
|
6
|
+
- insert:
|
|
7
|
+
- id: gatherthread-dsh-native
|
|
8
|
+
# The Loader row must use the package root. DSH 0.1.2's public Client
|
|
9
|
+
# module discovery intentionally ignores package subpaths, while this
|
|
10
|
+
# package's root export is the same native Host plugin.
|
|
11
|
+
name: '@gatherthread/dsh-host'
|
|
12
|
+
inject: [agents, sessions, sessionPersistence, sessionQuery, sessionTitle, workspaceRegistry, llm, credentials, connection]
|
|
13
|
+
config:
|
|
14
|
+
enabled: true
|