luckytri 1.0.0 → 1.0.2
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/.github/SECURITY.md +4 -4
- package/README.en.md +252 -250
- package/README.md +252 -250
- package/bin/luckytri.js +5 -1
- package/docs/en/CHANGELOG.md +8 -0
- package/docs/en/connect.md +18 -18
- package/docs/en/guide.md +202 -216
- package/docs/en/npm-release.md +25 -25
- package/docs/zh/CHANGELOG.md +8 -0
- package/docs/zh/connect.md +17 -17
- package/docs/zh/guide.md +202 -216
- package/docs/zh/npm-release.md +33 -33
- package/package.json +110 -108
- package/plugins/sdk/index.d.ts +155 -155
- package/plugins/sdk/testing.js +58 -58
- package/plugins/weather/index.js +82 -82
- package/plugins/weather/luckytri-plugin.json +29 -29
- package/public/app/assets/ChatsPage-BhlJHseG.js +3 -0
- package/public/app/assets/Empty-DP6WMGHi.js +2 -0
- package/public/app/assets/Field-CtbdqaiB.js +1 -0
- package/public/app/assets/HeartPage-32BeTPn2.js +1 -0
- package/public/app/assets/LifePage-CxSm0XKI.js +1 -0
- package/public/app/assets/MemoryPage-BeKBcPCY.js +1 -0
- package/public/app/assets/NaturePage-NEPYl2HS.js +3 -0
- package/public/app/assets/PeoplePage-DxtpekwM.js +1 -0
- package/public/app/assets/PluginsPage-CVVQwEBI.css +1 -0
- package/public/app/assets/PluginsPage-DksEZbUU.js +2 -0
- package/public/app/assets/{SystemPage-CAMPoYqM.css → SystemPage-BkRu0Rvx.css} +1 -1
- package/public/app/assets/SystemPage-BvzJlHuY.js +1 -0
- package/public/app/assets/TimePage-7-awH5XL.js +3 -0
- package/public/app/assets/index-4rItIkD-.js +4 -0
- package/public/app/assets/{index-YDz1ree1.css → index-DY4e15Kh.css} +1 -1
- package/public/app/assets/{knowledge-DG0e0uKw.js → knowledge-C6_HvoyB.js} +1 -1
- package/public/app/assets/media-SMfTT-yc.js +1 -0
- package/public/app/index.html +3 -3
- package/public/guide.en.html +72 -99
- package/public/guide.html +44 -86
- package/public/tutorial.en.md +25 -39
- package/public/tutorial.md +25 -39
- package/scripts/backup.js +311 -311
- package/scripts/browser.js +41 -41
- package/scripts/import-data.js +128 -0
- package/scripts/launch.js +101 -94
- package/scripts/plugin.js +150 -150
- package/scripts/reset-password.js +22 -0
- package/scripts/runtime.js +25 -25
- package/scripts/service.js +56 -47
- package/scripts/setup.js +50 -50
- package/scripts/start-server.js +41 -41
- package/scripts/stop.js +36 -36
- package/server/app.js +112 -96
- package/server/auth.js +217 -0
- package/server/channels/contract.js +57 -57
- package/server/channels/hub.js +165 -165
- package/server/channels/index.js +28 -25
- package/server/channels/onebot/index.js +335 -336
- package/server/channels/onebot/token.js +22 -3
- package/server/core/capabilities.js +208 -208
- package/server/core/events.js +64 -60
- package/server/core/image-preprocessor.js +208 -208
- package/server/core/model-manager.js +1213 -1213
- package/server/core/net-guard.js +49 -49
- package/server/core/orchestrator.js +569 -569
- package/server/core/perceivers.js +91 -91
- package/server/core/persona-manager.js +243 -243
- package/server/core/repository.js +293 -293
- package/server/core/response-validator.js +508 -508
- package/server/core/turn-processor.js +630 -630
- package/server/core/turn.js +236 -236
- package/server/core/vision-manager.js +633 -633
- package/server/i18n/errors.js +548 -497
- package/server/i18n/labels.js +649 -653
- package/server/index.js +151 -102
- package/server/knowledge/manager.js +386 -386
- package/server/knowledge/schema.js +12 -8
- package/server/local-access.js +32 -0
- package/server/mind/anticipations.js +507 -507
- package/server/mind/api.js +703 -703
- package/server/mind/budget.js +152 -152
- package/server/mind/context-selection.js +61 -61
- package/server/mind/day-planner.js +324 -324
- package/server/mind/evidence.js +148 -148
- package/server/mind/index.js +445 -445
- package/server/mind/life/diary.js +786 -786
- package/server/mind/life/index.js +1049 -1049
- package/server/mind/observations.js +84 -84
- package/server/mind/own-day.js +324 -324
- package/server/mind/own-voice.js +318 -318
- package/server/mind/senses.js +53 -53
- package/server/mind/time/activity-clock.js +196 -196
- package/server/mind/time/availability.js +69 -69
- package/server/mind/time/commitment-review.js +198 -198
- package/server/mind/time/executor.js +512 -512
- package/server/mind/time/experiences.js +128 -128
- package/server/mind/time/games.js +562 -562
- package/server/mind/time/index.js +739 -739
- package/server/mind/time/intent.js +166 -166
- package/server/mind/time/kinds.js +171 -171
- package/server/mind/time/plugin-activity.js +207 -207
- package/server/mind/time/tasks.js +587 -587
- package/server/mind/util.js +110 -110
- package/server/mind/view.js +273 -273
- package/server/paths.js +102 -102
- package/server/plugins/api.js +204 -204
- package/server/plugins/host.js +967 -967
- package/server/plugins/install.js +295 -295
- package/server/plugins/manifest.js +201 -201
- package/server/plugins/permissions.js +42 -42
- package/server/plugins/runner/guard.mjs +36 -36
- package/server/plugins/runner/worker.mjs +250 -250
- package/server/plugins/schema.js +91 -91
- package/server/plugins/zip.js +112 -112
- package/server/startup-lock.js +118 -118
- package/server/storage/archive.js +242 -241
- package/server/storage/backup-cleanup-worker.js +8 -8
- package/server/storage/backup-cleanup.js +415 -414
- package/server/storage/backup-lock.js +76 -76
- package/server/storage/backup-scheduler.js +131 -131
- package/server/storage/data-package.js +298 -0
- package/server/storage/data-transfer-api.js +50 -0
- package/server/storage/data-transfer-worker.js +18 -0
- package/server/storage/data-transfer.js +415 -0
- package/server/storage/store.js +202 -199
- package/server/studio/readiness.js +181 -190
- package/server/studio/settings.js +236 -236
- package/public/app/assets/ChatsPage-C-FcRapR.js +0 -3
- package/public/app/assets/Empty-CpP5kQHH.js +0 -2
- package/public/app/assets/Field-D4gY-INU.js +0 -1
- package/public/app/assets/HeartPage-CPy_thQd.js +0 -1
- package/public/app/assets/LifePage-CxWvwmSf.js +0 -1
- package/public/app/assets/MemoryPage-DC4rhQ68.js +0 -1
- package/public/app/assets/NaturePage-IvYw04Zq.js +0 -3
- package/public/app/assets/PeoplePage-Cqefm9-q.js +0 -1
- package/public/app/assets/PluginsPage-DKSaKvXb.js +0 -2
- package/public/app/assets/PluginsPage-HyBe5D8w.css +0 -1
- package/public/app/assets/SystemPage-CJmtJkrK.js +0 -1
- package/public/app/assets/TimePage-Cw44inM4.js +0 -3
- package/public/app/assets/index-BheRW-1t.js +0 -4
- package/public/app/assets/media-BzPDBm7e.js +0 -1
|
@@ -1,190 +1,181 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { defaultModel, pickModel } from "../core/model-manager.js";
|
|
3
|
-
|
|
4
|
-
// The health panel and connection test must describe the same saved profile.
|
|
5
|
-
export function savedConnectionSettings(store, modelId = "") {
|
|
6
|
-
const hasProfiles = store.db
|
|
7
|
-
.prepare(
|
|
8
|
-
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='core_config'",
|
|
9
|
-
)
|
|
10
|
-
.get();
|
|
11
|
-
const row =
|
|
12
|
-
hasProfiles &&
|
|
13
|
-
store.db.prepare("SELECT value FROM core_config WHERE id='models'").get();
|
|
14
|
-
const models = row ? JSON.parse(row.value) : [];
|
|
15
|
-
const profile = Array.isArray(models)
|
|
16
|
-
? modelId
|
|
17
|
-
? models.find((m) => m.id === modelId)
|
|
18
|
-
: pickModel(models, "default")
|
|
19
|
-
: null;
|
|
20
|
-
if (modelId && !profile) return null;
|
|
21
|
-
const settings = store.settings();
|
|
22
|
-
const resolved = profile || defaultModel(settings);
|
|
23
|
-
return {
|
|
24
|
-
isDefault: !profile || !!profile.isDefault,
|
|
25
|
-
profile: resolved,
|
|
26
|
-
settings: profile
|
|
27
|
-
? {
|
|
28
|
-
...settings,
|
|
29
|
-
baseUrl: profile.baseUrl,
|
|
30
|
-
model: profile.model,
|
|
31
|
-
apiKey: profile.apiKey || "",
|
|
32
|
-
providerPreset: profile.provider || settings.providerPreset,
|
|
33
|
-
reasoningEffort: profile.reasoningEffort || "none",
|
|
34
|
-
temperature: profile.temperature ?? settings.temperature,
|
|
35
|
-
topP: profile.topP ?? settings.topP,
|
|
36
|
-
maxTokens: profile.maxOutputTokens || settings.maxTokens || 256,
|
|
37
|
-
}
|
|
38
|
-
: settings,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
export function modelSignature(settings) {
|
|
42
|
-
return createHash("sha256")
|
|
43
|
-
.update(
|
|
44
|
-
JSON.stringify([
|
|
45
|
-
settings.baseUrl,
|
|
46
|
-
settings.model,
|
|
47
|
-
process.env.LLM_API_KEY || settings.apiKey,
|
|
48
|
-
settings.providerPreset,
|
|
49
|
-
settings.reasoningEffort,
|
|
50
|
-
settings.temperature,
|
|
51
|
-
settings.topP,
|
|
52
|
-
settings.maxTokens,
|
|
53
|
-
]),
|
|
54
|
-
)
|
|
55
|
-
.digest("hex");
|
|
56
|
-
}
|
|
57
|
-
export function recordModelCheck(store, settings, ok, latency, error = "") {
|
|
58
|
-
store.db
|
|
59
|
-
.prepare(
|
|
60
|
-
"INSERT INTO model_checks(id,signature,ok,latency,time,error) VALUES (1,?,?,?,?,?) ON CONFLICT(id) DO UPDATE SET signature=excluded.signature,ok=excluded.ok,latency=excluded.latency,time=excluded.time,error=excluded.error",
|
|
61
|
-
)
|
|
62
|
-
.run(modelSignature(settings), Number(ok), latency, Date.now(), error);
|
|
63
|
-
}
|
|
64
|
-
// The steps to get a channel talking. Which two come first depends on how she
|
|
65
|
-
// is connected: a OneBot client needs the shared token, the official bot needs
|
|
66
|
-
// its AppID and AppSecret.
|
|
67
|
-
function connectionChecks({ channel, online, tokenConfigured, qqbot }) {
|
|
68
|
-
if (channel === "qqbot") {
|
|
69
|
-
const configured = !!qqbot?.configured;
|
|
70
|
-
const groups = qqbot?.groupMessages;
|
|
71
|
-
return [
|
|
72
|
-
{
|
|
73
|
-
id: "credentials",
|
|
74
|
-
name: "QQ 官方机器人凭据",
|
|
75
|
-
done: configured,
|
|
76
|
-
detail: configured
|
|
77
|
-
? `AppID ${qqbot.appId}`
|
|
78
|
-
: "在「系统 → 连接 QQ」填写机器人的 AppID 和 AppSecret",
|
|
79
|
-
tab: "setup",
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
id: "qq",
|
|
83
|
-
name: "QQ 连接",
|
|
84
|
-
done: online,
|
|
85
|
-
detail: online
|
|
86
|
-
? groups === "mentions"
|
|
87
|
-
? "已连接 QQ 开放平台;群里目前只收到 @ 她的消息,在开放平台开启「接收所有消息」后她才能看到整个群"
|
|
88
|
-
: "已连接 QQ 开放平台"
|
|
89
|
-
: qqbot?.error ||
|
|
90
|
-
(configured ? "正在连接 QQ 开放平台" : "填写凭据后自动连接"),
|
|
91
|
-
tab: "settings",
|
|
92
|
-
},
|
|
93
|
-
];
|
|
94
|
-
}
|
|
95
|
-
return [
|
|
96
|
-
{
|
|
97
|
-
id: "
|
|
98
|
-
name: "QQ
|
|
99
|
-
done:
|
|
100
|
-
detail:
|
|
101
|
-
? "
|
|
102
|
-
: "
|
|
103
|
-
tab: "
|
|
104
|
-
},
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
tab: "
|
|
165
|
-
},
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
? {
|
|
183
|
-
current,
|
|
184
|
-
ok: current && !!record.ok,
|
|
185
|
-
time: record.time,
|
|
186
|
-
latency: record.latency,
|
|
187
|
-
}
|
|
188
|
-
: null,
|
|
189
|
-
};
|
|
190
|
-
}
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { defaultModel, pickModel } from "../core/model-manager.js";
|
|
3
|
+
|
|
4
|
+
// The health panel and connection test must describe the same saved profile.
|
|
5
|
+
export function savedConnectionSettings(store, modelId = "") {
|
|
6
|
+
const hasProfiles = store.db
|
|
7
|
+
.prepare(
|
|
8
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='core_config'",
|
|
9
|
+
)
|
|
10
|
+
.get();
|
|
11
|
+
const row =
|
|
12
|
+
hasProfiles &&
|
|
13
|
+
store.db.prepare("SELECT value FROM core_config WHERE id='models'").get();
|
|
14
|
+
const models = row ? JSON.parse(row.value) : [];
|
|
15
|
+
const profile = Array.isArray(models)
|
|
16
|
+
? modelId
|
|
17
|
+
? models.find((m) => m.id === modelId)
|
|
18
|
+
: pickModel(models, "default")
|
|
19
|
+
: null;
|
|
20
|
+
if (modelId && !profile) return null;
|
|
21
|
+
const settings = store.settings();
|
|
22
|
+
const resolved = profile || defaultModel(settings);
|
|
23
|
+
return {
|
|
24
|
+
isDefault: !profile || !!profile.isDefault,
|
|
25
|
+
profile: resolved,
|
|
26
|
+
settings: profile
|
|
27
|
+
? {
|
|
28
|
+
...settings,
|
|
29
|
+
baseUrl: profile.baseUrl,
|
|
30
|
+
model: profile.model,
|
|
31
|
+
apiKey: profile.apiKey || "",
|
|
32
|
+
providerPreset: profile.provider || settings.providerPreset,
|
|
33
|
+
reasoningEffort: profile.reasoningEffort || "none",
|
|
34
|
+
temperature: profile.temperature ?? settings.temperature,
|
|
35
|
+
topP: profile.topP ?? settings.topP,
|
|
36
|
+
maxTokens: profile.maxOutputTokens || settings.maxTokens || 256,
|
|
37
|
+
}
|
|
38
|
+
: settings,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function modelSignature(settings) {
|
|
42
|
+
return createHash("sha256")
|
|
43
|
+
.update(
|
|
44
|
+
JSON.stringify([
|
|
45
|
+
settings.baseUrl,
|
|
46
|
+
settings.model,
|
|
47
|
+
process.env.LLM_API_KEY || settings.apiKey,
|
|
48
|
+
settings.providerPreset,
|
|
49
|
+
settings.reasoningEffort,
|
|
50
|
+
settings.temperature,
|
|
51
|
+
settings.topP,
|
|
52
|
+
settings.maxTokens,
|
|
53
|
+
]),
|
|
54
|
+
)
|
|
55
|
+
.digest("hex");
|
|
56
|
+
}
|
|
57
|
+
export function recordModelCheck(store, settings, ok, latency, error = "") {
|
|
58
|
+
store.db
|
|
59
|
+
.prepare(
|
|
60
|
+
"INSERT INTO model_checks(id,signature,ok,latency,time,error) VALUES (1,?,?,?,?,?) ON CONFLICT(id) DO UPDATE SET signature=excluded.signature,ok=excluded.ok,latency=excluded.latency,time=excluded.time,error=excluded.error",
|
|
61
|
+
)
|
|
62
|
+
.run(modelSignature(settings), Number(ok), latency, Date.now(), error);
|
|
63
|
+
}
|
|
64
|
+
// The steps to get a channel talking. Which two come first depends on how she
|
|
65
|
+
// is connected: a OneBot client needs the shared token, the official bot needs
|
|
66
|
+
// its AppID and AppSecret.
|
|
67
|
+
function connectionChecks({ channel, online, tokenConfigured, qqbot }) {
|
|
68
|
+
if (channel === "qqbot") {
|
|
69
|
+
const configured = !!qqbot?.configured;
|
|
70
|
+
const groups = qqbot?.groupMessages;
|
|
71
|
+
return [
|
|
72
|
+
{
|
|
73
|
+
id: "credentials",
|
|
74
|
+
name: "QQ 官方机器人凭据",
|
|
75
|
+
done: configured,
|
|
76
|
+
detail: configured
|
|
77
|
+
? `AppID ${qqbot.appId}`
|
|
78
|
+
: "在「系统 → 连接 QQ」填写机器人的 AppID 和 AppSecret",
|
|
79
|
+
tab: "setup",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "qq",
|
|
83
|
+
name: "QQ 连接",
|
|
84
|
+
done: online,
|
|
85
|
+
detail: online
|
|
86
|
+
? groups === "mentions"
|
|
87
|
+
? "已连接 QQ 开放平台;群里目前只收到 @ 她的消息,在开放平台开启「接收所有消息」后她才能看到整个群"
|
|
88
|
+
: "已连接 QQ 开放平台"
|
|
89
|
+
: qqbot?.error ||
|
|
90
|
+
(configured ? "正在连接 QQ 开放平台" : "填写凭据后自动连接"),
|
|
91
|
+
tab: "settings",
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
return [
|
|
96
|
+
{
|
|
97
|
+
id: "qq",
|
|
98
|
+
name: "QQ 连接",
|
|
99
|
+
done: online,
|
|
100
|
+
detail: online
|
|
101
|
+
? "OneBot 11 反向 WebSocket 已连接"
|
|
102
|
+
: "本机接入无需单独令牌,请确认接入端已登录并启用反向 WebSocket 客户端",
|
|
103
|
+
tab: "settings",
|
|
104
|
+
},
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function readiness(
|
|
109
|
+
store,
|
|
110
|
+
{
|
|
111
|
+
online = false,
|
|
112
|
+
tokenConfigured = !!process.env.ONEBOT_TOKEN,
|
|
113
|
+
channel = "onebot",
|
|
114
|
+
qqbot = null,
|
|
115
|
+
} = {},
|
|
116
|
+
) {
|
|
117
|
+
const s = store.settings(),
|
|
118
|
+
record = store.db.prepare("SELECT * FROM model_checks WHERE id=1").get();
|
|
119
|
+
const effective = savedConnectionSettings(store).settings;
|
|
120
|
+
// Previous connection tests signed their temporary 256-token test budget.
|
|
121
|
+
// Accept that exact historical signature, without marking untested profiles ready.
|
|
122
|
+
const current =
|
|
123
|
+
!!record &&
|
|
124
|
+
[
|
|
125
|
+
effective,
|
|
126
|
+
{ ...effective, maxTokens: Math.min(effective.maxTokens || 256, 256) },
|
|
127
|
+
].some((value) => record.signature === modelSignature(value));
|
|
128
|
+
const sessions = store.db
|
|
129
|
+
.prepare("SELECT COUNT(*) n FROM sessions WHERE enabled=1")
|
|
130
|
+
.get().n;
|
|
131
|
+
const checks = [
|
|
132
|
+
...connectionChecks({ channel, online, tokenConfigured, qqbot }),
|
|
133
|
+
{
|
|
134
|
+
id: "model",
|
|
135
|
+
name: "当前模型已测试",
|
|
136
|
+
done: current && !!record.ok,
|
|
137
|
+
detail: current
|
|
138
|
+
? record.ok
|
|
139
|
+
? `测试成功 · ${record.latency} ms`
|
|
140
|
+
: record.error
|
|
141
|
+
: record
|
|
142
|
+
? "模型配置已变更,请重新测试"
|
|
143
|
+
: "保存模型配置后,点击测试模型连接",
|
|
144
|
+
tab: "settings",
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: "mode",
|
|
148
|
+
name: "开启真实回复",
|
|
149
|
+
done: !s.demo && s.enabled,
|
|
150
|
+
detail: s.demo
|
|
151
|
+
? "当前是模拟模式,不向 QQ 发言"
|
|
152
|
+
: !s.enabled
|
|
153
|
+
? "总开关已关闭"
|
|
154
|
+
: "真实模式与总开关已开启",
|
|
155
|
+
tab: "settings",
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: "session",
|
|
159
|
+
name: "选择参与的会话",
|
|
160
|
+
done: sessions > 0,
|
|
161
|
+
detail: sessions
|
|
162
|
+
? `${sessions} 个会话已开启`
|
|
163
|
+
: "到「对话」里开启一个群聊或私聊",
|
|
164
|
+
tab: "sessions",
|
|
165
|
+
},
|
|
166
|
+
];
|
|
167
|
+
return {
|
|
168
|
+
checks,
|
|
169
|
+
completed: checks.filter((x) => x.done).length,
|
|
170
|
+
total: checks.length,
|
|
171
|
+
ready: checks.every((x) => x.done),
|
|
172
|
+
modelTest: record
|
|
173
|
+
? {
|
|
174
|
+
current,
|
|
175
|
+
ok: current && !!record.ok,
|
|
176
|
+
time: record.time,
|
|
177
|
+
latency: record.latency,
|
|
178
|
+
}
|
|
179
|
+
: null,
|
|
180
|
+
};
|
|
181
|
+
}
|