@modesto_ma/dsh-guanxiang 0.1.0-beta.1 → 0.1.0-beta.2

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
@@ -57,7 +57,7 @@
57
57
  父 Agent 按应用分节汇总,涉及多个应用时给出跨应用的推演
58
58
  ```
59
59
 
60
- - **一个入口。** 插件只提供用户斜杠入口 `/guanxiang`(模型不会自行加载)。dsh 在 Web、TUI、ACP 以及经 dsh_channels 的飞书消息中都能识别它。
60
+ - **一个入口,两种触发。** 入口 SKILL 是 `/guanxiang`,dsh 在 Web、TUI、ACP 以及经 dsh_channels 的飞书消息中都能识别它。有可用应用且 `autoInvoke` 开启(默认)时,模型也能自行加载:dsh 给顶层 Agent 的 skill 目录里,观象的描述会列出每个可用应用的名称、id、别名和日志特征(总长不超过 500 字,放不下的只计数),用户说「分析这份订单服务的日志」或贴出带 `OrderController` 的报错时,模型会先加载观象。日志明显不属于任何已注册应用时,入口 SKILL 让模型按普通方式处理,不派发也不追问。子 Agent 的工作目录在 `reposDir` 内,那里不列出观象。应用增删时 dsh 会追加一条新的 skill 目录消息;想要确定触发时仍用 `/guanxiang`。
61
61
  - **派发由代码完成。** 模型只给出应用 id、问题和日志位置;仓库目录、应用 SKILL、并发上限都由插件决定。应用 id 必须与注册表一一对应,否则整次派发被拒绝。
62
62
  - **日志一律以绝对路径交给子 Agent。** 附件和本机文件直接传路径;消息里的对象存储下载链接由派发工具下载(只允许白名单域名,不带鉴权,限制大小与耗时)。日志包不解压,由应用的日志分析 SKILL 在运行目录里解压。
63
63
  - **应用目录随时最新。** 每次加载 `/guanxiang` 都先 `git fetch` 各仓库(间隔由 `fetchIntervalMs` 控制,默认每次),再从远端分支最新提交的 git 对象读取应用描述生成应用目录。fetch 不改动任何检出,所以正在进行的分析不会挡住更新。最多等待 `refreshWaitMs`,超时就先用已有的目录,fetch 在后台继续。
@@ -93,6 +93,10 @@ npm 发行包已包含构建产物;本地 `link:` 安装前必须先执行 `pn
93
93
  - url: git@github.com:example/order-service.git
94
94
  - url: https://github.com/example/pay-service.git
95
95
  branch: release # 可选,默认跟随远端默认分支
96
+ gitCredentials: # https 仓库的访问令牌,只写令牌名称,不写令牌值
97
+ - url: https://gitlab.example.com # 地址前缀;其下的 https 仓库使用该令牌
98
+ username: oauth2 # 可选,默认 oauth2
99
+ tokenEnv: GITLAB_TOKEN # DSH 凭据库中的令牌名称,也可以是同名环境变量
96
100
  fetchIntervalMs: 0 # 加载 /guanxiang 时距上次拉取超过该时间的仓库先 fetch;0 = 每次
97
101
  maxAppsPerRequest: 4 # 单次派发的任务上限
98
102
  dispatchTimeoutMs: 1800000 # 等待全部子 Agent 的上限
@@ -103,9 +107,21 @@ npm 发行包已包含构建产物;本地 `link:` 安装前必须先执行 `pn
103
107
  subagentProvider: spawn # 子 Agent 使用的 ctx.subagents provider
104
108
  gitTimeoutMs: 600000 # 单条 git 命令的超时
105
109
  refreshWaitMs: 15000 # 加载 /guanxiang 时等待 fetch 的上限;0 = 不等待
110
+ autoInvoke: true # 任务匹配已注册应用时模型可自行加载入口 SKILL;false = 只能输入 /guanxiang
106
111
  ```
107
112
 
