codexmate 0.0.14 → 0.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +153 -358
- package/README.md +153 -357
- package/cli.js +1351 -67
- package/doc/CHANGELOG.md +14 -9
- package/doc/CHANGELOG.zh-CN.md +7 -0
- package/package.json +3 -3
- package/web-ui/app.js +24 -324
- package/web-ui/index.html +45 -22
- package/web-ui/modules/config-mode.computed.mjs +123 -0
- package/web-ui/modules/skills.computed.mjs +82 -0
- package/web-ui/modules/skills.methods.mjs +344 -0
- package/web-ui/styles.css +4 -4
package/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ const crypto = require('crypto');
|
|
|
6
6
|
const toml = require('@iarna/toml');
|
|
7
7
|
const JSON5 = require('json5');
|
|
8
8
|
const zipLib = require('zip-lib');
|
|
9
|
+
const yauzl = require('yauzl');
|
|
9
10
|
const { exec, execSync, spawn, spawnSync } = require('child_process');
|
|
10
11
|
const http = require('http');
|
|
11
12
|
const https = require('https');
|
|
@@ -108,12 +109,10 @@ const MAX_SESSION_PATH_LIST_SIZE = 2000;
|
|
|
108
109
|
const AGENTS_FILE_NAME = 'AGENTS.md';
|
|
109
110
|
const CODEX_SKILLS_DIR = path.join(CONFIG_DIR, 'skills');
|
|
110
111
|
const CLAUDE_SKILLS_DIR = path.join(CLAUDE_DIR, 'skills');
|
|
111
|
-
const
|
|
112
|
-
const OPENCODE_SKILLS_DIR = path.join(os.homedir(), '.opencode', 'skills');
|
|
112
|
+
const AGENTS_SKILLS_DIR = path.join(os.homedir(), '.agents', 'skills');
|
|
113
113
|
const SKILL_IMPORT_SOURCES = Object.freeze([
|
|
114
114
|
{ app: 'claude', label: 'Claude Code', dir: CLAUDE_SKILLS_DIR },
|
|
115
|
-
{ app: '
|
|
116
|
-
{ app: 'opencode', label: 'OpenCode', dir: OPENCODE_SKILLS_DIR }
|
|
115
|
+
{ app: 'agents', label: 'Agents', dir: AGENTS_SKILLS_DIR }
|
|
117
116
|
]);
|
|
118
117
|
const MODELS_CACHE_TTL_MS = 60 * 1000;
|
|
119
118
|
const MODELS_NEGATIVE_CACHE_TTL_MS = 5 * 1000;
|
|
@@ -121,6 +120,12 @@ const MODELS_CACHE_MAX_ENTRIES = 50;
|
|
|
121
120
|
const MODELS_RESPONSE_MAX_BYTES = 1024 * 1024;
|
|
122
121
|
const MAX_RECENT_CONFIGS = 3;
|
|
123
122
|
const MAX_UPLOAD_SIZE = 200 * 1024 * 1024;
|
|
123
|
+
const MAX_SKILLS_ZIP_UPLOAD_SIZE = 20 * 1024 * 1024;
|
|
124
|
+
const MAX_SKILLS_ZIP_ENTRY_COUNT = 2000;
|
|
125
|
+
const MAX_SKILLS_ZIP_UNCOMPRESSED_BYTES = 512 * 1024 * 1024;
|
|
126
|
+
const DEFAULT_EXTRACT_SUFFIXES = Object.freeze(['.json']);
|
|
127
|
+
const DOWNLOAD_ARTIFACT_TTL_MS = 10 * 60 * 1000;
|
|
128
|
+
const g_downloadArtifacts = new Map();
|
|
124
129
|
const BUILTIN_PROXY_PROVIDER_NAME = 'codexmate-proxy';
|
|
125
130
|
const DEFAULT_BUILTIN_PROXY_SETTINGS = Object.freeze({
|
|
126
131
|
enabled: false,
|
|
@@ -1804,6 +1809,7 @@ function importCodexSkills(params = {}) {
|
|
|
1804
1809
|
const visitedRealPaths = new Set([sourceDirForCopy]);
|
|
1805
1810
|
copyDirRecursive(sourceDirForCopy, targetPath, {
|
|
1806
1811
|
dereferenceSymlinks: true,
|
|
1812
|
+
allowedRootRealPath: sourceDirForCopy,
|
|
1807
1813
|
visitedRealPaths
|
|
1808
1814
|
});
|
|
1809
1815
|
copiedToTarget = true;
|
|
@@ -1835,6 +1841,306 @@ function importCodexSkills(params = {}) {
|
|
|
1835
1841
|
};
|
|
1836
1842
|
}
|
|
1837
1843
|
|
|
1844
|
+
function collectSkillDirectoriesFromRoot(rootDir, limit = MAX_SKILLS_ZIP_ENTRY_COUNT) {
|
|
1845
|
+
const results = [];
|
|
1846
|
+
let truncated = false;
|
|
1847
|
+
if (!rootDir || !fs.existsSync(rootDir)) {
|
|
1848
|
+
return { results, truncated };
|
|
1849
|
+
}
|
|
1850
|
+
const normalizedLimit = Number.isFinite(limit) && limit > 0
|
|
1851
|
+
? Math.floor(limit)
|
|
1852
|
+
: MAX_SKILLS_ZIP_ENTRY_COUNT;
|
|
1853
|
+
const stack = [rootDir];
|
|
1854
|
+
while (stack.length > 0) {
|
|
1855
|
+
if (results.length >= normalizedLimit) {
|
|
1856
|
+
truncated = true;
|
|
1857
|
+
break;
|
|
1858
|
+
}
|
|
1859
|
+
const currentDir = stack.pop();
|
|
1860
|
+
let entries = [];
|
|
1861
|
+
try {
|
|
1862
|
+
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
1863
|
+
} catch (e) {
|
|
1864
|
+
continue;
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
const hasSkillFile = entries.some((entry) => entry && entry.isFile() && String(entry.name || '') === 'SKILL.md');
|
|
1868
|
+
if (hasSkillFile) {
|
|
1869
|
+
results.push(currentDir);
|
|
1870
|
+
continue;
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
for (const entry of entries) {
|
|
1874
|
+
if (!entry || !entry.isDirectory()) continue;
|
|
1875
|
+
const entryName = typeof entry.name === 'string' ? entry.name.trim() : '';
|
|
1876
|
+
if (!entryName || entryName.startsWith('.')) {
|
|
1877
|
+
continue;
|
|
1878
|
+
}
|
|
1879
|
+
stack.push(path.join(currentDir, entryName));
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
return { results, truncated };
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
function resolveSkillNameFromImportedDirectory(skillDir, extractionRoot, fallbackName = '') {
|
|
1886
|
+
const directoryBaseName = path.basename(skillDir || '');
|
|
1887
|
+
const extractionBaseName = path.basename(extractionRoot || '');
|
|
1888
|
+
let candidate = directoryBaseName;
|
|
1889
|
+
if (!candidate || candidate === extractionBaseName || candidate.startsWith('.')) {
|
|
1890
|
+
const fallback = typeof fallbackName === 'string' ? fallbackName.trim() : '';
|
|
1891
|
+
const fallbackBase = fallback ? path.basename(fallback, path.extname(fallback)) : '';
|
|
1892
|
+
candidate = fallbackBase || candidate;
|
|
1893
|
+
}
|
|
1894
|
+
return normalizeCodexSkillName(candidate);
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
async function importCodexSkillsFromZipFile(zipPath, options = {}) {
|
|
1898
|
+
const fallbackName = typeof options.fallbackName === 'string' ? options.fallbackName : '';
|
|
1899
|
+
const tempDir = typeof options.tempDir === 'string' ? options.tempDir : '';
|
|
1900
|
+
const imported = [];
|
|
1901
|
+
const failed = [];
|
|
1902
|
+
const dedupNames = new Set();
|
|
1903
|
+
const extractionRoot = path.join(tempDir || path.dirname(zipPath), 'extract');
|
|
1904
|
+
|
|
1905
|
+
try {
|
|
1906
|
+
await inspectZipArchiveLimits(zipPath, {
|
|
1907
|
+
maxEntryCount: MAX_SKILLS_ZIP_ENTRY_COUNT,
|
|
1908
|
+
maxUncompressedBytes: MAX_SKILLS_ZIP_UNCOMPRESSED_BYTES
|
|
1909
|
+
});
|
|
1910
|
+
|
|
1911
|
+
await extractUploadZip(zipPath, extractionRoot);
|
|
1912
|
+
const discovery = collectSkillDirectoriesFromRoot(extractionRoot, MAX_SKILLS_ZIP_ENTRY_COUNT);
|
|
1913
|
+
const discoveredDirs = discovery.results;
|
|
1914
|
+
if (discoveredDirs.length === 0) {
|
|
1915
|
+
return { error: '压缩包中未发现包含 SKILL.md 的技能目录' };
|
|
1916
|
+
}
|
|
1917
|
+
if (discovery.truncated) {
|
|
1918
|
+
return { error: '压缩包中的技能目录数量超出导入上限' };
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
ensureDir(CODEX_SKILLS_DIR);
|
|
1922
|
+
for (const skillDir of discoveredDirs) {
|
|
1923
|
+
const normalizedName = resolveSkillNameFromImportedDirectory(skillDir, extractionRoot, fallbackName);
|
|
1924
|
+
if (normalizedName.error) {
|
|
1925
|
+
failed.push({
|
|
1926
|
+
name: path.basename(skillDir || ''),
|
|
1927
|
+
error: normalizedName.error
|
|
1928
|
+
});
|
|
1929
|
+
continue;
|
|
1930
|
+
}
|
|
1931
|
+
const dedupKey = normalizedName.name.toLowerCase();
|
|
1932
|
+
if (dedupNames.has(dedupKey)) {
|
|
1933
|
+
continue;
|
|
1934
|
+
}
|
|
1935
|
+
dedupNames.add(dedupKey);
|
|
1936
|
+
|
|
1937
|
+
const targetPath = path.join(CODEX_SKILLS_DIR, normalizedName.name);
|
|
1938
|
+
const targetRelative = path.relative(CODEX_SKILLS_DIR, targetPath);
|
|
1939
|
+
if (targetRelative.startsWith('..') || path.isAbsolute(targetRelative)) {
|
|
1940
|
+
failed.push({
|
|
1941
|
+
name: normalizedName.name,
|
|
1942
|
+
error: '目标路径非法'
|
|
1943
|
+
});
|
|
1944
|
+
continue;
|
|
1945
|
+
}
|
|
1946
|
+
if (fs.existsSync(targetPath)) {
|
|
1947
|
+
failed.push({
|
|
1948
|
+
name: normalizedName.name,
|
|
1949
|
+
error: 'Codex 中已存在同名 skill'
|
|
1950
|
+
});
|
|
1951
|
+
continue;
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
let copiedToTarget = false;
|
|
1955
|
+
try {
|
|
1956
|
+
const sourceRealPath = fs.realpathSync(skillDir);
|
|
1957
|
+
const sourceStat = fs.statSync(sourceRealPath);
|
|
1958
|
+
if (!sourceStat.isDirectory()) {
|
|
1959
|
+
failed.push({
|
|
1960
|
+
name: normalizedName.name,
|
|
1961
|
+
error: '来源 skill 无法读取'
|
|
1962
|
+
});
|
|
1963
|
+
continue;
|
|
1964
|
+
}
|
|
1965
|
+
const visitedRealPaths = new Set([sourceRealPath]);
|
|
1966
|
+
copyDirRecursive(sourceRealPath, targetPath, {
|
|
1967
|
+
dereferenceSymlinks: true,
|
|
1968
|
+
allowedRootRealPath: sourceRealPath,
|
|
1969
|
+
visitedRealPaths
|
|
1970
|
+
});
|
|
1971
|
+
copiedToTarget = true;
|
|
1972
|
+
imported.push({
|
|
1973
|
+
name: normalizedName.name,
|
|
1974
|
+
path: targetPath
|
|
1975
|
+
});
|
|
1976
|
+
} catch (e) {
|
|
1977
|
+
if (!copiedToTarget && fs.existsSync(targetPath)) {
|
|
1978
|
+
try {
|
|
1979
|
+
removeDirectoryRecursive(targetPath);
|
|
1980
|
+
} catch (_) {}
|
|
1981
|
+
}
|
|
1982
|
+
failed.push({
|
|
1983
|
+
name: normalizedName.name,
|
|
1984
|
+
error: e && e.message ? e.message : '导入失败'
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
if (imported.length === 0 && failed.length > 0) {
|
|
1990
|
+
return {
|
|
1991
|
+
error: failed[0].error || '导入失败',
|
|
1992
|
+
imported,
|
|
1993
|
+
failed,
|
|
1994
|
+
root: CODEX_SKILLS_DIR
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
return {
|
|
1999
|
+
success: failed.length === 0,
|
|
2000
|
+
imported,
|
|
2001
|
+
failed,
|
|
2002
|
+
root: CODEX_SKILLS_DIR
|
|
2003
|
+
};
|
|
2004
|
+
} catch (e) {
|
|
2005
|
+
return {
|
|
2006
|
+
error: `导入失败:${e && e.message ? e.message : '未知错误'}`
|
|
2007
|
+
};
|
|
2008
|
+
} finally {
|
|
2009
|
+
if (tempDir) {
|
|
2010
|
+
try {
|
|
2011
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
2012
|
+
} catch (_) {}
|
|
2013
|
+
} else if (fs.existsSync(extractionRoot)) {
|
|
2014
|
+
try {
|
|
2015
|
+
fs.rmSync(extractionRoot, { recursive: true, force: true });
|
|
2016
|
+
} catch (_) {}
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
async function importCodexSkillsFromZip(payload = {}) {
|
|
2022
|
+
if (!payload || typeof payload.fileBase64 !== 'string' || !payload.fileBase64.trim()) {
|
|
2023
|
+
return { error: '缺少技能压缩包内容' };
|
|
2024
|
+
}
|
|
2025
|
+
const upload = writeUploadZip(payload.fileBase64, 'codex-skills-import', payload.fileName || 'codex-skills.zip');
|
|
2026
|
+
if (upload.error) {
|
|
2027
|
+
return { error: upload.error };
|
|
2028
|
+
}
|
|
2029
|
+
return importCodexSkillsFromZipFile(upload.zipPath, {
|
|
2030
|
+
tempDir: upload.tempDir,
|
|
2031
|
+
fallbackName: payload.fileName || ''
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
async function exportCodexSkills(params = {}) {
|
|
2036
|
+
const rawNames = Array.isArray(params.names) ? params.names : [];
|
|
2037
|
+
const uniqueNames = Array.from(new Set(rawNames
|
|
2038
|
+
.map((item) => (typeof item === 'string' ? item.trim() : ''))
|
|
2039
|
+
.filter(Boolean)));
|
|
2040
|
+
if (uniqueNames.length === 0) {
|
|
2041
|
+
return { error: '请先选择要导出的 skill' };
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
const exported = [];
|
|
2045
|
+
const failed = [];
|
|
2046
|
+
const stagingTempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-skills-export-'));
|
|
2047
|
+
const stagingRoot = path.join(stagingTempDir, 'skills');
|
|
2048
|
+
ensureDir(stagingRoot);
|
|
2049
|
+
|
|
2050
|
+
try {
|
|
2051
|
+
for (const rawName of uniqueNames) {
|
|
2052
|
+
const normalizedName = normalizeCodexSkillName(rawName);
|
|
2053
|
+
if (normalizedName.error) {
|
|
2054
|
+
failed.push({ name: rawName, error: normalizedName.error });
|
|
2055
|
+
continue;
|
|
2056
|
+
}
|
|
2057
|
+
const sourcePath = path.join(CODEX_SKILLS_DIR, normalizedName.name);
|
|
2058
|
+
const sourceRelative = path.relative(CODEX_SKILLS_DIR, sourcePath);
|
|
2059
|
+
if (sourceRelative.startsWith('..') || path.isAbsolute(sourceRelative)) {
|
|
2060
|
+
failed.push({ name: normalizedName.name, error: '来源路径非法' });
|
|
2061
|
+
continue;
|
|
2062
|
+
}
|
|
2063
|
+
if (!fs.existsSync(sourcePath)) {
|
|
2064
|
+
failed.push({ name: normalizedName.name, error: 'skill 不存在' });
|
|
2065
|
+
continue;
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
try {
|
|
2069
|
+
const lstat = fs.lstatSync(sourcePath);
|
|
2070
|
+
if (!lstat.isDirectory() && !lstat.isSymbolicLink()) {
|
|
2071
|
+
failed.push({ name: normalizedName.name, error: '来源不是技能目录' });
|
|
2072
|
+
continue;
|
|
2073
|
+
}
|
|
2074
|
+
const sourceDirForCopy = lstat.isSymbolicLink() ? fs.realpathSync(sourcePath) : sourcePath;
|
|
2075
|
+
const sourceStat = fs.statSync(sourceDirForCopy);
|
|
2076
|
+
if (!sourceStat.isDirectory()) {
|
|
2077
|
+
failed.push({ name: normalizedName.name, error: '来源 skill 无法读取' });
|
|
2078
|
+
continue;
|
|
2079
|
+
}
|
|
2080
|
+
const targetPath = path.join(stagingRoot, normalizedName.name);
|
|
2081
|
+
const visitedRealPaths = new Set([sourceDirForCopy]);
|
|
2082
|
+
copyDirRecursive(sourceDirForCopy, targetPath, {
|
|
2083
|
+
dereferenceSymlinks: true,
|
|
2084
|
+
allowedRootRealPath: sourceDirForCopy,
|
|
2085
|
+
visitedRealPaths
|
|
2086
|
+
});
|
|
2087
|
+
exported.push({
|
|
2088
|
+
name: normalizedName.name,
|
|
2089
|
+
path: sourcePath
|
|
2090
|
+
});
|
|
2091
|
+
} catch (e) {
|
|
2092
|
+
failed.push({
|
|
2093
|
+
name: normalizedName.name,
|
|
2094
|
+
error: e && e.message ? e.message : '导出失败'
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
if (exported.length === 0) {
|
|
2100
|
+
return {
|
|
2101
|
+
error: failed[0] && failed[0].error ? failed[0].error : '无可导出的 skill',
|
|
2102
|
+
exported,
|
|
2103
|
+
failed,
|
|
2104
|
+
root: CODEX_SKILLS_DIR
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
const randomToken = crypto.randomBytes(12).toString('hex');
|
|
2109
|
+
const zipFileName = `codex-skills-${randomToken}.zip`;
|
|
2110
|
+
const zipFilePath = path.join(os.tmpdir(), zipFileName);
|
|
2111
|
+
if (fs.existsSync(zipFilePath)) {
|
|
2112
|
+
try {
|
|
2113
|
+
fs.unlinkSync(zipFilePath);
|
|
2114
|
+
} catch (_) {}
|
|
2115
|
+
}
|
|
2116
|
+
await zipLib.archiveFolder(stagingRoot, zipFilePath);
|
|
2117
|
+
const artifact = registerDownloadArtifact(zipFilePath, {
|
|
2118
|
+
fileName: zipFileName,
|
|
2119
|
+
deleteAfterDownload: true
|
|
2120
|
+
});
|
|
2121
|
+
|
|
2122
|
+
return {
|
|
2123
|
+
success: failed.length === 0,
|
|
2124
|
+
fileName: zipFileName,
|
|
2125
|
+
downloadPath: artifact.downloadPath,
|
|
2126
|
+
exported,
|
|
2127
|
+
failed,
|
|
2128
|
+
root: CODEX_SKILLS_DIR
|
|
2129
|
+
};
|
|
2130
|
+
} catch (e) {
|
|
2131
|
+
return {
|
|
2132
|
+
error: `导出失败:${e && e.message ? e.message : '未知错误'}`,
|
|
2133
|
+
exported,
|
|
2134
|
+
failed,
|
|
2135
|
+
root: CODEX_SKILLS_DIR
|
|
2136
|
+
};
|
|
2137
|
+
} finally {
|
|
2138
|
+
try {
|
|
2139
|
+
fs.rmSync(stagingTempDir, { recursive: true, force: true });
|
|
2140
|
+
} catch (_) {}
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
|
|
1838
2144
|
function removeDirectoryRecursive(targetPath) {
|
|
1839
2145
|
if (typeof fs.rmSync === 'function') {
|
|
1840
2146
|
fs.rmSync(targetPath, { recursive: true, force: false });
|
|
@@ -6629,6 +6935,70 @@ function readClaudeSettingsInfo() {
|
|
|
6629
6935
|
};
|
|
6630
6936
|
}
|
|
6631
6937
|
|
|
6938
|
+
function registerDownloadArtifact(filePath, options = {}) {
|
|
6939
|
+
const token = crypto.randomBytes(16).toString('hex');
|
|
6940
|
+
const fileName = typeof options.fileName === 'string' && options.fileName.trim()
|
|
6941
|
+
? options.fileName.trim()
|
|
6942
|
+
: path.basename(filePath || '');
|
|
6943
|
+
const ttlMs = Number.isFinite(options.ttlMs) && options.ttlMs > 0
|
|
6944
|
+
? Math.floor(options.ttlMs)
|
|
6945
|
+
: DOWNLOAD_ARTIFACT_TTL_MS;
|
|
6946
|
+
const expiresAt = Date.now() + ttlMs;
|
|
6947
|
+
const deleteAfterDownload = options.deleteAfterDownload !== false;
|
|
6948
|
+
|
|
6949
|
+
g_downloadArtifacts.set(token, {
|
|
6950
|
+
filePath,
|
|
6951
|
+
fileName,
|
|
6952
|
+
deleteAfterDownload,
|
|
6953
|
+
expiresAt
|
|
6954
|
+
});
|
|
6955
|
+
|
|
6956
|
+
setTimeout(() => {
|
|
6957
|
+
const artifact = g_downloadArtifacts.get(token);
|
|
6958
|
+
if (!artifact) return;
|
|
6959
|
+
if (Date.now() < artifact.expiresAt) return;
|
|
6960
|
+
g_downloadArtifacts.delete(token);
|
|
6961
|
+
if (artifact.deleteAfterDownload && artifact.filePath && fs.existsSync(artifact.filePath)) {
|
|
6962
|
+
try {
|
|
6963
|
+
fs.unlinkSync(artifact.filePath);
|
|
6964
|
+
} catch (_) {}
|
|
6965
|
+
}
|
|
6966
|
+
}, ttlMs + 2000);
|
|
6967
|
+
|
|
6968
|
+
return {
|
|
6969
|
+
token,
|
|
6970
|
+
fileName,
|
|
6971
|
+
downloadPath: `/download/${encodeURIComponent(token)}`
|
|
6972
|
+
};
|
|
6973
|
+
}
|
|
6974
|
+
|
|
6975
|
+
function resolveDownloadArtifact(tokenOrFileName, options = {}) {
|
|
6976
|
+
if (!tokenOrFileName) return null;
|
|
6977
|
+
const token = typeof tokenOrFileName === 'string' ? tokenOrFileName.trim() : '';
|
|
6978
|
+
if (!token) return null;
|
|
6979
|
+
|
|
6980
|
+
const artifact = g_downloadArtifacts.get(token);
|
|
6981
|
+
if (!artifact) {
|
|
6982
|
+
return null;
|
|
6983
|
+
}
|
|
6984
|
+
if (Date.now() > artifact.expiresAt) {
|
|
6985
|
+
g_downloadArtifacts.delete(token);
|
|
6986
|
+
if (artifact.deleteAfterDownload && artifact.filePath && fs.existsSync(artifact.filePath)) {
|
|
6987
|
+
try {
|
|
6988
|
+
fs.unlinkSync(artifact.filePath);
|
|
6989
|
+
} catch (_) {}
|
|
6990
|
+
}
|
|
6991
|
+
return null;
|
|
6992
|
+
}
|
|
6993
|
+
if (options && options.consume === true) {
|
|
6994
|
+
g_downloadArtifacts.delete(token);
|
|
6995
|
+
}
|
|
6996
|
+
return {
|
|
6997
|
+
token,
|
|
6998
|
+
...artifact
|
|
6999
|
+
};
|
|
7000
|
+
}
|
|
7001
|
+
|
|
6632
7002
|
// API: 打包 Claude 配置目录(系统 zip 可用则使用,否则回退 zip-lib)
|
|
6633
7003
|
async function prepareClaudeDirDownload() {
|
|
6634
7004
|
try {
|
|
@@ -6693,12 +7063,16 @@ async function prepareCodexDirDownload() {
|
|
|
6693
7063
|
|
|
6694
7064
|
function copyDirRecursive(srcDir, destDir, options = {}) {
|
|
6695
7065
|
const dereferenceSymlinks = !!(options && options.dereferenceSymlinks);
|
|
7066
|
+
const allowedRootRealPath = (options && typeof options.allowedRootRealPath === 'string')
|
|
7067
|
+
? options.allowedRootRealPath
|
|
7068
|
+
: '';
|
|
6696
7069
|
const visitedRealPaths = options && options.visitedRealPaths instanceof Set
|
|
6697
7070
|
? options.visitedRealPaths
|
|
6698
7071
|
: new Set();
|
|
6699
7072
|
const childOptions = {
|
|
6700
7073
|
...options,
|
|
6701
7074
|
dereferenceSymlinks,
|
|
7075
|
+
allowedRootRealPath,
|
|
6702
7076
|
visitedRealPaths
|
|
6703
7077
|
};
|
|
6704
7078
|
ensureDir(destDir);
|
|
@@ -6712,6 +7086,9 @@ function copyDirRecursive(srcDir, destDir, options = {}) {
|
|
|
6712
7086
|
continue;
|
|
6713
7087
|
}
|
|
6714
7088
|
const realPath = fs.realpathSync(srcPath);
|
|
7089
|
+
if (allowedRootRealPath && !isPathInside(realPath, allowedRootRealPath)) {
|
|
7090
|
+
throw new Error(`symlink escapes skill root: ${srcPath}`);
|
|
7091
|
+
}
|
|
6715
7092
|
if (visitedRealPaths.has(realPath)) {
|
|
6716
7093
|
continue;
|
|
6717
7094
|
}
|
|
@@ -6724,6 +7101,9 @@ function copyDirRecursive(srcDir, destDir, options = {}) {
|
|
|
6724
7101
|
} else if (entry.isSymbolicLink()) {
|
|
6725
7102
|
if (dereferenceSymlinks) {
|
|
6726
7103
|
const realPath = fs.realpathSync(srcPath);
|
|
7104
|
+
if (allowedRootRealPath && !isPathInside(realPath, allowedRootRealPath)) {
|
|
7105
|
+
throw new Error(`symlink escapes skill root: ${srcPath}`);
|
|
7106
|
+
}
|
|
6727
7107
|
const realStat = fs.statSync(realPath);
|
|
6728
7108
|
if (realStat.isDirectory()) {
|
|
6729
7109
|
if (visitedRealPaths.has(realPath)) {
|
|
@@ -6748,6 +7128,139 @@ function copyDirRecursive(srcDir, destDir, options = {}) {
|
|
|
6748
7128
|
}
|
|
6749
7129
|
}
|
|
6750
7130
|
|
|
7131
|
+
function inspectZipArchiveLimits(zipPath, options = {}) {
|
|
7132
|
+
const maxEntryCount = Number.isFinite(options.maxEntryCount) && options.maxEntryCount > 0
|
|
7133
|
+
? Math.floor(options.maxEntryCount)
|
|
7134
|
+
: MAX_SKILLS_ZIP_ENTRY_COUNT;
|
|
7135
|
+
const maxUncompressedBytes = Number.isFinite(options.maxUncompressedBytes) && options.maxUncompressedBytes > 0
|
|
7136
|
+
? Math.floor(options.maxUncompressedBytes)
|
|
7137
|
+
: MAX_SKILLS_ZIP_UNCOMPRESSED_BYTES;
|
|
7138
|
+
|
|
7139
|
+
return new Promise((resolve, reject) => {
|
|
7140
|
+
yauzl.open(zipPath, { lazyEntries: true, autoClose: true }, (openErr, zipFile) => {
|
|
7141
|
+
if (openErr) {
|
|
7142
|
+
reject(openErr);
|
|
7143
|
+
return;
|
|
7144
|
+
}
|
|
7145
|
+
if (!zipFile) {
|
|
7146
|
+
reject(new Error('无法读取 ZIP 文件'));
|
|
7147
|
+
return;
|
|
7148
|
+
}
|
|
7149
|
+
let entryCount = 0;
|
|
7150
|
+
let totalUncompressedBytes = 0;
|
|
7151
|
+
let settled = false;
|
|
7152
|
+
const finish = (err, data) => {
|
|
7153
|
+
if (settled) return;
|
|
7154
|
+
settled = true;
|
|
7155
|
+
try {
|
|
7156
|
+
zipFile.close();
|
|
7157
|
+
} catch (_) {}
|
|
7158
|
+
if (err) {
|
|
7159
|
+
reject(err);
|
|
7160
|
+
} else {
|
|
7161
|
+
resolve(data);
|
|
7162
|
+
}
|
|
7163
|
+
};
|
|
7164
|
+
|
|
7165
|
+
zipFile.on('entry', (entry) => {
|
|
7166
|
+
if (settled) return;
|
|
7167
|
+
entryCount += 1;
|
|
7168
|
+
const entrySize = Number.isFinite(entry.uncompressedSize) ? entry.uncompressedSize : 0;
|
|
7169
|
+
totalUncompressedBytes += entrySize;
|
|
7170
|
+
if (entryCount > maxEntryCount) {
|
|
7171
|
+
finish(new Error(`压缩包条目过多(>${maxEntryCount})`));
|
|
7172
|
+
return;
|
|
7173
|
+
}
|
|
7174
|
+
if (totalUncompressedBytes > maxUncompressedBytes) {
|
|
7175
|
+
finish(new Error(`压缩包解压总大小超限(>${Math.floor(maxUncompressedBytes / 1024 / 1024)}MB)`));
|
|
7176
|
+
return;
|
|
7177
|
+
}
|
|
7178
|
+
zipFile.readEntry();
|
|
7179
|
+
});
|
|
7180
|
+
|
|
7181
|
+
zipFile.on('end', () => {
|
|
7182
|
+
finish(null, { entryCount, totalUncompressedBytes });
|
|
7183
|
+
});
|
|
7184
|
+
|
|
7185
|
+
zipFile.on('error', (zipErr) => {
|
|
7186
|
+
finish(zipErr);
|
|
7187
|
+
});
|
|
7188
|
+
|
|
7189
|
+
zipFile.readEntry();
|
|
7190
|
+
});
|
|
7191
|
+
});
|
|
7192
|
+
}
|
|
7193
|
+
|
|
7194
|
+
function writeUploadZipStream(req, prefix, originalName = '', maxSize = MAX_SKILLS_ZIP_UPLOAD_SIZE) {
|
|
7195
|
+
return new Promise((resolve, reject) => {
|
|
7196
|
+
const lengthHeader = parseInt(req.headers['content-length'] || '0', 10);
|
|
7197
|
+
if (Number.isFinite(lengthHeader) && lengthHeader > maxSize) {
|
|
7198
|
+
reject(new Error(`备份文件过大(>${Math.floor(maxSize / 1024 / 1024)}MB)`));
|
|
7199
|
+
return;
|
|
7200
|
+
}
|
|
7201
|
+
|
|
7202
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `${prefix}-`));
|
|
7203
|
+
const rawName = originalName && typeof originalName === 'string' ? originalName : `${prefix}.zip`;
|
|
7204
|
+
const fileName = path.basename(rawName);
|
|
7205
|
+
const zipPath = path.join(tempDir, fileName.toLowerCase().endsWith('.zip') ? fileName : `${fileName}.zip`);
|
|
7206
|
+
const stream = fs.createWriteStream(zipPath);
|
|
7207
|
+
let bytesWritten = 0;
|
|
7208
|
+
let settled = false;
|
|
7209
|
+
let hasContent = false;
|
|
7210
|
+
|
|
7211
|
+
const fail = (err) => {
|
|
7212
|
+
if (settled) return;
|
|
7213
|
+
settled = true;
|
|
7214
|
+
try {
|
|
7215
|
+
stream.destroy();
|
|
7216
|
+
} catch (_) {}
|
|
7217
|
+
try {
|
|
7218
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
7219
|
+
} catch (_) {}
|
|
7220
|
+
reject(err);
|
|
7221
|
+
};
|
|
7222
|
+
|
|
7223
|
+
const done = () => {
|
|
7224
|
+
if (settled) return;
|
|
7225
|
+
settled = true;
|
|
7226
|
+
if (!hasContent || bytesWritten <= 0) {
|
|
7227
|
+
try {
|
|
7228
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
7229
|
+
} catch (_) {}
|
|
7230
|
+
reject(new Error('备份文件为空'));
|
|
7231
|
+
return;
|
|
7232
|
+
}
|
|
7233
|
+
resolve({ tempDir, zipPath });
|
|
7234
|
+
};
|
|
7235
|
+
|
|
7236
|
+
req.on('error', (err) => fail(err));
|
|
7237
|
+
req.on('aborted', () => fail(new Error('上传已中断')));
|
|
7238
|
+
req.on('close', () => {
|
|
7239
|
+
if (!settled && !req.complete) {
|
|
7240
|
+
fail(new Error('上传已中断'));
|
|
7241
|
+
}
|
|
7242
|
+
});
|
|
7243
|
+
stream.on('error', (err) => fail(err));
|
|
7244
|
+
req.on('data', (chunk) => {
|
|
7245
|
+
if (settled) return;
|
|
7246
|
+
hasContent = true;
|
|
7247
|
+
bytesWritten += chunk.length;
|
|
7248
|
+
if (bytesWritten > maxSize) {
|
|
7249
|
+
fail(new Error(`备份文件过大(>${Math.floor(maxSize / 1024 / 1024)}MB)`));
|
|
7250
|
+
try {
|
|
7251
|
+
req.destroy();
|
|
7252
|
+
} catch (_) {}
|
|
7253
|
+
return;
|
|
7254
|
+
}
|
|
7255
|
+
stream.write(chunk);
|
|
7256
|
+
});
|
|
7257
|
+
req.on('end', () => {
|
|
7258
|
+
if (settled) return;
|
|
7259
|
+
stream.end(() => done());
|
|
7260
|
+
});
|
|
7261
|
+
});
|
|
7262
|
+
}
|
|
7263
|
+
|
|
6751
7264
|
function writeUploadZip(base64, prefix, originalName = '') {
|
|
6752
7265
|
let buffer;
|
|
6753
7266
|
try {
|
|
@@ -7272,27 +7785,339 @@ async function cmdUnzip(zipPath, outputDir) {
|
|
|
7272
7785
|
}
|
|
7273
7786
|
}
|
|
7274
7787
|
|
|
7275
|
-
function
|
|
7276
|
-
|
|
7277
|
-
|
|
7278
|
-
return fallback;
|
|
7788
|
+
function splitExtractSuffixInput(rawValue) {
|
|
7789
|
+
if (Array.isArray(rawValue)) {
|
|
7790
|
+
return rawValue.flatMap((item) => splitExtractSuffixInput(item));
|
|
7279
7791
|
}
|
|
7280
|
-
|
|
7281
|
-
|
|
7282
|
-
const resolved = path.resolve(trimmed);
|
|
7283
|
-
const hasTrailingSep = /[\\\/]$/.test(trimmed);
|
|
7284
|
-
if (hasTrailingSep) {
|
|
7285
|
-
ensureDir(resolved);
|
|
7286
|
-
return path.join(resolved, defaultFileName);
|
|
7792
|
+
if (typeof rawValue !== 'string') {
|
|
7793
|
+
return [];
|
|
7287
7794
|
}
|
|
7795
|
+
return rawValue
|
|
7796
|
+
.split(/[,\s]+/g)
|
|
7797
|
+
.map((item) => item.trim())
|
|
7798
|
+
.filter(Boolean);
|
|
7799
|
+
}
|
|
7288
7800
|
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
|
|
7801
|
+
function normalizeExtractSuffix(rawSuffix, fallbackSuffixes = DEFAULT_EXTRACT_SUFFIXES) {
|
|
7802
|
+
const fallbackItems = splitExtractSuffixInput(fallbackSuffixes);
|
|
7803
|
+
const sourceItems = splitExtractSuffixInput(rawSuffix);
|
|
7804
|
+
const source = sourceItems.length > 0 ? sourceItems : fallbackItems;
|
|
7805
|
+
const dedup = new Set();
|
|
7806
|
+
|
|
7807
|
+
for (const item of source) {
|
|
7808
|
+
const lower = item.toLowerCase();
|
|
7809
|
+
if (!lower) {
|
|
7810
|
+
continue;
|
|
7811
|
+
}
|
|
7812
|
+
const normalized = lower.startsWith('.') ? lower : `.${lower}`;
|
|
7813
|
+
if (normalized.length > 1) {
|
|
7814
|
+
dedup.add(normalized);
|
|
7815
|
+
}
|
|
7816
|
+
}
|
|
7817
|
+
|
|
7818
|
+
if (dedup.size === 0) {
|
|
7819
|
+
return [...DEFAULT_EXTRACT_SUFFIXES];
|
|
7820
|
+
}
|
|
7821
|
+
return Array.from(dedup);
|
|
7822
|
+
}
|
|
7823
|
+
|
|
7824
|
+
function buildDefaultExtractOutputDir(baseCwd = process.cwd()) {
|
|
7825
|
+
const normalizedCwd = path.resolve(baseCwd);
|
|
7826
|
+
const parentDir = path.dirname(normalizedCwd);
|
|
7827
|
+
const timestamp = formatTimestampForFileName().replace(/-/g, '');
|
|
7828
|
+
return path.join(parentDir, timestamp);
|
|
7829
|
+
}
|
|
7830
|
+
|
|
7831
|
+
function sanitizeNameSegment(rawValue, fallback = 'item') {
|
|
7832
|
+
const value = typeof rawValue === 'string' ? rawValue.trim() : '';
|
|
7833
|
+
const sanitized = value
|
|
7834
|
+
.replace(/[^\w.-]+/g, '_')
|
|
7835
|
+
.replace(/^_+|_+$/g, '');
|
|
7836
|
+
return sanitized || fallback;
|
|
7837
|
+
}
|
|
7838
|
+
|
|
7839
|
+
function resolveDuplicateOutputPath(outputDir, originalFileName, zipPath = '', counters = new Map()) {
|
|
7840
|
+
const fallbackName = `file${path.extname(originalFileName || '')}`;
|
|
7841
|
+
const fileName = path.basename(originalFileName || '') || fallbackName;
|
|
7842
|
+
const firstChoice = path.join(outputDir, fileName);
|
|
7843
|
+
const firstChoiceKey = `exact:${fileName}`;
|
|
7844
|
+
if (!counters.has(firstChoiceKey)) {
|
|
7845
|
+
counters.set(firstChoiceKey, true);
|
|
7846
|
+
if (!fs.existsSync(firstChoice)) {
|
|
7847
|
+
return firstChoice;
|
|
7848
|
+
}
|
|
7849
|
+
}
|
|
7850
|
+
|
|
7851
|
+
const ext = path.extname(fileName);
|
|
7852
|
+
const baseName = path.basename(fileName, ext);
|
|
7853
|
+
const safeBaseName = sanitizeNameSegment(baseName, 'file');
|
|
7854
|
+
const zipBaseName = sanitizeNameSegment(path.basename(zipPath || '', '.zip'), 'zip');
|
|
7855
|
+
const duplicateKey = `dup:${safeBaseName}|${zipBaseName}|${ext}`;
|
|
7856
|
+
let index = counters.has(duplicateKey) ? counters.get(duplicateKey) : 1;
|
|
7857
|
+
|
|
7858
|
+
for (; index <= 100000; index++) {
|
|
7859
|
+
const candidateName = `${safeBaseName}__${zipBaseName}__${index}${ext}`;
|
|
7860
|
+
const candidatePath = path.join(outputDir, candidateName);
|
|
7861
|
+
if (!fs.existsSync(candidatePath)) {
|
|
7862
|
+
counters.set(duplicateKey, index + 1);
|
|
7863
|
+
return candidatePath;
|
|
7864
|
+
}
|
|
7865
|
+
}
|
|
7866
|
+
|
|
7867
|
+
throw new Error(`重名文件过多,无法生成唯一文件名: ${fileName}`);
|
|
7868
|
+
}
|
|
7869
|
+
|
|
7870
|
+
function collectZipFilesFromDir(rootDir, recursive = true) {
|
|
7871
|
+
const queue = [rootDir];
|
|
7872
|
+
const result = [];
|
|
7873
|
+
|
|
7874
|
+
while (queue.length > 0) {
|
|
7875
|
+
const currentDir = queue.shift();
|
|
7876
|
+
let entries = [];
|
|
7877
|
+
try {
|
|
7878
|
+
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
7879
|
+
} catch (e) {
|
|
7880
|
+
throw new Error(`读取目录失败: ${currentDir} (${e.message})`);
|
|
7881
|
+
}
|
|
7882
|
+
|
|
7883
|
+
for (const entry of entries) {
|
|
7884
|
+
const entryPath = path.join(currentDir, entry.name);
|
|
7885
|
+
if (entry.isDirectory()) {
|
|
7886
|
+
if (recursive) {
|
|
7887
|
+
queue.push(entryPath);
|
|
7888
|
+
}
|
|
7889
|
+
continue;
|
|
7890
|
+
}
|
|
7891
|
+
if (entry.isFile() && entry.name.toLowerCase().endsWith('.zip')) {
|
|
7892
|
+
result.push(entryPath);
|
|
7893
|
+
}
|
|
7894
|
+
}
|
|
7895
|
+
}
|
|
7896
|
+
|
|
7897
|
+
result.sort((a, b) => a.localeCompare(b));
|
|
7898
|
+
return result;
|
|
7899
|
+
}
|
|
7900
|
+
|
|
7901
|
+
function extractMatchedEntriesFromZip(zipPath, outputDir, suffixes, duplicateCounters = new Map()) {
|
|
7902
|
+
const normalizedSuffixes = normalizeExtractSuffix(suffixes);
|
|
7903
|
+
return new Promise((resolve, reject) => {
|
|
7904
|
+
yauzl.open(zipPath, { lazyEntries: true, autoClose: false }, (openErr, zipFile) => {
|
|
7905
|
+
if (openErr) {
|
|
7906
|
+
reject(openErr);
|
|
7907
|
+
return;
|
|
7908
|
+
}
|
|
7909
|
+
if (!zipFile) {
|
|
7910
|
+
reject(new Error('无法读取 ZIP 文件'));
|
|
7911
|
+
return;
|
|
7912
|
+
}
|
|
7913
|
+
|
|
7914
|
+
let settled = false;
|
|
7915
|
+
let matched = 0;
|
|
7916
|
+
let extracted = 0;
|
|
7917
|
+
let skippedDir = 0;
|
|
7918
|
+
let skippedExt = 0;
|
|
7919
|
+
|
|
7920
|
+
const finish = (err) => {
|
|
7921
|
+
if (settled) return;
|
|
7922
|
+
settled = true;
|
|
7923
|
+
try {
|
|
7924
|
+
zipFile.close();
|
|
7925
|
+
} catch (_) {}
|
|
7926
|
+
if (err) {
|
|
7927
|
+
reject(err);
|
|
7928
|
+
} else {
|
|
7929
|
+
resolve({ matched, extracted, skippedDir, skippedExt });
|
|
7930
|
+
}
|
|
7931
|
+
};
|
|
7932
|
+
|
|
7933
|
+
zipFile.on('entry', (entry) => {
|
|
7934
|
+
if (settled) return;
|
|
7935
|
+
const rawEntryName = typeof entry.fileName === 'string' ? entry.fileName : '';
|
|
7936
|
+
const normalizedEntryName = rawEntryName.replace(/\\/g, '/');
|
|
7937
|
+
|
|
7938
|
+
if (!normalizedEntryName || normalizedEntryName.endsWith('/')) {
|
|
7939
|
+
skippedDir += 1;
|
|
7940
|
+
zipFile.readEntry();
|
|
7941
|
+
return;
|
|
7942
|
+
}
|
|
7943
|
+
|
|
7944
|
+
const entryBaseName = path.basename(normalizedEntryName);
|
|
7945
|
+
const lowerBaseName = entryBaseName.toLowerCase();
|
|
7946
|
+
const matchedSuffix = normalizedSuffixes.some((suffix) => lowerBaseName.endsWith(suffix));
|
|
7947
|
+
if (!entryBaseName || !matchedSuffix) {
|
|
7948
|
+
skippedExt += 1;
|
|
7949
|
+
zipFile.readEntry();
|
|
7950
|
+
return;
|
|
7951
|
+
}
|
|
7952
|
+
|
|
7953
|
+
matched += 1;
|
|
7954
|
+
zipFile.openReadStream(entry, (streamErr, readStream) => {
|
|
7955
|
+
if (streamErr || !readStream) {
|
|
7956
|
+
finish(streamErr || new Error('无法读取 ZIP 条目流'));
|
|
7957
|
+
return;
|
|
7958
|
+
}
|
|
7959
|
+
|
|
7960
|
+
let completed = false;
|
|
7961
|
+
const outputPath = resolveDuplicateOutputPath(outputDir, entryBaseName, zipPath, duplicateCounters);
|
|
7962
|
+
const writeStream = fs.createWriteStream(outputPath);
|
|
7963
|
+
const fail = (writeErr) => {
|
|
7964
|
+
if (completed) return;
|
|
7965
|
+
completed = true;
|
|
7966
|
+
try {
|
|
7967
|
+
readStream.destroy();
|
|
7968
|
+
} catch (_) {}
|
|
7969
|
+
try {
|
|
7970
|
+
writeStream.destroy();
|
|
7971
|
+
} catch (_) {}
|
|
7972
|
+
try {
|
|
7973
|
+
if (fs.existsSync(outputPath)) {
|
|
7974
|
+
fs.unlinkSync(outputPath);
|
|
7975
|
+
}
|
|
7976
|
+
} catch (_) {}
|
|
7977
|
+
finish(writeErr);
|
|
7978
|
+
};
|
|
7979
|
+
|
|
7980
|
+
readStream.on('error', fail);
|
|
7981
|
+
writeStream.on('error', fail);
|
|
7982
|
+
writeStream.on('finish', () => {
|
|
7983
|
+
if (completed || settled) return;
|
|
7984
|
+
completed = true;
|
|
7985
|
+
extracted += 1;
|
|
7986
|
+
zipFile.readEntry();
|
|
7987
|
+
});
|
|
7988
|
+
|
|
7989
|
+
readStream.pipe(writeStream);
|
|
7990
|
+
});
|
|
7991
|
+
});
|
|
7992
|
+
|
|
7993
|
+
zipFile.on('end', () => {
|
|
7994
|
+
finish(null);
|
|
7995
|
+
});
|
|
7996
|
+
zipFile.on('error', (zipErr) => {
|
|
7997
|
+
finish(zipErr);
|
|
7998
|
+
});
|
|
7999
|
+
|
|
8000
|
+
zipFile.readEntry();
|
|
8001
|
+
});
|
|
8002
|
+
});
|
|
8003
|
+
}
|
|
8004
|
+
|
|
8005
|
+
async function cmdUnzipExt(zipDirPath, outputDir, options = {}) {
|
|
8006
|
+
if (!zipDirPath) {
|
|
8007
|
+
console.error('用法: codexmate unzip-ext <zip目录> [输出目录] [--ext:后缀[,后缀...]] [--no-recursive]');
|
|
8008
|
+
console.log('\n示例:');
|
|
8009
|
+
console.log(' codexmate unzip-ext ./archives');
|
|
8010
|
+
console.log(' codexmate unzip-ext ./archives ./output --ext:json,txt');
|
|
8011
|
+
console.log(' codexmate unzip-ext D:/data/zips --ext:txt --no-recursive');
|
|
8012
|
+
console.log(' 说明: 默认递归扫描子目录,可通过 --no-recursive 关闭递归');
|
|
8013
|
+
process.exit(1);
|
|
8014
|
+
}
|
|
8015
|
+
|
|
8016
|
+
const recursive = options.recursive !== false;
|
|
8017
|
+
const suffixes = normalizeExtractSuffix(options.ext);
|
|
8018
|
+
const absZipDir = path.resolve(zipDirPath);
|
|
8019
|
+
const absOutputDir = outputDir ? path.resolve(outputDir) : buildDefaultExtractOutputDir(process.cwd());
|
|
8020
|
+
|
|
8021
|
+
if (!fs.existsSync(absZipDir)) {
|
|
8022
|
+
console.error('错误: 目录不存在:', absZipDir);
|
|
8023
|
+
process.exit(1);
|
|
8024
|
+
}
|
|
8025
|
+
try {
|
|
8026
|
+
if (!fs.statSync(absZipDir).isDirectory()) {
|
|
8027
|
+
console.error('错误: 仅支持目录路径:', absZipDir);
|
|
8028
|
+
process.exit(1);
|
|
8029
|
+
}
|
|
8030
|
+
} catch (e) {
|
|
8031
|
+
console.error('错误: 无法读取目录信息:', e.message);
|
|
8032
|
+
process.exit(1);
|
|
8033
|
+
}
|
|
8034
|
+
|
|
8035
|
+
let zipFiles = [];
|
|
8036
|
+
try {
|
|
8037
|
+
zipFiles = collectZipFilesFromDir(absZipDir, recursive);
|
|
8038
|
+
} catch (e) {
|
|
8039
|
+
console.error('扫描 ZIP 文件失败:', e.message);
|
|
8040
|
+
process.exit(1);
|
|
8041
|
+
}
|
|
8042
|
+
|
|
8043
|
+
if (zipFiles.length === 0) {
|
|
8044
|
+
console.error('错误: 未找到任何 ZIP 文件');
|
|
8045
|
+
process.exit(1);
|
|
8046
|
+
}
|
|
8047
|
+
|
|
8048
|
+
ensureDir(absOutputDir);
|
|
8049
|
+
|
|
8050
|
+
console.log('\n批量解压配置:');
|
|
8051
|
+
console.log(' ZIP 目录:', absZipDir);
|
|
8052
|
+
console.log(' 输出目录:', absOutputDir);
|
|
8053
|
+
console.log(' 后缀过滤:', suffixes.join(', '));
|
|
8054
|
+
console.log(' 递归扫描:', recursive ? '是' : '否');
|
|
8055
|
+
console.log(' ZIP 数量:', zipFiles.length);
|
|
8056
|
+
console.log('\n开始提取...\n');
|
|
8057
|
+
|
|
8058
|
+
let totalMatched = 0;
|
|
8059
|
+
let totalExtracted = 0;
|
|
8060
|
+
let totalSkippedDir = 0;
|
|
8061
|
+
let totalSkippedExt = 0;
|
|
8062
|
+
const failed = [];
|
|
8063
|
+
const duplicateCounters = new Map();
|
|
8064
|
+
|
|
8065
|
+
for (const zipFilePath of zipFiles) {
|
|
8066
|
+
try {
|
|
8067
|
+
await inspectZipArchiveLimits(zipFilePath, {
|
|
8068
|
+
maxEntryCount: MAX_SKILLS_ZIP_ENTRY_COUNT,
|
|
8069
|
+
maxUncompressedBytes: MAX_SKILLS_ZIP_UNCOMPRESSED_BYTES
|
|
8070
|
+
});
|
|
8071
|
+
const result = await extractMatchedEntriesFromZip(zipFilePath, absOutputDir, suffixes, duplicateCounters);
|
|
8072
|
+
totalMatched += result.matched;
|
|
8073
|
+
totalExtracted += result.extracted;
|
|
8074
|
+
totalSkippedDir += result.skippedDir;
|
|
8075
|
+
totalSkippedExt += result.skippedExt;
|
|
8076
|
+
console.log(`✓ ${path.basename(zipFilePath)}: 命中 ${result.matched},提取 ${result.extracted}`);
|
|
8077
|
+
} catch (e) {
|
|
8078
|
+
failed.push({ zipFilePath, message: e && e.message ? e.message : String(e) });
|
|
8079
|
+
console.error(`✗ ${path.basename(zipFilePath)}: ${e && e.message ? e.message : e}`);
|
|
8080
|
+
}
|
|
8081
|
+
}
|
|
8082
|
+
|
|
8083
|
+
console.log('\n提取结果:');
|
|
8084
|
+
console.log(' 输出目录:', absOutputDir);
|
|
8085
|
+
console.log(' 扫描 ZIP:', zipFiles.length);
|
|
8086
|
+
console.log(' 命中条目:', totalMatched);
|
|
8087
|
+
console.log(' 已提取:', totalExtracted);
|
|
8088
|
+
console.log(' 已跳过(目录条目):', totalSkippedDir);
|
|
8089
|
+
console.log(' 已跳过(后缀不匹配):', totalSkippedExt);
|
|
8090
|
+
if (failed.length > 0) {
|
|
8091
|
+
console.error(' 失败数量:', failed.length);
|
|
8092
|
+
for (const item of failed) {
|
|
8093
|
+
console.error(` - ${item.zipFilePath}: ${item.message}`);
|
|
8094
|
+
}
|
|
8095
|
+
process.exit(1);
|
|
8096
|
+
}
|
|
8097
|
+
console.log();
|
|
8098
|
+
}
|
|
8099
|
+
|
|
8100
|
+
function resolveExportOutputPath(outputPath, defaultFileName) {
|
|
8101
|
+
const fallback = path.resolve(process.cwd(), defaultFileName);
|
|
8102
|
+
if (typeof outputPath !== 'string' || !outputPath.trim()) {
|
|
8103
|
+
return fallback;
|
|
8104
|
+
}
|
|
8105
|
+
|
|
8106
|
+
const trimmed = outputPath.trim();
|
|
8107
|
+
const resolved = path.resolve(trimmed);
|
|
8108
|
+
const hasTrailingSep = /[\\\/]$/.test(trimmed);
|
|
8109
|
+
if (hasTrailingSep) {
|
|
8110
|
+
ensureDir(resolved);
|
|
8111
|
+
return path.join(resolved, defaultFileName);
|
|
8112
|
+
}
|
|
8113
|
+
|
|
8114
|
+
if (fs.existsSync(resolved)) {
|
|
8115
|
+
try {
|
|
8116
|
+
const stat = fs.statSync(resolved);
|
|
8117
|
+
if (stat.isDirectory()) {
|
|
8118
|
+
return path.join(resolved, defaultFileName);
|
|
8119
|
+
}
|
|
8120
|
+
} catch (e) {}
|
|
7296
8121
|
}
|
|
7297
8122
|
|
|
7298
8123
|
return resolved;
|
|
@@ -7443,7 +8268,7 @@ async function cmdExportSession(args = []) {
|
|
|
7443
8268
|
}
|
|
7444
8269
|
|
|
7445
8270
|
function parseStartOptions(args = []) {
|
|
7446
|
-
const options = { host: '' };
|
|
8271
|
+
const options = { host: '', noBrowser: false };
|
|
7447
8272
|
if (!Array.isArray(args)) {
|
|
7448
8273
|
return options;
|
|
7449
8274
|
}
|
|
@@ -7451,6 +8276,10 @@ function parseStartOptions(args = []) {
|
|
|
7451
8276
|
for (let i = 0; i < args.length; i++) {
|
|
7452
8277
|
const arg = args[i];
|
|
7453
8278
|
if (!arg) continue;
|
|
8279
|
+
if (arg === '--no-browser') {
|
|
8280
|
+
options.noBrowser = true;
|
|
8281
|
+
continue;
|
|
8282
|
+
}
|
|
7454
8283
|
if (arg.startsWith('--host=')) {
|
|
7455
8284
|
options.host = arg.slice('--host='.length);
|
|
7456
8285
|
continue;
|
|
@@ -7523,11 +8352,125 @@ function watchPathsForRestart(targets, onChange) {
|
|
|
7523
8352
|
};
|
|
7524
8353
|
}
|
|
7525
8354
|
|
|
8355
|
+
function writeJsonResponse(res, statusCode, payload) {
|
|
8356
|
+
const body = JSON.stringify(payload, null, 2);
|
|
8357
|
+
res.writeHead(statusCode, {
|
|
8358
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
8359
|
+
'Content-Length': Buffer.byteLength(body, 'utf-8')
|
|
8360
|
+
});
|
|
8361
|
+
res.end(body, 'utf-8');
|
|
8362
|
+
}
|
|
8363
|
+
|
|
8364
|
+
function streamZipDownloadResponse(res, filePath, options = {}) {
|
|
8365
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
8366
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
8367
|
+
res.end('File Not Found');
|
|
8368
|
+
return;
|
|
8369
|
+
}
|
|
8370
|
+
const stat = fs.statSync(filePath);
|
|
8371
|
+
if (!stat.isFile()) {
|
|
8372
|
+
res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
8373
|
+
res.end('Not a File');
|
|
8374
|
+
return;
|
|
8375
|
+
}
|
|
8376
|
+
const downloadName = typeof options.fileName === 'string' && options.fileName.trim()
|
|
8377
|
+
? options.fileName.trim()
|
|
8378
|
+
: path.basename(filePath);
|
|
8379
|
+
const deleteAfterDownload = !!options.deleteAfterDownload;
|
|
8380
|
+
const onAfterComplete = typeof options.onAfterComplete === 'function'
|
|
8381
|
+
? options.onAfterComplete
|
|
8382
|
+
: null;
|
|
8383
|
+
res.writeHead(200, {
|
|
8384
|
+
'Content-Type': 'application/zip',
|
|
8385
|
+
'Content-Disposition': `attachment; filename="${path.basename(downloadName)}"`,
|
|
8386
|
+
'Content-Length': stat.size
|
|
8387
|
+
});
|
|
8388
|
+
|
|
8389
|
+
const stream = fs.createReadStream(filePath);
|
|
8390
|
+
let finished = false;
|
|
8391
|
+
const finalize = () => {
|
|
8392
|
+
if (finished) return;
|
|
8393
|
+
finished = true;
|
|
8394
|
+
if (deleteAfterDownload && fs.existsSync(filePath)) {
|
|
8395
|
+
try {
|
|
8396
|
+
fs.unlinkSync(filePath);
|
|
8397
|
+
} catch (_) {}
|
|
8398
|
+
}
|
|
8399
|
+
if (onAfterComplete) {
|
|
8400
|
+
try {
|
|
8401
|
+
onAfterComplete();
|
|
8402
|
+
} catch (_) {}
|
|
8403
|
+
}
|
|
8404
|
+
};
|
|
8405
|
+
stream.on('error', () => {
|
|
8406
|
+
if (!res.headersSent) {
|
|
8407
|
+
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
8408
|
+
res.end('Download Error');
|
|
8409
|
+
} else {
|
|
8410
|
+
try {
|
|
8411
|
+
res.destroy();
|
|
8412
|
+
} catch (_) {}
|
|
8413
|
+
}
|
|
8414
|
+
finalize();
|
|
8415
|
+
});
|
|
8416
|
+
res.on('finish', finalize);
|
|
8417
|
+
res.on('close', finalize);
|
|
8418
|
+
stream.pipe(res);
|
|
8419
|
+
}
|
|
8420
|
+
|
|
8421
|
+
function resolveUploadFileNameFromRequest(req, fallbackName = 'codex-skills.zip') {
|
|
8422
|
+
const rawHeader = req.headers['x-codexmate-file-name'];
|
|
8423
|
+
const source = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader;
|
|
8424
|
+
const fallback = typeof fallbackName === 'string' && fallbackName.trim()
|
|
8425
|
+
? fallbackName.trim()
|
|
8426
|
+
: 'codex-skills.zip';
|
|
8427
|
+
if (!source || typeof source !== 'string') {
|
|
8428
|
+
return fallback;
|
|
8429
|
+
}
|
|
8430
|
+
const decoded = (() => {
|
|
8431
|
+
try {
|
|
8432
|
+
return decodeURIComponent(source);
|
|
8433
|
+
} catch (_) {
|
|
8434
|
+
return source;
|
|
8435
|
+
}
|
|
8436
|
+
})();
|
|
8437
|
+
const normalized = path.basename(decoded.trim());
|
|
8438
|
+
return normalized || fallback;
|
|
8439
|
+
}
|
|
8440
|
+
|
|
8441
|
+
async function handleImportCodexSkillsZipUpload(req, res) {
|
|
8442
|
+
if (req.method !== 'POST') {
|
|
8443
|
+
writeJsonResponse(res, 405, { error: 'Method Not Allowed' });
|
|
8444
|
+
return;
|
|
8445
|
+
}
|
|
8446
|
+
try {
|
|
8447
|
+
const fileName = resolveUploadFileNameFromRequest(req, 'codex-skills.zip');
|
|
8448
|
+
const upload = await writeUploadZipStream(
|
|
8449
|
+
req,
|
|
8450
|
+
'codex-skills-import',
|
|
8451
|
+
fileName,
|
|
8452
|
+
MAX_SKILLS_ZIP_UPLOAD_SIZE
|
|
8453
|
+
);
|
|
8454
|
+
const result = await importCodexSkillsFromZipFile(upload.zipPath, {
|
|
8455
|
+
tempDir: upload.tempDir,
|
|
8456
|
+
fallbackName: fileName
|
|
8457
|
+
});
|
|
8458
|
+
writeJsonResponse(res, 200, result || {});
|
|
8459
|
+
} catch (e) {
|
|
8460
|
+
const message = e && e.message ? e.message : '上传失败';
|
|
8461
|
+
writeJsonResponse(res, 400, { error: message });
|
|
8462
|
+
}
|
|
8463
|
+
}
|
|
8464
|
+
|
|
7526
8465
|
function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser }) {
|
|
7527
8466
|
const connections = new Set();
|
|
7528
8467
|
|
|
7529
8468
|
const server = http.createServer((req, res) => {
|
|
7530
8469
|
const requestPath = (req.url || '/').split('?')[0];
|
|
8470
|
+
if (requestPath === '/api/import-codex-skills-zip') {
|
|
8471
|
+
void handleImportCodexSkillsZipUpload(req, res);
|
|
8472
|
+
return;
|
|
8473
|
+
}
|
|
7531
8474
|
if (requestPath === '/api') {
|
|
7532
8475
|
let body = '';
|
|
7533
8476
|
req.on('data', chunk => body += chunk);
|
|
@@ -7651,6 +8594,9 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
7651
8594
|
case 'import-codex-skills':
|
|
7652
8595
|
result = importCodexSkills(params || {});
|
|
7653
8596
|
break;
|
|
8597
|
+
case 'export-codex-skills':
|
|
8598
|
+
result = await exportCodexSkills(params || {});
|
|
8599
|
+
break;
|
|
7654
8600
|
case 'get-openclaw-config':
|
|
7655
8601
|
result = readOpenclawConfigFile();
|
|
7656
8602
|
break;
|
|
@@ -7909,32 +8855,35 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
7909
8855
|
fs.createReadStream(filePath).pipe(res);
|
|
7910
8856
|
} else if (requestPath.startsWith('/download/')) {
|
|
7911
8857
|
const fileName = requestPath.slice('/download/'.length);
|
|
7912
|
-
|
|
7913
|
-
|
|
7914
|
-
|
|
7915
|
-
|
|
7916
|
-
|
|
7917
|
-
res.
|
|
7918
|
-
res.end('Forbidden');
|
|
8858
|
+
let decodedFileName = '';
|
|
8859
|
+
try {
|
|
8860
|
+
decodedFileName = decodeURIComponent(fileName);
|
|
8861
|
+
} catch (_) {
|
|
8862
|
+
res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
8863
|
+
res.end('Bad Request');
|
|
7919
8864
|
return;
|
|
7920
8865
|
}
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
|
|
8866
|
+
|
|
8867
|
+
const artifact = resolveDownloadArtifact(decodedFileName, { consume: true });
|
|
8868
|
+
if (artifact) {
|
|
8869
|
+
streamZipDownloadResponse(res, artifact.filePath, {
|
|
8870
|
+
fileName: artifact.fileName,
|
|
8871
|
+
deleteAfterDownload: artifact.deleteAfterDownload !== false
|
|
8872
|
+
});
|
|
7924
8873
|
return;
|
|
7925
8874
|
}
|
|
7926
|
-
|
|
7927
|
-
|
|
7928
|
-
|
|
7929
|
-
|
|
8875
|
+
|
|
8876
|
+
const tempDir = os.tmpdir();
|
|
8877
|
+
const legacyFilePath = path.join(tempDir, decodedFileName);
|
|
8878
|
+
if (!isPathInside(legacyFilePath, tempDir)) {
|
|
8879
|
+
res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
8880
|
+
res.end('Forbidden');
|
|
7930
8881
|
return;
|
|
7931
8882
|
}
|
|
7932
|
-
res
|
|
7933
|
-
|
|
7934
|
-
|
|
7935
|
-
'Content-Length': stat.size
|
|
8883
|
+
streamZipDownloadResponse(res, legacyFilePath, {
|
|
8884
|
+
fileName: path.basename(legacyFilePath),
|
|
8885
|
+
deleteAfterDownload: false
|
|
7936
8886
|
});
|
|
7937
|
-
fs.createReadStream(filePath).pipe(res);
|
|
7938
8887
|
} else if (requestPath.startsWith('/res/')) {
|
|
7939
8888
|
const normalized = path.normalize(requestPath).replace(/^([\\.\\/])+/, '');
|
|
7940
8889
|
const filePath = path.join(__dirname, normalized);
|
|
@@ -8057,7 +9006,7 @@ function cmdStart(options = {}) {
|
|
|
8057
9006
|
webDir,
|
|
8058
9007
|
host,
|
|
8059
9008
|
port,
|
|
8060
|
-
openBrowser:
|
|
9009
|
+
openBrowser: !options.noBrowser
|
|
8061
9010
|
});
|
|
8062
9011
|
|
|
8063
9012
|
const proxySettings = readBuiltinProxySettings();
|
|
@@ -8597,7 +9546,317 @@ async function cmdWorkflow(args = []) {
|
|
|
8597
9546
|
throw new Error(`未知 workflow 子命令: ${subcommand}`);
|
|
8598
9547
|
}
|
|
8599
9548
|
|
|
8600
|
-
|
|
9549
|
+
// #region parseCodexProxyOptions
|
|
9550
|
+
function parseCodexProxyOptions(args = []) {
|
|
9551
|
+
const options = {
|
|
9552
|
+
passthroughArgs: [],
|
|
9553
|
+
queuedFollowUps: []
|
|
9554
|
+
};
|
|
9555
|
+
const argv = Array.isArray(args) ? args : [];
|
|
9556
|
+
|
|
9557
|
+
const pushFollowUp = (value, optionName) => {
|
|
9558
|
+
const raw = value === undefined || value === null ? '' : String(value);
|
|
9559
|
+
if (!raw.trim()) {
|
|
9560
|
+
throw new Error(`${optionName} 需要提供非空内容`);
|
|
9561
|
+
}
|
|
9562
|
+
options.queuedFollowUps.push(raw);
|
|
9563
|
+
};
|
|
9564
|
+
|
|
9565
|
+
for (let i = 0; i < argv.length; i++) {
|
|
9566
|
+
const arg = argv[i];
|
|
9567
|
+
if (arg === undefined || arg === null) {
|
|
9568
|
+
continue;
|
|
9569
|
+
}
|
|
9570
|
+
const text = String(arg);
|
|
9571
|
+
if (text === '--') {
|
|
9572
|
+
options.passthroughArgs.push(...argv.slice(i).map((item) => String(item)));
|
|
9573
|
+
break;
|
|
9574
|
+
}
|
|
9575
|
+
if (text === '--queued-follow-up' || text === '--follow-up') {
|
|
9576
|
+
const next = argv[i + 1];
|
|
9577
|
+
if (next === undefined) {
|
|
9578
|
+
throw new Error(`${text} 需要提供内容`);
|
|
9579
|
+
}
|
|
9580
|
+
pushFollowUp(next, text);
|
|
9581
|
+
i += 1;
|
|
9582
|
+
continue;
|
|
9583
|
+
}
|
|
9584
|
+
if (text.startsWith('--queued-follow-up=')) {
|
|
9585
|
+
pushFollowUp(text.slice('--queued-follow-up='.length), '--queued-follow-up');
|
|
9586
|
+
continue;
|
|
9587
|
+
}
|
|
9588
|
+
if (text.startsWith('--follow-up=')) {
|
|
9589
|
+
pushFollowUp(text.slice('--follow-up='.length), '--follow-up');
|
|
9590
|
+
continue;
|
|
9591
|
+
}
|
|
9592
|
+
options.passthroughArgs.push(text);
|
|
9593
|
+
}
|
|
9594
|
+
|
|
9595
|
+
return options;
|
|
9596
|
+
}
|
|
9597
|
+
// #endregion parseCodexProxyOptions
|
|
9598
|
+
|
|
9599
|
+
function shellEscapePosixArg(value) {
|
|
9600
|
+
const text = value === undefined || value === null ? '' : String(value);
|
|
9601
|
+
return `'${text.replace(/'/g, `'\"'\"'`)}'`;
|
|
9602
|
+
}
|
|
9603
|
+
|
|
9604
|
+
// #region buildScriptCommandArgs
|
|
9605
|
+
function buildScriptCommandArgs(commandLine) {
|
|
9606
|
+
const platform = process.platform;
|
|
9607
|
+
// util-linux script needs -e/--return to propagate child exit code.
|
|
9608
|
+
if (platform === 'linux' || platform === 'android') {
|
|
9609
|
+
return ['-q', '-e', '-c', commandLine, '/dev/null'];
|
|
9610
|
+
}
|
|
9611
|
+
// NetBSD supports -e/-c, matching util-linux style contract.
|
|
9612
|
+
if (platform === 'netbsd') {
|
|
9613
|
+
return ['-q', '-e', '-c', commandLine, '/dev/null'];
|
|
9614
|
+
}
|
|
9615
|
+
// OpenBSD supports "-c <command>" with a trailing output file path.
|
|
9616
|
+
if (platform === 'openbsd') {
|
|
9617
|
+
return ['-c', commandLine, '/dev/null'];
|
|
9618
|
+
}
|
|
9619
|
+
// BSD/macOS script does not support util-linux "-c <cmd>" syntax.
|
|
9620
|
+
if (platform === 'darwin' || platform === 'freebsd') {
|
|
9621
|
+
return ['-q', '/dev/null', 'sh', '-lc', commandLine];
|
|
9622
|
+
}
|
|
9623
|
+
throw new Error(`当前平台暂不支持 --follow-up 自动排队(platform=${platform})`);
|
|
9624
|
+
}
|
|
9625
|
+
// #endregion buildScriptCommandArgs
|
|
9626
|
+
|
|
9627
|
+
// #region runProxyCommandWithQueuedFollowUps
|
|
9628
|
+
async function runProxyCommandWithQueuedFollowUps(selectedBin, finalArgs = [], queuedFollowUps = []) {
|
|
9629
|
+
if (!process.stdin || !process.stdin.isTTY) {
|
|
9630
|
+
throw new Error('当前 stdin 不是 TTY,无法使用 --follow-up 自动排队。');
|
|
9631
|
+
}
|
|
9632
|
+
|
|
9633
|
+
const scriptPath = resolveCommandPath('script');
|
|
9634
|
+
if (!scriptPath) {
|
|
9635
|
+
throw new Error('未找到 script 命令,无法自动注入 queued follow-up 消息。');
|
|
9636
|
+
}
|
|
9637
|
+
|
|
9638
|
+
const commandLine = [selectedBin, ...finalArgs].map((item) => shellEscapePosixArg(item)).join(' ');
|
|
9639
|
+
const scriptArgs = buildScriptCommandArgs(commandLine);
|
|
9640
|
+
|
|
9641
|
+
return new Promise((resolve, reject) => {
|
|
9642
|
+
let settled = false;
|
|
9643
|
+
const child = spawn(scriptPath, scriptArgs, {
|
|
9644
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
9645
|
+
});
|
|
9646
|
+
|
|
9647
|
+
const stdin = process.stdin;
|
|
9648
|
+
const hadRawMode = !!stdin.isRaw;
|
|
9649
|
+
let cleanedUp = false;
|
|
9650
|
+
let waitingDrain = false;
|
|
9651
|
+
let followUpsFlushed = false;
|
|
9652
|
+
let outputReadyDetected = false;
|
|
9653
|
+
const timers = [];
|
|
9654
|
+
const pendingWrites = [];
|
|
9655
|
+
let onChildStdinDrain = null;
|
|
9656
|
+
let onChildStdinError = null;
|
|
9657
|
+
const resolveOnce = (code) => {
|
|
9658
|
+
if (settled) return;
|
|
9659
|
+
settled = true;
|
|
9660
|
+
resolve(code);
|
|
9661
|
+
};
|
|
9662
|
+
const rejectOnce = (error) => {
|
|
9663
|
+
if (settled) return;
|
|
9664
|
+
settled = true;
|
|
9665
|
+
reject(error);
|
|
9666
|
+
};
|
|
9667
|
+
const handleWriteFailure = (error) => {
|
|
9668
|
+
const err = error instanceof Error ? error : new Error(String(error || 'unknown'));
|
|
9669
|
+
cleanup();
|
|
9670
|
+
try {
|
|
9671
|
+
if (!child.killed) {
|
|
9672
|
+
child.kill('SIGTERM');
|
|
9673
|
+
}
|
|
9674
|
+
} catch (_) {
|
|
9675
|
+
// Ignore failure to terminate child after stdin write failure.
|
|
9676
|
+
}
|
|
9677
|
+
rejectOnce(new Error(`写入 ${selectedBin} stdin 失败: ${err.message}`));
|
|
9678
|
+
};
|
|
9679
|
+
const flushPendingWrites = () => {
|
|
9680
|
+
if (cleanedUp || child.stdin.destroyed) {
|
|
9681
|
+
pendingWrites.length = 0;
|
|
9682
|
+
return;
|
|
9683
|
+
}
|
|
9684
|
+
while (pendingWrites.length > 0) {
|
|
9685
|
+
const chunk = pendingWrites[0];
|
|
9686
|
+
let canContinue = true;
|
|
9687
|
+
try {
|
|
9688
|
+
canContinue = child.stdin.write(chunk, (error) => {
|
|
9689
|
+
if (error) {
|
|
9690
|
+
handleWriteFailure(error);
|
|
9691
|
+
}
|
|
9692
|
+
});
|
|
9693
|
+
} catch (error) {
|
|
9694
|
+
handleWriteFailure(error);
|
|
9695
|
+
return;
|
|
9696
|
+
}
|
|
9697
|
+
pendingWrites.shift();
|
|
9698
|
+
if (!canContinue) {
|
|
9699
|
+
waitingDrain = true;
|
|
9700
|
+
try {
|
|
9701
|
+
stdin.pause();
|
|
9702
|
+
} catch (_) {
|
|
9703
|
+
// Ignore stdin pause failures.
|
|
9704
|
+
}
|
|
9705
|
+
return;
|
|
9706
|
+
}
|
|
9707
|
+
}
|
|
9708
|
+
waitingDrain = false;
|
|
9709
|
+
try {
|
|
9710
|
+
stdin.resume();
|
|
9711
|
+
} catch (_) {
|
|
9712
|
+
// Ignore stdin resume failures.
|
|
9713
|
+
}
|
|
9714
|
+
};
|
|
9715
|
+
const enqueueWrite = (chunk) => {
|
|
9716
|
+
if (cleanedUp) return;
|
|
9717
|
+
pendingWrites.push(chunk);
|
|
9718
|
+
flushPendingWrites();
|
|
9719
|
+
};
|
|
9720
|
+
const onInput = (chunk) => {
|
|
9721
|
+
if (!child.stdin.destroyed) {
|
|
9722
|
+
enqueueWrite(chunk);
|
|
9723
|
+
}
|
|
9724
|
+
};
|
|
9725
|
+
const flushQueuedFollowUps = () => {
|
|
9726
|
+
if (followUpsFlushed) return;
|
|
9727
|
+
followUpsFlushed = true;
|
|
9728
|
+
queuedFollowUps.forEach((message, index) => {
|
|
9729
|
+
const timer = setTimeout(() => {
|
|
9730
|
+
if (!child.stdin.destroyed) {
|
|
9731
|
+
// PTY submit should use CR instead of LF.
|
|
9732
|
+
enqueueWrite(`${message}\r`);
|
|
9733
|
+
}
|
|
9734
|
+
}, index * 80);
|
|
9735
|
+
timers.push(timer);
|
|
9736
|
+
});
|
|
9737
|
+
};
|
|
9738
|
+
const markOutputReady = () => {
|
|
9739
|
+
if (outputReadyDetected) return;
|
|
9740
|
+
outputReadyDetected = true;
|
|
9741
|
+
timers.push(setTimeout(() => {
|
|
9742
|
+
flushQueuedFollowUps();
|
|
9743
|
+
}, 120));
|
|
9744
|
+
};
|
|
9745
|
+
const onStdoutData = (chunk) => {
|
|
9746
|
+
process.stdout.write(chunk);
|
|
9747
|
+
markOutputReady();
|
|
9748
|
+
};
|
|
9749
|
+
const onStderrData = (chunk) => {
|
|
9750
|
+
process.stderr.write(chunk);
|
|
9751
|
+
markOutputReady();
|
|
9752
|
+
};
|
|
9753
|
+
const onProcessExit = () => {
|
|
9754
|
+
cleanup();
|
|
9755
|
+
};
|
|
9756
|
+
const onProcessSigint = () => {
|
|
9757
|
+
cleanup();
|
|
9758
|
+
try {
|
|
9759
|
+
if (!child.killed) {
|
|
9760
|
+
child.kill('SIGINT');
|
|
9761
|
+
}
|
|
9762
|
+
} catch (_) {
|
|
9763
|
+
// Ignore forwarding failures and keep exit path deterministic.
|
|
9764
|
+
}
|
|
9765
|
+
process.exit(130);
|
|
9766
|
+
};
|
|
9767
|
+
const onProcessSigterm = () => {
|
|
9768
|
+
cleanup();
|
|
9769
|
+
try {
|
|
9770
|
+
if (!child.killed) {
|
|
9771
|
+
child.kill('SIGTERM');
|
|
9772
|
+
}
|
|
9773
|
+
} catch (_) {
|
|
9774
|
+
// Ignore forwarding failures and keep exit path deterministic.
|
|
9775
|
+
}
|
|
9776
|
+
process.exit(143);
|
|
9777
|
+
};
|
|
9778
|
+
const cleanup = () => {
|
|
9779
|
+
if (cleanedUp) return;
|
|
9780
|
+
cleanedUp = true;
|
|
9781
|
+
stdin.removeListener('data', onInput);
|
|
9782
|
+
process.removeListener('exit', onProcessExit);
|
|
9783
|
+
process.removeListener('SIGINT', onProcessSigint);
|
|
9784
|
+
process.removeListener('SIGTERM', onProcessSigterm);
|
|
9785
|
+
child.stdout.removeListener('data', onStdoutData);
|
|
9786
|
+
child.stderr.removeListener('data', onStderrData);
|
|
9787
|
+
if (onChildStdinDrain) {
|
|
9788
|
+
child.stdin.removeListener('drain', onChildStdinDrain);
|
|
9789
|
+
}
|
|
9790
|
+
if (onChildStdinError) {
|
|
9791
|
+
child.stdin.removeListener('error', onChildStdinError);
|
|
9792
|
+
}
|
|
9793
|
+
while (timers.length > 0) {
|
|
9794
|
+
clearTimeout(timers.pop());
|
|
9795
|
+
}
|
|
9796
|
+
try {
|
|
9797
|
+
if (typeof stdin.setRawMode === 'function' && !hadRawMode) {
|
|
9798
|
+
stdin.setRawMode(false);
|
|
9799
|
+
}
|
|
9800
|
+
} catch (_) {
|
|
9801
|
+
// Ignore raw mode restore failures at shutdown.
|
|
9802
|
+
}
|
|
9803
|
+
};
|
|
9804
|
+
|
|
9805
|
+
process.on('exit', onProcessExit);
|
|
9806
|
+
process.on('SIGINT', onProcessSigint);
|
|
9807
|
+
process.on('SIGTERM', onProcessSigterm);
|
|
9808
|
+
child.stdout.on('data', onStdoutData);
|
|
9809
|
+
child.stderr.on('data', onStderrData);
|
|
9810
|
+
onChildStdinDrain = () => {
|
|
9811
|
+
waitingDrain = false;
|
|
9812
|
+
flushPendingWrites();
|
|
9813
|
+
};
|
|
9814
|
+
onChildStdinError = (error) => {
|
|
9815
|
+
handleWriteFailure(error);
|
|
9816
|
+
};
|
|
9817
|
+
child.stdin.on('drain', onChildStdinDrain);
|
|
9818
|
+
child.stdin.on('error', onChildStdinError);
|
|
9819
|
+
try {
|
|
9820
|
+
if (typeof stdin.setRawMode === 'function' && !hadRawMode) {
|
|
9821
|
+
stdin.setRawMode(true);
|
|
9822
|
+
}
|
|
9823
|
+
} catch (_) {
|
|
9824
|
+
// Keep graceful fallback if raw mode toggle is not supported.
|
|
9825
|
+
}
|
|
9826
|
+
|
|
9827
|
+
stdin.resume();
|
|
9828
|
+
stdin.on('data', onInput);
|
|
9829
|
+
// Fallback in case the child stays silent before prompt render.
|
|
9830
|
+
timers.push(setTimeout(() => {
|
|
9831
|
+
flushQueuedFollowUps();
|
|
9832
|
+
}, 1500));
|
|
9833
|
+
|
|
9834
|
+
child.on('error', (err) => {
|
|
9835
|
+
cleanup();
|
|
9836
|
+
rejectOnce(new Error(`运行 ${selectedBin} 失败: ${err.message}`));
|
|
9837
|
+
});
|
|
9838
|
+
|
|
9839
|
+
child.on('close', (code, signal) => {
|
|
9840
|
+
cleanup();
|
|
9841
|
+
if (typeof code === 'number') {
|
|
9842
|
+
resolveOnce(code);
|
|
9843
|
+
return;
|
|
9844
|
+
}
|
|
9845
|
+
if (signal === 'SIGINT') {
|
|
9846
|
+
resolveOnce(130);
|
|
9847
|
+
return;
|
|
9848
|
+
}
|
|
9849
|
+
if (signal === 'SIGTERM') {
|
|
9850
|
+
resolveOnce(143);
|
|
9851
|
+
return;
|
|
9852
|
+
}
|
|
9853
|
+
resolveOnce(1);
|
|
9854
|
+
});
|
|
9855
|
+
});
|
|
9856
|
+
}
|
|
9857
|
+
// #endregion runProxyCommandWithQueuedFollowUps
|
|
9858
|
+
|
|
9859
|
+
async function runProxyCommand(displayName, binNames, args = [], installTip = '', runtimeOptions = {}) {
|
|
8601
9860
|
const extraArgs = Array.isArray(args) ? args.filter(arg => arg !== undefined) : [];
|
|
8602
9861
|
const hasYolo = extraArgs.includes('--yolo');
|
|
8603
9862
|
const finalArgs = hasYolo ? extraArgs : ['--yolo', ...extraArgs];
|
|
@@ -8623,6 +9882,14 @@ async function runProxyCommand(displayName, binNames, args = [], installTip = ''
|
|
|
8623
9882
|
throw new Error(msg);
|
|
8624
9883
|
}
|
|
8625
9884
|
|
|
9885
|
+
const queuedFollowUps = runtimeOptions && Array.isArray(runtimeOptions.queuedFollowUps)
|
|
9886
|
+
? runtimeOptions.queuedFollowUps.filter((item) => typeof item === 'string' && item.trim())
|
|
9887
|
+
: [];
|
|
9888
|
+
|
|
9889
|
+
if (queuedFollowUps.length > 0) {
|
|
9890
|
+
return runProxyCommandWithQueuedFollowUps(selectedBin, finalArgs, queuedFollowUps);
|
|
9891
|
+
}
|
|
9892
|
+
|
|
8626
9893
|
return new Promise((resolve, reject) => {
|
|
8627
9894
|
const child = spawn(selectedBin, finalArgs, {
|
|
8628
9895
|
stdio: 'inherit',
|
|
@@ -8652,27 +9919,16 @@ async function runProxyCommand(displayName, binNames, args = [], installTip = ''
|
|
|
8652
9919
|
}
|
|
8653
9920
|
|
|
8654
9921
|
async function cmdCodex(args = []) {
|
|
8655
|
-
const
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
: '内建代理准备失败';
|
|
8660
|
-
throw new Error(message);
|
|
8661
|
-
}
|
|
8662
|
-
if (ensureResult.runtime && ensureResult.runtime.listenUrl) {
|
|
8663
|
-
console.log(`~ Codex 默认走内建代理: ${ensureResult.runtime.listenUrl}`);
|
|
8664
|
-
}
|
|
8665
|
-
return runProxyCommand('Codex', 'codex', args);
|
|
9922
|
+
const parsed = parseCodexProxyOptions(args);
|
|
9923
|
+
return runProxyCommand('Codex', 'codex', parsed.passthroughArgs, '', {
|
|
9924
|
+
queuedFollowUps: parsed.queuedFollowUps
|
|
9925
|
+
});
|
|
8666
9926
|
}
|
|
8667
9927
|
|
|
8668
9928
|
async function cmdQwen(args = []) {
|
|
8669
9929
|
return runProxyCommand('Qwen', ['qwen', 'qwen-code'], args, 'npm install -g @qwen-code/qwen-code');
|
|
8670
9930
|
}
|
|
8671
9931
|
|
|
8672
|
-
async function cmdGemini(args = []) {
|
|
8673
|
-
return runProxyCommand('Gemini', ['gemini', 'gemini-cli'], args, 'npm install -g @google/gemini-cli');
|
|
8674
|
-
}
|
|
8675
|
-
|
|
8676
9932
|
function parseMcpOptions(args = []) {
|
|
8677
9933
|
const options = {
|
|
8678
9934
|
subcommand: 'serve',
|
|
@@ -10132,14 +11388,15 @@ async function main() {
|
|
|
10132
11388
|
console.log(' codexmate auth <list|import|switch|delete|status> 认证文件管理');
|
|
10133
11389
|
console.log(' codexmate proxy <status|set|apply|enable|start|stop> 内建代理');
|
|
10134
11390
|
console.log(' codexmate workflow <list|get|validate|run|runs> MCP 工作流中心');
|
|
10135
|
-
console.log(' codexmate run [--host <HOST>] 启动 Web 界面');
|
|
10136
|
-
console.log(' codexmate codex [参数...] 等同于 codex --yolo');
|
|
11391
|
+
console.log(' codexmate run [--host <HOST>] [--no-browser] 启动 Web 界面');
|
|
11392
|
+
console.log(' codexmate codex [参数...] [--follow-up <文本>|--queued-follow-up <文本> 可重复] 等同于 codex --yolo(不会自动启用内建代理)');
|
|
11393
|
+
console.log(' 注: follow-up 自动排队仅支持 linux/android/netbsd/openbsd/darwin/freebsd 且 stdin 必须是 TTY,其他平台会报错');
|
|
10137
11394
|
console.log(' codexmate qwen [参数...] 等同于 qwen --yolo');
|
|
10138
|
-
console.log(' codexmate gemini [参数...] 等同于 gemini --yolo');
|
|
10139
11395
|
console.log(' codexmate mcp [serve] [--transport stdio] [--allow-write|--read-only]');
|
|
10140
11396
|
console.log(' codexmate export-session --source <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
|
|
10141
11397
|
console.log(' codexmate zip <路径> [--max:级别] 压缩(系统 zip 优先,其次 zip-lib)');
|
|
10142
11398
|
console.log(' codexmate unzip <zip文件> [输出目录] 解压(zip-lib)');
|
|
11399
|
+
console.log(' codexmate unzip-ext <zip目录> [输出目录] [--ext:后缀[,后缀...]] [--no-recursive] 批量提取 ZIP 指定后缀文件(默认递归)');
|
|
10143
11400
|
console.log('');
|
|
10144
11401
|
process.exit(0);
|
|
10145
11402
|
}
|
|
@@ -10174,11 +11431,6 @@ async function main() {
|
|
|
10174
11431
|
process.exit(exitCode);
|
|
10175
11432
|
break;
|
|
10176
11433
|
}
|
|
10177
|
-
case 'gemini': {
|
|
10178
|
-
const exitCode = await cmdGemini(args.slice(1));
|
|
10179
|
-
process.exit(exitCode);
|
|
10180
|
-
break;
|
|
10181
|
-
}
|
|
10182
11434
|
case 'mcp': await cmdMcp(args.slice(1)); break;
|
|
10183
11435
|
case 'export-session': await cmdExportSession(args.slice(1)); break;
|
|
10184
11436
|
case 'zip': {
|
|
@@ -10197,6 +11449,38 @@ async function main() {
|
|
|
10197
11449
|
break;
|
|
10198
11450
|
}
|
|
10199
11451
|
case 'unzip': await cmdUnzip(args[1], args[2]); break;
|
|
11452
|
+
case 'unzip-ext': {
|
|
11453
|
+
const unzipExtOptions = {
|
|
11454
|
+
ext: [],
|
|
11455
|
+
recursive: true
|
|
11456
|
+
};
|
|
11457
|
+
let zipDirPath = null;
|
|
11458
|
+
let outputDir = null;
|
|
11459
|
+
for (let i = 1; i < args.length; i++) {
|
|
11460
|
+
const arg = args[i];
|
|
11461
|
+
if (arg.startsWith('--ext:')) {
|
|
11462
|
+
unzipExtOptions.ext.push(...splitExtractSuffixInput(arg.substring(6)));
|
|
11463
|
+
} else if (arg.startsWith('--ext=')) {
|
|
11464
|
+
unzipExtOptions.ext.push(...splitExtractSuffixInput(arg.substring(6)));
|
|
11465
|
+
} else if (arg === '--ext') {
|
|
11466
|
+
const nextArg = args[i + 1];
|
|
11467
|
+
if (typeof nextArg === 'string' && !nextArg.startsWith('--')) {
|
|
11468
|
+
unzipExtOptions.ext.push(...splitExtractSuffixInput(nextArg));
|
|
11469
|
+
i += 1;
|
|
11470
|
+
}
|
|
11471
|
+
} else if (arg === '--recursive') {
|
|
11472
|
+
unzipExtOptions.recursive = true;
|
|
11473
|
+
} else if (arg === '--no-recursive') {
|
|
11474
|
+
unzipExtOptions.recursive = false;
|
|
11475
|
+
} else if (!zipDirPath) {
|
|
11476
|
+
zipDirPath = arg;
|
|
11477
|
+
} else if (!outputDir) {
|
|
11478
|
+
outputDir = arg;
|
|
11479
|
+
}
|
|
11480
|
+
}
|
|
11481
|
+
await cmdUnzipExt(zipDirPath, outputDir, unzipExtOptions);
|
|
11482
|
+
break;
|
|
11483
|
+
}
|
|
10200
11484
|
default:
|
|
10201
11485
|
console.error('错误: 未知命令:', command);
|
|
10202
11486
|
console.log('运行 "codexmate" 查看帮助');
|