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.
@@ -38,6 +38,10 @@
38
38
  chatBots.forEach((id) => { chatStates[id] = emptyState() })
39
39
  const chatSubs = new Set()
40
40
  let chatCtx = null
41
+ /** The `remote.agentTeams` client namespace, injected by mount.js so the
42
+ * team page can enable Team mode without resolving the `remote` service
43
+ * (which erpbot does not inject). */
44
+ let teamRemote = null
41
45
 
42
46
  /** Resolve one client service lazily (sessions / workspaces / uiWorkspace). */
43
47
  function assistantService(name) {
@@ -49,6 +53,11 @@
49
53
  }
50
54
  }
51
55
 
56
+ /** Wire the `remote.agentTeams` namespace for enabling Team mode. */
57
+ function setTeamRemote(agentTeams) {
58
+ teamRemote = agentTeams || null
59
+ }
60
+
52
61
  /** Notify every React subscriber with the fresh map. */
53
62
  function notifyChat() {
54
63
  chatSubs.forEach((fn) => {
@@ -88,6 +97,29 @@
88
97
  return snap || emptyState()
89
98
  }
90
99
 
100
+ /**
101
+ * Aggregate activity across every bot, as one boolean. The rail's brand
102
+ * avatar shows a single status dot, so it needs "is anything running"
103
+ * rather than four separate tones; calling useChatState per bot inside a
104
+ * loop would break the rules of hooks.
105
+ */
106
+ function readChatBusy() {
107
+ return chatBots.some((id) => {
108
+ const state = chatStates[id]
109
+ return !!(state && (state.pending || state.running))
110
+ })
111
+ }
112
+
113
+ /** React binding for the aggregate activity flag. */
114
+ function useChatBusy() {
115
+ const [busy, setBusy] = React.useState(() => readChatBusy())
116
+ React.useEffect(() => {
117
+ setBusy(readChatBusy())
118
+ return subscribeChat(() => setBusy(readChatBusy()))
119
+ }, [])
120
+ return busy
121
+ }
122
+
91
123
  /** Status tone used by the header dot. */
92
124
  function chatTone(state) {
93
125
  if (!state) return ''
@@ -98,10 +130,23 @@
98
130
  return ''
99
131
  }
100
132
 
101
- /** Read the persisted session id of one bot. */
133
+ /** Bot ids used before the pinyin → English rename, for storage migration. */
134
+ const LEGACY_BOT_IDS = { finance: 'cai', funds: 'zi', forecast: 'tui', tax: 'shui' }
135
+
136
+ /** Read the persisted session id of one bot, migrating a pre-rename key once. */
102
137
  function readSessionId(botId) {
103
138
  try {
104
- return window.localStorage.getItem(SESSION_PREFIX + botId) || null
139
+ const current = window.localStorage.getItem(SESSION_PREFIX + botId)
140
+ if (current) return current
141
+ /* The rename changed the key suffix; carry the legacy id over so an
142
+ existing conversation is not silently replaced by a fresh one. */
143
+ const legacy = LEGACY_BOT_IDS[botId]
144
+ if (!legacy) return null
145
+ const old = window.localStorage.getItem(SESSION_PREFIX + legacy)
146
+ if (!old) return null
147
+ window.localStorage.setItem(SESSION_PREFIX + botId, old)
148
+ window.localStorage.removeItem(SESSION_PREFIX + legacy)
149
+ return old
105
150
  } catch (error) {
106
151
  return null
107
152
  }
@@ -219,6 +264,55 @@
219
264
  return rows.slice(0, LIST_LIMIT)
220
265
  }
221
266
 
267
+ /**
268
+ * The resident Session instance's own blank bit — the earliest truthful
269
+ * signal that a conversation left its blank state.
270
+ *
271
+ * The list snapshot only flips `blank` once the `sessionListMetadata`
272
+ * projection folds the first `turn/start`, which trails the accepted
273
+ * submission by a tick. A retained Session flips its own `blankBit`
274
+ * synchronously inside `acceptPrompt`, so reading the instance closes
275
+ * that gap: the welcome block disappears the moment the composer accepts
276
+ * the message instead of when the projection catches up.
277
+ * @param sessionId - Session to inspect.
278
+ * @returns the blank bit, or undefined when no resident instance is held.
279
+ */
280
+ function residentBlank(sessionId) {
281
+ if (sessionId === null) return undefined
282
+ try {
283
+ const sessions = assistantService('sessions')
284
+ if (sessions === null || typeof sessions.binding !== 'function') return undefined
285
+ const binding = sessions.binding(sessionId)
286
+ if (binding === undefined || binding === null) return undefined
287
+ const session = binding.session
288
+ if (session === undefined || session === null || typeof session.getSnapshot !== 'function') return undefined
289
+ const snapshot = session.getSnapshot()
290
+ if (snapshot === undefined || snapshot === null) return undefined
291
+ /* A running/attempted session is never blank even when the projection
292
+ has not folded its first turn/start yet. */
293
+ if (snapshot.running === true || snapshot.promptAttempted === true || snapshot.awaitingFirstTurn === true) return false
294
+ if (typeof snapshot.blank === 'boolean') return snapshot.blank
295
+ } catch (error) {
296
+ return undefined
297
+ }
298
+ return undefined
299
+ }
300
+
301
+ /**
302
+ * Blank state for one session: the resident instance first, the list row
303
+ * second, the caller's cached value last.
304
+ * @param sessionId - Session to inspect.
305
+ * @param row - matching session-list row, when the list knows the Session.
306
+ * @param fallback - value to keep when neither source is available.
307
+ * @returns the resolved blank bit.
308
+ */
309
+ function resolveBlank(sessionId, row, fallback) {
310
+ const resident = residentBlank(sessionId)
311
+ if (resident !== undefined) return resident
312
+ if (row !== undefined) return row.blank === true
313
+ return fallback
314
+ }
315
+
222
316
  /** Recompute one bot's session list and current status. */
223
317
  function refreshChat(botId) {
224
318
  const state = chatStates[botId]
@@ -229,13 +323,14 @@
229
323
  ? assistantService('sessions').list.getSnapshot()
230
324
  : null
231
325
  const row = catalog !== null && state.sessionId !== null ? catalog.byId[state.sessionId] : undefined
326
+ const blank = resolveBlank(state.sessionId, row, state.blank)
232
327
  patchChat(botId, {
233
328
  sessions: rows,
234
329
  running: current ? current.running : false,
235
330
  pending: current ? current.pending : false,
236
331
  unread: current ? current.unread : false,
237
- blank: row !== undefined ? row.blank === true : state.blank,
238
- fresh: state.fresh && row !== undefined && row.blank !== true ? false : state.fresh,
332
+ blank,
333
+ fresh: state.fresh && blank === false ? false : state.fresh,
239
334
  })
240
335
  }
241
336
 
@@ -349,6 +444,244 @@
349
444
  }
350
445
  }
