dsh-config-manager 0.1.86 → 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/lib/cli/web/page.js +2 -2
- package/lib/client.d.ts +86 -1
- package/lib/client.js +2371 -778
- package/lib/core/native-path-picker.d.ts +49 -0
- package/lib/core/native-path-picker.js +475 -0
- package/lib/core/path-browse.d.ts +42 -0
- package/lib/core/path-browse.js +134 -0
- package/lib/foreign/claude-sessions.d.ts +1 -1
- package/lib/foreign/claude-sessions.js +1 -1
- package/lib/index.d.ts +2 -2
- package/lib/index.js +3 -3
- package/lib/market/security.js +1 -1
- package/lib/market/types.d.ts +1 -1
- package/lib/market/types.js +1 -1
- package/lib/routes/index.d.ts +1 -1
- package/lib/routes/index.js +2 -0
- package/lib/routes/kit.d.ts +2 -2
- package/lib/routes/path-browse.d.ts +3 -0
- package/lib/routes/path-browse.js +63 -0
- package/lib/security/secret-scanner.js +1 -1
- package/lib/ui/sync-settings-view.d.ts +18 -0
- package/lib/ui/sync-settings-view.js +20 -0
- package/package.json +2 -2
- package/src/cli/web/page.ts +2 -2
- package/src/client/ConfigManagerSection.tsx +20 -17
- package/src/client/api.ts +60 -1
- package/src/client/bundle-selfcontained.test.ts +9 -0
- package/src/client/chat/chat-entry-view.test.ts +1 -1
- package/src/client/chat/rollback-entry-view.test.ts +14 -0
- package/src/client/chat/rollback-entry-view.ts +2 -1
- package/src/client/common/CopyButton.tsx +21 -2
- package/src/client/common/Icon.tsx +7 -0
- package/src/client/common/routes.ts +5 -0
- package/src/client/config-manager.module.css +70 -0
- package/src/client/import/ImportWizardView.tsx +1 -0
- package/src/client/import/PathMappingForm.tsx +62 -8
- package/src/client/import/PathPickerDialog.tsx +172 -0
- package/src/client/import/dsh-directory-pick.test.ts +55 -0
- package/src/client/import/dsh-directory-pick.ts +55 -0
- package/src/client/import/import-wizard-steps.tsx +5 -2
- package/src/client/index.ts +12 -1
- package/src/client/locales.ts +25 -0
- package/src/client/lucide-icons.d.ts +2 -0
- package/src/client/sync/SyncPanel.tsx +4 -4
- package/src/client/sync/sync-view.ts +1 -1
- package/src/core/native-path-picker.test.ts +118 -0
- package/src/core/native-path-picker.ts +517 -0
- package/src/core/path-browse.test.ts +88 -0
- package/src/core/path-browse.ts +166 -0
- package/src/foreign/claude-sessions.ts +3 -3
- package/src/index.ts +3 -3
- package/src/market/security.ts +1 -1
- package/src/market/types.ts +1 -1
- package/src/routes/index.ts +3 -1
- package/src/routes/kit.ts +2 -2
- package/src/routes/path-browse.ts +70 -0
- package/src/security/secret-scanner.test.ts +1 -1
- package/src/security/secret-scanner.ts +1 -1
- package/src/ui/session-issue-labels.ts +1 -1
- package/src/ui/sync-settings-view.test.ts +18 -1
- package/src/ui/sync-settings-view.ts +21 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本机目录列举(宿主半)—— 「路径映射 → 选择本机路径」弹窗的唯一数据源。
|
|
3
|
+
*
|
|
4
|
+
* 为什么要有它
|
|
5
|
+
* ------------
|
|
6
|
+
* 导入向导的路径映射要求用户为一堆**绝对路径**指定本机新位置,而此前只能凭记忆手打
|
|
7
|
+
* (浏览器拿不到真实路径:File System Access API 只给 handle,`<input type="file">` 只给文件名)。
|
|
8
|
+
* 于是「路径选不对 = 导不进」全落在用户的打字准确性上。这里把「列目录」做成宿主能力,
|
|
9
|
+
* 浏览器只负责画界面。
|
|
10
|
+
*
|
|
11
|
+
* 边界
|
|
12
|
+
* ----
|
|
13
|
+
* - **只读**:只 readdir/stat,绝不创建、改写、删除任何东西;
|
|
14
|
+
* - **不产出文案**:`error` 是枚举码(not-found / denied / failed / not-absolute),
|
|
15
|
+
* 用户可见句子由客户端按字典解析(i18n 硬规则:引擎不写中文);
|
|
16
|
+
* - 有界:单次列举最多 {@link BROWSE_ENTRY_LIMIT} 条并回报 `truncated`,盘符探测逐根带硬超时
|
|
17
|
+
* (离线光驱/断开的网络盘 stat 会挂住,路由不许被一个 panic 盘符拖死)。
|
|
18
|
+
*/
|
|
19
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
20
|
+
import { dirname, join, parse } from 'node:path';
|
|
21
|
+
import { homedir } from 'node:os';
|
|
22
|
+
/** 单次列举上限:够翻一个源码目录,又不至于把响应体撑成几 MB。 */
|
|
23
|
+
export const BROWSE_ENTRY_LIMIT = 500;
|
|
24
|
+
/** 盘符/根探测的单项硬超时(ms):断开的网络盘 stat 会长时间不返回。 */
|
|
25
|
+
export const ROOT_PROBE_TIMEOUT_MS = 400;
|
|
26
|
+
/** 带硬超时的 Promise(只为本模块的盘符探测服务;超时后原 promise 的迟到结果被丢弃)。 */
|
|
27
|
+
function withDeadline(promise, ms) {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const timer = setTimeout(() => { reject(new Error('timeout')); }, ms);
|
|
30
|
+
promise.then((value) => { clearTimeout(timer); resolve(value); }, (error) => { clearTimeout(timer); reject(error); });
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/** 一个路径是不是「根」(Windows 盘符根 / POSIX 的 `/`)。 */
|
|
34
|
+
function isRoot(target) {
|
|
35
|
+
return parse(target).root === target;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 本机根列表:Windows 上是**探测得到的**盘符根(不硬编码 C:),其余平台是 `/`。
|
|
39
|
+
* 探测逐根 400ms 超时;全部失败时至少给出 `parse(process.cwd()).root`,界面不至于空成白板。
|
|
40
|
+
*/
|
|
41
|
+
export async function listRoots() {
|
|
42
|
+
if (process.platform !== 'win32') {
|
|
43
|
+
return [{ name: '/', path: '/', kind: 'dir' }];
|
|
44
|
+
}
|
|
45
|
+
const letters = 'CDEFGHIJKLMNOPQRSTUVWXYZAB'.split('');
|
|
46
|
+
const probed = await Promise.all(letters.map(async (letter) => {
|
|
47
|
+
const root = `${letter}:\\`;
|
|
48
|
+
try {
|
|
49
|
+
const info = await withDeadline(stat(root), ROOT_PROBE_TIMEOUT_MS);
|
|
50
|
+
return info.isDirectory() ? root : null;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}));
|
|
56
|
+
const roots = probed.filter((root) => root !== null);
|
|
57
|
+
const fallback = parse(process.cwd()).root;
|
|
58
|
+
const ordered = roots.length > 0 ? roots : (fallback === '' ? [] : [fallback]);
|
|
59
|
+
return ordered.map((root) => ({ name: root, path: root, kind: 'dir' }));
|
|
60
|
+
}
|
|
61
|
+
/** 文件系统错误 → 原因码(只有 ENOENT 才叫「不存在」)。 */
|
|
62
|
+
function errorCodeOf(error) {
|
|
63
|
+
const code = error?.code;
|
|
64
|
+
if (code === 'ENOENT' || code === 'ENOTDIR')
|
|
65
|
+
return 'not-found';
|
|
66
|
+
if (code === 'EACCES' || code === 'EPERM')
|
|
67
|
+
return 'denied';
|
|
68
|
+
return 'failed';
|
|
69
|
+
}
|
|
70
|
+
/** 名字排序:目录在前、同类按名字字典序(不依赖 locale,测试可复现)。 */
|
|
71
|
+
function compareEntries(a, b) {
|
|
72
|
+
if (a.kind !== b.kind)
|
|
73
|
+
return a.kind === 'dir' ? -1 : 1;
|
|
74
|
+
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* 列出一个目录(缺省 = 根列表)。
|
|
78
|
+
*
|
|
79
|
+
* `target` 缺省或空串 = 根列表;凡不是绝对路径一律 `not-absolute`(不猜基准目录 —— 猜错就列出别人的东西)。
|
|
80
|
+
* @param options.limit 覆盖 {@link BROWSE_ENTRY_LIMIT}。
|
|
81
|
+
*/
|
|
82
|
+
export async function browseDirectory(target, options) {
|
|
83
|
+
const limit = Math.max(1, Math.trunc(options?.limit ?? BROWSE_ENTRY_LIMIT));
|
|
84
|
+
const home = homedir();
|
|
85
|
+
const wanted = (target ?? '').trim();
|
|
86
|
+
if (wanted === '') {
|
|
87
|
+
const roots = await listRoots();
|
|
88
|
+
return { path: '', parent: null, home, entries: roots.slice(0, limit), truncated: roots.length > limit, error: null };
|
|
89
|
+
}
|
|
90
|
+
if (!parse(wanted).root) {
|
|
91
|
+
return { path: wanted, parent: null, home, entries: [], truncated: false, error: 'not-absolute' };
|
|
92
|
+
}
|
|
93
|
+
const current = parse(wanted).root === wanted ? wanted : wanted.replace(/[\\/]+$/, '');
|
|
94
|
+
let dirents;
|
|
95
|
+
try {
|
|
96
|
+
dirents = await readdir(current, { withFileTypes: true });
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
return { path: current, parent: isRoot(current) ? null : dirname(current), home, entries: [], truncated: false, error: errorCodeOf(error) };
|
|
100
|
+
}
|
|
101
|
+
const entries = [];
|
|
102
|
+
for (const dirent of dirents) {
|
|
103
|
+
const entryPath = join(current, dirent.name);
|
|
104
|
+
if (dirent.isDirectory()) {
|
|
105
|
+
entries.push({ name: dirent.name, path: entryPath, kind: 'dir' });
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (dirent.isFile()) {
|
|
109
|
+
entries.push({ name: dirent.name, path: entryPath, kind: 'file' });
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
// 符号链接 / junction / 其它特殊项:跟随一次(读不到就整条丢掉,绝不猜类型)
|
|
113
|
+
try {
|
|
114
|
+
const info = await stat(entryPath);
|
|
115
|
+
if (info.isDirectory())
|
|
116
|
+
entries.push({ name: dirent.name, path: entryPath, kind: 'dir' });
|
|
117
|
+
else if (info.isFile())
|
|
118
|
+
entries.push({ name: dirent.name, path: entryPath, kind: 'file' });
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// 断链:不展示(点了也进不去)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
entries.sort(compareEntries);
|
|
125
|
+
return {
|
|
126
|
+
path: current,
|
|
127
|
+
parent: isRoot(current) ? null : dirname(current),
|
|
128
|
+
home,
|
|
129
|
+
entries: entries.slice(0, limit),
|
|
130
|
+
truncated: entries.length > limit,
|
|
131
|
+
error: null,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=path-browse.js.map
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
* ③ **sessionId 不一致**:`input.recordSessionId` 存在且 ≠ `input.id` → 非致命发现
|
|
38
38
|
* `session-id-mismatch`(会话 id 仍以文件名 stem 为准,见 `TranscodeResult.findings`)。
|
|
39
39
|
* ④ **推理块**(Claude Code 的 `thinking` / `redacted_thinking`):共享 IR 没有 reasoning
|
|
40
|
-
* 承载位(docs/
|
|
40
|
+
* 承载位(docs/maintenance/known-gaps.md F-01)→ 计报 `thinking`,**绝不伪装成正文**;非 Claude 原生的
|
|
41
41
|
* `{type:'reasoning',text:…}` 刻意保持既有口径(理由与切换方式见 `REASONING_BLOCK_TYPES`)。
|
|
42
42
|
*/
|
|
43
43
|
import type { Stats } from 'node:fs';
|
|
@@ -28,7 +28,7 @@ function usageField(usage, key) {
|
|
|
28
28
|
* 沿「对象带 text 才算文本」的兜底把它变成一个**空文本块** —— 内容静默丢失,且那个空块还会多插入
|
|
29
29
|
* 一个换行。本仓其余 20+ 来源一律「推理不伪装成正文、只计数」(read-crush / read-opencode /
|
|
30
30
|
* read-qwen / read-pi / read-openclaw …),这里对齐同一口径;共享 IR 无 reasoning 承载位这件事
|
|
31
|
-
* 记在 docs/
|
|
31
|
+
* 记在 docs/maintenance/known-gaps.md 的 F-01。
|
|
32
32
|
*
|
|
33
33
|
* **为什么不把 `{type:'reasoning', text:'…'}` 一并纳入**(如实记明,别当成漏掉):
|
|
34
34
|
* ① Claude Code 不写这种块(它是别的导出器的归一命名,各来源自己的解析器已各自计报);
|
package/lib/index.d.ts
CHANGED
|
@@ -77,7 +77,7 @@ export declare const name = "config-manager";
|
|
|
77
77
|
/** Services required before the engine can mount (present in every profile). */
|
|
78
78
|
export declare const inject: string[];
|
|
79
79
|
/** Plugin version, kept in sync with package.json ("version"). */
|
|
80
|
-
export declare const PLUGIN_VERSION = "0.
|
|
80
|
+
export declare const PLUGIN_VERSION = "0.2.0";
|
|
81
81
|
/**
|
|
82
82
|
* Star 引导弹窗指向的 GitHub 仓库(用户引导点 Star 的目标)。
|
|
83
83
|
* 与 package.json 的 repository 字段保持一致;界面不可改(硬编码,参照
|
|
@@ -130,7 +130,7 @@ export interface Config {
|
|
|
130
130
|
/**
|
|
131
131
|
* 本文件保留的 7 条路由的路径(被源码级守卫按文件窗口钉住,见下方 routesList 注释)。
|
|
132
132
|
*
|
|
133
|
-
* 其余
|
|
133
|
+
* 其余 76 条路由的路径**声明在各自的组文件里**(src/routes/*.ts 的 endpoint({ path, methods }))——
|
|
134
134
|
* 新增一条 API 只需那一条声明,不再有「常量表 + 路由对象」两处要同步。
|
|
135
135
|
*/
|
|
136
136
|
export declare const API: {
|
package/lib/index.js
CHANGED
|
@@ -136,7 +136,7 @@ export const name = 'config-manager';
|
|
|
136
136
|
/** Services required before the engine can mount (present in every profile). */
|
|
137
137
|
export const inject = ['settings', 'credentials'];
|
|
138
138
|
/** Plugin version, kept in sync with package.json ("version"). */
|
|
139
|
-
export const PLUGIN_VERSION = '0.
|
|
139
|
+
export const PLUGIN_VERSION = '0.2.0';
|
|
140
140
|
/** Plugin own package name — excluded from its own exported plugins list. */
|
|
141
141
|
const PLUGIN_NAME = 'dsh-config-manager';
|
|
142
142
|
/**
|
|
@@ -158,7 +158,7 @@ export const DEFAULT_GITHUB_CLIENT_ID = 'Ov23liq4i7n8UsylGRfb';
|
|
|
158
158
|
/**
|
|
159
159
|
* 本文件保留的 7 条路由的路径(被源码级守卫按文件窗口钉住,见下方 routesList 注释)。
|
|
160
160
|
*
|
|
161
|
-
* 其余
|
|
161
|
+
* 其余 76 条路由的路径**声明在各自的组文件里**(src/routes/*.ts 的 endpoint({ path, methods }))——
|
|
162
162
|
* 新增一条 API 只需那一条声明,不再有「常量表 + 路由对象」两处要同步。
|
|
163
163
|
*/
|
|
164
164
|
export const API = {
|
|
@@ -3027,7 +3027,7 @@ function makeRoutes(deps) {
|
|
|
3027
3027
|
/**
|
|
3028
3028
|
* 路由表:本文件只保留被源码级守卫按**文件窗口**钉住的 7 条(host-entry 审计 F-04 记录的
|
|
3029
3029
|
* tests/host/**、src/core/model-tools.test.ts、src/core/phase1-wiring.test.ts 的窗口断言)。
|
|
3030
|
-
* 其余
|
|
3030
|
+
* 其余 76 条已按域拆到 src/routes/*.ts,由 buildRoutes 组装。
|
|
3031
3031
|
* 注册顺序不影响匹配:命名路由必须互不相同(webServer 契约)。
|
|
3032
3032
|
*/
|
|
3033
3033
|
const routeEnv = makeRouteEnv();
|
package/lib/market/security.js
CHANGED
|
@@ -102,7 +102,7 @@ export function validateMarketItem(itemId, manifestRaw, zipBytes) {
|
|
|
102
102
|
if (!enabled)
|
|
103
103
|
continue;
|
|
104
104
|
// 市场条目禁止分区(BANNED_MARKET_SECTIONS:sessions 历史会话 / pluginFiles 任意文件 / self 本地环境):
|
|
105
|
-
// 与 secrets 同级硬约束(产品决策 2026-08-19,guide docs/
|
|
105
|
+
// 与 secrets 同级硬约束(产品决策 2026-08-19,guide docs/spec/market-index-v1.md)。
|
|
106
106
|
if (BANNED_MARKET_SECTIONS.includes(sectionId)) {
|
|
107
107
|
return { status: 'invalid', errors: [marketMsg('market.security.bannedSection', { section: sectionId })], warnings, manifest, internalManifest, sections: [], checksumsOk: true };
|
|
108
108
|
}
|
package/lib/market/types.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export type MarketPublishMode = 'migrate' | 'share';
|
|
|
26
26
|
*/
|
|
27
27
|
export declare const MAX_MARKET_ZIP_BYTES: number;
|
|
28
28
|
/**
|
|
29
|
-
* 市场条目禁止携带的分区(产品决策 2026-08-19,guide docs/
|
|
29
|
+
* 市场条目禁止携带的分区(产品决策 2026-08-19,guide docs/spec/market-index-v1.md):
|
|
30
30
|
* - sessions:历史会话,含个人交互记录/上下文;
|
|
31
31
|
* - pluginFiles:任意文件直通(目录放啥带啥),无内容过滤,最易泄漏 token/密钥文件;
|
|
32
32
|
* - self:本地环境专属(sync 通道 URL / WebDAV 地址 / 市场配置 / UI 偏好),无分享价值且泄漏环境信息。
|
package/lib/market/types.js
CHANGED
|
@@ -8,7 +8,7 @@ export const MARKET_ITEM_SCHEMA_VERSION = 1;
|
|
|
8
8
|
*/
|
|
9
9
|
export const MAX_MARKET_ZIP_BYTES = 64 * 1024 * 1024;
|
|
10
10
|
/**
|
|
11
|
-
* 市场条目禁止携带的分区(产品决策 2026-08-19,guide docs/
|
|
11
|
+
* 市场条目禁止携带的分区(产品决策 2026-08-19,guide docs/spec/market-index-v1.md):
|
|
12
12
|
* - sessions:历史会话,含个人交互记录/上下文;
|
|
13
13
|
* - pluginFiles:任意文件直通(目录放啥带啥),无内容过滤,最易泄漏 token/密钥文件;
|
|
14
14
|
* - self:本地环境专属(sync 通道 URL / WebDAV 地址 / 市场配置 / UI 偏好),无分享价值且泄漏环境信息。
|
package/lib/routes/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 顺序对匹配无影响:DSH webServer 的契约是「命名路由必须互不相同,注册顺序不影响请求」
|
|
5
5
|
* (见 @deepseek-ai/dsh-host-webserver 的 WebServer 文档)。src/index.ts 保留的 7 条被源码级
|
|
6
|
-
* 守卫按文件窗口钉住,其余
|
|
6
|
+
* 守卫按文件窗口钉住,其余 76 条在这里组装(总数 83:含 path-browse)。
|
|
7
7
|
*/
|
|
8
8
|
import type { WebRoute } from './kit.ts';
|
|
9
9
|
import type { RoutesEnv } from './context.ts';
|
package/lib/routes/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { meRoutes } from './me.js';
|
|
|
11
11
|
import { historyRoutes } from './history.js';
|
|
12
12
|
import { recoveryRoutes } from './recovery.js';
|
|
13
13
|
import { sessionExportRoutes } from './session-export.js';
|
|
14
|
+
import { pathBrowseRoutes } from './path-browse.js';
|
|
14
15
|
export function buildRoutes(env) {
|
|
15
16
|
return [
|
|
16
17
|
...importRoutes(env),
|
|
@@ -26,6 +27,7 @@ export function buildRoutes(env) {
|
|
|
26
27
|
...historyRoutes(env),
|
|
27
28
|
...recoveryRoutes(env),
|
|
28
29
|
...sessionExportRoutes(env),
|
|
30
|
+
...pathBrowseRoutes(env),
|
|
29
31
|
];
|
|
30
32
|
}
|
|
31
33
|
//# sourceMappingURL=index.js.map
|
package/lib/routes/kit.d.ts
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* - 「每条路由首行 guard」这条安全不变量此前**只写在注释里**,零测试覆盖;
|
|
9
9
|
* - 3 条 prefix 路由自造围栏(裸 isLoopbackRequest + 自判方法)。
|
|
10
10
|
*
|
|
11
|
-
* 计数口径(三路一致 =
|
|
12
|
-
* 路由快照
|
|
11
|
+
* 计数口径(三路一致 = 83,勿再照抄旧审计初稿的计数 —— 那是计数口径 overcount):
|
|
12
|
+
* 路由快照 83 条 = 现行 `endpoint()` 声明 83 条 = 运行期 `buildRoutes()` 76 条 + `src/index.ts` 保留的 7 条。
|
|
13
13
|
* 增删路由后必须同步这段计数、`src/routes/index.ts` 的头注释与 parity 快照清单;`tests/route/route-parity.test.ts`
|
|
14
14
|
* 的「路由计数散文」守卫会拿真实声明数逐条对账(写错即红灯)。
|
|
15
15
|
*
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 路由组:本机路径浏览(导入向导「路径映射」→ 选择本机路径弹窗的唯一数据源)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要一条宿主路由:浏览器拿不到用户机器的真实目录树(File System Access API 只给 handle、
|
|
5
|
+
* `<input type="file">` 只给文件名),而路径映射要求为一堆绝对路径逐个指定本机新位置。
|
|
6
|
+
* 没有这条路由,用户只能凭记忆手打,打错就是「导不进」。
|
|
7
|
+
*
|
|
8
|
+
* 语义与边界
|
|
9
|
+
* ----------
|
|
10
|
+
* - **只读**:readdir/stat,绝不创建/改写/删除;不读文件内容;
|
|
11
|
+
* - POST 形态只**弹系统对话框**(core/native-path-picker.ts),同样不碰盘面;不可用时回
|
|
12
|
+
* `reason:'unsupported'|'failed'`,客户端回落 GET 的内置浏览器 —— 对话框不是用户的操作死路;
|
|
13
|
+
* - 空 `path` = 「此电脑」根列表(Windows 盘符探测,见 core/path-browse.ts);
|
|
14
|
+
* - 失败用枚举码回报(`error: 'not-found'|'denied'|'failed'|'not-absolute'`,成功为 null),
|
|
15
|
+
* 用户可见句子由客户端按字典解析 —— 引擎不写中文;
|
|
16
|
+
* - 有界:单次列举 500 条上限 + truncated 标记;整体套 withTimeout,不让一个 panic 盘符拖住请求。
|
|
17
|
+
*/
|
|
18
|
+
import { endpoint, errorMessage, queryParam, readJsonBody, writeJson } from './kit.js';
|
|
19
|
+
import { browseDirectory } from '../core/path-browse.js';
|
|
20
|
+
import { pickNativeDirectory } from '../core/native-path-picker.js';
|
|
21
|
+
import { withTimeout } from './route-support.js';
|
|
22
|
+
/** 单次列举的整体时限:正常目录远快于此;网络盘悬挂时宁可报错也不让请求挂着。 */
|
|
23
|
+
const BROWSE_TIMEOUT_MS = 10_000;
|
|
24
|
+
/** POST 可透传文本的长度上限:只用来当对话框的**起点**与**标题**,都不该是长文本。 */
|
|
25
|
+
const PICK_TEXT_MAX = 512;
|
|
26
|
+
/** 取请求体里的短文本字段:非字符串/空串/超长一律视为未提供(对话框走缺省值,不报错)。 */
|
|
27
|
+
function pickText(body, key) {
|
|
28
|
+
const value = body?.[key];
|
|
29
|
+
if (typeof value !== 'string')
|
|
30
|
+
return undefined;
|
|
31
|
+
const trimmed = value.trim();
|
|
32
|
+
return trimmed === '' || trimmed.length > PICK_TEXT_MAX ? undefined : trimmed;
|
|
33
|
+
}
|
|
34
|
+
export function pathBrowseRoutes(_env) {
|
|
35
|
+
return [
|
|
36
|
+
// ---------------------------------------------------- path-browse
|
|
37
|
+
// GET ?path=<绝对路径>:列一层目录(目录在前)。path 缺省 = 根列表。
|
|
38
|
+
// POST { startPath?, title? }:弹**系统原生**目录选择器(Windows IFileOpenDialog / macOS
|
|
39
|
+
// choose folder / Linux zenity·kdialog)。回 `{ ok:false, reason:'unsupported'|'failed' }`
|
|
40
|
+
// 时客户端回落到 GET 的内置浏览器 —— 同一条路径承载两种能力,避免为一个对话框新开一条路由。
|
|
41
|
+
// 标题由客户端按当前语言传进来(引擎不产中文)。
|
|
42
|
+
endpoint({ path: '/api/dsh-config-manager/path-browse', methods: ['GET', 'POST'] }, async (req, res) => {
|
|
43
|
+
if (req.method === 'POST') {
|
|
44
|
+
// 用户可能在系统对话框里逛几分钟 —— 时限由 core 的口子兜(10 分钟),此处不再叠加。
|
|
45
|
+
const body = await readJsonBody(req);
|
|
46
|
+
writeJson(res, 200, await pickNativeDirectory({
|
|
47
|
+
startPath: pickText(body, 'startPath'),
|
|
48
|
+
title: pickText(body, 'title'),
|
|
49
|
+
}));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
53
|
+
try {
|
|
54
|
+
const result = await withTimeout(browseDirectory(queryParam(url, 'path')), BROWSE_TIMEOUT_MS, 'path browse timed out');
|
|
55
|
+
writeJson(res, 200, result);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
writeJson(res, 500, { error: errorMessage(error) });
|
|
59
|
+
}
|
|
60
|
+
}),
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=path-browse.js.map
|
|
@@ -72,7 +72,7 @@ const BEARER_TOKEN_CHARS = '[A-Za-z0-9._~+/=-]';
|
|
|
72
72
|
* 于是 `bearer credentials are required` / `bearer authentication failed` / `BEARER HEADER NOT SET`
|
|
73
73
|
* 这类纯小写或纯大写的英文词**不命中**;而 `bearer abc12345`、`BEARER AbCdEfGh`、`bearer <24+ 字符>` 命中。
|
|
74
74
|
*
|
|
75
|
-
* 已知残余(登记在 `docs/
|
|
75
|
+
* 已知残余(登记在 `docs/maintenance/known-gaps.md` G-36):scheme 用非规范大小写**且** token 是 8–23 个**纯字母、
|
|
76
76
|
* 单一大小写**时本条不命中;规范 `Bearer …` 形态不受此限(上面那条 `bearer-token` 只要求 8+ 字符,行为不变)。
|
|
77
77
|
*/
|
|
78
78
|
const BEARER_TOKEN_ANY_CASE = new RegExp(BEARER_SCHEME_ANY_CASE +
|
|
@@ -511,4 +511,22 @@ export declare function workdirBacklog(workDir: {
|
|
|
511
511
|
};
|
|
512
512
|
error?: string;
|
|
513
513
|
} | undefined): WorkdirBacklog;
|
|
514
|
+
/**
|
|
515
|
+
* 「这一步该按哪个方向」—— 动作行只留一个 primary(issue 115:两个按钮并排、首次使用看不出该按哪个)。
|
|
516
|
+
*
|
|
517
|
+
* 判定只用**已确证**的远端事实,没证据就回落下行:一键同步先拉远端再三方合并、逐项确认,
|
|
518
|
+
* 不会拿本机默认值覆盖远端历史;上行推送才是产生新快照、改写远端历史的那个动作。
|
|
519
|
+
* 唯一让上行成为 primary 的情形是「远端确证零快照」(git 工作副本自检 remoteSnapshots === 0):
|
|
520
|
+
* 此时同步无事可做,用户要的就是第一次备份。判定入口收在本函数里(channel 一并传入),
|
|
521
|
+
* 组件不用在调用点再判一次通道 —— 判据只有一处。
|
|
522
|
+
*/
|
|
523
|
+
export declare function pickSyncPrimary(statusInfo: {
|
|
524
|
+
workDir?: {
|
|
525
|
+
facts?: {
|
|
526
|
+
ahead?: number | null;
|
|
527
|
+
remoteSnapshots?: number | null;
|
|
528
|
+
};
|
|
529
|
+
error?: string;
|
|
530
|
+
};
|
|
531
|
+
} | null | undefined, channel: string): 'push' | 'sync';
|
|
514
532
|
export declare function formatSyncUrlPreview(url: string, max?: number): string;
|
|
@@ -562,6 +562,26 @@ export function workdirBacklog(workDir) {
|
|
|
562
562
|
return { kind: 'never-synced', ahead };
|
|
563
563
|
return { kind: 'ahead', ahead, remoteSnapshots };
|
|
564
564
|
}
|
|
565
|
+
/**
|
|
566
|
+
* 「这一步该按哪个方向」—— 动作行只留一个 primary(issue 115:两个按钮并排、首次使用看不出该按哪个)。
|
|
567
|
+
*
|
|
568
|
+
* 判定只用**已确证**的远端事实,没证据就回落下行:一键同步先拉远端再三方合并、逐项确认,
|
|
569
|
+
* 不会拿本机默认值覆盖远端历史;上行推送才是产生新快照、改写远端历史的那个动作。
|
|
570
|
+
* 唯一让上行成为 primary 的情形是「远端确证零快照」(git 工作副本自检 remoteSnapshots === 0):
|
|
571
|
+
* 此时同步无事可做,用户要的就是第一次备份。判定入口收在本函数里(channel 一并传入),
|
|
572
|
+
* 组件不用在调用点再判一次通道 —— 判据只有一处。
|
|
573
|
+
*/
|
|
574
|
+
export function pickSyncPrimary(statusInfo, channel) {
|
|
575
|
+
// 只有 git 通道会做工作副本自检;别的通道没有这份事实,一律回落下行。
|
|
576
|
+
if (channel !== 'git')
|
|
577
|
+
return 'sync';
|
|
578
|
+
const backlog = workdirBacklog(statusInfo?.workDir);
|
|
579
|
+
if (backlog.kind === 'never-synced')
|
|
580
|
+
return 'push';
|
|
581
|
+
if (backlog.kind === 'clean' && backlog.remoteSnapshots === 0)
|
|
582
|
+
return 'push';
|
|
583
|
+
return 'sync';
|
|
584
|
+
}
|
|
565
585
|
export function formatSyncUrlPreview(url, max = 60) {
|
|
566
586
|
return url.slice(0, max);
|
|
567
587
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-config-manager",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Backup, restore, export, import, migrate and sync your DeepSeek Harness (DSH) configuration: settings, plugins, MCP servers, skills, agent presets and workspaces. One-click migration to another machine. DSH 配置一键备份、恢复、导出、导入、迁移与同步插件。",
|
|
6
6
|
"icon": "./icon.svg",
|
|
@@ -199,7 +199,7 @@
|
|
|
199
199
|
"lightningcss": "1.33.0",
|
|
200
200
|
"lucide": "^1.61.0",
|
|
201
201
|
"lucide-react": "^1.61.0",
|
|
202
|
-
"morphicons": "^
|
|
202
|
+
"morphicons": "^2.0.0",
|
|
203
203
|
"react": "^18.3.1",
|
|
204
204
|
"react-dom": "^18.3.1",
|
|
205
205
|
"schemastery": "^3.18.0",
|
package/src/cli/web/page.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* HTML:**零脚本、零外部资源、零构建产物**,配合 http.ts 里收紧的 CSP。
|
|
6
6
|
* 样式内联在 <style> 里(独立于插件 UI 的 CSS Modules;该新 pattern 已登记进 DESIGN.md)。
|
|
7
7
|
* 这套样式表**有一份外部出处**(亮色 IBM Carbon / 暗色 Raycast 阶梯)与逐条对照表:
|
|
8
|
-
* docs/
|
|
8
|
+
* docs/components/cli-rescue-console.md —— 改 STYLE 前先读它,改完跑 web.test.ts 的 R3-02。
|
|
9
9
|
*
|
|
10
10
|
* 呈现纪律:每个数字都要能回答「你怎么知道的」—— 离线拿不到的(结构化分区/凭据/同步令牌)
|
|
11
11
|
* 一律显式列出并说明原因,绝不显示为 0 或留空。
|
|
@@ -32,7 +32,7 @@ export interface PageChrome {
|
|
|
32
32
|
|
|
33
33
|
const STYLE = [
|
|
34
34
|
// ---- 设计 token:IBM Carbon(亮色语义四色)+ Raycast(暗色 surface 阶梯)----
|
|
35
|
-
// 映射表、取的章节与全部偏离记录:docs/
|
|
35
|
+
// 映射表、取的章节与全部偏离记录:docs/components/cli-rescue-console.md
|
|
36
36
|
// 文字色一律取「能读」的那一档:Carbon 的 #24a148 / #f1c21b 只做填充,正文用 #198038 / #8e6a00
|
|
37
37
|
// (AA 对比度由 web.test.ts 的 R3-02 逐对钉住,改色先过那条测试)。
|
|
38
38
|
':root{color-scheme:light dark;--font:system-ui,-apple-system,"Segoe UI",Roboto,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","Noto Sans SC",sans-serif;--mono:ui-monospace,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace;--fg:#161616;--muted:#525252;--bg:#f4f4f4;--card:#fff;--surface:#f4f4f4;--line:#e0e0e0;--accent:#0f62fe;--accent-hover:#0043ce;--on-accent:#fff;--ok:#198038;--warn:#8e6a00;--bad:#da1e28;--scrim:rgba(22,22,22,.45)}',
|
|
@@ -165,15 +165,6 @@ const navLayoutResult = navWidths === null
|
|
|
165
165
|
*/
|
|
166
166
|
const [libraryPending, setLibraryPending] = useState<ArtifactPendingAction | null>(null)
|
|
167
167
|
|
|
168
|
-
/**
|
|
169
|
-
* 导入面板当前是否停在「从其它 agent 导入」这一页(t17)。
|
|
170
|
-
*
|
|
171
|
-
* 为什么是壳层 state 而不是 runStore:它与「导入面板」同生命周期,且**不该被持久化** ——
|
|
172
|
-
* 刷新后重放一个「正在选来源」的界面只会骗人(来源检测结果要重新拉)。
|
|
173
|
-
* 关面板 / 切走即随组件卸载归零,与 libraryAction 同一档语义。
|
|
174
|
-
*/
|
|
175
|
-
const [foreignImportOpen, setForeignImportOpen] = useState(false)
|
|
176
|
-
|
|
177
168
|
/* ---------------- m-star-prompt:Star 引导弹窗(保持既有能力) ---------------- */
|
|
178
169
|
const [starPromptOpen, setStarPromptOpen] = useState(false)
|
|
179
170
|
/** 弹窗展示的 GitHub 仓库地址(GET /star-prompt 返回;不落 store) */
|
|
@@ -441,7 +432,7 @@ const navLayoutResult = navWidths === null
|
|
|
441
432
|
case 'market.publish.open': openTask('market', { subView: 'myconfigs' }); return
|
|
442
433
|
// 维护与诊断是全屏子视图:命令直达它,而不是只把用户送到环境页
|
|
443
434
|
// 从其它 agent 导入:开导入面板并停在来源选择(与首页工具栏同一路径)
|
|
444
|
-
case 'import.foreign': openTask('import'
|
|
435
|
+
case 'import.foreign': openTask('import', { foreign: true }); return
|
|
445
436
|
case 'maintenance.open': openMaintenance(); return
|
|
446
437
|
case 'activity.open': openTask('runs'); return
|
|
447
438
|
case 'history.open': openTask('history'); return
|
|
@@ -468,10 +459,7 @@ const navLayoutResult = navWidths === null
|
|
|
468
459
|
t={t}
|
|
469
460
|
syncT={syncT}
|
|
470
461
|
// 直达来源选择:开导入面板 + 把面板切到「从其它 agent 导入」那一步
|
|
471
|
-
openForeignImport={() => {
|
|
472
|
-
openTask('import')
|
|
473
|
-
setForeignImportOpen(true)
|
|
474
|
-
}}
|
|
462
|
+
openForeignImport={() => { openTask('import', { foreign: true }) }}
|
|
475
463
|
/>
|
|
476
464
|
)
|
|
477
465
|
break
|
|
@@ -638,7 +626,7 @@ const navLayoutResult = navWidths === null
|
|
|
638
626
|
{/* 导入与逛市场都是多阶段流程(§1),同样由本面板承载。
|
|
639
627
|
此前只渲染了 export / runs / history / about —— 从产物库点「导入」会开到**一个空面板**,
|
|
640
628
|
而导入其实在页面那条路径里跑(返回后正好看到它),两处状态分叉。 */}
|
|
641
|
-
{activeTask.kind === 'import' && (
|
|
629
|
+
{activeTask.kind === 'import' && (importTaskForeign(activeTask.payload) ? (
|
|
642
630
|
/* 「从其它 agent 导入」(t17):页面级步骤,复用同一块面板 ——
|
|
643
631
|
与既有导入向导并列,避免「弹窗套向导」。产包成功后把 zipPath 写进
|
|
644
632
|
runStore.library.pendingZip(产物库「一键导入」那条已验证的通道),
|
|
@@ -648,8 +636,9 @@ const navLayoutResult = navWidths === null
|
|
|
648
636
|
t={t}
|
|
649
637
|
uiT={uiT}
|
|
650
638
|
onReady={(result) => {
|
|
651
|
-
|
|
639
|
+
// 产包成功 = 这一页的使命结束:入参改回「向导」,并把 zipPath 交给既有导入通道
|
|
652
640
|
runStore.patch({
|
|
641
|
+
task: { ...activeTask, payload: { foreign: false } },
|
|
653
642
|
library: {
|
|
654
643
|
pendingZip: { zipPath: result.zipPath, name: result.name },
|
|
655
644
|
},
|
|
@@ -661,7 +650,8 @@ const navLayoutResult = navWidths === null
|
|
|
661
650
|
api={api}
|
|
662
651
|
t={t}
|
|
663
652
|
uiT={uiT}
|
|
664
|
-
|
|
653
|
+
// 向导 → 来源选择:同样是改 Task 入参(面板状态只有一个事实源)
|
|
654
|
+
onOpenForeignImport={() => { runStore.patch({ task: { ...activeTask, payload: { foreign: true } } }) }}
|
|
665
655
|
/>
|
|
666
656
|
))}
|
|
667
657
|
{activeTask.kind === 'market' && (
|
|
@@ -783,6 +773,19 @@ function marketTaskSubView(payload: unknown): 'browse' | 'myconfigs' | undefined
|
|
|
783
773
|
return v === 'myconfigs' || v === 'browse' ? v : undefined
|
|
784
774
|
}
|
|
785
775
|
|
|
776
|
+
/**
|
|
777
|
+
* Task 入参里取「导入面板停在来源选择」(t17「从其它 agent 导入」)。
|
|
778
|
+
*
|
|
779
|
+
* 为什么放进 Task 入参而不是壳层 useState(原 bug 的根因):壳层 state 与「导入面板」**不同**
|
|
780
|
+
* 生命周期 —— 关面板只是 `task: null`,壳层不重挂,于是「先点从其它 agent 导入 → 关掉 → 再点导入」
|
|
781
|
+
* 会带着旧的 true 进来,看到的是来源选择页而不是导入向导。入参随 Task 一起生灭(且 Task 本身
|
|
782
|
+
* 不持久化,见 run-store 的 `task: null`),开一个**新的**导入面板自然就是向导。
|
|
783
|
+
*/
|
|
784
|
+
function importTaskForeign(payload: unknown): boolean {
|
|
785
|
+
if (typeof payload !== 'object' || payload === null) return false
|
|
786
|
+
return (payload as { foreign?: unknown }).foreign === true
|
|
787
|
+
}
|
|
788
|
+
|
|
786
789
|
function marketTaskItemId(payload: unknown): string | undefined {
|
|
787
790
|
if (typeof payload !== 'object' || payload === null) return undefined
|
|
788
791
|
const id: unknown = (payload as { marketId?: unknown }).marketId
|
package/src/client/api.ts
CHANGED
|
@@ -42,6 +42,8 @@ import type { SessionExportProbe } from './session-export/session-export-view.ts
|
|
|
42
42
|
import type { ForeignSkip } from '../foreign/types.ts';
|
|
43
43
|
import type { ForeignSourcesResponse as ForeignSourcesViewInput } from '../ui/foreign-view.ts';
|
|
44
44
|
import type { DiskUsageReport } from '../core/disk-usage.ts';
|
|
45
|
+
import type { BrowseResult } from '../core/path-browse.ts';
|
|
46
|
+
import type { NativePickResult } from '../core/native-path-picker.ts';
|
|
45
47
|
import type { UiT } from '../ui/i18n.ts';
|
|
46
48
|
import { failedSectionsFromResponse } from '../ui/export-flow.ts';
|
|
47
49
|
import type { SessionFormatDisposition } from '../ui/session-format-disposition.ts';
|
|
@@ -291,6 +293,8 @@ const LONG_OPTS: RequestOptions = { timeoutMs: LONG_REQUEST_TIMEOUT_MS };
|
|
|
291
293
|
|
|
292
294
|
/** 插件自更新请求的超时:宿主侧安装上限 15 分钟,客户端留出余量(16 分钟)避免误报「请求超时」。 */
|
|
293
295
|
const SELF_UPDATE_OPTS: RequestOptions = { timeoutMs: 16 * 60 * 1000 };
|
|
296
|
+
/** 原生目录选择器:宿主侧 10 分钟上限,这里留出传输余量。 */
|
|
297
|
+
const NATIVE_PICK_OPTS: RequestOptions = { timeoutMs: 11 * 60 * 1000 };
|
|
294
298
|
|
|
295
299
|
/** query-string 辅助(跳过 undefined/空串,与 dsh-ssh 一致) */
|
|
296
300
|
function query(params: Record<string, string | number | undefined>): string {
|
|
@@ -337,6 +341,14 @@ function triggerBlobDownload(blob: Blob, filename: string): void {
|
|
|
337
341
|
setTimeout(() => { URL.revokeObjectURL(url) }, 15_000)
|
|
338
342
|
}
|
|
339
343
|
|
|
344
|
+
/**
|
|
345
|
+
* DSH 宿主自带目录选择器的结果。
|
|
346
|
+
* - `ok`:用户选中(`path` 非空);
|
|
347
|
+
* - `canceled`:用户点了取消,调用方静默收手;
|
|
348
|
+
* - `unavailable`:宿主没有这个能力(非桌面宿主 / 版本较旧 / 调用抛错),调用方回落下一层。
|
|
349
|
+
*/
|
|
350
|
+
export type DshPickResult = { ok: true; path: string } | { ok: false; reason: 'canceled' | 'unavailable' }
|
|
351
|
+
|
|
340
352
|
/**
|
|
341
353
|
* Config Manager 浏览器半的唯一数据入口。
|
|
342
354
|
* 同时实现 `ExportPort`(export-flow 用)与 `ImportPort`(import-wizard 用)。
|
|
@@ -350,9 +362,16 @@ export class ConfigManagerApi {
|
|
|
350
362
|
exportPassword: string | null = null
|
|
351
363
|
/** 客户端展示层翻译器(zh 源 / en 镜像,见 ui/i18n.ts)。 */
|
|
352
364
|
readonly t: UiT
|
|
365
|
+
/**
|
|
366
|
+
* DSH 自带目录选择器(桌面壳全局 `__DSH_DIRECTORY_PICKER__.pick` 或客户端服务 `ctx.get('uiWorkspace').pickDirectory`),
|
|
367
|
+
* 由 `apply(ctx)` 注入的**惰取** thunk(解析顺序见 `import/dsh-directory-pick.ts`)。
|
|
368
|
+
* 宿主没提供该能力时保持 `undefined`。
|
|
369
|
+
*/
|
|
370
|
+
private readonly dshDirectoryPicker: (() => Promise<string | null>) | undefined
|
|
353
371
|
|
|
354
|
-
constructor(t: UiT) {
|
|
372
|
+
constructor(t: UiT, dshDirectoryPicker?: () => Promise<string | null>) {
|
|
355
373
|
this.t = t
|
|
374
|
+
this.dshDirectoryPicker = dshDirectoryPicker
|
|
356
375
|
}
|
|
357
376
|
|
|
358
377
|
// ------------------------------------------------------------- status
|
|
@@ -575,6 +594,46 @@ export class ConfigManagerApi {
|
|
|
575
594
|
);
|
|
576
595
|
}
|
|
577
596
|
|
|
597
|
+
/**
|
|
598
|
+
* 列出一层本机目录(只读;路径映射「选本机路径」弹窗的数据源)。
|
|
599
|
+
* `target` 缺省或空串 = 「此电脑」根列表(Windows 盘符 / POSIX `/`)。
|
|
600
|
+
* 失败不抛异常:宿主回 `error: 'not-found'|'denied'|'failed'|'not-absolute'`,弹窗照常可用。
|
|
601
|
+
*/
|
|
602
|
+
async browsePath(target?: string): Promise<BrowseResult> {
|
|
603
|
+
const suffix = target === undefined || target === '' ? '' : query({ path: target });
|
|
604
|
+
return getJson<BrowseResult>(CONFIG_MANAGER_API.pathBrowse + suffix, this.t);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* 弹**系统原生**目录选择器(同一端点的 POST 形态)。
|
|
609
|
+
*
|
|
610
|
+
* 用户可能在系统对话框里逛几分钟,所以超时必须远大于常规请求:内核侧 10 分钟兜底,这里再加一道。
|
|
611
|
+
* 回 `reason: 'unsupported' | 'failed'` 时调用方回落到内置目录浏览器,不当错误处理。
|
|
612
|
+
*/
|
|
613
|
+
async pickNativePath(startPath?: string, title?: string): Promise<NativePickResult> {
|
|
614
|
+
const body: Record<string, string> = {};
|
|
615
|
+
if (startPath !== undefined && startPath !== '') body.startPath = startPath;
|
|
616
|
+
if (title !== undefined && title !== '') body.title = title;
|
|
617
|
+
return postJson<NativePickResult>(CONFIG_MANAGER_API.pathBrowse, body, this.t, NATIVE_PICK_OPTS);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* 复用 **DSH 宿主自带**的原生目录选择器(就是「选择工作区目录」那个窗口)。
|
|
622
|
+
*
|
|
623
|
+
* 比插件自己起系统对话框更稳:窗口由 DSH 主进程弹,跟着宿主走,不受插件宿主的进程形态限制。
|
|
624
|
+
* 完全不设客户端超时 —— 用户可能在原生窗口里逛几分钟,宿主侧 `host.pickDirectory` 也是 caller-signal-only。
|
|
625
|
+
* 宿主没有该能力或调用抛错一律归成 `unavailable`,由调用方回落插件自己的原生对话框,不当错误处理。
|
|
626
|
+
*/
|
|
627
|
+
async pickDshPath(): Promise<DshPickResult> {
|
|
628
|
+
if (this.dshDirectoryPicker === undefined) return { ok: false, reason: 'unavailable' }
|
|
629
|
+
try {
|
|
630
|
+
const path = await this.dshDirectoryPicker()
|
|
631
|
+
return typeof path === 'string' && path !== '' ? { ok: true, path } : { ok: false, reason: 'canceled' }
|
|
632
|
+
} catch {
|
|
633
|
+
return { ok: false, reason: 'unavailable' }
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
578
637
|
/** ImportPort.decryptArchive:解锁整体加密备份容器 → 明文 ZIP 路径 + 解密覆盖的凭据 ref 名 */
|
|
579
638
|
async decryptArchive(zipPath: string, password: string): Promise<DecryptArchiveResponse> {
|
|
580
639
|
return postJson<DecryptArchiveResponse>(CONFIG_MANAGER_API.decryptArchive, { zipPath, password }, this.t, LONG_OPTS);
|
|
@@ -73,6 +73,15 @@ test('lib/client.js 自包含:只 require 运行时白名单内的外部包',
|
|
|
73
73
|
`修复方向:把该依赖加回 tsdown.config.ts 的 deps.alwaysBundle(并保留在 devDependencies)。`,
|
|
74
74
|
)
|
|
75
75
|
|
|
76
|
+
// 相对 require = 依赖同目录 chunk:loader 只取 lib/client.js,不会请求 ./client2.js。
|
|
77
|
+
const relativeSpecifiers = result.specifiers.filter((s) => s.startsWith('.'))
|
|
78
|
+
assert.deepEqual(
|
|
79
|
+
relativeSpecifiers,
|
|
80
|
+
[],
|
|
81
|
+
`lib/client.js 依赖了同目录 chunk(loader 不会请求它们):${relativeSpecifiers.join(', ')}\n` +
|
|
82
|
+
'修复方向:tsdown.config.ts 的 outputOptions.inlineDynamicImports = true。',
|
|
83
|
+
)
|
|
84
|
+
|
|
76
85
|
// node:* 内置模块不属于本护栏判定范围(浏览器半不应依赖它们,但那是另一条不变量)。
|
|
77
86
|
if (result.nodeBuiltins.length > 0) {
|
|
78
87
|
t.diagnostic(`注意:bundle 内出现 Node 内置 require:${result.nodeBuiltins.join(', ')}`)
|