coding-tool-x 3.3.8 → 3.4.0
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/CHANGELOG.md +17 -2
- package/README.md +253 -326
- package/dist/web/assets/{Analytics-DLpoDZ2M.js → Analytics-DEjfL5Jx.js} +4 -4
- package/dist/web/assets/Analytics-RNn1BUbG.css +1 -0
- package/dist/web/assets/{ConfigTemplates-D_hRb55W.js → ConfigTemplates-DkRL_-tf.js} +1 -1
- package/dist/web/assets/Home-BQxQ1LhR.css +1 -0
- package/dist/web/assets/Home-CF-L640I.js +1 -0
- package/dist/web/assets/{PluginManager-JXsyym1s.js → PluginManager-BzNYTdNB.js} +1 -1
- package/dist/web/assets/{ProjectList-DZWSeb-q.js → ProjectList-C0-JgHMM.js} +1 -1
- package/dist/web/assets/{SessionList-Cs624DR3.js → SessionList-CkZUdX5N.js} +1 -1
- package/dist/web/assets/{SkillManager-bEliz7qz.js → SkillManager-Cak0-4d4.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-J3RecFGn.js → WorkspaceManager-CGDJzwEr.js} +1 -1
- package/dist/web/assets/{icons-Cuc23WS7.js → icons-B5Pl4lrD.js} +1 -1
- package/dist/web/assets/index-D_WItvHE.js +2 -0
- package/dist/web/assets/index-Dz7v9OM0.css +1 -0
- package/dist/web/assets/{markdown-C9MYpaSi.js → markdown-DyTJGI4N.js} +1 -1
- package/dist/web/assets/{naive-ui-CxpuzdjU.js → naive-ui-Bdxp09n2.js} +1 -1
- package/dist/web/assets/{vendors-DMjSfzlv.js → vendors-CKPV1OAU.js} +2 -2
- package/dist/web/assets/{vue-vendor-DET08QYg.js → vue-vendor-3bf-fPGP.js} +1 -1
- package/dist/web/index.html +7 -7
- package/docs/home.png +0 -0
- package/package.json +13 -5
- package/src/commands/daemon.js +3 -2
- package/src/commands/security.js +1 -2
- package/src/config/paths.js +638 -93
- package/src/server/api/agents.js +1 -1
- package/src/server/api/claude-hooks.js +13 -8
- package/src/server/api/codex-proxy.js +5 -4
- package/src/server/api/hooks.js +45 -0
- package/src/server/api/plugins.js +0 -1
- package/src/server/api/statistics.js +4 -4
- package/src/server/api/ui-config.js +5 -0
- package/src/server/api/workspaces.js +1 -3
- package/src/server/codex-proxy-server.js +89 -59
- package/src/server/gemini-proxy-server.js +107 -88
- package/src/server/index.js +1 -0
- package/src/server/opencode-proxy-server.js +381 -225
- package/src/server/proxy-server.js +86 -60
- package/src/server/services/alias.js +3 -3
- package/src/server/services/channels.js +3 -2
- package/src/server/services/codex-channels.js +38 -87
- package/src/server/services/codex-env-manager.js +426 -0
- package/src/server/services/codex-settings-manager.js +15 -15
- package/src/server/services/codex-statistics-service.js +3 -27
- package/src/server/services/config-export-service.js +20 -7
- package/src/server/services/config-registry-service.js +3 -2
- package/src/server/services/config-sync-manager.js +1 -1
- package/src/server/services/favorites.js +4 -3
- package/src/server/services/gemini-channels.js +3 -3
- package/src/server/services/gemini-statistics-service.js +3 -25
- package/src/server/services/mcp-service.js +2 -3
- package/src/server/services/model-detector.js +4 -3
- package/src/server/services/native-oauth-adapters.js +2 -1
- package/src/server/services/network-access.js +39 -1
- package/src/server/services/notification-hooks.js +951 -0
- package/src/server/services/opencode-channels.js +6 -6
- package/src/server/services/opencode-sessions.js +2 -2
- package/src/server/services/opencode-statistics-service.js +3 -27
- package/src/server/services/plugins-service.js +110 -31
- package/src/server/services/prompts-service.js +2 -3
- package/src/server/services/proxy-log-helper.js +242 -0
- package/src/server/services/proxy-runtime.js +6 -4
- package/src/server/services/repo-scanner-base.js +12 -4
- package/src/server/services/request-logger.js +7 -7
- package/src/server/services/security-config.js +4 -4
- package/src/server/services/session-cache.js +2 -2
- package/src/server/services/sessions.js +2 -2
- package/src/server/services/skill-service.js +174 -55
- package/src/server/services/statistics-service.js +10 -6
- package/src/server/services/ui-config.js +4 -3
- package/src/server/services/workspace-service.js +101 -156
- package/src/server/websocket-server.js +5 -4
- package/dist/web/assets/Analytics-DuYvId7u.css +0 -1
- package/dist/web/assets/Home-BMoFdAwy.css +0 -1
- package/dist/web/assets/Home-DNwp-0J-.js +0 -1
- package/dist/web/assets/index-BXeSvAwU.js +0 -2
- package/dist/web/assets/index-DWAC3Tdv.css +0 -1
- package/docs/bannel.png +0 -0
- package/docs/model-redirection.md +0 -251
|
@@ -43,17 +43,17 @@ function normalizeChannelName(value) {
|
|
|
43
43
|
|
|
44
44
|
// 获取渠道存储文件路径
|
|
45
45
|
function getChannelsFilePath() {
|
|
46
|
-
const
|
|
47
|
-
if (!fs.existsSync(
|
|
48
|
-
fs.mkdirSync(
|
|
46
|
+
const channelsDir = path.dirname(PATHS.channels.opencode);
|
|
47
|
+
if (!fs.existsSync(channelsDir)) {
|
|
48
|
+
fs.mkdirSync(channelsDir, { recursive: true });
|
|
49
49
|
}
|
|
50
50
|
return PATHS.channels.opencode;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
function getCodexChannelsFilePath() {
|
|
54
|
-
const
|
|
55
|
-
if (!fs.existsSync(
|
|
56
|
-
fs.mkdirSync(
|
|
54
|
+
const channelsDir = path.dirname(PATHS.channels.codex);
|
|
55
|
+
if (!fs.existsSync(channelsDir)) {
|
|
56
|
+
fs.mkdirSync(channelsDir, { recursive: true });
|
|
57
57
|
}
|
|
58
58
|
return PATHS.channels.codex;
|
|
59
59
|
}
|
|
@@ -9,8 +9,8 @@ const { NATIVE_PATHS, PATHS } = require('../../config/paths');
|
|
|
9
9
|
* 读取 OpenCode SQLite 会话数据
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
const PROJECT_ORDER_FILE =
|
|
13
|
-
const SESSION_ORDER_FILE =
|
|
12
|
+
const PROJECT_ORDER_FILE = PATHS.opencodeProjectOrder;
|
|
13
|
+
const SESSION_ORDER_FILE = PATHS.opencodeSessionOrder;
|
|
14
14
|
const OPENCODE_DB_PATH = path.join(NATIVE_PATHS.opencode.data, 'opencode.db');
|
|
15
15
|
const COUNTS_CACHE_TTL_MS = 30 * 1000;
|
|
16
16
|
const EMPTY_COUNTS = Object.freeze({ projectCount: 0, sessionCount: 0 });
|
|
@@ -4,36 +4,12 @@ const {
|
|
|
4
4
|
getDailyStatistics: getSharedDailyStatistics,
|
|
5
5
|
getTodayStatistics: getSharedTodayStatistics
|
|
6
6
|
} = require('./statistics-service');
|
|
7
|
+
const { normalizeUsageTokens, toNumber } = require('./proxy-log-helper');
|
|
7
8
|
|
|
8
9
|
const TOOL_TYPE = 'opencode';
|
|
9
10
|
|
|
10
|
-
function toNumber(value) {
|
|
11
|
-
const num = Number(value);
|
|
12
|
-
return Number.isFinite(num) ? num : 0;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function normalizeToolTokens(tokens = {}) {
|
|
16
|
-
const input = toNumber(tokens.input);
|
|
17
|
-
const output = toNumber(tokens.output);
|
|
18
|
-
const reasoning = toNumber(tokens.reasoning);
|
|
19
|
-
const cached = toNumber(tokens.cached);
|
|
20
|
-
const cacheCreation = toNumber(tokens.cacheCreation);
|
|
21
|
-
const cacheRead = toNumber(tokens.cacheRead || cached);
|
|
22
|
-
const total = toNumber(tokens.total) || (input + output + reasoning);
|
|
23
|
-
|
|
24
|
-
return {
|
|
25
|
-
input,
|
|
26
|
-
output,
|
|
27
|
-
reasoning,
|
|
28
|
-
cached,
|
|
29
|
-
cacheCreation,
|
|
30
|
-
cacheRead,
|
|
31
|
-
total
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
11
|
function toLegacyEntryShape(entry = {}, includeName = false) {
|
|
36
|
-
const normalized =
|
|
12
|
+
const normalized = normalizeUsageTokens(TOOL_TYPE, entry.tokens || {});
|
|
37
13
|
const result = {
|
|
38
14
|
requests: toNumber(entry.requests),
|
|
39
15
|
tokens: {
|
|
@@ -126,7 +102,7 @@ function buildDailyStatistics(sharedDaily = {}, fallbackDate) {
|
|
|
126
102
|
}
|
|
127
103
|
|
|
128
104
|
function recordRequest(requestData = {}) {
|
|
129
|
-
const normalizedTokens =
|
|
105
|
+
const normalizedTokens = normalizeUsageTokens(TOOL_TYPE, requestData.tokens || {});
|
|
130
106
|
return recordSharedRequest({
|
|
131
107
|
...requestData,
|
|
132
108
|
toolType: TOOL_TYPE,
|
|
@@ -10,7 +10,7 @@ const { listPlugins, getPlugin, updatePlugin: updatePluginRegistry } = require('
|
|
|
10
10
|
const { installPlugin: installPluginCore, uninstallPlugin: uninstallPluginCore } = require('../../plugins/plugin-installer');
|
|
11
11
|
const { initializePlugins, shutdownPlugins } = require('../../plugins/plugin-manager');
|
|
12
12
|
const { INSTALLED_DIR, CONFIG_DIR } = require('../../plugins/constants');
|
|
13
|
-
const { NATIVE_PATHS,
|
|
13
|
+
const { NATIVE_PATHS, PATHS } = require('../../config/paths');
|
|
14
14
|
|
|
15
15
|
const CLAUDE_PLUGINS_DIR = path.join(path.dirname(NATIVE_PATHS.claude.settings), 'plugins');
|
|
16
16
|
const CLAUDE_INSTALLED_FILE = path.join(CLAUDE_PLUGINS_DIR, 'installed_plugins.json');
|
|
@@ -106,14 +106,79 @@ function stripJsonComments(input = '') {
|
|
|
106
106
|
class PluginsService {
|
|
107
107
|
constructor(platform = 'claude') {
|
|
108
108
|
this.platform = ['claude', 'opencode'].includes(platform) ? platform : 'claude';
|
|
109
|
-
this.ccToolConfigDir = path.
|
|
109
|
+
this.ccToolConfigDir = path.dirname(PATHS.pluginRepos.claude);
|
|
110
110
|
this.opencodePluginsDir = path.join(OPENCODE_CONFIG_DIR, 'plugins');
|
|
111
111
|
this.opencodeLegacyPluginsDir = path.join(OPENCODE_CONFIG_DIR, 'plugin');
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
this.marketCachePath = this.platform === 'opencode'
|
|
113
|
+
? PATHS.pluginMarketCache.opencode
|
|
114
|
+
: PATHS.pluginMarketCache.claude;
|
|
114
115
|
this._marketCache = null;
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
clearMarketCache({ removeFile = true } = {}) {
|
|
119
|
+
this._marketCache = null;
|
|
120
|
+
if (removeFile) {
|
|
121
|
+
try {
|
|
122
|
+
if (fs.existsSync(this.marketCachePath)) {
|
|
123
|
+
fs.unlinkSync(this.marketCachePath);
|
|
124
|
+
}
|
|
125
|
+
} catch (err) {
|
|
126
|
+
// ignore cache deletion errors
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
loadMarketCacheFromFile() {
|
|
132
|
+
try {
|
|
133
|
+
if (fs.existsSync(this.marketCachePath)) {
|
|
134
|
+
const data = JSON.parse(fs.readFileSync(this.marketCachePath, 'utf-8'));
|
|
135
|
+
if (Array.isArray(data.plugins)) {
|
|
136
|
+
return data.plugins;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch (err) {
|
|
140
|
+
// ignore cache read errors
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
saveMarketCacheToFile(plugins) {
|
|
146
|
+
try {
|
|
147
|
+
this._ensureDir(path.dirname(this.marketCachePath));
|
|
148
|
+
fs.writeFileSync(this.marketCachePath, JSON.stringify({ plugins }), 'utf-8');
|
|
149
|
+
} catch (err) {
|
|
150
|
+
// ignore cache write errors
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
prepareMarketPlugins(plugins = []) {
|
|
155
|
+
const preparedPlugins = Array.isArray(plugins)
|
|
156
|
+
? plugins.map(plugin => ({ ...plugin }))
|
|
157
|
+
: [];
|
|
158
|
+
const seen = new Set();
|
|
159
|
+
const installedPlugins = this.listPlugins().plugins;
|
|
160
|
+
const installedNames = new Set(installedPlugins.map(p => p.name));
|
|
161
|
+
|
|
162
|
+
const deduped = [];
|
|
163
|
+
for (const plugin of preparedPlugins) {
|
|
164
|
+
const key = [
|
|
165
|
+
plugin.name || '',
|
|
166
|
+
plugin.repoOwner || '',
|
|
167
|
+
plugin.repoName || '',
|
|
168
|
+
plugin.directory || plugin.installSource || ''
|
|
169
|
+
].join('::');
|
|
170
|
+
if (seen.has(key)) continue;
|
|
171
|
+
seen.add(key);
|
|
172
|
+
deduped.push({
|
|
173
|
+
...plugin,
|
|
174
|
+
isInstalled: installedNames.has(plugin.name)
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
deduped.sort((a, b) => (a.name || '').toLowerCase().localeCompare((b.name || '').toLowerCase()));
|
|
179
|
+
return deduped;
|
|
180
|
+
}
|
|
181
|
+
|
|
117
182
|
_ensureDir(dirPath) {
|
|
118
183
|
if (!fs.existsSync(dirPath)) {
|
|
119
184
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
@@ -780,11 +845,9 @@ class PluginsService {
|
|
|
780
845
|
* @returns {string} Config file path
|
|
781
846
|
*/
|
|
782
847
|
getReposConfigPath() {
|
|
783
|
-
this.
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
}
|
|
787
|
-
return path.join(this.ccToolConfigDir, 'plugin-repos.json');
|
|
848
|
+
const filePath = this._isOpenCode() ? PATHS.pluginRepos.opencode : PATHS.pluginRepos.claude;
|
|
849
|
+
this._ensureDir(path.dirname(filePath));
|
|
850
|
+
return filePath;
|
|
788
851
|
}
|
|
789
852
|
|
|
790
853
|
_getDefaultRepos() {
|
|
@@ -936,6 +999,7 @@ class PluginsService {
|
|
|
936
999
|
|
|
937
1000
|
config.repos.push(newRepo);
|
|
938
1001
|
this.saveReposConfig(config);
|
|
1002
|
+
this.clearMarketCache();
|
|
939
1003
|
|
|
940
1004
|
return config.repos;
|
|
941
1005
|
}
|
|
@@ -950,6 +1014,7 @@ class PluginsService {
|
|
|
950
1014
|
const config = this.loadReposConfig();
|
|
951
1015
|
config.repos = config.repos.filter(r => !(r.owner === owner && r.name === name));
|
|
952
1016
|
this.saveReposConfig(config);
|
|
1017
|
+
this.clearMarketCache();
|
|
953
1018
|
return config.repos;
|
|
954
1019
|
}
|
|
955
1020
|
|
|
@@ -968,6 +1033,7 @@ class PluginsService {
|
|
|
968
1033
|
}
|
|
969
1034
|
repo.enabled = enabled;
|
|
970
1035
|
this.saveReposConfig(config);
|
|
1036
|
+
this.clearMarketCache();
|
|
971
1037
|
return config.repos;
|
|
972
1038
|
}
|
|
973
1039
|
|
|
@@ -1157,21 +1223,29 @@ class PluginsService {
|
|
|
1157
1223
|
* @returns {Promise<Array>} List of available market plugins
|
|
1158
1224
|
*/
|
|
1159
1225
|
async getMarketPlugins(forceRefresh = false) {
|
|
1160
|
-
if (
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
}
|
|
1226
|
+
if (forceRefresh) {
|
|
1227
|
+
this.clearMarketCache({ removeFile: false });
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
const fileCache = this.loadMarketCacheFromFile();
|
|
1231
|
+
|
|
1232
|
+
if (!forceRefresh && Array.isArray(this._marketCache) && this._marketCache.length > 0) {
|
|
1233
|
+
if (Array.isArray(fileCache) && fileCache.length > this._marketCache.length) {
|
|
1234
|
+
this._marketCache = this.prepareMarketPlugins(fileCache);
|
|
1235
|
+
return this._marketCache;
|
|
1236
|
+
}
|
|
1237
|
+
this._marketCache = this.prepareMarketPlugins(this._marketCache);
|
|
1238
|
+
return this._marketCache;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
if (!forceRefresh && Array.isArray(fileCache) && fileCache.length > 0) {
|
|
1242
|
+
this._marketCache = this.prepareMarketPlugins(fileCache);
|
|
1243
|
+
return this._marketCache;
|
|
1171
1244
|
}
|
|
1172
1245
|
|
|
1173
1246
|
const repos = this.getRepos().filter(r => r.enabled);
|
|
1174
1247
|
const marketPlugins = [];
|
|
1248
|
+
let repoFailureCount = 0;
|
|
1175
1249
|
|
|
1176
1250
|
for (const repo of repos) {
|
|
1177
1251
|
try {
|
|
@@ -1264,24 +1338,29 @@ class PluginsService {
|
|
|
1264
1338
|
}
|
|
1265
1339
|
}
|
|
1266
1340
|
} catch (err) {
|
|
1341
|
+
repoFailureCount++;
|
|
1267
1342
|
console.error(`[PluginsService] Failed to fetch plugins from ${repo.owner}/${repo.name}:`, err.message);
|
|
1268
1343
|
}
|
|
1269
1344
|
}
|
|
1270
1345
|
|
|
1271
|
-
|
|
1272
|
-
const
|
|
1273
|
-
|
|
1346
|
+
const preparedPlugins = this.prepareMarketPlugins(marketPlugins);
|
|
1347
|
+
const preparedFileCache = Array.isArray(fileCache) && fileCache.length > 0
|
|
1348
|
+
? this.prepareMarketPlugins(fileCache)
|
|
1349
|
+
: null;
|
|
1350
|
+
const shouldUseStaleFileCache = preparedFileCache && (
|
|
1351
|
+
(repos.length > 0 && repoFailureCount === repos.length) ||
|
|
1352
|
+
(repoFailureCount > 0 && preparedFileCache.length > preparedPlugins.length)
|
|
1353
|
+
);
|
|
1274
1354
|
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1355
|
+
if (shouldUseStaleFileCache) {
|
|
1356
|
+
this._marketCache = preparedFileCache;
|
|
1357
|
+
return this._marketCache;
|
|
1358
|
+
}
|
|
1278
1359
|
|
|
1279
|
-
this._marketCache =
|
|
1280
|
-
|
|
1281
|
-
fs.writeFileSync(this.marketCachePath, JSON.stringify({ plugins: marketPlugins }), 'utf-8');
|
|
1282
|
-
} catch (err) { /* ignore */ }
|
|
1360
|
+
this._marketCache = preparedPlugins;
|
|
1361
|
+
this.saveMarketCacheToFile(preparedPlugins);
|
|
1283
1362
|
|
|
1284
|
-
return
|
|
1363
|
+
return preparedPlugins;
|
|
1285
1364
|
}
|
|
1286
1365
|
}
|
|
1287
1366
|
|
|
@@ -7,14 +7,13 @@
|
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const os = require('os');
|
|
10
|
-
const { NATIVE_PATHS } = require('../../config/paths');
|
|
10
|
+
const { NATIVE_PATHS, PATHS } = require('../../config/paths');
|
|
11
11
|
const { resolvePreferredHomeDir } = require('../../utils/home-dir');
|
|
12
12
|
|
|
13
13
|
const HOME_DIR = resolvePreferredHomeDir(process.platform, process.env, os.homedir());
|
|
14
14
|
|
|
15
15
|
// Prompts 配置文件路径
|
|
16
|
-
const
|
|
17
|
-
const PROMPTS_FILE = path.join(CC_TOOL_DIR, 'prompts.json');
|
|
16
|
+
const PROMPTS_FILE = PATHS.prompts;
|
|
18
17
|
|
|
19
18
|
// 各平台提示词文件路径
|
|
20
19
|
const CLAUDE_PROMPT_PATH = path.join(HOME_DIR, '.claude', 'CLAUDE.md');
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
function toNumber(value) {
|
|
2
|
+
const num = Number(value);
|
|
3
|
+
return Number.isFinite(num) ? num : 0;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function normalizeToolSource(source = '') {
|
|
7
|
+
const normalized = String(source || '').trim().toLowerCase();
|
|
8
|
+
if (normalized === 'claude' || normalized === 'claude-code') return 'claude';
|
|
9
|
+
if (normalized === 'codex') return 'codex';
|
|
10
|
+
if (normalized === 'gemini') return 'gemini';
|
|
11
|
+
if (normalized === 'opencode') return 'opencode';
|
|
12
|
+
return 'claude';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeUsageTokens(source, tokens = {}) {
|
|
16
|
+
const normalizedSource = normalizeToolSource(source);
|
|
17
|
+
const input = toNumber(tokens.input);
|
|
18
|
+
const output = toNumber(tokens.output);
|
|
19
|
+
const cacheCreation = toNumber(tokens.cacheCreation);
|
|
20
|
+
const cacheRead = toNumber(tokens.cacheRead);
|
|
21
|
+
const cached = toNumber(tokens.cached);
|
|
22
|
+
const reasoning = toNumber(tokens.reasoning);
|
|
23
|
+
let total = toNumber(tokens.total);
|
|
24
|
+
|
|
25
|
+
if (total <= 0) {
|
|
26
|
+
if (normalizedSource === 'claude') {
|
|
27
|
+
total = input + output + cacheCreation + cacheRead;
|
|
28
|
+
} else {
|
|
29
|
+
total = input + output;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
input,
|
|
35
|
+
output,
|
|
36
|
+
cacheCreation,
|
|
37
|
+
cacheRead,
|
|
38
|
+
cached,
|
|
39
|
+
reasoning,
|
|
40
|
+
total
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function hasMeaningfulUsage(source, tokens = {}) {
|
|
45
|
+
const normalized = normalizeUsageTokens(source, tokens);
|
|
46
|
+
if (normalized.total > 0) return true;
|
|
47
|
+
if (normalized.input > 0 || normalized.output > 0) return true;
|
|
48
|
+
if (normalized.cacheCreation > 0 || normalized.cacheRead > 0) return true;
|
|
49
|
+
if (normalized.cached > 0 || normalized.reasoning > 0) return true;
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function formatRealtimeTime(timestamp = Date.now()) {
|
|
54
|
+
return new Date(timestamp).toLocaleTimeString('zh-CN', {
|
|
55
|
+
hour12: false,
|
|
56
|
+
hour: '2-digit',
|
|
57
|
+
minute: '2-digit',
|
|
58
|
+
second: '2-digit'
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function buildSuccessLogPayload({
|
|
63
|
+
source,
|
|
64
|
+
requestId,
|
|
65
|
+
channel,
|
|
66
|
+
model,
|
|
67
|
+
tokens,
|
|
68
|
+
cost = 0,
|
|
69
|
+
timestamp = Date.now()
|
|
70
|
+
}) {
|
|
71
|
+
const normalized = normalizeUsageTokens(source, tokens);
|
|
72
|
+
return {
|
|
73
|
+
type: 'log',
|
|
74
|
+
status: 'success',
|
|
75
|
+
id: requestId,
|
|
76
|
+
time: formatRealtimeTime(timestamp),
|
|
77
|
+
channel,
|
|
78
|
+
model: model || '',
|
|
79
|
+
inputTokens: normalized.input,
|
|
80
|
+
outputTokens: normalized.output,
|
|
81
|
+
cacheCreation: normalized.cacheCreation,
|
|
82
|
+
cacheRead: normalized.cacheRead,
|
|
83
|
+
cachedTokens: normalized.cached,
|
|
84
|
+
reasoningTokens: normalized.reasoning,
|
|
85
|
+
totalTokens: normalized.total,
|
|
86
|
+
cost,
|
|
87
|
+
source: normalizeToolSource(source),
|
|
88
|
+
timestamp
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function buildFailureLogPayload({
|
|
93
|
+
source,
|
|
94
|
+
requestId,
|
|
95
|
+
channel,
|
|
96
|
+
model,
|
|
97
|
+
message,
|
|
98
|
+
error,
|
|
99
|
+
statusCode,
|
|
100
|
+
stage,
|
|
101
|
+
timestamp = Date.now()
|
|
102
|
+
}) {
|
|
103
|
+
const errorMessage = String(
|
|
104
|
+
error?.message
|
|
105
|
+
|| message
|
|
106
|
+
|| error
|
|
107
|
+
|| 'Request failed'
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
type: 'log',
|
|
112
|
+
status: 'error',
|
|
113
|
+
id: requestId || `${normalizeToolSource(source)}-error-${timestamp}-${Math.random().toString(36).slice(2, 8)}`,
|
|
114
|
+
time: formatRealtimeTime(timestamp),
|
|
115
|
+
channel: channel || 'Unknown',
|
|
116
|
+
model: model || '',
|
|
117
|
+
message: errorMessage,
|
|
118
|
+
error: errorMessage,
|
|
119
|
+
statusCode: Number.isFinite(Number(statusCode)) ? Number(statusCode) : null,
|
|
120
|
+
stage: stage || 'proxy',
|
|
121
|
+
inputTokens: 0,
|
|
122
|
+
outputTokens: 0,
|
|
123
|
+
cacheCreation: 0,
|
|
124
|
+
cacheRead: 0,
|
|
125
|
+
cachedTokens: 0,
|
|
126
|
+
reasoningTokens: 0,
|
|
127
|
+
totalTokens: 0,
|
|
128
|
+
cost: 0,
|
|
129
|
+
source: normalizeToolSource(source),
|
|
130
|
+
timestamp
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function publishUsageLog({
|
|
135
|
+
source,
|
|
136
|
+
metadata = {},
|
|
137
|
+
model,
|
|
138
|
+
tokens,
|
|
139
|
+
calculateCost,
|
|
140
|
+
broadcastLog,
|
|
141
|
+
recordRequest,
|
|
142
|
+
recordSuccess,
|
|
143
|
+
allowBroadcast = true
|
|
144
|
+
}) {
|
|
145
|
+
if (!hasMeaningfulUsage(source, tokens)) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const normalizedSource = normalizeToolSource(source);
|
|
150
|
+
const normalizedTokens = normalizeUsageTokens(normalizedSource, tokens);
|
|
151
|
+
const requestId = metadata.id || `${normalizedSource}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
152
|
+
const timestamp = Date.now();
|
|
153
|
+
const cost = typeof calculateCost === 'function'
|
|
154
|
+
? calculateCost(model || '', normalizedTokens)
|
|
155
|
+
: 0;
|
|
156
|
+
|
|
157
|
+
if (allowBroadcast && typeof broadcastLog === 'function') {
|
|
158
|
+
broadcastLog(buildSuccessLogPayload({
|
|
159
|
+
source: normalizedSource,
|
|
160
|
+
requestId,
|
|
161
|
+
channel: metadata.channel,
|
|
162
|
+
model,
|
|
163
|
+
tokens: normalizedTokens,
|
|
164
|
+
cost,
|
|
165
|
+
timestamp
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (typeof recordRequest === 'function') {
|
|
170
|
+
recordRequest({
|
|
171
|
+
id: requestId,
|
|
172
|
+
timestamp: new Date(metadata.startTime || timestamp).toISOString(),
|
|
173
|
+
toolType: normalizedSource === 'claude' ? 'claude-code' : normalizedSource,
|
|
174
|
+
channel: metadata.channel,
|
|
175
|
+
channelId: metadata.channelId,
|
|
176
|
+
model: model || '',
|
|
177
|
+
tokens: {
|
|
178
|
+
input: normalizedTokens.input,
|
|
179
|
+
output: normalizedTokens.output,
|
|
180
|
+
reasoning: normalizedTokens.reasoning,
|
|
181
|
+
cached: normalizedTokens.cached,
|
|
182
|
+
cacheCreation: normalizedTokens.cacheCreation,
|
|
183
|
+
cacheRead: normalizedTokens.cacheRead,
|
|
184
|
+
total: normalizedTokens.total
|
|
185
|
+
},
|
|
186
|
+
duration: Math.max(0, timestamp - toNumber(metadata.startTime || timestamp)),
|
|
187
|
+
success: true,
|
|
188
|
+
cost
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (typeof recordSuccess === 'function' && metadata.channelId) {
|
|
193
|
+
recordSuccess(metadata.channelId, normalizedSource);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
cost,
|
|
198
|
+
tokens: normalizedTokens,
|
|
199
|
+
timestamp
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function publishFailureLog({
|
|
204
|
+
source,
|
|
205
|
+
metadata = {},
|
|
206
|
+
channel,
|
|
207
|
+
model,
|
|
208
|
+
message,
|
|
209
|
+
error,
|
|
210
|
+
statusCode,
|
|
211
|
+
stage,
|
|
212
|
+
broadcastLog
|
|
213
|
+
}) {
|
|
214
|
+
if (typeof broadcastLog !== 'function') {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const payload = buildFailureLogPayload({
|
|
219
|
+
source,
|
|
220
|
+
requestId: metadata.id,
|
|
221
|
+
channel: channel || metadata.channel,
|
|
222
|
+
model: model || metadata.model,
|
|
223
|
+
message,
|
|
224
|
+
error,
|
|
225
|
+
statusCode,
|
|
226
|
+
stage
|
|
227
|
+
});
|
|
228
|
+
broadcastLog(payload);
|
|
229
|
+
return payload;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
module.exports = {
|
|
233
|
+
toNumber,
|
|
234
|
+
normalizeToolSource,
|
|
235
|
+
normalizeUsageTokens,
|
|
236
|
+
hasMeaningfulUsage,
|
|
237
|
+
formatRealtimeTime,
|
|
238
|
+
buildSuccessLogPayload,
|
|
239
|
+
buildFailureLogPayload,
|
|
240
|
+
publishUsageLog,
|
|
241
|
+
publishFailureLog
|
|
242
|
+
};
|
|
@@ -3,11 +3,13 @@ const path = require('path');
|
|
|
3
3
|
const { PATHS } = require('../../config/paths');
|
|
4
4
|
|
|
5
5
|
function getRuntimeFilePath(proxyType) {
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
const filePath = PATHS.proxyRuntime?.[proxyType]
|
|
7
|
+
|| path.join(path.dirname(PATHS.proxyRuntime.claude), `${proxyType}-proxy.json`);
|
|
8
|
+
const dir = path.dirname(filePath);
|
|
9
|
+
if (!fs.existsSync(dir)) {
|
|
10
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
9
11
|
}
|
|
10
|
-
return
|
|
12
|
+
return filePath;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
function saveProxyStartTime(proxyType, preserveExisting = false) {
|
|
@@ -11,7 +11,7 @@ const https = require('https');
|
|
|
11
11
|
const http = require('http');
|
|
12
12
|
const { createWriteStream } = require('fs');
|
|
13
13
|
const AdmZip = require('adm-zip');
|
|
14
|
-
const {
|
|
14
|
+
const { PATHS, getRepoScannerReposPath, getRepoScannerCachePath } = require('../../config/paths');
|
|
15
15
|
|
|
16
16
|
// 缓存有效期(5分钟)
|
|
17
17
|
const CACHE_TTL = 5 * 60 * 1000;
|
|
@@ -70,9 +70,9 @@ class RepoScannerBase {
|
|
|
70
70
|
this.fileExtension = options.fileExtension || '.md';
|
|
71
71
|
this.defaultRepos = options.defaultRepos || [];
|
|
72
72
|
|
|
73
|
-
this.configDir =
|
|
74
|
-
this.reposConfigPath =
|
|
75
|
-
this.cachePath =
|
|
73
|
+
this.configDir = PATHS.config;
|
|
74
|
+
this.reposConfigPath = getRepoScannerReposPath(this.type);
|
|
75
|
+
this.cachePath = getRepoScannerCachePath(this.type);
|
|
76
76
|
|
|
77
77
|
// 内存缓存
|
|
78
78
|
this.itemsCache = null;
|
|
@@ -89,6 +89,14 @@ class RepoScannerBase {
|
|
|
89
89
|
if (!fs.existsSync(this.configDir)) {
|
|
90
90
|
fs.mkdirSync(this.configDir, { recursive: true });
|
|
91
91
|
}
|
|
92
|
+
const reposDir = path.dirname(this.reposConfigPath);
|
|
93
|
+
if (!fs.existsSync(reposDir)) {
|
|
94
|
+
fs.mkdirSync(reposDir, { recursive: true });
|
|
95
|
+
}
|
|
96
|
+
const cacheDir = path.dirname(this.cachePath);
|
|
97
|
+
if (!fs.existsSync(cacheDir)) {
|
|
98
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
99
|
+
}
|
|
92
100
|
}
|
|
93
101
|
|
|
94
102
|
// ==================== 仓库配置管理 ====================
|
|
@@ -18,7 +18,7 @@ const fs = require('fs');
|
|
|
18
18
|
const path = require('path');
|
|
19
19
|
const { PATHS } = require('../../config/paths');
|
|
20
20
|
|
|
21
|
-
const
|
|
21
|
+
const REQUEST_SNAPSHOTS_DIR = path.dirname(PATHS.requestSnapshots.claude);
|
|
22
22
|
|
|
23
23
|
function ensureDir(dir) {
|
|
24
24
|
if (!fs.existsSync(dir)) {
|
|
@@ -55,8 +55,8 @@ function persistProxyRequestSnapshot(source, payload) {
|
|
|
55
55
|
if (!isProxyRequestLoggingEnabled()) return;
|
|
56
56
|
|
|
57
57
|
try {
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
const logPath = PATHS.requestSnapshots[source] || path.join(REQUEST_SNAPSHOTS_DIR, `${source}.jsonl`);
|
|
59
|
+
ensureDir(path.dirname(logPath));
|
|
60
60
|
fs.appendFile(logPath, `${JSON.stringify(payload)}\n`, (error) => {
|
|
61
61
|
if (error) {
|
|
62
62
|
console.error(`[request-logger] Failed to persist ${source} request snapshot:`, error);
|
|
@@ -97,8 +97,8 @@ function createApiRequestLogger() {
|
|
|
97
97
|
};
|
|
98
98
|
|
|
99
99
|
try {
|
|
100
|
-
ensureDir(
|
|
101
|
-
const logPath = path.join(
|
|
100
|
+
ensureDir(PATHS.logs);
|
|
101
|
+
const logPath = path.join(PATHS.logs, 'api-requests.jsonl');
|
|
102
102
|
fs.appendFile(logPath, `${JSON.stringify(entry)}\n`, (err) => {
|
|
103
103
|
if (err) {
|
|
104
104
|
console.error('[request-logger] Failed to write API request log:', err);
|
|
@@ -122,7 +122,7 @@ function createApiRequestLogger() {
|
|
|
122
122
|
};
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
const CLAUDE_TEMPLATE_PATH =
|
|
125
|
+
const CLAUDE_TEMPLATE_PATH = PATHS.claudeRequestTemplate;
|
|
126
126
|
const CLAUDE_TEMPLATE_MIN_SYSTEM_CHARS = 100;
|
|
127
127
|
|
|
128
128
|
const FALLBACK_CLAUDE_SYSTEM = Object.freeze([
|
|
@@ -280,7 +280,7 @@ function persistClaudeRequestTemplate(body) {
|
|
|
280
280
|
if (systemCharCount < CLAUDE_TEMPLATE_MIN_SYSTEM_CHARS) return;
|
|
281
281
|
|
|
282
282
|
try {
|
|
283
|
-
ensureDir(
|
|
283
|
+
ensureDir(path.dirname(CLAUDE_TEMPLATE_PATH));
|
|
284
284
|
const template = { updatedAt: Date.now(), userId, system, tools };
|
|
285
285
|
fs.writeFile(CLAUDE_TEMPLATE_PATH, JSON.stringify(template), (err) => {
|
|
286
286
|
if (err) console.error('[request-logger] Failed to write claude-request-template.json:', err);
|
|
@@ -3,8 +3,7 @@ const path = require('path');
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const { PATHS } = require('../../config/paths');
|
|
5
5
|
|
|
6
|
-
const
|
|
7
|
-
const SECURITY_FILE = path.join(SECURITY_DIR, 'security.json');
|
|
6
|
+
const SECURITY_FILE = PATHS.security;
|
|
8
7
|
|
|
9
8
|
const DEFAULT_SECURITY_CONFIG = {
|
|
10
9
|
passwordHash: '',
|
|
@@ -17,8 +16,9 @@ const PBKDF2_KEYLEN = 64;
|
|
|
17
16
|
const PBKDF2_DIGEST = 'sha512';
|
|
18
17
|
|
|
19
18
|
function ensureSecurityDir() {
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
const dir = path.dirname(SECURITY_FILE);
|
|
20
|
+
if (!fs.existsSync(dir)) {
|
|
21
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|