android-midscene-automation 0.1.24 → 0.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -5
- package/README.md +15 -5
- package/package.json +1 -1
- package/server/http-api.ts +141 -105
- package/server/script-agent.ts +50 -45
- package/src/App.vue +133 -54
- package/src/api.ts +15 -10
- package/src/appium-recorder/AppiumPage.vue +156 -55
- package/src/appium-recorder/components/FlowCanvas.vue +2 -2
- package/src/appium-recorder/components/FlowStepEditor.vue +93 -3
- package/src/appium-recorder/components/NestedConditionBranches.vue +6 -0
- package/src/appium-recorder/flow-copy.ts +49 -18
- package/src/appium-recorder/flow-graph.ts +3 -2
- package/src/appium-recorder/flow-labels.ts +4 -1
- package/src/config/prompt-example.ts +4 -0
- package/src/config/prompt-presets.ts +172 -0
- package/src/pages/AiGeneratorPage.vue +188 -5
- package/src/pages/AutomationPage.vue +3 -3
- package/src/pages/ConfigPage.vue +13 -35
- package/src/pages/GeneratorPage.vue +7 -0
- package/src/style.css +108 -139
- package/src/types.ts +0 -14
- package/server/model-call-usage-importer.ts +0 -108
- package/server/model-usage-repository.ts +0 -131
- package/src/components/config/ModelUsageChart.vue +0 -188
package/src/style.css
CHANGED
|
@@ -120,6 +120,52 @@ select {
|
|
|
120
120
|
padding: 6px 16px 14px;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
.action-dialog__footer-actions {
|
|
124
|
+
display: flex;
|
|
125
|
+
justify-content: flex-end;
|
|
126
|
+
gap: 8px;
|
|
127
|
+
width: 100%;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.action-dialog__footer-button.el-button {
|
|
131
|
+
min-width: 88px;
|
|
132
|
+
height: 34px;
|
|
133
|
+
padding: 0 16px;
|
|
134
|
+
border-radius: 7px;
|
|
135
|
+
font-size: 14px;
|
|
136
|
+
font-weight: 700;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.action-dialog__footer-button.el-button + .action-dialog__footer-button.el-button {
|
|
140
|
+
margin-left: 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.action-dialog__footer-button--secondary.el-button {
|
|
144
|
+
border-color: #e5e7eb;
|
|
145
|
+
background: #fff;
|
|
146
|
+
color: #8b95a5;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.action-dialog__footer-button--secondary.el-button:hover,
|
|
150
|
+
.action-dialog__footer-button--secondary.el-button:focus {
|
|
151
|
+
border-color: #d7dce5;
|
|
152
|
+
background: #f8fafc;
|
|
153
|
+
color: #6b7280;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.action-dialog__footer-button--primary.el-button {
|
|
157
|
+
border-color: #ffd436;
|
|
158
|
+
background: #ffd436;
|
|
159
|
+
color: #111827;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.action-dialog__footer-button--primary.el-button:hover,
|
|
163
|
+
.action-dialog__footer-button--primary.el-button:focus {
|
|
164
|
+
border-color: #ffca1f;
|
|
165
|
+
background: #ffca1f;
|
|
166
|
+
color: #111827;
|
|
167
|
+
}
|
|
168
|
+
|
|
123
169
|
@keyframes action-dialog-spin {
|
|
124
170
|
from {
|
|
125
171
|
transform: rotate(0deg);
|
|
@@ -149,6 +195,10 @@ select {
|
|
|
149
195
|
gap: 12px;
|
|
150
196
|
}
|
|
151
197
|
|
|
198
|
+
.layout-header__button--loading .el-icon {
|
|
199
|
+
animation: action-dialog-spin 1s linear infinite;
|
|
200
|
+
}
|
|
201
|
+
|
|
152
202
|
.page-body {
|
|
153
203
|
height: 100%;
|
|
154
204
|
min-height: 0;
|
|
@@ -192,7 +242,7 @@ select {
|
|
|
192
242
|
.appium-recorder-layout {
|
|
193
243
|
display: grid;
|
|
194
244
|
flex: 1 1 auto;
|
|
195
|
-
grid-template-columns: minmax(
|
|
245
|
+
grid-template-columns: minmax(320px, 2.8fr) minmax(0, 3fr) minmax(0, 4.2fr);
|
|
196
246
|
gap: 12px;
|
|
197
247
|
align-items: stretch;
|
|
198
248
|
height: auto;
|
|
@@ -1197,6 +1247,20 @@ select {
|
|
|
1197
1247
|
gap: 8px;
|
|
1198
1248
|
}
|
|
1199
1249
|
|
|
1250
|
+
.appium-flow-editor__grid--swipe {
|
|
1251
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
.appium-swipe-message-box {
|
|
1255
|
+
width: min(560px, calc(100vw - 32px));
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
.appium-swipe-dialog-grid {
|
|
1259
|
+
display: grid;
|
|
1260
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1261
|
+
gap: 10px 12px;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1200
1264
|
.appium-flow-insert {
|
|
1201
1265
|
position: relative;
|
|
1202
1266
|
z-index: 1;
|
|
@@ -1609,137 +1673,6 @@ select {
|
|
|
1609
1673
|
gap: 18px;
|
|
1610
1674
|
}
|
|
1611
1675
|
|
|
1612
|
-
.model-usage-chart {
|
|
1613
|
-
margin-top: 20px;
|
|
1614
|
-
padding-top: 18px;
|
|
1615
|
-
border-top: 1px solid #ebeef5;
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1618
|
-
.model-usage-chart__body {
|
|
1619
|
-
display: grid;
|
|
1620
|
-
gap: 10px;
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
.model-usage-chart__summary,
|
|
1624
|
-
.model-usage-chart__legend,
|
|
1625
|
-
.model-usage-chart__record {
|
|
1626
|
-
display: flex;
|
|
1627
|
-
align-items: center;
|
|
1628
|
-
gap: 12px;
|
|
1629
|
-
min-width: 0;
|
|
1630
|
-
color: #64748b;
|
|
1631
|
-
font-size: 12px;
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
|
-
.model-usage-chart__summary {
|
|
1635
|
-
flex-wrap: wrap;
|
|
1636
|
-
}
|
|
1637
|
-
|
|
1638
|
-
.model-usage-chart__svg {
|
|
1639
|
-
width: 100%;
|
|
1640
|
-
height: 180px;
|
|
1641
|
-
display: block;
|
|
1642
|
-
border: 1px solid #ebeef5;
|
|
1643
|
-
border-radius: 6px;
|
|
1644
|
-
background: #fbfdff;
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
.model-usage-chart__axis {
|
|
1648
|
-
stroke: #d8dee9;
|
|
1649
|
-
stroke-width: 1;
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1652
|
-
.model-usage-chart__line {
|
|
1653
|
-
fill: none;
|
|
1654
|
-
stroke-linecap: round;
|
|
1655
|
-
stroke-linejoin: round;
|
|
1656
|
-
stroke-width: 2.5;
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1659
|
-
.model-usage-chart__line--prompt,
|
|
1660
|
-
.model-usage-chart__point--prompt {
|
|
1661
|
-
stroke: #2563eb;
|
|
1662
|
-
}
|
|
1663
|
-
|
|
1664
|
-
.model-usage-chart__line--completion,
|
|
1665
|
-
.model-usage-chart__point--completion {
|
|
1666
|
-
stroke: #d97706;
|
|
1667
|
-
}
|
|
1668
|
-
|
|
1669
|
-
.model-usage-chart__line--duration,
|
|
1670
|
-
.model-usage-chart__point--duration {
|
|
1671
|
-
stroke: #16a34a;
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
|
-
.model-usage-chart__point {
|
|
1675
|
-
fill: #fff;
|
|
1676
|
-
stroke-width: 2;
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
|
-
.model-usage-chart__legend {
|
|
1680
|
-
flex-wrap: wrap;
|
|
1681
|
-
}
|
|
1682
|
-
|
|
1683
|
-
.model-usage-chart__legend span {
|
|
1684
|
-
display: inline-flex;
|
|
1685
|
-
align-items: center;
|
|
1686
|
-
gap: 6px;
|
|
1687
|
-
}
|
|
1688
|
-
|
|
1689
|
-
.model-usage-chart__swatch {
|
|
1690
|
-
width: 18px;
|
|
1691
|
-
height: 3px;
|
|
1692
|
-
border-radius: 999px;
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
.model-usage-chart__swatch--prompt {
|
|
1696
|
-
background: #2563eb;
|
|
1697
|
-
}
|
|
1698
|
-
|
|
1699
|
-
.model-usage-chart__swatch--completion {
|
|
1700
|
-
background: #d97706;
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
.model-usage-chart__swatch--duration {
|
|
1704
|
-
background: #16a34a;
|
|
1705
|
-
}
|
|
1706
|
-
|
|
1707
|
-
.model-usage-chart__records {
|
|
1708
|
-
display: grid;
|
|
1709
|
-
gap: 6px;
|
|
1710
|
-
height: 220px;
|
|
1711
|
-
align-content: start;
|
|
1712
|
-
overflow-y: auto;
|
|
1713
|
-
padding-right: 4px;
|
|
1714
|
-
}
|
|
1715
|
-
|
|
1716
|
-
.model-usage-chart__record {
|
|
1717
|
-
display: grid;
|
|
1718
|
-
grid-template-columns: 86px 64px minmax(120px, 1fr) 84px 84px 84px 72px;
|
|
1719
|
-
justify-content: initial;
|
|
1720
|
-
min-height: 38px;
|
|
1721
|
-
padding: 6px 8px;
|
|
1722
|
-
border-radius: 6px;
|
|
1723
|
-
background: #f8fafc;
|
|
1724
|
-
}
|
|
1725
|
-
|
|
1726
|
-
.model-usage-chart__record--head {
|
|
1727
|
-
position: sticky;
|
|
1728
|
-
top: 0;
|
|
1729
|
-
z-index: 1;
|
|
1730
|
-
min-height: 30px;
|
|
1731
|
-
background: #eef2f7;
|
|
1732
|
-
color: #475569;
|
|
1733
|
-
font-weight: 700;
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
.model-usage-chart__record span {
|
|
1737
|
-
min-width: 0;
|
|
1738
|
-
overflow: hidden;
|
|
1739
|
-
text-overflow: ellipsis;
|
|
1740
|
-
white-space: nowrap;
|
|
1741
|
-
}
|
|
1742
|
-
|
|
1743
1676
|
.app-preset-list {
|
|
1744
1677
|
display: grid;
|
|
1745
1678
|
gap: 8px;
|
|
@@ -2101,6 +2034,7 @@ select {
|
|
|
2101
2034
|
display: grid;
|
|
2102
2035
|
grid-template-columns: 420px minmax(0, 1fr);
|
|
2103
2036
|
gap: 16px;
|
|
2037
|
+
height: 100%;
|
|
2104
2038
|
min-height: 0;
|
|
2105
2039
|
}
|
|
2106
2040
|
|
|
@@ -2126,12 +2060,19 @@ select {
|
|
|
2126
2060
|
|
|
2127
2061
|
.execution-card > .el-card__body {
|
|
2128
2062
|
display: flex;
|
|
2063
|
+
flex: 1 1 auto;
|
|
2064
|
+
min-height: 0;
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
.execution-card.el-card {
|
|
2068
|
+
height: 100%;
|
|
2129
2069
|
}
|
|
2130
2070
|
|
|
2131
2071
|
.execution-card .el-form {
|
|
2132
2072
|
display: flex;
|
|
2133
2073
|
flex: 1;
|
|
2134
2074
|
width: 100%;
|
|
2075
|
+
height: 100%;
|
|
2135
2076
|
min-width: 0;
|
|
2136
2077
|
min-height: 0;
|
|
2137
2078
|
flex-direction: column;
|
|
@@ -2141,12 +2082,17 @@ select {
|
|
|
2141
2082
|
flex: 0 0 auto;
|
|
2142
2083
|
}
|
|
2143
2084
|
|
|
2085
|
+
.execution-card .execution-script-item {
|
|
2086
|
+
margin-bottom: 12px;
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2144
2089
|
.execution-card .execution-output-item {
|
|
2145
2090
|
display: flex;
|
|
2146
2091
|
flex-direction: column;
|
|
2147
2092
|
align-items: stretch;
|
|
2148
2093
|
flex: 1 1 auto;
|
|
2149
2094
|
min-height: 0;
|
|
2095
|
+
margin-bottom: 0;
|
|
2150
2096
|
}
|
|
2151
2097
|
|
|
2152
2098
|
.execution-card .execution-output-item .el-form-item__content {
|
|
@@ -2574,11 +2520,13 @@ select {
|
|
|
2574
2520
|
flex: 1 1 auto;
|
|
2575
2521
|
width: 100%;
|
|
2576
2522
|
min-width: 0;
|
|
2577
|
-
height:
|
|
2523
|
+
height: 100%;
|
|
2524
|
+
min-height: 0;
|
|
2578
2525
|
padding: 0;
|
|
2579
|
-
border: 1px solid #
|
|
2526
|
+
border: 1px solid #263244;
|
|
2580
2527
|
border-radius: 6px;
|
|
2581
|
-
background: #
|
|
2528
|
+
background: #101722;
|
|
2529
|
+
scrollbar-color: #526174 #101722;
|
|
2582
2530
|
}
|
|
2583
2531
|
|
|
2584
2532
|
.execution-output-header {
|
|
@@ -2623,15 +2571,30 @@ select {
|
|
|
2623
2571
|
border: 0;
|
|
2624
2572
|
outline: 0;
|
|
2625
2573
|
resize: none;
|
|
2626
|
-
background: #
|
|
2627
|
-
color: #
|
|
2574
|
+
background: #101722;
|
|
2575
|
+
color: #d7e0ea;
|
|
2628
2576
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
2629
2577
|
font-size: 12px;
|
|
2630
2578
|
line-height: 1.6;
|
|
2631
2579
|
overflow: auto;
|
|
2632
|
-
white-space: pre;
|
|
2580
|
+
white-space: pre-wrap;
|
|
2633
2581
|
overflow-wrap: normal;
|
|
2634
|
-
word-break:
|
|
2582
|
+
word-break: break-word;
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
.execution-log__output::-webkit-scrollbar {
|
|
2586
|
+
width: 10px;
|
|
2587
|
+
height: 10px;
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
.execution-log__output::-webkit-scrollbar-track {
|
|
2591
|
+
background: #101722;
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
.execution-log__output::-webkit-scrollbar-thumb {
|
|
2595
|
+
border: 2px solid #101722;
|
|
2596
|
+
border-radius: 6px;
|
|
2597
|
+
background: #526174;
|
|
2635
2598
|
}
|
|
2636
2599
|
|
|
2637
2600
|
.execution-process {
|
|
@@ -2745,12 +2708,18 @@ select {
|
|
|
2745
2708
|
|
|
2746
2709
|
.automation-layout {
|
|
2747
2710
|
height: auto;
|
|
2711
|
+
min-height: calc(100vh - 108px);
|
|
2748
2712
|
}
|
|
2749
2713
|
|
|
2750
2714
|
.automation-stage {
|
|
2751
2715
|
grid-template-columns: 1fr;
|
|
2752
2716
|
}
|
|
2753
2717
|
|
|
2718
|
+
.execution-card.el-card {
|
|
2719
|
+
height: min(760px, calc(100vh - 120px));
|
|
2720
|
+
min-height: 560px;
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2754
2723
|
.appium-recorder-layout {
|
|
2755
2724
|
grid-template-columns: 1fr;
|
|
2756
2725
|
height: auto;
|
package/src/types.ts
CHANGED
|
@@ -34,20 +34,6 @@ export type ConfigForm = {
|
|
|
34
34
|
};
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
-
export type ModelUsageRecord = {
|
|
38
|
-
id: string;
|
|
39
|
-
modelKey: 'midscene' | 'scriptOptimizer';
|
|
40
|
-
provider: string;
|
|
41
|
-
modelName: string;
|
|
42
|
-
family: string;
|
|
43
|
-
success: boolean;
|
|
44
|
-
durationMs: number;
|
|
45
|
-
promptTokens?: number;
|
|
46
|
-
completionTokens?: number;
|
|
47
|
-
totalTokens?: number;
|
|
48
|
-
createdAt: string;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
37
|
export type SavedScript = {
|
|
52
38
|
id: string;
|
|
53
39
|
name: string;
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { appDataPath } from './paths';
|
|
4
|
-
import { saveModelUsageRecord } from './model-usage-repository';
|
|
5
|
-
|
|
6
|
-
type MidsceneModelConfig = {
|
|
7
|
-
provider?: string;
|
|
8
|
-
name?: string;
|
|
9
|
-
family?: string;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
type ModelCallUsage = {
|
|
13
|
-
prompt_tokens?: number;
|
|
14
|
-
completion_tokens?: number;
|
|
15
|
-
total_tokens?: number;
|
|
16
|
-
time_cost?: number;
|
|
17
|
-
model_name?: string;
|
|
18
|
-
response_model_name?: string;
|
|
19
|
-
model_description?: string;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
type ModelCallRecord = {
|
|
23
|
-
timestamp?: string;
|
|
24
|
-
type?: string;
|
|
25
|
-
modelFamily?: string;
|
|
26
|
-
provider?: string;
|
|
27
|
-
final?: {
|
|
28
|
-
usage?: ModelCallUsage;
|
|
29
|
-
timeCost?: number;
|
|
30
|
-
};
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
function modelRequestDir() {
|
|
34
|
-
return appDataPath('midscene_run', 'model-requests');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function numberValue(value: unknown) {
|
|
38
|
-
return typeof value === 'number' && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : undefined;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function listModelRequestFilesSince(sinceMs: number) {
|
|
42
|
-
const dir = modelRequestDir();
|
|
43
|
-
if (!fs.existsSync(dir)) return [];
|
|
44
|
-
|
|
45
|
-
return fs
|
|
46
|
-
.readdirSync(dir)
|
|
47
|
-
.filter((name) => name.endsWith('.jsonl'))
|
|
48
|
-
.map((name) => path.join(dir, name))
|
|
49
|
-
.filter((filePath) => {
|
|
50
|
-
const stat = fs.statSync(filePath);
|
|
51
|
-
return stat.mtimeMs >= sinceMs - 1_000;
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function recordsFromFile(filePath: string) {
|
|
56
|
-
return fs
|
|
57
|
-
.readFileSync(filePath, 'utf8')
|
|
58
|
-
.split(/\r?\n/)
|
|
59
|
-
.map((line) => line.trim())
|
|
60
|
-
.filter(Boolean)
|
|
61
|
-
.flatMap((line) => {
|
|
62
|
-
try {
|
|
63
|
-
return [JSON.parse(line) as ModelCallRecord];
|
|
64
|
-
} catch {
|
|
65
|
-
return [];
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function importMidsceneModelUsage(input: {
|
|
71
|
-
sinceMs: number;
|
|
72
|
-
model: MidsceneModelConfig;
|
|
73
|
-
success: boolean;
|
|
74
|
-
}) {
|
|
75
|
-
let imported = 0;
|
|
76
|
-
const files = listModelRequestFilesSince(input.sinceMs);
|
|
77
|
-
|
|
78
|
-
for (const filePath of files) {
|
|
79
|
-
for (const record of recordsFromFile(filePath)) {
|
|
80
|
-
if (record.type !== 'response') continue;
|
|
81
|
-
const usage = record.final?.usage;
|
|
82
|
-
if (!usage) continue;
|
|
83
|
-
|
|
84
|
-
const promptTokens = numberValue(usage.prompt_tokens);
|
|
85
|
-
const completionTokens = numberValue(usage.completion_tokens);
|
|
86
|
-
const totalTokens = numberValue(usage.total_tokens) ?? numberValue((promptTokens || 0) + (completionTokens || 0));
|
|
87
|
-
|
|
88
|
-
saveModelUsageRecord({
|
|
89
|
-
modelKey: 'midscene',
|
|
90
|
-
provider: input.model.provider || (record.provider === 'codex-app-server' ? 'codex' : 'custom'),
|
|
91
|
-
modelName: usage.model_name || usage.response_model_name || input.model.name || '',
|
|
92
|
-
family: input.model.family || record.modelFamily || '',
|
|
93
|
-
success: input.success,
|
|
94
|
-
durationMs: numberValue(usage.time_cost) ?? numberValue(record.final?.timeCost) ?? 0,
|
|
95
|
-
promptTokens,
|
|
96
|
-
completionTokens,
|
|
97
|
-
totalTokens,
|
|
98
|
-
createdAt: record.timestamp,
|
|
99
|
-
});
|
|
100
|
-
imported += 1;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return {
|
|
105
|
-
imported,
|
|
106
|
-
files,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createId,
|
|
3
|
-
querySql,
|
|
4
|
-
runSql,
|
|
5
|
-
sqlNullableString,
|
|
6
|
-
sqlString,
|
|
7
|
-
} from './storage/sqlite';
|
|
8
|
-
|
|
9
|
-
export type ModelUsageRecord = {
|
|
10
|
-
id: string;
|
|
11
|
-
modelKey: 'midscene' | 'scriptOptimizer';
|
|
12
|
-
provider: string;
|
|
13
|
-
modelName: string;
|
|
14
|
-
family: string;
|
|
15
|
-
success: boolean;
|
|
16
|
-
durationMs: number;
|
|
17
|
-
promptTokens?: number;
|
|
18
|
-
completionTokens?: number;
|
|
19
|
-
totalTokens?: number;
|
|
20
|
-
errorMessage?: string;
|
|
21
|
-
createdAt: string;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
type ModelUsageRow = {
|
|
25
|
-
id: string;
|
|
26
|
-
model_key: 'midscene' | 'scriptOptimizer';
|
|
27
|
-
provider: string;
|
|
28
|
-
model_name: string;
|
|
29
|
-
family: string | null;
|
|
30
|
-
success: 0 | 1;
|
|
31
|
-
duration_ms: number;
|
|
32
|
-
prompt_tokens: number | null;
|
|
33
|
-
completion_tokens: number | null;
|
|
34
|
-
total_tokens: number | null;
|
|
35
|
-
error_message: string | null;
|
|
36
|
-
created_at: string;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
let initialized = false;
|
|
40
|
-
|
|
41
|
-
function sqlNullableNumber(value: number | null | undefined) {
|
|
42
|
-
return typeof value === 'number' && Number.isFinite(value) ? String(Math.max(0, Math.floor(value))) : 'NULL';
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function rowToRecord(row: ModelUsageRow): ModelUsageRecord {
|
|
46
|
-
return {
|
|
47
|
-
id: row.id,
|
|
48
|
-
modelKey: row.model_key,
|
|
49
|
-
provider: row.provider,
|
|
50
|
-
modelName: row.model_name,
|
|
51
|
-
family: row.family || '',
|
|
52
|
-
success: row.success === 1,
|
|
53
|
-
durationMs: row.duration_ms,
|
|
54
|
-
promptTokens: row.prompt_tokens ?? undefined,
|
|
55
|
-
completionTokens: row.completion_tokens ?? undefined,
|
|
56
|
-
totalTokens: row.total_tokens ?? undefined,
|
|
57
|
-
errorMessage: row.error_message || undefined,
|
|
58
|
-
createdAt: row.created_at,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function initModelUsageRepository() {
|
|
63
|
-
if (initialized) return;
|
|
64
|
-
initialized = true;
|
|
65
|
-
runSql(`
|
|
66
|
-
PRAGMA journal_mode = WAL;
|
|
67
|
-
CREATE TABLE IF NOT EXISTS model_usage_records (
|
|
68
|
-
id TEXT PRIMARY KEY,
|
|
69
|
-
model_key TEXT NOT NULL,
|
|
70
|
-
provider TEXT NOT NULL,
|
|
71
|
-
model_name TEXT NOT NULL,
|
|
72
|
-
family TEXT NULL,
|
|
73
|
-
success INTEGER NOT NULL,
|
|
74
|
-
duration_ms INTEGER NOT NULL,
|
|
75
|
-
prompt_tokens INTEGER NULL,
|
|
76
|
-
completion_tokens INTEGER NULL,
|
|
77
|
-
total_tokens INTEGER NULL,
|
|
78
|
-
error_message TEXT NULL,
|
|
79
|
-
created_at TEXT NOT NULL
|
|
80
|
-
);
|
|
81
|
-
CREATE INDEX IF NOT EXISTS idx_model_usage_records_created_at ON model_usage_records(created_at DESC);
|
|
82
|
-
`);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function saveModelUsageRecord(input: Omit<ModelUsageRecord, 'id' | 'createdAt'> & { createdAt?: string }) {
|
|
86
|
-
initModelUsageRepository();
|
|
87
|
-
const id = createId('usage');
|
|
88
|
-
const createdAt = input.createdAt || new Date().toISOString();
|
|
89
|
-
runSql(`
|
|
90
|
-
INSERT INTO model_usage_records (
|
|
91
|
-
id,
|
|
92
|
-
model_key,
|
|
93
|
-
provider,
|
|
94
|
-
model_name,
|
|
95
|
-
family,
|
|
96
|
-
success,
|
|
97
|
-
duration_ms,
|
|
98
|
-
prompt_tokens,
|
|
99
|
-
completion_tokens,
|
|
100
|
-
total_tokens,
|
|
101
|
-
error_message,
|
|
102
|
-
created_at
|
|
103
|
-
)
|
|
104
|
-
VALUES (
|
|
105
|
-
${sqlString(id)},
|
|
106
|
-
${sqlString(input.modelKey)},
|
|
107
|
-
${sqlString(input.provider || 'custom')},
|
|
108
|
-
${sqlString(input.modelName || '')},
|
|
109
|
-
${sqlNullableString(input.family)},
|
|
110
|
-
${input.success ? 1 : 0},
|
|
111
|
-
${sqlNullableNumber(input.durationMs)},
|
|
112
|
-
${sqlNullableNumber(input.promptTokens)},
|
|
113
|
-
${sqlNullableNumber(input.completionTokens)},
|
|
114
|
-
${sqlNullableNumber(input.totalTokens)},
|
|
115
|
-
${sqlNullableString(input.errorMessage)},
|
|
116
|
-
${sqlString(createdAt)}
|
|
117
|
-
);
|
|
118
|
-
`);
|
|
119
|
-
return listModelUsageRecords(1)[0];
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export function listModelUsageRecords(limit = 50) {
|
|
123
|
-
initModelUsageRepository();
|
|
124
|
-
const boundedLimit = Math.max(1, Math.min(200, Math.floor(limit)));
|
|
125
|
-
return querySql<ModelUsageRow>(`
|
|
126
|
-
SELECT *
|
|
127
|
-
FROM model_usage_records
|
|
128
|
-
ORDER BY datetime(created_at) DESC
|
|
129
|
-
LIMIT ${boundedLimit};
|
|
130
|
-
`).map(rowToRecord).reverse();
|
|
131
|
-
}
|