dsh-archify-projects 0.1.1 → 0.2.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/README.md CHANGED
@@ -98,6 +98,8 @@ pnpm add link:/absolute/path/to/dsh-archify-projects # 或在 package.json 里
98
98
  3. 首次添加会自动跑一次 `create`:结构图 + 领域图各一张,通常几分钟。
99
99
  4. 之后随时「更新」:切回记录的分支 → 快进到最新 → 重画两张图。
100
100
  5. 图是可以直接看的 HTML(archify 产物:可搜索、可缩放、可导出),不是截图。
101
+ 6. 右上角**齿轮**是设置:只有一个「分析模型」下拉,选项就是你在 DSH 里已经配好、
102
+ 当前真能路由的模型;不选=跟随宿主默认。改完对下一次运行生效。
101
103
 
102
104
  ---
103
105
 
@@ -126,6 +128,16 @@ pnpm add link:/absolute/path/to/dsh-archify-projects # 或在 package.json 里
126
128
  `restrict` 只能装在 scoped ctx(`agent.ctx`)上,写在 preset 行里会直接抛 ——
127
129
  所以白名单与守卫在 `src/host/policy.ts` 里,运行开始时对当次会话安装。
128
130
 
131
+ ### 分析跑在子代理会话里,不占你的会话列表
132
+
133
+ 每次出图 / 提问都用一个 **`origin: 'subagent'` 的会话**(`ctx.agents.create` +
134
+ `agentPresets.mount`),所以:
135
+
136
+ - DSH 左侧会话列表按 origin 过滤子代理会话 —— 分析不会往你的对话列表里塞新条目;
137
+ - 会话的模型走 `agentOptions`,**只影响这一次运行**,不会改掉 DSH 里你的默认模型;
138
+ - 宿主版本没有这条路(缺 `agents.create` / `agentPresets`)时安静退回普通会话,
139
+ 工具白名单、preset、产物回收都不变。
140
+
129
141
  ### 深浅色
130
142
 