351
446
 
447
+ /** Write a prompt into the team composer's draft (best effort). */
448
+ function prefillTeamChat(text) {
449
+ try {
450
+ if (teamState.sessionId === null || text === '') return
451
+ const sessions = assistantService('sessions')
452
+ if (sessions === null) return
453
+ const binding = sessions.binding(teamState.sessionId)
454
+ const scoped = sessions.scope(teamState.sessionId)
455
+ const conversation = scoped === undefined ? undefined : scoped.get('conversation')
456
+ if (binding === undefined || conversation === undefined) return
457
+ conversation.input.requestDraftInitialization(binding, { prompt: text, whenDraftExists: 'replace' })
458
+ } catch (error) {
459
+ console.warn('[dsh-erpbot] team prefill failed', error)
460
+ }
461
+ }
462
+
463
+ /* =====================================================================
464
+ * TEAM CHAT — the "团队模式" page embeds one root session whose Team mode
465
+ * is enabled. This is the same embedding trick as a single bot's chat
466
+ * (conversation.content), except the session is created in the shared team
467
+ * workspace and then remote.agentTeams.setEnabled turns on the board, so
468
+ * the official member tree + task board slots render inside this session.
469
+ * ===================================================================== */
470
+
471
+ /** Team chat state, independent of the four single-bot states. */
472
+ let teamState = emptyState()
473
+
474
+ /** Read the team session id, migrating nothing (team had no pinyin id). */
475
+ function readTeamSessionId() {
476
+ try {
477
+ return window.localStorage.getItem('dsh-erpbot.team.session.v1') || null
478
+ } catch (error) {
479
+ return null
480
+ }
481
+ }
482
+
483
+ /** Persist (or clear) the team session id. */
484
+ function writeTeamSessionId(id) {
485
+ try {
486
+ if (id) window.localStorage.setItem('dsh-erpbot.team.session.v1', id)
487
+ else window.localStorage.removeItem('dsh-erpbot.team.session.v1')
488
+ } catch (error) {
489
+ /* storage may be unavailable */
490
+ }
491
+ }
492
+
493
+ /** Create or reuse the shared team workspace via the host route. */
494
+ async function ensureTeamWorkspace() {
495
+ const response = await fetch('/erp/team/workspace', {
496
+ method: 'POST',
497
+ headers: { 'content-type': 'application/json' },
498
+ body: '{}',
499
+ })
500
+ const data = await response.json().catch(() => null)
501
+ if (!response.ok || data === null || data.ok !== true) {
502
+ throw new Error(data !== null && data.error ? data.error.message : 'team workspace unavailable')
503
+ }
504
+ return { workspaceId: data.workspaceId, path: data.path || '' }
505
+ }
506
+
507
+ /** Ensure the team root session exists, is retained, and has Team mode on. */
508
+ async function ensureTeamChat() {
509
+ if (teamState.phase === 'ready' && teamState.reference !== null) return teamState.reference
510
+ if (teamState.phase === 'preparing') return null
511
+ if (assistantCtxMissing()) {
512
+ teamState = Object.assign({}, teamState, { phase: 'unsupported', error: 'chat services unavailable' })
513
+ notifyTeam()
514
+ return null
515
+ }
516
+ teamState = Object.assign({}, teamState, { phase: 'preparing', error: null })
517
+ notifyTeam()
518
+ try {
519
+ const workspace = await ensureTeamWorkspace()
520
+ const sessions = assistantService('sessions')
521
+ const stored = readTeamSessionId()
522
+ let id = null
523
+ if (stored !== null) {
524
+ const catalog = sessions.list.getSnapshot()
525
+ const row = catalog.byId[stored]
526
+ if (row !== undefined && row.origin === undefined) id = stored
527
+ }
528
+ if (id === null) {
529
+ id = await sessions.create({ workspaceId: workspace.workspaceId })
530
+ writeTeamSessionId(id)
531
+ }
532
+ /* Retain FIRST: activating the session makes it a live root Agent,
533
+ which `remote.agentTeams.setEnabled` requires (its host half calls
534
+ `subagents.rootOf(agent)`, which rejects a not-yet-live session). */
535
+ const reference = sessions.retain(id, { source: 'erpbot-team' })
536
+ /* Now enable Team mode on this live root session so the official
537
+ member tree + task board slots render. */
538
+ try {
539
+ if (teamRemote !== null && typeof teamRemote.setEnabled === 'function') {
540
+ const response = await teamRemote.setEnabled(id, true)
541
+ if (response && response.ok === false) {
542
+ console.warn('[dsh-erpbot] team mode enable rejected:', response.error)
543
+ }
544
+ } else {
545
+ console.warn('[dsh-erpbot] team remote unavailable, team mode not enabled')
546
+ }
547
+ } catch (error) {
548
+ console.warn('[dsh-erpbot] team mode enable failed', error)
549
+ }
550
+ teamState = Object.assign({}, teamState, {
551
+ phase: 'ready',
552
+ workspaceId: workspace.workspaceId,
553
+ workspacePath: workspace.path,
554
+ sessionId: id,
555
+ reference,
556
+ error: null,
557
+ })
558
+ notifyTeam()
559
+ return reference
560
+ } catch (error) {
561
+ teamState = Object.assign({}, teamState, { phase: 'error', error: String((error && error.message) || error) })
562
+ notifyTeam()
563
+ return null
564
+ }
565
+ }
566
+
567
+ /** Notify every React subscriber of the team state change. */
568
+ function notifyTeam() {
569
+ notifyChat()
570
+ }
571
+
572
+ /** React binding for the team chat state. */
573
+ function useTeamChat() {
574
+ const [snap, setSnap] = React.useState(teamState)
575
+ React.useEffect(() => {
576
+ setSnap(teamState)
577
+ const off = subscribeChat(() => setSnap(teamState))
578
+ return () => off()
579
+ }, [])
580
+ return snap || emptyState()
581
+ }
582
+
583
+ /** Rows of the team workspace's sessions (current blank included). */
584
+ function teamSessionRows() {
585
+ const workspaces = assistantService('workspaces')
586
+ const sessions = assistantService('sessions')
587
+ if (workspaces === null || sessions === null || teamState.workspaceId === null) return []
588
+ const catalog = sessions.list.getSnapshot()
589
+ const workspace = (workspaces.list.getSnapshot().items || [])
590
+ .find((item) => item.workspaceId === teamState.workspaceId)
591
+ if (workspace === undefined) return []
592
+ const uiSession = assistantService('uiSession')
593
+ const statuses = uiSession !== null ? uiSession.sessionStatus.getSnapshot() : null
594
+ const rows = []
595
+ for (const id of workspace.sessionIds) {
596
+ const row = catalog.byId[id]
597
+ if (row === undefined) continue
598
+ if (row.blank === true && id !== teamState.sessionId) continue
599
+ const status = statuses !== null && statuses !== undefined ? statuses.get(id) : undefined
600
+ rows.push({
601
+ id,
602
+ title: row.title || row.displayTitle || t('ai.blankChat'),
603
+ at: row.updatedAt || 0,
604
+ blank: row.blank === true,
605
+ running: status !== undefined ? status.running === true : row.running === true,
606
+ pending: status !== undefined && status.pendingInteraction !== undefined && status.pendingInteraction !== null,
607
+ unread: status !== undefined && status.completionUnread === true,
608
+ })
609
+ }
610
+ rows.sort((a, b) => b.at - a.at)
611
+ return rows.slice(0, LIST_LIMIT)
612
+ }
613
+
614
+ /** Refresh the team session's status, sessions list + current session. */
615
+ function refreshTeamChat() {
616
+ if (assistantCtxMissing() || teamState.workspaceId === null) return
617
+ const sessions = assistantService('sessions')
618
+ const rows = teamSessionRows()
619
+ const current = rows.find((row) => row.id === teamState.sessionId)
620
+ const catalog = sessions !== null ? sessions.list.getSnapshot() : null
621
+ const row = catalog !== null && teamState.sessionId !== null ? catalog.byId[teamState.sessionId] : undefined
622
+ const blank = resolveBlank(teamState.sessionId, row, teamState.blank)
623
+ teamState = Object.assign({}, teamState, {
624
+ sessions: rows,
625
+ running: current ? current.running : false,
626
+ pending: current ? current.pending : false,
627
+ unread: current ? current.unread : false,
628
+ blank,
629
+ fresh: teamState.fresh && blank === false ? false : teamState.fresh,
630
+ })
631
+ notifyTeam()
632
+ }
633
+
634
+ /** Switch the team page to another session of its workspace. */
635
+ function switchTeamSession(id) {
636
+ const sessions = assistantService('sessions')
637
+ if (sessions === null) return
638
+ try {
639
+ const reference = sessions.retain(id, { source: 'erpbot-team' })
640
+ const previous = teamState.reference
641
+ teamState = Object.assign({}, teamState, { sessionId: id, reference, error: null, fresh: false })
642
+ writeTeamSessionId(id)
643
+ if (previous !== null && previous !== reference) {
644
+ setTimeout(() => { try { previous.release() } catch (error) { /* released */ } }, 0)
645
+ }
646
+ refreshTeamChat()
647
+ } catch (error) {
648
+ teamState = Object.assign({}, teamState, { error: String((error && error.message) || error) })
649
+ notifyTeam()
650
+ }
651
+ }
652
+
653
+ /** Create a fresh team root session, enable Team mode, switch to it. */
654
+ async function newTeamSession() {
655
+ if (teamState.phase !== 'ready') return
656
+ const sessions = assistantService('sessions')
657
+ if (sessions === null) return
658
+ try {
659
+ const id = await sessions.create({ workspaceId: teamState.workspaceId })
660
+ /* Retain first so the session is a live root, then enable Team mode. */
661
+ const previous = teamState.reference
662
+ const reference = sessions.retain(id, { source: 'erpbot-team' })
663
+ try {
664
+ if (teamRemote !== null && typeof teamRemote.setEnabled === 'function') {
665
+ const response = await teamRemote.setEnabled(id, true)
666
+ if (response && response.ok === false) {
667
+ console.warn('[dsh-erpbot] team mode enable rejected:', response.error)
668
+ }
669
+ }
670
+ } catch (error) {
671
+ console.warn('[dsh-erpbot] team mode enable failed', error)
672
+ }
673
+ teamState = Object.assign({}, teamState, { sessionId: id, reference, error: null, blank: true, fresh: true })
674
+ writeTeamSessionId(id)
675
+ if (previous !== null && previous !== reference) {
676
+ setTimeout(() => { try { previous.release() } catch (error) { /* released */ } }, 0)
677
+ }
678
+ refreshTeamChat()
679
+ } catch (error) {
680
+ teamState = Object.assign({}, teamState, { error: String((error && error.message) || error) })
681
+ notifyTeam()
682
+ }
683
+ }
684
+
352
685
  /** Open / close one bot's session list. */
