cc-viewer 1.6.288 → 1.6.290

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 (91) hide show
  1. package/dist/assets/App-BIHUxfib.css +1 -0
  2. package/dist/assets/App-CAYgGmOg.js +1 -0
  3. package/dist/assets/{MdxEditorPanel-B7qeMjuJ.js → MdxEditorPanel-BRXw748j.js} +1 -1
  4. package/dist/assets/Mobile-DrIiNo2k.js +1 -0
  5. package/dist/assets/{_baseUniq-CZjCNqFY.js → _baseUniq-CY7wER8M.js} +1 -1
  6. package/dist/assets/{arc-CyAPrr6q.js → arc-DifwFfjI.js} +1 -1
  7. package/dist/assets/{architectureDiagram-Q4EWVU46-C5tlWzGO.js → architectureDiagram-Q4EWVU46-vxisGk93.js} +1 -1
  8. package/dist/assets/{blockDiagram-DXYQGD6D-CNgc9L1v.js → blockDiagram-DXYQGD6D-1Z1EuByB.js} +1 -1
  9. package/dist/assets/{c4Diagram-AHTNJAMY-D-LA0g8q.js → c4Diagram-AHTNJAMY-DtlxU5jH.js} +1 -1
  10. package/dist/assets/{channel-D2dCUaZg.js → channel-nzM7I2W4.js} +1 -1
  11. package/dist/assets/{chunk-4BX2VUAB-BQ-_WT2b.js → chunk-4BX2VUAB-C2UZLxjY.js} +1 -1
  12. package/dist/assets/{chunk-4TB4RGXK-CBJtah3V.js → chunk-4TB4RGXK-oqTPIHTb.js} +1 -1
  13. package/dist/assets/{chunk-55IACEB6-7SD98JbP.js → chunk-55IACEB6-JcdHyRbR.js} +1 -1
  14. package/dist/assets/{chunk-EDXVE4YY-CpNd_HjZ.js → chunk-EDXVE4YY-BbbLi1a3.js} +1 -1
  15. package/dist/assets/{chunk-FMBD7UC4-CdvcHWxv.js → chunk-FMBD7UC4-DndrHMoU.js} +1 -1
  16. package/dist/assets/{chunk-OYMX7WX6-xp2Wo3IQ.js → chunk-OYMX7WX6-DXUVMfBg.js} +1 -1
  17. package/dist/assets/{chunk-QZHKN3VN-CTtEIcIV.js → chunk-QZHKN3VN-BMzbbCV2.js} +1 -1
  18. package/dist/assets/{chunk-YZCP3GAM-sczbEN5L.js → chunk-YZCP3GAM-CnosXLiO.js} +1 -1
  19. package/dist/assets/classDiagram-6PBFFD2Q-CUXkafJT.js +1 -0
  20. package/dist/assets/classDiagram-v2-HSJHXN6E-CUXkafJT.js +1 -0
  21. package/dist/assets/clone-BWXYQRFP.js +1 -0
  22. package/dist/assets/{cose-bilkent-S5V4N54A-CqksYR9J.js → cose-bilkent-S5V4N54A-C0k93G8C.js} +1 -1
  23. package/dist/assets/{dagre-KV5264BT-BB_mC1O8.js → dagre-KV5264BT-DlvseVFx.js} +1 -1
  24. package/dist/assets/{diagram-5BDNPKRD-W6wr5Z0z.js → diagram-5BDNPKRD-CpeP8gCZ.js} +1 -1
  25. package/dist/assets/{diagram-G4DWMVQ6-BqeFMuy1.js → diagram-G4DWMVQ6-nTZRlkUW.js} +1 -1
  26. package/dist/assets/{diagram-MMDJMWI5-DKCKhGQq.js → diagram-MMDJMWI5-CZmxRJHr.js} +1 -1
  27. package/dist/assets/{diagram-TYMM5635-CrZwzzkS.js → diagram-TYMM5635-D1oFwDYt.js} +1 -1
  28. package/dist/assets/{erDiagram-SMLLAGMA-DLSEmh1R.js → erDiagram-SMLLAGMA-CN56CLXd.js} +1 -1
  29. package/dist/assets/{flowDiagram-DWJPFMVM-Bknmm2ed.js → flowDiagram-DWJPFMVM-DYFYyiT1.js} +1 -1
  30. package/dist/assets/{ganttDiagram-T4ZO3ILL-CF7UYyG0.js → ganttDiagram-T4ZO3ILL-Cod-6sTs.js} +1 -1
  31. package/dist/assets/{gitGraphDiagram-UUTBAWPF-DBlAknqy.js → gitGraphDiagram-UUTBAWPF-CmOM3xUD.js} +1 -1
  32. package/dist/assets/{graph-CZf9XKk_.js → graph-CF0TX7yu.js} +1 -1
  33. package/dist/assets/{index-Dz6njiZy.js → index-BPPzAUWO.js} +1 -1
  34. package/dist/assets/{index-NWqtd4TO.js → index-Bw6THA8X.js} +1 -1
  35. package/dist/assets/{index-satUsYjD.js → index-C29CuRSN.js} +1 -1
  36. package/dist/assets/{index-BT1jIW_4.js → index-DNOG0Ft9.js} +1 -1
  37. package/dist/assets/{index-V6lUWPmH.js → index-WPRXfvav.js} +1 -1
  38. package/dist/assets/index-Xtmpu1BO.js +2 -0
  39. package/dist/assets/{index-BsjIyUzx.js → index-ZaaYk8_N.js} +1 -1
  40. package/dist/assets/{index-CxEhDoYw.js → index-qteuBiB9.js} +1 -1
  41. package/dist/assets/{infoDiagram-42DDH7IO-CcN6cPOM.js → infoDiagram-42DDH7IO-DqXG1YqF.js} +1 -1
  42. package/dist/assets/{ishikawaDiagram-UXIWVN3A-BUw3xxt7.js → ishikawaDiagram-UXIWVN3A-BYqlvzbM.js} +1 -1
  43. package/dist/assets/{journeyDiagram-VCZTEJTY-entnH7TI.js → journeyDiagram-VCZTEJTY-1p7xwE9T.js} +1 -1
  44. package/dist/assets/{jszip.min-C5ONIxXo.js → jszip.min-56GCbwhg.js} +1 -1
  45. package/dist/assets/{kanban-definition-6JOO6SKY-Du0JJrC8.js → kanban-definition-6JOO6SKY-CyBzEFcP.js} +1 -1
  46. package/dist/assets/{layout-CRhg3LQ0.js → layout-DU3NNWDD.js} +1 -1
  47. package/dist/assets/{linear-DUE-zhnh.js → linear-D4VO1BqY.js} +1 -1
  48. package/dist/assets/{mermaid.core-C2nP7kV9.js → mermaid.core-Cf-7b6gy.js} +2 -2
  49. package/dist/assets/{min-BA1syrmY.js → min-CugqfU35.js} +1 -1
  50. package/dist/assets/{mindmap-definition-QFDTVHPH-rgB7-n1J.js → mindmap-definition-QFDTVHPH-C0KcWI5g.js} +1 -1
  51. package/dist/assets/{pieDiagram-DEJITSTG-C-6Q34RJ.js → pieDiagram-DEJITSTG-CJRW4PvK.js} +1 -1
  52. package/dist/assets/{quadrantDiagram-34T5L4WZ-CDNlG-6A.js → quadrantDiagram-34T5L4WZ-DhC4nG16.js} +1 -1
  53. package/dist/assets/{requirementDiagram-MS252O5E-BCoH7Pkn.js → requirementDiagram-MS252O5E-CxREJrbW.js} +1 -1
  54. package/dist/assets/{sankeyDiagram-XADWPNL6-Bd1j_SK2.js → sankeyDiagram-XADWPNL6-SUJo_IcF.js} +1 -1
  55. package/dist/assets/{seqResourceLoaders-g2fruG5N.css → seqResourceLoaders-8TtVkzZF.css} +2 -2
  56. package/dist/assets/seqResourceLoaders-BA6SO9Ei.js +2 -0
  57. package/dist/assets/{sequenceDiagram-FGHM5R23-CEGvrQVr.js → sequenceDiagram-FGHM5R23-CPXAiSCQ.js} +1 -1
  58. package/dist/assets/{stateDiagram-FHFEXIEX-Bxw8dP31.js → stateDiagram-FHFEXIEX-BLVV2eFt.js} +1 -1
  59. package/dist/assets/{stateDiagram-v2-QKLJ7IA2-DlOV6dJP.js → stateDiagram-v2-QKLJ7IA2-C9H3sIEI.js} +1 -1
  60. package/dist/assets/{timeline-definition-GMOUNBTQ-CF15I6sm.js → timeline-definition-GMOUNBTQ-CL2-3Y2a.js} +1 -1
  61. package/dist/assets/{vendor-antd-Dx8tRqWW.js → vendor-antd-CtM90v5R.js} +1 -1
  62. package/dist/assets/{vendor-codemirror-CYU_jFNR.js → vendor-codemirror-BrlhoIRC.js} +1 -1
  63. package/dist/assets/{vendor-mdxeditor-9MfaZIbX.js → vendor-mdxeditor-ERvI0V3G.js} +2 -2
  64. package/dist/assets/{vendor-qrcode-DDedOS4m.js → vendor-qrcode-DFSKTNnw.js} +1 -1
  65. package/dist/assets/{vendor-virtuoso-DiDJNj6_.js → vendor-virtuoso-CvUq0YV8.js} +1 -1
  66. package/dist/assets/{vennDiagram-DHZGUBPP-DRZjFIXT.js → vennDiagram-DHZGUBPP-BiST9SzL.js} +1 -1
  67. package/dist/assets/{wardley-RL74JXVD-DNJ8rZRv.js → wardley-RL74JXVD-BdMwjpLp.js} +1 -1
  68. package/dist/assets/{wardleyDiagram-NUSXRM2D-Dyd3cDbM.js → wardleyDiagram-NUSXRM2D-Zqm2aFeN.js} +1 -1
  69. package/dist/assets/{xychartDiagram-5P7HB3ND-DPYYxbLe.js → xychartDiagram-5P7HB3ND-CWKV6dzf.js} +1 -1
  70. package/dist/index.html +4 -4
  71. package/package.json +13 -2
  72. package/server/i18n.js +36 -0
  73. package/server/lib/adapters/dingtalk-adapter.js +129 -0
  74. package/server/lib/adapters/discord-adapter.js +140 -0
  75. package/server/lib/adapters/feishu-adapter.js +133 -0
  76. package/server/lib/adapters/wecom-adapter.js +146 -0
  77. package/server/lib/dingtalk-bridge.js +28 -474
  78. package/server/lib/dingtalk-config.js +11 -153
  79. package/server/lib/im-bridge-core.js +530 -0
  80. package/server/lib/im-config.js +246 -0
  81. package/server/routes/im.js +133 -0
  82. package/server/routes/preferences.js +13 -6
  83. package/server/server.js +31 -18
  84. package/dist/assets/App-BRgb-Ukj.css +0 -1
  85. package/dist/assets/App-D73sTzGX.js +0 -1
  86. package/dist/assets/Mobile-fEx_3vW1.js +0 -1
  87. package/dist/assets/classDiagram-6PBFFD2Q-CCCvEBzj.js +0 -1
  88. package/dist/assets/classDiagram-v2-HSJHXN6E-CCCvEBzj.js +0 -1
  89. package/dist/assets/clone-C7Abfw99.js +0 -1
  90. package/dist/assets/index-C_SecKTB.js +0 -2
  91. package/dist/assets/seqResourceLoaders-wAP4dArl.js +0 -2
