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/src/assistant/index.js
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
|
|
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
|
|
238
|
-
fresh: 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 = ['
|
|
6
|
+
const BOT_IDS = ['finance', 'funds', 'forecast', 'tax']
|
|
7
7
|
|
|
8
8
|
const SKILL_IDS = {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
17
|
-
id: '
|
|
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
|
-
|
|
50
|
-
id: '
|
|
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
|
-
|
|
83
|
-
id: '
|
|
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
|
-
|
|
116
|
-
id: '
|
|
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
package/src/cards/index.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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
|
}
|