108
- 配置在加载时校验,不合法时插件加载失败。git 地址支持 https/http/ssh/git/file URL、`user@host:path` 和绝对路径,拉取时使用本机已有的 git 凭据(SSH key 或 credential helper),插件不保存密码或 token,也不会弹出交互式输入。
113
+ 配置在加载时校验,不合法时插件加载失败。git 地址支持 https/http/ssh/git/file URL、`user@host:path` 和绝对路径,拉取时使用本机已有的 git 凭据(SSH key 或 credential helper),不会弹出交互式输入。
114
+
115
+ ### 访问令牌
116
+
117
+ 用 https 地址拉取 GitLab 等私有仓库时,可以配置个人访问令牌(GitLab 需要 `read_repository` 权限):
118
+
119
+ - **配置只写名称。** `gitCredentials` 的每一项把一个地址前缀(`https://主机[:端口][/路径]`)对应到一个令牌名称 `tokenEnv`;地址在前缀下的 https 仓库使用该令牌,多个前缀匹配时取路径最长的。ssh、`user@host:path` 和本地路径不受影响。
120
+ - **令牌值在 DSH 凭据库。** 令牌通过 DSH 的 `ctx.credentials` 读取(默认组合加载的 `@deepseek-ai/dsh-credentials-local`,文件 `$DSH_HOME/.credentials.yaml`,仅本机用户可读),可以在设置面板的「访问令牌」分页保存,也可以设置同名环境变量或写进 `.env`。每次拉取都重新读取,更换令牌不用重启。面板接口只接受已保存的配置引用的名称,也从不返回令牌值。
121
+ - **只交给 git。** 拉取时令牌经环境变量交给这一次的 git 进程,由只对该主机生效的凭据助手在服务器要求认证时提供,本机为该主机配置的其他凭据助手不参与;令牌不出现在命令行参数、`.git/config`、日志或模型上下文里。需要 git 2.31 及以上版本。
122
+ - **缺失时明确报错。** 地址匹配了令牌配置但令牌未设置,或 DSH 组合没有凭据服务时,该仓库拉取失败,原因显示在设置面板。
123
+
124
+ 凭据库文件只防其他系统用户。与 DSH 中的模型 API Key 一样,Agent 的命令以同一系统用户运行,文件权限不能把令牌与 Agent 隔离(见 `@deepseek-ai/dsh-credentials-local` 文档);建议使用只读权限、设置过期时间的令牌。
109
125
 
110
126
  子 Agent 的命令在 dsh 的文件沙箱中运行:仓库只读,只有父会话的项目目录可写,运行目录就在其中。Linux 的 bubblewrap 沙箱会把 `/tmp` 换成空的临时目录,所以 `reposDir` 不要放在 `/tmp` 下,否则子 Agent 的命令看不到仓库。
111
127
 
@@ -118,10 +134,11 @@ DSH Web 左侧栏的「观象」入口打开整页设置面板:
118
134
  | 分页 | 内容 |
119
135
  |---|---|
120
136
  | 应用仓库 | git 地址列表(分支可选)、每个仓库的拉取状态、提交号、应用描述与不可用原因,单个或全部重新拉取 |
137
+ | 访问令牌 | https 地址前缀、用户名与令牌名称;保存后填入令牌,显示是否已设置及来源,可清除 |
121
138
  | 日志下载 | 允许下载的域名、单文件大小上限、下载超时 |
122
139
  | 高级 | 拉取目录、更新间隔、单次应用数上限、派发超时,恢复 cordis.yml 配置 |
123
140
 
124
- 面板保存的值写入 `$DSH_HOME/guanxiang/settings.json`(只保存与 cordis.yml 不同的字段),按与配置相同的规则校验。面板接口 `/guanxiang/*` 使用 DSH 自己的登录与来源校验,写操作另做同源校验。
141
+ 面板保存的值写入 `$DSH_HOME/guanxiang/settings.json`(只保存与 cordis.yml 不同的字段),按与配置相同的规则校验;令牌值不在其中,存入 DSH 凭据库。面板接口 `/guanxiang/*` 使用 DSH 自己的登录与来源校验,写操作另做同源校验。
125
142
 
126
143
  ## 应用仓库约定
127
144
 
@@ -168,7 +185,7 @@ pnpm check # 类型检查(Host 与 Client)+ 单元测试 + 构建 +
168
185
  |---|---|
169
186
  | `src/index.ts` | Cordis 插件入口 |