@@ -0,0 +1,246 @@
1
+ // Generic multi-IM bridge config — pure storage logic, unit-tested.
2
+ //
3
+ // Each platform's config is persisted as a flat top-level key inside the same
4
+ // LOG_DIR/preferences.json the rest of cc-viewer uses (e.g. `dingtalk`, `feishu`).
5
+ // Flat sibling keys (not nested under `im.<platform>`) mean adding a platform needs
6
+ // NO on-disk migration. Like the rest of cc-viewer, the IM binding is GLOBAL ONLY
7
+ // (one bot ↔ one cc-viewer instance) — there is no per-project scope, which would
8
+ // fight the singleton-PTY model.
9
+ //
10
+ // Credential fields (`cred`: appKey/appId, low sensitivity) and secret fields
11
+ // (`secret`: appSecret) are both base64-encoded on disk so preferences.json never
12
+ // shows them in literal plaintext. This is light obfuscation, NOT encryption. The
13
+ // admin API masks secret fields entirely (→ hasSecret).
14
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'node:fs';
15
+ import { join, dirname } from 'node:path';
16
+ import { LOG_DIR } from '../../findcc.js';
17
+
18
+ const MIN_CHUNK = 500;
19
+ const MAX_CHUNK = 5000;
20
+ const DEFAULT_CHUNK = 3800;
21
+
22
+ // ─── per-platform descriptors ───
23
+ // `fields` drives normalize / encode-on-disk / decode-on-load / admin-mask uniformly.
24
+ // type 'bool' → !!v
25
+ // type 'cred' → trimmed string, base64 on disk, returned plaintext in config + admin state
26
+ // type 'secret' → trimmed string, base64 on disk, plaintext in config, MASKED (→hasSecret) in
27
+ // admin state, PRESERVED when saved empty
28
+ // type 'idlist' → de-duplicated trimmed string[]
29
+ // type 'chunk' → number clamped to [500, 5000], default 3800
30
+ // type 'region' → 'lark' | 'feishu' (Feishu/Lark cloud selector; plaintext)
31
+ const DESCRIPTORS = {
32
+ dingtalk: {
33
+ prefKey: 'dingtalk',
34
+ allowListField: 'allowStaffIds',
35
+ defaults: {
36
+ enabled: false, appKey: '', appSecret: '', allowStaffIds: [],
37
+ maxChunkChars: 3800, blockOnSkipPermissions: false,
38
+ },
39
+ fields: [
40
+ { key: 'enabled', type: 'bool' },
41
+ { key: 'appKey', type: 'cred' },
42
+ { key: 'appSecret', type: 'secret' },
43
+ { key: 'allowStaffIds', type: 'idlist' },
44
+ { key: 'maxChunkChars', type: 'chunk' },
45
+ { key: 'blockOnSkipPermissions', type: 'bool' },
46
+ ],
47
+ },
48
+ feishu: {
49
+ prefKey: 'feishu',
50
+ allowListField: 'allowUserIds',
51
+ defaults: {
52
+ enabled: false, appId: '', appSecret: '', region: 'feishu', allowUserIds: [],
53
+ maxChunkChars: 3800, blockOnSkipPermissions: false,
54
+ },
55
+ fields: [
56
+ { key: 'enabled', type: 'bool' },
57
+ { key: 'appId', type: 'cred' },
58
+ { key: 'appSecret', type: 'secret' },
59
+ { key: 'region', type: 'region' },
60
+ { key: 'allowUserIds', type: 'idlist' },
61
+ { key: 'maxChunkChars', type: 'chunk' },
62
+ { key: 'blockOnSkipPermissions', type: 'bool' },
63
+ ],
64
+ },
65
+ wecom: {
66
+ prefKey: 'wecom',
67
+ allowListField: 'allowUserIds',
68
+ defaults: {
69
+ enabled: false, botId: '', secret: '', allowUserIds: [],
70
+ maxChunkChars: 3800, blockOnSkipPermissions: false,
71
+ },
72
+ fields: [
73
+ { key: 'enabled', type: 'bool' },
74
+ { key: 'botId', type: 'cred' },
75
+ { key: 'secret', type: 'secret' },
76
+ { key: 'allowUserIds', type: 'idlist' },
77
+ { key: 'maxChunkChars', type: 'chunk' },
78
+ { key: 'blockOnSkipPermissions', type: 'bool' },
79
+ ],
80
+ },
81
+ discord: {
82
+ prefKey: 'discord',
83
+ allowListField: 'allowUserIds',
84
+ defaults: {
85
+ // 1900 < Discord's hard 2000-char/message limit (the adapter also hard-splits as defense).
86
+ enabled: false, botToken: '', allowUserIds: [],
87
+ maxChunkChars: 1900, blockOnSkipPermissions: false,
88
+ },
89
+ fields: [
90
+ { key: 'enabled', type: 'bool' },
91
+ { key: 'botToken', type: 'secret' }, // Discord's only credential (one secret, no separate cred)
92
+ { key: 'allowUserIds', type: 'idlist' },
93
+ { key: 'maxChunkChars', type: 'chunk', default: 1900 }, // < Discord's 2000-char limit
94
+ { key: 'blockOnSkipPermissions', type: 'bool' },
95
+ ],
96
+ },
97
+ };
98
+
99
+ export function getDescriptor(id) { return DESCRIPTORS[id]; }
100
+ export function listPlatforms() { return Object.keys(DESCRIPTORS); }
101
+
102
+ /** Path computed fresh each call: LOG_DIR is a live binding and tests redirect it via CCV_LOG_DIR before import. */
103
+ export function getPrefsPath() {
104
+ return join(LOG_DIR, 'preferences.json');
105
+ }
106
+
107
+ function readPrefs() {
108
+ try {
109
+ const p = getPrefsPath();
110
+ if (!existsSync(p)) return {};
111
+ const obj = JSON.parse(readFileSync(p, 'utf-8'));
112
+ return obj && typeof obj === 'object' ? obj : {};
113
+ } catch {
114
+ return {};
115
+ }
116
+ }
117
+
118
+ function writePrefs(prefs) {
119
+ const p = getPrefsPath();
120
+ const dir = dirname(p);
121
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
122
+ writeFileSync(p, JSON.stringify(prefs, null, 2), { mode: 0o600 });
123
+ // writeFileSync's mode only applies on creation; re-assert 0600 — the file now carries
124
+ // the (base64) secrets.
125
+ try { chmodSync(p, 0o600); } catch { /* best-effort; non-POSIX or race */ }
126
+ }
127
+
128
+ export function encodeSecret(plain) {
129
+ return plain ? Buffer.from(plain, 'utf-8').toString('base64') : '';
130
+ }
131
+ export function decodeSecret(stored) {
132
+ if (!stored || typeof stored !== 'string') return '';
133
+ try { return Buffer.from(stored, 'base64').toString('utf-8'); } catch { return ''; }
134
+ }
135
+
136
+ function clampChunk(n, dflt = DEFAULT_CHUNK) {
137
+ const v = Number(n);
138
+ if (!Number.isFinite(v)) return dflt; // missing/invalid → the field's default (per-platform, e.g. Discord 1900)
139
+ return Math.min(MAX_CHUNK, Math.max(MIN_CHUNK, Math.round(v)));
140
+ }
141
+
142
+ function normalizeIdList(v) {
143
+ if (!Array.isArray(v)) return [];
144
+ const seen = new Set();
145
+ const out = [];
146
+ for (const s of v) {
147
+ if (typeof s !== 'string') continue;
148
+ const t = s.trim();
149
+ if (!t || seen.has(t)) continue;
150
+ seen.add(t);
151
+ out.push(t);
152
+ }
153
+ return out;
154
+ }
155
+
156
+ function normField(type, v, dflt) {
157
+ switch (type) {
158
+ case 'bool': return !!v;
159
+ case 'cred':
160
+ case 'secret': return typeof v === 'string' ? v.trim() : '';
161
+ case 'idlist': return normalizeIdList(v);
162
+ case 'chunk': return clampChunk(v, dflt);
163
+ case 'region': return v === 'lark' ? 'lark' : 'feishu';
164
+ default: return typeof v === 'string' ? v : '';
165
+ }
166
+ }
167
+
168
+ function decodeField(type, v, dflt) {
169
+ switch (type) {
170
+ case 'cred':
171
+ case 'secret': return decodeSecret(v);
172
+ case 'bool': return !!v;
173
+ case 'idlist': return normalizeIdList(v);
174
+ case 'chunk': return clampChunk(v, dflt);
175
+ case 'region': return v === 'lark' ? 'lark' : 'feishu';
176
+ default: return typeof v === 'string' ? v : '';
177
+ }
178
+ }
179
+
180
+ /** Pure normalization (no disk I/O). Returns the in-memory plaintext shape. */
181
+ export function normalize(id, cfg) {
182
+ const desc = DESCRIPTORS[id];
183
+ if (!desc) throw new Error(`unknown IM platform: ${id}`);
184
+ const out = {};
185
+ for (const f of desc.fields) out[f.key] = normField(f.type, cfg ? cfg[f.key] : undefined, f.default);
186
+ return out;
187
+ }
188
+
189
+ function decodeStored(id, stored) {
190
+ const desc = DESCRIPTORS[id];
191
+ const out = {};
192
+ for (const f of desc.fields) out[f.key] = decodeField(f.type, stored ? stored[f.key] : undefined, f.default);
193
+ return out;
194
+ }
195
+
196
+ function encodeForDisk(id, n) {
197
+ const desc = DESCRIPTORS[id];
198
+ const out = {};
199
+ for (const f of desc.fields) {
200
+ out[f.key] = (f.type === 'cred' || f.type === 'secret') ? encodeSecret(n[f.key]) : n[f.key];
201
+ }
202
+ return out;
203
+ }
204
+
205
+ /** Effective config for the backend (plaintext cred/secret fields). */
206
+ export function loadConfig(id) {
207
+ return decodeStored(id, readPrefs()[DESCRIPTORS[id].prefKey]);
208
+ }
209
+
210
+ /**
211
+ * Admin-facing state: secret fields are NEVER returned — only `hasSecret`. cred fields are
212
+ * returned (low sensitivity, lets the admin confirm which app). The route layer adds live
213
+ * connection status.
214
+ */
215
+ export function loadState(id) {
216
+ const desc = DESCRIPTORS[id];
217
+ const c = decodeStored(id, readPrefs()[desc.prefKey]);
218
+ const out = {};
219
+ for (const f of desc.fields) {
220
+ if (f.type === 'secret') out.hasSecret = !!c[f.key];
221
+ else out[f.key] = c[f.key];
222
+ }
223
+ return out;
224
+ }
225
+
226
+ /**
227
+ * Persist a platform's config (read-merge-write, preserving all other prefs and other
228
+ * platforms). If a secret field is empty AND a secret is already stored, the existing
229
+ * secret is PRESERVED (lets the admin edit other fields without re-typing the secret).
230
+ * To remove the secret, disable the bridge. Stored base64; returns the in-memory
231
+ * (plaintext) normalized shape.
232
+ */
233
+ export function saveConfig(id, cfg) {
234
+ const desc = DESCRIPTORS[id];
235
+ const prefs = readPrefs();
236
+ const normalized = normalize(id, cfg);
237
+ for (const f of desc.fields) {
238
+ if (f.type === 'secret' && !normalized[f.key]) {
239
+ const existing = decodeSecret(prefs[desc.prefKey] && prefs[desc.prefKey][f.key]);
240
+ if (existing) normalized[f.key] = existing;
241
+ }
242
+ }
243
+ prefs[desc.prefKey] = encodeForDisk(id, normalized);
244
+ writePrefs(prefs);
245
+ return normalized;
246
+ }
@@ -0,0 +1,133 @@
1
+ // Generic multi-IM bridge config API. Mirrors server/routes/dingtalk.js but is platform-parametric:
2
+ //
3
+ // GET /api/im/:platform/status — public; remote callers get only enabled+hasSecret+connection,
4
+ // the local (admin) caller additionally gets plaintext secrets.
5
+ // POST /api/im/:platform/config — loopback-only (!isLocal → 403); save creds, reload bridge.
6
+ // POST /api/im/:platform/test — loopback-only; validate creds (fetch an access token).
7
+ //
8
+ // :platform must be a known descriptor (im-config.js) — unknown → 404. The DingTalk surface keeps
9
+ // its own /api/dingtalk/* routes (back-compat); new platforms (feishu, …) use these generic ones.
10
+ import { getDescriptor, loadConfig, loadState, saveConfig } from '../lib/im-config.js';
11
+
12
+ const JSON_HEADERS = { 'Content-Type': 'application/json' };
13
+ const IM_RE = /^\/api\/im\/([a-z0-9_-]+)\/(status|config|test)$/;
14
+
15
+ /** Resolve a known platform id from the URL, or null (→ 404) for an unknown one. */
16
+ function platformOf(url) {
17
+ const m = IM_RE.exec(url);
18
+ if (!m) return null;
19
+ return getDescriptor(m[1]) ? m[1] : null;
20
+ }
21
+
22
+ function imPredicate(verb, method) {
23
+ return (url, m) => {
24
+ if (m !== method) return false;
25
+ const x = IM_RE.exec(url);
26
+ return !!x && x[2] === verb;
27
+ };
28
+ }
29
+
30
+ function notFound(res) {
31
+ res.writeHead(404, JSON_HEADERS);
32
+ res.end(JSON.stringify({ error: 'Unknown IM platform' }));
33
+ }
34
+
35
+ function secretKeys(id) {
36
+ return getDescriptor(id).fields.filter((f) => f.type === 'secret').map((f) => f.key);
37
+ }
38
+
39
+ function readBody(req, deps, cb) {
40
+ let body = '';
41
+ req.on('data', (chunk) => {
42
+ body += chunk;
43
+ if (body.length > deps.MAX_POST_BODY) req.destroy();
44
+ });
45
+ req.on('end', () => cb(body));
46
+ }
47
+
48
+ function imStatus(req, res, parsedUrl, isLocal, deps) {
49
+ const id = platformOf(parsedUrl.pathname);
50
+ if (!id) { notFound(res); return; }
51
+ const conn = deps.im.getBridgeStatus(id);
52
+ const state = loadState(id);
53
+ res.writeHead(200, JSON_HEADERS);
54
+ if (!isLocal) {
55
+ // Loopback gate: a token-authorized LAN client must not see cred fields, the allowlist, the
56
+ // bound conversation id, or raw error strings. Expose only what the header status chip needs.
57
+ res.end(JSON.stringify({
58
+ enabled: state.enabled,
59
+ hasSecret: state.hasSecret,
60
+ connection: { running: conn.running, connected: conn.connected },
61
+ }));
62
+ return;
63
+ }
64
+ // 本机(127.0.0.1)= admin:附带明文密钥供本人查阅/复制(镜像 DingTalk 的策略)。
65
+ const cfg = loadConfig(id);
66
+ const secrets = {};
67
+ for (const k of secretKeys(id)) secrets[k] = cfg[k];
68
+ res.end(JSON.stringify({ ...state, ...secrets, connection: conn }));
69
+ }
70
+
71
+ function imConfigPost(req, res, parsedUrl, isLocal, deps) {
72
+ const id = platformOf(parsedUrl.pathname);
73
+ if (!id) { notFound(res); return; }
74
+ // Loopback-only: a secret must never be settable over the LAN even with a valid token.
75
+ if (!isLocal) {
76
+ res.writeHead(403, JSON_HEADERS);
77
+ res.end(JSON.stringify({ error: 'Loopback only' }));
78
+ return;
79
+ }
80
+ readBody(req, deps, (body) => {
81
+ let incoming;
82
+ try { incoming = JSON.parse(body); }
83
+ catch {
84
+ res.writeHead(400, JSON_HEADERS);
85
+ res.end(JSON.stringify({ error: 'Invalid JSON' }));
86
+ return;
87
+ }
88
+ // saveConfig normalizes to the descriptor's fields and preserves a stored secret when empty.
89
+ saveConfig(id, incoming);
90
+ // Apply immediately: stop the old connection and (re)start with the new config.
91
+ Promise.resolve(deps.im.reloadBridge(id)).catch(() => {});
92
+ res.writeHead(200, JSON_HEADERS);
93
+ res.end(JSON.stringify({ ...loadState(id), connection: deps.im.getBridgeStatus(id) }));
94
+ });
95
+ }
96
+
97
+ function imTestPost(req, res, parsedUrl, isLocal, deps) {
98
+ const id = platformOf(parsedUrl.pathname);
99
+ if (!id) { notFound(res); return; }
100
+ if (!isLocal) {
101
+ res.writeHead(403, JSON_HEADERS);
102
+ res.end(JSON.stringify({ error: 'Loopback only' }));
103
+ return;
104
+ }
105
+ readBody(req, deps, async (body) => {
106
+ let incoming = {};
107
+ try { incoming = body ? JSON.parse(body) : {}; } catch { /* fall back to stored */ }
108
+ // Merge incoming over stored per descriptor field (empty secret → use the stored one).
109
+ const stored = loadConfig(id);
110
+ const cfg = {};
111
+ for (const f of getDescriptor(id).fields) cfg[f.key] = incoming[f.key] || stored[f.key];
112
+ // Validate the credential fields are present BEFORE hitting the network, so an empty form
113
+ // yields "missing appId/botToken" instead of a cryptic adapter/transport error (mirrors the
114
+ // DingTalk route). cred + secret are the credential field types; everything else is optional.
115
+ const missing = getDescriptor(id).fields
116
+ .filter((f) => (f.type === 'cred' || f.type === 'secret') && !cfg[f.key])
117
+ .map((f) => f.key);
118
+ if (missing.length) {
119
+ res.writeHead(200, JSON_HEADERS);
120
+ res.end(JSON.stringify({ ok: false, detail: `missing ${missing.join('/')}` }));
121
+ return;
122
+ }
123
+ const result = await deps.im.testConnection(id, cfg);
124
+ res.writeHead(200, JSON_HEADERS);
125
+ res.end(JSON.stringify(result));
126
+ });
127
+ }
128
+
129
+ export const imRoutes = [
130
+ { predicate: imPredicate('status', 'GET'), handler: imStatus },
131
+ { predicate: imPredicate('config', 'POST'), handler: imConfigPost },
132
+ { predicate: imPredicate('test', 'POST'), handler: imTestPost },
133
+ ];
@@ -9,6 +9,14 @@ import { reconcileVoicePackPrefs as vpReconcile } from '../lib/voice-pack-manage
9
9
  import { mergeApprovalModalPrefs as vpMergeAM } from '../lib/approval-modal-prefs.js';
