@dsh-ft/dsh-fft-settings 0.0.0-stage → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +395 -2
- package/cordis.patch.yml +18 -0
- package/docs/compatibility-dsh-0.1.6-alpha.1.md +58 -0
- package/docs/extension-service-design.md +351 -0
- package/docs/extension-service-sequences.html +545 -0
- package/docs/ftshare-settings-er.html +139 -0
- package/docs/ftshare-settings-er.svg +113 -0
- package/docs/ftshare-settings-results-er.html +148 -0
- package/docs/ftshare-settings-results-er.svg +122 -0
- package/docs/kline-integration.md +258 -0
- package/lib/agent.d.ts +24 -0
- package/lib/agent.js +26 -0
- package/lib/brand.d.ts +53 -0
- package/lib/brand.js +50 -0
- package/lib/client.d.ts +8 -0
- package/lib/client.js +806 -0
- package/lib/client.js.map +1 -0
- package/lib/contract.d.ts +14 -0
- package/lib/contract.js +14 -0
- package/lib/core/client-transport.d.ts +15 -0
- package/lib/core/client-transport.js +31 -0
- package/lib/core/contract.d.ts +62 -0
- package/lib/core/contract.js +28 -0
- package/lib/core/module-registry.d.ts +54 -0
- package/lib/core/module-registry.js +68 -0
- package/lib/core/rpc-core.d.ts +69 -0
- package/lib/core/rpc-core.js +88 -0
- package/lib/core/shell.d.ts +59 -0
- package/lib/core/shell.js +28 -0
- package/lib/core/styles.d.ts +31 -0
- package/lib/core/styles.js +43 -0
- package/lib/core/transport.d.ts +19 -0
- package/lib/core/transport.js +80 -0
- package/lib/default-preset.d.ts +52 -0
- package/lib/default-preset.js +121 -0
- package/lib/features/api-key/card.d.ts +24 -0
- package/lib/features/api-key/card.js +39 -0
- package/lib/features/api-key/client-api.d.ts +4 -0
- package/lib/features/api-key/client-api.js +11 -0
- package/lib/features/api-key/contract.d.ts +69 -0
- package/lib/features/api-key/contract.js +24 -0
- package/lib/features/api-key/host.d.ts +17 -0
- package/lib/features/api-key/host.js +32 -0
- package/lib/features/api-key/index.d.ts +13 -0
- package/lib/features/api-key/index.js +12 -0
- package/lib/features/api-key/logging.d.ts +15 -0
- package/lib/features/api-key/logging.js +1 -0
- package/lib/features/api-key/model.d.ts +24 -0
- package/lib/features/api-key/model.js +105 -0
- package/lib/features/api-key/probe.d.ts +17 -0
- package/lib/features/api-key/probe.js +148 -0
- package/lib/features/api-key/rpc.d.ts +4 -0
- package/lib/features/api-key/rpc.js +17 -0
- package/lib/features/api-key/service.d.ts +21 -0
- package/lib/features/api-key/service.js +181 -0
- package/lib/features/api-key/styles.d.ts +9 -0
- package/lib/features/api-key/styles.js +24 -0
- package/lib/features/logs/archive.d.ts +87 -0
- package/lib/features/logs/archive.js +156 -0
- package/lib/features/logs/card.d.ts +22 -0
- package/lib/features/logs/card.js +107 -0
- package/lib/features/logs/client-api.d.ts +4 -0
- package/lib/features/logs/client-api.js +11 -0
- package/lib/features/logs/collector.d.ts +91 -0
- package/lib/features/logs/collector.js +258 -0
- package/lib/features/logs/config.d.ts +70 -0
- package/lib/features/logs/config.js +97 -0
- package/lib/features/logs/contract.d.ts +130 -0
- package/lib/features/logs/contract.js +67 -0
- package/lib/features/logs/export.d.ts +92 -0
- package/lib/features/logs/export.js +212 -0
- package/lib/features/logs/format.d.ts +42 -0
- package/lib/features/logs/format.js +172 -0
- package/lib/features/logs/home.d.ts +18 -0
- package/lib/features/logs/home.js +39 -0
- package/lib/features/logs/host.d.ts +19 -0
- package/lib/features/logs/host.js +139 -0
- package/lib/features/logs/index.d.ts +12 -0
- package/lib/features/logs/index.js +12 -0
- package/lib/features/logs/rpc.d.ts +6 -0
- package/lib/features/logs/rpc.js +27 -0
- package/lib/features/logs/styles.d.ts +9 -0
- package/lib/features/logs/styles.js +13 -0
- package/lib/guidance.d.ts +41 -0
- package/lib/guidance.js +88 -0
- package/lib/index.d.ts +67 -0
- package/lib/index.js +50 -0
- package/lib/rpc.d.ts +10 -0
- package/lib/rpc.js +10 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +108 -4
- package/src/agent.ts +31 -0
- package/src/brand.ts +79 -0
- package/src/client.tsx +21 -0
- package/src/contract.ts +14 -0
- package/src/core/client-transport.ts +37 -0
- package/src/core/contract.ts +57 -0
- package/src/core/module-registry.ts +102 -0
- package/src/core/rpc-core.ts +107 -0
- package/src/core/shell.tsx +81 -0
- package/src/core/styles.ts +43 -0
- package/src/core/transport.ts +82 -0
- package/src/default-preset.ts +151 -0
- package/src/features/api-key/card.tsx +77 -0
- package/src/features/api-key/client-api.ts +12 -0
- package/src/features/api-key/contract.ts +63 -0
- package/src/features/api-key/host.ts +36 -0
- package/src/features/api-key/index.ts +13 -0
- package/src/features/api-key/logging.ts +15 -0
- package/src/features/api-key/model.ts +63 -0
- package/src/features/api-key/probe.ts +125 -0
- package/src/features/api-key/rpc.ts +18 -0
- package/src/features/api-key/service.ts +157 -0
- package/src/features/api-key/styles.ts +24 -0
- package/src/features/logs/archive.ts +230 -0
- package/src/features/logs/card.tsx +169 -0
- package/src/features/logs/client-api.ts +20 -0
- package/src/features/logs/collector.ts +313 -0
- package/src/features/logs/config.ts +115 -0
- package/src/features/logs/contract.ts +149 -0
- package/src/features/logs/export.ts +274 -0
- package/src/features/logs/format.ts +188 -0
- package/src/features/logs/home.ts +43 -0
- package/src/features/logs/host.ts +153 -0
- package/src/features/logs/index.ts +12 -0
- package/src/features/logs/rpc.ts +29 -0
- package/src/features/logs/styles.ts +13 -0
- package/src/guidance.ts +114 -0
- package/src/index.ts +59 -0
- package/src/rpc.ts +10 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,806 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@dsh-ft/dsh-fft-settings",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
+
const FTSHARE_CREDENTIAL_EVENTS = ["credentials/reference-updated"];
|
|
10
|
+
/** Local syntax validation, never an assertion of upstream validity. */
|
|
11
|
+
function normalizeApiKey(input) {
|
|
12
|
+
const value = input.trim();
|
|
13
|
+
if (!value) throw new Error("请输入 API Key");
|
|
14
|
+
if (value.length > 512 || /[\x00-\x1f\x7f]/.test(value)) throw new Error("API Key 长度不能超过 512 个字符,且不能包含控制字符");
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
// src/core/contract.ts
|
|
18
|
+
/**
|
|
19
|
+
* Identity of the settings section that carries this plugin's cards.
|
|
20
|
+
*
|
|
21
|
+
* The id replaces the old `ftshare`. The label is a neutral product name, not
|
|
22
|
+
* the internal brand: the page ships publicly, and the data source it
|
|
23
|
+
* integrates is named inside the cards instead.
|
|
24
|
+
*/
|
|
25
|
+
const FTSHARE_SETTINGS_SECTION = {
|
|
26
|
+
id: "dsh-fft",
|
|
27
|
+
order: 120,
|
|
28
|
+
label: "金融终端"
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Child slot of {@link FTSHARE_SETTINGS_SECTION} that carries its cards.
|
|
32
|
+
*
|
|
33
|
+
* The section declares the slot when it registers; every card — ours and a
|
|
34
|
+
* third party's — contributes through it with `id`/`order`/`label`, and
|
|
35
|
+
* `order` (ascending, default 100) decides the card's position.
|
|
36
|
+
*/
|
|
37
|
+
const FTSHARE_SETTINGS_CARD_SLOT = "ftshare.settings.card";
|
|
38
|
+
/**
|
|
39
|
+
* Browser-facing route for this plugin's settings calls.
|
|
40
|
+
*
|
|
41
|
+
* It sits below `/api` so the shared connection carrier applies the Host/Origin
|
|
42
|
+
* fence and browser authentication before dispatch; this plugin only handles
|
|
43
|
+
* the call itself.
|
|
44
|
+
*/
|
|
45
|
+
const FTSHARE_SETTINGS_ROUTE = "/api/ftshare-settings";
|
|
46
|
+
// src/features/api-key/model.ts
|
|
47
|
+
var FtshareSettingsModel = class {
|
|
48
|
+
api;
|
|
49
|
+
state = {
|
|
50
|
+
busy: false,
|
|
51
|
+
message: ""
|
|
52
|
+
};
|
|
53
|
+
listeners = /* @__PURE__ */ new Set();
|
|
54
|
+
request = 0;
|
|
55
|
+
lifetime = 0;
|
|
56
|
+
constructor(api) {
|
|
57
|
+
this.api = api;
|
|
58
|
+
}
|
|
59
|
+
getSnapshot = () => this.state;
|
|
60
|
+
subscribe = (listener) => {
|
|
61
|
+
this.listeners.add(listener);
|
|
62
|
+
return () => {
|
|
63
|
+
this.listeners.delete(listener);
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
update(patch) {
|
|
67
|
+
this.state = {
|
|
68
|
+
...this.state,
|
|
69
|
+
...patch
|
|
70
|
+
};
|
|
71
|
+
for (const listener of this.listeners) listener();
|
|
72
|
+
}
|
|
73
|
+
connect(subscribe) {
|
|
74
|
+
const dispose = subscribe?.(() => {
|
|
75
|
+
this.refresh();
|
|
76
|
+
});
|
|
77
|
+
this.refresh();
|
|
78
|
+
return () => {
|
|
79
|
+
this.request++;
|
|
80
|
+
this.lifetime++;
|
|
81
|
+
dispose?.();
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
async refresh() {
|
|
85
|
+
const request = ++this.request;
|
|
86
|
+
try {
|
|
87
|
+
const status = await this.api.describe();
|
|
88
|
+
if (!status || typeof status.configured !== "boolean") throw new Error("missing_status");
|
|
89
|
+
if (request === this.request) this.update({
|
|
90
|
+
status,
|
|
91
|
+
statusError: void 0
|
|
92
|
+
});
|
|
93
|
+
} catch {
|
|
94
|
+
if (request === this.request) this.update({
|
|
95
|
+
status: void 0,
|
|
96
|
+
statusError: "无法读取设置状态,请刷新重试"
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async configureAndTest(input) {
|
|
101
|
+
if (this.state.busy || !this.state.status?.writable) return false;
|
|
102
|
+
let value;
|
|
103
|
+
try {
|
|
104
|
+
value = normalizeApiKey(input);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
this.update({ message: error.message });
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
const lifetime = this.lifetime;
|
|
110
|
+
this.update({
|
|
111
|
+
busy: true,
|
|
112
|
+
save: void 0,
|
|
113
|
+
message: "正在保存并检查远端服务和 API Key…"
|
|
114
|
+
});
|
|
115
|
+
let saved = false;
|
|
116
|
+
try {
|
|
117
|
+
const result = await this.api.configureAndTest(value);
|
|
118
|
+
saved = result.save.status === "saved";
|
|
119
|
+
if (lifetime === this.lifetime) this.update({
|
|
120
|
+
save: result.save,
|
|
121
|
+
message: saved ? validationMessage(result.validation.status) : result.save.message
|
|
122
|
+
});
|
|
123
|
+
} catch {
|
|
124
|
+
if (lifetime === this.lifetime) this.update({ message: "请求未完成,保存状态待确认;请刷新后测试当前连接" });
|
|
125
|
+
}
|
|
126
|
+
if (lifetime === this.lifetime) {
|
|
127
|
+
await this.refresh();
|
|
128
|
+
this.update({ busy: false });
|
|
129
|
+
}
|
|
130
|
+
return saved;
|
|
131
|
+
}
|
|
132
|
+
async testConnection() {
|
|
133
|
+
if (this.state.busy || !this.state.status) return;
|
|
134
|
+
const lifetime = this.lifetime;
|
|
135
|
+
this.update({
|
|
136
|
+
busy: true,
|
|
137
|
+
message: "正在检查远端服务和当前 API Key…"
|
|
138
|
+
});
|
|
139
|
+
try {
|
|
140
|
+
const result = await this.api.testConnection();
|
|
141
|
+
if (lifetime === this.lifetime) this.update({ message: validationMessage(result.status) });
|
|
142
|
+
} catch {
|
|
143
|
+
if (lifetime === this.lifetime) this.update({ message: "测试请求失败,请重试" });
|
|
144
|
+
}
|
|
145
|
+
if (lifetime === this.lifetime) {
|
|
146
|
+
await this.refresh();
|
|
147
|
+
this.update({ busy: false });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async clear() {
|
|
151
|
+
if (this.state.busy || !this.state.status?.writable) return;
|
|
152
|
+
const lifetime = this.lifetime;
|
|
153
|
+
this.update({
|
|
154
|
+
busy: true,
|
|
155
|
+
save: void 0,
|
|
156
|
+
message: ""
|
|
157
|
+
});
|
|
158
|
+
try {
|
|
159
|
+
await this.api.clear();
|
|
160
|
+
if (lifetime === this.lifetime) this.update({ message: "已清除 DSH 保存的密钥;若有 .env 配置,下方显示其有效状态" });
|
|
161
|
+
} catch {
|
|
162
|
+
if (lifetime === this.lifetime) this.update({ message: "清除失败,请刷新状态后重试" });
|
|
163
|
+
}
|
|
164
|
+
if (lifetime === this.lifetime) {
|
|
165
|
+
await this.refresh();
|
|
166
|
+
this.update({ busy: false });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
function validationMessage(status) {
|
|
171
|
+
return {
|
|
172
|
+
passed: "远端服务正常,API Key 验证通过",
|
|
173
|
+
partial: "远端服务正常,请查看 API Key 检查结果",
|
|
174
|
+
failed: "测试未通过,请查看分项结果",
|
|
175
|
+
not_run: "未执行测试,请先确认凭据配置",
|
|
176
|
+
superseded: "凭据已变化,请重新测试",
|
|
177
|
+
not_tested: "尚未测试"
|
|
178
|
+
}[status] ?? "请查看分项结果";
|
|
179
|
+
}
|
|
180
|
+
// src/core/rpc-core.ts
|
|
181
|
+
/**
|
|
182
|
+
* 把一次失败的信封翻译成调用方能展示的原因。
|
|
183
|
+
*
|
|
184
|
+
* `not_found` 是「浏览器半边比宿主半边新」的可观测症状:升级插件后客户端 bundle 由磁盘重新
|
|
185
|
+
* 加载,而宿主进程仍持有旧包,它没有新命名空间。此时重试永远不会成功,必须点名重启宿主。
|
|
186
|
+
* @param endpoint - 这次调用的 `<namespace>/<method>`。
|
|
187
|
+
* @param error - 宿主回传的失败原因,畸形信封下可能缺省。
|
|
188
|
+
* @returns 面向用户的失败描述。
|
|
189
|
+
*/
|
|
190
|
+
function describeSettingsFailure(endpoint, error) {
|
|
191
|
+
if (error?.code === "not_found") return `FTShare 设置接口 ${endpoint} 不存在:宿主插件还是升级前的版本,重启 DSH 后再试`;
|
|
192
|
+
return `FTShare 设置操作失败(${error?.code ?? "unknown"}):${error?.message ?? "宿主未说明原因"}`;
|
|
193
|
+
}
|
|
194
|
+
async function callSettingsRpc(transport, namespace, method, payload = {}) {
|
|
195
|
+
const endpoint = `${namespace}/${method}`;
|
|
196
|
+
const result = await transport.call(endpoint, payload);
|
|
197
|
+
if (!result.ok) throw new Error(describeSettingsFailure(endpoint, result.error));
|
|
198
|
+
return result.value;
|
|
199
|
+
}
|
|
200
|
+
// src/features/api-key/client-api.ts
|
|
201
|
+
/** Browser adapter deliberately has no resolve/createEnvironment method. */
|
|
202
|
+
function createApiKeyClient(transport) {
|
|
203
|
+
async function call(method, payload = {}) {
|
|
204
|
+
return callSettingsRpc(transport, "apiKey", method, payload);
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
describe: () => call("describe"),
|
|
208
|
+
set: (value) => call("set", { value }),
|
|
209
|
+
clear: () => call("clear"),
|
|
210
|
+
configureAndTest: (value) => call("configureAndTest", { value }),
|
|
211
|
+
testConnection: () => call("testConnection"),
|
|
212
|
+
getValidationStatus: () => call("getValidationStatus")
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
// src/core/client-transport.ts
|
|
216
|
+
/**
|
|
217
|
+
* Browser half of the settings transport.
|
|
218
|
+
*
|
|
219
|
+
* Calls are same-origin fetches to the plugin route, so the browser session
|
|
220
|
+
* cookie carries authentication and this plugin needs no Connection RPC client.
|
|
221
|
+
*/
|
|
222
|
+
/**
|
|
223
|
+
* Create the browser-backed caller used by the settings cards.
|
|
224
|
+
* @param doFetch - transport override; defaults to the page's global fetch.
|
|
225
|
+
* @returns caller that owns request encoding and envelope decoding.
|
|
226
|
+
*/
|
|
227
|
+
function createSettingsTransport(doFetch) {
|
|
228
|
+
const send = doFetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
229
|
+
return { async call(endpoint, payload = {}, signal) {
|
|
230
|
+
const response = await send(FTSHARE_SETTINGS_ROUTE, {
|
|
231
|
+
method: "POST",
|
|
232
|
+
headers: { "content-type": "application/json" },
|
|
233
|
+
body: JSON.stringify({
|
|
234
|
+
endpoint,
|
|
235
|
+
payload
|
|
236
|
+
}),
|
|
237
|
+
...signal === void 0 ? {} : { signal }
|
|
238
|
+
});
|
|
239
|
+
if (!response.ok) throw new Error(`transport failure for ${endpoint}: HTTP ${response.status}`);
|
|
240
|
+
return await response.json();
|
|
241
|
+
} };
|
|
242
|
+
}
|
|
243
|
+
// src/features/api-key/styles.ts
|
|
244
|
+
/**
|
|
245
|
+
* The API Key card's own styles, rendered by the card component's `<style>`.
|
|
246
|
+
*
|
|
247
|
+
* A card owns everything past the section shell: the core declares the
|
|
248
|
+
* `--ftshare-*` tokens and the section container, never card internals.
|
|
249
|
+
* Every selector stays scoped under `.ftshare-settings`, so a card never
|
|
250
|
+
* styles another card's markup.
|
|
251
|
+
*/
|
|
252
|
+
const API_KEY_CARD_CSS = `
|
|
253
|
+
.ftshare-settings .ftshare-eyebrow { color:var(--ftshare-accent-on-surface); font-size:11px; letter-spacing:.12em; }
|
|
254
|
+
.ftshare-settings .ftshare-card { padding:22px; background:var(--ftshare-panel); border:0.5px solid var(--ftshare-border); border-radius:var(--dsw-radius-xl); margin-top:16px; }
|
|
255
|
+
.ftshare-settings label { display:block; font-weight:500; }
|
|
256
|
+
.ftshare-settings input { width:100%; height:32px; border:0.5px solid var(--ftshare-control); border-radius:var(--dsw-radius-md); padding:0 12px; background:var(--ftshare-input); color:inherit; font:inherit; }
|
|
257
|
+
.ftshare-settings input::placeholder { color:var(--ftshare-muted); opacity:1; }
|
|
258
|
+
.ftshare-settings .ftshare-actions { display:flex; flex-wrap:wrap; gap:10px; align-items:center; margin-top:16px; }
|
|
259
|
+
.ftshare-settings button { min-height:36px; border:0.5px solid var(--ftshare-control); border-radius:var(--dsw-radius-md); padding:0 14px; font:inherit; background:transparent; color:inherit; cursor:pointer; }
|
|
260
|
+
.ftshare-settings button.primary { background:var(--ftshare-accent); color:var(--ftshare-accent-ink); border-color:var(--ftshare-accent); font-weight:500; }
|
|
261
|
+
.ftshare-settings button:disabled { opacity:.5; cursor:not-allowed; }
|
|
262
|
+
.ftshare-settings dl { margin:0; }
|
|
263
|
+
.ftshare-settings .ftshare-result { display:grid; grid-template-columns:130px 1fr; gap:12px; padding:10px 0; border-top:0.5px solid var(--ftshare-border); }
|
|
264
|
+
.ftshare-settings dd { margin:0; overflow-wrap:anywhere; }
|
|
265
|
+
.ftshare-settings .ftshare-error { color:var(--ftshare-error); }
|
|
266
|
+
@media(max-width:540px) { .ftshare-settings .ftshare-card { padding:16px; } .ftshare-settings .ftshare-result { grid-template-columns:1fr; gap:4px; } }
|
|
267
|
+
`;
|
|
268
|
+
// src/features/api-key/card.tsx
|
|
269
|
+
/**
|
|
270
|
+
* Contribute the built-in API Key card through the same slot a third-party
|
|
271
|
+
* card uses. Nothing about it is privileged: same declaration, same options.
|
|
272
|
+
* @param ctx - client context carrying the renderer's slots service.
|
|
273
|
+
* @returns idempotent disposer for the card registration.
|
|
274
|
+
*/
|
|
275
|
+
function registerApiKeyCard(ctx) {
|
|
276
|
+
return ctx.slots.inject(FTSHARE_SETTINGS_CARD_SLOT, () => ctx.slots.register({
|
|
277
|
+
name: FTSHARE_SETTINGS_CARD_SLOT,
|
|
278
|
+
id: "apiKey",
|
|
279
|
+
order: 10
|
|
280
|
+
}, createApiKeyCard(ctx)));
|
|
281
|
+
}
|
|
282
|
+
/** Bind the card's adapters; subscriptions are acquired in the component's effect. */
|
|
283
|
+
function createApiKeyCard(ctx) {
|
|
284
|
+
const api = createApiKeyClient(createSettingsTransport());
|
|
285
|
+
const subscribeChanges = (listener) => {
|
|
286
|
+
const disposers = FTSHARE_CREDENTIAL_EVENTS.map((event) => ctx.remote.$on(event, (ref) => {
|
|
287
|
+
if (ref === "FTSHARE_API_KEY") listener();
|
|
288
|
+
}));
|
|
289
|
+
disposers.push(ctx.on("connection/reset", listener));
|
|
290
|
+
return () => {
|
|
291
|
+
for (const dispose of disposers) dispose();
|
|
292
|
+
};
|
|
293
|
+
};
|
|
294
|
+
return () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ApiKeyCard, {
|
|
295
|
+
api,
|
|
296
|
+
subscribeChanges
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
function ApiKeyCard({ api, subscribeChanges }) {
|
|
300
|
+
const model = (0, react.useMemo)(() => new FtshareSettingsModel(api), [api]);
|
|
301
|
+
const { status, busy, message, statusError, save } = (0, react.useSyncExternalStore)(model.subscribe, model.getSnapshot, model.getSnapshot);
|
|
302
|
+
const [draft, setDraft] = (0, react.useState)("");
|
|
303
|
+
(0, react.useEffect)(() => model.connect(subscribeChanges), [model, subscribeChanges]);
|
|
304
|
+
const disabled = busy || !status?.writable;
|
|
305
|
+
const configure = async () => {
|
|
306
|
+
if (await model.configureAndTest(draft)) setDraft("");
|
|
307
|
+
};
|
|
308
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: API_KEY_CARD_CSS }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
309
|
+
className: "ftshare-settings",
|
|
310
|
+
"aria-label": "FTShare 设置",
|
|
311
|
+
"aria-busy": busy,
|
|
312
|
+
children: [
|
|
313
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("header", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
314
|
+
className: "ftshare-eyebrow",
|
|
315
|
+
children: "FTSHARE / API KEY"
|
|
316
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", { children: "访问凭据" })] }) }),
|
|
317
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "目前集成 FTShare 数据源。" }),
|
|
318
|
+
status && !status.writable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "当前来源为只读,可测试连接;修改密钥请到对应来源操作。" }),
|
|
319
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
320
|
+
className: "ftshare-card",
|
|
321
|
+
children: [
|
|
322
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
323
|
+
htmlFor: "ftshare-api-key",
|
|
324
|
+
children: "FTShare API Key"
|
|
325
|
+
}),
|
|
326
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
327
|
+
id: "ftshare-api-key",
|
|
328
|
+
type: "password",
|
|
329
|
+
value: draft,
|
|
330
|
+
placeholder: status?.configured ? status.maskedValue ?? "••••••••" : "请输入 API Key",
|
|
331
|
+
onChange: (e) => setDraft(e.target.value),
|
|
332
|
+
disabled,
|
|
333
|
+
autoComplete: "new-password",
|
|
334
|
+
maxLength: 512
|
|
335
|
+
}),
|
|
336
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
337
|
+
className: "ftshare-actions",
|
|
338
|
+
children: [
|
|
339
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
340
|
+
className: "primary",
|
|
341
|
+
type: "button",
|
|
342
|
+
onClick: () => void configure(),
|
|
343
|
+
disabled: disabled || !draft.trim(),
|
|
344
|
+
children: "配置并测试"
|
|
345
|
+
}),
|
|
346
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
347
|
+
type: "button",
|
|
348
|
+
onClick: () => void model.testConnection(),
|
|
349
|
+
disabled: busy || !status,
|
|
350
|
+
children: "测试当前连接"
|
|
351
|
+
}),
|
|
352
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
353
|
+
type: "button",
|
|
354
|
+
onClick: () => void model.clear(),
|
|
355
|
+
disabled: disabled || !status?.configured,
|
|
356
|
+
children: "清除已保存密钥"
|
|
357
|
+
}),
|
|
358
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
359
|
+
type: "button",
|
|
360
|
+
onClick: () => void model.refresh(),
|
|
361
|
+
disabled: busy,
|
|
362
|
+
title: "重新读取已保存的配置和上次检测结果,不会发起联网检测",
|
|
363
|
+
children: "重新读取状态"
|
|
364
|
+
}),
|
|
365
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
366
|
+
href: "https://ftai.chat/me/profile?tab=api-key",
|
|
367
|
+
target: "_blank",
|
|
368
|
+
rel: "noreferrer",
|
|
369
|
+
children: "获取 API Key ↗"
|
|
370
|
+
})
|
|
371
|
+
]
|
|
372
|
+
}),
|
|
373
|
+
draft && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: "“测试当前连接”使用已生效的密钥,不测试输入框内尚未保存的内容。" }),
|
|
374
|
+
busy && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
375
|
+
role: "status",
|
|
376
|
+
children: "正在处理,请稍候…"
|
|
377
|
+
})
|
|
378
|
+
]
|
|
379
|
+
}),
|
|
380
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
381
|
+
className: "ftshare-card",
|
|
382
|
+
"aria-live": "polite",
|
|
383
|
+
children: [
|
|
384
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: "验证结果" }),
|
|
385
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dl", { children: [
|
|
386
|
+
save && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
387
|
+
className: "ftshare-result",
|
|
388
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: "本次保存" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: save.message })]
|
|
389
|
+
}),
|
|
390
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
391
|
+
className: "ftshare-result",
|
|
392
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: "远端服务" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: status?.validation.remote.message ?? "状态待确认" })]
|
|
393
|
+
}),
|
|
394
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
395
|
+
className: "ftshare-result",
|
|
396
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: "API Key" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: status?.validation.apiKey.message ?? "状态待确认" })]
|
|
397
|
+
}),
|
|
398
|
+
status?.validation.testedAt && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
399
|
+
className: "ftshare-result",
|
|
400
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: "测试时间" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: new Date(status.validation.testedAt).toLocaleString() })]
|
|
401
|
+
})
|
|
402
|
+
] }),
|
|
403
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: "节假日或收盘后的空数据不影响验证。" })
|
|
404
|
+
]
|
|
405
|
+
}),
|
|
406
|
+
message && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
407
|
+
role: "status",
|
|
408
|
+
children: message
|
|
409
|
+
}),
|
|
410
|
+
statusError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
411
|
+
className: "ftshare-error",
|
|
412
|
+
role: "alert",
|
|
413
|
+
children: statusError
|
|
414
|
+
})
|
|
415
|
+
]
|
|
416
|
+
})] });
|
|
417
|
+
}
|
|
418
|
+
// src/features/logs/contract.ts
|
|
419
|
+
/**
|
|
420
|
+
* `logs` 命名空间的共享契约:只有常量与类型,浏览器半边可以安全导入。
|
|
421
|
+
*
|
|
422
|
+
* 采集器(Host 常驻)与导出器(设置页)通过文件解耦;本文件是两者唯一的共同语言。
|
|
423
|
+
*/
|
|
424
|
+
/**
|
|
425
|
+
* 卡片在 `ftshare.settings.card` 槽位里的 id 与 order。
|
|
426
|
+
*
|
|
427
|
+
* 归 feature 所有(与业务契约同一份来源):卡片组件与浏览器侧的守卫测试都读它,
|
|
428
|
+
* order 20 排在 API Key 卡(10)之后。
|
|
429
|
+
*/
|
|
430
|
+
const LOGS_CARD_ID = "logs";
|
|
431
|
+
/**
|
|
432
|
+
* 诊断包路由(规格 §9 / §12 决策 3):`/api` 载体上的第二条精确路由,归 `logs` feature 所有。
|
|
433
|
+
*
|
|
434
|
+
* 卡片用 `<a download>` 直接指向它:浏览器同源 GET 自带凭据,不需要 fetch/Blob,
|
|
435
|
+
* `/api` 前缀的鉴权由载体负责,本 feature 不重复鉴权、也不读任何请求参数。
|
|
436
|
+
* 它随 owner fiber 释放(`ctx.effect` + `register` 返回的 disposer)。
|
|
437
|
+
*/
|
|
438
|
+
const LOGS_ARCHIVE_ROUTE = "/api/ftshare-settings/logs.zip";
|
|
439
|
+
// src/features/logs/client-api.ts
|
|
440
|
+
/** 浏览器适配器:只读,且不暴露本地路径以外的任何 Host 能力(刷盘与释放句柄)。 */
|
|
441
|
+
function createLogsClient(transport) {
|
|
442
|
+
const call = (method, payload = {}) => callSettingsRpc(transport, "logs", method, payload);
|
|
443
|
+
return {
|
|
444
|
+
stats: () => call("stats"),
|
|
445
|
+
preview: (lines = 80) => call("preview", { lines }),
|
|
446
|
+
export: () => call("export")
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
// src/features/logs/styles.ts
|
|
450
|
+
/**
|
|
451
|
+
* The `logs` card's own styles, rendered by the card component's `<style>`.
|
|
452
|
+
*
|
|
453
|
+
* These two rules used to live in the core section stylesheet; a card owns
|
|
454
|
+
* everything past the section shell, so they moved here with the card. Every
|
|
455
|
+
* selector stays scoped under `.ftshare-settings`, so a card never styles
|
|
456
|
+
* another card's markup.
|
|
457
|
+
*/
|
|
458
|
+
const LOGS_CARD_CSS = `
|
|
459
|
+
.ftshare-settings select { height:32px; border:0.5px solid var(--ftshare-control); border-radius:var(--dsw-radius-md); padding:0 10px; background:var(--ftshare-input); color:inherit; font:inherit; }
|
|
460
|
+
.ftshare-settings .ftshare-download { display:inline-flex; align-items:center; min-height:32px; padding:0 14px; border:0.5px solid var(--ftshare-control); border-radius:var(--dsw-radius-md); color:inherit; text-decoration:none; cursor:pointer; }
|
|
461
|
+
.ftshare-settings .ftshare-log-preview { max-height:320px; overflow:auto; margin:0 0 12px; padding:12px; background:var(--ftshare-input); border:0.5px solid var(--ftshare-border); border-radius:var(--dsw-radius-lg); font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:12px; line-height:1.5; white-space:pre-wrap; overflow-wrap:anywhere; }
|
|
462
|
+
`;
|
|
463
|
+
// src/features/logs/card.tsx
|
|
464
|
+
/** 预览行数候选:上限由 Host 侧校验兜底。 */
|
|
465
|
+
const PREVIEW_CHOICES = [
|
|
466
|
+
50,
|
|
467
|
+
80,
|
|
468
|
+
200
|
|
469
|
+
];
|
|
470
|
+
/**
|
|
471
|
+
* 快照缺失时的占位文案。
|
|
472
|
+
*
|
|
473
|
+
* 失败后不能继续写「读取中…」:那会把一次已经结束的失败说成正在进行的读取,掩盖真正的
|
|
474
|
+
* 原因(例如宿主插件没有这个接口,重试永远不会成功)。
|
|
475
|
+
* @param failed - 本次读取是否已经失败。
|
|
476
|
+
* @returns 失败提示,或仍在读取的提示。
|
|
477
|
+
*/
|
|
478
|
+
function statsPlaceholder(failed) {
|
|
479
|
+
return failed ? "读取失败" : "读取中…";
|
|
480
|
+
}
|
|
481
|
+
function displayBytes(bytes) {
|
|
482
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
483
|
+
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
484
|
+
return `${(bytes / 1024 / 1024).toFixed(2)} MiB`;
|
|
485
|
+
}
|
|
486
|
+
/** 把文本包成 Blob,用 a[download] 触发下载(Web 与桌面同一套)。 */
|
|
487
|
+
function downloadText(fileName, text) {
|
|
488
|
+
const url = URL.createObjectURL(new Blob([text], { type: "text/plain;charset=utf-8" }));
|
|
489
|
+
const anchor = document.createElement("a");
|
|
490
|
+
anchor.href = url;
|
|
491
|
+
anchor.download = fileName;
|
|
492
|
+
anchor.rel = "noopener";
|
|
493
|
+
document.body.appendChild(anchor);
|
|
494
|
+
anchor.click();
|
|
495
|
+
anchor.remove();
|
|
496
|
+
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Contribute the built-in runtime-log card through the same slot a third-party
|
|
500
|
+
* card uses. Nothing about it is privileged: same declaration, same options.
|
|
501
|
+
* @param ctx - client context carrying the renderer's slots service.
|
|
502
|
+
* @returns idempotent disposer for the card registration.
|
|
503
|
+
*/
|
|
504
|
+
function registerLogsCard(ctx) {
|
|
505
|
+
return ctx.slots.inject(FTSHARE_SETTINGS_CARD_SLOT, () => ctx.slots.register({
|
|
506
|
+
name: FTSHARE_SETTINGS_CARD_SLOT,
|
|
507
|
+
id: LOGS_CARD_ID,
|
|
508
|
+
order: 20
|
|
509
|
+
}, createLogsCard()));
|
|
510
|
+
}
|
|
511
|
+
/** Bind the card's adapter; the component only reads through it. */
|
|
512
|
+
function createLogsCard() {
|
|
513
|
+
const api = createLogsClient(createSettingsTransport());
|
|
514
|
+
return () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LogsSection, { api });
|
|
515
|
+
}
|
|
516
|
+
function LogsSection({ api }) {
|
|
517
|
+
const [stats, setStats] = (0, react.useState)();
|
|
518
|
+
const [preview, setPreview] = (0, react.useState)();
|
|
519
|
+
const [lines, setLines] = (0, react.useState)(80);
|
|
520
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
521
|
+
const [message, setMessage] = (0, react.useState)("");
|
|
522
|
+
const [error, setError] = (0, react.useState)("");
|
|
523
|
+
/** 读取已失败且还没有任何快照时,每个字段都必须说出来,而不是假装还在读。 */
|
|
524
|
+
const placeholder = statsPlaceholder(error !== "");
|
|
525
|
+
const refresh = (0, react.useCallback)(async () => {
|
|
526
|
+
setBusy(true);
|
|
527
|
+
setError("");
|
|
528
|
+
try {
|
|
529
|
+
setStats(await api.stats());
|
|
530
|
+
setPreview(await api.preview(lines));
|
|
531
|
+
} catch (cause) {
|
|
532
|
+
setError(cause instanceof Error ? cause.message : "读取日志状态失败");
|
|
533
|
+
} finally {
|
|
534
|
+
setBusy(false);
|
|
535
|
+
}
|
|
536
|
+
}, [api, lines]);
|
|
537
|
+
(0, react.useEffect)(() => {
|
|
538
|
+
refresh();
|
|
539
|
+
}, [refresh]);
|
|
540
|
+
const doExport = async () => {
|
|
541
|
+
setBusy(true);
|
|
542
|
+
setError("");
|
|
543
|
+
setMessage("");
|
|
544
|
+
try {
|
|
545
|
+
const result = await api.export();
|
|
546
|
+
downloadText(result.fileName, result.text);
|
|
547
|
+
setMessage(`已导出 ${result.fileName}(${displayBytes(result.bytes)}${result.truncated ? ",已达体积上限并截断" : ""})`);
|
|
548
|
+
} catch (cause) {
|
|
549
|
+
setError(cause instanceof Error ? cause.message : "导出日志失败");
|
|
550
|
+
} finally {
|
|
551
|
+
setBusy(false);
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: LOGS_CARD_CSS }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
555
|
+
className: "ftshare-settings",
|
|
556
|
+
"aria-label": "FT 运行日志",
|
|
557
|
+
"aria-busy": busy,
|
|
558
|
+
children: [
|
|
559
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("header", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
560
|
+
className: "ftshare-eyebrow",
|
|
561
|
+
children: "FTSHARE / LOGS"
|
|
562
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", { children: "运行日志" })] }) }),
|
|
563
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
564
|
+
className: "ftshare-card",
|
|
565
|
+
children: [
|
|
566
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: "采集状态" }),
|
|
567
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dl", { children: [
|
|
568
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
569
|
+
className: "ftshare-result",
|
|
570
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: "日志文件" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: stats?.file ?? placeholder })]
|
|
571
|
+
}),
|
|
572
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
573
|
+
className: "ftshare-result",
|
|
574
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: "记录 / 时间范围" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dd", { children: [stats === void 0 ? placeholder : `${stats.records} 条`, stats?.firstAt !== void 0 && `(${stats.firstAt} ~ ${stats.lastAt})`] })]
|
|
575
|
+
}),
|
|
576
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
577
|
+
className: "ftshare-result",
|
|
578
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: "文件大小 / 上限" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: stats === void 0 ? placeholder : `${displayBytes(stats.fileBytes)} / ${displayBytes(stats.maxBytes)}` })]
|
|
579
|
+
}),
|
|
580
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
581
|
+
className: "ftshare-result",
|
|
582
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: "丢弃 / 轮转 / 写失败" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: stats === void 0 ? placeholder : `${stats.dropped} / ${stats.rotations} / ${stats.writeFailures}` })]
|
|
583
|
+
}),
|
|
584
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
585
|
+
className: "ftshare-result",
|
|
586
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: "日志级别" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dd", { children: [stats?.level ?? placeholder, " "] })]
|
|
587
|
+
}),
|
|
588
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
589
|
+
className: "ftshare-result",
|
|
590
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: "统计" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: stats === void 0 ? placeholder : Object.keys(stats.byName).length === 0 ? "暂无" : Object.entries(stats.byName).map(([name, count]) => `${name}=${count}`).join("、") })]
|
|
591
|
+
}),
|
|
592
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
593
|
+
className: "ftshare-result",
|
|
594
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: "额外来源" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: stats === void 0 ? placeholder : stats.sources.length === 0 ? "暂无" : stats.sources.map((source) => `${source.path}(${source.kind}${source.bytes === void 0 ? "" : ` ${displayBytes(source.bytes)}`})`).join(";") })]
|
|
595
|
+
})
|
|
596
|
+
] }),
|
|
597
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
598
|
+
className: "ftshare-actions",
|
|
599
|
+
children: [
|
|
600
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
601
|
+
className: "primary",
|
|
602
|
+
type: "button",
|
|
603
|
+
onClick: () => void doExport(),
|
|
604
|
+
disabled: busy,
|
|
605
|
+
children: "导出日志"
|
|
606
|
+
}),
|
|
607
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
608
|
+
className: "ftshare-download",
|
|
609
|
+
href: LOGS_ARCHIVE_ROUTE,
|
|
610
|
+
download: "",
|
|
611
|
+
children: "导出诊断包"
|
|
612
|
+
}),
|
|
613
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
614
|
+
type: "button",
|
|
615
|
+
onClick: () => void refresh(),
|
|
616
|
+
disabled: busy,
|
|
617
|
+
children: "刷新"
|
|
618
|
+
}),
|
|
619
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
620
|
+
htmlFor: "ftshare-log-lines",
|
|
621
|
+
children: "预览行数"
|
|
622
|
+
}),
|
|
623
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
624
|
+
id: "ftshare-log-lines",
|
|
625
|
+
value: lines,
|
|
626
|
+
onChange: (e) => setLines(Number(e.target.value)),
|
|
627
|
+
disabled: busy,
|
|
628
|
+
children: PREVIEW_CHOICES.map((choice) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
629
|
+
value: choice,
|
|
630
|
+
children: choice
|
|
631
|
+
}, choice))
|
|
632
|
+
})
|
|
633
|
+
]
|
|
634
|
+
}),
|
|
635
|
+
busy && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
636
|
+
role: "status",
|
|
637
|
+
children: "正在处理,请稍候…"
|
|
638
|
+
}),
|
|
639
|
+
message && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
640
|
+
role: "status",
|
|
641
|
+
children: message
|
|
642
|
+
}),
|
|
643
|
+
error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
644
|
+
className: "ftshare-error",
|
|
645
|
+
role: "alert",
|
|
646
|
+
children: error
|
|
647
|
+
})
|
|
648
|
+
]
|
|
649
|
+
}),
|
|
650
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
651
|
+
className: "ftshare-card",
|
|
652
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h3", { children: ["最近日志", preview?.truncated === true ? "(已截断)" : ""] }), preview === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: error === "" ? "正在读取…" : "日志预览读取失败,见上方提示。" }) : preview.lines.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "无" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
653
|
+
className: "ftshare-log-preview",
|
|
654
|
+
children: preview.lines.join("\n")
|
|
655
|
+
})]
|
|
656
|
+
})
|
|
657
|
+
]
|
|
658
|
+
})] });
|
|
659
|
+
}
|
|
660
|
+
// src/core/styles.ts
|
|
661
|
+
/**
|
|
662
|
+
* The section shell's styles plus the theme tokens every card reads.
|
|
663
|
+
*
|
|
664
|
+
* The shell owns the container, typography, focus ring and link rules that
|
|
665
|
+
* every card sits inside; a card owns its own CSS and renders it from its own
|
|
666
|
+
* component, and core never styles card internals.
|
|
667
|
+
*
|
|
668
|
+
* The theme layer declares the plugin's `--ftshare-*` tokens once, on
|
|
669
|
+
* `.ftshare-settings` — the root element every card renders. Surfaces and text
|
|
670
|
+
* take their value from the host palette: `--dsw-alias-*` is what DSH
|
|
671
|
+
* publishes for the light, the dark and every registered theme, so one
|
|
672
|
+
* stylesheet follows the live palette with no script, no `prefers-color-scheme`
|
|
673
|
+
* copy and no second palette to keep in sync. The literal inside each `var()`
|
|
674
|
+
* is the fallback for a host that publishes no palette.
|
|
675
|
+
*
|
|
676
|
+
* The declarations sit on the card root rather than on `:root` because a
|
|
677
|
+
* custom property resolves where it is *declared*: `--dsw-alias-*` exists from
|
|
678
|
+
* `body` downwards, so a value derived from it above `body` would resolve to
|
|
679
|
+
* nothing and fall back. Consequence: the place to pin a look is the host
|
|
680
|
+
* palette itself (`ctx.theme.register` / `ctx.theme.overrideTokens`), not these
|
|
681
|
+
* names.
|
|
682
|
+
*
|
|
683
|
+
* `--ftshare-accent*` is the plugin's own brand and has no host equivalent. The
|
|
684
|
+
* values come from the FT brand layer's `--ft-*` tokens (`src/brand.ts`), with
|
|
685
|
+
* the same literal as the fallback so the card keeps its colors when that layer
|
|
686
|
+
* is absent. The fill and its ink are scheme-invariant;
|
|
687
|
+
* `--ftshare-accent-on-surface` mixes the same yellow towards the host's text
|
|
688
|
+
* colour, so the brand stays legible as the 11px eyebrow and as the focus ring
|
|
689
|
+
* on either surface.
|
|
690
|
+
*/
|
|
691
|
+
const SETTINGS_CSS = `
|
|
692
|
+
.ftshare-settings { --ftshare-text:var(--dsw-alias-label-primary,#f4f4f5); --ftshare-muted:var(--dsw-alias-label-secondary,#aeb2bd); --ftshare-panel:var(--dsw-alias-settings-card-fill,#292a30); --ftshare-border:var(--dsw-alias-settings-card-stroke,#44464f); --ftshare-control:var(--dsw-alias-border-l4,#696c77); --ftshare-input:var(--dsw-alias-bg-layer-1,#1d1e23); --ftshare-link:var(--dsw-alias-link,#d8c4fc); --ftshare-error:var(--dsw-alias-state-error-primary,#ffb5ad); --ftshare-accent:var(--ft-accent,#f0c433); --ftshare-accent-ink:var(--ft-accent-ink,#171717); --ftshare-accent-on-surface:color-mix(in srgb,var(--ftshare-accent) 50%,var(--dsw-alias-label-primary,#f4f4f5)); }
|
|
693
|
+
.ftshare-settings { color:var(--ftshare-text); max-width:760px; font-size:14px; line-height:1.6; }
|
|
694
|
+
.ftshare-settings * { box-sizing:border-box; }
|
|
695
|
+
.ftshare-settings h2 { font-size:26px; margin:4px 0 8px; }
|
|
696
|
+
.ftshare-settings h3 { font-size:15px; margin:0 0 12px; }
|
|
697
|
+
.ftshare-settings p { margin:8px 0; color:var(--ftshare-muted); }
|
|
698
|
+
.ftshare-settings header { display:flex; justify-content:space-between; align-items:flex-start; gap:20px; margin-bottom:20px; }
|
|
699
|
+
.ftshare-settings small { display:block; color:var(--ftshare-muted); font-size:12px; margin:5px 0 12px; }
|
|
700
|
+
.ftshare-settings :is(button,input,a):focus-visible { outline:2px solid var(--ftshare-accent-on-surface); outline-offset:3px; }
|
|
701
|
+
.ftshare-settings a { color:var(--ftshare-link); }
|
|
702
|
+
@media(max-width:540px) { .ftshare-settings header { flex-direction:column; gap:10px; } }
|
|
703
|
+
`;
|
|
704
|
+
// src/core/shell.tsx
|
|
705
|
+
/** Section slot this seam contributes into; declared by the settings shell. */
|
|
706
|
+
const FTSHARE_SETTINGS_SECTION_NAME = "settings.section";
|
|
707
|
+
/**
|
|
708
|
+
* The settings section shell: core CSS variables, the section chrome, and the
|
|
709
|
+
* single render site every card appears at. Cards own everything inside it.
|
|
710
|
+
*/
|
|
711
|
+
function FtshareSettingsSection({ renderSlot }) {
|
|
712
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: SETTINGS_CSS }), renderSlot(FTSHARE_SETTINGS_CARD_SLOT, {})] });
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Declare the card slot and contribute the section that renders it.
|
|
716
|
+
*
|
|
717
|
+
* Declaring the child slot is what authorizes the render site; a card
|
|
718
|
+
* registering through {@link FTSHARE_SETTINGS_CARD_SLOT} before this runs waits
|
|
719
|
+
* for the declaration instead of failing.
|
|
720
|
+
* @param ctx - client context carrying the renderer's slots service.
|
|
721
|
+
* @returns idempotent disposer for the section and its declaration.
|
|
722
|
+
*/
|
|
723
|
+
function registerSettingsSection(ctx) {
|
|
724
|
+
return ctx.slots.inject(FTSHARE_SETTINGS_SECTION_NAME, () => ctx.slots.register({
|
|
725
|
+
name: FTSHARE_SETTINGS_SECTION_NAME,
|
|
726
|
+
...FTSHARE_SETTINGS_SECTION,
|
|
727
|
+
children: { [FTSHARE_SETTINGS_CARD_SLOT]: {
|
|
728
|
+
kind: "list",
|
|
729
|
+
scope: "root"
|
|
730
|
+
} }
|
|
731
|
+
}, FtshareSettingsSection));
|
|
732
|
+
}
|
|
733
|
+
// src/brand.ts
|
|
734
|
+
/** One value for both palettes: brand fills are scheme-invariant. */
|
|
735
|
+
const invariant = (value) => ({
|
|
736
|
+
light: value,
|
|
737
|
+
dark: value
|
|
738
|
+
});
|
|
739
|
+
/** Layer identity reported to `ctx.theme.overrideTokens` (one layer per source). */
|
|
740
|
+
const FT_BRAND_SOURCE = "@dsh-ft/dsh-fft-settings";
|
|
741
|
+
/**
|
|
742
|
+
* The published brand colors.
|
|
743
|
+
*
|
|
744
|
+
* `accent` is the FT yellow used by this plugin's primary action and eyebrow;
|
|
745
|
+
* `accent-warm` is the orange trading accent shared by market and trade
|
|
746
|
+
* surfaces (it is the only palette-aware group); `wechat` is the enterprise-
|
|
747
|
+
* WeChat green the SSO card keeps as its identity. Inks are the text colors on
|
|
748
|
+
* those fills, and `-soft` is the tinted surface behind the accent.
|
|
749
|
+
*/
|
|
750
|
+
const FT_BRAND_TOKENS = Object.freeze({
|
|
751
|
+
"--ft-accent": invariant("#f0c433"),
|
|
752
|
+
"--ft-accent-ink": invariant("#171717"),
|
|
753
|
+
"--ft-accent-warm": {
|
|
754
|
+
light: "#cf5623",
|
|
755
|
+
dark: "#ff8352"
|
|
756
|
+
},
|
|
757
|
+
"--ft-accent-warm-hover": {
|
|
758
|
+
light: "#b8481b",
|
|
759
|
+
dark: "#ff9a72"
|
|
760
|
+
},
|
|
761
|
+
"--ft-accent-warm-ink": {
|
|
762
|
+
light: "#ffffff",
|
|
763
|
+
dark: "#1a120e"
|
|
764
|
+
},
|
|
765
|
+
"--ft-accent-warm-soft": {
|
|
766
|
+
light: "#fbeee8",
|
|
767
|
+
dark: "#2b1d16"
|
|
768
|
+
},
|
|
769
|
+
"--ft-wechat": invariant("#2ebd68"),
|
|
770
|
+
"--ft-wechat-ink": invariant("#102419")
|
|
771
|
+
});
|
|
772
|
+
/**
|
|
773
|
+
* Stack the brand layer once the host theme service exists.
|
|
774
|
+
*
|
|
775
|
+
* The deferred `inject` is deliberate: this plugin's declared client context
|
|
776
|
+
* names only the services its cards read, and a host without `theme` must
|
|
777
|
+
* still load the settings page — the CSS fallbacks then carry the colors.
|
|
778
|
+
* @param ctx - client context carrying the host theme service.
|
|
779
|
+
*/
|
|
780
|
+
function applyBrandTokens(ctx) {
|
|
781
|
+
ctx.inject(["theme"], (scope) => {
|
|
782
|
+
scope.effect(() => scope.theme.overrideTokens(FT_BRAND_SOURCE, FT_BRAND_TOKENS), "ft-settings: FT brand tokens");
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
// src/client.tsx
|
|
786
|
+
const inject = ["slots", "remote"];
|
|
787
|
+
function apply(ctx) {
|
|
788
|
+
registerFtshareSettings(ctx);
|
|
789
|
+
applyBrandTokens(ctx);
|
|
790
|
+
}
|
|
791
|
+
/** Contribute the section shell, then the built-in cards through the slot it declares. */
|
|
792
|
+
function registerFtshareSettings(ctx) {
|
|
793
|
+
registerSettingsSection(ctx);
|
|
794
|
+
registerApiKeyCard(ctx);
|
|
795
|
+
registerLogsCard(ctx);
|
|
796
|
+
}
|
|
797
|
+
exports.FtshareSettingsSection = FtshareSettingsSection;
|
|
798
|
+
exports.apply = apply;
|
|
799
|
+
exports.inject = inject;
|
|
800
|
+
exports.registerFtshareSettings = registerFtshareSettings;
|
|
801
|
+
exports.registerSettingsSection = registerSettingsSection;
|
|
802
|
+
return module.exports;
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
//# sourceMappingURL=client.js.map
|