codexmate 0.0.15 → 0.0.17
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 +13 -3
- package/README.md +13 -3
- package/cli.js +683 -15
- package/package.json +2 -2
- package/web-ui/app.js +219 -46
- package/web-ui/index.html +15 -1
- package/web-ui/logic.mjs +113 -13
- package/web-ui/styles.css +203 -25
package/cli.js
CHANGED
|
@@ -123,6 +123,7 @@ const MAX_UPLOAD_SIZE = 200 * 1024 * 1024;
|
|
|
123
123
|
const MAX_SKILLS_ZIP_UPLOAD_SIZE = 20 * 1024 * 1024;
|
|
124
124
|
const MAX_SKILLS_ZIP_ENTRY_COUNT = 2000;
|
|
125
125
|
const MAX_SKILLS_ZIP_UNCOMPRESSED_BYTES = 512 * 1024 * 1024;
|
|
126
|
+
const DEFAULT_EXTRACT_SUFFIXES = Object.freeze(['.json']);
|
|
126
127
|
const DOWNLOAD_ARTIFACT_TTL_MS = 10 * 60 * 1000;
|
|
127
128
|
const g_downloadArtifacts = new Map();
|
|
128
129
|
const BUILTIN_PROXY_PROVIDER_NAME = 'codexmate-proxy';
|
|
@@ -5890,8 +5891,15 @@ function buildProviderSharePayload(params = {}) {
|
|
|
5890
5891
|
|
|
5891
5892
|
const baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
|
|
5892
5893
|
const apiKey = typeof provider.preferred_auth_method === 'string'
|
|
5893
|
-
? provider.preferred_auth_method
|
|
5894
|
+
? provider.preferred_auth_method.trim()
|
|
5895
|
+
: '';
|
|
5896
|
+
const currentModels = readCurrentModels();
|
|
5897
|
+
const savedModel = currentModels && typeof currentModels[name] === 'string'
|
|
5898
|
+
? currentModels[name].trim()
|
|
5894
5899
|
: '';
|
|
5900
|
+
const activeProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
|
|
5901
|
+
const activeModel = typeof config.model === 'string' ? config.model.trim() : '';
|
|
5902
|
+
const model = savedModel || (activeProvider === name ? activeModel : '');
|
|
5895
5903
|
|
|
5896
5904
|
if (!baseUrl) {
|
|
5897
5905
|
return { error: `提供商 ${name} 缺少 base_url` };
|
|
@@ -5901,7 +5909,8 @@ function buildProviderSharePayload(params = {}) {
|
|
|
5901
5909
|
payload: {
|
|
5902
5910
|
name,
|
|
5903
5911
|
baseUrl,
|
|
5904
|
-
apiKey
|
|
5912
|
+
apiKey,
|
|
5913
|
+
model
|
|
5905
5914
|
}
|
|
5906
5915
|
};
|
|
5907
5916
|
}
|
|
@@ -6928,6 +6937,8 @@ function readClaudeSettingsInfo() {
|
|
|
6928
6937
|
exists: !!readResult.exists,
|
|
6929
6938
|
targetPath: CLAUDE_SETTINGS_FILE,
|
|
6930
6939
|
apiKey: typeof env.ANTHROPIC_API_KEY === 'string' ? env.ANTHROPIC_API_KEY : '',
|
|
6940
|
+
authToken: typeof env.ANTHROPIC_AUTH_TOKEN === 'string' ? env.ANTHROPIC_AUTH_TOKEN : '',
|
|
6941
|
+
useKey: typeof env.CLAUDE_CODE_USE_KEY === 'string' ? env.CLAUDE_CODE_USE_KEY : '',
|
|
6931
6942
|
baseUrl: typeof env.ANTHROPIC_BASE_URL === 'string' ? env.ANTHROPIC_BASE_URL : '',
|
|
6932
6943
|
model: typeof env.ANTHROPIC_MODEL === 'string' ? env.ANTHROPIC_MODEL : '',
|
|
6933
6944
|
env
|
|
@@ -7784,6 +7795,318 @@ async function cmdUnzip(zipPath, outputDir) {
|
|
|
7784
7795
|
}
|
|
7785
7796
|
}
|
|
7786
7797
|
|
|
7798
|
+
function splitExtractSuffixInput(rawValue) {
|
|
7799
|
+
if (Array.isArray(rawValue)) {
|
|
7800
|
+
return rawValue.flatMap((item) => splitExtractSuffixInput(item));
|
|
7801
|
+
}
|
|
7802
|
+
if (typeof rawValue !== 'string') {
|
|
7803
|
+
return [];
|
|
7804
|
+
}
|
|
7805
|
+
return rawValue
|
|
7806
|
+
.split(/[,\s]+/g)
|
|
7807
|
+
.map((item) => item.trim())
|
|
7808
|
+
.filter(Boolean);
|
|
7809
|
+
}
|
|
7810
|
+
|
|
7811
|
+
function normalizeExtractSuffix(rawSuffix, fallbackSuffixes = DEFAULT_EXTRACT_SUFFIXES) {
|
|
7812
|
+
const fallbackItems = splitExtractSuffixInput(fallbackSuffixes);
|
|
7813
|
+
const sourceItems = splitExtractSuffixInput(rawSuffix);
|
|
7814
|
+
const source = sourceItems.length > 0 ? sourceItems : fallbackItems;
|
|
7815
|
+
const dedup = new Set();
|
|
7816
|
+
|
|
7817
|
+
for (const item of source) {
|
|
7818
|
+
const lower = item.toLowerCase();
|
|
7819
|
+
if (!lower) {
|
|
7820
|
+
continue;
|
|
7821
|
+
}
|
|
7822
|
+
const normalized = lower.startsWith('.') ? lower : `.${lower}`;
|
|
7823
|
+
if (normalized.length > 1) {
|
|
7824
|
+
dedup.add(normalized);
|
|
7825
|
+
}
|
|
7826
|
+
}
|
|
7827
|
+
|
|
7828
|
+
if (dedup.size === 0) {
|
|
7829
|
+
return [...DEFAULT_EXTRACT_SUFFIXES];
|
|
7830
|
+
}
|
|
7831
|
+
return Array.from(dedup);
|
|
7832
|
+
}
|
|
7833
|
+
|
|
7834
|
+
function buildDefaultExtractOutputDir(baseCwd = process.cwd()) {
|
|
7835
|
+
const normalizedCwd = path.resolve(baseCwd);
|
|
7836
|
+
const parentDir = path.dirname(normalizedCwd);
|
|
7837
|
+
const timestamp = formatTimestampForFileName().replace(/-/g, '');
|
|
7838
|
+
return path.join(parentDir, timestamp);
|
|
7839
|
+
}
|
|
7840
|
+
|
|
7841
|
+
function sanitizeNameSegment(rawValue, fallback = 'item') {
|
|
7842
|
+
const value = typeof rawValue === 'string' ? rawValue.trim() : '';
|
|
7843
|
+
const sanitized = value
|
|
7844
|
+
.replace(/[^\w.-]+/g, '_')
|
|
7845
|
+
.replace(/^_+|_+$/g, '');
|
|
7846
|
+
return sanitized || fallback;
|
|
7847
|
+
}
|
|
7848
|
+
|
|
7849
|
+
function resolveDuplicateOutputPath(outputDir, originalFileName, zipPath = '', counters = new Map()) {
|
|
7850
|
+
const fallbackName = `file${path.extname(originalFileName || '')}`;
|
|
7851
|
+
const fileName = path.basename(originalFileName || '') || fallbackName;
|
|
7852
|
+
const firstChoice = path.join(outputDir, fileName);
|
|
7853
|
+
const firstChoiceKey = `exact:${fileName}`;
|
|
7854
|
+
if (!counters.has(firstChoiceKey)) {
|
|
7855
|
+
counters.set(firstChoiceKey, true);
|
|
7856
|
+
if (!fs.existsSync(firstChoice)) {
|
|
7857
|
+
return firstChoice;
|
|
7858
|
+
}
|
|
7859
|
+
}
|
|
7860
|
+
|
|
7861
|
+
const ext = path.extname(fileName);
|
|
7862
|
+
const baseName = path.basename(fileName, ext);
|
|
7863
|
+
const safeBaseName = sanitizeNameSegment(baseName, 'file');
|
|
7864
|
+
const zipBaseName = sanitizeNameSegment(path.basename(zipPath || '', '.zip'), 'zip');
|
|
7865
|
+
const duplicateKey = `dup:${safeBaseName}|${zipBaseName}|${ext}`;
|
|
7866
|
+
let index = counters.has(duplicateKey) ? counters.get(duplicateKey) : 1;
|
|
7867
|
+
|
|
7868
|
+
for (; index <= 100000; index++) {
|
|
7869
|
+
const candidateName = `${safeBaseName}__${zipBaseName}__${index}${ext}`;
|
|
7870
|
+
const candidatePath = path.join(outputDir, candidateName);
|
|
7871
|
+
if (!fs.existsSync(candidatePath)) {
|
|
7872
|
+
counters.set(duplicateKey, index + 1);
|
|
7873
|
+
return candidatePath;
|
|
7874
|
+
}
|
|
7875
|
+
}
|
|
7876
|
+
|
|
7877
|
+
throw new Error(`重名文件过多,无法生成唯一文件名: ${fileName}`);
|
|
7878
|
+
}
|
|
7879
|
+
|
|
7880
|
+
function collectZipFilesFromDir(rootDir, recursive = true) {
|
|
7881
|
+
const queue = [rootDir];
|
|
7882
|
+
const result = [];
|
|
7883
|
+
|
|
7884
|
+
while (queue.length > 0) {
|
|
7885
|
+
const currentDir = queue.shift();
|
|
7886
|
+
let entries = [];
|
|
7887
|
+
try {
|
|
7888
|
+
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
7889
|
+
} catch (e) {
|
|
7890
|
+
throw new Error(`读取目录失败: ${currentDir} (${e.message})`);
|
|
7891
|
+
}
|
|
7892
|
+
|
|
7893
|
+
for (const entry of entries) {
|
|
7894
|
+
const entryPath = path.join(currentDir, entry.name);
|
|
7895
|
+
if (entry.isDirectory()) {
|
|
7896
|
+
if (recursive) {
|
|
7897
|
+
queue.push(entryPath);
|
|
7898
|
+
}
|
|
7899
|
+
continue;
|
|
7900
|
+
}
|
|
7901
|
+
if (entry.isFile() && entry.name.toLowerCase().endsWith('.zip')) {
|
|
7902
|
+
result.push(entryPath);
|
|
7903
|
+
}
|
|
7904
|
+
}
|
|
7905
|
+
}
|
|
7906
|
+
|
|
7907
|
+
result.sort((a, b) => a.localeCompare(b));
|
|
7908
|
+
return result;
|
|
7909
|
+
}
|
|
7910
|
+
|
|
7911
|
+
function extractMatchedEntriesFromZip(zipPath, outputDir, suffixes, duplicateCounters = new Map()) {
|
|
7912
|
+
const normalizedSuffixes = normalizeExtractSuffix(suffixes);
|
|
7913
|
+
return new Promise((resolve, reject) => {
|
|
7914
|
+
yauzl.open(zipPath, { lazyEntries: true, autoClose: false }, (openErr, zipFile) => {
|
|
7915
|
+
if (openErr) {
|
|
7916
|
+
reject(openErr);
|
|
7917
|
+
return;
|
|
7918
|
+
}
|
|
7919
|
+
if (!zipFile) {
|
|
7920
|
+
reject(new Error('无法读取 ZIP 文件'));
|
|
7921
|
+
return;
|
|
7922
|
+
}
|
|
7923
|
+
|
|
7924
|
+
let settled = false;
|
|
7925
|
+
let matched = 0;
|
|
7926
|
+
let extracted = 0;
|
|
7927
|
+
let skippedDir = 0;
|
|
7928
|
+
let skippedExt = 0;
|
|
7929
|
+
|
|
7930
|
+
const finish = (err) => {
|
|
7931
|
+
if (settled) return;
|
|
7932
|
+
settled = true;
|
|
7933
|
+
try {
|
|
7934
|
+
zipFile.close();
|
|
7935
|
+
} catch (_) {}
|
|
7936
|
+
if (err) {
|
|
7937
|
+
reject(err);
|
|
7938
|
+
} else {
|
|
7939
|
+
resolve({ matched, extracted, skippedDir, skippedExt });
|
|
7940
|
+
}
|
|
7941
|
+
};
|
|
7942
|
+
|
|
7943
|
+
zipFile.on('entry', (entry) => {
|
|
7944
|
+
if (settled) return;
|
|
7945
|
+
const rawEntryName = typeof entry.fileName === 'string' ? entry.fileName : '';
|
|
7946
|
+
const normalizedEntryName = rawEntryName.replace(/\\/g, '/');
|
|
7947
|
+
|
|
7948
|
+
if (!normalizedEntryName || normalizedEntryName.endsWith('/')) {
|
|
7949
|
+
skippedDir += 1;
|
|
7950
|
+
zipFile.readEntry();
|
|
7951
|
+
return;
|
|
7952
|
+
}
|
|
7953
|
+
|
|
7954
|
+
const entryBaseName = path.basename(normalizedEntryName);
|
|
7955
|
+
const lowerBaseName = entryBaseName.toLowerCase();
|
|
7956
|
+
const matchedSuffix = normalizedSuffixes.some((suffix) => lowerBaseName.endsWith(suffix));
|
|
7957
|
+
if (!entryBaseName || !matchedSuffix) {
|
|
7958
|
+
skippedExt += 1;
|
|
7959
|
+
zipFile.readEntry();
|
|
7960
|
+
return;
|
|
7961
|
+
}
|
|
7962
|
+
|
|
7963
|
+
matched += 1;
|
|
7964
|
+
zipFile.openReadStream(entry, (streamErr, readStream) => {
|
|
7965
|
+
if (streamErr || !readStream) {
|
|
7966
|
+
finish(streamErr || new Error('无法读取 ZIP 条目流'));
|
|
7967
|
+
return;
|
|
7968
|
+
}
|
|
7969
|
+
|
|
7970
|
+
let completed = false;
|
|
7971
|
+
const outputPath = resolveDuplicateOutputPath(outputDir, entryBaseName, zipPath, duplicateCounters);
|
|
7972
|
+
const writeStream = fs.createWriteStream(outputPath);
|
|
7973
|
+
const fail = (writeErr) => {
|
|
7974
|
+
if (completed) return;
|
|
7975
|
+
completed = true;
|
|
7976
|
+
try {
|
|
7977
|
+
readStream.destroy();
|
|
7978
|
+
} catch (_) {}
|
|
7979
|
+
try {
|
|
7980
|
+
writeStream.destroy();
|
|
7981
|
+
} catch (_) {}
|
|
7982
|
+
try {
|
|
7983
|
+
if (fs.existsSync(outputPath)) {
|
|
7984
|
+
fs.unlinkSync(outputPath);
|
|
7985
|
+
}
|
|
7986
|
+
} catch (_) {}
|
|
7987
|
+
finish(writeErr);
|
|
7988
|
+
};
|
|
7989
|
+
|
|
7990
|
+
readStream.on('error', fail);
|
|
7991
|
+
writeStream.on('error', fail);
|
|
7992
|
+
writeStream.on('finish', () => {
|
|
7993
|
+
if (completed || settled) return;
|
|
7994
|
+
completed = true;
|
|
7995
|
+
extracted += 1;
|
|
7996
|
+
zipFile.readEntry();
|
|
7997
|
+
});
|
|
7998
|
+
|
|
7999
|
+
readStream.pipe(writeStream);
|
|
8000
|
+
});
|
|
8001
|
+
});
|
|
8002
|
+
|
|
8003
|
+
zipFile.on('end', () => {
|
|
8004
|
+
finish(null);
|
|
8005
|
+
});
|
|
8006
|
+
zipFile.on('error', (zipErr) => {
|
|
8007
|
+
finish(zipErr);
|
|
8008
|
+
});
|
|
8009
|
+
|
|
8010
|
+
zipFile.readEntry();
|
|
8011
|
+
});
|
|
8012
|
+
});
|
|
8013
|
+
}
|
|
8014
|
+
|
|
8015
|
+
async function cmdUnzipExt(zipDirPath, outputDir, options = {}) {
|
|
8016
|
+
if (!zipDirPath) {
|
|
8017
|
+
console.error('用法: codexmate unzip-ext <zip目录> [输出目录] [--ext:后缀[,后缀...]] [--no-recursive]');
|
|
8018
|
+
console.log('\n示例:');
|
|
8019
|
+
console.log(' codexmate unzip-ext ./archives');
|
|
8020
|
+
console.log(' codexmate unzip-ext ./archives ./output --ext:json,txt');
|
|
8021
|
+
console.log(' codexmate unzip-ext D:/data/zips --ext:txt --no-recursive');
|
|
8022
|
+
console.log(' 说明: 默认递归扫描子目录,可通过 --no-recursive 关闭递归');
|
|
8023
|
+
process.exit(1);
|
|
8024
|
+
}
|
|
8025
|
+
|
|
8026
|
+
const recursive = options.recursive !== false;
|
|
8027
|
+
const suffixes = normalizeExtractSuffix(options.ext);
|
|
8028
|
+
const absZipDir = path.resolve(zipDirPath);
|
|
8029
|
+
const absOutputDir = outputDir ? path.resolve(outputDir) : buildDefaultExtractOutputDir(process.cwd());
|
|
8030
|
+
|
|
8031
|
+
if (!fs.existsSync(absZipDir)) {
|
|
8032
|
+
console.error('错误: 目录不存在:', absZipDir);
|
|
8033
|
+
process.exit(1);
|
|
8034
|
+
}
|
|
8035
|
+
try {
|
|
8036
|
+
if (!fs.statSync(absZipDir).isDirectory()) {
|
|
8037
|
+
console.error('错误: 仅支持目录路径:', absZipDir);
|
|
8038
|
+
process.exit(1);
|
|
8039
|
+
}
|
|
8040
|
+
} catch (e) {
|
|
8041
|
+
console.error('错误: 无法读取目录信息:', e.message);
|
|
8042
|
+
process.exit(1);
|
|
8043
|
+
}
|
|
8044
|
+
|
|
8045
|
+
let zipFiles = [];
|
|
8046
|
+
try {
|
|
8047
|
+
zipFiles = collectZipFilesFromDir(absZipDir, recursive);
|
|
8048
|
+
} catch (e) {
|
|
8049
|
+
console.error('扫描 ZIP 文件失败:', e.message);
|
|
8050
|
+
process.exit(1);
|
|
8051
|
+
}
|
|
8052
|
+
|
|
8053
|
+
if (zipFiles.length === 0) {
|
|
8054
|
+
console.error('错误: 未找到任何 ZIP 文件');
|
|
8055
|
+
process.exit(1);
|
|
8056
|
+
}
|
|
8057
|
+
|
|
8058
|
+
ensureDir(absOutputDir);
|
|
8059
|
+
|
|
8060
|
+
console.log('\n批量解压配置:');
|
|
8061
|
+
console.log(' ZIP 目录:', absZipDir);
|
|
8062
|
+
console.log(' 输出目录:', absOutputDir);
|
|
8063
|
+
console.log(' 后缀过滤:', suffixes.join(', '));
|
|
8064
|
+
console.log(' 递归扫描:', recursive ? '是' : '否');
|
|
8065
|
+
console.log(' ZIP 数量:', zipFiles.length);
|
|
8066
|
+
console.log('\n开始提取...\n');
|
|
8067
|
+
|
|
8068
|
+
let totalMatched = 0;
|
|
8069
|
+
let totalExtracted = 0;
|
|
8070
|
+
let totalSkippedDir = 0;
|
|
8071
|
+
let totalSkippedExt = 0;
|
|
8072
|
+
const failed = [];
|
|
8073
|
+
const duplicateCounters = new Map();
|
|
8074
|
+
|
|
8075
|
+
for (const zipFilePath of zipFiles) {
|
|
8076
|
+
try {
|
|
8077
|
+
await inspectZipArchiveLimits(zipFilePath, {
|
|
8078
|
+
maxEntryCount: MAX_SKILLS_ZIP_ENTRY_COUNT,
|
|
8079
|
+
maxUncompressedBytes: MAX_SKILLS_ZIP_UNCOMPRESSED_BYTES
|
|
8080
|
+
});
|
|
8081
|
+
const result = await extractMatchedEntriesFromZip(zipFilePath, absOutputDir, suffixes, duplicateCounters);
|
|
8082
|
+
totalMatched += result.matched;
|
|
8083
|
+
totalExtracted += result.extracted;
|
|
8084
|
+
totalSkippedDir += result.skippedDir;
|
|
8085
|
+
totalSkippedExt += result.skippedExt;
|
|
8086
|
+
console.log(`✓ ${path.basename(zipFilePath)}: 命中 ${result.matched},提取 ${result.extracted}`);
|
|
8087
|
+
} catch (e) {
|
|
8088
|
+
failed.push({ zipFilePath, message: e && e.message ? e.message : String(e) });
|
|
8089
|
+
console.error(`✗ ${path.basename(zipFilePath)}: ${e && e.message ? e.message : e}`);
|
|
8090
|
+
}
|
|
8091
|
+
}
|
|
8092
|
+
|
|
8093
|
+
console.log('\n提取结果:');
|
|
8094
|
+
console.log(' 输出目录:', absOutputDir);
|
|
8095
|
+
console.log(' 扫描 ZIP:', zipFiles.length);
|
|
8096
|
+
console.log(' 命中条目:', totalMatched);
|
|
8097
|
+
console.log(' 已提取:', totalExtracted);
|
|
8098
|
+
console.log(' 已跳过(目录条目):', totalSkippedDir);
|
|
8099
|
+
console.log(' 已跳过(后缀不匹配):', totalSkippedExt);
|
|
8100
|
+
if (failed.length > 0) {
|
|
8101
|
+
console.error(' 失败数量:', failed.length);
|
|
8102
|
+
for (const item of failed) {
|
|
8103
|
+
console.error(` - ${item.zipFilePath}: ${item.message}`);
|
|
8104
|
+
}
|
|
8105
|
+
process.exit(1);
|
|
8106
|
+
}
|
|
8107
|
+
console.log();
|
|
8108
|
+
}
|
|
8109
|
+
|
|
7787
8110
|
function resolveExportOutputPath(outputPath, defaultFileName) {
|
|
7788
8111
|
const fallback = path.resolve(process.cwd(), defaultFileName);
|
|
7789
8112
|
if (typeof outputPath !== 'string' || !outputPath.trim()) {
|
|
@@ -9233,7 +9556,317 @@ async function cmdWorkflow(args = []) {
|
|
|
9233
9556
|
throw new Error(`未知 workflow 子命令: ${subcommand}`);
|
|
9234
9557
|
}
|
|
9235
9558
|
|
|
9236
|
-
|
|
9559
|
+
// #region parseCodexProxyOptions
|
|
9560
|
+
function parseCodexProxyOptions(args = []) {
|
|
9561
|
+
const options = {
|
|
9562
|
+
passthroughArgs: [],
|
|
9563
|
+
queuedFollowUps: []
|
|
9564
|
+
};
|
|
9565
|
+
const argv = Array.isArray(args) ? args : [];
|
|
9566
|
+
|
|
9567
|
+
const pushFollowUp = (value, optionName) => {
|
|
9568
|
+
const raw = value === undefined || value === null ? '' : String(value);
|
|
9569
|
+
if (!raw.trim()) {
|
|
9570
|
+
throw new Error(`${optionName} 需要提供非空内容`);
|
|
9571
|
+
}
|
|
9572
|
+
options.queuedFollowUps.push(raw);
|
|
9573
|
+
};
|
|
9574
|
+
|
|
9575
|
+
for (let i = 0; i < argv.length; i++) {
|
|
9576
|
+
const arg = argv[i];
|
|
9577
|
+
if (arg === undefined || arg === null) {
|
|
9578
|
+
continue;
|
|
9579
|
+
}
|
|
9580
|
+
const text = String(arg);
|
|
9581
|
+
if (text === '--') {
|
|
9582
|
+
options.passthroughArgs.push(...argv.slice(i).map((item) => String(item)));
|
|
9583
|
+
break;
|
|
9584
|
+
}
|
|
9585
|
+
if (text === '--queued-follow-up' || text === '--follow-up') {
|
|
9586
|
+
const next = argv[i + 1];
|
|
9587
|
+
if (next === undefined) {
|
|
9588
|
+
throw new Error(`${text} 需要提供内容`);
|
|
9589
|
+
}
|
|
9590
|
+
pushFollowUp(next, text);
|
|
9591
|
+
i += 1;
|
|
9592
|
+
continue;
|
|
9593
|
+
}
|
|
9594
|
+
if (text.startsWith('--queued-follow-up=')) {
|
|
9595
|
+
pushFollowUp(text.slice('--queued-follow-up='.length), '--queued-follow-up');
|
|
9596
|
+
continue;
|
|
9597
|
+
}
|
|
9598
|
+
if (text.startsWith('--follow-up=')) {
|
|
9599
|
+
pushFollowUp(text.slice('--follow-up='.length), '--follow-up');
|
|
9600
|
+
continue;
|
|
9601
|
+
}
|
|
9602
|
+
options.passthroughArgs.push(text);
|
|
9603
|
+
}
|
|
9604
|
+
|
|
9605
|
+
return options;
|
|
9606
|
+
}
|
|
9607
|
+
// #endregion parseCodexProxyOptions
|
|
9608
|
+
|
|
9609
|
+
function shellEscapePosixArg(value) {
|
|
9610
|
+
const text = value === undefined || value === null ? '' : String(value);
|
|
9611
|
+
return `'${text.replace(/'/g, `'\"'\"'`)}'`;
|
|
9612
|
+
}
|
|
9613
|
+
|
|
9614
|
+
// #region buildScriptCommandArgs
|
|
9615
|
+
function buildScriptCommandArgs(commandLine) {
|
|
9616
|
+
const platform = process.platform;
|
|
9617
|
+
// util-linux script needs -e/--return to propagate child exit code.
|
|
9618
|
+
if (platform === 'linux' || platform === 'android') {
|
|
9619
|
+
return ['-q', '-e', '-c', commandLine, '/dev/null'];
|
|
9620
|
+
}
|
|
9621
|
+
// NetBSD supports -e/-c, matching util-linux style contract.
|
|
9622
|
+
if (platform === 'netbsd') {
|
|
9623
|
+
return ['-q', '-e', '-c', commandLine, '/dev/null'];
|
|
9624
|
+
}
|
|
9625
|
+
// OpenBSD supports "-c <command>" with a trailing output file path.
|
|
9626
|
+
if (platform === 'openbsd') {
|
|
9627
|
+
return ['-c', commandLine, '/dev/null'];
|
|
9628
|
+
}
|
|
9629
|
+
// BSD/macOS script does not support util-linux "-c <cmd>" syntax.
|
|
9630
|
+
if (platform === 'darwin' || platform === 'freebsd') {
|
|
9631
|
+
return ['-q', '/dev/null', 'sh', '-lc', commandLine];
|
|
9632
|
+
}
|
|
9633
|
+
throw new Error(`当前平台暂不支持 --follow-up 自动排队(platform=${platform})`);
|
|
9634
|
+
}
|
|
9635
|
+
// #endregion buildScriptCommandArgs
|
|
9636
|
+
|
|
9637
|
+
// #region runProxyCommandWithQueuedFollowUps
|
|
9638
|
+
async function runProxyCommandWithQueuedFollowUps(selectedBin, finalArgs = [], queuedFollowUps = []) {
|
|
9639
|
+
if (!process.stdin || !process.stdin.isTTY) {
|
|
9640
|
+
throw new Error('当前 stdin 不是 TTY,无法使用 --follow-up 自动排队。');
|
|
9641
|
+
}
|
|
9642
|
+
|
|
9643
|
+
const scriptPath = resolveCommandPath('script');
|
|
9644
|
+
if (!scriptPath) {
|
|
9645
|
+
throw new Error('未找到 script 命令,无法自动注入 queued follow-up 消息。');
|
|
9646
|
+
}
|
|
9647
|
+
|
|
9648
|
+
const commandLine = [selectedBin, ...finalArgs].map((item) => shellEscapePosixArg(item)).join(' ');
|
|
9649
|
+
const scriptArgs = buildScriptCommandArgs(commandLine);
|
|
9650
|
+
|
|
9651
|
+
return new Promise((resolve, reject) => {
|
|
9652
|
+
let settled = false;
|
|
9653
|
+
const child = spawn(scriptPath, scriptArgs, {
|
|
9654
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
9655
|
+
});
|
|
9656
|
+
|
|
9657
|
+
const stdin = process.stdin;
|
|
9658
|
+
const hadRawMode = !!stdin.isRaw;
|
|
9659
|
+
let cleanedUp = false;
|
|
9660
|
+
let waitingDrain = false;
|
|
9661
|
+
let followUpsFlushed = false;
|
|
9662
|
+
let outputReadyDetected = false;
|
|
9663
|
+
const timers = [];
|
|
9664
|
+
const pendingWrites = [];
|
|
9665
|
+
let onChildStdinDrain = null;
|
|
9666
|
+
let onChildStdinError = null;
|
|
9667
|
+
const resolveOnce = (code) => {
|
|
9668
|
+
if (settled) return;
|
|
9669
|
+
settled = true;
|
|
9670
|
+
resolve(code);
|
|
9671
|
+
};
|
|
9672
|
+
const rejectOnce = (error) => {
|
|
9673
|
+
if (settled) return;
|
|
9674
|
+
settled = true;
|
|
9675
|
+
reject(error);
|
|
9676
|
+
};
|
|
9677
|
+
const handleWriteFailure = (error) => {
|
|
9678
|
+
const err = error instanceof Error ? error : new Error(String(error || 'unknown'));
|
|
9679
|
+
cleanup();
|
|
9680
|
+
try {
|
|
9681
|
+
if (!child.killed) {
|
|
9682
|
+
child.kill('SIGTERM');
|
|
9683
|
+
}
|
|
9684
|
+
} catch (_) {
|
|
9685
|
+
// Ignore failure to terminate child after stdin write failure.
|
|
9686
|
+
}
|
|
9687
|
+
rejectOnce(new Error(`写入 ${selectedBin} stdin 失败: ${err.message}`));
|
|
9688
|
+
};
|
|
9689
|
+
const flushPendingWrites = () => {
|
|
9690
|
+
if (cleanedUp || child.stdin.destroyed) {
|
|
9691
|
+
pendingWrites.length = 0;
|
|
9692
|
+
return;
|
|
9693
|
+
}
|
|
9694
|
+
while (pendingWrites.length > 0) {
|
|
9695
|
+
const chunk = pendingWrites[0];
|
|
9696
|
+
let canContinue = true;
|
|
9697
|
+
try {
|
|
9698
|
+
canContinue = child.stdin.write(chunk, (error) => {
|
|
9699
|
+
if (error) {
|
|
9700
|
+
handleWriteFailure(error);
|
|
9701
|
+
}
|
|
9702
|
+
});
|
|
9703
|
+
} catch (error) {
|
|
9704
|
+
handleWriteFailure(error);
|
|
9705
|
+
return;
|
|
9706
|
+
}
|
|
9707
|
+
pendingWrites.shift();
|
|
9708
|
+
if (!canContinue) {
|
|
9709
|
+
waitingDrain = true;
|
|
9710
|
+
try {
|
|
9711
|
+
stdin.pause();
|
|
9712
|
+
} catch (_) {
|
|
9713
|
+
// Ignore stdin pause failures.
|
|
9714
|
+
}
|
|
9715
|
+
return;
|
|
9716
|
+
}
|
|
9717
|
+
}
|
|
9718
|
+
waitingDrain = false;
|
|
9719
|
+
try {
|
|
9720
|
+
stdin.resume();
|
|
9721
|
+
} catch (_) {
|
|
9722
|
+
// Ignore stdin resume failures.
|
|
9723
|
+
}
|
|
9724
|
+
};
|
|
9725
|
+
const enqueueWrite = (chunk) => {
|
|
9726
|
+
if (cleanedUp) return;
|
|
9727
|
+
pendingWrites.push(chunk);
|
|
9728
|
+
flushPendingWrites();
|
|
9729
|
+
};
|
|
9730
|
+
const onInput = (chunk) => {
|
|
9731
|
+
if (!child.stdin.destroyed) {
|
|
9732
|
+
enqueueWrite(chunk);
|
|
9733
|
+
}
|
|
9734
|
+
};
|
|
9735
|
+
const flushQueuedFollowUps = () => {
|
|
9736
|
+
if (followUpsFlushed) return;
|
|
9737
|
+
followUpsFlushed = true;
|
|
9738
|
+
queuedFollowUps.forEach((message, index) => {
|
|
9739
|
+
const timer = setTimeout(() => {
|
|
9740
|
+
if (!child.stdin.destroyed) {
|
|
9741
|
+
// PTY submit should use CR instead of LF.
|
|
9742
|
+
enqueueWrite(`${message}\r`);
|
|
9743
|
+
}
|
|
9744
|
+
}, index * 80);
|
|
9745
|
+
timers.push(timer);
|
|
9746
|
+
});
|
|
9747
|
+
};
|
|
9748
|
+
const markOutputReady = () => {
|
|
9749
|
+
if (outputReadyDetected) return;
|
|
9750
|
+
outputReadyDetected = true;
|
|
9751
|
+
timers.push(setTimeout(() => {
|
|
9752
|
+
flushQueuedFollowUps();
|
|
9753
|
+
}, 120));
|
|
9754
|
+
};
|
|
9755
|
+
const onStdoutData = (chunk) => {
|
|
9756
|
+
process.stdout.write(chunk);
|
|
9757
|
+
markOutputReady();
|
|
9758
|
+
};
|
|
9759
|
+
const onStderrData = (chunk) => {
|
|
9760
|
+
process.stderr.write(chunk);
|
|
9761
|
+
markOutputReady();
|
|
9762
|
+
};
|
|
9763
|
+
const onProcessExit = () => {
|
|
9764
|
+
cleanup();
|
|
9765
|
+
};
|
|
9766
|
+
const onProcessSigint = () => {
|
|
9767
|
+
cleanup();
|
|
9768
|
+
try {
|
|
9769
|
+
if (!child.killed) {
|
|
9770
|
+
child.kill('SIGINT');
|
|
9771
|
+
}
|
|
9772
|
+
} catch (_) {
|
|
9773
|
+
// Ignore forwarding failures and keep exit path deterministic.
|
|
9774
|
+
}
|
|
9775
|
+
process.exit(130);
|
|
9776
|
+
};
|
|
9777
|
+
const onProcessSigterm = () => {
|
|
9778
|
+
cleanup();
|
|
9779
|
+
try {
|
|
9780
|
+
if (!child.killed) {
|
|
9781
|
+
child.kill('SIGTERM');
|
|
9782
|
+
}
|
|
9783
|
+
} catch (_) {
|
|
9784
|
+
// Ignore forwarding failures and keep exit path deterministic.
|
|
9785
|
+
}
|
|
9786
|
+
process.exit(143);
|
|
9787
|
+
};
|
|
9788
|
+
const cleanup = () => {
|
|
9789
|
+
if (cleanedUp) return;
|
|
9790
|
+
cleanedUp = true;
|
|
9791
|
+
stdin.removeListener('data', onInput);
|
|
9792
|
+
process.removeListener('exit', onProcessExit);
|
|
9793
|
+
process.removeListener('SIGINT', onProcessSigint);
|
|
9794
|
+
process.removeListener('SIGTERM', onProcessSigterm);
|
|
9795
|
+
child.stdout.removeListener('data', onStdoutData);
|
|
9796
|
+
child.stderr.removeListener('data', onStderrData);
|
|
9797
|
+
if (onChildStdinDrain) {
|
|
9798
|
+
child.stdin.removeListener('drain', onChildStdinDrain);
|
|
9799
|
+
}
|
|
9800
|
+
if (onChildStdinError) {
|
|
9801
|
+
child.stdin.removeListener('error', onChildStdinError);
|
|
9802
|
+
}
|
|
9803
|
+
while (timers.length > 0) {
|
|
9804
|
+
clearTimeout(timers.pop());
|
|
9805
|
+
}
|
|
9806
|
+
try {
|
|
9807
|
+
if (typeof stdin.setRawMode === 'function' && !hadRawMode) {
|
|
9808
|
+
stdin.setRawMode(false);
|
|
9809
|
+
}
|
|
9810
|
+
} catch (_) {
|
|
9811
|
+
// Ignore raw mode restore failures at shutdown.
|
|
9812
|
+
}
|
|
9813
|
+
};
|
|
9814
|
+
|
|
9815
|
+
process.on('exit', onProcessExit);
|
|
9816
|
+
process.on('SIGINT', onProcessSigint);
|
|
9817
|
+
process.on('SIGTERM', onProcessSigterm);
|
|
9818
|
+
child.stdout.on('data', onStdoutData);
|
|
9819
|
+
child.stderr.on('data', onStderrData);
|
|
9820
|
+
onChildStdinDrain = () => {
|
|
9821
|
+
waitingDrain = false;
|
|
9822
|
+
flushPendingWrites();
|
|
9823
|
+
};
|
|
9824
|
+
onChildStdinError = (error) => {
|
|
9825
|
+
handleWriteFailure(error);
|
|
9826
|
+
};
|
|
9827
|
+
child.stdin.on('drain', onChildStdinDrain);
|
|
9828
|
+
child.stdin.on('error', onChildStdinError);
|
|
9829
|
+
try {
|
|
9830
|
+
if (typeof stdin.setRawMode === 'function' && !hadRawMode) {
|
|
9831
|
+
stdin.setRawMode(true);
|
|
9832
|
+
}
|
|
9833
|
+
} catch (_) {
|
|
9834
|
+
// Keep graceful fallback if raw mode toggle is not supported.
|
|
9835
|
+
}
|
|
9836
|
+
|
|
9837
|
+
stdin.resume();
|
|
9838
|
+
stdin.on('data', onInput);
|
|
9839
|
+
// Fallback in case the child stays silent before prompt render.
|
|
9840
|
+
timers.push(setTimeout(() => {
|
|
9841
|
+
flushQueuedFollowUps();
|
|
9842
|
+
}, 1500));
|
|
9843
|
+
|
|
9844
|
+
child.on('error', (err) => {
|
|
9845
|
+
cleanup();
|
|
9846
|
+
rejectOnce(new Error(`运行 ${selectedBin} 失败: ${err.message}`));
|
|
9847
|
+
});
|
|
9848
|
+
|
|
9849
|
+
child.on('close', (code, signal) => {
|
|
9850
|
+
cleanup();
|
|
9851
|
+
if (typeof code === 'number') {
|
|
9852
|
+
resolveOnce(code);
|
|
9853
|
+
return;
|
|
9854
|
+
}
|
|
9855
|
+
if (signal === 'SIGINT') {
|
|
9856
|
+
resolveOnce(130);
|
|
9857
|
+
return;
|
|
9858
|
+
}
|
|
9859
|
+
if (signal === 'SIGTERM') {
|
|
9860
|
+
resolveOnce(143);
|
|
9861
|
+
return;
|
|
9862
|
+
}
|
|
9863
|
+
resolveOnce(1);
|
|
9864
|
+
});
|
|
9865
|
+
});
|
|
9866
|
+
}
|
|
9867
|
+
// #endregion runProxyCommandWithQueuedFollowUps
|
|
9868
|
+
|
|
9869
|
+
async function runProxyCommand(displayName, binNames, args = [], installTip = '', runtimeOptions = {}) {
|
|
9237
9870
|
const extraArgs = Array.isArray(args) ? args.filter(arg => arg !== undefined) : [];
|
|
9238
9871
|
const hasYolo = extraArgs.includes('--yolo');
|
|
9239
9872
|
const finalArgs = hasYolo ? extraArgs : ['--yolo', ...extraArgs];
|
|
@@ -9259,6 +9892,14 @@ async function runProxyCommand(displayName, binNames, args = [], installTip = ''
|
|
|
9259
9892
|
throw new Error(msg);
|
|
9260
9893
|
}
|
|
9261
9894
|
|
|
9895
|
+
const queuedFollowUps = runtimeOptions && Array.isArray(runtimeOptions.queuedFollowUps)
|
|
9896
|
+
? runtimeOptions.queuedFollowUps.filter((item) => typeof item === 'string' && item.trim())
|
|
9897
|
+
: [];
|
|
9898
|
+
|
|
9899
|
+
if (queuedFollowUps.length > 0) {
|
|
9900
|
+
return runProxyCommandWithQueuedFollowUps(selectedBin, finalArgs, queuedFollowUps);
|
|
9901
|
+
}
|
|
9902
|
+
|
|
9262
9903
|
return new Promise((resolve, reject) => {
|
|
9263
9904
|
const child = spawn(selectedBin, finalArgs, {
|
|
9264
9905
|
stdio: 'inherit',
|
|
@@ -9288,17 +9929,10 @@ async function runProxyCommand(displayName, binNames, args = [], installTip = ''
|
|
|
9288
9929
|
}
|
|
9289
9930
|
|
|
9290
9931
|
async function cmdCodex(args = []) {
|
|
9291
|
-
const
|
|
9292
|
-
|
|
9293
|
-
|
|
9294
|
-
|
|
9295
|
-
: '内建代理准备失败';
|
|
9296
|
-
throw new Error(message);
|
|
9297
|
-
}
|
|
9298
|
-
if (ensureResult.runtime && ensureResult.runtime.listenUrl) {
|
|
9299
|
-
console.log(`~ Codex 默认走内建代理: ${ensureResult.runtime.listenUrl}`);
|
|
9300
|
-
}
|
|
9301
|
-
return runProxyCommand('Codex', 'codex', args);
|
|
9932
|
+
const parsed = parseCodexProxyOptions(args);
|
|
9933
|
+
return runProxyCommand('Codex', 'codex', parsed.passthroughArgs, '', {
|
|
9934
|
+
queuedFollowUps: parsed.queuedFollowUps
|
|
9935
|
+
});
|
|
9302
9936
|
}
|
|
9303
9937
|
|
|
9304
9938
|
async function cmdQwen(args = []) {
|
|
@@ -10765,12 +11399,14 @@ async function main() {
|
|
|
10765
11399
|
console.log(' codexmate proxy <status|set|apply|enable|start|stop> 内建代理');
|
|
10766
11400
|
console.log(' codexmate workflow <list|get|validate|run|runs> MCP 工作流中心');
|
|
10767
11401
|
console.log(' codexmate run [--host <HOST>] [--no-browser] 启动 Web 界面');
|
|
10768
|
-
console.log(' codexmate codex [参数...] 等同于 codex --yolo');
|
|
11402
|
+
console.log(' codexmate codex [参数...] [--follow-up <文本>|--queued-follow-up <文本> 可重复] 等同于 codex --yolo(不会自动启用内建代理)');
|
|
11403
|
+
console.log(' 注: follow-up 自动排队仅支持 linux/android/netbsd/openbsd/darwin/freebsd 且 stdin 必须是 TTY,其他平台会报错');
|
|
10769
11404
|
console.log(' codexmate qwen [参数...] 等同于 qwen --yolo');
|
|
10770
11405
|
console.log(' codexmate mcp [serve] [--transport stdio] [--allow-write|--read-only]');
|
|
10771
11406
|
console.log(' codexmate export-session --source <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
|
|
10772
11407
|
console.log(' codexmate zip <路径> [--max:级别] 压缩(系统 zip 优先,其次 zip-lib)');
|
|
10773
11408
|
console.log(' codexmate unzip <zip文件> [输出目录] 解压(zip-lib)');
|
|
11409
|
+
console.log(' codexmate unzip-ext <zip目录> [输出目录] [--ext:后缀[,后缀...]] [--no-recursive] 批量提取 ZIP 指定后缀文件(默认递归)');
|
|
10774
11410
|
console.log('');
|
|
10775
11411
|
process.exit(0);
|
|
10776
11412
|
}
|
|
@@ -10823,6 +11459,38 @@ async function main() {
|
|
|
10823
11459
|
break;
|
|
10824
11460
|
}
|
|
10825
11461
|
case 'unzip': await cmdUnzip(args[1], args[2]); break;
|
|
11462
|
+
case 'unzip-ext': {
|
|
11463
|
+
const unzipExtOptions = {
|
|
11464
|
+
ext: [],
|
|
11465
|
+
recursive: true
|
|
11466
|
+
};
|
|
11467
|
+
let zipDirPath = null;
|
|
11468
|
+
let outputDir = null;
|
|
11469
|
+
for (let i = 1; i < args.length; i++) {
|
|
11470
|
+
const arg = args[i];
|
|
11471
|
+
if (arg.startsWith('--ext:')) {
|
|
11472
|
+
unzipExtOptions.ext.push(...splitExtractSuffixInput(arg.substring(6)));
|
|
11473
|
+
} else if (arg.startsWith('--ext=')) {
|
|
11474
|
+
unzipExtOptions.ext.push(...splitExtractSuffixInput(arg.substring(6)));
|
|
11475
|
+
} else if (arg === '--ext') {
|
|
11476
|
+
const nextArg = args[i + 1];
|
|
11477
|
+
if (typeof nextArg === 'string' && !nextArg.startsWith('--')) {
|
|
11478
|
+
unzipExtOptions.ext.push(...splitExtractSuffixInput(nextArg));
|
|
11479
|
+
i += 1;
|
|
11480
|
+
}
|
|
11481
|
+
} else if (arg === '--recursive') {
|
|
11482
|
+
unzipExtOptions.recursive = true;
|
|
11483
|
+
} else if (arg === '--no-recursive') {
|
|
11484
|
+
unzipExtOptions.recursive = false;
|
|
11485
|
+
} else if (!zipDirPath) {
|
|
11486
|
+
zipDirPath = arg;
|
|
11487
|
+
} else if (!outputDir) {
|
|
11488
|
+
outputDir = arg;
|
|
11489
|
+
}
|
|
11490
|
+
}
|
|
11491
|
+
await cmdUnzipExt(zipDirPath, outputDir, unzipExtOptions);
|
|
11492
|
+
break;
|
|
11493
|
+
}
|
|
10826
11494
|
default:
|
|
10827
11495
|
console.error('错误: 未知命令:', command);
|
|
10828
11496
|
console.log('运行 "codexmate" 查看帮助');
|