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.
Files changed (79) hide show
  1. package/CHANGELOG.md +17 -2
  2. package/README.md +253 -326
  3. package/dist/web/assets/{Analytics-DLpoDZ2M.js → Analytics-DEjfL5Jx.js} +4 -4
  4. package/dist/web/assets/Analytics-RNn1BUbG.css +1 -0
  5. package/dist/web/assets/{ConfigTemplates-D_hRb55W.js → ConfigTemplates-DkRL_-tf.js} +1 -1
  6. package/dist/web/assets/Home-BQxQ1LhR.css +1 -0
  7. package/dist/web/assets/Home-CF-L640I.js +1 -0
  8. package/dist/web/assets/{PluginManager-JXsyym1s.js → PluginManager-BzNYTdNB.js} +1 -1
  9. package/dist/web/assets/{ProjectList-DZWSeb-q.js → ProjectList-C0-JgHMM.js} +1 -1
  10. package/dist/web/assets/{SessionList-Cs624DR3.js → SessionList-CkZUdX5N.js} +1 -1
  11. package/dist/web/assets/{SkillManager-bEliz7qz.js → SkillManager-Cak0-4d4.js} +1 -1
  12. package/dist/web/assets/{WorkspaceManager-J3RecFGn.js → WorkspaceManager-CGDJzwEr.js} +1 -1
  13. package/dist/web/assets/{icons-Cuc23WS7.js → icons-B5Pl4lrD.js} +1 -1
  14. package/dist/web/assets/index-D_WItvHE.js +2 -0
  15. package/dist/web/assets/index-Dz7v9OM0.css +1 -0
  16. package/dist/web/assets/{markdown-C9MYpaSi.js → markdown-DyTJGI4N.js} +1 -1
  17. package/dist/web/assets/{naive-ui-CxpuzdjU.js → naive-ui-Bdxp09n2.js} +1 -1
  18. package/dist/web/assets/{vendors-DMjSfzlv.js → vendors-CKPV1OAU.js} +2 -2
  19. package/dist/web/assets/{vue-vendor-DET08QYg.js → vue-vendor-3bf-fPGP.js} +1 -1
  20. package/dist/web/index.html +7 -7
  21. package/docs/home.png +0 -0
  22. package/package.json +13 -5
  23. package/src/commands/daemon.js +3 -2
  24. package/src/commands/security.js +1 -2
  25. package/src/config/paths.js +638 -93
  26. package/src/server/api/agents.js +1 -1
  27. package/src/server/api/claude-hooks.js +13 -8
  28. package/src/server/api/codex-proxy.js +5 -4
  29. package/src/server/api/hooks.js +45 -0
  30. package/src/server/api/plugins.js +0 -1
  31. package/src/server/api/statistics.js +4 -4
  32. package/src/server/api/ui-config.js +5 -0
  33. package/src/server/api/workspaces.js +1 -3
  34. package/src/server/codex-proxy-server.js +89 -59
  35. package/src/server/gemini-proxy-server.js +107 -88
  36. package/src/server/index.js +1 -0
  37. package/src/server/opencode-proxy-server.js +381 -225
  38. package/src/server/proxy-server.js +86 -60
  39. package/src/server/services/alias.js +3 -3
  40. package/src/server/services/channels.js +3 -2
  41. package/src/server/services/codex-channels.js +38 -87
  42. package/src/server/services/codex-env-manager.js +426 -0
  43. package/src/server/services/codex-settings-manager.js +15 -15
  44. package/src/server/services/codex-statistics-service.js +3 -27
  45. package/src/server/services/config-export-service.js +20 -7
  46. package/src/server/services/config-registry-service.js +3 -2
  47. package/src/server/services/config-sync-manager.js +1 -1
  48. package/src/server/services/favorites.js +4 -3
  49. package/src/server/services/gemini-channels.js +3 -3
  50. package/src/server/services/gemini-statistics-service.js +3 -25
  51. package/src/server/services/mcp-service.js +2 -3
  52. package/src/server/services/model-detector.js +4 -3
  53. package/src/server/services/native-oauth-adapters.js +2 -1
  54. package/src/server/services/network-access.js +39 -1
  55. package/src/server/services/notification-hooks.js +951 -0
  56. package/src/server/services/opencode-channels.js +6 -6
  57. package/src/server/services/opencode-sessions.js +2 -2
  58. package/src/server/services/opencode-statistics-service.js +3 -27
  59. package/src/server/services/plugins-service.js +110 -31
  60. package/src/server/services/prompts-service.js +2 -3
  61. package/src/server/services/proxy-log-helper.js +242 -0
  62. package/src/server/services/proxy-runtime.js +6 -4
  63. package/src/server/services/repo-scanner-base.js +12 -4
  64. package/src/server/services/request-logger.js +7 -7
  65. package/src/server/services/security-config.js +4 -4
  66. package/src/server/services/session-cache.js +2 -2
  67. package/src/server/services/sessions.js +2 -2
  68. package/src/server/services/skill-service.js +174 -55
  69. package/src/server/services/statistics-service.js +10 -6
  70. package/src/server/services/ui-config.js +4 -3
  71. package/src/server/services/workspace-service.js +101 -156
  72. package/src/server/websocket-server.js +5 -4
  73. package/dist/web/assets/Analytics-DuYvId7u.css +0 -1
  74. package/dist/web/assets/Home-BMoFdAwy.css +0 -1
  75. package/dist/web/assets/Home-DNwp-0J-.js +0 -1
  76. package/dist/web/assets/index-BXeSvAwU.js +0 -2
  77. package/dist/web/assets/index-DWAC3Tdv.css +0 -1
  78. package/docs/bannel.png +0 -0
  79. package/docs/model-redirection.md +0 -251
