@gatherthread/dsh-host 0.1.0-alpha.5 → 0.1.0-alpha.7

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
@@ -19,7 +19,7 @@ The package does not install a DSH runtime dependency.
19
19
  After `@gatherthread/dsh-host` is published, the ordinary three-step path is:
20
20
 
21
21
  ```text
22
- npx @deepseek-ai/dsh@0.1.2-rc.1 plugin --profile web add @gatherthread/dsh-host@0.1.0-alpha.5
22
+ npx @deepseek-ai/dsh@0.1.2-rc.1 plugin --profile web add @gatherthread/dsh-host@0.1.0-alpha.7
23
23
  npx @deepseek-ai/dsh@0.1.2-rc.1 web
24
24
  # In DSH: Settings -> GatherThread / 共序 -> Sign in and pair
25
25
  ```
@@ -98,13 +98,20 @@ bounded exponential delay.
98
98
 
99
99
  Every eligible Session has its own DSH Session id, HTTP abort scope, connector
100
100
  state file, runtime identity, heartbeat, canonical cursor and durable outbox.
101
- The project gate bounds the whole claim-to-settlement operation. This is
102
- required because the current GatherThread alpha has no claim-abandon or claim
103
- lease-expiry API. Removing or downgrading a Session cancels its queued permit
104
- before it can claim a request. Existing single-Session configuration remains
101
+ The project gate bounds the whole claim-to-settlement operation. Claims are
102
+ leased and renewed by accepted progress, so a Session that stops producing work
103
+ releases its request for the exact recorded runtime to reclaim; the gate still bounds this
104
+ connector's own concurrency while it holds one. Removing or downgrading a Session
105
+ cancels its queued permit before it can claim a request. Existing single-Session configuration remains
105
106
  valid when `bindingMode` is omitted; both modes resolve the real current actor
106
107
  before applying Solo permissions.
107
108
 
109
+ During an active prompt, newly durable DSH Host events queue a rate-limited,
110
+ generic progress marker; event bursts coalesce. The marker carries no event text,
111
+ reasoning, headers, or private stream data, but renews the exact claim attempt. Progress, completion,
112
+ and request-linked tool outbox entries are rebound to the renewed attempt after a
113
+ crash recovery, while stale attempts remain fenced by the server.
114
+
108
115
  The installed native plugin stores one account-level model route and reconciles
109
116
  every active Project visible to the paired GatherThread identity. Adding a
110
117
  Project starts its isolated manager without another pairing step. Archiving a
@@ -112,6 +119,14 @@ Project or revoking access stops only that Project manager; its local workspace
112
119
  and DSH Session history are retained. A failure in one Project does not stop
113
120
  healthy peers, and discovery retries periodically.
114
121
 
122
+ For a `deepseek-official` route, the plugin also advertises the exact models and
123
+ reasoning efforts returned by DSH's public LLM catalog. The GatherThread work
124
+ page may select one advertised combination for an individual Agent request.
125
+ That selection is installed only on the borrowed Agent while the request runs
126
+ and is removed in `finally`; it does not rewrite DSH Web's saved model choice.
127
+ Unsupported combinations fail closed. Older plugins and non-DeepSeek routes
128
+ retain the account route's fixed provider and model.
129
+
115
130
  An existing schema-v1 single-Project pairing keeps its device credential when
116
131
  the plugin is upgraded, but deliberately drops the old model binding. The user
117
132
  must choose the DSH model and explicitly select **Connect all accessible
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "name": "@gatherthread/dsh-host",
4
- "version": "0.1.0-alpha.5",
4
+ "version": "0.1.0-alpha.7",
5
5
  "license": "Apache-2.0",
6
6
  "defaultEnabled": false,
7
7
  "hostEntry": "@gatherthread/dsh-host",
package/client/client.js CHANGED
@@ -105,7 +105,9 @@ function GatherThreadStatusPanel({ connection, sessions }) {
105
105
  }
106
106
  onSuccess?.(next);
