@openvulcan/dsh-vulcancode 0.4.0 → 0.4.3

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
@@ -10,62 +10,102 @@
10
10
  **把 VulcanCode 的体验带进 DeepSeek Harness(dsh)**
11
11
 
12
12
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
13
+ [![npm](https://img.shields.io/npm/v/@openvulcan/dsh-vulcancode)](https://www.npmjs.com/package/@openvulcan/dsh-vulcancode)
13
14
 
14
15
  **简体中文** | [English](README.en.md)
15
16
 
17
+ [特色功能](#特色功能) · [架构与包结构](#架构与包结构) · [快速开始](#快速开始) · [开发](#开发)
18
+
16
19
  </div>
17
20
 
18
21
  ---
19
22
 
20
23
  ## 关于项目
21
24
 
22
- dsh-vulcancode 是 DeepSeek Harness(dsh)的一组社区插件,安装一个包即可得到 VulcanCode 的几项核心能力。
23
- 适配 dsh `0.2.1-rc.2` 及以上、`0.3.0` 以下:
24
-
25
- ```sh
26
- dsh plugin add @openvulcan/dsh-vulcancode
27
- ```
25
+ dsh-vulcancode 是 DeepSeek Harness(dsh)的社区插件套件。安装一个包,即可在 dsh 中使用 VulcanCode 的界面风格、交互式回答、自动审批、LuaSkills 和长期记忆,并为 pi-ai 模型配置思考级别。
28
26
 
29
27
  本项目与 DeepSeek 无关联,dsh 是 DeepSeek 的产品。
30
28
 
31
- | 组成 | 作用 |
32
- | --- | --- |
33
- | OpenVulcan 账号 | 登录一次,套件内各插件共用;凭据保存在 dsh 的凭据存储中。设置位于「设置 › OpenVulcan 账号」 |
34
- | 自动审批(Vulcan Decision) | 每次工具调用前由专用判定模型判断放行、需确认或拦下;未放行的可交复核 LLM,再询问你或拒绝 |
35
- | 交互式回答 | 模型可以用 VulcanCode 的 `ui-*` 组件作答,在对话中渲染;启用后默认开启,可按会话关闭 |
36
- | LuaSkills | 技能以工具形式提供给 AI,默认技能 codekit(代码导航);运行库约 190 MB,在「设置 › LuaSkills」中按需安装,装好即自动接入。侧栏的 LuaSkills 页面管理已装技能并连接 LuaSkills Hub |
37
- | Vulcan 界面风格 | VulcanCode 的主题、直角与配色 |
38
- | 长期记忆 | VulcanCode 的 Markdown 长期记忆:AI 用 `memory_outline`、`memory_search`、`memory_write` 维护用户画像、空间记忆与项目记忆,每次会话开始时注入一份快照。侧栏的「长期记忆」页面可编辑、调整注入预算,并导入导出 |
29
+ ## 特色功能
39
30
 
40
- > **我们承诺:云端命令判定模型永久免费提供。**(2026 年 10 月 27 日)
31
+ - **桌面工作台**采用 VulcanCode 配色、可独立开关的直角外观,以及加宽后能充分利用内容区的设置页面。
32
+ - **结果可交互**,模型可在对话中用图表、流程图和决策卡片呈现结果。卡片里的选择可以带回输入框继续讨论。
33
+ - **工具调用可审查**,Vulcan Decision 在执行前使用云端或本机模型判定,并支持配置复核和人工确认。
34
+ - **技能与经验可复用**,LuaSkills 把技能发布为 AI 可调用的工具,Markdown 长期记忆保留用户、空间和项目经验。
35
+ - **模型可单独配置思考级别**,pi-ai 模型的可选级别和接口传输值可按供应商、按模型设置。
41
36
 
42
- ## 使用
37
+ ## 架构与包结构
43
38
 
44
- 1. 安装后重启 dsh,打开「设置 › OpenVulcan 账号」,选择国际站或中国站登录。
45
- 2. 打开「设置 › 自动审批」,在权限选择器中选择“自动审查”,或把接入方式改为“独立开关”。不登录也可以改用本机
46
- 模型判定。
47
- 3. 需要 LuaSkills 时,打开「设置 › LuaSkills」点击安装运行库,进度显示在页面上,完成后技能工具自动出现。
48
- 4. 交互式回答、界面风格和长期记忆安装即生效,可在各自的设置或侧栏页面中调整。
39
+ dsh 通过套件的 `cordis.patch.yml` 装载七个插件。自动审批调用判定引擎,使用云端判定时通过 `openvulcanAccount` 获取账号凭据。账号插件和判定引擎复用同一账号库。发布包随附这九个工作区依赖,用户只需安装套件。
49
40
 
50
- ## 自动审批的边界
41
+ ### 套件入口
42
+
43
+ #### `@openvulcan/dsh-vulcancode`
44
+
45
+ 负责声明 dsh 兼容范围和插件组。安装入口是 `dsh plugin add @openvulcan/dsh-vulcancode`,插件管理页可查看套件。源码在 [packages/dsh-vulcancode](packages/dsh-vulcancode)。
46
+
47
+ ### dsh 插件
48
+
49
+ #### `@openvulcan/dsh-vulcan-account`
50
+
51
+ 在「设置 › OpenVulcan 账号」选择国际站或中国站,浏览器授权后即可在 dsh 内查看账号状态或退出登录。插件将凭据保存到 dsh 的凭据存储,并通过 `openvulcanAccount` 向云端判定等集成提供账号服务。源码在 [packages/dsh-vulcan-account](packages/dsh-vulcan-account)。
52
+
53
+ #### `@openvulcan/dsh-decision-guard`
54
+
55
+ 在「设置 › 自动审批」选择与权限预设的接入方式,配置云端或本机判定、复核 LLM 和询问规则,并查看审批记录。判定模型给出放行、需确认或拦截三类结果。未放行的调用按配置进入复核、询问或拒绝。详见[自动审批的边界](#自动审批的边界)和[插件说明](packages/dsh-decision-guard/README.md)。
56
+
57
+ #### `@openvulcan/dsh-vulcan-answers`
58
+
59
+ 在对话中渲染模型输出的 `ui-*` 代码围栏,例如对比表、图表、流程图、时序图和决策卡片。决策选项可以插入输入框或发送。对话默认使用交互模式,输入框里的「回答」可以按会话切换为标准 Markdown,从下次请求起生效。源码在 [packages/dsh-vulcan-answers](packages/dsh-vulcan-answers)。
60
+
61
+ #### `@openvulcan/dsh-vulcan-luaskills`
62
+
63
+ 在「设置 › LuaSkills」按需安装约 190 MB 的运行库和默认代码导航技能 CodeKit。安装后引擎和工具自动接入,可以调整 VM 池、缓存、受管 Python / Node 和网络设置。侧栏「LuaSkills」可启用、停用、更新或卸载技能,也可从 LuaSkills Hub 搜索和安装新技能。源码在 [packages/dsh-vulcan-luaskills](packages/dsh-vulcan-luaskills)。
64
+
65
+ #### `@openvulcan/dsh-vulcan-memory`
66
+
67
+ 以 Markdown 保存用户画像、空间记忆和项目记忆。向模型提供 `memory_outline`、`memory_search`、`memory_write` 与 `memory-curator` 技能,并在会话开始时按预算注入快照。侧栏「长期记忆」支持编辑、调整注入预算和导入导出。当前可用的是 Markdown 模式。源码在 [packages/dsh-vulcan-memory](packages/dsh-vulcan-memory)。
51
68
 
52
- - 它防的是 AI 执行命令时的**失误**,不防 AI **刻意绕过**(例如先写一个脚本再运行它)。
53
- - 任何错误都不会被当作放行;无人值守的会话从不询问你,复核未放行即拒绝。
54
- - 自动审批只降低事故概率,不保证拒绝一切风险;请同时使用沙箱、Git 与备份。
69
+ #### `@openvulcan/dsh-vulcan-reasoning`
55
70
 
56
- ## 包结构
71
+ 在「设置 › 模型」展开已配置的 pi-ai 提供商卡片中的「自定义思考级别」。按模型选择跟随目录、关闭思考,或自定义对话中可选的级别及接口传输值。保存后在对话模型选择器中选用,传输值以对应模型接口支持的参数为准。源码在 [packages/dsh-vulcan-reasoning](packages/dsh-vulcan-reasoning)。
72
+
73
+ #### `@openvulcan/dsh-vulcan-style`
74
+
75
+ 在「设置 › 通用设置」的「Vulcan 界面风格」选择跟随系统、亮色、暗色、指挥舱或 dsh 自带主题。直角开关可与任意主题搭配,覆盖 Windows 桌面内容区等界面边角。设置布局为模型目录、内置插件和 LuaSkills 等页面留出更多可用宽度。源码在 [packages/dsh-vulcan-style](packages/dsh-vulcan-style)。
76
+
77
+ ### 共享库与命令行
78
+
79
+ #### `@openvulcan/vulcan-decision`
80
+
81
+ 为宿主提供 `allow`、`review` 和 `block` 判定,支持 OpenVulcan 云端端点和按需安装的本机运行包。随附的 `vulcan-decision` CLI 可用于探测云端、检查硬件、管理本机运行包和模型,以及对单次工具调用执行判定。详见[判定引擎与命令行说明](packages/vulcan-decision/README.md)。
82
+
83
+ #### `@openvulcan/openvulcan-account`
84
+
85
+ 封装设备授权、凭据刷新、账号资料查询和设备注销,并定义 `CredentialStore` 存储接口。dsh 插件接入宿主凭据存储,独立 CLI 可使用库内的文件存储实现。源码在 [packages/openvulcan-account](packages/openvulcan-account)。
86
+
87
+ ## 快速开始
88
+
89
+ 套件声明兼容 dsh `>=0.2.1-rc.2 <0.3.0`。
90
+
91
+ ```sh
92
+ dsh plugin add @openvulcan/dsh-vulcancode
93
+ ```
94
+
95
+ ### 使用
96
+
97
+ 1. 安装后重启 dsh。使用云端判定时,在「设置 › OpenVulcan 账号」完成浏览器授权。选用本机判定可以跳过账号登录。
98
+ 2. 打开「设置 › 自动审批」配置判定方式。在权限选择器中选「自动审查」可对当前会话启用,该预设使用完全权限。改用「独立开关」会为所有会话叠加判定,并保留已有权限预设的文件沙箱。
99
+ 3. 需要技能工具时,在「设置 › LuaSkills」安装运行库。进度显示在页面上,完成后技能工具自动接入。
100
+ 4. 界面风格、交互式回答和 Markdown 长期记忆默认启用。思考级别可以在配置好 pi-ai 提供商后按模型调整。
101
+
102
+ > **我们承诺云端命令判定模型永久免费提供。**(2026 年 10 月 27 日)
103
+
104
+ ## 自动审批的边界
57
105
 
58
- | 包 | 说明 |
59
- | --- | --- |
60
- | `@openvulcan/dsh-vulcancode` | 套件本体,dsh 安装的唯一入口,其余包都打包在内 |
61
- | `@openvulcan/dsh-vulcan-account` | OpenVulcan 账号插件,向其他插件提供 `openvulcanAccount` 服务 |
62
- | `@openvulcan/dsh-decision-guard` | 自动审批插件 |
63
- | `@openvulcan/dsh-vulcan-answers` | 交互式回答插件 |
64
- | `@openvulcan/dsh-vulcan-luaskills` | LuaSkills 插件:运行库安装、技能工具、VM 设置与 Hub |
65
- | `@openvulcan/dsh-vulcan-style` | 界面风格插件 |
66
- | `@openvulcan/dsh-vulcan-memory` | 长期记忆插件:记忆工具、会话快照注入、memory-curator 技能与记忆页面 |
67
- | `@openvulcan/vulcan-decision` | 判定引擎与命令行 `vulcan-decision` |
68
- | `@openvulcan/openvulcan-account` | OpenVulcan 账号库:登录、刷新与凭据存储接口 |
106
+ - 自动审批可以降低 AI 误执行命令的风险。AI 刻意绕过时,仍可能先写脚本再运行它。
107
+ - 任何错误都不会被当作放行。无人值守时,需要复核的调用只有在复核明确放行后才会执行。
108
+ - 请同时使用沙箱、Git 与备份。
69
109
 
70
110
  ## 开发
71
111
 
@@ -76,7 +116,7 @@ pnpm run test
76
116
  pnpm run pack:dsh # 生成 dist/openvulcan-dsh-vulcancode-<版本>.tgz
77
117
  ```
78
118
 
79
- 本地安装打包结果:`dsh plugin add ./dist/openvulcan-dsh-vulcancode-<版本>.tgz`。
119
+ 本地安装打包结果使用 `dsh plugin add ./dist/openvulcan-dsh-vulcancode-<版本>.tgz`。
80
120
 
81
121
  ## 致谢
82
122
 
package/cordis.patch.yml CHANGED
@@ -15,3 +15,5 @@
15
15
  name: '@openvulcan/dsh-vulcan-luaskills'
16
16
  - id: vulcan-memory
17
17
  name: '@openvulcan/dsh-vulcan-memory'
18
+ - id: vulcan-reasoning
19
+ name: '@openvulcan/dsh-vulcan-reasoning'
@@ -305,6 +305,10 @@ window.__ModuleLoader__.load({
305
305
  'conflict.failed': '停用失败:',
306
306
  'page.title': '自动审批',
307
307
  'page.lead': 'AI 每次调用工具前,先由 Vulcan Decision 判定模型判断:安全的直接放行,拿不准的交给复核 LLM 或由你确认,危险的拦截。任何错误都不会被当作放行。',
308
+ 'page.sections': '自动审批设置分区',
309
+ 'page.section.engine': '判定引擎',
310
+ 'page.section.policy': '复核与规则',
311
+ 'page.section.records': '判定记录',
308
312
  'loading': '读取中…',
309
313
  'saveFailed': '保存失败:{error}',
310
314
  'readOnly': '当前配置为只读,无法修改。',
@@ -593,8 +597,6 @@ window.__ModuleLoader__.load({
593
597
  'records.enabled': '记录判定',
594
598
  'records.enabledHint': '保存判定模型或复核参与的每次决定,含原始命令与路径,只保存在这台电脑上。关闭后不再写入新记录,已有记录保留到你清除为止。',
595
599
  'records.entryHint': '查看判定模型的决定,标记判错的结果并导出为训练样本。',
596
- 'records.open': '查看记录',
597
- 'records.back': '返回自动审批',
598
600
  'records.paused': '记录判定已关闭,不会写入新记录。',
599
601
  'records.hint': '保留最近 {records} 条、最长 {days} 天的记录。记录包含原始命令与路径,只保存在这台电脑上。',
600
602
  'records.count': '{count} 条记录',
@@ -660,6 +662,10 @@ window.__ModuleLoader__.load({
660
662
  'conflict.failed': 'Could not disable: ',
661
663
  'page.title': 'Auto approval',
662
664
  'page.lead': 'Before the AI runs a tool, the Vulcan Decision model judges the call: safe calls run, uncertain ones go to the review LLM or to you, dangerous ones are blocked. No error ever counts as an allow.',
665
+ 'page.sections': 'Auto approval settings sections',
666
+ 'page.section.engine': 'Decision engine',
667
+ 'page.section.policy': 'Review and rules',
668
+ 'page.section.records': 'Decision records',
663
669
  'loading': 'Loading…',
664
670
  'saveFailed': 'Could not save: {error}',
665
671
  'readOnly': 'The configuration is read-only.',
@@ -948,8 +954,6 @@ window.__ModuleLoader__.load({
948
954
  'records.enabled': 'Record decisions',
949
955
  'records.enabledHint': 'Keep every decision the decision model or the review took part in, with the original commands and paths, on this computer only. Turning it off stops new records; existing ones stay until you clear them.',
950
956
  'records.entryHint': 'See the decision model\'s answers, mark wrong ones and export them as training samples.',
951
- 'records.open': 'View records',
952
- 'records.back': 'Back to auto approval',
953
957
  'records.paused': 'Recording is off, so no new records are written.',
954
958
  'records.hint': 'Keeps the latest {records} records for at most {days} days. Records hold the original commands and paths, on this computer only.',
955
959
  'records.count': '{count} records',
@@ -1112,6 +1116,12 @@ window.__ModuleLoader__.load({
1112
1116
  locale: i18n_1.NAMESPACE,
1113
1117
  inject: withServices,
1114
1118
  }, ({ t, services: injected }) => (0, jsx_runtime_1.jsx)(settings_1.SettingsPage, { services: injected, t: t })));
1119
+ // dsh 0.2.2 declares this optional slot. The injection waits on older hosts.
1120
+ // dsh 0.2.2 声明了此可选插槽,旧版宿主上的注入会保持等待。
1121
+ ctx.slots.inject('settings.section.icon', () => ctx.slots.register({
1122
+ name: 'settings.section.icon',
1123
+ id: shared_1.SETTINGS_SECTION,
1124
+ }, SettingsIcon));
1115
1125
  ctx.slots.inject('plugins.detail.badge', () => ctx.slots.register({
1116
1126
  name: 'plugins.detail.badge',
1117
1127
  id: 'vulcan-decision-guard-conflict',
@@ -1158,6 +1168,13 @@ window.__ModuleLoader__.load({
1158
1168
  },
1159
1169
  }, quick_1.QuickSetting));
1160
1170
  }
1171
+ /**
1172
+ * Render the approval shield at the square size requested by dsh's settings navigation.
1173
+ * 按 dsh 设置导航请求的方形尺寸渲染审批盾牌图标。
1174
+ */
1175
+ function SettingsIcon({ size }) {
1176
+ return ((0, jsx_runtime_1.jsxs)("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.7, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", focusable: "false", children: [(0, jsx_runtime_1.jsx)("path", { d: "M12 3 20 6v5c0 4.7-2.8 7.9-8 10-5.2-2.1-8-5.3-8-10V6l8-3Z" }), (0, jsx_runtime_1.jsx)("path", { d: "m9 12 2 2 4-4" })] }));
1177
+ }
1161
1178
 
1162
1179
  },
1163
1180
  "./local": function (module, exports, require) {
@@ -1356,7 +1373,7 @@ window.__ModuleLoader__.load({
1356
1373
  "./records": function (module, exports, require) {
1357
1374
  "use strict";
1358
1375
  Object.defineProperty(exports, "__esModule", { value: true });
1359
- exports.RecordsPage = RecordsPage;
1376
+ exports.RecordsPanel = RecordsPanel;
1360
1377
  const jsx_runtime_1 = require("react/jsx-runtime");
1361
1378
  /**
1362
1379
  * Settings › Auto approval › Decision log, after VulcanCode's records page: every decision the
@@ -1403,7 +1420,11 @@ window.__ModuleLoader__.load({
1403
1420
  query.set('before', before);
1404
1421
  return `records?${query.toString()}`;
1405
1422
  }
1406
- function RecordsPage({ t, recording, onBack }) {
1423
+ /**
1424
+ * Render decision history and its actions using the dictionary and current recording state.
1425
+ * 根据本地化词典和当前记录开关状态渲染判定历史及其操作。
1426
+ */
1427
+ function RecordsPanel({ t, recording }) {
1407
1428
  const [filters, setFilters] = (0, react_1.useState)({ label: undefined, workspace: undefined, markedOnly: false });
1408
1429
  const [page, setPage] = (0, react_1.useState)(undefined);
1409
1430
  const [loadError, setLoadError] = (0, react_1.useState)(undefined);
@@ -1467,7 +1488,7 @@ window.__ModuleLoader__.load({
1467
1488
  }, 'records.clearFailed');
1468
1489
  };
1469
1490
  const clearButton = ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "vdg-btn ghost sm", disabled: busy !== undefined, onClick: () => { setClearing(true); }, children: t('records.clear') }));
1470
- return ((0, jsx_runtime_1.jsxs)("div", { className: "vdg vdg-page", children: [(0, jsx_runtime_1.jsxs)("div", { className: "vdg-page-head", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "vdg-link", onClick: onBack, children: ["\u2190 ", t('records.back')] }), (0, jsx_runtime_1.jsx)("h2", { style: { marginTop: 8 }, children: t('records.title') }), (0, jsx_runtime_1.jsx)("p", { className: "vdg-lead", children: t('records.entryHint') })] }), recording ? null : (0, jsx_runtime_1.jsx)("div", { className: "vdg-alert warn", children: t('records.paused') }), page === undefined
1491
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [recording ? null : (0, jsx_runtime_1.jsx)("div", { className: "vdg-alert warn", children: t('records.paused') }), page === undefined
1471
1492
  ? (loadError === undefined
1472
1493
  ? (0, jsx_runtime_1.jsx)("p", { className: "vdg-hint", children: t('loading') })
1473
1494
  : ((0, jsx_runtime_1.jsx)("div", { className: "vdg-alert bad", children: (0, jsx_runtime_1.jsxs)("div", { className: "vdg-between", children: [(0, jsx_runtime_1.jsx)("span", { children: (0, i18n_1.fill)(t('records.loadFailed'), { error: loadError }) }), clearButton] }) })))
@@ -1544,6 +1565,9 @@ window.__ModuleLoader__.load({
1544
1565
  safe: { tool: 'shell_command', args: { command: 'git status', shell: 'bash' } },
1545
1566
  risky: { tool: 'shell_command', args: { command: 'git push --force origin main', shell: 'bash' } },
1546
1567
  };
1568
+ // The sections appear in the same order in navigation and in the page.
1569
+ // 分区在导航和正文中按同一顺序排列。
1570
+ const SETTINGS_VIEWS = ['engine', 'policy', 'records'];
1547
1571
  /** What the status section says about decisions, from the saved settings and this machine's state. */
1548
1572
  function decisionStatus(t, value, live, models) {
1549
1573
  if (live == null)
@@ -1631,8 +1655,12 @@ window.__ModuleLoader__.load({
1631
1655
  }, children: t('prompts.save') })] })] }));
1632
1656
  }
1633
1657
  /** The decision log switch and the way into the log. */
1634
- function RecordsSection({ t, value, disabled, save, onOpen }) {
1635
- return ((0, jsx_runtime_1.jsx)(ui_1.Section, { title: t('records.title'), lead: t('records.entryHint'), actions: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "vdg-btn sm", onClick: onOpen, children: [t('records.open'), " \u2192"] }), children: (0, jsx_runtime_1.jsxs)("label", { className: "vdg-check", children: [(0, jsx_runtime_1.jsx)("input", { type: "checkbox", checked: value.recordDecisions, disabled: disabled, onChange: (event) => { save({ recordDecisions: event.target.checked }); } }), (0, jsx_runtime_1.jsxs)("span", { children: [(0, jsx_runtime_1.jsx)("span", { className: "vdg-choice-title", children: t('records.enabled') }), (0, jsx_runtime_1.jsx)("span", { className: "vdg-choice-hint", children: t('records.enabledHint') })] })] }) }));
1658
+ /**
1659
+ * Render the recording preference using the dictionary, current settings, disabled state and save callback.
1660
+ * 使用文案、当前设置、禁用状态与保存回调渲染判定记录开关。
1661
+ */
1662
+ function RecordsSection({ t, value, disabled, save }) {
1663
+ return ((0, jsx_runtime_1.jsx)(ui_1.Section, { title: t('records.title'), lead: t('records.entryHint'), children: (0, jsx_runtime_1.jsxs)("label", { className: "vdg-check", children: [(0, jsx_runtime_1.jsx)("input", { type: "checkbox", checked: value.recordDecisions, disabled: disabled, onChange: (event) => { save({ recordDecisions: event.target.checked }); } }), (0, jsx_runtime_1.jsxs)("span", { children: [(0, jsx_runtime_1.jsx)("span", { className: "vdg-choice-title", children: t('records.enabled') }), (0, jsx_runtime_1.jsx)("span", { className: "vdg-choice-hint", children: t('records.enabledHint') })] })] }) }));
1636
1664
  }
1637
1665
  function AskSection({ t, value, disabled, save }) {
1638
1666
  const [text, setText] = (0, react_1.useState)(String(value.askTimeout));
@@ -1661,10 +1689,22 @@ window.__ModuleLoader__.load({
1661
1689
  ? null
1662
1690
  : ((0, jsx_runtime_1.jsxs)("div", { className: "vdg-inline", children: [(0, jsx_runtime_1.jsx)(ui_1.Badge, { tone: verdict.decision === 'allow' ? 'ok' : verdict.decision === 'review' ? 'warn' : 'bad', children: ['allow', 'review', 'block'].includes(verdict.decision) ? t(`decision.${verdict.decision}`) : verdict.decision }), (0, jsx_runtime_1.jsx)("span", { className: "vdg-hint", children: (0, i18n_1.fill)(t('try.result'), { decision: verdict.decision, ms: verdict.elapsed_ms, backend: verdict.backend, via: verdict.via }) })] })), (0, jsx_runtime_1.jsx)(ui_1.ErrorLine, { text: error === undefined ? undefined : (0, i18n_1.fill)(t('try.failed'), { error }) })] }));
1663
1691
  }
1692
+ /**
1693
+ * Render the engine, policy and records views using the guard services and localized dictionary.
1694
+ * 使用审批服务与本地化词典渲染判定引擎、复核规则与记录分区。
1695
+ */
1664
1696
  function SettingsPage({ services, t }) {
1665
1697
  const settings = (0, ui_1.useSettings)(services.form, t);
1666
1698
  const live = (0, ui_1.useWatch)(services.panel);
1667
- const [view, setView] = (0, react_1.useState)('settings');
1699
+ // The selected section controls visibility while keeping settings drafts mounted.
1700
+ // 所选分区控制显示状态,同时保留已挂载的设置草稿。
1701
+ const [view, setView] = (0, react_1.useState)('engine');
1702
+ // The root is the scroll target when the user changes sections from the sticky navigation.
1703
+ // 用户从吸附导航切换分区时,将设置页根节点作为滚动目标。
1704
+ const page = (0, react_1.useRef)(null);
1705
+ // Each navigation control points to its own panel within this settings instance.
1706
+ // 每个分区按钮指向当前设置实例中的对应面板。
1707
+ const viewId = (0, react_1.useId)();
1668
1708
  const active = live != null && (0, shared_1.running)(live.job);
1669
1709
  (0, ui_1.usePolling)(services.panel, active ? 1000 : undefined);
1670
1710
  const value = settings.value;
@@ -1675,25 +1715,33 @@ window.__ModuleLoader__.load({
1675
1715
  return (0, jsx_runtime_1.jsx)("div", { className: "vdg vdg-page", children: (0, jsx_runtime_1.jsx)("div", { className: "vdg-alert warn", children: t('unavailable') }) });
1676
1716
  }
1677
1717
  const { disabled, save, saveAsync } = settings;
1678
- if (view === 'records')
1679
- return (0, jsx_runtime_1.jsx)(records_1.RecordsPage, { t: t, recording: value.recordDecisions, onBack: () => { setView('settings'); } });
1680
1718
  const refresh = () => {
1681
1719
  services.panel.refresh();
1682
1720
  services.cloudModels.refresh();
1683
1721
  services.endpoints.refresh();
1684
1722
  };
1685
- return ((0, jsx_runtime_1.jsxs)("div", { className: "vdg vdg-page", children: [(0, jsx_runtime_1.jsxs)("div", { className: "vdg-page-head", children: [(0, jsx_runtime_1.jsx)("h2", { children: t('page.title') }), (0, jsx_runtime_1.jsx)("p", { className: "vdg-lead", children: t('page.lead') })] }), settings.writable ? null : (0, jsx_runtime_1.jsx)("div", { className: "vdg-alert warn", children: t('readOnly') }), (0, jsx_runtime_1.jsx)(ui_1.ErrorLine, { text: settings.error }), (0, jsx_runtime_1.jsx)(StatusSection, { services: services, t: t, value: value, live: live }), (0, jsx_runtime_1.jsx)(ui_1.Section, { title: t('mode.title'), children: (0, jsx_runtime_1.jsx)(ui_1.Choices, { label: t('mode.title'), value: value.mode, options: ['auto-preset', 'standalone'].map(item => ({ value: item, title: t(`mode.${item}`), hint: t(`mode.${item}.hint`) })), disabled: disabled, onChoose: (next) => { save({ mode: next }); } }) }), (0, jsx_runtime_1.jsx)(ui_1.Section, { title: t('backend.title'), children: (0, jsx_runtime_1.jsx)(ui_1.Choices, { label: t('backend.title'), value: value.backend, options: ['cloud', 'local'].map(item => ({ value: item, title: t(`backend.${item}`), hint: t(`backend.${item}.hint`) })), disabled: disabled, onChoose: (next) => { save({ backend: next }); } }) }), live === undefined
1686
- ? (0, jsx_runtime_1.jsx)("p", { className: "vdg-hint", children: t('loading') })
1687
- : live === null
1688
- ? (0, jsx_runtime_1.jsx)("div", { className: "vdg-alert bad", children: t('panel.unavailable') })
1689
- : value.backend === 'cloud'
1690
- ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(cloud_1.AccountSection, { services: services, t: t, account: live.account }), live.account.signedIn
1691
- ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(cloud_1.ModelSection, { services: services, t: t, saved: value.cloudModel, disabled: disabled, onChoose: (model) => {
1692
- // A lock belongs to the model it was chosen for.
1693
- void saveAsync({ cloudModel: model === '' ? undefined : model, cloudEndpoint: undefined }).then(() => { services.endpoints.refresh(); });
1694
- } }), (0, jsx_runtime_1.jsx)(cloud_1.EndpointsSection, { services: services, t: t, disabled: disabled, onLock: endpoint => (0, cloud_1.lockEndpoint)(saveAsync, value.cloudModel, endpoint, t('readOnly')) })] }))
1695
- : null] }))
1696
- : (0, jsx_runtime_1.jsx)(local_1.LocalSections, { t: t, live: live, value: value, disabled: disabled, save: save, refresh: refresh }), (0, jsx_runtime_1.jsx)(OptionsSection, { t: t, value: value, disabled: disabled, save: save }), (0, jsx_runtime_1.jsx)(ReviewSection, { services: services, t: t, value: value, disabled: disabled, save: save }), value.review === 'off' ? null : (0, jsx_runtime_1.jsx)(RulesSection, { t: t, value: value, disabled: disabled, saveAsync: saveAsync }), value.review === 'off' ? null : (0, jsx_runtime_1.jsx)(PromptsSection, { t: t, value: value, disabled: disabled, saveAsync: saveAsync }), (0, jsx_runtime_1.jsx)(AskSection, { t: t, value: value, disabled: disabled, save: save }), (0, jsx_runtime_1.jsx)(RecordsSection, { t: t, value: value, disabled: disabled, save: save, onOpen: () => { setView('records'); } }), (0, jsx_runtime_1.jsx)(TrySection, { t: t })] }));
1723
+ /**
1724
+ * Activate the requested settings section and return to its start in the host's scroll pane.
1725
+ * 激活指定的设置分区,并在宿主滚动容器内回到分区开头。
1726
+ */
1727
+ const selectView = (next) => {
1728
+ if (view === next)
1729
+ return;
1730
+ setView(next);
1731
+ page.current?.scrollIntoView({ block: 'start' });
1732
+ };
1733
+ return ((0, jsx_runtime_1.jsxs)("div", { ref: page, className: "vdg vdg-page", "data-vulcan-settings-width": "full", children: [(0, jsx_runtime_1.jsxs)("header", { className: "vdg-page-head", "data-vulcan-settings-header": true, children: [(0, jsx_runtime_1.jsx)("h2", { children: t('page.title') }), (0, jsx_runtime_1.jsx)("p", { className: "vdg-lead", children: t('page.lead') })] }), (0, jsx_runtime_1.jsx)("div", { className: "vdg-settings-navigation", role: "group", "aria-label": t('page.sections'), children: SETTINGS_VIEWS.map(item => ((0, jsx_runtime_1.jsx)("button", { id: `${viewId}-${item}-button`, type: "button", "aria-pressed": view === item, "aria-controls": `${viewId}-${item}`, onClick: () => { selectView(item); }, children: t(`page.section.${item}`) }, item))) }), settings.writable ? null : (0, jsx_runtime_1.jsx)("div", { className: "vdg-alert warn", children: t('readOnly') }), (0, jsx_runtime_1.jsx)(ui_1.ErrorLine, { text: settings.error }), (0, jsx_runtime_1.jsxs)("section", { className: "vdg-settings-group", id: `${viewId}-engine`, "aria-labelledby": `${viewId}-engine-button`, hidden: view !== 'engine', children: [(0, jsx_runtime_1.jsx)(StatusSection, { services: services, t: t, value: value, live: live }), (0, jsx_runtime_1.jsx)(ui_1.Section, { title: t('mode.title'), children: (0, jsx_runtime_1.jsx)(ui_1.Choices, { label: t('mode.title'), value: value.mode, options: ['auto-preset', 'standalone'].map(item => ({ value: item, title: t(`mode.${item}`), hint: t(`mode.${item}.hint`) })), disabled: disabled, onChoose: (next) => { save({ mode: next }); } }) }), (0, jsx_runtime_1.jsx)(ui_1.Section, { title: t('backend.title'), children: (0, jsx_runtime_1.jsx)(ui_1.Choices, { label: t('backend.title'), value: value.backend, options: ['cloud', 'local'].map(item => ({ value: item, title: t(`backend.${item}`), hint: t(`backend.${item}.hint`) })), disabled: disabled, onChoose: (next) => { save({ backend: next }); } }) }), live === undefined
1734
+ ? (0, jsx_runtime_1.jsx)("p", { className: "vdg-hint", children: t('loading') })
1735
+ : live === null
1736
+ ? (0, jsx_runtime_1.jsx)("div", { className: "vdg-alert bad", children: t('panel.unavailable') })
1737
+ : value.backend === 'cloud'
1738
+ ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(cloud_1.AccountSection, { services: services, t: t, account: live.account }), live.account.signedIn
1739
+ ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(cloud_1.ModelSection, { services: services, t: t, saved: value.cloudModel, disabled: disabled, onChoose: (model) => {
1740
+ // A lock belongs to the model it was chosen for.
1741
+ void saveAsync({ cloudModel: model === '' ? undefined : model, cloudEndpoint: undefined }).then(() => { services.endpoints.refresh(); });
1742
+ } }), (0, jsx_runtime_1.jsx)(cloud_1.EndpointsSection, { services: services, t: t, disabled: disabled, onLock: endpoint => (0, cloud_1.lockEndpoint)(saveAsync, value.cloudModel, endpoint, t('readOnly')) })] }))
1743
+ : null] }))
1744
+ : (0, jsx_runtime_1.jsx)(local_1.LocalSections, { t: t, live: live, value: value, disabled: disabled, save: save, refresh: refresh }), (0, jsx_runtime_1.jsx)(OptionsSection, { t: t, value: value, disabled: disabled, save: save }), (0, jsx_runtime_1.jsx)(TrySection, { t: t })] }), (0, jsx_runtime_1.jsxs)("section", { className: "vdg-settings-group", id: `${viewId}-policy`, "aria-labelledby": `${viewId}-policy-button`, hidden: view !== 'policy', children: [(0, jsx_runtime_1.jsx)(ReviewSection, { services: services, t: t, value: value, disabled: disabled, save: save }), value.review === 'off' ? null : (0, jsx_runtime_1.jsx)(RulesSection, { t: t, value: value, disabled: disabled, saveAsync: saveAsync }), value.review === 'off' ? null : (0, jsx_runtime_1.jsx)(PromptsSection, { t: t, value: value, disabled: disabled, saveAsync: saveAsync }), (0, jsx_runtime_1.jsx)(AskSection, { t: t, value: value, disabled: disabled, save: save })] }), (0, jsx_runtime_1.jsx)("section", { className: "vdg-settings-group", id: `${viewId}-records`, "aria-labelledby": `${viewId}-records-button`, hidden: view !== 'records', children: view === 'records' ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(RecordsSection, { t: t, value: value, disabled: disabled, save: save }), (0, jsx_runtime_1.jsx)(records_1.RecordsPanel, { t: t, recording: value.recordDecisions })] })) : null })] }));
1697
1745
  }
1698
1746
 
1699
1747
  },
@@ -1838,6 +1886,15 @@ window.__ModuleLoader__.load({
1838
1886
  .vdg *, .vdg *::before, .vdg *::after { box-sizing: border-box; }
1839
1887
  .vdg-page { display: flex; flex-direction: column; gap: calc(var(--vk-u) * 4); counter-reset: vdg-section; padding-bottom: calc(var(--vk-u) * 2); }
1840
1888
  .vdg-page-head h2 { margin: 0; font-size: 16px; font-weight: 600; }
1889
+
1890
+ /* Keep the section switcher reachable inside the host's single scrolling pane. */
1891
+ /* 在宿主唯一的滚动正文内,让分区切换栏始终可触达。 */
1892
+ .vdg-settings-navigation { position: sticky; top: 0; z-index: 1; display: flex; flex-wrap: wrap; gap: 4px; border-bottom: 1px solid var(--vk-line); background: var(--vk-panel); }
1893
+ .vdg-settings-navigation > button { min-height: 36px; padding: 6px 12px; border: 0; border-bottom: 2px solid transparent; background: none; color: var(--vk-ink-2); font: inherit; cursor: pointer; }
1894
+ .vdg-settings-navigation > button:hover { background: var(--vk-hover); color: var(--vk-ink); }
1895
+ .vdg-settings-navigation > button[aria-pressed="true"] { border-bottom-color: var(--vk-ember); color: var(--vk-ink); font-weight: 600; }
1896
+ .vdg-settings-navigation > button:focus-visible { outline: 2px solid var(--vk-signal); outline-offset: -3px; }
1897
+ .vdg-settings-group:not([hidden]) { display: flex; flex-direction: column; gap: calc(var(--vk-u) * 4); min-width: 0; }
1841
1898
  .vdg-lead { margin: calc(var(--vk-u) * 1) 0 0; color: var(--vk-ink-2); }
1842
1899
  .vdg-hint { margin: 0; color: var(--vk-ink-3); font-size: 12px; }
1843
1900
  .vdg-error { margin: 0; color: var(--vk-bad); font-size: 12px; overflow-wrap: anywhere; }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openvulcan/dsh-decision-guard",
3
- "version": "0.4.0",
3
+ "version": "0.4.3",
4
4
  "description": "Vulcan Decision permission guard for DeepSeek Harness: every tool call is judged before it runs.",
5
5
  "license": "MIT",
6
6
  "icon": "./assets/decision-guard-icon.png",
@@ -44,7 +44,7 @@
44
44
  }
45
45
  },
46
46
  "dependencies": {
47
- "@openvulcan/vulcan-decision": "0.4.0"
47
+ "@openvulcan/vulcan-decision": "0.4.3"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "@deepseek-ai/cordis": "^4.0.5",
@@ -32,6 +32,7 @@ window.__ModuleLoader__.load({
32
32
  "use strict";
33
33
  Object.defineProperty(exports, "__esModule", { value: true });
34
34
  exports.AccountFooter = AccountFooter;
35
+ exports.VulcanMark = VulcanMark;
35
36
  const jsx_runtime_1 = require("react/jsx-runtime");
36
37
  /**
37
38
  * The OpenVulcan account in the sidebar foot, above Settings: who is signed in and their plan, or
@@ -70,23 +71,6 @@ window.__ModuleLoader__.load({
70
71
  view.refresh();
71
72
  });
72
73
  };
73
- const signIn = (site) => {
74
- // Opened during the click, so no popup blocker stops it, and pointed at the page once known.
75
- const popup = window.open('about:blank', '_blank');
76
- act(async () => {
77
- try {
78
- const { approvalUrl } = await (0, shared_1.callAccount)('POST', `signin?site=${site}`);
79
- if (popup !== null) {
80
- popup.opener = null;
81
- popup.location.href = approvalUrl;
82
- }
83
- }
84
- catch (failure) {
85
- popup?.close();
86
- throw failure;
87
- }
88
- });
89
- };
90
74
  // Without the plugin routes there is nothing to show or do here.
91
75
  if (live === null)
92
76
  return null;
@@ -108,11 +92,17 @@ window.__ModuleLoader__.load({
108
92
  }
109
93
  const failure = live.signInError ?? error;
110
94
  const note = failure !== undefined ? t('footer.failed') : live.account.reauthRequired === true ? t('footer.reauth') : t('footer.signedOut');
111
- return ((0, jsx_runtime_1.jsxs)("div", { className: "vac-foot", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "vac-foot-home", "aria-label": t('footer.open'), title: failure ?? t('footer.open'), onClick: openSettings, children: [(0, jsx_runtime_1.jsx)(VulcanMark, {}), (0, jsx_runtime_1.jsxs)("span", { className: "vac-foot-text", children: [(0, jsx_runtime_1.jsx)("span", { className: "vac-foot-name", children: t('footer.brand') }), (0, jsx_runtime_1.jsx)("span", { className: failure === undefined && live.account.reauthRequired !== true ? 'vac-foot-sub' : 'vac-foot-sub warn', children: note })] })] }), (0, jsx_runtime_1.jsx)("span", { className: "vac-foot-actions", children: shared_1.SITES.map(item => ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "vac-foot-chip", disabled: busy, title: (0, i18n_1.fill)(t('footer.signInAt'), { site: t(`account.site.${item.site}`), domain: item.domain }), onClick: () => { signIn(item.site); }, children: t(`footer.chip.${item.site}`) }, item.site))) })] }));
95
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "vac-foot", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "vac-foot-home", "aria-label": t('footer.open'), title: failure ?? t('footer.open'), onClick: openSettings, children: [(0, jsx_runtime_1.jsx)(VulcanMark, {}), (0, jsx_runtime_1.jsxs)("span", { className: "vac-foot-text", children: [(0, jsx_runtime_1.jsx)("span", { className: "vac-foot-name", children: t('footer.brand') }), (0, jsx_runtime_1.jsx)("span", { className: failure === undefined && live.account.reauthRequired !== true ? 'vac-foot-sub' : 'vac-foot-sub warn', children: note })] })] }), (0, jsx_runtime_1.jsx)("span", { className: "vac-foot-actions", children: shared_1.SITES.map(item => ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "vac-foot-chip", disabled: busy, title: (0, i18n_1.fill)(t('footer.signInAt'), { site: t(`account.site.${item.site}`), domain: item.domain }), onClick: () => { act(() => (0, shared_1.startAccountSignIn)(item.site)); }, children: t(`footer.chip.${item.site}`) }, item.site))) })] }));
112
96
  }
113
- /** The OpenVulcan mark: the chevron takes the text colour, the flame keeps its own. */
114
- function VulcanMark() {
115
- return ((0, jsx_runtime_1.jsxs)("svg", { className: "vac-foot-mark", viewBox: "32 29 232 259", "aria-hidden": "true", focusable: "false", children: [(0, jsx_runtime_1.jsx)("defs", { children: (0, jsx_runtime_1.jsxs)("linearGradient", { id: "vac-foot-flame", x1: "0%", y1: "100%", x2: "100%", y2: "0%", children: [(0, jsx_runtime_1.jsx)("stop", { offset: "0%", stopColor: "#FF6527" }), (0, jsx_runtime_1.jsx)("stop", { offset: "100%", stopColor: "#F44E1C" })] }) }), (0, jsx_runtime_1.jsx)("path", { d: "M 2 0 L 84 0 C 88 0 91 1 95 5 L 153 76 C 159 83 159 95 153 102 L 95 173 C 91 177 88 178 84 178 L 2 178 L 78 89 Z", fill: "currentColor", transform: "translate(30 110)" }), (0, jsx_runtime_1.jsx)("path", { d: "M 73 0 C 64 14 51 28 38 40 C 21 55 7 68 2 87 C -2 107 8 129 25 150 C 39 134 53 119 67 107 C 82 95 91 79 93 60 C 95 40 86 18 73 0 Z", fill: "url(#vac-foot-flame)", transform: "translate(169 29)" })] }));
97
+ /**
98
+ * Render the OpenVulcan mark with the requested square size, or use the footer dimensions when omitted.
99
+ * 按指定的方形尺寸渲染 OpenVulcan 标识,未指定时使用侧栏底部的尺寸。
100
+ */
101
+ function VulcanMark({ size }) {
102
+ // The footer and settings can render at once, so each SVG owns its gradient reference.
103
+ // 侧栏底部与设置页可以同时渲染,因此每个 SVG 使用独立的渐变标识。
104
+ const flameId = (0, react_1.useId)();
105
+ return ((0, jsx_runtime_1.jsxs)("svg", { className: size === undefined ? 'vac-foot-mark' : undefined, width: size, height: size, viewBox: "32 29 232 259", "aria-hidden": "true", focusable: "false", children: [(0, jsx_runtime_1.jsx)("defs", { children: (0, jsx_runtime_1.jsxs)("linearGradient", { id: flameId, x1: "0%", y1: "100%", x2: "100%", y2: "0%", children: [(0, jsx_runtime_1.jsx)("stop", { offset: "0%", stopColor: "#FF6527" }), (0, jsx_runtime_1.jsx)("stop", { offset: "100%", stopColor: "#F44E1C" })] }) }), (0, jsx_runtime_1.jsx)("path", { d: "M 2 0 L 84 0 C 88 0 91 1 95 5 L 153 76 C 159 83 159 95 153 102 L 95 173 C 91 177 88 178 84 178 L 2 178 L 78 89 Z", fill: "currentColor", transform: "translate(30 110)" }), (0, jsx_runtime_1.jsx)("path", { d: "M 73 0 C 64 14 51 28 38 40 C 21 55 7 68 2 87 C -2 107 8 129 25 150 C 39 134 53 119 67 107 C 82 95 91 79 93 60 C 95 40 86 18 73 0 Z", fill: `url(#${flameId})`, transform: "translate(169 29)" })] }));
116
106
  }
117
107
 
118
108
  },
@@ -140,8 +130,8 @@ window.__ModuleLoader__.load({
140
130
  'account.signedIn': '已登录',
141
131
  'account.expires': '凭据有效期至 {expires}',
142
132
  'account.reauth': '凭据轮换中断,需要重新登录。',
143
- 'account.waiting': '已打开授权页,批准后自动完成登录。',
144
- 'account.openAgain': '重新打开授权页',
133
+ 'account.waiting': '等待浏览器授权,批准后自动完成登录。',
134
+ 'account.openAgain': '打开授权页',
145
135
  'account.cancel': '取消登录',
146
136
  'account.signOut': '退出登录',
147
137
  'account.failed': '登录失败:{error}',
@@ -153,7 +143,7 @@ window.__ModuleLoader__.load({
153
143
  'footer.chip.global': '国际',
154
144
  'footer.chip.cn': '中国',
155
145
  'footer.waiting': '等待授权…',
156
- 'footer.reopen': '重开',
146
+ 'footer.reopen': '授权页',
157
147
  'footer.cancel': '取消',
158
148
  'footer.reauth': '需要重新登录',
159
149
  'footer.failed': '登录失败',
@@ -175,8 +165,8 @@ window.__ModuleLoader__.load({
175
165
  'account.signedIn': 'Signed in',
176
166
  'account.expires': 'Credential valid until {expires}',
177
167
  'account.reauth': 'A credential rotation was interrupted; sign in again.',
178
- 'account.waiting': 'The approval page is open; sign-in completes once you approve.',
179
- 'account.openAgain': 'Open the approval page again',
168
+ 'account.waiting': 'Approve in your browser to finish signing in.',
169
+ 'account.openAgain': 'Open approval page',
180
170
  'account.cancel': 'Cancel sign-in',
181
171
  'account.signOut': 'Sign out',
182
172
  'account.failed': 'Sign-in failed: {error}',
@@ -188,7 +178,7 @@ window.__ModuleLoader__.load({
188
178
  'footer.chip.global': 'Global',
189
179
  'footer.chip.cn': 'China',
190
180
  'footer.waiting': 'Waiting for approval…',
191
- 'footer.reopen': 'Reopen',
181
+ 'footer.reopen': 'Approve',
192
182
  'footer.cancel': 'Cancel',
193
183
  'footer.reauth': 'Sign in again',
194
184
  'footer.failed': 'Sign-in failed',
@@ -246,6 +236,12 @@ window.__ModuleLoader__.load({
246
236
  locale: i18n_1.NAMESPACE,
247
237
  inject: () => ({ view }),
248
238
  }, ({ t, view: injected }) => (0, jsx_runtime_1.jsx)(page_1.AccountPage, { view: injected, t: t })));
239
+ // dsh 0.2.2 declares this optional slot. The injection waits on older hosts.
240
+ // dsh 0.2.2 声明了此可选插槽,旧版宿主上的注入会保持等待。
241
+ ctx.slots.inject('settings.section.icon', () => ctx.slots.register({
242
+ name: 'settings.section.icon',
243
+ id: shared_1.SETTINGS_SECTION,
244
+ }, footer_1.VulcanMark));
249
245
  // Above Settings in the sidebar foot.
250
246
  ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({
251
247
  name: 'sidebar.footer.action',
@@ -296,23 +292,6 @@ window.__ModuleLoader__.load({
296
292
  view.refresh();
297
293
  });
298
294
  };
299
- const signIn = (site) => {
300
- // Opened during the click, so no popup blocker stops it, and pointed at the page once known.
301
- const popup = window.open('about:blank', '_blank');
302
- act(async () => {
303
- try {
304
- const { approvalUrl } = await (0, shared_1.callAccount)('POST', `signin?site=${site}`);
305
- if (popup !== null) {
306
- popup.opener = null;
307
- popup.location.href = approvalUrl;
308
- }
309
- }
310
- catch (failure) {
311
- popup?.close();
312
- throw failure;
313
- }
314
- });
315
- };
316
295
  let body;
317
296
  if (live === undefined)
318
297
  body = (0, jsx_runtime_1.jsx)("p", { className: "vac-hint", children: t('loading') });
@@ -323,13 +302,13 @@ window.__ModuleLoader__.load({
323
302
  body = ((0, jsx_runtime_1.jsxs)("div", { className: "vac-between", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("div", { className: "vac-inline", children: [(0, jsx_runtime_1.jsx)("span", { className: "vac-badge ok", children: t('account.signedIn') }), (0, jsx_runtime_1.jsx)("strong", { children: t(`account.site.${account.site ?? 'global'}`) }), (0, jsx_runtime_1.jsx)("span", { className: "vac-hint vac-mono", children: shared_1.SITES.find(item => item.site === account.site)?.domain })] }), account.expiresAt === undefined ? null : (0, jsx_runtime_1.jsx)("p", { className: "vac-hint", children: (0, i18n_1.fill)(t('account.expires'), { expires: when(account.expiresAt) }) })] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "vac-btn", disabled: busy, onClick: () => { act(() => (0, shared_1.callAccount)('POST', 'signout')); }, children: t('account.signOut') })] }));
324
303
  }
325
304
  else if (live.signIn !== null) {
326
- body = ((0, jsx_runtime_1.jsxs)("div", { className: "vac-between", children: [(0, jsx_runtime_1.jsx)("span", { children: t('account.waiting') }), (0, jsx_runtime_1.jsxs)("span", { className: "vac-inline", children: [(0, jsx_runtime_1.jsx)("a", { className: "vac-link", href: live.signIn.approvalUrl, target: "_blank", rel: "noopener noreferrer", children: t('account.openAgain') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "vac-btn sm", disabled: busy, onClick: () => { act(() => (0, shared_1.callAccount)('POST', 'signin/cancel')); }, children: t('account.cancel') })] })] }));
305
+ body = ((0, jsx_runtime_1.jsxs)("div", { className: "vac-between", children: [(0, jsx_runtime_1.jsx)("span", { role: "status", children: t('account.waiting') }), (0, jsx_runtime_1.jsxs)("span", { className: "vac-inline", children: [(0, jsx_runtime_1.jsx)("a", { className: "vac-link", href: live.signIn.approvalUrl, target: "_blank", rel: "noopener noreferrer", children: t('account.openAgain') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "vac-btn sm", disabled: busy, onClick: () => { act(() => (0, shared_1.callAccount)('POST', 'signin/cancel')); }, children: t('account.cancel') })] })] }));
327
306
  }
328
307
  else {
329
- body = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [live.account.reauthRequired === true ? (0, jsx_runtime_1.jsx)("div", { className: "vac-alert warn", children: t('account.reauth') }) : null, (0, jsx_runtime_1.jsx)("div", { className: "vac-sites", children: shared_1.SITES.map(item => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "vac-site", disabled: busy, onClick: () => { signIn(item.site); }, children: [(0, jsx_runtime_1.jsx)("strong", { children: t(`account.site.${item.site}`) }), (0, jsx_runtime_1.jsx)("span", { children: item.domain }), (0, jsx_runtime_1.jsxs)("span", { style: { fontFamily: 'inherit', color: 'var(--vk-signal)' }, children: [t('account.signIn'), " \u2192"] })] }, item.site))) })] }));
308
+ body = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [live.account.reauthRequired === true ? (0, jsx_runtime_1.jsx)("div", { className: "vac-alert warn", children: t('account.reauth') }) : null, (0, jsx_runtime_1.jsx)("div", { className: "vac-sites", children: shared_1.SITES.map(item => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "vac-site", disabled: busy, onClick: () => { act(() => (0, shared_1.startAccountSignIn)(item.site)); }, children: [(0, jsx_runtime_1.jsx)("strong", { children: t(`account.site.${item.site}`) }), (0, jsx_runtime_1.jsx)("span", { children: item.domain }), (0, jsx_runtime_1.jsxs)("span", { style: { fontFamily: 'inherit', color: 'var(--vk-signal)' }, children: [t('account.signIn'), " \u2192"] })] }, item.site))) })] }));
330
309
  }
