codexmate 0.0.15 → 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 +12 -1
- package/README.md +12 -1
- package/cli.js +671 -13
- package/package.json +1 -1
- package/web-ui/styles.css +4 -4
package/README.en.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
**Local configuration and session manager for Codex / Claude Code / OpenClaw**
|
|
6
6
|
|
|
7
|
-
> Current version: `v0.0.
|
|
7
|
+
> Current version: `v0.0.16`
|
|
8
8
|
|
|
9
9
|
[](https://github.com/SakuraByteCore/codexmate/actions/workflows/release.yml)
|
|
10
10
|
[](https://www.npmjs.com/package/codexmate)
|
|
@@ -130,11 +130,22 @@ codexmate run --no-browser
|
|
|
130
130
|
| `codexmate auth <list\|import\|switch\|delete\|status>` | Auth profile management |
|
|
131
131
|
| `codexmate proxy <status\|set\|apply\|enable\|start\|stop>` | Built-in proxy management |
|
|
132
132
|
| `codexmate workflow <list\|get\|validate\|run\|runs>` | MCP workflow management |
|
|
133
|
+
| `codexmate codex [args...] [--follow-up <text> repeatable]` | Codex CLI passthrough entrypoint (auto-adds `--yolo`, supports queued follow-up appends) |
|
|
133
134
|
| `codexmate qwen [args...]` | Qwen CLI passthrough entrypoint |
|
|
134
135
|
| `codexmate run [--host <HOST>] [--no-browser]` | Start Web UI |
|
|
135
136
|
| `codexmate mcp serve [--read-only\|--allow-write]` | Start MCP stdio server |
|
|
136
137
|
| `codexmate export-session --source <codex\|claude> ...` | Export session to Markdown |
|
|
137
138
|
| `codexmate zip <path> [--max:0-9]` / `codexmate unzip <zip> [out]` | Zip / unzip |
|
|
139
|
+
| `codexmate unzip-ext <zip-dir> [out] [--ext:suffix[,suffix...]] [--no-recursive]` | Extract files with target suffixes from ZIP files in a directory (default `.json`, recursive by default) |
|
|
140
|
+
|
|
141
|
+
### Codex Follow-up Append (Optional)
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
codexmate codex --follow-up "scan repository first" --follow-up "then fix failing tests"
|
|
145
|
+
codexmate codex --model gpt-5.3-codex --follow-up "step1" --follow-up "step2"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
> Note: both `--follow-up` and `--queued-follow-up` are accepted and repeatable.
|
|
138
149
|
|
|
139
150
|
## Web UI
|
|
140
151
|
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
**Codex / Claude Code / OpenClaw 的本地配置与会话管理工具**
|
|
6
6
|
|
|
7
|
-
> 当前版本:`v0.0.
|
|
7
|
+
> 当前版本:`v0.0.16`
|
|
8
8
|
|
|
9
9
|
[](https://github.com/SakuraByteCore/codexmate/actions/workflows/release.yml)
|
|
10
10
|
[](https://www.npmjs.com/package/codexmate)
|
|
@@ -130,11 +130,22 @@ codexmate run --no-browser
|
|
|
130
130
|
| `codexmate auth <list\|import\|switch\|delete\|status>` | 认证档案管理 |
|
|
131
131
|
| `codexmate proxy <status\|set\|apply\|enable\|start\|stop>` | 内建代理管理 |
|
|
132
132
|
| `codexmate workflow <list\|get\|validate\|run\|runs>` | MCP 工作流管理 |
|
|
133
|
+
| `codexmate codex [args...] [--follow-up <文本> 可重复]` | Codex CLI 透传入口(默认补 `--yolo`,可追加 queued follow-up) |
|
|
133
134
|
| `codexmate qwen [args...]` | Qwen CLI 透传入口 |
|
|
134
135
|
| `codexmate run [--host <HOST>] [--no-browser]` | 启动 Web UI |
|
|
135
136
|
| `codexmate mcp serve [--read-only\|--allow-write]` | 启动 MCP stdio 服务 |
|
|
136
137
|
| `codexmate export-session --source <codex\|claude> ...` | 导出会话为 Markdown |
|
|
137
138
|
| `codexmate zip <path> [--max:0-9]` / `codexmate unzip <zip> [out]` | 压缩 / 解压 |
|
|
139
|
+
| `codexmate unzip-ext <zip-dir> [out] [--ext:suffix[,suffix...]] [--no-recursive]` | 批量提取目录下 ZIP 内指定后缀文件(默认 `.json`,默认递归) |
|
|
140
|
+
|
|
141
|
+
### Codex follow-up 追加(可选)
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
codexmate codex --follow-up "先扫描项目" --follow-up "再修复失败测试"
|
|
145
|
+
codexmate codex --model gpt-5.3-codex --follow-up "步骤1" --follow-up "步骤2"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
> 说明:`--follow-up` / `--queued-follow-up` 都可用,支持重复。
|
|
138
149
|
|
|
139
150
|
## Web 界面
|
|
140
151
|
|
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';
|
|
@@ -7784,6 +7785,318 @@ async function cmdUnzip(zipPath, outputDir) {
|
|
|
7784
7785
|
}
|
|
7785
7786
|
}
|
|
7786
7787
|
|
|
7788
|
+
function splitExtractSuffixInput(rawValue) {
|
|
7789
|
+
if (Array.isArray(rawValue)) {
|
|
7790
|
+
return rawValue.flatMap((item) => splitExtractSuffixInput(item));
|
|
7791
|
+
}
|
|
7792
|
+
if (typeof rawValue !== 'string') {
|
|
7793
|
+
return [];
|
|
7794
|
+
}
|
|
7795
|
+
return rawValue
|
|
7796
|
+
.split(/[,\s]+/g)
|
|
7797
|
+
.map((item) => item.trim())
|
|
7798
|
+
.filter(Boolean);
|
|
7799
|
+
}
|
|
7800
|
+
|
|
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
|
+
|
|
7787
8100
|
function resolveExportOutputPath(outputPath, defaultFileName) {
|
|
7788
8101
|
const fallback = path.resolve(process.cwd(), defaultFileName);
|
|
7789
8102
|
if (typeof outputPath !== 'string' || !outputPath.trim()) {
|
|
@@ -9233,7 +9546,317 @@ async function cmdWorkflow(args = []) {
|
|
|
9233
9546
|
throw new Error(`未知 workflow 子命令: ${subcommand}`);
|
|
9234
9547
|
}
|
|
9235
9548
|
|
|
9236
|
-
|
|
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 = {}) {
|
|
9237
9860
|
const extraArgs = Array.isArray(args) ? args.filter(arg => arg !== undefined) : [];
|
|
9238
9861
|
const hasYolo = extraArgs.includes('--yolo');
|
|
9239
9862
|
const finalArgs = hasYolo ? extraArgs : ['--yolo', ...extraArgs];
|
|
@@ -9259,6 +9882,14 @@ async function runProxyCommand(displayName, binNames, args = [], installTip = ''
|
|
|
9259
9882
|
throw new Error(msg);
|
|
9260
9883
|
}
|
|
9261
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
|
+
|
|
9262
9893
|
return new Promise((resolve, reject) => {
|
|
9263
9894
|
const child = spawn(selectedBin, finalArgs, {
|
|
9264
9895
|
stdio: 'inherit',
|
|
@@ -9288,17 +9919,10 @@ async function runProxyCommand(displayName, binNames, args = [], installTip = ''
|
|
|
9288
9919
|
}
|
|
9289
9920
|
|
|
9290
9921
|
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);
|
|
9922
|
+
const parsed = parseCodexProxyOptions(args);
|
|
9923
|
+
return runProxyCommand('Codex', 'codex', parsed.passthroughArgs, '', {
|
|
9924
|
+
queuedFollowUps: parsed.queuedFollowUps
|
|
9925
|
+
});
|
|
9302
9926
|
}
|
|
9303
9927
|
|
|
9304
9928
|
async function cmdQwen(args = []) {
|
|
@@ -10765,12 +11389,14 @@ async function main() {
|
|
|
10765
11389
|
console.log(' codexmate proxy <status|set|apply|enable|start|stop> 内建代理');
|
|
10766
11390
|
console.log(' codexmate workflow <list|get|validate|run|runs> MCP 工作流中心');
|
|
10767
11391
|
console.log(' codexmate run [--host <HOST>] [--no-browser] 启动 Web 界面');
|
|
10768
|
-
console.log(' codexmate codex [参数...] 等同于 codex --yolo');
|
|
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,其他平台会报错');
|
|
10769
11394
|
console.log(' codexmate qwen [参数...] 等同于 qwen --yolo');
|
|
10770
11395
|
console.log(' codexmate mcp [serve] [--transport stdio] [--allow-write|--read-only]');
|
|
10771
11396
|
console.log(' codexmate export-session --source <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
|
|
10772
11397
|
console.log(' codexmate zip <路径> [--max:级别] 压缩(系统 zip 优先,其次 zip-lib)');
|
|
10773
11398
|
console.log(' codexmate unzip <zip文件> [输出目录] 解压(zip-lib)');
|
|
11399
|
+
console.log(' codexmate unzip-ext <zip目录> [输出目录] [--ext:后缀[,后缀...]] [--no-recursive] 批量提取 ZIP 指定后缀文件(默认递归)');
|
|
10774
11400
|
console.log('');
|
|
10775
11401
|
process.exit(0);
|
|
10776
11402
|
}
|
|
@@ -10823,6 +11449,38 @@ async function main() {
|
|
|
10823
11449
|
break;
|
|
10824
11450
|
}
|
|
10825
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
|
+
}
|
|
10826
11484
|
default:
|
|
10827
11485
|
console.error('错误: 未知命令:', command);
|
|
10828
11486
|
console.log('运行 "codexmate" 查看帮助');
|
package/package.json
CHANGED
package/web-ui/styles.css
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Source+Sans+3:wght@400;500;600&family=Space+Grotesk:wght@400;500;600;700&display=swap');
|
|
1
|
+
@import url('https://fonts.googleapis.com/css2?family=Fira+Mono:wght@400;500&family=JetBrains+Mono:wght@400;500&family=Source+Sans+3:wght@400;500;600&family=Space+Grotesk:wght@400;500;600;700&display=swap');
|
|
2
2
|
|
|
3
3
|
/* ============================================
|
|
4
4
|
设计系统 - Design Tokens
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
linear-gradient(135deg, #F8F2EA 0%, #F1E4D8 44%, #F8F2EA 100%);
|
|
33
33
|
|
|
34
34
|
/* 字体系统 */
|
|
35
|
-
--font-family-body: '
|
|
36
|
-
--font-family-display: '
|
|
37
|
-
--font-family-mono: 'JetBrains Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', monospace;
|
|
35
|
+
--font-family-body: 'JetBrainsMono Nerd Font Mono', 'OPPO Sans 4.0', 'Fira Mono', 'JetBrains Mono', 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', monospace;
|
|
36
|
+
--font-family-display: 'JetBrainsMono Nerd Font Mono', 'OPPO Sans 4.0', 'Fira Mono', 'JetBrains Mono', 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', monospace;
|
|
37
|
+
--font-family-mono: 'JetBrainsMono Nerd Font Mono', 'OPPO Sans 4.0', 'Fira Mono', 'JetBrains Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', monospace;
|
|
38
38
|
--font-family: var(--font-family-body);
|
|
39
39
|
|
|
40
40
|
--font-size-display: 52px;
|