@@ -43,17 +43,17 @@ function normalizeChannelName(value) {
43
43
 
44
44
  // 获取渠道存储文件路径
45
45
  function getChannelsFilePath() {
46
- const ccToolDir = PATHS.base;
47
- if (!fs.existsSync(ccToolDir)) {
48
- fs.mkdirSync(ccToolDir, { recursive: true });
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 ccToolDir = PATHS.base;
55
- if (!fs.existsSync(ccToolDir)) {
56
- fs.mkdirSync(ccToolDir, { recursive: true });
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 = path.join(PATHS.base, 'opencode-project-order.json');
13
- const SESSION_ORDER_FILE = path.join(PATHS.base, 'opencode-session-order.json');
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 = normalizeToolTokens(entry.tokens || {});
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 = normalizeToolTokens(requestData.tokens || {});
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, HOME_DIR } = require('../../config/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.join(HOME_DIR, '.cc-tool');
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
- const prefix = this.platform === 'opencode' ? 'opencode-' : '';
113
- this.marketCachePath = path.join(this.ccToolConfigDir, `${prefix}plugins-market-cache.json`);
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._ensureDir(this.ccToolConfigDir);
784
- if (this._isOpenCode()) {
785
- return path.join(this.ccToolConfigDir, 'opencode-plugin-repos.json');
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 (!forceRefresh) {
1161
- if (this._marketCache) return this._marketCache;
1162
- try {
1163
- if (fs.existsSync(this.marketCachePath)) {
1164
- const data = JSON.parse(fs.readFileSync(this.marketCachePath, 'utf-8'));
1165
- if (Array.isArray(data.plugins)) {
1166
- this._marketCache = data.plugins;
1167
- return this._marketCache;
1168
- }
1169
- }
1170
- } catch (err) { /* ignore */ }
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
- // Mark installed plugins
1272
- const installedPlugins = this.listPlugins().plugins;
1273
- const installedNames = new Set(installedPlugins.map(p => p.name));
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
- marketPlugins.forEach(plugin => {
1276
- plugin.isInstalled = installedNames.has(plugin.name);
1277
- });
1355
+ if (shouldUseStaleFileCache) {
1356
+ this._marketCache = preparedFileCache;
1357
+ return this._marketCache;
1358
+ }
1278
1359
 
1279
- this._marketCache = marketPlugins;
1280
- try {
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 marketPlugins;
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 CC_TOOL_DIR = path.join(HOME_DIR, '.cc-tool');
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 ccToolDir = PATHS.base;
7
- if (!fs.existsSync(ccToolDir)) {
8
- fs.mkdirSync(ccToolDir, { recursive: true });
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 path.join(ccToolDir, `${proxyType}-proxy-runtime.json`);
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 { HOME_DIR } = require('../../config/paths');
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 = path.join(HOME_DIR, '.cc-tool');
74
- this.reposConfigPath = path.join(this.configDir, `${this.type}-repos.json`);
75
- this.cachePath = path.join(this.configDir, `${this.type}-cache.json`);
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 CC_TOOL_DIR = PATHS.base;
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
- ensureDir(CC_TOOL_DIR);
59
- const logPath = path.join(CC_TOOL_DIR, `${source}-requests.jsonl`);
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(path.join(CC_TOOL_DIR, 'logs'));
101
- const logPath = path.join(CC_TOOL_DIR, 'logs', 'api-requests.jsonl');
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 = path.join(CC_TOOL_DIR, 'claude-request-template.json');
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(CC_TOOL_DIR);
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 SECURITY_DIR = PATHS.base;
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
- if (!fs.existsSync(SECURITY_DIR)) {
21
- fs.mkdirSync(SECURITY_DIR, { recursive: true });
19
+ const dir = path.dirname(SECURITY_FILE);
20
+ if (!fs.existsSync(dir)) {
21
+ fs.mkdirSync(dir, { recursive: true });
22
22
  }
23
23
  }
24
24