331
310
  const failure = live?.signInError === undefined ? error : (0, i18n_1.fill)(t('account.failed'), { error: live.signInError });
332
- return ((0, jsx_runtime_1.jsxs)("div", { className: "vac vac-page", children: [(0, jsx_runtime_1.jsxs)("div", { className: "vac-page-head", children: [(0, jsx_runtime_1.jsx)("h2", { children: t('page.title') }), (0, jsx_runtime_1.jsx)("p", { className: "vac-lead", children: t('page.lead') })] }), (0, jsx_runtime_1.jsxs)("section", { className: "vac-section", children: [(0, jsx_runtime_1.jsx)("div", { className: "vac-section-head", children: (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h3", { className: "vac-section-title", children: t('account.title') }), live?.account.signedIn === true ? null : (0, jsx_runtime_1.jsx)("p", { className: "vac-section-lead", children: t('account.lead') })] }) }), (0, jsx_runtime_1.jsxs)("div", { className: "vac-section-body", children: [body, failure === undefined ? null : (0, jsx_runtime_1.jsx)("p", { className: "vac-error", role: "alert", children: failure })] })] })] }));
311
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "vac vac-page", "data-vulcan-settings-width": "readable", children: [(0, jsx_runtime_1.jsxs)("header", { className: "vac-page-head", "data-vulcan-settings-header": true, children: [(0, jsx_runtime_1.jsx)("h2", { children: t('page.title') }), (0, jsx_runtime_1.jsx)("p", { className: "vac-lead", children: t('page.lead') })] }), (0, jsx_runtime_1.jsxs)("section", { className: "vac-section", children: [(0, jsx_runtime_1.jsx)("div", { className: "vac-section-head", children: (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h3", { className: "vac-section-title", children: t('account.title') }), live?.account.signedIn === true ? null : (0, jsx_runtime_1.jsx)("p", { className: "vac-section-lead", children: t('account.lead') })] }) }), (0, jsx_runtime_1.jsxs)("div", { className: "vac-section-body", children: [body, failure === undefined ? null : (0, jsx_runtime_1.jsx)("p", { className: "vac-error", role: "alert", children: failure })] })] })] }));
333
312
  }
334
313
  /** The suite's entry on the Plugins page: what it holds, and the way to its settings. */
335
314
  function PluginPageSummary({ view, t, open }) {
@@ -349,6 +328,7 @@ window.__ModuleLoader__.load({
349
328
  Object.defineProperty(exports, "__esModule", { value: true });
350
329
  exports.HostWatch = exports.SITES = exports.GUARD_SECTION = exports.SUITE_PACKAGE = exports.SETTINGS_SECTION = exports.PACKAGE_NAME = void 0;
351
330
  exports.callAccount = callAccount;
331
+ exports.startAccountSignIn = startAccountSignIn;
352
332
  exports.messageOf = messageOf;
353
333
  /** Checked against `package.json` by `scripts/bundle-client.mjs`. */
354
334
  exports.PACKAGE_NAME = '@openvulcan/dsh-vulcan-account';
@@ -372,6 +352,34 @@ window.__ModuleLoader__.load({
372
352
  throw new Error(answer?.error?.message ?? `HTTP ${response.status}`);
373
353
  return answer.value;
374
354
  }
355
+ /**
356
+ * Start device sign-in for the selected site and request its authorization page. Resolves after
357
+ * asking the browser or desktop shell to open it, and rejects if the sign-in request fails.
358
+ * 为所选站点发起设备登录并请求打开授权页。向浏览器或桌面壳发出打开请求后完成,登录请求失败时拒绝。
359
+ */
360
+ async function startAccountSignIn(site) {
361
+ // A browser needs the click to open a tab before the asynchronous sign-in request.
362
+ // 浏览器需要利用点击手势在异步登录请求之前打开标签页。
363
+ const popup = window.open('about:blank', '_blank');
364
+ try {
365
+ // The account service validates the approval URL against the selected site and sign-in state.
366
+ // 账号服务会根据所选站点和登录状态验证授权地址。
367
+ const { approvalUrl } = await callAccount('POST', `signin?site=${site}`);
368
+ if (popup === null) {
369
+ // dsh Desktop denies the blank tab and sends HTTP(S) URLs directly to the system browser.
370
+ // dsh 桌面端拒绝空白标签页,并将 HTTP(S) 地址直接交给系统浏览器。
371
+ window.open(approvalUrl, '_blank', 'noopener,noreferrer');
372
+ }
373
+ else if (!popup.closed) {
374
+ popup.opener = null;
375
+ popup.location.replace(approvalUrl);
376
+ }
377
+ }
378
+ catch (failure) {
379
+ popup?.close();
380
+ throw failure;
381
+ }
382
+ }
375
383
  function messageOf(error) {
376
384
  return error instanceof Error ? error.message : String(error);
377
385
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openvulcan/dsh-vulcan-account",
3
- "version": "0.4.0",
3
+ "version": "0.4.3",
4
4
  "description": "OpenVulcan account for DeepSeek Harness: sign in once, used by every VulcanCode plugin.",
5
5
  "license": "MIT",
6
6
  "icon": "./assets/account-icon.png",
@@ -42,7 +42,7 @@
42
42
  }
43
43
  },
44
44
  "dependencies": {
45
- "@openvulcan/openvulcan-account": "0.4.0"
45
+ "@openvulcan/openvulcan-account": "0.4.3"
46
46
  },
47
47
  "peerDependencies": {
48
48
  "@deepseek-ai/cordis": "^4.0.5",