@ai-workbench/connect 0.0.0-stage → 0.7.6

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.
Files changed (131) hide show
  1. package/NOTICE.md +18 -0
  2. package/README.md +26 -2
  3. package/client/index.cjs +124 -0
  4. package/cordis.patch.yml +3 -0
  5. package/helper/auth-status.cjs +18 -0
  6. package/helper/claude-callback.cjs +29 -0
  7. package/helper/main.cjs +19 -0
  8. package/helper/projects.cjs +57 -0
  9. package/helper/remote-command.cjs +15 -0
  10. package/helper/remote-login.cjs +25 -0
  11. package/helper/window.cjs +52 -0
  12. package/lib/api.d.ts +196 -0
  13. package/lib/api.js +1 -0
  14. package/lib/archive.d.ts +1 -0
  15. package/lib/archive.js +63 -0
  16. package/lib/auth-status.d.ts +3 -0
  17. package/lib/auth-status.js +18 -0
  18. package/lib/cache.d.ts +21 -0
  19. package/lib/cache.js +156 -0
  20. package/lib/component-download.d.ts +32 -0
  21. package/lib/component-download.js +76 -0
  22. package/lib/component-source.d.ts +52 -0
  23. package/lib/component-source.js +146 -0
  24. package/lib/components.d.ts +10 -0
  25. package/lib/components.js +154 -0
  26. package/lib/connect.d.ts +10 -0
  27. package/lib/connect.js +246 -0
  28. package/lib/cordis.d.ts +15 -0
  29. package/lib/cordis.js +1 -0
  30. package/lib/defaults.d.ts +30 -0
  31. package/lib/defaults.js +4 -0
  32. package/lib/electron.d.ts +8 -0
  33. package/lib/electron.js +83 -0
  34. package/lib/enrollment.d.ts +3 -0
  35. package/lib/enrollment.js +78 -0
  36. package/lib/index.d.ts +42 -0
  37. package/lib/index.js +55 -0
  38. package/lib/integration.d.ts +6 -0
  39. package/lib/integration.js +56 -0
  40. package/lib/login.d.ts +35 -0
  41. package/lib/login.js +81 -0
  42. package/lib/native-import-picker.d.ts +11 -0
  43. package/lib/native-import-picker.js +49 -0
  44. package/lib/needs.d.ts +9 -0
  45. package/lib/needs.js +52 -0
  46. package/lib/network.d.ts +26 -0
  47. package/lib/network.js +226 -0
  48. package/lib/physical-fs.d.ts +3 -0
  49. package/lib/physical-fs.js +7 -0
  50. package/lib/ports.d.ts +43 -0
  51. package/lib/ports.js +1 -0
  52. package/lib/private-files.d.ts +3 -0
  53. package/lib/private-files.js +41 -0
  54. package/lib/processes.d.ts +5 -0
  55. package/lib/processes.js +147 -0
  56. package/lib/profile-activation.d.ts +32 -0
  57. package/lib/profile-activation.js +118 -0
  58. package/lib/profile-enrollment.d.ts +21 -0
  59. package/lib/profile-enrollment.js +275 -0
  60. package/lib/profile-runtime.d.ts +6 -0
  61. package/lib/profile-runtime.js +47 -0
  62. package/lib/profile-settings.d.ts +2 -0
  63. package/lib/profile-settings.js +13 -0
  64. package/lib/projects.d.ts +4 -0
  65. package/lib/projects.js +41 -0
  66. package/lib/provider.d.ts +73 -0
  67. package/lib/provider.js +39 -0
  68. package/lib/remote-login.d.ts +2 -0
  69. package/lib/remote-login.js +9 -0
  70. package/lib/ssh.d.ts +10 -0
  71. package/lib/ssh.js +78 -0
  72. package/lib/startup.d.ts +18 -0
  73. package/lib/startup.js +211 -0
  74. package/lib/workspace-bindings.d.ts +32 -0
  75. package/lib/workspace-bindings.js +328 -0
  76. package/locale/en.json +6 -0
  77. package/locale/zh.json +6 -0
  78. package/node_modules/argparse/LICENSE +254 -0
  79. package/node_modules/argparse/README.md +84 -0
  80. package/node_modules/argparse/argparse.js +3707 -0
  81. package/node_modules/argparse/lib/sub.js +67 -0
  82. package/node_modules/argparse/lib/textwrap.js +440 -0
  83. package/node_modules/argparse/package.json +31 -0
  84. package/node_modules/js-yaml/LICENSE +21 -0
  85. package/node_modules/js-yaml/README.md +233 -0
  86. package/node_modules/js-yaml/bin/js-yaml.js +117 -0
  87. package/node_modules/js-yaml/dist/js-yaml.js +3022 -0
  88. package/node_modules/js-yaml/dist/js-yaml.js.map +1 -0
  89. package/node_modules/js-yaml/dist/js-yaml.min.js +2 -0
  90. package/node_modules/js-yaml/dist/js-yaml.min.js.map +1 -0
  91. package/node_modules/js-yaml/dist/js-yaml.mjs +3082 -0
  92. package/node_modules/js-yaml/dist/js-yaml.mjs.map +1 -0
  93. package/node_modules/js-yaml/index.js +44 -0
  94. package/node_modules/js-yaml/lib/common.js +50 -0
  95. package/node_modules/js-yaml/lib/dumper.js +937 -0
  96. package/node_modules/js-yaml/lib/exception.js +51 -0
  97. package/node_modules/js-yaml/lib/index_vite_proxy.tmp.mjs +37 -0
  98. package/node_modules/js-yaml/lib/loader.js +1803 -0
  99. package/node_modules/js-yaml/lib/schema/core.js +9 -0
  100. package/node_modules/js-yaml/lib/schema/default.js +20 -0
  101. package/node_modules/js-yaml/lib/schema/failsafe.js +14 -0
  102. package/node_modules/js-yaml/lib/schema/json.js +17 -0
  103. package/node_modules/js-yaml/lib/schema.js +109 -0
  104. package/node_modules/js-yaml/lib/snippet.js +96 -0
  105. package/node_modules/js-yaml/lib/type/binary.js +122 -0
  106. package/node_modules/js-yaml/lib/type/bool.js +35 -0
  107. package/node_modules/js-yaml/lib/type/float.js +99 -0
  108. package/node_modules/js-yaml/lib/type/int.js +142 -0
  109. package/node_modules/js-yaml/lib/type/map.js +8 -0
  110. package/node_modules/js-yaml/lib/type/merge.js +12 -0
  111. package/node_modules/js-yaml/lib/type/null.js +35 -0
  112. package/node_modules/js-yaml/lib/type/omap.js +45 -0
  113. package/node_modules/js-yaml/lib/type/pairs.js +50 -0
  114. package/node_modules/js-yaml/lib/type/seq.js +8 -0
  115. package/node_modules/js-yaml/lib/type/set.js +29 -0
  116. package/node_modules/js-yaml/lib/type/str.js +8 -0
  117. package/node_modules/js-yaml/lib/type/timestamp.js +88 -0
  118. package/node_modules/js-yaml/lib/type.js +66 -0
  119. package/node_modules/js-yaml/package.json +83 -0
  120. package/node_modules/pend/LICENSE +23 -0
  121. package/node_modules/pend/README.md +41 -0
  122. package/node_modules/pend/index.js +55 -0
  123. package/node_modules/pend/package.json +18 -0
  124. package/node_modules/pend/test.js +137 -0
  125. package/node_modules/yauzl/LICENSE +21 -0
  126. package/node_modules/yauzl/README.md +1015 -0
  127. package/node_modules/yauzl/crc32.js +45 -0
  128. package/node_modules/yauzl/fd-slicer.js +188 -0
  129. package/node_modules/yauzl/index.js +1096 -0
  130. package/node_modules/yauzl/package.json +40 -0
  131. package/package.json +89 -4
