session-viewer-plugin 0.1.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.
@@ -0,0 +1,117 @@
1
+ // AGC(华为云存储)凭证解析:按「环境变量 > 包内默认文件」优先级提供当前生效凭证。
2
+ // 环境变量支持两种形态:AGC_CREDENTIALS_FILE 指向凭证 JSON 文件、AGC_CREDENTIALS_JSON 为内联 JSON 字符串;
3
+ // 包内默认文件为随包发布的 assets/agc-apiclient.json,环境变量未设置时兜底。
4
+ // 凭证在进程生命周期内不变,解析结果进程内缓存。
5
+ import fs from 'fs/promises';
6
+ import path from 'path';
7
+ import { fileURLToPath } from 'url';
8
+ import { CloudStorageError } from './cloudError.js';
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+ /** 包内默认凭证文件路径(随包发布,作为最低优先级兜底) */
11
+ const BUNDLED_CREDENTIALS_FILE = path.join(__dirname, '..', 'assets', 'agc-apiclient.json');
12
+ /** 凭证必填字段(缺任一即视为无效凭证) */
13
+ const REQUIRED_FIELDS = [
14
+ 'client_id',
15
+ 'client_secret',
16
+ 'project_id',
17
+ 'bucket_name',
18
+ ];
19
+ /** 已成功解析的凭证缓存(凭证进程内不变,无需失效机制) */
20
+ let cached = null;
21
+ /**
22
+ * 校验凭证对象是否包含全部必填字段。
23
+ * @param cred 待校验的凭证对象
24
+ * @returns 缺失字段说明;校验通过返回 null
25
+ */
26
+ function validateCredentials(cred) {
27
+ const missing = REQUIRED_FIELDS.filter((field) => !String(cred[field] ?? '').trim());
28
+ return missing.length > 0 ? `缺少必填字段: ${missing.join(', ')}` : null;
29
+ }
30
+ /**
31
+ * 读取并解析 JSON 凭证文件。
32
+ * @param filePath 凭证文件绝对路径
33
+ * @returns 解析后的凭证对象
34
+ * @throws 文件不存在或内容不是合法 JSON 时抛错
35
+ */
36
+ async function readCredentialsFile(filePath) {
37
+ const raw = await fs.readFile(filePath, 'utf-8');
38
+ return JSON.parse(raw);
39
+ }
40
+ /**
41
+ * 从环境变量读取凭证(文件路径或内联 JSON 二选一)。
42
+ * @returns 凭证对象;未设置相关环境变量时返回 null
43
+ */
44
+ async function resolveFromEnv() {
45
+ const filePath = process.env.AGC_CREDENTIALS_FILE?.trim();
46
+ if (filePath) {
47
+ return readCredentialsFile(path.resolve(filePath));
48
+ }
49
+ const inlineJson = process.env.AGC_CREDENTIALS_JSON?.trim();
50
+ if (inlineJson) {
51
+ return JSON.parse(inlineJson);
52
+ }
53
+ return null;
54
+ }
55
+ /**
56
+ * 补齐凭证可选字段默认值(region 缺省按 CN 站点处理)。
57
+ * @param cred 原始凭证对象(应已通过必填校验)
58
+ * @returns 字段完整的凭证对象
59
+ */
60
+ function normalizeCredentials(cred) {
61
+ return {
62
+ type: cred.type ?? '',
63
+ developer_id: cred.developer_id ?? '',
64
+ project_id: cred.project_id ?? '',
65
+ client_id: cred.client_id ?? '',
66
+ client_secret: cred.client_secret ?? '',
67
+ configuration_version: cred.configuration_version ?? '',
68
+ region: cred.region?.trim() || 'CN',
69
+ bucket_name: cred.bucket_name ?? '',
70
+ };
71
+ }
72
+ /**
73
+ * 按优先级解析凭证:环境变量 > 包内默认文件。
74
+ * 已设置的来源优先级最高,即使其内容无效也不再降级(避免静默使用错误配置)。
75
+ * @returns 成功时返回凭证与来源;全部来源不可用时返回错误说明
76
+ */
77
+ async function resolveCredentialSources() {
78
+ try {
79
+ const envCred = await resolveFromEnv();
80
+ if (envCred) {
81
+ const invalid = validateCredentials(envCred);
82
+ if (invalid)
83
+ return { error: `环境变量凭证无效: ${invalid}` };
84
+ return { cred: normalizeCredentials(envCred), source: 'env' };
85
+ }
86
+ }
87
+ catch (err) {
88
+ return { error: `环境变量凭证读取失败: ${err.message}` };
89
+ }
90
+ try {
91
+ const bundledCred = await readCredentialsFile(BUNDLED_CREDENTIALS_FILE);
92
+ const invalid = validateCredentials(bundledCred);
93
+ if (invalid)
94
+ return { error: `包内默认凭证无效: ${invalid}` };
95
+ return { cred: normalizeCredentials(bundledCred), source: 'default' };
96
+ }
97
+ catch {
98
+ return {
99
+ error: '未配置 AGC 凭证:请设置 AGC_CREDENTIALS_FILE 或 AGC_CREDENTIALS_JSON 环境变量(发布包不含凭证文件,源码运行时可用 src/assets/agc-apiclient.json 兜底)',
100
+ };
101
+ }
102
+ }
103
+ /**
104
+ * 获取当前生效的 AGC 凭证(结果进程内缓存)。
105
+ * @returns 当前生效的凭证对象
106
+ * @throws 所有来源均不可用时抛出 CloudStorageError(503)
107
+ */
108
+ export async function resolveAgcCredentials() {
109
+ if (cached)
110
+ return cached.cred;
111
+ const result = await resolveCredentialSources();
112
+ if ('error' in result) {
113
+ throw new CloudStorageError(result.error, 503);
114
+ }
115
+ cached = { cred: result.cred, source: result.source };
116
+ return result.cred;
117
+ }
@@ -0,0 +1,171 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ // ─── Token aggregation ───────────────────────────────────────
4
+ const EMPTY_TOKENS = {
5
+ input: 0,
6
+ output: 0,
7
+ reasoning: 0,
8
+ cacheRead: 0,
9
+ cacheWrite: 0,
10
+ total: 0,
11
+ };
12
+ /** Element-wise sum of token usages. Derived fields (cacheHitRate/avg) are not summed. */
13
+ function sumTokens(items) {
14
+ return items.reduce((acc, t) => ({
15
+ input: acc.input + (t.input ?? 0),
16
+ output: acc.output + (t.output ?? 0),
17
+ reasoning: acc.reasoning + (t.reasoning ?? 0),
18
+ cacheRead: acc.cacheRead + (t.cacheRead ?? 0),
19
+ cacheWrite: acc.cacheWrite + (t.cacheWrite ?? 0),
20
+ total: acc.total + (t.total ?? 0),
21
+ }), { ...EMPTY_TOKENS });
22
+ }
23
+ // ─── In-memory cache ──────────────────────────────────────────
24
+ const CACHE_TTL = 30_000; // 30 seconds
25
+ const sessionsCache = new Map();
26
+ const detailCache = new Map();
27
+ function cacheKey(dirs) {
28
+ return dirs.slice().sort().join('|');
29
+ }
30
+ function getCached(map, key) {
31
+ const entry = map.get(key);
32
+ if (!entry)
33
+ return null;
34
+ if (Date.now() - entry.ts > CACHE_TTL) {
35
+ map.delete(key);
36
+ return null;
37
+ }
38
+ return entry.data;
39
+ }
40
+ function setCache(map, key, data) {
41
+ map.set(key, { data, ts: Date.now() });
42
+ }
43
+ // ─── File reading ─────────────────────────────────────────────
44
+ async function readSessionFile(filePath) {
45
+ try {
46
+ const content = await fs.readFile(filePath, 'utf-8');
47
+ return JSON.parse(content);
48
+ }
49
+ catch (err) {
50
+ console.error('Error reading file:', filePath, err);
51
+ return null;
52
+ }
53
+ }
54
+ function subtractTokens(a, b) {
55
+ return {
56
+ input: (a.input ?? 0) - (b.input ?? 0),
57
+ output: (a.output ?? 0) - (b.output ?? 0),
58
+ reasoning: (a.reasoning ?? 0) - (b.reasoning ?? 0),
59
+ cacheRead: (a.cacheRead ?? 0) - (b.cacheRead ?? 0),
60
+ cacheWrite: (a.cacheWrite ?? 0) - (b.cacheWrite ?? 0),
61
+ total: (a.total ?? 0) - (b.total ?? 0),
62
+ };
63
+ }
64
+ function sessionToSummary(m, id, filePath) {
65
+ const subagents = m.subagents ?? [];
66
+ const subagentTokens = subagents.length > 0 ? sumTokens(subagents.map((s) => s.tokens)) : undefined;
67
+ const mainTokens = subagentTokens ? subtractTokens(m.tokens, subagentTokens) : undefined;
68
+ const rawFirst = m.rounds?.find((r) => r.userMessage)?.userMessage;
69
+ const firstUserMessage = Array.isArray(rawFirst) ? rawFirst[0] : rawFirst;
70
+ return {
71
+ id,
72
+ sessionId: m.sessionId,
73
+ directory: m.header.workingDirectory,
74
+ model: m.header.model,
75
+ agent: m.header.agent,
76
+ stepCount: m.steps.length,
77
+ totalTokens: m.tokens,
78
+ mainTokens,
79
+ subagentTokens,
80
+ subagentCount: subagents.length > 0 ? subagents.length : undefined,
81
+ totalCost: 0,
82
+ duration: m.duration,
83
+ createdAt: m.header.startTime,
84
+ startTime: m.startTime,
85
+ endTime: m.endTime,
86
+ toolCalls: m.tools.totalCalls,
87
+ successRate: m.tools.successRate,
88
+ firstUserMessage,
89
+ filePath,
90
+ };
91
+ }
92
+ function sessionToDetail(m, id) {
93
+ const summary = sessionToSummary(m, id);
94
+ return {
95
+ ...summary,
96
+ steps: m.steps,
97
+ tools: m.tools,
98
+ codeStats: m.codeStats,
99
+ rounds: m.rounds,
100
+ subagents: m.subagents,
101
+ skills: m.skills,
102
+ planning: m.planning,
103
+ systemPrompts: m.systemPrompts,
104
+ };
105
+ }
106
+ export function invalidateCache() {
107
+ sessionsCache.clear();
108
+ detailCache.clear();
109
+ }
110
+ export async function getSessions(dirs) {
111
+ const key = cacheKey(dirs);
112
+ const cached = getCached(sessionsCache, key);
113
+ if (cached)
114
+ return cached;
115
+ const seen = new Map();
116
+ // Read all directories in parallel
117
+ const dirResults = await Promise.allSettled(dirs.map(async (dir) => {
118
+ console.error('[getSessions] Reading directory:', dir);
119
+ const files = await fs.readdir(dir);
120
+ const jsonFiles = files.filter((f) => f.endsWith('.json')).sort();
121
+ console.error('[getSessions] JSON files:', jsonFiles.length, 'in', dir);
122
+ // Read all files in a directory in parallel, tracking file paths
123
+ const fileResults = await Promise.allSettled(jsonFiles.map(async (file) => {
124
+ const filePath = path.join(dir, file);
125
+ const session = await readSessionFile(filePath);
126
+ return { session, filePath, id: file.slice(0, -'.json'.length) };
127
+ }));
128
+ return fileResults;
129
+ }));
130
+ for (const result of dirResults) {
131
+ if (result.status === 'rejected')
132
+ continue;
133
+ for (const fileResult of result.value) {
134
+ if (fileResult.status === 'rejected')
135
+ continue;
136
+ const { session, filePath, id } = fileResult.value;
137
+ if (id && session?.sessionId && session.header && !seen.has(id)) {
138
+ seen.set(id, sessionToSummary(session, id, filePath));
139
+ }
140
+ }
141
+ }
142
+ const sessions = Array.from(seen.values());
143
+ sessions.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
144
+ console.error('[getSessions] Returning', sessions.length, 'sessions');
145
+ setCache(sessionsCache, key, sessions);
146
+ return sessions;
147
+ }
148
+ export async function getSessionDetail(dirs, id) {
149
+ const cacheKeyStr = `${id}:${cacheKey(dirs)}`;
150
+ const cached = getCached(detailCache, cacheKeyStr);
151
+ if (cached)
152
+ return cached;
153
+ // Try all directories in parallel
154
+ const results = await Promise.allSettled(dirs.map(async (dir) => {
155
+ const filePath = path.resolve(dir, `${id}.json`);
156
+ if (!filePath.startsWith(path.resolve(dir) + path.sep))
157
+ return null;
158
+ return readSessionFile(filePath);
159
+ }));
160
+ for (const result of results) {
161
+ if (result.status === 'rejected')
162
+ continue;
163
+ const session = result.value;
164
+ if (session?.sessionId && session.header) {
165
+ const detail = sessionToDetail(session, id);
166
+ setCache(detailCache, cacheKeyStr, detail);
167
+ return detail;
168
+ }
169
+ }
170
+ return null;
171
+ }
@@ -0,0 +1,15 @@
1
+ // 云端存储统一错误类型:携带 HTTP 状态码,供 AGC 凭证解析与存储操作共用。
2
+ export class CloudStorageError extends Error {
3
+ /** 期望返回给调用方的 HTTP 状态码 */
4
+ status;
5
+ /**
6
+ * 创建云端存储错误。
7
+ * @param message 错误描述
8
+ * @param status HTTP 状态码,默认 502(上游网关错误)
9
+ */
10
+ constructor(message, status = 502) {
11
+ super(message);
12
+ this.name = 'CloudStorageError';
13
+ this.status = status;
14
+ }
15
+ }
@@ -0,0 +1,305 @@
1
+ import { CloudStorageError } from './cloudError.js';
2
+ import { resolveAgcCredentials } from './agcCredentials.js';
3
+ // ─── AGC 端点(单点调整区) ────────────────────────────────────
4
+ // 文档: https://developer.huawei.com/consumer/cn/doc/AppGallery-connect-References/storage-restapi-listfile-0000001498518878
5
+ // 站点对照: token 域名 connect-api*.cloud.huawei.com / 存储域名 ops-server-*.agcstorage.link/v0
6
+ const AGC_SITES = {
7
+ CN: {
8
+ tokenHost: 'https://connect-api.cloud.huawei.com',
9
+ storageHost: 'https://ops-server-drcn.agcstorage.link/v0',
10
+ downloadHost: 'https://agc-storage-drcn.platform.dbankcloud.cn',
11
+ },
12
+ DE: {
13
+ tokenHost: 'https://connect-api-dre.cloud.huawei.com',
14
+ storageHost: 'https://ops-server-dre.agcstorage.link/v0',
15
+ // 海外下载域名按 dbankcloud 海外域推导(当前桶为 CN,未实测)
16
+ downloadHost: 'https://agc-storage-dre.platform.dbankcloud.com',
17
+ },
18
+ SG: {
19
+ tokenHost: 'https://connect-api-dra.cloud.huawei.com',
20
+ storageHost: 'https://ops-server-dra.agcstorage.link/v0',
21
+ downloadHost: 'https://agc-storage-dra.platform.dbankcloud.com',
22
+ },
23
+ RURU: {
24
+ tokenHost: 'https://connect-api-drru.cloud.huawei.com',
25
+ storageHost: 'https://ops-server-drru.agcstorage.link/v0',
26
+ downloadHost: 'https://agc-storage-drru.platform.dbankcloud.com',
27
+ },
28
+ };
29
+ // ─── Access Token(缓存,过期前 60 秒刷新) ───────────────────
30
+ let cachedToken = null;
31
+ async function getCloudAccessToken(cred) {
32
+ if (cachedToken && Date.now() < cachedToken.expiresAt)
33
+ return cachedToken.token;
34
+ const site = resolveSite(cred.region);
35
+ let res;
36
+ try {
37
+ res = await fetch(`${site.tokenHost}/api/oauth2/v1/token`, {
38
+ method: 'POST',
39
+ headers: { 'Content-Type': 'application/json' },
40
+ body: JSON.stringify({
41
+ grant_type: 'client_credentials',
42
+ client_id: cred.client_id,
43
+ client_secret: cred.client_secret,
44
+ }),
45
+ });
46
+ }
47
+ catch (err) {
48
+ console.error('AGC token request error:', err);
49
+ throw new CloudStorageError('Failed to reach AGC token endpoint', 502);
50
+ }
51
+ if (!res.ok) {
52
+ const text = await res.text().catch(() => '');
53
+ console.error('AGC token request failed:', res.status, text);
54
+ throw new CloudStorageError(`AGC token request failed (${res.status}): ${text.slice(0, 300)}`, 502);
55
+ }
56
+ const data = (await res.json());
57
+ if (!data.access_token) {
58
+ throw new CloudStorageError('AGC token response missing access_token', 502);
59
+ }
60
+ const expiresInSeconds = Number(data.expires_in) || 172800;
61
+ cachedToken = { token: data.access_token, expiresAt: Date.now() + (expiresInSeconds - 60) * 1000 };
62
+ return data.access_token;
63
+ }
64
+ function resolveSite(region) {
65
+ return AGC_SITES[region] ?? AGC_SITES.CN;
66
+ }
67
+ // ─── 公共请求构造 ────────────────────────────────────────────
68
+ async function buildAgcHeaders(cred) {
69
+ const token = await getCloudAccessToken(cred);
70
+ return {
71
+ client_id: cred.client_id,
72
+ productId: cred.project_id,
73
+ Authorization: `Bearer ${token}`,
74
+ 'Content-Type': 'application/json',
75
+ };
76
+ }
77
+ async function resolveContext() {
78
+ const cred = await resolveAgcCredentials();
79
+ const site = resolveSite(cred.region);
80
+ const headers = await buildAgcHeaders(cred);
81
+ return { cred, storageHost: site.storageHost, downloadHost: site.downloadHost, headers };
82
+ }
83
+ // 对象路径逐段 encodeURIComponent(见文档「通用注意事项」)
84
+ function encodeObjectKey(key) {
85
+ return key.split('/').map(encodeURIComponent).join('/');
86
+ }
87
+ function buildObjectUrl(storageHost, bucketName, key, query) {
88
+ const base = `${storageHost}/${encodeURIComponent(bucketName)}/${encodeObjectKey(key)}`;
89
+ return query ? `${base}?${query}` : base;
90
+ }
91
+ async function agcFetch(url, init, action) {
92
+ let res;
93
+ try {
94
+ res = await fetch(url, init);
95
+ }
96
+ catch (err) {
97
+ console.error(`AGC ${action} request error:`, err);
98
+ throw new CloudStorageError(`Failed to reach AGC storage endpoint (${action})`, 502);
99
+ }
100
+ if (!res.ok) {
101
+ const text = await res.text().catch(() => '');
102
+ console.error(`AGC ${action} failed:`, res.status, text);
103
+ throw new CloudStorageError(`AGC ${action} failed (${res.status}): ${text.slice(0, 300)}`, 502);
104
+ }
105
+ return res;
106
+ }
107
+ // ─── 列举文件 ─────────────────────────────────────────────────
108
+ /** AGC listFiles 响应 → 稳定的 CloudFileListResult(size 字符串转数字) */
109
+ function normalizeListResponse(data) {
110
+ const obj = (data ?? {});
111
+ const files = Array.isArray(obj.contents)
112
+ ? obj.contents
113
+ .map((f) => ({
114
+ name: String(f.key ?? ''),
115
+ size: Number(f.size ?? 0),
116
+ contentType: String(f.contentType ?? ''),
117
+ lastModified: String(f.lastModified ?? ''),
118
+ }))
119
+ .filter((f) => f.name)
120
+ : [];
121
+ const folders = Array.isArray(obj.commonPrefixes)
122
+ ? obj.commonPrefixes
123
+ .map((p) => {
124
+ const prefix = String(p.prefix ?? '');
125
+ const trimmed = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
126
+ const name = trimmed.split('/').pop() || trimmed;
127
+ return { prefix, name };
128
+ })
129
+ .filter((f) => f.name)
130
+ : [];
131
+ return {
132
+ folders,
133
+ files,
134
+ nextMarker: obj.isTruncated && obj.nextMarker ? String(obj.nextMarker) : null,
135
+ };
136
+ }
137
+ export async function listCloudFiles(options = {}) {
138
+ const { prefix = '', marker, limit = 100, delimiter = false } = options;
139
+ const { cred, storageHost, headers } = await resolveContext();
140
+ const url = new URL(`${storageHost}/${encodeURIComponent(cred.bucket_name)}`);
141
+ if (prefix)
142
+ url.searchParams.set('prefix', prefix);
143
+ if (marker)
144
+ url.searchParams.set('marker', marker);
145
+ url.searchParams.set('max-keys', String(limit));
146
+ if (delimiter)
147
+ url.searchParams.set('delimiter', '/');
148
+ const res = await agcFetch(url, { headers }, 'listFiles');
149
+ try {
150
+ return normalizeListResponse(JSON.parse(await res.text()));
151
+ }
152
+ catch (err) {
153
+ console.error('AGC listFiles response parse error:', err);
154
+ throw new CloudStorageError('Failed to parse AGC listFiles response', 502);
155
+ }
156
+ }
157
+ /**
158
+ * 拉取指定前缀下的全部对象(自动翻页,无 delimiter)。
159
+ * 供模糊查询与文件夹递归删除使用;桶内对象量级 1 万以内。
160
+ * @param prefix 前缀(可选,空为整桶)
161
+ * @param maxPages 翻页上限保护(默认 50 页 = 5 万对象,超出抛 400)
162
+ * @returns 全部对象列表(key/size/contentType/lastModified)
163
+ */
164
+ export async function listAllCloudFiles(prefix = '', maxPages = 50) {
165
+ const files = [];
166
+ let marker;
167
+ for (let page = 0; page < maxPages; page++) {
168
+ const result = await listCloudFiles({ prefix, marker, limit: 1000, delimiter: false });
169
+ files.push(...result.files);
170
+ if (!result.nextMarker)
171
+ return files;
172
+ marker = result.nextMarker;
173
+ }
174
+ throw new CloudStorageError(`对象数超过翻页上限(${maxPages * 1000}),请缩小前缀范围`, 400);
175
+ }
176
+ export async function downloadCloudFile(key) {
177
+ const { cred, storageHost, headers } = await resolveContext();
178
+ const url = buildObjectUrl(storageHost, cred.bucket_name, key);
179
+ const res = await agcFetch(url, { headers }, 'downloadFile');
180
+ return {
181
+ body: res.body,
182
+ contentType: res.headers.get('content-type') || 'application/octet-stream',
183
+ contentLength: res.headers.get('content-length'),
184
+ filename: key.split('/').pop() || 'file',
185
+ };
186
+ }
187
+ // ─── 下载令牌 ─────────────────────────────────────────────────
188
+ /**
189
+ * 生成对象下载令牌并组装公网下载 URL(免鉴权,效期以 AGC 官方为准)。
190
+ * 底层为 ops-server `?token=create`,同对象重复生成返回相同令牌(幂等,已实测)。
191
+ * URL 格式与 AGC 控制台「复制下载令牌」一致:下载域名/v0/{bucket}/{encodeURIComponent(key)}?token=...
192
+ */
193
+ export async function getCloudFileDownloadToken(key) {
194
+ const { cred, storageHost, downloadHost, headers } = await resolveContext();
195
+ const url = buildObjectUrl(storageHost, cred.bucket_name, key, 'token=create');
196
+ const res = await agcFetch(url, { headers }, 'createToken');
197
+ const data = (await res.json().catch(() => null));
198
+ const token = Array.isArray(data) ? String(data[0] ?? '') : '';
199
+ if (!token) {
200
+ throw new CloudStorageError('AGC createToken returned empty token', 502);
201
+ }
202
+ const downloadUrl = `${downloadHost}/v0/${encodeURIComponent(cred.bucket_name)}/${encodeURIComponent(key)}?token=${token}`;
203
+ return { token, url: downloadUrl };
204
+ }
205
+ // ─── 删除文件 ─────────────────────────────────────────────────
206
+ export async function deleteCloudFile(key) {
207
+ const { cred, storageHost, headers } = await resolveContext();
208
+ const url = buildObjectUrl(storageHost, cred.bucket_name, key);
209
+ await agcFetch(url, { method: 'DELETE', headers }, 'deleteFile');
210
+ }
211
+ // ─── 上传 / 重命名 ───────────────────────────────────────────
212
+ export async function cloudFileExists(key) {
213
+ const { cred, storageHost, headers } = await resolveContext();
214
+ const url = buildObjectUrl(storageHost, cred.bucket_name, key, 'metadata=query');
215
+ try {
216
+ const res = await fetch(url, { headers });
217
+ return res.ok;
218
+ }
219
+ catch {
220
+ return false;
221
+ }
222
+ }
223
+ export async function uploadCloudFile(key, content, contentType) {
224
+ const { cred, storageHost, headers } = await resolveContext();
225
+ const url = buildObjectUrl(storageHost, cred.bucket_name, key);
226
+ await agcFetch(url, {
227
+ method: 'PUT',
228
+ headers: {
229
+ ...headers,
230
+ 'X-Agc-Content-Type': contentType || 'application/octet-stream',
231
+ },
232
+ body: new Uint8Array(content),
233
+ }, 'uploadFile');
234
+ }
235
+ export async function renameCloudFile(oldKey, newKey) {
236
+ const { cred, storageHost, headers } = await resolveContext();
237
+ const sourceUrl = buildObjectUrl(storageHost, cred.bucket_name, oldKey);
238
+ const res = await agcFetch(sourceUrl, { headers }, 'downloadFile');
239
+ const content = Buffer.from(await res.arrayBuffer());
240
+ const contentType = res.headers.get('content-type') || 'application/octet-stream';
241
+ await uploadCloudFile(newKey, content, contentType);
242
+ await deleteCloudFile(oldKey);
243
+ }
244
+ // ─── 文件元数据 ──────────────────────────────────────────────
245
+ function normalizeMetadata(data) {
246
+ const obj = (data ?? {});
247
+ const custom = (obj.metadata ?? {});
248
+ const customMetadata = {};
249
+ for (const [k, v] of Object.entries(custom)) {
250
+ customMetadata[k] = String(v);
251
+ }
252
+ return {
253
+ name: String(obj.name ?? ''),
254
+ bucket: obj.bucket ? String(obj.bucket) : undefined,
255
+ contentType: obj.contentType ? String(obj.contentType) : undefined,
256
+ contentDisposition: obj.contentDisposition ? String(obj.contentDisposition) : undefined,
257
+ contentEncoding: obj.contentEncoding ? String(obj.contentEncoding) : undefined,
258
+ cacheControl: obj.cacheControl ? String(obj.cacheControl) : undefined,
259
+ contentLanguage: obj.contentLanguage ? String(obj.contentLanguage) : undefined,
260
+ size: obj.size != null ? Number(obj.size) : undefined,
261
+ createTime: obj.createTime ? String(obj.createTime) : undefined,
262
+ updateTime: obj.updateTime ? String(obj.updateTime) : undefined,
263
+ storageType: obj.storageType ? String(obj.storageType) : undefined,
264
+ customMetadata,
265
+ };
266
+ }
267
+ export async function getCloudFileMetadata(key) {
268
+ const { cred, storageHost, headers } = await resolveContext();
269
+ const url = buildObjectUrl(storageHost, cred.bucket_name, key, 'metadata=query');
270
+ const res = await agcFetch(url, { headers }, 'getMetadata');
271
+ try {
272
+ return normalizeMetadata(JSON.parse(await res.text()));
273
+ }
274
+ catch (err) {
275
+ console.error('AGC getMetadata response parse error:', err);
276
+ throw new CloudStorageError('Failed to parse AGC metadata response', 502);
277
+ }
278
+ }
279
+ export async function updateCloudFileMetadata(key, update) {
280
+ const { cred, storageHost, headers } = await resolveContext();
281
+ const url = buildObjectUrl(storageHost, cred.bucket_name, key, 'metadata=update');
282
+ const metaHeaders = { ...headers };
283
+ if (update.contentType)
284
+ metaHeaders['X-Agc-Content-Type'] = update.contentType;
285
+ if (update.contentDisposition)
286
+ metaHeaders['X-Agc-Content-Disposition'] = update.contentDisposition;
287
+ if (update.contentEncoding)
288
+ metaHeaders['X-Agc-Content-Encoding'] = update.contentEncoding;
289
+ if (update.cacheControl)
290
+ metaHeaders['X-Agc-Cache-Control'] = update.cacheControl;
291
+ if (update.contentLanguage)
292
+ metaHeaders['X-Agc-Content-Language'] = update.contentLanguage;
293
+ for (const [k, v] of Object.entries(update.customMetadata ?? {})) {
294
+ if (k)
295
+ metaHeaders[`X-Agc-meta-${k}`] = String(v);
296
+ }
297
+ const res = await agcFetch(url, { method: 'GET', headers: metaHeaders }, 'updateMetadata');
298
+ try {
299
+ return normalizeMetadata(JSON.parse(await res.text()));
300
+ }
301
+ catch (err) {
302
+ console.error('AGC updateMetadata response parse error:', err);
303
+ throw new CloudStorageError('Failed to parse AGC updateMetadata response', 502);
304
+ }
305
+ }
@@ -0,0 +1,47 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
5
+ const DATA_DIR = path.join(__dirname, '..', '..', 'data');
6
+ const CONFIG_FILE = path.join(DATA_DIR, 'config.json');
7
+ const DEFAULT_CONFIG = {
8
+ favorites: [],
9
+ customDirs: [],
10
+ };
11
+ let cachedConfig = null;
12
+ export async function ensureDataDir() {
13
+ await fs.mkdir(DATA_DIR, { recursive: true });
14
+ }
15
+ export async function getConfig() {
16
+ if (cachedConfig)
17
+ return cachedConfig;
18
+ try {
19
+ const raw = await fs.readFile(CONFIG_FILE, 'utf-8');
20
+ const parsed = JSON.parse(raw);
21
+ cachedConfig = {
22
+ favorites: Array.isArray(parsed.favorites) ? parsed.favorites : [],
23
+ customDirs: Array.isArray(parsed.customDirs) ? parsed.customDirs : [],
24
+ };
25
+ }
26
+ catch {
27
+ cachedConfig = { ...DEFAULT_CONFIG };
28
+ }
29
+ return cachedConfig;
30
+ }
31
+ export async function saveConfig(config) {
32
+ await ensureDataDir();
33
+ await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
34
+ cachedConfig = config;
35
+ }
36
+ export async function toggleFavorite(sessionId) {
37
+ const config = await getConfig();
38
+ const index = config.favorites.indexOf(sessionId);
39
+ if (index >= 0) {
40
+ config.favorites.splice(index, 1);
41
+ }
42
+ else {
43
+ config.favorites.push(sessionId);
44
+ }
45
+ await saveConfig(config);
46
+ return config.favorites;
47
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export * from './session.js';
2
+ export * from './cloud.js';
@@ -0,0 +1 @@
1
+ export {};