131
143
  页面只用 DSH 的主题令牌(`--dsw-alias-*` / `--dsw-specific-*` / `--dsw-radius-*` /
@@ -217,6 +229,7 @@ scripts/ artifact 门禁
217
229
  | 位置 | 内容 |
218
230
  | ------------------------------------------------------------ | --------------------------------------------------------------------------------------- |
219
231
  | `~/.dsh/archify-projects/projects.json`(可用 `dataDir` 改) | 台账:项目、Git 快照、两张图的状态、运行历史(保留最近 20 条) |
232
+ | 同目录 `settings.json` | 插件设置:分析用哪个模型(`{ provider, model }`,null = 跟随宿主默认) |
220
233
  | `~/.dsh/archify-projects/workspace/<projectId>/` | 每个项目的工作目录;`.archify/<kind>-<slug>-<stamp>/` 里是 `candidate.json` 与成品 HTML |
221
234
  | `<插件目录>/skills/archify/` | 内嵌的 archify skill(不进 git:见 `.gitignore`;npm 包用 `files` 带上) |
222
235
 
package/dist/client.js CHANGED
@@ -28,6 +28,8 @@ const ROUTES = {
28
28
  mergeQa: (id, qaId) => `${ROUTE_PREFIX}/projects/${encodeURIComponent(id)}/qa/${encodeURIComponent(qaId)}/merge`,
29
29
  childQa: (id, qaId) => `${ROUTE_PREFIX}/projects/${encodeURIComponent(id)}/qa/${encodeURIComponent(qaId)}/child`,
30
30
  runs: (id) => `${ROUTE_PREFIX}/projects/${encodeURIComponent(id)}/runs`,
31
+ /** 插件级设置(目前只有「分析用哪个模型」)。 */
32
+ settings: `${ROUTE_PREFIX}/settings`,
31
33
  artifact: (relativePath) => `${ROUTE_PREFIX}/artifact/${relativePath.split(/[\\/]+/).map((part) => encodeURIComponent(part)).join("/")}`
32
34
  };
33
35
 
@@ -163,7 +165,7 @@ async function api(path, init) {
163
165
  const message = result.error?.message ?? "未知错误";
164
166
  throw new ApiError(code, message);
165
167
  }
166
- /** POST/PATCH 的便捷封装。 */
168
+ /** POST/PUT/PATCH/DELETE 的便捷封装。 */
167
169
  function send(path, method, body) {
168
170
  return api(path, {
169
171
  method,
@@ -171,6 +173,12 @@ function send(path, method, body) {
171
173
  });
172
174
  }
173
175
 
176
+ // src/shared/model-key.ts
177
+ /** 生成路由键。 */
178
+ function modelKey(ref) {
179
+ return `${ref.provider}::${ref.model}`;
180
+ }
181
+
174
182
  // src/client/components.tsx
175
183
  /**
176
184
  * 页面里可复用的小组件:标签、状态点、两个对话框。
@@ -531,6 +539,136 @@ function NewDiagramDialog({ t, project, selectedId, busy, onClose, onSubmit }) {
531
539
  })
532
540
  });
533
541
  }
542
+ /** 齿轮图标(设置入口)。尺寸跟随字号,颜色跟随按钮。 */
543
+ function GearIcon({ size = 13 }) {
544
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
545
+ width: size,
546
+ height: size,
547
+ viewBox: "0 0 24 24",
548
+ fill: "none",
549
+ stroke: "currentColor",
550
+ strokeWidth: 1.7,
551
+ strokeLinecap: "round",
552
+ strokeLinejoin: "round",
553
+ "aria-hidden": "true",
554
+ focusable: "false",
555
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
556
+ cx: "12",
557
+ cy: "12",
558
+ r: "3.1"
559
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M19.4 15a1.6 1.6 0 0 0 .32 1.77l.06.06a1.9 1.9 0 1 1-2.7 2.7l-.06-.06a1.6 1.6 0 0 0-1.77-.32 1.6 1.6 0 0 0-.97 1.47V21a1.9 1.9 0 1 1-3.8 0v-.1a1.6 1.6 0 0 0-1.05-1.47 1.6 1.6 0 0 0-1.77.32l-.06.06a1.9 1.9 0 1 1-2.7-2.7l.06-.06A1.6 1.6 0 0 0 4.6 15a1.6 1.6 0 0 0-1.47-.97H3a1.9 1.9 0 1 1 0-3.8h.1a1.6 1.6 0 0 0 1.47-1.05 1.6 1.6 0 0 0-.32-1.77l-.06-.06a1.9 1.9 0 1 1 2.7-2.7l.06.06A1.6 1.6 0 0 0 9 4.6a1.6 1.6 0 0 0 .97-1.47V3a1.9 1.9 0 1 1 3.8 0v.1a1.6 1.6 0 0 0 .97 1.47 1.6 1.6 0 0 0 1.77-.32l.06-.06a1.9 1.9 0 1 1 2.7 2.7l-.06.06a1.6 1.6 0 0 0-.32 1.77V9a1.6 1.6 0 0 0 1.47.97H21a1.9 1.9 0 1 1 0 3.8h-.1a1.6 1.6 0 0 0-1.47.97z" })]
560
+ });
561
+ }
562
+ /**
563
+ * 设置对话框:**只有一个字段**——分析用哪个模型。
564
+ *
565
+ * 模型选项不是插件编的,是宿主 `modelCatalog()` 列出来的(用户在 DSH 里
566
+ * 配好、当前真的能路由到的那些)。目录拿不到时只留「跟随宿主默认」一项,
567
+ * 并且把原因写在提示里,不假装能选。
568
+ */
569
+ function SettingsDialog({ t, settings, busy, onClose, onSubmit }) {
570
+ const catalog = settings?.catalog ?? null;
571
+ const current = settings?.model ?? null;
572
+ const [chosen, setChosen] = (0, react.useState)(current === null ? "" : modelKey(current));
573
+ const options = catalog?.options ?? [];
574
+ const selectedOption = options.find((option) => option.key === chosen) ?? null;
575
+ const submit = () => {
576
+ onSubmit(selectedOption === null ? null : {
577
+ provider: selectedOption.provider,
578
+ model: selectedOption.model
579
+ });
580
+ };
581
+ const fallbackText = catalog !== null && catalog.fallback !== null ? `${catalog.fallback.provider} / ${catalog.fallback.model}` : null;
582
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
583
+ className: "dap-mask",
584
+ role: "presentation",
585
+ onClick: (e) => e.target === e.currentTarget && onClose(),
586
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
587
+ className: "dap-dialog",
588
+ role: "dialog",
589
+ "aria-modal": "true",
590
+ "aria-label": t("dialog.settings"),
591
+ children: [
592
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
593
+ className: "dap-dialog__title",
594
+ children: t("dialog.settings")
595
+ }),
596
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
597
+ className: "dap-dialog__sub",
598
+ children: t("dialog.settingsSub")
599
+ }),
600
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
601
+ className: "dap-dialog__row",
602
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
603
+ className: "dap-field__label",
604
+ children: t("field.model")
605
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
606
+ className: "dap-select",
607
+ value: chosen,
608
+ onChange: (e) => setChosen(e.target.value),
609
+ "aria-label": t("field.model"),
610
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
611
+ value: "",
612
+ children: fallbackText === null ? t("model.followDefault") : t("model.followDefaultNamed", { model: fallbackText })
613
+ }), options.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
614
+ value: option.key,
615
+ children: [
616
+ option.providerName,
617
+ " · ",
618
+ option.name
619
+ ]
620
+ }, option.key))]
621
+ })]
622
+ }),
623
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
624
+ className: "dap-hint",
625
+ children: catalog === null || catalog.source === "unavailable" ? t("settings.unavailable") : options.length === 0 ? t("settings.noModels") : t("field.modelHint")
626
+ }),
627
+ selectedOption?.description != null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
628
+ className: "dap-hint",
629
+ children: selectedOption.description
630
+ }) : null,
631
+ catalog !== null && catalog.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
632
+ className: "dap-hint dap-hint--warn",
633
+ children: t("settings.providerFailed", { list: catalog.failures.map((f) => f.providerName).join("、") })
634
+ }) : null,
635
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
636
+ className: "dap-dialog__row",
637
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
638
+ className: "dap-field__label",
639
+ children: t("field.sessionMode")
640
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
641
+ className: "dap-hint",
642
+ children: settings?.subagentSessions === true ? t("settings.sessionSubagent") : t("settings.sessionPlain")
643
+ })]
644
+ }),
645
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
646
+ className: "dap-dialog__foot",
647
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
648
+ className: "dap-hint",
649
+ children: t("settings.scopeHint")
650
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
651
+ style: {
652
+ display: "flex",
653
+ gap: 6
654
+ },
655
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
656
+ className: "dap-btn dap-btn--sm",
657
+ onClick: onClose,
658
+ disabled: busy,
659
+ children: t("action.cancel")
660
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
661
+ className: "dap-btn dap-btn--sm dap-btn--primary",
662
+ onClick: submit,
663
+ disabled: busy,
664
+ children: t("action.save")
665
+ })]
666
+ })]
667
+ })
668
+ ]
669
+ })
670
+ });
671
+ }
534
672
 
