dsh-config-manager 0.1.39 → 0.1.41
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/client.d.ts +90 -8
- package/lib/client.js +1904 -1086
- package/lib/client.js.map +1 -1
- package/lib/core/cache-cleaner.d.ts +39 -0
- package/lib/core/cache-cleaner.js +164 -0
- package/lib/core/cache-cleaner.js.map +1 -0
- package/lib/index.js +172 -3
- package/lib/index.js.map +1 -1
- package/lib/market/github-repos.d.ts +19 -8
- package/lib/market/github-repos.js +62 -33
- package/lib/market/github-repos.js.map +1 -1
- package/lib/market/my-repo.d.ts +111 -3
- package/lib/market/my-repo.js +309 -26
- package/lib/market/my-repo.js.map +1 -1
- package/lib/market/prepare.js +3 -2
- package/lib/market/prepare.js.map +1 -1
- package/lib/market/types.d.ts +2 -0
- package/lib/security/secret-scanner.d.ts +18 -3
- package/lib/security/secret-scanner.js +144 -20
- package/lib/security/secret-scanner.js.map +1 -1
- package/package.json +1 -1
- package/src/client/common/ConfirmDialog.tsx +135 -0
- package/src/client/config-manager.module.css +101 -0
- package/src/client/market/MarketPanel.tsx +138 -45
- package/src/client/market/MyConfigsView.tsx +515 -132
- package/src/client/market/disclaimer.test.ts +57 -0
- package/src/client/market/disclaimer.ts +43 -0
- package/src/client/market/market-api.test.ts +3 -0
- package/src/client/market/market-locales.ts +37 -13
- package/src/client/market/my-configs-api.ts +20 -2
- package/src/client/market/my-configs-view.test.ts +137 -2
- package/src/client/market/my-configs-view.ts +174 -2
- package/src/client/run-store.test.ts +40 -2
- package/src/client/run-store.ts +70 -10
- package/src/client/sync/SyncConfirmView.tsx +28 -22
- package/src/client/sync/SyncSettingsView.tsx +175 -65
- package/src/client/sync/sync-api.ts +19 -0
- package/src/client/sync/sync-locales.ts +14 -2
- package/src/core/cache-cleaner.test.ts +333 -0
- package/src/core/cache-cleaner.ts +196 -0
- package/src/index.ts +165 -5
- package/src/market/github-repos.test.ts +99 -8
- package/src/market/github-repos.ts +67 -35
- package/src/market/my-repo.test.ts +218 -27
- package/src/market/my-repo.ts +403 -30
- package/src/market/prepare.test.ts +71 -1
- package/src/market/prepare.ts +3 -2
- package/src/market/types.ts +2 -0
- package/src/security/secret-scanner.ts +139 -22
- package/src/security/security.test.ts +111 -2
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** 临时文件缺省保留期:24 小时(覆盖跨会话「刷新恢复导入」窗口,昨天的残留自动清) */
|
|
2
|
+
export declare const TMP_RETENTION_DEFAULT_MS: number;
|
|
3
|
+
/** 导出产物缺省保留期:7 天(导出时用户已通过浏览器下载/另存到本地,host 端副本按周回收) */
|
|
4
|
+
export declare const EXPORTS_RETENTION_DEFAULT_MS: number;
|
|
5
|
+
/** 市场缓存/工作副本缺省保留期:7 天(重建成本 = 一次网络拉取) */
|
|
6
|
+
export declare const MARKET_RETENTION_DEFAULT_MS: number;
|
|
7
|
+
export interface CacheCleanupOptions {
|
|
8
|
+
/** 临时目录($DSH_HOME/dsh-config-manager/tmp) */
|
|
9
|
+
tmpDir: string;
|
|
10
|
+
/** 导出产物目录($DSH_HOME/dsh-config-manager/exports) */
|
|
11
|
+
exportsDir: string;
|
|
12
|
+
/** 市场缓存根($DSH_HOME/dsh-config-manager/market/cache) */
|
|
13
|
+
marketCacheRoot: string;
|
|
14
|
+
/** 市场工作副本根($DSH_HOME/dsh-config-manager/market/work) */
|
|
15
|
+
marketWorkRoot: string;
|
|
16
|
+
/** 临时文件保留期(缺省 24h) */
|
|
17
|
+
tmpRetentionMs?: number;
|
|
18
|
+
/** 导出产物保留期(缺省 7 天) */
|
|
19
|
+
exportsRetentionMs?: number;
|
|
20
|
+
/** 市场缓存/工作副本保留期(缺省 7 天) */
|
|
21
|
+
marketRetentionMs?: number;
|
|
22
|
+
/** 时间源(测试注入;缺省 Date.now) */
|
|
23
|
+
now?: () => number;
|
|
24
|
+
}
|
|
25
|
+
export interface CacheCleanupResult {
|
|
26
|
+
/** 删除条目数(文件 + 目录) */
|
|
27
|
+
removed: number;
|
|
28
|
+
/** 释放字节数(仅被删文件的 size 累计;目录删除统计为 0) */
|
|
29
|
+
freedBytes: number;
|
|
30
|
+
/** 单项失败数(不影响主流程) */
|
|
31
|
+
errors: number;
|
|
32
|
+
/** 每条删除记录(相对 dataDir 的描述 + 字节数),供日志/审计 */
|
|
33
|
+
detail: string[];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 执行一次缓存清理(幂等;可重复调用)。
|
|
37
|
+
* 只清理超期(超过保留期)的缓存/临时条目,其余一律保留。
|
|
38
|
+
*/
|
|
39
|
+
export declare function cleanupCaches(opts: CacheCleanupOptions): Promise<CacheCleanupResult>;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 缓存自动清理(cache-cleaner)
|
|
3
|
+
*
|
|
4
|
+
* 只清理「可重建 / 一次性」的缓存与临时文件,绝不触碰用户数据与安全网:
|
|
5
|
+
* - tmpDir 下过期的 `.zip`(upload-* / market-* / publish-* / decrypted-* /
|
|
6
|
+
* export-plain-* 等导入/导出/市场暂存,以及 SyncEngine 遗留的临时目录
|
|
7
|
+
* dsh-sync-pull-*)——保留期内文件不删(供「刷新恢复导入」等跨请求流程继续消费);
|
|
8
|
+
* - exportsDir 下过期的导出产物 `.zip`(导出时用户已通过浏览器下载/另存到本地,
|
|
9
|
+
* host 端只是暂存副本,按保留期回收);
|
|
10
|
+
* - market/cache/<url-hash>/(市场 index 缓存与条目缓存,refresh/download 可重建);
|
|
11
|
+
* - market/work/<url-hash>/(市场 git 只读工作副本,readIndex 时自动重新 clone)。
|
|
12
|
+
*
|
|
13
|
+
* 保留不动(属用户数据/安全网,由各自 UI 或业务逻辑管理):
|
|
14
|
+
* - snapshots/(导入前强制快照)、sync/(同步配置/历史/git 工作副本)。
|
|
15
|
+
*
|
|
16
|
+
* 实现:与 DSH 运行时解耦的纯函数(node:fs/promises),保留期与时间源参数化,
|
|
17
|
+
* 任何单项失败均不阻断其余清理(尽力而为),返回清理报告供宿主日志。
|
|
18
|
+
* 健康性:删除前按 mtime 判定超期(now - mtime > retention),stat 失败保守不删。
|
|
19
|
+
*/
|
|
20
|
+
import fs from 'node:fs/promises';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
/** 临时文件缺省保留期:24 小时(覆盖跨会话「刷新恢复导入」窗口,昨天的残留自动清) */
|
|
23
|
+
export const TMP_RETENTION_DEFAULT_MS = 24 * 60 * 60 * 1000;
|
|
24
|
+
/** 导出产物缺省保留期:7 天(导出时用户已通过浏览器下载/另存到本地,host 端副本按周回收) */
|
|
25
|
+
export const EXPORTS_RETENTION_DEFAULT_MS = 7 * 24 * 60 * 60 * 1000;
|
|
26
|
+
/** 市场缓存/工作副本缺省保留期:7 天(重建成本 = 一次网络拉取) */
|
|
27
|
+
export const MARKET_RETENTION_DEFAULT_MS = 7 * 24 * 60 * 60 * 1000;
|
|
28
|
+
/** SyncEngine 在 zipDir(即 tmpDir)下 mkdtemp 的目录前缀(用完即删,崩溃残留由清理兜底) */
|
|
29
|
+
const SYNC_TMP_DIR_PREFIX = 'dsh-sync-pull-';
|
|
30
|
+
/** 目录是否超期(stat 失败保守视为未超期 → 不删) */
|
|
31
|
+
async function isExpired(target, retentionMs, nowMs) {
|
|
32
|
+
try {
|
|
33
|
+
const st = await fs.stat(target);
|
|
34
|
+
return nowMs - st.mtimeMs > retentionMs;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** 删除一个文件/目录并计入报告;目录的 freedBytes 统计为 0(递归统计不划算,用途仅是日志) */
|
|
41
|
+
async function removeEntry(target, label, result) {
|
|
42
|
+
try {
|
|
43
|
+
let size = 0;
|
|
44
|
+
try {
|
|
45
|
+
const st = await fs.stat(target);
|
|
46
|
+
size = st.isFile() ? st.size : 0;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
/* stat 失败仍尝试删除(rm 自己会兜底不存在) */
|
|
50
|
+
}
|
|
51
|
+
await fs.rm(target, { recursive: true, force: true });
|
|
52
|
+
result.removed += 1;
|
|
53
|
+
result.freedBytes += size;
|
|
54
|
+
result.detail.push(`${label} (${size} bytes)`);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
result.errors += 1;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* 执行一次缓存清理(幂等;可重复调用)。
|
|
62
|
+
* 只清理超期(超过保留期)的缓存/临时条目,其余一律保留。
|
|
63
|
+
*/
|
|
64
|
+
export async function cleanupCaches(opts) {
|
|
65
|
+
const nowMs = (opts.now ?? Date.now)();
|
|
66
|
+
const tmpRetentionMs = opts.tmpRetentionMs ?? TMP_RETENTION_DEFAULT_MS;
|
|
67
|
+
const exportsRetentionMs = opts.exportsRetentionMs ?? EXPORTS_RETENTION_DEFAULT_MS;
|
|
68
|
+
const marketRetentionMs = opts.marketRetentionMs ?? MARKET_RETENTION_DEFAULT_MS;
|
|
69
|
+
const result = { removed: 0, freedBytes: 0, errors: 0, detail: [] };
|
|
70
|
+
// 1) tmpDir:过期 .zip(导入/导出/市场/解密暂存)与 SyncEngine 遗留的 dsh-sync-pull-* 目录
|
|
71
|
+
try {
|
|
72
|
+
const entries = await fs.readdir(opts.tmpDir, { withFileTypes: true });
|
|
73
|
+
for (const entry of entries) {
|
|
74
|
+
const target = path.join(opts.tmpDir, entry.name);
|
|
75
|
+
const isTmpish = (entry.isFile() && entry.name.endsWith('.zip')) ||
|
|
76
|
+
(entry.isDirectory() && entry.name.startsWith(SYNC_TMP_DIR_PREFIX));
|
|
77
|
+
if (!isTmpish)
|
|
78
|
+
continue;
|
|
79
|
+
if (await isExpired(target, tmpRetentionMs, nowMs)) {
|
|
80
|
+
await removeEntry(target, `tmp/${entry.name}`, result);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// tmpDir 不存在/不可读 → 跳过(尽力而为)
|
|
86
|
+
result.errors += 1;
|
|
87
|
+
}
|
|
88
|
+
// 2) exportsDir:过期的导出产物 .zip(导出时已下载/另存到本地,host 端副本按保留期回收)
|
|
89
|
+
try {
|
|
90
|
+
const entries = await fs.readdir(opts.exportsDir, { withFileTypes: true });
|
|
91
|
+
for (const entry of entries) {
|
|
92
|
+
if (!entry.isFile() || !entry.name.endsWith('.zip'))
|
|
93
|
+
continue;
|
|
94
|
+
const target = path.join(opts.exportsDir, entry.name);
|
|
95
|
+
if (await isExpired(target, exportsRetentionMs, nowMs)) {
|
|
96
|
+
await removeEntry(target, `exports/${entry.name}`, result);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// exportsDir 不存在/不可读 → 跳过
|
|
102
|
+
result.errors += 1;
|
|
103
|
+
}
|
|
104
|
+
// 3) market/cache:过期 index.json 与 items/<itemId> 条目缓存;删空后回收 hash 目录
|
|
105
|
+
try {
|
|
106
|
+
const hashes = await fs.readdir(opts.marketCacheRoot);
|
|
107
|
+
for (const hash of hashes) {
|
|
108
|
+
const hashDir = path.join(opts.marketCacheRoot, hash);
|
|
109
|
+
let st;
|
|
110
|
+
try {
|
|
111
|
+
st = await fs.stat(hashDir);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
continue; // 竞态删除/不可读 → 跳过该 hash
|
|
115
|
+
}
|
|
116
|
+
if (!st.isDirectory())
|
|
117
|
+
continue;
|
|
118
|
+
const indexFile = path.join(hashDir, 'index.json');
|
|
119
|
+
if (await isExpired(indexFile, marketRetentionMs, nowMs)) {
|
|
120
|
+
await removeEntry(indexFile, `market/cache/${hash}/index.json`, result);
|
|
121
|
+
}
|
|
122
|
+
const itemsDir = path.join(hashDir, 'items');
|
|
123
|
+
const itemDirs = await fs.readdir(itemsDir).catch(() => []);
|
|
124
|
+
for (const itemId of itemDirs) {
|
|
125
|
+
const itemDir = path.join(itemsDir, itemId);
|
|
126
|
+
if (await isExpired(itemDir, marketRetentionMs, nowMs)) {
|
|
127
|
+
await removeEntry(itemDir, `market/cache/${hash}/items/${itemId}`, result);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// items 子目录删空后回收;hash 目录删空后回收
|
|
131
|
+
const itemsLeft = await fs.readdir(itemsDir).catch(() => []);
|
|
132
|
+
if (itemsLeft.length === 0) {
|
|
133
|
+
await removeEntry(itemsDir, `market/cache/${hash}/items`, result);
|
|
134
|
+
}
|
|
135
|
+
const remaining = await fs.readdir(hashDir).catch(() => []);
|
|
136
|
+
if (remaining.length === 0) {
|
|
137
|
+
await removeEntry(hashDir, `market/cache/${hash}`, result);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// marketCacheRoot 不存在/不可读 → 跳过
|
|
143
|
+
result.errors += 1;
|
|
144
|
+
}
|
|
145
|
+
// 3) market/work:过期 git 只读工作副本(readIndex 时会按需重新 clone)
|
|
146
|
+
try {
|
|
147
|
+
const hashes = await fs.readdir(opts.marketWorkRoot);
|
|
148
|
+
for (const hash of hashes) {
|
|
149
|
+
const workDir = path.join(opts.marketWorkRoot, hash);
|
|
150
|
+
const st = await fs.stat(workDir).catch(() => null);
|
|
151
|
+
if (st === null || !st.isDirectory())
|
|
152
|
+
continue;
|
|
153
|
+
if (await isExpired(workDir, marketRetentionMs, nowMs)) {
|
|
154
|
+
await removeEntry(workDir, `market/work/${hash}`, result);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// marketWorkRoot 不存在/不可读 → 跳过
|
|
160
|
+
result.errors += 1;
|
|
161
|
+
}
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=cache-cleaner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-cleaner.js","sourceRoot":"","sources":["../../src/core/cache-cleaner.ts"],"names":[],"mappings":"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"}
|
package/lib/index.js
CHANGED
|
@@ -44,6 +44,7 @@ import { credentialRef } from '@deepseek-ai/dsh-credentials';
|
|
|
44
44
|
import { dshHomePath, resolveDshHome } from '@deepseek-ai/dsh-home-paths';
|
|
45
45
|
import * as yaml from 'js-yaml';
|
|
46
46
|
import { Exporter, FileSnapshotStore, Importer } from "./core/index.js";
|
|
47
|
+
import { cleanupCaches } from "./core/cache-cleaner.js";
|
|
47
48
|
import { listSnapshots, planRestore } from "./core/restore.js";
|
|
48
49
|
import { rollback as performRollback } from "./core/rollback.js";
|
|
49
50
|
import { RunRegistry } from "./core/run-registry.js";
|
|
@@ -72,7 +73,7 @@ import { prepareMarketItem } from "./market/prepare.js";
|
|
|
72
73
|
import { validateMarketRepoUrl } from "./market/url.js";
|
|
73
74
|
import { marketItemWarnings, toMarketListItem } from "./market/view.js";
|
|
74
75
|
import { GitHubAuthRest, GitHubApiError } from "./market/github-repos.js";
|
|
75
|
-
import { MyRepoService, USER_CONFIGS_REPO, userConfigsRepoUrl } from "./market/my-repo.js";
|
|
76
|
+
import { MyRepoError, MyRepoService, USER_CONFIGS_REPO, userConfigsRepoUrl } from "./market/my-repo.js";
|
|
76
77
|
import { createGitFileWriter } from "./market/git-file-writer.js";
|
|
77
78
|
import { redact } from "./security/redaction.js";
|
|
78
79
|
import { sha256Hex } from "./utils/hashing.js";
|
|
@@ -87,9 +88,11 @@ export const name = 'config-manager';
|
|
|
87
88
|
/** Services required before the engine can mount (present in every profile). */
|
|
88
89
|
export const inject = ['settings', 'credentials'];
|
|
89
90
|
/** Plugin version, kept in sync with package.json ("version"). */
|
|
90
|
-
const PLUGIN_VERSION = '0.1.
|
|
91
|
+
const PLUGIN_VERSION = '0.1.41';
|
|
91
92
|
/** Plugin own package name — excluded from its own exported plugins list. */
|
|
92
93
|
const PLUGIN_NAME = 'dsh-config-manager';
|
|
94
|
+
/** 缓存自动清理周期:24 小时(启动即清一次 + 此后每日一次;与 cache-cleaner 保留期独立) */
|
|
95
|
+
const CACHE_CLEANUP_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
93
96
|
/**
|
|
94
97
|
* 内置 GitHub OAuth App 的 client_id(「使用 GitHub 登录」device flow 缺省值)。
|
|
95
98
|
* Client ID 是公开标识(GitHub 官方明确非机密):内置后所有安装者开箱即用,
|
|
@@ -120,6 +123,8 @@ const API = {
|
|
|
120
123
|
syncGithubStart: '/api/dsh-config-manager/sync/github/start',
|
|
121
124
|
syncGithubPoll: '/api/dsh-config-manager/sync/github/poll',
|
|
122
125
|
syncGithubCancel: '/api/dsh-config-manager/sync/github/cancel',
|
|
126
|
+
// m-sync-github-valid:校验已存 token 是否有效(决定「已登录」→ 隐藏登录区块)
|
|
127
|
+
syncGithubValidate: '/api/dsh-config-manager/sync/github/validate',
|
|
123
128
|
// P2:同步历史 / 自动应用 / 一键回滚
|
|
124
129
|
syncHistory: '/api/dsh-config-manager/sync/history',
|
|
125
130
|
syncRollback: '/api/dsh-config-manager/sync/rollback',
|
|
@@ -147,6 +152,9 @@ const API = {
|
|
|
147
152
|
meUpload: '/api/dsh-config-manager/me/upload',
|
|
148
153
|
meItems: '/api/dsh-config-manager/me/items',
|
|
149
154
|
meUpdate: '/api/dsh-config-manager/me/update',
|
|
155
|
+
meListing: '/api/dsh-config-manager/me/listing',
|
|
156
|
+
meRelist: '/api/dsh-config-manager/me/relist',
|
|
157
|
+
meDelete: '/api/dsh-config-manager/me/delete',
|
|
150
158
|
};
|
|
151
159
|
/**
|
|
152
160
|
* 同步 token 的 DSH credentials 引用名(POSIX env-var 形态,满足 CredentialRef 品牌要求)。
|
|
@@ -1095,6 +1103,10 @@ function parseMeForm(raw) {
|
|
|
1095
1103
|
if (name === '')
|
|
1096
1104
|
return null;
|
|
1097
1105
|
const form = { name };
|
|
1106
|
+
// update 模式的可选显式 id(「更新」按钮预填;upload 时省略)
|
|
1107
|
+
const idRaw = obj['id'];
|
|
1108
|
+
if (typeof idRaw === 'string' && idRaw.trim() !== '')
|
|
1109
|
+
form.id = idRaw.trim();
|
|
1098
1110
|
const description = obj['description'];
|
|
1099
1111
|
if (typeof description === 'string' && description.trim() !== '')
|
|
1100
1112
|
form.description = description.trim();
|
|
@@ -1268,6 +1280,12 @@ function makeRoutes(deps) {
|
|
|
1268
1280
|
const marketCacheIndex = (url) => join(marketCacheRoot, urlHash(url), 'index.json');
|
|
1269
1281
|
const marketCacheItemDir = (url) => join(marketCacheRoot, urlHash(url), 'items');
|
|
1270
1282
|
const marketWorkDir = (url) => join(marketWorkRoot, urlHash(url));
|
|
1283
|
+
/**
|
|
1284
|
+
* 进程生命周期标记:本次 dsh 启动后市场是否已成功刷新过一次(内存态,dsh 重启后自动归零)。
|
|
1285
|
+
* 供「首次打开市场页自动更新一次市场」:MarketPanel 挂载时读 status 返回的
|
|
1286
|
+
* bootAutoRefreshed 判断是否要自动拉取;refresh(含手动「拉取最新」)成功后置位。
|
|
1287
|
+
*/
|
|
1288
|
+
let marketBootAutoRefreshed = false;
|
|
1271
1289
|
/** 请求级装配只读 GitMarketReader(公开市场无凭据;url = BUILTIN_MARKET_URL,已由 validateRepoUrl 拒绝 userinfo)。 */
|
|
1272
1290
|
const makeMarketReader = () => new GitMarketReader({ msg, timeoutMs: 60_000 });
|
|
1273
1291
|
/** 读缓存的 index.json(结构校验通过才返回,否则 null)。 */
|
|
@@ -2130,6 +2148,47 @@ function makeRoutes(deps) {
|
|
|
2130
2148
|
writeJson(res, 200, { ok: true });
|
|
2131
2149
|
},
|
|
2132
2150
|
},
|
|
2151
|
+
// -------------------------------------------------- sync/github/validate
|
|
2152
|
+
// m-sync-github-valid:校验 SYNC_CREDENTIAL_REF 中已存 token 是否有效(GET /user),
|
|
2153
|
+
// 供 UI 判定「是否已登录」→ 已登录隐藏 GitHub 登录区块、token 失效则重新展示。
|
|
2154
|
+
// 只回布尔 + 登录名(非敏感),token 值绝不回传;仅 401(无效/过期)→ valid:false,
|
|
2155
|
+
// 其余错误(网络/限流)向上抛,由 UI 兜底(不误判登出)。
|
|
2156
|
+
{
|
|
2157
|
+
kind: 'exact',
|
|
2158
|
+
path: API.syncGithubValidate,
|
|
2159
|
+
handler: async (req, res) => {
|
|
2160
|
+
if (!guard(req, res, 'POST'))
|
|
2161
|
+
return;
|
|
2162
|
+
try {
|
|
2163
|
+
let configured = false;
|
|
2164
|
+
let valid = false;
|
|
2165
|
+
let login;
|
|
2166
|
+
const resolved = await meTokenProvider();
|
|
2167
|
+
configured = resolved !== '';
|
|
2168
|
+
if (configured) {
|
|
2169
|
+
try {
|
|
2170
|
+
const user = await meGitHubRest.getUser();
|
|
2171
|
+
valid = true;
|
|
2172
|
+
login = user.login;
|
|
2173
|
+
}
|
|
2174
|
+
catch (error) {
|
|
2175
|
+
// 仅 401(token 无效/过期)→ 视为未登录;其余错误(网络/限流)向上抛
|
|
2176
|
+
if (!(error instanceof GitHubApiError && error.code === 'unauthorized'))
|
|
2177
|
+
throw error;
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
writeJson(res, 200, {
|
|
2181
|
+
ok: true,
|
|
2182
|
+
configured,
|
|
2183
|
+
valid,
|
|
2184
|
+
...(login !== undefined ? { login } : {}),
|
|
2185
|
+
});
|
|
2186
|
+
}
|
|
2187
|
+
catch (error) {
|
|
2188
|
+
writeJson(res, 500, { error: redact(error instanceof Error ? error.message : String(error)) });
|
|
2189
|
+
}
|
|
2190
|
+
},
|
|
2191
|
+
},
|
|
2133
2192
|
// ------------------------------------------------------ sync/history
|
|
2134
2193
|
// P2:列出本地祖先快照目录的 manifest.json(id/createdAt/sectionHashes),
|
|
2135
2194
|
// 同时统计 review-queue 中关联到该 snapshotId 的项数。
|
|
@@ -2544,7 +2603,13 @@ function makeRoutes(deps) {
|
|
|
2544
2603
|
return;
|
|
2545
2604
|
try {
|
|
2546
2605
|
const summary = await buildMarketSummary({ url: BUILTIN_MARKET_URL, addedAt: '' });
|
|
2547
|
-
writeJson(res, 200, {
|
|
2606
|
+
writeJson(res, 200, {
|
|
2607
|
+
ok: true,
|
|
2608
|
+
configured: true,
|
|
2609
|
+
markets: [summary],
|
|
2610
|
+
// 首次打开市场页自动更新一次的判据:本次 dsh 启动后是否已刷新过(进程内存,重启重置)
|
|
2611
|
+
bootAutoRefreshed: marketBootAutoRefreshed,
|
|
2612
|
+
});
|
|
2548
2613
|
}
|
|
2549
2614
|
catch (error) {
|
|
2550
2615
|
writeJson(res, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
@@ -2574,6 +2639,8 @@ function makeRoutes(deps) {
|
|
|
2574
2639
|
// 写缓存 index(供离线重复浏览)。内容始终视为不可信,读取时再结构校验。
|
|
2575
2640
|
await fs.mkdir(dirname(marketCacheIndex(url)), { recursive: true });
|
|
2576
2641
|
await fs.writeFile(marketCacheIndex(url), text, 'utf8');
|
|
2642
|
+
// 刷新成功 → 置位「本次启动已刷新」标记(手动「拉取最新」同样生效;失败不置位,下次打开可重试)
|
|
2643
|
+
marketBootAutoRefreshed = true;
|
|
2577
2644
|
const summary = await buildMarketSummary({ url, addedAt: new Date().toISOString() });
|
|
2578
2645
|
writeJson(res, 200, { ok: true, items: parsed.index.items, market: { ...summary, lastFetchedAt: fetchedAt } });
|
|
2579
2646
|
}
|
|
@@ -2887,6 +2954,85 @@ function makeRoutes(deps) {
|
|
|
2887
2954
|
}
|
|
2888
2955
|
},
|
|
2889
2956
|
},
|
|
2957
|
+
// ---------------------------------------------------- me/listing
|
|
2958
|
+
// 查询收录/下架任务状态(结果卡轮询):任务表命中 → 直接返回;未命中 → 回退 GitHub 实况推导;
|
|
2959
|
+
// 无任务且无实况 → 200 null。401(token 过期)→ 401,UI 引导重新登录。
|
|
2960
|
+
{
|
|
2961
|
+
kind: 'exact',
|
|
2962
|
+
path: API.meListing,
|
|
2963
|
+
handler: async (req, res) => {
|
|
2964
|
+
if (!guard(req, res, 'POST'))
|
|
2965
|
+
return;
|
|
2966
|
+
const body = await readJsonBody(req);
|
|
2967
|
+
const itemId = typeof body?.['itemId'] === 'string' ? body['itemId'] : '';
|
|
2968
|
+
if (itemId === '') {
|
|
2969
|
+
writeJson(res, 400, { error: 'itemId is required' });
|
|
2970
|
+
return;
|
|
2971
|
+
}
|
|
2972
|
+
try {
|
|
2973
|
+
const status = await meService.listingStatus(itemId);
|
|
2974
|
+
writeJson(res, 200, status); // null → 200 null
|
|
2975
|
+
}
|
|
2976
|
+
catch (error) {
|
|
2977
|
+
const status = error instanceof GitHubApiError && error.code === 'unauthorized' ? 401 : 500;
|
|
2978
|
+
writeJson(res, status, { error: redact(error instanceof Error ? error.message : String(error)) });
|
|
2979
|
+
}
|
|
2980
|
+
},
|
|
2981
|
+
},
|
|
2982
|
+
// ---------------------------------------------------- me/relist
|
|
2983
|
+
// 重新提交收录(收录失败 / 进程重启丢失后的一键重试):幂等复用已存在 fork/open PR。
|
|
2984
|
+
{
|
|
2985
|
+
kind: 'exact',
|
|
2986
|
+
path: API.meRelist,
|
|
2987
|
+
handler: async (req, res) => {
|
|
2988
|
+
if (!guard(req, res, 'POST'))
|
|
2989
|
+
return;
|
|
2990
|
+
const body = await readJsonBody(req);
|
|
2991
|
+
const itemId = typeof body?.['itemId'] === 'string' ? body['itemId'] : '';
|
|
2992
|
+
if (itemId === '') {
|
|
2993
|
+
writeJson(res, 400, { error: 'itemId is required' });
|
|
2994
|
+
return;
|
|
2995
|
+
}
|
|
2996
|
+
try {
|
|
2997
|
+
const status = await meService.relist(itemId);
|
|
2998
|
+
writeJson(res, 200, status);
|
|
2999
|
+
}
|
|
3000
|
+
catch (error) {
|
|
3001
|
+
const code = error instanceof GitHubApiError && error.code === 'unauthorized' ? 401 : 500;
|
|
3002
|
+
const message = error instanceof MyRepoError && error.code === 'item_not_found'
|
|
3003
|
+
? 404
|
|
3004
|
+
: code;
|
|
3005
|
+
writeJson(res, message, { error: redact(error instanceof Error ? error.message : String(error)) });
|
|
3006
|
+
}
|
|
3007
|
+
},
|
|
3008
|
+
},
|
|
3009
|
+
// ---------------------------------------------------- me/delete
|
|
3010
|
+
// 删除条目:同步删用户仓库索引 + items/<id>/ 文件;已收录 → 后台异步提下架 PR;待审核 → 关闭收录 PR。
|
|
3011
|
+
{
|
|
3012
|
+
kind: 'exact',
|
|
3013
|
+
path: API.meDelete,
|
|
3014
|
+
handler: async (req, res) => {
|
|
3015
|
+
if (!guard(req, res, 'POST'))
|
|
3016
|
+
return;
|
|
3017
|
+
const body = await readJsonBody(req);
|
|
3018
|
+
const itemId = typeof body?.['itemId'] === 'string' ? body['itemId'] : '';
|
|
3019
|
+
if (itemId === '') {
|
|
3020
|
+
writeJson(res, 400, { error: 'itemId is required' });
|
|
3021
|
+
return;
|
|
3022
|
+
}
|
|
3023
|
+
try {
|
|
3024
|
+
const result = await meService.deleteItem(itemId);
|
|
3025
|
+
writeJson(res, 200, result);
|
|
3026
|
+
}
|
|
3027
|
+
catch (error) {
|
|
3028
|
+
const code = error instanceof GitHubApiError && error.code === 'unauthorized' ? 401 : 500;
|
|
3029
|
+
const message = error instanceof MyRepoError && error.code === 'item_not_found'
|
|
3030
|
+
? 404
|
|
3031
|
+
: code;
|
|
3032
|
+
writeJson(res, message, { error: redact(error instanceof Error ? error.message : String(error)) });
|
|
3033
|
+
}
|
|
3034
|
+
},
|
|
3035
|
+
},
|
|
2890
3036
|
];
|
|
2891
3037
|
return { routes: routesList, scheduler };
|
|
2892
3038
|
}
|
|
@@ -2915,6 +3061,29 @@ export function apply(ctx, config) {
|
|
|
2915
3061
|
mkdirSync(syncDir, { recursive: true });
|
|
2916
3062
|
mkdirSync(marketDir, { recursive: true });
|
|
2917
3063
|
const host = new ConfigManagerHostContext(ctx, homeDir, resolveProfileName(config));
|
|
3064
|
+
// 缓存自动清理:启动即清一次 + 每 24h 定时清一次。
|
|
3065
|
+
// 只清「可重建/一次性」缓存与临时文件(tmp 暂存、exports 导出副本、market cache/work),
|
|
3066
|
+
// 保留期内的文件不删(供刷新恢复导入/下载等窗口继续消费);snapshots 与 sync 属用户数据/安全网不动。
|
|
3067
|
+
// 尽力而为:任何失败仅记日志,不影响插件挂载与其他功能。
|
|
3068
|
+
const runCacheCleanup = () => {
|
|
3069
|
+
void cleanupCaches({
|
|
3070
|
+
tmpDir,
|
|
3071
|
+
exportsDir,
|
|
3072
|
+
marketCacheRoot: join(marketDir, 'cache'),
|
|
3073
|
+
marketWorkRoot: join(marketDir, 'work'),
|
|
3074
|
+
})
|
|
3075
|
+
.then((report) => {
|
|
3076
|
+
if (report.removed > 0) {
|
|
3077
|
+
host.log.info('缓存自动清理完成', { removed: report.removed, freedBytes: report.freedBytes });
|
|
3078
|
+
}
|
|
3079
|
+
})
|
|
3080
|
+
.catch((error) => {
|
|
3081
|
+
host.log.warn('缓存自动清理失败', { error: error instanceof Error ? error.message : String(error) });
|
|
3082
|
+
});
|
|
3083
|
+
};
|
|
3084
|
+
runCacheCleanup();
|
|
3085
|
+
const cacheCleanupTimer = setInterval(runCacheCleanup, CACHE_CLEANUP_INTERVAL_MS);
|
|
3086
|
+
ctx.effect(() => () => clearInterval(cacheCleanupTimer), 'config-manager: cache cleanup scheduler');
|
|
2918
3087
|
// self 分区目录(相对 ~/.dsh 根):dataDir 在 homeDir 下 → 用相对路径挂载 self adapter;
|
|
2919
3088
|
// 自定义 dataDir 位于 ~/.dsh 之外时 Host fs 门面无法覆盖(confined to home root),
|
|
2920
3089
|
// 不挂 self 分区并告警(其余分区不受影响)。
|