170
187
  | `src/service.ts` | 生效设置、注册表、入口 SKILL 与派发工具的注册 |
171
- | `src/repos/` | git 拉取、应用描述校验、应用注册表 |
188
+ | `src/repos/` | git 拉取、访问令牌、应用描述校验、应用注册表 |
172
189
  | `src/skill/entry.ts` | `/guanxiang` 入口 SKILL |
173
190
  | `src/dispatch/` | 派发工具、日志下载、子 Agent 首条消息 |
174
191
  | `src/settings/` | 面板契约、覆盖文件、HTTP 路由 |
package/lib/client.js CHANGED
@@ -48,6 +48,8 @@ window.__ModuleLoader__.load({
48
48
  const SETTINGS_ROUTE = "/guanxiang/settings";
49
49
  /** POST:立即重新拉取全部仓库,或 body `{ url }` 指定的一个仓库。 */
50
50
  const SYNC_ROUTE = "/guanxiang/sync";
51
+ /** PUT:body `{ tokenEnv, value }` 把令牌存入 DSH 凭据库;DELETE:body `{ tokenEnv }` 删除已存的值。只接受已保存的访问令牌配置引用的名称。 */
52
+ const TOKEN_ROUTE = "/guanxiang/token";
51
53
  //#endregion
52
54
  //#region src/client/api.ts
53
55
  /**
@@ -126,11 +128,38 @@ window.__ModuleLoader__.load({
126
128
  body: JSON.stringify(url === void 0 ? {} : { url })
127
129
  })).state;
128
130
  }
131
+ /**
132
+ * Store an access token in DSH's credential store.
133
+ * @param tokenEnv - credential reference named by a saved access-token configuration.
134
+ * @param value - token value.
135
+ * @returns the new state, which reports only whether the token is set.
136
+ */
137
+ async function saveToken(tokenEnv, value) {
138
+ return (await request(TOKEN_ROUTE, {
139
+ method: "PUT",
140
+ body: JSON.stringify({
141
+ tokenEnv,
142
+ value
143
+ })
144
+ })).state;
145
+ }
146
+ /**
147
+ * Remove a stored access token.
148
+ * @param tokenEnv - credential reference named by a saved access-token configuration.
149
+ * @returns the new state.
150
+ */
151
+ async function clearToken(tokenEnv) {
152
+ return (await request(TOKEN_ROUTE, {
153
+ method: "DELETE",
154
+ body: JSON.stringify({ tokenEnv })
155
+ })).state;
156
+ }
129
157
  //#endregion
130
158
  //#region src/client/page.tsx
131
159
  /**
132
- * 左侧栏「观象」主区页面:应用仓库、日志下载、高级三个分页,共用一份草稿和
133
- * 底部保存条。仓库拉取中时每 2 秒刷新状态。
160
+ * 左侧栏「观象」主区页面:应用仓库、访问令牌、日志下载、高级四个分页,共用
161
+ * 一份草稿和底部保存条;令牌值不进草稿,单独存入 DSH 凭据库。仓库拉取中时
162
+ * 每 2 秒刷新状态。
134
163
  *
135
164
  * @module dsh-guanxiang/client/page
136
165
  */
@@ -143,6 +172,10 @@ window.__ModuleLoader__.load({
143
172
  id: "repos",
144
173
  label: "应用仓库"
145
174
  },
175
+ {
176
+ id: "tokens",
177
+ label: "访问令牌"
178
+ },
146
179
  {
147
180
  id: "download",
148
181
  label: "日志下载"
@@ -159,6 +192,7 @@ window.__ModuleLoader__.load({
159
192
  return {
160
193
  reposDir: settings.reposDir,
161
194
  repos: settings.repos.map((repo) => ({ ...repo })),
195
+ gitCredentials: settings.gitCredentials.map((credential) => ({ ...credential })),
162
196
  fetchIntervalMin: trimNumber(settings.fetchIntervalMs / MINUTE),
163
197
  maxAppsPerRequest: String(settings.maxAppsPerRequest),
164
198
  dispatchTimeoutMin: trimNumber(settings.dispatchTimeoutMs / MINUTE),
@@ -181,6 +215,11 @@ window.__ModuleLoader__.load({
181
215
  url: repo.url.trim(),
182
216
  branch: repo.branch.trim()
183
217
  }),
218
+ gitCredentials: draft.gitCredentials.map((credential) => ({
219
+ url: credential.url.trim(),
220
+ username: credential.username.trim(),
221
+ tokenEnv: credential.tokenEnv.trim()
222
+ })),
184
223
  fetchIntervalMs: Math.round(positive(draft.fetchIntervalMin, "更新间隔", true) * MINUTE),
185
224
  maxAppsPerRequest: maxApps,
186
225
  dispatchTimeoutMs: Math.round(positive(draft.dispatchTimeoutMin, "派发超时") * MINUTE),
@@ -291,11 +330,13 @@ window.__ModuleLoader__.load({
291
330
  kind: "ok",
292
331
  text: success
293
332
  });
333
+ return true;
294
334
  } catch (error) {
295
335
  setNotice({
296
336
  kind: "error",
297
337
  text: messageOf(error)
298
338
  });
339
+ return false;
299
340
  } finally {
300
341
  setBusy(false);
301
342
  }
@@ -404,6 +445,14 @@ window.__ModuleLoader__.load({
404
445
  run(() => syncRepos(url), url === void 0 ? "已开始重新拉取全部仓库" : "已开始重新拉取", false);
405
446
  }
406
447
  }) : null,
448
+ tab === "tokens" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TokensTab, {
449
+ draft,
450
+ setDraft,
451
+ state,
452
+ busy,
453
+ onSave: (tokenEnv, value) => run(() => saveToken(tokenEnv, value), `已保存 ${tokenEnv},使用它的仓库会重新拉取`, false),
454
+ onClear: (tokenEnv) => run(() => clearToken(tokenEnv), `已清除面板保存的 ${tokenEnv}`, false)
455
+ }) : null,
407
456
  tab === "download" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DownloadTab, {
408
457
  draft,
409
458
  setDraft,
@@ -617,7 +666,7 @@ window.__ModuleLoader__.load({
617
666
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
618
667
  className: "gx__secFoot",
619
668
  children: [
620
- "使用本机已有的 git 凭据(SSH key 或 credential helper)拉取,不留分支时跟随远端默认分支。每个仓库需包含 ",
669
+ "使用本机已有的 git 凭据(SSH key 或 credential helper)拉取;https 地址可在「访问令牌」中配置个人访问令牌。不留分支时跟随远端默认分支。每个仓库需包含 ",
621
670
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
622
671
  className: "gx__mono",
623
672
  children: ".dsh/guanxiang.yaml"
@@ -637,6 +686,232 @@ window.__ModuleLoader__.load({
637
686
  ]
638
687
  });
639
688
  }
689
+ const DEFAULT_USERNAME = "oauth2";
690
+ const DEFAULT_TOKEN_ENV = "GITLAB_TOKEN";
691
+ /** Credential store layers of `@deepseek-ai/dsh-credentials-local` other than its own file. */
692
+ const TOKEN_SOURCES = {
693
+ "env": "启动环境变量",
694
+ "project-env": "项目 .env",
695
+ "user-env": "$DSH_HOME/.env"
696
+ };
697
+ function tokenPill(token) {
698
+ if (token === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
699
+ className: "gx__pill",
700
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "gx__dot" }), "保存后设置令牌"]
701
+ });
702
+ if (token.error !== void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
703
+ className: "gx__pill gx__pill--bad",
704
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "gx__dot" }), "无法读取"]
705
+ });
706
+ if (!token.configured) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
707
+ className: "gx__pill gx__pill--warn",
708
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "gx__dot" }), "未设置令牌"]
709
+ });
710
+ const source = token.source === void 0 || token.source === "file" ? void 0 : TOKEN_SOURCES[token.source] ?? token.source;
711
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
712
+ className: "gx__pill gx__pill--ok",
713
+ children: [
714
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "gx__dot" }),
715
+ "已设置",
716
+ source === void 0 ? "" : `(来自${source})`
717
+ ]
718
+ });
719
+ }
720
+ function TokensTab({ draft, setDraft, state, busy, onSave, onClear }) {
721
+ const [url, setUrl] = (0, react.useState)("");
722
+ const [username, setUsername] = (0, react.useState)(DEFAULT_USERNAME);
723
+ const [tokenEnv, setTokenEnv] = (0, react.useState)(DEFAULT_TOKEN_ENV);
724
+ const [values, setValues] = (0, react.useState)({});
725
+ const tokens = (0, react.useMemo)(() => new Map(state.credentials.tokens.map((token) => [token.tokenEnv, token])), [state.credentials.tokens]);
726
+ const saved = (0, react.useMemo)(() => new Set(state.settings.gitCredentials.map((credential) => credential.tokenEnv)), [state.settings.gitCredentials]);
727
+ const add = () => {
728
+ const trimmed = url.trim();
729
+ if (trimmed === "" || draft.gitCredentials.some((credential) => credential.url === trimmed)) return;
730
+ setDraft({
731
+ ...draft,
732
+ gitCredentials: [...draft.gitCredentials, {
733
+ url: trimmed,
734
+ username: username.trim() === "" ? DEFAULT_USERNAME : username.trim(),
735
+ tokenEnv: tokenEnv.trim() === "" ? DEFAULT_TOKEN_ENV : tokenEnv.trim()
736
+ }]
737
+ });
738
+ setUrl("");
739
+ };
740
+ const save = (name) => {
741
+ const value = values[name]?.trim() ?? "";
742
+ if (value === "") return;
743
+ onSave(name, value).then((ok) => {
744
+ if (ok) setValues((current) => ({
745
+ ...current,
746
+ [name]: ""
747
+ }));
748
+ });
749
+ };
750
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
751
+ className: "gx__sec",
752
+ children: [
753
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h2", {
754
+ className: "gx__secTitle",
755
+ children: ["HTTPS 访问令牌 ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Overridden, {
756
+ keys: state.overridden,
757
+ name: "gitCredentials"
758
+ })]
759
+ }),
760
+ state.credentials.available ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
761
+ className: "gx__notice gx__notice--error",
762
+ children: "当前 DSH 组合没有加载凭据服务(@deepseek-ai/dsh-credentials-local),无法保存或读取令牌。"
763
+ }),
764
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
765
+ className: "gx__list",
766
+ children: [
767
+ draft.gitCredentials.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
768
+ className: "gx__emptyRow",
769
+ children: "还没有访问令牌。用 https 地址拉取 GitLab 等私有仓库时,添加服务器地址后保存,再填入个人访问令牌。"
770
+ }) : null,
771
+ draft.gitCredentials.map((credential, index) => {
772
+ const token = saved.has(credential.tokenEnv) ? tokens.get(credential.tokenEnv) : void 0;
773
+ const value = values[credential.tokenEnv] ?? "";
774
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
775
+ className: "gx__row gx__row--top",
776
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
777
+ className: "gx__rowText",
778
+ children: [
779
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
780
+ className: "gx__rowLabel",
781
+ children: [
782
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
783
+ className: "gx__mono",
784
+ children: credential.url
785
+ }),
786
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
787
+ className: "gx__tag",
788
+ children: ["用户名 ", credential.username]
789
+ }),
790
+ tokenPill(token)
791
+ ]
792
+ }),
793
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
794
+ className: "gx__rowSummary",
795
+ children: ["令牌名称 ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
796
+ className: "gx__mono",
797
+ children: credential.tokenEnv
798
+ })]
799
+ }),
800
+ token?.error === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
801
+ className: "gx__rowError",
802
+ children: token.error
803
+ }),
804
+ token !== void 0 && token.writable ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
805
+ className: "gx__addRow gx__addRow--flush",
806
+ children: [
807
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
808
+ className: "gx__input gx__mono",
809
+ type: "password",
810
+ autoComplete: "new-password",
811
+ spellCheck: false,
812
+ placeholder: token.configured ? "输入新令牌以替换" : "粘贴个人访问令牌,例如 glpat-…",
813
+ value,
814
+ onChange: (event) => {
815
+ setValues({
816
+ ...values,
817
+ [credential.tokenEnv]: event.target.value
818
+ });
819
+ },
820
+ onKeyDown: (event) => {
821
+ if (event.key === "Enter") save(credential.tokenEnv);
822
+ }
823
+ }),
824
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
825
+ type: "button",
826
+ className: "gx__btn",
827
+ disabled: busy || value.trim() === "",
828
+ onClick: () => {
829
+ save(credential.tokenEnv);
830
+ },
831
+ children: "保存令牌"
832
+ }),
833
+ token.configured && token.source === "file" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
834
+ type: "button",
835
+ className: "gx__btn gx__btn--ghost",
836
+ disabled: busy,
837
+ onClick: () => {
838
+ onClear(credential.tokenEnv);
839
+ },
840
+ children: "清除"
841
+ }) : null
842
+ ]
843
+ }) : null
844
+ ]
845
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
846
+ className: "gx__rowCtl",
847
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
848
+ type: "button",
849
+ className: "gx__btn gx__btn--danger",
850
+ onClick: () => {
851
+ setDraft({
852
+ ...draft,
853
+ gitCredentials: draft.gitCredentials.filter((_, position) => position !== index)
854
+ });
855
+ },
856
+ children: "删除"
857
+ })
858
+ })]
859
+ }, credential.url);
860
+ }),
861
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
862
+ className: "gx__addRow",
863
+ children: [
864
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
865
+ className: "gx__input",
866
+ placeholder: "服务器地址,例如 https://gitlab.example.com",
867
+ value: url,
868
+ onChange: (event) => {
869
+ setUrl(event.target.value);
870
+ },
871
+ onKeyDown: (event) => {
872
+ if (event.key === "Enter") add();
873
+ }
874
+ }),
875
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
876
+ className: "gx__input gx__input--branch",
877
+ placeholder: `用户名(默认 ${DEFAULT_USERNAME})`,
878
+ value: username,
879
+ onChange: (event) => {
880
+ setUsername(event.target.value);
881
+ },
882
+ onKeyDown: (event) => {
883
+ if (event.key === "Enter") add();
884
+ }
885
+ }),
886
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
887
+ className: "gx__input gx__input--branch gx__mono",
888
+ placeholder: "令牌名称",
889
+ value: tokenEnv,
890
+ onChange: (event) => {
891
+ setTokenEnv(event.target.value);
892
+ },
893
+ onKeyDown: (event) => {
894
+ if (event.key === "Enter") add();
895
+ }
896
+ }),
897
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
898
+ type: "button",
899
+ className: "gx__btn",
900
+ disabled: url.trim() === "",
901
+ onClick: add,
902
+ children: "添加"
903
+ })
904
+ ]
905
+ })
906
+ ]
907
+ }),
908
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
909
+ className: "gx__secFoot",
910
+ children: "地址在这个前缀下的 https 仓库使用对应令牌拉取(多个匹配时取路径最长的),ssh 地址不受影响。GitLab 个人访问令牌需要 read_repository 权限,用户名填 oauth2 即可。令牌保存在 DSH 凭据库(默认 $DSH_HOME/.credentials.yaml,仅本机用户可读),面板只显示是否已设置;也可以设置与令牌名称同名的环境变量。拉取时令牌只经环境变量交给 git,不写入仓库配置、日志或模型上下文。"
911
+ })
912
+ ]
913
+ });
914
+ }
640
915
  function DownloadTab({ draft, setDraft, overridden }) {
641
916
  const [host, setHost] = (0, react.useState)("");
642
917
  const add = () => {
@@ -946,6 +1221,7 @@ window.__ModuleLoader__.load({
946
1221
  .gx__addRow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:10px 14px}
947
1222
  .gx__addRow .gx__input{flex:1 1 160px;width:auto}
948
1223
  .gx__addRow .gx__input--branch{flex:0 1 140px}
1224
+ .gx__addRow--flush{padding:6px 0 0}
949
1225
  .gx__emptyRow{padding:11px 14px;color:var(--gx-fg3);font-size:12px}
950
1226
  .gx__btn{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:4px;height:28px;padding:0 12px;border:1px solid var(--gx-line);border-radius:7px;background:transparent;color:inherit;font:inherit;font-size:13px;cursor:pointer;white-space:nowrap}
951
1227
  .gx__btn:hover:not(:disabled){background:var(--gx-hover)}