535
673
  // src/client/page.tsx
536
674
  /**
@@ -557,6 +695,7 @@ function StudioPage({ t }) {
557
695
  const [diagramId, setDiagramId] = (0, react.useState)(null);
558
696
  const [tab, setTab] = (0, react.useState)("info");
559
697
  const [dialog, setDialog] = (0, react.useState)(null);
698
+ const [settings, setSettings] = (0, react.useState)(null);
560
699
  const [busy, setBusy] = (0, react.useState)(null);
561
700
  const [notice, setNotice] = (0, react.useState)(null);
562
701
  const [question, setQuestion] = (0, react.useState)("");
@@ -590,6 +729,18 @@ function StudioPage({ t }) {
590
729
  (0, react.useEffect)(() => {
591
730
  load();
592
731
  }, [load]);
732
+ (0, react.useEffect)(() => {
733
+ let alive = true;
734
+ (async () => {
735
+ try {
736
+ const value = await api(ROUTES.settings);
737
+ if (alive) setSettings(value);
738
+ } catch {}
739
+ })();
740
+ return () => {
741
+ alive = false;
742
+ };
743
+ }, []);
593
744
  const running = state.projects.some((project) => isRunning(project));
594
745
  (0, react.useEffect)(() => {
595
746
  if (!running) return void 0;
@@ -634,6 +785,19 @@ function StudioPage({ t }) {
634
785
  setBusy(null);
635
786
  }
636
787
  };
788
+ const saveSettings = async (model) => {
789
+ setBusy("settings");
790
+ setNotice(null);
791
+ try {
792
+ const value = await send(ROUTES.settings, "PUT", { model });
793
+ if (mounted.current) setSettings(value);
794
+ setDialog(null);
795
+ } catch (err) {
796
+ fail(err);
797
+ } finally {
798
+ setBusy(null);
799
+ }
800
+ };
637
801
  const updateAll = async () => {
638
802
  for (const item of state.projects) {
639
803
  if (isRunning(item)) continue;
@@ -783,14 +947,20 @@ function StudioPage({ t }) {
783
947
  className: "dap-head__sub",
784
948
  children: project.repoPath
785
949
  })
786
- ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
950
+ ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
787
951
  className: "dap-head__actions",
788
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
952
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
953
+ className: "dap-iconbtn",
954
+ onClick: () => setDialog("settings"),
955
+ title: t("action.settings"),
956
+ "aria-label": t("action.settings"),
957
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GearIcon, {})
958
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
789
959
  className: "dap-btn dap-btn--sm dap-btn--primary",
790
960
  onClick: () => void runUpdate(project),
791
961
  disabled: busy !== null || isRunning(project),
792
962
  children: t("action.update")
793
- })
963
+ })]
794
964
  })]
795
965
  }),
796
966
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -818,6 +988,13 @@ function StudioPage({ t }) {
818
988
  onNewDiagram: () => setDialog("diagram")
819
989
  })]
820
990
  }),
991
+ dialog === "settings" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsDialog, {
992
+ t,
993
+ settings,
994
+ busy: busy !== null,
995
+ onClose: () => setDialog(null),
996
+ onSubmit: (model) => void saveSettings(model)
997
+ }) : null,
821
998
  dialog === "diagram" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NewDiagramDialog, {
822
999
  t,
823
1000
  project,
@@ -841,16 +1018,26 @@ function StudioPage({ t }) {
841
1018
  children: t("page.subtitle")
842
1019
  })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
843
1020
  className: "dap-head__actions",
844
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
845
- className: "dap-btn dap-btn--sm",
846
- onClick: () => void updateAll(),
847
- disabled: busy !== null || state.projects.length === 0,
848
- children: t("action.updateAll")
849
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
850
- className: "dap-btn dap-btn--sm dap-btn--primary",
851
- onClick: () => setDialog("project"),
852
- children: ["+ ", t("action.add")]
853
- })]
1021
+ children: [
1022
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1023
+ className: "dap-iconbtn",
1024
+ onClick: () => setDialog("settings"),
1025
+ title: t("action.settings"),
1026
+ "aria-label": t("action.settings"),
1027
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GearIcon, {})
1028
+ }),
1029
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1030
+ className: "dap-btn dap-btn--sm",
1031
+ onClick: () => void updateAll(),
1032
+ disabled: busy !== null || state.projects.length === 0,
1033
+ children: t("action.updateAll")
1034
+ }),
1035
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1036
+ className: "dap-btn dap-btn--sm dap-btn--primary",
1037
+ onClick: () => setDialog("project"),
1038
+ children: ["+ ", t("action.add")]
1039
+ })
1040
+ ]
854
1041
  })]
855
1042
  }),
856
1043
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -938,6 +1125,13 @@ function StudioPage({ t }) {
938
1125
  })] })
939
1126
  ]
940
1127
  }),
1128
+ dialog === "settings" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsDialog, {
1129
+ t,
1130
+ settings,
1131
+ busy: busy !== null,
1132
+ onClose: () => setDialog(null),
1133
+ onSubmit: (model) => void saveSettings(model)
1134
+ }) : null,
941
1135
  dialog === "project" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NewProjectDialog, {
942
1136
  t,
943
1137
  busy: busy !== null,
@@ -1638,6 +1832,20 @@ const zh = {
1638
1832
  "action.delete": "删除",
1639
1833
  "action.openSession": "查看会话",
1640
1834
  "action.cancel": "取消",
1835
+ "action.settings": "设置",
1836
+ "dialog.settings": "设置",
1837
+ "dialog.settingsSub": "只影响这个插件的分析会话,不动 DSH 里你自己的默认模型。",
1838
+ "field.model": "分析模型",
1839
+ "field.modelHint": "选项来自你在 DSH 里已经配好、当前能路由的模型。",
1840
+ "field.sessionMode": "会话形态",
1841
+ "model.followDefault": "跟随宿主默认",
1842
+ "model.followDefaultNamed": "跟随宿主默认({model})",
1843
+ "settings.noModels": "宿主的模型目录是空的:去 DSH 设置里配一个模型,这里才有得选。",
1844
+ "settings.unavailable": "这个宿主版本没有提供模型目录,只能跟随宿主默认。",
1845
+ "settings.providerFailed": "{list} 的模型列表取不到(网络或凭据),暂时不能选。",
1846
+ "settings.sessionSubagent": "子代理会话(不占用左侧会话列表)",
1847
+ "settings.sessionPlain": "普通会话(宿主不支持子代理会话)",
1848
+ "settings.scopeHint": "保存后对下一次运行生效",
1641
1849
  "action.submit": "创建并分析",
1642
1850
  "action.close": "关闭",
1643
1851
  "action.back": "返回图册",
@@ -1799,6 +2007,20 @@ const en = {
1799
2007
  "action.delete": "Delete",
1800
2008
  "action.openSession": "Open session",
1801
2009
  "action.cancel": "Cancel",
2010
+ "action.settings": "Settings",
2011
+ "dialog.settings": "Settings",
2012
+ "dialog.settingsSub": "Only affects this plugin's analysis sessions; your own default model in DSH stays untouched.",
2013
+ "field.model": "Analysis model",
2014
+ "field.modelHint": "Options are the models already configured in DSH and currently routable.",
2015
+ "field.sessionMode": "Session",
2016
+ "model.followDefault": "Follow the host default",
2017
+ "model.followDefaultNamed": "Follow the host default ({model})",
2018
+ "settings.noModels": "The host catalog is empty. Configure a model in DSH settings to pick one here.",
2019
+ "settings.unavailable": "This host version exposes no model catalog; the host default is used.",
2020
+ "settings.providerFailed": "Could not list models for {list} (network or credentials).",
2021
+ "settings.sessionSubagent": "Subagent session (kept out of the sidebar list)",
2022
+ "settings.sessionPlain": "Plain session (this host lacks subagent sessions)",
2023
+ "settings.scopeHint": "Applies to the next run",
1802
2024
  "action.submit": "Create and analyse",
1803
2025
  "action.close": "Close",
1804
2026
  "action.back": "Back to atlas",
@@ -2190,6 +2412,25 @@ const PAGE_CSS = `
2190
2412
 
2191
2413
 
2192
2414
  .dap-dialog--wide { width: 680px; }
2415
+
2416
+ /* 头部图标按钮(设置齿轮):与 dap-btn--sm 同高,窄一点 */
2417
+ .dap-iconbtn {
2418
+ display: inline-flex; align-items: center; justify-content: center;
2419
+ width: 26px; height: 26px; border-radius: var(--dsw-radius-md);
2420
+ border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-2);
2421
+ color: var(--dsw-alias-label-secondary); cursor: pointer;
2422
+ }
2423
+ .dap-iconbtn:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-border-l2); }
2424
+
2425
+ /* 设置弹窗的模型下拉:原生 select 最省事,长列表也能滚动 */
2426
+ .dap-select {
2427
+ min-width: 240px; max-width: 380px; height: 28px; padding: 0 8px;
2428
+ border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-md);
2429
+ background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary);
2430
+ font: inherit; font-size: 12px; outline: none; cursor: pointer;
2431
+ }
2432
+ .dap-select:focus { border-color: var(--dsw-alias-border-l3); }
2433
+ .dap-hint--warn { color: var(--dsw-alias-state-warning-primary, var(--dsw-alias-label-tertiary)); }
2193
2434
  .dap-typebar { display: flex; flex-wrap: wrap; gap: 5px; margin: 6px 0 8px; }
2194
2435
  .dap-type-tag {
2195
2436
  display: inline-flex; align-items: center; gap: 5px; height: 27px; padding: 0 9px;