10
10
  import { readClaudeProjectModel } from '../lib/context-watcher.js';
11
11
  import { sendEventToClients } from '../lib/log-watcher.js';
12
+ import { listPlatforms } from '../lib/im-config.js';
13
+
14
+ // IM bridge configs (dingtalk, feishu, …) carry base64 app secrets and are only exposed via the
15
+ // admin-only /api/dingtalk/* and /api/im/* surfaces (with secrets masked). Strip every platform's
16
+ // key from any /api/preferences read/write so an authorized LAN client can never see or set them.
17
+ function stripImConfigs(obj) {
18
+ if (obj) for (const id of listPlatforms()) delete obj[id];
19
+ }
12
20
 
13
21
  function preferencesGet(req, res, parsedUrl, isLocal, deps) {
14
22
  let prefs = {};
@@ -18,9 +26,7 @@ function preferencesGet(req, res, parsedUrl, isLocal, deps) {
18
26
  // 全局 auth 与每个项目的 authByProject 覆盖都要剥离(后者同样含明文密码)。
19
27
  delete prefs.auth;
20
28
  delete prefs.authByProject;
21
- // dingtalk 配置(含 base64 app_secret)同存于 preferences.json,只能经 admin-only
22
- // /api/dingtalk/* 暴露(且 secret 始终脱敏);绝不能从这里下发给已授权的远程客户端。
23
- delete prefs.dingtalk;
29
+ stripImConfigs(prefs); // dingtalk / feishu / admin-only, never to a LAN client
24
30
  prefs.logDir = LOG_DIR; // 始终返回当前运行时的日志目录
25
31
  // home-friendly 展示形态:设了 CLAUDE_CONFIG_DIR 的用户看到真实路径,默认用户看到 "~/.claude"
26
32
  // join() 而非字符串拼接,避免 Windows 分隔符不匹配导致比较失败
@@ -46,8 +52,9 @@ function preferencesPost(req, res, parsedUrl, isLocal, deps) {
46
52
  // 任意项目的覆盖,绕过 admin 门禁。
47
53
  delete incoming.auth;
48
54
  delete incoming.authByProject;
49
- // dingtalk 同理:只能经 admin-only 的 /api/dingtalk/config 修改,禁止借 /api/preferences 植入凭据。
50
- delete incoming.dingtalk;
55
+ // IM bridge configs 同理:只能经 admin-only 的 /api/dingtalk/config、/api/im/* 修改,禁止借
56
+ // /api/preferences 植入凭据。
57
+ stripImConfigs(incoming);
51
58
  // 如果修改了日志目录,先切换再保存到新位置(新目录下生成 preferences.json)
52
59
  if (incoming.logDir && typeof incoming.logDir === 'string') {
53
60
  setLogDir(incoming.logDir);
@@ -102,7 +109,7 @@ function preferencesPost(req, res, parsedUrl, isLocal, deps) {
102
109
  // 已授权的远程客户端。磁盘上的值已在上面写入,这里只清内存对象供响应用。
103
110
  delete prefs.auth;
104
111
  delete prefs.authByProject;
105
- delete prefs.dingtalk;
112
+ stripImConfigs(prefs);
106
113
  prefs.logDir = LOG_DIR;
107
114
  res.writeHead(200, { 'Content-Type': 'application/json' });
108
115
  res.end(JSON.stringify(prefs));
package/server/server.js CHANGED
@@ -31,8 +31,13 @@ import { askPermRoutes } from './routes/ask-perm.js';
31
31
  import { teamRoutes } from './routes/team.js';
32
32
  import { authRoutes } from './routes/auth.js';
33
33
  import { dingtalkRoutes } from './routes/dingtalk.js';
34
- import * as dingtalkBridge from './lib/dingtalk-bridge.js';
35
- import { loadDingTalkConfig } from './lib/dingtalk-config.js';
34
+ import { imRoutes } from './routes/im.js';
35
+ import * as imCore from './lib/im-bridge-core.js';
36
+ import './lib/adapters/dingtalk-adapter.js'; // side-effect: registers the DingTalk adapter
37
+ import './lib/adapters/feishu-adapter.js'; // side-effect: registers the Feishu adapter
38
+ import './lib/adapters/wecom-adapter.js'; // side-effect: registers the WeCom adapter
39
+ import './lib/adapters/discord-adapter.js'; // side-effect: registers the Discord adapter
40
+ import { loadConfig } from './lib/im-config.js';
36
41
 
37
42
  const execFileAsync = promisify(execFile);
38
43
  const execAsync = promisify(exec);
@@ -475,11 +480,17 @@ const deps = {
475
480
  return authConfig;
476
481
  },
477
482
  // Constants local to server.js.
478
- // DingTalk bridge admin surface (config route → bridge lifecycle).
483
+ // Generic IM bridge admin surface (config routes → bridge lifecycle), keyed by platform id.
484
+ im: {
485
+ getBridgeStatus: (id) => imCore.getBridgeStatus(id),
486
+ reloadBridge: (id) => imCore.reloadBridge(id),
487
+ testConnection: (id, cfg) => imCore.testConnection(id, cfg),
488
+ },
489
+ // DingTalk back-compat alias over the generic IM core (the unchanged dingtalk route uses this).
479
490
  dingtalk: {
480
- getBridgeStatus: () => dingtalkBridge.getBridgeStatus(),
481
- reloadBridge: () => dingtalkBridge.reloadBridge(),
482
- testConnection: (cfg) => dingtalkBridge.testConnection(cfg),
491
+ getBridgeStatus: () => imCore.getBridgeStatus('dingtalk'),
492
+ reloadBridge: () => imCore.reloadBridge('dingtalk'),
493
+ testConnection: (cfg) => imCore.testConnection('dingtalk', cfg),
483
494
  },
484
495
  ACCESS_TOKEN,
485
496
  INTERNAL_TOKEN,
@@ -520,6 +531,7 @@ const _routes = [
520
531
  ...askPermRoutes,
521
532
  ...teamRoutes,
522
533
  ...dingtalkRoutes,
534
+ ...imRoutes,
523
535
  ];
524
536
  const dispatch = createDispatcher(_routes);
525
537
 
@@ -902,20 +914,21 @@ export async function startViewer() {
902
914
  resolveSdkApproval: (...args) => _sdkResolveApproval?.(...args),
903
915
  },
904
916
  });
905
- // DingTalk Stream bridge: only meaningful in CLI mode (where the singleton PTY
906
- // lives). startBridge saves deps then no-ops unless enabled+creds present, so
907
- // calling it unconditionally also primes reloadBridge() for later enable-via-UI.
917
+ // IM bridges (DingTalk, Feishu, …): only meaningful in CLI mode (where the singleton
918
+ // PTY lives). startAll primes each adapter's deps then no-ops unless enabled+creds
919
+ // present, so calling it unconditionally also primes reloadBridge() for later
920
+ // enable-via-UI. The PTY deps are shared across platforms; getConfig is per-platform.
908
921
  if (isCliMode) {
909
922
  const pmb = await import('./pty-manager.js');
910
- dingtalkBridge.startBridge({
923
+ await imCore.startAll((id) => ({
911
924
  writeToPty: pmb.writeToPty,
912
925
  writeToPtySequential: pmb.writeToPtySequential,
913
926
  getPtyState: pmb.getPtyState,
914
927
  getPtyKind: pmb.getPtyKind,
915
928
  getPtySkipPermissions: pmb.getPtySkipPermissions,
916
929
  isStreaming: () => streamingState.active,
917
- getConfig: () => loadDingTalkConfig(),
918
- });
930
+ getConfig: () => loadConfig(id),
931
+ }));
919
932
  }
920
933
  resolve(server);
921
934
  });
@@ -1621,9 +1634,9 @@ function _emitTurnEnd(sessionId, ts, transcriptPath = null) {
1621
1634
  if (clients.length > 0 && sendEventToClients) {
1622
1635
  sendEventToClients(clients, 'turn_end', { sessionId: sid, ts: t });
1623
1636
  }
1624
- // Forward the (clean) assistant reply for this turn to DingTalk, if the bridge is live.
1625
- // Fire-and-forget: a bridge failure must never affect SSE broadcast.
1626
- try { dingtalkBridge.notifyTurnEnd(sid, t, transcriptPath); } catch { /* best-effort */ }
1637
+ // Forward the (clean) assistant reply for this turn to whichever IM bridge owns the in-flight
1638
+ // turn, if any. Fire-and-forget: a bridge failure must never affect SSE broadcast.
1639
+ try { imCore.notifyTurnEnd(sid, t, transcriptPath); } catch { /* best-effort */ }
1627
1640
  if (typeof _onTurnEndBroadcastForTests === 'function') {
1628
1641
  try { _onTurnEndBroadcastForTests({ sessionId: sid, ts: t }); }
1629
1642
  catch (e) { if (process.env.NODE_ENV === 'test') throw e; /* prod 不让测试桩污染 */ }
@@ -1758,9 +1771,9 @@ async function _doStop() {
1758
1771
  // 对称 startViewer:下一次启动后第一次 active 才算 rising edge
1759
1772
  _lastSdkActive = false;
1760
1773
  _lastCliActive = false;
1761
- // Tear down the DingTalk Stream connection so a stop/start cycle (Electron tab switch,
1762
- // tests) never leaks a second WS to the same app_key. Idempotent + swallows errors.
1763
- try { await dingtalkBridge.stopBridge(); } catch { }
1774
+ // Tear down all IM bridge connections so a stop/start cycle (Electron tab switch, tests) never
1775
+ // leaks a second WS to the same app. Idempotent + swallows errors.
1776
+ try { await imCore.stopAll(); } catch { }
1764
1777
  try { await Promise.race([runParallelHook('serverStopping'), new Promise(r => setTimeout(r, 3000))]); } catch { }
1765
1778
  // 如果用户未做选择,将临时文件转为正式文件
1766
1779
  if (_resumeState && _resumeState.tempFile) {
@@ -1 +0,0 @@
1
- ._liveTag_sg7sp_3{position:relative;display:inline-flex;align-items:center;justify-content:flex-start;border-radius:999px;border:1px solid;border-color:var(--ctx-color);color:var(--ctx-color);padding:0 10px;height:100%;font-size:12px;line-height:1;overflow:hidden;transition:border-color .3s,color .3s;white-space:nowrap;background:var(--bg-base-pure)}._liveTagFill_sg7sp_23{position:absolute;left:0;top:0;bottom:0;width:var(--ctx-percent, 0);background-color:var(--ctx-color);opacity:.35;transition:width .5s ease,background-color .3s;pointer-events:none}._liveTagContent_sg7sp_36{position:relative;z-index:1;display:inline-flex;align-items:center}._liveTagHistory_sg7sp_44{background:var(--bg-surface);border-color:var(--border-light);color:var(--text-primary)}._liveTagText_sg7sp_51{margin-left:4px;font-variant-numeric:tabular-nums}._cachePopoverPlaceholder_sg7sp_57{min-width:300px}._editButton_31hdb_10{display:inline-flex;align-items:center;justify-content:center;vertical-align:middle;background:transparent;border:none;padding:0 2px;margin-left:4px;cursor:pointer;font-size:inherit;line-height:1;color:var(--text-secondary, #888);opacity:0;border-radius:3px;transition:opacity .12s ease,color .12s ease,background-color .12s ease}._editButton_31hdb_10 .anticon{display:inline-flex;align-items:center;line-height:1;position:relative;top:-1px}._editButton_31hdb_10:hover,._editButton_31hdb_10:focus-visible{opacity:1;color:var(--text-primary, #333);background-color:var(--bg-hover, rgba(0, 0, 0, .04));outline:none}._editButton_31hdb_10:focus-visible{box-shadow:0 0 0 2px var(--focus-ring, rgba(64, 158, 255, .4))}._footer_31hdb_55{display:flex;justify-content:space-between;align-items:center;gap:8px}._footerLeft_31hdb_61,._footerRight_31hdb_65{display:flex;gap:8px}._projectNameRow_31hdb_70{display:flex;align-items:center;gap:8px;margin-bottom:12px;padding:6px 10px;background:var(--bg-secondary, rgba(0, 0, 0, .03));border-radius:4px;font-size:12px}._projectNameLabel_31hdb_80{color:var(--text-secondary, #888)}._projectNameValue_31hdb_83{color:var(--text-primary, #333);font-family:var(--font-mono, ui-monospace, SFMono-Regular, monospace);word-break:break-all}._panel_ziacd_1{display:flex;flex-direction:column;gap:14px}._required_ziacd_7{margin-left:2px;color:var(--color-error-light, #ff7b7b)}._optional_ziacd_12{margin-left:6px;font-size:12px;font-weight:400;color:var(--text-secondary)}._row_ziacd_19{display:flex;align-items:center;justify-content:space-between;gap:12px}._label_ziacd_26{font-size:14px;font-weight:500}._control_ziacd_31{display:inline-flex;align-items:center;gap:10px}._field_ziacd_37{display:flex;flex-direction:column;gap:6px}._fieldLabel_ziacd_43{font-size:13px;color:var(--text-secondary)}._help_ziacd_48{font-size:12px;color:var(--text-secondary);line-height:1.4}._warn_ziacd_54{font-size:12px;line-height:1.5;color:var(--color-error-light, #ff7b7b)}._hint_ziacd_60{font-size:12px;line-height:1.5;color:var(--text-secondary)}._detailsToggle_ziacd_66{display:inline-flex;align-items:center;gap:5px;align-self:flex-start;padding:0;border:none;background:none;cursor:pointer;font-size:12px;color:var(--text-secondary)}._detailsToggle_ziacd_66:hover{color:var(--text-primary, var(--text-secondary))}._details_ziacd_66{display:flex;flex-direction:column;gap:10px}._actions_ziacd_89{display:flex;justify-content:flex-end;gap:10px;margin-top:4px}._tabRow_14vqz_4{display:flex;align-items:flex-end;gap:6px;padding:0 8px;flex-wrap:nowrap;overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none}._tabRow_14vqz_4::-webkit-scrollbar{display:none}._tabBtn_14vqz_24{display:inline-flex;align-items:center;gap:7px;padding:8px 16px;font-size:14px;line-height:1.5;border:1px solid var(--border-primary);border-radius:10px;background:transparent;color:var(--text-secondary);cursor:pointer;transition:color .18s,border-color .18s,background .18s;white-space:nowrap;flex-shrink:0;-webkit-user-select:none;user-select:none}._tabBtn_14vqz_24:hover{border-color:var(--color-primary);color:var(--color-primary)}._tabBtn_14vqz_24._tabBtnActive_14vqz_52,._tabBtn_14vqz_24._tabBtnActive_14vqz_52:hover{background:var(--bg-container);color:var(--color-primary);border-color:var(--border-primary);border-bottom:2px solid var(--bg-container);border-radius:10px 10px 0 0;font-weight:500;margin-bottom:-1px;position:relative;z-index:2}._toolBody_14vqz_8{border:1px solid var(--border-primary);border-top:none;border-radius:8px;background:var(--bg-container);padding:18px 20px;min-width:0}[data-theme=light] ._toolBody_14vqz_8{box-shadow:0 3px 8px #00000014}[data-theme=light] ._tabBtnActive_14vqz_52{box-shadow:0 -3px 8px #0000000f}[data-theme=dark] ._toolBody_14vqz_8 .ant-input,[data-theme=dark] ._toolBody_14vqz_8 .ant-input-affix-wrapper,[data-theme=dark] ._toolBody_14vqz_8 .ant-select-selector{background-color:var(--bg-elevated)}._chip_5hs7f_3{line-height:0;cursor:pointer;-webkit-user-select:none;user-select:none;transition:opacity .15s}._chip_5hs7f_3:hover{opacity:.75}._logo_5hs7f_14{display:block;transition:color .15s}._connected_5hs7f_20{color:#1677ff}._disconnected_5hs7f_24{color:var(--text-tertiary, #999)}._modelCard_1gpju_3{border:1px solid var(--border-secondary);border-radius:6px;padding:8px 10px;background:var(--bg-container)}._modelName_1gpju_9{font-size:13px;font-weight:600;color:var(--text-primary);margin-bottom:8px;padding-bottom:4px;border-bottom:1px solid var(--border-secondary)}._statsTable_1gpju_17{width:100%;border-collapse:collapse}._th_1gpju_21{padding:2px 12px;font-size:12px;font-family:monospace;white-space:nowrap;color:var(--text-tertiary);font-weight:400;text-align:right}._td_1gpju_30{padding:2px 12px;font-size:12px;font-family:monospace;white-space:nowrap;color:var(--text-primary);text-align:right}._label_1gpju_38{padding:2px 12px;font-size:12px;font-family:monospace;white-space:nowrap;color:var(--text-light);font-weight:400;text-align:left}._rowBorder_1gpju_47{border-bottom:1px solid var(--border-primary)}._rebuildTotalRow_1gpju_50{border-top:1px solid var(--border-light)}._rebuildTotalRow_1gpju_50 td{font-weight:600}._cachePopoverEmpty_1gpju_56{padding:8px 4px;color:var(--text-tertiary);font-size:13px}._toolChipGrid_1gpju_61{display:flex;flex-wrap:wrap;gap:4px;padding:2px 0 6px 2px}._cacheToolChip_1gpju_67{font-size:11px;padding:0 6px;border-radius:3px;background:var(--bg-surface);color:var(--text-secondary);line-height:18px;max-width:280px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border:1px solid var(--border-primary);cursor:help}._titleIcon_1gpju_81{margin-right:8px}._detailMarkdownCard_1gpju_86{border:1px solid var(--border-secondary);border-radius:6px;padding:8px 10px;background:var(--bg-container)}._memoryMarkdown_1gpju_93{font-size:12.5px;line-height:1.55;color:var(--text-primary);word-break:break-word}._memoryMarkdown_1gpju_93 p{margin:0 0 6px}._memoryMarkdown_1gpju_93 ul,._memoryMarkdown_1gpju_93 ol{margin:4px 0 6px;padding-left:20px}._memoryMarkdown_1gpju_93 li{margin:2px 0}._memoryMarkdown_1gpju_93 h1,._memoryMarkdown_1gpju_93 h2,._memoryMarkdown_1gpju_93 h3,._memoryMarkdown_1gpju_93 h4{font-size:13px;font-weight:600;margin:8px 0 4px;color:var(--text-primary)}._memoryMarkdown_1gpju_93 h1{font-size:14px}._memoryMarkdown_1gpju_93 a{color:var(--primary-color, #1677ff);text-decoration:none;cursor:pointer}._memoryMarkdown_1gpju_93 a:hover{text-decoration:underline}._memoryMarkdown_1gpju_93 code{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:12px;padding:1px 4px;border-radius:3px;background:var(--bg-surface);color:var(--text-primary)}._memoryMarkdown_1gpju_93 pre{margin:6px 0;padding:8px 10px;border-radius:4px;background:var(--bg-surface);overflow-x:auto}._memoryMarkdown_1gpju_93 pre code{padding:0;background:transparent;font-size:12px}._memoryMarkdown_1gpju_93 blockquote{margin:4px 0;padding:2px 8px;border-left:3px solid var(--border-hover);color:var(--text-secondary)}._memoryMarkdown_1gpju_93 hr{margin:8px 0;border:none;border-top:1px solid var(--border-secondary)}._headerBar_1d8jv_2{display:flex;align-items:center;justify-content:space-between;width:100%;height:100%}._logoWrap_1d8jv_10{display:inline-flex;align-items:center;position:relative;margin-top:14px}._logoWrapActive_1d8jv_17:after{content:"";position:absolute;top:-10px;bottom:-10px;left:0;right:-200px}._logoImage_1d8jv_26{height:24px;width:24px;border-radius:3px;vertical-align:middle;opacity:.75;transition:opacity .2s;cursor:pointer}._logoImageActive_1d8jv_36{opacity:1}._compactBtn_1d8jv_43{font-size:12px;height:30px;display:inline-flex;align-items:center;justify-content:center}._compactBtnNoBorder_1d8jv_52{width:30px;height:30px;min-width:30px;padding:0;border:none;font-size:18px;line-height:1;display:inline-flex;align-items:center;justify-content:center}._compactBtnNoBorder_1d8jv_52 .anticon{display:inline-flex;align-items:center;justify-content:center;line-height:0}._headerProjectName_1d8jv_79{font-size:12px;color:inherit;white-space:nowrap}._headerProjectName_1d8jv_79:hover [data-alias-edit-trigger],._headerProjectName_1d8jv_79:focus-within [data-alias-edit-trigger]{opacity:.55}._countdownStrong_1d8jv_96{font-variant-numeric:tabular-nums}._qrcodePopover_1d8jv_101{display:flex;flex-direction:column;align-items:center;padding:8px}._qrcodeSection_1d8jv_108{display:flex;flex-direction:column;align-items:center;padding:16px;margin-bottom:12px;border:1px solid var(--border-secondary);border-radius:8px;background:var(--bg-container)}._qrcodeTitle_1d8jv_119{font-size:14px;font-weight:600;color:var(--text-primary);margin-bottom:12px}._qrcodeUrlInput_1d8jv_126{margin-top:12px;font-size:12px;font-family:Menlo,Monaco,monospace}._qrcodeUrlCopy_1d8jv_132{cursor:pointer;color:var(--text-tertiary);transition:color .2s}._qrcodeUrlCopy_1d8jv_132:hover{color:var(--color-primary-light)}._authSection_1d8jv_143{display:flex;flex-direction:column;align-items:stretch;box-sizing:border-box;width:100%;margin-top:12px;padding:12px;border:1px solid var(--border-secondary);border-radius:8px;background:var(--bg-container);gap:8px}._authHeaderRow_1d8jv_157{display:flex;align-items:center;justify-content:space-between}._authTitle_1d8jv_163{font-size:13px;font-weight:600;color:var(--text-primary)}._authPasswordLabel_1d8jv_169{font-size:12px;color:var(--text-secondary)}._authPasswordInput_1d8jv_174{font-size:12px;font-family:Menlo,Monaco,monospace}._authSaveBtn_1d8jv_179{align-self:flex-end}._authEmptyWarn_1d8jv_183{font-size:12px;color:var(--color-error-light);line-height:1.4}._settingsGroupBox_1d8jv_191{border:1px solid var(--border-secondary);border-radius:8px;background:var(--bg-container);padding:4px 16px;margin-bottom:12px}._settingsGroupTitle_1d8jv_199{font-size:14px;font-weight:600;color:var(--text-primary);padding:12px 0 4px;border-bottom:1px solid var(--border-secondary)}._settingsItem_1d8jv_208{display:flex;justify-content:space-between;align-items:center;padding:12px 0}._settingsLabel_1d8jv_215{font-size:14px}._settingsHelpIcon_1d8jv_220{font-size:16px;color:var(--text-disabled);cursor:help}._settingsDivider_1d8jv_226{border-top:1px solid var(--border-primary);margin:12px 0}._logDirInput_1d8jv_231{margin-top:8px;background:var(--bg-base-alt);border-color:var(--border-light);color:var(--text-primary);font-family:monospace;font-size:13px}._tokenStatsEmpty_1d8jv_241{padding:8px 4px;color:var(--text-tertiary);font-size:13px}._tokenStatsContainer_1d8jv_248{display:flex;gap:12px;align-items:flex-start}._tokenStatsColumn_1d8jv_254{min-width:240px}._toolStatsColumn_1d8jv_258{min-width:180px}._modelCardSpaced_1d8jv_264{margin-bottom:10px}._rebuildCard_1d8jv_273{border:1px solid var(--border-secondary);border-radius:6px;padding:8px 10px;margin-top:10px;background:var(--bg-container)}._promptExportBar_1d8jv_282{margin-bottom:12px}._promptScrollArea_1d8jv_286{max-height:500px;overflow:auto}._promptEmpty_1d8jv_291{color:var(--text-tertiary);padding:12px}._promptTimestamp_1d8jv_297{color:var(--text-muted);font-size:12px;margin:12px 0 4px;padding-bottom:6px}._textPromptCard_1d8jv_305{margin:4px 0;background:var(--bg-container);border-radius:6px;border:1px solid var(--border-secondary);padding:10px 14px}._preText_1d8jv_314{white-space:pre-wrap;word-break:break-word;font-size:13px;line-height:1.6;color:var(--text-primary);margin:4px 0}._systemCollapse_1d8jv_324{margin:4px 0;background:var(--bg-elevated);border:1px solid var(--border-secondary);border-radius:6px}._systemLabel_1d8jv_331{color:var(--text-tertiary);font-size:12px}._preSys_1d8jv_336{white-space:pre-wrap;word-break:break-word;font-size:12px;line-height:1.5;color:var(--text-tertiary);margin:0}._promptTextarea_1d8jv_346{box-sizing:border-box;background:var(--bg-base-pure);width:100%;min-height:400px;color:var(--text-primary);font-family:monospace;font-size:13px;line-height:1.6;border:none;resize:vertical;padding:10px 14px;outline:none}._projectStatsCenter_1d8jv_362{display:flex;justify-content:center;padding:40px 0}._projectStatsEmpty_1d8jv_368{color:var(--text-tertiary);padding:40px 0;text-align:center;font-size:13px}._projectStatsContent_1d8jv_375{display:flex;flex-direction:column;gap:16px}._projectStatsUpdated_1d8jv_381{color:var(--text-muted);font-size:12px;text-align:right}._projectStatsSummary_1d8jv_387{display:grid;grid-template-columns:1fr 1fr;gap:10px}._projectStatCard_1d8jv_393{background:var(--bg-container);border:1px solid var(--border-secondary);border-radius:8px;padding:14px 12px;text-align:center}._projectStatValue_1d8jv_401{font-size:22px;font-weight:700;color:var(--text-primary);font-family:monospace;font-variant-numeric:tabular-nums}._projectStatLabel_1d8jv_409{font-size:12px;color:var(--text-tertiary);margin-top:4px}._projectStatsSection_1d8jv_415{display:flex;flex-direction:column;gap:10px}._projectStatsSectionTitle_1d8jv_421{font-size:14px;font-weight:600;color:var(--text-secondary);padding-bottom:4px;border-bottom:1px solid var(--border-secondary)}._projectStatsModelCard_1d8jv_429{background:var(--bg-container);border:1px solid var(--border-secondary);border-radius:6px;padding:10px 12px}._projectStatsModelHeader_1d8jv_436{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;padding-bottom:4px;border-bottom:1px solid var(--border-secondary)}._projectStatsModelName_1d8jv_445{font-size:13px;font-weight:600;color:var(--text-primary)}._projectStatsModelCount_1d8jv_451{font-size:12px;color:var(--text-tertiary);font-family:monospace}._cacheCopyBtn_1d8jv_459{font-size:14px;color:var(--text-tertiary);cursor:pointer;transition:color .2s;margin-left:8px}._cacheCopyBtn_1d8jv_459:hover{color:var(--text-primary)}._cacheTokenInfo_1d8jv_471{display:flex;align-items:center;font-size:12px;font-family:monospace;color:var(--text-light);margin-bottom:8px}._cacheCodeBlock_1d8jv_484{white-space:pre-wrap;word-break:break-word;font-size:12px;line-height:1.5;color:var(--text-secondary);background:var(--bg-container);border:1px solid var(--border-primary);border-radius:4px;padding:8px;margin:4px 0;font-family:Menlo,Monaco,monospace}._cacheCodeBlockSystem_1d8jv_498{white-space:pre-wrap;word-break:break-word;font-size:12px;line-height:1.5;color:var(--text-secondary);background:var(--bg-code-system);border:1px solid var(--border-code-system);border-radius:4px;padding:8px;margin:4px 0;font-family:Menlo,Monaco,monospace}._cacheNavBtn_1d8jv_512{margin-left:auto;font-size:11px;color:var(--color-primary);cursor:pointer;border:1px solid var(--color-primary);border-radius:3px;padding:1px 6px;white-space:nowrap}._cacheNavBtn_1d8jv_512:hover{background:var(--color-primary-bg-light)}._cacheNavList_1d8jv_527{width:600px;max-height:300px;overflow-y:auto}._cacheNavItem_1d8jv_533{padding:4px 8px;font-size:12px;color:var(--text-secondary);cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;border-radius:3px}._cacheNavItem_1d8jv_533:hover{background:var(--color-primary-bg-lighter);color:var(--text-white)}._cacheBlockHighlight_1d8jv_549{box-shadow:0 0 10px var(--color-primary-shadow);transition:box-shadow .2s ease-in}._cacheBlockHighlightFading_1d8jv_554{box-shadow:0 0 10px transparent;transition:box-shadow 3s ease-out}._cacheBorderSvg_1d8jv_559{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;pointer-events:none;overflow:visible}._cacheBorderSvgFading_1d8jv_568{opacity:0;transition:opacity 3s ease-out}._cacheBorderRect_1d8jv_573{animation:_cacheDashRotate_1d8jv_1 4s linear infinite}@keyframes _cacheDashRotate_1d8jv_1{0%{stroke-dashoffset:0}to{stroke-dashoffset:-100}}._thLeft_1d8jv_583{text-align:left}._cacheWriteToken_1d8jv_598{color:var(--color-code-orange)}._cacheReadToken_1d8jv_602{color:var(--color-success)}._cacheCtxPercent_1d8jv_606{color:var(--text-tertiary);margin-left:6px}._qrcodeIcon_1d8jv_626{width:30px;height:30px;padding:6px;box-sizing:border-box;color:var(--text-secondary);cursor:pointer;border-radius:6px;transition:color .15s ease,background-color .15s ease;display:inline-flex;align-items:center;justify-content:center;vertical-align:middle;background:none;border:none}._qrcodeIcon_1d8jv_626:hover{color:var(--text-primary);background:var(--bg-hover, rgba(0, 0, 0, .04))}._qrcodeIcon_1d8jv_626:focus-visible{outline:2px solid var(--primary-color, #1677ff);outline-offset:1px}._approvalBell_1d8jv_652{position:relative;width:30px;height:30px;padding:6px;box-sizing:border-box;color:var(--color-warning, #faad14);cursor:pointer;border-radius:6px;transition:color .15s ease,background-color .15s ease;display:inline-flex;align-items:center;justify-content:center;vertical-align:middle;background:none;border:none}._approvalBell_1d8jv_652:hover{color:var(--text-primary);background:var(--bg-hover, rgba(0, 0, 0, .04))}._approvalBell_1d8jv_652:focus-visible{outline:2px solid var(--primary-color, #1677ff);outline-offset:1px}._approvalBellBadge_1d8jv_677{position:absolute;top:0;right:0;min-width:14px;height:14px;padding:0 3px;box-sizing:border-box;background:var(--color-error, #ff4d4f);color:#fff;font-size:9px;line-height:14px;border-radius:7px;text-align:center;font-weight:600}._proxySwapIcon_1d8jv_694{margin-right:4px;font-size:11px}._proxyProfileTag_1d8jv_700{border-radius:12px;background:var(--border-secondary);border:1px solid var(--border-light);color:var(--text-tertiary);font-size:12px;cursor:pointer;transition:color .2s,border-color .2s}._proxyProfileTag_1d8jv_700:hover{color:var(--text-secondary);border-color:var(--text-disabled)}._themeToggle_1d8jv_719{position:relative;box-sizing:border-box;display:inline-flex;vertical-align:middle;width:56px;height:30px;border-radius:15px;border:1px solid var(--border-hover);cursor:pointer;padding:0;overflow:hidden;transition:background-color .2s ease,border-color .2s ease;flex-shrink:0;outline:none}._themeToggle_1d8jv_719:focus-visible{box-shadow:0 0 0 2px var(--primary-color, #1677ff)}._themeToggle_1d8jv_719[data-theme=light]{background:linear-gradient(135deg,#eaf2fa,#d9e4ef);border-color:#c8d4e0}._themeToggle_1d8jv_719[data-theme=dark]{background:#1a1a1a;border-color:#2a2a2a}._themeToggleKnob_1d8jv_747{position:absolute;top:50%;left:2px;width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;transform:translateY(-50%);transition:transform .28s cubic-bezier(.4,.2,.2,1),background-color .2s ease,color .2s ease,box-shadow .2s ease;pointer-events:none}._themeToggle_1d8jv_719[data-theme=light] ._themeToggleKnob_1d8jv_747{background:#fff;color:#8fa2b7;box-shadow:0 1px 3px #1e32502e}._themeToggle_1d8jv_719[data-theme=dark] ._themeToggleKnob_1d8jv_747{transform:translate(28px,-50%);background:#2a2a2a;color:#e8e8e8;box-shadow:0 1px 3px #0006}._themeToggleIcon_1d8jv_777{display:block}._headerRightRow_1d8jv_782 .ant-space-item{display:inline-flex;align-items:center}._headerCountdownTag_1d8jv_788{height:30px;margin:0;padding:0 10px;display:inline-flex;align-items:center;background:var(--bg-surface);border:1px solid var(--border-hover);border-radius:6px;line-height:1}._centerEmpty_midza_1{display:flex;align-items:center;justify-content:center;height:100%}._scrollContainer_midza_8{overflow:auto;height:100%;-webkit-overflow-scrolling:touch;will-change:scroll-position}._listItem_midza_15{cursor:pointer;padding:8px 12px;border-left:6px solid transparent;border-right:1px solid var(--border-primary);border-top:1px solid transparent;border-bottom:1px solid var(--border-primary);transition:background .15s}._listItem_midza_15:hover{border-left-color:var(--border-hover)}._listItemActive_midza_29{background:var(--color-primary-bg-faint);border-left-color:var(--color-primary-light);border-right:1px solid var(--color-primary-light);border-top:1px solid var(--color-primary-light);border-bottom:1px solid var(--color-primary-light)}._listItemActive_midza_29,._listItemActive_midza_29:hover{background:var(--color-primary-bg-faint);border-left-color:var(--color-primary-light);border-right-color:var(--color-primary-light);border-top-color:var(--color-primary-light);border-bottom-color:var(--color-primary-light)}._itemContent_midza_46{width:100%;min-width:0}._itemHeader_midza_51{display:flex;align-items:center;gap:6px;margin-bottom:4px;font-size:12px}._tagNoMargin_midza_59{margin:0;font-size:12px}._modelName_midza_64{font-size:12px;color:var(--text-tertiary)}._modelNameMain_midza_69{color:var(--color-code-orange)}._time_midza_73{font-size:12px;color:var(--text-gray);margin-left:auto}._detailRow_midza_79{display:flex;gap:8px;font-size:12px;align-items:center}._urlText_midza_86{color:var(--text-disabled);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}._duration_midza_95{color:var(--text-gray);flex-shrink:0}._statusOk_midza_100{color:var(--color-success);opacity:.5;flex-shrink:0}._statusErr_midza_106{color:var(--color-error);flex-shrink:0}._statusDefault_midza_111{color:var(--text-tertiary);flex-shrink:0}._usageBox_midza_116{background:var(--bg-container);border-radius:4px;padding:3px 6px;margin-top:4px;font-size:12px;color:var(--text-gray);line-height:1.6}._cacheDot_midza_126{display:inline-block;width:6px;height:6px;border-radius:50%;margin:0 3px;vertical-align:middle}._cacheDotLoss_midza_135{background-color:var(--color-red-dark-bg);cursor:help}._cacheDotNormal_midza_140{background-color:var(--border-hover)}._tagMainAgent_midza_144{color:var(--color-code-orange);border-color:var(--color-code-orange-border);background:var(--color-code-orange-bg)}._tagPlan_midza_150{color:var(--color-error-muted);border-color:var(--color-error-muted);background-color:var(--bg-base-pure)}._tagMuted_midza_156{color:var(--text-muted);border-color:var(--border-light);background-color:var(--bg-base-pure)}._tooltipPreLine_midza_162{white-space:pre-line}._GzYRV{line-height:1.2;white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-pre-wrap;white-space:-o-pre-wrap;word-wrap:break-word}._3eOF8{margin-right:5px;font-weight:700}._3eOF8+._3eOF8{margin-left:-5px}._1MFti{cursor:pointer}._f10Tu{font-size:1.2em;margin-right:5px;-webkit-user-select:none;-moz-user-select:none;user-select:none}._1UmXx:after{content:"▸"}._1LId0:after{content:"▾"}._1pNG9{margin-right:5px}._1pNG9:after{content:"...";font-size:.8em}._2IvMF{background:#eee}._2bkNM{margin:0;padding:0 10px}._1BXBN{margin:0;padding:0}._1MGIk{font-weight:600;margin-right:5px;color:#000}._3uHL6{color:#000}._2T6PJ,._1Gho6{color:#df113a}._vGjyY{color:#2a3f3c}._1bQdo{color:#0b75f5}._3zQKs{color:#469038}._1xvuR{color:#43413d}._oLqym,._2AXVT,._2KJWg{color:#000}._11RoI{background:#002b36}._17H2C,._3QHg2,._3fDAz{color:#fdf6e3}._2bSDX{font-weight:bolder;margin-right:5px;color:#fdf6e3}._gsbQL{color:#fdf6e3}._LaAZe,._GTKgm{color:#81b5ac}._Chy1W{color:#cb4b16}._2bveF{color:#d33682}._2vRm-{color:#ae81ff}._1prJR{color:#268bd2}._container_qeuid_1{background:var(--bg-container);border-radius:6px;border:1px solid var(--border-primary);padding:12px;font-size:13px;font-family:monospace;overflow:auto}._root_17dqd_1{display:flex;height:100%;min-height:0;gap:0}._sidebar_17dqd_9{width:220px;flex-shrink:0;border-right:1px solid var(--border-primary);overflow-y:auto;padding:4px 0;-webkit-overflow-scrolling:touch}._section_17dqd_18{-webkit-user-select:none;user-select:none}._sectionHeader_17dqd_22{display:flex;align-items:center;gap:6px;width:100%;padding:6px 10px;cursor:pointer;color:var(--text-primary);font-size:12px;font-weight:600;transition:background .15s;background:none;border:0;text-align:left;font:inherit}._sectionHeader_17dqd_22:hover{background:var(--overlay-light-faint)}._sectionHeader_17dqd_22:focus-visible{outline:1px solid var(--color-primary-outline);outline-offset:-1px}._arrow_17dqd_48{font-size:10px;color:var(--text-tertiary);flex-shrink:0}._sectionTitle_17dqd_54{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}._sectionCount_17dqd_62{font-size:11px;color:var(--text-muted);background:var(--bg-elevated);border-radius:10px;padding:0 6px;line-height:18px}._sectionBody_17dqd_71{padding:2px 0}._historyToggle_17dqd_76{display:flex;align-items:center;gap:6px;width:100%;padding:4px 10px 4px 14px;cursor:pointer;color:var(--text-muted);font-size:11px;transition:color .15s,background .15s;background:none;border:0;text-align:left;font:inherit}._historyToggle_17dqd_76:hover{color:var(--text-tertiary);background:var(--overlay-light-faint)}._historyToggle_17dqd_76:focus-visible{outline:1px solid var(--color-primary-outline);outline-offset:-1px}._historyToggleLabel_17dqd_102{flex:1}._item_17dqd_107{display:flex;align-items:center;justify-content:space-between;width:100%;padding:4px 9px 4px 23px;font-size:12px;color:var(--text-tertiary);cursor:pointer;transition:background .15s,color .15s;background:none;border:1px solid transparent;border-radius:4px;box-sizing:border-box;text-align:left;font:inherit}._item_17dqd_107:hover{background:var(--overlay-light-faint);color:var(--text-primary)}._item_17dqd_107:focus-visible{outline:1px solid var(--color-primary-outline);outline-offset:-1px}._itemActive_17dqd_135,._itemActive_17dqd_135:hover{background:var(--color-primary-bg-faint);color:var(--color-primary);border-color:var(--color-primary-light)}._itemContent_17dqd_142{flex:1;min-width:0;overflow:hidden}._itemLabel_17dqd_148{font-family:monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block}._itemSublabel_17dqd_156{font-size:10px;color:var(--text-disabled);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:1px}._itemTime_17dqd_165{font-size:9px;color:var(--text-disabled);flex-shrink:0;margin-left:4px;font-family:monospace}._content_17dqd_174{flex:1;min-width:0;overflow:auto;padding:12px 16px;-webkit-overflow-scrolling:touch}._contentEmpty_17dqd_182{height:100%;display:flex;align-items:center;justify-content:center}._contentInner_17dqd_189{padding-bottom:20px}._emptyWrap_17dqd_193{display:flex;align-items:center;justify-content:center;height:200px}._roleHeader_17dqd_201{display:flex;align-items:center;gap:8px;margin-bottom:8px;flex-wrap:nowrap}._roleBadge_17dqd_209{font-size:10px;font-weight:600;letter-spacing:.04em;padding:2px 7px;border-radius:4px;flex-shrink:0}._role_user_17dqd_218{background:var(--color-primary-bg-lighter);color:var(--color-primary-lighter);border:1px solid var(--color-primary-bg-medium)}._role_assistant_17dqd_224{background:var(--color-purple-bg);color:var(--color-code-purple);border:1px solid var(--color-purple-border)}._roleLabel_17dqd_230{font-size:11px;color:var(--text-muted);flex:1;min-width:0}._contentTime_17dqd_237{margin-left:auto;font-size:10px;color:var(--text-disabled);font-family:monospace;flex-shrink:0}._turnDivider_17dqd_245{border:none;border-top:1px solid var(--border-primary);margin:14px 0}._textBlock_17dqd_252{margin-bottom:8px;border:1px solid var(--border-primary);border-radius:6px;overflow:hidden}._textBlockBar_17dqd_259{display:flex;align-items:center;gap:6px;padding:4px 10px;background:var(--bg-container);border-bottom:1px solid var(--border-primary)}._textBlockBody_17dqd_268{padding:10px 12px;font-size:13px;line-height:1.7;color:var(--text-primary);word-break:break-word}._textBlockCompact_17dqd_276{position:relative;padding:8px 10px;font-size:12px;color:var(--text-light)}._textBlockCompactFloat_17dqd_283{float:right;margin-left:6px;margin-bottom:2px}._thinkingBlock_17dqd_290{margin-bottom:8px;border:1px solid var(--color-thinking-border);border-radius:6px;overflow:hidden;background:var(--color-thinking-bg)}._thinkingHeader_17dqd_298{display:flex;align-items:center;gap:6px;padding:5px 10px;cursor:pointer;font-size:12px;color:var(--text-tertiary);transition:background .15s}._thinkingHeader_17dqd_298:hover{background:var(--overlay-light-faint)}._thinkingPreview_17dqd_313{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-muted);font-size:11px}._thinkingBody_17dqd_323{padding:8px 12px;border-top:1px solid var(--color-thinking-border)}._toolBlock_17dqd_329{margin-bottom:8px;border:1px solid var(--border-primary);border-radius:6px;overflow:hidden}._toolBlockResult_17dqd_336{border-color:var(--color-green-border)}._toolBlockError_17dqd_340{border-color:var(--color-red-dark-border)}._toolBlockHeader_17dqd_344{display:flex;align-items:center;gap:8px;padding:5px 10px;background:var(--bg-container);border-bottom:1px solid var(--border-primary);font-size:12px;flex-wrap:wrap}._toolBlockBody_17dqd_355{padding:8px 10px;font-size:12px}._toolName_17dqd_360{color:var(--text-primary);font-weight:500;font-family:monospace}._toolId_17dqd_366{color:var(--text-disabled);font-size:10px;font-family:monospace;margin-left:auto}._errorLabel_17dqd_373{font-size:10px;color:var(--color-error-light);background:var(--color-error-bg-light);border:1px solid var(--color-error-border);border-radius:3px;padding:1px 5px}._blockTag_17dqd_383{font-size:9px;font-weight:600;letter-spacing:.05em;text-transform:uppercase;padding:1px 5px;border-radius:3px;background:var(--bg-elevated);color:var(--text-muted);border:1px solid var(--border-primary);flex-shrink:0}._blockTagText_17dqd_396{background:var(--color-primary-bg-faint);color:var(--color-primary-lighter);border-color:var(--color-primary-bg-lighter)}._blockTagThinking_17dqd_402{background:var(--color-warning-bg-faint);color:var(--color-warning);border-color:var(--color-warning-border-light)}._blockTagResult_17dqd_408{background:var(--color-green-dark-bg);color:var(--color-success);border-color:var(--color-green-dark-border)}._blockTagError_17dqd_414{background:var(--color-error-bg-faint);color:var(--color-error-light);border-color:var(--color-error-border-light)}._jsonBlock_17dqd_421{margin-bottom:8px;border:1px solid var(--border-primary);border-radius:6px;overflow:hidden}._jsonBlockLabel_17dqd_428{font-size:10px;color:var(--text-muted);padding:3px 10px;background:var(--bg-container);border-bottom:1px solid var(--border-primary);font-family:monospace}._blockSeparator_17dqd_438{border:none;border-top:1px solid var(--border-primary);margin:16px 0}._markdownBody_17dqd_445{font-size:13px;line-height:1.7;color:var(--text-primary);word-break:break-word}._container_rg6mx_1{height:100%;overflow:hidden;padding:0 16px;display:flex;flex-direction:column;background:var(--bg-base)}._emptyState_rg6mx_10{display:flex;align-items:center;justify-content:center;height:100%}._urlSection_rg6mx_17{padding:12px 0;border-bottom:1px solid var(--border-primary);display:flex;align-items:flex-start;flex-shrink:0}._urlLeft_rg6mx_25{flex:1;min-width:0}._tokenStatsBox_rg6mx_30{flex-shrink:0;padding-left:12px;display:flex;align-items:center}._tokenGrid_rg6mx_37{display:flex;border:1px solid var(--border-secondary);border-radius:6px;overflow:hidden;min-width:360px;font-size:11px;line-height:1.6}._tokenRows_rg6mx_47{flex:1}._tokenRow_rg6mx_47{display:flex}._tokenRowBorder_rg6mx_55{border-top:1px solid var(--border-secondary)}._tokenLabel_rg6mx_59{color:var(--text-tertiary);padding:4px 8px;white-space:nowrap;font-weight:600}._tokenTd_rg6mx_66{flex:1;color:var(--text-primary);text-align:right;padding:4px 8px;font-family:monospace;white-space:nowrap}._tokenHitRate_rg6mx_75{display:flex;flex-direction:column;align-items:center;justify-content:center;color:var(--text-primary);padding:4px 8px;font-family:monospace;white-space:nowrap;border-left:1px solid var(--border-secondary);min-width:100px}._tokenHitRateLabel_rg6mx_88{color:var(--text-tertiary);font-size:10px;font-family:sans-serif}._tokenRowBorder_rg6mx_55 td{border-top:1px solid var(--border-secondary)}._urlText_rg6mx_98{color:var(--text-primary);font-size:13px;margin-bottom:8px;word-break:break-all}._metaText_rg6mx_105,._headersContainer_rg6mx_109{font-size:12px}._headerRow_rg6mx_113{display:flex;padding:4px 0;border-bottom:1px solid var(--border-primary)}._headerKey_rg6mx_119{min-width:200px;flex-shrink:0}._headerValue_rg6mx_124{word-break:break-all;margin-left:8px}._streamingBox_rg6mx_129{padding:20px;background:var(--bg-elevated);border-radius:6px;border:1px solid var(--border-primary)}._bodyToolbar_rg6mx_136{display:flex;gap:8px;margin-bottom:8px}._rawTextPre_rg6mx_142{background:var(--bg-code-dark);border:1px solid var(--border-primary);border-radius:6px;padding:12px;font-size:12px;color:var(--text-primary);overflow:auto;max-height:600px;white-space:pre-wrap;word-break:break-all}._tabContent_rg6mx_155{padding:16px 0 0;height:100%;overflow-y:auto;-webkit-overflow-scrolling:touch}._collapseSpacing_rg6mx_163{margin-bottom:16px}._bodyLabel_rg6mx_167{margin:0}._bodyHeader_rg6mx_171{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}._diffSection_rg6mx_178{margin-bottom:16px}._diffToggle_rg6mx_182{display:inline-block;margin-bottom:8px;cursor:pointer}._diffIcon_rg6mx_188{font-size:12px;margin-left:4px}._viewInChatBtn_rg6mx_193{display:inline-flex;align-items:center;height:26px;border-radius:13px;border:1px solid var(--border-light);background:#0000;color:var(--text-tertiary);cursor:pointer;font-size:12px;transition:all .2s;padding:0 10px;white-space:nowrap}._viewInChatBtn_rg6mx_193:hover{border-color:var(--text-muted);color:var(--text-primary);background:var(--overlay-light-faint)}._reminderSelect_rg6mx_214{min-width:140px;font-size:12px}._reminderSelect_rg6mx_214 .ant-select-selector.ant-select-selector{border-radius:2px;border-color:var(--border-light);background:#0000;min-height:26px;height:auto;padding:0 8px;font-family:monospace}._reminderSelect_rg6mx_214 .ant-select-selection-placeholder{font-size:11px}._diffHeaderRow_rg6mx_233{display:flex;align-items:center;gap:8px}._diffSpaceRight_rg6mx_239{margin-left:auto}._reminderFilterWrapper_rg6mx_243{display:inline-flex;align-items:center;gap:4px}._reminderLabel_rg6mx_249{color:var(--text-tertiary);font-size:12px;font-family:monospace}._cacheTabContent_rg6mx_255{padding-top:0;overflow:hidden}._userPromptList_rg6mx_260{width:600px;max-height:300px;overflow-y:auto}._userPromptItem_rg6mx_266{padding:4px 8px;font-size:12px;color:var(--text-secondary);cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;border-radius:3px}._userPromptNavBtn_rg6mx_277{margin-left:auto;font-size:11px;color:var(--color-primary);cursor:pointer;border:1px solid var(--color-primary);border-radius:3px;padding:1px 6px;white-space:nowrap}._cacheContent_rg6mx_288{padding:8px 0;height:100%;display:flex;flex-direction:column}._cacheTokenBar_rg6mx_295{display:flex;align-items:center;font-size:12px;font-family:monospace;color:var(--text-light);margin-bottom:12px;flex-shrink:0}._cacheTokenWrite_rg6mx_305{color:var(--color-code-orange)}._cacheTokenRead_rg6mx_309{color:var(--color-success)}._cacheCopyIcon_rg6mx_313{margin-left:8px;cursor:pointer;color:var(--text-tertiary);transition:color .2s}._cacheScrollArea_rg6mx_320{flex:1;overflow-y:auto;min-height:0}._cacheSectionBlock_rg6mx_326{margin-bottom:12px}._cacheSectionHeader_rg6mx_330{font-size:13px;font-weight:600;color:var(--text-primary);margin-bottom:6px;cursor:pointer;-webkit-user-select:none;user-select:none;display:flex;align-items:center;gap:4px}._cacheCollapseArrow_rg6mx_342{display:inline-block;transition:transform .2s;font-size:10px}._cachePre_rg6mx_348{white-space:pre-wrap;word-break:break-word;font-size:12px;line-height:1.5;color:var(--text-secondary);background:var(--bg-container);border:1px solid var(--border-primary);border-radius:4px;padding:8px;margin:4px 0;font-family:Menlo,Monaco,monospace}._cacheHighlightSvg_rg6mx_362{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;pointer-events:none;overflow:visible}._cachePreSystem_rg6mx_371{white-space:pre-wrap;word-break:break-word;font-size:12px;line-height:1.5;color:var(--text-secondary);background:var(--bg-code-system);border:1px solid var(--border-code-system);border-radius:4px;padding:8px;margin:4px 0;font-family:Menlo,Monaco,monospace}._container_rg6mx_1 .ant-tabs>.ant-tabs-nav{margin-bottom:0}._container_rg6mx_1 .ant-tabs{flex:1;min-height:0;display:flex;flex-direction:column}._container_rg6mx_1 .ant-tabs>.ant-tabs-content-holder{flex:1;min-height:0}._container_rg6mx_1 .ant-tabs-content,._container_rg6mx_1 .ant-tabs-tabpane-active{height:100%}._resizer_pzn4b_1{width:6px;cursor:col-resize;background:var(--bg-elevated);flex-shrink:0;transition:background .2s}._resizer_pzn4b_1:hover{background:var(--color-primary-light)}._flag_1q4ri_3{display:inline-flex;align-items:center;justify-content:center;font-size:13px;line-height:1;cursor:help;-webkit-user-select:none;user-select:none;height:14px;background:none;border:none;padding:0;color:inherit;font-family:inherit}._flag_1q4ri_3:focus-visible{outline:2px solid var(--primary-color, #1677ff);outline-offset:2px;border-radius:2px}._popover_1q4ri_27{color:var(--text-secondary);font-size:13px;line-height:22px}._meta_1q4ri_33{color:var(--text-tertiary);font-size:12px}._usagePill_srdlo_4{position:relative;display:inline-flex;align-items:center;justify-content:flex-start;border-radius:999px;border:1px solid;border-color:var(--text-disabled);color:var(--text-disabled);padding:0 7px;height:15px;font-size:11px;line-height:1;overflow:hidden;white-space:nowrap;cursor:default;background:var(--bg-base-pure)}._usageFill_srdlo_25{position:absolute;left:0;top:0;bottom:0;width:var(--usage-percent, 0);background-color:var(--text-disabled);opacity:.25;transition:width .5s ease;pointer-events:none}._usageContent_srdlo_37{position:relative;z-index:1;display:inline-flex;align-items:center}._usageText_srdlo_44{font-variant-numeric:tabular-nums}._muted_srdlo_49{border-color:var(--border-light);color:var(--text-disabled);background:var(--bg-surface)}._pop_srdlo_56{min-width:220px;font-size:12px;color:var(--text-primary)}._popTitle_srdlo_62{font-weight:600;margin-bottom:6px}._popTable_srdlo_68{border-collapse:collapse}._popTable_srdlo_68 td{padding-top:3px;padding-bottom:3px;vertical-align:middle}._tdName_srdlo_73{color:var(--text-secondary);padding-right:5px;white-space:nowrap}._tdBar_srdlo_73{padding-right:5px;white-space:nowrap}._tdReset_srdlo_91{color:var(--text-secondary);font-variant-numeric:tabular-nums;white-space:nowrap}._bar_srdlo_98{position:relative;display:inline-flex;align-items:center;justify-content:center;width:100px;height:14px;border-radius:999px;border:1px solid var(--text-disabled);overflow:hidden;background:var(--bg-base-pure);vertical-align:middle}._barFill_srdlo_112{position:absolute;left:0;top:0;bottom:0;background-color:var(--text-disabled);opacity:.25;pointer-events:none}._barText_srdlo_122{position:relative;z-index:1;font-size:11px;line-height:1;font-variant-numeric:tabular-nums;color:var(--text-primary)}