353
686
  function toggleChatList(botId) {
354
687
  const state = chatStates[botId] || emptyState()
@@ -440,18 +773,70 @@
440
773
  chatCtx = ctx
441
774
  }
442
775
 
776
+ /**
777
+ * The team root session's embedded conversation. Identical to
778
+ * BotChatSurface but bound to the team state (a shared workspace session
779
+ * with Team mode on), so the official member tree + task board render.
780
+ * @param props - `{ kit }`.
781
+ * @returns the embedded conversation element.
782
+ */
783
+ function BotTeamSurface(props) {
784
+ const kit = props.kit || {}
785
+ const state = useTeamChat()
786
+
787
+ if (state.phase === 'idle' || state.phase === 'preparing') {
788
+ return h('div', { className: 'assist-note bot-note' }, t('ai.preparing'))
789
+ }
790
+ if (state.phase === 'error') {
791
+ return h('div', { className: 'assist-note bot-note' },
792
+ h('span', null, t('ai.error') + ' - ' + String(state.error || '')),
793
+ h('button', {
794
+ className: 'assist-link',
795
+ type: 'button',
796
+ onClick: () => {
797
+ teamState = Object.assign({}, teamState, { phase: 'idle', error: null })
798
+ notifyTeam()
799
+ ensureTeamChat()
800
+ },
801
+ }, t('ai.retry')))
802
+ }
803
+ if (state.phase === 'unsupported' || state.reference === null
804
+ || typeof kit.SessionProvider !== 'function' || typeof kit.renderFactorySlot !== 'function') {
805
+ return h('div', { className: 'assist-note bot-note' },
806
+ h('span', null, t('ai.fallback')))
807
+ }
808
+
809
+ return h('div', { className: 'bot-conversation team-conversation', 'data-bot': 'team' },
810
+ h(kit.SessionProvider, { session: state.reference },
811
+ kit.renderFactorySlot('conversation.content', {
812
+ variant: 'embedded',
813
+ phase: 'active',
814
+ hero: false,
815
+ }, BOT_FACTORY_OPTIONS)))
816
+ }
817
+
443
818
  return {
444
819
  chatTone,
445
820
  useChatState,
821
+ useChatBusy,
822
+ readChatBusy,
446
823
  ensureChat,
824
+ ensureTeamChat,
825
+ useTeamChat,
447
826
  refreshChat,
827
+ refreshTeamChat,
448
828
  switchChatSession,
449
829
  newChatSession,
830
+ newTeamSession,
831
+ switchTeamSession,
450
832
  prefillChat,
833
+ prefillTeamChat,
451
834
  toggleChatList,
452
835
  primeChats,
453
836
  setAssistantCtx,
837
+ setTeamRemote,
454
838
  BotChatSurface,
839
+ BotTeamSurface,
455
840
  assistantTime,
456
841
  chatSessions: (botId) => (chatStates[botId] || emptyState()).sessions,
457
842
  chatState: (botId) => chatStates[botId] || emptyState(),
package/src/bot-data.js CHANGED
@@ -3,18 +3,18 @@
3
3
  * 纯数据片段:无函数、无 h(、无 React;由 workbench / entry 通过 deps 注入。
4
4
  * ===================================================================== */
5
5
 
6
- const BOT_IDS = ['cai', 'zi', 'tui', 'shui']
6
+ const BOT_IDS = ['finance', 'funds', 'forecast', 'tax']
7
7
 
8
8
  const SKILL_IDS = {
9
- cai: ['vouchers', 'ledgers', 'ar-aging', 'payables', 'fixed-assets', 'close', 'statements', 'workpapers'],
10
- zi: ['accounts', 'payments', 'recon', 'plan', 'restricted', 'bills'],
11
- tui: ['budget', 'forecast', 'scenario', 'assumption', 'allocation', 'mgmt-report', 'variance', 'cash-plan'],
12
- shui: ['output-invoice', 'input-verify', 'invoice-check', 'file', 'pay', 'burden'],
9
+ finance: ['vouchers', 'ledgers', 'ar-aging', 'payables', 'fixed-assets', 'close', 'statements', 'workpapers'],
10
+ funds: ['accounts', 'payments', 'recon', 'plan', 'restricted', 'bills'],
11
+ forecast: ['budget', 'forecast', 'scenario', 'assumption', 'allocation', 'mgmt-report', 'variance', 'cash-plan'],
12
+ tax: ['output-invoice', 'input-verify', 'invoice-check', 'file', 'pay', 'burden'],
13
13
  }
14
14
 
15
15
  const BOTS = {
16
- cai: {
17
- id: 'cai',
16
+ finance: {
17
+ id: 'finance',
18
18
  name: { zh: '账房', en: 'The Ledger' },
19
19
  short: { zh: '账', en: 'L' },
20
20
  role: { zh: '把账记对、把账关好、把证据留全', en: 'Keep the books correct, close them cleanly, keep the evidence complete' },
@@ -46,8 +46,8 @@
46
46
  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.',
47
47
  },
48
48
  },
49
- zi: {
50
- id: 'zi',
49
+ funds: {
50
+ id: 'funds',
51
51
  name: { zh: '司库', en: 'The Treasurer' },
52
52
  short: { zh: '司', en: 'T' },
53
53
  role: { zh: '守住可支付能力与安全垫', en: 'Guard the payment capacity and the safety buffer' },
@@ -79,8 +79,8 @@
79
79
  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.',
80
80
  },
81
81
  },
82
- tui: {
83
- id: 'tui',
82
+ forecast: {
83
+ id: 'forecast',
84
84
  name: { zh: '军师', en: 'The Planner' },
85
85
  short: { zh: '军', en: 'P' },
86
86
  role: { zh: '把不确定性与假设讲清楚', en: 'Make uncertainty and assumptions explicit' },
@@ -112,8 +112,8 @@
112
112
  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.',
113
113
  },
114
114
  },
115
- shui: {
116
- id: 'shui',
115
+ tax: {
116
+ id: 'tax',
117
117
  name: { zh: '税官', en: 'The Tax Officer' },
118
118
  short: { zh: '税', en: 'X' },
119
119
  role: { zh: '不错报、不漏报、红线可见', en: 'No wrong filing, no missed filing, red lines visible' },
@@ -146,3 +146,15 @@
146
146
  },
147
147
  },
148
148
  }
149
+
150
+ /** Skill dependency declarations: skill name → prerequisite skill names.
151
+ * When a user adds a skill, the UI warns if any prerequisite is missing.
152
+ * These are only the cross-skill deps that matter at binding time; the
153
+ * skill's own SKILL.md is the authority for runtime requirements. */
154
+ const SKILL_DEPS = {
155
+ /* DSH standard skills that need MCP channels or other skills. */
156
+ 'intelligent-recon': ['bank-mcp-channel'],
157
+ 'invoice-check': ['tax-system-channel'],
158
+ 'tax-filing-assistant': ['tax-system-channel'],
159
+ 'bank-rpa': ['bank-mcp-channel'],
160
+ }
package/src/bundle.json CHANGED
@@ -147,6 +147,7 @@
147
147
  "BOTS",
148
148
  "PLATFORM_SKILLS",
149
149
  "SKILL_IDS",
150
+ "SKILL_DEPS",
150
151
  "CARDS",
151
152
  "CARD_AUTO_MS",
152
153
  "DONUT_ARCS",
@@ -5,11 +5,16 @@
5
5
  * ===================================================================== */
6
6
 
7
7
  function createCards(deps) {
8
- const { React, h, t, metricLabel, metricSub, PAGES, Icon, Spark, drawTrend } = deps
8
+ const { React, h, t, compactMoney, readLang, metricLabel, metricSub, PAGES, Icon, Spark, drawTrend } = deps
9
9
 
10
10
  /* Legacy deep links: the data and platform pages were merged into one
11
- base page, so `#/erp/shu` and `#/erp/guan` now land on it. */
12
- const HASH_ALIAS = { shu: 'base', guan: 'base' }
11
+ base page, so `#/erp/shu` and `#/erp/guan` now land on it. The four bot
12
+ pages were renamed from pinyin ids to English ones, so the old hashes
13
+ still resolve instead of falling back to the overview. */
14
+ const HASH_ALIAS = {
15
+ shu: 'base', guan: 'base',
16
+ cai: 'finance', zi: 'funds', tui: 'forecast', shui: 'tax',
17
+ }
13
18
 
14
19
  /** Read the page id out of `#/erp/<id>`; unknown ids fall back to overview. */
15
20
  function readHashPage() {
@@ -305,6 +310,12 @@
305
310
  /**
306
311
  * Tiny chart drawn on the block card: bars for 账, a line for 资, a ring for
307
312
  * 推. White with opacities so it reads on every card tint.
313
+ *
314
+ * The centre figure is formatted for the ring, not printed in full. The
315
+ * ring's hole is only about half its width, so "¥12,000,000.00" at full
316
+ * length was wider than the hole it sits in and spilled across both arcs.
317
+ * compactMoney scales it to "¥1,200万"; the exact figure still appears in
318
+ * the card's own cells row and in the detail modal, so nothing is lost.
308
319
  */
309
320
  function MiniChart(props) {
310
321
  const chart = props.chart || {}
@@ -330,6 +341,9 @@
330
341
  h('i', { style: { background: 'rgba(255,255,255,' + slice.tone + ')' } }),
331
342
  h('span', null, slice.key ? t(slice.key) : ''),
332
343
  h('b', null, slice.v + '%')))
344
+ const centre = chart.total
345
+ ? (compactMoney ? compactMoney(chart.total, readLang ? readLang() : 'zh') : chart.total)
346
+ : ''
333
347
  return h('div', { className: 'visa-chart donut' },
334
348
  h('div', { className: 'mini-donut' },
335
349
  h('svg', { viewBox: '0 0 44 44', 'aria-hidden': 'true' },
@@ -340,7 +354,7 @@
340
354
  }),
341
355
  arcs)),
342
356
  h('div', { className: 'mini-donut-center' },
343
- h('b', null, chart.total || ''),
357
+ h('b', null, centre),
344
358
  chart.totalLabelKey ? h('span', null, t(chart.totalLabelKey)) : null)),
345
359
  h('ul', { className: 'mini-legend' }, legend))
346
360
  }