dsh-erpbot 0.1.0 → 0.1.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/README.md +50 -29
- package/bot-identity.js +246 -0
- package/client.js +2438 -627
- package/cordis.patch.yml +128 -0
- package/docs/README.md +39 -0
- package/docs/acceptance.md +136 -0
- package/docs/architecture.md +161 -0
- package/docs/bot-agents-spec.md +205 -0
- package/docs/bot-collaboration-spec.md +161 -0
- package/docs/data-contract.md +262 -0
- package/docs/mcp-inventory-analysis.md +58 -0
- package/docs/requirements.md +234 -0
- package/index.js +270 -15
- package/package.json +6 -2
- package/scripts/check-bot-identity.mjs +71 -0
- package/scripts/lint-factories.mjs +315 -0
- package/scripts/lint-scopes.mjs +179 -0
- package/scripts/scaffold-bots.mjs +76 -0
- package/scripts/test-manifest.mjs +72 -0
- package/scripts/verify.mjs +159 -0
- package/spec/README.md +299 -0
- package/spec/base-content-spec.md +129 -0
- package/spec/cai-content-spec.md +257 -0
- package/spec/shui-content-spec.md +252 -0
- package/spec/spec-CORE.md +155 -0
- package/spec/tui-content-spec.md +259 -0
- package/spec/zi-content-spec.md +256 -0
- package/src/assistant/index.js +389 -4
- package/src/bot-data.js +25 -13
- package/src/bundle.json +1 -0
- package/src/cards/index.js +18 -4
- package/src/dashboard/index.js +65 -25
- package/src/entry.js +7 -7
- package/src/i18n/index.js +80 -60
- package/src/mount.js +19 -2
- package/src/nav.js +7 -5
- package/src/pages-data.js +29 -262
- package/src/shell/index.js +57 -33
- package/src/styles.js +396 -78
- package/src/workbench/index.js +1297 -85
package/client.js
CHANGED
|
@@ -207,24 +207,27 @@ window.__ModuleLoader__.load({
|
|
|
207
207
|
}
|
|
208
208
|
/* Only the primary icon group gives way in a short box; the settings/
|
|
209
209
|
language group at the bottom is always kept on screen instead of being
|
|
210
|
-
pushed out of the panel.
|
|
211
|
-
|
|
210
|
+
pushed out of the panel.
|
|
211
|
+
|
|
212
|
+
NOTE: this must NOT be ":first-of-type". The rail now opens with the brand
|
|
213
|
+
avatar slot, which is also a <div>, so ":first-of-type" would match the
|
|
214
|
+
brand instead of the button group and the scroll guard would silently
|
|
215
|
+
switch off — visible only as the button group pushing the wordmark out of
|
|
216
|
+
a short panel. */
|
|
217
|
+
.erp-app .t1-rail > .rail-group{
|
|
212
218
|
flex:0 1 auto;min-height:0;overflow-y:auto;overflow-x:hidden;
|
|
213
219
|
}
|
|
214
|
-
.erp-app .t1-rail > .rail-group
|
|
220
|
+
.erp-app .t1-rail > .rail-group::-webkit-scrollbar{width:0;height:0}
|
|
215
221
|
/* The bottom lockup (erpBot wordmark + 中/EN switch) is never squeezed out:
|
|
216
222
|
it keeps its natural height while only the icon group above may scroll. */
|
|
217
223
|
.erp-app .t1-rail .rail-foot{flex:none}
|
|
218
224
|
.erp-app .rail-spacer{flex:1 1 auto;min-height:0}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
rgba(198,203,225,.85) calc(100% - 34px),
|
|
226
|
-
rgba(198,203,225,0) 100%);
|
|
227
|
-
}
|
|
225
|
+
/* The vertical hairline between the rail and the content column is gone: the
|
|
226
|
+
rail already reads as its own column through the inset icon card and the
|
|
227
|
+
whitespace gutter, so the line was a redundant second edge. Kept as an
|
|
228
|
+
explicit display:none rather than deleting the rule, so the dark override
|
|
229
|
+
below stays paired with it. */
|
|
230
|
+
.erp-app .t1-rail::after{display:none}
|
|
228
231
|
.erp-app .rail-brand{
|
|
229
232
|
width:92px;flex:none;display:flex;align-items:center;justify-content:center;gap:5px;
|
|
230
233
|
white-space:nowrap;height:58px;
|
|
@@ -274,27 +277,6 @@ window.__ModuleLoader__.load({
|
|
|
274
277
|
box-shadow:0 10px 18px -12px rgba(21,25,51,.95);
|
|
275
278
|
}
|
|
276
279
|
|
|
277
|
-
/* rail-top brand tile: the B mark, spanning the full width of the nav rail so
|
|
278
|
-
the brand block is exactly as wide as the icon column below it. */
|
|
279
|
-
.erp-app .rail-logo{
|
|
280
|
-
width:100%;flex:none;display:flex;align-items:center;justify-content:center;
|
|
281
|
-
margin-bottom:6px;
|
|
282
|
-
}
|
|
283
|
-
/* The B tile fills the rail's width: the brand block and the icon column below
|
|
284
|
-
share the same edges, so the mark reads as the header of the nav column. */
|
|
285
|
-
.erp-app .rail-logo .rail-mark{
|
|
286
|
-
width:100%;height:52px;flex:none;
|
|
287
|
-
display:grid;place-items:center;
|
|
288
|
-
border-radius:16px;
|
|
289
|
-
background:linear-gradient(140deg,#3f2fbe,#7c5cff 52%,#c48bfb);
|
|
290
|
-
color:#fff;font-size:26px;font-weight:800;line-height:1;letter-spacing:.02em;
|
|
291
|
-
box-shadow:0 10px 22px -12px rgba(94,72,232,.72);
|
|
292
|
-
}
|
|
293
|
-
.erp-app[data-theme='dark'] .rail-logo .rail-mark{
|
|
294
|
-
background:linear-gradient(140deg,#4a37d6,#8a68ff 52%,#cfa2ff);
|
|
295
|
-
box-shadow:0 10px 22px -12px rgba(0,0,0,.8);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
280
|
/* language switch: a two-segment pill (中 | EN) under the erpBot wordmark.
|
|
299
281
|
Both languages stay visible, the active segment is lit, and the inactive one
|
|
300
282
|
only reacts to hover. */
|
|
@@ -401,6 +383,13 @@ window.__ModuleLoader__.load({
|
|
|
401
383
|
|
|
402
384
|
/* ---------------- stat cards ---------------- */
|
|
403
385
|
.erp-app .t1-stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:13px}
|
|
386
|
+
.erp-app .mcp-bar{display:flex;align-items:center;gap:5px;padding:0 4px 9px;font-size:11px;color:var(--erp-dim)}
|
|
387
|
+
.erp-app .mcp-bar-label{opacity:.72}
|
|
388
|
+
.erp-app .mcp-dot{display:inline-flex;align-items:center}
|
|
389
|
+
.erp-app .mcp-dot .bot-dot{width:6px;height:6px;border-radius:50%;flex:none}
|
|
390
|
+
.erp-app .mcp-dot.ok .bot-dot{background:#4dba6a}
|
|
391
|
+
.erp-app .mcp-dot.warn .bot-dot{background:#eb943b}
|
|
392
|
+
.erp-app .mcp-dot.off .bot-dot{background:var(--erp-dim);opacity:.4}
|
|
404
393
|
.erp-app .stat{padding:13px 15px 11px;display:flex;flex-direction:column;gap:8px}
|
|
405
394
|
.erp-app .stat-top{display:flex;align-items:flex-start;justify-content:space-between;gap:6px}
|
|
406
395
|
.erp-app .stat-label{font-size:11.4px;font-weight:600;color:#8c93ac;letter-spacing:.005em}
|
|
@@ -704,10 +693,16 @@ window.__ModuleLoader__.load({
|
|
|
704
693
|
inline-size container above), which is exactly the area the host hands us.
|
|
705
694
|
Width and height both get handled: narrow boxes reflow the grid, short boxes
|
|
706
695
|
shrink the fixed-height chart so the page still fits. */
|
|
707
|
-
@container erp (min-width:
|
|
708
|
-
/* Wide enough that the
|
|
696
|
+
@container erp (min-width:901px){
|
|
697
|
+
/* Wide enough that the board is still one screen: let the two content rows
|
|
709
698
|
share the leftover height instead of sitting at their natural size, so a
|
|
710
|
-
tall panel fills up and a short one compresses rather than being clipped.
|
|
699
|
+
tall panel fills up and a short one compresses rather than being clipped.
|
|
700
|
+
|
|
701
|
+
The floor used to be 1181px. Between 901 and 1180 the panel did NOT stack
|
|
702
|
+
(row 2 and row 3 were still side by side) yet the rows were plain "auto",
|
|
703
|
+
so any spare height just collected as dead space at the bottom of the
|
|
704
|
+
column — 191px of it at 1100x900. The split the rows need is the 900px
|
|
705
|
+
breakpoint below, not 1181, so the range moves with it. */
|
|
711
706
|
.erp-app .t1-main{
|
|
712
707
|
grid-template-rows:auto minmax(var(--erp-row2-min),1.42fr) minmax(var(--erp-row3-min),1fr);
|
|
713
708
|
}
|
|
@@ -909,8 +904,10 @@ window.__ModuleLoader__.load({
|
|
|
909
904
|
.erp-app .rail-foot{gap:5px}
|
|
910
905
|
.erp-app .rail-lang{padding:1px}
|
|
911
906
|
.erp-app .rail-lang .lang-seg{min-width:21px;height:16px;padding:0 4px;font-size:8.2px}
|
|
912
|
-
|
|
913
|
-
|
|
907
|
+
/* short box: the avatar trades its breathing room for the icon column, but
|
|
908
|
+
keeps the ring — that ring is the only thing separating brand from a
|
|
909
|
+
navigation button, so it is the last thing to give up. */
|
|
910
|
+
.erp-app .rail-brand-slot{margin:0 0 6px}
|
|
914
911
|
}
|
|
915
912
|
/* The card art is the one element that cannot simply shrink: below a certain
|
|
916
913
|
height the 1.62:1 ratio squeezes it into a sliver too small to read. So when
|
|
@@ -946,16 +943,17 @@ window.__ModuleLoader__.load({
|
|
|
946
943
|
}
|
|
947
944
|
.erp-app[data-theme='dark'] .erp-board{
|
|
948
945
|
background:#12141f;
|
|
946
|
+
/* the avatar's seam and dot halo read this, so it must follow the surface */
|
|
947
|
+
--erp-board-bg:#12141f;
|
|
949
948
|
}
|
|
950
949
|
.erp-app[data-theme='dark'] .erp-board::before{display:none}
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
}
|
|
950
|
+
/* paired with the light rule above: the divider is off in both themes */
|
|
951
|
+
.erp-app[data-theme='dark'] .t1-rail::after{display:none}
|
|
954
952
|
.erp-app[data-theme='dark'] .rail-group{background:#171a2c;border-color:#242840;box-shadow:0 12px 26px -22px rgba(0,0,0,.8)}
|
|
955
953
|
.erp-app[data-theme='dark'] .rb-name{color:#eef0fa}
|
|
956
954
|
.erp-app[data-theme='dark'] .rail-btn{background:#1d2136;border-color:#2a2f4a;color:#98a0bd}
|
|
957
955
|
.erp-app[data-theme='dark'] .rail-btn:hover{background:#242944;color:#eef0fa;border-color:#3a4166}
|
|
958
|
-
.erp-app[data-theme='dark'] .rail-btn.active{background:#
|
|
956
|
+
.erp-app[data-theme='dark'] .rail-btn.active{background:#5b3cf0;border-color:#5b3cf0;color:#fff;box-shadow:0 10px 18px -12px rgba(91,60,240,.9)}
|
|
959
957
|
.erp-app[data-theme='dark'] .brand-name{color:#f0f2fb}
|
|
960
958
|
.erp-app[data-theme='dark'] .t1-greet b{color:#f0f2fb}
|
|
961
959
|
.erp-app[data-theme='dark'] .t1-greet small{color:#8b93b0}
|
|
@@ -1314,9 +1312,18 @@ window.__ModuleLoader__.load({
|
|
|
1314
1312
|
.erp-app .visa.focus .mini-donut-center{
|
|
1315
1313
|
position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;
|
|
1316
1314
|
justify-content:center;gap:2px;text-align:center;pointer-events:none;
|
|
1315
|
+
/* The centre is inset to the RING's inner diameter, not the ring's box: the
|
|
1316
|
+
stroke is 6/44 of the viewBox, so the usable hole is about 68% wide. Text
|
|
1317
|
+
that fills the whole box visually touches the arcs. */
|
|
1318
|
+
padding:0 16%;
|
|
1317
1319
|
}
|
|
1318
1320
|
.erp-app .visa.focus .mini-donut-center b{
|
|
1319
1321
|
font-size:clamp(13px,4cqi,18px);font-weight:800;letter-spacing:-.03em;font-variant-numeric:tabular-nums;
|
|
1322
|
+
/* The hole is roughly half the ring's outer width; anything wider spills
|
|
1323
|
+
across both arcs. compactMoney already shortens the figure, and this is
|
|
1324
|
+
the belt-and-braces so an unexpectedly long string shrinks to fit instead
|
|
1325
|
+
of overflowing. */
|
|
1326
|
+
max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
|
|
1320
1327
|
}
|
|
1321
1328
|
.erp-app .visa.focus .mini-donut-center span{
|
|
1322
1329
|
font-size:clamp(7px,2cqi,9px);opacity:.68;font-weight:600;
|
|
@@ -1350,16 +1357,33 @@ window.__ModuleLoader__.load({
|
|
|
1350
1357
|
flex:1 1 0;min-width:0;max-width:none;justify-content:center;
|
|
1351
1358
|
}
|
|
1352
1359
|
|
|
1353
|
-
/* ---------------- donut row:
|
|
1360
|
+
/* ---------------- donut row: full-width ring + legend pair ----------------
|
|
1361
|
+
This row used to inherit max-width:46% from the generic .erp-app .donut
|
|
1362
|
+
rule (line ~441), which was written for the expense card where the ring
|
|
1363
|
+
shares its row with a legend that has its own column. Here it is the ONLY
|
|
1364
|
+
thing in the middle lane, so capping it at 46% left the row 121-218px wide
|
|
1365
|
+
while the card was 304-514px: the ring ate 96px and the legend got 0-30px,
|
|
1366
|
+
i.e. every label ellipsised away. The override below restores the full lane. */
|
|
1354
1367
|
.erp-app .visa.focus .visa-chart.donut{
|
|
1355
|
-
width:100%;justify-content:center;
|
|
1356
|
-
gap:clamp(
|
|
1368
|
+
width:100%;max-width:none;justify-content:center;
|
|
1369
|
+
gap:clamp(20px,4cqi,34px);
|
|
1370
|
+
/* The two floating arrows are positioned against the CARD (right:-13px), not
|
|
1371
|
+
against this row, so the row has to reserve their lane itself or the
|
|
1372
|
+
legend runs underneath the next arrow. The button is 26px and 13px of it
|
|
1373
|
+
hangs outside the card, so 30px of inset keeps ring and legend clear. */
|
|
1374
|
+
padding:0 30px;
|
|
1375
|
+
box-sizing:border-box;
|
|
1357
1376
|
}
|
|
1358
1377
|
.erp-app .visa.focus .mini-donut{
|
|
1359
|
-
width:clamp(
|
|
1378
|
+
width:clamp(96px,32cqi,168px);height:auto;max-height:100%;aspect-ratio:1/1;flex:none;
|
|
1360
1379
|
}
|
|
1380
|
+
/* The legend used to be width-limited to 26cqi, which on a narrow card left it
|
|
1381
|
+
with 9-27px — narrower than its own text, so every row ellipsised to nothing.
|
|
1382
|
+
It now takes the room the ring does not need, with a floor that keeps the
|
|
1383
|
+
label readable and lets the row wrap rather than crush. */
|
|
1361
1384
|
.erp-app .visa.focus .mini-legend{
|
|
1362
|
-
flex:
|
|
1385
|
+
flex:1 1 auto;width:auto;min-width:96px;max-width:min(230px,42cqi);
|
|
1386
|
+
justify-content:center;
|
|
1363
1387
|
}
|
|
1364
1388
|
|
|
1365
1389
|
/* ---------------- card anatomy: header / middle / footer ---------------- */
|
|
@@ -1618,6 +1642,12 @@ window.__ModuleLoader__.load({
|
|
|
1618
1642
|
|
|
1619
1643
|
/* ---------------- nested pages: four KPIs + one section workbench ---------------- */
|
|
1620
1644
|
.erp-app .t1-main[data-shell='true']{grid-template-rows:auto minmax(0,1fr)}
|
|
1645
|
+
/* The team page has no KPI row: it is ONE card that must fill the whole column.
|
|
1646
|
+
It sets data-shell="team" (see shell/index.js), but only [data-shell='true']
|
|
1647
|
+
had a rule here, so the team card fell back to content height and left a
|
|
1648
|
+
large empty band under it. A single minmax row gives it the full height, and
|
|
1649
|
+
align-content must stop being "start" or the row keeps its content height. */
|
|
1650
|
+
.erp-app .t1-main[data-shell='team']{grid-template-rows:minmax(0,1fr);align-content:stretch}
|
|
1621
1651
|
.erp-app .workbench{display:flex;flex-direction:row;min-height:0;overflow:hidden;padding:0}
|
|
1622
1652
|
.erp-app .wb-nav{flex:none;width:200px;border-right:1px solid #f2f3fa;padding:12px 10px;display:flex;flex-direction:column;gap:2px;overflow-y:auto}
|
|
1623
1653
|
.erp-app .wb-hd{font-size:9.6px;font-weight:700;letter-spacing:.05em;color:#a6acc2;padding:2px 8px 8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
@@ -2034,6 +2064,9 @@ window.__ModuleLoader__.load({
|
|
|
2034
2064
|
.erp-app .bot-avatar{position:relative;width:40px;height:40px;border-radius:13px;flex:none;display:grid;place-items:center;background:linear-gradient(135deg,#3f2fbe,#7c5cff 62%,#c48bfb);color:#fff;font-weight:800;box-shadow:0 8px 18px -8px rgba(91,70,224,.6);transition:transform .2s,box-shadow .2s}
|
|
2035
2065
|
.erp-app .bot-avatar:hover{transform:translateY(-1px);box-shadow:0 10px 22px -8px rgba(91,70,224,.72)}
|
|
2036
2066
|
.erp-app .bot-avatar-t{font-size:14px;letter-spacing:.5px}
|
|
2067
|
+
/* Team avatar uses the users glyph instead of a Latin initial; the shared
|
|
2068
|
+
gradient stays so the team reads as a fifth member of the same family. */
|
|
2069
|
+
.erp-app .bot-avatar.team-avatar svg{width:18px;height:18px}
|
|
2037
2070
|
.erp-app .bot-avatar-state{position:absolute;right:-1px;bottom:-1px;width:10px;height:10px;border-radius:50%;background:#c3c7d6;border:2px solid #fff}
|
|
2038
2071
|
.erp-app .bot-avatar-state.live{background:#22b07d}
|
|
2039
2072
|
.erp-app .bot-avatar-state.warn{background:#e8a13c}
|
|
@@ -2057,10 +2090,35 @@ window.__ModuleLoader__.load({
|
|
|
2057
2090
|
.erp-app .bot-act.on{background:#efecff;color:#5b46e0}
|
|
2058
2091
|
.erp-app .bot-act svg{width:15px;height:15px}
|
|
2059
2092
|
.erp-app .bot-body{position:relative;flex:1;min-height:0;display:flex;flex-direction:column;padding:0}
|
|
2093
|
+
/* Blank session: the official surface (BotChatSurface) stays mounted so the
|
|
2094
|
+
composer — the only input — remains pinned to the bottom. The welcome block
|
|
2095
|
+
is lifted out of the flex flow with absolute positioning, overlaying the
|
|
2096
|
+
empty message area ABOVE the composer instead of splitting the pane in half
|
|
2097
|
+
(which pushed the composer to the middle). */
|
|
2098
|
+
.erp-app .bot-body.is-blank>.bot-welcome{
|
|
2099
|
+
position:absolute;inset:0;padding:16px var(--erp-gutter) 120px;
|
|
2100
|
+
pointer-events:none;z-index:1;
|
|
2101
|
+
}
|
|
2102
|
+
.erp-app .bot-body.is-blank .bot-chips{pointer-events:auto}
|
|
2060
2103
|
.erp-app .bot-conversation{flex:1;min-height:0;display:flex;flex-direction:column;overflow:hidden}
|
|
2061
2104
|
.erp-app .bot-conversation>*{flex:1;min-height:0}
|
|
2062
|
-
|
|
2063
|
-
|
|
2105
|
+
/* Welcome state, centred in the empty conversation area.
|
|
2106
|
+
It used to sit at the very top: .bot-welcome is a sibling of the chat
|
|
2107
|
+
surface inside .bot-body (a column), and both were flex:1 with the
|
|
2108
|
+
welcome block ordered first, so when the session is blank the greeting
|
|
2109
|
+
hugged the header instead of the middle of the pane. Growing it and
|
|
2110
|
+
centring on the cross+main axis puts the orb, name and greeting where an
|
|
2111
|
+
empty conversation expects them — and it collapses back to flex:none as
|
|
2112
|
+
soon as the surface renders real messages. */
|
|
2113
|
+
.erp-app .bot-welcome{
|
|
2114
|
+
/* When the session is blank the chat surface contributes nothing, so the
|
|
2115
|
+
welcome block must take the whole body and centre inside it. It is the
|
|
2116
|
+
only child that grows; the surface stays at content height. */
|
|
2117
|
+
flex:1 1 auto;min-height:0;display:flex;flex-direction:column;
|
|
2118
|
+
align-items:center;justify-content:center;
|
|
2119
|
+
text-align:center;padding:16px var(--erp-gutter);
|
|
2120
|
+
}
|
|
2121
|
+
.erp-app .bot-orb{width:40px;height:40px;margin:0 0 10px;flex:none}
|
|
2064
2122
|
.erp-app .bot-welcome b{display:block;font-size:15px;color:#1a1d38}
|
|
2065
2123
|
.erp-app .bot-welcome-sub{display:block;margin:6px auto 0;max-width:560px;font-size:11.5px;color:#8b90a6;line-height:1.65}
|
|
2066
2124
|
.erp-app .bot-chips{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;margin-top:12px}
|
|
@@ -2093,11 +2151,47 @@ window.__ModuleLoader__.load({
|
|
|
2093
2151
|
.erp-app .bot-tab.on{color:#4a35d0;font-weight:700;border-bottom-color:#6a54f5}
|
|
2094
2152
|
.erp-app .bot-dr-bd{flex:1;min-height:0;overflow:auto;padding:14px var(--erp-gutter)}
|
|
2095
2153
|
.erp-app .bot-dr-ft{flex:none;display:flex;align-items:center;gap:8px;padding:10px 16px;border-top:1px solid #eef0f8;font-size:10.5px;color:#9aa0b4}
|
|
2154
|
+
/* Write-back conflict strip — sits above the footer so it cannot be scrolled
|
|
2155
|
+
out of view while the user is deciding what to do. */
|
|
2156
|
+
.erp-app .bot-conflict{flex:none;padding:9px 16px;background:#fff8ec;border-top:1px solid #f4e4c8}
|
|
2157
|
+
.erp-app .bot-conflict .tx{font-size:10.5px;line-height:1.6;color:#8a5a12}
|
|
2158
|
+
.erp-app .bot-conflict .acts{display:flex;gap:6px;margin-top:7px}
|
|
2159
|
+
.erp-app .bot-conflict button{border-radius:7px;padding:4px 10px;font-size:10px;font-weight:650;cursor:pointer}
|
|
2160
|
+
.erp-app .bot-conflict .p{background:#b5741a;border:0;color:#fff}
|
|
2161
|
+
.erp-app .bot-conflict .p:hover{filter:brightness(1.08)}
|
|
2162
|
+
.erp-app .bot-conflict .g{background:#fff;border:1px solid #e6d4b4;color:#8a5a12}
|
|
2163
|
+
.erp-app .bot-conflict .g:hover{background:#fdf6ea}
|
|
2164
|
+
.erp-app[data-theme='dark'] .bot-conflict{background:#2b2317;border-top-color:#3d3120}
|
|
2165
|
+
.erp-app[data-theme='dark'] .bot-conflict .tx{color:#e0b878}
|
|
2166
|
+
.erp-app[data-theme='dark'] .bot-conflict .p{background:#c8871f;color:#171a2c}
|
|
2167
|
+
.erp-app[data-theme='dark'] .bot-conflict .g{background:#1d2136;border-color:#3d3120;color:#e0b878}
|
|
2168
|
+
.erp-app[data-theme='dark'] .bot-conflict .g:hover{background:#2b2317}
|
|
2096
2169
|
.erp-app .bot-link{margin-left:auto;color:#5b46e0;font-size:11px}
|
|
2097
2170
|
.erp-app .bot-idcard{display:flex;align-items:center;gap:10px;padding:10px;margin-bottom:12px;border:1px solid #efebff;border-radius:12px;background:linear-gradient(135deg,#f8f5ff,#fdfbff)}
|
|
2098
|
-
.
|
|
2171
|
+
/* Identity disc. The glyph used to be a CJK single character (税/账/司/军) set at
|
|
2172
|
+
13px inside 34px, which at this size smears into an unreadable blob — CJK
|
|
2173
|
+
strokes need roughly 20px+ to stay distinct, and there is no room for that
|
|
2174
|
+
here. It is now the bot's Latin initial: one thick stroke, crisp at any size,
|
|
2175
|
+
and it matches the brand avatar's monogram language. */
|
|
2176
|
+
.erp-app .bot-idcard-ava{
|
|
2177
|
+
width:34px;height:34px;border-radius:50%;flex:none;
|
|
2178
|
+
display:grid;place-items:center;color:#fff;
|
|
2179
|
+
font-weight:800;font-size:15px;line-height:1;letter-spacing:0;
|
|
2180
|
+
background:linear-gradient(135deg,#3f2fbe,#7c5cff 58%,#a06fe8);
|
|
2181
|
+
box-shadow:0 6px 14px -8px rgba(91,70,224,.72);
|
|
2182
|
+
}
|
|
2183
|
+
/* Per-bot accent. The accent field is set on every bot in bot-data (aurora-v1..v4) but
|
|
2184
|
+
had no matching rule, so all four identity cards rendered the same gradient. */
|
|
2185
|
+
.erp-app .bot-idcard-ava.aurora-v1{background:linear-gradient(135deg,#2e2a8f,#6a54f5 58%,#a99bff)}
|
|
2186
|
+
.erp-app .bot-idcard-ava.aurora-v2{background:linear-gradient(135deg,#123f6b,#2f8fd0 58%,#7fd3f2)}
|
|
2187
|
+
.erp-app .bot-idcard-ava.aurora-v3{background:linear-gradient(135deg,#5a2a6e,#a24fc4 58%,#e0a3f2)}
|
|
2188
|
+
.erp-app .bot-idcard-ava.aurora-v4{background:linear-gradient(135deg,#2f5a3e,#3f9a6a 58%,#8fd9b0)}
|
|
2099
2189
|
.erp-app .bot-idcard b{display:block;font-size:12px;color:#1a1d38}
|
|
2100
|
-
|
|
2190
|
+
/* Scoped to the text column: as .bot-idcard span this rule also matched the
|
|
2191
|
+
avatar disc (same element type, higher specificity than .bot-idcard-ava),
|
|
2192
|
+
forcing its glyph to 10.5px and adding margin-top:2px — which is why the
|
|
2193
|
+
initial looked like a smeared blob and sat off-centre. */
|
|
2194
|
+
.erp-app .bot-idcard > div span{display:block;font-size:10.5px;color:#8b90a6;margin-top:2px}
|
|
2101
2195
|
.erp-app .bot-field{margin-bottom:11px}
|
|
2102
2196
|
.erp-app .bot-field>label{display:block;font-size:10.5px;color:#8b90a6;margin-bottom:4px}
|
|
2103
2197
|
.erp-app .bot-ro{background:#fafafd;border:1px solid #f0f0f7;border-radius:9px;padding:8px 10px;color:#4a4f66;font-size:11px;line-height:1.6;white-space:pre-wrap}
|
|
@@ -2105,6 +2199,155 @@ window.__ModuleLoader__.load({
|
|
|
2105
2199
|
.erp-app .bot-input:focus{border-color:#c9bcff;box-shadow:0 0 0 3px rgba(106,84,245,.08)}
|
|
2106
2200
|
.erp-app .bot-row{display:flex;gap:10px}
|
|
2107
2201
|
.erp-app .bot-row .bot-field{flex:1;min-width:0}
|
|
2202
|
+
|
|
2203
|
+
/* ================= Bot résumé pane =================
|
|
2204
|
+
The drawer stopped being a flat field list: it now reads AGENTS.md and renders
|
|
2205
|
+
a résumé — qualification blocks drawn from sections 3/5/9, a coverage radar
|
|
2206
|
+
computed from structural counts, and fields that can be edited in place.
|
|
2207
|
+
Everything here is scoped under .bot-sec / .bot-qual / .bot-radar so it cannot
|
|
2208
|
+
leak into other bot-page surfaces. */
|
|
2209
|
+
|
|
2210
|
+
/* sync badge in the identity card */
|
|
2211
|
+
.erp-app .bot-sync{
|
|
2212
|
+
flex:none;display:inline-flex;align-items:center;gap:4px;height:19px;padding:0 7px;
|
|
2213
|
+
border-radius:6px;font-size:9px;font-weight:700;white-space:nowrap;
|
|
2214
|
+
}
|
|
2215
|
+
.erp-app .bot-sync.ok{background:rgba(34,176,125,.14);color:#1a8f66}
|
|
2216
|
+
.erp-app .bot-sync.dirty{background:rgba(240,160,42,.16);color:#a2680f}
|
|
2217
|
+
.erp-app .bot-sync i{width:5px;height:5px;border-radius:50%;background:currentColor;flex:none}
|
|
2218
|
+
.erp-app[data-theme='dark'] .bot-sync.ok{background:rgba(61,220,151,.14);color:#3ddc97}
|
|
2219
|
+
.erp-app[data-theme='dark'] .bot-sync.dirty{background:rgba(240,160,42,.16);color:#f0a02a}
|
|
2220
|
+
|
|
2221
|
+
/* section header with a rule that fills the remaining width */
|
|
2222
|
+
.erp-app .bot-sec-hd{
|
|
2223
|
+
display:flex;align-items:center;gap:7px;margin-bottom:8px;
|
|
2224
|
+
font-size:9.5px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:#a6acc2;
|
|
2225
|
+
}
|
|
2226
|
+
.erp-app .bot-sec-hd .rule{flex:1;height:1px;background:#eef0f8}
|
|
2227
|
+
.erp-app .bot-sec-hd .cnt{
|
|
2228
|
+
order:2;letter-spacing:0;text-transform:none;font-size:9px;font-weight:600;color:#b0b6c9;
|
|
2229
|
+
}
|
|
2230
|
+
.erp-app[data-theme='dark'] .bot-sec-hd{color:#767e9c}
|
|
2231
|
+
.erp-app[data-theme='dark'] .bot-sec-hd .rule{background:#242840}
|
|
2232
|
+
.erp-app[data-theme='dark'] .bot-sec-hd .cnt{color:#6a7290}
|
|
2233
|
+
|
|
2234
|
+
/* qualification blocks (AGENTS.md sections 3 / 5 / 9) */
|
|
2235
|
+
.erp-app .bot-qual{border:1px solid #eef0f8;border-radius:11px;background:#fafafd;overflow:hidden}
|
|
2236
|
+
.erp-app .bot-qual+.bot-qual{margin-top:7px}
|
|
2237
|
+
.erp-app .bot-qual>summary{
|
|
2238
|
+
display:flex;align-items:center;gap:8px;padding:9px 11px;cursor:pointer;list-style:none;
|
|
2239
|
+
font-size:11px;font-weight:650;color:#3c4157;
|
|
2240
|
+
}
|
|
2241
|
+
.erp-app .bot-qual>summary::-webkit-details-marker{display:none}
|
|
2242
|
+
.erp-app .bot-qual>summary:hover{color:#1a1d38}
|
|
2243
|
+
.erp-app .bot-qual .ico{
|
|
2244
|
+
width:16px;height:16px;border-radius:5px;display:grid;place-items:center;flex:none;
|
|
2245
|
+
background:#efecff;color:#5b46e0;font-size:9px;font-weight:800;
|
|
2246
|
+
}
|
|
2247
|
+
.erp-app .bot-qual .chev{margin-left:auto;color:#a6acc2;font-size:9px;transition:transform .18s}
|
|
2248
|
+
.erp-app .bot-qual[open] .chev{transform:rotate(90deg)}
|
|
2249
|
+
.erp-app .bot-qual .bd{padding:0 11px 11px;font-size:10.5px;line-height:1.75;color:#5f647d;white-space:pre-wrap}
|
|
2250
|
+
.erp-app[data-theme='dark'] .bot-qual{background:#1a1d2e;border-color:#242840}
|
|
2251
|
+
.erp-app[data-theme='dark'] .bot-qual>summary{color:#c9cee6}
|
|
2252
|
+
.erp-app[data-theme='dark'] .bot-qual>summary:hover{color:#eef0fa}
|
|
2253
|
+
.erp-app[data-theme='dark'] .bot-qual .ico{background:#2a2350;color:#c4b6ff}
|
|
2254
|
+
.erp-app[data-theme='dark'] .bot-qual .bd{color:#9aa1bd}
|
|
2255
|
+
|
|
2256
|
+
/* coverage radar */
|
|
2257
|
+
.erp-app .bot-radar{border:1px solid #eef0f8;border-radius:11px;background:#fafafd;padding:11px 11px 8px}
|
|
2258
|
+
.erp-app .bot-radar-top{display:flex;align-items:baseline;gap:8px;margin-bottom:3px}
|
|
2259
|
+
.erp-app .bot-radar-top b{font-size:11px;font-weight:700;color:#1a1d38}
|
|
2260
|
+
.erp-app .bot-radar-top .note{margin-left:auto;font-size:9px;color:#b0b6c9}
|
|
2261
|
+
.erp-app .bot-radar-wrap{display:flex;align-items:center;gap:10px}
|
|
2262
|
+
.erp-app .bot-radar-wrap svg{display:block;flex:none}
|
|
2263
|
+
.erp-app .bot-legend{flex:1;min-width:0;display:flex;flex-direction:column;gap:5px}
|
|
2264
|
+
.erp-app .bot-legend .rl{
|
|
2265
|
+
display:flex;align-items:center;gap:7px;font-size:10.5px;cursor:pointer;
|
|
2266
|
+
border-radius:6px;padding:2px 3px;margin:-2px -3px;
|
|
2267
|
+
}
|
|
2268
|
+
.erp-app .bot-legend .rl:hover,.erp-app .bot-legend .rl.on{background:#f1f2f9}
|
|
2269
|
+
.erp-app .bot-legend .rl .dot{width:5px;height:5px;border-radius:50%;background:#6a54f5;flex:none}
|
|
2270
|
+
.erp-app .bot-legend .rl .n{flex:1;min-width:0;color:#5f647d;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
2271
|
+
.erp-app .bot-legend .rl .v{font-weight:750;color:#1a1d38;font-variant-numeric:tabular-nums}
|
|
2272
|
+
.erp-app .bot-legend .rl .bar{width:30px;height:3px;border-radius:2px;background:#eceef7;overflow:hidden;flex:none}
|
|
2273
|
+
.erp-app .bot-legend .rl .bar i{display:block;height:100%;background:linear-gradient(90deg,#6a54f5,#a98bfb)}
|
|
2274
|
+
.erp-app .bot-evidence{
|
|
2275
|
+
margin-top:6px;padding:8px 9px;border-radius:8px;background:#f4f5fb;border:1px solid #eef0f8;
|
|
2276
|
+
font-size:9.8px;line-height:1.7;color:#767e9c;
|
|
2277
|
+
}
|
|
2278
|
+
.erp-app .bot-evidence b{color:#4a4f66;font-weight:650}
|
|
2279
|
+
.erp-app .radar-fill{fill:rgba(106,84,245,.13);stroke:#6a54f5;stroke-width:1.5;stroke-linejoin:round}
|
|
2280
|
+
.erp-app .radar-grid{fill:none;stroke:#e4e7f3;stroke-width:1}
|
|
2281
|
+
.erp-app .radar-spoke{stroke:#e4e7f3;stroke-width:1}
|
|
2282
|
+
.erp-app .radar-node{fill:#6a54f5;stroke:#fafafd;stroke-width:1.4}
|
|
2283
|
+
.erp-app .radar-cap{font-size:8px;fill:#a6acc2;font-weight:600}
|
|
2284
|
+
.erp-app[data-theme='dark'] .bot-radar{background:#1a1d2e;border-color:#242840}
|
|
2285
|
+
.erp-app[data-theme='dark'] .bot-radar-top b{color:#eef0fa}
|
|
2286
|
+
.erp-app[data-theme='dark'] .bot-radar-top .note{color:#6a7290}
|
|
2287
|
+
.erp-app[data-theme='dark'] .bot-legend .rl:hover,.erp-app[data-theme='dark'] .bot-legend .rl.on{background:#20243a}
|
|
2288
|
+
.erp-app[data-theme='dark'] .bot-legend .rl .n{color:#9aa1bd}
|
|
2289
|
+
.erp-app[data-theme='dark'] .bot-legend .rl .v{color:#eef0fa}
|
|
2290
|
+
.erp-app[data-theme='dark'] .bot-legend .rl .bar{background:#2a2f4a}
|
|
2291
|
+
.erp-app[data-theme='dark'] .bot-evidence{background:#20243a;border-color:#2a2f4a;color:#8b93b0}
|
|
2292
|
+
.erp-app[data-theme='dark'] .bot-evidence b{color:#c9cee6}
|
|
2293
|
+
.erp-app[data-theme='dark'] .radar-grid,.erp-app[data-theme='dark'] .radar-spoke{stroke:#2a2f4a}
|
|
2294
|
+
.erp-app[data-theme='dark'] .radar-node{stroke:#1a1d2e}
|
|
2295
|
+
.erp-app[data-theme='dark'] .radar-cap{fill:#767e9c}
|
|
2296
|
+
|
|
2297
|
+
/* editable identity rows */
|
|
2298
|
+
.erp-app .bot-editable{border:1px solid #eef0f8;border-radius:11px;background:#fafafd;overflow:hidden}
|
|
2299
|
+
.erp-app .bot-erow{display:flex;align-items:flex-start;gap:8px;padding:9px 11px;border-bottom:1px solid #f2f3fa}
|
|
2300
|
+
.erp-app .bot-erow:last-child{border-bottom:0}
|
|
2301
|
+
.erp-app .bot-erow .lab{width:62px;flex:none;font-size:9.5px;color:#a6acc2;font-weight:650;padding-top:1px}
|
|
2302
|
+
.erp-app .bot-erow .val{flex:1;min-width:0;font-size:10.5px;line-height:1.7;color:#4a4f66;white-space:pre-wrap}
|
|
2303
|
+
.erp-app .bot-erow .ed{
|
|
2304
|
+
flex:none;border:0;background:none;color:#6a54f5;cursor:pointer;
|
|
2305
|
+
font-size:9.5px;font-weight:650;padding:0;
|
|
2306
|
+
}
|
|
2307
|
+
.erp-app .bot-erow .ed:hover{text-decoration:underline}
|
|
2308
|
+
.erp-app .bot-erow.edit-on{background:#f4f5fb;display:block}
|
|
2309
|
+
.erp-app .bot-erow textarea{
|
|
2310
|
+
width:100%;min-height:60px;padding:7px 8px;border-radius:8px;border:1px solid #e0e3ef;
|
|
2311
|
+
background:#fff;color:#3c4157;font:inherit;font-size:10.5px;line-height:1.7;resize:vertical;
|
|
2312
|
+
}
|
|
2313
|
+
.erp-app .bot-erow textarea:focus{outline:none;border-color:#c9bcff;box-shadow:0 0 0 3px rgba(106,84,245,.08)}
|
|
2314
|
+
.erp-app .bot-eacts{display:flex;gap:6px;margin-top:6px}
|
|
2315
|
+
.erp-app .bot-eacts .p{
|
|
2316
|
+
border:0;border-radius:7px;padding:5px 11px;font-size:10px;font-weight:650;cursor:pointer;
|
|
2317
|
+
background:#6a54f5;color:#fff;
|
|
2318
|
+
}
|
|
2319
|
+
.erp-app .bot-eacts .p:hover{filter:brightness(1.08)}
|
|
2320
|
+
.erp-app .bot-eacts .g{
|
|
2321
|
+
border:1px solid #e0e3ef;border-radius:7px;padding:5px 11px;font-size:10px;font-weight:650;
|
|
2322
|
+
cursor:pointer;background:#fff;color:#5f647d;
|
|
2323
|
+
}
|
|
2324
|
+
.erp-app .bot-eacts .g:hover{color:#1a1d38}
|
|
2325
|
+
.erp-app[data-theme='dark'] .bot-editable{background:#1a1d2e;border-color:#242840}
|
|
2326
|
+
.erp-app[data-theme='dark'] .bot-erow{border-bottom-color:#242840}
|
|
2327
|
+
.erp-app[data-theme='dark'] .bot-erow .lab{color:#6a7290}
|
|
2328
|
+
.erp-app[data-theme='dark'] .bot-erow .val{color:#b6bdcf}
|
|
2329
|
+
.erp-app[data-theme='dark'] .bot-erow.edit-on{background:#20243a}
|
|
2330
|
+
.erp-app[data-theme='dark'] .bot-erow textarea{background:#141726;border-color:#2f3550;color:#e7e9f6}
|
|
2331
|
+
.erp-app[data-theme='dark'] .bot-eacts .g{background:#20243a;border-color:#2f3550;color:#9aa1bd}
|
|
2332
|
+
.erp-app[data-theme='dark'] .bot-eacts .g:hover{color:#eef0fa}
|
|
2333
|
+
|
|
2334
|
+
/* red-line list (append-only) */
|
|
2335
|
+
.erp-app .bot-reds{border:1px solid #eef0f8;border-radius:11px;background:#fafafd;overflow:hidden}
|
|
2336
|
+
.erp-app .bot-reds ul{margin:0;padding:10px 11px 4px 26px;font-size:10.5px;line-height:1.9;color:#4a4f66}
|
|
2337
|
+
.erp-app .bot-reds li::marker{color:#e25563}
|
|
2338
|
+
.erp-app .bot-reds .ft{padding:0 11px 11px}
|
|
2339
|
+
.erp-app .bot-tags{display:flex;flex-wrap:wrap;gap:5px;margin-top:7px}
|
|
2340
|
+
.erp-app .bot-tags .tag{
|
|
2341
|
+
border:1px dashed #dfe2ee;background:none;color:#8b90a6;border-radius:6px;
|
|
2342
|
+
padding:3px 7px;font-size:9px;cursor:pointer;
|
|
2343
|
+
}
|
|
2344
|
+
.erp-app .bot-tags .tag:hover{border-color:#6a54f5;color:#5b46e0;background:#f6f3ff}
|
|
2345
|
+
.erp-app .bot-tags .tag:disabled{opacity:.45;cursor:not-allowed;border-style:solid;color:#22b07d;border-color:#c9ead9}
|
|
2346
|
+
.erp-app[data-theme='dark'] .bot-reds{background:#1a1d2e;border-color:#242840}
|
|
2347
|
+
.erp-app[data-theme='dark'] .bot-reds ul{color:#b6bdcf}
|
|
2348
|
+
.erp-app[data-theme='dark'] .bot-tags .tag{border-color:#2f3550;color:#8b93b0}
|
|
2349
|
+
.erp-app[data-theme='dark'] .bot-tags .tag:hover{background:#2a2350;border-color:#6a54f5;color:#c4b6ff}
|
|
2350
|
+
.erp-app[data-theme='dark'] .bot-tags .tag:disabled{color:#3ddc97;border-color:#1d4a38}
|
|
2108
2351
|
.erp-app .bot-hint{font-size:10.5px;color:#9aa0b4;line-height:1.6;margin-top:6px}
|
|
2109
2352
|
.erp-app .bot-skills-hd{display:flex;align-items:center;font-size:11px;color:#8b90a6;margin-bottom:6px}
|
|
2110
2353
|
.erp-app .bot-skills-src{margin-left:auto;color:#9aa0b4;font-size:10px}
|
|
@@ -2222,15 +2465,18 @@ window.__ModuleLoader__.load({
|
|
|
2222
2465
|
@container erp (max-width:560px){
|
|
2223
2466
|
.erp-app .bot-role,.erp-app .bot-welcome-sub{display:none}
|
|
2224
2467
|
.erp-app .bot-avatar{width:34px;height:34px;border-radius:11px}
|
|
2225
|
-
.erp-app .bot-welcome{padding:12px var(--erp-gutter)
|
|
2468
|
+
.erp-app .bot-welcome{padding:12px var(--erp-gutter)}
|
|
2226
2469
|
}
|
|
2227
2470
|
@container board (max-height:560px){
|
|
2228
|
-
.erp-app .bot-welcome{padding:10px var(--erp-gutter)
|
|
2471
|
+
.erp-app .bot-welcome{padding:10px var(--erp-gutter)}
|
|
2229
2472
|
.erp-app .bot-orb{width:30px;height:30px;margin-bottom:4px}
|
|
2230
2473
|
.erp-app .bot-welcome-sub{display:none}
|
|
2231
2474
|
}
|
|
2232
2475
|
|
|
2233
2476
|
.erp-app[data-theme='dark'] .bot-hd,.erp-app[data-theme='dark'] .bot-dr-hd,.erp-app[data-theme='dark'] .bot-tabs,.erp-app[data-theme='dark'] .bot-dr-ft{border-color:#242840}
|
|
2477
|
+
.erp-app[data-theme='dark'] .bot-tab{color:#8b93b0}
|
|
2478
|
+
.erp-app[data-theme='dark'] .bot-tab:hover{color:#c9cee6}
|
|
2479
|
+
.erp-app[data-theme='dark'] .bot-tab.on{color:#c4b6ff;border-bottom-color:#8c72ff}
|
|
2234
2480
|
.erp-app[data-theme='dark'] .bot-name b,.erp-app[data-theme='dark'] .bot-welcome b,.erp-app[data-theme='dark'] .bot-idcard b,.erp-app[data-theme='dark'] .bot-dr-hd b,.erp-app[data-theme='dark'] .bot-cv-hd b,.erp-app[data-theme='dark'] .bot-cv-chip b,.erp-app[data-theme='dark'] .bot-skill-top b,.erp-app[data-theme='dark'] .bot-rel b{color:#eef0fa}
|
|
2235
2481
|
.erp-app[data-theme='dark'] .bot-role,.erp-app[data-theme='dark'] .bot-session-title,.erp-app[data-theme='dark'] .bot-welcome-sub,.erp-app[data-theme='dark'] .bot-hint,.erp-app[data-theme='dark'] .bot-empty{color:#9aa0b4}
|
|
2236
2482
|
.erp-app[data-theme='dark'] .bot-avatar-state{border-color:#171a2c}
|
|
@@ -2278,6 +2524,15 @@ window.__ModuleLoader__.load({
|
|
|
2278
2524
|
.erp-app[data-theme='dark'] .rail-hub.on .rail-hub-ico,.erp-app[data-theme='dark'] .rail-base.on .rail-base-ico{background:#2a2350;border-color:#3b3470;color:#c4b6ff}
|
|
2279
2525
|
.erp-app[data-theme='dark'] .t1-rail.open .rail-bot .rail-label,.erp-app[data-theme='dark'] .t1-rail.open .rail-base .rail-label{color:#e6e8f5}
|
|
2280
2526
|
.erp-app[data-theme='dark'] .rail-divider{background:#2a2e48}
|
|
2527
|
+
/* The separator has to lift, not darken, on the dark card: a dimmer line would
|
|
2528
|
+
disappear into #171a2c, so it uses white at low alpha like the other edges. */
|
|
2529
|
+
.erp-app[data-theme='dark'] .rail-sep{
|
|
2530
|
+
background:linear-gradient(90deg,
|
|
2531
|
+
rgba(255,255,255,0) 0%,
|
|
2532
|
+
rgba(255,255,255,.22) 16%,
|
|
2533
|
+
rgba(255,255,255,.22) 84%,
|
|
2534
|
+
rgba(255,255,255,0) 100%);
|
|
2535
|
+
}
|
|
2281
2536
|
.erp-app[data-theme='dark'] .t1-rail.open .rail-bot.on{background:#1d2136}
|
|
2282
2537
|
.erp-app[data-theme='dark'] .rail-bot-dot{border-color:#171a2c}
|
|
2283
2538
|
.erp-app[data-theme='dark'] .rail-me-txt b{color:#eef0fa}
|
|
@@ -2294,6 +2549,10 @@ window.__ModuleLoader__.load({
|
|
|
2294
2549
|
--erp-rail-btn:38px; /* icon button = brand tile footprint */
|
|
2295
2550
|
--erp-rail-pad:7px; /* white group's inner padding */
|
|
2296
2551
|
--erp-rail-gutter:8px; /* whitespace outside the group, both sides */
|
|
2552
|
+
/* The board's own surface colour, named so the brand avatar's seam and its
|
|
2553
|
+
status-dot halo can punch back to the page instead of hard-coding white
|
|
2554
|
+
(which shows as a bright ring in dark mode). Overridden in the dark block. */
|
|
2555
|
+
--erp-board-bg:#f6f7fb;
|
|
2297
2556
|
--erp-rail-w:calc(var(--erp-rail-btn) + var(--erp-rail-pad) * 2 + 2px + var(--erp-rail-gutter) * 2);
|
|
2298
2557
|
grid-template-columns:var(--erp-rail-w) minmax(0,1fr);
|
|
2299
2558
|
}
|
|
@@ -2302,29 +2561,72 @@ window.__ModuleLoader__.load({
|
|
|
2302
2561
|
padding:12px var(--erp-rail-gutter) 18px;overflow:hidden;
|
|
2303
2562
|
background:transparent;box-shadow:none;border-right:0;border-radius:0;transition:none;
|
|
2304
2563
|
}
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2564
|
+
/* paired with the rule at the top of the file: the divider is off */
|
|
2565
|
+
.erp-app .t1-rail::after{display:none}
|
|
2566
|
+
/* ---------------- brand avatar (rail head) ----------------
|
|
2567
|
+
Three nested discs: ring → board-coloured seam → gradient face. The brand
|
|
2568
|
+
and the active page both used to be full-bleed saturated blocks, so they
|
|
2569
|
+
fought for the same focal slot; now the brand owns a structural signature
|
|
2570
|
+
(a ring) that no navigation button has, and colour only has to carry theme.
|
|
2571
|
+
|
|
2572
|
+
Sized off --erp-rail-btn so the footprint (46px) always fits the rail's
|
|
2573
|
+
54px content box and rescales with the existing breakpoints. */
|
|
2574
|
+
.erp-app .rail-brand-slot{
|
|
2575
|
+
flex:none;width:100%;display:flex;justify-content:center;
|
|
2576
|
+
margin:2px 0 10px;
|
|
2577
|
+
}
|
|
2578
|
+
.erp-app .brand-av{
|
|
2579
|
+
--brand-ring:2px;
|
|
2580
|
+
--brand-gap:2px;
|
|
2581
|
+
position:relative;display:grid;place-items:center;
|
|
2582
|
+
width:calc(var(--erp-rail-btn) + (var(--brand-ring) + var(--brand-gap)) * 2);
|
|
2583
|
+
height:calc(var(--erp-rail-btn) + (var(--brand-ring) + var(--brand-gap)) * 2);
|
|
2584
|
+
border-radius:50%;
|
|
2320
2585
|
background:linear-gradient(140deg,#3f2fbe,#7c5cff 52%,#c48bfb);
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
}
|
|
2324
|
-
.erp-app
|
|
2325
|
-
|
|
2326
|
-
|
|
2586
|
+
box-shadow:0 8px 18px -12px rgba(94,72,232,.8);
|
|
2587
|
+
cursor:pointer;transition:transform .2s,box-shadow .2s,filter .2s;
|
|
2588
|
+
}
|
|
2589
|
+
.erp-app .brand-av:hover{transform:scale(1.04);filter:brightness(1.06)}
|
|
2590
|
+
.erp-app .brand-av:focus-visible{
|
|
2591
|
+
outline:none;
|
|
2592
|
+
box-shadow:0 0 0 2px var(--erp-board-bg),0 0 0 4px #7c5cff;
|
|
2593
|
+
}
|
|
2594
|
+
.erp-app .brand-av-gap{
|
|
2595
|
+
width:calc(100% - var(--brand-ring) * 2);
|
|
2596
|
+
height:calc(100% - var(--brand-ring) * 2);
|
|
2597
|
+
border-radius:50%;background:var(--erp-board-bg);
|
|
2598
|
+
display:grid;place-items:center;
|
|
2327
2599
|
}
|
|
2600
|
+
/* The face's light end stops at #a06fe8 rather than #c48bfb: white on
|
|
2601
|
+
#c48bfb is only about 2.5:1, below even the 3:1 large-text floor. */
|
|
2602
|
+
.erp-app .brand-av-face{
|
|
2603
|
+
width:calc(100% - var(--brand-gap) * 2);
|
|
2604
|
+
height:calc(100% - var(--brand-gap) * 2);
|
|
2605
|
+
border-radius:50%;display:grid;place-items:center;color:#fff;
|
|
2606
|
+
background:linear-gradient(140deg,#241a7a,#5b3cf0 50%,#a06fe8);
|
|
2607
|
+
font-size:calc(var(--erp-rail-btn) * .58);font-weight:800;line-height:1;
|
|
2608
|
+
letter-spacing:.02em;
|
|
2609
|
+
box-shadow:inset 0 1px 0 rgba(255,255,255,.26),inset 0 -8px 14px rgba(16,18,40,.18);
|
|
2610
|
+
}
|
|
2611
|
+
/* The dot is anchored on the ring at 45 degrees, not on the box corner.
|
|
2612
|
+
Geometry: outer radius R = btn/2 + (ring + gap) = btn/2 + 4. The ring's
|
|
2613
|
+
midline is one half-ring inside that, R - ring/2 = btn/2 + 3, and the dot's
|
|
2614
|
+
CENTRE must land there along the diagonal, i.e. (btn/2 + 3)/sqrt(2) per axis.
|
|
2615
|
+
Converting centre to box edge collapses to 0.0415*btn + 1.88px for
|
|
2616
|
+
ring = gap = 2. Written proportionally instead of a fixed 4px so the dot
|
|
2617
|
+
stays on the ring at 38 / 34 / 30 — a hard-coded inset drifts inward and
|
|
2618
|
+
ends up half on the face at btn 30. */
|
|
2619
|
+
.erp-app .brand-dot{
|
|
2620
|
+
position:absolute;
|
|
2621
|
+
width:calc(var(--erp-rail-btn) * .21);
|
|
2622
|
+
height:calc(var(--erp-rail-btn) * .21);
|
|
2623
|
+
right:calc(var(--erp-rail-btn) * .0415 + 1.88px);
|
|
2624
|
+
bottom:calc(var(--erp-rail-btn) * .0415 + 1.88px);
|
|
2625
|
+
border-radius:50%;
|
|
2626
|
+
border:2px solid var(--erp-board-bg);
|
|
2627
|
+
background:#22c55e;
|
|
2628
|
+
}
|
|
2629
|
+
.erp-app .brand-dot.busy{background:#f0a02a}
|
|
2328
2630
|
/* Foot lockup and brand tile share the group's width, so the column reads as
|
|
2329
2631
|
one continuous stack: B tile → icon group → wordmark → 中/EN pill. */
|
|
2330
2632
|
.erp-app .rail-brand{width:100%;height:36px;display:flex;align-items:center;justify-content:center;gap:0;padding:0}
|
|
@@ -2341,13 +2643,26 @@ window.__ModuleLoader__.load({
|
|
|
2341
2643
|
dead white gutter. */
|
|
2342
2644
|
.erp-app .rail-group{width:100%;display:flex;flex-direction:column;align-items:center;gap:7px;padding:var(--erp-rail-pad);border-radius:24px;background:#fff;border:1px solid #f1f2f9;box-shadow:0 12px 26px -22px rgba(24,30,66,.7),0 1px 2px rgba(24,30,66,.035)}
|
|
2343
2645
|
.erp-app .rail-group + .rail-group{margin-top:12px}
|
|
2344
|
-
.erp-app .t1-rail > .rail-group
|
|
2345
|
-
.erp-app .t1-rail > .rail-group
|
|
2646
|
+
.erp-app .t1-rail > .rail-group{flex:0 1 auto;min-height:0;overflow-y:auto;overflow-x:hidden}
|
|
2647
|
+
.erp-app .t1-rail > .rail-group::-webkit-scrollbar{width:0;height:0}
|
|
2346
2648
|
.erp-app .rail-foot{width:100%;flex:none;display:flex;flex-direction:column;align-items:center;gap:7px;margin-bottom:2px}
|
|
2347
2649
|
.erp-app .rail-spacer{flex:1 1 auto;min-height:0}
|
|
2348
2650
|
.erp-app .rail-btn{width:var(--erp-rail-btn);height:var(--erp-rail-btn);border-radius:50%;display:grid;place-items:center;background:#f3f4f9;border:1px solid #e9ebf3;color:#828aa3}
|
|
2349
2651
|
.erp-app .rail-btn.active{background:#151933;color:#fff;border-color:#151933;box-shadow:0 10px 20px -10px rgba(21,25,51,.6)}
|
|
2350
2652
|
.erp-app .rail-btn svg{width:18px;height:18px}
|
|
2653
|
+
/* Group separator inside the icon card: a short hairline that marks where the
|
|
2654
|
+
business pages end and the infrastructure page (底座) begins. It is inset
|
|
2655
|
+
rather than full width so it reads as a grouping mark, not as a card edge,
|
|
2656
|
+
and it borrows the group's own padding so it never touches the card sides. */
|
|
2657
|
+
.erp-app .rail-sep{
|
|
2658
|
+
display:block;flex:none;width:calc(100% - 12px);
|
|
2659
|
+
height:1px;margin:6px 0;border-radius:1px;
|
|
2660
|
+
background:linear-gradient(90deg,
|
|
2661
|
+
rgba(122,134,166,0) 0%,
|
|
2662
|
+
rgba(122,134,166,.72) 16%,
|
|
2663
|
+
rgba(122,134,166,.72) 84%,
|
|
2664
|
+
rgba(122,134,166,0) 100%);
|
|
2665
|
+
}
|
|
2351
2666
|
/* The language pill fills the rail as well: its two segments split the width
|
|
2352
2667
|
evenly, so it no longer sits narrower than the blocks stacked above it. */
|
|
2353
2668
|
.erp-app .rail-lang{display:flex;width:100%;justify-content:center;gap:2px;padding:2px}
|
|
@@ -2360,14 +2675,17 @@ window.__ModuleLoader__.load({
|
|
|
2360
2675
|
@container erp (max-width:900px){
|
|
2361
2676
|
.erp-app .erp-board{--erp-rail-btn:34px}
|
|
2362
2677
|
.erp-app .t1-rail{padding-bottom:12px}
|
|
2363
|
-
|
|
2678
|
+
/* Below this size the monogram stops counting as WCAG large text, so the 3:1
|
|
2679
|
+
large-text floor becomes a 4.5:1 body-text floor: flatten the light end of
|
|
2680
|
+
the face gradient to keep white legible instead of just shrinking it. */
|
|
2681
|
+
.erp-app .brand-av-face{background:linear-gradient(140deg,#241a7a,#4d31d8 60%,#7d54e0)}
|
|
2364
2682
|
.erp-app .rail-btn svg{width:16px;height:16px}
|
|
2365
2683
|
.erp-app .rail-lang .lang-seg{font-size:8.4px}
|
|
2366
2684
|
.erp-app .rail-greet{display:none}
|
|
2367
2685
|
}
|
|
2368
2686
|
@container erp (max-width:560px){
|
|
2369
2687
|
.erp-app .erp-board{--erp-rail-btn:30px;--erp-rail-pad:6px}
|
|
2370
|
-
.erp-app .
|
|
2688
|
+
.erp-app .brand-av{--brand-ring:1.5px;--brand-gap:1.5px}
|
|
2371
2689
|
.erp-app .rail-group{border-radius:20px}
|
|
2372
2690
|
}`
|
|
2373
2691
|
/* =====================================================================
|
|
@@ -2462,30 +2780,30 @@ window.__ModuleLoader__.load({
|
|
|
2462
2780
|
titleKey: 'ov.blocks',
|
|
2463
2781
|
items: [
|
|
2464
2782
|
{
|
|
2465
|
-
key: 'nav.
|
|
2466
|
-
page: '
|
|
2783
|
+
key: 'nav.finance', value: '1,284', subKey: 'focus.finance.sub', timeKey: 'focus.time.past', pct: 67,
|
|
2784
|
+
page: 'finance', icon: 'doc', unitKey: 'blocks.unit.vouchers',
|
|
2467
2785
|
period: '2026-11',
|
|
2468
2786
|
chart: { type: 'bars', data: [58, 72, 64, 80, 70, 86] },
|
|
2469
2787
|
cells: [
|
|
2470
|
-
{ k: '
|
|
2471
|
-
{ k: '
|
|
2472
|
-
{ k: '
|
|
2788
|
+
{ k: 'finance.diff', v: '0.00' },
|
|
2789
|
+
{ k: 'finance.pending', v: '37' },
|
|
2790
|
+
{ k: 'finance.reportsDone', v: '2/3' },
|
|
2473
2791
|
],
|
|
2474
2792
|
},
|
|
2475
2793
|
{
|
|
2476
|
-
key: 'nav.
|
|
2477
|
-
page: '
|
|
2794
|
+
key: 'nav.funds', value: '¥8,450,000.00', subKey: 'focus.funds.sub', timeKey: 'focus.time.now', pct: 82,
|
|
2795
|
+
page: 'funds', icon: 'wallet',
|
|
2478
2796
|
period: '2026-11',
|
|
2479
2797
|
chart: { type: 'line', data: [52, 61, 58, 70, 66, 78] },
|
|
2480
2798
|
cells: [
|
|
2481
2799
|
{ k: 'focus.cash', v: '¥7,100,000.00' },
|
|
2482
|
-
{ k: '
|
|
2483
|
-
{ k: '
|
|
2800
|
+
{ k: 'funds.unreconciled', v: '3' },
|
|
2801
|
+
{ k: 'funds.plan', v: '¥2,480,000.00' },
|
|
2484
2802
|
],
|
|
2485
2803
|
},
|
|
2486
2804
|
{
|
|
2487
|
-
key: 'nav.
|
|
2488
|
-
page: '
|
|
2805
|
+
key: 'nav.forecast', value: '64%', subKey: 'focus.forecast.sub', timeKey: 'focus.time.future', pct: 64,
|
|
2806
|
+
page: 'forecast', icon: 'chart',
|
|
2489
2807
|
period: '2026-11',
|
|
2490
2808
|
chart: {
|
|
2491
2809
|
type: 'donut',
|
|
@@ -2500,19 +2818,19 @@ window.__ModuleLoader__.load({
|
|
|
2500
2818
|
},
|
|
2501
2819
|
cells: [
|
|
2502
2820
|
{ k: 'focus.budget', v: '64%' },
|
|
2503
|
-
{ k: '
|
|
2504
|
-
{ k: '
|
|
2821
|
+
{ k: 'forecast.revenue', v: '¥2,480,000.00' },
|
|
2822
|
+
{ k: 'forecast.variance', v: '+0.5%' },
|
|
2505
2823
|
],
|
|
2506
2824
|
},
|
|
2507
2825
|
{
|
|
2508
|
-
key: 'nav.
|
|
2509
|
-
page: '
|
|
2826
|
+
key: 'nav.tax', value: '¥86,400.00', subKey: 'focus.tax.sub', pct: 72,
|
|
2827
|
+
page: 'tax', icon: 'list',
|
|
2510
2828
|
period: '2026-10',
|
|
2511
2829
|
chart: { type: 'bars', data: [62, 70, 66, 78, 72, 84] },
|
|
2512
2830
|
cells: [
|
|
2513
|
-
{ k: '
|
|
2514
|
-
{ k: '
|
|
2515
|
-
{ k: '
|
|
2831
|
+
{ k: 'tax.output', v: '¥150,800.00' },
|
|
2832
|
+
{ k: 'tax.input', v: '¥64,400.00' },
|
|
2833
|
+
{ k: 'tax.filing', v: '2/3' },
|
|
2516
2834
|
],
|
|
2517
2835
|
},
|
|
2518
2836
|
],
|
|
@@ -2642,245 +2960,12 @@ window.__ModuleLoader__.load({
|
|
|
2642
2960
|
}
|
|
2643
2961
|
},
|
|
2644
2962
|
],
|
|
2645
|
-
sections: [
|
|
2646
|
-
{
|
|
2647
|
-
icon: 'swap',
|
|
2648
|
-
label: { zh: 'MCP 通道与服务', en: 'MCP channels and services' },
|
|
2649
|
-
kind: 'table',
|
|
2650
|
-
pill: { zh: '6 条 · 48 工具', en: '6 channels · 48 tools' },
|
|
2651
|
-
summary: [{ label: { zh: '已连接', en: 'Linked' }, value: { zh: '5', en: '5' }, tone: 'ok' }, { label: { zh: '部分授权', en: 'Partial auth' }, value: { zh: '1', en: '1' }, tone: 'warn' }, { label: { zh: '启用 / 注册工具', en: 'Tools enabled / registered' }, value: { zh: '39 / 48', en: '39 / 48' } }],
|
|
2652
|
-
data: {
|
|
2653
|
-
columns: [{ label: { zh: '通道', en: 'Channel' } }, { label: { zh: '类型', en: 'Type' } }, { label: { zh: '协议与鉴权', en: 'Protocol and auth' } }, { label: { zh: '工具(启用 / 注册)', en: 'Tools (enabled / registered)' }, align: 'right' }, { label: { zh: '可用率', en: 'Uptime' }, align: 'right' }, { label: { zh: 'P95', en: 'P95' }, align: 'right' }, { label: { zh: '最近调用', en: 'Last call' }, align: 'right' }, { label: { zh: '状态', en: 'Status' } }],
|
|
2654
|
-
rows: [
|
|
2655
|
-
[{ zh: '用友 U8', en: 'Yonyou U8' }, { zh: 'ERP 适配', en: 'ERP adapter' }, { zh: 'MCP over stdio · 服务账号 + 密钥', en: 'MCP over stdio · service account and key' }, { zh: '14 / 18', en: '14 / 18' }, { zh: '100.0%', en: '100.0%', tone: 'ok' }, '2.1s', '23:12', { zh: '已连接', en: 'Linked', tone: 'ok' }],
|
|
2656
|
-
[{ zh: '金蝶云星空', en: 'Kingdee Cloud' }, { zh: 'ERP 适配', en: 'ERP adapter' }, { zh: 'MCP over HTTP · OAuth2', en: 'MCP over HTTP · OAuth2' }, { zh: '9 / 12', en: '9 / 12' }, { zh: '100.0%', en: '100.0%', tone: 'ok' }, '1.9s', '18:55', { zh: '已连接', en: 'Linked', tone: 'ok' }],
|
|
2657
|
-
[{ zh: '数电发票', en: 'E-invoice' }, { zh: '票税平台', en: 'Tax platform' }, { zh: 'MCP over HTTPS · 服务账号 + 证书', en: 'MCP over HTTPS · service account and certificate' }, { zh: '6 / 6', en: '6 / 6' }, { zh: '100.0%', en: '100.0%', tone: 'ok' }, '1.6s', '23:05', { zh: '已连接', en: 'Linked', tone: 'ok' }],
|
|
2658
|
-
[{ zh: '电子税务局', en: 'E-tax bureau' }, { zh: '票税平台', en: 'Tax platform' }, { zh: 'MCP over HTTPS · 证书 + 身份快照', en: 'MCP over HTTPS · certificate and identity snapshot' }, { zh: '4 / 4', en: '4 / 4' }, { zh: '100.0%', en: '100.0%', tone: 'ok' }, '2.6s', '22:40', { zh: '已连接', en: 'Linked', tone: 'ok' }],
|
|
2659
|
-
[{ zh: '影像 OCR', en: 'Imaging OCR' }, { zh: '文档识别', en: 'Document reading' }, { zh: 'MCP over HTTP · 服务账号', en: 'MCP over HTTP · service account' }, { zh: '3 / 3', en: '3 / 3' }, { zh: '99.9%', en: '99.9%', tone: 'warn' }, '5.0s', '18:40', { zh: '已连接', en: 'Linked', tone: 'ok' }],
|
|
2660
|
-
[{ zh: '银行直连', en: 'Bank link' }, { zh: '银行直连', en: 'Bank link' }, { zh: 'MCP over HTTPS · 证书 + 双人授权', en: 'MCP over HTTPS · certificate and dual approval' }, { zh: '3 / 5', en: '3 / 5' }, { zh: '99.7%', en: '99.7%', tone: 'warn' }, '2.4s', '21:30', { zh: '部分授权', en: 'Partial auth', tone: 'warn' }],
|
|
2661
|
-
],
|
|
2662
|
-
total: [{ zh: '合计', en: 'Total' }, { zh: '4 类', en: '4 types' }, '—', { zh: '39 / 48', en: '39 / 48' }, { zh: '99.6%', en: '99.6%' }, '—', { zh: '最近 23:12', en: 'Last 23:12' }, { zh: '5 已连接 / 1 部分授权', en: '5 linked / 1 partial' }]
|
|
2663
|
-
},
|
|
2664
|
-
alt: {
|
|
2665
|
-
kind: 'table',
|
|
2666
|
-
label: { zh: '可联通资产', en: 'Connected assets' },
|
|
2667
|
-
data: {
|
|
2668
|
-
columns: [{ label: { zh: '资产类型', en: 'Asset type' } }, { label: { zh: '资产', en: 'Asset' } }, { label: { zh: '所属通道', en: 'Channel' } }, { label: { zh: '取数内容', en: 'Data scope' } }, { label: { zh: '状态', en: 'Status' } }],
|
|
2669
|
-
rows: [
|
|
2670
|
-
[{ zh: '账套', en: 'Ledger' }, { zh: 'U8 2026 年度账', en: 'U8 FY2026 ledger' }, { zh: '用友 U8', en: 'Yonyou U8' }, { zh: '凭证 / 账簿 / 报表', en: 'Vouchers, ledgers, reports' }, { zh: '已联通', en: 'Linked', tone: 'ok' }],
|
|
2671
|
-
[{ zh: '账套', en: 'Ledger' }, { zh: '金蝶 2026 账套', en: 'Kingdee FY2026 ledger' }, { zh: '金蝶云星空', en: 'Kingdee Cloud' }, { zh: '采购 / 库存 / 应收', en: 'Purchasing, stock, AR' }, { zh: '已联通', en: 'Linked', tone: 'ok' }],
|
|
2672
|
-
[{ zh: '账户', en: 'Account' }, { zh: '工行 4821', en: 'ICBC 4821' }, { zh: '银行直连', en: 'Bank link' }, { zh: '余额与流水', en: 'Balances and statements' }, { zh: '已联通', en: 'Linked', tone: 'ok' }],
|
|
2673
|
-
[{ zh: '账户', en: 'Account' }, { zh: '工行 7356', en: 'ICBC 7356' }, { zh: '银行直连', en: 'Bank link' }, { zh: '余额与流水', en: 'Balances and statements' }, { zh: '已联通', en: 'Linked', tone: 'ok' }],
|
|
2674
|
-
[{ zh: '账户', en: 'Account' }, { zh: '招行 2093', en: 'CMB 2093' }, { zh: '银行直连', en: 'Bank link' }, { zh: '余额与流水', en: 'Balances and statements' }, { zh: '已联通', en: 'Linked', tone: 'ok' }],
|
|
2675
|
-
[{ zh: '发票', en: 'Invoice' }, { zh: '数电销项', en: 'E-invoice output' }, { zh: '数电发票', en: 'E-invoice' }, { zh: '销项票面', en: 'Output invoice face' }, { zh: '已联通', en: 'Linked', tone: 'ok' }],
|
|
2676
|
-
[{ zh: '发票', en: 'Invoice' }, { zh: '数电进项', en: 'E-invoice input' }, { zh: '数电发票', en: 'E-invoice' }, { zh: '进项与勾选', en: 'Input and selection' }, { zh: '已联通', en: 'Linked', tone: 'ok' }],
|
|
2677
|
-
[{ zh: '费控', en: 'Expense control' }, { zh: '费控系统', en: 'Expense system' }, { zh: '费控(文件)', en: 'Expense files' }, { zh: '报销单与预算', en: 'Claims and budget' }, { zh: '已联通', en: 'Linked', tone: 'ok' }]
|
|
2678
|
-
]
|
|
2679
|
-
}
|
|
2680
|
-
},
|
|
2681
|
-
foot: {
|
|
2682
|
-
zh: '通道 6 = U8 1 + 金蝶 1 + 数电发票 1 + 电子税务局 1 + 影像 OCR 1 + 银行直连 1;已连接 5 + 部分授权 1(建行 2 户待授权);工具 48 = 18+12+6+4+3+5,启用 39 / 策略禁用 7 / 待授权 2(48 = 39 + 7 + 2);越权拦截 3 均留回执;alt 8 项资产 = 账套 2 + 账户 3 + 发票 2 + 费控 1(未联通 3 账户 = 建行 2 户 + 中行 1 户);可用率 / P95 取近 30 天:可用率 = (720 − 2.9) ÷ 720 = 717.1 ÷ 720 = 99.5972% → 99.6%,6 行不可用分钟 110 + 30 + 8 + 8 + 9 + 9 = 174 分钟 = 2.9h(去重前窗口,平台按去重后 2.9h 计);配置 CFG-2026-11-08(生效 11-08);口径 C-2026-11-01',
|
|
2683
|
-
en: '6 channels = U8 1 + Kingdee 1 + e-invoice 1 + e-tax bureau 1 + imaging OCR 1 + bank link 1; 5 linked plus 1 partial (2 CCB accounts pending auth); tools 48 = 18+12+6+4+3+5 with 39 enabled, 7 policy-disabled and 2 pending auth (48 = 39 + 7 + 2); the 3 blocked calls each leave a receipt; 8 assets = 2 ledgers + 3 accounts + 2 invoice sets + 1 expense system (3 accounts not linked = 2 CCB + 1 BOC); uptime and P95 over 30 days: uptime = (720 - 2.9) / 720 = 717.1 / 720 = 99.5972% to 99.6%, and the six down-minute figures 110 + 30 + 8 + 8 + 9 + 9 sum to 174 minutes = 2.9h as the pre-dedup window while the platform counts 2.9h after dedup; config CFG-2026-11-08 effective Nov 8; caliber C-2026-11-01'
|
|
2684
|
-
}
|
|
2685
|
-
},
|
|
2686
|
-
{
|
|
2687
|
-
icon: 'spark',
|
|
2688
|
-
label: { zh: '技能与场景', en: 'Skills and scenarios' },
|
|
2689
|
-
kind: 'table',
|
|
2690
|
-
pill: { zh: 'S3 · 12 技能 / 5 场景', en: 'S3 · 12 skills / 5 scenarios' },
|
|
2691
|
-
summary: [{ label: { zh: '已启用', en: 'Enabled' }, value: { zh: '9', en: '9' }, tone: 'ok' }, { label: { zh: '待升级', en: 'To upgrade' }, value: { zh: '3', en: '3' }, tone: 'warn' }, { label: { zh: '调用 7 天', en: 'Calls 7 days' }, value: { zh: '1,284', en: '1,284' } }],
|
|
2692
|
-
data: {
|
|
2693
|
-
columns: [{ label: { zh: '技能', en: 'Skill' } }, { label: { zh: '版本', en: 'Version' } }, { label: { zh: '启用范围', en: 'Scope' } }, { label: { zh: '依赖通道', en: 'Channels' } }, { label: { zh: '状态', en: 'Status' } }, { label: { zh: '近 7 天调用', en: 'Calls 7 days' }, align: 'right' }],
|
|
2694
|
-
rows: [
|
|
2695
|
-
[{ zh: '凭证识别录入', en: 'Voucher capture' }, 'v3.2', { zh: '财', en: 'Books' }, { zh: 'U8 + 影像 OCR', en: 'U8 and OCR' }, { zh: '启用', en: 'Enabled', tone: 'ok' }, '186'],
|
|
2696
|
-
[{ zh: '智能对账', en: 'Smart reconciliation' }, 'v2.4', { zh: '资', en: 'Cash' }, { zh: '银行直连 + U8', en: 'Bank link and U8' }, { zh: '启用', en: 'Enabled', tone: 'ok' }, '142'],
|
|
2697
|
-
[{ zh: '账龄分析', en: 'AR aging' }, 'v2.0', { zh: '财 + 总览', en: 'Books and overview' }, { zh: 'U8', en: 'U8' }, { zh: '启用', en: 'Enabled', tone: 'ok' }, '98'],
|
|
2698
|
-
[{ zh: '现金流预测', en: 'Cash forecast' }, 'v2.1', { zh: '资 + 推', en: 'Cash and plan' }, { zh: 'U8 + 银行直连', en: 'U8 and bank link' }, { zh: '灰度', en: 'Gray', tone: 'warn' }, '36'],
|
|
2699
|
-
[{ zh: '月末关账助手', en: 'Month-end close' }, 'v3.0', { zh: '财', en: 'Books' }, { zh: 'U8', en: 'U8' }, { zh: '启用', en: 'Enabled', tone: 'ok' }, '12'],
|
|
2700
|
-
[{ zh: '税务申报助手', en: 'Tax filing' }, 'v1.6', { zh: '税', en: 'Tax' }, { zh: '电子税务局 + 数电', en: 'Tax bureau and e-invoice' }, { zh: '灰度', en: 'Gray', tone: 'warn' }, '8'],
|
|
2701
|
-
[{ zh: '发票查验', en: 'Invoice checks' }, 'v1.2', { zh: '税', en: 'Tax' }, { zh: '数电发票', en: 'E-invoice' }, { zh: '启用', en: 'Enabled', tone: 'ok' }, '62'],
|
|
2702
|
-
[{ zh: '旧银企 RPA', en: 'Legacy bank RPA' }, 'v0.9', { zh: '—', en: '—' }, { zh: '银行直连', en: 'Bank link' }, { zh: '停用', en: 'Off', tone: 'off' }, '0']
|
|
2703
|
-
],
|
|
2704
|
-
total: [{ zh: '合计', en: 'Total' }, { zh: 'S3 生效', en: 'S3 active' }, '—', '—', { zh: '9 启用 / 2 灰度 / 1 停用', en: '9 on / 2 gray / 1 off' }, '1,284']
|
|
2705
|
-
},
|
|
2706
|
-
alt: {
|
|
2707
|
-
kind: 'list',
|
|
2708
|
-
label: { zh: '场景与运行', en: 'Scenarios and runs' },
|
|
2709
|
-
data: {
|
|
2710
|
-
rows: [
|
|
2711
|
-
{ name: { zh: '月末关账助手', en: 'Month-end close' }, desc: { zh: '运行中 · 每月 · 最近 11-11 09:00 · 技能 月末关账助手 v3.0', en: 'Running · monthly · last Nov 11 09:00 · skill v3.0' }, badge: { zh: '运行中', en: 'Running' }, tone: 'ok', meta: { zh: '财', en: 'Books' } },
|
|
2712
|
-
{ name: { zh: '资金日报', en: 'Daily cash brief' }, desc: { zh: '运行中 · 每日 4 次(08:00 / 12:00 / 16:00 / 20:00)· 最近 11-11 20:00 · 技能 管报生成 v1.4', en: 'Running · 4 times daily (08:00, 12:00, 16:00, 20:00) · last Nov 11 20:00 · skill v1.4' }, badge: { zh: '运行中', en: 'Running' }, tone: 'ok', meta: { zh: '资 · 总览', en: 'Cash, overview' } },
|
|
2713
|
-
{ name: { zh: '应收催收', en: 'AR collection' }, desc: { zh: '运行中 · 每周一 · 11-09 · 技能 账龄分析 v2.0', en: 'Running · weekly on Monday · Nov 9 · skill v2.0' }, badge: { zh: '运行中', en: 'Running' }, tone: 'ok', meta: { zh: '财', en: 'Books' } },
|
|
2714
|
-
{ name: { zh: '预算偏差预警', en: 'Budget variance alert' }, desc: { zh: '待配置 · 触发未设 · 技能 差异归因 v1.1', en: 'To configure · trigger missing · skill v1.1' }, badge: { zh: '待配置', en: 'To configure' }, tone: 'warn', meta: { zh: '推', en: 'Plan' } },
|
|
2715
|
-
{ name: { zh: '税务申报助手', en: 'Tax filing agent' }, desc: { zh: '停用 · 11-05 起 · 技能 税务申报助手 v1.6(灰度)· 停用只停触发,按需调用仍入回执', en: 'Off · since Nov 5 · skill v1.6 (gray); off stops triggers only, on-demand calls still leave receipts' }, badge: { zh: '停用', en: 'Off' }, tone: 'off', meta: { zh: '税', en: 'Tax' } }
|
|
2716
|
-
]
|
|
2717
|
-
}
|
|
2718
|
-
},
|
|
2719
|
-
foot: {
|
|
2720
|
-
zh: '12 技能另 4 项(预算编制 v2.2、管报生成 v1.4、差异归因 v1.1、主数据匹配 v1.0,均启用)合计调用 740,全量 1,284;策略包 = 步骤 / 工具选择 / 口径选择 / 降级 / 复核触发;待升级 3 为版本维度,与灰度 2 不互算;技能会改变助手执行方案,必须可版本、可回滚、可审计;场景侧 5 = 运行中 3 + 待配置 1 + 停用 1;场景 = 技能 + 触发 + 适用页面 / 角色;近 7 天 30 次 = 资金日报 4 × 7 + 应收催收 11-09 + 月末关账 11-11;全量运行记录属本页,推页只展示产出本页结果的运行(R-1111-06)',
|
|
2721
|
-
en: 'The other 4 skills (budget v2.2, management report v1.4, variance v1.1, master data match v1.0, all enabled) account for 740 calls, 1,284 in total; a policy pack holds steps, tool choice, caliber choice, fallback and review triggers; 3 pending upgrades is a version measure and does not combine with 2 gray; skill changes alter the agent plan and must be versioned, rollbackable and auditable; on the scenario side: 5 = 3 running + 1 to configure + 1 off; a scenario is a skill plus a trigger plus pages and roles; 30 runs in 7 days = 4 x 7 daily briefs + one collection run on Nov 9 + one close run on Nov 11; the full run log lives here, while the planning page shows only runs that produced its results (R-1111-06)'
|
|
2722
|
-
}
|
|
2723
|
-
},
|
|
2724
|
-
{
|
|
2725
|
-
icon: 'doc',
|
|
2726
|
-
label: { zh: '数据源', en: 'Sources' },
|
|
2727
|
-
kind: 'table',
|
|
2728
|
-
pill: { zh: '12 源', en: '12 sources' },
|
|
2729
|
-
summary: [{ label: { zh: '已接入', en: 'Connected' }, value: { zh: '12', en: '12' }, tone: 'ok' }, { label: { zh: '权威 / 参考', en: 'Primary and reference' }, value: { zh: '9 / 3', en: '9 / 3' } }, { label: { zh: '待处理接入', en: 'Pending auth or manual' }, value: { zh: '建行 2 户 · 中行 1 户', en: 'CCB 2, BOC 1' }, tone: 'warn' }],
|
|
2730
|
-
data: {
|
|
2731
|
-
columns: [{ label: { zh: '数据源', en: 'Source' } }, { label: { zh: '类型', en: 'Type' } }, { label: { zh: '接入方式', en: 'Ingest' } }, { label: { zh: '最近批次', en: 'Last batch' } }, { label: { zh: '水位', en: 'Watermark' } }, { label: { zh: '状态', en: 'Status' } }],
|
|
2732
|
-
rows: [
|
|
2733
|
-
[{ zh: '用友 U8', en: 'Yonyou U8' }, { zh: '核算', en: 'Books' }, { zh: '只读接口 · 增量', en: 'Read-only API, incremental' }, 'B-20261111-109', '2026-11-11 23:10', { zh: '已接入', en: 'Connected', tone: 'ok' }],
|
|
2734
|
-
[{ zh: '金蝶云星空', en: 'Kingdee Cloud' }, { zh: '核算', en: 'Books' }, { zh: '只读接口 · 增量', en: 'Read-only API, incremental' }, 'B-20261111-108', '2026-11-11 23:10', { zh: '已接入', en: 'Connected', tone: 'ok' }],
|
|
2735
|
-
[{ zh: '工行', en: 'ICBC' }, { zh: '银行', en: 'Bank' }, { zh: '银企直连', en: 'Direct bank link' }, 'B-20261111-107', '2026-11-11 23:10', { zh: '已接入', en: 'Connected', tone: 'ok' }],
|
|
2736
|
-
[{ zh: '招行', en: 'CMB' }, { zh: '银行', en: 'Bank' }, { zh: '银企直连', en: 'Direct bank link' }, 'B-20261111-107', '2026-11-11 23:10', { zh: '已接入', en: 'Connected', tone: 'ok' }],
|
|
2737
|
-
[{ zh: '建行 2 户', en: 'CCB, 2 accounts' }, { zh: '银行', en: 'Bank' }, { zh: '银企待授权', en: 'Bank link pending auth' }, 'B-20261110-101', '2026-11-10', { zh: '待授权', en: 'Auth pending', tone: 'warn' }],
|
|
2738
|
-
[{ zh: '中行 1 户', en: 'BOC, 1 account' }, { zh: '银行', en: 'Bank' }, { zh: '手工导入', en: 'Manual import' }, 'B-20261110-102', '2026-11-10', { zh: '手工接入', en: 'Manual access', tone: 'warn' }],
|
|
2739
|
-
[{ zh: '票税平台 2 源', en: 'Tax platform, 2 sources' }, { zh: '票税', en: 'Tax' }, { zh: '平台接口', en: 'Platform API' }, 'B-20261111-109', '2026-11-11 23:10', { zh: '已接入', en: 'Connected', tone: 'ok' }],
|
|
2740
|
-
[{ zh: '业务文件 4 源', en: 'Business files, 4 sources' }, { zh: '业务', en: 'Business' }, { zh: '文件与接口', en: 'Files and API' }, 'B-20261111-106', '2026-11-11 23:10', { zh: '已接入', en: 'Connected', tone: 'ok' }]
|
|
2741
|
-
],
|
|
2742
|
-
total: [{ zh: '合计', en: 'Total' }, { zh: '12 源', en: '12 sources' }, { zh: '权威 9 / 参考 3', en: '9 primary, 3 reference' }, { zh: '最近 B-20261111-109', en: 'Last B-20261111-109' }, { zh: '2026-11-11 23:10', en: '2026-11-11 23:10' }, { zh: '建行 2 户待授权 · 中行 1 户手工', en: 'CCB 2 pending auth, BOC 1 manual' }]
|
|
2743
|
-
},
|
|
2744
|
-
foot: { zh: '12 源 = 1+1+1+1+1+1+2+4;权威 9 / 参考 3;建行 2 户待授权、中行 1 户手工导入(已接入 · 手工非自动);业务文件 4 源中影像 OCR 近 24h 1 批超时(见『质量与对账』),接入状态仍为已接入', en: '12 sources = 1+1+1+1+1+1+2+4; 9 primary, 3 reference; CCB 2 accounts pending auth, BOC 1 manual import (connected, manual not automatic); imaging OCR in business files had 1 timeout in 24h (see Quality and recon), access status stays connected' }
|
|
2745
|
-
},
|
|
2746
|
-
{
|
|
2747
|
-
icon: 'pulse',
|
|
2748
|
-
label: { zh: '蒸馏批次', en: 'Pipelines' },
|
|
2749
|
-
kind: 'chart',
|
|
2750
|
-
pill: { zh: '868 批 / 7 天', en: '868 batches / 7 days' },
|
|
2751
|
-
summary: [{ label: { zh: '近 7 天批次', en: 'Batches, 7 days' }, value: { zh: '868', en: '868' }, tone: 'ok' }, { label: { zh: '成功', en: 'Succeeded' }, value: { zh: '861', en: '861' }, tone: 'ok' }, { label: { zh: '失败', en: 'Failed' }, value: { zh: '7', en: '7' }, tone: 'warn' }],
|
|
2752
|
-
data: {
|
|
2753
|
-
mode: 'line',
|
|
2754
|
-
series: [120, 126, 122, 128, 124, 122, 126],
|
|
2755
|
-
labels: [{ zh: '11-05', en: 'Nov 5' }, { zh: '11-06', en: 'Nov 6' }, { zh: '11-07', en: 'Nov 7' }, { zh: '11-08', en: 'Nov 8' }, { zh: '11-09', en: 'Nov 9' }, { zh: '11-10', en: 'Nov 10' }, { zh: '11-11', en: 'Nov 11' }],
|
|
2756
|
-
caption: { zh: '每日批次;滑动 24h 124 批(成功 123 / 失败 1);日历日 11-11 126 批;全量批次 11-09 02:00', en: 'Daily batches; rolling 24h 124 (123 succeeded, 1 failed); calendar day Nov 11 126; full batch at 02:00 on Nov 9' }
|
|
2757
|
-
},
|
|
2758
|
-
alt: {
|
|
2759
|
-
kind: 'table',
|
|
2760
|
-
label: { zh: '逐日明细', en: 'Daily detail' },
|
|
2761
|
-
data: {
|
|
2762
|
-
columns: [{ label: { zh: '日期', en: 'Date' } }, { label: { zh: '批次', en: 'Batches' }, align: 'right' }, { label: { zh: '成功', en: 'Succeeded' }, align: 'right' }, { label: { zh: '失败', en: 'Failed' }, align: 'right' }, { label: { zh: '成功率', en: 'Pass rate' }, align: 'right' }, { label: { zh: '回放', en: 'Replay' } }],
|
|
2763
|
-
rows: [
|
|
2764
|
-
['2026-11-05', '120', '120', '0', '100.0%', { zh: '未回放', en: 'Not replayed' }],
|
|
2765
|
-
['2026-11-06', '126', '125', '1', '99.2%', { zh: '未回放', en: 'Not replayed' }],
|
|
2766
|
-
['2026-11-07', '122', '120', '2', '98.4%', { zh: '已回放', en: 'Replayed' }],
|
|
2767
|
-
['2026-11-08', '128', '127', '1', '99.2%', { zh: '未回放', en: 'Not replayed' }],
|
|
2768
|
-
['2026-11-09', '124', '123', '1', '99.2%', { zh: '未回放', en: 'Not replayed' }],
|
|
2769
|
-
['2026-11-10', '122', '121', '1', '99.2%', { zh: '未回放', en: 'Not replayed' }],
|
|
2770
|
-
['2026-11-11', '126', '125', '1', '99.2%', { zh: '未回放', en: 'Not replayed' }]
|
|
2771
|
-
],
|
|
2772
|
-
total: [{ zh: '合计', en: 'Total' }, '868', '861', '7', '99.2%', { zh: '已回放 1 批', en: '1 replayed' }]
|
|
2773
|
-
}
|
|
2774
|
-
},
|
|
2775
|
-
foot: { zh: '近 7 天 868 批 = 成功 861 + 失败 7(99.2%);日均 124 批;滑动 24h 124 批(成功 123 / 失败 1,影像 OCR 超时),日历日 11-11 126 批;全量批次 11-09 02:00;批次只增不改,回放不覆盖原批次;同步延迟 4 分钟 / SLA 15 分钟(B-20261111-109 完成 23:14 − 水位 23:10,余量 11 分钟,最慢源=银行流水)', en: '7 days: 868 batches = 861 succeeded + 7 failed (99.2%); 124 per day on average; rolling 24h 124 (123 succeeded, 1 imaging OCR timeout), calendar day Nov 11 126; full batch at 02:00 on Nov 9; batches are append-only and replay never overwrites; sync lag 4 minutes against a 15-minute SLA (batch B-20261111-109 done 23:14 minus watermark 23:10, 11 minutes headroom, slowest source is bank statements)' }
|
|
2776
|
-
},
|
|
2777
|
-
{
|
|
2778
|
-
icon: 'users',
|
|
2779
|
-
label: { zh: '主数据映射', en: 'Master data' },
|
|
2780
|
-
kind: 'table',
|
|
2781
|
-
pill: { zh: '7 项待复核', en: '7 to review' },
|
|
2782
|
-
summary: [{ label: { zh: '实体条数', en: 'Entities' }, value: { zh: '500', en: '500' } }, { label: { zh: '待复核', en: 'To review' }, value: { zh: '7', en: '7' }, tone: 'warn' }, { label: { zh: '未映射', en: 'Unmapped' }, value: { zh: '3', en: '3' }, tone: 'warn' }],
|
|
2783
|
-
data: {
|
|
2784
|
-
columns: [{ label: { zh: '实体', en: 'Entity' } }, { label: { zh: '条数', en: 'Records' }, align: 'right' }, { label: { zh: '待复核', en: 'To review' }, align: 'right' }, { label: { zh: '原因', en: 'Reason' } }, { label: { zh: '状态', en: 'Status' } }],
|
|
2785
|
-
rows: [
|
|
2786
|
-
[{ zh: '客商', en: 'Partners' }, '386', '3', { zh: '未映射 2 · 新维度 1', en: '2 unmapped · 1 new' }, { zh: '待复核', en: 'To review', tone: 'warn' }],
|
|
2787
|
-
[{ zh: '科目', en: 'Accounts' }, '86', '1', { zh: '生效日冲突 1', en: '1 date conflict' }, { zh: '待复核', en: 'To review', tone: 'warn' }],
|
|
2788
|
-
[{ zh: '成本中心', en: 'Cost centres' }, '12', '1', { zh: '生效日冲突 1', en: '1 date conflict' }, { zh: '待复核', en: 'To review', tone: 'warn' }],
|
|
2789
|
-
[{ zh: '利润中心', en: 'Profit centres' }, '6', '1', { zh: '新维度 1', en: '1 new dimension' }, { zh: '待复核', en: 'To review', tone: 'warn' }],
|
|
2790
|
-
[{ zh: '业务条线', en: 'Business lines' }, '4', '0', { zh: '—', en: 'None' }, { zh: '正常', en: 'Clear', tone: 'ok' }],
|
|
2791
|
-
[{ zh: '银行账户', en: 'Bank accounts' }, '6', '1', { zh: '未映射 1', en: '1 unmapped' }, { zh: '待复核', en: 'To review', tone: 'warn' }]
|
|
2792
|
-
],
|
|
2793
|
-
total: [{ zh: '合计', en: 'Total' }, '500', '7', { zh: '未映射 3 · 生效日冲突 2 · 新维度 2', en: '3 unmapped · 2 conflicts · 2 new' }, { zh: '1 ok / 5 warn', en: '1 ok / 5 warn' }]
|
|
2794
|
-
},
|
|
2795
|
-
foot: { zh: '待复核 7 = 未映射 3 + 生效日冲突 2 + 新维度 2;来源=ERP 蒸馏镜像,本页只做映射与复核,不新增、不改写;科目 86 与财页全量 86 科目同数,银行账户 6 与资页 6 账户同数', en: '7 to review = 3 unmapped + 2 date conflicts + 2 new dimensions; source is the ERP mirror, this page only maps and reviews, never creates or rewrites; 86 accounts match the books page and 6 bank accounts match the funds page' }
|
|
2796
|
-
},
|
|
2797
|
-
{
|
|
2798
|
-
icon: 'grid',
|
|
2799
|
-
label: { zh: '指标与口径', en: 'Metrics and caliber' },
|
|
2800
|
-
kind: 'bars',
|
|
2801
|
-
pill: { zh: '86 指标', en: '86 metrics' },
|
|
2802
|
-
summary: [{ label: { zh: '可用指标', en: 'Usable' }, value: { zh: '86', en: '86' }, tone: 'ok' }, { label: { zh: '口径版本', en: 'Caliber' }, value: { zh: 'C-2026-11-01', en: 'C-2026-11-01' } }, { label: { zh: '六域', en: 'Domains' }, value: { zh: '6', en: '6' }, tone: 'ok' }],
|
|
2803
|
-
data: { items: [{ label: { zh: '财', en: 'Finance' }, valueText: '22 项', pct: 25.6 }, { label: { zh: '资', en: 'Funds' }, valueText: '16 项', pct: 18.6 }, { label: { zh: '推', en: 'Forecast' }, valueText: '18 项', pct: 20.9 }, { label: { zh: '税', en: 'Tax' }, valueText: '14 项', pct: 16.3 }, { label: { zh: '数', en: 'Data' }, valueText: '10 项', pct: 11.6 }, { label: { zh: '总览', en: 'Overview' }, valueText: '6 项', pct: 7 }] },
|
|
2804
|
-
alt: {
|
|
2805
|
-
kind: 'list',
|
|
2806
|
-
label: { zh: '六域清单', en: 'Six domains' },
|
|
2807
|
-
data: {
|
|
2808
|
-
rows: [
|
|
2809
|
-
{ name: { zh: '财 · 22 项', en: 'Finance, 22' }, desc: { zh: '已发布 · 口径 C-2026-11-01 · 血缘 3 层', en: 'Published · caliber C-2026-11-01 · 3 lineage levels' }, badge: { zh: '生效', en: 'Active' }, tone: 'ok', meta: { zh: '22 项', en: '22' } },
|
|
2810
|
-
{ name: { zh: '资 · 16 项', en: 'Funds, 16' }, desc: { zh: '已发布 · 口径 C-2026-11-01 · 血缘 3 层', en: 'Published · caliber C-2026-11-01 · 3 lineage levels' }, badge: { zh: '生效', en: 'Active' }, tone: 'ok', meta: { zh: '16 项', en: '16' } },
|
|
2811
|
-
{ name: { zh: '推 · 18 项', en: 'Forecast, 18' }, desc: { zh: '已发布 + 灰度 · 随假设 A3 重算', en: 'Published plus preview · recomputed with assumption A3' }, badge: { zh: '生效', en: 'Active' }, tone: 'ok', meta: { zh: '18 项', en: '18' } },
|
|
2812
|
-
{ name: { zh: '税 · 14 项', en: 'Tax, 14' }, desc: { zh: '已发布 · 口径 C-2026-11-01 · 预计口径单列', en: 'Published · caliber C-2026-11-01 · estimate basis kept apart' }, badge: { zh: '生效', en: 'Active' }, tone: 'ok', meta: { zh: '14 项', en: '14' } },
|
|
2813
|
-
{ name: { zh: '数 · 10 项', en: 'Data, 10' }, desc: { zh: '10 项可用 · 口径 C-2026-11-01 · 血缘 3 层', en: '10 usable · caliber C-2026-11-01 · 3 lineage levels' }, badge: { zh: '生效', en: 'Active' }, tone: 'ok', meta: { zh: '10 项', en: '10' } },
|
|
2814
|
-
{ name: { zh: '总览 · 6 项', en: 'Overview, 6' }, desc: { zh: '已发布 · 引用四页口径 · 血缘 3 层', en: 'Published · reuses four page calibers · 3 lineage levels' }, badge: { zh: '生效', en: 'Active' }, tone: 'ok', meta: { zh: '6 项', en: '6' } }
|
|
2815
|
-
]
|
|
2816
|
-
}
|
|
2817
|
-
},
|
|
2818
|
-
foot: {
|
|
2819
|
-
zh: '指标 86 = 财 22 + 资 16 + 推 18 + 税 14 + 数 10 + 总览 6(六域可用口径);总 102 = 已发布 78 + 灰度 8 + 停用 4 + 草稿 12(四态只在 KPI 2 弹层,不按域拆);口径版本 C-2026-11-01(11-01 发布并生效),版本链 C-2026-10-01(已作废,保留引用)→ C-2026-11-01 →(计划)C-2026-12-01;决策表与费用标准随版本发布、带生效日',
|
|
2820
|
-
en: '86 metrics = finance 22 + funds 16 + forecast 18 + tax 14 + data 10 + overview 6 on a usable basis; 102 total = 78 published + 8 preview + 4 retired + 12 draft, shown only in the KPI 2 modal, never split by domain; caliber C-2026-11-01 released and effective on Nov 1; chain C-2026-10-01 superseded but kept, C-2026-11-01 effective, C-2026-12-01 planned for Dec 1; decision tables ship with effective dates'
|
|
2821
|
-
}
|
|
2822
|
-
},
|
|
2823
|
-
{
|
|
2824
|
-
icon: 'eye',
|
|
2825
|
-
label: { zh: '质量与对账', en: 'Quality and recon' },
|
|
2826
|
-
kind: 'checks',
|
|
2827
|
-
pill: { zh: '3 待处理', en: '3 open' },
|
|
2828
|
-
summary: [{ label: { zh: '规则', en: 'Rules' }, value: { zh: '28', en: '28' } }, { label: { zh: '近 24h 命中', en: 'Hits in 24h' }, value: { zh: '11', en: '11' }, tone: 'warn' }, { label: { zh: '待处理', en: 'Open' }, value: { zh: '3', en: '3' }, tone: 'warn' }],
|
|
2829
|
-
data: {
|
|
2830
|
-
items: [
|
|
2831
|
-
{ name: { zh: '凭证借贷平衡', en: 'Voucher debit-credit balance' }, desc: { zh: '1,284 张 · 借贷差 ¥0.00 · 2026-11 期间', en: '1,284 vouchers · gap ¥0.00 · Nov 2026' }, state: 'ok', meta: { zh: '通过', en: 'Passed' } },
|
|
2832
|
-
{ name: { zh: '科目余额六栏平衡', en: 'Six-column trial balance' }, desc: { zh: '借 = 贷 = ¥29,915,200.00 · 试算差 ¥0.00', en: 'Debits equal credits at ¥29,915,200.00 · gap ¥0.00' }, state: 'ok', meta: { zh: '通过', en: 'Passed' } },
|
|
2833
|
-
{ name: { zh: '银行余额对账', en: 'Bank balance reconciliation' }, desc: { zh: '10-31 同期 · 银行侧 ¥8,550,000.00 vs 账面 ¥8,450,000.00 · 未达 3 笔 ¥100,000.00(62,000 + 35,000 + 3,000)· 调节后 ¥0.00', en: 'Oct 31 basis · bank side ¥8,550,000.00 vs books ¥8,450,000.00 · 3 unreconciled items ¥100,000.00 (62,000 + 35,000 + 3,000) · nil after adjusting' }, state: 'warn', meta: { zh: '差异待核', en: 'Gap open' } },
|
|
2834
|
-
{ name: { zh: '销项票面 vs 申报', en: 'Output invoices vs filing' }, desc: { zh: '4 张 · 销项 ¥150,800.00 · 票面与申报一致', en: '4 invoices · output VAT ¥150,800.00 · invoice and filing agree' }, state: 'ok', meta: { zh: '通过', en: 'Passed' } },
|
|
2835
|
-
{ name: { zh: '主数据唯一性', en: 'Master data uniqueness' }, desc: { zh: '客商 386 · 重复 0 · 主键去重校验', en: '386 partners · 0 duplicates · key uniqueness check' }, state: 'ok', meta: { zh: '通过', en: 'Passed' } },
|
|
2836
|
-
{ name: { zh: '批次完整性', en: 'Batch completeness' }, desc: { zh: 'B-109 分区 124/124 · 全量校验 11-09 通过', en: 'B-109 partitions 124 of 124 · full check passed Nov 9' }, state: 'ok', meta: { zh: '通过', en: 'Passed' } },
|
|
2837
|
-
{ name: { zh: '影像 OCR 识别率', en: 'Imaging OCR recognition' }, desc: { zh: '98.7% · 阈值 99% · 准确性规则 · 低于阈值 0.3 个百分点', en: '98.7% · threshold 99% · accuracy rule · 0.3 points below' }, state: 'warn', meta: { zh: '待处理', en: 'Open' } },
|
|
2838
|
-
{ name: { zh: '接入及时性', en: 'Ingest timeliness' }, desc: { zh: '建行 2 户 + 中行 1 户 T-1 未更新 · 涉及建行、中行 2 行 / 3 源', en: 'CCB 2 accounts plus BOC 1 not refreshed for T-1 · 2 rows and 3 sources' }, state: 'warn', meta: { zh: '待处理', en: 'Open' } }
|
|
2839
|
-
]
|
|
2840
|
-
},
|
|
2841
|
-
foot: { zh: '规则 28 = 完整性 8 + 一致性 7 + 准确性 6 + 及时性 4 + 唯一性 3;近 24h 命中 11 = 已处置 8 + 待处理 3;待处理 3 = 影像 1 + 及时性 1 + 在途差异 1,规则类为准确性 1(影像 OCR)+ 一致性 1(银行未达)+ 及时性 1(T-1 未更新)', en: '28 rules = 8 completeness + 7 consistency + 6 accuracy + 4 timeliness + 3 uniqueness; 11 hits in 24h = 8 handled + 3 open; 3 open = imaging 1 + timeliness 1 + in-transit gap 1, by rule class accuracy 1 (imaging OCR) + consistency 1 (bank gap) + timeliness 1 (T-1)' }
|
|
2842
|
-
},
|
|
2843
|
-
{
|
|
2844
|
-
icon: 'bell',
|
|
2845
|
-
label: { zh: '回执与审计', en: 'Receipts and audit' },
|
|
2846
|
-
kind: 'table',
|
|
2847
|
-
pill: { zh: '1,284 次 / 7 天', en: '1,284 calls / 7 days' },
|
|
2848
|
-
summary: [{ label: { zh: '近 7 天调用', en: 'Calls 7 days' }, value: { zh: '1,284', en: '1,284' } }, { label: { zh: '失败', en: 'Failures' }, value: { zh: '12', en: '12' }, tone: 'warn' }, { label: { zh: '审计断链', en: 'Chain breaks' }, value: { zh: '0', en: '0' }, tone: 'ok' }],
|
|
2849
|
-
data: {
|
|
2850
|
-
columns: [{ label: { zh: '时间', en: 'Time' }, align: 'right' }, { label: { zh: '场景或技能', en: 'Scenario or skill' } }, { label: { zh: '通道工具', en: 'Channel and tool' } }, { label: { zh: '结果', en: 'Result' } }, { label: { zh: '耗时', en: 'Latency' }, align: 'right' }, { label: { zh: '回执号', en: 'Receipt' } }],
|
|
2851
|
-
rows: [
|
|
2852
|
-
['11-11 23:12', { zh: '月末关账助手 · 场景触发', en: 'Month-end close · trigger' }, { zh: 'U8 · 凭证检索', en: 'U8 · voucher search' }, { zh: '成功', en: 'Success', tone: 'ok' }, '820ms', 'RC-1111-2312-183'],
|
|
2853
|
-
['11-11 23:05', { zh: '发票查验 v1.2 · 按需', en: 'Invoice checks v1.2 · on demand' }, { zh: '数电发票 · 查验', en: 'E-invoice · verify' }, { zh: '成功', en: 'Success', tone: 'ok' }, '640ms', 'RC-1111-2305-179'],
|
|
2854
|
-
['11-11 22:40', { zh: '税务申报助手(灰度)· 按需', en: 'Tax filing (gray) · on demand' }, { zh: '电子税务局 · 申报预检', en: 'Tax bureau · pre-check' }, { zh: '成功', en: 'Success', tone: 'ok' }, '1.2s', 'RC-1111-2240-168'],
|
|
2855
|
-
['11-11 21:30', { zh: '智能对账 · 按需', en: 'Smart reconciliation · on demand' }, { zh: '银行直连 · 流水拉取', en: 'Bank link · statements' }, { zh: '成功', en: 'Success', tone: 'ok' }, '2.1s', 'RC-1111-2130-151'],
|
|
2856
|
-
['11-11 20:15', { zh: '未知调用方 · 按需', en: 'Unknown caller · on demand' }, { zh: '银行直连 · 付款接口', en: 'Bank link · payment API' }, { zh: '拦截', en: 'Blocked', tone: 'warn' }, '0ms', 'RC-1111-2015-142'],
|
|
2857
|
-
['11-11 18:40', { zh: '凭证识别录入 v3.2 · 按需', en: 'Voucher capture v3.2 · on demand' }, { zh: '影像 OCR · 识别', en: 'OCR · reading' }, { zh: '失败(超时)', en: 'Failed (timeout)', tone: 'warn' }, '5.0s', 'RC-1111-1840-121'],
|
|
2858
|
-
['11-11 17:20', { zh: '账龄分析 v2.0 · 按需', en: 'AR aging v2.0 · on demand' }, { zh: 'U8 · 凭证检索', en: 'U8 · voucher search' }, { zh: '成功', en: 'Success', tone: 'ok' }, '900ms', 'RC-1111-1720-112'],
|
|
2859
|
-
['11-11 06:30', { zh: '滚动预测 R-1111-06 · 场景触发', en: 'Rolling forecast R-1111-06 · trigger' }, { zh: 'U8 + 银行直连 · 现金流预测 v2.1', en: 'U8 and bank link · cash forecast v2.1' }, { zh: '成功', en: 'Success', tone: 'ok' }, '1.8s', 'RC-1111-0630-012']
|
|
2860
|
-
]
|
|
2861
|
-
},
|
|
2862
|
-
alt: {
|
|
2863
|
-
kind: 'list',
|
|
2864
|
-
label: { zh: '导出与回执', en: 'Exports and receipts' },
|
|
2865
|
-
data: {
|
|
2866
|
-
rows: [
|
|
2867
|
-
{ name: { zh: '资产负债表', en: 'Balance sheet' }, desc: { zh: '2026-10 期次 · 水印五件齐全 · 导出人 王静 · 11-11 09:20', en: 'Oct 2026 · all five watermarks · exported by Wang Jing · Nov 11 09:20' }, badge: { zh: '已导出', en: 'Exported' }, tone: 'ok', meta: { zh: '业务单号 EX-0912', en: 'Export No. EX-0912' } },
|
|
2868
|
-
{ name: { zh: '利润表', en: 'Income statement' }, desc: { zh: '2026-10 期次 · 水印五件齐全 · 导出人 王静 · 11-11 09:20', en: 'Oct 2026 · all five watermarks · exported by Wang Jing · Nov 11 09:20' }, badge: { zh: '已导出', en: 'Exported' }, tone: 'ok', meta: { zh: '业务单号 EX-0913', en: 'Export No. EX-0913' } },
|
|
2869
|
-
{ name: { zh: '现金流量表', en: 'Cash-flow statement' }, desc: { zh: '2026-10 期次 · 底稿待复核 · 水印标待复核 · 导出人 王静', en: 'Oct 2026 · workpaper pending review · watermark marked draft · exported by Wang Jing' }, badge: { zh: '待复核版', en: 'Draft version' }, tone: 'warn', meta: { zh: '业务单号 EX-0914', en: 'Export No. EX-0914' } },
|
|
2870
|
-
{ name: { zh: '资金日报', en: 'Daily cash brief' }, desc: { zh: '2026-11-10 · 水印五件齐全 · 导出人 陈可 · 11-11 08:10', en: 'Nov 10 2026 · all five watermarks · exported by Chen Ke · Nov 11 08:10' }, badge: { zh: '已导出', en: 'Exported' }, tone: 'ok', meta: { zh: '业务单号 EX-0915', en: 'Export No. EX-0915' } },
|
|
2871
|
-
{ name: { zh: '税务申报表', en: 'Tax return' }, desc: { zh: '2026-10 期次 · 待报送草稿 · 水印五件齐全 · 导出人 李珂', en: 'Oct 2026 · draft before filing · all five watermarks · exported by Li Ke' }, badge: { zh: '草稿导出', en: 'Draft export' }, tone: 'warn', meta: { zh: '业务单号 EX-0916', en: 'Export No. EX-0916' } }
|
|
2872
|
-
]
|
|
2873
|
-
}
|
|
2874
|
-
},
|
|
2875
|
-
foot: {
|
|
2876
|
-
zh: '近 7 天 1,284 = 成功 1,269 + 失败 12 + 越权拦截 3,审计断链 0;失败 12 = 超时 9 + 鉴权 2 + 限流 1;RC 号 = RC-{MMDD}-{HHMM}-{三位当日流水},当日 183 条;拦截不算失败;R-1111-06 属运行编号、RC 属回执编号,不同域;导出侧 5 = 财报导出 3 + 资金日报 1 + 税务申报表 1,失败 0,最近 11-11 09:20;水印五件 = 口径版本 + 数据时点 + 血缘短链 + 导出人 + 导出时间;业务单号 EX-… 为导出凭据号,RC-… 在审计链,两套编号不互称;导出发起 / 审批 / 下载不在本页,本页只读',
|
|
2877
|
-
en: '7-day calls 1,284 = 1,269 succeeded + 12 failed + 3 blocked, with 0 chain breaks; 12 failures = 9 timeouts + 2 auth + 1 rate limit; RC numbers follow RC-{MMDD}-{HHMM}-{daily serial} with 183 that day; a blocked call is not a failure; R-1111-06 is a run number and RC is a receipt number, different domains; exports: 5 = 3 statement exports + 1 daily cash brief + 1 tax return with 0 failures, latest at 09:20 on Nov 11; five watermarks = caliber version + data time + lineage link + exporter + export time; EX- numbers are export references and RC- numbers live in the audit chain, the two are never interchanged; export initiation, approval and download happen outside this page, which stays read only'
|
|
2878
|
-
}
|
|
2879
|
-
},
|
|
2880
|
-
],
|
|
2881
2963
|
},
|
|
2882
|
-
|
|
2883
|
-
|
|
2964
|
+
team: {
|
|
2965
|
+
title: { zh: '团队模式', en: 'Team mode' },
|
|
2966
|
+
},
|
|
2967
|
+
finance: {
|
|
2968
|
+
botId: 'finance',
|
|
2884
2969
|
title: { zh: '财 · 过去', en: 'Books · Past' },
|
|
2885
2970
|
kpis: [
|
|
2886
2971
|
{
|
|
@@ -3121,8 +3206,8 @@ window.__ModuleLoader__.load({
|
|
|
3121
3206
|
},
|
|
3122
3207
|
],
|
|
3123
3208
|
},
|
|
3124
|
-
|
|
3125
|
-
botId: '
|
|
3209
|
+
funds: {
|
|
3210
|
+
botId: 'funds',
|
|
3126
3211
|
title: { zh: '资 · 现在', en: 'Funds · Now' },
|
|
3127
3212
|
kpis: [
|
|
3128
3213
|
{
|
|
@@ -3312,8 +3397,8 @@ window.__ModuleLoader__.load({
|
|
|
3312
3397
|
},
|
|
3313
3398
|
],
|
|
3314
3399
|
},
|
|
3315
|
-
|
|
3316
|
-
botId: '
|
|
3400
|
+
forecast: {
|
|
3401
|
+
botId: 'forecast',
|
|
3317
3402
|
title: { zh: '推 · 未来', en: 'Forecast · Future' },
|
|
3318
3403
|
kpis: [
|
|
3319
3404
|
{
|
|
@@ -3567,8 +3652,8 @@ window.__ModuleLoader__.load({
|
|
|
3567
3652
|
},
|
|
3568
3653
|
],
|
|
3569
3654
|
},
|
|
3570
|
-
|
|
3571
|
-
botId: '
|
|
3655
|
+
tax: {
|
|
3656
|
+
botId: 'tax',
|
|
3572
3657
|
title: { zh: '税 · 税务', en: 'Tax · Filing' },
|
|
3573
3658
|
kpis: [
|
|
3574
3659
|
{
|
|
@@ -3778,18 +3863,18 @@ window.__ModuleLoader__.load({
|
|
|
3778
3863
|
* 纯数据片段:无函数、无 h(、无 React;由 workbench / entry 通过 deps 注入。
|
|
3779
3864
|
* ===================================================================== */
|
|
3780
3865
|
|
|
3781
|
-
const BOT_IDS = ['
|
|
3866
|
+
const BOT_IDS = ['finance', 'funds', 'forecast', 'tax']
|
|
3782
3867
|
|
|
3783
3868
|
const SKILL_IDS = {
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3869
|
+
finance: ['vouchers', 'ledgers', 'ar-aging', 'payables', 'fixed-assets', 'close', 'statements', 'workpapers'],
|
|
3870
|
+
funds: ['accounts', 'payments', 'recon', 'plan', 'restricted', 'bills'],
|
|
3871
|
+
forecast: ['budget', 'forecast', 'scenario', 'assumption', 'allocation', 'mgmt-report', 'variance', 'cash-plan'],
|
|
3872
|
+
tax: ['output-invoice', 'input-verify', 'invoice-check', 'file', 'pay', 'burden'],
|
|
3788
3873
|
}
|
|
3789
3874
|
|
|
3790
3875
|
const BOTS = {
|
|
3791
|
-
|
|
3792
|
-
id: '
|
|
3876
|
+
finance: {
|
|
3877
|
+
id: 'finance',
|
|
3793
3878
|
name: { zh: '账房', en: 'The Ledger' },
|
|
3794
3879
|
short: { zh: '账', en: 'L' },
|
|
3795
3880
|
role: { zh: '把账记对、把账关好、把证据留全', en: 'Keep the books correct, close them cleanly, keep the evidence complete' },
|
|
@@ -3821,8 +3906,8 @@ window.__ModuleLoader__.load({
|
|
|
3821
3906
|
en: 'For questions about whether the books balance, where the close stands or whether statements are ready: give the conclusion first, then the definition and the evidence path (statements, workpapers, ledgers, vouchers). Name the document or step behind every gap.',
|
|
3822
3907
|
},
|
|
3823
3908
|
},
|
|
3824
|
-
|
|
3825
|
-
id: '
|
|
3909
|
+
funds: {
|
|
3910
|
+
id: 'funds',
|
|
3826
3911
|
name: { zh: '司库', en: 'The Treasurer' },
|
|
3827
3912
|
short: { zh: '司', en: 'T' },
|
|
3828
3913
|
role: { zh: '守住可支付能力与安全垫', en: 'Guard the payment capacity and the safety buffer' },
|
|
@@ -3854,8 +3939,8 @@ window.__ModuleLoader__.load({
|
|
|
3854
3939
|
en: 'For questions about how much cash is available, whether payments are covered or where the gap is: give the available balance and the buffer verdict first, then the timestamp, restrictions and unreconciled items. Flag payment dependencies when they matter.',
|
|
3855
3940
|
},
|
|
3856
3941
|
},
|
|
3857
|
-
|
|
3858
|
-
id: '
|
|
3942
|
+
forecast: {
|
|
3943
|
+
id: 'forecast',
|
|
3859
3944
|
name: { zh: '军师', en: 'The Planner' },
|
|
3860
3945
|
short: { zh: '军', en: 'P' },
|
|
3861
3946
|
role: { zh: '把不确定性与假设讲清楚', en: 'Make uncertainty and assumptions explicit' },
|
|
@@ -3887,8 +3972,8 @@ window.__ModuleLoader__.load({
|
|
|
3887
3972
|
en: 'For questions about budget headroom, the next three months or what an assumption change does: give the range and the assumption version first, then the conclusion and sensitivities. Keep forecast and commitment separate.',
|
|
3888
3973
|
},
|
|
3889
3974
|
},
|
|
3890
|
-
|
|
3891
|
-
id: '
|
|
3975
|
+
tax: {
|
|
3976
|
+
id: 'tax',
|
|
3892
3977
|
name: { zh: '税官', en: 'The Tax Officer' },
|
|
3893
3978
|
short: { zh: '税', en: 'X' },
|
|
3894
3979
|
role: { zh: '不错报、不漏报、红线可见', en: 'No wrong filing, no missed filing, red lines visible' },
|
|
@@ -3921,13 +4006,27 @@ window.__ModuleLoader__.load({
|
|
|
3921
4006
|
},
|
|
3922
4007
|
},
|
|
3923
4008
|
}
|
|
3924
|
-
|
|
4009
|
+
|
|
4010
|
+
/** Skill dependency declarations: skill name → prerequisite skill names.
|
|
4011
|
+
* When a user adds a skill, the UI warns if any prerequisite is missing.
|
|
4012
|
+
* These are only the cross-skill deps that matter at binding time; the
|
|
4013
|
+
* skill's own SKILL.md is the authority for runtime requirements. */
|
|
4014
|
+
const SKILL_DEPS = {
|
|
4015
|
+
/* DSH standard skills that need MCP channels or other skills. */
|
|
4016
|
+
'intelligent-recon': ['bank-mcp-channel'],
|
|
4017
|
+
'invoice-check': ['tax-system-channel'],
|
|
4018
|
+
'tax-filing-assistant': ['tax-system-channel'],
|
|
4019
|
+
'bank-rpa': ['bank-mcp-channel'],
|
|
4020
|
+
}
|
|
4021
|
+
/** Side navigation: 总览 + 四个业务 Bot + 团队模式 + 底座(MCP 与数据合一)。
|
|
4022
|
+
团队模式排在底座之前:底座是「基础设施」类条目,放最后收尾更像栏目边界。 */
|
|
3925
4023
|
const NAV = [
|
|
3926
4024
|
{ id: 'overview', icon: 'grid', key: 'nav.overview' },
|
|
3927
|
-
{ id: '
|
|
3928
|
-
{ id: '
|
|
3929
|
-
{ id: '
|
|
3930
|
-
{ id: '
|
|
4025
|
+
{ id: 'finance', icon: 'doc', key: 'nav.finance' },
|
|
4026
|
+
{ id: 'funds', icon: 'wallet', key: 'nav.funds' },
|
|
4027
|
+
{ id: 'forecast', icon: 'chart', key: 'nav.forecast' },
|
|
4028
|
+
{ id: 'tax', icon: 'list', key: 'nav.tax' },
|
|
4029
|
+
{ id: 'team', icon: 'users', key: 'nav.team' },
|
|
3931
4030
|
{ id: 'base', icon: 'layers', key: 'nav.base' },
|
|
3932
4031
|
]
|
|
3933
4032
|
/** Normalized label of a KPI (inline literal or dictionary key). */
|
|
@@ -4406,6 +4505,15 @@ window.__ModuleLoader__.load({
|
|
|
4406
4505
|
'ai.expand': '展开助手',
|
|
4407
4506
|
'ai.prompt.insights': '总结一下本月账务的关键异常和待办。',
|
|
4408
4507
|
'ai.prompt.forecast': '按当前数据预测未来 3 个月现金流,并给出风险点。',
|
|
4508
|
+
'team.title': '团队协作',
|
|
4509
|
+
'team.role': '账房 · 司库 · 军师 · 税官',
|
|
4510
|
+
'team.sub': '共享工作区 · 任务板 · 多 Bot 协作',
|
|
4511
|
+
'team.avatar': '团队',
|
|
4512
|
+
'team.session': '团队会话',
|
|
4513
|
+
'team.greeting': '我是财务团队。月末关账、资金对账、税务申报、现金流预测都可以交给团队;先告诉我这次要做什么?',
|
|
4514
|
+
'team.chip.close': '做一次月末关账',
|
|
4515
|
+
'team.chip.recon': '对一下银行账户',
|
|
4516
|
+
'team.chip.tax': '核一下本期要交的税',
|
|
4409
4517
|
'ai.prompt.reports': '生成本月经营简报:收入、支出、利润和异常项。',
|
|
4410
4518
|
'aging.title': '应收账款账龄',
|
|
4411
4519
|
'aging.totalLabel': '应收合计',
|
|
@@ -4427,11 +4535,12 @@ window.__ModuleLoader__.load({
|
|
|
4427
4535
|
'metric.recent': '近 10 个时点',
|
|
4428
4536
|
'metric.empty': '暂无明细',
|
|
4429
4537
|
'nav.overview': '总览',
|
|
4430
|
-
'nav.
|
|
4431
|
-
'nav.
|
|
4432
|
-
'nav.
|
|
4433
|
-
'nav.
|
|
4538
|
+
'nav.finance': '财 · 过去',
|
|
4539
|
+
'nav.funds': '资 · 现在',
|
|
4540
|
+
'nav.forecast': '推 · 未来',
|
|
4541
|
+
'nav.tax': '税 · 税务',
|
|
4434
4542
|
'nav.base': '底座 · MCP 与数据',
|
|
4543
|
+
'nav.team': '团队模式',
|
|
4435
4544
|
'theme.toDark': '切换到深色',
|
|
4436
4545
|
'theme.toLight': '切换到浅色',
|
|
4437
4546
|
'list.empty': '暂无数据',
|
|
@@ -4461,15 +4570,15 @@ window.__ModuleLoader__.load({
|
|
|
4461
4570
|
'feed.approvalSub': '18 分钟前',
|
|
4462
4571
|
'feed.alert': '数据源预警',
|
|
4463
4572
|
'feed.alertSub': '1 小时前',
|
|
4464
|
-
'
|
|
4465
|
-
'
|
|
4466
|
-
'
|
|
4467
|
-
'
|
|
4468
|
-
'
|
|
4469
|
-
'
|
|
4470
|
-
'
|
|
4471
|
-
'
|
|
4472
|
-
'
|
|
4573
|
+
'finance.vouchers': '本期凭证数',
|
|
4574
|
+
'finance.vouchersSub': '环比 +6.2%',
|
|
4575
|
+
'finance.diff': '借贷差异',
|
|
4576
|
+
'finance.diffSub': '差 0 笔',
|
|
4577
|
+
'finance.pending': '待处理单据',
|
|
4578
|
+
'finance.pendingSub': '较昨日 -12',
|
|
4579
|
+
'finance.close': '期末进度',
|
|
4580
|
+
'finance.closeSub': '已完成 8 / 12',
|
|
4581
|
+
'finance.books': '账务',
|
|
4473
4582
|
'books.period': '期间与关账',
|
|
4474
4583
|
'books.periodSub': '2026-11 · 未锁账',
|
|
4475
4584
|
'books.vouchers': '凭证与分录',
|
|
@@ -4478,7 +4587,7 @@ window.__ModuleLoader__.load({
|
|
|
4478
4587
|
'books.ledgerSub': '明细账 / 总账',
|
|
4479
4588
|
'books.snapshot': '期末镜像',
|
|
4480
4589
|
'books.snapshotSub': 'ERP 关账状态',
|
|
4481
|
-
'
|
|
4590
|
+
'finance.reports': '报表',
|
|
4482
4591
|
'reports.statutory': '法定报表',
|
|
4483
4592
|
'reports.statutorySub': '资产负债表 / 利润表 / 现金流量表',
|
|
4484
4593
|
'reports.consol': '合并(占位)',
|
|
@@ -4487,29 +4596,29 @@ window.__ModuleLoader__.load({
|
|
|
4487
4596
|
'reports.taxSub': '进项 / 销项',
|
|
4488
4597
|
'reports.workpaper': '底稿与回执',
|
|
4489
4598
|
'reports.workpaperSub': '取数链 / 凭据',
|
|
4490
|
-
'
|
|
4599
|
+
'finance.closeMirror': '期末镜像',
|
|
4491
4600
|
'close.step1': '关账步骤',
|
|
4492
4601
|
'close.step1Sub': 'ERP 推进中',
|
|
4493
4602
|
'close.step2': '锁账',
|
|
4494
4603
|
'close.step2Sub': '未锁定',
|
|
4495
4604
|
'close.step3': '重跑',
|
|
4496
4605
|
'close.step3Sub': '本架构不发起',
|
|
4497
|
-
'
|
|
4606
|
+
'finance.check': '核对异常',
|
|
4498
4607
|
'check.recon': '勾稽差异',
|
|
4499
4608
|
'check.reconSub': '本期',
|
|
4500
4609
|
'check.cross': '跨表差异',
|
|
4501
4610
|
'check.crossSub': '待处理',
|
|
4502
4611
|
'check.miss': '漏账断言',
|
|
4503
4612
|
'check.missSub': '本期',
|
|
4504
|
-
'
|
|
4613
|
+
'finance.datasets': '数据集目录',
|
|
4505
4614
|
'ds.voucher': '凭证与分录',
|
|
4506
4615
|
'ds.voucherSub': '只读',
|
|
4507
4616
|
'ds.ledger': '账簿',
|
|
4508
4617
|
'ds.ledgerSub': '只读',
|
|
4509
4618
|
'ds.docs': '单据(报销 / 发票 / 合同)',
|
|
4510
4619
|
'ds.docsSub': '只读',
|
|
4511
|
-
'
|
|
4512
|
-
'
|
|
4620
|
+
'finance.reportsDone': '报表完成',
|
|
4621
|
+
'finance.reportsDoneSub': '已出 3 / 4 张',
|
|
4513
4622
|
'progress.done': '已完成',
|
|
4514
4623
|
'bars.debit': '借方',
|
|
4515
4624
|
'bars.credit': '贷方',
|
|
@@ -4528,17 +4637,17 @@ window.__ModuleLoader__.load({
|
|
|
4528
4637
|
'focus.time.past': '过去 · 已发生',
|
|
4529
4638
|
'focus.time.now': '现在 · 在途',
|
|
4530
4639
|
'focus.time.future': '未来 · 假设',
|
|
4531
|
-
'focus.
|
|
4532
|
-
'focus.
|
|
4533
|
-
'focus.
|
|
4534
|
-
'focus.
|
|
4535
|
-
'
|
|
4536
|
-
'
|
|
4537
|
-
'
|
|
4538
|
-
'
|
|
4539
|
-
'
|
|
4540
|
-
'
|
|
4541
|
-
'
|
|
4640
|
+
'focus.finance.sub': '本期凭证与期末进度',
|
|
4641
|
+
'focus.funds.sub': '总余额与未达项',
|
|
4642
|
+
'focus.forecast.sub': '预算使用与预测',
|
|
4643
|
+
'focus.tax.sub': '票税与申报进度',
|
|
4644
|
+
'funds.unreconciled': '未达项',
|
|
4645
|
+
'funds.plan': '90 天计划',
|
|
4646
|
+
'forecast.revenue': '预测收入',
|
|
4647
|
+
'forecast.variance': '与上版差异',
|
|
4648
|
+
'tax.output': '销项税额',
|
|
4649
|
+
'tax.input': '进项税额',
|
|
4650
|
+
'tax.filing': '申报进度',
|
|
4542
4651
|
'blocks.paused': '已暂停',
|
|
4543
4652
|
'blocks.open': '打开板块',
|
|
4544
4653
|
'blocks.unit.vouchers': '凭证',
|
|
@@ -4659,6 +4768,15 @@ window.__ModuleLoader__.load({
|
|
|
4659
4768
|
'ai.expand': 'Expand assistant',
|
|
4660
4769
|
'ai.prompt.insights': 'Summarize this month’s key exceptions and open items.',
|
|
4661
4770
|
'ai.prompt.forecast': 'Forecast the next three months of cash flow from the current data and list the risks.',
|
|
4771
|
+
'team.title': 'Team',
|
|
4772
|
+
'team.role': 'Ledger · Treasurer · Planner · Tax Officer',
|
|
4773
|
+
'team.sub': 'Shared workspace · task board · multi-bot',
|
|
4774
|
+
'team.avatar': 'Team',
|
|
4775
|
+
'team.session': 'Team session',
|
|
4776
|
+
'team.greeting': 'I am the finance team. Month-end close, bank reconciliation, tax filing, cash-flow forecast — tell me what to do first.',
|
|
4777
|
+
'team.chip.close': 'Run a month-end close',
|
|
4778
|
+
'team.chip.recon': 'Reconcile the bank accounts',
|
|
4779
|
+
'team.chip.tax': 'Check the tax due this period',
|
|
4662
4780
|
'ai.prompt.reports': 'Draft this month’s operating brief: income, expenses, profit, and exceptions.',
|
|
4663
4781
|
'aging.title': 'AR aging',
|
|
4664
4782
|
'aging.totalLabel': 'Outstanding',
|
|
@@ -4680,11 +4798,12 @@ window.__ModuleLoader__.load({
|
|
|
4680
4798
|
'metric.recent': 'Last 10 points',
|
|
4681
4799
|
'metric.empty': 'No details yet',
|
|
4682
4800
|
'nav.overview': 'Overview',
|
|
4683
|
-
'nav.
|
|
4684
|
-
'nav.
|
|
4685
|
-
'nav.
|
|
4686
|
-
'nav.
|
|
4801
|
+
'nav.finance': 'Books · Past',
|
|
4802
|
+
'nav.funds': 'Funds · Now',
|
|
4803
|
+
'nav.forecast': 'Forecast · Future',
|
|
4804
|
+
'nav.tax': 'Tax · Filing',
|
|
4687
4805
|
'nav.base': 'Base · MCP and Data',
|
|
4806
|
+
'nav.team': 'Team mode',
|
|
4688
4807
|
'theme.toDark': 'Switch to dark',
|
|
4689
4808
|
'theme.toLight': 'Switch to light',
|
|
4690
4809
|
'list.empty': 'No data',
|
|
@@ -4714,15 +4833,15 @@ window.__ModuleLoader__.load({
|
|
|
4714
4833
|
'feed.approvalSub': '18 minutes ago',
|
|
4715
4834
|
'feed.alert': 'Source alert',
|
|
4716
4835
|
'feed.alertSub': '1 hour ago',
|
|
4717
|
-
'
|
|
4718
|
-
'
|
|
4719
|
-
'
|
|
4720
|
-
'
|
|
4721
|
-
'
|
|
4722
|
-
'
|
|
4723
|
-
'
|
|
4724
|
-
'
|
|
4725
|
-
'
|
|
4836
|
+
'finance.vouchers': 'Vouchers',
|
|
4837
|
+
'finance.vouchersSub': '+6.2% MoM',
|
|
4838
|
+
'finance.diff': 'Debit-credit diff',
|
|
4839
|
+
'finance.diffSub': '0 lines off',
|
|
4840
|
+
'finance.pending': 'Pending documents',
|
|
4841
|
+
'finance.pendingSub': '-12 vs yesterday',
|
|
4842
|
+
'finance.close': 'Period close',
|
|
4843
|
+
'finance.closeSub': '8 of 12 done',
|
|
4844
|
+
'finance.books': 'Books',
|
|
4726
4845
|
'books.period': 'Period & close',
|
|
4727
4846
|
'books.periodSub': '2026-11 · open',
|
|
4728
4847
|
'books.vouchers': 'Vouchers & lines',
|
|
@@ -4731,7 +4850,7 @@ window.__ModuleLoader__.load({
|
|
|
4731
4850
|
'books.ledgerSub': 'Detail / general',
|
|
4732
4851
|
'books.snapshot': 'Close mirror',
|
|
4733
4852
|
'books.snapshotSub': 'ERP close status',
|
|
4734
|
-
'
|
|
4853
|
+
'finance.reports': 'Reports',
|
|
4735
4854
|
'reports.statutory': 'Statutory reports',
|
|
4736
4855
|
'reports.statutorySub': 'BS / IS / CF',
|
|
4737
4856
|
'reports.consol': 'Consolidation (placeholder)',
|
|
@@ -4740,29 +4859,29 @@ window.__ModuleLoader__.load({
|
|
|
4740
4859
|
'reports.taxSub': 'Input / output VAT',
|
|
4741
4860
|
'reports.workpaper': 'Workpapers & receipts',
|
|
4742
4861
|
'reports.workpaperSub': 'Lineage / credentials',
|
|
4743
|
-
'
|
|
4862
|
+
'finance.closeMirror': 'Close mirror',
|
|
4744
4863
|
'close.step1': 'Close steps',
|
|
4745
4864
|
'close.step1Sub': 'ERP in progress',
|
|
4746
4865
|
'close.step2': 'Lock',
|
|
4747
4866
|
'close.step2Sub': 'Not locked',
|
|
4748
4867
|
'close.step3': 'Rerun',
|
|
4749
4868
|
'close.step3Sub': 'Not initiated here',
|
|
4750
|
-
'
|
|
4869
|
+
'finance.check': 'Check exceptions',
|
|
4751
4870
|
'check.recon': 'Reconciliation diff',
|
|
4752
4871
|
'check.reconSub': 'This period',
|
|
4753
4872
|
'check.cross': 'Cross-table diff',
|
|
4754
4873
|
'check.crossSub': 'Open',
|
|
4755
4874
|
'check.miss': 'Missing-posting assertion',
|
|
4756
4875
|
'check.missSub': 'This period',
|
|
4757
|
-
'
|
|
4876
|
+
'finance.datasets': 'Datasets',
|
|
4758
4877
|
'ds.voucher': 'Vouchers & lines',
|
|
4759
4878
|
'ds.voucherSub': 'Read-only',
|
|
4760
4879
|
'ds.ledger': 'Ledgers',
|
|
4761
4880
|
'ds.ledgerSub': 'Read-only',
|
|
4762
4881
|
'ds.docs': 'Documents (expense / invoice / contract)',
|
|
4763
4882
|
'ds.docsSub': 'Read-only',
|
|
4764
|
-
'
|
|
4765
|
-
'
|
|
4883
|
+
'finance.reportsDone': 'Reports done',
|
|
4884
|
+
'finance.reportsDoneSub': '3 of 4 issued',
|
|
4766
4885
|
'progress.done': 'completed',
|
|
4767
4886
|
'bars.debit': 'Debit',
|
|
4768
4887
|
'bars.credit': 'Credit',
|
|
@@ -4781,17 +4900,17 @@ window.__ModuleLoader__.load({
|
|
|
4781
4900
|
'focus.time.past': 'Past · booked',
|
|
4782
4901
|
'focus.time.now': 'Now · in flight',
|
|
4783
4902
|
'focus.time.future': 'Future · assumed',
|
|
4784
|
-
'focus.
|
|
4785
|
-
'focus.
|
|
4786
|
-
'focus.
|
|
4787
|
-
'focus.
|
|
4788
|
-
'
|
|
4789
|
-
'
|
|
4790
|
-
'
|
|
4791
|
-
'
|
|
4792
|
-
'
|
|
4793
|
-
'
|
|
4794
|
-
'
|
|
4903
|
+
'focus.finance.sub': 'Vouchers & close progress',
|
|
4904
|
+
'focus.funds.sub': 'Balance & open items',
|
|
4905
|
+
'focus.forecast.sub': 'Budget usage & forecast',
|
|
4906
|
+
'focus.tax.sub': 'VAT & filing progress',
|
|
4907
|
+
'funds.unreconciled': 'Open items',
|
|
4908
|
+
'funds.plan': '90-day plan',
|
|
4909
|
+
'forecast.revenue': 'Forecast revenue',
|
|
4910
|
+
'forecast.variance': 'Variance vs prior',
|
|
4911
|
+
'tax.output': 'Output VAT',
|
|
4912
|
+
'tax.input': 'Input VAT',
|
|
4913
|
+
'tax.filing': 'Filing progress',
|
|
4795
4914
|
'blocks.paused': 'Paused',
|
|
4796
4915
|
'blocks.open': 'Open block',
|
|
4797
4916
|
'blocks.unit.vouchers': 'vouchers',
|
|
@@ -5122,6 +5241,10 @@ window.__ModuleLoader__.load({
|
|
|
5122
5241
|
chatBots.forEach((id) => { chatStates[id] = emptyState() })
|
|
5123
5242
|
const chatSubs = new Set()
|
|
5124
5243
|
let chatCtx = null
|
|
5244
|
+
/** The `remote.agentTeams` client namespace, injected by mount.js so the
|
|
5245
|
+
* team page can enable Team mode without resolving the `remote` service
|
|
5246
|
+
* (which erpbot does not inject). */
|
|
5247
|
+
let teamRemote = null
|
|
5125
5248
|
|
|
5126
5249
|
/** Resolve one client service lazily (sessions / workspaces / uiWorkspace). */
|
|
5127
5250
|
function assistantService(name) {
|
|
@@ -5133,6 +5256,11 @@ window.__ModuleLoader__.load({
|
|
|
5133
5256
|
}
|
|
5134
5257
|
}
|
|
5135
5258
|
|
|
5259
|
+
/** Wire the `remote.agentTeams` namespace for enabling Team mode. */
|
|
5260
|
+
function setTeamRemote(agentTeams) {
|
|
5261
|
+
teamRemote = agentTeams || null
|
|
5262
|
+
}
|
|
5263
|
+
|
|
5136
5264
|
/** Notify every React subscriber with the fresh map. */
|
|
5137
5265
|
function notifyChat() {
|
|
5138
5266
|
chatSubs.forEach((fn) => {
|
|
@@ -5172,6 +5300,29 @@ window.__ModuleLoader__.load({
|
|
|
5172
5300
|
return snap || emptyState()
|
|
5173
5301
|
}
|
|
5174
5302
|
|
|
5303
|
+
/**
|
|
5304
|
+
* Aggregate activity across every bot, as one boolean. The rail's brand
|
|
5305
|
+
* avatar shows a single status dot, so it needs "is anything running"
|
|
5306
|
+
* rather than four separate tones; calling useChatState per bot inside a
|
|
5307
|
+
* loop would break the rules of hooks.
|
|
5308
|
+
*/
|
|
5309
|
+
function readChatBusy() {
|
|
5310
|
+
return chatBots.some((id) => {
|
|
5311
|
+
const state = chatStates[id]
|
|
5312
|
+
return !!(state && (state.pending || state.running))
|
|
5313
|
+
})
|
|
5314
|
+
}
|
|
5315
|
+
|
|
5316
|
+
/** React binding for the aggregate activity flag. */
|
|
5317
|
+
function useChatBusy() {
|
|
5318
|
+
const [busy, setBusy] = React.useState(() => readChatBusy())
|
|
5319
|
+
React.useEffect(() => {
|
|
5320
|
+
setBusy(readChatBusy())
|
|
5321
|
+
return subscribeChat(() => setBusy(readChatBusy()))
|
|
5322
|
+
}, [])
|
|
5323
|
+
return busy
|
|
5324
|
+
}
|
|
5325
|
+
|
|
5175
5326
|
/** Status tone used by the header dot. */
|
|
5176
5327
|
function chatTone(state) {
|
|
5177
5328
|
if (!state) return ''
|
|
@@ -5182,10 +5333,23 @@ window.__ModuleLoader__.load({
|
|
|
5182
5333
|
return ''
|
|
5183
5334
|
}
|
|
5184
5335
|
|
|
5185
|
-
/**
|
|
5336
|
+
/** Bot ids used before the pinyin → English rename, for storage migration. */
|
|
5337
|
+
const LEGACY_BOT_IDS = { finance: 'cai', funds: 'zi', forecast: 'tui', tax: 'shui' }
|
|
5338
|
+
|
|
5339
|
+
/** Read the persisted session id of one bot, migrating a pre-rename key once. */
|
|
5186
5340
|
function readSessionId(botId) {
|
|
5187
5341
|
try {
|
|
5188
|
-
|
|
5342
|
+
const current = window.localStorage.getItem(SESSION_PREFIX + botId)
|
|
5343
|
+
if (current) return current
|
|
5344
|
+
/* The rename changed the key suffix; carry the legacy id over so an
|
|
5345
|
+
existing conversation is not silently replaced by a fresh one. */
|
|
5346
|
+
const legacy = LEGACY_BOT_IDS[botId]
|
|
5347
|
+
if (!legacy) return null
|
|
5348
|
+
const old = window.localStorage.getItem(SESSION_PREFIX + legacy)
|
|
5349
|
+
if (!old) return null
|
|
5350
|
+
window.localStorage.setItem(SESSION_PREFIX + botId, old)
|
|
5351
|
+
window.localStorage.removeItem(SESSION_PREFIX + legacy)
|
|
5352
|
+
return old
|
|
5189
5353
|
} catch (error) {
|
|
5190
5354
|
return null
|
|
5191
5355
|
}
|
|
@@ -5303,6 +5467,55 @@ window.__ModuleLoader__.load({
|
|
|
5303
5467
|
return rows.slice(0, LIST_LIMIT)
|
|
5304
5468
|
}
|
|
5305
5469
|
|
|
5470
|
+
/**
|
|
5471
|
+
* The resident Session instance's own blank bit — the earliest truthful
|
|
5472
|
+
* signal that a conversation left its blank state.
|
|
5473
|
+
*
|
|
5474
|
+
* The list snapshot only flips `blank` once the `sessionListMetadata`
|
|
5475
|
+
* projection folds the first `turn/start`, which trails the accepted
|
|
5476
|
+
* submission by a tick. A retained Session flips its own `blankBit`
|
|
5477
|
+
* synchronously inside `acceptPrompt`, so reading the instance closes
|
|
5478
|
+
* that gap: the welcome block disappears the moment the composer accepts
|
|
5479
|
+
* the message instead of when the projection catches up.
|
|
5480
|
+
* @param sessionId - Session to inspect.
|
|
5481
|
+
* @returns the blank bit, or undefined when no resident instance is held.
|
|
5482
|
+
*/
|
|
5483
|
+
function residentBlank(sessionId) {
|
|
5484
|
+
if (sessionId === null) return undefined
|
|
5485
|
+
try {
|
|
5486
|
+
const sessions = assistantService('sessions')
|
|
5487
|
+
if (sessions === null || typeof sessions.binding !== 'function') return undefined
|
|
5488
|
+
const binding = sessions.binding(sessionId)
|
|
5489
|
+
if (binding === undefined || binding === null) return undefined
|
|
5490
|
+
const session = binding.session
|
|
5491
|
+
if (session === undefined || session === null || typeof session.getSnapshot !== 'function') return undefined
|
|
5492
|
+
const snapshot = session.getSnapshot()
|
|
5493
|
+
if (snapshot === undefined || snapshot === null) return undefined
|
|
5494
|
+
/* A running/attempted session is never blank even when the projection
|
|
5495
|
+
has not folded its first turn/start yet. */
|
|
5496
|
+
if (snapshot.running === true || snapshot.promptAttempted === true || snapshot.awaitingFirstTurn === true) return false
|
|
5497
|
+
if (typeof snapshot.blank === 'boolean') return snapshot.blank
|
|
5498
|
+
} catch (error) {
|
|
5499
|
+
return undefined
|
|
5500
|
+
}
|
|
5501
|
+
return undefined
|
|
5502
|
+
}
|
|
5503
|
+
|
|
5504
|
+
/**
|
|
5505
|
+
* Blank state for one session: the resident instance first, the list row
|
|
5506
|
+
* second, the caller's cached value last.
|
|
5507
|
+
* @param sessionId - Session to inspect.
|
|
5508
|
+
* @param row - matching session-list row, when the list knows the Session.
|
|
5509
|
+
* @param fallback - value to keep when neither source is available.
|
|
5510
|
+
* @returns the resolved blank bit.
|
|
5511
|
+
*/
|
|
5512
|
+
function resolveBlank(sessionId, row, fallback) {
|
|
5513
|
+
const resident = residentBlank(sessionId)
|
|
5514
|
+
if (resident !== undefined) return resident
|
|
5515
|
+
if (row !== undefined) return row.blank === true
|
|
5516
|
+
return fallback
|
|
5517
|
+
}
|
|
5518
|
+
|
|
5306
5519
|
/** Recompute one bot's session list and current status. */
|
|
5307
5520
|
function refreshChat(botId) {
|
|
5308
5521
|
const state = chatStates[botId]
|
|
@@ -5313,13 +5526,14 @@ window.__ModuleLoader__.load({
|
|
|
5313
5526
|
? assistantService('sessions').list.getSnapshot()
|
|
5314
5527
|
: null
|
|
5315
5528
|
const row = catalog !== null && state.sessionId !== null ? catalog.byId[state.sessionId] : undefined
|
|
5529
|
+
const blank = resolveBlank(state.sessionId, row, state.blank)
|
|
5316
5530
|
patchChat(botId, {
|
|
5317
5531
|
sessions: rows,
|
|
5318
5532
|
running: current ? current.running : false,
|
|
5319
5533
|
pending: current ? current.pending : false,
|
|
5320
5534
|
unread: current ? current.unread : false,
|
|
5321
|
-
blank
|
|
5322
|
-
fresh: state.fresh &&
|
|
5535
|
+
blank,
|
|
5536
|
+
fresh: state.fresh && blank === false ? false : state.fresh,
|
|
5323
5537
|
})
|
|
5324
5538
|
}
|
|
5325
5539
|
|
|
@@ -5433,69 +5647,307 @@ window.__ModuleLoader__.load({
|
|
|
5433
5647
|
}
|
|
5434
5648
|
}
|
|
5435
5649
|
|
|
5436
|
-
/**
|
|
5437
|
-
function
|
|
5438
|
-
|
|
5439
|
-
|
|
5650
|
+
/** Write a prompt into the team composer's draft (best effort). */
|
|
5651
|
+
function prefillTeamChat(text) {
|
|
5652
|
+
try {
|
|
5653
|
+
if (teamState.sessionId === null || text === '') return
|
|
5654
|
+
const sessions = assistantService('sessions')
|
|
5655
|
+
if (sessions === null) return
|
|
5656
|
+
const binding = sessions.binding(teamState.sessionId)
|
|
5657
|
+
const scoped = sessions.scope(teamState.sessionId)
|
|
5658
|
+
const conversation = scoped === undefined ? undefined : scoped.get('conversation')
|
|
5659
|
+
if (binding === undefined || conversation === undefined) return
|
|
5660
|
+
conversation.input.requestDraftInitialization(binding, { prompt: text, whenDraftExists: 'replace' })
|
|
5661
|
+
} catch (error) {
|
|
5662
|
+
console.warn('[dsh-erpbot] team prefill failed', error)
|
|
5663
|
+
}
|
|
5440
5664
|
}
|
|
5441
5665
|
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5666
|
+
/* =====================================================================
|
|
5667
|
+
* TEAM CHAT — the "团队模式" page embeds one root session whose Team mode
|
|
5668
|
+
* is enabled. This is the same embedding trick as a single bot's chat
|
|
5669
|
+
* (conversation.content), except the session is created in the shared team
|
|
5670
|
+
* workspace and then remote.agentTeams.setEnabled turns on the board, so
|
|
5671
|
+
* the official member tree + task board slots render inside this session.
|
|
5672
|
+
* ===================================================================== */
|
|
5673
|
+
|
|
5674
|
+
/** Team chat state, independent of the four single-bot states. */
|
|
5675
|
+
let teamState = emptyState()
|
|
5676
|
+
|
|
5677
|
+
/** Read the team session id, migrating nothing (team had no pinyin id). */
|
|
5678
|
+
function readTeamSessionId() {
|
|
5679
|
+
try {
|
|
5680
|
+
return window.localStorage.getItem('dsh-erpbot.team.session.v1') || null
|
|
5681
|
+
} catch (error) {
|
|
5682
|
+
return null
|
|
5683
|
+
}
|
|
5684
|
+
}
|
|
5685
|
+
|
|
5686
|
+
/** Persist (or clear) the team session id. */
|
|
5687
|
+
function writeTeamSessionId(id) {
|
|
5688
|
+
try {
|
|
5689
|
+
if (id) window.localStorage.setItem('dsh-erpbot.team.session.v1', id)
|
|
5690
|
+
else window.localStorage.removeItem('dsh-erpbot.team.session.v1')
|
|
5691
|
+
} catch (error) {
|
|
5692
|
+
/* storage may be unavailable */
|
|
5693
|
+
}
|
|
5694
|
+
}
|
|
5695
|
+
|
|
5696
|
+
/** Create or reuse the shared team workspace via the host route. */
|
|
5697
|
+
async function ensureTeamWorkspace() {
|
|
5698
|
+
const response = await fetch('/erp/team/workspace', {
|
|
5699
|
+
method: 'POST',
|
|
5700
|
+
headers: { 'content-type': 'application/json' },
|
|
5701
|
+
body: '{}',
|
|
5454
5702
|
})
|
|
5703
|
+
const data = await response.json().catch(() => null)
|
|
5704
|
+
if (!response.ok || data === null || data.ok !== true) {
|
|
5705
|
+
throw new Error(data !== null && data.error ? data.error.message : 'team workspace unavailable')
|
|
5706
|
+
}
|
|
5707
|
+
return { workspaceId: data.workspaceId, path: data.path || '' }
|
|
5455
5708
|
}
|
|
5456
5709
|
|
|
5457
|
-
/**
|
|
5458
|
-
function
|
|
5710
|
+
/** Ensure the team root session exists, is retained, and has Team mode on. */
|
|
5711
|
+
async function ensureTeamChat() {
|
|
5712
|
+
if (teamState.phase === 'ready' && teamState.reference !== null) return teamState.reference
|
|
5713
|
+
if (teamState.phase === 'preparing') return null
|
|
5714
|
+
if (assistantCtxMissing()) {
|
|
5715
|
+
teamState = Object.assign({}, teamState, { phase: 'unsupported', error: 'chat services unavailable' })
|
|
5716
|
+
notifyTeam()
|
|
5717
|
+
return null
|
|
5718
|
+
}
|
|
5719
|
+
teamState = Object.assign({}, teamState, { phase: 'preparing', error: null })
|
|
5720
|
+
notifyTeam()
|
|
5459
5721
|
try {
|
|
5460
|
-
const
|
|
5461
|
-
const
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5722
|
+
const workspace = await ensureTeamWorkspace()
|
|
5723
|
+
const sessions = assistantService('sessions')
|
|
5724
|
+
const stored = readTeamSessionId()
|
|
5725
|
+
let id = null
|
|
5726
|
+
if (stored !== null) {
|
|
5727
|
+
const catalog = sessions.list.getSnapshot()
|
|
5728
|
+
const row = catalog.byId[stored]
|
|
5729
|
+
if (row !== undefined && row.origin === undefined) id = stored
|
|
5730
|
+
}
|
|
5731
|
+
if (id === null) {
|
|
5732
|
+
id = await sessions.create({ workspaceId: workspace.workspaceId })
|
|
5733
|
+
writeTeamSessionId(id)
|
|
5734
|
+
}
|
|
5735
|
+
/* Retain FIRST: activating the session makes it a live root Agent,
|
|
5736
|
+
which `remote.agentTeams.setEnabled` requires (its host half calls
|
|
5737
|
+
`subagents.rootOf(agent)`, which rejects a not-yet-live session). */
|
|
5738
|
+
const reference = sessions.retain(id, { source: 'erpbot-team' })
|
|
5739
|
+
/* Now enable Team mode on this live root session so the official
|
|
5740
|
+
member tree + task board slots render. */
|
|
5741
|
+
try {
|
|
5742
|
+
if (teamRemote !== null && typeof teamRemote.setEnabled === 'function') {
|
|
5743
|
+
const response = await teamRemote.setEnabled(id, true)
|
|
5744
|
+
if (response && response.ok === false) {
|
|
5745
|
+
console.warn('[dsh-erpbot] team mode enable rejected:', response.error)
|
|
5746
|
+
}
|
|
5747
|
+
} else {
|
|
5748
|
+
console.warn('[dsh-erpbot] team remote unavailable, team mode not enabled')
|
|
5749
|
+
}
|
|
5750
|
+
} catch (error) {
|
|
5751
|
+
console.warn('[dsh-erpbot] team mode enable failed', error)
|
|
5465
5752
|
}
|
|
5753
|
+
teamState = Object.assign({}, teamState, {
|
|
5754
|
+
phase: 'ready',
|
|
5755
|
+
workspaceId: workspace.workspaceId,
|
|
5756
|
+
workspacePath: workspace.path,
|
|
5757
|
+
sessionId: id,
|
|
5758
|
+
reference,
|
|
5759
|
+
error: null,
|
|
5760
|
+
})
|
|
5761
|
+
notifyTeam()
|
|
5762
|
+
return reference
|
|
5466
5763
|
} catch (error) {
|
|
5467
|
-
|
|
5764
|
+
teamState = Object.assign({}, teamState, { phase: 'error', error: String((error && error.message) || error) })
|
|
5765
|
+
notifyTeam()
|
|
5766
|
+
return null
|
|
5468
5767
|
}
|
|
5469
5768
|
}
|
|
5470
5769
|
|
|
5471
|
-
/**
|
|
5472
|
-
function
|
|
5473
|
-
|
|
5770
|
+
/** Notify every React subscriber of the team state change. */
|
|
5771
|
+
function notifyTeam() {
|
|
5772
|
+
notifyChat()
|
|
5474
5773
|
}
|
|
5475
|
-
const BOT_FACTORY_OPTIONS = { slots: { views: BotFixedChat } }
|
|
5476
5774
|
|
|
5477
|
-
/**
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5775
|
+
/** React binding for the team chat state. */
|
|
5776
|
+
function useTeamChat() {
|
|
5777
|
+
const [snap, setSnap] = React.useState(teamState)
|
|
5778
|
+
React.useEffect(() => {
|
|
5779
|
+
setSnap(teamState)
|
|
5780
|
+
const off = subscribeChat(() => setSnap(teamState))
|
|
5781
|
+
return () => off()
|
|
5782
|
+
}, [])
|
|
5783
|
+
return snap || emptyState()
|
|
5784
|
+
}
|
|
5486
5785
|
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5786
|
+
/** Rows of the team workspace's sessions (current blank included). */
|
|
5787
|
+
function teamSessionRows() {
|
|
5788
|
+
const workspaces = assistantService('workspaces')
|
|
5789
|
+
const sessions = assistantService('sessions')
|
|
5790
|
+
if (workspaces === null || sessions === null || teamState.workspaceId === null) return []
|
|
5791
|
+
const catalog = sessions.list.getSnapshot()
|
|
5792
|
+
const workspace = (workspaces.list.getSnapshot().items || [])
|
|
5793
|
+
.find((item) => item.workspaceId === teamState.workspaceId)
|
|
5794
|
+
if (workspace === undefined) return []
|
|
5795
|
+
const uiSession = assistantService('uiSession')
|
|
5796
|
+
const statuses = uiSession !== null ? uiSession.sessionStatus.getSnapshot() : null
|
|
5797
|
+
const rows = []
|
|
5798
|
+
for (const id of workspace.sessionIds) {
|
|
5799
|
+
const row = catalog.byId[id]
|
|
5800
|
+
if (row === undefined) continue
|
|
5801
|
+
if (row.blank === true && id !== teamState.sessionId) continue
|
|
5802
|
+
const status = statuses !== null && statuses !== undefined ? statuses.get(id) : undefined
|
|
5803
|
+
rows.push({
|
|
5804
|
+
id,
|
|
5805
|
+
title: row.title || row.displayTitle || t('ai.blankChat'),
|
|
5806
|
+
at: row.updatedAt || 0,
|
|
5807
|
+
blank: row.blank === true,
|
|
5808
|
+
running: status !== undefined ? status.running === true : row.running === true,
|
|
5809
|
+
pending: status !== undefined && status.pendingInteraction !== undefined && status.pendingInteraction !== null,
|
|
5810
|
+
unread: status !== undefined && status.completionUnread === true,
|
|
5811
|
+
})
|
|
5812
|
+
}
|
|
5813
|
+
rows.sort((a, b) => b.at - a.at)
|
|
5814
|
+
return rows.slice(0, LIST_LIMIT)
|
|
5815
|
+
}
|
|
5816
|
+
|
|
5817
|
+
/** Refresh the team session's status, sessions list + current session. */
|
|
5818
|
+
function refreshTeamChat() {
|
|
5819
|
+
if (assistantCtxMissing() || teamState.workspaceId === null) return
|
|
5820
|
+
const sessions = assistantService('sessions')
|
|
5821
|
+
const rows = teamSessionRows()
|
|
5822
|
+
const current = rows.find((row) => row.id === teamState.sessionId)
|
|
5823
|
+
const catalog = sessions !== null ? sessions.list.getSnapshot() : null
|
|
5824
|
+
const row = catalog !== null && teamState.sessionId !== null ? catalog.byId[teamState.sessionId] : undefined
|
|
5825
|
+
const blank = resolveBlank(teamState.sessionId, row, teamState.blank)
|
|
5826
|
+
teamState = Object.assign({}, teamState, {
|
|
5827
|
+
sessions: rows,
|
|
5828
|
+
running: current ? current.running : false,
|
|
5829
|
+
pending: current ? current.pending : false,
|
|
5830
|
+
unread: current ? current.unread : false,
|
|
5831
|
+
blank,
|
|
5832
|
+
fresh: teamState.fresh && blank === false ? false : teamState.fresh,
|
|
5833
|
+
})
|
|
5834
|
+
notifyTeam()
|
|
5835
|
+
}
|
|
5836
|
+
|
|
5837
|
+
/** Switch the team page to another session of its workspace. */
|
|
5838
|
+
function switchTeamSession(id) {
|
|
5839
|
+
const sessions = assistantService('sessions')
|
|
5840
|
+
if (sessions === null) return
|
|
5841
|
+
try {
|
|
5842
|
+
const reference = sessions.retain(id, { source: 'erpbot-team' })
|
|
5843
|
+
const previous = teamState.reference
|
|
5844
|
+
teamState = Object.assign({}, teamState, { sessionId: id, reference, error: null, fresh: false })
|
|
5845
|
+
writeTeamSessionId(id)
|
|
5846
|
+
if (previous !== null && previous !== reference) {
|
|
5847
|
+
setTimeout(() => { try { previous.release() } catch (error) { /* released */ } }, 0)
|
|
5848
|
+
}
|
|
5849
|
+
refreshTeamChat()
|
|
5850
|
+
} catch (error) {
|
|
5851
|
+
teamState = Object.assign({}, teamState, { error: String((error && error.message) || error) })
|
|
5852
|
+
notifyTeam()
|
|
5853
|
+
}
|
|
5854
|
+
}
|
|
5855
|
+
|
|
5856
|
+
/** Create a fresh team root session, enable Team mode, switch to it. */
|
|
5857
|
+
async function newTeamSession() {
|
|
5858
|
+
if (teamState.phase !== 'ready') return
|
|
5859
|
+
const sessions = assistantService('sessions')
|
|
5860
|
+
if (sessions === null) return
|
|
5861
|
+
try {
|
|
5862
|
+
const id = await sessions.create({ workspaceId: teamState.workspaceId })
|
|
5863
|
+
/* Retain first so the session is a live root, then enable Team mode. */
|
|
5864
|
+
const previous = teamState.reference
|
|
5865
|
+
const reference = sessions.retain(id, { source: 'erpbot-team' })
|
|
5866
|
+
try {
|
|
5867
|
+
if (teamRemote !== null && typeof teamRemote.setEnabled === 'function') {
|
|
5868
|
+
const response = await teamRemote.setEnabled(id, true)
|
|
5869
|
+
if (response && response.ok === false) {
|
|
5870
|
+
console.warn('[dsh-erpbot] team mode enable rejected:', response.error)
|
|
5871
|
+
}
|
|
5872
|
+
}
|
|
5873
|
+
} catch (error) {
|
|
5874
|
+
console.warn('[dsh-erpbot] team mode enable failed', error)
|
|
5875
|
+
}
|
|
5876
|
+
teamState = Object.assign({}, teamState, { sessionId: id, reference, error: null, blank: true, fresh: true })
|
|
5877
|
+
writeTeamSessionId(id)
|
|
5878
|
+
if (previous !== null && previous !== reference) {
|
|
5879
|
+
setTimeout(() => { try { previous.release() } catch (error) { /* released */ } }, 0)
|
|
5880
|
+
}
|
|
5881
|
+
refreshTeamChat()
|
|
5882
|
+
} catch (error) {
|
|
5883
|
+
teamState = Object.assign({}, teamState, { error: String((error && error.message) || error) })
|
|
5884
|
+
notifyTeam()
|
|
5885
|
+
}
|
|
5886
|
+
}
|
|
5887
|
+
|
|
5888
|
+
/** Open / close one bot's session list. */
|
|
5889
|
+
function toggleChatList(botId) {
|
|
5890
|
+
const state = chatStates[botId] || emptyState()
|
|
5891
|
+
patchChat(botId, { listOpen: !state.listOpen })
|
|
5892
|
+
}
|
|
5893
|
+
|
|
5894
|
+
/** Scan the workspace store and restore each bot's state (no creation). */
|
|
5895
|
+
function primeChats() {
|
|
5896
|
+
const workspaces = assistantService('workspaces')
|
|
5897
|
+
if (workspaces === null) return
|
|
5898
|
+
chatBots.forEach((botId) => {
|
|
5899
|
+
const found = findWorkspace(botId)
|
|
5900
|
+
if (found === undefined) return
|
|
5901
|
+
const state = chatStates[botId] || emptyState()
|
|
5902
|
+
if (state.workspaceId !== found.workspaceId) {
|
|
5903
|
+
patchChat(botId, { workspaceId: found.workspaceId, workspacePath: found.path || '' })
|
|
5904
|
+
}
|
|
5905
|
+
refreshChat(botId)
|
|
5906
|
+
})
|
|
5907
|
+
}
|
|
5908
|
+
|
|
5909
|
+
/** Last-resort path when the embedded occurrence cannot be rendered. */
|
|
5910
|
+
function openOfficialConversation(botId) {
|
|
5911
|
+
try {
|
|
5912
|
+
const navigation = assistantService('uiWorkspace')
|
|
5913
|
+
const state = chatStates[botId] || emptyState()
|
|
5914
|
+
if (navigation !== null) {
|
|
5915
|
+
if (state.sessionId !== null) navigation.openSession(state.sessionId)
|
|
5916
|
+
else navigation.startSession(undefined, { prompt: t('ai.prompt.insights') })
|
|
5917
|
+
}
|
|
5918
|
+
} catch (error) {
|
|
5919
|
+
console.warn('[dsh-erpbot] official conversation fallback failed', error)
|
|
5920
|
+
}
|
|
5921
|
+
}
|
|
5922
|
+
|
|
5923
|
+
/** Fixed Chat view for the embedded occurrence. */
|
|
5924
|
+
function BotFixedChat(props) {
|
|
5925
|
+
return props.renderSlot('conversation.session', { view: 'chat' })
|
|
5926
|
+
}
|
|
5927
|
+
const BOT_FACTORY_OPTIONS = { slots: { views: BotFixedChat } }
|
|
5928
|
+
|
|
5929
|
+
/**
|
|
5930
|
+
* The official conversation for one bot (or its degraded state).
|
|
5931
|
+
* @param props - `{ botId, kit }`.
|
|
5932
|
+
* @returns the embedded conversation element.
|
|
5933
|
+
*/
|
|
5934
|
+
function BotChatSurface(props) {
|
|
5935
|
+
const botId = props.botId
|
|
5936
|
+
const kit = props.kit || {}
|
|
5937
|
+
const state = useChatState(botId)
|
|
5938
|
+
|
|
5939
|
+
if (state.phase === 'idle' || state.phase === 'preparing') {
|
|
5940
|
+
return h('div', { className: 'assist-note bot-note' }, t('ai.preparing'))
|
|
5941
|
+
}
|
|
5942
|
+
if (state.phase === 'error') {
|
|
5943
|
+
return h('div', { className: 'assist-note bot-note' },
|
|
5944
|
+
h('span', null, t('ai.error') + ' - ' + String(state.error || '')),
|
|
5945
|
+
h('button', {
|
|
5946
|
+
className: 'assist-link',
|
|
5947
|
+
type: 'button',
|
|
5948
|
+
onClick: () => {
|
|
5949
|
+
patchChat(botId, { phase: 'idle', error: null })
|
|
5950
|
+
ensureChat(botId)
|
|
5499
5951
|
},
|
|
5500
5952
|
}, t('ai.retry')))
|
|
5501
5953
|
}
|
|
@@ -5524,18 +5976,70 @@ window.__ModuleLoader__.load({
|
|
|
5524
5976
|
chatCtx = ctx
|
|
5525
5977
|
}
|
|
5526
5978
|
|
|
5979
|
+
/**
|
|
5980
|
+
* The team root session's embedded conversation. Identical to
|
|
5981
|
+
* BotChatSurface but bound to the team state (a shared workspace session
|
|
5982
|
+
* with Team mode on), so the official member tree + task board render.
|
|
5983
|
+
* @param props - `{ kit }`.
|
|
5984
|
+
* @returns the embedded conversation element.
|
|
5985
|
+
*/
|
|
5986
|
+
function BotTeamSurface(props) {
|
|
5987
|
+
const kit = props.kit || {}
|
|
5988
|
+
const state = useTeamChat()
|
|
5989
|
+
|
|
5990
|
+
if (state.phase === 'idle' || state.phase === 'preparing') {
|
|
5991
|
+
return h('div', { className: 'assist-note bot-note' }, t('ai.preparing'))
|
|
5992
|
+
}
|
|
5993
|
+
if (state.phase === 'error') {
|
|
5994
|
+
return h('div', { className: 'assist-note bot-note' },
|
|
5995
|
+
h('span', null, t('ai.error') + ' - ' + String(state.error || '')),
|
|
5996
|
+
h('button', {
|
|
5997
|
+
className: 'assist-link',
|
|
5998
|
+
type: 'button',
|
|
5999
|
+
onClick: () => {
|
|
6000
|
+
teamState = Object.assign({}, teamState, { phase: 'idle', error: null })
|
|
6001
|
+
notifyTeam()
|
|
6002
|
+
ensureTeamChat()
|
|
6003
|
+
},
|
|
6004
|
+
}, t('ai.retry')))
|
|
6005
|
+
}
|
|
6006
|
+
if (state.phase === 'unsupported' || state.reference === null
|
|
6007
|
+
|| typeof kit.SessionProvider !== 'function' || typeof kit.renderFactorySlot !== 'function') {
|
|
6008
|
+
return h('div', { className: 'assist-note bot-note' },
|
|
6009
|
+
h('span', null, t('ai.fallback')))
|
|
6010
|
+
}
|
|
6011
|
+
|
|
6012
|
+
return h('div', { className: 'bot-conversation team-conversation', 'data-bot': 'team' },
|
|
6013
|
+
h(kit.SessionProvider, { session: state.reference },
|
|
6014
|
+
kit.renderFactorySlot('conversation.content', {
|
|
6015
|
+
variant: 'embedded',
|
|
6016
|
+
phase: 'active',
|
|
6017
|
+
hero: false,
|
|
6018
|
+
}, BOT_FACTORY_OPTIONS)))
|
|
6019
|
+
}
|
|
6020
|
+
|
|
5527
6021
|
return {
|
|
5528
6022
|
chatTone,
|
|
5529
6023
|
useChatState,
|
|
6024
|
+
useChatBusy,
|
|
6025
|
+
readChatBusy,
|
|
5530
6026
|
ensureChat,
|
|
6027
|
+
ensureTeamChat,
|
|
6028
|
+
useTeamChat,
|
|
5531
6029
|
refreshChat,
|
|
6030
|
+
refreshTeamChat,
|
|
5532
6031
|
switchChatSession,
|
|
5533
6032
|
newChatSession,
|
|
6033
|
+
newTeamSession,
|
|
6034
|
+
switchTeamSession,
|
|
5534
6035
|
prefillChat,
|
|
6036
|
+
prefillTeamChat,
|
|
5535
6037
|
toggleChatList,
|
|
5536
6038
|
primeChats,
|
|
5537
6039
|
setAssistantCtx,
|
|
6040
|
+
setTeamRemote,
|
|
5538
6041
|
BotChatSurface,
|
|
6042
|
+
BotTeamSurface,
|
|
5539
6043
|
assistantTime,
|
|
5540
6044
|
chatSessions: (botId) => (chatStates[botId] || emptyState()).sessions,
|
|
5541
6045
|
chatState: (botId) => chatStates[botId] || emptyState(),
|
|
@@ -5548,11 +6052,16 @@ window.__ModuleLoader__.load({
|
|
|
5548
6052
|
* ===================================================================== */
|
|
5549
6053
|
|
|
5550
6054
|
function createCards(deps) {
|
|
5551
|
-
const { React, h, t, metricLabel, metricSub, PAGES, Icon, Spark, drawTrend } = deps
|
|
6055
|
+
const { React, h, t, compactMoney, readLang, metricLabel, metricSub, PAGES, Icon, Spark, drawTrend } = deps
|
|
5552
6056
|
|
|
5553
6057
|
/* Legacy deep links: the data and platform pages were merged into one
|
|
5554
|
-
base page, so `#/erp/shu` and `#/erp/guan` now land on it.
|
|
5555
|
-
|
|
6058
|
+
base page, so `#/erp/shu` and `#/erp/guan` now land on it. The four bot
|
|
6059
|
+
pages were renamed from pinyin ids to English ones, so the old hashes
|
|
6060
|
+
still resolve instead of falling back to the overview. */
|
|
6061
|
+
const HASH_ALIAS = {
|
|
6062
|
+
shu: 'base', guan: 'base',
|
|
6063
|
+
cai: 'finance', zi: 'funds', tui: 'forecast', shui: 'tax',
|
|
6064
|
+
}
|
|
5556
6065
|
|
|
5557
6066
|
/** Read the page id out of `#/erp/<id>`; unknown ids fall back to overview. */
|
|
5558
6067
|
function readHashPage() {
|
|
@@ -5848,6 +6357,12 @@ window.__ModuleLoader__.load({
|
|
|
5848
6357
|
/**
|
|
5849
6358
|
* Tiny chart drawn on the block card: bars for 账, a line for 资, a ring for
|
|
5850
6359
|
* 推. White with opacities so it reads on every card tint.
|
|
6360
|
+
*
|
|
6361
|
+
* The centre figure is formatted for the ring, not printed in full. The
|
|
6362
|
+
* ring's hole is only about half its width, so "¥12,000,000.00" at full
|
|
6363
|
+
* length was wider than the hole it sits in and spilled across both arcs.
|
|
6364
|
+
* compactMoney scales it to "¥1,200万"; the exact figure still appears in
|
|
6365
|
+
* the card's own cells row and in the detail modal, so nothing is lost.
|
|
5851
6366
|
*/
|
|
5852
6367
|
function MiniChart(props) {
|
|
5853
6368
|
const chart = props.chart || {}
|
|
@@ -5873,6 +6388,9 @@ window.__ModuleLoader__.load({
|
|
|
5873
6388
|
h('i', { style: { background: 'rgba(255,255,255,' + slice.tone + ')' } }),
|
|
5874
6389
|
h('span', null, slice.key ? t(slice.key) : ''),
|
|
5875
6390
|
h('b', null, slice.v + '%')))
|
|
6391
|
+
const centre = chart.total
|
|
6392
|
+
? (compactMoney ? compactMoney(chart.total, readLang ? readLang() : 'zh') : chart.total)
|
|
6393
|
+
: ''
|
|
5876
6394
|
return h('div', { className: 'visa-chart donut' },
|
|
5877
6395
|
h('div', { className: 'mini-donut' },
|
|
5878
6396
|
h('svg', { viewBox: '0 0 44 44', 'aria-hidden': 'true' },
|
|
@@ -5883,7 +6401,7 @@ window.__ModuleLoader__.load({
|
|
|
5883
6401
|
}),
|
|
5884
6402
|
arcs)),
|
|
5885
6403
|
h('div', { className: 'mini-donut-center' },
|
|
5886
|
-
h('b', null,
|
|
6404
|
+
h('b', null, centre),
|
|
5887
6405
|
chart.totalLabelKey ? h('span', null, t(chart.totalLabelKey)) : null)),
|
|
5888
6406
|
h('ul', { className: 'mini-legend' }, legend))
|
|
5889
6407
|
}
|
|
@@ -6026,7 +6544,7 @@ window.__ModuleLoader__.load({
|
|
|
6026
6544
|
* ===================================================================== */
|
|
6027
6545
|
|
|
6028
6546
|
function createDashboard(deps) {
|
|
6029
|
-
const { React, h, t, L, readLang, setLang, LANGS, SHORT_LOCALE, LANG_BADGE, useChatState, chatTone, drawOverview, CARDS, CARD_AUTO_MS, DONUT_ARCS, EXPENSE_LEGEND, TRANSACTIONS, nextUid, NAV, Icon, Spark, TxTile } = deps
|
|
6547
|
+
const { React, h, t, L, readLang, setLang, LANGS, SHORT_LOCALE, LANG_BADGE, useChatState, useChatBusy, chatTone, drawOverview, CARDS, CARD_AUTO_MS, DONUT_ARCS, EXPENSE_LEGEND, TRANSACTIONS, nextUid, NAV, Icon, Spark, TxTile } = deps
|
|
6030
6548
|
|
|
6031
6549
|
/* =====================================================================
|
|
6032
6550
|
* SECTIONS
|
|
@@ -6085,46 +6603,86 @@ window.__ModuleLoader__.load({
|
|
|
6085
6603
|
|
|
6086
6604
|
const RAIL_LABEL = {
|
|
6087
6605
|
overview: { zh: '总览', en: 'Overview' },
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6606
|
+
finance: { zh: '账房 · 财', en: 'The Ledger · Books' },
|
|
6607
|
+
funds: { zh: '司库 · 资', en: 'The Treasurer · Funds' },
|
|
6608
|
+
forecast: { zh: '军师 · 推', en: 'The Planner · Forecast' },
|
|
6609
|
+
tax: { zh: '税官 · 税', en: 'The Tax Officer · Tax' },
|
|
6610
|
+
team: { zh: '团队模式', en: 'Team mode' },
|
|
6092
6611
|
base: { zh: '底座 · MCP 与数据', en: 'Base · MCP and Data' },
|
|
6093
6612
|
}
|
|
6094
6613
|
|
|
6095
6614
|
/**
|
|
6096
|
-
* Rail-top brand
|
|
6097
|
-
*
|
|
6098
|
-
*
|
|
6099
|
-
*
|
|
6615
|
+
* Rail-top brand avatar: the erpBot identity lockup at the head of the
|
|
6616
|
+
* nav column. Three nested discs — a gradient ring, a board-coloured gap
|
|
6617
|
+
* and the gradient face — so the brand separates from the flat single-
|
|
6618
|
+
* layer buttons below it by STRUCTURE rather than by being a second
|
|
6619
|
+
* saturated block that competes with the active page.
|
|
6620
|
+
*
|
|
6621
|
+
* The whole thing is sized off `--erp-rail-btn`, so it stays inside the
|
|
6622
|
+
* rail's existing content width and rescales at every breakpoint without
|
|
6623
|
+
* a single extra media query. Footprint 46px against the 54px content
|
|
6624
|
+
* box: 4px of air on each side.
|
|
6625
|
+
*
|
|
6626
|
+
* The status dot aggregates the four bots into one boolean via
|
|
6627
|
+
* `useChatBusy` (amber while any bot is mid-flight, green else), so the
|
|
6628
|
+
* avatar never needs a per-bot hook loop. Decorative for assistive tech —
|
|
6629
|
+
* the button label already names the tile.
|
|
6100
6630
|
*/
|
|
6101
|
-
function
|
|
6102
|
-
|
|
6631
|
+
function BrandAvatar(props) {
|
|
6632
|
+
const busy = useChatBusy()
|
|
6633
|
+
const tone = busy ? 'busy' : 'on'
|
|
6634
|
+
const label = t('brand.name')
|
|
6635
|
+
const goHome = L(RAIL_LABEL.overview)
|
|
6636
|
+
return h('div', { className: 'rail-brand-slot' },
|
|
6637
|
+
h('button', {
|
|
6638
|
+
type: 'button',
|
|
6639
|
+
className: 'brand-av',
|
|
6640
|
+
title: label + ' · ' + goHome,
|
|
6641
|
+
'aria-label': label + ' · ' + goHome,
|
|
6642
|
+
onClick: () => props.onNav('overview'),
|
|
6643
|
+
},
|
|
6644
|
+
h('span', { className: 'brand-av-gap' },
|
|
6645
|
+
h('span', { className: 'brand-av-face', 'aria-hidden': 'true' }, 'B')),
|
|
6646
|
+
h('i', { className: 'brand-dot ' + tone, 'aria-hidden': 'true' })))
|
|
6103
6647
|
}
|
|
6104
6648
|
|
|
6105
6649
|
/**
|
|
6106
|
-
* Left rail: the
|
|
6650
|
+
* Left rail: the brand avatar at the top-left corner, the grouped page
|
|
6107
6651
|
* icons, then the bottom lockup — the erpBot wordmark with the 中 / EN
|
|
6108
6652
|
* switch directly beneath it, so the language lives at the panel's
|
|
6109
6653
|
* bottom-left corner.
|
|
6654
|
+
*
|
|
6655
|
+
* The avatar sits OUTSIDE `.rail-group` on purpose. Inside it, the brand
|
|
6656
|
+
* was the group's first item: same white card, same rhythm as the page
|
|
6657
|
+
* buttons, which made it read as "the 0th navigation item" rather than
|
|
6658
|
+
* as the product's identity. Pulling it out is what fixes the semantics;
|
|
6659
|
+
* the ring is what makes the two read as different things.
|
|
6110
6660
|
*/
|
|
6111
6661
|
function Rail(props) {
|
|
6112
6662
|
const navActive = props.page
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6663
|
+
/* `base` opens the infrastructure group, so a hairline separates it from
|
|
6664
|
+
the business pages above (总览 + 四个 Bot + 团队). Without it the last
|
|
6665
|
+
two entries read as one flat list and the difference in kind between
|
|
6666
|
+
"a working page" and "the MCP/data foundation" is invisible. */
|
|
6667
|
+
const primary = []
|
|
6668
|
+
NAV.forEach((item) => {
|
|
6669
|
+
if (item.id === 'base') {
|
|
6670
|
+
primary.push(h('span', { className: 'rail-sep', key: 'sep-base', 'aria-hidden': 'true' }))
|
|
6671
|
+
}
|
|
6672
|
+
primary.push(h('button', {
|
|
6673
|
+
key: item.id,
|
|
6674
|
+
type: 'button',
|
|
6675
|
+
className: 'rail-btn' + (navActive === item.id ? ' active' : ''),
|
|
6676
|
+
title: L(RAIL_LABEL[item.id] || { zh: item.id, en: item.id }),
|
|
6677
|
+
'aria-label': L(RAIL_LABEL[item.id] || { zh: item.id, en: item.id }),
|
|
6678
|
+
'aria-current': navActive === item.id ? 'page' : 'false',
|
|
6679
|
+
onClick: () => props.onNav(item.id),
|
|
6680
|
+
}, h(Icon, { name: item.icon })))
|
|
6681
|
+
})
|
|
6122
6682
|
|
|
6123
6683
|
return h('aside', { className: 't1-rail' },
|
|
6124
|
-
h(
|
|
6125
|
-
|
|
6126
|
-
h(RailMark)),
|
|
6127
|
-
primary),
|
|
6684
|
+
h(BrandAvatar, { onNav: props.onNav }),
|
|
6685
|
+
h('div', { className: 'rail-group' }, primary),
|
|
6128
6686
|
h('div', { className: 'rail-spacer' }),
|
|
6129
6687
|
h('div', { className: 'rail-foot' },
|
|
6130
6688
|
h('div', { className: 'rail-brand' },
|
|
@@ -6361,7 +6919,7 @@ window.__ModuleLoader__.load({
|
|
|
6361
6919
|
* ===================================================================== */
|
|
6362
6920
|
|
|
6363
6921
|
function createWorkbench(deps) {
|
|
6364
|
-
const { React, h, t, L, readLang, KpiFigure, compactMoney, metricLabel, metricSub, moneyAmount, Icon, Spark, PAGES, BOTS, useChatState, chatTone, ensureChat, refreshChat, switchChatSession, newChatSession, prefillChat, BotChatSurface } = deps
|
|
6922
|
+
const { React, h, t, L, readLang, KpiFigure, compactMoney, metricLabel, metricSub, moneyAmount, Icon, Spark, PAGES, BOTS, SKILL_DEPS, useChatState, chatTone, ensureChat, ensureTeamChat, useTeamChat, refreshChat, refreshTeamChat, switchChatSession, newChatSession, newTeamSession, switchTeamSession, prefillChat, prefillTeamChat, BotChatSurface, BotTeamSurface } = deps
|
|
6365
6923
|
|
|
6366
6924
|
/** KPI card for the nested-page shell (labels are inline localized). */
|
|
6367
6925
|
function ShellKpi(props) {
|
|
@@ -6406,7 +6964,7 @@ window.__ModuleLoader__.load({
|
|
|
6406
6964
|
}
|
|
6407
6965
|
|
|
6408
6966
|
function PaneEmpty(section) {
|
|
6409
|
-
return h('div', { className: '
|
|
6967
|
+
return h('div', { className: 'wb-empty' },
|
|
6410
6968
|
h('span', { className: 'orb orb-md', 'aria-hidden': 'true' }),
|
|
6411
6969
|
h('b', null, L(section.label)),
|
|
6412
6970
|
h('span', null, L({ zh: '该模块内容待接入', en: 'This module is not wired yet' })))
|
|
@@ -6730,6 +7288,288 @@ window.__ModuleLoader__.load({
|
|
|
6730
7288
|
]
|
|
6731
7289
|
}
|
|
6732
7290
|
|
|
7291
|
+
/* =====================================================================
|
|
7292
|
+
* BASE SHELL — 底座 · MCP 与数据(精简两栏):左侧 MCP 清单,右侧接口清单。
|
|
7293
|
+
* 数据来自 Host 桥接的 ctx.mcpManager(/erp/mcp/list + /erp/mcp/probe),
|
|
7294
|
+
* 不含任何业务数据、不含 KPI 行、不含 8 清单项——本页只回答「连了哪些
|
|
7295
|
+
* MCP、每个 MCP 有哪些接口」。
|
|
7296
|
+
* ===================================================================== */
|
|
7297
|
+
|
|
7298
|
+
/** Map a manager status state to a dot tone and a human label. */
|
|
7299
|
+
function mcpStatusTone(st) {
|
|
7300
|
+
if (!st) return 'off'
|
|
7301
|
+
if (st.state === 'connected') return 'ok'
|
|
7302
|
+
if (st.state === 'connecting' || st.state === 'reconnecting') return 'warn'
|
|
7303
|
+
return 'off'
|
|
7304
|
+
}
|
|
7305
|
+
function mcpStatusText(st) {
|
|
7306
|
+
if (!st) return ''
|
|
7307
|
+
switch (st.state) {
|
|
7308
|
+
case 'connected': return L({ zh: '已连接', en: 'Connected' })
|
|
7309
|
+
case 'connecting': return L({ zh: '连接中', en: 'Connecting' })
|
|
7310
|
+
case 'reconnecting': return L({ zh: '重连中', en: 'Reconnecting' })
|
|
7311
|
+
case 'waiting-credentials': return L({ zh: '待凭据', en: 'Waiting credentials' })
|
|
7312
|
+
case 'disabled': return L({ zh: '已停用', en: 'Disabled' })
|
|
7313
|
+
case 'failed': return L({ zh: '失败', en: 'Failed' })
|
|
7314
|
+
case 'closed': return L({ zh: '已关闭', en: 'Closed' })
|
|
7315
|
+
default: return ''
|
|
7316
|
+
}
|
|
7317
|
+
}
|
|
7318
|
+
|
|
7319
|
+
/** Base page: live MCP fleet list (left) and per-server tool inventory (right). */
|
|
7320
|
+
function BaseShell(props) {
|
|
7321
|
+
const cfg = props.cfg || {}
|
|
7322
|
+
const [servers, setServers] = React.useState([])
|
|
7323
|
+
const [activeName, setActiveName] = React.useState('')
|
|
7324
|
+
const [listError, setListError] = React.useState('')
|
|
7325
|
+
const [probe, setProbe] = React.useState(null)
|
|
7326
|
+
|
|
7327
|
+
/* Load the live MCP fleet once. */
|
|
7328
|
+
React.useEffect(() => {
|
|
7329
|
+
let alive = true
|
|
7330
|
+
hostJson('/erp/mcp/list').then((data) => {
|
|
7331
|
+
if (!alive) return
|
|
7332
|
+
if (data !== null && data.ok === true && Array.isArray(data.servers)) {
|
|
7333
|
+
setServers(data.servers)
|
|
7334
|
+
setListError('')
|
|
7335
|
+
if (data.servers.length === 0) {
|
|
7336
|
+
setActiveName('')
|
|
7337
|
+
} else if (!data.servers.some((s) => s.name === activeName)) {
|
|
7338
|
+
setActiveName(data.servers[0].name)
|
|
7339
|
+
}
|
|
7340
|
+
} else {
|
|
7341
|
+
setListError(L({ zh: '读不到 MCP 清单', en: 'MCP server list unavailable' }))
|
|
7342
|
+
}
|
|
7343
|
+
})
|
|
7344
|
+
return () => { alive = false }
|
|
7345
|
+
}, [])
|
|
7346
|
+
|
|
7347
|
+
/* Probe the selected server for its tool list. */
|
|
7348
|
+
React.useEffect(() => {
|
|
7349
|
+
if (activeName === '') { setProbe(null); return }
|
|
7350
|
+
let alive = true
|
|
7351
|
+
setProbe({ loading: true })
|
|
7352
|
+
hostJson('/erp/mcp/probe?name=' + encodeURIComponent(activeName)).then((data) => {
|
|
7353
|
+
if (!alive) return
|
|
7354
|
+
if (data !== null && data.ok === true) {
|
|
7355
|
+
setProbe({ loading: false, result: data.probe })
|
|
7356
|
+
} else {
|
|
7357
|
+
setProbe({ loading: false, error: data !== null && data.error ? data.error.message : 'probe failed' })
|
|
7358
|
+
}
|
|
7359
|
+
})
|
|
7360
|
+
return () => { alive = false }
|
|
7361
|
+
}, [activeName])
|
|
7362
|
+
|
|
7363
|
+
const active = servers.find((s) => s.name === activeName) || null
|
|
7364
|
+
const activeStatus = active ? active.status : null
|
|
7365
|
+
const toolCount = activeStatus && activeStatus.state === 'connected' ? activeStatus.toolCount : 0
|
|
7366
|
+
|
|
7367
|
+
const listItems = servers.length
|
|
7368
|
+
? servers.map((s) => {
|
|
7369
|
+
const tone = mcpStatusTone(s.status)
|
|
7370
|
+
const tc = s.status && s.status.state === 'connected' ? s.status.toolCount : null
|
|
7371
|
+
return h('button', {
|
|
7372
|
+
key: s.name,
|
|
7373
|
+
type: 'button',
|
|
7374
|
+
className: 'wb-item' + (s.name === activeName ? ' on' : ''),
|
|
7375
|
+
onClick: () => setActiveName(s.name),
|
|
7376
|
+
},
|
|
7377
|
+
h('i', { className: 'mcp-dot ' + tone, 'aria-hidden': 'true' }),
|
|
7378
|
+
h('span', null, s.name),
|
|
7379
|
+
tc !== null ? h('span', { className: 'wb-pill-sm' }, String(tc)) : null)
|
|
7380
|
+
})
|
|
7381
|
+
: null
|
|
7382
|
+
|
|
7383
|
+
/** Right pane: the selected server's tool inventory. */
|
|
7384
|
+
function toolsPane() {
|
|
7385
|
+
if (listError) {
|
|
7386
|
+
return h('div', { className: 'wb-empty' },
|
|
7387
|
+
h('span', { className: 'orb orb-md', 'aria-hidden': 'true' }),
|
|
7388
|
+
h('b', null, listError))
|
|
7389
|
+
}
|
|
7390
|
+
if (servers.length === 0) {
|
|
7391
|
+
return h('div', { className: 'wb-empty' },
|
|
7392
|
+
h('span', { className: 'orb orb-md', 'aria-hidden': 'true' }),
|
|
7393
|
+
h('b', null, L({ zh: '尚未配置 MCP', en: 'No MCP configured' })),
|
|
7394
|
+
h('span', null, L({ zh: '在 DSH 设置里添加 MCP 服务器后,这里会列出通道与接口。', en: 'Add an MCP server in DSH settings; channels and tools appear here.' })))
|
|
7395
|
+
}
|
|
7396
|
+
if (!active) return null
|
|
7397
|
+
if (probe && probe.loading) {
|
|
7398
|
+
return h('div', { className: 'wb-empty' },
|
|
7399
|
+
h('span', { className: 'orb orb-md', 'aria-hidden': 'true' }),
|
|
7400
|
+
h('b', null, L({ zh: '读取接口清单…', en: 'Reading tools…' })))
|
|
7401
|
+
}
|
|
7402
|
+
if (probe && probe.error) {
|
|
7403
|
+
return h('div', { className: 'wb-empty' },
|
|
7404
|
+
h('span', { className: 'orb orb-md', 'aria-hidden': 'true' }),
|
|
7405
|
+
h('b', null, L({ zh: '接口清单读取失败', en: 'Tool list failed' })),
|
|
7406
|
+
h('span', null, String(probe.error)))
|
|
7407
|
+
}
|
|
7408
|
+
const result = probe && probe.result ? probe.result : null
|
|
7409
|
+
const tools = result && result.ok === true ? result.toolNames : []
|
|
7410
|
+
if (tools.length === 0) {
|
|
7411
|
+
return h('div', { className: 'wb-empty' },
|
|
7412
|
+
h('span', { className: 'orb orb-md', 'aria-hidden': 'true' }),
|
|
7413
|
+
h('b', null, L({ zh: '无接口', en: 'No tools' })),
|
|
7414
|
+
h('span', null, L({ zh: '该 MCP 未声明任何工具。', en: 'This MCP server declares no tools.' })))
|
|
7415
|
+
}
|
|
7416
|
+
return h('div', { className: 'wb-list' },
|
|
7417
|
+
tools.map((tn) => h('div', { className: 'wb-row', key: tn },
|
|
7418
|
+
h('span', { className: 'wb-row-ico' }, h(Icon, { name: 'spark' })),
|
|
7419
|
+
h('div', { className: 'wb-row-main' },
|
|
7420
|
+
h('div', { className: 'wb-row-top' }, h('b', null, tn))),
|
|
7421
|
+
h('span', { className: 'wb-row-meta' }, 'mcp__' + activeName + '__' + tn))))
|
|
7422
|
+
}
|
|
7423
|
+
|
|
7424
|
+
const header = h('div', { className: 'wb-main-hd' },
|
|
7425
|
+
h('b', null, activeName || L({ zh: 'MCP', en: 'MCP' })),
|
|
7426
|
+
activeStatus && mcpStatusText(activeStatus) ? h('span', { className: 'wb-pill' }, mcpStatusText(activeStatus)) : null,
|
|
7427
|
+
toolCount > 0 ? h('span', { className: 'wb-pill' }, String(toolCount) + ' ' + L({ zh: '个接口', en: 'tools' })) : null,
|
|
7428
|
+
active && active.status && active.status.state === 'connected' && active.status.connectedAt
|
|
7429
|
+
? h('span', { className: 'wb-pill' }, L({ zh: '连接于', en: 'since' }) + ' ' + new Date(active.status.connectedAt).toLocaleString())
|
|
7430
|
+
: null)
|
|
7431
|
+
|
|
7432
|
+
/* Four KPI cards: the first ("MCP channels") reflects the live fleet;
|
|
7433
|
+
the other three keep their static finance caliber. */
|
|
7434
|
+
function kpiCards() {
|
|
7435
|
+
const base = cfg.kpis || []
|
|
7436
|
+
if (!base.length) return null
|
|
7437
|
+
const total = servers.length
|
|
7438
|
+
const connected = servers.filter((s) => s.status && s.status.state === 'connected').length
|
|
7439
|
+
const first = Object.assign({}, base[0], {
|
|
7440
|
+
value: String(total),
|
|
7441
|
+
cardSub: { zh: total + ' 条 · 已连接 ' + connected, en: total + ' channels · ' + connected + ' connected' },
|
|
7442
|
+
parts: [
|
|
7443
|
+
{ label: { zh: '已连接', en: 'Connected' }, value: String(connected) },
|
|
7444
|
+
{ label: { zh: '其它', en: 'Other' }, value: String(total - connected) },
|
|
7445
|
+
],
|
|
7446
|
+
})
|
|
7447
|
+
return [first].concat(base.slice(1)).map((k, i) => h(PageKpi, { key: i, k, onOpen: props.onOpen }))
|
|
7448
|
+
}
|
|
7449
|
+
|
|
7450
|
+
return [
|
|
7451
|
+
h('div', { className: 't1-stats', key: 'kpis' }, kpiCards()),
|
|
7452
|
+
h('div', { className: 'card workbench pg-workbench', key: 'workbench' },
|
|
7453
|
+
h('aside', { className: 'wb-nav' },
|
|
7454
|
+
h('div', { className: 'wb-hd' }, L({ zh: 'MCP 清单', en: 'MCP servers' })),
|
|
7455
|
+
listItems),
|
|
7456
|
+
h('section', { className: 'wb-main pg-pane' },
|
|
7457
|
+
header,
|
|
7458
|
+
h('div', { className: 'pg-body' }, toolsPane()))),
|
|
7459
|
+
]
|
|
7460
|
+
}
|
|
7461
|
+
|
|
7462
|
+
/* =====================================================================
|
|
7463
|
+
* TEAM SHELL — 团队模式页:一个嵌入了「开了 Team 模式」的根会话的页面。
|
|
7464
|
+
* 会话正文、成员树、任务板全部由 DSH 官方 slot 渲染(同 chart 嫁接原理),
|
|
7465
|
+
* 本组件只负责创建会话 + 开 team + 挂官方 conversation.content。
|
|
7466
|
+
* ===================================================================== */
|
|
7467
|
+
function TeamShell(props) {
|
|
7468
|
+
const team = useTeamChat()
|
|
7469
|
+
const [drawer, setDrawer] = React.useState('')
|
|
7470
|
+
|
|
7471
|
+
React.useEffect(() => {
|
|
7472
|
+
if (team.phase === 'idle' || team.phase === 'error') ensureTeamChat()
|
|
7473
|
+
}, [team.phase])
|
|
7474
|
+
|
|
7475
|
+
React.useEffect(() => {
|
|
7476
|
+
refreshTeamChat()
|
|
7477
|
+
}, [team.sessionId])
|
|
7478
|
+
|
|
7479
|
+
const tone = chatTone(team)
|
|
7480
|
+
const statusText = L(TONE_LABEL[tone] || TONE_LABEL[''])
|
|
7481
|
+
const currentSession = team.sessions.find((row) => row.id === team.sessionId)
|
|
7482
|
+
const sessionTitle = currentSession ? currentSession.title : t('team.session')
|
|
7483
|
+
const blankChat = team.phase === 'ready' && (team.blank || team.fresh)
|
|
7484
|
+
|
|
7485
|
+
/** Ask a guided team question (prefills the composer). */
|
|
7486
|
+
function askTeam(textKey) {
|
|
7487
|
+
prefillTeamChat(t(textKey))
|
|
7488
|
+
}
|
|
7489
|
+
|
|
7490
|
+
/** History pane: sessions of the team workspace. */
|
|
7491
|
+
function teamHistoryPane() {
|
|
7492
|
+
return team.sessions.length
|
|
7493
|
+
? team.sessions.map((row) => h('button', {
|
|
7494
|
+
key: row.id,
|
|
7495
|
+
type: 'button',
|
|
7496
|
+
className: 'bot-hist-row' + (row.id === team.sessionId ? ' on' : ''),
|
|
7497
|
+
onClick: () => {
|
|
7498
|
+
switchTeamSession(row.id)
|
|
7499
|
+
setDrawer('')
|
|
7500
|
+
},
|
|
7501
|
+
},
|
|
7502
|
+
h('span', { className: 'bot-hist-title' }, row.blank ? t('ai.blankChat') : row.title),
|
|
7503
|
+
h('span', { className: 'bot-hist-meta' },
|
|
7504
|
+
h('i', { className: 'bot-dot ' + (row.running ? 'live' : row.pending ? 'warn' : row.unread ? 'unread' : '') }),
|
|
7505
|
+
h('span', null, row.id === team.sessionId ? L({ zh: '当前', en: 'Current' }) : ''))))
|
|
7506
|
+
: h('div', { className: 'bot-empty' }, t('ai.noHistory'))
|
|
7507
|
+
}
|
|
7508
|
+
|
|
7509
|
+
return h('div', { className: 'card bot-card team-card' + (drawer ? ' has-drawer' : ''), key: 'team' },
|
|
7510
|
+
h('div', { className: 'bot-main team-main' },
|
|
7511
|
+
h('div', { className: 'bot-hd' },
|
|
7512
|
+
h('button', {
|
|
7513
|
+
className: 'bot-avatar team-avatar',
|
|
7514
|
+
type: 'button',
|
|
7515
|
+
title: t('team.title'),
|
|
7516
|
+
'aria-label': t('team.title'),
|
|
7517
|
+
},
|
|
7518
|
+
h(Icon, { name: 'users' }),
|
|
7519
|
+
h('i', { className: 'bot-avatar-state ' + tone, 'aria-hidden': 'true' })),
|
|
7520
|
+
h('div', { className: 'bot-meta' },
|
|
7521
|
+
h('div', { className: 'bot-name' },
|
|
7522
|
+
h('b', null, t('team.title')),
|
|
7523
|
+
h('span', { className: 'bot-role' }, t('team.role'))),
|
|
7524
|
+
h('div', { className: 'bot-sub' },
|
|
7525
|
+
h('i', { className: 'bot-dot ' + tone }),
|
|
7526
|
+
h('span', null, statusText),
|
|
7527
|
+
h('span', { className: 'bot-sep' }, '·'),
|
|
7528
|
+
h('span', { className: 'bot-session-title' }, sessionTitle))),
|
|
7529
|
+
h('div', { className: 'bot-grow' }),
|
|
7530
|
+
h('div', { className: 'bot-hd-acts' },
|
|
7531
|
+
h('button', {
|
|
7532
|
+
className: 'bot-act',
|
|
7533
|
+
type: 'button',
|
|
7534
|
+
title: t('ai.newChat'),
|
|
7535
|
+
'aria-label': t('ai.newChat'),
|
|
7536
|
+
onClick: () => newTeamSession(),
|
|
7537
|
+
}, h(Icon, { name: 'plus' })),
|
|
7538
|
+
h('button', {
|
|
7539
|
+
className: 'bot-act' + (drawer === 'history' ? ' on' : ''),
|
|
7540
|
+
type: 'button',
|
|
7541
|
+
title: t('ai.history'),
|
|
7542
|
+
'aria-label': t('ai.history'),
|
|
7543
|
+
onClick: () => setDrawer(drawer === 'history' ? '' : 'history'),
|
|
7544
|
+
}, h(Icon, { name: 'list' })))),
|
|
7545
|
+
h('div', { className: 'bot-body team-body' + (blankChat ? ' is-blank' : '') },
|
|
7546
|
+
/* The official surface is ALWAYS mounted, exactly as on the Bot
|
|
7547
|
+
page: its composer is the only input, so unmounting it in a
|
|
7548
|
+
blank session left the team page with no place to type. The
|
|
7549
|
+
welcome block is an overlay on the empty message area, not a
|
|
7550
|
+
replacement for the surface. */
|
|
7551
|
+
h(BotTeamSurface, { kit: props.kit }),
|
|
7552
|
+
blankChat
|
|
7553
|
+
? h('div', { className: 'bot-welcome' },
|
|
7554
|
+
h('span', { className: 'orb orb-md bot-orb' }),
|
|
7555
|
+
h('b', null, t('team.title')),
|
|
7556
|
+
h('span', { className: 'bot-welcome-sub' }, t('team.greeting')),
|
|
7557
|
+
h('div', { className: 'bot-chips' },
|
|
7558
|
+
[t('team.chip.close'), t('team.chip.recon'), t('team.chip.tax')].map((label) =>
|
|
7559
|
+
h('button', {
|
|
7560
|
+
key: label,
|
|
7561
|
+
type: 'button',
|
|
7562
|
+
className: 'bot-chip',
|
|
7563
|
+
onClick: () => askTeam(
|
|
7564
|
+
label === t('team.chip.close') ? 'team.chip.close'
|
|
7565
|
+
: label === t('team.chip.recon') ? 'team.chip.recon' : 'team.chip.tax'),
|
|
7566
|
+
}, label))))
|
|
7567
|
+
: null)),
|
|
7568
|
+
h('aside', { className: 'bot-drawer history' + (drawer === 'history' ? ' on' : '') },
|
|
7569
|
+
drawerHead(L({ zh: '团队会话', en: 'Team conversations' }), t('team.title'), () => setDrawer('')),
|
|
7570
|
+
h('div', { className: 'bot-dr-bd' }, teamHistoryPane())))
|
|
7571
|
+
}
|
|
7572
|
+
|
|
6733
7573
|
/* =====================================================================
|
|
6734
7574
|
* BOT SHELL — 对话主导的 Bot 页:KPI 行 + Chat 卡 + 头像配置抽屉 + 画布。
|
|
6735
7575
|
* 会话由 assistant 板块(DSH 官方会话嵌入)驱动;本板块只做呈现与配置。
|
|
@@ -6748,10 +7588,28 @@ window.__ModuleLoader__.load({
|
|
|
6748
7588
|
return 'dsh-erpbot.bot.config.v1.' + botId
|
|
6749
7589
|
}
|
|
6750
7590
|
|
|
7591
|
+
/** Bot ids used before the pinyin → English rename, for storage migration. */
|
|
7592
|
+
const LEGACY_BOT_IDS = { finance: 'cai', funds: 'zi', forecast: 'tui', tax: 'shui' }
|
|
7593
|
+
|
|
7594
|
+
/** Carry one legacy localStorage value to its renamed key, once. */
|
|
7595
|
+
function migrateKey(prefix, botId) {
|
|
7596
|
+
const legacy = LEGACY_BOT_IDS[botId]
|
|
7597
|
+
if (!legacy) return null
|
|
7598
|
+
try {
|
|
7599
|
+
const old = window.localStorage.getItem(prefix + legacy)
|
|
7600
|
+
if (!old) return null
|
|
7601
|
+
window.localStorage.setItem(prefix + botId, old)
|
|
7602
|
+
window.localStorage.removeItem(prefix + legacy)
|
|
7603
|
+
return old
|
|
7604
|
+
} catch (error) {
|
|
7605
|
+
return null
|
|
7606
|
+
}
|
|
7607
|
+
}
|
|
7608
|
+
|
|
6751
7609
|
/** Read the local mirror of one bot's config: added DSH skill names. */
|
|
6752
7610
|
function readBotConfig(botId) {
|
|
6753
7611
|
try {
|
|
6754
|
-
const raw = window.localStorage.getItem(botConfigKey(botId))
|
|
7612
|
+
const raw = window.localStorage.getItem(botConfigKey(botId)) || migrateKey('dsh-erpbot.bot.config.v1.', botId)
|
|
6755
7613
|
if (!raw) return { bot: botId, skills: [], updatedAt: null }
|
|
6756
7614
|
const parsed = JSON.parse(raw)
|
|
6757
7615
|
return {
|
|
@@ -6778,51 +7636,166 @@ window.__ModuleLoader__.load({
|
|
|
6778
7636
|
* Build the AGENTS.md written into the bot workspace. It carries the bot
|
|
6779
7637
|
* identity, its scene prompt and the DSH skills the user added, so every
|
|
6780
7638
|
* conversation in that workspace starts from the same context.
|
|
7639
|
+
*
|
|
7640
|
+
* Schema v3 (erp.bot.agents.v3) — 9 sections per bot-agents-spec.md.
|
|
7641
|
+
*
|
|
6781
7642
|
* @param meta - bot identity from bot-data.
|
|
6782
7643
|
* @param added - DSH skill names added to this bot.
|
|
6783
7644
|
* @param prefs - user preferences (reply tone, period).
|
|
6784
7645
|
* @returns markdown text.
|
|
6785
7646
|
*/
|
|
7647
|
+
|
|
7648
|
+
/* Cross-boundary routing table — one bot forwards a topic to the other
|
|
7649
|
+
three. Single source of truth for both the generated AGENTS.md section
|
|
7650
|
+
6 and the config drawer's "越界转介" block. */
|
|
7651
|
+
const TONE_LABELS = {
|
|
7652
|
+
rigorous: { zh: '严谨 · 审计', en: 'Rigorous · audit' },
|
|
7653
|
+
concise: { zh: '简洁 · 结论先行', en: 'Concise · conclusion first' },
|
|
7654
|
+
warm: { zh: '亲和 · 解释型', en: 'Warm · explanatory' },
|
|
7655
|
+
}
|
|
7656
|
+
|
|
7657
|
+
/**
|
|
7658
|
+
* Shared drawer chrome, used by three drawers (bot config, bot history,
|
|
7659
|
+
* team history) that used to spell out the same header by hand.
|
|
7660
|
+
*
|
|
7661
|
+
* It lives at FACTORY scope, not inside a component: it is called from
|
|
7662
|
+
* both BotShell and TeamShell, and when it was defined inside BotShell
|
|
7663
|
+
* only, TeamShell threw "drawerHead is not defined" at render time — a
|
|
7664
|
+
* blank panel that every static check happily passed. `onClose` is passed
|
|
7665
|
+
* in because each shell owns its own drawer state.
|
|
7666
|
+
*/
|
|
7667
|
+
function drawerHead(title, sub, onClose) {
|
|
7668
|
+
return h('div', { className: 'bot-dr-hd' },
|
|
7669
|
+
h('b', null, title),
|
|
7670
|
+
h('span', { className: 'bot-dr-sub' }, sub),
|
|
7671
|
+
h('button', {
|
|
7672
|
+
className: 'bot-x',
|
|
7673
|
+
type: 'button',
|
|
7674
|
+
title: L({ zh: '收起', en: 'Collapse' }),
|
|
7675
|
+
'aria-label': L({ zh: '收起', en: 'Collapse' }),
|
|
7676
|
+
onClick: onClose,
|
|
7677
|
+
}, h(Icon, { name: 'close' })))
|
|
7678
|
+
}
|
|
7679
|
+
|
|
7680
|
+
const ROUTING = {
|
|
7681
|
+
finance: [
|
|
7682
|
+
{ zh: '钱够不够、资金缺口 → 司库(funds)', en: 'Cash position, funding gaps → Treasurer (funds)' },
|
|
7683
|
+
{ zh: '预算与预测 → 军师(forecast)', en: 'Budget and forecast → Planner (forecast)' },
|
|
7684
|
+
{ zh: '税务与申报 → 税官(tax)', en: 'Tax and filing → Tax Officer (tax)' },
|
|
7685
|
+
],
|
|
7686
|
+
funds: [
|
|
7687
|
+
{ zh: '账务处理与凭证 → 账房(finance)', en: 'Bookkeeping and vouchers → Ledger (finance)' },
|
|
7688
|
+
{ zh: '资金计划与预测 → 军师(forecast)', en: 'Cash plan and forecast → Planner (forecast)' },
|
|
7689
|
+
{ zh: '涉税事项 → 税官(tax)', en: 'Tax matters → Tax Officer (tax)' },
|
|
7690
|
+
],
|
|
7691
|
+
forecast: [
|
|
7692
|
+
{ zh: '已发生事实与账务 → 账房(finance)', en: 'Actuals and bookkeeping → Ledger (finance)' },
|
|
7693
|
+
{ zh: '当前资金与收付 → 司库(funds)', en: 'Current cash and payments → Treasurer (funds)' },
|
|
7694
|
+
{ zh: '税务申报 → 税官(tax)', en: 'Tax filing → Tax Officer (tax)' },
|
|
7695
|
+
],
|
|
7696
|
+
tax: [
|
|
7697
|
+
{ zh: '账务处理与凭证 → 账房(finance)', en: 'Bookkeeping and vouchers → Ledger (finance)' },
|
|
7698
|
+
{ zh: '资金与收付 → 司库(funds)', en: 'Cash and payments → Treasurer (funds)' },
|
|
7699
|
+
{ zh: '预算与预测 → 军师(forecast)', en: 'Budget and forecast → Planner (forecast)' },
|
|
7700
|
+
],
|
|
7701
|
+
}
|
|
7702
|
+
|
|
6786
7703
|
function buildAgentsDoc(meta, added, prefs) {
|
|
6787
|
-
const prefsBody = [
|
|
6788
|
-
'- 回复语气:' + (prefs.replyTone || 'rigorous'),
|
|
6789
|
-
'- 默认时间口径:' + (prefs.periodWindow || '(按问题判断)'),
|
|
6790
|
-
].join('\n')
|
|
6791
|
-
const rulesBody = [
|
|
6792
|
-
'- 只回答本领域问题,越界时指向对应 Bot。',
|
|
6793
|
-
'- 数字必须能回答口径、时点与数据集;不引入本文件之外的新数。',
|
|
6794
|
-
'- 不直写 ERP;受控写经 MCP 并留回执。',
|
|
6795
|
-
].join('\n')
|
|
6796
7704
|
const tick = String.fromCharCode(96)
|
|
7705
|
+
const h2 = '##'
|
|
7706
|
+
|
|
7707
|
+
/* 8. User preferences — map raw ids to human-readable labels. */
|
|
7708
|
+
const toneValue = TONE_LABELS[prefs.replyTone] || { zh: prefs.replyTone || '严谨 · 审计', en: prefs.replyTone || 'Rigorous · audit' }
|
|
7709
|
+
const periodValue = prefs.periodWindow || '(按问题判断)'
|
|
7710
|
+
|
|
7711
|
+
/* Cross-boundary routing lives at the factory scope (see ROUTING above),
|
|
7712
|
+
shared by both the generated file and the config drawer. */
|
|
7713
|
+
const botRoute = ROUTING[meta.id] || []
|
|
7714
|
+
const routeLines = botRoute.map(function(r) { return '- ' + r.zh }).join('\n')
|
|
7715
|
+
const routeLinesEn = botRoute.map(function(r) { return '- ' + r.en }).join('\n')
|
|
7716
|
+
|
|
7717
|
+
/* Skill list. */
|
|
6797
7718
|
const skillLines = added.length
|
|
6798
|
-
? added.map((name)
|
|
7719
|
+
? added.map(function(name) { return '- ' + tick + name + tick }).join('\n')
|
|
6799
7720
|
: '(暂未添加技能。用户可在 Bot 配置 > 技能里从 DSH 技能库添加。)'
|
|
7721
|
+
|
|
6800
7722
|
return [
|
|
6801
|
-
'
|
|
7723
|
+
'<!-- schema: erp.bot.agents.v3 -->',
|
|
7724
|
+
'',
|
|
7725
|
+
/* Everything between these two markers is rewritten on save. Anything
|
|
7726
|
+
the user adds OUTSIDE them survives — that is the whole point of
|
|
7727
|
+
the managed region (see the parser block above). */
|
|
7728
|
+
'<!-- BEGIN managed:erpbot v3 -->',
|
|
7729
|
+
'',
|
|
7730
|
+
'# erpBot ' + String.fromCharCode(183) + ' ' + (meta.name ? meta.name.zh : '') + '(' + (meta.id || '') + ')',
|
|
7731
|
+
'',
|
|
7732
|
+
'> ' + (meta.role ? meta.role.zh : '') + ' | ' + (meta.role ? meta.role.en : ''),
|
|
6802
7733
|
'',
|
|
6803
|
-
'
|
|
6804
|
-
'- 角色:' + (meta.role ? meta.role.zh : ''),
|
|
6805
|
-
'- 职责:' + (meta.duties ? meta.duties.zh : ''),
|
|
6806
|
-
'- 语气:' + (meta.tone ? meta.tone.zh : ''),
|
|
6807
|
-
'- 边界:' + (meta.boundaries ? meta.boundaries.zh : ''),
|
|
7734
|
+
h2 + ' 1. 身份(Identity)',
|
|
6808
7735
|
'',
|
|
6809
|
-
'
|
|
7736
|
+
'- 角色(Role):' + (meta.role ? meta.role.zh : ''),
|
|
7737
|
+
'- 职责(Duties):' + (meta.duties ? meta.duties.zh : ''),
|
|
7738
|
+
'- 语气(Tone):' + (meta.tone ? meta.tone.zh : ''),
|
|
7739
|
+
'- 边界(Boundaries):' + (meta.boundaries ? meta.boundaries.zh : ''),
|
|
7740
|
+
meta.notApplicable && meta.notApplicable.zh
|
|
7741
|
+
? '- 不适用(Not applicable):' + meta.notApplicable.zh + '(越界请转介,不要硬答)'
|
|
7742
|
+
: '',
|
|
7743
|
+
'',
|
|
7744
|
+
h2 + ' 2. 回答顺序(Scene prompt)',
|
|
6810
7745
|
'',
|
|
6811
7746
|
meta.scene ? meta.scene.zh : '',
|
|
6812
7747
|
'',
|
|
6813
|
-
'
|
|
7748
|
+
h2 + ' 3. 数字可信(Digital trust — 硬要求)',
|
|
7749
|
+
'',
|
|
7750
|
+
'- 口径(Definition):每个数字必须能说出计算定义与生效版本。',
|
|
7751
|
+
'- 时点(Timestamp):业务发生时点、数据观测时点、财务确认时点分别说明。版本指认用「口径版本 + 生效时点」,不得用「当前」当版本指认。',
|
|
7752
|
+
'- 数据集(Dataset):说明数字来自哪份只读数据集。页面与 Bot 都不直连 ERP,不在前端重算业务数。',
|
|
7753
|
+
'- 证据(Evidence):结论必须能给出凭据 / 回执编号,或明确写「无依据」。无依据即不下结论。',
|
|
7754
|
+
'',
|
|
7755
|
+
h2 + ' 4. 红线(Red lines)',
|
|
7756
|
+
'',
|
|
7757
|
+
'**不做(结构上无通道)**:不直写 ERP、不代为审批 / 签批 / 申报、不勾选确认、不改口径、不覆盖历史版本。',
|
|
7758
|
+
'**只能走通道**:反写仅经受控写通道并留回执;调账由待办发起;改口径走版本变更流程。',
|
|
7759
|
+
'违反以上任一条时:拒绝执行 + 说明该走哪条通道 + 指出谁能批。',
|
|
7760
|
+
'',
|
|
7761
|
+
h2 + ' 5. 不确定怎么办(Suspension discipline)',
|
|
7762
|
+
'',
|
|
7763
|
+
'判不准 / 版本指认失败 / 口径冲突 / 同一动作重复到达但载荷不同 → 一律挂起转人工裁定:',
|
|
7764
|
+
'- 写明挂起点、缺什么、谁能定。',
|
|
7765
|
+
'- 不猜测、不用默认值填空、不静默重试。',
|
|
7766
|
+
'',
|
|
7767
|
+
h2 + ' 6. 越界转介(Cross-boundary routing)',
|
|
7768
|
+
'',
|
|
7769
|
+
routeLines || '(无转介配置)',
|
|
7770
|
+
'',
|
|
7771
|
+
h2 + ' 7. 已绑定 DSH 技能(Bound skills — 优先检索)',
|
|
6814
7772
|
'',
|
|
6815
7773
|
skillLines,
|
|
6816
7774
|
'',
|
|
6817
|
-
'
|
|
7775
|
+
'规则:',
|
|
7776
|
+
'1. 回答前,先判断任务是否命中以上技能;命中则加载对应 skill 并按其 SKILL.md 执行。',
|
|
7777
|
+
'2. 未命中时,才回退到本会话全局 skill 目录(<available_skills>)检索。',
|
|
7778
|
+
'3. 不得自行发明技能名;加载技能不改变身份与口径。',
|
|
6818
7779
|
'',
|
|
6819
|
-
'
|
|
7780
|
+
h2 + ' 8. 用户偏好(Preferences)',
|
|
6820
7781
|
'',
|
|
6821
|
-
|
|
7782
|
+
'- 回复语气:' + toneValue.zh,
|
|
7783
|
+
'- 默认时间口径:' + periodValue,
|
|
6822
7784
|
'',
|
|
6823
|
-
'
|
|
7785
|
+
h2 + ' 9. 交付前自检(Self-check before delivery)',
|
|
6824
7786
|
'',
|
|
6825
|
-
|
|
7787
|
+
'- [ ] 每个数字都说了口径与时点?',
|
|
7788
|
+
'- [ ] 每个结论都有依据或明确写了「无依据」?',
|
|
7789
|
+
'- [ ] 有没有越过红线、有没有替人批?',
|
|
7790
|
+
'- [ ] 拿不准的是否已挂起而不是猜?',
|
|
7791
|
+
'- [ ] 是否越界到别的 Bot 的领域?',
|
|
7792
|
+
'',
|
|
7793
|
+
'---',
|
|
7794
|
+
'',
|
|
7795
|
+
'> 本文件由 dsh-erpbot 自动生成(schema: erp.bot.agents.v3)。托管区由面板重写,标记外的内容保留。',
|
|
7796
|
+
'> 中文为主,字段有对应英文。',
|
|
7797
|
+
'',
|
|
7798
|
+
'<!-- END managed:erpbot v3 -->',
|
|
6826
7799
|
'',
|
|
6827
7800
|
].join('\n')
|
|
6828
7801
|
}
|
|
@@ -6853,6 +7826,289 @@ window.__ModuleLoader__.load({
|
|
|
6853
7826
|
return { ok: true, config: data.config, agentsPath: data.agentsPath }
|
|
6854
7827
|
}
|
|
6855
7828
|
|
|
7829
|
+
/* =====================================================================
|
|
7830
|
+
* AGENTS.md PARSER — the config drawer reads the FILE, not bot-data.
|
|
7831
|
+
*
|
|
7832
|
+
* Why this exists: every field the drawer used to show came from the
|
|
7833
|
+
* static BOTS constant, so a bot whose AGENTS.md had been edited still
|
|
7834
|
+
* displayed its factory definition. The host already returns the file
|
|
7835
|
+
* text (`data.agents`); the client only used it to decide whether to
|
|
7836
|
+
* migrate old schemas, never to render. That made the panel a spec sheet
|
|
7837
|
+
* instead of a résumé of the bot that actually exists.
|
|
7838
|
+
*
|
|
7839
|
+
* Managed region: content between
|
|
7840
|
+
* <!-- BEGIN managed:erpbot vN --> ... <!-- END managed:erpbot vN -->
|
|
7841
|
+
* is rewritten on save. Everything outside is the user's own text and is
|
|
7842
|
+
* preserved verbatim — that is what makes hand-editing safe.
|
|
7843
|
+
* ===================================================================== */
|
|
7844
|
+
|
|
7845
|
+
const MANAGED_BEGIN = /<!--\s*BEGIN managed:[^>]*-->/i
|
|
7846
|
+
const MANAGED_END = /<!--\s*END managed:[^>]*-->/i
|
|
7847
|
+
|
|
7848
|
+
/** Split a file into pre / begin / managed / end / post. */
|
|
7849
|
+
function splitManagedDoc(text) {
|
|
7850
|
+
const src = typeof text === 'string' ? text : ''
|
|
7851
|
+
const b = MANAGED_BEGIN.exec(src)
|
|
7852
|
+
const e = MANAGED_END.exec(src)
|
|
7853
|
+
if (b === null || e === null || e.index <= b.index) {
|
|
7854
|
+
/* No markers yet (legacy file): treat the whole thing as managed. */
|
|
7855
|
+
return { pre: '', begin: '', managed: src, end: '', post: '', marked: false }
|
|
7856
|
+
}
|
|
7857
|
+
return {
|
|
7858
|
+
pre: src.slice(0, b.index),
|
|
7859
|
+
begin: b[0],
|
|
7860
|
+
managed: src.slice(b.index + b[0].length, e.index),
|
|
7861
|
+
end: e[0],
|
|
7862
|
+
post: src.slice(e.index + e[0].length),
|
|
7863
|
+
marked: true,
|
|
7864
|
+
}
|
|
7865
|
+
}
|
|
7866
|
+
|
|
7867
|
+
/** Parse `## N. Title` sections out of the managed region. */
|
|
7868
|
+
function parseAgentsSections(managed) {
|
|
7869
|
+
const out = []
|
|
7870
|
+
let cur = null
|
|
7871
|
+
for (const line of String(managed).split('\n')) {
|
|
7872
|
+
const m = /^##\s+(\d+)\.\s*(.+?)\s*$/.exec(line)
|
|
7873
|
+
if (m !== null) {
|
|
7874
|
+
cur = { num: Number(m[1]), title: m[2], lines: [] }
|
|
7875
|
+
out.push(cur)
|
|
7876
|
+
continue
|
|
7877
|
+
}
|
|
7878
|
+
if (cur !== null) cur.lines.push(line)
|
|
7879
|
+
}
|
|
7880
|
+
for (const s of out) s.text = s.lines.join('\n').replace(/^\n+|\s+$/g, '')
|
|
7881
|
+
return out
|
|
7882
|
+
}
|
|
7883
|
+
|
|
7884
|
+
/**
|
|
7885
|
+
* Parse one `- 键(Key):值` line out of the identity section.
|
|
7886
|
+
* The section is bilingual (中文 label + Latin key in parens); accept both.
|
|
7887
|
+
*/
|
|
7888
|
+
function parseIdentityFields(section) {
|
|
7889
|
+
const map = {}
|
|
7890
|
+
if (!section) return map
|
|
7891
|
+
for (const raw of section.text.split('\n')) {
|
|
7892
|
+
const line = raw.trim()
|
|
7893
|
+
const m = /^-\s*([^((]+?)(?:[((][^))]*[))])?\s*[::]\s*(.*)$/.exec(line)
|
|
7894
|
+
if (m !== null) map[m[1].trim()] = m[2].trim()
|
|
7895
|
+
}
|
|
7896
|
+
return map
|
|
7897
|
+
}
|
|
7898
|
+
|
|
7899
|
+
/** `- \`skill-name\`` lines under section 7. */
|
|
7900
|
+
function parseSkillLines(section) {
|
|
7901
|
+
const out = []
|
|
7902
|
+
if (!section) return out
|
|
7903
|
+
for (const raw of section.text.split('\n')) {
|
|
7904
|
+
const m = /^-\s*`([^`]+)`\s*$/.exec(raw.trim())
|
|
7905
|
+
if (m !== null) out.push(m[1])
|
|
7906
|
+
}
|
|
7907
|
+
return out
|
|
7908
|
+
}
|
|
7909
|
+
|
|
7910
|
+
/** Bullet count in a section (section 6 routing, etc). */
|
|
7911
|
+
function countBullets(section) {
|
|
7912
|
+
if (!section) return 0
|
|
7913
|
+
return section.text.split('\n').filter(function (l) {
|
|
7914
|
+
return /^\s*-\s+\S/.test(l)
|
|
7915
|
+
}).length
|
|
7916
|
+
}
|
|
7917
|
+
|
|
7918
|
+
/** The "不做(…)" list under section 4, split on the enumeration comma. */
|
|
7919
|
+
function parseRedLines(section) {
|
|
7920
|
+
if (!section) return []
|
|
7921
|
+
const m = /\*\*不做[^*]*\*\*\s*[::]\s*([^\n]+)/.exec(section.text)
|
|
7922
|
+
if (m === null) return []
|
|
7923
|
+
return m[1].split('、').map(function (x) { return x.trim() }).filter(function (x) { return x !== '' })
|
|
7924
|
+
}
|
|
7925
|
+
|
|
7926
|
+
/** `- 回复语气:…` / `- 默认时间口径:…` under section 8. */
|
|
7927
|
+
function parsePreferences(section) {
|
|
7928
|
+
const out = { replyTone: '', periodWindow: '' }
|
|
7929
|
+
if (!section) return out
|
|
7930
|
+
for (const raw of section.text.split('\n')) {
|
|
7931
|
+
const line = raw.trim()
|
|
7932
|
+
const t = /^-\s*回复语气\s*[::]\s*(.*)$/.exec(line)
|
|
7933
|
+
if (t !== null) out.replyTone = t[1].trim()
|
|
7934
|
+
const p = /^-\s*默认时间口径\s*[::]\s*(.*)$/.exec(line)
|
|
7935
|
+
if (p !== null) out.periodWindow = p[1].trim()
|
|
7936
|
+
}
|
|
7937
|
+
return out
|
|
7938
|
+
}
|
|
7939
|
+
|
|
7940
|
+
/**
|
|
7941
|
+
* Whole-file parse into the shape the drawer renders.
|
|
7942
|
+
*
|
|
7943
|
+
* Coverage metrics are deliberately STRUCTURAL — bullet counts and
|
|
7944
|
+
* present/absent flags — never punctuation-sliced prose. An earlier
|
|
7945
|
+
* prototype counted duties by splitting the Chinese text on 「;」, so
|
|
7946
|
+
* merely rewording a field moved the number; a résumé metric that jumps
|
|
7947
|
+
* when the user rephrases a sentence is worse than no metric.
|
|
7948
|
+
*/
|
|
7949
|
+
function parseAgentsDoc(text) {
|
|
7950
|
+
const doc = splitManagedDoc(text)
|
|
7951
|
+
const sections = parseAgentsSections(doc.managed)
|
|
7952
|
+
const by = {}
|
|
7953
|
+
for (const s of sections) by[s.num] = s
|
|
7954
|
+
const identity = parseIdentityFields(by[1])
|
|
7955
|
+
const skills = parseSkillLines(by[7])
|
|
7956
|
+
const redLines = parseRedLines(by[4])
|
|
7957
|
+
const prefs = parsePreferences(by[8])
|
|
7958
|
+
const notApplicable = (identity['不适用'] || '').split(';')
|
|
7959
|
+
.map(function (x) { return x.trim() }).filter(function (x) { return x !== '' })
|
|
7960
|
+
const hasScene = !!(by[2] && by[2].text.trim())
|
|
7961
|
+
return {
|
|
7962
|
+
doc: doc,
|
|
7963
|
+
sections: sections,
|
|
7964
|
+
by: by,
|
|
7965
|
+
identity: identity,
|
|
7966
|
+
skills: skills,
|
|
7967
|
+
redLines: redLines,
|
|
7968
|
+
prefs: prefs,
|
|
7969
|
+
notApplicable: notApplicable,
|
|
7970
|
+
scene: hasScene ? (by[2].text || '') : '',
|
|
7971
|
+
/* Section 3 / 5 / 9 are the credibility trio. */
|
|
7972
|
+
trust: by[3] ? (by[3].text || '') : '',
|
|
7973
|
+
suspension: by[5] ? (by[5].text || '') : '',
|
|
7974
|
+
selfCheck: by[9] ? (by[9].text || '') : '',
|
|
7975
|
+
coverage: {
|
|
7976
|
+
skills: skills.length,
|
|
7977
|
+
routes: countBullets(by[6]),
|
|
7978
|
+
redLines: redLines.length,
|
|
7979
|
+
scene: hasScene ? 1 : 0,
|
|
7980
|
+
notApplicable: notApplicable.length,
|
|
7981
|
+
},
|
|
7982
|
+
}
|
|
7983
|
+
}
|
|
7984
|
+
|
|
7985
|
+
/** Rewrite one `## N.` section body inside the managed region. */
|
|
7986
|
+
function replaceAgentsSection(text, num, newBody) {
|
|
7987
|
+
const doc = splitManagedDoc(text)
|
|
7988
|
+
const lines = doc.managed.split('\n')
|
|
7989
|
+
let start = -1
|
|
7990
|
+
let end = lines.length
|
|
7991
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
7992
|
+
const m = /^##\s+(\d+)\.\s/.exec(lines[i])
|
|
7993
|
+
if (m === null) continue
|
|
7994
|
+
if (start === -1 && Number(m[1]) === num) { start = i; continue }
|
|
7995
|
+
if (start !== -1) { end = i; break }
|
|
7996
|
+
}
|
|
7997
|
+
if (start === -1) return text
|
|
7998
|
+
const tail = lines.slice(end)
|
|
7999
|
+
while (tail.length > 0 && tail[0].trim() === '') tail.shift()
|
|
8000
|
+
const rebuilt = lines.slice(0, start + 1)
|
|
8001
|
+
.concat([''], newBody.split('\n'), [''])
|
|
8002
|
+
.concat(tail)
|
|
8003
|
+
return doc.pre + doc.begin + rebuilt.join('\n') + doc.end + doc.post
|
|
8004
|
+
}
|
|
8005
|
+
|
|
8006
|
+
/**
|
|
8007
|
+
* Replace one `- 标签(Key):值` line inside section 1.
|
|
8008
|
+
* Returns null when the line is absent — the caller reports that instead
|
|
8009
|
+
* of silently regenerating the file (which would drop other edits).
|
|
8010
|
+
*/
|
|
8011
|
+
function replaceIdentityLine(text, label, value) {
|
|
8012
|
+
const doc = splitManagedDoc(text)
|
|
8013
|
+
const sec = parseAgentsSections(doc.managed).filter(function (s) { return s.num === 1 })[0]
|
|
8014
|
+
if (!sec) return null
|
|
8015
|
+
const re = new RegExp('^(-\\s*' + label + '(?:[((][^))]*[))])?\\s*[::]\\s*)(.*)$', 'm')
|
|
8016
|
+
if (!re.test(sec.text)) return null
|
|
8017
|
+
const body = sec.text.replace(re, function (_all, head) { return head + value })
|
|
8018
|
+
return replaceAgentsSection(text, 1, body)
|
|
8019
|
+
}
|
|
8020
|
+
|
|
8021
|
+
/**
|
|
8022
|
+
* Append one entry to the section 4 「不做」 list.
|
|
8023
|
+
* Append-only by design: existing entries are never removed, because the
|
|
8024
|
+
* red lines are the reason the bot is trustworthy — a boundary the user
|
|
8025
|
+
* can quietly widen is no boundary at all at audit time.
|
|
8026
|
+
*/
|
|
8027
|
+
function appendRedLineToDoc(text, item) {
|
|
8028
|
+
const doc = splitManagedDoc(text)
|
|
8029
|
+
const sec = parseAgentsSections(doc.managed).filter(function (s) { return s.num === 4 })[0]
|
|
8030
|
+
if (!sec) return null
|
|
8031
|
+
const existing = parseRedLines(sec)
|
|
8032
|
+
if (existing.indexOf(item) >= 0) return text
|
|
8033
|
+
const re = /(\*\*不做[^*]*\*\*\s*[::]\s*)([^\n]+)/
|
|
8034
|
+
if (!re.test(sec.text)) return null
|
|
8035
|
+
const body = sec.text.replace(re, function (_all, head, list) {
|
|
8036
|
+
return head + list.replace(/\s+$/, '') + '、' + item
|
|
8037
|
+
})
|
|
8038
|
+
return replaceAgentsSection(text, 4, body)
|
|
8039
|
+
}
|
|
8040
|
+
|
|
8041
|
+
/* ---------------------------------------------------------------------
|
|
8042
|
+
* Section 7 / 8 bodies — the ONLY two sections the skill list and the
|
|
8043
|
+
* preference fields own. Written here once so the patch path and the
|
|
8044
|
+
* full generator cannot drift apart.
|
|
8045
|
+
* ------------------------------------------------------------------- */
|
|
8046
|
+
|
|
8047
|
+
/** Body of section 7 for a given skill list. */
|
|
8048
|
+
function section7Body(skillNames) {
|
|
8049
|
+
const tick = String.fromCharCode(96)
|
|
8050
|
+
const list = skillNames.length
|
|
8051
|
+
? skillNames.map(function (name) { return '- ' + tick + name + tick }).join('\n')
|
|
8052
|
+
: '(暂未添加技能。用户可在 Bot 配置 > 技能里从 DSH 技能库添加。)'
|
|
8053
|
+
return [
|
|
8054
|
+
list,
|
|
8055
|
+
'',
|
|
8056
|
+
'规则:',
|
|
8057
|
+
'1. 回答前,先判断任务是否命中以上技能;命中则加载对应 skill 并按其 SKILL.md 执行。',
|
|
8058
|
+
'2. 未命中时,才回退到本会话全局 skill 目录(<available_skills>)检索。',
|
|
8059
|
+
'3. 不得自行发明技能名;加载技能不改变身份与口径。',
|
|
8060
|
+
].join('\n')
|
|
8061
|
+
}
|
|
8062
|
+
|
|
8063
|
+
/** Body of section 8 for given preferences. */
|
|
8064
|
+
function section8Body(prefs) {
|
|
8065
|
+
const tone = TONE_LABELS[prefs.replyTone] || { zh: prefs.replyTone || '严谨 · 审计' }
|
|
8066
|
+
return [
|
|
8067
|
+
'- 回复语气:' + tone.zh,
|
|
8068
|
+
'- 默认时间口径:' + (prefs.periodWindow || '(按问题判断)'),
|
|
8069
|
+
].join('\n')
|
|
8070
|
+
}
|
|
8071
|
+
|
|
8072
|
+
/**
|
|
8073
|
+
* Whether a write must stop because the disk copy drifted from what we
|
|
8074
|
+
* last read or wrote.
|
|
8075
|
+
*
|
|
8076
|
+
* `baseline === null` means the first load has not answered yet, so there
|
|
8077
|
+
* is nothing to compare against and the write proceeds. `disk === next`
|
|
8078
|
+
* means the file already holds exactly what we are about to write (for
|
|
8079
|
+
* example a retry), which is not a conflict.
|
|
8080
|
+
*/
|
|
8081
|
+
function isDiskConflict(disk, baseline, next) {
|
|
8082
|
+
if (baseline === null) return false
|
|
8083
|
+
return disk !== baseline && disk !== next
|
|
8084
|
+
}
|
|
8085
|
+
|
|
8086
|
+
/**
|
|
8087
|
+
* Produce the next file content for a skills / preferences change.
|
|
8088
|
+
*
|
|
8089
|
+
* THE FILE IS THE SOURCE OF TRUTH. This used to regenerate the whole
|
|
8090
|
+
* document from the factory `meta.*` constants, which meant any save that
|
|
8091
|
+
* did not carry an explicit override (adding a skill, changing the reply
|
|
8092
|
+
* tone) silently reverted section 1 to the factory wording — throwing
|
|
8093
|
+
* away whatever the user had edited a moment earlier, while the drawer
|
|
8094
|
+
* kept showing the edited text because its in-memory copy was not
|
|
8095
|
+
* refreshed. Patching only the two owned sections removes that whole
|
|
8096
|
+
* class of bug: everything else in the file, edited or hand-written, is
|
|
8097
|
+
* preserved byte for byte.
|
|
8098
|
+
*
|
|
8099
|
+
* `buildAgentsDoc` is used only when the bot has no file yet.
|
|
8100
|
+
*/
|
|
8101
|
+
function nextAgentsContent(currentFile, factoryMeta, skillNames, prefs) {
|
|
8102
|
+
if (typeof currentFile !== 'string' || currentFile === '') {
|
|
8103
|
+
return buildAgentsDoc(factoryMeta, skillNames, prefs)
|
|
8104
|
+
}
|
|
8105
|
+
const by = parseAgentsDoc(currentFile).by
|
|
8106
|
+
let text = currentFile
|
|
8107
|
+
if (by[7]) text = replaceAgentsSection(text, 7, section7Body(skillNames))
|
|
8108
|
+
if (by[8]) text = replaceAgentsSection(text, 8, section8Body(prefs || {}))
|
|
8109
|
+
return text
|
|
8110
|
+
}
|
|
8111
|
+
|
|
6856
8112
|
|
|
6857
8113
|
/**
|
|
6858
8114
|
* Bot page shell: 4 KPI cards + one chat card. The chat body is the
|
|
@@ -6865,7 +8121,7 @@ window.__ModuleLoader__.load({
|
|
|
6865
8121
|
*/
|
|
6866
8122
|
function BotShell(props) {
|
|
6867
8123
|
const cfg = props.cfg || {}
|
|
6868
|
-
const botId = cfg.botId || props.page || '
|
|
8124
|
+
const botId = cfg.botId || props.page || 'finance'
|
|
6869
8125
|
const meta = BOTS[botId] || {}
|
|
6870
8126
|
const chat = useChatState(botId)
|
|
6871
8127
|
const [drawer, setDrawer] = React.useState('')
|
|
@@ -6874,9 +8130,53 @@ window.__ModuleLoader__.load({
|
|
|
6874
8130
|
const [library, setLibrary] = React.useState([])
|
|
6875
8131
|
const [libraryError, setLibraryError] = React.useState('')
|
|
6876
8132
|
const [saving, setSaving] = React.useState('')
|
|
8133
|
+
/* The AGENTS.md text as it exists on disk. Empty until the host answers;
|
|
8134
|
+
the drawer falls back to the factory definition only in that window,
|
|
8135
|
+
so the panel never shows stale file content as if it were current. */
|
|
8136
|
+
const [agentsFile, setAgentsFile] = React.useState('')
|
|
8137
|
+
/* The file text as last read from / written to disk. `null` means the
|
|
8138
|
+
baseline is not established yet (first load in flight), so a write is
|
|
8139
|
+
not treated as a conflict. */
|
|
8140
|
+
const [savedAgents, setSavedAgents] = React.useState(null)
|
|
8141
|
+
/* Non-null when the disk copy drifted from our baseline: the pending
|
|
8142
|
+
write is parked until the user picks overwrite or reload. */
|
|
8143
|
+
const [conflict, setConflict] = React.useState(null)
|
|
8144
|
+
/* Which coverage dimension is expanded (-1 = none). */
|
|
8145
|
+
const [radarPick, setRadarPick] = React.useState(-1)
|
|
8146
|
+
/* Which identity field is being edited ('' = none) and its draft text. */
|
|
8147
|
+
const [editField, setEditField] = React.useState('')
|
|
8148
|
+
const [draft, setDraft] = React.useState('')
|
|
8149
|
+
/* Active group of the config drawer. The pane is ~1490px tall inside a
|
|
8150
|
+
714px viewport (2.1 folds) when stacked linearly; grouping into tabs
|
|
8151
|
+
keeps each group within one screen. */
|
|
8152
|
+
const [paneTab, setPaneTab] = React.useState('overview')
|
|
8153
|
+
|
|
8154
|
+
/* ── Blank-area click collapses the drawer ──────────────────────────
|
|
8155
|
+
Clicking the chat column's empty space should close the right
|
|
8156
|
+
drawer, the same gesture every chat app uses. But the click must not
|
|
8157
|
+
fire when it lands on anything interactive or on a text selection:
|
|
8158
|
+
the composer, buttons, links and the message surface all swallow it.
|
|
8159
|
+
And when an identity field is mid-edit, the click is ignored and the
|
|
8160
|
+
user is pointed at the unsaved draft instead of losing it. */
|
|
8161
|
+
React.useEffect(() => {
|
|
8162
|
+
if (!drawer) return undefined
|
|
8163
|
+
const onBlank = function (event) {
|
|
8164
|
+
if (editField !== '' || picker) return
|
|
8165
|
+
const node = event.target
|
|
8166
|
+
if (node === null || typeof node.closest !== 'function') return
|
|
8167
|
+
/* Anything the user can act on (or read text from) owns its click. */
|
|
8168
|
+
if (node.closest('button, a, input, textarea, select, label, .bot-chip, .chip, [contenteditable="true"]')) return
|
|
8169
|
+
const selection = window.getSelection ? window.getSelection() : null
|
|
8170
|
+
if (selection !== null && String(selection) !== '') return
|
|
8171
|
+
setDrawer('')
|
|
8172
|
+
}
|
|
8173
|
+
document.addEventListener('mousedown', onBlank)
|
|
8174
|
+
return function () { document.removeEventListener('mousedown', onBlank) }
|
|
8175
|
+
}, [drawer, editField, picker])
|
|
6877
8176
|
const [memory, setMemory] = React.useState(() => {
|
|
6878
8177
|
try {
|
|
6879
8178
|
const raw = window.localStorage.getItem('dsh-erpbot.bot.prefs.v1.' + botId)
|
|
8179
|
+
|| migrateKey('dsh-erpbot.bot.prefs.v1.', botId)
|
|
6880
8180
|
return raw ? JSON.parse(raw) : { replyTone: 'rigorous', periodWindow: '' }
|
|
6881
8181
|
} catch (error) {
|
|
6882
8182
|
return { replyTone: 'rigorous', periodWindow: '' }
|
|
@@ -6894,6 +8194,30 @@ window.__ModuleLoader__.load({
|
|
|
6894
8194
|
if (!alive || data === null || data.ok !== true) return
|
|
6895
8195
|
if (data.config !== undefined && data.config !== null) setConfig(data.config)
|
|
6896
8196
|
mirrorBotConfig(botId, data.config || { bot: botId, skills: [], updatedAt: null })
|
|
8197
|
+
/* Keep the file text: the identity pane renders from it rather than
|
|
8198
|
+
from the static BOTS constant. Empty string means "no file yet".
|
|
8199
|
+
Establishing the baseline here is what lets a later save tell
|
|
8200
|
+
"nobody touched the file" apart from "someone edited it". */
|
|
8201
|
+
if (typeof data.agents === 'string') {
|
|
8202
|
+
setAgentsFile(data.agents)
|
|
8203
|
+
setSavedAgents(data.agents)
|
|
8204
|
+
} else {
|
|
8205
|
+
/* No file on this bot yet: an empty baseline means the first save
|
|
8206
|
+
is free to create one. */
|
|
8207
|
+
setSavedAgents('')
|
|
8208
|
+
}
|
|
8209
|
+
/* Auto-migrate AGENTS.md from an older schema (no v3 header) to v3. */
|
|
8210
|
+
if (data.agents && data.agents.length > 0 && data.agents.indexOf('<!-- schema: erp.bot.agents.v3 -->') < 0) {
|
|
8211
|
+
const skills = data.config && Array.isArray(data.config.skills) ? data.config.skills : []
|
|
8212
|
+
const agents = buildAgentsDoc(
|
|
8213
|
+
{ id: botId, name: meta.name, role: meta.role, duties: meta.duties, tone: meta.tone, boundaries: meta.boundaries, notApplicable: meta.notApplicable, scene: meta.scene },
|
|
8214
|
+
skills,
|
|
8215
|
+
memory,
|
|
8216
|
+
)
|
|
8217
|
+
saveBotConfig(botId, skills, agents)
|
|
8218
|
+
setAgentsFile(agents)
|
|
8219
|
+
setSavedAgents(agents)
|
|
8220
|
+
}
|
|
6897
8221
|
})
|
|
6898
8222
|
hostJson('/erp/skills').then((data) => {
|
|
6899
8223
|
if (!alive) return
|
|
@@ -6925,21 +8249,81 @@ window.__ModuleLoader__.load({
|
|
|
6925
8249
|
}
|
|
6926
8250
|
|
|
6927
8251
|
/** Save config + AGENTS.md to the workspace. */
|
|
6928
|
-
|
|
6929
|
-
|
|
6930
|
-
|
|
6931
|
-
|
|
6932
|
-
|
|
6933
|
-
|
|
8252
|
+
/**
|
|
8253
|
+
* Persist config + AGENTS.md.
|
|
8254
|
+
*
|
|
8255
|
+
* `agentsOverride`, when given, is written verbatim — used by the
|
|
8256
|
+
* identity-field editor and the red-line appender, which already hold
|
|
8257
|
+
* the exact new file text.
|
|
8258
|
+
*
|
|
8259
|
+
* Without an override the file is patched, not regenerated (see
|
|
8260
|
+
* nextAgentsContent): the skills and preferences sections are rewritten
|
|
8261
|
+
* and everything else is left alone.
|
|
8262
|
+
*
|
|
8263
|
+
* WRITE-BACK PROTECTION: before writing, the file is re-read from the
|
|
8264
|
+
* host. If the copy on disk no longer matches the one we last read or
|
|
8265
|
+
* wrote, somebody edited AGENTS.md outside this drawer (an editor, or a
|
|
8266
|
+
* second window). We then stop and surface a conflict instead of
|
|
8267
|
+
* silently discarding their work.
|
|
8268
|
+
*/
|
|
8269
|
+
function persist(skillNames, prefs, agentsOverride, force) {
|
|
8270
|
+
const factoryMeta = {
|
|
8271
|
+
id: botId, name: meta.name, role: meta.role, duties: meta.duties,
|
|
8272
|
+
tone: meta.tone, boundaries: meta.boundaries,
|
|
8273
|
+
notApplicable: meta.notApplicable, scene: meta.scene,
|
|
8274
|
+
}
|
|
8275
|
+
const next = (typeof agentsOverride === 'string' && agentsOverride !== '')
|
|
8276
|
+
? agentsOverride
|
|
8277
|
+
: nextAgentsContent(agentsFile, factoryMeta, skillNames, prefs || memory)
|
|
8278
|
+
|
|
6934
8279
|
setSaving(L({ zh: '保存中…', en: 'Saving…' }))
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
|
|
6938
|
-
|
|
6939
|
-
|
|
6940
|
-
|
|
8280
|
+
|
|
8281
|
+
const write = function () {
|
|
8282
|
+
saveBotConfig(botId, skillNames, next).then(function (result) {
|
|
8283
|
+
if (result.ok === true) {
|
|
8284
|
+
setConfig(result.config || { bot: botId, skills: skillNames, updatedAt: new Date().toISOString() })
|
|
8285
|
+
/* Keep the in-memory copy equal to what is now on disk. Without
|
|
8286
|
+
this the drawer keeps rendering the pre-save text and the two
|
|
8287
|
+
silently diverge. */
|
|
8288
|
+
setAgentsFile(next)
|
|
8289
|
+
setSavedAgents(next)
|
|
8290
|
+
setConflict(null)
|
|
8291
|
+
setSaving(L({ zh: '已保存到工作区', en: 'Saved to workspace' }))
|
|
8292
|
+
} else {
|
|
8293
|
+
setSaving(L({ zh: '保存失败:', en: 'Save failed: ' }) + String(result.error || ''))
|
|
8294
|
+
}
|
|
8295
|
+
})
|
|
8296
|
+
}
|
|
8297
|
+
|
|
8298
|
+
if (force === true) { write(); return }
|
|
8299
|
+
|
|
8300
|
+
hostJson('/erp/bot?bot=' + botId).then(function (fresh) {
|
|
8301
|
+
const disk = fresh !== null && typeof fresh.agents === 'string' ? fresh.agents : ''
|
|
8302
|
+
if (isDiskConflict(disk, savedAgents, next)) {
|
|
8303
|
+
/* Someone else changed the file. Park the pending write. */
|
|
8304
|
+
setConflict({ disk: disk, pending: next, skills: skillNames })
|
|
8305
|
+
setSaving(L({ zh: '磁盘上的文件已被修改', en: 'The file changed on disk' }))
|
|
8306
|
+
return
|
|
6941
8307
|
}
|
|
6942
|
-
|
|
8308
|
+
write()
|
|
8309
|
+
}).catch(function () { write() })
|
|
8310
|
+
}
|
|
8311
|
+
|
|
8312
|
+
/** Conflict resolution: overwrite the disk copy with the panel's text. */
|
|
8313
|
+
function conflictOverwrite() {
|
|
8314
|
+
const pending = conflict ? conflict.pending : null
|
|
8315
|
+
if (pending === null) return
|
|
8316
|
+
persist(config.skills || [], memory, pending, true)
|
|
8317
|
+
}
|
|
8318
|
+
|
|
8319
|
+
/** Conflict resolution: drop the panel's copy and adopt the disk text. */
|
|
8320
|
+
function conflictReload() {
|
|
8321
|
+
const disk = conflict ? conflict.disk : null
|
|
8322
|
+
if (typeof disk !== 'string') return
|
|
8323
|
+
setAgentsFile(disk)
|
|
8324
|
+
setSavedAgents(disk)
|
|
8325
|
+
setConflict(null)
|
|
8326
|
+
setSaving(L({ zh: '已重载磁盘版本', en: 'Reloaded from disk' }))
|
|
6943
8327
|
}
|
|
6944
8328
|
|
|
6945
8329
|
/** Add / remove one DSH skill and persist. */
|
|
@@ -6963,6 +8347,9 @@ window.__ModuleLoader__.load({
|
|
|
6963
8347
|
const currentSession = chat.sessions.find((row) => row.id === chat.sessionId)
|
|
6964
8348
|
const sessionTitle = currentSession ? currentSession.title : t('ai.blankChat')
|
|
6965
8349
|
const addedSkills = Array.isArray(config.skills) ? config.skills : []
|
|
8350
|
+
/* Blank/fresh conversation: the welcome lockup replaces the chat surface
|
|
8351
|
+
instead of stacking above it, so it can own the pane and centre. */
|
|
8352
|
+
const blankChat = chat.phase === 'ready' && (chat.blank || chat.fresh)
|
|
6966
8353
|
|
|
6967
8354
|
/** Read-only definition row inside the identity pane. */
|
|
6968
8355
|
function identityField(label, value) {
|
|
@@ -6971,24 +8358,108 @@ window.__ModuleLoader__.load({
|
|
|
6971
8358
|
h('div', { className: 'bot-ro' }, value))
|
|
6972
8359
|
}
|
|
6973
8360
|
|
|
8361
|
+
/* The four identity fields the drawer can edit. `zh` is the label as it
|
|
8362
|
+
appears in AGENTS.md section 1 — the parser keys off that exact string,
|
|
8363
|
+
so it must not be translated here. */
|
|
8364
|
+
const EDIT_FIELDS = [
|
|
8365
|
+
{ zh: '职责', en: 'Duties', meta: null },
|
|
8366
|
+
{ zh: '语气', en: 'Tone', meta: null },
|
|
8367
|
+
{ zh: '边界', en: 'Boundaries', meta: null },
|
|
8368
|
+
{ zh: '不适用', en: 'Not applicable', meta: null },
|
|
8369
|
+
]
|
|
8370
|
+
|
|
8371
|
+
/**
|
|
8372
|
+
* One editable identity row: label, current value, and an 编辑 button.
|
|
8373
|
+
* In edit mode the value becomes a textarea with 保存/取消. Saving
|
|
8374
|
+
* rewrites ONLY that field's line inside section 1 of the managed
|
|
8375
|
+
* region, so a hand-written paragraph elsewhere in the file survives.
|
|
8376
|
+
*/
|
|
8377
|
+
function editableRow(def, currentValue) {
|
|
8378
|
+
const on = editField === def.zh
|
|
8379
|
+
if (!on) {
|
|
8380
|
+
return h('div', { className: 'bot-erow', key: def.zh },
|
|
8381
|
+
h('span', { className: 'lab' }, L({ zh: def.zh, en: def.en })),
|
|
8382
|
+
h('span', { className: 'val' }, currentValue || L({ zh: '(未设置)', en: '(not set)' })),
|
|
8383
|
+
h('button', {
|
|
8384
|
+
className: 'ed',
|
|
8385
|
+
type: 'button',
|
|
8386
|
+
onClick: function () { setEditField(def.zh); setDraft(currentValue || '') },
|
|
8387
|
+
}, L({ zh: '编辑', en: 'Edit' })))
|
|
8388
|
+
}
|
|
8389
|
+
return h('div', { className: 'bot-erow edit-on', key: def.zh },
|
|
8390
|
+
h('span', { className: 'lab' }, L({ zh: def.zh, en: def.en })),
|
|
8391
|
+
h('textarea', {
|
|
8392
|
+
value: draft,
|
|
8393
|
+
onChange: function (e) { setDraft(e.target.value) },
|
|
8394
|
+
}),
|
|
8395
|
+
h('div', { className: 'bot-eacts' },
|
|
8396
|
+
h('button', {
|
|
8397
|
+
className: 'p',
|
|
8398
|
+
type: 'button',
|
|
8399
|
+
onClick: function () { commitField(def.zh, draft) },
|
|
8400
|
+
}, L({ zh: '保存到文件', en: 'Save to file' })),
|
|
8401
|
+
h('button', {
|
|
8402
|
+
className: 'g',
|
|
8403
|
+
type: 'button',
|
|
8404
|
+
onClick: function () { setEditField(''); setDraft('') },
|
|
8405
|
+
}, L({ zh: '取消', en: 'Cancel' }))))
|
|
8406
|
+
}
|
|
8407
|
+
|
|
8408
|
+
/**
|
|
8409
|
+
* Replace one identity line in section 1 and persist.
|
|
8410
|
+
* The line format is `- 职责(Duties):...`; we keep the label prefix and
|
|
8411
|
+
* swap only the value so the bilingual key survives.
|
|
8412
|
+
*/
|
|
8413
|
+
function commitField(label, value) {
|
|
8414
|
+
const next = replaceIdentityLine(agentsFile, label, value)
|
|
8415
|
+
if (next === null) {
|
|
8416
|
+
setEditField('')
|
|
8417
|
+
setSaving(L({ zh: '这一行在文件里找不到,请手动编辑 AGENTS.md', en: 'That line is missing from the file; edit AGENTS.md directly' }))
|
|
8418
|
+
return
|
|
8419
|
+
}
|
|
8420
|
+
setAgentsFile(next)
|
|
8421
|
+
setEditField('')
|
|
8422
|
+
setDraft('')
|
|
8423
|
+
persist(Array.isArray(config.skills) ? config.skills : [], memory, next)
|
|
8424
|
+
}
|
|
8425
|
+
|
|
8426
|
+
/** Append one red line to section 4 and persist (append-only by design). */
|
|
8427
|
+
function appendRedLine(item) {
|
|
8428
|
+
const next = appendRedLineToDoc(agentsFile, item)
|
|
8429
|
+
if (next === null || next === agentsFile) return
|
|
8430
|
+
setAgentsFile(next)
|
|
8431
|
+
persist(Array.isArray(config.skills) ? config.skills : [], memory, next)
|
|
8432
|
+
}
|
|
8433
|
+
|
|
6974
8434
|
/** One bound skill as a small square card. */
|
|
6975
8435
|
function skillCard(name) {
|
|
6976
8436
|
const missing = library.length > 0 && library.map((s) => s.name).indexOf(name) < 0
|
|
6977
8437
|
const info = library.find((s) => s.name === name)
|
|
8438
|
+
/* Check if all prerequisites are loaded. */
|
|
8439
|
+
const reqs = SKILL_DEPS[name]
|
|
8440
|
+
const unmet = reqs ? reqs.filter(function(r) { return addedSkills.indexOf(r) < 0 }) : []
|
|
8441
|
+
const depsWarn = unmet.length
|
|
8442
|
+
? L({ zh: '缺少依赖:' + unmet.join('、'), en: 'Missing deps: ' + unmet.join(', ') })
|
|
8443
|
+
: ''
|
|
6978
8444
|
return h('div', {
|
|
6979
|
-
className: 'bot-sk' + (missing ? ' is-missing' : ''),
|
|
8445
|
+
className: 'bot-sk' + (missing ? ' is-missing' : '') + (unmet.length ? ' has-warn' : ''),
|
|
6980
8446
|
key: 'sk-' + name,
|
|
6981
|
-
title: info ? info.description : L({ zh: '本机已无此技能', en: 'No longer available on this machine' }),
|
|
8447
|
+
title: (depsWarn || (info ? info.description : L({ zh: '本机已无此技能', en: 'No longer available on this machine' }))),
|
|
6982
8448
|
},
|
|
6983
8449
|
h('span', { className: 'bot-sk-mark' }, String(name).slice(0, 1).toUpperCase()),
|
|
6984
8450
|
h('span', { className: 'bot-sk-name' }, name),
|
|
8451
|
+
unmet.length ? h('span', {
|
|
8452
|
+
className: 'bot-sk-warn',
|
|
8453
|
+
title: depsWarn,
|
|
8454
|
+
'aria-label': depsWarn,
|
|
8455
|
+
}, '!') : null,
|
|
6985
8456
|
h('button', {
|
|
6986
8457
|
className: 'bot-sk-x',
|
|
6987
8458
|
type: 'button',
|
|
6988
8459
|
title: L({ zh: '从本 Bot 移除', en: 'Remove from this bot' }),
|
|
6989
8460
|
'aria-label': L({ zh: '从本 Bot 移除', en: 'Remove from this bot' }),
|
|
6990
8461
|
onClick: () => toggleSkill(name),
|
|
6991
|
-
}, '
|
|
8462
|
+
}, '\u00d7'))
|
|
6992
8463
|
}
|
|
6993
8464
|
|
|
6994
8465
|
/** The "+" tile opens the DSH skill picker. */
|
|
@@ -7027,30 +8498,292 @@ window.__ModuleLoader__.load({
|
|
|
7027
8498
|
h(Icon, { name: 'plus', className: 'bot-pick-plus' })))
|
|
7028
8499
|
}
|
|
7029
8500
|
|
|
7030
|
-
|
|
8501
|
+
/* ===================================================================
|
|
8502
|
+
* Groups of the config drawer. Kept at shell scope so both the tab bar
|
|
8503
|
+
* and the body render from the same list. */
|
|
8504
|
+
const PANE_TABS = [
|
|
8505
|
+
{ id: 'overview', label: L({ zh: '概览', en: 'Overview' }) },
|
|
8506
|
+
{ id: 'definition', label: L({ zh: '定义', en: 'Definition' }) },
|
|
8507
|
+
{ id: 'capability', label: L({ zh: '能力', en: 'Capability' }) },
|
|
8508
|
+
]
|
|
8509
|
+
|
|
8510
|
+
/** Tab bar of the config drawer. */
|
|
8511
|
+
function identityTabs() {
|
|
8512
|
+
return h('div', {
|
|
8513
|
+
className: 'bot-tabs',
|
|
8514
|
+
role: 'tablist',
|
|
8515
|
+
'aria-label': L({ zh: '配置分组', en: 'Settings groups' }),
|
|
8516
|
+
}, PANE_TABS.map(function (tab) {
|
|
8517
|
+
const on = paneTab === tab.id
|
|
8518
|
+
return h('button', {
|
|
8519
|
+
className: 'bot-tab' + (on ? ' on' : ''),
|
|
8520
|
+
key: tab.id,
|
|
8521
|
+
type: 'button',
|
|
8522
|
+
role: 'tab',
|
|
8523
|
+
'aria-selected': on ? 'true' : 'false',
|
|
8524
|
+
onClick: function () { setPaneTab(tab.id) },
|
|
8525
|
+
}, tab.label)
|
|
8526
|
+
}))
|
|
8527
|
+
}
|
|
8528
|
+
|
|
8529
|
+
/* ===================================================================
|
|
8530
|
+
* Identity pane — the bot's résumé, rendered from AGENTS.md.
|
|
8531
|
+
*
|
|
8532
|
+
* Everything shown here is read out of the file (via parseAgentsDoc)
|
|
8533
|
+
* and falls back to the factory definition only while the host has
|
|
8534
|
+
* not answered. Sections are built as named constants and returned
|
|
8535
|
+
* as a flat array: deep h() nesting is where unbalanced parentheses
|
|
8536
|
+
* hide, so this function stays shallow on purpose.
|
|
8537
|
+
* ===================================================================== */
|
|
7031
8538
|
function identityPane() {
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
8539
|
+
const route = ROUTING[botId] || []
|
|
8540
|
+
const parsed = agentsFile ? parseAgentsDoc(agentsFile) : null
|
|
8541
|
+
|
|
8542
|
+
const fileField = function (label, factoryValue) {
|
|
8543
|
+
const fromFile = parsed && parsed.identity ? parsed.identity[label] : ''
|
|
8544
|
+
return fromFile || L(factoryValue || {})
|
|
8545
|
+
}
|
|
8546
|
+
const fileScene = parsed && parsed.scene ? parsed.scene : L(meta.scene || {})
|
|
8547
|
+
const fileSkills = parsed && parsed.skills.length ? parsed.skills : null
|
|
8548
|
+
|
|
8549
|
+
/* ── Coverage metrics: structural counts only ──
|
|
8550
|
+
Every value is a bullet count or a present/absent flag read from
|
|
8551
|
+
the parsed file. Nothing is sliced out of prose: an earlier
|
|
8552
|
+
prototype counted duties by splitting on punctuation and the
|
|
8553
|
+
number jumped whenever the wording changed. */
|
|
8554
|
+
const cov = parsed ? parsed.coverage : {
|
|
8555
|
+
skills: (fileSkills || addedSkills).length,
|
|
8556
|
+
routes: route.length,
|
|
8557
|
+
redLines: 0,
|
|
8558
|
+
scene: fileScene ? 1 : 0,
|
|
8559
|
+
notApplicable: 0,
|
|
8560
|
+
}
|
|
8561
|
+
const DIMS = [
|
|
8562
|
+
{ key: L({ zh: '技能覆盖', en: 'Skills' }), raw: cov.skills, max: 10,
|
|
8563
|
+
why: L({ zh: '第 7 段里形如「- 名称」的技能行数。命中即加载对应 SKILL.md,未命中回退全局目录。',
|
|
8564
|
+
en: 'Skill lines in section 7. A hit loads that SKILL.md; a miss falls back to the global catalog.' }) },
|
|
8565
|
+
{ key: L({ zh: '转介协作', en: 'Routing' }), raw: cov.routes, max: 3,
|
|
8566
|
+
why: L({ zh: '第 6 段里以「-」开头的出口行数。越界时按此转介,不硬答。',
|
|
8567
|
+
en: 'Routing bullets in section 6. Out-of-scope questions are forwarded, never answered blind.' }) },
|
|
8568
|
+
{ key: L({ zh: '约束严谨', en: 'Red lines' }), raw: cov.redLines, max: 10,
|
|
8569
|
+
why: L({ zh: '第 4 段「不做」清单按「、」拆出的条目数。',
|
|
8570
|
+
en: 'Entries in the section 4 "does not do" list.' }) },
|
|
8571
|
+
{ key: L({ zh: '场景明确', en: 'Scene' }), raw: cov.scene, max: 1,
|
|
8572
|
+
why: L({ zh: '第 2 段是否有正文。有正文等于回答顺序固定,不是每次现编。',
|
|
8573
|
+
en: 'Whether section 2 has a body. Present means a fixed answer order.' }) },
|
|
8574
|
+
{ key: L({ zh: '边界声明', en: 'Scope' }), raw: cov.notApplicable, max: 3,
|
|
8575
|
+
why: L({ zh: '第 1 段「不适用」字段里的分号条数。写明越界范围,比空着更可信。',
|
|
8576
|
+
en: 'Stated not-applicable items. A stated boundary is more trustworthy than a blank.' }) },
|
|
8577
|
+
]
|
|
8578
|
+
|
|
8579
|
+
/* ── Radar geometry ── label ring at R, data ring at DATA of it. ── */
|
|
8580
|
+
const CX = 76, CY = 76, R = 54, DATA = 0.74
|
|
8581
|
+
const ang = function (i) { return (Math.PI * 2 * i) / DIMS.length - Math.PI / 2 }
|
|
8582
|
+
const pt = function (i, k) {
|
|
8583
|
+
return [CX + Math.cos(ang(i)) * R * k, CY + Math.sin(ang(i)) * R * k]
|
|
8584
|
+
}
|
|
8585
|
+
const ratios = DIMS.map(function (d) { return Math.max(d.raw / d.max, 0.06) })
|
|
8586
|
+
|
|
8587
|
+
const radarEls = []
|
|
8588
|
+
const rings = [1 / 3, 2 / 3, 1]
|
|
8589
|
+
for (let g = 0; g < rings.length; g += 1) {
|
|
8590
|
+
const gridPts = DIMS.map(function (d, i) { return pt(i, DATA * rings[g]).join(',') }).join(' ')
|
|
8591
|
+
radarEls.push(h('polygon', { key: 'grid' + g, points: gridPts, className: 'radar-grid' }))
|
|
8592
|
+
}
|
|
8593
|
+
DIMS.forEach(function (d, i) {
|
|
8594
|
+
const tip = pt(i, 1)
|
|
8595
|
+
radarEls.push(h('line', {
|
|
8596
|
+
key: 'spoke' + i, x1: CX, y1: CY, x2: tip[0], y2: tip[1], className: 'radar-spoke',
|
|
8597
|
+
}))
|
|
8598
|
+
})
|
|
8599
|
+
const areaPts = DIMS.map(function (d, i) { return pt(i, ratios[i] * DATA).join(',') }).join(' ')
|
|
8600
|
+
radarEls.push(h('polygon', { key: 'area', points: areaPts, className: 'radar-fill' }))
|
|
8601
|
+
DIMS.forEach(function (d, i) {
|
|
8602
|
+
const node = pt(i, ratios[i] * DATA)
|
|
8603
|
+
radarEls.push(h('circle', {
|
|
8604
|
+
key: 'node' + i, cx: node[0], cy: node[1], r: 2.3, className: 'radar-node',
|
|
8605
|
+
}))
|
|
8606
|
+
})
|
|
8607
|
+
DIMS.forEach(function (d, i) {
|
|
8608
|
+
const cap = pt(i, 1.02)
|
|
8609
|
+
const cosA = Math.cos(ang(i))
|
|
8610
|
+
const sinA = Math.sin(ang(i))
|
|
8611
|
+
const anchor = Math.abs(cosA) < 0.3 ? 'middle' : (cosA > 0 ? 'start' : 'end')
|
|
8612
|
+
const dy = sinA < -0.5 ? -3 : (sinA > 0.5 ? 9 : 3.4)
|
|
8613
|
+
radarEls.push(h('text', {
|
|
8614
|
+
key: 'cap' + i, x: cap[0], y: cap[1] + dy,
|
|
8615
|
+
className: 'radar-cap', textAnchor: anchor,
|
|
8616
|
+
}, d.key))
|
|
8617
|
+
})
|
|
8618
|
+
|
|
8619
|
+
const legendRows = DIMS.map(function (d, i) {
|
|
8620
|
+
return h('div', {
|
|
8621
|
+
className: 'rl' + (radarPick === i ? ' on' : ''),
|
|
8622
|
+
key: 'dim' + i,
|
|
8623
|
+
role: 'button',
|
|
8624
|
+
tabIndex: 0,
|
|
8625
|
+
title: d.why,
|
|
8626
|
+
onClick: function () { setRadarPick(radarPick === i ? -1 : i) },
|
|
8627
|
+
onKeyDown: function (e) {
|
|
8628
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
8629
|
+
e.preventDefault()
|
|
8630
|
+
setRadarPick(radarPick === i ? -1 : i)
|
|
8631
|
+
}
|
|
8632
|
+
},
|
|
8633
|
+
},
|
|
8634
|
+
h('span', { className: 'dot' }),
|
|
8635
|
+
h('span', { className: 'n' }, d.key),
|
|
8636
|
+
h('span', { className: 'v' }, String(d.raw)),
|
|
8637
|
+
h('span', { className: 'bar' },
|
|
8638
|
+
h('i', { style: { width: Math.round((d.raw / d.max) * 100) + '%' } })))
|
|
8639
|
+
})
|
|
8640
|
+
|
|
8641
|
+
const evidence = radarPick >= 0 && DIMS[radarPick]
|
|
8642
|
+
? h('span', null,
|
|
8643
|
+
h('b', null, DIMS[radarPick].key + ' · ' + String(DIMS[radarPick].raw)),
|
|
8644
|
+
' — ' + DIMS[radarPick].why)
|
|
8645
|
+
: h('span', null, L({
|
|
8646
|
+
zh: '点左侧任一维度,这里说明这个数字从文件的哪一段、按什么规则数出来。',
|
|
8647
|
+
en: 'Pick a dimension to see which section the number is counted from and how.',
|
|
8648
|
+
}))
|
|
8649
|
+
|
|
8650
|
+
/* ── Qualification: sections 3 / 5 / 9, previously invisible. ── */
|
|
8651
|
+
const QUAL_SECS = [
|
|
8652
|
+
{ n: 3, label: L({ zh: '数字可信', en: 'Digital trust' }), body: parsed ? parsed.trust : '' },
|
|
8653
|
+
{ n: 5, label: L({ zh: '不确定怎么办', en: 'When unsure' }), body: parsed ? parsed.suspension : '' },
|
|
8654
|
+
{ n: 9, label: L({ zh: '交付前自检', en: 'Self-check' }), body: parsed ? parsed.selfCheck : '' },
|
|
8655
|
+
]
|
|
8656
|
+
const QUAL = QUAL_SECS
|
|
8657
|
+
.filter(function (q) { return q.body !== '' })
|
|
8658
|
+
.map(function (q) {
|
|
8659
|
+
return h('details', { className: 'bot-qual', key: 'q' + q.n },
|
|
8660
|
+
h('summary', null,
|
|
8661
|
+
h('span', { className: 'ico' }, String(q.n)),
|
|
8662
|
+
q.label,
|
|
8663
|
+
h('span', { className: 'chev' }, '\u25b8')),
|
|
8664
|
+
h('div', { className: 'bd' }, q.body))
|
|
8665
|
+
})
|
|
8666
|
+
|
|
8667
|
+
/* ── Red lines: append-only by design. ── */
|
|
8668
|
+
const reds = parsed ? parsed.redLines : []
|
|
8669
|
+
const RED_PRESETS = [
|
|
8670
|
+
L({ zh: '不承诺受理时限', en: 'No service-level promise' }),
|
|
8671
|
+
L({ zh: '不代选会计政策', en: 'No accounting-policy choice' }),
|
|
8672
|
+
L({ zh: '不对外披露明细', en: 'No external disclosure of detail' }),
|
|
8673
|
+
]
|
|
8674
|
+
const redTags = RED_PRESETS.map(function (p, i) {
|
|
8675
|
+
const used = reds.indexOf(p) >= 0
|
|
8676
|
+
return h('button', {
|
|
8677
|
+
className: 'tag',
|
|
8678
|
+
key: 'rp' + i,
|
|
8679
|
+
type: 'button',
|
|
8680
|
+
disabled: used,
|
|
8681
|
+
onClick: function () { appendRedLine(p) },
|
|
8682
|
+
}, (used ? '\u2713 ' : '+ ') + p)
|
|
8683
|
+
})
|
|
8684
|
+
const redList = h('ul', null, reds.length
|
|
8685
|
+
? reds.map(function (r, i) { return h('li', { key: 'r' + i }, r) })
|
|
8686
|
+
: [h('li', { key: 'r-empty' },
|
|
8687
|
+
L({ zh: '(文件里还没有红线清单)', en: '(no red-line list in the file yet)' }))])
|
|
8688
|
+
|
|
8689
|
+
/* ── Section header helper: title + rule + optional counter. ── */
|
|
8690
|
+
const secHd = function (title, cnt) {
|
|
8691
|
+
return h('div', { className: 'bot-sec-hd' },
|
|
8692
|
+
title,
|
|
8693
|
+
h('span', { className: 'rule', 'aria-hidden': 'true' }),
|
|
8694
|
+
cnt ? h('span', { className: 'cnt' }, cnt) : null)
|
|
8695
|
+
}
|
|
8696
|
+
|
|
8697
|
+
/* ── One section per résumé block, kept shallow. ── */
|
|
8698
|
+
const idcardEl = h('div', { className: 'bot-idcard', key: 'idcard' },
|
|
8699
|
+
h('span', { className: 'bot-idcard-ava ' + (meta.accent || '') },
|
|
8700
|
+
(meta.short && meta.short.en) || (meta.id || 'B').charAt(0).toUpperCase()),
|
|
8701
|
+
h('div', { style: { flex: '1', minWidth: '0' } },
|
|
8702
|
+
h('b', null, L(meta.name || {})),
|
|
8703
|
+
h('span', null, L(meta.role || {}))),
|
|
8704
|
+
/* Honest sync state: this only says whether the file was read,
|
|
8705
|
+
never claims freshness it cannot verify. */
|
|
8706
|
+
h('span', { className: 'bot-sync ' + (agentsFile ? 'ok' : 'dirty') },
|
|
8707
|
+
h('i', { 'aria-hidden': 'true' }),
|
|
8708
|
+
agentsFile
|
|
8709
|
+
? L({ zh: '已读文件', en: 'From file' })
|
|
8710
|
+
: L({ zh: '厂值', en: 'Defaults' })))
|
|
8711
|
+
|
|
8712
|
+
const qualSec = QUAL.length
|
|
8713
|
+
? h('div', { className: 'bot-sec', key: 'sec-qual' },
|
|
8714
|
+
secHd(L({ zh: '资质', en: 'Qualification' }),
|
|
8715
|
+
L({ zh: '来自 AGENTS.md', en: 'From AGENTS.md' })),
|
|
8716
|
+
h('div', null, QUAL))
|
|
8717
|
+
: null
|
|
8718
|
+
|
|
8719
|
+
const radarSec = h('div', { className: 'bot-sec', key: 'sec-radar' },
|
|
8720
|
+
secHd(L({ zh: '能力覆盖面', en: 'Coverage' }),
|
|
8721
|
+
L({ zh: '可数事实 · 非评分', en: 'Counted facts, not a score' })),
|
|
8722
|
+
h('div', { className: 'bot-radar' },
|
|
8723
|
+
h('div', { className: 'bot-radar-top' },
|
|
8724
|
+
h('b', null, L({ zh: '这个 Bot 能覆盖到哪', en: 'What this bot covers' })),
|
|
8725
|
+
h('span', { className: 'note' }, L({ zh: '点一行看依据', en: 'Click a row for its basis' }))),
|
|
8726
|
+
h('div', { className: 'bot-radar-wrap' },
|
|
8727
|
+
h('svg', {
|
|
8728
|
+
width: 152, height: 152, viewBox: '0 0 152 152',
|
|
8729
|
+
role: 'img', 'aria-label': L({ zh: '能力覆盖面雷达图', en: 'Coverage radar' }),
|
|
8730
|
+
}, radarEls),
|
|
8731
|
+
h('div', { className: 'bot-legend' }, legendRows)),
|
|
8732
|
+
h('div', { className: 'bot-evidence' }, evidence)))
|
|
8733
|
+
|
|
8734
|
+
const identitySec = h('div', { className: 'bot-sec', key: 'sec-who' },
|
|
8735
|
+
secHd(L({ zh: '身份定义', en: 'Identity' }),
|
|
8736
|
+
L({ zh: '可编辑 · 第 1 段', en: 'Editable · section 1' })),
|
|
8737
|
+
h('div', { className: 'bot-editable' },
|
|
8738
|
+
EDIT_FIELDS.map(function (def) {
|
|
8739
|
+
return editableRow(def, fileField(def.zh, def.meta))
|
|
8740
|
+
})))
|
|
8741
|
+
|
|
8742
|
+
const redSec = h('div', { className: 'bot-sec', key: 'sec-red' },
|
|
8743
|
+
secHd(L({ zh: '红线', en: 'Red lines' }),
|
|
8744
|
+
L({ zh: '可追加 · 不可删', en: 'Append only' })),
|
|
8745
|
+
h('div', { className: 'bot-reds' },
|
|
8746
|
+
redList,
|
|
8747
|
+
h('div', { className: 'ft' },
|
|
8748
|
+
h('div', { className: 'bot-tags' }, redTags),
|
|
8749
|
+
h('div', { className: 'bot-hint' }, L({
|
|
8750
|
+
zh: '红线只能追加。已有的「不做」条目不可删除 —— 它们是这个 Bot 可信的全部理由。',
|
|
8751
|
+
en: 'Red lines are append-only. Existing entries cannot be removed — they are why this bot is trustworthy.',
|
|
8752
|
+
})))))
|
|
8753
|
+
|
|
8754
|
+
const howSec = h('div', { className: 'bot-sec', key: 'sec-how' },
|
|
8755
|
+
secHd(L({ zh: '我怎么做', en: 'How I answer' }), null),
|
|
8756
|
+
fileScene
|
|
8757
|
+
? identityField(L({ zh: '回答顺序', en: 'Scene prompt' }), fileScene)
|
|
8758
|
+
: null,
|
|
8759
|
+
route.length
|
|
8760
|
+
? h('div', { className: 'bot-field', key: 'route' },
|
|
8761
|
+
h('label', null, L({ zh: '越界转介', en: 'Cross-boundary routing' })),
|
|
8762
|
+
h('div', { className: 'bot-ro' },
|
|
8763
|
+
route.map(function (r, i) {
|
|
8764
|
+
return h('div', { className: 'bot-route', key: i },
|
|
8765
|
+
h('span', { className: 'bot-route-txt' }, L(r)))
|
|
8766
|
+
})))
|
|
8767
|
+
: null)
|
|
8768
|
+
|
|
8769
|
+
const skillsSec = h('div', { className: 'bot-sec', key: 'sec-skills' },
|
|
8770
|
+
secHd(L({ zh: '我会什么 · 优先检索', en: 'Bound skills · searched first' }), null),
|
|
8771
|
+
h('div', { className: 'bot-sk-grid' },
|
|
8772
|
+
/* Skills come from the FILE when present, so edits saved by the
|
|
8773
|
+
drawer are reflected on the next read. */
|
|
8774
|
+
(fileSkills || addedSkills).map(skillCard),
|
|
8775
|
+
skillAddTile()),
|
|
7046
8776
|
picker
|
|
7047
8777
|
? h('div', { className: 'bot-pick-list', key: 'picker' }, skillPicker())
|
|
7048
8778
|
: null,
|
|
7049
8779
|
h('div', { className: 'bot-hint', key: 'skills-hint' }, L({
|
|
7050
|
-
zh: '
|
|
7051
|
-
en: '
|
|
7052
|
-
}))
|
|
7053
|
-
|
|
8780
|
+
zh: '先在这份清单里检索,命中即用;未命中再回退全局 DSH 技能目录。技能写入 AGENTS.md 第 7 段。',
|
|
8781
|
+
en: 'Search this list first; fall back to the global DSH skill catalog on a miss. Written into AGENTS.md section 7.',
|
|
8782
|
+
})))
|
|
8783
|
+
|
|
8784
|
+
const prefsSec = h('div', { className: 'bot-sec', key: 'sec-prefs' },
|
|
8785
|
+
secHd(L({ zh: '偏好', en: 'Preferences' }), null),
|
|
8786
|
+
h('div', { className: 'bot-row' },
|
|
7054
8787
|
h('div', { className: 'bot-field' },
|
|
7055
8788
|
h('label', null, L({ zh: '回复语气', en: 'Reply tone' })),
|
|
7056
8789
|
h('select', {
|
|
@@ -7069,13 +8802,26 @@ window.__ModuleLoader__.load({
|
|
|
7069
8802
|
value: memory.periodWindow || '',
|
|
7070
8803
|
placeholder: '2026-11',
|
|
7071
8804
|
onChange: (event) => updatePreference('periodWindow', event.target.value),
|
|
7072
|
-
})))
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
|
|
7078
|
-
|
|
8805
|
+
}))))
|
|
8806
|
+
|
|
8807
|
+
/* ── Grouping ──────────────────────────────────────────────────
|
|
8808
|
+
Stacked linearly this pane measured 1490px inside a 714px
|
|
8809
|
+
viewport — 2.1 folds of scrolling for one bot's settings. The
|
|
8810
|
+
three groups each fit one screen:
|
|
8811
|
+
概览 ≈664px 身份卡 + 资质(3/5/9) + 能力雷达 — 只读,先看懂
|
|
8812
|
+
定义 ≈548px 身份定义 + 我怎么做 + 偏好 — 叙述类,可编辑
|
|
8813
|
+
能力 ≈261px 技能 + 红线 — 清单类,可增减
|
|
8814
|
+
Overview is deliberately read-only: a user opening settings wants
|
|
8815
|
+
to know what the bot IS before touching anything. */
|
|
8816
|
+
const groups = {
|
|
8817
|
+
overview: [idcardEl, qualSec, radarSec],
|
|
8818
|
+
definition: [identitySec, howSec, prefsSec],
|
|
8819
|
+
capability: [skillsSec, redSec],
|
|
8820
|
+
}
|
|
8821
|
+
const body = groups[paneTab] || groups.overview
|
|
8822
|
+
return body.concat([
|
|
8823
|
+
h('div', { className: 'bot-hint', key: 'hint-saving' }, saving),
|
|
8824
|
+
])
|
|
7079
8825
|
}
|
|
7080
8826
|
|
|
7081
8827
|
/** History pane: sessions of this bot workspace only. */
|
|
@@ -7116,7 +8862,8 @@ window.__ModuleLoader__.load({
|
|
|
7116
8862
|
setDrawer(drawer === 'config' ? '' : 'config')
|
|
7117
8863
|
},
|
|
7118
8864
|
},
|
|
7119
|
-
h('span', { className: 'bot-avatar-t' },
|
|
8865
|
+
h('span', { className: 'bot-avatar-t' },
|
|
8866
|
+
(meta.short && meta.short.en) || (meta.id || 'B').charAt(0).toUpperCase()),
|
|
7120
8867
|
h('i', { className: 'bot-avatar-state ' + tone, 'aria-hidden': 'true' })),
|
|
7121
8868
|
h('div', { className: 'bot-meta' },
|
|
7122
8869
|
h('div', { className: 'bot-name' },
|
|
@@ -7143,8 +8890,13 @@ window.__ModuleLoader__.load({
|
|
|
7143
8890
|
'aria-label': t('ai.history'),
|
|
7144
8891
|
onClick: () => setDrawer(drawer === 'history' ? '' : 'history'),
|
|
7145
8892
|
}, h(Icon, { name: 'list' })))),
|
|
7146
|
-
h('div', { className: 'bot-body' },
|
|
7147
|
-
|
|
8893
|
+
h('div', { className: 'bot-body' + (blankChat ? ' is-blank' : '') },
|
|
8894
|
+
/* The official surface is ALWAYS mounted: its composer is the
|
|
8895
|
+
only input, so hiding it in a blank session left the bot page
|
|
8896
|
+
with no way to type. The welcome block overlays the empty
|
|
8897
|
+
message area instead of replacing the surface. */
|
|
8898
|
+
h(BotChatSurface, { botId, kit: props.kit }),
|
|
8899
|
+
blankChat
|
|
7148
8900
|
? h('div', { className: 'bot-welcome' },
|
|
7149
8901
|
h('span', { className: 'orb orb-md bot-orb' }),
|
|
7150
8902
|
h('b', null, L(meta.name || {})),
|
|
@@ -7157,23 +8909,41 @@ window.__ModuleLoader__.load({
|
|
|
7157
8909
|
className: 'bot-chip',
|
|
7158
8910
|
onClick: () => askAbout(name),
|
|
7159
8911
|
}, name))))
|
|
7160
|
-
: null,
|
|
7161
|
-
h(BotChatSurface, { botId, kit: props.kit }))),
|
|
8912
|
+
: null)),
|
|
7162
8913
|
h('aside', { className: 'bot-drawer config' + (drawer === 'config' ? ' on' : '') },
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
8914
|
+
drawerHead(L({ zh: 'Bot 配置', en: 'Bot settings' }),
|
|
8915
|
+
L(meta.name || {}) + ' · ' + (meta.version || ''), () => setDrawer('')),
|
|
8916
|
+
identityTabs(),
|
|
8917
|
+
/* key={paneTab} remounts the scroll box on every switch, so a
|
|
8918
|
+
group always opens at its top instead of inheriting the
|
|
8919
|
+
previous group's scroll offset. */
|
|
8920
|
+
h('div', { className: 'bot-dr-bd', key: 'bd-' + paneTab }, identityPane()),
|
|
8921
|
+
/* Write-back conflict strip: the disk copy changed under us, so
|
|
8922
|
+
the pending write is parked until the user chooses. */
|
|
8923
|
+
conflict
|
|
8924
|
+
? h('div', { className: 'bot-conflict', role: 'alert' },
|
|
8925
|
+
h('div', { className: 'tx' }, L({
|
|
8926
|
+
zh: 'AGENTS.md 在磁盘上已被修改,保存会覆盖它。',
|
|
8927
|
+
en: 'AGENTS.md changed on disk; saving would overwrite it.',
|
|
8928
|
+
})),
|
|
8929
|
+
h('div', { className: 'acts' },
|
|
8930
|
+
h('button', {
|
|
8931
|
+
className: 'p',
|
|
8932
|
+
type: 'button',
|
|
8933
|
+
onClick: conflictOverwrite,
|
|
8934
|
+
}, L({ zh: '用面板内容覆盖', en: 'Overwrite with mine' })),
|
|
8935
|
+
h('button', {
|
|
8936
|
+
className: 'g',
|
|
8937
|
+
type: 'button',
|
|
8938
|
+
onClick: conflictReload,
|
|
8939
|
+
}, L({ zh: '改为加载磁盘版本', en: 'Reload from disk' }))))
|
|
8940
|
+
: null,
|
|
7168
8941
|
h('div', { className: 'bot-dr-ft' },
|
|
7169
8942
|
h('span', null, L({ zh: '写入 Bot 工作区', en: 'Saved to the bot workspace' })),
|
|
7170
8943
|
h('button', { className: 'bot-link', type: 'button', onClick: () => setDrawer('') }, L({ zh: '关闭', en: 'Close' })))),
|
|
7171
8944
|
h('aside', { className: 'bot-drawer history' + (drawer === 'history' ? ' on' : '') },
|
|
7172
|
-
|
|
7173
|
-
h('
|
|
7174
|
-
h('span', { className: 'bot-dr-sub' }, L(meta.name || {})),
|
|
7175
|
-
h('button', { className: 'bot-x', type: 'button', onClick: () => setDrawer('') }, h(Icon, { name: 'close' }))),
|
|
7176
|
-
h('div', { className: 'bot-dr-bd' }, historyPane()))),
|
|
8945
|
+
drawerHead(L({ zh: '会话', en: 'Conversations' }), L(meta.name || {}), () => setDrawer('')),
|
|
8946
|
+
h('div', { className: 'bot-dr-bd' }, historyPane()))),
|
|
7177
8947
|
]
|
|
7178
8948
|
}
|
|
7179
8949
|
|
|
@@ -7182,7 +8952,7 @@ window.__ModuleLoader__.load({
|
|
|
7182
8952
|
return renderPane(normalizeSection(section))
|
|
7183
8953
|
}
|
|
7184
8954
|
|
|
7185
|
-
return { SectionShell, BotShell, PageKpi, renderSection }
|
|
8955
|
+
return { SectionShell, BotShell, BaseShell, TeamShell, PageKpi, renderSection }
|
|
7186
8956
|
}
|
|
7187
8957
|
/* =====================================================================
|
|
7188
8958
|
* SHELL — 面板根板块:ErpPanel(路由 + 布局)+ ErpNavIcon(左栏图标)。
|
|
@@ -7191,7 +8961,7 @@ window.__ModuleLoader__.load({
|
|
|
7191
8961
|
* ===================================================================== */
|
|
7192
8962
|
|
|
7193
8963
|
function createShell(deps) {
|
|
7194
|
-
const { React, h, readLang, subscribeLang, useHostTheme, Rail, CardView, KpiCard, TodoPage, readHashPage, writeHashPage, SectionShell, BotShell, MetricDetailModal, PAGES, ICONS } = deps
|
|
8964
|
+
const { React, h, readLang, subscribeLang, useHostTheme, Rail, CardView, KpiCard, TodoPage, readHashPage, writeHashPage, SectionShell, BotShell, BaseShell, TeamShell, MetricDetailModal, PAGES, ICONS } = deps
|
|
7195
8965
|
|
|
7196
8966
|
|
|
7197
8967
|
|
|
@@ -7221,36 +8991,51 @@ window.__ModuleLoader__.load({
|
|
|
7221
8991
|
const cfg = PAGES[page] || PAGES.overview
|
|
7222
8992
|
const revision = activeLang + ':' + theme
|
|
7223
8993
|
const [openMetric, setOpenMetric] = React.useState(null)
|
|
7224
|
-
const body =
|
|
7225
|
-
? (
|
|
7226
|
-
|
|
7227
|
-
|
|
7228
|
-
|
|
7229
|
-
?
|
|
7230
|
-
|
|
7231
|
-
h(
|
|
7232
|
-
|
|
7233
|
-
h(
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
8994
|
+
const body = page === 'team'
|
|
8995
|
+
? h(TeamShell, { key: page, kit: props })
|
|
8996
|
+
: page === 'base'
|
|
8997
|
+
? h(BaseShell, { key: page, cfg, onOpen: setOpenMetric })
|
|
8998
|
+
: cfg.sections
|
|
8999
|
+
? (cfg.botId
|
|
9000
|
+
? h(BotShell, { key: page, cfg, page, onOpen: setOpenMetric, kit: props })
|
|
9001
|
+
: h(SectionShell, { key: page, cfg, pageId: page, onOpen: setOpenMetric }))
|
|
9002
|
+
: cfg.todo
|
|
9003
|
+
? h(TodoPage)
|
|
9004
|
+
: [
|
|
9005
|
+
h('div', { className: 't1-stats', key: 'kpis' },
|
|
9006
|
+
cfg.kpis.map((card, i) => h(KpiCard, { key: i, card, onOpen: setOpenMetric }))),
|
|
9007
|
+
h('div', { className: 't1-row2', key: 'core' },
|
|
9008
|
+
cfg.core.map((card, i) => h(CardView, { key: i, card, theme, revision, onNav: go }))),
|
|
9009
|
+
h('div', { className: 't1-row3', key: 'sub' },
|
|
9010
|
+
cfg.sub.map((card, i) => h(CardView, { key: i, card, theme, revision }))),
|
|
9011
|
+
]
|
|
7238
9012
|
|
|
7239
9013
|
return h('div', { className: 'erp-app', 'data-theme': theme },
|
|
7240
9014
|
h('div', { className: 'erp-board' },
|
|
7241
9015
|
h(Rail, { lang: activeLang, page, onNav: go }),
|
|
7242
9016
|
h('div', {
|
|
7243
9017
|
className: 't1-main',
|
|
7244
|
-
'data-shell': cfg.sections ? 'true' : 'false',
|
|
9018
|
+
'data-shell': page === 'team' ? 'team' : (page === 'base' || cfg.sections) ? 'true' : 'false',
|
|
7245
9019
|
}, body)),
|
|
7246
9020
|
h(MetricDetailModal, { metric: openMetric, onClose: () => setOpenMetric(null), onGo: go }))
|
|
7247
9021
|
}
|
|
7248
9022
|
|
|
7249
9023
|
|
|
7250
9024
|
/**
|
|
7251
|
-
* Left-nav row glyph: the erpBot mark
|
|
7252
|
-
*
|
|
7253
|
-
*
|
|
9025
|
+
* Left-nav row glyph: the erpBot mark.
|
|
9026
|
+
*
|
|
9027
|
+
* Two layers only — a gradient disc and a thin bright rim. The earlier
|
|
9028
|
+
* version stacked four (ring + cut-out seam + face + letter B), which is
|
|
9029
|
+
* more information than the 18px box the host allocates can carry: at
|
|
9030
|
+
* `sidebar.panellist` size the seam collapsed and the letter smeared, so
|
|
9031
|
+
* the mark read as a small muddy blob next to the host's crisp line icons.
|
|
9032
|
+
* Dropping to two layers means every pixel carries the brand colour
|
|
9033
|
+
* instead of fighting three other edges.
|
|
9034
|
+
*
|
|
9035
|
+
* It stays a filled disc rather than a line icon on purpose: the sidebar
|
|
9036
|
+
* lists "插件 / 更多" as thin outlines, and a solid disc is what marks this
|
|
9037
|
+
* row as an app rather than another menu entry. The rim is what keeps that
|
|
9038
|
+
* disc from looking like a flat dot.
|
|
7254
9039
|
* @param props - `{size, active}`.
|
|
7255
9040
|
* @returns the inline `<svg>`.
|
|
7256
9041
|
*/
|
|
@@ -7264,21 +9049,30 @@ window.__ModuleLoader__.load({
|
|
|
7264
9049
|
style: { display: 'block', flex: 'none' },
|
|
7265
9050
|
},
|
|
7266
9051
|
h('defs', { key: 'd' },
|
|
7267
|
-
h('linearGradient', {
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
h('stop', { key: '
|
|
7271
|
-
|
|
7272
|
-
|
|
9052
|
+
h('linearGradient', {
|
|
9053
|
+
id: 'erp-nav-face', x1: 3, y1: 21, x2: 21, y2: 3, gradientUnits: 'userSpaceOnUse',
|
|
9054
|
+
},
|
|
9055
|
+
h('stop', { key: 'f0', stopColor: '#3a2bb4' }),
|
|
9056
|
+
h('stop', { key: 'f1', offset: '.52', stopColor: '#6d4cf5' }),
|
|
9057
|
+
h('stop', { key: 'f2', offset: '1', stopColor: '#a98bfb' }))),
|
|
9058
|
+
h('circle', { key: 'face', cx: 12, cy: 12, r: 11, fill: 'url(#erp-nav-face)' }),
|
|
9059
|
+
/* Rim sits on the disc's own edge so the bounding box stays 24/24;
|
|
9060
|
+
.62 alpha is the point where it still reads against the disc's
|
|
9061
|
+
light end (#a98bfb) instead of dissolving into it. */
|
|
9062
|
+
h('circle', {
|
|
9063
|
+
key: 'rim', cx: 12, cy: 12, r: 11, fill: 'none',
|
|
9064
|
+
stroke: 'rgba(255,255,255,.62)', strokeWidth: 1.2,
|
|
9065
|
+
}),
|
|
9066
|
+
/* One glyph, no seam, no mask: a "B" spelled as two counters would
|
|
9067
|
+
smear at 18px, so the mark is a single thick stem plus a bowl —
|
|
9068
|
+
the smallest shape that still reads as an initial rather than as a
|
|
9069
|
+
status dot. */
|
|
9070
|
+
h('path', {
|
|
7273
9071
|
key: 'b',
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
7278
|
-
fontSize: 13,
|
|
7279
|
-
fontWeight: 800,
|
|
7280
|
-
fill: '#fff',
|
|
7281
|
-
}, 'B'))
|
|
9072
|
+
d: 'M9.6 7.1v9.8M9.6 7.1h2.9a2.5 2.5 0 0 1 0 5H9.6M9.6 12.1h3.3a2.45 2.45 0 0 1 0 4.8H9.6',
|
|
9073
|
+
fill: 'none', stroke: '#fff', strokeWidth: 1.85,
|
|
9074
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
9075
|
+
}))
|
|
7282
9076
|
}
|
|
7283
9077
|
|
|
7284
9078
|
|
|
@@ -7287,26 +9081,26 @@ window.__ModuleLoader__.load({
|
|
|
7287
9081
|
/* =====================================================================
|
|
7288
9082
|
* ENTRY — 唯一接线点。按拓扑序实例化 7 个板块工厂,把符号解构到 factory
|
|
7289
9083
|
* 作用域。bot-data 片段在此之前已声明 BOT_IDS / BOTS / PLATFORM_SKILLS /
|
|
7290
|
-
* SKILL_IDS,由 deps 显式注入到需要的工厂。
|
|
9084
|
+
* SKILL_IDS / SKILL_DEPS,由 deps 显式注入到需要的工厂。
|
|
7291
9085
|
* ===================================================================== */
|
|
7292
9086
|
|
|
7293
9087
|
const i18n = createI18n()
|
|
7294
9088
|
const chart = createChart({ SVG_NS, t: i18n.t })
|
|
7295
9089
|
|
|
7296
9090
|
const assistant = createAssistant({ React, h, t: i18n.t, readLang: i18n.readLang, chatBots: BOT_IDS })
|
|
7297
|
-
const cards = createCards({ React, h, t: i18n.t, metricLabel, metricSub, PAGES, Icon, Spark, drawTrend: chart.drawTrend })
|
|
7298
|
-
const dashboard = createDashboard({ React, h, t: i18n.t, L: i18n.L, readLang: i18n.readLang, setLang: i18n.setLang, LANGS: i18n.LANGS, SHORT_LOCALE: i18n.SHORT_LOCALE, LANG_BADGE: i18n.LANG_BADGE, useChatState: assistant.useChatState, chatTone: assistant.chatTone, drawOverview: chart.drawOverview, CARDS, CARD_AUTO_MS, DONUT_ARCS, EXPENSE_LEGEND, TRANSACTIONS, nextUid, NAV, Icon, Spark, TxTile })
|
|
7299
|
-
const workbench = createWorkbench({ React, h, t: i18n.t, L: i18n.L, readLang: i18n.readLang, KpiFigure, compactMoney, metricLabel, metricSub, moneyAmount, Icon, Spark, PAGES, BOTS, useChatState: assistant.useChatState, chatTone: assistant.chatTone, ensureChat: assistant.ensureChat, refreshChat: assistant.refreshChat, switchChatSession: assistant.switchChatSession, newChatSession: assistant.newChatSession, prefillChat: assistant.prefillChat, BotChatSurface: assistant.BotChatSurface })
|
|
7300
|
-
const shell = createShell({ React, h, readLang: i18n.readLang, subscribeLang: i18n.subscribeLang, useHostTheme, Rail: dashboard.Rail, CardView: cards.CardView, KpiCard: cards.KpiCard, TodoPage: cards.TodoPage, readHashPage: cards.readHashPage, writeHashPage: cards.writeHashPage, SectionShell: workbench.SectionShell, BotShell: workbench.BotShell, MetricDetailModal, PAGES, ICONS })
|
|
9091
|
+
const cards = createCards({ React, h, t: i18n.t, readLang: i18n.readLang, compactMoney, metricLabel, metricSub, PAGES, Icon, Spark, drawTrend: chart.drawTrend })
|
|
9092
|
+
const dashboard = createDashboard({ React, h, t: i18n.t, L: i18n.L, readLang: i18n.readLang, setLang: i18n.setLang, LANGS: i18n.LANGS, SHORT_LOCALE: i18n.SHORT_LOCALE, LANG_BADGE: i18n.LANG_BADGE, useChatState: assistant.useChatState, useChatBusy: assistant.useChatBusy, chatTone: assistant.chatTone, drawOverview: chart.drawOverview, CARDS, CARD_AUTO_MS, DONUT_ARCS, EXPENSE_LEGEND, TRANSACTIONS, nextUid, NAV, Icon, Spark, TxTile })
|
|
9093
|
+
const workbench = createWorkbench({ React, h, t: i18n.t, L: i18n.L, readLang: i18n.readLang, KpiFigure, compactMoney, metricLabel, metricSub, moneyAmount, Icon, Spark, PAGES, BOTS, SKILL_DEPS, useChatState: assistant.useChatState, chatTone: assistant.chatTone, ensureChat: assistant.ensureChat, ensureTeamChat: assistant.ensureTeamChat, useTeamChat: assistant.useTeamChat, refreshChat: assistant.refreshChat, refreshTeamChat: assistant.refreshTeamChat, switchChatSession: assistant.switchChatSession, newChatSession: assistant.newChatSession, newTeamSession: assistant.newTeamSession, switchTeamSession: assistant.switchTeamSession, prefillChat: assistant.prefillChat, prefillTeamChat: assistant.prefillTeamChat, BotChatSurface: assistant.BotChatSurface, BotTeamSurface: assistant.BotTeamSurface })
|
|
9094
|
+
const shell = createShell({ React, h, readLang: i18n.readLang, subscribeLang: i18n.subscribeLang, useHostTheme, Rail: dashboard.Rail, CardView: cards.CardView, KpiCard: cards.KpiCard, TodoPage: cards.TodoPage, readHashPage: cards.readHashPage, writeHashPage: cards.writeHashPage, SectionShell: workbench.SectionShell, BotShell: workbench.BotShell, BaseShell: workbench.BaseShell, TeamShell: workbench.TeamShell, MetricDetailModal, PAGES, ICONS })
|
|
7301
9095
|
|
|
7302
9096
|
// 摊回 factory 作用域(mount.js 与横切小片仍按顶层符号名引用)
|
|
7303
9097
|
const { ZH, EN, t, L, setLang, subscribeLang, readLang } = i18n
|
|
7304
9098
|
const { mk, smooth, INCOME, EXPENSE, HILITE, drawTrend, drawOverview } = chart
|
|
7305
9099
|
const { ErpPanel, ErpNavIcon } = shell
|
|
7306
|
-
const { ensureChat, refreshChat, primeChats, setAssistantCtx, useChatState, chatTone, BotChatSurface, newChatSession, switchChatSession, prefillChat } = assistant
|
|
9100
|
+
const { ensureChat, refreshChat, refreshTeamChat, primeChats, setAssistantCtx, setTeamRemote, useChatState, chatTone, BotChatSurface, newChatSession, switchChatSession, prefillChat } = assistant
|
|
7307
9101
|
const { KpiCard, CardView, TodoPage, readHashPage, writeHashPage } = cards
|
|
7308
9102
|
const { TopBar, Rail } = dashboard
|
|
7309
|
-
const { SectionShell, BotShell } = workbench
|
|
9103
|
+
const { SectionShell, BotShell, BaseShell, TeamShell } = workbench
|
|
7310
9104
|
/* =====================================================================
|
|
7311
9105
|
* MOUNT
|
|
7312
9106
|
* ===================================================================== */
|
|
@@ -7371,7 +9165,13 @@ window.__ModuleLoader__.load({
|
|
|
7371
9165
|
const sessions = scoped.get('sessions')
|
|
7372
9166
|
const workspaces = scoped.get('workspaces')
|
|
7373
9167
|
scoped.effect(() => {
|
|
7374
|
-
|
|
9168
|
+
/* The team page shares the same stores, so its state must ride the
|
|
9169
|
+
same refresh: without it `team.blank` kept the value it had when
|
|
9170
|
+
the session was created, and the welcome overlay never left. */
|
|
9171
|
+
const refreshAll = () => {
|
|
9172
|
+
BOT_IDS.forEach((botId) => refreshChat(botId))
|
|
9173
|
+
refreshTeamChat()
|
|
9174
|
+
}
|
|
7375
9175
|
const offSessions = sessions.list.subscribe(refreshAll)
|
|
7376
9176
|
const offWorkspaces = workspaces.list.subscribe(() => {
|
|
7377
9177
|
primeChats()
|
|
@@ -7387,10 +9187,21 @@ window.__ModuleLoader__.load({
|
|
|
7387
9187
|
ctx.inject(['uiSession'], (scoped) => {
|
|
7388
9188
|
const uiSession = scoped.get('uiSession')
|
|
7389
9189
|
scoped.effect(() => {
|
|
7390
|
-
const off = uiSession.sessionStatus.subscribe(() =>
|
|
9190
|
+
const off = uiSession.sessionStatus.subscribe(() => {
|
|
9191
|
+
BOT_IDS.forEach((botId) => refreshChat(botId))
|
|
9192
|
+
refreshTeamChat()
|
|
9193
|
+
})
|
|
7391
9194
|
return () => off()
|
|
7392
9195
|
}, 'dsh-erpbot: chat status subscription')
|
|
7393
9196
|
})
|
|
9197
|
+
/* The team page enables Team mode on its root session through the
|
|
9198
|
+
official `remote.agentTeams` namespace. erpbot does not inject the
|
|
9199
|
+
whole `remote` service, so this narrow injection hands assistant the
|
|
9200
|
+
one namespace it needs (same shape DSH's own agent-team UI uses). */
|
|
9201
|
+
ctx.inject(['remote', 'remote.agentTeams'], (scoped) => {
|
|
9202
|
+
const agentTeams = scoped.remote !== undefined ? scoped.remote.agentTeams : null
|
|
9203
|
+
if (agentTeams !== undefined && agentTeams !== null) setTeamRemote(agentTeams)
|
|
9204
|
+
})
|
|
7394
9205
|
} catch (error) {
|
|
7395
9206
|
console.error('[dsh-erpbot] chat services unavailable', error)
|
|
7396
9207
|
}
|