package/lib/api.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export declare function unpackZip(zipPath: string, root: string, maxBytes: number, signal?: AbortSignal): Promise<void>;
package/lib/archive.js ADDED
@@ -0,0 +1,63 @@
1
+ import yauzl from 'yauzl';
2
+ import { createWriteStream, mkdir, symlink } from "./physical-fs.js";
3
+ import { dirname, resolve, relative, isAbsolute } from 'node:path';
4
+ import { pipeline } from 'node:stream/promises';
5
+ export async function unpackZip(zipPath, root, maxBytes, signal) {
6
+ signal?.throwIfAborted();
7
+ const zip = await new Promise((yes, no) => yauzl.open(zipPath, { lazyEntries: true, validateEntrySizes: true }, (e, z) => e ? no(e) : yes(z)));
8
+ const links = [], seen = new Set();
9
+ let total = 0;
10
+ const inside = (path) => { const r = relative(root, path); return r !== '' && !r.startsWith('..') && !isAbsolute(r); };
11
+ try {
12
+ await new Promise((yes, no) => {
13
+ zip.on('error', no);
14
+ zip.on('end', yes);
15
+ zip.on('entry', (entry) => {
16
+ void (async () => {
17
+ signal?.throwIfAborted();
18
+ const path = resolve(root, entry.fileName);
19
+ if (!inside(path) || seen.has(path))
20
+ throw new Error('ZIP_PATH_INVALID');
21
+ seen.add(path);
22
+ total += entry.uncompressedSize;
23
+ if (total > maxBytes)
24
+ throw new Error('EXTRACTED_SIZE_EXCEEDED');
25
+ const mode = (entry.externalFileAttributes >>> 16) & 0xffff, type = mode & 0o170000;
26
+ if (entry.fileName.endsWith('/')) {
27
+ await mkdir(path, { recursive: true, mode: 0o700 });
28
+ zip.readEntry();
29
+ return;
30
+ }
31
+ if (type !== 0 && type !== 0o100000 && type !== 0o120000)
32
+ throw new Error('ZIP_TYPE_INVALID');
33
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
34
+ const stream = await new Promise((res, rej) => zip.openReadStream(entry, (e, s) => e ? rej(e) : res(s)));
35
+ if (type === 0o120000) {
36
+ const chunks = [];
37
+ for await (const c of stream) {
38
+ signal?.throwIfAborted();
39
+ chunks.push(Buffer.from(c));
40
+ }
41
+ const target = Buffer.concat(chunks).toString('utf8');
42
+ if (isAbsolute(target) || !inside(resolve(dirname(path), target)))
43
+ throw new Error('ZIP_LINK_INVALID');
44
+ links.push({ path, target });
45
+ }
46
+ else {
47
+ await pipeline(stream, createWriteStream(path, { flags: 'wx', mode: mode & 0o111 ? 0o700 : 0o600 }), { signal });
48
+ }
49
+ zip.readEntry();
50
+ })().catch(no);
51
+ });
52
+ zip.readEntry();
53
+ });
54
+ // All ordinary entries are written before symlinks, so no write can traverse an archive symlink.
55
+ for (const link of links) {
56
+ signal?.throwIfAborted();
57
+ await symlink(link.target, link.path);
58
+ }
59
+ }
60
+ finally {
61
+ zip.close();
62
+ }
63
+ }
@@ -0,0 +1,3 @@
1
+ import type { LoginState, Provider, Result } from './api.ts';
2
+ import type { CommandResult } from './ports.ts';
3
+ export declare function parseLoginStatus(provider: Provider, result: CommandResult): Result<LoginState>;
@@ -0,0 +1,18 @@
1
+ import { fail, ok } from "./needs.js";
2
+ export function parseLoginStatus(provider, result) {
3
+ let v;
4
+ try {
5
+ v = JSON.parse(result.stdout);
6
+ }
7
+ catch {
8
+ return fail('AUTH_STATUS_INVALID');
9
+ }
10
+ if (!v || typeof v !== 'object' || Array.isArray(v) || Object.keys(v).sort().join(',') !== 'exitCode,loggedIn,protocolVersion,provider')
11
+ return fail('AUTH_STATUS_INVALID');
12
+ const status = v;
13
+ if (status.protocolVersion !== 1 || status.provider !== provider || status.exitCode !== result.exitCode || typeof status.loggedIn !== 'boolean' || result.stderr !== '')
14
+ return fail('AUTH_STATUS_INVALID');
15
+ if (status.loggedIn ? result.exitCode !== 0 : provider === 'codex' ? result.exitCode !== 1 : result.exitCode !== 0 && result.exitCode !== 1)
16
+ return fail('AUTH_STATUS_INVALID');
17
+ return ok({ provider, loggedIn: status.loggedIn });
18
+ }
package/lib/cache.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ import type { Result } from './api.ts';
2
+ import { type ComponentProgress } from './component-download.ts';
3
+ export interface HelperSettings {
4
+ source: string;
5
+ cacheDir: string;
6
+ sha256: string;
7
+ maxDownloadBytes: number;
8
+ maxExtractedBytes: number;
9
+ executable: string;
10
+ }
11
+ interface CacheIo {
12
+ download: (source: string) => Promise<Response>;
13
+ unpack: (zip: string, dir: string, maxBytes: number) => Promise<void>;
14
+ }
15
+ export declare function ensureHelper(s: HelperSettings | undefined, io?: {
16
+ download: CacheIo['download'];
17
+ unpack?: CacheIo['unpack'];
18
+ signal?: AbortSignal;
19
+ onProgress?: (progress: ComponentProgress) => void;
20
+ }): Promise<Result<string>>;
21
+ export {};
package/lib/cache.js ADDED
@@ -0,0 +1,156 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { open, lstat, readFile, writeFile, readdir, readlink, realpath, rename, rm, mkdir } from "./physical-fs.js";
3
+ import { join, isAbsolute, relative } from 'node:path';
4
+ import { dependency, fail, ok, need } from "./needs.js";
5
+ import { ComponentDownloadError } from "./component-download.js";
6
+ import { privateDirectory } from "./private-files.js";
7
+ import { unpackZip } from "./archive.js";
8
+ async function treeHash(root) {
9
+ const hash = createHash('sha256');
10
+ const walk = async (dir) => {
11
+ for (const name of (await readdir(dir)).sort()) {
12
+ const p = join(dir, name), s = await lstat(p), rel = relative(root, p);
13
+ hash.update(rel + '\0' + (s.mode & 0o777) + '\0');
14
+ if (s.isDirectory())
15
+ await walk(p);
16
+ else if (s.isSymbolicLink()) {
17
+ const dest = await realpath(p);
18
+ if (!dest.startsWith(root + '/'))
19
+ throw new Error('CACHE_LINK_ESCAPE');
20
+ hash.update(await readlink(p));
21
+ }
22
+ else if (s.isFile())
23
+ hash.update(await readFile(p));
24
+ else
25
+ throw new Error('CACHE_TYPE_INVALID');
26
+ }
27
+ };
28
+ await walk(root);
29
+ return hash.digest('hex');
30
+ }
31
+ function valid(s) {
32
+ try {
33
+ return new URL(s.source).protocol === 'https:' && !new URL(s.source).username && !new URL(s.source).password && isAbsolute(s.cacheDir) && /^[a-f0-9]{64}$/.test(s.sha256) && Number.isSafeInteger(s.maxDownloadBytes) && s.maxDownloadBytes > 0 && Number.isSafeInteger(s.maxExtractedBytes) && s.maxExtractedBytes > 0 && !isAbsolute(s.executable) && !s.executable.split('/').includes('..') && s.executable !== '';
34
+ }
35
+ catch {
36
+ return false;
37
+ }
38
+ }
39
+ export async function ensureHelper(s, io) {
40
+ if (!s || !valid(s))
41
+ return fail('SETTINGS_REQUIRED');
42
+ if (!io?.download)
43
+ return dependency('组件官方地址的直接HTTPS下载通道');
44
+ const final = join(s.cacheDir, s.sha256), payload = join(final, 'runtime'), executable = join(payload, s.executable);
45
+ let temp, receivedBytes = 0, totalBytes = null;
46
+ const emit = (phase, errorCode) => io.onProgress?.({ phase, receivedBytes, totalBytes, ...errorCode ? { errorCode } : {} });
47
+ const cancelled = () => { if (io.signal?.aborted)
48
+ throw new ComponentDownloadError('COMPONENT_DOWNLOAD_CANCELLED'); };
49
+ try {
50
+ cancelled();
51
+ await privateDirectory(s.cacheDir);
52
+ let existing = false;
53
+ try {
54
+ const st = await lstat(final);
55
+ if (st.isSymbolicLink() || !st.isDirectory())
56
+ throw new ComponentDownloadError('COMPONENT_CACHE_INVALID');
57
+ existing = true;
58
+ }
59
+ catch (e) {
60
+ if (e.code !== 'ENOENT')
61
+ throw e;
62
+ }
63
+ if (existing) {
64
+ const receipt = JSON.parse(await readFile(join(final, 'receipt.json'), 'utf8'));
65
+ if (receipt.sha256 !== s.sha256 || receipt.treeHash !== await treeHash(payload))
66
+ throw new ComponentDownloadError('COMPONENT_CACHE_INVALID');
67
+ const file = await lstat(executable);
68
+ if (!file.isFile() || (file.mode & 0o100) === 0)
69
+ throw new ComponentDownloadError('COMPONENT_CACHE_INVALID');
70
+ cancelled();
71
+ emit('ready');
72
+ return ok(executable);
73
+ }
74
+ temp = join(s.cacheDir, 'download-' + randomUUID());
75
+ await mkdir(temp, { mode: 0o700 });
76
+ const archive = join(temp, 'electron.zip');
77
+ emit('downloading');
78
+ const response = await io.download(s.source);
79
+ cancelled();
80
+ if (!response.ok || !response.body)
81
+ throw new ComponentDownloadError('COMPONENT_DOWNLOAD_RESPONSE');
82
+ const length = response.headers.get('content-length');
83
+ if (length !== null) {
84
+ const n = Number(length);
85
+ if (!/^\d+$/.test(length) || !Number.isSafeInteger(n) || n < 0)
86
+ throw new ComponentDownloadError('COMPONENT_DOWNLOAD_LENGTH_INVALID');
87
+ totalBytes = n;
88
+ if (n > s.maxDownloadBytes) {
89
+ await response.body.cancel();
90
+ throw new ComponentDownloadError('COMPONENT_DOWNLOAD_SIZE');
91
+ }
92
+ }
93
+ emit('downloading');
94
+ const file = await open(archive, 'wx', 0o600), hash = createHash('sha256');
95
+ let total = 0;
96
+ try {
97
+ for await (const chunk of response.body) {
98
+ cancelled();
99
+ total += chunk.byteLength;
100
+ receivedBytes = total;
101
+ if (total > s.maxDownloadBytes)
102
+ throw new ComponentDownloadError('COMPONENT_DOWNLOAD_SIZE');
103
+ hash.update(chunk);
104
+ await file.write(chunk);
105
+ emit('downloading');
106
+ }
107
+ }
108
+ finally {
109
+ await file.close();
110
+ }
111
+ cancelled();
112
+ emit('verifying');
113
+ if (hash.digest('hex') !== s.sha256)
114
+ throw new ComponentDownloadError('COMPONENT_SHA256_MISMATCH');
115
+ const runtime = join(temp, 'runtime');
116
+ await privateDirectory(runtime);
117
+ emit('unpacking');
118
+ cancelled();
119
+ if (io.unpack)
120
+ await io.unpack(archive, runtime, s.maxExtractedBytes);
121
+ else
122
+ await unpackZip(archive, runtime, s.maxExtractedBytes, io.signal);
123
+ cancelled();
124
+ let unpacked = 0;
125
+ const measure = async (dir) => { for (const entry of await readdir(dir, { withFileTypes: true })) {
126
+ const p = join(dir, entry.name);
127
+ if (entry.isDirectory())
128
+ await measure(p);
129
+ else if (entry.isFile())
130
+ unpacked += (await lstat(p)).size;
131
+ if (unpacked > s.maxExtractedBytes)
132
+ throw new Error('EXTRACTED_SIZE_EXCEEDED');
133
+ } };
134
+ await measure(runtime);
135
+ const exe = join(runtime, s.executable), st = await lstat(exe);
136
+ if (!st.isFile() || (st.mode & 0o100) === 0)
137
+ throw new ComponentDownloadError('COMPONENT_EXECUTABLE_INVALID');
138
+ await writeFile(join(temp, 'receipt.json'), JSON.stringify({ sha256: s.sha256, treeHash: await treeHash(runtime) }), { mode: 0o600 });
139
+ await rm(archive);
140
+ cancelled();
141
+ await rename(temp, final);
142
+ temp = undefined;
143
+ cancelled();
144
+ emit('ready');
145
+ return ok(executable);
146
+ }
147
+ catch (error) {
148
+ const code = io.signal?.aborted ? 'COMPONENT_DOWNLOAD_CANCELLED' : error instanceof ComponentDownloadError ? error.message : 'COMPONENT_CACHE_IO';
149
+ emit(code === 'COMPONENT_DOWNLOAD_CANCELLED' ? 'cancelled' : 'failed', code);
150
+ return { ok: false, error: { ...need('LOGIN_FAILED'), requires: '登录组件从官方地址下载并通过完整校验;未重试或换源', officialReason: code } };
151
+ }
152
+ finally {
153
+ if (temp)
154
+ await rm(temp, { recursive: true, force: true });
155
+ }
156
+ }
@@ -0,0 +1,32 @@
1
+ export type ComponentPhase = 'idle' | 'downloading' | 'verifying' | 'unpacking' | 'ready' | 'failed' | 'cancelled';
2
+ export interface ComponentProgress {
3
+ phase: ComponentPhase;
4
+ receivedBytes: number;
5
+ totalBytes: number | null;
6
+ errorCode?: string;
7
+ }
8
+ export interface ComponentDownloadState extends ComponentProgress {
9
+ instanceId: string;
10
+ revision: number;
11
+ attemptId: string | null;
12
+ lastProgressAt?: number;
13
+ sourceLabel?: string;
14
+ sourceId?: string;
15
+ }
16
+ export declare function createComponentProgress(now?: () => number): {
17
+ getState: () => ComponentDownloadState;
18
+ onState(fn: () => void): () => void;
19
+ cancel(attemptId: string): boolean;
20
+ begin(source?: {
21
+ id: string;
22
+ label: string;
23
+ }, cancel?: () => void): {
24
+ attemptId: `${string}-${string}-${string}-${string}-${string}`;
25
+ update: (progress: ComponentProgress) => void;
26
+ };
27
+ };
28
+ export type ComponentProgressStore = ReturnType<typeof createComponentProgress>;
29
+ export declare class ComponentDownloadError extends Error {
30
+ constructor(code: string);
31
+ }
32
+ export declare function downloadComponent(source: string, signal: AbortSignal, maxRedirects: number): Promise<Response>;
@@ -0,0 +1,76 @@
1
+ import https from 'node:https';
2
+ import { Readable } from 'node:stream';
3
+ import { randomUUID } from 'node:crypto';
4
+ export function createComponentProgress(now = Date.now) {
5
+ let state = { instanceId: randomUUID(), revision: 0, attemptId: null, phase: 'idle', receivedBytes: 0, totalBytes: null };
6
+ const listeners = new Set();
7
+ let cancelDownload;
8
+ return { getState: () => structuredClone(state), onState(fn) { listeners.add(fn); return () => { listeners.delete(fn); }; }, cancel(attemptId) { if (state.attemptId !== attemptId || !['downloading', 'verifying', 'unpacking'].includes(state.phase) || !cancelDownload)
9
+ return false; const cancel = cancelDownload; cancelDownload = undefined; cancel(); return true; }, begin(source, cancel) { cancelDownload = cancel; const attemptId = randomUUID(); const update = (progress) => { if (state.attemptId !== attemptId)
10
+ return; if (['ready', 'failed', 'cancelled'].includes(progress.phase))
11
+ cancelDownload = undefined; state = { ...progress, lastProgressAt: progress.receivedBytes > state.receivedBytes ? now() : state.lastProgressAt, ...(source ? { sourceLabel: source.label, sourceId: source.id } : {}), instanceId: state.instanceId, revision: state.revision + 1, attemptId }; for (const fn of listeners)
12
+ fn(); }; state = { ...state, attemptId, receivedBytes: 0, lastProgressAt: now() }; update({ phase: 'downloading', receivedBytes: 0, totalBytes: null }); return { attemptId, update }; } };
13
+ }
14
+ export class ComponentDownloadError extends Error {
15
+ constructor(code) { super(code); this.name = 'ComponentDownloadError'; }
16
+ }
17
+ const sourceUrl = (source) => { let u; try {
18
+ u = new URL(source);
19
+ }
20
+ catch {
21
+ throw new ComponentDownloadError('COMPONENT_SOURCE_INVALID');
22
+ } if (u.protocol !== 'https:' || u.username || u.password)
23
+ throw new ComponentDownloadError('COMPONENT_SOURCE_INVALID'); return u; };
24
+ // 仅固定组件供给。不接收OAuth URL、ProxyLease、认证头或cookie;不读取系统/N代理。
25
+ export async function downloadComponent(source, signal, maxRedirects) {
26
+ if (!Number.isSafeInteger(maxRedirects) || maxRedirects < 0)
27
+ throw new ComponentDownloadError('COMPONENT_REDIRECT_LIMIT_INVALID');
28
+ let url = sourceUrl(source);
29
+ for (let redirects = 0;; redirects++) {
30
+ if (signal.aborted)
31
+ throw new ComponentDownloadError('COMPONENT_DOWNLOAD_CANCELLED');
32
+ const response = await new Promise((yes, no) => {
33
+ const req = https.request(url, { signal, headers: { Accept: 'application/octet-stream' } }, yes);
34
+ req.once('error', () => no(new ComponentDownloadError(signal.aborted ? 'COMPONENT_DOWNLOAD_CANCELLED' : 'COMPONENT_DOWNLOAD_NETWORK')));
35
+ req.end();
36
+ });
37
+ const status = response.statusCode ?? 0;
38
+ if ([301, 302, 303, 307, 308].includes(status)) {
39
+ const location = response.headers.location;
40
+ response.destroy();
41
+ if (redirects >= maxRedirects)
42
+ throw new ComponentDownloadError('COMPONENT_REDIRECT_LIMIT');
43
+ if (!location)
44
+ throw new ComponentDownloadError('COMPONENT_REDIRECT_INVALID');
45
+ try {
46
+ url = sourceUrl(new URL(location, url).href);
47
+ }
48
+ catch {
49
+ throw new ComponentDownloadError('COMPONENT_REDIRECT_INVALID');
50
+ }
51
+ continue;
52
+ }
53
+ if (status !== 200) {
54
+ response.destroy();
55
+ throw new ComponentDownloadError('COMPONENT_DOWNLOAD_HTTP_' + status);
56
+ }
57
+ const headers = new Headers();
58
+ for (const name of ['content-length', 'content-type']) {
59
+ const v = response.headers[name];
60
+ if (typeof v === 'string')
61
+ headers.set(name, v);
62
+ }
63
+ const reader = Readable.toWeb(response).getReader();
64
+ const body = new ReadableStream({ async pull(controller) { try {
65
+ const part = await reader.read();
66
+ if (part.done)
67
+ controller.close();
68
+ else
69
+ controller.enqueue(part.value);
70
+ }
71
+ catch {
72
+ controller.error(new ComponentDownloadError(signal.aborted ? 'COMPONENT_DOWNLOAD_CANCELLED' : 'COMPONENT_DOWNLOAD_BODY'));
73
+ } }, cancel: reason => reader.cancel(reason) });
74
+ return new Response(body, { status, headers });
75
+ }
76
+ }
@@ -0,0 +1,52 @@
1
+ export declare const COMPONENT_SOURCES: Readonly<{
2
+ github: {
3
+ label: string;
4
+ source: "https://github.com/electron/electron/releases/download/v44.0.0/electron-v44.0.0-darwin-arm64.zip";
5
+ };
6
+ npmmirror: {
7
+ label: string;
8
+ source: string;
9
+ };
10
+ }>;
11
+ export type ComponentSourceId = keyof typeof COMPONENT_SOURCES;
12
+ export declare const DEFAULT_SOURCE_REASON = "\u9ED8\u8BA4\u9009\u62E9 npmmirror\uFF1A\u4E2D\u56FD\u955C\u50CF\u53EF\u907F\u5F00\u90E8\u5206\u7F51\u7EDC\u4E0B\u65E0\u6CD5\u8BBF\u95EE\u7684 GitHub \u53D1\u5E03\u6587\u4EF6 CDN\uFF1B\u672A\u81EA\u52A8\u6D4B\u901F\u6216\u5931\u8D25\u6362\u6E90\u3002";
13
+ export declare const preset: (id: unknown) => id is ComponentSourceId;
14
+ interface Options {
15
+ profileDir?: string;
16
+ metadataMaxBytes: number;
17
+ source?: string;
18
+ sha256?: string;
19
+ reason: string;
20
+ busy: () => boolean;
21
+ }
22
+ export declare function createComponentSources(options: Options): {
23
+ ready: Promise<void>;
24
+ getState: () => {
25
+ changing: boolean;
26
+ instanceId: `${string}-${string}-${string}-${string}-${string}`;
27
+ revision: number;
28
+ phase: "loading" | "ready" | "blocked";
29
+ selectedId: string;
30
+ selectedLabel: string;
31
+ reason: string;
32
+ version: string;
33
+ sha256: string | null;
34
+ errorCode: string | null;
35
+ selectionErrorCode: string | null;
36
+ choices: {
37
+ id: string;
38
+ label: string;
39
+ }[];
40
+ };
41
+ onState(fn: () => void): () => void;
42
+ resolve: () => Promise<{
43
+ source: string;
44
+ id: string;
45
+ label: string;
46
+ reason: string;
47
+ }>;
48
+ select: (id: unknown) => Promise<void>;
49
+ dispose(): Promise<void>;
50
+ };
51
+ export type ComponentSources = ReturnType<typeof createComponentSources>;
52
+ export {};
@@ -0,0 +1,146 @@
1
+ import { join, isAbsolute, resolve } from 'node:path';
2
+ import { lstat, realpath, open, rename, rm } from 'node:fs/promises';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { privateDirectory, readPrivate } from "./private-files.js";
5
+ import { ELECTRON_PIN } from "./defaults.js";
6
+ import { ComponentDownloadError } from "./component-download.js";
7
+ export const COMPONENT_SOURCES = Object.freeze({
8
+ github: { label: 'GitHub 官方源', source: ELECTRON_PIN.source },
9
+ npmmirror: { label: 'npmmirror 镜像(Electron 官方文档列出)', source: 'https://npmmirror.com/mirrors/electron/44.0.0/electron-v44.0.0-darwin-arm64.zip' },
10
+ });
11
+ export const DEFAULT_SOURCE_REASON = '默认选择 npmmirror:中国镜像可避开部分网络下无法访问的 GitHub 发布文件 CDN;未自动测速或失败换源。';
12
+ export const preset = (id) => typeof id === 'string' && Object.hasOwn(COMPONENT_SOURCES, id);
13
+ export function createComponentSources(options) {
14
+ let selected = Object.entries(COMPONENT_SOURCES).find(([, v]) => v.source === options.source)?.[0];
15
+ let state = { instanceId: randomUUID(), revision: 0, phase: 'loading', selectedId: selected ?? 'custom', selectedLabel: selected ? COMPONENT_SOURCES[selected].label : '管理员自定义源', reason: options.reason, version: '44.0.0', sha256: options.sha256 ?? null, errorCode: null, selectionErrorCode: null, choices: Object.entries(COMPONENT_SOURCES).map(([id, v]) => ({ id, label: v.label })) };
16
+ let source = options.source, disposed = false, changing = false;
17
+ const listeners = new Set();
18
+ const pending = new Set();
19
+ function error(code) { throw new ComponentDownloadError(code); }
20
+ const notify = () => { if (!disposed) {
21
+ state = { ...state, revision: state.revision + 1 };
22
+ for (const fn of listeners)
23
+ fn();
24
+ } };
25
+ const blocked = (code) => { state = { ...state, phase: 'blocked', errorCode: code }; notify(); };
26
+ const directory = options.profileDir ? join(options.profileDir, 'ai-workbench-connect') : undefined;
27
+ const file = directory ? join(directory, 'component-source.json') : undefined;
28
+ const checkPin = () => { if (options.sha256 !== ELECTRON_PIN.sha256)
29
+ error('COMPONENT_PIN_MISMATCH'); };
30
+ async function canonical() { if (!options.profileDir || !directory || !isAbsolute(options.profileDir) || await realpath(options.profileDir) !== resolve(options.profileDir))
31
+ error('COMPONENT_SOURCE_PROFILE_REQUIRED'); await privateDirectory(directory); if (await realpath(directory) !== resolve(directory))
32
+ error('COMPONENT_SOURCE_STATE_INVALID'); }
33
+ const ready = (async () => {
34
+ try {
35
+ if (!file)
36
+ error('COMPONENT_SOURCE_PROFILE_REQUIRED');
37
+ if (!source)
38
+ error('COMPONENT_SOURCE_INVALID');
39
+ const initialUrl = new URL(source);
40
+ if (initialUrl.protocol !== 'https:' || initialUrl.username || initialUrl.password)
41
+ error('COMPONENT_SOURCE_INVALID');
42
+ await canonical();
43
+ try {
44
+ const stat = await lstat(file);
45
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1)
46
+ error('COMPONENT_SOURCE_STATE_INVALID');
47
+ const saved = JSON.parse(await readPrivate(file, options.metadataMaxBytes));
48
+ if (saved.schemaVersion !== 1 || !preset(saved.sourceId))
49
+ error('COMPONENT_SOURCE_STATE_INVALID');
50
+ const savedId = saved.sourceId;
51
+ selected = savedId;
52
+ source = COMPONENT_SOURCES[savedId].source;
53
+ state = { ...state, selectedId: savedId, selectedLabel: COMPONENT_SOURCES[savedId].label, reason: '沿用你在本设置节保存的下载源;未自动切源。' };
54
+ }
55
+ catch (e) {
56
+ if (e.code !== 'ENOENT')
57
+ throw e;
58
+ }
59
+ if (!source)
60
+ error('COMPONENT_SOURCE_INVALID');
61
+ const url = new URL(source);
62
+ if (url.protocol !== 'https:' || url.username || url.password)
63
+ error('COMPONENT_SOURCE_INVALID');
64
+ state = { ...state, phase: 'ready' };
65
+ notify();
66
+ }
67
+ catch (e) {
68
+ blocked(e instanceof ComponentDownloadError ? e.message : 'COMPONENT_SOURCE_STATE_INVALID');
69
+ }
70
+ })();
71
+ async function resolveSource() { await ready; if (disposed)
72
+ error('COMPONENT_SOURCE_DISPOSED'); if (state.phase === 'blocked')
73
+ error(state.errorCode); if (changing)
74
+ error('COMPONENT_SOURCE_BUSY'); if (selected)
75
+ checkPin(); return { source: source, id: state.selectedId, label: state.selectedLabel, reason: state.reason }; }
76
+ function select(id) {
77
+ const task = (async () => {
78
+ if (!preset(id))
79
+ error('COMPONENT_SOURCE_INVALID');
80
+ if (disposed)
81
+ error('COMPONENT_SOURCE_DISPOSED');
82
+ if (changing || options.busy())
83
+ error('COMPONENT_SOURCE_BUSY');
84
+ changing = true;
85
+ notify();
86
+ let temp;
87
+ try {
88
+ await ready;
89
+ if (disposed)
90
+ error('COMPONENT_SOURCE_DISPOSED');
91
+ if (state.phase === 'blocked')
92
+ error(state.errorCode);
93
+ if (options.busy())
94
+ error('COMPONENT_SOURCE_BUSY');
95
+ checkPin();
96
+ await canonical();
97
+ const data = JSON.stringify({ schemaVersion: 1, sourceId: id });
98
+ if (!Number.isSafeInteger(options.metadataMaxBytes) || Buffer.byteLength(data) > options.metadataMaxBytes)
99
+ error('COMPONENT_SOURCE_STATE_INVALID');
100
+ temp = join(directory, '.component-source-' + randomUUID());
101
+ const handle = await open(temp, 'wx', 0o600);
102
+ try {
103
+ await handle.writeFile(data);
104
+ await handle.sync();
105
+ }
106
+ finally {
107
+ await handle.close();
108
+ }
109
+ if (disposed)
110
+ error('COMPONENT_SOURCE_DISPOSED');
111
+ if (options.busy())
112
+ error('COMPONENT_SOURCE_BUSY');
113
+ await rename(temp, file);
114
+ temp = undefined;
115
+ const parent = await open(directory, 'r');
116
+ try {
117
+ await parent.sync();
118
+ }
119
+ finally {
120
+ await parent.close();
121
+ }
122
+ selected = id;
123
+ source = COMPONENT_SOURCES[id].source;
124
+ state = { ...state, selectedId: id, selectedLabel: COMPONENT_SOURCES[id].label, reason: '你在本设置节显式选择并保存了此下载源;未自动切源。', errorCode: null, selectionErrorCode: null };
125
+ }
126
+ catch (e) {
127
+ if (!(e instanceof ComponentDownloadError)) {
128
+ blocked('COMPONENT_SOURCE_STATE_INVALID');
129
+ error('COMPONENT_SOURCE_STATE_INVALID');
130
+ }
131
+ state = { ...state, selectionErrorCode: e.message };
132
+ throw e;
133
+ }
134
+ finally {
135
+ if (temp)
136
+ await rm(temp, { force: true });
137
+ changing = false;
138
+ notify();
139
+ }
140
+ })();
141
+ pending.add(task);
142
+ void task.then(() => pending.delete(task), () => pending.delete(task));
143
+ return task;
144
+ }
145
+ return { ready, getState: () => structuredClone({ ...state, changing }), onState(fn) { listeners.add(fn); return () => { listeners.delete(fn); }; }, resolve: resolveSource, select, async dispose() { disposed = true; listeners.clear(); await ready; await Promise.allSettled([...pending]); } };
146
+ }
@@ -0,0 +1,10 @@
1
+ import type { ComponentSources } from './component-source.ts';
2
+ import type { ComponentProgressStore } from './component-download.ts';
3
+ import type { Context } from '@deepseek-ai/cordis';
4
+ export interface ComponentInventory {
5
+ network: string;
6
+ connect: string;
7
+ claude: string;
8
+ codex?: string;
9
+ }
10
+ export declare function registerComponents(ctx: Context, inventory?: ComponentInventory, temporaryDataParent?: string, component?: ComponentProgressStore, sources?: ComponentSources, observationIntervalMs?: number): void;