107
107
  } catch {
108
- setNotice(endpoint.startsWith("sync/")
108
+ setNotice(endpoint.startsWith("code/")
109
+ ? "代码同步未完成。请检查本机授权、项目权限和云端版本;不会强制覆盖文件。"
110
+ : endpoint.startsWith("sync/")
109
111
  ? "上传操作未完成。请检查连接和会话权限后重试。"
110
112
  : "操作未完成。请检查服务器地址、当前授权和模型配置。");
111
113
  } finally {
@@ -181,9 +183,86 @@ function GatherThreadStatusPanel({ connection, sessions }) {
181
183
  setProvider,
182
184
  setModel,
183
185
  runAction,
186
+ }),
187
+ nativeState === undefined || unavailable ? null : renderCodeSyncControls(nativeState, busy, runAction));
188
+ }
189
+
190
+ function renderCodeSyncControls(state, busy, runAction) {
191
+ if (!state.codeSync?.length) return null;
192
+ const actions = [
193
+ ["code_sync_status", "检查状态"],
194
+ ["code_upload", "上传代码"],
195
+ ["code_download", "下载更新"],
196
+ ["code_recover", "恢复到新目录"],
197
+ ];
198
+ return React.createElement("section", { style: styles.connectionCard, "aria-label": "项目代码同步" },
199
+ React.createElement("h3", { style: styles.sectionTitle }, "项目代码 · Git"),
200
+ React.createElement("p", { style: styles.muted },
201
+ "代码与会话上传独立。每位成员使用自己的云端分支;项目成员均可读取代码。上传前请检查源文件,不上传密钥或私人资料。"),
202
+ ...state.codeSync.map((project) => {
203
+ const invoke = (action) => void runAction("code/action", { projectId: project.projectId, action });
204
+ const status = project.status;
205
+ return React.createElement("details", { key: project.projectId, style: { ...styles.row, display: "block" } },
206
+ React.createElement("summary", { style: styles.sessionTitle }, project.projectName),
207
+ React.createElement("div", { style: styles.form },
208
+ React.createElement("label", { style: styles.syncToggle },
209
+ React.createElement("input", {
210
+ type: "checkbox", checked: project.authorized, disabled: busy,
211
+ onChange: (event) => void runAction("code/authorize", {
212
+ projectId: project.projectId, enabled: event.target.checked,
213
+ }),
214
+ }), "允许此 DSH 同步该项目代码"),
215
+ React.createElement("p", { style: styles.muted }, "仅访问此项目已经绑定的本地目录,不改动原 Git 分支或暂存区。下载要求本地没有未上传改动;恢复始终新建目录,不更换当前 Agent 工作目录。"),
216
+ status ? React.createElement("p", { style: styles.sessionMeta },
217
+ status.local_status_unknown ? "已恢复云端副本;原工作区状态未知,请单独检查。" : status.enabled
218
+ ? `${status.file_count} 个源文件 · ${status.local_changes} 项待上传 · ${status.excluded_count} 项已排除 · 云端 ${status.cloud_commit?.slice(0, 8) ?? "尚无版本"}`
219
+ : "请先由项目创建者在 GatherThread 的「项目代码」中启用 Git。") : null,
220
+ status?.needs_download ? React.createElement("p", { style: styles.notice }, "云端有新版本。请先下载;本地有改动时请恢复到新目录比较,不会自动覆盖。") : null,
221
+ project.authorized ? React.createElement("label", { style: styles.syncToggle },
222
+ React.createElement("input", {
223
+ type: "checkbox", checked: status?.automatic_upload === true,
224
+ disabled: busy || !status?.enabled,
225
+ onChange: (event) => invoke(event.target.checked ? "code_auto_upload_enable" : "code_auto_upload_disable"),
226
+ }), "空闲时自动上传本地代码至云端") : null,
227
+ React.createElement("div", { style: styles.syncActions }, ...actions.map(([action, label]) => (
228
+ React.createElement("button", {
229
+ key: action, type: "button", style: styles.compactButton,
230
+ disabled: busy || !project.authorized || (action !== "code_sync_status" && !status?.enabled),
231
+ onClick: () => {
232
+ if (action === "code_recover" && !window.confirm("将云端代码恢复到当前项目旁的新目录,原目录和会话保持不变。继续?")) return;
233
+ invoke(action);
234
+ },
235
+ }, label)
236
+ ))),
237
+ status?.recovery_directory ? React.createElement("p", { role: "status", style: styles.notice },
238
+ `已恢复至项目同级目录:${status.recovery_directory}。在本地 Agent 中打开该目录继续工作;原会话保持不变。`) : null,
239
+ project.error ? React.createElement("p", { role: "status", style: styles.notice }, codeSyncErrorLabel(project.error)) : null,
240
+ ));
184
241
  }));
185
242
  }
186
243
 
