codexmate 0.0.17 → 0.0.19
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 +65 -33
- package/README.md +61 -37
- package/cli.js +1877 -408
- package/lib/text-diff.js +303 -0
- package/package.json +1 -1
- package/web-ui/app.js +1749 -447
- package/web-ui/index.html +580 -199
- package/web-ui/logic.mjs +390 -0
- package/web-ui/modules/config-mode.computed.mjs +1 -0
- package/web-ui/modules/skills.computed.mjs +26 -1
- package/web-ui/modules/skills.methods.mjs +160 -23
- package/web-ui/session-helpers.mjs +362 -0
- package/web-ui/styles.css +652 -13
- package/doc/CHANGELOG.md +0 -32
- package/doc/CHANGELOG.zh-CN.md +0 -34
package/cli.js
CHANGED
|
@@ -38,6 +38,7 @@ const {
|
|
|
38
38
|
writeJsonAtomic,
|
|
39
39
|
formatTimestampForFileName
|
|
40
40
|
} = require('./lib/cli-file-utils');
|
|
41
|
+
const { buildLineDiff } = require('./lib/text-diff');
|
|
41
42
|
const {
|
|
42
43
|
extractModelNames,
|
|
43
44
|
hasModelsListPayload,
|
|
@@ -63,7 +64,8 @@ const {
|
|
|
63
64
|
} = require('./lib/workflow-engine');
|
|
64
65
|
|
|
65
66
|
const DEFAULT_WEB_PORT = 3737;
|
|
66
|
-
const DEFAULT_WEB_HOST = '
|
|
67
|
+
const DEFAULT_WEB_HOST = '0.0.0.0';
|
|
68
|
+
const DEFAULT_WEB_OPEN_HOST = '127.0.0.1';
|
|
67
69
|
|
|
68
70
|
// ============================================================================
|
|
69
71
|
// 配置
|
|
@@ -78,6 +80,9 @@ const CURRENT_MODELS_FILE = path.join(CONFIG_DIR, 'provider-current-models.json'
|
|
|
78
80
|
const INIT_MARK_FILE = path.join(CONFIG_DIR, 'codexmate-init.json');
|
|
79
81
|
const BUILTIN_PROXY_SETTINGS_FILE = path.join(CONFIG_DIR, 'codexmate-proxy.json');
|
|
80
82
|
const CODEX_SESSIONS_DIR = path.join(CONFIG_DIR, 'sessions');
|
|
83
|
+
const SESSION_TRASH_DIR = path.join(CONFIG_DIR, 'codexmate-session-trash');
|
|
84
|
+
const SESSION_TRASH_FILES_DIR = path.join(SESSION_TRASH_DIR, 'files');
|
|
85
|
+
const SESSION_TRASH_INDEX_FILE = path.join(SESSION_TRASH_DIR, 'index.json');
|
|
81
86
|
const OPENCLAW_DIR = path.join(os.homedir(), '.openclaw');
|
|
82
87
|
const OPENCLAW_CONFIG_FILE = path.join(OPENCLAW_DIR, 'openclaw.json');
|
|
83
88
|
const OPENCLAW_WORKSPACE_DIR = path.join(OPENCLAW_DIR, 'workspace');
|
|
@@ -94,6 +99,7 @@ const DEFAULT_MODELS = ['gpt-5.3-codex', 'gpt-5.1-codex-max', 'gpt-4-turbo', 'gp
|
|
|
94
99
|
const SPEED_TEST_TIMEOUT_MS = 8000;
|
|
95
100
|
const HEALTH_CHECK_TIMEOUT_MS = 6000;
|
|
96
101
|
const MAX_SESSION_LIST_SIZE = 300;
|
|
102
|
+
const MAX_SESSION_TRASH_LIST_SIZE = 500;
|
|
97
103
|
const MAX_EXPORT_MESSAGES = 1000;
|
|
98
104
|
const DEFAULT_SESSION_DETAIL_MESSAGES = 300;
|
|
99
105
|
const MAX_SESSION_DETAIL_MESSAGES = 1000;
|
|
@@ -102,6 +108,7 @@ const CODEXMATE_MANAGED_MARKER = '# codexmate-managed: true';
|
|
|
102
108
|
const SESSION_LIST_CACHE_TTL_MS = 4000;
|
|
103
109
|
const SESSION_SUMMARY_READ_BYTES = 256 * 1024;
|
|
104
110
|
const SESSION_CONTENT_READ_BYTES = SESSION_SUMMARY_READ_BYTES;
|
|
111
|
+
const EXACT_MESSAGE_COUNT_CACHE_MAX_ENTRIES = 800;
|
|
105
112
|
const DEFAULT_CONTENT_SCAN_LIMIT = 50;
|
|
106
113
|
const SESSION_SCAN_FACTOR = 4;
|
|
107
114
|
const SESSION_SCAN_MIN_FILES = 800;
|
|
@@ -110,9 +117,13 @@ const AGENTS_FILE_NAME = 'AGENTS.md';
|
|
|
110
117
|
const CODEX_SKILLS_DIR = path.join(CONFIG_DIR, 'skills');
|
|
111
118
|
const CLAUDE_SKILLS_DIR = path.join(CLAUDE_DIR, 'skills');
|
|
112
119
|
const AGENTS_SKILLS_DIR = path.join(os.homedir(), '.agents', 'skills');
|
|
120
|
+
const SKILL_TARGETS = Object.freeze([
|
|
121
|
+
Object.freeze({ app: 'codex', label: 'Codex', dir: getCodexSkillsDir() }),
|
|
122
|
+
Object.freeze({ app: 'claude', label: 'Claude Code', dir: getClaudeSkillsDir() })
|
|
123
|
+
]);
|
|
113
124
|
const SKILL_IMPORT_SOURCES = Object.freeze([
|
|
114
|
-
|
|
115
|
-
{ app: 'agents', label: 'Agents', dir: AGENTS_SKILLS_DIR }
|
|
125
|
+
...SKILL_TARGETS,
|
|
126
|
+
Object.freeze({ app: 'agents', label: 'Agents', dir: AGENTS_SKILLS_DIR })
|
|
116
127
|
]);
|
|
117
128
|
const MODELS_CACHE_TTL_MS = 60 * 1000;
|
|
118
129
|
const MODELS_NEGATIVE_CACHE_TTL_MS = 5 * 1000;
|
|
@@ -121,6 +132,7 @@ const MODELS_RESPONSE_MAX_BYTES = 1024 * 1024;
|
|
|
121
132
|
const MAX_RECENT_CONFIGS = 3;
|
|
122
133
|
const MAX_UPLOAD_SIZE = 200 * 1024 * 1024;
|
|
123
134
|
const MAX_SKILLS_ZIP_UPLOAD_SIZE = 20 * 1024 * 1024;
|
|
135
|
+
const MAX_API_BODY_SIZE = 4 * 1024 * 1024;
|
|
124
136
|
const MAX_SKILLS_ZIP_ENTRY_COUNT = 2000;
|
|
125
137
|
const MAX_SKILLS_ZIP_UNCOMPRESSED_BYTES = 512 * 1024 * 1024;
|
|
126
138
|
const DEFAULT_EXTRACT_SUFFIXES = Object.freeze(['.json']);
|
|
@@ -160,6 +172,38 @@ const CLI_INSTALL_TARGETS = Object.freeze([
|
|
|
160
172
|
const HTTP_KEEP_ALIVE_AGENT = new http.Agent({ keepAlive: true });
|
|
161
173
|
const HTTPS_KEEP_ALIVE_AGENT = new https.Agent({ keepAlive: true });
|
|
162
174
|
|
|
175
|
+
function getCodexSkillsDir() {
|
|
176
|
+
const envCodexHome = typeof process.env.CODEX_HOME === 'string' ? process.env.CODEX_HOME.trim() : '';
|
|
177
|
+
if (envCodexHome) {
|
|
178
|
+
const target = path.join(envCodexHome, 'skills');
|
|
179
|
+
return resolveExistingDir([target], target);
|
|
180
|
+
}
|
|
181
|
+
const xdgConfig = typeof process.env.XDG_CONFIG_HOME === 'string' ? process.env.XDG_CONFIG_HOME.trim() : '';
|
|
182
|
+
if (xdgConfig) {
|
|
183
|
+
const target = path.join(xdgConfig, 'codex', 'skills');
|
|
184
|
+
return resolveExistingDir([target], target);
|
|
185
|
+
}
|
|
186
|
+
const homeConfigDir = path.join(os.homedir(), '.config', 'codex', 'skills');
|
|
187
|
+
return resolveExistingDir([homeConfigDir], CODEX_SKILLS_DIR);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function getClaudeSkillsDir() {
|
|
191
|
+
const envClaudeHome = typeof process.env.CLAUDE_HOME === 'string' && process.env.CLAUDE_HOME.trim()
|
|
192
|
+
? process.env.CLAUDE_HOME.trim()
|
|
193
|
+
: (typeof process.env.CLAUDE_CONFIG_DIR === 'string' ? process.env.CLAUDE_CONFIG_DIR.trim() : '');
|
|
194
|
+
if (envClaudeHome) {
|
|
195
|
+
const target = path.join(envClaudeHome, 'skills');
|
|
196
|
+
return resolveExistingDir([target], target);
|
|
197
|
+
}
|
|
198
|
+
const xdgConfig = typeof process.env.XDG_CONFIG_HOME === 'string' ? process.env.XDG_CONFIG_HOME.trim() : '';
|
|
199
|
+
if (xdgConfig) {
|
|
200
|
+
const target = path.join(xdgConfig, 'claude', 'skills');
|
|
201
|
+
return resolveExistingDir([target], target);
|
|
202
|
+
}
|
|
203
|
+
const homeConfigDir = path.join(os.homedir(), '.config', 'claude', 'skills');
|
|
204
|
+
return resolveExistingDir([homeConfigDir], CLAUDE_SKILLS_DIR);
|
|
205
|
+
}
|
|
206
|
+
|
|
163
207
|
function resolveWebPort() {
|
|
164
208
|
const raw = process.env.CODEXMATE_PORT;
|
|
165
209
|
if (!raw) return DEFAULT_WEB_PORT;
|
|
@@ -202,13 +246,14 @@ stream_idle_timeout_ms = 300000
|
|
|
202
246
|
|
|
203
247
|
let g_initNotice = '';
|
|
204
248
|
let g_sessionListCache = new Map();
|
|
249
|
+
let g_exactMessageCountCache = new Map();
|
|
205
250
|
let g_modelsCache = new Map();
|
|
206
251
|
let g_modelsInFlight = new Map();
|
|
207
252
|
let g_builtinProxyRuntime = null;
|
|
208
253
|
const DEFAULT_LOCAL_PROVIDER_NAME = 'local';
|
|
209
254
|
|
|
210
255
|
function isBuiltinProxyProvider(providerName) {
|
|
211
|
-
return typeof providerName === 'string' && providerName.trim() === BUILTIN_PROXY_PROVIDER_NAME;
|
|
256
|
+
return typeof providerName === 'string' && providerName.trim().toLowerCase() === BUILTIN_PROXY_PROVIDER_NAME.toLowerCase();
|
|
212
257
|
}
|
|
213
258
|
|
|
214
259
|
function isReservedProviderNameForCreation(providerName) {
|
|
@@ -881,12 +926,18 @@ function normalizeAuthRegistry(raw) {
|
|
|
881
926
|
};
|
|
882
927
|
}
|
|
883
928
|
|
|
929
|
+
function ensureAuthProfileStoragePrepared() {
|
|
930
|
+
ensureDir(AUTH_PROFILES_DIR);
|
|
931
|
+
}
|
|
932
|
+
|
|
884
933
|
function readAuthRegistry() {
|
|
934
|
+
ensureAuthProfileStoragePrepared();
|
|
885
935
|
const parsed = readJsonFile(AUTH_REGISTRY_FILE, null);
|
|
886
936
|
return normalizeAuthRegistry(parsed);
|
|
887
937
|
}
|
|
888
938
|
|
|
889
939
|
function writeAuthRegistry(registry) {
|
|
940
|
+
ensureAuthProfileStoragePrepared();
|
|
890
941
|
writeJsonAtomic(AUTH_REGISTRY_FILE, normalizeAuthRegistry(registry));
|
|
891
942
|
}
|
|
892
943
|
|
|
@@ -945,6 +996,7 @@ function listAuthProfilesInfo() {
|
|
|
945
996
|
}
|
|
946
997
|
|
|
947
998
|
function upsertAuthProfile(payload, options = {}) {
|
|
999
|
+
ensureAuthProfileStoragePrepared();
|
|
948
1000
|
const safePayload = parseAuthProfileJson(JSON.stringify(payload || {}));
|
|
949
1001
|
const sourceFile = typeof options.sourceFile === 'string' ? options.sourceFile : '';
|
|
950
1002
|
const preferredName = normalizeAuthProfileName(options.name || '');
|
|
@@ -1034,6 +1086,7 @@ function importAuthProfileFromUpload(payload = {}) {
|
|
|
1034
1086
|
}
|
|
1035
1087
|
|
|
1036
1088
|
function switchAuthProfile(name, options = {}) {
|
|
1089
|
+
ensureAuthProfileStoragePrepared();
|
|
1037
1090
|
const profileName = normalizeAuthProfileName(name);
|
|
1038
1091
|
if (!profileName) {
|
|
1039
1092
|
throw new Error('认证名称不能为空');
|
|
@@ -1079,6 +1132,7 @@ function switchAuthProfile(name, options = {}) {
|
|
|
1079
1132
|
}
|
|
1080
1133
|
|
|
1081
1134
|
function deleteAuthProfile(name) {
|
|
1135
|
+
ensureAuthProfileStoragePrepared();
|
|
1082
1136
|
const profileName = normalizeAuthProfileName(name);
|
|
1083
1137
|
if (!profileName) {
|
|
1084
1138
|
return { error: '认证名称不能为空' };
|
|
@@ -1127,6 +1181,7 @@ function deleteAuthProfile(name) {
|
|
|
1127
1181
|
}
|
|
1128
1182
|
|
|
1129
1183
|
function resolveAuthTokenFromCurrentProfile() {
|
|
1184
|
+
ensureAuthProfileStoragePrepared();
|
|
1130
1185
|
const registry = readAuthRegistry();
|
|
1131
1186
|
if (!registry.current) return '';
|
|
1132
1187
|
const profile = registry.items.find((item) => item.name === registry.current);
|
|
@@ -1382,8 +1437,40 @@ function normalizeCodexSkillName(name) {
|
|
|
1382
1437
|
return { name: value };
|
|
1383
1438
|
}
|
|
1384
1439
|
|
|
1385
|
-
function
|
|
1386
|
-
const
|
|
1440
|
+
function normalizeSkillTargetApp(app) {
|
|
1441
|
+
const value = typeof app === 'string' ? app.trim().toLowerCase() : '';
|
|
1442
|
+
return SKILL_TARGETS.some((item) => item.app === value) ? value : '';
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
function getSkillTargetByApp(app) {
|
|
1446
|
+
const normalizedApp = normalizeSkillTargetApp(app);
|
|
1447
|
+
if (!normalizedApp) return null;
|
|
1448
|
+
return SKILL_TARGETS.find((item) => item.app === normalizedApp) || null;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
function resolveSkillTarget(params = {}, defaultApp = 'codex') {
|
|
1452
|
+
const hasExplicitTargetApp = !!(params && typeof params === 'object'
|
|
1453
|
+
&& Object.prototype.hasOwnProperty.call(params, 'targetApp'));
|
|
1454
|
+
const hasExplicitTarget = !!(params && typeof params === 'object'
|
|
1455
|
+
&& Object.prototype.hasOwnProperty.call(params, 'target'));
|
|
1456
|
+
const hasAnyExplicitTarget = hasExplicitTargetApp || hasExplicitTarget;
|
|
1457
|
+
const rawTargetApp = hasExplicitTargetApp ? params.targetApp : '';
|
|
1458
|
+
const rawTarget = hasExplicitTarget ? params.target : '';
|
|
1459
|
+
const raw = rawTargetApp || rawTarget || '';
|
|
1460
|
+
if (hasAnyExplicitTarget && raw === '') {
|
|
1461
|
+
return null;
|
|
1462
|
+
}
|
|
1463
|
+
if (hasAnyExplicitTarget && !getSkillTargetByApp(raw)) {
|
|
1464
|
+
return null;
|
|
1465
|
+
}
|
|
1466
|
+
return getSkillTargetByApp(raw)
|
|
1467
|
+
|| getSkillTargetByApp(defaultApp)
|
|
1468
|
+
|| SKILL_TARGETS[0]
|
|
1469
|
+
|| null;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
function isSkillDirectoryEntryAtRoot(rootDir, entryName) {
|
|
1473
|
+
const targetPath = path.join(rootDir, entryName);
|
|
1387
1474
|
try {
|
|
1388
1475
|
const stat = fs.statSync(targetPath);
|
|
1389
1476
|
return stat.isDirectory();
|
|
@@ -1552,13 +1639,13 @@ function readCodexSkillMetadata(skillPath) {
|
|
|
1552
1639
|
}
|
|
1553
1640
|
}
|
|
1554
1641
|
|
|
1555
|
-
function
|
|
1556
|
-
const targetPath = path.join(
|
|
1642
|
+
function getSkillEntryInfoByName(rootDir, entryName) {
|
|
1643
|
+
const targetPath = path.join(rootDir, entryName);
|
|
1557
1644
|
const normalized = normalizeCodexSkillName(entryName);
|
|
1558
1645
|
if (normalized.error) {
|
|
1559
1646
|
return null;
|
|
1560
1647
|
}
|
|
1561
|
-
const relativePath = path.relative(
|
|
1648
|
+
const relativePath = path.relative(rootDir, targetPath);
|
|
1562
1649
|
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
|
|
1563
1650
|
return null;
|
|
1564
1651
|
}
|
|
@@ -1569,7 +1656,7 @@ function getCodexSkillEntryInfoByName(entryName) {
|
|
|
1569
1656
|
if (!lstat.isDirectory() && !isSymbolicLink) {
|
|
1570
1657
|
return null;
|
|
1571
1658
|
}
|
|
1572
|
-
if (isSymbolicLink && !
|
|
1659
|
+
if (isSymbolicLink && !isSkillDirectoryEntryAtRoot(rootDir, entryName)) {
|
|
1573
1660
|
return null;
|
|
1574
1661
|
}
|
|
1575
1662
|
const metadata = readCodexSkillMetadata(targetPath);
|
|
@@ -1587,26 +1674,34 @@ function getCodexSkillEntryInfoByName(entryName) {
|
|
|
1587
1674
|
}
|
|
1588
1675
|
}
|
|
1589
1676
|
|
|
1590
|
-
function
|
|
1591
|
-
|
|
1677
|
+
function listSkills(params = {}) {
|
|
1678
|
+
const target = resolveSkillTarget(params);
|
|
1679
|
+
if (!target) {
|
|
1680
|
+
return { error: '目标宿主不支持' };
|
|
1681
|
+
}
|
|
1682
|
+
if (!fs.existsSync(target.dir)) {
|
|
1592
1683
|
return {
|
|
1593
|
-
|
|
1684
|
+
targetApp: target.app,
|
|
1685
|
+
targetLabel: target.label,
|
|
1686
|
+
root: target.dir,
|
|
1594
1687
|
exists: false,
|
|
1595
1688
|
items: []
|
|
1596
1689
|
};
|
|
1597
1690
|
}
|
|
1598
1691
|
try {
|
|
1599
|
-
const entries = fs.readdirSync(
|
|
1692
|
+
const entries = fs.readdirSync(target.dir, { withFileTypes: true });
|
|
1600
1693
|
const items = entries
|
|
1601
1694
|
.map((entry) => {
|
|
1602
1695
|
const name = entry && entry.name ? entry.name : '';
|
|
1603
1696
|
if (!name || name.startsWith('.')) return null;
|
|
1604
|
-
return
|
|
1697
|
+
return getSkillEntryInfoByName(target.dir, name);
|
|
1605
1698
|
})
|
|
1606
1699
|
.filter(Boolean)
|
|
1607
1700
|
.sort((a, b) => a.displayName.localeCompare(b.displayName, 'zh-Hans-CN'));
|
|
1608
1701
|
return {
|
|
1609
|
-
|
|
1702
|
+
targetApp: target.app,
|
|
1703
|
+
targetLabel: target.label,
|
|
1704
|
+
root: target.dir,
|
|
1610
1705
|
exists: true,
|
|
1611
1706
|
items
|
|
1612
1707
|
};
|
|
@@ -1615,6 +1710,10 @@ function listCodexSkills() {
|
|
|
1615
1710
|
}
|
|
1616
1711
|
}
|
|
1617
1712
|
|
|
1713
|
+
function listCodexSkills() {
|
|
1714
|
+
return listSkills({ targetApp: 'codex' });
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1618
1717
|
function listSkillEntriesByRoot(rootDir) {
|
|
1619
1718
|
if (!rootDir || !fs.existsSync(rootDir)) {
|
|
1620
1719
|
return [];
|
|
@@ -1660,8 +1759,13 @@ function listSkillEntriesByRoot(rootDir) {
|
|
|
1660
1759
|
}
|
|
1661
1760
|
}
|
|
1662
1761
|
|
|
1663
|
-
function
|
|
1664
|
-
const
|
|
1762
|
+
function scanUnmanagedSkills(params = {}) {
|
|
1763
|
+
const target = resolveSkillTarget(params);
|
|
1764
|
+
if (!target) {
|
|
1765
|
+
return { error: '目标宿主不支持' };
|
|
1766
|
+
}
|
|
1767
|
+
const targetRoot = resolveCopyTargetRoot(target.dir);
|
|
1768
|
+
const existing = listSkills({ targetApp: target.app });
|
|
1665
1769
|
if (existing.error) {
|
|
1666
1770
|
return { error: existing.error };
|
|
1667
1771
|
}
|
|
@@ -1670,9 +1774,14 @@ function scanUnmanagedCodexSkills() {
|
|
|
1670
1774
|
.filter(Boolean));
|
|
1671
1775
|
|
|
1672
1776
|
const items = [];
|
|
1673
|
-
|
|
1777
|
+
const sources = SKILL_IMPORT_SOURCES.filter((source) => source.app !== target.app);
|
|
1778
|
+
for (const source of sources) {
|
|
1674
1779
|
const sourceEntries = listSkillEntriesByRoot(source.dir);
|
|
1675
1780
|
for (const entry of sourceEntries) {
|
|
1781
|
+
const targetCandidate = path.join(targetRoot, entry.name);
|
|
1782
|
+
if (fs.existsSync(targetCandidate)) {
|
|
1783
|
+
continue;
|
|
1784
|
+
}
|
|
1676
1785
|
if (existingNames.has(entry.name)) {
|
|
1677
1786
|
continue;
|
|
1678
1787
|
}
|
|
@@ -1698,9 +1807,11 @@ function scanUnmanagedCodexSkills() {
|
|
|
1698
1807
|
});
|
|
1699
1808
|
|
|
1700
1809
|
return {
|
|
1701
|
-
|
|
1810
|
+
targetApp: target.app,
|
|
1811
|
+
targetLabel: target.label,
|
|
1812
|
+
root: target.dir,
|
|
1702
1813
|
items,
|
|
1703
|
-
sources:
|
|
1814
|
+
sources: sources.map((source) => ({
|
|
1704
1815
|
app: source.app,
|
|
1705
1816
|
label: source.label,
|
|
1706
1817
|
path: source.dir,
|
|
@@ -1709,14 +1820,21 @@ function scanUnmanagedCodexSkills() {
|
|
|
1709
1820
|
};
|
|
1710
1821
|
}
|
|
1711
1822
|
|
|
1712
|
-
function
|
|
1823
|
+
function scanUnmanagedCodexSkills() {
|
|
1824
|
+
return scanUnmanagedSkills({ targetApp: 'codex' });
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
function importSkills(params = {}) {
|
|
1828
|
+
const target = resolveSkillTarget(params);
|
|
1829
|
+
if (!target) {
|
|
1830
|
+
return { error: '目标宿主不支持' };
|
|
1831
|
+
}
|
|
1832
|
+
const targetRoot = resolveCopyTargetRoot(target.dir);
|
|
1713
1833
|
const rawItems = Array.isArray(params.items) ? params.items : [];
|
|
1714
1834
|
if (!rawItems.length) {
|
|
1715
1835
|
return { error: '请先选择要导入的 skill' };
|
|
1716
1836
|
}
|
|
1717
1837
|
|
|
1718
|
-
ensureDir(CODEX_SKILLS_DIR);
|
|
1719
|
-
|
|
1720
1838
|
const imported = [];
|
|
1721
1839
|
const failed = [];
|
|
1722
1840
|
const dedup = new Set();
|
|
@@ -1741,6 +1859,14 @@ function importCodexSkills(params = {}) {
|
|
|
1741
1859
|
});
|
|
1742
1860
|
continue;
|
|
1743
1861
|
}
|
|
1862
|
+
if (source.app === target.app) {
|
|
1863
|
+
failed.push({
|
|
1864
|
+
name: normalizedName.name,
|
|
1865
|
+
sourceApp: source.app,
|
|
1866
|
+
error: '来源与目标相同,无需导入'
|
|
1867
|
+
});
|
|
1868
|
+
continue;
|
|
1869
|
+
}
|
|
1744
1870
|
const dedupKey = `${source.app}:${normalizedName.name}`;
|
|
1745
1871
|
if (dedup.has(dedupKey)) {
|
|
1746
1872
|
continue;
|
|
@@ -1766,8 +1892,8 @@ function importCodexSkills(params = {}) {
|
|
|
1766
1892
|
continue;
|
|
1767
1893
|
}
|
|
1768
1894
|
|
|
1769
|
-
const targetPath = path.join(
|
|
1770
|
-
const targetRelative = path.relative(
|
|
1895
|
+
const targetPath = path.join(targetRoot, normalizedName.name);
|
|
1896
|
+
const targetRelative = path.relative(targetRoot, targetPath);
|
|
1771
1897
|
if (targetRelative.startsWith('..') || path.isAbsolute(targetRelative)) {
|
|
1772
1898
|
failed.push({
|
|
1773
1899
|
name: normalizedName.name,
|
|
@@ -1780,7 +1906,7 @@ function importCodexSkills(params = {}) {
|
|
|
1780
1906
|
failed.push({
|
|
1781
1907
|
name: normalizedName.name,
|
|
1782
1908
|
sourceApp: source.app,
|
|
1783
|
-
error:
|
|
1909
|
+
error: `${target.label} 中已存在同名 skill`
|
|
1784
1910
|
});
|
|
1785
1911
|
continue;
|
|
1786
1912
|
}
|
|
@@ -1806,6 +1932,15 @@ function importCodexSkills(params = {}) {
|
|
|
1806
1932
|
});
|
|
1807
1933
|
continue;
|
|
1808
1934
|
}
|
|
1935
|
+
if (isPathInside(targetRoot, sourceDirForCopy)) {
|
|
1936
|
+
failed.push({
|
|
1937
|
+
name: normalizedName.name,
|
|
1938
|
+
sourceApp: source.app,
|
|
1939
|
+
error: '目标路径不能位于来源 skill 目录内'
|
|
1940
|
+
});
|
|
1941
|
+
continue;
|
|
1942
|
+
}
|
|
1943
|
+
ensureDir(targetRoot);
|
|
1809
1944
|
const visitedRealPaths = new Set([sourceDirForCopy]);
|
|
1810
1945
|
copyDirRecursive(sourceDirForCopy, targetPath, {
|
|
1811
1946
|
dereferenceSymlinks: true,
|
|
@@ -1817,6 +1952,8 @@ function importCodexSkills(params = {}) {
|
|
|
1817
1952
|
name: normalizedName.name,
|
|
1818
1953
|
sourceApp: source.app,
|
|
1819
1954
|
sourceLabel: source.label,
|
|
1955
|
+
targetApp: target.app,
|
|
1956
|
+
targetLabel: target.label,
|
|
1820
1957
|
path: targetPath
|
|
1821
1958
|
});
|
|
1822
1959
|
} catch (e) {
|
|
@@ -1837,10 +1974,16 @@ function importCodexSkills(params = {}) {
|
|
|
1837
1974
|
success: failed.length === 0,
|
|
1838
1975
|
imported,
|
|
1839
1976
|
failed,
|
|
1840
|
-
|
|
1977
|
+
targetApp: target.app,
|
|
1978
|
+
targetLabel: target.label,
|
|
1979
|
+
root: targetRoot
|
|
1841
1980
|
};
|
|
1842
1981
|
}
|
|
1843
1982
|
|
|
1983
|
+
function importCodexSkills(params = {}) {
|
|
1984
|
+
return importSkills({ ...(params || {}), targetApp: 'codex' });
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1844
1987
|
function collectSkillDirectoriesFromRoot(rootDir, limit = MAX_SKILLS_ZIP_ENTRY_COUNT) {
|
|
1845
1988
|
const results = [];
|
|
1846
1989
|
let truncated = false;
|
|
@@ -1894,15 +2037,22 @@ function resolveSkillNameFromImportedDirectory(skillDir, extractionRoot, fallbac
|
|
|
1894
2037
|
return normalizeCodexSkillName(candidate);
|
|
1895
2038
|
}
|
|
1896
2039
|
|
|
1897
|
-
async function
|
|
2040
|
+
async function importSkillsFromZipFile(zipPath, options = {}) {
|
|
1898
2041
|
const fallbackName = typeof options.fallbackName === 'string' ? options.fallbackName : '';
|
|
1899
2042
|
const tempDir = typeof options.tempDir === 'string' ? options.tempDir : '';
|
|
1900
2043
|
const imported = [];
|
|
1901
2044
|
const failed = [];
|
|
1902
2045
|
const dedupNames = new Set();
|
|
1903
2046
|
const extractionRoot = path.join(tempDir || path.dirname(zipPath), 'extract');
|
|
2047
|
+
let target = null;
|
|
2048
|
+
let targetRoot = '';
|
|
1904
2049
|
|
|
1905
2050
|
try {
|
|
2051
|
+
target = resolveSkillTarget(options, 'codex');
|
|
2052
|
+
if (!target) {
|
|
2053
|
+
return { error: '目标宿主不支持' };
|
|
2054
|
+
}
|
|
2055
|
+
targetRoot = resolveCopyTargetRoot(target.dir);
|
|
1906
2056
|
await inspectZipArchiveLimits(zipPath, {
|
|
1907
2057
|
maxEntryCount: MAX_SKILLS_ZIP_ENTRY_COUNT,
|
|
1908
2058
|
maxUncompressedBytes: MAX_SKILLS_ZIP_UNCOMPRESSED_BYTES
|
|
@@ -1918,7 +2068,6 @@ async function importCodexSkillsFromZipFile(zipPath, options = {}) {
|
|
|
1918
2068
|
return { error: '压缩包中的技能目录数量超出导入上限' };
|
|
1919
2069
|
}
|
|
1920
2070
|
|
|
1921
|
-
ensureDir(CODEX_SKILLS_DIR);
|
|
1922
2071
|
for (const skillDir of discoveredDirs) {
|
|
1923
2072
|
const normalizedName = resolveSkillNameFromImportedDirectory(skillDir, extractionRoot, fallbackName);
|
|
1924
2073
|
if (normalizedName.error) {
|
|
@@ -1934,8 +2083,8 @@ async function importCodexSkillsFromZipFile(zipPath, options = {}) {
|
|
|
1934
2083
|
}
|
|
1935
2084
|
dedupNames.add(dedupKey);
|
|
1936
2085
|
|
|
1937
|
-
const targetPath = path.join(
|
|
1938
|
-
const targetRelative = path.relative(
|
|
2086
|
+
const targetPath = path.join(targetRoot, normalizedName.name);
|
|
2087
|
+
const targetRelative = path.relative(targetRoot, targetPath);
|
|
1939
2088
|
if (targetRelative.startsWith('..') || path.isAbsolute(targetRelative)) {
|
|
1940
2089
|
failed.push({
|
|
1941
2090
|
name: normalizedName.name,
|
|
@@ -1946,7 +2095,7 @@ async function importCodexSkillsFromZipFile(zipPath, options = {}) {
|
|
|
1946
2095
|
if (fs.existsSync(targetPath)) {
|
|
1947
2096
|
failed.push({
|
|
1948
2097
|
name: normalizedName.name,
|
|
1949
|
-
error:
|
|
2098
|
+
error: `${target.label} 中已存在同名 skill`
|
|
1950
2099
|
});
|
|
1951
2100
|
continue;
|
|
1952
2101
|
}
|
|
@@ -1962,6 +2111,14 @@ async function importCodexSkillsFromZipFile(zipPath, options = {}) {
|
|
|
1962
2111
|
});
|
|
1963
2112
|
continue;
|
|
1964
2113
|
}
|
|
2114
|
+
if (isPathInside(targetRoot, sourceRealPath)) {
|
|
2115
|
+
failed.push({
|
|
2116
|
+
name: normalizedName.name,
|
|
2117
|
+
error: '目标路径不能位于来源 skill 目录内'
|
|
2118
|
+
});
|
|
2119
|
+
continue;
|
|
2120
|
+
}
|
|
2121
|
+
ensureDir(targetRoot);
|
|
1965
2122
|
const visitedRealPaths = new Set([sourceRealPath]);
|
|
1966
2123
|
copyDirRecursive(sourceRealPath, targetPath, {
|
|
1967
2124
|
dereferenceSymlinks: true,
|
|
@@ -1971,6 +2128,8 @@ async function importCodexSkillsFromZipFile(zipPath, options = {}) {
|
|
|
1971
2128
|
copiedToTarget = true;
|
|
1972
2129
|
imported.push({
|
|
1973
2130
|
name: normalizedName.name,
|
|
2131
|
+
targetApp: target.app,
|
|
2132
|
+
targetLabel: target.label,
|
|
1974
2133
|
path: targetPath
|
|
1975
2134
|
});
|
|
1976
2135
|
} catch (e) {
|
|
@@ -1991,7 +2150,9 @@ async function importCodexSkillsFromZipFile(zipPath, options = {}) {
|
|
|
1991
2150
|
error: failed[0].error || '导入失败',
|
|
1992
2151
|
imported,
|
|
1993
2152
|
failed,
|
|
1994
|
-
|
|
2153
|
+
targetApp: target.app,
|
|
2154
|
+
targetLabel: target.label,
|
|
2155
|
+
root: targetRoot
|
|
1995
2156
|
};
|
|
1996
2157
|
}
|
|
1997
2158
|
|
|
@@ -1999,7 +2160,9 @@ async function importCodexSkillsFromZipFile(zipPath, options = {}) {
|
|
|
1999
2160
|
success: failed.length === 0,
|
|
2000
2161
|
imported,
|
|
2001
2162
|
failed,
|
|
2002
|
-
|
|
2163
|
+
targetApp: target.app,
|
|
2164
|
+
targetLabel: target.label,
|
|
2165
|
+
root: targetRoot
|
|
2003
2166
|
};
|
|
2004
2167
|
} catch (e) {
|
|
2005
2168
|
return {
|
|
@@ -2018,21 +2181,40 @@ async function importCodexSkillsFromZipFile(zipPath, options = {}) {
|
|
|
2018
2181
|
}
|
|
2019
2182
|
}
|
|
2020
2183
|
|
|
2021
|
-
async function
|
|
2184
|
+
async function importCodexSkillsFromZipFile(zipPath, options = {}) {
|
|
2185
|
+
return importSkillsFromZipFile(zipPath, { ...(options || {}), targetApp: 'codex' });
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
async function importSkillsFromZip(payload = {}) {
|
|
2022
2189
|
if (!payload || typeof payload.fileBase64 !== 'string' || !payload.fileBase64.trim()) {
|
|
2023
2190
|
return { error: '缺少技能压缩包内容' };
|
|
2024
2191
|
}
|
|
2025
|
-
const
|
|
2192
|
+
const fallbackTarget = resolveSkillTarget(payload, 'codex');
|
|
2193
|
+
const fallbackTargetApp = fallbackTarget ? fallbackTarget.app : 'codex';
|
|
2194
|
+
const fallbackName = payload.fileName || `${fallbackTargetApp}-skills.zip`;
|
|
2195
|
+
const upload = writeUploadZip(payload.fileBase64, 'codex-skills-import', fallbackName);
|
|
2026
2196
|
if (upload.error) {
|
|
2027
2197
|
return { error: upload.error };
|
|
2028
2198
|
}
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
}
|
|
2199
|
+
const importOptions = { tempDir: upload.tempDir, fallbackName };
|
|
2200
|
+
if (Object.prototype.hasOwnProperty.call(payload, 'targetApp')) {
|
|
2201
|
+
importOptions.targetApp = payload.targetApp;
|
|
2202
|
+
}
|
|
2203
|
+
if (Object.prototype.hasOwnProperty.call(payload, 'target')) {
|
|
2204
|
+
importOptions.target = payload.target;
|
|
2205
|
+
}
|
|
2206
|
+
return importSkillsFromZipFile(upload.zipPath, importOptions);
|
|
2033
2207
|
}
|
|
2034
2208
|
|
|
2035
|
-
async function
|
|
2209
|
+
async function importCodexSkillsFromZip(payload = {}) {
|
|
2210
|
+
return importSkillsFromZip({ ...(payload || {}), targetApp: 'codex' });
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
async function exportSkills(params = {}) {
|
|
2214
|
+
const target = resolveSkillTarget(params);
|
|
2215
|
+
if (!target) {
|
|
2216
|
+
return { error: '目标宿主不支持' };
|
|
2217
|
+
}
|
|
2036
2218
|
const rawNames = Array.isArray(params.names) ? params.names : [];
|
|
2037
2219
|
const uniqueNames = Array.from(new Set(rawNames
|
|
2038
2220
|
.map((item) => (typeof item === 'string' ? item.trim() : ''))
|
|
@@ -2054,8 +2236,8 @@ async function exportCodexSkills(params = {}) {
|
|
|
2054
2236
|
failed.push({ name: rawName, error: normalizedName.error });
|
|
2055
2237
|
continue;
|
|
2056
2238
|
}
|
|
2057
|
-
const sourcePath = path.join(
|
|
2058
|
-
const sourceRelative = path.relative(
|
|
2239
|
+
const sourcePath = path.join(target.dir, normalizedName.name);
|
|
2240
|
+
const sourceRelative = path.relative(target.dir, sourcePath);
|
|
2059
2241
|
if (sourceRelative.startsWith('..') || path.isAbsolute(sourceRelative)) {
|
|
2060
2242
|
failed.push({ name: normalizedName.name, error: '来源路径非法' });
|
|
2061
2243
|
continue;
|
|
@@ -2101,12 +2283,14 @@ async function exportCodexSkills(params = {}) {
|
|
|
2101
2283
|
error: failed[0] && failed[0].error ? failed[0].error : '无可导出的 skill',
|
|
2102
2284
|
exported,
|
|
2103
2285
|
failed,
|
|
2104
|
-
|
|
2286
|
+
targetApp: target.app,
|
|
2287
|
+
targetLabel: target.label,
|
|
2288
|
+
root: target.dir
|
|
2105
2289
|
};
|
|
2106
2290
|
}
|
|
2107
2291
|
|
|
2108
2292
|
const randomToken = crypto.randomBytes(12).toString('hex');
|
|
2109
|
-
const zipFileName =
|
|
2293
|
+
const zipFileName = `${target.app}-skills-${randomToken}.zip`;
|
|
2110
2294
|
const zipFilePath = path.join(os.tmpdir(), zipFileName);
|
|
2111
2295
|
if (fs.existsSync(zipFilePath)) {
|
|
2112
2296
|
try {
|
|
@@ -2125,14 +2309,18 @@ async function exportCodexSkills(params = {}) {
|
|
|
2125
2309
|
downloadPath: artifact.downloadPath,
|
|
2126
2310
|
exported,
|
|
2127
2311
|
failed,
|
|
2128
|
-
|
|
2312
|
+
targetApp: target.app,
|
|
2313
|
+
targetLabel: target.label,
|
|
2314
|
+
root: target.dir
|
|
2129
2315
|
};
|
|
2130
2316
|
} catch (e) {
|
|
2131
2317
|
return {
|
|
2132
2318
|
error: `导出失败:${e && e.message ? e.message : '未知错误'}`,
|
|
2133
2319
|
exported,
|
|
2134
2320
|
failed,
|
|
2135
|
-
|
|
2321
|
+
targetApp: target.app,
|
|
2322
|
+
targetLabel: target.label,
|
|
2323
|
+
root: target.dir
|
|
2136
2324
|
};
|
|
2137
2325
|
} finally {
|
|
2138
2326
|
try {
|
|
@@ -2141,6 +2329,10 @@ async function exportCodexSkills(params = {}) {
|
|
|
2141
2329
|
}
|
|
2142
2330
|
}
|
|
2143
2331
|
|
|
2332
|
+
async function exportCodexSkills(params = {}) {
|
|
2333
|
+
return exportSkills({ ...(params || {}), targetApp: 'codex' });
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2144
2336
|
function removeDirectoryRecursive(targetPath) {
|
|
2145
2337
|
if (typeof fs.rmSync === 'function') {
|
|
2146
2338
|
fs.rmSync(targetPath, { recursive: true, force: false });
|
|
@@ -2149,7 +2341,11 @@ function removeDirectoryRecursive(targetPath) {
|
|
|
2149
2341
|
fs.rmdirSync(targetPath, { recursive: true });
|
|
2150
2342
|
}
|
|
2151
2343
|
|
|
2152
|
-
function
|
|
2344
|
+
function deleteSkills(params = {}) {
|
|
2345
|
+
const target = resolveSkillTarget(params);
|
|
2346
|
+
if (!target) {
|
|
2347
|
+
return { error: '目标宿主不支持' };
|
|
2348
|
+
}
|
|
2153
2349
|
const rawList = Array.isArray(params.names) ? params.names : [];
|
|
2154
2350
|
const uniqueNames = Array.from(new Set(rawList
|
|
2155
2351
|
.map((item) => (typeof item === 'string' ? item.trim() : ''))
|
|
@@ -2167,8 +2363,8 @@ function deleteCodexSkills(params = {}) {
|
|
|
2167
2363
|
continue;
|
|
2168
2364
|
}
|
|
2169
2365
|
|
|
2170
|
-
const skillPath = path.join(
|
|
2171
|
-
const relativePath = path.relative(
|
|
2366
|
+
const skillPath = path.join(target.dir, normalized.name);
|
|
2367
|
+
const relativePath = path.relative(target.dir, skillPath);
|
|
2172
2368
|
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
|
|
2173
2369
|
failed.push({ name: normalized.name, error: '技能路径非法' });
|
|
2174
2370
|
continue;
|
|
@@ -2198,10 +2394,16 @@ function deleteCodexSkills(params = {}) {
|
|
|
2198
2394
|
success: failed.length === 0,
|
|
2199
2395
|
deleted,
|
|
2200
2396
|
failed,
|
|
2201
|
-
|
|
2397
|
+
targetApp: target.app,
|
|
2398
|
+
targetLabel: target.label,
|
|
2399
|
+
root: target.dir
|
|
2202
2400
|
};
|
|
2203
2401
|
}
|
|
2204
2402
|
|
|
2403
|
+
function deleteCodexSkills(params = {}) {
|
|
2404
|
+
return deleteSkills({ ...(params || {}), targetApp: 'codex' });
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2205
2407
|
function readAgentsFile(params = {}) {
|
|
2206
2408
|
const filePath = resolveAgentsFilePath(params);
|
|
2207
2409
|
const dirCheck = validateAgentsBaseDir(filePath);
|
|
@@ -2218,6 +2420,15 @@ function readAgentsFile(params = {}) {
|
|
|
2218
2420
|
};
|
|
2219
2421
|
}
|
|
2220
2422
|
|
|
2423
|
+
if (params.metaOnly) {
|
|
2424
|
+
return {
|
|
2425
|
+
exists: true,
|
|
2426
|
+
path: filePath,
|
|
2427
|
+
content: '',
|
|
2428
|
+
lineEnding: os.EOL === '\r\n' ? '\r\n' : '\n'
|
|
2429
|
+
};
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2221
2432
|
try {
|
|
2222
2433
|
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
2223
2434
|
return {
|
|
@@ -2251,6 +2462,48 @@ function applyAgentsFile(params = {}) {
|
|
|
2251
2462
|
}
|
|
2252
2463
|
}
|
|
2253
2464
|
|
|
2465
|
+
function normalizeDiffText(input) {
|
|
2466
|
+
const safe = typeof input === 'string' ? input : '';
|
|
2467
|
+
return normalizeLineEnding(stripUtf8Bom(safe), '\n');
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
function buildAgentsDiff(params = {}) {
|
|
2471
|
+
const hasBaseContent = typeof params.baseContent === 'string';
|
|
2472
|
+
const contextRaw = typeof params.context === 'string' ? params.context.trim() : '';
|
|
2473
|
+
const context = contextRaw || 'codex';
|
|
2474
|
+
const metaOnly = hasBaseContent;
|
|
2475
|
+
let readResult;
|
|
2476
|
+
if (context === 'openclaw') {
|
|
2477
|
+
readResult = readOpenclawAgentsFile({ metaOnly });
|
|
2478
|
+
} else if (context === 'openclaw-workspace') {
|
|
2479
|
+
readResult = readOpenclawWorkspaceFile({ ...params, metaOnly });
|
|
2480
|
+
} else if (context === 'codex') {
|
|
2481
|
+
readResult = readAgentsFile({ ...params, metaOnly });
|
|
2482
|
+
} else {
|
|
2483
|
+
return { error: `Unsupported agents diff context: ${context}` };
|
|
2484
|
+
}
|
|
2485
|
+
if (readResult && readResult.error) {
|
|
2486
|
+
return { error: readResult.error };
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
const beforeText = normalizeDiffText(
|
|
2490
|
+
hasBaseContent ? params.baseContent : (readResult && readResult.content ? readResult.content : '')
|
|
2491
|
+
);
|
|
2492
|
+
const afterText = normalizeDiffText(params.content);
|
|
2493
|
+
const diff = buildLineDiff(beforeText, afterText);
|
|
2494
|
+
const hasChanges = diff.truncated ? beforeText !== afterText : (diff.stats.added > 0 || diff.stats.removed > 0);
|
|
2495
|
+
return {
|
|
2496
|
+
diff: {
|
|
2497
|
+
...diff,
|
|
2498
|
+
hasChanges
|
|
2499
|
+
},
|
|
2500
|
+
path: readResult && readResult.path ? readResult.path : '',
|
|
2501
|
+
exists: !!(readResult && readResult.exists),
|
|
2502
|
+
context,
|
|
2503
|
+
configError: readResult && readResult.configError ? readResult.configError : ''
|
|
2504
|
+
};
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2254
2507
|
function resolveOpenclawWorkspaceDir(config) {
|
|
2255
2508
|
const workspace = config
|
|
2256
2509
|
&& config.agents
|
|
@@ -2348,7 +2601,7 @@ function getOpenclawWorkspaceInfo() {
|
|
|
2348
2601
|
};
|
|
2349
2602
|
}
|
|
2350
2603
|
|
|
2351
|
-
function readOpenclawAgentsFile() {
|
|
2604
|
+
function readOpenclawAgentsFile(params = {}) {
|
|
2352
2605
|
const workspaceInfo = getOpenclawWorkspaceInfo();
|
|
2353
2606
|
const baseDir = workspaceInfo.workspaceDir;
|
|
2354
2607
|
const filePath = path.join(baseDir, AGENTS_FILE_NAME);
|
|
@@ -2365,7 +2618,7 @@ function readOpenclawAgentsFile() {
|
|
|
2365
2618
|
};
|
|
2366
2619
|
}
|
|
2367
2620
|
|
|
2368
|
-
const readResult = readAgentsFile({ baseDir });
|
|
2621
|
+
const readResult = readAgentsFile({ baseDir, metaOnly: !!params.metaOnly });
|
|
2369
2622
|
return {
|
|
2370
2623
|
...readResult,
|
|
2371
2624
|
workspaceDir: baseDir,
|
|
@@ -2420,6 +2673,17 @@ function readOpenclawWorkspaceFile(params = {}) {
|
|
|
2420
2673
|
};
|
|
2421
2674
|
}
|
|
2422
2675
|
|
|
2676
|
+
if (params.metaOnly) {
|
|
2677
|
+
return {
|
|
2678
|
+
exists: true,
|
|
2679
|
+
path: filePath,
|
|
2680
|
+
content: '',
|
|
2681
|
+
lineEnding: os.EOL === '\r\n' ? '\r\n' : '\n',
|
|
2682
|
+
workspaceDir: baseDir,
|
|
2683
|
+
configError: workspaceInfo.configError
|
|
2684
|
+
};
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2423
2687
|
try {
|
|
2424
2688
|
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
2425
2689
|
return {
|
|
@@ -2921,11 +3185,40 @@ function buildVirtualDefaultConfig() {
|
|
|
2921
3185
|
return toml.parse(EMPTY_CONFIG_FALLBACK_TEMPLATE);
|
|
2922
3186
|
}
|
|
2923
3187
|
|
|
3188
|
+
function sanitizeRemovedBuiltinProxyProvider(config) {
|
|
3189
|
+
const safeConfig = isPlainObject(config) ? config : {};
|
|
3190
|
+
const providers = isPlainObject(safeConfig.model_providers) ? safeConfig.model_providers : null;
|
|
3191
|
+
const currentProvider = typeof safeConfig.model_provider === 'string' ? safeConfig.model_provider.trim() : '';
|
|
3192
|
+
const hasRemovedBuiltin = !!(providers && providers[BUILTIN_PROXY_PROVIDER_NAME]);
|
|
3193
|
+
const currentIsRemovedBuiltin = currentProvider === BUILTIN_PROXY_PROVIDER_NAME;
|
|
3194
|
+
|
|
3195
|
+
if (!hasRemovedBuiltin && !currentIsRemovedBuiltin) {
|
|
3196
|
+
return safeConfig;
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
const nextProviders = providers ? { ...providers } : {};
|
|
3200
|
+
delete nextProviders[BUILTIN_PROXY_PROVIDER_NAME];
|
|
3201
|
+
const providerNames = Object.keys(nextProviders);
|
|
3202
|
+
const fallbackProvider = providerNames[0] || '';
|
|
3203
|
+
const currentModels = readCurrentModels();
|
|
3204
|
+
const fallbackModel = fallbackProvider
|
|
3205
|
+
? (currentModels[fallbackProvider] || (typeof safeConfig.model === 'string' ? safeConfig.model : ''))
|
|
3206
|
+
: '';
|
|
3207
|
+
|
|
3208
|
+
return {
|
|
3209
|
+
...safeConfig,
|
|
3210
|
+
model_providers: nextProviders,
|
|
3211
|
+
model_provider: currentIsRemovedBuiltin ? fallbackProvider : safeConfig.model_provider,
|
|
3212
|
+
model: currentIsRemovedBuiltin ? fallbackModel : safeConfig.model
|
|
3213
|
+
};
|
|
3214
|
+
}
|
|
3215
|
+
|
|
2924
3216
|
function readConfigOrVirtualDefault() {
|
|
2925
3217
|
if (fs.existsSync(CONFIG_FILE)) {
|
|
2926
3218
|
try {
|
|
3219
|
+
removePersistedBuiltinProxyProviderFromConfig();
|
|
2927
3220
|
return {
|
|
2928
|
-
config: readConfig(),
|
|
3221
|
+
config: sanitizeRemovedBuiltinProxyProvider(readConfig()),
|
|
2929
3222
|
isVirtual: false,
|
|
2930
3223
|
reason: '',
|
|
2931
3224
|
detail: '',
|
|
@@ -2942,7 +3235,9 @@ function readConfigOrVirtualDefault() {
|
|
|
2942
3235
|
? e.configDetail.trim()
|
|
2943
3236
|
: (e && e.message ? e.message : publicReason);
|
|
2944
3237
|
return {
|
|
2945
|
-
config: errorType === 'missing'
|
|
3238
|
+
config: errorType === 'missing'
|
|
3239
|
+
? sanitizeRemovedBuiltinProxyProvider(buildVirtualDefaultConfig())
|
|
3240
|
+
: {},
|
|
2946
3241
|
isVirtual: true,
|
|
2947
3242
|
reason: publicReason,
|
|
2948
3243
|
detail,
|
|
@@ -2952,7 +3247,7 @@ function readConfigOrVirtualDefault() {
|
|
|
2952
3247
|
}
|
|
2953
3248
|
|
|
2954
3249
|
return {
|
|
2955
|
-
config: buildVirtualDefaultConfig(),
|
|
3250
|
+
config: sanitizeRemovedBuiltinProxyProvider(buildVirtualDefaultConfig()),
|
|
2956
3251
|
isVirtual: true,
|
|
2957
3252
|
reason: '未检测到 config.toml',
|
|
2958
3253
|
detail: `配置文件不存在: ${CONFIG_FILE}`,
|
|
@@ -3048,8 +3343,8 @@ function getConfigTemplate(params = {}) {
|
|
|
3048
3343
|
}
|
|
3049
3344
|
} catch (e) {}
|
|
3050
3345
|
}
|
|
3051
|
-
const selectedProvider = params.provider
|
|
3052
|
-
const selectedModel = params.model
|
|
3346
|
+
const selectedProvider = typeof params.provider === 'string' ? params.provider.trim() : '';
|
|
3347
|
+
const selectedModel = typeof params.model === 'string' ? params.model.trim() : '';
|
|
3053
3348
|
let template = normalizeTopLevelConfigWithTemplate(content, selectedProvider, selectedModel);
|
|
3054
3349
|
if (typeof params.serviceTier === 'string') {
|
|
3055
3350
|
template = applyServiceTierToTemplate(template, params.serviceTier);
|
|
@@ -3126,7 +3421,7 @@ function addProviderToConfig(params = {}) {
|
|
|
3126
3421
|
return { error: 'local provider 为系统保留名称,不可新增' };
|
|
3127
3422
|
}
|
|
3128
3423
|
if (isBuiltinProxyProvider(name) && !allowManaged) {
|
|
3129
|
-
return { error: '
|
|
3424
|
+
return { error: 'codexmate-proxy 为保留名称,不可手动添加' };
|
|
3130
3425
|
}
|
|
3131
3426
|
|
|
3132
3427
|
ensureConfigDir();
|
|
@@ -3204,7 +3499,7 @@ function updateProviderInConfig(params = {}) {
|
|
|
3204
3499
|
if (isDefaultLocalProvider(name)) {
|
|
3205
3500
|
return { error: 'local provider 为系统保留项,不可编辑' };
|
|
3206
3501
|
}
|
|
3207
|
-
return { error: '
|
|
3502
|
+
return { error: 'codexmate-proxy 为保留名称,不可编辑' };
|
|
3208
3503
|
}
|
|
3209
3504
|
|
|
3210
3505
|
try {
|
|
@@ -3222,7 +3517,7 @@ function deleteProviderFromConfig(params = {}) {
|
|
|
3222
3517
|
if (isDefaultLocalProvider(name)) {
|
|
3223
3518
|
return { error: 'local provider 为系统保留项,不可删除' };
|
|
3224
3519
|
}
|
|
3225
|
-
return { error: '
|
|
3520
|
+
return { error: 'codexmate-proxy 为保留名称,不可删除' };
|
|
3226
3521
|
}
|
|
3227
3522
|
if (!fs.existsSync(CONFIG_FILE)) {
|
|
3228
3523
|
return { error: 'config.toml 不存在' };
|
|
@@ -3252,7 +3547,7 @@ function performProviderDeletion(name, options = {}) {
|
|
|
3252
3547
|
if (isNonDeletableProvider(name)) {
|
|
3253
3548
|
const msg = isDefaultLocalProvider(name)
|
|
3254
3549
|
? 'local provider 为系统保留项,不可删除'
|
|
3255
|
-
: '
|
|
3550
|
+
: 'codexmate-proxy 为保留名称,不可删除';
|
|
3256
3551
|
if (!silent) console.error('错误:', msg);
|
|
3257
3552
|
return { error: msg };
|
|
3258
3553
|
}
|
|
@@ -3555,6 +3850,27 @@ function isPathInside(targetPath, rootPath) {
|
|
|
3555
3850
|
return resolvedTarget.startsWith(rootWithSlash);
|
|
3556
3851
|
}
|
|
3557
3852
|
|
|
3853
|
+
function resolveCopyTargetRoot(targetDir) {
|
|
3854
|
+
const suffixSegments = [];
|
|
3855
|
+
let current = path.resolve(targetDir || '');
|
|
3856
|
+
while (current && !fs.existsSync(current)) {
|
|
3857
|
+
const parent = path.dirname(current);
|
|
3858
|
+
if (!parent || parent === current) {
|
|
3859
|
+
break;
|
|
3860
|
+
}
|
|
3861
|
+
suffixSegments.unshift(path.basename(current));
|
|
3862
|
+
current = parent;
|
|
3863
|
+
}
|
|
3864
|
+
let resolvedRoot = normalizePathForCompare(current || targetDir);
|
|
3865
|
+
if (!resolvedRoot) {
|
|
3866
|
+
resolvedRoot = path.resolve(targetDir || '');
|
|
3867
|
+
}
|
|
3868
|
+
for (const segment of suffixSegments) {
|
|
3869
|
+
resolvedRoot = path.join(resolvedRoot, segment);
|
|
3870
|
+
}
|
|
3871
|
+
return resolvedRoot;
|
|
3872
|
+
}
|
|
3873
|
+
|
|
3558
3874
|
function collectJsonlFiles(rootDir, maxFiles = 5000) {
|
|
3559
3875
|
if (!fs.existsSync(rootDir)) {
|
|
3560
3876
|
return [];
|
|
@@ -3656,6 +3972,102 @@ function parseJsonlHeadRecords(filePath, maxBytes = SESSION_SUMMARY_READ_BYTES)
|
|
|
3656
3972
|
return parseJsonlContent(headText);
|
|
3657
3973
|
}
|
|
3658
3974
|
|
|
3975
|
+
function buildClaudeStoredIndexMessageCount(messageCount) {
|
|
3976
|
+
const safeCount = Number.isFinite(Number(messageCount))
|
|
3977
|
+
? Math.max(0, Math.floor(Number(messageCount)))
|
|
3978
|
+
: 0;
|
|
3979
|
+
return safeCount + 1;
|
|
3980
|
+
}
|
|
3981
|
+
|
|
3982
|
+
function getFileStatSafe(filePath) {
|
|
3983
|
+
try {
|
|
3984
|
+
return fs.statSync(filePath);
|
|
3985
|
+
} catch (e) {
|
|
3986
|
+
return null;
|
|
3987
|
+
}
|
|
3988
|
+
}
|
|
3989
|
+
|
|
3990
|
+
function getFileMtimeMs(filePath, stat = null) {
|
|
3991
|
+
const fileStat = stat || getFileStatSafe(filePath);
|
|
3992
|
+
if (!fileStat || !Number.isFinite(Number(fileStat.mtimeMs))) {
|
|
3993
|
+
return 0;
|
|
3994
|
+
}
|
|
3995
|
+
return Math.max(0, Math.floor(Number(fileStat.mtimeMs)));
|
|
3996
|
+
}
|
|
3997
|
+
|
|
3998
|
+
function isSessionSummaryMessageCountExact(stat, maxBytes = SESSION_SUMMARY_READ_BYTES) {
|
|
3999
|
+
if (!stat || !Number.isFinite(Number(stat.size))) {
|
|
4000
|
+
return false;
|
|
4001
|
+
}
|
|
4002
|
+
return Number(stat.size) <= maxBytes;
|
|
4003
|
+
}
|
|
4004
|
+
|
|
4005
|
+
function buildExactMessageCountCacheKey(filePath, source, stat = null) {
|
|
4006
|
+
const validSource = source === 'claude' ? 'claude' : (source === 'codex' ? 'codex' : '');
|
|
4007
|
+
if (!validSource || !filePath) {
|
|
4008
|
+
return '';
|
|
4009
|
+
}
|
|
4010
|
+
const mtimeMs = getFileMtimeMs(filePath, stat);
|
|
4011
|
+
if (!mtimeMs) {
|
|
4012
|
+
return '';
|
|
4013
|
+
}
|
|
4014
|
+
return `${validSource}:${path.resolve(filePath)}:${mtimeMs}`;
|
|
4015
|
+
}
|
|
4016
|
+
|
|
4017
|
+
function readExactMessageCountCache(filePath, source, stat = null) {
|
|
4018
|
+
const cacheKey = buildExactMessageCountCacheKey(filePath, source, stat);
|
|
4019
|
+
if (!cacheKey) {
|
|
4020
|
+
return null;
|
|
4021
|
+
}
|
|
4022
|
+
if (!g_exactMessageCountCache.has(cacheKey)) {
|
|
4023
|
+
return null;
|
|
4024
|
+
}
|
|
4025
|
+
const cached = g_exactMessageCountCache.get(cacheKey);
|
|
4026
|
+
g_exactMessageCountCache.delete(cacheKey);
|
|
4027
|
+
g_exactMessageCountCache.set(cacheKey, cached);
|
|
4028
|
+
return Number.isFinite(Number(cached)) ? Math.max(0, Math.floor(Number(cached))) : null;
|
|
4029
|
+
}
|
|
4030
|
+
|
|
4031
|
+
function writeExactMessageCountCache(filePath, source, messageCount, stat = null) {
|
|
4032
|
+
const cacheKey = buildExactMessageCountCacheKey(filePath, source, stat);
|
|
4033
|
+
const safeCount = Number.isFinite(Number(messageCount))
|
|
4034
|
+
? Math.max(0, Math.floor(Number(messageCount)))
|
|
4035
|
+
: null;
|
|
4036
|
+
if (!cacheKey || safeCount === null) {
|
|
4037
|
+
return;
|
|
4038
|
+
}
|
|
4039
|
+
if (g_exactMessageCountCache.has(cacheKey)) {
|
|
4040
|
+
g_exactMessageCountCache.delete(cacheKey);
|
|
4041
|
+
}
|
|
4042
|
+
g_exactMessageCountCache.set(cacheKey, safeCount);
|
|
4043
|
+
if (g_exactMessageCountCache.size <= EXACT_MESSAGE_COUNT_CACHE_MAX_ENTRIES) {
|
|
4044
|
+
return;
|
|
4045
|
+
}
|
|
4046
|
+
const firstKey = g_exactMessageCountCache.keys().next().value;
|
|
4047
|
+
if (firstKey) {
|
|
4048
|
+
g_exactMessageCountCache.delete(firstKey);
|
|
4049
|
+
}
|
|
4050
|
+
}
|
|
4051
|
+
|
|
4052
|
+
async function mapWithConcurrency(items, concurrency, mapper) {
|
|
4053
|
+
const list = Array.isArray(items) ? items : [];
|
|
4054
|
+
if (list.length === 0) {
|
|
4055
|
+
return [];
|
|
4056
|
+
}
|
|
4057
|
+
const safeConcurrency = Math.max(1, Math.min(Math.floor(Number(concurrency)) || 1, list.length));
|
|
4058
|
+
const results = new Array(list.length);
|
|
4059
|
+
let nextIndex = 0;
|
|
4060
|
+
const workers = Array.from({ length: safeConcurrency }, async () => {
|
|
4061
|
+
while (nextIndex < list.length) {
|
|
4062
|
+
const currentIndex = nextIndex;
|
|
4063
|
+
nextIndex += 1;
|
|
4064
|
+
results[currentIndex] = await mapper(list[currentIndex], currentIndex);
|
|
4065
|
+
}
|
|
4066
|
+
});
|
|
4067
|
+
await Promise.all(workers);
|
|
4068
|
+
return results.filter((item) => item !== undefined);
|
|
4069
|
+
}
|
|
4070
|
+
|
|
3659
4071
|
function isBootstrapLikeText(text) {
|
|
3660
4072
|
if (!text || typeof text !== 'string') {
|
|
3661
4073
|
return false;
|
|
@@ -3723,48 +4135,349 @@ function countConversationMessagesInRecords(records, source) {
|
|
|
3723
4135
|
return removeLeadingSystemMessage(messages).length;
|
|
3724
4136
|
}
|
|
3725
4137
|
|
|
3726
|
-
function
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
return
|
|
3731
|
-
});
|
|
3732
|
-
return items;
|
|
3733
|
-
}
|
|
3734
|
-
|
|
3735
|
-
function mergeAndLimitSessions(items, limit) {
|
|
3736
|
-
const deduped = [];
|
|
3737
|
-
const seen = new Set();
|
|
3738
|
-
for (const item of items) {
|
|
3739
|
-
if (!item || !item.filePath) continue;
|
|
3740
|
-
const key = `${item.source}:${item.filePath}`;
|
|
3741
|
-
if (seen.has(key)) continue;
|
|
3742
|
-
seen.add(key);
|
|
3743
|
-
deduped.push(item);
|
|
4138
|
+
async function countConversationMessagesInFile(filePath, source) {
|
|
4139
|
+
const fileStat = getFileStatSafe(filePath);
|
|
4140
|
+
const cached = readExactMessageCountCache(filePath, source, fileStat);
|
|
4141
|
+
if (cached !== null) {
|
|
4142
|
+
return cached;
|
|
3744
4143
|
}
|
|
3745
4144
|
|
|
3746
|
-
|
|
3747
|
-
|
|
4145
|
+
let stream;
|
|
4146
|
+
let rl;
|
|
4147
|
+
let messageCount = 0;
|
|
4148
|
+
let leadingSystem = true;
|
|
3748
4149
|
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
4150
|
+
try {
|
|
4151
|
+
stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
|
|
4152
|
+
rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
4153
|
+
|
|
4154
|
+
for await (const line of rl) {
|
|
4155
|
+
const trimmed = line.trim();
|
|
4156
|
+
if (!trimmed) continue;
|
|
4157
|
+
|
|
4158
|
+
let record;
|
|
4159
|
+
try {
|
|
4160
|
+
record = JSON.parse(trimmed);
|
|
4161
|
+
} catch (e) {
|
|
4162
|
+
continue;
|
|
4163
|
+
}
|
|
4164
|
+
|
|
4165
|
+
let role = '';
|
|
4166
|
+
let text = '';
|
|
4167
|
+
if (source === 'codex') {
|
|
4168
|
+
if (record.type === 'response_item' && record.payload && record.payload.type === 'message') {
|
|
4169
|
+
role = normalizeRole(record.payload.role);
|
|
4170
|
+
text = extractMessageText(record.payload.content);
|
|
4171
|
+
}
|
|
4172
|
+
} else {
|
|
4173
|
+
role = normalizeRole(record.type);
|
|
4174
|
+
if (role === 'assistant' || role === 'user' || role === 'system') {
|
|
4175
|
+
const content = record.message ? record.message.content : '';
|
|
4176
|
+
text = extractMessageText(content);
|
|
4177
|
+
} else {
|
|
4178
|
+
role = '';
|
|
4179
|
+
}
|
|
4180
|
+
}
|
|
4181
|
+
if (!role) {
|
|
4182
|
+
continue;
|
|
4183
|
+
}
|
|
4184
|
+
|
|
4185
|
+
const hasText = text.length > 0;
|
|
4186
|
+
if (leadingSystem && (role === 'system' || (hasText && isBootstrapLikeText(text)))) {
|
|
4187
|
+
continue;
|
|
4188
|
+
}
|
|
4189
|
+
|
|
4190
|
+
leadingSystem = false;
|
|
4191
|
+
messageCount += 1;
|
|
4192
|
+
}
|
|
4193
|
+
const safeCount = Math.max(0, messageCount);
|
|
4194
|
+
writeExactMessageCountCache(filePath, source, safeCount, fileStat);
|
|
4195
|
+
return safeCount;
|
|
4196
|
+
} catch (e) {
|
|
4197
|
+
const safeCount = countConversationMessagesInRecords(readJsonlRecords(filePath), source);
|
|
4198
|
+
writeExactMessageCountCache(filePath, source, safeCount, fileStat);
|
|
4199
|
+
return safeCount;
|
|
4200
|
+
} finally {
|
|
4201
|
+
if (rl) {
|
|
4202
|
+
try { rl.close(); } catch (e) {}
|
|
4203
|
+
}
|
|
4204
|
+
if (stream && !stream.destroyed && stream.destroy) {
|
|
4205
|
+
try { stream.destroy(); } catch (e) {}
|
|
4206
|
+
}
|
|
3752
4207
|
}
|
|
3753
|
-
const trimmed = pathFilter.trim();
|
|
3754
|
-
return trimmed ? trimmed.toLowerCase() : '';
|
|
3755
4208
|
}
|
|
3756
4209
|
|
|
3757
|
-
function
|
|
3758
|
-
if (!
|
|
3759
|
-
return
|
|
4210
|
+
function appendSessionDetailTailMessage(state, record, source, lineIndex = -1) {
|
|
4211
|
+
if (!state || typeof state !== 'object') {
|
|
4212
|
+
return;
|
|
3760
4213
|
}
|
|
3761
|
-
|
|
3762
|
-
|
|
4214
|
+
|
|
4215
|
+
const message = extractMessageFromRecord(record, source);
|
|
4216
|
+
if (!message) {
|
|
4217
|
+
return;
|
|
3763
4218
|
}
|
|
3764
4219
|
|
|
3765
|
-
const
|
|
3766
|
-
|
|
3767
|
-
|
|
4220
|
+
const role = normalizeRole(message.role);
|
|
4221
|
+
const text = typeof message.text === 'string' ? message.text : '';
|
|
4222
|
+
if (!role || !text) {
|
|
4223
|
+
return;
|
|
4224
|
+
}
|
|
4225
|
+
|
|
4226
|
+
if (state.leadingSystem && (role === 'system' || isBootstrapLikeText(text))) {
|
|
4227
|
+
return;
|
|
4228
|
+
}
|
|
4229
|
+
|
|
4230
|
+
state.leadingSystem = false;
|
|
4231
|
+
state.totalMessages += 1;
|
|
4232
|
+
if (!Number.isFinite(state.tailLimit) || state.tailLimit <= 0) {
|
|
4233
|
+
return;
|
|
4234
|
+
}
|
|
4235
|
+
|
|
4236
|
+
if (state.messages.length >= state.tailLimit) {
|
|
4237
|
+
state.messages.shift();
|
|
4238
|
+
}
|
|
4239
|
+
state.messages.push({
|
|
4240
|
+
role,
|
|
4241
|
+
text,
|
|
4242
|
+
timestamp: toIsoTime(record && record.timestamp, ''),
|
|
4243
|
+
recordLineIndex: Number.isInteger(lineIndex) ? lineIndex : -1
|
|
4244
|
+
});
|
|
4245
|
+
}
|
|
4246
|
+
|
|
4247
|
+
function applySessionDetailRecordMetadata(record, source, state) {
|
|
4248
|
+
if (!state || typeof state !== 'object' || !record) {
|
|
4249
|
+
return;
|
|
4250
|
+
}
|
|
4251
|
+
|
|
4252
|
+
if (record.timestamp) {
|
|
4253
|
+
state.updatedAt = toIsoTime(record.timestamp, state.updatedAt);
|
|
4254
|
+
}
|
|
4255
|
+
|
|
4256
|
+
if (source === 'codex') {
|
|
4257
|
+
if (record.type === 'session_meta' && record.payload) {
|
|
4258
|
+
state.sessionId = record.payload.id || state.sessionId;
|
|
4259
|
+
state.cwd = record.payload.cwd || state.cwd;
|
|
4260
|
+
}
|
|
4261
|
+
return;
|
|
4262
|
+
}
|
|
4263
|
+
|
|
4264
|
+
if (!state.sessionId && record.sessionId) {
|
|
4265
|
+
state.sessionId = record.sessionId;
|
|
4266
|
+
}
|
|
4267
|
+
if (!state.cwd && record.cwd) {
|
|
4268
|
+
state.cwd = record.cwd;
|
|
4269
|
+
}
|
|
4270
|
+
}
|
|
4271
|
+
|
|
4272
|
+
function extractSessionDetailPreviewFromRecords(records, source, messageLimit) {
|
|
4273
|
+
const safeMessageLimit = Number.isFinite(Number(messageLimit))
|
|
4274
|
+
? Math.max(1, Math.floor(Number(messageLimit)))
|
|
4275
|
+
: DEFAULT_SESSION_DETAIL_MESSAGES;
|
|
4276
|
+
const state = {
|
|
4277
|
+
sessionId: '',
|
|
4278
|
+
cwd: '',
|
|
4279
|
+
updatedAt: '',
|
|
4280
|
+
messages: [],
|
|
4281
|
+
tailLimit: safeMessageLimit,
|
|
4282
|
+
totalMessages: 0,
|
|
4283
|
+
leadingSystem: true
|
|
4284
|
+
};
|
|
4285
|
+
|
|
4286
|
+
for (let lineIndex = 0; lineIndex < records.length; lineIndex++) {
|
|
4287
|
+
const record = records[lineIndex];
|
|
4288
|
+
applySessionDetailRecordMetadata(record, source, state);
|
|
4289
|
+
appendSessionDetailTailMessage(state, record, source, lineIndex);
|
|
4290
|
+
}
|
|
4291
|
+
|
|
4292
|
+
return state;
|
|
4293
|
+
}
|
|
4294
|
+
|
|
4295
|
+
async function extractSessionDetailPreviewFromFile(filePath, source, messageLimit) {
|
|
4296
|
+
const safeMessageLimit = Number.isFinite(Number(messageLimit))
|
|
4297
|
+
? Math.max(1, Math.floor(Number(messageLimit)))
|
|
4298
|
+
: DEFAULT_SESSION_DETAIL_MESSAGES;
|
|
4299
|
+
const state = {
|
|
4300
|
+
sessionId: '',
|
|
4301
|
+
cwd: '',
|
|
4302
|
+
updatedAt: '',
|
|
4303
|
+
messages: [],
|
|
4304
|
+
tailLimit: safeMessageLimit,
|
|
4305
|
+
totalMessages: 0,
|
|
4306
|
+
leadingSystem: true
|
|
4307
|
+
};
|
|
4308
|
+
|
|
4309
|
+
let stream;
|
|
4310
|
+
let rl;
|
|
4311
|
+
try {
|
|
4312
|
+
stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
|
|
4313
|
+
rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
4314
|
+
|
|
4315
|
+
let lineIndex = 0;
|
|
4316
|
+
for await (const line of rl) {
|
|
4317
|
+
const currentLineIndex = lineIndex;
|
|
4318
|
+
lineIndex += 1;
|
|
4319
|
+
|
|
4320
|
+
const trimmed = line.trim();
|
|
4321
|
+
if (!trimmed) {
|
|
4322
|
+
continue;
|
|
4323
|
+
}
|
|
4324
|
+
|
|
4325
|
+
let record;
|
|
4326
|
+
try {
|
|
4327
|
+
record = JSON.parse(trimmed);
|
|
4328
|
+
} catch (e) {
|
|
4329
|
+
continue;
|
|
4330
|
+
}
|
|
4331
|
+
|
|
4332
|
+
applySessionDetailRecordMetadata(record, source, state);
|
|
4333
|
+
appendSessionDetailTailMessage(state, record, source, currentLineIndex);
|
|
4334
|
+
}
|
|
4335
|
+
return state;
|
|
4336
|
+
} catch (e) {
|
|
4337
|
+
return extractSessionDetailPreviewFromRecords(readJsonlRecords(filePath), source, safeMessageLimit);
|
|
4338
|
+
} finally {
|
|
4339
|
+
if (rl) {
|
|
4340
|
+
try { rl.close(); } catch (e) {}
|
|
4341
|
+
}
|
|
4342
|
+
if (stream && !stream.destroyed && stream.destroy) {
|
|
4343
|
+
try { stream.destroy(); } catch (e) {}
|
|
4344
|
+
}
|
|
4345
|
+
}
|
|
4346
|
+
}
|
|
4347
|
+
|
|
4348
|
+
async function resolveSessionTrashEntryExactMessageCount(entry) {
|
|
4349
|
+
const normalizedEntry = normalizeSessionTrashEntry(entry);
|
|
4350
|
+
if (!normalizedEntry) {
|
|
4351
|
+
return null;
|
|
4352
|
+
}
|
|
4353
|
+
const trashFilePath = resolveSessionTrashFilePath(normalizedEntry);
|
|
4354
|
+
if (!trashFilePath || !fs.existsSync(trashFilePath)) {
|
|
4355
|
+
return normalizedEntry;
|
|
4356
|
+
}
|
|
4357
|
+
const trashFileStat = getFileStatSafe(trashFilePath);
|
|
4358
|
+
const trashFileMtimeMs = getFileMtimeMs(trashFilePath, trashFileStat);
|
|
4359
|
+
if (
|
|
4360
|
+
Number.isFinite(Number(normalizedEntry.messageCount))
|
|
4361
|
+
&& normalizedEntry.messageCount >= 0
|
|
4362
|
+
&& trashFileMtimeMs > 0
|
|
4363
|
+
&& normalizedEntry.messageCountMtimeMs === trashFileMtimeMs
|
|
4364
|
+
) {
|
|
4365
|
+
return normalizedEntry;
|
|
4366
|
+
}
|
|
4367
|
+
|
|
4368
|
+
const exactMessageCount = await countConversationMessagesInFile(trashFilePath, normalizedEntry.source);
|
|
4369
|
+
if (!Number.isFinite(Number(exactMessageCount))) {
|
|
4370
|
+
return normalizedEntry;
|
|
4371
|
+
}
|
|
4372
|
+
|
|
4373
|
+
const safeMessageCount = Math.max(0, Math.floor(Number(exactMessageCount)));
|
|
4374
|
+
if (
|
|
4375
|
+
normalizedEntry.messageCount === safeMessageCount
|
|
4376
|
+
&& normalizedEntry.messageCountMtimeMs === trashFileMtimeMs
|
|
4377
|
+
) {
|
|
4378
|
+
return normalizedEntry;
|
|
4379
|
+
}
|
|
4380
|
+
|
|
4381
|
+
return {
|
|
4382
|
+
...normalizedEntry,
|
|
4383
|
+
messageCount: safeMessageCount,
|
|
4384
|
+
messageCountMtimeMs: trashFileMtimeMs
|
|
4385
|
+
};
|
|
4386
|
+
}
|
|
4387
|
+
|
|
4388
|
+
async function hydrateSessionTrashEntries(entries, options = {}) {
|
|
4389
|
+
const source = options.source === 'claude' ? 'claude' : (options.source === 'codex' ? 'codex' : 'all');
|
|
4390
|
+
const hydratedEntries = await mapWithConcurrency(Array.isArray(entries) ? entries : [], 8, async (entry) => {
|
|
4391
|
+
const normalizedEntry = normalizeSessionTrashEntry(entry);
|
|
4392
|
+
if (!normalizedEntry) {
|
|
4393
|
+
return undefined;
|
|
4394
|
+
}
|
|
4395
|
+
return await resolveSessionTrashEntryExactMessageCount(normalizedEntry);
|
|
4396
|
+
});
|
|
4397
|
+
|
|
4398
|
+
if (source === 'codex' || source === 'claude') {
|
|
4399
|
+
return hydratedEntries.filter((entry) => entry.source === source);
|
|
4400
|
+
}
|
|
4401
|
+
return hydratedEntries;
|
|
4402
|
+
}
|
|
4403
|
+
|
|
4404
|
+
async function hydrateSessionItemsExactMessageCount(items) {
|
|
4405
|
+
return await mapWithConcurrency(Array.isArray(items) ? items : [], 8, async (item) => {
|
|
4406
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
4407
|
+
return undefined;
|
|
4408
|
+
}
|
|
4409
|
+
if (item.__messageCountExact === true) {
|
|
4410
|
+
return item;
|
|
4411
|
+
}
|
|
4412
|
+
const source = item.source === 'claude' ? 'claude' : (item.source === 'codex' ? 'codex' : '');
|
|
4413
|
+
const filePath = typeof item.filePath === 'string' ? item.filePath : '';
|
|
4414
|
+
if (!source || !filePath || !fs.existsSync(filePath)) {
|
|
4415
|
+
return item;
|
|
4416
|
+
}
|
|
4417
|
+
|
|
4418
|
+
const exactMessageCount = await countConversationMessagesInFile(filePath, source);
|
|
4419
|
+
if (!Number.isFinite(Number(exactMessageCount))) {
|
|
4420
|
+
return item;
|
|
4421
|
+
}
|
|
4422
|
+
|
|
4423
|
+
const safeMessageCount = Math.max(0, Math.floor(Number(exactMessageCount)));
|
|
4424
|
+
if (Number(item.messageCount) === safeMessageCount) {
|
|
4425
|
+
return {
|
|
4426
|
+
...item,
|
|
4427
|
+
__messageCountExact: true
|
|
4428
|
+
};
|
|
4429
|
+
}
|
|
4430
|
+
|
|
4431
|
+
return {
|
|
4432
|
+
...item,
|
|
4433
|
+
messageCount: safeMessageCount,
|
|
4434
|
+
__messageCountExact: true
|
|
4435
|
+
};
|
|
4436
|
+
});
|
|
4437
|
+
}
|
|
4438
|
+
|
|
4439
|
+
function sortSessionsByUpdatedAt(items) {
|
|
4440
|
+
items.sort((a, b) => {
|
|
4441
|
+
const aTime = Date.parse(a.updatedAt || '') || 0;
|
|
4442
|
+
const bTime = Date.parse(b.updatedAt || '') || 0;
|
|
4443
|
+
return bTime - aTime;
|
|
4444
|
+
});
|
|
4445
|
+
return items;
|
|
4446
|
+
}
|
|
4447
|
+
|
|
4448
|
+
function mergeAndLimitSessions(items, limit) {
|
|
4449
|
+
const deduped = [];
|
|
4450
|
+
const seen = new Set();
|
|
4451
|
+
for (const item of items) {
|
|
4452
|
+
if (!item || !item.filePath) continue;
|
|
4453
|
+
const key = `${item.source}:${item.filePath}`;
|
|
4454
|
+
if (seen.has(key)) continue;
|
|
4455
|
+
seen.add(key);
|
|
4456
|
+
deduped.push(item);
|
|
4457
|
+
}
|
|
4458
|
+
|
|
4459
|
+
return sortSessionsByUpdatedAt(deduped).slice(0, limit);
|
|
4460
|
+
}
|
|
4461
|
+
|
|
4462
|
+
function normalizeSessionPathFilter(pathFilter) {
|
|
4463
|
+
if (typeof pathFilter !== 'string') {
|
|
4464
|
+
return '';
|
|
4465
|
+
}
|
|
4466
|
+
const trimmed = pathFilter.trim();
|
|
4467
|
+
return trimmed ? trimmed.toLowerCase() : '';
|
|
4468
|
+
}
|
|
4469
|
+
|
|
4470
|
+
function matchesSessionPathFilter(session, normalizedFilter) {
|
|
4471
|
+
if (!normalizedFilter) {
|
|
4472
|
+
return true;
|
|
4473
|
+
}
|
|
4474
|
+
if (!session || typeof session !== 'object') {
|
|
4475
|
+
return false;
|
|
4476
|
+
}
|
|
4477
|
+
|
|
4478
|
+
const cwd = typeof session.cwd === 'string' ? session.cwd.toLowerCase() : '';
|
|
4479
|
+
return cwd.includes(normalizedFilter);
|
|
4480
|
+
}
|
|
3768
4481
|
|
|
3769
4482
|
function normalizeQueryTokens(query) {
|
|
3770
4483
|
if (typeof query !== 'string') {
|
|
@@ -4240,6 +4953,7 @@ function parseCodexSessionSummary(filePath) {
|
|
|
4240
4953
|
createdAt,
|
|
4241
4954
|
updatedAt,
|
|
4242
4955
|
messageCount,
|
|
4956
|
+
__messageCountExact: isSessionSummaryMessageCountExact(stat),
|
|
4243
4957
|
filePath,
|
|
4244
4958
|
keywords: [],
|
|
4245
4959
|
capabilities: {}
|
|
@@ -4329,6 +5043,7 @@ function parseClaudeSessionSummary(filePath) {
|
|
|
4329
5043
|
createdAt,
|
|
4330
5044
|
updatedAt,
|
|
4331
5045
|
messageCount,
|
|
5046
|
+
__messageCountExact: isSessionSummaryMessageCountExact(stat),
|
|
4332
5047
|
filePath,
|
|
4333
5048
|
keywords: [],
|
|
4334
5049
|
capabilities: { code: true }
|
|
@@ -4425,7 +5140,8 @@ function listClaudeSessions(limit, options = {}) {
|
|
|
4425
5140
|
}
|
|
4426
5141
|
filePath = filePath ? path.resolve(filePath) : '';
|
|
4427
5142
|
|
|
4428
|
-
|
|
5143
|
+
const fileStat = getFileStatSafe(filePath);
|
|
5144
|
+
if (!fileStat) {
|
|
4429
5145
|
continue;
|
|
4430
5146
|
}
|
|
4431
5147
|
|
|
@@ -4434,7 +5150,7 @@ function listClaudeSessions(limit, options = {}) {
|
|
|
4434
5150
|
let title = truncateText(entry.summary || entry.firstPrompt || sessionId, 120);
|
|
4435
5151
|
let messageCount = Number.isFinite(entry.messageCount) ? Math.max(0, entry.messageCount - 1) : 0;
|
|
4436
5152
|
|
|
4437
|
-
const quickRecords = parseJsonlHeadRecords(filePath,
|
|
5153
|
+
const quickRecords = parseJsonlHeadRecords(filePath, SESSION_SUMMARY_READ_BYTES);
|
|
4438
5154
|
if (quickRecords.length > 0) {
|
|
4439
5155
|
const filteredCount = countConversationMessagesInRecords(quickRecords, 'claude');
|
|
4440
5156
|
if (filteredCount > 0 || messageCount === 0) {
|
|
@@ -4472,6 +5188,7 @@ function listClaudeSessions(limit, options = {}) {
|
|
|
4472
5188
|
createdAt,
|
|
4473
5189
|
updatedAt,
|
|
4474
5190
|
messageCount,
|
|
5191
|
+
__messageCountExact: quickRecords.length > 0 && isSessionSummaryMessageCountExact(fileStat),
|
|
4475
5192
|
filePath,
|
|
4476
5193
|
keywords,
|
|
4477
5194
|
capabilities
|
|
@@ -4566,6 +5283,42 @@ function listAllSessions(params = {}) {
|
|
|
4566
5283
|
return result;
|
|
4567
5284
|
}
|
|
4568
5285
|
|
|
5286
|
+
async function listAllSessionsData(params = {}) {
|
|
5287
|
+
const source = params.source === 'codex' || params.source === 'claude'
|
|
5288
|
+
? params.source
|
|
5289
|
+
: 'all';
|
|
5290
|
+
const rawLimit = Number(params.limit);
|
|
5291
|
+
const limit = Number.isFinite(rawLimit)
|
|
5292
|
+
? Math.max(1, Math.min(rawLimit, MAX_SESSION_LIST_SIZE))
|
|
5293
|
+
: 120;
|
|
5294
|
+
const forceRefresh = !!params.forceRefresh;
|
|
5295
|
+
const normalizedPathFilter = normalizeSessionPathFilter(params.pathFilter);
|
|
5296
|
+
const queryTokens = expandSessionQueryTokens(normalizeQueryTokens(params.query));
|
|
5297
|
+
const hasQuery = queryTokens.length > 0;
|
|
5298
|
+
const cacheKey = hasQuery ? '' : `exact:${source}:${limit}:${normalizedPathFilter}`;
|
|
5299
|
+
if (!hasQuery) {
|
|
5300
|
+
const cached = getSessionListCache(cacheKey, forceRefresh);
|
|
5301
|
+
if (cached) {
|
|
5302
|
+
return cached;
|
|
5303
|
+
}
|
|
5304
|
+
}
|
|
5305
|
+
|
|
5306
|
+
const sessions = listAllSessions(params);
|
|
5307
|
+
const hydratedSessions = await hydrateSessionItemsExactMessageCount(sessions);
|
|
5308
|
+
const result = hydratedSessions.map((item) => {
|
|
5309
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
5310
|
+
return item;
|
|
5311
|
+
}
|
|
5312
|
+
const normalized = { ...item };
|
|
5313
|
+
delete normalized.__messageCountExact;
|
|
5314
|
+
return normalized;
|
|
5315
|
+
});
|
|
5316
|
+
if (!hasQuery) {
|
|
5317
|
+
setSessionListCache(cacheKey, result);
|
|
5318
|
+
}
|
|
5319
|
+
return result;
|
|
5320
|
+
}
|
|
5321
|
+
|
|
4569
5322
|
function listSessionPaths(params = {}) {
|
|
4570
5323
|
const source = typeof params.source === 'string' ? params.source.trim().toLowerCase() : '';
|
|
4571
5324
|
if (source && source !== 'codex' && source !== 'claude' && source !== 'all') {
|
|
@@ -4647,6 +5400,19 @@ function resolveSessionFilePath(source, filePath, sessionId) {
|
|
|
4647
5400
|
return '';
|
|
4648
5401
|
}
|
|
4649
5402
|
|
|
5403
|
+
function getSessionFileArg(params = {}) {
|
|
5404
|
+
if (!params || typeof params !== 'object') {
|
|
5405
|
+
return '';
|
|
5406
|
+
}
|
|
5407
|
+
if (typeof params.filePath === 'string' && params.filePath.trim()) {
|
|
5408
|
+
return params.filePath.trim();
|
|
5409
|
+
}
|
|
5410
|
+
if (typeof params.file === 'string' && params.file.trim()) {
|
|
5411
|
+
return params.file.trim();
|
|
5412
|
+
}
|
|
5413
|
+
return '';
|
|
5414
|
+
}
|
|
5415
|
+
|
|
4650
5416
|
function findClaudeSessionIndexPath(sessionFilePath) {
|
|
4651
5417
|
const root = getClaudeProjectsDir();
|
|
4652
5418
|
if (!root || !sessionFilePath) {
|
|
@@ -4786,6 +5552,124 @@ function buildProxyListenUrl(settings) {
|
|
|
4786
5552
|
return `http://${host}:${settings.port}`;
|
|
4787
5553
|
}
|
|
4788
5554
|
|
|
5555
|
+
function buildBuiltinProxyProviderBaseUrl(settings) {
|
|
5556
|
+
return `${buildProxyListenUrl(settings).replace(/\/+$/, '')}/v1`;
|
|
5557
|
+
}
|
|
5558
|
+
|
|
5559
|
+
function buildBuiltinProxyProviderConfig(settings) {
|
|
5560
|
+
return {
|
|
5561
|
+
name: BUILTIN_PROXY_PROVIDER_NAME,
|
|
5562
|
+
base_url: buildBuiltinProxyProviderBaseUrl(settings),
|
|
5563
|
+
wire_api: 'responses',
|
|
5564
|
+
requires_openai_auth: false,
|
|
5565
|
+
preferred_auth_method: '',
|
|
5566
|
+
request_max_retries: 4,
|
|
5567
|
+
stream_max_retries: 10,
|
|
5568
|
+
stream_idle_timeout_ms: 300000
|
|
5569
|
+
};
|
|
5570
|
+
}
|
|
5571
|
+
|
|
5572
|
+
function injectBuiltinProxyProvider(config) {
|
|
5573
|
+
return isPlainObject(config) ? config : {};
|
|
5574
|
+
}
|
|
5575
|
+
|
|
5576
|
+
function removePersistedBuiltinProxyProviderFromConfig() {
|
|
5577
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
5578
|
+
return { success: true, removed: false };
|
|
5579
|
+
}
|
|
5580
|
+
|
|
5581
|
+
let config;
|
|
5582
|
+
try {
|
|
5583
|
+
config = readConfig();
|
|
5584
|
+
} catch (e) {
|
|
5585
|
+
return { error: e.message || '读取 config.toml 失败' };
|
|
5586
|
+
}
|
|
5587
|
+
|
|
5588
|
+
if (!config.model_providers || !config.model_providers[BUILTIN_PROXY_PROVIDER_NAME]) {
|
|
5589
|
+
return { success: true, removed: false };
|
|
5590
|
+
}
|
|
5591
|
+
|
|
5592
|
+
const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
5593
|
+
const lineEnding = content.includes('\r\n') ? '\r\n' : '\n';
|
|
5594
|
+
const hasBom = content.charCodeAt(0) === 0xFEFF;
|
|
5595
|
+
const providerConfig = config.model_providers[BUILTIN_PROXY_PROVIDER_NAME];
|
|
5596
|
+
const providerSegments = providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segments)
|
|
5597
|
+
? providerConfig.__codexmate_legacy_segments
|
|
5598
|
+
: null;
|
|
5599
|
+
const providerSegmentVariants = (() => {
|
|
5600
|
+
const variants = [];
|
|
5601
|
+
const seen = new Set();
|
|
5602
|
+
const pushVariant = (segments) => {
|
|
5603
|
+
const normalized = normalizeLegacySegments(segments);
|
|
5604
|
+
const key = buildLegacySegmentsKey(normalized);
|
|
5605
|
+
if (!key || seen.has(key)) return;
|
|
5606
|
+
seen.add(key);
|
|
5607
|
+
variants.push(normalized);
|
|
5608
|
+
};
|
|
5609
|
+
if (providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segments)) {
|
|
5610
|
+
pushVariant(providerConfig.__codexmate_legacy_segments);
|
|
5611
|
+
}
|
|
5612
|
+
if (providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segment_variants)) {
|
|
5613
|
+
for (const segments of providerConfig.__codexmate_legacy_segment_variants) {
|
|
5614
|
+
pushVariant(segments);
|
|
5615
|
+
}
|
|
5616
|
+
}
|
|
5617
|
+
if (providerSegments) {
|
|
5618
|
+
pushVariant(providerSegments);
|
|
5619
|
+
}
|
|
5620
|
+
if (variants.length === 0) {
|
|
5621
|
+
pushVariant(String(BUILTIN_PROXY_PROVIDER_NAME || '').split('.').filter((item) => item));
|
|
5622
|
+
}
|
|
5623
|
+
return variants;
|
|
5624
|
+
})();
|
|
5625
|
+
|
|
5626
|
+
let updatedContent = null;
|
|
5627
|
+
const combinedRanges = [];
|
|
5628
|
+
for (const segments of providerSegmentVariants) {
|
|
5629
|
+
combinedRanges.push(...findProviderSectionRanges(content, BUILTIN_PROXY_PROVIDER_NAME, segments));
|
|
5630
|
+
combinedRanges.push(...findProviderDescendantSectionRanges(content, segments));
|
|
5631
|
+
}
|
|
5632
|
+
if (combinedRanges.length === 0) {
|
|
5633
|
+
combinedRanges.push(...findProviderSectionRanges(content, BUILTIN_PROXY_PROVIDER_NAME, providerSegments));
|
|
5634
|
+
}
|
|
5635
|
+
|
|
5636
|
+
if (combinedRanges.length > 0) {
|
|
5637
|
+
const sorted = combinedRanges.sort((a, b) => b.start - a.start || b.end - a.end);
|
|
5638
|
+
const seen = new Set();
|
|
5639
|
+
let removedContent = content;
|
|
5640
|
+
for (const range of sorted) {
|
|
5641
|
+
const rangeKey = `${range.start}:${range.end}`;
|
|
5642
|
+
if (seen.has(rangeKey)) continue;
|
|
5643
|
+
seen.add(rangeKey);
|
|
5644
|
+
removedContent = removedContent.slice(0, range.start) + removedContent.slice(range.end);
|
|
5645
|
+
}
|
|
5646
|
+
updatedContent = removedContent.replace(/\n{3,}/g, lineEnding + lineEnding);
|
|
5647
|
+
}
|
|
5648
|
+
|
|
5649
|
+
if (!updatedContent) {
|
|
5650
|
+
const rebuilt = JSON.parse(JSON.stringify(config));
|
|
5651
|
+
delete rebuilt.model_providers[BUILTIN_PROXY_PROVIDER_NAME];
|
|
5652
|
+
const hasMarker = content.includes(CODEXMATE_MANAGED_MARKER);
|
|
5653
|
+
let rebuiltToml = toml.stringify(rebuilt).trimEnd();
|
|
5654
|
+
rebuiltToml = rebuiltToml.replace(/\n/g, lineEnding);
|
|
5655
|
+
if (hasMarker && !rebuiltToml.includes(CODEXMATE_MANAGED_MARKER)) {
|
|
5656
|
+
rebuiltToml = `${CODEXMATE_MANAGED_MARKER}${lineEnding}${rebuiltToml}`;
|
|
5657
|
+
}
|
|
5658
|
+
updatedContent = rebuiltToml + lineEnding;
|
|
5659
|
+
if (hasBom && updatedContent.charCodeAt(0) !== 0xFEFF) {
|
|
5660
|
+
updatedContent = '\uFEFF' + updatedContent;
|
|
5661
|
+
}
|
|
5662
|
+
}
|
|
5663
|
+
|
|
5664
|
+
try {
|
|
5665
|
+
writeConfig(updatedContent.trimEnd() + lineEnding);
|
|
5666
|
+
} catch (e) {
|
|
5667
|
+
return { error: e.message || '写入 config.toml 失败' };
|
|
5668
|
+
}
|
|
5669
|
+
|
|
5670
|
+
return { success: true, removed: true };
|
|
5671
|
+
}
|
|
5672
|
+
|
|
4789
5673
|
function hasCodexConfigReadyForProxy() {
|
|
4790
5674
|
const result = readConfigOrVirtualDefault();
|
|
4791
5675
|
if (!result || result.isVirtual) {
|
|
@@ -5061,168 +5945,703 @@ function getBuiltinProxyStatus() {
|
|
|
5061
5945
|
};
|
|
5062
5946
|
}
|
|
5063
5947
|
|
|
5064
|
-
function applyBuiltinProxyProvider(params = {}) {
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
}
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5948
|
+
function applyBuiltinProxyProvider(params = {}) {
|
|
5949
|
+
return { error: '该功能已移除' };
|
|
5950
|
+
}
|
|
5951
|
+
|
|
5952
|
+
async function ensureBuiltinProxyForCodexDefault(params = {}) {
|
|
5953
|
+
return { error: '该功能已移除' };
|
|
5954
|
+
}
|
|
5955
|
+
|
|
5956
|
+
function removeClaudeSessionIndexEntry(indexPath, sessionFilePath, sessionId) {
|
|
5957
|
+
if (!indexPath || !fs.existsSync(indexPath)) {
|
|
5958
|
+
return { removed: false, entry: null };
|
|
5959
|
+
}
|
|
5960
|
+
const index = readJsonFile(indexPath, null);
|
|
5961
|
+
if (!index || !Array.isArray(index.entries)) {
|
|
5962
|
+
return { removed: false, entry: null };
|
|
5963
|
+
}
|
|
5964
|
+
const ignoreCase = process.platform === 'win32';
|
|
5965
|
+
const resolvedFile = sessionFilePath
|
|
5966
|
+
? normalizePathForCompare(sessionFilePath, { ignoreCase })
|
|
5967
|
+
: '';
|
|
5968
|
+
let removedEntry = null;
|
|
5969
|
+
const filtered = index.entries.filter((entry) => {
|
|
5970
|
+
if (!entry || typeof entry !== 'object') {
|
|
5971
|
+
return false;
|
|
5972
|
+
}
|
|
5973
|
+
if (entry.fullPath) {
|
|
5974
|
+
const expanded = expandHomePath(entry.fullPath);
|
|
5975
|
+
const entryPath = expanded
|
|
5976
|
+
? normalizePathForCompare(expanded, { ignoreCase })
|
|
5977
|
+
: '';
|
|
5978
|
+
if (entryPath && resolvedFile && entryPath === resolvedFile) {
|
|
5979
|
+
if (!removedEntry) {
|
|
5980
|
+
removedEntry = entry;
|
|
5981
|
+
}
|
|
5982
|
+
return false;
|
|
5983
|
+
}
|
|
5984
|
+
}
|
|
5985
|
+
const entrySessionId = typeof entry.sessionId === 'string' ? entry.sessionId : '';
|
|
5986
|
+
if (!resolvedFile && sessionId && entrySessionId === sessionId) {
|
|
5987
|
+
if (!removedEntry) {
|
|
5988
|
+
removedEntry = entry;
|
|
5989
|
+
}
|
|
5990
|
+
return false;
|
|
5991
|
+
}
|
|
5992
|
+
return true;
|
|
5993
|
+
});
|
|
5994
|
+
if (filtered.length === index.entries.length) {
|
|
5995
|
+
return { removed: false, entry: null };
|
|
5996
|
+
}
|
|
5997
|
+
index.entries = filtered;
|
|
5998
|
+
writeJsonAtomic(indexPath, index);
|
|
5999
|
+
return {
|
|
6000
|
+
removed: true,
|
|
6001
|
+
entry: removedEntry && typeof removedEntry === 'object'
|
|
6002
|
+
? JSON.parse(JSON.stringify(removedEntry))
|
|
6003
|
+
: null
|
|
6004
|
+
};
|
|
6005
|
+
}
|
|
6006
|
+
|
|
6007
|
+
function moveFileSync(sourcePath, targetPath) {
|
|
6008
|
+
ensureDir(path.dirname(targetPath));
|
|
6009
|
+
try {
|
|
6010
|
+
fs.renameSync(sourcePath, targetPath);
|
|
6011
|
+
return;
|
|
6012
|
+
} catch (error) {
|
|
6013
|
+
if (!error || error.code !== 'EXDEV') {
|
|
6014
|
+
throw error;
|
|
6015
|
+
}
|
|
6016
|
+
}
|
|
6017
|
+
|
|
6018
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
6019
|
+
try {
|
|
6020
|
+
fs.unlinkSync(sourcePath);
|
|
6021
|
+
} catch (error) {
|
|
6022
|
+
try {
|
|
6023
|
+
fs.unlinkSync(targetPath);
|
|
6024
|
+
} catch (_) {}
|
|
6025
|
+
throw error;
|
|
6026
|
+
}
|
|
6027
|
+
}
|
|
6028
|
+
|
|
6029
|
+
function buildSessionSummaryFallback(source, filePath, sessionId = '') {
|
|
6030
|
+
const resolvedSessionId = sessionId || path.basename(filePath, '.jsonl');
|
|
6031
|
+
const sourceLabel = source === 'claude' ? 'Claude Code' : 'Codex';
|
|
6032
|
+
return {
|
|
6033
|
+
source,
|
|
6034
|
+
sourceLabel,
|
|
6035
|
+
provider: source === 'claude' ? 'claude' : 'codex',
|
|
6036
|
+
sessionId: resolvedSessionId,
|
|
6037
|
+
title: resolvedSessionId,
|
|
6038
|
+
cwd: '',
|
|
6039
|
+
createdAt: '',
|
|
6040
|
+
updatedAt: '',
|
|
6041
|
+
messageCount: 0,
|
|
6042
|
+
filePath,
|
|
6043
|
+
keywords: [],
|
|
6044
|
+
capabilities: source === 'claude' ? { code: true } : {}
|
|
6045
|
+
};
|
|
6046
|
+
}
|
|
6047
|
+
|
|
6048
|
+
function generateSessionTrashId() {
|
|
6049
|
+
if (crypto.randomUUID) {
|
|
6050
|
+
return `trash-${crypto.randomUUID()}`;
|
|
6051
|
+
}
|
|
6052
|
+
return `trash-${Date.now().toString(36)}-${crypto.randomBytes(8).toString('hex')}`;
|
|
6053
|
+
}
|
|
6054
|
+
|
|
6055
|
+
function allocateSessionTrashTarget() {
|
|
6056
|
+
ensureDir(SESSION_TRASH_FILES_DIR);
|
|
6057
|
+
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
6058
|
+
const trashId = generateSessionTrashId();
|
|
6059
|
+
const trashFileName = `${trashId}.jsonl`;
|
|
6060
|
+
const trashFilePath = path.join(SESSION_TRASH_FILES_DIR, trashFileName);
|
|
6061
|
+
if (!fs.existsSync(trashFilePath)) {
|
|
6062
|
+
return { trashId, trashFileName, trashFilePath };
|
|
6063
|
+
}
|
|
6064
|
+
}
|
|
6065
|
+
const fallbackId = `trash-${Date.now().toString(36)}-${crypto.randomBytes(8).toString('hex')}`;
|
|
6066
|
+
return {
|
|
6067
|
+
trashId: fallbackId,
|
|
6068
|
+
trashFileName: `${fallbackId}.jsonl`,
|
|
6069
|
+
trashFilePath: path.join(SESSION_TRASH_FILES_DIR, `${fallbackId}.jsonl`)
|
|
6070
|
+
};
|
|
6071
|
+
}
|
|
6072
|
+
|
|
6073
|
+
function normalizeSessionTrashEntry(entry) {
|
|
6074
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
6075
|
+
return null;
|
|
6076
|
+
}
|
|
6077
|
+
const source = entry.source === 'claude' ? 'claude' : (entry.source === 'codex' ? 'codex' : '');
|
|
6078
|
+
const trashId = typeof entry.trashId === 'string' ? entry.trashId.trim() : '';
|
|
6079
|
+
if (!source || !trashId || trashId.includes('/') || trashId.includes('\\') || trashId.includes('\0')) {
|
|
6080
|
+
return null;
|
|
6081
|
+
}
|
|
6082
|
+
const sessionId = typeof entry.sessionId === 'string' ? entry.sessionId.trim() : '';
|
|
6083
|
+
const trashFileNameRaw = typeof entry.trashFileName === 'string' ? entry.trashFileName.trim() : '';
|
|
6084
|
+
const trashFileName = path.basename(trashFileNameRaw || `${trashId}.jsonl`);
|
|
6085
|
+
if (!trashFileName || trashFileName === '.' || trashFileName === '..' || trashFileName.includes('\0')) {
|
|
6086
|
+
return null;
|
|
6087
|
+
}
|
|
6088
|
+
return {
|
|
6089
|
+
trashId,
|
|
6090
|
+
trashFileName,
|
|
6091
|
+
source,
|
|
6092
|
+
sourceLabel: source === 'claude' ? 'Claude Code' : 'Codex',
|
|
6093
|
+
sessionId: sessionId || trashId,
|
|
6094
|
+
title: typeof entry.title === 'string' && entry.title.trim() ? entry.title.trim() : (sessionId || trashId),
|
|
6095
|
+
cwd: typeof entry.cwd === 'string' ? entry.cwd : '',
|
|
6096
|
+
createdAt: typeof entry.createdAt === 'string' ? entry.createdAt : '',
|
|
6097
|
+
updatedAt: typeof entry.updatedAt === 'string' ? entry.updatedAt : '',
|
|
6098
|
+
deletedAt: typeof entry.deletedAt === 'string' ? entry.deletedAt : '',
|
|
6099
|
+
messageCount: Number.isFinite(Number(entry.messageCount))
|
|
6100
|
+
? Math.max(0, Math.floor(Number(entry.messageCount)))
|
|
6101
|
+
: 0,
|
|
6102
|
+
messageCountMtimeMs: Number.isFinite(Number(entry.messageCountMtimeMs))
|
|
6103
|
+
? Math.max(0, Math.floor(Number(entry.messageCountMtimeMs)))
|
|
6104
|
+
: 0,
|
|
6105
|
+
originalFilePath: typeof entry.originalFilePath === 'string' ? entry.originalFilePath : '',
|
|
6106
|
+
provider: typeof entry.provider === 'string' && entry.provider.trim()
|
|
6107
|
+
? entry.provider.trim()
|
|
6108
|
+
: (source === 'claude' ? 'claude' : 'codex'),
|
|
6109
|
+
keywords: normalizeKeywords(entry.keywords),
|
|
6110
|
+
capabilities: normalizeCapabilities(entry.capabilities),
|
|
6111
|
+
claudeIndexPath: typeof entry.claudeIndexPath === 'string' ? entry.claudeIndexPath : '',
|
|
6112
|
+
claudeIndexEntry: entry.claudeIndexEntry && typeof entry.claudeIndexEntry === 'object' && !Array.isArray(entry.claudeIndexEntry)
|
|
6113
|
+
? entry.claudeIndexEntry
|
|
6114
|
+
: null
|
|
6115
|
+
};
|
|
6116
|
+
}
|
|
6117
|
+
|
|
6118
|
+
function resolveSessionTrashFilePath(entry) {
|
|
6119
|
+
const normalized = normalizeSessionTrashEntry(entry);
|
|
6120
|
+
if (!normalized) {
|
|
6121
|
+
return '';
|
|
6122
|
+
}
|
|
6123
|
+
const filePath = path.join(SESSION_TRASH_FILES_DIR, normalized.trashFileName);
|
|
6124
|
+
return isPathInside(filePath, SESSION_TRASH_FILES_DIR) ? filePath : '';
|
|
6125
|
+
}
|
|
6126
|
+
|
|
6127
|
+
function writeSessionTrashEntries(entries) {
|
|
6128
|
+
writeJsonAtomic(SESSION_TRASH_INDEX_FILE, {
|
|
6129
|
+
version: 1,
|
|
6130
|
+
updatedAt: new Date().toISOString(),
|
|
6131
|
+
entries
|
|
6132
|
+
});
|
|
6133
|
+
}
|
|
6134
|
+
|
|
6135
|
+
function readSessionTrashEntries(options = {}) {
|
|
6136
|
+
const cleanup = options.cleanup !== false;
|
|
6137
|
+
const parsed = readJsonFile(SESSION_TRASH_INDEX_FILE, null);
|
|
6138
|
+
if (!parsed || !Array.isArray(parsed.entries)) {
|
|
6139
|
+
return [];
|
|
6140
|
+
}
|
|
6141
|
+
|
|
6142
|
+
const normalizedEntries = [];
|
|
6143
|
+
let dirty = false;
|
|
6144
|
+
for (const rawEntry of parsed.entries) {
|
|
6145
|
+
const entry = normalizeSessionTrashEntry(rawEntry);
|
|
6146
|
+
if (!entry) {
|
|
6147
|
+
dirty = true;
|
|
6148
|
+
continue;
|
|
6149
|
+
}
|
|
6150
|
+
const trashFilePath = resolveSessionTrashFilePath(entry);
|
|
6151
|
+
if (!trashFilePath || !fs.existsSync(trashFilePath)) {
|
|
6152
|
+
dirty = true;
|
|
6153
|
+
continue;
|
|
6154
|
+
}
|
|
6155
|
+
normalizedEntries.push(entry);
|
|
6156
|
+
}
|
|
6157
|
+
|
|
6158
|
+
if (dirty && cleanup) {
|
|
6159
|
+
writeSessionTrashEntries(normalizedEntries);
|
|
6160
|
+
}
|
|
6161
|
+
|
|
6162
|
+
return normalizedEntries;
|
|
6163
|
+
}
|
|
6164
|
+
|
|
6165
|
+
function buildSessionTrashEntry(summary, options = {}) {
|
|
6166
|
+
const source = options.source === 'claude' ? 'claude' : 'codex';
|
|
6167
|
+
const sessionId = options.sessionId || summary.sessionId || path.basename(options.originalFilePath || summary.filePath || '', '.jsonl');
|
|
6168
|
+
const claudeIndexEntry = options.claudeIndexEntry && typeof options.claudeIndexEntry === 'object' && !Array.isArray(options.claudeIndexEntry)
|
|
6169
|
+
? options.claudeIndexEntry
|
|
6170
|
+
: null;
|
|
6171
|
+
const deletedAt = typeof options.deletedAt === 'string' && options.deletedAt
|
|
6172
|
+
? options.deletedAt
|
|
6173
|
+
: new Date().toISOString();
|
|
6174
|
+
const sourceLabel = source === 'claude' ? 'Claude Code' : 'Codex';
|
|
6175
|
+
const fallbackTitle = truncateText(
|
|
6176
|
+
(claudeIndexEntry && (claudeIndexEntry.summary || claudeIndexEntry.firstPrompt)) || sessionId,
|
|
6177
|
+
120
|
|
6178
|
+
);
|
|
6179
|
+
const rawFallbackMessageCount = claudeIndexEntry && claudeIndexEntry.messageCount;
|
|
6180
|
+
const fallbackMessageCount = Number.isFinite(Number(rawFallbackMessageCount))
|
|
6181
|
+
? Math.max(0, Number(rawFallbackMessageCount))
|
|
6182
|
+
: 0;
|
|
6183
|
+
const resolvedMessageCount = Number.isFinite(Number(summary && summary.messageCount))
|
|
6184
|
+
? Math.max(0, Math.floor(Number(summary.messageCount)))
|
|
6185
|
+
: fallbackMessageCount;
|
|
6186
|
+
const messageCountMtimeMs = getFileMtimeMs(options.trashFilePath);
|
|
6187
|
+
const normalizedClaudeKeywords = claudeIndexEntry && Array.isArray(claudeIndexEntry.keywords)
|
|
6188
|
+
? normalizeKeywords(claudeIndexEntry.keywords)
|
|
6189
|
+
: [];
|
|
6190
|
+
const normalizedClaudeCapabilities = claudeIndexEntry
|
|
6191
|
+
? normalizeCapabilities(claudeIndexEntry.capabilities)
|
|
6192
|
+
: {};
|
|
6193
|
+
const normalizedSummaryKeywords = normalizeKeywords(summary.keywords);
|
|
6194
|
+
const normalizedSummaryCapabilities = normalizeCapabilities(summary.capabilities);
|
|
6195
|
+
return {
|
|
6196
|
+
trashId: options.trashId,
|
|
6197
|
+
trashFileName: options.trashFileName,
|
|
6198
|
+
source,
|
|
6199
|
+
sourceLabel,
|
|
6200
|
+
sessionId,
|
|
6201
|
+
title: summary.title || fallbackTitle || sessionId,
|
|
6202
|
+
cwd: summary.cwd || (claudeIndexEntry && typeof claudeIndexEntry.projectPath === 'string' ? claudeIndexEntry.projectPath : ''),
|
|
6203
|
+
createdAt: summary.createdAt || toIsoTime(claudeIndexEntry && claudeIndexEntry.created, ''),
|
|
6204
|
+
updatedAt: summary.updatedAt || toIsoTime(claudeIndexEntry && (claudeIndexEntry.modified || claudeIndexEntry.fileMtime), ''),
|
|
6205
|
+
deletedAt,
|
|
6206
|
+
messageCount: resolvedMessageCount,
|
|
6207
|
+
messageCountMtimeMs,
|
|
6208
|
+
originalFilePath: options.originalFilePath || summary.filePath || '',
|
|
6209
|
+
provider: (claudeIndexEntry && typeof claudeIndexEntry.provider === 'string' && claudeIndexEntry.provider.trim())
|
|
6210
|
+
? claudeIndexEntry.provider.trim()
|
|
6211
|
+
: (summary.provider || (source === 'claude' ? 'claude' : 'codex')),
|
|
6212
|
+
keywords: normalizedClaudeKeywords.length > 0 ? normalizedClaudeKeywords : normalizedSummaryKeywords,
|
|
6213
|
+
capabilities: Object.keys(normalizedClaudeCapabilities).length > 0
|
|
6214
|
+
? normalizedClaudeCapabilities
|
|
6215
|
+
: normalizedSummaryCapabilities,
|
|
6216
|
+
claudeIndexPath: typeof options.claudeIndexPath === 'string' ? options.claudeIndexPath : '',
|
|
6217
|
+
claudeIndexEntry
|
|
6218
|
+
};
|
|
6219
|
+
}
|
|
6220
|
+
|
|
6221
|
+
function resolveSessionRestoreTarget(entry) {
|
|
6222
|
+
const normalized = normalizeSessionTrashEntry(entry);
|
|
6223
|
+
if (!normalized) {
|
|
6224
|
+
return '';
|
|
6225
|
+
}
|
|
6226
|
+
const root = normalized.source === 'claude' ? getClaudeProjectsDir() : getCodexSessionsDir();
|
|
6227
|
+
const originalFilePath = typeof normalized.originalFilePath === 'string' ? normalized.originalFilePath.trim() : '';
|
|
6228
|
+
if (!root || !originalFilePath) {
|
|
6229
|
+
return '';
|
|
6230
|
+
}
|
|
6231
|
+
const expanded = expandHomePath(originalFilePath);
|
|
6232
|
+
const resolved = expanded ? path.resolve(expanded) : '';
|
|
6233
|
+
if (!resolved || !isPathInside(resolved, root)) {
|
|
6234
|
+
return '';
|
|
6235
|
+
}
|
|
6236
|
+
return resolved;
|
|
6237
|
+
}
|
|
6238
|
+
|
|
6239
|
+
function resolveClaudeSessionRestoreIndexPath(entry, targetFilePath) {
|
|
6240
|
+
const fallbackIndexPath = findClaudeSessionIndexPath(targetFilePath) || path.join(path.dirname(targetFilePath), 'sessions-index.json');
|
|
6241
|
+
const fallbackResolved = fallbackIndexPath ? path.resolve(fallbackIndexPath) : '';
|
|
6242
|
+
const candidateRaw = entry && typeof entry.claudeIndexPath === 'string' ? entry.claudeIndexPath.trim() : '';
|
|
6243
|
+
if (!candidateRaw) {
|
|
6244
|
+
return fallbackResolved;
|
|
6245
|
+
}
|
|
6246
|
+
const claudeProjectsDir = getClaudeProjectsDir();
|
|
6247
|
+
if (!claudeProjectsDir) {
|
|
6248
|
+
return fallbackResolved;
|
|
6249
|
+
}
|
|
6250
|
+
const candidateIndexPath = path.resolve(candidateRaw);
|
|
6251
|
+
if (path.basename(candidateIndexPath).toLowerCase() !== 'sessions-index.json') {
|
|
6252
|
+
return fallbackResolved;
|
|
6253
|
+
}
|
|
6254
|
+
if (!isPathInside(candidateIndexPath, claudeProjectsDir)) {
|
|
6255
|
+
return fallbackResolved;
|
|
6256
|
+
}
|
|
6257
|
+
if (!isPathInside(targetFilePath, path.dirname(candidateIndexPath))) {
|
|
6258
|
+
return fallbackResolved;
|
|
6259
|
+
}
|
|
6260
|
+
return candidateIndexPath;
|
|
6261
|
+
}
|
|
6262
|
+
|
|
6263
|
+
function buildClaudeSessionIndexEntry(entry, sessionFilePath) {
|
|
6264
|
+
const normalized = normalizeSessionTrashEntry(entry);
|
|
6265
|
+
const stored = normalized && normalized.claudeIndexEntry && typeof normalized.claudeIndexEntry === 'object'
|
|
6266
|
+
? JSON.parse(JSON.stringify(normalized.claudeIndexEntry))
|
|
6267
|
+
: {};
|
|
6268
|
+
const storedCapabilities = stored && stored.capabilities && typeof stored.capabilities === 'object' && !Array.isArray(stored.capabilities)
|
|
6269
|
+
? stored.capabilities
|
|
6270
|
+
: null;
|
|
6271
|
+
const storedKeywords = Array.isArray(stored && stored.keywords)
|
|
6272
|
+
? stored.keywords
|
|
6273
|
+
: null;
|
|
6274
|
+
const normalizedMessageCount = Number(normalized && normalized.messageCount);
|
|
6275
|
+
const storedMessageCount = Number(stored && stored.messageCount);
|
|
6276
|
+
let modifiedAt = '';
|
|
6277
|
+
try {
|
|
6278
|
+
modifiedAt = fs.statSync(sessionFilePath).mtime.toISOString();
|
|
6279
|
+
} catch (e) {
|
|
6280
|
+
modifiedAt = normalized && normalized.updatedAt ? normalized.updatedAt : new Date().toISOString();
|
|
6281
|
+
}
|
|
6282
|
+
const projectDir = path.dirname(sessionFilePath);
|
|
6283
|
+
return {
|
|
6284
|
+
...stored,
|
|
6285
|
+
sessionId: normalized.sessionId,
|
|
6286
|
+
fullPath: sessionFilePath,
|
|
6287
|
+
projectPath: (stored && typeof stored.projectPath === 'string' && stored.projectPath.trim())
|
|
6288
|
+
? stored.projectPath.trim()
|
|
6289
|
+
: projectDir,
|
|
6290
|
+
created: (stored && typeof stored.created === 'string' && stored.created.trim())
|
|
6291
|
+
? stored.created.trim()
|
|
6292
|
+
: (normalized.createdAt || modifiedAt),
|
|
6293
|
+
modified: modifiedAt,
|
|
6294
|
+
summary: (stored && typeof stored.summary === 'string' && stored.summary.trim())
|
|
6295
|
+
? stored.summary.trim()
|
|
6296
|
+
: (normalized.title || normalized.sessionId),
|
|
6297
|
+
provider: (stored && typeof stored.provider === 'string' && stored.provider.trim())
|
|
6298
|
+
? stored.provider.trim()
|
|
6299
|
+
: (normalized.provider || 'claude'),
|
|
6300
|
+
capabilities: normalizeCapabilities(
|
|
6301
|
+
storedCapabilities && Object.keys(storedCapabilities).length > 0
|
|
6302
|
+
? storedCapabilities
|
|
6303
|
+
: normalized.capabilities
|
|
6304
|
+
),
|
|
6305
|
+
keywords: normalizeKeywords(
|
|
6306
|
+
storedKeywords && storedKeywords.length > 0
|
|
6307
|
+
? storedKeywords
|
|
6308
|
+
: normalized.keywords
|
|
6309
|
+
),
|
|
6310
|
+
messageCount: Number.isFinite(normalizedMessageCount)
|
|
6311
|
+
? buildClaudeStoredIndexMessageCount(normalizedMessageCount)
|
|
6312
|
+
: (
|
|
6313
|
+
Number.isFinite(storedMessageCount)
|
|
6314
|
+
? Math.max(0, Math.floor(storedMessageCount))
|
|
6315
|
+
: buildClaudeStoredIndexMessageCount(normalized && normalized.messageCount)
|
|
6316
|
+
)
|
|
6317
|
+
};
|
|
6318
|
+
}
|
|
6319
|
+
|
|
6320
|
+
function upsertClaudeSessionIndexEntry(indexPath, sessionFilePath, entry) {
|
|
6321
|
+
if (!indexPath) {
|
|
6322
|
+
return;
|
|
6323
|
+
}
|
|
6324
|
+
const parsed = readJsonFile(indexPath, null);
|
|
6325
|
+
const index = parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
6326
|
+
? parsed
|
|
6327
|
+
: {};
|
|
6328
|
+
const entries = Array.isArray(index.entries) ? index.entries : [];
|
|
6329
|
+
const ignoreCase = process.platform === 'win32';
|
|
6330
|
+
const resolvedFile = normalizePathForCompare(sessionFilePath, { ignoreCase });
|
|
6331
|
+
const normalizedEntry = normalizeSessionTrashEntry(entry);
|
|
6332
|
+
const filtered = entries.filter((item) => {
|
|
6333
|
+
if (!item || typeof item !== 'object') {
|
|
6334
|
+
return false;
|
|
6335
|
+
}
|
|
6336
|
+
if (typeof item.fullPath === 'string' && item.fullPath) {
|
|
6337
|
+
const expanded = expandHomePath(item.fullPath);
|
|
6338
|
+
const itemPath = expanded
|
|
6339
|
+
? normalizePathForCompare(expanded, { ignoreCase })
|
|
6340
|
+
: '';
|
|
6341
|
+
if (itemPath && itemPath === resolvedFile) {
|
|
6342
|
+
return false;
|
|
6343
|
+
}
|
|
6344
|
+
}
|
|
6345
|
+
const itemSessionId = typeof item.sessionId === 'string' ? item.sessionId : '';
|
|
6346
|
+
if (!resolvedFile && normalizedEntry.sessionId && itemSessionId === normalizedEntry.sessionId) {
|
|
6347
|
+
return false;
|
|
6348
|
+
}
|
|
6349
|
+
return true;
|
|
6350
|
+
});
|
|
6351
|
+
filtered.unshift(buildClaudeSessionIndexEntry(normalizedEntry, sessionFilePath));
|
|
6352
|
+
index.entries = filtered;
|
|
6353
|
+
if (!index.originalPath) {
|
|
6354
|
+
index.originalPath = path.dirname(indexPath);
|
|
6355
|
+
}
|
|
6356
|
+
writeJsonAtomic(indexPath, index);
|
|
6357
|
+
}
|
|
6358
|
+
|
|
6359
|
+
async function listSessionTrashItems(params = {}) {
|
|
6360
|
+
const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : 'all');
|
|
6361
|
+
const countOnly = params.countOnly === true;
|
|
6362
|
+
const rawLimit = Number(params.limit);
|
|
6363
|
+
const limit = Number.isFinite(rawLimit)
|
|
6364
|
+
? Math.max(1, Math.min(rawLimit, MAX_SESSION_TRASH_LIST_SIZE))
|
|
6365
|
+
: 200;
|
|
6366
|
+
const allEntries = readSessionTrashEntries();
|
|
6367
|
+
let items = source === 'codex' || source === 'claude'
|
|
6368
|
+
? allEntries.filter((entry) => entry.source === source)
|
|
6369
|
+
: allEntries.slice();
|
|
6370
|
+
items.sort((a, b) => {
|
|
6371
|
+
const aTime = Date.parse(a.deletedAt || a.updatedAt || '') || 0;
|
|
6372
|
+
const bTime = Date.parse(b.deletedAt || b.updatedAt || '') || 0;
|
|
6373
|
+
return bTime - aTime;
|
|
6374
|
+
});
|
|
6375
|
+
const totalCount = items.length;
|
|
6376
|
+
if (countOnly) {
|
|
6377
|
+
return {
|
|
6378
|
+
totalCount,
|
|
6379
|
+
items: []
|
|
6380
|
+
};
|
|
5088
6381
|
}
|
|
5089
|
-
|
|
5090
|
-
const
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
6382
|
+
const visibleEntries = items.slice(0, limit);
|
|
6383
|
+
const hydratedVisibleEntries = await hydrateSessionTrashEntries(visibleEntries, { source });
|
|
6384
|
+
const updatedEntriesById = new Map();
|
|
6385
|
+
for (let index = 0; index < visibleEntries.length; index += 1) {
|
|
6386
|
+
const originalEntry = visibleEntries[index];
|
|
6387
|
+
const hydratedEntry = hydratedVisibleEntries[index];
|
|
6388
|
+
if (!originalEntry || !hydratedEntry) {
|
|
6389
|
+
continue;
|
|
6390
|
+
}
|
|
6391
|
+
if (
|
|
6392
|
+
originalEntry.messageCount !== hydratedEntry.messageCount
|
|
6393
|
+
|| originalEntry.messageCountMtimeMs !== hydratedEntry.messageCountMtimeMs
|
|
6394
|
+
) {
|
|
6395
|
+
updatedEntriesById.set(originalEntry.trashId, hydratedEntry);
|
|
5097
6396
|
}
|
|
5098
6397
|
}
|
|
5099
|
-
|
|
6398
|
+
if (updatedEntriesById.size > 0) {
|
|
6399
|
+
const latestEntries = readSessionTrashEntries({ cleanup: false });
|
|
6400
|
+
writeSessionTrashEntries(latestEntries.map((entry) => updatedEntriesById.get(entry.trashId) || entry));
|
|
6401
|
+
}
|
|
5100
6402
|
return {
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
6403
|
+
totalCount,
|
|
6404
|
+
items: hydratedVisibleEntries.map((item) => ({
|
|
6405
|
+
...item,
|
|
6406
|
+
trashFilePath: resolveSessionTrashFilePath(item)
|
|
6407
|
+
}))
|
|
5106
6408
|
};
|
|
5107
6409
|
}
|
|
5108
6410
|
|
|
5109
|
-
async function
|
|
5110
|
-
const
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
payload.enabled = true;
|
|
5114
|
-
|
|
5115
|
-
const saveResult = saveBuiltinProxySettings(payload);
|
|
5116
|
-
if (saveResult.error) {
|
|
5117
|
-
return { error: saveResult.error };
|
|
6411
|
+
async function restoreSessionTrashItem(params = {}) {
|
|
6412
|
+
const trashId = typeof params.trashId === 'string' ? params.trashId.trim() : '';
|
|
6413
|
+
if (!trashId) {
|
|
6414
|
+
return { error: '请先选择要恢复的回收站记录' };
|
|
5118
6415
|
}
|
|
5119
|
-
let nextSettings = saveResult.settings;
|
|
5120
6416
|
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
6417
|
+
const entries = readSessionTrashEntries();
|
|
6418
|
+
const entry = entries.find((item) => item.trashId === trashId);
|
|
6419
|
+
if (!entry) {
|
|
6420
|
+
return { error: '回收站记录不存在' };
|
|
6421
|
+
}
|
|
6422
|
+
const hydratedEntry = await resolveSessionTrashEntryExactMessageCount(entry);
|
|
6423
|
+
if (!hydratedEntry) {
|
|
6424
|
+
return { error: '回收站记录不存在' };
|
|
5124
6425
|
}
|
|
5125
6426
|
|
|
5126
|
-
const
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|| runtime.settings.authSource !== nextSettings.authSource
|
|
5131
|
-
|| runtime.settings.timeoutMs !== nextSettings.timeoutMs
|
|
5132
|
-
|| runtime.upstream.providerName !== upstreamResult.providerName
|
|
5133
|
-
|| runtime.upstream.baseUrl !== upstreamResult.baseUrl
|
|
5134
|
-
|| runtime.upstream.authHeader !== upstreamResult.authHeader
|
|
5135
|
-
);
|
|
6427
|
+
const trashFilePath = resolveSessionTrashFilePath(hydratedEntry);
|
|
6428
|
+
if (!trashFilePath || !fs.existsSync(trashFilePath)) {
|
|
6429
|
+
return { error: '回收站文件不存在' };
|
|
6430
|
+
}
|
|
5136
6431
|
|
|
5137
|
-
|
|
5138
|
-
|
|
6432
|
+
const targetFilePath = resolveSessionRestoreTarget(hydratedEntry);
|
|
6433
|
+
if (!targetFilePath) {
|
|
6434
|
+
return { error: '原始会话路径非法,无法恢复' };
|
|
6435
|
+
}
|
|
6436
|
+
if (fs.existsSync(targetFilePath)) {
|
|
6437
|
+
return { error: '原始会话路径已存在同名文件,请先手动处理冲突' };
|
|
5139
6438
|
}
|
|
5140
6439
|
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
6440
|
+
let claudeIndexPath = '';
|
|
6441
|
+
try {
|
|
6442
|
+
const latestEntries = readSessionTrashEntries({ cleanup: false });
|
|
6443
|
+
const latestEntry = latestEntries.find((item) => item && item.trashId === trashId);
|
|
6444
|
+
if (!latestEntry) {
|
|
6445
|
+
return { error: '回收站记录不存在' };
|
|
6446
|
+
}
|
|
6447
|
+
const remainingEntries = latestEntries.filter((item) => item.trashId !== trashId);
|
|
6448
|
+
moveFileSync(trashFilePath, targetFilePath);
|
|
6449
|
+
if (hydratedEntry.source === 'claude') {
|
|
6450
|
+
claudeIndexPath = resolveClaudeSessionRestoreIndexPath(hydratedEntry, targetFilePath);
|
|
6451
|
+
upsertClaudeSessionIndexEntry(claudeIndexPath, targetFilePath, hydratedEntry);
|
|
6452
|
+
}
|
|
6453
|
+
writeSessionTrashEntries(remainingEntries);
|
|
6454
|
+
} catch (e) {
|
|
6455
|
+
let rollbackSucceeded = false;
|
|
6456
|
+
if (fs.existsSync(targetFilePath) && !fs.existsSync(trashFilePath)) {
|
|
6457
|
+
try {
|
|
6458
|
+
moveFileSync(targetFilePath, trashFilePath);
|
|
6459
|
+
rollbackSucceeded = true;
|
|
6460
|
+
} catch (_) {}
|
|
5160
6461
|
}
|
|
5161
|
-
if (
|
|
5162
|
-
|
|
6462
|
+
if (rollbackSucceeded && entry.source === 'claude' && claudeIndexPath && fs.existsSync(claudeIndexPath)) {
|
|
6463
|
+
try {
|
|
6464
|
+
removeClaudeSessionIndexEntry(claudeIndexPath, targetFilePath, entry.sessionId);
|
|
6465
|
+
} catch (_) {}
|
|
5163
6466
|
}
|
|
6467
|
+
return { error: `恢复会话失败: ${e.message}` };
|
|
5164
6468
|
}
|
|
5165
6469
|
|
|
5166
|
-
|
|
6470
|
+
invalidateSessionListCache();
|
|
6471
|
+
|
|
6472
|
+
return {
|
|
5167
6473
|
success: true,
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
6474
|
+
restored: true,
|
|
6475
|
+
trashId,
|
|
6476
|
+
source: entry.source,
|
|
6477
|
+
sessionId: entry.sessionId,
|
|
6478
|
+
filePath: targetFilePath
|
|
5172
6479
|
};
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
6480
|
+
}
|
|
6481
|
+
|
|
6482
|
+
async function purgeSessionTrashItems(params = {}) {
|
|
6483
|
+
const entries = readSessionTrashEntries();
|
|
6484
|
+
if (entries.length === 0) {
|
|
6485
|
+
return { success: true, purged: [], count: 0 };
|
|
6486
|
+
}
|
|
6487
|
+
|
|
6488
|
+
const all = params.all === true;
|
|
6489
|
+
const trashIds = Array.isArray(params.trashIds)
|
|
6490
|
+
? params.trashIds
|
|
6491
|
+
.map((item) => (typeof item === 'string' ? item.trim() : ''))
|
|
6492
|
+
.filter(Boolean)
|
|
6493
|
+
: [];
|
|
6494
|
+
const singleTrashId = typeof params.trashId === 'string' ? params.trashId.trim() : '';
|
|
6495
|
+
const targetIds = all
|
|
6496
|
+
? new Set(entries.map((item) => item.trashId))
|
|
6497
|
+
: new Set(singleTrashId ? [singleTrashId, ...trashIds] : trashIds);
|
|
6498
|
+
|
|
6499
|
+
if (targetIds.size === 0) {
|
|
6500
|
+
return { error: '请先选择要彻底删除的回收站记录' };
|
|
6501
|
+
}
|
|
6502
|
+
|
|
6503
|
+
const purged = [];
|
|
6504
|
+
const remaining = [];
|
|
6505
|
+
let purgeError = null;
|
|
6506
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
6507
|
+
const entry = entries[index];
|
|
6508
|
+
if (!targetIds.has(entry.trashId)) {
|
|
6509
|
+
remaining.push(entry);
|
|
6510
|
+
continue;
|
|
5177
6511
|
}
|
|
6512
|
+
const trashFilePath = resolveSessionTrashFilePath(entry);
|
|
6513
|
+
if (trashFilePath && fs.existsSync(trashFilePath)) {
|
|
6514
|
+
try {
|
|
6515
|
+
fs.unlinkSync(trashFilePath);
|
|
6516
|
+
} catch (e) {
|
|
6517
|
+
if (!purgeError) purgeError = e;
|
|
6518
|
+
remaining.push(entry);
|
|
6519
|
+
continue;
|
|
6520
|
+
}
|
|
6521
|
+
}
|
|
6522
|
+
purged.push({
|
|
6523
|
+
trashId: entry.trashId,
|
|
6524
|
+
source: entry.source,
|
|
6525
|
+
sessionId: entry.sessionId
|
|
6526
|
+
});
|
|
6527
|
+
}
|
|
6528
|
+
|
|
6529
|
+
try {
|
|
6530
|
+
writeSessionTrashEntries(remaining);
|
|
6531
|
+
} catch (e) {
|
|
6532
|
+
return { error: `回收站索引更新失败: ${e.message}` };
|
|
6533
|
+
}
|
|
6534
|
+
|
|
6535
|
+
if (purgeError) {
|
|
6536
|
+
return { error: `彻底删除失败: ${purgeError.message}` };
|
|
5178
6537
|
}
|
|
5179
6538
|
|
|
5180
|
-
const status = getBuiltinProxyStatus();
|
|
5181
6539
|
return {
|
|
5182
6540
|
success: true,
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
switched: applyRes.switched,
|
|
5186
|
-
model: applyRes.model || '',
|
|
5187
|
-
settings: status.settings,
|
|
5188
|
-
runtime: status.runtime
|
|
6541
|
+
purged,
|
|
6542
|
+
count: purged.length
|
|
5189
6543
|
};
|
|
5190
6544
|
}
|
|
5191
6545
|
|
|
5192
|
-
function
|
|
5193
|
-
|
|
5194
|
-
|
|
6546
|
+
async function trashSessionData(params = {}) {
|
|
6547
|
+
const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
|
|
6548
|
+
if (!source) {
|
|
6549
|
+
return { error: 'Invalid source' };
|
|
5195
6550
|
}
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
6551
|
+
|
|
6552
|
+
const filePath = resolveSessionFilePath(source, getSessionFileArg(params), params.sessionId);
|
|
6553
|
+
if (!filePath) {
|
|
6554
|
+
return { error: 'Session file not found' };
|
|
5199
6555
|
}
|
|
5200
|
-
|
|
5201
|
-
const
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
6556
|
+
|
|
6557
|
+
const summary = (source === 'claude' ? parseClaudeSessionSummary(filePath) : parseCodexSessionSummary(filePath))
|
|
6558
|
+
|| buildSessionSummaryFallback(source, filePath, params.sessionId);
|
|
6559
|
+
const exactMessageCount = await countConversationMessagesInFile(filePath, source);
|
|
6560
|
+
if (Number.isFinite(Number(exactMessageCount))) {
|
|
6561
|
+
summary.messageCount = Math.max(0, Math.floor(Number(exactMessageCount)));
|
|
6562
|
+
}
|
|
6563
|
+
const sessionId = summary.sessionId || params.sessionId || path.basename(filePath, '.jsonl');
|
|
6564
|
+
const { trashId, trashFileName, trashFilePath } = allocateSessionTrashTarget();
|
|
6565
|
+
const deletedAt = new Date().toISOString();
|
|
6566
|
+
const claudeIndexPath = source === 'claude' ? findClaudeSessionIndexPath(filePath) : '';
|
|
6567
|
+
let removedClaudeIndexEntry = null;
|
|
6568
|
+
|
|
6569
|
+
try {
|
|
6570
|
+
moveFileSync(filePath, trashFilePath);
|
|
6571
|
+
} catch (e) {
|
|
6572
|
+
return { error: `移入回收站失败: ${e.message}` };
|
|
6573
|
+
}
|
|
6574
|
+
|
|
6575
|
+
try {
|
|
6576
|
+
if (source === 'claude' && claudeIndexPath) {
|
|
6577
|
+
const removal = removeClaudeSessionIndexEntry(claudeIndexPath, filePath, sessionId);
|
|
6578
|
+
removedClaudeIndexEntry = removal && removal.entry ? removal.entry : null;
|
|
6579
|
+
}
|
|
6580
|
+
const entry = buildSessionTrashEntry(summary, {
|
|
6581
|
+
trashId,
|
|
6582
|
+
trashFileName,
|
|
6583
|
+
trashFilePath,
|
|
6584
|
+
source,
|
|
6585
|
+
sessionId,
|
|
6586
|
+
deletedAt,
|
|
6587
|
+
originalFilePath: filePath,
|
|
6588
|
+
claudeIndexPath,
|
|
6589
|
+
claudeIndexEntry: removedClaudeIndexEntry
|
|
6590
|
+
});
|
|
6591
|
+
const entries = readSessionTrashEntries({ cleanup: false });
|
|
6592
|
+
const totalCount = entries.length + 1;
|
|
6593
|
+
const nextEntries = [entry, ...entries].slice(0, MAX_SESSION_TRASH_LIST_SIZE);
|
|
6594
|
+
writeSessionTrashEntries(nextEntries);
|
|
6595
|
+
summary.totalCount = Math.min(totalCount, MAX_SESSION_TRASH_LIST_SIZE);
|
|
6596
|
+
} catch (e) {
|
|
6597
|
+
let rollbackSucceeded = false;
|
|
6598
|
+
if (fs.existsSync(trashFilePath) && !fs.existsSync(filePath)) {
|
|
6599
|
+
try {
|
|
6600
|
+
moveFileSync(trashFilePath, filePath);
|
|
6601
|
+
rollbackSucceeded = true;
|
|
6602
|
+
} catch (_) {}
|
|
5205
6603
|
}
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
6604
|
+
if (rollbackSucceeded && source === 'claude' && claudeIndexPath && removedClaudeIndexEntry) {
|
|
6605
|
+
try {
|
|
6606
|
+
upsertClaudeSessionIndexEntry(claudeIndexPath, filePath, {
|
|
6607
|
+
source,
|
|
6608
|
+
sessionId,
|
|
6609
|
+
title: summary.title,
|
|
6610
|
+
messageCount: summary.messageCount,
|
|
6611
|
+
capabilities: summary.capabilities,
|
|
6612
|
+
keywords: summary.keywords,
|
|
6613
|
+
updatedAt: summary.updatedAt,
|
|
6614
|
+
createdAt: summary.createdAt,
|
|
6615
|
+
claudeIndexEntry: removedClaudeIndexEntry,
|
|
6616
|
+
originalFilePath: filePath,
|
|
6617
|
+
trashId,
|
|
6618
|
+
trashFileName
|
|
6619
|
+
});
|
|
6620
|
+
} catch (_) {}
|
|
5209
6621
|
}
|
|
5210
|
-
if (
|
|
5211
|
-
|
|
5212
|
-
const entryPath = expanded ? path.resolve(expanded) : '';
|
|
5213
|
-
if (entryPath && resolvedLower && entryPath.toLowerCase() === resolvedLower) {
|
|
5214
|
-
return false;
|
|
5215
|
-
}
|
|
6622
|
+
if (!rollbackSucceeded && fs.existsSync(trashFilePath)) {
|
|
6623
|
+
try { fs.unlinkSync(trashFilePath); } catch (_) {}
|
|
5216
6624
|
}
|
|
5217
|
-
return
|
|
5218
|
-
});
|
|
5219
|
-
if (filtered.length === index.entries.length) {
|
|
5220
|
-
return;
|
|
6625
|
+
return { error: `移入回收站失败: ${e.message}` };
|
|
5221
6626
|
}
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
6627
|
+
|
|
6628
|
+
invalidateSessionListCache();
|
|
6629
|
+
|
|
6630
|
+
return {
|
|
6631
|
+
success: true,
|
|
6632
|
+
source,
|
|
6633
|
+
sessionId,
|
|
6634
|
+
filePath,
|
|
6635
|
+
trashed: true,
|
|
6636
|
+
trashId,
|
|
6637
|
+
deletedAt,
|
|
6638
|
+
totalCount: Number.isFinite(Number(summary && summary.totalCount))
|
|
6639
|
+
? Math.max(0, Math.floor(Number(summary.totalCount)))
|
|
6640
|
+
: undefined,
|
|
6641
|
+
messageCount: Number.isFinite(Number(summary && summary.messageCount))
|
|
6642
|
+
? Math.max(0, Math.floor(Number(summary.messageCount)))
|
|
6643
|
+
: 0
|
|
6644
|
+
};
|
|
5226
6645
|
}
|
|
5227
6646
|
|
|
5228
6647
|
async function deleteSessionData(params = {}) {
|
|
@@ -5231,14 +6650,16 @@ async function deleteSessionData(params = {}) {
|
|
|
5231
6650
|
return { error: 'Invalid source' };
|
|
5232
6651
|
}
|
|
5233
6652
|
|
|
5234
|
-
const filePath = resolveSessionFilePath(source, params
|
|
6653
|
+
const filePath = resolveSessionFilePath(source, getSessionFileArg(params), params.sessionId);
|
|
5235
6654
|
if (!filePath) {
|
|
5236
6655
|
return { error: 'Session file not found' };
|
|
5237
6656
|
}
|
|
5238
6657
|
|
|
5239
6658
|
const sessionId = params.sessionId || path.basename(filePath, '.jsonl');
|
|
6659
|
+
let fileDeleted = false;
|
|
5240
6660
|
try {
|
|
5241
6661
|
fs.unlinkSync(filePath);
|
|
6662
|
+
fileDeleted = true;
|
|
5242
6663
|
} catch (e) {
|
|
5243
6664
|
return { error: `删除会话失败: ${e.message}` };
|
|
5244
6665
|
}
|
|
@@ -5246,7 +6667,14 @@ async function deleteSessionData(params = {}) {
|
|
|
5246
6667
|
if (source === 'claude') {
|
|
5247
6668
|
const indexPath = findClaudeSessionIndexPath(filePath);
|
|
5248
6669
|
if (indexPath) {
|
|
5249
|
-
|
|
6670
|
+
try {
|
|
6671
|
+
removeClaudeSessionIndexEntry(indexPath, filePath, sessionId);
|
|
6672
|
+
} catch (e) {
|
|
6673
|
+
console.warn('删除会话索引失败:', e && e.message ? e.message : e);
|
|
6674
|
+
if (!fileDeleted) {
|
|
6675
|
+
return { error: `删除会话失败: ${e.message || e}` };
|
|
6676
|
+
}
|
|
6677
|
+
}
|
|
5250
6678
|
}
|
|
5251
6679
|
}
|
|
5252
6680
|
|
|
@@ -5256,7 +6684,8 @@ async function deleteSessionData(params = {}) {
|
|
|
5256
6684
|
success: true,
|
|
5257
6685
|
source,
|
|
5258
6686
|
sessionId,
|
|
5259
|
-
filePath
|
|
6687
|
+
filePath,
|
|
6688
|
+
deleted: true
|
|
5260
6689
|
};
|
|
5261
6690
|
}
|
|
5262
6691
|
|
|
@@ -5311,7 +6740,7 @@ async function cloneCodexSession(params = {}) {
|
|
|
5311
6740
|
return { error: '仅支持 Codex 会话克隆' };
|
|
5312
6741
|
}
|
|
5313
6742
|
|
|
5314
|
-
const filePath = resolveSessionFilePath(source, params
|
|
6743
|
+
const filePath = resolveSessionFilePath(source, getSessionFileArg(params), params.sessionId);
|
|
5315
6744
|
if (!filePath) {
|
|
5316
6745
|
return { error: 'Session file not found' };
|
|
5317
6746
|
}
|
|
@@ -5690,26 +7119,26 @@ async function readSessionDetail(params = {}) {
|
|
|
5690
7119
|
return { error: 'Invalid source' };
|
|
5691
7120
|
}
|
|
5692
7121
|
|
|
5693
|
-
const filePath = resolveSessionFilePath(source, params
|
|
7122
|
+
const filePath = resolveSessionFilePath(source, getSessionFileArg(params), params.sessionId);
|
|
5694
7123
|
if (!filePath) {
|
|
5695
7124
|
return { error: 'Session file not found' };
|
|
5696
7125
|
}
|
|
5697
7126
|
|
|
5698
|
-
const
|
|
7127
|
+
const rawMaxMessages = Number(params.maxMessages);
|
|
7128
|
+
const rawLimit = Number.isFinite(rawMaxMessages) ? rawMaxMessages : Number(params.messageLimit);
|
|
5699
7129
|
const messageLimit = Number.isFinite(rawLimit)
|
|
5700
7130
|
? Math.max(1, Math.min(rawLimit, MAX_SESSION_DETAIL_MESSAGES))
|
|
5701
7131
|
: DEFAULT_SESSION_DETAIL_MESSAGES;
|
|
5702
7132
|
|
|
5703
|
-
const extracted = await
|
|
7133
|
+
const extracted = await extractSessionDetailPreviewFromFile(filePath, source, messageLimit);
|
|
5704
7134
|
const sessionId = extracted.sessionId || params.sessionId || path.basename(filePath, '.jsonl');
|
|
5705
7135
|
const sourceLabel = source === 'codex' ? 'Codex' : 'Claude Code';
|
|
5706
|
-
const
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
const clippedMessages = allMessages.slice(startIndex);
|
|
7136
|
+
const clippedMessages = Array.isArray(extracted.messages) ? extracted.messages : [];
|
|
7137
|
+
const startIndex = Math.max(0, extracted.totalMessages - clippedMessages.length);
|
|
7138
|
+
const indexedMessages = clippedMessages.map((message, messageIndex) => ({
|
|
7139
|
+
...message,
|
|
7140
|
+
messageIndex: startIndex + messageIndex
|
|
7141
|
+
}));
|
|
5713
7142
|
|
|
5714
7143
|
return {
|
|
5715
7144
|
source,
|
|
@@ -5717,10 +7146,10 @@ async function readSessionDetail(params = {}) {
|
|
|
5717
7146
|
sessionId,
|
|
5718
7147
|
cwd: extracted.cwd || '',
|
|
5719
7148
|
updatedAt: extracted.updatedAt || '',
|
|
5720
|
-
totalMessages:
|
|
5721
|
-
clipped:
|
|
7149
|
+
totalMessages: extracted.totalMessages,
|
|
7150
|
+
clipped: extracted.totalMessages > indexedMessages.length,
|
|
5722
7151
|
messageLimit,
|
|
5723
|
-
messages:
|
|
7152
|
+
messages: indexedMessages,
|
|
5724
7153
|
filePath
|
|
5725
7154
|
};
|
|
5726
7155
|
}
|
|
@@ -5731,7 +7160,7 @@ async function readSessionPlain(params = {}) {
|
|
|
5731
7160
|
return { error: 'Invalid source' };
|
|
5732
7161
|
}
|
|
5733
7162
|
|
|
5734
|
-
const filePath = resolveSessionFilePath(source, params
|
|
7163
|
+
const filePath = resolveSessionFilePath(source, getSessionFileArg(params), params.sessionId);
|
|
5735
7164
|
if (!filePath) {
|
|
5736
7165
|
return { error: 'Session file not found' };
|
|
5737
7166
|
}
|
|
@@ -5777,7 +7206,7 @@ async function exportSessionData(params = {}) {
|
|
|
5777
7206
|
}
|
|
5778
7207
|
|
|
5779
7208
|
const maxMessages = resolveMaxMessagesValue(params.maxMessages, MAX_EXPORT_MESSAGES);
|
|
5780
|
-
const filePath = resolveSessionFilePath(source, params
|
|
7209
|
+
const filePath = resolveSessionFilePath(source, getSessionFileArg(params), params.sessionId);
|
|
5781
7210
|
if (!filePath) {
|
|
5782
7211
|
return { error: 'Session file not found' };
|
|
5783
7212
|
}
|
|
@@ -6163,7 +7592,8 @@ async function cmdSetup() {
|
|
|
6163
7592
|
const { config } = readConfigOrVirtualDefault();
|
|
6164
7593
|
const providers = config.model_providers || {};
|
|
6165
7594
|
const providerNames = Object.keys(providers);
|
|
6166
|
-
const
|
|
7595
|
+
const currentProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
|
|
7596
|
+
const defaultProvider = currentProvider || providerNames[0] || '';
|
|
6167
7597
|
let availableModels = [];
|
|
6168
7598
|
let defaultModel = config.model || '';
|
|
6169
7599
|
let modelFetchUnlimited = false;
|
|
@@ -6449,7 +7879,7 @@ async function cmdModels() {
|
|
|
6449
7879
|
|
|
6450
7880
|
// 切换提供商
|
|
6451
7881
|
function cmdSwitch(providerName, silent = false) {
|
|
6452
|
-
const config = readConfig();
|
|
7882
|
+
const config = sanitizeRemovedBuiltinProxyProvider(readConfig());
|
|
6453
7883
|
const providers = config.model_providers || {};
|
|
6454
7884
|
|
|
6455
7885
|
if (!providers[providerName]) {
|
|
@@ -6552,6 +7982,10 @@ function cmdAdd(name, baseUrl, apiKey, silent = false) {
|
|
|
6552
7982
|
if (!silent) console.error('错误: local provider 为系统保留名称,不可新增');
|
|
6553
7983
|
throw new Error('local provider 为系统保留名称,不可新增');
|
|
6554
7984
|
}
|
|
7985
|
+
if (isBuiltinProxyProvider(providerName)) {
|
|
7986
|
+
if (!silent) console.error('错误: codexmate-proxy 为保留名称,不可手动添加');
|
|
7987
|
+
throw new Error('codexmate-proxy 为保留名称,不可手动添加');
|
|
7988
|
+
}
|
|
6555
7989
|
|
|
6556
7990
|
const config = readConfig();
|
|
6557
7991
|
if (config.model_providers && config.model_providers[providerName]) {
|
|
@@ -6616,7 +8050,7 @@ function cmdUpdate(name, baseUrl, apiKey, silent = false, options = {}) {
|
|
|
6616
8050
|
if (isNonEditableProvider(name) && !allowManaged) {
|
|
6617
8051
|
const msg = isDefaultLocalProvider(name)
|
|
6618
8052
|
? 'local provider 为系统保留项,不可编辑'
|
|
6619
|
-
: '
|
|
8053
|
+
: 'codexmate-proxy 为保留名称,不可编辑';
|
|
6620
8054
|
if (!silent) console.error(`错误: ${msg}`);
|
|
6621
8055
|
throw new Error(msg);
|
|
6622
8056
|
}
|
|
@@ -8448,22 +9882,55 @@ function resolveUploadFileNameFromRequest(req, fallbackName = 'codex-skills.zip'
|
|
|
8448
9882
|
return normalized || fallback;
|
|
8449
9883
|
}
|
|
8450
9884
|
|
|
8451
|
-
|
|
9885
|
+
function resolveSkillTargetAppFromRequest(req, fallbackApp = 'codex') {
|
|
9886
|
+
const fallbackTarget = resolveSkillTarget({}, fallbackApp);
|
|
9887
|
+
const fallback = fallbackTarget ? fallbackTarget.app : 'codex';
|
|
9888
|
+
try {
|
|
9889
|
+
const parsed = new URL(req.url || '/', 'http://localhost');
|
|
9890
|
+
const hasTargetApp = parsed.searchParams.has('targetApp');
|
|
9891
|
+
const hasTarget = parsed.searchParams.has('target');
|
|
9892
|
+
if (hasTargetApp || hasTarget) {
|
|
9893
|
+
const target = resolveSkillTarget({
|
|
9894
|
+
...(hasTargetApp ? { targetApp: parsed.searchParams.get('targetApp') } : {}),
|
|
9895
|
+
...(hasTarget ? { target: parsed.searchParams.get('target') } : {})
|
|
9896
|
+
}, fallback);
|
|
9897
|
+
return target ? target.app : null;
|
|
9898
|
+
}
|
|
9899
|
+
return fallback;
|
|
9900
|
+
} catch (_) {
|
|
9901
|
+
return fallback;
|
|
9902
|
+
}
|
|
9903
|
+
}
|
|
9904
|
+
|
|
9905
|
+
async function handleImportSkillsZipUpload(req, res, options = {}) {
|
|
8452
9906
|
if (req.method !== 'POST') {
|
|
9907
|
+
if (req && typeof req.resume === 'function') {
|
|
9908
|
+
req.resume();
|
|
9909
|
+
}
|
|
8453
9910
|
writeJsonResponse(res, 405, { error: 'Method Not Allowed' });
|
|
8454
9911
|
return;
|
|
8455
9912
|
}
|
|
8456
9913
|
try {
|
|
8457
|
-
const
|
|
9914
|
+
const forcedTargetApp = normalizeSkillTargetApp(options && options.targetApp ? options.targetApp : '');
|
|
9915
|
+
const targetApp = forcedTargetApp || resolveSkillTargetAppFromRequest(req, 'codex');
|
|
9916
|
+
if (!targetApp) {
|
|
9917
|
+
if (req && typeof req.resume === 'function') {
|
|
9918
|
+
req.resume();
|
|
9919
|
+
}
|
|
9920
|
+
writeJsonResponse(res, 400, { error: '目标宿主不支持' });
|
|
9921
|
+
return;
|
|
9922
|
+
}
|
|
9923
|
+
const fileName = resolveUploadFileNameFromRequest(req, `${targetApp}-skills.zip`);
|
|
8458
9924
|
const upload = await writeUploadZipStream(
|
|
8459
9925
|
req,
|
|
8460
9926
|
'codex-skills-import',
|
|
8461
9927
|
fileName,
|
|
8462
9928
|
MAX_SKILLS_ZIP_UPLOAD_SIZE
|
|
8463
9929
|
);
|
|
8464
|
-
const result = await
|
|
9930
|
+
const result = await importSkillsFromZipFile(upload.zipPath, {
|
|
8465
9931
|
tempDir: upload.tempDir,
|
|
8466
|
-
fallbackName: fileName
|
|
9932
|
+
fallbackName: fileName,
|
|
9933
|
+
targetApp
|
|
8467
9934
|
});
|
|
8468
9935
|
writeJsonResponse(res, 200, result || {});
|
|
8469
9936
|
} catch (e) {
|
|
@@ -8477,14 +9944,33 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8477
9944
|
|
|
8478
9945
|
const server = http.createServer((req, res) => {
|
|
8479
9946
|
const requestPath = (req.url || '/').split('?')[0];
|
|
9947
|
+
if (requestPath === '/api/import-skills-zip') {
|
|
9948
|
+
void handleImportSkillsZipUpload(req, res);
|
|
9949
|
+
return;
|
|
9950
|
+
}
|
|
8480
9951
|
if (requestPath === '/api/import-codex-skills-zip') {
|
|
8481
|
-
void
|
|
9952
|
+
void handleImportSkillsZipUpload(req, res, { targetApp: 'codex' });
|
|
8482
9953
|
return;
|
|
8483
9954
|
}
|
|
8484
9955
|
if (requestPath === '/api') {
|
|
8485
9956
|
let body = '';
|
|
8486
|
-
|
|
9957
|
+
let bodySize = 0;
|
|
9958
|
+
let bodyTooLarge = false;
|
|
9959
|
+
req.on('data', chunk => {
|
|
9960
|
+
if (bodyTooLarge) return;
|
|
9961
|
+
bodySize += chunk.length;
|
|
9962
|
+
if (bodySize > MAX_API_BODY_SIZE) {
|
|
9963
|
+
bodyTooLarge = true;
|
|
9964
|
+
writeJsonResponse(res, 413, {
|
|
9965
|
+
error: `请求体过大(>${Math.floor(MAX_API_BODY_SIZE / 1024 / 1024)}MB)`
|
|
9966
|
+
});
|
|
9967
|
+
req.destroy();
|
|
9968
|
+
return;
|
|
9969
|
+
}
|
|
9970
|
+
body += chunk;
|
|
9971
|
+
});
|
|
8487
9972
|
req.on('end', async () => {
|
|
9973
|
+
if (bodyTooLarge) return;
|
|
8488
9974
|
try {
|
|
8489
9975
|
const { action, params } = JSON.parse(body);
|
|
8490
9976
|
let result;
|
|
@@ -8592,6 +10078,24 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8592
10078
|
case 'apply-agents-file':
|
|
8593
10079
|
result = applyAgentsFile(params || {});
|
|
8594
10080
|
break;
|
|
10081
|
+
case 'preview-agents-diff':
|
|
10082
|
+
result = buildAgentsDiff(params || {});
|
|
10083
|
+
break;
|
|
10084
|
+
case 'list-skills':
|
|
10085
|
+
result = listSkills(params || {});
|
|
10086
|
+
break;
|
|
10087
|
+
case 'delete-skills':
|
|
10088
|
+
result = deleteSkills(params || {});
|
|
10089
|
+
break;
|
|
10090
|
+
case 'scan-unmanaged-skills':
|
|
10091
|
+
result = scanUnmanagedSkills(params || {});
|
|
10092
|
+
break;
|
|
10093
|
+
case 'import-skills':
|
|
10094
|
+
result = importSkills(params || {});
|
|
10095
|
+
break;
|
|
10096
|
+
case 'export-skills':
|
|
10097
|
+
result = await exportSkills(params || {});
|
|
10098
|
+
break;
|
|
8595
10099
|
case 'list-codex-skills':
|
|
8596
10100
|
result = listCodexSkills();
|
|
8597
10101
|
break;
|
|
@@ -8679,7 +10183,7 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8679
10183
|
result = { error: 'Invalid source. Must be codex, claude, or all' };
|
|
8680
10184
|
} else {
|
|
8681
10185
|
result = {
|
|
8682
|
-
sessions:
|
|
10186
|
+
sessions: await listAllSessionsData(params),
|
|
8683
10187
|
source: source || 'all'
|
|
8684
10188
|
};
|
|
8685
10189
|
}
|
|
@@ -8697,6 +10201,18 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8697
10201
|
}
|
|
8698
10202
|
}
|
|
8699
10203
|
break;
|
|
10204
|
+
case 'list-session-trash':
|
|
10205
|
+
result = await listSessionTrashItems(params || {});
|
|
10206
|
+
break;
|
|
10207
|
+
case 'restore-session-trash':
|
|
10208
|
+
result = await restoreSessionTrashItem(params || {});
|
|
10209
|
+
break;
|
|
10210
|
+
case 'purge-session-trash':
|
|
10211
|
+
result = await purgeSessionTrashItems(params || {});
|
|
10212
|
+
break;
|
|
10213
|
+
case 'trash-session':
|
|
10214
|
+
result = await trashSessionData(params || {});
|
|
10215
|
+
break;
|
|
8700
10216
|
case 'export-session':
|
|
8701
10217
|
result = await exportSessionData(params);
|
|
8702
10218
|
break;
|
|
@@ -8939,7 +10455,9 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8939
10455
|
process.exit(1);
|
|
8940
10456
|
});
|
|
8941
10457
|
|
|
8942
|
-
const openHost =
|
|
10458
|
+
const openHost = host === '::'
|
|
10459
|
+
? '::1'
|
|
10460
|
+
: (host === '0.0.0.0' ? DEFAULT_WEB_OPEN_HOST : host);
|
|
8943
10461
|
const openUrl = `http://${formatHostForUrl(openHost)}:${port}`;
|
|
8944
10462
|
server.listen(port, host, () => {
|
|
8945
10463
|
console.log('\n✓ Web UI 已启动:', openUrl);
|
|
@@ -8995,6 +10513,69 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8995
10513
|
return { server, stop };
|
|
8996
10514
|
}
|
|
8997
10515
|
|
|
10516
|
+
// Region markers are used by unit tests that extract these helpers directly.
|
|
10517
|
+
// #region createSerializedWebUiRestartHandler
|
|
10518
|
+
function createSerializedWebUiRestartHandler(runRestart) {
|
|
10519
|
+
let restartQueued = false;
|
|
10520
|
+
let latestRestartInfo = null;
|
|
10521
|
+
let restartInFlight = null;
|
|
10522
|
+
|
|
10523
|
+
const drainRestartQueue = async () => {
|
|
10524
|
+
try {
|
|
10525
|
+
while (restartQueued) {
|
|
10526
|
+
restartQueued = false;
|
|
10527
|
+
await runRestart(latestRestartInfo);
|
|
10528
|
+
}
|
|
10529
|
+
} finally {
|
|
10530
|
+
restartInFlight = null;
|
|
10531
|
+
if (restartQueued) {
|
|
10532
|
+
restartInFlight = drainRestartQueue();
|
|
10533
|
+
return restartInFlight;
|
|
10534
|
+
}
|
|
10535
|
+
}
|
|
10536
|
+
};
|
|
10537
|
+
|
|
10538
|
+
return (info) => {
|
|
10539
|
+
latestRestartInfo = info;
|
|
10540
|
+
restartQueued = true;
|
|
10541
|
+
if (!restartInFlight) {
|
|
10542
|
+
restartInFlight = drainRestartQueue();
|
|
10543
|
+
}
|
|
10544
|
+
return restartInFlight;
|
|
10545
|
+
};
|
|
10546
|
+
}
|
|
10547
|
+
// #endregion createSerializedWebUiRestartHandler
|
|
10548
|
+
|
|
10549
|
+
// #region restartWebUiServerAfterFrontendChange
|
|
10550
|
+
async function restartWebUiServerAfterFrontendChange({
|
|
10551
|
+
serverHandle,
|
|
10552
|
+
serverOptions,
|
|
10553
|
+
createServer = createWebServer,
|
|
10554
|
+
delayMs = 3000,
|
|
10555
|
+
wait = setTimeout,
|
|
10556
|
+
logger = console
|
|
10557
|
+
}) {
|
|
10558
|
+
logger.log(' 正在停止旧服务...');
|
|
10559
|
+
try {
|
|
10560
|
+
await serverHandle.stop();
|
|
10561
|
+
logger.log(' 旧服务已停止');
|
|
10562
|
+
} catch (e) {
|
|
10563
|
+
logger.warn('! 停止旧服务失败:', e.message || e);
|
|
10564
|
+
}
|
|
10565
|
+
|
|
10566
|
+
await new Promise((resolve) => wait(resolve, delayMs));
|
|
10567
|
+
|
|
10568
|
+
try {
|
|
10569
|
+
const nextServerHandle = await createServer(serverOptions);
|
|
10570
|
+
logger.log('✓ 已重启 Web UI 服务\n');
|
|
10571
|
+
return nextServerHandle;
|
|
10572
|
+
} catch (e) {
|
|
10573
|
+
logger.error('! 重启失败:', e.message || e);
|
|
10574
|
+
return serverHandle;
|
|
10575
|
+
}
|
|
10576
|
+
}
|
|
10577
|
+
// #endregion restartWebUiServerAfterFrontendChange
|
|
10578
|
+
|
|
8998
10579
|
// 打开 Web UI
|
|
8999
10580
|
function cmdStart(options = {}) {
|
|
9000
10581
|
const webDir = path.join(__dirname, 'web-ui');
|
|
@@ -9019,51 +10600,28 @@ function cmdStart(options = {}) {
|
|
|
9019
10600
|
openBrowser: !options.noBrowser
|
|
9020
10601
|
});
|
|
9021
10602
|
|
|
9022
|
-
const
|
|
9023
|
-
const shouldAutoStartProxy = proxySettings.enabled || hasCodexConfigReadyForProxy();
|
|
9024
|
-
if (shouldAutoStartProxy) {
|
|
9025
|
-
ensureBuiltinProxyForCodexDefault({
|
|
9026
|
-
...proxySettings,
|
|
9027
|
-
switchToProxy: false
|
|
9028
|
-
}).then((res) => {
|
|
9029
|
-
if (res && res.success && res.runtime && res.runtime.listenUrl) {
|
|
9030
|
-
const entryProvider = res.runtime.provider || DEFAULT_LOCAL_PROVIDER_NAME;
|
|
9031
|
-
const upstreamLabel = res.runtime.upstreamProvider ? `(上游: ${res.runtime.upstreamProvider})` : '';
|
|
9032
|
-
console.log(`~ 内建代理已启动(${entryProvider}): ${res.runtime.listenUrl}${upstreamLabel}`);
|
|
9033
|
-
} else if (res && res.error) {
|
|
9034
|
-
console.warn(`! 内建代理启动失败: ${res.error}`);
|
|
9035
|
-
}
|
|
9036
|
-
}).catch((err) => {
|
|
9037
|
-
console.warn(`! 内建代理启动失败: ${err && err.message ? err.message : err}`);
|
|
9038
|
-
});
|
|
9039
|
-
}
|
|
9040
|
-
|
|
9041
|
-
const stopWatch = watchPathsForRestart(
|
|
9042
|
-
[webDir, legacyHtmlPath],
|
|
9043
|
-
async (info) => {
|
|
10603
|
+
const requestWebUiRestart = createSerializedWebUiRestartHandler(async (info) => {
|
|
9044
10604
|
const fileLabel = info && info.filename ? info.filename : (info && info.target ? path.basename(info.target) : 'unknown');
|
|
9045
10605
|
console.log(`\n~ 侦测到前端变更 (${fileLabel}),重启中...`);
|
|
9046
|
-
|
|
9047
|
-
|
|
9048
|
-
|
|
9049
|
-
console.log(' 旧服务已停止');
|
|
9050
|
-
} catch (e) {
|
|
9051
|
-
console.warn('! 停止旧服务失败:', e.message || e);
|
|
9052
|
-
}
|
|
9053
|
-
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
9054
|
-
try {
|
|
9055
|
-
serverHandle = createWebServer({
|
|
10606
|
+
serverHandle = await restartWebUiServerAfterFrontendChange({
|
|
10607
|
+
serverHandle,
|
|
10608
|
+
serverOptions: {
|
|
9056
10609
|
htmlPath,
|
|
9057
10610
|
assetsDir,
|
|
9058
10611
|
webDir,
|
|
9059
10612
|
host,
|
|
9060
10613
|
port,
|
|
9061
10614
|
openBrowser: false
|
|
9062
|
-
}
|
|
9063
|
-
|
|
9064
|
-
|
|
9065
|
-
|
|
9066
|
-
|
|
10615
|
+
}
|
|
10616
|
+
});
|
|
10617
|
+
});
|
|
10618
|
+
|
|
10619
|
+
const stopWatch = watchPathsForRestart(
|
|
10620
|
+
[webDir, legacyHtmlPath],
|
|
10621
|
+
(info) => {
|
|
10622
|
+
void requestWebUiRestart(info).catch((err) => {
|
|
10623
|
+
console.error('! 重启 Web UI 失败:', err && err.message ? err.message : err);
|
|
10624
|
+
});
|
|
9067
10625
|
}
|
|
9068
10626
|
);
|
|
9069
10627
|
|
|
@@ -9233,111 +10791,8 @@ function parseProxyCliOptions(args = []) {
|
|
|
9233
10791
|
}
|
|
9234
10792
|
|
|
9235
10793
|
async function cmdProxy(args = []) {
|
|
9236
|
-
|
|
9237
|
-
|
|
9238
|
-
if (optionResult.error) {
|
|
9239
|
-
throw new Error(optionResult.error);
|
|
9240
|
-
}
|
|
9241
|
-
const options = optionResult.payload || {};
|
|
9242
|
-
|
|
9243
|
-
if (subcommand === 'status') {
|
|
9244
|
-
const status = getBuiltinProxyStatus();
|
|
9245
|
-
const settings = status.settings || DEFAULT_BUILTIN_PROXY_SETTINGS;
|
|
9246
|
-
console.log('\n内建代理状态:');
|
|
9247
|
-
console.log(' 运行中:', status.running ? '是' : '否');
|
|
9248
|
-
console.log(' 启用:', settings.enabled ? '是' : '否');
|
|
9249
|
-
console.log(' 监听:', buildProxyListenUrl(settings));
|
|
9250
|
-
console.log(' 上游 provider:', settings.provider || '(自动)');
|
|
9251
|
-
console.log(' 鉴权来源:', settings.authSource);
|
|
9252
|
-
if (status.runtime) {
|
|
9253
|
-
console.log(' 实际上游:', status.runtime.upstreamProvider);
|
|
9254
|
-
console.log(' 启动时间:', status.runtime.startedAt);
|
|
9255
|
-
}
|
|
9256
|
-
console.log();
|
|
9257
|
-
return;
|
|
9258
|
-
}
|
|
9259
|
-
|
|
9260
|
-
if (subcommand === 'set' || subcommand === 'config') {
|
|
9261
|
-
const result = saveBuiltinProxySettings(options);
|
|
9262
|
-
if (result.error) {
|
|
9263
|
-
throw new Error(result.error);
|
|
9264
|
-
}
|
|
9265
|
-
const settings = result.settings;
|
|
9266
|
-
console.log('✓ 内建代理配置已保存');
|
|
9267
|
-
console.log(' 监听:', buildProxyListenUrl(settings));
|
|
9268
|
-
console.log(' 上游 provider:', settings.provider || '(自动)');
|
|
9269
|
-
console.log(' 鉴权来源:', settings.authSource);
|
|
9270
|
-
console.log();
|
|
9271
|
-
return;
|
|
9272
|
-
}
|
|
9273
|
-
|
|
9274
|
-
if (subcommand === 'apply' || subcommand === 'apply-provider') {
|
|
9275
|
-
const result = applyBuiltinProxyProvider({
|
|
9276
|
-
switchToProxy: options.switchToProxy !== false
|
|
9277
|
-
});
|
|
9278
|
-
if (result.error) {
|
|
9279
|
-
throw new Error(result.error);
|
|
9280
|
-
}
|
|
9281
|
-
console.log(`✓ 已写入本地代理 provider: ${result.provider}`);
|
|
9282
|
-
console.log(` URL: ${result.baseUrl}`);
|
|
9283
|
-
if (result.switched) {
|
|
9284
|
-
console.log(` 已切换到 ${result.provider}${result.model ? ` / ${result.model}` : ''}`);
|
|
9285
|
-
}
|
|
9286
|
-
console.log();
|
|
9287
|
-
return;
|
|
9288
|
-
}
|
|
9289
|
-
|
|
9290
|
-
if (subcommand === 'enable' || subcommand === 'default-codex') {
|
|
9291
|
-
const result = await ensureBuiltinProxyForCodexDefault(options);
|
|
9292
|
-
if (result.error) {
|
|
9293
|
-
throw new Error(result.error);
|
|
9294
|
-
}
|
|
9295
|
-
const listenUrl = result.runtime && result.runtime.listenUrl
|
|
9296
|
-
? result.runtime.listenUrl
|
|
9297
|
-
: buildProxyListenUrl(result.settings || DEFAULT_BUILTIN_PROXY_SETTINGS);
|
|
9298
|
-
console.log('✓ 已启用 Codex 内建代理默认模式');
|
|
9299
|
-
console.log(` 监听: ${listenUrl}`);
|
|
9300
|
-
if (result.runtime && result.runtime.upstreamProvider) {
|
|
9301
|
-
console.log(` 上游 provider: ${result.runtime.upstreamProvider}`);
|
|
9302
|
-
}
|
|
9303
|
-
console.log(` 当前 provider: ${result.provider}${result.model ? ` / ${result.model}` : ''}`);
|
|
9304
|
-
console.log();
|
|
9305
|
-
return;
|
|
9306
|
-
}
|
|
9307
|
-
|
|
9308
|
-
if (subcommand === 'start') {
|
|
9309
|
-
const result = await startBuiltinProxyRuntime({
|
|
9310
|
-
...options,
|
|
9311
|
-
enabled: true
|
|
9312
|
-
});
|
|
9313
|
-
if (result.error) {
|
|
9314
|
-
throw new Error(result.error);
|
|
9315
|
-
}
|
|
9316
|
-
console.log(`✓ 内建代理已启动: ${result.listenUrl}`);
|
|
9317
|
-
console.log(` 上游 provider: ${result.upstreamProvider}`);
|
|
9318
|
-
console.log(' 按 Ctrl+C 停止代理\n');
|
|
9319
|
-
|
|
9320
|
-
await new Promise((resolve) => {
|
|
9321
|
-
let stopping = false;
|
|
9322
|
-
const gracefulStop = async () => {
|
|
9323
|
-
if (stopping) return;
|
|
9324
|
-
stopping = true;
|
|
9325
|
-
await stopBuiltinProxyRuntime();
|
|
9326
|
-
resolve();
|
|
9327
|
-
};
|
|
9328
|
-
process.once('SIGINT', gracefulStop);
|
|
9329
|
-
process.once('SIGTERM', gracefulStop);
|
|
9330
|
-
});
|
|
9331
|
-
return;
|
|
9332
|
-
}
|
|
9333
|
-
|
|
9334
|
-
if (subcommand === 'stop') {
|
|
9335
|
-
await stopBuiltinProxyRuntime();
|
|
9336
|
-
console.log('✓ 内建代理已停止\n');
|
|
9337
|
-
return;
|
|
9338
|
-
}
|
|
9339
|
-
|
|
9340
|
-
throw new Error(`未知 proxy 子命令: ${subcommand}`);
|
|
10794
|
+
void args;
|
|
10795
|
+
throw new Error('该功能已移除');
|
|
9341
10796
|
}
|
|
9342
10797
|
|
|
9343
10798
|
function parseWorkflowInputArg(rawInput) {
|
|
@@ -10389,7 +11844,7 @@ function createWorkflowToolCatalog() {
|
|
|
10389
11844
|
}
|
|
10390
11845
|
return {
|
|
10391
11846
|
source: source || 'all',
|
|
10392
|
-
sessions:
|
|
11847
|
+
sessions: await listAllSessionsData({
|
|
10393
11848
|
...args,
|
|
10394
11849
|
source: source || 'all'
|
|
10395
11850
|
})
|
|
@@ -10756,7 +12211,7 @@ function createMcpTools(options = {}) {
|
|
|
10756
12211
|
source: source || 'all'
|
|
10757
12212
|
};
|
|
10758
12213
|
return {
|
|
10759
|
-
sessions:
|
|
12214
|
+
sessions: await listAllSessionsData(normalizedInput),
|
|
10760
12215
|
source: source || 'all'
|
|
10761
12216
|
};
|
|
10762
12217
|
}
|
|
@@ -10992,18 +12447,34 @@ function createMcpTools(options = {}) {
|
|
|
10992
12447
|
handler: async (args = {}) => applyOpenclawConfig(args || {})
|
|
10993
12448
|
});
|
|
10994
12449
|
|
|
12450
|
+
pushTool({
|
|
12451
|
+
name: 'codexmate.session.trash',
|
|
12452
|
+
description: 'Move one entire session file into session trash.',
|
|
12453
|
+
readOnly: false,
|
|
12454
|
+
inputSchema: {
|
|
12455
|
+
type: 'object',
|
|
12456
|
+
properties: {
|
|
12457
|
+
source: { type: 'string' },
|
|
12458
|
+
sessionId: { type: 'string' },
|
|
12459
|
+
filePath: { type: 'string' },
|
|
12460
|
+
file: { type: 'string' }
|
|
12461
|
+
},
|
|
12462
|
+
additionalProperties: true
|
|
12463
|
+
},
|
|
12464
|
+
handler: async (args = {}) => trashSessionData(args || {})
|
|
12465
|
+
});
|
|
12466
|
+
|
|
10995
12467
|
pushTool({
|
|
10996
12468
|
name: 'codexmate.session.delete',
|
|
10997
|
-
description: '
|
|
12469
|
+
description: 'Permanently delete one entire session file.',
|
|
10998
12470
|
readOnly: false,
|
|
10999
12471
|
inputSchema: {
|
|
11000
12472
|
type: 'object',
|
|
11001
12473
|
properties: {
|
|
11002
12474
|
source: { type: 'string' },
|
|
11003
12475
|
sessionId: { type: 'string' },
|
|
11004
|
-
|
|
11005
|
-
|
|
11006
|
-
recordLineIndices: { type: 'array', items: { type: 'number' } }
|
|
12476
|
+
filePath: { type: 'string' },
|
|
12477
|
+
file: { type: 'string' }
|
|
11007
12478
|
},
|
|
11008
12479
|
additionalProperties: true
|
|
11009
12480
|
},
|
|
@@ -11153,7 +12624,7 @@ function createMcpResources() {
|
|
|
11153
12624
|
}
|
|
11154
12625
|
const payload = {
|
|
11155
12626
|
source: normalizedSource || 'all',
|
|
11156
|
-
sessions:
|
|
12627
|
+
sessions: await listAllSessionsData({
|
|
11157
12628
|
source: normalizedSource || 'all',
|
|
11158
12629
|
query,
|
|
11159
12630
|
pathFilter,
|
|
@@ -11395,11 +12866,9 @@ async function main() {
|
|
|
11395
12866
|
console.log(' codexmate claude <BaseURL> <API密钥> [模型] 写入 Claude Code 配置');
|
|
11396
12867
|
console.log(' codexmate add-model <模型> 添加模型');
|
|
11397
12868
|
console.log(' codexmate delete-model <模型> 删除模型');
|
|
11398
|
-
console.log(' codexmate auth <list|import|switch|delete|status> 认证文件管理');
|
|
11399
|
-
console.log(' codexmate proxy <status|set|apply|enable|start|stop> 内建代理');
|
|
11400
12869
|
console.log(' codexmate workflow <list|get|validate|run|runs> MCP 工作流中心');
|
|
11401
12870
|
console.log(' codexmate run [--host <HOST>] [--no-browser] 启动 Web 界面');
|
|
11402
|
-
console.log(' codexmate codex [参数...] [--follow-up <文本>|--queued-follow-up <文本> 可重复] 等同于 codex --yolo
|
|
12871
|
+
console.log(' codexmate codex [参数...] [--follow-up <文本>|--queued-follow-up <文本> 可重复] 等同于 codex --yolo');
|
|
11403
12872
|
console.log(' 注: follow-up 自动排队仅支持 linux/android/netbsd/openbsd/darwin/freebsd 且 stdin 必须是 TTY,其他平台会报错');
|
|
11404
12873
|
console.log(' codexmate qwen [参数...] 等同于 qwen --yolo');
|
|
11405
12874
|
console.log(' codexmate mcp [serve] [--transport stdio] [--allow-write|--read-only]');
|