244
+ function codeSyncErrorLabel(code) {
245
+ const labels = {
246
+ code_sync_disabled: "请先允许此 DSH 同步项目代码。",
247
+ code_sync_busy: "Agent 正在工作,请等待本轮结束后再同步。",
248
+ code_workspace_busy: "Agent 正在工作,请等待本轮结束后再同步。",
249
+ code_sync_dirty: "本地有未上传修改。先上传,或恢复云端版本到新目录比较。",
250
+ code_sync_recovery_required: "本地目录缺失或上次下载中断,请先将云端副本恢复到新目录。",
251
+ code_sync_forbidden: "代码访问被拒绝,请检查该设备的登录状态与项目权限。",
252
+ code_sync_conflict: "云端版本已变化。请先安全下载或恢复到新目录比较,未覆盖任何云端版本。",
253
+ code_conflict: "云端版本冲突。请检查最新分支,保留本地修改后重试。",
254
+ code_stale_head: "云端已有新版本。请恢复到新目录比较,不要覆盖未上传的本地修改。",
255
+ code_merge_conflict: "分支存在冲突。请保留本地修改,比较云端版本并处理冲突。",
256
+ code_storage_quota_exceeded: "项目代码存储额度已满,请联系服务管理员。",
257
+ code_storage_check_required: "代码写入因存储检查暂时停止,请联系服务管理员;会话同步仍可使用。",
258
+ code_git_unavailable: "Git 不可用,请检查服务器与本机的 Git 安装。",
259
+ code_secret_detected: "检测到可能的密钥,已阻止上传。请检查项目文件。",
260
+ code_not_enabled: "请先由项目创建者在 GatherThread 启用项目 Git。",
261
+ code_sync_secret: "检测到可能的密钥,已阻止上传。请检查项目文件。",
262
+ };
263
+ return labels[code] ?? "代码同步未完成。请检查授权、文件和云端状态后重试;会话同步不受影响。";
264
+ }
265
+
187
266
  async function readHostState(connection, signal) {
188
267
  if (typeof connection?.rpc?.call === "function") {
189
268
  try {
@@ -302,6 +381,13 @@ function renderNativeControls(input) {
302
381
  const models = selectedProvider?.models ?? [];
303
382
  controls.push(
304
383
  connectionSummary(state),
384
+ // Reaching this branch means authorization is already "paired": the
385
+ // "unpaired" and "pairing" states are handled by earlier branches. The
386
+ // browser approval succeeded, but without a provider and model no runtime
387
+ // is registered, so a bare stopped connection is indistinguishable from a
388
+ // fresh install. State the remaining step explicitly.
389
+ React.createElement("p", { key: "select-model-notice", style: styles.notice },
390
+ "配对完成后,尚未选择 DSH Provider 与 Model。完成选择并点击下方按钮后,本机 DSH 运行时才会注册到 GatherThread;在此之前,GatherThread 网页无法发现这台 DSH。"),
305
391
  input.catalog === undefined
306
392
  ? React.createElement("button", {
307
393
  key: "load-catalog",
@@ -413,7 +499,7 @@ function detail(label, value) {
413
499
  function parseNativeState(value) {
414
500
  exactObject(value, [
415
501
  "schemaVersion", "integration", "authorization", "compatibility", "runtime",
416
- "officialServerUrl", "serverUrl", "deviceName", "route", "projectCount", "bindings", "localSync", "pairing", "recoverableError",
502
+ "officialServerUrl", "serverUrl", "deviceName", "route", "projectCount", "bindings", "localSync", "codeSync", "pairing", "recoverableError",
417
503
  ]);
418
504
  if (value.schemaVersion !== 2 || value.integration !== "gatherthread") throw new Error("invalid native status identity");
419
505
  if (!AUTHORIZATION_STATES.has(value.authorization)) throw new Error("invalid native authorization state");
@@ -437,6 +523,7 @@ function parseNativeState(value) {
437
523
  let route;
438
524
  let bindings;
439
525
  let localSync;
526
+ let codeSync;
440
527
  if (value.route !== undefined || value.bindings !== undefined || value.projectCount !== undefined) {
441
528
  if (value.route === undefined || !Number.isSafeInteger(value.projectCount) || value.projectCount < 0
442
529
  || !Array.isArray(value.bindings) || value.bindings.length > 100
@@ -477,6 +564,35 @@ function parseNativeState(value) {
477
564
  }
478
565
  return { ...sync };
479
566
  });
567
+ const codeSyncValue = value.codeSync ?? [];
568
+ if (!Array.isArray(codeSyncValue) || codeSyncValue.length > 100) throw new Error("invalid code sync status");
569
+ codeSync = codeSyncValue.map((project) => {
570
+ exactObject(project, ["projectId", "projectName", "authorized", "status", "error"]);
571
+ boundedText(project.projectId, 128);
572
+ boundedText(project.projectName, 160);
573
+ if (typeof project.authorized !== "boolean") throw new Error("invalid code sync authorization");
574
+ if (project.error !== undefined && !/^code_[a-z_]{1,60}$/u.test(project.error)) throw new Error("invalid code sync error");
575
+ if (project.status !== undefined) {
576
+ const status = project.status;
577
+ exactObject(status, ["enabled", "automatic_upload", "local_changes", "file_count", "excluded_count", "base_commit", "cloud_commit", "branch_id", "needs_download", "recovery_directory", "local_status_unknown"]);
578
+ if (status.local_status_unknown !== undefined && typeof status.local_status_unknown !== "boolean") throw new Error("invalid local status flag");
579
+ for (const key of ["enabled", "automatic_upload", "needs_download"]) {
580
+ if (typeof status[key] !== "boolean") throw new Error("invalid code sync flag");
581
+ }
582
+ for (const key of ["local_changes", "file_count", "excluded_count"]) {
583
+ if (!Number.isSafeInteger(status[key]) || status[key] < 0) throw new Error("invalid code sync count");
584
+ }
585
+ for (const key of ["base_commit", "cloud_commit"]) {
586
+ if (status[key] !== null && !/^[a-f0-9]{40}$/u.test(status[key])) throw new Error("invalid code commit");
587
+ }
588
+ if (status.branch_id !== null) boundedText(status.branch_id, 128);
589
+ if (status.recovery_directory !== undefined) {
590
+ boundedText(status.recovery_directory, 255);
591
+ if (/[\\/\u0000-\u001f]/u.test(status.recovery_directory)) throw new Error("invalid recovery directory");
592
+ }
593
+ }
594
+ return { ...project };
595
+ });
480
596
  }
481
597
  let pairing;
482
598
  if (value.pairing !== undefined) {
@@ -512,7 +628,7 @@ function parseNativeState(value) {
512
628
  ...(officialServerUrl === undefined ? {} : { officialServerUrl }),
513
629
  ...(serverUrl === undefined ? {} : { serverUrl }),
514
630
  ...(value.deviceName === undefined ? {} : { deviceName: value.deviceName }),
515
- ...(route === undefined ? {} : { route, projectCount: value.projectCount, bindings, localSync }),
631
+ ...(route === undefined ? {} : { route, projectCount: value.projectCount, bindings, localSync, codeSync }),
516
632
  ...(pairing === undefined ? {} : { pairing }),
517
633
  ...(value.recoverableError === undefined ? {} : { recoverableError: value.recoverableError }),
518
634
  };
package/cordis.patch.yml CHANGED
@@ -12,3 +12,21 @@
12
12
  inject: [agents, sessions, sessionPersistence, sessionQuery, sessionTitle, workspaceRegistry, llm, credentials, connection]
13
13
  config:
14
14
  enabled: true
15
+
16
+ # DSH 0.1.5-rc.x dropped `webServer` from the Connection plugin's own inject list
17
+ # (`["webServer", "credentials"]` became `["credentials"]`), moving it into a
18
+ # nested scope that covers only DSH's new index-injection feature. Connection
19
+ # still reads `owner.webServer` from its *providing* context -- cordis starts
20
+ # every service lookup at the shadow origin, `(ctx[symbols.shadow] ?? ctx).fiber`
21
+ # -- so `connection.rpc.handle()` raises "cannot get property \"webServer\"
22
+ # without inject" for every caller, and the whole plugin tree fails to load.
23
+ # 0.1.2-rc.1 kept the dependency at the top level, so restoring it here is what
24
+ # makes the Host plugin load on both versions.
25
+ #
26
+ # A row patch replaces `inject` wholesale rather than merging, so each entry the
27
+ # 0.1.5 row already declares is repeated: `webRuntime` still feeds this row's
28
+ # `trustedHosts: !!js ctx.webRuntime.trustedHosts` expression, and `credentials`
29
+ # is repeated so the row stays self-sufficient no matter how the Loader folds a
30
+ # row's list together with the module's own `inject` export.
31
+ - id: connection
32
+ inject: [webRuntime, credentials, webServer]